@lumoai/cli 1.20.0 → 1.20.2
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/assets/skill/SKILL.md +3 -3
- package/assets/skill/references/memory.md +23 -1
- package/assets/skill/references/task-context.md +3 -3
- package/dist/cli/src/commands/memory-project-add.js +23 -1
- package/dist/cli/src/commands/memory-task-add.js +25 -3
- package/dist/cli/src/commands/task-lineage.js +40 -3
- package/dist/cli/src/index.js +2 -1
- package/dist/cli/src/lib/transcript-usage.js +48 -15
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lumo
|
|
3
|
-
description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context".'
|
|
3
|
+
description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context", "--signal", "usage signal health", "auto usage audit", "自动使用审计", "signal-health".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Prerequisites
|
|
@@ -49,7 +49,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
49
49
|
- `lumo task figma context <id> <linkId>` — Figma link metadata (v1)
|
|
50
50
|
- `lumo task comments list <id>` — full comment thread (read-only; ≠ `task comment`)
|
|
51
51
|
- `lumo task pr show <id> <number>` — synced PR metadata (v1)
|
|
52
|
-
- `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome + the run's token/loop cost (read-only audit view)
|
|
52
|
+
- `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome + the run's token/loop cost (read-only audit view); `lumo task lineage <id> --signal` also appends workspace-level usage signal-health (used distribution, per-session variance, used-vs-base merge rate)
|
|
53
53
|
|
|
54
54
|
**Tasks** — see [tasks.md](references/tasks.md)
|
|
55
55
|
|
|
@@ -98,7 +98,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
98
98
|
|
|
99
99
|
- `lumo session attach <id>` — bind this session to a task (then run `task context`)
|
|
100
100
|
- `lumo session status` / `lumo session detach` — show / clear binding
|
|
101
|
-
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt
|
|
101
|
+
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt. Usage is now also audited automatically when a task reaches DONE (evidence-gated, true-only — confident fragments marked used, the rest left NULL); `session wrap --used` remains the manual override and takes precedence for a session.
|
|
102
102
|
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
103
103
|
|
|
104
104
|
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
@@ -34,6 +34,28 @@ When the session is bound (`lumo session attach <LUM-N>`), omit the identifier:
|
|
|
34
34
|
`lumo task memory add --category trap --trigger ... --outcome ...` records onto
|
|
35
35
|
the bound task; `lumo project memory add ...` records onto its project.
|
|
36
36
|
|
|
37
|
+
### Reconcile-on-write & deduplication
|
|
38
|
+
|
|
39
|
+
`memory add` does **not** unconditionally insert a new row. Before writing it:
|
|
40
|
+
|
|
41
|
+
1. Retrieves the nearest existing memories in the store (embedding similarity).
|
|
42
|
+
2. If a near-duplicate is found — including a **cross-language match** (e.g. you
|
|
43
|
+
supply content in Chinese but an equivalent English memory already exists) —
|
|
44
|
+
an LLM decides the outcome; otherwise the new memory is inserted directly.
|
|
45
|
+
|
|
46
|
+
The command prints **one** of these outcome lines:
|
|
47
|
+
|
|
48
|
+
| Output line | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `Added <CATEGORY> <SCOPE> memory …` | No near-duplicate found; stored as a new row. |
|
|
51
|
+
| `Merged into existing memory <id> (near-duplicate) …` | Near-duplicate found; the existing row was refined/updated in-place (UPDATE). |
|
|
52
|
+
| `Superseded an existing memory; new version <id> …` | New content contradicts an old memory; old row invalidated, new row created (SUPERSEDE). |
|
|
53
|
+
| `Skipped — duplicate of existing memory <id>, nothing written …` | Exact or near-exact duplicate; no write performed (NOOP). |
|
|
54
|
+
|
|
55
|
+
Content is **always normalized to English** before storing — the memory store has
|
|
56
|
+
a single canonical language. If you supply text in another language the CLI
|
|
57
|
+
translates it automatically; the stored memory will be in English.
|
|
58
|
+
|
|
37
59
|
### When to record a memory (worthiness)
|
|
38
60
|
|
|
39
61
|
Record only knowledge that is **invisible in the codebase** — the _why_ behind a
|
|
@@ -44,7 +66,7 @@ developer could learn from the source, git log, or docs. When unsure, don't.
|
|
|
44
66
|
|
|
45
67
|
### Which category
|
|
46
68
|
|
|
47
|
-
- `trap` — a pitfall. Describe the
|
|
69
|
+
- `trap` — a pitfall. Describe the problem (`--trigger`, `--outcome`); put a **one-line fix** inline via `--workaround`. Only when the fix is a genuine multi-step workflow, omit `--workaround` and add a separate `procedural` instead — never both (a `procedural` that just restates a trap's `--workaround` is a double-write).
|
|
48
70
|
- `decision` — an engineering decision (`--what` + `--why`, optional `--alternatives`/`--implications`).
|
|
49
71
|
- `convention` — a team rule (`--rule` + `--applies` = where it applies).
|
|
50
72
|
- `procedural` — a reusable workflow (`--workflow` + `--trigger` + `--step`…).
|
|
@@ -114,14 +114,14 @@ lumo task pr show LUM-42 128
|
|
|
114
114
|
Read-only audit view over the task's `LineageEdge` rows. Given a task
|
|
115
115
|
identifier (`LUM-N`), prints the causal trail:
|
|
116
116
|
|
|
117
|
-
- **Totals banner** — distinct
|
|
117
|
+
- **Totals banner** — distinct sessions, fragment count, edge count,
|
|
118
118
|
total tokens (input/output/cache split) and loops, and the outcome
|
|
119
119
|
distribution.
|
|
120
|
-
- **One block per
|
|
120
|
+
- **One block per session** — the group's cost shown **once** (token/loop),
|
|
121
121
|
the date it consumed context, then each context fragment as
|
|
122
122
|
`[OUTCOME] TYPE — <source label>`, plus a per-group outcome summary.
|
|
123
123
|
|
|
124
|
-
Cost is attributed once per
|
|
124
|
+
Cost is attributed once per session (a session that injected many fragments is
|
|
125
125
|
not double-counted). Fragment ids are canonical — MEMORY fragments survive
|
|
126
126
|
consolidation drift.
|
|
127
127
|
|
|
@@ -82,5 +82,27 @@ async function memoryProjectAdd(refArg, options) {
|
|
|
82
82
|
console.error(m ? `Error: ${(0, sanitize_1.sanitizeField)(m)}` : `Error: memory add failed (HTTP ${res.status})`);
|
|
83
83
|
return 1;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
let outcome = 'ADD';
|
|
86
|
+
let memId = '';
|
|
87
|
+
try {
|
|
88
|
+
const b = (await res.json());
|
|
89
|
+
if (typeof b.outcome === 'string')
|
|
90
|
+
outcome = b.outcome;
|
|
91
|
+
if (b.memory && typeof b.memory.id === 'string')
|
|
92
|
+
memId = b.memory.id;
|
|
93
|
+
}
|
|
94
|
+
catch { /* keep defaults */ }
|
|
95
|
+
switch (outcome) {
|
|
96
|
+
case 'NOOP':
|
|
97
|
+
process.stdout.write(`Skipped — duplicate of existing memory ${memId}, nothing written${echoSuffix}\n`);
|
|
98
|
+
break;
|
|
99
|
+
case 'UPDATE':
|
|
100
|
+
process.stdout.write(`Merged into existing memory ${memId} (near-duplicate)${echoSuffix}\n`);
|
|
101
|
+
break;
|
|
102
|
+
case 'SUPERSEDE':
|
|
103
|
+
process.stdout.write(`Superseded an existing memory; new version ${memId}${echoSuffix}\n`);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
process.stdout.write(`Added ${built.category} PROJECT memory${echoSuffix}\n`);
|
|
107
|
+
}
|
|
86
108
|
}
|
|
@@ -93,7 +93,29 @@ async function memoryTaskAdd(identifierArg, options) {
|
|
|
93
93
|
console.error(m ? `Error: ${(0, sanitize_1.sanitizeField)(m)}` : `Error: memory add failed (HTTP ${res.status})`);
|
|
94
94
|
return 1;
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const boundSuffix = identifierArg ? '' : ' (from bound session)';
|
|
97
|
+
let outcome = 'ADD';
|
|
98
|
+
let memId = '';
|
|
99
|
+
try {
|
|
100
|
+
const b = (await res.json());
|
|
101
|
+
if (typeof b.outcome === 'string')
|
|
102
|
+
outcome = b.outcome;
|
|
103
|
+
if (b.memory && typeof b.memory.id === 'string')
|
|
104
|
+
memId = b.memory.id;
|
|
105
|
+
}
|
|
106
|
+
catch { /* keep defaults */ }
|
|
107
|
+
switch (outcome) {
|
|
108
|
+
case 'NOOP':
|
|
109
|
+
process.stdout.write(`Skipped — duplicate of existing memory ${memId}, nothing written${boundSuffix}\n`);
|
|
110
|
+
break;
|
|
111
|
+
case 'UPDATE':
|
|
112
|
+
process.stdout.write(`Merged into existing memory ${memId} (near-duplicate)${boundSuffix}\n`);
|
|
113
|
+
break;
|
|
114
|
+
case 'SUPERSEDE':
|
|
115
|
+
process.stdout.write(`Superseded an existing memory; new version ${memId}${boundSuffix}\n`);
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
process.stdout.write(`Added ${built.category} memory to ${identifier}${boundSuffix}\n` +
|
|
119
|
+
'Tip: only useful for this task? If it generalizes, use `lumo project memory add` or `lumo memory promote` later.\n');
|
|
120
|
+
}
|
|
99
121
|
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.taskLineage = taskLineage;
|
|
4
4
|
exports.formatLineageMarkdown = formatLineageMarkdown;
|
|
5
|
+
exports.formatSignalHealth = formatSignalHealth;
|
|
5
6
|
const config_1 = require("../lib/config");
|
|
6
7
|
const api_1 = require("../lib/api");
|
|
7
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
-
async function taskLineage(identifier) {
|
|
9
|
+
async function taskLineage(identifier, opts) {
|
|
9
10
|
if (!identifier) {
|
|
10
11
|
console.error('Error: missing <identifier>. Usage: lumo task lineage <LUM-42>');
|
|
11
12
|
return 1;
|
|
@@ -42,6 +43,26 @@ async function taskLineage(identifier) {
|
|
|
42
43
|
}
|
|
43
44
|
const data = (await res.json());
|
|
44
45
|
process.stdout.write(formatLineageMarkdown(data));
|
|
46
|
+
if (opts?.signal) {
|
|
47
|
+
const signalUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/lineage/signal`;
|
|
48
|
+
let signalRes;
|
|
49
|
+
try {
|
|
50
|
+
signalRes = await fetch(signalUrl, {
|
|
51
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
process.stderr.write(`Warning: could not fetch signal health (${msg})\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!signalRes.ok) {
|
|
60
|
+
process.stderr.write(`Warning: signal health fetch failed (HTTP ${signalRes.status})\n`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const signalJson = (await signalRes.json());
|
|
64
|
+
process.stdout.write('\n' + formatSignalHealth(signalJson) + '\n');
|
|
65
|
+
}
|
|
45
66
|
}
|
|
46
67
|
/** Deterministic thousands separator (no locale dependency, test-stable). */
|
|
47
68
|
function groupThousands(n) {
|
|
@@ -64,6 +85,9 @@ function fragmentOutcomeCounts(fragments) {
|
|
|
64
85
|
c[f.outcome] += 1;
|
|
65
86
|
return c;
|
|
66
87
|
}
|
|
88
|
+
function usageMarker(used) {
|
|
89
|
+
return used === true ? '✓' : used === false ? '✗' : '·';
|
|
90
|
+
}
|
|
67
91
|
/**
|
|
68
92
|
* Render a LineageResponse as the audit-facing markdown trail. Pure function
|
|
69
93
|
* (no clock / env) so the CLI output is deterministic and unit-testable.
|
|
@@ -75,7 +99,7 @@ function formatLineageMarkdown(data) {
|
|
|
75
99
|
lines.push('');
|
|
76
100
|
if (data.groups.length === 0) {
|
|
77
101
|
lines.push('_No lineage edges recorded yet. Lineage is captured when a ' +
|
|
78
|
-
"
|
|
102
|
+
"bound session consumes this task's context; once that happens " +
|
|
79
103
|
'(and a PR merges / the task closes), the causal trail and its cost ' +
|
|
80
104
|
'will appear here._');
|
|
81
105
|
lines.push('');
|
|
@@ -104,10 +128,23 @@ function formatLineageMarkdown(data) {
|
|
|
104
128
|
}
|
|
105
129
|
const summary = outcomeSummary(fragmentOutcomeCounts(g.fragments));
|
|
106
130
|
lines.push(`**Fragments** (${g.fragments.length}${summary ? `: ${summary}` : ''}):`);
|
|
131
|
+
lines.push('_✓ used · · abstained · ✗ unused (manual)_');
|
|
107
132
|
for (const f of g.fragments) {
|
|
108
|
-
lines.push(`- [${f.outcome}] ${f.fragmentType} — ${(0, sanitize_1.sanitizeField)(f.sourceLabel)}`);
|
|
133
|
+
lines.push(`- ${usageMarker(f.used)} [${f.outcome}] ${f.fragmentType} — ${(0, sanitize_1.sanitizeField)(f.sourceLabel)}`);
|
|
109
134
|
}
|
|
110
135
|
lines.push('');
|
|
111
136
|
}
|
|
112
137
|
return lines.join('\n');
|
|
113
138
|
}
|
|
139
|
+
function formatSignalHealth(h) {
|
|
140
|
+
const lines = ['', '## Signal health'];
|
|
141
|
+
lines.push(`- Distribution: used ${h.distribution.used} · null ${h.distribution.abstained} · false ${h.distribution.unused}`);
|
|
142
|
+
lines.push(`- Per-session variance: ${h.perSessionVariance.toFixed(2)} (${h.votedSessions} voted sessions)`);
|
|
143
|
+
if (h.usedMergeRate !== null && h.baseMergeRate !== null) {
|
|
144
|
+
lines.push(`- Used × outcome: merge-rate(used) ${Math.round(h.usedMergeRate * 100)}% vs base ${Math.round(h.baseMergeRate * 100)}%`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
lines.push('- Used × outcome: insufficient resolved tasks');
|
|
148
|
+
}
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -313,7 +313,8 @@ taskPr
|
|
|
313
313
|
task
|
|
314
314
|
.command('lineage <identifier>')
|
|
315
315
|
.description("Show the causal trail: which context fragments fed this task, each fragment's outcome, and the token/loop cost of the run that consumed it")
|
|
316
|
-
.
|
|
316
|
+
.option('--signal', 'Append usage-signal health section (fetches /api/lineage/signal)')
|
|
317
|
+
.action(wrap((id, options) => (0, task_lineage_1.taskLineage)(id, options)));
|
|
317
318
|
const taskMemory = task
|
|
318
319
|
.command('memory')
|
|
319
320
|
.description('View and record memories scoped to a task');
|
|
@@ -2,31 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sumTranscriptUsage = sumTranscriptUsage;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
5
6
|
function asNumber(v) {
|
|
6
7
|
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Entries with no model string bucket under "unknown". Best-effort: returns
|
|
13
|
-
* null if the file can't be read or has no real assistant usage. Never throws.
|
|
10
|
+
* Sum `message.usage` across all (non-synthetic) assistant entries of one
|
|
11
|
+
* transcript JSONL file into the shared accumulators. Returns true if at
|
|
12
|
+
* least one real usage entry was seen. Never throws.
|
|
14
13
|
*/
|
|
15
|
-
function
|
|
14
|
+
function accumulateFileUsage(path, total, byModel) {
|
|
16
15
|
let text;
|
|
17
16
|
try {
|
|
18
|
-
text = (0, node_fs_1.readFileSync)(
|
|
17
|
+
text = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
19
18
|
}
|
|
20
19
|
catch {
|
|
21
|
-
return
|
|
20
|
+
return false;
|
|
22
21
|
}
|
|
23
|
-
const byModel = {};
|
|
24
|
-
const total = {
|
|
25
|
-
inputTokens: 0,
|
|
26
|
-
outputTokens: 0,
|
|
27
|
-
cacheReadTokens: 0,
|
|
28
|
-
cacheCreationTokens: 0,
|
|
29
|
-
};
|
|
30
22
|
let seen = false;
|
|
31
23
|
for (const raw of text.split('\n')) {
|
|
32
24
|
const line = raw.trim();
|
|
@@ -73,5 +65,46 @@ function sumTranscriptUsage(transcriptPath) {
|
|
|
73
65
|
m.cacheCreation += cacheCreation;
|
|
74
66
|
byModel[model] = m;
|
|
75
67
|
}
|
|
68
|
+
return seen;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Subagent (Task tool) turns are NOT in the main transcript — Claude Code
|
|
72
|
+
* writes them to sibling files at `<dir>/<sessionId>/subagents/agent-*.jsonl`.
|
|
73
|
+
* Resolve those for a main transcript path. Best-effort: returns [] when the
|
|
74
|
+
* directory is missing or unreadable. Never throws.
|
|
75
|
+
*/
|
|
76
|
+
function listSubagentTranscripts(transcriptPath) {
|
|
77
|
+
if (!transcriptPath.endsWith('.jsonl'))
|
|
78
|
+
return [];
|
|
79
|
+
const subagentsDir = (0, node_path_1.join)(transcriptPath.slice(0, -'.jsonl'.length), 'subagents');
|
|
80
|
+
try {
|
|
81
|
+
return (0, node_fs_1.readdirSync)(subagentsDir)
|
|
82
|
+
.filter(name => name.startsWith('agent-') && name.endsWith('.jsonl'))
|
|
83
|
+
.map(name => (0, node_path_1.join)(subagentsDir, name));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read a Claude Code transcript JSONL — plus any subagent transcripts under
|
|
91
|
+
* `<dir>/<sessionId>/subagents/agent-*.jsonl` — and sum `message.usage`
|
|
92
|
+
* across all (non-synthetic) assistant entries, grouped by `message.model`
|
|
93
|
+
* -> cumulative session totals. Entries with no model string bucket under
|
|
94
|
+
* "unknown". Best-effort: returns null if nothing readable has real
|
|
95
|
+
* assistant usage. Never throws.
|
|
96
|
+
*/
|
|
97
|
+
function sumTranscriptUsage(transcriptPath) {
|
|
98
|
+
const byModel = {};
|
|
99
|
+
const total = {
|
|
100
|
+
inputTokens: 0,
|
|
101
|
+
outputTokens: 0,
|
|
102
|
+
cacheReadTokens: 0,
|
|
103
|
+
cacheCreationTokens: 0,
|
|
104
|
+
};
|
|
105
|
+
let seen = accumulateFileUsage(transcriptPath, total, byModel);
|
|
106
|
+
for (const path of listSubagentTranscripts(transcriptPath)) {
|
|
107
|
+
seen = accumulateFileUsage(path, total, byModel) || seen;
|
|
108
|
+
}
|
|
76
109
|
return seen ? { total, byModel } : null;
|
|
77
110
|
}
|