modelstat 0.0.22 → 0.0.24

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
@@ -4339,6 +4339,18 @@ var init_schemas = __esm({
4339
4339
  tool_calls: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).default({}),
4340
4340
  // Files touched, relative to git root. Never absolute — scrubbed by agent.
4341
4341
  files_touched: external_exports.array(external_exports.string().max(512)).max(256).default([]),
4342
+ // Redacted excerpt of the conversation turn (user prompt or
4343
+ // assistant response). The PARSER is responsible for:
4344
+ // 1. Pulling a representative snippet from the turn (≤320 chars).
4345
+ // 2. Running it through @modelstat/core/redact PLUS, when
4346
+ // available, the on-device Privacy Filter adapter.
4347
+ // 3. Stripping code blocks and file-path noise.
4348
+ // Optional — events without it fall back to metadata-only abstracts
4349
+ // (the historical behaviour). The companion-core pipeline runs
4350
+ // redact() over it again as defence-in-depth before building the
4351
+ // summarize prompt; it never gets stored long-term server-side, only
4352
+ // used to construct the summarize input.
4353
+ content_excerpt: external_exports.string().max(320).optional(),
4342
4354
  // Reference to originating file for reparsing
4343
4355
  source_file: external_exports.string().max(1024).nullable(),
4344
4356
  source_byte_offset: external_exports.number().int().nonnegative().nullable(),
@@ -4354,7 +4366,7 @@ var init_schemas = __esm({
4354
4366
  secrets_found: external_exports.number().int().nonnegative().default(0),
4355
4367
  emails_redacted: external_exports.number().int().nonnegative().default(0),
4356
4368
  paths_redacted_absolute: external_exports.number().int().nonnegative().default(0)
4357
- });
4369
+ }).catchall(external_exports.number().int().nonnegative());
4358
4370
  TaxonomyHintRooted = external_exports.object({
4359
4371
  root_key: external_exports.string().max(60),
4360
4372
  name: external_exports.string().max(120),
@@ -4651,6 +4663,28 @@ import { createHash } from "crypto";
4651
4663
  import { createReadStream } from "fs";
4652
4664
  import { stat } from "fs/promises";
4653
4665
  import { createInterface } from "readline";
4666
+ function extractExcerpt(content) {
4667
+ if (!content) return void 0;
4668
+ let text = "";
4669
+ if (typeof content === "string") {
4670
+ text = content;
4671
+ } else if (Array.isArray(content)) {
4672
+ const parts = [];
4673
+ for (const block of content) {
4674
+ if (block && block.type === "text" && typeof block.text === "string") {
4675
+ parts.push(block.text);
4676
+ }
4677
+ }
4678
+ text = parts.join(" ");
4679
+ }
4680
+ if (!text) return void 0;
4681
+ text = text.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ");
4682
+ text = text.replace(/\s+/g, " ").trim();
4683
+ if (!text) return void 0;
4684
+ const cleaned = redact(text).text;
4685
+ const truncated = cleaned.slice(0, 320);
4686
+ return truncated.length > 0 ? truncated : void 0;
4687
+ }
4654
4688
  async function parseClaudeCodeJsonl(ctx) {
4655
4689
  const events = [];
4656
4690
  let rawLines = 0;
@@ -4701,6 +4735,7 @@ async function parseClaudeCodeJsonl(ctx) {
4701
4735
  continue;
4702
4736
  }
4703
4737
  const slug = guessRepoSlugFromPath(cwd);
4738
+ const excerpt = extractExcerpt(a.message?.content);
4704
4739
  events.push({
4705
4740
  source_event_id: sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart),
4706
4741
  ts: a.timestamp,
@@ -4729,6 +4764,7 @@ async function parseClaudeCodeJsonl(ctx) {
4729
4764
  duration_ms: null,
4730
4765
  tool_calls: {},
4731
4766
  files_touched: [],
4767
+ ...excerpt ? { content_excerpt: excerpt } : {},
4732
4768
  source_file: ctx.sourceFile,
4733
4769
  source_byte_offset: offsetAtLineStart,
4734
4770
  // Files in ~/.claude/projects/ come from the Claude Code app
@@ -4743,6 +4779,7 @@ async function parseClaudeCodeJsonl(ctx) {
4743
4779
  skipped += 1;
4744
4780
  continue;
4745
4781
  }
4782
+ const excerpt = extractExcerpt(u.message?.content);
4746
4783
  events.push({
4747
4784
  source_event_id: sourceEventId(ctx.deviceId, ctx.sourceFile, offsetAtLineStart),
4748
4785
  ts: u.timestamp,
@@ -4759,6 +4796,7 @@ async function parseClaudeCodeJsonl(ctx) {
4759
4796
  duration_ms: null,
4760
4797
  tool_calls: {},
4761
4798
  files_touched: [],
4799
+ ...excerpt ? { content_excerpt: excerpt } : {},
4762
4800
  source_file: ctx.sourceFile,
4763
4801
  source_byte_offset: offsetAtLineStart,
4764
4802
  billing: "subscription"
@@ -7487,6 +7525,107 @@ var init_src2 = __esm({
7487
7525
  }
7488
7526
  });
7489
7527
 
7528
+ // src/identity.ts
7529
+ import {
7530
+ chmodSync,
7531
+ mkdirSync,
7532
+ readFileSync as readFileSync2,
7533
+ renameSync,
7534
+ writeFileSync,
7535
+ existsSync as existsSync3
7536
+ } from "fs";
7537
+ import { homedir as homedir2, hostname as osHostname } from "os";
7538
+ import { join as join2 } from "path";
7539
+ function ensureRoot() {
7540
+ mkdirSync(ROOT, { recursive: true, mode: 448 });
7541
+ }
7542
+ function writeAtomic(meta) {
7543
+ ensureRoot();
7544
+ const tmp = `${IDENTITY_FILE}.${process.pid}.tmp`;
7545
+ writeFileSync(tmp, JSON.stringify(meta, null, 2), { mode: 384 });
7546
+ renameSync(tmp, IDENTITY_FILE);
7547
+ try {
7548
+ chmodSync(IDENTITY_FILE, 384);
7549
+ } catch {
7550
+ }
7551
+ }
7552
+ function identityPath() {
7553
+ return IDENTITY_FILE;
7554
+ }
7555
+ function hasIdentityFile() {
7556
+ return existsSync3(IDENTITY_FILE);
7557
+ }
7558
+ function parseFile() {
7559
+ try {
7560
+ const raw = readFileSync2(IDENTITY_FILE, "utf8");
7561
+ const obj = JSON.parse(raw);
7562
+ if (!obj.deviceUuid || !obj.deviceId || !obj.bearerToken) {
7563
+ return null;
7564
+ }
7565
+ return {
7566
+ deviceUuid: obj.deviceUuid,
7567
+ deviceId: obj.deviceId,
7568
+ bearerToken: obj.bearerToken,
7569
+ claimCode: obj.claimCode ?? null,
7570
+ claimUrl: obj.claimUrl ?? null,
7571
+ hostname: obj.hostname ?? osHostname(),
7572
+ createdAt: obj.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7573
+ userEmail: obj.userEmail ?? null,
7574
+ defaultOrgId: obj.defaultOrgId ?? null
7575
+ };
7576
+ } catch {
7577
+ return null;
7578
+ }
7579
+ }
7580
+ function loadIdentity(migrateFromConf2) {
7581
+ const fromFile = parseFile();
7582
+ if (fromFile) return fromFile;
7583
+ if (!migrateFromConf2) return null;
7584
+ const legacy = migrateFromConf2();
7585
+ if (!legacy) return null;
7586
+ if (!legacy.deviceUuid || !legacy.deviceId || !legacy.bearerToken) {
7587
+ return null;
7588
+ }
7589
+ const migrated = {
7590
+ deviceUuid: legacy.deviceUuid,
7591
+ deviceId: legacy.deviceId,
7592
+ bearerToken: legacy.bearerToken,
7593
+ claimCode: legacy.claimCode ?? null,
7594
+ claimUrl: legacy.claimUrl ?? null,
7595
+ hostname: osHostname(),
7596
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7597
+ userEmail: legacy.userEmail ?? null,
7598
+ defaultOrgId: legacy.defaultOrgId ?? null
7599
+ };
7600
+ writeAtomic(migrated);
7601
+ return migrated;
7602
+ }
7603
+ function saveIdentity(meta) {
7604
+ writeAtomic(meta);
7605
+ }
7606
+ function backupIdentity() {
7607
+ if (!existsSync3(IDENTITY_FILE)) return null;
7608
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7609
+ const dest = `${IDENTITY_FILE}.bak-${stamp}`;
7610
+ renameSync(IDENTITY_FILE, dest);
7611
+ return dest;
7612
+ }
7613
+ function updateIdentity(patch) {
7614
+ const current = parseFile();
7615
+ if (!current) return null;
7616
+ const merged = { ...current, ...patch };
7617
+ writeAtomic(merged);
7618
+ return merged;
7619
+ }
7620
+ var ROOT, IDENTITY_FILE;
7621
+ var init_identity = __esm({
7622
+ "src/identity.ts"() {
7623
+ "use strict";
7624
+ ROOT = join2(homedir2(), ".modelstat");
7625
+ IDENTITY_FILE = join2(ROOT, "identity.json");
7626
+ }
7627
+ });
7628
+
7490
7629
  // ../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/core/symbols.js
7491
7630
  var require_symbols = __commonJS({
7492
7631
  "../../node_modules/.pnpm/undici@7.25.0/node_modules/undici/lib/core/symbols.js"(exports, module) {
@@ -33061,15 +33200,15 @@ function envPaths(name, { suffix = "nodejs" } = {}) {
33061
33200
  }
33062
33201
  return linux(name);
33063
33202
  }
33064
- var homedir2, tmpdir, env, macos, windows, linux;
33203
+ var homedir3, tmpdir, env, macos, windows, linux;
33065
33204
  var init_env_paths = __esm({
33066
33205
  "../../node_modules/.pnpm/env-paths@3.0.0/node_modules/env-paths/index.js"() {
33067
33206
  "use strict";
33068
- homedir2 = os.homedir();
33207
+ homedir3 = os.homedir();
33069
33208
  tmpdir = os.tmpdir();
33070
33209
  ({ env } = process2);
33071
33210
  macos = (name) => {
33072
- const library = path.join(homedir2, "Library");
33211
+ const library = path.join(homedir3, "Library");
33073
33212
  return {
33074
33213
  data: path.join(library, "Application Support", name),
33075
33214
  config: path.join(library, "Preferences", name),
@@ -33079,8 +33218,8 @@ var init_env_paths = __esm({
33079
33218
  };
33080
33219
  };
33081
33220
  windows = (name) => {
33082
- const appData = env.APPDATA || path.join(homedir2, "AppData", "Roaming");
33083
- const localAppData = env.LOCALAPPDATA || path.join(homedir2, "AppData", "Local");
33221
+ const appData = env.APPDATA || path.join(homedir3, "AppData", "Roaming");
33222
+ const localAppData = env.LOCALAPPDATA || path.join(homedir3, "AppData", "Local");
33084
33223
  return {
33085
33224
  // Data/config/cache/log are invented by me as Windows isn't opinionated about this
33086
33225
  data: path.join(localAppData, name, "Data"),
@@ -33091,13 +33230,13 @@ var init_env_paths = __esm({
33091
33230
  };
33092
33231
  };
33093
33232
  linux = (name) => {
33094
- const username = path.basename(homedir2);
33233
+ const username = path.basename(homedir3);
33095
33234
  return {
33096
- data: path.join(env.XDG_DATA_HOME || path.join(homedir2, ".local", "share"), name),
33097
- config: path.join(env.XDG_CONFIG_HOME || path.join(homedir2, ".config"), name),
33098
- cache: path.join(env.XDG_CACHE_HOME || path.join(homedir2, ".cache"), name),
33235
+ data: path.join(env.XDG_DATA_HOME || path.join(homedir3, ".local", "share"), name),
33236
+ config: path.join(env.XDG_CONFIG_HOME || path.join(homedir3, ".config"), name),
33237
+ cache: path.join(env.XDG_CACHE_HOME || path.join(homedir3, ".cache"), name),
33099
33238
  // https://wiki.debian.org/XDGBaseDirectorySpecification#state
33100
- log: path.join(env.XDG_STATE_HOME || path.join(homedir2, ".local", "state"), name),
33239
+ log: path.join(env.XDG_STATE_HOME || path.join(homedir3, ".local", "state"), name),
33101
33240
  temp: path.join(tmpdir, username, name)
33102
33241
  };
33103
33242
  };
@@ -33538,9 +33677,9 @@ import { once } from "events";
33538
33677
  import { createWriteStream } from "fs";
33539
33678
  import path3 from "path";
33540
33679
  import { Readable } from "stream";
33541
- function writeFileSync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
33680
+ function writeFileSync2(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
33542
33681
  if (isString(options))
33543
- return writeFileSync(filePath, data, { encoding: options });
33682
+ return writeFileSync2(filePath, data, { encoding: options });
33544
33683
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
33545
33684
  const retryOptions = { timeout };
33546
33685
  let tempDisposer = null;
@@ -43663,7 +43802,7 @@ var init_source = __esm({
43663
43802
  fs2.writeFileSync(this.path, data, { mode: this.#options.configFileMode });
43664
43803
  } else {
43665
43804
  try {
43666
- writeFileSync(this.path, data, { mode: this.#options.configFileMode });
43805
+ writeFileSync2(this.path, data, { mode: this.#options.configFileMode });
43667
43806
  } catch (error) {
43668
43807
  if (error?.code === "EXDEV") {
43669
43808
  fs2.writeFileSync(this.path, data, { mode: this.#options.configFileMode });
@@ -43765,20 +43904,52 @@ var init_source = __esm({
43765
43904
  });
43766
43905
 
43767
43906
  // src/config.ts
43768
- import { existsSync as existsSync3 } from "fs";
43907
+ import { existsSync as existsSync4 } from "fs";
43769
43908
  import { hostname } from "os";
43770
43909
  import { dirname as dirname2, resolve as resolve3 } from "path";
43771
43910
  import { fileURLToPath } from "url";
43772
- var import_dotenv, here, DEFAULT_API_URL, LEGACY_LOCALHOST_API, store, state;
43911
+ function migrateFromConf() {
43912
+ const bearer = store.get("bearerToken");
43913
+ const deviceUuid = store.get("deviceUuid");
43914
+ const deviceId = store.get("deviceId");
43915
+ if (!bearer || !deviceUuid || !deviceId) return null;
43916
+ return {
43917
+ bearerToken: bearer,
43918
+ deviceId,
43919
+ deviceUuid,
43920
+ claimCode: store.get("claimCode"),
43921
+ claimUrl: store.get("claimUrl"),
43922
+ userEmail: store.get("userEmail"),
43923
+ defaultOrgId: store.get("defaultOrgId")
43924
+ };
43925
+ }
43926
+ function clearConfIdentity() {
43927
+ store.set("bearerToken", null);
43928
+ store.set("deviceId", null);
43929
+ store.set("deviceUuid", null);
43930
+ store.set("claimCode", null);
43931
+ store.set("claimUrl", null);
43932
+ }
43933
+ function writeThrough(patch) {
43934
+ if (!cachedIdentity) {
43935
+ throw new Error(
43936
+ "config: no identity yet \u2014 call state.saveFreshIdentity() first"
43937
+ );
43938
+ }
43939
+ cachedIdentity = { ...cachedIdentity, ...patch };
43940
+ updateIdentity(patch);
43941
+ }
43942
+ var import_dotenv, here, DEFAULT_API_URL, LEGACY_LOCALHOST_API, store, cachedIdentity, state;
43773
43943
  var init_config2 = __esm({
43774
43944
  "src/config.ts"() {
43775
43945
  "use strict";
43776
43946
  import_dotenv = __toESM(require_main(), 1);
43777
43947
  init_source();
43948
+ init_identity();
43778
43949
  here = dirname2(fileURLToPath(import.meta.url));
43779
43950
  for (let d = here, i = 0; i < 8; i++, d = resolve3(d, "..")) {
43780
43951
  const candidate = resolve3(d, ".env");
43781
- if (existsSync3(candidate)) {
43952
+ if (existsSync4(candidate)) {
43782
43953
  (0, import_dotenv.config)({ path: candidate });
43783
43954
  break;
43784
43955
  }
@@ -43804,6 +43975,14 @@ var init_config2 = __esm({
43804
43975
  cursor: {}
43805
43976
  }
43806
43977
  });
43978
+ cachedIdentity = (() => {
43979
+ const had = store.get("bearerToken") !== null;
43980
+ const id = loadIdentity(migrateFromConf);
43981
+ if (id && had && !store.path.includes("fallback")) {
43982
+ clearConfIdentity();
43983
+ }
43984
+ return id;
43985
+ })();
43807
43986
  state = {
43808
43987
  /** Resolution order: env var → stored value (if user ran `setApiUrl`
43809
43988
  * or paired pre-0.0.8) → production default. The legacy localhost
@@ -43817,42 +43996,66 @@ var init_config2 = __esm({
43817
43996
  setApiUrl(v) {
43818
43997
  store.set("apiUrl", v);
43819
43998
  },
43999
+ // ── Identity: backed by ~/.modelstat/identity.json ─────────────
44000
+ /** Seed a fresh identity after a successful self-register. Writes
44001
+ * the file atomically; use `state.backupAndReset()` first if
44002
+ * overwriting an existing identity. */
44003
+ saveFreshIdentity(meta) {
44004
+ const id = {
44005
+ deviceUuid: meta.deviceUuid,
44006
+ deviceId: meta.deviceId,
44007
+ bearerToken: meta.bearerToken,
44008
+ claimCode: meta.claimCode,
44009
+ claimUrl: meta.claimUrl,
44010
+ hostname: hostname(),
44011
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
44012
+ userEmail: null,
44013
+ defaultOrgId: null
44014
+ };
44015
+ saveIdentity(id);
44016
+ cachedIdentity = id;
44017
+ },
44018
+ get identity() {
44019
+ return cachedIdentity;
44020
+ },
43820
44021
  get bearer() {
43821
- return store.get("bearerToken");
44022
+ return cachedIdentity?.bearerToken ?? null;
43822
44023
  },
43823
44024
  setBearer(v) {
43824
- store.set("bearerToken", v);
44025
+ if (v === null) {
44026
+ cachedIdentity = null;
44027
+ return;
44028
+ }
44029
+ writeThrough({ bearerToken: v });
43825
44030
  },
43826
44031
  get deviceId() {
43827
- return store.get("deviceId");
43828
- },
43829
- setDeviceId(v) {
43830
- store.set("deviceId", v);
43831
- },
43832
- get userEmail() {
43833
- return store.get("userEmail");
43834
- },
43835
- setUserEmail(v) {
43836
- store.set("userEmail", v);
44032
+ return cachedIdentity?.deviceId ?? null;
43837
44033
  },
43838
44034
  get deviceUuid() {
43839
- return store.get("deviceUuid");
43840
- },
43841
- setDeviceUuid(v) {
43842
- store.set("deviceUuid", v);
44035
+ return cachedIdentity?.deviceUuid ?? null;
43843
44036
  },
43844
44037
  get claimCode() {
43845
- return store.get("claimCode");
44038
+ return cachedIdentity?.claimCode ?? null;
43846
44039
  },
43847
44040
  setClaimCode(v) {
43848
- store.set("claimCode", v);
44041
+ if (!cachedIdentity) return;
44042
+ writeThrough({ claimCode: v });
43849
44043
  },
43850
44044
  get claimUrl() {
43851
- return store.get("claimUrl");
44045
+ return cachedIdentity?.claimUrl ?? null;
43852
44046
  },
43853
44047
  setClaimUrl(v) {
43854
- store.set("claimUrl", v);
44048
+ if (!cachedIdentity) return;
44049
+ writeThrough({ claimUrl: v });
43855
44050
  },
44051
+ get userEmail() {
44052
+ return cachedIdentity?.userEmail ?? null;
44053
+ },
44054
+ setUserEmail(v) {
44055
+ if (!cachedIdentity) return;
44056
+ writeThrough({ userEmail: v });
44057
+ },
44058
+ // ── Runtime state: stays in conf ───────────────────────────────
43856
44059
  getCursor(path5) {
43857
44060
  return store.get("cursor")[path5];
43858
44061
  },
@@ -44032,9 +44235,9 @@ var OLLAMA_CHAT_MODEL, OLLAMA_EMBED_MODEL, SUMMARISER_SYSTEM_PROMPT, SUMMARISER_
44032
44235
  var init_prompts = __esm({
44033
44236
  "../../packages/companion-core/src/pipeline/prompts.ts"() {
44034
44237
  "use strict";
44035
- OLLAMA_CHAT_MODEL = "qwen3:0.6b";
44238
+ OLLAMA_CHAT_MODEL = "qwen3.5:0.8b";
44036
44239
  OLLAMA_EMBED_MODEL = "bge-small-en-v1.5";
44037
- SUMMARISER_SYSTEM_PROMPT = "You summarise an AI coding session in ONE sentence, \u2264 240 characters. Focus on what the human was trying to accomplish. No quotes, no PII, no code literals, no file paths. Reply with only the sentence.";
44240
+ 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.";
44038
44241
  SUMMARISER_MAX_TOKENS = 120;
44039
44242
  SUMMARISER_TEMPERATURE = 0.2;
44040
44243
  QWEN_CHARS_PER_TOKEN = 3.3;
@@ -44145,7 +44348,14 @@ async function summariseSlice(sessionId, slice, adapters2) {
44145
44348
  first.files_touched?.length ? `files touched: ${first.files_touched.slice(0, 5).join(", ")}` : null,
44146
44349
  Object.keys(first.tool_calls ?? {}).length ? `tool calls: ${Object.keys(first.tool_calls).slice(0, 5).join(", ")}` : null
44147
44350
  ].filter(Boolean).join("; ");
44148
- const prompt = `Session context: ${promptFacts || "generic coding session"}.
44351
+ const excerpts = sampleAndRedactExcerpts(slice);
44352
+ const excerptBlock = excerpts.length ? excerpts.map((e, i) => ` [turn ${i + 1}] "${e.replace(/\s+/g, " ").trim()}"`).join("\n") : "";
44353
+ const prompt = excerptBlock ? `Session context: ${promptFacts || "generic coding session"}.
44354
+
44355
+ Sampled excerpts from the conversation (already redacted of PII and secrets):
44356
+ ${excerptBlock}
44357
+
44358
+ Write ONE sentence (\u2264240 chars) describing what the human was working on. Focus on the substance \u2014 what was being built, debugged, or designed. No quotes, no PII, no code literals, no file paths.` : `Session context: ${promptFacts || "generic coding session"}.
44149
44359
  Write one sentence describing what the human was doing.`;
44150
44360
  let rawAbstract;
44151
44361
  try {
@@ -44153,7 +44363,20 @@ Write one sentence describing what the human was doing.`;
44153
44363
  } catch {
44154
44364
  rawAbstract = promptFacts || `${first.tool} session with ${slice.length} turns`;
44155
44365
  }
44156
- const redacted = redact(rawAbstract);
44366
+ const regexPass = redact(rawAbstract);
44367
+ let abstractText = regexPass.text;
44368
+ const counts = { ...regexPass.counts };
44369
+ if (adapters2.redact) {
44370
+ try {
44371
+ const modelPass = await adapters2.redact(regexPass.text);
44372
+ abstractText = modelPass.text;
44373
+ for (const [k, v] of Object.entries(modelPass.counts)) {
44374
+ if (k.startsWith("pf_")) counts[k] = v;
44375
+ }
44376
+ } catch {
44377
+ }
44378
+ }
44379
+ const redacted = { text: abstractText, counts };
44157
44380
  const tags = [
44158
44381
  { root_key: "tools", name: first.tool, confidence: 1 },
44159
44382
  { root_key: "providers", name: first.provider, confidence: 1 }
@@ -44194,11 +44417,39 @@ Write one sentence describing what the human was doing.`;
44194
44417
  abstract: redacted.text.slice(0, ABSTRACT_MAX_CHARS),
44195
44418
  tokens,
44196
44419
  tags,
44420
+ // counts is `Record<string, number>` after the optional model
44421
+ // merge; the schema's RedactionReport requires the three regex
44422
+ // counters (always populated from regexPass.counts) plus a
44423
+ // number-valued catchall for pf_*.
44197
44424
  redaction: redacted.counts,
44198
44425
  source_event_ids: sourceEventIds,
44199
44426
  abstract_embedding: segmentEmbedding && segmentEmbedding.length === 384 ? segmentEmbedding : void 0
44200
44427
  };
44201
44428
  }
44429
+ function sampleAndRedactExcerpts(slice) {
44430
+ const withContent = [];
44431
+ for (let i = 0; i < slice.length; i++) {
44432
+ const c = slice[i]?.content_excerpt;
44433
+ if (c && c.trim().length > 0) withContent.push({ idx: i, text: c });
44434
+ }
44435
+ if (withContent.length === 0) return [];
44436
+ const picks = [0];
44437
+ if (withContent.length > 1) picks.push(withContent.length - 1);
44438
+ for (const frac of [0.25, 0.5, 0.75]) {
44439
+ const idx = Math.floor(withContent.length * frac);
44440
+ if (!picks.includes(idx)) picks.push(idx);
44441
+ if (picks.length >= 5) break;
44442
+ }
44443
+ picks.sort((a, b) => a - b);
44444
+ const out = [];
44445
+ for (const i of picks) {
44446
+ const raw = withContent[i]?.text;
44447
+ if (!raw) continue;
44448
+ const redacted = redact(raw).text;
44449
+ out.push(redacted.slice(0, 200));
44450
+ }
44451
+ return out;
44452
+ }
44202
44453
  function turnSurface(e) {
44203
44454
  const parts = [e.kind, e.tool];
44204
44455
  if (e.model) parts.push(e.model);
@@ -44402,23 +44653,23 @@ var init_pipeline2 = __esm({
44402
44653
 
44403
44654
  // src/scan.ts
44404
44655
  import { readdir, stat as stat2 } from "fs/promises";
44405
- import { homedir as homedir3 } from "os";
44406
- import { join as join2 } from "path";
44656
+ import { homedir as homedir4 } from "os";
44657
+ import { join as join3 } from "path";
44407
44658
  async function scanAll(cb = {}) {
44408
44659
  const deviceId = state.deviceId;
44409
44660
  if (!deviceId) throw new Error("agent not enrolled \u2014 run `register` first");
44410
44661
  const jobs = [];
44411
44662
  try {
44412
- const base = join2(homedir3(), ".claude/projects");
44663
+ const base = join3(homedir4(), ".claude/projects");
44413
44664
  const projects = await readdir(base).catch(() => []);
44414
44665
  for (const p of projects) {
44415
- const dir = join2(base, p);
44666
+ const dir = join3(base, p);
44416
44667
  const ds = await stat2(dir).catch(() => null);
44417
44668
  if (!ds?.isDirectory()) continue;
44418
44669
  const files = await readdir(dir);
44419
44670
  for (const f of files) {
44420
44671
  if (!f.endsWith(".jsonl")) continue;
44421
- const full = join2(dir, f);
44672
+ const full = join3(dir, f);
44422
44673
  jobs.push({
44423
44674
  path: full,
44424
44675
  parse: async () => {
@@ -44432,17 +44683,17 @@ async function scanAll(cb = {}) {
44432
44683
  console.warn("claude scan skipped:", e.message);
44433
44684
  }
44434
44685
  try {
44435
- const base = join2(homedir3(), ".codex/sessions");
44686
+ const base = join3(homedir4(), ".codex/sessions");
44436
44687
  const years = await readdir(base).catch(() => []);
44437
44688
  for (const y of years) {
44438
- const months = await readdir(join2(base, y)).catch(() => []);
44689
+ const months = await readdir(join3(base, y)).catch(() => []);
44439
44690
  for (const m of months) {
44440
- const days = await readdir(join2(base, y, m)).catch(() => []);
44691
+ const days = await readdir(join3(base, y, m)).catch(() => []);
44441
44692
  for (const d of days) {
44442
- const files = await readdir(join2(base, y, m, d)).catch(() => []);
44693
+ const files = await readdir(join3(base, y, m, d)).catch(() => []);
44443
44694
  for (const f of files) {
44444
44695
  if (!f.startsWith("rollout-") || !f.endsWith(".jsonl")) continue;
44445
- const full = join2(base, y, m, d, f);
44696
+ const full = join3(base, y, m, d, f);
44446
44697
  jobs.push({
44447
44698
  path: full,
44448
44699
  parse: async () => {
@@ -44516,7 +44767,7 @@ var init_scan = __esm({
44516
44767
  init_pipeline2();
44517
44768
  init_config2();
44518
44769
  init_api();
44519
- AGENT_VERSION = "agent-dev-0.0.22";
44770
+ AGENT_VERSION = "agent-dev-0.0.23";
44520
44771
  BATCH_MAX_EVENTS = 2e3;
44521
44772
  }
44522
44773
  });
@@ -44524,17 +44775,17 @@ var init_scan = __esm({
44524
44775
  // src/lock.ts
44525
44776
  import {
44526
44777
  closeSync,
44527
- existsSync as existsSync5,
44528
- mkdirSync as mkdirSync2,
44778
+ existsSync as existsSync6,
44779
+ mkdirSync as mkdirSync3,
44529
44780
  openSync,
44530
- readFileSync as readFileSync2,
44531
- renameSync,
44781
+ readFileSync as readFileSync3,
44782
+ renameSync as renameSync2,
44532
44783
  unlinkSync as unlinkSync2,
44533
- writeFileSync as writeFileSync3,
44784
+ writeFileSync as writeFileSync4,
44534
44785
  writeSync
44535
44786
  } from "fs";
44536
- import { homedir as homedir5 } from "os";
44537
- import { join as join4 } from "path";
44787
+ import { homedir as homedir6 } from "os";
44788
+ import { join as join5 } from "path";
44538
44789
  function isProcessAlive(pid) {
44539
44790
  if (!pid || pid <= 0) return false;
44540
44791
  try {
@@ -44548,7 +44799,7 @@ function isProcessAlive(pid) {
44548
44799
  }
44549
44800
  function readLock() {
44550
44801
  try {
44551
- const raw = readFileSync2(LOCK_FILE, "utf8");
44802
+ const raw = readFileSync3(LOCK_FILE, "utf8");
44552
44803
  const obj = JSON.parse(raw);
44553
44804
  if (typeof obj.pid !== "number") return null;
44554
44805
  return {
@@ -44562,7 +44813,7 @@ function readLock() {
44562
44813
  }
44563
44814
  }
44564
44815
  function writeLockAtomic(meta) {
44565
- mkdirSync2(LOCK_DIR, { recursive: true });
44816
+ mkdirSync3(LOCK_DIR, { recursive: true });
44566
44817
  const tmp = `${LOCK_FILE}.${meta.pid}.${Date.now()}.tmp`;
44567
44818
  const fd = openSync(tmp, "wx");
44568
44819
  try {
@@ -44570,7 +44821,7 @@ function writeLockAtomic(meta) {
44570
44821
  } finally {
44571
44822
  closeSync(fd);
44572
44823
  }
44573
- renameSync(tmp, LOCK_FILE);
44824
+ renameSync2(tmp, LOCK_FILE);
44574
44825
  }
44575
44826
  function removeLockIfOwned(ownerPid) {
44576
44827
  const lock = readLock();
@@ -44635,8 +44886,8 @@ var LOCK_DIR, LOCK_FILE;
44635
44886
  var init_lock = __esm({
44636
44887
  "src/lock.ts"() {
44637
44888
  "use strict";
44638
- LOCK_DIR = join4(homedir5(), ".modelstat");
44639
- LOCK_FILE = join4(LOCK_DIR, "daemon.lock");
44889
+ LOCK_DIR = join5(homedir6(), ".modelstat");
44890
+ LOCK_FILE = join5(LOCK_DIR, "daemon.lock");
44640
44891
  }
44641
44892
  });
44642
44893
 
@@ -46366,7 +46617,7 @@ __export(daemon_exports, {
46366
46617
  setProgress: () => setProgress,
46367
46618
  setQueue: () => setQueue
46368
46619
  });
46369
- import { existsSync as existsSync6, statSync as statSync2 } from "fs";
46620
+ import { existsSync as existsSync7, statSync as statSync2 } from "fs";
46370
46621
  function setPhase(phase, message) {
46371
46622
  status.phase = phase;
46372
46623
  status.message = message ?? null;
@@ -46488,21 +46739,21 @@ async function runDaemon(opts = {}) {
46488
46739
  await runDiscovery();
46489
46740
  await runScanCycle("startup");
46490
46741
  const chokidar = (await Promise.resolve().then(() => (init_esm2(), esm_exports))).default;
46491
- const { homedir: homedir7, platform: platform5 } = await import("os");
46492
- const { join: join8 } = await import("path");
46493
- const home2 = homedir7();
46742
+ const { homedir: homedir8, platform: platform5 } = await import("os");
46743
+ const { join: join9 } = await import("path");
46744
+ const home2 = homedir8();
46494
46745
  const dirs = [
46495
- join8(home2, ".claude/projects"),
46496
- join8(home2, ".codex/sessions"),
46497
- join8(home2, ".cursor/ai-tracking"),
46498
- join8(home2, ".gemini"),
46746
+ join9(home2, ".claude/projects"),
46747
+ join9(home2, ".codex/sessions"),
46748
+ join9(home2, ".cursor/ai-tracking"),
46749
+ join9(home2, ".gemini"),
46499
46750
  ...platform5() === "darwin" ? [
46500
- join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46501
- join8(home2, "Library/Application Support/Claude")
46751
+ join9(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46752
+ join9(home2, "Library/Application Support/Claude")
46502
46753
  ] : [
46503
- join8(home2, ".config/Cursor/User/workspaceStorage")
46754
+ join9(home2, ".config/Cursor/User/workspaceStorage")
46504
46755
  ]
46505
- ].filter((p) => existsSync6(p) && statSync2(p).isDirectory());
46756
+ ].filter((p) => existsSync7(p) && statSync2(p).isDirectory());
46506
46757
  setPhase("watching", `Watching ${dirs.length} directories`);
46507
46758
  const watcher = chokidar.watch(dirs, {
46508
46759
  persistent: true,
@@ -46549,7 +46800,7 @@ var init_daemon = __esm({
46549
46800
  init_config2();
46550
46801
  init_lock();
46551
46802
  init_scan();
46552
- AGENT_VERSION2 = "agent-dev-0.0.22";
46803
+ AGENT_VERSION2 = "agent-dev-0.0.23";
46553
46804
  HEARTBEAT_INTERVAL_MS = 1e4;
46554
46805
  SCAN_INTERVAL_MS = 5 * 60 * 1e3;
46555
46806
  status = {
@@ -46569,37 +46820,37 @@ var watch_exports = {};
46569
46820
  __export(watch_exports, {
46570
46821
  watchForever: () => watchForever
46571
46822
  });
46572
- import { existsSync as existsSync7 } from "fs";
46573
- import { homedir as homedir6, platform as platform3 } from "os";
46574
- import { join as join7 } from "path";
46823
+ import { existsSync as existsSync8 } from "fs";
46824
+ import { homedir as homedir7, platform as platform3 } from "os";
46825
+ import { join as join8 } from "path";
46575
46826
  function resolveWatchDirs() {
46576
- const home2 = homedir6();
46577
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(home2, ".config");
46578
- const xdgData = process.env.XDG_DATA_HOME ?? join7(home2, ".local/share");
46827
+ const home2 = homedir7();
46828
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(home2, ".config");
46829
+ const xdgData = process.env.XDG_DATA_HOME ?? join8(home2, ".local/share");
46579
46830
  const candidates = [
46580
46831
  // universal (default HOME-rooted CLI data dirs)
46581
- join7(home2, ".claude/projects"),
46582
- join7(home2, ".codex/sessions"),
46583
- join7(home2, ".cursor/ai-tracking"),
46584
- join7(home2, ".gemini"),
46585
- join7(home2, ".aider"),
46832
+ join8(home2, ".claude/projects"),
46833
+ join8(home2, ".codex/sessions"),
46834
+ join8(home2, ".cursor/ai-tracking"),
46835
+ join8(home2, ".gemini"),
46836
+ join8(home2, ".aider"),
46586
46837
  // XDG / Linux
46587
- join7(xdgConfig, "claude/projects"),
46588
- join7(xdgConfig, "codex/sessions"),
46589
- join7(xdgConfig, "Cursor/User/workspaceStorage"),
46590
- join7(xdgConfig, "Code/User/workspaceStorage"),
46591
- join7(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46592
- join7(xdgData, "claude/projects"),
46838
+ join8(xdgConfig, "claude/projects"),
46839
+ join8(xdgConfig, "codex/sessions"),
46840
+ join8(xdgConfig, "Cursor/User/workspaceStorage"),
46841
+ join8(xdgConfig, "Code/User/workspaceStorage"),
46842
+ join8(xdgConfig, "Code - Insiders/User/workspaceStorage"),
46843
+ join8(xdgData, "claude/projects"),
46593
46844
  // macOS
46594
46845
  ...platform3() === "darwin" ? [
46595
- join7(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46596
- join7(home2, "Library/Application Support/Claude"),
46597
- join7(home2, "Library/Application Support/Code/User/workspaceStorage"),
46598
- join7(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46599
- join7(home2, "Library/Application Support/Zed")
46846
+ join8(home2, "Library/Application Support/Cursor/User/workspaceStorage"),
46847
+ join8(home2, "Library/Application Support/Claude"),
46848
+ join8(home2, "Library/Application Support/Code/User/workspaceStorage"),
46849
+ join8(home2, "Library/Application Support/Windsurf/User/workspaceStorage"),
46850
+ join8(home2, "Library/Application Support/Zed")
46600
46851
  ] : []
46601
46852
  ];
46602
- return Array.from(new Set(candidates)).filter((p) => existsSync7(p));
46853
+ return Array.from(new Set(candidates)).filter((p) => existsSync8(p));
46603
46854
  }
46604
46855
  async function safeScan(reason) {
46605
46856
  if (scanning) {
@@ -46659,51 +46910,53 @@ var init_watch = __esm({
46659
46910
 
46660
46911
  // src/cli.ts
46661
46912
  init_src2();
46913
+ init_identity();
46662
46914
  init_api();
46663
46915
  init_config2();
46664
46916
  init_scan();
46665
46917
  import { spawn } from "child_process";
46666
46918
  import { randomBytes } from "crypto";
46667
46919
  import { platform as platform4, release, arch as cpuArch, hostname as hostname2 } from "os";
46920
+ import { createInterface as createInterface3 } from "readline";
46668
46921
 
46669
46922
  // src/service.ts
46670
46923
  import { spawnSync } from "child_process";
46671
46924
  import {
46672
46925
  copyFileSync,
46673
- existsSync as existsSync4,
46674
- mkdirSync,
46926
+ existsSync as existsSync5,
46927
+ mkdirSync as mkdirSync2,
46675
46928
  unlinkSync,
46676
- writeFileSync as writeFileSync2
46929
+ writeFileSync as writeFileSync3
46677
46930
  } from "fs";
46678
- import { homedir as homedir4, platform as platform2, userInfo } from "os";
46679
- import { dirname as dirname4, join as join3 } from "path";
46931
+ import { homedir as homedir5, platform as platform2, userInfo } from "os";
46932
+ import { dirname as dirname4, join as join4 } from "path";
46680
46933
  import { fileURLToPath as fileURLToPath2 } from "url";
46681
46934
  var SERVICE_LABEL = "ai.modelstat.agent";
46682
46935
  var SYSTEMD_UNIT = "modelstat";
46683
46936
  function home() {
46684
- return homedir4();
46937
+ return homedir5();
46685
46938
  }
46686
46939
  function stateDir() {
46687
- return join3(home(), ".modelstat");
46940
+ return join4(home(), ".modelstat");
46688
46941
  }
46689
46942
  function binDir() {
46690
- return join3(stateDir(), "bin");
46943
+ return join4(stateDir(), "bin");
46691
46944
  }
46692
46945
  function logDir() {
46693
- return join3(stateDir(), "logs");
46946
+ return join4(stateDir(), "logs");
46694
46947
  }
46695
46948
  function installedCliPath() {
46696
- return join3(binDir(), "modelstat.mjs");
46949
+ return join4(binDir(), "modelstat.mjs");
46697
46950
  }
46698
46951
  function runningCliPath() {
46699
46952
  return fileURLToPath2(import.meta.url).replace(/service\.(mjs|js|ts)$/, "cli.mjs");
46700
46953
  }
46701
46954
  function installBundle() {
46702
- mkdirSync(binDir(), { recursive: true });
46703
- mkdirSync(logDir(), { recursive: true });
46955
+ mkdirSync2(binDir(), { recursive: true });
46956
+ mkdirSync2(logDir(), { recursive: true });
46704
46957
  const src = runningCliPath();
46705
46958
  const dest = installedCliPath();
46706
- if (!existsSync4(src)) {
46959
+ if (!existsSync5(src)) {
46707
46960
  throw new Error(
46708
46961
  `Can't find the CLI bundle to install from (${src}). Are you running a local dev build?`
46709
46962
  );
@@ -46715,21 +46968,21 @@ function nodeBinary() {
46715
46968
  return process.execPath;
46716
46969
  }
46717
46970
  function plistPath() {
46718
- return join3(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
46971
+ return join4(home(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
46719
46972
  }
46720
46973
  function locateTrayExecutable() {
46721
46974
  const candidates = [
46722
- join3(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
46975
+ join4(home(), "Applications", "ModelstatTray.app", "Contents", "MacOS", "modelstat-tray"),
46723
46976
  "/Applications/ModelstatTray.app/Contents/MacOS/modelstat-tray"
46724
46977
  ];
46725
46978
  for (const p of candidates) {
46726
- if (existsSync4(p)) return p;
46979
+ if (existsSync5(p)) return p;
46727
46980
  }
46728
46981
  return null;
46729
46982
  }
46730
46983
  function writePlist(cliPath) {
46731
46984
  const p = plistPath();
46732
- mkdirSync(dirname4(p), { recursive: true });
46985
+ mkdirSync2(dirname4(p), { recursive: true });
46733
46986
  const tray = locateTrayExecutable();
46734
46987
  const programArgs = tray ? ` <string>${tray}</string>` : [
46735
46988
  ` <string>${nodeBinary()}</string>`,
@@ -46749,8 +47002,8 @@ ${programArgs}
46749
47002
  <key>KeepAlive</key>
46750
47003
  <dict><key>SuccessfulExit</key><false/></dict>
46751
47004
  <key>ThrottleInterval</key><integer>30</integer>
46752
- <key>StandardOutPath</key><string>${join3(logDir(), "out.log")}</string>
46753
- <key>StandardErrorPath</key><string>${join3(logDir(), "err.log")}</string>
47005
+ <key>StandardOutPath</key><string>${join4(logDir(), "out.log")}</string>
47006
+ <key>StandardErrorPath</key><string>${join4(logDir(), "err.log")}</string>
46754
47007
  <key>EnvironmentVariables</key>
46755
47008
  <dict>
46756
47009
  <key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
@@ -46759,7 +47012,7 @@ ${programArgs}
46759
47012
  </dict>
46760
47013
  </plist>
46761
47014
  `;
46762
- writeFileSync2(p, plist, { mode: 420 });
47015
+ writeFileSync3(p, plist, { mode: 420 });
46763
47016
  return p;
46764
47017
  }
46765
47018
  function launchctl(args) {
@@ -46790,7 +47043,7 @@ function macUninstall() {
46790
47043
  const target = `gui/${uid}/${SERVICE_LABEL}`;
46791
47044
  launchctl(["bootout", target]);
46792
47045
  const plist = plistPath();
46793
- if (existsSync4(plist)) {
47046
+ if (existsSync5(plist)) {
46794
47047
  try {
46795
47048
  unlinkSync(plist);
46796
47049
  } catch {
@@ -46803,12 +47056,12 @@ function macStatus() {
46803
47056
  return { running: r.ok, hint: r.ok ? "launchd managed" : "not installed" };
46804
47057
  }
46805
47058
  function systemdUnitPath() {
46806
- const xdg = process.env.XDG_CONFIG_HOME ?? join3(home(), ".config");
46807
- return join3(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
47059
+ const xdg = process.env.XDG_CONFIG_HOME ?? join4(home(), ".config");
47060
+ return join4(xdg, "systemd", "user", `${SYSTEMD_UNIT}.service`);
46808
47061
  }
46809
47062
  function writeSystemdUnit(cliPath) {
46810
47063
  const unitPath = systemdUnitPath();
46811
- mkdirSync(dirname4(unitPath), { recursive: true });
47064
+ mkdirSync2(dirname4(unitPath), { recursive: true });
46812
47065
  const unit = `[Unit]
46813
47066
  Description=modelstat agent
46814
47067
  Documentation=https://modelstat.ai
@@ -46823,13 +47076,13 @@ RestartSec=10
46823
47076
  # Don't restart-storm if the service is persistently unreachable.
46824
47077
  StartLimitIntervalSec=300
46825
47078
  StartLimitBurst=10
46826
- StandardOutput=append:${join3(logDir(), "out.log")}
46827
- StandardError=append:${join3(logDir(), "err.log")}
47079
+ StandardOutput=append:${join4(logDir(), "out.log")}
47080
+ StandardError=append:${join4(logDir(), "err.log")}
46828
47081
 
46829
47082
  [Install]
46830
47083
  WantedBy=default.target
46831
47084
  `;
46832
- writeFileSync2(unitPath, unit, { mode: 420 });
47085
+ writeFileSync3(unitPath, unit, { mode: 420 });
46833
47086
  return unitPath;
46834
47087
  }
46835
47088
  function systemctl(args) {
@@ -46849,7 +47102,7 @@ function linuxInstall() {
46849
47102
  function linuxUninstall() {
46850
47103
  systemctl(["disable", "--now", `${SYSTEMD_UNIT}.service`]);
46851
47104
  const unit = systemdUnitPath();
46852
- if (existsSync4(unit)) {
47105
+ if (existsSync5(unit)) {
46853
47106
  try {
46854
47107
  unlinkSync(unit);
46855
47108
  } catch {
@@ -46893,9 +47146,9 @@ function logsDir() {
46893
47146
  }
46894
47147
  function installTrayApp(sourceAppPath) {
46895
47148
  if (platform2() !== "darwin") return null;
46896
- if (!existsSync4(sourceAppPath)) return null;
46897
- const dest = join3(home(), "Applications", "ModelstatTray.app");
46898
- mkdirSync(dirname4(dest), { recursive: true });
47149
+ if (!existsSync5(sourceAppPath)) return null;
47150
+ const dest = join4(home(), "Applications", "ModelstatTray.app");
47151
+ mkdirSync2(dirname4(dest), { recursive: true });
46899
47152
  spawnSync("rm", ["-rf", dest]);
46900
47153
  const r = spawnSync("cp", ["-R", sourceAppPath, dest], { encoding: "utf8" });
46901
47154
  if (r.status !== 0) {
@@ -46908,25 +47161,25 @@ function bundledTrayAppPath() {
46908
47161
  const here2 = dirname4(fileURLToPath2(import.meta.url));
46909
47162
  const candidates = [
46910
47163
  // Pre-built .app — CI with codesigning drops one here.
46911
- join3(here2, "..", "vendor", "ModelstatTray.app"),
47164
+ join4(here2, "..", "vendor", "ModelstatTray.app"),
46912
47165
  // Local dev layout: apps/agent-dev/src/service.ts → ../../tray-mac/build/ModelstatTray.app
46913
- join3(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
47166
+ join4(here2, "..", "..", "tray-mac", "build", "ModelstatTray.app")
46914
47167
  ];
46915
47168
  for (const c of candidates) {
46916
- if (existsSync4(c)) return c;
47169
+ if (existsSync5(c)) return c;
46917
47170
  }
46918
47171
  const sourceDirs = [
46919
- join3(here2, "..", "vendor", "tray-mac"),
46920
- join3(here2, "..", "..", "tray-mac")
47172
+ join4(here2, "..", "vendor", "tray-mac"),
47173
+ join4(here2, "..", "..", "tray-mac")
46921
47174
  ];
46922
47175
  for (const src of sourceDirs) {
46923
- const build = join3(src, "build-app.sh");
46924
- if (!existsSync4(build)) continue;
47176
+ const build = join4(src, "build-app.sh");
47177
+ if (!existsSync5(build)) continue;
46925
47178
  if (!hasSwift()) return null;
46926
47179
  const r = spawnSync("bash", [build], { cwd: src, encoding: "utf8" });
46927
47180
  if (r.status === 0) {
46928
- const app = join3(src, "build", "ModelstatTray.app");
46929
- if (existsSync4(app)) return app;
47181
+ const app = join4(src, "build", "ModelstatTray.app");
47182
+ if (existsSync5(app)) return app;
46930
47183
  }
46931
47184
  }
46932
47185
  return null;
@@ -46942,6 +47195,20 @@ function trayStatus() {
46942
47195
  }
46943
47196
 
46944
47197
  // src/cli.ts
47198
+ async function confirmPrompt(question, defaultYes) {
47199
+ if (process.stdin.isTTY !== true) return defaultYes;
47200
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
47201
+ try {
47202
+ const raw = await new Promise((resolve6) => rl.question(question, resolve6));
47203
+ const ans = raw.trim().toLowerCase();
47204
+ if (ans === "") return defaultYes;
47205
+ if (ans === "y" || ans === "yes") return true;
47206
+ if (ans === "n" || ans === "no") return false;
47207
+ return defaultYes;
47208
+ } finally {
47209
+ rl.close();
47210
+ }
47211
+ }
46945
47212
  function tryOpenBrowser(url) {
46946
47213
  const p = platform4();
46947
47214
  const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
@@ -47008,11 +47275,13 @@ async function cmdSelfRegister() {
47008
47275
  device_uuid: deviceUuid,
47009
47276
  fingerprint
47010
47277
  });
47011
- state.setDeviceUuid(res.device_uuid);
47012
- state.setDeviceId(res.device_id);
47013
- state.setBearer(res.device_secret);
47014
- state.setClaimCode(res.claim_code);
47015
- state.setClaimUrl(res.claim_url);
47278
+ state.saveFreshIdentity({
47279
+ deviceUuid: res.device_uuid,
47280
+ deviceId: res.device_id,
47281
+ bearerToken: res.device_secret,
47282
+ claimCode: res.claim_code,
47283
+ claimUrl: res.claim_url
47284
+ });
47016
47285
  process.stdout.write(` \x1B[32m\u2713\x1B[0m registered device_id=${res.device_id}
47017
47286
  `);
47018
47287
  process.stdout.write(` \x1B[32m\u2713\x1B[0m secret ${res.secret_prefix}\u2026 (hashed on server, never re-sent)
@@ -47072,19 +47341,21 @@ async function cmdConnect(opts) {
47072
47341
  };
47073
47342
  const wipeAndSelfRegister = async (reason) => {
47074
47343
  warn(`${reason} \u2014 re-registering this device`);
47344
+ const bak = backupIdentity();
47345
+ if (bak) warn(`old identity moved to ${bak}`);
47075
47346
  state.setBearer(null);
47076
- state.setDeviceId(null);
47077
- state.setDeviceUuid(null);
47078
- state.setClaimCode(null);
47079
- state.setClaimUrl(null);
47080
47347
  await cmdSelfRegister();
47081
47348
  };
47082
- if (!state.deviceUuid || !state.bearer || !state.deviceId) {
47349
+ if (opts.fresh && hasIdentityFile()) {
47350
+ step("`--fresh` passed \u2014 minting a new device identity");
47351
+ await wipeAndSelfRegister("forced fresh start");
47352
+ } else if (!state.deviceUuid || !state.bearer || !state.deviceId) {
47083
47353
  step("Registering this device with modelstat.ai");
47084
47354
  await cmdSelfRegister();
47085
47355
  } else {
47086
47356
  step("Re-using existing device identity");
47087
47357
  ok(`device ${state.deviceId}`);
47358
+ ok(`identity file ${identityPath()}`);
47088
47359
  try {
47089
47360
  const me = await fetchDeviceMe(state.bearer);
47090
47361
  if (me.claim_code && me.claim_code !== state.claimCode) {
@@ -47096,6 +47367,15 @@ async function cmdConnect(opts) {
47096
47367
  }
47097
47368
  } catch (e) {
47098
47369
  if (e instanceof DeviceMeUnauthorized) {
47370
+ const interactive = !opts.yes && process.stdin.isTTY === true;
47371
+ if (interactive) {
47372
+ const prompt = "cached credentials no longer accepted by the server. Re-register this device? [Y/n] ";
47373
+ const answer = await confirmPrompt(prompt, true);
47374
+ if (!answer) {
47375
+ warn("keeping existing identity; connect aborted");
47376
+ return;
47377
+ }
47378
+ }
47099
47379
  await wipeAndSelfRegister("cached credentials no longer valid");
47100
47380
  } else {
47101
47381
  warn(`couldn't refresh device state: ${e.message}`);
@@ -47375,7 +47655,9 @@ function cmdPaths(args) {
47375
47655
  function parseConnectOpts(argv) {
47376
47656
  return {
47377
47657
  json: argv.includes("--json"),
47378
- noBrowser: argv.includes("--no-browser")
47658
+ noBrowser: argv.includes("--no-browser"),
47659
+ fresh: argv.includes("--fresh"),
47660
+ yes: argv.includes("--yes") || argv.includes("-y")
47379
47661
  };
47380
47662
  }
47381
47663
  async function main() {