@ken-jo/agent-connector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +348 -0
- package/dist/amp-QSFH2V7Y.js +217 -0
- package/dist/amp-QSFH2V7Y.js.map +1 -0
- package/dist/amp-TULUPXXL.js +237 -0
- package/dist/amp-TULUPXXL.js.map +1 -0
- package/dist/antigravity-CX2YLSY7.js +17 -0
- package/dist/antigravity-CX2YLSY7.js.map +1 -0
- package/dist/antigravity-STHZWMP3.js +171 -0
- package/dist/antigravity-STHZWMP3.js.map +1 -0
- package/dist/antigravity-cli-P6HADOAO.js +121 -0
- package/dist/antigravity-cli-P6HADOAO.js.map +1 -0
- package/dist/antigravity-cli-YQDFVV4U.js +86 -0
- package/dist/antigravity-cli-YQDFVV4U.js.map +1 -0
- package/dist/chunk-54EMKTTX.js +191 -0
- package/dist/chunk-54EMKTTX.js.map +1 -0
- package/dist/chunk-7L6ITC4T.js +54 -0
- package/dist/chunk-7L6ITC4T.js.map +1 -0
- package/dist/chunk-7MSDGHJW.js +316 -0
- package/dist/chunk-7MSDGHJW.js.map +1 -0
- package/dist/chunk-BCHWFTV4.js +196 -0
- package/dist/chunk-BCHWFTV4.js.map +1 -0
- package/dist/chunk-DBQMX75U.js +36 -0
- package/dist/chunk-DBQMX75U.js.map +1 -0
- package/dist/chunk-EXLS7BSI.js +29 -0
- package/dist/chunk-EXLS7BSI.js.map +1 -0
- package/dist/chunk-JNM3TPLX.js +593 -0
- package/dist/chunk-JNM3TPLX.js.map +1 -0
- package/dist/chunk-JWNT5KW7.js +197 -0
- package/dist/chunk-JWNT5KW7.js.map +1 -0
- package/dist/chunk-KHO5WNTP.js +30 -0
- package/dist/chunk-KHO5WNTP.js.map +1 -0
- package/dist/chunk-LTT56GI3.js +155 -0
- package/dist/chunk-LTT56GI3.js.map +1 -0
- package/dist/chunk-MEH7GC7R.js +111 -0
- package/dist/chunk-MEH7GC7R.js.map +1 -0
- package/dist/chunk-MQPNU2CY.js +64 -0
- package/dist/chunk-MQPNU2CY.js.map +1 -0
- package/dist/chunk-OPXAP3AC.js +12 -0
- package/dist/chunk-OPXAP3AC.js.map +1 -0
- package/dist/chunk-RK6VBLXP.js +50 -0
- package/dist/chunk-RK6VBLXP.js.map +1 -0
- package/dist/chunk-TIIP3RG2.js +420 -0
- package/dist/chunk-TIIP3RG2.js.map +1 -0
- package/dist/chunk-TRKWUNO4.js +702 -0
- package/dist/chunk-TRKWUNO4.js.map +1 -0
- package/dist/chunk-TRQKHP7D.js +93 -0
- package/dist/chunk-TRQKHP7D.js.map +1 -0
- package/dist/chunk-WCETQWWK.js +59 -0
- package/dist/chunk-WCETQWWK.js.map +1 -0
- package/dist/chunk-WV63X7IK.js +357 -0
- package/dist/chunk-WV63X7IK.js.map +1 -0
- package/dist/chunk-XD7MY6HB.js +343 -0
- package/dist/chunk-XD7MY6HB.js.map +1 -0
- package/dist/chunk-XNAM7F35.js +276 -0
- package/dist/chunk-XNAM7F35.js.map +1 -0
- package/dist/chunk-XUCWLJY5.js +238 -0
- package/dist/chunk-XUCWLJY5.js.map +1 -0
- package/dist/chunk-YNZU6PAV.js +800 -0
- package/dist/chunk-YNZU6PAV.js.map +1 -0
- package/dist/chunk-YT43MUSJ.js +146 -0
- package/dist/chunk-YT43MUSJ.js.map +1 -0
- package/dist/claude-code-HKDV7T32.js +637 -0
- package/dist/claude-code-HKDV7T32.js.map +1 -0
- package/dist/claude-code-W74MNTAC.js +652 -0
- package/dist/claude-code-W74MNTAC.js.map +1 -0
- package/dist/cli/sdk.d.ts +54 -0
- package/dist/cli/sdk.js +82 -0
- package/dist/cli/sdk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/codebuff-3PMZJJHP.js +307 -0
- package/dist/codebuff-3PMZJJHP.js.map +1 -0
- package/dist/codebuff-QX57FJDN.js +220 -0
- package/dist/codebuff-QX57FJDN.js.map +1 -0
- package/dist/codex-JFQTS6LU.js +593 -0
- package/dist/codex-JFQTS6LU.js.map +1 -0
- package/dist/codex-Q3ATRRX3.js +364 -0
- package/dist/codex-Q3ATRRX3.js.map +1 -0
- package/dist/copilot-cli-MTCGHZ2T.js +622 -0
- package/dist/copilot-cli-MTCGHZ2T.js.map +1 -0
- package/dist/copilot-cli-SJGIXVSD.js +369 -0
- package/dist/copilot-cli-SJGIXVSD.js.map +1 -0
- package/dist/crush-I7PK3FGI.js +360 -0
- package/dist/crush-I7PK3FGI.js.map +1 -0
- package/dist/crush-LN5QFFR2.js +183 -0
- package/dist/crush-LN5QFFR2.js.map +1 -0
- package/dist/cursor-4TMXKJOZ.js +603 -0
- package/dist/cursor-4TMXKJOZ.js.map +1 -0
- package/dist/cursor-QZ6MTO74.js +194 -0
- package/dist/cursor-QZ6MTO74.js.map +1 -0
- package/dist/detect-WIAGVYS4.js +68 -0
- package/dist/detect-WIAGVYS4.js.map +1 -0
- package/dist/doctor-EZW2K4NI.js +372 -0
- package/dist/doctor-EZW2K4NI.js.map +1 -0
- package/dist/droid-KN6VRMMT.js +424 -0
- package/dist/droid-KN6VRMMT.js.map +1 -0
- package/dist/droid-W5WF5XEX.js +224 -0
- package/dist/droid-W5WF5XEX.js.map +1 -0
- package/dist/gemini-cli-CIRW37TU.js +369 -0
- package/dist/gemini-cli-CIRW37TU.js.map +1 -0
- package/dist/gemini-cli-Y35FFK4M.js +705 -0
- package/dist/gemini-cli-Y35FFK4M.js.map +1 -0
- package/dist/goose-A3M5GODH.js +212 -0
- package/dist/goose-A3M5GODH.js.map +1 -0
- package/dist/goose-ZTGTSUFJ.js +480 -0
- package/dist/goose-ZTGTSUFJ.js.map +1 -0
- package/dist/hermes-H2KV44GK.js +470 -0
- package/dist/hermes-H2KV44GK.js.map +1 -0
- package/dist/hermes-ZYLMOFLZ.js +196 -0
- package/dist/hermes-ZYLMOFLZ.js.map +1 -0
- package/dist/hook-YVDILAMZ.js +75 -0
- package/dist/hook-YVDILAMZ.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/install-HQRYZOQ2.js +60 -0
- package/dist/install-HQRYZOQ2.js.map +1 -0
- package/dist/jetbrains-copilot-HWA3DMH7.js +633 -0
- package/dist/jetbrains-copilot-HWA3DMH7.js.map +1 -0
- package/dist/kilo-NSWAVP6P.js +235 -0
- package/dist/kilo-NSWAVP6P.js.map +1 -0
- package/dist/kilo-QXJRCSLT.js +384 -0
- package/dist/kilo-QXJRCSLT.js.map +1 -0
- package/dist/kilo-cli-EAQTHXUW.js +587 -0
- package/dist/kilo-cli-EAQTHXUW.js.map +1 -0
- package/dist/kilo-cli-EVBKUODA.js +160 -0
- package/dist/kilo-cli-EVBKUODA.js.map +1 -0
- package/dist/kimi-EI3UM3ZV.js +155 -0
- package/dist/kimi-EI3UM3ZV.js.map +1 -0
- package/dist/kimi-GQ7VXNYT.js +423 -0
- package/dist/kimi-GQ7VXNYT.js.map +1 -0
- package/dist/kiro-UCLSLBW2.js +217 -0
- package/dist/kiro-UCLSLBW2.js.map +1 -0
- package/dist/kiro-VQTT6LO2.js +451 -0
- package/dist/kiro-VQTT6LO2.js.map +1 -0
- package/dist/leaderboard-K6ON3SWW.js +137 -0
- package/dist/leaderboard-K6ON3SWW.js.map +1 -0
- package/dist/mux-T2TV4CIQ.js +211 -0
- package/dist/mux-T2TV4CIQ.js.map +1 -0
- package/dist/mux-ZMLVAXOO.js +96 -0
- package/dist/mux-ZMLVAXOO.js.map +1 -0
- package/dist/omp-PB4VDKS6.js +586 -0
- package/dist/omp-PB4VDKS6.js.map +1 -0
- package/dist/openclaw-ETN2M63O.js +751 -0
- package/dist/openclaw-ETN2M63O.js.map +1 -0
- package/dist/openclaw-IYA7ASL6.js +150 -0
- package/dist/openclaw-IYA7ASL6.js.map +1 -0
- package/dist/opencode-FZ33JKUZ.js +734 -0
- package/dist/opencode-FZ33JKUZ.js.map +1 -0
- package/dist/opencode-HWT3RSMN.js +216 -0
- package/dist/opencode-HWT3RSMN.js.map +1 -0
- package/dist/package-DLOVE2E4.js +1132 -0
- package/dist/package-DLOVE2E4.js.map +1 -0
- package/dist/pi-CIU2QHT4.js +208 -0
- package/dist/pi-CIU2QHT4.js.map +1 -0
- package/dist/pi-UFX2OYFV.js +139 -0
- package/dist/pi-UFX2OYFV.js.map +1 -0
- package/dist/qwen-code-5POCNU6P.js +573 -0
- package/dist/qwen-code-5POCNU6P.js.map +1 -0
- package/dist/qwen-code-W7NUCDEN.js +134 -0
- package/dist/qwen-code-W7NUCDEN.js.map +1 -0
- package/dist/roo-code-IOIUXRDE.js +242 -0
- package/dist/roo-code-IOIUXRDE.js.map +1 -0
- package/dist/roo-code-ZLKH4A7N.js +229 -0
- package/dist/roo-code-ZLKH4A7N.js.map +1 -0
- package/dist/runtime/index.d.ts +157 -0
- package/dist/runtime/index.js +20 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/serve-HVWWY4B6.js +65 -0
- package/dist/serve-HVWWY4B6.js.map +1 -0
- package/dist/status-FE7TECZD.js +128 -0
- package/dist/status-FE7TECZD.js.map +1 -0
- package/dist/synthetic-UKR6SMGS.js +178 -0
- package/dist/synthetic-UKR6SMGS.js.map +1 -0
- package/dist/telemetry-36GESKQQ.js +349 -0
- package/dist/telemetry-36GESKQQ.js.map +1 -0
- package/dist/trae-JSNDZFKL.js +204 -0
- package/dist/trae-JSNDZFKL.js.map +1 -0
- package/dist/trae-PA26DPBW.js +157 -0
- package/dist/trae-PA26DPBW.js.map +1 -0
- package/dist/types-CYUB1OTr.d.ts +427 -0
- package/dist/uninstall-IGCKNOSF.js +72 -0
- package/dist/uninstall-IGCKNOSF.js.map +1 -0
- package/dist/upgrade-JV2VK242.js +104 -0
- package/dist/upgrade-JV2VK242.js.map +1 -0
- package/dist/usage-6ER6EMEY.js +324 -0
- package/dist/usage-6ER6EMEY.js.map +1 -0
- package/dist/usage-event-JKBI6XCF.js +48 -0
- package/dist/usage-event-JKBI6XCF.js.map +1 -0
- package/dist/vscode-copilot-CK3NO54M.js +715 -0
- package/dist/vscode-copilot-CK3NO54M.js.map +1 -0
- package/dist/warp-JNQA7PBU.js +126 -0
- package/dist/warp-JNQA7PBU.js.map +1 -0
- package/dist/warp-QNVDXOG4.js +199 -0
- package/dist/warp-QNVDXOG4.js.map +1 -0
- package/dist/zed-NNNX5MNT.js +272 -0
- package/dist/zed-NNNX5MNT.js.map +1 -0
- package/dist/zed-VRJKWYUR.js +209 -0
- package/dist/zed-VRJKWYUR.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveEnvRefsDeep
|
|
4
|
+
} from "./chunk-DBQMX75U.js";
|
|
5
|
+
import {
|
|
6
|
+
BaseAdapter,
|
|
7
|
+
parseJsonc
|
|
8
|
+
} from "./chunk-TIIP3RG2.js";
|
|
9
|
+
import "./chunk-7L6ITC4T.js";
|
|
10
|
+
import {
|
|
11
|
+
buildServeWrapperCommand,
|
|
12
|
+
shouldWrapForTelemetry
|
|
13
|
+
} from "./chunk-MEH7GC7R.js";
|
|
14
|
+
import "./chunk-TRQKHP7D.js";
|
|
15
|
+
|
|
16
|
+
// src/adapters/openclaw/index.ts
|
|
17
|
+
import {
|
|
18
|
+
chmodSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
writeFileSync
|
|
24
|
+
} from "fs";
|
|
25
|
+
import { homedir } from "os";
|
|
26
|
+
import { dirname, join, resolve } from "path";
|
|
27
|
+
var HOST = "openclaw";
|
|
28
|
+
var MCP_ROOT_KEY = "mcp";
|
|
29
|
+
var MCP_SERVERS_KEY = "servers";
|
|
30
|
+
var PLUGINS_KEY = "plugins";
|
|
31
|
+
var PLUGINS_ENTRIES_KEY = "entries";
|
|
32
|
+
var PLUGINS_LOAD_KEY = "load";
|
|
33
|
+
var PLUGINS_LOAD_PATHS_KEY = "paths";
|
|
34
|
+
var PLUGIN_MANIFEST_FILE = "openclaw.plugin.json";
|
|
35
|
+
var EVENT_TO_OPENCLAW = {
|
|
36
|
+
PreToolUse: "before_tool_call",
|
|
37
|
+
PostToolUse: "after_tool_call",
|
|
38
|
+
SessionStart: "session_start"
|
|
39
|
+
};
|
|
40
|
+
var OpenClawAdapter = class extends BaseAdapter {
|
|
41
|
+
id = HOST;
|
|
42
|
+
name = "OpenClaw";
|
|
43
|
+
paradigm = "ts-plugin";
|
|
44
|
+
capabilities = {
|
|
45
|
+
preToolUse: true,
|
|
46
|
+
postToolUse: true,
|
|
47
|
+
// OpenClaw compaction is owned by a context engine, not wired through the
|
|
48
|
+
// universal bridge here.
|
|
49
|
+
preCompact: false,
|
|
50
|
+
sessionStart: true,
|
|
51
|
+
sessionEnd: false,
|
|
52
|
+
userPromptSubmit: false,
|
|
53
|
+
stop: false,
|
|
54
|
+
notification: false,
|
|
55
|
+
// before_tool_call mutates event.params in place → input rewrite supported.
|
|
56
|
+
canModifyArgs: true,
|
|
57
|
+
// after_tool_call observes but cannot rewrite the tool result upstream.
|
|
58
|
+
canModifyOutput: false,
|
|
59
|
+
// injected via before_prompt_build at prompt assembly.
|
|
60
|
+
canInjectSessionContext: true,
|
|
61
|
+
// OpenClaw's MCP sidecar registration is stdio-only in practice.
|
|
62
|
+
transports: ["stdio"]
|
|
63
|
+
};
|
|
64
|
+
// ── Tolerant JSON5/JSONC read (override base strict JSON.parse) ──────────
|
|
65
|
+
/**
|
|
66
|
+
* openclaw.json is officially JSON5 (comments + trailing commas allowed). A
|
|
67
|
+
* strict JSON.parse false-fails a perfectly valid commented file, so we strip
|
|
68
|
+
* comments and trailing commas before parsing. Writes remain strict JSON
|
|
69
|
+
* (writeJson in BaseAdapter), which is itself valid JSON5 — so round-trips
|
|
70
|
+
* stay safe and idempotent.
|
|
71
|
+
*/
|
|
72
|
+
readJson(path) {
|
|
73
|
+
if (!existsSync(path)) return null;
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = readFileSync(path, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return parseJsonc(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── Detection ──────────────────────────────────────────────────────────
|
|
87
|
+
detectInstalled(projectDir) {
|
|
88
|
+
const userConfig = resolveOpenClawConfigPath();
|
|
89
|
+
const userDir = join(homedir(), ".openclaw");
|
|
90
|
+
const projectConfig = join(projectDir, "openclaw.json");
|
|
91
|
+
const userInstalled = existsSync(userConfig) || existsSync(userDir);
|
|
92
|
+
const projInstalled = existsSync(projectConfig);
|
|
93
|
+
const installed = userInstalled || projInstalled;
|
|
94
|
+
const scope = projInstalled && !userInstalled ? "project" : "user";
|
|
95
|
+
const configPath = scope === "project" ? projectConfig : userConfig;
|
|
96
|
+
return {
|
|
97
|
+
id: this.id,
|
|
98
|
+
name: this.name,
|
|
99
|
+
installed,
|
|
100
|
+
paradigm: this.paradigm,
|
|
101
|
+
capabilities: this.capabilities,
|
|
102
|
+
configPath,
|
|
103
|
+
scope,
|
|
104
|
+
reason: installed ? scope === "project" ? `found project OpenClaw config at ${projectConfig}` : `found OpenClaw config at ${configPath}` : `no OpenClaw config at ${userConfig} or ${userDir}`,
|
|
105
|
+
confidence: installed ? "high" : "low"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ── Native paths ───────────────────────────────────────────────────────
|
|
109
|
+
getConfigDir(ctx) {
|
|
110
|
+
return ctx.scope === "project" ? ctx.projectDir : dirname(resolveOpenClawConfigPath());
|
|
111
|
+
}
|
|
112
|
+
getServerConfigPath(ctx) {
|
|
113
|
+
return ctx.scope === "project" ? join(ctx.projectDir, "openclaw.json") : resolveOpenClawConfigPath();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* For ts-plugin hosts the "hook config path" is the generated plugin FILE.
|
|
117
|
+
* OpenClaw additionally needs the plugins.entries reference written into
|
|
118
|
+
* openclaw.json (handled in installHooks); this is the module on disk.
|
|
119
|
+
*/
|
|
120
|
+
getHookConfigPath(ctx) {
|
|
121
|
+
return join(this.pluginDir(ctx), this.pluginFileName());
|
|
122
|
+
}
|
|
123
|
+
/** Extensions directory the synthesized plugin module is written into. */
|
|
124
|
+
pluginDir(ctx) {
|
|
125
|
+
const base = ctx.scope === "project" ? join(ctx.projectDir, ".openclaw") : dirname(resolveOpenClawConfigPath());
|
|
126
|
+
return join(base, "extensions", ctx.connector.id);
|
|
127
|
+
}
|
|
128
|
+
/** Plugin module file name (ESM so the gateway loads it without a loader). */
|
|
129
|
+
pluginFileName() {
|
|
130
|
+
return "index.mjs";
|
|
131
|
+
}
|
|
132
|
+
// ── MCP server install / uninstall (mcp.servers.<id>) ───────────────────
|
|
133
|
+
installServer(ctx) {
|
|
134
|
+
const server = this.effectiveServer(ctx);
|
|
135
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
136
|
+
if (!server) {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
platform: this.id,
|
|
140
|
+
action: "skip",
|
|
141
|
+
path: serverPath,
|
|
142
|
+
detail: ctx.connector.server ? "server registration disabled for openclaw" : "connector declares no MCP server"
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
const entry = this.renderServerEntry(ctx, server);
|
|
147
|
+
return [this.upsertNestedServer(serverPath, ctx.connector.id, entry, ctx.dryRun)];
|
|
148
|
+
}
|
|
149
|
+
uninstallServer(ctx) {
|
|
150
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
151
|
+
return [this.removeNestedServer(serverPath, ctx.connector.id, ctx.dryRun)];
|
|
152
|
+
}
|
|
153
|
+
/** Resolve the per-platform server override into an effective ServerDef. */
|
|
154
|
+
effectiveServer(ctx) {
|
|
155
|
+
const override = ctx.connector.platforms[HOST]?.server;
|
|
156
|
+
if (override === false) return void 0;
|
|
157
|
+
const base = ctx.connector.server;
|
|
158
|
+
if (!base) return void 0;
|
|
159
|
+
return override && typeof override === "object" ? { ...base, ...override } : base;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Render a normalized ServerDef into OpenClaw's native mcp.servers.<id> entry.
|
|
163
|
+
*
|
|
164
|
+
* stdio → { command, args?, env? } (NO transport key — a stdio sidecar is
|
|
165
|
+
* inferred from `command`; OpenClaw's validator rejects
|
|
166
|
+
* transport:"stdio" and only accepts "sse"/"streamable-http")
|
|
167
|
+
* remote → { url, transport: "sse"|"http", headers? }
|
|
168
|
+
*
|
|
169
|
+
* OpenClaw documents no native ${env:VAR} token, so refs resolve to literals.
|
|
170
|
+
*/
|
|
171
|
+
renderServerEntry(ctx, server) {
|
|
172
|
+
if (server.transport === "stdio") {
|
|
173
|
+
let command = server.command ?? "";
|
|
174
|
+
let args = [...server.args ?? []];
|
|
175
|
+
if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {
|
|
176
|
+
const wrapped = buildServeWrapperCommand(
|
|
177
|
+
ctx.homeBinPath,
|
|
178
|
+
ctx.connector.id,
|
|
179
|
+
command,
|
|
180
|
+
args,
|
|
181
|
+
ctx.scope,
|
|
182
|
+
this.id
|
|
183
|
+
);
|
|
184
|
+
command = wrapped.command;
|
|
185
|
+
args = wrapped.args;
|
|
186
|
+
}
|
|
187
|
+
const entry2 = {
|
|
188
|
+
command: resolveEnvRefsDeep(command)
|
|
189
|
+
};
|
|
190
|
+
const resolvedArgs = resolveEnvRefsDeep(args).filter((s) => s !== "");
|
|
191
|
+
if (resolvedArgs.length > 0) entry2.args = resolvedArgs;
|
|
192
|
+
const env = this.renderEnv(server.env);
|
|
193
|
+
if (env) entry2.env = env;
|
|
194
|
+
if (server.enabled === false) entry2.enabled = false;
|
|
195
|
+
return entry2;
|
|
196
|
+
}
|
|
197
|
+
const transport = server.transport === "http" ? "http" : "sse";
|
|
198
|
+
const entry = {
|
|
199
|
+
url: resolveEnvRefsDeep(server.url ?? ""),
|
|
200
|
+
transport
|
|
201
|
+
};
|
|
202
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
203
|
+
entry.headers = resolveEnvRefsDeep({ ...server.headers });
|
|
204
|
+
}
|
|
205
|
+
if (server.enabled === false) entry.enabled = false;
|
|
206
|
+
return entry;
|
|
207
|
+
}
|
|
208
|
+
/** Resolve every ${env:VAR} in env values to literals (no native token). */
|
|
209
|
+
renderEnv(env) {
|
|
210
|
+
if (!env || Object.keys(env).length === 0) return void 0;
|
|
211
|
+
return resolveEnvRefsDeep({ ...env });
|
|
212
|
+
}
|
|
213
|
+
/** Upsert config.mcp.servers.<id> idempotently. */
|
|
214
|
+
upsertNestedServer(path, id, entry, dryRun) {
|
|
215
|
+
if (this.isPresentButUnparseable(path)) {
|
|
216
|
+
return {
|
|
217
|
+
platform: this.id,
|
|
218
|
+
action: "warn",
|
|
219
|
+
path,
|
|
220
|
+
detail: `existing ${path} is not parseable; left untouched (back it up / fix it, then re-run)`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const cfg = this.readJson(path) ?? {};
|
|
224
|
+
const servers = this.nestedServersBucket(cfg);
|
|
225
|
+
const before = JSON.stringify(servers[id]);
|
|
226
|
+
const after = JSON.stringify(entry);
|
|
227
|
+
let action;
|
|
228
|
+
if (before === void 0) action = "create";
|
|
229
|
+
else if (before === after) action = "skip";
|
|
230
|
+
else action = "update";
|
|
231
|
+
if (action !== "skip") {
|
|
232
|
+
servers[id] = entry;
|
|
233
|
+
this.writeJson(path, cfg, dryRun);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
platform: this.id,
|
|
237
|
+
action,
|
|
238
|
+
path,
|
|
239
|
+
detail: `${MCP_ROOT_KEY}.${MCP_SERVERS_KEY}.${id}`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Remove config.mcp.servers.<id>. */
|
|
243
|
+
removeNestedServer(path, id, dryRun) {
|
|
244
|
+
const cfg = this.readJson(path);
|
|
245
|
+
const mcp = cfg?.[MCP_ROOT_KEY];
|
|
246
|
+
const servers = mcp && typeof mcp === "object" && !Array.isArray(mcp) ? mcp[MCP_SERVERS_KEY] : void 0;
|
|
247
|
+
if (!cfg || !servers || !(id in servers)) {
|
|
248
|
+
return {
|
|
249
|
+
platform: this.id,
|
|
250
|
+
action: "skip",
|
|
251
|
+
path,
|
|
252
|
+
detail: `${MCP_ROOT_KEY}.${MCP_SERVERS_KEY}.${id} absent`
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
delete servers[id];
|
|
256
|
+
this.writeJson(path, cfg, dryRun);
|
|
257
|
+
return {
|
|
258
|
+
platform: this.id,
|
|
259
|
+
action: "remove",
|
|
260
|
+
path,
|
|
261
|
+
detail: `${MCP_ROOT_KEY}.${MCP_SERVERS_KEY}.${id}`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Get-or-create config.mcp.servers as a mutable object. */
|
|
265
|
+
nestedServersBucket(cfg) {
|
|
266
|
+
let mcp = cfg[MCP_ROOT_KEY];
|
|
267
|
+
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) {
|
|
268
|
+
mcp = {};
|
|
269
|
+
cfg[MCP_ROOT_KEY] = mcp;
|
|
270
|
+
}
|
|
271
|
+
const mcpObj = mcp;
|
|
272
|
+
let servers = mcpObj[MCP_SERVERS_KEY];
|
|
273
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
274
|
+
servers = {};
|
|
275
|
+
mcpObj[MCP_SERVERS_KEY] = servers;
|
|
276
|
+
}
|
|
277
|
+
return servers;
|
|
278
|
+
}
|
|
279
|
+
// ── Hook install / uninstall (ts-plugin + DUAL REGISTRATION) ────────────
|
|
280
|
+
installHooks(ctx) {
|
|
281
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
282
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
283
|
+
if (ctx.connector.platforms[HOST]?.hooks === false) {
|
|
284
|
+
return [
|
|
285
|
+
{ platform: this.id, action: "skip", path: pluginPath, detail: "hooks disabled for openclaw" }
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
if (ctx.connector.hookEvents.length === 0) {
|
|
289
|
+
return [
|
|
290
|
+
{ platform: this.id, action: "skip", path: pluginPath, detail: "connector declares no hooks" }
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
const changes = [];
|
|
294
|
+
for (const file of this.synthesizePlugin(ctx)) {
|
|
295
|
+
const before = existsSync(file.path) ? this.safeRead(file.path) : void 0;
|
|
296
|
+
let action;
|
|
297
|
+
if (before === void 0) action = "create";
|
|
298
|
+
else if (before === file.contents) action = "skip";
|
|
299
|
+
else action = "update";
|
|
300
|
+
if (action !== "skip" && !ctx.dryRun) {
|
|
301
|
+
ensureDir(dirname(file.path));
|
|
302
|
+
writeFileSync(file.path, file.contents, "utf8");
|
|
303
|
+
chmodSync(file.path, file.executable ? 493 : 420);
|
|
304
|
+
}
|
|
305
|
+
changes.push({
|
|
306
|
+
platform: this.id,
|
|
307
|
+
action,
|
|
308
|
+
path: file.path,
|
|
309
|
+
detail: `openclaw plugin module (${this.hookDetail(ctx)})`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
changes.push(this.upsertPluginEntry(configPath, ctx, this.pluginDir(ctx)));
|
|
313
|
+
return changes;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Human-facing summary of which declared events the synthesized module ACTUALLY
|
|
317
|
+
* wires. Only events present in EVENT_TO_OPENCLAW are mapped/wired; any declared
|
|
318
|
+
* event with no OpenClaw mapping (e.g. UserPromptSubmit) is reported separately
|
|
319
|
+
* as "unsupported here" so the detail never overstates coverage.
|
|
320
|
+
*/
|
|
321
|
+
hookDetail(ctx) {
|
|
322
|
+
const declared = ctx.connector.hookEvents;
|
|
323
|
+
const mapped = declared.filter((e) => EVENT_TO_OPENCLAW[e] !== void 0);
|
|
324
|
+
const unsupported = declared.filter((e) => EVENT_TO_OPENCLAW[e] === void 0);
|
|
325
|
+
const base = mapped.join(",");
|
|
326
|
+
return unsupported.length > 0 ? `${base}; unsupported here: ${unsupported.join(",")}` : base;
|
|
327
|
+
}
|
|
328
|
+
uninstallHooks(ctx) {
|
|
329
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
330
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
331
|
+
const changes = [];
|
|
332
|
+
changes.push(
|
|
333
|
+
this.removePluginEntry(configPath, ctx.connector.id, this.pluginDir(ctx), ctx.dryRun)
|
|
334
|
+
);
|
|
335
|
+
const manifestPath = join(this.pluginDir(ctx), PLUGIN_MANIFEST_FILE);
|
|
336
|
+
for (const [path, label] of [
|
|
337
|
+
[pluginPath, "openclaw plugin module"],
|
|
338
|
+
[manifestPath, "openclaw plugin manifest"]
|
|
339
|
+
]) {
|
|
340
|
+
if (existsSync(path)) {
|
|
341
|
+
if (!ctx.dryRun) rmSync(path, { force: true });
|
|
342
|
+
changes.push({ platform: this.id, action: "remove", path, detail: label });
|
|
343
|
+
} else {
|
|
344
|
+
changes.push({
|
|
345
|
+
platform: this.id,
|
|
346
|
+
action: "skip",
|
|
347
|
+
path,
|
|
348
|
+
detail: `no ${label} present`
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
changes.push(this.removeDirIfEmpty(this.pluginDir(ctx), ctx.dryRun));
|
|
353
|
+
return changes;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Upsert plugins.entries.<id> = { enabled: true } AND add `pluginDir` to
|
|
357
|
+
* plugins.load.paths idempotently.
|
|
358
|
+
*
|
|
359
|
+
* `openclaw config validate` rejects a per-entry `module` field, so the entry
|
|
360
|
+
* carries ONLY `{ enabled: true }`. The gateway discovers the synthesized
|
|
361
|
+
* module by scanning the directories in plugins.load.paths (the dir holding
|
|
362
|
+
* index.mjs + openclaw.plugin.json), so the dir is added there.
|
|
363
|
+
*/
|
|
364
|
+
upsertPluginEntry(configPath, ctx, pluginDir) {
|
|
365
|
+
if (this.isPresentButUnparseable(configPath)) {
|
|
366
|
+
return {
|
|
367
|
+
platform: this.id,
|
|
368
|
+
action: "warn",
|
|
369
|
+
path: configPath,
|
|
370
|
+
detail: `existing ${configPath} is not parseable; left untouched (back it up / fix it, then re-run)`
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const cfg = this.readJson(configPath) ?? {};
|
|
374
|
+
const entries = this.pluginEntriesBucket(cfg);
|
|
375
|
+
const id = ctx.connector.id;
|
|
376
|
+
const desired = { enabled: true };
|
|
377
|
+
const entryBefore = JSON.stringify(entries[id]);
|
|
378
|
+
const entryAfter = JSON.stringify(desired);
|
|
379
|
+
const loadPaths = this.pluginLoadPathsBucket(cfg);
|
|
380
|
+
const pathPresent = loadPaths.includes(pluginDir);
|
|
381
|
+
const entryChanged = entryBefore !== entryAfter;
|
|
382
|
+
const pathChanged = !pathPresent;
|
|
383
|
+
let action;
|
|
384
|
+
if (entryBefore === void 0) action = "create";
|
|
385
|
+
else if (!entryChanged && !pathChanged) action = "skip";
|
|
386
|
+
else action = "update";
|
|
387
|
+
if (action !== "skip") {
|
|
388
|
+
entries[id] = desired;
|
|
389
|
+
if (!pathPresent) loadPaths.push(pluginDir);
|
|
390
|
+
this.writeJson(configPath, cfg, ctx.dryRun);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
platform: this.id,
|
|
394
|
+
action,
|
|
395
|
+
path: configPath,
|
|
396
|
+
detail: `${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}.${id} + ${PLUGINS_KEY}.${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY}`
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** Remove plugins.entries.<id> AND drop `pluginDir` from plugins.load.paths. */
|
|
400
|
+
removePluginEntry(configPath, id, pluginDir, dryRun) {
|
|
401
|
+
const cfg = this.readJson(configPath);
|
|
402
|
+
const plugins = cfg?.[PLUGINS_KEY];
|
|
403
|
+
const pluginsObj = plugins && typeof plugins === "object" && !Array.isArray(plugins) ? plugins : void 0;
|
|
404
|
+
const entries = pluginsObj?.[PLUGINS_ENTRIES_KEY];
|
|
405
|
+
const load = pluginsObj?.[PLUGINS_LOAD_KEY];
|
|
406
|
+
const loadPaths = load && Array.isArray(load[PLUGINS_LOAD_PATHS_KEY]) ? load[PLUGINS_LOAD_PATHS_KEY] : void 0;
|
|
407
|
+
const entryPresent = Boolean(entries && id in entries);
|
|
408
|
+
const pathIdx = loadPaths ? loadPaths.indexOf(pluginDir) : -1;
|
|
409
|
+
if (!cfg || !entryPresent && pathIdx < 0) {
|
|
410
|
+
return {
|
|
411
|
+
platform: this.id,
|
|
412
|
+
action: "skip",
|
|
413
|
+
path: configPath,
|
|
414
|
+
detail: `${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}.${id} absent`
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (entryPresent && entries) delete entries[id];
|
|
418
|
+
if (loadPaths && pathIdx >= 0) loadPaths.splice(pathIdx, 1);
|
|
419
|
+
this.writeJson(configPath, cfg, dryRun);
|
|
420
|
+
return {
|
|
421
|
+
platform: this.id,
|
|
422
|
+
action: "remove",
|
|
423
|
+
path: configPath,
|
|
424
|
+
detail: `${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}.${id} + ${PLUGINS_KEY}.${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY}`
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/** Get-or-create config.plugins.entries as a mutable object. */
|
|
428
|
+
pluginEntriesBucket(cfg) {
|
|
429
|
+
let plugins = cfg[PLUGINS_KEY];
|
|
430
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) {
|
|
431
|
+
plugins = {};
|
|
432
|
+
cfg[PLUGINS_KEY] = plugins;
|
|
433
|
+
}
|
|
434
|
+
const pluginsObj = plugins;
|
|
435
|
+
let entries = pluginsObj[PLUGINS_ENTRIES_KEY];
|
|
436
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
437
|
+
entries = {};
|
|
438
|
+
pluginsObj[PLUGINS_ENTRIES_KEY] = entries;
|
|
439
|
+
}
|
|
440
|
+
return entries;
|
|
441
|
+
}
|
|
442
|
+
/** Get-or-create config.plugins.load.paths as a mutable string array. */
|
|
443
|
+
pluginLoadPathsBucket(cfg) {
|
|
444
|
+
let plugins = cfg[PLUGINS_KEY];
|
|
445
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) {
|
|
446
|
+
plugins = {};
|
|
447
|
+
cfg[PLUGINS_KEY] = plugins;
|
|
448
|
+
}
|
|
449
|
+
const pluginsObj = plugins;
|
|
450
|
+
let load = pluginsObj[PLUGINS_LOAD_KEY];
|
|
451
|
+
if (!load || typeof load !== "object" || Array.isArray(load)) {
|
|
452
|
+
load = {};
|
|
453
|
+
pluginsObj[PLUGINS_LOAD_KEY] = load;
|
|
454
|
+
}
|
|
455
|
+
const loadObj = load;
|
|
456
|
+
const existing = loadObj[PLUGINS_LOAD_PATHS_KEY];
|
|
457
|
+
const paths = Array.isArray(existing) ? existing.filter((p) => typeof p === "string") : [];
|
|
458
|
+
loadObj[PLUGINS_LOAD_PATHS_KEY] = paths;
|
|
459
|
+
return paths;
|
|
460
|
+
}
|
|
461
|
+
// ── ts-plugin synthesis ────────────────────────────────────────────────
|
|
462
|
+
/**
|
|
463
|
+
* Build ONE self-contained ESM plugin module for OpenClaw.
|
|
464
|
+
*
|
|
465
|
+
* The module imports nothing from agent-connector. It embeds two constants
|
|
466
|
+
* (the absolute home-bin path and the connector id) and a `bridge()` helper
|
|
467
|
+
* that shells out to the universal hook entrypoint via execFileSync, feeding
|
|
468
|
+
* the OpenClaw event payload on stdin and JSON.parsing the normalized
|
|
469
|
+
* HookResponse back from stdout (fail-open: any error → null). Its default
|
|
470
|
+
* export is the OpenClaw plugin definition { id, name, configSchema,
|
|
471
|
+
* register(api) }; register wires api.on(...) handlers for the events this
|
|
472
|
+
* connector actually declares.
|
|
473
|
+
*/
|
|
474
|
+
synthesizePlugin(ctx) {
|
|
475
|
+
const path = this.getHookConfigPath(ctx);
|
|
476
|
+
const contents = this.buildPluginSource(ctx);
|
|
477
|
+
const manifestPath = join(this.pluginDir(ctx), PLUGIN_MANIFEST_FILE);
|
|
478
|
+
const manifest = this.buildPluginManifest(ctx);
|
|
479
|
+
return [
|
|
480
|
+
{ path, contents, executable: false },
|
|
481
|
+
{ path: manifestPath, contents: manifest, executable: false }
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Build the openclaw.plugin.json manifest the gateway reads inside a
|
|
486
|
+
* plugins.load.paths directory. It names the plugin and points at the ESM
|
|
487
|
+
* module entry (index.mjs) so the loader knows what to import.
|
|
488
|
+
*/
|
|
489
|
+
buildPluginManifest(ctx) {
|
|
490
|
+
const manifest = {
|
|
491
|
+
id: ctx.connector.id,
|
|
492
|
+
name: ctx.connector.displayName || ctx.connector.id,
|
|
493
|
+
main: this.pluginFileName(),
|
|
494
|
+
enabled: true
|
|
495
|
+
};
|
|
496
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
/** Compose the generated plugin source with plain string concatenation. */
|
|
500
|
+
buildPluginSource(ctx) {
|
|
501
|
+
const homeBin = JSON.stringify(ctx.homeBinPath);
|
|
502
|
+
const connectorId = JSON.stringify(ctx.connector.id);
|
|
503
|
+
const connectorName = JSON.stringify(ctx.connector.displayName || ctx.connector.id);
|
|
504
|
+
const events = ctx.connector.hookEvents.filter(
|
|
505
|
+
(e) => EVENT_TO_OPENCLAW[e] !== void 0
|
|
506
|
+
);
|
|
507
|
+
const has = (e) => events.includes(e);
|
|
508
|
+
const header = "/**\n * AUTO-GENERATED by agent-connector \u2014 DO NOT EDIT.\n *\n * Self-contained OpenClaw plugin bridge for connector " + ctx.connector.id + `.
|
|
509
|
+
* It imports nothing from agent-connector: every hook invocation shells out
|
|
510
|
+
* to the stable home binary's universal entrypoint and JSON-parses the
|
|
511
|
+
* normalized response. Fail-open: any bridge error degrades to a no-op.
|
|
512
|
+
*
|
|
513
|
+
* Registered via DUAL REGISTRATION in openclaw.json: plugins.entries.<id>
|
|
514
|
+
* (loads this module) + mcp.servers.<id> (surfaces the MCP tools). The
|
|
515
|
+
* gateway hot-reloads its config on SIGUSR1.
|
|
516
|
+
*/
|
|
517
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
518
|
+
|
|
519
|
+
const HOME_BIN = ` + homeBin + ";\nconst CONNECTOR_ID = " + connectorId + `;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Invoke the universal hook entrypoint for one canonical event.
|
|
523
|
+
* @param {string} event canonical event name (PreToolUse|PostToolUse|SessionStart)
|
|
524
|
+
* @param {object} payload OpenClaw-shaped payload posted on stdin
|
|
525
|
+
* @returns {object|null} normalized HookResponse, or null on any failure
|
|
526
|
+
*/
|
|
527
|
+
function bridge(event, payload) {
|
|
528
|
+
try {
|
|
529
|
+
// On Windows HOME_BIN is the agent-connector.cmd launcher: Node cannot
|
|
530
|
+
// execFile a batch file, and shell+args is deprecated (DEP0190), so run
|
|
531
|
+
// one quoted command line via a shell. POSIX keeps the direct execFile.
|
|
532
|
+
const args = ["hook", "openclaw", event, "--connector", CONNECTOR_ID];
|
|
533
|
+
const opts = { input: JSON.stringify(payload), encoding: "utf8" };
|
|
534
|
+
const stdout =
|
|
535
|
+
process.platform === "win32"
|
|
536
|
+
? execSync([HOME_BIN, ...args].map((a) => '"' + a + '"').join(" "), opts)
|
|
537
|
+
: execFileSync(HOME_BIN, args, opts);
|
|
538
|
+
const text = (stdout || "").trim();
|
|
539
|
+
if (text === "") return { decision: "allow" };
|
|
540
|
+
return JSON.parse(text);
|
|
541
|
+
} catch {
|
|
542
|
+
// Fail-open \u2014 never wedge a tool call on a bridge error.
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Module-scoped context the prompt-build hook injects at session start.
|
|
548
|
+
// before_prompt_build is the verified injection point (session_start itself
|
|
549
|
+
// returns no context payload). The flag prevents double-injection.
|
|
550
|
+
let PROJECT_DIR = process.cwd();
|
|
551
|
+
let SESSION_ID = "";
|
|
552
|
+
let pendingContext = null;
|
|
553
|
+
let contextInjected = false;
|
|
554
|
+
|
|
555
|
+
`;
|
|
556
|
+
const reg = [];
|
|
557
|
+
reg.push(
|
|
558
|
+
' register(api) {\n PROJECT_DIR = process.cwd();\n const on = (event, handler) => {\n if (typeof api?.on === "function") {\n try { api.on(event, handler); return; } catch { /* fall through */ }\n }\n if (typeof api?.registerHook === "function") {\n try {\n api.registerHook(event, handler, { name: CONNECTOR_ID + ":" + event, description: "agent-connector bridge" });\n } catch { /* best effort */ }\n }\n };\n'
|
|
559
|
+
);
|
|
560
|
+
if (has("PreToolUse")) {
|
|
561
|
+
reg.push(
|
|
562
|
+
'\n // PreToolUse \u2192 before_tool_call: block (return { block, blockReason })\n // for deny/ask; rewrite args by mutating event.params in place for modify.\n on("before_tool_call", async (event) => {\n const e = event || {};\n const toolInput = e.params || {};\n const res = bridge("PreToolUse", {\n toolName: e.toolName || "",\n toolInput,\n sessionId: SESSION_ID,\n projectDir: PROJECT_DIR,\n });\n if (!res) return undefined;\n // OpenClaw has no separate ask gate \u2014 deny and ask both block (safe).\n if (res.decision === "deny" || res.decision === "ask") {\n return { block: true, blockReason: res.reason || ("Blocked by " + CONNECTOR_ID) };\n }\n if (res.decision === "modify" && res.updatedInput && typeof toolInput === "object") {\n Object.assign(toolInput, res.updatedInput);\n }\n return undefined;\n });\n'
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (has("PostToolUse")) {
|
|
566
|
+
reg.push(
|
|
567
|
+
'\n // PostToolUse \u2192 after_tool_call: observe the completed tool call.\n // OpenClaw exposes the result (v2+) or output (older builds); it cannot\n // rewrite the tool result, so updatedOutput is intentionally dropped.\n on("after_tool_call", async (event) => {\n const e = event || {};\n const rawResult = e.result !== undefined ? e.result : e.output;\n const toolOutput =\n typeof rawResult === "string"\n ? rawResult\n : rawResult != null\n ? JSON.stringify(rawResult)\n : undefined;\n bridge("PostToolUse", {\n toolName: e.toolName || "",\n toolInput: e.params || {},\n toolOutput,\n isError: Boolean(e.error || e.isError),\n sessionId: SESSION_ID,\n projectDir: PROJECT_DIR,\n });\n return undefined;\n });\n'
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (has("SessionStart")) {
|
|
571
|
+
reg.push(
|
|
572
|
+
'\n // SessionStart \u2192 session_start records the real session id and fetches\n // the additionalContext to inject; before_prompt_build performs the\n // injection (the verified context-injection point upstream).\n on("session_start", async (event) => {\n const e = event || {};\n SESSION_ID = e.sessionId || e.sessionKey || SESSION_ID;\n const res = bridge("SessionStart", {\n sessionId: SESSION_ID,\n source: e.resumedFrom ? "resume" : "startup",\n projectDir: PROJECT_DIR,\n });\n contextInjected = false;\n pendingContext =\n res && typeof res.additionalContext === "string" && res.additionalContext\n ? res.additionalContext\n : null;\n return undefined;\n });\n\n // Inject the session-start context once, at prompt-build time.\n on("before_prompt_build", () => {\n if (contextInjected || !pendingContext) return undefined;\n contextInjected = true;\n return { appendSystemContext: pendingContext };\n });\n'
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
reg.push(" },\n");
|
|
576
|
+
const definition = "const plugin = {\n id: CONNECTOR_ID,\n name: " + connectorName + ',\n configSchema: {\n type: "object",\n properties: {\n enabled: {\n type: "boolean",\n default: true,\n description: "Enable or disable the " + CONNECTOR_ID + " plugin.",\n },\n },\n additionalProperties: false,\n },\n' + reg.join("") + "};\n\nexport default plugin;\n";
|
|
577
|
+
return header + definition;
|
|
578
|
+
}
|
|
579
|
+
// ── Runtime: parse OUR bridge payload → normalized event ───────────────
|
|
580
|
+
/**
|
|
581
|
+
* `raw` is the payload OUR generated plugin posts (NOT a host-native shape):
|
|
582
|
+
* { toolName, toolInput, toolOutput?, isError?, sessionId, source?, projectDir }
|
|
583
|
+
* so this maps straight through.
|
|
584
|
+
*/
|
|
585
|
+
parseEvent(event, raw) {
|
|
586
|
+
const input = raw ?? {};
|
|
587
|
+
const base = {
|
|
588
|
+
hostPlatform: HOST,
|
|
589
|
+
connectorId: "",
|
|
590
|
+
sessionId: typeof input.sessionId === "string" ? input.sessionId : "",
|
|
591
|
+
raw,
|
|
592
|
+
...typeof input.projectDir === "string" ? { projectDir: input.projectDir } : {}
|
|
593
|
+
};
|
|
594
|
+
switch (event) {
|
|
595
|
+
case "PreToolUse": {
|
|
596
|
+
const ev = {
|
|
597
|
+
...base,
|
|
598
|
+
toolName: input.toolName ?? "",
|
|
599
|
+
toolInput: input.toolInput ?? {}
|
|
600
|
+
};
|
|
601
|
+
return ev;
|
|
602
|
+
}
|
|
603
|
+
case "PostToolUse": {
|
|
604
|
+
const ev = {
|
|
605
|
+
...base,
|
|
606
|
+
toolName: input.toolName ?? "",
|
|
607
|
+
toolInput: input.toolInput ?? {},
|
|
608
|
+
...typeof input.toolOutput === "string" ? { toolOutput: input.toolOutput } : {},
|
|
609
|
+
...typeof input.isError === "boolean" ? { isError: input.isError } : {}
|
|
610
|
+
};
|
|
611
|
+
return ev;
|
|
612
|
+
}
|
|
613
|
+
case "SessionStart": {
|
|
614
|
+
const source = input.source === "resume" ? "resume" : input.source === "compact" ? "compact" : input.source === "clear" ? "clear" : "startup";
|
|
615
|
+
const ev = { ...base, source };
|
|
616
|
+
return ev;
|
|
617
|
+
}
|
|
618
|
+
default:
|
|
619
|
+
return { ...base, source: "startup" };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// ── Runtime: normalized response → reply the generated bridge parses ───
|
|
623
|
+
/**
|
|
624
|
+
* Unlike json-stdio hosts (whose reply is the host's NATIVE control payload),
|
|
625
|
+
* OUR generated bridge consumes this stdout directly. So the reply body is the
|
|
626
|
+
* NORMALIZED HookResponse itself — the bridge JSON.parses it and reads
|
|
627
|
+
* decision / updatedInput / additionalContext.
|
|
628
|
+
*/
|
|
629
|
+
formatReply(_event, response) {
|
|
630
|
+
return {
|
|
631
|
+
exitCode: 0,
|
|
632
|
+
stdout: JSON.stringify(response ?? { decision: "allow" })
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
// ── Diagnostics ────────────────────────────────────────────────────────
|
|
636
|
+
getHealthChecks(ctx) {
|
|
637
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
638
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
639
|
+
const pluginDir = this.pluginDir(ctx);
|
|
640
|
+
const id = ctx.connector.id;
|
|
641
|
+
const hasHooks = ctx.connector.hookEvents.length > 0;
|
|
642
|
+
const hasServer = Boolean(ctx.connector.server);
|
|
643
|
+
return [
|
|
644
|
+
{
|
|
645
|
+
name: `${this.name}: openclaw.json present`,
|
|
646
|
+
check: () => existsSync(configPath) ? { status: "OK", detail: configPath } : { status: "FAIL", detail: `not found: ${configPath}` }
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
// HARDEST PART — dual registration must be CONSISTENT. The plugin only
|
|
650
|
+
// delivers tools when it is LOADED (plugins.entries.<id> enabled AND its
|
|
651
|
+
// dir in plugins.load.paths) AND in mcp.servers. A config with one half
|
|
652
|
+
// but not the other is a tool-less (or never-loaded) install, so FAIL on
|
|
653
|
+
// loaded XOR mcp.servers. (We assert plugins.load.paths — NOT a per-entry
|
|
654
|
+
// `module` field, which openclaw config validate rejects.)
|
|
655
|
+
name: `${this.name}: dual registration (plugins.entries+load.paths + mcp.servers.${id})`,
|
|
656
|
+
check: () => {
|
|
657
|
+
const cfg = this.readJson(configPath);
|
|
658
|
+
if (!cfg) return { status: "FAIL", detail: `cannot read ${configPath}` };
|
|
659
|
+
const inEntries = this.hasPluginEntry(cfg, id) && this.hasLoadPath(cfg, pluginDir);
|
|
660
|
+
const inMcp = this.hasMcpServer(cfg, id);
|
|
661
|
+
if (!hasServer) {
|
|
662
|
+
return inEntries || !hasHooks ? { status: "OK", detail: "hooks-only connector (no MCP server)" } : {
|
|
663
|
+
status: "FAIL",
|
|
664
|
+
detail: `${id} not enabled in ${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY} + ${PLUGINS_KEY}.${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY} (plugin will not load)`
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (inEntries && inMcp) {
|
|
668
|
+
return {
|
|
669
|
+
status: "OK",
|
|
670
|
+
detail: `${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}.${id}+${PLUGINS_KEY}.${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY} + ${MCP_ROOT_KEY}.${MCP_SERVERS_KEY}.${id}`
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (inEntries && !inMcp) {
|
|
674
|
+
return {
|
|
675
|
+
status: "FAIL",
|
|
676
|
+
detail: `loaded (${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}+${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY}) but missing from ${MCP_ROOT_KEY}.${MCP_SERVERS_KEY} \u2014 plugin loads but no tools reach the agent`
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (!inEntries && inMcp) {
|
|
680
|
+
return {
|
|
681
|
+
status: "FAIL",
|
|
682
|
+
detail: `in ${MCP_ROOT_KEY}.${MCP_SERVERS_KEY} but not loaded (missing ${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY} or ${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY}) \u2014 tools declared but plugin never loads`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
status: "FAIL",
|
|
687
|
+
detail: `not loaded (${PLUGINS_KEY}.${PLUGINS_ENTRIES_KEY}+${PLUGINS_LOAD_KEY}.${PLUGINS_LOAD_PATHS_KEY}) and not in ${MCP_ROOT_KEY}.${MCP_SERVERS_KEY}`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: `${this.name}: plugin module present`,
|
|
693
|
+
check: () => {
|
|
694
|
+
if (!hasHooks) return { status: "OK", detail: "no hooks declared" };
|
|
695
|
+
return existsSync(pluginPath) ? { status: "OK", detail: pluginPath } : { status: "FAIL", detail: `not found: ${pluginPath}` };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
/** True when config.plugins.entries.<id> exists (and is not disabled). */
|
|
701
|
+
hasPluginEntry(cfg, id) {
|
|
702
|
+
const plugins = cfg[PLUGINS_KEY];
|
|
703
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) return false;
|
|
704
|
+
const entries = plugins[PLUGINS_ENTRIES_KEY];
|
|
705
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return false;
|
|
706
|
+
const entry = entries[id];
|
|
707
|
+
if (entry === void 0) return false;
|
|
708
|
+
return !(entry && typeof entry === "object" && entry.enabled === false);
|
|
709
|
+
}
|
|
710
|
+
/** True when config.plugins.load.paths includes `pluginDir`. */
|
|
711
|
+
hasLoadPath(cfg, pluginDir) {
|
|
712
|
+
const plugins = cfg[PLUGINS_KEY];
|
|
713
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) return false;
|
|
714
|
+
const load = plugins[PLUGINS_LOAD_KEY];
|
|
715
|
+
if (!load || typeof load !== "object" || Array.isArray(load)) return false;
|
|
716
|
+
const paths = load[PLUGINS_LOAD_PATHS_KEY];
|
|
717
|
+
return Array.isArray(paths) && paths.includes(pluginDir);
|
|
718
|
+
}
|
|
719
|
+
/** True when config.mcp.servers.<id> exists. */
|
|
720
|
+
hasMcpServer(cfg, id) {
|
|
721
|
+
const mcp = cfg[MCP_ROOT_KEY];
|
|
722
|
+
if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return false;
|
|
723
|
+
const servers = mcp[MCP_SERVERS_KEY];
|
|
724
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return false;
|
|
725
|
+
return id in servers;
|
|
726
|
+
}
|
|
727
|
+
/** Read a file, returning undefined on any error (idempotency compare). */
|
|
728
|
+
safeRead(path) {
|
|
729
|
+
try {
|
|
730
|
+
return readFileSync(path, "utf8");
|
|
731
|
+
} catch {
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
function resolveOpenClawConfigPath(env = process.env) {
|
|
737
|
+
if (env.OPENCLAW_CONFIG_PATH) return resolve(env.OPENCLAW_CONFIG_PATH);
|
|
738
|
+
const stateDir = env.OPENCLAW_STATE_DIR ? resolve(env.OPENCLAW_STATE_DIR) : resolve(homedir(), ".openclaw");
|
|
739
|
+
return resolve(stateDir, "openclaw.json");
|
|
740
|
+
}
|
|
741
|
+
function ensureDir(dir) {
|
|
742
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
743
|
+
}
|
|
744
|
+
var adapter = new OpenClawAdapter();
|
|
745
|
+
var openclaw_default = adapter;
|
|
746
|
+
export {
|
|
747
|
+
OpenClawAdapter,
|
|
748
|
+
adapter,
|
|
749
|
+
openclaw_default as default
|
|
750
|
+
};
|
|
751
|
+
//# sourceMappingURL=openclaw-ETN2M63O.js.map
|