veefore 1.4.0 → 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 +1221 -274
  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),
@@ -586,16 +586,6 @@ var LOGO = [
586
586
  " \u2588\u2588 \u2588\u2588 \u2588\u2588",
587
587
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588"
588
588
  ];
589
- var LOGO_SMALL = [
590
- "\u2580\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u2580",
591
- " \u2580\u2584 \u2584\u2584 \u2580\u2588 \u2588\u2580 \u2584\u2584 \u2584\u2588",
592
- " \u2588\u2584 \u2588 \u2588\u2580 \u2588 \u2584\u2588",
593
- " \u2588\u2584 \u2588 \u2588\u2580 \u2588 \u2584\u2588",
594
- " \u2588 \u2580\u2588\u2584\u2580 \u2588\u2580 \u2588",
595
- " \u2588 \u2580\u2588 \u2588\u2580 \u2588",
596
- " \u2580\u2588 \u2584\u2580 \u2588\u2580",
597
- " \u2580\u2588\u2584\u2584\u2588\u2588 \u2588\u2580"
598
- ];
599
589
 
600
590
  // src/tui.ts
601
591
  function isInteractive() {
@@ -655,7 +645,7 @@ var STARS = [
655
645
  ];
656
646
  async function showWelcome(context, options = {}) {
657
647
  if (!isInteractive()) return;
658
- const lines = welcomeLines(context, options.reserve ?? 0);
648
+ const lines = welcomeLines(context);
659
649
  if ((options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0) {
660
650
  for (const line of lines) {
661
651
  out(line);
@@ -665,9 +655,9 @@ async function showWelcome(context, options = {}) {
665
655
  }
666
656
  for (const line of lines) out(line);
667
657
  }
668
- function welcomeLines(context, reserve) {
658
+ function welcomeLines(context) {
669
659
  const width = Math.min(columns(), 100);
670
- const art = rows() >= LOGO.length + 6 + reserve ? LOGO : LOGO_SMALL;
660
+ const art = LOGO;
671
661
  const logoWidth = Math.max(...art.map((line) => [...line].length));
672
662
  const indent = 5;
673
663
  const gap = 6;
@@ -677,7 +667,7 @@ function welcomeLines(context, reserve) {
677
667
  );
678
668
  const sideBySide = width >= indent + logoWidth + gap + panelWidth + 2;
679
669
  const panel = renderPanel(context.facts);
680
- const showLogo = width >= indent + logoWidth + 2 && (sideBySide || rows() >= art.length + 6 + panel.length + 2 + reserve);
670
+ const showLogo = width >= indent + logoWidth + 2;
681
671
  const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
682
672
  const lines = [];
683
673
  const add = (line = "") => void lines.push(line);
@@ -735,7 +725,7 @@ function renderPanel(facts) {
735
725
  }
736
726
  function clearScreen() {
737
727
  if (!isInteractive()) return;
738
- process.stdout.write("\x1B[H\x1B[2J");
728
+ process.stdout.write("\x1B[H\x1B[J");
739
729
  }
740
730
  var appDepth = 0;
741
731
  var restoreInstalled = false;
@@ -749,7 +739,7 @@ function enterApp() {
749
739
  if (appDepth > 0) process.stdout.write("\x1B[?25h\x1B[?1049l");
750
740
  });
751
741
  }
752
- process.stdout.write("\x1B[?1049h\x1B[H\x1B[2J");
742
+ process.stdout.write("\x1B[?1049h\x1B[?25l\x1B[H\x1B[J");
753
743
  }
754
744
  function leaveApp() {
755
745
  if (!isInteractive() || appDepth === 0) return;
@@ -757,6 +747,9 @@ function leaveApp() {
757
747
  if (appDepth > 0) return;
758
748
  process.stdout.write("\x1B[?25h\x1B[?1049l");
759
749
  }
750
+ function inApp() {
751
+ return appDepth > 0;
752
+ }
760
753
  async function onOrdinaryScreen(body) {
761
754
  const depth = appDepth;
762
755
  if (depth > 0) {
@@ -768,35 +761,19 @@ async function onOrdinaryScreen(body) {
768
761
  } finally {
769
762
  if (depth > 0) {
770
763
  appDepth = depth;
771
- process.stdout.write("\x1B[?1049h\x1B[H\x1B[2J");
764
+ process.stdout.write("\x1B[?1049h\x1B[H\x1B[J");
772
765
  }
773
766
  }
774
767
  }
775
- function compactHeader(context) {
776
- const facts = Object.fromEntries(context.facts);
777
- const parts = [
778
- brand(style.bold("Veefore")),
779
- style.grey(`v${context.version}`),
780
- ...[facts.Folder, facts["Signed in"], facts.Veefore].filter((v) => Boolean(v))
781
- ];
782
- out();
783
- out(fit(` ${parts.join(style.grey(" \xB7 "))}`, columns() - 1));
784
- out(` ${style.grey("\xB7".repeat(Math.max(10, Math.min(columns() - 2, 72))))}`);
785
- }
786
768
  async function beginScreen(source, options = {}) {
787
769
  if (!isInteractive()) return;
788
770
  clearScreen();
789
- const reserve = options.reserve ?? 0;
790
- if (options.compact || rows() < LOGO_SMALL.length + 6 + reserve) compactHeader(source());
791
- else await showWelcome(source(), { animate: options.animate ?? false, reserve });
771
+ await showWelcome(source(), { animate: options.animate ?? false });
792
772
  }
793
- function drawScreen(source, options = {}) {
773
+ function drawScreen(source) {
794
774
  if (!isInteractive()) return;
795
775
  clearScreen();
796
- const reserve = options.reserve ?? 0;
797
- const context = source();
798
- if (options.compact || rows() < LOGO_SMALL.length + 6 + reserve) compactHeader(context);
799
- else for (const line of welcomeLines(context, reserve)) out(line);
776
+ for (const line of welcomeLines(source())) out(line);
800
777
  }
801
778
  async function showOutput(lines, options) {
802
779
  if (!isInteractive()) {
@@ -987,14 +964,19 @@ async function select(question, real, settings = {}) {
987
964
  return new Promise((resolve, reject) => {
988
965
  const wasRaw = stdin.isRaw;
989
966
  const restoreCursor = () => {
990
- stdout.write("\x1B[?25h");
967
+ if (!inApp()) stdout.write("\x1B[?25h");
991
968
  };
992
969
  const stopWatching = settings.redraw ? watchFacts(repaint) : void 0;
970
+ const onResize = () => {
971
+ if (settings.redraw) repaint();
972
+ else draw();
973
+ };
993
974
  const finish = (result) => {
994
975
  stdin.off("keypress", onKey);
995
976
  stdin.setRawMode(wasRaw);
996
977
  stdin.pause();
997
978
  stopWatching?.();
979
+ process.stdout.off("resize", onResize);
998
980
  process.off("exit", restoreCursor);
999
981
  if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
1000
982
  restoreCursor();
@@ -1044,6 +1026,7 @@ async function select(question, real, settings = {}) {
1044
1026
  stdin.resume();
1045
1027
  stdout.write("\x1B[?25l");
1046
1028
  stdin.on("keypress", onKey);
1029
+ process.stdout.on("resize", onResize);
1047
1030
  draw();
1048
1031
  });
1049
1032
  }
@@ -1963,11 +1946,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
1963
1946
  out(` ${style.red("This action is irreversible for this identity.")}`);
1964
1947
  out();
1965
1948
  }
1966
- async function lifecycle(args, flags, path13, resultingStatus) {
1949
+ async function lifecycle(args, flags, path14, resultingStatus) {
1967
1950
  const context = await resolveContext(flags);
1968
1951
  const agentId = positional(args) ?? context.config.agentId;
1969
1952
  const agent = await context.api.get(`/v1/agents/${agentId}`);
1970
- const reasonRequired = path13 === "suspend";
1953
+ const reasonRequired = path14 === "suspend";
1971
1954
  const reason = valueFor(args, "--reason");
1972
1955
  if (reasonRequired && (!reason || reason.trim().length < 10)) {
1973
1956
  throw new CliError(
@@ -1977,24 +1960,24 @@ async function lifecycle(args, flags, path13, resultingStatus) {
1977
1960
  }
1978
1961
  out();
1979
1962
  out(
1980
- `${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})`)}`
1981
1964
  );
1982
- if (path13 !== "resume") {
1965
+ if (path14 !== "resume") {
1983
1966
  const confirmed = await confirm("Continue?", { yes: flags.yes });
1984
1967
  if (!confirmed) {
1985
1968
  out(style.grey("Cancelled."));
1986
1969
  return 1;
1987
1970
  }
1988
1971
  }
1989
- const actionSpinner = spinner(`Applying ${path13}`);
1972
+ const actionSpinner = spinner(`Applying ${path14}`);
1990
1973
  try {
1991
1974
  await withStepUp(
1992
1975
  context.api,
1993
- () => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
1976
+ () => context.api.post(`/v1/agents/${agentId}/${path14}`, reason ? { reason } : {})
1994
1977
  );
1995
1978
  actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
1996
1979
  } catch (error) {
1997
- actionSpinner.fail(`Could not ${path13} the agent`);
1980
+ actionSpinner.fail(`Could not ${path14} the agent`);
1998
1981
  throw error;
1999
1982
  }
2000
1983
  out();
@@ -2306,6 +2289,7 @@ import { spawnSync } from "node:child_process";
2306
2289
  import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
2307
2290
  import path3 from "node:path";
2308
2291
  var MIN_APPROVAL_WAIT_SDK = "1.1.0";
2292
+ var MIN_CHECKPOINT_SDK = "1.2.0";
2309
2293
  function versionLessThan(a, b) {
2310
2294
  if (!a) return true;
2311
2295
  const left = a.split(/[.+-]/).slice(0, 3).map(Number);
@@ -2352,7 +2336,8 @@ function installedSdks(root, options = {}) {
2352
2336
  language: "js",
2353
2337
  location: path3.relative(top, path3.dirname(manifest)) || ".",
2354
2338
  version,
2355
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2339
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2340
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2356
2341
  });
2357
2342
  }
2358
2343
  if (options.python) {
@@ -2369,7 +2354,8 @@ function installedSdks(root, options = {}) {
2369
2354
  language: "py",
2370
2355
  location: "python: veefore-py",
2371
2356
  version,
2372
- waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2357
+ waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK),
2358
+ hasCheckpoint: !versionLessThan(version, MIN_CHECKPOINT_SDK)
2373
2359
  });
2374
2360
  }
2375
2361
  }
@@ -3317,12 +3303,12 @@ function isLoosening(from, to) {
3317
3303
  }
3318
3304
 
3319
3305
  // src/commands/init.ts
3320
- import { readFileSync as readFileSync9 } from "node:fs";
3321
- import path12 from "node:path";
3306
+ import { readFileSync as readFileSync10 } from "node:fs";
3307
+ import path13 from "node:path";
3322
3308
 
3323
3309
  // src/integrate.ts
3324
- import { readFileSync as readFileSync8 } from "node:fs";
3325
- import path9 from "node:path";
3310
+ import { readFileSync as readFileSync9 } from "node:fs";
3311
+ import path10 from "node:path";
3326
3312
 
3327
3313
  // ../types/src/permissions.ts
3328
3314
  var PERMISSIONS = [
@@ -3659,6 +3645,11 @@ var EVENT_TYPES = [
3659
3645
  "user.profile_updated",
3660
3646
  "onboarding.completed",
3661
3647
  "session.revoked",
3648
+ // Sign-in is served from its own origin, so a session cannot be carried
3649
+ // across to the console by cookie; it is re-issued there from a one-time
3650
+ // code. Recorded at both ends, so the trail shows one session ending and
3651
+ // its replacement beginning rather than an unexplained gap.
3652
+ "session.handed_over",
3662
3653
  "cli.authorization_approved",
3663
3654
  "cli.authorization_denied",
3664
3655
  "cli.session_issued",
@@ -4201,10 +4192,33 @@ function printDispatchInstructions(items, where) {
4201
4192
  out(
4202
4193
  ` ${items.length} tool${items.length === 1 ? " is" : "s are"} defined as data, so no one function performs each tool.`
4203
4194
  );
4204
- if (where) {
4205
- out(
4206
- ` Your code runs them in ${style.bold(`${where.file}:${where.line}`)}${where.functionName ? ` (${where.functionName})` : ""}.`
4207
- );
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)}`);
4208
4222
  out(" Wrap the call there that performs the chosen tool, once:");
4209
4223
  } else {
4210
4224
  out(" Wrap the code that runs whichever tool the model picked, once:");
@@ -4222,7 +4236,9 @@ function printDispatchInstructions(items, where) {
4222
4236
  out(` ${style.cyan("import { guard } from '@veefore/sdk';")}`);
4223
4237
  out();
4224
4238
  out(` ${style.grey("// before: const result = await runTool(call);")}`);
4225
- 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
+ );
4226
4242
  }
4227
4243
  out();
4228
4244
  out(
@@ -4250,7 +4266,7 @@ function toCamel(key) {
4250
4266
  }
4251
4267
  function findGuardedKeys(sources) {
4252
4268
  const found = /* @__PURE__ */ new Map();
4253
- 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;
4254
4270
  for (const { file, text } of sources) {
4255
4271
  for (const match of text.matchAll(pattern)) {
4256
4272
  const key = match[1];
@@ -4663,12 +4679,95 @@ function restoreBackup(root, backup, options = {}) {
4663
4679
  }
4664
4680
 
4665
4681
  // src/rewrite.ts
4666
- import { readFileSync as readFileSync7 } from "node:fs";
4667
- import path8 from "node:path";
4682
+ import { readFileSync as readFileSync8 } from "node:fs";
4683
+ import path9 from "node:path";
4668
4684
 
4669
4685
  // src/scan.ts
4670
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
4671
- 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
+ ];
4672
4771
 
4673
4772
  // src/framework.ts
4674
4773
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
@@ -4864,19 +4963,11 @@ function integrationSnippet(framework) {
4864
4963
  ].join("\n");
4865
4964
  }
4866
4965
 
4867
- // src/scan.ts
4868
- var MINOR_AMOUNT_PARAMETER = /["']?\b(amount_minor|amountMinor|amount_cents|amountCents|amount_in_cents|amountInCents|amount_minor_units|amountMinorUnits)\b["']?\s*[:=(]/;
4869
- var CURRENCY_PARAMETER = /["']?\b(currency_code|currencyCode|currency)\b["']?\s*[:=(]/;
4870
- function inferMoneyMapping(definition) {
4871
- const amount = MINOR_AMOUNT_PARAMETER.exec(definition)?.[1] ?? null;
4872
- const currency = CURRENCY_PARAMETER.exec(definition)?.[1] ?? null;
4873
- if (!amount && !currency) return null;
4874
- return {
4875
- amountParameter: amount,
4876
- amountUnit: amount ? "MINOR" : null,
4877
- currencyParameter: currency
4878
- };
4879
- }
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";
4880
4971
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
4881
4972
  ".ts",
4882
4973
  ".tsx",
@@ -4912,8 +5003,247 @@ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
4912
5003
  "tests",
4913
5004
  "test"
4914
5005
  ]);
4915
- var MAX_FILES = 2e3;
4916
- 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
+ }
4917
5247
  var MODEL_HOSTS = [
4918
5248
  "api.openai.com",
4919
5249
  "api.anthropic.com",
@@ -4930,24 +5260,74 @@ var MODEL_SECRET = /^(OPENAI|ANTHROPIC|GOOGLE|GEMINI|AZURE_OPENAI|MISTRAL|GROQ|C
4930
5260
  var SECRET_NAME = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|DSN)(_|$)|^DATABASE_URL$|_CONNECTION_STRING$/;
4931
5261
  var NOT_SECRET = /^(VEEFORE_|NODE_|NEXT_PUBLIC_|VITE_|PUBLIC_|NPM_)/;
4932
5262
  function listSourceFiles(root = process.cwd()) {
4933
- const files = [];
4934
- walk(root, root, files);
4935
- 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 ".";
4936
5321
  }
4937
5322
  function scanProject(root = process.cwd()) {
4938
5323
  const framework = detectFramework(root);
4939
- const files = [];
4940
- 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;
4941
5327
  const tools = [];
4942
5328
  const hosts = /* @__PURE__ */ new Map();
4943
5329
  const secrets = /* @__PURE__ */ new Map();
4944
- for (const relative of files) {
4945
- let text;
4946
- try {
4947
- text = readFileSync6(path7.join(root, relative), "utf8");
4948
- } catch {
4949
- continue;
4950
- }
5330
+ for (const { file: relative, text } of readFiles(root, candidates.files)) {
4951
5331
  const python = relative.endsWith(".py");
4952
5332
  const lineOf = lineLocator(text);
4953
5333
  const fileHosts = findHosts(text).map((hit) => ({ ...hit, line: lineOf(hit.index) }));
@@ -4963,7 +5343,7 @@ function scanProject(root = process.cwd()) {
4963
5343
  found.forEach((hit, position) => {
4964
5344
  const end = found[position + 1]?.index ?? text.length;
4965
5345
  const start = hit.index;
4966
- const inRegion = (index) => index >= start && index < end;
5346
+ const inRegion = (index2) => index2 >= start && index2 < end;
4967
5347
  const ownHosts = unique(fileHosts.filter((h) => inRegion(h.index)).map((h) => h.host));
4968
5348
  const ownSecrets = unique(
4969
5349
  fileSecrets.filter((s) => inRegion(s.index) && classifySecret(s.name) === "service").map((s) => s.name)
@@ -4991,7 +5371,9 @@ function scanProject(root = process.cwd()) {
4991
5371
  projectName: readProjectName(root),
4992
5372
  framework,
4993
5373
  entryPoints: findEntryPoints(root),
4994
- filesScanned: files.length,
5374
+ filesScanned: index.files.length,
5375
+ filesParsed: candidates.files.length,
5376
+ searchMethod: candidates.method,
4995
5377
  truncated,
4996
5378
  tools: dedupeTools(tools),
4997
5379
  hosts: [...hosts.entries()].map(([host, locations]) => ({ host, locations })).sort((a, b) => a.host.localeCompare(b.host)),
@@ -5065,18 +5447,8 @@ function findJsTools(text) {
5065
5447
  }
5066
5448
  }
5067
5449
  }
5068
- for (const match of text.matchAll(/type\s*:\s*['"]function['"]/g)) {
5069
- const index = match.index ?? 0;
5070
- const body = text.slice(index, index + 400);
5071
- const name = stringField(body, "name");
5072
- if (name) {
5073
- hits.push({
5074
- name,
5075
- source: "openai-function",
5076
- index,
5077
- description: stringField(body, "description")
5078
- });
5079
- }
5450
+ for (const hit of findFunctionToolSchemas(text)) {
5451
+ hits.push({ ...hit, source: "openai-function" });
5080
5452
  }
5081
5453
  addSchemaTools(hits, text, false);
5082
5454
  if (mcp) {
@@ -5140,6 +5512,71 @@ function findToolSchemas(text, python) {
5140
5512
  }
5141
5513
  return hits;
5142
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
+ }
5143
5580
  function addSchemaTools(hits, text, python) {
5144
5581
  const known = new Set(hits.map((hit) => toToolKey(hit.name)));
5145
5582
  for (const hit of findToolSchemas(text, python)) {
@@ -5431,52 +5868,21 @@ function toToolKey(name) {
5431
5868
  const safe = /^[a-z]/.test(key) ? key : `tool_${key}`;
5432
5869
  return safe.slice(0, 80) || "tool";
5433
5870
  }
5434
- function walk(root, dir, out2) {
5435
- let entries;
5436
- try {
5437
- entries = readdirSync3(dir).sort();
5438
- } catch {
5439
- return false;
5440
- }
5441
- for (const entry of entries) {
5442
- if (out2.length >= MAX_FILES) return true;
5443
- if (entry.startsWith(".") && entry !== ".") {
5444
- continue;
5445
- }
5446
- const full = path7.join(dir, entry);
5447
- let stats;
5448
- try {
5449
- stats = statSync4(full);
5450
- } catch {
5451
- continue;
5452
- }
5453
- if (stats.isDirectory()) {
5454
- if (SKIP_DIRECTORIES.has(entry)) continue;
5455
- if (walk(root, full, out2)) return true;
5456
- continue;
5457
- }
5458
- if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
5459
- if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
5460
- if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
5461
- out2.push(path7.relative(root, full));
5462
- }
5463
- return false;
5464
- }
5465
5871
  function readProjectName(root) {
5466
- const pkg = path7.join(root, "package.json");
5872
+ const pkg = path8.join(root, "package.json");
5467
5873
  if (existsSync6(pkg)) {
5468
5874
  try {
5469
- const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
5875
+ const name = JSON.parse(readFileSync7(pkg, "utf8")).name;
5470
5876
  if (typeof name === "string" && name.trim()) return name.replace(/^@[^/]+\//, "").trim();
5471
5877
  } catch {
5472
5878
  }
5473
5879
  }
5474
- const pyproject = path7.join(root, "pyproject.toml");
5880
+ const pyproject = path8.join(root, "pyproject.toml");
5475
5881
  if (existsSync6(pyproject)) {
5476
- 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];
5477
5883
  if (name) return name.trim();
5478
5884
  }
5479
- return path7.basename(root) || null;
5885
+ return path8.basename(root) || null;
5480
5886
  }
5481
5887
  function stringField(body, field) {
5482
5888
  const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
@@ -5531,7 +5937,7 @@ function planRewrite(root, files, toolKeys) {
5531
5937
  for (const file of files) {
5532
5938
  let before;
5533
5939
  try {
5534
- before = readFileSync7(path8.join(root, file), "utf8");
5940
+ before = readFileSync8(path9.join(root, file), "utf8");
5535
5941
  } catch {
5536
5942
  continue;
5537
5943
  }
@@ -5636,27 +6042,20 @@ function rewriteJs(source, wanted) {
5636
6042
  wrap3(key, match.index ?? 0, start, trimEnd(source, start, handler.end));
5637
6043
  }
5638
6044
  }
5639
- for (const match of source.matchAll(/type\s*:\s*['"]function['"]/g)) {
5640
- const name = /\bname\s*:\s*['"]([\w.-]{1,80})['"]/.exec(
5641
- source.slice(match.index ?? 0, (match.index ?? 0) + 400)
5642
- )?.[1];
5643
- if (!name || !wanted.has(toToolKey(name))) continue;
5644
- changes.push({
5645
- key: toToolKey(name),
5646
- line: lineOf(match.index ?? 0),
5647
- status: "manual",
5648
- reason: `this is only the schema; wrap the function your code calls for it, e.g. \`guard(${quote(toToolKey(name))}, yourFunction)\``
5649
- });
5650
- }
5651
- changes.push(...schemaOnly(source, false, wanted));
6045
+ changes.push(...schemaOnly(source, false, wanted, new Set(changes.map((c) => c.key))));
5652
6046
  const wrapped = edits.length > 0;
5653
6047
  let after = applyEdits(source, edits);
5654
6048
  if (wrapped) after = addJsImport(after, alias);
5655
6049
  return { after, changes: dedupeChanges(changes) };
5656
6050
  }
5657
6051
  function addJsImport(source, alias) {
5658
- if (existingGuardImport(source, "js")) return source;
5659
- 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;
5660
6059
  const imports = [
5661
6060
  ...source.matchAll(
5662
6061
  /^import\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s*)?['"][^'"\n]+['"]\s*;?[^\S\n]*$/gm
@@ -5669,11 +6068,33 @@ function addJsImport(source, alias) {
5669
6068
  import { ${specifier} } from '@veefore/sdk';${source.slice(at)}`;
5670
6069
  }
5671
6070
  const cjs = /\brequire\(\s*['"][^'"]+['"]\s*\)/.test(source) && !/^\s*export\s/m.test(source);
5672
- 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';`;
5673
6072
  const prologue = /^(?:#![^\n]*\n)?(?:\s*(['"])use [a-z ]+\1;?[^\S\n]*\n)*/.exec(source)?.[0] ?? "";
5674
6073
  return `${source.slice(0, prologue.length)}${line}
5675
6074
  ${source.slice(prologue.length)}`;
5676
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
+ }
5677
6098
  function rewritePython(source, wanted) {
5678
6099
  const changes = [];
5679
6100
  const lines = source.split("\n");
@@ -5716,7 +6137,7 @@ function rewritePython(source, wanted) {
5716
6137
  reason: `decorate the function passed as func= with @guard(${JSON.stringify(toToolKey(name))})`
5717
6138
  });
5718
6139
  }
5719
- changes.push(...schemaOnly(source, true, wanted));
6140
+ changes.push(...schemaOnly(source, true, wanted, new Set(changes.map((c) => c.key))));
5720
6141
  if (inserts.length === 0) return { after: source, changes: dedupeChanges(changes) };
5721
6142
  const out2 = [...lines];
5722
6143
  for (const insert of [...inserts].sort((a, b) => b.before - a.before)) {
@@ -5926,9 +6347,63 @@ function topLevelString(text, open, close, name) {
5926
6347
  const literal = /^(['"`])([\w .-]{1,80})\1$/.exec(text.slice(value.start, value.end));
5927
6348
  return literal?.[2] ?? null;
5928
6349
  }
5929
- 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;
5930
6351
  var KEY_PROPERTIES = ["tool", "name", "toolName", "tool_name"];
5931
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
+ }
5932
6407
  function planDispatch(files, read, toolNames) {
5933
6408
  let best = null;
5934
6409
  for (const file of files) {
@@ -5950,27 +6425,36 @@ function planDispatch(files, read, toolNames) {
5950
6425
  };
5951
6426
  }
5952
6427
  }
5953
- if (!best || best.fn === null) {
5954
- best = { file, line: lineOf(found.index), score: Number.MAX_SAFE_INTEGER, fn: found.name };
5955
- }
6428
+ best ??= { file, line: lineOf(found.index), fn: found.name };
5956
6429
  }
5957
- const named = toolNames.filter(
5958
- (name) => new RegExp(
5959
- `['"\`]${escapeForRegex(name)}['"\`]\\s*[:)]|case\\s+['"\`]${escapeForRegex(name)}['"\`]`
5960
- ).test(source)
5961
- ).length;
5962
- if (named >= 2 && (!best || named > best.score)) {
5963
- const first = toolNames.map((name) => source.search(new RegExp(`case\\s+['"\`]${escapeForRegex(name)}['"\`]`))).filter((index) => index >= 0).sort((a, b) => a - b)[0];
5964
- 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]);
5965
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
+ };
5966
6450
  }
5967
- if (!best) return null;
5968
6451
  return {
5969
6452
  file: best.file,
5970
6453
  line: best.line,
5971
6454
  functionName: best.fn,
5972
6455
  after: null,
5973
- 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
5974
6458
  };
5975
6459
  }
5976
6460
  function dispatchFunctions(source, python) {
@@ -6043,13 +6527,220 @@ function guardDispatch(source, fn, typescript) {
6043
6527
  );
6044
6528
  return { after, at: target.start };
6045
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
+ }
6046
6735
  function unguardJs(source) {
6047
- const imported = jsGuardImport(source);
6048
- 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;
6049
6740
  const { alias } = imported;
6050
6741
  const lineOf = lineLocator2(source);
6051
6742
  const edits = [];
6052
- const leftovers = [];
6743
+ const leftovers = [...checkpoints.leftovers];
6053
6744
  const calls = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`, "g");
6054
6745
  for (const match of source.matchAll(calls)) {
6055
6746
  const start = match.index ?? 0;
@@ -6088,8 +6779,35 @@ function unguardJs(source) {
6088
6779
  }
6089
6780
  let after = applyEdits(source, edits);
6090
6781
  const stillUsed = new RegExp(`(?<![\\w$.])${escapeForRegex(alias)}\\s*\\(`).test(after);
6091
- if (!stillUsed) after = removeJsGuardImport(after);
6092
- 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 };
6093
6811
  }
6094
6812
  function unguardPython(source) {
6095
6813
  const imported = /^from[ \t]+veefore[ \t]+import[ \t]+([^\n]+)$/m.exec(source);
@@ -6180,29 +6898,28 @@ function jsGuardImport(source) {
6180
6898
  }
6181
6899
  return null;
6182
6900
  }
6183
- function removeJsGuardImport(source) {
6184
- const drop = (full, names, rebuild, isGuard) => {
6185
- 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));
6186
6905
  return rest.length === 0 ? "" : rebuild(rest);
6187
6906
  };
6188
6907
  let after = source.replace(
6189
6908
  /^[ \t]*import\s*\{([^}]*)\}\s*from\s*(['"])@veefore\/sdk\2\s*;?[ \t]*\r?\n?/m,
6190
- (full, names, quote2) => drop(
6191
- full,
6909
+ (_full, names, quote2) => drop(
6192
6910
  names,
6193
6911
  (rest) => `import { ${rest.join(", ")} } from ${quote2}@veefore/sdk${quote2};
6194
6912
  `,
6195
- /^guard(\s+as\s+[\w$]+)?$/
6913
+ new RegExp(`^${escaped}(\\s+as\\s+[\\w$]+)?$`)
6196
6914
  )
6197
6915
  );
6198
6916
  after = after.replace(
6199
6917
  /^[ \t]*const\s*\{([^}]*)\}\s*=\s*require\(\s*(['"])@veefore\/sdk\2\s*\)\s*;?[ \t]*\r?\n?/m,
6200
- (full, names, quote2) => drop(
6201
- full,
6918
+ (_full, names, quote2) => drop(
6202
6919
  names,
6203
6920
  (rest) => `const { ${rest.join(", ")} } = require(${quote2}@veefore/sdk${quote2});
6204
6921
  `,
6205
- /^guard(\s*:\s*[\w$]+)?$/
6922
+ new RegExp(`^${escaped}(\\s*:\\s*[\\w$]+)?$`)
6206
6923
  )
6207
6924
  );
6208
6925
  return after;
@@ -6213,22 +6930,35 @@ function escapeForRegex(value) {
6213
6930
  function isGuarded(value, alias) {
6214
6931
  return new RegExp(`^\\s*(${alias}|guard|veeforeGuard)\\(`).test(value);
6215
6932
  }
6216
- var JS_GUARD_IMPORT = /import\s*\{[^}]*\bguard\b(?:\s+as\s+([A-Za-z_$][\w$]*))?[^}]*\}\s*from\s*['"]@veefore\/sdk['"]/;
6217
- var CJS_GUARD_IMPORT = /\{[^}]*\bguard\b(?:\s*:\s*([A-Za-z_$][\w$]*))?[^}]*\}\s*=\s*require\(\s*['"]@veefore\/sdk['"]\s*\)/;
6218
- var PY_GUARD_IMPORT = /^from\s+veefore\s+import\s+[^\n]*\bguard\b(?:\s+as\s+(\w+))?/m;
6219
- function existingGuardImport(source, language) {
6933
+ function existingImport(source, name, language) {
6934
+ const escaped = escapeForRegex(name);
6220
6935
  if (language === "py") {
6221
- const match2 = PY_GUARD_IMPORT.exec(source);
6222
- return match2 ? match2[1] ?? "guard" : null;
6223
- }
6224
- const match = JS_GUARD_IMPORT.exec(source) ?? CJS_GUARD_IMPORT.exec(source);
6225
- 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;
6226
6950
  }
6227
- function chooseAlias(source, language) {
6228
- 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);
6229
6956
  if (existing) return existing;
6230
- if (!/\bguard\b/.test(source)) return "guard";
6231
- 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);
6232
6962
  }
6233
6963
  function applyEdits(source, edits) {
6234
6964
  let out2 = source;
@@ -6237,14 +6967,22 @@ function applyEdits(source, edits) {
6237
6967
  }
6238
6968
  return out2;
6239
6969
  }
6240
- 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()) {
6241
6972
  const lineOf = lineLocator2(source);
6242
- return findToolSchemas(source, python).filter((hit) => wanted.has(toToolKey(hit.name))).map((hit) => ({
6243
- key: toToolKey(hit.name),
6244
- line: lineOf(hit.index),
6245
- status: "manual",
6246
- reason: "this is only the schema; guard the code that runs the tool the model picked (often one dispatch function for every tool)"
6247
- }));
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;
6248
6986
  }
6249
6987
  function dedupeChanges(changes) {
6250
6988
  const seen = /* @__PURE__ */ new Set();
@@ -6364,10 +7102,30 @@ function myers(a, b) {
6364
7102
 
6365
7103
  // src/integrate.ts
6366
7104
  async function integrate(root, toolKeys, options) {
6367
- 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);
6368
7111
  const interactive = options.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true && !options.yes);
6369
- 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);
6370
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
+ }
6371
7129
  let manual = plan.files.flatMap(
6372
7130
  (file) => file.changes.filter((change) => change.status === "manual").map((change) => ({
6373
7131
  file: file.file,
@@ -6391,7 +7149,7 @@ async function integrate(root, toolKeys, options) {
6391
7149
  }
6392
7150
  dispatch = [];
6393
7151
  } else if (dispatch.length > 0) {
6394
- dispatchPlan = planDispatch(listSourceFiles(root), (file) => currentText(root, plan, file), [
7152
+ dispatchPlan = planDispatch(sourceFiles, (file) => currentText(root, plan, file), [
6395
7153
  ...new Set(dispatch.map((item) => item.key))
6396
7154
  ]);
6397
7155
  if (dispatchPlan?.after) {
@@ -6399,7 +7157,22 @@ async function integrate(root, toolKeys, options) {
6399
7157
  dispatch = [];
6400
7158
  }
6401
7159
  }
6402
- 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
+ ];
6403
7176
  const dispatchItems = dispatchPlan ? dispatchKeys.map((key) => ({
6404
7177
  file: dispatchPlan?.file,
6405
7178
  key,
@@ -6407,7 +7180,7 @@ async function integrate(root, toolKeys, options) {
6407
7180
  reason: `guard the code that runs the tool: ${dispatchPlan?.reason ?? ""}`,
6408
7181
  language: languageOf(dispatchPlan?.file)
6409
7182
  })) : [];
6410
- const unplaced = dispatchPlan ? [] : dispatch;
7183
+ const unplaced = dispatchPlan ? dispatch.filter((item) => runsElsewhere(item.key)) : dispatch;
6411
7184
  const pending = [...manual, ...dispatchItems];
6412
7185
  if (pending.length > 0 && !options.dryRun) {
6413
7186
  const hostedAvailable = await hostedEnabled(options.api ?? null);
@@ -6462,8 +7235,9 @@ async function integrate(root, toolKeys, options) {
6462
7235
  const entry = plan.files.find((file) => file.file === proposal.file);
6463
7236
  if (!entry) continue;
6464
7237
  entry.after = proposal.proposal;
7238
+ const guarded = findGuardedKeys([{ file: proposal.file, text: proposal.proposal }]);
6465
7239
  for (const change of entry.changes) {
6466
- if (change.status === "manual" && proposal.keys.includes(change.key)) {
7240
+ if (change.status === "manual" && guarded.has(change.key)) {
6467
7241
  change.status = "wrapped";
6468
7242
  change.via = "ai";
6469
7243
  delete change.reason;
@@ -6477,29 +7251,37 @@ async function integrate(root, toolKeys, options) {
6477
7251
  dispatch = remaining.filter(isDispatchItem);
6478
7252
  if (method !== "skip" && !options.quiet) {
6479
7253
  if (manual.length > 0) printManualInstructions(manual);
6480
- if (dispatch.length > 0) printDispatchInstructions(dispatch, dispatchPlan);
7254
+ printDispatchInstructions([...dispatch, ...unplaced], dispatchPlan);
6481
7255
  }
6482
7256
  } else if (pending.length > 0 && !options.quiet) {
6483
- if (dispatchItems.length > 0) printDispatchInstructions(dispatchItems, dispatchPlan);
7257
+ printDispatchInstructions([...dispatchItems, ...unplaced], dispatchPlan);
6484
7258
  dispatch = dispatchItems;
7259
+ } else if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
7260
+ printDispatchInstructions(unplaced, dispatchPlan);
6485
7261
  }
6486
- if (unplaced.length > 0 && !options.quiet && options.assist !== "skip") {
6487
- printDispatchInstructions(unplaced, null);
6488
- }
6489
- manual = [...manual, ...dispatch, ...unplaced];
7262
+ manual = stillManual([...manual, ...dispatch, ...unplaced], plan);
6490
7263
  const toWrite = plan.files.filter((file) => file.before !== file.after);
6491
7264
  const languages = new Set(toWrite.map((file) => file.file.endsWith(".py") ? "py" : "js"));
6492
7265
  if (!options.quiet) printPlan(plan);
6493
7266
  if (toWrite.length === 0) {
6494
- if (!options.quiet && !options.dryRun) {
7267
+ const quiet = options.quiet === true || options.dryRun === true;
7268
+ if (!quiet) {
6495
7269
  const guardedFiles = plan.files.filter((file) => file.changes.some((change) => change.status !== "manual")).map((file) => file.file);
6496
7270
  reportSdkReadiness(root, [.../* @__PURE__ */ new Set([...guardedFiles, ...dispatchGuardFiles])]);
6497
7271
  }
6498
- 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 };
6499
7274
  }
6500
7275
  if (options.dryRun) {
6501
7276
  out(style.grey("Dry run: nothing was written."));
6502
- 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
+ };
6503
7285
  }
6504
7286
  if (!options.quiet) {
6505
7287
  reportSdkReadiness(
@@ -6513,7 +7295,14 @@ async function integrate(root, toolKeys, options) {
6513
7295
  );
6514
7296
  if (!proceed) {
6515
7297
  out(style.grey("No files were changed. Run `veefore integrate` when you are ready."));
6516
- 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
+ };
6517
7306
  }
6518
7307
  const jsReady = languages.has("js") && sdkInstalled(root, "js");
6519
7308
  const baseline = jsReady ? typecheck(
@@ -6521,13 +7310,20 @@ async function integrate(root, toolKeys, options) {
6521
7310
  toWrite.map((file) => file.file)
6522
7311
  ) : null;
6523
7312
  for (const file of toWrite) {
6524
- if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
7313
+ if (readFileSync9(path10.join(root, file.file), "utf8") !== file.before) {
6525
7314
  out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
6526
- 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
+ };
6527
7323
  }
6528
7324
  }
6529
7325
  const backup = createBackup(root, toWrite);
6530
- 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);
6531
7327
  out(
6532
7328
  `${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
6533
7329
  );
@@ -6583,7 +7379,14 @@ async function integrate(root, toolKeys, options) {
6583
7379
  `${symbol.warn} A check failed after the change, so every file was put back as it was ${style.grey(`(${restored.restored.length} restored)`)}.`
6584
7380
  );
6585
7381
  out(style.grey(" Please report this: the rewrite should never break a project."));
6586
- 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
+ };
6587
7390
  }
6588
7391
  for (const language of languages) {
6589
7392
  if (!sdkInstalled(root, language)) {
@@ -6592,13 +7395,34 @@ async function integrate(root, toolKeys, options) {
6592
7395
  );
6593
7396
  }
6594
7397
  }
7398
+ const unguarded = reportProtection(root, sourceFiles, toolKeys, options.quiet === true);
6595
7399
  out(style.grey(" Undo at any time with `veefore undo`."));
6596
- 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;
6597
7421
  }
6598
7422
  function reportSdkReadiness(root, files) {
6599
7423
  const problems = [];
6600
7424
  if (files.length === 0) return problems;
6601
- const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
7425
+ const dirs = [...new Set(files.map((file) => path10.dirname(file)))];
6602
7426
  const python = files.some((file) => file.endsWith(".py"));
6603
7427
  const sdks = installedSdks(root, { dirs, python });
6604
7428
  for (const sdk of sdks) {
@@ -6675,7 +7499,7 @@ async function removeGuards(root, options) {
6675
7499
  const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
6676
7500
  const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
6677
7501
  const backup = createBackup(root, toWrite);
6678
- 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);
6679
7503
  const checks = [];
6680
7504
  const py = changed.filter((file) => file.endsWith(".py"));
6681
7505
  if (py.length > 0) checks.push(pythonSyntax(root, py));
@@ -6699,7 +7523,7 @@ function currentText(root, plan, file) {
6699
7523
  const entry = plan.files.find((candidate) => candidate.file === file);
6700
7524
  if (entry) return entry.after;
6701
7525
  try {
6702
- return readFileSync8(path9.join(root, file), "utf8");
7526
+ return readFileSync9(path10.join(root, file), "utf8");
6703
7527
  } catch {
6704
7528
  return null;
6705
7529
  }
@@ -6721,17 +7545,76 @@ function applyDispatch(root, plan, file, after, line, items, via) {
6721
7545
  entry.changes.push({ key, line, status: "wrapped", ...via ? { via } : {} });
6722
7546
  }
6723
7547
  }
6724
- function readSources(root) {
6725
- 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) => {
6726
7610
  try {
6727
- return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
7611
+ return [{ file, text: readFileSync9(path10.join(root, file), "utf8") }];
6728
7612
  } catch {
6729
7613
  return [];
6730
7614
  }
6731
7615
  });
6732
7616
  }
6733
- function checkIntegration(root, toolKeys) {
6734
- const sources = readSources(root);
7617
+ function guardedToolKeys(sources, toolKeys) {
6735
7618
  const guarded = findGuardedKeys(sources);
6736
7619
  const dispatchFiles = findDispatchGuards(sources);
6737
7620
  if (dispatchFiles.length > 0) {
@@ -6746,6 +7629,10 @@ function checkIntegration(root, toolKeys) {
6746
7629
  }
6747
7630
  }
6748
7631
  }
7632
+ return new Map([...guarded].filter(([key]) => toolKeys.includes(key)));
7633
+ }
7634
+ function checkIntegration(root, toolKeys) {
7635
+ const guarded = guardedToolKeys(readSources(root), toolKeys);
6749
7636
  heading("Protection check");
6750
7637
  for (const key of toolKeys) {
6751
7638
  const file = guarded.get(key);
@@ -6808,22 +7695,31 @@ function printPlan(plan) {
6808
7695
  const wrapped = all.filter((entry) => entry.change.status === "wrapped");
6809
7696
  const already = all.filter((entry) => entry.change.status === "already");
6810
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))];
6811
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()")}`;
6812
7704
  out(
6813
- `${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:`
6814
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
+ };
6815
7711
  for (const { file, change } of wrapped) {
6816
7712
  out(
6817
- ` ${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] ?? ""}` : "")
6818
7714
  );
6819
7715
  }
6820
7716
  }
6821
7717
  if (already.length > 0) {
6822
- out(`${symbol.pass} Already protected: ${already.map((entry) => entry.change.key).join(", ")}`);
7718
+ out(`${symbol.pass} Already protected: ${keys(already).join(", ")}`);
6823
7719
  }
6824
7720
  if (manual.length > 0) {
6825
7721
  out(
6826
- `${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)")}`
6827
7723
  );
6828
7724
  }
6829
7725
  if (plan.notFound.length > 0) {
@@ -6845,8 +7741,8 @@ function printPlan(plan) {
6845
7741
  }
6846
7742
 
6847
7743
  // src/commands/remove.ts
6848
- import { rmSync as rmSync3 } from "node:fs";
6849
- import path10 from "node:path";
7744
+ import { rmSync as rmSync4 } from "node:fs";
7745
+ import path11 from "node:path";
6850
7746
 
6851
7747
  // src/discover.ts
6852
7748
  var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
@@ -6960,7 +7856,7 @@ async function removeCommand(args, flags, known) {
6960
7856
  if (!file || !config) {
6961
7857
  throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
6962
7858
  }
6963
- const root = path10.dirname(file);
7859
+ const root = path11.dirname(file);
6964
7860
  const state = known ?? await agentState(config);
6965
7861
  heading("Remove Veefore from this project");
6966
7862
  details([
@@ -7011,7 +7907,7 @@ async function removeCommand(args, flags, known) {
7011
7907
  out(style.grey("The project stays connected."));
7012
7908
  return 0;
7013
7909
  }
7014
- rmSync3(file);
7910
+ rmSync4(file);
7015
7911
  const envRemoved = removeLocalEnvBlock(root);
7016
7912
  out(
7017
7913
  `${symbol.pass} Disconnected: removed veefore.config.json${envRemoved ? " and the Veefore block in .env.local" : ""}.`
@@ -7095,6 +7991,7 @@ async function integrateCommand(args, flags) {
7095
7991
  files: outcome.plan.files.map((file) => ({ file: file.file, changes: file.changes })),
7096
7992
  not_found: outcome.plan.notFound,
7097
7993
  manual: outcome.manual,
7994
+ unguarded: outcome.unguarded,
7098
7995
  checks: outcome.checks
7099
7996
  },
7100
7997
  null,
@@ -7494,10 +8391,10 @@ async function resolveAuthorizedTools(context, filter) {
7494
8391
 
7495
8392
  // src/panel.ts
7496
8393
  import { homedir as homedir2 } from "node:os";
7497
- import path11 from "node:path";
8394
+ import path12 from "node:path";
7498
8395
 
7499
8396
  // src/version.ts
7500
- var CLI_VERSION = "1.4.0";
8397
+ var CLI_VERSION = "1.5.0";
7501
8398
 
7502
8399
  // src/panel.ts
7503
8400
  function welcomeSource(flags = {}) {
@@ -7521,7 +8418,7 @@ function welcomeSource(flags = {}) {
7521
8418
  }
7522
8419
  function tilde(dir) {
7523
8420
  const home = homedir2();
7524
- 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;
7525
8422
  }
7526
8423
  function hostOf(url) {
7527
8424
  try {
@@ -7588,7 +8485,7 @@ function planTool(tool) {
7588
8485
  }
7589
8486
  function printScan(scan) {
7590
8487
  out(
7591
- `${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")
7592
8489
  );
7593
8490
  if (scan.tools.length === 0) {
7594
8491
  out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
@@ -7621,6 +8518,40 @@ function printScan(scan) {
7621
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)")}`
7622
8519
  );
7623
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
+ );
7624
8555
  }
7625
8556
  function printPlan2(plan, environment) {
7626
8557
  out();
@@ -8164,20 +9095,36 @@ async function homeCommand(args, flags) {
8164
9095
  return await homeLoop(args, flags);
8165
9096
  } finally {
8166
9097
  leaveApp();
9098
+ printFarewell();
8167
9099
  }
8168
9100
  }
9101
+ var farewellPrinted = false;
9102
+ function printFarewell() {
9103
+ if (!isInteractive() || inApp() || farewellPrinted) return;
9104
+ const project = readProjectConfig();
9105
+ if (!project) return;
9106
+ farewellPrinted = true;
9107
+ out();
9108
+ out(
9109
+ `${brand(style.bold("Veefore"))} ${style.grey("\xB7")} ${style.bold(project.agentName)} ${style.grey(`\xB7 ${project.environmentName} \xB7 ${project.controlPlaneUrl}`)}`
9110
+ );
9111
+ out(style.grey(" veefore dev watch decisions live"));
9112
+ out(style.grey(" veefore doctor check the security posture"));
9113
+ out(style.grey(" veefore open this again"));
9114
+ out();
9115
+ }
8169
9116
  async function homeLoop(args, flags) {
8170
9117
  const source = welcomeSource(flags);
8171
9118
  let first = true;
8172
9119
  for (; ; ) {
8173
9120
  if (readProjectConfig()) return initCommand(args, flags);
8174
9121
  const body = () => {
8175
- drawScreen(source, { reserve: 11 });
9122
+ drawScreen(source);
8176
9123
  out();
8177
9124
  out(style.bold("Let's get started."));
8178
9125
  out();
8179
9126
  };
8180
- await beginScreen(source, { animate: first, reserve: 11 });
9127
+ await beginScreen(source, { animate: first });
8181
9128
  first = false;
8182
9129
  out();
8183
9130
  out(style.bold("Let's get started."));
@@ -8209,7 +9156,11 @@ async function homeLoop(args, flags) {
8209
9156
  // Setup done: "Continue", and the next pass opens the project home
8210
9157
  // with the panel filled in. Otherwise "Back to menu", to read why and
8211
9158
  // choose again.
8212
- label: () => readProjectConfig() ? "Continue" : "Back to menu"
9159
+ label: () => readProjectConfig() ? "Continue" : "Back to menu",
9160
+ // Setup asks questions and draws its own screens as it goes, so it has
9161
+ // to reach the terminal as it runs. Collecting it would hold those
9162
+ // screens back and then print them all at once.
9163
+ live: true
8213
9164
  });
8214
9165
  continue;
8215
9166
  }
@@ -8234,12 +9185,8 @@ async function initWithWelcome(args, flags) {
8234
9185
  return initCommand(args, flags);
8235
9186
  }
8236
9187
  async function runOnScreen(source, flags, run2, options = {}) {
8237
- const header = () => {
8238
- out();
8239
- compactHeader(source());
8240
- };
9188
+ const header = () => drawScreen(source);
8241
9189
  if (options.live) {
8242
- clearScreen();
8243
9190
  header();
8244
9191
  let code2 = 0;
8245
9192
  try {
@@ -8566,7 +9513,7 @@ async function initCommand(args, flags) {
8566
9513
  }
8567
9514
  function previousApprovalMode(dir = process.cwd()) {
8568
9515
  try {
8569
- const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
9516
+ const text = readFileSync10(path13.join(dir, ".env.local"), "utf8");
8570
9517
  const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
8571
9518
  return value === "wait" || value === "throw" ? value : null;
8572
9519
  } catch {
@@ -8708,7 +9655,7 @@ async function selectEnvironment(api, me, workspaceId, options, flags) {
8708
9655
  }
8709
9656
  }
8710
9657
  async function stepScreen(source, decided, step, total) {
8711
- await beginScreen(source, { compact: true });
9658
+ await beginScreen(source);
8712
9659
  out();
8713
9660
  out(` ${style.bold("Set up this project")} ${style.grey(`step ${step} of ${total}`)}`);
8714
9661
  if (decided.length > 0) {
@@ -8943,13 +9890,14 @@ async function projectHome(existing, flags) {
8943
9890
  return await projectHomeLoop(existing, flags);
8944
9891
  } finally {
8945
9892
  leaveApp();
9893
+ printFarewell();
8946
9894
  }
8947
9895
  }
8948
9896
  async function projectHomeLoop(existing, flags) {
8949
9897
  const source = welcomeSource(flags);
8950
9898
  let last = 0;
8951
9899
  for (let round = 0; ; round += 1) {
8952
- await beginScreen(source, { animate: round === 0, reserve: 18 });
9900
+ await beginScreen(source, { animate: round === 0 });
8953
9901
  const state = await agentState(existing);
8954
9902
  printProjectSummary(existing, state);
8955
9903
  const choices = homeChoices(existing, state, flags);
@@ -8964,7 +9912,7 @@ async function projectHomeLoop(existing, flags) {
8964
9912
  // Repainted as soon as a sign-in, sign-out or project change is
8965
9913
  // noticed, so the panel and the status never sit stale.
8966
9914
  redraw: () => {
8967
- drawScreen(source, { reserve: 18 });
9915
+ drawScreen(source);
8968
9916
  printProjectSummary(existing, state);
8969
9917
  out();
8970
9918
  }
@@ -8976,8 +9924,7 @@ async function projectHomeLoop(existing, flags) {
8976
9924
  }
8977
9925
  if (picked.leaves) {
8978
9926
  return onOrdinaryScreen(async () => {
8979
- clearScreen();
8980
- compactHeader(source());
9927
+ drawScreen(source);
8981
9928
  try {
8982
9929
  return await picked.run();
8983
9930
  } catch (error) {