@phnx-labs/agents-cli 1.22.53 → 1.22.54
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 +190 -0
- package/README.md +41 -8
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/daemon.js +30 -4
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.js +8 -1
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +130 -29
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +582 -57
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/package.json +2 -1
|
@@ -34,7 +34,7 @@ import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
|
34
34
|
import { machineForSessionFile } from './origin-machine.js';
|
|
35
35
|
export { machineForSessionFile } from './origin-machine.js';
|
|
36
36
|
import { mapBounded } from '../concurrency.js';
|
|
37
|
-
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, scanInProgressByLivePid, cacheLinearProject, } from './db.js';
|
|
37
|
+
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, getSessionById, tryClaimScan, releaseScan, scanInProgressByLivePid, cacheLinearProject, CONTENT_INDEX_VERSION, } from './db.js';
|
|
38
38
|
import { buildRunNameMap } from './run-names.js';
|
|
39
39
|
import { resolveLinearApiKey } from '../linear-cache.js';
|
|
40
40
|
import { ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, } from './tool-calls.js';
|
|
@@ -628,8 +628,15 @@ export function resolveSessionById(sessions, idQuery) {
|
|
|
628
628
|
// Content-index search (FTS5-backed)
|
|
629
629
|
// ---------------------------------------------------------------------------
|
|
630
630
|
/**
|
|
631
|
-
* Run an FTS5 search over the DB and
|
|
632
|
-
*
|
|
631
|
+
* Run an FTS5 search over the DB and union hits with the given session list.
|
|
632
|
+
*
|
|
633
|
+
* The listing pool is a minority of the index — cwd-scoped, default-capped at
|
|
634
|
+
* 50, and skipping whole classes of indexed transcript — so intersecting FTS
|
|
635
|
+
* hits with it dropped grep-visible sessions that the index already found
|
|
636
|
+
* (PHNX-2767: `agents sessions "tmux pane"` returned 0 while the project
|
|
637
|
+
* transcripts matched). Hits already in the pool keep the caller's SessionMeta;
|
|
638
|
+
* hits the pool missed are hydrated from the index so a content query returns
|
|
639
|
+
* the transcript FTS matched.
|
|
633
640
|
*/
|
|
634
641
|
export function searchContentIndex(sessions, query) {
|
|
635
642
|
if (!query.trim())
|
|
@@ -640,13 +647,14 @@ export function searchContentIndex(sessions, query) {
|
|
|
640
647
|
const byId = new Map(sessions.map(s => [s.id, s]));
|
|
641
648
|
const result = new Map();
|
|
642
649
|
for (const hit of hits) {
|
|
643
|
-
const session = byId.get(hit.sessionId);
|
|
650
|
+
const session = byId.get(hit.sessionId) ?? getSessionById(hit.sessionId);
|
|
644
651
|
if (!session)
|
|
645
652
|
continue;
|
|
646
653
|
result.set(hit.sessionId, {
|
|
647
654
|
...session,
|
|
648
655
|
_matchedTerms: hit.matchedTerms,
|
|
649
656
|
_bm25Score: hit.score,
|
|
657
|
+
snippet: hit.snippet,
|
|
650
658
|
});
|
|
651
659
|
}
|
|
652
660
|
return result;
|
|
@@ -678,6 +686,13 @@ export function filterChangedFiles(filePaths) {
|
|
|
678
686
|
* stat. Same debounce and change-detection as filterChangedFiles; the raw
|
|
679
687
|
* mtime is floored here so warm files match the ledger exactly as the stat path
|
|
680
688
|
* does (Math.floor(stat.mtimeMs)).
|
|
689
|
+
*
|
|
690
|
+
* A file is also treated as "changed" — independent of (mtime, size) — when
|
|
691
|
+
* its ledger row's `extractor_version` is behind {@link CONTENT_INDEX_VERSION}.
|
|
692
|
+
* This is the lever that backfills a content-extractor improvement (e.g.
|
|
693
|
+
* indexing assistant answers, not just user prompts) into every already-scanned
|
|
694
|
+
* session on its next pass, without a `DELETE FROM scan_ledger` that would also
|
|
695
|
+
* discard the Claude/Codex resumable parser_state.
|
|
681
696
|
*/
|
|
682
697
|
export function filterChangedEntries(entries) {
|
|
683
698
|
const ledger = getScanStampsForPaths(entries.map(e => e.filePath));
|
|
@@ -689,10 +704,11 @@ export function filterChangedEntries(entries) {
|
|
|
689
704
|
fileSize: entry.fileSize,
|
|
690
705
|
};
|
|
691
706
|
const prev = ledger.get(entry.filePath);
|
|
692
|
-
|
|
707
|
+
const contentIndexStale = prev?.extractorVersion !== CONTENT_INDEX_VERSION;
|
|
708
|
+
if (prev && prev.fileMtimeMs === scan.fileMtimeMs && prev.fileSize === scan.fileSize && !contentIndexStale) {
|
|
693
709
|
continue;
|
|
694
710
|
}
|
|
695
|
-
if (prev && shouldDeferRecentAppend(prev, scan, now)) {
|
|
711
|
+
if (prev && !contentIndexStale && shouldDeferRecentAppend(prev, scan, now)) {
|
|
696
712
|
continue;
|
|
697
713
|
}
|
|
698
714
|
out.push({ filePath: entry.filePath, scan });
|
|
@@ -1000,6 +1016,7 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
|
|
|
1000
1016
|
entries.push({
|
|
1001
1017
|
meta: result.meta,
|
|
1002
1018
|
content: result.content,
|
|
1019
|
+
assistantContent: result.assistantContent,
|
|
1003
1020
|
scan,
|
|
1004
1021
|
events: result.events,
|
|
1005
1022
|
toolCalls: result.toolCalls,
|
|
@@ -1152,6 +1169,7 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
1152
1169
|
entries.push({
|
|
1153
1170
|
meta: result.meta,
|
|
1154
1171
|
content: result.content,
|
|
1172
|
+
assistantContent: result.assistantContent,
|
|
1155
1173
|
scan,
|
|
1156
1174
|
parserState: result.parserState,
|
|
1157
1175
|
contentText: result.contentText,
|
|
@@ -1185,7 +1203,12 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
1185
1203
|
* next scan.
|
|
1186
1204
|
*/
|
|
1187
1205
|
async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
1188
|
-
|
|
1206
|
+
// A prior continuation extracted at an older CONTENT_INDEX_VERSION is missing
|
|
1207
|
+
// whatever the current extractor adds (e.g. assistant-answer text) — resuming
|
|
1208
|
+
// from it would fold only the NEWLY appended lines into that gap, never
|
|
1209
|
+
// backfilling the file's earlier assistant text. Treat it as absent so the
|
|
1210
|
+
// file gets one full from-offset-0 reparse instead.
|
|
1211
|
+
const prior = priorRow?.extractorVersion === CONTENT_INDEX_VERSION ? parsePriorClaudeState(priorRow) : null;
|
|
1189
1212
|
const { scan, newState, toolCalls, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
1190
1213
|
if (mode === 'incremental')
|
|
1191
1214
|
claudeIncrementalScanCount++;
|
|
@@ -1290,6 +1313,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1290
1313
|
return {
|
|
1291
1314
|
meta,
|
|
1292
1315
|
content: scan.contentText || '',
|
|
1316
|
+
assistantContent: scan.assistantText || '',
|
|
1293
1317
|
// Persist the continuation so the next scan of this file can resume from the
|
|
1294
1318
|
// offset instead of a full reparse. content_text is the same accumulated user
|
|
1295
1319
|
// doc, cached so the resume can hydrate userTexts without re-reading the file.
|
|
@@ -1431,6 +1455,7 @@ async function scanCodexIncremental(onProgress) {
|
|
|
1431
1455
|
entries.push({
|
|
1432
1456
|
meta: result.meta,
|
|
1433
1457
|
content: result.content,
|
|
1458
|
+
assistantContent: result.assistantContent,
|
|
1434
1459
|
scan,
|
|
1435
1460
|
parserState: result.parserState,
|
|
1436
1461
|
contentText: result.contentText,
|
|
@@ -1541,7 +1566,10 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1541
1566
|
let toolCalls;
|
|
1542
1567
|
let toolIndexMode = 'replace';
|
|
1543
1568
|
if (scanStamp) {
|
|
1544
|
-
|
|
1569
|
+
// See readClaudeMeta: a prior continuation from an older content extractor
|
|
1570
|
+
// is missing what the current extractor adds, so it must not be resumed
|
|
1571
|
+
// from — treat it as absent and force one full reparse.
|
|
1572
|
+
const prior = priorRow?.extractorVersion === CONTENT_INDEX_VERSION ? parsePriorCodexState(priorRow) : null;
|
|
1545
1573
|
const result = await scanCodexSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
1546
1574
|
if (result.mode === 'incremental')
|
|
1547
1575
|
codexIncrementalScanCount++;
|
|
@@ -1597,6 +1625,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1597
1625
|
return {
|
|
1598
1626
|
meta,
|
|
1599
1627
|
content: scan.contentText || '',
|
|
1628
|
+
assistantContent: scan.assistantText || '',
|
|
1600
1629
|
// Persist the continuation so the next scan of this rollout resumes from the
|
|
1601
1630
|
// offset instead of a full reparse; content_text caches the accumulated user
|
|
1602
1631
|
// doc for the resume's hydrate. Absent when no stamp was supplied.
|
|
@@ -1675,7 +1704,7 @@ async function scanGeminiIncremental(onProgress) {
|
|
|
1675
1704
|
const result = readGeminiMeta(filePath, hashDir, projectMap, currentVersion);
|
|
1676
1705
|
if (result && !seen.has(result.meta.id)) {
|
|
1677
1706
|
seen.add(result.meta.id);
|
|
1678
|
-
entries.push({ meta: result.meta, content: result.content, scan });
|
|
1707
|
+
entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
|
|
1679
1708
|
}
|
|
1680
1709
|
else {
|
|
1681
1710
|
// Gemini file without a sessionId — record scan so we don't re-parse it next run.
|
|
@@ -1729,6 +1758,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1729
1758
|
let firstTsMs;
|
|
1730
1759
|
let lastTsMs;
|
|
1731
1760
|
const userTexts = [];
|
|
1761
|
+
const assistantTexts = [];
|
|
1732
1762
|
for (const message of messages) {
|
|
1733
1763
|
if (message.type === 'user') {
|
|
1734
1764
|
const text = extractGeminiMessageText(message.content);
|
|
@@ -1740,8 +1770,10 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1740
1770
|
}
|
|
1741
1771
|
}
|
|
1742
1772
|
else if (message.type === 'gemini') {
|
|
1743
|
-
|
|
1773
|
+
const text = extractGeminiMessageText(message.content);
|
|
1774
|
+
if (text) {
|
|
1744
1775
|
messageCount++;
|
|
1776
|
+
assistantTexts.push(text);
|
|
1745
1777
|
}
|
|
1746
1778
|
}
|
|
1747
1779
|
// Duration: messages carry a `timestamp` on most Gemini CLI versions.
|
|
@@ -1819,7 +1851,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1819
1851
|
costUsdNoCache: sawCost ? costUsdNoCache : undefined,
|
|
1820
1852
|
durationMs,
|
|
1821
1853
|
};
|
|
1822
|
-
return { meta, content: userTexts.join('\n') };
|
|
1854
|
+
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
1823
1855
|
}
|
|
1824
1856
|
/** Build a hash-to-project mapping from Gemini's projects.json and history directories. */
|
|
1825
1857
|
function buildGeminiProjectMap() {
|
|
@@ -2396,7 +2428,7 @@ async function scanRushIncremental(onProgress) {
|
|
|
2396
2428
|
const sessionId = path.basename(path.dirname(filePath));
|
|
2397
2429
|
const result = await readRushMeta(filePath, sessionId);
|
|
2398
2430
|
if (result) {
|
|
2399
|
-
entries.push({ meta: result.meta, content: result.content, scan });
|
|
2431
|
+
entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
|
|
2400
2432
|
}
|
|
2401
2433
|
else {
|
|
2402
2434
|
touched.push({ filePath, scan });
|
|
@@ -2428,7 +2460,7 @@ async function readRushMeta(filePath, sessionId) {
|
|
|
2428
2460
|
topic: scan.topic,
|
|
2429
2461
|
messageCount: scan.messageCount,
|
|
2430
2462
|
};
|
|
2431
|
-
return { meta, content: scan.contentText || '' };
|
|
2463
|
+
return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
|
|
2432
2464
|
}
|
|
2433
2465
|
/** Stream a Rush messages.jsonl file and extract scan-level metadata. */
|
|
2434
2466
|
async function scanRushSession(filePath) {
|
|
@@ -2439,6 +2471,7 @@ async function scanRushSession(filePath) {
|
|
|
2439
2471
|
let agentId;
|
|
2440
2472
|
let messageCount = 0;
|
|
2441
2473
|
const userTexts = [];
|
|
2474
|
+
const assistantTexts = [];
|
|
2442
2475
|
try {
|
|
2443
2476
|
for await (const line of rl) {
|
|
2444
2477
|
if (!line.trim())
|
|
@@ -2475,6 +2508,9 @@ async function scanRushSession(filePath) {
|
|
|
2475
2508
|
if (!topic)
|
|
2476
2509
|
topic = extractSessionTopic(cleaned);
|
|
2477
2510
|
}
|
|
2511
|
+
else if (parsed.role === 'assistant') {
|
|
2512
|
+
assistantTexts.push(cleaned);
|
|
2513
|
+
}
|
|
2478
2514
|
}
|
|
2479
2515
|
}
|
|
2480
2516
|
finally {
|
|
@@ -2487,6 +2523,7 @@ async function scanRushSession(filePath) {
|
|
|
2487
2523
|
agentId,
|
|
2488
2524
|
messageCount,
|
|
2489
2525
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
2526
|
+
assistantText: assistantTexts.length > 0 ? assistantTexts.join('\n') : undefined,
|
|
2490
2527
|
};
|
|
2491
2528
|
}
|
|
2492
2529
|
// ---------------------------------------------------------------------------
|
|
@@ -2529,7 +2566,7 @@ async function scanHermesIncremental(onProgress) {
|
|
|
2529
2566
|
const result = readHermesMeta(filePath);
|
|
2530
2567
|
if (result && !seen.has(result.meta.id)) {
|
|
2531
2568
|
seen.add(result.meta.id);
|
|
2532
|
-
scanEntries.push({ meta: result.meta, content: result.content, scan });
|
|
2569
|
+
scanEntries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
|
|
2533
2570
|
}
|
|
2534
2571
|
else {
|
|
2535
2572
|
touched.push({ filePath, scan });
|
|
@@ -2558,6 +2595,7 @@ function readHermesMeta(filePath) {
|
|
|
2558
2595
|
return null;
|
|
2559
2596
|
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
2560
2597
|
const userTexts = [];
|
|
2598
|
+
const assistantTexts = [];
|
|
2561
2599
|
let topic;
|
|
2562
2600
|
let messageCount = 0;
|
|
2563
2601
|
for (const msg of messages) {
|
|
@@ -2570,6 +2608,9 @@ function readHermesMeta(filePath) {
|
|
|
2570
2608
|
if (!topic)
|
|
2571
2609
|
topic = extractSessionTopic(text);
|
|
2572
2610
|
}
|
|
2611
|
+
else if (msg?.role === 'assistant') {
|
|
2612
|
+
assistantTexts.push(text);
|
|
2613
|
+
}
|
|
2573
2614
|
}
|
|
2574
2615
|
const stat = safeStatSync(filePath);
|
|
2575
2616
|
const timestamp = typeof session.last_updated === 'string'
|
|
@@ -2592,7 +2633,7 @@ function readHermesMeta(filePath) {
|
|
|
2592
2633
|
topic,
|
|
2593
2634
|
messageCount: messageCount || (typeof session.message_count === 'number' ? session.message_count : undefined),
|
|
2594
2635
|
};
|
|
2595
|
-
return { meta, content: userTexts.join('\n') };
|
|
2636
|
+
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
2596
2637
|
}
|
|
2597
2638
|
/**
|
|
2598
2639
|
* Muse Code stores one session per directory under
|
|
@@ -2660,7 +2701,7 @@ function scanMuseIncremental(onProgress) {
|
|
|
2660
2701
|
const result = readMuseMeta(filePath);
|
|
2661
2702
|
if (result && !seen.has(result.meta.id)) {
|
|
2662
2703
|
seen.add(result.meta.id);
|
|
2663
|
-
scanEntries.push({ meta: result.meta, content: result.content, scan });
|
|
2704
|
+
scanEntries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
|
|
2664
2705
|
}
|
|
2665
2706
|
else {
|
|
2666
2707
|
touched.push({ filePath, scan });
|
|
@@ -2690,6 +2731,7 @@ function readMuseMeta(filePath) {
|
|
|
2690
2731
|
return null;
|
|
2691
2732
|
}
|
|
2692
2733
|
const userTexts = [];
|
|
2734
|
+
const assistantTexts = [];
|
|
2693
2735
|
let topic;
|
|
2694
2736
|
let messageCount = 0;
|
|
2695
2737
|
let model;
|
|
@@ -2734,6 +2776,9 @@ function readMuseMeta(filePath) {
|
|
|
2734
2776
|
const event = payload?.event ?? payload;
|
|
2735
2777
|
if (event?.kind === 'assistant_message_committed' && typeof event.text === 'string') {
|
|
2736
2778
|
messageCount++;
|
|
2779
|
+
const text = event.text.trim();
|
|
2780
|
+
if (text)
|
|
2781
|
+
assistantTexts.push(text);
|
|
2737
2782
|
}
|
|
2738
2783
|
if (event?.kind === 'model_request_configured' && typeof event.model === 'string') {
|
|
2739
2784
|
model = event.model;
|
|
@@ -2757,7 +2802,7 @@ function readMuseMeta(filePath) {
|
|
|
2757
2802
|
topic,
|
|
2758
2803
|
messageCount: messageCount || undefined,
|
|
2759
2804
|
};
|
|
2760
|
-
return { meta, content: userTexts.join('\n') };
|
|
2805
|
+
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
2761
2806
|
}
|
|
2762
2807
|
/** Extract plain text from a Hermes message content field (string or list of parts). */
|
|
2763
2808
|
function extractHermesMessageText(content) {
|
|
@@ -2805,7 +2850,7 @@ async function scanDroidIncremental(onProgress) {
|
|
|
2805
2850
|
const result = await readDroidMeta(filePath, currentVersion);
|
|
2806
2851
|
if (result && !seen.has(result.meta.id)) {
|
|
2807
2852
|
seen.add(result.meta.id);
|
|
2808
|
-
entries.push({ meta: result.meta, content: result.content, scan });
|
|
2853
|
+
entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan });
|
|
2809
2854
|
}
|
|
2810
2855
|
else {
|
|
2811
2856
|
touched.push({ filePath, scan });
|
|
@@ -2868,7 +2913,7 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2868
2913
|
costUsdNoCache: costUsd > 0 ? costUsdNoCache : undefined,
|
|
2869
2914
|
durationMs: scan.durationMs,
|
|
2870
2915
|
};
|
|
2871
|
-
return { meta, content: scan.contentText || '' };
|
|
2916
|
+
return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
|
|
2872
2917
|
}
|
|
2873
2918
|
/** Read model + token usage from a Droid `<uuid>.settings.json` sidecar. */
|
|
2874
2919
|
function readDroidSettings(settingsPath) {
|
|
@@ -2911,6 +2956,7 @@ async function scanDroidSession(filePath) {
|
|
|
2911
2956
|
let firstTsMs;
|
|
2912
2957
|
let lastTsMs;
|
|
2913
2958
|
const userTexts = [];
|
|
2959
|
+
const assistantTexts = [];
|
|
2914
2960
|
try {
|
|
2915
2961
|
for await (const line of rl) {
|
|
2916
2962
|
if (!line.trim())
|
|
@@ -2960,6 +3006,9 @@ async function scanDroidSession(filePath) {
|
|
|
2960
3006
|
if (!firstUserTopic)
|
|
2961
3007
|
firstUserTopic = extractSessionTopic(text);
|
|
2962
3008
|
}
|
|
3009
|
+
else if (msg.role === 'assistant') {
|
|
3010
|
+
assistantTexts.push(text);
|
|
3011
|
+
}
|
|
2963
3012
|
}
|
|
2964
3013
|
}
|
|
2965
3014
|
finally {
|
|
@@ -2981,6 +3030,7 @@ async function scanDroidSession(filePath) {
|
|
|
2981
3030
|
durationMs,
|
|
2982
3031
|
lastActivity: lastTsMs !== undefined ? new Date(lastTsMs).toISOString() : undefined,
|
|
2983
3032
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
3033
|
+
assistantText: assistantTexts.length > 0 ? assistantTexts.join('\n') : undefined,
|
|
2984
3034
|
};
|
|
2985
3035
|
}
|
|
2986
3036
|
/** Extract plain text from a Droid message content field (Anthropic-shaped blocks). */
|
|
@@ -3026,6 +3076,7 @@ export function initClaudeParseState() {
|
|
|
3026
3076
|
lastTsMs: undefined,
|
|
3027
3077
|
seenAssistantIds: new Set(),
|
|
3028
3078
|
userTexts: [],
|
|
3079
|
+
assistantTexts: [],
|
|
3029
3080
|
sawPrCreate: false,
|
|
3030
3081
|
prUrl: undefined,
|
|
3031
3082
|
prNumber: undefined,
|
|
@@ -3217,6 +3268,9 @@ export function applyClaudeLine(state, parsed) {
|
|
|
3217
3268
|
return;
|
|
3218
3269
|
state.seenAssistantIds.add(logicalId);
|
|
3219
3270
|
state.messageCount++;
|
|
3271
|
+
const assistantText = extractClaudeAssistantText(parsed);
|
|
3272
|
+
if (assistantText)
|
|
3273
|
+
state.assistantTexts.push(assistantText);
|
|
3220
3274
|
const usageObj = parsed.message?.usage || parsed.usage;
|
|
3221
3275
|
const usage = getClaudeUsageTotal(usageObj);
|
|
3222
3276
|
if (usage !== null) {
|
|
@@ -3295,6 +3349,7 @@ export function finalizeClaudeScan(state) {
|
|
|
3295
3349
|
durationMs,
|
|
3296
3350
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
3297
3351
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3352
|
+
assistantText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
|
|
3298
3353
|
prUrl: state.prUrl,
|
|
3299
3354
|
prNumber: state.prNumber,
|
|
3300
3355
|
worktreeSlug: worktree?.slug,
|
|
@@ -3349,7 +3404,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3349
3404
|
: allIds;
|
|
3350
3405
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3351
3406
|
return {
|
|
3352
|
-
v:
|
|
3407
|
+
v: 5,
|
|
3353
3408
|
subAgents: state.subAgents,
|
|
3354
3409
|
backgroundShells: state.backgroundShells,
|
|
3355
3410
|
offset,
|
|
@@ -3391,6 +3446,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3391
3446
|
// not be persisted.
|
|
3392
3447
|
ticketId: ticket?.id,
|
|
3393
3448
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3449
|
+
assistantContentText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
|
|
3394
3450
|
checklistEvents: state.checklistEvents,
|
|
3395
3451
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3396
3452
|
toolCalls: state.toolCollector.snapshot(),
|
|
@@ -3449,6 +3505,9 @@ export function hydrateClaudeParseState(prior) {
|
|
|
3449
3505
|
lastTsMs: prior.lastTsMs,
|
|
3450
3506
|
seenAssistantIds: seen,
|
|
3451
3507
|
userTexts: prior.contentText !== undefined && prior.contentText.length > 0 ? [prior.contentText] : [],
|
|
3508
|
+
assistantTexts: prior.assistantContentText !== undefined && prior.assistantContentText.length > 0
|
|
3509
|
+
? [prior.assistantContentText]
|
|
3510
|
+
: [],
|
|
3452
3511
|
sawPrCreate: prior.sawPrCreate,
|
|
3453
3512
|
prUrl: prior.prUrl,
|
|
3454
3513
|
prNumber: prior.prNumber,
|
|
@@ -3592,7 +3651,7 @@ function parsePriorClaudeState(row) {
|
|
|
3592
3651
|
return null;
|
|
3593
3652
|
try {
|
|
3594
3653
|
const parsed = JSON.parse(row.parserState);
|
|
3595
|
-
if (parsed?.v !==
|
|
3654
|
+
if (parsed?.v !== 5 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3596
3655
|
return null;
|
|
3597
3656
|
return parsed;
|
|
3598
3657
|
}
|
|
@@ -3629,6 +3688,7 @@ export function initCodexParseState() {
|
|
|
3629
3688
|
firstTsMs: undefined,
|
|
3630
3689
|
lastTsMs: undefined,
|
|
3631
3690
|
userTexts: [],
|
|
3691
|
+
assistantTexts: [],
|
|
3632
3692
|
sawPrCreate: false,
|
|
3633
3693
|
prUrl: undefined,
|
|
3634
3694
|
prNumber: undefined,
|
|
@@ -3738,6 +3798,9 @@ function applyCodexLine(state, parsed) {
|
|
|
3738
3798
|
if (!state.topic)
|
|
3739
3799
|
state.topic = extractSessionTopic(text);
|
|
3740
3800
|
}
|
|
3801
|
+
else {
|
|
3802
|
+
state.assistantTexts.push(text);
|
|
3803
|
+
}
|
|
3741
3804
|
return;
|
|
3742
3805
|
}
|
|
3743
3806
|
if (parsed.type === 'event_msg' && parsed.payload?.type === 'token_count') {
|
|
@@ -3813,6 +3876,7 @@ function finalizeCodexScan(state) {
|
|
|
3813
3876
|
durationMs,
|
|
3814
3877
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
3815
3878
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3879
|
+
assistantText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
|
|
3816
3880
|
prUrl: state.prUrl,
|
|
3817
3881
|
prNumber: state.prNumber,
|
|
3818
3882
|
worktreeSlug: worktree?.slug,
|
|
@@ -3856,7 +3920,7 @@ async function scanCodexSession(filePath) {
|
|
|
3856
3920
|
export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
3857
3921
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3858
3922
|
return {
|
|
3859
|
-
v:
|
|
3923
|
+
v: 3,
|
|
3860
3924
|
offset,
|
|
3861
3925
|
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3862
3926
|
sessionId: state.sessionId,
|
|
@@ -3883,6 +3947,7 @@ export function serializeCodexParserState(state, offset, jsonlDroppingOversizedL
|
|
|
3883
3947
|
// be persisted.
|
|
3884
3948
|
ticketId: ticket?.id,
|
|
3885
3949
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3950
|
+
assistantContentText: state.assistantTexts.length > 0 ? state.assistantTexts.join('\n') : undefined,
|
|
3886
3951
|
checklistEvents: state.checklistEvents,
|
|
3887
3952
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3888
3953
|
toolCalls: state.toolCollector.snapshot(),
|
|
@@ -3913,6 +3978,9 @@ function hydrateCodexParseState(prior) {
|
|
|
3913
3978
|
firstTsMs: prior.firstTsMs,
|
|
3914
3979
|
lastTsMs: prior.lastTsMs,
|
|
3915
3980
|
userTexts: prior.contentText !== undefined && prior.contentText.length > 0 ? [prior.contentText] : [],
|
|
3981
|
+
assistantTexts: prior.assistantContentText !== undefined && prior.assistantContentText.length > 0
|
|
3982
|
+
? [prior.assistantContentText]
|
|
3983
|
+
: [],
|
|
3916
3984
|
sawPrCreate: prior.sawPrCreate,
|
|
3917
3985
|
prUrl: prior.prUrl,
|
|
3918
3986
|
prNumber: prior.prNumber,
|
|
@@ -4043,7 +4111,7 @@ function parsePriorCodexState(row) {
|
|
|
4043
4111
|
return null;
|
|
4044
4112
|
try {
|
|
4045
4113
|
const parsed = JSON.parse(row.parserState);
|
|
4046
|
-
if (parsed?.v !==
|
|
4114
|
+
if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
4047
4115
|
return null;
|
|
4048
4116
|
return parsed;
|
|
4049
4117
|
}
|
|
@@ -4144,6 +4212,29 @@ function extractClaudeUserText(parsed) {
|
|
|
4144
4212
|
function isLocalCommandMessage(text) {
|
|
4145
4213
|
return /<local-command-caveat>|<bash-(input|stdout|stderr)>/i.test(text);
|
|
4146
4214
|
}
|
|
4215
|
+
/**
|
|
4216
|
+
* Extract the assistant's own answer text from a Claude JSONL assistant event —
|
|
4217
|
+
* the reply text blocks, skipping tool_use/tool_result blocks and an
|
|
4218
|
+
* interrupted turn, mirroring {@link extractClaudeUserText}'s filters. Unlike
|
|
4219
|
+
* the user side (which keeps only the first text block), a single assistant
|
|
4220
|
+
* turn can legitimately carry multiple text blocks around tool calls, so every
|
|
4221
|
+
* one is joined.
|
|
4222
|
+
*/
|
|
4223
|
+
function extractClaudeAssistantText(parsed) {
|
|
4224
|
+
const content = parsed.message?.content;
|
|
4225
|
+
if (typeof content === 'string') {
|
|
4226
|
+
const text = content.trim();
|
|
4227
|
+
return text && !text.startsWith('[Request interrupted') ? text : undefined;
|
|
4228
|
+
}
|
|
4229
|
+
if (!Array.isArray(content))
|
|
4230
|
+
return undefined;
|
|
4231
|
+
const text = content
|
|
4232
|
+
.filter((block) => block.type === 'text')
|
|
4233
|
+
.map((block) => String(block.text || '').trim())
|
|
4234
|
+
.filter((value) => value && !value.startsWith('[Request interrupted'))
|
|
4235
|
+
.join('\n');
|
|
4236
|
+
return text || undefined;
|
|
4237
|
+
}
|
|
4147
4238
|
/** Sum all token usage fields from a Claude assistant message's usage object. */
|
|
4148
4239
|
function getClaudeUsageTotal(usage) {
|
|
4149
4240
|
if (!usage || typeof usage !== 'object')
|
|
@@ -4308,7 +4399,7 @@ async function scanCursorIncremental(onProgress) {
|
|
|
4308
4399
|
const result = readCursorMeta(filePath, currentVersion);
|
|
4309
4400
|
if (result && !seen.has(result.meta.id)) {
|
|
4310
4401
|
seen.add(result.meta.id);
|
|
4311
|
-
entries.push({ meta: result.meta, content: result.content, scan, events: result.events });
|
|
4402
|
+
entries.push({ meta: result.meta, content: result.content, assistantContent: result.assistantContent, scan, events: result.events });
|
|
4312
4403
|
}
|
|
4313
4404
|
else {
|
|
4314
4405
|
touched.push({ filePath, scan });
|
|
@@ -4385,6 +4476,9 @@ export function readCursorMeta(filePath, currentVersion) {
|
|
|
4385
4476
|
const userTexts = events
|
|
4386
4477
|
.filter((event) => event.type === 'message' && event.role === 'user' && event.content)
|
|
4387
4478
|
.map((event) => event.content);
|
|
4479
|
+
const assistantTexts = events
|
|
4480
|
+
.filter((event) => event.type === 'message' && event.role === 'assistant' && event.content)
|
|
4481
|
+
.map((event) => event.content);
|
|
4388
4482
|
const firstUserText = userTexts[0];
|
|
4389
4483
|
const title = typeof chatMeta?.title === 'string'
|
|
4390
4484
|
? cleanGeneratedSessionLabel(chatMeta.title)
|
|
@@ -4404,7 +4498,7 @@ export function readCursorMeta(filePath, currentVersion) {
|
|
|
4404
4498
|
messageCount: events.filter((event) => event.type === 'message').length,
|
|
4405
4499
|
todos: extractTodoProgressFromEvents(events),
|
|
4406
4500
|
};
|
|
4407
|
-
return { meta, content: userTexts.join('\n'), events };
|
|
4501
|
+
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n'), events };
|
|
4408
4502
|
}
|
|
4409
4503
|
// ---------------------------------------------------------------------------
|
|
4410
4504
|
// Kimi
|
|
@@ -85,6 +85,20 @@ export interface InsightFacets {
|
|
|
85
85
|
assistantTurns: number;
|
|
86
86
|
toolCount: number;
|
|
87
87
|
errorCount: number;
|
|
88
|
+
/**
|
|
89
|
+
* Shell invocations bucketed by the actual executable + first subcommand token, not
|
|
90
|
+
* lumped under the harness tool name. `bucketKey` collapses each command to
|
|
91
|
+
* `git commit`, `gh pr`, `agents ssh`, `find`, `ssh→git pull`, … so the tool mix says
|
|
92
|
+
* which binary ran, not just that `Bash` did. Cross-harness: keyed on `e.command`,
|
|
93
|
+
* which the codex parser sets for `exec_command` too, so it is not Claude-`Bash`-only.
|
|
94
|
+
*/
|
|
95
|
+
bashCommands: Record<string, number>;
|
|
96
|
+
/**
|
|
97
|
+
* Same bucketing for the shell command that was running when a tool call failed —
|
|
98
|
+
* turns one `failed tool loop: Bash` line into per-binary failure counts
|
|
99
|
+
* (`git reconcile`, `find`, `gh`) so the friction is attributable to a command.
|
|
100
|
+
*/
|
|
101
|
+
bashCommandFailures: Record<string, number>;
|
|
88
102
|
/** Deterministic evidence buckets used by the actions-forward report. */
|
|
89
103
|
frictionSignals: Record<string, number>;
|
|
90
104
|
correctionSignals: Record<string, number>;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { computeSummaryStats, shortenModel } from './render.js';
|
|
24
24
|
import { classifyFileChanges, EDIT_TOOLS, WRITE_TOOLS } from './digest.js';
|
|
25
|
+
import { bucketKey } from './bash-command.js';
|
|
25
26
|
/** File extension → language label. Mirrors the set `/insights` attributes by. */
|
|
26
27
|
const LANGUAGE_BY_EXT = {
|
|
27
28
|
'.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript', '.cts': 'TypeScript',
|
|
@@ -82,6 +83,7 @@ function emptyFacets() {
|
|
|
82
83
|
shellCommandsSeen: 0,
|
|
83
84
|
messageHours: new Array(24).fill(0), userTurns: 0, assistantTurns: 0,
|
|
84
85
|
toolCount: 0, errorCount: 0,
|
|
86
|
+
bashCommands: {}, bashCommandFailures: {},
|
|
85
87
|
frictionSignals: {}, correctionSignals: {}, automationSignals: {},
|
|
86
88
|
};
|
|
87
89
|
}
|
|
@@ -172,6 +174,9 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
172
174
|
/** Shortened model id of the last assistant activity (for stall attribution). */
|
|
173
175
|
let lastAssistantModel = null;
|
|
174
176
|
let lastFailedTool = null;
|
|
177
|
+
// Most recent shell command seen per tool, so an `error` event (which carries the
|
|
178
|
+
// failing tool name but not its command) can attribute the failure to a binary.
|
|
179
|
+
const lastCommandByTool = {};
|
|
175
180
|
for (const e of events) {
|
|
176
181
|
const ts = new Date(e.timestamp).getTime();
|
|
177
182
|
const hasTs = !Number.isNaN(ts);
|
|
@@ -191,16 +196,27 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
191
196
|
lastAssistantModel = m;
|
|
192
197
|
}
|
|
193
198
|
break;
|
|
194
|
-
case 'error':
|
|
199
|
+
case 'error': {
|
|
195
200
|
bump(f.errorCategories, categorizeError(e.content ?? e.output ?? ''));
|
|
196
201
|
if (e.tool && e.tool === lastFailedTool)
|
|
197
202
|
bump(f.frictionSignals, `failed tool loop: ${e.tool}`);
|
|
203
|
+
// Attribute the failure to the binary of that tool's most recent command,
|
|
204
|
+
// so `Bash` failures split into `git reconcile`/`find`/`gh` rather than one lump.
|
|
205
|
+
const failedCmd = e.tool ? lastCommandByTool[e.tool] : undefined;
|
|
206
|
+
if (failedCmd)
|
|
207
|
+
bump(f.bashCommandFailures, bucketKey(failedCmd));
|
|
198
208
|
lastFailedTool = e.tool ?? null;
|
|
199
209
|
classifyFriction(e.content ?? e.output ?? '', f.frictionSignals);
|
|
200
210
|
break;
|
|
211
|
+
}
|
|
201
212
|
case 'tool_result':
|
|
202
|
-
if (e.success !== false && e.outcome !== 'error')
|
|
213
|
+
if (e.success !== false && e.outcome !== 'error') {
|
|
203
214
|
lastFailedTool = null;
|
|
215
|
+
// Drop the succeeded tool's remembered command so a later failing call that
|
|
216
|
+
// carries no command of its own is not misattributed to this stale binary.
|
|
217
|
+
if (e.tool)
|
|
218
|
+
delete lastCommandByTool[e.tool];
|
|
219
|
+
}
|
|
204
220
|
break;
|
|
205
221
|
case 'message':
|
|
206
222
|
if (e.role === 'assistant') {
|
|
@@ -277,6 +293,9 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
277
293
|
f.gitCommits += countGitOp(e.command, 'commit');
|
|
278
294
|
f.gitPushes += countGitOp(e.command, 'push');
|
|
279
295
|
classifyAutomation(e.command, f.automationSignals);
|
|
296
|
+
bump(f.bashCommands, bucketKey(e.command));
|
|
297
|
+
if (toolName)
|
|
298
|
+
lastCommandByTool[toolName] = e.command;
|
|
280
299
|
}
|
|
281
300
|
break;
|
|
282
301
|
}
|
|
@@ -430,6 +449,10 @@ export function mergeFacets(into, add) {
|
|
|
430
449
|
bump(into.slashCommands, k, v);
|
|
431
450
|
for (const [k, v] of Object.entries(add.errorCategories))
|
|
432
451
|
bump(into.errorCategories, k, v);
|
|
452
|
+
for (const [k, v] of Object.entries(add.bashCommands ?? {}))
|
|
453
|
+
bump(into.bashCommands, k, v);
|
|
454
|
+
for (const [k, v] of Object.entries(add.bashCommandFailures ?? {}))
|
|
455
|
+
bump(into.bashCommandFailures, k, v);
|
|
433
456
|
for (const [k, v] of Object.entries(add.frictionSignals ?? {}))
|
|
434
457
|
bump(into.frictionSignals, k, v);
|
|
435
458
|
for (const [k, v] of Object.entries(add.correctionSignals ?? {}))
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 2. `workspaceUrlKey` in the linear-cli config (`~/.linear-cli/config.json`).
|
|
10
10
|
*
|
|
11
11
|
* When neither is set the ticket stays plain text — it's still shown, just not
|
|
12
|
-
* linked. This keeps the
|
|
12
|
+
* linked. This keeps the published CLI free of any Rush/workspace-specific string.
|
|
13
13
|
*/
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import os from 'node:os';
|
|
@@ -9,6 +9,23 @@ export interface ShellProgramOccurrence {
|
|
|
9
9
|
program: string;
|
|
10
10
|
role: ShellProgramRole;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Harness tool NAMES that run a shell command — Claude's `Bash`, Codex's
|
|
14
|
+
* `exec_command`/`exec` (`exec` is newer gpt-5.6-sol), Droid's `Execute`, plus the
|
|
15
|
+
* generic `shell`/`run_shell_command`/`run_command`/`execute`. This is the single
|
|
16
|
+
* source of truth for "does this tool row carry a shell command?", consumed by the
|
|
17
|
+
* trajectory model (program resolution + command-arg extraction), the directory-touched
|
|
18
|
+
* scan in `state.ts`, the tool-call indexer, and stream rendering — replacing five
|
|
19
|
+
* hand-synced copies that had already drifted apart.
|
|
20
|
+
*
|
|
21
|
+
* Distinct from {@link SHELL_WRAPPERS}, which is interpreter binaries (`bash`, `sh`…)
|
|
22
|
+
* found INSIDE a command string, not the harness's tool name. Match through
|
|
23
|
+
* {@link isShellExecTool} so casing is normalized in one place — harnesses disagree on
|
|
24
|
+
* case (`Bash` vs `bash`, `Execute` vs `exec_command`).
|
|
25
|
+
*/
|
|
26
|
+
export declare const SHELL_EXEC_TOOLS: Set<string>;
|
|
27
|
+
/** Case-insensitive membership test for {@link SHELL_EXEC_TOOLS}. */
|
|
28
|
+
export declare function isShellExecTool(tool: string | undefined): boolean;
|
|
12
29
|
/** Return a word only when Bash expansion cannot change its value. */
|
|
13
30
|
export declare function staticShellWord(word: Word | undefined): string | undefined;
|
|
14
31
|
/** Parse Bash without executing it and return every statically identifiable program. */
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { parse, } from 'unbash';
|
|
3
|
+
/**
|
|
4
|
+
* Harness tool NAMES that run a shell command — Claude's `Bash`, Codex's
|
|
5
|
+
* `exec_command`/`exec` (`exec` is newer gpt-5.6-sol), Droid's `Execute`, plus the
|
|
6
|
+
* generic `shell`/`run_shell_command`/`run_command`/`execute`. This is the single
|
|
7
|
+
* source of truth for "does this tool row carry a shell command?", consumed by the
|
|
8
|
+
* trajectory model (program resolution + command-arg extraction), the directory-touched
|
|
9
|
+
* scan in `state.ts`, the tool-call indexer, and stream rendering — replacing five
|
|
10
|
+
* hand-synced copies that had already drifted apart.
|
|
11
|
+
*
|
|
12
|
+
* Distinct from {@link SHELL_WRAPPERS}, which is interpreter binaries (`bash`, `sh`…)
|
|
13
|
+
* found INSIDE a command string, not the harness's tool name. Match through
|
|
14
|
+
* {@link isShellExecTool} so casing is normalized in one place — harnesses disagree on
|
|
15
|
+
* case (`Bash` vs `bash`, `Execute` vs `exec_command`).
|
|
16
|
+
*/
|
|
17
|
+
export const SHELL_EXEC_TOOLS = new Set([
|
|
18
|
+
'bash', 'exec', 'execute', 'exec_command', 'run_command', 'run_shell_command', 'shell',
|
|
19
|
+
]);
|
|
20
|
+
/** Case-insensitive membership test for {@link SHELL_EXEC_TOOLS}. */
|
|
21
|
+
export function isShellExecTool(tool) {
|
|
22
|
+
return tool !== undefined && SHELL_EXEC_TOOLS.has(tool.toLowerCase());
|
|
23
|
+
}
|
|
3
24
|
const SHELL_WRAPPERS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
|
|
4
25
|
const PROGRAM_WRAPPERS = new Set(['command', 'builtin', 'env', 'nohup', 'sudo']);
|
|
5
26
|
const MAX_NESTED_DEPTH = 3;
|