@inferhub/usage 0.1.7 → 0.1.9
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/bin/statusline.mjs +34 -16
- package/dist/format.d.ts +2 -0
- package/dist/format.js +14 -7
- package/dist/format.test.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/session-id.d.ts +19 -0
- package/dist/session-id.js +103 -0
- package/package.json +1 -1
- package/src/format.test.ts +4 -4
- package/src/format.ts +14 -7
- package/src/index.ts +5 -0
- package/src/session-id.ts +106 -0
package/bin/statusline.mjs
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code statusline — one compact line.
|
|
4
|
+
*
|
|
5
|
+
* Session id strategy:
|
|
6
|
+
* - Prefer conversation-prefix hash from transcript (matches proxy sessionid.Derive)
|
|
7
|
+
* - Also try Claude's session_id (if proxy stored user/metadata session)
|
|
8
|
+
* - Never fall back to "latest account session" (that jumps between chats)
|
|
9
|
+
*/
|
|
2
10
|
import { readFileSync } from "node:fs";
|
|
3
11
|
import { dirname, join } from "node:path";
|
|
4
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -14,35 +22,45 @@ function readStdin() {
|
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
let
|
|
25
|
+
let input = {};
|
|
18
26
|
try {
|
|
19
|
-
|
|
27
|
+
input = JSON.parse(readStdin() || "{}");
|
|
20
28
|
} catch {
|
|
21
|
-
|
|
29
|
+
input = {};
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
const sessionId = client.sessionIdFromClaudeStatuslineInput({
|
|
34
|
+
session_id: input.session_id,
|
|
35
|
+
transcript_path: input.transcript_path,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Fetch with explicit session when known; otherwise omit session block rather
|
|
39
|
+
// than showing the account's latest unrelated conversation.
|
|
29
40
|
const { usage } = await client.fetchAccountUsageAuto({
|
|
30
41
|
window: "day",
|
|
31
|
-
|
|
42
|
+
sessionId: sessionId || undefined,
|
|
32
43
|
});
|
|
44
|
+
|
|
45
|
+
// If we couldn't derive a session id, strip session so we don't show wrong data
|
|
46
|
+
if (!sessionId) {
|
|
47
|
+
usage.session = null;
|
|
48
|
+
} else if (usage.session && usage.session.id !== sessionId) {
|
|
49
|
+
// API returned a different session (e.g. latest) — hide it
|
|
50
|
+
usage.session = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
const contextPct =
|
|
34
|
-
typeof
|
|
35
|
-
?
|
|
54
|
+
typeof input?.context_window?.used_percentage === "number"
|
|
55
|
+
? input.context_window.used_percentage
|
|
36
56
|
: null;
|
|
37
|
-
const model =
|
|
57
|
+
const model = input?.model?.display_name || input?.model?.id || null;
|
|
58
|
+
|
|
38
59
|
process.stdout.write(
|
|
39
|
-
client.formatStatusLine(usage, {
|
|
40
|
-
contextPct,
|
|
41
|
-
model,
|
|
42
|
-
}) + "\n",
|
|
60
|
+
client.formatStatusLine(usage, { contextPct, model }) + "\n",
|
|
43
61
|
);
|
|
44
62
|
} catch (e) {
|
|
45
63
|
const msg = e instanceof Error ? e.message : String(e);
|
|
46
|
-
process.stdout.write("InferHub · offline (" + msg.slice(0,
|
|
64
|
+
process.stdout.write("InferHub · offline (" + msg.slice(0, 40) + ")\n");
|
|
47
65
|
process.exitCode = 0;
|
|
48
66
|
}
|
package/dist/format.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AccountUsage } from "./types.js";
|
|
2
2
|
export declare function money(usdc: string | number | null | undefined, digits?: number): string;
|
|
3
3
|
export declare function tokens(n: number | null | undefined): string;
|
|
4
|
+
/** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
|
|
5
|
+
export declare function compactCount(n: number | null | undefined): string;
|
|
4
6
|
export declare function shortModel(model: string | null | undefined): string;
|
|
5
7
|
/**
|
|
6
8
|
* One-line Claude statusline.
|
package/dist/format.js
CHANGED
|
@@ -9,12 +9,19 @@ export function money(usdc, digits = 2) {
|
|
|
9
9
|
return `$${n.toFixed(2)}`;
|
|
10
10
|
}
|
|
11
11
|
export function tokens(n) {
|
|
12
|
+
return compactCount(n);
|
|
13
|
+
}
|
|
14
|
+
/** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
|
|
15
|
+
export function compactCount(n) {
|
|
12
16
|
const v = Number(n ?? 0);
|
|
13
|
-
if (v
|
|
17
|
+
if (!Number.isFinite(v))
|
|
18
|
+
return "0";
|
|
19
|
+
const abs = Math.abs(v);
|
|
20
|
+
if (abs >= 1_000_000_000)
|
|
14
21
|
return `${(v / 1_000_000_000).toFixed(1)}B`;
|
|
15
|
-
if (
|
|
22
|
+
if (abs >= 1_000_000)
|
|
16
23
|
return `${(v / 1_000_000).toFixed(1)}M`;
|
|
17
|
-
if (
|
|
24
|
+
if (abs >= 1_000)
|
|
18
25
|
return `${(v / 1_000).toFixed(1)}k`;
|
|
19
26
|
return String(Math.round(v));
|
|
20
27
|
}
|
|
@@ -25,8 +32,8 @@ export function shortModel(model) {
|
|
|
25
32
|
return parts[parts.length - 1] || model;
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
|
-
* Compact metric: "4r/1.2M" free, or "$0.
|
|
29
|
-
*
|
|
35
|
+
* Compact metric: "4r/1.2M" free, or "$0.41/3.9kr/355.7M" when billed.
|
|
36
|
+
* Both requests and tokens use k/M/B.
|
|
30
37
|
*/
|
|
31
38
|
function compact(spendUsdc, requests, totalTokens) {
|
|
32
39
|
const spend = Number(spendUsdc ?? 0);
|
|
@@ -38,9 +45,9 @@ function compact(spendUsdc, requests, totalTokens) {
|
|
|
38
45
|
if (spend > 0)
|
|
39
46
|
bits.push(money(spend));
|
|
40
47
|
if (req > 0)
|
|
41
|
-
bits.push(`${req}r`);
|
|
48
|
+
bits.push(`${compactCount(req)}r`);
|
|
42
49
|
if (tok > 0)
|
|
43
|
-
bits.push(
|
|
50
|
+
bits.push(compactCount(tok));
|
|
44
51
|
return bits.join("/") || "—";
|
|
45
52
|
}
|
|
46
53
|
/**
|
package/dist/format.test.js
CHANGED
|
@@ -49,11 +49,11 @@ test("formatStatusLine includes tokens compactly", () => {
|
|
|
49
49
|
cache: { cached: false, ttl_seconds: 30 },
|
|
50
50
|
};
|
|
51
51
|
const line = formatStatusLine(usage, { model: "grok-4.5", contextPct: 8 });
|
|
52
|
-
assert.equal(line, "InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/
|
|
53
|
-
assert.ok(line.length <
|
|
54
|
-
assert.match(line, /
|
|
55
|
-
assert.match(line, /26\.4M/);
|
|
52
|
+
assert.equal(line, "InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/3.9kr/354.2M · bal $1.31 · top gpt-5.5");
|
|
53
|
+
assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
|
|
54
|
+
assert.match(line, /3\.9kr/);
|
|
56
55
|
assert.match(line, /354\.2M/);
|
|
56
|
+
assert.doesNotMatch(line, /3933r/);
|
|
57
57
|
// empty session omitted
|
|
58
58
|
const noSess = { ...usage, session: null };
|
|
59
59
|
assert.doesNotMatch(formatStatusLine(noSess), /sess /);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind, } from "./types.js";
|
|
2
2
|
export { DEFAULT_BASE_URL, fetchAccountUsage, fetchAccountUsageAuto, normalizeBaseUrl, resolveAuth, usageUrl, } from "./client.js";
|
|
3
3
|
export { formatReport, formatStatusLine, money, shortModel, tokens } from "./format.js";
|
|
4
|
+
export { deriveConversationSessionId, messagesFromTranscript, sessionIdFromClaudeStatuslineInput, } from "./session-id.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { DEFAULT_BASE_URL, fetchAccountUsage, fetchAccountUsageAuto, normalizeBaseUrl, resolveAuth, usageUrl, } from "./client.js";
|
|
2
2
|
export { formatReport, formatStatusLine, money, shortModel, tokens } from "./format.js";
|
|
3
|
+
export { deriveConversationSessionId, messagesFromTranscript, sessionIdFromClaudeStatuslineInput, } from "./session-id.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Same algorithm as proxy sessionid.conversationPrefix + sha256[:16]. */
|
|
2
|
+
export declare function deriveConversationSessionId(messages: Array<{
|
|
3
|
+
role?: string;
|
|
4
|
+
content?: unknown;
|
|
5
|
+
type?: string;
|
|
6
|
+
}>): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort extract ordered messages from a Claude transcript jsonl.
|
|
9
|
+
* Handles common shapes: {type:"user"|"assistant", message:{role,content}} etc.
|
|
10
|
+
*/
|
|
11
|
+
export declare function messagesFromTranscript(transcriptPath: string): Array<{
|
|
12
|
+
role?: string;
|
|
13
|
+
content?: unknown;
|
|
14
|
+
type?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function sessionIdFromClaudeStatuslineInput(input: {
|
|
17
|
+
session_id?: string;
|
|
18
|
+
transcript_path?: string;
|
|
19
|
+
}): string | null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match proxy/internal/sessionid.Derive conversation-prefix hashing so the
|
|
3
|
+
* Claude statusline can query the same session_id that request_logs stores.
|
|
4
|
+
*
|
|
5
|
+
* Priority mirrors the proxy:
|
|
6
|
+
* 1. Explicit session id (Claude stdin session_id) if we ever store it server-side
|
|
7
|
+
* 2. sha256(system + first user message)[:16] hex — same as proxy
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
function flattenContent(content) {
|
|
12
|
+
if (content == null)
|
|
13
|
+
return "";
|
|
14
|
+
if (typeof content === "string")
|
|
15
|
+
return content;
|
|
16
|
+
if (Array.isArray(content)) {
|
|
17
|
+
return content.map(flattenContent).join("");
|
|
18
|
+
}
|
|
19
|
+
if (typeof content === "object") {
|
|
20
|
+
const o = content;
|
|
21
|
+
if (typeof o.text === "string")
|
|
22
|
+
return o.text;
|
|
23
|
+
if (o.content != null)
|
|
24
|
+
return flattenContent(o.content);
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
/** Same algorithm as proxy sessionid.conversationPrefix + sha256[:16]. */
|
|
29
|
+
export function deriveConversationSessionId(messages) {
|
|
30
|
+
let system = "";
|
|
31
|
+
let firstUser = "";
|
|
32
|
+
for (const m of messages) {
|
|
33
|
+
const role = (m.role || m.type || "").toLowerCase();
|
|
34
|
+
if (role === "system") {
|
|
35
|
+
const t = flattenContent(m.content);
|
|
36
|
+
if (t)
|
|
37
|
+
system += t + "\n";
|
|
38
|
+
}
|
|
39
|
+
else if (role === "user" || role === "human") {
|
|
40
|
+
firstUser = flattenContent(m.content);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!firstUser)
|
|
45
|
+
return null;
|
|
46
|
+
const prefix = system + firstUser;
|
|
47
|
+
const sum = createHash("sha256").update(prefix, "utf8").digest("hex");
|
|
48
|
+
return sum.slice(0, 32); // 16 bytes = 32 hex chars (proxy uses sum[:16] bytes)
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Best-effort extract ordered messages from a Claude transcript jsonl.
|
|
52
|
+
* Handles common shapes: {type:"user"|"assistant", message:{role,content}} etc.
|
|
53
|
+
*/
|
|
54
|
+
export function messagesFromTranscript(transcriptPath) {
|
|
55
|
+
if (!transcriptPath || !existsSync(transcriptPath))
|
|
56
|
+
return [];
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = readFileSync(transcriptPath, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const line of text.split(/\r?\n/)) {
|
|
66
|
+
if (!line.trim())
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const row = JSON.parse(line);
|
|
70
|
+
// Claude Code transcript variants
|
|
71
|
+
if (row.type === "user" || row.type === "assistant" || row.type === "system") {
|
|
72
|
+
const msg = row.message || row;
|
|
73
|
+
out.push({
|
|
74
|
+
role: String(msg.role || row.type),
|
|
75
|
+
content: msg.content ?? row.content,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (typeof row.role === "string" && row.content != null) {
|
|
80
|
+
out.push({ role: row.role, content: row.content });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* skip bad lines */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
export function sessionIdFromClaudeStatuslineInput(input) {
|
|
90
|
+
// Prefer Claude UUID — proxy now stores metadata.user_id / session_id when
|
|
91
|
+
// present, so each Claude conversation keeps its own counter.
|
|
92
|
+
if (input.session_id && input.session_id.length > 8)
|
|
93
|
+
return input.session_id;
|
|
94
|
+
// Fallback: conversation-prefix hash (matches proxy sessionid.Derive when no
|
|
95
|
+
// explicit session was sent).
|
|
96
|
+
if (input.transcript_path) {
|
|
97
|
+
const msgs = messagesFromTranscript(input.transcript_path);
|
|
98
|
+
const derived = deriveConversationSessionId(msgs);
|
|
99
|
+
if (derived)
|
|
100
|
+
return derived;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
package/package.json
CHANGED
package/src/format.test.ts
CHANGED
|
@@ -59,12 +59,12 @@ test("formatStatusLine includes tokens compactly", () => {
|
|
|
59
59
|
const line = formatStatusLine(usage, { model: "grok-4.5", contextPct: 8 });
|
|
60
60
|
assert.equal(
|
|
61
61
|
line,
|
|
62
|
-
"InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/
|
|
62
|
+
"InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/3.9kr/354.2M · bal $1.31 · top gpt-5.5",
|
|
63
63
|
);
|
|
64
|
-
assert.ok(line.length <
|
|
65
|
-
assert.match(line, /
|
|
66
|
-
assert.match(line, /26\.4M/);
|
|
64
|
+
assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
|
|
65
|
+
assert.match(line, /3\.9kr/);
|
|
67
66
|
assert.match(line, /354\.2M/);
|
|
67
|
+
assert.doesNotMatch(line, /3933r/);
|
|
68
68
|
|
|
69
69
|
// empty session omitted
|
|
70
70
|
const noSess = { ...usage, session: null };
|
package/src/format.ts
CHANGED
|
@@ -9,10 +9,17 @@ export function money(usdc: string | number | null | undefined, digits = 2): str
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function tokens(n: number | null | undefined): string {
|
|
12
|
+
return compactCount(n);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
|
|
16
|
+
export function compactCount(n: number | null | undefined): string {
|
|
12
17
|
const v = Number(n ?? 0);
|
|
13
|
-
if (v
|
|
14
|
-
|
|
15
|
-
if (
|
|
18
|
+
if (!Number.isFinite(v)) return "0";
|
|
19
|
+
const abs = Math.abs(v);
|
|
20
|
+
if (abs >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
|
|
21
|
+
if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
22
|
+
if (abs >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
|
|
16
23
|
return String(Math.round(v));
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -23,8 +30,8 @@ export function shortModel(model: string | null | undefined): string {
|
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
* Compact metric: "4r/1.2M" free, or "$0.
|
|
27
|
-
*
|
|
33
|
+
* Compact metric: "4r/1.2M" free, or "$0.41/3.9kr/355.7M" when billed.
|
|
34
|
+
* Both requests and tokens use k/M/B.
|
|
28
35
|
*/
|
|
29
36
|
function compact(
|
|
30
37
|
spendUsdc: string | number | null | undefined,
|
|
@@ -38,8 +45,8 @@ function compact(
|
|
|
38
45
|
|
|
39
46
|
const bits: string[] = [];
|
|
40
47
|
if (spend > 0) bits.push(money(spend));
|
|
41
|
-
if (req > 0) bits.push(`${req}r`);
|
|
42
|
-
if (tok > 0) bits.push(
|
|
48
|
+
if (req > 0) bits.push(`${compactCount(req)}r`);
|
|
49
|
+
if (tok > 0) bits.push(compactCount(tok));
|
|
43
50
|
return bits.join("/") || "—";
|
|
44
51
|
}
|
|
45
52
|
|
package/src/index.ts
CHANGED
|
@@ -13,3 +13,8 @@ export {
|
|
|
13
13
|
usageUrl,
|
|
14
14
|
} from "./client.js";
|
|
15
15
|
export { formatReport, formatStatusLine, money, shortModel, tokens } from "./format.js";
|
|
16
|
+
export {
|
|
17
|
+
deriveConversationSessionId,
|
|
18
|
+
messagesFromTranscript,
|
|
19
|
+
sessionIdFromClaudeStatuslineInput,
|
|
20
|
+
} from "./session-id.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match proxy/internal/sessionid.Derive conversation-prefix hashing so the
|
|
3
|
+
* Claude statusline can query the same session_id that request_logs stores.
|
|
4
|
+
*
|
|
5
|
+
* Priority mirrors the proxy:
|
|
6
|
+
* 1. Explicit session id (Claude stdin session_id) if we ever store it server-side
|
|
7
|
+
* 2. sha256(system + first user message)[:16] hex — same as proxy
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
function flattenContent(content: unknown): string {
|
|
13
|
+
if (content == null) return "";
|
|
14
|
+
if (typeof content === "string") return content;
|
|
15
|
+
if (Array.isArray(content)) {
|
|
16
|
+
return content.map(flattenContent).join("");
|
|
17
|
+
}
|
|
18
|
+
if (typeof content === "object") {
|
|
19
|
+
const o = content as { text?: unknown; content?: unknown };
|
|
20
|
+
if (typeof o.text === "string") return o.text;
|
|
21
|
+
if (o.content != null) return flattenContent(o.content);
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Same algorithm as proxy sessionid.conversationPrefix + sha256[:16]. */
|
|
27
|
+
export function deriveConversationSessionId(messages: Array<{
|
|
28
|
+
role?: string;
|
|
29
|
+
content?: unknown;
|
|
30
|
+
type?: string;
|
|
31
|
+
}>): string | null {
|
|
32
|
+
let system = "";
|
|
33
|
+
let firstUser = "";
|
|
34
|
+
for (const m of messages) {
|
|
35
|
+
const role = (m.role || m.type || "").toLowerCase();
|
|
36
|
+
if (role === "system") {
|
|
37
|
+
const t = flattenContent(m.content);
|
|
38
|
+
if (t) system += t + "\n";
|
|
39
|
+
} else if (role === "user" || role === "human") {
|
|
40
|
+
firstUser = flattenContent(m.content);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!firstUser) return null;
|
|
45
|
+
const prefix = system + firstUser;
|
|
46
|
+
const sum = createHash("sha256").update(prefix, "utf8").digest("hex");
|
|
47
|
+
return sum.slice(0, 32); // 16 bytes = 32 hex chars (proxy uses sum[:16] bytes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Best-effort extract ordered messages from a Claude transcript jsonl.
|
|
52
|
+
* Handles common shapes: {type:"user"|"assistant", message:{role,content}} etc.
|
|
53
|
+
*/
|
|
54
|
+
export function messagesFromTranscript(transcriptPath: string): Array<{
|
|
55
|
+
role?: string;
|
|
56
|
+
content?: unknown;
|
|
57
|
+
type?: string;
|
|
58
|
+
}> {
|
|
59
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return [];
|
|
60
|
+
let text: string;
|
|
61
|
+
try {
|
|
62
|
+
text = readFileSync(transcriptPath, "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const out: Array<{ role?: string; content?: unknown; type?: string }> = [];
|
|
67
|
+
for (const line of text.split(/\r?\n/)) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
try {
|
|
70
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
71
|
+
// Claude Code transcript variants
|
|
72
|
+
if (row.type === "user" || row.type === "assistant" || row.type === "system") {
|
|
73
|
+
const msg = (row.message as Record<string, unknown>) || row;
|
|
74
|
+
out.push({
|
|
75
|
+
role: String(msg.role || row.type),
|
|
76
|
+
content: msg.content ?? row.content,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof row.role === "string" && row.content != null) {
|
|
81
|
+
out.push({ role: row.role, content: row.content });
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* skip bad lines */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function sessionIdFromClaudeStatuslineInput(input: {
|
|
91
|
+
session_id?: string;
|
|
92
|
+
transcript_path?: string;
|
|
93
|
+
}): string | null {
|
|
94
|
+
// Prefer Claude UUID — proxy now stores metadata.user_id / session_id when
|
|
95
|
+
// present, so each Claude conversation keeps its own counter.
|
|
96
|
+
if (input.session_id && input.session_id.length > 8) return input.session_id;
|
|
97
|
+
|
|
98
|
+
// Fallback: conversation-prefix hash (matches proxy sessionid.Derive when no
|
|
99
|
+
// explicit session was sent).
|
|
100
|
+
if (input.transcript_path) {
|
|
101
|
+
const msgs = messagesFromTranscript(input.transcript_path);
|
|
102
|
+
const derived = deriveConversationSessionId(msgs);
|
|
103
|
+
if (derived) return derived;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|