@phnx-labs/agents-cli 1.20.57 → 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.
- package/CHANGELOG.md +30 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface SessionRow {
|
|
|
24
24
|
label: string | null;
|
|
25
25
|
message_count: number | null;
|
|
26
26
|
token_count: number | null;
|
|
27
|
+
output_tokens: number | null;
|
|
27
28
|
cost_usd: number | null;
|
|
28
29
|
duration_ms: number | null;
|
|
29
30
|
file_path: string;
|
|
@@ -184,6 +185,8 @@ export interface UsageRollupRow {
|
|
|
184
185
|
durationMs: number;
|
|
185
186
|
sessionCount: number;
|
|
186
187
|
tokenCount: number;
|
|
188
|
+
/** Real generated (output) tokens — excludes cache-read/-write context. */
|
|
189
|
+
outputTokens: number;
|
|
187
190
|
}
|
|
188
191
|
/** What to group a usage rollup by. */
|
|
189
192
|
export type UsageRollupGroup = 'agent' | 'project' | 'day';
|
package/dist/lib/session/db.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
13
13
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
14
|
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
16
|
+
const SCHEMA_VERSION = 12;
|
|
17
17
|
/**
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -54,6 +54,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
54
54
|
label TEXT,
|
|
55
55
|
message_count INTEGER,
|
|
56
56
|
token_count INTEGER,
|
|
57
|
+
output_tokens INTEGER,
|
|
57
58
|
cost_usd REAL,
|
|
58
59
|
duration_ms INTEGER,
|
|
59
60
|
file_path TEXT NOT NULL,
|
|
@@ -224,6 +225,16 @@ function migrateSchema(db, fromVersion) {
|
|
|
224
225
|
db.exec(`ALTER TABLE sessions ADD COLUMN plan TEXT`);
|
|
225
226
|
db.exec(`DELETE FROM scan_ledger;`);
|
|
226
227
|
}
|
|
228
|
+
if (fromVersion < 12) {
|
|
229
|
+
// v11 → v12: `output_tokens` — the real generated-token count, kept separate
|
|
230
|
+
// from `token_count` (which sums cache-read/-write and so is dominated by
|
|
231
|
+
// cheap re-counted context). This is the honest "output" metric powering
|
|
232
|
+
// `agents output`. Additive column; rescan to backfill from transcripts.
|
|
233
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
234
|
+
if (!cols.some(c => c.name === 'output_tokens'))
|
|
235
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN output_tokens INTEGER`);
|
|
236
|
+
db.exec(`DELETE FROM scan_ledger;`);
|
|
237
|
+
}
|
|
227
238
|
}
|
|
228
239
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
229
240
|
export function getDB() {
|
|
@@ -438,13 +449,13 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
438
449
|
INSERT INTO sessions (
|
|
439
450
|
id, short_id, agent, version, account, timestamp, last_activity,
|
|
440
451
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
441
|
-
cost_usd, duration_ms,
|
|
452
|
+
output_tokens, cost_usd, duration_ms,
|
|
442
453
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
443
454
|
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
444
455
|
) VALUES (
|
|
445
456
|
@id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
|
|
446
457
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
447
|
-
@cost_usd, @duration_ms,
|
|
458
|
+
@output_tokens, @cost_usd, @duration_ms,
|
|
448
459
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
449
460
|
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan
|
|
450
461
|
)
|
|
@@ -462,6 +473,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
462
473
|
label = excluded.label,
|
|
463
474
|
message_count = excluded.message_count,
|
|
464
475
|
token_count = excluded.token_count,
|
|
476
|
+
output_tokens = excluded.output_tokens,
|
|
465
477
|
cost_usd = excluded.cost_usd,
|
|
466
478
|
duration_ms = excluded.duration_ms,
|
|
467
479
|
file_path = excluded.file_path,
|
|
@@ -510,6 +522,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
510
522
|
label: meta.label ?? null,
|
|
511
523
|
message_count: meta.messageCount ?? null,
|
|
512
524
|
token_count: meta.tokenCount ?? null,
|
|
525
|
+
output_tokens: meta.outputTokens ?? null,
|
|
513
526
|
cost_usd: meta.costUsd ?? null,
|
|
514
527
|
duration_ms: meta.durationMs ?? null,
|
|
515
528
|
file_path: meta.filePath,
|
|
@@ -599,6 +612,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
599
612
|
label: meta.label ?? null,
|
|
600
613
|
message_count: meta.messageCount ?? null,
|
|
601
614
|
token_count: meta.tokenCount ?? null,
|
|
615
|
+
output_tokens: meta.outputTokens ?? null,
|
|
602
616
|
cost_usd: meta.costUsd ?? null,
|
|
603
617
|
duration_ms: meta.durationMs ?? null,
|
|
604
618
|
file_path: meta.filePath,
|
|
@@ -772,6 +786,7 @@ function rowToMeta(row) {
|
|
|
772
786
|
gitBranch: row.git_branch ?? undefined,
|
|
773
787
|
messageCount: row.message_count ?? undefined,
|
|
774
788
|
tokenCount: row.token_count ?? undefined,
|
|
789
|
+
outputTokens: row.output_tokens ?? undefined,
|
|
775
790
|
costUsd: row.cost_usd ?? undefined,
|
|
776
791
|
durationMs: row.duration_ms ?? undefined,
|
|
777
792
|
version: row.version ?? undefined,
|
|
@@ -956,7 +971,8 @@ export function queryUsageRollup(options) {
|
|
|
956
971
|
IFNULL(SUM(cost_usd), 0) AS costUsd,
|
|
957
972
|
IFNULL(SUM(duration_ms), 0) AS durationMs,
|
|
958
973
|
COUNT(*) AS sessionCount,
|
|
959
|
-
IFNULL(SUM(token_count), 0) AS tokenCount
|
|
974
|
+
IFNULL(SUM(token_count), 0) AS tokenCount,
|
|
975
|
+
IFNULL(SUM(output_tokens), 0) AS outputTokens
|
|
960
976
|
FROM sessions
|
|
961
977
|
${clause}
|
|
962
978
|
GROUP BY key
|
|
@@ -46,6 +46,8 @@ interface ClaudeSessionScan {
|
|
|
46
46
|
topic?: string;
|
|
47
47
|
messageCount: number;
|
|
48
48
|
tokenCount?: number;
|
|
49
|
+
/** Real generated (output) tokens, excluding cache-read/-write context. */
|
|
50
|
+
outputTokens?: number;
|
|
49
51
|
/** Total USD cost accumulated from per-(model, direction) token usage. */
|
|
50
52
|
costUsd?: number;
|
|
51
53
|
/** Wall-clock duration in ms between the first and last timestamped event. */
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -55,6 +55,8 @@ function sanitizeEvent(e) {
|
|
|
55
55
|
e.command = sanitizeForTerminal(e.command);
|
|
56
56
|
if (e.path)
|
|
57
57
|
e.path = sanitizeForTerminal(e.path);
|
|
58
|
+
if (e.name)
|
|
59
|
+
e.name = sanitizeForTerminal(e.name);
|
|
58
60
|
if (e.output)
|
|
59
61
|
e.output = sanitizeForTerminal(e.output);
|
|
60
62
|
if (e.tool)
|
|
@@ -66,6 +68,36 @@ function sanitizeEvent(e) {
|
|
|
66
68
|
if (e.args)
|
|
67
69
|
e.args = sanitizeArgsDeep(e.args);
|
|
68
70
|
}
|
|
71
|
+
function firstString(...values) {
|
|
72
|
+
for (const value of values) {
|
|
73
|
+
if (typeof value === 'string' && value.trim())
|
|
74
|
+
return value.trim();
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function attachmentPath(block, source) {
|
|
79
|
+
return firstString(source?.path, source?.file_path, source?.filePath, source?.url, source?.ref, block?.path, block?.file_path, block?.filePath, block?.ref);
|
|
80
|
+
}
|
|
81
|
+
function attachmentName(block, source, filePath) {
|
|
82
|
+
return firstString(block?.name, block?.title, source?.name, source?.filename, source?.file_name, source?.fileName, filePath ? path.basename(filePath) : undefined);
|
|
83
|
+
}
|
|
84
|
+
function normalizedAttachmentEvent(agent, timestamp, block, source, defaultMediaType, sizeBytes) {
|
|
85
|
+
const filePath = attachmentPath(block, source);
|
|
86
|
+
const name = attachmentName(block, source, filePath);
|
|
87
|
+
const explicitSize = typeof source?.sizeBytes === 'number' ? source.sizeBytes :
|
|
88
|
+
typeof source?.size === 'number' ? source.size :
|
|
89
|
+
typeof block?.sizeBytes === 'number' ? block.sizeBytes :
|
|
90
|
+
undefined;
|
|
91
|
+
return {
|
|
92
|
+
type: 'attachment',
|
|
93
|
+
agent,
|
|
94
|
+
timestamp,
|
|
95
|
+
path: filePath,
|
|
96
|
+
name,
|
|
97
|
+
mediaType: firstString(source?.media_type, source?.mediaType, block?.media_type, block?.mediaType) || defaultMediaType,
|
|
98
|
+
sizeBytes: sizeBytes || explicitSize || 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
69
101
|
/**
|
|
70
102
|
* Read a session file, refusing files above maxBytes. Bounded read protects
|
|
71
103
|
* against multi-GB session blobs that would OOM the CLI or exceed V8's
|
|
@@ -351,24 +383,15 @@ export function parseClaudeContent(content) {
|
|
|
351
383
|
const source = block.source || {};
|
|
352
384
|
if (source.type === 'base64') {
|
|
353
385
|
const sizeBytes = Math.ceil((source.data?.length || 0) * 0.75);
|
|
354
|
-
events.push(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
mediaType: source.media_type || 'image/png',
|
|
359
|
-
sizeBytes,
|
|
360
|
-
});
|
|
386
|
+
events.push(normalizedAttachmentEvent('claude', timestamp, block, source, 'image/png', sizeBytes));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
events.push(normalizedAttachmentEvent('claude', timestamp, block, source, 'image/png', 0));
|
|
361
390
|
}
|
|
362
391
|
}
|
|
363
392
|
else if (block.type === 'document') {
|
|
364
393
|
const source = block.source || {};
|
|
365
|
-
events.push(
|
|
366
|
-
type: 'attachment',
|
|
367
|
-
agent: 'claude',
|
|
368
|
-
timestamp,
|
|
369
|
-
mediaType: source.media_type || 'application/pdf',
|
|
370
|
-
sizeBytes: 0,
|
|
371
|
-
});
|
|
394
|
+
events.push(normalizedAttachmentEvent('claude', timestamp, block, source, 'application/pdf', 0));
|
|
372
395
|
}
|
|
373
396
|
else if (block.type === 'tool_result') {
|
|
374
397
|
const toolId = block.tool_use_id;
|
|
@@ -1554,7 +1577,7 @@ export function parseDroid(filePath) {
|
|
|
1554
1577
|
else if (block.type === 'image') {
|
|
1555
1578
|
const source = block.source || {};
|
|
1556
1579
|
const sizeBytes = source.type === 'base64' ? Math.ceil((source.data?.length || 0) * 0.75) : 0;
|
|
1557
|
-
events.push(
|
|
1580
|
+
events.push(normalizedAttachmentEvent('droid', timestamp, block, source, 'image/png', sizeBytes));
|
|
1558
1581
|
}
|
|
1559
1582
|
}
|
|
1560
1583
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* markers. Codex has no such tools, so it falls back to last-role + question
|
|
15
15
|
* shape + mtime — same function, driven off the normalized events.
|
|
16
16
|
*/
|
|
17
|
-
import type { SessionEvent } from './types.js';
|
|
17
|
+
import type { SessionAttachment, SessionEvent } from './types.js';
|
|
18
18
|
export type SessionActivity = 'working' | 'waiting_input' | 'idle';
|
|
19
19
|
export type AwaitingReason = 'question' | 'plan_review' | 'permission';
|
|
20
20
|
/** One discrete choice the agent offered the user. */
|
|
@@ -98,6 +98,8 @@ export interface SessionState {
|
|
|
98
98
|
createdTickets?: string[];
|
|
99
99
|
/** Team name this session SPAWNED via `agents teams create/add`. */
|
|
100
100
|
spawnedTeam?: string;
|
|
101
|
+
/** Displayable files/screenshots attached to the session prompt. */
|
|
102
|
+
attachments?: SessionAttachment[];
|
|
101
103
|
}
|
|
102
104
|
export interface StateContext {
|
|
103
105
|
/** Session file mtime; drives running-vs-stale. */
|
|
@@ -169,6 +171,7 @@ export declare function detectDurableSignals(events: SessionEvent[]): {
|
|
|
169
171
|
ticket?: DetectedTicket;
|
|
170
172
|
createdTickets?: string[];
|
|
171
173
|
spawnedTeam?: string;
|
|
174
|
+
attachments?: SessionAttachment[];
|
|
172
175
|
};
|
|
173
176
|
/** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
|
|
174
177
|
export declare function inferSessionState(events: SessionEvent[], ctx?: StateContext): SessionState;
|
|
@@ -352,6 +352,8 @@ export function detectDurableSignals(events) {
|
|
|
352
352
|
let sawTicketCreate = false;
|
|
353
353
|
let spawnedTeam;
|
|
354
354
|
const createdTickets = new Set();
|
|
355
|
+
const attachments = [];
|
|
356
|
+
const seenAttachments = new Set();
|
|
355
357
|
for (const e of events) {
|
|
356
358
|
// Structural PR signal: a real `gh pr create` tool call, then the pull URL
|
|
357
359
|
// from a following tool_result — never a bare URL mentioned in prose.
|
|
@@ -384,18 +386,32 @@ export function detectDurableSignals(events) {
|
|
|
384
386
|
if (!ticket && e.type === 'message' && e.role === 'user') {
|
|
385
387
|
ticket = detectTicket(e.content);
|
|
386
388
|
}
|
|
389
|
+
if (e.type === 'attachment') {
|
|
390
|
+
const mediaType = e.mediaType || 'application/octet-stream';
|
|
391
|
+
const key = e.path || e.name || `${mediaType}:${e.sizeBytes ?? 0}:${e.timestamp}`;
|
|
392
|
+
if (key && !seenAttachments.has(key)) {
|
|
393
|
+
seenAttachments.add(key);
|
|
394
|
+
attachments.push({
|
|
395
|
+
path: e.path,
|
|
396
|
+
name: e.name,
|
|
397
|
+
mediaType,
|
|
398
|
+
sizeBytes: e.sizeBytes,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
387
402
|
}
|
|
388
403
|
return {
|
|
389
404
|
pr,
|
|
390
405
|
ticket,
|
|
391
406
|
createdTickets: createdTickets.size > 0 ? [...createdTickets] : undefined,
|
|
392
407
|
spawnedTeam,
|
|
408
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
393
409
|
};
|
|
394
410
|
}
|
|
395
411
|
/** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
|
|
396
412
|
export function inferSessionState(events, ctx = {}) {
|
|
397
413
|
const state = inferActivity(events, ctx);
|
|
398
|
-
const { pr, ticket, createdTickets, spawnedTeam } = detectDurableSignals(events);
|
|
414
|
+
const { pr, ticket, createdTickets, spawnedTeam, attachments } = detectDurableSignals(events);
|
|
399
415
|
const worktree = detectWorktree(ctx.cwd, ctx.gitBranch);
|
|
400
416
|
// Rate-limit: scan the most recent assistant messages + tool errors (tail-first).
|
|
401
417
|
let rateLimited = false;
|
|
@@ -421,6 +437,7 @@ export function inferSessionState(events, ctx = {}) {
|
|
|
421
437
|
ticket: ticket ?? detectTicket(undefined, ctx.gitBranch) ?? state.ticket,
|
|
422
438
|
createdTickets,
|
|
423
439
|
spawnedTeam,
|
|
440
|
+
attachments,
|
|
424
441
|
rateLimited: rateLimited || undefined,
|
|
425
442
|
};
|
|
426
443
|
}
|
|
@@ -30,9 +30,17 @@ export interface SessionEvent {
|
|
|
30
30
|
outputTokens?: number;
|
|
31
31
|
cacheReadTokens?: number;
|
|
32
32
|
cacheCreationTokens?: number;
|
|
33
|
+
name?: string;
|
|
33
34
|
mediaType?: string;
|
|
34
35
|
sizeBytes?: number;
|
|
35
36
|
}
|
|
37
|
+
/** A displayable file attachment discovered in a session transcript. */
|
|
38
|
+
export interface SessionAttachment {
|
|
39
|
+
path?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
mediaType: string;
|
|
42
|
+
sizeBytes?: number;
|
|
43
|
+
}
|
|
36
44
|
/** Metadata attached when a session was spawned by `agents teams`. */
|
|
37
45
|
export interface TeamOrigin {
|
|
38
46
|
/** Teammate name if set, otherwise first 8 chars of the agent UUID. */
|
|
@@ -58,6 +66,8 @@ export interface SessionMeta {
|
|
|
58
66
|
gitBranch?: string;
|
|
59
67
|
messageCount?: number;
|
|
60
68
|
tokenCount?: number;
|
|
69
|
+
/** Real generated (output) tokens — excludes cache-read/-write context (issue: `agents output`). */
|
|
70
|
+
outputTokens?: number;
|
|
61
71
|
/** Total USD cost, computed at scan time from per-model token usage (issue #323). */
|
|
62
72
|
costUsd?: number;
|
|
63
73
|
/** Wall-clock duration in ms (lastTs − firstTs), persisted at scan time. */
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as TOML from 'smol-toml';
|
|
16
|
+
import * as yaml from 'yaml';
|
|
16
17
|
import { capableAgents } from '../../capabilities.js';
|
|
17
18
|
import { discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, } from '../../permissions.js';
|
|
18
19
|
import { lazyAgentMap } from '../writers/lazy-map.js';
|
|
@@ -83,7 +84,7 @@ function buildOpenCodeDetector() {
|
|
|
83
84
|
kind: 'permissions',
|
|
84
85
|
agent: 'opencode',
|
|
85
86
|
list({ versionHome }) {
|
|
86
|
-
const opencodeConfigPath = path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
87
|
+
const opencodeConfigPath = path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
87
88
|
if (!fs.existsSync(opencodeConfigPath))
|
|
88
89
|
return [];
|
|
89
90
|
try {
|
|
@@ -181,6 +182,65 @@ function buildKimiDetector() {
|
|
|
181
182
|
},
|
|
182
183
|
};
|
|
183
184
|
}
|
|
185
|
+
function buildCursorDetector() {
|
|
186
|
+
return {
|
|
187
|
+
kind: 'permissions',
|
|
188
|
+
agent: 'cursor',
|
|
189
|
+
list({ versionHome }) {
|
|
190
|
+
const configPath = path.join(versionHome, '.cursor', 'cli-config.json');
|
|
191
|
+
if (!fs.existsSync(configPath))
|
|
192
|
+
return [];
|
|
193
|
+
try {
|
|
194
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
195
|
+
const allow = config.permissions?.allow?.length ?? 0;
|
|
196
|
+
const deny = config.permissions?.deny?.length ?? 0;
|
|
197
|
+
if (allow + deny > 0)
|
|
198
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
199
|
+
}
|
|
200
|
+
catch { /* parse fail */ }
|
|
201
|
+
return [];
|
|
202
|
+
},
|
|
203
|
+
};
|
|
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
|
+
}
|
|
225
|
+
function buildKiroDetector() {
|
|
226
|
+
return {
|
|
227
|
+
kind: 'permissions',
|
|
228
|
+
agent: 'kiro',
|
|
229
|
+
list({ versionHome }) {
|
|
230
|
+
const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
231
|
+
if (!fs.existsSync(permissionsPath))
|
|
232
|
+
return [];
|
|
233
|
+
try {
|
|
234
|
+
const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
235
|
+
if (config && Array.isArray(config.rules) && config.rules.length > 0) {
|
|
236
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { /* parse fail */ }
|
|
240
|
+
return [];
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
184
244
|
const handlers = {
|
|
185
245
|
claude: buildClaudeDetector,
|
|
186
246
|
codex: buildCodexDetector,
|
|
@@ -189,6 +249,9 @@ const handlers = {
|
|
|
189
249
|
antigravity: buildAntigravityDetector,
|
|
190
250
|
grok: buildGrokDetector,
|
|
191
251
|
kimi: buildKimiDetector,
|
|
252
|
+
cursor: buildCursorDetector,
|
|
253
|
+
droid: buildDroidDetector,
|
|
254
|
+
kiro: buildKiroDetector,
|
|
192
255
|
};
|
|
193
256
|
export const permissionsDetectors = lazyAgentMap(() => {
|
|
194
257
|
const m = {};
|
|
@@ -57,6 +57,20 @@ function buildDroidDetector() {
|
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
function buildCopilotDetector() {
|
|
61
|
+
return {
|
|
62
|
+
kind: 'subagents',
|
|
63
|
+
agent: 'copilot',
|
|
64
|
+
list({ versionHome }) {
|
|
65
|
+
const agentsDir = path.join(versionHome, '.copilot', 'agents');
|
|
66
|
+
if (!fs.existsSync(agentsDir))
|
|
67
|
+
return [];
|
|
68
|
+
return fs.readdirSync(agentsDir)
|
|
69
|
+
.filter(f => f.endsWith('.agent.md'))
|
|
70
|
+
.map(f => f.replace('.agent.md', ''));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
60
74
|
function buildOpenclawDetector() {
|
|
61
75
|
return {
|
|
62
76
|
kind: 'subagents',
|
|
@@ -86,6 +100,20 @@ function buildKimiDetector() {
|
|
|
86
100
|
},
|
|
87
101
|
};
|
|
88
102
|
}
|
|
103
|
+
function buildKiroDetector() {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'subagents',
|
|
106
|
+
agent: 'kiro',
|
|
107
|
+
list({ versionHome }) {
|
|
108
|
+
const agentsDir = path.join(versionHome, '.kiro', 'agents');
|
|
109
|
+
if (!fs.existsSync(agentsDir))
|
|
110
|
+
return [];
|
|
111
|
+
return fs.readdirSync(agentsDir)
|
|
112
|
+
.filter(f => f.endsWith('.json'))
|
|
113
|
+
.map(f => f.replace(/\.json$/, ''));
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
89
117
|
function buildOpenCodeDetector() {
|
|
90
118
|
return {
|
|
91
119
|
kind: 'subagents',
|
|
@@ -102,12 +130,14 @@ function buildOpenCodeDetector() {
|
|
|
102
130
|
}
|
|
103
131
|
const handlers = {
|
|
104
132
|
claude: buildClaudeDetector,
|
|
133
|
+
copilot: buildCopilotDetector,
|
|
105
134
|
grok: buildGrokDetector,
|
|
106
135
|
codex: buildCodexDetector,
|
|
107
136
|
kimi: buildKimiDetector,
|
|
108
137
|
opencode: buildOpenCodeDetector,
|
|
109
138
|
droid: buildDroidDetector,
|
|
110
139
|
openclaw: buildOpenclawDetector,
|
|
140
|
+
kiro: buildKiroDetector,
|
|
111
141
|
};
|
|
112
142
|
export const subagentsDetectors = lazyAgentMap(() => {
|
|
113
143
|
const m = {};
|
|
@@ -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
|
-
//
|
|
40
|
-
//
|
|
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
|
-
|
|
42
|
+
return { synced: [] };
|
|
43
43
|
}
|
|
44
44
|
const skillRoots = trustedSkillRoots();
|
|
45
45
|
const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
|