@indigoai-us/hq-cli 5.345.91 → 5.345.93

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +17 -4
  3. package/dist/command-catalog.generated.d.ts +252 -0
  4. package/dist/command-catalog.generated.js +306 -0
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/capacity.d.ts +71 -0
  8. package/dist/commands/capacity.js +666 -0
  9. package/dist/commands/integrations-connect.js +64 -5
  10. package/dist/commands/lanes-accounts.d.ts +1 -0
  11. package/dist/commands/lanes-accounts.js +6 -0
  12. package/dist/lib/capacity/accounts.d.ts +36 -0
  13. package/dist/lib/capacity/accounts.js +90 -0
  14. package/dist/lib/capacity/collect.d.ts +33 -0
  15. package/dist/lib/capacity/collect.js +164 -0
  16. package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
  17. package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
  18. package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
  19. package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
  20. package/dist/lib/capacity/collectors/codex.d.ts +120 -0
  21. package/dist/lib/capacity/collectors/codex.js +1134 -0
  22. package/dist/lib/capacity/collectors/grok.d.ts +86 -0
  23. package/dist/lib/capacity/collectors/grok.js +588 -0
  24. package/dist/lib/capacity/lane-login.d.ts +29 -0
  25. package/dist/lib/capacity/lane-login.js +91 -0
  26. package/dist/lib/capacity/ledger.d.ts +399 -0
  27. package/dist/lib/capacity/ledger.js +1441 -0
  28. package/dist/lib/capacity/project.d.ts +12 -0
  29. package/dist/lib/capacity/project.js +264 -0
  30. package/dist/lib/capacity/read.d.ts +62 -0
  31. package/dist/lib/capacity/read.js +264 -0
  32. package/dist/lib/capacity/refusal.d.ts +30 -0
  33. package/dist/lib/capacity/refusal.js +183 -0
  34. package/dist/lib/capacity/scheduler.d.ts +76 -0
  35. package/dist/lib/capacity/scheduler.js +800 -0
  36. package/dist/lib/capacity/sign-in.d.ts +103 -0
  37. package/dist/lib/capacity/sign-in.js +517 -0
  38. package/dist/lib/capacity/widget.d.ts +22 -0
  39. package/dist/lib/capacity/widget.js +418 -0
  40. package/dist/lib/lanes/spawn.js +12 -0
  41. package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
  42. package/dist/lib/mesh/live/daemon/run.js +29 -1
  43. package/dist/lib/search-index/background.d.ts +31 -8
  44. package/dist/lib/search-index/background.js +86 -7
  45. package/dist/lib/search-index/index.d.ts +24 -0
  46. package/dist/lib/search-index/index.js +25 -0
  47. package/package.json +1 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Claude Code statusline collector (US-004).
3
+ *
4
+ * The statusline hot path is a shell append to
5
+ * `<ledgerRoot>/spool/claude-statusline.jsonl`. This module parses those
6
+ * lines and the same JSON from `hq capacity ingest claude-statusline`.
7
+ * It does not start a daemon, spawn a process, or call a provider.
8
+ *
9
+ * observedAt is when the line was written (spooledAt), not when the
10
+ * daemon woke up. A missing rate_limits object is the normal pre-response
11
+ * payload and is not a parser_failure. A malformed rate_limits object is
12
+ * a parser_failure once per Claude Code version.
13
+ */
14
+ import { type LedgerOptions, type NowView, type ParserFailureEvent, type ReadingInput } from "../ledger.js";
15
+ export declare const CLAUDE_PARSER_VERSION = "claude-statusline.1";
16
+ export declare const CLAUDE_PROVIDER = "claude";
17
+ export declare const CLAUDE_STATUSLINE_SOURCE: "statusline";
18
+ export declare const CLAUDE_SPOOL_DIR = "spool";
19
+ export declare const CLAUDE_SPOOL_FILE = "claude-statusline.jsonl";
20
+ export declare const STATUSLINE_SPOOL_SCRIPT = "statusline-spool.sh";
21
+ export declare const STATUSLINE_LINES_TO_ADD: readonly ["f=\"${HQ_CAPACITY_ROOT:-$HOME/.hq/capacity}/statusline-spool.sh\"", "[ -r \"$f\" ] && . \"$f\"", "command -v hq_capacity_statusline_spool >/dev/null 2>&1 && hq_capacity_statusline_spool \"$input\" || true"];
22
+ export declare function statuslineSpoolScriptPath(root: string): string;
23
+ export declare function installStatuslineSpoolScript(root: string): void;
24
+ export interface StatuslineParseResult {
25
+ readings: ReadingInput[];
26
+ failures: ParserFailureEvent[];
27
+ /** Session model display name or id. Not stored on the reading. */
28
+ model: string | null;
29
+ }
30
+ export interface StatuslineIngestResult extends StatuslineParseResult {
31
+ /** Set when stdin is empty or not JSON. Nothing was written. */
32
+ error?: string;
33
+ view: NowView | null;
34
+ }
35
+ export interface SpoolConsumeResult extends StatuslineParseResult {
36
+ consumedLines: number;
37
+ view: NowView | null;
38
+ }
39
+ export declare function claudeStatuslineSpoolPath(root: string): string;
40
+ export declare function hashClaudeAccountId(raw: string): string;
41
+ export declare function parseStatuslinePayload(payload: unknown, options: {
42
+ observedAt: string;
43
+ cliVersion?: string;
44
+ accountId?: string;
45
+ readingSalt?: string;
46
+ }): StatuslineParseResult;
47
+ /** Parse one spool line. Blank lines return null. */
48
+ export declare function parseClaudeStatuslineSpoolLine(line: string, options?: {
49
+ now?: Date;
50
+ accountId?: string;
51
+ readingSalt?: string;
52
+ }): StatuslineParseResult | null;
53
+ export declare function ingestClaudeStatuslinePayload(text: string, options?: LedgerOptions): StatuslineIngestResult;
54
+ /**
55
+ * Read the spool, append complete lines once, and drop only the bytes
56
+ * that were consumed. Rename-then-read: a shell `>>` that opens after the
57
+ * rename creates a new spool file and is left for the next tick.
58
+ */
59
+ export declare function consumeClaudeStatuslineSpool(options: {
60
+ root: string;
61
+ now?: Date;
62
+ afterClaim?: (paths: {
63
+ live: string;
64
+ reading: string;
65
+ }) => void;
66
+ /** When set, a spooled configDir selects the account label and account id. */
67
+ accounts?: readonly {
68
+ label: string;
69
+ home: string;
70
+ }[];
71
+ }): SpoolConsumeResult;
72
+ //# sourceMappingURL=claude-statusline.d.ts.map
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Claude Code statusline collector (US-004).
3
+ *
4
+ * The statusline hot path is a shell append to
5
+ * `<ledgerRoot>/spool/claude-statusline.jsonl`. This module parses those
6
+ * lines and the same JSON from `hq capacity ingest claude-statusline`.
7
+ * It does not start a daemon, spawn a process, or call a provider.
8
+ *
9
+ * observedAt is when the line was written (spooledAt), not when the
10
+ * daemon woke up. A missing rate_limits object is the normal pre-response
11
+ * payload and is not a parser_failure. A malformed rate_limits object is
12
+ * a parser_failure once per Claude Code version.
13
+ */
14
+ import * as crypto from "node:crypto";
15
+ import * as fs from "node:fs";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+ import { appendReadings, LedgerMissing, readLedgerEvents, } from "../ledger.js";
19
+ export const CLAUDE_PARSER_VERSION = "claude-statusline.1";
20
+ export const CLAUDE_PROVIDER = "claude";
21
+ export const CLAUDE_STATUSLINE_SOURCE = "statusline";
22
+ export const CLAUDE_SPOOL_DIR = "spool";
23
+ export const CLAUDE_SPOOL_FILE = "claude-statusline.jsonl";
24
+ export const STATUSLINE_SPOOL_SCRIPT = "statusline-spool.sh";
25
+ export const STATUSLINE_LINES_TO_ADD = [
26
+ 'f="${HQ_CAPACITY_ROOT:-$HOME/.hq/capacity}/statusline-spool.sh"',
27
+ '[ -r "$f" ] && . "$f"',
28
+ 'command -v hq_capacity_statusline_spool >/dev/null 2>&1 && hq_capacity_statusline_spool "$input" || true',
29
+ ];
30
+ const STATUSLINE_SCRIPT_BODY = "#!/bin/sh\n# Claude Code statusline spool. Hot path: one append, no stdout, no stderr.\n# A missing spool directory returns 0. Never calls hq, node, or jq.\n#\n# hq capacity enable installs this file at\n# ${HQ_CAPACITY_ROOT:-$HOME/.hq/capacity}/statusline-spool.sh\n# The status line sources that path and no-ops when it is missing:\n# f=\"${HQ_CAPACITY_ROOT:-$HOME/.hq/capacity}/statusline-spool.sh\"\n# [ -r \"$f\" ] && . \"$f\"\n# command -v hq_capacity_statusline_spool >/dev/null 2>&1 && hq_capacity_statusline_spool \"$input\" || true\n#\n# The status line runs this several times a minute, so the bash path forks at\n# most once per call (date, on bash before 5) and checks the spool size on\n# about one call in 32. Variables are local so nothing leaks into the\n# status-line script that sources this file. Other shells take the POSIX path\n# in a subshell, which is slower but leaks nothing either.\n\n# Spool cap. The daemon drains the spool; this bounds it while the daemon is down.\nHQ_CAPACITY_SPOOL_MAX_BYTES=1048576\n\nif [ -n \"${BASH_VERSION-}\" ]; then\n hq_capacity_statusline_spool() {\n [ -n \"${1-}\" ] || return 0\n local dir=\"${HQ_CAPACITY_ROOT:-${HOME:-}/.hq/capacity}/spool\"\n [ -d \"$dir\" ] || return 0\n local file=\"${dir}/claude-statusline.jsonl\"\n if [ $((RANDOM % 32)) -eq 0 ] && [ -f \"$file\" ]; then\n local size\n size=$(wc -c < \"$file\" 2>/dev/null) || size=0\n [ \"${size// /}\" -lt \"$HQ_CAPACITY_SPOOL_MAX_BYTES\" ] 2>/dev/null || return 0\n fi\n # Control characters (newlines included) would split or forge spool lines.\n local payload=\"${1//[$'\\001'-$'\\037']/}\"\n [ -n \"$payload\" ] || return 0\n local now_s=\"${EPOCHSECONDS-}\"\n if [ -z \"$now_s\" ]; then\n now_s=$(date +%s 2>/dev/null) || return 0\n fi\n local config_field=\"\"\n if [ -n \"${CLAUDE_CONFIG_DIR-}\" ]; then\n local config_dir=\"$CLAUDE_CONFIG_DIR\"\n # A config dir with control characters is dropped rather than repaired.\n if [ \"${config_dir//[$'\\001'-$'\\037']/}\" = \"$config_dir\" ]; then\n case $config_dir in\n /*) ;;\n *) config_dir=\"${PWD}/${config_dir}\" ;;\n esac\n config_dir=${config_dir//\\\\/\\\\\\\\}\n config_dir=${config_dir//\\\"/\\\\\\\"}\n config_field=\",\\\"configDir\\\":\\\"${config_dir}\\\"\"\n fi\n fi\n # The format string is fixed so payload metacharacters stay literal.\n printf '%s\\n' \"{\\\"spooledAt\\\":$((now_s * 1000))${config_field},\\\"payload\\\":${payload}}\" 2>/dev/null >> \"$file\" || return 0\n }\nelse\n hq_capacity_statusline_spool() (\n [ -n \"${1-}\" ] || exit 0\n dir=\"${HQ_CAPACITY_ROOT:-${HOME:-}/.hq/capacity}/spool\"\n [ -d \"$dir\" ] || exit 0\n file=\"${dir}/claude-statusline.jsonl\"\n if [ -f \"$file\" ]; then\n size=$(wc -c < \"$file\" 2>/dev/null) || size=0\n size=$(printf '%s' \"$size\" | tr -d ' ')\n [ \"$size\" -lt \"$HQ_CAPACITY_SPOOL_MAX_BYTES\" ] 2>/dev/null || exit 0\n fi\n payload=$(printf '%s' \"$1\" | tr -d '\\000-\\037')\n [ -n \"$payload\" ] || exit 0\n now_s=$(date +%s 2>/dev/null) || exit 0\n config_field=\"\"\n if [ -n \"${CLAUDE_CONFIG_DIR-}\" ]; then\n config_dir=$CLAUDE_CONFIG_DIR\n cleaned=$(printf '%s' \"$config_dir\" | tr -d '\\000-\\037')\n if [ \"$cleaned\" = \"$config_dir\" ]; then\n case $config_dir in\n /*) ;;\n *) config_dir=\"$(pwd)/${config_dir}\" ;;\n esac\n escaped=$(printf '%s' \"$config_dir\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g')\n config_field=\",\\\"configDir\\\":\\\"${escaped}\\\"\"\n fi\n fi\n printf '%s\\n' \"{\\\"spooledAt\\\":$((now_s * 1000))${config_field},\\\"payload\\\":${payload}}\" 2>/dev/null >> \"$file\" || exit 0\n )\nfi\n\nif [ -n \"${BASH_VERSION-}\" ]; then\n if [ \"${BASH_SOURCE[0]-}\" = \"$0\" ]; then\n hq_capacity_statusline_spool \"$(cat)\"\n fi\nfi\n";
31
+ export function statuslineSpoolScriptPath(root) {
32
+ return path.join(root, STATUSLINE_SPOOL_SCRIPT);
33
+ }
34
+ export function installStatuslineSpoolScript(root) {
35
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
36
+ const dest = statuslineSpoolScriptPath(root);
37
+ fs.writeFileSync(dest, STATUSLINE_SCRIPT_BODY, { mode: 0o700 });
38
+ fs.chmodSync(dest, 0o700);
39
+ }
40
+ const LOCAL_ACCOUNT = "claude-local";
41
+ const WINDOWS = [
42
+ { key: "five_hour", limitId: "five_hour", kind: "session", windowMinutes: 5 * 60 },
43
+ { key: "seven_day", limitId: "seven_day", kind: "weekly_all", windowMinutes: 7 * 24 * 60 },
44
+ ];
45
+ export function claudeStatuslineSpoolPath(root) {
46
+ return path.join(root, CLAUDE_SPOOL_DIR, CLAUDE_SPOOL_FILE);
47
+ }
48
+ export function hashClaudeAccountId(raw) {
49
+ const digest = crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 16);
50
+ return `acct_${digest}`;
51
+ }
52
+ export function parseStatuslinePayload(payload, options) {
53
+ const accountId = hashClaudeAccountId(options.accountId ?? LOCAL_ACCOUNT);
54
+ const salt = options.readingSalt ?? options.observedAt;
55
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
56
+ return {
57
+ readings: [],
58
+ model: null,
59
+ failures: [failure(options.observedAt, options.cliVersion ?? "unknown", accountId, "claude statusline payload is not an object")],
60
+ };
61
+ }
62
+ const record = payload;
63
+ const cliVersion = readCliVersion(record, options.cliVersion);
64
+ const model = readModel(record);
65
+ if (!("rate_limits" in record) || record.rate_limits === null) {
66
+ return { readings: [], failures: [], model: model.name };
67
+ }
68
+ const limits = record.rate_limits;
69
+ if (!limits || typeof limits !== "object" || Array.isArray(limits)) {
70
+ return {
71
+ readings: [],
72
+ model: model.name,
73
+ failures: [failure(options.observedAt, cliVersion, accountId, "claude statusline rate_limits malformed")],
74
+ };
75
+ }
76
+ const readings = [];
77
+ let drifted = model.drifted;
78
+ const windowRecord = limits;
79
+ for (const spec of WINDOWS) {
80
+ if (!(spec.key in windowRecord) || windowRecord[spec.key] === null)
81
+ continue;
82
+ const window = windowRecord[spec.key];
83
+ const parsed = parseWindow(window);
84
+ if (!parsed) {
85
+ drifted = true;
86
+ continue;
87
+ }
88
+ readings.push({
89
+ readingId: makeReadingId([CLAUDE_STATUSLINE_SOURCE, salt, accountId, spec.limitId, options.observedAt, String(parsed.usedPercent)]),
90
+ observedAt: options.observedAt,
91
+ provider: CLAUDE_PROVIDER,
92
+ accountId,
93
+ source: CLAUDE_STATUSLINE_SOURCE,
94
+ cliVersion,
95
+ confidence: "observed",
96
+ parserVersion: CLAUDE_PARSER_VERSION,
97
+ limit: {
98
+ limitId: spec.limitId,
99
+ kind: spec.kind,
100
+ scope: null,
101
+ usedPercent: parsed.usedPercent,
102
+ windowMinutes: spec.windowMinutes,
103
+ resetsAt: parsed.resetsAt,
104
+ balance: null,
105
+ planType: null,
106
+ },
107
+ });
108
+ }
109
+ return {
110
+ readings,
111
+ model: model.name,
112
+ failures: drifted
113
+ ? [failure(options.observedAt, cliVersion, accountId, "claude statusline rate_limits malformed")]
114
+ : [],
115
+ };
116
+ }
117
+ /** Parse one spool line. Blank lines return null. */
118
+ export function parseClaudeStatuslineSpoolLine(line, options = {}) {
119
+ if (line.trim() === "")
120
+ return null;
121
+ const observedFallback = (options.now ?? new Date()).toISOString();
122
+ const accountId = hashClaudeAccountId(options.accountId ?? LOCAL_ACCOUNT);
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(line);
126
+ }
127
+ catch {
128
+ return {
129
+ readings: [],
130
+ model: null,
131
+ failures: [failure(observedFallback, "unknown", accountId, "claude statusline spool line is not JSON")],
132
+ };
133
+ }
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ return {
136
+ readings: [],
137
+ model: null,
138
+ failures: [failure(observedFallback, "unknown", accountId, "claude statusline spool line malformed")],
139
+ };
140
+ }
141
+ const record = parsed;
142
+ const spooledAt = record.spooledAt;
143
+ if (typeof spooledAt !== "number" || !Number.isFinite(spooledAt)) {
144
+ return {
145
+ readings: [],
146
+ model: null,
147
+ failures: [failure(observedFallback, "unknown", accountId, "claude statusline spool line malformed")],
148
+ };
149
+ }
150
+ const observedAt = new Date(spooledAt).toISOString();
151
+ if (Number.isNaN(Date.parse(observedAt))) {
152
+ return {
153
+ readings: [],
154
+ model: null,
155
+ failures: [failure(observedFallback, "unknown", accountId, "claude statusline spool line malformed")],
156
+ };
157
+ }
158
+ if (!("payload" in record)) {
159
+ return {
160
+ readings: [],
161
+ model: null,
162
+ failures: [failure(observedAt, "unknown", accountId, "claude statusline spool line malformed")],
163
+ };
164
+ }
165
+ return parseStatuslinePayload(record.payload, {
166
+ observedAt,
167
+ accountId: options.accountId,
168
+ readingSalt: options.readingSalt ?? `${spooledAt}`,
169
+ });
170
+ }
171
+ export function ingestClaudeStatuslinePayload(text, options) {
172
+ const observedAt = (options?.now ? options.now() : new Date()).toISOString();
173
+ const trimmed = text.trim();
174
+ if (!trimmed) {
175
+ return { readings: [], failures: [], model: null, view: null, error: "claude statusline payload is empty" };
176
+ }
177
+ let payload;
178
+ try {
179
+ payload = JSON.parse(trimmed);
180
+ }
181
+ catch {
182
+ return { readings: [], failures: [], model: null, view: null, error: "claude statusline payload is not JSON" };
183
+ }
184
+ const parsed = parseStatuslinePayload(payload, { observedAt, readingSalt: `stdin\0${observedAt}` });
185
+ const failures = takeNewFailures(parsed.failures, failureVersions(options));
186
+ const view = writeIfAny(parsed.readings, failures, options);
187
+ return { readings: parsed.readings, failures, model: parsed.model, view };
188
+ }
189
+ /**
190
+ * Read the spool, append complete lines once, and drop only the bytes
191
+ * that were consumed. Rename-then-read: a shell `>>` that opens after the
192
+ * rename creates a new spool file and is left for the next tick.
193
+ */
194
+ export function consumeClaudeStatuslineSpool(options) {
195
+ const live = claudeStatuslineSpoolPath(options.root);
196
+ const ledger = { root: options.root, ...(options.now ? { now: () => options.now } : {}) };
197
+ const claims = claimStatuslineSpool(live);
198
+ if (claims.length === 0) {
199
+ return { readings: [], failures: [], model: null, consumedLines: 0, view: null };
200
+ }
201
+ const reading = claims[claims.length - 1] ?? live;
202
+ options.afterClaim?.({ live, reading });
203
+ const chunks = [];
204
+ for (const claim of claims) {
205
+ if (fs.existsSync(claim))
206
+ chunks.push(readSettled(claim));
207
+ }
208
+ const { lines, remainder } = splitCompleteLines(joinChunks(chunks));
209
+ const readings = [];
210
+ const rawFailures = [];
211
+ let model = null;
212
+ for (let index = 0; index < lines.length; index += 1) {
213
+ const line = lines[index];
214
+ if (line === undefined)
215
+ continue;
216
+ const parsed = parseClaudeStatuslineSpoolLine(line, {
217
+ now: options.now,
218
+ readingSalt: `${index}`,
219
+ });
220
+ if (!parsed)
221
+ continue;
222
+ if (options.accounts)
223
+ stampStatuslineAccount(line, parsed, options.accounts);
224
+ if (parsed.model)
225
+ model = parsed.model;
226
+ readings.push(...parsed.readings);
227
+ rawFailures.push(...parsed.failures);
228
+ }
229
+ const failures = takeNewFailures(rawFailures, failureVersions(ledger));
230
+ // A throw here leaves the claimed spool file in place for the next tick.
231
+ const view = writeIfAny(readings, failures, ledger);
232
+ for (const claim of claims) {
233
+ if (claim === reading && remainder.length > 0) {
234
+ fs.writeFileSync(claim, remainder, { mode: 0o600 });
235
+ continue;
236
+ }
237
+ if (fs.existsSync(claim))
238
+ fs.unlinkSync(claim);
239
+ }
240
+ return { readings, failures, model, consumedLines: lines.length, view };
241
+ }
242
+ function claimStatuslineSpool(live) {
243
+ const claimed = [];
244
+ const dir = path.dirname(live);
245
+ const base = path.basename(live);
246
+ if (fs.existsSync(dir)) {
247
+ let names;
248
+ try {
249
+ names = fs.readdirSync(dir);
250
+ }
251
+ catch {
252
+ names = [];
253
+ }
254
+ for (const name of names) {
255
+ if (name.startsWith(`${base}.reading.`))
256
+ claimed.push(path.join(dir, name));
257
+ }
258
+ }
259
+ const mine = `${live}.reading.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString("hex")}`;
260
+ try {
261
+ fs.renameSync(live, mine);
262
+ claimed.push(mine);
263
+ }
264
+ catch (err) {
265
+ const code = err.code;
266
+ if (code !== "ENOENT")
267
+ throw err;
268
+ }
269
+ return claimed;
270
+ }
271
+ function stampStatuslineAccount(line, parsed, accounts) {
272
+ let configDir;
273
+ try {
274
+ const record = JSON.parse(line);
275
+ if (typeof record.configDir === "string" && record.configDir.trim())
276
+ configDir = record.configDir.trim();
277
+ }
278
+ catch {
279
+ configDir = undefined;
280
+ }
281
+ const match = configDir
282
+ ? accounts.filter((account) => path.resolve(account.home) === path.resolve(configDir))
283
+ : [];
284
+ const chosen = match.find((account) => account.label !== "default")
285
+ ?? match[0]
286
+ ?? accounts.find((account) => account.label === "default");
287
+ const label = chosen?.label ?? "default";
288
+ const home = chosen?.home ?? configDir;
289
+ const accountId = home ? statuslineAccountId(home) : undefined;
290
+ for (const reading of parsed.readings) {
291
+ reading.accountLabel = label;
292
+ reading.accountLabels = [label];
293
+ if (home)
294
+ reading.accountHome = home;
295
+ if (accountId)
296
+ reading.accountId = accountId;
297
+ }
298
+ for (const failure of parsed.failures) {
299
+ failure.accountLabel = label;
300
+ failure.accountLabels = [label];
301
+ if (home)
302
+ failure.accountHome = home;
303
+ if (accountId)
304
+ failure.accountId = accountId;
305
+ }
306
+ }
307
+ function statuslineAccountId(home) {
308
+ const resolved = path.resolve(home);
309
+ if (process.platform === "darwin") {
310
+ const defaultHome = path.resolve(path.join(os.homedir(), ".claude"));
311
+ const service = resolved === defaultHome
312
+ ? "Claude Code-credentials"
313
+ : `Claude Code-credentials-${crypto.createHash("sha256").update(resolved, "utf8").digest("hex").slice(0, 8)}`;
314
+ return hashClaudeAccountId(`keychain:${service}`);
315
+ }
316
+ return hashClaudeAccountId(`file:${resolved}`);
317
+ }
318
+ function writeIfAny(readings, failures, options) {
319
+ if (readings.length === 0 && failures.length === 0)
320
+ return null;
321
+ return appendReadings(readings, failures, null, options);
322
+ }
323
+ function failure(observedAt, cliVersion, accountId, error) {
324
+ return {
325
+ event: "parser_failure",
326
+ observedAt,
327
+ provider: CLAUDE_PROVIDER,
328
+ source: CLAUDE_STATUSLINE_SOURCE,
329
+ error,
330
+ accountId,
331
+ cliVersion,
332
+ parserVersion: CLAUDE_PARSER_VERSION,
333
+ };
334
+ }
335
+ function failureVersions(options) {
336
+ let events;
337
+ try {
338
+ events = readLedgerEvents(options);
339
+ }
340
+ catch (err) {
341
+ if (err instanceof LedgerMissing)
342
+ return new Set();
343
+ throw err;
344
+ }
345
+ const seen = new Set();
346
+ for (const event of events) {
347
+ if (!("event" in event) || event.event !== "parser_failure")
348
+ continue;
349
+ if (event.provider !== CLAUDE_PROVIDER || event.source !== CLAUDE_STATUSLINE_SOURCE)
350
+ continue;
351
+ seen.add(event.cliVersion ?? "unknown");
352
+ }
353
+ return seen;
354
+ }
355
+ function takeNewFailures(failures, seen) {
356
+ const kept = [];
357
+ for (const item of failures) {
358
+ const version = item.cliVersion ?? "unknown";
359
+ if (seen.has(version))
360
+ continue;
361
+ seen.add(version);
362
+ kept.push(item);
363
+ }
364
+ return kept;
365
+ }
366
+ function readCliVersion(record, fallback) {
367
+ const version = record.version;
368
+ if (typeof version === "string" && version.trim())
369
+ return version.trim();
370
+ if (fallback && fallback.trim())
371
+ return fallback.trim();
372
+ return "unknown";
373
+ }
374
+ function readModel(record) {
375
+ if (!("model" in record) || record.model === null)
376
+ return { name: null, drifted: false };
377
+ const model = record.model;
378
+ if (typeof model === "string") {
379
+ const name = model.trim();
380
+ return name ? { name, drifted: false } : { name: null, drifted: true };
381
+ }
382
+ if (typeof model === "object" && !Array.isArray(model)) {
383
+ const fields = model;
384
+ const display = typeof fields.display_name === "string" ? fields.display_name.trim() : "";
385
+ const id = typeof fields.id === "string" ? fields.id.trim() : "";
386
+ if (display || id)
387
+ return { name: display || id, drifted: false };
388
+ return { name: null, drifted: true };
389
+ }
390
+ return { name: null, drifted: true };
391
+ }
392
+ function parseWindow(value) {
393
+ if (!value || typeof value !== "object" || Array.isArray(value))
394
+ return null;
395
+ const record = value;
396
+ const used = record.used_percentage;
397
+ const resets = record.resets_at;
398
+ if (typeof used !== "number" || !Number.isFinite(used) || used < 0 || used > 100)
399
+ return null;
400
+ if (typeof resets !== "number" || !Number.isFinite(resets) || resets <= 0)
401
+ return null;
402
+ return { usedPercent: used, resetsAt: resets };
403
+ }
404
+ function makeReadingId(parts) {
405
+ const digest = crypto.createHash("sha256").update(parts.join("\0"), "utf8").digest("hex").slice(0, 20);
406
+ return `rdg_${digest}`;
407
+ }
408
+ function readSettled(file) {
409
+ const first = fs.readFileSync(file);
410
+ if (fs.statSync(file).size === first.length)
411
+ return first;
412
+ return fs.readFileSync(file);
413
+ }
414
+ function joinChunks(chunks) {
415
+ if (chunks.length === 0)
416
+ return Buffer.alloc(0);
417
+ const parts = [];
418
+ for (const chunk of chunks) {
419
+ if (chunk.length === 0)
420
+ continue;
421
+ const prev = parts[parts.length - 1];
422
+ if (prev && prev[prev.length - 1] !== 0x0a)
423
+ parts.push(Buffer.from("\n"));
424
+ parts.push(chunk);
425
+ }
426
+ return parts.length === 0 ? Buffer.alloc(0) : Buffer.concat(parts);
427
+ }
428
+ function splitCompleteLines(buf) {
429
+ const end = buf.lastIndexOf(0x0a);
430
+ if (end < 0)
431
+ return { lines: [], remainder: buf };
432
+ const text = buf.subarray(0, end).toString("utf8");
433
+ const lines = text.length === 0 ? [] : text.split("\n").filter((line) => line.length > 0);
434
+ return { lines, remainder: buf.subarray(end + 1) };
435
+ }
436
+ //# sourceMappingURL=claude-statusline.js.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Codex capacity collector for the resident daemon (US-008 calls these;
3
+ * this module does not start the daemon).
4
+ *
5
+ * The session JSONL tail is the primary feed: server-reported token_count
6
+ * rate limits, resumed by byte offset. The app-server read is a live
7
+ * account/rateLimits/read after initialize. That method does not refresh
8
+ * tokens. Starting the app-server process can still rewrite
9
+ * ~/.codex/auth.json for a managed ChatGPT login, so the read stays
10
+ * idle-gated. It never starts a thread, never sends a prompt, and never
11
+ * opens the Codex credential file.
12
+ */
13
+ import { type LedgerOptions, type NowView, type ParserFailureEvent, type RateLimitResetCredits, type ReadingInput } from "../ledger.js";
14
+ export declare const CODEX_PARSER_VERSION = "codex.1";
15
+ export declare const CODEX_IDLE_MS: number;
16
+ export declare const CODEX_APP_SERVER_TIMEOUT_MS = 20000;
17
+ export declare const CODEX_APP_SERVER_BACKOFF_MS: number;
18
+ export declare const CODEX_PROVIDER = "codex";
19
+ export interface CodexTailCursor {
20
+ offset: number;
21
+ ino: number;
22
+ dev: number;
23
+ /**
24
+ * Hash of the bytes just before `offset`. Linux reuses a freed inode
25
+ * number at once, so a file unlinked and recreated at the same path can
26
+ * match ino, dev, and size. A different mark means a different file.
27
+ * Cursors written before this field existed have no mark and are trusted.
28
+ */
29
+ mark?: string;
30
+ }
31
+ export interface CodexTailState {
32
+ files: Record<string, CodexTailCursor>;
33
+ }
34
+ export interface CodexProviderMeta {
35
+ observedAt: string;
36
+ source: "codex-app-server";
37
+ cliVersion: string;
38
+ accountId: string;
39
+ ordinaryUsageAllowed: boolean | null;
40
+ rateLimitResetCredits: RateLimitResetCredits | null;
41
+ }
42
+ export interface CodexCollectResult {
43
+ readings: ReadingInput[];
44
+ failures: ParserFailureEvent[];
45
+ providerMeta: CodexProviderMeta | null;
46
+ }
47
+ export interface CodexTailResult extends CodexCollectResult {
48
+ state: CodexTailState;
49
+ }
50
+ export interface CodexAppServerResult extends CodexCollectResult {
51
+ ok: boolean;
52
+ reason?: "spawn-failed" | "timeout" | "exited" | "rpc-error";
53
+ backoffMs?: number;
54
+ }
55
+ export interface CodexAppServerChild {
56
+ stdin: {
57
+ write(chunk: string): unknown;
58
+ end(): unknown;
59
+ on?(event: "error", listener: (error: unknown) => void): unknown;
60
+ };
61
+ stdout: {
62
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
63
+ };
64
+ kill(signal?: NodeJS.Signals): unknown;
65
+ once?(event: "exit" | "error", listener: (...args: unknown[]) => void): unknown;
66
+ pid?: number;
67
+ }
68
+ export type CodexAppServerSpawn = (command: string, args: readonly string[], env?: NodeJS.ProcessEnv) => CodexAppServerChild;
69
+ export interface ShouldReadAppServerInput {
70
+ lastSessionWriteAt: number | null;
71
+ codexProcessRunning: boolean;
72
+ now: number;
73
+ }
74
+ export declare function hashAccountId(raw: string): string;
75
+ export declare function hashCreditId(raw: string): string;
76
+ export declare function parseCodexCliVersion(text: string): string | null;
77
+ /** Idle at least 30 minutes and no codex process. A missing session write counts as idle. */
78
+ export declare function shouldReadAppServer(input: ShouldReadAppServerInput): boolean;
79
+ /** True when a process listing shows a `codex` executable, not a directory named codex. */
80
+ export declare function isCodexProcessRunning(psOutput: string): boolean;
81
+ export declare function latestCodexSessionWriteAt(sessionsDir: string, recentDays?: number): number | null;
82
+ export declare function defaultCodexSpawn(command: string, args: readonly string[], env?: NodeJS.ProcessEnv): CodexAppServerChild;
83
+ export declare function readInstalledCodexVersion(spawnImpl?: CodexAppServerSpawn): Promise<string>;
84
+ export declare function tailCodexSessions(options: {
85
+ sessionsDir: string;
86
+ state?: CodexTailState;
87
+ cliVersion: string;
88
+ now?: Date;
89
+ accountId?: string;
90
+ accountLabel?: string;
91
+ accountLabels?: string[];
92
+ accountHome?: string;
93
+ /** Only session day directories this recent are walked. */
94
+ recentDays?: number;
95
+ }): CodexTailResult;
96
+ /** Map an account/rateLimits/read result. Does not spawn. */
97
+ export declare function mapCodexAppServerBody(body: unknown, options: {
98
+ cliVersion: string;
99
+ now?: Date;
100
+ accountId?: string;
101
+ accountLabel?: string;
102
+ accountLabels?: string[];
103
+ accountHome?: string;
104
+ }): CodexCollectResult;
105
+ export declare function readCodexAppServer(options: {
106
+ spawn?: CodexAppServerSpawn;
107
+ cliVersion: string;
108
+ now?: Date;
109
+ timeoutMs?: number;
110
+ command?: string;
111
+ accountId?: string;
112
+ /** Sets CODEX_HOME for the app-server child. Omitted on the single-home path. */
113
+ home?: string;
114
+ accountLabel?: string;
115
+ accountLabels?: string[];
116
+ accountHome?: string;
117
+ signal?: AbortSignal;
118
+ }): Promise<CodexAppServerResult>;
119
+ export declare function ingestCodexResult(result: CodexCollectResult, options?: LedgerOptions): NowView | null;
120
+ //# sourceMappingURL=codex.d.ts.map