veefore 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/veefore.mjs +1617 -247
  2. package/package.json +1 -1
package/dist/veefore.mjs CHANGED
@@ -236,29 +236,29 @@ var ApiClient = class {
236
236
  setToken(token) {
237
237
  this.token = token;
238
238
  }
239
- async get(path13, query = {}) {
239
+ async get(path14, query = {}) {
240
240
  const search = new URLSearchParams();
241
241
  for (const [key, value] of Object.entries(query)) {
242
242
  if (value !== void 0) search.set(key, String(value));
243
243
  }
244
244
  const suffix = search.toString() === "" ? "" : `?${search.toString()}`;
245
- return this.send("GET", `${path13}${suffix}`);
245
+ return this.send("GET", `${path14}${suffix}`);
246
246
  }
247
- async post(path13, body) {
248
- return this.send("POST", path13, body);
247
+ async post(path14, body) {
248
+ return this.send("POST", path14, body);
249
249
  }
250
- async patch(path13, body) {
251
- return this.send("PATCH", path13, body);
250
+ async patch(path14, body) {
251
+ return this.send("PATCH", path14, body);
252
252
  }
253
- async delete(path13) {
254
- return this.send("DELETE", path13);
253
+ async delete(path14) {
254
+ return this.send("DELETE", path14);
255
255
  }
256
256
  /** Raw request for endpoints that return a non-JSON body (exports). */
257
- async raw(method, path13, body) {
258
- return this.fetchWithTimeout(method, path13, body);
257
+ async raw(method, path14, body) {
258
+ return this.fetchWithTimeout(method, path14, body);
259
259
  }
260
- async send(method, path13, body) {
261
- const response = await this.fetchWithTimeout(method, path13, body);
260
+ async send(method, path14, body) {
261
+ const response = await this.fetchWithTimeout(method, path14, body);
262
262
  const text = await response.text();
263
263
  const parsed = parseJson(text);
264
264
  if (response.ok) {
@@ -266,14 +266,14 @@ var ApiClient = class {
266
266
  if (parsed && "data" in parsed) return parsed.data;
267
267
  throw new CliApiError(
268
268
  "UNEXPECTED_RESPONSE",
269
- `The control plane returned an unexpected body for ${method} ${path13}.`,
269
+ `The control plane returned an unexpected body for ${method} ${path14}.`,
270
270
  response.status,
271
271
  response.headers.get("x-request-id")
272
272
  );
273
273
  }
274
274
  const error = new CliApiError(
275
275
  parsed?.error?.code ?? `HTTP_${response.status}`,
276
- parsed?.error?.message ?? `${method} ${path13} failed with ${response.status} ${response.statusText}.`,
276
+ parsed?.error?.message ?? `${method} ${path14} failed with ${response.status} ${response.statusText}.`,
277
277
  response.status,
278
278
  parsed?.error?.request_id ?? response.headers.get("x-request-id"),
279
279
  parsed?.error?.details ?? {}
@@ -291,7 +291,7 @@ var ApiClient = class {
291
291
  }
292
292
  throw error;
293
293
  }
294
- async fetchWithTimeout(method, path13, body) {
294
+ async fetchWithTimeout(method, path14, body) {
295
295
  const controller = new AbortController();
296
296
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
297
297
  const headers = { accept: "application/json" };
@@ -300,7 +300,7 @@ var ApiClient = class {
300
300
  if (this.workspaceId) headers["x-veefore-workspace"] = this.workspaceId;
301
301
  if (this.environmentId) headers["x-veefore-environment"] = this.environmentId;
302
302
  try {
303
- return await fetch(`${this.controlPlaneUrl}${path13}`, {
303
+ return await fetch(`${this.controlPlaneUrl}${path14}`, {
304
304
  method,
305
305
  headers,
306
306
  body: body === void 0 ? void 0 : JSON.stringify(body),
@@ -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,
@@ -1946,11 +2037,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
1946
2037
  out(` ${style.red("This action is irreversible for this identity.")}`);
1947
2038
  out();
1948
2039
  }
1949
- async function lifecycle(args, flags, path13, resultingStatus) {
2040
+ async function lifecycle(args, flags, path14, resultingStatus) {
1950
2041
  const context = await resolveContext(flags);
1951
2042
  const agentId = positional(args) ?? context.config.agentId;
1952
2043
  const agent = await context.api.get(`/v1/agents/${agentId}`);
1953
- const reasonRequired = path13 === "suspend";
2044
+ const reasonRequired = path14 === "suspend";
1954
2045
  const reason = valueFor(args, "--reason");
1955
2046
  if (reasonRequired && (!reason || reason.trim().length < 10)) {
1956
2047
  throw new CliError(
@@ -1960,24 +2051,24 @@ async function lifecycle(args, flags, path13, resultingStatus) {
1960
2051
  }
1961
2052
  out();
1962
2053
  out(
1963
- `${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
2054
+ `${symbol.info} ${path14} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1964
2055
  );
1965
- if (path13 !== "resume") {
2056
+ if (path14 !== "resume") {
1966
2057
  const confirmed = await confirm("Continue?", { yes: flags.yes });
1967
2058
  if (!confirmed) {
1968
2059
  out(style.grey("Cancelled."));
1969
2060
  return 1;
1970
2061
  }
1971
2062
  }
1972
- const actionSpinner = spinner(`Applying ${path13}`);
2063
+ const actionSpinner = spinner(`Applying ${path14}`);
1973
2064
  try {
1974
2065
  await withStepUp(
1975
2066
  context.api,
1976
- () => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
2067
+ () => context.api.post(`/v1/agents/${agentId}/${path14}`, reason ? { reason } : {})
1977
2068
  );
1978
2069
  actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
1979
2070
  } catch (error) {
1980
- actionSpinner.fail(`Could not ${path13} the agent`);
2071
+ actionSpinner.fail(`Could not ${path14} the agent`);
1981
2072
  throw error;
1982
2073
  }
1983
2074
  out();
@@ -2289,6 +2380,7 @@ import { spawnSync } from "node:child_process";
2289
2380
  import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
2290
2381
  import path3 from "node:path";
2291
2382
  var MIN_APPROVAL_WAIT_SDK = "1.1.0";
2383
+ var MIN_CHECKPOINT_SDK = "1.2.0";
2292
2384
  function versionLessThan(a, b) {
2293
2385
  if (!a) return true;
2294
2386
  const left = a.split(/[.+-]/).slice(0, 3).map(Number);
@@ -2335,7 +2427,8 @@ function installedSdks(root, options = {}) {
2335
2427
  language: "js",
2336
2428
  location: path3.relative(top, path3.dirname(manifest)) || ".",
2337
2429
  version,
2338
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2430
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2431
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2339
2432
  });
2340
2433
  }
2341
2434
  if (options.python) {
@@ -2352,7 +2445,8 @@ function installedSdks(root, options = {}) {
2352
2445
  language: "py",
2353
2446
  location: "python: veefore-py",
2354
2447
  version,
2355
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2448
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2449
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2356
2450
  });
2357
2451
  }
2358
2452
  }
@@ -3300,12 +3394,12 @@ function isLoosening(from, to) {
3300
3394
  }
3301
3395
 
3302
3396
  // src/commands/init.ts
3303
- import { readFileSync as readFileSync9 } from "node:fs";
3304
- import path12 from "node:path";
3397
+ import { readFileSync as readFileSync10 } from "node:fs";
3398
+ import path13 from "node:path";
3305
3399
 
3306
3400
  // src/integrate.ts
3307
- import { readFileSync as readFileSync8 } from "node:fs";
3308
- import path9 from "node:path";
3401
+ import { readFileSync as readFileSync9 } from "node:fs";
3402
+ import path10 from "node:path";
3309
3403
 
3310
3404
  // ../types/src/permissions.ts
3311
3405
  var PERMISSIONS = [
@@ -4189,10 +4283,33 @@ function printDispatchInstructions(items, where) {
4189
4283
  out(
4190
4284
  ` ${items.length} tool${items.length === 1 ? " is" : "s are"} defined as data, so no one function performs each tool.`
4191
4285
  );
4192
- if (where) {
4193
- out(
4194
- ` Your code runs them in ${style.bold(`${where.file}:${where.line}`)}${where.functionName ? ` (${where.functionName})` : ""}.`
4195
- );
4286
+ const keys = [...new Set(items.map((item) => item.key))];
4287
+ const sites = new Map(
4288
+ keys.flatMap((key) => {
4289
+ const site = where?.sites?.get(key);
4290
+ return site ? [[key, site]] : [];
4291
+ })
4292
+ );
4293
+ const places = new Set([...sites.values()].map((site) => `${site.file}:${site.line}`));
4294
+ const only = places.size === 1 && sites.size === keys.length ? [...sites.values()][0] : null;
4295
+ if (sites.size > 0 && !only) {
4296
+ out(" Wrap the call that performs each one, where your code runs it:");
4297
+ out();
4298
+ for (const key of keys) {
4299
+ const site = sites.get(key);
4300
+ out(
4301
+ ` ${style.bold(key)} ${style.grey(site ? `${site.file}:${site.line}` : "where it is run; this could not be found")}`
4302
+ );
4303
+ if (site) out(` ${style.grey(site.code)}`);
4304
+ }
4305
+ out();
4306
+ out(" The change is the same shape in each place:");
4307
+ } else if (only || where) {
4308
+ const file = only?.file ?? where?.file;
4309
+ const line = only?.line ?? where?.line ?? 1;
4310
+ const named = where?.functionName && file === where.file ? ` (${where.functionName})` : "";
4311
+ out(` Your code runs them in ${style.bold(`${file}:${line}`)}${named}.`);
4312
+ if (only) out(` ${style.grey(only.code)}`);
4196
4313
  out(" Wrap the call there that performs the chosen tool, once:");
4197
4314
  } else {
4198
4315
  out(" Wrap the code that runs whichever tool the model picked, once:");
@@ -4210,7 +4327,9 @@ function printDispatchInstructions(items, where) {
4210
4327
  out(` ${style.cyan("import { guard } from '@veefore/sdk';")}`);
4211
4328
  out();
4212
4329
  out(` ${style.grey("// before: const result = await runTool(call);")}`);
4213
- out(` ${style.cyan("const result = await runTool(call);")}`);
4330
+ out(
4331
+ ` ${style.cyan("const result = await guard(call.name, (_args) => runTool(call))(call.args);")}`
4332
+ );
4214
4333
  }
4215
4334
  out();
4216
4335
  out(
@@ -4230,6 +4349,9 @@ function findDispatchGuards(sources) {
4230
4349
  const dynamic = /(?<![\w$.])[A-Za-z_$]*[gG]uard\(\s*(?!['"`])[A-Za-z_$][\w$.]*(?:\[[^\]\n]{1,40}\])?\s*[,)]/;
4231
4350
  return sources.filter(({ text }) => imports.test(text) && dynamic.test(stripDefinitions(text))).map(({ file }) => file);
4232
4351
  }
4352
+ function escapeRegex(value) {
4353
+ return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
4354
+ }
4233
4355
  function stripDefinitions(text) {
4234
4356
  return text.replace(/\b(?:function|def)\s+[A-Za-z_$]*[gG]uard\s*\(/g, "");
4235
4357
  }
@@ -4238,7 +4360,12 @@ function toCamel(key) {
4238
4360
  }
4239
4361
  function findGuardedKeys(sources) {
4240
4362
  const found = /* @__PURE__ */ new Map();
4241
- const pattern = /(?:\b|@)[A-Za-z_$]*[gG]uard\(\s*['"]([a-z][a-z0-9_]*)['"]/g;
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
+ }
4242
4369
  for (const { file, text } of sources) {
4243
4370
  for (const match of text.matchAll(pattern)) {
4244
4371
  const key = match[1];
@@ -4247,6 +4374,24 @@ function findGuardedKeys(sources) {
4247
4374
  }
4248
4375
  return found;
4249
4376
  }
4377
+ function dynamicCheckpointKeys(text, file) {
4378
+ const found = [];
4379
+ const calls = /(?:\b)[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 || rest.length === 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
+ }
4250
4395
  var PROVIDER_ENV = {
4251
4396
  anthropic: "ANTHROPIC_API_KEY",
4252
4397
  openai: "OPENAI_API_KEY"
@@ -4651,12 +4796,95 @@ function restoreBackup(root, backup, options = {}) {
4651
4796
  }
4652
4797
 
4653
4798
  // src/rewrite.ts
4654
- import { readFileSync as readFileSync7 } from "node:fs";
4655
- import path8 from "node:path";
4799
+ import { readFileSync as readFileSync8 } from "node:fs";
4800
+ import path9 from "node:path";
4656
4801
 
4657
4802
  // src/scan.ts
4658
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
4659
- import path7 from "node:path";
4803
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
4804
+ import path8 from "node:path";
4805
+
4806
+ // src/anchors.ts
4807
+ var TOOL_ANCHORS = [
4808
+ // Schema-defined tools: findToolSchemas requires one of these keys.
4809
+ "parameters",
4810
+ "input_schema",
4811
+ "inputSchema",
4812
+ "parametersJsonSchema",
4813
+ // The OpenAI function shape, which may carry no description at all.
4814
+ "'function'",
4815
+ '"function"',
4816
+ "`function`",
4817
+ // Framework calls and decorators.
4818
+ "tool(",
4819
+ "Tool(",
4820
+ "tool (",
4821
+ "Tool (",
4822
+ "registerTool",
4823
+ // Python decorators, including the `@mcp.tool` / `@agent.tool` forms, which
4824
+ // carry neither `@tool` nor `tool(`.
4825
+ "@tool",
4826
+ ".tool",
4827
+ "function_tool",
4828
+ "StructuredTool",
4829
+ "DynamicTool"
4830
+ ];
4831
+ var AGENT_ANCHORS = [
4832
+ // Model SDKs and agent frameworks.
4833
+ "openai",
4834
+ "OpenAI",
4835
+ "@anthropic-ai/sdk",
4836
+ "anthropic",
4837
+ "Anthropic",
4838
+ "@google/genai",
4839
+ "@google/generative-ai",
4840
+ "google.generativeai",
4841
+ "google-generativeai",
4842
+ "langchain",
4843
+ "langgraph",
4844
+ "crewai",
4845
+ "llama_index",
4846
+ "llamaindex",
4847
+ "autogen",
4848
+ "litellm",
4849
+ "mistralai",
4850
+ "ollama",
4851
+ "bedrock-runtime",
4852
+ "bedrock_runtime",
4853
+ "@modelcontextprotocol",
4854
+ "mcp.server",
4855
+ // The vocabulary of a tool call, whatever produced it.
4856
+ "tool_calls",
4857
+ "toolCalls",
4858
+ "tool_call",
4859
+ "toolCall",
4860
+ "function_call",
4861
+ "functionCall",
4862
+ "tool_choice",
4863
+ "toolChoice",
4864
+ "toolUse",
4865
+ "tool_use"
4866
+ ];
4867
+ var GUARD_ANCHORS = ["uard(", "uard (", "checkpoint("];
4868
+ var RISK_ANCHORS = [
4869
+ "://",
4870
+ "process.env",
4871
+ "import.meta.env",
4872
+ "os.environ",
4873
+ "getenv",
4874
+ "Deno.env"
4875
+ ];
4876
+ function toolKeyAnchors(toolKeys) {
4877
+ const anchors = /* @__PURE__ */ new Set();
4878
+ for (const key of toolKeys) {
4879
+ anchors.add(key);
4880
+ const camel = key.replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase());
4881
+ if (camel !== key) anchors.add(camel);
4882
+ }
4883
+ return [...anchors];
4884
+ }
4885
+ var SCAN_ANCHORS = [
4886
+ .../* @__PURE__ */ new Set([...TOOL_ANCHORS, ...AGENT_ANCHORS, ...GUARD_ANCHORS, ...RISK_ANCHORS])
4887
+ ];
4660
4888
 
4661
4889
  // src/framework.ts
4662
4890
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
@@ -4852,19 +5080,11 @@ function integrationSnippet(framework) {
4852
5080
  ].join("\n");
4853
5081
  }
4854
5082
 
4855
- // src/scan.ts
4856
- var MINOR_AMOUNT_PARAMETER = /["']?\b(amount_minor|amountMinor|amount_cents|amountCents|amount_in_cents|amountInCents|amount_minor_units|amountMinorUnits)\b["']?\s*[:=(]/;
4857
- var CURRENCY_PARAMETER = /["']?\b(currency_code|currencyCode|currency)\b["']?\s*[:=(]/;
4858
- function inferMoneyMapping(definition) {
4859
- const amount = MINOR_AMOUNT_PARAMETER.exec(definition)?.[1] ?? null;
4860
- const currency = CURRENCY_PARAMETER.exec(definition)?.[1] ?? null;
4861
- if (!amount && !currency) return null;
4862
- return {
4863
- amountParameter: amount,
4864
- amountUnit: amount ? "MINOR" : null,
4865
- currencyParameter: currency
4866
- };
4867
- }
5083
+ // src/search.ts
5084
+ import { execFileSync } from "node:child_process";
5085
+ import { mkdtempSync, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
5086
+ import { tmpdir } from "node:os";
5087
+ import path7 from "node:path";
4868
5088
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
4869
5089
  ".ts",
4870
5090
  ".tsx",
@@ -4900,8 +5120,260 @@ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
4900
5120
  "tests",
4901
5121
  "test"
4902
5122
  ]);
4903
- var MAX_FILES = 2e3;
4904
- var MAX_FILE_BYTES = 512 * 1024;
5123
+ var SKIP_FILES = /\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$|\.min\.[cm]?js$/;
5124
+ var DEFAULT_BUDGET_MS = 6e4;
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
+ }
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) {
5140
+ const deadline2 = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS);
5141
+ const fromGit = gitFiles(root, deadline2);
5142
+ if (fromGit) {
5143
+ return {
5144
+ files: fromGit.tracked.filter(isCandidate),
5145
+ method: "git",
5146
+ truncated: false,
5147
+ unsearchable: fromGit.unsearchable.filter(isCandidate)
5148
+ };
5149
+ }
5150
+ const files = [];
5151
+ const truncated = walk(root, root, files, deadline2);
5152
+ return { files, method: "walk", truncated, unsearchable: [] };
5153
+ }
5154
+ function gitFiles(root, deadline2) {
5155
+ const output = git(
5156
+ root,
5157
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
5158
+ deadline2
5159
+ );
5160
+ if (output === null) return null;
5161
+ const tracked = new Set(output.split("\0").filter((entry) => entry !== ""));
5162
+ const unsearchable = /* @__PURE__ */ new Set();
5163
+ const submodules = git(root, ["ls-files", "-z", "--cached", "--recurse-submodules"], deadline2);
5164
+ if (submodules !== null) {
5165
+ for (const entry of submodules.split("\0")) {
5166
+ if (entry !== "" && !tracked.has(entry)) unsearchable.add(entry);
5167
+ }
5168
+ }
5169
+ for (const file of ignoredSources(root, deadline2)) {
5170
+ if (!tracked.has(file)) unsearchable.add(file);
5171
+ }
5172
+ return { tracked: [...tracked, ...unsearchable], unsearchable: [...unsearchable] };
5173
+ }
5174
+ function ignoredSources(root, deadline2) {
5175
+ const output = git(
5176
+ root,
5177
+ ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"],
5178
+ deadline2
5179
+ );
5180
+ if (output === null) return [];
5181
+ const files = [];
5182
+ for (const entry of output.split("\0")) {
5183
+ if (entry === "") continue;
5184
+ if (entry.endsWith("/")) {
5185
+ const name = entry.replace(/\/$/, "").split("/").pop() ?? "";
5186
+ if (SKIP_DIRECTORIES.has(name) || name.startsWith(".")) continue;
5187
+ const nested = [];
5188
+ walk(root, path7.join(root, entry), nested, deadline2);
5189
+ files.push(...nested);
5190
+ continue;
5191
+ }
5192
+ if (isCandidate(entry)) files.push(entry);
5193
+ }
5194
+ return files;
5195
+ }
5196
+ function git(root, args, deadline2) {
5197
+ const timeout = deadline2 - Date.now();
5198
+ if (timeout <= 0) return null;
5199
+ try {
5200
+ return execFileSync("git", ["-c", "core.quotepath=off", ...args], {
5201
+ cwd: root,
5202
+ encoding: "utf8",
5203
+ timeout,
5204
+ maxBuffer: 512 * 1024 * 1024,
5205
+ stdio: ["ignore", "pipe", "ignore"]
5206
+ });
5207
+ } catch {
5208
+ return null;
5209
+ }
5210
+ }
5211
+ function isCandidate(file) {
5212
+ if (!SOURCE_EXTENSIONS.has(path7.extname(file))) return false;
5213
+ const segments = file.split(/[/\\]/);
5214
+ const name = segments[segments.length - 1] ?? "";
5215
+ if (SKIP_FILES.test(name)) return false;
5216
+ return !segments.slice(0, -1).some((segment) => SKIP_DIRECTORIES.has(segment) || segment.startsWith("."));
5217
+ }
5218
+ function walk(root, dir, out2, deadline2) {
5219
+ let entries;
5220
+ try {
5221
+ entries = readdirSyncSafe(dir);
5222
+ } catch {
5223
+ return false;
5224
+ }
5225
+ for (const entry of entries) {
5226
+ if (Date.now() > deadline2) return true;
5227
+ if (entry.startsWith(".")) continue;
5228
+ const full = path7.join(dir, entry);
5229
+ let directory;
5230
+ try {
5231
+ directory = statSync4(full).isDirectory();
5232
+ } catch {
5233
+ continue;
5234
+ }
5235
+ if (directory) {
5236
+ if (SKIP_DIRECTORIES.has(entry)) continue;
5237
+ if (walk(root, full, out2, deadline2)) return true;
5238
+ continue;
5239
+ }
5240
+ const relative = path7.relative(root, full);
5241
+ if (isCandidate(relative)) out2.push(relative);
5242
+ }
5243
+ return false;
5244
+ }
5245
+ function readdirSyncSafe(dir) {
5246
+ return readdirSync3(dir).sort();
5247
+ }
5248
+ function shortlist(root, index, anchors, options = {}) {
5249
+ const considered = index.files.length;
5250
+ if (anchors.length === 0 || considered === 0) {
5251
+ return { files: [], method: "in-process", considered, truncated: false };
5252
+ }
5253
+ const deadline2 = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS);
5254
+ const allowed = new Set(index.files);
5255
+ const reached = index.unsearchable.filter((file) => allowed.has(file));
5256
+ const extra = reached.length > 0 ? inProcessSearch(root, reached, anchors, deadline2, reached.length) : null;
5257
+ const combine = (files, method) => ({
5258
+ files: [.../* @__PURE__ */ new Set([...files, ...extra?.files ?? []])],
5259
+ method,
5260
+ considered,
5261
+ truncated: extra?.truncated ?? false
5262
+ });
5263
+ if (index.method === "git") {
5264
+ const matched = searchWith(
5265
+ "git",
5266
+ root,
5267
+ ["-c", "core.quotepath=off", "grep", "--no-color", "-l", "-I", "-z", "-F", "--untracked"],
5268
+ anchors,
5269
+ deadline2
5270
+ );
5271
+ if (matched) {
5272
+ return combine(
5273
+ matched.filter((file) => allowed.has(file)),
5274
+ "git-grep"
5275
+ );
5276
+ }
5277
+ }
5278
+ const ripgrep = searchWith(
5279
+ "rg",
5280
+ root,
5281
+ ["--files-with-matches", "--fixed-strings", "--null", "--no-messages"],
5282
+ anchors,
5283
+ deadline2
5284
+ );
5285
+ if (ripgrep) {
5286
+ return combine(
5287
+ ripgrep.map(normalise).filter((file) => allowed.has(file)),
5288
+ "ripgrep"
5289
+ );
5290
+ }
5291
+ return inProcessSearch(root, index.files, anchors, deadline2, considered);
5292
+ }
5293
+ function searchWith(command, root, args, anchors, deadline2) {
5294
+ const timeout = deadline2 - Date.now();
5295
+ if (timeout <= 0) return null;
5296
+ let directory = null;
5297
+ try {
5298
+ directory = mkdtempSync(path7.join(tmpdir(), "veefore-search-"));
5299
+ const patterns = path7.join(directory, "anchors.txt");
5300
+ writeFileSync4(patterns, `${anchors.join("\n")}
5301
+ `);
5302
+ const output = execFileSync(command, [...args, "-f", patterns], {
5303
+ cwd: root,
5304
+ encoding: "utf8",
5305
+ timeout,
5306
+ maxBuffer: 512 * 1024 * 1024,
5307
+ stdio: ["ignore", "pipe", "ignore"]
5308
+ });
5309
+ return output.split("\0").filter((entry) => entry !== "");
5310
+ } catch (error) {
5311
+ const status = error.status;
5312
+ if (status === 1) return [];
5313
+ return null;
5314
+ } finally {
5315
+ if (directory) rmSync3(directory, { recursive: true, force: true });
5316
+ }
5317
+ }
5318
+ function inProcessSearch(root, files, anchors, deadline2, considered) {
5319
+ const needles = anchors.map((anchor) => Buffer.from(anchor, "utf8"));
5320
+ const matched = [];
5321
+ let truncated = false;
5322
+ for (const [position, file] of files.entries()) {
5323
+ if ((position & 63) === 0 && Date.now() > deadline2) {
5324
+ truncated = true;
5325
+ break;
5326
+ }
5327
+ let buffer;
5328
+ try {
5329
+ buffer = readFileSync6(path7.join(root, file));
5330
+ } catch {
5331
+ continue;
5332
+ }
5333
+ const nul = buffer.indexOf(0);
5334
+ if (nul >= 0 && nul < 8192) continue;
5335
+ if (needles.some((needle) => buffer.includes(needle))) matched.push(file);
5336
+ }
5337
+ return { files: matched, method: "in-process", considered, truncated };
5338
+ }
5339
+ function normalise(file) {
5340
+ return file.startsWith(`.${path7.sep}`) || file.startsWith("./") ? file.slice(2) : file;
5341
+ }
5342
+ function tooBigToParse(text) {
5343
+ if (text.length > MAX_PARSE_BYTES) return true;
5344
+ if (text.length < 128 * 1024) return false;
5345
+ const head = text.slice(0, 128 * 1024);
5346
+ const lines = head.split("\n").length;
5347
+ return head.length / lines > 500;
5348
+ }
5349
+ function readFiles(root, files) {
5350
+ const out2 = [];
5351
+ for (const file of files) {
5352
+ let text;
5353
+ try {
5354
+ text = readFileSync6(path7.join(root, file), "utf8");
5355
+ } catch {
5356
+ continue;
5357
+ }
5358
+ if (tooBigToParse(text)) continue;
5359
+ out2.push({ file, text });
5360
+ }
5361
+ return out2;
5362
+ }
5363
+
5364
+ // src/scan.ts
5365
+ var MINOR_AMOUNT_PARAMETER = /["']?\b(amount_minor|amountMinor|amount_cents|amountCents|amount_in_cents|amountInCents|amount_minor_units|amountMinorUnits)\b["']?\s*[:=(]/;
5366
+ var CURRENCY_PARAMETER = /["']?\b(currency_code|currencyCode|currency)\b["']?\s*[:=(]/;
5367
+ function inferMoneyMapping(definition) {
5368
+ const amount = MINOR_AMOUNT_PARAMETER.exec(definition)?.[1] ?? null;
5369
+ const currency = CURRENCY_PARAMETER.exec(definition)?.[1] ?? null;
5370
+ if (!amount && !currency) return null;
5371
+ return {
5372
+ amountParameter: amount,
5373
+ amountUnit: amount ? "MINOR" : null,
5374
+ currencyParameter: currency
5375
+ };
5376
+ }
4905
5377
  var MODEL_HOSTS = [
4906
5378
  "api.openai.com",
4907
5379
  "api.anthropic.com",
@@ -4918,24 +5390,74 @@ var MODEL_SECRET = /^(OPENAI|ANTHROPIC|GOOGLE|GEMINI|AZURE_OPENAI|MISTRAL|GROQ|C
4918
5390
  var SECRET_NAME = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|DSN)(_|$)|^DATABASE_URL$|_CONNECTION_STRING$/;
4919
5391
  var NOT_SECRET = /^(VEEFORE_|NODE_|NEXT_PUBLIC_|VITE_|PUBLIC_|NPM_)/;
4920
5392
  function listSourceFiles(root = process.cwd()) {
4921
- const files = [];
4922
- walk(root, root, files);
4923
- return files;
5393
+ return listSources(root).files;
5394
+ }
5395
+ function listSources(root = process.cwd()) {
5396
+ const index = indexFiles(root);
5397
+ return { files: index.files, truncated: index.truncated, method: index.method };
5398
+ }
5399
+ function discoverAgents(root = process.cwd(), reuse) {
5400
+ const scan = reuse ?? scanProject(root);
5401
+ const index = indexFiles(root);
5402
+ const byDirectory = /* @__PURE__ */ new Map();
5403
+ for (const tool of scan.tools) {
5404
+ const directory = nearestManifest(root, tool.location.file);
5405
+ const existing = byDirectory.get(directory);
5406
+ const agent = existing ?? {
5407
+ directory,
5408
+ name: readProjectName(path8.join(root, directory)),
5409
+ files: [],
5410
+ tools: []
5411
+ };
5412
+ if (!agent.files.includes(tool.location.file)) agent.files.push(tool.location.file);
5413
+ agent.tools.push(tool);
5414
+ byDirectory.set(directory, agent);
5415
+ }
5416
+ const known = new Set(scan.tools.map((tool) => tool.location.file));
5417
+ const agentFiles = shortlist(root, index, [...AGENT_ANCHORS]);
5418
+ const unreadable = [];
5419
+ for (const { file, text } of readFiles(root, agentFiles.files)) {
5420
+ if (known.has(file)) continue;
5421
+ const python = file.endsWith(".py");
5422
+ if ((python ? findPythonTools(text) : findJsTools(text)).length > 0) continue;
5423
+ const hint = /\btools?\s*[:=]|\btool_calls\b|\btoolCalls\b|\bfunction_call\b/.exec(text);
5424
+ if (!hint) continue;
5425
+ unreadable.push({
5426
+ file,
5427
+ line: lineLocator(text)(hint.index),
5428
+ reason: `passes tools to a model, but none could be read from this file`
5429
+ });
5430
+ }
5431
+ return {
5432
+ agents: [...byDirectory.values()].sort((a, b) => a.directory.localeCompare(b.directory)),
5433
+ unreadable,
5434
+ filesScanned: scan.filesScanned,
5435
+ truncated: scan.truncated || agentFiles.truncated
5436
+ };
5437
+ }
5438
+ function nearestManifest(root, file) {
5439
+ const MANIFESTS = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml", "setup.py"];
5440
+ let dir = path8.dirname(file);
5441
+ for (let depth = 0; depth < 32; depth += 1) {
5442
+ const absolute = path8.join(root, dir);
5443
+ if (MANIFESTS.some((manifest) => existsSync6(path8.join(absolute, manifest)))) {
5444
+ return dir === "" ? "." : dir;
5445
+ }
5446
+ const parent = path8.dirname(dir);
5447
+ if (parent === dir || dir === "." || dir === "") break;
5448
+ dir = parent;
5449
+ }
5450
+ return ".";
4924
5451
  }
4925
5452
  function scanProject(root = process.cwd()) {
4926
5453
  const framework = detectFramework(root);
4927
- const files = [];
4928
- const truncated = walk(root, root, files);
5454
+ const index = indexFiles(root);
5455
+ const candidates = shortlist(root, index, [...SCAN_ANCHORS]);
5456
+ const truncated = index.truncated || candidates.truncated;
4929
5457
  const tools = [];
4930
5458
  const hosts = /* @__PURE__ */ new Map();
4931
5459
  const secrets = /* @__PURE__ */ new Map();
4932
- for (const relative of files) {
4933
- let text;
4934
- try {
4935
- text = readFileSync6(path7.join(root, relative), "utf8");
4936
- } catch {
4937
- continue;
4938
- }
5460
+ for (const { file: relative, text } of readFiles(root, candidates.files)) {
4939
5461
  const python = relative.endsWith(".py");
4940
5462
  const lineOf = lineLocator(text);
4941
5463
  const fileHosts = findHosts(text).map((hit) => ({ ...hit, line: lineOf(hit.index) }));
@@ -4951,7 +5473,7 @@ function scanProject(root = process.cwd()) {
4951
5473
  found.forEach((hit, position) => {
4952
5474
  const end = found[position + 1]?.index ?? text.length;
4953
5475
  const start = hit.index;
4954
- const inRegion = (index) => index >= start && index < end;
5476
+ const inRegion = (index2) => index2 >= start && index2 < end;
4955
5477
  const ownHosts = unique(fileHosts.filter((h) => inRegion(h.index)).map((h) => h.host));
4956
5478
  const ownSecrets = unique(
4957
5479
  fileSecrets.filter((s) => inRegion(s.index) && classifySecret(s.name) === "service").map((s) => s.name)
@@ -4979,7 +5501,9 @@ function scanProject(root = process.cwd()) {
4979
5501
  projectName: readProjectName(root),
4980
5502
  framework,
4981
5503
  entryPoints: findEntryPoints(root),
4982
- filesScanned: files.length,
5504
+ filesScanned: index.files.length,
5505
+ filesParsed: candidates.files.length,
5506
+ searchMethod: candidates.method,
4983
5507
  truncated,
4984
5508
  tools: dedupeTools(tools),
4985
5509
  hosts: [...hosts.entries()].map(([host, locations]) => ({ host, locations })).sort((a, b) => a.host.localeCompare(b.host)),
@@ -4997,7 +5521,7 @@ function combineRisk(inferred, declared) {
4997
5521
  function findJsTools(text) {
4998
5522
  const hits = [];
4999
5523
  const importsFrom = (pkg) => new RegExp(
5000
- `from\\s+['"]${escapeRegex(pkg)}(/[^'"]*)?['"]|require\\(\\s*['"]${escapeRegex(pkg)}`
5524
+ `from\\s+['"]${escapeRegex2(pkg)}(/[^'"]*)?['"]|require\\(\\s*['"]${escapeRegex2(pkg)}`
5001
5525
  ).test(text);
5002
5526
  const vercel = importsFrom("ai");
5003
5527
  const langchain = importsFrom("@langchain/core") || importsFrom("langchain");
@@ -5053,18 +5577,8 @@ function findJsTools(text) {
5053
5577
  }
5054
5578
  }
5055
5579
  }
5056
- for (const match of text.matchAll(/type\s*:\s*['"]function['"]/g)) {
5057
- const index = match.index ?? 0;
5058
- const body = text.slice(index, index + 400);
5059
- const name = stringField(body, "name");
5060
- if (name) {
5061
- hits.push({
5062
- name,
5063
- source: "openai-function",
5064
- index,
5065
- description: stringField(body, "description")
5066
- });
5067
- }
5580
+ for (const hit of findFunctionToolSchemas(text)) {
5581
+ hits.push({ ...hit, source: "openai-function" });
5068
5582
  }
5069
5583
  addSchemaTools(hits, text, false);
5070
5584
  if (mcp) {
@@ -5128,13 +5642,78 @@ function findToolSchemas(text, python) {
5128
5642
  }
5129
5643
  return hits;
5130
5644
  }
5131
- function addSchemaTools(hits, text, python) {
5132
- const known = new Set(hits.map((hit) => toToolKey(hit.name)));
5133
- for (const hit of findToolSchemas(text, python)) {
5134
- const key = toToolKey(hit.name);
5135
- if (known.has(key)) continue;
5136
- known.add(key);
5137
- hits.push({ ...hit, source: "tool-schema" });
5645
+ function findFunctionToolSchemas(text) {
5646
+ const hits = [];
5647
+ const NAME = /\bname\s*:\s*(['"`])([\w.-]{1,80})\1/;
5648
+ for (const match of text.matchAll(/\btype\s*:\s*(['"`])function\1/g)) {
5649
+ const at = match.index ?? 0;
5650
+ const from = at + match[0].length;
5651
+ const sibling = /^[^{}]{0,400}?(?<![\w$.])function\s*:\s*\{/.exec(text.slice(from));
5652
+ if (sibling) {
5653
+ const open = from + sibling[0].length - 1;
5654
+ const close = matchingBrace(text, open);
5655
+ if (close > open) {
5656
+ const inner = text.slice(open, close + 1);
5657
+ const found = NAME.exec(inner);
5658
+ if (found) {
5659
+ hits.push({
5660
+ name: found[2],
5661
+ index: open + found.index,
5662
+ description: stringField(inner, "description")
5663
+ });
5664
+ continue;
5665
+ }
5666
+ }
5667
+ }
5668
+ const opener = text.lastIndexOf("{", at);
5669
+ const windows = [
5670
+ [
5671
+ Math.max(opener + 1, at - 400),
5672
+ opener < 0 ? "" : text.slice(Math.max(opener + 1, at - 400), at)
5673
+ ],
5674
+ [from, text.slice(from, from + 400).split(/[{}]/)[0] ?? ""]
5675
+ ];
5676
+ for (const [offset, window] of windows) {
5677
+ const found = NAME.exec(window);
5678
+ if (!found) continue;
5679
+ hits.push({
5680
+ name: found[2],
5681
+ index: offset + found.index,
5682
+ description: stringField(window, "description")
5683
+ });
5684
+ break;
5685
+ }
5686
+ }
5687
+ return hits;
5688
+ }
5689
+ function matchingBrace(text, open) {
5690
+ let depth = 0;
5691
+ for (let index = open; index < text.length; index += 1) {
5692
+ const char = text[index];
5693
+ if (char === '"' || char === "'" || char === "`") {
5694
+ const quote2 = char;
5695
+ index += 1;
5696
+ while (index < text.length && text[index] !== quote2) {
5697
+ if (text[index] === "\\") index += 1;
5698
+ index += 1;
5699
+ }
5700
+ continue;
5701
+ }
5702
+ if (char === "{") depth += 1;
5703
+ else if (char === "}") {
5704
+ depth -= 1;
5705
+ if (depth === 0) return index;
5706
+ }
5707
+ }
5708
+ return -1;
5709
+ }
5710
+ function addSchemaTools(hits, text, python) {
5711
+ const known = new Set(hits.map((hit) => toToolKey(hit.name)));
5712
+ for (const hit of findToolSchemas(text, python)) {
5713
+ const key = toToolKey(hit.name);
5714
+ if (known.has(key)) continue;
5715
+ known.add(key);
5716
+ hits.push({ ...hit, source: "tool-schema" });
5138
5717
  }
5139
5718
  }
5140
5719
  function objectLiterals(text, python) {
@@ -5419,52 +5998,21 @@ function toToolKey(name) {
5419
5998
  const safe = /^[a-z]/.test(key) ? key : `tool_${key}`;
5420
5999
  return safe.slice(0, 80) || "tool";
5421
6000
  }
5422
- function walk(root, dir, out2) {
5423
- let entries;
5424
- try {
5425
- entries = readdirSync3(dir).sort();
5426
- } catch {
5427
- return false;
5428
- }
5429
- for (const entry of entries) {
5430
- if (out2.length >= MAX_FILES) return true;
5431
- if (entry.startsWith(".") && entry !== ".") {
5432
- continue;
5433
- }
5434
- const full = path7.join(dir, entry);
5435
- let stats;
5436
- try {
5437
- stats = statSync4(full);
5438
- } catch {
5439
- continue;
5440
- }
5441
- if (stats.isDirectory()) {
5442
- if (SKIP_DIRECTORIES.has(entry)) continue;
5443
- if (walk(root, full, out2)) return true;
5444
- continue;
5445
- }
5446
- if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
5447
- if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
5448
- if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
5449
- out2.push(path7.relative(root, full));
5450
- }
5451
- return false;
5452
- }
5453
6001
  function readProjectName(root) {
5454
- const pkg = path7.join(root, "package.json");
6002
+ const pkg = path8.join(root, "package.json");
5455
6003
  if (existsSync6(pkg)) {
5456
6004
  try {
5457
- const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
6005
+ const name = JSON.parse(readFileSync7(pkg, "utf8")).name;
5458
6006
  if (typeof name === "string" && name.trim()) return name.replace(/^@[^/]+\//, "").trim();
5459
6007
  } catch {
5460
6008
  }
5461
6009
  }
5462
- const pyproject = path7.join(root, "pyproject.toml");
6010
+ const pyproject = path8.join(root, "pyproject.toml");
5463
6011
  if (existsSync6(pyproject)) {
5464
- const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
6012
+ const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync7(pyproject, "utf8"))?.[1];
5465
6013
  if (name) return name.trim();
5466
6014
  }
5467
- return path7.basename(root) || null;
6015
+ return path8.basename(root) || null;
5468
6016
  }
5469
6017
  function stringField(body, field) {
5470
6018
  const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
@@ -5506,7 +6054,7 @@ function push(map, key, value) {
5506
6054
  function unique(values) {
5507
6055
  return [...new Set(values)];
5508
6056
  }
5509
- function escapeRegex(value) {
6057
+ function escapeRegex2(value) {
5510
6058
  return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
5511
6059
  }
5512
6060
 
@@ -5519,7 +6067,7 @@ function planRewrite(root, files, toolKeys) {
5519
6067
  for (const file of files) {
5520
6068
  let before;
5521
6069
  try {
5522
- before = readFileSync7(path8.join(root, file), "utf8");
6070
+ before = readFileSync8(path9.join(root, file), "utf8");
5523
6071
  } catch {
5524
6072
  continue;
5525
6073
  }
@@ -5624,27 +6172,20 @@ function rewriteJs(source, wanted) {
5624
6172
  wrap3(key, match.index ?? 0, start, trimEnd(source, start, handler.end));
5625
6173
  }
5626
6174
  }
5627
- for (const match of source.matchAll(/type\s*:\s*['"]function['"]/g)) {
5628
- const name = /\bname\s*:\s*['"]([\w.-]{1,80})['"]/.exec(
5629
- source.slice(match.index ?? 0, (match.index ?? 0) + 400)
5630
- )?.[1];
5631
- if (!name || !wanted.has(toToolKey(name))) continue;
5632
- changes.push({
5633
- key: toToolKey(name),
5634
- line: lineOf(match.index ?? 0),
5635
- status: "manual",
5636
- reason: `this is only the schema; wrap the function your code calls for it, e.g. \`guard(${quote(toToolKey(name))}, yourFunction)\``
5637
- });
5638
- }
5639
- changes.push(...schemaOnly(source, false, wanted));
6175
+ changes.push(...schemaOnly(source, false, wanted, new Set(changes.map((c) => c.key))));
5640
6176
  const wrapped = edits.length > 0;
5641
6177
  let after = applyEdits(source, edits);
5642
6178
  if (wrapped) after = addJsImport(after, alias);
5643
6179
  return { after, changes: dedupeChanges(changes) };
5644
6180
  }
5645
6181
  function addJsImport(source, alias) {
5646
- if (existingGuardImport(source, "js")) return source;
5647
- const specifier = alias === "guard" ? "guard" : `guard as ${alias}`;
6182
+ return addJsSdkImport(source, "guard", alias);
6183
+ }
6184
+ function addJsSdkImport(source, name, alias) {
6185
+ if (existingImport(source, name, "js")) return source;
6186
+ const specifier = alias === name ? name : `${name} as ${alias}`;
6187
+ const joined = joinExistingSdkImport(source, specifier, alias, name);
6188
+ if (joined) return joined;
5648
6189
  const imports = [
5649
6190
  ...source.matchAll(
5650
6191
  /^import\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s*)?['"][^'"\n]+['"]\s*;?[^\S\n]*$/gm
@@ -5657,11 +6198,33 @@ function addJsImport(source, alias) {
5657
6198
  import { ${specifier} } from '@veefore/sdk';${source.slice(at)}`;
5658
6199
  }
5659
6200
  const cjs = /\brequire\(\s*['"][^'"]+['"]\s*\)/.test(source) && !/^\s*export\s/m.test(source);
5660
- const line = cjs ? `const { ${alias === "guard" ? "guard" : `guard: ${alias}`} } = require('@veefore/sdk');` : `import { ${specifier} } from '@veefore/sdk';`;
6201
+ const line = cjs ? `const { ${alias === name ? name : `${name}: ${alias}`} } = require('@veefore/sdk');` : `import { ${specifier} } from '@veefore/sdk';`;
5661
6202
  const prologue = /^(?:#![^\n]*\n)?(?:\s*(['"])use [a-z ]+\1;?[^\S\n]*\n)*/.exec(source)?.[0] ?? "";
5662
6203
  return `${source.slice(0, prologue.length)}${line}
5663
6204
  ${source.slice(prologue.length)}`;
5664
6205
  }
6206
+ function joinExistingSdkImport(source, specifier, alias, name) {
6207
+ const esm = /import\s*\{([^}]*)\}\s*from\s*(['"])@veefore\/sdk\2/.exec(source);
6208
+ if (esm) {
6209
+ const names = (esm[1] ?? "").trim();
6210
+ const replacement = esm[0].replace(
6211
+ `{${esm[1] ?? ""}}`,
6212
+ names === "" ? `{ ${specifier} }` : `{ ${names}, ${specifier} }`
6213
+ );
6214
+ return source.replace(esm[0], replacement);
6215
+ }
6216
+ const cjs = /const\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])@veefore\/sdk\2\s*\)/.exec(source);
6217
+ if (cjs) {
6218
+ const names = (cjs[1] ?? "").trim();
6219
+ const entry = alias === name ? name : `${name}: ${alias}`;
6220
+ const replacement = cjs[0].replace(
6221
+ `{${cjs[1] ?? ""}}`,
6222
+ names === "" ? `{ ${entry} }` : `{ ${names}, ${entry} }`
6223
+ );
6224
+ return source.replace(cjs[0], replacement);
6225
+ }
6226
+ return null;
6227
+ }
5665
6228
  function rewritePython(source, wanted) {
5666
6229
  const changes = [];
5667
6230
  const lines = source.split("\n");
@@ -5704,7 +6267,7 @@ function rewritePython(source, wanted) {
5704
6267
  reason: `decorate the function passed as func= with @guard(${JSON.stringify(toToolKey(name))})`
5705
6268
  });
5706
6269
  }
5707
- changes.push(...schemaOnly(source, true, wanted));
6270
+ changes.push(...schemaOnly(source, true, wanted, new Set(changes.map((c) => c.key))));
5708
6271
  if (inserts.length === 0) return { after: source, changes: dedupeChanges(changes) };
5709
6272
  const out2 = [...lines];
5710
6273
  for (const insert of [...inserts].sort((a, b) => b.before - a.before)) {
@@ -5914,9 +6477,63 @@ function topLevelString(text, open, close, name) {
5914
6477
  const literal = /^(['"`])([\w .-]{1,80})\1$/.exec(text.slice(value.start, value.end));
5915
6478
  return literal?.[2] ?? null;
5916
6479
  }
5917
- var DISPATCH_NAME = /^(?:execute|run|call|dispatch|handle|invoke|perform)_?(?:tool|tool_?call|function_?call)s?$/i;
6480
+ var DISPATCH_NAME = /^(?:execute|run|call|dispatch|handle|invoke|perform)[A-Za-z0-9_]*?_?(?:tool|tool_?call|function_?call)s?$/i;
5918
6481
  var KEY_PROPERTIES = ["tool", "name", "toolName", "tool_name"];
5919
6482
  var ARG_PROPERTIES = ["params", "args", "arguments", "input", "parameters"];
6483
+ function findRunSites(files, read, toolNames) {
6484
+ const sites = /* @__PURE__ */ new Map();
6485
+ if (toolNames.length === 0) return sites;
6486
+ const patterns = (name) => {
6487
+ const quoted = `['"\`]${escapeForRegex(name)}['"\`]`;
6488
+ return [
6489
+ // `switch (call.tool) { case 'x':` — unmistakable.
6490
+ new RegExp(`case\\s+${quoted}`),
6491
+ // `call.name === 'x'`: the key read off the call being run, which is
6492
+ // what a dispatch does and what a view's own state does not.
6493
+ new RegExp(
6494
+ `\\.\\s*[A-Za-z_$][\\w$]*\\s*={2,3}\\s*${quoted}|${quoted}\\s*={2,3}\\s*[A-Za-z_$][\\w$]*\\s*\\.`
6495
+ ),
6496
+ // `handlers['x']`, `executors['x']`
6497
+ new RegExp(`\\[\\s*${quoted}\\s*\\]`),
6498
+ // `'x': async (args) => …`, a map of handlers by key.
6499
+ new RegExp(`${quoted}\\s*:\\s*(?:async\\b|function\\b|\\(|[\\w$]+\\s*=>)`),
6500
+ // Any other comparison against the key. `!==` is deliberately not one:
6501
+ // a tool is not run where its name is ruled out, and taking the first
6502
+ // of those sent the instruction to a view that merely checks which
6503
+ // tool is selected.
6504
+ new RegExp(`(?<![!<>=])={2,3}\\s*${quoted}|${quoted}\\s*={2,3}`)
6505
+ ];
6506
+ };
6507
+ const best = /* @__PURE__ */ new Map();
6508
+ for (const file of files) {
6509
+ const source = read(file);
6510
+ if (source === null) continue;
6511
+ const lineOf = lineLocator2(source);
6512
+ for (const name of toolNames) {
6513
+ if (!source.includes(name)) continue;
6514
+ for (const [rank, pattern] of patterns(name).entries()) {
6515
+ const found = pattern.exec(source);
6516
+ if (!found) continue;
6517
+ const current = best.get(name);
6518
+ if (current && current.rank <= rank) break;
6519
+ const at = found.index;
6520
+ const start = source.lastIndexOf("\n", at) + 1;
6521
+ const end = source.indexOf("\n", at);
6522
+ best.set(name, {
6523
+ rank,
6524
+ site: {
6525
+ file,
6526
+ line: lineOf(at),
6527
+ code: source.slice(start, end < 0 ? source.length : end).trim().slice(0, 160)
6528
+ }
6529
+ });
6530
+ break;
6531
+ }
6532
+ }
6533
+ }
6534
+ for (const [name, entry] of best) sites.set(name, entry.site);
6535
+ return sites;
6536
+ }
5920
6537
  function planDispatch(files, read, toolNames) {
5921
6538
  let best = null;
5922
6539
  for (const file of files) {
@@ -5938,27 +6555,36 @@ function planDispatch(files, read, toolNames) {
5938
6555
  };
5939
6556
  }
5940
6557
  }
5941
- if (!best || best.fn === null) {
5942
- best = { file, line: lineOf(found.index), score: Number.MAX_SAFE_INTEGER, fn: found.name };
5943
- }
6558
+ best ??= { file, line: lineOf(found.index), fn: found.name };
5944
6559
  }
5945
- const named = toolNames.filter(
5946
- (name) => new RegExp(
5947
- `['"\`]${escapeForRegex(name)}['"\`]\\s*[:)]|case\\s+['"\`]${escapeForRegex(name)}['"\`]`
5948
- ).test(source)
5949
- ).length;
5950
- if (named >= 2 && (!best || named > best.score)) {
5951
- const first = toolNames.map((name) => source.search(new RegExp(`case\\s+['"\`]${escapeForRegex(name)}['"\`]`))).filter((index) => index >= 0).sort((a, b) => a - b)[0];
5952
- best = { file, line: first === void 0 ? 1 : lineOf(first), score: named, fn: null };
6560
+ }
6561
+ const sites = findRunSites(files, read, toolNames);
6562
+ if (!best) {
6563
+ const byFile = /* @__PURE__ */ new Map();
6564
+ for (const site of sites.values()) {
6565
+ byFile.set(site.file, [...byFile.get(site.file) ?? [], site]);
5953
6566
  }
6567
+ const chosen = [...byFile.entries()].sort(
6568
+ (a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])
6569
+ )[0];
6570
+ if (!chosen) return null;
6571
+ const line = Math.min(...chosen[1].map((site) => site.line));
6572
+ return {
6573
+ file: chosen[0],
6574
+ line,
6575
+ functionName: null,
6576
+ after: null,
6577
+ reason: "this file runs the tools by name",
6578
+ sites
6579
+ };
5954
6580
  }
5955
- if (!best) return null;
5956
6581
  return {
5957
6582
  file: best.file,
5958
6583
  line: best.line,
5959
6584
  functionName: best.fn,
5960
6585
  after: null,
5961
- reason: best.fn ? `${best.fn}() runs the tools, but where to put the guard inside it is not certain` : "this file runs the tools by name"
6586
+ reason: `${best.fn}() runs the tools, but where to put the guard inside it is not certain`,
6587
+ sites
5962
6588
  };
5963
6589
  }
5964
6590
  function dispatchFunctions(source, python) {
@@ -6031,13 +6657,396 @@ function guardDispatch(source, fn, typescript) {
6031
6657
  );
6032
6658
  return { after, at: target.start };
6033
6659
  }
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
+ }
6840
+ function planCheckpoints(files, read, toolKeys) {
6841
+ const plans = [];
6842
+ const outstanding = new Set(toolKeys);
6843
+ if (outstanding.size === 0) return plans;
6844
+ for (const file of files) {
6845
+ if (outstanding.size === 0) break;
6846
+ if (file.endsWith(".py") || !JS_EXTENSIONS.test(file)) continue;
6847
+ const source = read(file);
6848
+ if (source === null) continue;
6849
+ const present = [...outstanding].filter(
6850
+ (key) => source.includes(key) || source.includes(camelOf(key))
6851
+ );
6852
+ if (present.length === 0) continue;
6853
+ const structure = readStructure(source);
6854
+ const lineOf = lineLocator2(source);
6855
+ const alias = chooseAliasFor(source, "checkpoint", "js");
6856
+ const sites = /* @__PURE__ */ new Map();
6857
+ for (const key of present) {
6858
+ const site = checkpointSite(source, key, structure);
6859
+ if (!site) continue;
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;
6870
+ const keyExpression = many && site.keyExpression ? site.keyExpression : quote(keys[0] ?? "");
6871
+ if (many && !site.keyExpression) continue;
6872
+ const args = site.argsExpression ? `, ${site.argsExpression}` : "";
6873
+ edits.push({
6874
+ start: at,
6875
+ end: at,
6876
+ text: `
6877
+ ${blockIndent(source, at)}await ${alias}(${keyExpression}${args});`
6878
+ });
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
+ }
6888
+ }
6889
+ if (edits.length === 0) continue;
6890
+ const after = addJsSdkImport(applyEdits(source, edits), "checkpoint", alias);
6891
+ plans.push({ file, before: source, after, placed });
6892
+ }
6893
+ return plans;
6894
+ }
6895
+ function checkpointSite(source, key, structure) {
6896
+ const quoted = `['"\`]${escapeForRegex(key)}['"\`]`;
6897
+ const usable = (at) => awaitAllowed(structure, at) === true;
6898
+ const bodyAfter = (close) => {
6899
+ const brace = /^\s*\{/.exec(source.slice(close + 1, close + 200));
6900
+ return brace ? close + 1 + brace[0].length : null;
6901
+ };
6902
+ for (const match of source.matchAll(new RegExp(`\\bcase\\s+${quoted}\\s*:`, "g"))) {
6903
+ const colon = (match.index ?? 0) + match[0].length;
6904
+ const braced = /^\s*\{/.exec(source.slice(colon));
6905
+ const at = braced ? colon + braced[0].length : colon;
6906
+ if (!usable(at)) continue;
6907
+ const discriminant = switchKey(source, match.index ?? 0);
6908
+ return {
6909
+ at,
6910
+ shape: "case",
6911
+ argsExpression: discriminant ? argumentsOf(source, discriminant.object) : null,
6912
+ keyExpression: discriminant?.expression ?? null
6913
+ };
6914
+ }
6915
+ for (const match of source.matchAll(new RegExp(quoted, "g"))) {
6916
+ const at = match.index ?? 0;
6917
+ if (!positiveComparison(source, at)) continue;
6918
+ const group = enclosing(structure.parens, at).find(
6919
+ (candidate) => BRANCH_KEYWORDS.has(candidate.name)
6920
+ );
6921
+ if (!group) continue;
6922
+ const insertAt = bodyAfter(group.close);
6923
+ if (insertAt === null || !usable(insertAt)) continue;
6924
+ const compared = comparedExpression(source, at);
6925
+ const loop = group.name === "for" ? loopVariable(source, group.open) : null;
6926
+ const subject = loop ?? compared?.object ?? null;
6927
+ return {
6928
+ at: insertAt,
6929
+ shape: group.name === "if" ? "branch" : "loop",
6930
+ argsExpression: subject ? argumentsOf(source, subject) : enclosingArguments(source, structure, insertAt),
6931
+ keyExpression: loop ? `${loop}.name` : compared?.expression ?? null
6932
+ };
6933
+ }
6934
+ const declaration = new RegExp(
6935
+ `\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=\\n]{0,120})?=[^;\\n]{0,400}?${quoted}[^;]{0,200}?;`,
6936
+ "g"
6937
+ );
6938
+ for (const match of source.matchAll(declaration)) {
6939
+ const name = match[1];
6940
+ const end = (match.index ?? 0) + match[0].length;
6941
+ const block = innermost(structure.blocks, end);
6942
+ const limit = block ? block.close : source.length;
6943
+ const head = new RegExp(`\\bif\\s*\\(\\s*${escapeForRegex(name)}\\b`).exec(
6944
+ source.slice(end, limit)
6945
+ );
6946
+ if (!head) continue;
6947
+ const parenOpen = end + (head.index ?? 0) + head[0].indexOf("(");
6948
+ const group = structure.parens.find((candidate) => candidate.open === parenOpen);
6949
+ if (!group) continue;
6950
+ const insertAt = bodyAfter(group.close);
6951
+ if (insertAt === null || !usable(insertAt)) continue;
6952
+ return {
6953
+ at: insertAt,
6954
+ shape: "found",
6955
+ argsExpression: argumentsOf(source, name),
6956
+ keyExpression: `${name}.name`
6957
+ };
6958
+ }
6959
+ const handler = new RegExp(
6960
+ `${quoted}\\s*:\\s*(?:async\\s*)?\\(([^)]{0,200})\\)\\s*(?::[^=>{]{0,120})?=>\\s*\\{`,
6961
+ "g"
6962
+ );
6963
+ for (const match of source.matchAll(handler)) {
6964
+ const at = (match.index ?? 0) + match[0].length;
6965
+ if (!usable(at)) continue;
6966
+ return {
6967
+ at,
6968
+ shape: "handler",
6969
+ argsExpression: parameterExpression(match[1] ?? ""),
6970
+ keyExpression: null
6971
+ };
6972
+ }
6973
+ const named = new RegExp(
6974
+ `\\basync\\s+function\\s+(?:${escapeForRegex(camelOf(key))}|${escapeForRegex(key)})\\s*\\(`,
6975
+ "g"
6976
+ );
6977
+ for (const match of source.matchAll(named)) {
6978
+ const parenOpen = (match.index ?? 0) + match[0].length - 1;
6979
+ const group = structure.parens.find((candidate) => candidate.open === parenOpen);
6980
+ if (!group) continue;
6981
+ const body = structure.blocks.filter((block) => block.fn && block.open > group.close).sort((a, b) => a.open - b.open)[0];
6982
+ if (!body) continue;
6983
+ return {
6984
+ at: body.open + 1,
6985
+ shape: "function",
6986
+ argsExpression: parameterExpression(source.slice(group.open + 1, group.close)),
6987
+ keyExpression: null
6988
+ };
6989
+ }
6990
+ return null;
6991
+ }
6992
+ function positiveComparison(source, at) {
6993
+ const before = source.slice(Math.max(0, at - 96), at);
6994
+ return /(?<![!<>])={2,3}\s*$/.test(before);
6995
+ }
6996
+ function comparedExpression(source, at) {
6997
+ const before = source.slice(Math.max(0, at - 160), at);
6998
+ const match = /([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*={2,3}\s*$/.exec(before);
6999
+ if (!match) return null;
7000
+ return { object: match[1], expression: `${match[1]}.${match[2]}` };
7001
+ }
7002
+ function switchKey(source, at) {
7003
+ const before = source.slice(Math.max(0, at - 4e3), at);
7004
+ const found = [
7005
+ ...before.matchAll(/\bswitch\s*\(\s*([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)/g)
7006
+ ];
7007
+ const last = found[found.length - 1];
7008
+ return last ? { object: last[1], expression: `${last[1]}.${last[2]}` } : null;
7009
+ }
7010
+ function loopVariable(source, parenOpen) {
7011
+ return /^\s*\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\b/.exec(
7012
+ source.slice(parenOpen, parenOpen + 120)
7013
+ )?.[1] ?? null;
7014
+ }
7015
+ function parameterExpression(parameters) {
7016
+ const names = parameters.split(",").map((part) => /^\s*(?:\.\.\.)?([A-Za-z_$][\w$]*)/.exec(part)?.[1]).filter((name) => name !== void 0);
7017
+ const named = names.find((name) => CHECKPOINT_ARG_PROPERTIES.includes(name));
7018
+ if (named) return named;
7019
+ return names.length === 1 ? names[0] : null;
7020
+ }
7021
+ function camelOf(key) {
7022
+ return key.replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase());
7023
+ }
7024
+ function argumentsOf(source, object) {
7025
+ for (const property of CHECKPOINT_ARG_PROPERTIES) {
7026
+ const pattern = new RegExp(`\\b${escapeForRegex(object)}\\s*(?:\\?\\.|\\.)\\s*${property}\\b`);
7027
+ if (pattern.test(source)) return `${object}.${property}`;
7028
+ }
7029
+ return null;
7030
+ }
7031
+ function blockIndent(source, at) {
7032
+ const lineEnd = source.indexOf("\n", at);
7033
+ if (lineEnd >= 0) {
7034
+ const next = /^([ \t]*)\S/.exec(source.slice(lineEnd + 1, lineEnd + 200));
7035
+ if (next && !source.slice(lineEnd + 1).trimStart().startsWith("}"))
7036
+ return next[1];
7037
+ }
7038
+ const lineStart = source.lastIndexOf("\n", at) + 1;
7039
+ return `${/^[ \t]*/.exec(source.slice(lineStart, at))?.[0] ?? ""} `;
7040
+ }
6034
7041
  function unguardJs(source) {
6035
- const imported = jsGuardImport(source);
6036
- if (!imported) return { after: source, removed: 0, leftovers: [] };
7042
+ const checkpoints = uncheckpointJs(source);
7043
+ const imported = jsGuardImport(checkpoints.after);
7044
+ if (!imported) return checkpoints;
7045
+ source = checkpoints.after;
6037
7046
  const { alias } = imported;
6038
7047
  const lineOf = lineLocator2(source);
6039
7048
  const edits = [];
6040
- const leftovers = [];
7049
+ const leftovers = [...checkpoints.leftovers];
6041
7050
  const calls = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`, "g");
6042
7051
  for (const match of source.matchAll(calls)) {
6043
7052
  const start = match.index ?? 0;
@@ -6076,14 +7085,62 @@ function unguardJs(source) {
6076
7085
  }
6077
7086
  let after = applyEdits(source, edits);
6078
7087
  const stillUsed = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`).test(after);
6079
- if (!stillUsed) after = removeJsGuardImport(after);
6080
- return { after, removed: edits.length, leftovers };
7088
+ if (!stillUsed) after = removeJsSdkImport(after, "guard");
7089
+ return { after, removed: edits.length + checkpoints.removed, leftovers };
7090
+ }
7091
+ function uncheckpointJs(source) {
7092
+ const alias = existingImport(source, "checkpoint", "js");
7093
+ if (!alias) return { after: source, removed: 0, leftovers: [] };
7094
+ const lineOf = lineLocator2(source);
7095
+ const leftovers = [];
7096
+ let removed = 0;
7097
+ const key = `(?:(['"\`])[a-z][a-z0-9_]*\\1|[A-Za-z_$][\\w$]*(?:\\s*(?:\\?\\.|\\.)\\s*[A-Za-z_$][\\w$]*)+)`;
7098
+ let after = source.replace(
7099
+ new RegExp(
7100
+ `^[ \\t]*(?:await[ \\t]+)?${escapeForRegex(alias)}\\(\\s*${key}[^\\n]*\\)[ \\t]*;?[ \\t]*\\r?\\n`,
7101
+ "gm"
7102
+ ),
7103
+ () => {
7104
+ removed += 1;
7105
+ return "";
7106
+ }
7107
+ );
7108
+ for (const match of after.matchAll(
7109
+ new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`, "g")
7110
+ )) {
7111
+ leftovers.push({
7112
+ line: lineOf(match.index ?? 0),
7113
+ text: after.slice(match.index ?? 0, (match.index ?? 0) + 80).split("\n")[0] ?? ""
7114
+ });
7115
+ }
7116
+ if (leftovers.length === 0) after = removeJsSdkImport(after, "checkpoint");
7117
+ return { after, removed, leftovers };
6081
7118
  }
6082
7119
  function unguardPython(source) {
6083
7120
  const imported = /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)$/m.exec(source);
6084
7121
  if (!imported) return { after: source, removed: 0, leftovers: [] };
6085
- const spec = (imported[1] ?? "").replace(/[()]/g, "").split(",").map((part) => /^guard(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
6086
- if (!spec) return { after: source, removed: 0, leftovers: [] };
7122
+ const names = (imported[1] ?? "").replace(/[()]/g, "").split(",");
7123
+ const spec = names.map((part) => /^guard(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
7124
+ const gate = names.map((part) => /^checkpoint(?:\s+as\s+(\w+))?$/.exec(part.trim())).find((found) => found !== null);
7125
+ if (!spec && !gate) return { after: source, removed: 0, leftovers: [] };
7126
+ if (!spec) {
7127
+ const alias2 = gate?.[1] ?? "checkpoint";
7128
+ let removed2 = 0;
7129
+ let after2 = source.replace(
7130
+ new RegExp(
7131
+ `^[ \\t]*${escapeForRegex(alias2)}\\(\\s*(['"])[a-z][a-z0-9_]*\\1[^\\n]*\\)[ \\t]*\\r?\\n`,
7132
+ "gm"
7133
+ ),
7134
+ () => {
7135
+ removed2 += 1;
7136
+ return "";
7137
+ }
7138
+ );
7139
+ if (!new RegExp(`(?<![\\w.])${escapeForRegex(alias2)}\\s*\\(`).test(after2)) {
7140
+ after2 = removePythonImport(after2, alias2);
7141
+ }
7142
+ return { after: after2, removed: removed2, leftovers: [] };
7143
+ }
6087
7144
  const alias = spec[1] ?? "guard";
6088
7145
  const lineOf = lineLocator2(source);
6089
7146
  let removed = 0;
@@ -6122,18 +7179,20 @@ function unguardPython(source) {
6122
7179
  after = applyEdits(after, edits);
6123
7180
  removed += edits.length;
6124
7181
  const stillUsed = new RegExp(`(?<![\\w.])@?${escapeForRegex(alias)}\\s*\\(`).test(after);
6125
- if (!stillUsed) {
6126
- after = after.replace(
6127
- /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)\r?\n?/m,
6128
- (line, names) => {
6129
- const rest = names.replace(/[()]/g, "").split(",").map((part) => part.trim()).filter((part) => part && !/^guard(?:\s+as\s+\w+)?$/.test(part));
6130
- return rest.length === 0 ? "" : `from veefore import ${rest.join(", ")}
6131
- `;
6132
- }
6133
- );
6134
- }
7182
+ if (!stillUsed) after = removePythonImport(after, "guard");
6135
7183
  return { after, removed, leftovers };
6136
7184
  }
7185
+ function removePythonImport(source, name) {
7186
+ const target = new RegExp(`^${escapeForRegex(name)}(?:\\s+as\\s+\\w+)?$`);
7187
+ return source.replace(
7188
+ /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)\r?\n?/m,
7189
+ (_line, names) => {
7190
+ const rest = names.replace(/[()]/g, "").split(",").map((part) => part.trim()).filter((part) => part && !target.test(part));
7191
+ return rest.length === 0 ? "" : `from veefore import ${rest.join(", ")}
7192
+ `;
7193
+ }
7194
+ );
7195
+ }
6137
7196
  function arrowExpressionBody(fn) {
6138
7197
  let cursor = 0;
6139
7198
  if (fn.startsWith("(")) {
@@ -6168,29 +7227,28 @@ function jsGuardImport(source) {
6168
7227
  }
6169
7228
  return null;
6170
7229
  }
6171
- function removeJsGuardImport(source) {
6172
- const drop = (full, names, rebuild, isGuard) => {
6173
- const rest = names.split(",").map((part) => part.trim()).filter((part) => part && !isGuard.test(part));
7230
+ function removeJsSdkImport(source, name) {
7231
+ const escaped = escapeForRegex(name);
7232
+ const drop = (names, rebuild, target) => {
7233
+ const rest = names.split(",").map((part) => part.trim()).filter((part) => part && !target.test(part));
6174
7234
  return rest.length === 0 ? "" : rebuild(rest);
6175
7235
  };
6176
7236
  let after = source.replace(
6177
7237
  /^[ \t]*import\s*\{([^}]*)\}\s*from\s*(['"])@veefore\/sdk\2\s*;?[ \t]*\r?\n?/m,
6178
- (full, names, quote2) => drop(
6179
- full,
7238
+ (_full, names, quote2) => drop(
6180
7239
  names,
6181
7240
  (rest) => `import { ${rest.join(", ")} } from ${quote2}@veefore/sdk${quote2};
6182
7241
  `,
6183
- /^guard(\s+as\s+[\w$]+)?$/
7242
+ new RegExp(`^${escaped}(\\s+as\\s+[\\w$]+)?$`)
6184
7243
  )
6185
7244
  );
6186
7245
  after = after.replace(
6187
7246
  /^[ \t]*const\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])@veefore\/sdk\2\s*\)\s*;?[ \t]*\r?\n?/m,
6188
- (full, names, quote2) => drop(
6189
- full,
7247
+ (_full, names, quote2) => drop(
6190
7248
  names,
6191
7249
  (rest) => `const { ${rest.join(", ")} } = require(${quote2}@veefore/sdk${quote2});
6192
7250
  `,
6193
- /^guard(\s*:\s*[\w$]+)?$/
7251
+ new RegExp(`^${escaped}(\\s*:\\s*[\\w$]+)?$`)
6194
7252
  )
6195
7253
  );
6196
7254
  return after;
@@ -6201,22 +7259,35 @@ function escapeForRegex(value) {
6201
7259
  function isGuarded(value, alias) {
6202
7260
  return new RegExp(`^\\s*(${alias}|guard|veeforeGuard)\\(`).test(value);
6203
7261
  }
6204
- var JS_GUARD_IMPORT = /import\s*\{[^}]*\bguard\b(?:\s+as\s+([A-Za-z_$][\w$]*))?[^}]*\}\s*from\s*['"]@veefore\/sdk['"]/;
6205
- var CJS_GUARD_IMPORT = /\{[^}]*\bguard\b(?:\s*:\s*([A-Za-z_$][\w$]*))?[^}]*\}\s*=\s*require\(\s*['"]@veefore\/sdk['"]\s*\)/;
6206
- var PY_GUARD_IMPORT = /^from\s+veefore\s+import\s+[^\n]*\bguard\b(?:\s+as\s+(\w+))?/m;
6207
- function existingGuardImport(source, language) {
7262
+ function existingImport(source, name, language) {
7263
+ const escaped = escapeForRegex(name);
6208
7264
  if (language === "py") {
6209
- const match2 = PY_GUARD_IMPORT.exec(source);
6210
- return match2 ? match2[1] ?? "guard" : null;
6211
- }
6212
- const match = JS_GUARD_IMPORT.exec(source) ?? CJS_GUARD_IMPORT.exec(source);
6213
- return match ? match[1] ?? "guard" : null;
7265
+ const match2 = new RegExp(
7266
+ `^from\\s+veefore\\s+import\\s+[^\\n]*\\b${escaped}\\b(?:\\s+as\\s+(\\w+))?`,
7267
+ "m"
7268
+ ).exec(source);
7269
+ return match2 ? match2[1] ?? name : null;
7270
+ }
7271
+ const esm = new RegExp(
7272
+ `import\\s*\\{[^}]*\\b${escaped}\\b(?:\\s+as\\s+([A-Za-z_$][\\w$]*))?[^}]*\\}\\s*from\\s*['"]@veefore/sdk['"]`
7273
+ ).exec(source);
7274
+ const cjs = new RegExp(
7275
+ `\\{[^}]*\\b${escaped}\\b(?:\\s*:\\s*([A-Za-z_$][\\w$]*))?[^}]*\\}\\s*=\\s*require\\(\\s*['"]@veefore/sdk['"]\\s*\\)`
7276
+ ).exec(source);
7277
+ const match = esm ?? cjs;
7278
+ return match ? match[1] ?? name : null;
6214
7279
  }
6215
- function chooseAlias(source, language) {
6216
- const existing = existingGuardImport(source, language);
7280
+ function existingGuardImport(source, language) {
7281
+ return existingImport(source, "guard", language);
7282
+ }
7283
+ function chooseAliasFor(source, name, language) {
7284
+ const existing = existingImport(source, name, language);
6217
7285
  if (existing) return existing;
6218
- if (!/\bguard\b/.test(source)) return "guard";
6219
- return language === "py" ? "veefore_guard" : "veeforeGuard";
7286
+ if (!new RegExp(`\\b${escapeForRegex(name)}\\b`).test(source)) return name;
7287
+ return language === "py" ? `veefore_${name}` : `veefore${name[0]?.toUpperCase() ?? ""}${name.slice(1)}`;
7288
+ }
7289
+ function chooseAlias(source, language) {
7290
+ return chooseAliasFor(source, "guard", language);
6220
7291
  }
6221
7292
  function applyEdits(source, edits) {
6222
7293
  let out2 = source;
@@ -6225,14 +7296,22 @@ function applyEdits(source, edits) {
6225
7296
  }
6226
7297
  return out2;
6227
7298
  }
6228
- function schemaOnly(source, python, wanted) {
7299
+ var SCHEMA_ONLY_REASON = "this is only the schema; guard the code that runs the tool the model picked (often one dispatch function for every tool)";
7300
+ function schemaOnly(source, python, wanted, placed = /* @__PURE__ */ new Set()) {
6229
7301
  const lineOf = lineLocator2(source);
6230
- return findToolSchemas(source, python).filter((hit) => wanted.has(toToolKey(hit.name))).map((hit) => ({
6231
- key: toToolKey(hit.name),
6232
- line: lineOf(hit.index),
6233
- status: "manual",
6234
- reason: "this is only the schema; guard the code that runs the tool the model picked (often one dispatch function for every tool)"
6235
- }));
7302
+ const hits = [
7303
+ ...findToolSchemas(source, python).map((hit) => ({ name: hit.name, index: hit.index })),
7304
+ ...python ? [] : findFunctionToolSchemas(source).map((hit) => ({ name: hit.name, index: hit.index }))
7305
+ ].sort((a, b) => a.index - b.index);
7306
+ const changes = [];
7307
+ const seen = new Set(placed);
7308
+ for (const hit of hits) {
7309
+ const key = toToolKey(hit.name);
7310
+ if (!wanted.has(key) || seen.has(key)) continue;
7311
+ seen.add(key);
7312
+ changes.push({ key, line: lineOf(hit.index), status: "manual", reason: SCHEMA_ONLY_REASON });
7313
+ }
7314
+ return changes;
6236
7315
  }
6237
7316
  function dedupeChanges(changes) {
6238
7317
  const seen = /* @__PURE__ */ new Set();
@@ -6352,10 +7431,55 @@ function myers(a, b) {
6352
7431
 
6353
7432
  // src/integrate.ts
6354
7433
  async function integrate(root, toolKeys, options) {
6355
- const plan = planRewrite(root, listSourceFiles(root), toolKeys);
7434
+ const index = indexFiles(root);
7435
+ const anchors = [...toolKeyAnchors(toolKeys), ...TOOL_ANCHORS, ...GUARD_ANCHORS];
7436
+ const candidates = shortlist(root, index, anchors);
7437
+ const sourceFiles = candidates.files;
7438
+ const truncated = index.truncated || candidates.truncated;
6356
7439
  const interactive = options.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true && !options.yes);
6357
- const dispatchGuardFiles = findDispatchGuards(readSources(root));
7440
+ if (truncated && !options.quiet) {
7441
+ out(
7442
+ `${symbol.warn} The search ran out of time before it finished, so a tool may be reported as not found, or the place it runs may be missed.`
7443
+ );
7444
+ }
7445
+ const sources = readSources(root, sourceFiles);
7446
+ const dispatchGuardFiles = findDispatchGuards(sources);
6358
7447
  const dispatchGuarded = dispatchGuardFiles.length > 0;
7448
+ const alreadyGuarded = guardedToolKeys(sources, toolKeys);
7449
+ const reconcile = (result) => {
7450
+ for (const file of result.files) {
7451
+ for (const change of file.changes) {
7452
+ if (change.status === "manual" && alreadyGuarded.has(change.key)) {
7453
+ change.status = "already";
7454
+ delete change.reason;
7455
+ }
7456
+ }
7457
+ }
7458
+ return result;
7459
+ };
7460
+ let plan = reconcile(planRewrite(root, sourceFiles, toolKeys));
7461
+ const changeable = [
7462
+ ...new Set(
7463
+ plan.files.flatMap(
7464
+ (file) => file.changes.filter((change) => change.status !== "already").map((change) => change.key)
7465
+ )
7466
+ )
7467
+ ];
7468
+ const selected = await chooseTools(changeable, plan, {
7469
+ requested: options.tools,
7470
+ interactive,
7471
+ quiet: options.quiet === true
7472
+ });
7473
+ const skipped = changeable.filter((key) => !selected.includes(key));
7474
+ if (skipped.length > 0) {
7475
+ plan = reconcile(
7476
+ planRewrite(
7477
+ root,
7478
+ sourceFiles,
7479
+ toolKeys.filter((key) => !skipped.includes(key))
7480
+ )
7481
+ );
7482
+ }
6359
7483
  let manual = plan.files.flatMap(
6360
7484
  (file) => file.changes.filter((change) => change.status === "manual").map((change) => ({
6361
7485
  file: file.file,
@@ -6379,7 +7503,7 @@ async function integrate(root, toolKeys, options) {
6379
7503
  }
6380
7504
  dispatch = [];
6381
7505
  } else if (dispatch.length > 0) {
6382
- dispatchPlan = planDispatch(listSourceFiles(root), (file) => currentText(root, plan, file), [
7506
+ dispatchPlan = planDispatch(sourceFiles, (file) => currentText(root, plan, file), [
6383
7507
  ...new Set(dispatch.map((item) => item.key))
6384
7508
  ]);
6385
7509
  if (dispatchPlan?.after) {
@@ -6387,7 +7511,22 @@ async function integrate(root, toolKeys, options) {
6387
7511
  dispatch = [];
6388
7512
  }
6389
7513
  }
6390
- const dispatchKeys = [...new Set(dispatch.map((item) => item.key))];
7514
+ if (dispatch.length > 0 && checkpointUsable(root, options.quiet === true)) {
7515
+ const placed = applyCheckpoints(
7516
+ plan,
7517
+ planCheckpoints(sourceFiles, (file) => currentText(root, plan, file), [
7518
+ ...new Set(dispatch.map((item) => item.key))
7519
+ ])
7520
+ );
7521
+ dispatch = dispatch.filter((item) => !placed.has(item.key));
7522
+ }
7523
+ const runsElsewhere = (key) => {
7524
+ const site = dispatchPlan?.sites?.get(key);
7525
+ return site !== void 0 && site.file !== dispatchPlan?.file;
7526
+ };
7527
+ const dispatchKeys = [
7528
+ ...new Set(dispatch.filter((item) => !runsElsewhere(item.key)).map((item) => item.key))
7529
+ ];
6391
7530
  const dispatchItems = dispatchPlan ? dispatchKeys.map((key) => ({
6392
7531
  file: dispatchPlan?.file,
6393
7532
  key,
@@ -6395,7 +7534,7 @@ async function integrate(root, toolKeys, options) {
6395
7534
  reason: `guard the code that runs the tool: ${dispatchPlan?.reason ?? ""}`,
6396
7535
  language: languageOf(dispatchPlan?.file)
6397
7536
  })) : [];
6398
- const unplaced = dispatchPlan ? [] : dispatch;
7537
+ const unplaced = dispatchPlan ? dispatch.filter((item) => runsElsewhere(item.key)) : dispatch;
6399
7538
  const pending = [...manual, ...dispatchItems];
6400
7539
  if (pending.length > 0 && !options.dryRun) {
6401
7540
  const hostedAvailable = await hostedEnabled(options.api ?? null);
@@ -6450,8 +7589,9 @@ async function integrate(root, toolKeys, options) {
6450
7589
  const entry = plan.files.find((file) => file.file === proposal.file);
6451
7590
  if (!entry) continue;
6452
7591
  entry.after = proposal.proposal;
7592
+ const guarded = findGuardedKeys([{ file: proposal.file, text: proposal.proposal }]);
6453
7593
  for (const change of entry.changes) {
6454
- if (change.status === "manual" && proposal.keys.includes(change.key)) {
7594
+ if (change.status === "manual" && guarded.has(change.key)) {
6455
7595
  change.status = "wrapped";
6456
7596
  change.via = "ai";
6457
7597
  delete change.reason;
@@ -6465,29 +7605,46 @@ async function integrate(root, toolKeys, options) {
6465
7605
  dispatch = remaining.filter(isDispatchItem);
6466
7606
  if (method !== "skip" && !options.quiet) {
6467
7607
  if (manual.length > 0) printManualInstructions(manual);
6468
- if (dispatch.length > 0) printDispatchInstructions(dispatch, dispatchPlan);
7608
+ printDispatchInstructions([...dispatch, ...unplaced], dispatchPlan);
6469
7609
  }
6470
7610
  } else if (pending.length > 0 && !options.quiet) {
6471
- if (dispatchItems.length > 0) printDispatchInstructions(dispatchItems, dispatchPlan);
7611
+ printDispatchInstructions([...dispatchItems, ...unplaced], dispatchPlan);
6472
7612
  dispatch = dispatchItems;
7613
+ } else if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
7614
+ printDispatchInstructions(unplaced, dispatchPlan);
6473
7615
  }
6474
- if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
6475
- printDispatchInstructions(unplaced, null);
6476
- }
6477
- manual = [...manual, ...dispatch, ...unplaced];
7616
+ manual = stillManual([...manual, ...dispatch, ...unplaced], plan);
6478
7617
  const toWrite = plan.files.filter((file) => file.before !== file.after);
6479
7618
  const languages = new Set(toWrite.map((file) => file.file.endsWith(".py") ? "py" : "js"));
6480
7619
  if (!options.quiet) printPlan(plan);
6481
7620
  if (toWrite.length === 0) {
6482
- if (!options.quiet && !options.dryRun) {
7621
+ const quiet = options.quiet === true || options.dryRun === true;
7622
+ if (!quiet) {
6483
7623
  const guardedFiles = plan.files.filter((file) => file.changes.some((change) => change.status !== "manual")).map((file) => file.file);
6484
7624
  reportSdkReadiness(root, [.../* @__PURE__ */ new Set([...guardedFiles, ...dispatchGuardFiles])]);
6485
7625
  }
6486
- return { status: "nothing-to-do", plan, checks: [], backupId: null, manual };
7626
+ const unguarded2 = reportProtection(root, sourceFiles, toolKeys, quiet);
7627
+ return {
7628
+ status: "nothing-to-do",
7629
+ plan,
7630
+ checks: [],
7631
+ backupId: null,
7632
+ manual,
7633
+ unguarded: unguarded2,
7634
+ skipped
7635
+ };
6487
7636
  }
6488
7637
  if (options.dryRun) {
6489
7638
  out(style.grey("Dry run: nothing was written."));
6490
- return { status: "dry-run", plan, checks: [], backupId: null, manual };
7639
+ return {
7640
+ status: "dry-run",
7641
+ plan,
7642
+ checks: [],
7643
+ backupId: null,
7644
+ manual,
7645
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7646
+ skipped
7647
+ };
6491
7648
  }
6492
7649
  if (!options.quiet) {
6493
7650
  reportSdkReadiness(
@@ -6501,7 +7658,15 @@ async function integrate(root, toolKeys, options) {
6501
7658
  );
6502
7659
  if (!proceed) {
6503
7660
  out(style.grey("No files were changed. Run `veefore integrate` when you are ready."));
6504
- return { status: "declined", plan, checks: [], backupId: null, manual };
7661
+ return {
7662
+ status: "declined",
7663
+ plan,
7664
+ checks: [],
7665
+ backupId: null,
7666
+ manual,
7667
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7668
+ skipped
7669
+ };
6505
7670
  }
6506
7671
  const jsReady = languages.has("js") && sdkInstalled(root, "js");
6507
7672
  const baseline = jsReady ? typecheck(
@@ -6509,13 +7674,22 @@ async function integrate(root, toolKeys, options) {
6509
7674
  toWrite.map((file) => file.file)
6510
7675
  ) : null;
6511
7676
  for (const file of toWrite) {
6512
- if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
7677
+ if (readFileSync9(path10.join(root, file.file), "utf8") !== file.before) {
6513
7678
  out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
6514
- return { status: "declined", plan, checks: [], backupId: null, manual };
7679
+ return {
7680
+ status: "declined",
7681
+ plan,
7682
+ checks: [],
7683
+ backupId: null,
7684
+ manual,
7685
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7686
+ skipped
7687
+ };
6515
7688
  }
6516
7689
  }
6517
7690
  const backup = createBackup(root, toWrite);
6518
- for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
7691
+ for (const file of toWrite) writeFilePreservingMode(path10.join(root, file.file), file.after);
7692
+ forgetIndex(root);
6519
7693
  out(
6520
7694
  `${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
6521
7695
  );
@@ -6571,7 +7745,15 @@ async function integrate(root, toolKeys, options) {
6571
7745
  `${symbol.warn} A check failed after the change, so every file was put back as it was ${style.grey(`(${restored.restored.length} restored)`)}.`
6572
7746
  );
6573
7747
  out(style.grey(" Please report this: the rewrite should never break a project."));
6574
- return { status: "rolled-back", plan, checks, backupId: null, manual };
7748
+ return {
7749
+ status: "rolled-back",
7750
+ plan,
7751
+ checks,
7752
+ backupId: null,
7753
+ manual,
7754
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true),
7755
+ skipped
7756
+ };
6575
7757
  }
6576
7758
  for (const language of languages) {
6577
7759
  if (!sdkInstalled(root, language)) {
@@ -6580,13 +7762,39 @@ async function integrate(root, toolKeys, options) {
6580
7762
  );
6581
7763
  }
6582
7764
  }
7765
+ if (skipped.length > 0 && !options.quiet) {
7766
+ out(
7767
+ `${symbol.info} Left out of this run, as you chose: ${style.bold(skipped.join(", "))} ${style.grey("(no file was changed for these)")}`
7768
+ );
7769
+ }
7770
+ const unguarded = reportProtection(root, sourceFiles, toolKeys, options.quiet === true);
6583
7771
  out(style.grey(" Undo at any time with `veefore undo`."));
6584
- return { status: "applied", plan, checks, backupId: backup.id, manual };
7772
+ return { status: "applied", plan, checks, backupId: backup.id, manual, unguarded, skipped };
7773
+ }
7774
+ function reportProtection(root, files, toolKeys, quiet) {
7775
+ const guarded = guardedToolKeys(readSources(root, files), toolKeys);
7776
+ const unguarded = toolKeys.filter((key) => !guarded.has(key));
7777
+ if (quiet) return unguarded;
7778
+ if (unguarded.length === 0) {
7779
+ out(
7780
+ `${symbol.pass} Every registered tool goes through Veefore ${style.grey(`(${toolKeys.length})`)}`
7781
+ );
7782
+ return unguarded;
7783
+ }
7784
+ out(
7785
+ `${symbol.warn} ${toolKeys.length - unguarded.length} of ${toolKeys.length} registered tools go through Veefore. Still unprotected: ${style.bold(unguarded.join(", "))}`
7786
+ );
7787
+ out(
7788
+ style.grey(
7789
+ " The instructions above say what each one needs. Then `veefore integrate --check` to confirm."
7790
+ )
7791
+ );
7792
+ return unguarded;
6585
7793
  }
6586
7794
  function reportSdkReadiness(root, files) {
6587
7795
  const problems = [];
6588
7796
  if (files.length === 0) return problems;
6589
- const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
7797
+ const dirs = [...new Set(files.map((file) => path10.dirname(file)))];
6590
7798
  const python = files.some((file) => file.endsWith(".py"));
6591
7799
  const sdks = installedSdks(root, { dirs, python });
6592
7800
  for (const sdk of sdks) {
@@ -6663,7 +7871,7 @@ async function removeGuards(root, options) {
6663
7871
  const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
6664
7872
  const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
6665
7873
  const backup = createBackup(root, toWrite);
6666
- for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
7874
+ for (const entry of toWrite) writeFilePreservingMode(path10.join(root, entry.file), entry.after);
6667
7875
  const checks = [];
6668
7876
  const py = changed.filter((file) => file.endsWith(".py"));
6669
7877
  if (py.length > 0) checks.push(pythonSyntax(root, py));
@@ -6687,7 +7895,7 @@ function currentText(root, plan, file) {
6687
7895
  const entry = plan.files.find((candidate) => candidate.file === file);
6688
7896
  if (entry) return entry.after;
6689
7897
  try {
6690
- return readFileSync8(path9.join(root, file), "utf8");
7898
+ return readFileSync9(path10.join(root, file), "utf8");
6691
7899
  } catch {
6692
7900
  return null;
6693
7901
  }
@@ -6709,17 +7917,113 @@ function applyDispatch(root, plan, file, after, line, items, via) {
6709
7917
  entry.changes.push({ key, line, status: "wrapped", ...via ? { via } : {} });
6710
7918
  }
6711
7919
  }
6712
- function readSources(root) {
6713
- return listSourceFiles(root).flatMap((file) => {
7920
+ function stillManual(items, plan) {
7921
+ const open = new Set(
7922
+ plan.files.flatMap(
7923
+ (file) => file.changes.filter((change) => change.status === "manual").map((change) => change.key)
7924
+ )
7925
+ );
7926
+ const seen = /* @__PURE__ */ new Set();
7927
+ return items.filter((item) => {
7928
+ if (!open.has(item.key)) return false;
7929
+ const id = isDispatchItem(item) ? item.key : `${item.key}\0${item.file}`;
7930
+ if (seen.has(id)) return false;
7931
+ seen.add(id);
7932
+ return true;
7933
+ });
7934
+ }
7935
+ async function chooseTools(changeable, plan, options) {
7936
+ if (options.requested) {
7937
+ const asked = new Set(options.requested);
7938
+ const unknown = options.requested.filter((key) => !changeable.includes(key));
7939
+ if (unknown.length > 0 && !options.quiet) {
7940
+ out(
7941
+ `${symbol.warn} Nothing to change for: ${unknown.join(", ")} ${style.grey("(already protected, or not found in the code)")}`
7942
+ );
7943
+ }
7944
+ return changeable.filter((key) => asked.has(key));
7945
+ }
7946
+ if (!options.interactive || changeable.length < 2) return changeable;
7947
+ const where = /* @__PURE__ */ new Map();
7948
+ for (const file of plan.files) {
7949
+ for (const change of file.changes) {
7950
+ if (change.status === "already" || where.has(change.key)) continue;
7951
+ where.set(change.key, { line: change.line, file: file.file, status: change.status });
7952
+ }
7953
+ }
7954
+ try {
7955
+ return await selectMany(
7956
+ "Which tools should Veefore decide for?",
7957
+ changeable.map((key) => {
7958
+ const at = where.get(key);
7959
+ return {
7960
+ label: key,
7961
+ value: key,
7962
+ hint: at ? `${at.status === "wrapped" ? "guard here" : "needs a change"} \xB7 ${at.file}:${at.line}` : ""
7963
+ };
7964
+ }),
7965
+ { help: "All of them unless you say otherwise. Only what is ticked gets changed." }
7966
+ );
7967
+ } catch (error) {
7968
+ if (error instanceof MenuCancelled) return [];
7969
+ throw error;
7970
+ }
7971
+ }
7972
+ function checkpointUsable(root, quiet) {
7973
+ const sdks = installedSdks(root).filter((sdk) => sdk.language === "js");
7974
+ const stale = sdks.filter((sdk) => !sdk.hasCheckpoint);
7975
+ if (sdks.length === 0 || stale.length === 0) return true;
7976
+ if (!quiet) {
7977
+ out(
7978
+ `${symbol.warn} Tools your code runs in a branch rather than a function are guarded with ${style.bold("checkpoint()")}, added in @veefore/sdk ${MIN_CHECKPOINT_SDK}. ${stale[0]?.version ?? "The installed version"} does not have it, so they are left for you.`
7979
+ );
7980
+ out(
7981
+ ` Upgrade and re-run: ${style.bold(`${installCommand(root, "js")}@^${MIN_CHECKPOINT_SDK}`)}`
7982
+ );
7983
+ }
7984
+ return false;
7985
+ }
7986
+ function applyCheckpoints(plan, plans) {
7987
+ const placed = /* @__PURE__ */ new Set();
7988
+ for (const checkpoint of plans) {
7989
+ let entry = plan.files.find((candidate) => candidate.file === checkpoint.file);
7990
+ if (!entry) {
7991
+ entry = {
7992
+ file: checkpoint.file,
7993
+ before: checkpoint.before,
7994
+ after: checkpoint.after,
7995
+ changes: []
7996
+ };
7997
+ plan.files.push(entry);
7998
+ }
7999
+ entry.after = checkpoint.after;
8000
+ for (const place of checkpoint.placed) {
8001
+ placed.add(place.key);
8002
+ entry.changes.push({
8003
+ key: place.key,
8004
+ line: place.line,
8005
+ status: "wrapped",
8006
+ via: "checkpoint"
8007
+ });
8008
+ }
8009
+ }
8010
+ for (const file of plan.files) {
8011
+ file.changes = file.changes.filter(
8012
+ (change) => !(change.status === "manual" && placed.has(change.key) && isDispatchItem({ reason: change.reason ?? "" }))
8013
+ );
8014
+ }
8015
+ return placed;
8016
+ }
8017
+ function readSources(root, files = listSourceFiles(root)) {
8018
+ return files.flatMap((file) => {
6714
8019
  try {
6715
- return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
8020
+ return [{ file, text: readFileSync9(path10.join(root, file), "utf8") }];
6716
8021
  } catch {
6717
8022
  return [];
6718
8023
  }
6719
8024
  });
6720
8025
  }
6721
- function checkIntegration(root, toolKeys) {
6722
- const sources = readSources(root);
8026
+ function guardedToolKeys(sources, toolKeys) {
6723
8027
  const guarded = findGuardedKeys(sources);
6724
8028
  const dispatchFiles = findDispatchGuards(sources);
6725
8029
  if (dispatchFiles.length > 0) {
@@ -6734,6 +8038,16 @@ function checkIntegration(root, toolKeys) {
6734
8038
  }
6735
8039
  }
6736
8040
  }
8041
+ return new Map([...guarded].filter(([key]) => toolKeys.includes(key)));
8042
+ }
8043
+ function checkIntegration(root, toolKeys) {
8044
+ const index = indexFiles(root);
8045
+ const candidates = shortlist(root, index, [
8046
+ ...toolKeyAnchors(toolKeys),
8047
+ ...TOOL_ANCHORS,
8048
+ ...GUARD_ANCHORS
8049
+ ]);
8050
+ const guarded = guardedToolKeys(readSources(root, candidates.files), toolKeys);
6737
8051
  heading("Protection check");
6738
8052
  for (const key of toolKeys) {
6739
8053
  const file = guarded.get(key);
@@ -6796,22 +8110,31 @@ function printPlan(plan) {
6796
8110
  const wrapped = all.filter((entry) => entry.change.status === "wrapped");
6797
8111
  const already = all.filter((entry) => entry.change.status === "already");
6798
8112
  const manual = all.filter((entry) => entry.change.status === "manual");
8113
+ const tools = (entries) => new Set(entries.map((e) => e.change.key)).size;
8114
+ const keys = (entries) => [...new Set(entries.map((e) => e.change.key))];
6799
8115
  if (wrapped.length > 0) {
8116
+ const count = tools(wrapped);
8117
+ const only = new Set(wrapped.map((entry) => entry.change.via ?? "guard"));
8118
+ const how = only.size === 1 && only.has("checkpoint") ? `gated with ${style.bold("checkpoint()")} where your code runs them` : `wrapped with ${style.bold("guard()")}`;
6800
8119
  out(
6801
- `${symbol.pass} ${wrapped.length} tool${wrapped.length === 1 ? "" : "s"} will be wrapped with ${style.bold("guard()")}, so Veefore decides each call:`
8120
+ `${symbol.pass} ${count} tool${count === 1 ? "" : "s"} will be ${how}, so Veefore decides each call:`
6802
8121
  );
8122
+ const note = {
8123
+ ai: style.amber("proposed by AI \u2014 review the diff below"),
8124
+ checkpoint: style.grey("guarded where your code runs it")
8125
+ };
6803
8126
  for (const { file, change } of wrapped) {
6804
8127
  out(
6805
- ` ${style.bold(change.key.padEnd(24))} ${style.grey(`${file}:${change.line}`)}` + (change.via === "ai" ? ` ${style.amber("proposed by AI \u2014 review the diff below")}` : "")
8128
+ ` ${style.bold(change.key.padEnd(24))} ${style.grey(`${file}:${change.line}`)}` + (change.via ? ` ${note[change.via] ?? ""}` : "")
6806
8129
  );
6807
8130
  }
6808
8131
  }
6809
8132
  if (already.length > 0) {
6810
- out(`${symbol.pass} Already protected: ${already.map((entry) => entry.change.key).join(", ")}`);
8133
+ out(`${symbol.pass} Already protected: ${keys(already).join(", ")}`);
6811
8134
  }
6812
8135
  if (manual.length > 0) {
6813
8136
  out(
6814
- `${symbol.warn} Not changed automatically: ${manual.map((entry) => entry.change.key).join(", ")} ${style.grey("(see instructions above)")}`
8137
+ `${symbol.warn} Not changed automatically: ${keys(manual).join(", ")} ${style.grey("(see instructions above)")}`
6815
8138
  );
6816
8139
  }
6817
8140
  if (plan.notFound.length > 0) {
@@ -6833,8 +8156,8 @@ function printPlan(plan) {
6833
8156
  }
6834
8157
 
6835
8158
  // src/commands/remove.ts
6836
- import { rmSync as rmSync3 } from "node:fs";
6837
- import path10 from "node:path";
8159
+ import { rmSync as rmSync4 } from "node:fs";
8160
+ import path11 from "node:path";
6838
8161
 
6839
8162
  // src/discover.ts
6840
8163
  var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
@@ -6948,7 +8271,7 @@ async function removeCommand(args, flags, known) {
6948
8271
  if (!file || !config) {
6949
8272
  throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
6950
8273
  }
6951
- const root = path10.dirname(file);
8274
+ const root = path11.dirname(file);
6952
8275
  const state = known ?? await agentState(config);
6953
8276
  heading("Remove Veefore from this project");
6954
8277
  details([
@@ -6999,7 +8322,7 @@ async function removeCommand(args, flags, known) {
6999
8322
  out(style.grey("The project stays connected."));
7000
8323
  return 0;
7001
8324
  }
7002
- rmSync3(file);
8325
+ rmSync4(file);
7003
8326
  const envRemoved = removeLocalEnvBlock(root);
7004
8327
  out(
7005
8328
  `${symbol.pass} Disconnected: removed veefore.config.json${envRemoved ? " and the Veefore block in .env.local" : ""}.`
@@ -7026,10 +8349,20 @@ async function integrateCommand(args, flags) {
7026
8349
  let assist;
7027
8350
  let dryRun = false;
7028
8351
  let check = false;
8352
+ let tools;
7029
8353
  for (const arg of args) {
7030
8354
  if (arg === "--dry-run") dryRun = true;
7031
8355
  else if (arg === "--check") check = true;
7032
- else if (arg.startsWith("--assist=")) {
8356
+ else if (arg === "--tools=all") tools = void 0;
8357
+ else if (arg.startsWith("--tools=")) {
8358
+ tools = arg.slice("--tools=".length).split(",").map((key) => key.trim()).filter((key) => key !== "");
8359
+ if (tools.length === 0) {
8360
+ throw new CliError(
8361
+ "`--tools` takes a comma-separated list of tool keys, or `all`.",
8362
+ "For example: --tools=refund_order,adjust_stock"
8363
+ );
8364
+ }
8365
+ } else if (arg.startsWith("--assist=")) {
7033
8366
  const value = ASSIST_VALUES[arg.slice("--assist=".length)];
7034
8367
  if (!value) {
7035
8368
  throw new CliError(
@@ -7072,7 +8405,8 @@ async function integrateCommand(args, flags) {
7072
8405
  dryRun,
7073
8406
  quiet: flags.json,
7074
8407
  assist,
7075
- api
8408
+ api,
8409
+ ...tools ? { tools } : {}
7076
8410
  });
7077
8411
  if (flags.json) {
7078
8412
  out(
@@ -7083,6 +8417,8 @@ async function integrateCommand(args, flags) {
7083
8417
  files: outcome.plan.files.map((file) => ({ file: file.file, changes: file.changes })),
7084
8418
  not_found: outcome.plan.notFound,
7085
8419
  manual: outcome.manual,
8420
+ unguarded: outcome.unguarded,
8421
+ skipped: outcome.skipped,
7086
8422
  checks: outcome.checks
7087
8423
  },
7088
8424
  null,
@@ -7482,10 +8818,10 @@ async function resolveAuthorizedTools(context, filter) {
7482
8818
 
7483
8819
  // src/panel.ts
7484
8820
  import { homedir as homedir2 } from "node:os";
7485
- import path11 from "node:path";
8821
+ import path12 from "node:path";
7486
8822
 
7487
8823
  // src/version.ts
7488
- var CLI_VERSION = "1.4.1";
8824
+ var CLI_VERSION = "1.6.0";
7489
8825
 
7490
8826
  // src/panel.ts
7491
8827
  function welcomeSource(flags = {}) {
@@ -7509,7 +8845,7 @@ function welcomeSource(flags = {}) {
7509
8845
  }
7510
8846
  function tilde(dir) {
7511
8847
  const home = homedir2();
7512
- return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
8848
+ return dir === home || dir.startsWith(`${home}${path12.sep}`) ? `~${dir.slice(home.length)}` : dir;
7513
8849
  }
7514
8850
  function hostOf(url) {
7515
8851
  try {
@@ -7576,7 +8912,7 @@ function planTool(tool) {
7576
8912
  }
7577
8913
  function printScan(scan) {
7578
8914
  out(
7579
- `${symbol.pass} Scanned ${scan.filesScanned} file${scan.filesScanned === 1 ? "" : "s"}` + (scan.truncated ? style.amber(" (limit reached; findings may be incomplete)") : "") + style.grey(" \u2014 locally, nothing was uploaded")
8915
+ `${symbol.pass} Searched ${scan.filesScanned.toLocaleString("en-US")} file${scan.filesScanned === 1 ? "" : "s"}, read ${scan.filesParsed.toLocaleString("en-US")}` + (scan.truncated ? style.amber(" (ran out of time; findings may be incomplete)") : "") + style.grey(" \u2014 locally, nothing was uploaded")
7580
8916
  );
7581
8917
  if (scan.tools.length === 0) {
7582
8918
  out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
@@ -7609,6 +8945,40 @@ function printScan(scan) {
7609
8945
  `${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)")}`
7610
8946
  );
7611
8947
  }
8948
+ printGaps(scan);
8949
+ }
8950
+ function printGaps(scan) {
8951
+ let discovery;
8952
+ try {
8953
+ discovery = discoverAgents(scan.root, scan);
8954
+ } catch {
8955
+ return;
8956
+ }
8957
+ if (discovery.agents.length > 1) {
8958
+ out();
8959
+ out(`${symbol.info} ${discovery.agents.length} agents found in this project:`);
8960
+ for (const agent of discovery.agents) {
8961
+ out(
8962
+ ` ${style.bold(agent.name ?? agent.directory)} ${style.grey(`${agent.directory} \u2014 ${agent.tools.length} tool${agent.tools.length === 1 ? "" : "s"}`)}`
8963
+ );
8964
+ }
8965
+ out(style.grey(" Set each one up from its own directory, so its tools stay its own."));
8966
+ }
8967
+ if (discovery.unreadable.length === 0) return;
8968
+ const shown = discovery.unreadable.slice(0, 5);
8969
+ out();
8970
+ out(
8971
+ `${symbol.warn} ${discovery.unreadable.length} file${discovery.unreadable.length === 1 ? "" : "s"} look${discovery.unreadable.length === 1 ? "s" : ""} like agent code whose tools could not be read:`
8972
+ );
8973
+ for (const gap of shown) out(` ${style.grey(`${gap.file}:${gap.line}`)}`);
8974
+ if (discovery.unreadable.length > shown.length) {
8975
+ out(style.grey(` \u2026and ${discovery.unreadable.length - shown.length} more`));
8976
+ }
8977
+ out(
8978
+ style.grey(
8979
+ " Tools defined in a shape this scan does not know are not protected. Register them\n in the console by hand, or report the shape so it can be detected."
8980
+ )
8981
+ );
7612
8982
  }
7613
8983
  function printPlan2(plan, environment) {
7614
8984
  out();
@@ -8570,7 +9940,7 @@ async function initCommand(args, flags) {
8570
9940
  }
8571
9941
  function previousApprovalMode(dir = process.cwd()) {
8572
9942
  try {
8573
- const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
9943
+ const text = readFileSync10(path13.join(dir, ".env.local"), "utf8");
8574
9944
  const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
8575
9945
  return value === "wait" || value === "throw" ? value : null;
8576
9946
  } catch {