minnimemory 1.0.0-beta.1

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 (59) hide show
  1. package/LICENSE +39 -0
  2. package/README.md +824 -0
  3. package/dist/bench.d.ts +98 -0
  4. package/dist/bench.js +142 -0
  5. package/dist/benchReport.d.ts +12 -0
  6. package/dist/benchReport.js +128 -0
  7. package/dist/bounds.d.ts +40 -0
  8. package/dist/bounds.js +44 -0
  9. package/dist/cli.d.ts +15 -0
  10. package/dist/cli.js +503 -0
  11. package/dist/compile.d.ts +187 -0
  12. package/dist/compile.js +516 -0
  13. package/dist/discover.d.ts +125 -0
  14. package/dist/discover.js +520 -0
  15. package/dist/doctor.d.ts +9 -0
  16. package/dist/doctor.js +67 -0
  17. package/dist/episodic.d.ts +47 -0
  18. package/dist/episodic.js +130 -0
  19. package/dist/hook.d.ts +45 -0
  20. package/dist/hook.js +104 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +18 -0
  23. package/dist/init.d.ts +125 -0
  24. package/dist/init.js +475 -0
  25. package/dist/instructions.d.ts +60 -0
  26. package/dist/instructions.js +270 -0
  27. package/dist/mcp.d.ts +109 -0
  28. package/dist/mcp.js +252 -0
  29. package/dist/mcpServer.d.ts +136 -0
  30. package/dist/mcpServer.js +997 -0
  31. package/dist/paths.d.ts +25 -0
  32. package/dist/paths.js +47 -0
  33. package/dist/recall.d.ts +113 -0
  34. package/dist/recall.js +256 -0
  35. package/dist/recallDir.d.ts +50 -0
  36. package/dist/recallDir.js +187 -0
  37. package/dist/reorganize.d.ts +62 -0
  38. package/dist/reorganize.js +216 -0
  39. package/dist/report.d.ts +16 -0
  40. package/dist/report.js +204 -0
  41. package/dist/router.d.ts +141 -0
  42. package/dist/router.js +314 -0
  43. package/dist/rules.d.ts +32 -0
  44. package/dist/rules.js +651 -0
  45. package/dist/scan.d.ts +110 -0
  46. package/dist/scan.js +173 -0
  47. package/dist/text.d.ts +158 -0
  48. package/dist/text.js +395 -0
  49. package/dist/tokenizer.d.ts +26 -0
  50. package/dist/tokenizer.js +69 -0
  51. package/dist/types.d.ts +156 -0
  52. package/dist/types.js +17 -0
  53. package/dist/version.d.ts +7 -0
  54. package/dist/version.js +7 -0
  55. package/dist/writeProtocol.d.ts +19 -0
  56. package/dist/writeProtocol.js +45 -0
  57. package/examples/CLAUDE.md +75 -0
  58. package/examples/README.md +7 -0
  59. package/package.json +52 -0
package/dist/cli.js ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * minnimemory CLI.
4
+ *
5
+ * Exit codes: 0 clean, 1 findings at or above the --fail-on threshold, 2 execution error.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parseArgs } from "node:util";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { BenchError, bounds, model, readWorkload } from "./bench.js";
13
+ import { renderBounds, renderModel, renderNoWorkload } from "./benchReport.js";
14
+ import { detectPhase, DiscoveryError } from "./discover.js";
15
+ import { doctor } from "./doctor.js";
16
+ import { parseHookInput, renderHook } from "./hook.js";
17
+ import { init, InitError, renderPlan } from "./init.js";
18
+ import { isProfileName } from "./instructions.js";
19
+ import { loadManifest, McpTargetError, recall } from "./recall.js";
20
+ import { createDisabledMcpServer, createMcpServer, DISABLED_MESSAGE } from "./mcpServer.js";
21
+ import { recallDir, RecallDirError } from "./recallDir.js";
22
+ import { exitCodeFor, renderHuman, renderJson } from "./report.js";
23
+ import { RULES } from "./rules.js";
24
+ import { VERSION } from "./version.js";
25
+ /**
26
+ * Single switch for whether `mcp` starts the real server or the disabled placeholder. Exported
27
+ * so tests/cli.test.ts can pin it without needing a live stdio transport in the test process.
28
+ *
29
+ * Re-enabled 2026-09-10 for internal testing, then shipped publicly in the 1.0.0-beta.1 npm
30
+ * release (2026-09-15). Flip back to true to re-stash the server in a future release.
31
+ */
32
+ export const MCP_STASHED = false;
33
+ const USAGE = `
34
+ minnimemory ${VERSION} the agent memory compiler
35
+
36
+ usage
37
+ minnimemory doctor [path] audit a memory setup, change nothing
38
+ minnimemory init [path] compile into .minnimemory/, dry run by default
39
+ minnimemory bench [path] what the compiled shape costs over a session
40
+ minnimemory mcp [path] start the MCP server. Serves check/recall by default;
41
+ --allow-write and --profile add more tools
42
+ minnimemory recall <path> <query...>
43
+ recall against a compiled workspace or a manifest-less memory
44
+ directory (Layout B), whichever <path> is, change nothing
45
+ minnimemory hook Claude Code UserPromptSubmit hook: reads hook JSON from stdin,
46
+ prints a fenced memory block to stdout, or nothing on no hit.
47
+ Experimental until the gate in
48
+ Research/FlagShipInterfaces/README.md part (c) runs
49
+
50
+ recall options
51
+ --max-tokens <n> cap on returned tokens (default 1500); the top hit is always returned
52
+ --headings print only "file > heading path (n tokens)" lines, not the content
53
+
54
+ hook options
55
+ --max-tokens <n> cap on returned tokens (default 600)
56
+ --headings print only "file > heading path (n tokens)" lines, not the content
57
+ --dir <path> an explicit memory directory (compiled or manifest-less), overriding
58
+ the hook JSON's own cwd resolution (cwd/.minnimemory, else cwd's
59
+ Claude Code auto-memory folder)
60
+
61
+ init options
62
+ --write apply the compile. Without it, init only prints the plan
63
+ --force overwrite an existing .minnimemory/
64
+ --update recompile a compiled workspace from its stub and OnDemandMemory files
65
+ on disk
66
+ --budget <n> always-loaded token budget AlwaysOnMemory must fit (default 2000)
67
+ --profile <name> how much token-discipline guidance to embed (default auto)
68
+ auto none under the budget, routing at or over it
69
+ none structure only, embed nothing
70
+ routing the rules that make routing work, about 200 tokens
71
+ full the complete set, about 690 tokens
72
+ --allow-secrets compile even when the source holds a credential-shaped string
73
+ --episodic-json write episodic OnDemandMemory files (changelogs, logs) as JSON, not
74
+ Markdown (O3)
75
+
76
+ mcp options
77
+ Every tool definition is charged to the client's prefix on every turn, called or not, so the
78
+ server only advertises the profile you asked for, and the basic profile only the half its
79
+ workspace can use. Real costs (Research/TokenTest/tool_cost.mjs, approx-v2): basic on an uncompiled
80
+ root 368 tokens read-only, 1,025 with --allow-write; basic on a compiled root 329 read-only,
81
+ 605 with --allow-write; full read-only 1,666, full --allow-write 2,668.
82
+ --profile <name> basic (default) or full
83
+ basic the four verbs a developer actually uses, two per launch,
84
+ chosen from the workspace on disk: before a compile
85
+ (no .minnimemory/) check, plus optimize with --allow-write;
86
+ once compiled recall, plus sync with --allow-write. The
87
+ surface is fixed per process: after compiling, relaunch
88
+ full recall, modules, outline, scan, doctor, plan, always;
89
+ apply, update, reorganize with --allow-write. Today's nine
90
+ tools, unchanged - for maintainers and for an agent driving
91
+ a folder reorganization directly
92
+ --allow-write adds the profile's write tools (basic: optimize, sync; full: apply,
93
+ update, reorganize). Without it those tools are not registered at
94
+ all: they are absent from tools/list, not present-and-refusing
95
+ --follow-external-imports
96
+ let doctor/plan/scan (full) and check/optimize (basic) follow @path
97
+ imports outside the server root. A launch flag, never a tool
98
+ argument: a connected client must not be able to widen the server's
99
+ reach by asking
100
+ --include-auto-memory let doctor/plan/apply/update/scan (full) and check/optimize (basic)
101
+ see the operator's OS-level Claude Code auto-memory folder, and
102
+ allow "auto-memory" as a target where that tool resolves one. Off by
103
+ default and separate from --follow-external-imports: reading the
104
+ operator's own memory and following an imported file's @path are
105
+ different reaches. A launch flag, never a tool argument
106
+
107
+ bench options
108
+ --turns <n> session length for the exact bounds (default 50)
109
+ --workload <file> one task description per line. Without it, only the
110
+ assumption-free bounds are reported
111
+ --whole-files model an agent that reads the whole file the OnDemandMemory list named,
112
+ instead of the sections recall() would return
113
+ --json machine-readable output
114
+
115
+ doctor options
116
+ --json machine-readable output
117
+ --ci terse output, no colour
118
+ --follow-external-imports
119
+ follow @path imports that resolve outside the target directory.
120
+ Off by default: an @ line is content the scanned repo controls,
121
+ and following one reads and reports files elsewhere on this machine
122
+ --fail-on <sev> exit 1 at this severity or above: low, med, high (default high)
123
+ --budget <n> always-loaded token budget for MM001 (default 2000)
124
+ --only <ids> run only these rules, comma separated
125
+ --ignore <ids> skip these rules, comma separated
126
+ --rules list the rule set and exit
127
+ -h, --help this message
128
+ -v, --version print the version
129
+
130
+ path defaults to the current directory. It may be a repo, a memory directory, or a single file.
131
+ `;
132
+ function listRules() {
133
+ const lines = ["", " rule set", ""];
134
+ for (const r of RULES) {
135
+ lines.push(` ${r.id.padEnd(8)}${r.severity.padEnd(6)}${r.title}`);
136
+ }
137
+ lines.push("");
138
+ return lines.join("\n");
139
+ }
140
+ function parseSeverity(value, fallback) {
141
+ if (value === undefined)
142
+ return fallback;
143
+ if (value === "low" || value === "med" || value === "high")
144
+ return value;
145
+ throw new Error(`--fail-on must be one of low, med, high (got "${value}")`);
146
+ }
147
+ function parseIds(value) {
148
+ if (!value)
149
+ return [];
150
+ return value
151
+ .split(",")
152
+ .map((s) => s.trim())
153
+ .filter(Boolean);
154
+ }
155
+ /** `minnimemory recall`'s own rendering: full units by default, or with --headings only the
156
+ * "file > heading path (n tokens)" line an agent needs to decide whether to widen the query. */
157
+ function renderRecall(result, headingsOnly) {
158
+ if (result.matches.length === 0) {
159
+ return `\n no match for "${result.query}"\n\n`;
160
+ }
161
+ if (headingsOnly) {
162
+ return `${result.matches.map((m) => `${m.onDemandFile} > ${m.path.join(" > ")} (${m.tokens} tokens)`).join("\n")}\n`;
163
+ }
164
+ return `${result.matches
165
+ .map((m) => `## ${m.onDemandFile} > ${m.path.join(" > ")} (lines ${m.startLine}-${m.endLine}, ${m.tokens} tokens, score ${m.score})\n\n${m.content}`)
166
+ .join("\n\n---\n\n")}\n`;
167
+ }
168
+ function parseBudget(value, fallback) {
169
+ if (value === undefined)
170
+ return fallback;
171
+ const n = Number(value);
172
+ if (!Number.isFinite(n) || n <= 0)
173
+ throw new Error(`--budget must be a positive number (got "${value}")`);
174
+ return Math.floor(n);
175
+ }
176
+ /**
177
+ * Starts the MCP server. Called from main() for the `mcp` command unless MCP_STASHED is true
178
+ * (see that constant); also exercised directly by tests/mcp.test.ts and tests/mcpServer.test.ts.
179
+ */
180
+ function runMcpCommand(target, values) {
181
+ let root;
182
+ try {
183
+ root = path.resolve(target);
184
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
185
+ throw new McpTargetError(`not a directory: ${root}`);
186
+ }
187
+ }
188
+ catch (err) {
189
+ if (err instanceof McpTargetError) {
190
+ process.stderr.write(`\n error: ${err.message}\n\n`);
191
+ return 2;
192
+ }
193
+ process.stderr.write(`\n unexpected error: ${err.stack ?? String(err)}\n\n`);
194
+ return 2;
195
+ }
196
+ const profileName = values.profile ?? "basic";
197
+ if (profileName !== "basic" && profileName !== "full") {
198
+ process.stderr.write(`\n error: --profile must be one of basic, full (got "${profileName}")\n\n`);
199
+ return 2;
200
+ }
201
+ // check/recall (basic) and recall/modules/outline (full) need a compiled workspace and
202
+ // report that themselves per call, the same way they already handle a missing manifest
203
+ // today; doctor/plan/scan (full) and check/optimize (basic) work on any target, compiled or
204
+ // not, and the write tools (behind --allow-write) are what makes one - which is the point of
205
+ // this command not failing fast here.
206
+ const server = createMcpServer(root, {
207
+ profile: profileName,
208
+ allowWrite: values["allow-write"] === true,
209
+ followExternalImports: values["follow-external-imports"] === true,
210
+ includeAutoMemory: values["include-auto-memory"] === true,
211
+ });
212
+ const transport = new StdioServerTransport();
213
+ // Fire-and-forget: the transport takes over stdin, which keeps the process alive on its own.
214
+ // main() stays synchronous everywhere else, so this is the one command that does not "finish"
215
+ // in the normal sense - it runs until the client disconnects (stdin closes).
216
+ server.connect(transport).catch((err) => {
217
+ process.stderr.write(`\n mcp server failed: ${err.message ?? String(err)}\n`);
218
+ process.exitCode = 1;
219
+ });
220
+ return 0;
221
+ }
222
+ export function main(argv) {
223
+ let parsed;
224
+ try {
225
+ parsed = parseArgs({
226
+ args: argv,
227
+ allowPositionals: true,
228
+ strict: true,
229
+ options: {
230
+ json: { type: "boolean", default: false },
231
+ write: { type: "boolean", default: false },
232
+ force: { type: "boolean", default: false },
233
+ update: { type: "boolean", default: false },
234
+ "allow-secrets": { type: "boolean", default: false },
235
+ "episodic-json": { type: "boolean", default: false },
236
+ "allow-write": { type: "boolean", default: false },
237
+ "follow-external-imports": { type: "boolean", default: false },
238
+ "include-auto-memory": { type: "boolean", default: false },
239
+ turns: { type: "string" },
240
+ workload: { type: "string" },
241
+ "whole-files": { type: "boolean", default: false },
242
+ "max-tokens": { type: "string" },
243
+ headings: { type: "boolean", default: false },
244
+ dir: { type: "string" },
245
+ profile: { type: "string" },
246
+ ci: { type: "boolean", default: false },
247
+ rules: { type: "boolean", default: false },
248
+ "fail-on": { type: "string" },
249
+ budget: { type: "string" },
250
+ only: { type: "string" },
251
+ ignore: { type: "string" },
252
+ help: { type: "boolean", short: "h", default: false },
253
+ version: { type: "boolean", short: "v", default: false },
254
+ },
255
+ });
256
+ }
257
+ catch (err) {
258
+ process.stderr.write(`\n error: ${err.message}\n${USAGE}`);
259
+ return 2;
260
+ }
261
+ const { values, positionals } = parsed;
262
+ if (values.help) {
263
+ process.stdout.write(USAGE);
264
+ return 0;
265
+ }
266
+ if (values.version) {
267
+ process.stdout.write(`${VERSION}\n`);
268
+ return 0;
269
+ }
270
+ if (values.rules) {
271
+ process.stdout.write(listRules());
272
+ return 0;
273
+ }
274
+ const command = positionals[0] ?? "doctor";
275
+ const target = positionals[1] ?? ".";
276
+ if (command === "init") {
277
+ const profileName = values.profile ?? "auto";
278
+ if (profileName !== "auto" && !isProfileName(profileName)) {
279
+ process.stderr.write(`\n error: --profile must be one of auto, none, routing, full (got "${profileName}")\n\n`);
280
+ return 2;
281
+ }
282
+ let budget;
283
+ try {
284
+ budget = parseBudget(values.budget, 2000);
285
+ }
286
+ catch (err) {
287
+ process.stderr.write(`\n error: ${err.message}\n\n`);
288
+ return 2;
289
+ }
290
+ const options = {
291
+ write: values.write === true,
292
+ force: values.force === true,
293
+ update: values.update === true,
294
+ allowSecrets: values["allow-secrets"] === true,
295
+ episodicJson: values["episodic-json"] === true,
296
+ profile: profileName,
297
+ budget,
298
+ };
299
+ try {
300
+ const plan = init(target, options);
301
+ process.stdout.write(renderPlan(plan, options));
302
+ return 0;
303
+ }
304
+ catch (err) {
305
+ if (err instanceof InitError || err instanceof DiscoveryError) {
306
+ process.stderr.write(`\n error: ${err.message}\n\n`);
307
+ return 2;
308
+ }
309
+ process.stderr.write(`\n unexpected error: ${err.stack ?? String(err)}\n\n`);
310
+ return 2;
311
+ }
312
+ }
313
+ if (command === "mcp") {
314
+ if (!MCP_STASHED)
315
+ return runMcpCommand(target, values);
316
+ // Re-stash path, dead while MCP_STASHED is false (see the constant's own comment above) but
317
+ // kept intact and reachable: flip the flag back to instantly return to a zero-tool
318
+ // placeholder instead of deleting this and having to rebuild it later. A connecting client
319
+ // gets a clean handshake either way - zero tools, DISABLED_MESSAGE in `instructions` - rather
320
+ // than a bare process-startup failure. tests/cli.test.ts pins MCP_STASHED, so either flip is
321
+ // a deliberate, visible change, not a silent one.
322
+ process.stderr.write(`\n mcp: ${DISABLED_MESSAGE}\n\n`);
323
+ const server = createDisabledMcpServer();
324
+ const transport = new StdioServerTransport();
325
+ server.connect(transport).catch((err) => {
326
+ process.stderr.write(`\n mcp server failed: ${err.message ?? String(err)}\n`);
327
+ process.exitCode = 1;
328
+ });
329
+ return 0;
330
+ }
331
+ if (command === "recall") {
332
+ const query = positionals.slice(2).join(" ").trim();
333
+ if (!query) {
334
+ process.stderr.write(`\n error: recall needs a query: minnimemory recall <path> <query...>\n\n`);
335
+ return 2;
336
+ }
337
+ let maxTokens;
338
+ if (values["max-tokens"] !== undefined) {
339
+ const n = Number(values["max-tokens"]);
340
+ if (!Number.isFinite(n) || n <= 0) {
341
+ process.stderr.write(`\n error: --max-tokens must be a positive number (got "${values["max-tokens"]}")\n\n`);
342
+ return 2;
343
+ }
344
+ maxTokens = Math.floor(n);
345
+ }
346
+ const root = path.resolve(target);
347
+ try {
348
+ const phase = detectPhase(root);
349
+ let result;
350
+ if (phase === "compiled") {
351
+ result = recall(root, loadManifest(root), query, { maxTokens });
352
+ }
353
+ else if (phase === "memory-dir") {
354
+ result = recallDir(root, query, { maxTokens });
355
+ }
356
+ else {
357
+ process.stderr.write(`\n error: ${root} is neither a compiled workspace (.minnimemory/manifest.json) nor a manifest-less ` +
358
+ `memory directory (an index file plus topic files) - nothing for recall to query\n\n`);
359
+ return 2;
360
+ }
361
+ process.stdout.write(renderRecall(result, values.headings === true));
362
+ return 0;
363
+ }
364
+ catch (err) {
365
+ if (err instanceof McpTargetError || err instanceof RecallDirError) {
366
+ process.stderr.write(`\n error: ${err.message}\n\n`);
367
+ return 2;
368
+ }
369
+ process.stderr.write(`\n unexpected error: ${err.stack ?? String(err)}\n\n`);
370
+ return 2;
371
+ }
372
+ }
373
+ if (command === "hook") {
374
+ // Exit 0 always: this runs as a Claude Code UserPromptSubmit hook, and a hook's stdout
375
+ // becomes context fed straight to the model, so a failure here must never surface as
376
+ // anything but silence on stdout. Everything that can go wrong (unreadable stdin, a bad
377
+ // target, no hit) is swallowed by renderHook() itself; this block only guards the read.
378
+ let raw = "";
379
+ try {
380
+ raw = fs.readFileSync(0, "utf8");
381
+ }
382
+ catch {
383
+ raw = "";
384
+ }
385
+ let maxTokens;
386
+ if (values["max-tokens"] !== undefined) {
387
+ const n = Number(values["max-tokens"]);
388
+ if (Number.isFinite(n) && n > 0)
389
+ maxTokens = Math.floor(n);
390
+ }
391
+ try {
392
+ const input = parseHookInput(raw);
393
+ const text = renderHook(input, { headings: values.headings === true, maxTokens, dir: values.dir });
394
+ if (text)
395
+ process.stdout.write(`${text}\n`);
396
+ }
397
+ catch (err) {
398
+ process.stderr.write(`\n hook: ${err.message ?? String(err)}\n`);
399
+ }
400
+ return 0;
401
+ }
402
+ if (command === "bench") {
403
+ try {
404
+ const root = path.resolve(target);
405
+ const manifest = loadManifest(root);
406
+ const turns = values.turns === undefined ? 50 : Number(values.turns);
407
+ if (!Number.isFinite(turns)) {
408
+ throw new BenchError(`--turns must be a positive whole number (got "${values.turns}")`);
409
+ }
410
+ const b = bounds(manifest, turns);
411
+ if (values.workload === undefined) {
412
+ if (values.json === true) {
413
+ process.stdout.write(`${JSON.stringify({ bounds: b, tokenizer: manifest.tokenizer }, null, 2)}\n`);
414
+ return 0;
415
+ }
416
+ process.stdout.write(renderBounds(b, manifest.tokenizer));
417
+ process.stdout.write(renderNoWorkload());
418
+ return 0;
419
+ }
420
+ const tasks = readWorkload(values.workload);
421
+ // Section-level routing, the same path recall() takes. --whole-files models an agent
422
+ // that reads the entire file the OnDemandMemory list pointed at instead.
423
+ const m = model(manifest, tasks, values["whole-files"] === true ? {} : { root });
424
+ if (values.json === true) {
425
+ process.stdout.write(`${JSON.stringify({ bounds: b, model: m, tokenizer: manifest.tokenizer }, null, 2)}\n`);
426
+ return 0;
427
+ }
428
+ process.stdout.write(renderBounds(b, manifest.tokenizer));
429
+ process.stdout.write(renderModel(m, manifest.tokenizer));
430
+ return 0;
431
+ }
432
+ catch (err) {
433
+ if (err instanceof BenchError || err instanceof McpTargetError) {
434
+ process.stderr.write(`\n error: ${err.message}\n\n`);
435
+ return 2;
436
+ }
437
+ process.stderr.write(`\n unexpected error: ${err.stack ?? String(err)}\n\n`);
438
+ return 2;
439
+ }
440
+ }
441
+ if (command !== "doctor") {
442
+ process.stderr.write(`\n error: unknown command "${command}"\n${USAGE}`);
443
+ return 2;
444
+ }
445
+ let config;
446
+ try {
447
+ config = {
448
+ budget: parseBudget(values.budget, 2000),
449
+ failOn: parseSeverity(values["fail-on"], "high"),
450
+ only: parseIds(values.only),
451
+ ignore: parseIds(values.ignore),
452
+ followExternalImports: values["follow-external-imports"] === true,
453
+ };
454
+ }
455
+ catch (err) {
456
+ process.stderr.write(`\n error: ${err.message}\n\n`);
457
+ return 2;
458
+ }
459
+ try {
460
+ const result = doctor(target, config);
461
+ const resolved = { ...result, findings: result.findings };
462
+ const full = {
463
+ budget: config.budget ?? 2000,
464
+ failOn: config.failOn ?? "high",
465
+ only: config.only ?? [],
466
+ ignore: config.ignore ?? [],
467
+ maxListLine: 120,
468
+ followExternalImports: config.followExternalImports === true,
469
+ includeAutoMemory: true,
470
+ };
471
+ if (values.json) {
472
+ process.stdout.write(`${renderJson(resolved, full)}\n`);
473
+ }
474
+ else {
475
+ const color = !values.ci && !process.env["NO_COLOR"] && process.stdout.isTTY === true;
476
+ process.stdout.write(renderHuman(resolved, full, { color, ci: values.ci === true }));
477
+ }
478
+ return exitCodeFor(result.findings, full.failOn);
479
+ }
480
+ catch (err) {
481
+ if (err instanceof DiscoveryError) {
482
+ process.stderr.write(`\n error: ${err.message}\n\n`);
483
+ return 2;
484
+ }
485
+ process.stderr.write(`\n unexpected error: ${err.stack ?? String(err)}\n\n`);
486
+ return 2;
487
+ }
488
+ }
489
+ /** Only run when invoked as the binary, so tests can import `main` safely. */
490
+ function invokedDirectly() {
491
+ const entry = process.argv[1];
492
+ if (!entry)
493
+ return false;
494
+ try {
495
+ return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(entry);
496
+ }
497
+ catch {
498
+ return false;
499
+ }
500
+ }
501
+ if (invokedDirectly()) {
502
+ process.exitCode = main(process.argv.slice(2));
503
+ }