@ken-jo/agent-connector 0.4.79 → 0.4.91
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/README.md +3 -3
- package/dist/{action-OHGFVKMC.js → action-L44DXFHT.js} +9 -9
- package/dist/antigravity-cli-JO6GUTV7.js +436 -0
- package/dist/antigravity-cli-JO6GUTV7.js.map +1 -0
- package/dist/{chunk-X457NOFE.js → chunk-25CPWJA7.js} +91 -11
- package/dist/chunk-25CPWJA7.js.map +1 -0
- package/dist/{chunk-VKRGQLY4.js → chunk-7HUUINPB.js} +18 -18
- package/dist/{chunk-LKAVD4HP.js → chunk-BLTVZACS.js} +8 -8
- package/dist/{chunk-PZHB5TVA.js → chunk-G4CEELKO.js} +2 -2
- package/dist/{chunk-VD4EG6SG.js → chunk-KIRCPLM4.js} +5 -5
- package/dist/{chunk-ZXX5DVSE.js → chunk-LAIITCMW.js} +6 -6
- package/dist/{chunk-UYZ53KWS.js → chunk-LF76VPGQ.js} +5 -5
- package/dist/{chunk-IRUYJD23.js → chunk-NLE2J5P4.js} +1 -1
- package/dist/{chunk-7NXEHZAK.js → chunk-NWLRJNXM.js} +2 -2
- package/dist/{chunk-YJP3KGLM.js → chunk-QARPXSIV.js} +4 -4
- package/dist/{chunk-AMK2K6XZ.js → chunk-R6LML2YS.js} +2 -2
- package/dist/{chunk-S4E6QQEM.js → chunk-ZMLVR4J7.js} +7 -2
- package/dist/chunk-ZMLVR4J7.js.map +1 -0
- package/dist/{claude-code-T6ZRCMA7.js → claude-code-WS4NAGZT.js} +4 -4
- package/dist/cli/sdk.js +4 -4
- package/dist/cli.js +1 -1
- package/dist/codebuddy-RBEZGXBG.js +643 -0
- package/dist/codebuddy-RBEZGXBG.js.map +1 -0
- package/dist/{codex-RHKQKNNG.js → codex-ZZ434GYR.js} +4 -4
- package/dist/{detect-ALAKE54S.js → detect-3S46RUMK.js} +4 -4
- package/dist/devin-PRJY2TTD.js +510 -0
- package/dist/devin-PRJY2TTD.js.map +1 -0
- package/dist/{doctor-UNF7FCF4.js → doctor-3CUBPQKU.js} +14 -14
- package/dist/grok-cli-5QD235UB.js +482 -0
- package/dist/grok-cli-5QD235UB.js.map +1 -0
- package/dist/{hook-GM2IDA3C.js → hook-P6EKDINW.js} +9 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{install-7O4XWH7T.js → install-7DMK53SB.js} +10 -10
- package/dist/{introspect-5o5ick0_.d.ts → introspect-DYOIX8Ov.d.ts} +1 -1
- package/dist/{jetbrains-copilot-4IOT3I2Y.js → jetbrains-copilot-T2EPKROP.js} +9 -3
- package/dist/jetbrains-copilot-T2EPKROP.js.map +1 -0
- package/dist/junie-SQ4SDLGK.js +227 -0
- package/dist/junie-SQ4SDLGK.js.map +1 -0
- package/dist/{kilo-TAHOPT2Q.js → kilo-OU2ZLZKB.js} +4 -4
- package/dist/{kilo-cli-X4NPJWGX.js → kilo-cli-NMWGYF4F.js} +4 -4
- package/dist/{kiro-YZ4TTZGD.js → kiro-4IN5IH46.js} +77 -2
- package/dist/kiro-4IN5IH46.js.map +1 -0
- package/dist/{leaderboard-DVW4WYJ6.js → leaderboard-LF5SP6X2.js} +4 -4
- package/dist/mistral-vibe-TRQDHAWV.js +300 -0
- package/dist/mistral-vibe-TRQDHAWV.js.map +1 -0
- package/dist/open-interpreter-4PKL56AA.js +264 -0
- package/dist/open-interpreter-4PKL56AA.js.map +1 -0
- package/dist/openhands-5LNH3BKB.js +384 -0
- package/dist/openhands-5LNH3BKB.js.map +1 -0
- package/dist/{package-H5V3AZHR.js → package-2EAIYSJ4.js} +7 -7
- package/dist/{pi-I7BLL4EI.js → pi-BUADR5MJ.js} +139 -2
- package/dist/pi-BUADR5MJ.js.map +1 -0
- package/dist/{qwen-code-JG7AF442.js → qwen-code-TUJNHKNI.js} +4 -4
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +7 -7
- package/dist/sdk/index.d.ts +3 -3
- package/dist/sdk/index.js +3 -3
- package/dist/sdk/test.d.ts +2 -2
- package/dist/sdk/test.js +7 -7
- package/dist/{serve-WMO2TUW3.js → serve-K62FW3V7.js} +9 -9
- package/dist/{status-WRLBTBRE.js → status-E5MCFZM6.js} +7 -7
- package/dist/{statusline-EUD4DMIP.js → statusline-LIPF2KVP.js} +8 -8
- package/dist/{telemetry-U3LGMR3D.js → telemetry-4OC56NTM.js} +5 -5
- package/dist/{types-D6RWGXm1.d.ts → types-C4qMgbJn.d.ts} +10 -9
- package/dist/{uninstall-TWPUEJEC.js → uninstall-2YYKN5DX.js} +10 -10
- package/dist/{upgrade-EM6Y7QLH.js → upgrade-55AFPJEF.js} +10 -10
- package/dist/{usage-3HQDTXGY.js → usage-3PB3WZSL.js} +3 -3
- package/dist/{usage-event-DWIGKO7V.js → usage-event-KD5FW6W4.js} +8 -8
- package/dist/{zed-LLOAUXSA.js → zed-ZF2OQ75O.js} +119 -3
- package/dist/zed-ZF2OQ75O.js.map +1 -0
- package/package.json +1 -1
- package/dist/antigravity-cli-KODEKUTM.js +0 -87
- package/dist/antigravity-cli-KODEKUTM.js.map +0 -1
- package/dist/chunk-S4E6QQEM.js.map +0 -1
- package/dist/chunk-X457NOFE.js.map +0 -1
- package/dist/jetbrains-copilot-4IOT3I2Y.js.map +0 -1
- package/dist/kiro-YZ4TTZGD.js.map +0 -1
- package/dist/pi-I7BLL4EI.js.map +0 -1
- package/dist/zed-LLOAUXSA.js.map +0 -1
- /package/dist/{action-OHGFVKMC.js.map → action-L44DXFHT.js.map} +0 -0
- /package/dist/{chunk-VKRGQLY4.js.map → chunk-7HUUINPB.js.map} +0 -0
- /package/dist/{chunk-LKAVD4HP.js.map → chunk-BLTVZACS.js.map} +0 -0
- /package/dist/{chunk-PZHB5TVA.js.map → chunk-G4CEELKO.js.map} +0 -0
- /package/dist/{chunk-VD4EG6SG.js.map → chunk-KIRCPLM4.js.map} +0 -0
- /package/dist/{chunk-ZXX5DVSE.js.map → chunk-LAIITCMW.js.map} +0 -0
- /package/dist/{chunk-UYZ53KWS.js.map → chunk-LF76VPGQ.js.map} +0 -0
- /package/dist/{chunk-IRUYJD23.js.map → chunk-NLE2J5P4.js.map} +0 -0
- /package/dist/{chunk-7NXEHZAK.js.map → chunk-NWLRJNXM.js.map} +0 -0
- /package/dist/{chunk-YJP3KGLM.js.map → chunk-QARPXSIV.js.map} +0 -0
- /package/dist/{chunk-AMK2K6XZ.js.map → chunk-R6LML2YS.js.map} +0 -0
- /package/dist/{claude-code-T6ZRCMA7.js.map → claude-code-WS4NAGZT.js.map} +0 -0
- /package/dist/{codex-RHKQKNNG.js.map → codex-ZZ434GYR.js.map} +0 -0
- /package/dist/{detect-ALAKE54S.js.map → detect-3S46RUMK.js.map} +0 -0
- /package/dist/{doctor-UNF7FCF4.js.map → doctor-3CUBPQKU.js.map} +0 -0
- /package/dist/{hook-GM2IDA3C.js.map → hook-P6EKDINW.js.map} +0 -0
- /package/dist/{install-7O4XWH7T.js.map → install-7DMK53SB.js.map} +0 -0
- /package/dist/{kilo-TAHOPT2Q.js.map → kilo-OU2ZLZKB.js.map} +0 -0
- /package/dist/{kilo-cli-X4NPJWGX.js.map → kilo-cli-NMWGYF4F.js.map} +0 -0
- /package/dist/{leaderboard-DVW4WYJ6.js.map → leaderboard-LF5SP6X2.js.map} +0 -0
- /package/dist/{package-H5V3AZHR.js.map → package-2EAIYSJ4.js.map} +0 -0
- /package/dist/{qwen-code-JG7AF442.js.map → qwen-code-TUJNHKNI.js.map} +0 -0
- /package/dist/{serve-WMO2TUW3.js.map → serve-K62FW3V7.js.map} +0 -0
- /package/dist/{status-WRLBTBRE.js.map → status-E5MCFZM6.js.map} +0 -0
- /package/dist/{statusline-EUD4DMIP.js.map → statusline-LIPF2KVP.js.map} +0 -0
- /package/dist/{telemetry-U3LGMR3D.js.map → telemetry-4OC56NTM.js.map} +0 -0
- /package/dist/{uninstall-TWPUEJEC.js.map → uninstall-2YYKN5DX.js.map} +0 -0
- /package/dist/{upgrade-EM6Y7QLH.js.map → upgrade-55AFPJEF.js.map} +0 -0
- /package/dist/{usage-3HQDTXGY.js.map → usage-3PB3WZSL.js.map} +0 -0
- /package/dist/{usage-event-DWIGKO7V.js.map → usage-event-KD5FW6W4.js.map} +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
normalizeSessionSource
|
|
4
|
+
} from "./chunk-H53QZQJI.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveEnvRefsDeep
|
|
7
|
+
} from "./chunk-BEFVJ2OE.js";
|
|
8
|
+
import {
|
|
9
|
+
BaseAdapter
|
|
10
|
+
} from "./chunk-VVFDU4IX.js";
|
|
11
|
+
import {
|
|
12
|
+
buildWrappedStdio,
|
|
13
|
+
isHomeBinHookCommand
|
|
14
|
+
} from "./chunk-HORHRHNG.js";
|
|
15
|
+
import "./chunk-YYNXIUR2.js";
|
|
16
|
+
import "./chunk-IXOQDZWC.js";
|
|
17
|
+
|
|
18
|
+
// src/adapters/openhands/index.ts
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
var HOST = "openhands";
|
|
23
|
+
var MCP_ROOT_KEY = "mcpServers";
|
|
24
|
+
function persistenceDir() {
|
|
25
|
+
return process.env.OPENHANDS_PERSISTENCE_DIR ?? join(homedir(), ".openhands");
|
|
26
|
+
}
|
|
27
|
+
var SUPPORTED_EVENTS = /* @__PURE__ */ new Set([
|
|
28
|
+
"PreToolUse",
|
|
29
|
+
"PostToolUse",
|
|
30
|
+
"UserPromptSubmit",
|
|
31
|
+
"SessionStart",
|
|
32
|
+
"SessionEnd",
|
|
33
|
+
"Stop"
|
|
34
|
+
]);
|
|
35
|
+
var OpenHandsAdapter = class extends BaseAdapter {
|
|
36
|
+
id = HOST;
|
|
37
|
+
name = "OpenHands";
|
|
38
|
+
paradigm = "json-stdio";
|
|
39
|
+
capabilities = {
|
|
40
|
+
// Memory surface: AGENTS.md-first managed block via the BaseAdapter default.
|
|
41
|
+
supportsMemory: true,
|
|
42
|
+
preToolUse: true,
|
|
43
|
+
postToolUse: true,
|
|
44
|
+
userPromptSubmit: true,
|
|
45
|
+
sessionStart: true,
|
|
46
|
+
sessionEnd: true,
|
|
47
|
+
stop: true,
|
|
48
|
+
// OpenHands fires NONE of these on its hook wire (HookEventType has only the
|
|
49
|
+
// six above) — install reports the standard skip-warn for any of them.
|
|
50
|
+
// The two REQUIRED PlatformCapabilities flags below are explicitly false;
|
|
51
|
+
// subagentStop / permissionRequest / postToolUseFailure / subagentStart are
|
|
52
|
+
// optional and stay unset (read as ?? false) — no OpenHands analog.
|
|
53
|
+
preCompact: false,
|
|
54
|
+
notification: false,
|
|
55
|
+
// PreToolUse/UserPromptSubmit/Stop can block (exit 2 or {"decision":"deny"}),
|
|
56
|
+
// but the hook wire cannot rewrite tool ARGS or already-emitted tool OUTPUT.
|
|
57
|
+
canModifyArgs: false,
|
|
58
|
+
canModifyOutput: false,
|
|
59
|
+
// additionalContext on the stdout reply is honored (executor.py parses it).
|
|
60
|
+
canInjectSessionContext: true,
|
|
61
|
+
// OpenHands registers stdio + remote (Streamable HTTP / SSE) MCP servers.
|
|
62
|
+
transports: ["stdio", "http"]
|
|
63
|
+
// Content surfaces (commands/skills/subagents/statusline/actions) are NOT
|
|
64
|
+
// wired: OpenHands' CLI uses microagents + an interactive TUI rather than a
|
|
65
|
+
// Claude-style file-per-command/skill/subagent tree, and no first-party
|
|
66
|
+
// file layout for those was byte-confirmed. Left unset (honest CEILING).
|
|
67
|
+
};
|
|
68
|
+
// ── Detection ────────────────────────────────────────────────────────────
|
|
69
|
+
detectInstalled(projectDir) {
|
|
70
|
+
const userDir = persistenceDir();
|
|
71
|
+
const userMcp = join(userDir, "mcp.json");
|
|
72
|
+
const projDir = join(projectDir, ".openhands");
|
|
73
|
+
const projHooks = join(projDir, "hooks.json");
|
|
74
|
+
const userInstalled = existsSync(userDir) || existsSync(userMcp);
|
|
75
|
+
const projInstalled = existsSync(projDir) || existsSync(projHooks);
|
|
76
|
+
const installed = userInstalled || projInstalled;
|
|
77
|
+
const scope = userInstalled || !projInstalled ? "user" : "project";
|
|
78
|
+
const configPath = scope === "user" ? userMcp : projHooks;
|
|
79
|
+
return {
|
|
80
|
+
id: this.id,
|
|
81
|
+
name: this.name,
|
|
82
|
+
installed,
|
|
83
|
+
paradigm: this.paradigm,
|
|
84
|
+
capabilities: this.capabilities,
|
|
85
|
+
configPath,
|
|
86
|
+
scope,
|
|
87
|
+
reason: installed ? `found OpenHands config (${scope}) at ${configPath}` : `no OpenHands config at ${userDir} or ${projDir}`,
|
|
88
|
+
confidence: installed ? "high" : "low"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Native paths ─────────────────────────────────────────────────────────
|
|
92
|
+
getConfigDir(ctx) {
|
|
93
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".openhands") : persistenceDir();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* MCP `mcp.json` lives in the CLI persistence dir, which is USER-scoped only —
|
|
97
|
+
* the CLI reads exactly one `<persistence>/mcp.json` and has no project
|
|
98
|
+
* variant. So this ALWAYS resolves to the user persistence dir regardless of
|
|
99
|
+
* ctx.scope (writing a project `.openhands/mcp.json` the CLI never reads would
|
|
100
|
+
* be a silent no-op).
|
|
101
|
+
*/
|
|
102
|
+
getServerConfigPath(_ctx) {
|
|
103
|
+
return join(persistenceDir(), "mcp.json");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Hooks live in a SEPARATE `.openhands/hooks.json`. HookConfig.load searches
|
|
107
|
+
* <working_dir>/.openhands/hooks.json then ~/.openhands/hooks.json, so both
|
|
108
|
+
* scopes are honored — project scope → <projectDir>/.openhands, user scope →
|
|
109
|
+
* the persistence dir.
|
|
110
|
+
*/
|
|
111
|
+
getHookConfigPath(ctx) {
|
|
112
|
+
return join(this.getConfigDir(ctx), "hooks.json");
|
|
113
|
+
}
|
|
114
|
+
// ── MCP server install / uninstall ───────────────────────────────────────
|
|
115
|
+
installServer(ctx) {
|
|
116
|
+
const { connector } = ctx;
|
|
117
|
+
const override = connector.platforms[HOST]?.server;
|
|
118
|
+
if (!connector.server || override === false) {
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
platform: this.id,
|
|
122
|
+
action: "skip",
|
|
123
|
+
detail: connector.server ? "server registration disabled for openhands" : "connector declares no MCP server"
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const server = override && typeof override === "object" ? { ...connector.server, ...override } : connector.server;
|
|
128
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
129
|
+
const entry = this.renderServerEntry(ctx, server);
|
|
130
|
+
return [
|
|
131
|
+
this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun)
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
uninstallServer(ctx) {
|
|
135
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
136
|
+
return [
|
|
137
|
+
this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun)
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
/** Render a normalized ServerDef into OpenHands' native mcpServers entry. */
|
|
141
|
+
renderServerEntry(ctx, server) {
|
|
142
|
+
const transport = server.transport;
|
|
143
|
+
if (transport === "stdio") {
|
|
144
|
+
let command = server.command ?? "";
|
|
145
|
+
let args = [...server.args ?? []];
|
|
146
|
+
({ command, args } = buildWrappedStdio(ctx, server, this.id, command, args));
|
|
147
|
+
const entry2 = {
|
|
148
|
+
command: resolveEnvRefsDeep(command),
|
|
149
|
+
transport: "stdio"
|
|
150
|
+
};
|
|
151
|
+
if (args.length > 0) entry2.args = resolveEnvRefsDeep(args);
|
|
152
|
+
const env = this.renderEnv(server.env);
|
|
153
|
+
if (env) entry2.env = env;
|
|
154
|
+
return entry2;
|
|
155
|
+
}
|
|
156
|
+
const entry = {
|
|
157
|
+
url: resolveEnvRefsDeep(server.url ?? ""),
|
|
158
|
+
transport: transport === "sse" ? "sse" : "http"
|
|
159
|
+
};
|
|
160
|
+
const headers = this.renderEnv(server.headers);
|
|
161
|
+
if (headers) entry.headers = headers;
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Render env/header values. Resolve `${env:VAR}` references to literals at
|
|
166
|
+
* install time (the safe path matching Droid).
|
|
167
|
+
*/
|
|
168
|
+
renderEnv(env) {
|
|
169
|
+
if (!env || Object.keys(env).length === 0) return void 0;
|
|
170
|
+
return resolveEnvRefsDeep({ ...env });
|
|
171
|
+
}
|
|
172
|
+
// ── Hook install / uninstall (separate hooks.json, nested-rule shape) ─────
|
|
173
|
+
installHooks(ctx) {
|
|
174
|
+
const { connector } = ctx;
|
|
175
|
+
if (connector.platforms[HOST]?.hooks === false) {
|
|
176
|
+
return [{ platform: this.id, action: "skip", detail: "hooks disabled for openhands" }];
|
|
177
|
+
}
|
|
178
|
+
if (connector.hookEvents.length === 0) {
|
|
179
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no hooks" }];
|
|
180
|
+
}
|
|
181
|
+
const pending = connector.hookEvents.map((event) => ({
|
|
182
|
+
event,
|
|
183
|
+
matcher: connector.hooks[event]?.matcher ?? ""
|
|
184
|
+
}));
|
|
185
|
+
return this.upsertHookEntries(
|
|
186
|
+
ctx,
|
|
187
|
+
this.getHookConfigPath(ctx),
|
|
188
|
+
pending,
|
|
189
|
+
this.hookDescriptor(ctx)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
uninstallHooks(ctx) {
|
|
193
|
+
return this.removeHookEntries(ctx, this.getHookConfigPath(ctx), this.hookDescriptor(ctx));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* OpenHands' hook-merge descriptor (NESTED shape `{ matcher, hooks: [...] }`).
|
|
197
|
+
* Ownership is CONNECTOR-GENERIC (ANY of our commands in the entry's inner
|
|
198
|
+
* `hooks`, not a specific-command match), identical to Droid:
|
|
199
|
+
* - mapEvent = drop events outside SUPPORTED_EVENTS; supported event names are
|
|
200
|
+
* Claude-identical, so they map 1:1.
|
|
201
|
+
* - entryOwnsCommand reuses isOurCommand over the inner commands.
|
|
202
|
+
* - uninstall = inner-strip ONLY (ownsEntryForRemove never-true).
|
|
203
|
+
*/
|
|
204
|
+
hookDescriptor(ctx) {
|
|
205
|
+
return {
|
|
206
|
+
mapEvent: (e) => SUPPORTED_EVENTS.has(e) ? e : void 0,
|
|
207
|
+
unmappedWarnDetail: (e) => `${e} has no OpenHands hook equivalent \u2014 skipped`,
|
|
208
|
+
renderEntry: (_event, matcher, command) => ({
|
|
209
|
+
matcher,
|
|
210
|
+
hooks: [{ type: "command", command }]
|
|
211
|
+
}),
|
|
212
|
+
entryOwnsCommand: (entry, _command) => (entry.hooks ?? []).some((h) => this.isOurCommand(h.command, ctx)),
|
|
213
|
+
ownsEntryForRemove: () => () => false,
|
|
214
|
+
stripInner: (c) => (entry) => {
|
|
215
|
+
const innerBefore = entry.hooks?.length ?? 0;
|
|
216
|
+
const inner = (entry.hooks ?? []).filter((h) => !this.isOurCommand(h.command, c));
|
|
217
|
+
return {
|
|
218
|
+
next: inner.length > 0 ? { matcher: entry.matcher ?? "", hooks: inner } : null,
|
|
219
|
+
removed: innerBefore - inner.length
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
skipDetail: (event) => `hooks.${event} already registered`,
|
|
223
|
+
removeDetail: (event, removed) => `hooks.${event} (${removed})`,
|
|
224
|
+
absentDetail: "no hooks section present",
|
|
225
|
+
noMatchDetail: "no matching hook entries"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** True when a hook command references our home binary AND this connector id
|
|
229
|
+
* (anchored so a shared-prefix id can't collide — see isHomeBinHookCommand). */
|
|
230
|
+
isOurCommand(command, ctx) {
|
|
231
|
+
return isHomeBinHookCommand(command, ctx.homeBinPath, ctx.connector.id);
|
|
232
|
+
}
|
|
233
|
+
// ── Diagnostics ──────────────────────────────────────────────────────────
|
|
234
|
+
getHealthChecks(ctx) {
|
|
235
|
+
const mcpPath = this.getServerConfigPath(ctx);
|
|
236
|
+
const hookPath = this.getHookConfigPath(ctx);
|
|
237
|
+
const connectorId = ctx.connector.id;
|
|
238
|
+
const homeBin = ctx.homeBinPath;
|
|
239
|
+
const hookEvents = ctx.connector.hookEvents;
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
name: `${this.name}: mcp.json present`,
|
|
243
|
+
check: () => existsSync(mcpPath) ? { status: "OK", detail: mcpPath } : { status: "FAIL", detail: `not found: ${mcpPath}` }
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: `${this.name}: server entry registered`,
|
|
247
|
+
check: () => {
|
|
248
|
+
if (!ctx.connector.server) {
|
|
249
|
+
return { status: "OK", detail: "no MCP server declared" };
|
|
250
|
+
}
|
|
251
|
+
const cfg = this.readJson(mcpPath);
|
|
252
|
+
const bucket = cfg?.[MCP_ROOT_KEY];
|
|
253
|
+
if (!cfg || !bucket) {
|
|
254
|
+
return { status: "FAIL", detail: `no ${MCP_ROOT_KEY} in ${mcpPath}` };
|
|
255
|
+
}
|
|
256
|
+
return connectorId in bucket ? { status: "OK", detail: `${MCP_ROOT_KEY}.${connectorId} present` } : {
|
|
257
|
+
status: "FAIL",
|
|
258
|
+
detail: `no ${MCP_ROOT_KEY}.${connectorId} in ${mcpPath}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: `${this.name}: hook command registered`,
|
|
264
|
+
check: () => {
|
|
265
|
+
if (hookEvents.length === 0) {
|
|
266
|
+
return { status: "OK", detail: "no hooks declared" };
|
|
267
|
+
}
|
|
268
|
+
const file = this.readJson(hookPath);
|
|
269
|
+
if (!file) return { status: "FAIL", detail: `cannot read ${hookPath}` };
|
|
270
|
+
const hooks = file.hooks ?? {};
|
|
271
|
+
const registered = Object.values(hooks).some(
|
|
272
|
+
(entries) => (entries ?? []).some(
|
|
273
|
+
(e) => (e.hooks ?? []).some(
|
|
274
|
+
(h) => isHomeBinHookCommand(h.command, homeBin, connectorId)
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
return registered ? { status: "OK", detail: "hook command present" } : { status: "FAIL", detail: `no hook for ${connectorId} in ${hookPath}` };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
// ── Runtime: parse OpenHands stdin JSON → normalized event ─────────────────
|
|
284
|
+
// The wire field names DIVERGE from Claude (event_type / working_dir / message
|
|
285
|
+
// / tool_response-dict) — see the header. Reading Claude's names here would be
|
|
286
|
+
// a silent false-friend (the kimi-code bug class).
|
|
287
|
+
parseEvent(event, raw) {
|
|
288
|
+
const input = raw ?? {};
|
|
289
|
+
const connectorId = typeof input.connector === "string" ? input.connector : "";
|
|
290
|
+
const sessionId = typeof input.session_id === "string" ? input.session_id : "";
|
|
291
|
+
const projectDir = typeof input.working_dir === "string" ? input.working_dir : void 0;
|
|
292
|
+
const base = {
|
|
293
|
+
hostPlatform: HOST,
|
|
294
|
+
connectorId,
|
|
295
|
+
sessionId,
|
|
296
|
+
raw,
|
|
297
|
+
...projectDir !== void 0 ? { projectDir } : {}
|
|
298
|
+
};
|
|
299
|
+
switch (event) {
|
|
300
|
+
case "PreToolUse": {
|
|
301
|
+
const ev = {
|
|
302
|
+
...base,
|
|
303
|
+
toolName: input.tool_name ?? "",
|
|
304
|
+
toolInput: input.tool_input ?? {}
|
|
305
|
+
};
|
|
306
|
+
return ev;
|
|
307
|
+
}
|
|
308
|
+
case "PostToolUse": {
|
|
309
|
+
const ev = {
|
|
310
|
+
...base,
|
|
311
|
+
toolName: input.tool_name ?? "",
|
|
312
|
+
toolInput: input.tool_input ?? {},
|
|
313
|
+
...toolResponseToString(input.tool_response) !== void 0 ? { toolOutput: toolResponseToString(input.tool_response) } : {}
|
|
314
|
+
};
|
|
315
|
+
return ev;
|
|
316
|
+
}
|
|
317
|
+
case "UserPromptSubmit": {
|
|
318
|
+
const ev = {
|
|
319
|
+
...base,
|
|
320
|
+
prompt: typeof input.message === "string" ? input.message : ""
|
|
321
|
+
};
|
|
322
|
+
return ev;
|
|
323
|
+
}
|
|
324
|
+
case "SessionStart": {
|
|
325
|
+
const ev = {
|
|
326
|
+
...base,
|
|
327
|
+
source: normalizeSessionSource(void 0)
|
|
328
|
+
};
|
|
329
|
+
return ev;
|
|
330
|
+
}
|
|
331
|
+
case "SessionEnd": {
|
|
332
|
+
const ev = { ...base };
|
|
333
|
+
return ev;
|
|
334
|
+
}
|
|
335
|
+
case "Stop": {
|
|
336
|
+
const ev = { ...base };
|
|
337
|
+
return ev;
|
|
338
|
+
}
|
|
339
|
+
default: {
|
|
340
|
+
throw new Error(`unsupported openhands hook event: ${String(event)}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// ── Runtime: normalized response → OpenHands native (FLAT JSON) hook reply ──
|
|
345
|
+
// Reply is a FLAT top-level object (NO hookSpecificOutput envelope) parsed by
|
|
346
|
+
// hooks/executor.py: keys `decision` (allow|deny), `reason`, `additionalContext`.
|
|
347
|
+
// There is NO "ask" decision (HookDecision is allow|deny only).
|
|
348
|
+
formatReply(event, response) {
|
|
349
|
+
const decision = response.decision ?? "allow";
|
|
350
|
+
if (event === "SessionEnd") {
|
|
351
|
+
return { exitCode: 0 };
|
|
352
|
+
}
|
|
353
|
+
if (decision === "deny" || decision === "ask") {
|
|
354
|
+
return this.stdout({
|
|
355
|
+
decision: "deny",
|
|
356
|
+
reason: response.reason ?? (decision === "ask" ? "Confirmation required by hook" : "Blocked by hook")
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (decision === "context" && response.additionalContext) {
|
|
360
|
+
return this.stdout({ additionalContext: response.additionalContext });
|
|
361
|
+
}
|
|
362
|
+
return { exitCode: 0 };
|
|
363
|
+
}
|
|
364
|
+
stdout(payload) {
|
|
365
|
+
return { exitCode: 0, stdout: JSON.stringify(payload) };
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
function toolResponseToString(value) {
|
|
369
|
+
if (value == null) return void 0;
|
|
370
|
+
if (typeof value === "string") return value;
|
|
371
|
+
try {
|
|
372
|
+
return JSON.stringify(value);
|
|
373
|
+
} catch {
|
|
374
|
+
return String(value);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var adapter = new OpenHandsAdapter();
|
|
378
|
+
var openhands_default = adapter;
|
|
379
|
+
export {
|
|
380
|
+
OpenHandsAdapter,
|
|
381
|
+
adapter,
|
|
382
|
+
openhands_default as default
|
|
383
|
+
};
|
|
384
|
+
//# sourceMappingURL=openhands-5LNH3BKB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/openhands/index.ts"],"sourcesContent":["/**\n * adapters/openhands — OpenHands (All-Hands-AI, ex-OpenDevin) platform adapter.\n *\n * OpenHands is a **json-stdio** host (the `openhands` CLI, installed via\n * `uv tool install openhands`). It exposes TWO native config surfaces that live\n * in DIFFERENT files, so getServerConfigPath ≠ getHookConfigPath:\n *\n * 1. MCP servers — `mcp.json` (root key `mcpServers`), in the CLI persistence\n * dir, which is USER-scoped only:\n * - persistence dir → ~/.openhands (override: $OPENHANDS_PERSISTENCE_DIR)\n * - file → <persistence>/mcp.json\n * Byte-confirmed from openhands_cli/locations.py (MCP_CONFIG_FILE=\"mcp.json\",\n * get_persistence_dir() → ~/.openhands, env $OPENHANDS_PERSISTENCE_DIR) and\n * openhands_cli/mcp/mcp_utils.py (MCPConfig.from_dict({\"mcpServers\": {}}),\n * config.mcpServers[name]; the error path literally suggests \"create\n * ~/.openhands/mcp.json manually\"). Native server entry shape is FastMCP's\n * StdioMCPServer { command, args, env, transport:\"stdio\", cwd? } and\n * RemoteMCPServer { url, transport, headers } (fastmcp.mcp_config).\n * There is NO project-scoped mcp.json for the CLI — it reads the single\n * persistence-dir file — so installServer ALWAYS targets the user file.\n *\n * 2. Hooks — a SEPARATE `.openhands/hooks.json` (root key `hooks`, the\n * Claude-Code-plugin-compatible wrapper). OpenHands ships a hook system\n * that the SDK explicitly documents as honoring \"Claude Code plugin hook\n * files\" (openhands-sdk/openhands/sdk/hooks/config.py — the\n * `_normalize_hooks_input` validator unwraps {\"hooks\": {...}} and accepts\n * PascalCase keys). HookConfig.load() searches, in order:\n * - <working_dir>/.openhands/hooks.json (project scope)\n * - ~/.openhands/hooks.json (user scope)\n * first-found wins, so BOTH scopes are valid; project takes precedence.\n * Registrations use the Claude NESTED-rule shape:\n * { hooks: { <Event>: [ { matcher?, hooks:[{ type:\"command\", command }] } ] } }\n * which is exactly what the shared hook-merge engine writes.\n *\n * Supported events (Claude-compatible, byte-confirmed from\n * openhands-sdk/openhands/sdk/hooks/types.py HookEventType): PreToolUse,\n * PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. OpenHands has\n * NO Notification / PreCompact / SubagentStop / PermissionRequest /\n * PostToolUseFailure / SubagentStart, so those degrade to a warn/skip at install.\n *\n * Runtime wire DIVERGES from Claude (do NOT assume Claude field names — verified\n * against HookEvent.model_dump_json() with use_enum_values=True in types.py):\n * - `event_type` (PascalCase string) — NOT `hook_event_name`\n * - `tool_name`, `tool_input` — Claude-identical\n * - `tool_response` is a DICT — Claude-identical key, object value\n * - `message` (the prompt text) — NOT `prompt`\n * - `session_id` — Claude-identical\n * - `working_dir` (the cwd) — NOT `cwd`\n * - `metadata` (free dict) — no normalized field\n *\n * Reply protocol (byte-confirmed from hooks/executor.py + the 33_hooks example):\n * exit 0 → success; stdout is parsed as a FLAT JSON object with keys\n * `decision` (\"allow\"|\"deny\"), `reason`, `additionalContext`. There is NO\n * `hookSpecificOutput` envelope and NO \"ask\" decision (HookDecision is\n * allow|deny; ASK is commented-out/future). exit 2 also blocks. So:\n * - deny → {\"decision\":\"deny\",\"reason\":...} (exit 0)\n * - context → {\"additionalContext\":...} (exit 0)\n * - ask → unsupported; degrades to a deny-style block with the reason\n * (OpenHands has no native confirm dialog on the hook wire)\n * canModifyOutput is false (it cannot rewrite already-emitted tool output);\n * canInjectSessionContext is true (additionalContext is honored).\n *\n * Env handling: env/header/url refs are resolved to literals at install time via\n * resolveEnvRefsDeep — the safe default matching the Droid/Kiro/Qwen adapters.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { BaseAdapter, type HookMergeDescriptor } from \"../base.js\";\nimport type { Adapter, HookReply, InstallContext, NormalizedEvent } from \"../spi.js\";\nimport type {\n ChangeRecord,\n DetectedPlatform,\n HealthCheck,\n HookEventName,\n HookParadigm,\n HookResponse,\n PlatformCapabilities,\n PlatformId,\n PostToolUseEvent,\n PreToolUseEvent,\n ServerDef,\n SessionEndEvent,\n SessionStartEvent,\n StopEvent,\n Transport,\n UserPromptSubmitEvent,\n} from \"../../core/types.js\";\nimport { resolveEnvRefsDeep } from \"../../core/interpolate.js\";\nimport {\n buildWrappedStdio,\n isHomeBinHookCommand,\n} from \"../../core/spawn.js\";\nimport { normalizeSessionSource } from \"../claude-code/wire.js\";\n\nconst HOST: PlatformId = \"openhands\";\nconst MCP_ROOT_KEY = \"mcpServers\";\n\n/**\n * Resolve the OpenHands CLI persistence dir. Byte-confirmed from\n * openhands_cli/locations.py: `os.environ.get(\"OPENHANDS_PERSISTENCE_DIR\",\n * os.path.expanduser(\"~/.openhands\"))`. The MCP `mcp.json` lives here regardless\n * of install scope (the CLI has no project-scoped mcp.json).\n */\nfunction persistenceDir(): string {\n return process.env.OPENHANDS_PERSISTENCE_DIR ?? join(homedir(), \".openhands\");\n}\n\n/**\n * Canonical events OpenHands actually fires (HookEventType in types.py). Event\n * names are Claude-identical PascalCase, so each canonical name registers 1:1.\n * OpenHands has no Notification / PreCompact / SubagentStop / PermissionRequest /\n * PostToolUseFailure / SubagentStart — those warn/skip at install time.\n */\nconst SUPPORTED_EVENTS: ReadonlySet<HookEventName> = new Set<HookEventName>([\n \"PreToolUse\",\n \"PostToolUse\",\n \"UserPromptSubmit\",\n \"SessionStart\",\n \"SessionEnd\",\n \"Stop\",\n]);\n\n/**\n * Native MCP server entry shapes OpenHands accepts under `mcpServers` (FastMCP\n * StdioMCPServer / RemoteMCPServer). A stdio entry carries an explicit\n * `transport:\"stdio\"` discriminator; remote transports register a URL.\n */\ninterface OpenHandsStdioServer {\n command: string;\n args?: string[];\n env?: Record<string, string>;\n transport: \"stdio\";\n}\ninterface OpenHandsRemoteServer {\n url: string;\n transport: \"http\" | \"sse\";\n headers?: Record<string, string>;\n}\n\n/** A single OpenHands native hook registration entry (Claude-shaped, nested). */\ninterface OpenHandsHookEntry {\n matcher: string;\n hooks: Array<{ type: \"command\"; command: string }>;\n}\n\n/** The shape of OpenHands' hooks.json (only the parts we touch). */\ninterface OpenHandsHooksFile {\n hooks?: Record<string, OpenHandsHookEntry[]>;\n [key: string]: unknown;\n}\n\n/**\n * Raw OpenHands CLI hook stdin payload. These are the EXACT serialized field\n * names of the SDK's HookEvent (use_enum_values=True), NOT Claude's wire —\n * see the header note on the divergences (`event_type`, `working_dir`,\n * `message`, `tool_response` dict).\n */\ninterface OpenHandsWireInput {\n /** PascalCase event string (\"PreToolUse\", …). */\n event_type?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n /** Tool result is a dict on the OpenHands wire (Claude sends it too). */\n tool_response?: Record<string, unknown> | null;\n /** UserPromptSubmit text rides on `message` (NOT `prompt`). */\n message?: string;\n session_id?: string;\n /** The cwd rides on `working_dir` (NOT `cwd`). */\n working_dir?: string;\n /** Free-form dict; no normalized field. */\n metadata?: Record<string, unknown>;\n /** Injected by the entrypoint so the runtime knows which connector to dispatch. */\n connector?: unknown;\n}\n\nexport class OpenHandsAdapter extends BaseAdapter implements Adapter {\n readonly id: PlatformId = HOST;\n readonly name = \"OpenHands\";\n readonly paradigm: HookParadigm = \"json-stdio\";\n\n readonly capabilities: PlatformCapabilities = {\n // Memory surface: AGENTS.md-first managed block via the BaseAdapter default.\n supportsMemory: true,\n preToolUse: true,\n postToolUse: true,\n userPromptSubmit: true,\n sessionStart: true,\n sessionEnd: true,\n stop: true,\n // OpenHands fires NONE of these on its hook wire (HookEventType has only the\n // six above) — install reports the standard skip-warn for any of them.\n // The two REQUIRED PlatformCapabilities flags below are explicitly false;\n // subagentStop / permissionRequest / postToolUseFailure / subagentStart are\n // optional and stay unset (read as ?? false) — no OpenHands analog.\n preCompact: false,\n notification: false,\n\n // PreToolUse/UserPromptSubmit/Stop can block (exit 2 or {\"decision\":\"deny\"}),\n // but the hook wire cannot rewrite tool ARGS or already-emitted tool OUTPUT.\n canModifyArgs: false,\n canModifyOutput: false,\n // additionalContext on the stdout reply is honored (executor.py parses it).\n canInjectSessionContext: true,\n // OpenHands registers stdio + remote (Streamable HTTP / SSE) MCP servers.\n transports: [\"stdio\", \"http\"],\n // Content surfaces (commands/skills/subagents/statusline/actions) are NOT\n // wired: OpenHands' CLI uses microagents + an interactive TUI rather than a\n // Claude-style file-per-command/skill/subagent tree, and no first-party\n // file layout for those was byte-confirmed. Left unset (honest CEILING).\n };\n\n // ── Detection ────────────────────────────────────────────────────────────\n\n detectInstalled(projectDir: string): DetectedPlatform {\n const userDir = persistenceDir();\n const userMcp = join(userDir, \"mcp.json\");\n const projDir = join(projectDir, \".openhands\");\n const projHooks = join(projDir, \"hooks.json\");\n\n const userInstalled = existsSync(userDir) || existsSync(userMcp);\n const projInstalled = existsSync(projDir) || existsSync(projHooks);\n const installed = userInstalled || projInstalled;\n // MCP only lives at the user persistence dir; project scope only carries\n // hooks. Default the reported scope to user (where the MCP config lives).\n const scope = userInstalled || !projInstalled ? \"user\" : \"project\";\n const configPath = scope === \"user\" ? userMcp : projHooks;\n\n return {\n id: this.id,\n name: this.name,\n installed,\n paradigm: this.paradigm,\n capabilities: this.capabilities,\n configPath,\n scope,\n reason: installed\n ? `found OpenHands config (${scope}) at ${configPath}`\n : `no OpenHands config at ${userDir} or ${projDir}`,\n confidence: installed ? \"high\" : \"low\",\n };\n }\n\n // ── Native paths ─────────────────────────────────────────────────────────\n\n getConfigDir(ctx: InstallContext): string {\n return ctx.scope === \"project\"\n ? join(ctx.projectDir, \".openhands\")\n : persistenceDir();\n }\n\n /**\n * MCP `mcp.json` lives in the CLI persistence dir, which is USER-scoped only —\n * the CLI reads exactly one `<persistence>/mcp.json` and has no project\n * variant. So this ALWAYS resolves to the user persistence dir regardless of\n * ctx.scope (writing a project `.openhands/mcp.json` the CLI never reads would\n * be a silent no-op).\n */\n getServerConfigPath(_ctx: InstallContext): string {\n return join(persistenceDir(), \"mcp.json\");\n }\n\n /**\n * Hooks live in a SEPARATE `.openhands/hooks.json`. HookConfig.load searches\n * <working_dir>/.openhands/hooks.json then ~/.openhands/hooks.json, so both\n * scopes are honored — project scope → <projectDir>/.openhands, user scope →\n * the persistence dir.\n */\n getHookConfigPath(ctx: InstallContext): string {\n return join(this.getConfigDir(ctx), \"hooks.json\");\n }\n\n // ── MCP server install / uninstall ───────────────────────────────────────\n\n installServer(ctx: InstallContext): ChangeRecord[] {\n const { connector } = ctx;\n const override = connector.platforms[HOST]?.server;\n if (!connector.server || override === false) {\n return [\n {\n platform: this.id,\n action: \"skip\",\n detail: connector.server\n ? \"server registration disabled for openhands\"\n : \"connector declares no MCP server\",\n },\n ];\n }\n\n // Shallow-merge any per-platform server override into the base ServerDef.\n const server: ServerDef =\n override && typeof override === \"object\"\n ? { ...connector.server, ...override }\n : connector.server;\n\n const serverPath = this.getServerConfigPath(ctx);\n const entry = this.renderServerEntry(ctx, server);\n\n return [\n this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun),\n ];\n }\n\n uninstallServer(ctx: InstallContext): ChangeRecord[] {\n const serverPath = this.getServerConfigPath(ctx);\n return [\n this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun),\n ];\n }\n\n /** Render a normalized ServerDef into OpenHands' native mcpServers entry. */\n private renderServerEntry(\n ctx: InstallContext,\n server: ServerDef,\n ): OpenHandsStdioServer | OpenHandsRemoteServer {\n const transport: Transport = server.transport;\n\n if (transport === \"stdio\") {\n let command = server.command ?? \"\";\n let args = [...(server.args ?? [])];\n\n // Transparent telemetry wrapping: route the real command through\n // `<homeBin> serve --connector <id> -- <command> <args...>`.\n ({ command, args } = buildWrappedStdio(ctx, server, this.id, command, args));\n\n // Resolve every ${env:VAR} to a literal at install time (safe path).\n const entry: OpenHandsStdioServer = {\n command: resolveEnvRefsDeep(command),\n transport: \"stdio\",\n };\n if (args.length > 0) entry.args = resolveEnvRefsDeep(args);\n const env = this.renderEnv(server.env);\n if (env) entry.env = env;\n return entry;\n }\n\n // http / sse — OpenHands registers a URL (FastMCP RemoteMCPServer). Default\n // a non-stdio transport to \"http\" (the recommended Streamable HTTP form);\n // honor an explicit \"sse\".\n const entry: OpenHandsRemoteServer = {\n url: resolveEnvRefsDeep(server.url ?? \"\"),\n transport: transport === \"sse\" ? \"sse\" : \"http\",\n };\n const headers = this.renderEnv(server.headers);\n if (headers) entry.headers = headers;\n return entry;\n }\n\n /**\n * Render env/header values. Resolve `${env:VAR}` references to literals at\n * install time (the safe path matching Droid).\n */\n private renderEnv(\n env: Record<string, string> | undefined,\n ): Record<string, string> | undefined {\n if (!env || Object.keys(env).length === 0) return undefined;\n return resolveEnvRefsDeep({ ...env });\n }\n\n // ── Hook install / uninstall (separate hooks.json, nested-rule shape) ─────\n\n installHooks(ctx: InstallContext): ChangeRecord[] {\n const { connector } = ctx;\n if (connector.platforms[HOST]?.hooks === false) {\n return [{ platform: this.id, action: \"skip\", detail: \"hooks disabled for openhands\" }];\n }\n if (connector.hookEvents.length === 0) {\n return [{ platform: this.id, action: \"skip\", detail: \"connector declares no hooks\" }];\n }\n\n const pending = connector.hookEvents.map((event) => ({\n event,\n matcher: connector.hooks[event]?.matcher ?? \"\",\n }));\n return this.upsertHookEntries(\n ctx,\n this.getHookConfigPath(ctx),\n pending,\n this.hookDescriptor(ctx),\n );\n }\n\n uninstallHooks(ctx: InstallContext): ChangeRecord[] {\n return this.removeHookEntries(ctx, this.getHookConfigPath(ctx), this.hookDescriptor(ctx));\n }\n\n /**\n * OpenHands' hook-merge descriptor (NESTED shape `{ matcher, hooks: [...] }`).\n * Ownership is CONNECTOR-GENERIC (ANY of our commands in the entry's inner\n * `hooks`, not a specific-command match), identical to Droid:\n * - mapEvent = drop events outside SUPPORTED_EVENTS; supported event names are\n * Claude-identical, so they map 1:1.\n * - entryOwnsCommand reuses isOurCommand over the inner commands.\n * - uninstall = inner-strip ONLY (ownsEntryForRemove never-true).\n */\n private hookDescriptor(ctx: InstallContext): HookMergeDescriptor<OpenHandsHookEntry> {\n return {\n mapEvent: (e) => (SUPPORTED_EVENTS.has(e as HookEventName) ? e : undefined),\n unmappedWarnDetail: (e) => `${e} has no OpenHands hook equivalent — skipped`,\n renderEntry: (_event, matcher, command) => ({\n matcher,\n hooks: [{ type: \"command\", command }],\n }),\n entryOwnsCommand: (entry, _command) =>\n (entry.hooks ?? []).some((h) => this.isOurCommand(h.command, ctx)),\n ownsEntryForRemove: () => () => false,\n stripInner: (c) => (entry) => {\n const innerBefore = entry.hooks?.length ?? 0;\n const inner = (entry.hooks ?? []).filter((h) => !this.isOurCommand(h.command, c));\n return {\n next: inner.length > 0 ? { matcher: entry.matcher ?? \"\", hooks: inner } : null,\n removed: innerBefore - inner.length,\n };\n },\n skipDetail: (event) => `hooks.${event} already registered`,\n removeDetail: (event, removed) => `hooks.${event} (${removed})`,\n absentDetail: \"no hooks section present\",\n noMatchDetail: \"no matching hook entries\",\n };\n }\n\n /** True when a hook command references our home binary AND this connector id\n * (anchored so a shared-prefix id can't collide — see isHomeBinHookCommand). */\n private isOurCommand(command: string | undefined, ctx: InstallContext): boolean {\n return isHomeBinHookCommand(command, ctx.homeBinPath, ctx.connector.id);\n }\n\n // ── Diagnostics ──────────────────────────────────────────────────────────\n\n override getHealthChecks(ctx: InstallContext): readonly HealthCheck[] {\n const mcpPath = this.getServerConfigPath(ctx);\n const hookPath = this.getHookConfigPath(ctx);\n const connectorId = ctx.connector.id;\n const homeBin = ctx.homeBinPath;\n const hookEvents = ctx.connector.hookEvents;\n return [\n {\n name: `${this.name}: mcp.json present`,\n check: () =>\n existsSync(mcpPath)\n ? { status: \"OK\", detail: mcpPath }\n : { status: \"FAIL\", detail: `not found: ${mcpPath}` },\n },\n {\n name: `${this.name}: server entry registered`,\n check: () => {\n if (!ctx.connector.server) {\n return { status: \"OK\", detail: \"no MCP server declared\" };\n }\n const cfg = this.readJson<{ [k: string]: Record<string, unknown> }>(mcpPath);\n const bucket = cfg?.[MCP_ROOT_KEY];\n if (!cfg || !bucket) {\n return { status: \"FAIL\", detail: `no ${MCP_ROOT_KEY} in ${mcpPath}` };\n }\n return connectorId in bucket\n ? { status: \"OK\", detail: `${MCP_ROOT_KEY}.${connectorId} present` }\n : {\n status: \"FAIL\",\n detail: `no ${MCP_ROOT_KEY}.${connectorId} in ${mcpPath}`,\n };\n },\n },\n {\n name: `${this.name}: hook command registered`,\n check: () => {\n if (hookEvents.length === 0) {\n return { status: \"OK\", detail: \"no hooks declared\" };\n }\n const file = this.readJson<OpenHandsHooksFile>(hookPath);\n if (!file) return { status: \"FAIL\", detail: `cannot read ${hookPath}` };\n const hooks = file.hooks ?? {};\n const registered = Object.values(hooks).some((entries) =>\n (entries ?? []).some((e) =>\n (e.hooks ?? []).some((h) =>\n isHomeBinHookCommand(h.command, homeBin, connectorId),\n ),\n ),\n );\n return registered\n ? { status: \"OK\", detail: \"hook command present\" }\n : { status: \"FAIL\", detail: `no hook for ${connectorId} in ${hookPath}` };\n },\n },\n ];\n }\n\n // ── Runtime: parse OpenHands stdin JSON → normalized event ─────────────────\n // The wire field names DIVERGE from Claude (event_type / working_dir / message\n // / tool_response-dict) — see the header. Reading Claude's names here would be\n // a silent false-friend (the kimi-code bug class).\n\n parseEvent(event: HookEventName, raw: unknown): NormalizedEvent {\n const input = (raw ?? {}) as OpenHandsWireInput;\n const connectorId = typeof input.connector === \"string\" ? input.connector : \"\";\n const sessionId = typeof input.session_id === \"string\" ? input.session_id : \"\";\n const projectDir =\n typeof input.working_dir === \"string\" ? input.working_dir : undefined;\n\n const base = {\n hostPlatform: HOST,\n connectorId,\n sessionId,\n raw,\n ...(projectDir !== undefined ? { projectDir } : {}),\n } as const;\n\n switch (event) {\n case \"PreToolUse\": {\n const ev: PreToolUseEvent = {\n ...base,\n toolName: input.tool_name ?? \"\",\n toolInput: input.tool_input ?? {},\n };\n return ev;\n }\n case \"PostToolUse\": {\n const ev: PostToolUseEvent = {\n ...base,\n toolName: input.tool_name ?? \"\",\n toolInput: input.tool_input ?? {},\n ...(toolResponseToString(input.tool_response) !== undefined\n ? { toolOutput: toolResponseToString(input.tool_response) }\n : {}),\n };\n return ev;\n }\n case \"UserPromptSubmit\": {\n // The prompt text rides on `message` (NOT `prompt`).\n const ev: UserPromptSubmitEvent = {\n ...base,\n prompt: typeof input.message === \"string\" ? input.message : \"\",\n };\n return ev;\n }\n case \"SessionStart\": {\n // OpenHands SessionStart carries no documented `source` discriminator on\n // the HookEvent wire; normalizeSessionSource(undefined) → the safe\n // \"startup\" default.\n const ev: SessionStartEvent = {\n ...base,\n source: normalizeSessionSource(undefined),\n };\n return ev;\n }\n case \"SessionEnd\": {\n // No documented `reason` enum on the HookEvent wire — omit the field.\n const ev: SessionEndEvent = { ...base };\n return ev;\n }\n case \"Stop\": {\n // No stop_hook_active loop-guard field on the OpenHands wire.\n const ev: StopEvent = { ...base };\n return ev;\n }\n default: {\n // OpenHands never delivers Notification / PreCompact / SubagentStop /\n // PermissionRequest / PostToolUseFailure / SubagentStart (no native\n // equivalent — see SUPPORTED_EVENTS). Surface it loudly rather than\n // silently mis-parse.\n throw new Error(`unsupported openhands hook event: ${String(event)}`);\n }\n }\n }\n\n // ── Runtime: normalized response → OpenHands native (FLAT JSON) hook reply ──\n // Reply is a FLAT top-level object (NO hookSpecificOutput envelope) parsed by\n // hooks/executor.py: keys `decision` (allow|deny), `reason`, `additionalContext`.\n // There is NO \"ask\" decision (HookDecision is allow|deny only).\n\n formatReply(event: HookEventName, response: HookResponse): HookReply {\n const decision = response.decision ?? \"allow\";\n\n // SessionStart / SessionEnd are observe-only (cannot block); they carry no\n // block surface. SessionStart still honors additionalContext, so it falls\n // through to the `context` branch; SessionEnd is an unconditional no-op.\n if (event === \"SessionEnd\") {\n return { exitCode: 0 };\n }\n\n // deny → block with a reason. OpenHands has no \"ask\" on the hook wire, so an\n // \"ask\" decision degrades to a deny-style block carrying the reason (the\n // closest honest mapping — a hook that wants confirmation blocks and tells\n // the agent why, rather than silently passing through).\n if (decision === \"deny\" || decision === \"ask\") {\n return this.stdout({\n decision: \"deny\",\n reason:\n response.reason ??\n (decision === \"ask\" ? \"Confirmation required by hook\" : \"Blocked by hook\"),\n });\n }\n\n // context → inject soft guidance (OpenHands honors additionalContext).\n if (decision === \"context\" && response.additionalContext) {\n return this.stdout({ additionalContext: response.additionalContext });\n }\n\n // allow / modify (unsupported — exit-code/decision protocol only) / void →\n // pass through with exit 0.\n return { exitCode: 0 };\n }\n\n private stdout(payload: unknown): HookReply {\n return { exitCode: 0, stdout: JSON.stringify(payload) };\n }\n}\n\n/** Coerce an OpenHands PostToolUse `tool_response` (a dict) into a string. */\nfunction toolResponseToString(value: unknown): string | undefined {\n if (value == null) return undefined;\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nexport const adapter = new OpenHandsAdapter();\nexport default adapter;\n"],"mappings":"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","names":["entry"]}
|
|
@@ -6,20 +6,20 @@ import {
|
|
|
6
6
|
isPackageFormat,
|
|
7
7
|
packageConnector,
|
|
8
8
|
packageConnectorAll
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LF76VPGQ.js";
|
|
10
10
|
import "./chunk-NBUEL74R.js";
|
|
11
|
+
import "./chunk-OPXAP3AC.js";
|
|
11
12
|
import {
|
|
12
13
|
fail,
|
|
13
14
|
print
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-7HUUINPB.js";
|
|
15
16
|
import {
|
|
16
17
|
findConnectorConfig,
|
|
17
18
|
loadConnectorFromPath
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
} from "./chunk-NWLRJNXM.js";
|
|
20
|
+
import "./chunk-G4CEELKO.js";
|
|
21
|
+
import "./chunk-25CPWJA7.js";
|
|
21
22
|
import "./chunk-56R2ER7K.js";
|
|
22
|
-
import "./chunk-OPXAP3AC.js";
|
|
23
23
|
import "./chunk-H53QZQJI.js";
|
|
24
24
|
import "./chunk-BEFVJ2OE.js";
|
|
25
25
|
import "./chunk-VVFDU4IX.js";
|
|
@@ -132,4 +132,4 @@ async function run(argv) {
|
|
|
132
132
|
export {
|
|
133
133
|
run
|
|
134
134
|
};
|
|
135
|
-
//# sourceMappingURL=package-
|
|
135
|
+
//# sourceMappingURL=package-2EAIYSJ4.js.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
renderBridgePrelude
|
|
4
|
+
} from "./chunk-FBJQGRET.js";
|
|
2
5
|
import {
|
|
3
6
|
BaseAdapter
|
|
4
7
|
} from "./chunk-VVFDU4IX.js";
|
|
@@ -42,7 +45,13 @@ var PiAdapter = class extends BaseAdapter {
|
|
|
42
45
|
// Content surfaces: prompt-template commands + skills.
|
|
43
46
|
supportsCommands: true,
|
|
44
47
|
supportsSkills: true,
|
|
45
|
-
supportsSubagents: false
|
|
48
|
+
supportsSubagents: false,
|
|
49
|
+
// Action surface: each declared action becomes a user-invokable command
|
|
50
|
+
// registered INSIDE a generated TS extension module (pi.registerCommand),
|
|
51
|
+
// shelling out to the home-bin `action` verb — the SAME mechanism the omp
|
|
52
|
+
// fork uses (omp's action support was inferred FROM pi). supportsActions →
|
|
53
|
+
// the installActions/uninstallActions overrides below.
|
|
54
|
+
supportsActions: true
|
|
46
55
|
};
|
|
47
56
|
// ── Detection ────────────────────────────────────────────────────────────
|
|
48
57
|
detectInstalled(projectDir) {
|
|
@@ -106,6 +115,21 @@ var PiAdapter = class extends BaseAdapter {
|
|
|
106
115
|
skillDir(ctx, name) {
|
|
107
116
|
return join(this.skillsDir(ctx), name);
|
|
108
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Extension package dir Pi loads the action module from, per scope:
|
|
120
|
+
* project scope → <projectDir>/.pi/extensions/<id>/
|
|
121
|
+
* user scope → ~/.pi/agent/extensions/<id>/
|
|
122
|
+
* (mirrors the prompts/skills scope split; the OMP fork uses the analogous
|
|
123
|
+
* <agentDir>/extensions/<id>/ path.)
|
|
124
|
+
*/
|
|
125
|
+
extensionDir(ctx) {
|
|
126
|
+
const base = ctx.scope === "project" ? join(ctx.projectDir, ".pi") : join(homedir(), ".pi", "agent");
|
|
127
|
+
return join(base, "extensions", ctx.connector.id);
|
|
128
|
+
}
|
|
129
|
+
/** The generated action extension entry module: <extensionDir>/index.js. */
|
|
130
|
+
actionEntryPath(ctx) {
|
|
131
|
+
return join(this.extensionDir(ctx), "index.js");
|
|
132
|
+
}
|
|
109
133
|
// ── MCP server (unavailable — Pi has no writable MCP config) ─────────────
|
|
110
134
|
installServer(_ctx) {
|
|
111
135
|
return [
|
|
@@ -261,6 +285,112 @@ var PiAdapter = class extends BaseAdapter {
|
|
|
261
285
|
if (skill.extra) Object.assign(frontmatter, skill.extra);
|
|
262
286
|
return this.renderFrontmatterMd(frontmatter, skill.body);
|
|
263
287
|
}
|
|
288
|
+
// ── Action surface (slash commands in a generated extension module) ───────
|
|
289
|
+
// Each declared action becomes a pi.registerCommand(...) inside a generated,
|
|
290
|
+
// self-contained TS extension module (<extensionDir>/index.js). This mirrors
|
|
291
|
+
// the OMP adapter's emitter near-verbatim (omp wraps Pi; its action support was
|
|
292
|
+
// inferred FROM Pi). UNLIKE omp the module is NOT shared with a hook surface —
|
|
293
|
+
// Pi has no lifecycle hook layer here — so it is actions-only AND
|
|
294
|
+
// uninstallActions owns the teardown directly (no uninstallHooks to delegate to).
|
|
295
|
+
// Idempotent (byte-identical → skip) via writeManagedFile; honors
|
|
296
|
+
// platforms["pi"].actions === false and the empty-actions skip.
|
|
297
|
+
installActions(ctx) {
|
|
298
|
+
const entryPath = this.actionEntryPath(ctx);
|
|
299
|
+
if (ctx.connector.platforms[HOST]?.actions === false) {
|
|
300
|
+
return [{ platform: this.id, action: "skip", path: entryPath, detail: "actions disabled for pi" }];
|
|
301
|
+
}
|
|
302
|
+
if (this.actionTriggers(ctx).length === 0) {
|
|
303
|
+
return [{ platform: this.id, action: "skip", path: entryPath, detail: "connector declares no actions" }];
|
|
304
|
+
}
|
|
305
|
+
return this.synthesizeExtension(ctx).map(
|
|
306
|
+
(file) => this.writeManagedFile(
|
|
307
|
+
file.path,
|
|
308
|
+
file.contents,
|
|
309
|
+
ctx.dryRun,
|
|
310
|
+
"pi extension module (action commands)",
|
|
311
|
+
file.executable
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
uninstallActions(ctx) {
|
|
316
|
+
const entryPath = this.actionEntryPath(ctx);
|
|
317
|
+
if (!existsSync(entryPath)) {
|
|
318
|
+
return [{ platform: this.id, action: "skip", path: entryPath, detail: "no pi extension present" }];
|
|
319
|
+
}
|
|
320
|
+
const changes = [
|
|
321
|
+
this.removeManagedFile(entryPath, ctx.dryRun, "pi extension module", "no pi extension module present")
|
|
322
|
+
];
|
|
323
|
+
changes.push(this.removeDirIfEmpty(this.extensionDir(ctx), ctx.dryRun));
|
|
324
|
+
return changes;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Build the Pi action extension: a single self-contained ESM module
|
|
328
|
+
* (<extensionDir>/index.js) whose default export is the Pi plugin factory
|
|
329
|
+
* `(pi) => void`. It imports nothing from agent-connector. For each declared
|
|
330
|
+
* action it calls `pi.registerCommand(action.id, { handler })`; the handler
|
|
331
|
+
* shells out to the stable home binary's `action pi <id>` verb over
|
|
332
|
+
* child_process (fail-open). Mirrors the OMP adapter's registerCommand emission
|
|
333
|
+
* (the shared renderBridgePrelude head + the per-action block), minus the hook
|
|
334
|
+
* handlers Pi has no surface for.
|
|
335
|
+
*/
|
|
336
|
+
synthesizeExtension(ctx) {
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
path: this.actionEntryPath(ctx),
|
|
340
|
+
contents: this.buildExtensionSource(ctx),
|
|
341
|
+
executable: false
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
/** Compose the generated extension source with plain string concatenation. */
|
|
346
|
+
buildExtensionSource(ctx) {
|
|
347
|
+
const header = `/**
|
|
348
|
+
* AUTO-GENERATED by agent-connector \u2014 DO NOT EDIT.
|
|
349
|
+
*
|
|
350
|
+
* Self-contained Pi extension for connector ${ctx.connector.id}. It imports
|
|
351
|
+
* nothing from agent-connector: each registered command shells out to the stable
|
|
352
|
+
* home binary's universal action entrypoint. Fail-open: any launcher error
|
|
353
|
+
* degrades to a no-op (never wedges Pi).
|
|
354
|
+
*
|
|
355
|
+
* Pi loads this module and calls its DEFAULT export, the plugin factory
|
|
356
|
+
* (pi) => void, once at load time; each pi.registerCommand(...) wires one
|
|
357
|
+
* user-invokable command.
|
|
358
|
+
*/
|
|
359
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
360
|
+
|
|
361
|
+
${renderBridgePrelude({
|
|
362
|
+
homeBin: ctx.homeBinPath,
|
|
363
|
+
connectorId: ctx.connector.id,
|
|
364
|
+
hookSlug: "pi",
|
|
365
|
+
payloadNoun: "Pi",
|
|
366
|
+
failOpenComment: "Fail-open \u2014 never break a Pi command invocation."
|
|
367
|
+
})}
|
|
368
|
+
`;
|
|
369
|
+
const actionHandlers = [];
|
|
370
|
+
for (const action of this.actionTriggers(ctx)) {
|
|
371
|
+
const id = JSON.stringify(action.id);
|
|
372
|
+
const description = JSON.stringify(action.description);
|
|
373
|
+
actionHandlers.push(` // Action /${action.id} \u2192 run the home-bin action verb directly.
|
|
374
|
+
pi.registerCommand(${id}, {
|
|
375
|
+
description: ${description},
|
|
376
|
+
handler: async (_args, _ctx) => {
|
|
377
|
+
try {
|
|
378
|
+
const args = ["action", "pi", ${id}, "--connector", CONNECTOR_ID];
|
|
379
|
+
process.platform === "win32"
|
|
380
|
+
? execSync([HOME_BIN, ...args].map((a) => '"' + a + '"').join(" "), { stdio: "inherit" })
|
|
381
|
+
: execFileSync(HOME_BIN, args, { stdio: "inherit" });
|
|
382
|
+
} catch { /* fail-open */ }
|
|
383
|
+
return undefined;
|
|
384
|
+
},
|
|
385
|
+
});`);
|
|
386
|
+
}
|
|
387
|
+
const factory = `
|
|
388
|
+
export default function plugin(pi) {
|
|
389
|
+
${actionHandlers.join("\n\n")}
|
|
390
|
+
}
|
|
391
|
+
`;
|
|
392
|
+
return header + factory;
|
|
393
|
+
}
|
|
264
394
|
// ── Diagnostics ──────────────────────────────────────────────────────────
|
|
265
395
|
getHealthChecks(ctx) {
|
|
266
396
|
const checks = [];
|
|
@@ -278,6 +408,13 @@ var PiAdapter = class extends BaseAdapter {
|
|
|
278
408
|
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
279
409
|
});
|
|
280
410
|
}
|
|
411
|
+
if (this.actionTriggers(ctx).length > 0 && ctx.connector.platforms[HOST]?.actions !== false) {
|
|
412
|
+
const p = this.actionEntryPath(ctx);
|
|
413
|
+
checks.push({
|
|
414
|
+
name: `${this.name}: action extension present`,
|
|
415
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
281
418
|
return checks;
|
|
282
419
|
}
|
|
283
420
|
};
|
|
@@ -288,4 +425,4 @@ export {
|
|
|
288
425
|
adapter,
|
|
289
426
|
pi_default as default
|
|
290
427
|
};
|
|
291
|
-
//# sourceMappingURL=pi-
|
|
428
|
+
//# sourceMappingURL=pi-BUADR5MJ.js.map
|