@agntn/harnesses 0.2.1 → 0.3.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 (50) hide show
  1. package/README.md +48 -9
  2. package/dist/_chunks/agents.mjs +5 -3
  3. package/dist/_chunks/agents.mjs.map +1 -1
  4. package/dist/_chunks/mcp-servers.mjs +5 -3
  5. package/dist/_chunks/mcp-servers.mjs.map +1 -1
  6. package/dist/_chunks/mcp.mjs +10 -1
  7. package/dist/_chunks/mcp.mjs.map +1 -1
  8. package/dist/_chunks/output.mjs +7 -1
  9. package/dist/_chunks/output.mjs.map +1 -1
  10. package/dist/_chunks/prompts.mjs +4 -2
  11. package/dist/_chunks/prompts.mjs.map +1 -1
  12. package/dist/_chunks/{prompt-sync.d.mts → skills-sync.d.mts} +174 -57
  13. package/dist/_chunks/skills-sync.d.mts.map +1 -0
  14. package/dist/_chunks/{prompt-sync.mjs → skills-sync.mjs} +665 -148
  15. package/dist/_chunks/skills-sync.mjs.map +1 -0
  16. package/dist/_chunks/skills.mjs +30 -0
  17. package/dist/_chunks/skills.mjs.map +1 -0
  18. package/dist/_chunks/tool-operations.mjs +38 -13
  19. package/dist/_chunks/tool-operations.mjs.map +1 -1
  20. package/dist/_chunks/tool-schemas.d.mts +22 -13
  21. package/dist/_chunks/tool-schemas.d.mts.map +1 -1
  22. package/dist/_chunks/tool-schemas.mjs +18 -11
  23. package/dist/_chunks/tool-schemas.mjs.map +1 -1
  24. package/dist/_chunks/types.mjs +5 -2
  25. package/dist/_chunks/types.mjs.map +1 -1
  26. package/dist/cli.mjs +29 -9
  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 +5 -3
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mcp.d.mts +2 -0
  33. package/dist/mcp.d.mts.map +1 -1
  34. package/dist/mcp.mjs +6261 -5
  35. package/dist/mcp.mjs.map +1 -1
  36. package/dist/tool-operations.d.mts +21 -4
  37. package/dist/tool-operations.d.mts.map +1 -1
  38. package/dist/tool-operations.mjs +3 -3
  39. package/dist/tool-schemas.d.mts +2 -2
  40. package/dist/tool-schemas.mjs +1 -1
  41. package/dist/typebox.LICENSE +23 -0
  42. package/package.json +18 -18
  43. package/packages/omp/extensions/harnesses.ts +19 -1
  44. package/packages/pi/extensions/harnesses.ts +27 -46
  45. package/packages/shared/tui.ts +13 -0
  46. package/dist/THIRD-PARTY-LICENSES.md +0 -36
  47. package/dist/_chunks/libs/typebox.mjs +0 -5439
  48. package/dist/_chunks/libs/typebox.mjs.map +0 -1
  49. package/dist/_chunks/prompt-sync.d.mts.map +0 -1
  50. package/dist/_chunks/prompt-sync.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { addAbortListener } from "node:events";
3
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, 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
6
  import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
@@ -8,13 +8,14 @@ import os, { homedir } from "node:os";
8
8
  import { stripVTControlCharacters } from "node:util";
9
9
  import { parse } from "smol-toml";
10
10
  import { randomUUID } from "node:crypto";
11
- import { parse as parse$1 } from "yaml";
12
- const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|%([^%]+)%/g;
11
+ //#region src/resolve.ts
12
+ const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|\$\{TMPDIR\}|%([^%]+)%/g;
13
13
  function resolvePathTemplate(template, options = {}) {
14
14
  const homeDir = options.homeDir ?? os.homedir();
15
15
  const projectRoot = options.projectRoot ?? process.cwd();
16
16
  return template.replaceAll(PATH_MARKERS, (match, name) => {
17
17
  if (match === "${PROJECT_ROOT}") return projectRoot;
18
+ if (match === "${TMPDIR}") return options.tempDir ?? os.tmpdir();
18
19
  if (name === void 0) return homeDir;
19
20
  return process.env[name] ?? match;
20
21
  });
@@ -27,11 +28,19 @@ function agntnDataDir(options = {}) {
27
28
  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
29
  return join(base, "agntn");
29
30
  }
31
+ //#endregion
32
+ //#region src/harness.ts
33
+ function moveRoot(path, root, value) {
34
+ if (path === root) return value;
35
+ const rest = path.slice(root.length);
36
+ return path.startsWith(root) && (rest.startsWith("/") || rest.startsWith("\\")) ? value + rest : path;
37
+ }
30
38
  const SUPPORTED_PLATFORMS = {
31
39
  linux: true,
32
40
  darwin: true,
33
41
  win32: true
34
42
  };
43
+ const PARENT_ENV = "AGNTN_HARNESSES_PARENT";
35
44
  const ALTERNATE_INVOCATION_MODE = {
36
45
  advisor: "agent",
37
46
  advisorStructured: "agentStructured",
@@ -57,6 +66,17 @@ function requestedInvocationMode(options) {
57
66
  if (options.tools === true) return options.structured === true ? "agentStructured" : "agent";
58
67
  return options.structured === true ? "advisorStructured" : "advisor";
59
68
  }
69
+ function accessConflictError(id, invocation, options) {
70
+ if (options.readOnly !== true || options.tools !== false) return null;
71
+ const retries = [];
72
+ if (invocationTemplate(invocation, requestedInvocationMode(options))) retries.push("tools: true to keep its read-only tools");
73
+ if (invocationTemplate(invocation, requestedInvocationMode({
74
+ ...options,
75
+ readOnly: false
76
+ }))) retries.push("readOnly: false to use its advisor without tools");
77
+ const error = `Harness ${id} cannot run readOnly with tools: false, since read-only access still uses tools`;
78
+ return retries.length === 0 ? error : `${error}; retry with ${retries.join(", or with ")}`;
79
+ }
60
80
  function invocationTemplate(invocation, mode) {
61
81
  switch (mode) {
62
82
  case "advisor": return invocation.noToolsArgs;
@@ -123,11 +143,29 @@ async function terminateCommand(pid) {
123
143
  await setTimeout$1(500);
124
144
  signalProcessGroup(pid, "SIGKILL");
125
145
  }
146
+ function spawnError(command, cwd, error) {
147
+ if (!(error instanceof Error && "code" in error)) return error;
148
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") return error;
149
+ let message;
150
+ if (cwd !== void 0 && !isDirectory(cwd)) message = existsSync(cwd) ? `Working directory is not a directory: ${cwd}` : `Working directory not found: ${cwd}`;
151
+ else if (error.code === "ENOENT") message = `Command not found: ${command}. Install it or add it to PATH`;
152
+ else return error;
153
+ return Object.assign(new Error(message, { cause: error }), { code: error.code });
154
+ }
155
+ function isDirectory(path) {
156
+ try {
157
+ return statSync(path).isDirectory();
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
126
162
  function executeCommand(command, args, options) {
127
163
  return new Promise((resolve, reject) => {
128
164
  let stdout = "";
129
165
  let stderr = "";
130
166
  let stopped;
167
+ let lastOutputAt = performance.now();
168
+ let idleMs;
131
169
  let settled = false;
132
170
  let timer;
133
171
  let abortListener;
@@ -136,28 +174,38 @@ function executeCommand(command, args, options) {
136
174
  finish(null);
137
175
  return;
138
176
  }
139
- const child = spawn(command, args, {
140
- cwd: options.cwd,
141
- env: options.env ? {
142
- ...process.env,
143
- ...options.env
144
- } : process.env,
145
- stdio: [
146
- "ignore",
147
- "pipe",
148
- "pipe"
149
- ],
150
- detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
151
- });
177
+ let child;
178
+ try {
179
+ child = spawn(command, args, {
180
+ cwd: options.cwd,
181
+ env: options.env ? {
182
+ ...process.env,
183
+ ...options.env
184
+ } : process.env,
185
+ stdio: [
186
+ "ignore",
187
+ "pipe",
188
+ "pipe"
189
+ ],
190
+ detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
191
+ });
192
+ } catch (error) {
193
+ fail(spawnError(command, options.cwd, error));
194
+ return;
195
+ }
152
196
  if (options.timeoutMs) timer = setTimeout(() => stop("timeout"), options.timeoutMs);
153
197
  if (options.signal) abortListener = addAbortListener(options.signal, () => stop("abort"));
198
+ child.stdout.setEncoding("utf8");
199
+ child.stderr.setEncoding("utf8");
154
200
  child.stdout.on("data", (chunk) => {
155
- stdout += chunk.toString("utf8");
201
+ stdout += chunk;
202
+ lastOutputAt = performance.now();
156
203
  });
157
204
  child.stderr.on("data", (chunk) => {
158
- stderr += chunk.toString("utf8");
205
+ stderr += chunk;
206
+ lastOutputAt = performance.now();
159
207
  });
160
- child.on("error", fail);
208
+ child.on("error", (error) => fail(spawnError(command, options.cwd, error)));
161
209
  child.on("close", (code) => {
162
210
  if (stopped === void 0) finish(code);
163
211
  });
@@ -174,6 +222,7 @@ function executeCommand(command, args, options) {
174
222
  function stop(reason) {
175
223
  if (settled || stopped !== void 0) return;
176
224
  stopped = reason;
225
+ idleMs = Math.round(performance.now() - lastOutputAt);
177
226
  cleanup();
178
227
  if (child.pid === void 0) return;
179
228
  terminateCommand(child.pid).then(() => {
@@ -197,14 +246,18 @@ function executeCommand(command, args, options) {
197
246
  stderr,
198
247
  exitCode: stopped === void 0 ? code : null,
199
248
  timedOut: stopped === "timeout",
200
- aborted: stopped === "abort"
249
+ aborted: stopped === "abort",
250
+ ...idleMs === void 0 ? {} : { idleMs }
201
251
  });
202
252
  }
203
253
  });
204
254
  }
205
255
  var Harness = class {
256
+ skillsSyncTarget = null;
206
257
  promptTemplates = [];
207
258
  promptTemplateSyncTarget = null;
259
+ temp = [];
260
+ envOverrides = [];
208
261
  modelListing = null;
209
262
  mcpConfigs = [];
210
263
  agentsFile = null;
@@ -258,6 +311,7 @@ var Harness = class {
258
311
  const command = this.invocation.binary ?? this.binaries[0];
259
312
  if (!command) return null;
260
313
  if (options.model !== void 0 && !this.invocation.modelArgs) return null;
314
+ if (accessConflictError(this.id, this.invocation, options) !== null) return null;
261
315
  const template = invocationTemplate(this.invocation, requestedInvocationMode(options));
262
316
  if (!template) return null;
263
317
  return {
@@ -268,11 +322,15 @@ var Harness = class {
268
322
  invocationError(options = {}) {
269
323
  if (!this.invocation) return `Harness ${this.id} has no non-interactive invocation`;
270
324
  if (options.model !== void 0 && !this.invocation.modelArgs) return `Harness ${this.id} does not support model selection`;
325
+ const conflict = accessConflictError(this.id, this.invocation, options);
326
+ if (conflict !== null) return conflict;
271
327
  const mode = requestedInvocationMode(options);
272
328
  if (invocationTemplate(this.invocation, mode)) return null;
273
329
  return `${`Harness ${this.id} has no ${INVOCATION_MODE_DESCRIPTION[mode]} invocation`}${invocationRetryHint(this.invocation, mode, options)}`;
274
330
  }
275
331
  invoke(prompt, options = {}) {
332
+ const parent = process.env[PARENT_ENV];
333
+ if (parent) return Promise.reject(/* @__PURE__ */ new Error(`Nested run refused: this process already runs inside ${parent}, started by harnesses. ${this.id} under it would stack agents, and in a read-only sandbox it gets no network and hangs until the deadline. Answer without it`));
276
334
  const invocationOptions = {
277
335
  model: options.model,
278
336
  structured: options.structured,
@@ -283,7 +341,22 @@ var Harness = class {
283
341
  if (!built) return Promise.reject(new Error(this.invocationError(invocationOptions) ?? "Invalid invocation"));
284
342
  const versionError = this.readOnlyVersionError(invocationOptions);
285
343
  if (versionError) return Promise.reject(new Error(versionError));
286
- return executeCommand(built.command, built.args, options);
344
+ const command = {
345
+ ...options,
346
+ env: {
347
+ ...options.env,
348
+ [PARENT_ENV]: this.id
349
+ }
350
+ };
351
+ const streamArgs = options.structured ? void 0 : this.invocation?.streamArgs;
352
+ if (!streamArgs) return executeCommand(built.command, built.args, command);
353
+ return executeCommand(built.command, [...built.args, ...streamArgs], command).then((result) => ({
354
+ ...result,
355
+ stdout: this.foldStreamOutput(result.stdout, !result.timedOut && !result.aborted)
356
+ }));
357
+ }
358
+ foldStreamOutput(_stdout, _complete) {
359
+ throw new Error(`Harness ${this.id} does not implement stream output folding`);
287
360
  }
288
361
  readOnlyVersionError(options) {
289
362
  const floor = this.invocation?.readOnlyMinVersion;
@@ -298,22 +371,33 @@ var Harness = class {
298
371
  if (!this.modelListing) return null;
299
372
  const command = this.binaries[0];
300
373
  if (!command) return null;
301
- const template = search === void 0 ? this.modelListing.args : this.modelListing.searchArgs;
302
- if (!template) return null;
303
374
  return {
304
375
  command,
305
- args: template.map((arg) => arg.replaceAll("{search}", () => search ?? ""))
376
+ args: (search === void 0 ? this.modelListing.args : this.modelListing.searchArgs ?? this.modelListing.args).map((arg) => arg.replaceAll("{search}", () => search ?? ""))
306
377
  };
307
378
  }
308
379
  async listModels(options = {}) {
309
380
  const built = this.buildModelListInvocation(options.search);
310
- 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`);
381
+ if (!built) throw new Error(`Harness ${this.id} does not support model listing`);
311
382
  const result = await executeCommand(built.command, built.args, options);
383
+ if (result.timedOut || result.aborted || result.exitCode !== 0) return {
384
+ ...result,
385
+ models: []
386
+ };
387
+ const models = this.parseModelListingOutput(result.stdout);
388
+ if (options.search === void 0 || this.modelListing?.searchArgs) return {
389
+ ...result,
390
+ models
391
+ };
392
+ const search = options.search.toLowerCase();
312
393
  return {
313
394
  ...result,
314
- models: !result.timedOut && !result.aborted && result.exitCode === 0 ? this.parseModelListingOutput(result.stdout) : []
395
+ models: models.filter((model) => model.id.toLowerCase().includes(search))
315
396
  };
316
397
  }
398
+ modelSelector(model) {
399
+ return `${model.provider}/${model.id}`;
400
+ }
317
401
  parseModelListingOutput(_stdout) {
318
402
  throw new Error(`Harness ${this.id} does not implement model-list output parsing`);
319
403
  }
@@ -332,13 +416,39 @@ var Harness = class {
332
416
  sessions: this.resolveCandidates(this.sessions, options),
333
417
  instructions: this.resolveCandidates(this.instructions, options),
334
418
  skills: this.resolveCandidates(this.skills, options),
419
+ skillsSyncTarget: this.skillsSyncTarget === null ? null : this.resolveCandidates([this.skillsSyncTarget], options)[0] ?? null,
335
420
  commands: this.resolveCandidates(this.commands, options),
336
421
  promptTemplates: this.resolveCandidates(this.promptTemplates, options),
337
422
  promptTemplateSyncTarget: this.promptTemplateSyncTarget === null ? null : this.resolveCandidates([this.promptTemplateSyncTarget], options)[0] ?? null,
338
- hooks: this.resolveCandidates(this.hooks, options)
423
+ hooks: this.resolveCandidates(this.hooks, options),
424
+ temp: this.resolveTemp(options)
339
425
  };
340
426
  }
427
+ resolveTemp(options) {
428
+ const temp = this.resolveCandidates(this.temp, options);
429
+ const override = this.resolveCandidates(this.envOverrides, options).find((entry) => entry.relocates.includes("temp"));
430
+ const value = override ? process.env[override.variable] : void 0;
431
+ if (!override || !value) return temp;
432
+ return temp.map((entry) => ({
433
+ ...entry,
434
+ path: moveRoot(entry.path, override.path, value)
435
+ }));
436
+ }
341
437
  };
438
+ //#endregion
439
+ //#region src/harnesses/antigravity.ts
440
+ function parseAntigravityModels(stdout) {
441
+ const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
442
+ if (lines.length === 0) throw new Error("Unexpected empty Antigravity model-list output");
443
+ return lines.map((line) => {
444
+ const match = /^(\S+)\t.+$/.exec(line);
445
+ if (!match?.[1]) throw new Error(`Unexpected Antigravity model-list row: ${JSON.stringify(line)}`);
446
+ return {
447
+ provider: "google",
448
+ id: match[1]
449
+ };
450
+ });
451
+ }
342
452
  var Antigravity = class extends Harness {
343
453
  id = "antigravity";
344
454
  name = "Google Antigravity CLI";
@@ -460,6 +570,11 @@ var Antigravity = class extends Harness {
460
570
  scope: "user",
461
571
  level: "official"
462
572
  }];
573
+ skillsSyncTarget = {
574
+ path: "~/.gemini/config/skills/",
575
+ scope: "user",
576
+ level: "official"
577
+ };
463
578
  commands = [];
464
579
  hooks = [{
465
580
  path: ".agents/hooks.json",
@@ -482,6 +597,11 @@ var Antigravity = class extends Harness {
482
597
  level: "official",
483
598
  note: "Non-interactive print mode."
484
599
  };
600
+ modelListing = {
601
+ args: ["models"],
602
+ level: "official",
603
+ note: "Prints one id and display name per row, with no default marked and no filter; verified with 1.2.5."
604
+ };
485
605
  mcpConfigs = [{
486
606
  path: "~/.gemini/config/mcp_config.json",
487
607
  scope: "user",
@@ -495,7 +615,62 @@ var Antigravity = class extends Harness {
495
615
  envVars: [],
496
616
  projectMarkers: [".antigravitycli"]
497
617
  };
618
+ parseModelListingOutput(stdout) {
619
+ return parseAntigravityModels(stdout);
620
+ }
621
+ modelSelector(model) {
622
+ return model.id;
623
+ }
498
624
  };
625
+ //#endregion
626
+ //#region src/harnesses/claude.ts
627
+ function parseStreamLine(line) {
628
+ try {
629
+ const parsed = JSON.parse(line);
630
+ return typeof parsed === "object" && parsed !== null && "type" in parsed && typeof parsed.type === "string" ? parsed : void 0;
631
+ } catch {
632
+ return;
633
+ }
634
+ }
635
+ function startsTextBlock(event) {
636
+ return event.type === "stream_event" && event.event?.type === "content_block_start" && event.event.content_block?.type === "text";
637
+ }
638
+ function textDelta(event) {
639
+ if (event.type !== "stream_event" || event.event?.type !== "content_block_delta") return void 0;
640
+ const delta = event.event.delta;
641
+ return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : void 0;
642
+ }
643
+ var ClaudeStreamFold = class {
644
+ blocks = [];
645
+ other = [];
646
+ result;
647
+ add(line) {
648
+ if (line.trim().length === 0) return;
649
+ const event = parseStreamLine(line);
650
+ if (!event) {
651
+ this.other.push(line);
652
+ return;
653
+ }
654
+ if (event.type === "result" && typeof event.result === "string") this.result = event.result;
655
+ if (startsTextBlock(event)) this.blocks.push("");
656
+ const text = textDelta(event);
657
+ if (text !== void 0) this.blocks.push((this.blocks.pop() ?? "") + text);
658
+ }
659
+ text(complete) {
660
+ const prefix = this.other.map((line) => `${line}\n`).join("");
661
+ if (complete && this.result !== void 0) return `${prefix}${this.result}\n`;
662
+ const text = this.blocks.filter((block) => block.length > 0).join("\n\n");
663
+ return text.length > 0 ? `${prefix}${text}\n` : prefix;
664
+ }
665
+ };
666
+ function foldClaudeStream(stdout, complete) {
667
+ const lines = stdout.split("\n");
668
+ const tail = lines.at(-1);
669
+ if (!complete && tail?.startsWith("{") && parseStreamLine(tail) === void 0) lines.pop();
670
+ const fold = new ClaudeStreamFold();
671
+ for (const line of lines) fold.add(line);
672
+ return fold.text(complete);
673
+ }
499
674
  var Claude = class extends Harness {
500
675
  id = "claude";
501
676
  name = "Anthropic Claude Code";
@@ -601,6 +776,11 @@ var Claude = class extends Harness {
601
776
  level: "official",
602
777
  note: "Global user-level skills."
603
778
  }];
779
+ skillsSyncTarget = {
780
+ path: "~/.claude/skills/",
781
+ scope: "user",
782
+ level: "official"
783
+ };
604
784
  commands = [{
605
785
  path: ".claude/commands/",
606
786
  scope: "project",
@@ -671,8 +851,14 @@ var Claude = class extends Harness {
671
851
  ],
672
852
  readOnlyMinVersion: "2.1.175",
673
853
  modelArgs: ["--model", "{model}"],
854
+ streamArgs: [
855
+ "--output-format",
856
+ "stream-json",
857
+ "--verbose",
858
+ "--include-partial-messages"
859
+ ],
674
860
  level: "official",
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."
861
+ 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."
676
862
  };
677
863
  mcpConfigs = [{
678
864
  path: "~/.claude.json",
@@ -689,6 +875,63 @@ var Claude = class extends Harness {
689
875
  key: ["mcpServers"],
690
876
  dialect: "standard"
691
877
  }];
878
+ temp = [
879
+ {
880
+ path: "${TMPDIR}/claude-<uid>/",
881
+ scope: "data",
882
+ level: "official",
883
+ platforms: ["linux"],
884
+ note: "Session scratchpads sit under <dash-encoded-cwd>/<session-id>/scratchpad."
885
+ },
886
+ {
887
+ path: "/tmp/claude-<uid>/",
888
+ scope: "data",
889
+ level: "official",
890
+ platforms: ["darwin"]
891
+ },
892
+ {
893
+ path: "${TMPDIR}/claude/",
894
+ scope: "data",
895
+ level: "official",
896
+ platforms: ["win32"]
897
+ }
898
+ ];
899
+ envOverrides = [
900
+ {
901
+ variable: "CLAUDE_CONFIG_DIR",
902
+ path: "~/.claude",
903
+ scope: "user",
904
+ level: "official",
905
+ relocates: [
906
+ "config",
907
+ "sessions",
908
+ "instructions",
909
+ "skills",
910
+ "commands",
911
+ "promptTemplates",
912
+ "hooks"
913
+ ],
914
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
915
+ },
916
+ {
917
+ variable: "CLAUDE_CODE_TMPDIR",
918
+ path: "${TMPDIR}",
919
+ scope: "data",
920
+ level: "official",
921
+ platforms: ["linux", "win32"],
922
+ relocates: ["temp"],
923
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
924
+ },
925
+ {
926
+ variable: "CLAUDE_CODE_TMPDIR",
927
+ path: "/tmp",
928
+ scope: "data",
929
+ level: "official",
930
+ platforms: ["darwin"],
931
+ relocates: ["temp"],
932
+ note: "Read from the shell or the env block of user or managed settings, not project settings."
933
+ }
934
+ ];
692
935
  agentsFile = "~/.claude/CLAUDE.md";
693
936
  detection = {
694
937
  envVars: [
@@ -704,7 +947,12 @@ var Claude = class extends Harness {
704
947
  "CLAUDE.md"
705
948
  ]
706
949
  };
950
+ foldStreamOutput(stdout, complete) {
951
+ return foldClaudeStream(stdout, complete);
952
+ }
707
953
  };
954
+ //#endregion
955
+ //#region src/harnesses/codex.ts
708
956
  var Codex = class extends Harness {
709
957
  id = "codex";
710
958
  name = "OpenAI Codex CLI";
@@ -827,11 +1075,16 @@ var Codex = class extends Harness {
827
1075
  platforms: ["linux", "darwin"]
828
1076
  }
829
1077
  ];
1078
+ skillsSyncTarget = {
1079
+ path: "~/.agents/skills/",
1080
+ scope: "user",
1081
+ level: "official"
1082
+ };
830
1083
  commands = [{
831
1084
  path: "~/.codex/prompts/",
832
1085
  scope: "user",
833
1086
  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."
1087
+ note: "Deprecated Markdown prompts invoked as /prompts:name. Top-level files only; use skills for shared prompts."
835
1088
  }];
836
1089
  promptTemplates = this.commands;
837
1090
  promptTemplateSyncTarget = {
@@ -887,6 +1140,20 @@ var Codex = class extends Harness {
887
1140
  key: ["mcp_servers"],
888
1141
  dialect: "standard"
889
1142
  }];
1143
+ envOverrides = [{
1144
+ variable: "CODEX_HOME",
1145
+ path: "~/.codex",
1146
+ scope: "user",
1147
+ level: "official",
1148
+ relocates: [
1149
+ "config",
1150
+ "sessions",
1151
+ "instructions",
1152
+ "skills",
1153
+ "commands",
1154
+ "promptTemplates"
1155
+ ]
1156
+ }];
890
1157
  agentsFile = "~/.codex/AGENTS.md";
891
1158
  detection = {
892
1159
  envVars: [],
@@ -898,6 +1165,8 @@ var Codex = class extends Harness {
898
1165
  ]
899
1166
  };
900
1167
  };
1168
+ //#endregion
1169
+ //#region src/harnesses/cursor.ts
901
1170
  var Cursor = class extends Harness {
902
1171
  id = "cursor";
903
1172
  name = "Cursor";
@@ -957,6 +1226,11 @@ var Cursor = class extends Harness {
957
1226
  note: "Cursor natively scans Codex skills directory."
958
1227
  }
959
1228
  ];
1229
+ skillsSyncTarget = {
1230
+ path: "~/.cursor/skills/",
1231
+ scope: "user",
1232
+ level: "official"
1233
+ };
960
1234
  commands = [];
961
1235
  hooks = [];
962
1236
  invocation = {
@@ -991,6 +1265,8 @@ var Cursor = class extends Harness {
991
1265
  projectMarkers: [".cursor", ".cursorrules"]
992
1266
  };
993
1267
  };
1268
+ //#endregion
1269
+ //#region src/harnesses/freebuff.ts
994
1270
  var Freebuff = class extends Harness {
995
1271
  id = "freebuff";
996
1272
  name = "Freebuff";
@@ -1097,6 +1373,8 @@ var Freebuff = class extends Harness {
1097
1373
  projectMarkers: [".agents", "knowledge.md"]
1098
1374
  };
1099
1375
  };
1376
+ //#endregion
1377
+ //#region src/harnesses/gemini.ts
1100
1378
  var Gemini = class extends Harness {
1101
1379
  id = "gemini";
1102
1380
  name = "Google Gemini CLI";
@@ -1191,6 +1469,11 @@ var Gemini = class extends Harness {
1191
1469
  level: "official"
1192
1470
  }
1193
1471
  ];
1472
+ skillsSyncTarget = {
1473
+ path: "~/.gemini/skills/",
1474
+ scope: "user",
1475
+ level: "official"
1476
+ };
1194
1477
  commands = [{
1195
1478
  path: ".gemini/commands/",
1196
1479
  scope: "project",
@@ -1243,6 +1526,8 @@ var Gemini = class extends Harness {
1243
1526
  projectMarkers: [".gemini", "GEMINI.md"]
1244
1527
  };
1245
1528
  };
1529
+ //#endregion
1530
+ //#region src/harnesses/github-copilot.ts
1246
1531
  var GitHubCopilot = class extends Harness {
1247
1532
  id = "github-copilot";
1248
1533
  name = "GitHub Copilot";
@@ -1311,6 +1596,22 @@ var GitHubCopilot = class extends Harness {
1311
1596
  ]
1312
1597
  };
1313
1598
  };
1599
+ //#endregion
1600
+ //#region src/harnesses/grok.ts
1601
+ function parseGrokModels(stdout) {
1602
+ const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1603
+ const start = lines.indexOf("Available models:");
1604
+ if (start === -1) throw new Error(`Unexpected Grok model-list output: ${JSON.stringify(stdout)}`);
1605
+ return lines.slice(start + 1).map((line) => {
1606
+ const match = /^([*-])\s+(\S+)(?:\s+\(default\))?$/.exec(line);
1607
+ if (!match?.[2]) throw new Error(`Unexpected Grok model-list row: ${JSON.stringify(line)}`);
1608
+ return {
1609
+ provider: "xai",
1610
+ id: match[2],
1611
+ ...match[1] === "*" ? { default: true } : {}
1612
+ };
1613
+ });
1614
+ }
1314
1615
  var Grok = class extends Harness {
1315
1616
  id = "grok";
1316
1617
  name = "xAI Grok CLI";
@@ -1396,6 +1697,11 @@ var Grok = class extends Harness {
1396
1697
  level: "official",
1397
1698
  note: "Global user-level skills."
1398
1699
  }];
1700
+ skillsSyncTarget = {
1701
+ path: "~/.grok/skills/",
1702
+ scope: "user",
1703
+ level: "official"
1704
+ };
1399
1705
  commands = [{
1400
1706
  path: ".grok/commands/",
1401
1707
  scope: "project",
@@ -1443,6 +1749,11 @@ var Grok = class extends Harness {
1443
1749
  level: "official",
1444
1750
  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."
1445
1751
  };
1752
+ modelListing = {
1753
+ args: ["models"],
1754
+ level: "official",
1755
+ note: "Prints bare ids with the default marked; verified with 1.0.41."
1756
+ };
1446
1757
  mcpConfigs = [{
1447
1758
  path: "~/.grok/config.toml",
1448
1759
  scope: "user",
@@ -1467,7 +1778,12 @@ var Grok = class extends Harness {
1467
1778
  ],
1468
1779
  projectMarkers: [".grok"]
1469
1780
  };
1781
+ parseModelListingOutput(stdout) {
1782
+ return parseGrokModels(stdout);
1783
+ }
1470
1784
  };
1785
+ //#endregion
1786
+ //#region src/harnesses/mastracode.ts
1471
1787
  function packageVersion(commandPath) {
1472
1788
  try {
1473
1789
  const require = createRequire(realpathSync(commandPath));
@@ -1623,6 +1939,11 @@ var MastraCode = class extends Harness {
1623
1939
  note: "Claude Code compatible."
1624
1940
  }
1625
1941
  ];
1942
+ skillsSyncTarget = {
1943
+ path: "~/.mastracode/skills/",
1944
+ scope: "user",
1945
+ level: "official"
1946
+ };
1626
1947
  commands = [
1627
1948
  {
1628
1949
  path: ".mastracode/commands/",
@@ -1713,6 +2034,8 @@ var MastraCode = class extends Harness {
1713
2034
  projectMarkers: [".mastracode"]
1714
2035
  };
1715
2036
  };
2037
+ //#endregion
2038
+ //#region src/harnesses/omp.ts
1716
2039
  var Omp = class extends Harness {
1717
2040
  id = "omp";
1718
2041
  name = "OMP (oh-my-pi)";
@@ -1730,7 +2053,7 @@ var Omp = class extends Harness {
1730
2053
  path: "~/.omp/agent/config.yml",
1731
2054
  scope: "user",
1732
2055
  level: "official",
1733
- note: "Agent dir overridable with PI_CODING_AGENT_DIR; profiles use ~/.omp/profiles/<name>/agent."
2056
+ note: "Profiles use ~/.omp/profiles/<name>/agent."
1734
2057
  },
1735
2058
  {
1736
2059
  path: "~/.omp/agent/settings.json",
@@ -1852,6 +2175,11 @@ var Omp = class extends Harness {
1852
2175
  note: "Auto-generated managed skills."
1853
2176
  }
1854
2177
  ];
2178
+ skillsSyncTarget = {
2179
+ path: "~/.omp/agent/skills/",
2180
+ scope: "user",
2181
+ level: "official"
2182
+ };
1855
2183
  commands = [{
1856
2184
  path: ".omp/commands/",
1857
2185
  scope: "project",
@@ -1912,12 +2240,29 @@ var Omp = class extends Harness {
1912
2240
  key: ["mcpServers"],
1913
2241
  dialect: "standard"
1914
2242
  }];
2243
+ envOverrides = [{
2244
+ variable: "PI_CODING_AGENT_DIR",
2245
+ path: "~/.omp/agent",
2246
+ scope: "user",
2247
+ level: "official",
2248
+ relocates: [
2249
+ "config",
2250
+ "sessions",
2251
+ "instructions",
2252
+ "skills",
2253
+ "commands",
2254
+ "promptTemplates"
2255
+ ],
2256
+ 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."
2257
+ }];
1915
2258
  agentsFile = "~/.omp/agent/AGENTS.md";
1916
2259
  detection = {
1917
2260
  envVars: ["OMP_PROFILE", "PI_CODING_AGENT_DIR"],
1918
2261
  projectMarkers: [".omp"]
1919
2262
  };
1920
2263
  };
2264
+ //#endregion
2265
+ //#region src/harnesses/opencode.ts
1921
2266
  var OpenCode = class extends Harness {
1922
2267
  id = "opencode";
1923
2268
  name = "OpenCode CLI";
@@ -2014,6 +2359,11 @@ var OpenCode = class extends Harness {
2014
2359
  scope: "project",
2015
2360
  level: "official"
2016
2361
  },
2362
+ {
2363
+ path: "~/.config/opencode/skills/",
2364
+ scope: "user",
2365
+ level: "official"
2366
+ },
2017
2367
  {
2018
2368
  path: "~/.claude/skills/",
2019
2369
  scope: "user",
@@ -2026,6 +2376,12 @@ var OpenCode = class extends Harness {
2026
2376
  level: "official"
2027
2377
  }
2028
2378
  ];
2379
+ skillsSyncTarget = {
2380
+ path: "~/.config/opencode/skills/",
2381
+ scope: "user",
2382
+ level: "official",
2383
+ note: "OpenCode also scans ~/.claude/skills/ and ~/.agents/skills/; skill names must be unique across them."
2384
+ };
2029
2385
  commands = [{
2030
2386
  path: ".opencode/commands/",
2031
2387
  scope: "project",
@@ -2081,6 +2437,8 @@ var OpenCode = class extends Harness {
2081
2437
  ]
2082
2438
  };
2083
2439
  };
2440
+ //#endregion
2441
+ //#region src/harnesses/pi.ts
2084
2442
  const MODEL_HEADERS = [
2085
2443
  "provider",
2086
2444
  "model",
@@ -2136,8 +2494,7 @@ var Pi = class extends Harness {
2136
2494
  config = [{
2137
2495
  path: "~/.pi/agent/settings.json",
2138
2496
  scope: "user",
2139
- level: "official",
2140
- note: "Config dir overridable with PI_CODING_AGENT_DIR."
2497
+ level: "official"
2141
2498
  }, {
2142
2499
  path: ".pi/settings.json",
2143
2500
  scope: "project",
@@ -2147,7 +2504,7 @@ var Pi = class extends Harness {
2147
2504
  path: "~/.pi/agent/sessions/<dash-encoded-cwd>/<timestamp>_<session-id>.jsonl",
2148
2505
  scope: "data",
2149
2506
  level: "official",
2150
- note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with PI_CODING_AGENT_SESSION_DIR or --session-dir."
2507
+ note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with --session-dir."
2151
2508
  }];
2152
2509
  persistence = [{
2153
2510
  format: "JSON",
@@ -2194,6 +2551,11 @@ var Pi = class extends Harness {
2194
2551
  level: "official",
2195
2552
  note: "Global user-level skills."
2196
2553
  }];
2554
+ skillsSyncTarget = {
2555
+ path: "~/.pi/agent/skills/",
2556
+ scope: "user",
2557
+ level: "official"
2558
+ };
2197
2559
  commands = [{
2198
2560
  path: ".pi/prompts/",
2199
2561
  scope: "project",
@@ -2257,6 +2619,28 @@ var Pi = class extends Harness {
2257
2619
  level: "official",
2258
2620
  note: "Lists models with configured provider authentication; accepts an optional fuzzy search."
2259
2621
  };
2622
+ envOverrides = [{
2623
+ variable: "PI_CODING_AGENT_DIR",
2624
+ path: "~/.pi/agent",
2625
+ scope: "user",
2626
+ level: "official",
2627
+ relocates: [
2628
+ "config",
2629
+ "sessions",
2630
+ "instructions",
2631
+ "skills",
2632
+ "commands",
2633
+ "promptTemplates"
2634
+ ],
2635
+ note: "A leading ~ in the value is expanded."
2636
+ }, {
2637
+ variable: "PI_CODING_AGENT_SESSION_DIR",
2638
+ path: "~/.pi/agent/sessions",
2639
+ scope: "data",
2640
+ level: "official",
2641
+ relocates: ["sessions"],
2642
+ note: "Takes precedence over PI_CODING_AGENT_DIR for sessions; --session-dir beats both."
2643
+ }];
2260
2644
  agentsFile = "~/.pi/agent/AGENTS.md";
2261
2645
  detection = {
2262
2646
  envVars: [
@@ -2270,6 +2654,8 @@ var Pi = class extends Harness {
2270
2654
  return parsePiModelTable(stdout, "Pi");
2271
2655
  }
2272
2656
  };
2657
+ //#endregion
2658
+ //#region src/harnesses/prime-agent.ts
2273
2659
  var PrimeAgent = class extends Harness {
2274
2660
  id = "prime-agent";
2275
2661
  name = "Prime Agent";
@@ -2371,6 +2757,11 @@ var PrimeAgent = class extends Harness {
2371
2757
  note: "Shared skills directory; root .md files are ignored there."
2372
2758
  }
2373
2759
  ];
2760
+ skillsSyncTarget = {
2761
+ path: "~/.prime/agent/skills/",
2762
+ scope: "user",
2763
+ level: "official"
2764
+ };
2374
2765
  commands = [{
2375
2766
  path: ".prime/agent/prompts/",
2376
2767
  scope: "project",
@@ -2442,6 +2833,8 @@ var PrimeAgent = class extends Harness {
2442
2833
  return parsePiModelTable(stdout, "Prime Agent");
2443
2834
  }
2444
2835
  };
2836
+ //#endregion
2837
+ //#region src/harnesses/index.ts
2445
2838
  const harnesses = [
2446
2839
  Antigravity,
2447
2840
  Claude,
@@ -2457,6 +2850,8 @@ const harnesses = [
2457
2850
  PrimeAgent,
2458
2851
  Pi
2459
2852
  ];
2853
+ //#endregion
2854
+ //#region src/registry.ts
2460
2855
  let registry;
2461
2856
  function getRegistry() {
2462
2857
  if (!registry) {
@@ -2504,6 +2899,8 @@ function detectHarness(cwd) {
2504
2899
  }
2505
2900
  return match;
2506
2901
  }
2902
+ //#endregion
2903
+ //#region src/mcp-servers.ts
2507
2904
  function resolveConfigs(harness, options) {
2508
2905
  return harness.resolveCandidates(harness.mcpConfigs, options).map((entry) => ({
2509
2906
  entry,
@@ -3147,6 +3544,8 @@ function syncMcpServers(harnesses, options = {}) {
3147
3544
  targets: harnesses.map((harness) => syncMcpTarget(harness, master, masterNames, options))
3148
3545
  };
3149
3546
  }
3547
+ //#endregion
3548
+ //#region src/agents-sync.ts
3150
3549
  function readIfExists(path) {
3151
3550
  try {
3152
3551
  return readFileSync(path, "utf8");
@@ -3235,6 +3634,14 @@ function moveFile(from, to) {
3235
3634
  renameSync(from, to);
3236
3635
  } catch (error) {
3237
3636
  if (error.code !== "EXDEV") throw error;
3637
+ if (lstatSync(from).isDirectory()) {
3638
+ cpSync(from, to, {
3639
+ recursive: true,
3640
+ verbatimSymlinks: true
3641
+ });
3642
+ rmSync(from, { recursive: true });
3643
+ return;
3644
+ }
3238
3645
  copyFileSync(from, to);
3239
3646
  unlinkSync(from);
3240
3647
  }
@@ -3286,7 +3693,7 @@ function syncFileTarget(id, path, source, check, options) {
3286
3693
  };
3287
3694
  return applyFileTarget(id, path, source, state, options);
3288
3695
  }
3289
- function syncTarget$1(harness, config, check, options) {
3696
+ function syncTarget$2(harness, config, check, options) {
3290
3697
  if (config.excludes.includes(harness.id)) return {
3291
3698
  id: harness.id,
3292
3699
  action: "skipped",
@@ -3337,23 +3744,144 @@ function syncAgentsFiles(harnesses, check = false, options = {}) {
3337
3744
  return {
3338
3745
  source: config.source,
3339
3746
  check,
3340
- targets: harnesses.map((harness) => syncTarget$1(harness, config, check, options))
3747
+ targets: harnesses.map((harness) => syncTarget$2(harness, config, check, options))
3341
3748
  };
3342
3749
  }
3750
+ //#endregion
3751
+ //#region src/directory-link.ts
3752
+ const CHECK_DETAIL = "check mode: not applied";
3753
+ function linkTarget(path) {
3754
+ const target = readlinkSync(path);
3755
+ return isAbsolute(target) ? target : resolve(dirname(path), target);
3756
+ }
3757
+ function isInsideDirectory(path, directory) {
3758
+ const remainder = relative(directory, path);
3759
+ return remainder !== "" && !remainder.startsWith("..") && !isAbsolute(remainder);
3760
+ }
3761
+ function tempPath(path, kind) {
3762
+ return join(dirname(path), `.${basename(path)}-${randomUUID()}.${kind}.tmp`);
3763
+ }
3764
+ function realDirectory(path) {
3765
+ try {
3766
+ return realpathSync(path);
3767
+ } catch {
3768
+ return resolve(path);
3769
+ }
3770
+ }
3771
+ function backupDestination(kind, id, path, options) {
3772
+ const backupDir = join(agntnDataDir(options), "diverged", kind, id);
3773
+ mkdirSync(backupDir, { recursive: true });
3774
+ const backup = join(backupDir, `${basename(path)}-${Date.now()}-${randomUUID()}`);
3775
+ if (lstatSync(path).isSymbolicLink()) {
3776
+ symlinkSync(linkTarget(path), backup);
3777
+ unlinkSync(path);
3778
+ return backup;
3779
+ }
3780
+ moveFile(path, backup);
3781
+ return backup;
3782
+ }
3783
+ function linkDirectory(path, source, kind) {
3784
+ mkdirSync(dirname(path), { recursive: true });
3785
+ const temp = tempPath(path, `${kind}-link`);
3786
+ symlinkSync(source, temp, "junction");
3787
+ renameSync(temp, path);
3788
+ }
3789
+ const DIRECTORY_ACTION = {
3790
+ missing: "linked",
3791
+ "correct-link": "unchanged",
3792
+ "wrong-link": "relinked",
3793
+ "legacy-links": "relinked",
3794
+ "local-files": "adopted",
3795
+ "not-directory": "skipped",
3796
+ source: "skipped"
3797
+ };
3798
+ const SKIP_DETAIL = {
3799
+ "not-directory": "destination exists and is not a directory or symlink",
3800
+ source: "destination holds the source directory"
3801
+ };
3802
+ function isEntryLink(path, sourceDirs) {
3803
+ if (!lstatSync(path).isSymbolicLink()) return false;
3804
+ const target = linkTarget(path);
3805
+ return sourceDirs.some((directory) => isInsideDirectory(target, directory));
3806
+ }
3807
+ function linksThrough(sourceDir, targetDir) {
3808
+ let path = sourceDir;
3809
+ for (let hop = 0; hop < 32; hop++) {
3810
+ if (path === targetDir || isInsideDirectory(path, targetDir)) return true;
3811
+ try {
3812
+ if (!lstatSync(path).isSymbolicLink()) return false;
3813
+ } catch {
3814
+ return false;
3815
+ }
3816
+ path = linkTarget(path);
3817
+ }
3818
+ return false;
3819
+ }
3820
+ function inspectDirectory(targetDir, sourceDir) {
3821
+ let stats;
3822
+ try {
3823
+ stats = lstatSync(targetDir);
3824
+ } catch (error) {
3825
+ if (error.code === "ENOENT") return { kind: "missing" };
3826
+ throw error;
3827
+ }
3828
+ if (stats.isSymbolicLink()) {
3829
+ if (linkTarget(targetDir) === sourceDir) return { kind: "correct-link" };
3830
+ return { kind: linksThrough(sourceDir, targetDir) ? "source" : "wrong-link" };
3831
+ }
3832
+ if (!stats.isDirectory()) return { kind: "not-directory" };
3833
+ return inspectLocalDirectory(targetDir, sourceDir);
3834
+ }
3835
+ function inspectLocalDirectory(targetDir, sourceDir) {
3836
+ const realSource = realDirectory(sourceDir);
3837
+ const realTarget = realpathSync(targetDir);
3838
+ if (realSource === realTarget || isInsideDirectory(realSource, realTarget)) return { kind: "source" };
3839
+ const sourceDirs = realSource === sourceDir ? [sourceDir] : [sourceDir, realSource];
3840
+ const entries = readdirSync(targetDir).map((entry) => join(targetDir, entry));
3841
+ return entries.every((entry) => isEntryLink(entry, sourceDirs)) ? {
3842
+ kind: "legacy-links",
3843
+ entries
3844
+ } : { kind: "local-files" };
3845
+ }
3846
+ function clearDirectory(kind, id, targetDir, state, options) {
3847
+ if (state.kind === "wrong-link" || state.kind === "local-files") return backupDestination(kind, id, targetDir, options);
3848
+ if (state.kind === "legacy-links") {
3849
+ for (const entry of state.entries) unlinkSync(entry);
3850
+ rmdirSync(targetDir);
3851
+ }
3852
+ }
3853
+ function syncDirectoryLink(kind, id, targetDir, sourceDir, check, options) {
3854
+ const state = inspectDirectory(targetDir, sourceDir);
3855
+ const action = DIRECTORY_ACTION[state.kind];
3856
+ if (state.kind === "correct-link") return { action };
3857
+ const skip = SKIP_DETAIL[state.kind];
3858
+ if (skip !== void 0) return {
3859
+ action,
3860
+ detail: skip
3861
+ };
3862
+ if (check) return {
3863
+ action,
3864
+ detail: CHECK_DETAIL
3865
+ };
3866
+ const detail = clearDirectory(kind, id, targetDir, state, options);
3867
+ linkDirectory(targetDir, sourceDir, kind);
3868
+ return {
3869
+ action,
3870
+ detail
3871
+ };
3872
+ }
3873
+ //#endregion
3874
+ //#region src/prompt-sync.ts
3343
3875
  const GENERATED_MARKER = "# Generated by @agntn/harnesses from ";
3344
3876
  const PORTABLE_PROMPT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
3345
3877
  const GEMINI_ACTION = {
3346
3878
  missing: "generated",
3347
- "correct-link": "adopted",
3348
- "wrong-link": "adopted",
3349
3879
  "owned-file": "replaced",
3350
3880
  "unmanaged-file": "adopted",
3351
3881
  unsupported: "skipped"
3352
3882
  };
3353
3883
  const GEMINI_BACKUP = {
3354
3884
  missing: false,
3355
- "correct-link": true,
3356
- "wrong-link": true,
3357
3885
  "owned-file": true,
3358
3886
  "unmanaged-file": true,
3359
3887
  unsupported: false
@@ -3391,7 +3919,7 @@ function generatedSource(content) {
3391
3919
  return null;
3392
3920
  }
3393
3921
  }
3394
- function inspectDestination(path, source) {
3922
+ function inspectDestination(path) {
3395
3923
  let stats;
3396
3924
  try {
3397
3925
  stats = lstatSync(path);
@@ -3399,11 +3927,7 @@ function inspectDestination(path, source) {
3399
3927
  if (error.code === "ENOENT") return { kind: "missing" };
3400
3928
  throw error;
3401
3929
  }
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
- }
3930
+ if (stats.isSymbolicLink()) return { kind: "unmanaged-file" };
3407
3931
  if (!stats.isFile()) return { kind: "unsupported" };
3408
3932
  const content = readFileSync(path, "utf8");
3409
3933
  if (generatedSource(content) !== null) return {
@@ -3412,75 +3936,17 @@ function inspectDestination(path, source) {
3412
3936
  };
3413
3937
  return { kind: "unmanaged-file" };
3414
3938
  }
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
3939
  function writeTemplate(path, content) {
3425
3940
  mkdirSync(dirname(path), { recursive: true });
3426
3941
  const temp = tempPath(path, "prompt-write");
3427
3942
  writeFileSync(temp, content);
3428
3943
  renameSync(temp, path);
3429
3944
  }
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
- };
3945
+ function parseYaml(raw) {
3946
+ return createRequire(import.meta.url)("yaml").parse(raw);
3481
3947
  }
3482
3948
  function frontmatterDescription(raw, path) {
3483
- const frontmatter = parse$1(raw);
3949
+ const frontmatter = parseYaml(raw);
3484
3950
  if (frontmatter === null) return void 0;
3485
3951
  if (typeof frontmatter !== "object" || Array.isArray(frontmatter)) throw new TypeError(`Prompt template frontmatter is not an object: ${path}`);
3486
3952
  const description = frontmatter.description;
@@ -3528,10 +3994,10 @@ function syncGeminiTemplate(id, targetDir, template, check, options) {
3528
3994
  source: template.path,
3529
3995
  path,
3530
3996
  action,
3531
- detail: checkDetail()
3997
+ detail: CHECK_DETAIL
3532
3998
  };
3533
3999
  let detail;
3534
- if (GEMINI_BACKUP[state.kind]) detail = backupDestination(id, path, options);
4000
+ if (GEMINI_BACKUP[state.kind]) detail = backupDestination("prompts", id, path, options);
3535
4001
  writeTemplate(path, content);
3536
4002
  return {
3537
4003
  name: template.name,
@@ -3541,23 +4007,6 @@ function syncGeminiTemplate(id, targetDir, template, check, options) {
3541
4007
  detail
3542
4008
  };
3543
4009
  }
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
4010
  function staleGeminiSource(path, sourceDir) {
3562
4011
  let content;
3563
4012
  try {
@@ -3569,31 +4018,43 @@ function staleGeminiSource(path, sourceDir) {
3569
4018
  if (source === null) return null;
3570
4019
  return isInsideDirectory(source, sourceDir) ? source : null;
3571
4020
  }
3572
- function pruneTarget(targetDir, format, sourceDir, activeSources, check) {
4021
+ function pruneGeminiTarget(id, targetDir, sourceDir, activeNames, check, options) {
3573
4022
  let entries;
3574
4023
  try {
3575
- entries = readdirSync(targetDir, { withFileTypes: true });
4024
+ entries = readdirSync(targetDir);
3576
4025
  } catch (error) {
3577
4026
  if (error.code === "ENOENT") return [];
3578
4027
  throw error;
3579
4028
  }
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,
4029
+ return entries.filter((entry) => !activeNames.has(entry)).map((entry) => {
4030
+ const path = join(targetDir, entry);
4031
+ const name = basename(entry, extname(entry));
4032
+ const source = staleGeminiSource(path, sourceDir);
4033
+ if (check) return {
4034
+ name,
4035
+ ...source === null ? {} : { source },
3590
4036
  path,
3591
4037
  action: "removed",
3592
- ...check ? { detail: checkDetail() } : {}
3593
- }];
4038
+ detail: CHECK_DETAIL
4039
+ };
4040
+ if (source !== null) {
4041
+ unlinkSync(path);
4042
+ return {
4043
+ name,
4044
+ source,
4045
+ path,
4046
+ action: "removed"
4047
+ };
4048
+ }
4049
+ return {
4050
+ name,
4051
+ path,
4052
+ action: "removed",
4053
+ detail: backupDestination("prompts", id, path, options)
4054
+ };
3594
4055
  });
3595
4056
  }
3596
- function syncTarget(harness, templates, sourceDir, check, options) {
4057
+ function syncTarget$1(harness, templates, sourceDir, check, options) {
3597
4058
  const target = harness.promptTemplateSyncTarget;
3598
4059
  if (target === null) return {
3599
4060
  id: harness.id,
@@ -3606,11 +4067,17 @@ function syncTarget(harness, templates, sourceDir, check, options) {
3606
4067
  detail: "prompt template destination is unavailable on this platform",
3607
4068
  templates: []
3608
4069
  };
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));
4070
+ const targetDir = resolve(resolved.path);
4071
+ if (resolved.format === "markdown") return {
4072
+ id: harness.id,
4073
+ path: targetDir,
4074
+ format: resolved.format,
4075
+ ...syncDirectoryLink("prompts", harness.id, targetDir, sourceDir, check, options),
4076
+ templates: []
4077
+ };
4078
+ const results = templates.map((template) => syncGeminiTemplate(harness.id, targetDir, template, check, options));
4079
+ const activeNames = new Set(templates.map((template) => `${template.name}.toml`));
4080
+ results.push(...pruneGeminiTarget(harness.id, targetDir, sourceDir, activeNames, check, options));
3614
4081
  return {
3615
4082
  id: harness.id,
3616
4083
  path: targetDir,
@@ -3629,9 +4096,59 @@ function syncPromptTemplates(harnesses, check = false, options = {}) {
3629
4096
  source,
3630
4097
  check,
3631
4098
  templates: prepared.map((template) => template.name),
3632
- targets: harnesses.map((harness) => syncTarget(harness, prepared, source, check, options))
4099
+ targets: harnesses.map((harness) => syncTarget$1(harness, prepared, source, check, options))
4100
+ };
4101
+ }
4102
+ //#endregion
4103
+ //#region src/skills-sync.ts
4104
+ function readSkills(sourceDir, check) {
4105
+ let entries;
4106
+ try {
4107
+ entries = readdirSync(sourceDir);
4108
+ } catch (error) {
4109
+ if (error.code !== "ENOENT") throw error;
4110
+ if (!check) mkdirSync(sourceDir, { recursive: true });
4111
+ return [];
4112
+ }
4113
+ return entries.filter((entry) => {
4114
+ const path = join(sourceDir, entry);
4115
+ try {
4116
+ return statSync(path).isDirectory() && existsSync(join(path, "SKILL.md"));
4117
+ } catch {
4118
+ return false;
4119
+ }
4120
+ }).sort((left, right) => left.localeCompare(right));
4121
+ }
4122
+ function syncTarget(harness, sourceDir, check, options) {
4123
+ const target = harness.skillsSyncTarget;
4124
+ if (target === null) return {
4125
+ id: harness.id,
4126
+ action: "skipped",
4127
+ detail: "no stable user-scope skills directory"
4128
+ };
4129
+ const resolved = harness.resolveCandidates([target], options)[0];
4130
+ if (resolved === void 0) return {
4131
+ id: harness.id,
4132
+ action: "skipped",
4133
+ detail: "skills directory is unavailable on this platform"
4134
+ };
4135
+ const targetDir = resolve(resolved.path);
4136
+ return {
4137
+ id: harness.id,
4138
+ path: targetDir,
4139
+ ...syncDirectoryLink("skills", harness.id, targetDir, sourceDir, check, options)
4140
+ };
4141
+ }
4142
+ function syncSkills(harnesses, check = false, options = {}) {
4143
+ const source = join(agntnDataDir(options), "skills");
4144
+ return {
4145
+ source,
4146
+ check,
4147
+ skills: readSkills(source, check),
4148
+ targets: harnesses.map((harness) => syncTarget(harness, source, check, options))
3633
4149
  };
3634
4150
  }
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 };
4151
+ //#endregion
4152
+ export { agntnDataDir as C, agntnConfigDir as S, getHarness as _, McpEnvError as a, registerHarness as b, masterMcpPath as c, removeMcpServer as d, syncMcpServers as f, getAllHarnesses as g, detectProjectHarnesses as h, syncAgentsFiles as i, parseJsonc as l, detectHarnessFromEnv as m, syncPromptTemplates as n, addMcpServer as o, detectHarness as p, readAgentsConfig as r, listMcpServers as s, syncSkills as t, readMasterMcpServers as u, isHarnessId as v, resolvePathTemplate as w, Harness as x, listHarnesses as y };
3636
4153
 
3637
- //# sourceMappingURL=prompt-sync.mjs.map
4154
+ //# sourceMappingURL=skills-sync.mjs.map