@phnx-labs/agents-cli 1.21.1 → 1.21.3
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 +206 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +34 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +28 -19
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/perf.d.ts +29 -0
- package/dist/commands/perf.js +90 -23
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.js +169 -23
- package/dist/commands/routines.js +46 -1
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +5 -0
- package/dist/commands/sessions.js +14 -0
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +22 -1
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +31 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/feed-broadcast.d.ts +52 -7
- package/dist/lib/feed-broadcast.js +125 -18
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +15 -18
- package/dist/lib/hooks.js +72 -17
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +7 -3
- package/dist/lib/perf/db.js +37 -19
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +26 -0
- package/dist/lib/runner.js +41 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +17 -1
- package/dist/lib/session/db.js +240 -6
- package/dist/lib/session/discover.d.ts +31 -0
- package/dist/lib/session/discover.js +50 -1
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.js +19 -3
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/types.d.ts +43 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +22 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
package/dist/lib/session/db.js
CHANGED
|
@@ -12,14 +12,18 @@ import Database from '../sqlite.js';
|
|
|
12
12
|
import { parseSession } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
|
+
import { query as queryEvents } from '../events.js';
|
|
15
16
|
import { machineForSessionFile } from './origin-machine.js';
|
|
16
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
|
+
import { extractSkills, extractSlashCommands } from './highlights.js';
|
|
19
|
+
import { resolveResource } from '../resources.js';
|
|
20
|
+
import { discoverPlugins } from '../plugins.js';
|
|
17
21
|
const SESSIONS_DIR = getSessionsDir();
|
|
18
22
|
const DB_PATH = getSessionsDbPath();
|
|
19
23
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
20
24
|
* assert against the constant instead of hardcoding a number that every bump
|
|
21
25
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
22
|
-
export const SCHEMA_VERSION =
|
|
26
|
+
export const SCHEMA_VERSION = 24;
|
|
23
27
|
/**
|
|
24
28
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
25
29
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -67,6 +71,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
67
71
|
cost_usd REAL,
|
|
68
72
|
duration_ms INTEGER,
|
|
69
73
|
model TEXT,
|
|
74
|
+
tool_call_count INTEGER,
|
|
70
75
|
file_path TEXT NOT NULL,
|
|
71
76
|
file_mtime_ms INTEGER,
|
|
72
77
|
file_size INTEGER,
|
|
@@ -84,7 +89,9 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
84
89
|
linear_project TEXT,
|
|
85
90
|
linear_project_url TEXT,
|
|
86
91
|
actor TEXT,
|
|
87
|
-
initiated_by TEXT
|
|
92
|
+
initiated_by TEXT,
|
|
93
|
+
used_browser INTEGER,
|
|
94
|
+
used_computer INTEGER
|
|
88
95
|
);
|
|
89
96
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
90
97
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -139,6 +146,29 @@ CREATE TABLE IF NOT EXISTS dir_ledger (
|
|
|
139
146
|
entry_count INTEGER NOT NULL,
|
|
140
147
|
scanned_at INTEGER NOT NULL
|
|
141
148
|
);
|
|
149
|
+
|
|
150
|
+
-- Skill/slash-command usage per session (#12), computed from a session's
|
|
151
|
+
-- parsed transcript (extractSkills / extractSlashCommands, session/highlights.ts)
|
|
152
|
+
-- and joined at write time against the currently-installed resource/plugin
|
|
153
|
+
-- (resolveResource / discoverPlugins) for provenance — repo_root + snapshot_sha
|
|
154
|
+
-- answer "which repo, which commit installed this skill/command", plugin/source
|
|
155
|
+
-- answer "which plugin, which DotAgents layer". A resource renamed or uninstalled
|
|
156
|
+
-- since the session ran leaves plugin/source/repo_root/snapshot_sha NULL rather
|
|
157
|
+
-- than a stale guess. One row per (session, kind, name); count is how many
|
|
158
|
+
-- times that skill/command fired in the session.
|
|
159
|
+
CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
160
|
+
session_id TEXT NOT NULL,
|
|
161
|
+
kind TEXT NOT NULL,
|
|
162
|
+
name TEXT NOT NULL,
|
|
163
|
+
plugin TEXT,
|
|
164
|
+
source TEXT,
|
|
165
|
+
repo_root TEXT,
|
|
166
|
+
snapshot_sha TEXT,
|
|
167
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
168
|
+
PRIMARY KEY (session_id, kind, name)
|
|
169
|
+
);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
142
172
|
`;
|
|
143
173
|
let dbInstance = null;
|
|
144
174
|
/**
|
|
@@ -399,6 +429,57 @@ function migrateSchema(db, fromVersion) {
|
|
|
399
429
|
db.exec(`ALTER TABLE sessions ADD COLUMN spawned_team TEXT`);
|
|
400
430
|
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
401
431
|
}
|
|
432
|
+
if (fromVersion < 22) {
|
|
433
|
+
// v21 → v22 (main): persist the transcript's tool-call count.
|
|
434
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
435
|
+
if (!cols.some(c => c.name === 'tool_call_count'))
|
|
436
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN tool_call_count INTEGER`);
|
|
437
|
+
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
438
|
+
}
|
|
439
|
+
if (fromVersion < 23) {
|
|
440
|
+
// v22 → v23: persist usedBrowser/usedComputer (#11) so the sessions picker
|
|
441
|
+
// preview can trust a positive detection instead of re-deriving it from a
|
|
442
|
+
// transcript regex on every render. Computed from a sessionId-scoped read
|
|
443
|
+
// of the events log (see detectToolUsage), not from the parsed transcript,
|
|
444
|
+
// so no ledger wipe is needed here — a rescan re-derives them regardless.
|
|
445
|
+
//
|
|
446
|
+
// Deliberately NO DEFAULT (NULL on ALTER, for every pre-existing row):
|
|
447
|
+
// NULL means "not yet computed by this scanner" (a legacy row), distinct
|
|
448
|
+
// from a real, computed 0/false. Collapsing that into a DEFAULT 0 would
|
|
449
|
+
// make every un-rescanned row look like a definite "never used
|
|
450
|
+
// browser/computer" and permanently blind the picker's fallback path for
|
|
451
|
+
// any row that isn't rescanned before it's next viewed.
|
|
452
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
453
|
+
if (!cols.some(c => c.name === 'used_browser'))
|
|
454
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_browser INTEGER`);
|
|
455
|
+
if (!cols.some(c => c.name === 'used_computer'))
|
|
456
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_computer INTEGER`);
|
|
457
|
+
}
|
|
458
|
+
if (fromVersion < 24) {
|
|
459
|
+
// v23 → v24: session_resource_usage (#12) — skill/slash-command usage per
|
|
460
|
+
// session, joined against the currently-installed resource/plugin for
|
|
461
|
+
// provenance (repo_root/snapshot_sha/plugin/source). A brand-new table
|
|
462
|
+
// needs only CREATE TABLE IF NOT EXISTS, already present in SCHEMA for a
|
|
463
|
+
// fresh DB — this repeats it for an existing DB whose SCHEMA exec ran
|
|
464
|
+
// before the table definition was added. No ledger wipe: this table is
|
|
465
|
+
// populated by writeResourceUsage() at upsert time, independent of the
|
|
466
|
+
// scan ledgers.
|
|
467
|
+
db.exec(`
|
|
468
|
+
CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
469
|
+
session_id TEXT NOT NULL,
|
|
470
|
+
kind TEXT NOT NULL,
|
|
471
|
+
name TEXT NOT NULL,
|
|
472
|
+
plugin TEXT,
|
|
473
|
+
source TEXT,
|
|
474
|
+
repo_root TEXT,
|
|
475
|
+
snapshot_sha TEXT,
|
|
476
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
477
|
+
PRIMARY KEY (session_id, kind, name)
|
|
478
|
+
);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
402
483
|
}
|
|
403
484
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
404
485
|
export function getDB() {
|
|
@@ -758,20 +839,20 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
758
839
|
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
759
840
|
version, account, timestamp, last_activity,
|
|
760
841
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
761
|
-
output_tokens, cost_usd, duration_ms, model,
|
|
842
|
+
output_tokens, cost_usd, duration_ms, model, tool_call_count,
|
|
762
843
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
763
844
|
pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
|
|
764
845
|
recent_directories_touched, linear_project, linear_project_url, machine,
|
|
765
|
-
actor, initiated_by
|
|
846
|
+
actor, initiated_by, used_browser, used_computer
|
|
766
847
|
) VALUES (
|
|
767
848
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
768
849
|
@version, @account, @timestamp, @last_activity,
|
|
769
850
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
770
|
-
@output_tokens, @cost_usd, @duration_ms, @model,
|
|
851
|
+
@output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
|
|
771
852
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
772
853
|
@pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
|
|
773
854
|
@recent_directories_touched, @linear_project, @linear_project_url, @machine,
|
|
774
|
-
@actor, @initiated_by
|
|
855
|
+
@actor, @initiated_by, @used_browser, @used_computer
|
|
775
856
|
)
|
|
776
857
|
ON CONFLICT(id) DO UPDATE SET
|
|
777
858
|
short_id = excluded.short_id,
|
|
@@ -802,6 +883,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
802
883
|
cost_usd = excluded.cost_usd,
|
|
803
884
|
duration_ms = excluded.duration_ms,
|
|
804
885
|
model = excluded.model,
|
|
886
|
+
tool_call_count = excluded.tool_call_count,
|
|
805
887
|
file_path = excluded.file_path,
|
|
806
888
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
807
889
|
file_size = excluded.file_size,
|
|
@@ -815,6 +897,8 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
815
897
|
plan = excluded.plan,
|
|
816
898
|
todos = excluded.todos,
|
|
817
899
|
recent_directories_touched = excluded.recent_directories_touched,
|
|
900
|
+
used_browser = excluded.used_browser,
|
|
901
|
+
used_computer = excluded.used_computer,
|
|
818
902
|
linear_project = CASE
|
|
819
903
|
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project
|
|
820
904
|
ELSE COALESCE(excluded.linear_project, sessions.linear_project)
|
|
@@ -833,11 +917,124 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
833
917
|
actor = COALESCE(sessions.actor, excluded.actor),
|
|
834
918
|
initiated_by = COALESCE(sessions.initiated_by, excluded.initiated_by)
|
|
835
919
|
`);
|
|
920
|
+
/**
|
|
921
|
+
* Did this session emit at least one browser/computer-automation event?
|
|
922
|
+
* A scoped, sessionId-filtered read of the events log — NOT a re-scan of the
|
|
923
|
+
* (potentially huge) transcript — since browser.navigate / browser.screenshot
|
|
924
|
+
* / computer.action are recorded there via events.ts's emit(), keyed by the
|
|
925
|
+
* same session id that is this row's `meta.id` (the provenance floor stamps
|
|
926
|
+
* AGENT_SESSION_ID/AGENTS_SESSION_ID on every such event at emit time).
|
|
927
|
+
*
|
|
928
|
+
* Called independently of {@link enrichCachedSessionMeta} (which SKIPS
|
|
929
|
+
* claude/codex entries in the batch path because their caller already parsed
|
|
930
|
+
* the transcript for todos/recentDirectoriesTouched) — this never touches the
|
|
931
|
+
* transcript, so it must run for every agent, every time.
|
|
932
|
+
*/
|
|
933
|
+
function detectToolUsage(sessionId) {
|
|
934
|
+
const usedBrowser = queryEvents({ sessionId, eventTypes: ['browser.navigate', 'browser.screenshot'], limit: 1 }).length > 0;
|
|
935
|
+
const usedComputer = queryEvents({ sessionId, eventTypes: ['computer.action'], limit: 1 }).length > 0;
|
|
936
|
+
return { usedBrowser, usedComputer };
|
|
937
|
+
}
|
|
938
|
+
const deleteResourceUsageStmt = (db) => db.prepare(`DELETE FROM session_resource_usage WHERE session_id = ?`);
|
|
939
|
+
const insertResourceUsageStmt = (db) => db.prepare(`
|
|
940
|
+
INSERT INTO session_resource_usage (session_id, kind, name, plugin, source, repo_root, snapshot_sha, count)
|
|
941
|
+
VALUES (@session_id, @kind, @name, @plugin, @source, @repo_root, @snapshot_sha, @count)
|
|
942
|
+
`);
|
|
943
|
+
/**
|
|
944
|
+
* Resolve a skill/slash-command's provenance for `session_resource_usage`
|
|
945
|
+
* (#12). A flat (non-namespaced) resource resolves via resolveResource()'s
|
|
946
|
+
* project/user/system/extra-repo scan. A namespaced one (`plugin:name`, e.g.
|
|
947
|
+
* `rush:design` — the shape both a plugin skill's `args.skill` and a plugin
|
|
948
|
+
* command's SessionEvent.slashCommand carry) is plugin-owned and lives under
|
|
949
|
+
* the plugin's own directory, invisible to resolveResource()'s flat scan —
|
|
950
|
+
* resolved instead against the already-discovered plugin list. Neither found
|
|
951
|
+
* (renamed or uninstalled since the session ran) returns all-undefined
|
|
952
|
+
* rather than a stale guess.
|
|
953
|
+
*/
|
|
954
|
+
function resolveResourceProvenance(kind, name, cwd, plugins) {
|
|
955
|
+
const listOf = (p) => (kind === 'skills' ? p.skills : p.commands);
|
|
956
|
+
const colonIdx = name.indexOf(':');
|
|
957
|
+
if (colonIdx > 0) {
|
|
958
|
+
const pluginName = name.slice(0, colonIdx);
|
|
959
|
+
const shortName = name.slice(colonIdx + 1);
|
|
960
|
+
const plugin = plugins.find((p) => p.name === pluginName && listOf(p).includes(shortName));
|
|
961
|
+
if (!plugin)
|
|
962
|
+
return {};
|
|
963
|
+
return { plugin: plugin.name, source: plugin.marketplace, repoRoot: plugin.repoRoot, snapshotSha: plugin.snapshotSha };
|
|
964
|
+
}
|
|
965
|
+
const resolved = resolveResource(kind, name, cwd);
|
|
966
|
+
if (resolved)
|
|
967
|
+
return { source: resolved.source, repoRoot: resolved.repoRoot, snapshotSha: resolved.snapshotSha };
|
|
968
|
+
const plugin = plugins.find((p) => listOf(p).includes(name));
|
|
969
|
+
if (!plugin)
|
|
970
|
+
return {};
|
|
971
|
+
return { plugin: plugin.name, source: plugin.marketplace, repoRoot: plugin.repoRoot, snapshotSha: plugin.snapshotSha };
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Persist already-computed skill/slash-command tallies for a session (#12)
|
|
975
|
+
* into `session_resource_usage`, replacing any prior rows for it (a rescan's
|
|
976
|
+
* usage supersedes the old — same replace-on-upsert shape as the FTS text
|
|
977
|
+
* below). `discoverPlugins()` (real I/O: manifest + directory reads) only
|
|
978
|
+
* runs when there is actually a skill/command to resolve, so a session with
|
|
979
|
+
* neither pays nothing beyond the DELETE.
|
|
980
|
+
*
|
|
981
|
+
* Split from {@link writeResourceUsage} so a caller that already has the
|
|
982
|
+
* tallies (claude/codex's incremental accumulator — see
|
|
983
|
+
* ClaudeParseState.skillEvents/slashCommandEvents, threaded onto
|
|
984
|
+
* SessionMeta.skillsUsed/slashCommandsUsed) can write without re-parsing the
|
|
985
|
+
* transcript to re-derive them.
|
|
986
|
+
*
|
|
987
|
+
* Deliberately NOT wrapped in its own `db.transaction()`: better-sqlite3
|
|
988
|
+
* (this repo's wrapper included) does not support nested transactions, and
|
|
989
|
+
* `upsertSessionsBatch` calls this from INSIDE its own outer transaction. A
|
|
990
|
+
* standalone caller (enrichCachedSessionMeta, outside any transaction) still
|
|
991
|
+
* gets each statement committed individually — a crash between the DELETE
|
|
992
|
+
* and an INSERT self-heals on the next rescan, the same risk profile as any
|
|
993
|
+
* other un-batched write in this file.
|
|
994
|
+
*/
|
|
995
|
+
function writeResourceUsageFromTallies(sessionId, skills, commands, cwd) {
|
|
996
|
+
const db = getDB();
|
|
997
|
+
const del = deleteResourceUsageStmt(db);
|
|
998
|
+
const ins = insertResourceUsageStmt(db);
|
|
999
|
+
del.run(sessionId);
|
|
1000
|
+
if (skills.length === 0 && commands.length === 0)
|
|
1001
|
+
return;
|
|
1002
|
+
const plugins = discoverPlugins({ cwd });
|
|
1003
|
+
for (const { name, count } of skills) {
|
|
1004
|
+
const prov = resolveResourceProvenance('skills', name, cwd, plugins);
|
|
1005
|
+
ins.run({
|
|
1006
|
+
session_id: sessionId, kind: 'skill', name, count,
|
|
1007
|
+
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1008
|
+
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
for (const { name, count } of commands) {
|
|
1012
|
+
const bare = name.replace(/^\//, '');
|
|
1013
|
+
const prov = resolveResourceProvenance('commands', bare, cwd, plugins);
|
|
1014
|
+
ins.run({
|
|
1015
|
+
session_id: sessionId, kind: 'command', name: bare, count,
|
|
1016
|
+
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1017
|
+
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Persist skill/slash-command usage for a session (#12) by deriving the
|
|
1023
|
+
* tallies from a full parsed transcript. Used by {@link enrichCachedSessionMeta}
|
|
1024
|
+
* (every `upsertSession()` call, and `upsertSessionsBatch` for every harness
|
|
1025
|
+
* EXCEPT claude/codex, which pre-compute skillsUsed/slashCommandsUsed via
|
|
1026
|
+
* their incremental accumulator instead — see writeResourceUsageFromTallies
|
|
1027
|
+
* and the call site in upsertSessionsBatch).
|
|
1028
|
+
*/
|
|
1029
|
+
function writeResourceUsage(sessionId, events, cwd) {
|
|
1030
|
+
writeResourceUsageFromTallies(sessionId, extractSkills(events), extractSlashCommands(events), cwd);
|
|
1031
|
+
}
|
|
836
1032
|
function enrichCachedSessionMeta(meta) {
|
|
837
1033
|
if (!meta.filePath)
|
|
838
1034
|
return meta;
|
|
839
1035
|
try {
|
|
840
1036
|
const events = parseSession(meta.filePath, meta.agent);
|
|
1037
|
+
writeResourceUsage(meta.id, events, meta.cwd);
|
|
841
1038
|
return {
|
|
842
1039
|
...meta,
|
|
843
1040
|
todos: extractTodoProgressFromEvents(events),
|
|
@@ -895,6 +1092,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
895
1092
|
// row AND backfills one indexed null-first (before its sidecar existed), while a
|
|
896
1093
|
// rescan carrying no actor still keeps the stored owner.
|
|
897
1094
|
const actorRec = meta.actor ? undefined : readSessionActorRecord(meta.id);
|
|
1095
|
+
const toolUsage = detectToolUsage(meta.id);
|
|
898
1096
|
const db = getDB();
|
|
899
1097
|
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
900
1098
|
const row = {
|
|
@@ -919,6 +1117,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
919
1117
|
cost_usd: meta.costUsd ?? null,
|
|
920
1118
|
duration_ms: meta.durationMs ?? null,
|
|
921
1119
|
model: meta.model ?? null,
|
|
1120
|
+
tool_call_count: meta.toolCallCount ?? null,
|
|
922
1121
|
file_path: meta.filePath,
|
|
923
1122
|
file_mtime_ms: scan?.fileMtimeMs ?? null,
|
|
924
1123
|
file_size: scan?.fileSize ?? null,
|
|
@@ -937,6 +1136,8 @@ export function upsertSession(meta, content, scan) {
|
|
|
937
1136
|
machine: resolveMachine(meta),
|
|
938
1137
|
actor: meta.actor ?? actorRec?.actor ?? null,
|
|
939
1138
|
initiated_by: meta.initiatedBy ?? actorRec?.initiatedBy ?? null,
|
|
1139
|
+
used_browser: toolUsage.usedBrowser ? 1 : 0,
|
|
1140
|
+
used_computer: toolUsage.usedComputer ? 1 : 0,
|
|
940
1141
|
};
|
|
941
1142
|
const txn = db.transaction(() => {
|
|
942
1143
|
upsert.run(row);
|
|
@@ -1017,6 +1218,15 @@ export function upsertSessionsBatch(entries) {
|
|
|
1017
1218
|
// back when the error escapes `fn`, so catching + skipping here leaves the txn valid
|
|
1018
1219
|
// and committable. We deliberately do NOT stamp the ledger for a skipped row, so the
|
|
1019
1220
|
// next scan re-tries it (self-healing once the underlying parser is fixed).
|
|
1221
|
+
const toolUsage = detectToolUsage(meta.id);
|
|
1222
|
+
// claude/codex skip enrichCachedSessionMeta above (preserving their
|
|
1223
|
+
// resumable-parse optimization) — write their pre-computed
|
|
1224
|
+
// skillsUsed/slashCommandsUsed (folded incrementally by discover.ts's
|
|
1225
|
+
// accumulator) here instead. Every other harness already got this
|
|
1226
|
+
// write from enrichCachedSessionMeta() in the .map() above.
|
|
1227
|
+
if (meta.agent === 'claude' || meta.agent === 'codex') {
|
|
1228
|
+
writeResourceUsageFromTallies(meta.id, meta.skillsUsed ?? [], meta.slashCommandsUsed ?? [], meta.cwd);
|
|
1229
|
+
}
|
|
1020
1230
|
try {
|
|
1021
1231
|
upsert.run({
|
|
1022
1232
|
id: meta.id,
|
|
@@ -1040,6 +1250,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1040
1250
|
cost_usd: meta.costUsd ?? null,
|
|
1041
1251
|
duration_ms: meta.durationMs ?? null,
|
|
1042
1252
|
model: meta.model ?? null,
|
|
1253
|
+
tool_call_count: meta.toolCallCount ?? null,
|
|
1043
1254
|
file_path: meta.filePath,
|
|
1044
1255
|
file_mtime_ms: scan?.fileMtimeMs ?? null,
|
|
1045
1256
|
file_size: scan?.fileSize ?? null,
|
|
@@ -1058,6 +1269,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1058
1269
|
machine: resolveMachine(meta),
|
|
1059
1270
|
actor: meta.actor ?? actorIndex.get(meta.id)?.actor ?? null,
|
|
1060
1271
|
initiated_by: meta.initiatedBy ?? actorIndex.get(meta.id)?.initiatedBy ?? null,
|
|
1272
|
+
used_browser: toolUsage.usedBrowser ? 1 : 0,
|
|
1273
|
+
used_computer: toolUsage.usedComputer ? 1 : 0,
|
|
1061
1274
|
});
|
|
1062
1275
|
delText.run(meta.id);
|
|
1063
1276
|
insText.run(meta.id,
|
|
@@ -1229,6 +1442,7 @@ function rowToMeta(row) {
|
|
|
1229
1442
|
costUsd: row.cost_usd ?? undefined,
|
|
1230
1443
|
durationMs: row.duration_ms ?? undefined,
|
|
1231
1444
|
model: row.model ?? undefined,
|
|
1445
|
+
toolCallCount: row.tool_call_count ?? undefined,
|
|
1232
1446
|
version: row.version ?? undefined,
|
|
1233
1447
|
account: row.account ?? undefined,
|
|
1234
1448
|
topic: row.topic ?? undefined,
|
|
@@ -1249,6 +1463,11 @@ function rowToMeta(row) {
|
|
|
1249
1463
|
// Narrow the free-text column to the known kinds; an unexpected value maps
|
|
1250
1464
|
// to undefined rather than being asserted as a valid kind.
|
|
1251
1465
|
initiatedBy: row.initiated_by === 'human' || row.initiated_by === 'agent' ? row.initiated_by : undefined,
|
|
1466
|
+
// NULL = never computed by this scanner (legacy row) — leave undefined so
|
|
1467
|
+
// the sessions picker knows to fall back to the transcript-regex detection
|
|
1468
|
+
// instead of trusting a false "never used browser/computer".
|
|
1469
|
+
usedBrowser: row.used_browser === null ? undefined : row.used_browser === 1,
|
|
1470
|
+
usedComputer: row.used_computer === null ? undefined : row.used_computer === 1,
|
|
1252
1471
|
};
|
|
1253
1472
|
}
|
|
1254
1473
|
function parseJsonColumn(value) {
|
|
@@ -1381,6 +1600,21 @@ function buildSessionWhere(options) {
|
|
|
1381
1600
|
if (options.onlyTeamOrigin) {
|
|
1382
1601
|
where.push('IFNULL(is_team_origin, 0) = 1');
|
|
1383
1602
|
}
|
|
1603
|
+
// #12: join against session_resource_usage. A subquery IN, not a real JOIN
|
|
1604
|
+
// on the base SELECT, keeps `SELECT * FROM sessions` untouched for every
|
|
1605
|
+
// other caller of buildSessionWhere() (countSessions, the usage rollup, …)
|
|
1606
|
+
// that never wants a skill/plugin filter.
|
|
1607
|
+
if (options.skill) {
|
|
1608
|
+
where.push(`id IN (
|
|
1609
|
+
SELECT session_id FROM session_resource_usage
|
|
1610
|
+
WHERE kind = 'skill' AND (name = ? COLLATE NOCASE OR name LIKE ? COLLATE NOCASE)
|
|
1611
|
+
)`);
|
|
1612
|
+
params.push(options.skill, `%:${options.skill}`);
|
|
1613
|
+
}
|
|
1614
|
+
if (options.plugin) {
|
|
1615
|
+
where.push(`id IN (SELECT session_id FROM session_resource_usage WHERE plugin = ? COLLATE NOCASE)`);
|
|
1616
|
+
params.push(options.plugin);
|
|
1617
|
+
}
|
|
1384
1618
|
const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
1385
1619
|
return { clause, params };
|
|
1386
1620
|
}
|
|
@@ -41,6 +41,10 @@ export interface DiscoverOptions {
|
|
|
41
41
|
sortBy?: 'timestamp' | 'cost' | 'duration';
|
|
42
42
|
/** Called as each agent makes parsing progress. Totals count only files that need re-parsing (cache misses). */
|
|
43
43
|
onProgress?: (progress: ScanProgress) => void;
|
|
44
|
+
/** Only sessions that invoked this skill (#12) — see QueryOptions.skill in session/db.ts. */
|
|
45
|
+
skill?: string;
|
|
46
|
+
/** Only sessions that used a skill/command owned by this plugin (#12) — see QueryOptions.plugin. */
|
|
47
|
+
plugin?: string;
|
|
44
48
|
}
|
|
45
49
|
/** Progress report emitted during incremental scanning. */
|
|
46
50
|
export interface ScanProgress {
|
|
@@ -66,6 +70,7 @@ interface ClaudeSessionScan {
|
|
|
66
70
|
durationMs?: number;
|
|
67
71
|
/** ISO time of the last timestamped event — the session's last activity. */
|
|
68
72
|
lastActivity?: string;
|
|
73
|
+
toolCallCount?: number;
|
|
69
74
|
/**
|
|
70
75
|
* Value of the JSONL `entrypoint` field on the first event that carries it.
|
|
71
76
|
* 'cli' for real interactive sessions, 'sdk-cli' for team-spawned ones.
|
|
@@ -86,6 +91,16 @@ interface ClaudeSessionScan {
|
|
|
86
91
|
plan?: string;
|
|
87
92
|
todos?: TodoProgress;
|
|
88
93
|
recentDirectoriesTouched?: string[];
|
|
94
|
+
/** Skills invoked (#12) — see SessionMeta.skillsUsed. */
|
|
95
|
+
skillsUsed?: Array<{
|
|
96
|
+
name: string;
|
|
97
|
+
count: number;
|
|
98
|
+
}>;
|
|
99
|
+
/** Slash commands invoked (#12) — see SessionMeta.slashCommandsUsed. */
|
|
100
|
+
slashCommandsUsed?: Array<{
|
|
101
|
+
name: string;
|
|
102
|
+
count: number;
|
|
103
|
+
}>;
|
|
89
104
|
}
|
|
90
105
|
/** Lightweight metadata extracted from a Codex JSONL file during incremental scan. */
|
|
91
106
|
interface CodexSessionScan {
|
|
@@ -374,6 +389,7 @@ export interface ClaudeParseState {
|
|
|
374
389
|
aiTitle?: string;
|
|
375
390
|
entrypoint?: string;
|
|
376
391
|
messageCount: number;
|
|
392
|
+
toolCallCount: number;
|
|
377
393
|
tokenCount: number;
|
|
378
394
|
outputTokens: number;
|
|
379
395
|
sawTokenCount: boolean;
|
|
@@ -392,6 +408,11 @@ export interface ClaudeParseState {
|
|
|
392
408
|
plan?: string;
|
|
393
409
|
checklistEvents: SessionEvent[];
|
|
394
410
|
recentDirectoriesTouched: string[];
|
|
411
|
+
/** Skill-invocation tool_use events, held for extractSkills() at finalize (#12). */
|
|
412
|
+
skillEvents: SessionEvent[];
|
|
413
|
+
/** Slash-command events (user-typed <command-name> wrapper OR a SlashCommand
|
|
414
|
+
* tool_use), held for extractSlashCommands() at finalize (#12). */
|
|
415
|
+
slashCommandEvents: SessionEvent[];
|
|
395
416
|
}
|
|
396
417
|
/** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
|
|
397
418
|
export declare function initClaudeParseState(): ClaudeParseState;
|
|
@@ -436,6 +457,7 @@ export interface ClaudeParserState {
|
|
|
436
457
|
plan?: string;
|
|
437
458
|
lastTsMs?: number;
|
|
438
459
|
messageCount: number;
|
|
460
|
+
toolCallCount: number;
|
|
439
461
|
tokenCount: number;
|
|
440
462
|
outputTokens: number;
|
|
441
463
|
sawTokenCount: boolean;
|
|
@@ -453,6 +475,9 @@ export interface ClaudeParserState {
|
|
|
453
475
|
contentText?: string;
|
|
454
476
|
checklistEvents: SessionEvent[];
|
|
455
477
|
recentDirectoriesTouched: string[];
|
|
478
|
+
/** #12: see ClaudeParseState.skillEvents/slashCommandEvents. */
|
|
479
|
+
skillEvents: SessionEvent[];
|
|
480
|
+
slashCommandEvents: SessionEvent[];
|
|
456
481
|
}
|
|
457
482
|
/**
|
|
458
483
|
* Snapshot a live {@link ClaudeParseState} into its serializable form at
|
|
@@ -528,6 +553,12 @@ export interface CodexParseState {
|
|
|
528
553
|
spawnedTeam?: string;
|
|
529
554
|
checklistEvents: SessionEvent[];
|
|
530
555
|
recentDirectoriesTouched: string[];
|
|
556
|
+
/** #12: see ClaudeParseState.skillEvents. Empty in practice today — no
|
|
557
|
+
* verified Codex skill-invocation tool name — but wired for parity so a
|
|
558
|
+
* future confirmed tool name (or a literal 'SlashCommand' function_call)
|
|
559
|
+
* is picked up with no further plumbing. */
|
|
560
|
+
skillEvents: SessionEvent[];
|
|
561
|
+
slashCommandEvents: SessionEvent[];
|
|
531
562
|
}
|
|
532
563
|
/** Zero-value accumulator for a fresh (from-byte-0) Codex parse. */
|
|
533
564
|
export declare function initCodexParseState(): CodexParseState;
|
|
@@ -23,7 +23,8 @@ import { walkForFilesWithStat } from '../fs-walk.js';
|
|
|
23
23
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
24
24
|
import { SESSION_AGENTS } from './types.js';
|
|
25
25
|
import { deriveShortId } from './short-id.js';
|
|
26
|
-
import { extractSessionTopic } from './prompt.js';
|
|
26
|
+
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
27
|
+
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
27
28
|
import { parseAntigravity, parseCursor } from './parse.js';
|
|
28
29
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
29
30
|
import { costOfUsage } from '../pricing/index.js';
|
|
@@ -310,6 +311,8 @@ function buildQueryOptions(options, agents, opts) {
|
|
|
310
311
|
onlyTeamOrigin: options?.onlyTeamOrigin,
|
|
311
312
|
origin: options?.origin,
|
|
312
313
|
sortBy: options?.sortBy,
|
|
314
|
+
skill: options?.skill,
|
|
315
|
+
plugin: options?.plugin,
|
|
313
316
|
};
|
|
314
317
|
}
|
|
315
318
|
/**
|
|
@@ -1015,6 +1018,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1015
1018
|
topic: scan.topic,
|
|
1016
1019
|
label,
|
|
1017
1020
|
messageCount: scan.messageCount,
|
|
1021
|
+
toolCallCount: scan.toolCallCount,
|
|
1018
1022
|
tokenCount: scan.tokenCount,
|
|
1019
1023
|
outputTokens: scan.outputTokens,
|
|
1020
1024
|
costUsd: scan.costUsd,
|
|
@@ -1029,6 +1033,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1029
1033
|
plan: scan.plan,
|
|
1030
1034
|
todos: scan.todos,
|
|
1031
1035
|
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1036
|
+
skillsUsed: scan.skillsUsed,
|
|
1037
|
+
slashCommandsUsed: scan.slashCommandsUsed,
|
|
1032
1038
|
};
|
|
1033
1039
|
}
|
|
1034
1040
|
else {
|
|
@@ -1044,6 +1050,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1044
1050
|
model: scan.model,
|
|
1045
1051
|
label,
|
|
1046
1052
|
messageCount: scan.messageCount,
|
|
1053
|
+
toolCallCount: scan.toolCallCount,
|
|
1047
1054
|
tokenCount: scan.tokenCount,
|
|
1048
1055
|
outputTokens: scan.outputTokens,
|
|
1049
1056
|
costUsd: scan.costUsd,
|
|
@@ -1059,6 +1066,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1059
1066
|
plan: scan.plan,
|
|
1060
1067
|
todos: scan.todos,
|
|
1061
1068
|
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1069
|
+
skillsUsed: scan.skillsUsed,
|
|
1070
|
+
slashCommandsUsed: scan.slashCommandsUsed,
|
|
1062
1071
|
};
|
|
1063
1072
|
}
|
|
1064
1073
|
return {
|
|
@@ -2425,6 +2434,7 @@ export function initClaudeParseState() {
|
|
|
2425
2434
|
aiTitle: undefined,
|
|
2426
2435
|
entrypoint: undefined,
|
|
2427
2436
|
messageCount: 0,
|
|
2437
|
+
toolCallCount: 0,
|
|
2428
2438
|
tokenCount: 0,
|
|
2429
2439
|
outputTokens: 0,
|
|
2430
2440
|
sawTokenCount: false,
|
|
@@ -2443,6 +2453,8 @@ export function initClaudeParseState() {
|
|
|
2443
2453
|
plan: undefined,
|
|
2444
2454
|
checklistEvents: [],
|
|
2445
2455
|
recentDirectoriesTouched: [],
|
|
2456
|
+
skillEvents: [],
|
|
2457
|
+
slashCommandEvents: [],
|
|
2446
2458
|
};
|
|
2447
2459
|
}
|
|
2448
2460
|
const CHECKLIST_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan', 'TaskCreate', 'TaskUpdate']);
|
|
@@ -2450,6 +2462,16 @@ const DIRECTORY_TOOLS = new Set(['Edit', 'Write', 'edit_file', 'write_file', 'cr
|
|
|
2450
2462
|
function foldDerivedToolState(state, event) {
|
|
2451
2463
|
if (CHECKLIST_TOOLS.has(event.tool ?? ''))
|
|
2452
2464
|
state.checklistEvents.push(event);
|
|
2465
|
+
// #12: skill/slash-command usage, held here instead of re-parsed later — the
|
|
2466
|
+
// same reason checklistEvents is folded incrementally rather than recomputed
|
|
2467
|
+
// from a full re-parse (see session/db.ts's writeResourceUsage doc comment).
|
|
2468
|
+
if (isSkillInvocation(event))
|
|
2469
|
+
state.skillEvents.push(event);
|
|
2470
|
+
if (event.tool === 'SlashCommand') {
|
|
2471
|
+
const slashCommand = extractSlashCommandFromToolInput(event.args);
|
|
2472
|
+
if (slashCommand)
|
|
2473
|
+
state.slashCommandEvents.push({ ...event, slashCommand });
|
|
2474
|
+
}
|
|
2453
2475
|
if (!DIRECTORY_TOOLS.has(event.tool ?? ''))
|
|
2454
2476
|
return;
|
|
2455
2477
|
const next = extractRecentDirectoriesTouched([event], state.cwd);
|
|
@@ -2482,6 +2504,7 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2482
2504
|
for (const b of parsed.message.content) {
|
|
2483
2505
|
if (b?.type !== 'tool_use')
|
|
2484
2506
|
continue;
|
|
2507
|
+
state.toolCallCount++;
|
|
2485
2508
|
if (!state.spawnedTeam && typeof b?.input?.command === 'string') {
|
|
2486
2509
|
const team = detectSpawnedTeam(b.input.command);
|
|
2487
2510
|
if (team)
|
|
@@ -2578,6 +2601,15 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2578
2601
|
state.userTexts.push(text);
|
|
2579
2602
|
if (!state.topic)
|
|
2580
2603
|
state.topic = extractSessionTopic(text);
|
|
2604
|
+
// #12: the USER typing a slash command — Claude injects a <command-name>
|
|
2605
|
+
// wrapper as the message content (extractClaudeUserText returns it
|
|
2606
|
+
// un-stripped; isLocalCommandMessage only filters bash-echo wrappers).
|
|
2607
|
+
const slashCommand = extractSlashCommandName(text);
|
|
2608
|
+
if (slashCommand) {
|
|
2609
|
+
state.slashCommandEvents.push({
|
|
2610
|
+
type: 'message', agent: 'claude', timestamp: parsed.timestamp || '', role: 'user', content: text, slashCommand,
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2581
2613
|
}
|
|
2582
2614
|
return;
|
|
2583
2615
|
}
|
|
@@ -2642,6 +2674,7 @@ export function finalizeClaudeScan(state) {
|
|
|
2642
2674
|
topic: resolvedTopic,
|
|
2643
2675
|
entrypoint: state.entrypoint,
|
|
2644
2676
|
messageCount: state.messageCount,
|
|
2677
|
+
toolCallCount: state.toolCallCount,
|
|
2645
2678
|
tokenCount: state.sawTokenCount ? state.tokenCount : undefined,
|
|
2646
2679
|
outputTokens: state.sawTokenCount ? state.outputTokens : undefined,
|
|
2647
2680
|
costUsd: state.sawCost ? state.costUsd : undefined,
|
|
@@ -2657,6 +2690,8 @@ export function finalizeClaudeScan(state) {
|
|
|
2657
2690
|
plan: state.plan,
|
|
2658
2691
|
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
2659
2692
|
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
2693
|
+
skillsUsed: state.skillEvents.length ? extractSkills(state.skillEvents) : undefined,
|
|
2694
|
+
slashCommandsUsed: state.slashCommandEvents.length ? extractSlashCommands(state.slashCommandEvents) : undefined,
|
|
2660
2695
|
};
|
|
2661
2696
|
}
|
|
2662
2697
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
@@ -2713,6 +2748,7 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2713
2748
|
plan: state.plan,
|
|
2714
2749
|
lastTsMs: state.lastTsMs,
|
|
2715
2750
|
messageCount: state.messageCount,
|
|
2751
|
+
toolCallCount: state.toolCallCount,
|
|
2716
2752
|
tokenCount: state.tokenCount,
|
|
2717
2753
|
outputTokens: state.outputTokens,
|
|
2718
2754
|
sawTokenCount: state.sawTokenCount,
|
|
@@ -2734,6 +2770,8 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2734
2770
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
2735
2771
|
checklistEvents: state.checklistEvents,
|
|
2736
2772
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
2773
|
+
skillEvents: state.skillEvents,
|
|
2774
|
+
slashCommandEvents: state.slashCommandEvents,
|
|
2737
2775
|
};
|
|
2738
2776
|
}
|
|
2739
2777
|
/**
|
|
@@ -2771,6 +2809,7 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2771
2809
|
aiTitle: prior.aiTitle,
|
|
2772
2810
|
entrypoint: prior.entrypoint,
|
|
2773
2811
|
messageCount: prior.messageCount,
|
|
2812
|
+
toolCallCount: prior.toolCallCount ?? 0,
|
|
2774
2813
|
tokenCount: prior.tokenCount,
|
|
2775
2814
|
outputTokens: prior.outputTokens,
|
|
2776
2815
|
sawTokenCount: prior.sawTokenCount,
|
|
@@ -2789,6 +2828,8 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2789
2828
|
plan: prior.plan,
|
|
2790
2829
|
checklistEvents: prior.checklistEvents ?? [],
|
|
2791
2830
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
2831
|
+
skillEvents: prior.skillEvents ?? [],
|
|
2832
|
+
slashCommandEvents: prior.slashCommandEvents ?? [],
|
|
2792
2833
|
};
|
|
2793
2834
|
}
|
|
2794
2835
|
/**
|
|
@@ -2990,6 +3031,8 @@ export function initCodexParseState() {
|
|
|
2990
3031
|
spawnedTeam: undefined,
|
|
2991
3032
|
checklistEvents: [],
|
|
2992
3033
|
recentDirectoriesTouched: [],
|
|
3034
|
+
skillEvents: [],
|
|
3035
|
+
slashCommandEvents: [],
|
|
2993
3036
|
};
|
|
2994
3037
|
}
|
|
2995
3038
|
/**
|
|
@@ -3237,6 +3280,11 @@ export function hydrateCodexParseState(prior) {
|
|
|
3237
3280
|
spawnedTeam: prior.spawnedTeam,
|
|
3238
3281
|
checklistEvents: prior.checklistEvents ?? [],
|
|
3239
3282
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
3283
|
+
// Not persisted in CodexParserState (always empty for Codex today — see
|
|
3284
|
+
// CodexParseState.skillEvents) — a resume starts fresh rather than
|
|
3285
|
+
// round-tripping an always-empty array through the continuation blob.
|
|
3286
|
+
skillEvents: [],
|
|
3287
|
+
slashCommandEvents: [],
|
|
3240
3288
|
};
|
|
3241
3289
|
}
|
|
3242
3290
|
/**
|
|
@@ -3734,6 +3782,7 @@ export function readCursorMeta(filePath, currentVersion) {
|
|
|
3734
3782
|
topic: firstUserText ? extractSessionTopic(firstUserText) : undefined,
|
|
3735
3783
|
label: title,
|
|
3736
3784
|
messageCount: events.filter((event) => event.type === 'message').length,
|
|
3785
|
+
todos: extractTodoProgressFromEvents(events),
|
|
3737
3786
|
};
|
|
3738
3787
|
return { meta, content: userTexts.join('\n') };
|
|
3739
3788
|
}
|