@agntn/harnesses 0.1.1 → 0.2.1

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 (47) hide show
  1. package/README.md +152 -117
  2. package/dist/_chunks/agents.mjs +2 -30
  3. package/dist/_chunks/agents.mjs.map +1 -1
  4. package/dist/_chunks/libs/typebox.mjs +535 -608
  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} +91 -44
  13. package/dist/_chunks/prompt-sync.d.mts.map +1 -0
  14. package/dist/_chunks/{agents-sync.mjs → prompt-sync.mjs} +565 -26
  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 +43 -10
  19. package/dist/_chunks/tool-operations.mjs.map +1 -1
  20. package/dist/_chunks/tool-schemas.d.mts +17 -13
  21. package/dist/_chunks/tool-schemas.d.mts.map +1 -1
  22. package/dist/_chunks/tool-schemas.mjs +4 -0
  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 +6 -2
  27. package/dist/cli.mjs.map +1 -1
  28. package/dist/index.d.mts +26 -12
  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 +1 -2
  33. package/dist/mcp.d.mts.map +1 -1
  34. package/dist/mcp.mjs +176 -1
  35. package/dist/mcp.mjs.map +1 -0
  36. package/dist/tool-operations.d.mts +42 -32
  37. package/dist/tool-operations.d.mts.map +1 -1
  38. package/dist/tool-operations.mjs +2 -2
  39. package/dist/tool-schemas.d.mts +2 -2
  40. package/package.json +24 -21
  41. package/packages/omp/extensions/harnesses.ts +19 -1
  42. package/packages/pi/extensions/harnesses.ts +26 -2
  43. package/packages/shared/tui.ts +22 -1
  44. package/dist/_chunks/agents-sync.d.mts.map +0 -1
  45. package/dist/_chunks/agents-sync.mjs.map +0 -1
  46. package/dist/_chunks/mcp2.mjs +0 -17
  47. package/dist/_chunks/mcp2.mjs.map +0 -1
@@ -1,11 +1,14 @@
1
+ import { createRequire } from "node:module";
1
2
  import { addAbortListener } from "node:events";
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
4
  import { execFile, execFileSync, spawn } from "node:child_process";
4
5
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
5
- 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";
6
7
  import os, { homedir } from "node:os";
7
8
  import { stripVTControlCharacters } from "node:util";
8
9
  import { parse } from "smol-toml";
10
+ import { randomUUID } from "node:crypto";
11
+ import { parse as parse$1 } from "yaml";
9
12
  const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|%([^%]+)%/g;
10
13
  function resolvePathTemplate(template, options = {}) {
11
14
  const homeDir = options.homeDir ?? os.homedir();
@@ -20,6 +23,10 @@ function agntnConfigDir(options = {}) {
20
23
  const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME !== "" ? process.env.XDG_CONFIG_HOME : join(options.homeDir ?? os.homedir(), ".config");
21
24
  return join(base, "agntn");
22
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
+ }
23
30
  const SUPPORTED_PLATFORMS = {
24
31
  linux: true,
25
32
  darwin: true,
@@ -196,6 +203,8 @@ function executeCommand(command, args, options) {
196
203
  });
197
204
  }
198
205
  var Harness = class {
206
+ promptTemplates = [];
207
+ promptTemplateSyncTarget = null;
199
208
  modelListing = null;
200
209
  mcpConfigs = [];
201
210
  agentsFile = null;
@@ -324,6 +333,8 @@ var Harness = class {
324
333
  instructions: this.resolveCandidates(this.instructions, options),
325
334
  skills: this.resolveCandidates(this.skills, options),
326
335
  commands: this.resolveCandidates(this.commands, options),
336
+ promptTemplates: this.resolveCandidates(this.promptTemplates, options),
337
+ promptTemplateSyncTarget: this.promptTemplateSyncTarget === null ? null : this.resolveCandidates([this.promptTemplateSyncTarget], options)[0] ?? null,
327
338
  hooks: this.resolveCandidates(this.hooks, options)
328
339
  };
329
340
  }
@@ -601,6 +612,13 @@ var Claude = class extends Harness {
601
612
  level: "official",
602
613
  note: "Legacy global slash commands."
603
614
  }];
615
+ promptTemplates = this.commands;
616
+ promptTemplateSyncTarget = {
617
+ path: "~/.claude/commands/",
618
+ scope: "user",
619
+ level: "official",
620
+ format: "markdown"
621
+ };
604
622
  hooks = [{
605
623
  path: ".claude/hooks/",
606
624
  scope: "project",
@@ -622,6 +640,7 @@ var Claude = class extends Harness {
622
640
  noToolsArgs: [
623
641
  "-p",
624
642
  "{prompt}",
643
+ "--strict-mcp-config",
625
644
  "--tools",
626
645
  ""
627
646
  ],
@@ -630,6 +649,7 @@ var Claude = class extends Harness {
630
649
  "--output-format",
631
650
  "json",
632
651
  "{prompt}",
652
+ "--strict-mcp-config",
633
653
  "--tools",
634
654
  ""
635
655
  ],
@@ -652,7 +672,7 @@ var Claude = class extends Harness {
652
672
  readOnlyMinVersion: "2.1.175",
653
673
  modelArgs: ["--model", "{model}"],
654
674
  level: "official",
655
- 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."
675
+ note: "Headless print mode; add --output-format json for structured output. --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."
656
676
  };
657
677
  mcpConfigs = [{
658
678
  path: "~/.claude.json",
@@ -807,7 +827,19 @@ var Codex = class extends Harness {
807
827
  platforms: ["linux", "darwin"]
808
828
  }
809
829
  ];
810
- commands = [];
830
+ commands = [{
831
+ path: "~/.codex/prompts/",
832
+ scope: "user",
833
+ level: "official",
834
+ note: "Deprecated Markdown prompts invoked as /prompts:name. Top-level files only; use skills for shared prompts. Base directory follows CODEX_HOME."
835
+ }];
836
+ promptTemplates = this.commands;
837
+ promptTemplateSyncTarget = {
838
+ path: "~/.codex/prompts/",
839
+ scope: "user",
840
+ level: "official",
841
+ format: "markdown"
842
+ };
811
843
  hooks = [];
812
844
  invocation = {
813
845
  args: [
@@ -963,6 +995,18 @@ var Freebuff = class extends Harness {
963
995
  id = "freebuff";
964
996
  name = "Freebuff";
965
997
  binaries = ["freebuff"];
998
+ get version() {
999
+ const configDir = join(homedir(), ".config", "manicode");
1000
+ const binaryName = process.platform === "win32" ? "freebuff.exe" : "freebuff";
1001
+ if (!existsSync(join(configDir, binaryName))) return null;
1002
+ try {
1003
+ const metadata = JSON.parse(readFileSync(join(configDir, "freebuff-metadata.json"), "utf8"));
1004
+ if (typeof metadata === "object" && metadata !== null && "version" in metadata && typeof metadata.version === "string" && metadata.version !== "") return metadata.version;
1005
+ } catch {
1006
+ return null;
1007
+ }
1008
+ return null;
1009
+ }
966
1010
  capabilities = {
967
1011
  mcp: true,
968
1012
  vision: true,
@@ -1158,6 +1202,13 @@ var Gemini = class extends Harness {
1158
1202
  level: "official",
1159
1203
  note: "Global custom commands in TOML format."
1160
1204
  }];
1205
+ promptTemplates = this.commands;
1206
+ promptTemplateSyncTarget = {
1207
+ path: "~/.gemini/commands/",
1208
+ scope: "user",
1209
+ level: "official",
1210
+ format: "gemini-toml"
1211
+ };
1161
1212
  hooks = [];
1162
1213
  invocation = {
1163
1214
  args: ["-p", "{prompt}"],
@@ -1417,10 +1468,43 @@ var Grok = class extends Harness {
1417
1468
  projectMarkers: [".grok"]
1418
1469
  };
1419
1470
  };
1471
+ function packageVersion(commandPath) {
1472
+ try {
1473
+ const require = createRequire(realpathSync(commandPath));
1474
+ const metadata = JSON.parse(readFileSync(require.resolve("mastracode/package.json"), "utf8"));
1475
+ if (typeof metadata === "object" && metadata !== null && "version" in metadata && typeof metadata.version === "string" && metadata.version !== "") return metadata.version;
1476
+ } catch {
1477
+ return null;
1478
+ }
1479
+ return null;
1480
+ }
1420
1481
  var MastraCode = class extends Harness {
1421
1482
  id = "mastracode";
1422
1483
  name = "Mastra Code";
1423
1484
  binaries = ["mastracode"];
1485
+ get version() {
1486
+ const locator = process.platform === "win32" ? "where" : "which";
1487
+ let output;
1488
+ try {
1489
+ output = execFileSync(locator, [this.binaries[0] ?? "mastracode"], {
1490
+ encoding: "utf8",
1491
+ stdio: [
1492
+ "pipe",
1493
+ "pipe",
1494
+ "pipe"
1495
+ ],
1496
+ timeout: 5e3
1497
+ });
1498
+ } catch {
1499
+ return null;
1500
+ }
1501
+ const paths = output.split(/\r?\n/u).filter((path) => path !== "");
1502
+ for (const path of paths) {
1503
+ const version = packageVersion(path);
1504
+ if (version !== null) return version;
1505
+ }
1506
+ return null;
1507
+ }
1424
1508
  capabilities = {
1425
1509
  mcp: true,
1426
1510
  vision: true,
@@ -1442,6 +1526,12 @@ var MastraCode = class extends Harness {
1442
1526
  level: "official",
1443
1527
  note: "Global MCP server configurations."
1444
1528
  },
1529
+ {
1530
+ path: ".mcp.json",
1531
+ scope: "project",
1532
+ level: "official",
1533
+ note: "Claude Code compatible project MCP servers, read below .mastracode/mcp.json."
1534
+ },
1445
1535
  {
1446
1536
  path: ".mastracode/hooks.json",
1447
1537
  scope: "project",
@@ -1533,16 +1623,51 @@ var MastraCode = class extends Harness {
1533
1623
  note: "Claude Code compatible."
1534
1624
  }
1535
1625
  ];
1536
- commands = [{
1537
- path: ".mastracode/commands/",
1538
- scope: "project",
1539
- level: "official"
1540
- }, {
1626
+ commands = [
1627
+ {
1628
+ path: ".mastracode/commands/",
1629
+ scope: "project",
1630
+ level: "official",
1631
+ note: "Markdown prompt commands; subdirectories become colon-separated command names."
1632
+ },
1633
+ {
1634
+ path: "~/.mastracode/commands/",
1635
+ scope: "user",
1636
+ level: "official",
1637
+ note: "Global Markdown prompt commands."
1638
+ },
1639
+ {
1640
+ path: ".claude/commands/",
1641
+ scope: "project",
1642
+ level: "official",
1643
+ note: "Claude compatibility."
1644
+ },
1645
+ {
1646
+ path: "~/.claude/commands/",
1647
+ scope: "user",
1648
+ level: "official",
1649
+ note: "Claude compatibility."
1650
+ },
1651
+ {
1652
+ path: ".opencode/command/",
1653
+ scope: "project",
1654
+ level: "official",
1655
+ note: "Mastra's OpenCode compatibility loader uses the singular command directory."
1656
+ },
1657
+ {
1658
+ path: "~/.opencode/command/",
1659
+ scope: "user",
1660
+ level: "official",
1661
+ note: "Mastra's OpenCode compatibility loader uses the singular command directory."
1662
+ }
1663
+ ];
1664
+ promptTemplates = this.commands;
1665
+ promptTemplateSyncTarget = {
1541
1666
  path: "~/.mastracode/commands/",
1542
1667
  scope: "user",
1543
1668
  level: "official",
1544
- note: "Global commands."
1545
- }];
1669
+ format: "markdown"
1670
+ };
1546
1671
  hooks = [{
1547
1672
  path: ".mastracode/hooks.json",
1548
1673
  scope: "project",
@@ -1554,6 +1679,35 @@ var MastraCode = class extends Harness {
1554
1679
  note: "Global hooks."
1555
1680
  }];
1556
1681
  invocation = null;
1682
+ mcpConfigs = [
1683
+ {
1684
+ path: "~/.mastracode/mcp.json",
1685
+ scope: "user",
1686
+ level: "official",
1687
+ format: "json",
1688
+ key: ["mcpServers"],
1689
+ dialect: "mastracode",
1690
+ 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."
1691
+ },
1692
+ {
1693
+ path: ".mastracode/mcp.json",
1694
+ scope: "project",
1695
+ level: "official",
1696
+ format: "json",
1697
+ key: ["mcpServers"],
1698
+ dialect: "mastracode",
1699
+ note: "Highest priority, overrides the other files entry by entry."
1700
+ },
1701
+ {
1702
+ path: ".mcp.json",
1703
+ scope: "project",
1704
+ level: "official",
1705
+ format: "json",
1706
+ key: ["mcpServers"],
1707
+ dialect: "mastracode",
1708
+ 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."
1709
+ }
1710
+ ];
1557
1711
  detection = {
1558
1712
  envVars: [],
1559
1713
  projectMarkers: [".mastracode"]
@@ -1701,13 +1855,35 @@ var Omp = class extends Harness {
1701
1855
  commands = [{
1702
1856
  path: ".omp/commands/",
1703
1857
  scope: "project",
1704
- level: "official"
1858
+ level: "official",
1859
+ note: "Markdown prompt commands and TypeScript commands; only Markdown files are templates."
1705
1860
  }, {
1706
1861
  path: "~/.omp/agent/commands/",
1707
1862
  scope: "user",
1708
1863
  level: "official",
1709
- note: "Global user-level prompt commands."
1864
+ note: "Global Markdown prompt commands and TypeScript commands; only Markdown files are templates."
1710
1865
  }];
1866
+ promptTemplates = [
1867
+ ...this.commands,
1868
+ {
1869
+ path: ".omp/prompts/",
1870
+ scope: "project",
1871
+ level: "official",
1872
+ note: "Markdown prompt templates invoked as /name; subdirectories are scanned."
1873
+ },
1874
+ {
1875
+ path: "~/.omp/agent/prompts/",
1876
+ scope: "user",
1877
+ level: "official",
1878
+ note: "Global Markdown prompt templates, separate from executable commands."
1879
+ }
1880
+ ];
1881
+ promptTemplateSyncTarget = {
1882
+ path: "~/.omp/agent/prompts/",
1883
+ scope: "user",
1884
+ level: "official",
1885
+ format: "markdown"
1886
+ };
1711
1887
  hooks = [];
1712
1888
  invocation = {
1713
1889
  args: ["-p", "{prompt}"],
@@ -1850,7 +2026,24 @@ var OpenCode = class extends Harness {
1850
2026
  level: "official"
1851
2027
  }
1852
2028
  ];
1853
- commands = [];
2029
+ commands = [{
2030
+ path: ".opencode/commands/",
2031
+ scope: "project",
2032
+ level: "official",
2033
+ note: "Markdown prompt commands; also configurable through command entries in opencode.json."
2034
+ }, {
2035
+ path: "~/.config/opencode/commands/",
2036
+ scope: "user",
2037
+ level: "official",
2038
+ note: "Global Markdown prompt commands."
2039
+ }];
2040
+ promptTemplates = this.commands;
2041
+ promptTemplateSyncTarget = {
2042
+ path: "~/.config/opencode/commands/",
2043
+ scope: "user",
2044
+ level: "official",
2045
+ format: "markdown"
2046
+ };
1854
2047
  hooks = [];
1855
2048
  invocation = {
1856
2049
  args: ["run", "{prompt}"],
@@ -2012,6 +2205,13 @@ var Pi = class extends Harness {
2012
2205
  level: "official",
2013
2206
  note: "Global user-level prompt templates."
2014
2207
  }];
2208
+ promptTemplates = this.commands;
2209
+ promptTemplateSyncTarget = {
2210
+ path: "~/.pi/agent/prompts/",
2211
+ scope: "user",
2212
+ level: "official",
2213
+ format: "markdown"
2214
+ };
2015
2215
  hooks = [];
2016
2216
  invocation = {
2017
2217
  args: ["-p", "{prompt}"],
@@ -2182,6 +2382,13 @@ var PrimeAgent = class extends Harness {
2182
2382
  level: "official",
2183
2383
  note: "Global user-level prompt templates."
2184
2384
  }];
2385
+ promptTemplates = this.commands;
2386
+ promptTemplateSyncTarget = {
2387
+ path: "~/.prime/agent/prompts/",
2388
+ scope: "user",
2389
+ level: "official",
2390
+ format: "markdown"
2391
+ };
2185
2392
  hooks = [];
2186
2393
  invocation = {
2187
2394
  args: ["-p", "{prompt}"],
@@ -2411,8 +2618,21 @@ function normalizePrime(name, raw) {
2411
2618
  env: primeEnv(raw.env)
2412
2619
  });
2413
2620
  }
2621
+ function normalizeMastracode(name, raw) {
2622
+ const url = stringField(raw, "url");
2623
+ return compact({
2624
+ name,
2625
+ transport: url ? "http" : "stdio",
2626
+ command: stringField(raw, "command"),
2627
+ args: asStringArray(raw.args),
2628
+ env: asStringRecord(raw.env),
2629
+ url,
2630
+ headers: asStringRecord(raw.headers)
2631
+ });
2632
+ }
2414
2633
  function normalizeEntry(name, raw, dialect) {
2415
2634
  if (dialect === "antigravity") return normalizeAntigravity(name, raw);
2635
+ if (dialect === "mastracode") return normalizeMastracode(name, raw);
2416
2636
  if (dialect === "opencode") return normalizeOpenCode(name, raw);
2417
2637
  if (dialect === "prime") return normalizePrime(name, raw);
2418
2638
  return normalizeStandard(name, raw);
@@ -2439,6 +2659,17 @@ function denormalizeOpenCode(server) {
2439
2659
  enabled: server.enabled
2440
2660
  });
2441
2661
  }
2662
+ function denormalizeMastracode(server) {
2663
+ if (server.url) return compact({
2664
+ url: server.url,
2665
+ headers: server.headers
2666
+ });
2667
+ return compact({
2668
+ command: server.command,
2669
+ args: server.args?.length ? server.args : void 0,
2670
+ env: server.env
2671
+ });
2672
+ }
2442
2673
  function denormalizeStandard(server) {
2443
2674
  return compact({
2444
2675
  type: server.transport,
@@ -2484,8 +2715,18 @@ function resolveEnvReferences(server, dialect) {
2484
2715
  env
2485
2716
  };
2486
2717
  }
2718
+ function shapeForDialect(server, dialect) {
2719
+ const shaped = resolveEnvReferences(server, dialect);
2720
+ if (dialect !== "mastracode") return shaped;
2721
+ return compact({
2722
+ ...shaped,
2723
+ transport: shaped.url ? "http" : "stdio",
2724
+ enabled: void 0
2725
+ });
2726
+ }
2487
2727
  function denormalizeEntry(server, dialect) {
2488
2728
  if (dialect === "antigravity") return denormalizeAntigravity(server);
2729
+ if (dialect === "mastracode") return denormalizeMastracode(server);
2489
2730
  if (dialect === "opencode") return denormalizeOpenCode(server);
2490
2731
  if (dialect === "prime") return denormalizePrime(server);
2491
2732
  return denormalizeStandard(server);
@@ -2543,9 +2784,6 @@ function writeAtomically(path, content) {
2543
2784
  writeFileSync(temp, content);
2544
2785
  renameSync(temp, path);
2545
2786
  }
2546
- function escapeRegExp(value) {
2547
- return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
2548
- }
2549
2787
  function tomlValue(value) {
2550
2788
  return JSON.stringify(value);
2551
2789
  }
@@ -2567,8 +2805,8 @@ function tomlSections(key, server) {
2567
2805
  return `${lines.join("\n")}\n`;
2568
2806
  }
2569
2807
  function stripTomlServer(raw, key, name) {
2570
- const prefix = escapeRegExp(key.map(tomlKeyPart).join("."));
2571
- const namePart = `(?:${escapeRegExp(tomlKeyPart(name))}|${escapeRegExp(JSON.stringify(name))})`;
2808
+ const prefix = RegExp.escape(key.map(tomlKeyPart).join("."));
2809
+ const namePart = `(?:${RegExp.escape(tomlKeyPart(name))}|${RegExp.escape(JSON.stringify(name))})`;
2572
2810
  const sectionRe = new RegExp(`^\\s*\\[${prefix}\\.${namePart}(\\.[^\\]]+)?\\]`);
2573
2811
  const bareTableRe = new RegExp(`^\\s*\\[${prefix}\\]\\s*(#.*)?$`);
2574
2812
  const anyHeaderRe = /^\s*\[/;
@@ -2624,7 +2862,7 @@ function removeTomlServer(path, key, name) {
2624
2862
  }
2625
2863
  function addMcpServer(harness, server, scope = "user", options = {}) {
2626
2864
  const { entry, path } = writableConfig(harness, scope, options);
2627
- const resolved = resolveEnvReferences(server, entry.dialect);
2865
+ const resolved = shapeForDialect(server, entry.dialect);
2628
2866
  if (entry.format === "toml") return {
2629
2867
  path,
2630
2868
  replaced: addTomlServer(path, entry.key, resolved)
@@ -2844,7 +3082,7 @@ function syncIncludedHarness(harness, master, existing, masterNames, initialPath
2844
3082
  let path = initialPath;
2845
3083
  const { dialect } = writableConfig(harness, "user", options).entry;
2846
3084
  for (const server of master.servers) try {
2847
- const resolved = resolveEnvReferences(server, dialect);
3085
+ const resolved = shapeForDialect(server, dialect);
2848
3086
  if (existing.get(server.name) === canonical(resolved)) {
2849
3087
  results.push({
2850
3088
  name: server.name,
@@ -2992,6 +3230,15 @@ function relink(path, source) {
2992
3230
  symlinkSync(source, temp);
2993
3231
  renameSync(temp, path);
2994
3232
  }
3233
+ function moveFile(from, to) {
3234
+ try {
3235
+ renameSync(from, to);
3236
+ } catch (error) {
3237
+ if (error.code !== "EXDEV") throw error;
3238
+ copyFileSync(from, to);
3239
+ unlinkSync(from);
3240
+ }
3241
+ }
2995
3242
  const CHECK_ACTION = {
2996
3243
  missing: "linked",
2997
3244
  "correct-link": "unchanged",
@@ -3004,7 +3251,7 @@ function applyFileTarget(id, path, source, state, options) {
3004
3251
  const backupDir = join(agntnConfigDir(options), "diverged");
3005
3252
  mkdirSync(backupDir, { recursive: true });
3006
3253
  const backup = join(backupDir, `${id}-${basename(path)}-${Date.now()}.md`);
3007
- renameSync(path, backup);
3254
+ moveFile(path, backup);
3008
3255
  relink(path, source);
3009
3256
  return {
3010
3257
  path,
@@ -3039,7 +3286,7 @@ function syncFileTarget(id, path, source, check, options) {
3039
3286
  };
3040
3287
  return applyFileTarget(id, path, source, state, options);
3041
3288
  }
3042
- function syncTarget(harness, config, check, options) {
3289
+ function syncTarget$1(harness, config, check, options) {
3043
3290
  if (config.excludes.includes(harness.id)) return {
3044
3291
  id: harness.id,
3045
3292
  action: "skipped",
@@ -3090,9 +3337,301 @@ function syncAgentsFiles(harnesses, check = false, options = {}) {
3090
3337
  return {
3091
3338
  source: config.source,
3092
3339
  check,
3093
- targets: harnesses.map((harness) => syncTarget(harness, config, check, options))
3340
+ targets: harnesses.map((harness) => syncTarget$1(harness, config, check, options))
3341
+ };
3342
+ }
3343
+ const GENERATED_MARKER = "# Generated by @agntn/harnesses from ";
3344
+ const PORTABLE_PROMPT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
3345
+ const GEMINI_ACTION = {
3346
+ missing: "generated",
3347
+ "correct-link": "adopted",
3348
+ "wrong-link": "adopted",
3349
+ "owned-file": "replaced",
3350
+ "unmanaged-file": "adopted",
3351
+ unsupported: "skipped"
3352
+ };
3353
+ const GEMINI_BACKUP = {
3354
+ missing: false,
3355
+ "correct-link": true,
3356
+ "wrong-link": true,
3357
+ "owned-file": true,
3358
+ "unmanaged-file": true,
3359
+ unsupported: false
3360
+ };
3361
+ function readTemplates(sourceDir, check) {
3362
+ let entries;
3363
+ try {
3364
+ entries = readdirSync(sourceDir, { withFileTypes: true });
3365
+ } catch (error) {
3366
+ if (error.code !== "ENOENT") throw error;
3367
+ if (!check) mkdirSync(sourceDir, { recursive: true });
3368
+ return [];
3369
+ }
3370
+ const templates = entries.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && extname(entry.name).toLowerCase() === ".md").map((entry) => {
3371
+ const name = basename(entry.name, extname(entry.name));
3372
+ if (!PORTABLE_PROMPT_NAME.test(name)) throw new TypeError(`Invalid prompt template filename: ${entry.name}`);
3373
+ const path = join(sourceDir, entry.name);
3374
+ return {
3375
+ name,
3376
+ path,
3377
+ content: readFileSync(path, "utf8")
3378
+ };
3379
+ }).sort((left, right) => left.name.localeCompare(right.name));
3380
+ const names = templates.map((template) => template.name.toLowerCase());
3381
+ if (new Set(names).size !== names.length) throw new Error(`Duplicate prompt template names in ${sourceDir}`);
3382
+ return templates;
3383
+ }
3384
+ function generatedSource(content) {
3385
+ const firstLine = content.split("\n", 1)[0] ?? "";
3386
+ if (!firstLine.startsWith(GENERATED_MARKER)) return null;
3387
+ try {
3388
+ const source = JSON.parse(firstLine.slice(37));
3389
+ return typeof source === "string" ? source : null;
3390
+ } catch {
3391
+ return null;
3392
+ }
3393
+ }
3394
+ function inspectDestination(path, source) {
3395
+ let stats;
3396
+ try {
3397
+ stats = lstatSync(path);
3398
+ } catch (error) {
3399
+ if (error.code === "ENOENT") return { kind: "missing" };
3400
+ throw error;
3401
+ }
3402
+ if (stats.isSymbolicLink()) {
3403
+ if (source === void 0) return { kind: "unmanaged-file" };
3404
+ const target = readlinkSync(path);
3405
+ return (isAbsolute(target) ? target : resolve(dirname(path), target)) === source ? { kind: "correct-link" } : { kind: "wrong-link" };
3406
+ }
3407
+ if (!stats.isFile()) return { kind: "unsupported" };
3408
+ const content = readFileSync(path, "utf8");
3409
+ if (generatedSource(content) !== null) return {
3410
+ kind: "owned-file",
3411
+ content
3412
+ };
3413
+ return { kind: "unmanaged-file" };
3414
+ }
3415
+ function tempPath(path, kind) {
3416
+ return join(dirname(path), `.${basename(path)}-${randomUUID()}.${kind}.tmp`);
3417
+ }
3418
+ function linkTemplate(path, source) {
3419
+ mkdirSync(dirname(path), { recursive: true });
3420
+ const temp = tempPath(path, "prompt-link");
3421
+ symlinkSync(source, temp);
3422
+ renameSync(temp, path);
3423
+ }
3424
+ function writeTemplate(path, content) {
3425
+ mkdirSync(dirname(path), { recursive: true });
3426
+ const temp = tempPath(path, "prompt-write");
3427
+ writeFileSync(temp, content);
3428
+ renameSync(temp, path);
3429
+ }
3430
+ function backupDestination(id, path, options) {
3431
+ const backupDir = join(agntnDataDir(options), "diverged", "prompts", id);
3432
+ mkdirSync(backupDir, { recursive: true });
3433
+ const backup = join(backupDir, `${basename(path)}-${Date.now()}-${randomUUID()}`);
3434
+ if (lstatSync(path).isSymbolicLink()) {
3435
+ const target = readlinkSync(path);
3436
+ symlinkSync(isAbsolute(target) ? target : resolve(dirname(path), target), backup);
3437
+ unlinkSync(path);
3438
+ return backup;
3439
+ }
3440
+ moveFile(path, backup);
3441
+ return backup;
3442
+ }
3443
+ function checkDetail() {
3444
+ return "check mode: not applied";
3445
+ }
3446
+ function syncMarkdownTemplate(id, targetDir, template, check, options) {
3447
+ const path = join(targetDir, `${template.name}.md`);
3448
+ const state = inspectDestination(path, template.path);
3449
+ if (state.kind === "correct-link") return {
3450
+ name: template.name,
3451
+ source: template.path,
3452
+ path,
3453
+ action: "unchanged"
3454
+ };
3455
+ if (state.kind === "unsupported") return {
3456
+ name: template.name,
3457
+ source: template.path,
3458
+ path,
3459
+ action: "skipped",
3460
+ detail: "destination exists and is not a file or symlink"
3461
+ };
3462
+ const action = state.kind === "missing" ? "linked" : state.kind === "wrong-link" ? "relinked" : "adopted";
3463
+ if (check) return {
3464
+ name: template.name,
3465
+ source: template.path,
3466
+ path,
3467
+ action,
3468
+ detail: checkDetail()
3469
+ };
3470
+ let detail;
3471
+ if (state.kind === "wrong-link") unlinkSync(path);
3472
+ if (state.kind === "owned-file" || state.kind === "unmanaged-file") detail = backupDestination(id, path, options);
3473
+ linkTemplate(path, template.path);
3474
+ return {
3475
+ name: template.name,
3476
+ source: template.path,
3477
+ path,
3478
+ action,
3479
+ detail
3480
+ };
3481
+ }
3482
+ function frontmatterDescription(raw, path) {
3483
+ const frontmatter = parse$1(raw);
3484
+ if (frontmatter === null) return void 0;
3485
+ if (typeof frontmatter !== "object" || Array.isArray(frontmatter)) throw new TypeError(`Prompt template frontmatter is not an object: ${path}`);
3486
+ const description = frontmatter.description;
3487
+ if (description !== void 0 && typeof description !== "string") throw new TypeError(`Prompt template description is not a string: ${path}`);
3488
+ return description;
3489
+ }
3490
+ function parseMarkdownTemplate(template) {
3491
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(template.content);
3492
+ if (match === null) return { prompt: template.content.replaceAll("$ARGUMENTS", "{{args}}") };
3493
+ const description = frontmatterDescription(match[1] ?? "", template.path);
3494
+ return {
3495
+ ...description === void 0 ? {} : { description },
3496
+ prompt: template.content.slice(match[0].length).replaceAll("$ARGUMENTS", "{{args}}")
3497
+ };
3498
+ }
3499
+ function geminiContent(template) {
3500
+ const parsed = parseMarkdownTemplate(template);
3501
+ return [
3502
+ `${GENERATED_MARKER}${JSON.stringify(template.path)}`,
3503
+ ...parsed.description === void 0 ? [] : [`description = ${JSON.stringify(parsed.description)}`],
3504
+ `prompt = ${JSON.stringify(parsed.prompt)}`,
3505
+ ""
3506
+ ].join("\n");
3507
+ }
3508
+ function syncGeminiTemplate(id, targetDir, template, check, options) {
3509
+ const path = join(targetDir, `${template.name}.toml`);
3510
+ const content = template.gemini ?? geminiContent(template);
3511
+ const state = inspectDestination(path);
3512
+ if (state.kind === "owned-file" && state.content === content) return {
3513
+ name: template.name,
3514
+ source: template.path,
3515
+ path,
3516
+ action: "unchanged"
3517
+ };
3518
+ if (state.kind === "unsupported") return {
3519
+ name: template.name,
3520
+ source: template.path,
3521
+ path,
3522
+ action: "skipped",
3523
+ detail: "destination exists and is not a file or symlink"
3524
+ };
3525
+ const action = GEMINI_ACTION[state.kind];
3526
+ if (check) return {
3527
+ name: template.name,
3528
+ source: template.path,
3529
+ path,
3530
+ action,
3531
+ detail: checkDetail()
3532
+ };
3533
+ let detail;
3534
+ if (GEMINI_BACKUP[state.kind]) detail = backupDestination(id, path, options);
3535
+ writeTemplate(path, content);
3536
+ return {
3537
+ name: template.name,
3538
+ source: template.path,
3539
+ path,
3540
+ action,
3541
+ detail
3542
+ };
3543
+ }
3544
+ function isInsideDirectory(path, directory) {
3545
+ const remainder = relative(directory, path);
3546
+ return remainder !== "" && !remainder.startsWith("..") && !isAbsolute(remainder);
3547
+ }
3548
+ function staleMarkdownSource(path, sourceDir) {
3549
+ if (inspectDestination(path).kind !== "unmanaged-file") return null;
3550
+ let stats;
3551
+ try {
3552
+ stats = lstatSync(path);
3553
+ } catch {
3554
+ return null;
3555
+ }
3556
+ if (!stats.isSymbolicLink()) return null;
3557
+ const target = readlinkSync(path);
3558
+ const resolved = isAbsolute(target) ? target : resolve(dirname(path), target);
3559
+ return isInsideDirectory(resolved, sourceDir) ? resolved : null;
3560
+ }
3561
+ function staleGeminiSource(path, sourceDir) {
3562
+ let content;
3563
+ try {
3564
+ content = readFileSync(path, "utf8");
3565
+ } catch {
3566
+ return null;
3567
+ }
3568
+ const source = generatedSource(content);
3569
+ if (source === null) return null;
3570
+ return isInsideDirectory(source, sourceDir) ? source : null;
3571
+ }
3572
+ function pruneTarget(targetDir, format, sourceDir, activeSources, check) {
3573
+ let entries;
3574
+ try {
3575
+ entries = readdirSync(targetDir, { withFileTypes: true });
3576
+ } catch (error) {
3577
+ if (error.code === "ENOENT") return [];
3578
+ throw error;
3579
+ }
3580
+ const extension = format === "markdown" ? ".md" : ".toml";
3581
+ const staleSource = format === "markdown" ? staleMarkdownSource : staleGeminiSource;
3582
+ return entries.filter((entry) => entry.name.toLowerCase().endsWith(extension)).flatMap((entry) => {
3583
+ const path = join(targetDir, entry.name);
3584
+ const source = staleSource(path, sourceDir);
3585
+ if (source === null || activeSources.has(source)) return [];
3586
+ if (!check) unlinkSync(path);
3587
+ return [{
3588
+ name: basename(entry.name, extension),
3589
+ source,
3590
+ path,
3591
+ action: "removed",
3592
+ ...check ? { detail: checkDetail() } : {}
3593
+ }];
3594
+ });
3595
+ }
3596
+ function syncTarget(harness, templates, sourceDir, check, options) {
3597
+ const target = harness.promptTemplateSyncTarget;
3598
+ if (target === null) return {
3599
+ id: harness.id,
3600
+ detail: "no stable user-scope prompt template destination",
3601
+ templates: []
3602
+ };
3603
+ const resolved = harness.resolveCandidates([target], options)[0];
3604
+ if (resolved === void 0) return {
3605
+ id: harness.id,
3606
+ detail: "prompt template destination is unavailable on this platform",
3607
+ templates: []
3608
+ };
3609
+ const targetDir = resolved.path;
3610
+ const syncTemplate = resolved.format === "markdown" ? syncMarkdownTemplate : syncGeminiTemplate;
3611
+ const results = templates.map((template) => syncTemplate(harness.id, targetDir, template, check, options));
3612
+ const activeSources = new Set(templates.map((template) => template.path));
3613
+ results.push(...pruneTarget(targetDir, resolved.format, sourceDir, activeSources, check));
3614
+ return {
3615
+ id: harness.id,
3616
+ path: targetDir,
3617
+ format: resolved.format,
3618
+ templates: results
3619
+ };
3620
+ }
3621
+ function syncPromptTemplates(harnesses, check = false, options = {}) {
3622
+ const source = join(agntnDataDir(options), "prompts");
3623
+ const templates = readTemplates(source, check);
3624
+ const prepared = harnesses.some((harness) => harness.promptTemplateSyncTarget?.format === "gemini-toml") ? templates.map((template) => ({
3625
+ ...template,
3626
+ gemini: geminiContent(template)
3627
+ })) : templates;
3628
+ return {
3629
+ source,
3630
+ check,
3631
+ templates: prepared.map((template) => template.name),
3632
+ targets: harnesses.map((harness) => syncTarget(harness, prepared, source, check, options))
3094
3633
  };
3095
3634
  }
3096
- export { Harness, McpEnvError, addMcpServer, detectHarness, detectHarnessFromEnv, detectProjectHarnesses, getAllHarnesses, getHarness, isHarnessId, listHarnesses, listMcpServers, masterMcpPath, parseJsonc, readAgentsConfig, readMasterMcpServers, registerHarness, removeMcpServer, resolvePathTemplate, syncAgentsFiles, syncMcpServers };
3635
+ export { Harness, McpEnvError, addMcpServer, agntnConfigDir, agntnDataDir, detectHarness, detectHarnessFromEnv, detectProjectHarnesses, getAllHarnesses, getHarness, isHarnessId, listHarnesses, listMcpServers, masterMcpPath, parseJsonc, readAgentsConfig, readMasterMcpServers, registerHarness, removeMcpServer, resolvePathTemplate, syncAgentsFiles, syncMcpServers, syncPromptTemplates };
3097
3636
 
3098
- //# sourceMappingURL=agents-sync.mjs.map
3637
+ //# sourceMappingURL=prompt-sync.mjs.map