@phnx-labs/agents-cli 1.21.2 → 1.21.3

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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/computer-actions.d.ts +4 -0
  4. package/dist/commands/computer-actions.js +34 -0
  5. package/dist/commands/computer.js +4 -2
  6. package/dist/commands/perf.d.ts +16 -0
  7. package/dist/commands/perf.js +11 -1
  8. package/dist/commands/projects.d.ts +11 -1
  9. package/dist/commands/projects.js +16 -2
  10. package/dist/commands/sessions-picker.js +17 -2
  11. package/dist/commands/sessions.d.ts +5 -0
  12. package/dist/commands/sessions.js +14 -0
  13. package/dist/index.js +8 -0
  14. package/dist/lib/browser/service.js +13 -0
  15. package/dist/lib/computer/dispatch.d.ts +3 -1
  16. package/dist/lib/computer/dispatch.js +10 -2
  17. package/dist/lib/event-stream.d.ts +2 -0
  18. package/dist/lib/event-stream.js +3 -0
  19. package/dist/lib/events.d.ts +3 -1
  20. package/dist/lib/events.js +4 -2
  21. package/dist/lib/git.d.ts +14 -0
  22. package/dist/lib/git.js +36 -0
  23. package/dist/lib/hooks/profile.js +1 -14
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  26. package/dist/lib/percentile.d.ts +12 -0
  27. package/dist/lib/percentile.js +24 -0
  28. package/dist/lib/perf/db.d.ts +1 -2
  29. package/dist/lib/perf/db.js +2 -14
  30. package/dist/lib/plugins.js +12 -1
  31. package/dist/lib/resources.d.ts +16 -0
  32. package/dist/lib/resources.js +25 -14
  33. package/dist/lib/routines.js +1 -14
  34. package/dist/lib/runner.js +8 -1
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/session/db.d.ts +16 -1
  38. package/dist/lib/session/db.js +227 -4
  39. package/dist/lib/session/discover.d.ts +28 -0
  40. package/dist/lib/session/discover.js +42 -1
  41. package/dist/lib/session/highlights.d.ts +24 -4
  42. package/dist/lib/session/highlights.js +52 -7
  43. package/dist/lib/session/parse.js +19 -3
  44. package/dist/lib/session/prompt.d.ts +19 -0
  45. package/dist/lib/session/prompt.js +43 -0
  46. package/dist/lib/session/types.d.ts +42 -0
  47. package/dist/lib/types.d.ts +14 -0
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.3
4
+
5
+ - **`agents projects import --from-factory` stops printing raw git errors.** Reading each
6
+ checkout's real remote is done per registry row, and a checkout with no `origin` makes git
7
+ write `error: No such remote 'origin'` straight to the terminal — its own stderr, which the
8
+ surrounding try/catch never sees. Importing 12 rows printed two of them between the progress
9
+ lines. The probe now discards git's stderr; an absent remote is an expected answer, not
10
+ something to report. Source: `apps/cli/src/commands/projects.ts`.
11
+
12
+ - **Sessions now track browser/computer tool use and skill/plugin/slash-command
13
+ usage, queryable with `agents sessions --skill <name>` / `--plugin <name>`.**
14
+ `browser.navigate`, `browser.screenshot`, and a new `computer.action` event
15
+ fire on every `agents browser`/`agents computer` action, carrying session
16
+ identity for free. The sessions index persists `usedBrowser`/`usedComputer`
17
+ (from a scoped events-log read, not a transcript re-scan) and a new
18
+ `session_resource_usage` table records every skill and slash-command
19
+ invocation with its owning plugin, source repo, and git commit — resolved
20
+ against `resolveResource()`/`discoverPlugins()` at scan time. The sessions
21
+ picker preview surfaces both as `browser`/`computer` and `Skills:` tags.
22
+ Source: `apps/cli/src/lib/browser/service.ts`, `apps/cli/src/commands/computer-actions.ts`,
23
+ `apps/cli/src/lib/session/db.ts`, `apps/cli/src/lib/session/highlights.ts`,
24
+ `apps/cli/src/lib/session/discover.ts`, `apps/cli/src/commands/sessions.ts`.
25
+ - **`ResolvedResource` and `DiscoveredPlugin` carry provenance: `repoRoot` and a
26
+ lazily-resolved `snapshotSha`.** Every resource/plugin resolution can now
27
+ answer "which DotAgents repo, which commit" without an extra lookup; the git
28
+ shell-out is memoized per repo root and only runs when a caller actually
29
+ reads `snapshotSha`. Source: `apps/cli/src/lib/resources.ts`,
30
+ `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/git.ts`.
31
+ - **`SessionEvent.slashCommand` captures a typed or model-invoked slash command**
32
+ (both the `<command-name>` wrapper and the `SlashCommand` tool call), and
33
+ `agents sessions`'s perf sample for `command.end` now carries the session id
34
+ and agent instead of being anonymous. Source: `apps/cli/src/lib/session/prompt.ts`,
35
+ `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/index.ts`.
36
+
3
37
  ## 1.21.2
4
38
 
5
39
  - **`agents trends` — resource and session analytics dashboard.** Baked recipes
package/dist/bin/agents CHANGED
Binary file
@@ -82,4 +82,8 @@ export declare function shouldRaise(opts: {
82
82
  }): boolean;
83
83
  export declare function appPathIsElectron(appPath: string | null, exists?: (p: string) => boolean): boolean;
84
84
  export declare function electronWebviewTip(appLabel: string): string;
85
+ export declare function emitComputerAction(verb: string, targetPid: number | undefined, opts: {
86
+ bundle?: string;
87
+ host?: string;
88
+ }, extra?: Record<string, unknown>): void;
85
89
  export declare function registerActionCommands(program: Command): void;
@@ -11,6 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { openComputerClient, describeTransport, resolvePolicyPath, } from '../lib/computer-rpc.js';
13
13
  import { COMPUTER_INPUT_GATED_VERBS, formatComputerPermissionGrantHint, } from '../lib/permissions.js';
14
+ import { emit as emitEvent } from '../lib/events.js';
14
15
  function isComputerInputVerb(verb) {
15
16
  return typeof verb === 'string' && COMPUTER_INPUT_GATED_VERBS.includes(verb);
16
17
  }
@@ -350,6 +351,22 @@ function emit(result, json, human) {
350
351
  console.log(human());
351
352
  }
352
353
  }
354
+ // Record one `computer.action` per verb invocation, one call site per command
355
+ // below. Session/agent/machine identity is stamped for free by emitEvent's
356
+ // provenance floor (events.ts resolveProvenance) — this only carries the
357
+ // action-specific facts: which verb, against which target pid/bundle/host.
358
+ // NOTE: the field is `targetPid`, never `pid` — `pid` is a reserved envelope
359
+ // key (the emitting process's OWN pid, events.ts RESERVED_META_KEYS) that
360
+ // sanitizePayload() silently strips from the payload before it can collide.
361
+ export function emitComputerAction(verb, targetPid, opts, extra = {}) {
362
+ emitEvent('computer.action', {
363
+ command: verb,
364
+ targetPid,
365
+ bundle: opts.bundle,
366
+ host: opts.host,
367
+ ...extra,
368
+ });
369
+ }
353
370
  // Add the shared --pid/--bundle/--host target options to a verb. `--host` routes
354
371
  // the verb at a remote Windows device: the `computer` preAction hook hydrates
355
372
  // COMPUTER_HELPER_TCP from the tunnel `start --host` recorded, so withClient's
@@ -376,6 +393,7 @@ export function registerActionCommands(program) {
376
393
  await withClient(async (client) => {
377
394
  const res = unwrap(await client.call('list_apps'));
378
395
  const list = res.apps || [];
396
+ emitComputerAction('apps', undefined, opts);
379
397
  emit(res, Boolean(opts.json), () => list.length === 0
380
398
  ? '(no allow-listed apps running)'
381
399
  : list
@@ -422,6 +440,7 @@ export function registerActionCommands(program) {
422
440
  if (opts.background)
423
441
  params.background = true;
424
442
  const res = unwrap(await client.call('click', params));
443
+ emitComputerAction('click', pid, opts, { id: opts.id, count: opts.count });
425
444
  emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
426
445
  });
427
446
  });
@@ -440,6 +459,7 @@ export function registerActionCommands(program) {
440
459
  }
441
460
  await applyFocusPolicy(client, pid, opts);
442
461
  const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
462
+ emitComputerAction('right-click', pid, opts, { id: opts.id });
443
463
  emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
444
464
  });
445
465
  });
@@ -466,6 +486,8 @@ export function registerActionCommands(program) {
466
486
  if (opts.allowSecureField)
467
487
  params.allow_secure_field = true;
468
488
  const res = unwrap(await client.call('type', params));
489
+ // textLength, never the text itself — users type passwords/secrets into fields.
490
+ emitComputerAction('type', pid, opts, { id: opts.id, textLength: opts.text.length, committed: Boolean(res.committed) });
469
491
  emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
470
492
  });
471
493
  });
@@ -493,6 +515,7 @@ export function registerActionCommands(program) {
493
515
  params.char_delay_ms = charDelay;
494
516
  const res = unwrap(await client.call('type_text', params));
495
517
  warnIfNotFrontmost(res);
518
+ emitComputerAction('type-text', pid, opts, { textLength: opts.text.length, committed: Boolean(opts.commit) });
496
519
  emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
497
520
  });
498
521
  });
@@ -513,6 +536,7 @@ export function registerActionCommands(program) {
513
536
  params.require_frontmost = true;
514
537
  const res = unwrap(await client.call('key', params));
515
538
  warnIfNotFrontmost(res);
539
+ emitComputerAction('key', pid, opts, { keys: opts.keys });
516
540
  emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
517
541
  });
518
542
  });
@@ -548,6 +572,7 @@ export function registerActionCommands(program) {
548
572
  if (opts.background)
549
573
  params.background = true;
550
574
  const res = unwrap(await client.call('drag', params));
575
+ emitComputerAction('drag', pid, opts, { from: opts.from, to: opts.to });
551
576
  emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
552
577
  });
553
578
  });
@@ -574,6 +599,7 @@ export function registerActionCommands(program) {
574
599
  if (opts.dx != null)
575
600
  params.dx = opts.dx;
576
601
  const res = unwrap(await client.call('scroll', params));
602
+ emitComputerAction('scroll', pid, opts, { id: opts.id, dx: opts.dx, dy: opts.dy });
577
603
  emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
578
604
  });
579
605
  });
@@ -587,6 +613,7 @@ export function registerActionCommands(program) {
587
613
  await withClient(async (client) => {
588
614
  const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
589
615
  const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
616
+ emitComputerAction('ax-action', pid, opts, { id: opts.id, action: opts.action });
590
617
  emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
591
618
  });
592
619
  });
@@ -599,6 +626,7 @@ export function registerActionCommands(program) {
599
626
  await withClient(async (client) => {
600
627
  const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
601
628
  const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
629
+ emitComputerAction('focus', pid, opts, { id: opts.id });
602
630
  emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
603
631
  });
604
632
  });
@@ -615,6 +643,7 @@ export function registerActionCommands(program) {
615
643
  await withClient(async (client) => {
616
644
  const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
617
645
  const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
646
+ emitComputerAction('raise', pid, opts, { windowId: opts.windowId, title: opts.title });
618
647
  emit(res, Boolean(opts.json), () => {
619
648
  const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
620
649
  return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
@@ -644,6 +673,9 @@ export function registerActionCommands(program) {
644
673
  if (params.duration_ms == null)
645
674
  params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
646
675
  const res = unwrap(await client.call('wait', params));
676
+ emitComputerAction('wait', params.pid, opts, {
677
+ until: opts.until, durationMs: opts.duration, satisfied: Boolean(res.satisfied),
678
+ });
647
679
  emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
648
680
  });
649
681
  });
@@ -662,6 +694,7 @@ export function registerActionCommands(program) {
662
694
  if (opts.maxChars != null)
663
695
  params.max_chars = opts.maxChars;
664
696
  const res = unwrap(await client.call('get_text', params));
697
+ emitComputerAction('get-text', pid, opts, { id: opts.id });
665
698
  emit(res, Boolean(opts.json), () => String(res.text ?? ''));
666
699
  });
667
700
  });
@@ -688,6 +721,7 @@ export function registerActionCommands(program) {
688
721
  if (opts.name)
689
722
  params.name = opts.name;
690
723
  const res = unwrap(await client.call('launch_app', params));
724
+ emitComputerAction('launch', res.pid, opts, { path: opts.path, name: opts.name });
691
725
  emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
692
726
  });
693
727
  });
@@ -7,7 +7,7 @@ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketP
7
7
  import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
8
8
  import { sshExec } from '../lib/ssh-exec.js';
9
9
  import { encodePowershell } from '../lib/hosts/remote-cmd.js';
10
- import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
10
+ import { registerActionCommands, withClient, unwrap, pickTarget, emitComputerAction } from './computer-actions.js';
11
11
  import { runComputerLoop } from '../lib/computer/loop.js';
12
12
  import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
13
13
  import { makeClaudeResponder, resolveApiKey, DEFAULT_CLAUDE_MODEL, DEFAULT_CLAUDE_BASE_URL } from '../lib/computer/model.js';
@@ -301,7 +301,7 @@ function registerRunCommand(program) {
301
301
  maxTokens: opts.maxTokens,
302
302
  });
303
303
  await withClient(async (client) => {
304
- const dispatch = makeVerbDispatcher(client);
304
+ const dispatch = makeVerbDispatcher(client, { host: opts.host });
305
305
  const targetInput = opts.bundle ? { bundle: opts.bundle } : {};
306
306
  const result = await runComputerLoop({
307
307
  task: opts.task,
@@ -375,6 +375,7 @@ function registerScreenshotCommand(program) {
375
375
  // --list: enumerate windows, no image.
376
376
  if (opts.list) {
377
377
  const res = unwrap(await client.call('screenshot', { pid, list: true }));
378
+ emitComputerAction('screenshot', pid, opts, { list: true });
378
379
  const windows = res.windows || [];
379
380
  if (opts.json) {
380
381
  console.log(JSON.stringify(res, null, 2));
@@ -402,6 +403,7 @@ function registerScreenshotCommand(program) {
402
403
  console.error('helper returned no image_data');
403
404
  process.exit(1);
404
405
  }
406
+ emitComputerAction('screenshot', pid, opts, { display: opts.display, windowId: opts.windowId });
405
407
  const buf = Buffer.from(b64, 'base64');
406
408
  // Sniff the real format and correct the extension so the filename never
407
409
  // lies about its bytes (macOS -> JPEG, Windows helper -> PNG).
@@ -14,6 +14,13 @@
14
14
  import type { Command } from 'commander';
15
15
  import { type PerfAggregateRow } from '../lib/perf/db.js';
16
16
  import { type HookProfileRow } from '../lib/hooks/profile.js';
17
+ interface PerfGlobalOpts {
18
+ days?: string;
19
+ warnMs?: string;
20
+ json?: boolean;
21
+ limit?: string;
22
+ project?: string;
23
+ }
17
24
  /** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
18
25
  export declare function asHookRows(rows: PerfAggregateRow[]): HookProfileRow[];
19
26
  export declare function renderHookTable(rows: HookProfileRow[], warnMs: number): void;
@@ -24,4 +31,13 @@ export declare function renderHookTable(rows: HookProfileRow[], warnMs: number):
24
31
  * hit ignores it (a caller filtering by project has no legacy rows to miss).
25
32
  */
26
33
  export declare function loadHookProfile(days: number, project?: string): HookProfileRow[];
34
+ /**
35
+ * Sessions stuck repeatedly hitting the SAME guard block (git-guard,
36
+ * rm-guard, git-require-clean-tree, …) instead of adapting after the first
37
+ * denial. Reads the `friction` event sink (emitFriction in events.ts) that
38
+ * guard hooks self-report into via `agents _internal friction` before they
39
+ * exit 2 — see lib/friction-heuristics.ts for the grouping.
40
+ */
41
+ export declare function frictionAction(opts: PerfGlobalOpts): void;
27
42
  export declare function registerPerfCommand(program: Command): void;
43
+ export {};
@@ -175,7 +175,17 @@ function runAction(opts) {
175
175
  * guard hooks self-report into via `agents _internal friction` before they
176
176
  * exit 2 — see lib/friction-heuristics.ts for the grouping.
177
177
  */
178
- function frictionAction(opts) {
178
+ export function frictionAction(opts) {
179
+ // --project is declared on the shared `perf` parent for hooks/commands/run,
180
+ // where every sample carries a cwd. friction events (emitFriction in
181
+ // events.ts) don't carry one today — agents _internal friction has no
182
+ // --cwd flag — so silently accepting the flag here would look like it
183
+ // filtered when it did nothing. Fail loud instead of no-op.
184
+ if (opts.project) {
185
+ console.error(chalk.red("agents perf friction does not support --project yet — friction events carry no cwd to filter on."));
186
+ process.exitCode = 1;
187
+ return;
188
+ }
179
189
  const days = parseDays(opts.days);
180
190
  const startDate = new Date(Date.now() - days * 86_400_000);
181
191
  const events = query({ eventTypes: ['friction'], startDate });
@@ -20,7 +20,17 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
20
20
  * string when everything answered.
21
21
  */
22
22
  export declare function formatFleetSkippedNote(skipped: string[]): string;
23
- /** Best-effort `owner/repo` from a repo's origin remote. */
23
+ /**
24
+ * Best-effort `owner/repo` from a repo's origin remote.
25
+ *
26
+ * `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
27
+ * makes git print `error: No such remote 'origin'` on ITS stderr, which is the
28
+ * terminal's — the catch below never sees it. That was invisible while this was
29
+ * called once from `add` inside a real repo, and became noise the moment
30
+ * `import --from-factory` started calling it per row: importing 12 registry
31
+ * rows printed two raw git errors between the progress lines. Absence of a
32
+ * remote is an expected answer here (`undefined`), not something to report.
33
+ */
24
34
  export declare function originSlug(cwd: string): string | undefined;
25
35
  /** One `projects list` row, pre-render. */
26
36
  export interface ProjectListRow {
@@ -57,10 +57,24 @@ function parseContextFlag(raw) {
57
57
  return { path: raw.trim(), purpose: '' };
58
58
  return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
59
59
  }
60
- /** Best-effort `owner/repo` from a repo's origin remote. */
60
+ /**
61
+ * Best-effort `owner/repo` from a repo's origin remote.
62
+ *
63
+ * `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
64
+ * makes git print `error: No such remote 'origin'` on ITS stderr, which is the
65
+ * terminal's — the catch below never sees it. That was invisible while this was
66
+ * called once from `add` inside a real repo, and became noise the moment
67
+ * `import --from-factory` started calling it per row: importing 12 registry
68
+ * rows printed two raw git errors between the progress lines. Absence of a
69
+ * remote is an expected answer here (`undefined`), not something to report.
70
+ */
61
71
  export function originSlug(cwd) {
62
72
  try {
63
- const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf8' }).trim();
73
+ const url = execFileSync('git', ['remote', 'get-url', 'origin'], {
74
+ cwd,
75
+ encoding: 'utf8',
76
+ stdio: ['ignore', 'pipe', 'ignore'],
77
+ }).trim();
64
78
  return parseOwnerRepoFromRemote(url) ?? undefined;
65
79
  }
66
80
  catch {
@@ -374,6 +374,13 @@ function formatCompactPreview(events, session) {
374
374
  let latestTodos;
375
375
  let subAgentCount = 0;
376
376
  const toolTags = new Set();
377
+ // usedBrowser/usedComputer are computed at scan time from a sessionId-scoped
378
+ // events-log read (session/db.ts detectToolUsage), NOT a transcript regex —
379
+ // undefined means a legacy row this scanner hasn't computed the field for
380
+ // yet, so only THEN does classifySessionTool's transcript-derived guess run
381
+ // below (mirrors directoriesTouched's prefer-persisted/fall-back-to-derived
382
+ // pattern for recentDirectoriesTouched).
383
+ const knownToolUsage = session.usedBrowser !== undefined;
377
384
  for (const event of events) {
378
385
  if (event.type === 'message') {
379
386
  if (event.role === 'user' && !firstUser && event.content) {
@@ -388,8 +395,10 @@ function formatCompactPreview(events, session) {
388
395
  else if (event.type === 'tool_use' && !event._local) {
389
396
  const tool = event.tool || '';
390
397
  const command = event.command || '';
391
- for (const tag of classifySessionTool(tool, command))
392
- toolTags.add(tag);
398
+ if (!knownToolUsage) {
399
+ for (const tag of classifySessionTool(tool, command))
400
+ toolTags.add(tag);
401
+ }
393
402
  if (isSubAgentTool(tool, command))
394
403
  subAgentCount++;
395
404
  const p = event.path || event.args?.file_path || event.args?.path || '';
@@ -412,6 +421,12 @@ function formatCompactPreview(events, session) {
412
421
  toolCalls++;
413
422
  }
414
423
  }
424
+ // Persisted field wins when computed — it comes from real browser.navigate/
425
+ // browser.screenshot/computer.action events, not a fuzzy tool-name regex.
426
+ if (session.usedBrowser)
427
+ toolTags.add('browser');
428
+ if (session.usedComputer)
429
+ toolTags.add('computer');
415
430
  // Prefer the transcript-derived list when we just re-parsed the file (freshest
416
431
  // checklist write); fall back to SessionMeta.todos for rows where the scan/
417
432
  // fan-out attached progress but the event stream has no TodoWrite yet.
@@ -70,6 +70,11 @@ interface SessionsOptions extends SessionFilterOptions {
70
70
  printCmd?: boolean;
71
71
  /** Print a compact preview of the matched session and exit (no pager). */
72
72
  preview?: boolean;
73
+ /** Only sessions that invoked this skill (#12) — matches a bare name or a
74
+ * namespaced plugin skill's short name (`--skill design` finds `rush:design`). */
75
+ skill?: string;
76
+ /** Only sessions that used a skill/command owned by this plugin (#12). */
77
+ plugin?: string;
73
78
  }
74
79
  /**
75
80
  * Strip terminal/harness noise from a preview so the column stays a single line
@@ -1172,6 +1172,12 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
1172
1172
  !options.markdown &&
1173
1173
  !options.until &&
1174
1174
  !options.project &&
1175
+ // The interactive browser picker is a fuzzy-search TUI over the discovered
1176
+ // pool, not a SQL-filtered listing — it cannot represent a skill/plugin
1177
+ // scope. Falling through to it here would silently drop the filter and
1178
+ // show the unfiltered pool instead (same reasoning as --project/--sort).
1179
+ !options.skill &&
1180
+ !options.plugin &&
1175
1181
  !options.sort &&
1176
1182
  !options.artifacts &&
1177
1183
  options.artifact === undefined &&
@@ -1202,6 +1208,10 @@ function canonicalSessionsCommand(query, options) {
1202
1208
  a.push('--device', h);
1203
1209
  if (options.project)
1204
1210
  a.push('--project', options.project);
1211
+ if (options.skill)
1212
+ a.push('--skill', options.skill);
1213
+ if (options.plugin)
1214
+ a.push('--plugin', options.plugin);
1205
1215
  if (options.all)
1206
1216
  a.push('--all');
1207
1217
  if (options.since)
@@ -1557,6 +1567,8 @@ limitSource) {
1557
1567
  until: options.until,
1558
1568
  sortBy,
1559
1569
  origin: options.routine ? 'routine' : undefined,
1570
+ skill: options.skill,
1571
+ plugin: options.plugin,
1560
1572
  };
1561
1573
  let hiddenUnmanaged = 0;
1562
1574
  let sessions = await discoverSessions({
@@ -3306,6 +3318,8 @@ export function registerSessionsCommands(program) {
3306
3318
  .option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
3307
3319
  .option('--routine', 'Show only sessions archived from routine runs')
3308
3320
  .option('-p, --project <name>', 'Filter by project name (searches across all directories)')
3321
+ .option('--skill <name>', 'Only sessions that invoked this skill (matches a bare name or a namespaced plugin skill\'s short name, e.g. --skill design finds rush:design)')
3322
+ .option('--plugin <name>', 'Only sessions that used a skill/command owned by this plugin')
3309
3323
  .option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
3310
3324
  .option('--until <time>', 'Only sessions older than this (ISO timestamp)')
3311
3325
  .option('-n, --limit <n>', 'Maximum number of sessions to return', DEFAULT_LIMIT)
package/dist/index.js CHANGED
@@ -99,6 +99,7 @@ import { applyGlobalHelpConventions } from './lib/help.js';
99
99
  import { renderWhatsNew } from './lib/whats-new.js';
100
100
  import { getCliLaunch } from './lib/cli-entry.js';
101
101
  import { emit, emitFriction, redactArgs } from './lib/events.js';
102
+ import { stampProvenance } from './lib/event-provenance.js';
102
103
  import { die } from './lib/format.js';
103
104
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
104
105
  // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
@@ -211,12 +212,19 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
211
212
  }
212
213
  // Disposable perf warehouse — fail-soft spool append (no SQLite on this path).
213
214
  if (durationMs !== undefined && parts[0] !== 'perf') {
215
+ // sessionId/agent are resolvable here the same way emit() resolves them
216
+ // for command.start/command.end above (the shared provenance floor,
217
+ // event-provenance.ts) — without this, every command.end perf sample
218
+ // was anonymous, unlike the audit log record right next to it.
219
+ const { sessionId, agent } = stampProvenance();
214
220
  void import('./lib/perf/spool.js').then(({ recordSample }) => {
215
221
  recordSample({
216
222
  kind: 'command.end',
217
223
  label: command,
218
224
  durationMs,
219
225
  cwd: process.cwd(),
226
+ sessionId,
227
+ agent,
220
228
  });
221
229
  }).catch(() => { });
222
230
  }
@@ -486,6 +486,7 @@ export class BrowserService {
486
486
  const sessionId = await this.getSessionId(conn, cdpTargetId);
487
487
  await conn.cdp.send('Page.navigate', { url }, sessionId);
488
488
  await this.saveTaskState(task.profile, conn.tasks);
489
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: currentShortId, created: false });
489
490
  return { tabId: currentShortId, url, created: false };
490
491
  }
491
492
  // No current tab - create one
@@ -500,6 +501,7 @@ export class BrowserService {
500
501
  task.tabs[shortId] = cdpTargetId;
501
502
  task.currentTabId = shortId;
502
503
  await this.saveTaskState(task.profile, conn.tasks);
504
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
503
505
  return { tabId: shortId, url, created: true };
504
506
  }
505
507
  // Chrome: create new tab
@@ -511,6 +513,7 @@ export class BrowserService {
511
513
  task.currentTabId = shortId;
512
514
  this.invalidateTargetCache(conn);
513
515
  await this.saveTaskState(task.profile, conn.tasks);
516
+ emit('browser.navigate', { profile: task.profile, task: task.name, url, tabId: shortId, created: true });
514
517
  return { tabId: shortId, url, created: true };
515
518
  }
516
519
  async tabAdd(taskId, url, profileName) {
@@ -705,6 +708,16 @@ export class BrowserService {
705
708
  await fs.promises.writeFile(finalPath, buffer);
706
709
  const dims = (extension === 'png' ? readPngDimensions(buffer) : readJpegDimensions(buffer)) ??
707
710
  { width: 0, height: 0 };
711
+ emit('browser.screenshot', {
712
+ profile: profileName,
713
+ task: task.name,
714
+ tabId: shortId,
715
+ path: finalPath,
716
+ bytes: buffer.length,
717
+ width: dims.width,
718
+ height: dims.height,
719
+ quality,
720
+ });
708
721
  return { path: finalPath, bytes: buffer.length, width: dims.width, height: dims.height };
709
722
  }
710
723
  /**
@@ -3,6 +3,8 @@ import { type AppInfo } from '../../commands/computer-actions.js';
3
3
  import type { VerbDispatcher } from './loop.js';
4
4
  declare function rpcMethodFor(verb: string): string;
5
5
  declare function toRpcParams(verb: string, input: Record<string, unknown>, pid?: number): Record<string, unknown>;
6
- export declare function makeVerbDispatcher(client: ComputerClient): VerbDispatcher;
6
+ export declare function makeVerbDispatcher(client: ComputerClient, context?: {
7
+ host?: string;
8
+ }): VerbDispatcher;
7
9
  export { rpcMethodFor, toRpcParams };
8
10
  export type { AppInfo };
@@ -5,7 +5,7 @@
5
5
  // it names the same RPC methods the explicit `agents computer <verb>` commands
6
6
  // call, so the external-agent verb surface is untouched. The daemon stays the
7
7
  // single authority on permissions and targeting.
8
- import { resolveTargetPidDecision } from '../../commands/computer-actions.js';
8
+ import { resolveTargetPidDecision, emitComputerAction } from '../../commands/computer-actions.js';
9
9
  // CLI verb -> daemon RPC method. Only the rename cases need listing; the rest
10
10
  // pass through by name.
11
11
  const RPC_METHOD = {
@@ -102,7 +102,14 @@ function toRpcParams(verb, input, pid) {
102
102
  // Build a dispatcher bound to a live client. Every call is a single daemon
103
103
  // round-trip; errors are returned (never thrown) so the loop can feed them
104
104
  // back to the model as a tool_result.
105
- export function makeVerbDispatcher(client) {
105
+ //
106
+ // `computer run`'s embedded model loop drives every verb (click, type,
107
+ // screenshot, ...) through THIS single seam rather than the explicit
108
+ // `agents computer <verb>` commands in computer-actions.ts, so it must emit
109
+ // the same `computer.action` event those commands emit — otherwise a session
110
+ // that only used the loop reads back usedComputer=false even though it drove
111
+ // real actions (reviewer-flagged regression on #1864).
112
+ export function makeVerbDispatcher(client, context = {}) {
106
113
  return async (call) => {
107
114
  const verb = call.name;
108
115
  const input = call.input ?? {};
@@ -118,6 +125,7 @@ export function makeVerbDispatcher(client) {
118
125
  const res = await client.call(method, params);
119
126
  if (res.error)
120
127
  return { ok: false, error: `${res.error.code}: ${res.error.message}` };
128
+ emitComputerAction(verb, pid, { bundle: input.bundle, host: context.host });
121
129
  return { ok: true, result: res.result ?? {} };
122
130
  };
123
131
  }
@@ -18,6 +18,8 @@ export interface UnifiedQuery {
18
18
  eventTypes?: EventType[];
19
19
  level?: EventLevel;
20
20
  agent?: string;
21
+ /** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
22
+ sessionId?: string;
21
23
  caller?: string;
22
24
  command?: string;
23
25
  module?: string;
@@ -29,6 +29,8 @@ function matches(r, q) {
29
29
  return false;
30
30
  if (q.agent && r.agent !== q.agent)
31
31
  return false;
32
+ if (q.sessionId && r.sessionId !== q.sessionId)
33
+ return false;
32
34
  if (q.caller && r.caller !== q.caller)
33
35
  return false;
34
36
  if (q.command && r.command !== q.command &&
@@ -51,6 +53,7 @@ export function readUnifiedEvents(q = {}) {
51
53
  eventTypes: q.eventTypes,
52
54
  level: q.level,
53
55
  agent: q.agent,
56
+ sessionId: q.sessionId,
54
57
  caller: q.caller,
55
58
  command: q.command,
56
59
  module: q.module,
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { ActorKind } from './actor.js';
15
15
  export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
16
- export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
16
+ export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
17
17
  /** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
18
18
  export declare const EVENT_TYPES: readonly EventType[];
19
19
  /** Runtime guard for an event kind arriving from outside this process. */
@@ -208,6 +208,8 @@ export declare function query(options: {
208
208
  eventTypes?: EventType[];
209
209
  level?: EventLevel;
210
210
  agent?: string;
211
+ /** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
212
+ sessionId?: string;
211
213
  caller?: string;
212
214
  command?: string;
213
215
  module?: string;
@@ -84,6 +84,7 @@ const EVENT_TYPE_TABLE = {
84
84
  'version.install': true, 'version.switch': true, 'version.remove': true,
85
85
  'skill.install': true, 'skill.remove': true,
86
86
  'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
87
+ 'computer.action': true,
87
88
  'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
88
89
  'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
89
90
  'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
@@ -327,7 +328,6 @@ export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.st
327
328
  }
328
329
  return { kind: stdoutIsTTY ? 'terminal' : 'script' };
329
330
  }
330
- // ─── Audit attribution ────────────────────────────────────────────────────────
331
331
  // ─── Core API ─────────────────────────────────────────────────────────────────
332
332
  /**
333
333
  * Emit a structured event to the append-only audit log.
@@ -729,7 +729,7 @@ export function maybeRotate() {
729
729
  * @returns Array of event records
730
730
  */
731
731
  export function query(options) {
732
- const { startDate, endDate = new Date(), eventTypes, level, agent, caller, command, module, limit } = options;
732
+ const { startDate, endDate = new Date(), eventTypes, level, agent, sessionId, caller, command, module, limit } = options;
733
733
  const results = [];
734
734
  if (!fs.existsSync(eventsDir()))
735
735
  return results;
@@ -774,6 +774,8 @@ export function query(options) {
774
774
  continue;
775
775
  if (agent && record.agent !== agent)
776
776
  continue;
777
+ if (sessionId && record.sessionId !== sessionId)
778
+ continue;
777
779
  if (caller && record.caller !== caller)
778
780
  continue;
779
781
  if (command && record.command !== command &&
package/dist/lib/git.d.ts CHANGED
@@ -103,6 +103,20 @@ export interface RepoStateSnapshot {
103
103
  * result is stable across git versions and never mutates the tree. Returns null
104
104
  * when the path is not a git worktree. */
105
105
  export declare function readRepoState(repoPath: string): RepoStateSnapshot | null;
106
+ /**
107
+ * The short HEAD sha of the git repo at `repoRoot` (`git -C <repoRoot>
108
+ * rev-parse --short HEAD`), for provenance — "which commit of this DotAgents
109
+ * repo was this resource/plugin resolved from". `undefined` when `repoRoot`
110
+ * isn't a git repo (or has no commits yet), never a throw.
111
+ *
112
+ * Deliberately synchronous + resolved once and cached: callers (resources.ts,
113
+ * plugins.ts) attach this as a lazy getter on the resolved object, so a
114
+ * consumer that never inspects provenance never pays for the git shell-out —
115
+ * see {@link ResolvedResource.snapshotSha} / {@link DiscoveredPlugin.snapshotSha}.
116
+ */
117
+ export declare function resolveSnapshotSha(repoRoot: string): string | undefined;
118
+ /** Test seam: clear the memoized snapshot-sha cache between test cases. */
119
+ export declare function _resetSnapshotShaCacheForTest(): void;
106
120
  /**
107
121
  * Get the current GitHub username using gh CLI.
108
122
  * Returns null if gh is not installed or user is not authenticated.