privateer-agent 0.6.7 → 0.6.9
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/SECURITY.md +5 -1
- package/bin/privateer-launch.mjs +33 -23
- package/extensions/privateer-brand.ts +152 -60
- package/extensions/privateer-connect.ts +754 -0
- package/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.80.3.patch +128 -4
- package/src/auth/accountSessions.ts +4 -1
- package/src/auth/privateer.ts +94 -22
- package/src/cli/chat.ts +2 -2
- package/src/daemon/index.ts +2 -2
- package/src/mcp/catalog.ts +178 -0
- package/src/providers/account.ts +132 -37
- package/src/providers/defaultModel.ts +42 -26
package/SECURITY.md
CHANGED
|
@@ -30,7 +30,11 @@ npm audit signatures # verifies registry signatures + p
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
If a version lacks provenance, it did not come from this workflow. Treat that as
|
|
33
|
-
suspicious and report it.
|
|
33
|
+
suspicious and report it — with one documented exception: **0.6.7 is the first release
|
|
34
|
+
published this way.** Trusted publishing was misconfigured until then, so every earlier
|
|
35
|
+
version (through 0.6.6) was published by hand from a maintainer's machine and carries no
|
|
36
|
+
attestation. Those are not forgeries, but they are not independently verifiable either.
|
|
37
|
+
If that distinction matters to you, use 0.6.7 or later.
|
|
34
38
|
|
|
35
39
|
## The permission gate
|
|
36
40
|
|
package/bin/privateer-launch.mjs
CHANGED
|
@@ -146,6 +146,7 @@ else {
|
|
|
146
146
|
const MANAGED = [
|
|
147
147
|
"privateer-brand", "privateer-context", "privateer-gate", "privateer-account",
|
|
148
148
|
"privateer-models", "privateer-posture", "privateer-tools", "privateer-privacy",
|
|
149
|
+
"privateer-connect",
|
|
149
150
|
"pi-privacy", "pi-web-access", "rpiv-web-tools", "pi-mcp-adapter", "pi-hypa", "pi-subagents",
|
|
150
151
|
];
|
|
151
152
|
for (const name of MANAGED) fs.rmSync(path.join(EXT_DIR, `${name}.ts`), { force: true });
|
|
@@ -171,6 +172,7 @@ else {
|
|
|
171
172
|
shim("privateer-posture", ext("privateer-posture.ts"));
|
|
172
173
|
shim("privateer-tools", ext("privateer-tools.ts"));
|
|
173
174
|
shim("privateer-privacy", ext("privateer-privacy.ts")); // pi-privacy + account tier resolver
|
|
175
|
+
shim("privateer-connect", ext("privateer-connect.ts")); // /connect — MCP connector manager
|
|
174
176
|
shim("rpiv-web-tools", dep("@juicesharp/rpiv-web-tools", "index.ts")); // private web tools
|
|
175
177
|
shim("pi-mcp-adapter", dep("pi-mcp-adapter", "index.ts"));
|
|
176
178
|
shim("pi-hypa", dep("@hypabolic/pi-hypa", "extensions", "index.ts"));
|
|
@@ -214,28 +216,39 @@ else {
|
|
|
214
216
|
// auto-install. Fire-and-forget: the event loop stays alive while the TUI child runs.
|
|
215
217
|
refreshUpdateCache();
|
|
216
218
|
|
|
217
|
-
// Default model.
|
|
218
|
-
//
|
|
219
|
-
//
|
|
219
|
+
// Default model. Mirrors src/providers/defaultModel.ts resolveDefaultModel() — keep
|
|
220
|
+
// the two in step. Tinfoil's GLM 5.2 is the default either way: direct when the user
|
|
221
|
+
// has a Tinfoil key (pi-privacy can client-attest the enclave), over the Privateer
|
|
222
|
+
// subscription otherwise.
|
|
223
|
+
//
|
|
224
|
+
// The last branch is the important one. A signed-out, keyless terminal used to launch
|
|
225
|
+
// on `openrouter/openai/gpt-4o-mini`, which it had no key for — so the first prompt
|
|
226
|
+
// died on "No API key found for openrouter", /login couldn't fix it (nothing switched
|
|
227
|
+
// the live model), and the error named a provider the user had never heard of. It now
|
|
228
|
+
// launches on the SAME account model it will use once signed in: nothing to switch,
|
|
229
|
+
// the status bar shows what they're about to get, and the error until then names
|
|
230
|
+
// Privateer and points at /login.
|
|
220
231
|
const CRED = path.join(PRIVATEER_HOME, "credentials.json");
|
|
221
232
|
const signedIn = fs.existsSync(CRED);
|
|
233
|
+
const ACCOUNT_MODEL = "privateer/tinfoil/glm-5-2";
|
|
222
234
|
const MODEL = process.env.PRIVATEER_MODEL
|
|
223
235
|
? process.env.PRIVATEER_MODEL
|
|
224
236
|
: haveTinfoilKey()
|
|
225
237
|
? "tinfoil/glm-5-2"
|
|
226
238
|
: signedIn
|
|
227
|
-
?
|
|
228
|
-
: "
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
+
? ACCOUNT_MODEL
|
|
240
|
+
: haveKey("ANTHROPIC_API_KEY")
|
|
241
|
+
? "anthropic/claude-opus-4-8"
|
|
242
|
+
: haveKey("OPENAI_API_KEY")
|
|
243
|
+
? "openai/gpt-5.5"
|
|
244
|
+
: haveKey("OPENROUTER_API_KEY")
|
|
245
|
+
? "openrouter/openai/gpt-4o-mini"
|
|
246
|
+
: ACCOUNT_MODEL;
|
|
247
|
+
|
|
248
|
+
// Nothing to run with: no model named, no BYO key, not signed in. The TUI still boots
|
|
249
|
+
// (that's where /login lives), but say why up front — a returning user whose login
|
|
250
|
+
// file vanished otherwise has no way to tell a cleared session from a first run.
|
|
251
|
+
if (!signedIn && !process.env.PRIVATEER_MODEL && !haveByoKey()) {
|
|
239
252
|
warnKeylessLaunch();
|
|
240
253
|
}
|
|
241
254
|
|
|
@@ -367,20 +380,17 @@ function warnKeylessLaunch() {
|
|
|
367
380
|
? [
|
|
368
381
|
"",
|
|
369
382
|
" ⚓ Your Privateer login is missing — this terminal isn't signed in.",
|
|
370
|
-
` (no ${path.join(PRIVATEER_HOME, "credentials.json")})`,
|
|
371
383
|
"",
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
" prompting fails with \"No API key found\" because no model key is set.",
|
|
384
|
+
" Run /login and approve the code in the Privateer app. You'll be back on your",
|
|
385
|
+
" subscription models straight away — no API key needed.",
|
|
375
386
|
"",
|
|
376
387
|
]
|
|
377
388
|
: [
|
|
378
389
|
"",
|
|
379
|
-
" ⚓
|
|
390
|
+
" ⚓ Welcome aboard. Run /login to connect your Privateer account.",
|
|
380
391
|
"",
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
" prompting fails with \"No API key found\".",
|
|
392
|
+
" One approval in the Privateer app and you're running Tinfoil GLM 5.2 in a",
|
|
393
|
+
" trusted enclave — no API key needed. Prefer your own key? /login keys.",
|
|
384
394
|
"",
|
|
385
395
|
];
|
|
386
396
|
process.stderr.write(lines.join("\n") + "\n");
|
|
@@ -5,25 +5,30 @@
|
|
|
5
5
|
// 1. A branded startup header — the anchor mark + "✻ PRIVATEER" wordmark + the
|
|
6
6
|
// "Chart your own course privately." tagline (ported from tree-cli's Banner).
|
|
7
7
|
// 2. A live status-bar badge (⚓ account) showing the sign-in state at a glance.
|
|
8
|
-
// 3.
|
|
8
|
+
// 3. Auth UX — /login, /logout, and a /privateer hub — which drive the
|
|
9
9
|
// device-code flow the account channel needs.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
11
|
+
// ONE vocabulary, deliberately: log in / log out. This file used to avoid shadowing
|
|
12
|
+
// Pi's built-in /login and /logout and shipped /signin and /signout alongside them,
|
|
13
|
+
// which left the user with two auth vocabularies and — worse — a /logout that did
|
|
14
|
+
// not log you out. Pi's /logout only clears Pi's own authStorage; the Privateer
|
|
15
|
+
// machine login lives in ~/.privateer/credentials.json and survived it, so /logout
|
|
16
|
+
// on a signed-in machine reported "No stored credentials to remove" and changed
|
|
17
|
+
// nothing. We now own both verbs: /logout here is canonical and Pi's built-in is
|
|
18
|
+
// redirected to it (patches/, same mechanism as the /model → /models redirect).
|
|
19
|
+
// /signin and /signout stay as undocumented aliases for muscle memory.
|
|
17
20
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
+
// The account provider also registers unconditionally (see makeAccountProvider), so
|
|
22
|
+
// Privateer appears under Pi's "Use a subscription" list and a first-time user can
|
|
23
|
+
// log in that way too. On a successful login we hot-register the account provider so
|
|
24
|
+
// privateer/* models appear immediately (the account catalog refreshes to the live
|
|
25
|
+
// listing without a restart).
|
|
21
26
|
|
|
22
27
|
import { readFileSync, appendFileSync } from "node:fs";
|
|
23
28
|
import { homedir } from "node:os";
|
|
24
29
|
import { join } from "node:path";
|
|
25
30
|
import * as priv from "../src/auth/privateer.ts";
|
|
26
|
-
import { makeAccountProvider } from "../src/providers/account.ts";
|
|
31
|
+
import { armAccountCredential, makeAccountProvider } from "../src/providers/account.ts";
|
|
27
32
|
import { resolveSignedInModel } from "../src/providers/defaultModel.ts";
|
|
28
33
|
import { discoverContextFiles, onContextChanged } from "../src/context.ts";
|
|
29
34
|
import { type Palette, paletteFor } from "../src/ui/palette.ts";
|
|
@@ -345,11 +350,18 @@ export default function privateerBrand(pi: any): void {
|
|
|
345
350
|
}
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
// /login. Signing in must leave the terminal ABLE TO PROMPT, not merely "connected" —
|
|
354
|
+
// so this runs the device flow, hot-registers the account provider, then hands off to
|
|
355
|
+
// activateSignedInModel to arm the channel and select the model. Every step reports.
|
|
348
356
|
async function doSignIn(ctx: any): Promise<void> {
|
|
349
357
|
if (priv.hasCredentials()) {
|
|
350
358
|
const u = priv.currentUser();
|
|
359
|
+
// Already linked: still make sure THIS session can use the account (a terminal
|
|
360
|
+
// launched before the login, or one whose channel never armed, otherwise sits
|
|
361
|
+
// "signed in" and unusable), then point at the two things they might have meant.
|
|
362
|
+
await activateSignedInModel(ctx, { switchModel: false });
|
|
351
363
|
return ctx?.ui?.notify?.(
|
|
352
|
-
`
|
|
364
|
+
`Signed in as ${u?.email ?? u?.id}. Run /logout to switch accounts, or /login keys to add a provider API key.`,
|
|
353
365
|
"info",
|
|
354
366
|
);
|
|
355
367
|
}
|
|
@@ -367,7 +379,7 @@ export default function privateerBrand(pi: any): void {
|
|
|
367
379
|
`${p.DIM}Approve this terminal in the Privateer app:${p.RESET}`,
|
|
368
380
|
` code ${p.BOLD}${p.ACCENT}${userCode}${p.RESET}`,
|
|
369
381
|
uri ? `${p.DIM} or open ${p.RESET}${p.INK}${uri}${p.RESET}` : "",
|
|
370
|
-
`${p.DIM} waiting for approval
|
|
382
|
+
`${p.DIM} waiting for approval… ${p.RESET}${p.DIM}(esc to cancel · ${p.RESET}${p.INK}/login keys${p.DIM} to use your own API key instead)${p.RESET}`,
|
|
371
383
|
].filter(Boolean),
|
|
372
384
|
{ placement: "aboveEditor" },
|
|
373
385
|
);
|
|
@@ -381,20 +393,31 @@ export default function privateerBrand(pi: any): void {
|
|
|
381
393
|
/* provider list fetch failed — models appear on next launch */
|
|
382
394
|
}
|
|
383
395
|
refresh(ctx);
|
|
384
|
-
ctx?.ui?.notify?.(`Signed in as ${user.email ?? user.id}
|
|
396
|
+
ctx?.ui?.notify?.(`Signed in as ${user.email ?? user.id}.`, "info");
|
|
397
|
+
// Arm the account channel and select the model, IN THIS SESSION. runDeviceLogin
|
|
398
|
+
// fires onSignedIn too, which does the same work — this await is what makes the
|
|
399
|
+
// outcome (and any failure) land before we hand the prompt back to the user.
|
|
400
|
+
await activateSignedInModel(ctx);
|
|
385
401
|
} catch (e) {
|
|
386
402
|
ctx?.ui?.setWidget?.("privateer-signin", undefined);
|
|
387
403
|
ctx?.ui?.notify?.((e as Error).message || "Sign-in failed.", "error");
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
|
|
407
|
+
// Log out of this MACHINE — the login and every terminal spawned from it (see
|
|
408
|
+
// priv.logout). Unlike the old terminal-scoped sign-out this makes a blocking
|
|
409
|
+
// network call, so say what's happening before the round trip rather than after.
|
|
391
410
|
async function doSignOut(ctx: any): Promise<void> {
|
|
392
411
|
if (!priv.hasCredentials()) return ctx?.ui?.notify?.("Not signed in.", "info");
|
|
393
412
|
const u = priv.currentUser();
|
|
394
|
-
|
|
413
|
+
ctx?.ui?.notify?.("Signing out of Privateer…", "info");
|
|
414
|
+
await priv.logout(); // never throws: local state is wiped whatever the network did
|
|
395
415
|
dropPersistedAccount(ctx);
|
|
396
416
|
refresh(ctx);
|
|
397
|
-
ctx?.ui?.notify?.(
|
|
417
|
+
ctx?.ui?.notify?.(
|
|
418
|
+
`Signed out${u?.email ? ` (${u.email})` : ""} — this machine and its terminals. Drop anchor for now.`,
|
|
419
|
+
"info",
|
|
420
|
+
);
|
|
398
421
|
}
|
|
399
422
|
|
|
400
423
|
function showStatus(ctx: any): void {
|
|
@@ -402,50 +425,103 @@ export default function privateerBrand(pi: any): void {
|
|
|
402
425
|
ctx?.ui?.notify?.(
|
|
403
426
|
u
|
|
404
427
|
? `Signed in to Privateer as ${u.email ?? u.id}.`
|
|
405
|
-
: "Not
|
|
428
|
+
: "Not logged in. Run /login to connect your Privateer account.",
|
|
406
429
|
"info",
|
|
407
430
|
);
|
|
408
431
|
}
|
|
409
432
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
433
|
+
// Make the terminal USABLE the instant the user signs in — the whole point of
|
|
434
|
+
// logging in, and the step that used to be missing. Two halves, in this order:
|
|
435
|
+
//
|
|
436
|
+
// 1. ARM the account channel. Pi stores an OAuth credential only for a login it
|
|
437
|
+
// drove itself, so after our own device-code /login the `privateer` provider has
|
|
438
|
+
// no key at all. Arming first also matters for the now-common case where the
|
|
439
|
+
// launch model ALREADY is the account model (a signed-out terminal boots on it,
|
|
440
|
+
// see providers/defaultModel.ts): there's no switch to make, only a key to fetch,
|
|
441
|
+
// and the old early-return skipped it and left the next prompt to fail.
|
|
442
|
+
// 2. SWITCH the live model, if we aren't already on it. A terminal that launched on
|
|
443
|
+
// a BYO key stays on that key otherwise, so signing in appears to do nothing.
|
|
444
|
+
//
|
|
445
|
+
// resolveSignedInModel picks Tinfoil GLM 5.2 — direct (client-attested) when a Tinfoil
|
|
446
|
+
// key is present, over the subscription otherwise. A deliberate PRIVATEER_MODEL is
|
|
447
|
+
// never overridden. Idempotent: sign-in fires this twice by design (once when
|
|
448
|
+
// credentials land, once when the channel is ready), and a second run is a no-op.
|
|
449
|
+
// Best-effort — but a failure is now REPORTED, because silently leaving the user on an
|
|
450
|
+
// unusable model is exactly the bug this replaces.
|
|
451
|
+
let activating = false;
|
|
452
|
+
async function activateSignedInModel(ctx: any, opts: { switchModel?: boolean } = {}): Promise<void> {
|
|
453
|
+
if (activating || process.env.PRIVATEER_MODEL?.trim()) return; // in-flight, or a deliberate override
|
|
454
|
+
activating = true;
|
|
455
|
+
try {
|
|
456
|
+
const spec = resolveSignedInModel();
|
|
457
|
+
const slash = spec.indexOf("/");
|
|
458
|
+
if (slash <= 0) return;
|
|
459
|
+
const provider = spec.slice(0, slash), id = spec.slice(slash + 1);
|
|
460
|
+
|
|
461
|
+
// The account channel needs a live session token before any privateer/* model can
|
|
462
|
+
// run. Retry briefly: this can be racing the credential the login itself minted.
|
|
463
|
+
if (provider === "privateer") {
|
|
464
|
+
let armed = false;
|
|
465
|
+
for (let attempt = 0; attempt < 3 && !armed; attempt++) {
|
|
466
|
+
armed = await armAccountCredential(ctx, { notify: false });
|
|
467
|
+
if (!armed) await new Promise((r) => setTimeout(r, 500));
|
|
468
|
+
}
|
|
469
|
+
if (!armed) {
|
|
470
|
+
dbg("activateSignedInModel: account channel not armed");
|
|
471
|
+
ctx?.ui?.notify?.(
|
|
472
|
+
"Signed in, but this terminal couldn't open an account session. Check your connection and run /login again, or sign a terminal out in the app if you're at the device limit.",
|
|
473
|
+
"error",
|
|
474
|
+
);
|
|
441
475
|
return;
|
|
442
476
|
}
|
|
443
|
-
} catch (e) {
|
|
444
|
-
dbg(`activateSignedInModel: setModel threw ${(e as Error).message}`);
|
|
445
477
|
}
|
|
446
|
-
|
|
478
|
+
|
|
479
|
+
// Re-running /login while already signed in arms the channel but must NOT move the
|
|
480
|
+
// user off a model they picked with /models. Only a genuine sign-in switches.
|
|
481
|
+
if (opts.switchModel === false) return;
|
|
482
|
+
|
|
483
|
+
// Already on the target — the common case now that a signed-out terminal launches
|
|
484
|
+
// on the account model. Nothing to switch, but SAY so: the channel just went live
|
|
485
|
+
// under them, and silence after a login is what made the old flow feel broken.
|
|
486
|
+
const currentSpec = ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
|
|
487
|
+
if (currentSpec === spec) {
|
|
488
|
+
dbg(`activateSignedInModel: already on ${spec}`);
|
|
489
|
+
refresh(ctx);
|
|
490
|
+
ctx?.ui?.notify?.(`${spec} is ready — private inference on your Privateer account.`, "info");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const reg = ctx?.modelRegistry;
|
|
495
|
+
if (!reg?.find || typeof pi.setModel !== "function") return;
|
|
496
|
+
const model = reg.find(provider, id);
|
|
497
|
+
if (!model) {
|
|
498
|
+
// The provider's live catalog may still be loading (or this is the first sign-in
|
|
499
|
+
// of the run, before the account provider was registered). Say something useful
|
|
500
|
+
// rather than stranding them on a model their account can't bill.
|
|
501
|
+
dbg(`activateSignedInModel: ${spec} not in registry`);
|
|
502
|
+
ctx?.ui?.notify?.(`Signed in. Run /models to pick a model — ${spec} isn't loaded yet.`, "warning");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
506
|
+
try {
|
|
507
|
+
const ok = await pi.setModel(model);
|
|
508
|
+
if (ok !== false) {
|
|
509
|
+
currentModelProvider = provider;
|
|
510
|
+
refresh(ctx);
|
|
511
|
+
ctx?.ui?.notify?.(`Now using ${spec} — private inference on your Privateer account.`, "info");
|
|
512
|
+
dbg(`activateSignedInModel: switched to ${spec}`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
dbg(`activateSignedInModel: setModel threw ${(e as Error).message}`);
|
|
517
|
+
}
|
|
518
|
+
await new Promise((r) => setTimeout(r, 400)); // registry still settling — retry
|
|
519
|
+
}
|
|
520
|
+
dbg(`activateSignedInModel: gave up switching to ${spec}`);
|
|
521
|
+
ctx?.ui?.notify?.(`Signed in, but couldn't switch to ${spec}. Run /models to pick one.`, "warning");
|
|
522
|
+
} finally {
|
|
523
|
+
activating = false;
|
|
447
524
|
}
|
|
448
|
-
dbg(`activateSignedInModel: gave up switching to ${spec}`);
|
|
449
525
|
}
|
|
450
526
|
|
|
451
527
|
dbg("extension loaded, onSignedIn listener registering");
|
|
@@ -529,27 +605,43 @@ export default function privateerBrand(pi: any): void {
|
|
|
529
605
|
// that's now dead server-side (see dropPersistedAccount).
|
|
530
606
|
dropPersistedAccount(ctxRef);
|
|
531
607
|
refresh(ctxRef);
|
|
532
|
-
ctxRef?.ui?.notify?.("Your Privateer session expired. Run /
|
|
608
|
+
ctxRef?.ui?.notify?.("Your Privateer session expired. Run /login to sign back in.", "warning");
|
|
533
609
|
});
|
|
534
610
|
|
|
535
611
|
pi.registerCommand?.("update", {
|
|
536
612
|
description: "Update Privateer to the latest release (npm i -g privateer-agent@latest)",
|
|
537
613
|
handler: (_args: string, ctx: any) => doUpdate(ctx),
|
|
538
614
|
});
|
|
539
|
-
|
|
540
|
-
|
|
615
|
+
// ONE vocabulary: log in / log out. Pi's own built-ins are /login and /logout, so
|
|
616
|
+
// matching them is what makes the pair feel like a single concept instead of two
|
|
617
|
+
// half-overlapping ones (Pi's /logout used to clear only Pi's authStorage while the
|
|
618
|
+
// machine login lived on, which read as "logout doesn't work"). doSignIn/doSignOut
|
|
619
|
+
// are canonical; patches/ redirects Pi's built-ins to `/privateer login|logout`
|
|
620
|
+
// rather than to these registrations, so the redirect can't re-enter the branch it
|
|
621
|
+
// was dispatched from — the same mechanism as the /model → /models redirect.
|
|
622
|
+
//
|
|
623
|
+
// In interactive mode that redirect means the built-in wins and these two never
|
|
624
|
+
// dispatch (patches/ also silences the resulting conflict warning). They stay
|
|
625
|
+
// registered for rpc/print mode, which has no such redirect and reaches them by name.
|
|
626
|
+
//
|
|
627
|
+
// signin/signout stay registered as undocumented aliases — they were the shipped
|
|
628
|
+
// names, they're in muscle memory and in older docs, and an alias costs nothing.
|
|
629
|
+
pi.registerCommand?.("login", {
|
|
630
|
+
description: "Log in to your Privateer account · /login keys for a provider API key",
|
|
541
631
|
handler: (_args: string, ctx: any) => doSignIn(ctx),
|
|
542
632
|
});
|
|
543
|
-
pi.registerCommand?.("
|
|
544
|
-
description: "
|
|
633
|
+
pi.registerCommand?.("logout", {
|
|
634
|
+
description: "Log out of Privateer on this machine (revokes all its terminals)",
|
|
545
635
|
handler: (_args: string, ctx: any) => doSignOut(ctx),
|
|
546
636
|
});
|
|
637
|
+
pi.registerCommand?.("signin", { description: "", handler: (_a: string, ctx: any) => doSignIn(ctx) });
|
|
638
|
+
pi.registerCommand?.("signout", { description: "", handler: (_a: string, ctx: any) => doSignOut(ctx) });
|
|
547
639
|
pi.registerCommand?.("privateer", {
|
|
548
|
-
description: "Privateer account: /privateer [status |
|
|
640
|
+
description: "Privateer account: /privateer [status | login | logout]",
|
|
549
641
|
handler: (args: string, ctx: any) => {
|
|
550
642
|
const sub = String(args ?? "").trim().toLowerCase().split(/\s+/)[0];
|
|
551
|
-
if (sub === "
|
|
552
|
-
if (sub === "
|
|
643
|
+
if (sub === "login" || sub === "signin") return doSignIn(ctx);
|
|
644
|
+
if (sub === "logout" || sub === "signout") return doSignOut(ctx);
|
|
553
645
|
return showStatus(ctx);
|
|
554
646
|
},
|
|
555
647
|
});
|