portable-agent-layer 0.64.0 → 0.65.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 +1 -1
- package/assets/skills/consulting-report/tools/generate-pdf.mjs +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +5 -2
- package/assets/skills/playwright/SKILL.md +2 -2
- package/assets/skills/playwright/tools/shot.ts +6 -7
- package/assets/skills/projects/SKILL.md +4 -1
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +15 -4
- package/src/cli/index.ts +93 -7
- package/src/cli/migrate.ts +69 -3
- package/src/hooks/lib/anchor.ts +90 -0
- package/src/hooks/lib/bindings.ts +117 -0
- package/src/hooks/lib/export.ts +38 -1
- package/src/hooks/lib/import-merge.ts +220 -0
- package/src/hooks/lib/inference.ts +113 -72
- package/src/hooks/lib/machine.ts +176 -0
- package/src/hooks/lib/projects.ts +223 -15
- package/src/hooks/lib/relationship.ts +3 -1
- package/src/hooks/lib/remote.ts +58 -0
- package/src/hooks/lib/retrieval.ts +8 -2
- package/src/hooks/lib/signals.ts +2 -1
- package/src/hooks/lib/stop.ts +5 -2
- package/src/targets/lib.ts +79 -34
- package/src/tools/agent/algorithm-reflect.ts +45 -11
- package/src/tools/agent/project.ts +148 -23
- package/src/tools/agent/thread.ts +7 -2
- package/assets/skills/playwright/tools/shot-lib.mjs +0 -44
- package/assets/skills/playwright/tools/shot.mjs +0 -89
package/src/hooks/lib/export.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { existsSync, readdirSync } from "node:fs";
|
|
7
7
|
import { relative, resolve } from "node:path";
|
|
8
8
|
import AdmZip from "adm-zip";
|
|
9
|
+
import { ensureRegistered } from "./machine";
|
|
9
10
|
import { palHome } from "./paths";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -59,9 +60,41 @@ export function collectExportFiles(): string[] {
|
|
|
59
60
|
return files;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
+
/** Archive metadata naming the machine that produced it. */
|
|
64
|
+
export const MANIFEST_NAME = "export-manifest.json";
|
|
65
|
+
|
|
66
|
+
export interface ExportManifest {
|
|
67
|
+
machineId: string;
|
|
68
|
+
label: string;
|
|
69
|
+
os: string;
|
|
70
|
+
exportedAt: string;
|
|
71
|
+
fileCount: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildManifest(
|
|
75
|
+
identity: { id: string; label: string; os: string },
|
|
76
|
+
fileCount: number
|
|
77
|
+
): ExportManifest {
|
|
78
|
+
return {
|
|
79
|
+
machineId: identity.id,
|
|
80
|
+
label: identity.label,
|
|
81
|
+
os: identity.os,
|
|
82
|
+
exportedAt: new Date().toISOString(),
|
|
83
|
+
fileCount,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Zip the given files and write to outputPath. Returns file count.
|
|
89
|
+
*
|
|
90
|
+
* The archive declares its source machine in a manifest. Registry entries under
|
|
91
|
+
* memory/machines/ travel with the corpus, so the manifest exists to say which
|
|
92
|
+
* machine produced THIS archive — after a merge an archive can carry entries
|
|
93
|
+
* for several machines.
|
|
94
|
+
*/
|
|
63
95
|
export function exportZip(outputPath: string): number {
|
|
64
96
|
const root = palHome();
|
|
97
|
+
const identity = ensureRegistered(root);
|
|
65
98
|
const files = collectExportFiles();
|
|
66
99
|
if (files.length === 0) return 0;
|
|
67
100
|
|
|
@@ -71,6 +104,10 @@ export function exportZip(outputPath: string): number {
|
|
|
71
104
|
const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
|
|
72
105
|
zip.addLocalFile(fullPath, dir);
|
|
73
106
|
}
|
|
107
|
+
zip.addFile(
|
|
108
|
+
MANIFEST_NAME,
|
|
109
|
+
Buffer.from(`${JSON.stringify(buildManifest(identity, files.length), null, 2)}\n`)
|
|
110
|
+
);
|
|
74
111
|
|
|
75
112
|
zip.writeZip(outputPath);
|
|
76
113
|
return files.length;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import merge — fold an export archive into an existing PAL home without
|
|
3
|
+
* destroying local records.
|
|
4
|
+
*
|
|
5
|
+
* `extractAllTo(home, true)` overwrites every colliding path, so importing
|
|
6
|
+
* machine A onto machine B silently discards B's side of every append-only log.
|
|
7
|
+
* This module replaces that with a per-type policy:
|
|
8
|
+
*
|
|
9
|
+
* *.jsonl union of both sides, deduplicated by exact line
|
|
10
|
+
* new files written as-is
|
|
11
|
+
* identical no-op
|
|
12
|
+
* diverged local kept in place, incoming quarantined under backups/
|
|
13
|
+
* denylisted never written (machine identity, rebuildable indexes)
|
|
14
|
+
*
|
|
15
|
+
* Every policy is idempotent: re-importing the same archive is a no-op on the
|
|
16
|
+
* corpus.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
import { type ExportManifest, MANIFEST_NAME } from "./export";
|
|
22
|
+
|
|
23
|
+
/** One file inside an export archive, decoupled from the zip library. */
|
|
24
|
+
export interface ArchiveEntry {
|
|
25
|
+
path: string;
|
|
26
|
+
data(): Buffer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MergeResult {
|
|
30
|
+
created: string[];
|
|
31
|
+
merged: string[];
|
|
32
|
+
identical: string[];
|
|
33
|
+
conflicts: string[];
|
|
34
|
+
skipped: string[];
|
|
35
|
+
linesAdded: number;
|
|
36
|
+
quarantineDir: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Paths that must never cross machines. `machine.json` carries this install's
|
|
41
|
+
* identity — importing it would give two machines one id and silently break
|
|
42
|
+
* every origin-scoped read. The retrieval index is rebuilt from its sources.
|
|
43
|
+
*/
|
|
44
|
+
const NEVER_IMPORT = [
|
|
45
|
+
"machine.json",
|
|
46
|
+
"export-manifest.json",
|
|
47
|
+
"memory/learning/.retrieval-index.json",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function normalize(path: string): string {
|
|
51
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isNeverImport(path: string): boolean {
|
|
55
|
+
const rel = normalize(path);
|
|
56
|
+
return NEVER_IMPORT.some((deny) => rel === deny || rel.endsWith(`/${deny}`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isJsonl(path: string): boolean {
|
|
60
|
+
return normalize(path).endsWith(".jsonl");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function splitLines(raw: string): string[] {
|
|
64
|
+
return raw.split("\n").filter((l) => l.trim().length > 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Union of two JSONL bodies, local order preserved, incoming lines appended
|
|
69
|
+
* only when not already present. Exact-line identity is the dedupe key — no
|
|
70
|
+
* schema is shared across PAL's jsonl files, and every writer serializes a
|
|
71
|
+
* record the same way, so byte equality is the only key that holds for all of
|
|
72
|
+
* them.
|
|
73
|
+
*/
|
|
74
|
+
export function mergeJsonlLines(
|
|
75
|
+
localRaw: string,
|
|
76
|
+
incomingRaw: string
|
|
77
|
+
): { text: string; added: number } {
|
|
78
|
+
const local = splitLines(localRaw);
|
|
79
|
+
const seen = new Set(local);
|
|
80
|
+
const added: string[] = [];
|
|
81
|
+
for (const line of splitLines(incomingRaw)) {
|
|
82
|
+
if (seen.has(line)) continue;
|
|
83
|
+
seen.add(line);
|
|
84
|
+
added.push(line);
|
|
85
|
+
}
|
|
86
|
+
const all = [...local, ...added];
|
|
87
|
+
return { text: all.length > 0 ? `${all.join("\n")}\n` : "", added: added.length };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeFileEnsuringDir(target: string, data: Buffer | string): void {
|
|
91
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
92
|
+
writeFileSync(target, data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function quarantine(quarantineDir: string, rel: string, data: Buffer): void {
|
|
96
|
+
writeFileEnsuringDir(resolve(quarantineDir, rel), data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Merge every archive entry into `home`. `quarantineDir` receives the incoming
|
|
101
|
+
* copy of any file that diverged from its local counterpart, so a conflict
|
|
102
|
+
* loses neither side.
|
|
103
|
+
*/
|
|
104
|
+
export function mergeArchive(
|
|
105
|
+
entries: ArchiveEntry[],
|
|
106
|
+
home: string,
|
|
107
|
+
quarantineDir: string
|
|
108
|
+
): MergeResult {
|
|
109
|
+
const result: MergeResult = {
|
|
110
|
+
created: [],
|
|
111
|
+
merged: [],
|
|
112
|
+
identical: [],
|
|
113
|
+
conflicts: [],
|
|
114
|
+
skipped: [],
|
|
115
|
+
linesAdded: 0,
|
|
116
|
+
quarantineDir: null,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const rel = normalize(entry.path);
|
|
121
|
+
if (rel.length === 0 || rel.endsWith("/")) continue;
|
|
122
|
+
|
|
123
|
+
if (isNeverImport(rel)) {
|
|
124
|
+
result.skipped.push(rel);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const target = resolve(home, rel);
|
|
129
|
+
const incoming = entry.data();
|
|
130
|
+
|
|
131
|
+
if (!existsSync(target)) {
|
|
132
|
+
writeFileEnsuringDir(target, incoming);
|
|
133
|
+
result.created.push(rel);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const localRaw = readFileSync(target);
|
|
138
|
+
if (localRaw.equals(incoming)) {
|
|
139
|
+
result.identical.push(rel);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isJsonl(rel)) {
|
|
144
|
+
const { text, added } = mergeJsonlLines(
|
|
145
|
+
localRaw.toString("utf-8"),
|
|
146
|
+
incoming.toString("utf-8")
|
|
147
|
+
);
|
|
148
|
+
writeFileSync(target, text);
|
|
149
|
+
result.merged.push(rel);
|
|
150
|
+
result.linesAdded += added;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
quarantine(quarantineDir, rel, incoming);
|
|
155
|
+
result.conflicts.push(rel);
|
|
156
|
+
result.quarantineDir = quarantineDir;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The source machine declared by an archive's manifest, or null when the
|
|
164
|
+
* archive predates manifests.
|
|
165
|
+
*/
|
|
166
|
+
export function readManifest(entries: ArchiveEntry[]): ExportManifest | null {
|
|
167
|
+
const hit = entries.find((e) => normalize(e.path) === MANIFEST_NAME);
|
|
168
|
+
if (!hit) return null;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(hit.data().toString("utf-8")) as Partial<ExportManifest>;
|
|
171
|
+
if (typeof parsed.machineId !== "string" || parsed.machineId.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
return {
|
|
174
|
+
machineId: parsed.machineId,
|
|
175
|
+
label: parsed.label ?? parsed.machineId,
|
|
176
|
+
os: parsed.os ?? "",
|
|
177
|
+
exportedAt: parsed.exportedAt ?? "",
|
|
178
|
+
fileCount: parsed.fileCount ?? 0,
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ImportLogEntry {
|
|
186
|
+
ts: string;
|
|
187
|
+
archive: string;
|
|
188
|
+
mode: "merge" | "overwrite";
|
|
189
|
+
created: number;
|
|
190
|
+
merged: number;
|
|
191
|
+
identical: number;
|
|
192
|
+
conflicts: number;
|
|
193
|
+
skipped: number;
|
|
194
|
+
linesAdded: number;
|
|
195
|
+
quarantineDir: string | null;
|
|
196
|
+
sourceMachineId?: string | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Append one record per import so a merged corpus stays attributable. */
|
|
200
|
+
export function appendImportLog(home: string, entry: ImportLogEntry): void {
|
|
201
|
+
const logPath = resolve(home, "memory", "state", "import-log.jsonl");
|
|
202
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
203
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
204
|
+
if (existsSync(logPath)) {
|
|
205
|
+
writeFileSync(logPath, readFileSync(logPath, "utf-8") + line);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(logPath, line);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function summarize(result: MergeResult): string {
|
|
212
|
+
const parts = [
|
|
213
|
+
`${result.created.length} new`,
|
|
214
|
+
`${result.merged.length} merged (+${result.linesAdded} records)`,
|
|
215
|
+
`${result.identical.length} unchanged`,
|
|
216
|
+
];
|
|
217
|
+
if (result.conflicts.length > 0) parts.push(`${result.conflicts.length} conflicts`);
|
|
218
|
+
if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped`);
|
|
219
|
+
return parts.join(", ");
|
|
220
|
+
}
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* yet wired and currently fall through to the API path.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { basename, join } from "node:path";
|
|
24
26
|
import {
|
|
25
27
|
getActiveAgent,
|
|
26
28
|
isClaude,
|
|
@@ -151,14 +153,14 @@ export async function inference(opts: InferenceOptions): Promise<InferenceResult
|
|
|
151
153
|
"inference",
|
|
152
154
|
`${tag} route=claude-spawn agent=${agent} model=${opts.model ?? HAIKU_MODEL}`
|
|
153
155
|
);
|
|
154
|
-
return
|
|
156
|
+
return inferenceViaClaudeSpawn(bin, opts);
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
if (isCodex()) {
|
|
158
160
|
const bin = getCodexBinary();
|
|
159
161
|
if (bin) {
|
|
160
162
|
logDebug("inference", `${tag} route=codex-spawn agent=${agent}`);
|
|
161
|
-
return inferenceViaCliSpawn(bin, buildCodexArgs(opts),
|
|
163
|
+
return inferenceViaCliSpawn(bin, buildCodexArgs(opts), buildCliPrompt(opts), opts);
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
if (isCodex() && hasOpenAiKey()) {
|
|
@@ -172,7 +174,7 @@ export async function inference(opts: InferenceOptions): Promise<InferenceResult
|
|
|
172
174
|
return inferenceViaCliSpawn(
|
|
173
175
|
bin,
|
|
174
176
|
buildOpencodeArgs(opts),
|
|
175
|
-
|
|
177
|
+
buildCliPrompt(opts),
|
|
176
178
|
opts,
|
|
177
179
|
extractOpencodeText
|
|
178
180
|
);
|
|
@@ -182,14 +184,19 @@ export async function inference(opts: InferenceOptions): Promise<InferenceResult
|
|
|
182
184
|
const bin = getCopilotBinary();
|
|
183
185
|
if (bin) {
|
|
184
186
|
logDebug("inference", `${tag} route=copilot-spawn agent=${agent}`);
|
|
185
|
-
return inferenceViaCliSpawn(
|
|
187
|
+
return inferenceViaCliSpawn(
|
|
188
|
+
bin,
|
|
189
|
+
buildCopilotArgs(opts),
|
|
190
|
+
buildCliPrompt(opts),
|
|
191
|
+
opts
|
|
192
|
+
);
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
if (isCursor()) {
|
|
189
196
|
const bin = getCursorBinary();
|
|
190
197
|
if (bin) {
|
|
191
198
|
logDebug("inference", `${tag} route=cursor-spawn agent=${agent}`);
|
|
192
|
-
return inferenceViaCliSpawn(bin, buildCursorArgs(opts),
|
|
199
|
+
return inferenceViaCliSpawn(bin, buildCursorArgs(opts), buildCliPrompt(opts), opts);
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
202
|
if (hasApiKey()) {
|
|
@@ -284,16 +291,23 @@ export function _resetCursorBinaryCache(): void {
|
|
|
284
291
|
cursorBinaryCache = undefined;
|
|
285
292
|
}
|
|
286
293
|
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Build the argv for `claude --print …`. Pure.
|
|
296
|
+
*
|
|
297
|
+
* `--system-prompt` is deliberately absent: PAL's system prompts run to several
|
|
298
|
+
* paragraphs, and an argv element cannot carry a newline on Windows once
|
|
299
|
+
* Bun.spawn resolves claude to its .cmd shim and cmd.exe re-parses the command
|
|
300
|
+
* line. System, user and any JSON-schema instruction all travel together on
|
|
301
|
+
* stdin instead, the same way every other agent receives them.
|
|
302
|
+
*/
|
|
303
|
+
export function buildClaudeArgs(
|
|
304
|
+
opts: InferenceOptions,
|
|
305
|
+
systemPromptFile?: string
|
|
306
|
+
): string[] {
|
|
293
307
|
const args = [
|
|
294
308
|
"--print",
|
|
295
309
|
"--model",
|
|
296
|
-
model,
|
|
310
|
+
opts.model ?? HAIKU_MODEL,
|
|
297
311
|
"--tools",
|
|
298
312
|
"",
|
|
299
313
|
"--output-format",
|
|
@@ -301,12 +315,38 @@ export function buildClaudeArgs(opts: InferenceOptions): string[] {
|
|
|
301
315
|
"--setting-sources",
|
|
302
316
|
"",
|
|
303
317
|
];
|
|
304
|
-
if (system)
|
|
305
|
-
args.push("--system-prompt", system);
|
|
306
|
-
}
|
|
318
|
+
if (systemPromptFile) args.push("--system-prompt-file", systemPromptFile);
|
|
307
319
|
return args;
|
|
308
320
|
}
|
|
309
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Claude keeps a real system prompt, unlike the other agents, so the system text
|
|
324
|
+
* reaches it through --system-prompt-file rather than --system-prompt. Only the
|
|
325
|
+
* path travels in argv, which is what makes this work on Windows: an argv
|
|
326
|
+
* element cannot carry a newline once Bun.spawn resolves claude to its .cmd
|
|
327
|
+
* shim, and PAL's system prompts run to several paragraphs. Folding the system
|
|
328
|
+
* text into the user message instead is not an option — Claude treats
|
|
329
|
+
* instructions embedded in message content as an injection attempt and refuses.
|
|
330
|
+
*/
|
|
331
|
+
async function inferenceViaClaudeSpawn(
|
|
332
|
+
bin: string,
|
|
333
|
+
opts: InferenceOptions
|
|
334
|
+
): Promise<InferenceResult> {
|
|
335
|
+
const system = opts.jsonSchema
|
|
336
|
+
? injectJsonSchemaInstruction(opts.system ?? "", opts.jsonSchema)
|
|
337
|
+
: opts.system;
|
|
338
|
+
if (!system) return inferenceViaCliSpawn(bin, buildClaudeArgs(opts), opts.user, opts);
|
|
339
|
+
|
|
340
|
+
const dir = await mkdtemp(join(tmpdir(), "pal-system-"));
|
|
341
|
+
try {
|
|
342
|
+
const file = join(dir, "system-prompt.md");
|
|
343
|
+
await writeFile(file, system, "utf-8");
|
|
344
|
+
return await inferenceViaCliSpawn(bin, buildClaudeArgs(opts, file), opts.user, opts);
|
|
345
|
+
} finally {
|
|
346
|
+
await rm(dir, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
310
350
|
/**
|
|
311
351
|
* Build the argv for `codex exec …` from inference options. Pure.
|
|
312
352
|
*
|
|
@@ -316,20 +356,16 @@ export function buildClaudeArgs(opts: InferenceOptions): string[] {
|
|
|
316
356
|
* --sandbox read-only → child cannot execute shell commands even if it tries
|
|
317
357
|
* --ephemeral → no session persistence; one-shot only
|
|
318
358
|
*
|
|
319
|
-
* Codex has no --system-prompt equivalent
|
|
320
|
-
*
|
|
321
|
-
* prompt
|
|
359
|
+
* Codex has no --system-prompt equivalent, so system + user + JSON-schema become
|
|
360
|
+
* one prompt. That prompt goes in on stdin, not as a positional argument, because
|
|
361
|
+
* a PAL prompt spans several paragraphs and an argv element cannot carry a
|
|
362
|
+
* newline on Windows: Bun.spawn resolves the CLI to its .cmd shim, cmd.exe
|
|
363
|
+
* re-parses the command line, and the child exits non-zero having written
|
|
364
|
+
* nothing. Codex reads its instructions from stdin when no positional prompt is
|
|
365
|
+
* given, so this costs nothing on POSIX and is the only thing that works on
|
|
366
|
+
* Windows. Do not move the prompt back into argv.
|
|
322
367
|
*/
|
|
323
|
-
export function buildCodexArgs(
|
|
324
|
-
const parts: string[] = [];
|
|
325
|
-
if (opts.system) parts.push(opts.system);
|
|
326
|
-
parts.push(opts.user);
|
|
327
|
-
if (opts.jsonSchema) {
|
|
328
|
-
parts.push(
|
|
329
|
-
`Respond with ONLY a JSON value matching this schema (no prose, no markdown): ${JSON.stringify(opts.jsonSchema)}`
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
const prompt = parts.join("\n\n");
|
|
368
|
+
export function buildCodexArgs(_opts: InferenceOptions): string[] {
|
|
333
369
|
return [
|
|
334
370
|
"exec",
|
|
335
371
|
"--color",
|
|
@@ -340,7 +376,6 @@ export function buildCodexArgs(opts: InferenceOptions): string[] {
|
|
|
340
376
|
"--sandbox",
|
|
341
377
|
"read-only",
|
|
342
378
|
"--ephemeral",
|
|
343
|
-
prompt,
|
|
344
379
|
];
|
|
345
380
|
}
|
|
346
381
|
|
|
@@ -354,21 +389,14 @@ export function buildCodexArgs(opts: InferenceOptions): string[] {
|
|
|
354
389
|
* text via extractOpencodeText() rather than wading through
|
|
355
390
|
* decoration ("> build · provider/model" banner etc).
|
|
356
391
|
*
|
|
357
|
-
* opencode (like codex) has no --system-prompt equivalent
|
|
358
|
-
*
|
|
359
|
-
*
|
|
392
|
+
* opencode (like codex) has no --system-prompt equivalent, so system + user +
|
|
393
|
+
* JSON-schema are concatenated and delivered on stdin rather than as the
|
|
394
|
+
* positional message, for the same reason as codex: a multi-paragraph argv
|
|
395
|
+
* element does not survive cmd.exe on Windows. Provider/model is left unset so
|
|
396
|
+
* opencode uses the user's configured default.
|
|
360
397
|
*/
|
|
361
|
-
export function buildOpencodeArgs(
|
|
362
|
-
|
|
363
|
-
if (opts.system) parts.push(opts.system);
|
|
364
|
-
parts.push(opts.user);
|
|
365
|
-
if (opts.jsonSchema) {
|
|
366
|
-
parts.push(
|
|
367
|
-
`Respond with ONLY a JSON value matching this schema (no prose, no markdown): ${JSON.stringify(opts.jsonSchema)}`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
const prompt = parts.join("\n\n");
|
|
371
|
-
return ["run", "--pure", "--format", "json", prompt];
|
|
398
|
+
export function buildOpencodeArgs(_opts: InferenceOptions): string[] {
|
|
399
|
+
return ["run", "--pure", "--format", "json"];
|
|
372
400
|
}
|
|
373
401
|
|
|
374
402
|
/**
|
|
@@ -385,23 +413,16 @@ export function buildOpencodeArgs(opts: InferenceOptions): string[] {
|
|
|
385
413
|
* of running inference. Safe to pair with --mode ask
|
|
386
414
|
* because that mode disallows tool calls anyway.
|
|
387
415
|
*
|
|
388
|
-
* cursor-agent has no --system-prompt flag
|
|
389
|
-
* concatenated into
|
|
416
|
+
* cursor-agent has no --system-prompt flag, so system + user + JSON-schema are
|
|
417
|
+
* concatenated into one prompt delivered on stdin rather than as the trailing
|
|
418
|
+
* positional argument: a multi-paragraph argv element cannot survive cmd.exe on
|
|
419
|
+
* Windows. `-p` stays because for cursor-agent it means --print, not --prompt.
|
|
390
420
|
*
|
|
391
421
|
* Auth note: cursor-agent picks up either `cursor-agent login` credentials or
|
|
392
422
|
* `CURSOR_API_KEY` env var. PAL doesn't manage these — that's the user's setup.
|
|
393
423
|
*/
|
|
394
|
-
export function buildCursorArgs(
|
|
395
|
-
|
|
396
|
-
if (opts.system) parts.push(opts.system);
|
|
397
|
-
parts.push(opts.user);
|
|
398
|
-
if (opts.jsonSchema) {
|
|
399
|
-
parts.push(
|
|
400
|
-
`Respond with ONLY a JSON value matching this schema (no prose, no markdown): ${JSON.stringify(opts.jsonSchema)}`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
const prompt = parts.join("\n\n");
|
|
404
|
-
return ["-p", "--mode", "ask", "--output-format", "text", "--trust", prompt];
|
|
424
|
+
export function buildCursorArgs(_opts: InferenceOptions): string[] {
|
|
425
|
+
return ["-p", "--mode", "ask", "--output-format", "text", "--trust"];
|
|
405
426
|
}
|
|
406
427
|
|
|
407
428
|
/**
|
|
@@ -417,22 +438,14 @@ export function buildCursorArgs(opts: InferenceOptions): string[] {
|
|
|
417
438
|
* --allow-all-tools → REQUIRED for non-interactive mode (without it,
|
|
418
439
|
* copilot prompts for tool-use confirmation)
|
|
419
440
|
*
|
|
420
|
-
* Copilot has no --system-prompt flag
|
|
421
|
-
* concatenated into
|
|
441
|
+
* Copilot has no --system-prompt flag, so system + user + JSON-schema are
|
|
442
|
+
* concatenated into one prompt delivered on stdin. `-p/--prompt` is deliberately
|
|
443
|
+
* absent: it takes the prompt inline, and a multi-paragraph argv element cannot
|
|
444
|
+
* survive cmd.exe when Bun.spawn resolves copilot to its Windows .cmd shim.
|
|
445
|
+
* Piping stdin keeps copilot non-interactive, so dropping -p costs nothing.
|
|
422
446
|
*/
|
|
423
|
-
export function buildCopilotArgs(
|
|
424
|
-
const parts: string[] = [];
|
|
425
|
-
if (opts.system) parts.push(opts.system);
|
|
426
|
-
parts.push(opts.user);
|
|
427
|
-
if (opts.jsonSchema) {
|
|
428
|
-
parts.push(
|
|
429
|
-
`Respond with ONLY a JSON value matching this schema (no prose, no markdown): ${JSON.stringify(opts.jsonSchema)}`
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
const prompt = parts.join("\n\n");
|
|
447
|
+
export function buildCopilotArgs(_opts: InferenceOptions): string[] {
|
|
433
448
|
return [
|
|
434
|
-
"-p",
|
|
435
|
-
prompt,
|
|
436
449
|
"--no-custom-instructions",
|
|
437
450
|
"--disable-builtin-mcps",
|
|
438
451
|
"--no-auto-update",
|
|
@@ -465,12 +478,40 @@ export function extractOpencodeText(rawStdout: string): string {
|
|
|
465
478
|
return texts.join("").trim();
|
|
466
479
|
}
|
|
467
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Render a JSON schema for a prompt without double quotes.
|
|
483
|
+
*
|
|
484
|
+
* Bun.spawn resolves an agent CLI on Windows to its `.cmd` shim, which npm
|
|
485
|
+
* installs it as, and cmd.exe re-parses the command line it is handed. A double
|
|
486
|
+
* quote inside an argv element does not survive that round trip: the child exits
|
|
487
|
+
* non-zero having written nothing, so the dispatcher sees an empty abort and
|
|
488
|
+
* gives up. Single quotes carry the same shape to a model and are inert to
|
|
489
|
+
* cmd.exe, so the schema travels intact on every platform.
|
|
490
|
+
*/
|
|
491
|
+
function schemaForPrompt(schema: Record<string, unknown>): string {
|
|
492
|
+
return JSON.stringify(schema).replaceAll('"', "'");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The one instruction line that asks a CLI agent for schema-shaped JSON. */
|
|
496
|
+
export function schemaInstruction(schema: Record<string, unknown>): string {
|
|
497
|
+
return `Respond with ONLY a JSON value matching this schema (no prose, no markdown): ${schemaForPrompt(schema)}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** system + user + schema instruction, the single prompt a CLI agent receives. */
|
|
501
|
+
export function buildCliPrompt(opts: InferenceOptions): string {
|
|
502
|
+
const parts: string[] = [];
|
|
503
|
+
if (opts.system) parts.push(opts.system);
|
|
504
|
+
parts.push(opts.user);
|
|
505
|
+
if (opts.jsonSchema) parts.push(schemaInstruction(opts.jsonSchema));
|
|
506
|
+
return parts.join("\n\n");
|
|
507
|
+
}
|
|
508
|
+
|
|
468
509
|
/** Append a JSON-schema instruction to the system prompt (PAI pattern). */
|
|
469
510
|
export function injectJsonSchemaInstruction(
|
|
470
511
|
systemPrompt: string,
|
|
471
512
|
schema: Record<string, unknown>
|
|
472
513
|
): string {
|
|
473
|
-
const schemaLine =
|
|
514
|
+
const schemaLine = schemaInstruction(schema);
|
|
474
515
|
return systemPrompt ? `${systemPrompt}\n\n${schemaLine}` : schemaLine;
|
|
475
516
|
}
|
|
476
517
|
|