@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.
- package/CHANGELOG.md +97 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +89 -21
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -2
- package/dist/commands/harness.js +316 -20
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +14 -2
- package/dist/commands/view.d.ts +7 -0
- package/dist/commands/view.js +1 -1
- package/dist/index.js +11 -1
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +41 -3
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +31 -9
- package/dist/lib/profiles.js +83 -13
- package/dist/lib/redact.js +4 -0
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/package.json +1 -1
|
Binary file
|
|
@@ -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();
|
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -31,9 +31,10 @@ export interface Profile {
|
|
|
31
31
|
preset?: string;
|
|
32
32
|
provider?: string;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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 — `
|
|
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 —
|
|
129
|
-
*
|
|
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
|
package/dist/lib/profiles.js
CHANGED
|
@@ -199,11 +199,46 @@ export function profileAuthLabel(profile) {
|
|
|
199
199
|
return provider;
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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.
|
|
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
|
package/dist/lib/redact.js
CHANGED
|
@@ -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
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -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; `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
/**
|