privateer-agent 0.12.34 → 0.12.36
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 +7 -0
- package/bin/apply-patches.mjs +29 -14
- package/bin/privateer-launch.mjs +244 -5
- package/bin/privateer-splash.mjs +2 -4
- package/bin/privateer-subagent.mjs +2 -0
- package/bin/startup-cache.d.mts +1 -0
- package/bin/startup-cache.mjs +19 -0
- package/extensions/privateer-models.ts +36 -9
- package/extensions/privateer-posture.ts +30 -1
- package/package.json +2 -2
- package/patches/@earendil-works+pi-ai+0.84.4.patch +46 -0
- package/patches/@earendil-works+pi-coding-agent+0.84.4.patch +88 -21
- package/patches/pi-background-tasks+2.4.2.patch +110 -0
- package/src/boot.ts +7 -0
- package/src/bridge/engineAdapter.ts +47 -49
- package/src/config/privacyDisabled.ts +45 -0
- package/src/config/privacyPolicy.ts +135 -40
- package/src/engine/contextBudget.ts +74 -0
- package/src/engine/errors.ts +89 -2
- package/src/engine/events.ts +3 -1
- package/src/providers/account.ts +205 -27
- package/src/providers/defaultModel.ts +4 -2
- package/src/remote/relayClient.ts +2 -0
package/README.md
CHANGED
|
@@ -171,6 +171,13 @@ for how you installed) or check your version with `privateer --version`.
|
|
|
171
171
|
> `~/.privateer/agent/settings.json`. Linux arm64 and Windows arm64 bundles aren't built
|
|
172
172
|
> yet — arm64 Windows runs the x64 bundle under emulation.
|
|
173
173
|
|
|
174
|
+
Background-task storage is created only when a task starts, not when you open Privateer.
|
|
175
|
+
Tasks normally write logs to `.pi/tasks` in your working directory. If that directory
|
|
176
|
+
denies writes (for example, Git Bash opened in `C:\Program Files\Git`), Privateer
|
|
177
|
+
uses `~/.privateer/agent/tasks/<project-id>/<run-id>` instead, respecting
|
|
178
|
+
`PRIVATEER_HOME`. The task's output path shows the actual location. This does **not**
|
|
179
|
+
change the working directory for commands or grant permission to edit protected files.
|
|
180
|
+
|
|
174
181
|
### Verifying what you're about to run
|
|
175
182
|
|
|
176
183
|
Privateer is a coding agent — it runs shell commands and edits files, so "should I trust
|
package/bin/apply-patches.mjs
CHANGED
|
@@ -67,13 +67,16 @@ export function resolveDep(from, name, ...rest) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function resolvePatchRoots(repo, patchFiles) {
|
|
70
|
-
const roots = new
|
|
70
|
+
const roots = new Map();
|
|
71
71
|
for (const file of patchFiles) {
|
|
72
72
|
// "@earendil-works+pi-coding-agent+0.80.3.patch" -> "@earendil-works/pi-coding-agent"
|
|
73
73
|
const parts = path.basename(file, ".patch").split("+");
|
|
74
74
|
const name = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
75
75
|
const root = findDepRoot(repo, name);
|
|
76
|
-
if (root)
|
|
76
|
+
if (root) {
|
|
77
|
+
if (!roots.has(root)) roots.set(root, []);
|
|
78
|
+
roots.get(root).push(file);
|
|
79
|
+
}
|
|
77
80
|
}
|
|
78
81
|
return [...roots];
|
|
79
82
|
}
|
|
@@ -112,23 +115,35 @@ export function applyPatchesIfNeeded(repo, nodeBin = process.execPath) {
|
|
|
112
115
|
if (!pp || !fs.existsSync(pp)) return "skipped";
|
|
113
116
|
|
|
114
117
|
let did = false;
|
|
115
|
-
for (const root of roots) {
|
|
118
|
+
for (const [root, files] of roots) {
|
|
116
119
|
const stampFile = path.join(root, "node_modules", ".privateer-patches.json");
|
|
117
120
|
try {
|
|
118
121
|
if (JSON.parse(fs.readFileSync(stampFile, "utf8")).hash === want) continue; // current
|
|
119
122
|
} catch { /* missing or unreadable stamp — (re)apply */ }
|
|
120
123
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
// Different targets can be hoisted to different roots. Apply only the patches
|
|
125
|
+
// belonging to this root, or patch-package fails on the absent/wrong copy of
|
|
126
|
+
// another target. Keep the staging dir on the SAME drive on Windows: --patch-dir
|
|
127
|
+
// must be relative (an absolute path is mangled by patch-package).
|
|
128
|
+
let stagedDir;
|
|
129
|
+
try {
|
|
130
|
+
let sourceDir = patchDir;
|
|
131
|
+
if (files.length !== patchFiles.length) {
|
|
132
|
+
stagedDir = fs.mkdtempSync(path.join(root, "node_modules", ".privateer-patches-"));
|
|
133
|
+
for (const file of files) fs.copyFileSync(path.join(patchDir, file), path.join(stagedDir, file));
|
|
134
|
+
sourceDir = stagedDir;
|
|
135
|
+
}
|
|
136
|
+
const relPatchDir = path.relative(root, sourceDir);
|
|
137
|
+
const r = spawnSync(nodeBin, [pp, "--error-on-fail", "--patch-dir", relPatchDir], {
|
|
138
|
+
cwd: root,
|
|
139
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
140
|
+
timeout: 60_000,
|
|
141
|
+
windowsHide: true,
|
|
142
|
+
});
|
|
143
|
+
if (r.status !== 0) return "failed";
|
|
144
|
+
} finally {
|
|
145
|
+
if (stagedDir) fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
132
147
|
did = true;
|
|
133
148
|
|
|
134
149
|
// Only stamp after a clean apply, so a partial/failed run retries next launch.
|
package/bin/privateer-launch.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
30
30
|
import { applyPatchesIfNeeded, resolveDep } from "./apply-patches.mjs";
|
|
31
31
|
import { routeUpdate } from "./update-route.mjs";
|
|
32
32
|
import { runToCompletion } from "./run-to-completion.mjs";
|
|
33
|
+
import { configureCompileCache } from "./startup-cache.mjs";
|
|
33
34
|
|
|
34
35
|
const HERE = path.dirname(fileURLToPath(import.meta.url)); // bin/
|
|
35
36
|
const REPO = path.resolve(HERE, "..");
|
|
@@ -41,15 +42,19 @@ const isWin = process.platform === "win32";
|
|
|
41
42
|
// Unicode wave blocks and emojis into 3-byte mojibake that wraps and floods the console.
|
|
42
43
|
if (isWin) {
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
: "chcp.com";
|
|
47
|
-
spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
|
|
45
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
46
|
+
spawnSync(comspec, ["/d", "/s", "/c", "chcp 65001 >nul"], { stdio: "inherit", windowsHide: true });
|
|
48
47
|
} catch {
|
|
49
48
|
/* best effort */
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
// Suppress upstream update checks: Pi's CLI update banner and pi-background-tasks'
|
|
53
|
+
// update check / footer notice ("bg ⬆ v... /bg-update"). Privateer manages updates
|
|
54
|
+
// at the distribution level via `privateer update` and its own banner.
|
|
55
|
+
if (!process.env.PI_SKIP_VERSION_CHECK) process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
56
|
+
if (!process.env.PI_BG_DISABLE_UPDATE_CHECK) process.env.PI_BG_DISABLE_UPDATE_CHECK = "1";
|
|
57
|
+
|
|
53
58
|
const PRIVATEER_HOME = process.env.PRIVATEER_HOME || path.join(os.homedir(), ".privateer");
|
|
54
59
|
const ENV_FILE = path.join(REPO, ".env"); // dev-only; a real install has none
|
|
55
60
|
const AGENT_DIR = path.join(PRIVATEER_HOME, "agent");
|
|
@@ -165,6 +170,26 @@ if (NO_QUARTER) {
|
|
|
165
170
|
);
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
// `--no-privacy` / `--privacy-off` — completely disable pi-privacy for this session.
|
|
174
|
+
// Stripped before Pi's cli.js sees it. Exports PRIVATEER_PRIVACY_OFF=1 and PI_PRIVACY_OFF=1;
|
|
175
|
+
// all PII scanning, prompts, and tool exfiltration/result checks are bypassed.
|
|
176
|
+
const NO_PRIVACY = args.some((a) => a === "--no-privacy" || a === "--privacy-off");
|
|
177
|
+
if (NO_PRIVACY) {
|
|
178
|
+
for (let i = args.length - 1; i >= 0; i--) if (args[i] === "--no-privacy" || args[i] === "--privacy-off") args.splice(i, 1);
|
|
179
|
+
process.env.PRIVATEER_PRIVACY_OFF = "1";
|
|
180
|
+
process.env.PI_PRIVACY_OFF = "1";
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
[
|
|
183
|
+
"",
|
|
184
|
+
" ⚓ \x1b[1;33mPrivacy off\x1b[0m — pi-privacy DISABLED for this session.",
|
|
185
|
+
" PII scanning, tool exfiltration guards, and result credential redaction are OFF.",
|
|
186
|
+
" Outbound requests and tool calls will NOT prompt or redact PII / IP addresses.",
|
|
187
|
+
" Run /privacy on in-session to restore privacy protections.",
|
|
188
|
+
"",
|
|
189
|
+
].join("\n") + "\n",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
168
193
|
// `--allow-computer-control` — arm GUI control (screenshots, mouse, keyboard) for this
|
|
169
194
|
// session. Stripped before Pi's cli.js sees it, exactly like --no-quarter above.
|
|
170
195
|
//
|
|
@@ -348,6 +373,134 @@ if (sub === "update") {
|
|
|
348
373
|
else updateSelf();
|
|
349
374
|
}
|
|
350
375
|
|
|
376
|
+
// --- `privateer init [directory]` ------------------------------------------
|
|
377
|
+
// Scaffold a starter PRIVATEER.md project-context file. Runs synchronously and exits
|
|
378
|
+
// immediately — no Pi boot, no splash screen, no API keys needed.
|
|
379
|
+
else if (sub === "init") {
|
|
380
|
+
const rest = args.slice(1);
|
|
381
|
+
const help = rest.includes("--help") || rest.includes("-h");
|
|
382
|
+
const cmd = process.env.PRIVATEER_CMD || "privateer";
|
|
383
|
+
if (help) {
|
|
384
|
+
console.log(
|
|
385
|
+
[
|
|
386
|
+
`${cmd} init — scaffold a starter PRIVATEER.md project-context file.`,
|
|
387
|
+
"",
|
|
388
|
+
`Usage: ${cmd} init [directory]`,
|
|
389
|
+
"",
|
|
390
|
+
"Options:",
|
|
391
|
+
" -h, --help Show this help message",
|
|
392
|
+
"",
|
|
393
|
+
"Creates a starter PRIVATEER.md in the current directory (or the specified directory).",
|
|
394
|
+
"Privateer loads this file automatically at startup to give the agent project context.",
|
|
395
|
+
].join("\n"),
|
|
396
|
+
);
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const unknownOpt = rest.find((a) => a.startsWith("-"));
|
|
401
|
+
if (unknownOpt) {
|
|
402
|
+
console.error(`privateer init: unknown option '${unknownOpt}'`);
|
|
403
|
+
console.error(`Run '${cmd} init --help' for usage.`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const positionals = rest.filter((a) => !a.startsWith("-"));
|
|
408
|
+
if (positionals.length > 1) {
|
|
409
|
+
console.error(`privateer init: unexpected argument '${positionals[1]}'`);
|
|
410
|
+
console.error(`Run '${cmd} init --help' for usage.`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const targetDir = positionals[0] ? path.resolve(positionals[0]) : process.cwd();
|
|
415
|
+
if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
|
|
416
|
+
console.error(`privateer init: directory not found '${targetDir}'`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const targetFile = path.join(targetDir, "PRIVATEER.md");
|
|
421
|
+
if (fs.existsSync(targetFile)) {
|
|
422
|
+
console.log(`PRIVATEER.md already exists at ${targetFile} — left untouched.`);
|
|
423
|
+
process.exit(0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const template = [
|
|
427
|
+
"# PRIVATEER.md",
|
|
428
|
+
"",
|
|
429
|
+
"Project context for the Privateer agent. Privateer loads this file automatically at",
|
|
430
|
+
"startup (the same way it loads AGENTS.md / CLAUDE.md) and prepends it to the model's",
|
|
431
|
+
"system prompt — so put anything the agent should always know about THIS project here.",
|
|
432
|
+
"",
|
|
433
|
+
"## Project",
|
|
434
|
+
"",
|
|
435
|
+
"<One or two lines: what this project is and what it does.>",
|
|
436
|
+
"",
|
|
437
|
+
"## Conventions",
|
|
438
|
+
"",
|
|
439
|
+
"- <Coding style, patterns, and idioms to follow.>",
|
|
440
|
+
"- <Things to avoid.>",
|
|
441
|
+
"",
|
|
442
|
+
"## Commands",
|
|
443
|
+
"",
|
|
444
|
+
"- build: <command>",
|
|
445
|
+
"- test: <command>",
|
|
446
|
+
"- run: <command>",
|
|
447
|
+
"",
|
|
448
|
+
"## Notes for the agent",
|
|
449
|
+
"",
|
|
450
|
+
"- <Domain context, gotchas, or constraints worth stating once.>",
|
|
451
|
+
"",
|
|
452
|
+
].join("\n");
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
fs.writeFileSync(targetFile, template, "utf8");
|
|
456
|
+
console.log(`\x1b[32m✓\x1b[0m Created ${targetFile}`);
|
|
457
|
+
console.log(" Edit it with your project's context — it loads automatically on every turn.");
|
|
458
|
+
process.exit(0);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error(`privateer init: failed to write ${targetFile} — ${err.message}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// --- `privateer help [command]` / `privateer --help` ------------------------
|
|
466
|
+
else if (sub === "help" || (args.length === 1 && (args[0] === "--help" || args[0] === "-h"))) {
|
|
467
|
+
const target = sub === "help" ? args[1] : undefined;
|
|
468
|
+
const cmd = process.env.PRIVATEER_CMD || "privateer";
|
|
469
|
+
if (target === "init") {
|
|
470
|
+
console.log(
|
|
471
|
+
[
|
|
472
|
+
`${cmd} init — scaffold a starter PRIVATEER.md project-context file.`,
|
|
473
|
+
"",
|
|
474
|
+
`Usage: ${cmd} init [directory]`,
|
|
475
|
+
"",
|
|
476
|
+
"Options:",
|
|
477
|
+
" -h, --help Show this help message",
|
|
478
|
+
"",
|
|
479
|
+
"Creates a starter PRIVATEER.md in the current directory (or the specified directory).",
|
|
480
|
+
"Privateer loads this file automatically at startup to give the agent project context.",
|
|
481
|
+
].join("\n"),
|
|
482
|
+
);
|
|
483
|
+
process.exit(0);
|
|
484
|
+
}
|
|
485
|
+
if (target === "update") {
|
|
486
|
+
console.log(
|
|
487
|
+
[
|
|
488
|
+
`${cmd} update — fetch the latest release, or newer tool packs.`,
|
|
489
|
+
"",
|
|
490
|
+
` ${cmd} update update the Privateer CLI itself`,
|
|
491
|
+
` ${cmd} update --extensions update every installed tool pack`,
|
|
492
|
+
` ${cmd} update <pack> update one pack (npm name or git URL)`,
|
|
493
|
+
` ${cmd} update --all tool packs, then the CLI`,
|
|
494
|
+
"",
|
|
495
|
+
"Inside a running terminal, /update fetches tool packs in place — no restart.",
|
|
496
|
+
].join("\n"),
|
|
497
|
+
);
|
|
498
|
+
process.exit(0);
|
|
499
|
+
}
|
|
500
|
+
printPrivateerHelp(cmd);
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
|
|
351
504
|
// --- `privateer harbor [run|install|uninstall|status]` ---------------------
|
|
352
505
|
// The resident background harbor (routines + app-driven headless task spawns). Boots
|
|
353
506
|
// straight into src/harbor via bin/privateer-harbor.mjs — the harbor loads the moat as
|
|
@@ -390,6 +543,10 @@ else if (sub === "acp") {
|
|
|
390
543
|
|
|
391
544
|
// --- normal launch: resolve the moat, then exec Pi's TUI with it -----------
|
|
392
545
|
else {
|
|
546
|
+
// Node 22 can reuse compiled code across launches instead of reparsing Pi and every
|
|
547
|
+
// provider SDK. Set the child env before any Node spawn; children inherit it too.
|
|
548
|
+
configureCompileCache(PRIVATEER_HOME);
|
|
549
|
+
|
|
393
550
|
// Windows has no bash out of the box, but Privateer's command tool needs one. If a
|
|
394
551
|
// real bash isn't reachable, stop here with a clear, actionable message — otherwise
|
|
395
552
|
// the user boots fine and only hits a cryptic `'bash' is not recognized` the first
|
|
@@ -447,6 +604,7 @@ else {
|
|
|
447
604
|
// Suppress Pi's upstream update banner (our banner is the startup surface). Disables
|
|
448
605
|
// ONLY the version fetch — fd/rg can still download on first run.
|
|
449
606
|
if (!process.env.PI_SKIP_VERSION_CHECK) process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
607
|
+
if (!process.env.PI_BG_DISABLE_UPDATE_CHECK) process.env.PI_BG_DISABLE_UPDATE_CHECK = "1";
|
|
450
608
|
|
|
451
609
|
// Quiet Pi's built-in startup chatter so our banner is the only greeting. Each key is
|
|
452
610
|
// set only when unset, so a user's own settings.json toggle still wins.
|
|
@@ -551,8 +709,21 @@ else {
|
|
|
551
709
|
// TUI branch only: harbor/acp/subagent children have no terminal to animate on (and
|
|
552
710
|
// acp's stdout is a JSON-RPC stream). pathToFileURL, not the bare path — a Windows
|
|
553
711
|
// absolute path reads as the URL scheme "d:"; see bin/privateer.mjs for the same trap.
|
|
712
|
+
const isNonInteractive = args.some(
|
|
713
|
+
(a) =>
|
|
714
|
+
a === "-p" ||
|
|
715
|
+
a === "--print" ||
|
|
716
|
+
a === "--mode" ||
|
|
717
|
+
a === "--export" ||
|
|
718
|
+
a === "--list-models" ||
|
|
719
|
+
a === "--help" ||
|
|
720
|
+
a === "-h" ||
|
|
721
|
+
a === "-v" ||
|
|
722
|
+
a === "-V" ||
|
|
723
|
+
a === "--version",
|
|
724
|
+
);
|
|
554
725
|
const splash = path.join(HERE, "privateer-splash.mjs");
|
|
555
|
-
if (fs.existsSync(splash)) nodeArgs.push("--import", pathToFileURL(splash).href);
|
|
726
|
+
if (!isNonInteractive && fs.existsSync(splash)) nodeArgs.push("--import", pathToFileURL(splash).href);
|
|
556
727
|
|
|
557
728
|
runToCompletion(NODE_BIN, [...nodeArgs, CLI, ...modelArgs, ...extArgs, ...skillArgs, ...args]);
|
|
558
729
|
}
|
|
@@ -701,6 +872,74 @@ function warnKeylessLaunch() {
|
|
|
701
872
|
process.stderr.write(lines.join("\n") + "\n");
|
|
702
873
|
}
|
|
703
874
|
|
|
875
|
+
function printPrivateerHelp(cmd = process.env.PRIVATEER_CMD || "privateer") {
|
|
876
|
+
console.log(
|
|
877
|
+
[
|
|
878
|
+
"privateer - Terminal & TUI coding agent with verified privacy and local control",
|
|
879
|
+
"",
|
|
880
|
+
"Usage:",
|
|
881
|
+
` ${cmd} [options] [--] [@files...] [messages...]`,
|
|
882
|
+
"",
|
|
883
|
+
"Commands:",
|
|
884
|
+
` ${cmd} init [directory] Create a starter PRIVATEER.md project-context file`,
|
|
885
|
+
` ${cmd} update [--all] Fetch the latest release or newer tool packs`,
|
|
886
|
+
` ${cmd} harbor <command> Manage the resident background Harbor daemon (run/install/uninstall/status)`,
|
|
887
|
+
` ${cmd} verify Check local installation integrity and patch state`,
|
|
888
|
+
` ${cmd} acp Run as an Agent Client Protocol server (JSON-RPC on stdio)`,
|
|
889
|
+
` ${cmd} install <source> Install extension source and add to settings`,
|
|
890
|
+
` ${cmd} remove <source> Remove extension source from settings`,
|
|
891
|
+
` ${cmd} uninstall <source> Alias for remove`,
|
|
892
|
+
` ${cmd} list List installed extensions from settings`,
|
|
893
|
+
` ${cmd} config Open TUI to enable/disable package resources`,
|
|
894
|
+
` ${cmd} auth <command> Print credentials or check provider readiness`,
|
|
895
|
+
` ${cmd} <command> --help Show help for a specific command`,
|
|
896
|
+
"",
|
|
897
|
+
"Options:",
|
|
898
|
+
" --provider <name> Provider name",
|
|
899
|
+
' --model <pattern> Model pattern or ID (supports "provider/id" and optional ":<thinking>")',
|
|
900
|
+
" --api-key <key> API key (defaults to env vars)",
|
|
901
|
+
" --system-prompt <text> System prompt (default: coding assistant prompt)",
|
|
902
|
+
" --append-system-prompt <text> Append text or file contents to the system prompt",
|
|
903
|
+
" --mode <mode> Output mode: text (default), json, or rpc",
|
|
904
|
+
" --print, -p Non-interactive mode: process prompt and exit",
|
|
905
|
+
" --continue, -c Continue previous session",
|
|
906
|
+
" --resume, -r Select a session to resume",
|
|
907
|
+
" --session <path|id> Use specific session file or partial UUID",
|
|
908
|
+
" --session-id <id> Use exact project session ID, creating it if missing",
|
|
909
|
+
" --fork <path|id> Fork specific session file or partial UUID into a new session",
|
|
910
|
+
" --session-dir <dir> Directory for session storage and lookup",
|
|
911
|
+
" --no-session Don't save session (ephemeral)",
|
|
912
|
+
" --name, -n <name> Set session display name",
|
|
913
|
+
" --models <patterns> Comma-separated model patterns for Ctrl+P cycling",
|
|
914
|
+
" --no-tools, -nt Disable all tools by default (built-in and extension)",
|
|
915
|
+
" --no-builtin-tools, -nbt Disable built-in tools by default",
|
|
916
|
+
" --tools, -t <tools> Comma-separated allowlist of tool names to enable",
|
|
917
|
+
" --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable",
|
|
918
|
+
" --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max",
|
|
919
|
+
" --extension, -e <path> Load an extension file (can be used multiple times)",
|
|
920
|
+
" --no-extensions, -ne Disable extension discovery (explicit -e paths still work)",
|
|
921
|
+
" --skill <path> Load a skill file or directory (can be used multiple times)",
|
|
922
|
+
" --no-skills, -ns Disable skills discovery and loading",
|
|
923
|
+
" --prompt-template <path> Load a prompt template file or directory",
|
|
924
|
+
" --no-prompt-templates, -np Disable prompt template discovery and loading",
|
|
925
|
+
" --theme <path> Load a theme file or directory",
|
|
926
|
+
" --use-theme <name[/name]> Set the initial interactive theme for this run",
|
|
927
|
+
" --no-themes Disable theme discovery and loading",
|
|
928
|
+
" --no-context-files, -nc Disable PRIVATEER.md, AGENTS.md and CLAUDE.md discovery",
|
|
929
|
+
" --export <file> Export session file to HTML and exit",
|
|
930
|
+
" --list-models [search] List available models (with optional fuzzy search)",
|
|
931
|
+
" --verbose Force verbose startup",
|
|
932
|
+
" --tui-mode <mode> TUI mode: regular (default) or fullscreen",
|
|
933
|
+
" --approve, -a Trust project-local files for this run",
|
|
934
|
+
" --no-approve, -na Ignore project-local files for this run",
|
|
935
|
+
" --offline Disable startup network operations",
|
|
936
|
+
" -- End option parsing; treat remaining arguments as messages/files",
|
|
937
|
+
" --help, -h Show this help",
|
|
938
|
+
" --version, -v, -V Show version number",
|
|
939
|
+
].join("\n"),
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
704
943
|
function refreshUpdateCache() {
|
|
705
944
|
const cache = path.join(PRIVATEER_HOME, "update-check.json");
|
|
706
945
|
try {
|
package/bin/privateer-splash.mjs
CHANGED
|
@@ -64,10 +64,8 @@ const enabled =
|
|
|
64
64
|
// than privateer.cmd, the console might still be on OEM CP437.
|
|
65
65
|
if (enabled && process.platform === "win32") {
|
|
66
66
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
: "chcp.com";
|
|
70
|
-
spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
|
|
67
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
68
|
+
spawnSync(comspec, ["/d", "/s", "/c", "chcp 65001 >nul"], { stdio: "inherit", windowsHide: true });
|
|
71
69
|
} catch {
|
|
72
70
|
/* best effort */
|
|
73
71
|
}
|
|
@@ -74,6 +74,8 @@ export function buildChildArgs(originalArgs, repoRoot = REPO, env = process.env)
|
|
|
74
74
|
|
|
75
75
|
// Run only when invoked directly (not when imported by the test).
|
|
76
76
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
77
|
+
if (!process.env.PI_SKIP_VERSION_CHECK) process.env.PI_SKIP_VERSION_CHECK = "1";
|
|
78
|
+
if (!process.env.PI_BG_DISABLE_UPDATE_CHECK) process.env.PI_BG_DISABLE_UPDATE_CHECK = "1";
|
|
77
79
|
const args = buildChildArgs(process.argv.slice(2));
|
|
78
80
|
const child = spawn(process.execPath, [piCliPath(), ...args], { stdio: "inherit", env: process.env });
|
|
79
81
|
// Propagate the child's exit faithfully so pi-subagents' parent reads the real
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function configureCompileCache(privateerHome: string, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Set this BEFORE spawning Node: enabling it after importing Pi misses the expensive
|
|
5
|
+
// module graph. Node keys bytecode by runtime version and source contents; /reload
|
|
6
|
+
// still evaluates fresh modules. This is not Jiti's in-memory module cache.
|
|
7
|
+
// Keep it outside the install/project (both may be read-only), under the same home
|
|
8
|
+
// as the rest of this Privateer instance. Only code is cached, not session state.
|
|
9
|
+
export function configureCompileCache(privateerHome, env = process.env) {
|
|
10
|
+
// Respect Node's own override/opt-out, including an explicitly empty cache path.
|
|
11
|
+
if (env.NODE_COMPILE_CACHE !== undefined || env.NODE_DISABLE_COMPILE_CACHE === "1") return;
|
|
12
|
+
try {
|
|
13
|
+
const dir = path.resolve(privateerHome, "cache", "node-compile");
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
15
|
+
env.NODE_COMPILE_CACHE = dir;
|
|
16
|
+
} catch {
|
|
17
|
+
// An unavailable cache must never prevent startup (or print on ACP stdout).
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
accountPosture,
|
|
31
31
|
fetchAccountCatalog,
|
|
32
32
|
} from "../src/providers/account.ts";
|
|
33
|
+
import { writePiDefaultModel } from "../src/providers/defaultModel.ts";
|
|
33
34
|
|
|
34
35
|
// A minimal view of Pi's theme (passed to the ui.custom factory) — enough to color
|
|
35
36
|
// text without pulling Pi's internal theme types into an extension.
|
|
@@ -353,11 +354,7 @@ export default function privateerModels(pi: {
|
|
|
353
354
|
argumentHint: "[search]",
|
|
354
355
|
handler: async (args: string, ctx: any): Promise<void> => {
|
|
355
356
|
const ui = ctx?.ui;
|
|
356
|
-
|
|
357
|
-
ctx?.ui?.notify?.("The model picker needs an interactive terminal.", "warning");
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const registry = ctx.modelRegistry;
|
|
357
|
+
const registry = ctx?.modelRegistry;
|
|
361
358
|
// Refresh so a just-fetched account catalog / models.json edit is reflected.
|
|
362
359
|
try {
|
|
363
360
|
registry?.refresh?.();
|
|
@@ -376,13 +373,43 @@ export default function privateerModels(pi: {
|
|
|
376
373
|
|
|
377
374
|
let available: ModelLike[] = [];
|
|
378
375
|
try {
|
|
379
|
-
available = (await registry
|
|
376
|
+
available = (await registry?.getAvailable?.()) as ModelLike[] ?? [];
|
|
380
377
|
} catch (e) {
|
|
381
|
-
ui
|
|
378
|
+
ui?.notify?.(`Couldn't list models: ${(e as Error).message}`, "error");
|
|
382
379
|
return;
|
|
383
380
|
}
|
|
384
381
|
if (!available.length) {
|
|
385
|
-
ui
|
|
382
|
+
ui?.notify?.("No models available. Use /login to add a provider.", "warning");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const initialQuery = String(args ?? "").trim();
|
|
387
|
+
if (!ui?.custom) {
|
|
388
|
+
if (initialQuery) {
|
|
389
|
+
const match = available.find(
|
|
390
|
+
(m) => `${m.provider}/${m.id}` === initialQuery || m.id === initialQuery,
|
|
391
|
+
);
|
|
392
|
+
if (match) {
|
|
393
|
+
try {
|
|
394
|
+
const ok = await pi.setModel!(match);
|
|
395
|
+
if (ok === false) {
|
|
396
|
+
ui?.notify?.(`No API key for ${match.provider}/${match.id}.`, "warning");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
writePiDefaultModel(`${match.provider}/${match.id}`);
|
|
400
|
+
const tier = baselineTier(match.provider, match.id);
|
|
401
|
+
ui?.notify?.(
|
|
402
|
+
`Model: ${match.provider}/${match.id} · ${TIERS[tier].label}`,
|
|
403
|
+
"info",
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
ui?.notify?.(`Couldn't switch model: ${(e as Error).message}`, "error");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
ui?.notify?.("The model picker needs an interactive terminal.", "warning");
|
|
386
413
|
return;
|
|
387
414
|
}
|
|
388
415
|
|
|
@@ -394,7 +421,6 @@ export default function privateerModels(pi: {
|
|
|
394
421
|
tier: baselineTier(m.provider, m.id),
|
|
395
422
|
}));
|
|
396
423
|
|
|
397
|
-
const initialQuery = String(args ?? "").trim();
|
|
398
424
|
const chosen: Row | undefined = await ui.custom(
|
|
399
425
|
(tui: TuiLike, theme: ThemeLike, _kb: unknown, close: (result?: Row) => void) => {
|
|
400
426
|
const picker = new ModelsPicker({
|
|
@@ -417,6 +443,7 @@ export default function privateerModels(pi: {
|
|
|
417
443
|
ui.notify?.(`No API key for ${chosen.provider}/${chosen.id}.`, "warning");
|
|
418
444
|
return;
|
|
419
445
|
}
|
|
446
|
+
writePiDefaultModel(`${chosen.provider}/${chosen.id}`);
|
|
420
447
|
ui.notify?.(
|
|
421
448
|
`Model: ${chosen.provider}/${chosen.id} · ${TIERS[chosen.tier].label}`,
|
|
422
449
|
"info",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { verifyModelPosture, TIERS, type PrivacyTier } from "pi-privacy";
|
|
10
10
|
import { accountPosture } from "../src/providers/account.ts";
|
|
11
11
|
import { type Palette, paletteFor } from "../src/ui/palette.ts";
|
|
12
|
+
import { privacyDisabled } from "../src/config/privacyDisabled.ts";
|
|
12
13
|
|
|
13
14
|
const DOT: Record<string, string> = { green: "🟢", yellow: "🟡", red: "🔴", neutral: "⚪" };
|
|
14
15
|
|
|
@@ -35,11 +36,39 @@ async function badgeFor(provider: string, modelId: string, p: Palette): Promise<
|
|
|
35
36
|
return `${DOT[info.posture] ?? "⚪"} ${info.label}`;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export async function updatePostureBadge(ctx: any, provider?: string, modelId?: string): Promise<void> {
|
|
40
|
+
if (!ctx?.ui?.setStatus) return;
|
|
41
|
+
if (privacyDisabled()) {
|
|
42
|
+
ctx.ui.setStatus("privacy", "⚑ privacy off");
|
|
43
|
+
ctx.ui.setStatus("pi-privacy", undefined);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const p = provider ?? ctx?.model?.provider;
|
|
47
|
+
const m = modelId ?? ctx?.model?.id;
|
|
48
|
+
if (!p || !m) {
|
|
49
|
+
ctx.ui.setStatus("privacy", undefined);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
ctx.ui.setStatus("privacy", "⛉ …");
|
|
54
|
+
const badge = await badgeFor(p, m, paletteFor(ctx?.ui?.theme));
|
|
55
|
+
ctx.ui.setStatus("privacy", badge);
|
|
56
|
+
} catch {
|
|
57
|
+
ctx.ui.setStatus("privacy", undefined);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
export default function privateerPosture(pi: any): void {
|
|
39
62
|
// "latest wins" so rapid model cycling (Ctrl+P) doesn't leave a stale badge.
|
|
40
63
|
let seq = 0;
|
|
41
64
|
const update = async (provider?: string, modelId?: string, ctx?: any) => {
|
|
42
|
-
if (!
|
|
65
|
+
if (!ctx?.ui?.setStatus) return;
|
|
66
|
+
if (privacyDisabled()) {
|
|
67
|
+
ctx.ui.setStatus("privacy", "⚑ privacy off");
|
|
68
|
+
ctx.ui.setStatus("pi-privacy", undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!provider || !modelId) return;
|
|
43
72
|
const mine = ++seq;
|
|
44
73
|
try {
|
|
45
74
|
ctx.ui.setStatus("privacy", "⛉ …"); // immediate placeholder while attesting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.36",
|
|
4
4
|
"description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"privateer-workflow": "0.1.0",
|
|
91
91
|
"tinfoil": "1.1.11",
|
|
92
92
|
"tsx": "4.23.0",
|
|
93
|
-
"typebox": "1.3.
|
|
93
|
+
"typebox": "1.3.7",
|
|
94
94
|
"undici": "7.29.0",
|
|
95
95
|
"ws": "8.21.0",
|
|
96
96
|
"yaml": "2.9.0",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
diff --git a/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js b/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js
|
|
2
|
+
index 216a45f..a1e3214 100644
|
|
3
|
+
--- a/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js
|
|
4
|
+
+++ b/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js
|
|
5
|
+
@@ -1,11 +1,38 @@
|
|
6
|
+
import { estimateContextTokens } from "../utils/estimate.js";
|
|
7
|
+
const CONTEXT_SAFETY_TOKENS = 4096;
|
|
8
|
+
-const MIN_MAX_TOKENS = 1;
|
|
9
|
+
+// Privateer patch: floor the answer budget at a usable answer, not at one token.
|
|
10
|
+
+//
|
|
11
|
+
+// Stock pi floored this at 1, so the moment the (chars/4) context estimate reached
|
|
12
|
+
+// the declared window every turn asked the provider for ONE token: the whole prompt
|
|
13
|
+
+// still sent, still billed, one token back with finish_reason "length" — which the
|
|
14
|
+
+// TUI renders as "Response was truncated before completion." Compaction gets one
|
|
15
|
+
+// attempt, and when the estimate is still over afterwards the session stays there,
|
|
16
|
+
+// burning a full-price prompt per turn and producing nothing. Privateer reaches it
|
|
17
|
+
+// early because every account model is registered with a flat contextWindow of
|
|
18
|
+
+// 128000 (src/providers/account.ts seedModel) — /api/models publishes no per-model
|
|
19
|
+
+// window — so a larger-windowed model hits this ceiling with room to spare.
|
|
20
|
+
+//
|
|
21
|
+
+// The floor only raises `available`; the Math.min still honours a deliberately small
|
|
22
|
+
+// ask (a summariser, a title), and it is smaller than the CONTEXT_SAFETY_TOKENS
|
|
23
|
+
+// already subtracted above, so a floored request was inside that margin anyway. When
|
|
24
|
+
+// `available` is genuinely negative the context really does not fit and the provider
|
|
25
|
+
+// says so — a context-length error, which pi routes to compaction via
|
|
26
|
+
+// isContextOverflow. That is the designed recovery, and strictly better than a
|
|
27
|
+
+// silent one-token stub that costs the same.
|
|
28
|
+
+//
|
|
29
|
+
+// The floor is MIN_ANSWER_TOKENS, this module's OWN constant — declared below at its
|
|
30
|
+
+// original site, deliberately not redeclared here (a second top-level `const` of that
|
|
31
|
+
+// name is a SyntaxError that takes the whole app down). Reading it from below is safe:
|
|
32
|
+
+// clampMaxTokensToContext is only ever called at request time, long after module
|
|
33
|
+
+// evaluation, so the temporal dead zone has closed. Same value, same meaning, and it
|
|
34
|
+
+// stays in sync if upstream retunes it.
|
|
35
|
+
+//
|
|
36
|
+
+// Mirrors src/engine/contextBudget.ts, which is where this is specified and tested.
|
|
37
|
+
export function clampMaxTokensToContext(model, context, maxTokens) {
|
|
38
|
+
if (model.contextWindow <= 0)
|
|
39
|
+
- return Math.max(MIN_MAX_TOKENS, maxTokens);
|
|
40
|
+
+ return Math.max(1, maxTokens);
|
|
41
|
+
const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;
|
|
42
|
+
- return Math.min(maxTokens, Math.max(MIN_MAX_TOKENS, available));
|
|
43
|
+
+ return Math.min(maxTokens, Math.max(MIN_ANSWER_TOKENS, available));
|
|
44
|
+
}
|
|
45
|
+
export function buildBaseOptions(model, context, options, apiKey) {
|
|
46
|
+
const samplingParams = model.samplingParams || options?.samplingParams
|