veefore 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/veefore.mjs +650 -147
  2. package/package.json +1 -1
package/dist/veefore.mjs CHANGED
@@ -1030,6 +1030,97 @@ async function select(question, real, settings = {}) {
1030
1030
  draw();
1031
1031
  });
1032
1032
  }
1033
+ async function selectMany(question, options, settings = {}) {
1034
+ if (options.length === 0) return [];
1035
+ const ticked = options.map((option) => settings.selected?.(option) ?? true);
1036
+ if (!isInteractive()) return options.filter((_, index) => ticked[index]).map((o) => o.value);
1037
+ flushCapture();
1038
+ let cursor = 0;
1039
+ let rendered = 0;
1040
+ const stdin = process.stdin;
1041
+ const stdout = process.stdout;
1042
+ const labelWidth = Math.max(...options.map((option) => [...option.label].length));
1043
+ const draw = () => {
1044
+ const width = columns();
1045
+ const count = ticked.filter(Boolean).length;
1046
+ const lines = [];
1047
+ lines.push(
1048
+ `${brand("?")} ${style.bold(question)} ${style.grey(`(${count}/${options.length})`)}`
1049
+ );
1050
+ if (settings.help) lines.push(` ${style.grey(settings.help)}`);
1051
+ const room = Math.max(3, (process.stdout.rows || 24) - lines.length - 3);
1052
+ const window = Math.min(options.length, room);
1053
+ let first = Math.max(0, Math.min(cursor - (window >> 1), options.length - window));
1054
+ if (window === options.length) first = 0;
1055
+ if (first > 0) lines.push(style.grey(` \u2191 ${first} more`));
1056
+ for (let index = first; index < first + window; index += 1) {
1057
+ const option = options[index];
1058
+ const active = index === cursor;
1059
+ const pointer = active ? brand("\u276F") : " ";
1060
+ const box = ticked[index] ? style.green("\u25C9") : style.grey("\u25EF");
1061
+ const text = option.hint ? option.label.padEnd(labelWidth) : option.label;
1062
+ const hint = option.hint ? ` ${style.grey(option.hint)}` : "";
1063
+ const label = active ? brand(text) : ticked[index] ? text : style.grey(text);
1064
+ lines.push(fit(`${pointer} ${box} ${label}${hint}`, width - 1));
1065
+ }
1066
+ const rest = options.length - (first + window);
1067
+ if (rest > 0) lines.push(style.grey(` \u2193 ${rest} more`));
1068
+ lines.push(style.grey(" \u2191\u2193 move \xB7 space toggle \xB7 a all/none \xB7 enter confirm \xB7 esc cancel"));
1069
+ if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
1070
+ stdout.write(`${lines.join("\n")}
1071
+ `);
1072
+ rendered = lines.length;
1073
+ };
1074
+ return new Promise((resolve, reject) => {
1075
+ const wasRaw = stdin.isRaw;
1076
+ const restoreCursor = () => {
1077
+ if (!inApp()) stdout.write("\x1B[?25h");
1078
+ };
1079
+ const onResize = () => draw();
1080
+ const finish = (confirmed) => {
1081
+ stdin.off("keypress", onKey);
1082
+ stdin.setRawMode(wasRaw);
1083
+ stdin.pause();
1084
+ process.stdout.off("resize", onResize);
1085
+ process.off("exit", restoreCursor);
1086
+ if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
1087
+ restoreCursor();
1088
+ if (!confirmed) {
1089
+ stdout.write(`${style.grey("\u2717")} ${question} ${style.grey("cancelled")}
1090
+ `);
1091
+ reject(new MenuCancelled());
1092
+ return;
1093
+ }
1094
+ const chosen = options.filter((_, index) => ticked[index]);
1095
+ const summary = chosen.length === options.length ? `all ${options.length}` : chosen.length === 0 ? "none" : `${chosen.length} of ${options.length}`;
1096
+ stdout.write(`${style.green("\u2713")} ${question} ${brand(summary)}
1097
+ `);
1098
+ resolve(chosen.map((option) => option.value));
1099
+ };
1100
+ const onKey = (input, key = {}) => {
1101
+ if (key.ctrl && key.name === "c" || key.name === "escape") return finish(false);
1102
+ if (key.name === "return" || key.name === "enter") return finish(true);
1103
+ if (key.name === "space") ticked[cursor] = !ticked[cursor];
1104
+ else if (input === "a" || input === "A") {
1105
+ const all = ticked.every(Boolean);
1106
+ ticked.fill(!all);
1107
+ } else if (key.name === "up" || key.name === "k") {
1108
+ cursor = (cursor - 1 + options.length) % options.length;
1109
+ } else if (key.name === "down" || key.name === "j" || key.name === "tab") {
1110
+ cursor = (cursor + 1) % options.length;
1111
+ } else return;
1112
+ draw();
1113
+ };
1114
+ process.once("exit", restoreCursor);
1115
+ emitKeypressEvents(stdin);
1116
+ stdin.setRawMode(true);
1117
+ stdin.resume();
1118
+ stdout.write("\x1B[?25l");
1119
+ stdin.on("keypress", onKey);
1120
+ process.stdout.on("resize", onResize);
1121
+ draw();
1122
+ });
1123
+ }
1033
1124
  async function selectYesNo(question, defaultYes = false) {
1034
1125
  return select(
1035
1126
  question,
@@ -4258,6 +4349,9 @@ function findDispatchGuards(sources) {
4258
4349
  const dynamic = /(?<![\w$.])[A-Za-z_$]*[gG]uard\(\s*(?!['"`])[A-Za-z_$][\w$.]*(?:\[[^\]\n]{1,40}\])?\s*[,)]/;
4259
4350
  return sources.filter(({ text }) => imports.test(text) && dynamic.test(stripDefinitions(text))).map(({ file }) => file);
4260
4351
  }
4352
+ function escapeRegex(value) {
4353
+ return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
4354
+ }
4261
4355
  function stripDefinitions(text) {
4262
4356
  return text.replace(/\b(?:function|def)\s+[A-Za-z_$]*[gG]uard\s*\(/g, "");
4263
4357
  }
@@ -4267,6 +4361,11 @@ function toCamel(key) {
4267
4361
  function findGuardedKeys(sources) {
4268
4362
  const found = /* @__PURE__ */ new Map();
4269
4363
  const pattern = /(?:\b|@)(?:[A-Za-z_$]*[gG]uard|[A-Za-z_$]*[cC]heckpoint)\(\s*['"]([a-z][a-z0-9_]*)['"]/g;
4364
+ for (const { file, text } of sources) {
4365
+ for (const [key, where] of dynamicCheckpointKeys(text, file)) {
4366
+ if (!found.has(key)) found.set(key, where);
4367
+ }
4368
+ }
4270
4369
  for (const { file, text } of sources) {
4271
4370
  for (const match of text.matchAll(pattern)) {
4272
4371
  const key = match[1];
@@ -4275,6 +4374,24 @@ function findGuardedKeys(sources) {
4275
4374
  }
4276
4375
  return found;
4277
4376
  }
4377
+ function dynamicCheckpointKeys(text, file) {
4378
+ const found = [];
4379
+ const calls = /(?<![\w$.])[A-Za-z_$]*[cC]heckpoint\(\s*([A-Za-z_$][\w$]*(?:\s*(?:\?\.|\.)\s*[A-Za-z_$][\w$]*)*)\s*[,)]/g;
4380
+ for (const call of text.matchAll(calls)) {
4381
+ const expression = (call[1] ?? "").replace(/\s+/g, "");
4382
+ if (expression === "") continue;
4383
+ const head = text.slice(Math.max(0, (call.index ?? 0) - 600), call.index ?? 0);
4384
+ const [object, ...rest] = expression.split(".");
4385
+ if (object === void 0) continue;
4386
+ const access = rest.map((part) => `\\s*(?:\\?\\.|\\.)\\s*${escapeRegex(part)}`).join("");
4387
+ const pattern = new RegExp(
4388
+ `(?<![\\w$.])${escapeRegex(object)}${access}\\s*={2,3}\\s*['"\`]([a-z][a-z0-9_]*)['"\`]`,
4389
+ "g"
4390
+ );
4391
+ for (const match of head.matchAll(pattern)) found.push([match[1], file]);
4392
+ }
4393
+ return found;
4394
+ }
4278
4395
  var PROVIDER_ENV = {
4279
4396
  anthropic: "ANTHROPIC_API_KEY",
4280
4397
  openai: "OPENAI_API_KEY"
@@ -5006,7 +5123,20 @@ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
5006
5123
  var SKIP_FILES = /\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$|\.min\.[cm]?js$/;
5007
5124
  var DEFAULT_BUDGET_MS = 6e4;
5008
5125
  var MAX_PARSE_BYTES = 4 * 1024 * 1024;
5126
+ var cache = /* @__PURE__ */ new Map();
5127
+ function forgetIndex(root) {
5128
+ if (root === void 0) cache.clear();
5129
+ else cache.delete(path7.resolve(root));
5130
+ }
5009
5131
  function indexFiles(root, options = {}) {
5132
+ const key = path7.resolve(root);
5133
+ const hit = cache.get(key);
5134
+ if (hit) return hit;
5135
+ const built = buildIndex(root, options);
5136
+ if (!built.truncated) cache.set(key, built);
5137
+ return built;
5138
+ }
5139
+ function buildIndex(root, options) {
5010
5140
  const deadline2 = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS);
5011
5141
  const fromGit = gitFiles(root, deadline2);
5012
5142
  if (fromGit) {
@@ -5266,8 +5396,8 @@ function listSources(root = process.cwd()) {
5266
5396
  const index = indexFiles(root);
5267
5397
  return { files: index.files, truncated: index.truncated, method: index.method };
5268
5398
  }
5269
- function discoverAgents(root = process.cwd()) {
5270
- const scan = scanProject(root);
5399
+ function discoverAgents(root = process.cwd(), reuse) {
5400
+ const scan = reuse ?? scanProject(root);
5271
5401
  const index = indexFiles(root);
5272
5402
  const byDirectory = /* @__PURE__ */ new Map();
5273
5403
  for (const tool of scan.tools) {
@@ -5391,7 +5521,7 @@ function combineRisk(inferred, declared) {
5391
5521
  function findJsTools(text) {
5392
5522
  const hits = [];
5393
5523
  const importsFrom = (pkg) => new RegExp(
5394
- `from\\s+['"]${escapeRegex(pkg)}(/[^'"]*)?['"]|require\\(\\s*['"]${escapeRegex(pkg)}`
5524
+ `from\\s+['"]${escapeRegex2(pkg)}(/[^'"]*)?['"]|require\\(\\s*['"]${escapeRegex2(pkg)}`
5395
5525
  ).test(text);
5396
5526
  const vercel = importsFrom("ai");
5397
5527
  const langchain = importsFrom("@langchain/core") || importsFrom("langchain");
@@ -5924,7 +6054,7 @@ function push(map, key, value) {
5924
6054
  function unique(values) {
5925
6055
  return [...new Set(values)];
5926
6056
  }
5927
- function escapeRegex(value) {
6057
+ function escapeRegex2(value) {
5928
6058
  return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
5929
6059
  }
5930
6060
 
@@ -6528,6 +6658,185 @@ function guardDispatch(source, fn, typescript) {
6528
6658
  return { after, at: target.start };
6529
6659
  }
6530
6660
  var CHECKPOINT_ARG_PROPERTIES = ["args", "arguments", "params", "parameters", "input", "payload"];
6661
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
6662
+ "if",
6663
+ "else",
6664
+ "for",
6665
+ "while",
6666
+ "do",
6667
+ "switch",
6668
+ "catch",
6669
+ "with",
6670
+ "return",
6671
+ "typeof",
6672
+ "await",
6673
+ "new",
6674
+ "throw",
6675
+ "case",
6676
+ "function",
6677
+ "delete",
6678
+ "void",
6679
+ "in",
6680
+ "of",
6681
+ "yield"
6682
+ ]);
6683
+ var BRANCH_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while"]);
6684
+ function readStructure(source) {
6685
+ const parens = [];
6686
+ const blocks = [];
6687
+ const parenStack = [];
6688
+ const braceStack = [];
6689
+ const closedAt = /* @__PURE__ */ new Map();
6690
+ let awaitingBody = null;
6691
+ let previous = "";
6692
+ let index = 0;
6693
+ while (index < source.length) {
6694
+ const char = source[index];
6695
+ const next = source[index + 1];
6696
+ if (char === "/" && next === "/") {
6697
+ const stop = source.indexOf("\n", index);
6698
+ index = stop < 0 ? source.length : stop;
6699
+ continue;
6700
+ }
6701
+ if (char === "/" && next === "*") {
6702
+ const stop = source.indexOf("*/", index + 2);
6703
+ index = stop < 0 ? source.length : stop + 2;
6704
+ continue;
6705
+ }
6706
+ if (char === '"' || char === "'") {
6707
+ const stop = skipString(source, index);
6708
+ index = stop < 0 ? nextLine(source, index) : stop;
6709
+ previous = "a";
6710
+ continue;
6711
+ }
6712
+ if (char === "`") {
6713
+ const stop = skipTemplate(source, index);
6714
+ index = stop < 0 ? nextLine(source, index) : stop;
6715
+ previous = "a";
6716
+ continue;
6717
+ }
6718
+ if (char === "/" && regexAllowed(source, index, previous)) {
6719
+ const stop = skipRegex(source, index);
6720
+ index = stop < 0 ? nextLine(source, index) : stop;
6721
+ previous = "a";
6722
+ continue;
6723
+ }
6724
+ if (char === "(") parenStack.push(index);
6725
+ else if (char === ")") {
6726
+ const open = parenStack.pop();
6727
+ if (open !== void 0) {
6728
+ const head = headBefore(source, open);
6729
+ const group = { open, close: index, name: head.name, async: head.async };
6730
+ parens.push(group);
6731
+ closedAt.set(index, group);
6732
+ if (!CONTROL_KEYWORDS.has(group.name) && /^\s*:/.test(source.slice(index + 1, index + 40))) {
6733
+ awaitingBody = { async: group.async, depth: braceStack.length, params: group };
6734
+ }
6735
+ }
6736
+ } else if (char === ";" || char === "=") {
6737
+ if (awaitingBody && braceStack.length === awaitingBody.depth) awaitingBody = null;
6738
+ } else if (char === "{") {
6739
+ const kind = bodyKind(source, index, closedAt, awaitingBody);
6740
+ if (kind.fn) awaitingBody = null;
6741
+ braceStack.push({ open: index, ...kind });
6742
+ } else if (char === "}") {
6743
+ const open = braceStack.pop();
6744
+ if (open !== void 0) blocks.push({ ...open, close: index });
6745
+ }
6746
+ if (!/\s/.test(char)) previous = char;
6747
+ index += 1;
6748
+ }
6749
+ for (const open of braceStack) blocks.push({ ...open, close: source.length });
6750
+ return { parens, blocks };
6751
+ }
6752
+ function nextLine(source, from) {
6753
+ const stop = source.indexOf("\n", from);
6754
+ return stop < 0 ? source.length : stop + 1;
6755
+ }
6756
+ function bodyKind(source, index, closedAt, awaitingBody) {
6757
+ const before = significantBefore(source, index);
6758
+ if (before.text === "=>") {
6759
+ let cursor = before.at - 1;
6760
+ while (cursor >= 0 && /\s/.test(source[cursor])) cursor -= 1;
6761
+ const group = source[cursor] === ")" ? closedAt.get(cursor) : void 0;
6762
+ if (group) return { fn: true, async: group.async, params: group };
6763
+ return { fn: true, async: asyncBefore(source, cursor + 1), params: null };
6764
+ }
6765
+ if (before.text === ")") {
6766
+ const group = closedAt.get(before.at);
6767
+ if (!group) return { fn: false, async: false, params: null };
6768
+ if (CONTROL_KEYWORDS.has(group.name)) return { fn: false, async: false, params: null };
6769
+ return { fn: true, async: group.async, params: group };
6770
+ }
6771
+ if (awaitingBody && /[>\])\w$]/.test(before.text)) {
6772
+ return { fn: true, async: awaitingBody.async, params: awaitingBody.params };
6773
+ }
6774
+ return { fn: false, async: false, params: null };
6775
+ }
6776
+ function significantBefore(source, index) {
6777
+ let cursor = index - 1;
6778
+ for (; ; ) {
6779
+ while (cursor >= 0 && /\s/.test(source[cursor])) cursor -= 1;
6780
+ if (cursor >= 1 && source[cursor] === "/" && source[cursor - 1] === "*") {
6781
+ const open = source.lastIndexOf("/*", cursor - 1);
6782
+ if (open < 0) break;
6783
+ cursor = open - 1;
6784
+ continue;
6785
+ }
6786
+ break;
6787
+ }
6788
+ if (cursor < 0) return { text: "", at: 0 };
6789
+ if (cursor >= 1 && source[cursor] === ">" && source[cursor - 1] === "=") {
6790
+ return { text: "=>", at: cursor - 1 };
6791
+ }
6792
+ return { text: source[cursor], at: cursor };
6793
+ }
6794
+ function headBefore(source, parenOpen) {
6795
+ let cursor = parenOpen - 1;
6796
+ while (cursor >= 0 && /\s/.test(source[cursor])) cursor -= 1;
6797
+ let end = cursor + 1;
6798
+ while (cursor >= 0 && /[\w$]/.test(source[cursor])) cursor -= 1;
6799
+ const name = source.slice(cursor + 1, end);
6800
+ let before = cursor;
6801
+ if (name === "") {
6802
+ return { name: "", async: asyncBefore(source, parenOpen) };
6803
+ }
6804
+ if (name === "async") return { name: "", async: true };
6805
+ while (before >= 0 && /[\s*]/.test(source[before])) before -= 1;
6806
+ end = before + 1;
6807
+ while (before >= 0 && /[\w$]/.test(source[before])) before -= 1;
6808
+ const keyword = source.slice(before + 1, end);
6809
+ if (keyword === "function") return { name, async: asyncBefore(source, before + 1) };
6810
+ return { name, async: asyncBefore(source, cursor + 1) };
6811
+ }
6812
+ function asyncBefore(source, at) {
6813
+ let cursor = at - 1;
6814
+ while (cursor >= 0 && /[\s*]/.test(source[cursor])) cursor -= 1;
6815
+ const end = cursor + 1;
6816
+ while (cursor >= 0 && /[\w$]/.test(source[cursor])) cursor -= 1;
6817
+ return source.slice(cursor + 1, end) === "async";
6818
+ }
6819
+ function innermost(list, at) {
6820
+ return enclosing(list, at)[0] ?? null;
6821
+ }
6822
+ function enclosing(list, at) {
6823
+ return list.filter((item) => at > item.open && at <= item.close).sort((a, b) => b.open - a.open);
6824
+ }
6825
+ function enclosingArguments(source, structure, at) {
6826
+ const body = innermost(
6827
+ structure.blocks.filter((block) => block.fn && block.params !== null),
6828
+ at
6829
+ );
6830
+ if (!body?.params) return null;
6831
+ return parameterExpression(source.slice(body.params.open + 1, body.params.close));
6832
+ }
6833
+ function awaitAllowed(structure, at) {
6834
+ const body = innermost(
6835
+ structure.blocks.filter((block) => block.fn),
6836
+ at
6837
+ );
6838
+ return body ? body.async : null;
6839
+ }
6531
6840
  function planCheckpoints(files, read, toolKeys) {
6532
6841
  const plans = [];
6533
6842
  const outstanding = new Set(toolKeys);
@@ -6541,24 +6850,41 @@ function planCheckpoints(files, read, toolKeys) {
6541
6850
  (key) => source.includes(key) || source.includes(camelOf(key))
6542
6851
  );
6543
6852
  if (present.length === 0) continue;
6544
- const scopes = functionScopes(source);
6853
+ const structure = readStructure(source);
6545
6854
  const lineOf = lineLocator2(source);
6546
6855
  const alias = chooseAliasFor(source, "checkpoint", "js");
6547
- const edits = [];
6548
- const placed = [];
6856
+ const sites = /* @__PURE__ */ new Map();
6549
6857
  for (const key of present) {
6550
- const site = checkpointSite(source, key, scopes);
6858
+ const site = checkpointSite(source, key, structure);
6551
6859
  if (!site) continue;
6552
- const indent = blockIndent(source, site.at);
6860
+ const existing = sites.get(site.at);
6861
+ if (existing) existing.keys.push(key);
6862
+ else sites.set(site.at, { keys: [key], site });
6863
+ }
6864
+ if (sites.size === 0) continue;
6865
+ const edits = [];
6866
+ const placed = [];
6867
+ for (const [at, { keys, site }] of sites) {
6868
+ if (/^\s*(?:await\s+)?[A-Za-z_$]*[cC]heckpoint\(/.test(source.slice(at, at + 200))) continue;
6869
+ const many = keys.length > 1 || site.shared === true;
6870
+ const keyExpression = many && site.keyExpression ? site.keyExpression : quote(keys[0] ?? "");
6871
+ if (many && !site.keyExpression) continue;
6553
6872
  const args = site.argsExpression ? `, ${site.argsExpression}` : "";
6554
6873
  edits.push({
6555
- start: site.at,
6556
- end: site.at,
6874
+ start: at,
6875
+ end: at,
6557
6876
  text: `
6558
- ${indent}await ${alias}(${quote(key)}${args});`
6877
+ ${blockIndent(source, at)}await ${alias}(${keyExpression}${args});`
6559
6878
  });
6560
- placed.push({ key, line: lineOf(site.at), shape: site.shape });
6561
- outstanding.delete(key);
6879
+ for (const key of keys) {
6880
+ placed.push({
6881
+ key,
6882
+ line: lineOf(at),
6883
+ shape: site.shape,
6884
+ ...many ? { dynamic: true } : {}
6885
+ });
6886
+ outstanding.delete(key);
6887
+ }
6562
6888
  }
6563
6889
  if (edits.length === 0) continue;
6564
6890
  const after = addJsSdkImport(applyEdits(source, edits), "checkpoint", alias);
@@ -6566,30 +6892,97 @@ ${indent}await ${alias}(${quote(key)}${args});`
6566
6892
  }
6567
6893
  return plans;
6568
6894
  }
6569
- function checkpointSite(source, key, scopes) {
6895
+ function planCheckpointRepairs(files, read, toolKeys) {
6896
+ const plans = [];
6897
+ const known = new Set(toolKeys);
6898
+ for (const file of files) {
6899
+ if (file.endsWith(".py") || !JS_EXTENSIONS.test(file)) continue;
6900
+ const source = read(file);
6901
+ if (source === null || !/[cC]heckpoint\(/.test(source)) continue;
6902
+ const structure = readStructure(source);
6903
+ const lineOf = lineLocator2(source);
6904
+ const edits = [];
6905
+ const placed = [];
6906
+ const calls = /(?<![\w$.])([A-Za-z_$]*[cC]heckpoint)\(\s*(['"`])([a-z][a-z0-9_]*)\2/g;
6907
+ for (const match of source.matchAll(calls)) {
6908
+ const key = match[3];
6909
+ if (!known.has(key)) continue;
6910
+ const at = match.index ?? 0;
6911
+ const group = enclosing(structure.parens, at).find(
6912
+ (candidate) => BRANCH_KEYWORDS.has(candidate.name)
6913
+ );
6914
+ const head = group ?? innermostBranchAbove(source, structure, at);
6915
+ if (!head) continue;
6916
+ const compared = comparedKeyExpression(source, head, key);
6917
+ if (!compared) continue;
6918
+ const served = branchKeys(source, head, compared);
6919
+ if (served.length < 2 || !served.includes(key)) continue;
6920
+ const keyStart = at + match[0].indexOf(match[2], match[1].length);
6921
+ edits.push({ start: keyStart, end: keyStart + (key.length + 2), text: compared });
6922
+ for (const served_key of served) {
6923
+ placed.push({ key: served_key, line: lineOf(at), shape: "branch", dynamic: true });
6924
+ }
6925
+ }
6926
+ if (edits.length === 0) continue;
6927
+ plans.push({ file, before: source, after: applyEdits(source, edits), placed });
6928
+ }
6929
+ return plans;
6930
+ }
6931
+ function innermostBranchAbove(source, structure, at) {
6932
+ const body = innermost(
6933
+ structure.blocks.filter((block) => !block.fn),
6934
+ at
6935
+ );
6936
+ if (!body) return null;
6937
+ return structure.parens.filter((group) => BRANCH_KEYWORDS.has(group.name) && group.close < body.open).sort((a, b) => b.close - a.close)[0] ?? null;
6938
+ }
6939
+ function comparedKeyExpression(source, head, key) {
6940
+ const text = source.slice(head.open, head.close);
6941
+ const pattern = new RegExp(
6942
+ `(?<![\\w$.])([A-Za-z_$][\\w$]*(?:\\s*(?:\\?\\.|\\.)\\s*[A-Za-z_$][\\w$]*)*)\\s*={2,3}\\s*['"\`]${escapeForRegex(key)}['"\`]`
6943
+ );
6944
+ const found = pattern.exec(text);
6945
+ return found ? found[1].replace(/\s+/g, "") : null;
6946
+ }
6947
+ function checkpointSite(source, key, structure) {
6570
6948
  const quoted = `['"\`]${escapeForRegex(key)}['"\`]`;
6571
- const usable = (at) => enclosingAsync(scopes, at) === true;
6949
+ const usable = (at) => awaitAllowed(structure, at) === true;
6950
+ const bodyAfter = (close) => {
6951
+ const brace = /^\s*\{/.exec(source.slice(close + 1, close + 200));
6952
+ return brace ? close + 1 + brace[0].length : null;
6953
+ };
6572
6954
  for (const match of source.matchAll(new RegExp(`\\bcase\\s+${quoted}\\s*:`, "g"))) {
6573
6955
  const colon = (match.index ?? 0) + match[0].length;
6574
6956
  const braced = /^\s*\{/.exec(source.slice(colon));
6575
6957
  const at = braced ? colon + braced[0].length : colon;
6576
6958
  if (!usable(at)) continue;
6577
- return { at, shape: "case", argsExpression: switchArguments(source, match.index ?? 0) };
6578
- }
6579
- const comparison = new RegExp(
6580
- `(?:if|else\\s+if)\\s*\\(([^()]{0,200}?)${quoted}([^()]{0,200}?)\\)\\s*\\{`,
6581
- "g"
6582
- );
6583
- for (const match of source.matchAll(comparison)) {
6584
- const left = match[1] ?? "";
6585
- if (!/(?<![!<>])={2,3}\s*$/.test(left)) continue;
6586
- const at = (match.index ?? 0) + match[0].length;
6587
- if (!usable(at)) continue;
6588
- const object = /([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*[A-Za-z_$][\w$]*\s*={2,3}\s*$/.exec(left);
6959
+ const discriminant = switchKey(source, match.index ?? 0);
6589
6960
  return {
6590
6961
  at,
6591
- shape: "branch",
6592
- argsExpression: object ? argumentsOf(source, object[1]) : null
6962
+ shape: "case",
6963
+ argsExpression: discriminant ? argumentsOf(source, discriminant.object) : null,
6964
+ keyExpression: discriminant?.expression ?? null
6965
+ };
6966
+ }
6967
+ for (const match of source.matchAll(new RegExp(quoted, "g"))) {
6968
+ const at = match.index ?? 0;
6969
+ if (!positiveComparison(source, at)) continue;
6970
+ const group = enclosing(structure.parens, at).find(
6971
+ (candidate) => BRANCH_KEYWORDS.has(candidate.name)
6972
+ );
6973
+ if (!group) continue;
6974
+ const insertAt = bodyAfter(group.close);
6975
+ if (insertAt === null || !usable(insertAt)) continue;
6976
+ const compared = comparedExpression(source, at);
6977
+ const loop = group.name === "for" ? loopVariable(source, group.open) : null;
6978
+ const subject = loop ?? compared?.object ?? null;
6979
+ const served = compared ? branchKeys(source, group, compared.expression) : [];
6980
+ return {
6981
+ at: insertAt,
6982
+ shape: group.name === "if" ? "branch" : "loop",
6983
+ argsExpression: subject ? argumentsOf(source, subject) : enclosingArguments(source, structure, insertAt),
6984
+ keyExpression: loop ? `${loop}.name` : compared?.expression ?? null,
6985
+ shared: served.length > 1
6593
6986
  };
6594
6987
  }
6595
6988
  const declaration = new RegExp(
@@ -6599,18 +6992,23 @@ function checkpointSite(source, key, scopes) {
6599
6992
  for (const match of source.matchAll(declaration)) {
6600
6993
  const name = match[1];
6601
6994
  const end = (match.index ?? 0) + match[0].length;
6602
- const head = new RegExp(`^\\s*if\\s*\\(\\s*${escapeForRegex(name)}\\b`).exec(
6603
- source.slice(end, end + 200)
6995
+ const block = innermost(structure.blocks, end);
6996
+ const limit = block ? block.close : source.length;
6997
+ const head = new RegExp(`\\bif\\s*\\(\\s*${escapeForRegex(name)}\\b`).exec(
6998
+ source.slice(end, limit)
6604
6999
  );
6605
7000
  if (!head) continue;
6606
- const parenOpen = end + head[0].lastIndexOf("(", head[0].length - name.length);
6607
- const parenClose = matchBracket(source, parenOpen);
6608
- if (parenClose < 0) continue;
6609
- const brace = /^\s*\{/.exec(source.slice(parenClose + 1));
6610
- if (!brace) continue;
6611
- const at = parenClose + 1 + brace[0].length;
6612
- if (!usable(at)) continue;
6613
- return { at, shape: "found", argsExpression: argumentsOf(source, name) };
7001
+ const parenOpen = end + (head.index ?? 0) + head[0].indexOf("(");
7002
+ const group = structure.parens.find((candidate) => candidate.open === parenOpen);
7003
+ if (!group) continue;
7004
+ const insertAt = bodyAfter(group.close);
7005
+ if (insertAt === null || !usable(insertAt)) continue;
7006
+ return {
7007
+ at: insertAt,
7008
+ shape: "found",
7009
+ argsExpression: argumentsOf(source, name),
7010
+ keyExpression: `${name}.name`
7011
+ };
6614
7012
  }
6615
7013
  const handler = new RegExp(
6616
7014
  `${quoted}\\s*:\\s*(?:async\\s*)?\\(([^)]{0,200})\\)\\s*(?::[^=>{]{0,120})?=>\\s*\\{`,
@@ -6619,28 +7017,68 @@ function checkpointSite(source, key, scopes) {
6619
7017
  for (const match of source.matchAll(handler)) {
6620
7018
  const at = (match.index ?? 0) + match[0].length;
6621
7019
  if (!usable(at)) continue;
6622
- return { at, shape: "handler", argsExpression: parameterExpression(match[1] ?? "") };
7020
+ return {
7021
+ at,
7022
+ shape: "handler",
7023
+ argsExpression: parameterExpression(match[1] ?? ""),
7024
+ keyExpression: null
7025
+ };
6623
7026
  }
6624
- const camel = camelOf(key);
6625
7027
  const named = new RegExp(
6626
- `\\basync\\s+function\\s+(?:${escapeForRegex(camel)}|${escapeForRegex(key)})\\s*\\(`,
7028
+ `\\basync\\s+function\\s+(?:${escapeForRegex(camelOf(key))}|${escapeForRegex(key)})\\s*\\(`,
6627
7029
  "g"
6628
7030
  );
6629
7031
  for (const match of source.matchAll(named)) {
6630
7032
  const parenOpen = (match.index ?? 0) + match[0].length - 1;
6631
- const parenClose = matchBracket(source, parenOpen);
6632
- if (parenClose < 0) continue;
6633
- const bodyOpen = source.indexOf("{", parenClose);
6634
- if (bodyOpen < 0) continue;
6635
- if (!/^\s*(?::[^{;]*)?$/.test(source.slice(parenClose + 1, bodyOpen))) continue;
7033
+ const group = structure.parens.find((candidate) => candidate.open === parenOpen);
7034
+ if (!group) continue;
7035
+ const body = structure.blocks.filter((block) => block.fn && block.open > group.close).sort((a, b) => a.open - b.open)[0];
7036
+ if (!body) continue;
6636
7037
  return {
6637
- at: bodyOpen + 1,
7038
+ at: body.open + 1,
6638
7039
  shape: "function",
6639
- argsExpression: parameterExpression(source.slice(parenOpen + 1, parenClose))
7040
+ argsExpression: parameterExpression(source.slice(group.open + 1, group.close)),
7041
+ keyExpression: null
6640
7042
  };
6641
7043
  }
6642
7044
  return null;
6643
7045
  }
7046
+ function positiveComparison(source, at) {
7047
+ const before = source.slice(Math.max(0, at - 96), at);
7048
+ return /(?<![!<>])={2,3}\s*$/.test(before);
7049
+ }
7050
+ function comparedExpression(source, at) {
7051
+ const before = source.slice(Math.max(0, at - 160), at);
7052
+ const member = /([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*={2,3}\s*$/.exec(before);
7053
+ if (member) return { object: member[1], expression: `${member[1]}.${member[2]}` };
7054
+ const bare = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*={2,3}\s*$/.exec(before);
7055
+ if (bare) return { object: null, expression: bare[1] };
7056
+ return null;
7057
+ }
7058
+ function branchKeys(source, group, expression) {
7059
+ const head = source.slice(group.open, group.close);
7060
+ const [object, ...rest] = expression.split(".");
7061
+ if (object === void 0) return [];
7062
+ const access = rest.map((part) => `\\s*(?:\\?\\.|\\.)\\s*${escapeForRegex(part)}`).join("");
7063
+ const pattern = new RegExp(
7064
+ `(?<![\\w$.])${escapeForRegex(object)}${access}\\s*={2,3}\\s*['"\`]([a-z][a-z0-9_]*)['"\`]`,
7065
+ "g"
7066
+ );
7067
+ return [...new Set([...head.matchAll(pattern)].map((match) => match[1]))];
7068
+ }
7069
+ function switchKey(source, at) {
7070
+ const before = source.slice(Math.max(0, at - 4e3), at);
7071
+ const found = [
7072
+ ...before.matchAll(/\bswitch\s*\(\s*([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)/g)
7073
+ ];
7074
+ const last = found[found.length - 1];
7075
+ return last ? { object: last[1], expression: `${last[1]}.${last[2]}` } : null;
7076
+ }
7077
+ function loopVariable(source, parenOpen) {
7078
+ return /^\s*\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\b/.exec(
7079
+ source.slice(parenOpen, parenOpen + 120)
7080
+ )?.[1] ?? null;
7081
+ }
6644
7082
  function parameterExpression(parameters) {
6645
7083
  const names = parameters.split(",").map((part) => /^\s*(?:\.\.\.)?([A-Za-z_$][\w$]*)/.exec(part)?.[1]).filter((name) => name !== void 0);
6646
7084
  const named = names.find((name) => CHECKPOINT_ARG_PROPERTIES.includes(name));
@@ -6650,14 +7088,6 @@ function parameterExpression(parameters) {
6650
7088
  function camelOf(key) {
6651
7089
  return key.replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase());
6652
7090
  }
6653
- function switchArguments(source, at) {
6654
- const before = source.slice(Math.max(0, at - 4e3), at);
6655
- const found = [
6656
- ...before.matchAll(/\bswitch\s*\(\s*([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*[A-Za-z_$][\w$]*/g)
6657
- ];
6658
- const last = found[found.length - 1];
6659
- return last ? argumentsOf(source, last[1]) : null;
6660
- }
6661
7091
  function argumentsOf(source, object) {
6662
7092
  for (const property of CHECKPOINT_ARG_PROPERTIES) {
6663
7093
  const pattern = new RegExp(`\\b${escapeForRegex(object)}\\s*(?:\\?\\.|\\.)\\s*${property}\\b`);
@@ -6675,63 +7105,6 @@ function blockIndent(source, at) {
6675
7105
  const lineStart = source.lastIndexOf("\n", at) + 1;
6676
7106
  return `${/^[ \t]*/.exec(source.slice(lineStart, at))?.[0] ?? ""} `;
6677
7107
  }
6678
- function functionScopes(source) {
6679
- const scopes = [];
6680
- const add = (isAsync, parenOpen, arrow) => {
6681
- const parenClose = matchBracket(source, parenOpen);
6682
- if (parenClose < 0) return;
6683
- const bodyOpen = source.indexOf("{", parenClose);
6684
- if (bodyOpen < 0) return;
6685
- const between = source.slice(parenClose + 1, bodyOpen);
6686
- if (arrow !== /=>/.test(between)) return;
6687
- if (!/^\s*(?::[^{;=]*)?(?:=>\s*)?$/.test(between)) return;
6688
- const bodyClose = matchBracket(source, bodyOpen);
6689
- if (bodyClose < 0) return;
6690
- scopes.push({ bodyOpen, bodyClose, async: isAsync });
6691
- };
6692
- for (const match of source.matchAll(/\b(async\s+)?function\s*\*?\s*[A-Za-z_$][\w$]*\s*\(/g)) {
6693
- add(match[1] !== void 0, (match.index ?? 0) + match[0].length - 1, false);
6694
- }
6695
- for (const match of source.matchAll(/(\basync\s*)?\(/g)) {
6696
- add(match[1] !== void 0, (match.index ?? 0) + match[0].length - 1, true);
6697
- }
6698
- const method = /^[ \t]*((?:(?:public|private|protected|static|override|async|get|set)\s+)*)\*?\s*([A-Za-z_$][\w$]*)\s*\(/gm;
6699
- for (const match of source.matchAll(method)) {
6700
- if (CONTROL_KEYWORDS.has(match[2])) continue;
6701
- add(/\basync\b/.test(match[1] ?? ""), (match.index ?? 0) + match[0].length - 1, false);
6702
- }
6703
- for (const match of source.matchAll(/\b(async\s+)?([A-Za-z_$][\w$]*)\s*=>\s*\{/g)) {
6704
- const bodyOpen = (match.index ?? 0) + match[0].length - 1;
6705
- const bodyClose = matchBracket(source, bodyOpen);
6706
- if (bodyClose > 0) scopes.push({ bodyOpen, bodyClose, async: match[1] !== void 0 });
6707
- }
6708
- return scopes;
6709
- }
6710
- var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
6711
- "if",
6712
- "else",
6713
- "for",
6714
- "while",
6715
- "do",
6716
- "switch",
6717
- "catch",
6718
- "with",
6719
- "return",
6720
- "typeof",
6721
- "new",
6722
- "await",
6723
- "function",
6724
- "throw",
6725
- "case"
6726
- ]);
6727
- function enclosingAsync(scopes, at) {
6728
- let best = null;
6729
- for (const scope of scopes) {
6730
- if (at <= scope.bodyOpen || at > scope.bodyClose) continue;
6731
- if (!best || scope.bodyOpen > best.bodyOpen) best = scope;
6732
- }
6733
- return best ? best.async : null;
6734
- }
6735
7108
  function unguardJs(source) {
6736
7109
  const checkpoints = uncheckpointJs(source);
6737
7110
  const imported = jsGuardImport(checkpoints.after);
@@ -6788,9 +7161,10 @@ function uncheckpointJs(source) {
6788
7161
  const lineOf = lineLocator2(source);
6789
7162
  const leftovers = [];
6790
7163
  let removed = 0;
7164
+ const key = `(?:(['"\`])[a-z][a-z0-9_]*\\1|[A-Za-z_$][\\w$]*(?:\\s*(?:\\?\\.|\\.)\\s*[A-Za-z_$][\\w$]*)+)`;
6791
7165
  let after = source.replace(
6792
7166
  new RegExp(
6793
- `^[ \\t]*(?:await[ \\t]+)?${escapeForRegex(alias)}\\(\\s*(['"\`])[a-z][a-z0-9_]*\\1[^\\n]*\\)[ \\t]*;?[ \\t]*\\r?\\n`,
7167
+ `^[ \\t]*(?:await[ \\t]+)?${escapeForRegex(alias)}\\(\\s*${key}[^\\n]*\\)[ \\t]*;?[ \\t]*\\r?\\n`,
6794
7168
  "gm"
6795
7169
  ),
6796
7170
  () => {
@@ -6812,8 +7186,28 @@ function uncheckpointJs(source) {
6812
7186
  function unguardPython(source) {
6813
7187
  const imported = /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)$/m.exec(source);
6814
7188
  if (!imported) return { after: source, removed: 0, leftovers: [] };
6815
- const spec = (imported[1] ?? "").replace(/[()]/g, "").split(",").map((part) => /^guard(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
6816
- if (!spec) return { after: source, removed: 0, leftovers: [] };
7189
+ const names = (imported[1] ?? "").replace(/[()]/g, "").split(",");
7190
+ const spec = names.map((part) => /^guard(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
7191
+ const gate = names.map((part) => /^checkpoint(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
7192
+ if (!spec && !gate) return { after: source, removed: 0, leftovers: [] };
7193
+ if (!spec) {
7194
+ const alias2 = gate?.[1] ?? "checkpoint";
7195
+ let removed2 = 0;
7196
+ let after2 = source.replace(
7197
+ new RegExp(
7198
+ `^[ \\t]*${escapeForRegex(alias2)}\\(\\s*(['"])[a-z][a-z0-9_]*\\1[^\\n]*\\)[ \\t]*\\r?\\n`,
7199
+ "gm"
7200
+ ),
7201
+ () => {
7202
+ removed2 += 1;
7203
+ return "";
7204
+ }
7205
+ );
7206
+ if (!new RegExp(`(?<![\\w.])${escapeForRegex(alias2)}\\s*\\(`).test(after2)) {
7207
+ after2 = removePythonImport(after2, alias2);
7208
+ }
7209
+ return { after: after2, removed: removed2, leftovers: [] };
7210
+ }
6817
7211
  const alias = spec[1] ?? "guard";
6818
7212
  const lineOf = lineLocator2(source);
6819
7213
  let removed = 0;
@@ -6852,18 +7246,20 @@ function unguardPython(source) {
6852
7246
  after = applyEdits(after, edits);
6853
7247
  removed += edits.length;
6854
7248
  const stillUsed = new RegExp(`(?<![\\w.])@?${escapeForRegex(alias)}\\s*\\(`).test(after);
6855
- if (!stillUsed) {
6856
- after = after.replace(
6857
- /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)\r?\n?/m,
6858
- (line, names) => {
6859
- const rest = names.replace(/[()]/g, "").split(",").map((part) => part.trim()).filter((part) => part && !/^guard(?:\s+as\s+\w+)?$/.test(part));
6860
- return rest.length === 0 ? "" : `from veefore import ${rest.join(", ")}
6861
- `;
6862
- }
6863
- );
6864
- }
7249
+ if (!stillUsed) after = removePythonImport(after, "guard");
6865
7250
  return { after, removed, leftovers };
6866
7251
  }
7252
+ function removePythonImport(source, name) {
7253
+ const target = new RegExp(`^${escapeForRegex(name)}(?:\\s+as\\s+\\w+)?$`);
7254
+ return source.replace(
7255
+ /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)\r?\n?/m,
7256
+ (_line, names) => {
7257
+ const rest = names.replace(/[()]/g, "").split(",").map((part) => part.trim()).filter((part) => part && !target.test(part));
7258
+ return rest.length === 0 ? "" : `from veefore import ${rest.join(", ")}
7259
+ `;
7260
+ }
7261
+ );
7262
+ }
6867
7263
  function arrowExpressionBody(fn) {
6868
7264
  let cursor = 0;
6869
7265
  if (fn.startsWith("(")) {
@@ -7107,7 +7503,6 @@ async function integrate(root, toolKeys, options) {
7107
7503
  const candidates = shortlist(root, index, anchors);
7108
7504
  const sourceFiles = candidates.files;
7109
7505
  const truncated = index.truncated || candidates.truncated;
7110
- const plan = planRewrite(root, sourceFiles, toolKeys);
7111
7506
  const interactive = options.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true && !options.yes);
7112
7507
  if (truncated && !options.quiet) {
7113
7508
  out(
@@ -7118,13 +7513,39 @@ async function integrate(root, toolKeys, options) {
7118
7513
  const dispatchGuardFiles = findDispatchGuards(sources);
7119
7514
  const dispatchGuarded = dispatchGuardFiles.length > 0;
7120
7515
  const alreadyGuarded = guardedToolKeys(sources, toolKeys);
7121
- for (const file of plan.files) {
7122
- for (const change of file.changes) {
7123
- if (change.status === "manual" && alreadyGuarded.has(change.key)) {
7124
- change.status = "already";
7125
- delete change.reason;
7516
+ const reconcile = (result) => {
7517
+ for (const file of result.files) {
7518
+ for (const change of file.changes) {
7519
+ if (change.status === "manual" && alreadyGuarded.has(change.key)) {
7520
+ change.status = "already";
7521
+ delete change.reason;
7522
+ }
7126
7523
  }
7127
7524
  }
7525
+ return result;
7526
+ };
7527
+ let plan = reconcile(planRewrite(root, sourceFiles, toolKeys));
7528
+ const changeable = [
7529
+ ...new Set(
7530
+ plan.files.flatMap(
7531
+ (file) => file.changes.filter((change) => change.status !== "already").map((change) => change.key)
7532
+ )
7533
+ )
7534
+ ];
7535
+ const selected = await chooseTools(changeable, plan, {
7536
+ requested: options.tools,
7537
+ interactive,
7538
+ quiet: options.quiet === true
7539
+ });
7540
+ const skipped = changeable.filter((key) => !selected.includes(key));
7541
+ if (skipped.length > 0) {
7542
+ plan = reconcile(
7543
+ planRewrite(
7544
+ root,
7545
+ sourceFiles,
7546
+ toolKeys.filter((key) => !skipped.includes(key))
7547
+ )
7548
+ );
7128
7549
  }
7129
7550
  let manual = plan.files.flatMap(
7130
7551
  (file) => file.changes.filter((change) => change.status === "manual").map((change) => ({
@@ -7157,6 +7578,15 @@ async function integrate(root, toolKeys, options) {
7157
7578
  dispatch = [];
7158
7579
  }
7159
7580
  }
7581
+ const repaired = applyCheckpoints(
7582
+ plan,
7583
+ planCheckpointRepairs(sourceFiles, (file) => currentText(root, plan, file), toolKeys)
7584
+ );
7585
+ if (repaired.size > 0 && !options.quiet) {
7586
+ out(
7587
+ `${symbol.warn} Corrected a guard that named one tool in a block that runs several: ${style.bold([...repaired].join(", "))} ${style.grey("(it now carries the key your code read)")}`
7588
+ );
7589
+ }
7160
7590
  if (dispatch.length > 0 && checkpointUsable(root, options.quiet === true)) {
7161
7591
  const placed = applyCheckpoints(
7162
7592
  plan,
@@ -7270,7 +7700,15 @@ async function integrate(root, toolKeys, options) {
7270
7700
  reportSdkReadiness(root, [.../* @__PURE__ */ new Set([...guardedFiles, ...dispatchGuardFiles])]);
7271
7701
  }
7272
7702
  const unguarded2 = reportProtection(root, sourceFiles, toolKeys, quiet);
7273
- return { status: "nothing-to-do", plan, checks: [], backupId: null, manual, unguarded: unguarded2 };
7703
+ return {
7704
+ status: "nothing-to-do",
7705
+ plan,
7706
+ checks: [],
7707
+ backupId: null,
7708
+ manual,
7709
+ unguarded: unguarded2,
7710
+ skipped
7711
+ };
7274
7712
  }
7275
7713
  if (options.dryRun) {
7276
7714
  out(style.grey("Dry run: nothing was written."));
@@ -7280,7 +7718,8 @@ async function integrate(root, toolKeys, options) {
7280
7718
  checks: [],
7281
7719
  backupId: null,
7282
7720
  manual,
7283
- unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7721
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7722
+ skipped
7284
7723
  };
7285
7724
  }
7286
7725
  if (!options.quiet) {
@@ -7301,7 +7740,8 @@ async function integrate(root, toolKeys, options) {
7301
7740
  checks: [],
7302
7741
  backupId: null,
7303
7742
  manual,
7304
- unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7743
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7744
+ skipped
7305
7745
  };
7306
7746
  }
7307
7747
  const jsReady = languages.has("js") && sdkInstalled(root, "js");
@@ -7318,12 +7758,14 @@ async function integrate(root, toolKeys, options) {
7318
7758
  checks: [],
7319
7759
  backupId: null,
7320
7760
  manual,
7321
- unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7761
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7762
+ skipped
7322
7763
  };
7323
7764
  }
7324
7765
  }
7325
7766
  const backup = createBackup(root, toWrite);
7326
7767
  for (const file of toWrite) writeFilePreservingMode(path10.join(root, file.file), file.after);
7768
+ forgetIndex(root);
7327
7769
  out(
7328
7770
  `${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
7329
7771
  );
@@ -7385,7 +7827,8 @@ async function integrate(root, toolKeys, options) {
7385
7827
  checks,
7386
7828
  backupId: null,
7387
7829
  manual,
7388
- unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7830
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7831
+ skipped
7389
7832
  };
7390
7833
  }
7391
7834
  for (const language of languages) {
@@ -7395,9 +7838,14 @@ async function integrate(root, toolKeys, options) {
7395
7838
  );
7396
7839
  }
7397
7840
  }
7841
+ if (skipped.length > 0 && !options.quiet) {
7842
+ out(
7843
+ `${symbol.info} Left out of this run, as you chose: ${style.bold(skipped.join(", "))} ${style.grey("(no file was changed for these)")}`
7844
+ );
7845
+ }
7398
7846
  const unguarded = reportProtection(root, sourceFiles, toolKeys, options.quiet === true);
7399
7847
  out(style.grey(" Undo at any time with `veefore undo`."));
7400
- return { status: "applied", plan, checks, backupId: backup.id, manual, unguarded };
7848
+ return { status: "applied", plan, checks, backupId: backup.id, manual, unguarded, skipped };
7401
7849
  }
7402
7850
  function reportProtection(root, files, toolKeys, quiet) {
7403
7851
  const guarded = guardedToolKeys(readSources(root, files), toolKeys);
@@ -7560,6 +8008,43 @@ function stillManual(items, plan) {
7560
8008
  return true;
7561
8009
  });
7562
8010
  }
8011
+ async function chooseTools(changeable, plan, options) {
8012
+ if (options.requested) {
8013
+ const asked = new Set(options.requested);
8014
+ const unknown = options.requested.filter((key) => !changeable.includes(key));
8015
+ if (unknown.length > 0 && !options.quiet) {
8016
+ out(
8017
+ `${symbol.warn} Nothing to change for: ${unknown.join(", ")} ${style.grey("(already protected, or not found in the code)")}`
8018
+ );
8019
+ }
8020
+ return changeable.filter((key) => asked.has(key));
8021
+ }
8022
+ if (!options.interactive || changeable.length < 2) return changeable;
8023
+ const where = /* @__PURE__ */ new Map();
8024
+ for (const file of plan.files) {
8025
+ for (const change of file.changes) {
8026
+ if (change.status === "already" || where.has(change.key)) continue;
8027
+ where.set(change.key, { line: change.line, file: file.file, status: change.status });
8028
+ }
8029
+ }
8030
+ try {
8031
+ return await selectMany(
8032
+ "Which tools should Veefore decide for?",
8033
+ changeable.map((key) => {
8034
+ const at = where.get(key);
8035
+ return {
8036
+ label: key,
8037
+ value: key,
8038
+ hint: at ? `${at.status === "wrapped" ? "guard here" : "needs a change"} \xB7 ${at.file}:${at.line}` : ""
8039
+ };
8040
+ }),
8041
+ { help: "All of them unless you say otherwise. Only what is ticked gets changed." }
8042
+ );
8043
+ } catch (error) {
8044
+ if (error instanceof MenuCancelled) return [];
8045
+ throw error;
8046
+ }
8047
+ }
7563
8048
  function checkpointUsable(root, quiet) {
7564
8049
  const sdks = installedSdks(root).filter((sdk) => sdk.language === "js");
7565
8050
  const stale = sdks.filter((sdk) => !sdk.hasCheckpoint);
@@ -7632,7 +8117,13 @@ function guardedToolKeys(sources, toolKeys) {
7632
8117
  return new Map([...guarded].filter(([key]) => toolKeys.includes(key)));
7633
8118
  }
7634
8119
  function checkIntegration(root, toolKeys) {
7635
- const guarded = guardedToolKeys(readSources(root), toolKeys);
8120
+ const index = indexFiles(root);
8121
+ const candidates = shortlist(root, index, [
8122
+ ...toolKeyAnchors(toolKeys),
8123
+ ...TOOL_ANCHORS,
8124
+ ...GUARD_ANCHORS
8125
+ ]);
8126
+ const guarded = guardedToolKeys(readSources(root, candidates.files), toolKeys);
7636
8127
  heading("Protection check");
7637
8128
  for (const key of toolKeys) {
7638
8129
  const file = guarded.get(key);
@@ -7934,10 +8425,20 @@ async function integrateCommand(args, flags) {
7934
8425
  let assist;
7935
8426
  let dryRun = false;
7936
8427
  let check = false;
8428
+ let tools;
7937
8429
  for (const arg of args) {
7938
8430
  if (arg === "--dry-run") dryRun = true;
7939
8431
  else if (arg === "--check") check = true;
7940
- else if (arg.startsWith("--assist=")) {
8432
+ else if (arg === "--tools=all") tools = void 0;
8433
+ else if (arg.startsWith("--tools=")) {
8434
+ tools = arg.slice("--tools=".length).split(",").map((key) => key.trim()).filter((key) => key !== "");
8435
+ if (tools.length === 0) {
8436
+ throw new CliError(
8437
+ "`--tools` takes a comma-separated list of tool keys, or `all`.",
8438
+ "For example: --tools=refund_order,adjust_stock"
8439
+ );
8440
+ }
8441
+ } else if (arg.startsWith("--assist=")) {
7941
8442
  const value = ASSIST_VALUES[arg.slice("--assist=".length)];
7942
8443
  if (!value) {
7943
8444
  throw new CliError(
@@ -7980,7 +8481,8 @@ async function integrateCommand(args, flags) {
7980
8481
  dryRun,
7981
8482
  quiet: flags.json,
7982
8483
  assist,
7983
- api
8484
+ api,
8485
+ ...tools ? { tools } : {}
7984
8486
  });
7985
8487
  if (flags.json) {
7986
8488
  out(
@@ -7992,6 +8494,7 @@ async function integrateCommand(args, flags) {
7992
8494
  not_found: outcome.plan.notFound,
7993
8495
  manual: outcome.manual,
7994
8496
  unguarded: outcome.unguarded,
8497
+ skipped: outcome.skipped,
7995
8498
  checks: outcome.checks
7996
8499
  },
7997
8500
  null,
@@ -8394,7 +8897,7 @@ import { homedir as homedir2 } from "node:os";
8394
8897
  import path12 from "node:path";
8395
8898
 
8396
8899
  // src/version.ts
8397
- var CLI_VERSION = "1.5.0";
8900
+ var CLI_VERSION = "1.6.1";
8398
8901
 
8399
8902
  // src/panel.ts
8400
8903
  function welcomeSource(flags = {}) {
@@ -8518,12 +9021,12 @@ function printScan(scan) {
8518
9021
  `${symbol.warn} Your code reads ${service.length} credential${service.length === 1 ? "" : "s"}: ${service.map((s) => s.name).join(", ")} ${style.grey("(names only; values were not read)")}`
8519
9022
  );
8520
9023
  }
8521
- printGaps(scan.root);
9024
+ printGaps(scan);
8522
9025
  }
8523
- function printGaps(root) {
9026
+ function printGaps(scan) {
8524
9027
  let discovery;
8525
9028
  try {
8526
- discovery = discoverAgents(root);
9029
+ discovery = discoverAgents(scan.root, scan);
8527
9030
  } catch {
8528
9031
  return;
8529
9032
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veefore",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Veefore command line: sign in, scan an agent project, set it up and protect its tools.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://veefore.com",