veefore 1.4.1 → 1.5.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 +1167 -224
  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),
@@ -1946,11 +1946,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
1946
1946
  out(` ${style.red("This action is irreversible for this identity.")}`);
1947
1947
  out();
1948
1948
  }
1949
- async function lifecycle(args, flags, path13, resultingStatus) {
1949
+ async function lifecycle(args, flags, path14, resultingStatus) {
1950
1950
  const context = await resolveContext(flags);
1951
1951
  const agentId = positional(args) ?? context.config.agentId;
1952
1952
  const agent = await context.api.get(`/v1/agents/${agentId}`);
1953
- const reasonRequired = path13 === "suspend";
1953
+ const reasonRequired = path14 === "suspend";
1954
1954
  const reason = valueFor(args, "--reason");
1955
1955
  if (reasonRequired && (!reason || reason.trim().length < 10)) {
1956
1956
  throw new CliError(
@@ -1960,24 +1960,24 @@ async function lifecycle(args, flags, path13, resultingStatus) {
1960
1960
  }
1961
1961
  out();
1962
1962
  out(
1963
- `${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1963
+ `${symbol.info} ${path14} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1964
1964
  );
1965
- if (path13 !== "resume") {
1965
+ if (path14 !== "resume") {
1966
1966
  const confirmed = await confirm("Continue?", { yes: flags.yes });
1967
1967
  if (!confirmed) {
1968
1968
  out(style.grey("Cancelled."));
1969
1969
  return 1;
1970
1970
  }
1971
1971
  }
1972
- const actionSpinner = spinner(`Applying ${path13}`);
1972
+ const actionSpinner = spinner(`Applying ${path14}`);
1973
1973
  try {
1974
1974
  await withStepUp(
1975
1975
  context.api,
1976
- () => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
1976
+ () => context.api.post(`/v1/agents/${agentId}/${path14}`, reason ? { reason } : {})
1977
1977
  );
1978
1978
  actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
1979
1979
  } catch (error) {
1980
- actionSpinner.fail(`Could not ${path13} the agent`);
1980
+ actionSpinner.fail(`Could not ${path14} the agent`);
1981
1981
  throw error;
1982
1982
  }
1983
1983
  out();
@@ -2289,6 +2289,7 @@ import { spawnSync } from "node:child_process";
2289
2289
  import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
2290
2290
  import path3 from "node:path";
2291
2291
  var MIN_APPROVAL_WAIT_SDK = "1.1.0";
2292
+ var MIN_CHECKPOINT_SDK = "1.2.0";
2292
2293
  function versionLessThan(a, b) {
2293
2294
  if (!a) return true;
2294
2295
  const left = a.split(/[.+-]/).slice(0, 3).map(Number);
@@ -2335,7 +2336,8 @@ function installedSdks(root, options = {}) {
2335
2336
  language: "js",
2336
2337
  location: path3.relative(top, path3.dirname(manifest)) || ".",
2337
2338
  version,
2338
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2339
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2340
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2339
2341
  });
2340
2342
  }
2341
2343
  if (options.python) {
@@ -2352,7 +2354,8 @@ function installedSdks(root, options = {}) {
2352
2354
  language: "py",
2353
2355
  location: "python: veefore-py",
2354
2356
  version,
2355
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2357
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2358
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2356
2359
  });
2357
2360
  }
2358
2361
  }
@@ -3300,12 +3303,12 @@ function isLoosening(from, to) {
3300
3303
  }
3301
3304
 
3302
3305
  // src/commands/init.ts
3303
- import { readFileSync as readFileSync9 } from "node:fs";
3304
- import path12 from "node:path";
3306
+ import { readFileSync as readFileSync10 } from "node:fs";
3307
+ import path13 from "node:path";
3305
3308
 
3306
3309
  // src/integrate.ts
3307
- import { readFileSync as readFileSync8 } from "node:fs";
3308
- import path9 from "node:path";
3310
+ import { readFileSync as readFileSync9 } from "node:fs";
3311
+ import path10 from "node:path";
3309
3312
 
3310
3313
  // ../types/src/permissions.ts
3311
3314
  var PERMISSIONS = [
@@ -4189,10 +4192,33 @@ function printDispatchInstructions(items, where) {
4189
4192
  out(
4190
4193
  ` ${items.length} tool${items.length === 1 ? " is" : "s are"} defined as data, so no one function performs each tool.`
4191
4194
  );
4192
- if (where) {
4193
- out(
4194
- ` Your code runs them in ${style.bold(`${where.file}:${where.line}`)}${where.functionName ? ` (${where.functionName})` : ""}.`
4195
- );
4195
+ const keys = [...new Set(items.map((item) => item.key))];
4196
+ const sites = new Map(
4197
+ keys.flatMap((key) => {
4198
+ const site = where?.sites?.get(key);
4199
+ return site ? [[key, site]] : [];
4200
+ })
4201
+ );
4202
+ const places = new Set([...sites.values()].map((site) => `${site.file}:${site.line}`));
4203
+ const only = places.size === 1 && sites.size === keys.length ? [...sites.values()][0] : null;
4204
+ if (sites.size > 0 && !only) {
4205
+ out(" Wrap the call that performs each one, where your code runs it:");
4206
+ out();
4207
+ for (const key of keys) {
4208
+ const site = sites.get(key);
4209
+ out(
4210
+ ` ${style.bold(key)} ${style.grey(site ? `${site.file}:${site.line}` : "where it is run; this could not be found")}`
4211
+ );
4212
+ if (site) out(` ${style.grey(site.code)}`);
4213
+ }
4214
+ out();
4215
+ out(" The change is the same shape in each place:");
4216
+ } else if (only || where) {
4217
+ const file = only?.file ?? where?.file;
4218
+ const line = only?.line ?? where?.line ?? 1;
4219
+ const named = where?.functionName && file === where.file ? ` (${where.functionName})` : "";
4220
+ out(` Your code runs them in ${style.bold(`${file}:${line}`)}${named}.`);
4221
+ if (only) out(` ${style.grey(only.code)}`);
4196
4222
  out(" Wrap the call there that performs the chosen tool, once:");
4197
4223
  } else {
4198
4224
  out(" Wrap the code that runs whichever tool the model picked, once:");
@@ -4210,7 +4236,9 @@ function printDispatchInstructions(items, where) {
4210
4236
  out(` ${style.cyan("import { guard } from '@veefore/sdk';")}`);
4211
4237
  out();
4212
4238
  out(` ${style.grey("// before: const result = await runTool(call);")}`);
4213
- out(` ${style.cyan("const result = await runTool(call);")}`);
4239
+ out(
4240
+ ` ${style.cyan("const result = await guard(call.name, (_args) => runTool(call))(call.args);")}`
4241
+ );
4214
4242
  }
4215
4243
  out();
4216
4244
  out(
@@ -4238,7 +4266,7 @@ function toCamel(key) {
4238
4266
  }
4239
4267
  function findGuardedKeys(sources) {
4240
4268
  const found = /* @__PURE__ */ new Map();
4241
- const pattern = /(?:\b|@)[A-Za-z_$]*[gG]uard\(\s*['"]([a-z][a-z0-9_]*)['"]/g;
4269
+ const pattern = /(?:\b|@)(?:[A-Za-z_$]*[gG]uard|[A-Za-z_$]*[cC]heckpoint)\(\s*['"]([a-z][a-z0-9_]*)['"]/g;
4242
4270
  for (const { file, text } of sources) {
4243
4271
  for (const match of text.matchAll(pattern)) {
4244
4272
  const key = match[1];
@@ -4651,12 +4679,95 @@ function restoreBackup(root, backup, options = {}) {
4651
4679
  }
4652
4680
 
4653
4681
  // src/rewrite.ts
4654
- import { readFileSync as readFileSync7 } from "node:fs";
4655
- import path8 from "node:path";
4682
+ import { readFileSync as readFileSync8 } from "node:fs";
4683
+ import path9 from "node:path";
4656
4684
 
4657
4685
  // 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";
4686
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
4687
+ import path8 from "node:path";
4688
+
4689
+ // src/anchors.ts
4690
+ var TOOL_ANCHORS = [
4691
+ // Schema-defined tools: findToolSchemas requires one of these keys.
4692
+ "parameters",
4693
+ "input_schema",
4694
+ "inputSchema",
4695
+ "parametersJsonSchema",
4696
+ // The OpenAI function shape, which may carry no description at all.
4697
+ "'function'",
4698
+ '"function"',
4699
+ "`function`",
4700
+ // Framework calls and decorators.
4701
+ "tool(",
4702
+ "Tool(",
4703
+ "tool (",
4704
+ "Tool (",
4705
+ "registerTool",
4706
+ // Python decorators, including the `@mcp.tool` / `@agent.tool` forms, which
4707
+ // carry neither `@tool` nor `tool(`.
4708
+ "@tool",
4709
+ ".tool",
4710
+ "function_tool",
4711
+ "StructuredTool",
4712
+ "DynamicTool"
4713
+ ];
4714
+ var AGENT_ANCHORS = [
4715
+ // Model SDKs and agent frameworks.
4716
+ "openai",
4717
+ "OpenAI",
4718
+ "@anthropic-ai/sdk",
4719
+ "anthropic",
4720
+ "Anthropic",
4721
+ "@google/genai",
4722
+ "@google/generative-ai",
4723
+ "google.generativeai",
4724
+ "google-generativeai",
4725
+ "langchain",
4726
+ "langgraph",
4727
+ "crewai",
4728
+ "llama_index",
4729
+ "llamaindex",
4730
+ "autogen",
4731
+ "litellm",
4732
+ "mistralai",
4733
+ "ollama",
4734
+ "bedrock-runtime",
4735
+ "bedrock_runtime",
4736
+ "@modelcontextprotocol",
4737
+ "mcp.server",
4738
+ // The vocabulary of a tool call, whatever produced it.
4739
+ "tool_calls",
4740
+ "toolCalls",
4741
+ "tool_call",
4742
+ "toolCall",
4743
+ "function_call",
4744
+ "functionCall",
4745
+ "tool_choice",
4746
+ "toolChoice",
4747
+ "toolUse",
4748
+ "tool_use"
4749
+ ];
4750
+ var GUARD_ANCHORS = ["uard(", "uard (", "checkpoint("];
4751
+ var RISK_ANCHORS = [
4752
+ "://",
4753
+ "process.env",
4754
+ "import.meta.env",
4755
+ "os.environ",
4756
+ "getenv",
4757
+ "Deno.env"
4758
+ ];
4759
+ function toolKeyAnchors(toolKeys) {
4760
+ const anchors = /* @__PURE__ */ new Set();
4761
+ for (const key of toolKeys) {
4762
+ anchors.add(key);
4763
+ const camel = key.replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase());
4764
+ if (camel !== key) anchors.add(camel);
4765
+ }
4766
+ return [...anchors];
4767
+ }
4768
+ var SCAN_ANCHORS = [
4769
+ .../* @__PURE__ */ new Set([...TOOL_ANCHORS, ...AGENT_ANCHORS, ...GUARD_ANCHORS, ...RISK_ANCHORS])
4770
+ ];
4660
4771
 
4661
4772
  // src/framework.ts
4662
4773
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
@@ -4852,19 +4963,11 @@ function integrationSnippet(framework) {
4852
4963
  ].join("\n");
4853
4964
  }
4854
4965
 
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
- }
4966
+ // src/search.ts
4967
+ import { execFileSync } from "node:child_process";
4968
+ import { mkdtempSync, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
4969
+ import { tmpdir } from "node:os";
4970
+ import path7 from "node:path";
4868
4971
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
4869
4972
  ".ts",
4870
4973
  ".tsx",
@@ -4900,8 +5003,247 @@ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
4900
5003
  "tests",
4901
5004
  "test"
4902
5005
  ]);
4903
- var MAX_FILES = 2e3;
4904
- var MAX_FILE_BYTES = 512 * 1024;
5006
+ var SKIP_FILES = /\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$|\.min\.[cm]?js$/;
5007
+ var DEFAULT_BUDGET_MS = 6e4;
5008
+ var MAX_PARSE_BYTES = 4 * 1024 * 1024;
5009
+ function indexFiles(root, options = {}) {
5010
+ const deadline2 = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS);
5011
+ const fromGit = gitFiles(root, deadline2);
5012
+ if (fromGit) {
5013
+ return {
5014
+ files: fromGit.tracked.filter(isCandidate),
5015
+ method: "git",
5016
+ truncated: false,
5017
+ unsearchable: fromGit.unsearchable.filter(isCandidate)
5018
+ };
5019
+ }
5020
+ const files = [];
5021
+ const truncated = walk(root, root, files, deadline2);
5022
+ return { files, method: "walk", truncated, unsearchable: [] };
5023
+ }
5024
+ function gitFiles(root, deadline2) {
5025
+ const output = git(
5026
+ root,
5027
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
5028
+ deadline2
5029
+ );
5030
+ if (output === null) return null;
5031
+ const tracked = new Set(output.split("\0").filter((entry) => entry !== ""));
5032
+ const unsearchable = /* @__PURE__ */ new Set();
5033
+ const submodules = git(root, ["ls-files", "-z", "--cached", "--recurse-submodules"], deadline2);
5034
+ if (submodules !== null) {
5035
+ for (const entry of submodules.split("\0")) {
5036
+ if (entry !== "" && !tracked.has(entry)) unsearchable.add(entry);
5037
+ }
5038
+ }
5039
+ for (const file of ignoredSources(root, deadline2)) {
5040
+ if (!tracked.has(file)) unsearchable.add(file);
5041
+ }
5042
+ return { tracked: [...tracked, ...unsearchable], unsearchable: [...unsearchable] };
5043
+ }
5044
+ function ignoredSources(root, deadline2) {
5045
+ const output = git(
5046
+ root,
5047
+ ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"],
5048
+ deadline2
5049
+ );
5050
+ if (output === null) return [];
5051
+ const files = [];
5052
+ for (const entry of output.split("\0")) {
5053
+ if (entry === "") continue;
5054
+ if (entry.endsWith("/")) {
5055
+ const name = entry.replace(/\/$/, "").split("/").pop() ?? "";
5056
+ if (SKIP_DIRECTORIES.has(name) || name.startsWith(".")) continue;
5057
+ const nested = [];
5058
+ walk(root, path7.join(root, entry), nested, deadline2);
5059
+ files.push(...nested);
5060
+ continue;
5061
+ }
5062
+ if (isCandidate(entry)) files.push(entry);
5063
+ }
5064
+ return files;
5065
+ }
5066
+ function git(root, args, deadline2) {
5067
+ const timeout = deadline2 - Date.now();
5068
+ if (timeout <= 0) return null;
5069
+ try {
5070
+ return execFileSync("git", ["-c", "core.quotepath=off", ...args], {
5071
+ cwd: root,
5072
+ encoding: "utf8",
5073
+ timeout,
5074
+ maxBuffer: 512 * 1024 * 1024,
5075
+ stdio: ["ignore", "pipe", "ignore"]
5076
+ });
5077
+ } catch {
5078
+ return null;
5079
+ }
5080
+ }
5081
+ function isCandidate(file) {
5082
+ if (!SOURCE_EXTENSIONS.has(path7.extname(file))) return false;
5083
+ const segments = file.split(/[/\\]/);
5084
+ const name = segments[segments.length - 1] ?? "";
5085
+ if (SKIP_FILES.test(name)) return false;
5086
+ return !segments.slice(0, -1).some((segment) => SKIP_DIRECTORIES.has(segment) || segment.startsWith("."));
5087
+ }
5088
+ function walk(root, dir, out2, deadline2) {
5089
+ let entries;
5090
+ try {
5091
+ entries = readdirSyncSafe(dir);
5092
+ } catch {
5093
+ return false;
5094
+ }
5095
+ for (const entry of entries) {
5096
+ if (Date.now() > deadline2) return true;
5097
+ if (entry.startsWith(".")) continue;
5098
+ const full = path7.join(dir, entry);
5099
+ let directory;
5100
+ try {
5101
+ directory = statSync4(full).isDirectory();
5102
+ } catch {
5103
+ continue;
5104
+ }
5105
+ if (directory) {
5106
+ if (SKIP_DIRECTORIES.has(entry)) continue;
5107
+ if (walk(root, full, out2, deadline2)) return true;
5108
+ continue;
5109
+ }
5110
+ const relative = path7.relative(root, full);
5111
+ if (isCandidate(relative)) out2.push(relative);
5112
+ }
5113
+ return false;
5114
+ }
5115
+ function readdirSyncSafe(dir) {
5116
+ return readdirSync3(dir).sort();
5117
+ }
5118
+ function shortlist(root, index, anchors, options = {}) {
5119
+ const considered = index.files.length;
5120
+ if (anchors.length === 0 || considered === 0) {
5121
+ return { files: [], method: "in-process", considered, truncated: false };
5122
+ }
5123
+ const deadline2 = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS);
5124
+ const allowed = new Set(index.files);
5125
+ const reached = index.unsearchable.filter((file) => allowed.has(file));
5126
+ const extra = reached.length > 0 ? inProcessSearch(root, reached, anchors, deadline2, reached.length) : null;
5127
+ const combine = (files, method) => ({
5128
+ files: [.../* @__PURE__ */ new Set([...files, ...extra?.files ?? []])],
5129
+ method,
5130
+ considered,
5131
+ truncated: extra?.truncated ?? false
5132
+ });
5133
+ if (index.method === "git") {
5134
+ const matched = searchWith(
5135
+ "git",
5136
+ root,
5137
+ ["-c", "core.quotepath=off", "grep", "--no-color", "-l", "-I", "-z", "-F", "--untracked"],
5138
+ anchors,
5139
+ deadline2
5140
+ );
5141
+ if (matched) {
5142
+ return combine(
5143
+ matched.filter((file) => allowed.has(file)),
5144
+ "git-grep"
5145
+ );
5146
+ }
5147
+ }
5148
+ const ripgrep = searchWith(
5149
+ "rg",
5150
+ root,
5151
+ ["--files-with-matches", "--fixed-strings", "--null", "--no-messages"],
5152
+ anchors,
5153
+ deadline2
5154
+ );
5155
+ if (ripgrep) {
5156
+ return combine(
5157
+ ripgrep.map(normalise).filter((file) => allowed.has(file)),
5158
+ "ripgrep"
5159
+ );
5160
+ }
5161
+ return inProcessSearch(root, index.files, anchors, deadline2, considered);
5162
+ }
5163
+ function searchWith(command, root, args, anchors, deadline2) {
5164
+ const timeout = deadline2 - Date.now();
5165
+ if (timeout <= 0) return null;
5166
+ let directory = null;
5167
+ try {
5168
+ directory = mkdtempSync(path7.join(tmpdir(), "veefore-search-"));
5169
+ const patterns = path7.join(directory, "anchors.txt");
5170
+ writeFileSync4(patterns, `${anchors.join("\n")}
5171
+ `);
5172
+ const output = execFileSync(command, [...args, "-f", patterns], {
5173
+ cwd: root,
5174
+ encoding: "utf8",
5175
+ timeout,
5176
+ maxBuffer: 512 * 1024 * 1024,
5177
+ stdio: ["ignore", "pipe", "ignore"]
5178
+ });
5179
+ return output.split("\0").filter((entry) => entry !== "");
5180
+ } catch (error) {
5181
+ const status = error.status;
5182
+ if (status === 1) return [];
5183
+ return null;
5184
+ } finally {
5185
+ if (directory) rmSync3(directory, { recursive: true, force: true });
5186
+ }
5187
+ }
5188
+ function inProcessSearch(root, files, anchors, deadline2, considered) {
5189
+ const needles = anchors.map((anchor) => Buffer.from(anchor, "utf8"));
5190
+ const matched = [];
5191
+ let truncated = false;
5192
+ for (const [position, file] of files.entries()) {
5193
+ if ((position & 63) === 0 && Date.now() > deadline2) {
5194
+ truncated = true;
5195
+ break;
5196
+ }
5197
+ let buffer;
5198
+ try {
5199
+ buffer = readFileSync6(path7.join(root, file));
5200
+ } catch {
5201
+ continue;
5202
+ }
5203
+ const nul = buffer.indexOf(0);
5204
+ if (nul >= 0 && nul < 8192) continue;
5205
+ if (needles.some((needle) => buffer.includes(needle))) matched.push(file);
5206
+ }
5207
+ return { files: matched, method: "in-process", considered, truncated };
5208
+ }
5209
+ function normalise(file) {
5210
+ return file.startsWith(`.${path7.sep}`) || file.startsWith("./") ? file.slice(2) : file;
5211
+ }
5212
+ function tooBigToParse(text) {
5213
+ if (text.length > MAX_PARSE_BYTES) return true;
5214
+ if (text.length < 128 * 1024) return false;
5215
+ const head = text.slice(0, 128 * 1024);
5216
+ const lines = head.split("\n").length;
5217
+ return head.length / lines > 500;
5218
+ }
5219
+ function readFiles(root, files) {
5220
+ const out2 = [];
5221
+ for (const file of files) {
5222
+ let text;
5223
+ try {
5224
+ text = readFileSync6(path7.join(root, file), "utf8");
5225
+ } catch {
5226
+ continue;
5227
+ }
5228
+ if (tooBigToParse(text)) continue;
5229
+ out2.push({ file, text });
5230
+ }
5231
+ return out2;
5232
+ }
5233
+
5234
+ // src/scan.ts
5235
+ var MINOR_AMOUNT_PARAMETER = /["']?\b(amount_minor|amountMinor|amount_cents|amountCents|amount_in_cents|amountInCents|amount_minor_units|amountMinorUnits)\b["']?\s*[:=(]/;
5236
+ var CURRENCY_PARAMETER = /["']?\b(currency_code|currencyCode|currency)\b["']?\s*[:=(]/;
5237
+ function inferMoneyMapping(definition) {
5238
+ const amount = MINOR_AMOUNT_PARAMETER.exec(definition)?.[1] ?? null;
5239
+ const currency = CURRENCY_PARAMETER.exec(definition)?.[1] ?? null;
5240
+ if (!amount && !currency) return null;
5241
+ return {
5242
+ amountParameter: amount,
5243
+ amountUnit: amount ? "MINOR" : null,
5244
+ currencyParameter: currency
5245
+ };
5246
+ }
4905
5247
  var MODEL_HOSTS = [
4906
5248
  "api.openai.com",
4907
5249
  "api.anthropic.com",
@@ -4918,24 +5260,74 @@ var MODEL_SECRET = /^(OPENAI|ANTHROPIC|GOOGLE|GEMINI|AZURE_OPENAI|MISTRAL|GROQ|C
4918
5260
  var SECRET_NAME = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|DSN)(_|$)|^DATABASE_URL$|_CONNECTION_STRING$/;
4919
5261
  var NOT_SECRET = /^(VEEFORE_|NODE_|NEXT_PUBLIC_|VITE_|PUBLIC_|NPM_)/;
4920
5262
  function listSourceFiles(root = process.cwd()) {
4921
- const files = [];
4922
- walk(root, root, files);
4923
- return files;
5263
+ return listSources(root).files;
5264
+ }
5265
+ function listSources(root = process.cwd()) {
5266
+ const index = indexFiles(root);
5267
+ return { files: index.files, truncated: index.truncated, method: index.method };
5268
+ }
5269
+ function discoverAgents(root = process.cwd()) {
5270
+ const scan = scanProject(root);
5271
+ const index = indexFiles(root);
5272
+ const byDirectory = /* @__PURE__ */ new Map();
5273
+ for (const tool of scan.tools) {
5274
+ const directory = nearestManifest(root, tool.location.file);
5275
+ const existing = byDirectory.get(directory);
5276
+ const agent = existing ?? {
5277
+ directory,
5278
+ name: readProjectName(path8.join(root, directory)),
5279
+ files: [],
5280
+ tools: []
5281
+ };
5282
+ if (!agent.files.includes(tool.location.file)) agent.files.push(tool.location.file);
5283
+ agent.tools.push(tool);
5284
+ byDirectory.set(directory, agent);
5285
+ }
5286
+ const known = new Set(scan.tools.map((tool) => tool.location.file));
5287
+ const agentFiles = shortlist(root, index, [...AGENT_ANCHORS]);
5288
+ const unreadable = [];
5289
+ for (const { file, text } of readFiles(root, agentFiles.files)) {
5290
+ if (known.has(file)) continue;
5291
+ const python = file.endsWith(".py");
5292
+ if ((python ? findPythonTools(text) : findJsTools(text)).length > 0) continue;
5293
+ const hint = /\btools?\s*[:=]|\btool_calls\b|\btoolCalls\b|\bfunction_call\b/.exec(text);
5294
+ if (!hint) continue;
5295
+ unreadable.push({
5296
+ file,
5297
+ line: lineLocator(text)(hint.index),
5298
+ reason: `passes tools to a model, but none could be read from this file`
5299
+ });
5300
+ }
5301
+ return {
5302
+ agents: [...byDirectory.values()].sort((a, b) => a.directory.localeCompare(b.directory)),
5303
+ unreadable,
5304
+ filesScanned: scan.filesScanned,
5305
+ truncated: scan.truncated || agentFiles.truncated
5306
+ };
5307
+ }
5308
+ function nearestManifest(root, file) {
5309
+ const MANIFESTS = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml", "setup.py"];
5310
+ let dir = path8.dirname(file);
5311
+ for (let depth = 0; depth < 32; depth += 1) {
5312
+ const absolute = path8.join(root, dir);
5313
+ if (MANIFESTS.some((manifest) => existsSync6(path8.join(absolute, manifest)))) {
5314
+ return dir === "" ? "." : dir;
5315
+ }
5316
+ const parent = path8.dirname(dir);
5317
+ if (parent === dir || dir === "." || dir === "") break;
5318
+ dir = parent;
5319
+ }
5320
+ return ".";
4924
5321
  }
4925
5322
  function scanProject(root = process.cwd()) {
4926
5323
  const framework = detectFramework(root);
4927
- const files = [];
4928
- const truncated = walk(root, root, files);
5324
+ const index = indexFiles(root);
5325
+ const candidates = shortlist(root, index, [...SCAN_ANCHORS]);
5326
+ const truncated = index.truncated || candidates.truncated;
4929
5327
  const tools = [];
4930
5328
  const hosts = /* @__PURE__ */ new Map();
4931
5329
  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
- }
5330
+ for (const { file: relative, text } of readFiles(root, candidates.files)) {
4939
5331
  const python = relative.endsWith(".py");
4940
5332
  const lineOf = lineLocator(text);
4941
5333
  const fileHosts = findHosts(text).map((hit) => ({ ...hit, line: lineOf(hit.index) }));
@@ -4951,7 +5343,7 @@ function scanProject(root = process.cwd()) {
4951
5343
  found.forEach((hit, position) => {
4952
5344
  const end = found[position + 1]?.index ?? text.length;
4953
5345
  const start = hit.index;
4954
- const inRegion = (index) => index >= start && index < end;
5346
+ const inRegion = (index2) => index2 >= start && index2 < end;
4955
5347
  const ownHosts = unique(fileHosts.filter((h) => inRegion(h.index)).map((h) => h.host));
4956
5348
  const ownSecrets = unique(
4957
5349
  fileSecrets.filter((s) => inRegion(s.index) && classifySecret(s.name) === "service").map((s) => s.name)
@@ -4979,7 +5371,9 @@ function scanProject(root = process.cwd()) {
4979
5371
  projectName: readProjectName(root),
4980
5372
  framework,
4981
5373
  entryPoints: findEntryPoints(root),
4982
- filesScanned: files.length,
5374
+ filesScanned: index.files.length,
5375
+ filesParsed: candidates.files.length,
5376
+ searchMethod: candidates.method,
4983
5377
  truncated,
4984
5378
  tools: dedupeTools(tools),
4985
5379
  hosts: [...hosts.entries()].map(([host, locations]) => ({ host, locations })).sort((a, b) => a.host.localeCompare(b.host)),
@@ -5053,18 +5447,8 @@ function findJsTools(text) {
5053
5447
  }
5054
5448
  }
5055
5449
  }
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
- }
5450
+ for (const hit of findFunctionToolSchemas(text)) {
5451
+ hits.push({ ...hit, source: "openai-function" });
5068
5452
  }
5069
5453
  addSchemaTools(hits, text, false);
5070
5454
  if (mcp) {
@@ -5128,6 +5512,71 @@ function findToolSchemas(text, python) {
5128
5512
  }
5129
5513
  return hits;
5130
5514
  }
5515
+ function findFunctionToolSchemas(text) {
5516
+ const hits = [];
5517
+ const NAME = /\bname\s*:\s*(['"`])([\w.-]{1,80})\1/;
5518
+ for (const match of text.matchAll(/\btype\s*:\s*(['"`])function\1/g)) {
5519
+ const at = match.index ?? 0;
5520
+ const from = at + match[0].length;
5521
+ const sibling = /^[^{}]{0,400}?(?<![\w$.])function\s*:\s*\{/.exec(text.slice(from));
5522
+ if (sibling) {
5523
+ const open = from + sibling[0].length - 1;
5524
+ const close = matchingBrace(text, open);
5525
+ if (close > open) {
5526
+ const inner = text.slice(open, close + 1);
5527
+ const found = NAME.exec(inner);
5528
+ if (found) {
5529
+ hits.push({
5530
+ name: found[2],
5531
+ index: open + found.index,
5532
+ description: stringField(inner, "description")
5533
+ });
5534
+ continue;
5535
+ }
5536
+ }
5537
+ }
5538
+ const opener = text.lastIndexOf("{", at);
5539
+ const windows = [
5540
+ [
5541
+ Math.max(opener + 1, at - 400),
5542
+ opener < 0 ? "" : text.slice(Math.max(opener + 1, at - 400), at)
5543
+ ],
5544
+ [from, text.slice(from, from + 400).split(/[{}]/)[0] ?? ""]
5545
+ ];
5546
+ for (const [offset, window] of windows) {
5547
+ const found = NAME.exec(window);
5548
+ if (!found) continue;
5549
+ hits.push({
5550
+ name: found[2],
5551
+ index: offset + found.index,
5552
+ description: stringField(window, "description")
5553
+ });
5554
+ break;
5555
+ }
5556
+ }
5557
+ return hits;
5558
+ }
5559
+ function matchingBrace(text, open) {
5560
+ let depth = 0;
5561
+ for (let index = open; index < text.length; index += 1) {
5562
+ const char = text[index];
5563
+ if (char === '"' || char === "'" || char === "`") {
5564
+ const quote2 = char;
5565
+ index += 1;
5566
+ while (index < text.length && text[index] !== quote2) {
5567
+ if (text[index] === "\\") index += 1;
5568
+ index += 1;
5569
+ }
5570
+ continue;
5571
+ }
5572
+ if (char === "{") depth += 1;
5573
+ else if (char === "}") {
5574
+ depth -= 1;
5575
+ if (depth === 0) return index;
5576
+ }
5577
+ }
5578
+ return -1;
5579
+ }
5131
5580
  function addSchemaTools(hits, text, python) {
5132
5581
  const known = new Set(hits.map((hit) => toToolKey(hit.name)));
5133
5582
  for (const hit of findToolSchemas(text, python)) {
@@ -5419,52 +5868,21 @@ function toToolKey(name) {
5419
5868
  const safe = /^[a-z]/.test(key) ? key : `tool_${key}`;
5420
5869
  return safe.slice(0, 80) || "tool";
5421
5870
  }
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
5871
  function readProjectName(root) {
5454
- const pkg = path7.join(root, "package.json");
5872
+ const pkg = path8.join(root, "package.json");
5455
5873
  if (existsSync6(pkg)) {
5456
5874
  try {
5457
- const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
5875
+ const name = JSON.parse(readFileSync7(pkg, "utf8")).name;
5458
5876
  if (typeof name === "string" && name.trim()) return name.replace(/^@[^/]+\//, "").trim();
5459
5877
  } catch {
5460
5878
  }
5461
5879
  }
5462
- const pyproject = path7.join(root, "pyproject.toml");
5880
+ const pyproject = path8.join(root, "pyproject.toml");
5463
5881
  if (existsSync6(pyproject)) {
5464
- const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
5882
+ const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync7(pyproject, "utf8"))?.[1];
5465
5883
  if (name) return name.trim();
5466
5884
  }
5467
- return path7.basename(root) || null;
5885
+ return path8.basename(root) || null;
5468
5886
  }
5469
5887
  function stringField(body, field) {
5470
5888
  const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
@@ -5519,7 +5937,7 @@ function planRewrite(root, files, toolKeys) {
5519
5937
  for (const file of files) {
5520
5938
  let before;
5521
5939
  try {
5522
- before = readFileSync7(path8.join(root, file), "utf8");
5940
+ before = readFileSync8(path9.join(root, file), "utf8");
5523
5941
  } catch {
5524
5942
  continue;
5525
5943
  }
@@ -5624,27 +6042,20 @@ function rewriteJs(source, wanted) {
5624
6042
  wrap3(key, match.index ?? 0, start, trimEnd(source, start, handler.end));
5625
6043
  }
5626
6044
  }
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));
6045
+ changes.push(...schemaOnly(source, false, wanted, new Set(changes.map((c) => c.key))));
5640
6046
  const wrapped = edits.length > 0;
5641
6047
  let after = applyEdits(source, edits);
5642
6048
  if (wrapped) after = addJsImport(after, alias);
5643
6049
  return { after, changes: dedupeChanges(changes) };
5644
6050
  }
5645
6051
  function addJsImport(source, alias) {
5646
- if (existingGuardImport(source, "js")) return source;
5647
- const specifier = alias === "guard" ? "guard" : `guard as ${alias}`;
6052
+ return addJsSdkImport(source, "guard", alias);
6053
+ }
6054
+ function addJsSdkImport(source, name, alias) {
6055
+ if (existingImport(source, name, "js")) return source;
6056
+ const specifier = alias === name ? name : `${name} as ${alias}`;
6057
+ const joined = joinExistingSdkImport(source, specifier, alias, name);
6058
+ if (joined) return joined;
5648
6059
  const imports = [
5649
6060
  ...source.matchAll(
5650
6061
  /^import\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s*)?['"][^'"\n]+['"]\s*;?[^\S\n]*$/gm
@@ -5657,11 +6068,33 @@ function addJsImport(source, alias) {
5657
6068
  import { ${specifier} } from '@veefore/sdk';${source.slice(at)}`;
5658
6069
  }
5659
6070
  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';`;
6071
+ const line = cjs ? `const { ${alias === name ? name : `${name}: ${alias}`} } = require('@veefore/sdk');` : `import { ${specifier} } from '@veefore/sdk';`;
5661
6072
  const prologue = /^(?:#![^\n]*\n)?(?:\s*(['"])use [a-z ]+\1;?[^\S\n]*\n)*/.exec(source)?.[0] ?? "";
5662
6073
  return `${source.slice(0, prologue.length)}${line}
5663
6074
  ${source.slice(prologue.length)}`;
5664
6075
  }
6076
+ function joinExistingSdkImport(source, specifier, alias, name) {
6077
+ const esm = /import\s*\{([^}]*)\}\s*from\s*(['"])@veefore\/sdk\2/.exec(source);
6078
+ if (esm) {
6079
+ const names = (esm[1] ?? "").trim();
6080
+ const replacement = esm[0].replace(
6081
+ `{${esm[1] ?? ""}}`,
6082
+ names === "" ? `{ ${specifier} }` : `{ ${names}, ${specifier} }`
6083
+ );
6084
+ return source.replace(esm[0], replacement);
6085
+ }
6086
+ const cjs = /const\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])@veefore\/sdk\2\s*\)/.exec(source);
6087
+ if (cjs) {
6088
+ const names = (cjs[1] ?? "").trim();
6089
+ const entry = alias === name ? name : `${name}: ${alias}`;
6090
+ const replacement = cjs[0].replace(
6091
+ `{${cjs[1] ?? ""}}`,
6092
+ names === "" ? `{ ${entry} }` : `{ ${names}, ${entry} }`
6093
+ );
6094
+ return source.replace(cjs[0], replacement);
6095
+ }
6096
+ return null;
6097
+ }
5665
6098
  function rewritePython(source, wanted) {
5666
6099
  const changes = [];
5667
6100
  const lines = source.split("\n");
@@ -5704,7 +6137,7 @@ function rewritePython(source, wanted) {
5704
6137
  reason: `decorate the function passed as func= with @guard(${JSON.stringify(toToolKey(name))})`
5705
6138
  });
5706
6139
  }
5707
- changes.push(...schemaOnly(source, true, wanted));
6140
+ changes.push(...schemaOnly(source, true, wanted, new Set(changes.map((c) => c.key))));
5708
6141
  if (inserts.length === 0) return { after: source, changes: dedupeChanges(changes) };
5709
6142
  const out2 = [...lines];
5710
6143
  for (const insert of [...inserts].sort((a, b) => b.before - a.before)) {
@@ -5914,9 +6347,63 @@ function topLevelString(text, open, close, name) {
5914
6347
  const literal = /^(['"`])([\w .-]{1,80})\1$/.exec(text.slice(value.start, value.end));
5915
6348
  return literal?.[2] ?? null;
5916
6349
  }
5917
- var DISPATCH_NAME = /^(?:execute|run|call|dispatch|handle|invoke|perform)_?(?:tool|tool_?call|function_?call)s?$/i;
6350
+ var DISPATCH_NAME = /^(?:execute|run|call|dispatch|handle|invoke|perform)[A-Za-z0-9_]*?_?(?:tool|tool_?call|function_?call)s?$/i;
5918
6351
  var KEY_PROPERTIES = ["tool", "name", "toolName", "tool_name"];
5919
6352
  var ARG_PROPERTIES = ["params", "args", "arguments", "input", "parameters"];
6353
+ function findRunSites(files, read, toolNames) {
6354
+ const sites = /* @__PURE__ */ new Map();
6355
+ if (toolNames.length === 0) return sites;
6356
+ const patterns = (name) => {
6357
+ const quoted = `['"\`]${escapeForRegex(name)}['"\`]`;
6358
+ return [
6359
+ // `switch (call.tool) { case 'x':` — unmistakable.
6360
+ new RegExp(`case\\s+${quoted}`),
6361
+ // `call.name === 'x'`: the key read off the call being run, which is
6362
+ // what a dispatch does and what a view's own state does not.
6363
+ new RegExp(
6364
+ `\\.\\s*[A-Za-z_$][\\w$]*\\s*={2,3}\\s*${quoted}|${quoted}\\s*={2,3}\\s*[A-Za-z_$][\\w$]*\\s*\\.`
6365
+ ),
6366
+ // `handlers['x']`, `executors['x']`
6367
+ new RegExp(`\\[\\s*${quoted}\\s*\\]`),
6368
+ // `'x': async (args) => …`, a map of handlers by key.
6369
+ new RegExp(`${quoted}\\s*:\\s*(?:async\\b|function\\b|\\(|[\\w$]+\\s*=>)`),
6370
+ // Any other comparison against the key. `!==` is deliberately not one:
6371
+ // a tool is not run where its name is ruled out, and taking the first
6372
+ // of those sent the instruction to a view that merely checks which
6373
+ // tool is selected.
6374
+ new RegExp(`(?<![!<>=])={2,3}\\s*${quoted}|${quoted}\\s*={2,3}`)
6375
+ ];
6376
+ };
6377
+ const best = /* @__PURE__ */ new Map();
6378
+ for (const file of files) {
6379
+ const source = read(file);
6380
+ if (source === null) continue;
6381
+ const lineOf = lineLocator2(source);
6382
+ for (const name of toolNames) {
6383
+ if (!source.includes(name)) continue;
6384
+ for (const [rank, pattern] of patterns(name).entries()) {
6385
+ const found = pattern.exec(source);
6386
+ if (!found) continue;
6387
+ const current = best.get(name);
6388
+ if (current && current.rank <= rank) break;
6389
+ const at = found.index;
6390
+ const start = source.lastIndexOf("\n", at) + 1;
6391
+ const end = source.indexOf("\n", at);
6392
+ best.set(name, {
6393
+ rank,
6394
+ site: {
6395
+ file,
6396
+ line: lineOf(at),
6397
+ code: source.slice(start, end < 0 ? source.length : end).trim().slice(0, 160)
6398
+ }
6399
+ });
6400
+ break;
6401
+ }
6402
+ }
6403
+ }
6404
+ for (const [name, entry] of best) sites.set(name, entry.site);
6405
+ return sites;
6406
+ }
5920
6407
  function planDispatch(files, read, toolNames) {
5921
6408
  let best = null;
5922
6409
  for (const file of files) {
@@ -5938,27 +6425,36 @@ function planDispatch(files, read, toolNames) {
5938
6425
  };
5939
6426
  }
5940
6427
  }
5941
- if (!best || best.fn === null) {
5942
- best = { file, line: lineOf(found.index), score: Number.MAX_SAFE_INTEGER, fn: found.name };
5943
- }
6428
+ best ??= { file, line: lineOf(found.index), fn: found.name };
5944
6429
  }
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 };
6430
+ }
6431
+ const sites = findRunSites(files, read, toolNames);
6432
+ if (!best) {
6433
+ const byFile = /* @__PURE__ */ new Map();
6434
+ for (const site of sites.values()) {
6435
+ byFile.set(site.file, [...byFile.get(site.file) ?? [], site]);
5953
6436
  }
6437
+ const chosen = [...byFile.entries()].sort(
6438
+ (a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0])
6439
+ )[0];
6440
+ if (!chosen) return null;
6441
+ const line = Math.min(...chosen[1].map((site) => site.line));
6442
+ return {
6443
+ file: chosen[0],
6444
+ line,
6445
+ functionName: null,
6446
+ after: null,
6447
+ reason: "this file runs the tools by name",
6448
+ sites
6449
+ };
5954
6450
  }
5955
- if (!best) return null;
5956
6451
  return {
5957
6452
  file: best.file,
5958
6453
  line: best.line,
5959
6454
  functionName: best.fn,
5960
6455
  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"
6456
+ reason: `${best.fn}() runs the tools, but where to put the guard inside it is not certain`,
6457
+ sites
5962
6458
  };
5963
6459
  }
5964
6460
  function dispatchFunctions(source, python) {
@@ -6031,13 +6527,220 @@ function guardDispatch(source, fn, typescript) {
6031
6527
  );
6032
6528
  return { after, at: target.start };
6033
6529
  }
6530
+ var CHECKPOINT_ARG_PROPERTIES = ["args", "arguments", "params", "parameters", "input", "payload"];
6531
+ function planCheckpoints(files, read, toolKeys) {
6532
+ const plans = [];
6533
+ const outstanding = new Set(toolKeys);
6534
+ if (outstanding.size === 0) return plans;
6535
+ for (const file of files) {
6536
+ if (outstanding.size === 0) break;
6537
+ if (file.endsWith(".py") || !JS_EXTENSIONS.test(file)) continue;
6538
+ const source = read(file);
6539
+ if (source === null) continue;
6540
+ const present = [...outstanding].filter(
6541
+ (key) => source.includes(key) || source.includes(camelOf(key))
6542
+ );
6543
+ if (present.length === 0) continue;
6544
+ const scopes = functionScopes(source);
6545
+ const lineOf = lineLocator2(source);
6546
+ const alias = chooseAliasFor(source, "checkpoint", "js");
6547
+ const edits = [];
6548
+ const placed = [];
6549
+ for (const key of present) {
6550
+ const site = checkpointSite(source, key, scopes);
6551
+ if (!site) continue;
6552
+ const indent = blockIndent(source, site.at);
6553
+ const args = site.argsExpression ? `, ${site.argsExpression}` : "";
6554
+ edits.push({
6555
+ start: site.at,
6556
+ end: site.at,
6557
+ text: `
6558
+ ${indent}await ${alias}(${quote(key)}${args});`
6559
+ });
6560
+ placed.push({ key, line: lineOf(site.at), shape: site.shape });
6561
+ outstanding.delete(key);
6562
+ }
6563
+ if (edits.length === 0) continue;
6564
+ const after = addJsSdkImport(applyEdits(source, edits), "checkpoint", alias);
6565
+ plans.push({ file, before: source, after, placed });
6566
+ }
6567
+ return plans;
6568
+ }
6569
+ function checkpointSite(source, key, scopes) {
6570
+ const quoted = `['"\`]${escapeForRegex(key)}['"\`]`;
6571
+ const usable = (at) => enclosingAsync(scopes, at) === true;
6572
+ for (const match of source.matchAll(new RegExp(`\\bcase\\s+${quoted}\\s*:`, "g"))) {
6573
+ const colon = (match.index ?? 0) + match[0].length;
6574
+ const braced = /^\s*\{/.exec(source.slice(colon));
6575
+ const at = braced ? colon + braced[0].length : colon;
6576
+ if (!usable(at)) continue;
6577
+ return { at, shape: "case", argsExpression: switchArguments(source, match.index ?? 0) };
6578
+ }
6579
+ const comparison = new RegExp(
6580
+ `(?:if|else\\s+if)\\s*\\(([^()]{0,200}?)${quoted}([^()]{0,200}?)\\)\\s*\\{`,
6581
+ "g"
6582
+ );
6583
+ for (const match of source.matchAll(comparison)) {
6584
+ const left = match[1] ?? "";
6585
+ if (!/(?<![!<>])={2,3}\s*$/.test(left)) continue;
6586
+ const at = (match.index ?? 0) + match[0].length;
6587
+ if (!usable(at)) continue;
6588
+ const object = /([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*[A-Za-z_$][\w$]*\s*={2,3}\s*$/.exec(left);
6589
+ return {
6590
+ at,
6591
+ shape: "branch",
6592
+ argsExpression: object ? argumentsOf(source, object[1]) : null
6593
+ };
6594
+ }
6595
+ const declaration = new RegExp(
6596
+ `\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=\\n]{0,120})?=[^;\\n]{0,400}?${quoted}[^;]{0,200}?;`,
6597
+ "g"
6598
+ );
6599
+ for (const match of source.matchAll(declaration)) {
6600
+ const name = match[1];
6601
+ const end = (match.index ?? 0) + match[0].length;
6602
+ const head = new RegExp(`^\\s*if\\s*\\(\\s*${escapeForRegex(name)}\\b`).exec(
6603
+ source.slice(end, end + 200)
6604
+ );
6605
+ if (!head) continue;
6606
+ const parenOpen = end + head[0].lastIndexOf("(", head[0].length - name.length);
6607
+ const parenClose = matchBracket(source, parenOpen);
6608
+ if (parenClose < 0) continue;
6609
+ const brace = /^\s*\{/.exec(source.slice(parenClose + 1));
6610
+ if (!brace) continue;
6611
+ const at = parenClose + 1 + brace[0].length;
6612
+ if (!usable(at)) continue;
6613
+ return { at, shape: "found", argsExpression: argumentsOf(source, name) };
6614
+ }
6615
+ const handler = new RegExp(
6616
+ `${quoted}\\s*:\\s*(?:async\\s*)?\\(([^)]{0,200})\\)\\s*(?::[^=>{]{0,120})?=>\\s*\\{`,
6617
+ "g"
6618
+ );
6619
+ for (const match of source.matchAll(handler)) {
6620
+ const at = (match.index ?? 0) + match[0].length;
6621
+ if (!usable(at)) continue;
6622
+ return { at, shape: "handler", argsExpression: parameterExpression(match[1] ?? "") };
6623
+ }
6624
+ const camel = camelOf(key);
6625
+ const named = new RegExp(
6626
+ `\\basync\\s+function\\s+(?:${escapeForRegex(camel)}|${escapeForRegex(key)})\\s*\\(`,
6627
+ "g"
6628
+ );
6629
+ for (const match of source.matchAll(named)) {
6630
+ const parenOpen = (match.index ?? 0) + match[0].length - 1;
6631
+ const parenClose = matchBracket(source, parenOpen);
6632
+ if (parenClose < 0) continue;
6633
+ const bodyOpen = source.indexOf("{", parenClose);
6634
+ if (bodyOpen < 0) continue;
6635
+ if (!/^\s*(?::[^{;]*)?$/.test(source.slice(parenClose + 1, bodyOpen))) continue;
6636
+ return {
6637
+ at: bodyOpen + 1,
6638
+ shape: "function",
6639
+ argsExpression: parameterExpression(source.slice(parenOpen + 1, parenClose))
6640
+ };
6641
+ }
6642
+ return null;
6643
+ }
6644
+ function parameterExpression(parameters) {
6645
+ const names = parameters.split(",").map((part) => /^\s*(?:\.\.\.)?([A-Za-z_$][\w$]*)/.exec(part)?.[1]).filter((name) => name !== void 0);
6646
+ const named = names.find((name) => CHECKPOINT_ARG_PROPERTIES.includes(name));
6647
+ if (named) return named;
6648
+ return names.length === 1 ? names[0] : null;
6649
+ }
6650
+ function camelOf(key) {
6651
+ return key.replace(/_([a-z0-9])/g, (_match, char) => char.toUpperCase());
6652
+ }
6653
+ function switchArguments(source, at) {
6654
+ const before = source.slice(Math.max(0, at - 4e3), at);
6655
+ const found = [
6656
+ ...before.matchAll(/\bswitch\s*\(\s*([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*[A-Za-z_$][\w$]*/g)
6657
+ ];
6658
+ const last = found[found.length - 1];
6659
+ return last ? argumentsOf(source, last[1]) : null;
6660
+ }
6661
+ function argumentsOf(source, object) {
6662
+ for (const property of CHECKPOINT_ARG_PROPERTIES) {
6663
+ const pattern = new RegExp(`\\b${escapeForRegex(object)}\\s*(?:\\?\\.|\\.)\\s*${property}\\b`);
6664
+ if (pattern.test(source)) return `${object}.${property}`;
6665
+ }
6666
+ return null;
6667
+ }
6668
+ function blockIndent(source, at) {
6669
+ const lineEnd = source.indexOf("\n", at);
6670
+ if (lineEnd >= 0) {
6671
+ const next = /^([ \t]*)\S/.exec(source.slice(lineEnd + 1, lineEnd + 200));
6672
+ if (next && !source.slice(lineEnd + 1).trimStart().startsWith("}"))
6673
+ return next[1];
6674
+ }
6675
+ const lineStart = source.lastIndexOf("\n", at) + 1;
6676
+ return `${/^[ \t]*/.exec(source.slice(lineStart, at))?.[0] ?? ""} `;
6677
+ }
6678
+ function functionScopes(source) {
6679
+ const scopes = [];
6680
+ const add = (isAsync, parenOpen, arrow) => {
6681
+ const parenClose = matchBracket(source, parenOpen);
6682
+ if (parenClose < 0) return;
6683
+ const bodyOpen = source.indexOf("{", parenClose);
6684
+ if (bodyOpen < 0) return;
6685
+ const between = source.slice(parenClose + 1, bodyOpen);
6686
+ if (arrow !== /=>/.test(between)) return;
6687
+ if (!/^\s*(?::[^{;=]*)?(?:=>\s*)?$/.test(between)) return;
6688
+ const bodyClose = matchBracket(source, bodyOpen);
6689
+ if (bodyClose < 0) return;
6690
+ scopes.push({ bodyOpen, bodyClose, async: isAsync });
6691
+ };
6692
+ for (const match of source.matchAll(/\b(async\s+)?function\s*\*?\s*[A-Za-z_$][\w$]*\s*\(/g)) {
6693
+ add(match[1] !== void 0, (match.index ?? 0) + match[0].length - 1, false);
6694
+ }
6695
+ for (const match of source.matchAll(/(\basync\s*)?\(/g)) {
6696
+ add(match[1] !== void 0, (match.index ?? 0) + match[0].length - 1, true);
6697
+ }
6698
+ const method = /^[ \t]*((?:(?:public|private|protected|static|override|async|get|set)\s+)*)\*?\s*([A-Za-z_$][\w$]*)\s*\(/gm;
6699
+ for (const match of source.matchAll(method)) {
6700
+ if (CONTROL_KEYWORDS.has(match[2])) continue;
6701
+ add(/\basync\b/.test(match[1] ?? ""), (match.index ?? 0) + match[0].length - 1, false);
6702
+ }
6703
+ for (const match of source.matchAll(/\b(async\s+)?([A-Za-z_$][\w$]*)\s*=>\s*\{/g)) {
6704
+ const bodyOpen = (match.index ?? 0) + match[0].length - 1;
6705
+ const bodyClose = matchBracket(source, bodyOpen);
6706
+ if (bodyClose > 0) scopes.push({ bodyOpen, bodyClose, async: match[1] !== void 0 });
6707
+ }
6708
+ return scopes;
6709
+ }
6710
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
6711
+ "if",
6712
+ "else",
6713
+ "for",
6714
+ "while",
6715
+ "do",
6716
+ "switch",
6717
+ "catch",
6718
+ "with",
6719
+ "return",
6720
+ "typeof",
6721
+ "new",
6722
+ "await",
6723
+ "function",
6724
+ "throw",
6725
+ "case"
6726
+ ]);
6727
+ function enclosingAsync(scopes, at) {
6728
+ let best = null;
6729
+ for (const scope of scopes) {
6730
+ if (at <= scope.bodyOpen || at > scope.bodyClose) continue;
6731
+ if (!best || scope.bodyOpen > best.bodyOpen) best = scope;
6732
+ }
6733
+ return best ? best.async : null;
6734
+ }
6034
6735
  function unguardJs(source) {
6035
- const imported = jsGuardImport(source);
6036
- if (!imported) return { after: source, removed: 0, leftovers: [] };
6736
+ const checkpoints = uncheckpointJs(source);
6737
+ const imported = jsGuardImport(checkpoints.after);
6738
+ if (!imported) return checkpoints;
6739
+ source = checkpoints.after;
6037
6740
  const { alias } = imported;
6038
6741
  const lineOf = lineLocator2(source);
6039
6742
  const edits = [];
6040
- const leftovers = [];
6743
+ const leftovers = [...checkpoints.leftovers];
6041
6744
  const calls = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`, "g");
6042
6745
  for (const match of source.matchAll(calls)) {
6043
6746
  const start = match.index ?? 0;
@@ -6076,8 +6779,35 @@ function unguardJs(source) {
6076
6779
  }
6077
6780
  let after = applyEdits(source, edits);
6078
6781
  const stillUsed = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`).test(after);
6079
- if (!stillUsed) after = removeJsGuardImport(after);
6080
- return { after, removed: edits.length, leftovers };
6782
+ if (!stillUsed) after = removeJsSdkImport(after, "guard");
6783
+ return { after, removed: edits.length + checkpoints.removed, leftovers };
6784
+ }
6785
+ function uncheckpointJs(source) {
6786
+ const alias = existingImport(source, "checkpoint", "js");
6787
+ if (!alias) return { after: source, removed: 0, leftovers: [] };
6788
+ const lineOf = lineLocator2(source);
6789
+ const leftovers = [];
6790
+ let removed = 0;
6791
+ let after = source.replace(
6792
+ new RegExp(
6793
+ `^[ \\t]*(?:await[ \\t]+)?${escapeForRegex(alias)}\\(\\s*(['"\`])[a-z][a-z0-9_]*\\1[^\\n]*\\)[ \\t]*;?[ \\t]*\\r?\\n`,
6794
+ "gm"
6795
+ ),
6796
+ () => {
6797
+ removed += 1;
6798
+ return "";
6799
+ }
6800
+ );
6801
+ for (const match of after.matchAll(
6802
+ new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`, "g")
6803
+ )) {
6804
+ leftovers.push({
6805
+ line: lineOf(match.index ?? 0),
6806
+ text: after.slice(match.index ?? 0, (match.index ?? 0) + 80).split("\n")[0] ?? ""
6807
+ });
6808
+ }
6809
+ if (leftovers.length === 0) after = removeJsSdkImport(after, "checkpoint");
6810
+ return { after, removed, leftovers };
6081
6811
  }
6082
6812
  function unguardPython(source) {
6083
6813
  const imported = /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)$/m.exec(source);
@@ -6168,29 +6898,28 @@ function jsGuardImport(source) {
6168
6898
  }
6169
6899
  return null;
6170
6900
  }
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));
6901
+ function removeJsSdkImport(source, name) {
6902
+ const escaped = escapeForRegex(name);
6903
+ const drop = (names, rebuild, target) => {
6904
+ const rest = names.split(",").map((part) => part.trim()).filter((part) => part && !target.test(part));
6174
6905
  return rest.length === 0 ? "" : rebuild(rest);
6175
6906
  };
6176
6907
  let after = source.replace(
6177
6908
  /^[ \t]*import\s*\{([^}]*)\}\s*from\s*(['"])@veefore\/sdk\2\s*;?[ \t]*\r?\n?/m,
6178
- (full, names, quote2) => drop(
6179
- full,
6909
+ (_full, names, quote2) => drop(
6180
6910
  names,
6181
6911
  (rest) => `import { ${rest.join(", ")} } from ${quote2}@veefore/sdk${quote2};
6182
6912
  `,
6183
- /^guard(\s+as\s+[\w$]+)?$/
6913
+ new RegExp(`^${escaped}(\\s+as\\s+[\\w$]+)?$`)
6184
6914
  )
6185
6915
  );
6186
6916
  after = after.replace(
6187
6917
  /^[ \t]*const\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])@veefore\/sdk\2\s*\)\s*;?[ \t]*\r?\n?/m,
6188
- (full, names, quote2) => drop(
6189
- full,
6918
+ (_full, names, quote2) => drop(
6190
6919
  names,
6191
6920
  (rest) => `const { ${rest.join(", ")} } = require(${quote2}@veefore/sdk${quote2});
6192
6921
  `,
6193
- /^guard(\s*:\s*[\w$]+)?$/
6922
+ new RegExp(`^${escaped}(\\s*:\\s*[\\w$]+)?$`)
6194
6923
  )
6195
6924
  );
6196
6925
  return after;
@@ -6201,22 +6930,35 @@ function escapeForRegex(value) {
6201
6930
  function isGuarded(value, alias) {
6202
6931
  return new RegExp(`^\\s*(${alias}|guard|veeforeGuard)\\(`).test(value);
6203
6932
  }
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) {
6933
+ function existingImport(source, name, language) {
6934
+ const escaped = escapeForRegex(name);
6208
6935
  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;
6936
+ const match2 = new RegExp(
6937
+ `^from\\s+veefore\\s+import\\s+[^\\n]*\\b${escaped}\\b(?:\\s+as\\s+(\\w+))?`,
6938
+ "m"
6939
+ ).exec(source);
6940
+ return match2 ? match2[1] ?? name : null;
6941
+ }
6942
+ const esm = new RegExp(
6943
+ `import\\s*\\{[^}]*\\b${escaped}\\b(?:\\s+as\\s+([A-Za-z_$][\\w$]*))?[^}]*\\}\\s*from\\s*['"]@veefore/sdk['"]`
6944
+ ).exec(source);
6945
+ const cjs = new RegExp(
6946
+ `\\{[^}]*\\b${escaped}\\b(?:\\s*:\\s*([A-Za-z_$][\\w$]*))?[^}]*\\}\\s*=\\s*require\\(\\s*['"]@veefore/sdk['"]\\s*\\)`
6947
+ ).exec(source);
6948
+ const match = esm ?? cjs;
6949
+ return match ? match[1] ?? name : null;
6214
6950
  }
6215
- function chooseAlias(source, language) {
6216
- const existing = existingGuardImport(source, language);
6951
+ function existingGuardImport(source, language) {
6952
+ return existingImport(source, "guard", language);
6953
+ }
6954
+ function chooseAliasFor(source, name, language) {
6955
+ const existing = existingImport(source, name, language);
6217
6956
  if (existing) return existing;
6218
- if (!/\bguard\b/.test(source)) return "guard";
6219
- return language === "py" ? "veefore_guard" : "veeforeGuard";
6957
+ if (!new RegExp(`\\b${escapeForRegex(name)}\\b`).test(source)) return name;
6958
+ return language === "py" ? `veefore_${name}` : `veefore${name[0]?.toUpperCase() ?? ""}${name.slice(1)}`;
6959
+ }
6960
+ function chooseAlias(source, language) {
6961
+ return chooseAliasFor(source, "guard", language);
6220
6962
  }
6221
6963
  function applyEdits(source, edits) {
6222
6964
  let out2 = source;
@@ -6225,14 +6967,22 @@ function applyEdits(source, edits) {
6225
6967
  }
6226
6968
  return out2;
6227
6969
  }
6228
- function schemaOnly(source, python, wanted) {
6970
+ 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)";
6971
+ function schemaOnly(source, python, wanted, placed = /* @__PURE__ */ new Set()) {
6229
6972
  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
- }));
6973
+ const hits = [
6974
+ ...findToolSchemas(source, python).map((hit) => ({ name: hit.name, index: hit.index })),
6975
+ ...python ? [] : findFunctionToolSchemas(source).map((hit) => ({ name: hit.name, index: hit.index }))
6976
+ ].sort((a, b) => a.index - b.index);
6977
+ const changes = [];
6978
+ const seen = new Set(placed);
6979
+ for (const hit of hits) {
6980
+ const key = toToolKey(hit.name);
6981
+ if (!wanted.has(key) || seen.has(key)) continue;
6982
+ seen.add(key);
6983
+ changes.push({ key, line: lineOf(hit.index), status: "manual", reason: SCHEMA_ONLY_REASON });
6984
+ }
6985
+ return changes;
6236
6986
  }
6237
6987
  function dedupeChanges(changes) {
6238
6988
  const seen = /* @__PURE__ */ new Set();
@@ -6352,10 +7102,30 @@ function myers(a, b) {
6352
7102
 
6353
7103
  // src/integrate.ts
6354
7104
  async function integrate(root, toolKeys, options) {
6355
- const plan = planRewrite(root, listSourceFiles(root), toolKeys);
7105
+ const index = indexFiles(root);
7106
+ const anchors = [...toolKeyAnchors(toolKeys), ...TOOL_ANCHORS, ...GUARD_ANCHORS];
7107
+ const candidates = shortlist(root, index, anchors);
7108
+ const sourceFiles = candidates.files;
7109
+ const truncated = index.truncated || candidates.truncated;
7110
+ const plan = planRewrite(root, sourceFiles, toolKeys);
6356
7111
  const interactive = options.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true && !options.yes);
6357
- const dispatchGuardFiles = findDispatchGuards(readSources(root));
7112
+ if (truncated && !options.quiet) {
7113
+ out(
7114
+ `${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.`
7115
+ );
7116
+ }
7117
+ const sources = readSources(root, sourceFiles);
7118
+ const dispatchGuardFiles = findDispatchGuards(sources);
6358
7119
  const dispatchGuarded = dispatchGuardFiles.length > 0;
7120
+ const alreadyGuarded = guardedToolKeys(sources, toolKeys);
7121
+ for (const file of plan.files) {
7122
+ for (const change of file.changes) {
7123
+ if (change.status === "manual" && alreadyGuarded.has(change.key)) {
7124
+ change.status = "already";
7125
+ delete change.reason;
7126
+ }
7127
+ }
7128
+ }
6359
7129
  let manual = plan.files.flatMap(
6360
7130
  (file) => file.changes.filter((change) => change.status === "manual").map((change) => ({
6361
7131
  file: file.file,
@@ -6379,7 +7149,7 @@ async function integrate(root, toolKeys, options) {
6379
7149
  }
6380
7150
  dispatch = [];
6381
7151
  } else if (dispatch.length > 0) {
6382
- dispatchPlan = planDispatch(listSourceFiles(root), (file) => currentText(root, plan, file), [
7152
+ dispatchPlan = planDispatch(sourceFiles, (file) => currentText(root, plan, file), [
6383
7153
  ...new Set(dispatch.map((item) => item.key))
6384
7154
  ]);
6385
7155
  if (dispatchPlan?.after) {
@@ -6387,7 +7157,22 @@ async function integrate(root, toolKeys, options) {
6387
7157
  dispatch = [];
6388
7158
  }
6389
7159
  }
6390
- const dispatchKeys = [...new Set(dispatch.map((item) => item.key))];
7160
+ if (dispatch.length > 0 && checkpointUsable(root, options.quiet === true)) {
7161
+ const placed = applyCheckpoints(
7162
+ plan,
7163
+ planCheckpoints(sourceFiles, (file) => currentText(root, plan, file), [
7164
+ ...new Set(dispatch.map((item) => item.key))
7165
+ ])
7166
+ );
7167
+ dispatch = dispatch.filter((item) => !placed.has(item.key));
7168
+ }
7169
+ const runsElsewhere = (key) => {
7170
+ const site = dispatchPlan?.sites?.get(key);
7171
+ return site !== void 0 && site.file !== dispatchPlan?.file;
7172
+ };
7173
+ const dispatchKeys = [
7174
+ ...new Set(dispatch.filter((item) => !runsElsewhere(item.key)).map((item) => item.key))
7175
+ ];
6391
7176
  const dispatchItems = dispatchPlan ? dispatchKeys.map((key) => ({
6392
7177
  file: dispatchPlan?.file,
6393
7178
  key,
@@ -6395,7 +7180,7 @@ async function integrate(root, toolKeys, options) {
6395
7180
  reason: `guard the code that runs the tool: ${dispatchPlan?.reason ?? ""}`,
6396
7181
  language: languageOf(dispatchPlan?.file)
6397
7182
  })) : [];
6398
- const unplaced = dispatchPlan ? [] : dispatch;
7183
+ const unplaced = dispatchPlan ? dispatch.filter((item) => runsElsewhere(item.key)) : dispatch;
6399
7184
  const pending = [...manual, ...dispatchItems];
6400
7185
  if (pending.length > 0 && !options.dryRun) {
6401
7186
  const hostedAvailable = await hostedEnabled(options.api ?? null);
@@ -6450,8 +7235,9 @@ async function integrate(root, toolKeys, options) {
6450
7235
  const entry = plan.files.find((file) => file.file === proposal.file);
6451
7236
  if (!entry) continue;
6452
7237
  entry.after = proposal.proposal;
7238
+ const guarded = findGuardedKeys([{ file: proposal.file, text: proposal.proposal }]);
6453
7239
  for (const change of entry.changes) {
6454
- if (change.status === "manual" && proposal.keys.includes(change.key)) {
7240
+ if (change.status === "manual" && guarded.has(change.key)) {
6455
7241
  change.status = "wrapped";
6456
7242
  change.via = "ai";
6457
7243
  delete change.reason;
@@ -6465,29 +7251,37 @@ async function integrate(root, toolKeys, options) {
6465
7251
  dispatch = remaining.filter(isDispatchItem);
6466
7252
  if (method !== "skip" && !options.quiet) {
6467
7253
  if (manual.length > 0) printManualInstructions(manual);
6468
- if (dispatch.length > 0) printDispatchInstructions(dispatch, dispatchPlan);
7254
+ printDispatchInstructions([...dispatch, ...unplaced], dispatchPlan);
6469
7255
  }
6470
7256
  } else if (pending.length > 0 && !options.quiet) {
6471
- if (dispatchItems.length > 0) printDispatchInstructions(dispatchItems, dispatchPlan);
7257
+ printDispatchInstructions([...dispatchItems, ...unplaced], dispatchPlan);
6472
7258
  dispatch = dispatchItems;
7259
+ } else if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
7260
+ printDispatchInstructions(unplaced, dispatchPlan);
6473
7261
  }
6474
- if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
6475
- printDispatchInstructions(unplaced, null);
6476
- }
6477
- manual = [...manual, ...dispatch, ...unplaced];
7262
+ manual = stillManual([...manual, ...dispatch, ...unplaced], plan);
6478
7263
  const toWrite = plan.files.filter((file) => file.before !== file.after);
6479
7264
  const languages = new Set(toWrite.map((file) => file.file.endsWith(".py") ? "py" : "js"));
6480
7265
  if (!options.quiet) printPlan(plan);
6481
7266
  if (toWrite.length === 0) {
6482
- if (!options.quiet && !options.dryRun) {
7267
+ const quiet = options.quiet === true || options.dryRun === true;
7268
+ if (!quiet) {
6483
7269
  const guardedFiles = plan.files.filter((file) => file.changes.some((change) => change.status !== "manual")).map((file) => file.file);
6484
7270
  reportSdkReadiness(root, [.../* @__PURE__ */ new Set([...guardedFiles, ...dispatchGuardFiles])]);
6485
7271
  }
6486
- return { status: "nothing-to-do", plan, checks: [], backupId: null, manual };
7272
+ const unguarded2 = reportProtection(root, sourceFiles, toolKeys, quiet);
7273
+ return { status: "nothing-to-do", plan, checks: [], backupId: null, manual, unguarded: unguarded2 };
6487
7274
  }
6488
7275
  if (options.dryRun) {
6489
7276
  out(style.grey("Dry run: nothing was written."));
6490
- return { status: "dry-run", plan, checks: [], backupId: null, manual };
7277
+ return {
7278
+ status: "dry-run",
7279
+ plan,
7280
+ checks: [],
7281
+ backupId: null,
7282
+ manual,
7283
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7284
+ };
6491
7285
  }
6492
7286
  if (!options.quiet) {
6493
7287
  reportSdkReadiness(
@@ -6501,7 +7295,14 @@ async function integrate(root, toolKeys, options) {
6501
7295
  );
6502
7296
  if (!proceed) {
6503
7297
  out(style.grey("No files were changed. Run `veefore integrate` when you are ready."));
6504
- return { status: "declined", plan, checks: [], backupId: null, manual };
7298
+ return {
7299
+ status: "declined",
7300
+ plan,
7301
+ checks: [],
7302
+ backupId: null,
7303
+ manual,
7304
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7305
+ };
6505
7306
  }
6506
7307
  const jsReady = languages.has("js") && sdkInstalled(root, "js");
6507
7308
  const baseline = jsReady ? typecheck(
@@ -6509,13 +7310,20 @@ async function integrate(root, toolKeys, options) {
6509
7310
  toWrite.map((file) => file.file)
6510
7311
  ) : null;
6511
7312
  for (const file of toWrite) {
6512
- if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
7313
+ if (readFileSync9(path10.join(root, file.file), "utf8") !== file.before) {
6513
7314
  out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
6514
- return { status: "declined", plan, checks: [], backupId: null, manual };
7315
+ return {
7316
+ status: "declined",
7317
+ plan,
7318
+ checks: [],
7319
+ backupId: null,
7320
+ manual,
7321
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7322
+ };
6515
7323
  }
6516
7324
  }
6517
7325
  const backup = createBackup(root, toWrite);
6518
- for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
7326
+ for (const file of toWrite) writeFilePreservingMode(path10.join(root, file.file), file.after);
6519
7327
  out(
6520
7328
  `${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
6521
7329
  );
@@ -6571,7 +7379,14 @@ async function integrate(root, toolKeys, options) {
6571
7379
  `${symbol.warn} A check failed after the change, so every file was put back as it was ${style.grey(`(${restored.restored.length} restored)`)}.`
6572
7380
  );
6573
7381
  out(style.grey(" Please report this: the rewrite should never break a project."));
6574
- return { status: "rolled-back", plan, checks, backupId: null, manual };
7382
+ return {
7383
+ status: "rolled-back",
7384
+ plan,
7385
+ checks,
7386
+ backupId: null,
7387
+ manual,
7388
+ unguarded: reportProtection(root, sourceFiles, toolKeys, true)
7389
+ };
6575
7390
  }
6576
7391
  for (const language of languages) {
6577
7392
  if (!sdkInstalled(root, language)) {
@@ -6580,13 +7395,34 @@ async function integrate(root, toolKeys, options) {
6580
7395
  );
6581
7396
  }
6582
7397
  }
7398
+ const unguarded = reportProtection(root, sourceFiles, toolKeys, options.quiet === true);
6583
7399
  out(style.grey(" Undo at any time with `veefore undo`."));
6584
- return { status: "applied", plan, checks, backupId: backup.id, manual };
7400
+ return { status: "applied", plan, checks, backupId: backup.id, manual, unguarded };
7401
+ }
7402
+ function reportProtection(root, files, toolKeys, quiet) {
7403
+ const guarded = guardedToolKeys(readSources(root, files), toolKeys);
7404
+ const unguarded = toolKeys.filter((key) => !guarded.has(key));
7405
+ if (quiet) return unguarded;
7406
+ if (unguarded.length === 0) {
7407
+ out(
7408
+ `${symbol.pass} Every registered tool goes through Veefore ${style.grey(`(${toolKeys.length})`)}`
7409
+ );
7410
+ return unguarded;
7411
+ }
7412
+ out(
7413
+ `${symbol.warn} ${toolKeys.length - unguarded.length} of ${toolKeys.length} registered tools go through Veefore. Still unprotected: ${style.bold(unguarded.join(", "))}`
7414
+ );
7415
+ out(
7416
+ style.grey(
7417
+ " The instructions above say what each one needs. Then `veefore integrate --check` to confirm."
7418
+ )
7419
+ );
7420
+ return unguarded;
6585
7421
  }
6586
7422
  function reportSdkReadiness(root, files) {
6587
7423
  const problems = [];
6588
7424
  if (files.length === 0) return problems;
6589
- const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
7425
+ const dirs = [...new Set(files.map((file) => path10.dirname(file)))];
6590
7426
  const python = files.some((file) => file.endsWith(".py"));
6591
7427
  const sdks = installedSdks(root, { dirs, python });
6592
7428
  for (const sdk of sdks) {
@@ -6663,7 +7499,7 @@ async function removeGuards(root, options) {
6663
7499
  const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
6664
7500
  const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
6665
7501
  const backup = createBackup(root, toWrite);
6666
- for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
7502
+ for (const entry of toWrite) writeFilePreservingMode(path10.join(root, entry.file), entry.after);
6667
7503
  const checks = [];
6668
7504
  const py = changed.filter((file) => file.endsWith(".py"));
6669
7505
  if (py.length > 0) checks.push(pythonSyntax(root, py));
@@ -6687,7 +7523,7 @@ function currentText(root, plan, file) {
6687
7523
  const entry = plan.files.find((candidate) => candidate.file === file);
6688
7524
  if (entry) return entry.after;
6689
7525
  try {
6690
- return readFileSync8(path9.join(root, file), "utf8");
7526
+ return readFileSync9(path10.join(root, file), "utf8");
6691
7527
  } catch {
6692
7528
  return null;
6693
7529
  }
@@ -6709,17 +7545,76 @@ function applyDispatch(root, plan, file, after, line, items, via) {
6709
7545
  entry.changes.push({ key, line, status: "wrapped", ...via ? { via } : {} });
6710
7546
  }
6711
7547
  }
6712
- function readSources(root) {
6713
- return listSourceFiles(root).flatMap((file) => {
7548
+ function stillManual(items, plan) {
7549
+ const open = new Set(
7550
+ plan.files.flatMap(
7551
+ (file) => file.changes.filter((change) => change.status === "manual").map((change) => change.key)
7552
+ )
7553
+ );
7554
+ const seen = /* @__PURE__ */ new Set();
7555
+ return items.filter((item) => {
7556
+ if (!open.has(item.key)) return false;
7557
+ const id = isDispatchItem(item) ? item.key : `${item.key}\0${item.file}`;
7558
+ if (seen.has(id)) return false;
7559
+ seen.add(id);
7560
+ return true;
7561
+ });
7562
+ }
7563
+ function checkpointUsable(root, quiet) {
7564
+ const sdks = installedSdks(root).filter((sdk) => sdk.language === "js");
7565
+ const stale = sdks.filter((sdk) => !sdk.hasCheckpoint);
7566
+ if (sdks.length === 0 || stale.length === 0) return true;
7567
+ if (!quiet) {
7568
+ out(
7569
+ `${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.`
7570
+ );
7571
+ out(
7572
+ ` Upgrade and re-run: ${style.bold(`${installCommand(root, "js")}@^${MIN_CHECKPOINT_SDK}`)}`
7573
+ );
7574
+ }
7575
+ return false;
7576
+ }
7577
+ function applyCheckpoints(plan, plans) {
7578
+ const placed = /* @__PURE__ */ new Set();
7579
+ for (const checkpoint of plans) {
7580
+ let entry = plan.files.find((candidate) => candidate.file === checkpoint.file);
7581
+ if (!entry) {
7582
+ entry = {
7583
+ file: checkpoint.file,
7584
+ before: checkpoint.before,
7585
+ after: checkpoint.after,
7586
+ changes: []
7587
+ };
7588
+ plan.files.push(entry);
7589
+ }
7590
+ entry.after = checkpoint.after;
7591
+ for (const place of checkpoint.placed) {
7592
+ placed.add(place.key);
7593
+ entry.changes.push({
7594
+ key: place.key,
7595
+ line: place.line,
7596
+ status: "wrapped",
7597
+ via: "checkpoint"
7598
+ });
7599
+ }
7600
+ }
7601
+ for (const file of plan.files) {
7602
+ file.changes = file.changes.filter(
7603
+ (change) => !(change.status === "manual" && placed.has(change.key) && isDispatchItem({ reason: change.reason ?? "" }))
7604
+ );
7605
+ }
7606
+ return placed;
7607
+ }
7608
+ function readSources(root, files = listSourceFiles(root)) {
7609
+ return files.flatMap((file) => {
6714
7610
  try {
6715
- return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
7611
+ return [{ file, text: readFileSync9(path10.join(root, file), "utf8") }];
6716
7612
  } catch {
6717
7613
  return [];
6718
7614
  }
6719
7615
  });
6720
7616
  }
6721
- function checkIntegration(root, toolKeys) {
6722
- const sources = readSources(root);
7617
+ function guardedToolKeys(sources, toolKeys) {
6723
7618
  const guarded = findGuardedKeys(sources);
6724
7619
  const dispatchFiles = findDispatchGuards(sources);
6725
7620
  if (dispatchFiles.length > 0) {
@@ -6734,6 +7629,10 @@ function checkIntegration(root, toolKeys) {
6734
7629
  }
6735
7630
  }
6736
7631
  }
7632
+ return new Map([...guarded].filter(([key]) => toolKeys.includes(key)));
7633
+ }
7634
+ function checkIntegration(root, toolKeys) {
7635
+ const guarded = guardedToolKeys(readSources(root), toolKeys);
6737
7636
  heading("Protection check");
6738
7637
  for (const key of toolKeys) {
6739
7638
  const file = guarded.get(key);
@@ -6796,22 +7695,31 @@ function printPlan(plan) {
6796
7695
  const wrapped = all.filter((entry) => entry.change.status === "wrapped");
6797
7696
  const already = all.filter((entry) => entry.change.status === "already");
6798
7697
  const manual = all.filter((entry) => entry.change.status === "manual");
7698
+ const tools = (entries) => new Set(entries.map((e) => e.change.key)).size;
7699
+ const keys = (entries) => [...new Set(entries.map((e) => e.change.key))];
6799
7700
  if (wrapped.length > 0) {
7701
+ const count = tools(wrapped);
7702
+ const only = new Set(wrapped.map((entry) => entry.change.via ?? "guard"));
7703
+ const how = only.size === 1 && only.has("checkpoint") ? `gated with ${style.bold("checkpoint()")} where your code runs them` : `wrapped with ${style.bold("guard()")}`;
6800
7704
  out(
6801
- `${symbol.pass} ${wrapped.length} tool${wrapped.length === 1 ? "" : "s"} will be wrapped with ${style.bold("guard()")}, so Veefore decides each call:`
7705
+ `${symbol.pass} ${count} tool${count === 1 ? "" : "s"} will be ${how}, so Veefore decides each call:`
6802
7706
  );
7707
+ const note = {
7708
+ ai: style.amber("proposed by AI \u2014 review the diff below"),
7709
+ checkpoint: style.grey("guarded where your code runs it")
7710
+ };
6803
7711
  for (const { file, change } of wrapped) {
6804
7712
  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")}` : "")
7713
+ ` ${style.bold(change.key.padEnd(24))} ${style.grey(`${file}:${change.line}`)}` + (change.via ? ` ${note[change.via] ?? ""}` : "")
6806
7714
  );
6807
7715
  }
6808
7716
  }
6809
7717
  if (already.length > 0) {
6810
- out(`${symbol.pass} Already protected: ${already.map((entry) => entry.change.key).join(", ")}`);
7718
+ out(`${symbol.pass} Already protected: ${keys(already).join(", ")}`);
6811
7719
  }
6812
7720
  if (manual.length > 0) {
6813
7721
  out(
6814
- `${symbol.warn} Not changed automatically: ${manual.map((entry) => entry.change.key).join(", ")} ${style.grey("(see instructions above)")}`
7722
+ `${symbol.warn} Not changed automatically: ${keys(manual).join(", ")} ${style.grey("(see instructions above)")}`
6815
7723
  );
6816
7724
  }
6817
7725
  if (plan.notFound.length > 0) {
@@ -6833,8 +7741,8 @@ function printPlan(plan) {
6833
7741
  }
6834
7742
 
6835
7743
  // src/commands/remove.ts
6836
- import { rmSync as rmSync3 } from "node:fs";
6837
- import path10 from "node:path";
7744
+ import { rmSync as rmSync4 } from "node:fs";
7745
+ import path11 from "node:path";
6838
7746
 
6839
7747
  // src/discover.ts
6840
7748
  var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
@@ -6948,7 +7856,7 @@ async function removeCommand(args, flags, known) {
6948
7856
  if (!file || !config) {
6949
7857
  throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
6950
7858
  }
6951
- const root = path10.dirname(file);
7859
+ const root = path11.dirname(file);
6952
7860
  const state = known ?? await agentState(config);
6953
7861
  heading("Remove Veefore from this project");
6954
7862
  details([
@@ -6999,7 +7907,7 @@ async function removeCommand(args, flags, known) {
6999
7907
  out(style.grey("The project stays connected."));
7000
7908
  return 0;
7001
7909
  }
7002
- rmSync3(file);
7910
+ rmSync4(file);
7003
7911
  const envRemoved = removeLocalEnvBlock(root);
7004
7912
  out(
7005
7913
  `${symbol.pass} Disconnected: removed veefore.config.json${envRemoved ? " and the Veefore block in .env.local" : ""}.`
@@ -7083,6 +7991,7 @@ async function integrateCommand(args, flags) {
7083
7991
  files: outcome.plan.files.map((file) => ({ file: file.file, changes: file.changes })),
7084
7992
  not_found: outcome.plan.notFound,
7085
7993
  manual: outcome.manual,
7994
+ unguarded: outcome.unguarded,
7086
7995
  checks: outcome.checks
7087
7996
  },
7088
7997
  null,
@@ -7482,10 +8391,10 @@ async function resolveAuthorizedTools(context, filter) {
7482
8391
 
7483
8392
  // src/panel.ts
7484
8393
  import { homedir as homedir2 } from "node:os";
7485
- import path11 from "node:path";
8394
+ import path12 from "node:path";
7486
8395
 
7487
8396
  // src/version.ts
7488
- var CLI_VERSION = "1.4.1";
8397
+ var CLI_VERSION = "1.5.0";
7489
8398
 
7490
8399
  // src/panel.ts
7491
8400
  function welcomeSource(flags = {}) {
@@ -7509,7 +8418,7 @@ function welcomeSource(flags = {}) {
7509
8418
  }
7510
8419
  function tilde(dir) {
7511
8420
  const home = homedir2();
7512
- return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
8421
+ return dir === home || dir.startsWith(`${home}${path12.sep}`) ? `~${dir.slice(home.length)}` : dir;
7513
8422
  }
7514
8423
  function hostOf(url) {
7515
8424
  try {
@@ -7576,7 +8485,7 @@ function planTool(tool) {
7576
8485
  }
7577
8486
  function printScan(scan) {
7578
8487
  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")
8488
+ `${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
8489
  );
7581
8490
  if (scan.tools.length === 0) {
7582
8491
  out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
@@ -7609,6 +8518,40 @@ function printScan(scan) {
7609
8518
  `${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
8519
  );
7611
8520
  }
8521
+ printGaps(scan.root);
8522
+ }
8523
+ function printGaps(root) {
8524
+ let discovery;
8525
+ try {
8526
+ discovery = discoverAgents(root);
8527
+ } catch {
8528
+ return;
8529
+ }
8530
+ if (discovery.agents.length > 1) {
8531
+ out();
8532
+ out(`${symbol.info} ${discovery.agents.length} agents found in this project:`);
8533
+ for (const agent of discovery.agents) {
8534
+ out(
8535
+ ` ${style.bold(agent.name ?? agent.directory)} ${style.grey(`${agent.directory} \u2014 ${agent.tools.length} tool${agent.tools.length === 1 ? "" : "s"}`)}`
8536
+ );
8537
+ }
8538
+ out(style.grey(" Set each one up from its own directory, so its tools stay its own."));
8539
+ }
8540
+ if (discovery.unreadable.length === 0) return;
8541
+ const shown = discovery.unreadable.slice(0, 5);
8542
+ out();
8543
+ out(
8544
+ `${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:`
8545
+ );
8546
+ for (const gap of shown) out(` ${style.grey(`${gap.file}:${gap.line}`)}`);
8547
+ if (discovery.unreadable.length > shown.length) {
8548
+ out(style.grey(` \u2026and ${discovery.unreadable.length - shown.length} more`));
8549
+ }
8550
+ out(
8551
+ style.grey(
8552
+ " 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."
8553
+ )
8554
+ );
7612
8555
  }
7613
8556
  function printPlan2(plan, environment) {
7614
8557
  out();
@@ -8570,7 +9513,7 @@ async function initCommand(args, flags) {
8570
9513
  }
8571
9514
  function previousApprovalMode(dir = process.cwd()) {
8572
9515
  try {
8573
- const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
9516
+ const text = readFileSync10(path13.join(dir, ".env.local"), "utf8");
8574
9517
  const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
8575
9518
  return value === "wait" || value === "throw" ? value : null;
8576
9519
  } catch {