privateer-agent 0.12.2 → 0.12.5
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 +33 -1
- package/extensions/privateer-hints.ts +132 -0
- package/extensions/privateer-speak.ts +109 -0
- package/package.json +4 -3
- package/src/auth/privateer.ts +8 -2
- package/src/config/moatManifest.json +4 -4
- package/src/providers/account.ts +8 -1
- package/src/providers/catalog.ts +1 -1
- package/src/providers/genModelsJson.ts +8 -1
package/README.md
CHANGED
|
@@ -390,6 +390,37 @@ the app are write-only — the app can name them but never read them back. Note
|
|
|
390
390
|
live in plaintext in `config.json` on your machine, and every channel action is appended to
|
|
391
391
|
`~/.privateer/channels-audit.log`.
|
|
392
392
|
|
|
393
|
+
## Drive it from Buzz or Zed — ACP
|
|
394
|
+
|
|
395
|
+
Privateer speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP v1 over
|
|
396
|
+
stdio). Any ACP host — [Buzz](https://buzz.xyz), Block's team messenger where agents are
|
|
397
|
+
teammates, or the [Zed](https://zed.dev) editor — can spawn `privateer acp` and drive it:
|
|
398
|
+
prompts stream back, tool activity shows live, and the host's model picker lists Privateer's
|
|
399
|
+
catalog with TEE-backed models labelled **confidential (TEE)**.
|
|
400
|
+
|
|
401
|
+
The part we care about: **the host renders the UI, but authority never leaves your machine.**
|
|
402
|
+
|
|
403
|
+
- Every action is classified by Privateer's own permission gate — ACP's
|
|
404
|
+
`session/request_permission` only delivers the prompt.
|
|
405
|
+
- The **tool ceiling** comes from your local config, not the host, and ships **read-only**
|
|
406
|
+
(`read`, `grep`, `find`, `ls`). The host cannot widen it.
|
|
407
|
+
- Filesystem access is **confined to one directory root**; out-of-tree access is refused,
|
|
408
|
+
not prompted.
|
|
409
|
+
- Every ambiguous outcome — unreachable host, cancelled dialog, unknown answer, aborted
|
|
410
|
+
turn — resolves to **deny**.
|
|
411
|
+
- "Allow for this session" lives in memory and dies with the session; dangerous shell
|
|
412
|
+
(`curl … | sh` and friends) can never become standing permission.
|
|
413
|
+
|
|
414
|
+
Zed setup (`settings.json`):
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{ "agent_servers": { "Privateer": { "command": "privateer", "args": ["acp"] } } }
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Honest caveat for Buzz: **Buzz currently auto-approves permission prompts**, so under Buzz
|
|
421
|
+
the tool ceiling *is* the control — which is exactly why the default is read-only. Full
|
|
422
|
+
setup, config, and limitations: [`docs/acp.md`](docs/acp.md).
|
|
423
|
+
|
|
393
424
|
## Connectors — MCP
|
|
394
425
|
|
|
395
426
|
Privateer is an **MCP client**. Point it at a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
@@ -543,7 +574,8 @@ drop your own into `~/.privateer/agent/extensions/` and it loads the same way, g
|
|
|
543
574
|
| `/update` · `/privateer` | update to the latest release / Privateer status and posture |
|
|
544
575
|
|
|
545
576
|
Shell subcommands: `privateer` (interactive), `privateer update`, `privateer harbor …`,
|
|
546
|
-
`privateer
|
|
577
|
+
`privateer acp` (serve the agent to an ACP host like Buzz or Zed — see
|
|
578
|
+
[`docs/acp.md`](docs/acp.md)), `privateer --no-quarter`, `privateer --version`.
|
|
547
579
|
|
|
548
580
|
## Develop
|
|
549
581
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Rotating tips in the working line. While the agent streams, the "Working..."
|
|
2
|
+
// loader is dead air for seconds at a time — the one spot a discoverability hint
|
|
3
|
+
// costs no vertical space and interrupts nothing. So while a turn is active we
|
|
4
|
+
// rotate short tips through it for the keys and commands nobody finds on their
|
|
5
|
+
// own: ctrl+t to hide thinking text (the ask that prompted this), shift+tab for
|
|
6
|
+
// the thinking level, @file mentions, /models, and so on.
|
|
7
|
+
//
|
|
8
|
+
// Mechanics: ctx.ui.setWorkingMessage(msg) both stores the message for the next
|
|
9
|
+
// working indicator AND live-updates the one currently on screen (and ONLY the
|
|
10
|
+
// "working" kind — Pi's guard means retry/compaction spinners are never touched),
|
|
11
|
+
// so a timer can rotate the text mid-stream. Calling it with no argument restores
|
|
12
|
+
// Pi's default "Working...", which is what agent_end does. Timers run strictly
|
|
13
|
+
// inside agent_start → agent_end, so an idle terminal owns no message at all.
|
|
14
|
+
//
|
|
15
|
+
// Keys render through pi-coding-agent's keyText, which reads the LIVE keybindings
|
|
16
|
+
// singleton the running app configured — a user remap shows the remapped key, and
|
|
17
|
+
// each hint carries a fallback for an unbound action. The first tip waits a beat
|
|
18
|
+
// (FIRST_MS) so short turns never see one; the cursor is module-lifetime so
|
|
19
|
+
// back-to-back turns continue the rotation instead of repeating tip #1.
|
|
20
|
+
//
|
|
21
|
+
// /hints on|off persists { hints: { enabled } } in ~/.privateer/config.json (ours;
|
|
22
|
+
// Pi never reads that file) — read-modify-write, preserving unrelated keys.
|
|
23
|
+
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
25
|
+
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { configPath, globalDir } from "../src/config/paths.ts";
|
|
27
|
+
|
|
28
|
+
const FIRST_MS = 6_000; // a turn shorter than this never shows a tip
|
|
29
|
+
const EVERY_MS = 12_000;
|
|
30
|
+
|
|
31
|
+
// keyText returns "" for an unbound action and could throw if the keybindings
|
|
32
|
+
// singleton isn't initialized yet (headless surfaces) — fall back either way.
|
|
33
|
+
function key(binding: Parameters<typeof keyText>[0], fallback: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return keyText(binding) || fallback;
|
|
36
|
+
} catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Lazy thunks, not strings: keys resolve at display time, after the app has
|
|
42
|
+
// loaded (and possibly remapped) its keybindings.
|
|
43
|
+
const HINTS: Array<() => string> = [
|
|
44
|
+
() => `${key("app.thinking.toggle", "ctrl+t")} hides thinking text — make it stick in /settings`,
|
|
45
|
+
() => `${key("app.thinking.cycle", "shift+tab")} cycles the thinking level`,
|
|
46
|
+
() => `${key("app.message.followUp", "alt+enter")} queues a follow-up without interrupting`,
|
|
47
|
+
() => `${key("app.tools.expand", "ctrl+o")} expands collapsed tool output`,
|
|
48
|
+
() => `type @ to reference a file in your prompt — Tab completes the path`,
|
|
49
|
+
() => `/models picks a model, with TEE/ZDR privacy shields`,
|
|
50
|
+
() => `/init writes a PRIVATEER.md so the agent knows this project`,
|
|
51
|
+
() => `/connect adds MCP connectors to this terminal`,
|
|
52
|
+
() => `${key("app.editor.external", "ctrl+g")} drafts long prompts in your $EDITOR`,
|
|
53
|
+
() => `these tips are /hints — /hints off silences them`,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Default ON: absent file, absent block, or unreadable JSON all mean enabled.
|
|
57
|
+
// Only an explicit { hints: { enabled: false } } turns the rotation off.
|
|
58
|
+
function hintsEnabled(): boolean {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(readFileSync(configPath(), "utf8"))?.hints?.enabled !== false;
|
|
61
|
+
} catch {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function persistEnabled(enabled: boolean): void {
|
|
67
|
+
let cfg: any = {};
|
|
68
|
+
try {
|
|
69
|
+
cfg = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
70
|
+
} catch {
|
|
71
|
+
/* no config yet */
|
|
72
|
+
}
|
|
73
|
+
if (typeof cfg !== "object" || cfg === null || Array.isArray(cfg)) cfg = {};
|
|
74
|
+
cfg.hints = { ...(cfg.hints ?? {}), enabled };
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(globalDir(), { recursive: true });
|
|
77
|
+
writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + "\n");
|
|
78
|
+
} catch {
|
|
79
|
+
/* best effort — the toggle still holds for this session */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default function privateerHints(pi: any): void {
|
|
84
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
85
|
+
let uiRef: any;
|
|
86
|
+
let cursor = 0;
|
|
87
|
+
|
|
88
|
+
const stop = (restoreDefault: boolean): void => {
|
|
89
|
+
if (timer !== undefined) {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
timer = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (restoreDefault) uiRef?.setWorkingMessage?.();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const showNext = (): void => {
|
|
97
|
+
const hint = HINTS[cursor % HINTS.length]();
|
|
98
|
+
cursor++;
|
|
99
|
+
uiRef?.setWorkingMessage?.(`Working... · tip: ${hint}`);
|
|
100
|
+
timer = setTimeout(showNext, EVERY_MS);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
pi.on("agent_start", (_e: any, ctx: any) => {
|
|
104
|
+
if (!ctx?.hasUI) return; // headless (print/rpc/harbor): no loader to write to
|
|
105
|
+
uiRef = ctx.ui;
|
|
106
|
+
stop(false); // an interrupted turn can restart without an agent_end between
|
|
107
|
+
if (!hintsEnabled()) return;
|
|
108
|
+
timer = setTimeout(showNext, FIRST_MS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
pi.on("agent_end", () => stop(true));
|
|
112
|
+
pi.on("session_shutdown", () => stop(false));
|
|
113
|
+
|
|
114
|
+
pi.registerCommand?.("hints", {
|
|
115
|
+
description: "Rotating tips in the working line: /hints [on | off]",
|
|
116
|
+
handler: (args: string, ctx: any) => {
|
|
117
|
+
uiRef = ctx?.ui ?? uiRef;
|
|
118
|
+
const sub = String(args ?? "").trim().toLowerCase().split(/\s+/)[0];
|
|
119
|
+
if (sub === "on" || sub === "off") {
|
|
120
|
+
const enabled = sub === "on";
|
|
121
|
+
persistEnabled(enabled);
|
|
122
|
+
if (!enabled) stop(true); // takes effect mid-turn; "on" starts next turn
|
|
123
|
+
ctx?.ui?.notify?.(`Working-line tips ${enabled ? "on" : "off"}.`, "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
ctx?.ui?.notify?.(
|
|
127
|
+
`Working-line tips are ${hintsEnabled() ? "on" : "off"} — /hints on|off to change.`,
|
|
128
|
+
"info",
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Spoken responses AND voice input for the TUI: the generic privateer-speak (pi-speak)
|
|
2
|
+
// extension, plus Privateer's own providers hooked into both its registries — the
|
|
3
|
+
// account's confidential-compute TTS (/api/audio/speech, same path as the app's Audio
|
|
4
|
+
// studio and generate_speech; today Tinfoil qwen3-tts in an attested enclave) and its
|
|
5
|
+
// confidential STT (/api/audio/transcribe, same tinfoil/near routing the app's voice
|
|
6
|
+
// features use). Both inherit the account's entitlement, caps and billing.
|
|
7
|
+
//
|
|
8
|
+
// PROVIDER ATTUNEMENT, NOT STOMPING. The provider registers with preferWhen: signed-in,
|
|
9
|
+
// which the registry ranks ABOVE the built-in local voice but BELOW a deliberate /speak
|
|
10
|
+
// provider pick — sign in and speech quietly upgrades from the OS voice to confidential
|
|
11
|
+
// TTS, exactly the resolveSignedInModel pattern, with the model-persistence lesson
|
|
12
|
+
// applied (a user who picked "local" stays on "local").
|
|
13
|
+
//
|
|
14
|
+
// HONEST FRAMING. "Confidential", never "private end-to-end": the utterance text leaves
|
|
15
|
+
// the machine for the attested TTS enclave. That is the claim the label makes and the
|
|
16
|
+
// only one it may make. And this only ever speaks in interactive UI sessions — pi-speak
|
|
17
|
+
// gates on hasUI, so harbor/daemon/ACP surfaces stay silent by design (this extension is
|
|
18
|
+
// manifest-only, like privateer-hints: buildMoat never includes it).
|
|
19
|
+
//
|
|
20
|
+
// VOICES. qwen3-tts speakers, mirrored from the server's TINFOIL_VOICES (the list is
|
|
21
|
+
// static there too; first entry is the server-side default). Tinfoil requires the voice
|
|
22
|
+
// param server-side, so the server applies "serena" when none is sent — leaving voice
|
|
23
|
+
// unset here is always safe. If the account's default TTS model ever changes, update
|
|
24
|
+
// this list with it.
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { makePiSpeakExtension, registerSpeechProvider, registerTranscriptionProvider } from "privateer-speak";
|
|
27
|
+
import { apiRequest, hasCredentials } from "../src/auth/privateer.ts";
|
|
28
|
+
import { globalDir } from "../src/config/paths.ts";
|
|
29
|
+
|
|
30
|
+
const QWEN3_TTS_VOICES = ["serena", "aiden", "dylan", "eric", "ono_anna", "ryan", "sohee", "uncle_fu", "vivian"];
|
|
31
|
+
|
|
32
|
+
// Call an account audio endpoint and hand back the parsed JSON, throwing the server's
|
|
33
|
+
// own (person-readable, localized) message on failure — same policy as media.ts's
|
|
34
|
+
// callAccount: surface, never swallow.
|
|
35
|
+
async function accountJson<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
|
|
36
|
+
let res: Response;
|
|
37
|
+
try {
|
|
38
|
+
res = await apiRequest(path, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
...(signal ? { signal } : {}),
|
|
43
|
+
});
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw new Error(`could not reach Privateer: ${e instanceof Error ? e.message : String(e)}`);
|
|
46
|
+
}
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
let message = `Privateer returned ${res.status}`;
|
|
49
|
+
try {
|
|
50
|
+
const err = (await res.json()) as { message?: string; error?: { message?: string } };
|
|
51
|
+
message = err?.message ?? err?.error?.message ?? message;
|
|
52
|
+
} catch {
|
|
53
|
+
/* non-JSON body — the status message stands */
|
|
54
|
+
}
|
|
55
|
+
throw new Error(message);
|
|
56
|
+
}
|
|
57
|
+
return (await res.json()) as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerSpeechProvider(
|
|
61
|
+
{
|
|
62
|
+
id: "privateer",
|
|
63
|
+
label: "Privateer account TTS (attested enclave)",
|
|
64
|
+
privacy: "confidential",
|
|
65
|
+
defaultVoice: QWEN3_TTS_VOICES[0],
|
|
66
|
+
voices: () => QWEN3_TTS_VOICES,
|
|
67
|
+
available: () => hasCredentials(),
|
|
68
|
+
fetchSpeech: async (text, { voice, signal }) => {
|
|
69
|
+
const { audioBase64 } = await accountJson<{ audioBase64?: string }>(
|
|
70
|
+
"/api/audio/speech",
|
|
71
|
+
{ text, ...(voice ? { voice } : {}) },
|
|
72
|
+
signal,
|
|
73
|
+
);
|
|
74
|
+
if (!audioBase64) throw new Error("Privateer returned no audio");
|
|
75
|
+
return { data: Buffer.from(audioBase64, "base64"), format: "mp3" };
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{ preferWhen: () => hasCredentials() },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
registerTranscriptionProvider(
|
|
82
|
+
{
|
|
83
|
+
id: "privateer",
|
|
84
|
+
label: "Privateer account STT (attested enclave)",
|
|
85
|
+
privacy: "confidential",
|
|
86
|
+
available: () => hasCredentials(),
|
|
87
|
+
transcribe: async (audio, { language, signal }) => {
|
|
88
|
+
const { text } = await accountJson<{ text?: string }>(
|
|
89
|
+
"/api/audio/transcribe",
|
|
90
|
+
{
|
|
91
|
+
audioBase64: Buffer.from(audio.data).toString("base64"),
|
|
92
|
+
format: audio.format,
|
|
93
|
+
...(language ? { language } : {}),
|
|
94
|
+
},
|
|
95
|
+
signal,
|
|
96
|
+
);
|
|
97
|
+
return typeof text === "string" ? text : "";
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{ preferWhen: () => hasCredentials() },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
export default function privateerSpeak(pi: any): void {
|
|
104
|
+
// Ours, beside config.json — NOT ~/.pi/speak.json, so a user who also runs plain Pi
|
|
105
|
+
// with the generic package keeps two independent setups instead of a fought-over
|
|
106
|
+
// file. globalDir() is read here, not at module load, so PRIVATEER_HOME set around
|
|
107
|
+
// session creation (tests, the daemon) is honoured.
|
|
108
|
+
makePiSpeakExtension({ configFile: join(globalDir(), "speak.json") })(pi);
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
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",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"start": "tsx src/main.ts",
|
|
61
61
|
"chat": "node --env-file=.env --import tsx src/cli/chat.ts",
|
|
62
62
|
"channels": "node --env-file=.env --import tsx src/channels/run.ts",
|
|
63
|
-
"dev": "
|
|
63
|
+
"dev": "node bin/privateer-launch.mjs",
|
|
64
|
+
"dev:fresh": "PRIVATEER_HOME=\"$PWD/.dev-home\" node bin/privateer-launch.mjs",
|
|
64
65
|
"typecheck": "tsc --noEmit",
|
|
65
66
|
"test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
|
|
66
67
|
},
|
|
@@ -71,7 +72,6 @@
|
|
|
71
72
|
"@earendil-works/pi-ai": "^0.80.3",
|
|
72
73
|
"@earendil-works/pi-coding-agent": "0.80.3",
|
|
73
74
|
"@earendil-works/pi-tui": "^0.80.3",
|
|
74
|
-
"@hypabolic/pi-hypa": "^0.1.6",
|
|
75
75
|
"@juicesharp/rpiv-ask-user-question": "^2.2.0",
|
|
76
76
|
"@juicesharp/rpiv-web-tools": "^1.20.0",
|
|
77
77
|
"@noble/ciphers": "^2.1.1",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"pi-privacy": "^0.11.0",
|
|
85
85
|
"pi-subagents": "^0.34.0",
|
|
86
86
|
"picomatch": "^4.0.4",
|
|
87
|
+
"privateer-speak": "0.1.0",
|
|
87
88
|
"privateer-workflow": "^0.1.0",
|
|
88
89
|
"tinfoil": "^1.1.11",
|
|
89
90
|
"tsx": "^4.16.0",
|
package/src/auth/privateer.ts
CHANGED
|
@@ -451,8 +451,14 @@ export async function runDeviceLogin(opts: {
|
|
|
451
451
|
// Pass the server's own message through so the user learns what to actually do.
|
|
452
452
|
async function spawnFailure(res: Response): Promise<Error> {
|
|
453
453
|
if (res.status === 401) {
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
// Announce only the signed-in → signed-out transition: concurrent launch-time
|
|
455
|
+
// spawns (warmSession + the account-channel seed) can both 401 on the same dead
|
|
456
|
+
// login, and whichever lands second must stay silent. Same guard as
|
|
457
|
+
// handleServerRevoke.
|
|
458
|
+
if (hasCredentials()) {
|
|
459
|
+
clearCredentials();
|
|
460
|
+
notifySessionExpired();
|
|
461
|
+
}
|
|
456
462
|
return new Error("Your Privateer session expired. Run /login to sign in again.");
|
|
457
463
|
}
|
|
458
464
|
let message: string | undefined;
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
{ "name": "privateer-privacy", "entry": "extensions/privateer-privacy.ts", "note": "pi-privacy + account tier resolver" },
|
|
12
12
|
{ "name": "privateer-connect", "entry": "extensions/privateer-connect.ts", "note": "/connect — MCP connector manager" },
|
|
13
13
|
{ "name": "privateer-media", "entry": "extensions/privateer-media.ts", "note": "image/video/speech/music + ffmpeg compose" },
|
|
14
|
+
{ "name": "privateer-hints", "entry": "extensions/privateer-hints.ts", "note": "rotating tips in the working line + /hints" },
|
|
15
|
+
{ "name": "privateer-speak", "entry": "extensions/privateer-speak.ts", "note": "spoken responses (/speak) + voice input (/talk) — pi-speak + confidential account TTS/STT" },
|
|
14
16
|
{ "name": "rpiv-web-tools", "dep": ["@juicesharp/rpiv-web-tools", "index.ts"], "note": "private web tools (user's own provider key)" },
|
|
15
17
|
{ "name": "rpiv-ask-user-question", "dep": ["@juicesharp/rpiv-ask-user-question", "index.ts"], "note": "ask_user_question" },
|
|
16
18
|
{ "name": "pi-mcp-adapter", "dep": ["pi-mcp-adapter", "index.ts"], "note": "MCP servers as first-class tools" },
|
|
17
|
-
{ "name": "pi-hypa", "dep": ["@hypabolic/pi-hypa", "extensions", "index.ts"], "note": "hypa pack" },
|
|
18
19
|
{ "name": "pi-subagents", "dep": ["pi-subagents", "src", "extension", "index.ts"], "note": "bounded parallel sub-agents" }
|
|
19
20
|
],
|
|
20
|
-
"retired": ["pi-privacy", "pi-web-access"],
|
|
21
|
+
"retired": ["pi-privacy", "pi-web-access", "pi-hypa"],
|
|
21
22
|
"reservedAliases": [
|
|
22
23
|
"@juicesharp/rpiv-web-tools",
|
|
23
|
-
"@juicesharp/rpiv-ask-user-question"
|
|
24
|
-
"@hypabolic/pi-hypa"
|
|
24
|
+
"@juicesharp/rpiv-ask-user-question"
|
|
25
25
|
]
|
|
26
26
|
}
|
package/src/providers/account.ts
CHANGED
|
@@ -56,6 +56,10 @@ const DEFAULT_MODELS = [
|
|
|
56
56
|
"anthropic/claude-sonnet-5",
|
|
57
57
|
"openai/gpt-5.6-sol",
|
|
58
58
|
"deepseek/deepseek-v4-flash",
|
|
59
|
+
// 2026-08 releases, confirmed live on OpenRouter (ZDR-covered ids reach the
|
|
60
|
+
// account catalog automatically; these seeds just make them resolve at launch).
|
|
61
|
+
"moonshotai/kimi-k3",
|
|
62
|
+
"z-ai/glm-5.2",
|
|
59
63
|
];
|
|
60
64
|
|
|
61
65
|
function seedModel(id: string) {
|
|
@@ -816,7 +820,10 @@ export async function armAccountCredential(
|
|
|
816
820
|
// so staying silent leaves the user to discover it as a bare "No API key found for
|
|
817
821
|
// privateer" on their first prompt.
|
|
818
822
|
const c = ctx as SeedContext;
|
|
819
|
-
|
|
823
|
+
// If the credentials vanished during the call, the spawn hit a 401 and
|
|
824
|
+
// onSessionExpired already announced the sign-out — a second line here would
|
|
825
|
+
// just repeat it (we returned early above if we STARTED signed out).
|
|
826
|
+
if (opts.notify !== false && c?.hasUI && hasCredentials()) {
|
|
820
827
|
c.ui?.notify?.(`Privateer account channel unavailable — ${(e as Error).message}`, "error");
|
|
821
828
|
}
|
|
822
829
|
return false;
|
package/src/providers/catalog.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const PROVIDERS: ProviderEntry[] = [
|
|
|
59
59
|
api: "openai-completions",
|
|
60
60
|
keyEnv: "${DASHSCOPE_API_KEY}",
|
|
61
61
|
compat: { thinkingFormat: "qwen" },
|
|
62
|
-
seedModels: ["qwen3-max", "qwen3-coder-plus", "qwen-max-latest"],
|
|
62
|
+
seedModels: ["qwen3.8-max-preview", "qwen3-max", "qwen3-coder-plus", "qwen-max-latest"],
|
|
63
63
|
},
|
|
64
64
|
{ id: "ollama", source: "pi-privacy" },
|
|
65
65
|
{ id: "nearai", source: "pi-privacy" },
|
|
@@ -36,6 +36,13 @@ export interface ModelsJson {
|
|
|
36
36
|
providers: Record<string, ModelsJsonProvider>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Per-id context-window overrides for seeds whose real window is far from the
|
|
40
|
+
// Appendix A.4 default (kept until the live listing refines them). Qwen3.8-Max
|
|
41
|
+
// serves ~1M tokens (983,616 advertised at launch, 2026-08-03).
|
|
42
|
+
const SEED_CONTEXT: Record<string, number> = {
|
|
43
|
+
"qwen3.8-max-preview": 1_000_000,
|
|
44
|
+
};
|
|
45
|
+
|
|
39
46
|
function seedModel(id: string, compat?: Record<string, unknown>): ModelsJsonModel {
|
|
40
47
|
return {
|
|
41
48
|
id,
|
|
@@ -43,7 +50,7 @@ function seedModel(id: string, compat?: Record<string, unknown>): ModelsJsonMode
|
|
|
43
50
|
reasoning: false,
|
|
44
51
|
input: ["text"],
|
|
45
52
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
-
contextWindow: 128000,
|
|
53
|
+
contextWindow: SEED_CONTEXT[id] ?? 128000,
|
|
47
54
|
maxTokens: 16384,
|
|
48
55
|
...(compat ? { compat } : {}),
|
|
49
56
|
};
|