@phnx-labs/agents-cli 1.20.85 → 1.20.86

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.86
4
+
5
+ - **`agents sessions` now shows a Kimi session's todo list and its file-touching
6
+ tool calls.** Kimi writes its checklist with `TodoList` (items shaped
7
+ `{title, status}`, where finished is `done`) rather than Claude's `TodoWrite`
8
+ (`{content, status: "completed"}`), so the checklist registry matched nothing
9
+ and every Kimi session rendered with no todos — in the picker preview, the
10
+ session detail, and the `--active` fan-out that carries progress off remote
11
+ devices. Kimi also names the file argument `path` where Claude names it
12
+ `file_path`, so `Read`/`Write`/`Edit` calls summarized as a bare `Read ` with
13
+ no file. Both spellings are now handled, and the snapshot-checklist tool names
14
+ live in one exported registry (`SNAPSHOT_TODO_TOOLS`) that the picker and the
15
+ state engine share instead of each hardcoding its own pair. Source:
16
+ `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/lib/session/state.ts`,
17
+ `apps/cli/src/commands/sessions-picker.ts`.
18
+
19
+ - **`agents view` now shows Grok's default model (e.g. `grok-4.5`).** Claude,
20
+ Codex, Antigravity, and Kimi already filled the model column via their
21
+ catalogs; Grok was missing from `locateModelSource`, so
22
+ `resolveConfiguredModel` returned null and the column stayed blank. Grok has
23
+ no `settings.json` `model` field (its config is `config.toml` +
24
+ `models_cache.json`); the authoritative default is `grok models` →
25
+ `Default model: <id>`. The catalog extractor now spawns that command against
26
+ the version-home binary (skipping failed-download stubs) and flags the
27
+ default, so `agents view`, `agents view --json` (`configuredModel`), and the
28
+ other identity-cluster surfaces show it. Source: `apps/cli/src/lib/models.ts`,
29
+ `apps/cli/src/commands/models.ts`.
30
+
31
+ - **`agents events --limit 0` now reads the whole stream, and a capped read says
32
+ so.** `--limit` parsed as `Math.max(1, parseInt(raw) || 50)`, so `--limit 0`
33
+ collapsed back to `50` (`0 || 50`) and there was no way to read past the default
34
+ cap at all. The cap is applied after filtering and before the caller sees
35
+ anything, so every aggregation over `--json` silently ranked the newest 50
36
+ records instead of the matching set — measured against a real 7-day corpus of
37
+ 2,135 CLI failures in 9 classes, 8 of 9 ranks came out wrong with counts off by
38
+ roughly 100x, and nothing warned. `--limit 0` now means no cap (29,649 records
39
+ on a 30-day stream here, against 50 before), a truncated read prints
40
+ `Showing the newest 50 — more events matched. Pass --limit 0 for all.` (on
41
+ stderr under `--json`, so a `| jq` pipeline still receives clean JSON), and a
42
+ non-numeric, negative, or empty `--limit` exits 2 rather than quietly becoming
43
+ 50 — an empty one (`--limit "$LIMIT"` with the variable unset) would otherwise
44
+ have read as "no cap" and returned the whole stream unannounced.
45
+ Source: `apps/cli/src/commands/events.ts`, `apps/cli/tests/events-limit.test.ts`,
46
+ `apps/cli/docs/06-observability.md`.
47
+
48
+ - **Desktop notifications now show the current agents-cli mark, not the old
49
+ logo.** The menu-bar helper's app icon — the icon macOS puts on the left of
50
+ every notification banner it posts (the menu bar helper's own notices and every
51
+ `agents run --notify` finish notice) — was generated from the retired gradient
52
+ "A" logo, so notifications carried stale branding while the menu-bar status
53
+ item already used the new lowercase `a`. The shared master logo
54
+ (`assets/logo.png`) is now the current `a` mark, so the menu-bar helper, the
55
+ `agents computer` helper, and the keychain helper all regenerate their
56
+ `AppIcon.icns` from it on the next build. Source: `assets/logo.png`,
57
+ `apps/cli/menubar/scripts/build.sh`.
58
+
3
59
  ## 1.20.85
4
60
 
5
61
  - **`agents feed post` can now be mirrored to the systems you actually watch.**
package/dist/bin/agents CHANGED
Binary file
@@ -14,4 +14,20 @@
14
14
  * today's operational log live.
15
15
  */
16
16
  import type { Command } from 'commander';
17
+ /**
18
+ * Resolve `--limit` into a record cap. `0` means "no cap" — without it there is
19
+ * no way to read the whole stream, and any aggregation (group-by failure, count
20
+ * per module) silently ranks the newest 50 records instead of the real set.
21
+ * A non-numeric or negative value is a usage error, not a quiet fallback.
22
+ */
23
+ export declare function resolveEventsLimit(raw: string | undefined): number | undefined;
24
+ /**
25
+ * Cap `fetched` (read with `limit + 1`) to `limit`, reporting whether records
26
+ * were dropped. The caller announces the cap so a truncated read is never
27
+ * mistaken for the complete set.
28
+ */
29
+ export declare function capRecords<T>(fetched: T[], limit: number | undefined): {
30
+ records: T[];
31
+ truncated: boolean;
32
+ };
17
33
  export declare function registerEventsCommand(program: Command): void;
@@ -17,6 +17,33 @@ import chalk from 'chalk';
17
17
  import * as fs from 'fs';
18
18
  import { getLogsPath } from '../lib/events.js';
19
19
  import { readUnifiedEvents } from '../lib/event-stream.js';
20
+ /**
21
+ * Resolve `--limit` into a record cap. `0` means "no cap" — without it there is
22
+ * no way to read the whole stream, and any aggregation (group-by failure, count
23
+ * per module) silently ranks the newest 50 records instead of the real set.
24
+ * A non-numeric or negative value is a usage error, not a quiet fallback.
25
+ */
26
+ export function resolveEventsLimit(raw) {
27
+ const token = raw ?? '50';
28
+ // Number('') and Number(' ') are both 0, which would read as "no cap" — an
29
+ // empty --limit (an unset "$LIMIT" in a script) must be rejected, not silently
30
+ // turned into the unbounded read.
31
+ const value = token.trim() === '' ? NaN : Number(token);
32
+ if (!Number.isInteger(value) || value < 0) {
33
+ throw new RangeError(`Invalid --limit ${raw} — pass a whole number, or 0 for no cap.`);
34
+ }
35
+ return value === 0 ? undefined : value;
36
+ }
37
+ /**
38
+ * Cap `fetched` (read with `limit + 1`) to `limit`, reporting whether records
39
+ * were dropped. The caller announces the cap so a truncated read is never
40
+ * mistaken for the complete set.
41
+ */
42
+ export function capRecords(fetched, limit) {
43
+ if (limit === undefined || fetched.length <= limit)
44
+ return { records: fetched, truncated: false };
45
+ return { records: fetched.slice(0, limit), truncated: true };
46
+ }
20
47
  /** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. */
21
48
  function parseSince(s) {
22
49
  const m = s.match(/^(\d+)([smhdw])$/);
@@ -80,7 +107,7 @@ export function registerEventsCommand(program) {
80
107
  .option('--agent <name>', 'Only events tagged with this agent')
81
108
  .option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
82
109
  .option('--audit', 'Operational events only (skip agent activity)')
83
- .option('--limit <n>', 'Max records to show (default 50)', '50')
110
+ .option('--limit <n>', 'Max records to show; 0 for no cap (default 50)', '50')
84
111
  .option('--json', 'Output raw records as JSON')
85
112
  .option('-f, --follow', "Tail today's operational log live")
86
113
  .addHelpText('after', `
@@ -90,31 +117,41 @@ Examples:
90
117
  agents events --audit Operational events only (secrets / teams / ...)
91
118
  agents events --event pr.opened --since 7d
92
119
  agents events --module secrets Every secret accessed or revealed
93
- agents events -f Live tail (operational)`)
120
+ agents events -f Live tail (operational)
121
+ agents events --event pr.opened --since 30d --limit 0 --json
122
+ Every match — use --limit 0 whenever you
123
+ aggregate, or you rank only the newest 50`)
94
124
  .action(async (options) => {
95
125
  if (options.follow) {
96
126
  await followLog();
97
127
  return;
98
128
  }
99
- const limit = Math.max(1, parseInt(options.limit ?? '50', 10) || 50);
129
+ let limit;
100
130
  let startDate;
101
131
  try {
132
+ limit = resolveEventsLimit(options.limit);
102
133
  startDate = options.since ? parseSince(options.since) : undefined;
103
134
  }
104
135
  catch (err) {
105
136
  console.error(chalk.red(err.message));
106
137
  process.exit(2);
107
138
  }
108
- const records = readUnifiedEvents({
139
+ // Read one past the cap so we can tell a full result from a clipped one.
140
+ const fetched = readUnifiedEvents({
109
141
  startDate,
110
142
  eventTypes: options.event && options.event.length ? options.event : undefined,
111
143
  agent: options.agent,
112
144
  command: options.command,
113
145
  module: options.module,
114
- limit,
146
+ limit: limit === undefined ? undefined : limit + 1,
115
147
  includeActivity: !options.audit,
116
148
  });
149
+ const { records, truncated } = capRecords(fetched, limit);
150
+ const capNote = `Showing the newest ${limit} — more events matched. Pass --limit 0 for all.`;
117
151
  if (options.json) {
152
+ // Notice goes to stderr so `--json | jq` still receives clean JSON.
153
+ if (truncated)
154
+ console.error(chalk.yellow(capNote));
118
155
  console.log(JSON.stringify(records, null, 2));
119
156
  return;
120
157
  }
@@ -126,6 +163,8 @@ Examples:
126
163
  for (const r of records.slice().reverse())
127
164
  console.log(renderRow(r));
128
165
  console.log(chalk.gray(`\n${records.length} event(s). Log: ${getLogsPath()}`));
166
+ if (truncated)
167
+ console.log(chalk.yellow(capNote));
129
168
  });
130
169
  }
131
170
  /** commander repeatable-option collector. */
@@ -13,7 +13,7 @@ import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersion
13
13
  import { getModelCatalog, locateModelSource } from '../lib/models.js';
14
14
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
15
15
  import { wrapJoined } from './inspect.js';
16
- const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi'];
16
+ const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok'];
17
17
  /**
18
18
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
19
19
  * via a curl script). For these, fall back to the PATH binary and synthesize
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import chalk from 'chalk';
11
11
  import { truncate, humanDuration } from '../lib/format.js';
12
- import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
12
+ import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
13
13
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
14
14
  import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
15
15
  import { linearIssueUrl } from '../lib/session/linear.js';
@@ -312,9 +312,10 @@ function formatCompactPreview(events, session) {
312
312
  if (!planFile && p && /\/plans\/[^/]+\.md$/.test(p)) {
313
313
  planFile = p;
314
314
  }
315
- // Claude TodoWrite (`todos`) and Codex update_plan (`plan`) same source as
316
- // extractTodoProgress in the state engine. Prefer the most recent write.
317
- if (tool === 'TodoWrite' || tool === 'update_plan') {
315
+ // Every harness's checklist-snapshot tool (Claude TodoWrite, Kimi TodoList,
316
+ // Codex update_plan, …) — the same registry the state engine folds through
317
+ // extractTodoProgress. Prefer the most recent write.
318
+ if (SNAPSHOT_TODO_TOOLS.has(tool)) {
318
319
  const progress = extractTodoProgress(event.args);
319
320
  if (progress)
320
321
  latestTodos = progress;
@@ -6,7 +6,7 @@
6
6
  <dict>
7
7
  <key>Resources/AppIcon.icns</key>
8
8
  <data>
9
- Vd5nfogg74zSRNspluv4uvDecUA=
9
+ DFq5H08EkhgWIC3UvGMR9B58BZw=
10
10
  </data>
11
11
  </dict>
12
12
  <key>files2</key>
@@ -15,7 +15,7 @@
15
15
  <dict>
16
16
  <key>hash2</key>
17
17
  <data>
18
- DznVe0VgYOux7+B/aiHNgGYLCvSJKzgXDTPJn2jNkNg=
18
+ mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
19
19
  </data>
20
20
  </dict>
21
21
  </dict>
@@ -57,6 +57,27 @@ export interface ModelSource {
57
57
  * Returns null if nothing usable is found.
58
58
  */
59
59
  export declare function locateModelSource(agent: AgentId, version: string): ModelSource | null;
60
+ /**
61
+ * Parse `grok models` stdout into a catalog. Exported for unit tests.
62
+ *
63
+ * Output shape (verified 0.2.118):
64
+ * You are logged in with grok.com.
65
+ *
66
+ * Default model: grok-4.5
67
+ *
68
+ * Available models:
69
+ * * grok-4.5 (default)
70
+ *
71
+ * The `Default model:` line is authoritative; rows may also carry a leading `*`
72
+ * and a `(default)` flag. Grok has no `--json` on this subcommand. Settings live
73
+ * in `config.toml` / `models_cache.json`, not `settings.json`, so the native
74
+ * settings.json reader cannot surface the default — the catalog is the source
75
+ * that makes `resolveConfiguredModel` return a cli-default for Grok.
76
+ */
77
+ export declare function parseGrokModelsStdout(stdout: string): {
78
+ models: ModelInfo[];
79
+ aliases: Record<string, string>;
80
+ };
60
81
  /**
61
82
  * Build (or load from cache) the model catalog for a specific (agent, version).
62
83
  * Cache is keyed on source-file mtime (binary or js module), so re-extracts
@@ -3,15 +3,15 @@
3
3
  *
4
4
  * Each agent ships its model list differently -- Claude and Codex embed it in
5
5
  * compiled bundles/binaries, Gemini exports it from a JS module, and OpenCode/
6
- * Cursor/OpenClaw expose it via CLI commands. This module provides a unified
7
- * `getModelCatalog()` and `resolveModel()` interface over all of them, backed
8
- * by a file-system cache keyed on source mtime.
6
+ * Cursor/OpenClaw/Antigravity/Kimi/Grok expose it via CLI commands. This
7
+ * module provides a unified `getModelCatalog()` and `resolveModel()` interface
8
+ * over all of them, backed by a file-system cache keyed on source mtime.
9
9
  */
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { execFileSync } from 'child_process';
13
13
  import chalk from 'chalk';
14
- import { getVersionDir, getVersionHomePath } from './versions.js';
14
+ import { getVersionDir, getVersionHomePath, getBinaryPath } from './versions.js';
15
15
  import { getModelsCachePath } from './state.js';
16
16
  import { agentConfigDirName } from './agents.js';
17
17
  import { resolveRunDefaults } from './run-defaults.js';
@@ -188,6 +188,36 @@ export function locateModelSource(agent, version) {
188
188
  return { path: pathBin, kind: 'cli' };
189
189
  return null;
190
190
  }
191
+ if (agent === 'grok') {
192
+ // Grok ships a native binary under the version home's `.grok/downloads/`,
193
+ // not node_modules/.bin. Prefer a real binary over a failed-download stub
194
+ // (a 99-byte placeholder sometimes left beside a prior good download).
195
+ const preferred = getBinaryPath('grok', version);
196
+ if (isUsableGrokBinary(preferred))
197
+ return { path: preferred, kind: 'cli' };
198
+ const downloads = path.join(getVersionHomePath('grok', version), '.grok', 'downloads');
199
+ try {
200
+ const candidates = fs
201
+ .readdirSync(downloads)
202
+ .filter((e) => e.startsWith('grok-'))
203
+ .map((e) => path.join(downloads, e))
204
+ .filter(isUsableGrokBinary)
205
+ .sort((a, b) => {
206
+ try {
207
+ return fs.statSync(b).size - fs.statSync(a).size;
208
+ }
209
+ catch {
210
+ return 0;
211
+ }
212
+ });
213
+ if (candidates[0])
214
+ return { path: candidates[0], kind: 'cli' };
215
+ }
216
+ catch {
217
+ /* empty downloads */
218
+ }
219
+ return null;
220
+ }
191
221
  if (agent === 'cursor') {
192
222
  // cursor-agent is installed via curl script, not agents-cli. Version argument
193
223
  // is accepted for API symmetry but ignored -- cursor lives on PATH.
@@ -198,6 +228,16 @@ export function locateModelSource(agent, version) {
198
228
  }
199
229
  return null;
200
230
  }
231
+ /** Real Grok binaries are ~100MB+; failed-download stubs are tens of bytes. */
232
+ function isUsableGrokBinary(filePath) {
233
+ try {
234
+ const st = fs.statSync(filePath);
235
+ return st.isFile() && st.size > 1024 * 1024;
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
201
241
  /** Search PATH for a command and return its absolute path, or null. */
202
242
  function findOnPath(command) {
203
243
  const pathEnv = process.env.PATH || '';
@@ -679,6 +719,93 @@ function extractAntigravityCatalog(binaryPath) {
679
719
  }
680
720
  return { models, aliases: {} };
681
721
  }
722
+ /**
723
+ * Parse `grok models` stdout into a catalog. Exported for unit tests.
724
+ *
725
+ * Output shape (verified 0.2.118):
726
+ * You are logged in with grok.com.
727
+ *
728
+ * Default model: grok-4.5
729
+ *
730
+ * Available models:
731
+ * * grok-4.5 (default)
732
+ *
733
+ * The `Default model:` line is authoritative; rows may also carry a leading `*`
734
+ * and a `(default)` flag. Grok has no `--json` on this subcommand. Settings live
735
+ * in `config.toml` / `models_cache.json`, not `settings.json`, so the native
736
+ * settings.json reader cannot surface the default — the catalog is the source
737
+ * that makes `resolveConfiguredModel` return a cli-default for Grok.
738
+ */
739
+ export function parseGrokModelsStdout(stdout) {
740
+ // Strip ANSI in case a spinner or color codes slip through.
741
+ // eslint-disable-next-line no-control-regex
742
+ const plain = stdout.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
743
+ let defaultId = null;
744
+ const defaultLine = plain.match(/Default model:\s*(\S+)/i);
745
+ if (defaultLine)
746
+ defaultId = defaultLine[1];
747
+ const models = [];
748
+ const seen = new Set();
749
+ for (const raw of plain.split('\n')) {
750
+ const line = raw.trim();
751
+ if (!line)
752
+ continue;
753
+ // Rows: "* grok-4.5 (default)" or "grok-4.5" or " grok-code-fast-1"
754
+ const m = line.match(/^\*?\s*([A-Za-z0-9][A-Za-z0-9._-]*)(?:\s+\(([^)]*)\))?\s*$/);
755
+ if (!m)
756
+ continue;
757
+ const id = m[1];
758
+ // Real model ids are grok-* (or match the Default model: line). Skip banner words.
759
+ if (!/^grok[-_]/i.test(id) && id !== defaultId)
760
+ continue;
761
+ if (seen.has(id))
762
+ continue;
763
+ seen.add(id);
764
+ const flags = (m[2] ?? '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
765
+ models.push({
766
+ id,
767
+ isDefault: (defaultId != null && id === defaultId) || flags.includes('default'),
768
+ });
769
+ }
770
+ // If Default model was set but did not appear as a row, still surface it.
771
+ if (defaultId && !seen.has(defaultId)) {
772
+ models.unshift({ id: defaultId, isDefault: true });
773
+ }
774
+ // Normalize: exactly one default when we know the Default model: id.
775
+ if (defaultId) {
776
+ for (const model of models)
777
+ model.isDefault = model.id === defaultId;
778
+ }
779
+ else if (models.length > 0 && !models.some((model) => model.isDefault)) {
780
+ models[0].isDefault = true;
781
+ }
782
+ return { models, aliases: {} };
783
+ }
784
+ /** Extract Grok's catalog via `grok models` (see parseGrokModelsStdout). */
785
+ function extractGrokCatalog(binaryPath) {
786
+ const env = { ...process.env };
787
+ // Point GROK_HOME at the version home that owns this binary so auth +
788
+ // models_cache come from the right install, not a host ~/.grok symlink.
789
+ // binary: <home>/.grok/downloads/grok-<ver>-...
790
+ const downloadsDir = path.dirname(binaryPath);
791
+ if (path.basename(downloadsDir) === 'downloads') {
792
+ env.GROK_HOME = path.dirname(downloadsDir);
793
+ }
794
+ let stdout;
795
+ try {
796
+ stdout = execFileSync(binaryPath, ['models'], {
797
+ encoding: 'utf-8',
798
+ stdio: ['ignore', 'pipe', 'ignore'],
799
+ timeout: 15_000,
800
+ maxBuffer: 8 * 1024 * 1024,
801
+ env,
802
+ });
803
+ }
804
+ catch {
805
+ return { models: [], aliases: {} };
806
+ }
807
+ return parseGrokModelsStdout(stdout);
808
+ }
682
809
  /**
683
810
  * Extract Kimi's catalog via `kimi provider list --json`, which emits the raw
684
811
  * providers/models config. Model ids are the `models` object keys (e.g.
@@ -789,6 +916,8 @@ export function getModelCatalog(agent, version) {
789
916
  ({ models, aliases } = extractAntigravityCatalog(src.path));
790
917
  else if (agent === 'kimi')
791
918
  ({ models, aliases } = extractKimiCatalog(src.path));
919
+ else if (agent === 'grok')
920
+ ({ models, aliases } = extractGrokCatalog(src.path));
792
921
  }
793
922
  const catalog = {
794
923
  agent,
@@ -6,7 +6,7 @@
6
6
  <dict>
7
7
  <key>Resources/AppIcon.icns</key>
8
8
  <data>
9
- Vd5nfogg74zSRNspluv4uvDecUA=
9
+ DFq5H08EkhgWIC3UvGMR9B58BZw=
10
10
  </data>
11
11
  </dict>
12
12
  <key>files2</key>
@@ -15,7 +15,7 @@
15
15
  <dict>
16
16
  <key>hash2</key>
17
17
  <data>
18
- DznVe0VgYOux7+B/aiHNgGYLCvSJKzgXDTPJn2jNkNg=
18
+ mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
19
19
  </data>
20
20
  </dict>
21
21
  <key>embedded.provisionprofile</key>
@@ -27,6 +27,17 @@ export declare function safeReadSessionFile(filePath: string, maxBytes?: number)
27
27
  export declare function parseSession(filePath: string, agent?: SessionAgentId): SessionEvent[];
28
28
  /** Infer the agent type from a session file path using known directory conventions. */
29
29
  export declare function detectAgent(filePath: string): SessionAgentId | null;
30
+ /**
31
+ * Checklist-snapshot tool names across harnesses — each sends the WHOLE list on
32
+ * every write, so the last call is the current checklist. Claude `TodoWrite`,
33
+ * Kimi `TodoList`, Droid/OpenCode `todo_write`, Codex `update_plan`.
34
+ */
35
+ export declare const SNAPSHOT_TODO_TOOLS: Set<string>;
36
+ /**
37
+ * Whether a harness's checklist status means "finished". Claude/Codex write
38
+ * `completed`; Kimi writes `done`.
39
+ */
40
+ export declare function isCompletedTodoStatus(status: unknown): boolean;
30
41
  /**
31
42
  * Summarize a tool_use into a one-liner string.
32
43
  */
@@ -205,6 +205,19 @@ export function detectAgent(filePath) {
205
205
  return 'gemini';
206
206
  return null;
207
207
  }
208
+ /**
209
+ * Checklist-snapshot tool names across harnesses — each sends the WHOLE list on
210
+ * every write, so the last call is the current checklist. Claude `TodoWrite`,
211
+ * Kimi `TodoList`, Droid/OpenCode `todo_write`, Codex `update_plan`.
212
+ */
213
+ export const SNAPSHOT_TODO_TOOLS = new Set(['TodoWrite', 'TodoList', 'todo_write', 'update_plan']);
214
+ /**
215
+ * Whether a harness's checklist status means "finished". Claude/Codex write
216
+ * `completed`; Kimi writes `done`.
217
+ */
218
+ export function isCompletedTodoStatus(status) {
219
+ return status === 'completed' || status === 'done';
220
+ }
208
221
  /**
209
222
  * Summarize a tool_use into a one-liner string.
210
223
  */
@@ -214,12 +227,13 @@ export function summarizeToolUse(tool, args) {
214
227
  switch (tool) {
215
228
  case 'Bash':
216
229
  return `Bash: ${truncate(String(args.command || '').replace(/\n/g, ' ').trim(), 120)}`;
230
+ // `path` is the Kimi spelling of Claude's `file_path` for the same tools.
217
231
  case 'Read':
218
- return `Read ${shortenPath(args.file_path || '')}`;
232
+ return `Read ${shortenPath(args.file_path || args.path || '')}`;
219
233
  case 'Write':
220
- return `Write ${shortenPath(args.file_path || '')}`;
234
+ return `Write ${shortenPath(args.file_path || args.path || '')}`;
221
235
  case 'Edit':
222
- return `Edit ${shortenPath(args.file_path || '')}`;
236
+ return `Edit ${shortenPath(args.file_path || args.path || '')}`;
223
237
  case 'Glob':
224
238
  return `Glob ${args.pattern || ''}`;
225
239
  case 'Grep':
@@ -234,14 +248,17 @@ export function summarizeToolUse(tool, args) {
234
248
  const steps = Array.isArray(args.plan) ? args.plan.length : 0;
235
249
  return `Plan: ${steps} step${steps === 1 ? '' : 's'}`;
236
250
  }
237
- // Claude's live checklist: show progress + the current step, not a bare "TodoWrite".
238
- case 'TodoWrite': {
251
+ // Live checklist: show progress + the current step, not a bare "TodoWrite".
252
+ // Claude writes `TodoWrite`, Kimi writes `TodoList`; both carry the whole list
253
+ // under `todos`, with Kimi spelling the item text `title` and "done" `done`.
254
+ case 'TodoWrite':
255
+ case 'TodoList': {
239
256
  const todos = Array.isArray(args.todos) ? args.todos : [];
240
257
  if (todos.length === 0)
241
258
  return 'Plan: 0 steps';
242
- const done = todos.filter((t) => t?.status === 'completed').length;
259
+ const done = todos.filter((t) => isCompletedTodoStatus(t?.status)).length;
243
260
  const active = todos.find((t) => t?.status === 'in_progress');
244
- const step = active?.activeForm || active?.content;
261
+ const step = active?.activeForm || active?.content || active?.title;
245
262
  return step
246
263
  ? `Plan ${done}/${todos.length}: ${truncate(String(step), 80)}`
247
264
  : `Plan: ${done}/${todos.length} done`;
@@ -119,11 +119,12 @@ export interface StateContext {
119
119
  activeWindowMs?: number;
120
120
  }
121
121
  /**
122
- * Derive live plan progress from a checklist tool call's args. Accepts both
123
- * Claude's `TodoWrite` (`todos: [{content,status,activeForm}]`) and Codex's
124
- * `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the single source
125
- * of checklist state for every agent. Returns undefined when there is no usable
126
- * list, so a session with no plan carries no `todos` field.
122
+ * Derive live plan progress from a checklist tool call's args. Accepts Claude's
123
+ * `TodoWrite` (`todos: [{content,status,activeForm}]`), Kimi's `TodoList`
124
+ * (`todos: [{title,status}]`, where finished is `done` rather than `completed`)
125
+ * and Codex's `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the
126
+ * single source of checklist state for every agent. Returns undefined when there
127
+ * is no usable list, so a session with no plan carries no `todos` field.
127
128
  */
128
129
  export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
129
130
  /** Fold snapshot checklist tools and Claude TaskCreate/TaskUpdate event logs. */
@@ -15,7 +15,7 @@
15
15
  * shape + mtime — same function, driven off the normalized events.
16
16
  */
17
17
  import * as path from 'path';
18
- import { summarizeToolUse } from './parse.js';
18
+ import { isCompletedTodoStatus, SNAPSHOT_TODO_TOOLS, summarizeToolUse } from './parse.js';
19
19
  /**
20
20
  * Detect per-session rate-limit / usage-limit signals in assistant or error
21
21
  * text (RUSH-1523). Matches the same shapes Factory's prewarm detectBlockingPrompt
@@ -48,15 +48,15 @@ const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
48
48
  /** Claude tool names that structurally mean "the agent handed control back to you". */
49
49
  const PLAN_TOOL = 'ExitPlanMode';
50
50
  const ASK_TOOL = 'AskUserQuestion';
51
- const SNAPSHOT_TODO_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan']);
52
51
  const TASK_CREATE_TOOL = 'TaskCreate';
53
52
  const TASK_UPDATE_TOOL = 'TaskUpdate';
54
53
  /**
55
- * Derive live plan progress from a checklist tool call's args. Accepts both
56
- * Claude's `TodoWrite` (`todos: [{content,status,activeForm}]`) and Codex's
57
- * `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the single source
58
- * of checklist state for every agent. Returns undefined when there is no usable
59
- * list, so a session with no plan carries no `todos` field.
54
+ * Derive live plan progress from a checklist tool call's args. Accepts Claude's
55
+ * `TodoWrite` (`todos: [{content,status,activeForm}]`), Kimi's `TodoList`
56
+ * (`todos: [{title,status}]`, where finished is `done` rather than `completed`)
57
+ * and Codex's `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the
58
+ * single source of checklist state for every agent. Returns undefined when there
59
+ * is no usable list, so a session with no plan carries no `todos` field.
60
60
  */
61
61
  export function extractTodoProgress(args) {
62
62
  const input = args?.input && typeof args.input === 'object' ? args.input : args;
@@ -76,10 +76,16 @@ export function extractTodoProgress(args) {
76
76
  ? t.text
77
77
  : typeof t?.step === 'string' && t.step
78
78
  ? t.step
79
- : activeForm ?? '';
79
+ : typeof t?.title === 'string' && t.title
80
+ ? t.title
81
+ : activeForm ?? '';
80
82
  if (!content)
81
83
  continue;
82
- const status = t?.status === 'completed' || t?.status === 'in_progress' ? t.status : 'pending';
84
+ const status = isCompletedTodoStatus(t?.status)
85
+ ? 'completed'
86
+ : t?.status === 'in_progress'
87
+ ? 'in_progress'
88
+ : 'pending';
83
89
  const description = typeof t?.description === 'string' && t.description ? t.description : undefined;
84
90
  items.push({ content, status, ...(description ? { description } : {}), ...(activeForm ? { activeForm } : {}) });
85
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.85",
3
+ "version": "1.20.86",
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",