@mostra/cli 0.0.0-stage → 0.1.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.
Files changed (195) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +18 -2
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +79 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/bootstrap-DfEe_yPr.js +113 -0
  7. package/dist/bootstrap-DfEe_yPr.js.map +1 -0
  8. package/dist/exit-CBIf8nAU.js +79 -0
  9. package/dist/exit-CBIf8nAU.js.map +1 -0
  10. package/dist/flush-BCuWs0Zh.js +718 -0
  11. package/dist/flush-BCuWs0Zh.js.map +1 -0
  12. package/dist/index.d.ts +230 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +3 -0
  15. package/dist/reference/cli.md +977 -0
  16. package/dist/reference/diagnostics.json +1255 -0
  17. package/dist/reference/errors/MOSTRA_A11Y_MINOR.md +23 -0
  18. package/dist/reference/errors/MOSTRA_A11Y_SERIOUS.md +23 -0
  19. package/dist/reference/errors/MOSTRA_ACTION_CANCELLED.md +23 -0
  20. package/dist/reference/errors/MOSTRA_ACTION_EVENT_INVALID.md +23 -0
  21. package/dist/reference/errors/MOSTRA_ACTION_EXPORT_INVALID.md +23 -0
  22. package/dist/reference/errors/MOSTRA_ACTION_INTERRUPTED.md +23 -0
  23. package/dist/reference/errors/MOSTRA_ACTION_RUN_NOT_FOUND.md +23 -0
  24. package/dist/reference/errors/MOSTRA_ACTION_THREW.md +23 -0
  25. package/dist/reference/errors/MOSTRA_ACTION_TOO_LARGE.md +23 -0
  26. package/dist/reference/errors/MOSTRA_ACTION_UNKNOWN_FUNCTION.md +23 -0
  27. package/dist/reference/errors/MOSTRA_ACTION_UNSERIALIZABLE.md +23 -0
  28. package/dist/reference/errors/MOSTRA_ARTIFACT_DIR_NOT_EMPTY.md +23 -0
  29. package/dist/reference/errors/MOSTRA_ARTIFACT_ENTRY_AMBIGUOUS.md +23 -0
  30. package/dist/reference/errors/MOSTRA_ARTIFACT_NOT_FOUND.md +23 -0
  31. package/dist/reference/errors/MOSTRA_ARTIFACT_NO_ENTRY.md +23 -0
  32. package/dist/reference/errors/MOSTRA_ARTIFACT_NO_PAGE.md +23 -0
  33. package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_MISSING.md +23 -0
  34. package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_UNREADABLE.md +23 -0
  35. package/dist/reference/errors/MOSTRA_ARTIFACT_TEMPLATE_UNKNOWN.md +23 -0
  36. package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_COLLECTED.md +23 -0
  37. package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_NOT_FOUND.md +23 -0
  38. package/dist/reference/errors/MOSTRA_BUILD_CRASHED.md +23 -0
  39. package/dist/reference/errors/MOSTRA_BUILD_FAILED.md +23 -0
  40. package/dist/reference/errors/MOSTRA_BUILD_TIMEOUT.md +23 -0
  41. package/dist/reference/errors/MOSTRA_BUILD_UNRESOLVED.md +23 -0
  42. package/dist/reference/errors/MOSTRA_CLI_CONFIG_UNWRITABLE.md +23 -0
  43. package/dist/reference/errors/MOSTRA_CLI_UNKNOWN_COMMAND.md +23 -0
  44. package/dist/reference/errors/MOSTRA_CLI_USAGE.md +23 -0
  45. package/dist/reference/errors/MOSTRA_CONSOLE_ERROR.md +23 -0
  46. package/dist/reference/errors/MOSTRA_CONSOLE_WARNING.md +23 -0
  47. package/dist/reference/errors/MOSTRA_DEP_INSTALL_FAILED.md +23 -0
  48. package/dist/reference/errors/MOSTRA_DEP_MISSING.md +23 -0
  49. package/dist/reference/errors/MOSTRA_DEP_NOT_FOUND.md +23 -0
  50. package/dist/reference/errors/MOSTRA_DEP_OFFLINE.md +23 -0
  51. package/dist/reference/errors/MOSTRA_DEP_SPEC_INVALID.md +23 -0
  52. package/dist/reference/errors/MOSTRA_DEP_TIMEOUT.md +23 -0
  53. package/dist/reference/errors/MOSTRA_DEP_WARM_CONFLICT.md +23 -0
  54. package/dist/reference/errors/MOSTRA_ENV_BIND_FAILED.md +23 -0
  55. package/dist/reference/errors/MOSTRA_ENV_BROWSER_MISSING.md +23 -0
  56. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED.md +23 -0
  57. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_INVALID.md +23 -0
  58. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_UNAVAILABLE.md +23 -0
  59. package/dist/reference/errors/MOSTRA_ENV_NODE_UNSUPPORTED.md +23 -0
  60. package/dist/reference/errors/MOSTRA_ENV_PORT_TAKEN.md +23 -0
  61. package/dist/reference/errors/MOSTRA_ENV_RUNTIME_MISSING.md +23 -0
  62. package/dist/reference/errors/MOSTRA_ENV_SERVICE_FAILED.md +23 -0
  63. package/dist/reference/errors/MOSTRA_ENV_SHELL_MISSING.md +23 -0
  64. package/dist/reference/errors/MOSTRA_ENV_WARM_MISSING.md +23 -0
  65. package/dist/reference/errors/MOSTRA_FIELD_CONFLICT.md +23 -0
  66. package/dist/reference/errors/MOSTRA_FIELD_DUPLICATE_ID.md +23 -0
  67. package/dist/reference/errors/MOSTRA_FIELD_ID_INVALID.md +23 -0
  68. package/dist/reference/errors/MOSTRA_FIELD_ID_UNSTABLE.md +23 -0
  69. package/dist/reference/errors/MOSTRA_FIELD_MUTATION_ID_REUSED.md +23 -0
  70. package/dist/reference/errors/MOSTRA_FIELD_SHARED.md +23 -0
  71. package/dist/reference/errors/MOSTRA_FIELD_TOO_LARGE.md +23 -0
  72. package/dist/reference/errors/MOSTRA_FIELD_UNBOUND.md +23 -0
  73. package/dist/reference/errors/MOSTRA_HOME_DISK_FULL.md +23 -0
  74. package/dist/reference/errors/MOSTRA_HOME_GC_INCOMPLETE.md +23 -0
  75. package/dist/reference/errors/MOSTRA_HOME_GONE.md +23 -0
  76. package/dist/reference/errors/MOSTRA_HOME_INTEGRITY.md +23 -0
  77. package/dist/reference/errors/MOSTRA_HOME_LEDGER_UNREADABLE.md +23 -0
  78. package/dist/reference/errors/MOSTRA_HOME_LOCKED.md +23 -0
  79. package/dist/reference/errors/MOSTRA_HOME_MISMATCH.md +23 -0
  80. package/dist/reference/errors/MOSTRA_HOME_NOT_ALWAYS_ON.md +23 -0
  81. package/dist/reference/errors/MOSTRA_HOME_NOT_OWNED.md +23 -0
  82. package/dist/reference/errors/MOSTRA_HOME_SOCKET_TOO_LONG.md +23 -0
  83. package/dist/reference/errors/MOSTRA_HOME_TOO_NEW.md +23 -0
  84. package/dist/reference/errors/MOSTRA_HOST_CONNECTION_CLOSED.md +23 -0
  85. package/dist/reference/errors/MOSTRA_HOST_CURSOR_COMPACTED.md +23 -0
  86. package/dist/reference/errors/MOSTRA_HOST_INITIALIZE_ORDER.md +23 -0
  87. package/dist/reference/errors/MOSTRA_HOST_INTERNAL_ERROR.md +23 -0
  88. package/dist/reference/errors/MOSTRA_HOST_INVALID_PARAMS.md +23 -0
  89. package/dist/reference/errors/MOSTRA_HOST_MALFORMED.md +23 -0
  90. package/dist/reference/errors/MOSTRA_HOST_MESSAGE_TOO_LARGE.md +23 -0
  91. package/dist/reference/errors/MOSTRA_HOST_METHOD_UNKNOWN.md +23 -0
  92. package/dist/reference/errors/MOSTRA_HOST_NOTIFICATION_UNSUPPORTED.md +23 -0
  93. package/dist/reference/errors/MOSTRA_HOST_PORT_UNSUPPORTED.md +23 -0
  94. package/dist/reference/errors/MOSTRA_HOST_REQUEST_ID_REUSED.md +23 -0
  95. package/dist/reference/errors/MOSTRA_HOST_REVISION_UNSUPPORTED.md +23 -0
  96. package/dist/reference/errors/MOSTRA_HOST_SHUTDOWN_REFUSED.md +23 -0
  97. package/dist/reference/errors/MOSTRA_HOST_SUBSCRIPTION_UNKNOWN.md +23 -0
  98. package/dist/reference/errors/MOSTRA_HOST_TIMEOUT.md +23 -0
  99. package/dist/reference/errors/MOSTRA_HYGIENE_CONTENT_TYPE.md +23 -0
  100. package/dist/reference/errors/MOSTRA_HYGIENE_CSRF_MISSING.md +23 -0
  101. package/dist/reference/errors/MOSTRA_HYGIENE_HEADERS_TOO_LARGE.md +23 -0
  102. package/dist/reference/errors/MOSTRA_HYGIENE_HOST_REFUSED.md +23 -0
  103. package/dist/reference/errors/MOSTRA_HYGIENE_NOT_SERVED.md +23 -0
  104. package/dist/reference/errors/MOSTRA_HYGIENE_ORIGIN_REFUSED.md +23 -0
  105. package/dist/reference/errors/MOSTRA_HYGIENE_RATE_LIMITED.md +23 -0
  106. package/dist/reference/errors/MOSTRA_HYGIENE_REQUEST_MALFORMED.md +23 -0
  107. package/dist/reference/errors/MOSTRA_HYGIENE_SCOPE_DENIED.md +23 -0
  108. package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_CONFLICT.md +23 -0
  109. package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_MISSING.md +23 -0
  110. package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_INVALID.md +23 -0
  111. package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_REVOKED.md +23 -0
  112. package/dist/reference/errors/MOSTRA_LIVE_SOURCE_FAILED.md +23 -0
  113. package/dist/reference/errors/MOSTRA_LIVE_TOO_LARGE.md +23 -0
  114. package/dist/reference/errors/MOSTRA_MANIFEST_INVALID.md +23 -0
  115. package/dist/reference/errors/MOSTRA_MANIFEST_NOT_LITERAL.md +23 -0
  116. package/dist/reference/errors/MOSTRA_MANIFEST_UNKNOWN_KEY.md +23 -0
  117. package/dist/reference/errors/MOSTRA_NETWORK_EXTERNAL_FAILED.md +23 -0
  118. package/dist/reference/errors/MOSTRA_NETWORK_REQUEST_FAILED.md +23 -0
  119. package/dist/reference/errors/MOSTRA_PLATFORM_UNSUPPORTED.md +23 -0
  120. package/dist/reference/errors/MOSTRA_PUBLISH_ID_CONFLICT.md +23 -0
  121. package/dist/reference/errors/MOSTRA_PUBLISH_SOURCE_CHANGED.md +23 -0
  122. package/dist/reference/errors/MOSTRA_PUBLISH_VERIFY_FAILED.md +23 -0
  123. package/dist/reference/errors/MOSTRA_REGRESSION_ACTION_REMOVED.md +23 -0
  124. package/dist/reference/errors/MOSTRA_REGRESSION_ARIA.md +23 -0
  125. package/dist/reference/errors/MOSTRA_REGRESSION_FIELD_REMOVED.md +23 -0
  126. package/dist/reference/errors/MOSTRA_REGRESSION_LIVE_REMOVED.md +23 -0
  127. package/dist/reference/errors/MOSTRA_REGRESSION_PAGE_REMOVED.md +23 -0
  128. package/dist/reference/errors/MOSTRA_REGRESSION_PIXELS.md +23 -0
  129. package/dist/reference/errors/MOSTRA_RUNTIME_BLANK.md +23 -0
  130. package/dist/reference/errors/MOSTRA_RUNTIME_NOT_READY.md +23 -0
  131. package/dist/reference/errors/MOSTRA_RUNTIME_PAGE_ERROR.md +23 -0
  132. package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_CRASHED.md +23 -0
  133. package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_PARKED.md +23 -0
  134. package/dist/reference/errors/MOSTRA_SECRET_MISSING.md +23 -0
  135. package/dist/reference/errors/MOSTRA_SMOKE_BUDGET.md +23 -0
  136. package/dist/reference/errors/MOSTRA_SMOKE_NO_EFFECT.md +23 -0
  137. package/dist/reference/errors/MOSTRA_SUBMIT_ALREADY.md +23 -0
  138. package/dist/reference/errors/MOSTRA_SUBMIT_BLOCKED.md +23 -0
  139. package/dist/reference/errors/MOSTRA_SUBMIT_CHANNEL_DENIED.md +23 -0
  140. package/dist/reference/errors/MOSTRA_SUBMIT_NOT_FOUND.md +23 -0
  141. package/dist/reference/errors/MOSTRA_SUBMIT_POLICY_NONE.md +23 -0
  142. package/dist/reference/errors/MOSTRA_SUBMIT_REVISION_MOVED.md +23 -0
  143. package/dist/reference/errors/MOSTRA_SUBMIT_SIGNAL_INVALID.md +23 -0
  144. package/dist/reference/errors/MOSTRA_SUBMIT_VIEW_INVALID.md +23 -0
  145. package/dist/reference/errors/MOSTRA_TARGET_UNAVAILABLE.md +23 -0
  146. package/dist/reference/errors/MOSTRA_TYPE_ERROR.md +23 -0
  147. package/dist/reference/errors/MOSTRA_TYPE_UNTYPED_PACKAGE.md +23 -0
  148. package/dist/reference/errors/MOSTRA_VERIFY_ACTION_SKIPPED.md +23 -0
  149. package/dist/reference/errors/MOSTRA_VERIFY_BASELINE_MISSING.md +23 -0
  150. package/dist/reference/errors/MOSTRA_VERIFY_CRASHED.md +23 -0
  151. package/dist/reference/errors/MOSTRA_VERIFY_LIVE_FAILED.md +23 -0
  152. package/dist/reference/errors/MOSTRA_VERIFY_NO_BASELINE.md +23 -0
  153. package/dist/reference/errors/MOSTRA_VERIFY_SECRET_MISSING.md +23 -0
  154. package/dist/reference/errors/MOSTRA_VERIFY_SKIP_CONSEQUENCE.md +23 -0
  155. package/dist/reference/errors/MOSTRA_VERIFY_TIMEOUT.md +23 -0
  156. package/dist/reference/telemetry.md +82 -0
  157. package/dist/run-D1gRM6Bm.js +2 -0
  158. package/dist/run-Du266XjI.js +3824 -0
  159. package/dist/run-Du266XjI.js.map +1 -0
  160. package/dist/telemetry/flush-main.d.ts +1 -0
  161. package/dist/telemetry/flush-main.js +14 -0
  162. package/dist/telemetry/flush-main.js.map +1 -0
  163. package/package.json +62 -3
  164. package/src/bin.ts +70 -0
  165. package/src/bootstrap.ts +156 -0
  166. package/src/browser.ts +186 -0
  167. package/src/client.ts +313 -0
  168. package/src/command.ts +147 -0
  169. package/src/commands/admin.ts +791 -0
  170. package/src/commands/authoring.ts +244 -0
  171. package/src/commands/dev.ts +106 -0
  172. package/src/commands/index.ts +71 -0
  173. package/src/commands/interaction.ts +288 -0
  174. package/src/commands/shared.ts +71 -0
  175. package/src/commands/viewing.ts +153 -0
  176. package/src/docs.ts +111 -0
  177. package/src/exit.ts +89 -0
  178. package/src/help.ts +170 -0
  179. package/src/home.ts +111 -0
  180. package/src/index.ts +23 -0
  181. package/src/node-gate.ts +15 -0
  182. package/src/parse.ts +246 -0
  183. package/src/run.ts +387 -0
  184. package/src/telemetry/decision.ts +101 -0
  185. package/src/telemetry/events.ts +147 -0
  186. package/src/telemetry/flush-main.ts +13 -0
  187. package/src/telemetry/flush.ts +143 -0
  188. package/src/telemetry/harness.ts +39 -0
  189. package/src/telemetry/index.ts +318 -0
  190. package/src/telemetry/store.ts +379 -0
  191. package/src/telemetry/timezone.ts +72 -0
  192. package/src/telemetry/words.ts +87 -0
  193. package/src/telemetry/zone-sets.ts +182 -0
  194. package/src/telemetry/zones.generated.ts +720 -0
  195. package/src/version.ts +43 -0
@@ -0,0 +1,718 @@
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";
4
+ import { DIAGNOSTIC_AREAS } from "@mostra/protocol";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as z from "zod";
7
+ import { request } from "node:http";
8
+ import { request as request$1 } from "node:https";
9
+ import { randomUUID } from "node:crypto";
10
+ //#region src/version.ts
11
+ /** The CLI's own package: its root directory and its release. */
12
+ let found;
13
+ function own() {
14
+ if (found) return found;
15
+ let dir = dirname(fileURLToPath(import.meta.url));
16
+ for (;;) {
17
+ try {
18
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
19
+ if (manifest.name === "@mostra/cli" && manifest.version) {
20
+ found = {
21
+ root: dir,
22
+ version: manifest.version
23
+ };
24
+ return found;
25
+ }
26
+ } catch {}
27
+ const parent = dirname(dir);
28
+ if (parent === dir) throw new Error("Couldn't find @mostra/cli's package.json.");
29
+ dir = parent;
30
+ }
31
+ }
32
+ /** This CLI's version (`mostra --version`, the client version it reports, telemetry's `version`). */
33
+ function cliVersion() {
34
+ return own().version;
35
+ }
36
+ /** The CLI package's directory: its `dist/`, `src/` and bundled reference live under it. */
37
+ function packageRoot() {
38
+ return own().root;
39
+ }
40
+ /** Whether this CLI runs from its TypeScript sources (tests), not its build. */
41
+ function fromSource() {
42
+ return import.meta.url.endsWith(".ts");
43
+ }
44
+ //#endregion
45
+ //#region src/telemetry/decision.ts
46
+ /** The build's endpoint: empty in this package and in every development build. */
47
+ function buildEndpoint() {
48
+ return "";
49
+ }
50
+ const OFF_VALUES = /* @__PURE__ */ new Set([
51
+ "0",
52
+ "false",
53
+ "off",
54
+ "no"
55
+ ]);
56
+ const ON_VALUES = /* @__PURE__ */ new Set([
57
+ "1",
58
+ "true",
59
+ "on",
60
+ "yes"
61
+ ]);
62
+ /** `DO_NOT_TRACK` and `MOSTRA_OFFLINE`'s parser: set to anything but empty, `0` or `false`. */
63
+ function truthy(value) {
64
+ if (value === void 0) return false;
65
+ const normalized = value.trim().toLowerCase();
66
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
67
+ }
68
+ /**
69
+ * The endpoint in use: `MOSTRA_TELEMETRY_ENDPOINT` when it is `https:` or `http:` to a loopback
70
+ * address, else the build's; `undefined` when there is none (D140 item 7).
71
+ */
72
+ function endpointOf(env, build) {
73
+ const override = env["MOSTRA_TELEMETRY_ENDPOINT"];
74
+ const chosen = override !== void 0 && override !== "" ? override : build;
75
+ if (chosen === "") return void 0;
76
+ let url;
77
+ try {
78
+ url = new URL(chosen);
79
+ } catch {
80
+ return;
81
+ }
82
+ if (url.username !== "" || url.password !== "") return void 0;
83
+ if (url.protocol === "https:") return url.href;
84
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
85
+ return url.protocol === "http:" && loopback ? url.href : void 0;
86
+ }
87
+ /** Decides, in D140's order. */
88
+ function decide(inputs) {
89
+ const { askFirst } = inputs;
90
+ const off = (reason) => ({
91
+ enabled: false,
92
+ reason,
93
+ askFirst
94
+ });
95
+ const endpoint = endpointOf(inputs.env, inputs.buildEndpoint);
96
+ if (endpoint === void 0) return off("no-collector");
97
+ const at = {
98
+ askFirst,
99
+ endpoint
100
+ };
101
+ if (inputs.embedded) return {
102
+ ...at,
103
+ enabled: false,
104
+ reason: "embedded"
105
+ };
106
+ if (truthy(inputs.env["DO_NOT_TRACK"])) return {
107
+ ...at,
108
+ enabled: false,
109
+ reason: "do-not-track"
110
+ };
111
+ const normalized = inputs.env["MOSTRA_TELEMETRY"]?.trim().toLowerCase();
112
+ if (normalized !== void 0 && OFF_VALUES.has(normalized)) return {
113
+ ...at,
114
+ enabled: false,
115
+ reason: "variable"
116
+ };
117
+ if (!inputs.settings.ok) return {
118
+ ...at,
119
+ enabled: false,
120
+ reason: "unreadable"
121
+ };
122
+ const choice = inputs.settings.settings.choice;
123
+ if (choice === "off") return {
124
+ ...at,
125
+ enabled: false,
126
+ reason: "setting"
127
+ };
128
+ if (normalized !== void 0 && normalized !== "") return ON_VALUES.has(normalized) ? {
129
+ ...at,
130
+ enabled: true,
131
+ reason: "variable",
132
+ consent: "explicit"
133
+ } : {
134
+ ...at,
135
+ enabled: false,
136
+ reason: "variable-invalid"
137
+ };
138
+ if (choice === "on") return {
139
+ ...at,
140
+ enabled: true,
141
+ reason: "setting",
142
+ consent: "explicit"
143
+ };
144
+ return askFirst ? {
145
+ ...at,
146
+ enabled: false,
147
+ reason: "default"
148
+ } : {
149
+ ...at,
150
+ enabled: true,
151
+ reason: "default",
152
+ consent: "default"
153
+ };
154
+ }
155
+ //#endregion
156
+ //#region src/telemetry/harness.ts
157
+ /** The agents Mostra recognizes, in the order their markers are checked. */
158
+ const HARNESSES = [
159
+ "claude-code",
160
+ "codex",
161
+ "gemini-cli",
162
+ "opencode",
163
+ "cursor"
164
+ ];
165
+ /** Each agent's own marker. */
166
+ const MARKERS = {
167
+ "claude-code": "CLAUDECODE",
168
+ codex: "CODEX_THREAD_ID",
169
+ "gemini-cli": "GEMINI_CLI",
170
+ opencode: "OPENCODE",
171
+ cursor: "CURSOR_AGENT"
172
+ };
173
+ const set = (value) => value !== void 0 && value !== "";
174
+ /**
175
+ * The agent that ran this process: `AI_AGENT` when it starts with a known ID (the convention
176
+ * `@vercel/detect-agent` reads), else the first agent whose own marker is set, else `other` when
177
+ * `AI_AGENT` or `AGENT` names something else, else `none`. Cursor's editor terminal sets
178
+ * `CURSOR_TRACE_ID` without `CURSOR_AGENT`: that is a person, so it isn't `cursor`.
179
+ */
180
+ function detectHarness(env) {
181
+ const declared = env["AI_AGENT"];
182
+ if (set(declared)) {
183
+ const known = HARNESSES.find((id) => declared?.startsWith(id));
184
+ if (known) return known;
185
+ }
186
+ for (const id of HARNESSES) if (set(env[MARKERS[id]])) return id;
187
+ if (set(declared) || set(env["AGENT"])) return "other";
188
+ return "none";
189
+ }
190
+ /** The duration buckets of `cli.command`. */
191
+ const DURATIONS = [
192
+ "<1s",
193
+ "1-5s",
194
+ "5-30s",
195
+ "30s-2m",
196
+ "2-10m",
197
+ ">10m"
198
+ ];
199
+ /** A command's duration bucket. */
200
+ function durationBucket(ms) {
201
+ if (ms < 1e3) return "<1s";
202
+ if (ms < 5e3) return "1-5s";
203
+ if (ms < 3e4) return "5-30s";
204
+ if (ms < 12e4) return "30s-2m";
205
+ if (ms < 6e5) return "2-10m";
206
+ return ">10m";
207
+ }
208
+ /** A catalog-shaped diagnostic code: `MOSTRA_<AREA>_<NAME>`, never a message. */
209
+ const code = z.string().regex(new RegExp(`^MOSTRA_(${DIAGNOSTIC_AREAS.join("|")})_[A-Z0-9_]{1,64}$`));
210
+ /** A command path from the definitions: lowercase words. */
211
+ const command = z.string().regex(/^[a-z]+( [a-z]+)?$/);
212
+ const common = {
213
+ schema: z.literal(1),
214
+ at: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00Z$/),
215
+ installId: z.uuid(),
216
+ version: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/),
217
+ os: z.enum(["darwin", "linux"]),
218
+ arch: z.string().regex(/^[a-z0-9]{2,12}$/),
219
+ node: z.int().min(0).max(999),
220
+ harness: z.enum([
221
+ ...HARNESSES,
222
+ "other",
223
+ "none"
224
+ ]),
225
+ ci: z.boolean(),
226
+ consent: z.enum(["explicit", "default"])
227
+ };
228
+ /** Every event Mostra may send, as a strict union on `event`. */
229
+ const TelemetryEvent = z.discriminatedUnion("event", [
230
+ z.strictObject({
231
+ ...common,
232
+ event: z.literal("cli.first_run")
233
+ }),
234
+ z.strictObject({
235
+ ...common,
236
+ event: z.literal("cli.command"),
237
+ command,
238
+ exit: z.int().min(0).max(255),
239
+ duration: z.enum(DURATIONS),
240
+ json: z.boolean(),
241
+ error: code.optional()
242
+ }),
243
+ z.strictObject({
244
+ ...common,
245
+ event: z.literal("verify.result"),
246
+ status: z.enum(["pass", "fail"]),
247
+ codes: z.array(code).max(50),
248
+ codesTruncated: z.boolean()
249
+ }),
250
+ z.strictObject({
251
+ ...common,
252
+ event: z.literal("publish.done"),
253
+ reused: z.boolean(),
254
+ forced: z.boolean()
255
+ }),
256
+ z.strictObject({
257
+ ...common,
258
+ event: z.literal("share.done"),
259
+ kind: z.enum([
260
+ "tailnet",
261
+ "funnel",
262
+ "cloudflare",
263
+ "remote"
264
+ ])
265
+ }),
266
+ z.strictObject({
267
+ ...common,
268
+ event: z.literal("mcp.session"),
269
+ client: z.enum([...HARNESSES, "other"]),
270
+ clientVersion: z.string().regex(/^[0-9A-Za-z.+-]{1,32}$/).optional()
271
+ })
272
+ ]);
273
+ /** `at`: now in UTC, rounded down to the minute. */
274
+ function minute(now) {
275
+ return `${now.toISOString().slice(0, 16)}:00Z`;
276
+ }
277
+ /** The distinct catalog codes of `codes`, sorted, at most 50, and whether some were left out. */
278
+ function catalogCodes(codes, known) {
279
+ const distinct = [...new Set(codes.filter((value) => known.has(value)))].sort();
280
+ return {
281
+ codes: distinct.slice(0, 50),
282
+ codesTruncated: distinct.length > 50
283
+ };
284
+ }
285
+ //#endregion
286
+ //#region src/telemetry/store.ts
287
+ /**
288
+ * Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,
289
+ * when the notice was shown, when a flush last started), `telemetry-id` (the random install ID)
290
+ * and `telemetry-queue.ndjson` (events waiting to be sent). The directory is 0700 and the files
291
+ * 0600; `telemetry.json` and the queue are written by rename. The install ID is made by an
292
+ * exclusive create (a hard link that fails when the file exists), so processes on a first run
293
+ * agree on one ID whatever happens to the lock. Every other change goes through one lock file,
294
+ * owned by a token and taken over only when its holder has died, so a flusher never loses an
295
+ * event appended while it sends.
296
+ */
297
+ /** A lock older than this whose owner no longer runs is taken over as stale. */
298
+ const STALE_LOCK_MS = 1e4;
299
+ /**
300
+ * A lock older than this is taken over whoever it names: a held section takes milliseconds, and
301
+ * a PID can name an unrelated live process once the owner died (in a container, PID 1 always
302
+ * runs), which would otherwise hold every later command up forever (Opus review finding 5).
303
+ */
304
+ const DEAD_LOCK_MS = 6e4;
305
+ /** The queue's bounds (D140 item 6). */
306
+ const QUEUE_MAX_EVENTS = 1e3;
307
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
308
+ /** Telemetry's files in one config directory. */
309
+ var TelemetryStore = class {
310
+ dir;
311
+ settingsFile;
312
+ queueFile;
313
+ idFile;
314
+ #lockFile;
315
+ constructor(dir) {
316
+ this.dir = dir;
317
+ this.settingsFile = join(dir, "telemetry.json");
318
+ this.queueFile = join(dir, "telemetry-queue.ndjson");
319
+ this.idFile = join(dir, "telemetry-id");
320
+ this.#lockFile = join(dir, "telemetry.lock");
321
+ }
322
+ /** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */
323
+ read() {
324
+ const id = this.#readId();
325
+ if (id === null) return {
326
+ ok: false,
327
+ file: this.idFile
328
+ };
329
+ let text;
330
+ try {
331
+ text = readFileSync(this.settingsFile, "utf8");
332
+ } catch (error) {
333
+ if (error.code === "ENOENT") return {
334
+ ok: true,
335
+ settings: id !== void 0 ? { installId: id } : {}
336
+ };
337
+ return {
338
+ ok: false,
339
+ file: this.settingsFile
340
+ };
341
+ }
342
+ try {
343
+ const data = JSON.parse(text);
344
+ if (typeof data !== "object" || data === null || Array.isArray(data)) throw new Error();
345
+ const { choice, noticeShownAt, askFirstNoticeShownAt, lastFlushAt } = data;
346
+ const installId = id;
347
+ if (choice !== void 0 && choice !== "on" && choice !== "off") throw new Error();
348
+ if (data["installId"] !== void 0) throw new Error();
349
+ if (noticeShownAt !== void 0 && typeof noticeShownAt !== "string") throw new Error();
350
+ if (askFirstNoticeShownAt !== void 0 && typeof askFirstNoticeShownAt !== "string") throw new Error();
351
+ if (lastFlushAt !== void 0 && typeof lastFlushAt !== "string") throw new Error();
352
+ return {
353
+ ok: true,
354
+ settings: {
355
+ ...choice !== void 0 ? { choice } : {},
356
+ ...installId !== void 0 ? { installId } : {},
357
+ ...noticeShownAt !== void 0 ? { noticeShownAt } : {},
358
+ ...askFirstNoticeShownAt !== void 0 ? { askFirstNoticeShownAt } : {},
359
+ ...lastFlushAt !== void 0 ? { lastFlushAt } : {}
360
+ }
361
+ };
362
+ } catch {
363
+ return {
364
+ ok: false,
365
+ file: this.settingsFile
366
+ };
367
+ }
368
+ }
369
+ /**
370
+ * Changes the settings under the lock and returns what they became; `undefined` when the file
371
+ * can't be read (nothing is written over it).
372
+ */
373
+ update(change) {
374
+ return this.locked(() => {
375
+ const current = this.read();
376
+ if (!current.ok) return void 0;
377
+ const next = change(current.settings);
378
+ this.#write(next);
379
+ return next;
380
+ });
381
+ }
382
+ /**
383
+ * The install ID, made when there is none yet. A new ID is written to a file of this process's
384
+ * own, then hard-linked to `telemetry-id`, which fails when the file exists: of any processes
385
+ * making one at once exactly one wins, and the others read the winner's. `undefined` when the
386
+ * settings can't be read.
387
+ */
388
+ installId() {
389
+ const current = this.read();
390
+ if (!current.ok) return void 0;
391
+ if (current.settings.installId !== void 0) return {
392
+ id: current.settings.installId,
393
+ created: false
394
+ };
395
+ const candidate = `${this.idFile}.${process.pid}.${randomUUID()}.tmp`;
396
+ const id = randomUUID();
397
+ try {
398
+ this.#prepareDir();
399
+ writeFileSync(candidate, `${id}\n`, { mode: 384 });
400
+ linkSync(candidate, this.idFile);
401
+ return {
402
+ id,
403
+ created: true
404
+ };
405
+ } catch (error) {
406
+ if (error.code !== "EEXIST") return void 0;
407
+ const winner = this.#readId();
408
+ return winner ? {
409
+ id: winner,
410
+ created: false
411
+ } : void 0;
412
+ } finally {
413
+ rmSync(candidate, { force: true });
414
+ }
415
+ }
416
+ /** The install ID in `telemetry-id`: `undefined` when there is none, `null` when it's damaged. */
417
+ #readId() {
418
+ let text;
419
+ try {
420
+ text = readFileSync(this.idFile, "utf8");
421
+ } catch (error) {
422
+ return error.code === "ENOENT" ? void 0 : null;
423
+ }
424
+ const id = text.trim();
425
+ return UUID.test(id) ? id : null;
426
+ }
427
+ /**
428
+ * Replaces a file that can't be read with these settings; only `telemetry on|off` do, as the
429
+ * person's explicit choice. Call it holding the lock.
430
+ */
431
+ reset(settings) {
432
+ const { installId: _id, ...rest } = settings;
433
+ this.#write(rest);
434
+ rmSync(this.idFile, { force: true });
435
+ if (settings.choice === "off") rmSync(this.queueFile, { force: true });
436
+ return rest;
437
+ }
438
+ /** Turns telemetry off: the choice, and the install ID and the queue deleted at once. */
439
+ turnOff() {
440
+ return this.locked(() => {
441
+ const current = this.read();
442
+ if (!current.ok) return false;
443
+ const { installId: _id, ...rest } = current.settings;
444
+ this.#write({
445
+ ...rest,
446
+ choice: "off"
447
+ });
448
+ rmSync(this.idFile, { force: true });
449
+ rmSync(this.queueFile, { force: true });
450
+ return true;
451
+ }) ?? false;
452
+ }
453
+ /** Appends events to the queue, keeping its bounds. */
454
+ append(lines, now) {
455
+ if (lines.length === 0) return;
456
+ this.locked(() => {
457
+ const kept = bounded([...this.#queued(), ...lines], now);
458
+ this.#writeQueue(kept);
459
+ });
460
+ }
461
+ /** The queued events, within their bounds. */
462
+ queued(now) {
463
+ return bounded(this.#queued(), now);
464
+ }
465
+ /**
466
+ * Removes the lines a flush sent from the head of the queue, after pruning what expired or
467
+ * fell outside the bounds, as {@link queued} did; how many sent lines were removed (0 when the
468
+ * lock can't be had). A flusher stops when this is 0, so it never sends a batch twice.
469
+ */
470
+ drop(sent, now) {
471
+ return this.locked(() => {
472
+ const queued = bounded(this.#queued(), now);
473
+ let index = 0;
474
+ while (index < sent.length && queued[index] === sent[index]) index += 1;
475
+ this.#writeQueue(queued.slice(index));
476
+ return index;
477
+ }) ?? 0;
478
+ }
479
+ #queued() {
480
+ try {
481
+ return readFileSync(this.queueFile, "utf8").split("\n").filter((line) => line.length > 0);
482
+ } catch {
483
+ return [];
484
+ }
485
+ }
486
+ #writeQueue(lines) {
487
+ if (lines.length === 0) {
488
+ rmSync(this.queueFile, { force: true });
489
+ return;
490
+ }
491
+ const temporary = `${this.queueFile}.${process.pid}.tmp`;
492
+ writeFileSync(temporary, `${lines.join("\n")}\n`, { mode: 384 });
493
+ renameSync(temporary, this.queueFile);
494
+ }
495
+ #write(settings) {
496
+ const { installId: _id, ...stored } = settings;
497
+ const temporary = `${this.settingsFile}.${process.pid}.tmp`;
498
+ writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 384 });
499
+ renameSync(temporary, this.settingsFile);
500
+ }
501
+ #prepareDir() {
502
+ mkdirSync(this.dir, {
503
+ recursive: true,
504
+ mode: 448
505
+ });
506
+ try {
507
+ chmodSync(this.dir, 448);
508
+ } catch {}
509
+ }
510
+ /**
511
+ * Runs `fn` holding the lock file; `undefined` when the lock can't be had within 2 s, or the
512
+ * directory or a file in it can't be written (a full disk, a directory someone else owns):
513
+ * telemetry never stops a command (D140 item 5). The lock
514
+ * holds its owner's PID and a token of its own, is released only by its owner, and is taken
515
+ * over when it is older than 10 s and its owner is no longer running (a crash), or older than
516
+ * 60 s whoever it names.
517
+ */
518
+ locked(fn) {
519
+ try {
520
+ this.#prepareDir();
521
+ } catch {
522
+ return;
523
+ }
524
+ const token = `${process.pid} ${randomUUID()}`;
525
+ const deadline = Date.now() + 2e3;
526
+ for (;;) try {
527
+ writeFileSync(this.#lockFile, token, {
528
+ flag: "wx",
529
+ mode: 384
530
+ });
531
+ break;
532
+ } catch (error) {
533
+ if (error.code !== "EEXIST") return void 0;
534
+ if (this.#breakStale()) continue;
535
+ if (Date.now() > deadline) return void 0;
536
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
537
+ }
538
+ try {
539
+ return fn();
540
+ } catch {
541
+ return;
542
+ } finally {
543
+ this.#release(token);
544
+ }
545
+ }
546
+ /** Removes the lock when it is still this owner's. */
547
+ #release(token) {
548
+ try {
549
+ if (readFileSync(this.#lockFile, "utf8") === token) rmSync(this.#lockFile, { force: true });
550
+ } catch {}
551
+ }
552
+ /**
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.
556
+ */
557
+ #breakStale() {
558
+ let seen;
559
+ let age;
560
+ let handle;
561
+ try {
562
+ handle = openSync(this.#lockFile, "r");
563
+ age = Date.now() - fstatSync(handle).mtimeMs;
564
+ if (age <= STALE_LOCK_MS) return false;
565
+ seen = readFileSync(handle, "utf8");
566
+ } catch {
567
+ return true;
568
+ } finally {
569
+ if (handle !== void 0) closeSync(handle);
570
+ }
571
+ if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return false;
572
+ const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;
573
+ try {
574
+ renameSync(this.#lockFile, aside);
575
+ } catch {
576
+ return true;
577
+ }
578
+ try {
579
+ if (readFileSync(aside, "utf8") !== seen) linkSync(aside, this.#lockFile);
580
+ } catch {} finally {
581
+ rmSync(aside, { force: true });
582
+ }
583
+ return true;
584
+ }
585
+ };
586
+ /** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */
587
+ function ownerAlive(lock) {
588
+ const pid = Number(lock.split(" ")[0]);
589
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
590
+ try {
591
+ process.kill(pid, 0);
592
+ return true;
593
+ } catch (error) {
594
+ return error.code === "EPERM";
595
+ }
596
+ }
597
+ /** The newest events within 1,000, none older than 7 days; lines that aren't JSON go. */
598
+ function bounded(lines, now) {
599
+ return lines.filter((line) => {
600
+ try {
601
+ const at = Date.parse(JSON.parse(line).at ?? "");
602
+ return Number.isFinite(at) && now - at <= 6048e5;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }).slice(-1e3);
607
+ }
608
+ /** A flush starts at most this often. */
609
+ const FLUSH_INTERVAL_MS = 6e4;
610
+ /** The flush interval; `MOSTRA_TEST_FLUSH_INTERVAL_MS` shortens it, for tests only. */
611
+ function flushInterval(env) {
612
+ const test = Number(env["MOSTRA_TEST_FLUSH_INTERVAL_MS"]);
613
+ return Number.isInteger(test) && test >= 0 && test < 6e4 ? test : FLUSH_INTERVAL_MS;
614
+ }
615
+ /** One request's limit. */
616
+ const REQUEST_TIMEOUT_MS = 5e3;
617
+ /** Posts one batch; resolves with whether the collector took it (any 2xx). */
618
+ function post(endpoint, body) {
619
+ return new Promise((resolve) => {
620
+ const url = new URL(endpoint);
621
+ const req = (url.protocol === "https:" ? request$1 : request)(url, {
622
+ method: "POST",
623
+ headers: {
624
+ "Content-Type": "application/json",
625
+ "User-Agent": `mostra-cli/${cliVersion()}`,
626
+ "Content-Length": Buffer.byteLength(body)
627
+ },
628
+ timeout: REQUEST_TIMEOUT_MS
629
+ }, (response) => {
630
+ response.resume();
631
+ response.on("end", () => resolve((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300));
632
+ response.on("error", () => resolve(false));
633
+ });
634
+ req.on("timeout", () => req.destroy());
635
+ req.on("error", () => resolve(false));
636
+ req.end(body);
637
+ });
638
+ }
639
+ /**
640
+ * Sends the queue: the decision again, then batches until one fails. Events the schema refuses
641
+ * never leave the machine.
642
+ */
643
+ async function flush(store, env, now) {
644
+ const settings = store.read();
645
+ const decision = decide({
646
+ env,
647
+ buildEndpoint: buildEndpoint(),
648
+ embedded: false,
649
+ settings,
650
+ askFirst: false
651
+ });
652
+ if (!decision.enabled || decision.endpoint === void 0) {
653
+ if (decision.reason !== "no-collector" && settings.ok) store.drop(store.queued(now()), now());
654
+ return;
655
+ }
656
+ for (let round = 0; round <= Math.ceil(QUEUE_MAX_EVENTS / 100); round += 1) {
657
+ const lines = store.queued(now()).slice(0, 100);
658
+ if (lines.length === 0) return;
659
+ const events = [];
660
+ for (const line of lines) try {
661
+ const parsed = TelemetryEvent.safeParse(JSON.parse(line));
662
+ if (parsed.success) events.push(parsed.data);
663
+ } catch {}
664
+ if (!(events.length === 0 || await post(decision.endpoint, JSON.stringify({ events })))) return;
665
+ if (store.drop(lines, now()) === 0) return;
666
+ }
667
+ }
668
+ /** The flusher's entry in the CLI's package. */
669
+ function flusherEntry() {
670
+ const root = packageRoot();
671
+ return fromSource() ? {
672
+ entry: join(root, "src", "telemetry", "flush-main.ts"),
673
+ nodeArgs: ["--conditions=@mostra/source"]
674
+ } : {
675
+ entry: join(root, "dist", "telemetry", "flush-main.js"),
676
+ nodeArgs: []
677
+ };
678
+ }
679
+ /**
680
+ * Starts a flusher when the queue has events and no flush started in the last minute, fully
681
+ * detached: no stdio, its own process group, never waited for, so `mostra … | jq` and every
682
+ * harness reading to end of file finish when the command does.
683
+ */
684
+ function startFlusher(store, env, now) {
685
+ if (store.queued(now).length === 0) return;
686
+ let due = false;
687
+ store.update((current) => {
688
+ const last = current.lastFlushAt === void 0 ? 0 : Date.parse(current.lastFlushAt);
689
+ if (Number.isFinite(last) && now - last < flushInterval(env)) return current;
690
+ due = true;
691
+ return {
692
+ ...current,
693
+ lastFlushAt: new Date(now).toISOString()
694
+ };
695
+ });
696
+ if (!due) return;
697
+ const { entry, nodeArgs } = flusherEntry();
698
+ try {
699
+ const child = spawn(process.execPath, [
700
+ ...nodeArgs,
701
+ entry,
702
+ store.dir
703
+ ], {
704
+ detached: true,
705
+ stdio: "ignore",
706
+ env: {
707
+ ...process.env,
708
+ ...env
709
+ }
710
+ });
711
+ child.on("error", () => {});
712
+ child.unref();
713
+ } catch {}
714
+ }
715
+ //#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 };
717
+
718
+ //# sourceMappingURL=flush-BCuWs0Zh.js.map