@ken-jo/agent-connector 0.4.1 → 0.4.6

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.
Files changed (92) hide show
  1. package/README.md +2 -2
  2. package/dist/{action-WT7GVRCC.js → action-EV63GAPY.js} +8 -8
  3. package/dist/amp-XJQ4TDAU.js +666 -0
  4. package/dist/amp-XJQ4TDAU.js.map +1 -0
  5. package/dist/{chunk-QREL7LZJ.js → chunk-2X4EH2D7.js} +2 -2
  6. package/dist/{chunk-RIMVWCZY.js → chunk-5G4EPJ4Y.js} +5 -5
  7. package/dist/{chunk-DCKFYWGM.js → chunk-BMD7ANKF.js} +10 -10
  8. package/dist/{chunk-4ENJK5S2.js → chunk-DCK5FADX.js} +2 -2
  9. package/dist/{chunk-EGCBZ2DC.js → chunk-EROJDXMW.js} +2 -2
  10. package/dist/{chunk-5OWS5CMA.js → chunk-H67V7DEL.js} +5 -5
  11. package/dist/{chunk-24IDL5RW.js → chunk-MZF2YCXX.js} +2 -2
  12. package/dist/{chunk-O5TWCIJN.js → chunk-QC5A3TFQ.js} +18 -18
  13. package/dist/{chunk-2AVLX7GH.js → chunk-QOGMDOUX.js} +3 -3
  14. package/dist/{chunk-URHQMFB3.js → chunk-RQPKL62M.js} +61 -15
  15. package/dist/chunk-RQPKL62M.js.map +1 -0
  16. package/dist/{chunk-XJ4S47CT.js → chunk-SRVAA2VN.js} +2 -2
  17. package/dist/{claude-code-W4PUFZL5.js → claude-code-T2LPFNC3.js} +4 -4
  18. package/dist/cli/sdk.js +4 -4
  19. package/dist/cli.js +1 -1
  20. package/dist/{codebuff-DH5Y7GAT.js → codebuff-PODYPOOP.js} +126 -5
  21. package/dist/codebuff-PODYPOOP.js.map +1 -0
  22. package/dist/{codex-DDHIV767.js → codex-O4G7D7KK.js} +19 -3
  23. package/dist/codex-O4G7D7KK.js.map +1 -0
  24. package/dist/{detect-KG5FG3EV.js → detect-AHJIUMBO.js} +4 -4
  25. package/dist/{doctor-FLZF34Y3.js → doctor-G5MLCPAB.js} +8 -8
  26. package/dist/{hook-XR3Z45FB.js → hook-76JBKTUX.js} +8 -8
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +2 -2
  29. package/dist/{install-BSR62UHJ.js → install-R7252QFH.js} +8 -8
  30. package/dist/{introspect-Nw8hTReX.d.ts → introspect-Cn3MSA4_.d.ts} +1 -1
  31. package/dist/{kilo-5NWOXVSD.js → kilo-IX4T3OHJ.js} +100 -4
  32. package/dist/kilo-IX4T3OHJ.js.map +1 -0
  33. package/dist/{kilo-cli-UZ7R5UOP.js → kilo-cli-2AYCXBQD.js} +107 -11
  34. package/dist/kilo-cli-2AYCXBQD.js.map +1 -0
  35. package/dist/{leaderboard-MJG24O35.js → leaderboard-V6VWA5ZV.js} +2 -2
  36. package/dist/{nemoclaw-W5KEJ3WX.js → nemoclaw-CQYXZVQN.js} +2 -2
  37. package/dist/{openclaw-EXJEFNZI.js → openclaw-UZC437RW.js} +2 -2
  38. package/dist/{opencode-WNXYJR4X.js → opencode-L4BGFP25.js} +4 -1
  39. package/dist/opencode-L4BGFP25.js.map +1 -0
  40. package/dist/{package-ACZXJHWB.js → package-DNLQTPNN.js} +6 -6
  41. package/dist/runtime/index.d.ts +1 -1
  42. package/dist/runtime/index.js +6 -6
  43. package/dist/sdk/index.d.ts +3 -3
  44. package/dist/sdk/index.js +3 -3
  45. package/dist/sdk/test.d.ts +2 -2
  46. package/dist/sdk/test.js +5 -5
  47. package/dist/{serve-GER7URPR.js → serve-EOVWYBGZ.js} +8 -8
  48. package/dist/{status-LAJE3BM5.js → status-VTD6XXLW.js} +6 -6
  49. package/dist/{statusline-L376ZPZU.js → statusline-3ET5YPQ5.js} +7 -7
  50. package/dist/{telemetry-B3DX3PWU.js → telemetry-QCTMKDBQ.js} +5 -5
  51. package/dist/{types-CRVrFWeU.d.ts → types-_6arHbX9.d.ts} +5 -4
  52. package/dist/{uninstall-ZYTW7YMZ.js → uninstall-FL3SH2DT.js} +8 -8
  53. package/dist/{upgrade-4LUEEOZW.js → upgrade-SLFG2FCT.js} +8 -8
  54. package/dist/{usage-ZFBVQF3H.js → usage-IRBLVUZM.js} +2 -2
  55. package/dist/{usage-event-GZ32AGEB.js → usage-event-VFF77WYY.js} +7 -7
  56. package/package.json +1 -1
  57. package/dist/amp-4U33CU7E.js +0 -300
  58. package/dist/amp-4U33CU7E.js.map +0 -1
  59. package/dist/chunk-URHQMFB3.js.map +0 -1
  60. package/dist/codebuff-DH5Y7GAT.js.map +0 -1
  61. package/dist/codex-DDHIV767.js.map +0 -1
  62. package/dist/kilo-5NWOXVSD.js.map +0 -1
  63. package/dist/kilo-cli-UZ7R5UOP.js.map +0 -1
  64. package/dist/opencode-WNXYJR4X.js.map +0 -1
  65. /package/dist/{action-WT7GVRCC.js.map → action-EV63GAPY.js.map} +0 -0
  66. /package/dist/{chunk-QREL7LZJ.js.map → chunk-2X4EH2D7.js.map} +0 -0
  67. /package/dist/{chunk-RIMVWCZY.js.map → chunk-5G4EPJ4Y.js.map} +0 -0
  68. /package/dist/{chunk-DCKFYWGM.js.map → chunk-BMD7ANKF.js.map} +0 -0
  69. /package/dist/{chunk-4ENJK5S2.js.map → chunk-DCK5FADX.js.map} +0 -0
  70. /package/dist/{chunk-EGCBZ2DC.js.map → chunk-EROJDXMW.js.map} +0 -0
  71. /package/dist/{chunk-5OWS5CMA.js.map → chunk-H67V7DEL.js.map} +0 -0
  72. /package/dist/{chunk-24IDL5RW.js.map → chunk-MZF2YCXX.js.map} +0 -0
  73. /package/dist/{chunk-O5TWCIJN.js.map → chunk-QC5A3TFQ.js.map} +0 -0
  74. /package/dist/{chunk-2AVLX7GH.js.map → chunk-QOGMDOUX.js.map} +0 -0
  75. /package/dist/{chunk-XJ4S47CT.js.map → chunk-SRVAA2VN.js.map} +0 -0
  76. /package/dist/{claude-code-W4PUFZL5.js.map → claude-code-T2LPFNC3.js.map} +0 -0
  77. /package/dist/{detect-KG5FG3EV.js.map → detect-AHJIUMBO.js.map} +0 -0
  78. /package/dist/{doctor-FLZF34Y3.js.map → doctor-G5MLCPAB.js.map} +0 -0
  79. /package/dist/{hook-XR3Z45FB.js.map → hook-76JBKTUX.js.map} +0 -0
  80. /package/dist/{install-BSR62UHJ.js.map → install-R7252QFH.js.map} +0 -0
  81. /package/dist/{leaderboard-MJG24O35.js.map → leaderboard-V6VWA5ZV.js.map} +0 -0
  82. /package/dist/{nemoclaw-W5KEJ3WX.js.map → nemoclaw-CQYXZVQN.js.map} +0 -0
  83. /package/dist/{openclaw-EXJEFNZI.js.map → openclaw-UZC437RW.js.map} +0 -0
  84. /package/dist/{package-ACZXJHWB.js.map → package-DNLQTPNN.js.map} +0 -0
  85. /package/dist/{serve-GER7URPR.js.map → serve-EOVWYBGZ.js.map} +0 -0
  86. /package/dist/{status-LAJE3BM5.js.map → status-VTD6XXLW.js.map} +0 -0
  87. /package/dist/{statusline-L376ZPZU.js.map → statusline-3ET5YPQ5.js.map} +0 -0
  88. /package/dist/{telemetry-B3DX3PWU.js.map → telemetry-QCTMKDBQ.js.map} +0 -0
  89. /package/dist/{uninstall-ZYTW7YMZ.js.map → uninstall-FL3SH2DT.js.map} +0 -0
  90. /package/dist/{upgrade-4LUEEOZW.js.map → upgrade-SLFG2FCT.js.map} +0 -0
  91. /package/dist/{usage-ZFBVQF3H.js.map → usage-IRBLVUZM.js.map} +0 -0
  92. /package/dist/{usage-event-GZ32AGEB.js.map → usage-event-VFF77WYY.js.map} +0 -0
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ rewriteEnvRefs
4
+ } from "./chunk-DBQMX75U.js";
5
+ import {
6
+ BaseAdapter
7
+ } from "./chunk-CPLK7GTE.js";
8
+ import {
9
+ renderSkillMd
10
+ } from "./chunk-7L6ITC4T.js";
11
+ import {
12
+ buildServeWrapperCommand,
13
+ shouldWrapForTelemetry
14
+ } from "./chunk-I5MLE6S7.js";
15
+ import "./chunk-N6HH7K6P.js";
16
+ import "./chunk-TRQKHP7D.js";
17
+
18
+ // src/adapters/amp/index.ts
19
+ import {
20
+ chmodSync,
21
+ existsSync,
22
+ mkdirSync,
23
+ readFileSync,
24
+ rmSync,
25
+ writeFileSync
26
+ } from "fs";
27
+ import { homedir } from "os";
28
+ import { dirname, join } from "path";
29
+ var HOST = "amp";
30
+ var MCP_ROOT_KEY = "amp.mcpServers";
31
+ var EVENT_TO_AMP = {
32
+ SessionStart: "session.start",
33
+ UserPromptSubmit: "agent.start",
34
+ PreToolUse: "tool.call",
35
+ PostToolUse: "tool.result",
36
+ Stop: "agent.end"
37
+ };
38
+ var AmpAdapter = class extends BaseAdapter {
39
+ id = HOST;
40
+ name = "Amp";
41
+ paradigm = "ts-plugin";
42
+ capabilities = {
43
+ // Memory surface: AGENTS.md-first managed block via the BaseAdapter default
44
+ // (memoryTargets: project <projectDir>/AGENTS.md; user scope where documented).
45
+ supportsMemory: true,
46
+ // amp.on lifecycle events with a canonical analog (EVENT_TO_AMP):
47
+ // tool.call / tool.result / session.start / agent.start / agent.end.
48
+ preToolUse: true,
49
+ postToolUse: true,
50
+ // Amp has no documented compaction hook.
51
+ preCompact: false,
52
+ sessionStart: true,
53
+ // Amp documents NO session.end event — SessionEnd is an honest gap.
54
+ sessionEnd: false,
55
+ userPromptSubmit: true,
56
+ stop: true,
57
+ notification: false,
58
+ // Newer events: Amp documents no permission/approval, post-tool-failure, or
59
+ // subagent lifecycle hook with a canonical analog, so permissionRequest /
60
+ // postToolUseFailure / subagentStart / subagentStop stay unset — the
61
+ // generated bridge never references them and install reports them as
62
+ // "unsupported here".
63
+ // tool.call's documented decision surface is { action: "allow" |
64
+ // "reject-and-continue" | "modify" | "synthesize" } (ampcode.com/manual →
65
+ // Plugins → tool.call). We wire allow + reject-and-continue (block); the
66
+ // "modify" input-rewrite shape is not byte-documented, so canModifyArgs:false.
67
+ canModifyArgs: false,
68
+ // tool.result says "return a replacement status/output" but the manual never
69
+ // documents the replacement OBJECT SHAPE (which keys). Rather than guess it,
70
+ // PostToolUse is observe-only and canModifyOutput stays false until the shape
71
+ // is primary-source-verified (honesty bar: under-claim, never emit a guessed
72
+ // mutation contract).
73
+ canModifyOutput: false,
74
+ // session.start has no documented context-injection surface.
75
+ canInjectSessionContext: false,
76
+ // Amp registers stdio and Streamable HTTP MCP servers.
77
+ transports: ["stdio", "http"],
78
+ // Skills: Amp reads SKILL.md from ~/.config/agents/skills/<name>/SKILL.md
79
+ // (user) and <projectDir>/.agents/skills/<name>/SKILL.md (project) — a root
80
+ // OUTSIDE the config dir, so the skillDir() helper resolves it explicitly.
81
+ supportsSkills: true,
82
+ // Native passthrough hooks: amp.on events with no canonical HookEventName
83
+ // analog are reachable via platforms["amp"].nativeHooks; the generated plugin
84
+ // registers + bridges each declared native event verbatim and the home-bin
85
+ // runtime dispatches it host-generically via runNativeHook.
86
+ supportsNativeHooks: true
87
+ };
88
+ // ── Detection ────────────────────────────────────────────────────────────
89
+ detectInstalled(projectDir) {
90
+ const userDir = join(homedir(), ".config", "amp");
91
+ const userSettings = join(userDir, "settings.json");
92
+ const projectDirAmp = join(projectDir, ".amp");
93
+ const projectSettings = join(projectDirAmp, "settings.json");
94
+ const userInstalled = existsSync(userDir) || existsSync(userSettings);
95
+ const projectInstalled = existsSync(projectDirAmp) || existsSync(projectSettings);
96
+ const installed = userInstalled || projectInstalled;
97
+ const scope = projectInstalled && !userInstalled ? "project" : "user";
98
+ const configPath = scope === "project" ? projectSettings : userSettings;
99
+ return {
100
+ id: this.id,
101
+ name: this.name,
102
+ installed,
103
+ paradigm: this.paradigm,
104
+ capabilities: this.capabilities,
105
+ configPath,
106
+ scope,
107
+ reason: installed ? `found Amp config (${scope}) at ${configPath}` : `no Amp config at ${userSettings} or ${projectSettings}`,
108
+ confidence: installed ? "high" : "low"
109
+ };
110
+ }
111
+ // ── Native paths ─────────────────────────────────────────────────────────
112
+ getConfigDir(ctx) {
113
+ return ctx.scope === "project" ? join(ctx.projectDir, ".amp") : join(homedir(), ".config", "amp");
114
+ }
115
+ getServerConfigPath(ctx) {
116
+ return join(this.getConfigDir(ctx), "settings.json");
117
+ }
118
+ /**
119
+ * For this ts-plugin host the "hook config path" is the generated plugin
120
+ * MODULE. Amp loads every `.ts` file in `.amp/plugins/`, so writing this file
121
+ * IS the registration. The `.amp/plugins/` dir is documented at PROJECT scope
122
+ * (Amp documents no user-scope plugins dir — see pluginsDir); the user-scope
123
+ * path is returned for doctor/uninstall symmetry but installHooks warn-skips it.
124
+ */
125
+ getHookConfigPath(ctx) {
126
+ return join(this.pluginsDir(ctx), `${ctx.connector.id}.ts`);
127
+ }
128
+ /**
129
+ * Plugin directory Amp auto-loads `.ts` modules from. PROJECT scope is the
130
+ * documented `<projectDir>/.amp/plugins`; user scope mirrors the MCP config dir
131
+ * (`~/.config/amp/plugins`) for path symmetry only — installHooks never writes
132
+ * there because Amp documents no user-scope plugins dir.
133
+ */
134
+ pluginsDir(ctx) {
135
+ return ctx.scope === "project" ? join(ctx.projectDir, ".amp", "plugins") : join(this.getConfigDir(ctx), "plugins");
136
+ }
137
+ // ── MCP server install / uninstall ───────────────────────────────────────
138
+ installServer(ctx) {
139
+ const { connector } = ctx;
140
+ const override = connector.platforms[HOST]?.server;
141
+ if (!connector.server || override === false) {
142
+ return [
143
+ {
144
+ platform: this.id,
145
+ action: "skip",
146
+ detail: connector.server ? "server registration disabled for amp" : "connector declares no MCP server"
147
+ }
148
+ ];
149
+ }
150
+ const server = override && typeof override === "object" ? { ...connector.server, ...override } : connector.server;
151
+ const serverPath = this.getServerConfigPath(ctx);
152
+ const entry = this.renderServerEntry(ctx, server);
153
+ return [
154
+ this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun)
155
+ ];
156
+ }
157
+ uninstallServer(ctx) {
158
+ const serverPath = this.getServerConfigPath(ctx);
159
+ return [
160
+ this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun)
161
+ ];
162
+ }
163
+ /** Render a normalized ServerDef into Amp's native `amp.mcpServers` entry. */
164
+ renderServerEntry(ctx, server) {
165
+ const transport = server.transport;
166
+ if (transport === "stdio") {
167
+ let command = server.command ?? "";
168
+ let args = [...server.args ?? []];
169
+ if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {
170
+ const wrapped = buildServeWrapperCommand(
171
+ ctx.homeBinPath,
172
+ ctx.connector.id,
173
+ command,
174
+ args,
175
+ ctx.scope,
176
+ this.id
177
+ );
178
+ command = wrapped.command;
179
+ args = wrapped.args;
180
+ }
181
+ const entry2 = { command: this.rewrite(command) };
182
+ if (args.length > 0) entry2.args = args.map((a) => this.rewrite(a));
183
+ const env = this.renderEnv(server.env);
184
+ if (env) entry2.env = env;
185
+ return entry2;
186
+ }
187
+ const entry = { url: this.rewrite(server.url ?? "") };
188
+ const headers = this.renderEnv(server.headers);
189
+ if (headers) entry.headers = headers;
190
+ return entry;
191
+ }
192
+ /**
193
+ * Render env/header values. Amp supports native `${VAR_NAME}` interpolation,
194
+ * so translate `${env:VAR}` refs to that native token rather than baking
195
+ * secrets into the file. Literals pass through unchanged.
196
+ */
197
+ renderEnv(env) {
198
+ if (!env || Object.keys(env).length === 0) return void 0;
199
+ const out = {};
200
+ for (const [k, v] of Object.entries(env)) out[k] = this.rewrite(v);
201
+ return out;
202
+ }
203
+ /** Translate `${env:VAR(:-default)}` to Amp's native `${VAR}` token. */
204
+ rewrite(value) {
205
+ return rewriteEnvRefs(value, ampEnvToken);
206
+ }
207
+ // ── Skills surface ───────────────────────────────────────────────────────
208
+ // CONTENT-ONLY: pure native-file writers under the skill root. No runtime
209
+ // dispatch, no home-bin pointer, no telemetry wrap. Idempotent (byte-identical
210
+ // → skip) via writeContentFile and reversible via removeContentFile. Honors
211
+ // platforms["amp"].skills === false to skip.
212
+ //
213
+ // QUIRK: the skill root is NOT under getConfigDir (~/.config/amp). Amp reads
214
+ // SKILL.md from the sibling cross-agent `agents` tree:
215
+ // user scope → ~/.config/agents/skills/<name>/SKILL.md
216
+ // project scope → <projectDir>/.agents/skills/<name>/SKILL.md
217
+ // so skillDir() resolves it explicitly rather than reusing getConfigDir.
218
+ /** Skill root: user `~/.config/agents/skills`, project `<projectDir>/.agents/skills`. */
219
+ skillsDir(ctx) {
220
+ return ctx.scope === "project" ? join(ctx.projectDir, ".agents", "skills") : join(homedir(), ".config", "agents", "skills");
221
+ }
222
+ /** Native skill dir: <skillRoot>/<name>. */
223
+ skillDir(ctx, name) {
224
+ return join(this.skillsDir(ctx), name);
225
+ }
226
+ installSkills(ctx) {
227
+ const { connector } = ctx;
228
+ if (connector.platforms[HOST]?.skills === false) {
229
+ return [{ platform: this.id, action: "skip", detail: "skills disabled for amp" }];
230
+ }
231
+ if (connector.skills.length === 0) {
232
+ return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
233
+ }
234
+ const changes = [];
235
+ for (const skill of connector.skills) {
236
+ const dir = this.skillDir(ctx, skill.name);
237
+ changes.push(
238
+ this.writeContentFile(join(dir, "SKILL.md"), this.renderSkill(skill), ctx.dryRun)
239
+ );
240
+ for (const [rel, contents] of Object.entries(skill.resources ?? {})) {
241
+ const target = this.resolveWithin(dir, rel);
242
+ if (target === null) {
243
+ changes.push({
244
+ platform: this.id,
245
+ action: "warn",
246
+ detail: `skill resource "${rel}" escapes the skill dir; skipped`
247
+ });
248
+ continue;
249
+ }
250
+ changes.push(this.writeContentFile(target, contents, ctx.dryRun));
251
+ }
252
+ }
253
+ return changes;
254
+ }
255
+ uninstallSkills(ctx) {
256
+ const { connector } = ctx;
257
+ if (connector.skills.length === 0) {
258
+ return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
259
+ }
260
+ const changes = [];
261
+ for (const skill of connector.skills) {
262
+ const dir = this.skillDir(ctx, skill.name);
263
+ changes.push(this.removeContentFile(join(dir, "SKILL.md"), ctx.dryRun));
264
+ for (const rel of Object.keys(skill.resources ?? {})) {
265
+ const target = this.resolveWithin(dir, rel);
266
+ if (target === null) continue;
267
+ changes.push(this.removeContentFile(target, ctx.dryRun));
268
+ }
269
+ changes.push(this.removeDirIfEmpty(dir, ctx.dryRun));
270
+ }
271
+ return changes;
272
+ }
273
+ /** Render a skill's SKILL.md (delegates to the shared renderer). */
274
+ renderSkill(skill) {
275
+ return renderSkillMd(skill);
276
+ }
277
+ // ── Hook install / uninstall (ts-plugin module) ──────────────────────────
278
+ installHooks(ctx) {
279
+ const pluginPath = this.getHookConfigPath(ctx);
280
+ if (ctx.scope !== "project") {
281
+ return [
282
+ {
283
+ platform: this.id,
284
+ action: "warn",
285
+ path: pluginPath,
286
+ detail: "amp plugins are project-scoped (.amp/plugins/) \u2014 no user-scope plugins dir is documented; skipped"
287
+ }
288
+ ];
289
+ }
290
+ const canonicalDisabled = ctx.connector.platforms[HOST]?.hooks === false;
291
+ const hasCanonical = !canonicalDisabled && ctx.connector.hookEvents.length > 0;
292
+ const hasNative = this.nativeHookEvents(ctx).length > 0;
293
+ if (!hasCanonical && !hasNative) {
294
+ return [
295
+ {
296
+ platform: this.id,
297
+ action: "skip",
298
+ path: pluginPath,
299
+ detail: canonicalDisabled ? "hooks disabled for amp" : "connector declares no hooks"
300
+ }
301
+ ];
302
+ }
303
+ const files = this.synthesizePlugin(ctx);
304
+ const changes = [];
305
+ for (const file of files) {
306
+ const before = existsSync(file.path) ? this.safeRead(file.path) : void 0;
307
+ let action;
308
+ if (before === void 0) action = "create";
309
+ else if (before === file.contents) action = "skip";
310
+ else action = "update";
311
+ if (action !== "skip" && !ctx.dryRun) {
312
+ ensureDir(dirname(file.path));
313
+ writeFileSync(file.path, file.contents, "utf8");
314
+ chmodSync(file.path, file.executable ? 493 : 420);
315
+ }
316
+ changes.push({
317
+ platform: this.id,
318
+ action,
319
+ path: file.path,
320
+ detail: `amp plugin module (${this.hookDetail(ctx)})`
321
+ });
322
+ }
323
+ return changes;
324
+ }
325
+ uninstallHooks(ctx) {
326
+ const pluginPath = this.getHookConfigPath(ctx);
327
+ if (!existsSync(pluginPath)) {
328
+ return [
329
+ {
330
+ platform: this.id,
331
+ action: "skip",
332
+ path: pluginPath,
333
+ detail: "no amp plugin module present"
334
+ }
335
+ ];
336
+ }
337
+ if (!ctx.dryRun) rmSync(pluginPath, { force: true });
338
+ return [
339
+ {
340
+ platform: this.id,
341
+ action: "remove",
342
+ path: pluginPath,
343
+ detail: "amp plugin module"
344
+ }
345
+ ];
346
+ }
347
+ /**
348
+ * Human-facing summary of which declared events the synthesized module ACTUALLY
349
+ * wires. Only events present in EVENT_TO_AMP are mapped/wired; any declared
350
+ * event with no Amp mapping (e.g. SessionEnd) is reported separately as
351
+ * "unsupported here" so the detail never overstates coverage.
352
+ */
353
+ hookDetail(ctx) {
354
+ const canonicalOff = ctx.connector.platforms[HOST]?.hooks === false;
355
+ const declared = canonicalOff ? [] : ctx.connector.hookEvents;
356
+ const mapped = declared.filter((e) => EVENT_TO_AMP[e] !== void 0);
357
+ const unsupported = declared.filter((e) => EVENT_TO_AMP[e] === void 0);
358
+ const native = this.nativeHookEvents(ctx);
359
+ const parts = [];
360
+ if (mapped.length > 0) parts.push(mapped.join(","));
361
+ if (native.length > 0) parts.push(`native: ${native.join(",")}`);
362
+ const base = parts.join("; ") || "no canonical hooks";
363
+ return unsupported.length > 0 ? `${base}; unsupported here: ${unsupported.join(",")}` : base;
364
+ }
365
+ /**
366
+ * Amp-native passthrough events declared on platforms["amp"].nativeHooks
367
+ * (amp.on event names with no canonical HookEventName analog). Emitted as
368
+ * generic bridge registrations and dispatched host-generically by the home-bin's
369
+ * runNativeHook.
370
+ */
371
+ nativeHookEvents(ctx) {
372
+ return Object.keys(ctx.connector.platforms[HOST]?.nativeHooks ?? {});
373
+ }
374
+ // ── ts-plugin synthesis ──────────────────────────────────────────────────
375
+ /**
376
+ * Build ONE self-contained Amp plugin module (`.amp/plugins/<id>.ts`). It
377
+ * imports nothing from agent-connector; each `amp.on(...)` handler shells out to
378
+ * the universal hook entrypoint via child_process and applies the normalized
379
+ * HookResponse. Its default export is the Amp plugin function `(amp) => void`.
380
+ */
381
+ synthesizePlugin(ctx) {
382
+ const path = this.getHookConfigPath(ctx);
383
+ const contents = this.buildPluginSource(ctx);
384
+ return [{ path, contents, executable: false }];
385
+ }
386
+ /** Compose the generated plugin source with plain string concatenation. */
387
+ buildPluginSource(ctx) {
388
+ const homeBin = JSON.stringify(ctx.homeBinPath);
389
+ const connectorId = JSON.stringify(ctx.connector.id);
390
+ const canonicalOff = ctx.connector.platforms[HOST]?.hooks === false;
391
+ const events = canonicalOff ? [] : ctx.connector.hookEvents.filter(
392
+ (e) => EVENT_TO_AMP[e] !== void 0
393
+ );
394
+ const has = (e) => events.includes(e);
395
+ const header = `/**
396
+ * AUTO-GENERATED by agent-connector \u2014 DO NOT EDIT.
397
+ *
398
+ * Self-contained Amp plugin for connector ${ctx.connector.id}.
399
+ * It imports nothing from agent-connector: every hook invocation shells out to
400
+ * the stable home binary's universal entrypoint and JSON-parses the normalized
401
+ * response. Fail-open: any bridge error degrades to a no-op (never wedges Amp).
402
+ *
403
+ * Amp loads this module from .amp/plugins/${ctx.connector.id}.ts; the default
404
+ * export is the plugin function (amp) => void called once with the PluginAPI.
405
+ */
406
+ import type { PluginAPI } from "@ampcode/plugin";
407
+ import { execFileSync, execSync } from "node:child_process";
408
+
409
+ const HOME_BIN = ${homeBin};
410
+ const CONNECTOR_ID = ${connectorId};
411
+
412
+ /**
413
+ * Invoke the universal hook entrypoint for one event.
414
+ * @param {string} event canonical event name
415
+ * @param {object} payload amp-shaped payload posted on stdin
416
+ * @returns {object|null} normalized HookResponse, or null on any failure
417
+ */
418
+ function bridge(event, payload) {
419
+ try {
420
+ // On Windows HOME_BIN is the agent-connector.cmd launcher: Node cannot
421
+ // execFile a batch file, and shell+args is deprecated (DEP0190), so run one
422
+ // quoted command line via a shell. POSIX keeps the direct execFile (no shell).
423
+ const args = ["hook", "amp", event, "--connector", CONNECTOR_ID];
424
+ const opts = { input: JSON.stringify(payload), encoding: "utf8" };
425
+ const stdout =
426
+ process.platform === "win32"
427
+ ? execSync([HOME_BIN, ...args].map((a) => '"' + a + '"').join(" "), opts)
428
+ : execFileSync(HOME_BIN, args, opts);
429
+ const text = (stdout || "").trim();
430
+ if (text === "") return { decision: "allow" };
431
+ return JSON.parse(text);
432
+ } catch {
433
+ // Fail-open \u2014 never wedge an Amp tool call / lifecycle event.
434
+ return null;
435
+ }
436
+ }
437
+
438
+ // Amp documents no project-dir env var; the plugin process runs in the workspace
439
+ // root, so resolve once at load. Session id is rebound on each session.start.
440
+ const PROJECT_DIR = process.cwd();
441
+ let SESSION_ID = "";
442
+ `;
443
+ const handlers = [];
444
+ if (has("SessionStart")) {
445
+ handlers.push(` // SessionStart \u2192 rebind the session id and notify the connector.
446
+ // Amp's session.start event carries event.thread.id (ampcode.com/manual \u2192
447
+ // Plugins \u2192 session.start: "Example session.start for \${event.thread.id}").
448
+ amp.on("session.start", async (event) => {
449
+ SESSION_ID =
450
+ (event && event.thread && event.thread.id) || "amp-session";
451
+ bridge("SessionStart", { sessionId: SESSION_ID, projectDir: PROJECT_DIR });
452
+ });`);
453
+ }
454
+ if (has("UserPromptSubmit")) {
455
+ handlers.push(` // UserPromptSubmit \u2192 agent.start "fires when the user submits a prompt"
456
+ // (ampcode.com/manual). The manual's example takes (_event) and documents no
457
+ // prompt field or return surface, so this is observe-only \u2014 the raw event is
458
+ // forwarded for the connector to inspect; any deny/context decision is a no-op.
459
+ amp.on("agent.start", async (event) => {
460
+ bridge("UserPromptSubmit", {
461
+ sessionId: SESSION_ID,
462
+ projectDir: PROJECT_DIR,
463
+ raw: event,
464
+ });
465
+ });`);
466
+ }
467
+ if (has("PreToolUse")) {
468
+ handlers.push(` // PreToolUse \u2192 Amp's tool.call returns a decision union (ampcode.com/manual \u2192
469
+ // Plugins \u2192 tool.call): { action: "allow" } to run, or { action:
470
+ // "reject-and-continue", message } to block and let the agent continue. The
471
+ // tool name is event.tool (verified); the input field is not byte-documented,
472
+ // so event.input is a best-effort hint for the connector's matcher (the matcher
473
+ // keys on the verified tool name). A deny/ask decision maps to reject-and-
474
+ // continue; everything else allows.
475
+ amp.on("tool.call", async (event) => {
476
+ const payload = {
477
+ toolName: (event && event.tool) || "",
478
+ toolInput: (event && event.input) || {},
479
+ sessionId: SESSION_ID,
480
+ projectDir: PROJECT_DIR,
481
+ };
482
+ const res = bridge("PreToolUse", payload);
483
+ if (res && (res.decision === "deny" || res.decision === "ask")) {
484
+ return {
485
+ action: "reject-and-continue",
486
+ message: res.reason || "Blocked by ${ctx.connector.id}",
487
+ };
488
+ }
489
+ return { action: "allow" };
490
+ });`);
491
+ }
492
+ if (has("PostToolUse")) {
493
+ handlers.push(` // PostToolUse \u2192 tool.result "fires after a tool finishes and before the result
494
+ // is sent back to the model" (ampcode.com/manual). The tool name is event.tool
495
+ // and the error signal is event.status === "error" (both verified). The manual
496
+ // says a replacement status/output CAN be returned but never documents the
497
+ // replacement object shape, so this is observe-only (canModifyOutput:false) \u2014
498
+ // the raw event is forwarded; we never return a guessed replacement.
499
+ amp.on("tool.result", async (event) => {
500
+ bridge("PostToolUse", {
501
+ toolName: (event && event.tool) || "",
502
+ isError: !!(event && event.status === "error"),
503
+ sessionId: SESSION_ID,
504
+ projectDir: PROJECT_DIR,
505
+ raw: event,
506
+ });
507
+ });`);
508
+ }
509
+ if (has("Stop")) {
510
+ handlers.push(` // Stop \u2192 observe turn end. Amp's agent.end exposes no decision surface, so
511
+ // this is observe-only (a deny decision degrades to a no-op).
512
+ amp.on("agent.end", async (event) => {
513
+ bridge("Stop", { sessionId: SESSION_ID, projectDir: PROJECT_DIR, raw: event });
514
+ });`);
515
+ }
516
+ for (const ev of this.nativeHookEvents(ctx)) {
517
+ const key = JSON.stringify(ev);
518
+ handlers.push(` // nativeHooks passthrough \u2192 ${ev}
519
+ amp.on(${key}, async (event) => {
520
+ bridge(${key}, { sessionId: SESSION_ID, projectDir: PROJECT_DIR, raw: event });
521
+ });`);
522
+ }
523
+ const factory = `
524
+ export default function plugin(amp: PluginAPI) {
525
+ ${handlers.join("\n\n")}
526
+ }
527
+ `;
528
+ return header + factory;
529
+ }
530
+ // ── Runtime: parse OUR bridge payload → normalized event ─────────────────
531
+ /**
532
+ * `raw` is the payload OUR generated plugin posts (NOT a host-native shape):
533
+ * { toolName?, toolInput?, toolOutput?, isError?, prompt?, sessionId?, projectDir? }
534
+ * so this maps straight through.
535
+ */
536
+ parseEvent(event, raw) {
537
+ const input = raw ?? {};
538
+ const base = {
539
+ hostPlatform: HOST,
540
+ connectorId: "",
541
+ sessionId: typeof input.sessionId === "string" ? input.sessionId : "",
542
+ raw,
543
+ ...typeof input.projectDir === "string" ? { projectDir: input.projectDir } : {}
544
+ };
545
+ switch (event) {
546
+ case "PreToolUse": {
547
+ const ev = {
548
+ ...base,
549
+ toolName: input.toolName ?? "",
550
+ toolInput: input.toolInput ?? {}
551
+ };
552
+ return ev;
553
+ }
554
+ case "PostToolUse": {
555
+ const ev = {
556
+ ...base,
557
+ toolName: input.toolName ?? "",
558
+ toolInput: input.toolInput ?? {},
559
+ ...typeof input.toolOutput === "string" ? { toolOutput: input.toolOutput } : {},
560
+ ...typeof input.isError === "boolean" ? { isError: input.isError } : {}
561
+ };
562
+ return ev;
563
+ }
564
+ case "UserPromptSubmit": {
565
+ const ev = {
566
+ ...base,
567
+ prompt: typeof input.prompt === "string" ? input.prompt : ""
568
+ };
569
+ return ev;
570
+ }
571
+ case "Stop": {
572
+ const ev = { ...base };
573
+ return ev;
574
+ }
575
+ case "SessionStart": {
576
+ const ev = { ...base, source: "startup" };
577
+ return ev;
578
+ }
579
+ default:
580
+ return { ...base, source: "startup" };
581
+ }
582
+ }
583
+ // ── Runtime: normalized response → reply the generated bridge parses ─────
584
+ /**
585
+ * Unlike json-stdio hosts (whose reply is the host's NATIVE control payload),
586
+ * OUR generated bridge consumes this stdout directly. So the reply body is the
587
+ * NORMALIZED HookResponse itself — the bridge JSON.parses it and, for tool.call,
588
+ * maps a deny/ask decision → { action: "reject-and-continue" } (else allow).
589
+ * Other events are observe-only, so their reply is ignored by the handler.
590
+ */
591
+ formatReply(_event, response) {
592
+ return {
593
+ exitCode: 0,
594
+ stdout: JSON.stringify(response ?? { decision: "allow" })
595
+ };
596
+ }
597
+ // ── Diagnostics ──────────────────────────────────────────────────────────
598
+ getHealthChecks(ctx) {
599
+ const settingsPath = this.getServerConfigPath(ctx);
600
+ const connectorId = ctx.connector.id;
601
+ return [
602
+ {
603
+ name: `${this.name}: settings.json present`,
604
+ check: () => existsSync(settingsPath) ? { status: "OK", detail: settingsPath } : { status: "FAIL", detail: `not found: ${settingsPath}` }
605
+ },
606
+ {
607
+ name: `${this.name}: server entry registered`,
608
+ check: () => {
609
+ if (!ctx.connector.server) {
610
+ return { status: "OK", detail: "no MCP server declared" };
611
+ }
612
+ const cfg = this.readJson(settingsPath);
613
+ const bucket = cfg?.[MCP_ROOT_KEY];
614
+ if (!cfg || !bucket) {
615
+ return { status: "FAIL", detail: `no "${MCP_ROOT_KEY}" in ${settingsPath}` };
616
+ }
617
+ return connectorId in bucket ? { status: "OK", detail: `"${MCP_ROOT_KEY}".${connectorId} present` } : {
618
+ status: "FAIL",
619
+ detail: `no "${MCP_ROOT_KEY}".${connectorId} in ${settingsPath}`
620
+ };
621
+ }
622
+ },
623
+ {
624
+ name: `${this.name}: plugin module present`,
625
+ check: () => {
626
+ const hasHooks = ctx.connector.hookEvents.length > 0 || this.nativeHookEvents(ctx).length > 0;
627
+ if (!hasHooks) return { status: "OK", detail: "no hooks declared" };
628
+ if (ctx.scope !== "project") {
629
+ return {
630
+ status: "OK",
631
+ detail: "amp plugins are project-scoped (user scope skipped)"
632
+ };
633
+ }
634
+ const pluginPath = this.getHookConfigPath(ctx);
635
+ return existsSync(pluginPath) ? { status: "OK", detail: pluginPath } : { status: "FAIL", detail: `not found: ${pluginPath}` };
636
+ }
637
+ }
638
+ ];
639
+ }
640
+ /** Read a file, returning undefined on any error (idempotency compare). */
641
+ safeRead(path) {
642
+ try {
643
+ return readFileSync(path, "utf8");
644
+ } catch {
645
+ return void 0;
646
+ }
647
+ }
648
+ };
649
+ function ensureDir(dir) {
650
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
651
+ }
652
+ function ampEnvToken(name, def) {
653
+ if (def !== void 0) {
654
+ const v = process.env[name];
655
+ return v != null && v !== "" ? v : def;
656
+ }
657
+ return `\${${name}}`;
658
+ }
659
+ var adapter = new AmpAdapter();
660
+ var amp_default = adapter;
661
+ export {
662
+ AmpAdapter,
663
+ adapter,
664
+ amp_default as default
665
+ };
666
+ //# sourceMappingURL=amp-XJQ4TDAU.js.map