@phnx-labs/agents-cli 1.22.6 → 1.22.7

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 (77) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +35 -4
  5. package/dist/commands/feed.d.ts +2 -1
  6. package/dist/commands/feed.js +47 -20
  7. package/dist/commands/focus.js +22 -1
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/projects.js +122 -107
  10. package/dist/commands/secrets.js +1 -1
  11. package/dist/commands/sessions-backfill.d.ts +33 -0
  12. package/dist/commands/sessions-backfill.js +83 -1
  13. package/dist/commands/sessions-stats.d.ts +36 -0
  14. package/dist/commands/sessions-stats.js +263 -0
  15. package/dist/commands/sessions.d.ts +1 -1
  16. package/dist/commands/sessions.js +21 -1
  17. package/dist/commands/view.d.ts +2 -1
  18. package/dist/commands/view.js +62 -11
  19. package/dist/index.js +1 -2
  20. package/dist/lib/activity.js +2 -2
  21. package/dist/lib/agents.js +68 -11
  22. package/dist/lib/analytics/recipes.js +11 -5
  23. package/dist/lib/browser/profiles.d.ts +15 -7
  24. package/dist/lib/browser/profiles.js +53 -12
  25. package/dist/lib/byok-usage.d.ts +38 -0
  26. package/dist/lib/byok-usage.js +117 -0
  27. package/dist/lib/capabilities.js +1 -1
  28. package/dist/lib/exec.d.ts +20 -0
  29. package/dist/lib/exec.js +73 -8
  30. package/dist/lib/feed-outcome.d.ts +1 -0
  31. package/dist/lib/feed-outcome.js +2 -0
  32. package/dist/lib/feed-post.js +1 -1
  33. package/dist/lib/feed-ranking.d.ts +1 -0
  34. package/dist/lib/feed-ranking.js +4 -0
  35. package/dist/lib/feed.d.ts +4 -1
  36. package/dist/lib/feed.js +25 -0
  37. package/dist/lib/hosts/passthrough.js +0 -1
  38. package/dist/lib/mcp.js +6 -1
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  40. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  41. package/dist/lib/model-tiers.js +4 -1
  42. package/dist/lib/models.js +63 -0
  43. package/dist/lib/profiles.d.ts +42 -1
  44. package/dist/lib/profiles.js +50 -2
  45. package/dist/lib/project-focus.d.ts +9 -0
  46. package/dist/lib/project-focus.js +23 -0
  47. package/dist/lib/project-key.d.ts +1 -1
  48. package/dist/lib/project-key.js +1 -1
  49. package/dist/lib/project-probe.d.ts +18 -0
  50. package/dist/lib/project-probe.js +46 -0
  51. package/dist/lib/project-status.d.ts +56 -0
  52. package/dist/lib/project-status.js +125 -18
  53. package/dist/lib/resources/mcp.js +3 -0
  54. package/dist/lib/resources/types.d.ts +1 -1
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/audit.js +1 -1
  58. package/dist/lib/secrets/index.d.ts +1 -0
  59. package/dist/lib/secrets/index.js +29 -15
  60. package/dist/lib/secrets/remote.d.ts +1 -1
  61. package/dist/lib/secrets/remote.js +1 -1
  62. package/dist/lib/session/active.d.ts +2 -0
  63. package/dist/lib/session/bash-command.d.ts +2 -3
  64. package/dist/lib/session/db.d.ts +92 -1
  65. package/dist/lib/session/db.js +230 -1
  66. package/dist/lib/session/digest.d.ts +1 -1
  67. package/dist/lib/session/digest.js +1 -1
  68. package/dist/lib/share/publish.js +24 -0
  69. package/dist/lib/startup/command-registry.d.ts +0 -1
  70. package/dist/lib/startup/command-registry.js +0 -2
  71. package/dist/lib/subagents-registry.js +3 -0
  72. package/dist/lib/types.d.ts +11 -2
  73. package/dist/lib/usage.d.ts +5 -0
  74. package/dist/lib/usage.js +3 -3
  75. package/package.json +1 -1
  76. package/dist/commands/activity.d.ts +0 -87
  77. package/dist/commands/activity.js +0 -346
@@ -215,7 +215,7 @@ export const AGENTS = {
215
215
  // Claude Code has no headless Anthropic-hosted dispatch CLI (only
216
216
  // --remote-control, which bridges a *local* session). Its cloud is Rush.
217
217
  cloudProvider: 'rush',
218
- capabilities: { hooks: true, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
218
+ capabilities: { hooks: true, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true, interactiveRepl: true },
219
219
  },
220
220
  // codex hooks: gated to >= 0.116.0 (introduced [features] codex_hooks flag).
221
221
  codex: {
@@ -237,7 +237,7 @@ export const AGENTS = {
237
237
  cloudProvider: 'codex',
238
238
  // Subagents: multi-agent plumbing since 0.117.0; custom agents as
239
239
  // ~/.codex/agents/*.toml (name, description, developer_instructions).
240
- capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'] },
240
+ capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
241
241
  },
242
242
  gemini: {
243
243
  id: 'gemini',
@@ -269,7 +269,7 @@ export const AGENTS = {
269
269
  },
270
270
  // gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
271
271
  // extensions: gemini-extension.json bundles shipped in v0.8.0; custom subagents in v0.36.0.
272
- capabilities: { hooks: { since: '0.26.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: { since: '0.8.0' }, subagents: { since: '0.36.0' }, rules: { file: 'GEMINI.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
272
+ capabilities: { hooks: { since: '0.26.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: { since: '0.8.0' }, subagents: { since: '0.36.0' }, rules: { file: 'GEMINI.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], rulesImports: true, interactiveRepl: false },
273
273
  },
274
274
  cursor: {
275
275
  id: 'cursor',
@@ -302,7 +302,9 @@ export const AGENTS = {
302
302
  // + warns for pre-2.4 installs); the direct `subagents add --agents cursor` path
303
303
  // writes unconditionally, same as the other since-gated agents.
304
304
  // See transformSubagentForCursor / https://cursor.com/docs/subagents.
305
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['edit', 'skip'] }, // allowlist: ~/.cursor/cli-config.json
305
+ // interactiveRepl: false — cursor-agent exits immediately with no argv. It requires a
306
+ // prompt to do anything useful; a bare invocation is not a REPL (RUSH-2185, EXEC-23a).
307
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['edit', 'skip'], interactiveRepl: false }, // allowlist: ~/.cursor/cli-config.json
306
308
  },
307
309
  opencode: {
308
310
  id: 'opencode',
@@ -324,7 +326,45 @@ export const AGENTS = {
324
326
  format: 'markdown',
325
327
  variableSyntax: '$ARGUMENTS',
326
328
  supportsHooks: true,
327
- capabilities: { hooks: { since: '0.3.130' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '1.1.1' }, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'] },
329
+ capabilities: { hooks: { since: '0.3.130' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '1.1.1' }, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'], interactiveRepl: true },
330
+ },
331
+ // Oh My Pi (`omp`, omp.sh) — a Bun-based, terminal-first coding agent that runs
332
+ // against many model providers (OpenRouter, OpenAI, Anthropic, xAI, DeepSeek,
333
+ // Ollama, LM Studio, …). It is Claude-compatible: it natively discovers
334
+ // `.claude/commands`, `.mcp.json`, and Claude-shaped subagents, and keeps its
335
+ // own native resources under `~/.omp/agent/` (config dir reported by
336
+ // `omp config path`). configDir points AT the agent dir (not `~/.omp`) so the
337
+ // rules file lands at `~/.omp/agent/AGENTS.md`, the user context file omp reads.
338
+ pi: {
339
+ id: 'pi',
340
+ name: 'Pi',
341
+ color: 'magenta',
342
+ cliCommand: 'omp',
343
+ npmPackage: '@oh-my-pi/pi-coding-agent',
344
+ configDir: path.join(HOME, '.omp', 'agent'),
345
+ commandsDir: path.join(HOME, '.omp', 'agent', 'commands'),
346
+ commandsSubdir: 'commands',
347
+ skillsDir: path.join(HOME, '.omp', 'agent', 'skills'),
348
+ hooksDir: 'hooks',
349
+ instructionsFile: 'AGENTS.md',
350
+ format: 'markdown',
351
+ variableSyntax: '$ARGUMENTS',
352
+ // omp hooks are per-tool JS/TS extension modules discovered from
353
+ // `~/.omp/agent/hooks/{pre,post}/<tool>.<ext>` (loaded as HookFactory code),
354
+ // NOT the event->shell-command registrations agents-cli's hook sync writes.
355
+ // The two models don't map, so hooks stay off (capabilities.hooks:false).
356
+ supportsHooks: false,
357
+ // MCP: omp reads `.mcp.json` with the Claude `{ "mcpServers": {...} }` schema
358
+ // at user scope (`~/.omp/agent/.mcp.json`) and project scope (`<root>/.mcp.json`),
359
+ // stdio + http + sse with headers. skills (`~/.omp/agent/skills/<name>/SKILL.md`),
360
+ // commands (`~/.omp/agent/commands/*.md`), and subagents (`~/.omp/agent/agents/*.md`,
361
+ // Claude-shaped) are all native. allowlist is OFF: omp gates approval per-TOOL
362
+ // only (`tools.approval` record: allow|prompt|deny) with no command/path/domain
363
+ // patterns, so agents-cli's granular permission format has nothing to map to.
364
+ // plugins are npm packages / TS modules, not the Claude marketplace manifest.
365
+ // interactiveRepl: true — bare `omp` runs the TUI; `omp -p` is the one-shot
366
+ // form that answers a prompt and exits (upstream README, "Four entry points").
367
+ capabilities: { hooks: false, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: true, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
328
368
  },
329
369
  openclaw: {
330
370
  id: 'openclaw',
@@ -348,7 +388,7 @@ export const AGENTS = {
348
388
  // OpenClaw is self-updating (no pinned since), so `true` is correct.
349
389
  // Workflows sync as Lobster `.lobster` files under `.openclaw/workflows/`;
350
390
  // the Lobster tool runs them by receiving the file path as `pipeline`.
351
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: true, memory: true, modes: ['plan', 'edit', 'skip'] },
391
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: true, memory: true, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
352
392
  },
353
393
  copilot: {
354
394
  id: 'copilot',
@@ -373,7 +413,9 @@ export const AGENTS = {
373
413
  format: 'markdown',
374
414
  variableSyntax: '$ARGUMENTS',
375
415
  supportsHooks: true,
376
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '0.0.353' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'auto', 'skip'] },
416
+ // interactiveRepl: false — copilot requires a prompt for meaningful work; bare invocation
417
+ // opens a welcome screen but not a persistent coding REPL suitable for agents focus.
418
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '0.0.353' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'auto', 'skip'], interactiveRepl: false },
377
419
  },
378
420
  amp: {
379
421
  id: 'amp',
@@ -390,7 +432,8 @@ export const AGENTS = {
390
432
  format: 'markdown',
391
433
  variableSyntax: '$ARGUMENTS',
392
434
  supportsHooks: false,
393
- capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'] },
435
+ // interactiveRepl: false — amp requires a prompt; bare invocation exits immediately.
436
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['plan', 'edit'], interactiveRepl: false },
394
437
  },
395
438
  kiro: {
396
439
  id: 'kiro',
@@ -413,7 +456,9 @@ export const AGENTS = {
413
456
  format: 'markdown',
414
457
  variableSyntax: '$ARGUMENTS',
415
458
  supportsHooks: true,
416
- capabilities: { hooks: { since: '0.10.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '2.8.0' }, skills: true, commands: true, plugins: false, subagents: { since: '1.23.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'] },
459
+ // interactiveRepl: false — kiro-cli requires a prompt for coding sessions; bare
460
+ // invocation does not open a persistent REPL.
461
+ capabilities: { hooks: { since: '0.10.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: { since: '2.8.0' }, skills: true, commands: true, plugins: false, subagents: { since: '1.23.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: false, modes: ['edit'], interactiveRepl: false },
417
462
  },
418
463
  goose: {
419
464
  id: 'goose',
@@ -445,7 +490,7 @@ export const AGENTS = {
445
490
  // under `slash_commands: [{ command, recipe_path }]` (see goose-commands.ts).
446
491
  // Subagents: recipe YAML named agents under ~/.config/goose/agents/<name>.yaml
447
492
  // (goose auto-discovers and delegates to them by name in autonomous mode).
448
- capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: { since: '1.25.0' }, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: true, memory: false, modes: ['edit'] },
493
+ capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: { since: '1.25.0' }, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: true, memory: false, modes: ['edit'], interactiveRepl: true },
449
494
  },
450
495
  // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
451
496
  // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
@@ -474,7 +519,7 @@ export const AGENTS = {
474
519
  variableSyntax: '{{args}}',
475
520
  supportsHooks: true,
476
521
  cloudProvider: 'antigravity',
477
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '1.0.16' }, rules: { file: 'AGENTS.md' }, workflows: { since: '1.0.6' }, memory: false, modes: ['edit', 'skip'], rulesImports: false }, // workflows: markdown files in the shared, HOME-global ~/.gemini/config/global_workflows/ (agy scans it at startup; not version-isolated — see workflows.ts), invoked as /<name> slash commands
522
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '1.0.16' }, rules: { file: 'AGENTS.md' }, workflows: { since: '1.0.6' }, memory: false, modes: ['edit', 'skip'], rulesImports: false, interactiveRepl: true }, // workflows: markdown files in the shared, HOME-global ~/.gemini/config/global_workflows/ (agy scans it at startup; not version-isolated — see workflows.ts), invoked as /<name> slash commands
478
523
  },
479
524
  // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
480
525
  // first launch, or XAI_API_KEY env var for headless. MCP servers configured inline
@@ -525,6 +570,7 @@ export const AGENTS = {
525
570
  // to auto (→ edit via resolveMode). Interactive plan is unaffected.
526
571
  headlessPlan: false,
527
572
  rulesImports: true,
573
+ interactiveRepl: true,
528
574
  },
529
575
  },
530
576
  // Kimi Code CLI (`kimi`) — Moonshot AI coding agent.
@@ -568,6 +614,7 @@ export const AGENTS = {
568
614
  // auto-runs). Interactive plan is unaffected.
569
615
  headlessPlan: false,
570
616
  rulesImports: false,
617
+ interactiveRepl: true,
571
618
  },
572
619
  },
573
620
  // Factory AI Droid CLI (`droid`) — agentic coding CLI from factory.ai.
@@ -649,6 +696,7 @@ export const AGENTS = {
649
696
  memory: false,
650
697
  modes: ['plan', 'edit', 'auto', 'skip'],
651
698
  rulesImports: false,
699
+ interactiveRepl: true,
652
700
  },
653
701
  },
654
702
  // Nous Hermes Agent. Config lives under ~/.hermes/config.yaml; MCP servers
@@ -698,6 +746,7 @@ export const AGENTS = {
698
746
  memory: true,
699
747
  modes: ['edit'],
700
748
  rulesImports: false,
749
+ interactiveRepl: true,
701
750
  },
702
751
  },
703
752
  };
@@ -2350,6 +2399,9 @@ export function getUserMcpConfigPath(agentId) {
2350
2399
  return path.join(agent.configDir, 'mcp.json');
2351
2400
  case 'hermes':
2352
2401
  return path.join(agent.configDir, 'config.yaml');
2402
+ case 'pi':
2403
+ // omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
2404
+ return path.join(agent.configDir, '.mcp.json');
2353
2405
  default:
2354
2406
  // Gemini and others use settings.json
2355
2407
  return path.join(agent.configDir, 'settings.json');
@@ -2386,6 +2438,8 @@ export function getMcpConfigPathForHome(agentId, home) {
2386
2438
  return path.join(home, '.factory', 'mcp.json');
2387
2439
  case 'hermes':
2388
2440
  return path.join(home, '.hermes', 'config.yaml');
2441
+ case 'pi':
2442
+ return path.join(home, '.omp', 'agent', '.mcp.json');
2389
2443
  default:
2390
2444
  return path.join(home, agentConfigDirName(agentId), 'settings.json');
2391
2445
  }
@@ -2425,6 +2479,9 @@ export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
2425
2479
  return path.join(cwd, '.factory', 'mcp.json');
2426
2480
  case 'hermes':
2427
2481
  return path.join(cwd, '.hermes', 'config.yaml');
2482
+ case 'pi':
2483
+ // omp reads project MCP from <root>/.mcp.json (Claude-compatible).
2484
+ return path.join(cwd, '.mcp.json');
2428
2485
  default:
2429
2486
  return path.join(cwd, `.${agentId}`, 'settings.json');
2430
2487
  }
@@ -92,13 +92,19 @@ export function recipeToolsPerSession(win) {
92
92
  if (!db)
93
93
  return { id, title, store: 'sessions', rows: [], empty: true };
94
94
  try {
95
- const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
96
- if (!cols.some((c) => c.name === 'tool_call_count')) {
95
+ // Counts come from tool_scan_ledger — one row per session the tool indexer
96
+ // has scanned, carrying that session's true call_count (0 included). The
97
+ // sessions.tool_call_count column is NOT the source: only the teams
98
+ // summarizer ever writes it (lib/teams/summarizer.ts), so reading it scored
99
+ // every non-teams session as 0 and pinned p50 at 0 fleet-wide.
100
+ const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'tool_scan_ledger'`).all();
101
+ if (tables.length === 0) {
97
102
  return { id, title, store: 'sessions', rows: [], empty: true };
98
103
  }
99
- const byAgent = db.prepare(`SELECT agent, tool_call_count AS n
100
- FROM sessions
101
- WHERE timestamp >= ? AND tool_call_count IS NOT NULL`).all(win.sinceIso);
104
+ const byAgent = db.prepare(`SELECT s.agent AS agent, l.call_count AS n
105
+ FROM tool_scan_ledger l
106
+ JOIN sessions s ON s.id = l.session_id
107
+ WHERE s.timestamp >= ?`).all(win.sinceIso);
102
108
  if (byAgent.length === 0)
103
109
  return { id, title, store: 'sessions', rows: [], empty: true };
104
110
  const groups = new Map();
@@ -30,13 +30,21 @@ export declare function getProfile(name: string): Promise<BrowserProfile | null>
30
30
  * Resolve the profile `agents browser start` uses when no `--profile` is given.
31
31
  *
32
32
  * Order: (1) the device-local configured default (`agents browser profiles
33
- * set-default <name>`) when it names an existing profile; (2) an existing
34
- * `default` profile as-is; (3) auto-pick the first installed Chromium-family
35
- * browser per the platform priority list in chrome.ts (macOS: chrome > brave >
36
- * edge > chromium > comet; Linux: chrome > chromium > brave > edge; Windows:
37
- * edge > chrome > brave > comet) and pin a new `default` profile to it. Throws an
38
- * actionable error if none of those binaries are installed. A configured default
39
- * that no longer exists warns and falls through to (2)/(3) — never a hard fail.
33
+ * set-default <name>`) when it names an existing profile that can launch here;
34
+ * (2) an existing `default` profile that can launch here; (3) auto-pick the
35
+ * first installed Chromium-family browser per the platform priority list in
36
+ * chrome.ts (macOS: chrome > brave > edge > chromium > comet; Linux: chrome >
37
+ * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
38
+ * `default` profile to it. Throws an actionable error if none of those binaries
39
+ * are installed.
40
+ *
41
+ * "Can launch here" matters because `agents.yaml` syncs across the fleet: a
42
+ * `default` auto-created on macOS carries a `/Applications/...` binary path that
43
+ * doesn't exist on a Linux box. Rather than hand that broken profile back (the
44
+ * top browser roadblock — "Custom binary not found"), we re-detect and
45
+ * regenerate the `default` for this machine. A configured or existing default
46
+ * that no longer exists, or can't launch here, warns and falls through — never a
47
+ * hard fail.
40
48
  */
41
49
  export declare function ensureDefaultBrowserProfile(): Promise<BrowserProfile>;
42
50
  /**
@@ -95,29 +95,63 @@ export async function getProfile(name) {
95
95
  return null;
96
96
  return configToProfile(name, config);
97
97
  }
98
+ /**
99
+ * True when `profile` can actually launch or attach on THIS machine.
100
+ *
101
+ * A remote (`ssh://`) profile runs its browser on the far host, so a local
102
+ * binary check doesn't apply — assume launchable. A local profile must have its
103
+ * configured browser/binary present on disk here; a `default` profile
104
+ * auto-created on a different OS (e.g. a macOS Chrome path reused on Linux — the
105
+ * "Custom binary not found" failure) fails this check so the caller re-detects
106
+ * for the current machine.
107
+ */
108
+ function isProfileLaunchableHere(profile) {
109
+ const remote = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('ssh://'));
110
+ if (remote)
111
+ return true;
112
+ try {
113
+ findBrowserPath(profile.browser, profile.binary);
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
98
120
  /**
99
121
  * Resolve the profile `agents browser start` uses when no `--profile` is given.
100
122
  *
101
123
  * Order: (1) the device-local configured default (`agents browser profiles
102
- * set-default <name>`) when it names an existing profile; (2) an existing
103
- * `default` profile as-is; (3) auto-pick the first installed Chromium-family
104
- * browser per the platform priority list in chrome.ts (macOS: chrome > brave >
105
- * edge > chromium > comet; Linux: chrome > chromium > brave > edge; Windows:
106
- * edge > chrome > brave > comet) and pin a new `default` profile to it. Throws an
107
- * actionable error if none of those binaries are installed. A configured default
108
- * that no longer exists warns and falls through to (2)/(3) — never a hard fail.
124
+ * set-default <name>`) when it names an existing profile that can launch here;
125
+ * (2) an existing `default` profile that can launch here; (3) auto-pick the
126
+ * first installed Chromium-family browser per the platform priority list in
127
+ * chrome.ts (macOS: chrome > brave > edge > chromium > comet; Linux: chrome >
128
+ * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
129
+ * `default` profile to it. Throws an actionable error if none of those binaries
130
+ * are installed.
131
+ *
132
+ * "Can launch here" matters because `agents.yaml` syncs across the fleet: a
133
+ * `default` auto-created on macOS carries a `/Applications/...` binary path that
134
+ * doesn't exist on a Linux box. Rather than hand that broken profile back (the
135
+ * top browser roadblock — "Custom binary not found"), we re-detect and
136
+ * regenerate the `default` for this machine. A configured or existing default
137
+ * that no longer exists, or can't launch here, warns and falls through — never a
138
+ * hard fail.
109
139
  */
110
140
  export async function ensureDefaultBrowserProfile() {
111
141
  const configured = getConfiguredDefaultProfileName();
112
142
  if (configured) {
113
143
  const chosen = await getProfile(configured);
114
- if (chosen)
144
+ if (chosen && isProfileLaunchableHere(chosen))
115
145
  return chosen;
116
- console.warn(`warning: configured default browser profile "${configured}" no longer exists; ` +
117
- `falling back to auto-detect. Fix with: agents browser profiles set-default <name> (or --unset)`);
146
+ console.warn(chosen
147
+ ? `warning: configured default browser profile "${configured}" can't launch on this ` +
148
+ `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
149
+ `Fix with: agents browser profiles set-default <name> (or --unset)`
150
+ : `warning: configured default browser profile "${configured}" no longer exists; ` +
151
+ `falling back to auto-detect. Fix with: agents browser profiles set-default <name> (or --unset)`);
118
152
  }
119
153
  const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
120
- if (existing)
154
+ if (existing && isProfileLaunchableHere(existing))
121
155
  return existing;
122
156
  const detected = findFirstInstalledBrowser();
123
157
  if (!detected) {
@@ -139,7 +173,14 @@ export async function ensureDefaultBrowserProfile() {
139
173
  height: DEFAULT_VIEWPORT.height,
140
174
  },
141
175
  };
142
- await createProfile(profile);
176
+ // A stale `default` (auto-created on another OS, unlaunchable here) is
177
+ // regenerated in place; otherwise this is the first-run create.
178
+ if (existing) {
179
+ await updateProfile(profile);
180
+ }
181
+ else {
182
+ await createProfile(profile);
183
+ }
143
184
  return profile;
144
185
  }
145
186
  /**
@@ -0,0 +1,38 @@
1
+ /**
2
+ * BYOK (Bring Your Own Key) budget fetcher.
3
+ *
4
+ * For custom harnesses that use a provider's API key stored in the keychain
5
+ * (e.g. OpenRouter), this module fetches the account's credit/usage data so
6
+ * `agents view` can show a budget bar alongside the harness detail row — the
7
+ * same bar style the native usage system uses, just sourced from the provider
8
+ * API rather than the CLI's own usage tracking.
9
+ */
10
+ import type { Profile } from './profiles.js';
11
+ export interface ByokBudgetInfo {
12
+ limitUsd: number | null;
13
+ remainingUsd: number | null;
14
+ usedUsd: number;
15
+ usedPercent: number | null;
16
+ fetchedAt: Date;
17
+ }
18
+ export interface ByokUsageResult {
19
+ budget: ByokBudgetInfo | null;
20
+ error: string | null;
21
+ }
22
+ export declare function setByokFetchForTest(fn: typeof globalThis.fetch): void;
23
+ export declare function resetByokCacheForTest(): void;
24
+ export declare function hasByokProvider(provider: string): boolean;
25
+ export declare function renderByokBar(result: ByokUsageResult): string;
26
+ /**
27
+ * Fetch the BYOK budget for a harness.
28
+ *
29
+ * Returns `null` when the harness has no registered BYOK provider or no auth.
30
+ * Returns a `ByokUsageResult` with `budget: null` when the token is absent
31
+ * from the keychain (so the caller can skip rendering a bar without error).
32
+ *
33
+ * Results are SWR-cached per `keychainItem` so harnesses sharing one API key
34
+ * deduplicate to a single network request.
35
+ */
36
+ export declare function getByokUsageForHarness(profile: Profile, opts?: {
37
+ forceRefresh?: boolean;
38
+ }): Promise<ByokUsageResult | null>;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * BYOK (Bring Your Own Key) budget fetcher.
3
+ *
4
+ * For custom harnesses that use a provider's API key stored in the keychain
5
+ * (e.g. OpenRouter), this module fetches the account's credit/usage data so
6
+ * `agents view` can show a budget bar alongside the harness detail row — the
7
+ * same bar style the native usage system uses, just sourced from the provider
8
+ * API rather than the CLI's own usage tracking.
9
+ */
10
+ import { hasKeychainToken, getKeychainToken } from './secrets/profiles.js';
11
+ import { renderBar, getUsageColor, USAGE_CACHE_FRESH_MS, USAGE_CACHE_SWR_MS } from './usage.js';
12
+ // ─── Injectable fetch seam (tests only) ──────────────────────────────────────
13
+ let _fetch = globalThis.fetch;
14
+ export function setByokFetchForTest(fn) {
15
+ _fetch = fn;
16
+ }
17
+ const _cache = new Map();
18
+ export function resetByokCacheForTest() {
19
+ _cache.clear();
20
+ }
21
+ async function fetchOpenRouter(keychainItem) {
22
+ try {
23
+ if (!hasKeychainToken(keychainItem))
24
+ return { budget: null, error: null };
25
+ }
26
+ catch {
27
+ return { budget: null, error: null };
28
+ }
29
+ let token;
30
+ try {
31
+ token = getKeychainToken(keychainItem);
32
+ }
33
+ catch {
34
+ return { budget: null, error: null };
35
+ }
36
+ try {
37
+ const res = await _fetch('https://openrouter.ai/api/v1/key', {
38
+ headers: { Authorization: `Bearer ${token}` },
39
+ });
40
+ if (!res.ok)
41
+ return { budget: null, error: `HTTP ${res.status}` };
42
+ const json = (await res.json());
43
+ const { limit, limit_remaining, usage } = json.data;
44
+ const limitUsd = limit ?? null;
45
+ const usedUsd = usage;
46
+ const usedPercent = limitUsd !== null && limitUsd > 0 ? (usedUsd / limitUsd) * 100 : null;
47
+ return {
48
+ budget: {
49
+ limitUsd,
50
+ remainingUsd: limit_remaining ?? null,
51
+ usedUsd,
52
+ usedPercent,
53
+ fetchedAt: new Date(),
54
+ },
55
+ error: null,
56
+ };
57
+ }
58
+ catch (err) {
59
+ return { budget: null, error: String(err) };
60
+ }
61
+ }
62
+ const BYOK_REGISTRY = {
63
+ openrouter: { fetch: fetchOpenRouter },
64
+ };
65
+ export function hasByokProvider(provider) {
66
+ return Object.prototype.hasOwnProperty.call(BYOK_REGISTRY, provider);
67
+ }
68
+ // ─── Bar renderer ─────────────────────────────────────────────────────────────
69
+ export function renderByokBar(result) {
70
+ if (!result.budget)
71
+ return '';
72
+ const { budget } = result;
73
+ if (budget.limitUsd === null) {
74
+ const bar = renderBar(0, 10, 0);
75
+ return `$: ${bar} $${budget.usedUsd.toFixed(2)} used (unlimited)`;
76
+ }
77
+ const pct = budget.usedPercent ?? 0;
78
+ const bar = renderBar(pct, 10, pct > 0 ? 1 : 0);
79
+ const color = getUsageColor(pct);
80
+ const remaining = budget.remainingUsd !== null ? `$${budget.remainingUsd.toFixed(2)}` : '?';
81
+ const limit = `$${budget.limitUsd.toFixed(2)}`;
82
+ return `$: ${bar} ${color(`${Math.round(pct)}%`)} (${remaining} left of ${limit})`;
83
+ }
84
+ // ─── Public API ───────────────────────────────────────────────────────────────
85
+ /**
86
+ * Fetch the BYOK budget for a harness.
87
+ *
88
+ * Returns `null` when the harness has no registered BYOK provider or no auth.
89
+ * Returns a `ByokUsageResult` with `budget: null` when the token is absent
90
+ * from the keychain (so the caller can skip rendering a bar without error).
91
+ *
92
+ * Results are SWR-cached per `keychainItem` so harnesses sharing one API key
93
+ * deduplicate to a single network request.
94
+ */
95
+ export async function getByokUsageForHarness(profile, opts) {
96
+ if (!profile.provider || !hasByokProvider(profile.provider) || !profile.auth)
97
+ return null;
98
+ const keychainItem = profile.auth.keychainItem;
99
+ const provider = BYOK_REGISTRY[profile.provider];
100
+ const now = Date.now();
101
+ const cached = _cache.get(keychainItem);
102
+ if (!opts?.forceRefresh && cached) {
103
+ const age = now - cached.fetchedAt;
104
+ if (age < USAGE_CACHE_FRESH_MS) {
105
+ return cached.result;
106
+ }
107
+ if (age < USAGE_CACHE_SWR_MS) {
108
+ void provider.fetch(keychainItem).then((result) => {
109
+ _cache.set(keychainItem, { result, fetchedAt: Date.now() });
110
+ });
111
+ return cached.result;
112
+ }
113
+ }
114
+ const result = await provider.fetch(keychainItem);
115
+ _cache.set(keychainItem, { result, fetchedAt: now });
116
+ return result;
117
+ }
@@ -54,7 +54,7 @@ export function supports(agent, cap, version) {
54
54
  const c = getCapability(agent, cap);
55
55
  if (c === false)
56
56
  return { ok: false, reason: 'unsupported' };
57
- if (c === true)
57
+ if (c === true || c === undefined)
58
58
  return { ok: true };
59
59
  if ('file' in c)
60
60
  return { ok: true };
@@ -308,6 +308,26 @@ export declare function resolveShimSpawn(platform: NodeJS.Platform, binary: stri
308
308
  * keeping version resolution in one place instead of reimplementing it in batch.
309
309
  */
310
310
  export declare function execShimPassthrough(agent: AgentId, rawArgs: string[], cwd: string, pinnedVersion?: string): Promise<number>;
311
+ /**
312
+ * Whether a dead pane's failure should be recapped to stderr (RUSH-2185 / EXEC-23a).
313
+ *
314
+ * For headless runs only a nonzero exit is a failure worth surfacing — a
315
+ * clean 0 means the agent finished the task before we could attach.
316
+ * For interactive runs ANY exit (including 0) is a failure: an instant clean
317
+ * exit means the harness has no bare REPL and the user would see only a mute
318
+ * `[detached]` with no explanation.
319
+ */
320
+ export declare function shouldRecapDeadPane(status: number | undefined, interactive: boolean): boolean;
321
+ /**
322
+ * True only when a `display-message #{pane_dead}` tmux query explicitly returned
323
+ * "0" (pane alive). Used to distinguish "pane alive" from "query failed" in
324
+ * situations where `paneExitStatus` conservatively returns `{dead: false}` for
325
+ * both (RUSH-2185 / EXEC-23a / F3).
326
+ *
327
+ * @param code The exit code of the `tmux display-message` command.
328
+ * @param stdout Its stdout (expected to be "0" when the pane is alive).
329
+ */
330
+ export declare function isPaneKnownAliveFromQueryResult(code: number, stdout: string): boolean;
311
331
  /** Inputs that decide whether an interactive spawn is wrapped in a shared-socket tmux session. */
312
332
  export interface TmuxWrapContext {
313
333
  /** resolveInteractive() result — only interactive REPL launches are wrapped. */