modelstat 0.0.45 → 0.0.47

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/dist/cli.mjs CHANGED
@@ -50,9 +50,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
50
50
  ));
51
51
 
52
52
  // ../../packages/parsers/src/types.ts
53
+ var PARSER_EVENT_CHUNK;
53
54
  var init_types = __esm({
54
55
  "../../packages/parsers/src/types.ts"() {
55
56
  "use strict";
57
+ PARSER_EVENT_CHUNK = 256;
56
58
  }
57
59
  });
58
60
 
@@ -76,7 +78,7 @@ var init_git = __esm({
76
78
  });
77
79
 
78
80
  // ../../packages/core/src/enums.ts
79
- var TOOLS, PROVIDERS, IDENTITY_OWNER_SCOPES, INSTALL_METHODS, OS_FAMILIES, EVENT_KINDS, COMPANION_PHASES, CLASSIFICATION_CONFIDENCE;
81
+ var TOOLS, PROVIDERS, IDENTITY_OWNER_SCOPES, INSTALL_METHODS, OS_FAMILIES, EVENT_KINDS, TOOL_CALL_STATUSES, COMPANION_PHASES, CLASSIFICATION_CONFIDENCE;
80
82
  var init_enums = __esm({
81
83
  "../../packages/core/src/enums.ts"() {
82
84
  "use strict";
@@ -160,6 +162,13 @@ var init_enums = __esm({
160
162
  "tool_result",
161
163
  "summary"
162
164
  ];
165
+ TOOL_CALL_STATUSES = [
166
+ "success",
167
+ "error",
168
+ "denied",
169
+ "timeout",
170
+ "unknown"
171
+ ];
163
172
  COMPANION_PHASES = [
164
173
  "starting",
165
174
  "discovering",
@@ -4302,7 +4311,7 @@ var init_zod = __esm({
4302
4311
  });
4303
4312
 
4304
4313
  // ../../packages/core/src/schemas.ts
4305
- var TokenUsage, GitContext, RawEvent, RedactionReport, TaxonomyHintRooted, Segment, IngestBatch, HeartbeatPayload, DeviceEnrollment, DeviceSelfRegister, DeviceClaimRequest, ProcessingMetadata, RedactionPolicy, DetectedInstallation, DetectedIdentity, DiscoveryReport, ClassificationConfidenceEnum;
4314
+ var TokenUsage, GitContext, RawEvent, RedactionReport, TaxonomyHintRooted, Segment, ToolCallWire, IngestBatch, HeartbeatPayload, DeviceEnrollment, DeviceSelfRegister, DeviceClaimRequest, ProcessingMetadata, RedactionPolicy, DetectedInstallation, DetectedIdentity, DiscoveryReport, ClassificationConfidenceEnum;
4306
4315
  var init_schemas = __esm({
4307
4316
  "../../packages/core/src/schemas.ts"() {
4308
4317
  "use strict";
@@ -4407,6 +4416,49 @@ var init_schemas = __esm({
4407
4416
  * Reserved for server-side similarity / clustering. */
4408
4417
  abstract_embedding: external_exports.array(external_exports.number()).length(384).optional()
4409
4418
  });
4419
+ ToolCallWire = external_exports.object({
4420
+ /** tool_use block `id` / codex `call_id`; parsers fall back to a
4421
+ * deterministic `tc_<djb2-base36>` of `${source_event_id}|${call_index}`
4422
+ * when the source line carries no id. */
4423
+ external_call_id: external_exports.string().max(120),
4424
+ /** Tool-local session id — same id space as RawEvent.session_id. */
4425
+ session_id: external_exports.string().max(120),
4426
+ /** The RawEvent that contained the tool_use (dedupe/replay anchor). */
4427
+ source_event_id: external_exports.string(),
4428
+ /** Segment containing source_event_id — filled by the companion at
4429
+ * batch-build time when known, else null. */
4430
+ segment_id: external_exports.string().max(64).nullable().default(null),
4431
+ /** The agent that made the call (TOOLS enum — "tool" in legacy naming). */
4432
+ agent: external_exports.enum(TOOLS),
4433
+ /** `builtin` or `mcp:<server>`. */
4434
+ server: external_exports.string().max(120),
4435
+ /** Bare tool name (`Bash`, `create_pr`) — normalised vendor identifier. */
4436
+ name: external_exports.string().max(120),
4437
+ turn_index: external_exports.number().int().nonnegative().nullable(),
4438
+ /** Ordinal of the call within its source event (0-based). */
4439
+ call_index: external_exports.number().int().nonnegative(),
4440
+ /** ts of the line carrying the tool_use. */
4441
+ started_at: external_exports.string().datetime({ offset: true }),
4442
+ /** ts of the line carrying the matching tool_result; null if unmatched. */
4443
+ ended_at: external_exports.string().datetime({ offset: true }).nullable(),
4444
+ status: external_exports.enum(TOOL_CALL_STATUSES),
4445
+ /** Hex sha256 of JSON.stringify(input); `""` when the call had no input. */
4446
+ args_hash: external_exports.string().max(64),
4447
+ /** Sha256 of the sorted top-level arg key names joined by `,`; the
4448
+ * literal `none` when input is not a non-empty object. */
4449
+ signature_hash: external_exports.string().max(64),
4450
+ /** UTF-8 byte length of JSON.stringify(input); 0 if none. */
4451
+ args_bytes: external_exports.number().int().nonnegative(),
4452
+ /** UTF-8 byte length of JSON.stringify(tool_result content); 0 if
4453
+ * unmatched/empty. */
4454
+ result_bytes: external_exports.number().int().nonnegative(),
4455
+ /** Model of the assistant message that issued the call. `<synthetic>`
4456
+ * kept verbatim per the PR #12 attribution rules. */
4457
+ model: external_exports.string().max(120).nullable(),
4458
+ /** ONLY for shell-ish tools: command verbs from the fixed allowlist
4459
+ * (@modelstat/parsers/shell-families). Never raw command text. */
4460
+ command_families: external_exports.array(external_exports.string().max(40)).max(3).default([])
4461
+ });
4410
4462
  IngestBatch = external_exports.object({
4411
4463
  batch_id: external_exports.string(),
4412
4464
  // ULID
@@ -4414,6 +4466,10 @@ var init_schemas = __esm({
4414
4466
  agent_version: external_exports.string().max(40),
4415
4467
  events: external_exports.array(RawEvent).max(1e4),
4416
4468
  segments: external_exports.array(Segment).max(2e3).default([]),
4469
+ /** Per-call tool invocations (additive — old agents omit it, old
4470
+ * servers ignore it). See ToolCallWire for the privacy contract:
4471
+ * hashes / byte sizes / allowlisted verbs only, never payloads. */
4472
+ tool_calls: external_exports.array(ToolCallWire).max(2e4).default([]),
4417
4473
  /** Optional per-session metadata hint: which installation produced them, etc. */
4418
4474
  session_installs: external_exports.record(
4419
4475
  external_exports.string(),
@@ -4682,22 +4738,222 @@ var init_src = __esm({
4682
4738
  }
4683
4739
  });
4684
4740
 
4685
- // ../../packages/parsers/src/claude-code/index.ts
4741
+ // ../../packages/parsers/src/shell-families/index.ts
4742
+ function extractCommandFamilies(command) {
4743
+ const families = [];
4744
+ for (const part of splitCommandParts(command)) {
4745
+ const verb = leadingVerb(part);
4746
+ if (!verb || !ALLOWLIST.has(verb)) continue;
4747
+ if (!families.includes(verb)) families.push(verb);
4748
+ if (families.length >= MAX_COMMAND_FAMILIES) break;
4749
+ }
4750
+ return families;
4751
+ }
4752
+ function splitCommandParts(command) {
4753
+ const parts = [];
4754
+ let current = "";
4755
+ let inSingle = false;
4756
+ let inDouble = false;
4757
+ for (let i = 0; i < command.length; i++) {
4758
+ const ch = command[i];
4759
+ if (!inSingle && ch === "\\") {
4760
+ current += ch + (command[i + 1] ?? "");
4761
+ i++;
4762
+ continue;
4763
+ }
4764
+ if (ch === "'" && !inDouble) {
4765
+ inSingle = !inSingle;
4766
+ current += ch;
4767
+ continue;
4768
+ }
4769
+ if (ch === '"' && !inSingle) {
4770
+ inDouble = !inDouble;
4771
+ current += ch;
4772
+ continue;
4773
+ }
4774
+ if (!inSingle && !inDouble) {
4775
+ if (ch === ";" || ch === "|" || ch === "\n") {
4776
+ parts.push(current);
4777
+ current = "";
4778
+ continue;
4779
+ }
4780
+ if (ch === "&" && command[i + 1] === "&") {
4781
+ parts.push(current);
4782
+ current = "";
4783
+ i++;
4784
+ continue;
4785
+ }
4786
+ }
4787
+ current += ch;
4788
+ }
4789
+ parts.push(current);
4790
+ return parts;
4791
+ }
4792
+ function leadingVerb(part) {
4793
+ const tokens = part.trim().split(/\s+/);
4794
+ let i = 0;
4795
+ while (i < tokens.length) {
4796
+ const tok = stripQuotes(tokens[i] ?? "");
4797
+ if (tok === "") {
4798
+ i++;
4799
+ continue;
4800
+ }
4801
+ if (VAR_ASSIGNMENT.test(tok)) {
4802
+ i++;
4803
+ continue;
4804
+ }
4805
+ if (WRAPPERS.has(tok)) {
4806
+ i++;
4807
+ while (i < tokens.length && (tokens[i] ?? "").startsWith("-")) i++;
4808
+ continue;
4809
+ }
4810
+ const base = tok.split("/").pop() ?? tok;
4811
+ return base === "" ? null : base;
4812
+ }
4813
+ return null;
4814
+ }
4815
+ function stripQuotes(token) {
4816
+ if (token.length >= 2) {
4817
+ const first = token[0];
4818
+ const last = token[token.length - 1];
4819
+ if (first === "'" && last === "'" || first === '"' && last === '"') {
4820
+ return token.slice(1, -1);
4821
+ }
4822
+ }
4823
+ return token;
4824
+ }
4825
+ var SHELL_FAMILY_ALLOWLIST, MAX_COMMAND_FAMILIES, ALLOWLIST, WRAPPERS, VAR_ASSIGNMENT;
4826
+ var init_shell_families = __esm({
4827
+ "../../packages/parsers/src/shell-families/index.ts"() {
4828
+ "use strict";
4829
+ SHELL_FAMILY_ALLOWLIST = [
4830
+ "git",
4831
+ "npm",
4832
+ "pnpm",
4833
+ "npx",
4834
+ "yarn",
4835
+ "node",
4836
+ "python",
4837
+ "python3",
4838
+ "pytest",
4839
+ "pip",
4840
+ "pip3",
4841
+ "cargo",
4842
+ "rustc",
4843
+ "go",
4844
+ "make",
4845
+ "cmake",
4846
+ "docker",
4847
+ "docker-compose",
4848
+ "kubectl",
4849
+ "helm",
4850
+ "terraform",
4851
+ "gh",
4852
+ "aws",
4853
+ "gcloud",
4854
+ "az",
4855
+ "curl",
4856
+ "wget",
4857
+ "rg",
4858
+ "grep",
4859
+ "find",
4860
+ "sed",
4861
+ "awk",
4862
+ "jq",
4863
+ "psql",
4864
+ "mysql",
4865
+ "redis-cli",
4866
+ "brew",
4867
+ "apt",
4868
+ "tsx",
4869
+ "vitest",
4870
+ "jest",
4871
+ "playwright",
4872
+ "ruby",
4873
+ "bundle",
4874
+ "mvn",
4875
+ "gradle",
4876
+ "ls",
4877
+ "cat"
4878
+ ];
4879
+ MAX_COMMAND_FAMILIES = 3;
4880
+ ALLOWLIST = new Set(SHELL_FAMILY_ALLOWLIST);
4881
+ WRAPPERS = /* @__PURE__ */ new Set(["sudo", "env", "time", "nice"]);
4882
+ VAR_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
4883
+ }
4884
+ });
4885
+
4886
+ // ../../packages/parsers/src/tool-hash/index.ts
4686
4887
  import { createHash } from "crypto";
4888
+ function hashArgs(input) {
4889
+ if (input === void 0 || input === null) {
4890
+ return { args_hash: "", signature_hash: SIGNATURE_NONE, args_bytes: 0 };
4891
+ }
4892
+ const json = JSON.stringify(input);
4893
+ if (json === void 0) {
4894
+ return { args_hash: "", signature_hash: SIGNATURE_NONE, args_bytes: 0 };
4895
+ }
4896
+ const argsHash = createHash("sha256").update(json).digest("hex");
4897
+ let signatureHash = SIGNATURE_NONE;
4898
+ if (typeof input === "object" && !Array.isArray(input)) {
4899
+ const keys = Object.keys(input).sort();
4900
+ if (keys.length > 0) {
4901
+ signatureHash = createHash("sha256").update(keys.join(",")).digest("hex");
4902
+ }
4903
+ }
4904
+ return {
4905
+ args_hash: argsHash,
4906
+ signature_hash: signatureHash,
4907
+ args_bytes: Buffer.byteLength(json, "utf8")
4908
+ };
4909
+ }
4910
+ function jsonBytes(value) {
4911
+ if (value === void 0 || value === null || value === "") return 0;
4912
+ const json = JSON.stringify(value);
4913
+ return json === void 0 ? 0 : Buffer.byteLength(json, "utf8");
4914
+ }
4915
+ function normalizeToolName(raw) {
4916
+ const cleaned = raw.normalize("NFC").trim().replace(UUID_RE, "<dyn>").replace(HEX_TAIL_RE, "<dyn>");
4917
+ return cleaned.slice(0, 120);
4918
+ }
4919
+ function splitObservedToolName(observed) {
4920
+ const m = /^mcp__([^_].*?)__(.+)$/.exec(observed.trim());
4921
+ if (m?.[1] && m[2]) {
4922
+ return {
4923
+ server: `mcp:${normalizeToolName(m[1]).slice(0, 116)}`,
4924
+ name: normalizeToolName(m[2])
4925
+ };
4926
+ }
4927
+ return { server: "builtin", name: normalizeToolName(observed) };
4928
+ }
4929
+ function toolIdentity(server, name) {
4930
+ return server === "builtin" ? name : `${server}/${name}`;
4931
+ }
4932
+ function fallbackCallId(sourceEventId2, callIndex) {
4933
+ const s = `${sourceEventId2}|${callIndex}`;
4934
+ let h = 5381n;
4935
+ for (let i = 0; i < s.length; i++) {
4936
+ h = h * 33n ^ BigInt(s.charCodeAt(i));
4937
+ h &= 0xffffffffffffffffn;
4938
+ }
4939
+ return `tc_${h.toString(36)}`;
4940
+ }
4941
+ var SIGNATURE_NONE, UUID_RE, HEX_TAIL_RE;
4942
+ var init_tool_hash = __esm({
4943
+ "../../packages/parsers/src/tool-hash/index.ts"() {
4944
+ "use strict";
4945
+ SIGNATURE_NONE = "none";
4946
+ UUID_RE = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
4947
+ HEX_TAIL_RE = /[0-9a-fA-F]{8,}(?=$|[^0-9a-zA-Z])/g;
4948
+ }
4949
+ });
4950
+
4951
+ // ../../packages/parsers/src/claude-code/index.ts
4952
+ import { createHash as createHash2 } from "crypto";
4687
4953
  import { createReadStream, existsSync as existsSync2, readdirSync } from "fs";
4688
4954
  import { stat } from "fs/promises";
4689
4955
  import { dirname as dirname2, join } from "path";
4690
4956
  import { createInterface } from "readline";
4691
- function countToolCalls(content) {
4692
- const counts = {};
4693
- if (!Array.isArray(content)) return counts;
4694
- for (const block of content) {
4695
- if (block && block.type === "tool_use" && typeof block.name === "string" && block.name) {
4696
- counts[block.name] = (counts[block.name] ?? 0) + 1;
4697
- }
4698
- }
4699
- return counts;
4700
- }
4701
4957
  function extractExcerpt(content) {
4702
4958
  if (!content) return void 0;
4703
4959
  let text = "";
@@ -4720,8 +4976,56 @@ function extractExcerpt(content) {
4720
4976
  const truncated = cleaned.slice(0, 320);
4721
4977
  return truncated.length > 0 ? truncated : void 0;
4722
4978
  }
4979
+ function commandFamiliesFor(server, name, input) {
4980
+ if (server !== "builtin" || !SHELL_TOOL_NAMES.has(name)) return [];
4981
+ if (typeof input !== "object" || input === null) return [];
4982
+ const command = input.command;
4983
+ return typeof command === "string" ? extractCommandFamilies(command) : [];
4984
+ }
4985
+ function buildToolCallDraft(opts) {
4986
+ const { server, name } = splitObservedToolName(opts.observedName);
4987
+ const hashes = hashArgs(opts.input);
4988
+ return {
4989
+ external_call_id: typeof opts.rawCallId === "string" && opts.rawCallId.trim() !== "" ? opts.rawCallId.trim().slice(0, 120) : fallbackCallId(opts.sourceEventId, opts.callIndex),
4990
+ session_id: opts.sessionId,
4991
+ source_event_id: opts.sourceEventId,
4992
+ agent: "claude_code",
4993
+ server,
4994
+ name,
4995
+ // This parser never derives a turn index (events above carry
4996
+ // turn_index: null too), so per-call records can't either.
4997
+ turn_index: null,
4998
+ call_index: opts.callIndex,
4999
+ started_at: opts.startedAt,
5000
+ ended_at: null,
5001
+ status: "unknown",
5002
+ args_hash: hashes.args_hash,
5003
+ signature_hash: hashes.signature_hash,
5004
+ args_bytes: hashes.args_bytes,
5005
+ result_bytes: 0,
5006
+ model: opts.model,
5007
+ command_families: commandFamiliesFor(server, name, opts.input)
5008
+ };
5009
+ }
4723
5010
  async function parseClaudeCodeJsonl(ctx) {
4724
5011
  const events = [];
5012
+ const toolCalls = [];
5013
+ const pendingByCallId = /* @__PURE__ */ new Map();
5014
+ let chunk = [];
5015
+ let emitted = 0;
5016
+ const emit = async (e) => {
5017
+ emitted += 1;
5018
+ if (!ctx.onEvents) {
5019
+ events.push(e);
5020
+ return;
5021
+ }
5022
+ chunk.push(e);
5023
+ if (chunk.length >= PARSER_EVENT_CHUNK) {
5024
+ const full = chunk;
5025
+ chunk = [];
5026
+ await ctx.onEvents(full);
5027
+ }
5028
+ };
4725
5029
  let rawLines = 0;
4726
5030
  let skipped = 0;
4727
5031
  let bytePos = 0;
@@ -4809,7 +5113,33 @@ async function parseClaudeCodeJsonl(ctx) {
4809
5113
  }
4810
5114
  const slug = guessRepoSlugFromPath(cwd);
4811
5115
  const excerpt = extractExcerpt(a.message?.content);
4812
- events.push({
5116
+ const aggregate = {};
5117
+ const blocks = Array.isArray(a.message?.content) ? a.message.content : [];
5118
+ let callIndex = 0;
5119
+ for (const block of blocks) {
5120
+ if (!block || block.type !== "tool_use") continue;
5121
+ const index = callIndex;
5122
+ callIndex += 1;
5123
+ const observed = typeof block.name === "string" ? block.name.trim() : "";
5124
+ if (!observed) continue;
5125
+ const draft = buildToolCallDraft({
5126
+ observedName: observed,
5127
+ rawCallId: block.id,
5128
+ input: block.input,
5129
+ sessionId,
5130
+ sourceEventId: eventId,
5131
+ callIndex: index,
5132
+ startedAt: a.timestamp,
5133
+ // Model verbatim from the issuing assistant message —
5134
+ // including "<synthetic>" (same rule as the event below).
5135
+ model: a.message?.model ?? null
5136
+ });
5137
+ const identity = toolIdentity(draft.server, draft.name);
5138
+ aggregate[identity] = (aggregate[identity] ?? 0) + 1;
5139
+ toolCalls.push(draft);
5140
+ if (typeof block.id === "string" && block.id) pendingByCallId.set(block.id, draft);
5141
+ }
5142
+ await emit({
4813
5143
  source_event_id: eventId,
4814
5144
  ts: a.timestamp,
4815
5145
  kind: "assistant_message",
@@ -4835,7 +5165,7 @@ async function parseClaudeCodeJsonl(ctx) {
4835
5165
  reasoning: 0
4836
5166
  },
4837
5167
  duration_ms: null,
4838
- tool_calls: countToolCalls(a.message?.content),
5168
+ tool_calls: aggregate,
4839
5169
  files_touched: [],
4840
5170
  ...excerpt ? { content_excerpt: excerpt } : {},
4841
5171
  source_file: ctx.sourceFile,
@@ -4848,6 +5178,20 @@ async function parseClaudeCodeJsonl(ctx) {
4848
5178
  });
4849
5179
  } else if (obj.type === "user") {
4850
5180
  const u = obj;
5181
+ const uContent = u.message?.content;
5182
+ if (Array.isArray(uContent)) {
5183
+ for (const block of uContent) {
5184
+ if (!block || block.type !== "tool_result") continue;
5185
+ const ref = block.tool_use_id;
5186
+ if (typeof ref !== "string") continue;
5187
+ const draft = pendingByCallId.get(ref);
5188
+ if (!draft) continue;
5189
+ pendingByCallId.delete(ref);
5190
+ draft.ended_at = u.timestamp;
5191
+ draft.status = block.is_error === true ? "error" : "success";
5192
+ draft.result_bytes = jsonBytes(block.content);
5193
+ }
5194
+ }
4851
5195
  if (!u.uuid || !sessionId) {
4852
5196
  skipped += 1;
4853
5197
  continue;
@@ -4858,7 +5202,7 @@ async function parseClaudeCodeJsonl(ctx) {
4858
5202
  continue;
4859
5203
  }
4860
5204
  const excerpt = extractExcerpt(u.message?.content);
4861
- events.push({
5205
+ await emit({
4862
5206
  source_event_id: eventId,
4863
5207
  ts: u.timestamp,
4864
5208
  kind: "user_message",
@@ -4879,13 +5223,39 @@ async function parseClaudeCodeJsonl(ctx) {
4879
5223
  source_byte_offset: offsetAtLineStart,
4880
5224
  billing: "subscription"
4881
5225
  });
5226
+ } else if (obj.type === "tool_use") {
5227
+ const t = obj;
5228
+ const observed = typeof t.name === "string" ? t.name.trim() : "";
5229
+ const ts = typeof t.timestamp === "string" ? t.timestamp : null;
5230
+ const sid = (typeof t.sessionId === "string" ? t.sessionId : null) ?? sessionId;
5231
+ if (!observed || !ts || !sid) {
5232
+ skipped += 1;
5233
+ continue;
5234
+ }
5235
+ const draft = buildToolCallDraft({
5236
+ observedName: observed,
5237
+ rawCallId: t.id,
5238
+ input: t.input,
5239
+ sessionId: sid,
5240
+ sourceEventId: sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart),
5241
+ callIndex: 0,
5242
+ startedAt: ts,
5243
+ // No issuing assistant message on this line — attribute to the
5244
+ // session's last real model, same as user_message attribution
5245
+ // (lastModel never holds "<synthetic>", per the rule above).
5246
+ model: lastModel
5247
+ });
5248
+ toolCalls.push(draft);
5249
+ if (typeof t.id === "string" && t.id) pendingByCallId.set(t.id, draft);
4882
5250
  } else {
4883
5251
  skipped += 1;
4884
5252
  }
4885
5253
  }
5254
+ if (ctx.onEvents && chunk.length > 0) await ctx.onEvents(chunk);
4886
5255
  return {
4887
5256
  events,
4888
- stats: { rawLines, emittedEvents: events.length, skipped },
5257
+ toolCalls,
5258
+ stats: { rawLines, emittedEvents: emitted, skipped },
4889
5259
  sourceFile: ctx.sourceFile
4890
5260
  };
4891
5261
  }
@@ -4899,15 +5269,26 @@ async function quickChecksum(path5) {
4899
5269
  start: Math.max(0, st.size - 4096),
4900
5270
  encoding: "utf8"
4901
5271
  });
4902
- const h = createHash("sha1");
5272
+ const h = createHash2("sha1");
4903
5273
  for await (const chunk of stream) h.update(chunk);
4904
5274
  return { size: st.size, mtime: st.mtimeMs, tailHash: h.digest("hex").slice(0, 16) };
4905
5275
  }
5276
+ var SHELL_TOOL_NAMES;
4906
5277
  var init_claude_code = __esm({
4907
5278
  "../../packages/parsers/src/claude-code/index.ts"() {
4908
5279
  "use strict";
4909
5280
  init_src();
4910
5281
  init_git();
5282
+ init_shell_families();
5283
+ init_tool_hash();
5284
+ init_types();
5285
+ SHELL_TOOL_NAMES = /* @__PURE__ */ new Set([
5286
+ "Bash",
5287
+ "shell",
5288
+ "local_shell_call",
5289
+ "exec_command",
5290
+ "run_terminal_cmd"
5291
+ ]);
4911
5292
  }
4912
5293
  });
4913
5294
 
@@ -4920,8 +5301,105 @@ function deriveSessionIdFromRolloutPath(path5) {
4920
5301
  );
4921
5302
  return m ? m[1] ?? null : null;
4922
5303
  }
5304
+ function commandFieldToString(cmd) {
5305
+ if (typeof cmd === "string") return cmd || null;
5306
+ if (Array.isArray(cmd)) {
5307
+ const parts = cmd.filter((p) => typeof p === "string");
5308
+ if (parts.length === 0) return null;
5309
+ const head = (parts[0] ?? "").split("/").pop() ?? "";
5310
+ const flag = parts[1] ?? "";
5311
+ if (parts.length >= 3 && SHELL_WRAPPER_BINARIES.has(head) && /^-[a-z]*c[a-z]*$/i.test(flag)) {
5312
+ return parts.slice(2).join("\n");
5313
+ }
5314
+ return parts.join(" ");
5315
+ }
5316
+ return null;
5317
+ }
5318
+ function firstString(...values) {
5319
+ for (const v of values) {
5320
+ if (typeof v === "string" && v) return v;
5321
+ }
5322
+ return null;
5323
+ }
5324
+ function extractToolCallPayload(pt, p) {
5325
+ const callId = firstString(p.call_id, p.id);
5326
+ const failed = p.status === "failed";
5327
+ if (pt === "local_shell_call") {
5328
+ const action = p.action && typeof p.action === "object" ? p.action : null;
5329
+ const command = commandFieldToString(action?.command);
5330
+ return {
5331
+ callId,
5332
+ server: "builtin",
5333
+ name: "shell",
5334
+ input: action,
5335
+ commandFamilies: command ? extractCommandFamilies(command) : [],
5336
+ failed
5337
+ };
5338
+ }
5339
+ const observed = firstString(p.name, p.tool);
5340
+ if (!observed) return null;
5341
+ let input = pt === "custom_tool_call" ? p.input : p.arguments ?? p.input;
5342
+ if (typeof input === "string" && !input.trim()) input = void 0;
5343
+ if (pt !== "custom_tool_call" && typeof input === "string") {
5344
+ try {
5345
+ input = JSON.parse(input);
5346
+ } catch {
5347
+ }
5348
+ }
5349
+ if (SHELL_TOOL_NAMES2.has(observed)) {
5350
+ const rec = input && typeof input === "object" && !Array.isArray(input) ? input : null;
5351
+ const command = commandFieldToString(
5352
+ rec?.command ?? rec?.cmd ?? (typeof input === "string" ? input : null)
5353
+ );
5354
+ return {
5355
+ callId,
5356
+ server: "builtin",
5357
+ name: "shell",
5358
+ input,
5359
+ commandFamilies: command ? extractCommandFamilies(command) : [],
5360
+ failed
5361
+ };
5362
+ }
5363
+ if (pt === "mcp_tool_call" && typeof p.server === "string" && p.server) {
5364
+ return {
5365
+ callId,
5366
+ // Mirror splitObservedToolName's cap: 116 + the `mcp:` prefix ≤ 120.
5367
+ server: `mcp:${normalizeToolName(p.server).slice(0, 116)}`,
5368
+ name: normalizeToolName(observed),
5369
+ input,
5370
+ commandFamilies: [],
5371
+ failed
5372
+ };
5373
+ }
5374
+ const { server, name } = splitObservedToolName(observed);
5375
+ return { callId, server, name, input, commandFamilies: [], failed };
5376
+ }
5377
+ function outputIndicatesError(p) {
5378
+ const out = p.output ?? p.result;
5379
+ if (out && typeof out === "object" && !Array.isArray(out)) {
5380
+ const o = out;
5381
+ if (o.success === false || o.is_error === true) return true;
5382
+ }
5383
+ return false;
5384
+ }
4923
5385
  async function parseCodexRollout(ctx) {
4924
5386
  const events = [];
5387
+ const toolCalls = [];
5388
+ let chunk = [];
5389
+ let emitted = 0;
5390
+ const emit = async (e) => {
5391
+ emitted += 1;
5392
+ if (!ctx.onEvents) {
5393
+ events.push(e);
5394
+ return;
5395
+ }
5396
+ chunk.push(e);
5397
+ if (chunk.length >= PARSER_EVENT_CHUNK) {
5398
+ const full = chunk;
5399
+ chunk = [];
5400
+ await ctx.onEvents(full);
5401
+ }
5402
+ };
4925
5403
  let rawLines = 0;
4926
5404
  let skipped = 0;
4927
5405
  let bytePos = 0;
@@ -4935,6 +5413,9 @@ async function parseCodexRollout(ctx) {
4935
5413
  let cwd = null;
4936
5414
  let model = null;
4937
5415
  let turnIndex = 0;
5416
+ let lastTs = null;
5417
+ const openCalls = /* @__PURE__ */ new Map();
5418
+ let pendingToolAggregate = {};
4938
5419
  for await (const line of rl) {
4939
5420
  const byteLen = Buffer.byteLength(line, "utf8") + 1;
4940
5421
  const offsetAtLineStart = startOffset + bytePos;
@@ -4951,22 +5432,91 @@ async function parseCodexRollout(ctx) {
4951
5432
  skipped += 1;
4952
5433
  continue;
4953
5434
  }
5435
+ const lineTs = obj.timestamp;
5436
+ if (typeof lineTs === "string" && lineTs) lastTs = lineTs;
4954
5437
  if (obj.type === "session_meta") {
4955
5438
  const m = obj;
4956
- sessionId = m.id ?? sessionId;
5439
+ const id = m.id ?? m.payload?.id ?? null;
5440
+ if (id && id !== sessionId) {
5441
+ sessionId = id;
5442
+ pendingToolAggregate = {};
5443
+ openCalls.clear();
5444
+ }
4957
5445
  continue;
4958
5446
  }
4959
5447
  if (obj.type === "turn_context") {
4960
5448
  const t = obj;
4961
- cwd = t.cwd ?? cwd;
4962
- model = t.model ?? model;
5449
+ cwd = t.cwd ?? t.payload?.cwd ?? cwd;
5450
+ model = t.model ?? t.payload?.model ?? model;
5451
+ continue;
5452
+ }
5453
+ if (obj.type === "response_item") {
5454
+ const r = obj;
5455
+ const payload = r.payload;
5456
+ const pt = payload && typeof payload.type === "string" ? payload.type : null;
5457
+ if (pt && TOOL_CALL_PAYLOAD_TYPES.has(pt) && sessionId) {
5458
+ const extracted = extractToolCallPayload(pt, payload);
5459
+ if (!extracted) {
5460
+ skipped += 1;
5461
+ continue;
5462
+ }
5463
+ const ts = r.timestamp ?? lastTs;
5464
+ if (!ts) {
5465
+ skipped += 1;
5466
+ continue;
5467
+ }
5468
+ const srcId = sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart);
5469
+ const { args_hash, signature_hash, args_bytes } = hashArgs(extracted.input);
5470
+ const draft = {
5471
+ external_call_id: (extracted.callId ?? fallbackCallId(srcId, 0)).slice(0, 120),
5472
+ session_id: sessionId,
5473
+ source_event_id: srcId,
5474
+ agent: "codex_cli",
5475
+ server: extracted.server,
5476
+ name: extracted.name,
5477
+ turn_index: turnIndex,
5478
+ // Each response_item line carries exactly one call.
5479
+ call_index: 0,
5480
+ started_at: ts,
5481
+ ended_at: null,
5482
+ status: extracted.failed ? "error" : "unknown",
5483
+ args_hash,
5484
+ signature_hash,
5485
+ args_bytes,
5486
+ result_bytes: 0,
5487
+ model,
5488
+ command_families: extracted.commandFamilies
5489
+ };
5490
+ toolCalls.push(draft);
5491
+ if (extracted.callId) openCalls.set(extracted.callId, draft);
5492
+ const identity = toolIdentity(extracted.server, extracted.name);
5493
+ pendingToolAggregate[identity] = (pendingToolAggregate[identity] ?? 0) + 1;
5494
+ continue;
5495
+ }
5496
+ if (pt && TOOL_CALL_OUTPUT_PAYLOAD_TYPES.has(pt)) {
5497
+ const p = payload;
5498
+ const callId = firstString(p.call_id, p.id);
5499
+ const open2 = callId ? openCalls.get(callId) : void 0;
5500
+ if (!callId || !open2) {
5501
+ skipped += 1;
5502
+ continue;
5503
+ }
5504
+ openCalls.delete(callId);
5505
+ open2.ended_at = r.timestamp ?? lastTs ?? open2.started_at;
5506
+ open2.result_bytes = jsonBytes(p.output ?? p.result);
5507
+ if (open2.status === "unknown") {
5508
+ open2.status = outputIndicatesError(p) ? "error" : "success";
5509
+ }
5510
+ continue;
5511
+ }
5512
+ skipped += 1;
4963
5513
  continue;
4964
5514
  }
4965
5515
  if (obj.type === "event_msg") {
4966
5516
  const m = obj;
4967
5517
  const ts = m.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
4968
5518
  const payload = m.payload;
4969
- if (!payload || !payload.type) {
5519
+ if (!payload?.type) {
4970
5520
  skipped += 1;
4971
5521
  continue;
4972
5522
  }
@@ -4977,7 +5527,7 @@ async function parseCodexRollout(ctx) {
4977
5527
  continue;
4978
5528
  }
4979
5529
  const slug = guessRepoSlugFromPath(cwd);
4980
- events.push({
5530
+ await emit({
4981
5531
  source_event_id: sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart),
4982
5532
  ts,
4983
5533
  kind: "assistant_message",
@@ -5003,11 +5553,12 @@ async function parseCodexRollout(ctx) {
5003
5553
  reasoning: tk.reasoning_output_tokens ?? 0
5004
5554
  },
5005
5555
  duration_ms: null,
5006
- tool_calls: {},
5556
+ tool_calls: pendingToolAggregate,
5007
5557
  files_touched: [],
5008
5558
  source_file: ctx.sourceFile,
5009
5559
  source_byte_offset: offsetAtLineStart
5010
5560
  });
5561
+ pendingToolAggregate = {};
5011
5562
  turnIndex += 1;
5012
5563
  continue;
5013
5564
  }
@@ -5016,7 +5567,7 @@ async function parseCodexRollout(ctx) {
5016
5567
  skipped += 1;
5017
5568
  continue;
5018
5569
  }
5019
- events.push({
5570
+ await emit({
5020
5571
  source_event_id: sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart),
5021
5572
  ts,
5022
5573
  kind: "user_message",
@@ -5042,17 +5593,42 @@ async function parseCodexRollout(ctx) {
5042
5593
  }
5043
5594
  skipped += 1;
5044
5595
  }
5596
+ if (ctx.onEvents && chunk.length > 0) await ctx.onEvents(chunk);
5045
5597
  return {
5046
5598
  events,
5047
- stats: { rawLines, emittedEvents: events.length, skipped },
5599
+ toolCalls,
5600
+ stats: { rawLines, emittedEvents: emitted, skipped },
5048
5601
  sourceFile: ctx.sourceFile
5049
5602
  };
5050
5603
  }
5604
+ var TOOL_CALL_PAYLOAD_TYPES, TOOL_CALL_OUTPUT_PAYLOAD_TYPES, SHELL_TOOL_NAMES2, SHELL_WRAPPER_BINARIES;
5051
5605
  var init_codex = __esm({
5052
5606
  "../../packages/parsers/src/codex/index.ts"() {
5053
5607
  "use strict";
5054
5608
  init_src();
5055
5609
  init_git();
5610
+ init_shell_families();
5611
+ init_tool_hash();
5612
+ init_types();
5613
+ TOOL_CALL_PAYLOAD_TYPES = /* @__PURE__ */ new Set([
5614
+ "function_call",
5615
+ "local_shell_call",
5616
+ "custom_tool_call",
5617
+ "mcp_tool_call"
5618
+ ]);
5619
+ TOOL_CALL_OUTPUT_PAYLOAD_TYPES = /* @__PURE__ */ new Set([
5620
+ "function_call_output",
5621
+ "local_shell_call_output",
5622
+ "custom_tool_call_output",
5623
+ "mcp_tool_call_output"
5624
+ ]);
5625
+ SHELL_TOOL_NAMES2 = /* @__PURE__ */ new Set([
5626
+ "shell",
5627
+ "local_shell_call",
5628
+ "exec_command",
5629
+ "run_terminal_cmd"
5630
+ ]);
5631
+ SHELL_WRAPPER_BINARIES = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish"]);
5056
5632
  }
5057
5633
  });
5058
5634
 
@@ -7622,107 +8198,6 @@ var init_src2 = __esm({
7622
8198
  }
7623
8199
  });
7624
8200
 
7625
- // src/identity.ts
7626
- import {
7627
- chmodSync,
7628
- mkdirSync,
7629
- readFileSync as readFileSync2,
7630
- renameSync,
7631
- writeFileSync,
7632
- existsSync as existsSync4
7633
- } from "fs";
7634
- import { homedir as homedir2, hostname as osHostname } from "os";
7635
- import { join as join3 } from "path";
7636
- function ensureRoot() {
7637
- mkdirSync(ROOT, { recursive: true, mode: 448 });
7638
- }
7639
- function writeAtomic(meta) {
7640
- ensureRoot();
7641
- const tmp = `${IDENTITY_FILE}.${process.pid}.tmp`;
7642
- writeFileSync(tmp, JSON.stringify(meta, null, 2), { mode: 384 });
7643
- renameSync(tmp, IDENTITY_FILE);
7644
- try {
7645
- chmodSync(IDENTITY_FILE, 384);
7646
- } catch {
7647
- }
7648
- }
7649
- function identityPath() {
7650
- return IDENTITY_FILE;
7651
- }
7652
- function hasIdentityFile() {
7653
- return existsSync4(IDENTITY_FILE);
7654
- }
7655
- function parseFile() {
7656
- try {
7657
- const raw = readFileSync2(IDENTITY_FILE, "utf8");
7658
- const obj = JSON.parse(raw);
7659
- if (!obj.deviceUuid || !obj.deviceId || !obj.bearerToken) {
7660
- return null;
7661
- }
7662
- return {
7663
- deviceUuid: obj.deviceUuid,
7664
- deviceId: obj.deviceId,
7665
- bearerToken: obj.bearerToken,
7666
- claimCode: obj.claimCode ?? null,
7667
- claimUrl: obj.claimUrl ?? null,
7668
- hostname: obj.hostname ?? osHostname(),
7669
- createdAt: obj.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7670
- userEmail: obj.userEmail ?? null,
7671
- defaultOrgId: obj.defaultOrgId ?? null
7672
- };
7673
- } catch {
7674
- return null;
7675
- }
7676
- }
7677
- function loadIdentity(migrateFromConf2) {
7678
- const fromFile = parseFile();
7679
- if (fromFile) return fromFile;
7680
- if (!migrateFromConf2) return null;
7681
- const legacy = migrateFromConf2();
7682
- if (!legacy) return null;
7683
- if (!legacy.deviceUuid || !legacy.deviceId || !legacy.bearerToken) {
7684
- return null;
7685
- }
7686
- const migrated = {
7687
- deviceUuid: legacy.deviceUuid,
7688
- deviceId: legacy.deviceId,
7689
- bearerToken: legacy.bearerToken,
7690
- claimCode: legacy.claimCode ?? null,
7691
- claimUrl: legacy.claimUrl ?? null,
7692
- hostname: osHostname(),
7693
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7694
- userEmail: legacy.userEmail ?? null,
7695
- defaultOrgId: legacy.defaultOrgId ?? null
7696
- };
7697
- writeAtomic(migrated);
7698
- return migrated;
7699
- }
7700
- function saveIdentity(meta) {
7701
- writeAtomic(meta);
7702
- }
7703
- function backupIdentity() {
7704
- if (!existsSync4(IDENTITY_FILE)) return null;
7705
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7706
- const dest = `${IDENTITY_FILE}.bak-${stamp}`;
7707
- renameSync(IDENTITY_FILE, dest);
7708
- return dest;
7709
- }
7710
- function updateIdentity(patch) {
7711
- const current = parseFile();
7712
- if (!current) return null;
7713
- const merged = { ...current, ...patch };
7714
- writeAtomic(merged);
7715
- return merged;
7716
- }
7717
- var ROOT, IDENTITY_FILE;
7718
- var init_identity = __esm({
7719
- "src/identity.ts"() {
7720
- "use strict";
7721
- ROOT = join3(homedir2(), ".modelstat");
7722
- IDENTITY_FILE = join3(ROOT, "identity.json");
7723
- }
7724
- });
7725
-
7726
8201
  // ../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/core/symbols.js
7727
8202
  var require_symbols = __commonJS({
7728
8203
  "../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/core/symbols.js"(exports, module) {
@@ -32614,6 +33089,9 @@ function classifyStatus(status2, attempt) {
32614
33089
  }
32615
33090
  return { type: "drop", reason: `http_${status2}` };
32616
33091
  }
33092
+ function wellFormedStringify(value) {
33093
+ return JSON.stringify(value, (_key, v) => typeof v === "string" ? v.toWellFormed() : v);
33094
+ }
32617
33095
  function sleep(ms) {
32618
33096
  return new Promise((r) => {
32619
33097
  setTimeout(r, ms);
@@ -32652,7 +33130,10 @@ var init_http = __esm({
32652
33130
  "content-type": "application/json",
32653
33131
  authorization: `Bearer ${token}`
32654
33132
  },
32655
- body: JSON.stringify(batch)
33133
+ // wellFormedStringify, not JSON.stringify: a truncated-emoji
33134
+ // lone surrogate in any excerpt 400s the whole batch on the
33135
+ // serde_json side. See the helper's doc comment.
33136
+ body: wellFormedStringify(batch)
32656
33137
  });
32657
33138
  } catch (err) {
32658
33139
  const detail = describeErrorWithCause(err);
@@ -33301,15 +33782,15 @@ function envPaths(name, { suffix = "nodejs" } = {}) {
33301
33782
  }
33302
33783
  return linux(name);
33303
33784
  }
33304
- var homedir3, tmpdir, env, macos, windows, linux;
33785
+ var homedir2, tmpdir, env, macos, windows, linux;
33305
33786
  var init_env_paths = __esm({
33306
33787
  "../../node_modules/.pnpm/env-paths@3.0.0/node_modules/env-paths/index.js"() {
33307
33788
  "use strict";
33308
- homedir3 = os.homedir();
33789
+ homedir2 = os.homedir();
33309
33790
  tmpdir = os.tmpdir();
33310
33791
  ({ env } = process2);
33311
33792
  macos = (name) => {
33312
- const library = path.join(homedir3, "Library");
33793
+ const library = path.join(homedir2, "Library");
33313
33794
  return {
33314
33795
  data: path.join(library, "Application Support", name),
33315
33796
  config: path.join(library, "Preferences", name),
@@ -33319,8 +33800,8 @@ var init_env_paths = __esm({
33319
33800
  };
33320
33801
  };
33321
33802
  windows = (name) => {
33322
- const appData = env.APPDATA || path.join(homedir3, "AppData", "Roaming");
33323
- const localAppData = env.LOCALAPPDATA || path.join(homedir3, "AppData", "Local");
33803
+ const appData = env.APPDATA || path.join(homedir2, "AppData", "Roaming");
33804
+ const localAppData = env.LOCALAPPDATA || path.join(homedir2, "AppData", "Local");
33324
33805
  return {
33325
33806
  // Data/config/cache/log are invented by me as Windows isn't opinionated about this
33326
33807
  data: path.join(localAppData, name, "Data"),
@@ -33331,13 +33812,13 @@ var init_env_paths = __esm({
33331
33812
  };
33332
33813
  };
33333
33814
  linux = (name) => {
33334
- const username = path.basename(homedir3);
33815
+ const username = path.basename(homedir2);
33335
33816
  return {
33336
- data: path.join(env.XDG_DATA_HOME || path.join(homedir3, ".local", "share"), name),
33337
- config: path.join(env.XDG_CONFIG_HOME || path.join(homedir3, ".config"), name),
33338
- cache: path.join(env.XDG_CACHE_HOME || path.join(homedir3, ".cache"), name),
33817
+ data: path.join(env.XDG_DATA_HOME || path.join(homedir2, ".local", "share"), name),
33818
+ config: path.join(env.XDG_CONFIG_HOME || path.join(homedir2, ".config"), name),
33819
+ cache: path.join(env.XDG_CACHE_HOME || path.join(homedir2, ".cache"), name),
33339
33820
  // https://wiki.debian.org/XDGBaseDirectorySpecification#state
33340
- log: path.join(env.XDG_STATE_HOME || path.join(homedir3, ".local", "state"), name),
33821
+ log: path.join(env.XDG_STATE_HOME || path.join(homedir2, ".local", "state"), name),
33341
33822
  temp: path.join(tmpdir, username, name)
33342
33823
  };
33343
33824
  };
@@ -33778,9 +34259,9 @@ import { once } from "events";
33778
34259
  import { createWriteStream } from "fs";
33779
34260
  import path3 from "path";
33780
34261
  import { Readable } from "stream";
33781
- function writeFileSync2(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
34262
+ function writeFileSync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
33782
34263
  if (isString(options))
33783
- return writeFileSync2(filePath, data, { encoding: options });
34264
+ return writeFileSync(filePath, data, { encoding: options });
33784
34265
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
33785
34266
  const retryOptions = { timeout };
33786
34267
  let tempDisposer = null;
@@ -43903,7 +44384,7 @@ var init_source = __esm({
43903
44384
  fs2.writeFileSync(this.path, data, { mode: this.#options.configFileMode });
43904
44385
  } else {
43905
44386
  try {
43906
- writeFileSync2(this.path, data, { mode: this.#options.configFileMode });
44387
+ writeFileSync(this.path, data, { mode: this.#options.configFileMode });
43907
44388
  } catch (error) {
43908
44389
  if (error?.code === "EXDEV") {
43909
44390
  fs2.writeFileSync(this.path, data, { mode: this.#options.configFileMode });
@@ -44004,6 +44485,107 @@ var init_source = __esm({
44004
44485
  }
44005
44486
  });
44006
44487
 
44488
+ // src/identity.ts
44489
+ import {
44490
+ chmodSync,
44491
+ mkdirSync,
44492
+ readFileSync as readFileSync2,
44493
+ renameSync,
44494
+ writeFileSync as writeFileSync2,
44495
+ existsSync as existsSync4
44496
+ } from "fs";
44497
+ import { homedir as homedir3, hostname as osHostname } from "os";
44498
+ import { join as join3 } from "path";
44499
+ function ensureRoot() {
44500
+ mkdirSync(ROOT, { recursive: true, mode: 448 });
44501
+ }
44502
+ function writeAtomic(meta) {
44503
+ ensureRoot();
44504
+ const tmp = `${IDENTITY_FILE}.${process.pid}.tmp`;
44505
+ writeFileSync2(tmp, JSON.stringify(meta, null, 2), { mode: 384 });
44506
+ renameSync(tmp, IDENTITY_FILE);
44507
+ try {
44508
+ chmodSync(IDENTITY_FILE, 384);
44509
+ } catch {
44510
+ }
44511
+ }
44512
+ function identityPath() {
44513
+ return IDENTITY_FILE;
44514
+ }
44515
+ function hasIdentityFile() {
44516
+ return existsSync4(IDENTITY_FILE);
44517
+ }
44518
+ function parseFile() {
44519
+ try {
44520
+ const raw = readFileSync2(IDENTITY_FILE, "utf8");
44521
+ const obj = JSON.parse(raw);
44522
+ if (!obj.deviceUuid || !obj.deviceId || !obj.bearerToken) {
44523
+ return null;
44524
+ }
44525
+ return {
44526
+ deviceUuid: obj.deviceUuid,
44527
+ deviceId: obj.deviceId,
44528
+ bearerToken: obj.bearerToken,
44529
+ claimCode: obj.claimCode ?? null,
44530
+ claimUrl: obj.claimUrl ?? null,
44531
+ hostname: obj.hostname ?? osHostname(),
44532
+ createdAt: obj.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
44533
+ userEmail: obj.userEmail ?? null,
44534
+ defaultOrgId: obj.defaultOrgId ?? null
44535
+ };
44536
+ } catch {
44537
+ return null;
44538
+ }
44539
+ }
44540
+ function loadIdentity(migrateFromConf2) {
44541
+ const fromFile = parseFile();
44542
+ if (fromFile) return fromFile;
44543
+ if (!migrateFromConf2) return null;
44544
+ const legacy = migrateFromConf2();
44545
+ if (!legacy) return null;
44546
+ if (!legacy.deviceUuid || !legacy.deviceId || !legacy.bearerToken) {
44547
+ return null;
44548
+ }
44549
+ const migrated = {
44550
+ deviceUuid: legacy.deviceUuid,
44551
+ deviceId: legacy.deviceId,
44552
+ bearerToken: legacy.bearerToken,
44553
+ claimCode: legacy.claimCode ?? null,
44554
+ claimUrl: legacy.claimUrl ?? null,
44555
+ hostname: osHostname(),
44556
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
44557
+ userEmail: legacy.userEmail ?? null,
44558
+ defaultOrgId: legacy.defaultOrgId ?? null
44559
+ };
44560
+ writeAtomic(migrated);
44561
+ return migrated;
44562
+ }
44563
+ function saveIdentity(meta) {
44564
+ writeAtomic(meta);
44565
+ }
44566
+ function backupIdentity() {
44567
+ if (!existsSync4(IDENTITY_FILE)) return null;
44568
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
44569
+ const dest = `${IDENTITY_FILE}.bak-${stamp}`;
44570
+ renameSync(IDENTITY_FILE, dest);
44571
+ return dest;
44572
+ }
44573
+ function updateIdentity(patch) {
44574
+ const current = parseFile();
44575
+ if (!current) return null;
44576
+ const merged = { ...current, ...patch };
44577
+ writeAtomic(merged);
44578
+ return merged;
44579
+ }
44580
+ var ROOT, IDENTITY_FILE;
44581
+ var init_identity = __esm({
44582
+ "src/identity.ts"() {
44583
+ "use strict";
44584
+ ROOT = join3(homedir3(), ".modelstat");
44585
+ IDENTITY_FILE = join3(ROOT, "identity.json");
44586
+ }
44587
+ });
44588
+
44007
44589
  // src/config.ts
44008
44590
  import { existsSync as existsSync5 } from "fs";
44009
44591
  import { hostname } from "os";
@@ -44350,6 +44932,16 @@ var init_ids2 = __esm({
44350
44932
  });
44351
44933
 
44352
44934
  // ../../packages/companion-core/src/queue/index.ts
44935
+ function attachSegmentIds(calls, segments) {
44936
+ const segmentByEvent = /* @__PURE__ */ new Map();
44937
+ for (const seg of segments) {
44938
+ for (const id of seg.source_event_ids) segmentByEvent.set(id, seg.segment_id);
44939
+ }
44940
+ return calls.map((c) => ({
44941
+ ...c,
44942
+ segment_id: segmentByEvent.get(c.source_event_id) ?? null
44943
+ }));
44944
+ }
44353
44945
  var init_queue = __esm({
44354
44946
  "../../packages/companion-core/src/queue/index.ts"() {
44355
44947
  "use strict";
@@ -44358,21 +44950,6 @@ var init_queue = __esm({
44358
44950
  }
44359
44951
  });
44360
44952
 
44361
- // ../../packages/companion-core/src/pipeline/prompts.ts
44362
- var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN, ABSTRACT_OUTPUT_MAX_CHARS;
44363
- var init_prompts = __esm({
44364
- "../../packages/companion-core/src/pipeline/prompts.ts"() {
44365
- "use strict";
44366
- OLLAMA_CHAT_MODEL = "qwen3:4b";
44367
- OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
44368
- SUMMARISER_SYSTEM_PROMPT = "You summarise an AI coding session in ONE sentence, \u2264 240 characters. If the user message includes sampled conversation excerpts, base your summary on what the developer was actually working on (the substance \u2014 what was being built, debugged, refactored, or designed). If only metadata is given, paraphrase the metadata. Never quote the excerpts verbatim. No PII, no code literals, no file paths, no API keys. Reply with only the sentence.";
44369
- SUMMARISER_MAX_TOKENS = 120;
44370
- SUMMARISER_TEMPERATURE = 0.2;
44371
- QWEN_CHARS_PER_TOKEN = 3.3;
44372
- ABSTRACT_OUTPUT_MAX_CHARS = 240;
44373
- }
44374
- });
44375
-
44376
44953
  // ../../packages/companion-core/src/pipeline/cognition.ts
44377
44954
  function buildCognitionUserPrompt(abstract) {
44378
44955
  return `Summary: "${abstract.replace(/\s+/g, " ").trim().slice(0, 480)}"
@@ -44455,6 +45032,21 @@ var init_cognition = __esm({
44455
45032
  }
44456
45033
  });
44457
45034
 
45035
+ // ../../packages/companion-core/src/pipeline/prompts.ts
45036
+ var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_MAX_TOKENS, SUMMARISER_TEMPERATURE, QWEN_CHARS_PER_TOKEN, ABSTRACT_OUTPUT_MAX_CHARS;
45037
+ var init_prompts = __esm({
45038
+ "../../packages/companion-core/src/pipeline/prompts.ts"() {
45039
+ "use strict";
45040
+ OLLAMA_CHAT_MODEL = "qwen3:4b";
45041
+ OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
45042
+ SUMMARISER_SYSTEM_PROMPT = "You summarise an AI coding session in ONE sentence, \u2264 240 characters. If the user message includes sampled conversation excerpts, base your summary on what the developer was actually working on (the substance \u2014 what was being built, debugged, refactored, or designed). If only metadata is given, paraphrase the metadata. Never quote the excerpts verbatim. No PII, no code literals, no file paths, no API keys. Reply with only the sentence.";
45043
+ SUMMARISER_MAX_TOKENS = 120;
45044
+ SUMMARISER_TEMPERATURE = 0.2;
45045
+ QWEN_CHARS_PER_TOKEN = 3.3;
45046
+ ABSTRACT_OUTPUT_MAX_CHARS = 240;
45047
+ }
45048
+ });
45049
+
44458
45050
  // ../../packages/companion-core/src/pipeline/title.ts
44459
45051
  function buildTitleUserPrompt(input) {
44460
45052
  const lines = input.abstracts.map(
@@ -44632,9 +45224,7 @@ async function buildForOneSession(sessionId, events, adapters2, onSlice) {
44632
45224
  } catch (err) {
44633
45225
  failed += 1;
44634
45226
  lastError = err instanceof Error ? err.message : String(err);
44635
- console.warn(
44636
- `[modelstat] slice failed in session ${sessionId}: ${lastError}`
44637
- );
45227
+ console.warn(`[modelstat] slice failed in session ${sessionId}: ${lastError}`);
44638
45228
  }
44639
45229
  }
44640
45230
  if (failed > 0) {
@@ -44739,6 +45329,24 @@ Write the SHORTEST keyword-dense paragraph (1-3 sentences, \u2264${ABSTRACT_OUTP
44739
45329
  for (const c of [...components].slice(0, 8)) {
44740
45330
  tags.push({ root_key: "components", name: c, confidence: 0.6 });
44741
45331
  }
45332
+ const toolCallCounts = /* @__PURE__ */ new Map();
45333
+ let toolCallTotal = 0;
45334
+ for (const ev of slice) {
45335
+ for (const [identity, n] of Object.entries(ev.tool_calls ?? {})) {
45336
+ if (!(n > 0)) continue;
45337
+ toolCallCounts.set(identity, (toolCallCounts.get(identity) ?? 0) + n);
45338
+ toolCallTotal += n;
45339
+ }
45340
+ }
45341
+ const topToolCalls = [...toolCallCounts.entries()].filter(([identity]) => identity.length <= 120).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
45342
+ for (const [identity, count] of topToolCalls) {
45343
+ const share = Math.round(count / toolCallTotal * 100) / 100;
45344
+ tags.push({
45345
+ root_key: "tool_calls",
45346
+ name: identity,
45347
+ confidence: Math.min(1, Math.max(0.05, share))
45348
+ });
45349
+ }
44742
45350
  let segmentEmbedding;
44743
45351
  try {
44744
45352
  const embedded = await adapters2.embed(redacted.text.slice(0, ABSTRACT_MAX_CHARS));
@@ -44826,13 +45434,13 @@ var SEGMENT_TIME_GAP_MS, SEGMENT_TOPIC_THRESHOLD, SEGMENT_MAX_TURNS, SEGMENT_MAX
44826
45434
  var init_pipeline = __esm({
44827
45435
  "../../packages/companion-core/src/pipeline/index.ts"() {
44828
45436
  "use strict";
44829
- init_redact();
44830
45437
  init_ids();
44831
- init_prompts();
44832
- init_cognition();
44833
45438
  init_redact();
45439
+ init_cognition();
44834
45440
  init_prompts();
45441
+ init_redact();
44835
45442
  init_cognition();
45443
+ init_prompts();
44836
45444
  init_title();
44837
45445
  SEGMENT_TIME_GAP_MS = 15 * 6e4;
44838
45446
  SEGMENT_TOPIC_THRESHOLD = 0.35;
@@ -45322,6 +45930,21 @@ var init_llama = __esm({
45322
45930
  }
45323
45931
  });
45324
45932
 
45933
+ // ../../packages/companion-core/src/optional-module.ts
45934
+ function isMissingOptionalModuleError(err) {
45935
+ const code = err?.code;
45936
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
45937
+ const msg = err instanceof Error ? err.message : String(err);
45938
+ return /cannot find (package|module)|cannot resolve|failed to resolve/i.test(msg);
45939
+ }
45940
+ var OPTIONAL_MODULE_MAX_LOAD_ATTEMPTS;
45941
+ var init_optional_module = __esm({
45942
+ "../../packages/companion-core/src/optional-module.ts"() {
45943
+ "use strict";
45944
+ OPTIONAL_MODULE_MAX_LOAD_ATTEMPTS = 3;
45945
+ }
45946
+ });
45947
+
45325
45948
  // ../../packages/companion-core/src/node/transformersjs-embed.ts
45326
45949
  async function loadPipeline(model) {
45327
45950
  if (cached) return cached;
@@ -45329,11 +45952,7 @@ async function loadPipeline(model) {
45329
45952
  if (!loadPromise2) {
45330
45953
  loadPromise2 = (async () => {
45331
45954
  try {
45332
- const moduleId = "@huggingface/transformers";
45333
- const tjs = await import(
45334
- /* @vite-ignore */
45335
- moduleId
45336
- );
45955
+ const tjs = await importModule("@huggingface/transformers");
45337
45956
  const p = await tjs.pipeline("feature-extraction", model, {
45338
45957
  device: "cpu",
45339
45958
  dtype: "fp32"
@@ -45342,12 +45961,16 @@ async function loadPipeline(model) {
45342
45961
  return p;
45343
45962
  } catch (err) {
45344
45963
  const msg = err.message;
45345
- if (/unsupported|architecture|not supported|onnx|cannot resolve/i.test(msg)) {
45964
+ loadAttempts += 1;
45965
+ if (isMissingOptionalModuleError(err) || /unsupported|architecture|not supported|onnx/i.test(msg) || loadAttempts >= OPTIONAL_MODULE_MAX_LOAD_ATTEMPTS) {
45346
45966
  loadFailedPermanently = true;
45347
45967
  }
45348
- console.warn(
45349
- `[modelstat] transformers.js embedder unavailable (segments will be re-embedded server-side): ${msg}`
45350
- );
45968
+ if (!warnedUnavailable) {
45969
+ warnedUnavailable = true;
45970
+ console.warn(
45971
+ `[modelstat] transformers.js embedder unavailable (segments will be re-embedded server-side; further attempts ${loadFailedPermanently ? "disabled" : `limited to ${OPTIONAL_MODULE_MAX_LOAD_ATTEMPTS}`}, warning once): ${msg}`
45972
+ );
45973
+ }
45351
45974
  loadPromise2 = null;
45352
45975
  return null;
45353
45976
  }
@@ -45364,21 +45987,32 @@ function createTransformersJsEmbedder(model = DEFAULT_MODEL) {
45364
45987
  const out = await pipe(text, { pooling: "mean", normalize: true });
45365
45988
  return Array.from(out.data);
45366
45989
  } catch (err) {
45367
- console.warn(
45368
- `[modelstat] embed error (returning empty vector, server will re-embed): ${err.message}`
45369
- );
45990
+ if (!warnedInferenceError) {
45991
+ warnedInferenceError = true;
45992
+ console.warn(
45993
+ `[modelstat] embed error (returning empty vectors, server will re-embed; warning once): ${err.message}`
45994
+ );
45995
+ }
45370
45996
  return [];
45371
45997
  }
45372
45998
  };
45373
45999
  }
45374
- var cached, loadPromise2, loadFailedPermanently, DEFAULT_MODEL;
46000
+ var cached, loadPromise2, loadFailedPermanently, loadAttempts, warnedUnavailable, warnedInferenceError, DEFAULT_MODEL, importModule;
45375
46001
  var init_transformersjs_embed = __esm({
45376
46002
  "../../packages/companion-core/src/node/transformersjs-embed.ts"() {
45377
46003
  "use strict";
46004
+ init_optional_module();
45378
46005
  cached = null;
45379
46006
  loadPromise2 = null;
45380
46007
  loadFailedPermanently = false;
46008
+ loadAttempts = 0;
46009
+ warnedUnavailable = false;
46010
+ warnedInferenceError = false;
45381
46011
  DEFAULT_MODEL = "Xenova/bge-small-en-v1.5";
46012
+ importModule = (id) => import(
46013
+ /* @vite-ignore */
46014
+ id
46015
+ );
45382
46016
  }
45383
46017
  });
45384
46018
 
@@ -45417,18 +46051,23 @@ async function createPrivacyFilterRedactor(opts = {}) {
45417
46051
  const device = opts.device ?? (isBrowser ? "webgpu" : "cpu");
45418
46052
  const dtype = opts.dtype ?? "q4";
45419
46053
  const modelId = opts.model ?? "openai/privacy-filter";
46054
+ const importModule2 = opts.importModule ?? ((id) => import(
46055
+ /* @vite-ignore */
46056
+ id
46057
+ ));
45420
46058
  let cached2 = null;
45421
46059
  let loadPromise3 = null;
46060
+ let loadFailedPermanently2 = false;
46061
+ let loadAttempts2 = 0;
46062
+ let warnedUnavailable2 = false;
46063
+ let warnedInferenceError2 = false;
45422
46064
  async function loadPipeline2() {
45423
46065
  if (cached2) return cached2;
46066
+ if (loadFailedPermanently2) return null;
45424
46067
  if (!loadPromise3) {
45425
46068
  loadPromise3 = (async () => {
45426
46069
  try {
45427
- const moduleId = "@huggingface/transformers";
45428
- const tjs = await import(
45429
- /* @vite-ignore */
45430
- moduleId
45431
- );
46070
+ const tjs = await importModule2("@huggingface/transformers");
45432
46071
  const p = await tjs.pipeline("token-classification", modelId, {
45433
46072
  device,
45434
46073
  dtype,
@@ -45438,10 +46077,17 @@ async function createPrivacyFilterRedactor(opts = {}) {
45438
46077
  return p;
45439
46078
  } catch (err) {
45440
46079
  loadPromise3 = null;
45441
- console.warn(
45442
- "[privacy-filter] adapter unavailable \u2014 install @huggingface/transformers in the consuming package to enable model-based redaction. Falling back to pass-through.",
45443
- err.message
45444
- );
46080
+ loadAttempts2 += 1;
46081
+ if (isMissingOptionalModuleError(err) || loadAttempts2 >= OPTIONAL_MODULE_MAX_LOAD_ATTEMPTS) {
46082
+ loadFailedPermanently2 = true;
46083
+ }
46084
+ if (!warnedUnavailable2) {
46085
+ warnedUnavailable2 = true;
46086
+ console.warn(
46087
+ "[privacy-filter] adapter unavailable \u2014 install @huggingface/transformers in the consuming package to enable model-based redaction. Falling back to pass-through (warning once).",
46088
+ err.message
46089
+ );
46090
+ }
45445
46091
  return null;
45446
46092
  }
45447
46093
  })();
@@ -45464,10 +46110,13 @@ async function createPrivacyFilterRedactor(opts = {}) {
45464
46110
  try {
45465
46111
  tokens = await classify(text);
45466
46112
  } catch (err) {
45467
- console.warn(
45468
- "[privacy-filter] inference failed, returning input unchanged:",
45469
- err.message
45470
- );
46113
+ if (!warnedInferenceError2) {
46114
+ warnedInferenceError2 = true;
46115
+ console.warn(
46116
+ "[privacy-filter] inference failed, returning input unchanged (warning once):",
46117
+ err.message
46118
+ );
46119
+ }
45471
46120
  return empty;
45472
46121
  }
45473
46122
  const spans = [];
@@ -45504,6 +46153,7 @@ async function createPrivacyFilterRedactor(opts = {}) {
45504
46153
  var init_privacy_filter = __esm({
45505
46154
  "../../packages/companion-core/src/redact/privacy-filter.ts"() {
45506
46155
  "use strict";
46156
+ init_optional_module();
45507
46157
  }
45508
46158
  });
45509
46159
 
@@ -45613,9 +46263,9 @@ async function scanAll(cb = {}) {
45613
46263
  const full = join5(dir, f);
45614
46264
  jobs.push({
45615
46265
  path: full,
45616
- parse: async () => {
45617
- const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full });
45618
- return { events: r.events, path: full };
46266
+ parse: async (sink2) => {
46267
+ const r = await parseClaudeCodeJsonl({ deviceId, sourceFile: full, onEvents: sink2 });
46268
+ return { toolCalls: r.toolCalls ?? [] };
45619
46269
  }
45620
46270
  });
45621
46271
  }
@@ -45637,9 +46287,9 @@ async function scanAll(cb = {}) {
45637
46287
  const full = join5(base, y, m, d, f);
45638
46288
  jobs.push({
45639
46289
  path: full,
45640
- parse: async () => {
45641
- const r = await parseCodexRollout({ deviceId, sourceFile: full });
45642
- return { events: r.events, path: full };
46290
+ parse: async (sink2) => {
46291
+ const r = await parseCodexRollout({ deviceId, sourceFile: full, onEvents: sink2 });
46292
+ return { toolCalls: r.toolCalls ?? [] };
45643
46293
  }
45644
46294
  });
45645
46295
  }
@@ -45655,10 +46305,11 @@ async function scanAll(cb = {}) {
45655
46305
  let eventsUploaded = 0;
45656
46306
  let segmentsUploaded = 0;
45657
46307
  let buffer = [];
46308
+ let toolCallBuffer = [];
45658
46309
  let pendingCursors = [];
45659
46310
  const runSegmentsBySession = /* @__PURE__ */ new Map();
45660
46311
  async function flushBatch() {
45661
- if (!buffer.length) return;
46312
+ if (!buffer.length && !toolCallBuffer.length) return;
45662
46313
  const events = buffer.map(withNonNullTokens);
45663
46314
  const segments = await buildSegments(events, cb.onProgress);
45664
46315
  for (const seg of segments) {
@@ -45682,6 +46333,10 @@ async function scanAll(cb = {}) {
45682
46333
  agent_version: AGENT_VERSION,
45683
46334
  events,
45684
46335
  segments,
46336
+ // Per-call tool invocations: now that segments exist, attribute
46337
+ // each call to the segment covering its source event (null when
46338
+ // no segment covers it — e.g. codex response_item anchors).
46339
+ tool_calls: attachSegmentIds(toolCallBuffer, segments),
45685
46340
  ...Object.keys(sessionTitles).length ? { session_titles: sessionTitles } : {}
45686
46341
  };
45687
46342
  cb.onUpload?.({ events: events.length, segments: segments.length });
@@ -45692,8 +46347,26 @@ async function scanAll(cb = {}) {
45692
46347
  for (const pc of pendingCursors) state.setCursor(pc.path, pc.cs);
45693
46348
  pendingCursors = [];
45694
46349
  buffer = [];
46350
+ toolCallBuffer = [];
45695
46351
  cb.onUploaded?.({ events: res.accepted, segments: segments.length });
45696
46352
  }
46353
+ const sink = async (events) => {
46354
+ for (const e of events) {
46355
+ buffer.push(e);
46356
+ if (buffer.length >= BATCH_MAX_EVENTS) await flushBatch();
46357
+ }
46358
+ if (buffer.length > BATCH_BUFFER_HARD_CAP) {
46359
+ throw new Error(
46360
+ `scan event buffer exceeded ${BATCH_BUFFER_HARD_CAP} events \u2014 incremental batch flushing has regressed (see BATCH_BUFFER_HARD_CAP)`
46361
+ );
46362
+ }
46363
+ };
46364
+ const bufferToolCalls = async (calls) => {
46365
+ for (const c of calls) {
46366
+ if (toolCallBuffer.length >= BATCH_MAX_TOOL_CALLS) await flushBatch();
46367
+ toolCallBuffer.push(c);
46368
+ }
46369
+ };
45697
46370
  for (let i = 0; i < jobs.length; i++) {
45698
46371
  const job = jobs[i];
45699
46372
  cb.onFile?.(job.path, i, jobs.length);
@@ -45705,13 +46378,8 @@ async function scanAll(cb = {}) {
45705
46378
  }
45706
46379
  filesScanned += 1;
45707
46380
  try {
45708
- const r = await job.parse();
45709
- if (r.events.length) {
45710
- for (const e of r.events) {
45711
- buffer.push(e);
45712
- if (buffer.length >= BATCH_MAX_EVENTS) await flushBatch();
45713
- }
45714
- }
46381
+ const r = await job.parse(sink);
46382
+ await bufferToolCalls(r.toolCalls);
45715
46383
  if (cs) pendingCursors.push({ path: job.path, cs });
45716
46384
  } catch (e) {
45717
46385
  console.warn(` ! parse failed for ${job.path}:`, e.message);
@@ -45720,17 +46388,20 @@ async function scanAll(cb = {}) {
45720
46388
  await flushBatch();
45721
46389
  return { filesScanned, filesUnchanged, batchesUploaded, eventsUploaded, segmentsUploaded };
45722
46390
  }
45723
- var AGENT_VERSION, BATCH_MAX_EVENTS, ZERO_TOKENS;
46391
+ var AGENT_VERSION, BATCH_MAX_EVENTS, BATCH_MAX_TOOL_CALLS, BATCH_BUFFER_HARD_CAP, ZERO_TOKENS;
45724
46392
  var init_scan = __esm({
45725
46393
  "src/scan.ts"() {
45726
46394
  "use strict";
45727
- init_src2();
45728
46395
  init_src3();
45729
- init_pipeline2();
45730
- init_config2();
46396
+ init_queue();
46397
+ init_src2();
45731
46398
  init_api();
45732
- AGENT_VERSION = true ? "agent-0.0.45" : "agent-dev";
46399
+ init_config2();
46400
+ init_pipeline2();
46401
+ AGENT_VERSION = true ? "agent-0.0.47" : "agent-dev";
45733
46402
  BATCH_MAX_EVENTS = 2e3;
46403
+ BATCH_MAX_TOOL_CALLS = 2e4;
46404
+ BATCH_BUFFER_HARD_CAP = BATCH_MAX_EVENTS * 2;
45734
46405
  ZERO_TOKENS = {
45735
46406
  input: 0,
45736
46407
  output: 0,
@@ -45766,9 +46437,9 @@ function isProcessAlive(pid) {
45766
46437
  return false;
45767
46438
  }
45768
46439
  }
45769
- function readLock() {
46440
+ function readDaemonLock(lockFile = LOCK_FILE) {
45770
46441
  try {
45771
- const raw = readFileSync4(LOCK_FILE, "utf8");
46442
+ const raw = readFileSync4(lockFile, "utf8");
45772
46443
  const obj = JSON.parse(raw);
45773
46444
  if (typeof obj.pid !== "number") return null;
45774
46445
  return {
@@ -45793,7 +46464,7 @@ function writeLockAtomic(meta) {
45793
46464
  renameSync2(tmp, LOCK_FILE);
45794
46465
  }
45795
46466
  function removeLockIfOwned(ownerPid) {
45796
- const lock = readLock();
46467
+ const lock = readDaemonLock();
45797
46468
  if (!lock) return;
45798
46469
  if (lock.pid !== ownerPid) return;
45799
46470
  try {
@@ -45802,7 +46473,7 @@ function removeLockIfOwned(ownerPid) {
45802
46473
  }
45803
46474
  }
45804
46475
  function acquireDaemonLock(opts) {
45805
- const existing = readLock();
46476
+ const existing = readDaemonLock();
45806
46477
  if (existing && isProcessAlive(existing.pid)) {
45807
46478
  if (!opts.force) {
45808
46479
  const ageSec = ageInSeconds(existing.startedAt);
@@ -45820,6 +46491,20 @@ function acquireDaemonLock(opts) {
45820
46491
  apiUrl: opts.apiUrl
45821
46492
  };
45822
46493
  writeLockAtomic(meta);
46494
+ const recheck = setTimeout(() => {
46495
+ const current = readDaemonLock();
46496
+ if (checkLockOwnership(process.pid, current) !== "lost") return;
46497
+ const winner = current;
46498
+ if (opts.onLockLost) {
46499
+ opts.onLockLost(winner);
46500
+ return;
46501
+ }
46502
+ console.log(
46503
+ `[modelstat] another daemon (pid ${winner.pid}, started ${winner.startedAt}) won the lock race \u2014 this instance is standing down.`
46504
+ );
46505
+ process.exit(0);
46506
+ }, LOCK_RECHECK_MS);
46507
+ recheck.unref();
45823
46508
  const cleanup = () => removeLockIfOwned(process.pid);
45824
46509
  process.once("beforeExit", cleanup);
45825
46510
  process.once("SIGINT", () => {
@@ -45837,6 +46522,11 @@ function acquireDaemonLock(opts) {
45837
46522
  });
45838
46523
  return { kind: "acquired" };
45839
46524
  }
46525
+ function checkLockOwnership(myPid, current, pidAlive = isProcessAlive) {
46526
+ if (!current || current.pid === myPid) return "owned";
46527
+ if (!pidAlive(current.pid)) return "winner_dead";
46528
+ return "lost";
46529
+ }
45840
46530
  function ageInSeconds(iso) {
45841
46531
  const t = Date.parse(iso);
45842
46532
  if (Number.isNaN(t)) return -1;
@@ -45851,12 +46541,13 @@ function formatAge(seconds) {
45851
46541
  const h = Math.floor(m / 60);
45852
46542
  return `${h}h ${m % 60}m`;
45853
46543
  }
45854
- var LOCK_DIR, LOCK_FILE;
46544
+ var LOCK_DIR, LOCK_FILE, LOCK_RECHECK_MS;
45855
46545
  var init_lock = __esm({
45856
46546
  "src/lock.ts"() {
45857
46547
  "use strict";
45858
46548
  LOCK_DIR = join7(homedir7(), ".modelstat");
45859
46549
  LOCK_FILE = join7(LOCK_DIR, "daemon.lock");
46550
+ LOCK_RECHECK_MS = 5e3;
45860
46551
  }
45861
46552
  });
45862
46553
 
@@ -45920,7 +46611,7 @@ var PROCESSING_VERSION;
45920
46611
  var init_processing_version = __esm({
45921
46612
  "src/processing-version.ts"() {
45922
46613
  "use strict";
45923
- PROCESSING_VERSION = 4;
46614
+ PROCESSING_VERSION = 5;
45924
46615
  }
45925
46616
  });
45926
46617
 
@@ -47730,17 +48421,44 @@ async function sendHeartbeat() {
47730
48421
  }
47731
48422
  writeLocalStatus(body).catch(() => void 0);
47732
48423
  }
48424
+ async function rotateRunawayLogs() {
48425
+ const { homedir: homedir10 } = await import("os");
48426
+ const { join: join12 } = await import("path");
48427
+ const { open: open2, stat: stat6, truncate, writeFile } = await import("fs/promises");
48428
+ const dir = join12(homedir10(), ".modelstat", "logs");
48429
+ for (const name of ["out.log", "err.log"]) {
48430
+ const p = join12(dir, name);
48431
+ try {
48432
+ const st = await stat6(p);
48433
+ if (st.size <= LOG_MAX_BYTES) continue;
48434
+ const keep = Math.min(LOG_TAIL_KEEP_BYTES, st.size);
48435
+ const fh = await open2(p, "r");
48436
+ try {
48437
+ const buf = Buffer.alloc(keep);
48438
+ await fh.read(buf, 0, keep, st.size - keep);
48439
+ await writeFile(p.replace(/\.log$/, ".old.log"), buf);
48440
+ } finally {
48441
+ await fh.close();
48442
+ }
48443
+ await truncate(p, 0);
48444
+ console.log(
48445
+ `[modelstat] rotated ${name}: was ${(st.size / 1024 / 1024).toFixed(0)} MB (> ${LOG_MAX_BYTES / 1024 / 1024} MB cap), tail kept in ${name.replace(/\.log$/, ".old.log")}`
48446
+ );
48447
+ } catch {
48448
+ }
48449
+ }
48450
+ }
47733
48451
  async function writeLocalStatus(snapshot) {
47734
- const { homedir: homedir9 } = await import("os");
47735
- const { join: join11 } = await import("path");
48452
+ const { homedir: homedir10 } = await import("os");
48453
+ const { join: join12 } = await import("path");
47736
48454
  const { writeFile, mkdir: mkdir2, rename } = await import("fs/promises");
47737
48455
  if (!lastStatusPath) {
47738
- const dir = join11(homedir9(), ".modelstat");
48456
+ const dir = join12(homedir10(), ".modelstat");
47739
48457
  try {
47740
48458
  await mkdir2(dir, { recursive: true });
47741
48459
  } catch {
47742
48460
  }
47743
- lastStatusPath = join11(dir, "last-status.json");
48461
+ lastStatusPath = join12(dir, "last-status.json");
47744
48462
  }
47745
48463
  const tmp = `${lastStatusPath}.tmp`;
47746
48464
  try {
@@ -47822,7 +48540,17 @@ async function runDaemon(opts = {}) {
47822
48540
  const lock = acquireDaemonLock({
47823
48541
  agentVersion: AGENT_VERSION2,
47824
48542
  apiUrl: state.apiUrl,
47825
- force: opts.force === true
48543
+ force: opts.force === true,
48544
+ // If a racing daemon out-renamed us for the lock (see lock.ts
48545
+ // recheck), stand down with exit 0: our supervisor's next health
48546
+ // check sees the winner and adopts it instead of respawning us.
48547
+ onLockLost: (winner) => {
48548
+ setPhase("offline", `another daemon (pid ${winner.pid}) owns the lock \u2014 standing down`);
48549
+ console.log(
48550
+ `[modelstat] another daemon (pid ${winner.pid}, started ${winner.startedAt}) won the lock race \u2014 this instance is standing down.`
48551
+ );
48552
+ process.exit(0);
48553
+ }
47826
48554
  });
47827
48555
  if (lock.kind === "already_running") {
47828
48556
  console.log(
@@ -47839,6 +48567,7 @@ async function runDaemon(opts = {}) {
47839
48567
  return;
47840
48568
  }
47841
48569
  setPhase("starting", "Booting");
48570
+ await rotateRunawayLogs();
47842
48571
  setStat("segments_sent", state.segmentsSent);
47843
48572
  const hb = setInterval(() => void sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
47844
48573
  hb.unref();
@@ -47862,19 +48591,19 @@ async function runDaemon(opts = {}) {
47862
48591
  await runDiscovery();
47863
48592
  await requestScan("startup");
47864
48593
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
47865
- const { homedir: homedir9, platform: platform5 } = await import("os");
47866
- const { join: join11 } = await import("path");
47867
- const home2 = homedir9();
48594
+ const { homedir: homedir10, platform: platform5 } = await import("os");
48595
+ const { join: join12 } = await import("path");
48596
+ const home2 = homedir10();
47868
48597
  const dirs = [
47869
- join11(home2, ".claude/projects"),
47870
- join11(home2, ".codex/sessions"),
47871
- join11(home2, ".cursor/ai-tracking"),
47872
- join11(home2, ".gemini"),
48598
+ join12(home2, ".claude/projects"),
48599
+ join12(home2, ".codex/sessions"),
48600
+ join12(home2, ".cursor/ai-tracking"),
48601
+ join12(home2, ".gemini"),
47873
48602
  ...platform5() === "darwin" ? [
47874
- join11(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47875
- join11(home2, "Library/Application Support/Claude")
48603
+ join12(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
48604
+ join12(home2, "Library/Application Support/Claude")
47876
48605
  ] : [
47877
- join11(home2, ".config/Cursor/User/workspaceStorage")
48606
+ join12(home2, ".config/Cursor/User/workspaceStorage")
47878
48607
  ]
47879
48608
  ].filter((p) => existsSync9(p) && statSync2(p).isDirectory());
47880
48609
  setPhase("watching", `Watching ${dirs.length} directories`);
@@ -47917,7 +48646,7 @@ async function runDaemon(opts = {}) {
47917
48646
  await new Promise(() => {
47918
48647
  });
47919
48648
  }
47920
- var import_undici2, AGENT_VERSION2, HEARTBEAT_INTERVAL_MS, SCAN_INTERVAL_MS, DISCOVERY_INTERVAL_MS, status, LOCAL_FLUSH_THROTTLE_MS, localFlushTimer, localFlushPending, lastStatusPath, scanRunner;
48649
+ var import_undici2, AGENT_VERSION2, HEARTBEAT_INTERVAL_MS, SCAN_INTERVAL_MS, DISCOVERY_INTERVAL_MS, status, LOCAL_FLUSH_THROTTLE_MS, localFlushTimer, localFlushPending, LOG_MAX_BYTES, LOG_TAIL_KEEP_BYTES, lastStatusPath, scanRunner;
47921
48650
  var init_daemon = __esm({
47922
48651
  "src/daemon.ts"() {
47923
48652
  "use strict";
@@ -47929,7 +48658,7 @@ var init_daemon = __esm({
47929
48658
  init_lock();
47930
48659
  init_scan();
47931
48660
  init_single_flight();
47932
- AGENT_VERSION2 = true ? "agent-0.0.45" : "agent-dev";
48661
+ AGENT_VERSION2 = true ? "agent-0.0.47" : "agent-dev";
47933
48662
  HEARTBEAT_INTERVAL_MS = 1e4;
47934
48663
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
47935
48664
  DISCOVERY_INTERVAL_MS = 6e4;
@@ -47945,6 +48674,8 @@ var init_daemon = __esm({
47945
48674
  LOCAL_FLUSH_THROTTLE_MS = 400;
47946
48675
  localFlushTimer = null;
47947
48676
  localFlushPending = false;
48677
+ LOG_MAX_BYTES = 64 * 1024 * 1024;
48678
+ LOG_TAIL_KEEP_BYTES = 4 * 1024 * 1024;
47948
48679
  lastStatusPath = null;
47949
48680
  scanRunner = createCoalescingRunner(runScanCycle);
47950
48681
  }
@@ -47956,33 +48687,33 @@ __export(watch_exports, {
47956
48687
  watchForever: () => watchForever
47957
48688
  });
47958
48689
  import { existsSync as existsSync10 } from "fs";
47959
- import { homedir as homedir8, platform as platform3 } from "os";
47960
- import { join as join10 } from "path";
48690
+ import { homedir as homedir9, platform as platform3 } from "os";
48691
+ import { join as join11 } from "path";
47961
48692
  function resolveWatchDirs() {
47962
- const home2 = homedir8();
47963
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join10(home2, ".config");
47964
- const xdgData = process.env.XDG_DATA_HOME ?? join10(home2, ".local/share");
48693
+ const home2 = homedir9();
48694
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join11(home2, ".config");
48695
+ const xdgData = process.env.XDG_DATA_HOME ?? join11(home2, ".local/share");
47965
48696
  const candidates = [
47966
48697
  // universal (default HOME-rooted CLI data dirs)
47967
- join10(home2, ".claude/projects"),
47968
- join10(home2, ".codex/sessions"),
47969
- join10(home2, ".cursor/ai-tracking"),
47970
- join10(home2, ".gemini"),
47971
- join10(home2, ".aider"),
48698
+ join11(home2, ".claude/projects"),
48699
+ join11(home2, ".codex/sessions"),
48700
+ join11(home2, ".cursor/ai-tracking"),
48701
+ join11(home2, ".gemini"),
48702
+ join11(home2, ".aider"),
47972
48703
  // XDG / Linux
47973
- join10(xdgConfig, "claude/projects"),
47974
- join10(xdgConfig, "codex/sessions"),
47975
- join10(xdgConfig, "Cursor/User/workspaceStorage"),
47976
- join10(xdgConfig, "Code/User/workspaceStorage"),
47977
- join10(xdgConfig, "Code - Insiders/User/workspaceStorage"),
47978
- join10(xdgData, "claude/projects"),
48704
+ join11(xdgConfig, "claude/projects"),
48705
+ join11(xdgConfig, "codex/sessions"),
48706
+ join11(xdgConfig, "Cursor/User/workspaceStorage"),
48707
+ join11(xdgConfig, "Code/User/workspaceStorage"),
48708
+ join11(xdgConfig, "Code - Insiders/User/workspaceStorage"),
48709
+ join11(xdgData, "claude/projects"),
47979
48710
  // macOS
47980
48711
  ...platform3() === "darwin" ? [
47981
- join10(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
47982
- join10(home2, "Library/Application Support/Claude"),
47983
- join10(home2, "Library/Application Support/Code/User/workspaceStorage"),
47984
- join10(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
47985
- join10(home2, "Library/Application Support/Zed")
48712
+ join11(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
48713
+ join11(home2, "Library/Application Support/Claude"),
48714
+ join11(home2, "Library/Application Support/Code/User/workspaceStorage"),
48715
+ join11(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
48716
+ join11(home2, "Library/Application Support/Zed")
47986
48717
  ] : []
47987
48718
  ];
47988
48719
  return Array.from(new Set(candidates)).filter((p) => existsSync10(p));
@@ -48047,13 +48778,13 @@ var init_watch = __esm({
48047
48778
 
48048
48779
  // src/cli.ts
48049
48780
  init_src2();
48050
- init_identity();
48051
48781
  init_api();
48052
48782
  init_config2();
48783
+ init_identity();
48053
48784
  init_scan();
48054
48785
  import { spawn } from "child_process";
48055
48786
  import { randomBytes } from "crypto";
48056
- import { platform as platform4, release, arch as cpuArch, hostname as hostname2 } from "os";
48787
+ import { arch as cpuArch, hostname as hostname2, platform as platform4, release } from "os";
48057
48788
  import { createInterface as createInterface3 } from "readline";
48058
48789
 
48059
48790
  // src/service.ts
@@ -48409,6 +49140,59 @@ function trayStatus() {
48409
49140
  return exe ? { installed: true, path: exe.replace(/\/Contents\/MacOS\/modelstat-tray$/, "") } : { installed: false, path: null };
48410
49141
  }
48411
49142
 
49143
+ // src/supervise.ts
49144
+ init_lock();
49145
+ import { readFileSync as readFileSync5 } from "fs";
49146
+ import { homedir as homedir8 } from "os";
49147
+ import { join as join8 } from "path";
49148
+ var STATUS_FRESH_MS = 12e4;
49149
+ var BOOT_GRACE_MS = 9e4;
49150
+ function decideSupervision(input) {
49151
+ const fresh = input.statusFreshMs ?? STATUS_FRESH_MS;
49152
+ const grace = input.bootGraceMs ?? BOOT_GRACE_MS;
49153
+ if (!input.lock || !input.ownerAlive) return "spawn";
49154
+ if (input.myAgentVersion && input.lock.agentVersion !== "unknown" && input.lock.agentVersion !== input.myAgentVersion) {
49155
+ return "replace";
49156
+ }
49157
+ if (input.statusAgeMs !== null && input.statusAgeMs <= fresh) return "adopt";
49158
+ if (input.lockAgeMs !== null && input.lockAgeMs <= grace) return "adopt";
49159
+ return "replace";
49160
+ }
49161
+ function daemonHealth(opts = {}) {
49162
+ const now = opts.now ?? Date.now();
49163
+ const pidAlive = opts.pidAlive ?? isProcessAlive;
49164
+ const statusPath = opts.statusPath ?? join8(homedir8(), ".modelstat", "last-status.json");
49165
+ const lock = opts.lockPath === void 0 ? readDaemonLock() : readDaemonLock(opts.lockPath);
49166
+ const ownerAlive = lock !== null && pidAlive(lock.pid);
49167
+ const lockAgeMs = lock ? ageMs(lock.startedAt, now) : null;
49168
+ let statusAgeMs = null;
49169
+ try {
49170
+ const raw = readFileSync5(statusPath, "utf8");
49171
+ const writtenAt = JSON.parse(raw).written_at;
49172
+ statusAgeMs = writtenAt ? ageMs(writtenAt, now) : null;
49173
+ } catch {
49174
+ statusAgeMs = null;
49175
+ }
49176
+ return {
49177
+ decision: decideSupervision({
49178
+ lock,
49179
+ ownerAlive,
49180
+ lockAgeMs,
49181
+ statusAgeMs,
49182
+ myAgentVersion: opts.myAgentVersion
49183
+ }),
49184
+ lock,
49185
+ ownerAlive,
49186
+ lockAgeMs,
49187
+ statusAgeMs
49188
+ };
49189
+ }
49190
+ function ageMs(iso, now) {
49191
+ const t = Date.parse(iso);
49192
+ if (Number.isNaN(t)) return null;
49193
+ return Math.max(0, now - t);
49194
+ }
49195
+
48412
49196
  // src/cli.ts
48413
49197
  async function confirmPrompt(question, defaultYes) {
48414
49198
  if (process.stdin.isTTY !== true) return defaultYes;
@@ -48439,7 +49223,7 @@ function tryOpenBrowser(url) {
48439
49223
  return false;
48440
49224
  }
48441
49225
  }
48442
- var AGENT_VERSION3 = true ? "agent-0.0.45" : "agent-dev";
49226
+ var AGENT_VERSION3 = true ? "agent-0.0.47" : "agent-dev";
48443
49227
  function osFamily() {
48444
49228
  const p = platform4();
48445
49229
  if (p === "darwin") return "macos";
@@ -48482,10 +49266,8 @@ async function cmdSelfRegister() {
48482
49266
  process.stdout.write(` \x1B[2mgenerated UUIDv7 ${deviceUuid.slice(0, 8)}\u2026\x1B[0m
48483
49267
  `);
48484
49268
  }
48485
- process.stdout.write(
48486
- ` \x1B[2m\u2192 POST ${state.apiUrl}/v1/devices/self-register\x1B[0m
48487
- `
48488
- );
49269
+ process.stdout.write(` \x1B[2m\u2192 POST ${state.apiUrl}/v1/devices/self-register\x1B[0m
49270
+ `);
48489
49271
  const res = await selfRegister({
48490
49272
  device_uuid: deviceUuid,
48491
49273
  fingerprint
@@ -48499,8 +49281,10 @@ async function cmdSelfRegister() {
48499
49281
  });
48500
49282
  process.stdout.write(` \x1B[32m\u2713\x1B[0m registered device_id=${res.device_id}
48501
49283
  `);
48502
- process.stdout.write(` \x1B[32m\u2713\x1B[0m secret ${res.secret_prefix}\u2026 (hashed on server, never re-sent)
48503
- `);
49284
+ process.stdout.write(
49285
+ ` \x1B[32m\u2713\x1B[0m secret ${res.secret_prefix}\u2026 (hashed on server, never re-sent)
49286
+ `
49287
+ );
48504
49288
  process.stdout.write(` \x1B[32m\u2713\x1B[0m claim code ${res.claim_code}
48505
49289
  `);
48506
49290
  }
@@ -48533,10 +49317,8 @@ async function cmdAwaitClaim() {
48533
49317
  }
48534
49318
  function emitEvent(opts, event, fields = {}) {
48535
49319
  if (!opts.json) return;
48536
- process.stdout.write(
48537
- `${JSON.stringify({ v: 1, ts: Date.now(), event, ...fields })}
48538
- `
48539
- );
49320
+ process.stdout.write(`${JSON.stringify({ v: 1, ts: Date.now(), event, ...fields })}
49321
+ `);
48540
49322
  }
48541
49323
  async function cmdConnect(opts) {
48542
49324
  const step = (msg) => {
@@ -48639,9 +49421,7 @@ async function cmdConnect(opts) {
48639
49421
  error: e.message
48640
49422
  });
48641
49423
  warn(`couldn't prepare summariser model: ${e.message}`);
48642
- warn(
48643
- "the background service will retry the download on its first scan"
48644
- );
49424
+ warn("the background service will retry the download on its first scan");
48645
49425
  }
48646
49426
  step("Installing/refreshing background service so the agent survives reboots");
48647
49427
  let serviceOk = false;
@@ -48657,9 +49437,7 @@ async function cmdConnect(opts) {
48657
49437
  } catch (e) {
48658
49438
  emitEvent(opts, "service_install_failed", { error: e.message });
48659
49439
  warn(`couldn't install service: ${e.message}`);
48660
- warn(
48661
- "the agent will not run in the background \u2014 re-run after fixing the issue"
48662
- );
49440
+ warn("the agent will not run in the background \u2014 re-run after fixing the issue");
48663
49441
  }
48664
49442
  step("Detecting installed AI tools and signed-in accounts");
48665
49443
  let discovered = null;
@@ -48677,9 +49455,7 @@ async function cmdConnect(opts) {
48677
49455
  identities: d.identities.length
48678
49456
  };
48679
49457
  emitEvent(opts, "discovered", discovered);
48680
- ok(
48681
- `${discovered.installations} installs \xB7 ${discovered.identities} accounts`
48682
- );
49458
+ ok(`${discovered.installations} installs \xB7 ${discovered.identities} accounts`);
48683
49459
  } catch (e) {
48684
49460
  emitEvent(opts, "discovery_failed", { error: e.message });
48685
49461
  warn(`couldn't detect accounts: ${e.message}`);
@@ -48775,9 +49551,7 @@ async function cmdRescan() {
48775
49551
  console.log(
48776
49552
  `[modelstat] cursors wiped \u2014 next \`modelstat scan\` (or daemon scan cycle) will re-read every JSONL from the start and re-summarise every session at processing version v${PROCESSING_VERSION2}.`
48777
49553
  );
48778
- console.log(
48779
- " If the daemon is running, kick it with: modelstat stop && modelstat start"
48780
- );
49554
+ console.log(" If the daemon is running, kick it with: modelstat stop && modelstat start");
48781
49555
  }
48782
49556
  async function cmdWatch() {
48783
49557
  const { watchForever: watchForever2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
@@ -48833,10 +49607,10 @@ function fmtTokens(v) {
48833
49607
  }
48834
49608
  async function readLocalStatus() {
48835
49609
  try {
48836
- const { homedir: homedir9 } = await import("os");
48837
- const { join: join11 } = await import("path");
49610
+ const { homedir: homedir10 } = await import("os");
49611
+ const { join: join12 } = await import("path");
48838
49612
  const { readFile } = await import("fs/promises");
48839
- const p = join11(homedir9(), ".modelstat", "last-status.json");
49613
+ const p = join12(homedir10(), ".modelstat", "last-status.json");
48840
49614
  const txt = await readFile(p, "utf8");
48841
49615
  return JSON.parse(txt);
48842
49616
  } catch {
@@ -48879,7 +49653,9 @@ async function cmdStats(args) {
48879
49653
  console.log("device is claimed \u2014 live stats available at:");
48880
49654
  console.log(` ${dashboard}`);
48881
49655
  if (local) {
48882
- console.log(`local agent: ${local.status ?? "?"}${local.message ? ` \xB7 ${local.message}` : ""}`);
49656
+ console.log(
49657
+ `local agent: ${local.status ?? "?"}${local.message ? ` \xB7 ${local.message}` : ""}`
49658
+ );
48883
49659
  const stats = local.stats ?? {};
48884
49660
  for (const [k, v] of Object.entries(stats)) console.log(` ${k}: ${v}`);
48885
49661
  }
@@ -48897,7 +49673,9 @@ async function cmdStats(args) {
48897
49673
  console.log(
48898
49674
  `status: ${view.device.agent_status ?? "(unknown)"}${view.device.last_seen_at ? ` \xB7 last seen ${view.device.last_seen_at}` : ""}`
48899
49675
  );
48900
- console.log(`claim: ${view.status}${view.status === "unclaimed" ? ` (at ${view.claim_url})` : ""}`);
49676
+ console.log(
49677
+ `claim: ${view.status}${view.status === "unclaimed" ? ` (at ${view.claim_url})` : ""}`
49678
+ );
48901
49679
  console.log(`sessions: ${fmtInt(view.analyzed.count)}`);
48902
49680
  console.log(`tokens: ${fmtTokens(view.analyzed.totalTokens)}`);
48903
49681
  console.log(`cost: ${fmtCost(view.analyzed.totalCostUsd)}`);
@@ -48922,10 +49700,8 @@ async function cmdJobs(args) {
48922
49700
  if (!jobs && !ledger) {
48923
49701
  const dashboard = `${state.apiUrl.replace(/\/$/, "")}/dashboard/jobs`;
48924
49702
  if (asJson) {
48925
- process.stdout.write(
48926
- `${JSON.stringify({ paired: true, claimed: true, dashboard })}
48927
- `
48928
- );
49703
+ process.stdout.write(`${JSON.stringify({ paired: true, claimed: true, dashboard })}
49704
+ `);
48929
49705
  } else {
48930
49706
  console.log("device is claimed \u2014 job queue at:");
48931
49707
  console.log(` ${dashboard}`);
@@ -49015,6 +49791,14 @@ async function main() {
49015
49791
  console.log(`\u2713 runtime staged at ${dest}`);
49016
49792
  return;
49017
49793
  }
49794
+ case "_daemon-health": {
49795
+ try {
49796
+ console.log(JSON.stringify(daemonHealth({ myAgentVersion: AGENT_VERSION3 })));
49797
+ } catch (e) {
49798
+ console.log(JSON.stringify({ decision: "spawn", error: e.message }));
49799
+ }
49800
+ return;
49801
+ }
49018
49802
  // ── Diagnostics / dev one-shots ────────────────────────────
49019
49803
  case "status":
49020
49804
  return cmdStatus();
@@ -49042,10 +49826,18 @@ async function main() {
49042
49826
  return cmdAwaitClaim();
49043
49827
  default:
49044
49828
  console.log("usage:");
49045
- console.log(" npx modelstat@latest \u2014 install or upgrade. Registers the device, installs the background service, exits.");
49046
- console.log(" flags: --json (NDJSON events on stdout), --no-browser, --fresh, -y");
49047
- console.log(" npx modelstat@latest remove \u2014 stop and uninstall the background service. Keeps your identity.");
49048
- console.log(" npx modelstat@latest reinstall \u2014 alias for the default. Useful when you want to be explicit.");
49829
+ console.log(
49830
+ " npx modelstat@latest \u2014 install or upgrade. Registers the device, installs the background service, exits."
49831
+ );
49832
+ console.log(
49833
+ " flags: --json (NDJSON events on stdout), --no-browser, --fresh, -y"
49834
+ );
49835
+ console.log(
49836
+ " npx modelstat@latest remove \u2014 stop and uninstall the background service. Keeps your identity."
49837
+ );
49838
+ console.log(
49839
+ " npx modelstat@latest reinstall \u2014 alias for the default. Useful when you want to be explicit."
49840
+ );
49049
49841
  console.log();
49050
49842
  console.log("Diagnostics:");
49051
49843
  console.log(" npx modelstat@latest status \u2014 show pairing + service state");
@@ -49056,11 +49848,17 @@ async function main() {
49056
49848
  console.log();
49057
49849
  console.log("Dev / one-shots:");
49058
49850
  console.log(" npx modelstat@latest scan \u2014 one-shot parse + upload of local JSONL");
49059
- console.log(" npx modelstat@latest rescan \u2014 wipe file cursors so the next scan re-reads & re-summarises everything");
49060
- console.log(" npx modelstat@latest watch \u2014 continuous (chokidar) with periodic backstop");
49851
+ console.log(
49852
+ " npx modelstat@latest rescan \u2014 wipe file cursors so the next scan re-reads & re-summarises everything"
49853
+ );
49854
+ console.log(
49855
+ " npx modelstat@latest watch \u2014 continuous (chokidar) with periodic backstop"
49856
+ );
49061
49857
  console.log(" npx modelstat@latest discover \u2014 one-shot report of installs/identities");
49062
49858
  console.log();
49063
- console.log("Internal (called by launchd/systemd, not by humans): _daemon, start, self-register, await-claim");
49859
+ console.log(
49860
+ "Internal (called by launchd/systemd, not by humans): _daemon, start, self-register, await-claim"
49861
+ );
49064
49862
  process.exit(1);
49065
49863
  }
49066
49864
  }