@agntn/harnesses 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/_chunks/libs/typebox.mjs +1 -1
- package/dist/_chunks/libs/typebox.mjs.map +1 -1
- package/dist/_chunks/prompt-sync.d.mts +73 -9
- package/dist/_chunks/prompt-sync.d.mts.map +1 -1
- package/dist/_chunks/prompt-sync.mjs +427 -110
- package/dist/_chunks/prompt-sync.mjs.map +1 -1
- package/dist/_chunks/tool-operations.mjs +16 -11
- package/dist/_chunks/tool-operations.mjs.map +1 -1
- package/dist/_chunks/tool-schemas.d.mts +1 -0
- package/dist/_chunks/tool-schemas.d.mts.map +1 -1
- package/dist/_chunks/tool-schemas.mjs +11 -10
- package/dist/_chunks/tool-schemas.mjs.map +1 -1
- package/dist/_chunks/types.mjs +1 -1
- package/dist/_chunks/types.mjs.map +1 -1
- package/dist/cli.mjs +20 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.d.mts.map +1 -1
- package/dist/mcp.mjs +6 -3
- package/dist/mcp.mjs.map +1 -1
- package/dist/tool-operations.d.mts +8 -2
- package/dist/tool-operations.d.mts.map +1 -1
- package/package.json +1 -1
- package/packages/omp/extensions/harnesses.ts +1 -1
- package/packages/pi/extensions/harnesses.ts +1 -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,13 @@ 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
|
-
|
|
12
|
-
const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|%([^%]+)%/g;
|
|
11
|
+
const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|\$\{TMPDIR\}|%([^%]+)%/g;
|
|
13
12
|
function resolvePathTemplate(template, options = {}) {
|
|
14
13
|
const homeDir = options.homeDir ?? os.homedir();
|
|
15
14
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
16
15
|
return template.replaceAll(PATH_MARKERS, (match, name) => {
|
|
17
16
|
if (match === "${PROJECT_ROOT}") return projectRoot;
|
|
17
|
+
if (match === "${TMPDIR}") return options.tempDir ?? os.tmpdir();
|
|
18
18
|
if (name === void 0) return homeDir;
|
|
19
19
|
return process.env[name] ?? match;
|
|
20
20
|
});
|
|
@@ -27,6 +27,11 @@ function agntnDataDir(options = {}) {
|
|
|
27
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
28
|
return join(base, "agntn");
|
|
29
29
|
}
|
|
30
|
+
function moveRoot(path, root, value) {
|
|
31
|
+
if (path === root) return value;
|
|
32
|
+
const rest = path.slice(root.length);
|
|
33
|
+
return path.startsWith(root) && (rest.startsWith("/") || rest.startsWith("\\")) ? value + rest : path;
|
|
34
|
+
}
|
|
30
35
|
const SUPPORTED_PLATFORMS = {
|
|
31
36
|
linux: true,
|
|
32
37
|
darwin: true,
|
|
@@ -57,6 +62,17 @@ function requestedInvocationMode(options) {
|
|
|
57
62
|
if (options.tools === true) return options.structured === true ? "agentStructured" : "agent";
|
|
58
63
|
return options.structured === true ? "advisorStructured" : "advisor";
|
|
59
64
|
}
|
|
65
|
+
function accessConflictError(id, invocation, options) {
|
|
66
|
+
if (options.readOnly !== true || options.tools !== false) return null;
|
|
67
|
+
const retries = [];
|
|
68
|
+
if (invocationTemplate(invocation, requestedInvocationMode(options))) retries.push("tools: true to keep its read-only tools");
|
|
69
|
+
if (invocationTemplate(invocation, requestedInvocationMode({
|
|
70
|
+
...options,
|
|
71
|
+
readOnly: false
|
|
72
|
+
}))) retries.push("readOnly: false to use its advisor without tools");
|
|
73
|
+
const error = `Harness ${id} cannot run readOnly with tools: false, since read-only access still uses tools`;
|
|
74
|
+
return retries.length === 0 ? error : `${error}; retry with ${retries.join(", or with ")}`;
|
|
75
|
+
}
|
|
60
76
|
function invocationTemplate(invocation, mode) {
|
|
61
77
|
switch (mode) {
|
|
62
78
|
case "advisor": return invocation.noToolsArgs;
|
|
@@ -123,11 +139,29 @@ async function terminateCommand(pid) {
|
|
|
123
139
|
await setTimeout$1(500);
|
|
124
140
|
signalProcessGroup(pid, "SIGKILL");
|
|
125
141
|
}
|
|
142
|
+
function spawnError(command, cwd, error) {
|
|
143
|
+
if (!(error instanceof Error && "code" in error)) return error;
|
|
144
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTDIR") return error;
|
|
145
|
+
let message;
|
|
146
|
+
if (cwd !== void 0 && !isDirectory(cwd)) message = existsSync(cwd) ? `Working directory is not a directory: ${cwd}` : `Working directory not found: ${cwd}`;
|
|
147
|
+
else if (error.code === "ENOENT") message = `Command not found: ${command}. Install it or add it to PATH`;
|
|
148
|
+
else return error;
|
|
149
|
+
return Object.assign(new Error(message, { cause: error }), { code: error.code });
|
|
150
|
+
}
|
|
151
|
+
function isDirectory(path) {
|
|
152
|
+
try {
|
|
153
|
+
return statSync(path).isDirectory();
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
126
158
|
function executeCommand(command, args, options) {
|
|
127
159
|
return new Promise((resolve, reject) => {
|
|
128
160
|
let stdout = "";
|
|
129
161
|
let stderr = "";
|
|
130
162
|
let stopped;
|
|
163
|
+
let lastOutputAt = performance.now();
|
|
164
|
+
let idleMs;
|
|
131
165
|
let settled = false;
|
|
132
166
|
let timer;
|
|
133
167
|
let abortListener;
|
|
@@ -136,28 +170,38 @@ function executeCommand(command, args, options) {
|
|
|
136
170
|
finish(null);
|
|
137
171
|
return;
|
|
138
172
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
let child;
|
|
174
|
+
try {
|
|
175
|
+
child = spawn(command, args, {
|
|
176
|
+
cwd: options.cwd,
|
|
177
|
+
env: options.env ? {
|
|
178
|
+
...process.env,
|
|
179
|
+
...options.env
|
|
180
|
+
} : process.env,
|
|
181
|
+
stdio: [
|
|
182
|
+
"ignore",
|
|
183
|
+
"pipe",
|
|
184
|
+
"pipe"
|
|
185
|
+
],
|
|
186
|
+
detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
fail(spawnError(command, options.cwd, error));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
152
192
|
if (options.timeoutMs) timer = setTimeout(() => stop("timeout"), options.timeoutMs);
|
|
153
193
|
if (options.signal) abortListener = addAbortListener(options.signal, () => stop("abort"));
|
|
194
|
+
child.stdout.setEncoding("utf8");
|
|
195
|
+
child.stderr.setEncoding("utf8");
|
|
154
196
|
child.stdout.on("data", (chunk) => {
|
|
155
|
-
stdout += chunk
|
|
197
|
+
stdout += chunk;
|
|
198
|
+
lastOutputAt = performance.now();
|
|
156
199
|
});
|
|
157
200
|
child.stderr.on("data", (chunk) => {
|
|
158
|
-
stderr += chunk
|
|
201
|
+
stderr += chunk;
|
|
202
|
+
lastOutputAt = performance.now();
|
|
159
203
|
});
|
|
160
|
-
child.on("error", fail);
|
|
204
|
+
child.on("error", (error) => fail(spawnError(command, options.cwd, error)));
|
|
161
205
|
child.on("close", (code) => {
|
|
162
206
|
if (stopped === void 0) finish(code);
|
|
163
207
|
});
|
|
@@ -174,6 +218,7 @@ function executeCommand(command, args, options) {
|
|
|
174
218
|
function stop(reason) {
|
|
175
219
|
if (settled || stopped !== void 0) return;
|
|
176
220
|
stopped = reason;
|
|
221
|
+
idleMs = Math.round(performance.now() - lastOutputAt);
|
|
177
222
|
cleanup();
|
|
178
223
|
if (child.pid === void 0) return;
|
|
179
224
|
terminateCommand(child.pid).then(() => {
|
|
@@ -197,7 +242,8 @@ function executeCommand(command, args, options) {
|
|
|
197
242
|
stderr,
|
|
198
243
|
exitCode: stopped === void 0 ? code : null,
|
|
199
244
|
timedOut: stopped === "timeout",
|
|
200
|
-
aborted: stopped === "abort"
|
|
245
|
+
aborted: stopped === "abort",
|
|
246
|
+
...idleMs === void 0 ? {} : { idleMs }
|
|
201
247
|
});
|
|
202
248
|
}
|
|
203
249
|
});
|
|
@@ -205,6 +251,8 @@ function executeCommand(command, args, options) {
|
|
|
205
251
|
var Harness = class {
|
|
206
252
|
promptTemplates = [];
|
|
207
253
|
promptTemplateSyncTarget = null;
|
|
254
|
+
temp = [];
|
|
255
|
+
envOverrides = [];
|
|
208
256
|
modelListing = null;
|
|
209
257
|
mcpConfigs = [];
|
|
210
258
|
agentsFile = null;
|
|
@@ -258,6 +306,7 @@ var Harness = class {
|
|
|
258
306
|
const command = this.invocation.binary ?? this.binaries[0];
|
|
259
307
|
if (!command) return null;
|
|
260
308
|
if (options.model !== void 0 && !this.invocation.modelArgs) return null;
|
|
309
|
+
if (accessConflictError(this.id, this.invocation, options) !== null) return null;
|
|
261
310
|
const template = invocationTemplate(this.invocation, requestedInvocationMode(options));
|
|
262
311
|
if (!template) return null;
|
|
263
312
|
return {
|
|
@@ -268,6 +317,8 @@ var Harness = class {
|
|
|
268
317
|
invocationError(options = {}) {
|
|
269
318
|
if (!this.invocation) return `Harness ${this.id} has no non-interactive invocation`;
|
|
270
319
|
if (options.model !== void 0 && !this.invocation.modelArgs) return `Harness ${this.id} does not support model selection`;
|
|
320
|
+
const conflict = accessConflictError(this.id, this.invocation, options);
|
|
321
|
+
if (conflict !== null) return conflict;
|
|
271
322
|
const mode = requestedInvocationMode(options);
|
|
272
323
|
if (invocationTemplate(this.invocation, mode)) return null;
|
|
273
324
|
return `${`Harness ${this.id} has no ${INVOCATION_MODE_DESCRIPTION[mode]} invocation`}${invocationRetryHint(this.invocation, mode, options)}`;
|
|
@@ -283,7 +334,15 @@ var Harness = class {
|
|
|
283
334
|
if (!built) return Promise.reject(new Error(this.invocationError(invocationOptions) ?? "Invalid invocation"));
|
|
284
335
|
const versionError = this.readOnlyVersionError(invocationOptions);
|
|
285
336
|
if (versionError) return Promise.reject(new Error(versionError));
|
|
286
|
-
|
|
337
|
+
const streamArgs = options.structured ? void 0 : this.invocation?.streamArgs;
|
|
338
|
+
if (!streamArgs) return executeCommand(built.command, built.args, options);
|
|
339
|
+
return executeCommand(built.command, [...built.args, ...streamArgs], options).then((result) => ({
|
|
340
|
+
...result,
|
|
341
|
+
stdout: this.foldStreamOutput(result.stdout, !result.timedOut && !result.aborted)
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
foldStreamOutput(_stdout, _complete) {
|
|
345
|
+
throw new Error(`Harness ${this.id} does not implement stream output folding`);
|
|
287
346
|
}
|
|
288
347
|
readOnlyVersionError(options) {
|
|
289
348
|
const floor = this.invocation?.readOnlyMinVersion;
|
|
@@ -298,22 +357,33 @@ var Harness = class {
|
|
|
298
357
|
if (!this.modelListing) return null;
|
|
299
358
|
const command = this.binaries[0];
|
|
300
359
|
if (!command) return null;
|
|
301
|
-
const template = search === void 0 ? this.modelListing.args : this.modelListing.searchArgs;
|
|
302
|
-
if (!template) return null;
|
|
303
360
|
return {
|
|
304
361
|
command,
|
|
305
|
-
args:
|
|
362
|
+
args: (search === void 0 ? this.modelListing.args : this.modelListing.searchArgs ?? this.modelListing.args).map((arg) => arg.replaceAll("{search}", () => search ?? ""))
|
|
306
363
|
};
|
|
307
364
|
}
|
|
308
365
|
async listModels(options = {}) {
|
|
309
366
|
const built = this.buildModelListInvocation(options.search);
|
|
310
|
-
if (!built) throw new Error(
|
|
367
|
+
if (!built) throw new Error(`Harness ${this.id} does not support model listing`);
|
|
311
368
|
const result = await executeCommand(built.command, built.args, options);
|
|
369
|
+
if (result.timedOut || result.aborted || result.exitCode !== 0) return {
|
|
370
|
+
...result,
|
|
371
|
+
models: []
|
|
372
|
+
};
|
|
373
|
+
const models = this.parseModelListingOutput(result.stdout);
|
|
374
|
+
if (options.search === void 0 || this.modelListing?.searchArgs) return {
|
|
375
|
+
...result,
|
|
376
|
+
models
|
|
377
|
+
};
|
|
378
|
+
const search = options.search.toLowerCase();
|
|
312
379
|
return {
|
|
313
380
|
...result,
|
|
314
|
-
models:
|
|
381
|
+
models: models.filter((model) => model.id.toLowerCase().includes(search))
|
|
315
382
|
};
|
|
316
383
|
}
|
|
384
|
+
modelSelector(model) {
|
|
385
|
+
return `${model.provider}/${model.id}`;
|
|
386
|
+
}
|
|
317
387
|
parseModelListingOutput(_stdout) {
|
|
318
388
|
throw new Error(`Harness ${this.id} does not implement model-list output parsing`);
|
|
319
389
|
}
|
|
@@ -335,10 +405,33 @@ var Harness = class {
|
|
|
335
405
|
commands: this.resolveCandidates(this.commands, options),
|
|
336
406
|
promptTemplates: this.resolveCandidates(this.promptTemplates, options),
|
|
337
407
|
promptTemplateSyncTarget: this.promptTemplateSyncTarget === null ? null : this.resolveCandidates([this.promptTemplateSyncTarget], options)[0] ?? null,
|
|
338
|
-
hooks: this.resolveCandidates(this.hooks, options)
|
|
408
|
+
hooks: this.resolveCandidates(this.hooks, options),
|
|
409
|
+
temp: this.resolveTemp(options)
|
|
339
410
|
};
|
|
340
411
|
}
|
|
412
|
+
resolveTemp(options) {
|
|
413
|
+
const temp = this.resolveCandidates(this.temp, options);
|
|
414
|
+
const override = this.resolveCandidates(this.envOverrides, options).find((entry) => entry.relocates.includes("temp"));
|
|
415
|
+
const value = override ? process.env[override.variable] : void 0;
|
|
416
|
+
if (!override || !value) return temp;
|
|
417
|
+
return temp.map((entry) => ({
|
|
418
|
+
...entry,
|
|
419
|
+
path: moveRoot(entry.path, override.path, value)
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
341
422
|
};
|
|
423
|
+
function parseAntigravityModels(stdout) {
|
|
424
|
+
const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
425
|
+
if (lines.length === 0) throw new Error("Unexpected empty Antigravity model-list output");
|
|
426
|
+
return lines.map((line) => {
|
|
427
|
+
const match = /^(\S+)\t.+$/.exec(line);
|
|
428
|
+
if (!match?.[1]) throw new Error(`Unexpected Antigravity model-list row: ${JSON.stringify(line)}`);
|
|
429
|
+
return {
|
|
430
|
+
provider: "google",
|
|
431
|
+
id: match[1]
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
342
435
|
var Antigravity = class extends Harness {
|
|
343
436
|
id = "antigravity";
|
|
344
437
|
name = "Google Antigravity CLI";
|
|
@@ -482,6 +575,11 @@ var Antigravity = class extends Harness {
|
|
|
482
575
|
level: "official",
|
|
483
576
|
note: "Non-interactive print mode."
|
|
484
577
|
};
|
|
578
|
+
modelListing = {
|
|
579
|
+
args: ["models"],
|
|
580
|
+
level: "official",
|
|
581
|
+
note: "Prints one id and display name per row, with no default marked and no filter; verified with 1.2.5."
|
|
582
|
+
};
|
|
485
583
|
mcpConfigs = [{
|
|
486
584
|
path: "~/.gemini/config/mcp_config.json",
|
|
487
585
|
scope: "user",
|
|
@@ -495,7 +593,60 @@ var Antigravity = class extends Harness {
|
|
|
495
593
|
envVars: [],
|
|
496
594
|
projectMarkers: [".antigravitycli"]
|
|
497
595
|
};
|
|
596
|
+
parseModelListingOutput(stdout) {
|
|
597
|
+
return parseAntigravityModels(stdout);
|
|
598
|
+
}
|
|
599
|
+
modelSelector(model) {
|
|
600
|
+
return model.id;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
function parseStreamLine(line) {
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(line);
|
|
606
|
+
return typeof parsed === "object" && parsed !== null && "type" in parsed && typeof parsed.type === "string" ? parsed : void 0;
|
|
607
|
+
} catch {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function startsTextBlock(event) {
|
|
612
|
+
return event.type === "stream_event" && event.event?.type === "content_block_start" && event.event.content_block?.type === "text";
|
|
613
|
+
}
|
|
614
|
+
function textDelta(event) {
|
|
615
|
+
if (event.type !== "stream_event" || event.event?.type !== "content_block_delta") return void 0;
|
|
616
|
+
const delta = event.event.delta;
|
|
617
|
+
return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : void 0;
|
|
618
|
+
}
|
|
619
|
+
var ClaudeStreamFold = class {
|
|
620
|
+
blocks = [];
|
|
621
|
+
other = [];
|
|
622
|
+
result;
|
|
623
|
+
add(line) {
|
|
624
|
+
if (line.trim().length === 0) return;
|
|
625
|
+
const event = parseStreamLine(line);
|
|
626
|
+
if (!event) {
|
|
627
|
+
this.other.push(line);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (event.type === "result" && typeof event.result === "string") this.result = event.result;
|
|
631
|
+
if (startsTextBlock(event)) this.blocks.push("");
|
|
632
|
+
const text = textDelta(event);
|
|
633
|
+
if (text !== void 0) this.blocks.push((this.blocks.pop() ?? "") + text);
|
|
634
|
+
}
|
|
635
|
+
text(complete) {
|
|
636
|
+
const prefix = this.other.map((line) => `${line}\n`).join("");
|
|
637
|
+
if (complete && this.result !== void 0) return `${prefix}${this.result}\n`;
|
|
638
|
+
const text = this.blocks.filter((block) => block.length > 0).join("\n\n");
|
|
639
|
+
return text.length > 0 ? `${prefix}${text}\n` : prefix;
|
|
640
|
+
}
|
|
498
641
|
};
|
|
642
|
+
function foldClaudeStream(stdout, complete) {
|
|
643
|
+
const lines = stdout.split("\n");
|
|
644
|
+
const tail = lines.at(-1);
|
|
645
|
+
if (!complete && tail?.startsWith("{") && parseStreamLine(tail) === void 0) lines.pop();
|
|
646
|
+
const fold = new ClaudeStreamFold();
|
|
647
|
+
for (const line of lines) fold.add(line);
|
|
648
|
+
return fold.text(complete);
|
|
649
|
+
}
|
|
499
650
|
var Claude = class extends Harness {
|
|
500
651
|
id = "claude";
|
|
501
652
|
name = "Anthropic Claude Code";
|
|
@@ -671,8 +822,14 @@ var Claude = class extends Harness {
|
|
|
671
822
|
],
|
|
672
823
|
readOnlyMinVersion: "2.1.175",
|
|
673
824
|
modelArgs: ["--model", "{model}"],
|
|
825
|
+
streamArgs: [
|
|
826
|
+
"--output-format",
|
|
827
|
+
"stream-json",
|
|
828
|
+
"--verbose",
|
|
829
|
+
"--include-partial-messages"
|
|
830
|
+
],
|
|
674
831
|
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."
|
|
832
|
+
note: "Headless print mode; add --output-format json for structured output. Plain text runs stream events instead and fold them back, since text mode prints nothing until the answer is complete and a timeout would lose all of it; --include-partial-messages streams a single long answer too, checked on 2.1.280. --tools only covers the built-in set, so every mode without the full agent adds --strict-mcp-config to drop configured MCP servers too; without it an advisor still sees every user and project MCP tool, checked on 2.1.280. Read-only runs keep the built-in Read, Glob and Grep tools, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through."
|
|
676
833
|
};
|
|
677
834
|
mcpConfigs = [{
|
|
678
835
|
path: "~/.claude.json",
|
|
@@ -689,6 +846,63 @@ var Claude = class extends Harness {
|
|
|
689
846
|
key: ["mcpServers"],
|
|
690
847
|
dialect: "standard"
|
|
691
848
|
}];
|
|
849
|
+
temp = [
|
|
850
|
+
{
|
|
851
|
+
path: "${TMPDIR}/claude-<uid>/",
|
|
852
|
+
scope: "data",
|
|
853
|
+
level: "official",
|
|
854
|
+
platforms: ["linux"],
|
|
855
|
+
note: "Session scratchpads sit under <dash-encoded-cwd>/<session-id>/scratchpad."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
path: "/tmp/claude-<uid>/",
|
|
859
|
+
scope: "data",
|
|
860
|
+
level: "official",
|
|
861
|
+
platforms: ["darwin"]
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
path: "${TMPDIR}/claude/",
|
|
865
|
+
scope: "data",
|
|
866
|
+
level: "official",
|
|
867
|
+
platforms: ["win32"]
|
|
868
|
+
}
|
|
869
|
+
];
|
|
870
|
+
envOverrides = [
|
|
871
|
+
{
|
|
872
|
+
variable: "CLAUDE_CONFIG_DIR",
|
|
873
|
+
path: "~/.claude",
|
|
874
|
+
scope: "user",
|
|
875
|
+
level: "official",
|
|
876
|
+
relocates: [
|
|
877
|
+
"config",
|
|
878
|
+
"sessions",
|
|
879
|
+
"instructions",
|
|
880
|
+
"skills",
|
|
881
|
+
"commands",
|
|
882
|
+
"promptTemplates",
|
|
883
|
+
"hooks"
|
|
884
|
+
],
|
|
885
|
+
note: "Read from the shell or the env block of user or managed settings, not project settings."
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
variable: "CLAUDE_CODE_TMPDIR",
|
|
889
|
+
path: "${TMPDIR}",
|
|
890
|
+
scope: "data",
|
|
891
|
+
level: "official",
|
|
892
|
+
platforms: ["linux", "win32"],
|
|
893
|
+
relocates: ["temp"],
|
|
894
|
+
note: "Read from the shell or the env block of user or managed settings, not project settings."
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
variable: "CLAUDE_CODE_TMPDIR",
|
|
898
|
+
path: "/tmp",
|
|
899
|
+
scope: "data",
|
|
900
|
+
level: "official",
|
|
901
|
+
platforms: ["darwin"],
|
|
902
|
+
relocates: ["temp"],
|
|
903
|
+
note: "Read from the shell or the env block of user or managed settings, not project settings."
|
|
904
|
+
}
|
|
905
|
+
];
|
|
692
906
|
agentsFile = "~/.claude/CLAUDE.md";
|
|
693
907
|
detection = {
|
|
694
908
|
envVars: [
|
|
@@ -704,6 +918,9 @@ var Claude = class extends Harness {
|
|
|
704
918
|
"CLAUDE.md"
|
|
705
919
|
]
|
|
706
920
|
};
|
|
921
|
+
foldStreamOutput(stdout, complete) {
|
|
922
|
+
return foldClaudeStream(stdout, complete);
|
|
923
|
+
}
|
|
707
924
|
};
|
|
708
925
|
var Codex = class extends Harness {
|
|
709
926
|
id = "codex";
|
|
@@ -831,7 +1048,7 @@ var Codex = class extends Harness {
|
|
|
831
1048
|
path: "~/.codex/prompts/",
|
|
832
1049
|
scope: "user",
|
|
833
1050
|
level: "official",
|
|
834
|
-
note: "Deprecated Markdown prompts invoked as /prompts:name. Top-level files only; use skills for shared prompts.
|
|
1051
|
+
note: "Deprecated Markdown prompts invoked as /prompts:name. Top-level files only; use skills for shared prompts."
|
|
835
1052
|
}];
|
|
836
1053
|
promptTemplates = this.commands;
|
|
837
1054
|
promptTemplateSyncTarget = {
|
|
@@ -887,6 +1104,20 @@ var Codex = class extends Harness {
|
|
|
887
1104
|
key: ["mcp_servers"],
|
|
888
1105
|
dialect: "standard"
|
|
889
1106
|
}];
|
|
1107
|
+
envOverrides = [{
|
|
1108
|
+
variable: "CODEX_HOME",
|
|
1109
|
+
path: "~/.codex",
|
|
1110
|
+
scope: "user",
|
|
1111
|
+
level: "official",
|
|
1112
|
+
relocates: [
|
|
1113
|
+
"config",
|
|
1114
|
+
"sessions",
|
|
1115
|
+
"instructions",
|
|
1116
|
+
"skills",
|
|
1117
|
+
"commands",
|
|
1118
|
+
"promptTemplates"
|
|
1119
|
+
]
|
|
1120
|
+
}];
|
|
890
1121
|
agentsFile = "~/.codex/AGENTS.md";
|
|
891
1122
|
detection = {
|
|
892
1123
|
envVars: [],
|
|
@@ -1311,6 +1542,20 @@ var GitHubCopilot = class extends Harness {
|
|
|
1311
1542
|
]
|
|
1312
1543
|
};
|
|
1313
1544
|
};
|
|
1545
|
+
function parseGrokModels(stdout) {
|
|
1546
|
+
const lines = stripVTControlCharacters(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1547
|
+
const start = lines.indexOf("Available models:");
|
|
1548
|
+
if (start === -1) throw new Error(`Unexpected Grok model-list output: ${JSON.stringify(stdout)}`);
|
|
1549
|
+
return lines.slice(start + 1).map((line) => {
|
|
1550
|
+
const match = /^([*-])\s+(\S+)(?:\s+\(default\))?$/.exec(line);
|
|
1551
|
+
if (!match?.[2]) throw new Error(`Unexpected Grok model-list row: ${JSON.stringify(line)}`);
|
|
1552
|
+
return {
|
|
1553
|
+
provider: "xai",
|
|
1554
|
+
id: match[2],
|
|
1555
|
+
...match[1] === "*" ? { default: true } : {}
|
|
1556
|
+
};
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1314
1559
|
var Grok = class extends Harness {
|
|
1315
1560
|
id = "grok";
|
|
1316
1561
|
name = "xAI Grok CLI";
|
|
@@ -1443,6 +1688,11 @@ var Grok = class extends Harness {
|
|
|
1443
1688
|
level: "official",
|
|
1444
1689
|
note: "-p is short for --single; add --output-format json for structured output. --sandbox read-only is kernel-enforced (Landlock, Seatbelt) regardless of the inherited permission mode and still allows writes to ~/.grok and temp dirs; verified on Linux with 1.0.13 and 1.0.25."
|
|
1445
1690
|
};
|
|
1691
|
+
modelListing = {
|
|
1692
|
+
args: ["models"],
|
|
1693
|
+
level: "official",
|
|
1694
|
+
note: "Prints bare ids with the default marked; verified with 1.0.41."
|
|
1695
|
+
};
|
|
1446
1696
|
mcpConfigs = [{
|
|
1447
1697
|
path: "~/.grok/config.toml",
|
|
1448
1698
|
scope: "user",
|
|
@@ -1467,6 +1717,9 @@ var Grok = class extends Harness {
|
|
|
1467
1717
|
],
|
|
1468
1718
|
projectMarkers: [".grok"]
|
|
1469
1719
|
};
|
|
1720
|
+
parseModelListingOutput(stdout) {
|
|
1721
|
+
return parseGrokModels(stdout);
|
|
1722
|
+
}
|
|
1470
1723
|
};
|
|
1471
1724
|
function packageVersion(commandPath) {
|
|
1472
1725
|
try {
|
|
@@ -1730,7 +1983,7 @@ var Omp = class extends Harness {
|
|
|
1730
1983
|
path: "~/.omp/agent/config.yml",
|
|
1731
1984
|
scope: "user",
|
|
1732
1985
|
level: "official",
|
|
1733
|
-
note: "
|
|
1986
|
+
note: "Profiles use ~/.omp/profiles/<name>/agent."
|
|
1734
1987
|
},
|
|
1735
1988
|
{
|
|
1736
1989
|
path: "~/.omp/agent/settings.json",
|
|
@@ -1912,6 +2165,21 @@ var Omp = class extends Harness {
|
|
|
1912
2165
|
key: ["mcpServers"],
|
|
1913
2166
|
dialect: "standard"
|
|
1914
2167
|
}];
|
|
2168
|
+
envOverrides = [{
|
|
2169
|
+
variable: "PI_CODING_AGENT_DIR",
|
|
2170
|
+
path: "~/.omp/agent",
|
|
2171
|
+
scope: "user",
|
|
2172
|
+
level: "official",
|
|
2173
|
+
relocates: [
|
|
2174
|
+
"config",
|
|
2175
|
+
"sessions",
|
|
2176
|
+
"instructions",
|
|
2177
|
+
"skills",
|
|
2178
|
+
"commands",
|
|
2179
|
+
"promptTemplates"
|
|
2180
|
+
],
|
|
2181
|
+
note: "Ignored under a named profile (OMP_PROFILE). On Linux, after omp config init-xdg, data, state and cache live under $XDG_*_HOME/omp instead."
|
|
2182
|
+
}];
|
|
1915
2183
|
agentsFile = "~/.omp/agent/AGENTS.md";
|
|
1916
2184
|
detection = {
|
|
1917
2185
|
envVars: ["OMP_PROFILE", "PI_CODING_AGENT_DIR"],
|
|
@@ -2136,8 +2404,7 @@ var Pi = class extends Harness {
|
|
|
2136
2404
|
config = [{
|
|
2137
2405
|
path: "~/.pi/agent/settings.json",
|
|
2138
2406
|
scope: "user",
|
|
2139
|
-
level: "official"
|
|
2140
|
-
note: "Config dir overridable with PI_CODING_AGENT_DIR."
|
|
2407
|
+
level: "official"
|
|
2141
2408
|
}, {
|
|
2142
2409
|
path: ".pi/settings.json",
|
|
2143
2410
|
scope: "project",
|
|
@@ -2147,7 +2414,7 @@ var Pi = class extends Harness {
|
|
|
2147
2414
|
path: "~/.pi/agent/sessions/<dash-encoded-cwd>/<timestamp>_<session-id>.jsonl",
|
|
2148
2415
|
scope: "data",
|
|
2149
2416
|
level: "official",
|
|
2150
|
-
note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with
|
|
2417
|
+
note: "Per-project session transcripts in JSONL format; session ids are UUIDv7. Overridable with --session-dir."
|
|
2151
2418
|
}];
|
|
2152
2419
|
persistence = [{
|
|
2153
2420
|
format: "JSON",
|
|
@@ -2257,6 +2524,28 @@ var Pi = class extends Harness {
|
|
|
2257
2524
|
level: "official",
|
|
2258
2525
|
note: "Lists models with configured provider authentication; accepts an optional fuzzy search."
|
|
2259
2526
|
};
|
|
2527
|
+
envOverrides = [{
|
|
2528
|
+
variable: "PI_CODING_AGENT_DIR",
|
|
2529
|
+
path: "~/.pi/agent",
|
|
2530
|
+
scope: "user",
|
|
2531
|
+
level: "official",
|
|
2532
|
+
relocates: [
|
|
2533
|
+
"config",
|
|
2534
|
+
"sessions",
|
|
2535
|
+
"instructions",
|
|
2536
|
+
"skills",
|
|
2537
|
+
"commands",
|
|
2538
|
+
"promptTemplates"
|
|
2539
|
+
],
|
|
2540
|
+
note: "A leading ~ in the value is expanded."
|
|
2541
|
+
}, {
|
|
2542
|
+
variable: "PI_CODING_AGENT_SESSION_DIR",
|
|
2543
|
+
path: "~/.pi/agent/sessions",
|
|
2544
|
+
scope: "data",
|
|
2545
|
+
level: "official",
|
|
2546
|
+
relocates: ["sessions"],
|
|
2547
|
+
note: "Takes precedence over PI_CODING_AGENT_DIR for sessions; --session-dir beats both."
|
|
2548
|
+
}];
|
|
2260
2549
|
agentsFile = "~/.pi/agent/AGENTS.md";
|
|
2261
2550
|
detection = {
|
|
2262
2551
|
envVars: [
|
|
@@ -3235,6 +3524,14 @@ function moveFile(from, to) {
|
|
|
3235
3524
|
renameSync(from, to);
|
|
3236
3525
|
} catch (error) {
|
|
3237
3526
|
if (error.code !== "EXDEV") throw error;
|
|
3527
|
+
if (lstatSync(from).isDirectory()) {
|
|
3528
|
+
cpSync(from, to, {
|
|
3529
|
+
recursive: true,
|
|
3530
|
+
verbatimSymlinks: true
|
|
3531
|
+
});
|
|
3532
|
+
rmSync(from, { recursive: true });
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3238
3535
|
copyFileSync(from, to);
|
|
3239
3536
|
unlinkSync(from);
|
|
3240
3537
|
}
|
|
@@ -3344,16 +3641,12 @@ const GENERATED_MARKER = "# Generated by @agntn/harnesses from ";
|
|
|
3344
3641
|
const PORTABLE_PROMPT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
|
3345
3642
|
const GEMINI_ACTION = {
|
|
3346
3643
|
missing: "generated",
|
|
3347
|
-
"correct-link": "adopted",
|
|
3348
|
-
"wrong-link": "adopted",
|
|
3349
3644
|
"owned-file": "replaced",
|
|
3350
3645
|
"unmanaged-file": "adopted",
|
|
3351
3646
|
unsupported: "skipped"
|
|
3352
3647
|
};
|
|
3353
3648
|
const GEMINI_BACKUP = {
|
|
3354
3649
|
missing: false,
|
|
3355
|
-
"correct-link": true,
|
|
3356
|
-
"wrong-link": true,
|
|
3357
3650
|
"owned-file": true,
|
|
3358
3651
|
"unmanaged-file": true,
|
|
3359
3652
|
unsupported: false
|
|
@@ -3391,7 +3684,11 @@ function generatedSource(content) {
|
|
|
3391
3684
|
return null;
|
|
3392
3685
|
}
|
|
3393
3686
|
}
|
|
3394
|
-
function
|
|
3687
|
+
function linkTarget(path) {
|
|
3688
|
+
const target = readlinkSync(path);
|
|
3689
|
+
return isAbsolute(target) ? target : resolve(dirname(path), target);
|
|
3690
|
+
}
|
|
3691
|
+
function inspectDestination(path) {
|
|
3395
3692
|
let stats;
|
|
3396
3693
|
try {
|
|
3397
3694
|
stats = lstatSync(path);
|
|
@@ -3399,11 +3696,7 @@ function inspectDestination(path, source) {
|
|
|
3399
3696
|
if (error.code === "ENOENT") return { kind: "missing" };
|
|
3400
3697
|
throw error;
|
|
3401
3698
|
}
|
|
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
|
-
}
|
|
3699
|
+
if (stats.isSymbolicLink()) return { kind: "unmanaged-file" };
|
|
3407
3700
|
if (!stats.isFile()) return { kind: "unsupported" };
|
|
3408
3701
|
const content = readFileSync(path, "utf8");
|
|
3409
3702
|
if (generatedSource(content) !== null) return {
|
|
@@ -3415,10 +3708,10 @@ function inspectDestination(path, source) {
|
|
|
3415
3708
|
function tempPath(path, kind) {
|
|
3416
3709
|
return join(dirname(path), `.${basename(path)}-${randomUUID()}.${kind}.tmp`);
|
|
3417
3710
|
}
|
|
3418
|
-
function
|
|
3711
|
+
function linkDirectory(path, source) {
|
|
3419
3712
|
mkdirSync(dirname(path), { recursive: true });
|
|
3420
3713
|
const temp = tempPath(path, "prompt-link");
|
|
3421
|
-
symlinkSync(source, temp);
|
|
3714
|
+
symlinkSync(source, temp, "junction");
|
|
3422
3715
|
renameSync(temp, path);
|
|
3423
3716
|
}
|
|
3424
3717
|
function writeTemplate(path, content) {
|
|
@@ -3432,8 +3725,7 @@ function backupDestination(id, path, options) {
|
|
|
3432
3725
|
mkdirSync(backupDir, { recursive: true });
|
|
3433
3726
|
const backup = join(backupDir, `${basename(path)}-${Date.now()}-${randomUUID()}`);
|
|
3434
3727
|
if (lstatSync(path).isSymbolicLink()) {
|
|
3435
|
-
|
|
3436
|
-
symlinkSync(isAbsolute(target) ? target : resolve(dirname(path), target), backup);
|
|
3728
|
+
symlinkSync(linkTarget(path), backup);
|
|
3437
3729
|
unlinkSync(path);
|
|
3438
3730
|
return backup;
|
|
3439
3731
|
}
|
|
@@ -3443,44 +3735,68 @@ function backupDestination(id, path, options) {
|
|
|
3443
3735
|
function checkDetail() {
|
|
3444
3736
|
return "check mode: not applied";
|
|
3445
3737
|
}
|
|
3446
|
-
function
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3738
|
+
function isInsideDirectory(path, directory) {
|
|
3739
|
+
const remainder = relative(directory, path);
|
|
3740
|
+
return remainder !== "" && !remainder.startsWith("..") && !isAbsolute(remainder);
|
|
3741
|
+
}
|
|
3742
|
+
function isLegacyTemplateLink(path, sourceDir) {
|
|
3743
|
+
return lstatSync(path).isSymbolicLink() && isInsideDirectory(linkTarget(path), sourceDir);
|
|
3744
|
+
}
|
|
3745
|
+
const DIRECTORY_ACTION = {
|
|
3746
|
+
missing: "linked",
|
|
3747
|
+
"correct-link": "unchanged",
|
|
3748
|
+
"wrong-link": "relinked",
|
|
3749
|
+
"legacy-links": "relinked",
|
|
3750
|
+
"local-files": "adopted",
|
|
3751
|
+
"not-directory": "skipped"
|
|
3752
|
+
};
|
|
3753
|
+
function inspectDirectory(targetDir, sourceDir) {
|
|
3754
|
+
let stats;
|
|
3755
|
+
try {
|
|
3756
|
+
stats = lstatSync(targetDir);
|
|
3757
|
+
} catch (error) {
|
|
3758
|
+
if (error.code === "ENOENT") return { kind: "missing" };
|
|
3759
|
+
throw error;
|
|
3760
|
+
}
|
|
3761
|
+
if (stats.isSymbolicLink()) return { kind: linkTarget(targetDir) === sourceDir ? "correct-link" : "wrong-link" };
|
|
3762
|
+
if (!stats.isDirectory()) return { kind: "not-directory" };
|
|
3763
|
+
const entries = readdirSync(targetDir).map((entry) => join(targetDir, entry));
|
|
3764
|
+
return entries.every((entry) => isLegacyTemplateLink(entry, sourceDir)) ? {
|
|
3765
|
+
kind: "legacy-links",
|
|
3766
|
+
entries
|
|
3767
|
+
} : { kind: "local-files" };
|
|
3768
|
+
}
|
|
3769
|
+
function clearDirectory(id, targetDir, state, options) {
|
|
3770
|
+
if (state.kind === "wrong-link" || state.kind === "local-files") return backupDestination(id, targetDir, options);
|
|
3771
|
+
if (state.kind === "legacy-links") {
|
|
3772
|
+
for (const entry of state.entries) unlinkSync(entry);
|
|
3773
|
+
rmdirSync(targetDir);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
function syncMarkdownDirectory(id, targetDir, sourceDir, check, options) {
|
|
3777
|
+
const state = inspectDirectory(targetDir, sourceDir);
|
|
3778
|
+
const action = DIRECTORY_ACTION[state.kind];
|
|
3779
|
+
if (state.kind === "correct-link") return { action };
|
|
3780
|
+
if (state.kind === "not-directory") return {
|
|
3781
|
+
action,
|
|
3782
|
+
detail: "destination exists and is not a directory or symlink"
|
|
3461
3783
|
};
|
|
3462
|
-
const action = state.kind === "missing" ? "linked" : state.kind === "wrong-link" ? "relinked" : "adopted";
|
|
3463
3784
|
if (check) return {
|
|
3464
|
-
name: template.name,
|
|
3465
|
-
source: template.path,
|
|
3466
|
-
path,
|
|
3467
3785
|
action,
|
|
3468
3786
|
detail: checkDetail()
|
|
3469
3787
|
};
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
if (state.kind === "owned-file" || state.kind === "unmanaged-file") detail = backupDestination(id, path, options);
|
|
3473
|
-
linkTemplate(path, template.path);
|
|
3788
|
+
const detail = clearDirectory(id, targetDir, state, options);
|
|
3789
|
+
linkDirectory(targetDir, sourceDir);
|
|
3474
3790
|
return {
|
|
3475
|
-
name: template.name,
|
|
3476
|
-
source: template.path,
|
|
3477
|
-
path,
|
|
3478
3791
|
action,
|
|
3479
3792
|
detail
|
|
3480
3793
|
};
|
|
3481
3794
|
}
|
|
3795
|
+
function parseYaml(raw) {
|
|
3796
|
+
return createRequire(import.meta.url)("yaml").parse(raw);
|
|
3797
|
+
}
|
|
3482
3798
|
function frontmatterDescription(raw, path) {
|
|
3483
|
-
const frontmatter =
|
|
3799
|
+
const frontmatter = parseYaml(raw);
|
|
3484
3800
|
if (frontmatter === null) return void 0;
|
|
3485
3801
|
if (typeof frontmatter !== "object" || Array.isArray(frontmatter)) throw new TypeError(`Prompt template frontmatter is not an object: ${path}`);
|
|
3486
3802
|
const description = frontmatter.description;
|
|
@@ -3541,23 +3857,6 @@ function syncGeminiTemplate(id, targetDir, template, check, options) {
|
|
|
3541
3857
|
detail
|
|
3542
3858
|
};
|
|
3543
3859
|
}
|
|
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
3860
|
function staleGeminiSource(path, sourceDir) {
|
|
3562
3861
|
let content;
|
|
3563
3862
|
try {
|
|
@@ -3569,28 +3868,40 @@ function staleGeminiSource(path, sourceDir) {
|
|
|
3569
3868
|
if (source === null) return null;
|
|
3570
3869
|
return isInsideDirectory(source, sourceDir) ? source : null;
|
|
3571
3870
|
}
|
|
3572
|
-
function
|
|
3871
|
+
function pruneGeminiTarget(id, targetDir, sourceDir, activeNames, check, options) {
|
|
3573
3872
|
let entries;
|
|
3574
3873
|
try {
|
|
3575
|
-
entries = readdirSync(targetDir
|
|
3874
|
+
entries = readdirSync(targetDir);
|
|
3576
3875
|
} catch (error) {
|
|
3577
3876
|
if (error.code === "ENOENT") return [];
|
|
3578
3877
|
throw error;
|
|
3579
3878
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
const
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
return [{
|
|
3588
|
-
name: basename(entry.name, extension),
|
|
3589
|
-
source,
|
|
3879
|
+
return entries.filter((entry) => !activeNames.has(entry)).map((entry) => {
|
|
3880
|
+
const path = join(targetDir, entry);
|
|
3881
|
+
const name = basename(entry, extname(entry));
|
|
3882
|
+
const source = staleGeminiSource(path, sourceDir);
|
|
3883
|
+
if (check) return {
|
|
3884
|
+
name,
|
|
3885
|
+
...source === null ? {} : { source },
|
|
3590
3886
|
path,
|
|
3591
3887
|
action: "removed",
|
|
3592
|
-
|
|
3593
|
-
}
|
|
3888
|
+
detail: checkDetail()
|
|
3889
|
+
};
|
|
3890
|
+
if (source !== null) {
|
|
3891
|
+
unlinkSync(path);
|
|
3892
|
+
return {
|
|
3893
|
+
name,
|
|
3894
|
+
source,
|
|
3895
|
+
path,
|
|
3896
|
+
action: "removed"
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3899
|
+
return {
|
|
3900
|
+
name,
|
|
3901
|
+
path,
|
|
3902
|
+
action: "removed",
|
|
3903
|
+
detail: backupDestination(id, path, options)
|
|
3904
|
+
};
|
|
3594
3905
|
});
|
|
3595
3906
|
}
|
|
3596
3907
|
function syncTarget(harness, templates, sourceDir, check, options) {
|
|
@@ -3606,11 +3917,17 @@ function syncTarget(harness, templates, sourceDir, check, options) {
|
|
|
3606
3917
|
detail: "prompt template destination is unavailable on this platform",
|
|
3607
3918
|
templates: []
|
|
3608
3919
|
};
|
|
3609
|
-
const targetDir = resolved.path;
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3920
|
+
const targetDir = resolve(resolved.path);
|
|
3921
|
+
if (resolved.format === "markdown") return {
|
|
3922
|
+
id: harness.id,
|
|
3923
|
+
path: targetDir,
|
|
3924
|
+
format: resolved.format,
|
|
3925
|
+
...syncMarkdownDirectory(harness.id, targetDir, sourceDir, check, options),
|
|
3926
|
+
templates: []
|
|
3927
|
+
};
|
|
3928
|
+
const results = templates.map((template) => syncGeminiTemplate(harness.id, targetDir, template, check, options));
|
|
3929
|
+
const activeNames = new Set(templates.map((template) => `${template.name}.toml`));
|
|
3930
|
+
results.push(...pruneGeminiTarget(harness.id, targetDir, sourceDir, activeNames, check, options));
|
|
3614
3931
|
return {
|
|
3615
3932
|
id: harness.id,
|
|
3616
3933
|
path: targetDir,
|