privateer-agent 0.3.2 → 0.3.4
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 +9 -0
- package/bin/privateer-tui +42 -4
- package/extensions/privateer-brand.ts +72 -14
- package/package.json +1 -1
- package/src/providers/account.ts +11 -3
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
<a href="https://www.npmjs.com/package/privateer-agent">
|
|
13
13
|
<img src="https://img.shields.io/npm/v/privateer-agent" alt="npm" />
|
|
14
14
|
</a>
|
|
15
|
+
<a href="https://github.com/privateer-agent/privateer-agent/releases">
|
|
16
|
+
<img src="https://img.shields.io/badge/changelog-what's%20new-5b8def" alt="Changelog" />
|
|
17
|
+
</a>
|
|
15
18
|
<img src="https://img.shields.io/badge/node-%E2%89%A522.19-brightgreen" alt="Node >= 22.19" />
|
|
16
19
|
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" />
|
|
17
20
|
<img src="https://img.shields.io/badge/permissions-safe%20by%20default-2ea44f" alt="Safe by default" />
|
|
@@ -247,6 +250,12 @@ npm run typecheck
|
|
|
247
250
|
npm test
|
|
248
251
|
```
|
|
249
252
|
|
|
253
|
+
## Changelog
|
|
254
|
+
|
|
255
|
+
Release notes and what's new in each version live on the
|
|
256
|
+
[**GitHub releases page**](https://github.com/privateer-agent/privateer-agent/releases).
|
|
257
|
+
Privateer keeps its startup clean — the app won't dump a changelog into your terminal.
|
|
258
|
+
|
|
250
259
|
## License
|
|
251
260
|
|
|
252
261
|
[MIT](LICENSE) © Patrick
|
package/bin/privateer-tui
CHANGED
|
@@ -18,6 +18,15 @@ while [ -h "$SOURCE" ]; do
|
|
|
18
18
|
done
|
|
19
19
|
REPO="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
|
|
20
20
|
|
|
21
|
+
# `privateer update` — pull the latest release from npm and exit. Handled here in the
|
|
22
|
+
# launcher (not the TUI) so it works even when a broken install won't boot. npm rewrites
|
|
23
|
+
# the global bin in place; replacing it while this process runs is safe on unix.
|
|
24
|
+
if [ "${1:-}" = "update" ]; then
|
|
25
|
+
echo "Updating privateer-agent to the latest release…"
|
|
26
|
+
npm install -g privateer-agent@latest
|
|
27
|
+
exit $?
|
|
28
|
+
fi
|
|
29
|
+
|
|
21
30
|
pick_node() {
|
|
22
31
|
if command -v node >/dev/null 2>&1 \
|
|
23
32
|
&& node -e 'process.exit((+process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' 2>/dev/null; then
|
|
@@ -59,17 +68,46 @@ shim pi-subagents "$REPO/node_modules/pi-subagents/src/extension/index.ts"
|
|
|
59
68
|
|
|
60
69
|
CLI="$REPO/node_modules/@earendil-works/pi-coding-agent/dist/cli.js"
|
|
61
70
|
export PI_CODING_AGENT_DIR="$AGENT_DIR"
|
|
71
|
+
# Suppress Pi's upstream "Update Available — run `pi update`" banner. It's noise on a
|
|
72
|
+
# Privateer install (our banner is the startup surface), it leaks the "pi" name, and
|
|
73
|
+
# `pi update` would fight our npm-managed install. PI_SKIP_VERSION_CHECK disables ONLY
|
|
74
|
+
# the version fetch — unlike PI_OFFLINE it still lets fd/rg download on first run.
|
|
75
|
+
export PI_SKIP_VERSION_CHECK=1
|
|
62
76
|
NODE_BIN="$(pick_node)"
|
|
63
77
|
|
|
64
|
-
# Quiet Pi's built-in startup
|
|
65
|
-
#
|
|
66
|
-
#
|
|
78
|
+
# Quiet Pi's built-in startup chatter so our banner is the only greeting. Each key is
|
|
79
|
+
# set only when unset, so a user's own settings.json toggle (or --verbose) still wins:
|
|
80
|
+
# quietStartup — hide the [Extensions]/[Context]/[Skills]/… resource listing
|
|
81
|
+
# collapseChangelog — never dump Pi's "What's New" changelog wall into the chat
|
|
82
|
+
# lastChangelogVersion — sentinel so Pi finds no "new" entries to show on upgrade;
|
|
83
|
+
# release notes live in the npm README / GitHub releases instead
|
|
67
84
|
"$NODE_BIN" -e '
|
|
68
85
|
const fs = require("fs"), p = process.argv[1];
|
|
69
86
|
let s = {}; try { s = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
70
|
-
|
|
87
|
+
let m = false;
|
|
88
|
+
if (s.quietStartup === undefined) { s.quietStartup = true; m = true; }
|
|
89
|
+
if (s.collapseChangelog === undefined) { s.collapseChangelog = true; m = true; }
|
|
90
|
+
if (s.lastChangelogVersion === undefined) { s.lastChangelogVersion = "9999.0.0"; m = true; }
|
|
91
|
+
if (m) fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n");
|
|
71
92
|
' "$AGENT_DIR/settings.json" 2>/dev/null || true
|
|
72
93
|
|
|
94
|
+
# Passive update check: refresh the cached "latest npm version" at most ~daily, in the
|
|
95
|
+
# BACKGROUND so it never blocks or breaks launch (offline-safe — a failed fetch just
|
|
96
|
+
# leaves the stale cache in place). The banner (privateer-brand) reads this cache and
|
|
97
|
+
# shows a one-line "↑ vX available · run privateer update" notice when we're behind. We
|
|
98
|
+
# never auto-install — the user stays in control of when new code lands (the whole point
|
|
99
|
+
# of an attestable tool). Detached with </dev/null &, so it outlives our exec into node.
|
|
100
|
+
UPDATE_CACHE="${PRIVATEER_HOME:-$HOME/.privateer}/update-check.json"
|
|
101
|
+
if [ -z "$(find "$UPDATE_CACHE" -mtime -1 2>/dev/null)" ]; then
|
|
102
|
+
(
|
|
103
|
+
latest="$(npm view privateer-agent version 2>/dev/null || true)"
|
|
104
|
+
case "$latest" in
|
|
105
|
+
[0-9]*) printf '{"latest":"%s"}\n' "$latest" > "$UPDATE_CACHE.tmp" 2>/dev/null \
|
|
106
|
+
&& mv -f "$UPDATE_CACHE.tmp" "$UPDATE_CACHE" 2>/dev/null ;;
|
|
107
|
+
esac
|
|
108
|
+
) </dev/null >/dev/null 2>&1 &
|
|
109
|
+
fi
|
|
110
|
+
|
|
73
111
|
# Default model: when signed in to a Privateer account, default to GLM 5.1 on the
|
|
74
112
|
# account's NEAR confidential-compute (TEE) channel — attestable, strongest privacy
|
|
75
113
|
# tier. Otherwise (BYO key, no account) fall back to a cheap OpenRouter model. An
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
// device-code flow the account channel needs.
|
|
10
10
|
//
|
|
11
11
|
// Pi already owns /login and /logout for PROVIDER auth (and /whoami), so we do NOT
|
|
12
|
-
// shadow them.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// shadow them. The account provider now registers unconditionally (see
|
|
13
|
+
// makeAccountProvider), so Privateer appears under Pi's /login "Use a subscription"
|
|
14
|
+
// list and a first-time user CAN sign in through provider auth. /signin remains as a
|
|
15
|
+
// friendlier, dedicated shortcut that drives the same account device-code flow
|
|
16
|
+
// directly — one obvious command instead of /login → pick a provider.
|
|
15
17
|
//
|
|
16
18
|
// On a successful /signin we hot-register the account provider so privateer/* models
|
|
17
|
-
// appear immediately
|
|
19
|
+
// appear immediately (the account catalog refreshes to the live listing without a
|
|
20
|
+
// restart).
|
|
18
21
|
|
|
19
22
|
import { readFileSync } from "node:fs";
|
|
20
23
|
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
21
25
|
import * as priv from "../src/auth/privateer.ts";
|
|
22
26
|
import { makeAccountProvider } from "../src/providers/account.ts";
|
|
23
27
|
|
|
@@ -54,7 +58,7 @@ const ANCHOR = [
|
|
|
54
58
|
" .-----. ", // lock body top (the shackle's base)
|
|
55
59
|
" | o | ", // lock body + keyhole
|
|
56
60
|
" '--+--' ", // lock body base, shank exits
|
|
57
|
-
"
|
|
61
|
+
" /\\ | /\\ ", // stock — arms flare from the shank (each \\ is one backslash)
|
|
58
62
|
" \\ | / ", // arms
|
|
59
63
|
" \\_|_/ ", // flukes
|
|
60
64
|
];
|
|
@@ -102,6 +106,34 @@ function accountLine(modelProvider?: string): string {
|
|
|
102
106
|
return `${DIM}not signed in · ${OCEAN_LIGHT}/signin${DIM} to connect your account${RESET}`;
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
// Is dotted version `a` newer than `b`? Plain numeric compare of major.minor.patch —
|
|
110
|
+
// enough for our npm releases; anything unparseable sorts as 0 and is treated as older.
|
|
111
|
+
function isNewer(a: string, b: string): boolean {
|
|
112
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
113
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
114
|
+
for (let i = 0; i < 3; i++) {
|
|
115
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
116
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The "update available" banner line, or "" when we're current / offline / unchecked.
|
|
122
|
+
// Reads the cache the launcher refreshes in the background (see bin/privateer-tui) —
|
|
123
|
+
// never fetches here, so the banner stays synchronous and never blocks on the network.
|
|
124
|
+
function updateNotice(): string {
|
|
125
|
+
try {
|
|
126
|
+
const home = process.env.PRIVATEER_HOME || join(homedir(), ".privateer");
|
|
127
|
+
const { latest } = JSON.parse(readFileSync(join(home, "update-check.json"), "utf8"));
|
|
128
|
+
if (typeof latest === "string" && isNewer(latest, VERSION)) {
|
|
129
|
+
return `${YELLOW}↑ v${latest} available${DIM} · run ${RESET}${OCEAN_LIGHT}privateer update${RESET}`;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// no cache yet, unreadable, or malformed — show nothing.
|
|
133
|
+
}
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
|
|
105
137
|
// Compose the framed banner: anchor column + text column, inside a rounded accent box.
|
|
106
138
|
function renderBanner(width: number, modelProvider?: string): string[] {
|
|
107
139
|
// Leading blanks drop the text block so the wordmark sits beside the lock body and
|
|
@@ -116,8 +148,11 @@ function renderBanner(width: number, modelProvider?: string): string[] {
|
|
|
116
148
|
`${DIM}privateer-agent ${OCEAN_LIGHT}v${VERSION}${RESET}`,
|
|
117
149
|
`${OCEAN_LIGHT}${shortCwd()}${RESET}`,
|
|
118
150
|
];
|
|
119
|
-
// Build the body rows (anchor + gutter + text).
|
|
151
|
+
// Build the body rows (anchor + gutter + text). A pending-update notice, if any, gets
|
|
152
|
+
// its own row under the block, indented to sit beneath the text column.
|
|
120
153
|
const rows = ANCHOR.map((a, i) => `${OCEAN}${a}${RESET} ${right[i] ?? ""}`);
|
|
154
|
+
const notice = updateNotice();
|
|
155
|
+
if (notice) rows.push(` ${notice}`);
|
|
121
156
|
const cap = Math.max(20, width - 4); // 2 border cells + 2 padding
|
|
122
157
|
const inner = Math.min(cap, Math.max(...rows.map(vlen)));
|
|
123
158
|
const bar = "─".repeat(inner + 2);
|
|
@@ -150,7 +185,6 @@ function accountBadge(): string {
|
|
|
150
185
|
export default function privateerBrand(pi: any): void {
|
|
151
186
|
let currentModelProvider: string | undefined;
|
|
152
187
|
let ctxRef: any = null;
|
|
153
|
-
let greeted = false;
|
|
154
188
|
|
|
155
189
|
const setHeader = (ctx: any) =>
|
|
156
190
|
ctx?.ui?.setHeader?.(() => headerComponent(currentModelProvider));
|
|
@@ -161,6 +195,31 @@ export default function privateerBrand(pi: any): void {
|
|
|
161
195
|
ctx?.ui?.setStatus?.("account", accountBadge());
|
|
162
196
|
};
|
|
163
197
|
|
|
198
|
+
// /update — run the global npm install in a child process and report the outcome via
|
|
199
|
+
// notify (the TUI keeps running the OLD code; npm swaps the global bin's inode in
|
|
200
|
+
// place, so replacing it under us is safe and the new version loads on next launch).
|
|
201
|
+
async function doUpdate(ctx: any): Promise<void> {
|
|
202
|
+
ctx?.ui?.notify?.("Updating Privateer — running npm install -g privateer-agent@latest…", "info");
|
|
203
|
+
try {
|
|
204
|
+
const { execFile } = await import("node:child_process");
|
|
205
|
+
const stderr: string = await new Promise((resolve, reject) => {
|
|
206
|
+
execFile(
|
|
207
|
+
"npm",
|
|
208
|
+
["install", "-g", "privateer-agent@latest"],
|
|
209
|
+
{ timeout: 180_000 },
|
|
210
|
+
(err, _out, errOut) => (err ? reject(new Error(String(errOut || err.message).trim())) : resolve(String(errOut || ""))),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
void stderr;
|
|
214
|
+
ctx?.ui?.notify?.("Updated. Restart `privateer` to run the new version.", "info");
|
|
215
|
+
} catch (e) {
|
|
216
|
+
ctx?.ui?.notify?.(
|
|
217
|
+
`Update failed: ${(e as Error).message || e}. Try manually: npm install -g privateer-agent@latest`,
|
|
218
|
+
"error",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
164
223
|
async function doSignIn(ctx: any): Promise<void> {
|
|
165
224
|
if (priv.hasCredentials()) {
|
|
166
225
|
const u = priv.currentUser();
|
|
@@ -227,13 +286,8 @@ export default function privateerBrand(pi: any): void {
|
|
|
227
286
|
if (!ctx?.hasUI) return; // headless (print/json): no banner or prompts
|
|
228
287
|
ctx?.ui?.setTitle?.("Privateer");
|
|
229
288
|
refresh(ctx);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
ctx?.ui?.notify?.(
|
|
233
|
-
"Not signed in — run /signin to connect your Privateer account (adds hosted models + remote access).",
|
|
234
|
-
"info",
|
|
235
|
-
);
|
|
236
|
-
}
|
|
289
|
+
// No startup notify here: the banner's account line already surfaces the
|
|
290
|
+
// "not signed in · /signin" prompt, so a second line would just be noise.
|
|
237
291
|
});
|
|
238
292
|
|
|
239
293
|
// Keep the header's account line in sync with the picked model (the "this model
|
|
@@ -254,6 +308,10 @@ export default function privateerBrand(pi: any): void {
|
|
|
254
308
|
ctxRef?.ui?.notify?.("Your Privateer session expired. Run /signin to sign back in.", "warning");
|
|
255
309
|
});
|
|
256
310
|
|
|
311
|
+
pi.registerCommand?.("update", {
|
|
312
|
+
description: "Update Privateer to the latest release (npm i -g privateer-agent@latest)",
|
|
313
|
+
handler: (_args: string, ctx: any) => doUpdate(ctx),
|
|
314
|
+
});
|
|
257
315
|
pi.registerCommand?.("signin", {
|
|
258
316
|
description: "Sign in to your Privateer account (device-code flow)",
|
|
259
317
|
handler: (_args: string, ctx: any) => doSignIn(ctx),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Privateer — a provider-agnostic, safe-by-default terminal coding agent with TEE/Tinfoil attestation, rebuilt on the Pi toolkit. Bring your own model across 20 providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/providers/account.ts
CHANGED
|
@@ -136,8 +136,16 @@ export async function accountPosture(modelId: string): Promise<AccountPosture> {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Extension factory: registers the account provider
|
|
140
|
-
//
|
|
139
|
+
// Extension factory: registers the account provider so `/login` can offer it.
|
|
140
|
+
//
|
|
141
|
+
// We register UNCONDITIONALLY (not only when a machine login already exists). Pi's
|
|
142
|
+
// `/login` builds its "Use a subscription" list from the OAuth providers registered
|
|
143
|
+
// here (authStorage.getOAuthProviders()); a not-yet-logged-in machine has no
|
|
144
|
+
// credentials, so gating registration on hasCredentials() left `/login` with no
|
|
145
|
+
// Privateer option — the classic chicken-and-egg where you can't log in because
|
|
146
|
+
// you're not logged in. The OAuth provider's login() itself runs the device-code
|
|
147
|
+
// flow when hasCredentials() is false (see privateerOAuthProvider.login), so first
|
|
148
|
+
// login works entirely through Pi once the provider is present.
|
|
141
149
|
//
|
|
142
150
|
// Registration is SYNCHRONOUS (seeded with the fallback), then refined once the live
|
|
143
151
|
// catalog is fetched. This matters: Pi flushes provider registrations made during the
|
|
@@ -151,7 +159,7 @@ export function makeAccountProvider() {
|
|
|
151
159
|
return (pi: {
|
|
152
160
|
registerProvider?: (name: string, config: unknown) => void;
|
|
153
161
|
}): void => {
|
|
154
|
-
if (
|
|
162
|
+
if (typeof pi.registerProvider !== "function") return;
|
|
155
163
|
const register = (ids: string[]): void =>
|
|
156
164
|
pi.registerProvider!("privateer", {
|
|
157
165
|
name: "Privateer account",
|