@phnx-labs/agents-cli 1.21.3 → 1.22.1

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 (90) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/doctor.js +34 -2
  6. package/dist/commands/exec.d.ts +27 -0
  7. package/dist/commands/exec.js +123 -6
  8. package/dist/commands/models.js +36 -1
  9. package/dist/commands/projects.js +120 -62
  10. package/dist/commands/sessions-backfill.d.ts +32 -0
  11. package/dist/commands/sessions-backfill.js +186 -0
  12. package/dist/commands/sessions.d.ts +17 -1
  13. package/dist/commands/sessions.js +317 -18
  14. package/dist/commands/teams.js +1 -1
  15. package/dist/commands/worktree.d.ts +3 -3
  16. package/dist/commands/worktree.js +35 -4
  17. package/dist/lib/app-bundle-install.d.ts +17 -0
  18. package/dist/lib/app-bundle-install.js +94 -0
  19. package/dist/lib/daemon.d.ts +5 -1
  20. package/dist/lib/daemon.js +63 -14
  21. package/dist/lib/devices/doctor-findings.js +12 -4
  22. package/dist/lib/devices/doctor-overview-cache.d.ts +45 -0
  23. package/dist/lib/devices/doctor-overview-cache.js +168 -0
  24. package/dist/lib/devices/fleet.js +7 -2
  25. package/dist/lib/devices/resolve-target.d.ts +6 -0
  26. package/dist/lib/devices/resolve-target.js +9 -3
  27. package/dist/lib/devices/self-host.d.ts +9 -0
  28. package/dist/lib/devices/self-host.js +61 -0
  29. package/dist/lib/exec.js +39 -8
  30. package/dist/lib/hosts/dispatch.d.ts +12 -0
  31. package/dist/lib/hosts/dispatch.js +23 -6
  32. package/dist/lib/hosts/passthrough.js +8 -6
  33. package/dist/lib/hosts/reconnect.d.ts +38 -0
  34. package/dist/lib/hosts/reconnect.js +85 -4
  35. package/dist/lib/hosts/run-target.js +14 -2
  36. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  38. package/dist/lib/menubar/install-menubar.js +27 -23
  39. package/dist/lib/model-tiers.d.ts +54 -0
  40. package/dist/lib/model-tiers.js +229 -0
  41. package/dist/lib/models.d.ts +3 -0
  42. package/dist/lib/models.js +44 -7
  43. package/dist/lib/pricing/prices.json +16 -1
  44. package/dist/lib/project-focus.d.ts +42 -0
  45. package/dist/lib/project-focus.js +80 -0
  46. package/dist/lib/project-schedule.d.ts +75 -0
  47. package/dist/lib/project-schedule.js +110 -0
  48. package/dist/lib/project-status.d.ts +7 -0
  49. package/dist/lib/project-status.js +9 -0
  50. package/dist/lib/projects.d.ts +11 -2
  51. package/dist/lib/projects.js +57 -9
  52. package/dist/lib/redact.d.ts +2 -0
  53. package/dist/lib/redact.js +22 -0
  54. package/dist/lib/remote-agents-json.d.ts +2 -0
  55. package/dist/lib/remote-agents-json.js +3 -3
  56. package/dist/lib/rotate.d.ts +84 -1
  57. package/dist/lib/rotate.js +155 -5
  58. package/dist/lib/runner.d.ts +4 -2
  59. package/dist/lib/runner.js +13 -4
  60. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  61. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  62. package/dist/lib/secrets/install-helper.js +28 -31
  63. package/dist/lib/session/bash-command.js +60 -9
  64. package/dist/lib/session/db.d.ts +7 -1
  65. package/dist/lib/session/db.js +301 -32
  66. package/dist/lib/session/discover.d.ts +40 -7
  67. package/dist/lib/session/discover.js +144 -83
  68. package/dist/lib/session/parse.d.ts +8 -1
  69. package/dist/lib/session/parse.js +83 -32
  70. package/dist/lib/session/remote-list.d.ts +71 -0
  71. package/dist/lib/session/remote-list.js +410 -2
  72. package/dist/lib/session/shell-programs.d.ts +15 -0
  73. package/dist/lib/session/shell-programs.js +359 -0
  74. package/dist/lib/session/tool-calls.d.ts +88 -0
  75. package/dist/lib/session/tool-calls.js +612 -0
  76. package/dist/lib/session/tool-index.d.ts +100 -0
  77. package/dist/lib/session/tool-index.js +773 -0
  78. package/dist/lib/session/tool-store.d.ts +15 -0
  79. package/dist/lib/session/tool-store.js +198 -0
  80. package/dist/lib/session/types.d.ts +7 -0
  81. package/dist/lib/state.d.ts +10 -1
  82. package/dist/lib/state.js +11 -2
  83. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  84. package/dist/lib/teams/remoteWorktree.js +3 -4
  85. package/dist/lib/teams/worktree.d.ts +11 -1
  86. package/dist/lib/teams/worktree.js +42 -4
  87. package/dist/lib/types.d.ts +17 -0
  88. package/dist/lib/types.js +17 -0
  89. package/dist/lib/versions.js +69 -22
  90. package/package.json +3 -1
@@ -18,6 +18,7 @@ import { createHash } from 'crypto';
18
18
  import * as fs from 'fs';
19
19
  import * as os from 'os';
20
20
  import * as path from 'path';
21
+ import { copyAppBundle, withInstallLock } from '../app-bundle-install.js';
21
22
  const APP_BUNDLE_NAME = 'Agents CLI.app';
22
23
  const INSTALL_DIR_NAME = 'agents-cli';
23
24
  let installRootOverride = null;
@@ -107,19 +108,6 @@ function spctlAssess(appPath) {
107
108
  });
108
109
  return { ok: r.status === 0, output: (r.stderr || r.stdout || '').toString().trim() };
109
110
  }
110
- function copyAppBundle(src, dest) {
111
- fs.mkdirSync(path.dirname(dest), { recursive: true });
112
- if (fs.existsSync(dest))
113
- fs.rmSync(dest, { recursive: true, force: true });
114
- // `cp -R` preserves the bundle's signature, symlinks, and resource forks.
115
- // `fs.cpSync({recursive: true})` works on simple trees but has historically
116
- // mishandled extended attributes on `.app` bundles, breaking codesign.
117
- const r = spawnSync('cp', ['-R', src, dest], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
118
- if (r.status !== 0) {
119
- const msg = (r.stderr || r.stdout || '').toString().trim();
120
- throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
121
- }
122
- }
123
111
  /**
124
112
  * Idempotent install. Copies the bundled `.app` to the stable user path. Skips
125
113
  * if the destination already exists, `codesign --verify` passes, AND the
@@ -135,25 +123,34 @@ function copyAppBundle(src, dest) {
135
123
  export function ensureKeychainHelperInstalled(opts = {}) {
136
124
  assertDarwin();
137
125
  const dest = installedAppPath();
138
- if (!opts.forceReinstall && fs.existsSync(dest)) {
139
- const { ok } = codesignVerify(dest);
140
- if (ok && !installedHelperIsStale())
126
+ const upToDate = () => fs.existsSync(dest) && codesignVerify(dest).ok && !installedHelperIsStale();
127
+ // Fast path: already installed, valid, and current — no lock, no copy.
128
+ if (!opts.forceReinstall && upToDate())
129
+ return;
130
+ // Serialize the install so concurrent `agents` invocations don't race the
131
+ // atomic swap (and don't each run a redundant `cp`). The re-check inside the
132
+ // lock means a burst of callers copies once, not N times — the stampede that
133
+ // transiently corrupted the bundle and tripped the "damaged" dialog.
134
+ withInstallLock(dest, (heartbeat) => {
135
+ if (!opts.forceReinstall && upToDate())
141
136
  return;
142
- }
143
- const src = sourceAppPath();
144
- copyAppBundle(src, dest);
145
- const verify = codesignVerify(dest);
146
- if (!verify.ok) {
147
- throw new Error(`Installed helper failed codesign verification at ${dest}.\n${verify.output}\n` +
148
- 'The bundle may be corrupted. Try `agents helper install` to reinstall, or reinstall agents-cli.');
149
- }
150
- const assess = spctlAssess(dest);
151
- if (!assess.ok) {
152
- // Warn, do not fail. Gatekeeper ticket lookup needs network; offline
153
- // installs and CI runners commonly fail this check. The ACL semantics
154
- // we care about depend on codesign, not spctl.
155
- process.stderr.write(`agents-cli: notarization check (spctl) did not pass for ${dest}: ${assess.output}\n`);
156
- }
137
+ const src = sourceAppPath();
138
+ copyAppBundle(src, dest);
139
+ heartbeat(); // cp -R done; keep the lock fresh across the codesign/spctl spawns
140
+ const verify = codesignVerify(dest);
141
+ if (!verify.ok) {
142
+ throw new Error(`Installed helper failed codesign verification at ${dest}.\n${verify.output}\n` +
143
+ 'The bundle may be corrupted. Try `agents helper install` to reinstall, or reinstall agents-cli.');
144
+ }
145
+ heartbeat(); // codesign done; spctl does a network Gatekeeper lookup — refresh again
146
+ const assess = spctlAssess(dest);
147
+ if (!assess.ok) {
148
+ // Warn, do not fail. Gatekeeper ticket lookup needs network; offline
149
+ // installs and CI runners commonly fail this check. The ACL semantics
150
+ // we care about depend on codesign, not spctl.
151
+ process.stderr.write(`agents-cli: notarization check (spctl) did not pass for ${dest}: ${assess.output}\n`);
152
+ }
153
+ });
157
154
  }
158
155
  /**
159
156
  * Return the absolute path to the helper executable. If the installed bundle
@@ -64,6 +64,7 @@ const TOOL_REGISTRY = {
64
64
  mv: { category: 'shell', signal: 'low', action: 'moving files' },
65
65
  cp: { category: 'shell', signal: 'low', action: 'copying files' },
66
66
  mkdir: { category: 'shell', signal: 'low', action: 'making directories' },
67
+ rmdir: { category: 'shell', signal: 'low', action: 'removing directories' },
67
68
  touch: { category: 'shell', signal: 'low', action: 'touching files' },
68
69
  echo: { category: 'shell', signal: 'low', action: 'echoing' },
69
70
  printf: { category: 'shell', signal: 'low', action: 'printing' },
@@ -115,6 +116,13 @@ const VALUE_FLAGS = {
115
116
  kubectl: new Set(['-n', '--namespace', '--kubeconfig', '--context', '--cluster', '--user', '-s', '--server', '--as', '--token', '--cache-dir', '--request-timeout']),
116
117
  rush: new Set(),
117
118
  openclaw: new Set(),
119
+ // The repo's own toolchain — heavy in real transcripts (`agents` alone was the
120
+ // top unrecognized token). Two-level so they bucket by subcommand
121
+ // (`agents sessions`, `linear list`) instead of one flat `other` pile (#1830).
122
+ // `ag` is deliberately NOT here — it is the silver searcher in TOOL_REGISTRY,
123
+ // not the agents alias, in this classifier's world.
124
+ agents: new Set(['-H', '--host', '--device']),
125
+ linear: new Set(),
118
126
  };
119
127
  /** Tools whose bucket key includes the second token (subcommand). */
120
128
  const TWO_LEVEL_TOOLS = new Set(Object.keys(VALUE_FLAGS));
@@ -141,8 +149,8 @@ export function unwrapCommand(cmd) {
141
149
  const prefix = s.match(/^(?:sudo|time)(?:\s+(?:-[uUgGhpCrtDR]\s+\S+|-\S+))*\s+(.+)$/);
142
150
  if (prefix)
143
151
  return unwrapCommand(prefix[1]);
144
- // cd foo && command
145
- const cd = s.match(/^cd\s+\S+\s*&&\s*(.+)$/);
152
+ // cd foo && command (also `cd foo; command` and newline-separated `cd foo\ncommand`)
153
+ const cd = s.match(/^cd\s+\S+\s*(?:&&|;|\n)\s*([\s\S]+)$/);
146
154
  if (cd)
147
155
  return unwrapCommand(cd[1]);
148
156
  // npx / bunx
@@ -191,7 +199,11 @@ function splitOnOperators(cmd) {
191
199
  i += 2;
192
200
  continue;
193
201
  }
194
- if (ch === '|' || ch === ';') {
202
+ // A newline separates commands the same way `;` does (an unquoted, un-escaped
203
+ // line break), so `cd X\ncmd` splits into two segments instead of reading as
204
+ // one `cd` command — the top source of `other` classifications (#1830). A
205
+ // `\`-continued line never reaches here (handled by the escape branch above).
206
+ if (ch === '|' || ch === ';' || ch === '\n') {
195
207
  if (current.trim())
196
208
  parts.push(current.trim());
197
209
  current = '';
@@ -247,22 +259,61 @@ function scanSubcommand(tokens, tool) {
247
259
  }
248
260
  return '';
249
261
  }
262
+ /**
263
+ * The classifier reads only the executable and the first non-flag token, both at
264
+ * the very head of the first simple command. Tokenizing the *entire* command —
265
+ * every pipeline segment, multi-KB heredoc bodies included — to reach the first
266
+ * word cost up to ~1ms per call (a 7.8KB `cat <<HEREDOC …` classified on the word
267
+ * `cat`). Tokenize only this much of the head instead: enough for the executable
268
+ * plus a subcommand and its flags, never a heredoc tail (#1830, ~3.3x faster).
269
+ */
270
+ const CLASSIFY_HEAD_LIMIT = 200;
271
+ /**
272
+ * Tokens of the first simple command only, tokenizing just the head of the
273
+ * unwrapped string. Mirrors {@link tokenizeBash}'s first-segment result for
274
+ * short commands but skips the cost of tokenizing the whole command; the full
275
+ * multi-segment tokenizer stays available for callers that need every segment.
276
+ */
277
+ function firstSimpleCommandTokens(command) {
278
+ const unwrapped = unwrapCommand(command);
279
+ const head = unwrapped.length > CLASSIFY_HEAD_LIMIT ? unwrapped.slice(0, CLASSIFY_HEAD_LIMIT) : unwrapped;
280
+ const firstSegment = splitOnOperators(head)[0] ?? '';
281
+ if (!firstSegment)
282
+ return [];
283
+ try {
284
+ return shlexSplit(firstSegment);
285
+ }
286
+ catch {
287
+ // The head cut a quote mid-string (e.g. a long quoted flag value like
288
+ // `git -c http.extraheader="Authorization: Bearer …" fetch`), so shlex threw
289
+ // on the unbalanced quote. A whitespace split of the truncated head would
290
+ // mis-read the value as the subcommand, so re-tokenize the FULL first
291
+ // segment instead — only the rare throw path pays that cost.
292
+ const fullFirst = splitOnOperators(unwrapped)[0] ?? firstSegment;
293
+ try {
294
+ return shlexSplit(fullFirst);
295
+ }
296
+ catch {
297
+ // Genuinely unbalanced quoting even in the full segment — a whitespace
298
+ // split of the full segment still yields the leading executable.
299
+ return fullFirst.split(/\s+/).filter(Boolean);
300
+ }
301
+ }
302
+ }
250
303
  /**
251
304
  * Classify the first simple command in a Bash string. Returns coarse metadata
252
305
  * (tool name, category, subcommand, human action) used for summaries and
253
306
  * activity logging. Unknown executables fall back to `other`.
254
307
  */
255
308
  export function classifyBashCommand(command) {
256
- const simpleCommands = tokenizeBash(command);
257
- if (!simpleCommands.length) {
258
- return { tool: 'other', category: 'other', subcommand: '', action: 'running command', summary: '', signal: 'low' };
259
- }
260
- const tokens = simpleCommands[0];
309
+ const tokens = firstSimpleCommandTokens(command);
261
310
  if (!tokens.length) {
262
311
  return { tool: 'other', category: 'other', subcommand: '', action: 'running command', summary: '', signal: 'low' };
263
312
  }
264
313
  const first = tokens[0];
265
- const baseRaw = first.replace(/^[./]+/, '').toLowerCase();
314
+ // Reduce a path executable to its basename so `~/.agents/skills/linear/scripts/linear`,
315
+ // `/usr/bin/git`, and `./tool` all resolve by tool name, not the full path (#1830).
316
+ const baseRaw = first.replace(/^.*\//, '').toLowerCase();
266
317
  const base = baseRaw.endsWith('.exe') ? baseRaw.slice(0, -4) : baseRaw;
267
318
  const canonical = ALIAS_MAP.get(base);
268
319
  const info = canonical ? TOOL_REGISTRY[canonical] : undefined;
@@ -8,10 +8,11 @@
8
8
  */
9
9
  import Database from '../sqlite.js';
10
10
  import type { SessionAgentId, SessionMeta } from './types.js';
11
+ import { type IndexedToolCall } from './tool-calls.js';
11
12
  /** Current schema version; bumped when migrations are added. Exported so tests
12
13
  * assert against the constant instead of hardcoding a number that every bump
13
14
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
14
- export declare const SCHEMA_VERSION = 24;
15
+ export declare const SCHEMA_VERSION = 30;
15
16
  /** Raw row shape returned from the sessions table. */
16
17
  export interface SessionRow {
17
18
  id: string;
@@ -91,6 +92,8 @@ export interface QueryOptions {
91
92
  * with NULLs sorted last so unpriced rows never crowd out real data.
92
93
  */
93
94
  sortBy?: 'timestamp' | 'cost' | 'duration';
95
+ /** Internal warm-cache path; callers must validate the small final result set. */
96
+ skipExistenceCheck?: boolean;
94
97
  /**
95
98
  * Only sessions that invoked this skill (#12), joined against
96
99
  * session_resource_usage.kind='skill'. Matches either the full stored name
@@ -204,6 +207,9 @@ export declare function upsertSessionsBatch(entries: Array<{
204
207
  scan?: ScanStamp;
205
208
  parserState?: string;
206
209
  contentText?: string;
210
+ toolCalls?: IndexedToolCall[];
211
+ toolScan?: ScanStamp;
212
+ toolIndexMode?: 'replace' | 'append';
207
213
  }>): void;
208
214
  /**
209
215
  * Sync labels for a set of sessions. For each id in the map, if the stored