privateer-agent 0.3.3 → 0.3.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/bin/privateer-tui +26 -0
- package/extensions/privateer-brand.ts +63 -2
- package/package.json +1 -1
- package/src/providers/account.ts +26 -10
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
|
|
@@ -82,6 +91,23 @@ if (s.lastChangelogVersion === undefined) { s.lastChangelogVersion = "9999.0.0";
|
|
|
82
91
|
if (m) fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n");
|
|
83
92
|
' "$AGENT_DIR/settings.json" 2>/dev/null || true
|
|
84
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
|
+
|
|
85
111
|
# Default model: when signed in to a Privateer account, default to GLM 5.1 on the
|
|
86
112
|
# account's NEAR confidential-compute (TEE) channel — attestable, strongest privacy
|
|
87
113
|
# tier. Otherwise (BYO key, no account) fall back to a cheap OpenRouter model. An
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { readFileSync } from "node:fs";
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
24
25
|
import * as priv from "../src/auth/privateer.ts";
|
|
25
26
|
import { makeAccountProvider } from "../src/providers/account.ts";
|
|
26
27
|
|
|
@@ -57,7 +58,7 @@ const ANCHOR = [
|
|
|
57
58
|
" .-----. ", // lock body top (the shackle's base)
|
|
58
59
|
" | o | ", // lock body + keyhole
|
|
59
60
|
" '--+--' ", // lock body base, shank exits
|
|
60
|
-
"
|
|
61
|
+
" /\\ | /\\ ", // stock — arms flare from the shank (each \\ is one backslash)
|
|
61
62
|
" \\ | / ", // arms
|
|
62
63
|
" \\_|_/ ", // flukes
|
|
63
64
|
];
|
|
@@ -105,6 +106,34 @@ function accountLine(modelProvider?: string): string {
|
|
|
105
106
|
return `${DIM}not signed in · ${OCEAN_LIGHT}/signin${DIM} to connect your account${RESET}`;
|
|
106
107
|
}
|
|
107
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
|
+
|
|
108
137
|
// Compose the framed banner: anchor column + text column, inside a rounded accent box.
|
|
109
138
|
function renderBanner(width: number, modelProvider?: string): string[] {
|
|
110
139
|
// Leading blanks drop the text block so the wordmark sits beside the lock body and
|
|
@@ -119,8 +148,11 @@ function renderBanner(width: number, modelProvider?: string): string[] {
|
|
|
119
148
|
`${DIM}privateer-agent ${OCEAN_LIGHT}v${VERSION}${RESET}`,
|
|
120
149
|
`${OCEAN_LIGHT}${shortCwd()}${RESET}`,
|
|
121
150
|
];
|
|
122
|
-
// 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.
|
|
123
153
|
const rows = ANCHOR.map((a, i) => `${OCEAN}${a}${RESET} ${right[i] ?? ""}`);
|
|
154
|
+
const notice = updateNotice();
|
|
155
|
+
if (notice) rows.push(` ${notice}`);
|
|
124
156
|
const cap = Math.max(20, width - 4); // 2 border cells + 2 padding
|
|
125
157
|
const inner = Math.min(cap, Math.max(...rows.map(vlen)));
|
|
126
158
|
const bar = "─".repeat(inner + 2);
|
|
@@ -163,6 +195,31 @@ export default function privateerBrand(pi: any): void {
|
|
|
163
195
|
ctx?.ui?.setStatus?.("account", accountBadge());
|
|
164
196
|
};
|
|
165
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
|
+
|
|
166
223
|
async function doSignIn(ctx: any): Promise<void> {
|
|
167
224
|
if (priv.hasCredentials()) {
|
|
168
225
|
const u = priv.currentUser();
|
|
@@ -251,6 +308,10 @@ export default function privateerBrand(pi: any): void {
|
|
|
251
308
|
ctxRef?.ui?.notify?.("Your Privateer session expired. Run /signin to sign back in.", "warning");
|
|
252
309
|
});
|
|
253
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
|
+
});
|
|
254
315
|
pi.registerCommand?.("signin", {
|
|
255
316
|
description: "Sign in to your Privateer account (device-code flow)",
|
|
256
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.5",
|
|
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
|
@@ -66,18 +66,34 @@ export async function fetchAccountModels(): Promise<string[]> {
|
|
|
66
66
|
export const privateerOAuthProvider = {
|
|
67
67
|
name: "Privateer account",
|
|
68
68
|
usesCallbackServer: false,
|
|
69
|
-
|
|
69
|
+
// Pi's login dialog passes `signal` (its cancel AbortController) alongside the
|
|
70
|
+
// callbacks. We MUST thread it into runDeviceLogin — otherwise escape/ctrl+c
|
|
71
|
+
// aborts the dialog's signal but our poll loop never sees it, the login()
|
|
72
|
+
// promise never settles, and Pi never restores the editor: the "Waiting for
|
|
73
|
+
// authentication…" screen hangs with no way out. See auth/privateer.ts
|
|
74
|
+
// pollForToken, which checks the signal and rejects with "Login cancelled.".
|
|
75
|
+
async login(cb: { onDeviceCode?: (info: unknown) => void; signal?: AbortSignal }) {
|
|
70
76
|
if (!hasCredentials()) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
cb.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
try {
|
|
78
|
+
await runDeviceLogin({
|
|
79
|
+
signal: cb.signal,
|
|
80
|
+
onCode: (code) =>
|
|
81
|
+
cb.onDeviceCode?.({
|
|
82
|
+
userCode: code.user_code,
|
|
83
|
+
verificationUri: code.verification_uri_complete ?? code.verification_uri ?? "",
|
|
84
|
+
intervalSeconds: code.interval,
|
|
85
|
+
expiresInSeconds: code.expires_in,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Normalize the cancel message to exactly "Login cancelled" (no period):
|
|
90
|
+
// Pi's login dialog only suppresses its "Failed to login…" error toast for
|
|
91
|
+
// that exact string, so a cancel should exit quietly, not flash an error.
|
|
92
|
+
if (cb.signal?.aborted) throw new Error("Login cancelled");
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
80
95
|
}
|
|
96
|
+
if (cb.signal?.aborted) throw new Error("Login cancelled");
|
|
81
97
|
return spawnAccountCredentials();
|
|
82
98
|
},
|
|
83
99
|
async refreshToken(creds: { refresh: string }) {
|