@phnx-labs/agents-cli 1.22.18 → 1.22.20
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 +67 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cost.js +10 -6
- package/dist/commands/output.js +10 -6
- package/dist/commands/routines.d.ts +6 -0
- package/dist/commands/routines.js +83 -5
- package/dist/lib/agents.d.ts +41 -0
- package/dist/lib/agents.js +53 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/refresh.js +35 -21
- package/dist/lib/routines.d.ts +80 -0
- package/dist/lib/routines.js +120 -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/session/claude-accounts.d.ts +128 -0
- package/dist/lib/session/claude-accounts.js +244 -0
- package/dist/lib/session/db.d.ts +13 -3
- package/dist/lib/session/db.js +117 -12
- package/dist/lib/session/discover.js +29 -44
- package/dist/lib/session/types.d.ts +13 -0
- package/package.json +1 -1
package/dist/lib/session/db.js
CHANGED
|
@@ -17,6 +17,7 @@ import { machineForSessionFile } from './origin-machine.js';
|
|
|
17
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
18
|
import { toolCallsFromEvents } from './tool-calls.js';
|
|
19
19
|
import { persistToolCalls, purgeToolCalls, toolEvidenceSourcePath } from './tool-store.js';
|
|
20
|
+
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
20
21
|
import { extractSkills, extractSlashCommands } from './highlights.js';
|
|
21
22
|
import { resolveResource } from '../resources.js';
|
|
22
23
|
import { discoverPlugins } from '../plugins.js';
|
|
@@ -25,7 +26,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
25
26
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
26
27
|
* assert against the constant instead of hardcoding a number that every bump
|
|
27
28
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
28
|
-
export const SCHEMA_VERSION =
|
|
29
|
+
export const SCHEMA_VERSION = 33;
|
|
29
30
|
/**
|
|
30
31
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
31
32
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -67,6 +68,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
67
68
|
routine_run_id TEXT,
|
|
68
69
|
version TEXT,
|
|
69
70
|
account TEXT,
|
|
71
|
+
account_key TEXT,
|
|
72
|
+
account_org TEXT,
|
|
70
73
|
mode TEXT,
|
|
71
74
|
timestamp TEXT NOT NULL,
|
|
72
75
|
last_activity TEXT,
|
|
@@ -730,6 +733,56 @@ function migrateSchema(db, fromVersion) {
|
|
|
730
733
|
if (!cols.has('mode'))
|
|
731
734
|
db.exec(`ALTER TABLE sessions ADD COLUMN mode TEXT`);
|
|
732
735
|
}
|
|
736
|
+
if (fromVersion < 33) {
|
|
737
|
+
// v32 → v33: attribute each Claude session to the account that produced it.
|
|
738
|
+
// Until now `account` held ONE email resolved process-globally and stamped on
|
|
739
|
+
// every row of a scan, so a machine with several signed-in accounts reported all
|
|
740
|
+
// of its history under whichever resolved first.
|
|
741
|
+
//
|
|
742
|
+
// Do NOT wipe scan_ledger. Attribution is a pure function of (file_path,
|
|
743
|
+
// version) — both already stored — so existing rows are repaired in place with
|
|
744
|
+
// no transcript re-parsed. Adding `DELETE FROM scan_ledger` here to match the
|
|
745
|
+
// other migrations would force a full re-parse of every indexed transcript to
|
|
746
|
+
// recompute something derivable from two columns. The v31 migration sets the
|
|
747
|
+
// same precedent.
|
|
748
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((column) => column.name));
|
|
749
|
+
if (!cols.has('account_key'))
|
|
750
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN account_key TEXT`);
|
|
751
|
+
if (!cols.has('account_org'))
|
|
752
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN account_org TEXT`);
|
|
753
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_account_key ON sessions(account_key)`);
|
|
754
|
+
backfillClaudeAccounts(db);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
759
|
+
* `file_path` and recorded `version`. Used by the v33 migration; idempotent, so it is
|
|
760
|
+
* safe to re-run.
|
|
761
|
+
*/
|
|
762
|
+
function backfillClaudeAccounts(db, scope = 'all') {
|
|
763
|
+
// 'unresolved' exists so the getDB repair touches ONLY rows that are actually
|
|
764
|
+
// broken. Re-resolving every Claude row on an unrelated trigger would silently
|
|
765
|
+
// downgrade a correct row whose version home has since been uninstalled and its
|
|
766
|
+
// trash snapshot pruned — the row would go from attributed to dark for no reason
|
|
767
|
+
// the user caused. The migration wants 'all'; the repair does not.
|
|
768
|
+
const where = scope === 'all'
|
|
769
|
+
? `agent = 'claude'`
|
|
770
|
+
: `agent = 'claude' AND (account_key IS NULL
|
|
771
|
+
OR (account_key LIKE 'unattributed:%' AND account IS NOT NULL))`;
|
|
772
|
+
const index = buildClaudeAccountIndex();
|
|
773
|
+
const rows = db.prepare(`SELECT id, file_path, version FROM sessions WHERE ${where}`).all();
|
|
774
|
+
if (rows.length === 0)
|
|
775
|
+
return;
|
|
776
|
+
// `account` is overwritten, not COALESCEd. Every pre-v33 row carries the wrong
|
|
777
|
+
// globally-resolved email; keeping it on a row we could not attribute would leave a
|
|
778
|
+
// known-false address on display (commands/sessions.ts prints it, and its fuzzy
|
|
779
|
+
// matcher scores on it) and would disagree with the scan path, which writes
|
|
780
|
+
// `account = excluded.account` unconditionally. A dark row reads NULL.
|
|
781
|
+
const update = db.prepare(`UPDATE sessions SET account_key = ?, account_org = ?, account = ? WHERE id = ?`);
|
|
782
|
+
for (const row of rows) {
|
|
783
|
+
const bucket = resolveClaudeAccount(index, row.file_path ?? '', row.version);
|
|
784
|
+
update.run(bucket.key, bucket.orgName, bucket.email, row.id);
|
|
785
|
+
}
|
|
733
786
|
}
|
|
734
787
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
735
788
|
export function getDB() {
|
|
@@ -776,6 +829,31 @@ export function getDB() {
|
|
|
776
829
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity DESC)`);
|
|
777
830
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_origin ON sessions(origin)`);
|
|
778
831
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_routine_run_id ON sessions(routine_run_id)`);
|
|
832
|
+
// Account attribution repair. Two ways a Claude row ends up wrong even at v33:
|
|
833
|
+
// an older CLI (whose INSERT does not name the column) writes NULL, and a DB
|
|
834
|
+
// migrated by a build that predates the "clear the stale email on a dark row" fix
|
|
835
|
+
// keeps a known-wrong address. The v33 migration cannot fix either — it never runs
|
|
836
|
+
// again. Cheap guard first so the common case is one indexed lookup, then repair.
|
|
837
|
+
// Same shape as the `machine` repair below, for the same reason.
|
|
838
|
+
{
|
|
839
|
+
// Column guard FIRST, like the `machine` repair below. schema_version can be
|
|
840
|
+
// stamped at SCHEMA_VERSION without the column existing — getDB writes the marker
|
|
841
|
+
// for any DB whose meta has no row (a hand-built or partially-created index), and
|
|
842
|
+
// migrateSchema never runs in that path. Querying account_key unguarded would
|
|
843
|
+
// then throw "no such column" and take down every command that opens the index.
|
|
844
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
845
|
+
if (cols.some((c) => c.name === 'account_key') && cols.some((c) => c.name === 'account_org')) {
|
|
846
|
+
const needsRepair = db.prepare(`
|
|
847
|
+
SELECT 1 FROM sessions
|
|
848
|
+
WHERE agent = 'claude'
|
|
849
|
+
AND (account_key IS NULL
|
|
850
|
+
OR (account_key LIKE 'unattributed:%' AND account IS NOT NULL))
|
|
851
|
+
LIMIT 1
|
|
852
|
+
`).get();
|
|
853
|
+
if (needsRepair)
|
|
854
|
+
db.transaction(() => backfillClaudeAccounts(db, 'unresolved'))();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
779
857
|
// machine column + indexes: only after the column is guaranteed present.
|
|
780
858
|
// Fresh SCHEMA (v17) includes the column; older DBs get it from migrate v17.
|
|
781
859
|
// If a partial upgrade left schema_version ahead of the column, repair here.
|
|
@@ -1124,7 +1202,7 @@ export function recordDirScans(entries) {
|
|
|
1124
1202
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
1125
1203
|
INSERT INTO sessions (
|
|
1126
1204
|
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
1127
|
-
version, account, mode, timestamp, last_activity,
|
|
1205
|
+
version, account, account_key, account_org, mode, timestamp, last_activity,
|
|
1128
1206
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
1129
1207
|
output_tokens, cost_usd, duration_ms, model, tool_call_count,
|
|
1130
1208
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
@@ -1133,7 +1211,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1133
1211
|
actor, initiated_by, used_browser, used_computer
|
|
1134
1212
|
) VALUES (
|
|
1135
1213
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
1136
|
-
@version, @account, @mode, @timestamp, @last_activity,
|
|
1214
|
+
@version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
|
|
1137
1215
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
1138
1216
|
@output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
|
|
1139
1217
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
@@ -1149,6 +1227,8 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1149
1227
|
routine_run_id = excluded.routine_run_id,
|
|
1150
1228
|
version = excluded.version,
|
|
1151
1229
|
account = excluded.account,
|
|
1230
|
+
account_key = excluded.account_key,
|
|
1231
|
+
account_org = excluded.account_org,
|
|
1152
1232
|
mode = COALESCE(excluded.mode, sessions.mode),
|
|
1153
1233
|
timestamp = excluded.timestamp,
|
|
1154
1234
|
last_activity = excluded.last_activity,
|
|
@@ -1290,20 +1370,22 @@ function writeResourceUsageFromTallies(sessionId, skills, commands, cwd) {
|
|
|
1290
1370
|
const plugins = discoverPlugins({ cwd });
|
|
1291
1371
|
for (const { name, count } of skills) {
|
|
1292
1372
|
const prov = resolveResourceProvenance('skills', name, cwd, plugins);
|
|
1293
|
-
|
|
1373
|
+
const bind = {
|
|
1294
1374
|
session_id: sessionId, kind: 'skill', name, count,
|
|
1295
1375
|
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1296
1376
|
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1297
|
-
}
|
|
1377
|
+
};
|
|
1378
|
+
ins.run(bind);
|
|
1298
1379
|
}
|
|
1299
1380
|
for (const { name, count } of commands) {
|
|
1300
1381
|
const bare = name.replace(/^\//, '');
|
|
1301
1382
|
const prov = resolveResourceProvenance('commands', bare, cwd, plugins);
|
|
1302
|
-
|
|
1383
|
+
const bind = {
|
|
1303
1384
|
session_id: sessionId, kind: 'command', name: bare, count,
|
|
1304
1385
|
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1305
1386
|
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1306
|
-
}
|
|
1387
|
+
};
|
|
1388
|
+
ins.run(bind);
|
|
1307
1389
|
}
|
|
1308
1390
|
}
|
|
1309
1391
|
/**
|
|
@@ -1392,6 +1474,8 @@ export function upsertSession(meta, content, scan) {
|
|
|
1392
1474
|
routine_run_id: meta.routineRunId ?? null,
|
|
1393
1475
|
version: meta.version ?? null,
|
|
1394
1476
|
account: meta.account ?? null,
|
|
1477
|
+
account_key: meta.accountKey ?? null,
|
|
1478
|
+
account_org: meta.accountOrg ?? null,
|
|
1395
1479
|
mode: meta.mode ?? actorRec?.mode ?? null,
|
|
1396
1480
|
timestamp: meta.timestamp,
|
|
1397
1481
|
last_activity: resolveLastActivity(meta, scan),
|
|
@@ -1547,7 +1631,12 @@ export function upsertSessionsBatch(entries) {
|
|
|
1547
1631
|
writeResourceUsageFromTallies(meta.id, meta.skillsUsed ?? [], meta.slashCommandsUsed ?? [], meta.cwd);
|
|
1548
1632
|
}
|
|
1549
1633
|
try {
|
|
1550
|
-
|
|
1634
|
+
// Typed, not a bare literal: bun binds named parameters in strict mode, where a
|
|
1635
|
+
// MISSING key throws (node binds NULL instead). A silently dropped key therefore
|
|
1636
|
+
// breaks only the shipped binary's runtime, and the per-row catch below swallows
|
|
1637
|
+
// it — the exact shape of the bug that shipped account_key unbound. Annotating
|
|
1638
|
+
// against SessionRow makes tsc reject the next omission.
|
|
1639
|
+
const row = {
|
|
1551
1640
|
id: meta.id,
|
|
1552
1641
|
short_id: meta.shortId,
|
|
1553
1642
|
agent: meta.agent,
|
|
@@ -1556,6 +1645,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1556
1645
|
routine_run_id: meta.routineRunId ?? null,
|
|
1557
1646
|
version: meta.version ?? null,
|
|
1558
1647
|
account: meta.account ?? null,
|
|
1648
|
+
account_key: meta.accountKey ?? null,
|
|
1649
|
+
account_org: meta.accountOrg ?? null,
|
|
1559
1650
|
mode: meta.mode ?? actorIndex.get(meta.id)?.mode ?? null,
|
|
1560
1651
|
timestamp: meta.timestamp,
|
|
1561
1652
|
last_activity: resolveLastActivity(meta, scan),
|
|
@@ -1591,7 +1682,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1591
1682
|
initiated_by: meta.initiatedBy ?? actorIndex.get(meta.id)?.initiatedBy ?? null,
|
|
1592
1683
|
used_browser: toolUsage.usedBrowser ? 1 : 0,
|
|
1593
1684
|
used_computer: toolUsage.usedComputer ? 1 : 0,
|
|
1594
|
-
}
|
|
1685
|
+
};
|
|
1686
|
+
upsert.run(row);
|
|
1595
1687
|
delText.run(meta.id);
|
|
1596
1688
|
insText.run(meta.id,
|
|
1597
1689
|
// Mirror upsertSession: index the label the upsert actually stored
|
|
@@ -1780,6 +1872,8 @@ function rowToMeta(row) {
|
|
|
1780
1872
|
toolCallCount: row.tool_call_count ?? undefined,
|
|
1781
1873
|
version: row.version ?? undefined,
|
|
1782
1874
|
account: row.account ?? undefined,
|
|
1875
|
+
accountKey: row.account_key ?? undefined,
|
|
1876
|
+
accountOrg: row.account_org ?? undefined,
|
|
1783
1877
|
mode: isSessionRunMode(row.mode) ? row.mode : undefined,
|
|
1784
1878
|
topic: row.topic ?? undefined,
|
|
1785
1879
|
label: row.label ?? undefined,
|
|
@@ -2075,12 +2169,23 @@ export function queryUsageRollup(options) {
|
|
|
2075
2169
|
? 'agent'
|
|
2076
2170
|
: options.groupBy === 'project'
|
|
2077
2171
|
? `IFNULL(NULLIF(project, ''), '(no project)')`
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2172
|
+
: options.groupBy === 'account'
|
|
2173
|
+
// A NULL account_key means this harness has no account attribution yet —
|
|
2174
|
+
// the mechanism is Claude-only today (see lib/session/claude-accounts.ts).
|
|
2175
|
+
// Bucket per agent so the rows are named honestly instead of being called
|
|
2176
|
+
// "not indexed", which they are not, and instead of joining a real account.
|
|
2177
|
+
? `IFNULL(NULLIF(account_key, ''), 'unattributed:' || agent)`
|
|
2178
|
+
// ISO timestamps are lexicographically date-sortable; the date is the
|
|
2179
|
+
// first 10 chars (YYYY-MM-DD).
|
|
2180
|
+
: `substr(timestamp, 1, 10)`;
|
|
2081
2181
|
const sql = `
|
|
2082
2182
|
SELECT
|
|
2083
2183
|
${keyExpr} AS key,
|
|
2184
|
+
${options.groupBy === 'account'
|
|
2185
|
+
// One label per account_key by construction, so MAX just picks it out.
|
|
2186
|
+
? `MAX(CASE WHEN account_org IS NOT NULL AND account IS NOT NULL
|
|
2187
|
+
THEN account_org || ' <' || account || '>' END) AS label,`
|
|
2188
|
+
: ''}
|
|
2084
2189
|
IFNULL(SUM(cost_usd), 0) AS costUsd,
|
|
2085
2190
|
IFNULL(SUM(duration_ms), 0) AS durationMs,
|
|
2086
2191
|
COUNT(*) AS sessionCount,
|
|
@@ -23,6 +23,7 @@ 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 { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
26
27
|
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
27
28
|
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
28
29
|
import { parseAntigravity, parseCursor } from './parse.js';
|
|
@@ -891,45 +892,20 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
|
|
|
891
892
|
// ---------------------------------------------------------------------------
|
|
892
893
|
// Claude account info
|
|
893
894
|
// ---------------------------------------------------------------------------
|
|
894
|
-
let
|
|
895
|
-
/**
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (!fs.existsSync(versionsBase))
|
|
909
|
-
continue;
|
|
910
|
-
try {
|
|
911
|
-
for (const version of fs.readdirSync(versionsBase)) {
|
|
912
|
-
candidates.push(path.join(versionsBase, version, 'home', '.claude', '.claude.json'));
|
|
913
|
-
candidates.push(path.join(versionsBase, version, 'home', '.claude.json'));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
catch { /* versions dir unreadable */ }
|
|
917
|
-
}
|
|
918
|
-
for (const candidate of candidates) {
|
|
919
|
-
try {
|
|
920
|
-
if (!fs.existsSync(candidate))
|
|
921
|
-
continue;
|
|
922
|
-
const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
923
|
-
const name = data.oauthAccount?.emailAddress || data.oauthAccount?.displayName;
|
|
924
|
-
if (name) {
|
|
925
|
-
cachedClaudeAccount = name;
|
|
926
|
-
return name;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
catch { /* auth file unreadable or malformed */ }
|
|
930
|
-
}
|
|
931
|
-
cachedClaudeAccount = '';
|
|
932
|
-
return undefined;
|
|
895
|
+
let cachedClaudeAccountIndex;
|
|
896
|
+
/**
|
|
897
|
+
* The account-attribution index, built at most once per scan pass.
|
|
898
|
+
*
|
|
899
|
+
* Rebuilt when `refresh` is set — `scanClaudeIncremental` does that at the start of
|
|
900
|
+
* each pass so a long-lived process (the daemon) picks up an `agents use` switch or a
|
|
901
|
+
* fresh login instead of attributing later sessions to a stale set of homes. Per-file
|
|
902
|
+
* resolution then reads the cached index, because rebuilding it per transcript would
|
|
903
|
+
* re-read every home's `.claude.json` thousands of times.
|
|
904
|
+
*/
|
|
905
|
+
function claudeAccountIndex(refresh = false) {
|
|
906
|
+
if (refresh || !cachedClaudeAccountIndex)
|
|
907
|
+
cachedClaudeAccountIndex = buildClaudeAccountIndex();
|
|
908
|
+
return cachedClaudeAccountIndex;
|
|
933
909
|
}
|
|
934
910
|
// ---------------------------------------------------------------------------
|
|
935
911
|
// Claude
|
|
@@ -974,7 +950,8 @@ export function buildClaudeLabelMap() {
|
|
|
974
950
|
}
|
|
975
951
|
/** Incrementally re-scan changed Claude session files and upsert into the DB. */
|
|
976
952
|
async function scanClaudeIncremental(onProgress) {
|
|
977
|
-
|
|
953
|
+
// Rebuild once per pass; readClaudeMeta resolves each transcript against it.
|
|
954
|
+
claudeAccountIndex(true);
|
|
978
955
|
const labelMap = buildClaudeLabelMap();
|
|
979
956
|
// Enumerate every leaf project dir across all Claude roots. The FIRST root
|
|
980
957
|
// returned by getAgentSessionDirs is the agent's live `~/.claude/projects` —
|
|
@@ -1042,7 +1019,7 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
1042
1019
|
const sessionId = path.basename(filePath).replace('.jsonl', '');
|
|
1043
1020
|
const label = labelMap.get(sessionId) ?? undefined;
|
|
1044
1021
|
const priorRow = priorStates.get(filePath);
|
|
1045
|
-
const result = await readClaudeMeta(filePath, sessionId, scan, priorRow,
|
|
1022
|
+
const result = await readClaudeMeta(filePath, sessionId, scan, priorRow, label);
|
|
1046
1023
|
if (result) {
|
|
1047
1024
|
entries.push({
|
|
1048
1025
|
meta: result.meta,
|
|
@@ -1079,7 +1056,7 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
1079
1056
|
* the serialized continuation (parser_state + content_text) to persist for the
|
|
1080
1057
|
* next scan.
|
|
1081
1058
|
*/
|
|
1082
|
-
async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow,
|
|
1059
|
+
async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
1083
1060
|
const prior = parsePriorClaudeState(priorRow);
|
|
1084
1061
|
const { scan, newState, toolCalls, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
1085
1062
|
if (mode === 'incremental')
|
|
@@ -1087,6 +1064,10 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1087
1064
|
else
|
|
1088
1065
|
claudeFullScanCount++;
|
|
1089
1066
|
const isTeamOrigin = scan.entrypoint === 'sdk-cli';
|
|
1067
|
+
// Which account produced this transcript. Resolved from the path plus the version
|
|
1068
|
+
// recorded inside the file, so rows under the mutable ~/.claude symlink are
|
|
1069
|
+
// attributed to the version that actually wrote them. See claude-accounts.ts.
|
|
1070
|
+
const acct = resolveClaudeAccount(claudeAccountIndex(), filePath, scan.version);
|
|
1090
1071
|
let meta;
|
|
1091
1072
|
if (scan.timestamp) {
|
|
1092
1073
|
const cwd = normalizeCwd(scan.cwd || '');
|
|
@@ -1102,7 +1083,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1102
1083
|
gitBranch: scan.gitBranch,
|
|
1103
1084
|
version: scan.version,
|
|
1104
1085
|
model: scan.model,
|
|
1105
|
-
account,
|
|
1086
|
+
account: acct.email ?? undefined,
|
|
1087
|
+
accountKey: acct.key,
|
|
1088
|
+
accountOrg: acct.orgName ?? undefined,
|
|
1106
1089
|
topic: scan.topic,
|
|
1107
1090
|
label,
|
|
1108
1091
|
messageCount: scan.messageCount,
|
|
@@ -1134,7 +1117,9 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1134
1117
|
timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
1135
1118
|
lastActivity: scan.lastActivity,
|
|
1136
1119
|
filePath,
|
|
1137
|
-
account,
|
|
1120
|
+
account: acct.email ?? undefined,
|
|
1121
|
+
accountKey: acct.key,
|
|
1122
|
+
accountOrg: acct.orgName ?? undefined,
|
|
1138
1123
|
model: scan.model,
|
|
1139
1124
|
label,
|
|
1140
1125
|
messageCount: scan.messageCount,
|
|
@@ -152,7 +152,20 @@ export interface SessionMeta {
|
|
|
152
152
|
model?: string;
|
|
153
153
|
toolCallCount?: number;
|
|
154
154
|
version?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Email of the account that produced the session. Display-only: two orgs can share
|
|
157
|
+
* one email, so never group on this — group on `accountKey`.
|
|
158
|
+
*/
|
|
155
159
|
account?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Org-scoped identity of the producing account (`claude:org=<uuid>`), or
|
|
162
|
+
* `unattributed:<reason>` when it cannot be established. The correct grouping key:
|
|
163
|
+
* a Team seat and a personal Max plan under one email are separate quota buckets.
|
|
164
|
+
* See lib/session/claude-accounts.ts for how a transcript is attributed.
|
|
165
|
+
*/
|
|
166
|
+
accountKey?: string;
|
|
167
|
+
/** Organization display name of the producing account, when known. */
|
|
168
|
+
accountOrg?: string;
|
|
156
169
|
/** Effective normalized launch mode captured by the SessionStart hook. */
|
|
157
170
|
mode?: SessionRunMode;
|
|
158
171
|
topic?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.20",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|