@phnx-labs/agents-cli 1.20.58 → 1.20.59

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.
@@ -508,6 +508,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
508
508
  label,
509
509
  messageCount: scan.messageCount,
510
510
  tokenCount: scan.tokenCount,
511
+ outputTokens: scan.outputTokens,
511
512
  costUsd: scan.costUsd,
512
513
  durationMs: scan.durationMs,
513
514
  isTeamOrigin,
@@ -533,6 +534,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
533
534
  label,
534
535
  messageCount: scan.messageCount,
535
536
  tokenCount: scan.tokenCount,
537
+ outputTokens: scan.outputTokens,
536
538
  costUsd: scan.costUsd,
537
539
  durationMs: scan.durationMs,
538
540
  topic: scan.topic,
@@ -748,6 +750,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion) {
748
750
  topic: scan.topic,
749
751
  messageCount: scan.messageCount,
750
752
  tokenCount: scan.tokenCount,
753
+ outputTokens: scan.outputTokens,
751
754
  costUsd: scan.costUsd,
752
755
  durationMs: scan.durationMs,
753
756
  account: resolveAccount?.(),
@@ -873,6 +876,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
873
876
  let topic;
874
877
  let messageCount = 0;
875
878
  let tokenCount = 0;
879
+ let outputTokens = 0;
876
880
  let sawTokenCount = false;
877
881
  let costUsd = 0;
878
882
  let sawCost = false;
@@ -910,6 +914,16 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
910
914
  tokenCount += total;
911
915
  sawTokenCount = true;
912
916
  }
917
+ // Output tokens: sum directional generation fields per message (output +
918
+ // thoughts + tool), mirroring the cost path — never `tokens.total`, which
919
+ // may be cumulative and would double-count when summed.
920
+ const gtk = message.tokens;
921
+ if (gtk && typeof gtk === 'object') {
922
+ outputTokens +=
923
+ (typeof gtk.output === 'number' ? gtk.output : 0) +
924
+ (typeof gtk.thoughts === 'number' ? gtk.thoughts : 0) +
925
+ (typeof gtk.tool === 'number' ? gtk.tool : 0);
926
+ }
913
927
  // Per-message cost: directional tokens × this message's model price.
914
928
  const msgModel = (typeof message.model === 'string' ? message.model : undefined) || sessionModel;
915
929
  const tk = message.tokens;
@@ -944,6 +958,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
944
958
  topic,
945
959
  messageCount,
946
960
  tokenCount: sawTokenCount ? tokenCount : undefined,
961
+ outputTokens: sawTokenCount ? outputTokens : undefined,
947
962
  costUsd: sawCost ? costUsd : undefined,
948
963
  durationMs,
949
964
  };
@@ -1158,6 +1173,7 @@ async function scanOpenCodeIncremental() {
1158
1173
  s.time_updated AS time_updated,
1159
1174
  COALESCE(stats.message_count, 0) AS message_count,
1160
1175
  stats.token_count AS token_count,
1176
+ stats.output_tokens AS output_tokens,
1161
1177
  COALESCE(stats.has_token_data, 0) AS has_token_data
1162
1178
  FROM session s
1163
1179
  LEFT JOIN (
@@ -1171,6 +1187,7 @@ async function scanOpenCodeIncremental() {
1171
1187
  COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
1172
1188
  COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
1173
1189
  ) AS token_count,
1190
+ SUM(COALESCE(json_extract(data, '$.tokens.output'), 0)) AS output_tokens,
1174
1191
  MAX(CASE WHEN json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
1175
1192
  FROM message
1176
1193
  GROUP BY session_id
@@ -1194,6 +1211,7 @@ async function scanOpenCodeIncremental() {
1194
1211
  const timeUpdated = asInt(row.time_updated);
1195
1212
  const messageCount = asInt(row.message_count);
1196
1213
  const tokenCount = asInt(row.token_count);
1214
+ const outputTokens = asInt(row.output_tokens);
1197
1215
  const hasTokenData = asInt(row.has_token_data) === 1;
1198
1216
  const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
1199
1217
  // OpenCode is one shared DB, not one file per session — its row carries a
@@ -1215,6 +1233,7 @@ async function scanOpenCodeIncremental() {
1215
1233
  topic,
1216
1234
  messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
1217
1235
  tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
1236
+ outputTokens: hasTokenData && !Number.isNaN(outputTokens) ? outputTokens : undefined,
1218
1237
  };
1219
1238
  entries.push({ meta, content: topic || '', scan: currentScan });
1220
1239
  }
@@ -1653,6 +1672,7 @@ async function readDroidMeta(filePath, currentVersion) {
1653
1672
  topic: scan.topic,
1654
1673
  messageCount: scan.messageCount,
1655
1674
  tokenCount,
1675
+ outputTokens: settings.usage?.outputTokens,
1656
1676
  costUsd: costUsd > 0 ? costUsd : undefined,
1657
1677
  durationMs: scan.durationMs,
1658
1678
  };
@@ -1801,6 +1821,7 @@ export async function scanClaudeSession(filePath) {
1801
1821
  let entrypoint;
1802
1822
  let messageCount = 0;
1803
1823
  let tokenCount = 0;
1824
+ let outputTokens = 0;
1804
1825
  let sawTokenCount = false;
1805
1826
  let costUsd = 0;
1806
1827
  let sawCost = false;
@@ -1961,6 +1982,8 @@ export async function scanClaudeSession(filePath) {
1961
1982
  tokenCount += usage;
1962
1983
  sawTokenCount = true;
1963
1984
  }
1985
+ if (typeof usageObj?.output_tokens === 'number')
1986
+ outputTokens += usageObj.output_tokens;
1964
1987
  // Per-assistant-message cost: each event carries its own model, so we
1965
1988
  // multiply that event's raw token directions by that model's price.
1966
1989
  const model = parsed.message?.model;
@@ -2000,6 +2023,7 @@ export async function scanClaudeSession(filePath) {
2000
2023
  entrypoint,
2001
2024
  messageCount,
2002
2025
  tokenCount: sawTokenCount ? tokenCount : undefined,
2026
+ outputTokens: sawTokenCount ? outputTokens : undefined,
2003
2027
  costUsd: sawCost ? costUsd : undefined,
2004
2028
  durationMs,
2005
2029
  lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
@@ -2167,6 +2191,9 @@ async function scanCodexSession(filePath) {
2167
2191
  topic,
2168
2192
  messageCount,
2169
2193
  tokenCount,
2194
+ outputTokens: lastTotalTokenUsage
2195
+ ? (lastTotalTokenUsage.output_tokens ?? 0) + (lastTotalTokenUsage.reasoning_output_tokens ?? 0)
2196
+ : undefined,
2170
2197
  costUsd,
2171
2198
  durationMs,
2172
2199
  lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
@@ -2500,7 +2527,7 @@ export function readKimiMeta(filePath) {
2500
2527
  }
2501
2528
  }
2502
2529
  // Parse wire.jsonl to extract message count and token usage
2503
- const { messageCount, tokenCount } = parseKimiWireMetrics(sessionDir);
2530
+ const { messageCount, tokenCount, outputTokens } = parseKimiWireMetrics(sessionDir);
2504
2531
  const meta = {
2505
2532
  id: sessionId,
2506
2533
  shortId,
@@ -2511,6 +2538,7 @@ export function readKimiMeta(filePath) {
2511
2538
  topic,
2512
2539
  messageCount,
2513
2540
  tokenCount: tokenCount > 0 ? tokenCount : undefined,
2541
+ outputTokens: outputTokens > 0 ? outputTokens : undefined,
2514
2542
  };
2515
2543
  return { meta, content: lastPrompt || '' };
2516
2544
  }
@@ -2522,8 +2550,9 @@ function parseKimiWireMetrics(sessionDir) {
2522
2550
  const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
2523
2551
  let messageCount = 0;
2524
2552
  let tokenCount = 0;
2553
+ let outputTokens = 0;
2525
2554
  if (!fs.existsSync(wirePath)) {
2526
- return { messageCount: 0, tokenCount: 0 };
2555
+ return { messageCount: 0, tokenCount: 0, outputTokens: 0 };
2527
2556
  }
2528
2557
  try {
2529
2558
  const lines = fs.readFileSync(wirePath, 'utf-8').split('\n');
@@ -2539,6 +2568,7 @@ function parseKimiWireMetrics(sessionDir) {
2539
2568
  // Kimi usage structure: inputOther + output + inputCacheRead + inputCacheCreation
2540
2569
  const u = event.usage;
2541
2570
  tokenCount += (u.inputOther || 0) + (u.output || 0) + (u.inputCacheRead || 0) + (u.inputCacheCreation || 0);
2571
+ outputTokens += (u.output || 0);
2542
2572
  }
2543
2573
  }
2544
2574
  catch {
@@ -2549,11 +2579,13 @@ function parseKimiWireMetrics(sessionDir) {
2549
2579
  catch {
2550
2580
  // If wire.jsonl can't be read, return 0s (graceful degradation)
2551
2581
  }
2552
- return { messageCount, tokenCount };
2582
+ return { messageCount, tokenCount, outputTokens };
2553
2583
  }
2554
2584
  /** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
2555
2585
  export function parseTimeFilter(input) {
2556
- const relativeMatch = input.match(/^(\d+)([mhdw])$/i);
2586
+ // Units: m=minute, h=hour, d=day, w=week, mo=month(30d), y=year(365d). `mo`
2587
+ // must precede the single-letter alternatives so "1mo" isn't read as "1m"+"o".
2588
+ const relativeMatch = input.match(/^(\d+)(mo|[mhdwy])$/i);
2557
2589
  if (relativeMatch) {
2558
2590
  const value = parseInt(relativeMatch[1], 10);
2559
2591
  const unit = relativeMatch[2].toLowerCase();
@@ -2565,6 +2597,10 @@ export function parseTimeFilter(input) {
2565
2597
  return Date.now() - value * 86_400_000;
2566
2598
  if (unit === 'w')
2567
2599
  return Date.now() - value * 7 * 86_400_000;
2600
+ if (unit === 'mo')
2601
+ return Date.now() - value * 30 * 86_400_000;
2602
+ if (unit === 'y')
2603
+ return Date.now() - value * 365 * 86_400_000;
2568
2604
  }
2569
2605
  const ts = new Date(input).getTime();
2570
2606
  return Number.isNaN(ts) ? 0 : ts;
@@ -66,6 +66,8 @@ export interface SessionMeta {
66
66
  gitBranch?: string;
67
67
  messageCount?: number;
68
68
  tokenCount?: number;
69
+ /** Real generated (output) tokens — excludes cache-read/-write context (issue: `agents output`). */
70
+ outputTokens?: number;
69
71
  /** Total USD cost, computed at scan time from per-model token usage (issue #323). */
70
72
  costUsd?: number;
71
73
  /** Wall-clock duration in ms (lastTs − firstTs), persisted at scan time. */
@@ -84,7 +84,7 @@ function buildOpenCodeDetector() {
84
84
  kind: 'permissions',
85
85
  agent: 'opencode',
86
86
  list({ versionHome }) {
87
- const opencodeConfigPath = path.join(versionHome, '.opencode', 'opencode.jsonc');
87
+ const opencodeConfigPath = path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
88
88
  if (!fs.existsSync(opencodeConfigPath))
89
89
  return [];
90
90
  try {
@@ -202,6 +202,26 @@ function buildCursorDetector() {
202
202
  },
203
203
  };
204
204
  }
205
+ function buildDroidDetector() {
206
+ return {
207
+ kind: 'permissions',
208
+ agent: 'droid',
209
+ list({ versionHome }) {
210
+ const settingsPath = path.join(versionHome, '.factory', 'settings.json');
211
+ if (!fs.existsSync(settingsPath))
212
+ return [];
213
+ try {
214
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
215
+ const hasAllow = Array.isArray(settings.commandAllowlist) && settings.commandAllowlist.length > 0;
216
+ const hasDeny = Array.isArray(settings.commandDenylist) && settings.commandDenylist.length > 0;
217
+ if (hasAllow || hasDeny)
218
+ return discoverPermissionGroups().map(g => g.name);
219
+ }
220
+ catch { /* parse fail */ }
221
+ return [];
222
+ },
223
+ };
224
+ }
205
225
  function buildKiroDetector() {
206
226
  return {
207
227
  kind: 'permissions',
@@ -230,6 +250,7 @@ const handlers = {
230
250
  grok: buildGrokDetector,
231
251
  kimi: buildKimiDetector,
232
252
  cursor: buildCursorDetector,
253
+ droid: buildDroidDetector,
233
254
  kiro: buildKiroDetector,
234
255
  };
235
256
  export const permissionsDetectors = lazyAgentMap(() => {
@@ -85,32 +85,32 @@ function buildOpenclawDetector() {
85
85
  },
86
86
  };
87
87
  }
88
- function buildKiroDetector() {
88
+ function buildKimiDetector() {
89
89
  return {
90
90
  kind: 'subagents',
91
- agent: 'kiro',
91
+ agent: 'kimi',
92
92
  list({ versionHome }) {
93
- const agentsDir = path.join(versionHome, '.kiro', 'agents');
93
+ const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
94
94
  if (!fs.existsSync(agentsDir))
95
95
  return [];
96
+ // Parent is `_agents-cli.yaml` (underscore-prefixed reserved name).
96
97
  return fs.readdirSync(agentsDir)
97
- .filter(f => f.endsWith('.json'))
98
- .map(f => f.replace('.json', ''));
98
+ .filter(f => f.endsWith('.yaml') && !f.startsWith('_'))
99
+ .map(f => f.replace(/\.yaml$/, ''));
99
100
  },
100
101
  };
101
102
  }
102
- function buildKimiDetector() {
103
+ function buildKiroDetector() {
103
104
  return {
104
105
  kind: 'subagents',
105
- agent: 'kimi',
106
+ agent: 'kiro',
106
107
  list({ versionHome }) {
107
- const agentsDir = path.join(versionHome, '.kimi-code', 'agents');
108
+ const agentsDir = path.join(versionHome, '.kiro', 'agents');
108
109
  if (!fs.existsSync(agentsDir))
109
110
  return [];
110
- // Parent is `_agents-cli.yaml` (underscore-prefixed reserved name).
111
111
  return fs.readdirSync(agentsDir)
112
- .filter(f => f.endsWith('.yaml') && !f.startsWith('_'))
113
- .map(f => f.replace(/\.yaml$/, ''));
112
+ .filter(f => f.endsWith('.json'))
113
+ .map(f => f.replace(/\.json$/, ''));
114
114
  },
115
115
  };
116
116
  }
@@ -36,10 +36,10 @@ function buildCommandsWriter(agent) {
36
36
  const agentDir = path.join(versionHome, agentConfigDirName(agent));
37
37
  const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
38
38
  const supportsCommands = supports(agent, 'commands', version).ok;
39
- // Writers fire only after supports() OR commands-as-skills says yes
40
- // both paths produce a usable result here.
39
+ // Version-gated agents (e.g. goose skills >= 1.25.0) are registered but
40
+ // may be called at a version too old for both paths — skip gracefully.
41
41
  if (!commandsAsSkills && !supportsCommands) {
42
- throw new Error(`commands writer reached for ${agent}@${version} with no path (cmd=false, asSkill=false)`);
42
+ return { synced: [] };
43
43
  }
44
44
  const skillRoots = trustedSkillRoots();
45
45
  const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
@@ -65,6 +65,7 @@ export declare const loadDrive: ModuleLoader;
65
65
  export declare const loadFactory: ModuleLoader;
66
66
  export declare const loadUsage: ModuleLoader;
67
67
  export declare const loadCost: ModuleLoader;
68
+ export declare const loadOutput: ModuleLoader;
68
69
  export declare const loadBudget: ModuleLoader;
69
70
  export declare const loadAlias: ModuleLoader;
70
71
  export declare const loadPty: ModuleLoader;
@@ -43,6 +43,7 @@ export const loadDrive = async () => (await import('../../commands/drive.js')).r
43
43
  export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
44
44
  export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
45
45
  export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
46
+ export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
46
47
  export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
47
48
  export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
48
49
  export const loadPty = async () => (await import('../../commands/pty.js')).registerPtyCommands;
@@ -137,6 +138,7 @@ export const COMMAND_LOADERS = {
137
138
  factory: [loadFactory],
138
139
  usage: [loadUsage],
139
140
  cost: [loadCost],
141
+ output: [loadOutput],
140
142
  budget: [loadBudget],
141
143
  alias: [loadAlias],
142
144
  pty: [loadPty],
@@ -159,6 +159,7 @@ export declare function subagentContentMatches(installedDir: string, sourceDir:
159
159
  * List subagents installed to a specific agent's home
160
160
  * Claude: scans ~/.claude/agents/{name}.md
161
161
  * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
162
+ * Kiro: scans ~/.kiro/agents/{name}.json
162
163
  * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
163
164
  */
164
165
  export declare function listSubagentsForAgent(agentId: AgentId, home: string): InstalledSubagent[];
@@ -393,21 +393,10 @@ export function writeKimiSubagentFiles(agentsDir, subagentDir, name) {
393
393
  export function transformSubagentForCodex(subagentDir) {
394
394
  const agentMd = path.join(subagentDir, 'AGENT.md');
395
395
  const frontmatter = parseSubagentFrontmatter(agentMd);
396
- const body = getSubagentBody(agentMd);
397
396
  if (!frontmatter) {
398
397
  throw new Error(`Invalid AGENT.md in ${subagentDir}`);
399
398
  }
400
- // Append other .md files into the developer_instructions body.
401
- let instructions = body.trim();
402
- const files = fs.readdirSync(subagentDir)
403
- .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
404
- .sort();
405
- for (const file of files) {
406
- const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
407
- const sectionName = file.replace('.md', '');
408
- const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
409
- instructions += `\n\n## ${title}\n\n${content}`;
410
- }
399
+ const instructions = flattenSubagentInstructions(subagentDir);
411
400
  // Escape TOML multi-line string (""") content — only """ needs escaping.
412
401
  const safeInstructions = instructions.replace(/"""/g, '\\"""');
413
402
  const safeName = frontmatter.name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
@@ -421,6 +410,19 @@ export function transformSubagentForCodex(subagentDir) {
421
410
  toml += `developer_instructions = """\n${safeInstructions}\n"""\n`;
422
411
  return toml;
423
412
  }
413
+ function flattenSubagentInstructions(subagentDir) {
414
+ let instructions = getSubagentBody(path.join(subagentDir, 'AGENT.md')).trim();
415
+ const files = fs.readdirSync(subagentDir)
416
+ .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
417
+ .sort();
418
+ for (const file of files) {
419
+ const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
420
+ const sectionName = file.replace('.md', '');
421
+ const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
422
+ instructions += `\n\n## ${title}\n\n${content}`;
423
+ }
424
+ return instructions;
425
+ }
424
426
  /**
425
427
  * Transform a subagent into a Kiro CLI custom-agent JSON file.
426
428
  *
@@ -655,6 +657,7 @@ export function subagentContentMatches(installedDir, sourceDir) {
655
657
  * List subagents installed to a specific agent's home
656
658
  * Claude: scans ~/.claude/agents/{name}.md
657
659
  * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
660
+ * Kiro: scans ~/.kiro/agents/{name}.json
658
661
  * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
659
662
  */
660
663
  export function listSubagentsForAgent(agentId, home) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.58",
3
+ "version": "1.20.59",
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",