@phnx-labs/agents-cli 1.20.86 → 1.20.88

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 (82) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +3 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.d.ts +0 -19
  5. package/dist/commands/doctor.js +219 -305
  6. package/dist/commands/exec.js +7 -19
  7. package/dist/commands/inspect.js +3 -5
  8. package/dist/commands/routines.js +2 -2
  9. package/dist/commands/sessions-browser.d.ts +18 -0
  10. package/dist/commands/sessions-browser.js +126 -24
  11. package/dist/commands/sessions-picker.d.ts +21 -8
  12. package/dist/commands/sessions-picker.js +83 -7
  13. package/dist/commands/sessions.d.ts +19 -0
  14. package/dist/commands/sessions.js +148 -18
  15. package/dist/commands/ssh.js +62 -4
  16. package/dist/commands/teams-picker.d.ts +2 -0
  17. package/dist/commands/teams-picker.js +2 -1
  18. package/dist/commands/teams.d.ts +4 -1
  19. package/dist/commands/teams.js +106 -70
  20. package/dist/commands/usage.d.ts +3 -2
  21. package/dist/commands/usage.js +2 -9
  22. package/dist/commands/view.js +14 -3
  23. package/dist/index.js +31 -1
  24. package/dist/lib/agents.d.ts +31 -1
  25. package/dist/lib/agents.js +55 -0
  26. package/dist/lib/claude-account-token.d.ts +12 -0
  27. package/dist/lib/claude-account-token.js +63 -0
  28. package/dist/lib/command-skills.d.ts +10 -0
  29. package/dist/lib/command-skills.js +14 -0
  30. package/dist/lib/commands.js +19 -1
  31. package/dist/lib/daemon.js +17 -2
  32. package/dist/lib/devices/doctor-findings.d.ts +167 -0
  33. package/dist/lib/devices/doctor-findings.js +893 -0
  34. package/dist/lib/devices/fleet-divergence.d.ts +22 -0
  35. package/dist/lib/devices/fleet-divergence.js +34 -10
  36. package/dist/lib/devices/fleet-inventory.d.ts +17 -6
  37. package/dist/lib/devices/fleet-inventory.js +56 -8
  38. package/dist/lib/devices/registry.d.ts +25 -0
  39. package/dist/lib/devices/registry.js +82 -1
  40. package/dist/lib/events.d.ts +8 -1
  41. package/dist/lib/events.js +13 -0
  42. package/dist/lib/exec.d.ts +14 -3
  43. package/dist/lib/exec.js +51 -9
  44. package/dist/lib/format.d.ts +7 -0
  45. package/dist/lib/format.js +11 -0
  46. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  47. package/dist/lib/project-resources.js +34 -20
  48. package/dist/lib/runner.d.ts +14 -1
  49. package/dist/lib/runner.js +37 -8
  50. package/dist/lib/sandbox.d.ts +2 -0
  51. package/dist/lib/sandbox.js +38 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  54. package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
  55. package/dist/lib/secrets/rc-hygiene.js +0 -24
  56. package/dist/lib/session/active.d.ts +6 -6
  57. package/dist/lib/session/active.js +6 -6
  58. package/dist/lib/session/db.d.ts +21 -0
  59. package/dist/lib/session/db.js +45 -4
  60. package/dist/lib/session/discover.d.ts +5 -0
  61. package/dist/lib/session/discover.js +137 -1
  62. package/dist/lib/session/parse.d.ts +2 -0
  63. package/dist/lib/session/parse.js +76 -37
  64. package/dist/lib/session/remote-list.d.ts +7 -0
  65. package/dist/lib/session/remote-list.js +8 -4
  66. package/dist/lib/session/state.js +69 -2
  67. package/dist/lib/session/sync/agents.js +0 -0
  68. package/dist/lib/session/team-filter.d.ts +22 -3
  69. package/dist/lib/session/team-filter.js +106 -17
  70. package/dist/lib/session/types.d.ts +9 -1
  71. package/dist/lib/session/types.js +1 -1
  72. package/dist/lib/signin-badge.d.ts +17 -0
  73. package/dist/lib/signin-badge.js +19 -0
  74. package/dist/lib/staleness/detectors/commands.js +14 -5
  75. package/dist/lib/staleness/types.d.ts +2 -0
  76. package/dist/lib/staleness/writers/commands.js +13 -7
  77. package/dist/lib/state.d.ts +2 -0
  78. package/dist/lib/state.js +2 -0
  79. package/dist/lib/usage.d.ts +72 -1
  80. package/dist/lib/usage.js +22 -87
  81. package/dist/lib/versions.js +30 -13
  82. package/package.json +1 -1
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
4
4
  import { supports } from './capabilities.js';
5
- import { buildCommandSkillContent, commandSkillName, readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
5
+ import { buildCommandSkillContent, commandSkillName, readSkillSourceCommandMarker, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill } from './command-skills.js';
6
6
  import { commandAppliesTo, parseCommandMetadata } from './commands.js';
7
7
  import { markdownToToml } from './convert.js';
8
8
  import { safeJoin } from './paths.js';
@@ -134,6 +134,7 @@ function skip(dest, projectRoot, result) {
134
134
  function syncProjectCommands(agent, version, projectAgentsDir, agentRoot, result, manifestPaths) {
135
135
  const cfg = AGENTS[agent];
136
136
  const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
137
+ const commandsAlsoAsSkills = shouldAlsoInstallCommandAsSkill(agent, version);
137
138
  const supportsCommands = supports(agent, 'commands', version).ok;
138
139
  if (!commandsAsSkills && !supportsCommands)
139
140
  return;
@@ -146,37 +147,50 @@ function syncProjectCommands(agent, version, projectAgentsDir, agentRoot, result
146
147
  const metadata = parseCommandMetadata(srcFile);
147
148
  if (!commandAppliesTo(agent, version, metadata).ok)
148
149
  continue;
149
- if (commandsAsSkills) {
150
+ const written = [];
151
+ if (commandsAsSkills || commandsAlsoAsSkills) {
150
152
  const sourceMarker = readSkillSourceCommandMarker(name, [path.join(projectAgentsDir, 'skills')]);
151
- if (pathExists(path.join(projectAgentsDir, 'skills', name)) && sourceMarker !== name)
152
- continue;
153
- const skillName = commandSkillName(name);
154
- const rel = path.join('skills', skillName);
155
- const destDir = path.join(agentRoot, rel);
156
- if (pathExists(destDir)) {
157
- skip(destDir, projectRoot, result);
153
+ if (pathExists(path.join(projectAgentsDir, 'skills', name)) && sourceMarker !== name) {
154
+ if (commandsAsSkills)
155
+ continue;
156
+ }
157
+ else {
158
+ const skillName = commandSkillName(name);
159
+ const rel = path.join('skills', skillName);
160
+ const destDir = path.join(agentRoot, rel);
161
+ if (pathExists(destDir)) {
162
+ skip(destDir, projectRoot, result);
163
+ }
164
+ else {
165
+ fs.mkdirSync(destDir, { recursive: true });
166
+ fs.writeFileSync(path.join(destDir, 'SKILL.md'), buildCommandSkillContent(name, srcFile), 'utf-8');
167
+ written.push(rel);
168
+ }
169
+ }
170
+ if (commandsAsSkills) {
171
+ if (written.length > 0)
172
+ record('commands', name, written, result, manifestPaths);
158
173
  continue;
159
174
  }
160
- fs.mkdirSync(destDir, { recursive: true });
161
- fs.writeFileSync(path.join(destDir, 'SKILL.md'), buildCommandSkillContent(name, srcFile), 'utf-8');
162
- record('commands', name, [rel], result, manifestPaths);
163
- continue;
164
175
  }
165
176
  const ext = cfg.format === 'toml' ? '.toml' : '.md';
166
177
  const rel = path.join(cfg.commandsSubdir, `${name}${ext}`);
167
178
  const destFile = path.join(agentRoot, rel);
168
179
  if (pathExists(destFile)) {
169
180
  skip(destFile, projectRoot, result);
170
- continue;
171
- }
172
- fs.mkdirSync(path.dirname(destFile), { recursive: true });
173
- if (cfg.format === 'toml') {
174
- fs.writeFileSync(destFile, markdownToToml(name, fs.readFileSync(srcFile, 'utf-8')), 'utf-8');
175
181
  }
176
182
  else {
177
- fs.copyFileSync(srcFile, destFile);
183
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
184
+ if (cfg.format === 'toml') {
185
+ fs.writeFileSync(destFile, markdownToToml(name, fs.readFileSync(srcFile, 'utf-8')), 'utf-8');
186
+ }
187
+ else {
188
+ fs.copyFileSync(srcFile, destFile);
189
+ }
190
+ written.push(rel);
178
191
  }
179
- record('commands', name, [rel], result, manifestPaths);
192
+ if (written.length > 0)
193
+ record('commands', name, written, result, manifestPaths);
180
194
  }
181
195
  }
182
196
  function syncProjectSkills(agent, version, projectAgentsDir, agentRoot, result, manifestPaths) {
@@ -23,6 +23,9 @@ export interface RunResult {
23
23
  meta: RunMeta;
24
24
  reportPath: string | null;
25
25
  }
26
+ /** Agents the daemon can actually run, derived from the command table above
27
+ * so the `--agent` help and any validation can never drift from it. */
28
+ export declare const ROUTINE_AGENT_IDS: readonly string[];
26
29
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
27
30
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
28
31
  export declare function archiveRoutineTranscripts(meta: Pick<RunMeta, 'jobName' | 'runId' | 'agent'>, runDir: string, overlayHome?: string): void;
@@ -63,7 +66,7 @@ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow
63
66
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
64
67
  * with `agents run`.
65
68
  */
66
- export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string): Record<string, string>;
69
+ export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string, overlayHome?: string): Record<string, string>;
67
70
  export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
68
71
  /**
69
72
  * Optional lifecycle callbacks for a detached routine run. The daemon passes an
@@ -80,5 +83,15 @@ export interface RoutineHooks {
80
83
  export declare function executeJobDetached(config: JobConfig, hooks?: RoutineHooks): Promise<RunMeta>;
81
84
  /** Extract the final assistant message from a stream-JSON log file as a markdown report. */
82
85
  export declare function extractReport(stdoutPath: string, agentType: AgentId): string | null;
86
+ /** Derive the final status of a detached run by reading the agent's stream-json
87
+ * tail. Detached children fire-and-forget, so we never see their exit code
88
+ * directly — but Claude's stream-json terminates with a `type: result` line
89
+ * that carries `is_error`. If we find it, the run completed cleanly (modulo
90
+ * agent-reported error). If not, the process likely died mid-stream and the
91
+ * caller should treat the run as failed. */
92
+ export declare function inferFinalStatusFromLog(stdoutPath: string, agent: AgentId): {
93
+ status: 'completed' | 'failed';
94
+ exitCode: number;
95
+ } | null;
83
96
  /** Scan all runs marked "running" and finalize any whose process has exited. */
84
97
  export declare function monitorRunningJobs(): void;
@@ -32,17 +32,23 @@ import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js
32
32
  import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
33
33
  import { readAuthHealth, isDeadVerdict } from './auth-health.js';
34
34
  import { machineId } from './machine-id.js';
35
+ import { isSelfUpdatingAgent } from './agents.js';
35
36
  /** CLI command templates per agent, with {prompt} as a placeholder. */
36
37
  const AGENT_COMMANDS = {
37
38
  claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
38
39
  codex: ['codex', 'exec', '--sandbox', 'workspace-write', '{prompt}', '--json'],
39
40
  gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
41
+ cursor: ['cursor-agent', '-p', '{prompt}', '--output-format', 'stream-json'],
40
42
  kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
41
43
  droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
42
44
  };
45
+ /** Agents the daemon can actually run, derived from the command table above
46
+ * so the `--agent` help and any validation can never drift from it. */
47
+ export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
43
48
  const ROUTINE_TRANSCRIPT_SPECS = {
44
49
  claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
45
50
  codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
51
+ cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
46
52
  };
47
53
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
48
54
  export function buildJobCommand(config, resolvedPrompt) {
@@ -133,6 +139,20 @@ export function buildJobCommand(config, resolvedPrompt) {
133
139
  }
134
140
  appendModelAndReasoning(cmd, config);
135
141
  }
142
+ if (config.agent === 'cursor') {
143
+ // cursor-agent supports --plan, but the capability registry has not been
144
+ // upgraded yet. RUSH-2101 tracks adding that read-only mode after PR #1721.
145
+ const cursorMode = resolveHeadlessMode('cursor', mode, false, `routine ${config.name}`);
146
+ if (cursorMode === 'skip') {
147
+ cmd.push('-f');
148
+ }
149
+ else {
150
+ // The configured cwd is the user's workspace trust decision. --trust is
151
+ // narrower than --yolo/-f because it does not bypass tool permissions.
152
+ cmd.push('--trust');
153
+ }
154
+ appendModelAndReasoning(cmd, config);
155
+ }
136
156
  if (config.agent === 'kimi') {
137
157
  // kimi daemon jobs always run headless via `--prompt`, which cannot be
138
158
  // combined with any startup-mode flag (--plan/--auto/--yolo all abort with
@@ -385,7 +405,7 @@ export function dispatchesViaAgentsRun(config) {
385
405
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
386
406
  * with `agents run`.
387
407
  */
388
- export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
408
+ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayHome) {
389
409
  const execEnv = buildExecEnv({
390
410
  agent,
391
411
  version,
@@ -415,6 +435,11 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
415
435
  // provisioned box does. Drop it here so a routine always uses the login of the
416
436
  // machine it runs on.
417
437
  delete out.CLAUDE_CODE_OAUTH_TOKEN;
438
+ if (agent === 'cursor' && overlayHome) {
439
+ // prepareJobHome links this host's Cursor auth file here. Pin XDG_CONFIG_HOME
440
+ // to the overlay so an ambient value cannot bypass the routine sandbox.
441
+ out.XDG_CONFIG_HOME = path.join(overlayHome, '.config');
442
+ }
418
443
  if (timezone)
419
444
  out.TZ = timezone;
420
445
  return out;
@@ -621,16 +646,20 @@ export async function executeJob(config, deps) {
621
646
  const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
622
647
  // Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
623
648
  if (config.loop) {
624
- const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone);
649
+ const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone, overlayHome);
625
650
  const execOptions = {
626
651
  agent: effectiveAgent,
627
- version: primaryVersion,
652
+ // Routine-supported self-updating CLIs (Cursor/Droid) use one global
653
+ // binary; a versioned shim would point at a nonexistent isolated install.
654
+ version: isSelfUpdatingAgent(effectiveAgent) ? undefined : primaryVersion,
628
655
  prompt: resolvedPrompt,
629
656
  mode: normalizeMode(config.mode),
630
657
  effort: config.effort,
631
658
  env: spawnEnv,
632
659
  json: true,
633
660
  headless: true,
661
+ modeWarningContext: `routine ${config.name}`,
662
+ modeWarningState: {},
634
663
  ...(config.config?.model ? { model: config.config.model } : {}),
635
664
  ...(config.allow?.dirs ? {
636
665
  addDirs: config.allow.dirs
@@ -681,7 +710,7 @@ export async function executeJob(config, deps) {
681
710
  e.TZ = config.timezone;
682
711
  return e;
683
712
  })()
684
- : buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone);
713
+ : buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone, overlayHome);
685
714
  // Remaining timeout budget shared across failover attempts.
686
715
  const elapsed = Date.now() - Date.parse(meta.startedAt);
687
716
  const remaining = Math.max(1_000, timeoutMs - (Number.isFinite(elapsed) ? elapsed : 0));
@@ -1067,7 +1096,7 @@ export async function executeJobDetached(config, hooks) {
1067
1096
  return e;
1068
1097
  })()
1069
1098
  // Non-command path only: config.agent is always set here (command/workflow branch earlier).
1070
- : buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone);
1099
+ : buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
1071
1100
  const effectiveAgent = config.workflow ? 'claude' : config.agent;
1072
1101
  const meta = {
1073
1102
  jobName: config.name,
@@ -1262,7 +1291,7 @@ export function extractReport(stdoutPath, agentType) {
1262
1291
  for (const line of lines) {
1263
1292
  try {
1264
1293
  const parsed = JSON.parse(line);
1265
- if (agentType === 'claude') {
1294
+ if (agentType === 'claude' || agentType === 'cursor') {
1266
1295
  if (parsed.type === 'assistant' && parsed.message?.content) {
1267
1296
  for (const block of parsed.message.content) {
1268
1297
  if (block.type === 'text' && block.text) {
@@ -1298,7 +1327,7 @@ export function extractReport(stdoutPath, agentType) {
1298
1327
  * that carries `is_error`. If we find it, the run completed cleanly (modulo
1299
1328
  * agent-reported error). If not, the process likely died mid-stream and the
1300
1329
  * caller should treat the run as failed. */
1301
- function inferFinalStatusFromLog(stdoutPath, agent) {
1330
+ export function inferFinalStatusFromLog(stdoutPath, agent) {
1302
1331
  if (!fs.existsSync(stdoutPath))
1303
1332
  return null;
1304
1333
  try {
@@ -1309,7 +1338,7 @@ function inferFinalStatusFromLog(stdoutPath, agent) {
1309
1338
  for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < 20; i--, scanned++) {
1310
1339
  try {
1311
1340
  const parsed = JSON.parse(lines[i]);
1312
- if (agent === 'claude' && parsed.type === 'result') {
1341
+ if ((agent === 'claude' || agent === 'cursor') && parsed.type === 'result') {
1313
1342
  return parsed.is_error
1314
1343
  ? { status: 'failed', exitCode: 1 }
1315
1344
  : { status: 'completed', exitCode: 0 };
@@ -21,6 +21,8 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
21
21
  export declare function getJobHomePath(name: string): string;
22
22
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
23
23
  export declare function prepareJobHome(config: JobConfig): string;
24
+ /** Link this host's Cursor login and CLI config into the disposable overlay. */
25
+ export declare function generateCursorConfig(overlayHome: string): void;
24
26
  /** Remove a job's overlay HOME directory entirely. */
25
27
  export declare function cleanJobHome(name: string): void;
26
28
  /** Symlink allowed directories into the overlay HOME, skipping paths outside the real HOME. */
@@ -100,11 +100,49 @@ export function prepareJobHome(config) {
100
100
  else if (config.agent === 'gemini') {
101
101
  generateGeminiConfig(overlayHome, config);
102
102
  }
103
+ else if (config.agent === 'cursor') {
104
+ generateCursorConfig(overlayHome);
105
+ }
103
106
  if (config.allow?.dirs) {
104
107
  symlinkAllowedDirs(overlayHome, config.allow.dirs);
105
108
  }
106
109
  return overlayHome;
107
110
  }
111
+ /** Link this host's Cursor login and CLI config into the disposable overlay. */
112
+ export function generateCursorConfig(overlayHome) {
113
+ const realConfigHome = process.env.XDG_CONFIG_HOME || path.join(resolveRealHome(), '.config');
114
+ const realAuth = path.join(realConfigHome, 'cursor', 'auth.json');
115
+ if (!fs.existsSync(realAuth))
116
+ return;
117
+ const overlayCursorDir = path.join(overlayHome, '.config', 'cursor');
118
+ fs.mkdirSync(overlayCursorDir, { recursive: true });
119
+ const overlayAuth = path.join(overlayCursorDir, 'auth.json');
120
+ if (process.platform === 'win32') {
121
+ // File symlinks need Developer Mode on Windows. A same-volume hard link
122
+ // shares the credential inode without copying its contents to another host.
123
+ try {
124
+ fs.linkSync(realAuth, overlayAuth);
125
+ }
126
+ catch { /* cross-volume or link creation refused: Cursor will fail auth loudly */ }
127
+ }
128
+ else {
129
+ fs.symlinkSync(realAuth, overlayAuth);
130
+ }
131
+ // Setting XDG_CONFIG_HOME makes Cursor look for cli-config.json beside auth.json.
132
+ // This file carries account identity (authId, displayName, email, userId) as
133
+ // well as preferences, so it must remain linked rather than copied.
134
+ const realCliConfig = path.join(resolveRealHome(), '.cursor', 'cli-config.json');
135
+ if (fs.existsSync(realCliConfig)) {
136
+ const overlayCliConfig = path.join(overlayCursorDir, 'cli-config.json');
137
+ try {
138
+ if (process.platform === 'win32')
139
+ fs.linkSync(realCliConfig, overlayCliConfig);
140
+ else
141
+ fs.symlinkSync(realCliConfig, overlayCliConfig);
142
+ }
143
+ catch { /* cross-volume or link creation refused: Cursor will fail auth loudly */ }
144
+ }
145
+ }
108
146
  /** Remove a job's overlay HOME directory entirely. */
109
147
  export function cleanJobHome(name) {
110
148
  const overlayHome = getJobHomePath(name);
@@ -47,9 +47,3 @@ export declare function scanRcExports(basename: string, content: string): RcSecr
47
47
  * unreadable files are skipped silently — an advisory, not a hard requirement.
48
48
  */
49
49
  export declare function scanUserRcFiles(homeDir?: string): RcSecretFinding[];
50
- /**
51
- * Build the advisory lines for `agents doctor` from a set of findings. Returns
52
- * `[]` when there is nothing to report. The first line is the headline; the
53
- * rest are indented detail. Names only — never values.
54
- */
55
- export declare function rcSecretWarningLines(findings: RcSecretFinding[]): string[];
@@ -128,27 +128,3 @@ export function scanUserRcFiles(homeDir = os.homedir()) {
128
128
  }
129
129
  return out;
130
130
  }
131
- /**
132
- * Build the advisory lines for `agents doctor` from a set of findings. Returns
133
- * `[]` when there is nothing to report. The first line is the headline; the
134
- * rest are indented detail. Names only — never values.
135
- */
136
- export function rcSecretWarningLines(findings) {
137
- if (findings.length === 0)
138
- return [];
139
- const lines = [];
140
- const master = findings.filter((f) => f.isMasterPassphrase);
141
- const others = findings.filter((f) => !f.isMasterPassphrase);
142
- const total = findings.length;
143
- lines.push(`${total} credential-shaped export${total === 1 ? '' : 's'} found in shell rc files — ` +
144
- `readable from /proc/<pid>/environ by any same-user process.`);
145
- for (const f of master) {
146
- lines.push(`${f.file}:${f.line} ${f.name} — the file-store master key. Move it off-env to ` +
147
- `~/.agents/.secrets-key/passphrase (chmod 600) and delete the export.`);
148
- }
149
- for (const f of others) {
150
- lines.push(`${f.file}:${f.line} ${f.name} — move to \`agents secrets\` and inject via \`agents secrets exec\`.`);
151
- }
152
- lines.push('Rule: no credentials in env vars or shell config. Use the keychain-backed store.');
153
- return lines;
154
- }
@@ -298,7 +298,7 @@ export declare function sessionFileTimes(sessionFile: string | undefined): {
298
298
  export declare function lifecycleStatus(pidAlive: boolean, mtimeMs: number | undefined, nowMs?: number): ActiveStatus | undefined;
299
299
  /**
300
300
  * The ONE place a fallback status is decided when no rich transcript state is
301
- * available — an opaque kind we cannot parse (cursor, openclaw), or a transcript
301
+ * available — an opaque kind we cannot parse (openclaw), or a transcript
302
302
  * whose parse/tail was empty or unreadable. Honest by construction: computed from
303
303
  * PID + mtime, never a fabricated `idle`.
304
304
  *
@@ -316,13 +316,13 @@ export declare function resolveFallbackStatus(sessionFile: string | undefined, p
316
316
  * Locate the live transcript for an agent process. Claude files are keyed by cwd
317
317
  * (+ optional session uuid), so they resolve straight off disk. Every OTHER
318
318
  * tracked harness — Codex (date-partitioned), plus grok / droid / rush / gemini /
319
- * kimi / hermes / opencode / antigravity (per-session dirs, SQLite, single-JSON)
319
+ * kimi / hermes / opencode / antigravity / cursor (per-session dirs, SQLite, single-JSON)
320
320
  * — is resolved through the session index by cwd: the newest indexed transcript
321
321
  * for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never borrows a
322
322
  * weeks-old transcript. This is what lets a live NON-claude/codex agent get a real
323
323
  * status instead of falling through to `unknown` (the file feeds
324
- * {@link computeLiveSignals}). An opaque kind we don't track (cursor) still yields
325
- * undefined here and degrades honestly to a live `running`.
324
+ * {@link computeLiveSignals}). An opaque kind we don't track still yields undefined
325
+ * here and degrades honestly to a live `running`.
326
326
  */
327
327
  export declare function findSessionFileForKind(kind: string, cwd?: string, sessionId?: string): string | undefined;
328
328
  /** Live per-session signals derived from one transcript-tail read. */
@@ -339,10 +339,10 @@ interface LiveSignals {
339
339
  * Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
340
340
  * the hot path, and the only two that also yield throughput (their raw lines
341
341
  * carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
342
- * rush, gemini, kimi, hermes, opencode, antigravity) is parsed with its own
342
+ * rush, gemini, kimi, hermes, opencode, antigravity, cursor) is parsed with its own
343
343
  * parser and run through the SAME {@link inferSessionState}, so a live
344
344
  * non-claude/codex agent gets a real working/waiting/idle instead of the blanket
345
- * `unknown` it used to fall through to. An opaque/untracked kind (cursor) or an
345
+ * `unknown` it used to fall through to. An opaque/untracked kind or an
346
346
  * unreadable/empty transcript yields an empty signal set, and the caller's
347
347
  * {@link resolveFallbackStatus} reports the honest live floor (`running`).
348
348
  */
@@ -358,7 +358,7 @@ export function lifecycleStatus(pidAlive, mtimeMs, nowMs = Date.now()) {
358
358
  }
359
359
  /**
360
360
  * The ONE place a fallback status is decided when no rich transcript state is
361
- * available — an opaque kind we cannot parse (cursor, openclaw), or a transcript
361
+ * available — an opaque kind we cannot parse (openclaw), or a transcript
362
362
  * whose parse/tail was empty or unreadable. Honest by construction: computed from
363
363
  * PID + mtime, never a fabricated `idle`.
364
364
  *
@@ -379,13 +379,13 @@ export function resolveFallbackStatus(sessionFile, pidAlive, nowMs = Date.now())
379
379
  * Locate the live transcript for an agent process. Claude files are keyed by cwd
380
380
  * (+ optional session uuid), so they resolve straight off disk. Every OTHER
381
381
  * tracked harness — Codex (date-partitioned), plus grok / droid / rush / gemini /
382
- * kimi / hermes / opencode / antigravity (per-session dirs, SQLite, single-JSON)
382
+ * kimi / hermes / opencode / antigravity / cursor (per-session dirs, SQLite, single-JSON)
383
383
  * — is resolved through the session index by cwd: the newest indexed transcript
384
384
  * for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never borrows a
385
385
  * weeks-old transcript. This is what lets a live NON-claude/codex agent get a real
386
386
  * status instead of falling through to `unknown` (the file feeds
387
- * {@link computeLiveSignals}). An opaque kind we don't track (cursor) still yields
388
- * undefined here and degrades honestly to a live `running`.
387
+ * {@link computeLiveSignals}). An opaque kind we don't track still yields undefined
388
+ * here and degrades honestly to a live `running`.
389
389
  */
390
390
  export function findSessionFileForKind(kind, cwd, sessionId) {
391
391
  if (!cwd)
@@ -432,10 +432,10 @@ function parseTailEventsForKind(agent, sessionFile) {
432
432
  * Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
433
433
  * the hot path, and the only two that also yield throughput (their raw lines
434
434
  * carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
435
- * rush, gemini, kimi, hermes, opencode, antigravity) is parsed with its own
435
+ * rush, gemini, kimi, hermes, opencode, antigravity, cursor) is parsed with its own
436
436
  * parser and run through the SAME {@link inferSessionState}, so a live
437
437
  * non-claude/codex agent gets a real working/waiting/idle instead of the blanket
438
- * `unknown` it used to fall through to. An opaque/untracked kind (cursor) or an
438
+ * `unknown` it used to fall through to. An opaque/untracked kind or an
439
439
  * unreadable/empty transcript yields an empty signal set, and the caller's
440
440
  * {@link resolveFallbackStatus} reports the honest live floor (`running`).
441
441
  */
@@ -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;
@@ -16,8 +16,10 @@ import { machineForSessionFile } from './origin-machine.js';
16
16
  import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
17
17
  const SESSIONS_DIR = getSessionsDir();
18
18
  const DB_PATH = getSessionsDbPath();
19
- /** Current schema version; bumped when migrations are added. */
20
- const SCHEMA_VERSION = 20;
19
+ /** Current schema version; bumped when migrations are added. Exported so tests
20
+ * assert against the constant instead of hardcoding a number that every bump
21
+ * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
22
+ export const SCHEMA_VERSION = 21;
21
23
  /**
22
24
  * Canonicalize a file path for use as a scan_ledger key. The same physical
23
25
  * session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
@@ -74,6 +76,7 @@ CREATE TABLE IF NOT EXISTS sessions (
74
76
  pr_number INTEGER,
75
77
  worktree_slug TEXT,
76
78
  ticket_id TEXT,
79
+ spawned_team TEXT,
77
80
  plan TEXT,
78
81
  machine TEXT,
79
82
  todos TEXT,
@@ -383,6 +386,19 @@ function migrateSchema(db, fromVersion) {
383
386
  db.exec(`ALTER TABLE sessions ADD COLUMN model TEXT`);
384
387
  db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
385
388
  }
389
+ if (fromVersion < 21) {
390
+ // v20 → v21: persist the team a session SPAWNED (`agents teams create/add`).
391
+ // The value was already derived at scan time (discover.ts detectSpawnedTeam)
392
+ // and set on SessionMeta, but had no column — so it was dropped at the write
393
+ // and no consumer ever saw it. Wipe BOTH ledgers, not just scan_ledger: with
394
+ // dir_ledger intact, collectChangedFilesInLeafDirs treats every non-live-root
395
+ // dir as unchanged and derives its hot set from the scan stamps just deleted,
396
+ // so archived dirs would never be re-parsed and would stay NULL forever.
397
+ const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
398
+ if (!cols.some(c => c.name === 'spawned_team'))
399
+ db.exec(`ALTER TABLE sessions ADD COLUMN spawned_team TEXT`);
400
+ db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
401
+ }
386
402
  }
387
403
  /** Open (or return the cached) sessions database, applying migrations as needed. */
388
404
  export function getDB() {
@@ -744,7 +760,7 @@ const upsertSessionStmt = (db) => db.prepare(`
744
760
  project, cwd, git_branch, topic, label, message_count, token_count,
745
761
  output_tokens, cost_usd, duration_ms, model,
746
762
  file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
747
- pr_url, pr_number, worktree_slug, ticket_id, plan, todos,
763
+ pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
748
764
  recent_directories_touched, linear_project, linear_project_url, machine,
749
765
  actor, initiated_by
750
766
  ) VALUES (
@@ -753,7 +769,7 @@ const upsertSessionStmt = (db) => db.prepare(`
753
769
  @project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
754
770
  @output_tokens, @cost_usd, @duration_ms, @model,
755
771
  @file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
756
- @pr_url, @pr_number, @worktree_slug, @ticket_id, @plan, @todos,
772
+ @pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
757
773
  @recent_directories_touched, @linear_project, @linear_project_url, @machine,
758
774
  @actor, @initiated_by
759
775
  )
@@ -795,6 +811,7 @@ const upsertSessionStmt = (db) => db.prepare(`
795
811
  pr_number = excluded.pr_number,
796
812
  worktree_slug = excluded.worktree_slug,
797
813
  ticket_id = excluded.ticket_id,
814
+ spawned_team = excluded.spawned_team,
798
815
  plan = excluded.plan,
799
816
  todos = excluded.todos,
800
817
  recent_directories_touched = excluded.recent_directories_touched,
@@ -911,6 +928,7 @@ export function upsertSession(meta, content, scan) {
911
928
  pr_number: meta.prNumber ?? null,
912
929
  worktree_slug: meta.worktreeSlug ?? null,
913
930
  ticket_id: meta.ticketId ?? null,
931
+ spawned_team: meta.spawnedTeam ?? null,
914
932
  plan: meta.plan ?? null,
915
933
  todos: meta.todos ? JSON.stringify(meta.todos) : null,
916
934
  recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
@@ -1031,6 +1049,7 @@ export function upsertSessionsBatch(entries) {
1031
1049
  pr_number: meta.prNumber ?? null,
1032
1050
  worktree_slug: meta.worktreeSlug ?? null,
1033
1051
  ticket_id: meta.ticketId ?? null,
1052
+ spawned_team: meta.spawnedTeam ?? null,
1034
1053
  plan: meta.plan ?? null,
1035
1054
  todos: meta.todos ? JSON.stringify(meta.todos) : null,
1036
1055
  recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
@@ -1219,6 +1238,7 @@ function rowToMeta(row) {
1219
1238
  prNumber: row.pr_number ?? undefined,
1220
1239
  worktreeSlug: row.worktree_slug ?? undefined,
1221
1240
  ticketId: row.ticket_id ?? undefined,
1241
+ spawnedTeam: row.spawned_team ?? undefined,
1222
1242
  plan: row.plan ?? undefined,
1223
1243
  todos: parseJsonColumn(row.todos),
1224
1244
  recentDirectoriesTouched: parseJsonColumn(row.recent_directories_touched),
@@ -1483,6 +1503,27 @@ export function queryUsageRollup(options) {
1483
1503
  `;
1484
1504
  return db.prepare(sql).all(...params);
1485
1505
  }
1506
+ /**
1507
+ * Map every team name to the session that ran `agents teams create/add` for it.
1508
+ *
1509
+ * One scan over the rows that carry a `spawned_team`, rather than a query per
1510
+ * team — `agents teams list` needs the whole map at once, and the column has no
1511
+ * index. When two sessions spawned the same team name (a team re-created after a
1512
+ * disband), the most recent wins, which is the one whose work the name refers to.
1513
+ */
1514
+ export function teamSpawners() {
1515
+ const db = getDB();
1516
+ const rows = db
1517
+ .prepare(`SELECT spawned_team, id, short_id, actor FROM sessions
1518
+ WHERE spawned_team IS NOT NULL AND spawned_team != ''
1519
+ ORDER BY timestamp ASC`)
1520
+ .all();
1521
+ const out = new Map();
1522
+ for (const r of rows) {
1523
+ out.set(r.spawned_team, { sessionId: r.id, shortId: r.short_id, actor: r.actor ?? undefined });
1524
+ }
1525
+ return out;
1526
+ }
1486
1527
  /**
1487
1528
  * Return the N most expensive sessions (cost_usd DESC, NULLs excluded),
1488
1529
  * honoring the same filter shape as querySessions. Drops rows whose JSONL
@@ -622,6 +622,11 @@ export declare function __codexScanBranchCountsForTest(): {
622
622
  };
623
623
  /** Test seam: reset the Codex parse-branch counters to observe a scan from a clean slate. */
624
624
  export declare function __resetCodexScanBranchCountsForTest(): void;
625
+ /** Parse one Cursor transcript and enrich it with the matching chat meta.json. */
626
+ export declare function readCursorMeta(filePath: string, currentVersion?: string): {
627
+ meta: SessionMeta;
628
+ content: string;
629
+ } | null;
625
630
  /** Parse a single Kimi session state.json file to extract session metadata. */
626
631
  export declare function readKimiMeta(filePath: string, priorRow?: {
627
632
  parserState: string | null;