@phnx-labs/agents-cli 1.22.6 → 1.22.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +35 -4
  5. package/dist/commands/feed.d.ts +2 -1
  6. package/dist/commands/feed.js +47 -20
  7. package/dist/commands/focus.js +22 -1
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/projects.js +122 -107
  10. package/dist/commands/secrets.js +1 -1
  11. package/dist/commands/sessions-backfill.d.ts +33 -0
  12. package/dist/commands/sessions-backfill.js +83 -1
  13. package/dist/commands/sessions-stats.d.ts +36 -0
  14. package/dist/commands/sessions-stats.js +263 -0
  15. package/dist/commands/sessions.d.ts +1 -1
  16. package/dist/commands/sessions.js +21 -1
  17. package/dist/commands/view.d.ts +2 -1
  18. package/dist/commands/view.js +62 -11
  19. package/dist/index.js +1 -2
  20. package/dist/lib/activity.js +2 -2
  21. package/dist/lib/agents.js +68 -11
  22. package/dist/lib/analytics/recipes.js +11 -5
  23. package/dist/lib/browser/profiles.d.ts +15 -7
  24. package/dist/lib/browser/profiles.js +53 -12
  25. package/dist/lib/byok-usage.d.ts +38 -0
  26. package/dist/lib/byok-usage.js +117 -0
  27. package/dist/lib/capabilities.js +1 -1
  28. package/dist/lib/exec.d.ts +20 -0
  29. package/dist/lib/exec.js +73 -8
  30. package/dist/lib/feed-outcome.d.ts +1 -0
  31. package/dist/lib/feed-outcome.js +2 -0
  32. package/dist/lib/feed-post.js +1 -1
  33. package/dist/lib/feed-ranking.d.ts +1 -0
  34. package/dist/lib/feed-ranking.js +4 -0
  35. package/dist/lib/feed.d.ts +4 -1
  36. package/dist/lib/feed.js +25 -0
  37. package/dist/lib/hosts/passthrough.js +0 -1
  38. package/dist/lib/mcp.js +6 -1
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  40. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  41. package/dist/lib/model-tiers.js +4 -1
  42. package/dist/lib/models.js +63 -0
  43. package/dist/lib/profiles.d.ts +42 -1
  44. package/dist/lib/profiles.js +50 -2
  45. package/dist/lib/project-focus.d.ts +9 -0
  46. package/dist/lib/project-focus.js +23 -0
  47. package/dist/lib/project-key.d.ts +1 -1
  48. package/dist/lib/project-key.js +1 -1
  49. package/dist/lib/project-probe.d.ts +18 -0
  50. package/dist/lib/project-probe.js +46 -0
  51. package/dist/lib/project-status.d.ts +56 -0
  52. package/dist/lib/project-status.js +125 -18
  53. package/dist/lib/resources/mcp.js +3 -0
  54. package/dist/lib/resources/types.d.ts +1 -1
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/audit.js +1 -1
  58. package/dist/lib/secrets/index.d.ts +1 -0
  59. package/dist/lib/secrets/index.js +29 -15
  60. package/dist/lib/secrets/remote.d.ts +1 -1
  61. package/dist/lib/secrets/remote.js +1 -1
  62. package/dist/lib/session/active.d.ts +2 -0
  63. package/dist/lib/session/bash-command.d.ts +2 -3
  64. package/dist/lib/session/db.d.ts +92 -1
  65. package/dist/lib/session/db.js +230 -1
  66. package/dist/lib/session/digest.d.ts +1 -1
  67. package/dist/lib/session/digest.js +1 -1
  68. package/dist/lib/share/publish.js +24 -0
  69. package/dist/lib/startup/command-registry.d.ts +0 -1
  70. package/dist/lib/startup/command-registry.js +0 -2
  71. package/dist/lib/subagents-registry.js +3 -0
  72. package/dist/lib/types.d.ts +11 -2
  73. package/dist/lib/usage.d.ts +5 -0
  74. package/dist/lib/usage.js +3 -3
  75. package/package.json +1 -1
  76. package/dist/commands/activity.d.ts +0 -87
  77. package/dist/commands/activity.js +0 -346
@@ -25,7 +25,14 @@ const DB_PATH = getSessionsDbPath();
25
25
  /** Current schema version; bumped when migrations are added. Exported so tests
26
26
  * assert against the constant instead of hardcoding a number that every bump
27
27
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
28
- export const SCHEMA_VERSION = 30;
28
+ export const SCHEMA_VERSION = 31;
29
+ /**
30
+ * Bump to force `agents sessions backfill resources` to re-derive every
31
+ * session's skill/slash-command tallies on its next run (resource_scan_ledger
32
+ * rows with a lower version are treated as stale — the same mechanism
33
+ * TOOL_INDEX_VERSION gives the tool backfill).
34
+ */
35
+ export const RESOURCE_INDEX_VERSION = 1;
29
36
  /**
30
37
  * Canonicalize a file path for use as a scan_ledger key. The same physical
31
38
  * session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
@@ -238,6 +245,25 @@ CREATE TABLE IF NOT EXISTS session_resource_usage (
238
245
  );
239
246
  CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
240
247
  CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
248
+
249
+ -- One-shot historical backfill bookkeeping for session_resource_usage, mirroring
250
+ -- tool_scan_ledger. The normal incremental scan writes resource usage for every
251
+ -- session it (re)parses, but a session indexed before #12 shipped keeps a fresh
252
+ -- scan_ledger row and is never re-derived — so its skill/slash-command tallies
253
+ -- were never recorded. The "agents sessions backfill resources" command walks
254
+ -- history, re-parses each transcript from byte 0, and stamps coverage here
255
+ -- (mtime + size + extractor_version) so reruns skip completed transcripts. This
256
+ -- is a SCAN LEDGER, not a second copy of the usage data — the usage itself lives
257
+ -- only in session_resource_usage.
258
+ CREATE TABLE IF NOT EXISTS resource_scan_ledger (
259
+ session_id TEXT PRIMARY KEY,
260
+ file_path TEXT NOT NULL UNIQUE,
261
+ file_mtime_ms INTEGER NOT NULL,
262
+ file_size INTEGER NOT NULL,
263
+ extractor_version INTEGER NOT NULL,
264
+ indexed_at INTEGER NOT NULL,
265
+ resource_count INTEGER NOT NULL
266
+ );
241
267
  `;
242
268
  let dbInstance = null;
243
269
  /**
@@ -678,6 +704,23 @@ function migrateSchema(db, fromVersion) {
678
704
  if (!cols.has('used_computer'))
679
705
  db.exec(`ALTER TABLE sessions ADD COLUMN used_computer INTEGER`);
680
706
  }
707
+ if (fromVersion < 31) {
708
+ // v30 → v31: independent ledger for the explicit resource-usage backfill
709
+ // (agents sessions backfill resources). Do NOT wipe scan_ledger — normal
710
+ // session listing stays warm; historical resource rows are filled once on
711
+ // demand, exactly like the tool-index ledger (v25).
712
+ db.exec(`
713
+ CREATE TABLE IF NOT EXISTS resource_scan_ledger (
714
+ session_id TEXT PRIMARY KEY,
715
+ file_path TEXT NOT NULL UNIQUE,
716
+ file_mtime_ms INTEGER NOT NULL,
717
+ file_size INTEGER NOT NULL,
718
+ extractor_version INTEGER NOT NULL,
719
+ indexed_at INTEGER NOT NULL,
720
+ resource_count INTEGER NOT NULL
721
+ );
722
+ `);
723
+ }
681
724
  }
682
725
  /** Open (or return the cached) sessions database, applying migrations as needed. */
683
726
  export function getDB() {
@@ -1829,6 +1872,10 @@ function buildSessionWhere(options) {
1829
1872
  where.push('LOWER(IFNULL(project, \'\')) LIKE ?');
1830
1873
  params.push(`%${options.project.toLowerCase()}%`);
1831
1874
  }
1875
+ if (options.machine) {
1876
+ where.push('machine = ? COLLATE NOCASE');
1877
+ params.push(options.machine);
1878
+ }
1832
1879
  // id lookup. SQLite's LIKE is case-insensitive for ASCII, so a lowercased
1833
1880
  // pattern matches mixed-case ids; the `=` exact compare adds COLLATE NOCASE
1834
1881
  // for the same reason. short_id carries its own index (idx_sessions_short_id);
@@ -2006,6 +2053,188 @@ export function queryUsageRollup(options) {
2006
2053
  `;
2007
2054
  return db.prepare(sql).all(...params);
2008
2055
  }
2056
+ /**
2057
+ * Roll up skill / slash-command usage from session_resource_usage, joined to
2058
+ * `sessions` for attribution so the same filter shape as querySessions
2059
+ * (agent / project / since / machine) narrows WHICH sessions count. Grouped by
2060
+ * resource identity (kind + name + plugin + source) and ordered by invocation
2061
+ * volume — the read side of "which skills/commands do I actually use, and which
2062
+ * are dead weight". `order: 'bottom'` ranks least-used first (the one-time
2063
+ * skills); `limit` caps the returned rows.
2064
+ *
2065
+ * The signal only captures EXPLICIT invocations (slash commands and `Skill`
2066
+ * tool calls). An auto-triggered skill (loaded by description match) emits no
2067
+ * event, so it reads as zero here — a 0 means "never explicitly invoked", not
2068
+ * "never loaded". Skill invocations are recorded for Claude and Kimi (the
2069
+ * `Skill`-tool harnesses); slash-commands are Claude-only.
2070
+ *
2071
+ * `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
2072
+ * distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
2073
+ * — deliberately not routed through buildSessionWhere so `--plugin rush` shows
2074
+ * only rush's resources rather than every resource used by a rush-touching
2075
+ * session.
2076
+ */
2077
+ export function queryResourceUsageStats(options) {
2078
+ const db = getDB();
2079
+ const { clause, params } = buildSessionWhere(options);
2080
+ // buildSessionWhere emits bare column names (agent, timestamp, machine, …) and
2081
+ // `id IN (…)` subqueries; in this join they resolve unambiguously to `sessions`
2082
+ // (session_resource_usage carries none of those columns, and its key is
2083
+ // session_id, not id). Strip the leading WHERE so resource predicates append.
2084
+ const base = clause.replace(/^WHERE\s+/, '');
2085
+ const preds = base ? [base] : [];
2086
+ const allParams = [...params];
2087
+ if (options.kind) {
2088
+ preds.push('r.kind = ?');
2089
+ allParams.push(options.kind);
2090
+ }
2091
+ if (options.pluginFilter) {
2092
+ preds.push('r.plugin = ? COLLATE NOCASE');
2093
+ allParams.push(options.pluginFilter);
2094
+ }
2095
+ const whereClause = preds.length ? `WHERE ${preds.join(' AND ')}` : '';
2096
+ const direction = options.order === 'bottom' ? 'ASC' : 'DESC';
2097
+ const limitClause = options.limit ? `LIMIT ${Math.max(1, Math.floor(options.limit))}` : '';
2098
+ // Group by resource IDENTITY = (kind, name) only, per SES-IF-4b. name already
2099
+ // embeds the `plugin:short` prefix for a plugin resource, so (kind, name) is
2100
+ // the true identity. plugin and source are PROVENANCE, not identity, and drift
2101
+ // per session — the same `rush:design` resolves plugin='rush' in a session
2102
+ // whose cwd discovered the plugin and plugin=NULL in one that didn't, and the
2103
+ // layer a flat resource resolved to varies (user vs system). Grouping on either
2104
+ // would split one resource into fractional rows. Aggregate both with MAX so the
2105
+ // row carries a representative (non-NULL when any session resolved it) label
2106
+ // while the counts stay whole.
2107
+ const sql = `
2108
+ SELECT
2109
+ r.kind AS kind,
2110
+ r.name AS name,
2111
+ MAX(r.plugin) AS plugin,
2112
+ MAX(r.source) AS source,
2113
+ COUNT(DISTINCT r.session_id) AS sessions,
2114
+ SUM(r.count) AS invocations
2115
+ FROM session_resource_usage r
2116
+ JOIN sessions s ON s.id = r.session_id
2117
+ ${whereClause}
2118
+ GROUP BY r.kind, r.name
2119
+ ORDER BY invocations ${direction}, sessions ${direction}, r.name ASC
2120
+ ${limitClause}
2121
+ `;
2122
+ return db.prepare(sql).all(...allParams);
2123
+ }
2124
+ /**
2125
+ * Coverage of the resource-usage signal: how many distinct sessions carry any
2126
+ * row in session_resource_usage vs. the total indexed. A low ratio means the
2127
+ * historical backfill (`agents sessions backfill resources`) hasn't run — the
2128
+ * stats surface uses this to tell the user their zero-counts may just be
2129
+ * un-scanned history, not genuine non-use.
2130
+ */
2131
+ export function resourceUsageCoverage() {
2132
+ const db = getDB();
2133
+ const covered = db.prepare(`SELECT COUNT(DISTINCT session_id) AS n FROM session_resource_usage`).get().n;
2134
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM sessions`).get().n;
2135
+ return { covered, total };
2136
+ }
2137
+ /** Has this session's resource usage been derived at the current extractor version for this exact file? */
2138
+ function needsResourceIndex(db, sessionId, stamp) {
2139
+ const row = db
2140
+ .prepare(`SELECT file_mtime_ms, file_size, extractor_version FROM resource_scan_ledger WHERE session_id = ?`)
2141
+ .get(sessionId);
2142
+ return !row
2143
+ || row.file_mtime_ms !== stamp.fileMtimeMs
2144
+ || row.file_size !== stamp.fileSize
2145
+ || row.extractor_version !== RESOURCE_INDEX_VERSION;
2146
+ }
2147
+ /** Record that a session's resource usage is current at RESOURCE_INDEX_VERSION for this file stamp. */
2148
+ function stampResourceLedger(db, sessionId, filePath, stamp, resourceCount) {
2149
+ db.prepare(`
2150
+ INSERT INTO resource_scan_ledger
2151
+ (session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, resource_count)
2152
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2153
+ ON CONFLICT(session_id) DO UPDATE SET
2154
+ file_path = excluded.file_path,
2155
+ file_mtime_ms = excluded.file_mtime_ms,
2156
+ file_size = excluded.file_size,
2157
+ extractor_version = excluded.extractor_version,
2158
+ indexed_at = excluded.indexed_at,
2159
+ resource_count = excluded.resource_count
2160
+ `).run(sessionId, filePath, stamp.fileMtimeMs, stamp.fileSize, RESOURCE_INDEX_VERSION, Date.now(), resourceCount);
2161
+ }
2162
+ /**
2163
+ * One-shot historical backfill of session_resource_usage (#12). The normal
2164
+ * incremental scan writes resource usage only for sessions whose transcript it
2165
+ * (re)parses; a session indexed before this feature shipped keeps a fresh
2166
+ * scan_ledger row and is never re-derived, so its skill/slash-command tallies
2167
+ * were never recorded. This walks the session index, re-parses each transcript
2168
+ * FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
2169
+ * claude/codex re-derive their tallies from scratch), writes the usage, and
2170
+ * stamps resource_scan_ledger so reruns skip completed transcripts — the same
2171
+ * independent-ledger shape `agents sessions backfill tools` uses.
2172
+ *
2173
+ * Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
2174
+ * every agent uniformly, so there is no per-harness branch to keep in parity.
2175
+ * Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
2176
+ */
2177
+ export function backfillResourceUsage(filter = {}, onProgress) {
2178
+ const db = getDB();
2179
+ // No LIMIT: the backfill covers the whole matching history. skipExistenceCheck
2180
+ // stays off so vanished transcripts are dropped, matching querySessions.
2181
+ const sessions = querySessions({ ...filter, limit: undefined });
2182
+ const result = { scanned: 0, updated: 0, skipped: 0, failed: 0, resourceRows: 0 };
2183
+ let done = 0;
2184
+ for (const meta of sessions) {
2185
+ if (!meta.filePath) {
2186
+ done++;
2187
+ onProgress?.(done, sessions.length);
2188
+ continue;
2189
+ }
2190
+ result.scanned++;
2191
+ let stamp;
2192
+ try {
2193
+ const st = fs.statSync(meta.filePath);
2194
+ stamp = { fileMtimeMs: st.mtimeMs, fileSize: st.size };
2195
+ }
2196
+ catch {
2197
+ result.failed++;
2198
+ done++;
2199
+ onProgress?.(done, sessions.length);
2200
+ continue;
2201
+ }
2202
+ if (!needsResourceIndex(db, meta.id, stamp)) {
2203
+ result.skipped++;
2204
+ done++;
2205
+ onProgress?.(done, sessions.length);
2206
+ continue;
2207
+ }
2208
+ try {
2209
+ const events = parseSession(meta.filePath, meta.agent);
2210
+ // Fail loud on a bad read. writeResourceUsage DELETEs the session's rows
2211
+ // before it re-inserts, so a transcript that parses to ZERO events (a
2212
+ // truncated / mid-sync / corrupt file — parseSession returns [] instead of
2213
+ // throwing) would silently wipe real historical usage and then stamp the
2214
+ // ledger "current", masking the loss. A completed historical transcript
2215
+ // always has turns, so an empty parse is never a legitimate "this session
2216
+ // used nothing" — it is an unreadable file. Skip it (count as failed, leave
2217
+ // the ledger unstamped) so a later good read retries.
2218
+ if (events.length === 0) {
2219
+ result.failed++;
2220
+ done++;
2221
+ onProgress?.(done, sessions.length);
2222
+ continue;
2223
+ }
2224
+ writeResourceUsage(meta.id, events, meta.cwd);
2225
+ const rows = db.prepare(`SELECT COUNT(*) AS n FROM session_resource_usage WHERE session_id = ?`).get(meta.id).n;
2226
+ stampResourceLedger(db, meta.id, meta.filePath, stamp, rows);
2227
+ result.updated++;
2228
+ result.resourceRows += rows;
2229
+ }
2230
+ catch {
2231
+ result.failed++;
2232
+ }
2233
+ done++;
2234
+ onProgress?.(done, sessions.length);
2235
+ }
2236
+ return result;
2237
+ }
2009
2238
  /**
2010
2239
  * Map every team name to the session that ran `agents teams create/add` for it.
2011
2240
  *
@@ -45,7 +45,7 @@ export declare function toolHistogram(toolCounts: Record<string, number>, top?:
45
45
  /**
46
46
  * Aggregate recognized external tools invoked via Bash across a session.
47
47
  * Unknown one-off commands are bucketed as `other` so the ranking surface
48
- * (e.g. `agents activity --tools`) only surfaces repeatable tools.
48
+ * only surfaces repeatable tools.
49
49
  */
50
50
  export declare function bashToolCounts(events: SessionEvent[]): Record<string, number>;
51
51
  export interface TestResult {
@@ -131,7 +131,7 @@ export function toolHistogram(toolCounts, top = 8) {
131
131
  /**
132
132
  * Aggregate recognized external tools invoked via Bash across a session.
133
133
  * Unknown one-off commands are bucketed as `other` so the ranking surface
134
- * (e.g. `agents activity --tools`) only surfaces repeatable tools.
134
+ * only surfaces repeatable tools.
135
135
  */
136
136
  export function bashToolCounts(events) {
137
137
  const counts = {};
@@ -81,6 +81,30 @@ function guessContentType(filePath) {
81
81
  return 'application/json';
82
82
  if (/\.svg$/i.test(filePath))
83
83
  return 'image/svg+xml';
84
+ // Raster images + video: agents publish screenshots and screen recordings as PR
85
+ // evidence, and GitHub's image proxy (camo) only renders an inline `![](url)` when
86
+ // the asset is served with a real image/video content-type — octet-stream is
87
+ // refused. Type them so the share URL embeds instead of downloading.
88
+ if (/\.png$/i.test(filePath))
89
+ return 'image/png';
90
+ if (/\.jpe?g$/i.test(filePath))
91
+ return 'image/jpeg';
92
+ if (/\.gif$/i.test(filePath))
93
+ return 'image/gif';
94
+ if (/\.webp$/i.test(filePath))
95
+ return 'image/webp';
96
+ if (/\.avif$/i.test(filePath))
97
+ return 'image/avif';
98
+ if (/\.ico$/i.test(filePath))
99
+ return 'image/x-icon';
100
+ if (/\.mp4$/i.test(filePath))
101
+ return 'video/mp4';
102
+ if (/\.mov$/i.test(filePath))
103
+ return 'video/quicktime';
104
+ if (/\.webm$/i.test(filePath))
105
+ return 'video/webm';
106
+ if (/\.pdf$/i.test(filePath))
107
+ return 'application/pdf';
84
108
  if (/\.txt$|\.md$/i.test(filePath))
85
109
  return 'text/plain; charset=utf-8';
86
110
  return 'application/octet-stream';
@@ -97,7 +97,6 @@ export declare const loadCloud: ModuleLoader;
97
97
  export declare const loadMessage: ModuleLoader;
98
98
  export declare const loadSend: ModuleLoader;
99
99
  export declare const loadFeed: ModuleLoader;
100
- export declare const loadActivity: ModuleLoader;
101
100
  export declare const loadMailboxes: ModuleLoader;
102
101
  export declare const loadServe: ModuleLoader;
103
102
  export declare const loadShare: ModuleLoader;
@@ -75,7 +75,6 @@ export const loadCloud = async () => (await import('../../commands/cloud.js')).r
75
75
  export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
76
76
  export const loadSend = async () => (await import('../../commands/send.js')).registerSendCommand;
77
77
  export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
78
- export const loadActivity = async () => (await import('../../commands/activity.js')).registerActivityCommand;
79
78
  export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
80
79
  export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
81
80
  export const loadShare = async () => (await import('../../commands/share.js')).registerShareCommands;
@@ -199,7 +198,6 @@ export const COMMAND_LOADERS = {
199
198
  send: [loadSend],
200
199
  notify: [loadSend],
201
200
  feed: [loadFeed],
202
- activity: [loadActivity],
203
201
  mailboxes: [loadMailboxes],
204
202
  mailbox: [loadMailboxes],
205
203
  serve: [loadServe],
@@ -252,6 +252,9 @@ export const SUBAGENT_TARGETS = {
252
252
  // Tier 1 -- flat markdown, Claude-compatible flatten.
253
253
  claude: flatFile({ subdir: ['.claude', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
254
254
  grok: flatFile({ subdir: ['.grok', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
255
+ // Oh My Pi discovers Claude-shaped agent markdown from ~/.omp/agent/agents/
256
+ // (`omp agents unpack` writes there); frontmatter name/description + body.
257
+ pi: flatFile({ subdir: ['.omp', 'agent', 'agents'], ext: '.md', transform: transformSubagentForClaude, readMeta: metaFrontmatterFallback }),
255
258
  droid: flatFile({ subdir: ['.factory', 'droids'], ext: '.md', transform: transformSubagentForDroid }),
256
259
  // Bespoke frontmatter/format, still one flat file.
257
260
  codex: flatFile({ subdir: ['.codex', 'agents'], ext: '.toml', transform: transformSubagentForCodex }),
@@ -8,7 +8,7 @@
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  import type { FeedBroadcastConfig } from './feed-broadcast.js';
10
10
  /** Unique identifier for a current or legacy AI coding agent. */
11
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
11
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
12
12
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
13
13
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
14
14
  /**
@@ -196,6 +196,15 @@ export interface AgentConfig {
196
196
  * rules file (inline all @-imports) when syncing it into the version home.
197
197
  */
198
198
  rulesImports?: boolean;
199
+ /**
200
+ * Whether the agent can open an interactive REPL session when launched with
201
+ * NO prompt (bare invocation). Agents whose CLI exits immediately without a
202
+ * prompt (e.g. cursor-agent) must declare `false` here; agents that open a
203
+ * TUI/REPL with no args declare `true`. Used by the `auto` harness picker
204
+ * to avoid routing a prompt-less interactive run to a harness that would
205
+ * exit silently (RUSH-2185, EXEC-23a).
206
+ */
207
+ interactiveRepl?: Capability;
199
208
  };
200
209
  }
201
210
  /**
@@ -213,7 +222,7 @@ export type RulesCapability = false | {
213
222
  file: string;
214
223
  };
215
224
  /** Names of every gateable capability on AgentConfig. */
216
- export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows' | 'memory';
225
+ export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows' | 'memory' | 'interactiveRepl';
217
226
  /**
218
227
  * Permission modes controlling agent autonomy.
219
228
  * plan read-only investigation; no writes, no shell side-effects
@@ -177,6 +177,7 @@ export declare const USAGE_FETCH_CONCURRENCY = 3;
177
177
  * minutes; that re-fired too often when delayed responses stacked.
178
178
  */
179
179
  export declare const USAGE_CACHE_FRESH_MS: number;
180
+ export declare const USAGE_CACHE_SWR_MS: number;
180
181
  /**
181
182
  * Unified entry for every multi-account usage lookup (`agents view`, rotation,
182
183
  * JSON export). Deduplicates by usage identity, then fans out through the
@@ -491,6 +492,10 @@ export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageS
491
492
  * Returns true only when a valid access token can be obtained.
492
493
  */
493
494
  export declare function isClaudeAuthValid(home?: string): Promise<boolean>;
495
+ /** Render a colored block-character progress bar. */
496
+ export declare function renderBar(usedPercent: number, length: number, minimumVisible?: number): string;
497
+ /** Return a chalk color function based on the usage percentage threshold. */
498
+ export declare function getUsageColor(usedPercent: number): (text: string) => string;
494
499
  /** Parse the latest billing info from Grok's unified log. */
495
500
  declare function getGrokUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
496
501
  /** Per-model bucket in Cursor's /api/usage response. */
package/dist/lib/usage.js CHANGED
@@ -219,7 +219,7 @@ const USAGE_BG_REFRESH_CONCURRENCY = 2;
219
219
  * minutes; that re-fired too often when delayed responses stacked.
220
220
  */
221
221
  export const USAGE_CACHE_FRESH_MS = 5 * 60 * 1000;
222
- const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
222
+ export const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
223
223
  /**
224
224
  * Unified entry for every multi-account usage lookup (`agents view`, rotation,
225
225
  * JSON export). Deduplicates by usage identity, then fans out through the
@@ -1696,7 +1696,7 @@ function renderCompactUsageBar(usedPercent) {
1696
1696
  return renderBar(usedPercent, COMPACT_BAR_LEN, usedPercent > 0 ? 1 : 0);
1697
1697
  }
1698
1698
  /** Render a colored block-character progress bar. */
1699
- function renderBar(usedPercent, length, minimumVisible = 0) {
1699
+ export function renderBar(usedPercent, length, minimumVisible = 0) {
1700
1700
  const rounded = Math.round((usedPercent / 100) * length);
1701
1701
  const filled = Math.max(minimumVisible, Math.max(0, Math.min(length, rounded)));
1702
1702
  const color = getUsageColor(usedPercent);
@@ -1707,7 +1707,7 @@ function colorUsage(text, usedPercent) {
1707
1707
  return getUsageColor(usedPercent)(text);
1708
1708
  }
1709
1709
  /** Return a chalk color function based on the usage percentage threshold. */
1710
- function getUsageColor(usedPercent) {
1710
+ export function getUsageColor(usedPercent) {
1711
1711
  if (usedPercent >= 100)
1712
1712
  return chalk.red;
1713
1713
  if (usedPercent >= 80)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.6",
3
+ "version": "1.22.7",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,87 +0,0 @@
1
- /**
2
- * `agents activity` -- the agent-activity lane: a running stream of what agents
3
- * DID (plan created, PR opened, worktree created, sub-agent spawned, files
4
- * edited), read from the append-only per-session activity logs.
5
- *
6
- * Complements `agents feed` (what agents are WAITING on). Events are emitted at
7
- * hook time by 11-activity-log.py, so this reader never re-parses transcripts --
8
- * it tails the logs and collapses routine work to counts, surfacing milestones
9
- * individually and newest-first.
10
- *
11
- * Fleet-wide and project-grouped BY DEFAULT: the question this command answers
12
- * is "what are my agents doing", and agents run on every box, so a local-only
13
- * flat stream answered a narrower question than anyone asked. Each run fans the
14
- * same `activity --json` payload out across the fleet the way `agents feed`
15
- * does, merges every peer's stream host-tagged, joins each item to live sessions
16
- * (project / ticket / execution host), and buckets it by project -- one level,
17
- * no sub-grouping, with the machines named in each project's header.
18
- *
19
- * `--local` scopes back to this machine, `--host <box>` to named peers, and
20
- * `--flat` (or `--group-by none`) restores the single newest-first stream.
21
- */
22
- import type { Command } from 'commander';
23
- import { type ActivityGroupBy } from '../lib/activity.js';
24
- /** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
25
- export declare const ACTIVITY_NO_FANOUT_ENV = "AGENTS_ACTIVITY_LOCAL";
26
- interface ActivityOpts {
27
- json?: boolean;
28
- all?: boolean;
29
- milestones?: boolean;
30
- limit: number;
31
- since?: number;
32
- local?: boolean;
33
- host?: string[];
34
- device?: string[];
35
- devicesAll?: boolean;
36
- hostsAll?: boolean;
37
- groupBy?: string;
38
- flat?: boolean;
39
- project?: string;
40
- filter?: string;
41
- }
42
- /**
43
- * Does this hook-registration error concern one of the activity-log hooks?
44
- * `registerHooksToSettings` prefixes each error with the manifest hook name
45
- * (`<name>: script not found`), so the entry names are the discriminator.
46
- */
47
- export declare function isActivityHookError(error: string): boolean;
48
- /**
49
- * Is this error attributable to a named manifest hook at all? Errors that name
50
- * no hook — `Failed to parse settings.json`, `Failed to write
51
- * agents-cli-hooks.ts: …` — are agent-level aborts, not per-hook noise, and
52
- * must never be filtered out.
53
- */
54
- export declare function isManifestHookError(error: string, manifestNames: string[]): boolean;
55
- /**
56
- * One compact trailing note for peers that didn't answer the fan-out. Now that
57
- * every run dials the fleet, an offline box is routine -- but never silent: the
58
- * reader has to know the timeline is missing a machine, not that the machine
59
- * was idle. Empty string when everything answered.
60
- */
61
- export declare function formatUnreachableNote(skipped: string[]): string;
62
- /**
63
- * Resolve `--group-by` / `--flat` into the grouping dimension, or `undefined`
64
- * for a flat stream. Throws on an unknown value so the caller can exit 1 --
65
- * a mistyped dimension must not silently fall back to the default.
66
- */
67
- export declare function resolveActivityGrouping(opts: Pick<ActivityOpts, 'groupBy' | 'flat'>): ActivityGroupBy | undefined;
68
- /** Which machines an invocation reads from. Pure -- no SSH, no device registry. */
69
- export interface ActivityScope {
70
- /** Read this box's own activity logs. */
71
- includeLocal: boolean;
72
- /** Dial peers over SSH. */
73
- wantRemote: boolean;
74
- /** Peers to dial; `undefined` means "every reachable device". */
75
- remoteHosts?: string[];
76
- }
77
- /**
78
- * Decide the read scope. Fleet-wide is the DEFAULT -- agents run on every box,
79
- * so "what are my agents doing" is a fleet question -- and three things narrow
80
- * it: `--local`, an explicit `--host`/`--device` list, and the recursion guard
81
- * a peer answering a fan-out carries (else every peer would re-fan the fleet).
82
- * `--devices-all`/`--hosts-all` are kept as explicit no-ops for scripts that
83
- * already pass them.
84
- */
85
- export declare function resolveActivityScope(opts: Pick<ActivityOpts, 'local' | 'host'>, self: string, noFanoutEnv?: boolean): ActivityScope;
86
- export declare function registerActivityCommand(program: Command): void;
87
- export {};