@agntn/harnesses 0.2.0 → 0.3.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.
Files changed (46) hide show
  1. package/README.md +47 -15
  2. package/dist/_chunks/agents.mjs +2 -30
  3. package/dist/_chunks/agents.mjs.map +1 -1
  4. package/dist/_chunks/libs/typebox.mjs +12 -9
  5. package/dist/_chunks/libs/typebox.mjs.map +1 -1
  6. package/dist/_chunks/mcp-servers.mjs +5 -34
  7. package/dist/_chunks/mcp-servers.mjs.map +1 -1
  8. package/dist/_chunks/mcp.mjs +13 -163
  9. package/dist/_chunks/mcp.mjs.map +1 -1
  10. package/dist/_chunks/output.mjs +28 -0
  11. package/dist/_chunks/output.mjs.map +1 -0
  12. package/dist/_chunks/{agents-sync.d.mts → prompt-sync.d.mts} +122 -10
  13. package/dist/_chunks/prompt-sync.d.mts.map +1 -0
  14. package/dist/_chunks/{agents-sync.mjs → prompt-sync.mjs} +863 -50
  15. package/dist/_chunks/prompt-sync.mjs.map +1 -0
  16. package/dist/_chunks/prompts.mjs +28 -0
  17. package/dist/_chunks/prompts.mjs.map +1 -0
  18. package/dist/_chunks/tool-operations.mjs +57 -19
  19. package/dist/_chunks/tool-operations.mjs.map +1 -1
  20. package/dist/_chunks/tool-schemas.d.mts +6 -0
  21. package/dist/_chunks/tool-schemas.d.mts.map +1 -1
  22. package/dist/_chunks/tool-schemas.mjs +14 -9
  23. package/dist/_chunks/tool-schemas.mjs.map +1 -1
  24. package/dist/_chunks/types.mjs +1 -1
  25. package/dist/_chunks/types.mjs.map +1 -1
  26. package/dist/cli.mjs +26 -8
  27. package/dist/cli.mjs.map +1 -1
  28. package/dist/index.d.mts +16 -2
  29. package/dist/index.d.mts.map +1 -1
  30. package/dist/index.mjs +2 -2
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mcp.d.mts.map +1 -1
  33. package/dist/mcp.mjs +179 -1
  34. package/dist/mcp.mjs.map +1 -0
  35. package/dist/tool-operations.d.mts +19 -3
  36. package/dist/tool-operations.d.mts.map +1 -1
  37. package/dist/tool-operations.mjs +2 -2
  38. package/dist/tool-schemas.d.mts +2 -2
  39. package/package.json +16 -14
  40. package/packages/omp/extensions/harnesses.ts +19 -1
  41. package/packages/pi/extensions/harnesses.ts +23 -1
  42. package/packages/shared/tui.ts +22 -1
  43. package/dist/_chunks/agents-sync.d.mts.map +0 -1
  44. package/dist/_chunks/agents-sync.mjs.map +0 -1
  45. package/dist/_chunks/mcp2.mjs +0 -17
  46. package/dist/_chunks/mcp2.mjs.map +0 -1
@@ -1,18 +1,20 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { addAbortListener } from "node:events";
3
- import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { execFile, execFileSync, spawn } from "node:child_process";
5
5
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
- import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
6
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
7
7
  import os, { homedir } from "node:os";
8
8
  import { stripVTControlCharacters } from "node:util";
9
9
  import { parse } from "smol-toml";
10
- const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|%([^%]+)%/g;
10
+ import { randomUUID } from "node:crypto";
11
+ const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|\$\{TMPDIR\}|%([^%]+)%/g;
11
12
  function resolvePathTemplate(template, options = {}) {
12
13
  const homeDir = options.homeDir ?? os.homedir();
13
14
  const projectRoot = options.projectRoot ?? process.cwd();
14
15
  return template.replaceAll(PATH_MARKERS, (match, name) => {
15
16
  if (match === "${PROJECT_ROOT}") return projectRoot;
17
+ if (match === "${TMPDIR}") return options.tempDir ?? os.tmpdir();
16
18
  if (name === void 0) return homeDir;
17
19
  return process.env[name] ?? match;
18
20
  });
@@ -21,6 +23,15 @@ function agntnConfigDir(options = {}) {
21
23
  const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME !== "" ? process.env.XDG_CONFIG_HOME : join(options.homeDir ?? os.homedir(), ".config");
22
24
  return join(base, "agntn");
23
25
  }
26
+ function agntnDataDir(options = {}) {
27
+ const base = process.env.XDG_DATA_HOME && process.env.XDG_DATA_HOME !== "" ? process.env.XDG_DATA_HOME : join(options.homeDir ?? os.homedir(), ".local", "share");
28
+ return join(base, "agntn");
29
+ }
30
+ function moveRoot(path, root, value) {
31
+ if (path === root) return value;
32
+ const rest = path.slice(root.length);
33
+ return path.startsWith(root) && (rest.startsWith("/") || rest.startsWith("\\")) ? value + rest : path;
34
+ }
24
35
  const SUPPORTED_PLATFORMS = {
25
36
  linux: true,
26
37
  darwin: true,
@@ -51,6 +62,17 @@ function requestedInvocationMode(options) {
51
62
  if (options.tools === true) return options.structured === true ? "agentStructured" : "agent";
52
63
  return options.structured === true ? "advisorStructured" : "advisor";
53
64
  }
65
+ function accessConflictError(id, invocation, options) {
66
+ if (options.readOnly !== true || options.tools !== false) return null;
67
+ const retries = [];
68
+ if (invocationTemplate(invocation, requestedInvocationMode(options))) retries.push("tools: true to keep its read-only tools");
69
+ if (invocationTemplate(invocation, requestedInvocationMode({
70
+ ...options,
71
+ readOnly: false
72
+ }))) retries.push("readOnly: false to use its advisor without tools");
73
+ const error = `Harness ${id} cannot run readOnly with tools: false, since read-only access still uses tools`;
74
+ return retries.length === 0 ? error : `${error}; retry with ${retries.join(", or with ")}`;
75
+ }
54
76
  function invocationTemplate(invocation, mode) {
55
77
  switch (mode) {
56
78
  case "advisor": return invocation.noToolsArgs;
@@ -117,11 +139,29 @@ async function terminateCommand(pid) {
117
139
  await setTimeout$1(500);
118
140
  signalProcessGroup(pid, "SIGKILL");
119
141
  }
142
+ function spawnError(command, cwd, error) {
143
+ if (!(error instanceof Error && "code" in error)) return error;
144
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") return error;
145
+ let message;
146
+ if (cwd !== void 0 && !isDirectory(cwd)) message = existsSync(cwd) ? `Working directory is not a directory: ${cwd}` : `Working directory not found: ${cwd}`;
147
+ else if (error.code === "ENOENT") message = `Command not found: ${command}. Install it or add it to PATH`;
148
+ else return error;
149
+ return Object.assign(new Error(message, { cause: error }), { code: error.code });
150
+ }
151
+ function isDirectory(path) {
152
+ try {
153
+ return statSync(path).isDirectory();
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
120
158
  function executeCommand(command, args, options) {
121
159
  return new Promise((resolve, reject) => {
122
160
  let stdout = "";
123
161
  let stderr = "";
124
162
  let stopped;
163
+ let lastOutputAt = performance.now();
164
+ let idleMs;
125
165
  let settled = false;
126
166
  let timer;
127
167
  let abortListener;
@@ -130,28 +170,38 @@ function executeCommand(command, args, options) {
130
170
  finish(null);
131
171
  return;
132
172
  }
133
- const child = spawn(command, args, {
134
- cwd: options.cwd,
135
- env: options.env ? {
136
- ...process.env,
137
- ...options.env
138
- } : process.env,
139
- stdio: [
140
- "ignore",
141
- "pipe",
142
- "pipe"
143
- ],
144
- detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
145
- });
173
+ let child;
174
+ try {
175
+ child = spawn(command, args, {
176
+ cwd: options.cwd,
177
+ env: options.env ? {
178
+ ...process.env,
179
+ ...options.env
180
+ } : process.env,
181
+ stdio: [
182
+ "ignore",
183
+ "pipe",
184
+ "pipe"
185
+ ],
186
+ detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
187
+ });
188
+ } catch (error) {
189
+ fail(spawnError(command, options.cwd, error));
190
+ return;
191
+ }
146
192
  if (options.timeoutMs) timer = setTimeout(() => stop("timeout"), options.timeoutMs);
147
193
  if (options.signal) abortListener = addAbortListener(options.signal, () => stop("abort"));
194
+ child.stdout.setEncoding("utf8");
195
+ child.stderr.setEncoding("utf8");
148
196
  child.stdout.on("data", (chunk) => {
149
- stdout += chunk.toString("utf8");
197
+ stdout += chunk;
198
+ lastOutputAt = performance.now();
150
199
  });
151
200
  child.stderr.on("data", (chunk) => {
152
- stderr += chunk.toString("utf8");
201
+ stderr += chunk;
202
+ lastOutputAt = performance.now();
153
203
  });
154
- child.on("error", fail);
204
+ child.on("error", (error) => fail(spawnError(command, options.cwd, error)));
155
205
  child.on("close", (code) => {
156
206
  if (stopped === void 0) finish(code);
157
207
  });
@@ -168,6 +218,7 @@ function executeCommand(command, args, options) {
168
218
  function stop(reason) {
169
219
  if (settled || stopped !== void 0) return;
170
220
  stopped = reason;
221
+ idleMs = Math.round(performance.now() - lastOutputAt);
171
222
  cleanup();
172
223
  if (child.pid === void 0) return;
173
224
  terminateCommand(child.pid).then(() => {
@@ -191,12 +242,17 @@ function executeCommand(command, args, options) {
191
242
  stderr,
192
243
  exitCode: stopped === void 0 ? code : null,
193
244
  timedOut: stopped === "timeout",
194
- aborted: stopped === "abort"
245
+ aborted: stopped === "abort",
246
+ ...idleMs === void 0 ? {} : { idleMs }
195
247
  });
196
248
  }
197
249
  });
198
250
  }
199
251
  var Harness = class {
252
+ promptTemplates = [];
253
+ promptTemplateSyncTarget = null;
254
+ temp = [];
255
+ envOverrides = [];
200
256
  modelListing = null;
201
257
  mcpConfigs = [];
202
258
  agentsFile = null;
@@ -250,6 +306,7 @@ var Harness = class {
250
306
  const command = this.invocation.binary ?? this.binaries[0];
251
307
  if (!command) return null;
252
308
  if (options.model !== void 0 && !this.invocation.modelArgs) return null;
309
+ if (accessConflictError(this.id, this.invocation, options) !== null) return null;
253
310
  const template = invocationTemplate(this.invocation, requestedInvocationMode(options));
254
311
  if (!template) return null;
255
312
  return {
@@ -260,6 +317,8 @@ var Harness = class {
260
317
  invocationError(options = {}) {
261
318
  if (!this.invocation) return `Harness ${this.id} has no non-interactive invocation`;
262
319
  if (options.model !== void 0 && !this.invocation.modelArgs) return `Harness ${this.id} does not support model selection`;
320
+ const conflict = accessConflictError(this.id, this.invocation, options);
321
+ if (conflict !== null) return conflict;
263
322
  const mode = requestedInvocationMode(options);
264
323
  if (invocationTemplate(this.invocation, mode)) return null;
265
324
  return `${`Harness ${this.id} has no ${INVOCATION_MODE_DESCRIPTION[mode]} invocation`}${invocationRetryHint(this.invocation, mode, options)}`;
@@ -275,7 +334,15 @@ var Harness = class {
275
334
  if (!built) return Promise.reject(new Error(this.invocationError(invocationOptions) ?? "Invalid invocation"));
276
335
  const versionError = this.readOnlyVersionError(invocationOptions);
277
336
  if (versionError) return Promise.reject(new Error(versionError));
278
- return executeCommand(built.command, built.args, options);
337
+ const streamArgs = options.structured ? void 0 : this.invocation?.streamArgs;
338
+ if (!streamArgs) return executeCommand(built.command, built.args, options);
339
+ return executeCommand(built.command, [...built.args, ...streamArgs], options).then((result) => ({
340
+ ...result,
341
+ stdout: this.foldStreamOutput(result.stdout, !result.timedOut && !result.aborted)
342
+ }));
343
+ }
344
+ foldStreamOutput(_stdout, _complete) {
345
+ throw new Error(`Harness ${this.id} does not implement stream output folding`);
279
346
  }
280
347
  readOnlyVersionError(options) {
281
348
  const floor = this.invocation?.readOnlyMinVersion;
@@ -290,22 +357,33 @@ var Harness = class {
290
357
  if (!this.modelListing) return null;
291
358
  const command = this.binaries[0];
292
359
  if (!command) return null;
293
- const template = search === void 0 ? this.modelListing.args : this.modelListing.searchArgs;
294
- if (!template) return null;
295
360
  return {
296
361
  command,
297
- args: template.map((arg) => arg.replaceAll("{search}", () => search ?? ""))
362
+ args: (search === void 0 ? this.modelListing.args : this.modelListing.searchArgs ?? this.modelListing.args).map((arg) => arg.replaceAll("{search}", () => search ?? ""))
298
363
  };
299
364
  }
300
365
  async listModels(options = {}) {
301
366
  const built = this.buildModelListInvocation(options.search);
302
- if (!built) throw new Error(this.modelListing ? `Harness ${this.id} does not support filtering its model listing` : `Harness ${this.id} does not support model listing`);
367
+ if (!built) throw new Error(`Harness ${this.id} does not support model listing`);
303
368
  const result = await executeCommand(built.command, built.args, options);
369
+ if (result.timedOut || result.aborted || result.exitCode !== 0) return {
370
+ ...result,
371
+ models: []
372
+ };
373
+ const models = this.parseModelListingOutput(result.stdout);
374
+ if (options.search === void 0 || this.modelListing?.searchArgs) return {
375
+ ...result,
376
+ models
377
+ };
378
+ const search = options.search.toLowerCase();
304
379
  return {
305
380
  ...result,
306
- models: !result.timedOut && !result.aborted && result.exitCode === 0 ? this.parseModelListingOutput(result.stdout) : []
381
+ models: models.filter((model) => model.id.toLowerCase().includes(search))
307
382
  };
308
383
  }
384
+ modelSelector(model) {
385
+ return `${model.provider}/${model.id}`;
386
+ }
309
387
  parseModelListingOutput(_stdout) {
310
388
  throw new Error(`Harness ${this.id} does not implement model-list output parsing`);
311
389
  }
@@ -325,10 +403,35 @@ var Harness = class {
325
403
  instructions: this.resolveCandidates(this.instructions, options),
326
404
  skills: this.resolveCandidates(this.skills, options),
327
405
  commands: this.resolveCandidates(this.commands, options),
328
- hooks: this.resolveCandidates(this.hooks, options)
406
+ promptTemplates: this.resolveCandidates(this.promptTemplates, options),
407
+ promptTemplateSyncTarget: this.promptTemplateSyncTarget === null ? null : this.resolveCandidates([this.promptTemplateSyncTarget], options)[0] ?? null,
408
+ hooks: this.resolveCandidates(this.hooks, options),
409
+ temp: this.resolveTemp(options)
329
410
  };
330
411
  }
412
+ resolveTemp(options) {
413
+ const temp = this.resolveCandidates(this.temp, options);
414
+ const override = this.resolveCandidates(this.envOverrides, options).find((entry) => entry.relocates.includes("temp"));
415
+ const value = override ? process.env[override.variable] : void 0;
416
+ if (!override || !value) return temp;
417
+ return temp.map((entry) => ({
418
+ ...entry,
419
+ path: moveRoot(entry.path, override.path, value)
420
+ }));
421
+ }
331
422
  };
423
+ function parseAntigravityModels(stdout) {
424
+ const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
425
+ if (lines.length === 0) throw new Error("Unexpected empty Antigravity model-list output");
426
+ return lines.map((line) => {
427
+ const match = /^(\S+)\t.+$/.exec(line);
428
+ if (!match?.[1]) throw new Error(`Unexpected Antigravity model-list row: ${JSON.stringify(line)}`);
429
+ return {
430
+ provider: "google",
431
+ id: match[1]
432
+ };
433
+ });
434
+ }
332
435
  var Antigravity = class extends Harness {
333
436
  id = "antigravity";
334
437
  name = "Google Antigravity CLI";
@@ -472,6 +575,11 @@ var Antigravity = class extends Harness {
472
575
  level: "official",
473
576
  note: "Non-interactive print mode."
474
577
  };
578
+ modelListing = {
579
+ args: ["models"],
580
+ level: "official",
581
+ note: "Prints one id and display name per row, with no default marked and no filter; verified with 1.2.5."
582
+ };
475
583
  mcpConfigs = [{
476
584
  path: "~/.gemini/config/mcp_config.json",
477
585
  scope: "user",
@@ -485,7 +593,60 @@ var Antigravity = class extends Harness {
485
593
  envVars: [],
486
594
  projectMarkers: [".antigravitycli"]
487
595
  };
596
+ parseModelListingOutput(stdout) {
597
+ return parseAntigravityModels(stdout);
598
+ }
599
+ modelSelector(model) {
600
+ return model.id;
601
+ }
488
602
  };
603
+ function parseStreamLine(line) {
604
+ try {
605
+ const parsed = JSON.parse(line);
606
+ return typeof parsed === "object" && parsed !== null && "type" in parsed && typeof parsed.type === "string" ? parsed : void 0;
607
+ } catch {
608
+ return;
609
+ }
610
+ }
611
+ function startsTextBlock(event) {
612
+ return event.type === "stream_event" && event.event?.type === "content_block_start" && event.event.content_block?.type === "text";
613
+ }
614
+ function textDelta(event) {
615
+ if (event.type !== "stream_event" || event.event?.type !== "content_block_delta") return void 0;
616
+ const delta = event.event.delta;
617
+ return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : void 0;
618
+ }
619
+ var ClaudeStreamFold = class {
620
+ blocks = [];
621
+ other = [];
622
+ result;
623
+ add(line) {
624
+ if (line.trim().length === 0) return;
625
+ const event = parseStreamLine(line);
626
+ if (!event) {
627
+ this.other.push(line);
628
+ return;
629
+ }
630
+ if (event.type === "result" && typeof event.result === "string") this.result = event.result;
631
+ if (startsTextBlock(event)) this.blocks.push("");
632
+ const text = textDelta(event);
633
+ if (text !== void 0) this.blocks.push((this.blocks.pop() ?? "") + text);
634
+ }
635
+ text(complete) {
636
+ const prefix = this.other.map((line) => `${line}\n`).join("");
637
+ if (complete && this.result !== void 0) return `${prefix}${this.result}\n`;
638
+ const text = this.blocks.filter((block) => block.length > 0).join("\n\n");
639
+ return text.length > 0 ? `${prefix}${text}\n` : prefix;
640
+ }
641
+ };
642
+ function foldClaudeStream(stdout, complete) {
643
+ const lines = stdout.split("\n");
644
+ const tail = lines.at(-1);
645
+ if (!complete && tail?.startsWith("{") && parseStreamLine(tail) === void 0) lines.pop();
646
+ const fold = new ClaudeStreamFold();
647
+ for (const line of lines) fold.add(line);
648
+ return fold.text(complete);
649
+ }
489
650
  var Claude = class extends Harness {
490
651
  id = "claude";
491
652
  name = "Anthropic Claude Code";
@@ -602,6 +763,13 @@ var Claude = class extends Harness {
602
763
  level: "official",
603
764
  note: "Legacy global slash commands."
604
765
  }];
766
+ promptTemplates = this.commands;
767
+ promptTemplateSyncTarget = {
768
+ path: "~/.claude/commands/",
769
+ scope: "user",
770
+ level: "official",
771
+ format: "markdown"
772
+ };
605
773
  hooks = [{
606
774
  path: ".claude/hooks/",
607
775
  scope: "project",
@@ -623,6 +791,7 @@ var Claude = class extends Harness {
623
791
  noToolsArgs: [
624
792
  "-p",
625
793
  "{prompt}",
794
+ "--strict-mcp-config",
626
795
  "--tools",
627
796
  ""
628
797
  ],
@@ -631,6 +800,7 @@ var Claude = class extends Harness {
631
800
  "--output-format",
632
801
  "json",
633
802
  "{prompt}",
803
+ "--strict-mcp-config",
634
804
  "--tools",
635
805
  ""
636
806
  ],
@@ -652,8 +822,14 @@ var Claude = class extends Harness {
652
822
  ],
653
823
  readOnlyMinVersion: "2.1.175",
654
824
  modelArgs: ["--model", "{model}"],
825
+ streamArgs: [
826
+ "--output-format",
827
+ "stream-json",
828
+ "--verbose",
829
+ "--include-partial-messages"
830
+ ],
655
831
  level: "official",
656
- note: "Headless print mode; add --output-format json for structured output. Read-only runs keep the built-in Read, Glob and Grep tools and --strict-mcp-config drops every configured MCP server, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through."
832
+ note: "Headless print mode; add --output-format json for structured output. Plain text runs stream events instead and fold them back, since text mode prints nothing until the answer is complete and a timeout would lose all of it; --include-partial-messages streams a single long answer too, checked on 2.1.280. --tools only covers the built-in set, so every mode without the full agent adds --strict-mcp-config to drop configured MCP servers too; without it an advisor still sees every user and project MCP tool, checked on 2.1.280. Read-only runs keep the built-in Read, Glob and Grep tools, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through."
657
833
  };
658
834
  mcpConfigs = [{
659
835
  path: "~/.claude.json",
@@ -670,6 +846,63 @@ var Claude = class extends Harness {
670
846
  key: ["mcpServers"],
671
847
  dialect: "standard"
672
848
  }];
849
+ temp = [
850
+ {
851
+ path: "${TMPDIR}/claude-<uid>/",
852
+ scope: "data",
853
+ level: "official",
854
+ platforms: ["linux"],
855
+ note: "Session scratchpads sit under <dash-encoded-cwd>/<session-id>/scratchpad."
856
+ },
857
+ {
858
+ path: "/tmp/claude-<uid>/",
859
+ scope: "data",
860
+ level: "official",
861
+ platforms: ["darwin"]
862
+ },
863
+ {
864
+ path: "${TMPDIR}/claude/",
865
+ scope: "data",
866
+ level: "official",
867
+ platforms: ["win32"]
868
+ }
869
+ ];
870
+ envOverrides = [
871
+ {
872
+ variable: "CLAUDE_CONFIG_DIR",
873
+ path: "~/.claude",
874
+ scope: "user",
875
+ level: "official",
876
+ relocates: [
877
+ "config",
878
+ "sessions",
879
+ "instructions",
880
+ "skills",
881
+ "commands",
882
+ "promptTemplates",
883
+ "hooks"
884
+ ],
885
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
886
+ },
887
+ {
888
+ variable: "CLAUDE_CODE_TMPDIR",
889
+ path: "${TMPDIR}",
890
+ scope: "data",
891
+ level: "official",
892
+ platforms: ["linux", "win32"],
893
+ relocates: ["temp"],
894
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
895
+ },
896
+ {
897
+ variable: "CLAUDE_CODE_TMPDIR",
898
+ path: "/tmp",
899
+ scope: "data",
900
+ level: "official",
901
+ platforms: ["darwin"],
902
+ relocates: ["temp"],
903
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
904
+ }
905
+ ];
673
906
  agentsFile = "~/.claude/CLAUDE.md";
674
907
  detection = {
675
908
  envVars: [
@@ -685,6 +918,9 @@ var Claude = class extends Harness {
685
918
  "CLAUDE.md"
686
919
  ]
687
920
  };
921
+ foldStreamOutput(stdout, complete) {
922
+ return foldClaudeStream(stdout, complete);
923
+ }
688
924
  };
689
925
  var Codex = class extends Harness {
690
926
  id = "codex";
@@ -808,7 +1044,19 @@ var Codex = class extends Harness {
808
1044
  platforms: ["linux", "darwin"]
809
1045
  }
810
1046
  ];
811
- commands = [];
1047
+ commands = [{
1048
+ path: "~/.codex/prompts/",
1049
+ scope: "user",
1050
+ level: "official",
1051
+ note: "Deprecated Markdown prompts invoked as /prompts:name. Top-level files only; use skills for shared prompts."
1052
+ }];
1053
+ promptTemplates = this.commands;
1054
+ promptTemplateSyncTarget = {
1055
+ path: "~/.codex/prompts/",
1056
+ scope: "user",
1057
+ level: "official",
1058
+ format: "markdown"
1059
+ };
812
1060
  hooks = [];
813
1061
  invocation = {
814
1062
  args: [
@@ -856,6 +1104,20 @@ var Codex = class extends Harness {
856
1104
  key: ["mcp_servers"],
857
1105
  dialect: "standard"
858
1106
  }];
1107
+ envOverrides = [{
1108
+ variable: "CODEX_HOME",
1109
+ path: "~/.codex",
1110
+ scope: "user",
1111
+ level: "official",
1112
+ relocates: [
1113
+ "config",
1114
+ "sessions",
1115
+ "instructions",
1116
+ "skills",
1117
+ "commands",
1118
+ "promptTemplates"
1119
+ ]
1120
+ }];
859
1121
  agentsFile = "~/.codex/AGENTS.md";
860
1122
  detection = {
861
1123
  envVars: [],
@@ -1171,6 +1433,13 @@ var Gemini = class extends Harness {
1171
1433
  level: "official",
1172
1434
  note: "Global custom commands in TOML format."
1173
1435
  }];
1436
+ promptTemplates = this.commands;
1437
+ promptTemplateSyncTarget = {
1438
+ path: "~/.gemini/commands/",
1439
+ scope: "user",
1440
+ level: "official",
1441
+ format: "gemini-toml"
1442
+ };
1174
1443
  hooks = [];
1175
1444
  invocation = {
1176
1445
  args: ["-p", "{prompt}"],
@@ -1273,6 +1542,20 @@ var GitHubCopilot = class extends Harness {
1273
1542
  ]
1274
1543
  };
1275
1544
  };
1545
+ function parseGrokModels(stdout) {
1546
+ const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1547
+ const start = lines.indexOf("Available models:");
1548
+ if (start === -1) throw new Error(`Unexpected Grok model-list output: ${JSON.stringify(stdout)}`);
1549
+ return lines.slice(start + 1).map((line) => {
1550
+ const match = /^([*-])\s+(\S+)(?:\s+\(default\))?$/.exec(line);
1551
+ if (!match?.[2]) throw new Error(`Unexpected Grok model-list row: ${JSON.stringify(line)}`);
1552
+ return {
1553
+ provider: "xai",
1554
+ id: match[2],
1555
+ ...match[1] === "*" ? { default: true } : {}
1556
+ };
1557
+ });
1558
+ }
1276
1559
  var Grok = class extends Harness {
1277
1560
  id = "grok";
1278
1561
  name = "xAI Grok CLI";
@@ -1405,6 +1688,11 @@ var Grok = class extends Harness {
1405
1688
  level: "official",
1406
1689
  note: "-p is short for --single; add --output-format json for structured output. --sandbox read-only is kernel-enforced (Landlock, Seatbelt) regardless of the inherited permission mode and still allows writes to ~/.grok and temp dirs; verified on Linux with 1.0.13 and 1.0.25."
1407
1690
  };
1691
+ modelListing = {
1692
+ args: ["models"],
1693
+ level: "official",
1694
+ note: "Prints bare ids with the default marked; verified with 1.0.41."
1695
+ };
1408
1696
  mcpConfigs = [{
1409
1697
  path: "~/.grok/config.toml",
1410
1698
  scope: "user",
@@ -1429,6 +1717,9 @@ var Grok = class extends Harness {
1429
1717
  ],
1430
1718
  projectMarkers: [".grok"]
1431
1719
  };
1720
+ parseModelListingOutput(stdout) {
1721
+ return parseGrokModels(stdout);
1722
+ }
1432
1723
  };
1433
1724
  function packageVersion(commandPath) {
1434
1725
  try {
@@ -1488,6 +1779,12 @@ var MastraCode = class extends Harness {
1488
1779
  level: "official",
1489
1780
  note: "Global MCP server configurations."
1490
1781
  },
1782
+ {
1783
+ path: ".mcp.json",
1784
+ scope: "project",
1785
+ level: "official",
1786
+ note: "Claude Code compatible project MCP servers, read below .mastracode/mcp.json."
1787
+ },
1491
1788
  {
1492
1789
  path: ".mastracode/hooks.json",
1493
1790
  scope: "project",
@@ -1579,16 +1876,51 @@ var MastraCode = class extends Harness {
1579
1876
  note: "Claude Code compatible."
1580
1877
  }
1581
1878
  ];
1582
- commands = [{
1583
- path: ".mastracode/commands/",
1584
- scope: "project",
1585
- level: "official"
1586
- }, {
1879
+ commands = [
1880
+ {
1881
+ path: ".mastracode/commands/",
1882
+ scope: "project",
1883
+ level: "official",
1884
+ note: "Markdown prompt commands; subdirectories become colon-separated command names."
1885
+ },
1886
+ {
1887
+ path: "~/.mastracode/commands/",
1888
+ scope: "user",
1889
+ level: "official",
1890
+ note: "Global Markdown prompt commands."
1891
+ },
1892
+ {
1893
+ path: ".claude/commands/",
1894
+ scope: "project",
1895
+ level: "official",
1896
+ note: "Claude compatibility."
1897
+ },
1898
+ {
1899
+ path: "~/.claude/commands/",
1900
+ scope: "user",
1901
+ level: "official",
1902
+ note: "Claude compatibility."
1903
+ },
1904
+ {
1905
+ path: ".opencode/command/",
1906
+ scope: "project",
1907
+ level: "official",
1908
+ note: "Mastra's OpenCode compatibility loader uses the singular command directory."
1909
+ },
1910
+ {
1911
+ path: "~/.opencode/command/",
1912
+ scope: "user",
1913
+ level: "official",
1914
+ note: "Mastra's OpenCode compatibility loader uses the singular command directory."
1915
+ }
1916
+ ];
1917
+ promptTemplates = this.commands;
1918
+ promptTemplateSyncTarget = {
1587
1919
  path: "~/.mastracode/commands/",
1588
1920
  scope: "user",
1589
1921
  level: "official",
1590
- note: "Global commands."
1591
- }];
1922
+ format: "markdown"
1923
+ };
1592
1924
  hooks = [{
1593
1925
  path: ".mastracode/hooks.json",
1594
1926
  scope: "project",
@@ -1600,6 +1932,35 @@ var MastraCode = class extends Harness {
1600
1932
  note: "Global hooks."
1601
1933
  }];
1602
1934
  invocation = null;
1935
+ mcpConfigs = [
1936
+ {
1937
+ path: "~/.mastracode/mcp.json",
1938
+ scope: "user",
1939
+ level: "official",
1940
+ format: "json",
1941
+ key: ["mcpServers"],
1942
+ dialect: "mastracode",
1943
+ note: "An entry with command is stdio and one with url is HTTP, type is ignored. Loaded above mcpServers from the project's .claude/settings.local.json and below both project files, later names winning."
1944
+ },
1945
+ {
1946
+ path: ".mastracode/mcp.json",
1947
+ scope: "project",
1948
+ level: "official",
1949
+ format: "json",
1950
+ key: ["mcpServers"],
1951
+ dialect: "mastracode",
1952
+ note: "Highest priority, overrides the other files entry by entry."
1953
+ },
1954
+ {
1955
+ path: ".mcp.json",
1956
+ scope: "project",
1957
+ level: "official",
1958
+ format: "json",
1959
+ key: ["mcpServers"],
1960
+ dialect: "mastracode",
1961
+ note: "Claude Code's project file, read between the user file and .mastracode/mcp.json. Project writes go to .mastracode/mcp.json, this one is Claude's to edit."
1962
+ }
1963
+ ];
1603
1964
  detection = {
1604
1965
  envVars: [],
1605
1966
  projectMarkers: [".mastracode"]
@@ -1622,7 +1983,7 @@ var Omp = class extends Harness {
1622
1983
  path: "~/.omp/agent/config.yml",
1623
1984
  scope: "user",
1624
1985
  level: "official",
1625
- note: "Agent dir overridable with PI_CODING_AGENT_DIR; profiles use ~/.omp/profiles/<name>/agent."
1986
+ note: "Profiles use ~/.omp/profiles/<name>/agent."
1626
1987
  },
1627
1988
  {
1628
1989
  path: "~/.omp/agent/settings.json",
@@ -1747,13 +2108,35 @@ var Omp = class extends Harness {
1747
2108
  commands = [{
1748
2109
  path: ".omp/commands/",
1749
2110
  scope: "project",
1750
- level: "official"
2111
+ level: "official",
2112
+ note: "Markdown prompt commands and TypeScript commands; only Markdown files are templates."
1751
2113
  }, {
1752
2114
  path: "~/.omp/agent/commands/",
1753
2115
  scope: "user",
1754
2116
  level: "official",
1755
- note: "Global user-level prompt commands."
2117
+ note: "Global Markdown prompt commands and TypeScript commands; only Markdown files are templates."
1756
2118
  }];
2119
+ promptTemplates = [
2120
+ ...this.commands,
2121
+ {
2122
+ path: ".omp/prompts/",
2123
+ scope: "project",
2124
+ level: "official",
2125
+ note: "Markdown prompt templates invoked as /name; subdirectories are scanned."
2126
+ },
2127
+ {
2128
+ path: "~/.omp/agent/prompts/",
2129
+ scope: "user",
2130
+ level: "official",
2131
+ note: "Global Markdown prompt templates, separate from executable commands."
2132
+ }
2133
+ ];
2134
+ promptTemplateSyncTarget = {
2135
+ path: "~/.omp/agent/prompts/",
2136
+ scope: "user",
2137
+ level: "official",
2138
+ format: "markdown"
2139
+ };
1757
2140
  hooks = [];
1758
2141
  invocation = {
1759
2142
  args: ["-p", "{prompt}"],
@@ -1782,6 +2165,21 @@ var Omp = class extends Harness {
1782
2165
  key: ["mcpServers"],
1783
2166
  dialect: "standard"
1784
2167
  }];
2168
+ envOverrides = [{
2169
+ variable: "PI_CODING_AGENT_DIR",
2170
+ path: "~/.omp/agent",
2171
+ scope: "user",
2172
+ level: "official",
2173
+ relocates: [
2174
+ "config",
2175
+ "sessions",
2176
+ "instructions",
2177
+ "skills",
2178
+ "commands",
2179
+ "promptTemplates"
2180
+ ],
2181
+ note: "Ignored under a named profile (OMP_PROFILE). On Linux, after omp config init-xdg, data, state and cache live under $XDG_*_HOME/omp instead."
2182
+ }];
1785
2183
  agentsFile = "~/.omp/agent/AGENTS.md";
1786
2184
  detection = {
1787
2185
  envVars: ["OMP_PROFILE", "PI_CODING_AGENT_DIR"],
@@ -1896,7 +2294,24 @@ var OpenCode = class extends Harness {
1896
2294
  level: "official"
1897
2295
  }
1898
2296
  ];
1899
- commands = [];
2297
+ commands = [{
2298
+ path: ".opencode/commands/",
2299
+ scope: "project",
2300
+ level: "official",
2301
+ note: "Markdown prompt commands; also configurable through command entries in opencode.json."
2302
+ }, {
2303
+ path: "~/.config/opencode/commands/",
2304
+ scope: "user",
2305
+ level: "official",
2306
+ note: "Global Markdown prompt commands."
2307
+ }];
2308
+ promptTemplates = this.commands;
2309
+ promptTemplateSyncTarget = {
2310
+ path: "~/.config/opencode/commands/",
2311
+ scope: "user",
2312
+ level: "official",
2313
+ format: "markdown"
2314
+ };
1900
2315
  hooks = [];
1901
2316
  invocation = {
1902
2317
  args: ["run", "{prompt}"],
@@ -1989,8 +2404,7 @@ var Pi = class extends Harness {
1989
2404
  config = [{
1990
2405
  path: "~/.pi/agent/settings.json",
1991
2406
  scope: "user",
1992
- level: "official",
1993
- note: "Config dir overridable with PI_CODING_AGENT_DIR."
2407
+ level: "official"
1994
2408
  }, {
1995
2409
  path: ".pi/settings.json",
1996
2410
  scope: "project",
@@ -2000,7 +2414,7 @@ var Pi = class extends Harness {
2000
2414
  path: "~/.pi/agent/sessions/<dash-encoded-cwd>/<timestamp>_<session-id>.jsonl",
2001
2415
  scope: "data",
2002
2416
  level: "official",
2003
- note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with PI_CODING_AGENT_SESSION_DIR or --session-dir."
2417
+ note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with --session-dir."
2004
2418
  }];
2005
2419
  persistence = [{
2006
2420
  format: "JSON",
@@ -2058,6 +2472,13 @@ var Pi = class extends Harness {
2058
2472
  level: "official",
2059
2473
  note: "Global user-level prompt templates."
2060
2474
  }];
2475
+ promptTemplates = this.commands;
2476
+ promptTemplateSyncTarget = {
2477
+ path: "~/.pi/agent/prompts/",
2478
+ scope: "user",
2479
+ level: "official",
2480
+ format: "markdown"
2481
+ };
2061
2482
  hooks = [];
2062
2483
  invocation = {
2063
2484
  args: ["-p", "{prompt}"],
@@ -2103,6 +2524,28 @@ var Pi = class extends Harness {
2103
2524
  level: "official",
2104
2525
  note: "Lists models with configured provider authentication; accepts an optional fuzzy search."
2105
2526
  };
2527
+ envOverrides = [{
2528
+ variable: "PI_CODING_AGENT_DIR",
2529
+ path: "~/.pi/agent",
2530
+ scope: "user",
2531
+ level: "official",
2532
+ relocates: [
2533
+ "config",
2534
+ "sessions",
2535
+ "instructions",
2536
+ "skills",
2537
+ "commands",
2538
+ "promptTemplates"
2539
+ ],
2540
+ note: "A leading ~ in the value is expanded."
2541
+ }, {
2542
+ variable: "PI_CODING_AGENT_SESSION_DIR",
2543
+ path: "~/.pi/agent/sessions",
2544
+ scope: "data",
2545
+ level: "official",
2546
+ relocates: ["sessions"],
2547
+ note: "Takes precedence over PI_CODING_AGENT_DIR for sessions; --session-dir beats both."
2548
+ }];
2106
2549
  agentsFile = "~/.pi/agent/AGENTS.md";
2107
2550
  detection = {
2108
2551
  envVars: [
@@ -2228,6 +2671,13 @@ var PrimeAgent = class extends Harness {
2228
2671
  level: "official",
2229
2672
  note: "Global user-level prompt templates."
2230
2673
  }];
2674
+ promptTemplates = this.commands;
2675
+ promptTemplateSyncTarget = {
2676
+ path: "~/.prime/agent/prompts/",
2677
+ scope: "user",
2678
+ level: "official",
2679
+ format: "markdown"
2680
+ };
2231
2681
  hooks = [];
2232
2682
  invocation = {
2233
2683
  args: ["-p", "{prompt}"],
@@ -2457,8 +2907,21 @@ function normalizePrime(name, raw) {
2457
2907
  env: primeEnv(raw.env)
2458
2908
  });
2459
2909
  }
2910
+ function normalizeMastracode(name, raw) {
2911
+ const url = stringField(raw, "url");
2912
+ return compact({
2913
+ name,
2914
+ transport: url ? "http" : "stdio",
2915
+ command: stringField(raw, "command"),
2916
+ args: asStringArray(raw.args),
2917
+ env: asStringRecord(raw.env),
2918
+ url,
2919
+ headers: asStringRecord(raw.headers)
2920
+ });
2921
+ }
2460
2922
  function normalizeEntry(name, raw, dialect) {
2461
2923
  if (dialect === "antigravity") return normalizeAntigravity(name, raw);
2924
+ if (dialect === "mastracode") return normalizeMastracode(name, raw);
2462
2925
  if (dialect === "opencode") return normalizeOpenCode(name, raw);
2463
2926
  if (dialect === "prime") return normalizePrime(name, raw);
2464
2927
  return normalizeStandard(name, raw);
@@ -2485,6 +2948,17 @@ function denormalizeOpenCode(server) {
2485
2948
  enabled: server.enabled
2486
2949
  });
2487
2950
  }
2951
+ function denormalizeMastracode(server) {
2952
+ if (server.url) return compact({
2953
+ url: server.url,
2954
+ headers: server.headers
2955
+ });
2956
+ return compact({
2957
+ command: server.command,
2958
+ args: server.args?.length ? server.args : void 0,
2959
+ env: server.env
2960
+ });
2961
+ }
2488
2962
  function denormalizeStandard(server) {
2489
2963
  return compact({
2490
2964
  type: server.transport,
@@ -2530,8 +3004,18 @@ function resolveEnvReferences(server, dialect) {
2530
3004
  env
2531
3005
  };
2532
3006
  }
3007
+ function shapeForDialect(server, dialect) {
3008
+ const shaped = resolveEnvReferences(server, dialect);
3009
+ if (dialect !== "mastracode") return shaped;
3010
+ return compact({
3011
+ ...shaped,
3012
+ transport: shaped.url ? "http" : "stdio",
3013
+ enabled: void 0
3014
+ });
3015
+ }
2533
3016
  function denormalizeEntry(server, dialect) {
2534
3017
  if (dialect === "antigravity") return denormalizeAntigravity(server);
3018
+ if (dialect === "mastracode") return denormalizeMastracode(server);
2535
3019
  if (dialect === "opencode") return denormalizeOpenCode(server);
2536
3020
  if (dialect === "prime") return denormalizePrime(server);
2537
3021
  return denormalizeStandard(server);
@@ -2667,7 +3151,7 @@ function removeTomlServer(path, key, name) {
2667
3151
  }
2668
3152
  function addMcpServer(harness, server, scope = "user", options = {}) {
2669
3153
  const { entry, path } = writableConfig(harness, scope, options);
2670
- const resolved = resolveEnvReferences(server, entry.dialect);
3154
+ const resolved = shapeForDialect(server, entry.dialect);
2671
3155
  if (entry.format === "toml") return {
2672
3156
  path,
2673
3157
  replaced: addTomlServer(path, entry.key, resolved)
@@ -2887,7 +3371,7 @@ function syncIncludedHarness(harness, master, existing, masterNames, initialPath
2887
3371
  let path = initialPath;
2888
3372
  const { dialect } = writableConfig(harness, "user", options).entry;
2889
3373
  for (const server of master.servers) try {
2890
- const resolved = resolveEnvReferences(server, dialect);
3374
+ const resolved = shapeForDialect(server, dialect);
2891
3375
  if (existing.get(server.name) === canonical(resolved)) {
2892
3376
  results.push({
2893
3377
  name: server.name,
@@ -3035,6 +3519,23 @@ function relink(path, source) {
3035
3519
  symlinkSync(source, temp);
3036
3520
  renameSync(temp, path);
3037
3521
  }
3522
+ function moveFile(from, to) {
3523
+ try {
3524
+ renameSync(from, to);
3525
+ } catch (error) {
3526
+ if (error.code !== "EXDEV") throw error;
3527
+ if (lstatSync(from).isDirectory()) {
3528
+ cpSync(from, to, {
3529
+ recursive: true,
3530
+ verbatimSymlinks: true
3531
+ });
3532
+ rmSync(from, { recursive: true });
3533
+ return;
3534
+ }
3535
+ copyFileSync(from, to);
3536
+ unlinkSync(from);
3537
+ }
3538
+ }
3038
3539
  const CHECK_ACTION = {
3039
3540
  missing: "linked",
3040
3541
  "correct-link": "unchanged",
@@ -3047,7 +3548,7 @@ function applyFileTarget(id, path, source, state, options) {
3047
3548
  const backupDir = join(agntnConfigDir(options), "diverged");
3048
3549
  mkdirSync(backupDir, { recursive: true });
3049
3550
  const backup = join(backupDir, `${id}-${basename(path)}-${Date.now()}.md`);
3050
- renameSync(path, backup);
3551
+ moveFile(path, backup);
3051
3552
  relink(path, source);
3052
3553
  return {
3053
3554
  path,
@@ -3082,7 +3583,7 @@ function syncFileTarget(id, path, source, check, options) {
3082
3583
  };
3083
3584
  return applyFileTarget(id, path, source, state, options);
3084
3585
  }
3085
- function syncTarget(harness, config, check, options) {
3586
+ function syncTarget$1(harness, config, check, options) {
3086
3587
  if (config.excludes.includes(harness.id)) return {
3087
3588
  id: harness.id,
3088
3589
  action: "skipped",
@@ -3133,9 +3634,321 @@ function syncAgentsFiles(harnesses, check = false, options = {}) {
3133
3634
  return {
3134
3635
  source: config.source,
3135
3636
  check,
3136
- targets: harnesses.map((harness) => syncTarget(harness, config, check, options))
3637
+ targets: harnesses.map((harness) => syncTarget$1(harness, config, check, options))
3638
+ };
3639
+ }
3640
+ const GENERATED_MARKER = "# Generated by @agntn/harnesses from ";
3641
+ const PORTABLE_PROMPT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
3642
+ const GEMINI_ACTION = {
3643
+ missing: "generated",
3644
+ "owned-file": "replaced",
3645
+ "unmanaged-file": "adopted",
3646
+ unsupported: "skipped"
3647
+ };
3648
+ const GEMINI_BACKUP = {
3649
+ missing: false,
3650
+ "owned-file": true,
3651
+ "unmanaged-file": true,
3652
+ unsupported: false
3653
+ };
3654
+ function readTemplates(sourceDir, check) {
3655
+ let entries;
3656
+ try {
3657
+ entries = readdirSync(sourceDir, { withFileTypes: true });
3658
+ } catch (error) {
3659
+ if (error.code !== "ENOENT") throw error;
3660
+ if (!check) mkdirSync(sourceDir, { recursive: true });
3661
+ return [];
3662
+ }
3663
+ const templates = entries.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && extname(entry.name).toLowerCase() === ".md").map((entry) => {
3664
+ const name = basename(entry.name, extname(entry.name));
3665
+ if (!PORTABLE_PROMPT_NAME.test(name)) throw new TypeError(`Invalid prompt template filename: ${entry.name}`);
3666
+ const path = join(sourceDir, entry.name);
3667
+ return {
3668
+ name,
3669
+ path,
3670
+ content: readFileSync(path, "utf8")
3671
+ };
3672
+ }).sort((left, right) => left.name.localeCompare(right.name));
3673
+ const names = templates.map((template) => template.name.toLowerCase());
3674
+ if (new Set(names).size !== names.length) throw new Error(`Duplicate prompt template names in ${sourceDir}`);
3675
+ return templates;
3676
+ }
3677
+ function generatedSource(content) {
3678
+ const firstLine = content.split("\n", 1)[0] ?? "";
3679
+ if (!firstLine.startsWith(GENERATED_MARKER)) return null;
3680
+ try {
3681
+ const source = JSON.parse(firstLine.slice(37));
3682
+ return typeof source === "string" ? source : null;
3683
+ } catch {
3684
+ return null;
3685
+ }
3686
+ }
3687
+ function linkTarget(path) {
3688
+ const target = readlinkSync(path);
3689
+ return isAbsolute(target) ? target : resolve(dirname(path), target);
3690
+ }
3691
+ function inspectDestination(path) {
3692
+ let stats;
3693
+ try {
3694
+ stats = lstatSync(path);
3695
+ } catch (error) {
3696
+ if (error.code === "ENOENT") return { kind: "missing" };
3697
+ throw error;
3698
+ }
3699
+ if (stats.isSymbolicLink()) return { kind: "unmanaged-file" };
3700
+ if (!stats.isFile()) return { kind: "unsupported" };
3701
+ const content = readFileSync(path, "utf8");
3702
+ if (generatedSource(content) !== null) return {
3703
+ kind: "owned-file",
3704
+ content
3705
+ };
3706
+ return { kind: "unmanaged-file" };
3707
+ }
3708
+ function tempPath(path, kind) {
3709
+ return join(dirname(path), `.${basename(path)}-${randomUUID()}.${kind}.tmp`);
3710
+ }
3711
+ function linkDirectory(path, source) {
3712
+ mkdirSync(dirname(path), { recursive: true });
3713
+ const temp = tempPath(path, "prompt-link");
3714
+ symlinkSync(source, temp, "junction");
3715
+ renameSync(temp, path);
3716
+ }
3717
+ function writeTemplate(path, content) {
3718
+ mkdirSync(dirname(path), { recursive: true });
3719
+ const temp = tempPath(path, "prompt-write");
3720
+ writeFileSync(temp, content);
3721
+ renameSync(temp, path);
3722
+ }
3723
+ function backupDestination(id, path, options) {
3724
+ const backupDir = join(agntnDataDir(options), "diverged", "prompts", id);
3725
+ mkdirSync(backupDir, { recursive: true });
3726
+ const backup = join(backupDir, `${basename(path)}-${Date.now()}-${randomUUID()}`);
3727
+ if (lstatSync(path).isSymbolicLink()) {
3728
+ symlinkSync(linkTarget(path), backup);
3729
+ unlinkSync(path);
3730
+ return backup;
3731
+ }
3732
+ moveFile(path, backup);
3733
+ return backup;
3734
+ }
3735
+ function checkDetail() {
3736
+ return "check mode: not applied";
3737
+ }
3738
+ function isInsideDirectory(path, directory) {
3739
+ const remainder = relative(directory, path);
3740
+ return remainder !== "" && !remainder.startsWith("..") && !isAbsolute(remainder);
3741
+ }
3742
+ function isLegacyTemplateLink(path, sourceDir) {
3743
+ return lstatSync(path).isSymbolicLink() && isInsideDirectory(linkTarget(path), sourceDir);
3744
+ }
3745
+ const DIRECTORY_ACTION = {
3746
+ missing: "linked",
3747
+ "correct-link": "unchanged",
3748
+ "wrong-link": "relinked",
3749
+ "legacy-links": "relinked",
3750
+ "local-files": "adopted",
3751
+ "not-directory": "skipped"
3752
+ };
3753
+ function inspectDirectory(targetDir, sourceDir) {
3754
+ let stats;
3755
+ try {
3756
+ stats = lstatSync(targetDir);
3757
+ } catch (error) {
3758
+ if (error.code === "ENOENT") return { kind: "missing" };
3759
+ throw error;
3760
+ }
3761
+ if (stats.isSymbolicLink()) return { kind: linkTarget(targetDir) === sourceDir ? "correct-link" : "wrong-link" };
3762
+ if (!stats.isDirectory()) return { kind: "not-directory" };
3763
+ const entries = readdirSync(targetDir).map((entry) => join(targetDir, entry));
3764
+ return entries.every((entry) => isLegacyTemplateLink(entry, sourceDir)) ? {
3765
+ kind: "legacy-links",
3766
+ entries
3767
+ } : { kind: "local-files" };
3768
+ }
3769
+ function clearDirectory(id, targetDir, state, options) {
3770
+ if (state.kind === "wrong-link" || state.kind === "local-files") return backupDestination(id, targetDir, options);
3771
+ if (state.kind === "legacy-links") {
3772
+ for (const entry of state.entries) unlinkSync(entry);
3773
+ rmdirSync(targetDir);
3774
+ }
3775
+ }
3776
+ function syncMarkdownDirectory(id, targetDir, sourceDir, check, options) {
3777
+ const state = inspectDirectory(targetDir, sourceDir);
3778
+ const action = DIRECTORY_ACTION[state.kind];
3779
+ if (state.kind === "correct-link") return { action };
3780
+ if (state.kind === "not-directory") return {
3781
+ action,
3782
+ detail: "destination exists and is not a directory or symlink"
3783
+ };
3784
+ if (check) return {
3785
+ action,
3786
+ detail: checkDetail()
3787
+ };
3788
+ const detail = clearDirectory(id, targetDir, state, options);
3789
+ linkDirectory(targetDir, sourceDir);
3790
+ return {
3791
+ action,
3792
+ detail
3793
+ };
3794
+ }
3795
+ function parseYaml(raw) {
3796
+ return createRequire(import.meta.url)("yaml").parse(raw);
3797
+ }
3798
+ function frontmatterDescription(raw, path) {
3799
+ const frontmatter = parseYaml(raw);
3800
+ if (frontmatter === null) return void 0;
3801
+ if (typeof frontmatter !== "object" || Array.isArray(frontmatter)) throw new TypeError(`Prompt template frontmatter is not an object: ${path}`);
3802
+ const description = frontmatter.description;
3803
+ if (description !== void 0 && typeof description !== "string") throw new TypeError(`Prompt template description is not a string: ${path}`);
3804
+ return description;
3805
+ }
3806
+ function parseMarkdownTemplate(template) {
3807
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(template.content);
3808
+ if (match === null) return { prompt: template.content.replaceAll("$ARGUMENTS", "{{args}}") };
3809
+ const description = frontmatterDescription(match[1] ?? "", template.path);
3810
+ return {
3811
+ ...description === void 0 ? {} : { description },
3812
+ prompt: template.content.slice(match[0].length).replaceAll("$ARGUMENTS", "{{args}}")
3813
+ };
3814
+ }
3815
+ function geminiContent(template) {
3816
+ const parsed = parseMarkdownTemplate(template);
3817
+ return [
3818
+ `${GENERATED_MARKER}${JSON.stringify(template.path)}`,
3819
+ ...parsed.description === void 0 ? [] : [`description = ${JSON.stringify(parsed.description)}`],
3820
+ `prompt = ${JSON.stringify(parsed.prompt)}`,
3821
+ ""
3822
+ ].join("\n");
3823
+ }
3824
+ function syncGeminiTemplate(id, targetDir, template, check, options) {
3825
+ const path = join(targetDir, `${template.name}.toml`);
3826
+ const content = template.gemini ?? geminiContent(template);
3827
+ const state = inspectDestination(path);
3828
+ if (state.kind === "owned-file" && state.content === content) return {
3829
+ name: template.name,
3830
+ source: template.path,
3831
+ path,
3832
+ action: "unchanged"
3833
+ };
3834
+ if (state.kind === "unsupported") return {
3835
+ name: template.name,
3836
+ source: template.path,
3837
+ path,
3838
+ action: "skipped",
3839
+ detail: "destination exists and is not a file or symlink"
3840
+ };
3841
+ const action = GEMINI_ACTION[state.kind];
3842
+ if (check) return {
3843
+ name: template.name,
3844
+ source: template.path,
3845
+ path,
3846
+ action,
3847
+ detail: checkDetail()
3848
+ };
3849
+ let detail;
3850
+ if (GEMINI_BACKUP[state.kind]) detail = backupDestination(id, path, options);
3851
+ writeTemplate(path, content);
3852
+ return {
3853
+ name: template.name,
3854
+ source: template.path,
3855
+ path,
3856
+ action,
3857
+ detail
3858
+ };
3859
+ }
3860
+ function staleGeminiSource(path, sourceDir) {
3861
+ let content;
3862
+ try {
3863
+ content = readFileSync(path, "utf8");
3864
+ } catch {
3865
+ return null;
3866
+ }
3867
+ const source = generatedSource(content);
3868
+ if (source === null) return null;
3869
+ return isInsideDirectory(source, sourceDir) ? source : null;
3870
+ }
3871
+ function pruneGeminiTarget(id, targetDir, sourceDir, activeNames, check, options) {
3872
+ let entries;
3873
+ try {
3874
+ entries = readdirSync(targetDir);
3875
+ } catch (error) {
3876
+ if (error.code === "ENOENT") return [];
3877
+ throw error;
3878
+ }
3879
+ return entries.filter((entry) => !activeNames.has(entry)).map((entry) => {
3880
+ const path = join(targetDir, entry);
3881
+ const name = basename(entry, extname(entry));
3882
+ const source = staleGeminiSource(path, sourceDir);
3883
+ if (check) return {
3884
+ name,
3885
+ ...source === null ? {} : { source },
3886
+ path,
3887
+ action: "removed",
3888
+ detail: checkDetail()
3889
+ };
3890
+ if (source !== null) {
3891
+ unlinkSync(path);
3892
+ return {
3893
+ name,
3894
+ source,
3895
+ path,
3896
+ action: "removed"
3897
+ };
3898
+ }
3899
+ return {
3900
+ name,
3901
+ path,
3902
+ action: "removed",
3903
+ detail: backupDestination(id, path, options)
3904
+ };
3905
+ });
3906
+ }
3907
+ function syncTarget(harness, templates, sourceDir, check, options) {
3908
+ const target = harness.promptTemplateSyncTarget;
3909
+ if (target === null) return {
3910
+ id: harness.id,
3911
+ detail: "no stable user-scope prompt template destination",
3912
+ templates: []
3913
+ };
3914
+ const resolved = harness.resolveCandidates([target], options)[0];
3915
+ if (resolved === void 0) return {
3916
+ id: harness.id,
3917
+ detail: "prompt template destination is unavailable on this platform",
3918
+ templates: []
3919
+ };
3920
+ const targetDir = resolve(resolved.path);
3921
+ if (resolved.format === "markdown") return {
3922
+ id: harness.id,
3923
+ path: targetDir,
3924
+ format: resolved.format,
3925
+ ...syncMarkdownDirectory(harness.id, targetDir, sourceDir, check, options),
3926
+ templates: []
3927
+ };
3928
+ const results = templates.map((template) => syncGeminiTemplate(harness.id, targetDir, template, check, options));
3929
+ const activeNames = new Set(templates.map((template) => `${template.name}.toml`));
3930
+ results.push(...pruneGeminiTarget(harness.id, targetDir, sourceDir, activeNames, check, options));
3931
+ return {
3932
+ id: harness.id,
3933
+ path: targetDir,
3934
+ format: resolved.format,
3935
+ templates: results
3936
+ };
3937
+ }
3938
+ function syncPromptTemplates(harnesses, check = false, options = {}) {
3939
+ const source = join(agntnDataDir(options), "prompts");
3940
+ const templates = readTemplates(source, check);
3941
+ const prepared = harnesses.some((harness) => harness.promptTemplateSyncTarget?.format === "gemini-toml") ? templates.map((template) => ({
3942
+ ...template,
3943
+ gemini: geminiContent(template)
3944
+ })) : templates;
3945
+ return {
3946
+ source,
3947
+ check,
3948
+ templates: prepared.map((template) => template.name),
3949
+ targets: harnesses.map((harness) => syncTarget(harness, prepared, source, check, options))
3137
3950
  };
3138
3951
  }
3139
- export { Harness, McpEnvError, addMcpServer, detectHarness, detectHarnessFromEnv, detectProjectHarnesses, getAllHarnesses, getHarness, isHarnessId, listHarnesses, listMcpServers, masterMcpPath, parseJsonc, readAgentsConfig, readMasterMcpServers, registerHarness, removeMcpServer, resolvePathTemplate, syncAgentsFiles, syncMcpServers };
3952
+ export { Harness, McpEnvError, addMcpServer, agntnConfigDir, agntnDataDir, detectHarness, detectHarnessFromEnv, detectProjectHarnesses, getAllHarnesses, getHarness, isHarnessId, listHarnesses, listMcpServers, masterMcpPath, parseJsonc, readAgentsConfig, readMasterMcpServers, registerHarness, removeMcpServer, resolvePathTemplate, syncAgentsFiles, syncMcpServers, syncPromptTemplates };
3140
3953
 
3141
- //# sourceMappingURL=agents-sync.mjs.map
3954
+ //# sourceMappingURL=prompt-sync.mjs.map