@phnx-labs/agents-cli 1.22.82 → 1.22.84
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/CHANGELOG.md +47 -0
- package/dist/commands/accounts.js +34 -6
- package/dist/commands/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -0
- package/dist/commands/view.js +3 -6
- package/dist/lib/account-registry.d.ts +40 -3
- package/dist/lib/account-registry.js +89 -18
- package/dist/lib/accounts/connect.d.ts +6 -0
- package/dist/lib/accounts/connect.js +70 -0
- package/dist/lib/claude-statusline.d.ts +13 -5
- package/dist/lib/claude-statusline.js +19 -6
- package/dist/lib/daemon-ticks.js +23 -2
- package/dist/lib/devices/fleet-inventory.js +3 -4
- package/dist/lib/devices/harness-inventory.js +4 -5
- package/dist/lib/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +103 -13
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- package/dist/lib/fleet-shared-state.d.ts +6 -0
- package/dist/lib/session/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +47 -2
- package/dist/lib/session/db.js +131 -4
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- package/dist/lib/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.84
|
|
4
|
+
|
|
5
|
+
- **`agents feed watch` is cheap enough to leave running (PHNX-3939).** The watcher
|
|
6
|
+
cost ~42% of a core steadily on a box with 1,437 activity logs / 64 MB, because
|
|
7
|
+
every 500 ms tick re-read and re-parsed the whole activity corpus and re-read
|
|
8
|
+
every row's block, resolution, and PR status. It now keeps a per-file cursor and
|
|
9
|
+
reads only the bytes appended since the last tick (`ActivityStream`), and
|
|
10
|
+
reconciles attention only when something announced a change — a block or
|
|
11
|
+
resolution written under the feed dir, watched directly — or when the 45 s
|
|
12
|
+
PR-status TTL has expired. Measured on the same box over 5 minutes, steady
|
|
13
|
+
state: `--local` **41.9% → 0.31% CPU** (RSS 313 MB → 140 MB) and the full
|
|
14
|
+
13-peer fleet fan-out **50.0% → 0.37% CPU** (RSS 606 MB → 143 MB), with an
|
|
15
|
+
identical envelope stream. Source: `cli/src/lib/feed/activity-stream.ts`,
|
|
16
|
+
`cli/src/lib/feed/watch.ts`.
|
|
17
|
+
|
|
18
|
+
- **A fleet peer that cannot be reached no longer gets a fresh `ssh` every few
|
|
19
|
+
seconds (PHNX-3939).** `agents feed watch` and `agents sessions watch` respawned
|
|
20
|
+
each peer's `--local` subscription on a bare 2 s timer with the child's stderr
|
|
21
|
+
discarded, so an offline box burned a ConnectTimeout-plus-2s cycle for the whole
|
|
22
|
+
life of the watcher and reported only `ssh exited 255`. Both fan-outs now share
|
|
23
|
+
one implementation with per-peer exponential backoff (2 s doubling to a 60 s
|
|
24
|
+
cap, reset on a healthy protocol event), the peer's own stderr surfaced as the
|
|
25
|
+
`unavailable` reason, and a peer parked after three consecutive failed spawns
|
|
26
|
+
until the device registry changes or the capped delay elapses. Source:
|
|
27
|
+
`cli/src/lib/session/remote/peer-stream.ts`.
|
|
28
|
+
|
|
29
|
+
- **`agents accounts connect` refuses on a worker before any install or browser login (PHNX-3940).** A worker never runs an interactive OAuth flow (credential-management.md invariant 7). On a non-headed device (`worker` or unmarked), connect throws and does nothing — no slot, no install, no browser. Add the account on a personal/desktop box; workers are provisioned from the durable credential. Source: `cli/src/lib/accounts/connect.ts`.
|
|
30
|
+
|
|
31
|
+
- **Every session row carries the request the agent was given and a timeline of what it did (PHNX-3939).** `agents sessions --active --json`, `sessions watch --json`, `feed watch --json`, history rows and the fleet session mirror gain three optional fields: `request` (the LATEST genuine user turn, tidied — the user's own sentence, with screenshot/clip paths, `@dir` mentions and pasted terminal echo pulled out into `attachments` / `pastedLines`, never rewritten), `timeline` (narration-anchored steps — one per line the agent said, with the tool calls under it folded into per-verb counts, failures, blocked calls and milestones; the last 8 in full plus a counter for everything older), and `files` (the paths the session created / modified / deleted, from the harness's own ledger where it keeps one). The fold is deterministic and needs no model; the optional summarizer now takes the narration as input and stays off by default. Computed by the daemon inside its existing reader-gated 15s tick, reading only the bytes each transcript grew by, and cached in the new stamp-validated `session_timelines` table — zero cost on the request path. Source: `cli/src/lib/session/timeline.ts`, `cli/src/lib/session/timeline-pass.ts`, `cli/src/lib/session/db.ts`.
|
|
32
|
+
- **A `/model` echo or a skill body can no longer become a session's title (PHNX-3939).** `extractSessionTopic` consulted a shorter skip list than `cleanFirstUserMessage`, so harness scaffolding — `<local-command-stdout>`, `Base directory for this skill:`, hook feedback, `<hook_result>`, `<notification>` — became a session's topic and then its displayed name. The two lists are now one, and the row title / `userPromptClean` are derived from the raw latest genuine user turn (with the row's attachments in hand) instead of the already-collapsed topic. Source: `cli/src/lib/session/prompt.ts`, `cli/src/lib/session/active.ts`.
|
|
33
|
+
- **`agents sessions trace <id> --steps` prints the step list the sidebar shows.** The same fold as the session row, as text: `NN +MMs source headline · 6 run · 2 blocked · worktree created`, with a totals footer. The CLI door to the timeline for an agent working without the extension. Source: `cli/src/commands/sessions-trace.ts`.
|
|
34
|
+
- **The parser keeps the per-tool label every harness writes (PHNX-3939).** Claude's Bash `input.description`, Kimi's `tool.call.description`, OpenCode's `state.input.description` / `state.title` and Codex's parsed command now ride the normalized `SessionEvent` as `label`, so a live row's now-line reads as the sentence the model wrote for it instead of a re-derived command summary. Codex additionally gains a reader for its typed `item_completed` stream (`AgentMessage` phase, `parsed_cmd`, `FileChange`, `Extension`, `SubAgentActivity`, `ImageView`, `ContextCompaction`) — kept separate from `parseCodexContent` because the two describe the same turn, and an unknown item type folds to a counted call rather than throwing. Claude permission/hook denials are now distinguished from command failures (`blocked` vs `failed`). Source: `cli/src/lib/session/parse.ts`.
|
|
35
|
+
- **`sessions watch` history rows cover every transcript-writing harness.** `readPreviousSessionsForWatch` queried only claude/codex/muse/opencode, so a Kimi, Grok, Cursor or Droid session never appeared as a Previous row. Source: `cli/src/lib/session/remote/watch.ts`.
|
|
36
|
+
- **The timeline redacts and de-escapes the transcript text it ships.** `projectTimeline` is the single place folded text leaves the fold, so step headlines, the now-line and milestone marks now run through `redactSecrets` (on by default; `sessions trace --no-redact` is the only opt-out) and always through `sanitizeForTerminal`. Without it a Bash call with no `description` fell back to raw command text, so a pasted token rode the session row into the git-tracked `~/.agents/devices/<device>/daemon-state.json`, and a transcript carrying ESC sequences could clear the operator's screen on `sessions trace --steps`. `--steps` now honours the command's own `--redact` default, and `sanitizeEvent` covers the new `label` and `phase` fields. Source: `cli/src/lib/session/timeline.ts`, `cli/src/lib/session/parse.ts`, `cli/src/commands/sessions-trace.ts`.
|
|
37
|
+
- **A 4-16 MiB transcript on a non-resumable harness gets a timeline.** Whole-file eligibility was gated on the per-session allowance, capped at 4 MiB however idle the tick was, while `unavailable` only fired above 16 MiB — so a kimi/grok/gemini/cursor/opencode/droid session in that band got no row at all (not `ready`, not `partial`, not `unavailable`) and was counted `reused`, leaving the daemon log reading healthy. It is now gated on what the tick can actually afford, the byte budget is debited by the bytes each branch really reads (the whole file for a whole-file re-parse, not its growth delta), and a session that genuinely does not fit writes a `partial` row naming the budget. A per-session fold that throws is logged and counted `skipped` instead of swallowed. Source: `cli/src/lib/session/timeline-pass.ts`.
|
|
38
|
+
- **Cursor and Droid keep their per-call label too.** Both write the same human one-liner through the shared Anthropic-message parser — Cursor as `description`, Droid as `summary` — and neither reached `SessionEvent.label`, so a Droid `Execute{command:"ls -la", summary:"List files"}` rendered its now-line as `ls -la` while the equivalent Claude call read `List files`. Source: `cli/src/lib/session/parse.ts`.
|
|
39
|
+
- **A tool cut short with Ctrl-C counts as blocked, not failed.** `toolUseResult.interrupted` joins `toolDenialKind` as an input to `blocked`, so interrupting an agent no longer inflates its failure count. Source: `cli/src/lib/session/parse.ts`.
|
|
40
|
+
- **`now` ships only on the live step, and `ready` is never an empty step list.** `attachTool` sets the now-line on every step as it folds; every finished step kept a stale one, so a consumer rendered a now-line on completed work at ~700 bytes a row. A fold that produced no step now reports its state honestly instead of `ready` with nothing in it. The fleet mirror also carries `mix` and `live` through the consume path, which rebuilt each step from a field list that dropped both while keeping `now`. Source: `cli/src/lib/session/timeline.ts`, `cli/src/lib/session/mirror.ts`.
|
|
41
|
+
|
|
42
|
+
## 1.22.83
|
|
43
|
+
|
|
44
|
+
- **The Claude status line shows the registered account NAME (PHNX-3987).** A login named with `agents accounts` (`work`, `dev`, …) now renders that name between the host and the model — `yosemite · work · Fable 5.1 · …` — which is what tells several same-harness logins apart at a glance; an unnamed login keeps the email label. The lookup is the one `agents view` and the device inventories already use, lifted into `findNativeAccountByIdentity` so the three copies share it. Source: `cli/src/lib/claude-statusline.ts`, `cli/src/lib/account-registry.ts`.
|
|
45
|
+
|
|
46
|
+
- **`agents accounts rename` / `remove` / `view` honor per-harness account names (PHNX-3988).** Native labels are unique per harness (PHNX-3887), but rename/remove still ran a fleet-wide uniqueness check and resolved a bare name to whichever harness row the store ordered first — so renaming Codex's `cxicloud` to `icloud` failed because Claude already owned `icloud`. Rename now scopes uniqueness to the target row's harness, so one harness can take a name another already uses; a duplicate within the same harness is still refused. Rename, remove, and view accept `<harness>#<name>` and refuse an ambiguous bare name rather than guessing. Source: `cli/src/lib/account-registry.ts`, `cli/src/commands/accounts.ts`.
|
|
47
|
+
|
|
48
|
+
- **Feed activity polling reuses unchanged log tails (PHNX-3939).** The activity reader caches file-version-validated tails and timestamp summaries within a bounded process-local budget. Cursor polls skip unchanged historical events without parsing them again; appends, rewrites, replacements and tail-budget changes invalidate the cached snapshot. Returned events remain independently mutable, with timestamp, filter and limit behavior preserved.
|
|
49
|
+
|
|
3
50
|
## 1.22.82
|
|
4
51
|
|
|
5
52
|
- **The Claude status line names the signed-in account before the model (PHNX-3987).** The managed `agents __claude-statusline` render now reads `host · account · model · 5h n% · 7d n% · ◆ reminder`, where the account is the email the running Claude is signed into (plus the org name for a Team/Enterprise seat, the same label `agents view` renders), read from the `.claude.json` of the home Claude is actually running with — the version home under the launch shim, `$HOME` otherwise. A never-signed-in home omits the part. Source: `cli/src/lib/claude-statusline.ts`.
|
|
@@ -21,7 +21,7 @@ import { connectRefusal, connectSupported, runConnect } from '../lib/accounts/co
|
|
|
21
21
|
import { acquireAuthOperationLock } from '../lib/accounts/auth-operation-lock.js';
|
|
22
22
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
23
23
|
import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
|
|
24
|
-
import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, nativeAccountHome, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
24
|
+
import { accountBindings, addAccount, addNativeAccount, assertUnambiguousNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, nativeAccountHome, parseAccountSelector, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
25
25
|
import { registerMintCommand } from './auth-mint.js';
|
|
26
26
|
/** Comma-joined list of harnesses `accounts connect` can drive today. */
|
|
27
27
|
function connectSupportedList() {
|
|
@@ -535,7 +535,7 @@ export function registerAccountsCommand(program) {
|
|
|
535
535
|
agents accounts connect codex personal
|
|
536
536
|
agents accounts connect claude work # again → reuses work's home, fails closed on a different identity
|
|
537
537
|
agents run claude --account work`,
|
|
538
|
-
notes: `Each NEW account gets a fresh opaque installation label and its own isolated home, so ten accounts can share the same upstream release while keeping separate logins. Connect installs the current release even when it is already installed under another label, then launches the harness's native login there — the OAuth credential is never copied or fleet-synced. Reconnecting an existing account by name reuses its home and refuses to overwrite a home now signed in as a different identity. Supported for version-scoped, isolable harnesses (${connectSupportedList()}); others fail with a clear reason. Provider API-key/token accounts use 'agents accounts add' instead.`,
|
|
538
|
+
notes: `Each NEW account gets a fresh opaque installation label and its own isolated home, so ten accounts can share the same upstream release while keeping separate logins. Connect installs the current release even when it is already installed under another label, then launches the harness's native login there — the OAuth credential is never copied or fleet-synced. Reconnecting an existing account by name reuses its home and refuses to overwrite a home now signed in as a different identity. Headed devices only: on a worker connect refuses before any slot, install, or browser — add the account on a personal/desktop box and provision workers from the durable credential. Supported for version-scoped, isolable harnesses (${connectSupportedList()}); others fail with a clear reason. Provider API-key/token accounts use 'agents accounts add' instead.`,
|
|
539
539
|
});
|
|
540
540
|
accounts.command('add <name>')
|
|
541
541
|
.description('Add a durable API key, setup token, or bearer token')
|
|
@@ -580,12 +580,21 @@ agents run claude --account work`,
|
|
|
580
580
|
console.log(chalk.green(`Updated credential for account '${name}'.`));
|
|
581
581
|
});
|
|
582
582
|
});
|
|
583
|
-
accounts.command('view <name>')
|
|
583
|
+
const viewCmd = accounts.command('view <name>')
|
|
584
|
+
.alias('inspect')
|
|
585
|
+
.description('Show safe account metadata, custody, and attachments. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
586
|
+
.option('--json', 'Machine-readable output')
|
|
587
|
+
.action(async (name, o, command) => {
|
|
584
588
|
await runAccountsAction(command, async () => {
|
|
585
589
|
const meta = readMeta();
|
|
586
|
-
const
|
|
590
|
+
const selector = parseAccountSelector(name);
|
|
591
|
+
assertUnambiguousNativeAccount(meta, selector.name, selector.agent);
|
|
592
|
+
const unified = findUnifiedAccount(selector.name, meta, undefined, selector.agent);
|
|
587
593
|
if (!unified)
|
|
588
594
|
throw new Error(`Unknown account '${name}'.`);
|
|
595
|
+
if (selector.agent && unified.kind === 'native' && unified.agent !== selector.agent) {
|
|
596
|
+
throw new Error(`Unknown ${selector.agent} account '${selector.name}'.`);
|
|
597
|
+
}
|
|
589
598
|
const account = unified.kind === 'provider'
|
|
590
599
|
? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
|
|
591
600
|
: { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
|
|
@@ -607,6 +616,11 @@ agents run claude --account work`,
|
|
|
607
616
|
console.log(` attached: ${account.attached.length ? account.attached.join(', ') : 'none'}`);
|
|
608
617
|
});
|
|
609
618
|
});
|
|
619
|
+
setHelpSections(viewCmd, {
|
|
620
|
+
examples: `agents accounts view work --json
|
|
621
|
+
agents accounts view codex#icloud`,
|
|
622
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
623
|
+
});
|
|
610
624
|
accounts.command('name <source> <name>')
|
|
611
625
|
.description('Name a signed-in native installation without copying its OAuth credentials')
|
|
612
626
|
.action(async (source, name, _o, command) => {
|
|
@@ -723,12 +737,25 @@ agents run codex#work`,
|
|
|
723
737
|
console.log(chalk.green(`Detached ${name} from ${target}.`));
|
|
724
738
|
});
|
|
725
739
|
});
|
|
726
|
-
accounts.command('rename <old> <new>')
|
|
740
|
+
const renameCmd = accounts.command('rename <old> <new>')
|
|
741
|
+
.description('Rename an account without changing its stable id. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
742
|
+
.action(async (oldName, newName, _o, command) => {
|
|
727
743
|
await runAccountsAction(command, () => { renameAccount(oldName, newName); });
|
|
728
744
|
});
|
|
729
|
-
|
|
745
|
+
setHelpSections(renameCmd, {
|
|
746
|
+
examples: `agents accounts rename cxicloud icloud
|
|
747
|
+
agents accounts rename codex#icloud cloud`,
|
|
748
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
749
|
+
});
|
|
750
|
+
const removeCmd = accounts.command('remove <name>')
|
|
751
|
+
.description('Remove an account and its device-local credential. Target may be <harness>#<name> when the name exists for several harnesses')
|
|
752
|
+
.action(async (name, _o, command) => {
|
|
730
753
|
await runAccountsAction(command, () => { removeAccount(name); });
|
|
731
754
|
});
|
|
755
|
+
setHelpSections(removeCmd, {
|
|
756
|
+
examples: `agents accounts remove claude#icloud`,
|
|
757
|
+
notes: 'Native names are unique per harness. A bare name that exists for several harnesses is refused — pick one with <harness>#<name>.',
|
|
758
|
+
});
|
|
732
759
|
accounts.command('set-default <agent> <name>')
|
|
733
760
|
.description('Use this account for a harness when --account is omitted')
|
|
734
761
|
.action(async (agentRaw, name, _o, command) => {
|
|
@@ -849,6 +876,7 @@ agents accounts name claude@2.1.220 work
|
|
|
849
876
|
agents accounts label codex@0.146.0 personal
|
|
850
877
|
agents accounts attach work claude@2.1.225
|
|
851
878
|
agents accounts view work --json
|
|
879
|
+
agents accounts view codex#icloud
|
|
852
880
|
agents accounts switch claude
|
|
853
881
|
agents accounts switch claude work
|
|
854
882
|
agents accounts set-default claude work
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
2
3
|
import type { SessionTrajectory } from '../lib/session/trajectory.js';
|
|
3
4
|
import type { TrajectoryComparison, TrajectoryDivergence, TrajectorySummary } from '../lib/session/trajectory-compare.js';
|
|
4
5
|
import type { TrajectoryStep } from '../lib/session/trajectory.js';
|
|
@@ -50,6 +51,7 @@ interface TraceOptions {
|
|
|
50
51
|
output?: string;
|
|
51
52
|
open?: boolean;
|
|
52
53
|
errorsOnly?: boolean;
|
|
54
|
+
steps?: boolean;
|
|
53
55
|
redact?: boolean;
|
|
54
56
|
all?: boolean;
|
|
55
57
|
since?: string;
|
|
@@ -73,6 +75,15 @@ export declare function chooseFormat(options: TraceOptions, isTTY: boolean): Ren
|
|
|
73
75
|
* clear message. Pure, so the boundaries are unit-tested without the command.
|
|
74
76
|
*/
|
|
75
77
|
export declare function decideTraceLayout(options: Pick<TraceOptions, 'tree' | 'compare'>, selectorCount: number, resolvedCount: number): 'single' | 'compare' | 'lineage';
|
|
78
|
+
/**
|
|
79
|
+
* Render one session's narration-anchored step list — the same fold the daemon
|
|
80
|
+
* caches and the sidebar renders, computed here from the transcript so the
|
|
81
|
+
* command works for any session, live or closed, cached or not.
|
|
82
|
+
*/
|
|
83
|
+
export declare function renderSessionSteps(session: SessionMeta, options?: {
|
|
84
|
+
redact?: boolean;
|
|
85
|
+
knownSecrets?: readonly string[];
|
|
86
|
+
}): string;
|
|
76
87
|
/** Attach the trace behaviour to a command node (canonical or top-level alias). */
|
|
77
88
|
export declare function configureTraceCommand(cmd: Command): Command;
|
|
78
89
|
/** Canonical `agents sessions trace <selectors...>`. */
|
|
@@ -23,6 +23,8 @@ import { knownSecretValuesFromEnv } from '../lib/redact.js';
|
|
|
23
23
|
import { getCacheDir } from '../lib/state.js';
|
|
24
24
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
25
25
|
import { parseSession } from '../lib/session/parse.js';
|
|
26
|
+
import { foldTimeline, projectSessionFiles, projectTimeline } from '../lib/session/timeline.js';
|
|
27
|
+
import { parseTimelineEvents } from '../lib/session/timeline-pass.js';
|
|
26
28
|
import { buildTrajectory } from '../lib/session/trajectory.js';
|
|
27
29
|
import { diffTrajectories } from '../lib/session/trajectory-compare.js';
|
|
28
30
|
import { buildLineage } from '../lib/session/trajectory-lineage.js';
|
|
@@ -134,6 +136,53 @@ export function decideTraceLayout(options, selectorCount, resolvedCount) {
|
|
|
134
136
|
}
|
|
135
137
|
return 'single';
|
|
136
138
|
}
|
|
139
|
+
/** One step's counters, rendered as the sidebar renders them: `6 run · 2 blocked`. */
|
|
140
|
+
function stepDetail(step) {
|
|
141
|
+
const mix = Object.entries(step.mix ?? {})
|
|
142
|
+
.filter(([, count]) => (count ?? 0) > 0)
|
|
143
|
+
.sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0))
|
|
144
|
+
.map(([verb, count]) => `${count} ${verb}`);
|
|
145
|
+
return [
|
|
146
|
+
...mix,
|
|
147
|
+
step.failed ? `${step.failed} failed` : '',
|
|
148
|
+
step.blocked ? `${step.blocked} blocked` : '',
|
|
149
|
+
...(step.marks ?? []),
|
|
150
|
+
].filter(Boolean).join(' · ');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Render one session's narration-anchored step list — the same fold the daemon
|
|
154
|
+
* caches and the sidebar renders, computed here from the transcript so the
|
|
155
|
+
* command works for any session, live or closed, cached or not.
|
|
156
|
+
*/
|
|
157
|
+
export function renderSessionSteps(session, options = {}) {
|
|
158
|
+
const state = foldTimeline(parseTimelineEvents(session.filePath, session.agent), undefined, {});
|
|
159
|
+
const timeline = projectTimeline(state, undefined, state.steps.length, options);
|
|
160
|
+
if (!timeline.steps.length) {
|
|
161
|
+
return `No steps folded for ${session.shortId || session.id}` +
|
|
162
|
+
`${timeline.reason ? ` — ${timeline.reason}` : ''}\n`;
|
|
163
|
+
}
|
|
164
|
+
const startMs = state.firstMs ?? Date.parse(timeline.steps[0].at);
|
|
165
|
+
const lines = timeline.steps.map((step, index) => {
|
|
166
|
+
const offsetS = Math.max(0, Math.round((Date.parse(step.at) - startMs) / 1000));
|
|
167
|
+
const detail = stepDetail(step);
|
|
168
|
+
return [
|
|
169
|
+
String(index + 1).padStart(2),
|
|
170
|
+
`+${offsetS}s`.padStart(7),
|
|
171
|
+
step.source.slice(0, 4).padEnd(4),
|
|
172
|
+
step.text,
|
|
173
|
+
detail ? `· ${detail}` : '',
|
|
174
|
+
].filter(Boolean).join(' ');
|
|
175
|
+
});
|
|
176
|
+
const files = projectSessionFiles(state);
|
|
177
|
+
const footer = [
|
|
178
|
+
`${timeline.steps.length} steps`,
|
|
179
|
+
`${timeline.tools} tools`,
|
|
180
|
+
timeline.failed ? `${timeline.failed} failed` : '',
|
|
181
|
+
timeline.blocked ? `${timeline.blocked} blocked` : '',
|
|
182
|
+
files ? `${files.total} file${files.total === 1 ? '' : 's'} changed` : '',
|
|
183
|
+
].filter(Boolean).join(' · ');
|
|
184
|
+
return `${lines.join('\n')}\n\n${footer}\n`;
|
|
185
|
+
}
|
|
137
186
|
/** Attach the trace behaviour to a command node (canonical or top-level alias). */
|
|
138
187
|
export function configureTraceCommand(cmd) {
|
|
139
188
|
cmd
|
|
@@ -144,6 +193,7 @@ export function configureTraceCommand(cmd) {
|
|
|
144
193
|
.option('-o, --output <path>', 'Write the rendering to a path instead of opening/printing')
|
|
145
194
|
.option('--no-open', 'Do not open the HTML; print its path instead')
|
|
146
195
|
.option('--errors-only', 'Collapse the text trajectory to error steps and their neighbours')
|
|
196
|
+
.option('--steps', 'Print the narration-anchored step list the sidebar shows (the same fold as the session row)')
|
|
147
197
|
.option('--compare', 'Force the compare layout for exactly two selectors (this is also the default for two)')
|
|
148
198
|
.option('--tree', 'Render the lineage of one session — it and every session it spawned')
|
|
149
199
|
.option('--no-redact', 'Local-only: skip secret redaction of derived labels (never for a shared file)')
|
|
@@ -161,6 +211,9 @@ agents sessions trace a1b2c3d4 --text
|
|
|
161
211
|
# Just the failures and their neighbours — for a triaging agent
|
|
162
212
|
agents sessions trace a1b2c3d4 --text --errors-only
|
|
163
213
|
|
|
214
|
+
# What the agent said it was doing, step by step — the sidebar's timeline as text
|
|
215
|
+
agents sessions trace a1b2c3d4 --steps
|
|
216
|
+
|
|
164
217
|
# The stable JSON envelope for the AGI EXT Fleet panel or a tool
|
|
165
218
|
agents sessions trace a1b2c3d4 --json
|
|
166
219
|
|
|
@@ -223,6 +276,9 @@ Three or more selectors, and --tree with more than one selector, fail loud.`,
|
|
|
223
276
|
// count — a single content-search selector matching two sessions must NOT
|
|
224
277
|
// silently become a compare. All the fail-loud boundaries live in the pure
|
|
225
278
|
// decideTraceLayout so they are unit-tested (see sessions-trace.test.ts).
|
|
279
|
+
if (options.steps && selectors.length > 1) {
|
|
280
|
+
throw new Error('--steps renders one session\'s step list; pass a single selector.');
|
|
281
|
+
}
|
|
226
282
|
const layout = decideTraceLayout(options, selectors.length, sessions.length);
|
|
227
283
|
const redact = options.redact !== false;
|
|
228
284
|
const knownSecrets = redact ? knownSecretValuesFromEnv() : undefined;
|
|
@@ -333,6 +389,21 @@ Three or more selectors, and --tree with more than one selector, fail loud.`,
|
|
|
333
389
|
}
|
|
334
390
|
// One selector — the single-session trajectory.
|
|
335
391
|
const session = sessions[0];
|
|
392
|
+
// --steps is the CLI door to the timeline fold the sidebar renders, so an
|
|
393
|
+
// agent can read what a session DID without the extension (PHNX-3939). It is
|
|
394
|
+
// a different model from the trajectory (narration beats, not tool steps),
|
|
395
|
+
// so it short-circuits before buildTrajectory rather than reshaping it.
|
|
396
|
+
if (options.steps) {
|
|
397
|
+
const out = renderSessionSteps(session, { redact, knownSecrets });
|
|
398
|
+
if (options.output) {
|
|
399
|
+
fs.writeFileSync(options.output, out, { mode: 0o600 });
|
|
400
|
+
process.stderr.write(chalk.green(`Wrote step list to ${options.output}\n`));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
process.stdout.write(out);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
336
407
|
const model = buildOne(session);
|
|
337
408
|
if (format === 'json') {
|
|
338
409
|
const out = JSON.stringify(buildTraceEnvelope([model]), null, 2) + '\n';
|
package/dist/commands/view.js
CHANGED
|
@@ -20,7 +20,7 @@ import { listCliStatus } from '../lib/cli-resources.js';
|
|
|
20
20
|
import { isCapable } from '../lib/capabilities.js';
|
|
21
21
|
import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins/plugins.js';
|
|
22
22
|
import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts, readMeta } from '../lib/state.js';
|
|
23
|
-
import {
|
|
23
|
+
import { findNativeAccountByIdentity } from '../lib/account-registry.js';
|
|
24
24
|
import { buildNativeCatalog } from '../lib/account-catalog.js';
|
|
25
25
|
import { connectSupported } from '../lib/accounts/connect.js';
|
|
26
26
|
import { readInstallation } from '../lib/installations/store.js';
|
|
@@ -67,11 +67,8 @@ export const accountColumnLabel = accountDisplayLabel;
|
|
|
67
67
|
*/
|
|
68
68
|
export function namedAccountColumnLabel(agentId, info) {
|
|
69
69
|
const display = accountColumnLabel(info);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
return display;
|
|
73
|
-
const saved = listNativeAccounts(readMeta()).find(item => item.agent === agentId && item.identityKey === identityKey);
|
|
74
|
-
return saved ? `${saved.name} · ${display || saved.identityLabel || identityKey}` : display;
|
|
70
|
+
const saved = findNativeAccountByIdentity(readMeta(), agentId, info);
|
|
71
|
+
return saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display;
|
|
75
72
|
}
|
|
76
73
|
/** Human account identity; release labels belong only in installation diagnostics. */
|
|
77
74
|
export function nativeAccountViewLabel(row) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentId, type Meta } from './types.js';
|
|
2
2
|
import { type AccountAuthKind } from './account-provider-registry.js';
|
|
3
3
|
export interface CredentialAccount {
|
|
4
4
|
id: string;
|
|
@@ -43,6 +43,18 @@ export declare function findAccount(name: string, doc?: AccountRegistryDocument)
|
|
|
43
43
|
* accumulate). On an id collision the device slice wins.
|
|
44
44
|
*/
|
|
45
45
|
export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): NativeAccount[];
|
|
46
|
+
/**
|
|
47
|
+
* The native account registered for one harness login, or null when that login
|
|
48
|
+
* is unnamed. `info` is the login's identity as `getAccountInfo` /
|
|
49
|
+
* `readClaudeHomeConfig` report it; the match key is the same value every
|
|
50
|
+
* catalog and inventory groups a home on — the stable `accountKey`, else the
|
|
51
|
+
* lowercased email. The one lookup behind `agents view`, the device
|
|
52
|
+
* inventories, and the Claude status line.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findNativeAccountByIdentity(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agent: AgentId, info: {
|
|
55
|
+
accountKey?: string | null;
|
|
56
|
+
email?: string | null;
|
|
57
|
+
} | null | undefined): NativeAccount | null;
|
|
46
58
|
/**
|
|
47
59
|
* Resolve one account by name or id across both stores, native first.
|
|
48
60
|
*
|
|
@@ -55,6 +67,23 @@ export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'device
|
|
|
55
67
|
* able to omit it.
|
|
56
68
|
*/
|
|
57
69
|
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument, preferAgent?: AgentId): UnifiedAccount | null;
|
|
70
|
+
/**
|
|
71
|
+
* Split a management selector into its harness and name. `<harness>#<name>`
|
|
72
|
+
* pins the harness; a bare name (or row id) carries none. Native names are
|
|
73
|
+
* unique per harness (PHNX-3887), so a bare name alone can legitimately match a
|
|
74
|
+
* claude row AND a codex row — the selector is how rename/remove say which.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseAccountSelector(input: string): {
|
|
77
|
+
agent?: AgentId;
|
|
78
|
+
name: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Refuse a bare name that native rows in several harnesses share. A
|
|
82
|
+
* harness-qualified selector (`agent` set) never hits this — uniqueness is
|
|
83
|
+
* per harness. Ids are unique so they never collide either. Shared by
|
|
84
|
+
* rename/remove/view so the message stays one string.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertUnambiguousNativeAccount(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, name: string, agent?: AgentId): void;
|
|
58
87
|
/**
|
|
59
88
|
* Validate a native account NAME (charset + per-harness uniqueness) WITHOUT a
|
|
60
89
|
* known identity — the pre-flight `agents accounts connect` runs before it
|
|
@@ -120,8 +149,16 @@ export interface AddAccountOptions {
|
|
|
120
149
|
}
|
|
121
150
|
export declare function addAccount(name: string, provider: string, auth: AccountAuthKind, secret: string, base?: string, opts?: AddAccountOptions): CredentialAccount;
|
|
122
151
|
export declare function setAccountSecret(name: string, secret: string, base?: string): void;
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
|
|
154
|
+
* carries its harness, so the new name only has to be free within THAT harness
|
|
155
|
+
* (PHNX-3887 / PHNX-3988): renaming codex's `cxicloud` to `icloud` is fine
|
|
156
|
+
* while claude's `icloud` stays untouched. Provider accounts have no harness
|
|
157
|
+
* and stay globally unique.
|
|
158
|
+
*/
|
|
159
|
+
export declare function renameAccount(oldSelector: string, newName: string, base?: string): void;
|
|
160
|
+
/** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
|
|
161
|
+
export declare function removeAccount(selector: string, base?: string): void;
|
|
125
162
|
export declare function inspectAccount(name: string, base?: string): CredentialAccount & {
|
|
126
163
|
secretPresent: boolean;
|
|
127
164
|
policy: 'never';
|
|
@@ -23,6 +23,7 @@ import * as yaml from 'yaml';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
25
25
|
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
26
|
+
import { isAgentId } from './types.js';
|
|
26
27
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken } from './secrets/index.js';
|
|
27
28
|
import { bundleExists, deleteBundle, listBundles, readAndResolveBundleEnv, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
|
|
28
29
|
import { getAccountProvider } from './account-provider-registry.js';
|
|
@@ -148,6 +149,20 @@ export function listNativeAccounts(meta) {
|
|
|
148
149
|
const merged = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
|
|
149
150
|
return Object.values(merged).map(account => ({ ...account, kind: 'native' }));
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The native account registered for one harness login, or null when that login
|
|
154
|
+
* is unnamed. `info` is the login's identity as `getAccountInfo` /
|
|
155
|
+
* `readClaudeHomeConfig` report it; the match key is the same value every
|
|
156
|
+
* catalog and inventory groups a home on — the stable `accountKey`, else the
|
|
157
|
+
* lowercased email. The one lookup behind `agents view`, the device
|
|
158
|
+
* inventories, and the Claude status line.
|
|
159
|
+
*/
|
|
160
|
+
export function findNativeAccountByIdentity(meta, agent, info) {
|
|
161
|
+
const identityKey = info?.accountKey ?? info?.email?.toLowerCase();
|
|
162
|
+
if (!identityKey)
|
|
163
|
+
return null;
|
|
164
|
+
return listNativeAccounts(meta).find(account => account.agent === agent && account.identityKey === identityKey) ?? null;
|
|
165
|
+
}
|
|
151
166
|
/**
|
|
152
167
|
* Resolve one account by name or id across both stores, native first.
|
|
153
168
|
*
|
|
@@ -168,8 +183,9 @@ export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
|
168
183
|
// merged store happened to order first, so `agents run claude#<email>` died with
|
|
169
184
|
// "Account 'personal' is a codex login and cannot authenticate the claude harness"
|
|
170
185
|
// while that identity's own claude login sat right there. Prefer the harness being
|
|
171
|
-
// launched; fall back to the first match
|
|
172
|
-
//
|
|
186
|
+
// launched; fall back to the first match when the caller has no harness in hand.
|
|
187
|
+
// rename/remove/view refuse an ambiguous *name* via assertUnambiguousNativeAccount
|
|
188
|
+
// before they get here — this fallback is for identityLabel collisions.
|
|
173
189
|
const native = matches.find(account => account.agent === preferAgent) ?? matches[0];
|
|
174
190
|
if (native)
|
|
175
191
|
return native;
|
|
@@ -179,10 +195,46 @@ export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
|
179
195
|
function nativeIdentityRows(meta, agent, identityKey) {
|
|
180
196
|
return listNativeAccounts(meta).filter(account => account.agent === agent && account.identityKey === identityKey);
|
|
181
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Split a management selector into its harness and name. `<harness>#<name>`
|
|
200
|
+
* pins the harness; a bare name (or row id) carries none. Native names are
|
|
201
|
+
* unique per harness (PHNX-3887), so a bare name alone can legitimately match a
|
|
202
|
+
* claude row AND a codex row — the selector is how rename/remove say which.
|
|
203
|
+
*/
|
|
204
|
+
export function parseAccountSelector(input) {
|
|
205
|
+
const hash = input.indexOf('#');
|
|
206
|
+
if (hash < 0)
|
|
207
|
+
return { name: input };
|
|
208
|
+
const agentRaw = input.slice(0, hash);
|
|
209
|
+
const name = input.slice(hash + 1).trim();
|
|
210
|
+
if (!isAgentId(agentRaw))
|
|
211
|
+
throw new Error(`Unknown agent '${agentRaw}'.`);
|
|
212
|
+
if (!name)
|
|
213
|
+
throw new Error('Select an account after #.');
|
|
214
|
+
return { agent: agentRaw, name };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Refuse a bare name that native rows in several harnesses share. A
|
|
218
|
+
* harness-qualified selector (`agent` set) never hits this — uniqueness is
|
|
219
|
+
* per harness. Ids are unique so they never collide either. Shared by
|
|
220
|
+
* rename/remove/view so the message stays one string.
|
|
221
|
+
*/
|
|
222
|
+
export function assertUnambiguousNativeAccount(meta, name, agent) {
|
|
223
|
+
const matches = listNativeAccounts(meta).filter(account => (agent === undefined || account.agent === agent) && (account.id === name || account.name === name));
|
|
224
|
+
const harnesses = [...new Set(matches.map(account => account.agent))].sort();
|
|
225
|
+
if (harnesses.length > 1) {
|
|
226
|
+
throw new Error(`Account '${name}' exists for several harnesses (${harnesses.join(', ')}). `
|
|
227
|
+
+ `Pick one with <harness>#${name}, e.g. ${harnesses[0]}#${name}.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
182
230
|
/** Every row (central + this box's device store) for the identity that `name`
|
|
183
|
-
* (id or account name) resolves to.
|
|
184
|
-
|
|
185
|
-
|
|
231
|
+
* (id or account name) resolves to. With `agent` set only that harness's rows
|
|
232
|
+
* are considered; without it, a name owned by rows in several harnesses is
|
|
233
|
+
* refused rather than resolved to whichever the store ordered first. */
|
|
234
|
+
function nativeRowsForNameOrId(meta, name, agent) {
|
|
235
|
+
assertUnambiguousNativeAccount(meta, name, agent);
|
|
236
|
+
const matches = listNativeAccounts(meta).filter(account => (agent === undefined || account.agent === agent) && (account.id === name || account.name === name));
|
|
237
|
+
const found = matches[0];
|
|
186
238
|
if (!found)
|
|
187
239
|
return [];
|
|
188
240
|
return nativeIdentityRows(meta, found.agent, found.identityKey);
|
|
@@ -197,9 +249,9 @@ function nativeRowsForNameOrId(meta, name) {
|
|
|
197
249
|
* actually ambiguous at the point of use: the selector is `<harness>#<label>`,
|
|
198
250
|
* and `findUnifiedAccount` already disambiguates via `preferAgent`.
|
|
199
251
|
*
|
|
200
|
-
* Pass `agent` to scope the check to that harness.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
252
|
+
* Pass `agent` to scope the check to that harness. Every native path has one
|
|
253
|
+
* in hand — connect/label from the caller, rename from the row being renamed —
|
|
254
|
+
* so the un-scoped form is only for provider accounts.
|
|
203
255
|
*
|
|
204
256
|
* Provider (non-native) accounts stay globally unique — they are selected by
|
|
205
257
|
* bare name via `--account`, with no harness to scope them by.
|
|
@@ -493,21 +545,32 @@ export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
|
|
|
493
545
|
bundle.created_at = readBundle(account.name).created_at; // rotate the secret, keep the bundle's birth time
|
|
494
546
|
writeBundleWithItems(bundle, items);
|
|
495
547
|
}
|
|
496
|
-
|
|
548
|
+
/**
|
|
549
|
+
* `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
|
|
550
|
+
* carries its harness, so the new name only has to be free within THAT harness
|
|
551
|
+
* (PHNX-3887 / PHNX-3988): renaming codex's `cxicloud` to `icloud` is fine
|
|
552
|
+
* while claude's `icloud` stays untouched. Provider accounts have no harness
|
|
553
|
+
* and stay globally unique.
|
|
554
|
+
*/
|
|
555
|
+
export function renameAccount(oldSelector, newName, base = getUserAgentsDir()) {
|
|
497
556
|
assertName(newName);
|
|
498
|
-
const doc = readAccountRegistry(base);
|
|
499
557
|
const meta = readMeta();
|
|
500
|
-
const
|
|
558
|
+
const selector = parseAccountSelector(oldSelector);
|
|
559
|
+
const rows = nativeRowsForNameOrId(meta, selector.name, selector.agent);
|
|
501
560
|
if (rows.length) {
|
|
502
|
-
assertUniqueUnifiedName(newName, meta,
|
|
561
|
+
assertUniqueUnifiedName(newName, meta, undefined, new Set(rows.map(account => account.id)), rows[0].agent);
|
|
503
562
|
// Sweep every row for the identity (PHNX-3206) in its owning store (PHNX-3315)
|
|
504
563
|
// and any per-harness default that points to the old name or row ids.
|
|
505
564
|
const rowScope = rows[0].scope;
|
|
506
|
-
const
|
|
565
|
+
const renamedAgent = rows[0].agent;
|
|
566
|
+
const renamedIds = new Set(rows.map(row => row.id));
|
|
507
567
|
updateMeta(current => {
|
|
508
568
|
const defaults = { ...current.accounts?.defaults };
|
|
509
569
|
for (const [agent, value] of Object.entries(defaults)) {
|
|
510
|
-
|
|
570
|
+
// Ids are unique so an id match may rewrite any harness's default. A
|
|
571
|
+
// bare-name match must only rewrite THIS harness — two harnesses may
|
|
572
|
+
// legitimately share the same native name (PHNX-3988).
|
|
573
|
+
if (renamedIds.has(value) || (agent === renamedAgent && value === selector.name))
|
|
511
574
|
defaults[agent] = newName;
|
|
512
575
|
}
|
|
513
576
|
const next = { ...current, accounts: { ...current.accounts, defaults } };
|
|
@@ -527,9 +590,12 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
527
590
|
});
|
|
528
591
|
return;
|
|
529
592
|
}
|
|
530
|
-
|
|
593
|
+
if (selector.agent)
|
|
594
|
+
throw new Error(`Unknown ${selector.agent} account '${selector.name}'.`);
|
|
595
|
+
const doc = readAccountRegistry(base);
|
|
596
|
+
const account = findAccount(selector.name, doc);
|
|
531
597
|
if (!account)
|
|
532
|
-
throw new Error(`Unknown account '${
|
|
598
|
+
throw new Error(`Unknown account '${selector.name}'.`);
|
|
533
599
|
assertUniqueUnifiedName(newName, meta, doc);
|
|
534
600
|
const idsToRename = new Set([account.name, account.id]);
|
|
535
601
|
updateMeta(current => {
|
|
@@ -543,9 +609,11 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
543
609
|
renameBundle(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
|
|
544
610
|
renameProfileConsumers(account.name, newName, base);
|
|
545
611
|
}
|
|
546
|
-
|
|
612
|
+
/** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
|
|
613
|
+
export function removeAccount(selector, base = getUserAgentsDir()) {
|
|
547
614
|
const meta = readMeta();
|
|
548
|
-
const
|
|
615
|
+
const parsed = parseAccountSelector(selector);
|
|
616
|
+
const rows = nativeRowsForNameOrId(meta, parsed.name, parsed.agent);
|
|
549
617
|
if (rows.length) {
|
|
550
618
|
const bindings = [...new Set(rows.flatMap(row => accountBindings(row.id, meta)))].sort();
|
|
551
619
|
if (bindings.length)
|
|
@@ -571,6 +639,9 @@ export function removeAccount(name, base = getUserAgentsDir()) {
|
|
|
571
639
|
});
|
|
572
640
|
return;
|
|
573
641
|
}
|
|
642
|
+
if (parsed.agent)
|
|
643
|
+
throw new Error(`Unknown ${parsed.agent} account '${parsed.name}'.`);
|
|
644
|
+
const name = parsed.name;
|
|
574
645
|
const account = findAccount(name, readAccountRegistry(base));
|
|
575
646
|
if (!account)
|
|
576
647
|
throw new Error(`Unknown account '${name}'.`);
|
|
@@ -18,6 +18,12 @@ export declare function connectSupported(agent: AgentId): boolean;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function connectRefusal(agent: AgentId): string | null;
|
|
20
20
|
export declare function assertConnectSupported(agent: AgentId): void;
|
|
21
|
+
/**
|
|
22
|
+
* Named reason connect refuses on a non-headed device, or null when this box
|
|
23
|
+
* may run an interactive login. Workers never mint a native OAuth login
|
|
24
|
+
* (credential-management.md invariant 7 + Provisioning model).
|
|
25
|
+
*/
|
|
26
|
+
export declare function connectWorkerRefusal(agent: AgentId, name?: string): string | null;
|
|
21
27
|
export declare function loginInvocation(agent: AgentId): LoginInvocation;
|
|
22
28
|
/**
|
|
23
29
|
* Mint a fresh, opaque installation slot. `acct-<hex>` — alnum + hyphen only, so
|