privateer-agent 0.14.5 → 0.15.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 CHANGED
@@ -78,7 +78,7 @@ What Privateer adds is a *moat* of Pi extensions layered on top:
78
78
  | `privateer-posture`, `privateer-tools` | live attestation shield + Privateer tool pack |
79
79
  | `privateer-web` | `web_search`/`web_fetch` — your account's search once signed in, or your own provider (`rpiv-web-tools`: self-hosted SearXNG, Brave, Tavily…) |
80
80
  | `rpiv-ask-user-question` | `ask_user_question` — a structured questionnaire the agent puts to you instead of guessing |
81
- | `pi-mcp-adapter`, `pi-subagents` | MCP servers · bounded parallel sub-agents |
81
+ | `privateer-codemode`, `pi-subagents` | codemode (how MCP tools are called — Pi's built-in MCP support) · bounded parallel sub-agents |
82
82
 
83
83
  They're ordinary Pi extensions — inspect them, replace them, or build your own alongside.
84
84
  **Extend by discovery:** drop an extension into `~/.privateer/agent/extensions/` (move the home
@@ -641,7 +641,7 @@ built-ins. Two kinds:
641
641
 
642
642
  ```
643
643
  /connect # add, enable, disable, or remove connectors
644
- /mcp # pi-mcp-adapter's own status view — what actually connected
644
+ /mcp # Pi's own status view — what connected; `/mcp login <name>` to sign in
645
645
  ```
646
646
 
647
647
  `/connect` opens a picker over a curated catalog of 21 connectors — GitHub, Slack, Notion,
@@ -748,9 +748,9 @@ plan-mode deny is the one thing it doesn't talk around.
748
748
  Everything below is a **Pi extension** loaded by discovery (see [Built on Pi](#built-on-pi)) —
749
749
  drop your own into `~/.privateer/agent/extensions/` and it loads the same way, gated like the rest.
750
750
 
751
- - **MCP servers** (`pi-mcp-adapter`) — declare them and their tools become first-class, gated
752
- like the rest (local stdio, or remote HTTP with interactive OAuth). Add them with
753
- `/connect`; see [Connectors — MCP](#connectors--mcp).
751
+ - **MCP servers** (Pi's built-in MCP support) — declare them and the model reaches their tools
752
+ through codemode, each call gated like the rest (local stdio, or remote streamable HTTP with
753
+ interactive OAuth). Add them with `/connect`; see [Connectors — MCP](#connectors--mcp).
754
754
  - **Sub-agents** (`pi-subagents`) — delegate investigations to bounded parallel agents. Children
755
755
  run as headless child processes that **inherit the moat**, so their actions hit the same
756
756
  permission gate and their approvals surface on your phone.
@@ -30,3 +30,13 @@ export function extractPrivateFlag(args: string[]): boolean;
30
30
  export function privateProblem(args: string[]): string | null;
31
31
 
32
32
  export function authProblem(args: string[], cmd?: string): string | null;
33
+
34
+ /** `auth check|print-*` for the privateer provider, answered by the launcher; null for Pi. */
35
+ export function privateerAuthAnswer(
36
+ args: string[],
37
+ signedIn: boolean,
38
+ cmd?: string,
39
+ ): { out: string; err: string; code: number } | null;
40
+
41
+ /** `auth --help` with our command name and the provider agents look for. */
42
+ export function authHelp(cmd?: string): string;
@@ -180,7 +180,7 @@ export function privateProblem(args) {
180
180
  // Pi's own subcommands. Pi recognizes each only as args[0], so the launcher must hand
181
181
  // them over with NOTHING in front — the normal launch prepends --model, -e and --skill,
182
182
  // which is exactly how `privateer auth check` used to become a chat message.
183
- export const PI_SUBCOMMANDS = ["auth", "install", "remove", "uninstall", "list", "config"];
183
+ export const PI_SUBCOMMANDS = ["auth", "install", "remove", "uninstall", "list", "config", "mcp"];
184
184
 
185
185
  const AUTH_SUBCOMMANDS = ["check", "print-api-key", "print-bearer-token"];
186
186
 
@@ -204,3 +204,74 @@ export function authProblem(args, cmd = "privateer") {
204
204
  `Nothing was sent to a model. To sign in, run \`${cmd}\` and type /login.`,
205
205
  ].join("\n");
206
206
  }
207
+
208
+ // The value of --provider / --model in `args`, in either `--flag v` or `--flag=v` form.
209
+ function flagValue(args, flag) {
210
+ for (let i = 0; i < args.length; i++) {
211
+ if (args[i] === flag) return args[i + 1];
212
+ if (args[i].startsWith(flag + "=")) return args[i].slice(flag.length + 1);
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ /**
218
+ * `auth check|print-api-key|print-bearer-token` for the `privateer` provider, answered
219
+ * here. Pi runs these with no extensions loaded, and `privateer` is registered by ours,
220
+ * so Pi always said `provider_not_found` — and agents probing a signed-in machine with
221
+ * the exact command `auth status` suggests concluded it was signed out. Readiness comes
222
+ * from the account login (the same file `auth status` reads); the per-launch session
223
+ * in auth.json is minted from it. The account token is never printed: it's the login
224
+ * itself, and nothing outside Privateer can use it as a provider key anyway.
225
+ *
226
+ * Returns { out, err, code } to print, or null for Pi to handle.
227
+ */
228
+ export function privateerAuthAnswer(args, signedIn, cmd = "privateer") {
229
+ if (args[0] !== "auth" || !AUTH_SUBCOMMANDS.includes(args[1])) return null;
230
+ if (args.includes("--help") || args.includes("-h")) return null;
231
+ const provider = flagValue(args, "--provider");
232
+ const model = flagValue(args, "--model");
233
+ if (provider !== "privateer" && !(provider === undefined && model?.startsWith("privateer/"))) return null;
234
+
235
+ const run = `${cmd} -p "<task>"`;
236
+ if (args[1] !== "check") {
237
+ return {
238
+ out: "",
239
+ err: [
240
+ `${cmd} auth ${args[1]}: the privateer provider's credential is your account login, and it isn't printed.`,
241
+ signedIn ? `This machine is signed in — run a task with: ${run}` : `Sign in first: run \`${cmd}\` and type /login.`,
242
+ ].join("\n"),
243
+ code: 1,
244
+ };
245
+ }
246
+ if (args.includes("--credentials")) {
247
+ // Pi's contract: --credentials emits the credential or reports it unavailable.
248
+ const result = { status: "not_ready", provider: "privateer", reason: "credential_not_available" };
249
+ return { out: args.includes("--json") ? JSON.stringify(result) : result.status, err: "", code: 1 };
250
+ }
251
+ const result = signedIn
252
+ ? { status: "ready", provider: "privateer", authType: "oauth" }
253
+ : { status: "not_ready", provider: "privateer", reason: "credentials_not_configured" };
254
+ return {
255
+ out: args.includes("--json") ? JSON.stringify(result) : result.status,
256
+ err: signedIn ? "" : `Not signed in: run \`${cmd}\` and type /login.`,
257
+ code: signedIn ? 0 : 1,
258
+ };
259
+ }
260
+
261
+ /** `auth --help` with our command name, the provider agents look for, and how to run a task. */
262
+ export function authHelp(cmd = "privateer") {
263
+ return [
264
+ "Usage:",
265
+ ` ${cmd} auth status`,
266
+ ` ${cmd} auth check [--provider <provider>] [--model <model>] [--json] [--credentials] [--no-refresh]`,
267
+ ` ${cmd} auth print-api-key [--provider <provider>] [--model <model>]`,
268
+ ` ${cmd} auth print-bearer-token [--provider <provider>] [--model <model>] [--min-expiry <duration>]`,
269
+ "",
270
+ "status reports whether this machine is signed in to a Privateer account.",
271
+ "check and print-* need --provider or --model. Your account's models are provider",
272
+ `\`privateer\`: \`${cmd} auth check --provider privateer\`. Its credential is the account`,
273
+ "login and is never printed. Other providers (openai, anthropic, …) use their own keys.",
274
+ "",
275
+ `To run a task non-interactively once signed in: ${cmd} -p "<task>"`,
276
+ ].join("\n");
277
+ }
@@ -29,7 +29,7 @@ import path from "node:path";
29
29
  import { fileURLToPath, pathToFileURL } from "node:url";
30
30
  import { applyPatchesIfNeeded, resolveDep } from "./apply-patches.mjs";
31
31
  import { routeUpdate } from "./update-route.mjs";
32
- import { APPROVE_IN_APP_ENV, CLI_SPEND_ENV, PI_SUBCOMMANDS, PRIVATE_ENV, authProblem, extractHeadlessFlags, extractPrivateFlag, privateProblem } from "./headless-flags.mjs";
32
+ import { APPROVE_IN_APP_ENV, CLI_SPEND_ENV, PI_SUBCOMMANDS, PRIVATE_ENV, authHelp, authProblem, extractHeadlessFlags, privateerAuthAnswer, extractPrivateFlag, privateProblem } from "./headless-flags.mjs";
33
33
  import { runToCompletion } from "./run-to-completion.mjs";
34
34
  import { modelArgs } from "./model-args.mjs";
35
35
  import { filterRespawnArgs, listTerminals, requestFresh, runSupervised } from "./fresh-supervisor.mjs";
@@ -669,6 +669,16 @@ else if (PI_SUBCOMMANDS.includes(sub)) {
669
669
  printAuthStatus(cmd);
670
670
  process.exit(0);
671
671
  }
672
+ if (sub === "auth" && (args[1] === undefined || args[1] === "help" || args.includes("--help") || args.includes("-h"))) {
673
+ console.log(authHelp(cmd));
674
+ process.exit(0);
675
+ }
676
+ const own = sub === "auth" ? privateerAuthAnswer(args, accountSignedIn(), cmd) : null;
677
+ if (own) {
678
+ if (own.out) console.log(own.out);
679
+ if (own.err) process.stderr.write(own.err + "\n");
680
+ process.exit(own.code);
681
+ }
672
682
  const CLI = resolveDep(REPO, "@earendil-works/pi-coding-agent", "dist", "cli.js");
673
683
  if (!CLI || !fs.existsSync(CLI)) {
674
684
  console.error(
@@ -680,6 +690,18 @@ else if (PI_SUBCOMMANDS.includes(sub)) {
680
690
  configureCompileCache(PRIVATEER_HOME); // same Pi module graph as a launch — reuse its compiled code
681
691
  ensurePatches(); // project `.privateer/` config dirs are a patch; -l scope needs them
682
692
  process.env.PI_CODING_AGENT_DIR = AGENT_DIR;
693
+ // `mcp list` / `mcp login` read agent/mcp.json. A file an older release wrote for
694
+ // pi-mcp-adapter is rejected entry by entry, so upgrade it first — the same one-time
695
+ // step every session runs (src/remote/mcpControl.ts). Best-effort: a failure leaves
696
+ // Pi to report the file as it stands.
697
+ if (sub === "mcp") {
698
+ const mcpControl = pathToFileURL(path.join(REPO, "src", "remote", "mcpControl.ts")).href;
699
+ spawnSync(NODE_BIN, ["--import", "tsx", "--input-type=module", "-e", `(await import(${JSON.stringify(mcpControl)})).migrateMcpProjection()`], {
700
+ cwd: REPO,
701
+ stdio: "ignore",
702
+ env: process.env,
703
+ });
704
+ }
683
705
  const nodeArgs = fs.existsSync(ENV_FILE) ? [`--env-file=${ENV_FILE}`] : [];
684
706
  runToCompletion(NODE_BIN, [...nodeArgs, CLI, ...args]);
685
707
  }
@@ -744,6 +766,11 @@ else {
744
766
  process.env.PI_SUBAGENT_PI_BINARY = path.join(REPO, "bin", "privateer-subagent.mjs");
745
767
  }
746
768
  process.env.PRIVATEER_CHILD_EXTENSIONS = MOAT_PATHS.join(path.delimiter);
769
+ // A child also gets Pi's MCP support and tool_search, which it loads as built-ins:
770
+ // children run --no-extensions (bin/privateer-subagent.mjs), which turns built-ins off
771
+ // too, so a subagent of this terminal would otherwise lose the connectors its parent
772
+ // has. codemode reaches it as our own moat extension (privateer-codemode).
773
+ process.env.PRIVATEER_CHILD_BUILTINS = "mcp,tool-search";
747
774
  // Suppress Pi's upstream update banner (our banner is the startup surface). Disables
748
775
  // ONLY the version fetch — fd/rg can still download on first run.
749
776
  if (!process.env.PI_SKIP_VERSION_CHECK) process.env.PI_SKIP_VERSION_CHECK = "1";
@@ -759,6 +786,22 @@ else {
759
786
  if (s.quietStartup === undefined) { s.quietStartup = true; m = true; }
760
787
  if (s.collapseChangelog === undefined) { s.collapseChangelog = true; m = true; }
761
788
  if (s.lastChangelogVersion === undefined) { s.lastChangelogVersion = "9999.0.0"; m = true; }
789
+ // Pi 1.0 made fullscreen the default; our footer, splash and compaction patches are
790
+ // built for the regular scrollback UI. A user who picks fullscreen keeps it.
791
+ if (s.tuiMode === undefined) { s.tuiMode = "regular"; m = true; }
792
+ // Pi 1.3's `/share` defaults to "auto", which uploads to Earendil's Radius when the
793
+ // user is signed in there. A share leaves the machine only to the user's own gist.
794
+ if (s.sessionShareDestination === undefined) { s.sessionShareDestination = "gist"; m = true; }
795
+ // Pi 0.99 auto-loads built-in extensions. builtin:mcp and builtin:tool-search are
796
+ // ours to keep — Pi's MCP support IS our MCP support. builtin:codemode is replaced by
797
+ // extensions/privateer-codemode.ts (the same tool without the `models` global), and
798
+ // builtin:llama.cpp, a local-model integration, stays off until adopted on purpose.
799
+ // Each is added once — a user's own +/- entry for that built-in is left alone.
800
+ const ext = Array.isArray(s.extensions) ? s.extensions : [];
801
+ for (const name of ["builtin:codemode", "builtin:llama.cpp"]) {
802
+ if (!ext.some((e) => typeof e === "string" && e.replace(/^[+\-!]/, "") === name)) { ext.push(`-${name}`); m = true; }
803
+ }
804
+ if (ext.length) s.extensions = ext;
762
805
  if (m) fs.writeFileSync(sp, JSON.stringify(s, null, 2) + "\n");
763
806
  } catch { /* best-effort */ }
764
807
 
@@ -1030,13 +1073,21 @@ function warnKeylessLaunch() {
1030
1073
 
1031
1074
  // `privateer auth status` — the question people (and agents) actually ask, answered
1032
1075
  // from local state only: no network, nothing sent anywhere, and no secret printed.
1033
- function printAuthStatus(cmd) {
1034
- let creds = null;
1076
+ function readAccountCredentials() {
1035
1077
  try {
1036
- creds = JSON.parse(fs.readFileSync(path.join(PRIVATEER_HOME, "credentials.json"), "utf8"));
1037
- } catch { /* absent or unreadable → signed out */ }
1078
+ const creds = JSON.parse(fs.readFileSync(path.join(PRIVATEER_HOME, "credentials.json"), "utf8"));
1079
+ return creds && typeof creds.accessToken === "string" ? creds : null;
1080
+ } catch { return null; /* absent or unreadable → signed out */ }
1081
+ }
1082
+
1083
+ function accountSignedIn() {
1084
+ return readAccountCredentials() !== null;
1085
+ }
1086
+
1087
+ function printAuthStatus(cmd) {
1088
+ const creds = readAccountCredentials();
1038
1089
  const lines = [];
1039
- if (creds && typeof creds.accessToken === "string") {
1090
+ if (creds) {
1040
1091
  const who = creds.user?.email || "your account";
1041
1092
  lines.push(`Privateer account: signed in as ${who}${creds.serverBaseUrl ? ` (${creds.serverBaseUrl})` : ""}`);
1042
1093
  lines.push(" This checks the local login only; a session revoked from the app shows up on the next request.");
@@ -1046,7 +1097,8 @@ function printAuthStatus(cmd) {
1046
1097
  }
1047
1098
  const keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "TINFOIL_API_KEY", "PRIVATEER_API_KEY"].filter(haveKey);
1048
1099
  lines.push(`Provider keys in the environment: ${keys.length ? keys.join(", ") : "none"}`);
1049
- lines.push(`To check one provider end to end: ${cmd} auth check --provider <name>`);
1100
+ lines.push(`Your account's models are provider \`privateer\`: ${cmd} auth check --provider privateer`);
1101
+ if (creds) lines.push(`To run a task non-interactively: ${cmd} -p "<task>"`);
1050
1102
  console.log(lines.join("\n"));
1051
1103
  }
1052
1104
 
@@ -67,9 +67,22 @@ export function piCliPath(repoRoot = REPO) {
67
67
  // Given the args pi-subagents built for the child, return the args to run the bundled
68
68
  // cli.js with: `--no-extensions` + one `-e <path>` per moat extension, THEN the
69
69
  // original args (so the injected flags precede the positional `Task:` prompt).
70
+ // Pi's built-in extensions the parent wants its children to keep (MCP, tool_search),
71
+ // by bare name. `--no-extensions` turns built-ins off too, so each is loaded back with
72
+ // `-e builtin:<name>`. A separate, comma-separated variable rather than entries in
73
+ // PRIVATEER_CHILD_EXTENSIONS: that one splits on path.delimiter, which is ":" on
74
+ // macOS and Linux — the very character in "builtin:mcp".
75
+ export function childBuiltins(env = process.env) {
76
+ return (env.PRIVATEER_CHILD_BUILTINS ?? "")
77
+ .split(",")
78
+ .map((n) => n.trim())
79
+ .filter((n) => /^[a-z0-9.-]+$/i.test(n));
80
+ }
81
+
70
82
  export function buildChildArgs(originalArgs, repoRoot = REPO, env = process.env) {
71
83
  const inject = ["--no-extensions"];
72
84
  for (const p of moatExtensionPaths(repoRoot, env)) inject.push("-e", p);
85
+ for (const name of childBuiltins(env)) inject.push("-e", `builtin:${name}`);
73
86
  return [...inject, ...originalArgs];
74
87
  }
75
88
 
@@ -0,0 +1,15 @@
1
+ // codemode — the model writes a short script that calls tools, and only the script's
2
+ // output comes back. It is how Pi's built-in MCP support reaches server tools by
3
+ // default, and it runs in a QuickJS sandbox with no file system, network or timers:
4
+ // every effect is a tool call, each one sent through the permission gate like a direct
5
+ // call (see classifyToolCall).
6
+ //
7
+ // Loaded here instead of as Pi's `builtin:codemode` (which the launcher turns off) for
8
+ // one option: `models: false`. Pi's copy gives scripts a `models` global that runs
9
+ // classifier and image models with the session's credentials — model calls the gate
10
+ // never sees, to whichever provider has a key, not necessarily the one this session was
11
+ // chosen for. Registering a tool named `codemode` replaces the built-in one, and Pi's
12
+ // MCP support activates it exactly as it would its own.
13
+ import { createCodemodeExtension } from "@earendil-works/pi-coding-agent";
14
+
15
+ export default createCodemodeExtension({ models: false });
@@ -2,10 +2,9 @@
2
2
  * The Privateer `/connect` panel — add, toggle, and remove MCP connectors without
3
3
  * leaving the terminal.
4
4
  *
5
- * WHY THIS EXISTS: pi-mcp-adapter owns `/mcp`, but that command is a *status and
6
- * connection* surface — its setup flow can only adopt configs from other hosts,
7
- * scaffold an EMPTY `.mcp.json`, or quick-add RepoPrompt. There is no way to type in
8
- * a server. So the terminal was the one surface where adding a connector meant
5
+ * WHY THIS EXISTS: Pi's `/mcp` is a *status and connection* surface — inspect,
6
+ * sign in, reconnect, toggle. It has no catalog and no form for typing in a server, so
7
+ * without this the terminal was the one surface where adding a connector meant
9
8
  * hand-editing JSON, while the phone and the desktop both had a real editor.
10
9
  *
11
10
  * ONE CONFIG, THREE SURFACES: this is deliberately a thin UI over the SAME
@@ -16,8 +15,8 @@
16
15
  * routine runs. Nothing about this panel is terminal-specific except the pixels.
17
16
  *
18
17
  * SECRETS: env values are typed into a masked field and written in PLAINTEXT to the
19
- * config on this machine — correct, and the same thing the desktop does: the adapter
20
- * has to hand the real token to the server process. The masking is shoulder-surfing
18
+ * config on this machine — correct, and the same thing the desktop does: Pi has to
19
+ * hand the real token to the server process. The masking is shoulder-surfing
21
20
  * and screen-share hygiene, not a storage claim. (Over the relay it's different —
22
21
  * there the value is sealed to the terminal's key; see mcpControl's header.)
23
22
  */
@@ -31,6 +30,7 @@ import {
31
30
  } from "@earendil-works/pi-tui";
32
31
  import {
33
32
  makeMcpControl,
33
+ migrateMcpProjection,
34
34
  type McpDraft,
35
35
  type RemoteMcpServer,
36
36
  } from "../src/remote/mcpControl.ts";
@@ -55,7 +55,7 @@ interface TuiLike {
55
55
 
56
56
  // What the panel hands back to the command handler when it closes.
57
57
  export interface ConnectResult {
58
- // True when the config changed, so the handler knows to reload the adapter.
58
+ // True when the config changed, so the handler knows to reload MCP.
59
59
  changed: boolean;
60
60
  message?: string;
61
61
  }
@@ -183,8 +183,8 @@ function customSteps(existing?: RemoteMcpServer): Step[] {
183
183
  key: "target",
184
184
  prompt: hosted ? "Server URL" : "Launch command, or an https:// URL",
185
185
  hint: hosted
186
- ? "e.g. https://mcp.example.com/sse — a hosted agent can't run a local command."
187
- : "e.g. npx -y @modelcontextprotocol/server-memory · or https://mcp.example.com/sse",
186
+ ? "e.g. https://mcp.example.com/mcp — a hosted agent can't run a local command."
187
+ : "e.g. npx -y @modelcontextprotocol/server-memory · or https://mcp.example.com/mcp",
188
188
  initial: target,
189
189
  validate: hosted
190
190
  ? (v) =>
@@ -253,13 +253,12 @@ function buildCustomDraft(answers: Record<string, string>): McpDraft {
253
253
  const hasHeaders = Object.keys(headers).length > 0;
254
254
  if (hasHeaders) draft.headers = headers;
255
255
  if (token) {
256
- // A static token: the adapter only sends the Authorization header when auth is
257
- // explicitly "bearer", so this pairing is not optional.
256
+ // A static token: mcpControl projects it into an Authorization header only when
257
+ // auth is "bearer", so this pairing is not optional.
258
258
  draft.auth = "bearer";
259
259
  draft.bearerToken = token;
260
260
  } else if (hasHeaders) {
261
- // Custom headers ARE the credential here — the adapter's supportsOAuth() refuses
262
- // OAuth once headers are configured, so claiming "oauth" would be a lie.
261
+ // Custom headers ARE the credential here, so claiming "oauth" would be a lie.
263
262
  draft.auth = "none";
264
263
  } else {
265
264
  draft.auth = "oauth";
@@ -325,7 +324,7 @@ function pendingFromCatalog(e: CatalogEntry): Pending {
325
324
  },
326
325
  note:
327
326
  e.needs === "oauth"
328
- ? `Authorize it in a browser on THIS machine: /mcp-auth ${e.name}`
327
+ ? `Authorize it in a browser on THIS machine: /mcp login ${e.name}`
329
328
  : e.localHttp
330
329
  // Not a warning — a localHttp connector is correct the moment it saves. But
331
330
  // it answers only while its host app is up, so a later failure means "the
@@ -507,7 +506,7 @@ class ConnectPanel extends Container {
507
506
  private statusOf(s: RemoteMcpServer): string {
508
507
  const t = this.theme;
509
508
  if (s.transport === "http" && s.oauth) {
510
- return t.fg("warning", `⚠ authorize: /mcp-auth ${s.name}`);
509
+ return t.fg("warning", `⚠ authorize: /mcp login ${s.name}`);
511
510
  }
512
511
  if (s.envKeys.length > 0) {
513
512
  const missing = s.envKeys.filter((k) => !s.secretsSet.includes(k));
@@ -880,6 +879,9 @@ export default function privateerConnect(pi: {
880
879
  registerCommand?: (name: string, opts: unknown) => void;
881
880
  }): void {
882
881
  if (typeof pi.registerCommand !== "function") return;
882
+ // Before Pi's MCP support reads mcp.json at session_start: a file an older release
883
+ // wrote in pi-mcp-adapter's format would otherwise lose every HTTP connector in it.
884
+ migrateMcpProjection();
883
885
 
884
886
  pi.registerCommand("connect", {
885
887
  description: "Add, enable, or remove MCP connectors (GitHub, Notion, Linear, …)",
@@ -896,9 +898,10 @@ export default function privateerConnect(pi: {
896
898
  );
897
899
 
898
900
  if (!result?.changed) return;
899
- // Reload so pi-mcp-adapter re-reads mcp.json in this session — the same thing
900
- // its own `/mcp setup` does after a write. Without it the new connector only
901
- // appears on the next launch, which reads as "it didn't work".
901
+ // Reload so Pi's MCP support re-reads mcp.json in this session (its own docs:
902
+ // "Run /reload after adding, removing, or changing a server outside the
903
+ // session"). Without it the new connector only appears on the next launch,
904
+ // which reads as "it didn't work".
902
905
  try {
903
906
  await ctx.reload?.();
904
907
  } catch {
@@ -33,7 +33,7 @@
33
33
  //
34
34
  // The specifiers are built rather than written as literals on purpose: it keeps tsc from
35
35
  // pulling that package's own .ts sources into our typecheck, the same trick config/moat.ts
36
- // uses for pi-mcp-adapter.
36
+ // used for pi-mcp-adapter.
37
37
  import { guardedWebToolDefinitions } from "../src/tools/web.ts";
38
38
  import { guardedCryptoToolDefinitions } from "../src/tools/crypto.ts";
39
39
  import { readWebToolsConfig, resolveWebMode, type ProviderMetaLike } from "../src/tools/webMode.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.14.5",
3
+ "version": "0.15.0",
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",
@@ -71,21 +71,20 @@
71
71
  "node": ">=22.19.0"
72
72
  },
73
73
  "dependencies": {
74
- "@earendil-works/pi-ai": "0.86.0",
75
- "@earendil-works/pi-coding-agent": "0.86.0",
76
- "@earendil-works/pi-tui": "0.86.0",
77
- "@juicesharp/rpiv-ask-user-question": "2.10.1",
78
- "@juicesharp/rpiv-web-tools": "2.10.1",
74
+ "@earendil-works/pi-ai": "1.3.0",
75
+ "@earendil-works/pi-coding-agent": "1.3.0",
76
+ "@earendil-works/pi-tui": "1.3.0",
77
+ "@juicesharp/rpiv-ask-user-question": "2.13.8",
78
+ "@juicesharp/rpiv-web-tools": "2.13.8",
79
79
  "@noble/ciphers": "2.2.0",
80
80
  "@noble/curves": "1.9.7",
81
81
  "@noble/hashes": "1.8.0",
82
82
  "@phala/dcap-qvl": "0.5.2",
83
83
  "@zed-industries/agent-client-protocol": "0.4.5",
84
84
  "patch-package": "8.0.1",
85
- "pi-background-tasks": "2.5.0",
86
- "pi-mcp-adapter": "2.34.0",
85
+ "pi-background-tasks": "2.6.10",
87
86
  "pi-privacy": "0.14.1",
88
- "pi-subagents": "0.70.0",
87
+ "pi-subagents": "0.78.0",
89
88
  "picomatch": "4.0.5",
90
89
  "privateer-speak": "0.8.0",
91
90
  "privateer-workflow": "0.1.0",
@@ -1,11 +1,11 @@
1
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
2
+ index 4e36f35..8a3f8a5 100644
3
3
  --- a/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js
4
4
  +++ b/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js
5
- @@ -1,11 +1,38 @@
5
+ @@ -2,11 +2,39 @@ import { clampThinkingLevel } from "../models.js";
6
6
  import { estimateContextTokens } from "../utils/estimate.js";
7
7
  const CONTEXT_SAFETY_TOKENS = 4096;
8
- -const MIN_MAX_TOKENS = 1;
8
+ const MIN_MAX_TOKENS = 1;
9
9
  +// Privateer patch: floor the answer budget at a usable answer, not at one token.
10
10
  +//
11
11
  +// Stock pi floored this at 1, so the moment the (chars/4) context estimate reached
@@ -36,11 +36,10 @@ index 216a45f..a1e3214 100644
36
36
  +// Mirrors src/engine/contextBudget.ts, which is where this is specified and tested.
37
37
  export function clampMaxTokensToContext(model, context, maxTokens) {
38
38
  if (model.contextWindow <= 0)
39
- - return Math.max(MIN_MAX_TOKENS, maxTokens);
40
- + return Math.max(1, maxTokens);
39
+ return Math.max(MIN_MAX_TOKENS, maxTokens);
41
40
  const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;
42
41
  - return Math.min(maxTokens, Math.max(MIN_MAX_TOKENS, available));
43
42
  + return Math.min(maxTokens, Math.max(MIN_ANSWER_TOKENS, available));
44
43
  }
45
- export function buildBaseOptions(model, context, options, apiKey) {
46
- const samplingParams = model.samplingParams || options?.samplingParams
44
+ export function resolveSamplingParams(model, thinkingLevel, requestParams) {
45
+ const effectiveThinkingLevel = clampThinkingLevel(model, thinkingLevel);