@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/dist/lib/git.js CHANGED
@@ -371,6 +371,42 @@ export function readRepoState(repoPath) {
371
371
  const dirty = porcelain != null && porcelain.length > 0;
372
372
  return { branch, head, dirty };
373
373
  }
374
+ /** Memoized per repoRoot — a resolveResource()/listResources()/plugin-discovery
375
+ * call that touches many resources from the SAME DotAgents repo must not shell
376
+ * out to git once per resource. */
377
+ const _snapshotShaCache = new Map();
378
+ /**
379
+ * The short HEAD sha of the git repo at `repoRoot` (`git -C <repoRoot>
380
+ * rev-parse --short HEAD`), for provenance — "which commit of this DotAgents
381
+ * repo was this resource/plugin resolved from". `undefined` when `repoRoot`
382
+ * isn't a git repo (or has no commits yet), never a throw.
383
+ *
384
+ * Deliberately synchronous + resolved once and cached: callers (resources.ts,
385
+ * plugins.ts) attach this as a lazy getter on the resolved object, so a
386
+ * consumer that never inspects provenance never pays for the git shell-out —
387
+ * see {@link ResolvedResource.snapshotSha} / {@link DiscoveredPlugin.snapshotSha}.
388
+ */
389
+ export function resolveSnapshotSha(repoRoot) {
390
+ const cached = _snapshotShaCache.get(repoRoot);
391
+ if (cached !== undefined || _snapshotShaCache.has(repoRoot))
392
+ return cached;
393
+ let sha;
394
+ try {
395
+ const raw = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--short', 'HEAD'], {
396
+ stdio: ['ignore', 'pipe', 'ignore'],
397
+ }).toString().trim();
398
+ sha = raw || undefined;
399
+ }
400
+ catch {
401
+ sha = undefined;
402
+ }
403
+ _snapshotShaCache.set(repoRoot, sha);
404
+ return sha;
405
+ }
406
+ /** Test seam: clear the memoized snapshot-sha cache between test cases. */
407
+ export function _resetSnapshotShaCacheForTest() {
408
+ _snapshotShaCache.clear();
409
+ }
374
410
  /**
375
411
  * Get the current GitHub username using gh CLI.
376
412
  * Returns null if gh is not installed or user is not authenticated.
@@ -12,6 +12,7 @@
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import { getLogsDir } from '../state.js';
15
+ import { percentile } from '../percentile.js';
15
16
  /**
16
17
  * Load every `hook.fire` event from the last `days` daily log files.
17
18
  * Lines that aren't JSON or aren't `hook.fire` events are silently skipped —
@@ -53,20 +54,6 @@ export function loadHookFireEvents(days = 7, logsDir = getLogsDir()) {
53
54
  }
54
55
  return events;
55
56
  }
56
- /** Percentile of a sorted-ascending array. p in [0,100]. Linear interpolation. */
57
- function percentile(sorted, p) {
58
- if (sorted.length === 0)
59
- return 0;
60
- if (sorted.length === 1)
61
- return sorted[0];
62
- const rank = (p / 100) * (sorted.length - 1);
63
- const lo = Math.floor(rank);
64
- const hi = Math.ceil(rank);
65
- if (lo === hi)
66
- return sorted[lo];
67
- const frac = rank - lo;
68
- return sorted[lo] * (1 - frac) + sorted[hi] * frac;
69
- }
70
57
  /** Aggregate fire events into a per-hook profile, sorted by p99 desc. */
71
58
  export function aggregateHookProfile(events) {
72
59
  const byHook = new Map();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Percentile of a sorted-ascending array, linear interpolation. p in [0,100].
3
+ *
4
+ * Deliberately its own file with zero imports: `perf/db.ts` (the SQLite
5
+ * warehouse), `hooks/profile.ts` (the legacy JSONL hook profile), and
6
+ * `routines.ts` (routineStats) all need this exact formula, but `routines.ts`
7
+ * and `hooks/profile.ts` must NOT pull in `perf/db.ts`'s `../sqlite.js`
8
+ * dependency just to round a percentile — sqlite is a heavier, perf-warehouse-
9
+ * specific dependency that has no business loading into every routines- or
10
+ * hooks-touching code path.
11
+ */
12
+ export declare function percentile(sorted: number[], p: number): number;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Percentile of a sorted-ascending array, linear interpolation. p in [0,100].
3
+ *
4
+ * Deliberately its own file with zero imports: `perf/db.ts` (the SQLite
5
+ * warehouse), `hooks/profile.ts` (the legacy JSONL hook profile), and
6
+ * `routines.ts` (routineStats) all need this exact formula, but `routines.ts`
7
+ * and `hooks/profile.ts` must NOT pull in `perf/db.ts`'s `../sqlite.js`
8
+ * dependency just to round a percentile — sqlite is a heavier, perf-warehouse-
9
+ * specific dependency that has no business loading into every routines- or
10
+ * hooks-touching code path.
11
+ */
12
+ export function percentile(sorted, p) {
13
+ if (sorted.length === 0)
14
+ return 0;
15
+ if (sorted.length === 1)
16
+ return sorted[0];
17
+ const rank = (p / 100) * (sorted.length - 1);
18
+ const lo = Math.floor(rank);
19
+ const hi = Math.ceil(rank);
20
+ if (lo === hi)
21
+ return sorted[lo];
22
+ const frac = rank - lo;
23
+ return sorted[lo] * (1 - frac) + sorted[hi] * frac;
24
+ }
@@ -8,14 +8,13 @@ import Database from '../sqlite.js';
8
8
  import type { AggregateOptions, PerfAggregateRow } from './types.js';
9
9
  export type { AggregateOptions, PerfAggregateRow, PerfSample } from './types.js';
10
10
  export { recordSample, shortSessionId, resolveSpoolPath } from './spool.js';
11
+ export { percentile } from '../percentile.js';
11
12
  export declare const PERF_SCHEMA_VERSION = 1;
12
13
  export declare const DEFAULT_RETENTION_DAYS = 30;
13
14
  /** Test seam — redirect the warehouse path (like AGENTS_EVENTS_PATH). */
14
15
  export declare function _resetPerfDbForTest(overridePath?: string | null): void;
15
16
  /** Drain the NDJSON spool into samples. Idempotent; truncates on success. */
16
17
  export declare function drainSpool(db?: Database.Database): number;
17
- /** Percentile of a sorted-ascending array. p in [0,100]. */
18
- export declare function percentile(sorted: number[], p: number): number;
19
18
  /**
20
19
  * Aggregate samples by (kind, label) with p50/p95/p99. Drains the spool first.
21
20
  *
@@ -10,8 +10,10 @@ import Database from '../sqlite.js';
10
10
  import { getPerfDbPath, getPerfDir } from '../state.js';
11
11
  import { localMachineId } from '../session/origin-machine.js';
12
12
  import { resolveProjectKey } from '../project-key.js';
13
+ import { percentile } from '../percentile.js';
13
14
  import { resolveSpoolPath, shortSessionId, _resetPerfSpoolForTest } from './spool.js';
14
15
  export { recordSample, shortSessionId, resolveSpoolPath } from './spool.js';
16
+ export { percentile } from '../percentile.js';
15
17
  export const PERF_SCHEMA_VERSION = 1;
16
18
  export const DEFAULT_RETENTION_DAYS = 30;
17
19
  const SCHEMA = `
@@ -186,20 +188,6 @@ function maybeRetain(db) {
186
188
  // ignore
187
189
  }
188
190
  }
189
- /** Percentile of a sorted-ascending array. p in [0,100]. */
190
- export function percentile(sorted, p) {
191
- if (sorted.length === 0)
192
- return 0;
193
- if (sorted.length === 1)
194
- return sorted[0];
195
- const rank = (p / 100) * (sorted.length - 1);
196
- const lo = Math.floor(rank);
197
- const hi = Math.ceil(rank);
198
- if (lo === hi)
199
- return sorted[lo];
200
- const frac = rank - lo;
201
- return sorted[lo] * (1 - frac) + sorted[hi] * frac;
202
- }
203
191
  /**
204
192
  * Aggregate samples by (kind, label) with p50/p95/p99. Drains the spool first.
205
193
  *
@@ -14,7 +14,7 @@ import * as yaml from 'yaml';
14
14
  import { execFileSync } from 'child_process';
15
15
  import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir, getSystemPluginsDir } from './state.js';
16
16
  import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
17
- import { assertSafeGitTransport } from './git.js';
17
+ import { assertSafeGitTransport, resolveSnapshotSha } from './git.js';
18
18
  import { listInstalledVersions, getVersionHomePath } from './versions.js';
19
19
  import { AGENTS, agentConfigDirName } from './agents.js';
20
20
  import { capableAgents, isCapable } from './capabilities.js';
@@ -90,6 +90,13 @@ export function discoverPlugins(opts = {}) {
90
90
  return out;
91
91
  }
92
92
  export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user' }) {
93
+ // Every marketplace kind lays plugins out as `<repo>/plugins/<name>`, so the
94
+ // repo root is always the grandparent of pluginRoot — true for user
95
+ // (~/.agents), system (~/.agents/.system), each extra repo, and the project
96
+ // repo (<cwd>/.agents) alike. Deriving it here means every caller of
97
+ // buildDiscoveredPlugin (discoverPluginsInDir, inspectPluginCapabilities, …)
98
+ // gets provenance for free with no signature change.
99
+ const repoRoot = path.dirname(path.dirname(pluginRoot));
93
100
  return {
94
101
  name: manifest.name,
95
102
  root: pluginRoot,
@@ -107,6 +114,10 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
107
114
  monitors: discoverPluginMonitors(pluginRoot),
108
115
  hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
109
116
  hasSettings: pluginHasNonPermissionSettings(pluginRoot),
117
+ repoRoot,
118
+ get snapshotSha() {
119
+ return resolveSnapshotSha(repoRoot);
120
+ },
110
121
  };
111
122
  }
112
123
  /**
@@ -16,6 +16,22 @@ export interface ResolvedResource {
16
16
  * or the alias name (e.g. 'rush') for extra repos registered in agents.yaml.
17
17
  */
18
18
  source: string;
19
+ /**
20
+ * Absolute path to the DotAgents repo root this resource resolved from (the
21
+ * project/user/system/extra-repo dir — one level above the `kind`
22
+ * subdirectory). DotAgents repos are git-tracked (plugins.ts), so this pairs
23
+ * with {@link snapshotSha} to answer "which commit of which repo".
24
+ */
25
+ repoRoot: string;
26
+ /**
27
+ * Short HEAD sha of `repoRoot`'s git checkout, lazily resolved (a getter,
28
+ * not computed at construction) and memoized per repoRoot
29
+ * (`git.ts` `resolveSnapshotSha`) — a caller that never inspects provenance
30
+ * never pays for the git shell-out, and resolving many resources from the
31
+ * same repo pays for exactly one. `undefined` when `repoRoot` isn't a git
32
+ * repo (or has no commits).
33
+ */
34
+ readonly snapshotSha: string | undefined;
19
35
  }
20
36
  /**
21
37
  * True when `rawName` (a filename with its extension already stripped) names a
@@ -15,6 +15,16 @@ import { WorkflowsHandler } from './resources/workflows.js';
15
15
  import { isCapable } from './capabilities.js';
16
16
  import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
17
17
  import { isNameActiveInResourceProfile } from './resource-profiles.js';
18
+ import { resolveSnapshotSha } from './git.js';
19
+ /** Build a ResolvedResource with a lazy, memoized `snapshotSha` getter. */
20
+ function withProvenance(base) {
21
+ return {
22
+ ...base,
23
+ get snapshotSha() {
24
+ return resolveSnapshotSha(base.repoRoot);
25
+ },
26
+ };
27
+ }
18
28
  function profiledKind(kind) {
19
29
  switch (kind) {
20
30
  case 'commands':
@@ -70,19 +80,19 @@ export function resolveResource(kind, name, cwd) {
70
80
  const projectDir = getProjectAgentsDir(cwd);
71
81
  const extraRepos = getEnabledExtraRepos();
72
82
  const candidates = [
73
- ...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
74
- [path.join(getUserAgentsDir(), kind), 'user'],
75
- [path.join(getSystemAgentsDir(), kind), 'system'],
76
- ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
83
+ ...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
84
+ [path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
85
+ [path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
86
+ ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
77
87
  ];
78
- for (const [dir, source] of candidates) {
88
+ for (const [dir, source, repoRoot] of candidates) {
79
89
  if (!fs.existsSync(dir))
80
90
  continue;
81
91
  // Try exact name (for directories like skills/subagents)
82
92
  const exactPath = path.join(dir, name);
83
93
  if (fs.existsSync(exactPath)) {
84
94
  if (resourceIsActive(kind, name, source)) {
85
- return { name, path: exactPath, source };
95
+ return withProvenance({ name, path: exactPath, source, repoRoot });
86
96
  }
87
97
  continue;
88
98
  }
@@ -94,7 +104,7 @@ export function resolveResource(kind, name, cwd) {
94
104
  const withExt = exactPath + ext;
95
105
  if (fs.existsSync(withExt)) {
96
106
  if (resourceIsActive(kind, name, source)) {
97
- return { name, path: withExt, source };
107
+ return withProvenance({ name, path: withExt, source, repoRoot });
98
108
  }
99
109
  continue;
100
110
  }
@@ -113,12 +123,12 @@ export function listResources(kind, cwd) {
113
123
  const projectDir = getProjectAgentsDir(cwd);
114
124
  const extraRepos = getEnabledExtraRepos();
115
125
  const roots = [
116
- ...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
117
- [path.join(getUserAgentsDir(), kind), 'user'],
118
- [path.join(getSystemAgentsDir(), kind), 'system'],
119
- ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
126
+ ...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
127
+ [path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
128
+ [path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
129
+ ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
120
130
  ];
121
- for (const [dir, source] of roots) {
131
+ for (const [dir, source, repoRoot] of roots) {
122
132
  if (!fs.existsSync(dir))
123
133
  continue;
124
134
  let entries;
@@ -143,11 +153,12 @@ export function listResources(kind, cwd) {
143
153
  if (!resourceIsActive(kind, rawName, source))
144
154
  continue;
145
155
  seen.add(rawName);
146
- results.push({
156
+ results.push(withProvenance({
147
157
  name: rawName,
148
158
  path: path.join(dir, entry.name),
149
159
  source,
150
- });
160
+ repoRoot,
161
+ }));
151
162
  }
152
163
  }
153
164
  return results;
@@ -16,6 +16,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
16
16
  import { ALL_AGENT_IDS } from './agents.js';
17
17
  import { machineId, normalizeHost } from './machine-id.js';
18
18
  import { resolveActor } from './actor.js';
19
+ import { percentile } from './percentile.js';
19
20
  export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
20
21
  /** Canonical set of accepted GitHub trigger events — single source for validation. */
21
22
  export const GITHUB_TRIGGER_EVENTS = [
@@ -964,20 +965,6 @@ export function getLatestCompletedRun(jobName) {
964
965
  }
965
966
  return null;
966
967
  }
967
- /** Percentile of a sorted-ascending array (linear interpolation). p in [0,100]. */
968
- function percentile(sorted, p) {
969
- if (sorted.length === 0)
970
- return 0;
971
- if (sorted.length === 1)
972
- return sorted[0];
973
- const rank = (p / 100) * (sorted.length - 1);
974
- const lo = Math.floor(rank);
975
- const hi = Math.ceil(rank);
976
- if (lo === hi)
977
- return sorted[lo];
978
- const frac = rank - lo;
979
- return sorted[lo] * (1 - frac) + sorted[hi] * frac;
980
- }
981
968
  /**
982
969
  * Fold a job's run history (`listRuns`) into a duration + outcome summary.
983
970
  * `missed` fires (no process ever ran) carry no `duration` and are excluded
@@ -66,7 +66,14 @@ const ROUTINE_TRANSCRIPT_SPECS = {
66
66
  gemini: [{ root: ['.gemini', 'tmp'], ext: '.json' }],
67
67
  antigravity: [{ root: ['.gemini', 'antigravity-cli', 'conversations'], ext: '.db' }],
68
68
  droid: [{ root: ['.factory', 'sessions'], ext: '.jsonl' }],
69
- kimi: [{ root: ['.kimi-code', 'sessions'], ext: '.json' }],
69
+ // Kimi splits a session across two files (session/discover.ts:4382-4384):
70
+ // state.json (title/timestamps) and agents/main/wire.jsonl (the actual
71
+ // conversation). Both extensions are needed — .json alone archives only
72
+ // the metadata shell and silently drops every message.
73
+ kimi: [
74
+ { root: ['.kimi-code', 'sessions'], ext: '.json' },
75
+ { root: ['.kimi-code', 'sessions'], ext: '.jsonl' },
76
+ ],
70
77
  grok: [{ root: ['.grok', 'sessions'], ext: '.json' }],
71
78
  };
72
79
  /** Stable working directory for routine children, independent of the daemon's launch cwd. */
@@ -11,7 +11,7 @@ import type { SessionAgentId, SessionMeta } from './types.js';
11
11
  /** Current schema version; bumped when migrations are added. Exported so tests
12
12
  * assert against the constant instead of hardcoding a number that every bump
13
13
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
14
- export declare const SCHEMA_VERSION = 22;
14
+ export declare const SCHEMA_VERSION = 24;
15
15
  /** Raw row shape returned from the sessions table. */
16
16
  export interface SessionRow {
17
17
  id: string;
@@ -54,6 +54,9 @@ export interface SessionRow {
54
54
  linear_project_url: string | null;
55
55
  actor: string | null;
56
56
  initiated_by: string | null;
57
+ /** NULL means "not yet computed" (a row scanned before this field existed) — see rowToMeta. */
58
+ used_browser: number | null;
59
+ used_computer: number | null;
57
60
  }
58
61
  /** File stat snapshot used to detect changes between scan runs. */
59
62
  export interface ScanStamp {
@@ -88,6 +91,18 @@ export interface QueryOptions {
88
91
  * with NULLs sorted last so unpriced rows never crowd out real data.
89
92
  */
90
93
  sortBy?: 'timestamp' | 'cost' | 'duration';
94
+ /**
95
+ * Only sessions that invoked this skill (#12), joined against
96
+ * session_resource_usage.kind='skill'. Matches either the full stored name
97
+ * (bare, or `plugin:name` for a plugin skill) or just the short name after
98
+ * the colon — `--skill design` finds a session that used `rush:design`.
99
+ */
100
+ skill?: string;
101
+ /**
102
+ * Only sessions that used a skill or slash-command owned by this plugin
103
+ * (#12), joined against session_resource_usage.plugin.
104
+ */
105
+ plugin?: string;
91
106
  }
92
107
  /** Open (or return the cached) sessions database, applying migrations as needed. */
93
108
  export declare function getDB(): Database.Database;