@phnx-labs/agents-cli 1.20.85 → 1.20.87
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 +168 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +44 -5
- package/dist/commands/models.js +1 -1
- package/dist/commands/sessions-browser.d.ts +18 -0
- package/dist/commands/sessions-browser.js +126 -24
- package/dist/commands/sessions-picker.d.ts +21 -8
- package/dist/commands/sessions-picker.js +88 -11
- package/dist/commands/sessions.d.ts +19 -0
- package/dist/commands/sessions.js +147 -18
- package/dist/commands/ssh.js +59 -1
- package/dist/commands/teams-picker.d.ts +2 -0
- package/dist/commands/teams-picker.js +2 -1
- package/dist/commands/teams.d.ts +4 -1
- package/dist/commands/teams.js +106 -70
- package/dist/commands/view.js +14 -3
- package/dist/index.js +31 -1
- package/dist/lib/claude-account-token.d.ts +12 -0
- package/dist/lib/claude-account-token.js +63 -0
- package/dist/lib/devices/registry.d.ts +25 -0
- package/dist/lib/devices/registry.js +82 -1
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +13 -0
- package/dist/lib/exec.js +10 -1
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/models.d.ts +21 -0
- package/dist/lib/models.js +133 -4
- 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/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/session/db.d.ts +21 -0
- package/dist/lib/session/db.js +45 -4
- package/dist/lib/session/parse.d.ts +11 -0
- package/dist/lib/session/parse.js +24 -7
- package/dist/lib/session/remote-list.d.ts +7 -0
- package/dist/lib/session/remote-list.js +8 -4
- package/dist/lib/session/state.d.ts +6 -5
- package/dist/lib/session/state.js +84 -11
- package/dist/lib/session/team-filter.d.ts +22 -3
- package/dist/lib/session/team-filter.js +106 -17
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/signin-badge.d.ts +17 -0
- package/dist/lib/signin-badge.js +19 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/usage.js +1 -60
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
2
|
+
export declare function claudeAccountTokenKey(account: string): string;
|
|
3
|
+
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
4
|
+
export declare function readClaudeAccountEmail(home?: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a long-lived `claude setup-token` for the account signed into `home`
|
|
7
|
+
* from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
|
|
8
|
+
* ONLY when the bundle is file-backed (never keychain), so this path itself can
|
|
9
|
+
* never trigger a Touch ID prompt — that is the entire point: usage/probe reads
|
|
10
|
+
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveClaudeSetupToken(home?: string): string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { bundleBackend, bundleExists, readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
|
|
7
|
+
* setup-tokens. Usage/probe reads authenticate with these instead of Claude
|
|
8
|
+
* Code's ACL-bound login item, so they never pop Touch ID. Keyed strictly
|
|
9
|
+
* per-account (`CLAUDE_CODE_OAUTH_TOKEN_<slug>` from the account email) — never a
|
|
10
|
+
* bare key, so one account's token can't be misapplied to another in a
|
|
11
|
+
* multi-account fleet.
|
|
12
|
+
*/
|
|
13
|
+
const AUTH_BUNDLE = 'auth';
|
|
14
|
+
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
15
|
+
export function claudeAccountTokenKey(account) {
|
|
16
|
+
const slug = account
|
|
17
|
+
.trim()
|
|
18
|
+
.toUpperCase()
|
|
19
|
+
.replace(/@/g, '_AT_')
|
|
20
|
+
.replace(/\./g, '_DOT_')
|
|
21
|
+
.replace(/[^A-Z0-9_]/g, '_');
|
|
22
|
+
return `CLAUDE_CODE_OAUTH_TOKEN_${slug}`;
|
|
23
|
+
}
|
|
24
|
+
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
25
|
+
export function readClaudeAccountEmail(home) {
|
|
26
|
+
const base = home ?? os.homedir();
|
|
27
|
+
for (const p of [path.join(base, '.claude', '.claude.json'), path.join(base, '.claude.json')]) {
|
|
28
|
+
try {
|
|
29
|
+
const email = JSON.parse(fs.readFileSync(p, 'utf-8')).oauthAccount?.emailAddress;
|
|
30
|
+
if (typeof email === 'string' && email.trim().length > 0)
|
|
31
|
+
return email.trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Missing/unreadable at this location — try the next.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a long-lived `claude setup-token` for the account signed into `home`
|
|
41
|
+
* from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
|
|
42
|
+
* ONLY when the bundle is file-backed (never keychain), so this path itself can
|
|
43
|
+
* never trigger a Touch ID prompt — that is the entire point: usage/probe reads
|
|
44
|
+
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveClaudeSetupToken(home) {
|
|
47
|
+
try {
|
|
48
|
+
// Require a known account (email) up front: without it we cannot key a
|
|
49
|
+
// per-account token, and we must NOT fall back to a bare shared key that
|
|
50
|
+
// would misapply one account's setup-token to another.
|
|
51
|
+
const email = readClaudeAccountEmail(home);
|
|
52
|
+
if (!email)
|
|
53
|
+
return null;
|
|
54
|
+
if (!bundleExists(AUTH_BUNDLE) || bundleBackend(AUTH_BUNDLE) !== 'file')
|
|
55
|
+
return null;
|
|
56
|
+
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
57
|
+
const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
|
|
58
|
+
return v.length > 0 ? v : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -142,3 +142,28 @@ export declare function addIgnored(name: string): Promise<Set<string>>;
|
|
|
142
142
|
/** Remove a node name from the ignore-list (un-ignore). Returns false if it was
|
|
143
143
|
* not ignored. */
|
|
144
144
|
export declare function removeIgnored(name: string): Promise<boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* Auto-launch preferences: which registered devices are eligible for Factory's
|
|
147
|
+
* auto-host selection, and which are preferred. Stored as a sibling to the
|
|
148
|
+
* registry and ignore-list under ~/.agents/.history/devices/.
|
|
149
|
+
*/
|
|
150
|
+
export interface AutoLaunchPreference {
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
preferred?: boolean;
|
|
153
|
+
}
|
|
154
|
+
export interface AutoLaunchPreferences {
|
|
155
|
+
devices: Record<string, AutoLaunchPreference>;
|
|
156
|
+
updatedAt: string;
|
|
157
|
+
}
|
|
158
|
+
/** Load auto-launch preferences. Missing or malformed file => empty map. */
|
|
159
|
+
export declare function loadAutoLaunchPreferences(): Promise<Record<string, AutoLaunchPreference>>;
|
|
160
|
+
/** True if the device is enabled for auto-launch. Missing entry defaults to true. */
|
|
161
|
+
export declare function isAutoLaunchEnabled(name: string): Promise<boolean>;
|
|
162
|
+
/** Set whether a device is enabled for auto-launch. Setting to the default
|
|
163
|
+
* (enabled) removes the entry to keep the file minimal. */
|
|
164
|
+
export declare function setAutoLaunchEnabled(name: string, enabled: boolean): Promise<void>;
|
|
165
|
+
/** True if the device is preferred for auto-launch ranking. */
|
|
166
|
+
export declare function isAutoLaunchPreferred(name: string): Promise<boolean>;
|
|
167
|
+
/** Set whether a device is preferred for auto-launch. Setting to the default
|
|
168
|
+
* (not preferred) removes the flag to keep the file minimal. */
|
|
169
|
+
export declare function setAutoLaunchPreferred(name: string, preferred: boolean): Promise<void>;
|
|
@@ -18,7 +18,7 @@ import * as fsSync from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { randomBytes } from 'crypto';
|
|
20
20
|
import lockfile from 'proper-lockfile';
|
|
21
|
-
import { getDevicesRegistryPath, getDevicesIgnoredPath } from '../state.js';
|
|
21
|
+
import { getDevicesRegistryPath, getDevicesIgnoredPath, getDevicesAutoLaunchPath } from '../state.js';
|
|
22
22
|
/** A device's effective role, defaulting to `worker` when unset. */
|
|
23
23
|
export function deviceRole(d) {
|
|
24
24
|
return d.role ?? 'worker';
|
|
@@ -288,3 +288,84 @@ export async function removeIgnored(name) {
|
|
|
288
288
|
return true;
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
|
+
function autoLaunchPath() {
|
|
292
|
+
return getDevicesAutoLaunchPath();
|
|
293
|
+
}
|
|
294
|
+
/** Load auto-launch preferences. Missing or malformed file => empty map. */
|
|
295
|
+
export async function loadAutoLaunchPreferences() {
|
|
296
|
+
const p = autoLaunchPath();
|
|
297
|
+
let raw;
|
|
298
|
+
try {
|
|
299
|
+
raw = await fs.readFile(p, 'utf-8');
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err && err.code === 'ENOENT')
|
|
303
|
+
return {};
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(raw);
|
|
308
|
+
return parsed.devices && typeof parsed.devices === 'object' ? parsed.devices : {};
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
throw new Error(`Device auto-launch preferences corrupted at ${p}: ${err?.message ?? err}. Inspect and restore from backup.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** True if the device is enabled for auto-launch. Missing entry defaults to true. */
|
|
315
|
+
export async function isAutoLaunchEnabled(name) {
|
|
316
|
+
assertValidDeviceName(name);
|
|
317
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
318
|
+
return prefs[name]?.enabled !== false;
|
|
319
|
+
}
|
|
320
|
+
/** Set whether a device is enabled for auto-launch. Setting to the default
|
|
321
|
+
* (enabled) removes the entry to keep the file minimal. */
|
|
322
|
+
export async function setAutoLaunchEnabled(name, enabled) {
|
|
323
|
+
assertValidDeviceName(name);
|
|
324
|
+
const p = autoLaunchPath();
|
|
325
|
+
await withRegistryLock(p, async () => {
|
|
326
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
327
|
+
if (enabled) {
|
|
328
|
+
if (prefs[name]) {
|
|
329
|
+
const { enabled: _, ...rest } = prefs[name];
|
|
330
|
+
if (Object.keys(rest).length === 0) {
|
|
331
|
+
delete prefs[name];
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
prefs[name] = rest;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
prefs[name] = { ...prefs[name], enabled: false };
|
|
340
|
+
}
|
|
341
|
+
await atomicWriteJson(p, { devices: prefs, updatedAt: new Date().toISOString() });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/** True if the device is preferred for auto-launch ranking. */
|
|
345
|
+
export async function isAutoLaunchPreferred(name) {
|
|
346
|
+
assertValidDeviceName(name);
|
|
347
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
348
|
+
return prefs[name]?.preferred === true;
|
|
349
|
+
}
|
|
350
|
+
/** Set whether a device is preferred for auto-launch. Setting to the default
|
|
351
|
+
* (not preferred) removes the flag to keep the file minimal. */
|
|
352
|
+
export async function setAutoLaunchPreferred(name, preferred) {
|
|
353
|
+
assertValidDeviceName(name);
|
|
354
|
+
const p = autoLaunchPath();
|
|
355
|
+
await withRegistryLock(p, async () => {
|
|
356
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
357
|
+
if (preferred) {
|
|
358
|
+
prefs[name] = { ...prefs[name], preferred: true };
|
|
359
|
+
}
|
|
360
|
+
else if (prefs[name]) {
|
|
361
|
+
const { preferred: _, ...rest } = prefs[name];
|
|
362
|
+
if (Object.keys(rest).length === 0) {
|
|
363
|
+
delete prefs[name];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
prefs[name] = rest;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
await atomicWriteJson(p, { devices: prefs, updatedAt: new Date().toISOString() });
|
|
370
|
+
});
|
|
371
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
export declare function levelFor(event: EventType): EventLevel;
|
|
18
18
|
export interface EventMeta {
|
|
19
19
|
ts: string;
|
|
@@ -156,6 +156,13 @@ export declare function emitCommand(command: string, args?: string[], payload?:
|
|
|
156
156
|
* Emit an error event with full details.
|
|
157
157
|
*/
|
|
158
158
|
export declare function emitError(err: Error | string, payload?: EventPayload): void;
|
|
159
|
+
/**
|
|
160
|
+
* Emit a friction event — a structured, point-of-use record of a failure or
|
|
161
|
+
* block the CLI just hit. `surface` is the subsystem (teams, browser, secrets,
|
|
162
|
+
* guard, …); `failureId` is a stable slug that lets the nightly routine group
|
|
163
|
+
* the same failure across sessions (e.g. 'remote-cwd-on-add', 'not-installed').
|
|
164
|
+
*/
|
|
165
|
+
export declare function emitFriction(surface: string, failureId: string, payload?: EventPayload): void;
|
|
159
166
|
/**
|
|
160
167
|
* Remove log files older than the retention period.
|
|
161
168
|
* Removes numbered gzip archives whose filesystem mtime exceeds retention.
|
package/dist/lib/events.js
CHANGED
|
@@ -523,6 +523,19 @@ export function emitError(err, payload = {}) {
|
|
|
523
523
|
errorStack: truncate(error.stack, 1000),
|
|
524
524
|
});
|
|
525
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Emit a friction event — a structured, point-of-use record of a failure or
|
|
528
|
+
* block the CLI just hit. `surface` is the subsystem (teams, browser, secrets,
|
|
529
|
+
* guard, …); `failureId` is a stable slug that lets the nightly routine group
|
|
530
|
+
* the same failure across sessions (e.g. 'remote-cwd-on-add', 'not-installed').
|
|
531
|
+
*/
|
|
532
|
+
export function emitFriction(surface, failureId, payload = {}) {
|
|
533
|
+
emit('friction', {
|
|
534
|
+
...payload,
|
|
535
|
+
surface,
|
|
536
|
+
failureId,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
526
539
|
// ─── Gzip rotation ──────────────────────────────────────────────────────────
|
|
527
540
|
/** Rotate the active file while its append lock is held. */
|
|
528
541
|
function maybeGzipRotateLocked(logPath) {
|
package/dist/lib/exec.js
CHANGED
|
@@ -28,6 +28,7 @@ import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
|
28
28
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
29
29
|
import { isTmuxInstalled } from './tmux/binary.js';
|
|
30
30
|
import { shellQuote } from './ssh-exec.js';
|
|
31
|
+
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
31
32
|
/**
|
|
32
33
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
33
34
|
*
|
|
@@ -259,7 +260,15 @@ export function buildExecEnv(options) {
|
|
|
259
260
|
? resolvedVersion
|
|
260
261
|
: (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
|
|
261
262
|
if (version) {
|
|
262
|
-
|
|
263
|
+
const versionHome = getVersionHomePath('claude', version);
|
|
264
|
+
result.CLAUDE_CONFIG_DIR = path.join(versionHome, '.claude');
|
|
265
|
+
const setupToken = resolveClaudeSetupToken(versionHome);
|
|
266
|
+
if (setupToken) {
|
|
267
|
+
// A token keyed to this version home's own account replaces any ambient
|
|
268
|
+
// shared value inherited from the launcher. options.env still wins below
|
|
269
|
+
// for explicit caller overrides.
|
|
270
|
+
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
271
|
+
}
|
|
263
272
|
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
264
273
|
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
265
274
|
// half-swapped and broken). Disable it so a pin stays a pin. Honor an
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
|
|
|
27
27
|
* keep the original red-stderr behavior.
|
|
28
28
|
*/
|
|
29
29
|
export declare function die(msg: string, code?: number, opts?: DieOptions): never;
|
|
30
|
+
/**
|
|
31
|
+
* `die()` with a structured friction event attached. Use this at CLI error
|
|
32
|
+
* chokepoints so the nightly routine can classify and rank recurring failures
|
|
33
|
+
* without re-parsing transcripts. `surface` is the subsystem (teams, browser,
|
|
34
|
+
* secrets, guard, …); `failureId` is a stable slug (e.g. 'remote-cwd-on-add').
|
|
35
|
+
*/
|
|
36
|
+
export declare function dieFriction(surface: string, failureId: string, msg: string, code?: number, opts?: DieOptions): never;
|
|
30
37
|
/**
|
|
31
38
|
* Truncate `s` to at most `max` characters, appending a single-char ellipsis
|
|
32
39
|
* (`…`) when shortened. Character-count based (not ANSI/width aware — use
|
package/dist/lib/format.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { readSync } from 'node:fs';
|
|
12
|
+
import { emitFriction } from './events.js';
|
|
12
13
|
/**
|
|
13
14
|
* Render a fatal error to the right stream. Pure — no I/O, no `process.exit` — so
|
|
14
15
|
* the human-vs-agent split is unit-testable. A `--json` caller gets
|
|
@@ -43,6 +44,16 @@ export function die(msg, code = 1, opts = {}) {
|
|
|
43
44
|
console.error(text);
|
|
44
45
|
process.exit(code);
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* `die()` with a structured friction event attached. Use this at CLI error
|
|
49
|
+
* chokepoints so the nightly routine can classify and rank recurring failures
|
|
50
|
+
* without re-parsing transcripts. `surface` is the subsystem (teams, browser,
|
|
51
|
+
* secrets, guard, …); `failureId` is a stable slug (e.g. 'remote-cwd-on-add').
|
|
52
|
+
*/
|
|
53
|
+
export function dieFriction(surface, failureId, msg, code = 1, opts = {}) {
|
|
54
|
+
emitFriction(surface, failureId, { error: msg });
|
|
55
|
+
die(msg, code, opts);
|
|
56
|
+
}
|
|
46
57
|
/**
|
|
47
58
|
* Truncate `s` to at most `max` characters, appending a single-char ellipsis
|
|
48
59
|
* (`…`) when shortened. Character-count based (not ANSI/width aware — use
|
|
Binary file
|
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<dict>
|
|
7
7
|
<key>Resources/AppIcon.icns</key>
|
|
8
8
|
<data>
|
|
9
|
-
|
|
9
|
+
DFq5H08EkhgWIC3UvGMR9B58BZw=
|
|
10
10
|
</data>
|
|
11
11
|
</dict>
|
|
12
12
|
<key>files2</key>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<dict>
|
|
16
16
|
<key>hash2</key>
|
|
17
17
|
<data>
|
|
18
|
-
|
|
18
|
+
mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
|
|
19
19
|
</data>
|
|
20
20
|
</dict>
|
|
21
21
|
</dict>
|
package/dist/lib/models.d.ts
CHANGED
|
@@ -57,6 +57,27 @@ export interface ModelSource {
|
|
|
57
57
|
* Returns null if nothing usable is found.
|
|
58
58
|
*/
|
|
59
59
|
export declare function locateModelSource(agent: AgentId, version: string): ModelSource | null;
|
|
60
|
+
/**
|
|
61
|
+
* Parse `grok models` stdout into a catalog. Exported for unit tests.
|
|
62
|
+
*
|
|
63
|
+
* Output shape (verified 0.2.118):
|
|
64
|
+
* You are logged in with grok.com.
|
|
65
|
+
*
|
|
66
|
+
* Default model: grok-4.5
|
|
67
|
+
*
|
|
68
|
+
* Available models:
|
|
69
|
+
* * grok-4.5 (default)
|
|
70
|
+
*
|
|
71
|
+
* The `Default model:` line is authoritative; rows may also carry a leading `*`
|
|
72
|
+
* and a `(default)` flag. Grok has no `--json` on this subcommand. Settings live
|
|
73
|
+
* in `config.toml` / `models_cache.json`, not `settings.json`, so the native
|
|
74
|
+
* settings.json reader cannot surface the default — the catalog is the source
|
|
75
|
+
* that makes `resolveConfiguredModel` return a cli-default for Grok.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseGrokModelsStdout(stdout: string): {
|
|
78
|
+
models: ModelInfo[];
|
|
79
|
+
aliases: Record<string, string>;
|
|
80
|
+
};
|
|
60
81
|
/**
|
|
61
82
|
* Build (or load from cache) the model catalog for a specific (agent, version).
|
|
62
83
|
* Cache is keyed on source-file mtime (binary or js module), so re-extracts
|
package/dist/lib/models.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each agent ships its model list differently -- Claude and Codex embed it in
|
|
5
5
|
* compiled bundles/binaries, Gemini exports it from a JS module, and OpenCode/
|
|
6
|
-
* Cursor/OpenClaw expose it via CLI commands. This
|
|
7
|
-
* `getModelCatalog()` and `resolveModel()` interface
|
|
8
|
-
* by a file-system cache keyed on source mtime.
|
|
6
|
+
* Cursor/OpenClaw/Antigravity/Kimi/Grok expose it via CLI commands. This
|
|
7
|
+
* module provides a unified `getModelCatalog()` and `resolveModel()` interface
|
|
8
|
+
* over all of them, backed by a file-system cache keyed on source mtime.
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { execFileSync } from 'child_process';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { getVersionDir, getVersionHomePath } from './versions.js';
|
|
14
|
+
import { getVersionDir, getVersionHomePath, getBinaryPath } from './versions.js';
|
|
15
15
|
import { getModelsCachePath } from './state.js';
|
|
16
16
|
import { agentConfigDirName } from './agents.js';
|
|
17
17
|
import { resolveRunDefaults } from './run-defaults.js';
|
|
@@ -188,6 +188,36 @@ export function locateModelSource(agent, version) {
|
|
|
188
188
|
return { path: pathBin, kind: 'cli' };
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
|
+
if (agent === 'grok') {
|
|
192
|
+
// Grok ships a native binary under the version home's `.grok/downloads/`,
|
|
193
|
+
// not node_modules/.bin. Prefer a real binary over a failed-download stub
|
|
194
|
+
// (a 99-byte placeholder sometimes left beside a prior good download).
|
|
195
|
+
const preferred = getBinaryPath('grok', version);
|
|
196
|
+
if (isUsableGrokBinary(preferred))
|
|
197
|
+
return { path: preferred, kind: 'cli' };
|
|
198
|
+
const downloads = path.join(getVersionHomePath('grok', version), '.grok', 'downloads');
|
|
199
|
+
try {
|
|
200
|
+
const candidates = fs
|
|
201
|
+
.readdirSync(downloads)
|
|
202
|
+
.filter((e) => e.startsWith('grok-'))
|
|
203
|
+
.map((e) => path.join(downloads, e))
|
|
204
|
+
.filter(isUsableGrokBinary)
|
|
205
|
+
.sort((a, b) => {
|
|
206
|
+
try {
|
|
207
|
+
return fs.statSync(b).size - fs.statSync(a).size;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
if (candidates[0])
|
|
214
|
+
return { path: candidates[0], kind: 'cli' };
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* empty downloads */
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
191
221
|
if (agent === 'cursor') {
|
|
192
222
|
// cursor-agent is installed via curl script, not agents-cli. Version argument
|
|
193
223
|
// is accepted for API symmetry but ignored -- cursor lives on PATH.
|
|
@@ -198,6 +228,16 @@ export function locateModelSource(agent, version) {
|
|
|
198
228
|
}
|
|
199
229
|
return null;
|
|
200
230
|
}
|
|
231
|
+
/** Real Grok binaries are ~100MB+; failed-download stubs are tens of bytes. */
|
|
232
|
+
function isUsableGrokBinary(filePath) {
|
|
233
|
+
try {
|
|
234
|
+
const st = fs.statSync(filePath);
|
|
235
|
+
return st.isFile() && st.size > 1024 * 1024;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
201
241
|
/** Search PATH for a command and return its absolute path, or null. */
|
|
202
242
|
function findOnPath(command) {
|
|
203
243
|
const pathEnv = process.env.PATH || '';
|
|
@@ -679,6 +719,93 @@ function extractAntigravityCatalog(binaryPath) {
|
|
|
679
719
|
}
|
|
680
720
|
return { models, aliases: {} };
|
|
681
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Parse `grok models` stdout into a catalog. Exported for unit tests.
|
|
724
|
+
*
|
|
725
|
+
* Output shape (verified 0.2.118):
|
|
726
|
+
* You are logged in with grok.com.
|
|
727
|
+
*
|
|
728
|
+
* Default model: grok-4.5
|
|
729
|
+
*
|
|
730
|
+
* Available models:
|
|
731
|
+
* * grok-4.5 (default)
|
|
732
|
+
*
|
|
733
|
+
* The `Default model:` line is authoritative; rows may also carry a leading `*`
|
|
734
|
+
* and a `(default)` flag. Grok has no `--json` on this subcommand. Settings live
|
|
735
|
+
* in `config.toml` / `models_cache.json`, not `settings.json`, so the native
|
|
736
|
+
* settings.json reader cannot surface the default — the catalog is the source
|
|
737
|
+
* that makes `resolveConfiguredModel` return a cli-default for Grok.
|
|
738
|
+
*/
|
|
739
|
+
export function parseGrokModelsStdout(stdout) {
|
|
740
|
+
// Strip ANSI in case a spinner or color codes slip through.
|
|
741
|
+
// eslint-disable-next-line no-control-regex
|
|
742
|
+
const plain = stdout.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
743
|
+
let defaultId = null;
|
|
744
|
+
const defaultLine = plain.match(/Default model:\s*(\S+)/i);
|
|
745
|
+
if (defaultLine)
|
|
746
|
+
defaultId = defaultLine[1];
|
|
747
|
+
const models = [];
|
|
748
|
+
const seen = new Set();
|
|
749
|
+
for (const raw of plain.split('\n')) {
|
|
750
|
+
const line = raw.trim();
|
|
751
|
+
if (!line)
|
|
752
|
+
continue;
|
|
753
|
+
// Rows: "* grok-4.5 (default)" or "grok-4.5" or " grok-code-fast-1"
|
|
754
|
+
const m = line.match(/^\*?\s*([A-Za-z0-9][A-Za-z0-9._-]*)(?:\s+\(([^)]*)\))?\s*$/);
|
|
755
|
+
if (!m)
|
|
756
|
+
continue;
|
|
757
|
+
const id = m[1];
|
|
758
|
+
// Real model ids are grok-* (or match the Default model: line). Skip banner words.
|
|
759
|
+
if (!/^grok[-_]/i.test(id) && id !== defaultId)
|
|
760
|
+
continue;
|
|
761
|
+
if (seen.has(id))
|
|
762
|
+
continue;
|
|
763
|
+
seen.add(id);
|
|
764
|
+
const flags = (m[2] ?? '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
765
|
+
models.push({
|
|
766
|
+
id,
|
|
767
|
+
isDefault: (defaultId != null && id === defaultId) || flags.includes('default'),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
// If Default model was set but did not appear as a row, still surface it.
|
|
771
|
+
if (defaultId && !seen.has(defaultId)) {
|
|
772
|
+
models.unshift({ id: defaultId, isDefault: true });
|
|
773
|
+
}
|
|
774
|
+
// Normalize: exactly one default when we know the Default model: id.
|
|
775
|
+
if (defaultId) {
|
|
776
|
+
for (const model of models)
|
|
777
|
+
model.isDefault = model.id === defaultId;
|
|
778
|
+
}
|
|
779
|
+
else if (models.length > 0 && !models.some((model) => model.isDefault)) {
|
|
780
|
+
models[0].isDefault = true;
|
|
781
|
+
}
|
|
782
|
+
return { models, aliases: {} };
|
|
783
|
+
}
|
|
784
|
+
/** Extract Grok's catalog via `grok models` (see parseGrokModelsStdout). */
|
|
785
|
+
function extractGrokCatalog(binaryPath) {
|
|
786
|
+
const env = { ...process.env };
|
|
787
|
+
// Point GROK_HOME at the version home that owns this binary so auth +
|
|
788
|
+
// models_cache come from the right install, not a host ~/.grok symlink.
|
|
789
|
+
// binary: <home>/.grok/downloads/grok-<ver>-...
|
|
790
|
+
const downloadsDir = path.dirname(binaryPath);
|
|
791
|
+
if (path.basename(downloadsDir) === 'downloads') {
|
|
792
|
+
env.GROK_HOME = path.dirname(downloadsDir);
|
|
793
|
+
}
|
|
794
|
+
let stdout;
|
|
795
|
+
try {
|
|
796
|
+
stdout = execFileSync(binaryPath, ['models'], {
|
|
797
|
+
encoding: 'utf-8',
|
|
798
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
799
|
+
timeout: 15_000,
|
|
800
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
801
|
+
env,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return { models: [], aliases: {} };
|
|
806
|
+
}
|
|
807
|
+
return parseGrokModelsStdout(stdout);
|
|
808
|
+
}
|
|
682
809
|
/**
|
|
683
810
|
* Extract Kimi's catalog via `kimi provider list --json`, which emits the raw
|
|
684
811
|
* providers/models config. Model ids are the `models` object keys (e.g.
|
|
@@ -789,6 +916,8 @@ export function getModelCatalog(agent, version) {
|
|
|
789
916
|
({ models, aliases } = extractAntigravityCatalog(src.path));
|
|
790
917
|
else if (agent === 'kimi')
|
|
791
918
|
({ models, aliases } = extractKimiCatalog(src.path));
|
|
919
|
+
else if (agent === 'grok')
|
|
920
|
+
({ models, aliases } = extractGrokCatalog(src.path));
|
|
792
921
|
}
|
|
793
922
|
const catalog = {
|
|
794
923
|
agent,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<dict>
|
|
7
7
|
<key>Resources/AppIcon.icns</key>
|
|
8
8
|
<data>
|
|
9
|
-
|
|
9
|
+
DFq5H08EkhgWIC3UvGMR9B58BZw=
|
|
10
10
|
</data>
|
|
11
11
|
</dict>
|
|
12
12
|
<key>files2</key>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<dict>
|
|
16
16
|
<key>hash2</key>
|
|
17
17
|
<data>
|
|
18
|
-
|
|
18
|
+
mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
|
|
19
19
|
</data>
|
|
20
20
|
</dict>
|
|
21
21
|
<key>embedded.provisionprofile</key>
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import Database from '../sqlite.js';
|
|
10
10
|
import type { SessionAgentId, SessionMeta } from './types.js';
|
|
11
|
+
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
12
|
+
* assert against the constant instead of hardcoding a number that every bump
|
|
13
|
+
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
14
|
+
export declare const SCHEMA_VERSION = 21;
|
|
11
15
|
/** Raw row shape returned from the sessions table. */
|
|
12
16
|
export interface SessionRow {
|
|
13
17
|
id: string;
|
|
@@ -40,6 +44,7 @@ export interface SessionRow {
|
|
|
40
44
|
pr_number: number | null;
|
|
41
45
|
worktree_slug: string | null;
|
|
42
46
|
ticket_id: string | null;
|
|
47
|
+
spawned_team: string | null;
|
|
43
48
|
plan: string | null;
|
|
44
49
|
machine: string | null;
|
|
45
50
|
todos: string | null;
|
|
@@ -297,6 +302,22 @@ export declare function queryAffinityRollup(options: {
|
|
|
297
302
|
export declare function queryUsageRollup(options: QueryOptions & {
|
|
298
303
|
groupBy: UsageRollupGroup;
|
|
299
304
|
}): UsageRollupRow[];
|
|
305
|
+
/** Who spawned a team: the orchestrator session, from its transcript. */
|
|
306
|
+
export interface TeamSpawner {
|
|
307
|
+
sessionId: string;
|
|
308
|
+
shortId: string;
|
|
309
|
+
/** The human the orchestrator ran as, when the row carries actor provenance. */
|
|
310
|
+
actor?: string;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Map every team name to the session that ran `agents teams create/add` for it.
|
|
314
|
+
*
|
|
315
|
+
* One scan over the rows that carry a `spawned_team`, rather than a query per
|
|
316
|
+
* team — `agents teams list` needs the whole map at once, and the column has no
|
|
317
|
+
* index. When two sessions spawned the same team name (a team re-created after a
|
|
318
|
+
* disband), the most recent wins, which is the one whose work the name refers to.
|
|
319
|
+
*/
|
|
320
|
+
export declare function teamSpawners(): Map<string, TeamSpawner>;
|
|
300
321
|
/** A session with its cost, for the top-N-by-cost listing. */
|
|
301
322
|
export interface TopCostSession {
|
|
302
323
|
meta: SessionMeta;
|