@phnx-labs/agents-cli 1.22.7 → 1.22.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +61 -0
  5. package/dist/commands/exec.js +89 -21
  6. package/dist/commands/feed.js +38 -1
  7. package/dist/commands/harness.d.ts +40 -2
  8. package/dist/commands/harness.js +316 -20
  9. package/dist/commands/monitors.js +2 -2
  10. package/dist/commands/profiles.d.ts +42 -0
  11. package/dist/commands/profiles.js +91 -3
  12. package/dist/commands/routines.js +2 -2
  13. package/dist/commands/run-account-picker.js +2 -0
  14. package/dist/commands/sessions-picker.js +3 -3
  15. package/dist/commands/sessions-render.d.ts +12 -0
  16. package/dist/commands/sessions-render.js +124 -0
  17. package/dist/commands/sessions.js +28 -1
  18. package/dist/commands/snapshot.d.ts +11 -0
  19. package/dist/commands/snapshot.js +107 -0
  20. package/dist/commands/ssh.js +26 -3
  21. package/dist/commands/teams.js +14 -2
  22. package/dist/commands/view.d.ts +7 -0
  23. package/dist/commands/view.js +1 -1
  24. package/dist/index.js +11 -1
  25. package/dist/lib/browser/ipc.js +2 -0
  26. package/dist/lib/browser/remote-control.d.ts +35 -0
  27. package/dist/lib/browser/remote-control.js +48 -0
  28. package/dist/lib/browser/service.d.ts +19 -0
  29. package/dist/lib/browser/service.js +19 -1
  30. package/dist/lib/browser/types.d.ts +14 -2
  31. package/dist/lib/crabbox/cli.d.ts +35 -0
  32. package/dist/lib/crabbox/cli.js +46 -0
  33. package/dist/lib/crabbox/config.d.ts +21 -0
  34. package/dist/lib/crabbox/config.js +43 -0
  35. package/dist/lib/crabbox/lease.d.ts +15 -5
  36. package/dist/lib/crabbox/lease.js +57 -17
  37. package/dist/lib/daemon.js +12 -11
  38. package/dist/lib/device-config.js +8 -0
  39. package/dist/lib/devices/resolve-target.d.ts +4 -3
  40. package/dist/lib/devices/resolve-target.js +4 -3
  41. package/dist/lib/hosts/passthrough.d.ts +10 -1
  42. package/dist/lib/hosts/passthrough.js +41 -3
  43. package/dist/lib/hosts/registry.d.ts +4 -0
  44. package/dist/lib/hosts/registry.js +16 -0
  45. package/dist/lib/hosts/remote-cmd.js +2 -0
  46. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  47. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  48. package/dist/lib/observe-aliases.d.ts +30 -0
  49. package/dist/lib/observe-aliases.js +56 -0
  50. package/dist/lib/placement.d.ts +82 -0
  51. package/dist/lib/placement.js +188 -0
  52. package/dist/lib/profiles.d.ts +31 -9
  53. package/dist/lib/profiles.js +83 -13
  54. package/dist/lib/redact.js +4 -0
  55. package/dist/lib/rotate.d.ts +19 -4
  56. package/dist/lib/rotate.js +24 -1
  57. package/dist/lib/routines.d.ts +2 -0
  58. package/dist/lib/runner.d.ts +3 -0
  59. package/dist/lib/runner.js +90 -7
  60. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  61. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  62. package/dist/lib/session/parse.d.ts +5 -1
  63. package/dist/lib/session/parse.js +23 -13
  64. package/dist/lib/session/prompt.js +5 -0
  65. package/dist/lib/session/render.d.ts +3 -0
  66. package/dist/lib/session/render.js +33 -10
  67. package/dist/lib/snapshot.d.ts +103 -0
  68. package/dist/lib/snapshot.js +99 -0
  69. package/dist/lib/startup/command-registry.d.ts +1 -0
  70. package/dist/lib/startup/command-registry.js +17 -1
  71. package/dist/lib/usage-refresh.d.ts +19 -11
  72. package/dist/lib/usage-refresh.js +75 -43
  73. package/dist/lib/usage.d.ts +12 -0
  74. package/dist/lib/usage.js +37 -6
  75. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Observe-umbrella aliases (Phase 3 surface consolidation).
3
+ *
4
+ * Thin name → real command expansion. No store merge — feed / sessions / events
5
+ * remain the stores; these are doors that point at the right reader.
6
+ *
7
+ * inbox → feed (needs-you default)
8
+ * timeline → feed --filter updates (agent progress stream)
9
+ * roster → sessions --active (live agent roster)
10
+ *
11
+ * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
+ * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
+ */
14
+ export type ObserveAlias = 'inbox' | 'timeline' | 'roster';
15
+ export declare const OBSERVE_ALIASES: readonly ObserveAlias[];
16
+ export interface ObserveExpandResult {
17
+ /** argv for the real command (no program name): e.g. ['feed', '--filter', 'updates'] */
18
+ argv: string[];
19
+ /** One-line note for stderr (optional); empty when silent. */
20
+ note: string;
21
+ }
22
+ /** True when `rest` already carries a `--filter` / `--filter=…` flag. */
23
+ export declare function hasFilterFlag(rest: readonly string[]): boolean;
24
+ /** True when `rest` already carries `--active`. */
25
+ export declare function hasActiveFlag(rest: readonly string[]): boolean;
26
+ /**
27
+ * Expand an observe alias + remaining user args into the real command argv.
28
+ * Returns null when `alias` is not an observe alias.
29
+ */
30
+ export declare function expandObserveAlias(alias: string, rest?: readonly string[]): ObserveExpandResult | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Observe-umbrella aliases (Phase 3 surface consolidation).
3
+ *
4
+ * Thin name → real command expansion. No store merge — feed / sessions / events
5
+ * remain the stores; these are doors that point at the right reader.
6
+ *
7
+ * inbox → feed (needs-you default)
8
+ * timeline → feed --filter updates (agent progress stream)
9
+ * roster → sessions --active (live agent roster)
10
+ *
11
+ * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
+ * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
+ */
14
+ export const OBSERVE_ALIASES = ['inbox', 'timeline', 'roster'];
15
+ /** True when `rest` already carries a `--filter` / `--filter=…` flag. */
16
+ export function hasFilterFlag(rest) {
17
+ return rest.some((a) => a === '--filter' || a.startsWith('--filter='));
18
+ }
19
+ /** True when `rest` already carries `--active`. */
20
+ export function hasActiveFlag(rest) {
21
+ return rest.some((a) => a === '--active');
22
+ }
23
+ /**
24
+ * Expand an observe alias + remaining user args into the real command argv.
25
+ * Returns null when `alias` is not an observe alias.
26
+ */
27
+ export function expandObserveAlias(alias, rest = []) {
28
+ const tail = [...rest];
29
+ switch (alias) {
30
+ case 'inbox':
31
+ return {
32
+ argv: ['feed', ...tail],
33
+ note: 'agents inbox → agents feed (needs-you inbox)',
34
+ };
35
+ case 'timeline': {
36
+ const argv = hasFilterFlag(tail)
37
+ ? ['feed', ...tail]
38
+ : ['feed', '--filter', 'updates', ...tail];
39
+ return {
40
+ argv,
41
+ note: 'agents timeline → agents feed --filter updates',
42
+ };
43
+ }
44
+ case 'roster': {
45
+ const argv = hasActiveFlag(tail)
46
+ ? ['sessions', ...tail]
47
+ : ['sessions', '--active', ...tail];
48
+ return {
49
+ argv,
50
+ note: 'agents roster → agents sessions --active',
51
+ };
52
+ }
53
+ default:
54
+ return null;
55
+ }
56
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Placement — one model for "where does the body run?"
3
+ *
4
+ * The CLI grew several doors that all mean execution target:
5
+ * run --host / --device / --lease / --box
6
+ * routines --placement / --run-on / hostStrategy
7
+ * monitors --run-on (body) vs --device (owner — NOT placement)
8
+ * teams --device (teammate pin)
9
+ * cloud run --provider host
10
+ *
11
+ * This module is the shared vocabulary. Old flags remain; --where is a
12
+ * thin alias on `agents run` that expands into them. Docs and help teach
13
+ * the matrix; stores stay separate (device registry, lease boxes, cloud).
14
+ *
15
+ * Owner (who may fire / evaluate) is NOT placement — see monitors.
16
+ */
17
+ /** Where a job body executes. */
18
+ export type PlacementKind = 'local' | 'device' | 'fleet' | 'cloud' | 'lease';
19
+ /**
20
+ * Canonical placement object.
21
+ *
22
+ * kind: local — this machine
23
+ * kind: device — named box or affinity pick (target: name | "auto")
24
+ * kind: fleet — pick one online device at fire time (routines)
25
+ * kind: cloud — vendor cloud dispatch
26
+ * kind: lease — disposable crabbox (target: optional backend)
27
+ */
28
+ export interface Placement {
29
+ kind: PlacementKind;
30
+ /** Device/host name, "auto", lease backend, or undefined. */
31
+ target?: string;
32
+ /** Flag or path that produced this (errors / diagnostics). */
33
+ source: string;
34
+ }
35
+ /** Run-flag bag the placement parser understands. */
36
+ export interface RunPlacementFlags {
37
+ where?: string;
38
+ host?: string;
39
+ device?: string;
40
+ on?: string;
41
+ computer?: string;
42
+ lease?: string | boolean;
43
+ box?: string;
44
+ }
45
+ export declare class PlacementError extends Error {
46
+ constructor(message: string);
47
+ }
48
+ /**
49
+ * Parse a `--where` / placement spec string.
50
+ *
51
+ * Accepted forms:
52
+ * local
53
+ * device[:name] | host[:name] (bare name → device:<name>)
54
+ * device:auto | auto | host:auto
55
+ * fleet
56
+ * cloud
57
+ * lease[:backend]
58
+ */
59
+ export declare function parseWhereSpec(raw: string, source?: string): Placement;
60
+ /** First non-empty host-family flag value (host / device / on / computer). */
61
+ export declare function hostFamilyTarget(flags: RunPlacementFlags): string | undefined;
62
+ /**
63
+ * Resolve placement from run flags. `--where` wins only when no other
64
+ * placement flag is set; mixing is a PlacementError.
65
+ */
66
+ export declare function placementFromRunFlags(flags: RunPlacementFlags): Placement;
67
+ /**
68
+ * Expand a resolved placement into the concrete run option fields the
69
+ * existing dispatch paths already understand. Pure — does not mutate input.
70
+ *
71
+ * `cloud` and `fleet` are not valid for a bare `agents run` (use `cloud run`
72
+ * / routines); they throw so callers fail loud.
73
+ */
74
+ export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box'>;
75
+ /** Map routines hostStrategy (+ optional host) onto the shared Placement. */
76
+ export declare function placementFromHostStrategy(strategy: 'local' | 'host' | 'fleet' | 'cloud', host?: string): Placement;
77
+ /** One-line human form for logs / help. */
78
+ export declare function formatPlacement(p: Placement): string;
79
+ /**
80
+ * Short matrix for help footers and docs. Keep in sync with 00-concepts.md.
81
+ */
82
+ export declare const PLACEMENT_MATRIX: string;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Placement — one model for "where does the body run?"
3
+ *
4
+ * The CLI grew several doors that all mean execution target:
5
+ * run --host / --device / --lease / --box
6
+ * routines --placement / --run-on / hostStrategy
7
+ * monitors --run-on (body) vs --device (owner — NOT placement)
8
+ * teams --device (teammate pin)
9
+ * cloud run --provider host
10
+ *
11
+ * This module is the shared vocabulary. Old flags remain; --where is a
12
+ * thin alias on `agents run` that expands into them. Docs and help teach
13
+ * the matrix; stores stay separate (device registry, lease boxes, cloud).
14
+ *
15
+ * Owner (who may fire / evaluate) is NOT placement — see monitors.
16
+ */
17
+ export class PlacementError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'PlacementError';
21
+ }
22
+ }
23
+ const KINDS = new Set(['local', 'device', 'fleet', 'cloud', 'lease', 'host']);
24
+ /**
25
+ * Parse a `--where` / placement spec string.
26
+ *
27
+ * Accepted forms:
28
+ * local
29
+ * device[:name] | host[:name] (bare name → device:<name>)
30
+ * device:auto | auto | host:auto
31
+ * fleet
32
+ * cloud
33
+ * lease[:backend]
34
+ */
35
+ export function parseWhereSpec(raw, source = '--where') {
36
+ const spec = raw.trim();
37
+ if (!spec) {
38
+ throw new PlacementError(`${source} requires a value (local | device:<name> | auto | lease | cloud | fleet)`);
39
+ }
40
+ const lower = spec.toLowerCase();
41
+ if (lower === 'local')
42
+ return { kind: 'local', source };
43
+ if (lower === 'auto')
44
+ return { kind: 'device', target: 'auto', source };
45
+ if (lower === 'fleet')
46
+ return { kind: 'fleet', source };
47
+ if (lower === 'cloud')
48
+ return { kind: 'cloud', source };
49
+ if (lower === 'lease')
50
+ return { kind: 'lease', source };
51
+ const colon = spec.indexOf(':');
52
+ if (colon === -1) {
53
+ // Bare token that is not a reserved kind → device target.
54
+ if (KINDS.has(lower)) {
55
+ // "device" / "host" alone means device with no pin (invalid for run).
56
+ throw new PlacementError(`${source} ${spec}: name a target (device:<name>, device:auto) or use local|lease|cloud|fleet`);
57
+ }
58
+ return { kind: 'device', target: spec, source };
59
+ }
60
+ const head = spec.slice(0, colon).toLowerCase();
61
+ const tail = spec.slice(colon + 1).trim();
62
+ if (!tail) {
63
+ throw new PlacementError(`${source} ${spec}: missing target after ':'`);
64
+ }
65
+ if (head === 'device' || head === 'host') {
66
+ return { kind: 'device', target: tail, source };
67
+ }
68
+ if (head === 'lease') {
69
+ return { kind: 'lease', target: tail, source };
70
+ }
71
+ if (head === 'cloud') {
72
+ return { kind: 'cloud', target: tail, source };
73
+ }
74
+ if (head === 'fleet') {
75
+ return { kind: 'fleet', target: tail, source };
76
+ }
77
+ throw new PlacementError(`${source} ${spec}: unknown kind '${head}' (use local | device:<name> | auto | lease[:backend] | cloud | fleet)`);
78
+ }
79
+ /** First non-empty host-family flag value (host / device / on / computer). */
80
+ export function hostFamilyTarget(flags) {
81
+ for (const v of [flags.host, flags.device, flags.on, flags.computer]) {
82
+ if (v)
83
+ return v;
84
+ }
85
+ return undefined;
86
+ }
87
+ /**
88
+ * Resolve placement from run flags. `--where` wins only when no other
89
+ * placement flag is set; mixing is a PlacementError.
90
+ */
91
+ export function placementFromRunFlags(flags) {
92
+ const where = flags.where?.trim();
93
+ const hostT = hostFamilyTarget(flags);
94
+ const hasLease = flags.lease !== undefined && flags.lease !== false;
95
+ const hasBox = !!flags.box;
96
+ const placementFlags = [];
97
+ if (where)
98
+ placementFlags.push('--where');
99
+ if (hostT)
100
+ placementFlags.push('--host/--device');
101
+ if (hasLease)
102
+ placementFlags.push('--lease');
103
+ if (hasBox)
104
+ placementFlags.push('--box');
105
+ if (placementFlags.length > 1) {
106
+ throw new PlacementError(`Conflicting placement flags: ${placementFlags.join(' + ')}. ` +
107
+ `Use one door — prefer --where (device:<name> | auto | lease | local).`);
108
+ }
109
+ if (where)
110
+ return parseWhereSpec(where, '--where');
111
+ if (hasBox)
112
+ return { kind: 'lease', target: flags.box, source: '--box' };
113
+ if (hasLease) {
114
+ const backend = typeof flags.lease === 'string' ? flags.lease : undefined;
115
+ return { kind: 'lease', target: backend, source: '--lease' };
116
+ }
117
+ if (hostT)
118
+ return { kind: 'device', target: hostT, source: '--host/--device' };
119
+ return { kind: 'local', source: 'default' };
120
+ }
121
+ /**
122
+ * Expand a resolved placement into the concrete run option fields the
123
+ * existing dispatch paths already understand. Pure — does not mutate input.
124
+ *
125
+ * `cloud` and `fleet` are not valid for a bare `agents run` (use `cloud run`
126
+ * / routines); they throw so callers fail loud.
127
+ */
128
+ export function expandPlacementToRunFlags(placement) {
129
+ switch (placement.kind) {
130
+ case 'local':
131
+ return {};
132
+ case 'device':
133
+ if (!placement.target) {
134
+ throw new PlacementError(`${placement.source}: device placement needs a target (name or auto)`);
135
+ }
136
+ // Canonical host flag; --device is an alias of the same path.
137
+ return { host: placement.target };
138
+ case 'lease':
139
+ // --box reuses a warm slug; --where lease[:backend] / --lease provisions.
140
+ if (placement.source === '--box')
141
+ return { box: placement.target };
142
+ return placement.target ? { lease: placement.target } : { lease: true };
143
+ case 'fleet':
144
+ throw new PlacementError(`fleet placement is for routines (agents routines add … --placement fleet), not agents run. ` +
145
+ `Use --where device:auto for an affinity pick, or --where device:<name>.`);
146
+ case 'cloud':
147
+ throw new PlacementError(`cloud placement is agents cloud run (vendor cloud), not agents run. ` +
148
+ `For a disposable box use --where lease; for your fleet use --where device:<name>.`);
149
+ }
150
+ }
151
+ /** Map routines hostStrategy (+ optional host) onto the shared Placement. */
152
+ export function placementFromHostStrategy(strategy, host) {
153
+ switch (strategy) {
154
+ case 'local':
155
+ return { kind: 'local', source: 'hostStrategy:local' };
156
+ case 'host':
157
+ return { kind: 'device', target: host, source: 'hostStrategy:host' };
158
+ case 'fleet':
159
+ return { kind: 'fleet', source: 'hostStrategy:fleet' };
160
+ case 'cloud':
161
+ return { kind: 'cloud', source: 'hostStrategy:cloud' };
162
+ }
163
+ }
164
+ /** One-line human form for logs / help. */
165
+ export function formatPlacement(p) {
166
+ if (p.kind === 'local')
167
+ return 'local';
168
+ if (p.target)
169
+ return `${p.kind}:${p.target}`;
170
+ return p.kind;
171
+ }
172
+ /**
173
+ * Short matrix for help footers and docs. Keep in sync with 00-concepts.md.
174
+ */
175
+ export const PLACEMENT_MATRIX = `
176
+ Intent Flag / path
177
+ ───────────────────────────── ──────────────────────────────────────────
178
+ This machine (default) or --where local
179
+ Named fleet box --where device:<name> (= --host / --device)
180
+ Affinity pick (14d usage) --where auto (= --device auto)
181
+ Disposable cloud box --where lease (= --lease)
182
+ Reuse warm crabbox --box <slug>
183
+ Routines: body on one box --run-on <name> / --placement host
184
+ Routines: pick any online --placement fleet
185
+ Vendor cloud task agents cloud run …
186
+ Monitors: who evaluates --device <owner> (NOT body placement)
187
+ Monitors: where action runs --run-on <host>
188
+ `.trim();
@@ -31,9 +31,10 @@ export interface Profile {
31
31
  preset?: string;
32
32
  provider?: string;
33
33
  /**
34
- * Human-facing label for the harness — what `agents view` prints as the
35
- * agent-type header, the same slot `AGENTS[id].name` fills for a native
36
- * harness. Defaults to the profile name when unset.
34
+ * Stored for backward-compatible YAML parsing only — no longer read for
35
+ * display. `profileLabel()` always derives the display name from `name` via
36
+ * the vendor/brand table. Old YAML files that carry this key still parse
37
+ * correctly; it is simply ignored.
37
38
  */
38
39
  label?: string;
39
40
  /**
@@ -71,7 +72,7 @@ export interface Profile {
71
72
  */
72
73
  export interface ProfileSummary {
73
74
  name: string;
74
- /** Human-facing header label — `label` when set, else the profile name. */
75
+ /** Human-facing header label — always derived from `name` via the vendor/brand table. */
75
76
  label: string;
76
77
  agent: AgentId;
77
78
  host: string;
@@ -125,8 +126,12 @@ export declare function profileModelEnvKey(profile: Profile): string | null;
125
126
  */
126
127
  export declare function profileAuthLabel(profile: Profile): string;
127
128
  /**
128
- * Header label for the harness — the slot `AGENTS[id].name` fills for a native
129
- * harness, so `agents view` can print custom and native harnesses the same way.
129
+ * Header label for the harness — derived from `profile.name` by splitting on
130
+ * `[-_]` and mapping each token through the vendor/brand table. Never reads
131
+ * the stored `label` field; old YAML files with a `label:` key are unaffected.
132
+ *
133
+ * Examples: `deepseek-flash` → `'DeepSeek Flash'`, `spark` → `'Spark'`,
134
+ * `deepseek_chat_v3` → `'DeepSeek Chat V3'`.
130
135
  */
131
136
  export declare function profileLabel(profile: Profile): string;
132
137
  /** Build a stable, machine-readable summary for list and view surfaces. */
@@ -153,8 +158,6 @@ export interface HostModelOptions {
153
158
  /** Env var the host reads its auth token from; pair with `provider` to attach keychain auth. */
154
159
  authEnvVar?: string;
155
160
  description?: string;
156
- /** Human-facing header label; defaults to the harness name. */
157
- label?: string;
158
161
  }
159
162
  /**
160
163
  * Build a custom-harness profile from a host CLI + model in one shot, without a
@@ -176,7 +179,6 @@ export interface ForkProfileOptions {
176
179
  authEnvVar?: string;
177
180
  /** Re-pin (or unpin, with an empty string) the host CLI version. */
178
181
  version?: string;
179
- label?: string;
180
182
  description?: string;
181
183
  }
182
184
  /**
@@ -185,6 +187,26 @@ export interface ForkProfileOptions {
185
187
  * diverge from here and deleting the source never affects the fork.
186
188
  */
187
189
  export declare function forkProfile(source: Profile, name: string, opts?: ForkProfileOptions): Profile;
190
+ /**
191
+ * Edit an existing profile in-place, applying overrides without changing its
192
+ * name or lineage. Reuses {@link forkProfile}'s validation and override logic
193
+ * (model swap, base-URL validation, auth repoint), then restores the original
194
+ * `forkedFrom` so an edit never self-references the profile.
195
+ *
196
+ * Note: this returns the updated `Profile` object but does NOT write it to
197
+ * disk — callers should follow up with `writeProfile(result)` if persistence
198
+ * is needed.
199
+ */
200
+ export declare function editProfile(source: Profile, opts?: ForkProfileOptions): Profile;
201
+ /**
202
+ * Rename a profile on disk, then rewrite `forkedFrom` in every other profile
203
+ * that pointed at the old name so lineage display never goes stale.
204
+ *
205
+ * Throws if `oldName` does not exist or `newName` already exists. There is no
206
+ * `--force` / overwrite path — a collision is a hard error directing the user
207
+ * to remove the target first.
208
+ */
209
+ export declare function renameProfile(oldName: string, newName: string): void;
188
210
  /**
189
211
  * Resolve a profile into the env block that should be injected into the
190
212
  * spawned agent process. Reads the token from keychain at exec time so the
@@ -199,11 +199,46 @@ export function profileAuthLabel(profile) {
199
199
  return provider;
200
200
  }
201
201
  /**
202
- * Header label for the harness — the slot `AGENTS[id].name` fills for a native
203
- * harness, so `agents view` can print custom and native harnesses the same way.
202
+ * Curated vendor/brand display names, matched case-insensitively per token.
203
+ * Entries with a space (e.g. 'Moonshot AI') are single-token → multi-word expansions.
204
+ */
205
+ const VENDOR_TABLE = [
206
+ ['deepseek', 'DeepSeek'],
207
+ ['openai', 'OpenAI'],
208
+ ['anthropic', 'Anthropic'],
209
+ ['claude', 'Claude'],
210
+ ['grok', 'Grok'],
211
+ ['xai', 'xAI'],
212
+ ['gpt', 'GPT'],
213
+ ['meta', 'Meta'],
214
+ ['mistral', 'Mistral'],
215
+ ['mistralai', 'Mistral'],
216
+ ['qwen', 'Qwen'],
217
+ ['gemini', 'Gemini'],
218
+ ['moonshot', 'Moonshot AI'],
219
+ ['moonshotai', 'Moonshot AI'],
220
+ ['kimi', 'Kimi'],
221
+ ['cohere', 'Cohere'],
222
+ ['perplexity', 'Perplexity'],
223
+ ];
224
+ function tokenToDisplayName(token) {
225
+ const lower = token.toLowerCase();
226
+ for (const [key, display] of VENDOR_TABLE) {
227
+ if (lower === key)
228
+ return display;
229
+ }
230
+ return token.charAt(0).toUpperCase() + token.slice(1);
231
+ }
232
+ /**
233
+ * Header label for the harness — derived from `profile.name` by splitting on
234
+ * `[-_]` and mapping each token through the vendor/brand table. Never reads
235
+ * the stored `label` field; old YAML files with a `label:` key are unaffected.
236
+ *
237
+ * Examples: `deepseek-flash` → `'DeepSeek Flash'`, `spark` → `'Spark'`,
238
+ * `deepseek_chat_v3` → `'DeepSeek Chat V3'`.
204
239
  */
205
240
  export function profileLabel(profile) {
206
- return profile.label || profile.name;
241
+ return profile.name.split(/[-_]/).map(tokenToDisplayName).join(' ');
207
242
  }
208
243
  /** Build a stable, machine-readable summary for list and view surfaces. */
209
244
  export function profileSummary(profile) {
@@ -300,8 +335,6 @@ export function profileFromHostModel(name, host, model, opts = {}) {
300
335
  provider: opts.provider ?? host,
301
336
  forkedFrom: host,
302
337
  };
303
- if (opts.label)
304
- profile.label = opts.label;
305
338
  if (opts.provider && opts.authEnvVar) {
306
339
  profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
307
340
  profile.authOptional = false;
@@ -337,14 +370,6 @@ export function forkProfile(source, name, opts = {}) {
337
370
  description: opts.description ?? (opts.model ? `Forked from ${source.name}: ${opts.model}` : source.description),
338
371
  forkedFrom: source.name,
339
372
  };
340
- // `label` is the header `agents view` prints, so an inherited one would make
341
- // the fork and its source visually identical — the ambiguity a per-harness
342
- // block exists to remove. A fork carries a label only when it is given one;
343
- // otherwise `profileLabel` falls back to the fork's own name.
344
- if (opts.label)
345
- forked.label = opts.label;
346
- else
347
- delete forked.label;
348
373
  // A fork that repoints the model or endpoint is no longer that preset — keep
349
374
  // the preset link only while the fork still matches what the preset defines.
350
375
  if (opts.model || opts.baseUrl)
@@ -360,6 +385,51 @@ export function forkProfile(source, name, opts = {}) {
360
385
  }
361
386
  return forked;
362
387
  }
388
+ /**
389
+ * Edit an existing profile in-place, applying overrides without changing its
390
+ * name or lineage. Reuses {@link forkProfile}'s validation and override logic
391
+ * (model swap, base-URL validation, auth repoint), then restores the original
392
+ * `forkedFrom` so an edit never self-references the profile.
393
+ *
394
+ * Note: this returns the updated `Profile` object but does NOT write it to
395
+ * disk — callers should follow up with `writeProfile(result)` if persistence
396
+ * is needed.
397
+ */
398
+ export function editProfile(source, opts = {}) {
399
+ const edited = forkProfile(source, source.name, opts);
400
+ // forkProfile sets forkedFrom = source.name; for an in-place edit that would
401
+ // be a self-reference. Restore the original lineage instead.
402
+ edited.forkedFrom = source.forkedFrom;
403
+ return edited;
404
+ }
405
+ /**
406
+ * Rename a profile on disk, then rewrite `forkedFrom` in every other profile
407
+ * that pointed at the old name so lineage display never goes stale.
408
+ *
409
+ * Throws if `oldName` does not exist or `newName` already exists. There is no
410
+ * `--force` / overwrite path — a collision is a hard error directing the user
411
+ * to remove the target first.
412
+ */
413
+ export function renameProfile(oldName, newName) {
414
+ validateProfileName(newName);
415
+ if (!profileExists(oldName)) {
416
+ throw new Error(`Profile '${oldName}' not found.`);
417
+ }
418
+ if (profileExists(newName)) {
419
+ throw new Error(`Profile '${newName}' already exists; remove it first.`);
420
+ }
421
+ const profile = readProfile(oldName);
422
+ profile.name = newName;
423
+ writeProfile(profile);
424
+ deleteProfile(oldName);
425
+ // Rewrite forkedFrom in every other profile that referenced the old name.
426
+ for (const other of listProfiles()) {
427
+ if (other.name !== newName && other.forkedFrom === oldName) {
428
+ other.forkedFrom = newName;
429
+ writeProfile(other);
430
+ }
431
+ }
432
+ }
363
433
  /**
364
434
  * Resolve a profile into the env block that should be injected into the
365
435
  * spawned agent process. Reads the token from keychain at exec time so the
@@ -2,6 +2,10 @@
2
2
  * Shared redaction helpers for text that may be exported or logged.
3
3
  */
4
4
  const SECRET_PATTERNS = [
5
+ // Local home paths identify operators and disclose internal filesystem layout.
6
+ // Keep the useful path suffix while masking the machine-specific home prefix.
7
+ [/(^|[\s,"'`(=:])\/(?:home|Users)\/[^/\s,"'`]+/g, '$1[HOME]'],
8
+ [/(^|[\s,"'`(=])[A-Z]:\\Users\\[^\\\s,"'`]+/gi, '$1[HOME]'],
5
9
  [/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
6
10
  // GitHub: classic PATs (ghp_), OAuth (gho_), app/refresh/server tokens
7
11
  // (ghs_/ghr_), and fine-grained PATs (github_pat_). All share the 36-char
@@ -8,6 +8,7 @@ import type { AgentId, RunStrategy } from './types.js';
8
8
  import type { FallbackEntry } from './exec.js';
9
9
  import { type AccountInfo, type CredentialPresence } from './agents.js';
10
10
  import { type UsageSnapshot } from './usage.js';
11
+ import { type AuthVerdict } from './auth-health.js';
11
12
  export interface RotateCandidate {
12
13
  agent: AgentId;
13
14
  version: string;
@@ -35,6 +36,18 @@ export interface RotateCandidate {
35
36
  usageMinutesToLimit: number | null;
36
37
  plan: string | null;
37
38
  signedIn: boolean;
39
+ /**
40
+ * Live auth-health verdict for this (agent, version) from the daemon's probe
41
+ * cache (`auth-health.ts`), or null when no probe row exists (cold cache, or a
42
+ * harness with no live-probe endpoint). `signedIn` only means "a credential
43
+ * file is present and its email decodes" — it cannot tell a good token from a
44
+ * revoked-but-unexpired one, so a server-rejected account reads
45
+ * `signedIn: true` but `authVerdict: 'revoked'`. Eligibility excludes a
46
+ * revoked account so rotation never launches into a doomed auth (see
47
+ * {@link readinessFromCandidate}). Fail-open: any non-revoked or null verdict
48
+ * does not gate — a stale/absent probe never blocks a launch.
49
+ */
50
+ authVerdict: AuthVerdict | null;
38
51
  lastActive: Date | null;
39
52
  }
40
53
  export interface RotateResult {
@@ -108,15 +121,17 @@ export declare const USAGE_DECISION_MAX_AGE_MS: number;
108
121
  export declare function isUsageVerified(candidate: RotateCandidate, nowMs?: number): boolean;
109
122
  /**
110
123
  * Whether a specific account can serve a run right now, and — when it can't —
111
- * why. `signed_out` covers a missing usable credential; `rate_limited` and
112
- * `out_of_credits` name the throttle. Used to pre-warn on a version-pinned
113
- * teammate whose account rotation won't route around (a pin IS the target).
124
+ * why. `signed_out` covers a missing usable credential; `revoked` is a token the
125
+ * server has actually rejected (401/403, from the live auth-health probe);
126
+ * `rate_limited` and `out_of_credits` name the throttle. Used to pre-warn on a
127
+ * version-pinned teammate whose account rotation won't route around (a pin IS
128
+ * the target).
114
129
  */
115
130
  export type AccountReadiness = {
116
131
  ready: true;
117
132
  } | {
118
133
  ready: false;
119
- reason: 'rate_limited' | 'out_of_credits' | 'signed_out';
134
+ reason: 'rate_limited' | 'out_of_credits' | 'signed_out' | 'revoked';
120
135
  email: string | null;
121
136
  };
122
137
  /**