@mostra/cli 0.3.0 → 0.5.0

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/bin.js CHANGED
@@ -54,7 +54,7 @@ async function start() {
54
54
  },
55
55
  stderrTty: process.stderr.isTTY === true,
56
56
  terminal: process.stdin.isTTY === true || process.stdout.isTTY === true || process.stderr.isTTY === true
57
- }) ?? await (await import("./run-CMyjOm0Q.js")).run({
57
+ }) ?? await (await import("./run-CC8PNXLU.js")).run({
58
58
  argv: process.argv.slice(2),
59
59
  env: process.env,
60
60
  cwd: process.cwd(),
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { chmodSync, closeSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
2
+ import { chmodSync, closeSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
4
4
  import { DIAGNOSTIC_AREAS } from "@mostra/protocol";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import * as z from "zod";
@@ -214,7 +214,11 @@ const common = {
214
214
  at: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00Z$/),
215
215
  installId: z.uuid(),
216
216
  version: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/),
217
- os: z.enum(["darwin", "linux"]),
217
+ os: z.enum([
218
+ "darwin",
219
+ "linux",
220
+ "win32"
221
+ ]),
218
222
  arch: z.string().regex(/^[a-z0-9]{2,12}$/),
219
223
  node: z.int().min(0).max(999),
220
224
  harness: z.enum([
@@ -283,6 +287,48 @@ function catalogCodes(codes, known) {
283
287
  };
284
288
  }
285
289
  //#endregion
290
+ //#region src/detached.ts
291
+ /**
292
+ * The directory and environment of the processes the CLI leaves running: its home's daemon and
293
+ * the telemetry flusher. They run in their file system's root, because on Windows nobody can
294
+ * rename or remove a running process's working directory, and they outlive the command (WP-W2,
295
+ * D151 item 3). Every path the environment names relative to the command's directory is made
296
+ * absolute first, so it still names what it named for the command (Opus review of WP-W2,
297
+ * finding 3).
298
+ */
299
+ /**
300
+ * The variables whose values are paths: Mostra's own, as the runtime, the SDK and the CLI read
301
+ * them. A test keeps the list complete.
302
+ */
303
+ const PATH_VARIABLES = [
304
+ "MOSTRA_HOME",
305
+ "MOSTRA_RUNTIME_DIR",
306
+ "MOSTRA_CONFIG_DIR",
307
+ "MOSTRA_CACHE_DIR",
308
+ "MOSTRA_BUNDLE",
309
+ "MOSTRA_RUNTIME_BIN",
310
+ "MOSTRA_SHELL_DIR",
311
+ "MOSTRA_WARM_DIR",
312
+ "MOSTRA_WORKER_ENTRY",
313
+ "MOSTRA_VERIFY_ENTRY",
314
+ "MOSTRA_WIN_HELPER",
315
+ "MOSTRA_TEST_WORKER_HOST",
316
+ "MOSTRA_TEST_BUNDLE_PINS"
317
+ ];
318
+ /** `env` with every relative path variable resolved against `cwd`. */
319
+ function detachedEnvironment(env, cwd = process.cwd()) {
320
+ const result = { ...env };
321
+ for (const name of PATH_VARIABLES) {
322
+ const value = result[name];
323
+ if (value !== void 0 && value !== "" && !isAbsolute(value)) result[name] = resolve(cwd, value);
324
+ }
325
+ return result;
326
+ }
327
+ /** The working directory of a process the CLI leaves running: the root of `anchor`'s file system. */
328
+ function detachedDirectory(anchor) {
329
+ return parse(resolve(anchor)).root;
330
+ }
331
+ //#endregion
286
332
  //#region src/telemetry/store.ts
287
333
  /**
288
334
  * Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,
@@ -312,8 +358,11 @@ var TelemetryStore = class {
312
358
  queueFile;
313
359
  idFile;
314
360
  #lockFile;
315
- constructor(dir) {
361
+ #platform;
362
+ /** `platform` is the system whose lock rules apply: this process's, but for tests. */
363
+ constructor(dir, platform = process.platform) {
316
364
  this.dir = dir;
365
+ this.#platform = platform;
317
366
  this.settingsFile = join(dir, "telemetry.json");
318
367
  this.queueFile = join(dir, "telemetry-queue.ndjson");
319
368
  this.idFile = join(dir, "telemetry-id");
@@ -321,6 +370,10 @@ var TelemetryStore = class {
321
370
  }
322
371
  /** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */
323
372
  read() {
373
+ if (blocked(this.dir)) return {
374
+ ok: false,
375
+ file: this.dir
376
+ };
324
377
  const id = this.#readId();
325
378
  if (id === null) return {
326
379
  ok: false,
@@ -490,19 +543,20 @@ var TelemetryStore = class {
490
543
  }
491
544
  const temporary = `${this.queueFile}.${process.pid}.tmp`;
492
545
  writeFileSync(temporary, `${lines.join("\n")}\n`, { mode: 384 });
493
- renameSync(temporary, this.queueFile);
546
+ replace(temporary, this.queueFile, this.#platform);
494
547
  }
495
548
  #write(settings) {
496
549
  const { installId: _id, ...stored } = settings;
497
550
  const temporary = `${this.settingsFile}.${process.pid}.tmp`;
498
551
  writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 384 });
499
- renameSync(temporary, this.settingsFile);
552
+ replace(temporary, this.settingsFile, this.#platform);
500
553
  }
501
554
  #prepareDir() {
502
555
  mkdirSync(this.dir, {
503
556
  recursive: true,
504
557
  mode: 448
505
558
  });
559
+ if (this.#platform === "win32") return;
506
560
  try {
507
561
  chmodSync(this.dir, 448);
508
562
  } catch {}
@@ -530,9 +584,14 @@ var TelemetryStore = class {
530
584
  });
531
585
  break;
532
586
  } catch (error) {
533
- if (error.code !== "EEXIST") return void 0;
534
- if (this.#breakStale()) continue;
587
+ const code = error.code;
588
+ if (!held(code, this.#platform)) return void 0;
535
589
  if (Date.now() > deadline) return void 0;
590
+ const next = this.#breakStale();
591
+ if (next === "absent") {
592
+ if (code === "EEXIST") continue;
593
+ if (!writable(this.dir)) return void 0;
594
+ } else if (next === "broken") continue;
536
595
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
537
596
  }
538
597
  try {
@@ -550,9 +609,11 @@ var TelemetryStore = class {
550
609
  } catch {}
551
610
  }
552
611
  /**
553
- * Takes a crashed owner's lock away; `true` when the caller should try again at once. The lock
554
- * is moved aside under a name of this process's own (a rename is atomic, so of two processes
555
- * breaking it one gets it), and a lock that turns out to be another, live owner's goes back.
612
+ * Takes a crashed owner's lock away. The lock is moved aside under a name of this process's own
613
+ * (a rename is atomic, so of two processes breaking it one gets it), and a lock that turns out
614
+ * to be another, live owner's goes back. Answers `broken` when the caller should try again at
615
+ * once, `absent` when no lock file exists, and `held` when the caller should wait: a live
616
+ * owner's lock, or one that can't be read (on Windows, a lock being deleted).
556
617
  */
557
618
  #breakStale() {
558
619
  let seen;
@@ -561,28 +622,84 @@ var TelemetryStore = class {
561
622
  try {
562
623
  handle = openSync(this.#lockFile, "r");
563
624
  age = Date.now() - fstatSync(handle).mtimeMs;
564
- if (age <= STALE_LOCK_MS) return false;
625
+ if (age <= STALE_LOCK_MS) return "held";
565
626
  seen = readFileSync(handle, "utf8");
566
- } catch {
567
- return true;
627
+ } catch (error) {
628
+ return error.code === "ENOENT" ? "absent" : "held";
568
629
  } finally {
569
630
  if (handle !== void 0) closeSync(handle);
570
631
  }
571
- if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return false;
632
+ if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return "held";
572
633
  const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;
573
634
  try {
574
635
  renameSync(this.#lockFile, aside);
575
- } catch {
576
- return true;
636
+ } catch (error) {
637
+ return error.code === "ENOENT" ? "broken" : "held";
577
638
  }
578
639
  try {
579
640
  if (readFileSync(aside, "utf8") !== seen) linkSync(aside, this.#lockFile);
580
641
  } catch {} finally {
581
642
  rmSync(aside, { force: true });
582
643
  }
583
- return true;
644
+ return "broken";
584
645
  }
585
646
  };
647
+ /**
648
+ * Renames `from` over `to`. Windows refuses to replace a file another process has open, as a
649
+ * process reading the settings outside the lock does, with `EPERM` (MT-01: CI's install-ID race,
650
+ * traced on `windows-latest`); the rename is tried again for up to a second, as the runtime's
651
+ * renames are (D150 item 8), and never turned into a copy.
652
+ */
653
+ function replace(from, to, platform, rename = renameSync) {
654
+ const deadline = Date.now() + 1e3;
655
+ for (let pause = 5;; pause = Math.min(pause * 2, 100)) try {
656
+ rename(from, to);
657
+ return;
658
+ } catch (error) {
659
+ const code = error.code;
660
+ if (platform !== "win32" || !(code === "EPERM" || code === "EACCES" || code === "EBUSY") || Date.now() > deadline) throw error;
661
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pause);
662
+ }
663
+ }
664
+ /** Whether this process can create a file in `dir`: a probe of its own, removed at once. */
665
+ function writable(dir, remove = (path) => rmSync(path, { force: true })) {
666
+ const probe = join(dir, `.probe-${process.pid}-${randomUUID()}`);
667
+ try {
668
+ writeFileSync(probe, "", {
669
+ flag: "wx",
670
+ mode: 384
671
+ });
672
+ } catch {
673
+ return false;
674
+ }
675
+ try {
676
+ remove(probe);
677
+ } catch {}
678
+ return true;
679
+ }
680
+ /**
681
+ * Whether a lock's exclusive create failed because another process holds it. On Windows a lock
682
+ * its owner just removed stays "delete pending" while anyone still has it open (another process
683
+ * judging its age), and creating it again meanwhile is refused as access denied, not `EEXIST`
684
+ * (WP-W2: twelve first runs at once on an arm64 runner): that is contention too, never a reason
685
+ * to give up.
686
+ */
687
+ function held(code, platform) {
688
+ return code === "EEXIST" || platform === "win32" && (code === "EPERM" || code === "EACCES");
689
+ }
690
+ /** Whether `dir` can never be created: its nearest existing ancestor isn't a directory. */
691
+ function blocked(dir) {
692
+ for (let path = dir;; path = dirname(path)) {
693
+ try {
694
+ return !statSync(path).isDirectory();
695
+ } catch (error) {
696
+ const code = error.code;
697
+ if (code === "ENOTDIR") return true;
698
+ if (code !== "ENOENT") return false;
699
+ }
700
+ if (dirname(path) === path) return false;
701
+ }
702
+ }
586
703
  /** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */
587
704
  function ownerAlive(lock) {
588
705
  const pid = Number(lock.split(" ")[0]);
@@ -703,16 +820,17 @@ function startFlusher(store, env, now) {
703
820
  ], {
704
821
  detached: true,
705
822
  stdio: "ignore",
706
- env: {
823
+ cwd: detachedDirectory(store.dir),
824
+ env: detachedEnvironment({
707
825
  ...process.env,
708
826
  ...env
709
- }
827
+ })
710
828
  });
711
829
  child.on("error", () => {});
712
830
  child.unref();
713
831
  } catch {}
714
832
  }
715
833
  //#endregion
716
- export { catalogCodes as a, detectHarness as c, truthy as d, cliVersion as f, TelemetryEvent as i, buildEndpoint as l, packageRoot as m, startFlusher as n, durationBucket as o, fromSource as p, TelemetryStore as r, minute as s, flush as t, decide as u };
834
+ export { detachedEnvironment as a, durationBucket as c, buildEndpoint as d, decide as f, packageRoot as g, fromSource as h, detachedDirectory as i, minute as l, cliVersion as m, startFlusher as n, TelemetryEvent as o, truthy as p, TelemetryStore as r, catalogCodes as s, flush as t, detectHarness as u };
717
835
 
718
- //# sourceMappingURL=flush-BCuWs0Zh.js.map
836
+ //# sourceMappingURL=flush-CFJkvi2R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flush-CFJkvi2R.js","names":["httpsRequest","httpRequest"],"sources":["../src/version.ts","../src/telemetry/decision.ts","../src/telemetry/harness.ts","../src/telemetry/events.ts","../src/detached.ts","../src/telemetry/store.ts","../src/telemetry/flush.ts"],"sourcesContent":["/** The CLI's own package: its root directory and its release. */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nlet found: { readonly root: string; readonly version: string } | undefined;\n\nfunction own(): { readonly root: string; readonly version: string } {\n if (found) return found;\n let dir = dirname(fileURLToPath(import.meta.url));\n for (;;) {\n try {\n const manifest = JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\")) as {\n name?: string;\n version?: string;\n };\n if (manifest.name === \"@mostra/cli\" && manifest.version) {\n found = { root: dir, version: manifest.version };\n return found;\n }\n } catch {\n // Keep looking upwards.\n }\n const parent = dirname(dir);\n if (parent === dir) throw new Error(\"Couldn't find @mostra/cli's package.json.\");\n dir = parent;\n }\n}\n\n/** This CLI's version (`mostra --version`, the client version it reports, telemetry's `version`). */\nexport function cliVersion(): string {\n return own().version;\n}\n\n/** The CLI package's directory: its `dist/`, `src/` and bundled reference live under it. */\nexport function packageRoot(): string {\n return own().root;\n}\n\n/** Whether this CLI runs from its TypeScript sources (tests), not its build. */\nexport function fromSource(): boolean {\n return import.meta.url.endsWith(\".ts\");\n}\n","/**\n * Whether telemetry sends, and why (D140 item 3). The first match decides, so an explicit \"off\"\n * anywhere wins over any \"on\", and withdrawn consent sticks.\n */\nimport type { Environment } from \"../home.ts\";\nimport type { ReadSettings } from \"./store.ts\";\nimport type { TelemetryReason } from \"./words.ts\";\n\n/** The endpoint this build sends to, set only by the release workflow at bundle time (D140 item 7). */\ndeclare const __MOSTRA_TELEMETRY_ENDPOINT__: string | undefined;\n\n/** The build's endpoint: empty in this package and in every development build. */\nexport function buildEndpoint(): string {\n return typeof __MOSTRA_TELEMETRY_ENDPOINT__ === \"string\" ? __MOSTRA_TELEMETRY_ENDPOINT__ : \"\";\n}\n\n/** What the decision reads. */\nexport interface DecisionInputs {\n readonly env: Environment;\n /** The build's endpoint ({@link buildEndpoint}); tests pass their own. */\n readonly buildEndpoint: string;\n /** Whether this use is embedded: a host-spawned instance, or a home a host configures. */\n readonly embedded: boolean;\n readonly settings: ReadSettings;\n /** Whether this machine's time zone asks first. */\n readonly askFirst: boolean;\n}\n\n/** The decision. */\nexport interface TelemetryDecision {\n readonly enabled: boolean;\n readonly reason: TelemetryReason;\n readonly askFirst: boolean;\n /** The endpoint events go to, when there is one. */\n readonly endpoint?: string;\n /** For an enabled decision: whether the person said yes, or it is the default. */\n readonly consent?: \"explicit\" | \"default\";\n}\n\nconst OFF_VALUES = new Set([\"0\", \"false\", \"off\", \"no\"]);\nconst ON_VALUES = new Set([\"1\", \"true\", \"on\", \"yes\"]);\n\n/** `DO_NOT_TRACK` and `MOSTRA_OFFLINE`'s parser: set to anything but empty, `0` or `false`. */\nexport function truthy(value: string | undefined): boolean {\n if (value === undefined) return false;\n const normalized = value.trim().toLowerCase();\n return normalized !== \"\" && normalized !== \"0\" && normalized !== \"false\";\n}\n\n/**\n * The endpoint in use: `MOSTRA_TELEMETRY_ENDPOINT` when it is `https:` or `http:` to a loopback\n * address, else the build's; `undefined` when there is none (D140 item 7).\n */\nexport function endpointOf(env: Environment, build: string): string | undefined {\n const override = env[\"MOSTRA_TELEMETRY_ENDPOINT\"];\n const chosen = override !== undefined && override !== \"\" ? override : build;\n if (chosen === \"\") return undefined;\n let url: URL;\n try {\n url = new URL(chosen);\n } catch {\n return undefined;\n }\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.protocol === \"https:\") return url.href;\n const loopback =\n url.hostname === \"127.0.0.1\" || url.hostname === \"[::1]\" || url.hostname === \"localhost\";\n return url.protocol === \"http:\" && loopback ? url.href : undefined;\n}\n\n/** Decides, in D140's order. */\nexport function decide(inputs: DecisionInputs): TelemetryDecision {\n const { askFirst } = inputs;\n const off = (reason: TelemetryReason): TelemetryDecision => ({\n enabled: false,\n reason,\n askFirst,\n });\n const endpoint = endpointOf(inputs.env, inputs.buildEndpoint);\n if (endpoint === undefined) return off(\"no-collector\");\n const at = { askFirst, endpoint };\n if (inputs.embedded) return { ...at, enabled: false, reason: \"embedded\" };\n if (truthy(inputs.env[\"DO_NOT_TRACK\"])) return { ...at, enabled: false, reason: \"do-not-track\" };\n const variable = inputs.env[\"MOSTRA_TELEMETRY\"];\n const normalized = variable?.trim().toLowerCase();\n if (normalized !== undefined && OFF_VALUES.has(normalized)) {\n return { ...at, enabled: false, reason: \"variable\" };\n }\n if (!inputs.settings.ok) return { ...at, enabled: false, reason: \"unreadable\" };\n const choice = inputs.settings.settings.choice;\n if (choice === \"off\") return { ...at, enabled: false, reason: \"setting\" };\n if (normalized !== undefined && normalized !== \"\") {\n return ON_VALUES.has(normalized)\n ? { ...at, enabled: true, reason: \"variable\", consent: \"explicit\" }\n : { ...at, enabled: false, reason: \"variable-invalid\" };\n }\n if (choice === \"on\") return { ...at, enabled: true, reason: \"setting\", consent: \"explicit\" };\n return askFirst\n ? { ...at, enabled: false, reason: \"default\" }\n : { ...at, enabled: true, reason: \"default\", consent: \"default\" };\n}\n","/**\n * Which agent ran the CLI (D140 item 9), from environment markers each agent documents or sets in\n * its source (VERIFIED_FACTS, WP-10). Only the agent's ID is ever sent, never a marker's value.\n */\nimport type { Environment } from \"../home.ts\";\n\n/** The agents Mostra recognizes, in the order their markers are checked. */\nexport const HARNESSES = [\"claude-code\", \"codex\", \"gemini-cli\", \"opencode\", \"cursor\"] as const;\n\n/** A recognized agent, `other` for an agent Mostra doesn't know, `none` for no agent at all. */\nexport type Harness = (typeof HARNESSES)[number] | \"other\" | \"none\";\n\n/** Each agent's own marker. */\nconst MARKERS: Readonly<Record<(typeof HARNESSES)[number], string>> = {\n \"claude-code\": \"CLAUDECODE\",\n codex: \"CODEX_THREAD_ID\",\n \"gemini-cli\": \"GEMINI_CLI\",\n opencode: \"OPENCODE\",\n cursor: \"CURSOR_AGENT\",\n};\n\nconst set = (value: string | undefined): boolean => value !== undefined && value !== \"\";\n\n/**\n * The agent that ran this process: `AI_AGENT` when it starts with a known ID (the convention\n * `@vercel/detect-agent` reads), else the first agent whose own marker is set, else `other` when\n * `AI_AGENT` or `AGENT` names something else, else `none`. Cursor's editor terminal sets\n * `CURSOR_TRACE_ID` without `CURSOR_AGENT`: that is a person, so it isn't `cursor`.\n */\nexport function detectHarness(env: Environment): Harness {\n const declared = env[\"AI_AGENT\"];\n if (set(declared)) {\n const known = HARNESSES.find((id) => declared?.startsWith(id));\n if (known) return known;\n }\n for (const id of HARNESSES) if (set(env[MARKERS[id]])) return id;\n if (set(declared) || set(env[\"AGENT\"])) return \"other\";\n return \"none\";\n}\n","/**\n * Telemetry's event schema 1 (D140 item 8), the one published in `docs/reference/telemetry.md`.\n * Every event is checked against it before it is queued and again before it is sent, so nothing\n * that isn't on that page can leave the machine: the objects are strict, every string is from a\n * closed set or a pattern, and no field takes free text.\n */\nimport { DIAGNOSTIC_AREAS } from \"@mostra/protocol\";\nimport * as z from \"zod\";\nimport { HARNESSES, type Harness } from \"./harness.ts\";\n\n/** The schema version every event carries. */\nexport const SCHEMA = 1;\n\n/** The duration buckets of `cli.command`. */\nexport const DURATIONS = [\"<1s\", \"1-5s\", \"5-30s\", \"30s-2m\", \"2-10m\", \">10m\"] as const;\n\n/** A command's duration bucket. */\nexport function durationBucket(ms: number): (typeof DURATIONS)[number] {\n if (ms < 1000) return \"<1s\";\n if (ms < 5000) return \"1-5s\";\n if (ms < 30_000) return \"5-30s\";\n if (ms < 120_000) return \"30s-2m\";\n if (ms < 600_000) return \"2-10m\";\n return \">10m\";\n}\n\n/** A catalog-shaped diagnostic code: `MOSTRA_<AREA>_<NAME>`, never a message. */\nconst code = z\n .string()\n .regex(new RegExp(`^MOSTRA_(${DIAGNOSTIC_AREAS.join(\"|\")})_[A-Z0-9_]{1,64}$`));\n\n/** A command path from the definitions: lowercase words. */\nconst command = z.string().regex(/^[a-z]+( [a-z]+)?$/);\n\nconst common = {\n schema: z.literal(SCHEMA),\n at: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:00Z$/),\n installId: z.uuid(),\n version: z.string().regex(/^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$/),\n os: z.enum([\"darwin\", \"linux\", \"win32\"]),\n arch: z.string().regex(/^[a-z0-9]{2,12}$/),\n node: z.int().min(0).max(999),\n harness: z.enum([...HARNESSES, \"other\", \"none\"]),\n ci: z.boolean(),\n consent: z.enum([\"explicit\", \"default\"]),\n};\n\n/** The fields every event carries. */\nexport interface CommonFields {\n readonly schema: typeof SCHEMA;\n readonly at: string;\n readonly installId: string;\n readonly version: string;\n readonly os: \"darwin\" | \"linux\" | \"win32\";\n readonly arch: string;\n readonly node: number;\n readonly harness: Harness;\n readonly ci: boolean;\n readonly consent: \"explicit\" | \"default\";\n}\n\n/** One event, as sent (D140 item 8). */\nexport type TelemetryEvent = CommonFields &\n (\n | { readonly event: \"cli.first_run\" }\n | {\n readonly event: \"cli.command\";\n readonly command: string;\n readonly exit: number;\n readonly duration: (typeof DURATIONS)[number];\n readonly json: boolean;\n readonly error?: string | undefined;\n }\n | {\n readonly event: \"verify.result\";\n readonly status: \"pass\" | \"fail\";\n readonly codes: readonly string[];\n readonly codesTruncated: boolean;\n }\n | { readonly event: \"publish.done\"; readonly reused: boolean; readonly forced: boolean }\n | {\n readonly event: \"share.done\";\n readonly kind: \"tailnet\" | \"funnel\" | \"cloudflare\" | \"remote\";\n }\n | {\n readonly event: \"mcp.session\";\n readonly client: (typeof HARNESSES)[number] | \"other\";\n readonly clientVersion?: string | undefined;\n }\n );\n\n/** Every event Mostra may send, as a strict union on `event`. */\nexport const TelemetryEvent: z.ZodType<TelemetryEvent> = z.discriminatedUnion(\"event\", [\n z.strictObject({ ...common, event: z.literal(\"cli.first_run\") }),\n z.strictObject({\n ...common,\n event: z.literal(\"cli.command\"),\n command,\n exit: z.int().min(0).max(255),\n duration: z.enum(DURATIONS),\n json: z.boolean(),\n error: code.optional(),\n }),\n z.strictObject({\n ...common,\n event: z.literal(\"verify.result\"),\n status: z.enum([\"pass\", \"fail\"]),\n codes: z.array(code).max(50),\n codesTruncated: z.boolean(),\n }),\n z.strictObject({\n ...common,\n event: z.literal(\"publish.done\"),\n reused: z.boolean(),\n forced: z.boolean(),\n }),\n // Emitted by WP-13 and WP-29.\n z.strictObject({\n ...common,\n event: z.literal(\"share.done\"),\n kind: z.enum([\"tailnet\", \"funnel\", \"cloudflare\", \"remote\"]),\n }),\n // Emitted by WP-15: a known agent's ID for the MCP client, or `other`; its version only then.\n z.strictObject({\n ...common,\n event: z.literal(\"mcp.session\"),\n client: z.enum([...HARNESSES, \"other\"]),\n clientVersion: z\n .string()\n .regex(/^[0-9A-Za-z.+-]{1,32}$/)\n .optional(),\n }),\n]);\n\n/** `at`: now in UTC, rounded down to the minute. */\nexport function minute(now: Date): string {\n return `${now.toISOString().slice(0, 16)}:00Z`;\n}\n\n/** The distinct catalog codes of `codes`, sorted, at most 50, and whether some were left out. */\nexport function catalogCodes(\n codes: readonly string[],\n known: ReadonlySet<string>,\n): { codes: string[]; codesTruncated: boolean } {\n const distinct = [...new Set(codes.filter((value) => known.has(value)))].sort();\n return { codes: distinct.slice(0, 50), codesTruncated: distinct.length > 50 };\n}\n","/**\n * The directory and environment of the processes the CLI leaves running: its home's daemon and\n * the telemetry flusher. They run in their file system's root, because on Windows nobody can\n * rename or remove a running process's working directory, and they outlive the command (WP-W2,\n * D151 item 3). Every path the environment names relative to the command's directory is made\n * absolute first, so it still names what it named for the command (Opus review of WP-W2,\n * finding 3).\n */\nimport { isAbsolute, parse, resolve } from \"node:path\";\n\n/**\n * The variables whose values are paths: Mostra's own, as the runtime, the SDK and the CLI read\n * them. A test keeps the list complete.\n */\nexport const PATH_VARIABLES: readonly string[] = [\n \"MOSTRA_HOME\",\n \"MOSTRA_RUNTIME_DIR\",\n \"MOSTRA_CONFIG_DIR\",\n \"MOSTRA_CACHE_DIR\",\n \"MOSTRA_BUNDLE\",\n \"MOSTRA_RUNTIME_BIN\",\n \"MOSTRA_SHELL_DIR\",\n \"MOSTRA_WARM_DIR\",\n \"MOSTRA_WORKER_ENTRY\",\n \"MOSTRA_VERIFY_ENTRY\",\n \"MOSTRA_WIN_HELPER\",\n \"MOSTRA_TEST_WORKER_HOST\",\n \"MOSTRA_TEST_BUNDLE_PINS\",\n];\n\n/** `env` with every relative path variable resolved against `cwd`. */\nexport function detachedEnvironment(\n env: Readonly<Record<string, string | undefined>>,\n cwd: string = process.cwd(),\n): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = { ...env };\n for (const name of PATH_VARIABLES) {\n const value = result[name];\n if (value !== undefined && value !== \"\" && !isAbsolute(value)) {\n result[name] = resolve(cwd, value);\n }\n }\n return result;\n}\n\n/** The working directory of a process the CLI leaves running: the root of `anchor`'s file system. */\nexport function detachedDirectory(anchor: string): string {\n return parse(resolve(anchor)).root;\n}\n","/**\n * Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,\n * when the notice was shown, when a flush last started), `telemetry-id` (the random install ID)\n * and `telemetry-queue.ndjson` (events waiting to be sent). The directory is 0700 and the files\n * 0600; `telemetry.json` and the queue are written by rename. The install ID is made by an\n * exclusive create (a hard link that fails when the file exists), so processes on a first run\n * agree on one ID whatever happens to the lock. Every other change goes through one lock file,\n * owned by a token and taken over only when its holder has died, so a flusher never loses an\n * event appended while it sends.\n */\nimport { randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** What `telemetry.json` holds. */\nexport interface TelemetrySettings {\n /** The person's choice: `mostra telemetry on|off`, or their answer to the question. */\n readonly choice?: \"on\" | \"off\";\n /**\n * The random install ID, made on the first run that sends; deleted when telemetry turns off.\n * Read from `telemetry-id`, never written into `telemetry.json`.\n */\n readonly installId?: string;\n /**\n * When the notice of what is sent was shown where sending follows: the default notice, the\n * question, or `mostra telemetry on`'s. A `default`-consent event needs it (Opus review\n * finding 3).\n */\n readonly noticeShownAt?: string;\n /**\n * When the ask-first notice (\"Nothing is sent unless you run: mostra telemetry on\") was\n * shown. It never stands for the default notice: a later run where telemetry is on by default\n * still shows that one before anything is sent.\n */\n readonly askFirstNoticeShownAt?: string;\n /** When a flush last started. */\n readonly lastFlushAt?: string;\n}\n\n/** The settings, or why they can't be read (which means off, D140 item 5). */\nexport type ReadSettings =\n | { readonly ok: true; readonly settings: TelemetrySettings }\n | { readonly ok: false; readonly file: string };\n\n/** A lock older than this whose owner no longer runs is taken over as stale. */\nconst STALE_LOCK_MS = 10_000;\n/**\n * A lock older than this is taken over whoever it names: a held section takes milliseconds, and\n * a PID can name an unrelated live process once the owner died (in a container, PID 1 always\n * runs), which would otherwise hold every later command up forever (Opus review finding 5).\n */\nconst DEAD_LOCK_MS = 60_000;\n/** The queue's bounds (D140 item 6). */\nexport const QUEUE_MAX_EVENTS = 1000;\nexport const QUEUE_MAX_AGE_MS: number = 7 * 24 * 60 * 60 * 1000;\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\n/** Telemetry's files in one config directory. */\nexport class TelemetryStore {\n readonly dir: string;\n readonly settingsFile: string;\n readonly queueFile: string;\n readonly idFile: string;\n readonly #lockFile: string;\n readonly #platform: string;\n\n /** `platform` is the system whose lock rules apply: this process's, but for tests. */\n constructor(dir: string, platform: string = process.platform) {\n this.dir = dir;\n this.#platform = platform;\n this.settingsFile = join(dir, \"telemetry.json\");\n this.queueFile = join(dir, \"telemetry-queue.ndjson\");\n this.idFile = join(dir, \"telemetry-id\");\n this.#lockFile = join(dir, \"telemetry.lock\");\n }\n\n /** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */\n read(): ReadSettings {\n // A directory that can never exist (a part of its path is a file) is unreadable, never\n // \"nothing stored yet\". POSIX says so for every read (ENOTDIR); Windows answers ENOENT, which\n // would read as the default, on, though nothing could ever be recorded (WP-W2).\n if (blocked(this.dir)) return { ok: false, file: this.dir };\n const id = this.#readId();\n if (id === null) return { ok: false, file: this.idFile };\n let text: string;\n try {\n text = readFileSync(this.settingsFile, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\")\n return { ok: true, settings: id !== undefined ? { installId: id } : {} };\n return { ok: false, file: this.settingsFile };\n }\n try {\n const data = JSON.parse(text) as Record<string, unknown>;\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) throw new Error();\n const { choice, noticeShownAt, askFirstNoticeShownAt, lastFlushAt } = data;\n const installId = id;\n if (choice !== undefined && choice !== \"on\" && choice !== \"off\") throw new Error();\n // An install ID belongs in `telemetry-id`; one here is a file Mostra didn't write.\n if (data[\"installId\"] !== undefined) throw new Error();\n if (noticeShownAt !== undefined && typeof noticeShownAt !== \"string\") throw new Error();\n if (askFirstNoticeShownAt !== undefined && typeof askFirstNoticeShownAt !== \"string\")\n throw new Error();\n if (lastFlushAt !== undefined && typeof lastFlushAt !== \"string\") throw new Error();\n return {\n ok: true,\n settings: {\n ...(choice !== undefined ? { choice } : {}),\n ...(installId !== undefined ? { installId } : {}),\n ...(noticeShownAt !== undefined ? { noticeShownAt } : {}),\n ...(askFirstNoticeShownAt !== undefined ? { askFirstNoticeShownAt } : {}),\n ...(lastFlushAt !== undefined ? { lastFlushAt } : {}),\n },\n };\n } catch {\n return { ok: false, file: this.settingsFile };\n }\n }\n\n /**\n * Changes the settings under the lock and returns what they became; `undefined` when the file\n * can't be read (nothing is written over it).\n */\n update(change: (current: TelemetrySettings) => TelemetrySettings): TelemetrySettings | undefined {\n return this.locked(() => {\n const current = this.read();\n if (!current.ok) return undefined;\n const next = change(current.settings);\n this.#write(next);\n return next;\n });\n }\n\n /**\n * The install ID, made when there is none yet. A new ID is written to a file of this process's\n * own, then hard-linked to `telemetry-id`, which fails when the file exists: of any processes\n * making one at once exactly one wins, and the others read the winner's. `undefined` when the\n * settings can't be read.\n */\n installId(): { readonly id: string; readonly created: boolean } | undefined {\n const current = this.read();\n if (!current.ok) return undefined;\n if (current.settings.installId !== undefined)\n return { id: current.settings.installId, created: false };\n const candidate = `${this.idFile}.${process.pid}.${randomUUID()}.tmp`;\n const id = randomUUID();\n try {\n this.#prepareDir();\n writeFileSync(candidate, `${id}\\n`, { mode: 0o600 });\n linkSync(candidate, this.idFile);\n return { id, created: true };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") return undefined;\n const winner = this.#readId();\n return winner ? { id: winner, created: false } : undefined;\n } finally {\n rmSync(candidate, { force: true });\n }\n }\n\n /** The install ID in `telemetry-id`: `undefined` when there is none, `null` when it's damaged. */\n #readId(): string | undefined | null {\n let text: string;\n try {\n text = readFileSync(this.idFile, \"utf8\");\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? undefined : null;\n }\n const id = text.trim();\n return UUID.test(id) ? id : null;\n }\n\n /**\n * Replaces a file that can't be read with these settings; only `telemetry on|off` do, as the\n * person's explicit choice. Call it holding the lock.\n */\n reset(settings: TelemetrySettings): TelemetrySettings {\n const { installId: _id, ...rest } = settings;\n this.#write(rest);\n // A damaged install ID goes too; `on` makes a new one on its first send.\n rmSync(this.idFile, { force: true });\n if (settings.choice === \"off\") rmSync(this.queueFile, { force: true });\n return rest;\n }\n\n /** Turns telemetry off: the choice, and the install ID and the queue deleted at once. */\n turnOff(): boolean {\n return (\n this.locked(() => {\n const current = this.read();\n if (!current.ok) return false;\n const { installId: _id, ...rest } = current.settings;\n this.#write({ ...rest, choice: \"off\" });\n rmSync(this.idFile, { force: true });\n rmSync(this.queueFile, { force: true });\n return true;\n }) ?? false\n );\n }\n\n /** Appends events to the queue, keeping its bounds. */\n append(lines: readonly string[], now: number): void {\n if (lines.length === 0) return;\n this.locked(() => {\n const kept = bounded([...this.#queued(), ...lines], now);\n this.#writeQueue(kept);\n });\n }\n\n /** The queued events, within their bounds. */\n queued(now: number): string[] {\n return bounded(this.#queued(), now);\n }\n\n /**\n * Removes the lines a flush sent from the head of the queue, after pruning what expired or\n * fell outside the bounds, as {@link queued} did; how many sent lines were removed (0 when the\n * lock can't be had). A flusher stops when this is 0, so it never sends a batch twice.\n */\n drop(sent: readonly string[], now: number): number {\n return (\n this.locked(() => {\n const queued = bounded(this.#queued(), now);\n let index = 0;\n while (index < sent.length && queued[index] === sent[index]) index += 1;\n this.#writeQueue(queued.slice(index));\n return index;\n }) ?? 0\n );\n }\n\n #queued(): string[] {\n try {\n return readFileSync(this.queueFile, \"utf8\")\n .split(\"\\n\")\n .filter((line) => line.length > 0);\n } catch {\n return [];\n }\n }\n\n #writeQueue(lines: readonly string[]): void {\n if (lines.length === 0) {\n rmSync(this.queueFile, { force: true });\n return;\n }\n const temporary = `${this.queueFile}.${process.pid}.tmp`;\n writeFileSync(temporary, `${lines.join(\"\\n\")}\\n`, { mode: 0o600 });\n replace(temporary, this.queueFile, this.#platform);\n }\n\n #write(settings: TelemetrySettings): void {\n const { installId: _id, ...stored } = settings;\n const temporary = `${this.settingsFile}.${process.pid}.tmp`;\n writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\\n`, { mode: 0o600 });\n replace(temporary, this.settingsFile, this.#platform);\n }\n\n #prepareDir(): void {\n mkdirSync(this.dir, { recursive: true, mode: 0o700 });\n // Windows has no modes: the directory keeps the access list `%APPDATA%` hands down.\n if (this.#platform === \"win32\") return;\n try {\n chmodSync(this.dir, 0o700);\n } catch {\n // A directory someone else owns: the files below fail or succeed on their own terms.\n }\n }\n\n /**\n * Runs `fn` holding the lock file; `undefined` when the lock can't be had within 2 s, or the\n * directory or a file in it can't be written (a full disk, a directory someone else owns):\n * telemetry never stops a command (D140 item 5). The lock\n * holds its owner's PID and a token of its own, is released only by its owner, and is taken\n * over when it is older than 10 s and its owner is no longer running (a crash), or older than\n * 60 s whoever it names.\n */\n locked<T>(fn: () => T): T | undefined {\n try {\n this.#prepareDir();\n } catch {\n return undefined;\n }\n const token = `${process.pid} ${randomUUID()}`;\n const deadline = Date.now() + 2000;\n for (;;) {\n try {\n writeFileSync(this.#lockFile, token, { flag: \"wx\", mode: 0o600 });\n break;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (!held(code, this.#platform)) return undefined;\n // Every path back to the create checks the deadline, so nothing can hold the CLI here\n // (GPT review of WP-W2), and only a lock that just went is tried again without a pause,\n // so nothing spins either (Opus review of WP-W2, finding 2).\n if (Date.now() > deadline) return undefined;\n const next = this.#breakStale();\n if (next === \"absent\") {\n // No lock there, though the create was refused. After `EEXIST` it was removed in\n // between: try again. After access denied, either a lock Windows was deleting went in\n // between (twelve processes at once keep that happening), or this user can't write the\n // directory and waiting changes nothing (Opus review of WP-W2, finding 2). A file of\n // this process's own tells which, whatever the timing (MT-01: a count of such rounds\n // in a row still gave up under contention).\n if (code === \"EEXIST\") continue;\n if (!writable(this.dir)) return undefined;\n } else if (next === \"broken\") continue;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);\n }\n }\n try {\n return fn();\n } catch {\n return undefined;\n } finally {\n this.#release(token);\n }\n }\n\n /** Removes the lock when it is still this owner's. */\n #release(token: string): void {\n try {\n if (readFileSync(this.#lockFile, \"utf8\") === token) rmSync(this.#lockFile, { force: true });\n } catch {\n // Gone already.\n }\n }\n\n /**\n * Takes a crashed owner's lock away. The lock is moved aside under a name of this process's own\n * (a rename is atomic, so of two processes breaking it one gets it), and a lock that turns out\n * to be another, live owner's goes back. Answers `broken` when the caller should try again at\n * once, `absent` when no lock file exists, and `held` when the caller should wait: a live\n * owner's lock, or one that can't be read (on Windows, a lock being deleted).\n */\n #breakStale(): \"broken\" | \"absent\" | \"held\" {\n // One descriptor for the age and the contents, so both describe the same file.\n let seen: string;\n let age: number;\n let handle: number | undefined;\n try {\n handle = openSync(this.#lockFile, \"r\");\n age = Date.now() - fstatSync(handle).mtimeMs;\n if (age <= STALE_LOCK_MS) return \"held\";\n seen = readFileSync(handle, \"utf8\");\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"absent\" : \"held\";\n } finally {\n if (handle !== undefined) closeSync(handle);\n }\n if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return \"held\";\n const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;\n try {\n renameSync(this.#lockFile, aside);\n } catch (error) {\n // Another breaker moved it first: try for it at once; anything else, wait.\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"broken\" : \"held\";\n }\n try {\n if (readFileSync(aside, \"utf8\") !== seen) linkSync(aside, this.#lockFile);\n } catch {\n // Another process holds a new lock already; ours goes, and the caller waits for theirs.\n } finally {\n rmSync(aside, { force: true });\n }\n return \"broken\";\n }\n}\n\n/**\n * Renames `from` over `to`. Windows refuses to replace a file another process has open, as a\n * process reading the settings outside the lock does, with `EPERM` (MT-01: CI's install-ID race,\n * traced on `windows-latest`); the rename is tried again for up to a second, as the runtime's\n * renames are (D150 item 8), and never turned into a copy.\n */\nexport function replace(\n from: string,\n to: string,\n platform: string,\n rename: (from: string, to: string) => void = renameSync,\n): void {\n const deadline = Date.now() + 1000;\n for (let pause = 5; ; pause = Math.min(pause * 2, 100)) {\n try {\n rename(from, to);\n return;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n const shared = code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n if (platform !== \"win32\" || !shared || Date.now() > deadline) throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pause);\n }\n }\n}\n\n/** Whether this process can create a file in `dir`: a probe of its own, removed at once. */\nexport function writable(\n dir: string,\n remove: (path: string) => void = (path) => rmSync(path, { force: true }),\n): boolean {\n const probe = join(dir, `.probe-${process.pid}-${randomUUID()}`);\n try {\n writeFileSync(probe, \"\", { flag: \"wx\", mode: 0o600 });\n } catch {\n return false;\n }\n // The probe was made, so the directory is writable whatever its removal meets: on Windows an\n // antivirus or the indexer may hold the new file for a moment (EBUSY, EPERM), and telemetry never\n // stops a command (D140 item 5; Opus review of MT-01, 8). An empty probe left behind is harmless.\n try {\n remove(probe);\n } catch {\n // Left for the next probe's directory to carry; nothing reads it.\n }\n return true;\n}\n\n/**\n * Whether a lock's exclusive create failed because another process holds it. On Windows a lock\n * its owner just removed stays \"delete pending\" while anyone still has it open (another process\n * judging its age), and creating it again meanwhile is refused as access denied, not `EEXIST`\n * (WP-W2: twelve first runs at once on an arm64 runner): that is contention too, never a reason\n * to give up.\n */\nfunction held(code: string | undefined, platform: string): boolean {\n return code === \"EEXIST\" || (platform === \"win32\" && (code === \"EPERM\" || code === \"EACCES\"));\n}\n\n/** Whether `dir` can never be created: its nearest existing ancestor isn't a directory. */\nfunction blocked(dir: string): boolean {\n for (let path = dir; ; path = dirname(path)) {\n try {\n return !statSync(path).isDirectory();\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === \"ENOTDIR\") return true;\n if (code !== \"ENOENT\") return false;\n }\n if (dirname(path) === path) return false;\n }\n}\n\n/** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */\nfunction ownerAlive(lock: string): boolean {\n const pid = Number(lock.split(\" \")[0]);\n if (!Number.isSafeInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** The newest events within 1,000, none older than 7 days; lines that aren't JSON go. */\nfunction bounded(lines: readonly string[], now: number): string[] {\n const fresh = lines.filter((line) => {\n try {\n const at = Date.parse((JSON.parse(line) as { at?: string }).at ?? \"\");\n return Number.isFinite(at) && now - at <= QUEUE_MAX_AGE_MS;\n } catch {\n return false;\n }\n });\n return fresh.slice(-QUEUE_MAX_EVENTS);\n}\n","/**\n * Sending the queue (D140 item 6): a detached process the CLI starts after a command, at most one\n * at a time and at most once a minute. It reads the decision again before it sends, so consent\n * withdrawn in between drops the events; it posts at most 100 events per request with no header\n * but `Content-Type`, `User-Agent` and the ones HTTP itself needs, and keeps what a failed\n * attempt couldn't send for the next flush.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { join } from \"node:path\";\nimport { detachedDirectory, detachedEnvironment } from \"../detached.ts\";\nimport type { Environment } from \"../home.ts\";\nimport { cliVersion, fromSource, packageRoot } from \"../version.ts\";\nimport { buildEndpoint, decide } from \"./decision.ts\";\nimport { TelemetryEvent } from \"./events.ts\";\nimport { QUEUE_MAX_EVENTS, type TelemetryStore } from \"./store.ts\";\n\n/** At most this many events per request. */\nexport const BATCH = 100;\n/** A flush starts at most this often. */\nexport const FLUSH_INTERVAL_MS = 60_000;\n/** The flush interval; `MOSTRA_TEST_FLUSH_INTERVAL_MS` shortens it, for tests only. */\nfunction flushInterval(env: Environment): number {\n const test = Number(env[\"MOSTRA_TEST_FLUSH_INTERVAL_MS\"]);\n return Number.isInteger(test) && test >= 0 && test < FLUSH_INTERVAL_MS ? test : FLUSH_INTERVAL_MS;\n}\n\n/** One request's limit. */\nconst REQUEST_TIMEOUT_MS = 5000;\n\n/** Posts one batch; resolves with whether the collector took it (any 2xx). */\nexport function post(endpoint: string, body: string): Promise<boolean> {\n return new Promise((resolve) => {\n const url = new URL(endpoint);\n const send = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n const req = send(\n url,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": `mostra-cli/${cliVersion()}`,\n \"Content-Length\": Buffer.byteLength(body),\n },\n timeout: REQUEST_TIMEOUT_MS,\n },\n (response) => {\n response.resume();\n response.on(\"end\", () =>\n resolve((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300),\n );\n response.on(\"error\", () => resolve(false));\n },\n );\n req.on(\"timeout\", () => req.destroy());\n req.on(\"error\", () => resolve(false));\n req.end(body);\n });\n}\n\n/**\n * Sends the queue: the decision again, then batches until one fails. Events the schema refuses\n * never leave the machine.\n */\nexport async function flush(\n store: TelemetryStore,\n env: Environment,\n now: () => number,\n): Promise<void> {\n const settings = store.read();\n // The queue holds only events of uses that aren't embedded; the rest of the order applies again.\n const decision = decide({\n env,\n buildEndpoint: buildEndpoint(),\n embedded: false,\n settings,\n askFirst: false,\n });\n if (!decision.enabled || decision.endpoint === undefined) {\n // Consent withdrawn (or never given) since the events were queued: they go unsent.\n if (decision.reason !== \"no-collector\" && settings.ok) store.drop(store.queued(now()), now());\n return;\n }\n // The queue holds at most QUEUE_MAX_EVENTS, so a flush needs at most this many batches.\n for (let round = 0; round <= Math.ceil(QUEUE_MAX_EVENTS / BATCH); round += 1) {\n const lines = store.queued(now()).slice(0, BATCH);\n if (lines.length === 0) return;\n const events: unknown[] = [];\n for (const line of lines) {\n try {\n const parsed = TelemetryEvent.safeParse(JSON.parse(line));\n if (parsed.success) events.push(parsed.data);\n } catch {\n // Not an event: dropped with the batch.\n }\n }\n const sent = events.length === 0 || (await post(decision.endpoint, JSON.stringify({ events })));\n if (!sent) return;\n // Nothing removed (another process changed the queue, or the lock was out of reach): stop\n // rather than send the same batch again.\n if (store.drop(lines, now()) === 0) return;\n }\n}\n\n/** The flusher's entry in the CLI's package. */\nfunction flusherEntry(): { readonly entry: string; readonly nodeArgs: readonly string[] } {\n const root = packageRoot();\n return fromSource()\n ? {\n entry: join(root, \"src\", \"telemetry\", \"flush-main.ts\"),\n nodeArgs: [\"--conditions=@mostra/source\"],\n }\n : { entry: join(root, \"dist\", \"telemetry\", \"flush-main.js\"), nodeArgs: [] };\n}\n\n/**\n * Starts a flusher when the queue has events and no flush started in the last minute, fully\n * detached: no stdio, its own process group, never waited for, so `mostra … | jq` and every\n * harness reading to end of file finish when the command does.\n */\nexport function startFlusher(store: TelemetryStore, env: Environment, now: number): void {\n if (store.queued(now).length === 0) return;\n let due = false;\n store.update((current) => {\n const last = current.lastFlushAt === undefined ? 0 : Date.parse(current.lastFlushAt);\n if (Number.isFinite(last) && now - last < flushInterval(env)) return current;\n due = true;\n return { ...current, lastFlushAt: new Date(now).toISOString() };\n });\n if (!due) return;\n const { entry, nodeArgs } = flusherEntry();\n try {\n const child = spawn(process.execPath, [...nodeArgs, entry, store.dir], {\n detached: true,\n stdio: \"ignore\",\n // Not the command's directory but its file system's root, which always exists: on Windows\n // a process's working directory can't be removed or renamed while it runs, and the flusher\n // outlives the command (WP-W2).\n cwd: detachedDirectory(store.dir),\n env: detachedEnvironment({ ...process.env, ...env }) as NodeJS.ProcessEnv,\n });\n child.on(\"error\", () => {});\n child.unref();\n } catch {\n // Telemetry never changes a command's outcome.\n }\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Diagnostic, MethodDefinition } from "@mostra/protocol";
2
+ import "@mostra/sdk";
2
3
  //#region src/home.d.ts
3
4
  /** The environment the CLI reads. */
4
5
  type Environment = Readonly<Record<string, string | undefined>>;
@@ -222,6 +223,8 @@ interface RunnerIo {
222
223
  };
223
224
  /** The platform; default process.platform. */
224
225
  readonly platform?: string;
226
+ /** The architecture; default process.arch. */
227
+ readonly arch?: string;
225
228
  }
226
229
  /** Runs one command line and returns its exit code. */
227
230
  export declare function run(io: RunnerIo): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/home.ts","../src/client.ts","../src/command.ts","../src/commands/index.ts","../src/exit.ts","../src/help.ts","../src/run.ts"],"mappings":";;;KAWY,cAAc,SAAS;;;;UCkBlB;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,SAAS;;WAET,mBAAmB;;EAE5B,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,GAAG,qBAAqB,QAAQ;;EAEnF,eAAe,gBAAgB,UAAU;;WAEhC,QAAQ;;EAEjB;;;;;KC/CU;;UAGK;;WAEN;;WAEA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,MAAM,SAAS;;WAEf,OAAO,SACd;;WAGO;;WAEA;;WAEA;;EAET,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,IAAI,QAAQ;;EAE/D,aAAa;;EAEb,IAAI;;EAEJ,KAAK,gBAAgB;;EAErB,KAAK,YAAY;;WAER,KAAK,SAAS;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;EAET,OAAO,OAAO;;;;;EAKd,WAAW,sBAAsB,QAAQ;;EAEzC,QACE,yBACA,6BACC;IAAU;IAAc;;;WAElB;;WAEA,SAAS;;WAET,SAAS;;;UAIH;;WAEN;;WAEA,QAAQ,SAAS;;;UAIX;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA,gBAAgB;;WAEhB,iBAAiB;;WAEjB;aAAmB;aAAuB;;;WAE1C;;WAEA;;WAEA,yBAAyB,OAAO;;WAEhC;;WAEA;;WAEA;;EAET,IAAI,SAAS,iBAAiB,QAAQ;;;;;qBChG3B,mBAAmB;;;;qBC9BnB;;WAEX;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;KAIU,mBAAmB,mBAAmB;;wBAwDlC,YAAY,eAAe;;;;wBCb3B,YAAY,SAAS;;wBAqDrB,kBAAkB,SAAS,aAAa;;wBA4BxC,kBAAkB,mBAAmB;;;;UChHpC;;WAEN;;WAEA,KAAK;;WAEL;;WAEA,SAAS;;WAET,SAAS;;WAET,OAAO,OAAO;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;;wBA+DW,IAAI,IAAI,WAAW"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/home.ts","../src/client.ts","../src/command.ts","../src/commands/index.ts","../src/exit.ts","../src/help.ts","../src/run.ts"],"mappings":";;;;KAWY,cAAc,SAAS;;;;UCoBlB;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,SAAS;;WAET,mBAAmB;;EAE5B,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,GAAG,qBAAqB,QAAQ;;EAEnF,eAAe,gBAAgB,UAAU;;WAEhC,QAAQ;;EAEjB;;;;;KCjDU;;UAGK;;WAEN;;WAEA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,MAAM,SAAS;;WAEf,OAAO,SACd;;WAGO;;WAEA;;WAEA;;EAET,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,IAAI,QAAQ;;EAE/D,aAAa;;EAEb,IAAI;;EAEJ,KAAK,gBAAgB;;EAErB,KAAK,YAAY;;WAER,KAAK,SAAS;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;EAET,OAAO,OAAO;;;;;EAKd,WAAW,sBAAsB,QAAQ;;EAEzC,QACE,yBACA,6BACC;IAAU;IAAc;;;WAElB;;WAEA,SAAS;;WAET,SAAS;;;UAIH;;WAEN;;WAEA,QAAQ,SAAS;;;UAIX;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA,gBAAgB;;WAEhB,iBAAiB;;WAEjB;aAAmB;aAAuB;;;WAE1C;;WAEA;;WAEA,yBAAyB,OAAO;;WAEhC;;WAEA;;WAEA;;EAET,IAAI,SAAS,iBAAiB,QAAQ;;;;;qBChG3B,mBAAmB;;;;qBC9BnB;;WAEX;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;KAIU,mBAAmB,mBAAmB;;wBAwDlC,YAAY,eAAe;;;;wBCb3B,YAAY,SAAS;;wBAqDrB,kBAAkB,SAAS,aAAa;;wBA4BxC,kBAAkB,mBAAmB;;;;UC/GpC;;WAEN;;WAEA,KAAK;;WAEL;;WAEA,SAAS;;WAET,SAAS;;WAET,OAAO,OAAO;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;WAEA;;;wBA+DW,IAAI,IAAI,WAAW"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as COMMANDS, i as referenceMarkdown, n as commandDefinition, r as commandHelp, t as run } from "./run-BJaRnz-Q.js";
1
+ import { a as COMMANDS, i as referenceMarkdown, n as commandDefinition, r as commandHelp, t as run } from "./run-BpVu5uQ8.js";
2
2
  import { n as exitCodeFor, t as EXIT } from "./exit-CBIf8nAU.js";
3
3
  export { COMMANDS, EXIT, commandDefinition, commandHelp, exitCodeFor, referenceMarkdown, run };
@@ -715,13 +715,13 @@ Example:
715
715
  ## mostra daemon install
716
716
 
717
717
  ```text
718
- Run the home's daemon as a login service (launchd or systemd), so links answer at any hour.
718
+ Run the home's daemon as a login service (launchd, systemd or Task Scheduler), so links answer at any hour.
719
719
 
720
720
  Usage:
721
721
  mostra daemon install [flags]
722
722
 
723
723
  Flags:
724
- --no-start Write or remove the service's file without asking launchd or systemd to load it.
724
+ --no-start Write or remove the service's definition without asking launchd, systemd or Task Scheduler to load it.
725
725
 
726
726
  Global flags:
727
727
  --json Print one JSON envelope on stdout (NDJSON for streams).
@@ -751,7 +751,7 @@ Usage:
751
751
  mostra daemon uninstall [flags]
752
752
 
753
753
  Flags:
754
- --no-start Write or remove the service's file without asking launchd or systemd to load it.
754
+ --no-start Write or remove the service's definition without asking launchd, systemd or Task Scheduler to load it.
755
755
 
756
756
  Global flags:
757
757
  --json Print one JSON envelope on stdout (NDJSON for streams).
@@ -420,8 +420,8 @@
420
420
  "phase": "host",
421
421
  "title": "Login service could not be installed",
422
422
  "hint": "Read the message, fix what it names, and run the install again.",
423
- "explanation": "`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent or systemd user unit failed, or `launchctl` or `systemctl --user` refused to load it.",
424
- "fix": "Check the named file's directory permissions, and on Linux that a systemd user session is running (`systemctl --user status`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`."
423
+ "explanation": "`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent, systemd user unit or Task Scheduler task failed, or `launchctl`, `systemctl --user` or `schtasks` refused it.",
424
+ "fix": "Check the named file's directory permissions; on Linux, that a systemd user session is running (`systemctl --user status`); on Windows, that Task Scheduler runs and your account may create tasks (`schtasks /Query`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`."
425
425
  },
426
426
  {
427
427
  "code": "MOSTRA_ENV_SHELL_MISSING",
@@ -583,7 +583,7 @@
583
583
  "title": "Long-lived artifacts without an always-on daemon",
584
584
  "hint": "Run `mostra daemon install` so the home's daemon runs as a login service and these artifacts answer at any hour.",
585
585
  "explanation": "The home has artifacts with submission policy `none`, which are meant to stay open (dashboards, task managers, D63), but no login service runs its daemon. Without one, the daemon exits after 30 idle minutes and their links stop answering until a command starts it again.",
586
- "fix": "Run `mostra daemon install`. It registers a launchd or systemd user service that keeps the home's daemon running; `mostra daemon uninstall` removes it."
586
+ "fix": "Run `mostra daemon install`. It registers a login service (a launchd agent, a systemd user service or a Task Scheduler task) that keeps the home's daemon running; `mostra daemon uninstall` removes it."
587
587
  },
588
588
  {
589
589
  "code": "MOSTRA_HOME_NOT_OWNED",
@@ -12,11 +12,11 @@ Login service could not be installed. Severity: error. Phase: host.
12
12
 
13
13
  ## What it means
14
14
 
15
- `mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent or systemd user unit failed, or `launchctl` or `systemctl --user` refused to load it.
15
+ `mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent, systemd user unit or Task Scheduler task failed, or `launchctl`, `systemctl --user` or `schtasks` refused it.
16
16
 
17
17
  ## How to fix it
18
18
 
19
- Check the named file's directory permissions, and on Linux that a systemd user session is running (`systemctl --user status`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`.
19
+ Check the named file's directory permissions; on Linux, that a systemd user session is running (`systemctl --user status`); on Windows, that Task Scheduler runs and your account may create tasks (`schtasks /Query`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`.
20
20
 
21
21
  ## Hint
22
22
 
@@ -16,7 +16,7 @@ The home has artifacts with submission policy `none`, which are meant to stay op
16
16
 
17
17
  ## How to fix it
18
18
 
19
- Run `mostra daemon install`. It registers a launchd or systemd user service that keeps the home's daemon running; `mostra daemon uninstall` removes it.
19
+ Run `mostra daemon install`. It registers a login service (a launchd agent, a systemd user service or a Task Scheduler task) that keeps the home's daemon running; `mostra daemon uninstall` removes it.
20
20
 
21
21
  ## Hint
22
22
 
@@ -51,7 +51,7 @@ one.
51
51
  | `at` | `2026-11-10T14:05:00Z` | When, to the minute |
52
52
  | `installId` | a random UUID | Count installs without knowing who you are |
53
53
  | `version` | `1.0.0` | Know which versions fail |
54
- | `os`, `arch` | `darwin`, `arm64` | Platform support |
54
+ | `os`, `arch` | `darwin`, `linux` or `win32`; `x64` or `arm64` | Platform support |
55
55
  | `node` | `22` | Which Node versions to support |
56
56
  | `harness` | `claude-code`, `codex`, `gemini-cli`, `opencode`, `cursor`, `other`, `none` | Which agents to work with next |
57
57
  | `ci` | `false` | Separate CI from people |