finitemonkey 0.1.0-next.1 → 0.1.0-next.3

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.
@@ -14,30 +14,223 @@ var isEntryPoint = (moduleUrl, argv1, realpath, platform) => {
14
14
  };
15
15
 
16
16
  // src/core/node-runtime.ts
17
- import { spawnSync } from "node:child_process";
17
+ import { spawnSync as spawnSync2 } from "node:child_process";
18
18
  import { readFileSync, realpathSync } from "node:fs";
19
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
20
+
21
+ // src/core/node-fs.ts
22
+ import { randomBytes } from "node:crypto";
23
+ import {
24
+ closeSync,
25
+ fsyncSync,
26
+ mkdirSync,
27
+ openSync,
28
+ readdirSync,
29
+ renameSync,
30
+ rmSync,
31
+ statSync,
32
+ unlinkSync,
33
+ writeSync
34
+ } from "node:fs";
35
+ import { basename, dirname, join, relative } from "node:path";
19
36
 
20
37
  // src/core/parsed.ts
21
38
  var accept = (value) => ({ ok: true, value });
22
39
  var reject = (error) => ({ ok: false, error });
23
40
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24
- var parseJson = (text) => {
41
+ var parseJson = (text2) => {
25
42
  try {
26
- return accept(JSON.parse(text));
43
+ return accept(JSON.parse(text2));
27
44
  } catch (error) {
28
45
  return reject(error instanceof Error ? error.message : String(error));
29
46
  }
30
47
  };
31
- var parseJsonObject = (text) => {
32
- const parsed = parseJson(text);
48
+ var parseJsonObject = (text2) => {
49
+ const parsed = parseJson(text2);
33
50
  if (!parsed.ok) return parsed;
34
51
  return isRecord(parsed.value) ? accept(parsed.value) : reject("expected a JSON object");
35
52
  };
36
- var optionalString = (record, key) => {
37
- const value = record[key];
53
+ var optionalString = (record2, key) => {
54
+ const value = record2[key];
38
55
  return typeof value === "string" ? value : void 0;
39
56
  };
40
57
 
58
+ // src/core/node-fs.ts
59
+ var NEW_FILE_MODE = 420;
60
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
61
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
62
+ var errnoOf = (error) => error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
63
+ var attempt = (work) => {
64
+ try {
65
+ work();
66
+ return accept(void 0);
67
+ } catch (error) {
68
+ return reject(messageOf(error));
69
+ }
70
+ };
71
+ var nodeWriteText = (path, text2) => {
72
+ const temp = `${path}.${String(process.pid)}.tmp`;
73
+ try {
74
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
75
+ const fd = openSync(temp, "w", 384);
76
+ try {
77
+ writeSync(fd, text2);
78
+ fsyncSync(fd);
79
+ } finally {
80
+ closeSync(fd);
81
+ }
82
+ renameSync(temp, path);
83
+ return accept(true);
84
+ } catch (error) {
85
+ rmSync(temp, { force: true });
86
+ return reject(messageOf(error));
87
+ }
88
+ };
89
+ var nodeStat = (path) => {
90
+ try {
91
+ const stats = statSync(path);
92
+ return { kind: stats.isDirectory() ? "dir" : "file", mtimeMs: stats.mtimeMs };
93
+ } catch {
94
+ return void 0;
95
+ }
96
+ };
97
+ var nodeListDir = (path) => {
98
+ try {
99
+ return accept(readdirSync(path).sort());
100
+ } catch (error) {
101
+ return reject(messageOf(error));
102
+ }
103
+ };
104
+ var modeOf = (path) => {
105
+ try {
106
+ return statSync(path).mode & 511;
107
+ } catch {
108
+ return NEW_FILE_MODE;
109
+ }
110
+ };
111
+ var writeAndSync = (file, content, mode) => {
112
+ const fd = openSync(file, "wx", mode);
113
+ try {
114
+ writeSync(fd, content, null, "utf8");
115
+ fsyncSync(fd);
116
+ } finally {
117
+ closeSync(fd);
118
+ }
119
+ };
120
+ var nodeWriteAtomic = (path, content) => {
121
+ const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
122
+ return attempt(() => {
123
+ try {
124
+ writeAndSync(temp, content, modeOf(path));
125
+ renameSync(temp, path);
126
+ } catch (error) {
127
+ rmSync(temp, { force: true });
128
+ throw error;
129
+ }
130
+ });
131
+ };
132
+ var nodeRename = (from, to) => attempt(() => {
133
+ renameSync(from, to);
134
+ });
135
+ var nodeMkdir = (path) => attempt(() => {
136
+ mkdirSync(path, { recursive: true });
137
+ });
138
+ var nodeRemove = (path) => attempt(() => {
139
+ rmSync(path, { recursive: true, force: true });
140
+ });
141
+ var createLock = (path, owner, now) => {
142
+ const fd = openSync(path, "wx", 384);
143
+ try {
144
+ writeSync(fd, `${JSON.stringify({ owner, pid: process.pid, at: now })}
145
+ `, null, "utf8");
146
+ } finally {
147
+ closeSync(fd);
148
+ }
149
+ };
150
+ var release = (path) => () => {
151
+ try {
152
+ unlinkSync(path);
153
+ } catch {
154
+ return;
155
+ }
156
+ };
157
+ var isStale = (path, staleMs, now) => {
158
+ const stat = nodeStat(path);
159
+ return stat !== void 0 && now - stat.mtimeMs > staleMs;
160
+ };
161
+ var nodeLock = (path, options) => {
162
+ for (let round = 0; round < 2; round += 1) {
163
+ const now = Date.now();
164
+ try {
165
+ createLock(path, options.owner, now);
166
+ return accept(release(path));
167
+ } catch (error) {
168
+ if (errnoOf(error) !== "EEXIST") return reject(messageOf(error));
169
+ if (!isStale(path, options.staleMs, now)) return reject(`locked: ${path}`);
170
+ rmSync(path, { force: true });
171
+ }
172
+ }
173
+ return reject(`locked: ${path}`);
174
+ };
175
+ var walkInto = (root, dir, out) => {
176
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
177
+ const full = join(dir, entry.name);
178
+ if (entry.isDirectory()) {
179
+ if (!SKIPPED_DIRS.has(entry.name)) walkInto(root, full, out);
180
+ } else if (entry.isFile()) {
181
+ out.push(relative(root, full).split("\\").join("/"));
182
+ }
183
+ }
184
+ };
185
+ var nodeWalkFiles = (root) => {
186
+ try {
187
+ const out = [];
188
+ walkInto(root, root, out);
189
+ return accept(out.sort());
190
+ } catch (error) {
191
+ return reject(messageOf(error));
192
+ }
193
+ };
194
+
195
+ // src/core/node-git.ts
196
+ import { spawn, spawnSync } from "node:child_process";
197
+ import { createInterface } from "node:readline";
198
+ var MAX_BUFFER = 256 * 1024 * 1024;
199
+ var GIT_ENV = { ...process.env, GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
200
+ var notFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
201
+ var nodeGit = (args, cwd) => {
202
+ const result = spawnSync("git", [...args], {
203
+ cwd,
204
+ encoding: "utf8",
205
+ maxBuffer: MAX_BUFFER,
206
+ windowsHide: true,
207
+ env: GIT_ENV
208
+ });
209
+ const stdout = result.stdout;
210
+ return {
211
+ found: !notFound(result.error),
212
+ ok: result.error === void 0 && result.status === 0,
213
+ stdout: typeof stdout === "string" ? stdout : ""
214
+ };
215
+ };
216
+ var nodeGitLines = (args, cwd, onLine) => new Promise((resolve2) => {
217
+ const child = spawn("git", [...args], {
218
+ cwd,
219
+ windowsHide: true,
220
+ env: GIT_ENV,
221
+ stdio: ["ignore", "pipe", "ignore"]
222
+ });
223
+ let found = true;
224
+ child.on("error", (error) => {
225
+ found = !notFound(error);
226
+ });
227
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
228
+ lines.on("line", onLine);
229
+ child.on("close", (code) => {
230
+ resolve2({ found, ok: found && code === 0, stdout: "" });
231
+ });
232
+ });
233
+
41
234
  // src/core/node-runtime.ts
42
235
  var SAFE_TOKEN = /^[A-Za-z0-9._-]+$/;
43
236
  var WINDOWS_NOT_FOUND = 9009;
@@ -47,7 +240,7 @@ var errnoCode = (error) => {
47
240
  };
48
241
  var spawnProbe = (command, args, timeout) => {
49
242
  const options = { encoding: "utf8", timeout, windowsHide: true };
50
- return process.platform === "win32" ? spawnSync([command, ...args].join(" "), { ...options, shell: true }) : spawnSync(command, [...args], options);
243
+ return process.platform === "win32" ? spawnSync2([command, ...args].join(" "), { ...options, shell: true }) : spawnSync2(command, [...args], options);
51
244
  };
52
245
  var nodeProbe = (command, args, timeoutMs) => {
53
246
  if (![command, ...args].every((token) => SAFE_TOKEN.test(token))) {
@@ -86,7 +279,20 @@ var nodeRuntime = () => ({
86
279
  return () => {
87
280
  clearTimeout(timer);
88
281
  };
89
- }
282
+ },
283
+ writeText: nodeWriteText,
284
+ listDir: nodeListDir,
285
+ walkFiles: nodeWalkFiles,
286
+ now: () => (/* @__PURE__ */ new Date()).toISOString(),
287
+ git: nodeGit,
288
+ gitLines: nodeGitLines,
289
+ stat: nodeStat,
290
+ writeAtomic: nodeWriteAtomic,
291
+ rename: nodeRename,
292
+ mkdir: nodeMkdir,
293
+ remove: nodeRemove,
294
+ lock: nodeLock,
295
+ selfPath: toAbsolutePath(fileURLToPath2(import.meta.url))
90
296
  });
91
297
 
92
298
  // src/doctor/system.ts
@@ -213,7 +419,7 @@ var doctorEnvFrom = (ctx) => ({
213
419
  cwd: ctx.cwd,
214
420
  runtime: ctx.runtime
215
421
  });
216
- var runChecks = (env, checks = CHECKS) => checks.flatMap((check) => check(env));
422
+ var runChecks = (env, checks = CHECKS) => checks.flatMap((check2) => check2(env));
217
423
  var toItem = (ctx, finding) => ({
218
424
  check: finding.check,
219
425
  status: finding.status,
@@ -301,12 +507,19 @@ var usageProblem = (code, messageKey, fix, params = {}) => ({
301
507
  fix,
302
508
  exitCode: ExitCode.Usage
303
509
  });
510
+ var needsPersonProblem = (messageKey, fix, params = {}) => ({
511
+ code: "needs_confirmation",
512
+ messageKey,
513
+ params,
514
+ fix,
515
+ exitCode: ExitCode.NeedsPerson
516
+ });
304
517
  var FmError = class extends Error {
305
518
  problem;
306
- constructor(problem) {
307
- super(problem.code);
519
+ constructor(problem2) {
520
+ super(problem2.code);
308
521
  this.name = "FmError";
309
- this.problem = problem;
522
+ this.problem = problem2;
310
523
  }
311
524
  };
312
525
 
@@ -377,11 +590,11 @@ var success = (data, human = []) => ({
377
590
  data,
378
591
  human
379
592
  });
380
- var failureFrom = (t, problem, human) => ({
593
+ var failureFrom = (t, problem2, human) => ({
381
594
  ok: false,
382
- exitCode: problem.exitCode,
383
- error: { code: problem.code, message: t(problem.messageKey, problem.params), fix: problem.fix },
384
- ...problem.data === void 0 ? {} : { data: problem.data },
595
+ exitCode: problem2.exitCode,
596
+ error: { code: problem2.code, message: t(problem2.messageKey, problem2.params), fix: problem2.fix },
597
+ ...problem2.data === void 0 ? {} : { data: problem2.data },
385
598
  ...human === void 0 ? {} : { human }
386
599
  });
387
600
  var toEnvelope = (command, result) => ({
@@ -406,7 +619,7 @@ var GLOBAL_FLAGS = [
406
619
  { flag: "--help, -h", key: "flag.help" }
407
620
  ];
408
621
  var exitCodeHelp = (ctx) => EXIT_CODE_NAMES.map((name) => ({ code: ExitCode[name], name, summary: ctx.t(exitCodeDocs[ExitCode[name]]) }));
409
- var pad = (text, width) => text.padEnd(Math.max(width, text.length + 1));
622
+ var pad = (text2, width) => text2.padEnd(Math.max(width, text2.length + 1));
410
623
  var describe = (ctx, spec) => ({
411
624
  name: commandName(spec),
412
625
  usage: spec.usage,
@@ -453,10 +666,10 @@ var helpFor = (ctx, spec) => {
453
666
  var topicHelp = (ctx, registry, topic) => {
454
667
  const resolved = resolveCommand(registry, topic);
455
668
  if (resolved !== void 0) return helpFor(ctx, resolved.spec);
456
- const problem = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
669
+ const problem2 = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
457
670
  command: topic.join(" ")
458
671
  });
459
- return failureFrom(ctx.t, problem);
672
+ return failureFrom(ctx.t, problem2);
460
673
  };
461
674
  var helpCommand = {
462
675
  path: ["help"],
@@ -504,9 +717,1044 @@ var parseDeadline = (raw) => {
504
717
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HOOK_DEADLINE_MS;
505
718
  };
506
719
 
720
+ // src/secrets/baseline.ts
721
+ import { join as join2 } from "node:path";
722
+ var BASELINE_FILE = ".finitemonkey/findings/baseline.json";
723
+ var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
724
+ var BASELINE_STATES = ["aceito", "falso-positivo"];
725
+ var EXCEPTION_MAX_DAYS = 90;
726
+ var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
727
+ var baselinePath = (root) => toAbsolutePath(join2(root, BASELINE_FILE));
728
+ var text = (record2, key) => {
729
+ const value = record2[key];
730
+ return typeof value === "string" && value !== "" ? value : void 0;
731
+ };
732
+ var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
733
+ var parseEntry = (value) => {
734
+ if (!isRecord(value)) return void 0;
735
+ const fields = ["id", "rule", "file", "fingerprint", "owner", "reason", "acceptedAt", "expiresAt"];
736
+ const [id, rule, file, fingerprint, owner, reason, acceptedAt, expiresAt] = fields.map((key) => text(value, key));
737
+ const state = stateOf(value["state"]);
738
+ const line = value["line"];
739
+ if (id === void 0 || rule === void 0 || file === void 0 || fingerprint === void 0) return void 0;
740
+ if (owner === void 0 || reason === void 0 || acceptedAt === void 0 || expiresAt === void 0)
741
+ return void 0;
742
+ if (state === void 0 || typeof line !== "number") return void 0;
743
+ return { id, rule, file, line, fingerprint, state, owner, reason, acceptedAt, expiresAt };
744
+ };
745
+ var parseBaseline = (raw) => {
746
+ const parsed = parseJsonObject(raw);
747
+ if (!parsed.ok) return parsed;
748
+ if (parsed.value["schema"] !== BASELINE_SCHEMA) return reject(`schema must be ${BASELINE_SCHEMA}`);
749
+ const list2 = parsed.value["entries"];
750
+ if (!Array.isArray(list2)) return reject("entries must be a list");
751
+ const entries = [];
752
+ for (const [index, item] of list2.entries()) {
753
+ const entry = parseEntry(item);
754
+ if (entry === void 0) return reject(`entry ${String(index)} is incomplete`);
755
+ entries.push(entry);
756
+ }
757
+ return accept({ schema: BASELINE_SCHEMA, entries });
758
+ };
759
+ var loadBaseline = (runtime, root) => {
760
+ const raw = runtime.readText(baselinePath(root));
761
+ return raw.ok ? parseBaseline(raw.value) : accept(EMPTY_BASELINE);
762
+ };
763
+ var serializeBaseline = (baseline) => `${JSON.stringify({ schema: baseline.schema, entries: [...baseline.entries].sort((a, b) => a.id.localeCompare(b.id)) }, null, 2)}
764
+ `;
765
+ var saveBaseline = (runtime, root, baseline) => runtime.writeText(baselinePath(root), serializeBaseline(baseline));
766
+ var ratchet = (findings, baseline, now, inScope) => {
767
+ const byId = new Map(baseline.entries.map((entry) => [entry.id, entry]));
768
+ const seen = new Set(findings.map((finding) => finding.id));
769
+ const entryOf3 = (finding) => byId.get(finding.id);
770
+ return {
771
+ fresh: findings.filter((finding) => entryOf3(finding) === void 0),
772
+ known: findings.filter((finding) => (entryOf3(finding)?.expiresAt ?? "") >= now),
773
+ expired: findings.filter((finding) => {
774
+ const entry = entryOf3(finding);
775
+ return entry !== void 0 && entry.expiresAt < now;
776
+ }),
777
+ fixed: inScope === void 0 ? [] : baseline.entries.filter((entry) => !seen.has(entry.id) && inScope(entry.file))
778
+ };
779
+ };
780
+ var withoutFixed = (baseline, fixed) => {
781
+ const gone = new Set(fixed.map((entry) => entry.id));
782
+ return { schema: baseline.schema, entries: baseline.entries.filter((entry) => !gone.has(entry.id)) };
783
+ };
784
+ var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry) => entry.expiresAt >= now).map((entry) => entry.fingerprint));
785
+
786
+ // src/core/posix-path.ts
787
+ var toPosixPath = (path) => path.replace(/\\/g, "/");
788
+ var withoutCarriageReturn = (line) => line.endsWith("\r") ? line.slice(0, -1) : line;
789
+
790
+ // src/secrets/allowlist.ts
791
+ var PLACEHOLDER_WORDS = [
792
+ "example",
793
+ "placeholder",
794
+ "your_",
795
+ "your-",
796
+ "yourkey",
797
+ "yourtoken",
798
+ "yoursecret",
799
+ "_here",
800
+ "-here",
801
+ "changeme",
802
+ "change_me",
803
+ "change-me",
804
+ "replace",
805
+ "redacted",
806
+ "dummy",
807
+ "fake",
808
+ "sample",
809
+ "mock",
810
+ "fixture",
811
+ "test",
812
+ "todo",
813
+ "xxxx",
814
+ "****",
815
+ "....",
816
+ "…",
817
+ "<",
818
+ ">",
819
+ "${",
820
+ "{{",
821
+ "%s",
822
+ "process.env",
823
+ "secrets.",
824
+ "env("
825
+ ];
826
+ var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
827
+ "password",
828
+ "passwd",
829
+ "secret",
830
+ "token",
831
+ "apikey",
832
+ "api_key",
833
+ "admin",
834
+ "root",
835
+ "postgres",
836
+ "changeit",
837
+ "user",
838
+ "pass"
839
+ ]);
840
+ var ENV_REFERENCE = /^\$\{?[A-Z_][A-Z0-9_]*\}?$/;
841
+ var LOCKFILES = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|go\.sum|Cargo\.lock|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/;
842
+ var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|__fixtures__|fixtures|spec|testdata|examples?)\/|\.(?:test|spec)\.[A-Za-z0-9]+$/;
843
+ var isPlaceholder = (value) => {
844
+ const lower = value.toLowerCase();
845
+ if (PLACEHOLDER_VALUES.has(lower) || ENV_REFERENCE.test(value)) return true;
846
+ if (PLACEHOLDER_WORDS.some((word) => lower.includes(word))) return true;
847
+ return new Set(lower).size < 4;
848
+ };
849
+ var isTestOrLockPath = (path) => path !== void 0 && (LOCKFILES.test(toPosixPath(path)) || TEST_PATH.test(toPosixPath(path)));
850
+
851
+ // src/secrets/entropy.ts
852
+ var shannonEntropy = (text2) => {
853
+ if (text2.length === 0) return 0;
854
+ const counts = /* @__PURE__ */ new Map();
855
+ for (const char of text2) counts.set(char, (counts.get(char) ?? 0) + 1);
856
+ let entropy = 0;
857
+ for (const count2 of counts.values()) {
858
+ const p = count2 / text2.length;
859
+ entropy -= p * Math.log2(p);
860
+ }
861
+ return entropy;
862
+ };
863
+
864
+ // src/secrets/fingerprint.ts
865
+ import { createHash } from "node:crypto";
866
+ var sha256Hex = (text2) => createHash("sha256").update(text2).digest("hex");
867
+ var fingerprintOf = (secret) => sha256Hex(`finitemonkey:secret:${secret}`).slice(0, 16);
868
+ var ALNUM = /[A-Za-z0-9]/g;
869
+ var MIN_TAIL_LENGTH = 12;
870
+ var tailOf = (secret) => {
871
+ const visible = (secret.match(ALNUM) ?? []).join("");
872
+ return visible.length >= MIN_TAIL_LENGTH ? visible.slice(-4) : "****";
873
+ };
874
+
875
+ // src/secrets/lines.ts
876
+ var lineLocator = (text2) => {
877
+ const starts = [0];
878
+ for (let index = text2.indexOf("\n"); index !== -1; index = text2.indexOf("\n", index + 1)) starts.push(index + 1);
879
+ return (offset) => {
880
+ let low = 0;
881
+ let high = starts.length - 1;
882
+ while (low < high) {
883
+ const mid = low + high + 1 >> 1;
884
+ if ((starts[mid] ?? 0) <= offset) low = mid;
885
+ else high = mid - 1;
886
+ }
887
+ return { line: low + 1, column: offset - (starts[low] ?? 0) + 1 };
888
+ };
889
+ };
890
+
891
+ // src/secrets/rules/types.ts
892
+ var defineRule = (spec) => ({
893
+ id: spec.id,
894
+ summary: spec.summary,
895
+ kind: spec.kind ?? "credential",
896
+ severity: spec.severity ?? "high",
897
+ keywords: spec.keywords.map((keyword) => keyword.toLowerCase()),
898
+ pattern: new RegExp(spec.source, `gd${spec.flags ?? ""}`),
899
+ minLength: spec.minLength ?? 8,
900
+ minEntropy: spec.minEntropy ?? 0,
901
+ requireDigit: spec.requireDigit ?? false,
902
+ minRandomRun: spec.minRandomRun ?? 0,
903
+ ignoredNames: (spec.ignoredNames ?? []).map((name) => name.toLowerCase()),
904
+ skipTestPaths: spec.skipTestPaths ?? false,
905
+ placeholders: spec.placeholders ?? true
906
+ });
907
+
908
+ // src/secrets/rules/providers.ts
909
+ var PROVIDER_RULES = [
910
+ defineRule({
911
+ id: "aws-access-token",
912
+ summary: "AWS access key id (AKIA, ASIA, ABIA, ACCA, A3T)",
913
+ keywords: ["akia", "asia", "abia", "acca", "a3t"],
914
+ source: String.raw`\b(?<secret>(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b`,
915
+ minLength: 20,
916
+ minEntropy: 3
917
+ }),
918
+ defineRule({
919
+ id: "aws-secret-access-key",
920
+ summary: "AWS secret access key assigned to aws_secret_access_key or a similar name",
921
+ keywords: ["secret"],
922
+ source: String.raw`(?:aws_?)?secret_?access_?key["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])`,
923
+ flags: "i",
924
+ minLength: 40,
925
+ minEntropy: 4
926
+ }),
927
+ defineRule({
928
+ id: "github-pat",
929
+ summary: "GitHub classic personal access token (ghp_)",
930
+ keywords: ["ghp_"],
931
+ source: String.raw`\b(?<secret>ghp_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
932
+ minEntropy: 3
933
+ }),
934
+ defineRule({
935
+ id: "github-oauth",
936
+ summary: "GitHub OAuth access token (gho_)",
937
+ keywords: ["gho_"],
938
+ source: String.raw`\b(?<secret>gho_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
939
+ minEntropy: 3
940
+ }),
941
+ defineRule({
942
+ id: "github-app-token",
943
+ summary: "GitHub App user or installation token (ghu_, ghs_)",
944
+ keywords: ["ghu_", "ghs_"],
945
+ source: String.raw`\b(?<secret>(?:ghu|ghs)_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
946
+ minEntropy: 3
947
+ }),
948
+ defineRule({
949
+ id: "github-refresh-token",
950
+ summary: "GitHub refresh token (ghr_)",
951
+ keywords: ["ghr_"],
952
+ source: String.raw`\b(?<secret>ghr_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
953
+ minEntropy: 3
954
+ }),
955
+ defineRule({
956
+ id: "github-fine-grained-pat",
957
+ summary: "GitHub fine-grained personal access token (github_pat_)",
958
+ keywords: ["github_pat_"],
959
+ source: String.raw`\b(?<secret>github_pat_[A-Za-z0-9_]{82})(?![A-Za-z0-9_])`,
960
+ minEntropy: 3
961
+ }),
962
+ defineRule({
963
+ id: "gitlab-pat",
964
+ summary: "GitLab personal access token (glpat-)",
965
+ keywords: ["glpat-"],
966
+ source: String.raw`\b(?<secret>glpat-[A-Za-z0-9_-]{20})(?![A-Za-z0-9_-])`,
967
+ minEntropy: 3
968
+ }),
969
+ defineRule({
970
+ id: "npm-access-token",
971
+ summary: "npm access token (npm_)",
972
+ keywords: ["npm_"],
973
+ source: String.raw`\b(?<secret>npm_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
974
+ flags: "i",
975
+ minEntropy: 3
976
+ }),
977
+ defineRule({
978
+ id: "slack-bot-token",
979
+ summary: "Slack bot token (xoxb-)",
980
+ keywords: ["xoxb-"],
981
+ source: String.raw`\b(?<secret>xoxb-[0-9]{10,13}-[0-9]{10,13}[A-Za-z0-9-]*)`,
982
+ minEntropy: 3
983
+ }),
984
+ defineRule({
985
+ id: "slack-user-token",
986
+ summary: "Slack user or export token (xoxp-, xoxe-)",
987
+ keywords: ["xoxp-", "xoxe-"],
988
+ source: String.raw`\b(?<secret>xox[pe](?:-[0-9]{10,13}){3}-[A-Za-z0-9-]{28,34})`,
989
+ minEntropy: 3
990
+ }),
991
+ defineRule({
992
+ id: "slack-app-token",
993
+ summary: "Slack app-level token (xapp-)",
994
+ keywords: ["xapp-"],
995
+ source: String.raw`\b(?<secret>xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+)`,
996
+ flags: "i",
997
+ minEntropy: 3
998
+ }),
999
+ defineRule({
1000
+ id: "slack-webhook-url",
1001
+ summary: "Slack incoming webhook URL",
1002
+ keywords: ["hooks.slack.com"],
1003
+ source: String.raw`(?<secret>hooks\.slack\.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56})`,
1004
+ minEntropy: 3
1005
+ }),
1006
+ defineRule({
1007
+ id: "stripe-access-token",
1008
+ summary: "Stripe live secret or restricted key (sk_live_, rk_live_)",
1009
+ keywords: ["sk_live_", "rk_live_", "sk_prod_", "rk_prod_"],
1010
+ source: String.raw`\b(?<secret>(?:sk|rk)_(?:live|prod)_[A-Za-z0-9]{20,99})(?![A-Za-z0-9])`,
1011
+ minEntropy: 3
1012
+ }),
1013
+ defineRule({
1014
+ id: "anthropic-api-key",
1015
+ summary: "Anthropic API or admin key (sk-ant-)",
1016
+ keywords: ["sk-ant-"],
1017
+ source: String.raw`\b(?<secret>sk-ant-(?:api|admin|oat)\d{2}-[A-Za-z0-9_-]{80,})`,
1018
+ minEntropy: 3.5
1019
+ }),
1020
+ defineRule({
1021
+ id: "openai-api-key",
1022
+ summary: "OpenAI API key (sk-proj-, sk-svcacct-, sk-admin- or the T3BlbkFJ marker)",
1023
+ keywords: ["t3blbkfj", "sk-proj-", "sk-svcacct-", "sk-admin-"],
1024
+ source: String.raw`\b(?<secret>sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}|sk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{58,})`,
1025
+ minEntropy: 3.5
1026
+ }),
1027
+ defineRule({
1028
+ id: "gcp-api-key",
1029
+ summary: "Google API key (AIza)",
1030
+ keywords: ["aiza"],
1031
+ source: String.raw`\b(?<secret>AIza[0-9A-Za-z_-]{35})(?![0-9A-Za-z_-])`,
1032
+ minEntropy: 3
1033
+ }),
1034
+ defineRule({
1035
+ id: "asaas-api-key",
1036
+ summary: "Asaas API key ($aact_, $aact_prod_, $aact_hmlg_) with a body; the bare prefix is a placeholder",
1037
+ keywords: ["$aact_"],
1038
+ source: String.raw`(?<secret>\$aact_(?:prod_|hmlg_)?[A-Za-z0-9+/=_:$-]{32,})`,
1039
+ minLength: 40,
1040
+ minEntropy: 3.5
1041
+ })
1042
+ ];
1043
+
1044
+ // src/secrets/rules/structural.ts
1045
+ var STRUCTURAL_RULES = [
1046
+ defineRule({
1047
+ id: "private-key",
1048
+ summary: "PEM private key block (RSA, EC, DSA, OPENSSH, PGP, PKCS#8)",
1049
+ keywords: ["private key"],
1050
+ source: String.raw`-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----(?<secret>[\s\S]{64,16384}?)-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----`,
1051
+ minLength: 64,
1052
+ minEntropy: 4
1053
+ }),
1054
+ defineRule({
1055
+ id: "jwt",
1056
+ summary: "JSON Web Token with a header, a payload and a signature, checked for entropy",
1057
+ keywords: ["eyj"],
1058
+ source: String.raw`\b(?<secret>eyJ[A-Za-z0-9_-]{17,}\.eyJ[A-Za-z0-9_-]{17,}\.[A-Za-z0-9_-]{10,})`,
1059
+ minLength: 40,
1060
+ minEntropy: 3
1061
+ }),
1062
+ defineRule({
1063
+ id: "credential-in-url",
1064
+ summary: "Password or token inside a URL (scheme://user:secret@host), including git remotes",
1065
+ keywords: ["://"],
1066
+ source: String.raw`\b[a-z][a-z0-9+.-]{1,20}://[^\s:/@"'\x60]{1,64}:(?<secret>[^\s:/@"'\x60]{6,128})@[A-Za-z0-9.-]`,
1067
+ flags: "i",
1068
+ minLength: 6,
1069
+ minEntropy: 2.5
1070
+ }),
1071
+ defineRule({
1072
+ id: "default-secret-fallback",
1073
+ summary: "JavaScript secret variable with a constant fallback (process.env.X ?? 'constant')",
1074
+ kind: "practice",
1075
+ severity: "medium",
1076
+ keywords: ["process.env."],
1077
+ source: String.raw`process\.env\.[A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Z0-9_]*[ \t]*(?:\?\?|\|\|)[ \t]*["'\x60](?<secret>[^"'\x60\n]{4,})["'\x60]`,
1078
+ minLength: 4,
1079
+ placeholders: false,
1080
+ skipTestPaths: true
1081
+ }),
1082
+ defineRule({
1083
+ id: "default-secret-fallback-python",
1084
+ summary: "Python secret variable with a constant default (os.getenv('X', 'constant'))",
1085
+ kind: "practice",
1086
+ severity: "medium",
1087
+ keywords: ["getenv", "environ.get"],
1088
+ source: String.raw`os\.(?:getenv|environ\.get)\([ \t]*["'][A-Za-z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Za-z0-9_]*["'][ \t]*,[ \t]*["'](?<secret>[^"'\n]{4,})["']`,
1089
+ minLength: 4,
1090
+ placeholders: false,
1091
+ skipTestPaths: true
1092
+ }),
1093
+ defineRule({
1094
+ id: "generic-api-key",
1095
+ summary: "High-entropy value assigned to a name with key, secret, token or credential",
1096
+ keywords: ["key", "secret", "token", "credential"],
1097
+ source: String.raw`(?:key|secret|token|credential)s?[A-Za-z0-9_.-]{0,30}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9_\-+/=.~$!@#%^&*]{16,200})(?![A-Za-z0-9_\-+/=.~$!@#%^&*(])`,
1098
+ flags: "i",
1099
+ minLength: 16,
1100
+ minEntropy: 3.5,
1101
+ requireDigit: true,
1102
+ minRandomRun: 12,
1103
+ ignoredNames: [
1104
+ "key",
1105
+ "keys",
1106
+ "cachekey",
1107
+ "querykey",
1108
+ "storagekey",
1109
+ "sortkey",
1110
+ "idempotencykey",
1111
+ "keyname",
1112
+ "tokentype"
1113
+ ],
1114
+ skipTestPaths: true
1115
+ }),
1116
+ defineRule({
1117
+ id: "password-assignment",
1118
+ summary: "Literal value assigned to a password name (password, passwd, pwd, senha, contraseña)",
1119
+ keywords: ["passw", "pwd", "senha", "contrase"],
1120
+ source: String.raw`(?:password|passwd|pwd|senha|contrase[ñn]a)[A-Za-z0-9_.-]{0,20}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[^\s"'\x60,;()[\]{}<>]{8,128})`,
1121
+ flags: "i",
1122
+ minLength: 8,
1123
+ minEntropy: 3,
1124
+ requireDigit: true,
1125
+ skipTestPaths: true
1126
+ })
1127
+ ];
1128
+
1129
+ // src/secrets/rules/catalog.ts
1130
+ var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES];
1131
+ var CREDENTIAL_RULES = RULES.filter((rule) => rule.kind === "credential");
1132
+ var ruleById = (id) => RULES.find((rule) => rule.id === id);
1133
+
1134
+ // src/secrets/engine.ts
1135
+ var DIGIT = /[0-9]/;
1136
+ var LETTER = /[A-Za-z]/;
1137
+ var RUN_ENTROPY = 3;
1138
+ var hasRandomRun = (secret, length) => (secret.match(new RegExp(`[A-Za-z0-9]{${String(length)},}`, "g")) ?? []).some(
1139
+ (run2) => DIGIT.test(run2) && LETTER.test(run2) && shannonEntropy(run2) >= RUN_ENTROPY
1140
+ );
1141
+ var accepted = (rule, secret) => secret.length >= rule.minLength && (!rule.requireDigit || DIGIT.test(secret)) && (rule.minRandomRun === 0 || hasRandomRun(secret, rule.minRandomRun)) && (!rule.placeholders || !isPlaceholder(secret)) && (rule.minEntropy === 0 || shannonEntropy(secret) >= rule.minEntropy);
1142
+ var NAME_CHAR = /[A-Za-z0-9_.-]/;
1143
+ var nameAround = (text2, at) => {
1144
+ let start = at;
1145
+ while (start > 0 && NAME_CHAR.test(text2.charAt(start - 1))) start -= 1;
1146
+ let end = at;
1147
+ while (end < text2.length && NAME_CHAR.test(text2.charAt(end))) end += 1;
1148
+ return text2.slice(start, end).toLowerCase();
1149
+ };
1150
+ var ignoredName = (rule, match) => rule.ignoredNames.length > 0 && rule.ignoredNames.includes(nameAround(match.input, match.index));
1151
+ var spanOf = (rule, match) => {
1152
+ const range = match.indices?.groups?.["secret"] ?? match.indices?.[0];
1153
+ if (range === void 0 || ignoredName(rule, match)) return void 0;
1154
+ const [start, end] = range;
1155
+ const secret = match.input.slice(start, end);
1156
+ return accepted(rule, secret) ? { rule, start, end, secret } : void 0;
1157
+ };
1158
+ var ruleSpans = (rule, text2) => {
1159
+ const spans = [];
1160
+ for (const match of text2.matchAll(rule.pattern)) {
1161
+ const span = spanOf(rule, match);
1162
+ if (span !== void 0) spans.push(span);
1163
+ }
1164
+ return spans;
1165
+ };
1166
+ var overlaps = (taken, span) => taken.some((other) => span.start < other.end && other.start < span.end);
1167
+ var applicable = (rules, lower, path) => {
1168
+ const testPath = isTestOrLockPath(path);
1169
+ return rules.filter(
1170
+ (rule) => !(testPath && rule.skipTestPaths) && rule.keywords.some((keyword) => lower.includes(keyword))
1171
+ );
1172
+ };
1173
+ var toFinding = (span, locate2) => {
1174
+ const position = locate2(span.start);
1175
+ return {
1176
+ ruleId: span.rule.id,
1177
+ severity: span.rule.severity,
1178
+ line: position.line,
1179
+ column: position.column,
1180
+ start: span.start,
1181
+ end: span.end,
1182
+ fingerprint: fingerprintOf(span.secret),
1183
+ tail: tailOf(span.secret)
1184
+ };
1185
+ };
1186
+ var scanText = (text2, options = {}) => {
1187
+ if (text2.length === 0) return [];
1188
+ const rules = applicable(options.rules ?? RULES, text2.toLowerCase(), options.path);
1189
+ if (rules.length === 0) return [];
1190
+ const taken = [];
1191
+ for (const rule of rules) {
1192
+ for (const span of ruleSpans(rule, text2)) if (!overlaps(taken, span)) taken.push(span);
1193
+ }
1194
+ if (taken.length === 0) return [];
1195
+ const locate2 = lineLocator(text2);
1196
+ return taken.sort((a, b) => a.start - b.start).map((span) => toFinding(span, locate2));
1197
+ };
1198
+
1199
+ // src/secrets/incidents.ts
1200
+ import { join as join3 } from "node:path";
1201
+ var INCIDENTS_DIR = ".finitemonkey/incidents";
1202
+ var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
1203
+ var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
1204
+ var ID = /^inc-[0-9a-f]{12}$/;
1205
+ var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
1206
+ var incidentsDir = (root) => toAbsolutePath(join3(root, INCIDENTS_DIR));
1207
+ var incidentFile = (root, id) => toAbsolutePath(join3(incidentsDir(root), `${id}.json`));
1208
+ var pick = (options, value) => options.find((option) => option === value);
1209
+ var str = (record2, key) => {
1210
+ const value = record2[key];
1211
+ return typeof value === "string" ? value : void 0;
1212
+ };
1213
+ var parseEvent = (value) => {
1214
+ if (!isRecord(value)) return void 0;
1215
+ const state = pick(INCIDENT_STATES, value["state"]);
1216
+ const at = str(value, "at");
1217
+ const by = str(value, "by");
1218
+ return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
1219
+ };
1220
+ var parseHistory = (value) => {
1221
+ if (!Array.isArray(value)) return void 0;
1222
+ const events = value.map(parseEvent);
1223
+ return events.every((event) => event !== void 0) ? events : void 0;
1224
+ };
1225
+ var parseIncident = (raw) => {
1226
+ const parsed = parseJsonObject(raw);
1227
+ if (!parsed.ok) return parsed;
1228
+ const record2 = parsed.value;
1229
+ const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str(record2, key));
1230
+ const state = pick(INCIDENT_STATES, record2["state"]);
1231
+ const source = pick(INCIDENT_SOURCES, record2["source"]);
1232
+ const history = parseHistory(record2["history"]);
1233
+ const firstCommit = str(record2, "firstCommit");
1234
+ if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
1235
+ if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
1236
+ if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
1237
+ const base = {
1238
+ schemaVersion: 1,
1239
+ id,
1240
+ rule,
1241
+ where,
1242
+ fingerprint,
1243
+ source,
1244
+ state,
1245
+ createdAt,
1246
+ updatedAt,
1247
+ history
1248
+ };
1249
+ return accept(firstCommit === void 0 ? base : { ...base, firstCommit });
1250
+ };
1251
+ var readIncident = (runtime, root, id) => {
1252
+ if (!ID.test(id)) return reject("not an incident id");
1253
+ const raw = runtime.readText(incidentFile(root, id));
1254
+ return raw.ok ? parseIncident(raw.value) : reject(raw.error);
1255
+ };
1256
+ var listIncidents = (runtime, root) => {
1257
+ const names = runtime.listDir(incidentsDir(root));
1258
+ if (!names.ok) return [];
1259
+ return names.value.filter((name) => name.endsWith(".json")).map((name) => readIncident(runtime, root, name.slice(0, -".json".length))).flatMap((parsed) => parsed.ok ? [parsed.value] : []).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1260
+ };
1261
+ var saveIncident = (runtime, root, incident) => {
1262
+ const ignore = runtime.writeText(toAbsolutePath(join3(incidentsDir(root), ".gitignore")), "*\n");
1263
+ if (!ignore.ok) return ignore;
1264
+ return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
1265
+ `);
1266
+ };
1267
+ var created = (draft, now, by) => ({
1268
+ schemaVersion: 1,
1269
+ id: incidentId(draft.rule, draft.fingerprint),
1270
+ rule: draft.rule,
1271
+ where: draft.where,
1272
+ fingerprint: draft.fingerprint,
1273
+ source: draft.source,
1274
+ state: draft.state,
1275
+ createdAt: now,
1276
+ updatedAt: now,
1277
+ ...draft.firstCommit === void 0 ? {} : { firstCommit: draft.firstCommit },
1278
+ history: [{ state: draft.state, at: now, by }]
1279
+ });
1280
+ var recordIncident = (runtime, root, draft, by) => {
1281
+ const now = runtime.now();
1282
+ const existing = readIncident(runtime, root, incidentId(draft.rule, draft.fingerprint));
1283
+ if (existing.ok && existing.value.state !== "fechado") return existing;
1284
+ const incident = existing.ok ? {
1285
+ ...existing.value,
1286
+ state: draft.state,
1287
+ updatedAt: now,
1288
+ history: [...existing.value.history, { state: draft.state, at: now, by }]
1289
+ } : created(draft, now, by);
1290
+ const saved = saveIncident(runtime, root, incident);
1291
+ return saved.ok ? accept(incident) : reject(saved.error);
1292
+ };
1293
+ var ALLOWED_FROM = {
1294
+ aberto: [],
1295
+ "exposicao-local": [],
1296
+ rotacionado: ["aberto", "exposicao-local"],
1297
+ fechado: ["rotacionado"]
1298
+ };
1299
+ var canMove = (from, to) => ALLOWED_FROM[to].includes(from);
1300
+ var moveIncident = (incident, to, now, by) => ({
1301
+ ...incident,
1302
+ state: to,
1303
+ updatedAt: now,
1304
+ history: [...incident.history, { state: to, at: now, by }]
1305
+ });
1306
+ var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
1307
+
1308
+ // src/secrets/guards/describe.ts
1309
+ var MAX_LISTED = 5;
1310
+ var describeFindings = (t, items) => {
1311
+ const listed2 = items.slice(0, MAX_LISTED).map(({ ruleId, where }) => t("secrets.found_item", { rule: ruleId, where }));
1312
+ const rest = items.length - listed2.length;
1313
+ return rest > 0 ? `${listed2.join("; ")}; ${t("secrets.found_more", { count: rest })}` : listed2.join("; ");
1314
+ };
1315
+ var lineWhere = (t, finding) => t("secrets.where.line", { line: finding.line, column: finding.column });
1316
+
1317
+ // src/secrets/guards/tool-input.ts
1318
+ var toolName = (payload) => optionalString(payload.raw, "tool_name") ?? "";
1319
+ var toolInput = (payload) => {
1320
+ const input = payload.raw["tool_input"];
1321
+ return isRecord(input) ? input : {};
1322
+ };
1323
+ var editTexts = (input, file) => {
1324
+ const edits = input["edits"];
1325
+ if (!Array.isArray(edits)) return [];
1326
+ return edits.flatMap((edit) => {
1327
+ if (!isRecord(edit)) return [];
1328
+ const text2 = optionalString(edit, "new_string");
1329
+ return text2 === void 0 ? [] : [{ file, text: text2, whole: false }];
1330
+ });
1331
+ };
1332
+ var writtenTexts = (tool, input) => {
1333
+ const file = optionalString(input, "file_path") ?? optionalString(input, "notebook_path") ?? "";
1334
+ const single = (key, whole) => {
1335
+ const text2 = optionalString(input, key);
1336
+ return text2 === void 0 ? [] : [{ file, text: text2, whole }];
1337
+ };
1338
+ switch (tool) {
1339
+ case "Write":
1340
+ return single("content", true);
1341
+ case "Edit":
1342
+ return single("new_string", false);
1343
+ case "MultiEdit":
1344
+ return editTexts(input, file);
1345
+ case "NotebookEdit":
1346
+ return single("new_source", false);
1347
+ default:
1348
+ return [];
1349
+ }
1350
+ };
1351
+ var stringLeaves = (value, out = []) => {
1352
+ if (typeof value === "string") out.push(value);
1353
+ else if (Array.isArray(value)) for (const item of value) stringLeaves(item, out);
1354
+ else if (isRecord(value)) for (const item of Object.values(value)) stringLeaves(item, out);
1355
+ return out;
1356
+ };
1357
+
1358
+ // src/secrets/guards/exposure.ts
1359
+ var HOOK_ACTOR = "finitemonkey";
1360
+ var liveFindings = (input, texts) => {
1361
+ const baseline = loadBaseline(input.runtime, input.cwd);
1362
+ const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
1363
+ return texts.flatMap((text2) => scanText(text2, { rules: CREDENTIAL_RULES })).filter((finding) => !accepted2.has(finding.fingerprint));
1364
+ };
1365
+ var record = (input, findings, draft, where) => {
1366
+ const ids = findings.map((finding) => {
1367
+ const saved = recordIncident(
1368
+ input.runtime,
1369
+ input.cwd,
1370
+ { ...draft, rule: finding.ruleId, fingerprint: finding.fingerprint, where: `${where}:${String(finding.line)}` },
1371
+ HOOK_ACTOR
1372
+ );
1373
+ return saved.ok ? saved.value.id : input.t("secrets.incident.unsaved", { detail: saved.error });
1374
+ });
1375
+ return [...new Set(ids)].join(", ");
1376
+ };
1377
+ var secretsUserPromptSubmit = (input) => {
1378
+ const prompt = optionalString(input.payload.raw, "prompt") ?? "";
1379
+ const findings = liveFindings(input, [prompt]);
1380
+ if (findings.length === 0) return { kind: "allow" };
1381
+ const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
1382
+ const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
1383
+ return {
1384
+ kind: "block",
1385
+ reason: input.t("secrets.block.prompt", { found: describeFindings(input.t, items), incident })
1386
+ };
1387
+ };
1388
+ var secretsPostToolUse = (input) => {
1389
+ const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
1390
+ if (findings.length === 0) return { kind: "allow" };
1391
+ const tool = toolName(input.payload) || "tool";
1392
+ const incident = record(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
1393
+ const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
1394
+ const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
1395
+ return {
1396
+ kind: "allow",
1397
+ output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: context } }
1398
+ };
1399
+ };
1400
+
1401
+ // src/secrets/guards/pre-tool.ts
1402
+ import { relative as relative3 } from "node:path";
1403
+
1404
+ // src/secrets/scan.ts
1405
+ import { join as join4, relative as relative2 } from "node:path";
1406
+
1407
+ // src/secrets/located.ts
1408
+ var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
1409
+ var locate = (finding, rawFile, line, commit2) => {
1410
+ const file = toPosixPath(rawFile);
1411
+ return {
1412
+ id: findingId(finding.ruleId, file, finding.fingerprint),
1413
+ ruleId: finding.ruleId,
1414
+ severity: finding.severity,
1415
+ file,
1416
+ line,
1417
+ column: finding.column,
1418
+ fingerprint: finding.fingerprint,
1419
+ tail: finding.tail,
1420
+ ...commit2 === void 0 ? {} : { commit: commit2 }
1421
+ };
1422
+ };
1423
+ var whereOf = (finding) => `${finding.file}:${String(finding.line)}`;
1424
+
1425
+ // src/secrets/diff.ts
1426
+ var COMMIT = /^commit ([0-9a-f]{7,64})\b/;
1427
+ var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
1428
+ var DEV_NULL = "/dev/null";
1429
+ var fresh = () => ({ file: void 0, inHunk: false, next: 0, text: [], lines: [] });
1430
+ var targetOf = (line) => {
1431
+ const path = line.slice(4).replace(/\t.*$/, "");
1432
+ if (path === DEV_NULL) return void 0;
1433
+ return path.startsWith("b/") ? path.slice(2) : path;
1434
+ };
1435
+ var collect = (chunk, commit2, found) => {
1436
+ const { file } = chunk;
1437
+ if (file === void 0 || chunk.text.length === 0) return;
1438
+ for (const finding of scanText(chunk.text.join("\n"), { path: file })) {
1439
+ const located = locate(finding, file, chunk.lines[finding.line - 1] ?? finding.line, commit2);
1440
+ found.set(located.id, located);
1441
+ }
1442
+ };
1443
+ var addLine = (chunk, line) => {
1444
+ const hunk = HUNK.exec(line);
1445
+ if (hunk !== null) {
1446
+ chunk.inHunk = true;
1447
+ chunk.next = Number(hunk[1]);
1448
+ } else if (!chunk.inHunk && line.startsWith("+++ ")) {
1449
+ chunk.file = targetOf(line);
1450
+ } else if (chunk.inHunk && line.startsWith("+")) {
1451
+ chunk.text.push(line.slice(1));
1452
+ chunk.lines.push(chunk.next);
1453
+ chunk.next += 1;
1454
+ } else if (chunk.inHunk && line.startsWith(" ")) {
1455
+ chunk.next += 1;
1456
+ }
1457
+ };
1458
+ var diffScanner = () => {
1459
+ const found = /* @__PURE__ */ new Map();
1460
+ let commit2;
1461
+ let chunk = fresh();
1462
+ const flush2 = () => {
1463
+ collect(chunk, commit2, found);
1464
+ chunk = fresh();
1465
+ };
1466
+ const header = (line) => {
1467
+ const match = COMMIT.exec(line);
1468
+ if (match !== null) {
1469
+ flush2();
1470
+ commit2 = match[1];
1471
+ return true;
1472
+ }
1473
+ if (line.startsWith("diff --git ")) {
1474
+ flush2();
1475
+ return true;
1476
+ }
1477
+ return false;
1478
+ };
1479
+ return {
1480
+ push: (raw) => {
1481
+ const line = withoutCarriageReturn(raw);
1482
+ if (!header(line)) addLine(chunk, line);
1483
+ },
1484
+ end: () => {
1485
+ flush2();
1486
+ return [...found.values()];
1487
+ }
1488
+ };
1489
+ };
1490
+
1491
+ // src/secrets/scan.ts
1492
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
1493
+ var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
1494
+ var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
1495
+ var binary = (text2) => text2.slice(0, 8e3).includes("\0");
1496
+ var listed = (runtime, root, target) => {
1497
+ const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
1498
+ if (git.ok)
1499
+ return accept(
1500
+ git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
1501
+ );
1502
+ const base = toAbsolutePath(target, root);
1503
+ const walked = runtime.walkFiles(base);
1504
+ if (!walked.ok) return walked;
1505
+ return accept(walked.value.map((name) => toPosixPath(relative2(root, join4(base, toPosixPath(name))))));
1506
+ };
1507
+ var scanFile = (runtime, root, file) => {
1508
+ const text2 = runtime.readText(toAbsolutePath(file, root));
1509
+ if (!text2.ok || text2.value.length > MAX_FILE_BYTES || binary(text2.value)) return [];
1510
+ return scanText(text2.value, { path: file }).map((finding) => locate(finding, file, finding.line));
1511
+ };
1512
+ var scanPath = (runtime, root, target) => {
1513
+ const files = listed(runtime, root, target);
1514
+ if (!files.ok) return files;
1515
+ return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
1516
+ };
1517
+ var diffFindings = (diff) => {
1518
+ const scanner = diffScanner();
1519
+ for (const line of diff.split("\n")) scanner.push(line);
1520
+ return scanner.end().filter((finding) => !SKIPPED.test(finding.file));
1521
+ };
1522
+ var scanStaged = (runtime, root, includeUnstaged = false) => {
1523
+ const args = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
1524
+ const git = runtime.git(args, root);
1525
+ if (!git.ok) return reject(git.found ? "git diff failed" : "git not found");
1526
+ return accept(diffFindings(git.stdout));
1527
+ };
1528
+ var scanHistory = async (runtime, root) => {
1529
+ const scanner = diffScanner();
1530
+ const args = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
1531
+ const git = await runtime.gitLines(args, root, scanner.push);
1532
+ if (!git.ok) return reject(git.found ? "git log failed" : "git not found");
1533
+ return accept(scanner.end().filter((finding) => !SKIPPED.test(finding.file)));
1534
+ };
1535
+
1536
+ // src/secrets/guards/paths.ts
1537
+ var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
1538
+ var ENV_FILE = /^\.env(?:\..+)?$/i;
1539
+ var KEY_FILE = /\.(?:pem|key|p12|pfx)$/i;
1540
+ var SSH_KEY = /^id_(?:rsa|dsa|ecdsa|ed25519)(?:_[A-Za-z0-9_-]+)?$/;
1541
+ var basename2 = (path) => path.split(/[\\/]/).pop() ?? path;
1542
+ var isSensitivePath = (path) => {
1543
+ const name = basename2(path.replace(/^["']|["']$/g, ""));
1544
+ if (ENV_FILE.test(name)) return !SAFE_ENV.test(name);
1545
+ return KEY_FILE.test(name) || SSH_KEY.test(name);
1546
+ };
1547
+
1548
+ // src/secrets/guards/bash-rules.ts
1549
+ var READERS = /* @__PURE__ */ new Set([
1550
+ "cat",
1551
+ "bat",
1552
+ "batcat",
1553
+ "less",
1554
+ "more",
1555
+ "head",
1556
+ "tail",
1557
+ "nl",
1558
+ "tac",
1559
+ "strings",
1560
+ "xxd",
1561
+ "hexdump",
1562
+ "od",
1563
+ "grep",
1564
+ "egrep",
1565
+ "fgrep",
1566
+ "rg",
1567
+ "ag",
1568
+ "ack",
1569
+ "awk",
1570
+ "gawk",
1571
+ "sed",
1572
+ "cut",
1573
+ "sort",
1574
+ "uniq",
1575
+ "jq",
1576
+ "yq",
1577
+ "base64",
1578
+ "type",
1579
+ "get-content",
1580
+ "gc",
1581
+ "select-string",
1582
+ "sls",
1583
+ "diff",
1584
+ "vim",
1585
+ "vi",
1586
+ "nano",
1587
+ "view",
1588
+ "dotenv"
1589
+ ]);
1590
+ var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
1591
+ var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
1592
+ var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
1593
+ var onlyFlags = (args) => args.every((arg) => arg.startsWith("-"));
1594
+ var readTarget = (segment) => {
1595
+ const redirected = segment.redirectedFrom.find(isSensitivePath);
1596
+ if (redirected !== void 0) return redirected;
1597
+ const program = segment.program.toLowerCase();
1598
+ if (!READERS.has(program)) return void 0;
1599
+ if (program === "sed" && segment.args.some((arg) => arg.startsWith("-i"))) return void 0;
1600
+ return segment.args.find((arg) => !arg.startsWith("-") && isSensitivePath(arg));
1601
+ };
1602
+ var dumpsEnv = (segment) => {
1603
+ const program = segment.program.toLowerCase();
1604
+ if (program === "env" || program === "printenv") {
1605
+ if (onlyFlags(segment.args)) return true;
1606
+ return program === "printenv" && segment.args.some((arg) => SECRET_NAME.test(arg));
1607
+ }
1608
+ if (program === "set" || program === "export" || program === "declare") {
1609
+ return segment.args.length === 0 || segment.args.every((arg) => arg === "-p" || arg === "-x");
1610
+ }
1611
+ return LISTS_ENV.has(program) && segment.args.some((arg) => /^env:/i.test(arg));
1612
+ };
1613
+ var bashFindings = (command, segments) => {
1614
+ const person = PERSON_ONLY.exec(command);
1615
+ if (person !== null) return [{ kind: "person-only", command: person[0] }];
1616
+ return segments.flatMap((segment) => {
1617
+ const target = readTarget(segment);
1618
+ if (target !== void 0) return [{ kind: "read", target }];
1619
+ if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
1620
+ return [];
1621
+ });
1622
+ };
1623
+ var GIT_GLOBALS_WITH_VALUE = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]);
1624
+ var commitIntent = (segments) => {
1625
+ for (const segment of segments) {
1626
+ if (segment.program !== "git") continue;
1627
+ let index = 0;
1628
+ while (index < segment.args.length && (segment.args[index] ?? "").startsWith("-")) {
1629
+ index += GIT_GLOBALS_WITH_VALUE.has(segment.args[index] ?? "") ? 2 : 1;
1630
+ }
1631
+ if (segment.args[index] !== "commit") continue;
1632
+ const flags = segment.args.slice(index + 1);
1633
+ const all = flags.some((flag2) => flag2 === "--all" || /^-[A-Za-z]*a[A-Za-z]*$/.test(flag2));
1634
+ return { includeUnstaged: all };
1635
+ }
1636
+ return void 0;
1637
+ };
1638
+
1639
+ // src/secrets/guards/shell.ts
1640
+ var SEPARATORS = /\|\||&&|[|;&\n]/;
1641
+ var TOKEN = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
1642
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
1643
+ var OUTPUT_REDIRECT = /^(?:\d|&)?>{1,2}\|?$/;
1644
+ var OUTPUT_ATTACHED = /^(?:\d|&)?>{1,2}\|?\S/;
1645
+ var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin"]);
1646
+ var tokensOf = (text2) => Array.from(text2.matchAll(TOKEN), (match) => match[1] ?? match[2] ?? match[3] ?? "");
1647
+ var splitRedirects = (tokens) => {
1648
+ const words = [];
1649
+ const inputs = [];
1650
+ for (let index = 0; index < tokens.length; index += 1) {
1651
+ const token = tokens[index] ?? "";
1652
+ if (OUTPUT_REDIRECT.test(token)) {
1653
+ index += 1;
1654
+ } else if (OUTPUT_ATTACHED.test(token)) {
1655
+ continue;
1656
+ } else if (token === "<") {
1657
+ inputs.push(tokens[index + 1] ?? "");
1658
+ index += 1;
1659
+ } else if (token.startsWith("<") && !token.startsWith("<<")) {
1660
+ inputs.push(token.slice(1));
1661
+ } else {
1662
+ words.push(token);
1663
+ }
1664
+ }
1665
+ return { words, inputs };
1666
+ };
1667
+ var segmentOf = (text2) => {
1668
+ const { words, inputs } = splitRedirects(tokensOf(text2));
1669
+ let index = 0;
1670
+ while (index < words.length && (ASSIGNMENT.test(words[index] ?? "") || WRAPPERS.has(words[index] ?? ""))) index += 1;
1671
+ const program = words[index];
1672
+ if (program === void 0) return inputs.length > 0 ? { program: "", args: [], redirectedFrom: inputs } : void 0;
1673
+ return { program: program.split(/[\\/]/).pop() ?? program, args: words.slice(index + 1), redirectedFrom: inputs };
1674
+ };
1675
+ var segmentsOf = (command) => command.split(SEPARATORS).flatMap((part) => {
1676
+ const segment = segmentOf(part);
1677
+ return segment === void 0 ? [] : [segment];
1678
+ });
1679
+
1680
+ // src/secrets/guards/pre-tool.ts
1681
+ var ALLOW = { kind: "allow" };
1682
+ var relativeFile = (cwd, file) => {
1683
+ if (file === "") return file;
1684
+ const rel = relative3(cwd, file).split("\\").join("/");
1685
+ return rel.startsWith("..") || rel === "" ? file : rel;
1686
+ };
1687
+ var writeDecision = (input, tool) => {
1688
+ const { t } = input;
1689
+ for (const written of writtenTexts(tool, toolInput(input.payload))) {
1690
+ const file = relativeFile(input.cwd, written.file);
1691
+ const found = scanText(written.text, { path: file });
1692
+ if (found.length === 0) continue;
1693
+ const items = found.map((finding) => ({
1694
+ ruleId: finding.ruleId,
1695
+ where: written.whole ? t("secrets.where.file", { file, line: finding.line, column: finding.column }) : t("secrets.where.new_text", { file, line: finding.line })
1696
+ }));
1697
+ return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
1698
+ }
1699
+ return ALLOW;
1700
+ };
1701
+ var bashMessage = (t, finding) => {
1702
+ switch (finding.kind) {
1703
+ case "read":
1704
+ return t("secrets.block.read", { target: finding.target });
1705
+ case "env-dump":
1706
+ return t("secrets.block.env_dump", { command: finding.command });
1707
+ case "person-only":
1708
+ return t("secrets.block.person_only", { command: finding.command });
1709
+ }
1710
+ };
1711
+ var commitDecision = (input, includeUnstaged) => {
1712
+ const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
1713
+ if (!staged.ok || staged.value.length === 0) return ALLOW;
1714
+ const items = staged.value.map((finding) => ({
1715
+ ruleId: finding.ruleId,
1716
+ where: input.t("secrets.where.file", { file: finding.file, line: finding.line, column: finding.column })
1717
+ }));
1718
+ return { kind: "block", reason: input.t("secrets.block.commit", { found: describeFindings(input.t, items) }) };
1719
+ };
1720
+ var bashDecision = (input) => {
1721
+ const command = optionalString(toolInput(input.payload), "command") ?? "";
1722
+ if (command === "") return ALLOW;
1723
+ const segments = segmentsOf(command);
1724
+ const [first] = bashFindings(command, segments);
1725
+ if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
1726
+ const found = scanText(command);
1727
+ if (found.length > 0) {
1728
+ const items = found.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
1729
+ return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
1730
+ }
1731
+ const commit2 = commitIntent(segments);
1732
+ return commit2 === void 0 ? ALLOW : commitDecision(input, commit2.includeUnstaged);
1733
+ };
1734
+ var readDecision = (input) => {
1735
+ const inputs = toolInput(input.payload);
1736
+ const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
1737
+ if (target === "" || !isSensitivePath(target)) return ALLOW;
1738
+ return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
1739
+ };
1740
+ var WRITERS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
1741
+ var SHELLS = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
1742
+ var READS = /* @__PURE__ */ new Set(["Read", "Grep"]);
1743
+ var secretsPreToolUse = (input) => {
1744
+ const tool = toolName(input.payload);
1745
+ if (WRITERS.has(tool)) return writeDecision(input, tool);
1746
+ if (SHELLS.has(tool)) return bashDecision(input);
1747
+ if (READS.has(tool)) return readDecision(input);
1748
+ return ALLOW;
1749
+ };
1750
+
507
1751
  // src/hooks/handlers.ts
508
1752
  var allow = () => ({ kind: "allow" });
509
- var DEFAULT_HANDLERS = {};
1753
+ var DEFAULT_HANDLERS = {
1754
+ PreToolUse: [secretsPreToolUse],
1755
+ UserPromptSubmit: [secretsUserPromptSubmit],
1756
+ PostToolUse: [secretsPostToolUse]
1757
+ };
510
1758
 
511
1759
  // src/hooks/payload.ts
512
1760
  var EMPTY_PAYLOAD = {
@@ -516,9 +1764,9 @@ var EMPTY_PAYLOAD = {
516
1764
  stopHookActive: false,
517
1765
  raw: {}
518
1766
  };
519
- var parseHookPayload = (event, text, interactive) => {
520
- if (interactive && text.trim() === "") return accept(EMPTY_PAYLOAD);
521
- const parsed = parseJsonObject(text);
1767
+ var parseHookPayload = (event, text2, interactive) => {
1768
+ if (interactive && text2.trim() === "") return accept(EMPTY_PAYLOAD);
1769
+ const parsed = parseJsonObject(text2);
522
1770
  if (!parsed.ok) return reject(`stdin: ${parsed.error}`);
523
1771
  const payload = {
524
1772
  hookEventName: optionalString(parsed.value, "hook_event_name"),
@@ -574,7 +1822,7 @@ var withDeadline = (work, ms, startTimer) => new Promise((resolve2, reject2) =>
574
1822
  });
575
1823
 
576
1824
  // src/hooks/runner.ts
577
- var messageOf = (error) => error instanceof Error ? error.message : String(error);
1825
+ var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
578
1826
  var decide = async (input, handlers) => {
579
1827
  let output = {};
580
1828
  for (const handler of handlers[input.event] ?? [allow]) {
@@ -585,14 +1833,21 @@ var decide = async (input, handlers) => {
585
1833
  return { kind: "allow", output };
586
1834
  };
587
1835
  var evaluate = async (event, options) => {
588
- const text = await readAll(options.stdin);
589
- const parsed = parseHookPayload(event, text, options.stdin.isTTY === true);
1836
+ const text2 = await readAll(options.stdin);
1837
+ const parsed = parseHookPayload(event, text2, options.stdin.isTTY === true);
590
1838
  if (!parsed.ok) return { kind: "failed", payload: void 0, detail: parsed.error };
591
1839
  const payload = parsed.value;
592
1840
  try {
593
- return { kind: "decided", payload, decision: await decide({ event, payload, cwd: options.cwd }, options.handlers) };
1841
+ return {
1842
+ kind: "decided",
1843
+ payload,
1844
+ decision: await decide(
1845
+ { event, payload, cwd: options.cwd, runtime: options.runtime, t: options.t },
1846
+ options.handlers
1847
+ )
1848
+ };
594
1849
  } catch (error) {
595
- return { kind: "failed", payload, detail: messageOf(error) };
1850
+ return { kind: "failed", payload, detail: messageOf2(error) };
596
1851
  }
597
1852
  };
598
1853
  var ALLOW_STDOUT = `${JSON.stringify({ continue: true })}
@@ -643,10 +1898,189 @@ var runHook = async (event, options) => {
643
1898
  options.t
644
1899
  );
645
1900
  } catch (error) {
646
- return failed(event, messageOf(error), options.t);
1901
+ return failed(event, messageOf2(error), options.t);
647
1902
  }
648
1903
  };
649
1904
 
1905
+ // src/install/plugin-yield.ts
1906
+ import { join as join5 } from "node:path";
1907
+
1908
+ // src/install/layout.ts
1909
+ var STATE_DIR = ".finitemonkey";
1910
+ var SETTINGS_FILE = ".claude/settings.json";
1911
+ var GITIGNORE_FILE = ".gitignore";
1912
+ var CONFIG_FILE = ".finitemonkey/config.yml";
1913
+ var LOCK_FILE = ".finitemonkey/lock.json";
1914
+ var BACKUPS_DIR = ".finitemonkey/backups";
1915
+ var LOCKS_DIR = ".finitemonkey/locks";
1916
+ var INSTALL_LOCK = ".finitemonkey/locks/install.lock";
1917
+ var LOCAL_BIN = "node_modules/finitemonkey/dist/finitemonkey.mjs";
1918
+ var VENDOR_BIN = ".finitemonkey/runtime/dist/finitemonkey.mjs";
1919
+ var VENDOR_PACKAGE = ".finitemonkey/runtime/package.json";
1920
+ var LOCK_SCHEMA = "finitemonkey/lock@1";
1921
+ var PLAN_SCHEMA = "finitemonkey/install-plan@1";
1922
+ var GENERATED_MARKER = "finitemonkey:generated v=1";
1923
+ var BLOCK_BEGIN = "# finitemonkey:begin v=1";
1924
+ var BLOCK_END = "# finitemonkey:end";
1925
+ var INSTALL_LOCK_STALE_MS = 5 * 60 * 1e3;
1926
+ var PRESETS = ["neutral", "gabriel"];
1927
+ var parsePreset = (raw) => raw === void 0 ? "neutral" : PRESETS.find((preset) => preset === raw);
1928
+ var COMMITTED_STATE = ["config.yml", "runtime/", "tasks/", "rules.yml", "policy.yml", "findings/"];
1929
+
1930
+ // src/install/hook-entries.ts
1931
+ var INSTALLED_HOOKS = [
1932
+ { event: "SessionStart", timeout: 30, guard: false },
1933
+ { event: "UserPromptSubmit", timeout: 10, guard: true },
1934
+ { event: "PreToolUse", timeout: 10, guard: true },
1935
+ { event: "PostToolUse", timeout: 30, guard: false },
1936
+ { event: "Stop", timeout: 120, guard: false },
1937
+ { event: "SubagentStart", timeout: 30, guard: false },
1938
+ { event: "SubagentStop", timeout: 120, guard: false },
1939
+ { event: "PreCompact", timeout: 30, guard: false },
1940
+ { event: "ConfigChange", timeout: 10, guard: true }
1941
+ ];
1942
+ var PROJECT_BIN = `\${CLAUDE_PROJECT_DIR}/${LOCAL_BIN}`;
1943
+ var VENDORED_BIN = `\${CLAUDE_PROJECT_DIR}/${VENDOR_BIN}`;
1944
+ var isScript = (path) => /\.(c|m)?js$/i.test(path);
1945
+ var execForm = (runner, event) => {
1946
+ if (runner.kind === "local") return { command: "node", args: [PROJECT_BIN, "hook", event] };
1947
+ if (runner.kind === "vendored") return { command: "node", args: [VENDORED_BIN, "hook", event] };
1948
+ if (isScript(runner.path)) return { command: "node", args: [runner.path, "hook", event] };
1949
+ return { command: runner.path, args: ["hook", event] };
1950
+ };
1951
+ var hookHandler = (runner, spec, failClosedSupported) => {
1952
+ const failure2 = spec.guard && failClosedSupported ? { onFailure: "block" } : {};
1953
+ const { command, args } = execForm(runner, spec.event);
1954
+ return { type: "command", command, args, timeout: spec.timeout, ...failure2 };
1955
+ };
1956
+ var OWN_SHELL = /(^|[\s/"'])finitemonkey(@[^\s"]+|\.mjs|\.cmd|\.exe)?"?\s+hook\s/;
1957
+ var OWN_SCRIPT = /finitemonkey(\.mjs|\.cmd|\.exe)?$/;
1958
+ var stringsOf = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1959
+ var isOwnHandler = (handler) => {
1960
+ if (!isRecord(handler)) return false;
1961
+ const command = handler["command"];
1962
+ if (typeof command !== "string") return false;
1963
+ const args = stringsOf(handler["args"]);
1964
+ if (args.length === 0) return OWN_SHELL.test(`${command} `);
1965
+ const hookAt = args.indexOf("hook");
1966
+ if (hookAt < 0) return false;
1967
+ const target = hookAt === 0 ? command : args[hookAt - 1] ?? "";
1968
+ return OWN_SCRIPT.test(target);
1969
+ };
1970
+
1971
+ // src/install/json-text.ts
1972
+ var DEFAULT_STYLE = { indent: " ", finalNewline: true };
1973
+ var styleOf = (text2) => {
1974
+ const indented = /\n([ \t]+)\S/.exec(text2);
1975
+ return { indent: indented?.[1] ?? DEFAULT_STYLE.indent, finalNewline: text2.endsWith("\n") };
1976
+ };
1977
+ var renderJson = (value, style) => `${JSON.stringify(value, null, style.indent)}${style.finalNewline ? "\n" : ""}`;
1978
+ var parseSettings = (text2) => text2 === void 0 || text2.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text2);
1979
+ var asList = (value) => Array.isArray(value) ? value : [];
1980
+ var asRecord = (value) => isRecord(value) ? value : {};
1981
+ var setKey = (record2, key, value) => {
1982
+ if (!(key in record2)) return value === void 0 ? record2 : { ...record2, [key]: value };
1983
+ return Object.fromEntries(
1984
+ Object.entries(record2).flatMap(([name, current]) => {
1985
+ if (name !== key) return [[name, current]];
1986
+ return value === void 0 ? [] : [[name, value]];
1987
+ })
1988
+ );
1989
+ };
1990
+
1991
+ // src/install/settings-merge.ts
1992
+ var SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
1993
+ var ATTRIBUTION_OFF = { commit: "", pr: "", sessionUrl: false };
1994
+ var MARKETPLACE_ENTRY = { source: { source: "github", repo: "gabrielvieiraf/finitemonkey" } };
1995
+ var MARKETPLACE_KEY = "finitemonkey";
1996
+ var PLUGIN_ID = "finitemonkey@finitemonkey";
1997
+ var withEntry = (settings, key, name, value) => {
1998
+ const inner = setKey(asRecord(settings[key]), name, value);
1999
+ return setKey(settings, key, Object.keys(inner).length === 0 ? void 0 : inner);
2000
+ };
2001
+ var hasEntry = (settings, key, name) => name in asRecord(settings[key]);
2002
+ var replaceInGroup = (group, isOwn, take) => {
2003
+ if (!isRecord(group)) return group;
2004
+ const handlers = asList(group["hooks"]).flatMap((handler) => {
2005
+ if (!isOwn(handler)) return [handler];
2006
+ const replacement = take();
2007
+ return replacement === void 0 ? [] : [replacement];
2008
+ });
2009
+ return handlers.length === 0 ? void 0 : { ...group, hooks: handlers };
2010
+ };
2011
+ var mergeEvent = (groups, isOwn, wanted) => {
2012
+ let pending = wanted;
2013
+ const take = () => {
2014
+ const next = pending;
2015
+ pending = void 0;
2016
+ return next;
2017
+ };
2018
+ const kept = groups.map((group) => replaceInGroup(group, isOwn, take)).filter((group) => group !== void 0);
2019
+ return pending === void 0 ? kept : [...kept, { hooks: [pending] }];
2020
+ };
2021
+ var mergeHooks = (hooks, isOwn, wanted) => {
2022
+ const events = [.../* @__PURE__ */ new Set([...Object.keys(hooks), ...Object.keys(wanted)])];
2023
+ const entries = events.flatMap((event) => {
2024
+ const groups = hooks[event];
2025
+ if (groups !== void 0 && !Array.isArray(groups)) return [[event, groups]];
2026
+ const merged = mergeEvent(asList(groups), isOwn, wanted[event]);
2027
+ return merged.length === 0 ? [] : [[event, merged]];
2028
+ });
2029
+ return Object.fromEntries(entries);
2030
+ };
2031
+ var withHooks = (settings, hooks) => {
2032
+ return setKey(settings, "hooks", Object.keys(hooks).length === 0 ? void 0 : hooks);
2033
+ };
2034
+ var withPermissions = (settings, deny) => {
2035
+ const permissions = asRecord(settings["permissions"]);
2036
+ const next = setKey(permissions, "deny", deny.length === 0 ? void 0 : deny);
2037
+ return setKey(settings, "permissions", Object.keys(next).length === 0 ? void 0 : next);
2038
+ };
2039
+ var ownHookMatcher = (recorded) => (handler) => isOwnHandler(handler) || recorded.some((own) => JSON.stringify(own) === JSON.stringify(handler));
2040
+ var mergeSettings = (current, wanted, recorded) => {
2041
+ const schema = Object.keys(current).length === 0;
2042
+ const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current;
2043
+ const hooks = mergeHooks(asRecord(base["hooks"]), ownHookMatcher(recorded), wanted.hooks);
2044
+ const existingDeny = asList(asRecord(base["permissions"])["deny"]);
2045
+ const deny = wanted.deny.filter((rule) => !existingDeny.includes(rule));
2046
+ const attribution = wanted.attributionOff && !("attribution" in base);
2047
+ const marketplace = wanted.marketplace && !hasEntry(base, "extraKnownMarketplaces", MARKETPLACE_KEY);
2048
+ const plugin = wanted.marketplace && !hasEntry(base, "enabledPlugins", PLUGIN_ID);
2049
+ let settings = withPermissions(withHooks(base, hooks), [...existingDeny, ...deny]);
2050
+ if (attribution) settings = { ...settings, attribution: ATTRIBUTION_OFF };
2051
+ if (marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, MARKETPLACE_ENTRY);
2052
+ if (plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, true);
2053
+ return { settings, added: { schema, deny, attribution, marketplace, plugin } };
2054
+ };
2055
+ var sameAttribution = (value) => JSON.stringify(value) === JSON.stringify(ATTRIBUTION_OFF);
2056
+ var unmergeSettings = (current, added, recorded) => {
2057
+ const hooks = mergeHooks(asRecord(current["hooks"]), ownHookMatcher(recorded), {});
2058
+ const deny = asList(asRecord(current["permissions"])["deny"]).filter(
2059
+ (rule) => typeof rule !== "string" || !added.deny.includes(rule)
2060
+ );
2061
+ let settings = withPermissions(withHooks(current, hooks), deny);
2062
+ if (added.attribution && sameAttribution(settings["attribution"])) {
2063
+ settings = setKey(settings, "attribution", void 0);
2064
+ }
2065
+ if (added.marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, void 0);
2066
+ if (added.plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, void 0);
2067
+ if (added.schema && Object.keys(settings).length === 1 && settings["$schema"] === SETTINGS_SCHEMA_URL) return {};
2068
+ return settings;
2069
+ };
2070
+ var ownHandlersIn = (settings) => Object.values(asRecord(settings["hooks"])).flatMap((groups) => asList(groups)).flatMap((group) => asList(asRecord(group)["hooks"])).filter(isOwnHandler).length;
2071
+
2072
+ // src/install/plugin-yield.ts
2073
+ var PLUGIN_VIA = "plugin";
2074
+ var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2075
+ `;
2076
+ var projectOwnsHooks = (ctx) => {
2077
+ const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2078
+ const text2 = ctx.runtime.readText(toAbsolutePath(join5(root, ...SETTINGS_FILE.split("/"))));
2079
+ if (!text2.ok) return false;
2080
+ const parsed = parseJsonObject(text2.value);
2081
+ return parsed.ok && ownHandlersIn(parsed.value) > 0;
2082
+ };
2083
+
650
2084
  // src/commands/hook.ts
651
2085
  var hookCommand = {
652
2086
  path: ["hook"],
@@ -654,6 +2088,7 @@ var hookCommand = {
654
2088
  usage: `finitemonkey hook <${HOOK_EVENTS.join("|")}>`,
655
2089
  status: "ready",
656
2090
  maxPositionals: 1,
2091
+ options: { via: { type: "string" } },
657
2092
  run: async (ctx, args) => {
658
2093
  const raw = args.positionals[0] ?? "";
659
2094
  const event = parseHookEvent(raw);
@@ -664,13 +2099,17 @@ var hookCommand = {
664
2099
  usageProblem("unknown_hook_event", "error.unknown_hook_event", "finitemonkey help hook", params)
665
2100
  );
666
2101
  }
2102
+ if (stringOption(args.values, "via") === PLUGIN_VIA && projectOwnsHooks(ctx)) {
2103
+ return { ok: true, exitCode: 0, data: { event, verdict: "allow" }, raw: { stdout: YIELD_STDOUT, stderr: "" } };
2104
+ }
667
2105
  const outcome = await runHook(event, {
668
2106
  stdin: ctx.io.stdin,
669
2107
  deadlineMs: parseDeadline(ctx.env["FINITEMONKEY_HOOK_DEADLINE_MS"]),
670
2108
  handlers: DEFAULT_HANDLERS,
671
2109
  cwd: ctx.cwd,
672
2110
  t: ctx.t,
673
- startTimer: ctx.runtime.startTimer
2111
+ startTimer: ctx.runtime.startTimer,
2112
+ runtime: ctx.runtime
674
2113
  });
675
2114
  const data = { event, verdict: outcome.verdict };
676
2115
  const output = { stdout: outcome.stdout, stderr: outcome.stderr };
@@ -687,7 +2126,7 @@ var hookCommand = {
687
2126
  };
688
2127
 
689
2128
  // src/commands/i18n-check.ts
690
- import { join } from "node:path";
2129
+ import { join as join6 } from "node:path";
691
2130
 
692
2131
  // src/i18n/en.json
693
2132
  var en_default = {
@@ -772,7 +2211,81 @@ var en_default = {
772
2211
  "exit.internal": "5 internal error: Finite Monkey itself failed; please report it",
773
2212
  "help.exit_codes": "Exit codes",
774
2213
  "hook.stop_released": "the {event} hook would block again ({detail}), but Claude Code is already continuing from a stop hook, so it lets the session stop. Run `finitemonkey doctor`",
775
- "error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}'
2214
+ "error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}',
2215
+ "secrets.command.scan": "Find secrets in files, the staged changes or the git history (local only)",
2216
+ "secrets.command.accept": "Accept one finding into the baseline, with a reason and an expiry (the person only)",
2217
+ "secrets.command.incident": "List secret incidents, mark them rotated or closed, and show the history purge steps",
2218
+ "secrets.found_item": "{rule} at {where}",
2219
+ "secrets.found_more": "and {count} more",
2220
+ "secrets.where.line": "line {line}, column {column}",
2221
+ "secrets.where.file": "{file}:{line}:{column}",
2222
+ "secrets.where.new_text": "{file} (line {line} of the new text)",
2223
+ "secrets.block.write": "blocked: the new text for {file} has a credential ({found}). Nothing was written. Keep the value in .env (outside git) and read it by its variable name. If the key is real, treat it as exposed and rotate it.",
2224
+ "secrets.block.bash": "blocked: this command has a credential ({found}) and did not run. Read the value from an environment variable instead of writing it in the command. If the key is real, treat it as exposed and rotate it.",
2225
+ "secrets.block.commit": "blocked: the staged changes have a credential ({found}), so the commit did not run. Remove it from the staged files, keep it in .env (outside git) and rotate it if it is real.",
2226
+ "secrets.block.read": "blocked: {target} holds secrets, and reading it would put their values in the conversation. Ask the person for the variable names, or read .env.example.",
2227
+ "secrets.block.env_dump": 'blocked: `{command}` prints every environment variable, secrets included. Print only the variable you need by name, or check that it exists with `test -n "$NAME"`.',
2228
+ "secrets.block.person_only": "blocked: `{command}` is a decision for the person, not the model. Ask the person to run it in their own terminal.",
2229
+ "secrets.block.prompt": "your message was not sent to the model: it has a credential ({found}). If this key was pasted anywhere else, treat it as exposed and rotate it. To use it, put it in .env and tell me only the variable name. Never paste keys into the chat. Incident: {incident}.",
2230
+ "secrets.post.context": "Finite Monkey: the {tool} output contained a credential ({found}). Incident {incident} is open; rotating the key is the person's decision. Finite Monkey output never repeats the value.",
2231
+ "secrets.incident.unsaved": "not recorded ({detail})",
2232
+ "secrets.scan.title": "Secrets scan ({mode}): {total} found, {new} new, {known} known, {expired} expired, {fixed} fixed",
2233
+ "secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
2234
+ "secrets.scan.clean": "No secrets found. The monkey keeps its bananas to itself.",
2235
+ "secrets.accept.done": "Accepted {id} ({rule} at {where}) as {state} until {expires}, by {owner}.",
2236
+ "secrets.action.accept": "accept a finding",
2237
+ "secrets.action.rotated": "mark an incident as rotated",
2238
+ "secrets.action.close": "close an incident",
2239
+ "secrets.incident.title": "{count} incident(s)",
2240
+ "secrets.incident.item": " {id} {state} {rule} {where} since {created}",
2241
+ "secrets.incident.none": "No secret incidents.",
2242
+ "secrets.incident.marked": "Incident {id} is now {state}.",
2243
+ "secrets.purge.title": "History purge for {id} ({where}, first commit {commit}). You run these steps; Finite Monkey never runs them:",
2244
+ "secrets.purge.step1": "1. Confirm the old key is revoked at the provider and the new one works in every consumer.",
2245
+ "secrets.purge.step2": "2. Outside the repository, write a replacements file with one line per leaked value: <value>==>REMOVED. Copy the value from your own records; Finite Monkey never stores or prints it.",
2246
+ "secrets.purge.step3": "3. In a fresh mirror clone, run: git filter-repo --replace-text <replacements file>",
2247
+ "secrets.purge.step4": "4. Relax branch protection yourself, push the rewritten refs, restore the protection and tell everyone with a clone to clone again.",
2248
+ "secrets.purge.step5": "5. Close the incident: finitemonkey secrets incident close {id}",
2249
+ "secrets.error.found": "{count} secret finding(s) outside the baseline or with an expired acceptance",
2250
+ "secrets.error.one_mode": "Choose only one of --staged, --history and --path",
2251
+ "secrets.error.git_unavailable": "The scan could not read git ({detail})",
2252
+ "secrets.error.baseline_invalid": "The baseline {file} is invalid: {detail}",
2253
+ "secrets.error.person_only": "Only the person can {action}, and this does not look like their terminal (no TTY, or running inside Claude Code)",
2254
+ "secrets.error.reason_required": "--reason is required: say why this finding is acceptable",
2255
+ "secrets.error.finding_not_found": "No current finding has the id {id}",
2256
+ "secrets.error.incident_not_found": "No incident has the id {id}",
2257
+ "secrets.error.not_rotated": "Incident {id} is {state}: rotate the key at the provider first, then mark it rotated",
2258
+ "secrets.error.bad_transition": "Incident {id} cannot go from {from} to {to}",
2259
+ "secrets.error.write_failed": "Could not write {file}: {detail}",
2260
+ "install.config.header": "Finite Monkey project settings. Commit this file. `finitemonkey update` keeps your edits.",
2261
+ "install.plan.title": "Finite Monkey plan for {root} (preset {preset}):",
2262
+ "install.warning": "warning: {message}",
2263
+ "install.warn.not_git": "this folder is not inside a git repository; installing here anyway",
2264
+ "install.warn.fail_open": "Claude Code {version} cannot block an action when a hook crashes (needs 2.1.295 or newer); guards fail open until you update Claude Code",
2265
+ "install.warn.authority": "{name} ({source}) also decides when a session may stop; Finite Monkey works alongside it and never turns it off",
2266
+ "install.warn.plugin": "the finitemonkey plugin is enabled ({source}); its hooks step aside while this project has its own Finite Monkey hooks",
2267
+ "install.warn.foreign": "{path} already exists and was not written by Finite Monkey; left untouched (proposed content in the diff)",
2268
+ "install.warn.edited": "{path} was edited after install; your version is kept (the new template is in the diff)",
2269
+ "install.warn.edited_kept": "kept {path}: you edited it after install",
2270
+ "install.dry_run": "Dry run: nothing was written.",
2271
+ "install.nothing_to_do": "Already up to date. Nothing to write.",
2272
+ "install.up_to_date": "The files Finite Monkey manages match this version.",
2273
+ "install.confirm_question": "apply the plan above (every touched file is backed up first)?",
2274
+ "install.done_init": "Installed. Open Claude Code and keep working: one monkey, finite time, finished work.",
2275
+ "install.done_update": "Updated. Your edits were kept.",
2276
+ "install.uninstall.title": "Removing Finite Monkey from {root}:",
2277
+ "install.uninstall.keeps_state": ".finitemonkey/ stays (backups and evidence). Add --purge to delete it too.",
2278
+ "install.uninstall.purge": ".finitemonkey/ will be deleted, including backups and evidence.",
2279
+ "install.uninstall.outside": "Not touched here, remove it yourself if you installed it: {command}",
2280
+ "install.uninstall.question": "remove what Finite Monkey added and restore the backups?",
2281
+ "install.uninstall.done": "Removed. The project is back to how it was, except files you edited.",
2282
+ "error.settings_unreadable": ".claude/settings.json is not valid JSON ({detail}), so it was not changed",
2283
+ "error.lock_unreadable": ".finitemonkey/lock.json is unreadable ({detail})",
2284
+ "error.install_locked": "another finitemonkey run holds {path}",
2285
+ "error.install_failed": "could not write {path} ({detail}); every file written in this run was put back",
2286
+ "error.not_installed": "Finite Monkey is not installed in this project (no .finitemonkey/lock.json)",
2287
+ "error.install_outdated": "the files Finite Monkey manages differ from this version",
2288
+ "error.invalid_preset": 'Unknown preset "{preset}". Use neutral or gabriel'
776
2289
  };
777
2290
 
778
2291
  // src/i18n/es.json
@@ -858,7 +2371,81 @@ var es_default = {
858
2371
  "exit.internal": "5 error interno: el propio Finite Monkey falló; por favor, repórtalo",
859
2372
  "help.exit_codes": "Códigos de salida",
860
2373
  "hook.stop_released": "el hook {event} bloquearía otra vez ({detail}), pero Claude Code ya está continuando por un hook de parada, así que deja que la sesión termine. Ejecuta `finitemonkey doctor`",
861
- "error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}'
2374
+ "error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}',
2375
+ "secrets.command.scan": "Busca secretos en los archivos, en los cambios preparados o en el historial de git (solo local)",
2376
+ "secrets.command.accept": "Acepta un hallazgo en el baseline, con motivo y vencimiento (solo la persona)",
2377
+ "secrets.command.incident": "Lista incidentes de secretos, los marca como rotados o cerrados y muestra los pasos para limpiar el historial",
2378
+ "secrets.found_item": "{rule} en {where}",
2379
+ "secrets.found_more": "y {count} más",
2380
+ "secrets.where.line": "línea {line}, columna {column}",
2381
+ "secrets.where.file": "{file}:{line}:{column}",
2382
+ "secrets.where.new_text": "{file} (línea {line} del texto nuevo)",
2383
+ "secrets.block.write": "bloqueado: el texto nuevo de {file} tiene una credencial ({found}). No se escribió nada. Deja el valor en .env (fuera de git) y léelo por el nombre de la variable. Si la clave es real, trátala como expuesta y rótala.",
2384
+ "secrets.block.bash": "bloqueado: este comando tiene una credencial ({found}) y no se ejecutó. Lee el valor de una variable de entorno en lugar de escribirlo en el comando. Si la clave es real, trátala como expuesta y rótala.",
2385
+ "secrets.block.commit": "bloqueado: los cambios preparados tienen una credencial ({found}), así que el commit no se ejecutó. Quítala de los archivos preparados, déjala en .env (fuera de git) y rótala si es real.",
2386
+ "secrets.block.read": "bloqueado: {target} guarda secretos, y leerlo pondría sus valores en la conversación. Pide a la persona los nombres de las variables, o lee .env.example.",
2387
+ "secrets.block.env_dump": 'bloqueado: `{command}` imprime todas las variables de entorno, secretos incluidos. Imprime solo la variable que necesitas por su nombre, o comprueba que existe con `test -n "$NAME"`.',
2388
+ "secrets.block.person_only": "bloqueado: `{command}` es una decisión de la persona, no del modelo. Pide a la persona que lo ejecute en su propia terminal.",
2389
+ "secrets.block.prompt": "tu mensaje no se envió al modelo: tiene una credencial ({found}). Si esta clave se pegó en otro lugar, trátala como expuesta y rótala. Para usarla, ponla en .env y dime solo el nombre de la variable. Nunca pegues claves en el chat. Incidente: {incident}.",
2390
+ "secrets.post.context": "Finite Monkey: la salida de {tool} contenía una credencial ({found}). El incidente {incident} está abierto; rotar la clave es decisión de la persona. La salida de Finite Monkey nunca repite el valor.",
2391
+ "secrets.incident.unsaved": "no registrado ({detail})",
2392
+ "secrets.scan.title": "Búsqueda de secretos ({mode}): {total} hallados, {new} nuevos, {known} conocidos, {expired} vencidos, {fixed} corregidos",
2393
+ "secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
2394
+ "secrets.scan.clean": "No se encontraron secretos. El mono se guarda sus bananas.",
2395
+ "secrets.accept.done": "Aceptado {id} ({rule} en {where}) como {state} hasta {expires}, por {owner}.",
2396
+ "secrets.action.accept": "aceptar un hallazgo",
2397
+ "secrets.action.rotated": "marcar un incidente como rotado",
2398
+ "secrets.action.close": "cerrar un incidente",
2399
+ "secrets.incident.title": "{count} incidente(s)",
2400
+ "secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
2401
+ "secrets.incident.none": "No hay incidentes de secretos.",
2402
+ "secrets.incident.marked": "El incidente {id} ahora está {state}.",
2403
+ "secrets.purge.title": "Limpieza del historial de {id} ({where}, primer commit {commit}). Estos pasos los ejecutas tú; Finite Monkey nunca los ejecuta:",
2404
+ "secrets.purge.step1": "1. Confirma que la clave vieja está revocada en el proveedor y que la nueva funciona en todos los consumidores.",
2405
+ "secrets.purge.step2": "2. Fuera del repositorio, escribe un archivo de reemplazos con una línea por valor filtrado: <valor>==>REMOVED. Copia el valor de tus propios registros; Finite Monkey nunca lo guarda ni lo imprime.",
2406
+ "secrets.purge.step3": "3. En un clon espejo nuevo, ejecuta: git filter-repo --replace-text <archivo de reemplazos>",
2407
+ "secrets.purge.step4": "4. Relaja tú la protección de la rama, envía las refs reescritas, restaura la protección y avisa a quien tenga un clon que vuelva a clonar.",
2408
+ "secrets.purge.step5": "5. Cierra el incidente: finitemonkey secrets incident close {id}",
2409
+ "secrets.error.found": "{count} hallazgo(s) de secretos fuera del baseline o con aceptación vencida",
2410
+ "secrets.error.one_mode": "Elige solo uno entre --staged, --history y --path",
2411
+ "secrets.error.git_unavailable": "La búsqueda no pudo leer git ({detail})",
2412
+ "secrets.error.baseline_invalid": "El baseline {file} no es válido: {detail}",
2413
+ "secrets.error.person_only": "Solo la persona puede {action}, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
2414
+ "secrets.error.reason_required": "--reason es obligatorio: di por qué este hallazgo es aceptable",
2415
+ "secrets.error.finding_not_found": "Ningún hallazgo actual tiene el id {id}",
2416
+ "secrets.error.incident_not_found": "Ningún incidente tiene el id {id}",
2417
+ "secrets.error.not_rotated": "El incidente {id} está {state}: rota la clave en el proveedor primero y luego márcalo como rotado",
2418
+ "secrets.error.bad_transition": "El incidente {id} no puede pasar de {from} a {to}",
2419
+ "secrets.error.write_failed": "No se pudo escribir {file}: {detail}",
2420
+ "install.config.header": "Configuración de Finite Monkey en este proyecto. Versiona este archivo. `finitemonkey update` conserva tus cambios.",
2421
+ "install.plan.title": "Plan de Finite Monkey para {root} (preset {preset}):",
2422
+ "install.warning": "aviso: {message}",
2423
+ "install.warn.not_git": "esta carpeta no está dentro de un repositorio git; se instala aquí de todos modos",
2424
+ "install.warn.fail_open": "Claude Code {version} no puede bloquear la acción cuando un hook falla (necesita 2.1.295 o más nueva); los guardas fallan abiertos hasta que actualices Claude Code",
2425
+ "install.warn.authority": "{name} ({source}) también decide cuándo puede terminar la sesión; Finite Monkey convive con él y nunca lo desactiva",
2426
+ "install.warn.plugin": "el plugin finitemonkey está habilitado ({source}); sus hooks ceden el paso mientras este proyecto tenga sus propios hooks de Finite Monkey",
2427
+ "install.warn.foreign": "{path} ya existe y no lo escribió Finite Monkey; quedó intacto (contenido propuesto en el diff)",
2428
+ "install.warn.edited": "{path} se editó después de instalar; se conserva tu versión (la plantilla nueva está en el diff)",
2429
+ "install.warn.edited_kept": "conservado {path}: lo editaste después de instalar",
2430
+ "install.dry_run": "Simulación: no se escribió nada.",
2431
+ "install.nothing_to_do": "Ya está al día. Nada que escribir.",
2432
+ "install.up_to_date": "Los archivos que gestiona Finite Monkey coinciden con esta versión.",
2433
+ "install.confirm_question": "¿aplicar el plan de arriba (cada archivo tocado se respalda antes)?",
2434
+ "install.done_init": "Instalado. Abre Claude Code y sigue trabajando: un mono, tiempo finito, trabajo terminado.",
2435
+ "install.done_update": "Actualizado. Se conservaron tus cambios.",
2436
+ "install.uninstall.title": "Quitando Finite Monkey de {root}:",
2437
+ "install.uninstall.keeps_state": ".finitemonkey/ se queda (respaldos y evidencias). Usa --purge para borrarlo también.",
2438
+ "install.uninstall.purge": ".finitemonkey/ se borrará, incluidos respaldos y evidencias.",
2439
+ "install.uninstall.outside": "No se toca aquí, quítalo tú si lo instalaste: {command}",
2440
+ "install.uninstall.question": "¿quitar lo que agregó Finite Monkey y restaurar los respaldos?",
2441
+ "install.uninstall.done": "Quitado. El proyecto volvió a como estaba, salvo los archivos que editaste.",
2442
+ "error.settings_unreadable": ".claude/settings.json no es JSON válido ({detail}), así que no se cambió",
2443
+ "error.lock_unreadable": ".finitemonkey/lock.json no se puede leer ({detail})",
2444
+ "error.install_locked": "otra ejecución de finitemonkey tiene {path}",
2445
+ "error.install_failed": "no se pudo escribir {path} ({detail}); se restauró cada archivo escrito en esta ejecución",
2446
+ "error.not_installed": "Finite Monkey no está instalado en este proyecto (no hay .finitemonkey/lock.json)",
2447
+ "error.install_outdated": "los archivos que gestiona Finite Monkey difieren de esta versión",
2448
+ "error.invalid_preset": 'Preset desconocido "{preset}". Usa neutral o gabriel'
862
2449
  };
863
2450
 
864
2451
  // src/i18n/pt-BR.json
@@ -944,7 +2531,81 @@ var pt_BR_default = {
944
2531
  "exit.internal": "5 erro interno: o próprio Finite Monkey falhou; por favor, reporte",
945
2532
  "help.exit_codes": "Códigos de saída",
946
2533
  "hook.stop_released": "o hook {event} bloquearia de novo ({detail}), mas o Claude Code já está continuando por causa de um hook de parada, então ele deixa a sessão parar. Rode `finitemonkey doctor`",
947
- "error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}'
2534
+ "error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}',
2535
+ "secrets.command.scan": "Procura segredos nos arquivos, nas mudanças preparadas ou no histórico do git (só local)",
2536
+ "secrets.command.accept": "Aceita um achado no baseline, com motivo e validade (só a pessoa)",
2537
+ "secrets.command.incident": "Lista incidentes de segredo, marca como rotacionado ou fechado e mostra os passos de limpeza do histórico",
2538
+ "secrets.found_item": "{rule} em {where}",
2539
+ "secrets.found_more": "e mais {count}",
2540
+ "secrets.where.line": "linha {line}, coluna {column}",
2541
+ "secrets.where.file": "{file}:{line}:{column}",
2542
+ "secrets.where.new_text": "{file} (linha {line} do texto novo)",
2543
+ "secrets.block.write": "bloqueado: o texto novo de {file} tem uma credencial ({found}). Nada foi gravado. Deixe o valor no .env (fora do git) e leia pelo nome da variável. Se a chave é real, trate como exposta e troque.",
2544
+ "secrets.block.bash": "bloqueado: este comando tem uma credencial ({found}) e não rodou. Leia o valor de uma variável de ambiente em vez de escrevê-lo no comando. Se a chave é real, trate como exposta e troque.",
2545
+ "secrets.block.commit": "bloqueado: as mudanças preparadas têm uma credencial ({found}), então o commit não rodou. Tire-a dos arquivos preparados, deixe no .env (fora do git) e troque a chave se for real.",
2546
+ "secrets.block.read": "bloqueado: {target} guarda segredos, e lê-lo colocaria os valores na conversa. Peça à pessoa os nomes das variáveis, ou leia o .env.example.",
2547
+ "secrets.block.env_dump": 'bloqueado: `{command}` imprime todas as variáveis de ambiente, segredos inclusive. Imprima só a variável que precisa, pelo nome, ou confira que ela existe com `test -n "$NAME"`.',
2548
+ "secrets.block.person_only": "bloqueado: `{command}` é decisão da pessoa, não do modelo. Peça à pessoa que rode no terminal dela.",
2549
+ "secrets.block.prompt": "sua mensagem não foi enviada ao modelo: ela tem uma credencial ({found}). Se essa chave foi colada em outro lugar, trate como exposta e troque. Para usá-la, ponha no .env e me diga só o nome da variável. Nunca cole chaves na conversa. Incidente: {incident}.",
2550
+ "secrets.post.context": "Finite Monkey: a saída de {tool} continha uma credencial ({found}). O incidente {incident} está aberto; trocar a chave é decisão da pessoa. A saída do Finite Monkey nunca repete o valor.",
2551
+ "secrets.incident.unsaved": "não registrado ({detail})",
2552
+ "secrets.scan.title": "Varredura de segredos ({mode}): {total} achados, {new} novos, {known} conhecidos, {expired} vencidos, {fixed} corrigidos",
2553
+ "secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
2554
+ "secrets.scan.clean": "Nenhum segredo encontrado. O macaco guarda as bananas para si.",
2555
+ "secrets.accept.done": "Aceito {id} ({rule} em {where}) como {state} até {expires}, por {owner}.",
2556
+ "secrets.action.accept": "aceitar um achado",
2557
+ "secrets.action.rotated": "marcar um incidente como rotacionado",
2558
+ "secrets.action.close": "fechar um incidente",
2559
+ "secrets.incident.title": "{count} incidente(s)",
2560
+ "secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
2561
+ "secrets.incident.none": "Nenhum incidente de segredo.",
2562
+ "secrets.incident.marked": "O incidente {id} agora está {state}.",
2563
+ "secrets.purge.title": "Limpeza do histórico de {id} ({where}, primeiro commit {commit}). Quem roda estes passos é você; o Finite Monkey nunca os roda:",
2564
+ "secrets.purge.step1": "1. Confirme que a chave antiga foi revogada no provedor e que a nova funciona em todos os consumidores.",
2565
+ "secrets.purge.step2": "2. Fora do repositório, escreva um arquivo de substituições com uma linha por valor vazado: <valor>==>REMOVED. Copie o valor dos seus registros; o Finite Monkey nunca o guarda nem imprime.",
2566
+ "secrets.purge.step3": "3. Num clone espelho novo, rode: git filter-repo --replace-text <arquivo de substituições>",
2567
+ "secrets.purge.step4": "4. Afrouxe a proteção da branch você mesmo, envie as refs reescritas, restaure a proteção e avise quem tem clone para clonar de novo.",
2568
+ "secrets.purge.step5": "5. Feche o incidente: finitemonkey secrets incident close {id}",
2569
+ "secrets.error.found": "{count} achado(s) de segredo fora do baseline ou com aceite vencido",
2570
+ "secrets.error.one_mode": "Escolha só um entre --staged, --history e --path",
2571
+ "secrets.error.git_unavailable": "A varredura não conseguiu ler o git ({detail})",
2572
+ "secrets.error.baseline_invalid": "O baseline {file} é inválido: {detail}",
2573
+ "secrets.error.person_only": "Só a pessoa pode {action}, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
2574
+ "secrets.error.reason_required": "--reason é obrigatório: diga por que este achado é aceitável",
2575
+ "secrets.error.finding_not_found": "Nenhum achado atual tem o id {id}",
2576
+ "secrets.error.incident_not_found": "Nenhum incidente tem o id {id}",
2577
+ "secrets.error.not_rotated": "O incidente {id} está {state}: troque a chave no provedor primeiro e depois marque como rotacionado",
2578
+ "secrets.error.bad_transition": "O incidente {id} não pode ir de {from} para {to}",
2579
+ "secrets.error.write_failed": "Não foi possível gravar {file}: {detail}",
2580
+ "install.config.header": "Configuração do Finite Monkey neste projeto. Versione este arquivo. O `finitemonkey update` mantém suas edições.",
2581
+ "install.plan.title": "Plano do Finite Monkey para {root} (preset {preset}):",
2582
+ "install.warning": "aviso: {message}",
2583
+ "install.warn.not_git": "esta pasta não está dentro de um repositório git; instalando aqui mesmo assim",
2584
+ "install.warn.fail_open": "o Claude Code {version} não consegue barrar a ação quando um hook quebra (precisa da 2.1.295 ou mais nova); as travas falham abertas até você atualizar o Claude Code",
2585
+ "install.warn.authority": "{name} ({source}) também decide quando a sessão pode parar; o Finite Monkey convive com ele e nunca o desliga",
2586
+ "install.warn.plugin": "o plugin finitemonkey está habilitado ({source}); os hooks dele cedem a vez enquanto este projeto tiver os próprios hooks do Finite Monkey",
2587
+ "install.warn.foreign": "{path} já existe e não foi escrito pelo Finite Monkey; ficou intocado (conteúdo proposto no diff)",
2588
+ "install.warn.edited": "{path} foi editado depois da instalação; sua versão foi mantida (o modelo novo está no diff)",
2589
+ "install.warn.edited_kept": "mantido {path}: você o editou depois da instalação",
2590
+ "install.dry_run": "Simulação: nada foi gravado.",
2591
+ "install.nothing_to_do": "Já está em dia. Nada a gravar.",
2592
+ "install.up_to_date": "Os arquivos que o Finite Monkey gerencia batem com esta versão.",
2593
+ "install.confirm_question": "aplicar o plano acima (todo arquivo tocado ganha backup antes)?",
2594
+ "install.done_init": "Instalado. Abra o Claude Code e siga trabalhando: um macaco, tempo finito, trabalho terminado.",
2595
+ "install.done_update": "Atualizado. Suas edições foram mantidas.",
2596
+ "install.uninstall.title": "Removendo o Finite Monkey de {root}:",
2597
+ "install.uninstall.keeps_state": ".finitemonkey/ fica (backups e evidências). Use --purge para apagar também.",
2598
+ "install.uninstall.purge": ".finitemonkey/ será apagado, inclusive backups e evidências.",
2599
+ "install.uninstall.outside": "Não mexido aqui, remova você mesmo se instalou: {command}",
2600
+ "install.uninstall.question": "remover o que o Finite Monkey acrescentou e restaurar os backups?",
2601
+ "install.uninstall.done": "Removido. O projeto voltou ao que era, exceto os arquivos que você editou.",
2602
+ "error.settings_unreadable": ".claude/settings.json não é JSON válido ({detail}), então não foi alterado",
2603
+ "error.lock_unreadable": ".finitemonkey/lock.json está ilegível ({detail})",
2604
+ "error.install_locked": "outra execução do finitemonkey está segurando {path}",
2605
+ "error.install_failed": "não foi possível gravar {path} ({detail}); todo arquivo gravado nesta execução foi restaurado",
2606
+ "error.not_installed": "O Finite Monkey não está instalado neste projeto (sem .finitemonkey/lock.json)",
2607
+ "error.install_outdated": "os arquivos que o Finite Monkey gerencia diferem desta versão",
2608
+ "error.invalid_preset": 'Preset desconhecido "{preset}". Use neutral ou gabriel'
948
2609
  };
949
2610
 
950
2611
  // src/i18n/catalogs.ts
@@ -1022,10 +2683,10 @@ var unreadable = (dir, file, detail) => ({
1022
2683
  exitCode: ExitCode.NotConfigured
1023
2684
  });
1024
2685
  var readCatalog = (ctx, dir, lang) => {
1025
- const file = toAbsolutePath(join(dir, `${lang}.json`));
1026
- const text = ctx.runtime.readText(file);
1027
- if (!text.ok) return reject(unreadable(dir, file, text.error));
1028
- const parsed = parseJsonObject(text.value);
2686
+ const file = toAbsolutePath(join6(dir, `${lang}.json`));
2687
+ const text2 = ctx.runtime.readText(file);
2688
+ if (!text2.ok) return reject(unreadable(dir, file, text2.error));
2689
+ const parsed = parseJsonObject(text2.value);
1029
2690
  return parsed.ok ? parsed : reject(unreadable(dir, file, parsed.error));
1030
2691
  };
1031
2692
  var loadCatalogs = (ctx, dir) => {
@@ -1037,12 +2698,12 @@ var loadCatalogs = (ctx, dir) => {
1037
2698
  if (!es.ok) return es;
1038
2699
  return accept({ en: en.value, "pt-BR": ptBR.value, es: es.value });
1039
2700
  };
1040
- var describeProblem = (ctx, problem) => {
1041
- const kind = ctx.t(`i18n.problem.${problem.kind}`, {
1042
- expected: (problem.expected ?? []).join(", "),
1043
- actual: (problem.actual ?? []).join(", ")
2701
+ var describeProblem = (ctx, problem2) => {
2702
+ const kind = ctx.t(`i18n.problem.${problem2.kind}`, {
2703
+ expected: (problem2.expected ?? []).join(", "),
2704
+ actual: (problem2.actual ?? []).join(", ")
1044
2705
  });
1045
- return ` ${problem.lang} ${problem.key} ${kind}`;
2706
+ return ` ${problem2.lang} ${problem2.key} ${kind}`;
1046
2707
  };
1047
2708
  var runI18nCheck = (ctx, dir) => {
1048
2709
  const input = dir === void 0 ? accept(catalogs) : loadCatalogs(ctx, dir);
@@ -1055,7 +2716,7 @@ var runI18nCheck = (ctx, dir) => {
1055
2716
  ok: false,
1056
2717
  exitCode: ExitCode.CheckFailed,
1057
2718
  data,
1058
- human: [summary, ...report.problems.map((problem) => describeProblem(ctx, problem))],
2719
+ human: [summary, ...report.problems.map((problem2) => describeProblem(ctx, problem2))],
1059
2720
  error: {
1060
2721
  code: "i18n_mismatch",
1061
2722
  message: ctx.t("error.i18n_mismatch", { count: report.problems.length }),
@@ -1075,35 +2736,1234 @@ var i18nCheckCommand = {
1075
2736
  }
1076
2737
  };
1077
2738
 
1078
- // src/commands/planned.ts
1079
- var planned = (name, summaryKey, options) => ({
1080
- path: [name],
1081
- summaryKey,
1082
- usage: `finitemonkey ${name}`,
1083
- status: "planned",
1084
- options,
1085
- maxPositionals: 8,
1086
- run: (ctx) => Promise.resolve(
1087
- failureFrom(ctx.t, {
1088
- code: "not_implemented",
1089
- messageKey: "error.not_implemented",
1090
- params: { command: name },
1091
- fix: "finitemonkey help",
1092
- exitCode: ExitCode.Usage,
1093
- data: { command: name, status: "planned" }
2739
+ // src/install/apply.ts
2740
+ import { dirname as dirname2 } from "node:path";
2741
+
2742
+ // src/install/hash.ts
2743
+ import { createHash as createHash2 } from "node:crypto";
2744
+ var hashText = (text2) => `sha256:${createHash2("sha256").update(text2, "utf8").digest("hex")}`;
2745
+
2746
+ // src/install/lock-file.ts
2747
+ var ENTRY_KINDS = ["settings", "gitignore", "generated"];
2748
+ var strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
2749
+ var parseAdded = (value) => {
2750
+ if (!isRecord(value)) return void 0;
2751
+ const deny = strings(value["deny"]);
2752
+ const { schema, attribution } = value;
2753
+ if (deny === void 0 || typeof schema !== "boolean" || typeof attribution !== "boolean") return void 0;
2754
+ const marketplace = value["marketplace"] === true;
2755
+ const plugin = value["plugin"] === true;
2756
+ return { schema, attribution, deny, marketplace, plugin };
2757
+ };
2758
+ var parseEntry2 = (value) => {
2759
+ if (!isRecord(value)) return void 0;
2760
+ const { path, kind, existed, hash, backup } = value;
2761
+ const known = ENTRY_KINDS.find((item) => item === kind);
2762
+ if (typeof path !== "string" || known === void 0 || typeof existed !== "boolean" || typeof hash !== "string") {
2763
+ return void 0;
2764
+ }
2765
+ const added = parseAdded(value["added"]);
2766
+ const handlers = Array.isArray(value["handlers"]) ? value["handlers"].filter(isRecord) : void 0;
2767
+ return {
2768
+ path,
2769
+ kind: known,
2770
+ existed,
2771
+ hash,
2772
+ ...typeof backup === "string" ? { backup } : {},
2773
+ ...added === void 0 ? {} : { added },
2774
+ ...handlers === void 0 ? {} : { handlers }
2775
+ };
2776
+ };
2777
+ var parseEntries = (value) => {
2778
+ if (!Array.isArray(value)) return void 0;
2779
+ const entries = value.map(parseEntry2);
2780
+ return entries.every((entry) => entry !== void 0) ? entries : void 0;
2781
+ };
2782
+ var fromRecord = (record2) => {
2783
+ const { schema, version, preset, installedAt, updatedAt } = record2;
2784
+ const known = PRESETS.find((item) => item === preset);
2785
+ const entries = parseEntries(record2["entries"]);
2786
+ const createdDirs = strings(record2["createdDirs"]);
2787
+ if (schema !== LOCK_SCHEMA) return reject(`schema is not ${LOCK_SCHEMA}`);
2788
+ if (typeof version !== "string" || typeof installedAt !== "string" || typeof updatedAt !== "string") {
2789
+ return reject("version, installedAt and updatedAt must be text");
2790
+ }
2791
+ if (known === void 0 || entries === void 0 || createdDirs === void 0) {
2792
+ return reject("preset, entries or createdDirs is malformed");
2793
+ }
2794
+ return accept({ schema, version, preset: known, installedAt, updatedAt, createdDirs, entries });
2795
+ };
2796
+ var parseLock = (text2) => {
2797
+ const parsed = parseJsonObject(text2);
2798
+ return parsed.ok ? fromRecord(parsed.value) : reject(parsed.error);
2799
+ };
2800
+ var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
2801
+ `;
2802
+
2803
+ // src/install/claude-version.ts
2804
+ var ON_FAILURE_MIN = [2, 1, 295];
2805
+ var PROBE_MS = 8e3;
2806
+ var parseClaudeVersion = (output) => {
2807
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
2808
+ return match === null ? void 0 : [Number(match[1]), Number(match[2]), Number(match[3])];
2809
+ };
2810
+ var atLeast = (version, min) => {
2811
+ for (const [index, part] of min.entries()) {
2812
+ const value = version[index] ?? 0;
2813
+ if (value !== part) return value > part;
2814
+ }
2815
+ return true;
2816
+ };
2817
+ var detectClaude = (probe) => {
2818
+ const result = probe("claude", ["--version"], PROBE_MS);
2819
+ const parsed = result.ok ? parseClaudeVersion(result.stdout) : void 0;
2820
+ if (parsed === void 0) return { version: void 0, failClosed: true };
2821
+ return { version: parsed.join("."), failClosed: atLeast(parsed, ON_FAILURE_MIN) };
2822
+ };
2823
+
2824
+ // src/install/config-template.ts
2825
+ var configTemplate = (t, preset, lang) => [
2826
+ `# ${GENERATED_MARKER}`,
2827
+ `# ${t("install.config.header")}`,
2828
+ "schemaVersion: 1",
2829
+ `preset: ${preset}`,
2830
+ `lang: ${lang}`,
2831
+ ""
2832
+ ].join("\n");
2833
+ var hasMarker = (text2) => text2.includes(GENERATED_MARKER);
2834
+
2835
+ // src/install/deny-floor.ts
2836
+ var DENY_FLOOR = {
2837
+ secrets: ["Read(./.env)", "Read(./.env.*)", "Read(./**/.env)", "Read(./**/.env.*)", "Read(./**/*.pem)"],
2838
+ git: [
2839
+ "Bash(git push --force *)",
2840
+ "Bash(git push -f *)",
2841
+ "Bash(git clean -f*)",
2842
+ "Bash(git reset --hard*)",
2843
+ "Bash(git worktree remove --force*)"
2844
+ ],
2845
+ proof: [
2846
+ "Edit(./.finitemonkey/evidence/**)",
2847
+ "Edit(./.finitemonkey/runtime/**)",
2848
+ "Edit(./.finitemonkey/lock.json)",
2849
+ "Edit(./.finitemonkey/log.jsonl)",
2850
+ "Edit(./.finitemonkey/state.md)"
2851
+ ]
2852
+ };
2853
+ var DENY_RULES = [...DENY_FLOOR.secrets, ...DENY_FLOOR.git, ...DENY_FLOOR.proof];
2854
+
2855
+ // src/install/detect.ts
2856
+ import { join as join7 } from "node:path";
2857
+ var PACKAGE_MANAGERS = [
2858
+ { name: "pnpm", files: ["pnpm-lock.yaml"] },
2859
+ { name: "yarn", files: ["yarn.lock"] },
2860
+ { name: "bun", files: ["bun.lock", "bun.lockb"] },
2861
+ { name: "npm", files: ["package-lock.json", "npm-shrinkwrap.json"] }
2862
+ ];
2863
+ var LANGUAGE_MARKERS = [
2864
+ { name: "typescript", files: ["tsconfig.json"] },
2865
+ { name: "javascript", files: ["package.json"] },
2866
+ { name: "python", files: ["pyproject.toml", "requirements.txt", "setup.py"] },
2867
+ { name: "go", files: ["go.mod"] },
2868
+ { name: "rust", files: ["Cargo.toml"] },
2869
+ { name: "ruby", files: ["Gemfile"] },
2870
+ { name: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts"] }
2871
+ ];
2872
+ var LOOP_PLUGINS = ["oh-my-claudecode", "ralph-loop", "ralph-wiggum", "everything-claude-code", "superclaude"];
2873
+ var WORKFLOW_DIRS = [
2874
+ { dir: ".planning", name: "gsd" },
2875
+ { dir: ".bmad-core", name: "bmad" },
2876
+ { dir: ".specify", name: "spec-kit" },
2877
+ { dir: "openspec", name: "openspec" }
2878
+ ];
2879
+ var fromPackageJson = (project) => {
2880
+ const text2 = project.read("package.json");
2881
+ const parsed = text2 === void 0 ? void 0 : parseJsonObject(text2);
2882
+ const field = parsed?.ok === true ? parsed.value["packageManager"] : void 0;
2883
+ if (typeof field !== "string") return void 0;
2884
+ return PACKAGE_MANAGERS.find(({ name }) => field.startsWith(`${name}@`))?.name;
2885
+ };
2886
+ var detectPackageManager = (project) => fromPackageJson(project) ?? PACKAGE_MANAGERS.find(({ files }) => files.some((file) => project.exists(file)))?.name;
2887
+ var detectLanguages = (project) => LANGUAGE_MARKERS.filter(({ files }) => files.some((file) => project.exists(file))).map(({ name }) => name);
2888
+ var handlersOf = (settings, event) => {
2889
+ const hooks = settings["hooks"];
2890
+ const groups = isRecord(hooks) ? hooks[event] : void 0;
2891
+ return asList(groups).flatMap((group) => asList(asRecord(group)["hooks"]));
2892
+ };
2893
+ var allHandlers = (settings) => {
2894
+ const hooks = settings["hooks"];
2895
+ return isRecord(hooks) ? Object.keys(hooks).flatMap((event) => handlersOf(settings, event)) : [];
2896
+ };
2897
+ var settingsInfo = (text2) => {
2898
+ if (text2 === void 0) return { exists: false, valid: true, userHooks: 0, ownHooks: 0 };
2899
+ const parsed = text2.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text2);
2900
+ if (!parsed.ok) return { exists: true, valid: false, userHooks: 0, ownHooks: 0 };
2901
+ const handlers = allHandlers(parsed.value);
2902
+ const own = handlers.filter(isOwnHandler).length;
2903
+ return { exists: true, valid: true, userHooks: handlers.length - own, ownHooks: own };
2904
+ };
2905
+ var pluginAuthorities = (settings, source) => {
2906
+ const enabled = settings["enabledPlugins"];
2907
+ if (!isRecord(enabled)) return [];
2908
+ return Object.entries(enabled).filter(([, on]) => on === true).map(([id]) => id.split("@")[0] ?? id).flatMap((name) => {
2909
+ if (name === "finitemonkey") return [{ kind: "finitemonkey-plugin", source, name }];
2910
+ return LOOP_PLUGINS.includes(name) ? [{ kind: "loop-plugin", source, name }] : [];
2911
+ });
2912
+ };
2913
+ var scopeAuthorities = (text2, source) => {
2914
+ const parsed = text2 === void 0 ? void 0 : parseJsonObject(text2);
2915
+ if (parsed?.ok !== true) return [];
2916
+ const stops = ["Stop", "SubagentStop"].flatMap(
2917
+ (event) => handlersOf(parsed.value, event).filter((handler) => !isOwnHandler(handler)).map((handler) => {
2918
+ const command = isRecord(handler) ? handler["command"] : void 0;
2919
+ return { kind: "stop-hook", source, name: `${event}: ${typeof command === "string" ? command : "?"}` };
1094
2920
  })
1095
- )
2921
+ );
2922
+ return [...stops, ...pluginAuthorities(parsed.value, source)];
2923
+ };
2924
+ var userSettingsText = (project, env) => {
2925
+ const home = env["CLAUDE_CONFIG_DIR"] ?? env["HOME"] ?? env["USERPROFILE"];
2926
+ if (home === void 0) return void 0;
2927
+ const file = env["CLAUDE_CONFIG_DIR"] === void 0 ? join7(home, ".claude", "settings.json") : join7(home, "settings.json");
2928
+ const text2 = project.runtime.readText(toAbsolutePath(file));
2929
+ return text2.ok ? text2.value : void 0;
2930
+ };
2931
+ var detectAuthorities = (project, env) => [
2932
+ ...scopeAuthorities(project.read(SETTINGS_FILE), "project"),
2933
+ ...scopeAuthorities(project.read(".claude/settings.local.json"), "local"),
2934
+ ...scopeAuthorities(userSettingsText(project, env), "user"),
2935
+ ...WORKFLOW_DIRS.filter(({ dir }) => project.isDir(dir)).map(({ name }) => ({
2936
+ kind: "workflow",
2937
+ source: "project-dir",
2938
+ name
2939
+ }))
2940
+ ];
2941
+ var chooseRunner = (project, bin) => {
2942
+ if (bin !== void 0) return { kind: "path", path: bin };
2943
+ return project.exists(LOCAL_BIN) ? { kind: "local" } : { kind: "vendored" };
2944
+ };
2945
+
2946
+ // src/install/gitignore-block.ts
2947
+ var gitignoreBlock = () => [
2948
+ BLOCK_BEGIN,
2949
+ `${STATE_DIR}/*`,
2950
+ ...COMMITTED_STATE.map((path) => `!${STATE_DIR}/${path}`),
2951
+ BLOCK_END
2952
+ ];
2953
+ var lineBreak = (text2) => text2.includes("\r\n") ? "\r\n" : "\n";
2954
+ var blockRange = (lines) => {
2955
+ const start = lines.findIndex((line) => line.trimEnd() === BLOCK_BEGIN);
2956
+ if (start < 0) return void 0;
2957
+ const end = lines.findIndex((line, index) => index > start && line.trimEnd() === BLOCK_END);
2958
+ return end < 0 ? void 0 : { start, end };
2959
+ };
2960
+ var withBlock = (text2) => {
2961
+ const eol = text2 === void 0 ? "\n" : lineBreak(text2);
2962
+ const block2 = gitignoreBlock();
2963
+ if (text2 === void 0 || text2 === "") return `${block2.join(eol)}${eol}`;
2964
+ const lines = text2.split(eol);
2965
+ const range = blockRange(lines);
2966
+ if (range !== void 0) return [...lines.slice(0, range.start), ...block2, ...lines.slice(range.end + 1)].join(eol);
2967
+ const separator = text2.endsWith(eol) ? "" : eol;
2968
+ return `${text2}${separator}${block2.join(eol)}${eol}`;
2969
+ };
2970
+ var withoutBlock = (text2) => {
2971
+ const eol = lineBreak(text2);
2972
+ const lines = text2.split(eol);
2973
+ const range = blockRange(lines);
2974
+ if (range === void 0) return text2;
2975
+ return [...lines.slice(0, range.start), ...lines.slice(range.end + 1)].join(eol);
2976
+ };
2977
+
2978
+ // src/install/vendor.ts
2979
+ var isBundle = (text2) => text2.startsWith("#!/usr/bin/env node") && text2.includes("finitemonkey");
2980
+ var isOwnPackage = (text2) => {
2981
+ const parsed = parseJsonObject(text2);
2982
+ return parsed.ok && parsed.value["name"] === "finitemonkey";
2983
+ };
2984
+ var vendoredPackage = (version) => `${JSON.stringify({ name: "finitemonkey", version, private: true, type: "module" }, null, 2)}
2985
+ `;
2986
+ var vendoredFiles = (bundle, version) => [
2987
+ { path: VENDOR_BIN, template: bundle, isOurs: isBundle },
2988
+ { path: VENDOR_PACKAGE, template: vendoredPackage(version), isOurs: isOwnPackage }
2989
+ ];
2990
+ var isBundlePath = (path) => path.endsWith(".mjs");
2991
+
2992
+ // src/install/plan.ts
2993
+ var CHANGING = ["create", "merge", "update"];
2994
+ var detect = (project, gitRoot, input) => ({
2995
+ root: project.root,
2996
+ gitRoot,
2997
+ packageManager: detectPackageManager(project),
2998
+ languages: detectLanguages(project),
2999
+ settings: settingsInfo(project.read(SETTINGS_FILE)),
3000
+ authorities: detectAuthorities(project, input.env),
3001
+ runner: chooseRunner(project, input.bin),
3002
+ claude: detectClaude(project.runtime.probe)
3003
+ });
3004
+ var readPreviousLock = (project) => {
3005
+ const text2 = project.read(LOCK_FILE);
3006
+ if (text2 === void 0) return accept(void 0);
3007
+ const parsed = parseLock(text2);
3008
+ return parsed.ok ? accept(parsed.value) : reject({ kind: "lock", detail: parsed.error });
3009
+ };
3010
+ var actionFor = (before, after, existing) => {
3011
+ if (before === void 0) return "create";
3012
+ return before === after ? "unchanged" : existing;
3013
+ };
3014
+ var union = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])];
3015
+ var joinAdded = (previous, next) => previous === void 0 ? next : {
3016
+ schema: previous.schema || next.schema,
3017
+ attribution: previous.attribution || next.attribution,
3018
+ deny: union(previous.deny, next.deny),
3019
+ marketplace: previous.marketplace || next.marketplace,
3020
+ plugin: previous.plugin || next.plugin
3021
+ };
3022
+ var wantedHooks = (detection) => Object.fromEntries(
3023
+ INSTALLED_HOOKS.map((spec) => [spec.event, hookHandler(detection.runner, spec, detection.claude.failClosed)])
3024
+ );
3025
+ var settingsItem = (project, detection, input, previous) => {
3026
+ const before = project.read(SETTINGS_FILE);
3027
+ const current = parseSettings(before);
3028
+ if (!current.ok) return reject({ kind: "settings", detail: current.error });
3029
+ const hooks = wantedHooks(detection);
3030
+ const wanted = {
3031
+ hooks,
3032
+ deny: DENY_RULES,
3033
+ attributionOff: input.preset === "gabriel",
3034
+ marketplace: input.marketplace
3035
+ };
3036
+ const merged = mergeSettings(current.value, wanted, previous?.handlers ?? []);
3037
+ const after = renderJson(merged.settings, before === void 0 ? DEFAULT_STYLE : styleOf(before));
3038
+ return accept({
3039
+ path: SETTINGS_FILE,
3040
+ kind: "settings",
3041
+ action: actionFor(before, after, "merge"),
3042
+ before,
3043
+ after,
3044
+ added: joinAdded(previous?.added, merged.added),
3045
+ handlers: Object.values(hooks)
3046
+ });
3047
+ };
3048
+ var gitignoreItem = (project) => {
3049
+ const before = project.read(GITIGNORE_FILE);
3050
+ const after = withBlock(before);
3051
+ return { path: GITIGNORE_FILE, kind: "gitignore", action: actionFor(before, after, "merge"), before, after };
3052
+ };
3053
+ var generatedItem = (project, file, previous) => {
3054
+ const before = project.read(file.path);
3055
+ const { template } = file;
3056
+ const base = { path: file.path, kind: "generated", before };
3057
+ if (before === void 0) return { ...base, action: "create", after: template };
3058
+ if (previous === void 0 && !file.isOurs(before)) {
3059
+ return { ...base, action: "skip-foreign", after: before, proposal: template };
3060
+ }
3061
+ if (hashText(before) !== previous?.hash) {
3062
+ return { ...base, action: before === template ? "unchanged" : "keep-edited", after: before, proposal: template };
3063
+ }
3064
+ return { ...base, action: actionFor(before, template, "update"), after: template };
3065
+ };
3066
+ var generatedFiles = (input, vendor) => {
3067
+ const config = { path: CONFIG_FILE, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker };
3068
+ if (!vendor) return accept([config]);
3069
+ if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
3070
+ return accept([config, ...vendoredFiles(input.bundle, input.version)]);
3071
+ };
3072
+ var entryFor = (lock, path) => lock?.entries.find((entry) => entry.path === path);
3073
+ var buildPlan = (project, detection, input, previous) => {
3074
+ const settings = settingsItem(project, detection, input, entryFor(previous, SETTINGS_FILE));
3075
+ if (!settings.ok) return settings;
3076
+ const vendor = detection.runner.kind === "vendored" || entryFor(previous, VENDOR_BIN) !== void 0;
3077
+ const files = generatedFiles(input, vendor);
3078
+ if (!files.ok) return files;
3079
+ const generated = files.value.map((file) => generatedItem(project, file, entryFor(previous, file.path)));
3080
+ return accept({
3081
+ detection,
3082
+ preset: input.preset,
3083
+ items: [settings.value, gitignoreItem(project), ...generated],
3084
+ previous
3085
+ });
3086
+ };
3087
+ var hasChanges = (plan) => plan.items.some((item) => CHANGING.includes(item.action));
3088
+ var needsWrite = (plan) => hasChanges(plan) || plan.previous === void 0;
3089
+
3090
+ // src/install/apply.ts
3091
+ var stamp = (iso) => iso.replace(/[:.]/g, "-");
3092
+ var ensureParent = (project, relative4) => project.runtime.mkdir(project.abs(dirname2(relative4)));
3093
+ var writeText = (project, relative4, text2) => {
3094
+ const parent = ensureParent(project, relative4);
3095
+ const written = parent.ok ? project.runtime.writeAtomic(project.abs(relative4), text2) : parent;
3096
+ return written.ok ? accept(void 0) : reject({ path: relative4, detail: written.error });
3097
+ };
3098
+ var restore = (project, items) => {
3099
+ for (const item of items) {
3100
+ if (item.before === void 0) project.runtime.remove(project.abs(item.path));
3101
+ else project.runtime.writeAtomic(project.abs(item.path), item.before);
3102
+ }
3103
+ };
3104
+ var backupItems = (project, dir, items) => {
3105
+ const manifest = items.map((item) => ({ path: item.path, hash: hashText(item.before ?? "") }));
3106
+ for (const item of items) {
3107
+ const saved = writeText(project, `${dir}/${item.path}`, item.before ?? "");
3108
+ if (!saved.ok) return saved;
3109
+ }
3110
+ return writeText(project, `${dir}/manifest.json`, `${JSON.stringify(manifest, null, 2)}
3111
+ `);
3112
+ };
3113
+ var entryOf = (plan, item, backupDir) => {
3114
+ const previous = plan.previous?.entries.find((entry) => entry.path === item.path);
3115
+ const existed = previous?.existed ?? item.before !== void 0;
3116
+ const backup = previous?.backup ?? (existed && backupDir !== void 0 ? `${backupDir}/${item.path}` : void 0);
3117
+ const hash = item.action === "keep-edited" ? previous?.hash ?? hashText(item.proposal ?? item.after) : hashText(item.after);
3118
+ return {
3119
+ path: item.path,
3120
+ kind: item.kind,
3121
+ existed,
3122
+ hash,
3123
+ ...backup === void 0 ? {} : { backup },
3124
+ ...item.added === void 0 ? {} : { added: item.added },
3125
+ ...item.handlers === void 0 ? {} : { handlers: item.handlers }
3126
+ };
3127
+ };
3128
+ var nextLock = (plan, version, now, backupDir, createdDirs) => ({
3129
+ schema: LOCK_SCHEMA,
3130
+ version,
3131
+ preset: plan.preset,
3132
+ installedAt: plan.previous?.installedAt ?? now,
3133
+ updatedAt: now,
3134
+ createdDirs: plan.previous?.createdDirs ?? createdDirs,
3135
+ entries: plan.items.filter((item) => item.action !== "skip-foreign").map((item) => entryOf(plan, item, backupDir))
3136
+ });
3137
+ var needsBackup = (plan, item) => item.before !== void 0 && plan.previous?.entries.find((entry) => entry.path === item.path)?.backup === void 0;
3138
+ var writeAll = (project, items) => {
3139
+ const done = [];
3140
+ for (const item of items) {
3141
+ const written = writeText(project, item.path, item.after);
3142
+ if (!written.ok) {
3143
+ restore(project, done);
3144
+ return written;
3145
+ }
3146
+ done.push(item);
3147
+ }
3148
+ return accept(void 0);
3149
+ };
3150
+ var createdBy = (project) => [".claude", STATE_DIR].filter((dir) => !project.exists(dir));
3151
+ var commit = (project, plan, version, createdDirs) => {
3152
+ const now = project.runtime.now();
3153
+ const changing = plan.items.filter((item) => CHANGING.includes(item.action));
3154
+ const toBackup = changing.filter((item) => needsBackup(plan, item));
3155
+ const backupDir = toBackup.length === 0 ? void 0 : `${BACKUPS_DIR}/${stamp(now)}`;
3156
+ if (backupDir !== void 0) {
3157
+ const saved = backupItems(project, backupDir, toBackup);
3158
+ if (!saved.ok) return saved;
3159
+ }
3160
+ const written = writeAll(project, changing);
3161
+ if (!written.ok) return written;
3162
+ const lock = nextLock(plan, version, now, backupDir, createdDirs);
3163
+ const locked = writeText(project, LOCK_FILE, renderLock(lock));
3164
+ if (!locked.ok) {
3165
+ restore(project, changing);
3166
+ return locked;
3167
+ }
3168
+ return accept({ lock, written: changing.map((item) => item.path), backup: backupDir });
3169
+ };
3170
+ var withInstallLock = (project, work) => {
3171
+ const dir = project.runtime.mkdir(project.abs(LOCKS_DIR));
3172
+ if (!dir.ok) return reject({ path: LOCKS_DIR, detail: dir.error });
3173
+ const lock = project.runtime.lock(project.abs(INSTALL_LOCK), {
3174
+ staleMs: INSTALL_LOCK_STALE_MS,
3175
+ owner: "finitemonkey"
3176
+ });
3177
+ if (!lock.ok) return reject({ path: INSTALL_LOCK, detail: lock.error });
3178
+ try {
3179
+ return work();
3180
+ } finally {
3181
+ lock.value();
3182
+ }
3183
+ };
3184
+ var applyPlan = (project, plan, version) => {
3185
+ const created2 = createdBy(project);
3186
+ return withInstallLock(project, () => commit(project, plan, version, created2));
3187
+ };
3188
+
3189
+ // src/install/diff.ts
3190
+ var MAX_DIFF_LINES = 120;
3191
+ var table = (a, b) => {
3192
+ const rows = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
3193
+ for (let i = a.length - 1; i >= 0; i -= 1) {
3194
+ const row = rows[i] ?? [];
3195
+ const below = rows[i + 1] ?? [];
3196
+ for (let j = b.length - 1; j >= 0; j -= 1) {
3197
+ row[j] = a[i] === b[j] ? (below[j + 1] ?? 0) + 1 : Math.max(below[j] ?? 0, row[j + 1] ?? 0);
3198
+ }
3199
+ }
3200
+ return rows;
3201
+ };
3202
+ var walk = (a, b) => {
3203
+ const lcs = table(a, b);
3204
+ const ops = [];
3205
+ let i = 0;
3206
+ let j = 0;
3207
+ while (i < a.length || j < b.length) {
3208
+ const left = a[i];
3209
+ const right = b[j];
3210
+ if (left !== void 0 && left === right) {
3211
+ ops.push(["=", left]);
3212
+ i += 1;
3213
+ j += 1;
3214
+ } else if (right !== void 0 && (left === void 0 || (lcs[i]?.[j + 1] ?? 0) >= (lcs[i + 1]?.[j] ?? 0))) {
3215
+ ops.push(["+", right]);
3216
+ j += 1;
3217
+ } else {
3218
+ ops.push(["-", left ?? ""]);
3219
+ i += 1;
3220
+ }
3221
+ }
3222
+ return ops;
3223
+ };
3224
+ var splitLines = (text2) => text2 === "" ? [] : text2.replace(/\r?\n$/, "").split(/\r?\n/);
3225
+ var lineDiff = (path, before, after) => {
3226
+ const changes = walk(splitLines(before), splitLines(after)).filter(([op]) => op !== "=");
3227
+ if (changes.length === 0) return [];
3228
+ const lines = changes.map(([op, line]) => `${op} ${line}`);
3229
+ const shown = lines.slice(0, MAX_DIFF_LINES);
3230
+ const hidden = lines.length - shown.length;
3231
+ return [`--- ${path}`, `+++ ${path}`, ...shown, ...hidden > 0 ? [`… ${String(hidden)} more`] : []];
3232
+ };
3233
+
3234
+ // src/install/report.ts
3235
+ var jsonDetection = (detection) => ({
3236
+ gitRoot: detection.gitRoot,
3237
+ packageManager: detection.packageManager ?? null,
3238
+ languages: detection.languages,
3239
+ settings: detection.settings,
3240
+ authorities: detection.authorities,
3241
+ runner: detection.runner.kind,
3242
+ claudeVersion: detection.claude.version ?? null,
3243
+ failClosedHooks: detection.claude.failClosed
3244
+ });
3245
+ var authorityWarning = (t, authority) => ({
3246
+ code: `authority_${authority.kind}`,
3247
+ message: t(`install.warn.${authority.kind === "finitemonkey-plugin" ? "plugin" : "authority"}`, {
3248
+ name: authority.name,
3249
+ source: authority.source
3250
+ })
1096
3251
  });
1097
- var initCommand = planned("init", "command.init.summary", {
1098
- preset: { type: "string" },
1099
- github: { type: "boolean" }
3252
+ var itemWarning = (t, item) => {
3253
+ if (item.action === "skip-foreign")
3254
+ return [{ code: "foreign_file", message: t("install.warn.foreign", { path: item.path }) }];
3255
+ if (item.action === "keep-edited")
3256
+ return [{ code: "edited_file", message: t("install.warn.edited", { path: item.path }) }];
3257
+ return [];
3258
+ };
3259
+ var planWarnings = (t, plan) => [
3260
+ ...plan.detection.gitRoot ? [] : [{ code: "not_git", message: t("install.warn.not_git") }],
3261
+ ...plan.detection.claude.failClosed ? [] : [{ code: "fail_open", message: t("install.warn.fail_open", { version: plan.detection.claude.version ?? "?" }) }],
3262
+ ...plan.detection.authorities.map((authority) => authorityWarning(t, authority)),
3263
+ ...plan.items.flatMap((item) => itemWarning(t, item))
3264
+ ];
3265
+ var planDiff = (plan) => plan.items.flatMap(
3266
+ (item) => item.action === "keep-edited" || item.action === "skip-foreign" ? lineDiff(item.path, item.before ?? "", item.proposal ?? item.after) : lineDiff(item.path, item.before ?? "", item.after)
3267
+ );
3268
+ var planData = (t, plan, mode, applied, backup) => ({
3269
+ schema: PLAN_SCHEMA,
3270
+ mode,
3271
+ applied,
3272
+ root: plan.detection.root,
3273
+ preset: plan.preset,
3274
+ detection: jsonDetection(plan.detection),
3275
+ actions: plan.items.map(({ path, action }) => ({ path, action })),
3276
+ warnings: planWarnings(t, plan),
3277
+ diff: planDiff(plan),
3278
+ ...backup === void 0 ? {} : { backup }
1100
3279
  });
1101
- var updateCommand = planned("update", "command.update.summary", { check: { type: "boolean" } });
1102
- var uninstallCommand = planned("uninstall", "command.uninstall.summary", {
1103
- purge: { type: "boolean" },
1104
- "keep-generated": { type: "boolean" }
3280
+ var pad2 = (text2, width) => text2.padEnd(width);
3281
+ var planLines = (t, data, withDiff) => [
3282
+ t("install.plan.title", { root: data.root, preset: data.preset }),
3283
+ ...data.actions.map(({ path, action }) => ` ${pad2(action, 13)}${path}`),
3284
+ ...data.warnings.map(({ message }) => t("install.warning", { message })),
3285
+ ...withDiff ? data.diff : []
3286
+ ];
3287
+
3288
+ // src/install/project.ts
3289
+ import { dirname as dirname3, join as join8 } from "node:path";
3290
+ var projectAt = (root, runtime) => {
3291
+ const abs = (relative4) => toAbsolutePath(join8(root, ...relative4.split("/")));
3292
+ return {
3293
+ root,
3294
+ runtime,
3295
+ abs,
3296
+ read: (relative4) => {
3297
+ const text2 = runtime.readText(abs(relative4));
3298
+ return text2.ok ? text2.value : void 0;
3299
+ },
3300
+ exists: (relative4) => runtime.stat(abs(relative4)) !== void 0,
3301
+ isDir: (relative4) => runtime.stat(abs(relative4))?.kind === "dir"
3302
+ };
3303
+ };
3304
+ var findGitRoot = (start, runtime) => {
3305
+ let current = start;
3306
+ for (; ; ) {
3307
+ if (runtime.stat(toAbsolutePath(join8(current, ".git"))) !== void 0) return toAbsolutePath(current);
3308
+ const parent = dirname3(current);
3309
+ if (parent === current) return void 0;
3310
+ current = parent;
3311
+ }
3312
+ };
3313
+
3314
+ // src/install/session.ts
3315
+ var openProject = (ctx) => {
3316
+ const root = findGitRoot(ctx.cwd, ctx.runtime);
3317
+ return { project: projectAt(root ?? ctx.cwd, ctx.runtime), gitRoot: root !== void 0 };
3318
+ };
3319
+ var problem = (code, messageKey, fix, exitCode, params) => ({
3320
+ code,
3321
+ messageKey,
3322
+ params,
3323
+ fix,
3324
+ exitCode
3325
+ });
3326
+ var planProblem = (error) => {
3327
+ if (error.kind === "bundle") {
3328
+ return problem("install_failed", "error.install_failed", "npx finitemonkey@latest init", ExitCode.NotConfigured, {
3329
+ path: ".finitemonkey/runtime/dist/finitemonkey.mjs",
3330
+ detail: error.detail
3331
+ });
3332
+ }
3333
+ return error.kind === "settings" ? problem(
3334
+ "settings_unreadable",
3335
+ "error.settings_unreadable",
3336
+ "fix the JSON in .claude/settings.json, then run finitemonkey init again",
3337
+ ExitCode.NotConfigured,
3338
+ { detail: error.detail }
3339
+ ) : problem(
3340
+ "lock_unreadable",
3341
+ "error.lock_unreadable",
3342
+ "finitemonkey uninstall --purge --yes",
3343
+ ExitCode.NotConfigured,
3344
+ { detail: error.detail }
3345
+ );
3346
+ };
3347
+ var applyProblem = (failure2) => failure2.detail.startsWith("locked:") ? problem(
3348
+ "install_locked",
3349
+ "error.install_locked",
3350
+ "wait for the other finitemonkey run to finish, then try again",
3351
+ ExitCode.CheckFailed,
3352
+ { path: failure2.path }
3353
+ ) : problem("install_failed", "error.install_failed", "finitemonkey doctor", ExitCode.NotConfigured, {
3354
+ path: failure2.path,
3355
+ detail: failure2.detail
3356
+ });
3357
+ var notInstalled = () => problem("not_installed", "error.not_installed", "finitemonkey init", ExitCode.NotConfigured, {});
3358
+ var confirmProblem = (command, question, fix) => problem("needs_confirmation", "error.needs_confirmation", fix, ExitCode.NeedsPerson, { command, question });
3359
+ var invalidPreset = (raw) => problem("invalid_arguments", "error.invalid_preset", "finitemonkey init --preset neutral", ExitCode.Usage, {
3360
+ preset: raw
3361
+ });
3362
+ var selfBundle = (ctx) => {
3363
+ if (!isBundlePath(ctx.runtime.selfPath)) return void 0;
3364
+ const text2 = ctx.runtime.readText(ctx.runtime.selfPath);
3365
+ return text2.ok ? text2.value : void 0;
3366
+ };
3367
+ var planInputFor = (ctx, preset, bin, marketplace) => ({
3368
+ preset,
3369
+ lang: ctx.lang,
3370
+ bin,
3371
+ version: ctx.version,
3372
+ marketplace,
3373
+ bundle: selfBundle(ctx),
3374
+ env: ctx.env,
3375
+ t: ctx.t
1105
3376
  });
1106
3377
 
3378
+ // src/commands/init.ts
3379
+ var flagsFor = (request, preset) => [
3380
+ `finitemonkey ${request.mode} --yes`,
3381
+ ...request.mode === "init" && preset !== "neutral" ? [`--preset ${preset}`] : [],
3382
+ ...request.bin === void 0 ? [] : [`--bin "${request.bin}"`],
3383
+ ...request.marketplace ? ["--marketplace"] : []
3384
+ ].join(" ");
3385
+ var presentPlan = (ctx, request, plan) => {
3386
+ const data = planData(ctx.t, plan, request.mode, false, void 0);
3387
+ const human = planLines(ctx.t, data, request.showDiff || ctx.dryRun);
3388
+ if (ctx.dryRun) return success(data, [...human, ctx.t("install.dry_run")]);
3389
+ if (!needsWrite(plan)) return success(data, [...human, ctx.t("install.nothing_to_do")]);
3390
+ const next = flagsFor(request, plan.preset);
3391
+ const problem2 = confirmProblem(request.mode, ctx.t("install.confirm_question"), next);
3392
+ return { ...failureFrom(ctx.t, { ...problem2, data }, human), next };
3393
+ };
3394
+ var applyAndReport = (ctx, request, plan) => {
3395
+ const project = openProject(ctx).project;
3396
+ const applied = applyPlan(project, plan, ctx.version);
3397
+ if (!applied.ok) return failureFrom(ctx.t, applyProblem(applied.error));
3398
+ const data = planData(ctx.t, plan, request.mode, true, applied.value.backup);
3399
+ const done = ctx.t(request.mode === "init" ? "install.done_init" : "install.done_update");
3400
+ return success(data, [...planLines(ctx.t, data, request.showDiff), done]);
3401
+ };
3402
+ var runInstall = (ctx, request, presetFallback) => {
3403
+ const preset = request.preset === void 0 ? presetFallback : parsePreset(request.preset);
3404
+ if (preset === void 0) return failureFrom(ctx.t, invalidPreset(request.preset ?? ""));
3405
+ const { project, gitRoot } = openProject(ctx);
3406
+ const previous = readPreviousLock(project);
3407
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3408
+ const input = planInputFor(ctx, preset, request.bin, request.marketplace);
3409
+ const plan = buildPlan(project, detect(project, gitRoot, input), input, previous.value);
3410
+ if (!plan.ok) return failureFrom(ctx.t, planProblem(plan.error));
3411
+ if (ctx.dryRun || !ctx.yes || !needsWrite(plan.value)) return presentPlan(ctx, request, plan.value);
3412
+ return applyAndReport(ctx, request, plan.value);
3413
+ };
3414
+ var initRequest = (args) => ({
3415
+ mode: "init",
3416
+ preset: stringOption(args.values, "preset"),
3417
+ bin: stringOption(args.values, "bin"),
3418
+ marketplace: args.values["marketplace"] === true,
3419
+ showDiff: false
3420
+ });
3421
+ var initCommand = {
3422
+ path: ["init"],
3423
+ summaryKey: "command.init.summary",
3424
+ usage: "finitemonkey init [--preset neutral|gabriel] [--bin <path>] [--marketplace] [--dry-run] [--yes]",
3425
+ status: "ready",
3426
+ options: {
3427
+ preset: { type: "string" },
3428
+ bin: { type: "string" },
3429
+ marketplace: { type: "boolean" }
3430
+ },
3431
+ run: (ctx, args) => Promise.resolve(runInstall(ctx, initRequest(args), "neutral"))
3432
+ };
3433
+
3434
+ // src/secrets/project.ts
3435
+ var projectRoot = (ctx) => {
3436
+ const top = ctx.runtime.git(["rev-parse", "--show-toplevel"], ctx.cwd);
3437
+ const path = top.stdout.trim();
3438
+ return top.ok && path !== "" ? toAbsolutePath(path) : ctx.cwd;
3439
+ };
3440
+ var isPerson = (ctx) => ctx.io.stdin.isTTY === true && ctx.env["CLAUDECODE"] === void 0 && ctx.env["CLAUDE_CODE_ENTRYPOINT"] === void 0;
3441
+ var personName = (ctx) => {
3442
+ const name = ctx.runtime.git(["config", "user.name"], ctx.cwd).stdout.trim();
3443
+ return name !== "" ? name : ctx.env["USER"] ?? ctx.env["USERNAME"] ?? "person";
3444
+ };
3445
+
3446
+ // src/commands/secrets-problems.ts
3447
+ var baselineInvalid = (detail) => ({
3448
+ code: "secrets_baseline_invalid",
3449
+ messageKey: "secrets.error.baseline_invalid",
3450
+ params: { file: BASELINE_FILE, detail },
3451
+ fix: `git checkout -- ${BASELINE_FILE}`,
3452
+ exitCode: ExitCode.NotConfigured
3453
+ });
3454
+ var writeFailed = (file, detail) => ({
3455
+ code: "secrets_write_failed",
3456
+ messageKey: "secrets.error.write_failed",
3457
+ params: { file, detail },
3458
+ fix: "finitemonkey doctor",
3459
+ exitCode: ExitCode.Internal
3460
+ });
3461
+ var gitUnavailable = (detail) => ({
3462
+ code: "secrets_git_unavailable",
3463
+ messageKey: "secrets.error.git_unavailable",
3464
+ params: { detail },
3465
+ fix: "finitemonkey doctor",
3466
+ exitCode: ExitCode.NotConfigured
3467
+ });
3468
+
3469
+ // src/commands/secrets-accept.ts
3470
+ var DAY_MS = 864e5;
3471
+ var daysOf = (values) => {
3472
+ const raw = stringOption(values, "days");
3473
+ if (raw === void 0) return EXCEPTION_MAX_DAYS;
3474
+ const days = Number.parseInt(raw, 10);
3475
+ return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
3476
+ };
3477
+ var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS).toISOString();
3478
+ var failure = (ctx, problem2) => failureFrom(ctx.t, problem2);
3479
+ var inputProblem = (ctx, id, reason, days) => {
3480
+ const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
3481
+ if (!isPerson(ctx)) {
3482
+ return needsPersonProblem("secrets.error.person_only", fix, { action: ctx.t("secrets.action.accept") });
3483
+ }
3484
+ if (id === "") return usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail: "<id>" });
3485
+ if (reason === void 0 || reason.trim() === "") {
3486
+ return usageProblem("invalid_arguments", "secrets.error.reason_required", fix);
3487
+ }
3488
+ if (days === void 0) {
3489
+ return usageProblem("invalid_arguments", "error.invalid_arguments", fix, {
3490
+ detail: `--days 1-${String(EXCEPTION_MAX_DAYS)}`
3491
+ });
3492
+ }
3493
+ return void 0;
3494
+ };
3495
+ var entryOf2 = (ctx, finding, args) => {
3496
+ const now = ctx.runtime.now();
3497
+ return {
3498
+ id: finding.id,
3499
+ rule: finding.ruleId,
3500
+ file: finding.file,
3501
+ line: finding.line,
3502
+ fingerprint: finding.fingerprint,
3503
+ state: args.falsePositive ? "falso-positivo" : "aceito",
3504
+ owner: personName(ctx),
3505
+ reason: args.reason,
3506
+ acceptedAt: now,
3507
+ expiresAt: plusDays(now, args.days)
3508
+ };
3509
+ };
3510
+ var acceptEntry = (ctx, args) => {
3511
+ const root = projectRoot(ctx);
3512
+ const baseline = loadBaseline(ctx.runtime, root);
3513
+ if (!baseline.ok) {
3514
+ return failure(ctx, baselineInvalid(baseline.error));
3515
+ }
3516
+ const scanned = scanPath(ctx.runtime, root, ".");
3517
+ const finding = scanned.ok ? scanned.value.find((item) => item.id === args.id) : void 0;
3518
+ if (finding === void 0) {
3519
+ return failure(
3520
+ ctx,
3521
+ usageProblem("secrets_finding_not_found", "secrets.error.finding_not_found", "finitemonkey secrets scan", {
3522
+ id: args.id
3523
+ })
3524
+ );
3525
+ }
3526
+ const entry = entryOf2(ctx, finding, args);
3527
+ const entries = [...baseline.value.entries.filter((item) => item.id !== entry.id), entry];
3528
+ const saved = saveBaseline(ctx.runtime, root, { schema: baseline.value.schema, entries });
3529
+ if (!saved.ok) {
3530
+ return failure(ctx, writeFailed(BASELINE_FILE, saved.error));
3531
+ }
3532
+ const line = ctx.t("secrets.accept.done", {
3533
+ id: entry.id,
3534
+ rule: entry.rule,
3535
+ where: whereOf(finding),
3536
+ state: entry.state,
3537
+ expires: entry.expiresAt.slice(0, 10),
3538
+ owner: entry.owner
3539
+ });
3540
+ return success(entry, [line]);
3541
+ };
3542
+ var secretsAcceptCommand = {
3543
+ path: ["secrets", "accept"],
3544
+ summaryKey: "secrets.command.accept",
3545
+ usage: "finitemonkey secrets accept <finding-id> --reason <why> [--days 1-90] [--false-positive]",
3546
+ status: "ready",
3547
+ maxPositionals: 1,
3548
+ options: { reason: { type: "string" }, days: { type: "string" }, "false-positive": { type: "boolean" } },
3549
+ run: (ctx, args) => {
3550
+ const id = args.positionals[0] ?? "";
3551
+ const reason = stringOption(args.values, "reason");
3552
+ const days = daysOf(args.values);
3553
+ const problem2 = inputProblem(ctx, id, reason, days);
3554
+ if (problem2 !== void 0 || reason === void 0 || days === void 0) {
3555
+ return Promise.resolve(
3556
+ failure(
3557
+ ctx,
3558
+ problem2 ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
3559
+ )
3560
+ );
3561
+ }
3562
+ return Promise.resolve(
3563
+ acceptEntry(ctx, { id, reason, days, falsePositive: args.values["false-positive"] === true })
3564
+ );
3565
+ }
3566
+ };
3567
+
3568
+ // src/commands/secrets-incident.ts
3569
+ var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
3570
+ var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
3571
+ var TARGET = { rotated: "rotacionado", close: "fechado" };
3572
+ var PURGE_STEPS = [
3573
+ "secrets.purge.step1",
3574
+ "secrets.purge.step2",
3575
+ "secrets.purge.step3",
3576
+ "secrets.purge.step4",
3577
+ "secrets.purge.step5"
3578
+ ];
3579
+ var fail = (ctx, problem2) => failureFrom(ctx.t, problem2);
3580
+ var notFound2 = (ctx, id) => fail(
3581
+ ctx,
3582
+ usageProblem("secrets_incident_not_found", "secrets.error.incident_not_found", `finitemonkey secrets incident`, {
3583
+ id
3584
+ })
3585
+ );
3586
+ var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
3587
+ id: incident.id,
3588
+ state: incident.state,
3589
+ rule: incident.rule,
3590
+ where: incident.where,
3591
+ created: incident.createdAt.slice(0, 10)
3592
+ });
3593
+ var list = (ctx, root) => {
3594
+ const incidents = listIncidents(ctx.runtime, root);
3595
+ const human = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [ctx.t("secrets.incident.title", { count: incidents.length }), ...incidents.map((item) => itemLine(ctx, item))];
3596
+ return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human);
3597
+ };
3598
+ var notRotated = (ctx, incident) => fail(ctx, {
3599
+ code: "secrets_not_rotated",
3600
+ messageKey: "secrets.error.not_rotated",
3601
+ params: { id: incident.id, state: incident.state },
3602
+ fix: `finitemonkey secrets incident rotated ${incident.id}`,
3603
+ exitCode: ExitCode.NeedsPerson
3604
+ });
3605
+ var move = (ctx, root, incident, action) => {
3606
+ const to = TARGET[action];
3607
+ if (!isPerson(ctx)) {
3608
+ return fail(
3609
+ ctx,
3610
+ needsPersonProblem("secrets.error.person_only", `finitemonkey secrets incident ${action} ${incident.id}`, {
3611
+ action: ctx.t(`secrets.action.${action}`)
3612
+ })
3613
+ );
3614
+ }
3615
+ if (!canMove(incident.state, to))
3616
+ return action === "close" ? notRotated(ctx, incident) : fail(
3617
+ ctx,
3618
+ usageProblem("invalid_arguments", "secrets.error.bad_transition", USAGE, {
3619
+ id: incident.id,
3620
+ from: incident.state,
3621
+ to
3622
+ })
3623
+ );
3624
+ const moved = moveIncident(incident, to, ctx.runtime.now(), personName(ctx));
3625
+ const saved = saveIncident(ctx.runtime, root, moved);
3626
+ if (!saved.ok) {
3627
+ return fail(ctx, writeFailed(incident.id, saved.error));
3628
+ }
3629
+ return success(moved, [ctx.t("secrets.incident.marked", { id: moved.id, state: moved.state })]);
3630
+ };
3631
+ var purge = (ctx, incident) => {
3632
+ if (!isRotated(incident)) return notRotated(ctx, incident);
3633
+ const params = { id: incident.id, where: incident.where, commit: incident.firstCommit ?? "?" };
3634
+ const steps = PURGE_STEPS.map((key) => ctx.t(key, params));
3635
+ return success({ id: incident.id, state: incident.state, steps }, [ctx.t("secrets.purge.title", params), ...steps]);
3636
+ };
3637
+ var actionOf = (raw) => INCIDENT_ACTIONS.find((action) => action === (raw ?? "list"));
3638
+ var secretsIncidentCommand = {
3639
+ path: ["secrets", "incident"],
3640
+ summaryKey: "secrets.command.incident",
3641
+ usage: USAGE,
3642
+ status: "ready",
3643
+ maxPositionals: 2,
3644
+ run: (ctx, args) => {
3645
+ const action = actionOf(args.positionals[0]);
3646
+ const root = projectRoot(ctx);
3647
+ if (action === void 0)
3648
+ return Promise.resolve(
3649
+ fail(
3650
+ ctx,
3651
+ usageProblem("invalid_arguments", "error.invalid_arguments", USAGE, { detail: args.positionals[0] ?? "" })
3652
+ )
3653
+ );
3654
+ if (action === "list") return Promise.resolve(list(ctx, root));
3655
+ const id = args.positionals[1] ?? "";
3656
+ const incident = readIncident(ctx.runtime, root, id);
3657
+ if (!incident.ok) return Promise.resolve(notFound2(ctx, id));
3658
+ return Promise.resolve(action === "purge" ? purge(ctx, incident.value) : move(ctx, root, incident.value, action));
3659
+ }
3660
+ };
3661
+
3662
+ // src/mask/mask.ts
3663
+ var maskToken = (finding) => `«secret:${finding.ruleId}:${finding.tail}»`;
3664
+ var maskText = (text2) => {
3665
+ const findings = scanText(text2, { rules: CREDENTIAL_RULES });
3666
+ if (findings.length === 0) return text2;
3667
+ let out = "";
3668
+ let cursor = 0;
3669
+ for (const finding of findings) {
3670
+ out += text2.slice(cursor, finding.start) + maskToken(finding);
3671
+ cursor = finding.end;
3672
+ }
3673
+ return out + text2.slice(cursor);
3674
+ };
3675
+ var maskDeep = (value) => {
3676
+ if (typeof value === "string") return maskText(value);
3677
+ if (Array.isArray(value)) return value.map(maskDeep);
3678
+ if (typeof value === "object" && value !== null) {
3679
+ return Object.fromEntries(Object.entries(value).map(([key, inner]) => [maskText(key), maskDeep(inner)]));
3680
+ }
3681
+ return value;
3682
+ };
3683
+
3684
+ // src/commands/secrets-scan.ts
3685
+ var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
3686
+ var view = (finding, status) => ({
3687
+ id: finding.id,
3688
+ rule: finding.ruleId,
3689
+ severity: finding.severity,
3690
+ file: finding.file,
3691
+ line: finding.line,
3692
+ column: finding.column,
3693
+ fingerprint: finding.fingerprint,
3694
+ mask: maskToken(finding),
3695
+ status,
3696
+ ...finding.commit === void 0 ? {} : { commit: finding.commit }
3697
+ });
3698
+ var modeOf2 = (values) => {
3699
+ const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
3700
+ if (chosen.filter(Boolean).length > 1) return void 0;
3701
+ if (chosen[0] === true) return "staged";
3702
+ return chosen[1] === true ? "history" : "path";
3703
+ };
3704
+ var normalizedTarget = (target) => target.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
3705
+ var scopeOf = (target) => {
3706
+ const prefix = normalizedTarget(target);
3707
+ return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
3708
+ };
3709
+ var runScan = async (ctx, root, mode, target) => {
3710
+ if (mode === "staged") return scanStaged(ctx.runtime, root);
3711
+ if (mode === "history") return scanHistory(ctx.runtime, root);
3712
+ return scanPath(ctx.runtime, root, normalizedTarget(target) || ".");
3713
+ };
3714
+ var historyIncidents = (ctx, root, fresh2) => fresh2.flatMap((finding) => {
3715
+ if (ruleById(finding.ruleId)?.kind !== "credential") return [];
3716
+ const draft = {
3717
+ rule: finding.ruleId,
3718
+ fingerprint: finding.fingerprint,
3719
+ where: whereOf(finding),
3720
+ source: "history",
3721
+ state: "aberto",
3722
+ ...finding.commit === void 0 ? {} : { firstCommit: finding.commit }
3723
+ };
3724
+ const saved = recordIncident(ctx.runtime, root, draft, HOOK_ACTOR);
3725
+ return saved.ok ? [saved.value.id] : [];
3726
+ });
3727
+ var dataOf = (mode, outcome, incidents) => {
3728
+ const findings = [
3729
+ ...outcome.fresh.map((finding) => view(finding, "new")),
3730
+ ...outcome.known.map((finding) => view(finding, "known")),
3731
+ ...outcome.expired.map((finding) => view(finding, "expired"))
3732
+ ];
3733
+ return {
3734
+ schema: SECRETS_SCAN_SCHEMA,
3735
+ mode,
3736
+ baseline: BASELINE_FILE,
3737
+ findings,
3738
+ fixed: outcome.fixed.map((entry) => entry.id),
3739
+ incidents,
3740
+ summary: {
3741
+ total: findings.length,
3742
+ new: outcome.fresh.length,
3743
+ known: outcome.known.length,
3744
+ expired: outcome.expired.length,
3745
+ fixed: outcome.fixed.length
3746
+ }
3747
+ };
3748
+ };
3749
+ var humanOf = (ctx, data) => [
3750
+ ctx.t("secrets.scan.title", { mode: data.mode, ...data.summary }),
3751
+ ...data.findings.map(
3752
+ (item) => ctx.t("secrets.scan.item", {
3753
+ status: item.status,
3754
+ rule: item.rule,
3755
+ where: `${item.file}:${String(item.line)}`,
3756
+ id: item.id,
3757
+ mask: item.mask
3758
+ })
3759
+ ),
3760
+ ...data.summary.total === 0 ? [ctx.t("secrets.scan.clean")] : []
3761
+ ];
3762
+ var resultOf = (ctx, data) => {
3763
+ const human = humanOf(ctx, data);
3764
+ const failing = data.summary.new + data.summary.expired;
3765
+ if (failing === 0) return success(data, human);
3766
+ return failureFrom(
3767
+ ctx.t,
3768
+ {
3769
+ code: "secrets_found",
3770
+ messageKey: "secrets.error.found",
3771
+ params: { count: failing },
3772
+ fix: "finitemonkey secrets accept <id> --reason <why>",
3773
+ exitCode: ExitCode.CheckFailed,
3774
+ data
3775
+ },
3776
+ human
3777
+ );
3778
+ };
3779
+ var prune = (ctx, root, baseline, outcome) => {
3780
+ if (outcome.fixed.length > 0) saveBaseline(ctx.runtime, root, withoutFixed(baseline, outcome.fixed));
3781
+ };
3782
+ var secretsScanCommand = {
3783
+ path: ["secrets", "scan"],
3784
+ summaryKey: "secrets.command.scan",
3785
+ usage: "finitemonkey secrets scan [--staged | --history | --path <dir>] [--json]",
3786
+ status: "ready",
3787
+ options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
3788
+ run: async (ctx, args) => {
3789
+ const mode = modeOf2(args.values);
3790
+ const fix = "finitemonkey help secrets scan";
3791
+ if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
3792
+ const root = projectRoot(ctx);
3793
+ const target = stringOption(args.values, "path") ?? ".";
3794
+ const scanned = await runScan(ctx, root, mode, target);
3795
+ if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
3796
+ const baseline = loadBaseline(ctx.runtime, root);
3797
+ if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
3798
+ const outcome = ratchet(
3799
+ scanned.value,
3800
+ baseline.value,
3801
+ ctx.runtime.now(),
3802
+ mode === "path" ? scopeOf(target) : void 0
3803
+ );
3804
+ if (mode === "path") prune(ctx, root, baseline.value, outcome);
3805
+ const incidents = mode === "history" ? historyIncidents(ctx, root, outcome.fresh) : [];
3806
+ return resultOf(ctx, dataOf(mode, outcome, incidents));
3807
+ }
3808
+ };
3809
+
3810
+ // src/install/uninstall.ts
3811
+ var structural = (entry, current) => {
3812
+ if (entry.kind === "gitignore") {
3813
+ const text2 = withoutBlock(current);
3814
+ return text2 === "" && !entry.existed ? void 0 : text2;
3815
+ }
3816
+ const parsed = parseSettings(current);
3817
+ if (!parsed.ok || entry.added === void 0) return current;
3818
+ const settings = unmergeSettings(parsed.value, entry.added, entry.handlers ?? []);
3819
+ return Object.keys(settings).length === 0 && !entry.existed ? void 0 : renderJson(settings, styleOf(current));
3820
+ };
3821
+ var unchanged = (project, entry) => {
3822
+ if (!entry.existed) return { path: entry.path, action: "delete", after: void 0 };
3823
+ const original = entry.backup === void 0 ? void 0 : project.read(entry.backup);
3824
+ return original === void 0 ? void 0 : { path: entry.path, action: "restore", after: original };
3825
+ };
3826
+ var removalFor = (project, entry, options) => {
3827
+ const current = project.read(entry.path);
3828
+ if (current === void 0) return { path: entry.path, action: "gone", after: void 0 };
3829
+ if (entry.kind === "generated" && options.keepGenerated) {
3830
+ return { path: entry.path, action: "keep-generated", after: current };
3831
+ }
3832
+ if (hashText(current) === entry.hash) {
3833
+ const exact2 = unchanged(project, entry);
3834
+ if (exact2 !== void 0) return exact2;
3835
+ }
3836
+ if (entry.kind === "generated") return { path: entry.path, action: "keep-edited", after: current };
3837
+ const after = structural(entry, current);
3838
+ return { path: entry.path, action: after === void 0 ? "delete" : "unmerge", after };
3839
+ };
3840
+ var planRemoval = (project, lock, options) => ({
3841
+ items: lock.entries.map((entry) => removalFor(project, entry, options)),
3842
+ purge: options.purge,
3843
+ removeDirs: lock.createdDirs.filter((dir) => dir !== STATE_DIR)
3844
+ });
3845
+ var applyItem = (project, item) => {
3846
+ if (item.action === "delete") {
3847
+ const removed = project.runtime.remove(project.abs(item.path));
3848
+ return removed.ok ? removed : reject({ path: item.path, detail: removed.error });
3849
+ }
3850
+ if (item.after === void 0 || item.action === "gone" || item.action.startsWith("keep")) return accept(void 0);
3851
+ return writeText(project, item.path, item.after);
3852
+ };
3853
+ var isEmptyDir = (project, relative4) => {
3854
+ const listed2 = project.runtime.listDir(project.abs(relative4));
3855
+ return listed2.ok && listed2.value.length === 0;
3856
+ };
3857
+ var finish = (project, plan) => {
3858
+ const target = plan.purge ? STATE_DIR : LOCK_FILE;
3859
+ const removed = project.runtime.remove(project.abs(target));
3860
+ if (!removed.ok) return reject({ path: target, detail: removed.error });
3861
+ for (const dir of plan.removeDirs) if (isEmptyDir(project, dir)) project.runtime.remove(project.abs(dir));
3862
+ return accept(void 0);
3863
+ };
3864
+ var applyRemoval = (project, plan) => {
3865
+ const changed = withInstallLock(project, () => {
3866
+ for (const item of plan.items) {
3867
+ const done = applyItem(project, item);
3868
+ if (!done.ok) return done;
3869
+ }
3870
+ return accept(void 0);
3871
+ });
3872
+ return changed.ok ? finish(project, plan) : changed;
3873
+ };
3874
+
3875
+ // src/commands/uninstall.ts
3876
+ var UNINSTALL_SCHEMA = "finitemonkey/uninstall-plan@1";
3877
+ var OUTSIDE = ["claude plugin uninstall finitemonkey@finitemonkey"];
3878
+ var dataOf2 = (root, plan, applied) => ({
3879
+ schema: UNINSTALL_SCHEMA,
3880
+ applied,
3881
+ purge: plan.purge,
3882
+ root,
3883
+ actions: plan.items.map(({ path, action }) => ({ path, action })),
3884
+ kept: plan.items.filter((item) => item.action.startsWith("keep")).map(({ path }) => path),
3885
+ outside: OUTSIDE
3886
+ });
3887
+ var linesOf = (ctx, data) => [
3888
+ ctx.t("install.uninstall.title", { root: data.root }),
3889
+ ...data.actions.map(({ path, action }) => ` ${action.padEnd(15)}${path}`),
3890
+ ...data.purge ? [ctx.t("install.uninstall.purge")] : [ctx.t("install.uninstall.keeps_state")],
3891
+ ...data.kept.map((path) => ctx.t("install.warn.edited_kept", { path })),
3892
+ ctx.t("install.uninstall.outside", { command: OUTSIDE.join(", ") })
3893
+ ];
3894
+ var fixFor = (args) => [
3895
+ "finitemonkey uninstall --yes",
3896
+ ...args.values["purge"] === true ? ["--purge"] : [],
3897
+ ...args.values["keep-generated"] === true ? ["--keep-generated"] : []
3898
+ ].join(" ");
3899
+ var runUninstall = (ctx, args) => {
3900
+ const { project } = openProject(ctx);
3901
+ const lock = readPreviousLock(project);
3902
+ if (!lock.ok) return failureFrom(ctx.t, planProblem(lock.error));
3903
+ if (lock.value === void 0) return failureFrom(ctx.t, notInstalled());
3904
+ const options = { purge: args.values["purge"] === true, keepGenerated: args.values["keep-generated"] === true };
3905
+ const plan = planRemoval(project, lock.value, options);
3906
+ const preview = dataOf2(project.root, plan, false);
3907
+ if (ctx.dryRun) return success(preview, [...linesOf(ctx, preview), ctx.t("install.dry_run")]);
3908
+ if (!ctx.yes) {
3909
+ const next = fixFor(args);
3910
+ const problem2 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next);
3911
+ return { ...failureFrom(ctx.t, { ...problem2, data: preview }, linesOf(ctx, preview)), next };
3912
+ }
3913
+ const removed = applyRemoval(project, plan);
3914
+ if (!removed.ok) return failureFrom(ctx.t, applyProblem(removed.error));
3915
+ const data = dataOf2(project.root, plan, true);
3916
+ return success(data, [...linesOf(ctx, data), ctx.t("install.uninstall.done")]);
3917
+ };
3918
+ var uninstallCommand = {
3919
+ path: ["uninstall"],
3920
+ summaryKey: "command.uninstall.summary",
3921
+ usage: "finitemonkey uninstall [--purge] [--keep-generated] [--dry-run] [--yes]",
3922
+ status: "ready",
3923
+ options: { purge: { type: "boolean" }, "keep-generated": { type: "boolean" } },
3924
+ run: (ctx, args) => Promise.resolve(runUninstall(ctx, args))
3925
+ };
3926
+
3927
+ // src/commands/update.ts
3928
+ var check = (ctx, bin) => {
3929
+ const { project, gitRoot } = openProject(ctx);
3930
+ const previous = readPreviousLock(project);
3931
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3932
+ if (previous.value === void 0) return failureFrom(ctx.t, notInstalled());
3933
+ const input = planInputFor(ctx, previous.value.preset, bin, false);
3934
+ const plan = buildPlan(project, detect(project, gitRoot, input), input, previous.value);
3935
+ if (!plan.ok) return failureFrom(ctx.t, planProblem(plan.error));
3936
+ const data = planData(ctx.t, plan.value, "update", false, void 0);
3937
+ const human = planLines(ctx.t, data, true);
3938
+ if (!hasChanges(plan.value)) return success(data, [...human, ctx.t("install.up_to_date")]);
3939
+ return {
3940
+ ok: false,
3941
+ exitCode: ExitCode.CheckFailed,
3942
+ data,
3943
+ human,
3944
+ error: { code: "install_outdated", message: ctx.t("error.install_outdated"), fix: "finitemonkey update --yes" }
3945
+ };
3946
+ };
3947
+ var runUpdate = (ctx, args) => {
3948
+ const bin = stringOption(args.values, "bin");
3949
+ if (args.values["check"] === true) return check(ctx, bin);
3950
+ const { project } = openProject(ctx);
3951
+ const previous = readPreviousLock(project);
3952
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3953
+ if (previous.value === void 0) return failureFrom(ctx.t, notInstalled());
3954
+ const preset = stringOption(args.values, "preset");
3955
+ const request = { mode: "update", preset, bin, marketplace: false, showDiff: true };
3956
+ return runInstall(ctx, request, previous.value.preset);
3957
+ };
3958
+ var updateCommand = {
3959
+ path: ["update"],
3960
+ summaryKey: "command.update.summary",
3961
+ usage: "finitemonkey update [--check] [--preset neutral|gabriel] [--bin <path>] [--yes]",
3962
+ status: "ready",
3963
+ options: { check: { type: "boolean" }, preset: { type: "string" }, bin: { type: "string" } },
3964
+ run: (ctx, args) => Promise.resolve(runUpdate(ctx, args))
3965
+ };
3966
+
1107
3967
  // src/commands/version.ts
1108
3968
  var versionCommand = {
1109
3969
  path: ["version"],
@@ -1124,6 +3984,9 @@ var REGISTRY = [
1124
3984
  uninstallCommand,
1125
3985
  i18nCheckCommand,
1126
3986
  hookCommand,
3987
+ secretsScanCommand,
3988
+ secretsAcceptCommand,
3989
+ secretsIncidentCommand,
1127
3990
  versionCommand,
1128
3991
  helpCommand
1129
3992
  ];
@@ -1163,9 +4026,9 @@ var unknownCommand = (registry, attempted) => {
1163
4026
  suggestion
1164
4027
  });
1165
4028
  };
1166
- var failWith = (ctx, name, problem) => ({
4029
+ var failWith = (ctx, name, problem2) => ({
1167
4030
  name,
1168
- execute: () => Promise.resolve(failureFrom(ctx.t, problem))
4031
+ execute: () => Promise.resolve(failureFrom(ctx.t, problem2))
1169
4032
  });
1170
4033
  var invoke = (ctx, argv, spec, registry) => {
1171
4034
  const name = commandName(spec);
@@ -1205,23 +4068,26 @@ var resultFromThrown = (thrown, t) => {
1205
4068
  };
1206
4069
  var block = (items) => items.length > 0 ? `${items.join("\n")}
1207
4070
  ` : "";
4071
+ var write = (sink, text2) => {
4072
+ if (text2 !== "") sink.write(maskText(text2));
4073
+ };
1208
4074
  var render = (ctx, command, result) => {
1209
4075
  if (result.raw !== void 0) {
1210
- ctx.io.stdout.write(result.raw.stdout);
1211
- ctx.io.stderr.write(result.raw.stderr);
4076
+ write(ctx.io.stdout, result.raw.stdout);
4077
+ write(ctx.io.stderr, result.raw.stderr);
1212
4078
  return;
1213
4079
  }
1214
4080
  if (ctx.json) {
1215
- ctx.io.stdout.write(`${JSON.stringify(toEnvelope(command, result))}
4081
+ ctx.io.stdout.write(`${JSON.stringify(maskDeep(toEnvelope(command, result)))}
1216
4082
  `);
1217
4083
  return;
1218
4084
  }
1219
- ctx.io.stdout.write(block(result.human ?? []));
4085
+ write(ctx.io.stdout, block(result.human ?? []));
1220
4086
  if (!result.ok) {
1221
4087
  const { message, fix } = result.error;
1222
- ctx.io.stderr.write(block([ctx.t("output.error", { message }), ctx.t("output.fix", { fix })]));
4088
+ write(ctx.io.stderr, block([ctx.t("output.error", { message }), ctx.t("output.fix", { fix })]));
1223
4089
  }
1224
- if (result.next !== void 0) ctx.io.stdout.write(block([ctx.t("output.next", { next: result.next })]));
4090
+ if (result.next !== void 0) write(ctx.io.stdout, block([ctx.t("output.next", { next: result.next })]));
1225
4091
  };
1226
4092
 
1227
4093
  // src/core/run.ts
@@ -1247,11 +4113,11 @@ var run = async (options) => {
1247
4113
  let result;
1248
4114
  try {
1249
4115
  if (loose.lang !== void 0 && normalizeLang(loose.lang) === void 0) {
1250
- const problem = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
4116
+ const problem2 = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
1251
4117
  lang: loose.lang,
1252
4118
  languages: LANGUAGES.join(", ")
1253
4119
  });
1254
- result = failureFrom(t, problem);
4120
+ result = failureFrom(t, problem2);
1255
4121
  } else {
1256
4122
  const dispatched = dispatch(ctx, options.argv, loose, options.registry ?? REGISTRY);
1257
4123
  name = dispatched.name;
@@ -1265,15 +4131,23 @@ var run = async (options) => {
1265
4131
  };
1266
4132
 
1267
4133
  // src/core/version.ts
1268
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4134
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1269
4135
  var UNKNOWN_VERSION = "0.0.0-unknown";
1270
- var readPackageVersion = (readText, moduleUrl) => {
1271
- const text = readText(toAbsolutePath(fileURLToPath2(new URL("../package.json", moduleUrl))));
1272
- if (!text.ok) return UNKNOWN_VERSION;
1273
- const parsed = parseJson(text.value);
1274
- if (!parsed.ok || !isRecord(parsed.value)) return UNKNOWN_VERSION;
4136
+ var VERSION_SOURCES = ["../package.json", "../.claude-plugin/plugin.json"];
4137
+ var versionAt = (readText, moduleUrl, relative4) => {
4138
+ const text2 = readText(toAbsolutePath(fileURLToPath3(new URL(relative4, moduleUrl))));
4139
+ if (!text2.ok) return void 0;
4140
+ const parsed = parseJson(text2.value);
4141
+ if (!parsed.ok || !isRecord(parsed.value)) return void 0;
1275
4142
  const version = parsed.value["version"];
1276
- return typeof version === "string" ? version : UNKNOWN_VERSION;
4143
+ return typeof version === "string" ? version : void 0;
4144
+ };
4145
+ var readPackageVersion = (readText, moduleUrl) => {
4146
+ for (const relative4 of VERSION_SOURCES) {
4147
+ const version = versionAt(readText, moduleUrl, relative4);
4148
+ if (version !== void 0) return version;
4149
+ }
4150
+ return UNKNOWN_VERSION;
1277
4151
  };
1278
4152
 
1279
4153
  // src/cli.ts