@mnemom/mnemom 0.17.5-next.0 → 0.17.5

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.
@@ -31,7 +31,6 @@ import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname, userInfo } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { statuslineLaunch } from "../lib/statusline.js";
35
34
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
36
35
  import { createApiKey, getAgentByName } from "../lib/api.js";
37
36
  import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
@@ -979,21 +978,6 @@ function launchTerminal(ctx) {
979
978
  applyGatewayHintHeaders(childEnv);
980
979
  if (ctx.options.slack)
981
980
  args.push(...slackChannelArgs(sessionName));
982
- // The status-bar savings counter (lib/statusline.ts). Claude Code only; the
983
- // user's own statusLine keeps running in front of it.
984
- if (ctx.cli.name === "claude") {
985
- const sl = statuslineLaunch({
986
- passthrough: ctx.options.passthrough ?? [],
987
- env: childEnv,
988
- cwd: process.cwd(),
989
- node: process.execPath,
990
- bin: join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "statusline-bin.js"),
991
- });
992
- if (sl) {
993
- args.push(...sl.args);
994
- Object.assign(childEnv, sl.env);
995
- }
996
- }
997
981
  if (ctx.contextHint) {
998
982
  seedContextHintFlag(childEnv);
999
983
  childEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
@@ -45,14 +45,6 @@ export declare function rowCost(r: Row): number;
45
45
  export declare function actualBill(rows: readonly Row[]): number;
46
46
  /** An ungoverned request's original context = its billed prompt. */
47
47
  export declare function origContext(r: Row): number;
48
- /**
49
- * One transcript line → its billed request, or null (not an assistant record with usage).
50
- * `id` is the message id (or record uuid); null when the record carries neither.
51
- */
52
- export declare function parseUsageLine(line: Buffer, subagent: boolean): {
53
- id: string | null;
54
- row: Row;
55
- } | null;
56
48
  /**
57
49
  * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
58
50
  * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
@@ -69,37 +69,42 @@ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
69
69
  const USAGE = Buffer.from('"usage"');
70
70
  const ASSISTANT = Buffer.from('"assistant"');
71
71
  /**
72
- * One transcript line → its billed request, or null (not an assistant record with usage).
73
- * `id` is the message id (or record uuid); null when the record carries neither.
72
+ * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
73
+ * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
74
+ * session copies messages) is skipped, so each request is billed once, to its origin.
74
75
  */
75
- export function parseUsageLine(line, subagent) {
76
- if (line.indexOf(USAGE) === -1 || line.indexOf(ASSISTANT) === -1)
77
- return null;
78
- const d = parseJson(line);
79
- if (!d || d.type !== "assistant")
80
- return null;
81
- const m = d.message;
82
- if (!m || typeof m !== "object")
83
- return null;
84
- const u = m.usage;
85
- if (!u || typeof u !== "object")
86
- return null;
87
- const model = typeof m.model === "string" ? m.model : "";
88
- if (!model || model.startsWith("<"))
89
- return null;
90
- const t = isoSeconds(d.timestamp);
91
- if (t === null)
92
- return null;
93
- const cc = u.cache_creation ?? {};
94
- const cw = num(u.cache_creation_input_tokens);
95
- const cw1h = num(cc.ephemeral_1h_input_tokens);
96
- // Older Claude Code versions omit the 5m split (or the whole cache_creation object):
97
- // derive 5m as total − 1h, and never let a reported 5m share leave writes unbilled.
98
- const cw5 = Math.max(num(cc.ephemeral_5m_input_tokens), cw - cw1h);
99
- const id = (typeof m.id === "string" && m.id) || (typeof d.uuid === "string" && d.uuid) || null;
100
- return {
101
- id,
102
- row: {
76
+ export function loadTranscript(path, seen) {
77
+ const byId = new Map();
78
+ const subagent = path.includes("/subagents/");
79
+ let anon = 0;
80
+ forEachLine(path, (line) => {
81
+ if (line.indexOf(USAGE) === -1 || line.indexOf(ASSISTANT) === -1)
82
+ return;
83
+ const d = parseJson(line);
84
+ if (!d || d.type !== "assistant")
85
+ return;
86
+ const m = d.message;
87
+ if (!m || typeof m !== "object")
88
+ return;
89
+ const u = m.usage;
90
+ if (!u || typeof u !== "object")
91
+ return;
92
+ const model = typeof m.model === "string" ? m.model : "";
93
+ if (!model || model.startsWith("<"))
94
+ return;
95
+ const t = isoSeconds(d.timestamp);
96
+ if (t === null)
97
+ return;
98
+ const cc = u.cache_creation ?? {};
99
+ const cw = num(u.cache_creation_input_tokens);
100
+ const cw1h = num(cc.ephemeral_1h_input_tokens);
101
+ // Older Claude Code versions omit the 5m split (or the whole cache_creation object):
102
+ // derive 5m as total − 1h, and never let a reported 5m share leave writes unbilled.
103
+ const cw5 = Math.max(num(cc.ephemeral_5m_input_tokens), cw - cw1h);
104
+ const id = (typeof m.id === "string" && m.id) ||
105
+ (typeof d.uuid === "string" && d.uuid) ||
106
+ `anon-${path}-${anon++}`;
107
+ byId.set(id, {
103
108
  t,
104
109
  model,
105
110
  inp: num(u.input_tokens),
@@ -109,22 +114,7 @@ export function parseUsageLine(line, subagent) {
109
114
  out: num(u.output_tokens),
110
115
  mainThread: d.isSidechain !== true,
111
116
  subagent,
112
- },
113
- };
114
- }
115
- /**
116
- * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
117
- * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
118
- * session copies messages) is skipped, so each request is billed once, to its origin.
119
- */
120
- export function loadTranscript(path, seen) {
121
- const byId = new Map();
122
- const subagent = path.includes("/subagents/");
123
- let anon = 0;
124
- forEachLine(path, (line) => {
125
- const r = parseUsageLine(line, subagent);
126
- if (r)
127
- byId.set(r.id ?? `anon-${path}-${anon++}`, r.row);
117
+ });
128
118
  });
129
119
  const out = [];
130
120
  for (const [id, row] of byId) {
@@ -7,7 +7,8 @@
7
7
  * priced set (index.ts pricedSessions: every session is counted). A headless
8
8
  * `claude -p` session (entrypoint sdk-cli) is marked: the env of the script that
9
9
  * launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
10
- * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
10
+ * - Sessions are discovered in ~/.claude/projects, every ~/.claude-*\/projects, and
11
+ * $CLAUDE_CONFIG_DIR/projects wherever it points (a Console login often lives there).
11
12
  * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
12
13
  * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
13
14
  * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
@@ -39,8 +40,11 @@ export interface ScopeResult {
39
40
  kept: Map<string, Session>;
40
41
  excluded: Map<string, string[]>;
41
42
  }
42
- /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
43
- export declare function defaultProjectRoots(home?: string): string[];
43
+ /**
44
+ * ~/.claude/projects, ~/.claude-*\/projects and $CLAUDE_CONFIG_DIR/projects that exist
45
+ * (realpath, de-duplicated). CLAUDE_CONFIG_DIR can point anywhere, not only ~/.claude-*.
46
+ */
47
+ export declare function defaultProjectRoots(home?: string, env?: Record<string, string | undefined>): string[];
44
48
  /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
45
49
  export declare function discover(roots: readonly string[]): Map<string, Session>;
46
50
  export interface ClassifyOptions {
@@ -7,7 +7,8 @@
7
7
  * priced set (index.ts pricedSessions: every session is counted). A headless
8
8
  * `claude -p` session (entrypoint sdk-cli) is marked: the env of the script that
9
9
  * launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
10
- * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
10
+ * - Sessions are discovered in ~/.claude/projects, every ~/.claude-*\/projects, and
11
+ * $CLAUDE_CONFIG_DIR/projects wherever it points (a Console login often lives there).
11
12
  * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
12
13
  * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
13
14
  * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
@@ -46,8 +47,11 @@ function list(dir) {
46
47
  return [];
47
48
  }
48
49
  }
49
- /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
50
- export function defaultProjectRoots(home = homedir()) {
50
+ /**
51
+ * ~/.claude/projects, ~/.claude-*\/projects and $CLAUDE_CONFIG_DIR/projects that exist
52
+ * (realpath, de-duplicated). CLAUDE_CONFIG_DIR can point anywhere, not only ~/.claude-*.
53
+ */
54
+ export function defaultProjectRoots(home = homedir(), env = process.env) {
51
55
  const out = [];
52
56
  const add = (p) => {
53
57
  const rp = realOrNull(p);
@@ -59,6 +63,9 @@ export function defaultProjectRoots(home = homedir()) {
59
63
  if (d.startsWith(".claude-"))
60
64
  add(join(home, d, "projects"));
61
65
  }
66
+ const configDir = (env.CLAUDE_CONFIG_DIR ?? "").trim();
67
+ if (configDir)
68
+ add(join(configDir, "projects"));
62
69
  return out;
63
70
  }
64
71
  /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.5-next.0",
3
+ "version": "0.17.5",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +0,0 @@
1
- /**
2
- * Entry for the `mnemom agent` status-bar counter (lib/statusline.ts). Claude Code runs it
3
- * on every status update, so it imports only the counter — not the whole CLI.
4
- */
5
- export {};
@@ -1,16 +0,0 @@
1
- /**
2
- * Entry for the `mnemom agent` status-bar counter (lib/statusline.ts). Claude Code runs it
3
- * on every status update, so it imports only the counter — not the whole CLI.
4
- */
5
- import { homedir } from "node:os";
6
- import { join } from "node:path";
7
- import { INNER_ENV, render } from "./statusline.js";
8
- const chunks = [];
9
- process.stdin.on("data", (c) => chunks.push(c));
10
- process.stdin.on("end", () => {
11
- const line = render(Buffer.concat(chunks).toString("utf8"), {
12
- stateDir: join(homedir(), ".mnemom", "statusline"),
13
- inner: process.env[INNER_ENV] || undefined,
14
- });
15
- process.stdout.write(`${line}\n`);
16
- });
@@ -1,86 +0,0 @@
1
- /**
2
- * The `mnemom agent` status-bar savings counter: a Claude Code statusLine command that
3
- * prints "mnemom saved ~$X this session".
4
- *
5
- * The number is the first-run look's estimator (first-run/backtest.ts), applied live to this
6
- * session's own transcript: each request priced at API list prices, times the empirical
7
- * curve's lower-bound saving rate for its original context. So the counter and the
8
- * first-run screen always agree, and it rounds DOWN to the cent.
9
- *
10
- * Claude Code runs the command on every status update with its status JSON on stdin
11
- * (session_id, transcript_path, …). It must be fast, so the transcript is read
12
- * incrementally: per session, a state file (~/.mnemom/statusline/<session>.json, 0600)
13
- * keeps each file's byte offset and each message id's contribution, and a tick reads at
14
- * most MAX_BYTES_PER_TICK of new bytes (a big resumed session catches up over ticks).
15
- *
16
- * A user's own statusLine is never replaced: the launch passes it in
17
- * MNEMOM_STATUSLINE_INNER, it runs with the same stdin, and our segment is appended.
18
- * Read-only on every transcript; no network. It lives outside lib/first-run because it runs
19
- * the user's own statusLine command, and the first-run code is held to spawning nothing.
20
- */
21
- export declare const STATE_DIR = "statusline";
22
- export declare const MAX_BYTES_PER_TICK: number;
23
- /** The launcher passes the user's own statusLine command here. */
24
- export declare const INNER_ENV = "MNEMOM_STATUSLINE_INNER";
25
- /** Set to 1 to launch without the counter. */
26
- export declare const NO_STATUSLINE_ENV = "MNEMOM_AGENT_NO_STATUSLINE";
27
- export declare const INNER_TIMEOUT_MS = 2000;
28
- export interface SessionState {
29
- v: 1;
30
- /** Byte offset read so far, per transcript file. */
31
- files: Record<string, number>;
32
- /** Per message id: [list-price cost, saving]. A re-written id replaces its old share. */
33
- ids: Record<string, [number, number]>;
34
- actual: number;
35
- saved: number;
36
- }
37
- export declare function freshState(): SessionState;
38
- /** The main transcript, then its subagent transcripts (sorted). */
39
- export declare function sessionFiles(transcriptPath: string): string[];
40
- /**
41
- * The complete lines of `path` from `offset`, at most `maxBytes` of them, and the offset
42
- * after the last complete line. null: the file is unreadable or shorter than `offset`.
43
- */
44
- export declare function readNewLines(path: string, offset: number, maxBytes: number): {
45
- lines: Buffer[];
46
- next: number;
47
- } | null;
48
- /** Read what is new in the session's files into `st`. A file that shrank resets the count. */
49
- export declare function update(st: SessionState, files: string[], maxBytes?: number): SessionState;
50
- /** The segment, rounded DOWN to the cent and never below $0.00. */
51
- export declare function segment(saved: number): string;
52
- export declare function loadState(path: string): SessionState;
53
- export declare function saveState(path: string, st: SessionState): void;
54
- /** Run the user's own statusLine command with the same stdin; its first line, or "". */
55
- export declare function runInner(command: string, input: string): string;
56
- export interface RenderDeps {
57
- stateDir: string;
58
- inner?: string;
59
- runInner?: (command: string, input: string) => string;
60
- }
61
- /** Claude Code's status JSON → the one status line. Never throws. */
62
- export declare function render(stdin: string, deps: RenderDeps): string;
63
- export interface UserStatusLine {
64
- command: string;
65
- padding?: number;
66
- }
67
- /**
68
- * The statusLine Claude Code would use without us: the first settings file (highest
69
- * precedence first) that sets one. Only a `command` statusLine can be wrapped.
70
- */
71
- export declare function userStatusLine(cwd: string, configDir: string): UserStatusLine | null;
72
- /**
73
- * Launch args + env that add the counter, or null when it should stay off: the opt-out
74
- * env, or the user passing their own --settings (two would fight over precedence).
75
- */
76
- export declare function statuslineLaunch(o: {
77
- passthrough: readonly string[];
78
- env: NodeJS.ProcessEnv;
79
- cwd: string;
80
- home?: string;
81
- node: string;
82
- bin: string;
83
- }): {
84
- args: string[];
85
- env: Record<string, string>;
86
- } | null;
@@ -1,240 +0,0 @@
1
- /**
2
- * The `mnemom agent` status-bar savings counter: a Claude Code statusLine command that
3
- * prints "mnemom saved ~$X this session".
4
- *
5
- * The number is the first-run look's estimator (first-run/backtest.ts), applied live to this
6
- * session's own transcript: each request priced at API list prices, times the empirical
7
- * curve's lower-bound saving rate for its original context. So the counter and the
8
- * first-run screen always agree, and it rounds DOWN to the cent.
9
- *
10
- * Claude Code runs the command on every status update with its status JSON on stdin
11
- * (session_id, transcript_path, …). It must be fast, so the transcript is read
12
- * incrementally: per session, a state file (~/.mnemom/statusline/<session>.json, 0600)
13
- * keeps each file's byte offset and each message id's contribution, and a tick reads at
14
- * most MAX_BYTES_PER_TICK of new bytes (a big resumed session catches up over ticks).
15
- *
16
- * A user's own statusLine is never replaced: the launch passes it in
17
- * MNEMOM_STATUSLINE_INNER, it runs with the same stdin, and our segment is appended.
18
- * Read-only on every transcript; no network. It lives outside lib/first-run because it runs
19
- * the user's own statusLine command, and the first-run code is held to spawning nothing.
20
- */
21
- import { execSync } from "node:child_process";
22
- import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
23
- import { homedir } from "node:os";
24
- import { join } from "node:path";
25
- import { origContext, parseUsageLine, rowCost, ruleRate } from "./first-run/backtest.js";
26
- import { parseJson } from "./first-run/lines.js";
27
- export const STATE_DIR = "statusline";
28
- export const MAX_BYTES_PER_TICK = 8 * 1024 * 1024;
29
- /** The launcher passes the user's own statusLine command here. */
30
- export const INNER_ENV = "MNEMOM_STATUSLINE_INNER";
31
- /** Set to 1 to launch without the counter. */
32
- export const NO_STATUSLINE_ENV = "MNEMOM_AGENT_NO_STATUSLINE";
33
- export const INNER_TIMEOUT_MS = 2000;
34
- export function freshState() {
35
- return { v: 1, files: {}, ids: {}, actual: 0, saved: 0 };
36
- }
37
- /** The main transcript, then its subagent transcripts (sorted). */
38
- export function sessionFiles(transcriptPath) {
39
- const dir = join(transcriptPath.replace(/\.jsonl$/, ""), "subagents");
40
- let subs = [];
41
- try {
42
- subs = readdirSync(dir)
43
- .filter((f) => f.endsWith(".jsonl"))
44
- .sort()
45
- .map((f) => join(dir, f));
46
- }
47
- catch {
48
- /* no subagents */
49
- }
50
- return [transcriptPath, ...subs];
51
- }
52
- /**
53
- * The complete lines of `path` from `offset`, at most `maxBytes` of them, and the offset
54
- * after the last complete line. null: the file is unreadable or shorter than `offset`.
55
- */
56
- export function readNewLines(path, offset, maxBytes) {
57
- let fd;
58
- try {
59
- fd = openSync(path, "r");
60
- }
61
- catch {
62
- return null;
63
- }
64
- try {
65
- const size = fstatSync(fd).size;
66
- if (size < offset)
67
- return null;
68
- const n = Math.min(size - offset, maxBytes);
69
- if (n <= 0)
70
- return { lines: [], next: offset };
71
- const buf = Buffer.allocUnsafe(n);
72
- const got = readSync(fd, buf, 0, n, offset);
73
- const view = buf.subarray(0, got);
74
- const last = view.lastIndexOf(0x0a);
75
- // One line longer than the whole budget: skip past it rather than stall forever.
76
- if (last === -1)
77
- return { lines: [], next: got === maxBytes ? offset + got : offset };
78
- const lines = [];
79
- let start = 0;
80
- for (let nl = view.indexOf(0x0a); nl !== -1 && nl <= last; nl = view.indexOf(0x0a, start)) {
81
- lines.push(view.subarray(start, nl));
82
- start = nl + 1;
83
- }
84
- return { lines, next: offset + last + 1 };
85
- }
86
- finally {
87
- closeSync(fd);
88
- }
89
- }
90
- /** Read what is new in the session's files into `st`. A file that shrank resets the count. */
91
- export function update(st, files, maxBytes = MAX_BYTES_PER_TICK) {
92
- let budget = maxBytes;
93
- for (const f of files) {
94
- if (budget <= 0)
95
- break;
96
- const from = st.files[f] ?? 0;
97
- const r = readNewLines(f, from, budget);
98
- if (r === null) {
99
- if (from > 0)
100
- return update(freshState(), files, maxBytes);
101
- continue;
102
- }
103
- budget -= r.next - from;
104
- const subagent = f.includes("/subagents/");
105
- r.lines.forEach((line, i) => {
106
- const p = parseUsageLine(line, subagent);
107
- if (!p)
108
- return;
109
- const key = p.id ?? `anon-${f}-${from}-${i}`;
110
- const cost = rowCost(p.row);
111
- const saved = ruleRate(origContext(p.row)).screen * cost;
112
- const prev = st.ids[key];
113
- if (prev) {
114
- st.actual -= prev[0];
115
- st.saved -= prev[1];
116
- }
117
- st.ids[key] = [cost, saved];
118
- st.actual += cost;
119
- st.saved += saved;
120
- });
121
- st.files[f] = r.next;
122
- }
123
- return st;
124
- }
125
- /** The segment, rounded DOWN to the cent and never below $0.00. */
126
- export function segment(saved) {
127
- const cents = Math.max(Math.floor(saved * 100 + 1e-9), 0);
128
- return `mnemom saved ~$${(cents / 100).toFixed(2)} this session`;
129
- }
130
- function statePath(stateDir, sessionId) {
131
- return join(stateDir, `${sessionId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
132
- }
133
- export function loadState(path) {
134
- try {
135
- const v = parseJson(readFileSync(path, "utf8"));
136
- if (v && v.v === 1 && v.files && v.ids)
137
- return v;
138
- }
139
- catch {
140
- /* none yet */
141
- }
142
- return freshState();
143
- }
144
- export function saveState(path, st) {
145
- mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 });
146
- const tmp = `${path}.${process.pid}.tmp`;
147
- writeFileSync(tmp, JSON.stringify(st), { mode: 0o600 });
148
- renameSync(tmp, path);
149
- }
150
- /** Run the user's own statusLine command with the same stdin; its first line, or "". */
151
- export function runInner(command, input) {
152
- try {
153
- const out = execSync(command, {
154
- input,
155
- encoding: "utf8",
156
- timeout: INNER_TIMEOUT_MS,
157
- stdio: ["pipe", "pipe", "ignore"],
158
- });
159
- return (out.split("\n")[0] ?? "").trimEnd();
160
- }
161
- catch {
162
- return "";
163
- }
164
- }
165
- /** Claude Code's status JSON → the one status line. Never throws. */
166
- export function render(stdin, deps) {
167
- const innerOut = deps.inner ? (deps.runInner ?? runInner)(deps.inner, stdin) : "";
168
- let seg = "mnemom";
169
- try {
170
- const d = parseJson(stdin);
171
- const tp = typeof d?.transcript_path === "string" ? d.transcript_path : null;
172
- const sid = typeof d?.session_id === "string" && d.session_id ? d.session_id : null;
173
- if (tp && sid) {
174
- const path = statePath(deps.stateDir, sid);
175
- const st = update(loadState(path), sessionFiles(tp));
176
- seg = segment(st.saved);
177
- try {
178
- saveState(path, st);
179
- }
180
- catch {
181
- /* an unwritable state dir only means the next tick re-reads */
182
- }
183
- }
184
- }
185
- catch {
186
- /* the counter never breaks the status bar */
187
- }
188
- return innerOut ? `${innerOut} · ${seg}` : seg;
189
- }
190
- /**
191
- * The statusLine Claude Code would use without us: the first settings file (highest
192
- * precedence first) that sets one. Only a `command` statusLine can be wrapped.
193
- */
194
- export function userStatusLine(cwd, configDir) {
195
- for (const p of [
196
- join(cwd, ".claude", "settings.local.json"),
197
- join(cwd, ".claude", "settings.json"),
198
- join(configDir, "settings.json"),
199
- ]) {
200
- let d;
201
- try {
202
- d = parseJson(readFileSync(p, "utf8"));
203
- }
204
- catch {
205
- continue;
206
- }
207
- const sl = d?.statusLine;
208
- if (!sl || typeof sl !== "object")
209
- continue;
210
- if (sl.type !== "command" || typeof sl.command !== "string" || !sl.command)
211
- return null;
212
- return typeof sl.padding === "number"
213
- ? { command: sl.command, padding: sl.padding }
214
- : { command: sl.command };
215
- }
216
- return null;
217
- }
218
- const sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
219
- /**
220
- * Launch args + env that add the counter, or null when it should stay off: the opt-out
221
- * env, or the user passing their own --settings (two would fight over precedence).
222
- */
223
- export function statuslineLaunch(o) {
224
- if (o.env[NO_STATUSLINE_ENV] === "1")
225
- return null;
226
- if (o.passthrough.some((a) => a === "--settings" || a.startsWith("--settings=")))
227
- return null;
228
- const configDir = o.env.CLAUDE_CONFIG_DIR || join(o.home ?? homedir(), ".claude");
229
- const user = userStatusLine(o.cwd, configDir);
230
- const statusLine = {
231
- type: "command",
232
- command: `${sq(o.node)} ${sq(o.bin)}`,
233
- };
234
- if (user?.padding !== undefined)
235
- statusLine.padding = user.padding;
236
- return {
237
- args: ["--settings", JSON.stringify({ statusLine })],
238
- env: user ? { [INNER_ENV]: user.command } : {},
239
- };
240
- }