@lumoai/cli 1.18.0 → 1.19.0
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.
|
@@ -36,6 +36,16 @@ do `generate + tsc` atomically once at the end.
|
|
|
36
36
|
in first) — `cli/` has no jest config, and running from the main checkout hits
|
|
37
37
|
the `cli/package.json` haste collision and silently runs the wrong tests.
|
|
38
38
|
|
|
39
|
+
**Never run `npm install` / `npm ci` inside a worktree.** The worktree shares
|
|
40
|
+
the main checkout's `node_modules` via a symlink; npm does not respect the link
|
|
41
|
+
— it deletes it and reifies a full standalone `node_modules` (thousands of
|
|
42
|
+
packages, ~1 min, and the shared prisma-client is gone). Run installs only in
|
|
43
|
+
the main checkout, then re-create the symlink if npm replaced it. (Older npm
|
|
44
|
+
could instead plant a self-referential `node_modules/node_modules` in the shared
|
|
45
|
+
tree, which hard-panics Turbopack's `next build`; the `prebuild`/`predev`/
|
|
46
|
+
`preanalyze` guard — `scripts/fix-nodemodules-selflink.ts` — removes that link
|
|
47
|
+
automatically, but the rule above avoids the whole mess.)
|
|
48
|
+
|
|
39
49
|
## `lumo worktree rm <LUM-N>`
|
|
40
50
|
|
|
41
51
|
Removes the worktree for a task. Requires `--yes`. Refuses a dirty worktree
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.formatHookStdoutLines = formatHookStdoutLines;
|
|
4
4
|
exports.formatAutoBindLine = formatAutoBindLine;
|
|
5
5
|
exports.resolveSessionStartStdout = resolveSessionStartStdout;
|
|
6
|
+
exports.augmentBodyWithUsage = augmentBodyWithUsage;
|
|
6
7
|
exports.runHook = runHook;
|
|
7
8
|
exports.runHookWithBody = runHookWithBody;
|
|
8
9
|
const config_1 = require("./config");
|
|
@@ -12,6 +13,7 @@ const sanitize_1 = require("./sanitize");
|
|
|
12
13
|
const agent_1 = require("./agent");
|
|
13
14
|
const git_task_1 = require("./git-task");
|
|
14
15
|
const format_1 = require("./format");
|
|
16
|
+
const transcript_usage_1 = require("./transcript-usage");
|
|
15
17
|
/**
|
|
16
18
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
17
19
|
* logged, and `runHook` exits 0 — Claude Code is never blocked beyond
|
|
@@ -260,6 +262,31 @@ async function postBindTask(sessionId, identifier, token, apiUrl) {
|
|
|
260
262
|
clearTimeout(timer);
|
|
261
263
|
}
|
|
262
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* For stop/stop-failure hooks, read the transcript named in the payload and
|
|
267
|
+
* fold cumulative token totals into the body under `_lumo_usage` so the server
|
|
268
|
+
* can persist them on the Session. Best-effort: returns the body unchanged on
|
|
269
|
+
* any failure (no transcript_path, unreadable, unparseable, no usage). The
|
|
270
|
+
* hook is never blocked or failed by this.
|
|
271
|
+
*/
|
|
272
|
+
function augmentBodyWithUsage(path, body) {
|
|
273
|
+
if (path !== 'stop' && path !== 'stop-failure')
|
|
274
|
+
return body;
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(body || '{}');
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return body;
|
|
281
|
+
}
|
|
282
|
+
const transcriptPath = parsed['transcript_path'];
|
|
283
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath)
|
|
284
|
+
return body;
|
|
285
|
+
const usage = (0, transcript_usage_1.sumTranscriptUsage)(transcriptPath);
|
|
286
|
+
if (!usage)
|
|
287
|
+
return body;
|
|
288
|
+
return JSON.stringify({ ...parsed, _lumo_usage: usage });
|
|
289
|
+
}
|
|
263
290
|
/**
|
|
264
291
|
* POST the hook body to /api/hooks/<path> with a short timeout. All errors
|
|
265
292
|
* — credential missing, network failure, timeout, non-2xx — are routed to
|
|
@@ -308,11 +335,12 @@ async function runHookWithBody(path, body, agentToken) {
|
|
|
308
335
|
const agentEnum = agentToken ? (0, agent_1.normalizeAgent)(agentToken) : null;
|
|
309
336
|
if (agentEnum)
|
|
310
337
|
headers['X-Lumo-Agent'] = agentEnum;
|
|
338
|
+
const outgoingBody = augmentBodyWithUsage(path, body) || '{}';
|
|
311
339
|
try {
|
|
312
340
|
const res = await fetch(url, {
|
|
313
341
|
method: 'POST',
|
|
314
342
|
headers,
|
|
315
|
-
body:
|
|
343
|
+
body: outgoingBody,
|
|
316
344
|
signal: controller.signal,
|
|
317
345
|
});
|
|
318
346
|
if (!res.ok) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sumTranscriptUsage = sumTranscriptUsage;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
function asNumber(v) {
|
|
6
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Read a Claude Code transcript JSONL and sum `message.usage` across all
|
|
10
|
+
* assistant entries -> cumulative session token totals. Best-effort: returns
|
|
11
|
+
* null if the file can't be read or has no assistant usage. Never throws.
|
|
12
|
+
*/
|
|
13
|
+
function sumTranscriptUsage(transcriptPath) {
|
|
14
|
+
let text;
|
|
15
|
+
try {
|
|
16
|
+
text = (0, node_fs_1.readFileSync)(transcriptPath, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const total = {
|
|
22
|
+
inputTokens: 0,
|
|
23
|
+
outputTokens: 0,
|
|
24
|
+
cacheReadTokens: 0,
|
|
25
|
+
cacheCreationTokens: 0,
|
|
26
|
+
};
|
|
27
|
+
let seen = false;
|
|
28
|
+
for (const raw of text.split('\n')) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (!line)
|
|
31
|
+
continue;
|
|
32
|
+
let obj;
|
|
33
|
+
try {
|
|
34
|
+
obj = JSON.parse(line);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!obj || typeof obj !== 'object')
|
|
40
|
+
continue;
|
|
41
|
+
const o = obj;
|
|
42
|
+
if (o.type !== 'assistant')
|
|
43
|
+
continue;
|
|
44
|
+
const usage = o.message?.usage;
|
|
45
|
+
if (!usage)
|
|
46
|
+
continue;
|
|
47
|
+
seen = true;
|
|
48
|
+
total.inputTokens += asNumber(usage.input_tokens);
|
|
49
|
+
total.outputTokens += asNumber(usage.output_tokens);
|
|
50
|
+
total.cacheReadTokens += asNumber(usage.cache_read_input_tokens);
|
|
51
|
+
total.cacheCreationTokens += asNumber(usage.cache_creation_input_tokens);
|
|
52
|
+
}
|
|
53
|
+
return seen ? total : null;
|
|
54
|
+
}
|
package/dist/shared/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ── Agent Error types ────────────────────────────────────────────────────────
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
|
|
4
|
+
exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
|
|
5
5
|
exports.userFriendlyError = userFriendlyError;
|
|
6
6
|
class AgentError extends Error {
|
|
7
7
|
code;
|
|
@@ -33,3 +33,6 @@ function userFriendlyError(code) {
|
|
|
33
33
|
var markdown_tiptap_1 = require("./markdown-tiptap");
|
|
34
34
|
Object.defineProperty(exports, "markdownToTiptap", { enumerable: true, get: function () { return markdown_tiptap_1.markdownToTiptap; } });
|
|
35
35
|
Object.defineProperty(exports, "tiptapToMarkdown", { enumerable: true, get: function () { return markdown_tiptap_1.tiptapToMarkdown; } });
|
|
36
|
+
// ── Stream-json run usage parser ──────────────────────────────────────────────
|
|
37
|
+
var stream_json_usage_1 = require("./stream-json-usage");
|
|
38
|
+
Object.defineProperty(exports, "parseStreamJsonUsage", { enumerable: true, get: function () { return stream_json_usage_1.parseStreamJsonUsage; } });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStreamJsonUsage = parseStreamJsonUsage;
|
|
4
|
+
function asNumber(v) {
|
|
5
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse the final `type: "result"` message of a stream-json claude run.
|
|
9
|
+
* Accepts either the whole stdout log or a single result line. Scans for the
|
|
10
|
+
* LAST parseable line whose `type === 'result'` and lifts `usage.*` +
|
|
11
|
+
* `num_turns`. Returns null when no result line is present (agent crashed or
|
|
12
|
+
* timed out before emitting one) — callers leave the cost columns null.
|
|
13
|
+
*/
|
|
14
|
+
function parseStreamJsonUsage(text) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return null;
|
|
17
|
+
const lines = text.split('\n');
|
|
18
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
+
const line = lines[i]?.trim();
|
|
20
|
+
if (!line)
|
|
21
|
+
continue;
|
|
22
|
+
let obj;
|
|
23
|
+
try {
|
|
24
|
+
obj = JSON.parse(line);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (obj &&
|
|
30
|
+
typeof obj === 'object' &&
|
|
31
|
+
obj.type === 'result') {
|
|
32
|
+
const r = obj;
|
|
33
|
+
const usage = r.usage ?? {};
|
|
34
|
+
return {
|
|
35
|
+
inputTokens: asNumber(usage['input_tokens']),
|
|
36
|
+
outputTokens: asNumber(usage['output_tokens']),
|
|
37
|
+
cacheReadTokens: asNumber(usage['cache_read_input_tokens']),
|
|
38
|
+
cacheCreationTokens: asNumber(usage['cache_creation_input_tokens']),
|
|
39
|
+
loopCount: asNumber(r.num_turns),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|