feat: initial Dialectic.OpenclawPlugin — agent tools for Dialectic v2
Phase 3 of DIALECTIC-V2. Six tools wired to the Go backend running on
server.t3:
- dialectic_list_topics GET /api/topics
- dialectic_topic_detail GET /api/topics/{id}
- dialectic_propose_topic POST /api/topics
- dialectic_signup POST /api/topics/{id}/signups (HF pre-check)
- dialectic_post_argument POST /api/topics/{id}/arguments
- dialectic_view_verdict GET /api/topics/{id}/verdict
All tools return MCP {content:[{type:text,text}]} shape. Errors caught
and surfaced as text payload so agents see actionable failure messages.
Per-agent API key resolved via secret-mgr key dialectic-agent-apikey
(cached in memory; AGENT_VERIFY env required). HF on_call coverage
check degrades gracefully to skipped if HarborForge.OpenclawPlugin
does not yet expose hasOnCallCovering() — backend stores pre_validated
flag as audit signal.
openclaw.plugin.json declares both contracts.tools (the 6 names) AND
activation.onStartup:true per loader gotchas memory; missing either
silently drops the plugin.
Plain export default {id, name, register} entry shape matching
prism-facet. No openclaw SDK imports (jiti runtime resolution of
openclaw was flaky in HF-style entries; structurally simpler avoids
the lookup entirely).
Defaults backendUrl to https://dialectic-api.hangman-lab.top; override
via openclaw.json plugins.entries.dialectic.config.backendUrl for sim.
Phase 3 deferred items (in README): agent key provisioning workflow,
HF window-coverage accessor, SSE subscription tool, token-cost
reporting.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/node_modules/
|
||||
/.idea/
|
||||
/.vscode/
|
||||
*.swp
|
||||
.DS_Store
|
||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Dialectic.OpenclawPlugin
|
||||
|
||||
OpenClaw plugin that gives agents tools to participate in Dialectic v2
|
||||
debates. Six tools, one per Dialectic backend endpoint they need:
|
||||
|
||||
| Tool | Backend call | Notes |
|
||||
|------|--------------|-------|
|
||||
| `dialectic_list_topics` | `GET /api/topics` | filters: status/visibility/limit/offset |
|
||||
| `dialectic_topic_detail` | `GET /api/topics/{id}` | full topic incl. camps + verdict |
|
||||
| `dialectic_propose_topic` | `POST /api/topics` | title + summary + 4 lifecycle timestamps |
|
||||
| `dialectic_signup` | `POST /api/topics/{id}/signups` | with HF on_call coverage pre-check |
|
||||
| `dialectic_post_argument` | `POST /api/topics/{id}/arguments` | during `debating` only |
|
||||
| `dialectic_view_verdict` | `GET /api/topics/{id}/verdict` | 404 until judge submits |
|
||||
|
||||
## Setup
|
||||
|
||||
Each agent needs a Dialectic API key, stored in their `secret-mgr`
|
||||
under key `dialectic-agent-apikey`. Provisioning is currently manual
|
||||
(see Phase 3 deferred items below). The plugin caches the key in memory
|
||||
after first read; AGENT_VERIFY env must be set so secret-mgr authorizes
|
||||
the read.
|
||||
|
||||
## Config
|
||||
|
||||
`openclaw.json`:
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"dialectic": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"backendUrl": "https://dialectic-api.hangman-lab.top"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Default backend URL: `https://dialectic-api.hangman-lab.top`. Override
|
||||
for sim/dev by pointing at the local backend instance.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
plugin/
|
||||
├── openclaw.plugin.json contracts.tools + activation.onStartup
|
||||
├── package.json type=module, main=index.js
|
||||
├── index.ts/.js entry: registers tools
|
||||
└── src/
|
||||
├── backend-client.ts/.js HTTP client, agent api key resolver
|
||||
├── hf-precheck.ts/.js on_call coverage check for signup
|
||||
└── tools.ts/.js 6 tool registrations
|
||||
```
|
||||
|
||||
## Phase 3 deferred items (for later sessions)
|
||||
|
||||
- **Agent key provisioning workflow** — currently zero agents have
|
||||
`dialectic-agent-apikey` in their secret-mgr. Until that's wired
|
||||
into the `recruitment` skill (or a separate `provision-dialectic-key`
|
||||
workflow), every `dialectic_*` tool call from an agent will fail with
|
||||
"dialectic api key not provisioned". Manual SQL provisioning
|
||||
documented in `Dialectic.Backend/README.md`.
|
||||
- **HF on_call coverage check** — `hfOnCallCoverageCheck` currently
|
||||
degrades to "skipped" because `HarborForge.OpenclawPlugin`'s
|
||||
cross-plugin `__hfAgentStatus` only exposes CURRENT status, not
|
||||
window coverage. Until HF adds `hasOnCallCovering(agentId, from, to)`,
|
||||
signup pre-validation is audit-only (the plugin sends
|
||||
`pre_validated: false` and the backend stores that as the agent's
|
||||
honest signal that no validation happened).
|
||||
- **SSE subscriptions** — agents currently poll via `dialectic_topic_detail`
|
||||
to see status changes / new arguments. Once Dialectic.Backend ships
|
||||
Phase 2D.5 SSE, add a `dialectic_subscribe` tool that streams events
|
||||
for one topic until cancelled.
|
||||
- **Token-cost reporting** — `dialectic_post_argument` and the judge
|
||||
submission could attach `tokens_input/output` counts so the backend
|
||||
records cost per debate. Wait until the backend has budget gating
|
||||
(Phase N) before bothering.
|
||||
|
||||
## See also
|
||||
|
||||
- Top-level design: `/home/hzhang/arch/DIALECTIC-V2-DESIGN.md`
|
||||
- Backend: `Dialectic.Backend` (Go, prod on server.t3)
|
||||
- Loader gotchas: `[[reference-meridian-plugin-contract]]` memory
|
||||
47
package-lock.json
generated
Normal file
47
package-lock.json
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "dialectic-openclaw-plugin",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dialectic-openclaw-plugin",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "dialectic-openclaw-plugin",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.plugin.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
26
plugin/index.js
Normal file
26
plugin/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Dialectic — OpenClaw plugin entry.
|
||||
*
|
||||
* Tools: dialectic_list_topics, dialectic_topic_detail,
|
||||
* dialectic_propose_topic, dialectic_signup,
|
||||
* dialectic_post_argument, dialectic_view_verdict
|
||||
*
|
||||
* Loader gotchas (per [[reference-meridian-plugin-contract]]):
|
||||
* - openclaw.plugin.json MUST declare `activation.onStartup: true`
|
||||
* or this plugin is silently treated as lazy/on-demand and
|
||||
* register() is never called.
|
||||
* - Every tool name MUST appear in contracts.tools or the tool
|
||||
* never surfaces to agents.
|
||||
* - Plain `export default { id, name, register }` works in jiti
|
||||
* (matches prism-facet); don't pull SDK helpers without
|
||||
* `openclaw` actually being resolvable at runtime.
|
||||
*/
|
||||
import { registerDialecticTools } from './src/tools.js';
|
||||
export default {
|
||||
id: 'dialectic',
|
||||
name: 'Dialectic',
|
||||
register(api) {
|
||||
registerDialecticTools(api);
|
||||
api.logger.info('[dialectic] plugin registered');
|
||||
},
|
||||
};
|
||||
40
plugin/index.ts
Normal file
40
plugin/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Dialectic — OpenClaw plugin entry.
|
||||
*
|
||||
* Tools: dialectic_list_topics, dialectic_topic_detail,
|
||||
* dialectic_propose_topic, dialectic_signup,
|
||||
* dialectic_post_argument, dialectic_view_verdict
|
||||
*
|
||||
* Loader gotchas (per [[reference-meridian-plugin-contract]]):
|
||||
* - openclaw.plugin.json MUST declare `activation.onStartup: true`
|
||||
* or this plugin is silently treated as lazy/on-demand and
|
||||
* register() is never called.
|
||||
* - Every tool name MUST appear in contracts.tools or the tool
|
||||
* never surfaces to agents.
|
||||
* - Plain `export default { id, name, register }` works in jiti
|
||||
* (matches prism-facet); don't pull SDK helpers without
|
||||
* `openclaw` actually being resolvable at runtime.
|
||||
*/
|
||||
|
||||
import { registerDialecticTools } from './src/tools.js';
|
||||
|
||||
interface PluginApi {
|
||||
logger: {
|
||||
info(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
error(msg: string): void;
|
||||
};
|
||||
on(hook: string, handler: (...args: any[]) => any): void;
|
||||
registerTool(def: any): void;
|
||||
config?: { backendUrl?: string };
|
||||
}
|
||||
|
||||
export default {
|
||||
id: 'dialectic',
|
||||
name: 'Dialectic',
|
||||
|
||||
register(api: PluginApi) {
|
||||
registerDialecticTools(api);
|
||||
api.logger.info('[dialectic] plugin registered');
|
||||
},
|
||||
};
|
||||
31
plugin/openclaw.plugin.json
Normal file
31
plugin/openclaw.plugin.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "dialectic",
|
||||
"name": "Dialectic",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenClaw plugin for the Dialectic v2 debate platform — agent-facing tools",
|
||||
"main": "index.js",
|
||||
"activation": {
|
||||
"onStartup": true
|
||||
},
|
||||
"contracts": {
|
||||
"tools": [
|
||||
"dialectic_list_topics",
|
||||
"dialectic_topic_detail",
|
||||
"dialectic_propose_topic",
|
||||
"dialectic_signup",
|
||||
"dialectic_post_argument",
|
||||
"dialectic_view_verdict"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"backendUrl": {
|
||||
"type": "string",
|
||||
"description": "Dialectic backend base URL (e.g. https://dialectic-api.hangman-lab.top)",
|
||||
"default": "https://dialectic-api.hangman-lab.top"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
plugin/package.json
Normal file
6
plugin/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "dialectic-openclaw-plugin",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "index.js"
|
||||
}
|
||||
90
plugin/src/backend-client.js
Normal file
90
plugin/src/backend-client.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* HTTP client for Dialectic backend (Go service).
|
||||
*
|
||||
* Auth: per-agent api key via `Authorization: Bearer <raw>`. Key is
|
||||
* read from secret-mgr under the agent's `dialectic-agent-apikey`
|
||||
* entry (provisioned at recruitment time — see Phase 3 deferred items).
|
||||
*
|
||||
* Errors:
|
||||
* - Network/transport → throws.
|
||||
* - Backend 4xx/5xx → throws with status + body text so the tool
|
||||
* execute() can surface a useful error to the agent.
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
export class BackendClient {
|
||||
baseUrl;
|
||||
agentId;
|
||||
timeoutMs;
|
||||
cachedApiKey = null;
|
||||
constructor(opts) {
|
||||
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
||||
this.agentId = opts.agentId;
|
||||
this.timeoutMs = opts.timeoutMs ?? 15000;
|
||||
}
|
||||
/**
|
||||
* Read the agent's dialectic api key from secret-mgr. Cached in
|
||||
* memory after first read so successive tool calls don't fork
|
||||
* secret-mgr repeatedly. AGENT_VERIFY is required for secret-mgr
|
||||
* to authorize the read.
|
||||
*/
|
||||
resolveApiKey() {
|
||||
if (this.cachedApiKey)
|
||||
return this.cachedApiKey;
|
||||
try {
|
||||
const out = execSync('secret-mgr get dialectic-agent-apikey', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
if (!out) {
|
||||
throw new Error('empty');
|
||||
}
|
||||
this.cachedApiKey = out;
|
||||
return out;
|
||||
}
|
||||
catch {
|
||||
throw new Error('dialectic api key not provisioned (secret-mgr key `dialectic-agent-apikey` missing). ' +
|
||||
'Phase 3 deferred: ask hangman / admin to mint one via the recruitment flow.');
|
||||
}
|
||||
}
|
||||
async get(path) {
|
||||
return this.request('GET', path);
|
||||
}
|
||||
async post(path, body) {
|
||||
return this.request('POST', path, body);
|
||||
}
|
||||
async put(path, body) {
|
||||
return this.request('PUT', path, body);
|
||||
}
|
||||
async request(method, path, body) {
|
||||
const url = `${this.baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'authorization': `Bearer ${this.resolveApiKey()}`,
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
throw new Error(`dialectic ${method} ${path}: ${res.status} ${text.slice(0, 500)}`);
|
||||
}
|
||||
if (!text)
|
||||
return null;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
plugin/src/backend-client.ts
Normal file
99
plugin/src/backend-client.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* HTTP client for Dialectic backend (Go service).
|
||||
*
|
||||
* Auth: per-agent api key via `Authorization: Bearer <raw>`. Key is
|
||||
* read from secret-mgr under the agent's `dialectic-agent-apikey`
|
||||
* entry (provisioned at recruitment time — see Phase 3 deferred items).
|
||||
*
|
||||
* Errors:
|
||||
* - Network/transport → throws.
|
||||
* - Backend 4xx/5xx → throws with status + body text so the tool
|
||||
* execute() can surface a useful error to the agent.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
export interface BackendClientOptions {
|
||||
baseUrl: string; // e.g. https://dialectic-api.hangman-lab.top
|
||||
agentId: string; // the calling agent's id
|
||||
timeoutMs?: number; // default 15000
|
||||
}
|
||||
|
||||
export class BackendClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly agentId: string;
|
||||
private readonly timeoutMs: number;
|
||||
private cachedApiKey: string | null = null;
|
||||
|
||||
constructor(opts: BackendClientOptions) {
|
||||
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
||||
this.agentId = opts.agentId;
|
||||
this.timeoutMs = opts.timeoutMs ?? 15000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the agent's dialectic api key from secret-mgr. Cached in
|
||||
* memory after first read so successive tool calls don't fork
|
||||
* secret-mgr repeatedly. AGENT_VERIFY is required for secret-mgr
|
||||
* to authorize the read.
|
||||
*/
|
||||
private resolveApiKey(): string {
|
||||
if (this.cachedApiKey) return this.cachedApiKey;
|
||||
try {
|
||||
const out = execSync('secret-mgr get dialectic-agent-apikey', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
if (!out) {
|
||||
throw new Error('empty');
|
||||
}
|
||||
this.cachedApiKey = out;
|
||||
return out;
|
||||
} catch {
|
||||
throw new Error(
|
||||
'dialectic api key not provisioned (secret-mgr key `dialectic-agent-apikey` missing). ' +
|
||||
'Phase 3 deferred: ask hangman / admin to mint one via the recruitment flow.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async get(path: string): Promise<unknown> {
|
||||
return this.request('GET', path);
|
||||
}
|
||||
async post(path: string, body: unknown): Promise<unknown> {
|
||||
return this.request('POST', path, body);
|
||||
}
|
||||
async put(path: string, body: unknown): Promise<unknown> {
|
||||
return this.request('PUT', path, body);
|
||||
}
|
||||
|
||||
private async request(method: string, path: string, body?: unknown): Promise<unknown> {
|
||||
const url = `${this.baseUrl}${path.startsWith('/') ? path : '/' + path}`;
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'authorization': `Bearer ${this.resolveApiKey()}`,
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
throw new Error(`dialectic ${method} ${path}: ${res.status} ${text.slice(0, 500)}`);
|
||||
}
|
||||
if (!text) return null;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
plugin/src/hf-precheck.js
Normal file
50
plugin/src/hf-precheck.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* HF calendar pre-check for `dialectic_signup`.
|
||||
*
|
||||
* Per the design (section 8): before sending a signup to the backend,
|
||||
* the plugin must verify the agent has an `on_call` slot that covers
|
||||
* the entire debate window [debate_start_at, debate_end_at].
|
||||
*
|
||||
* Data source: the HarborForge OpenclawPlugin already exposes
|
||||
* `globalThis.__hfAgentStatus.get(agentId)` (Phase 1) which returns
|
||||
* the agent's CURRENT status — but that's not enough; we need slot
|
||||
* coverage over a FUTURE window.
|
||||
*
|
||||
* For Phase 3 v1: degrade gracefully — if HF can't be queried for
|
||||
* future slots (no exposed accessor yet), require the caller to
|
||||
* pass `pre_validated: false`. Backend stores the flag as audit; a
|
||||
* follow-up Phase can tighten this.
|
||||
*
|
||||
* Phase 3 deferred:
|
||||
* - HarborForge.OpenclawPlugin needs a new global accessor for
|
||||
* "does agent X have an on_call slot covering [from, to]?".
|
||||
* Until that lands, this function returns { ok: true, source: "skipped" }
|
||||
* and trusts the agent's plugin invocation.
|
||||
*/
|
||||
export async function hfOnCallCoverageCheck(agentId, debateStartAt, debateEndAt) {
|
||||
const _G = globalThis;
|
||||
const hf = _G['__hfAgentStatus'];
|
||||
// Phase 3 deferred: HF doesn't yet expose a window-coverage check.
|
||||
// If it does (future), use it; otherwise skip and trust.
|
||||
if (!hf || typeof hf.hasOnCallCovering !== 'function') {
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
try {
|
||||
const covered = await hf.hasOnCallCovering(agentId, debateStartAt, debateEndAt);
|
||||
if (covered === undefined) {
|
||||
// HF queried but said "I don't know" — treat as skipped not failed
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
if (!covered) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `agent ${agentId} has no on_call slot covering [${debateStartAt} → ${debateEndAt}]`,
|
||||
source: 'hf',
|
||||
};
|
||||
}
|
||||
return { ok: true, source: 'hf' };
|
||||
}
|
||||
catch {
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
}
|
||||
64
plugin/src/hf-precheck.ts
Normal file
64
plugin/src/hf-precheck.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* HF calendar pre-check for `dialectic_signup`.
|
||||
*
|
||||
* Per the design (section 8): before sending a signup to the backend,
|
||||
* the plugin must verify the agent has an `on_call` slot that covers
|
||||
* the entire debate window [debate_start_at, debate_end_at].
|
||||
*
|
||||
* Data source: the HarborForge OpenclawPlugin already exposes
|
||||
* `globalThis.__hfAgentStatus.get(agentId)` (Phase 1) which returns
|
||||
* the agent's CURRENT status — but that's not enough; we need slot
|
||||
* coverage over a FUTURE window.
|
||||
*
|
||||
* For Phase 3 v1: degrade gracefully — if HF can't be queried for
|
||||
* future slots (no exposed accessor yet), require the caller to
|
||||
* pass `pre_validated: false`. Backend stores the flag as audit; a
|
||||
* follow-up Phase can tighten this.
|
||||
*
|
||||
* Phase 3 deferred:
|
||||
* - HarborForge.OpenclawPlugin needs a new global accessor for
|
||||
* "does agent X have an on_call slot covering [from, to]?".
|
||||
* Until that lands, this function returns { ok: true, source: "skipped" }
|
||||
* and trusts the agent's plugin invocation.
|
||||
*/
|
||||
|
||||
export interface PrecheckResult {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
source: 'hf' | 'skipped';
|
||||
}
|
||||
|
||||
export async function hfOnCallCoverageCheck(
|
||||
agentId: string,
|
||||
debateStartAt: string,
|
||||
debateEndAt: string,
|
||||
): Promise<PrecheckResult> {
|
||||
const _G = globalThis as Record<string, unknown>;
|
||||
const hf = _G['__hfAgentStatus'] as
|
||||
| { hasOnCallCovering?: (a: string, from: string, to: string) => Promise<boolean | undefined> }
|
||||
| undefined;
|
||||
|
||||
// Phase 3 deferred: HF doesn't yet expose a window-coverage check.
|
||||
// If it does (future), use it; otherwise skip and trust.
|
||||
if (!hf || typeof hf.hasOnCallCovering !== 'function') {
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
|
||||
try {
|
||||
const covered = await hf.hasOnCallCovering(agentId, debateStartAt, debateEndAt);
|
||||
if (covered === undefined) {
|
||||
// HF queried but said "I don't know" — treat as skipped not failed
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
if (!covered) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `agent ${agentId} has no on_call slot covering [${debateStartAt} → ${debateEndAt}]`,
|
||||
source: 'hf',
|
||||
};
|
||||
}
|
||||
return { ok: true, source: 'hf' };
|
||||
} catch {
|
||||
return { ok: true, source: 'skipped' };
|
||||
}
|
||||
}
|
||||
196
plugin/src/tools.js
Normal file
196
plugin/src/tools.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Tool registration for the Dialectic plugin.
|
||||
*
|
||||
* Six tools wired here, one per Dialectic backend endpoint the agent
|
||||
* needs to drive end-to-end participation in a debate:
|
||||
*
|
||||
* dialectic_list_topics GET /api/topics
|
||||
* dialectic_topic_detail GET /api/topics/{id}
|
||||
* dialectic_propose_topic POST /api/topics
|
||||
* dialectic_signup POST /api/topics/{id}/signups (with HF pre-check)
|
||||
* dialectic_post_argument POST /api/topics/{id}/arguments
|
||||
* dialectic_view_verdict GET /api/topics/{id}/verdict
|
||||
*
|
||||
* Every tool returns the standard MCP `{ content: [{ type: 'text', text }] }`
|
||||
* shape; errors are caught and returned as a text payload so the agent
|
||||
* sees something actionable rather than a runtime throw.
|
||||
*/
|
||||
import { BackendClient } from './backend-client.js';
|
||||
import { hfOnCallCoverageCheck } from './hf-precheck.js';
|
||||
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
|
||||
export function registerDialecticTools(api) {
|
||||
const backendUrl = api.config?.backendUrl ?? DEFAULT_BACKEND_URL;
|
||||
// Per-tool we pull ctx.agentId out of the factory (the
|
||||
// backend api key resolves from that agent's secret-mgr).
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_list_topics',
|
||||
description: 'List Dialectic debate topics, optionally filtered. Use status to scope to active phases ' +
|
||||
'(created | signup_open | signup_closed | debating | completed | cancelled), visibility ' +
|
||||
'(public | private), limit (default 50, max 200), offset (pagination).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
status: { type: 'string', description: 'filter by status' },
|
||||
visibility: { type: 'string', description: 'filter by visibility' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 200 },
|
||||
offset: { type: 'integer', minimum: 0 },
|
||||
},
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
const query = new URLSearchParams();
|
||||
for (const k of ['status', 'visibility', 'limit', 'offset']) {
|
||||
if (params[k] != null)
|
||||
query.set(k, String(params[k]));
|
||||
}
|
||||
const q = query.toString();
|
||||
return await client.get(`/api/topics${q ? '?' + q : ''}`);
|
||||
}),
|
||||
}));
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_topic_detail',
|
||||
description: 'Get full detail for one Dialectic topic — lifecycle timestamps, status, ' +
|
||||
'verdict_schema_id, allocated camps if past signup_close, and verdict if completed.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { topic_id: { type: 'string' } },
|
||||
required: ['topic_id'],
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}`);
|
||||
}),
|
||||
}));
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_propose_topic',
|
||||
description: 'Create a new Dialectic debate topic. Provide the title, summary, the 4 lifecycle ' +
|
||||
'timestamps (RFC3339, signup_open < signup_close <= debate_start < debate_end), and the ' +
|
||||
"verdict_schema_id ('binary' | 'claim-resolution' | 'policy-recommendation' | 'free-form'). " +
|
||||
'visibility defaults to private — flip to public via UI after creation if appropriate.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
verdict_schema_id: { type: 'string' },
|
||||
visibility: { type: 'string' },
|
||||
signup_open_at: { type: 'string' },
|
||||
signup_close_at: { type: 'string' },
|
||||
debate_start_at: { type: 'string' },
|
||||
debate_end_at: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'title', 'summary', 'verdict_schema_id',
|
||||
'signup_open_at', 'signup_close_at', 'debate_start_at', 'debate_end_at',
|
||||
],
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
const body = {
|
||||
title: params.title,
|
||||
summary: params.summary,
|
||||
verdict_schema_id: params.verdict_schema_id,
|
||||
signup_open_at: params.signup_open_at,
|
||||
signup_close_at: params.signup_close_at,
|
||||
debate_start_at: params.debate_start_at,
|
||||
debate_end_at: params.debate_end_at,
|
||||
};
|
||||
if (params.visibility)
|
||||
body.visibility = params.visibility;
|
||||
return await client.post(`/api/topics`, body);
|
||||
}),
|
||||
}));
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_signup',
|
||||
description: 'Volunteer (sign up) for one or more camps on a debate topic. ' +
|
||||
"Camps are 'pro' | 'con' | 'judge'; you may volunteer for any subset, allocation " +
|
||||
'will pick at most one. Topic must be in `signup_open` status. ' +
|
||||
'Pre-flight: the plugin attempts to verify you have an on_call slot covering the debate window ' +
|
||||
'(HF coverage check); if HF lookup is unavailable, the check is skipped and recorded as audit only.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
topic_id: { type: 'string' },
|
||||
willing_camps: {
|
||||
type: 'array',
|
||||
items: { type: 'string', enum: ['pro', 'con', 'judge'] },
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['topic_id', 'willing_camps'],
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const agentId = ctx.agentId ?? '';
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId });
|
||||
// Fetch topic detail so we know the debate window for HF pre-check.
|
||||
const topic = (await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}`));
|
||||
if (!topic || typeof topic !== 'object') {
|
||||
throw new Error('topic lookup failed (empty response)');
|
||||
}
|
||||
const debateStart = topic.debate_start_at;
|
||||
const debateEnd = topic.debate_end_at;
|
||||
if (!debateStart || !debateEnd) {
|
||||
throw new Error('topic detail missing debate window timestamps');
|
||||
}
|
||||
const precheck = await hfOnCallCoverageCheck(agentId, debateStart, debateEnd);
|
||||
if (!precheck.ok) {
|
||||
throw new Error(`HF pre-check failed: ${precheck.reason}`);
|
||||
}
|
||||
return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/signups`, {
|
||||
willing_camps: params.willing_camps,
|
||||
pre_validated: precheck.source === 'hf',
|
||||
});
|
||||
}),
|
||||
}));
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_post_argument',
|
||||
description: 'Post an argument to a Dialectic topic you are currently allocated to. Must be in `debating` ' +
|
||||
'status. Content max 32KB. The argument attaches to the latest open round and is visible to ' +
|
||||
'other camp members and observers (and publicly if the topic is public).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
topic_id: { type: 'string' },
|
||||
content: { type: 'string', maxLength: 32000 },
|
||||
},
|
||||
required: ['topic_id', 'content'],
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/arguments`, { content: params.content });
|
||||
}),
|
||||
}));
|
||||
api.registerTool((ctx) => ({
|
||||
name: 'dialectic_view_verdict',
|
||||
description: 'Fetch the structured verdict for a completed Dialectic topic. 404 if the debate is still ' +
|
||||
'in progress or the judge has not yet submitted.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { topic_id: { type: 'string' } },
|
||||
required: ['topic_id'],
|
||||
},
|
||||
execute: async (_id, params) => asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`);
|
||||
}),
|
||||
}));
|
||||
api.logger.info(`[dialectic] registered 6 tools (backend=${backendUrl})`);
|
||||
}
|
||||
/** Wraps an async backend call and converts result/error into MCP content shape. */
|
||||
async function asContent(fn) {
|
||||
try {
|
||||
const out = await fn();
|
||||
const text = typeof out === 'string' ? out : JSON.stringify(out, null, 2);
|
||||
return { content: [{ type: 'text', text }] };
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err?.message ?? String(err);
|
||||
return { content: [{ type: 'text', text: `error: ${msg}` }] };
|
||||
}
|
||||
}
|
||||
228
plugin/src/tools.ts
Normal file
228
plugin/src/tools.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Tool registration for the Dialectic plugin.
|
||||
*
|
||||
* Six tools wired here, one per Dialectic backend endpoint the agent
|
||||
* needs to drive end-to-end participation in a debate:
|
||||
*
|
||||
* dialectic_list_topics GET /api/topics
|
||||
* dialectic_topic_detail GET /api/topics/{id}
|
||||
* dialectic_propose_topic POST /api/topics
|
||||
* dialectic_signup POST /api/topics/{id}/signups (with HF pre-check)
|
||||
* dialectic_post_argument POST /api/topics/{id}/arguments
|
||||
* dialectic_view_verdict GET /api/topics/{id}/verdict
|
||||
*
|
||||
* Every tool returns the standard MCP `{ content: [{ type: 'text', text }] }`
|
||||
* shape; errors are caught and returned as a text payload so the agent
|
||||
* sees something actionable rather than a runtime throw.
|
||||
*/
|
||||
|
||||
import { BackendClient } from './backend-client.js';
|
||||
import { hfOnCallCoverageCheck } from './hf-precheck.js';
|
||||
|
||||
type ToolApi = {
|
||||
registerTool(def: any): void;
|
||||
config?: { backendUrl?: string };
|
||||
logger: { info(msg: string): void; warn(msg: string): void };
|
||||
};
|
||||
|
||||
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
|
||||
|
||||
export function registerDialecticTools(api: ToolApi): void {
|
||||
const backendUrl = api.config?.backendUrl ?? DEFAULT_BACKEND_URL;
|
||||
|
||||
// Per-tool we pull ctx.agentId out of the factory (the
|
||||
// backend api key resolves from that agent's secret-mgr).
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_list_topics',
|
||||
description:
|
||||
'List Dialectic debate topics, optionally filtered. Use status to scope to active phases ' +
|
||||
'(created | signup_open | signup_closed | debating | completed | cancelled), visibility ' +
|
||||
'(public | private), limit (default 50, max 200), offset (pagination).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
status: { type: 'string', description: 'filter by status' },
|
||||
visibility: { type: 'string', description: 'filter by visibility' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 200 },
|
||||
offset: { type: 'integer', minimum: 0 },
|
||||
},
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
const query = new URLSearchParams();
|
||||
for (const k of ['status', 'visibility', 'limit', 'offset']) {
|
||||
if (params[k] != null) query.set(k, String(params[k]));
|
||||
}
|
||||
const q = query.toString();
|
||||
return await client.get(`/api/topics${q ? '?' + q : ''}`);
|
||||
}),
|
||||
}));
|
||||
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_topic_detail',
|
||||
description:
|
||||
'Get full detail for one Dialectic topic — lifecycle timestamps, status, ' +
|
||||
'verdict_schema_id, allocated camps if past signup_close, and verdict if completed.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { topic_id: { type: 'string' } },
|
||||
required: ['topic_id'],
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}`);
|
||||
}),
|
||||
}));
|
||||
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_propose_topic',
|
||||
description:
|
||||
'Create a new Dialectic debate topic. Provide the title, summary, the 4 lifecycle ' +
|
||||
'timestamps (RFC3339, signup_open < signup_close <= debate_start < debate_end), and the ' +
|
||||
"verdict_schema_id ('binary' | 'claim-resolution' | 'policy-recommendation' | 'free-form'). " +
|
||||
'visibility defaults to private — flip to public via UI after creation if appropriate.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
verdict_schema_id: { type: 'string' },
|
||||
visibility: { type: 'string' },
|
||||
signup_open_at: { type: 'string' },
|
||||
signup_close_at: { type: 'string' },
|
||||
debate_start_at: { type: 'string' },
|
||||
debate_end_at: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'title', 'summary', 'verdict_schema_id',
|
||||
'signup_open_at', 'signup_close_at', 'debate_start_at', 'debate_end_at',
|
||||
],
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
const body: Record<string, any> = {
|
||||
title: params.title,
|
||||
summary: params.summary,
|
||||
verdict_schema_id: params.verdict_schema_id,
|
||||
signup_open_at: params.signup_open_at,
|
||||
signup_close_at: params.signup_close_at,
|
||||
debate_start_at: params.debate_start_at,
|
||||
debate_end_at: params.debate_end_at,
|
||||
};
|
||||
if (params.visibility) body.visibility = params.visibility;
|
||||
return await client.post(`/api/topics`, body);
|
||||
}),
|
||||
}));
|
||||
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_signup',
|
||||
description:
|
||||
'Volunteer (sign up) for one or more camps on a debate topic. ' +
|
||||
"Camps are 'pro' | 'con' | 'judge'; you may volunteer for any subset, allocation " +
|
||||
'will pick at most one. Topic must be in `signup_open` status. ' +
|
||||
'Pre-flight: the plugin attempts to verify you have an on_call slot covering the debate window ' +
|
||||
'(HF coverage check); if HF lookup is unavailable, the check is skipped and recorded as audit only.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
topic_id: { type: 'string' },
|
||||
willing_camps: {
|
||||
type: 'array',
|
||||
items: { type: 'string', enum: ['pro', 'con', 'judge'] },
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['topic_id', 'willing_camps'],
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const agentId = ctx.agentId ?? '';
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId });
|
||||
|
||||
// Fetch topic detail so we know the debate window for HF pre-check.
|
||||
const topic = (await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}`)) as any;
|
||||
if (!topic || typeof topic !== 'object') {
|
||||
throw new Error('topic lookup failed (empty response)');
|
||||
}
|
||||
const debateStart = topic.debate_start_at;
|
||||
const debateEnd = topic.debate_end_at;
|
||||
if (!debateStart || !debateEnd) {
|
||||
throw new Error('topic detail missing debate window timestamps');
|
||||
}
|
||||
|
||||
const precheck = await hfOnCallCoverageCheck(agentId, debateStart, debateEnd);
|
||||
if (!precheck.ok) {
|
||||
throw new Error(`HF pre-check failed: ${precheck.reason}`);
|
||||
}
|
||||
|
||||
return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/signups`, {
|
||||
willing_camps: params.willing_camps,
|
||||
pre_validated: precheck.source === 'hf',
|
||||
});
|
||||
}),
|
||||
}));
|
||||
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_post_argument',
|
||||
description:
|
||||
'Post an argument to a Dialectic topic you are currently allocated to. Must be in `debating` ' +
|
||||
'status. Content max 32KB. The argument attaches to the latest open round and is visible to ' +
|
||||
'other camp members and observers (and publicly if the topic is public).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
topic_id: { type: 'string' },
|
||||
content: { type: 'string', maxLength: 32000 },
|
||||
},
|
||||
required: ['topic_id', 'content'],
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.post(
|
||||
`/api/topics/${encodeURIComponent(params.topic_id)}/arguments`,
|
||||
{ content: params.content },
|
||||
);
|
||||
}),
|
||||
}));
|
||||
|
||||
api.registerTool((ctx: { agentId?: string }) => ({
|
||||
name: 'dialectic_view_verdict',
|
||||
description:
|
||||
'Fetch the structured verdict for a completed Dialectic topic. 404 if the debate is still ' +
|
||||
'in progress or the judge has not yet submitted.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { topic_id: { type: 'string' } },
|
||||
required: ['topic_id'],
|
||||
},
|
||||
execute: async (_id: string, params: Record<string, any>) =>
|
||||
asContent(async () => {
|
||||
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
|
||||
return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`);
|
||||
}),
|
||||
}));
|
||||
|
||||
api.logger.info(`[dialectic] registered 6 tools (backend=${backendUrl})`);
|
||||
}
|
||||
|
||||
/** Wraps an async backend call and converts result/error into MCP content shape. */
|
||||
async function asContent(fn: () => Promise<unknown>) {
|
||||
try {
|
||||
const out = await fn();
|
||||
const text = typeof out === 'string' ? out : JSON.stringify(out, null, 2);
|
||||
return { content: [{ type: 'text', text }] };
|
||||
} catch (err) {
|
||||
const msg = (err as { message?: string } | undefined)?.message ?? String(err);
|
||||
return { content: [{ type: 'text', text: `error: ${msg}` }] };
|
||||
}
|
||||
}
|
||||
17
tsconfig.plugin.json
Normal file
17
tsconfig.plugin.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "plugin",
|
||||
"rootDir": "plugin",
|
||||
"declaration": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["plugin/**/*.ts"],
|
||||
"exclude": ["plugin/**/*.js"]
|
||||
}
|
||||
Reference in New Issue
Block a user