@ken-jo/agent-connector 0.1.0 → 0.2.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/README.md +176 -24
- package/dist/{amp-QSFH2V7Y.js → amp-YGV66GJT.js} +9 -2
- package/dist/amp-YGV66GJT.js.map +1 -0
- package/dist/{antigravity-CX2YLSY7.js → antigravity-7IBYFO6P.js} +4 -3
- package/dist/{antigravity-cli-YQDFVV4U.js → antigravity-cli-JEOUP2VN.js} +4 -3
- package/dist/{antigravity-cli-YQDFVV4U.js.map → antigravity-cli-JEOUP2VN.js.map} +1 -1
- package/dist/{chunk-LTT56GI3.js → chunk-3JCRUROU.js} +18 -5
- package/dist/chunk-3JCRUROU.js.map +1 -0
- package/dist/{chunk-YNZU6PAV.js → chunk-4ML7SZ7R.js} +69 -16
- package/dist/chunk-4ML7SZ7R.js.map +1 -0
- package/dist/{chunk-54EMKTTX.js → chunk-AKJGNTU3.js} +17 -17
- package/dist/chunk-AKJGNTU3.js.map +1 -0
- package/dist/chunk-GSGDG2BV.js +568 -0
- package/dist/chunk-GSGDG2BV.js.map +1 -0
- package/dist/{chunk-TRKWUNO4.js → chunk-KQSNDFBN.js} +13 -2
- package/dist/chunk-KQSNDFBN.js.map +1 -0
- package/dist/chunk-N6HH7K6P.js +494 -0
- package/dist/chunk-N6HH7K6P.js.map +1 -0
- package/dist/{chunk-XUCWLJY5.js → chunk-O2SBA5KR.js} +30 -30
- package/dist/{chunk-TIIP3RG2.js → chunk-PD4VFEBY.js} +342 -2
- package/dist/chunk-PD4VFEBY.js.map +1 -0
- package/dist/{chunk-WV63X7IK.js → chunk-SCVBYSF7.js} +2 -2
- package/dist/chunk-SCVBYSF7.js.map +1 -0
- package/dist/{chunk-7MSDGHJW.js → chunk-Z2BVYKZL.js} +60 -7
- package/dist/chunk-Z2BVYKZL.js.map +1 -0
- package/dist/claude-code-4275HQU4.js +1432 -0
- package/dist/claude-code-4275HQU4.js.map +1 -0
- package/dist/cli/sdk.js +4 -3
- package/dist/cli/sdk.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/{codebuff-QX57FJDN.js → codebuff-LE3KBZGM.js} +9 -2
- package/dist/codebuff-LE3KBZGM.js.map +1 -0
- package/dist/{codex-JFQTS6LU.js → codex-4BY2ZJIW.js} +148 -7
- package/dist/codex-4BY2ZJIW.js.map +1 -0
- package/dist/{copilot-cli-MTCGHZ2T.js → copilot-cli-IPISKO3V.js} +109 -2
- package/dist/copilot-cli-IPISKO3V.js.map +1 -0
- package/dist/{crush-I7PK3FGI.js → crush-X3E25EQG.js} +31 -3
- package/dist/crush-X3E25EQG.js.map +1 -0
- package/dist/{cursor-4TMXKJOZ.js → cursor-UH3W622V.js} +66 -5
- package/dist/cursor-UH3W622V.js.map +1 -0
- package/dist/{detect-WIAGVYS4.js → detect-JK7T46N6.js} +7 -3
- package/dist/detect-JK7T46N6.js.map +1 -0
- package/dist/{doctor-EZW2K4NI.js → doctor-62BHIXOR.js} +13 -5
- package/dist/doctor-62BHIXOR.js.map +1 -0
- package/dist/{droid-KN6VRMMT.js → droid-T5VBRXZF.js} +32 -3
- package/dist/droid-T5VBRXZF.js.map +1 -0
- package/dist/{gemini-cli-Y35FFK4M.js → gemini-cli-ZSUK55UA.js} +51 -3
- package/dist/gemini-cli-ZSUK55UA.js.map +1 -0
- package/dist/{goose-ZTGTSUFJ.js → goose-M3RR3CZG.js} +63 -4
- package/dist/goose-M3RR3CZG.js.map +1 -0
- package/dist/{hermes-H2KV44GK.js → hermes-CUNKIAR5.js} +76 -3
- package/dist/hermes-CUNKIAR5.js.map +1 -0
- package/dist/{hook-YVDILAMZ.js → hook-AC7APC66.js} +26 -11
- package/dist/hook-AC7APC66.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/{install-HQRYZOQ2.js → install-DMW2C7SV.js} +12 -7
- package/dist/install-DMW2C7SV.js.map +1 -0
- package/dist/{jetbrains-copilot-HWA3DMH7.js → jetbrains-copilot-C3HDGGML.js} +16 -2
- package/dist/jetbrains-copilot-C3HDGGML.js.map +1 -0
- package/dist/{kilo-QXJRCSLT.js → kilo-ISR5FBBJ.js} +26 -2
- package/dist/kilo-ISR5FBBJ.js.map +1 -0
- package/dist/{kilo-cli-EAQTHXUW.js → kilo-cli-2RNODHWY.js} +30 -2
- package/dist/kilo-cli-2RNODHWY.js.map +1 -0
- package/dist/{kimi-GQ7VXNYT.js → kimi-Q5RG7CYT.js} +89 -6
- package/dist/kimi-Q5RG7CYT.js.map +1 -0
- package/dist/{kiro-VQTT6LO2.js → kiro-EKTRW424.js} +31 -2
- package/dist/kiro-EKTRW424.js.map +1 -0
- package/dist/{leaderboard-K6ON3SWW.js → leaderboard-R5HRJHNU.js} +3 -3
- package/dist/{mux-T2TV4CIQ.js → mux-5CI5FP2W.js} +9 -2
- package/dist/mux-5CI5FP2W.js.map +1 -0
- package/dist/{omp-PB4VDKS6.js → omp-AXA7XY22.js} +13 -2
- package/dist/omp-AXA7XY22.js.map +1 -0
- package/dist/{openclaw-ETN2M63O.js → openclaw-UZ5BMCGQ.js} +101 -4
- package/dist/openclaw-UZ5BMCGQ.js.map +1 -0
- package/dist/{opencode-FZ33JKUZ.js → opencode-NZBRLJN3.js} +53 -7
- package/dist/opencode-NZBRLJN3.js.map +1 -0
- package/dist/{package-DLOVE2E4.js → package-O63K4S7T.js} +10 -5
- package/dist/package-O63K4S7T.js.map +1 -0
- package/dist/{pi-CIU2QHT4.js → pi-SJPIL4VE.js} +6 -2
- package/dist/pi-SJPIL4VE.js.map +1 -0
- package/dist/{qwen-code-5POCNU6P.js → qwen-code-IFCM6BJY.js} +110 -2
- package/dist/qwen-code-IFCM6BJY.js.map +1 -0
- package/dist/{roo-code-IOIUXRDE.js → roo-code-NI5UU6JB.js} +26 -2
- package/dist/roo-code-NI5UU6JB.js.map +1 -0
- package/dist/runtime/index.d.ts +38 -4
- package/dist/runtime/index.js +9 -4
- package/dist/{serve-HVWWY4B6.js → serve-LFOUSZAD.js} +7 -6
- package/dist/{serve-HVWWY4B6.js.map → serve-LFOUSZAD.js.map} +1 -1
- package/dist/{status-FE7TECZD.js → status-M7JLZVLC.js} +6 -5
- package/dist/{status-FE7TECZD.js.map → status-M7JLZVLC.js.map} +1 -1
- package/dist/{telemetry-36GESKQQ.js → telemetry-3NVEXPYX.js} +5 -4
- package/dist/{telemetry-36GESKQQ.js.map → telemetry-3NVEXPYX.js.map} +1 -1
- package/dist/{trae-JSNDZFKL.js → trae-CQC72RHH.js} +9 -2
- package/dist/trae-CQC72RHH.js.map +1 -0
- package/dist/types-CNigfEuR.d.ts +757 -0
- package/dist/{uninstall-IGCKNOSF.js → uninstall-H6FGMIHN.js} +7 -6
- package/dist/{uninstall-IGCKNOSF.js.map → uninstall-H6FGMIHN.js.map} +1 -1
- package/dist/{upgrade-JV2VK242.js → upgrade-76BQZBMA.js} +7 -6
- package/dist/{upgrade-JV2VK242.js.map → upgrade-76BQZBMA.js.map} +1 -1
- package/dist/{usage-6ER6EMEY.js → usage-G3GSCX23.js} +3 -3
- package/dist/{usage-event-JKBI6XCF.js → usage-event-QUTMNBS5.js} +6 -5
- package/dist/{usage-event-JKBI6XCF.js.map → usage-event-QUTMNBS5.js.map} +1 -1
- package/dist/{vscode-copilot-CK3NO54M.js → vscode-copilot-4RVRTCKV.js} +53 -4
- package/dist/vscode-copilot-4RVRTCKV.js.map +1 -0
- package/dist/{warp-QNVDXOG4.js → warp-LLOSXR7A.js} +30 -2
- package/dist/warp-LLOSXR7A.js.map +1 -0
- package/dist/{zed-VRJKWYUR.js → zed-HYOITK7F.js} +53 -3
- package/dist/zed-HYOITK7F.js.map +1 -0
- package/package.json +4 -3
- package/dist/amp-QSFH2V7Y.js.map +0 -1
- package/dist/chunk-54EMKTTX.js.map +0 -1
- package/dist/chunk-7MSDGHJW.js.map +0 -1
- package/dist/chunk-LTT56GI3.js.map +0 -1
- package/dist/chunk-TIIP3RG2.js.map +0 -1
- package/dist/chunk-TRKWUNO4.js.map +0 -1
- package/dist/chunk-WV63X7IK.js.map +0 -1
- package/dist/chunk-XNAM7F35.js +0 -276
- package/dist/chunk-XNAM7F35.js.map +0 -1
- package/dist/chunk-YNZU6PAV.js.map +0 -1
- package/dist/claude-code-HKDV7T32.js +0 -637
- package/dist/claude-code-HKDV7T32.js.map +0 -1
- package/dist/codebuff-QX57FJDN.js.map +0 -1
- package/dist/codex-JFQTS6LU.js.map +0 -1
- package/dist/copilot-cli-MTCGHZ2T.js.map +0 -1
- package/dist/crush-I7PK3FGI.js.map +0 -1
- package/dist/cursor-4TMXKJOZ.js.map +0 -1
- package/dist/detect-WIAGVYS4.js.map +0 -1
- package/dist/doctor-EZW2K4NI.js.map +0 -1
- package/dist/droid-KN6VRMMT.js.map +0 -1
- package/dist/gemini-cli-Y35FFK4M.js.map +0 -1
- package/dist/goose-ZTGTSUFJ.js.map +0 -1
- package/dist/hermes-H2KV44GK.js.map +0 -1
- package/dist/hook-YVDILAMZ.js.map +0 -1
- package/dist/install-HQRYZOQ2.js.map +0 -1
- package/dist/jetbrains-copilot-HWA3DMH7.js.map +0 -1
- package/dist/kilo-QXJRCSLT.js.map +0 -1
- package/dist/kilo-cli-EAQTHXUW.js.map +0 -1
- package/dist/kimi-GQ7VXNYT.js.map +0 -1
- package/dist/kiro-VQTT6LO2.js.map +0 -1
- package/dist/mux-T2TV4CIQ.js.map +0 -1
- package/dist/omp-PB4VDKS6.js.map +0 -1
- package/dist/openclaw-ETN2M63O.js.map +0 -1
- package/dist/opencode-FZ33JKUZ.js.map +0 -1
- package/dist/package-DLOVE2E4.js.map +0 -1
- package/dist/pi-CIU2QHT4.js.map +0 -1
- package/dist/qwen-code-5POCNU6P.js.map +0 -1
- package/dist/roo-code-IOIUXRDE.js.map +0 -1
- package/dist/trae-JSNDZFKL.js.map +0 -1
- package/dist/types-CYUB1OTr.d.ts +0 -427
- package/dist/vscode-copilot-CK3NO54M.js.map +0 -1
- package/dist/warp-QNVDXOG4.js.map +0 -1
- package/dist/zed-VRJKWYUR.js.map +0 -1
- /package/dist/{antigravity-CX2YLSY7.js.map → antigravity-7IBYFO6P.js.map} +0 -0
- /package/dist/{chunk-XUCWLJY5.js.map → chunk-O2SBA5KR.js.map} +0 -0
- /package/dist/{leaderboard-K6ON3SWW.js.map → leaderboard-R5HRJHNU.js.map} +0 -0
- /package/dist/{usage-6ER6EMEY.js.map → usage-G3GSCX23.js.map} +0 -0
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readRegisteredMeta
|
|
4
|
+
} from "./chunk-3JCRUROU.js";
|
|
5
|
+
import {
|
|
6
|
+
addLedgerOwner,
|
|
7
|
+
configPatchManualEdit,
|
|
8
|
+
configPatchNamespaceViolation,
|
|
9
|
+
createLedgerEntry,
|
|
10
|
+
describeJsonValue,
|
|
11
|
+
dropLedgerEntry,
|
|
12
|
+
findLedgerEntry,
|
|
13
|
+
hashJsonValue,
|
|
14
|
+
isValidConfigPatchKey,
|
|
15
|
+
jsonDeepEquals,
|
|
16
|
+
ledgerEntriesOwnedBy,
|
|
17
|
+
loadConfigPatchLedger,
|
|
18
|
+
removeLedgerOwner,
|
|
19
|
+
saveConfigPatchLedger
|
|
20
|
+
} from "./chunk-GSGDG2BV.js";
|
|
21
|
+
import {
|
|
22
|
+
resolveEnvRefsDeep,
|
|
23
|
+
rewriteEnvRefs
|
|
24
|
+
} from "./chunk-DBQMX75U.js";
|
|
25
|
+
import {
|
|
26
|
+
BaseAdapter
|
|
27
|
+
} from "./chunk-PD4VFEBY.js";
|
|
28
|
+
import {
|
|
29
|
+
renderCommandMd,
|
|
30
|
+
renderSkillMd,
|
|
31
|
+
renderSubagentMd
|
|
32
|
+
} from "./chunk-7L6ITC4T.js";
|
|
33
|
+
import {
|
|
34
|
+
buildHomeBinHookCommand,
|
|
35
|
+
buildServeWrapperCommand,
|
|
36
|
+
isHomeBinHookCommand,
|
|
37
|
+
shouldWrapForTelemetry
|
|
38
|
+
} from "./chunk-MEH7GC7R.js";
|
|
39
|
+
import {
|
|
40
|
+
linesOutsideFences,
|
|
41
|
+
listManagedBlocks,
|
|
42
|
+
loadMemoryLedger,
|
|
43
|
+
recordMemoryTarget,
|
|
44
|
+
removeBlocksFromText,
|
|
45
|
+
removeManagedBlocksFile,
|
|
46
|
+
saveMemoryLedger,
|
|
47
|
+
upsertManagedBlockFile
|
|
48
|
+
} from "./chunk-N6HH7K6P.js";
|
|
49
|
+
import {
|
|
50
|
+
backupsDir,
|
|
51
|
+
ensureDir
|
|
52
|
+
} from "./chunk-TRQKHP7D.js";
|
|
53
|
+
|
|
54
|
+
// src/adapters/claude-code/index.ts
|
|
55
|
+
import { copyFileSync, existsSync, lstatSync, readFileSync, realpathSync } from "fs";
|
|
56
|
+
import { homedir } from "os";
|
|
57
|
+
import { basename, dirname, join } from "path";
|
|
58
|
+
|
|
59
|
+
// src/adapters/claude-code/wire.ts
|
|
60
|
+
function extractSessionId(input) {
|
|
61
|
+
if (typeof input.transcript_path === "string") {
|
|
62
|
+
const m = input.transcript_path.match(/([a-f0-9-]{36})\.jsonl$/);
|
|
63
|
+
if (m && m[1]) return m[1];
|
|
64
|
+
}
|
|
65
|
+
if (typeof input.session_id === "string" && input.session_id !== "") {
|
|
66
|
+
return input.session_id;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
function toolResponseToString(value) {
|
|
71
|
+
if (value == null) return void 0;
|
|
72
|
+
if (typeof value === "string") return value;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
} catch {
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/adapters/claude-code/index.ts
|
|
81
|
+
var HOST = "claude-code";
|
|
82
|
+
var MCP_ROOT_KEY = "mcpServers";
|
|
83
|
+
var CLAUDE_AGENTS_IMPORT_BLOCK_ID = "_shared/claude-agents-import";
|
|
84
|
+
var ClaudeCodeAdapter = class extends BaseAdapter {
|
|
85
|
+
id = HOST;
|
|
86
|
+
name = "Claude Code";
|
|
87
|
+
paradigm = "json-stdio";
|
|
88
|
+
capabilities = {
|
|
89
|
+
preToolUse: true,
|
|
90
|
+
postToolUse: true,
|
|
91
|
+
preCompact: true,
|
|
92
|
+
sessionStart: true,
|
|
93
|
+
sessionEnd: true,
|
|
94
|
+
userPromptSubmit: true,
|
|
95
|
+
stop: true,
|
|
96
|
+
notification: true,
|
|
97
|
+
// Newer events — Claude Code is the reference host for all four.
|
|
98
|
+
permissionRequest: true,
|
|
99
|
+
postToolUseFailure: true,
|
|
100
|
+
subagentStart: true,
|
|
101
|
+
subagentStop: true,
|
|
102
|
+
// Claude Code can rewrite PreToolUse input (updatedInput) but does NOT let a
|
|
103
|
+
// PostToolUse hook rewrite already-emitted tool output.
|
|
104
|
+
canModifyArgs: true,
|
|
105
|
+
canModifyOutput: false,
|
|
106
|
+
canInjectSessionContext: true,
|
|
107
|
+
// Native passthrough hooks: settings.json hook keys are free-form event
|
|
108
|
+
// names, so any host event (TaskCreated, TeammateIdle, WorktreeCreate, …)
|
|
109
|
+
// declared under platforms["claude-code"].nativeHooks installs verbatim.
|
|
110
|
+
supportsNativeHooks: true,
|
|
111
|
+
// Declarative host-config key patches: claude-code is the ONLY v1 host
|
|
112
|
+
// (set-if-absent leaf keys in settings.json, refcounted ownership ledger,
|
|
113
|
+
// sensitive-key denylist — see SENSITIVE_KEY_RULES below).
|
|
114
|
+
supportsConfigPatch: true,
|
|
115
|
+
transports: ["stdio", "http"],
|
|
116
|
+
// Content surfaces: Claude Code is the reference implementation for all three.
|
|
117
|
+
supportsCommands: true,
|
|
118
|
+
supportsSkills: true,
|
|
119
|
+
supportsSubagents: true,
|
|
120
|
+
// Memory surface — EXCEPTION host: Claude Code reads CLAUDE.md, NOT
|
|
121
|
+
// AGENTS.md (docs are explicit; no AGENTS.md support through v2.1.172), so
|
|
122
|
+
// memoryTargets/installMemory below override the base AGENTS.md default.
|
|
123
|
+
supportsMemory: true
|
|
124
|
+
};
|
|
125
|
+
// ── Detection ────────────────────────────────────────────────────────────
|
|
126
|
+
detectInstalled(projectDir) {
|
|
127
|
+
const userDir = join(homedir(), ".claude");
|
|
128
|
+
const userSettings = join(userDir, "settings.json");
|
|
129
|
+
const userServers = join(homedir(), ".claude.json");
|
|
130
|
+
const projectServers = join(projectDir, ".mcp.json");
|
|
131
|
+
const userInstalled = existsSync(userDir) || existsSync(userSettings) || existsSync(userServers);
|
|
132
|
+
const projInstalled = existsSync(projectServers);
|
|
133
|
+
const installed = userInstalled || projInstalled;
|
|
134
|
+
const scope = projInstalled && !userInstalled ? "project" : "user";
|
|
135
|
+
const configPath = scope === "project" ? projectServers : userSettings;
|
|
136
|
+
return {
|
|
137
|
+
id: this.id,
|
|
138
|
+
name: this.name,
|
|
139
|
+
installed,
|
|
140
|
+
paradigm: this.paradigm,
|
|
141
|
+
capabilities: this.capabilities,
|
|
142
|
+
configPath,
|
|
143
|
+
scope,
|
|
144
|
+
reason: installed ? scope === "project" ? `found project Claude Code config at ${projectServers}` : `found Claude Code config under ${userDir}` : `no Claude Code config at ${userDir}`,
|
|
145
|
+
confidence: installed ? "high" : "low"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ── Native paths ─────────────────────────────────────────────────────────
|
|
149
|
+
getConfigDir(ctx) {
|
|
150
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".claude") : join(homedir(), ".claude");
|
|
151
|
+
}
|
|
152
|
+
getServerConfigPath(ctx) {
|
|
153
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".mcp.json") : join(homedir(), ".claude.json");
|
|
154
|
+
}
|
|
155
|
+
getHookConfigPath(ctx) {
|
|
156
|
+
return join(this.getConfigDir(ctx), "settings.json");
|
|
157
|
+
}
|
|
158
|
+
// ── MCP server install / uninstall ───────────────────────────────────────
|
|
159
|
+
installServer(ctx) {
|
|
160
|
+
const { connector } = ctx;
|
|
161
|
+
const override = connector.platforms[HOST]?.server;
|
|
162
|
+
if (!connector.server || override === false) {
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
platform: this.id,
|
|
166
|
+
action: "skip",
|
|
167
|
+
detail: connector.server ? "server registration disabled for claude-code" : "connector declares no MCP server"
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
const server = override && typeof override === "object" ? { ...connector.server, ...override } : connector.server;
|
|
172
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
173
|
+
const entry = this.renderServerEntry(ctx, server);
|
|
174
|
+
return [
|
|
175
|
+
this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun)
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
uninstallServer(ctx) {
|
|
179
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
180
|
+
return [
|
|
181
|
+
this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun)
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
/** Render a normalized ServerDef into Claude Code's native mcpServers entry. */
|
|
185
|
+
renderServerEntry(ctx, server) {
|
|
186
|
+
const transport = server.transport;
|
|
187
|
+
if (transport === "stdio") {
|
|
188
|
+
let command = server.command ?? "";
|
|
189
|
+
let args = [...server.args ?? []];
|
|
190
|
+
if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {
|
|
191
|
+
const wrapped = buildServeWrapperCommand(
|
|
192
|
+
ctx.homeBinPath,
|
|
193
|
+
ctx.connector.id,
|
|
194
|
+
command,
|
|
195
|
+
args,
|
|
196
|
+
ctx.scope,
|
|
197
|
+
this.id
|
|
198
|
+
);
|
|
199
|
+
command = wrapped.command;
|
|
200
|
+
args = wrapped.args;
|
|
201
|
+
}
|
|
202
|
+
const entry2 = { type: "stdio", command, args };
|
|
203
|
+
const env = this.renderEnv(server.env);
|
|
204
|
+
if (env) entry2.env = env;
|
|
205
|
+
if (server.cwd) entry2.cwd = resolveEnvRefsDeep(server.cwd);
|
|
206
|
+
return entry2;
|
|
207
|
+
}
|
|
208
|
+
const entry = {
|
|
209
|
+
type: "http",
|
|
210
|
+
url: rewriteEnvRefs(server.url ?? "", claudeEnvToken)
|
|
211
|
+
};
|
|
212
|
+
if (server.headers) {
|
|
213
|
+
entry.headers = this.renderEnv(server.headers) ?? {};
|
|
214
|
+
}
|
|
215
|
+
return entry;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Render env/header values. Claude Code supports its own `${VAR}` native
|
|
219
|
+
* interpolation, so translate `${env:VAR}` refs to that native token rather
|
|
220
|
+
* than baking secrets into the file. Literals pass through unchanged.
|
|
221
|
+
*/
|
|
222
|
+
renderEnv(env) {
|
|
223
|
+
if (!env || Object.keys(env).length === 0) return void 0;
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const [k, v] of Object.entries(env)) {
|
|
226
|
+
out[k] = rewriteEnvRefs(v, claudeEnvToken);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
// ── Hook install / uninstall ─────────────────────────────────────────────
|
|
231
|
+
installHooks(ctx) {
|
|
232
|
+
const { connector } = ctx;
|
|
233
|
+
const override = connector.platforms[HOST];
|
|
234
|
+
const normalizedEvents = override?.hooks === false ? [] : connector.hookEvents;
|
|
235
|
+
const nativeHooks = override?.nativeHooks ?? {};
|
|
236
|
+
const nativeEvents = Object.keys(nativeHooks);
|
|
237
|
+
if (normalizedEvents.length === 0 && nativeEvents.length === 0) {
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
platform: this.id,
|
|
241
|
+
action: "skip",
|
|
242
|
+
detail: override?.hooks === false ? "hooks disabled for claude-code" : "connector declares no hooks"
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
const settingsPath = this.getHookConfigPath(ctx);
|
|
247
|
+
const settings = this.readJson(settingsPath) ?? {};
|
|
248
|
+
const hooks = settings.hooks ??= {};
|
|
249
|
+
const changes = [];
|
|
250
|
+
let mutated = false;
|
|
251
|
+
const pending = [
|
|
252
|
+
...normalizedEvents.map((event) => ({
|
|
253
|
+
event,
|
|
254
|
+
matcher: connector.hooks[event]?.matcher ?? ""
|
|
255
|
+
})),
|
|
256
|
+
...nativeEvents.map((event) => ({
|
|
257
|
+
event,
|
|
258
|
+
matcher: nativeHooks[event]?.matcher ?? ""
|
|
259
|
+
}))
|
|
260
|
+
];
|
|
261
|
+
for (const { event, matcher } of pending) {
|
|
262
|
+
const command = buildHomeBinHookCommand(ctx.homeBinPath, HOST, event, connector.id);
|
|
263
|
+
const entry = {
|
|
264
|
+
matcher,
|
|
265
|
+
hooks: [{ type: "command", command }]
|
|
266
|
+
};
|
|
267
|
+
const bucket = hooks[event] ??= [];
|
|
268
|
+
const existingIdx = bucket.findIndex((e) => this.entryHasCommand(e, command));
|
|
269
|
+
if (existingIdx >= 0) {
|
|
270
|
+
if (JSON.stringify(bucket[existingIdx]) === JSON.stringify(entry)) {
|
|
271
|
+
changes.push({
|
|
272
|
+
platform: this.id,
|
|
273
|
+
action: "skip",
|
|
274
|
+
path: settingsPath,
|
|
275
|
+
detail: `hooks.${event} already registered`
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
bucket[existingIdx] = entry;
|
|
280
|
+
changes.push({
|
|
281
|
+
platform: this.id,
|
|
282
|
+
action: "update",
|
|
283
|
+
path: settingsPath,
|
|
284
|
+
detail: `hooks.${event}`
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
bucket.push(entry);
|
|
288
|
+
changes.push({
|
|
289
|
+
platform: this.id,
|
|
290
|
+
action: "create",
|
|
291
|
+
path: settingsPath,
|
|
292
|
+
detail: `hooks.${event}`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
mutated = true;
|
|
296
|
+
}
|
|
297
|
+
if (mutated) this.writeJson(settingsPath, settings, ctx.dryRun);
|
|
298
|
+
return changes;
|
|
299
|
+
}
|
|
300
|
+
uninstallHooks(ctx) {
|
|
301
|
+
const settingsPath = this.getHookConfigPath(ctx);
|
|
302
|
+
const settings = this.readJson(settingsPath);
|
|
303
|
+
const hooks = settings?.hooks;
|
|
304
|
+
if (!settings || !hooks) {
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
platform: this.id,
|
|
308
|
+
action: "skip",
|
|
309
|
+
path: settingsPath,
|
|
310
|
+
detail: "no hooks section present"
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
const changes = [];
|
|
315
|
+
let mutated = false;
|
|
316
|
+
for (const event of Object.keys(hooks)) {
|
|
317
|
+
const bucket = hooks[event];
|
|
318
|
+
if (!Array.isArray(bucket)) continue;
|
|
319
|
+
const next = [];
|
|
320
|
+
let removed = 0;
|
|
321
|
+
for (const e of bucket) {
|
|
322
|
+
const innerBefore = e.hooks?.length ?? 0;
|
|
323
|
+
const inner = (e.hooks ?? []).filter(
|
|
324
|
+
(h) => !this.isOurCommand(h.command, ctx)
|
|
325
|
+
);
|
|
326
|
+
removed += innerBefore - inner.length;
|
|
327
|
+
if (inner.length > 0) next.push({ matcher: e.matcher ?? "", hooks: inner });
|
|
328
|
+
}
|
|
329
|
+
if (removed > 0) {
|
|
330
|
+
if (next.length > 0) hooks[event] = next;
|
|
331
|
+
else delete hooks[event];
|
|
332
|
+
changes.push({
|
|
333
|
+
platform: this.id,
|
|
334
|
+
action: "remove",
|
|
335
|
+
path: settingsPath,
|
|
336
|
+
detail: `hooks.${event} (${removed})`
|
|
337
|
+
});
|
|
338
|
+
mutated = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (mutated) this.writeJson(settingsPath, settings, ctx.dryRun);
|
|
342
|
+
if (changes.length === 0) {
|
|
343
|
+
changes.push({
|
|
344
|
+
platform: this.id,
|
|
345
|
+
action: "skip",
|
|
346
|
+
path: settingsPath,
|
|
347
|
+
detail: "no matching hook entries"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return changes;
|
|
351
|
+
}
|
|
352
|
+
entryHasCommand(entry, command) {
|
|
353
|
+
return (entry.hooks ?? []).some((h) => h.command === command);
|
|
354
|
+
}
|
|
355
|
+
// ── Declarative host-config key patches (configPatch) ────────────────────
|
|
356
|
+
// FIXED semantics (docs/ARCHITECTURE.md §4): set-if-absent on a single
|
|
357
|
+
// dotted LEAF key of settings.json, skip-warn on ANY conflict (present key,
|
|
358
|
+
// non-object intermediate, drift), refcounted ownership in the persisted
|
|
359
|
+
// ledger at <dataRoot>/state/config-patches.json. Reuses the adapter's
|
|
360
|
+
// existing whole-file settings.json JSON IO (the same handling the hook
|
|
361
|
+
// install performs on this exact file) — Claude Code itself rewrites this
|
|
362
|
+
// file, so format preservation is a non-issue here. NOTE for future hosts:
|
|
363
|
+
// VS Code JSONC needs jsonc-parser modify/applyEdits and Codex TOML needs an
|
|
364
|
+
// anchored edit — core/toml.ts's round-trip is BANNED for configPatch.
|
|
365
|
+
/** The ONLY file configPatch may touch on claude-code: settings.json at scope. */
|
|
366
|
+
getPatchableConfigPath(ctx) {
|
|
367
|
+
return join(this.getConfigDir(ctx), "settings.json");
|
|
368
|
+
}
|
|
369
|
+
installConfigPatches(ctx) {
|
|
370
|
+
const { connector } = ctx;
|
|
371
|
+
const patches = connector.platforms[HOST]?.configPatch ?? [];
|
|
372
|
+
if (patches.length === 0) {
|
|
373
|
+
return [
|
|
374
|
+
{ platform: this.id, action: "skip", detail: "connector declares no configPatch entries" }
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
const filePath = this.getPatchableConfigPath(ctx);
|
|
378
|
+
if (this.isPresentButUnparseable(filePath)) {
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
platform: this.id,
|
|
382
|
+
action: "warn",
|
|
383
|
+
path: filePath,
|
|
384
|
+
detail: `existing ${filePath} is not parseable; configPatch left unapplied (back it up / fix it, then re-run)`
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
const settings = this.readJson(filePath) ?? {};
|
|
389
|
+
if (typeof settings !== "object" || Array.isArray(settings)) {
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
platform: this.id,
|
|
393
|
+
action: "warn",
|
|
394
|
+
path: filePath,
|
|
395
|
+
detail: `existing ${filePath} is not a JSON object; configPatch left unapplied`
|
|
396
|
+
}
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
const ledger = loadConfigPatchLedger(ctx.dataRoot);
|
|
400
|
+
const changes = [];
|
|
401
|
+
let fileMutated = false;
|
|
402
|
+
let ledgerMutated = false;
|
|
403
|
+
for (const patch of patches) {
|
|
404
|
+
if (!isValidConfigPatchKey(patch.key)) {
|
|
405
|
+
changes.push({
|
|
406
|
+
platform: this.id,
|
|
407
|
+
action: "warn",
|
|
408
|
+
detail: `configPatch ${JSON.stringify(patch.key)} refused: not a dotted leaf path (segments must match [A-Za-z0-9_-]+; no array indices)`
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const namespace = configPatchNamespaceViolation(patch.key);
|
|
413
|
+
if (namespace) {
|
|
414
|
+
changes.push({
|
|
415
|
+
platform: this.id,
|
|
416
|
+
action: "warn",
|
|
417
|
+
detail: `configPatch refused: ${namespace}`
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const sensitive = claudeSensitiveKeyViolation(patch.key);
|
|
422
|
+
if (sensitive) {
|
|
423
|
+
changes.push({
|
|
424
|
+
platform: this.id,
|
|
425
|
+
action: "warn",
|
|
426
|
+
detail: `configPatch ${patch.key} refused: matches the claude-code sensitive-key denylist (${sensitive}); security-relevant keys are never patched`
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const desired = resolveEnvRefsDeep(patch.value);
|
|
431
|
+
const segments = patch.key.split(".");
|
|
432
|
+
const leaf = readJsonLeaf(settings, segments);
|
|
433
|
+
const entry = findLedgerEntry(ledger, HOST, filePath, patch.key);
|
|
434
|
+
if (leaf.kind === "blocked") {
|
|
435
|
+
changes.push({
|
|
436
|
+
platform: this.id,
|
|
437
|
+
action: "warn",
|
|
438
|
+
path: filePath,
|
|
439
|
+
detail: `configPatch ${patch.key} skipped: "${leaf.atPath}" exists but is not an object \u2014 ${configPatchManualEdit(patch)}`
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (leaf.kind === "absent") {
|
|
444
|
+
writeJsonLeaf(settings, segments, desired);
|
|
445
|
+
fileMutated = true;
|
|
446
|
+
if (entry) {
|
|
447
|
+
entry.writtenValue = desired;
|
|
448
|
+
entry.writtenValueHash = hashJsonValue(desired);
|
|
449
|
+
addLedgerOwner(entry, connector.id, connector.version);
|
|
450
|
+
} else {
|
|
451
|
+
createLedgerEntry(ledger, {
|
|
452
|
+
platform: HOST,
|
|
453
|
+
file: filePath,
|
|
454
|
+
key: patch.key,
|
|
455
|
+
value: desired,
|
|
456
|
+
connectorId: connector.id,
|
|
457
|
+
connectorVersion: connector.version
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
ledgerMutated = true;
|
|
461
|
+
changes.push({
|
|
462
|
+
platform: this.id,
|
|
463
|
+
action: "create",
|
|
464
|
+
path: filePath,
|
|
465
|
+
detail: `configPatch ${patch.key}: <absent> \u2192 ${describeJsonValue(desired)} (${patch.reason})`
|
|
466
|
+
});
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!entry) {
|
|
470
|
+
changes.push({
|
|
471
|
+
platform: this.id,
|
|
472
|
+
action: "warn",
|
|
473
|
+
path: filePath,
|
|
474
|
+
detail: `configPatch ${patch.key} skipped: already set to ${describeJsonValue(leaf.value)} (not created by agent-connector; desired ${describeJsonValue(desired)}) \u2014 ` + configPatchManualEdit(patch)
|
|
475
|
+
});
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!jsonDeepEquals(leaf.value, entry.writtenValue)) {
|
|
479
|
+
changes.push({
|
|
480
|
+
platform: this.id,
|
|
481
|
+
action: "warn",
|
|
482
|
+
path: filePath,
|
|
483
|
+
detail: `configPatch ${patch.key}: value changed since install (current ${describeJsonValue(leaf.value)}, wrote ${describeJsonValue(entry.writtenValue)}); leaving in place \u2014 ${configPatchManualEdit(patch)}`
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (jsonDeepEquals(desired, leaf.value)) {
|
|
488
|
+
const owners = entry.owners.map((o) => o.connectorId);
|
|
489
|
+
if (addLedgerOwner(entry, connector.id, connector.version)) {
|
|
490
|
+
ledgerMutated = true;
|
|
491
|
+
changes.push({
|
|
492
|
+
platform: this.id,
|
|
493
|
+
action: "skip",
|
|
494
|
+
path: filePath,
|
|
495
|
+
detail: `configPatch ${patch.key} already installed; registered as co-owner (co-owned with ${owners.join(", ")})`
|
|
496
|
+
});
|
|
497
|
+
} else {
|
|
498
|
+
changes.push({
|
|
499
|
+
platform: this.id,
|
|
500
|
+
action: "skip",
|
|
501
|
+
path: filePath,
|
|
502
|
+
detail: `configPatch ${patch.key} already installed`
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
changes.push({
|
|
508
|
+
platform: this.id,
|
|
509
|
+
action: "warn",
|
|
510
|
+
path: filePath,
|
|
511
|
+
detail: `configPatch ${patch.key} skipped: already owned by ${entry.owners.map((o) => o.connectorId).join(", ")} with a different value (current ${describeJsonValue(leaf.value)}, desired ${describeJsonValue(desired)}) \u2014 ` + configPatchManualEdit(patch)
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (fileMutated) this.writeJson(filePath, settings, ctx.dryRun);
|
|
515
|
+
if (ledgerMutated && !ctx.dryRun) saveConfigPatchLedger(ctx.dataRoot, ledger);
|
|
516
|
+
return changes;
|
|
517
|
+
}
|
|
518
|
+
uninstallConfigPatches(ctx) {
|
|
519
|
+
const connectorId = ctx.connector.id;
|
|
520
|
+
const ledger = loadConfigPatchLedger(ctx.dataRoot);
|
|
521
|
+
const owned = ledgerEntriesOwnedBy(ledger, HOST, connectorId);
|
|
522
|
+
const declared = ctx.connector.platforms[HOST]?.configPatch ?? [];
|
|
523
|
+
const changes = [];
|
|
524
|
+
const ownedKeys = new Set(owned.map((e) => e.key));
|
|
525
|
+
for (const patch of declared) {
|
|
526
|
+
if (!ownedKeys.has(patch.key)) {
|
|
527
|
+
changes.push({
|
|
528
|
+
platform: this.id,
|
|
529
|
+
action: "skip",
|
|
530
|
+
detail: `configPatch ${patch.key}: no ownership recorded; left untouched`
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (owned.length === 0) return changes;
|
|
535
|
+
let ledgerMutated = false;
|
|
536
|
+
const backedUp = /* @__PURE__ */ new Set();
|
|
537
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
538
|
+
for (const entry of owned) {
|
|
539
|
+
const bucket = byFile.get(entry.file) ?? [];
|
|
540
|
+
bucket.push(entry);
|
|
541
|
+
byFile.set(entry.file, bucket);
|
|
542
|
+
}
|
|
543
|
+
for (const [filePath, entries] of byFile) {
|
|
544
|
+
const unparseable = this.isPresentButUnparseable(filePath);
|
|
545
|
+
const settings = unparseable ? null : this.readJson(filePath);
|
|
546
|
+
let fileMutated = false;
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
const { lastOwner } = removeLedgerOwner(entry, connectorId);
|
|
549
|
+
ledgerMutated = true;
|
|
550
|
+
if (!lastOwner) {
|
|
551
|
+
changes.push({
|
|
552
|
+
platform: this.id,
|
|
553
|
+
action: "skip",
|
|
554
|
+
path: filePath,
|
|
555
|
+
detail: `configPatch ${entry.key} retained: still owned by ${entry.owners.map((o) => o.connectorId).join(", ")}`
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
dropLedgerEntry(ledger, entry);
|
|
560
|
+
if (unparseable) {
|
|
561
|
+
changes.push({
|
|
562
|
+
platform: this.id,
|
|
563
|
+
action: "warn",
|
|
564
|
+
path: filePath,
|
|
565
|
+
detail: `configPatch ${entry.key}: ${filePath} is not parseable; key left in place (ownership released)`
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
570
|
+
changes.push({
|
|
571
|
+
platform: this.id,
|
|
572
|
+
action: "skip",
|
|
573
|
+
path: filePath,
|
|
574
|
+
detail: `configPatch ${entry.key} already absent (no settings file); ownership record dropped`
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const leaf = readJsonLeaf(settings, entry.key.split("."));
|
|
579
|
+
if (leaf.kind !== "present") {
|
|
580
|
+
changes.push({
|
|
581
|
+
platform: this.id,
|
|
582
|
+
action: "skip",
|
|
583
|
+
path: filePath,
|
|
584
|
+
detail: `configPatch ${entry.key} already absent; ownership record dropped`
|
|
585
|
+
});
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (entry.prior?.present !== false || !jsonDeepEquals(leaf.value, entry.writtenValue)) {
|
|
589
|
+
changes.push({
|
|
590
|
+
platform: this.id,
|
|
591
|
+
action: "warn",
|
|
592
|
+
path: filePath,
|
|
593
|
+
detail: `configPatch ${entry.key}: value changed since install (current ${describeJsonValue(leaf.value)}, wrote ${describeJsonValue(entry.writtenValue)}); left in place`
|
|
594
|
+
});
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!ctx.dryRun && !backedUp.has(filePath) && existsSync(filePath)) {
|
|
598
|
+
const backup = this.backupFileForConfigPatch(filePath);
|
|
599
|
+
backedUp.add(filePath);
|
|
600
|
+
if (backup) {
|
|
601
|
+
changes.push({
|
|
602
|
+
platform: this.id,
|
|
603
|
+
action: "create",
|
|
604
|
+
path: backup,
|
|
605
|
+
detail: "backed up settings before configPatch removal"
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
deleteJsonLeaf(settings, entry.key.split("."));
|
|
610
|
+
fileMutated = true;
|
|
611
|
+
changes.push({
|
|
612
|
+
platform: this.id,
|
|
613
|
+
action: "remove",
|
|
614
|
+
path: filePath,
|
|
615
|
+
detail: `configPatch ${entry.key} removed (was ${describeJsonValue(entry.writtenValue)})`
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (fileMutated && settings) this.writeJson(filePath, settings, ctx.dryRun);
|
|
619
|
+
}
|
|
620
|
+
if (ledgerMutated && !ctx.dryRun) saveConfigPatchLedger(ctx.dataRoot, ledger);
|
|
621
|
+
return changes;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Back up ONE exact file (the ledger-recorded patch target, which may belong
|
|
625
|
+
* to a different scope than the current ctx) into the standard backups dir.
|
|
626
|
+
* Best-effort: a failed copy returns null and the caller proceeds — the
|
|
627
|
+
* removal is already value-verified.
|
|
628
|
+
*/
|
|
629
|
+
backupFileForConfigPatch(filePath) {
|
|
630
|
+
try {
|
|
631
|
+
ensureDir(backupsDir());
|
|
632
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
633
|
+
const dest = join(backupsDir(), `${this.id}-${stamp}-${basename(filePath)}`);
|
|
634
|
+
copyFileSync(filePath, dest);
|
|
635
|
+
return dest;
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Doctor: per-ledger-entry configPatch state for this connector —
|
|
642
|
+
* ok → key present and still exactly what we wrote;
|
|
643
|
+
* drifted → user changed the value (we print the manual edit, NEVER
|
|
644
|
+
* auto-fix — sync re-asserts only absent keys);
|
|
645
|
+
* missing → key deleted by the user; sync will re-assert it;
|
|
646
|
+
* orphaned → ledger row whose owners' connector records are all gone.
|
|
647
|
+
* Plus a re-print of the manual edit for declared patches that hold no
|
|
648
|
+
* ownership (skipped at install).
|
|
649
|
+
*/
|
|
650
|
+
configPatchDiagnostics(ctx) {
|
|
651
|
+
const results = [];
|
|
652
|
+
const ledger = loadConfigPatchLedger(ctx.dataRoot);
|
|
653
|
+
const connectorId = ctx.connector.id;
|
|
654
|
+
const platformEntries = ledger.entries.filter((e) => e.platform === this.id);
|
|
655
|
+
for (const entry of platformEntries) {
|
|
656
|
+
const ownedByThis = entry.owners.some((o) => o.connectorId === connectorId);
|
|
657
|
+
if (!ownedByThis) {
|
|
658
|
+
const orphaned = entry.owners.length === 0 || entry.owners.every((o) => readRegisteredMeta(o.connectorId) === null);
|
|
659
|
+
if (orphaned) {
|
|
660
|
+
results.push({
|
|
661
|
+
check: `${this.name}: configPatch ${entry.key}`,
|
|
662
|
+
status: "warn",
|
|
663
|
+
message: `orphaned ledger entry in ${entry.file}: owning connector record(s) gone (${entry.owners.map((o) => o.connectorId).join(", ") || "none"})`,
|
|
664
|
+
fix: `re-run \`agent-connector uninstall <owner-id>\` to release it, or remove ${entry.key} from ${entry.file} manually`
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const state = this.configPatchState(entry);
|
|
670
|
+
if (state.kind === "ok") {
|
|
671
|
+
results.push({
|
|
672
|
+
check: `${this.name}: configPatch ${entry.key}`,
|
|
673
|
+
status: "pass",
|
|
674
|
+
message: `ok \u2014 ${entry.key} = ${describeJsonValue(entry.writtenValue)} (${entry.file})`
|
|
675
|
+
});
|
|
676
|
+
} else if (state.kind === "missing") {
|
|
677
|
+
results.push({
|
|
678
|
+
check: `${this.name}: configPatch ${entry.key}`,
|
|
679
|
+
status: "warn",
|
|
680
|
+
message: `missing \u2014 key deleted from ${entry.file} since install`,
|
|
681
|
+
fix: `agent-connector install (sync) will re-assert it`
|
|
682
|
+
});
|
|
683
|
+
} else {
|
|
684
|
+
results.push({
|
|
685
|
+
check: `${this.name}: configPatch ${entry.key}`,
|
|
686
|
+
status: "warn",
|
|
687
|
+
message: `drifted \u2014 value changed since install (current ${describeJsonValue(state.current)}, wrote ${describeJsonValue(entry.writtenValue)}); never auto-fixed`,
|
|
688
|
+
fix: `manual edit if wanted: set ${entry.key} = ${describeJsonValue(entry.writtenValue)} in ${entry.file}`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const declared = ctx.connector.platforms[HOST]?.configPatch ?? [];
|
|
693
|
+
const ownedKeys = new Set(
|
|
694
|
+
ledgerEntriesOwnedBy(ledger, HOST, connectorId).map((e) => e.key)
|
|
695
|
+
);
|
|
696
|
+
for (const patch of declared) {
|
|
697
|
+
if (ownedKeys.has(patch.key)) continue;
|
|
698
|
+
results.push({
|
|
699
|
+
check: `${this.name}: configPatch ${patch.key}`,
|
|
700
|
+
status: "warn",
|
|
701
|
+
message: `declared but not owned (skipped at install or not yet installed)`,
|
|
702
|
+
fix: configPatchManualEdit(patch)
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return results;
|
|
706
|
+
}
|
|
707
|
+
/** Current on-disk state of one owned ledger entry. */
|
|
708
|
+
configPatchState(entry) {
|
|
709
|
+
const settings = this.readJson(entry.file);
|
|
710
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
711
|
+
return { kind: "missing" };
|
|
712
|
+
}
|
|
713
|
+
const leaf = readJsonLeaf(settings, entry.key.split("."));
|
|
714
|
+
if (leaf.kind !== "present") return { kind: "missing" };
|
|
715
|
+
if (jsonDeepEquals(leaf.value, entry.writtenValue)) return { kind: "ok" };
|
|
716
|
+
return { kind: "drifted", current: leaf.value };
|
|
717
|
+
}
|
|
718
|
+
doctor(ctx) {
|
|
719
|
+
const results = super.doctor(ctx);
|
|
720
|
+
results.push(...this.configPatchDiagnostics(ctx));
|
|
721
|
+
return results;
|
|
722
|
+
}
|
|
723
|
+
// ── Content surfaces: commands / skills / subagents ──────────────────────
|
|
724
|
+
// CONTENT-ONLY: pure native-file writers under <configDir>/{commands,skills,
|
|
725
|
+
// agents}. No runtime dispatch, no home-bin pointer, no telemetry wrap. Each
|
|
726
|
+
// method is idempotent (byte-identical → skip) via BaseAdapter.writeContentFile
|
|
727
|
+
// and reversible via removeContentFile. Honors platforms["claude-code"]
|
|
728
|
+
// per-surface false to skip.
|
|
729
|
+
commandsDir(ctx) {
|
|
730
|
+
return join(this.getConfigDir(ctx), "commands");
|
|
731
|
+
}
|
|
732
|
+
skillsDir(ctx) {
|
|
733
|
+
return join(this.getConfigDir(ctx), "skills");
|
|
734
|
+
}
|
|
735
|
+
agentsDir(ctx) {
|
|
736
|
+
return join(this.getConfigDir(ctx), "agents");
|
|
737
|
+
}
|
|
738
|
+
/** Native command file path: <configDir>/commands/<name>.md. */
|
|
739
|
+
commandPath(ctx, name) {
|
|
740
|
+
return join(this.commandsDir(ctx), `${name}.md`);
|
|
741
|
+
}
|
|
742
|
+
/** Native skill dir: <configDir>/skills/<name>. */
|
|
743
|
+
skillDir(ctx, name) {
|
|
744
|
+
return join(this.skillsDir(ctx), name);
|
|
745
|
+
}
|
|
746
|
+
/** Native subagent file path: <configDir>/agents/<name>.md. */
|
|
747
|
+
subagentPath(ctx, name) {
|
|
748
|
+
return join(this.agentsDir(ctx), `${name}.md`);
|
|
749
|
+
}
|
|
750
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
751
|
+
installCommands(ctx) {
|
|
752
|
+
const { connector } = ctx;
|
|
753
|
+
if (connector.platforms[HOST]?.commands === false) {
|
|
754
|
+
return [{ platform: this.id, action: "skip", detail: "commands disabled for claude-code" }];
|
|
755
|
+
}
|
|
756
|
+
if (connector.commands.length === 0) {
|
|
757
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no commands" }];
|
|
758
|
+
}
|
|
759
|
+
return connector.commands.map(
|
|
760
|
+
(cmd) => this.writeContentFile(
|
|
761
|
+
this.commandPath(ctx, cmd.name),
|
|
762
|
+
this.renderCommand(cmd),
|
|
763
|
+
ctx.dryRun
|
|
764
|
+
)
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
uninstallCommands(ctx) {
|
|
768
|
+
const { connector } = ctx;
|
|
769
|
+
if (connector.commands.length === 0) {
|
|
770
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no commands" }];
|
|
771
|
+
}
|
|
772
|
+
return connector.commands.map(
|
|
773
|
+
(cmd) => this.removeContentFile(this.commandPath(ctx, cmd.name), ctx.dryRun)
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
/** Render a command to md+frontmatter (delegates to the shared renderer). */
|
|
777
|
+
renderCommand(cmd) {
|
|
778
|
+
return renderCommandMd(cmd);
|
|
779
|
+
}
|
|
780
|
+
// ── Skills ────────────────────────────────────────────────────────────────
|
|
781
|
+
installSkills(ctx) {
|
|
782
|
+
const { connector } = ctx;
|
|
783
|
+
if (connector.platforms[HOST]?.skills === false) {
|
|
784
|
+
return [{ platform: this.id, action: "skip", detail: "skills disabled for claude-code" }];
|
|
785
|
+
}
|
|
786
|
+
if (connector.skills.length === 0) {
|
|
787
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
|
|
788
|
+
}
|
|
789
|
+
const changes = [];
|
|
790
|
+
for (const skill of connector.skills) {
|
|
791
|
+
const dir = this.skillDir(ctx, skill.name);
|
|
792
|
+
changes.push(
|
|
793
|
+
this.writeContentFile(join(dir, "SKILL.md"), this.renderSkill(skill), ctx.dryRun)
|
|
794
|
+
);
|
|
795
|
+
for (const [rel, contents] of Object.entries(skill.resources ?? {})) {
|
|
796
|
+
const target = this.resolveWithin(dir, rel);
|
|
797
|
+
if (target === null) {
|
|
798
|
+
changes.push({
|
|
799
|
+
platform: this.id,
|
|
800
|
+
action: "warn",
|
|
801
|
+
detail: `skill resource "${rel}" escapes the skill dir; skipped`
|
|
802
|
+
});
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
changes.push(this.writeContentFile(target, contents, ctx.dryRun));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return changes;
|
|
809
|
+
}
|
|
810
|
+
uninstallSkills(ctx) {
|
|
811
|
+
const { connector } = ctx;
|
|
812
|
+
if (connector.skills.length === 0) {
|
|
813
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
|
|
814
|
+
}
|
|
815
|
+
const changes = [];
|
|
816
|
+
for (const skill of connector.skills) {
|
|
817
|
+
const dir = this.skillDir(ctx, skill.name);
|
|
818
|
+
changes.push(this.removeContentFile(join(dir, "SKILL.md"), ctx.dryRun));
|
|
819
|
+
for (const rel of Object.keys(skill.resources ?? {})) {
|
|
820
|
+
const target = this.resolveWithin(dir, rel);
|
|
821
|
+
if (target === null) continue;
|
|
822
|
+
changes.push(this.removeContentFile(target, ctx.dryRun));
|
|
823
|
+
}
|
|
824
|
+
changes.push(this.removeDirIfEmpty(dir, ctx.dryRun));
|
|
825
|
+
}
|
|
826
|
+
return changes;
|
|
827
|
+
}
|
|
828
|
+
/** Render a skill's SKILL.md (delegates to the shared renderer). */
|
|
829
|
+
renderSkill(skill) {
|
|
830
|
+
return renderSkillMd(skill);
|
|
831
|
+
}
|
|
832
|
+
// ── Subagents ───────────────────────────────────────────────────────────────
|
|
833
|
+
installSubagents(ctx) {
|
|
834
|
+
const { connector } = ctx;
|
|
835
|
+
if (connector.platforms[HOST]?.subagents === false) {
|
|
836
|
+
return [{ platform: this.id, action: "skip", detail: "subagents disabled for claude-code" }];
|
|
837
|
+
}
|
|
838
|
+
if (connector.subagents.length === 0) {
|
|
839
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no subagents" }];
|
|
840
|
+
}
|
|
841
|
+
return connector.subagents.map(
|
|
842
|
+
(agent) => this.writeContentFile(
|
|
843
|
+
this.subagentPath(ctx, agent.name),
|
|
844
|
+
this.renderSubagent(agent),
|
|
845
|
+
ctx.dryRun
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
uninstallSubagents(ctx) {
|
|
850
|
+
const { connector } = ctx;
|
|
851
|
+
if (connector.subagents.length === 0) {
|
|
852
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no subagents" }];
|
|
853
|
+
}
|
|
854
|
+
return connector.subagents.map(
|
|
855
|
+
(agent) => this.removeContentFile(this.subagentPath(ctx, agent.name), ctx.dryRun)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
/** Render a subagent to md+frontmatter (delegates to the shared renderer). */
|
|
859
|
+
renderSubagent(agent) {
|
|
860
|
+
return renderSubagentMd(agent);
|
|
861
|
+
}
|
|
862
|
+
// ── Memory surface: CLAUDE.md managed block + AGENTS.md interop ────────────
|
|
863
|
+
// EXCEPTION host. Claude Code reads CLAUDE.md, NOT AGENTS.md (docs verbatim:
|
|
864
|
+
// "Claude Code reads CLAUDE.md, not AGENTS.md"; no AGENTS.md support through
|
|
865
|
+
// v2.1.172), so the base AGENTS.md-first default is overridden:
|
|
866
|
+
// - mode "block" (default): the managed block goes straight into CLAUDE.md.
|
|
867
|
+
// HTML-comment markers are CORRECT here — Claude strips HTML comments
|
|
868
|
+
// from CLAUDE.md before context injection, so the markers and the
|
|
869
|
+
// do-not-edit notice are INVISIBLE to the model while remaining fully
|
|
870
|
+
// parseable by us for sync/doctor/uninstall.
|
|
871
|
+
// - mode "agents-import" (opt-in via platforms["claude-code"].memory.mode):
|
|
872
|
+
// canonical block goes into AGENTS.md, plus a SHARED `@AGENTS.md` import
|
|
873
|
+
// bridge block in CLAUDE.md (Anthropic's documented interop). Opt-in
|
|
874
|
+
// because the import makes Claude read the ENTIRE AGENTS.md — a behavior
|
|
875
|
+
// change agent-connector must not make silently.
|
|
876
|
+
// - AUTO interop: when CLAUDE.md ALREADY imports `@AGENTS.md` (a line
|
|
877
|
+
// outside code fences and outside our own blocks) or IS a symlink
|
|
878
|
+
// resolving to AGENTS.md, the canonical block goes to AGENTS.md and
|
|
879
|
+
// CLAUDE.md is NOT touched — the pre-existing user wiring is never
|
|
880
|
+
// claimed as agent-connector-managed (prevents double-reads and writes
|
|
881
|
+
// "through" a symlink). Order-independent: keyed off the import/symlink
|
|
882
|
+
// existing, never off whether the AGENTS.md block was written yet.
|
|
883
|
+
/** CLAUDE.md path: project `<projectDir>/CLAUDE.md`, user `~/.claude/CLAUDE.md`. */
|
|
884
|
+
claudeMdPath(ctx) {
|
|
885
|
+
return ctx.scope === "project" ? join(ctx.projectDir, "CLAUDE.md") : join(homedir(), ".claude", "CLAUDE.md");
|
|
886
|
+
}
|
|
887
|
+
/** Sibling AGENTS.md at the same scope (agents-import / auto-interop target). */
|
|
888
|
+
agentsMdPath(ctx) {
|
|
889
|
+
return ctx.scope === "project" ? join(ctx.projectDir, "AGENTS.md") : join(homedir(), ".claude", "AGENTS.md");
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* True when the user ALREADY wired CLAUDE.md to AGENTS.md themselves:
|
|
893
|
+
* an `@AGENTS.md` import line outside code fences and outside agent-connector
|
|
894
|
+
* managed blocks (our own bridge must not count), or CLAUDE.md being a
|
|
895
|
+
* symlink that resolves to the sibling AGENTS.md.
|
|
896
|
+
*/
|
|
897
|
+
claudeMdWiredToAgentsMd(ctx) {
|
|
898
|
+
const claudeMd = this.claudeMdPath(ctx);
|
|
899
|
+
if (!existsSync(claudeMd)) return false;
|
|
900
|
+
try {
|
|
901
|
+
if (lstatSync(claudeMd).isSymbolicLink()) {
|
|
902
|
+
const agentsMd = this.agentsMdPath(ctx);
|
|
903
|
+
const resolved = realpathSync(claudeMd);
|
|
904
|
+
const agentsResolved = existsSync(agentsMd) ? realpathSync(agentsMd) : agentsMd;
|
|
905
|
+
if (resolved === agentsResolved) return true;
|
|
906
|
+
}
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
909
|
+
let raw;
|
|
910
|
+
try {
|
|
911
|
+
raw = readFileSync(claudeMd, "utf8");
|
|
912
|
+
} catch {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
const withoutBlocks = removeBlocksFromText(raw, { blockIdPrefix: "" }).text;
|
|
916
|
+
const importRe = /(^|\s)@(?:AGENTS\.md|~\/\.claude\/AGENTS\.md)(\s|$)/;
|
|
917
|
+
return linesOutsideFences(withoutBlocks).some((line) => importRe.test(line));
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Effective memory mode for this install:
|
|
921
|
+
* "user-import" — the user already wired CLAUDE.md→AGENTS.md (wins over
|
|
922
|
+
* everything; we never write a duplicate or a second import);
|
|
923
|
+
* "agents-import" — explicit opt-in via platforms["claude-code"].memory.mode;
|
|
924
|
+
* "block" — the default (managed block directly in CLAUDE.md).
|
|
925
|
+
*/
|
|
926
|
+
effectiveMemoryMode(ctx) {
|
|
927
|
+
if (this.claudeMdWiredToAgentsMd(ctx)) return "user-import";
|
|
928
|
+
return this.memoryOverride(ctx)?.mode === "agents-import" ? "agents-import" : "block";
|
|
929
|
+
}
|
|
930
|
+
memoryTargets(ctx) {
|
|
931
|
+
if (this.memoryOverride(ctx)?.path) return super.memoryTargets(ctx);
|
|
932
|
+
if (ctx.scope !== "project" && ctx.scope !== "user") return [];
|
|
933
|
+
const mode = this.effectiveMemoryMode(ctx);
|
|
934
|
+
if (mode === "block") {
|
|
935
|
+
return [
|
|
936
|
+
{
|
|
937
|
+
path: this.claudeMdPath(ctx),
|
|
938
|
+
reason: "CLAUDE.md (Claude Code reads CLAUDE.md, not AGENTS.md; HTML-comment markers are stripped from the model's context)"
|
|
939
|
+
}
|
|
940
|
+
];
|
|
941
|
+
}
|
|
942
|
+
return [
|
|
943
|
+
{
|
|
944
|
+
path: this.agentsMdPath(ctx),
|
|
945
|
+
reason: mode === "user-import" ? "AGENTS.md (CLAUDE.md already imports/symlinks it \u2014 user wiring respected)" : "AGENTS.md (agents-import mode; @AGENTS.md bridge managed in CLAUDE.md)"
|
|
946
|
+
}
|
|
947
|
+
];
|
|
948
|
+
}
|
|
949
|
+
installMemory(ctx) {
|
|
950
|
+
const changes = super.installMemory(ctx);
|
|
951
|
+
const { connector } = ctx;
|
|
952
|
+
if (connector.platforms[HOST]?.memory === false || (connector.memory ?? []).length === 0) {
|
|
953
|
+
return changes;
|
|
954
|
+
}
|
|
955
|
+
if (ctx.scope !== "project" && ctx.scope !== "user") return changes;
|
|
956
|
+
if (this.memoryOverride(ctx)?.path) return changes;
|
|
957
|
+
const mode = this.effectiveMemoryMode(ctx);
|
|
958
|
+
if (mode === "user-import") {
|
|
959
|
+
changes.push({
|
|
960
|
+
platform: this.id,
|
|
961
|
+
action: "skip",
|
|
962
|
+
path: this.claudeMdPath(ctx),
|
|
963
|
+
detail: "memory: CLAUDE.md already imports AGENTS.md; canonical block in AGENTS.md suffices (pre-existing user import never claimed as managed)"
|
|
964
|
+
});
|
|
965
|
+
return changes;
|
|
966
|
+
}
|
|
967
|
+
if (mode !== "agents-import") return changes;
|
|
968
|
+
const claudeMd = this.claudeMdPath(ctx);
|
|
969
|
+
const importLine = ctx.scope === "project" ? "@AGENTS.md" : "@~/.claude/AGENTS.md";
|
|
970
|
+
const res = upsertManagedBlockFile(claudeMd, {
|
|
971
|
+
blockId: CLAUDE_AGENTS_IMPORT_BLOCK_ID,
|
|
972
|
+
connectorId: "_shared",
|
|
973
|
+
content: importLine,
|
|
974
|
+
notice: "Managed by agent-connector (shared bridge). This import makes Claude Code read AGENTS.md; it is removed automatically when the last agent-connector block leaves AGENTS.md.",
|
|
975
|
+
force: ctx.force ?? false,
|
|
976
|
+
dryRun: ctx.dryRun
|
|
977
|
+
});
|
|
978
|
+
changes.push({
|
|
979
|
+
platform: this.id,
|
|
980
|
+
action: res.action,
|
|
981
|
+
path: claudeMd,
|
|
982
|
+
detail: `memory: ${res.detail} \u2014 @AGENTS.md import bridge (agents-import mode)`
|
|
983
|
+
});
|
|
984
|
+
if (res.backupPath) {
|
|
985
|
+
changes.push({
|
|
986
|
+
platform: this.id,
|
|
987
|
+
action: "create",
|
|
988
|
+
path: res.backupPath,
|
|
989
|
+
detail: "backed up CLAUDE.md before destructive change"
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
if (res.action !== "warn" && !ctx.dryRun) {
|
|
993
|
+
const ledger = loadMemoryLedger(connector.id);
|
|
994
|
+
recordMemoryTarget(ledger, {
|
|
995
|
+
platform: this.id,
|
|
996
|
+
scope: ctx.scope,
|
|
997
|
+
path: claudeMd,
|
|
998
|
+
blockId: CLAUDE_AGENTS_IMPORT_BLOCK_ID,
|
|
999
|
+
createdFile: res.createdFile,
|
|
1000
|
+
hash: res.hash
|
|
1001
|
+
});
|
|
1002
|
+
saveMemoryLedger(connector.id, ledger);
|
|
1003
|
+
}
|
|
1004
|
+
return changes;
|
|
1005
|
+
}
|
|
1006
|
+
uninstallMemory(ctx) {
|
|
1007
|
+
const priorRows = loadMemoryLedger(ctx.connector.id).targets.filter(
|
|
1008
|
+
(t) => t.platform === this.id && t.blockId === CLAUDE_AGENTS_IMPORT_BLOCK_ID
|
|
1009
|
+
);
|
|
1010
|
+
const changes = super.uninstallMemory(ctx);
|
|
1011
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
1012
|
+
if (ctx.scope === "project" || ctx.scope === "user") {
|
|
1013
|
+
pairs.set(this.claudeMdPath(ctx), {
|
|
1014
|
+
agentsMd: this.agentsMdPath(ctx),
|
|
1015
|
+
createdFile: false
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
for (const row of priorRows) {
|
|
1019
|
+
const existing = pairs.get(row.path);
|
|
1020
|
+
pairs.set(row.path, {
|
|
1021
|
+
agentsMd: existing?.agentsMd ?? join(dirname(row.path), "AGENTS.md"),
|
|
1022
|
+
createdFile: (existing?.createdFile ?? false) || row.createdFile
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
for (const [claudeMd, { agentsMd, createdFile }] of pairs) {
|
|
1026
|
+
changes.push(...this.releaseAgentsImportBridge(ctx, claudeMd, agentsMd, createdFile));
|
|
1027
|
+
}
|
|
1028
|
+
return changes;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Refcounted bridge release: remove the `_shared/claude-agents-import` block
|
|
1032
|
+
* from CLAUDE.md ONLY when the sibling AGENTS.md holds zero agent-connector
|
|
1033
|
+
* blocks from OTHER connectors (namespace-wide count, excluding this
|
|
1034
|
+
* connector's own blocks so the answer is identical on dry-run and real run).
|
|
1035
|
+
* A user-authored `@AGENTS.md` import (no markers) is NEVER touched.
|
|
1036
|
+
*/
|
|
1037
|
+
releaseAgentsImportBridge(ctx, claudeMd, agentsMd, createdFile) {
|
|
1038
|
+
if (!existsSync(claudeMd)) return [];
|
|
1039
|
+
let raw;
|
|
1040
|
+
try {
|
|
1041
|
+
raw = readFileSync(claudeMd, "utf8");
|
|
1042
|
+
} catch {
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
if (!listManagedBlocks(raw).some((b) => b.blockId === CLAUDE_AGENTS_IMPORT_BLOCK_ID)) {
|
|
1046
|
+
return [];
|
|
1047
|
+
}
|
|
1048
|
+
let remaining = 0;
|
|
1049
|
+
if (existsSync(agentsMd)) {
|
|
1050
|
+
try {
|
|
1051
|
+
remaining = listManagedBlocks(readFileSync(agentsMd, "utf8")).filter(
|
|
1052
|
+
(b) => !b.blockId.startsWith(`${ctx.connector.id}/`)
|
|
1053
|
+
).length;
|
|
1054
|
+
} catch {
|
|
1055
|
+
remaining = 1;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (remaining > 0) {
|
|
1059
|
+
return [
|
|
1060
|
+
{
|
|
1061
|
+
platform: this.id,
|
|
1062
|
+
action: "skip",
|
|
1063
|
+
path: claudeMd,
|
|
1064
|
+
detail: `memory: @AGENTS.md import bridge retained \u2014 ${remaining} agent-connector block(s) from other connectors remain in ${agentsMd}`
|
|
1065
|
+
}
|
|
1066
|
+
];
|
|
1067
|
+
}
|
|
1068
|
+
return removeManagedBlocksFile(
|
|
1069
|
+
claudeMd,
|
|
1070
|
+
{ blockId: CLAUDE_AGENTS_IMPORT_BLOCK_ID },
|
|
1071
|
+
{ dryRun: ctx.dryRun, deleteFileIfCreated: createdFile }
|
|
1072
|
+
).map((r) => ({
|
|
1073
|
+
platform: this.id,
|
|
1074
|
+
action: r.action,
|
|
1075
|
+
path: r.path,
|
|
1076
|
+
detail: `memory: ${r.detail}`
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
/** True when a hook command references our home binary AND this connector id
|
|
1080
|
+
* (anchored so a shared-prefix id can't collide — see isHomeBinHookCommand). */
|
|
1081
|
+
isOurCommand(command, ctx) {
|
|
1082
|
+
return isHomeBinHookCommand(command, ctx.homeBinPath, ctx.connector.id);
|
|
1083
|
+
}
|
|
1084
|
+
// ── Diagnostics ──────────────────────────────────────────────────────────
|
|
1085
|
+
getHealthChecks(ctx) {
|
|
1086
|
+
const settingsPath = this.getHookConfigPath(ctx);
|
|
1087
|
+
const connectorId = ctx.connector.id;
|
|
1088
|
+
const homeBin = ctx.homeBinPath;
|
|
1089
|
+
const hookEvents = ctx.connector.hookEvents;
|
|
1090
|
+
const declaredHookCount = hookEvents.length + Object.keys(ctx.connector.platforms[HOST]?.nativeHooks ?? {}).length;
|
|
1091
|
+
const checks = [
|
|
1092
|
+
{
|
|
1093
|
+
name: `${this.name}: settings.json present`,
|
|
1094
|
+
check: () => {
|
|
1095
|
+
if (declaredHookCount === 0) {
|
|
1096
|
+
return { status: "OK", detail: "no hooks declared" };
|
|
1097
|
+
}
|
|
1098
|
+
return existsSync(settingsPath) ? { status: "OK", detail: settingsPath } : { status: "FAIL", detail: `not found: ${settingsPath}` };
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: `${this.name}: hook command registered`,
|
|
1103
|
+
check: () => {
|
|
1104
|
+
if (declaredHookCount === 0) {
|
|
1105
|
+
return { status: "OK", detail: "no hooks declared" };
|
|
1106
|
+
}
|
|
1107
|
+
const settings = this.readJson(
|
|
1108
|
+
settingsPath
|
|
1109
|
+
);
|
|
1110
|
+
if (!settings) return { status: "FAIL", detail: `cannot read ${settingsPath}` };
|
|
1111
|
+
const hooks = settings.hooks ?? {};
|
|
1112
|
+
const registered = Object.values(hooks).some(
|
|
1113
|
+
(entries) => (entries ?? []).some(
|
|
1114
|
+
(e) => (e.hooks ?? []).some(
|
|
1115
|
+
(h) => isHomeBinHookCommand(h.command, homeBin, connectorId)
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
return registered ? { status: "OK", detail: "hook command present" } : {
|
|
1120
|
+
status: "FAIL",
|
|
1121
|
+
detail: `no hook for ${connectorId} in ${settingsPath}`
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
];
|
|
1126
|
+
for (const cmd of ctx.connector.commands) {
|
|
1127
|
+
const p = this.commandPath(ctx, cmd.name);
|
|
1128
|
+
checks.push({
|
|
1129
|
+
name: `${this.name}: command ${cmd.name} present`,
|
|
1130
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
for (const skill of ctx.connector.skills) {
|
|
1134
|
+
const p = join(this.skillDir(ctx, skill.name), "SKILL.md");
|
|
1135
|
+
checks.push({
|
|
1136
|
+
name: `${this.name}: skill ${skill.name} present`,
|
|
1137
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
for (const agent of ctx.connector.subagents) {
|
|
1141
|
+
const p = this.subagentPath(ctx, agent.name);
|
|
1142
|
+
checks.push({
|
|
1143
|
+
name: `${this.name}: subagent ${agent.name} present`,
|
|
1144
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
return checks;
|
|
1148
|
+
}
|
|
1149
|
+
// ── Runtime: parse Claude stdin JSON → normalized event ──────────────────
|
|
1150
|
+
parseEvent(event, raw) {
|
|
1151
|
+
const input = raw ?? {};
|
|
1152
|
+
const connectorId = typeof input.connector === "string" ? input.connector : "";
|
|
1153
|
+
const sessionId = extractSessionId(input);
|
|
1154
|
+
const projectDir = typeof input.cwd === "string" ? input.cwd : void 0;
|
|
1155
|
+
const base = {
|
|
1156
|
+
hostPlatform: HOST,
|
|
1157
|
+
connectorId,
|
|
1158
|
+
sessionId,
|
|
1159
|
+
raw,
|
|
1160
|
+
...projectDir !== void 0 ? { projectDir } : {}
|
|
1161
|
+
};
|
|
1162
|
+
switch (event) {
|
|
1163
|
+
case "PreToolUse": {
|
|
1164
|
+
const ev = {
|
|
1165
|
+
...base,
|
|
1166
|
+
toolName: input.tool_name ?? "",
|
|
1167
|
+
toolInput: input.tool_input ?? {}
|
|
1168
|
+
};
|
|
1169
|
+
return ev;
|
|
1170
|
+
}
|
|
1171
|
+
case "PostToolUse": {
|
|
1172
|
+
const ev = {
|
|
1173
|
+
...base,
|
|
1174
|
+
toolName: input.tool_name ?? "",
|
|
1175
|
+
toolInput: input.tool_input ?? {},
|
|
1176
|
+
...toolResponseToString(input.tool_response) !== void 0 ? { toolOutput: toolResponseToString(input.tool_response) } : {}
|
|
1177
|
+
};
|
|
1178
|
+
return ev;
|
|
1179
|
+
}
|
|
1180
|
+
case "SessionStart": {
|
|
1181
|
+
const ev = {
|
|
1182
|
+
...base,
|
|
1183
|
+
source: normalizeSessionSource(input.source)
|
|
1184
|
+
};
|
|
1185
|
+
return ev;
|
|
1186
|
+
}
|
|
1187
|
+
case "SessionEnd": {
|
|
1188
|
+
const ev = {
|
|
1189
|
+
...base,
|
|
1190
|
+
...typeof input.reason === "string" ? { reason: input.reason } : {}
|
|
1191
|
+
};
|
|
1192
|
+
return ev;
|
|
1193
|
+
}
|
|
1194
|
+
case "UserPromptSubmit": {
|
|
1195
|
+
const ev = {
|
|
1196
|
+
...base,
|
|
1197
|
+
prompt: typeof input.prompt === "string" ? input.prompt : ""
|
|
1198
|
+
};
|
|
1199
|
+
return ev;
|
|
1200
|
+
}
|
|
1201
|
+
case "PreCompact": {
|
|
1202
|
+
const ev = {
|
|
1203
|
+
...base,
|
|
1204
|
+
...input.trigger === "auto" || input.trigger === "manual" ? { trigger: input.trigger } : {}
|
|
1205
|
+
};
|
|
1206
|
+
return ev;
|
|
1207
|
+
}
|
|
1208
|
+
case "Stop": {
|
|
1209
|
+
const ev = {
|
|
1210
|
+
...base,
|
|
1211
|
+
...typeof input.stop_hook_active === "boolean" ? { stopHookActive: input.stop_hook_active } : {}
|
|
1212
|
+
};
|
|
1213
|
+
return ev;
|
|
1214
|
+
}
|
|
1215
|
+
case "Notification": {
|
|
1216
|
+
const ev = {
|
|
1217
|
+
...base,
|
|
1218
|
+
message: typeof input.message === "string" ? input.message : ""
|
|
1219
|
+
};
|
|
1220
|
+
return ev;
|
|
1221
|
+
}
|
|
1222
|
+
case "PermissionRequest": {
|
|
1223
|
+
const ev = {
|
|
1224
|
+
...base,
|
|
1225
|
+
toolName: input.tool_name ?? "",
|
|
1226
|
+
toolInput: input.tool_input ?? {},
|
|
1227
|
+
...Array.isArray(input.permission_suggestions) ? { permissionSuggestions: input.permission_suggestions } : {}
|
|
1228
|
+
};
|
|
1229
|
+
return ev;
|
|
1230
|
+
}
|
|
1231
|
+
case "PostToolUseFailure": {
|
|
1232
|
+
const ev = {
|
|
1233
|
+
...base,
|
|
1234
|
+
toolName: input.tool_name ?? "",
|
|
1235
|
+
toolInput: input.tool_input ?? {},
|
|
1236
|
+
error: typeof input.error === "string" ? input.error : "",
|
|
1237
|
+
...typeof input.tool_use_id === "string" ? { toolUseId: input.tool_use_id } : {},
|
|
1238
|
+
...typeof input.is_interrupt === "boolean" ? { isInterrupt: input.is_interrupt } : {},
|
|
1239
|
+
...typeof input.duration_ms === "number" ? { durationMs: input.duration_ms } : {}
|
|
1240
|
+
};
|
|
1241
|
+
return ev;
|
|
1242
|
+
}
|
|
1243
|
+
case "SubagentStart": {
|
|
1244
|
+
const ev = {
|
|
1245
|
+
...base,
|
|
1246
|
+
...typeof input.agent_id === "string" ? { agentId: input.agent_id } : {},
|
|
1247
|
+
...typeof input.agent_type === "string" ? { agentType: input.agent_type } : {}
|
|
1248
|
+
};
|
|
1249
|
+
return ev;
|
|
1250
|
+
}
|
|
1251
|
+
case "SubagentStop": {
|
|
1252
|
+
const ev = {
|
|
1253
|
+
...base,
|
|
1254
|
+
...typeof input.agent_id === "string" ? { agentId: input.agent_id } : {},
|
|
1255
|
+
...typeof input.agent_type === "string" ? { agentType: input.agent_type } : {},
|
|
1256
|
+
...typeof input.agent_transcript_path === "string" ? { agentTranscriptPath: input.agent_transcript_path } : {},
|
|
1257
|
+
...typeof input.last_assistant_message === "string" ? { lastAssistantMessage: input.last_assistant_message } : {},
|
|
1258
|
+
...typeof input.stop_hook_active === "boolean" ? { stopHookActive: input.stop_hook_active } : {}
|
|
1259
|
+
};
|
|
1260
|
+
return ev;
|
|
1261
|
+
}
|
|
1262
|
+
default: {
|
|
1263
|
+
const _never = event;
|
|
1264
|
+
throw new Error(`unsupported claude-code hook event: ${String(_never)}`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
// ── Runtime: normalized response → Claude native hook reply ──────────────
|
|
1269
|
+
formatReply(event, response) {
|
|
1270
|
+
const hookEventName = event;
|
|
1271
|
+
const decision = response.decision ?? "allow";
|
|
1272
|
+
if (event === "PermissionRequest") {
|
|
1273
|
+
if (response.decision === "deny") {
|
|
1274
|
+
return this.stdout({
|
|
1275
|
+
hookSpecificOutput: {
|
|
1276
|
+
hookEventName,
|
|
1277
|
+
decision: {
|
|
1278
|
+
behavior: "deny",
|
|
1279
|
+
message: response.reason ?? "Blocked by hook"
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
if (response.decision === "allow" || response.decision === "modify" && response.updatedInput) {
|
|
1285
|
+
return this.stdout({
|
|
1286
|
+
hookSpecificOutput: {
|
|
1287
|
+
hookEventName,
|
|
1288
|
+
decision: {
|
|
1289
|
+
behavior: "allow",
|
|
1290
|
+
...response.updatedInput ? { updatedInput: response.updatedInput } : {}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
return { exitCode: 0 };
|
|
1296
|
+
}
|
|
1297
|
+
if (event === "PostToolUseFailure" || event === "SubagentStart") {
|
|
1298
|
+
const context = decision === "context" ? response.additionalContext : decision === "deny" ? response.reason ?? response.additionalContext : void 0;
|
|
1299
|
+
if (context) {
|
|
1300
|
+
return this.stdout({
|
|
1301
|
+
hookSpecificOutput: { hookEventName, additionalContext: context }
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
return { exitCode: 0 };
|
|
1305
|
+
}
|
|
1306
|
+
if (decision === "deny") {
|
|
1307
|
+
if (event === "Stop" || event === "SubagentStop" || event === "UserPromptSubmit" || event === "PostToolUse") {
|
|
1308
|
+
return this.stdout({
|
|
1309
|
+
decision: "block",
|
|
1310
|
+
reason: response.reason ?? "Blocked by hook"
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
return this.stdout({
|
|
1314
|
+
hookSpecificOutput: {
|
|
1315
|
+
hookEventName,
|
|
1316
|
+
permissionDecision: "deny",
|
|
1317
|
+
permissionDecisionReason: response.reason ?? "Blocked by hook"
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (decision === "ask") {
|
|
1322
|
+
return this.stdout({
|
|
1323
|
+
hookSpecificOutput: {
|
|
1324
|
+
hookEventName,
|
|
1325
|
+
permissionDecision: "ask",
|
|
1326
|
+
permissionDecisionReason: response.reason ?? "Confirmation required by hook"
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (decision === "modify") {
|
|
1331
|
+
if (event === "PreToolUse" && response.updatedInput) {
|
|
1332
|
+
return this.stdout({
|
|
1333
|
+
hookSpecificOutput: { hookEventName, updatedInput: response.updatedInput }
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (decision === "context" && response.additionalContext) {
|
|
1338
|
+
return this.stdout({
|
|
1339
|
+
hookSpecificOutput: { hookEventName, additionalContext: response.additionalContext }
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
return { exitCode: 0 };
|
|
1343
|
+
}
|
|
1344
|
+
stdout(payload) {
|
|
1345
|
+
return { exitCode: 0, stdout: JSON.stringify(payload) };
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
var SENSITIVE_KEY_RULES = [
|
|
1349
|
+
{ re: /^permissions(\.|$)/, label: "permissions" },
|
|
1350
|
+
{ re: /^allowedTools(\.|$)/, label: "allowedTools" },
|
|
1351
|
+
{ re: /^disallowedTools(\.|$)/, label: "disallowedTools" },
|
|
1352
|
+
{ re: /^apiKey/i, label: "apiKey*" },
|
|
1353
|
+
{ re: /^awsAuthRefresh$/, label: "awsAuthRefresh" },
|
|
1354
|
+
{ re: /^awsCredentialExport$/, label: "awsCredentialExport" },
|
|
1355
|
+
{ re: /^forceLoginMethod$/, label: "forceLoginMethod" },
|
|
1356
|
+
{ re: /^forceLoginOrgUUID$/, label: "forceLoginOrgUUID" },
|
|
1357
|
+
{ re: /^otelHeadersHelper$/, label: "otelHeadersHelper" },
|
|
1358
|
+
{ re: /^env\.ANTHROPIC_/, label: "env.ANTHROPIC_*" },
|
|
1359
|
+
{ re: /^env\.AWS_/, label: "env.AWS_*" },
|
|
1360
|
+
{ re: /^env\.[^.]*_PROXY(\.|$)/i, label: "env.*_PROXY" },
|
|
1361
|
+
{ re: /^env\.[^.]*TOKEN/i, label: "env.*TOKEN*" },
|
|
1362
|
+
{ re: /^env\.[^.]*KEY/i, label: "env.*KEY*" },
|
|
1363
|
+
{ re: /^env\.[^.]*SECRET/i, label: "env.*SECRET*" }
|
|
1364
|
+
];
|
|
1365
|
+
function claudeSensitiveKeyViolation(key) {
|
|
1366
|
+
for (const rule of SENSITIVE_KEY_RULES) {
|
|
1367
|
+
if (rule.re.test(key)) return rule.label;
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
function readJsonLeaf(root, segments) {
|
|
1372
|
+
let node = root;
|
|
1373
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
1374
|
+
const next = node[segments[i]];
|
|
1375
|
+
if (next === void 0) return { kind: "absent" };
|
|
1376
|
+
if (next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
1377
|
+
return { kind: "blocked", atPath: segments.slice(0, i + 1).join(".") };
|
|
1378
|
+
}
|
|
1379
|
+
node = next;
|
|
1380
|
+
}
|
|
1381
|
+
const leaf = node[segments[segments.length - 1]];
|
|
1382
|
+
if (leaf === void 0) return { kind: "absent" };
|
|
1383
|
+
return { kind: "present", value: leaf };
|
|
1384
|
+
}
|
|
1385
|
+
function writeJsonLeaf(root, segments, value) {
|
|
1386
|
+
let node = root;
|
|
1387
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
1388
|
+
const seg = segments[i];
|
|
1389
|
+
const next = node[seg];
|
|
1390
|
+
if (next === void 0) {
|
|
1391
|
+
const created = {};
|
|
1392
|
+
node[seg] = created;
|
|
1393
|
+
node = created;
|
|
1394
|
+
} else {
|
|
1395
|
+
node = next;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
node[segments[segments.length - 1]] = value;
|
|
1399
|
+
}
|
|
1400
|
+
function deleteJsonLeaf(root, segments) {
|
|
1401
|
+
let node = root;
|
|
1402
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
1403
|
+
const next = node[segments[i]];
|
|
1404
|
+
if (next === null || typeof next !== "object" || Array.isArray(next)) return;
|
|
1405
|
+
node = next;
|
|
1406
|
+
}
|
|
1407
|
+
delete node[segments[segments.length - 1]];
|
|
1408
|
+
}
|
|
1409
|
+
function claudeEnvToken(name) {
|
|
1410
|
+
return `\${${name}}`;
|
|
1411
|
+
}
|
|
1412
|
+
function normalizeSessionSource(source) {
|
|
1413
|
+
switch (source) {
|
|
1414
|
+
case "compact":
|
|
1415
|
+
return "compact";
|
|
1416
|
+
case "resume":
|
|
1417
|
+
return "resume";
|
|
1418
|
+
case "clear":
|
|
1419
|
+
return "clear";
|
|
1420
|
+
default:
|
|
1421
|
+
return "startup";
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
var adapter = new ClaudeCodeAdapter();
|
|
1425
|
+
var claude_code_default = adapter;
|
|
1426
|
+
export {
|
|
1427
|
+
ClaudeCodeAdapter,
|
|
1428
|
+
adapter,
|
|
1429
|
+
claudeSensitiveKeyViolation,
|
|
1430
|
+
claude_code_default as default
|
|
1431
|
+
};
|
|
1432
|
+
//# sourceMappingURL=claude-code-4275HQU4.js.map
|