veefore 1.2.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/veefore.mjs +1847 -1537
  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(path12, query = {}) {
239
+ async get(path13, 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", `${path12}${suffix}`);
245
+ return this.send("GET", `${path13}${suffix}`);
246
246
  }
247
- async post(path12, body) {
248
- return this.send("POST", path12, body);
247
+ async post(path13, body) {
248
+ return this.send("POST", path13, body);
249
249
  }
250
- async patch(path12, body) {
251
- return this.send("PATCH", path12, body);
250
+ async patch(path13, body) {
251
+ return this.send("PATCH", path13, body);
252
252
  }
253
- async delete(path12) {
254
- return this.send("DELETE", path12);
253
+ async delete(path13) {
254
+ return this.send("DELETE", path13);
255
255
  }
256
256
  /** Raw request for endpoints that return a non-JSON body (exports). */
257
- async raw(method, path12, body) {
258
- return this.fetchWithTimeout(method, path12, body);
257
+ async raw(method, path13, body) {
258
+ return this.fetchWithTimeout(method, path13, body);
259
259
  }
260
- async send(method, path12, body) {
261
- const response = await this.fetchWithTimeout(method, path12, body);
260
+ async send(method, path13, body) {
261
+ const response = await this.fetchWithTimeout(method, path13, 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} ${path12}.`,
269
+ `The control plane returned an unexpected body for ${method} ${path13}.`,
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} ${path12} failed with ${response.status} ${response.statusText}.`,
276
+ parsed?.error?.message ?? `${method} ${path13} 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, path12, body) {
294
+ async fetchWithTimeout(method, path13, 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}${path12}`, {
303
+ return await fetch(`${this.controlPlaneUrl}${path13}`, {
304
304
  method,
305
305
  headers,
306
306
  body: body === void 0 ? void 0 : JSON.stringify(body),
@@ -568,6 +568,8 @@ function sleep(ms) {
568
568
  import { createInterface } from "node:readline/promises";
569
569
 
570
570
  // src/tui.ts
571
+ import { statSync } from "node:fs";
572
+ import path2 from "node:path";
571
573
  import { emitKeypressEvents } from "node:readline";
572
574
 
573
575
  // src/logo.ts
@@ -612,8 +614,8 @@ var BRAND_TOP = [150, 172, 236];
612
614
  var BRAND_BOTTOM = [84, 108, 186];
613
615
  var BRAND = [118, 140, 214];
614
616
  var brand = (text) => paint(BRAND, text);
615
- function gradient(row, rows) {
616
- const t = rows <= 1 ? 0 : row / (rows - 1);
617
+ function gradient(row, rows2) {
618
+ const t = rows2 <= 1 ? 0 : row / (rows2 - 1);
617
619
  const mix = (a, b) => Math.round(a + (b - a) * t);
618
620
  return [
619
621
  mix(BRAND_TOP[0], BRAND_BOTTOM[0]),
@@ -628,6 +630,9 @@ function visibleLength(text) {
628
630
  function columns() {
629
631
  return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
630
632
  }
633
+ function rows() {
634
+ return process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : 24;
635
+ }
631
636
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
632
637
  var STARS = [
633
638
  [0, -3, "*"],
@@ -640,8 +645,20 @@ var STARS = [
640
645
  ];
641
646
  async function showWelcome(context, options = {}) {
642
647
  if (!isInteractive()) return;
648
+ const lines = welcomeLines(context);
649
+ if ((options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0) {
650
+ for (const line of lines) {
651
+ out(line);
652
+ await sleep2(14);
653
+ }
654
+ return;
655
+ }
656
+ for (const line of lines) out(line);
657
+ }
658
+ function welcomeLines(context) {
643
659
  const width = Math.min(columns(), 100);
644
- const logoWidth = Math.max(...LOGO.map((line) => [...line].length));
660
+ const art = LOGO;
661
+ const logoWidth = Math.max(...art.map((line) => [...line].length));
645
662
  const indent = 5;
646
663
  const gap = 6;
647
664
  const panelWidth = Math.max(
@@ -649,43 +666,45 @@ async function showWelcome(context, options = {}) {
649
666
  ...context.facts.map(([label, value]) => label.length + 2 + value.length)
650
667
  );
651
668
  const sideBySide = width >= indent + logoWidth + gap + panelWidth + 2;
669
+ const panel = renderPanel(context.facts);
652
670
  const showLogo = width >= indent + logoWidth + 2;
653
671
  const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
654
- const animate = (options.animate ?? true) && process.env.VEEFORE_NO_ANIMATION === void 0;
655
- out();
656
- out(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
657
- out(` ${rule}`);
658
- out();
659
- const panel = renderPanel(context.facts);
672
+ const lines = [];
673
+ const add = (line = "") => void lines.push(line);
674
+ add();
675
+ add(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
676
+ add(` ${rule}`);
677
+ add();
660
678
  if (showLogo) {
661
- for (let row = 0; row < LOGO.length; row += 1) {
662
- const line = LOGO[row] ?? "";
663
- const left = starsFor(row, indent, (column) => column < 0, -indent);
664
- const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth);
665
- const art = paint(gradient(row, LOGO.length), line.padEnd(logoWidth));
666
- let text = `${left}${art}`;
679
+ for (let row = 0; row < art.length; row += 1) {
680
+ const line = art[row] ?? "";
681
+ const left = starsFor(row, indent, (column) => column < 0, -indent, art.length);
682
+ const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth, art.length);
683
+ const painted = paint(gradient(row, art.length), line.padEnd(logoWidth));
684
+ let text = `${left}${painted}`;
667
685
  if (sideBySide) {
668
- const top = Math.max(0, Math.floor((LOGO.length - panel.length) / 2));
686
+ const top = Math.max(0, Math.floor((art.length - panel.length) / 2));
669
687
  const side = panel[row - top];
670
688
  text += side === void 0 ? right : `${" ".repeat(gap)}${side}`;
671
689
  } else {
672
690
  text += right;
673
691
  }
674
- out(text.replace(/\s+$/, ""));
675
- if (animate) await sleep2(14);
692
+ add(text.replace(/\s+$/, ""));
676
693
  }
677
694
  }
678
695
  if (!sideBySide) {
679
- out();
680
- for (const line of panel) out(fit(` ${line}`, width - 1));
696
+ add();
697
+ for (const line of panel) add(fit(` ${line}`, width - 1));
681
698
  }
682
- out();
683
- out(` ${rule}`);
699
+ add();
700
+ add(` ${rule}`);
701
+ return lines;
684
702
  }
685
- function starsFor(row, width, inBand, offset) {
703
+ function starsFor(row, width, inBand, offset, artRows) {
686
704
  const cells = Array.from({ length: Math.max(width, 6) }, () => " ");
687
705
  for (const [starRow, column, glyph] of STARS) {
688
- if (starRow !== row || !inBand(column)) continue;
706
+ const scaled = Math.round(starRow * (artRows - 1) / (LOGO.length - 1));
707
+ if (scaled !== row || !inBand(column)) continue;
689
708
  const index = column - offset;
690
709
  if (index >= 0 && index < cells.length) cells[index] = style.grey(glyph);
691
710
  }
@@ -706,27 +725,139 @@ function renderPanel(facts) {
706
725
  }
707
726
  function clearScreen() {
708
727
  if (!isInteractive()) return;
709
- process.stdout.write("\x1B[H\x1B[2J");
710
- }
711
- function compactHeader(context) {
712
- const facts = Object.fromEntries(context.facts);
713
- const parts = [
714
- brand(style.bold("Veefore")),
715
- style.grey(`v${context.version}`),
716
- ...[facts.Folder, facts["Signed in"], facts.Veefore].filter((v) => Boolean(v))
717
- ];
718
- out();
719
- out(fit(` ${parts.join(style.grey(" \xB7 "))}`, columns() - 1));
720
- out(` ${style.grey("\xB7".repeat(Math.max(10, Math.min(columns() - 2, 72))))}`);
728
+ process.stdout.write("\x1B[H\x1B[J");
729
+ }
730
+ var appDepth = 0;
731
+ var restoreInstalled = false;
732
+ function enterApp() {
733
+ if (!isInteractive()) return;
734
+ appDepth += 1;
735
+ if (appDepth > 1) return;
736
+ if (!restoreInstalled) {
737
+ restoreInstalled = true;
738
+ process.on("exit", () => {
739
+ if (appDepth > 0) process.stdout.write("\x1B[?25h\x1B[?1049l");
740
+ });
741
+ }
742
+ process.stdout.write("\x1B[?1049h\x1B[?25l\x1B[H\x1B[J");
743
+ }
744
+ function leaveApp() {
745
+ if (!isInteractive() || appDepth === 0) return;
746
+ appDepth -= 1;
747
+ if (appDepth > 0) return;
748
+ process.stdout.write("\x1B[?25h\x1B[?1049l");
749
+ }
750
+ function inApp() {
751
+ return appDepth > 0;
752
+ }
753
+ async function onOrdinaryScreen(body) {
754
+ const depth = appDepth;
755
+ if (depth > 0) {
756
+ appDepth = 0;
757
+ process.stdout.write("\x1B[?25h\x1B[?1049l");
758
+ }
759
+ try {
760
+ return await body();
761
+ } finally {
762
+ if (depth > 0) {
763
+ appDepth = depth;
764
+ process.stdout.write("\x1B[?1049h\x1B[H\x1B[J");
765
+ }
766
+ }
721
767
  }
722
768
  async function beginScreen(source, options = {}) {
723
769
  if (!isInteractive()) return;
724
770
  clearScreen();
725
- if (options.compact) compactHeader(source());
726
- else await showWelcome(source(), { animate: options.animate ?? false });
771
+ await showWelcome(source(), { animate: options.animate ?? false });
772
+ }
773
+ function drawScreen(source) {
774
+ if (!isInteractive()) return;
775
+ clearScreen();
776
+ for (const line of welcomeLines(source())) out(line);
777
+ }
778
+ async function showOutput(lines, options) {
779
+ if (!isInteractive()) {
780
+ for (const line of lines) process.stdout.write(`${line}
781
+ `);
782
+ return;
783
+ }
784
+ const stdout = process.stdout;
785
+ const stdin = process.stdin;
786
+ const body = lines.length > 0 ? lines : [style.grey(" (no output)")];
787
+ let headerRows = 0;
788
+ const measure = () => {
789
+ let count = 0;
790
+ const original = stdout.write.bind(stdout);
791
+ stdout.write = ((chunk) => {
792
+ count += String(chunk).split("\n").length - 1;
793
+ return true;
794
+ });
795
+ try {
796
+ options.drawHeader();
797
+ } finally {
798
+ stdout.write = original;
799
+ }
800
+ headerRows = count;
801
+ };
802
+ measure();
803
+ let top = 0;
804
+ const viewport = () => Math.max(3, rows() - headerRows - 2);
805
+ const draw = () => {
806
+ const height = viewport();
807
+ const maxTop = Math.max(0, body.length - height);
808
+ top = Math.min(Math.max(0, top), maxTop);
809
+ clearScreen();
810
+ options.drawHeader();
811
+ for (const line of body.slice(top, top + height)) {
812
+ stdout.write(`${fit(line, columns() - 1)}
813
+ `);
814
+ }
815
+ const hidden = body.length > height;
816
+ const position = hidden ? `${Math.min(top + height, body.length)}/${body.length}` : `${body.length} line${body.length === 1 ? "" : "s"}`;
817
+ const keys = hidden ? "\u2191\u2193 scroll \xB7 " : "";
818
+ stdout.write(
819
+ `${brand("\u2190")} ${style.bold(options.label ?? "Back")} ${style.grey(`\xB7 ${keys}enter or esc \xB7 ${position}`)}`
820
+ );
821
+ };
822
+ await new Promise((resolve) => {
823
+ const wasRaw = stdin.isRaw;
824
+ const done = () => {
825
+ stdin.off("keypress", onKey);
826
+ stdin.setRawMode(wasRaw);
827
+ stdin.pause();
828
+ process.stdout.off("resize", onResize);
829
+ stdout.write("\r\x1B[K");
830
+ resolve();
831
+ };
832
+ const onResize = () => {
833
+ measure();
834
+ draw();
835
+ };
836
+ const onKey = (input, key = {}) => {
837
+ const height = viewport();
838
+ if (key.name === "return" || key.name === "enter" || key.name === "escape" || key.name === "left" || input === "b" || key.ctrl && key.name === "c") {
839
+ return done();
840
+ }
841
+ if (key.name === "down" || input === "j") top += 1;
842
+ else if (key.name === "up" || input === "k") top -= 1;
843
+ else if (key.name === "pagedown" || input === " ") top += height;
844
+ else if (key.name === "pageup") top -= height;
845
+ else if (input === "G") top = body.length;
846
+ else if (input === "g") top = 0;
847
+ else return;
848
+ draw();
849
+ };
850
+ emitKeypressEvents(stdin);
851
+ stdin.setRawMode(true);
852
+ stdin.resume();
853
+ stdin.on("keypress", onKey);
854
+ process.stdout.on("resize", onResize);
855
+ draw();
856
+ });
727
857
  }
728
858
  async function pressBack(label = "Back") {
729
859
  if (!isInteractive()) return;
860
+ flushCapture();
730
861
  const stdin = process.stdin;
731
862
  const stdout = process.stdout;
732
863
  out();
@@ -751,6 +882,29 @@ async function pressBack(label = "Back") {
751
882
  stdin.on("keypress", onKey);
752
883
  });
753
884
  }
885
+ function watchFacts(onChange) {
886
+ const files = [
887
+ CREDENTIALS_FILE,
888
+ findProjectConfig() ?? path2.join(process.cwd(), PROJECT_CONFIG_FILE)
889
+ ];
890
+ const signature = () => files.map((file) => {
891
+ try {
892
+ const stats = statSync(file);
893
+ return `${stats.mtimeMs}:${stats.size}`;
894
+ } catch {
895
+ return "absent";
896
+ }
897
+ }).join("|");
898
+ let last = signature();
899
+ const timer = setInterval(() => {
900
+ const now = signature();
901
+ if (now === last) return;
902
+ last = now;
903
+ onChange();
904
+ }, 500);
905
+ timer.unref();
906
+ return () => clearInterval(timer);
907
+ }
754
908
  var BACK = /* @__PURE__ */ Symbol("veefore.back");
755
909
  var MenuCancelled = class extends CliError {
756
910
  constructor() {
@@ -765,6 +919,7 @@ async function select(question, real, settings = {}) {
765
919
  const choosable = enabled.filter((index) => options[index]?.value !== BACK);
766
920
  const fallback = choosable.includes(settings.initial ?? -1) ? settings.initial : choosable[0] ?? enabled[0];
767
921
  if (!isInteractive()) return options[fallback].value;
922
+ flushCapture();
768
923
  let cursor = fallback;
769
924
  let rendered = 0;
770
925
  const stdin = process.stdin;
@@ -794,6 +949,13 @@ async function select(question, real, settings = {}) {
794
949
  `);
795
950
  rendered = lines.length;
796
951
  };
952
+ const repaint = () => {
953
+ if (!settings.redraw) return;
954
+ clearScreen();
955
+ settings.redraw();
956
+ rendered = 0;
957
+ draw();
958
+ };
797
959
  const move = (direction) => {
798
960
  const position = enabled.indexOf(cursor);
799
961
  const next = (position + direction + enabled.length) % enabled.length;
@@ -802,12 +964,19 @@ async function select(question, real, settings = {}) {
802
964
  return new Promise((resolve, reject) => {
803
965
  const wasRaw = stdin.isRaw;
804
966
  const restoreCursor = () => {
805
- stdout.write("\x1B[?25h");
967
+ if (!inApp()) stdout.write("\x1B[?25h");
968
+ };
969
+ const stopWatching = settings.redraw ? watchFacts(repaint) : void 0;
970
+ const onResize = () => {
971
+ if (settings.redraw) repaint();
972
+ else draw();
806
973
  };
807
974
  const finish = (result) => {
808
975
  stdin.off("keypress", onKey);
809
976
  stdin.setRawMode(wasRaw);
810
977
  stdin.pause();
978
+ stopWatching?.();
979
+ process.stdout.off("resize", onResize);
811
980
  process.off("exit", restoreCursor);
812
981
  if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
813
982
  restoreCursor();
@@ -857,6 +1026,7 @@ async function select(question, real, settings = {}) {
857
1026
  stdin.resume();
858
1027
  stdout.write("\x1B[?25l");
859
1028
  stdin.on("keypress", onKey);
1029
+ process.stdout.on("resize", onResize);
860
1030
  draw();
861
1031
  });
862
1032
  }
@@ -912,11 +1082,38 @@ var symbol = {
912
1082
  info: style.cyan("\u203A"),
913
1083
  bullet: style.grey("\u2022")
914
1084
  };
1085
+ var captured = null;
1086
+ function beginCapture() {
1087
+ captured = [];
1088
+ }
1089
+ function endCapture() {
1090
+ const lines = captured ?? [];
1091
+ captured = null;
1092
+ return lines;
1093
+ }
1094
+ function flushCapture() {
1095
+ if (captured === null) return;
1096
+ const lines = captured;
1097
+ captured = null;
1098
+ for (const line of lines) process.stdout.write(`${line}
1099
+ `);
1100
+ }
1101
+ function isCapturing() {
1102
+ return captured !== null;
1103
+ }
915
1104
  function out(line = "") {
1105
+ if (captured !== null) {
1106
+ captured.push(line);
1107
+ return;
1108
+ }
916
1109
  process.stdout.write(`${line}
917
1110
  `);
918
1111
  }
919
1112
  function err(line = "") {
1113
+ if (captured !== null) {
1114
+ captured.push(line);
1115
+ return;
1116
+ }
920
1117
  process.stderr.write(`${line}
921
1118
  `);
922
1119
  }
@@ -925,15 +1122,15 @@ function heading(text) {
925
1122
  out(style.bold(text));
926
1123
  out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
927
1124
  }
928
- function table(columns2, rows) {
929
- if (rows.length === 0) {
1125
+ function table(columns2, rows2) {
1126
+ if (rows2.length === 0) {
930
1127
  out(style.grey(" (none)"));
931
1128
  return;
932
1129
  }
933
1130
  const widths = columns2.map((column) => {
934
1131
  const longest = Math.max(
935
1132
  column.header.length,
936
- ...rows.map((row) => (row[column.key] ?? "").length)
1133
+ ...rows2.map((row) => (row[column.key] ?? "").length)
937
1134
  );
938
1135
  return Math.min(column.width ?? longest, longest);
939
1136
  });
@@ -949,7 +1146,7 @@ function table(columns2, rows) {
949
1146
  columns2.map((column) => column.header),
950
1147
  style.grey
951
1148
  );
952
- for (const row of rows) {
1149
+ for (const row of rows2) {
953
1150
  renderRow(
954
1151
  columns2.map((column) => row[column.key] ?? ""),
955
1152
  (text) => text
@@ -966,6 +1163,7 @@ function keyValue(label, value) {
966
1163
  out(` ${style.grey(`${label}:`)} ${value}`);
967
1164
  }
968
1165
  async function ask(question, fallback = "") {
1166
+ flushCapture();
969
1167
  if (!process.stdin.isTTY) return fallback;
970
1168
  const rl = createInterface({ input: process.stdin, output: process.stdout });
971
1169
  try {
@@ -976,6 +1174,7 @@ async function ask(question, fallback = "") {
976
1174
  }
977
1175
  }
978
1176
  async function askSecret(question) {
1177
+ flushCapture();
979
1178
  if (!process.stdin.isTTY) {
980
1179
  throw new Error(
981
1180
  "A secret is required but the terminal is not interactive. Provide it through the documented environment variable instead."
@@ -1018,6 +1217,7 @@ async function askSecret(question) {
1018
1217
  });
1019
1218
  }
1020
1219
  async function confirm(question, options = {}) {
1220
+ flushCapture();
1021
1221
  if (options.yes) {
1022
1222
  out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
1023
1223
  return true;
@@ -1040,6 +1240,12 @@ async function confirm(question, options = {}) {
1040
1240
  return ["y", "yes"].includes(answer.toLowerCase());
1041
1241
  }
1042
1242
  function spinner(label) {
1243
+ if (isCapturing()) {
1244
+ return {
1245
+ succeed: (text) => out(`${symbol.pass} ${text ?? label}`),
1246
+ fail: (text) => out(`${symbol.fail} ${text ?? label}`)
1247
+ };
1248
+ }
1043
1249
  if (!process.stdout.isTTY) {
1044
1250
  out(`${symbol.info} ${label}`);
1045
1251
  return {
@@ -1163,6 +1369,7 @@ async function resolveContext(flags) {
1163
1369
  };
1164
1370
  }
1165
1371
  async function login(controlPlaneUrl, options = {}) {
1372
+ flushCapture();
1166
1373
  await assertControlPlane(controlPlaneUrl);
1167
1374
  const ciCredentials = Boolean(process.env.VEEFORE_EMAIL && process.env.VEEFORE_PASSWORD);
1168
1375
  if (options.method === "password" || options.method === void 0 && ciCredentials) {
@@ -1739,11 +1946,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
1739
1946
  out(` ${style.red("This action is irreversible for this identity.")}`);
1740
1947
  out();
1741
1948
  }
1742
- async function lifecycle(args, flags, path12, resultingStatus) {
1949
+ async function lifecycle(args, flags, path13, resultingStatus) {
1743
1950
  const context = await resolveContext(flags);
1744
1951
  const agentId = positional(args) ?? context.config.agentId;
1745
1952
  const agent = await context.api.get(`/v1/agents/${agentId}`);
1746
- const reasonRequired = path12 === "suspend";
1953
+ const reasonRequired = path13 === "suspend";
1747
1954
  const reason = valueFor(args, "--reason");
1748
1955
  if (reasonRequired && (!reason || reason.trim().length < 10)) {
1749
1956
  throw new CliError(
@@ -1753,24 +1960,24 @@ async function lifecycle(args, flags, path12, resultingStatus) {
1753
1960
  }
1754
1961
  out();
1755
1962
  out(
1756
- `${symbol.info} ${path12} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1963
+ `${symbol.info} ${path13} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1757
1964
  );
1758
- if (path12 !== "resume") {
1965
+ if (path13 !== "resume") {
1759
1966
  const confirmed = await confirm("Continue?", { yes: flags.yes });
1760
1967
  if (!confirmed) {
1761
1968
  out(style.grey("Cancelled."));
1762
1969
  return 1;
1763
1970
  }
1764
1971
  }
1765
- const actionSpinner = spinner(`Applying ${path12}`);
1972
+ const actionSpinner = spinner(`Applying ${path13}`);
1766
1973
  try {
1767
1974
  await withStepUp(
1768
1975
  context.api,
1769
- () => context.api.post(`/v1/agents/${agentId}/${path12}`, reason ? { reason } : {})
1976
+ () => context.api.post(`/v1/agents/${agentId}/${path13}`, reason ? { reason } : {})
1770
1977
  );
1771
1978
  actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
1772
1979
  } catch (error) {
1773
- actionSpinner.fail(`Could not ${path12} the agent`);
1980
+ actionSpinner.fail(`Could not ${path13} the agent`);
1774
1981
  throw error;
1775
1982
  }
1776
1983
  out();
@@ -2074,13 +2281,13 @@ async function exportEvents(args, flags) {
2074
2281
  }
2075
2282
 
2076
2283
  // src/commands/doctor.ts
2077
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "node:fs";
2078
- import path3 from "node:path";
2284
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
2285
+ import path4 from "node:path";
2079
2286
 
2080
2287
  // src/project-checks.ts
2081
2288
  import { spawnSync } from "node:child_process";
2082
2289
  import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
2083
- import path2 from "node:path";
2290
+ import path3 from "node:path";
2084
2291
  var MIN_APPROVAL_WAIT_SDK = "1.1.0";
2085
2292
  function versionLessThan(a, b) {
2086
2293
  if (!a) return true;
@@ -2097,19 +2304,19 @@ function versionLessThan(a, b) {
2097
2304
  function installedSdks(root, options = {}) {
2098
2305
  const found = [];
2099
2306
  const seen = /* @__PURE__ */ new Set();
2100
- const top = path2.resolve(root);
2307
+ const top = path3.resolve(root);
2101
2308
  const candidates = /* @__PURE__ */ new Set([top]);
2102
2309
  for (const dir of options.dirs ?? []) {
2103
- let current = path2.resolve(top, dir);
2310
+ let current = path3.resolve(top, dir);
2104
2311
  while (current.startsWith(top)) {
2105
2312
  candidates.add(current);
2106
- const parent = path2.dirname(current);
2313
+ const parent = path3.dirname(current);
2107
2314
  if (parent === current) break;
2108
2315
  current = parent;
2109
2316
  }
2110
2317
  }
2111
2318
  for (const dir of candidates) {
2112
- const manifest = path2.join(dir, "node_modules", "@veefore", "sdk", "package.json");
2319
+ const manifest = path3.join(dir, "node_modules", "@veefore", "sdk", "package.json");
2113
2320
  if (!existsSync2(manifest)) continue;
2114
2321
  let real = manifest;
2115
2322
  try {
@@ -2126,7 +2333,7 @@ function installedSdks(root, options = {}) {
2126
2333
  }
2127
2334
  found.push({
2128
2335
  language: "js",
2129
- location: path2.relative(top, path2.dirname(manifest)) || ".",
2336
+ location: path3.relative(top, path3.dirname(manifest)) || ".",
2130
2337
  version,
2131
2338
  waitsForApproval: !versionLessThan(version, MIN_APPROVAL_WAIT_SDK)
2132
2339
  });
@@ -2158,7 +2365,7 @@ function upgradeCommand(root, language) {
2158
2365
  }
2159
2366
  function configuredApprovalMode(root) {
2160
2367
  for (const name of [".env.local", ".env"]) {
2161
- const file = path2.join(root, name);
2368
+ const file = path3.join(root, name);
2162
2369
  if (!existsSync2(file)) continue;
2163
2370
  const match = /^\s*(?:export\s+)?VEEFORE_APPROVAL_MODE\s*=\s*["']?(\w+)["']?/m.exec(
2164
2371
  readFileSync2(file, "utf8")
@@ -2168,7 +2375,7 @@ function configuredApprovalMode(root) {
2168
2375
  return null;
2169
2376
  }
2170
2377
  function sdkInstalled(root, language) {
2171
- if (language === "js") return existsSync2(path2.join(root, "node_modules", "@veefore", "sdk"));
2378
+ if (language === "js") return existsSync2(path3.join(root, "node_modules", "@veefore", "sdk"));
2172
2379
  const python = projectPython(root);
2173
2380
  if (!python) return false;
2174
2381
  return spawnSync(python, ["-c", "import veefore"], { cwd: root, stdio: "ignore", timeout: 2e4 }).status === 0;
@@ -2177,18 +2384,18 @@ function typecheck(root, files) {
2177
2384
  const name = "TypeScript typecheck";
2178
2385
  const projects = files ? [
2179
2386
  ...new Set(
2180
- files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root, path2.dirname(path2.join(root, file)), "tsconfig.json")).filter((dir) => dir !== null)
2387
+ files.filter((file) => /\.(m|c)?tsx?$/.test(file)).map((file) => nearestUp(root, path3.dirname(path3.join(root, file)), "tsconfig.json")).filter((dir) => dir !== null)
2181
2388
  )
2182
- ] : existsSync2(path2.join(root, "tsconfig.json")) ? [root] : [];
2389
+ ] : existsSync2(path3.join(root, "tsconfig.json")) ? [root] : [];
2183
2390
  if (projects.length === 0) return { name, ran: false, ok: true, detail: "no tsconfig.json" };
2184
2391
  const binary = process.platform === "win32" ? "tsc.cmd" : "tsc";
2185
2392
  for (const project of projects) {
2186
- const bin = nearestUp(root, project, path2.join("node_modules", ".bin", binary));
2393
+ const bin = nearestUp(root, project, path3.join("node_modules", ".bin", binary));
2187
2394
  if (!bin) {
2188
2395
  return { name, ran: false, ok: true, detail: "TypeScript is not installed in this project" };
2189
2396
  }
2190
2397
  const result = spawnSync(
2191
- path2.join(bin, "node_modules", ".bin", binary),
2398
+ path3.join(bin, "node_modules", ".bin", binary),
2192
2399
  ["--noEmit", "-p", "."],
2193
2400
  {
2194
2401
  cwd: project,
@@ -2198,7 +2405,7 @@ function typecheck(root, files) {
2198
2405
  );
2199
2406
  if (result.status !== 0) {
2200
2407
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2201
- const where = path2.relative(root, project) || ".";
2408
+ const where = path3.relative(root, project) || ".";
2202
2409
  return {
2203
2410
  name,
2204
2411
  ran: true,
@@ -2211,12 +2418,12 @@ ${output.split("\n").slice(0, 12).join("\n")}`
2211
2418
  return { name, ran: true, ok: true, detail: "" };
2212
2419
  }
2213
2420
  function nearestUp(root, start, entry) {
2214
- const top = path2.resolve(root);
2215
- let dir = path2.resolve(start);
2421
+ const top = path3.resolve(root);
2422
+ let dir = path3.resolve(start);
2216
2423
  for (; ; ) {
2217
- if (existsSync2(path2.join(dir, entry))) return dir;
2424
+ if (existsSync2(path3.join(dir, entry))) return dir;
2218
2425
  if (dir === top) return null;
2219
- const parent = path2.dirname(dir);
2426
+ const parent = path3.dirname(dir);
2220
2427
  if (parent === dir || !parent.startsWith(top)) return null;
2221
2428
  dir = parent;
2222
2429
  }
@@ -2270,20 +2477,20 @@ function pythonSyntax(root, files) {
2270
2477
  }
2271
2478
  function projectPython(root) {
2272
2479
  for (const candidate of [".venv/bin/python", "venv/bin/python", ".venv/Scripts/python.exe"]) {
2273
- if (existsSync2(path2.join(root, candidate))) return path2.join(root, candidate);
2480
+ if (existsSync2(path3.join(root, candidate))) return path3.join(root, candidate);
2274
2481
  }
2275
2482
  const probe = spawnSync("python3", ["--version"], { stdio: "ignore" });
2276
2483
  return probe.status === 0 ? "python3" : null;
2277
2484
  }
2278
2485
  function installCommand(root, language) {
2279
2486
  if (language === "py") {
2280
- if (existsSync2(path2.join(root, "uv.lock"))) return "uv add veefore-py";
2281
- if (existsSync2(path2.join(root, "poetry.lock"))) return "poetry add veefore-py";
2487
+ if (existsSync2(path3.join(root, "uv.lock"))) return "uv add veefore-py";
2488
+ if (existsSync2(path3.join(root, "poetry.lock"))) return "poetry add veefore-py";
2282
2489
  return "pip install veefore-py";
2283
2490
  }
2284
- if (existsSync2(path2.join(root, "pnpm-lock.yaml"))) return "pnpm add @veefore/sdk";
2285
- if (existsSync2(path2.join(root, "yarn.lock"))) return "yarn add @veefore/sdk";
2286
- if (existsSync2(path2.join(root, "bun.lockb")) || existsSync2(path2.join(root, "bun.lock"))) {
2491
+ if (existsSync2(path3.join(root, "pnpm-lock.yaml"))) return "pnpm add @veefore/sdk";
2492
+ if (existsSync2(path3.join(root, "yarn.lock"))) return "yarn add @veefore/sdk";
2493
+ if (existsSync2(path3.join(root, "bun.lockb")) || existsSync2(path3.join(root, "bun.lock"))) {
2287
2494
  return "bun add @veefore/sdk";
2288
2495
  }
2289
2496
  return "npm install @veefore/sdk";
@@ -2403,8 +2610,8 @@ async function checkTransportSecurity(config, add) {
2403
2610
  }
2404
2611
  function checkLocalSecrets(config, add) {
2405
2612
  const configFile = findProjectConfig();
2406
- const projectDir = configFile ? path3.dirname(configFile) : process.cwd();
2407
- const envFile = path3.join(projectDir, ".env.local");
2613
+ const projectDir = configFile ? path4.dirname(configFile) : process.cwd();
2614
+ const envFile = path4.join(projectDir, ".env.local");
2408
2615
  if (!existsSync3(envFile)) {
2409
2616
  add({
2410
2617
  check: "Local agent credentials",
@@ -2414,7 +2621,7 @@ function checkLocalSecrets(config, add) {
2414
2621
  });
2415
2622
  return;
2416
2623
  }
2417
- const mode = statSync(envFile).mode & 511;
2624
+ const mode = statSync2(envFile).mode & 511;
2418
2625
  if ((mode & 63) !== 0) {
2419
2626
  add({
2420
2627
  check: "Local agent credentials",
@@ -2429,7 +2636,7 @@ function checkLocalSecrets(config, add) {
2429
2636
  message: `.env.local is mode ${mode.toString(8)} (owner only).`
2430
2637
  });
2431
2638
  }
2432
- const gitignore = path3.join(projectDir, ".gitignore");
2639
+ const gitignore = path4.join(projectDir, ".gitignore");
2433
2640
  const ignored = existsSync3(gitignore) && readFileSync3(gitignore, "utf8").split(/\r?\n/).some((line) => line.trim() === ".env.local" || line.trim() === "*.local");
2434
2641
  add({
2435
2642
  check: "Secrets excluded from version control",
@@ -2757,8 +2964,8 @@ async function checkCredentialRotation(api, add) {
2757
2964
  }
2758
2965
  async function checkDependencies(add, dir) {
2759
2966
  const configFile = findProjectConfig();
2760
- const projectDir = dir ?? (configFile ? path3.dirname(configFile) : process.cwd());
2761
- const manifest = path3.join(projectDir, "package.json");
2967
+ const projectDir = dir ?? (configFile ? path4.dirname(configFile) : process.cwd());
2968
+ const manifest = path4.join(projectDir, "package.json");
2762
2969
  if (!existsSync3(manifest)) {
2763
2970
  add({
2764
2971
  check: "SDK dependency",
@@ -2800,9 +3007,9 @@ async function checkDependencies(add, dir) {
2800
3007
  }
2801
3008
  function checkApprovalWaiting(add, projectDir) {
2802
3009
  const configFile = findProjectConfig();
2803
- const dir = projectDir ?? (configFile ? path3.dirname(configFile) : process.cwd());
3010
+ const dir = projectDir ?? (configFile ? path4.dirname(configFile) : process.cwd());
2804
3011
  const python = ["pyproject.toml", "requirements.txt", "setup.py"].some(
2805
- (name) => existsSync3(path3.join(dir, name))
3012
+ (name) => existsSync3(path4.join(dir, name))
2806
3013
  );
2807
3014
  const sdks = installedSdks(dir, { python });
2808
3015
  const old = sdks.filter((sdk) => !sdk.waitsForApproval);
@@ -3094,11 +3301,11 @@ function isLoosening(from, to) {
3094
3301
 
3095
3302
  // src/commands/init.ts
3096
3303
  import { readFileSync as readFileSync9 } from "node:fs";
3097
- import path11 from "node:path";
3304
+ import path12 from "node:path";
3098
3305
 
3099
3306
  // src/integrate.ts
3100
3307
  import { readFileSync as readFileSync8 } from "node:fs";
3101
- import path8 from "node:path";
3308
+ import path9 from "node:path";
3102
3309
 
3103
3310
  // ../types/src/permissions.ts
3104
3311
  var PERMISSIONS = [
@@ -3435,6 +3642,11 @@ var EVENT_TYPES = [
3435
3642
  "user.profile_updated",
3436
3643
  "onboarding.completed",
3437
3644
  "session.revoked",
3645
+ // Sign-in is served from its own origin, so a session cannot be carried
3646
+ // across to the console by cookie; it is re-issued there from a one-time
3647
+ // code. Recorded at both ends, so the trail shows one session ending and
3648
+ // its replacement beginning rather than an unexplained gap.
3649
+ "session.handed_over",
3438
3650
  "cli.authorization_approved",
3439
3651
  "cli.authorization_denied",
3440
3652
  "cli.session_issued",
@@ -4368,21 +4580,21 @@ import {
4368
4580
  readdirSync,
4369
4581
  renameSync as renameSync2,
4370
4582
  rmSync as rmSync2,
4371
- statSync as statSync2,
4583
+ statSync as statSync3,
4372
4584
  writeFileSync as writeFileSync3
4373
4585
  } from "node:fs";
4374
- import path4 from "node:path";
4375
- var BACKUP_DIR = path4.join(".veefore", "backups");
4586
+ import path5 from "node:path";
4587
+ var BACKUP_DIR = path5.join(".veefore", "backups");
4376
4588
  function sha256(text) {
4377
4589
  return createHash2("sha256").update(text).digest("hex");
4378
4590
  }
4379
4591
  function createBackup(root, changes) {
4380
4592
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
4381
4593
  const id = createdAt.replace(/[:.]/g, "-");
4382
- const dir = path4.join(root, BACKUP_DIR, id);
4594
+ const dir = path5.join(root, BACKUP_DIR, id);
4383
4595
  for (const change of changes) {
4384
- const target = path4.join(dir, "files", change.file);
4385
- mkdirSync2(path4.dirname(target), { recursive: true });
4596
+ const target = path5.join(dir, "files", change.file);
4597
+ mkdirSync2(path5.dirname(target), { recursive: true });
4386
4598
  writeFileSync3(target, change.before, { mode: 384 });
4387
4599
  }
4388
4600
  const files = changes.map((change) => ({
@@ -4391,7 +4603,7 @@ function createBackup(root, changes) {
4391
4603
  afterSha256: sha256(change.after)
4392
4604
  }));
4393
4605
  writeFileSync3(
4394
- path4.join(dir, "manifest.json"),
4606
+ path5.join(dir, "manifest.json"),
4395
4607
  `${JSON.stringify({ version: 1, createdAt, files }, null, 2)}
4396
4608
  `,
4397
4609
  { mode: 384 }
@@ -4399,28 +4611,28 @@ function createBackup(root, changes) {
4399
4611
  return { id, dir, createdAt, files };
4400
4612
  }
4401
4613
  function writeFilePreservingMode(file, content) {
4402
- const mode = existsSync4(file) ? statSync2(file).mode & 511 : 420;
4614
+ const mode = existsSync4(file) ? statSync3(file).mode & 511 : 420;
4403
4615
  const temp = `${file}.veefore-tmp-${process.pid}`;
4404
4616
  writeFileSync3(temp, content, { mode });
4405
4617
  renameSync2(temp, file);
4406
4618
  }
4407
4619
  function latestBackup(root) {
4408
- const base2 = path4.join(root, BACKUP_DIR);
4620
+ const base2 = path5.join(root, BACKUP_DIR);
4409
4621
  if (!existsSync4(base2)) return null;
4410
- const ids = readdirSync(base2).filter((entry) => existsSync4(path4.join(base2, entry, "manifest.json"))).sort();
4622
+ const ids = readdirSync(base2).filter((entry) => existsSync4(path5.join(base2, entry, "manifest.json"))).sort();
4411
4623
  const id = ids[ids.length - 1];
4412
4624
  if (!id) return null;
4413
- const dir = path4.join(base2, id);
4414
- const manifest = JSON.parse(readFileSync4(path4.join(dir, "manifest.json"), "utf8"));
4625
+ const dir = path5.join(base2, id);
4626
+ const manifest = JSON.parse(readFileSync4(path5.join(dir, "manifest.json"), "utf8"));
4415
4627
  return { id, dir, createdAt: manifest.createdAt, files: manifest.files };
4416
4628
  }
4417
4629
  function restoreBackup(root, backup, options = {}) {
4418
4630
  const restored = [];
4419
4631
  const conflicts = [];
4420
4632
  for (const entry of backup.files) {
4421
- const target = path4.join(root, entry.file);
4633
+ const target = path5.join(root, entry.file);
4422
4634
  const current = existsSync4(target) ? readFileSync4(target, "utf8") : null;
4423
- const original = readFileSync4(path4.join(backup.dir, "files", entry.file), "utf8");
4635
+ const original = readFileSync4(path5.join(backup.dir, "files", entry.file), "utf8");
4424
4636
  if (current !== null && sha256(current) === entry.beforeSha256) {
4425
4637
  restored.push(entry.file);
4426
4638
  continue;
@@ -4440,15 +4652,15 @@ function restoreBackup(root, backup, options = {}) {
4440
4652
 
4441
4653
  // src/rewrite.ts
4442
4654
  import { readFileSync as readFileSync7 } from "node:fs";
4443
- import path7 from "node:path";
4655
+ import path8 from "node:path";
4444
4656
 
4445
4657
  // src/scan.ts
4446
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
4447
- import path6 from "node:path";
4658
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
4659
+ import path7 from "node:path";
4448
4660
 
4449
4661
  // src/framework.ts
4450
4662
  import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "node:fs";
4451
- import path5 from "node:path";
4663
+ import path6 from "node:path";
4452
4664
  var SIGNATURES = [
4453
4665
  {
4454
4666
  key: "langchain",
@@ -4530,8 +4742,8 @@ function detectFramework(dir = process.cwd()) {
4530
4742
  };
4531
4743
  }
4532
4744
  }
4533
- if (nodeDeps.size > 0 || existsSync5(path5.join(dir, "package.json"))) {
4534
- const typescript = existsSync5(path5.join(dir, "tsconfig.json")) || nodeDeps.has("typescript");
4745
+ if (nodeDeps.size > 0 || existsSync5(path6.join(dir, "package.json"))) {
4746
+ const typescript = existsSync5(path6.join(dir, "tsconfig.json")) || nodeDeps.has("typescript");
4535
4747
  return {
4536
4748
  key: "custom",
4537
4749
  name: "Custom agent",
@@ -4553,7 +4765,7 @@ function normalize(name) {
4553
4765
  return name.toLowerCase().replace(/_/g, "-");
4554
4766
  }
4555
4767
  function readPackageJsonDependencies(dir) {
4556
- const file = path5.join(dir, "package.json");
4768
+ const file = path6.join(dir, "package.json");
4557
4769
  if (!existsSync5(file)) return /* @__PURE__ */ new Set();
4558
4770
  try {
4559
4771
  const parsed = JSON.parse(readFileSync5(file, "utf8"));
@@ -4570,12 +4782,12 @@ function readPackageJsonDependencies(dir) {
4570
4782
  }
4571
4783
  function hasPythonProject(dir) {
4572
4784
  return ["pyproject.toml", "requirements.txt", "Pipfile", "setup.py"].some(
4573
- (file) => existsSync5(path5.join(dir, file))
4785
+ (file) => existsSync5(path6.join(dir, file))
4574
4786
  );
4575
4787
  }
4576
4788
  function readPythonDependencies(dir) {
4577
4789
  const found = /* @__PURE__ */ new Set();
4578
- const requirements = path5.join(dir, "requirements.txt");
4790
+ const requirements = path6.join(dir, "requirements.txt");
4579
4791
  if (existsSync5(requirements)) {
4580
4792
  for (const line of readFileSync5(requirements, "utf8").split(/\r?\n/)) {
4581
4793
  const trimmed = line.trim();
@@ -4584,7 +4796,7 @@ function readPythonDependencies(dir) {
4584
4796
  if (name) found.add(normalize(name));
4585
4797
  }
4586
4798
  }
4587
- const pyproject = path5.join(dir, "pyproject.toml");
4799
+ const pyproject = path6.join(dir, "pyproject.toml");
4588
4800
  if (existsSync5(pyproject)) {
4589
4801
  const text = readFileSync5(pyproject, "utf8");
4590
4802
  for (const match of text.matchAll(
@@ -4616,7 +4828,7 @@ function findEntryPoints(dir = process.cwd(), limit = 5) {
4616
4828
  "app.py",
4617
4829
  "src/main.py"
4618
4830
  ];
4619
- const found = candidates.filter((file) => existsSync5(path5.join(dir, file)));
4831
+ const found = candidates.filter((file) => existsSync5(path6.join(dir, file)));
4620
4832
  if (found.length > 0) return found.slice(0, limit);
4621
4833
  try {
4622
4834
  return readdirSync2(dir).filter((file) => /\.(ts|js|py)$/.test(file)).slice(0, limit);
@@ -4720,7 +4932,7 @@ function scanProject(root = process.cwd()) {
4720
4932
  for (const relative of files) {
4721
4933
  let text;
4722
4934
  try {
4723
- text = readFileSync6(path6.join(root, relative), "utf8");
4935
+ text = readFileSync6(path7.join(root, relative), "utf8");
4724
4936
  } catch {
4725
4937
  continue;
4726
4938
  }
@@ -5219,10 +5431,10 @@ function walk(root, dir, out2) {
5219
5431
  if (entry.startsWith(".") && entry !== ".") {
5220
5432
  continue;
5221
5433
  }
5222
- const full = path6.join(dir, entry);
5434
+ const full = path7.join(dir, entry);
5223
5435
  let stats;
5224
5436
  try {
5225
- stats = statSync3(full);
5437
+ stats = statSync4(full);
5226
5438
  } catch {
5227
5439
  continue;
5228
5440
  }
@@ -5232,14 +5444,14 @@ function walk(root, dir, out2) {
5232
5444
  continue;
5233
5445
  }
5234
5446
  if (!stats.isFile() || stats.size > MAX_FILE_BYTES) continue;
5235
- if (!SOURCE_EXTENSIONS.has(path6.extname(entry))) continue;
5447
+ if (!SOURCE_EXTENSIONS.has(path7.extname(entry))) continue;
5236
5448
  if (/\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$|\.d\.ts$/.test(entry)) continue;
5237
- out2.push(path6.relative(root, full));
5449
+ out2.push(path7.relative(root, full));
5238
5450
  }
5239
5451
  return false;
5240
5452
  }
5241
5453
  function readProjectName(root) {
5242
- const pkg = path6.join(root, "package.json");
5454
+ const pkg = path7.join(root, "package.json");
5243
5455
  if (existsSync6(pkg)) {
5244
5456
  try {
5245
5457
  const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
@@ -5247,12 +5459,12 @@ function readProjectName(root) {
5247
5459
  } catch {
5248
5460
  }
5249
5461
  }
5250
- const pyproject = path6.join(root, "pyproject.toml");
5462
+ const pyproject = path7.join(root, "pyproject.toml");
5251
5463
  if (existsSync6(pyproject)) {
5252
5464
  const name = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(readFileSync6(pyproject, "utf8"))?.[1];
5253
5465
  if (name) return name.trim();
5254
5466
  }
5255
- return path6.basename(root) || null;
5467
+ return path7.basename(root) || null;
5256
5468
  }
5257
5469
  function stringField(body, field) {
5258
5470
  const match = new RegExp(`\\b${field}\\s*:\\s*(['"\`])([^]{1,300}?)\\1`).exec(body);
@@ -5307,7 +5519,7 @@ function planRewrite(root, files, toolKeys) {
5307
5519
  for (const file of files) {
5308
5520
  let before;
5309
5521
  try {
5310
- before = readFileSync7(path7.join(root, file), "utf8");
5522
+ before = readFileSync7(path8.join(root, file), "utf8");
5311
5523
  } catch {
5312
5524
  continue;
5313
5525
  }
@@ -6297,13 +6509,13 @@ async function integrate(root, toolKeys, options) {
6297
6509
  toWrite.map((file) => file.file)
6298
6510
  ) : null;
6299
6511
  for (const file of toWrite) {
6300
- if (readFileSync8(path8.join(root, file.file), "utf8") !== file.before) {
6512
+ if (readFileSync8(path9.join(root, file.file), "utf8") !== file.before) {
6301
6513
  out(`${symbol.fail} ${file.file} changed while setup was running. Nothing was written.`);
6302
6514
  return { status: "declined", plan, checks: [], backupId: null, manual };
6303
6515
  }
6304
6516
  }
6305
6517
  const backup = createBackup(root, toWrite);
6306
- for (const file of toWrite) writeFilePreservingMode(path8.join(root, file.file), file.after);
6518
+ for (const file of toWrite) writeFilePreservingMode(path9.join(root, file.file), file.after);
6307
6519
  out(
6308
6520
  `${symbol.pass} Updated ${toWrite.length} file${toWrite.length === 1 ? "" : "s"} ${style.grey(`(backup ${backup.id})`)}`
6309
6521
  );
@@ -6374,7 +6586,7 @@ async function integrate(root, toolKeys, options) {
6374
6586
  function reportSdkReadiness(root, files) {
6375
6587
  const problems = [];
6376
6588
  if (files.length === 0) return problems;
6377
- const dirs = [...new Set(files.map((file) => path8.dirname(file)))];
6589
+ const dirs = [...new Set(files.map((file) => path9.dirname(file)))];
6378
6590
  const python = files.some((file) => file.endsWith(".py"));
6379
6591
  const sdks = installedSdks(root, { dirs, python });
6380
6592
  for (const sdk of sdks) {
@@ -6451,7 +6663,7 @@ async function removeGuards(root, options) {
6451
6663
  const tsFiles = changed.filter((file) => /\.(m|c)?tsx?$/.test(file));
6452
6664
  const baseline = tsFiles.length > 0 ? typecheck(root, tsFiles) : null;
6453
6665
  const backup = createBackup(root, toWrite);
6454
- for (const entry of toWrite) writeFilePreservingMode(path8.join(root, entry.file), entry.after);
6666
+ for (const entry of toWrite) writeFilePreservingMode(path9.join(root, entry.file), entry.after);
6455
6667
  const checks = [];
6456
6668
  const py = changed.filter((file) => file.endsWith(".py"));
6457
6669
  if (py.length > 0) checks.push(pythonSyntax(root, py));
@@ -6475,7 +6687,7 @@ function currentText(root, plan, file) {
6475
6687
  const entry = plan.files.find((candidate) => candidate.file === file);
6476
6688
  if (entry) return entry.after;
6477
6689
  try {
6478
- return readFileSync8(path8.join(root, file), "utf8");
6690
+ return readFileSync8(path9.join(root, file), "utf8");
6479
6691
  } catch {
6480
6692
  return null;
6481
6693
  }
@@ -6500,7 +6712,7 @@ function applyDispatch(root, plan, file, after, line, items, via) {
6500
6712
  function readSources(root) {
6501
6713
  return listSourceFiles(root).flatMap((file) => {
6502
6714
  try {
6503
- return [{ file, text: readFileSync8(path8.join(root, file), "utf8") }];
6715
+ return [{ file, text: readFileSync8(path9.join(root, file), "utf8") }];
6504
6716
  } catch {
6505
6717
  return [];
6506
6718
  }
@@ -6622,7 +6834,7 @@ function printPlan(plan) {
6622
6834
 
6623
6835
  // src/commands/remove.ts
6624
6836
  import { rmSync as rmSync3 } from "node:fs";
6625
- import path9 from "node:path";
6837
+ import path10 from "node:path";
6626
6838
 
6627
6839
  // src/discover.ts
6628
6840
  var LOCAL_CANDIDATE_PORTS = [4e3, 4400, 4100, 4200, 4300, 4500, 8e3, 8080, 3e3];
@@ -6736,7 +6948,7 @@ async function removeCommand(args, flags, known) {
6736
6948
  if (!file || !config) {
6737
6949
  throw new CliError("This project is not connected to Veefore.", "There is nothing to remove.");
6738
6950
  }
6739
- const root = path9.dirname(file);
6951
+ const root = path10.dirname(file);
6740
6952
  const state = known ?? await agentState(config);
6741
6953
  heading("Remove Veefore from this project");
6742
6954
  details([
@@ -7270,10 +7482,10 @@ async function resolveAuthorizedTools(context, filter) {
7270
7482
 
7271
7483
  // src/panel.ts
7272
7484
  import { homedir as homedir2 } from "node:os";
7273
- import path10 from "node:path";
7485
+ import path11 from "node:path";
7274
7486
 
7275
7487
  // src/version.ts
7276
- var CLI_VERSION = "1.2.0";
7488
+ var CLI_VERSION = "1.4.1";
7277
7489
 
7278
7490
  // src/panel.ts
7279
7491
  function welcomeSource(flags = {}) {
@@ -7297,7 +7509,7 @@ function welcomeSource(flags = {}) {
7297
7509
  }
7298
7510
  function tilde(dir) {
7299
7511
  const home = homedir2();
7300
- return dir === home || dir.startsWith(`${home}${path10.sep}`) ? `~${dir.slice(home.length)}` : dir;
7512
+ return dir === home || dir.startsWith(`${home}${path11.sep}`) ? `~${dir.slice(home.length)}` : dir;
7301
7513
  }
7302
7514
  function hostOf(url) {
7303
7515
  try {
@@ -7637,1542 +7849,1640 @@ function sourceLabel(source) {
7637
7849
  }
7638
7850
  }
7639
7851
 
7640
- // src/commands/init.ts
7641
- var DEFAULT_GATEWAY = "http://localhost:4010";
7642
- async function initCommand(args, flags) {
7643
- const options = parseInitArgs(args);
7644
- const existing = readProjectConfig();
7645
- if (existing && !options.force && isInteractive() && !flags.json) {
7646
- return projectHome(existing, flags);
7647
- }
7648
- if (existing && !options.force) {
7649
- out();
7650
- out(`${symbol.warn} This project is already connected to Veefore.`);
7651
- details([
7652
- ["Agent", `${existing.agentName} (${existing.agentId})`],
7653
- ["Environment", `${existing.environmentName} (${existing.environmentType})`],
7654
- ["Control plane", existing.controlPlaneUrl]
7655
- ]);
7656
- out();
7657
- out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
7852
+ // src/commands/scan.ts
7853
+ async function scanCommand(_args, flags) {
7854
+ const scan = scanProject(process.cwd());
7855
+ if (flags.json) {
7856
+ out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
7658
7857
  return 0;
7659
7858
  }
7660
- heading("Veefore init");
7661
- const framework = detectFramework();
7859
+ heading("Veefore scan");
7662
7860
  out(
7663
- `${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
7861
+ `${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
7664
7862
  );
7665
- if (framework.key === "unknown") {
7666
- out(
7667
- style.grey(
7668
- " No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
7669
- )
7670
- );
7671
- }
7672
- const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
7673
- const controlPlaneUrl = found.controlPlaneUrl;
7674
- if (found.source !== "entered") {
7675
- out(
7676
- `${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
7677
- );
7678
- }
7679
- let session = loadSession(controlPlaneUrl);
7680
- if (!session) {
7681
- await login(controlPlaneUrl);
7682
- session = loadSession(controlPlaneUrl);
7683
- }
7684
- if (!session) throw new CliError("Sign-in did not produce a usable session.");
7685
- const api = new ApiClient({ controlPlaneUrl, token: session.token });
7686
- let me = await api.get("/v1/auth/session");
7687
- const STEPS = ["organization", "workspace", "environment"];
7688
- const asked = /* @__PURE__ */ new Map();
7689
- let organization;
7690
- let workspace;
7691
- let environment;
7692
- for (let at = 0; at < STEPS.length; ) {
7693
- const step = STEPS[at];
7694
- const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
7695
- if (answer === BACK) {
7696
- let previous = at - 1;
7697
- while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
7698
- if (previous < 0) {
7699
- throw new MenuCancelled();
7700
- }
7701
- at = previous;
7702
- continue;
7703
- }
7704
- asked.set(step, answer.asked);
7705
- if (step === "organization") {
7706
- organization = answer.value;
7707
- if (answer.asked) me = await api.get("/v1/auth/session");
7708
- } else if (step === "workspace") {
7709
- workspace = answer.value;
7710
- } else {
7711
- environment = answer.value;
7712
- }
7713
- at += 1;
7714
- }
7715
- heading("Looking at your project");
7716
- const scan = scanProject(process.cwd());
7717
7863
  printScan(scan);
7718
- const plan = buildPlan(scan, {
7719
- agentName: options.agentName,
7720
- extraCapabilities: options.capabilities
7721
- });
7722
- for (; ; ) {
7723
- if (!options.agentName && !flags.yes) {
7724
- const name = await askAgentName(plan.agentName);
7725
- if (name === BACK) {
7726
- const again = await selectEnvironment(api, me, workspace.id, options, flags);
7727
- if (again !== BACK) environment = again.value;
7728
- continue;
7729
- }
7730
- plan.agentName = name;
7731
- }
7732
- printPlan2(plan, environment);
7733
- out();
7734
- const proceed = await confirm("Set this up?", { yes: flags.yes });
7735
- if (proceed) break;
7736
- if (flags.yes || options.agentName) {
7737
- out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
7738
- return 1;
7739
- }
7740
- const next = await select(
7741
- "What would you like to do?",
7864
+ out();
7865
+ out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
7866
+ return 0;
7867
+ }
7868
+
7869
+ // src/help.ts
7870
+ var COMMANDS = [
7871
+ {
7872
+ name: "init",
7873
+ summary: "Set up this project with Veefore. Also runs as plain `veefore`.",
7874
+ usage: "veefore init [options]",
7875
+ description: "Connects to hosted Veefore, or to the self-hosted Veefore named with --control-plane (remembered afterwards), signs you in through the browser, scans the project locally (framework, tools, the hosts they call, the credentials they read \u2014 names only, never values), shows what it will set up and asks once. It then creates an agent with a key pair generated on this machine, registers each tool with its own capability and authorizes it for this agent, adds a starter policy in OBSERVE mode, writes veefore.config.json and .env.local, and validates the connection. The private key is written to .env.local only and is never sent, printed or recoverable.",
7876
+ options: [
7877
+ ["--force", "Replace an existing veefore.config.json."],
7742
7878
  [
7743
- { label: "Change the agent name", value: "name" },
7744
- { label: "Choose a different environment", value: "environment" },
7745
- { label: "Leave setup", value: "leave" }
7879
+ "--new-agent",
7880
+ "Create a separate agent even if one with this name exists. By default the existing agent is reused with a new key, so its delegations and limits keep applying."
7746
7881
  ],
7747
- { initial: 0 }
7748
- );
7749
- if (next === "leave") {
7750
- out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
7751
- return 1;
7752
- }
7753
- if (next === "environment") {
7754
- const again = await selectEnvironment(api, me, workspace.id, options, flags);
7755
- if (again !== BACK) environment = again.value;
7756
- }
7757
- }
7758
- heading("Setting up");
7759
- let created;
7760
- try {
7761
- created = await applyPlan(
7762
- api,
7763
- plan,
7764
- {
7765
- workspaceId: workspace.id,
7766
- environmentId: environment.id,
7767
- framework: framework.key === "unknown" ? null : framework.key
7768
- },
7769
- void 0,
7770
- { newAgent: options.newAgent === true }
7771
- );
7772
- } catch (error) {
7773
- if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
7774
- throw new CliError(
7775
- "Your role cannot set up agents in this workspace.",
7776
- 'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
7777
- );
7778
- }
7779
- throw error;
7780
- }
7781
- let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
7782
- if (!gatewayUrl) {
7783
- const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
7784
- if (published && await gatewayReachable(published)) {
7785
- gatewayUrl = published;
7786
- out(
7787
- `${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
7788
- );
7789
- } else {
7790
- const suggestion = published ?? DEFAULT_GATEWAY;
7791
- out(
7792
- `${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
7793
- );
7794
- gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
7795
- }
7882
+ ["--control-plane <url>", "Self-hosted Veefore address. Needed once; remembered after."],
7883
+ ["--gateway <url>", "Gateway base URL that agents will call."],
7884
+ ["--organization <id>", "Skip the organization prompt."],
7885
+ ["--workspace <id>", "Skip the workspace prompt."],
7886
+ ["--environment <id>", "Skip the environment prompt."],
7887
+ ["--name <name>", "Agent display name."],
7888
+ ["--capability <key>", "Grant a capability. Repeatable."],
7889
+ ["--skip-policy", "Do not create a starter policy."],
7890
+ ["--skip-code", "Do not offer to change your code; run `veefore integrate` later."],
7891
+ [
7892
+ "--approval-mode <wait|throw>",
7893
+ "When approval is required, guarded tools wait for the decision (default) or return at once."
7894
+ ]
7895
+ ],
7896
+ examples: ["veefore init", "veefore init --name finance-agent --capability refund.create"]
7897
+ },
7898
+ {
7899
+ name: "scan",
7900
+ summary: "Preview what setup would find, without changing anything.",
7901
+ usage: "veefore scan [--json]",
7902
+ description: "Scans the project locally and lists its framework, tools, the external hosts each tool calls and the credentials the code reads. Reads source files only: .env files and other hidden files are never opened, and secret values are never read. Needs no sign-in.",
7903
+ options: [["--json", "Machine-readable scan and the setup plan it implies."]]
7904
+ },
7905
+ {
7906
+ name: "integrate",
7907
+ summary: "Wrap your registered tools in guard() so Veefore decides every call.",
7908
+ usage: "veefore integrate [--dry-run] [--check] [--assist=manual|own|veefore|off] [--yes]",
7909
+ description: "Finds each tool registered for this agent in your code and wraps its function in guard() (TypeScript/JavaScript) or adds @guard above it (Python). Your function bodies are not edited. Shows a diff and asks before writing, backs up every file it changes, then runs your TypeScript compiler (--noEmit), node --check or a Python syntax check; if a check newly fails, every file is restored automatically. For tools it will not change on its own, you choose: make the change yourself from exact instructions, use AI with your own Anthropic or OpenAI key (code goes only to that provider), or use Veefore AI if enabled. Before anything is sent, secrets are replaced with placeholders and you see what goes where and must agree; an AI change is accepted only if it keeps every line of your code and adds nothing but the guard, and you still review the diff.",
7910
+ options: [
7911
+ ["--dry-run", "Show the diff and write nothing."],
7912
+ ["--check", "Report which registered tools are guarded in the code. Exits 1 if any are not."],
7913
+ ["--assist=<how>", "manual (default when not interactive), own, veefore, or off."],
7914
+ ["--yes", "Apply without asking."]
7915
+ ],
7916
+ examples: [
7917
+ "veefore integrate --dry-run",
7918
+ "veefore integrate --assist=manual",
7919
+ "ANTHROPIC_API_KEY=\u2026 veefore integrate --assist=own",
7920
+ "veefore integrate --check"
7921
+ ]
7922
+ },
7923
+ {
7924
+ name: "undo",
7925
+ summary: "Restore the files the last `veefore integrate` changed.",
7926
+ usage: "veefore undo [--force]",
7927
+ description: "Restores each file from the backup taken before the last rewrite. A file you edited afterwards is left alone and listed, unless --force is given.",
7928
+ options: [["--force", "Restore files even if they were edited after the rewrite."]]
7929
+ },
7930
+ {
7931
+ name: "remove",
7932
+ summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
7933
+ usage: "veefore remove [--dry-run] [--keep-agent]",
7934
+ description: "Offers to revoke the agent in Veefore if it is still active, removes every guard() this CLI added (showing the diff, asking once, backing up and rolling back if your checks fail), then deletes veefore.config.json and the Veefore block in .env.local, including the private key. Edits you made after setup are kept. `veefore` offers this automatically when the agent was revoked in the console.",
7935
+ options: [
7936
+ ["--dry-run", "Show what would change and write nothing."],
7937
+ ["--keep-agent", "Do not offer to revoke the agent in Veefore."]
7938
+ ],
7939
+ examples: ["veefore remove --dry-run", "veefore remove"]
7940
+ },
7941
+ {
7942
+ name: "dev",
7943
+ summary: "Watch decisions live while you build.",
7944
+ usage: "veefore dev [--all]",
7945
+ description: "Confirms the gateway is reachable, reports the enforcement mode, then streams every decision for this agent with the next concrete step for each outcome.",
7946
+ options: [["--all", "Stream decisions for every agent in the environment."]]
7947
+ },
7948
+ {
7949
+ name: "doctor",
7950
+ summary: "Audit the security posture of this setup.",
7951
+ usage: "veefore doctor [--strict]",
7952
+ description: "Checks authentication, MFA, TLS, local secret permissions, identity and passport validity, gateway health, dangerous tools, network restrictions, policy coverage, production data access, MCP transport and authorization, audit chain integrity, credential rotation, and SDK version alignment.",
7953
+ options: [["--strict", "Exit non-zero on warnings as well as failures."]]
7954
+ },
7955
+ {
7956
+ name: "status",
7957
+ summary: "Show the current environment, mode and agent health.",
7958
+ usage: "veefore status",
7959
+ description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
7960
+ },
7961
+ {
7962
+ name: "test",
7963
+ summary: "Verify enforcement works, without performing any action.",
7964
+ usage: "veefore test [--tool <key>]",
7965
+ description: "Runs a dry-run probe across every tool this agent is authorized for, checks that policy test cases pass, and fails if a dangerous tool is permitted only by the default decision. Suitable as a CI gate.",
7966
+ options: [["--tool <key>", "Probe a single tool."]]
7967
+ },
7968
+ {
7969
+ name: "agents",
7970
+ summary: "Inspect and control the agent fleet.",
7971
+ usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
7972
+ description: "Lifecycle control. `revoke` is irreversible for the identity and shows the full blast radius before asking for confirmation. `rotate-key` issues a new key pair and invalidates every outstanding session for that agent.",
7973
+ options: [
7974
+ ["--all", "List across the whole organization, not just this environment."],
7975
+ ["--status <status>", "Filter by lifecycle status."],
7976
+ ['--reason "<text>"', "Reason recorded in the audit trail."]
7977
+ ],
7978
+ examples: [
7979
+ "veefore agents list",
7980
+ 'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
7981
+ ]
7982
+ },
7983
+ {
7984
+ name: "policies",
7985
+ summary: "List, test and deploy policies.",
7986
+ usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
7987
+ description: "`test` runs the recorded test cases. `deploy` activates a version and refuses to do so while any test case fails; it requires a reason and step-up authentication.",
7988
+ options: [
7989
+ ["--version <n>", "Version to deploy, for `deploy`."],
7990
+ ["--from <n> --to <n>", "Versions to compare, for `diff`."],
7991
+ ['--reason "<text>"', "Reason recorded in the audit trail."]
7992
+ ],
7993
+ examples: [
7994
+ "veefore policies list",
7995
+ "veefore policies test",
7996
+ "veefore policies deploy pol_01ABC"
7997
+ ]
7998
+ },
7999
+ {
8000
+ name: "logs",
8001
+ summary: "Read or stream the decision log.",
8002
+ usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
8003
+ description: "Every line is a server-side decision with the policy and rule that produced it.",
8004
+ options: [
8005
+ ["--follow, -f", "Stream new decisions as they happen."],
8006
+ ["--decision <d>", "Filter by decision, e.g. BLOCK."],
8007
+ ["--agent <id>", "Filter by agent."],
8008
+ ["--all", "Include every agent in the environment."],
8009
+ ["--limit <n>", "Number of recent decisions to show."]
8010
+ ]
8011
+ },
8012
+ {
8013
+ name: "audit",
8014
+ summary: "Query, verify and export the audit trail.",
8015
+ usage: "veefore audit <list|show|verify|export> [eventId]",
8016
+ description: "`verify` re-checks the hash chain and exits non-zero if it is broken. `export` returns a JSON or CSV file together with a content hash an auditor can use to prove the file is unaltered.",
8017
+ options: [
8018
+ ["--severity <s>", "Filter by severity."],
8019
+ ["--type <t>", "Filter by event type."],
8020
+ ["--from <iso> --to <iso>", "Time range."],
8021
+ ["--format <json|csv>", "Export format."],
8022
+ ['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
8023
+ ["--out <file>", "Write the export to a file (mode 0600)."]
8024
+ ],
8025
+ examples: [
8026
+ "veefore audit verify",
8027
+ 'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
8028
+ ]
8029
+ },
8030
+ {
8031
+ name: "deploy",
8032
+ summary: "Promote the environment to enforcement.",
8033
+ usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
8034
+ description: "Runs preflight checks, replays recent observed traffic to show exactly what starts being refused, then requires confirmation, a reason and step-up authentication. Reducing enforcement always requires a reason and is recorded as a CRITICAL audit event.",
8035
+ options: [
8036
+ ["--mode <mode>", "Target mode. Defaults to ENFORCE."],
8037
+ ['--reason "<text>"', "Reason recorded in the audit trail."],
8038
+ ["--skip-preflight", "Incident use only. Recorded in the audit reason."]
8039
+ ]
8040
+ },
8041
+ {
8042
+ name: "login",
8043
+ summary: "Sign in through your browser and store a session token.",
8044
+ usage: "veefore login [--device] [--password] [--control-plane <url>]",
8045
+ description: "Opens the console in your browser, where you sign in (or create an account) and approve this terminal. The terminal never sees your password. Over SSH, or with --device, it prints a link and a code to enter in any browser instead. Only the resulting session token is stored, in ~/.veefore/credentials.json at mode 0600, and the session can be revoked from your profile. In CI, set VEEFORE_EMAIL and VEEFORE_PASSWORD.",
8046
+ options: [
8047
+ ["--device", "Show a link and a code instead of opening a browser here."],
8048
+ ["--password", "Type email and password in the terminal instead of using the browser."]
8049
+ ]
8050
+ },
8051
+ {
8052
+ name: "logout",
8053
+ summary: "Sign out of Veefore on this machine.",
8054
+ usage: "veefore logout [--all]",
8055
+ description: 'Ends the session on the server and deletes the stored token. Signing out of the console with "Also sign out of the Veefore CLI" ticked does the same for every terminal you approved; the next command here then says you were signed out and asks you to sign in again.',
8056
+ options: [["--all", "Sign out of every control plane this machine is signed in to."]]
7796
8057
  }
7797
- gatewayUrl = normalizeUrl(gatewayUrl);
7798
- const config = {
7799
- version: 1,
7800
- controlPlaneUrl,
7801
- gatewayUrl,
7802
- organizationId: organization.id,
7803
- organizationName: organization.name,
7804
- workspaceId: workspace.id,
7805
- workspaceName: workspace.name,
7806
- environmentId: environment.id,
7807
- environmentName: environment.name,
7808
- environmentType: environment.type,
7809
- agentId: created.agentId,
7810
- agentName: created.agentName,
7811
- keyId: created.keyId,
7812
- framework: framework.key === "unknown" ? null : framework.key,
7813
- declaredTools: created.tools.map((tool) => tool.key),
7814
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
7815
- };
7816
- const configFile = writeProjectConfig(config);
8058
+ ];
8059
+ var GLOBAL_OPTIONS = [
8060
+ ["--json", "Machine-readable output."],
8061
+ ["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
8062
+ ["--verbose", "Expanded output."],
8063
+ [
8064
+ "--control-plane <url>",
8065
+ "Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
8066
+ ],
8067
+ ["--gateway <url>", "Override the configured gateway URL."],
8068
+ ["--version, -V", "Print the CLI version."],
8069
+ ["--help, -h", "Show help."]
8070
+ ];
8071
+ function printRootHelp(version) {
8072
+ out();
7817
8073
  out(
7818
- `${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
8074
+ `${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
7819
8075
  );
7820
- const audience = await discoverAudience(gatewayUrl);
7821
- ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
7822
- const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
7823
- const envFile = writeLocalEnv({
7824
- VEEFORE_GATEWAY_URL: gatewayUrl,
7825
- VEEFORE_GATEWAY_AUDIENCE: audience,
7826
- VEEFORE_AGENT_ID: created.agentId,
7827
- VEEFORE_AGENT_KEY_ID: created.keyId,
7828
- VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
7829
- ...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
7830
- // Written explicitly so the behaviour is visible and editable in one place.
7831
- VEEFORE_APPROVAL_MODE: approvalMode
7832
- });
7833
- if (audience !== gatewayUrl) {
7834
- out(
7835
- `${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
7836
- `(it reports this, rather than the ${gatewayUrl} you dialled)`
7837
- )}`
7838
- );
8076
+ out();
8077
+ out(style.bold("USAGE"));
8078
+ out(" veefore <command> [options]");
8079
+ out();
8080
+ out(style.bold("COMMANDS"));
8081
+ const width = Math.max(...COMMANDS.map((command) => command.name.length));
8082
+ for (const command of COMMANDS) {
8083
+ out(` ${style.cyan(command.name.padEnd(width))} ${command.summary}`);
7839
8084
  }
7840
- out(
7841
- `${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
7842
- );
7843
8085
  out();
8086
+ out(style.bold("GLOBAL OPTIONS"));
8087
+ renderOptions(GLOBAL_OPTIONS);
8088
+ out();
8089
+ out(style.bold("GETTING STARTED"));
7844
8090
  out(
7845
- ` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
8091
+ ` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
7846
8092
  );
7847
8093
  out(
7848
- ` never received it and cannot recover it. Move it into your secret manager before deploying.`
8094
+ ` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
7849
8095
  );
7850
- out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
7851
- await configureStarterPolicy(api, workspace.id, flags, options);
7852
- let integrated = false;
7853
- if (created.tools.length > 0 && !options.skipCode) {
7854
- const outcome = await integrate(
7855
- process.cwd(),
7856
- created.tools.map((tool) => tool.key),
7857
- { yes: flags.yes, quiet: flags.json, api }
7858
- );
7859
- integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
7860
- }
7861
- heading("Validating the connection");
7862
- const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
7863
- for (const check of checks) {
7864
- const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
7865
- out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
8096
+ out(` ${style.grey("3.")} veefore doctor ${style.grey("check the security posture")}`);
8097
+ out(` ${style.grey("4.")} veefore deploy ${style.grey("turn enforcement on")}`);
8098
+ out();
8099
+ out(style.grey(" Run `veefore <command> --help` for detail on any command."));
8100
+ out();
8101
+ }
8102
+ function printCommandHelp(name) {
8103
+ const command = COMMANDS.find((entry) => entry.name === name);
8104
+ if (!command) return false;
8105
+ out();
8106
+ out(`${style.bold(`veefore ${command.name}`)} \u2014 ${command.summary}`);
8107
+ out();
8108
+ out(style.bold("USAGE"));
8109
+ out(` ${command.usage}`);
8110
+ out();
8111
+ out(style.bold("DESCRIPTION"));
8112
+ for (const line of wrap2(command.description, 76)) out(` ${line}`);
8113
+ if (command.options && command.options.length > 0) {
8114
+ out();
8115
+ out(style.bold("OPTIONS"));
8116
+ renderOptions(command.options);
7866
8117
  }
7867
- const failed = checks.filter((check) => check.status === "fail");
7868
- if (!integrated) {
7869
- heading("Protect each tool");
8118
+ if (command.examples && command.examples.length > 0) {
7870
8119
  out();
7871
- for (const line of integrationSnippet(framework).split("\n")) {
7872
- out(` ${style.cyan(line)}`);
7873
- }
7874
- const entryPoints = findEntryPoints();
7875
- if (entryPoints.length > 0) {
7876
- out();
7877
- out(
7878
- style.grey(
7879
- ` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
7880
- )
7881
- );
7882
- }
7883
- if (created.tools.length > 0) {
7884
- out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
8120
+ out(style.bold("EXAMPLES"));
8121
+ for (const example of command.examples) out(` ${style.grey("$")} ${example}`);
8122
+ }
8123
+ out();
8124
+ return true;
8125
+ }
8126
+ function renderOptions(options) {
8127
+ const width = Math.max(...options.map(([flag]) => flag.length));
8128
+ for (const [flag, description] of options) {
8129
+ out(` ${style.cyan(flag.padEnd(width))} ${description}`);
8130
+ }
8131
+ }
8132
+ function wrap2(text, width) {
8133
+ const words2 = text.split(/\s+/);
8134
+ const lines = [];
8135
+ let current = "";
8136
+ for (const word of words2) {
8137
+ if (current.length + word.length + 1 > width) {
8138
+ lines.push(current);
8139
+ current = word;
8140
+ } else {
8141
+ current = current === "" ? word : `${current} ${word}`;
7885
8142
  }
7886
8143
  }
7887
- heading("When a human must approve");
7888
- for (const line of approvalModeExplanation(approvalMode)) out(line);
7889
- heading("Next");
7890
- out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
7891
- out(
7892
- ` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
7893
- );
7894
- out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
8144
+ if (current !== "") lines.push(current);
8145
+ return lines;
8146
+ }
8147
+
8148
+ // src/home.ts
8149
+ async function homeCommand(args, flags) {
8150
+ enterApp();
8151
+ try {
8152
+ return await homeLoop(args, flags);
8153
+ } finally {
8154
+ leaveApp();
8155
+ printFarewell();
8156
+ }
8157
+ }
8158
+ var farewellPrinted = false;
8159
+ function printFarewell() {
8160
+ if (!isInteractive() || inApp() || farewellPrinted) return;
8161
+ const project = readProjectConfig();
8162
+ if (!project) return;
8163
+ farewellPrinted = true;
7895
8164
  out();
7896
8165
  out(
7897
- style.grey(
7898
- ` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
7899
- )
8166
+ `${brand(style.bold("Veefore"))} ${style.grey("\xB7")} ${style.bold(project.agentName)} ${style.grey(`\xB7 ${project.environmentName} \xB7 ${project.controlPlaneUrl}`)}`
7900
8167
  );
7901
- out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
8168
+ out(style.grey(" veefore dev watch decisions live"));
8169
+ out(style.grey(" veefore doctor check the security posture"));
8170
+ out(style.grey(" veefore open this again"));
7902
8171
  out();
7903
- if (flags.json) {
7904
- out(
7905
- JSON.stringify(
7906
- {
7907
- config_file: configFile,
7908
- env_file: envFile,
7909
- agent_id: created.agentId,
7910
- key_id: created.keyId,
7911
- fingerprint: created.fingerprint,
7912
- tools: created.tools,
7913
- checks
7914
- },
7915
- null,
7916
- 2
7917
- )
7918
- );
7919
- }
7920
- return failed.length > 0 ? 1 : 0;
7921
8172
  }
7922
- function previousApprovalMode(dir = process.cwd()) {
7923
- try {
7924
- const text = readFileSync9(path11.join(dir, ".env.local"), "utf8");
7925
- const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
7926
- return value === "wait" || value === "throw" ? value : null;
7927
- } catch {
7928
- return null;
8173
+ async function homeLoop(args, flags) {
8174
+ const source = welcomeSource(flags);
8175
+ let first = true;
8176
+ for (; ; ) {
8177
+ if (readProjectConfig()) return initCommand(args, flags);
8178
+ const body = () => {
8179
+ drawScreen(source);
8180
+ out();
8181
+ out(style.bold("Let's get started."));
8182
+ out();
8183
+ };
8184
+ await beginScreen(source, { animate: first });
8185
+ first = false;
8186
+ out();
8187
+ out(style.bold("Let's get started."));
8188
+ out();
8189
+ let action;
8190
+ try {
8191
+ action = await select(
8192
+ "What would you like to do?",
8193
+ [
8194
+ { label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
8195
+ {
8196
+ label: "Preview what setup would find",
8197
+ hint: "veefore scan \xB7 changes nothing",
8198
+ value: "scan"
8199
+ },
8200
+ { label: "Sign in", hint: "veefore login", value: "login" },
8201
+ { label: "Show every command", hint: "veefore help", value: "help" },
8202
+ { label: "Exit", hint: "esc", value: "exit" }
8203
+ ],
8204
+ { redraw: body }
8205
+ );
8206
+ } catch (error) {
8207
+ if (error instanceof MenuCancelled) return 0;
8208
+ throw error;
8209
+ }
8210
+ if (action === "exit") return 0;
8211
+ if (action === "setup") {
8212
+ await runOnScreen(source, flags, () => initCommand(args, flags), {
8213
+ // Setup done: "Continue", and the next pass opens the project home
8214
+ // with the panel filled in. Otherwise "Back to menu", to read why and
8215
+ // choose again.
8216
+ label: () => readProjectConfig() ? "Continue" : "Back to menu",
8217
+ // Setup asks questions and draws its own screens as it goes, so it has
8218
+ // to reach the terminal as it runs. Collecting it would hold those
8219
+ // screens back and then print them all at once.
8220
+ live: true
8221
+ });
8222
+ continue;
8223
+ }
8224
+ const code = await runOnScreen(source, flags, async () => {
8225
+ switch (action) {
8226
+ case "scan":
8227
+ return scanCommand([], flags);
8228
+ case "login":
8229
+ await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
8230
+ return 0;
8231
+ case "help":
8232
+ printRootHelp(source().version);
8233
+ return 0;
8234
+ }
8235
+ return 0;
8236
+ });
8237
+ if (!isInteractive()) return code;
7929
8238
  }
7930
8239
  }
7931
- function approvalModeExplanation(mode) {
7932
- if (mode === "wait") {
7933
- return [
7934
- ` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
7935
- ` ${symbol.bullet} approved: the tool runs once and returns its real result`,
7936
- ` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
7937
- style.grey(
7938
- " The agent process must stay running while it waits (up to the approval's expiry)."
7939
- ),
7940
- style.grey(
7941
- " For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
7942
- ),
7943
- style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
7944
- ];
7945
- }
7946
- return [
7947
- ` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
7948
- " carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
7949
- " or the action never happens.",
7950
- style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
7951
- ];
8240
+ async function initWithWelcome(args, flags) {
8241
+ await beginScreen(welcomeSource(flags), { animate: true });
8242
+ return initCommand(args, flags);
7952
8243
  }
7953
- async function selectOrganization(api, me, options, flags) {
7954
- const organizations = me.organizations.length > 0 ? me.organizations : [
7955
- {
7956
- id: me.organization.id,
7957
- name: me.organization.name,
7958
- slug: me.organization.slug,
7959
- roleKey: me.roleKey
7960
- }
7961
- ];
7962
- if (options.organizationId) {
7963
- const match = organizations.find((org) => org.id === options.organizationId);
7964
- if (!match) {
7965
- throw new CliError(
7966
- `You are not a member of organization ${options.organizationId}.`,
7967
- `Available: ${organizations.map((org) => org.id).join(", ")}`
7968
- );
8244
+ async function runOnScreen(source, flags, run2, options = {}) {
8245
+ const header = () => drawScreen(source);
8246
+ if (options.live) {
8247
+ header();
8248
+ let code2 = 0;
8249
+ try {
8250
+ code2 = await run2();
8251
+ } catch (error) {
8252
+ code2 = error instanceof MenuCancelled ? 0 : reportError(error, flags);
7969
8253
  }
7970
- return { value: match, asked: false };
8254
+ const label2 = options.label ?? "Back";
8255
+ await pressBack(typeof label2 === "function" ? label2() : label2);
8256
+ return code2;
7971
8257
  }
7972
- if (organizations.length === 1 || flags.yes) {
7973
- const chosen2 = organizations[0];
7974
- if (!chosen2) throw new CliError("Your account is not a member of any organization.");
7975
- out(`${symbol.pass} Organization: ${style.bold(chosen2.name)}`);
7976
- return { value: chosen2, asked: false };
8258
+ beginCapture();
8259
+ let code = 0;
8260
+ try {
8261
+ code = await run2();
8262
+ } catch (error) {
8263
+ code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
7977
8264
  }
7978
- out();
7979
- const chosen = await select(
7980
- "Which organization?",
7981
- organizations.map((org) => ({
7982
- label: org.name,
7983
- hint: `${org.roleKey} \xB7 ${org.id}`,
7984
- value: org
7985
- })),
7986
- { quiet: true, back: true, backLabel: "Back" }
7987
- );
7988
- if (chosen === BACK) return BACK;
7989
- await api.post("/v1/auth/scope", { organization_id: chosen.id });
7990
- out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
7991
- return { value: chosen, asked: true };
8265
+ const lines = endCapture();
8266
+ const label = options.label ?? "Back";
8267
+ await showOutput(lines, {
8268
+ drawHeader: header,
8269
+ label: typeof label === "function" ? label() : label
8270
+ });
8271
+ return code;
7992
8272
  }
7993
- async function selectWorkspace(api, me, options, flags) {
7994
- const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
7995
- if (workspaces.length === 0) {
7996
- throw new CliError(
7997
- "This organization has no workspaces.",
7998
- "Create one in the console, or ask an admin to."
7999
- );
8273
+
8274
+ // src/commands/init.ts
8275
+ var DEFAULT_GATEWAY = "http://localhost:4010";
8276
+ async function initCommand(args, flags) {
8277
+ const options = parseInitArgs(args);
8278
+ const existing = readProjectConfig();
8279
+ if (existing && !options.force && isInteractive() && !flags.json) {
8280
+ return projectHome(existing, flags);
8000
8281
  }
8001
- if (options.workspaceId) {
8002
- const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
8003
- if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
8004
- return { value: match, asked: false };
8005
- }
8006
- if (workspaces.length === 1 || flags.yes) {
8007
- const chosen2 = workspaces[0];
8008
- out(`${symbol.pass} Workspace: ${style.bold(chosen2.name)}`);
8009
- return { value: chosen2, asked: false };
8282
+ if (existing && !options.force) {
8283
+ out();
8284
+ out(`${symbol.warn} This project is already connected to Veefore.`);
8285
+ details([
8286
+ ["Agent", `${existing.agentName} (${existing.agentId})`],
8287
+ ["Environment", `${existing.environmentName} (${existing.environmentType})`],
8288
+ ["Control plane", existing.controlPlaneUrl]
8289
+ ]);
8290
+ out();
8291
+ out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
8292
+ return 0;
8010
8293
  }
8011
- out();
8012
- const chosen = await select(
8013
- "Which workspace?",
8014
- workspaces.map((workspace) => ({
8015
- label: workspace.name,
8016
- hint: workspace.id,
8017
- value: workspace
8018
- })),
8019
- { quiet: true, back: true }
8294
+ heading("Veefore init");
8295
+ const framework = detectFramework();
8296
+ out(
8297
+ `${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
8020
8298
  );
8021
- if (chosen === BACK) return BACK;
8022
- out(`${symbol.pass} Workspace: ${style.bold(chosen.name)}`);
8023
- return { value: chosen, asked: true };
8024
- }
8025
- async function selectEnvironment(api, me, workspaceId, options, flags) {
8026
- const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
8027
- const environments = all.filter((environment) => environment.workspaceId === workspaceId);
8028
- if (environments.length === 0) {
8029
- throw new CliError(
8030
- "That workspace has no environments.",
8031
- "Create one in the console, or ask an admin to."
8299
+ if (framework.key === "unknown") {
8300
+ out(
8301
+ style.grey(
8302
+ " No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
8303
+ )
8032
8304
  );
8033
8305
  }
8034
- if (options.environmentId) {
8035
- const match = environments.find((environment) => environment.id === options.environmentId);
8036
- if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
8037
- return { value: match, asked: false };
8306
+ const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
8307
+ const controlPlaneUrl = found.controlPlaneUrl;
8308
+ if (found.source !== "entered") {
8309
+ out(
8310
+ `${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
8311
+ );
8038
8312
  }
8039
- if (flags.yes) {
8040
- const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
8041
- out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
8042
- return { value: chosen, asked: false };
8313
+ let session = loadSession(controlPlaneUrl);
8314
+ if (!session) {
8315
+ await login(controlPlaneUrl);
8316
+ session = loadSession(controlPlaneUrl);
8043
8317
  }
8044
- const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
8045
- for (; ; ) {
8046
- out();
8047
- const chosen = await select(
8048
- "Which environment?",
8049
- environments.map((environment) => ({
8050
- label: environment.name,
8051
- hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
8052
- value: environment
8053
- })),
8054
- { quiet: true, initial: development >= 0 ? development : 0, back: true }
8055
- );
8056
- if (chosen === BACK) return BACK;
8057
- if (chosen.type === "PRODUCTION" && !options.force) {
8058
- out();
8059
- out(
8060
- `${symbol.warn} ${style.amber("You selected a PRODUCTION environment.")} A new agent here starts in ${style.bold("OBSERVE")} mode and performs no enforcement until you switch it.`
8061
- );
8062
- if (!await confirm("Continue with the production environment?", { yes: flags.yes })) {
8063
- out(style.grey(" Choose another environment."));
8318
+ if (!session) throw new CliError("Sign-in did not produce a usable session.");
8319
+ const api = new ApiClient({ controlPlaneUrl, token: session.token });
8320
+ let me = await api.get("/v1/auth/session");
8321
+ const STEPS = ["organization", "workspace", "environment"];
8322
+ const asked = /* @__PURE__ */ new Map();
8323
+ let organization;
8324
+ let workspace;
8325
+ let environment;
8326
+ const source = welcomeSource(flags);
8327
+ const decided = [];
8328
+ enterApp();
8329
+ try {
8330
+ for (let at = 0; at < STEPS.length; ) {
8331
+ const step = STEPS[at];
8332
+ decided.length = at;
8333
+ await stepScreen(source, decided, at + 1, STEPS.length);
8334
+ const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
8335
+ if (answer === BACK) {
8336
+ let previous = at - 1;
8337
+ while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
8338
+ if (previous < 0) {
8339
+ throw new MenuCancelled();
8340
+ }
8341
+ at = previous;
8064
8342
  continue;
8065
8343
  }
8344
+ asked.set(step, answer.asked);
8345
+ if (step === "organization") {
8346
+ organization = answer.value;
8347
+ decided[at] = ["Organization", organization.name];
8348
+ if (answer.asked) me = await api.get("/v1/auth/session");
8349
+ } else if (step === "workspace") {
8350
+ workspace = answer.value;
8351
+ decided[at] = ["Workspace", workspace.name];
8352
+ } else {
8353
+ environment = answer.value;
8354
+ decided[at] = ["Environment", `${environment.name} (${environment.type})`];
8355
+ }
8356
+ at += 1;
8066
8357
  }
8067
- out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
8068
- return { value: chosen, asked: true };
8358
+ } finally {
8359
+ leaveApp();
8069
8360
  }
8070
- }
8071
- async function askAgentName(suggested) {
8361
+ for (const [label, value] of decided) {
8362
+ out(`${symbol.pass} ${label}: ${style.bold(value)}`);
8363
+ }
8364
+ heading("Looking at your project");
8365
+ const scan = scanProject(process.cwd());
8366
+ printScan(scan);
8367
+ const plan = buildPlan(scan, {
8368
+ agentName: options.agentName,
8369
+ extraCapabilities: options.capabilities
8370
+ });
8072
8371
  for (; ; ) {
8372
+ if (!options.agentName && !flags.yes) {
8373
+ const name = await askAgentName(plan.agentName);
8374
+ if (name === BACK) {
8375
+ const again = await selectEnvironment(api, me, workspace.id, options, flags);
8376
+ if (again !== BACK) environment = again.value;
8377
+ continue;
8378
+ }
8379
+ plan.agentName = name;
8380
+ }
8381
+ printPlan2(plan, environment);
8073
8382
  out();
8074
- const choice = await select(
8075
- "Agent name",
8383
+ const proceed = await confirm("Set this up?", { yes: flags.yes });
8384
+ if (proceed) break;
8385
+ if (flags.yes || options.agentName) {
8386
+ out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
8387
+ return 1;
8388
+ }
8389
+ const next = await select(
8390
+ "What would you like to do?",
8076
8391
  [
8077
- { label: suggested, hint: "suggested", value: "keep" },
8078
- { label: "Type a different name\u2026", value: "type" }
8392
+ { label: "Change the agent name", value: "name" },
8393
+ { label: "Choose a different environment", value: "environment" },
8394
+ { label: "Leave setup", value: "leave" }
8079
8395
  ],
8080
- { quiet: true, back: true }
8396
+ { initial: 0 }
8081
8397
  );
8082
- if (choice === BACK) return BACK;
8083
- if (choice === "keep") {
8084
- out(`${symbol.pass} Agent name: ${style.bold(suggested)}`);
8085
- return suggested;
8398
+ if (next === "leave") {
8399
+ out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
8400
+ return 1;
8401
+ }
8402
+ if (next === "environment") {
8403
+ const again = await selectEnvironment(api, me, workspace.id, options, flags);
8404
+ if (again !== BACK) environment = again.value;
8086
8405
  }
8087
- const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
8088
- if (typed === "") continue;
8089
- out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
8090
- return typed;
8091
- }
8092
- }
8093
- async function discoverAudience(gatewayUrl) {
8094
- try {
8095
- const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
8096
- headers: { accept: "application/json" }
8097
- });
8098
- if (!response.ok) return gatewayUrl;
8099
- const body = await response.json();
8100
- const audience = body.data?.audience;
8101
- return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
8102
- } catch {
8103
- return gatewayUrl;
8104
- }
8105
- }
8106
- async function configureStarterPolicy(api, workspaceId, flags, options) {
8107
- if (options.skipPolicy) {
8108
- out(`${symbol.info} Skipped starter policy (--skip-policy).`);
8109
- return;
8110
- }
8111
- const existing = await api.get(
8112
- "/v1/policies",
8113
- { workspace_id: workspaceId }
8114
- );
8115
- const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
8116
- if (count > 0) {
8117
- out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
8118
- return;
8119
- }
8120
- const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
8121
- if (!create) {
8122
- out(style.grey(" Skipped. Create one later from a template in the console."));
8123
- return;
8124
8406
  }
8125
- const policySpinner = spinner("Creating starter policy from the safe-defaults template");
8407
+ heading("Setting up");
8408
+ let created;
8126
8409
  try {
8127
- const result = await api.post("/v1/policies", {
8128
- workspace_id: workspaceId,
8129
- name: "Starter \u2014 safe defaults",
8130
- template_key: "starter-observe-safe-defaults"
8131
- });
8132
- await api.post(`/v1/policies/${result.policy.id}/bindings`, {
8133
- scope: "WORKSPACE",
8134
- scope_id: workspaceId
8135
- });
8136
- policySpinner.succeed(
8137
- `Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
8138
- );
8139
- keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
8140
- out(
8141
- style.grey(
8142
- " Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
8143
- )
8410
+ created = await applyPlan(
8411
+ api,
8412
+ plan,
8413
+ {
8414
+ workspaceId: workspace.id,
8415
+ environmentId: environment.id,
8416
+ framework: framework.key === "unknown" ? null : framework.key
8417
+ },
8418
+ void 0,
8419
+ { newAgent: options.newAgent === true }
8144
8420
  );
8145
8421
  } catch (error) {
8146
- policySpinner.fail("Could not create the starter policy");
8147
- if (error instanceof CliApiError) {
8148
- out(style.grey(` ${error.message}`));
8149
- out(style.grey(" You can create one from a template in the console instead."));
8150
- return;
8422
+ if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
8423
+ throw new CliError(
8424
+ "Your role cannot set up agents in this workspace.",
8425
+ 'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
8426
+ );
8151
8427
  }
8152
8428
  throw error;
8153
8429
  }
8154
- }
8155
- async function validateConnection(api, gatewayUrl, agentId, environmentId) {
8156
- const checks = [];
8157
- try {
8158
- await api.get("/v1/auth/session");
8159
- checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
8160
- } catch (error) {
8161
- checks.push({
8162
- name: "Control plane reachable",
8163
- status: "fail",
8164
- message: error instanceof Error ? error.message : String(error)
8165
- });
8430
+ let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
8431
+ if (!gatewayUrl) {
8432
+ const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
8433
+ if (published && await gatewayReachable(published)) {
8434
+ gatewayUrl = published;
8435
+ out(
8436
+ `${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
8437
+ );
8438
+ } else {
8439
+ const suggestion = published ?? DEFAULT_GATEWAY;
8440
+ out(
8441
+ `${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
8442
+ );
8443
+ gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
8444
+ }
8166
8445
  }
8167
- try {
8168
- const response = await fetch(`${gatewayUrl}/health`, {
8169
- signal: AbortSignal.timeout(5e3)
8170
- });
8171
- const body = await response.json();
8172
- checks.push({
8173
- name: "Gateway reachable",
8174
- status: response.ok ? "pass" : "fail",
8175
- message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
8176
- });
8177
- } catch (error) {
8178
- checks.push({
8179
- name: "Gateway reachable",
8180
- status: "fail",
8181
- message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
8182
- });
8183
- }
8184
- try {
8185
- const identities = await api.get(
8186
- `/v1/agents/${agentId}/identities`
8187
- );
8188
- const active = identities.find((identity) => identity.status === "ACTIVE");
8189
- checks.push({
8190
- name: "Agent identity active",
8191
- status: active ? "pass" : "fail",
8192
- message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
8193
- });
8194
- } catch (error) {
8195
- checks.push({
8196
- name: "Agent identity active",
8197
- status: "warn",
8198
- message: error instanceof Error ? error.message : String(error)
8199
- });
8200
- }
8201
- try {
8202
- const passport = await api.get(
8203
- `/v1/passports/${agentId}`
8446
+ gatewayUrl = normalizeUrl(gatewayUrl);
8447
+ const config = {
8448
+ version: 1,
8449
+ controlPlaneUrl,
8450
+ gatewayUrl,
8451
+ organizationId: organization.id,
8452
+ organizationName: organization.name,
8453
+ workspaceId: workspace.id,
8454
+ workspaceName: workspace.name,
8455
+ environmentId: environment.id,
8456
+ environmentName: environment.name,
8457
+ environmentType: environment.type,
8458
+ agentId: created.agentId,
8459
+ agentName: created.agentName,
8460
+ keyId: created.keyId,
8461
+ framework: framework.key === "unknown" ? null : framework.key,
8462
+ declaredTools: created.tools.map((tool) => tool.key),
8463
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
8464
+ };
8465
+ const configFile = writeProjectConfig(config);
8466
+ out(
8467
+ `${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
8468
+ );
8469
+ const audience = await discoverAudience(gatewayUrl);
8470
+ ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
8471
+ const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
8472
+ const envFile = writeLocalEnv({
8473
+ VEEFORE_GATEWAY_URL: gatewayUrl,
8474
+ VEEFORE_GATEWAY_AUDIENCE: audience,
8475
+ VEEFORE_AGENT_ID: created.agentId,
8476
+ VEEFORE_AGENT_KEY_ID: created.keyId,
8477
+ VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
8478
+ ...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
8479
+ // Written explicitly so the behaviour is visible and editable in one place.
8480
+ VEEFORE_APPROVAL_MODE: approvalMode
8481
+ });
8482
+ if (audience !== gatewayUrl) {
8483
+ out(
8484
+ `${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
8485
+ `(it reports this, rather than the ${gatewayUrl} you dialled)`
8486
+ )}`
8204
8487
  );
8205
- checks.push({
8206
- name: "Agent passport issued",
8207
- status: passport ? "pass" : "warn",
8208
- message: passport ? `version ${passport.version}` : "not issued yet"
8209
- });
8210
- } catch {
8211
- checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
8212
8488
  }
8213
- try {
8214
- const environment = await api.get(
8215
- `/v1/environments/${environmentId}`
8489
+ out(
8490
+ `${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
8491
+ );
8492
+ out();
8493
+ out(
8494
+ ` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
8495
+ );
8496
+ out(
8497
+ ` never received it and cannot recover it. Move it into your secret manager before deploying.`
8498
+ );
8499
+ out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
8500
+ await configureStarterPolicy(api, workspace.id, flags, options);
8501
+ let integrated = false;
8502
+ if (created.tools.length > 0 && !options.skipCode) {
8503
+ const outcome = await integrate(
8504
+ process.cwd(),
8505
+ created.tools.map((tool) => tool.key),
8506
+ { yes: flags.yes, quiet: flags.json, api }
8216
8507
  );
8217
- checks.push({
8218
- name: "Enforcement mode",
8219
- status: "pass",
8220
- message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
8221
- });
8222
- } catch {
8223
- checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
8508
+ integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
8224
8509
  }
8225
- return checks;
8226
- }
8227
- function parseInitArgs(args) {
8228
- const options = { force: false, skipPolicy: false, skipCode: false };
8229
- for (let index = 0; index < args.length; index += 1) {
8230
- const arg = args[index];
8231
- const next = () => {
8232
- const value = args[index + 1];
8233
- if (value === void 0 || value.startsWith("-")) {
8234
- throw new CliError(`Option ${String(arg)} requires a value.`);
8235
- }
8236
- index += 1;
8237
- return value;
8238
- };
8239
- switch (arg) {
8240
- case "--force":
8241
- options.force = true;
8242
- break;
8243
- case "--skip-policy":
8244
- options.skipPolicy = true;
8245
- break;
8246
- case "--skip-code":
8247
- options.skipCode = true;
8248
- break;
8249
- case "--new-agent":
8250
- options.newAgent = true;
8251
- break;
8252
- case "--control-plane":
8253
- options.controlPlaneUrl = next();
8254
- break;
8255
- case "--gateway":
8256
- options.gatewayUrl = next();
8257
- break;
8258
- case "--organization":
8259
- options.organizationId = next();
8260
- break;
8261
- case "--workspace":
8262
- options.workspaceId = next();
8263
- break;
8264
- case "--environment":
8265
- options.environmentId = next();
8266
- break;
8267
- case "--name":
8268
- options.agentName = next();
8269
- break;
8270
- case "--capability":
8271
- options.capabilities = [...options.capabilities ?? [], next()];
8272
- break;
8273
- case "--approval-mode": {
8274
- const value = next().toLowerCase();
8275
- if (value !== "wait" && value !== "throw") {
8276
- throw new CliError('--approval-mode must be "wait" or "throw".');
8277
- }
8278
- options.approvalMode = value;
8279
- break;
8280
- }
8281
- default:
8282
- if (arg?.startsWith("-")) {
8283
- throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
8284
- }
8285
- }
8510
+ heading("Validating the connection");
8511
+ const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
8512
+ for (const check of checks) {
8513
+ const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
8514
+ out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
8286
8515
  }
8287
- return options;
8288
- }
8289
- async function projectHome(existing, flags) {
8290
- const source = welcomeSource(flags);
8291
- let last = 0;
8292
- for (let round = 0; ; round += 1) {
8293
- await beginScreen(source, { animate: round === 0 });
8294
- const state = await agentState(existing);
8295
- printProjectSummary(existing, state);
8296
- const choices = homeChoices(existing, state, flags);
8297
- choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
8516
+ const failed = checks.filter((check) => check.status === "fail");
8517
+ if (!integrated) {
8518
+ heading("Protect each tool");
8298
8519
  out();
8299
- let picked;
8300
- try {
8301
- picked = await select(
8302
- "What would you like to do?",
8303
- choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice }))
8304
- );
8305
- } catch (error) {
8306
- if (error instanceof MenuCancelled) return last;
8307
- throw error;
8520
+ for (const line of integrationSnippet(framework).split("\n")) {
8521
+ out(` ${style.cyan(line)}`);
8308
8522
  }
8309
- if (picked.leaves) {
8310
- await beginScreen(source, { compact: true });
8311
- try {
8312
- return await picked.run();
8313
- } catch (error) {
8314
- return error instanceof MenuCancelled ? last : reportError(error, flags);
8315
- }
8523
+ const entryPoints = findEntryPoints();
8524
+ if (entryPoints.length > 0) {
8525
+ out();
8526
+ out(
8527
+ style.grey(
8528
+ ` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
8529
+ )
8530
+ );
8316
8531
  }
8317
- await beginScreen(source, { compact: true });
8318
- try {
8319
- last = await picked.run();
8320
- } catch (error) {
8321
- last = error instanceof MenuCancelled ? last : reportError(error, flags);
8532
+ if (created.tools.length > 0) {
8533
+ out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
8322
8534
  }
8323
- if (!isInteractive()) return last;
8324
- await pressBack("Back to menu");
8325
8535
  }
8326
- }
8327
- function printProjectSummary(existing, state) {
8536
+ heading("When a human must approve");
8537
+ for (const line of approvalModeExplanation(approvalMode)) out(line);
8538
+ heading("Next");
8539
+ out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
8540
+ out(
8541
+ ` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
8542
+ );
8543
+ out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
8328
8544
  out();
8329
- details([
8330
- ["Agent", `${existing.agentName} (${existing.agentId})`],
8331
- ["Environment", `${existing.environmentName} (${existing.environmentType})`],
8332
- [
8333
- "Status",
8334
- state.kind === "ok" ? decisionStyle(state.status) : state.kind === "signed-out" ? style.amber("not signed in") : state.kind === "session-ended" ? style.amber("signed out") : state.kind === "missing" ? style.red("agent not found") : style.amber("Veefore not reachable")
8335
- ]
8336
- ]);
8337
- if (state.kind === "session-ended") {
8338
- out(`${symbol.warn} ${state.message}`);
8339
- }
8340
- }
8341
- function homeChoices(existing, state, flags) {
8342
- const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
8343
- if (ended) {
8344
- out();
8545
+ out(
8546
+ style.grey(
8547
+ ` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
8548
+ )
8549
+ );
8550
+ out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
8551
+ out();
8552
+ if (flags.json) {
8345
8553
  out(
8346
- `${symbol.warn} ${style.amber(
8347
- state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
8348
- )} Every tool call wrapped in guard() is now refused.`
8349
- );
8350
- return [
8351
- {
8352
- label: "Remove Veefore from this project",
8353
- hint: "take guard() out of the code, disconnect",
8354
- run: () => removeCommand([], flags, state),
8355
- leaves: true
8356
- },
8357
- {
8358
- label: "Set it up again with a new agent",
8359
- hint: "the guarded code keeps working",
8360
- run: () => initCommand(["--force"], flags),
8361
- leaves: true
8362
- }
8363
- ];
8364
- }
8365
- const choices = [];
8366
- if (state.kind === "signed-out" || state.kind === "session-ended") {
8367
- choices.push({
8368
- label: "Sign in",
8369
- hint: "veefore login",
8370
- run: async () => {
8371
- await login(existing.controlPlaneUrl);
8372
- return 0;
8373
- }
8374
- });
8375
- }
8376
- choices.push(
8377
- { label: "Watch decisions live", hint: "veefore dev", run: () => devCommand([], flags) },
8378
- { label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
8379
- {
8380
- label: "Protect tools in the code",
8381
- hint: "veefore integrate",
8382
- run: () => integrateCommand([], flags)
8383
- },
8384
- {
8385
- label: "Check every tool is guarded",
8386
- hint: "veefore integrate --check",
8387
- run: () => integrateCommand(["--check"], flags)
8388
- },
8389
- { label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
8390
- {
8391
- label: "Audit the security posture",
8392
- hint: "veefore doctor",
8393
- run: () => doctorCommand([], flags)
8394
- },
8395
- { label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
8396
- {
8397
- label: "Re-run setup for this project",
8398
- hint: "veefore init --force",
8399
- run: () => initCommand(["--force"], flags),
8400
- leaves: true
8401
- },
8402
- {
8403
- label: "Remove Veefore from this project",
8404
- hint: "veefore remove",
8405
- run: () => removeCommand([], flags, state),
8406
- leaves: true
8407
- }
8408
- );
8409
- if (state.kind === "ok" || state.kind === "unreachable") {
8410
- choices.push({
8411
- label: "Sign out",
8412
- hint: "veefore logout",
8413
- run: async () => {
8414
- await logout(existing.controlPlaneUrl);
8415
- return 0;
8416
- }
8417
- // Deliberately not `leaves`: signing out comes back to this menu, so the
8418
- // panel and the status redraw as signed out and the next step on offer
8419
- // is Sign in. Leaving instead would quit on the one action whose result
8420
- // a person most wants to see confirmed.
8421
- });
8554
+ JSON.stringify(
8555
+ {
8556
+ config_file: configFile,
8557
+ env_file: envFile,
8558
+ agent_id: created.agentId,
8559
+ key_id: created.keyId,
8560
+ fingerprint: created.fingerprint,
8561
+ tools: created.tools,
8562
+ checks
8563
+ },
8564
+ null,
8565
+ 2
8566
+ )
8567
+ );
8422
8568
  }
8423
- return choices;
8569
+ return failed.length > 0 ? 1 : 0;
8424
8570
  }
8425
-
8426
- // src/commands/scan.ts
8427
- async function scanCommand(_args, flags) {
8428
- const scan = scanProject(process.cwd());
8429
- if (flags.json) {
8430
- out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
8431
- return 0;
8571
+ function previousApprovalMode(dir = process.cwd()) {
8572
+ try {
8573
+ const text = readFileSync9(path12.join(dir, ".env.local"), "utf8");
8574
+ const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
8575
+ return value === "wait" || value === "throw" ? value : null;
8576
+ } catch {
8577
+ return null;
8432
8578
  }
8433
- heading("Veefore scan");
8434
- out(
8435
- `${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
8436
- );
8437
- printScan(scan);
8438
- out();
8439
- out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
8440
- return 0;
8441
8579
  }
8442
-
8443
- // src/commands/policies.ts
8444
- function caseCount(result) {
8445
- return result.passed + result.failed;
8580
+ function approvalModeExplanation(mode) {
8581
+ if (mode === "wait") {
8582
+ return [
8583
+ ` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
8584
+ ` ${symbol.bullet} approved: the tool runs once and returns its real result`,
8585
+ ` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
8586
+ style.grey(
8587
+ " The agent process must stay running while it waits (up to the approval's expiry)."
8588
+ ),
8589
+ style.grey(
8590
+ " For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
8591
+ ),
8592
+ style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
8593
+ ];
8594
+ }
8595
+ return [
8596
+ ` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
8597
+ " carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
8598
+ " or the action never happens.",
8599
+ style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
8600
+ ];
8446
8601
  }
8447
- async function policiesCommand(args, flags) {
8448
- const [subcommand, ...rest] = args;
8449
- switch (subcommand) {
8450
- case void 0:
8451
- case "list":
8452
- return listPolicies(rest, flags);
8453
- case "show":
8454
- return showPolicy(rest, flags);
8455
- case "test":
8456
- return testPolicies(rest, flags);
8457
- case "deploy":
8458
- return deployPolicy(rest, flags);
8459
- case "diff":
8460
- return diffPolicy(rest, flags);
8461
- default:
8602
+ async function selectOrganization(api, me, options, flags) {
8603
+ const organizations = me.organizations.length > 0 ? me.organizations : [
8604
+ {
8605
+ id: me.organization.id,
8606
+ name: me.organization.name,
8607
+ slug: me.organization.slug,
8608
+ roleKey: me.roleKey
8609
+ }
8610
+ ];
8611
+ if (options.organizationId) {
8612
+ const match = organizations.find((org) => org.id === options.organizationId);
8613
+ if (!match) {
8462
8614
  throw new CliError(
8463
- `Unknown subcommand \`veefore policies ${subcommand}\`.`,
8464
- "Available: list, show, test, deploy, diff."
8615
+ `You are not a member of organization ${options.organizationId}.`,
8616
+ `Available: ${organizations.map((org) => org.id).join(", ")}`
8465
8617
  );
8618
+ }
8619
+ return { value: match, asked: false };
8466
8620
  }
8467
- }
8468
- async function listPolicies(args, flags) {
8469
- const context = await resolveContext(flags);
8470
- const policies = await context.api.get("/v1/policies", {
8471
- workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
8472
- status: valueFor(args, "--status")
8473
- });
8474
- if (flags.json) {
8475
- printJson({ items: policies });
8476
- return 0;
8621
+ if (organizations.length === 1 || flags.yes) {
8622
+ const chosen2 = organizations[0];
8623
+ if (!chosen2) throw new CliError("Your account is not a member of any organization.");
8624
+ return { value: chosen2, asked: false };
8477
8625
  }
8478
- heading(`Policies in ${context.config.workspaceName}`);
8479
- table(
8480
- [
8481
- { header: "NAME", key: "name", width: 30 },
8482
- { header: "KEY", key: "key", width: 24 },
8483
- { header: "STATUS", key: "status", width: 18 },
8484
- { header: "ACTIVE v", key: "active", width: 10, align: "right" },
8485
- { header: "LATEST v", key: "latest", width: 10, align: "right" },
8486
- { header: "BINDINGS", key: "bindings", width: 9, align: "right" },
8487
- { header: "24H", key: "evaluations", width: 7, align: "right" }
8488
- ],
8489
- policies.map((policy) => ({
8490
- name: policy.name,
8491
- key: policy.key,
8492
- status: decisionStyle(policy.status),
8493
- active: policy.activeVersion ? String(policy.activeVersion.versionNumber) : "\u2014",
8494
- latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
8495
- bindings: String(policy.bindings.length),
8496
- evaluations: String(policy.evaluations24h)
8497
- }))
8498
- );
8499
- const unbound = policies.filter(
8500
- (policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
8501
- );
8502
- const drafts = policies.filter((policy) => policy.status === "DRAFT");
8503
8626
  out();
8504
- if (unbound.length > 0) {
8505
- out(
8506
- ` ${symbol.warn} ${style.amber(`${unbound.length} active policy/policies are not bound to any scope, so they evaluate nothing.`)}`
8627
+ const chosen = await select(
8628
+ "Which organization?",
8629
+ organizations.map((org) => ({
8630
+ label: org.name,
8631
+ hint: `${org.roleKey} \xB7 ${org.id}`,
8632
+ value: org
8633
+ })),
8634
+ { quiet: true, back: true, backLabel: "Back" }
8635
+ );
8636
+ if (chosen === BACK) return BACK;
8637
+ await api.post("/v1/auth/scope", { organization_id: chosen.id });
8638
+ return { value: chosen, asked: true };
8639
+ }
8640
+ async function selectWorkspace(api, me, options, flags) {
8641
+ const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
8642
+ if (workspaces.length === 0) {
8643
+ throw new CliError(
8644
+ "This organization has no workspaces.",
8645
+ "Create one in the console, or ask an admin to."
8507
8646
  );
8508
8647
  }
8509
- if (drafts.length > 0) {
8510
- out(
8511
- ` ${symbol.info} ${drafts.length} draft(s). Run ${style.bold("veefore policies deploy <policyId>")} once the tests pass.`
8512
- );
8648
+ if (options.workspaceId) {
8649
+ const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
8650
+ if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
8651
+ return { value: match, asked: false };
8513
8652
  }
8514
- if (policies.length === 0) {
8515
- out(` ${symbol.warn} No policies exist. Nothing is being evaluated against rules.`);
8653
+ if (workspaces.length === 1 || flags.yes) {
8654
+ const chosen2 = workspaces[0];
8655
+ return { value: chosen2, asked: false };
8516
8656
  }
8517
8657
  out();
8518
- return 0;
8519
- }
8520
- async function showPolicy(args, flags) {
8521
- const context = await resolveContext(flags);
8522
- const policyId = positional(args);
8523
- if (!policyId)
8524
- throw new CliError("A policy id is required. Usage: veefore policies show <policyId>");
8525
- const policy = await context.api.get(
8526
- `/v1/policies/${policyId}`
8658
+ const chosen = await select(
8659
+ "Which workspace?",
8660
+ workspaces.map((workspace) => ({
8661
+ label: workspace.name,
8662
+ hint: workspace.id,
8663
+ value: workspace
8664
+ })),
8665
+ { quiet: true, back: true }
8527
8666
  );
8528
- const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
8529
- if (flags.json) {
8530
- printJson({ policy, versions });
8531
- return 0;
8532
- }
8533
- heading(policy.name);
8534
- details([
8535
- ["ID", policy.id],
8536
- ["Key", policy.key],
8537
- ["Status", decisionStyle(policy.status)],
8538
- ["Priority", String(policy.priority)],
8539
- [
8540
- "Active version",
8541
- policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
8542
- ],
8543
- ["Bound agents", String(policy.boundAgentCount)],
8544
- ["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
8545
- ["Owner", policy.ownerEmail ?? style.grey("unknown")],
8546
- ["Description", policy.description || style.grey("none")]
8547
- ]);
8548
- if (policy.plainEnglish && policy.plainEnglish.length > 0) {
8549
- heading("In plain English");
8550
- for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
8551
- }
8552
- if (policy.bindings.length > 0) {
8553
- heading("Bindings");
8554
- table(
8555
- [
8556
- { header: "SCOPE", key: "scope", width: 14 },
8557
- { header: "TARGET", key: "target", width: 34 },
8558
- { header: "ENABLED", key: "enabled", width: 8 }
8559
- ],
8560
- policy.bindings.map((binding) => ({
8561
- scope: binding.scope,
8562
- target: binding.scopeId,
8563
- enabled: binding.enabled ? style.green("yes") : style.red("no")
8564
- }))
8565
- );
8566
- }
8567
- heading("Versions");
8568
- table(
8569
- [
8570
- { header: "v", key: "version", width: 4, align: "right" },
8571
- { header: "STATUS", key: "status", width: 20 },
8572
- { header: "CREATED", key: "created", width: 20 },
8573
- { header: "SUMMARY", key: "summary", width: 40 }
8574
- ],
8575
- versions.map((version) => ({
8576
- version: String(version.versionNumber),
8577
- status: decisionStyle(version.status),
8578
- created: version.createdAt.slice(0, 16).replace("T", " "),
8579
- summary: version.changeSummary ?? ""
8580
- }))
8581
- );
8582
- out();
8583
- return 0;
8667
+ if (chosen === BACK) return BACK;
8668
+ return { value: chosen, asked: true };
8584
8669
  }
8585
- async function testPolicies(args, flags) {
8586
- const context = await resolveContext(flags);
8587
- const explicit = positional(args);
8588
- const targets = explicit ? [await context.api.get(`/v1/policies/${explicit}`)] : await context.api.get("/v1/policies", {
8589
- workspace_id: context.config.workspaceId
8590
- });
8591
- if (targets.length === 0) {
8592
- out(`${symbol.warn} No policies to test.`);
8593
- return 0;
8670
+ async function selectEnvironment(api, me, workspaceId, options, flags) {
8671
+ const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
8672
+ const environments = all.filter((environment) => environment.workspaceId === workspaceId);
8673
+ if (environments.length === 0) {
8674
+ throw new CliError(
8675
+ "That workspace has no environments.",
8676
+ "Create one in the console, or ask an admin to."
8677
+ );
8594
8678
  }
8595
- const reports = [];
8596
- for (const policy of targets) {
8597
- try {
8598
- const result = await context.api.post(
8599
- `/v1/policies/${policy.id}/tests/run`,
8600
- {}
8601
- );
8602
- reports.push({ policy, result });
8603
- } catch (error) {
8604
- reports.push({
8605
- policy,
8606
- result: null,
8607
- error: error instanceof Error ? error.message : String(error)
8608
- });
8609
- }
8679
+ if (options.environmentId) {
8680
+ const match = environments.find((environment) => environment.id === options.environmentId);
8681
+ if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
8682
+ return { value: match, asked: false };
8610
8683
  }
8611
- if (flags.json) {
8612
- printJson(reports);
8613
- } else {
8614
- renderTestReports(reports);
8684
+ if (flags.yes) {
8685
+ const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
8686
+ return { value: chosen, asked: false };
8615
8687
  }
8616
- const failed = reports.filter(
8617
- (report) => report.error !== void 0 || (report.result?.failed ?? 0) > 0
8618
- );
8619
- return failed.length > 0 ? 1 : 0;
8620
- }
8621
- function renderTestReports(reports) {
8622
- heading("Policy tests");
8623
- let totalCases = 0;
8624
- let totalFailed = 0;
8625
- let withoutTests = 0;
8626
- for (const report of reports) {
8627
- if (report.error) {
8628
- out(`${symbol.fail} ${style.bold(report.policy.name)} ${style.grey(report.error)}`);
8629
- totalFailed += 1;
8630
- continue;
8631
- }
8632
- const result = report.result;
8633
- if (!result || caseCount(result) === 0) {
8634
- withoutTests += 1;
8635
- out(
8636
- `${symbol.warn} ${style.bold(report.policy.name)} ${style.amber("no test cases recorded")}`
8637
- );
8638
- continue;
8639
- }
8640
- totalCases += caseCount(result);
8641
- totalFailed += result.failed;
8642
- const icon = result.failed === 0 ? symbol.pass : symbol.fail;
8643
- out(
8644
- `${icon} ${style.bold(report.policy.name)} ${style.grey(`${result.passed}/${caseCount(result)} passing`)}`
8688
+ const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
8689
+ for (; ; ) {
8690
+ out();
8691
+ const chosen = await select(
8692
+ "Which environment?",
8693
+ environments.map((environment) => ({
8694
+ label: environment.name,
8695
+ hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
8696
+ value: environment
8697
+ })),
8698
+ { quiet: true, initial: development >= 0 ? development : 0, back: true }
8645
8699
  );
8646
- for (const testCase of result.results.filter((entry) => !entry.passed)) {
8700
+ if (chosen === BACK) return BACK;
8701
+ if (chosen.type === "PRODUCTION" && !options.force) {
8702
+ out();
8647
8703
  out(
8648
- ` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
8704
+ `${symbol.warn} ${style.amber("You selected a PRODUCTION environment.")} A new agent here starts in ${style.bold("OBSERVE")} mode and performs no enforcement until you switch it.`
8649
8705
  );
8650
- if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
8706
+ if (!await confirm("Continue with the production environment?", { yes: flags.yes })) {
8707
+ out(style.grey(" Choose another environment."));
8708
+ continue;
8709
+ }
8651
8710
  }
8711
+ return { value: chosen, asked: true };
8652
8712
  }
8713
+ }
8714
+ async function stepScreen(source, decided, step, total) {
8715
+ await beginScreen(source);
8653
8716
  out();
8654
- out(
8655
- totalFailed === 0 ? ` ${symbol.pass} ${style.green(`All ${totalCases} test case(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${totalFailed} failure(s) across ${totalCases} test case(s).`)}`
8656
- );
8657
- if (withoutTests > 0) {
8658
- out(
8659
- ` ${symbol.warn} ${style.amber(`${withoutTests} policy/policies have no test cases.`)} Add cases so activation can be proven safe.`
8660
- );
8717
+ out(` ${style.bold("Set up this project")} ${style.grey(`step ${step} of ${total}`)}`);
8718
+ if (decided.length > 0) {
8719
+ out();
8720
+ const width = Math.max(...decided.map(([label]) => label.length));
8721
+ for (const [label, value] of decided) {
8722
+ out(` ${symbol.pass} ${style.grey(label.padEnd(width))} ${value}`);
8723
+ }
8661
8724
  }
8662
- out();
8663
8725
  }
8664
- async function deployPolicy(args, flags) {
8665
- const context = await resolveContext(flags);
8666
- const policyId = positional(args);
8667
- if (!policyId) {
8668
- throw new CliError(
8669
- "A policy id is required.",
8670
- 'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
8671
- );
8672
- }
8673
- const policy = await context.api.get(`/v1/policies/${policyId}`);
8674
- const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
8675
- const requested = valueFor(args, "--version");
8676
- const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
8677
- if (!target) {
8678
- throw new CliError(
8679
- requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
8680
- "Edit the policy in the console to create a new draft, then deploy it."
8681
- );
8682
- }
8683
- if (target.versionNumber === policy.activeVersion?.versionNumber) {
8684
- out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
8685
- return 0;
8686
- }
8687
- heading(`Deploy ${policy.name} v${target.versionNumber}`);
8688
- details([
8689
- ["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
8690
- ["Policy", `${policy.name} (${policy.id})`],
8691
- [
8692
- "Currently active",
8693
- policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
8694
- ],
8695
- ["Deploying", `v${target.versionNumber}`],
8696
- ["Bindings", String(policy.bindings.length)]
8697
- ]);
8698
- if (policy.bindings.length === 0) {
8726
+ async function askAgentName(suggested) {
8727
+ for (; ; ) {
8699
8728
  out();
8700
- out(
8701
- ` ${symbol.warn} ${style.amber("This policy is not bound to any scope.")} Activating it will not change any decision until you bind it.`
8729
+ const choice = await select(
8730
+ "Agent name",
8731
+ [
8732
+ { label: suggested, hint: "suggested", value: "keep" },
8733
+ { label: "Type a different name\u2026", value: "type" }
8734
+ ],
8735
+ { quiet: true, back: true }
8702
8736
  );
8703
- }
8704
- if (policy.activeVersion !== null) {
8705
- const diff = await context.api.get(`/v1/policies/${policyId}/diff`, {
8706
- from: policy.activeVersion.versionNumber,
8707
- to: target.versionNumber
8708
- }).catch(() => null);
8709
- if (diff) {
8710
- heading("Changes");
8711
- for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
8712
- for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
8713
- for (const rule of diff.changed) {
8714
- out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
8715
- }
8716
- if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
8717
- out(style.grey(" No rule changes."));
8718
- }
8719
- if (diff.riskAssessment) {
8720
- out();
8721
- out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
8722
- }
8737
+ if (choice === BACK) return BACK;
8738
+ if (choice === "keep") {
8739
+ out(`${symbol.pass} Agent name: ${style.bold(suggested)}`);
8740
+ return suggested;
8723
8741
  }
8742
+ const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
8743
+ if (typed === "") continue;
8744
+ out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
8745
+ return typed;
8724
8746
  }
8725
- heading("Tests");
8726
- const testSpinner = spinner("Running recorded test cases");
8727
- let testResult = null;
8747
+ }
8748
+ async function discoverAudience(gatewayUrl) {
8728
8749
  try {
8729
- testResult = await context.api.post(`/v1/policies/${policyId}/tests/run`, {
8730
- version_id: target.id
8750
+ const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
8751
+ headers: { accept: "application/json" }
8731
8752
  });
8732
- testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
8733
- } catch (error) {
8734
- testSpinner.fail("Could not run the test cases");
8735
- throw error;
8753
+ if (!response.ok) return gatewayUrl;
8754
+ const body = await response.json();
8755
+ const audience = body.data?.audience;
8756
+ return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
8757
+ } catch {
8758
+ return gatewayUrl;
8736
8759
  }
8737
- if (testResult.failed > 0) {
8738
- out();
8739
- for (const failure of testResult.results.filter((entry) => !entry.passed)) {
8740
- out(
8741
- ` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
8742
- );
8743
- }
8744
- out();
8745
- throw new CliError(
8746
- `${testResult.failed} test case(s) fail. This version will not be deployed.`,
8747
- "Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
8748
- );
8760
+ }
8761
+ async function configureStarterPolicy(api, workspaceId, flags, options) {
8762
+ if (options.skipPolicy) {
8763
+ out(`${symbol.info} Skipped starter policy (--skip-policy).`);
8764
+ return;
8749
8765
  }
8750
- if (caseCount(testResult) === 0) {
8751
- out();
8766
+ const existing = await api.get(
8767
+ "/v1/policies",
8768
+ { workspace_id: workspaceId }
8769
+ );
8770
+ const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
8771
+ if (count > 0) {
8772
+ out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
8773
+ return;
8774
+ }
8775
+ const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
8776
+ if (!create) {
8777
+ out(style.grey(" Skipped. Create one later from a template in the console."));
8778
+ return;
8779
+ }
8780
+ const policySpinner = spinner("Creating starter policy from the safe-defaults template");
8781
+ try {
8782
+ const result = await api.post("/v1/policies", {
8783
+ workspace_id: workspaceId,
8784
+ name: "Starter \u2014 safe defaults",
8785
+ template_key: "starter-observe-safe-defaults"
8786
+ });
8787
+ await api.post(`/v1/policies/${result.policy.id}/bindings`, {
8788
+ scope: "WORKSPACE",
8789
+ scope_id: workspaceId
8790
+ });
8791
+ policySpinner.succeed(
8792
+ `Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
8793
+ );
8794
+ keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
8752
8795
  out(
8753
- ` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
8796
+ style.grey(
8797
+ " Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
8798
+ )
8754
8799
  );
8755
- const proceed = await confirm("Deploy anyway?", { yes: flags.yes });
8756
- if (!proceed) {
8757
- out(style.grey("Cancelled. Add test cases first."));
8758
- return 1;
8800
+ } catch (error) {
8801
+ policySpinner.fail("Could not create the starter policy");
8802
+ if (error instanceof CliApiError) {
8803
+ out(style.grey(` ${error.message}`));
8804
+ out(style.grey(" You can create one from a template in the console instead."));
8805
+ return;
8759
8806
  }
8807
+ throw error;
8760
8808
  }
8761
- const reason = valueFor(args, "--reason") ?? await ask("Reason for this change (recorded in the audit trail, min 10 characters):");
8762
- if (reason.trim().length < 10) {
8763
- throw new CliError("A reason of at least 10 characters is required.");
8764
- }
8765
- if (context.config.environmentType === "PRODUCTION") {
8766
- out();
8767
- out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
8809
+ }
8810
+ async function validateConnection(api, gatewayUrl, agentId, environmentId) {
8811
+ const checks = [];
8812
+ try {
8813
+ await api.get("/v1/auth/session");
8814
+ checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
8815
+ } catch (error) {
8816
+ checks.push({
8817
+ name: "Control plane reachable",
8818
+ status: "fail",
8819
+ message: error instanceof Error ? error.message : String(error)
8820
+ });
8768
8821
  }
8769
- const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
8770
- if (!confirmed) {
8771
- out(style.grey("Cancelled. No change was made."));
8772
- return 1;
8822
+ try {
8823
+ const response = await fetch(`${gatewayUrl}/health`, {
8824
+ signal: AbortSignal.timeout(5e3)
8825
+ });
8826
+ const body = await response.json();
8827
+ checks.push({
8828
+ name: "Gateway reachable",
8829
+ status: response.ok ? "pass" : "fail",
8830
+ message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
8831
+ });
8832
+ } catch (error) {
8833
+ checks.push({
8834
+ name: "Gateway reachable",
8835
+ status: "fail",
8836
+ message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
8837
+ });
8773
8838
  }
8774
- const deploySpinner = spinner(`Activating v${target.versionNumber}`);
8775
8839
  try {
8776
- await withStepUp(
8777
- context.api,
8778
- () => context.api.post(`/v1/policies/${policyId}/activate`, {
8779
- version_id: target.id,
8780
- reason
8781
- })
8840
+ const identities = await api.get(
8841
+ `/v1/agents/${agentId}/identities`
8782
8842
  );
8783
- deploySpinner.succeed(`${policy.name} v${target.versionNumber} is ACTIVE`);
8843
+ const active = identities.find((identity) => identity.status === "ACTIVE");
8844
+ checks.push({
8845
+ name: "Agent identity active",
8846
+ status: active ? "pass" : "fail",
8847
+ message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
8848
+ });
8784
8849
  } catch (error) {
8785
- deploySpinner.fail("Activation failed");
8786
- throw error;
8850
+ checks.push({
8851
+ name: "Agent identity active",
8852
+ status: "warn",
8853
+ message: error instanceof Error ? error.message : String(error)
8854
+ });
8787
8855
  }
8788
- out();
8789
- out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
8790
- out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
8791
- out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
8792
- out();
8793
- return 0;
8794
- }
8795
- async function diffPolicy(args, flags) {
8796
- const context = await resolveContext(flags);
8797
- const policyId = positional(args);
8798
- if (!policyId) {
8799
- throw new CliError(
8800
- "A policy id is required.",
8801
- "Usage: veefore policies diff <policyId> --from <n> --to <n>"
8856
+ try {
8857
+ const passport = await api.get(
8858
+ `/v1/passports/${agentId}`
8802
8859
  );
8860
+ checks.push({
8861
+ name: "Agent passport issued",
8862
+ status: passport ? "pass" : "warn",
8863
+ message: passport ? `version ${passport.version}` : "not issued yet"
8864
+ });
8865
+ } catch {
8866
+ checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
8803
8867
  }
8804
- const from = valueFor(args, "--from");
8805
- const to = valueFor(args, "--to");
8806
- const diff = await context.api.get(`/v1/policies/${policyId}/diff`, { from, to });
8807
- if (flags.json) {
8808
- printJson(diff);
8809
- return 0;
8810
- }
8811
- heading(`Policy diff v${diff.from} \u2192 v${diff.to}`);
8812
- for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
8813
- for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
8814
- for (const rule of diff.changed) {
8815
- out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
8868
+ try {
8869
+ const environment = await api.get(
8870
+ `/v1/environments/${environmentId}`
8871
+ );
8872
+ checks.push({
8873
+ name: "Enforcement mode",
8874
+ status: "pass",
8875
+ message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
8876
+ });
8877
+ } catch {
8878
+ checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
8816
8879
  }
8817
- if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
8818
- out(style.grey(" No differences."));
8880
+ return checks;
8881
+ }
8882
+ function parseInitArgs(args) {
8883
+ const options = { force: false, skipPolicy: false, skipCode: false };
8884
+ for (let index = 0; index < args.length; index += 1) {
8885
+ const arg = args[index];
8886
+ const next = () => {
8887
+ const value = args[index + 1];
8888
+ if (value === void 0 || value.startsWith("-")) {
8889
+ throw new CliError(`Option ${String(arg)} requires a value.`);
8890
+ }
8891
+ index += 1;
8892
+ return value;
8893
+ };
8894
+ switch (arg) {
8895
+ case "--force":
8896
+ options.force = true;
8897
+ break;
8898
+ case "--skip-policy":
8899
+ options.skipPolicy = true;
8900
+ break;
8901
+ case "--skip-code":
8902
+ options.skipCode = true;
8903
+ break;
8904
+ case "--new-agent":
8905
+ options.newAgent = true;
8906
+ break;
8907
+ case "--control-plane":
8908
+ options.controlPlaneUrl = next();
8909
+ break;
8910
+ case "--gateway":
8911
+ options.gatewayUrl = next();
8912
+ break;
8913
+ case "--organization":
8914
+ options.organizationId = next();
8915
+ break;
8916
+ case "--workspace":
8917
+ options.workspaceId = next();
8918
+ break;
8919
+ case "--environment":
8920
+ options.environmentId = next();
8921
+ break;
8922
+ case "--name":
8923
+ options.agentName = next();
8924
+ break;
8925
+ case "--capability":
8926
+ options.capabilities = [...options.capabilities ?? [], next()];
8927
+ break;
8928
+ case "--approval-mode": {
8929
+ const value = next().toLowerCase();
8930
+ if (value !== "wait" && value !== "throw") {
8931
+ throw new CliError('--approval-mode must be "wait" or "throw".');
8932
+ }
8933
+ options.approvalMode = value;
8934
+ break;
8935
+ }
8936
+ default:
8937
+ if (arg?.startsWith("-")) {
8938
+ throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
8939
+ }
8940
+ }
8819
8941
  }
8820
- if (diff.riskAssessment) {
8821
- out();
8822
- out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
8942
+ return options;
8943
+ }
8944
+ async function projectHome(existing, flags) {
8945
+ enterApp();
8946
+ try {
8947
+ return await projectHomeLoop(existing, flags);
8948
+ } finally {
8949
+ leaveApp();
8950
+ printFarewell();
8823
8951
  }
8824
- out();
8825
- return 0;
8826
8952
  }
8827
-
8828
- // src/help.ts
8829
- var COMMANDS = [
8830
- {
8831
- name: "init",
8832
- summary: "Set up this project with Veefore. Also runs as plain `veefore`.",
8833
- usage: "veefore init [options]",
8834
- description: "Connects to hosted Veefore, or to the self-hosted Veefore named with --control-plane (remembered afterwards), signs you in through the browser, scans the project locally (framework, tools, the hosts they call, the credentials they read \u2014 names only, never values), shows what it will set up and asks once. It then creates an agent with a key pair generated on this machine, registers each tool with its own capability and authorizes it for this agent, adds a starter policy in OBSERVE mode, writes veefore.config.json and .env.local, and validates the connection. The private key is written to .env.local only and is never sent, printed or recoverable.",
8835
- options: [
8836
- ["--force", "Replace an existing veefore.config.json."],
8837
- [
8838
- "--new-agent",
8839
- "Create a separate agent even if one with this name exists. By default the existing agent is reused with a new key, so its delegations and limits keep applying."
8840
- ],
8841
- ["--control-plane <url>", "Self-hosted Veefore address. Needed once; remembered after."],
8842
- ["--gateway <url>", "Gateway base URL that agents will call."],
8843
- ["--organization <id>", "Skip the organization prompt."],
8844
- ["--workspace <id>", "Skip the workspace prompt."],
8845
- ["--environment <id>", "Skip the environment prompt."],
8846
- ["--name <name>", "Agent display name."],
8847
- ["--capability <key>", "Grant a capability. Repeatable."],
8848
- ["--skip-policy", "Do not create a starter policy."],
8849
- ["--skip-code", "Do not offer to change your code; run `veefore integrate` later."],
8850
- [
8851
- "--approval-mode <wait|throw>",
8852
- "When approval is required, guarded tools wait for the decision (default) or return at once."
8853
- ]
8854
- ],
8855
- examples: ["veefore init", "veefore init --name finance-agent --capability refund.create"]
8856
- },
8857
- {
8858
- name: "scan",
8859
- summary: "Preview what setup would find, without changing anything.",
8860
- usage: "veefore scan [--json]",
8861
- description: "Scans the project locally and lists its framework, tools, the external hosts each tool calls and the credentials the code reads. Reads source files only: .env files and other hidden files are never opened, and secret values are never read. Needs no sign-in.",
8862
- options: [["--json", "Machine-readable scan and the setup plan it implies."]]
8863
- },
8864
- {
8865
- name: "integrate",
8866
- summary: "Wrap your registered tools in guard() so Veefore decides every call.",
8867
- usage: "veefore integrate [--dry-run] [--check] [--assist=manual|own|veefore|off] [--yes]",
8868
- description: "Finds each tool registered for this agent in your code and wraps its function in guard() (TypeScript/JavaScript) or adds @guard above it (Python). Your function bodies are not edited. Shows a diff and asks before writing, backs up every file it changes, then runs your TypeScript compiler (--noEmit), node --check or a Python syntax check; if a check newly fails, every file is restored automatically. For tools it will not change on its own, you choose: make the change yourself from exact instructions, use AI with your own Anthropic or OpenAI key (code goes only to that provider), or use Veefore AI if enabled. Before anything is sent, secrets are replaced with placeholders and you see what goes where and must agree; an AI change is accepted only if it keeps every line of your code and adds nothing but the guard, and you still review the diff.",
8869
- options: [
8870
- ["--dry-run", "Show the diff and write nothing."],
8871
- ["--check", "Report which registered tools are guarded in the code. Exits 1 if any are not."],
8872
- ["--assist=<how>", "manual (default when not interactive), own, veefore, or off."],
8873
- ["--yes", "Apply without asking."]
8874
- ],
8875
- examples: [
8876
- "veefore integrate --dry-run",
8877
- "veefore integrate --assist=manual",
8878
- "ANTHROPIC_API_KEY=\u2026 veefore integrate --assist=own",
8879
- "veefore integrate --check"
8880
- ]
8881
- },
8882
- {
8883
- name: "undo",
8884
- summary: "Restore the files the last `veefore integrate` changed.",
8885
- usage: "veefore undo [--force]",
8886
- description: "Restores each file from the backup taken before the last rewrite. A file you edited afterwards is left alone and listed, unless --force is given.",
8887
- options: [["--force", "Restore files even if they were edited after the rewrite."]]
8888
- },
8889
- {
8890
- name: "remove",
8891
- summary: "Take Veefore out of this project: guard() from the code, then disconnect.",
8892
- usage: "veefore remove [--dry-run] [--keep-agent]",
8893
- description: "Offers to revoke the agent in Veefore if it is still active, removes every guard() this CLI added (showing the diff, asking once, backing up and rolling back if your checks fail), then deletes veefore.config.json and the Veefore block in .env.local, including the private key. Edits you made after setup are kept. `veefore` offers this automatically when the agent was revoked in the console.",
8894
- options: [
8895
- ["--dry-run", "Show what would change and write nothing."],
8896
- ["--keep-agent", "Do not offer to revoke the agent in Veefore."]
8897
- ],
8898
- examples: ["veefore remove --dry-run", "veefore remove"]
8899
- },
8900
- {
8901
- name: "dev",
8902
- summary: "Watch decisions live while you build.",
8903
- usage: "veefore dev [--all]",
8904
- description: "Confirms the gateway is reachable, reports the enforcement mode, then streams every decision for this agent with the next concrete step for each outcome.",
8905
- options: [["--all", "Stream decisions for every agent in the environment."]]
8906
- },
8907
- {
8908
- name: "doctor",
8909
- summary: "Audit the security posture of this setup.",
8910
- usage: "veefore doctor [--strict]",
8911
- description: "Checks authentication, MFA, TLS, local secret permissions, identity and passport validity, gateway health, dangerous tools, network restrictions, policy coverage, production data access, MCP transport and authorization, audit chain integrity, credential rotation, and SDK version alignment.",
8912
- options: [["--strict", "Exit non-zero on warnings as well as failures."]]
8913
- },
8914
- {
8915
- name: "status",
8916
- summary: "Show the current environment, mode and agent health.",
8917
- usage: "veefore status",
8918
- description: "Prints the active organization, workspace and environment, the enforcement mode, the agent lifecycle state, gateway health, and the last few decisions."
8919
- },
8920
- {
8921
- name: "test",
8922
- summary: "Verify enforcement works, without performing any action.",
8923
- usage: "veefore test [--tool <key>]",
8924
- description: "Runs a dry-run probe across every tool this agent is authorized for, checks that policy test cases pass, and fails if a dangerous tool is permitted only by the default decision. Suitable as a CI gate.",
8925
- options: [["--tool <key>", "Probe a single tool."]]
8926
- },
8927
- {
8928
- name: "agents",
8929
- summary: "Inspect and control the agent fleet.",
8930
- usage: "veefore agents <list|show|pause|resume|suspend|revoke|rotate-key> [agentId]",
8931
- description: "Lifecycle control. `revoke` is irreversible for the identity and shows the full blast radius before asking for confirmation. `rotate-key` issues a new key pair and invalidates every outstanding session for that agent.",
8932
- options: [
8933
- ["--all", "List across the whole organization, not just this environment."],
8934
- ["--status <status>", "Filter by lifecycle status."],
8935
- ['--reason "<text>"', "Reason recorded in the audit trail."]
8936
- ],
8937
- examples: [
8938
- "veefore agents list",
8939
- 'veefore agents revoke agt_01ABC --reason "Key suspected compromised"'
8940
- ]
8941
- },
8942
- {
8943
- name: "policies",
8944
- summary: "List, test and deploy policies.",
8945
- usage: "veefore policies <list|show|test|deploy|diff> [policyId]",
8946
- description: "`test` runs the recorded test cases. `deploy` activates a version and refuses to do so while any test case fails; it requires a reason and step-up authentication.",
8947
- options: [
8948
- ["--version <n>", "Version to deploy, for `deploy`."],
8949
- ["--from <n> --to <n>", "Versions to compare, for `diff`."],
8950
- ['--reason "<text>"', "Reason recorded in the audit trail."]
8951
- ],
8952
- examples: [
8953
- "veefore policies list",
8954
- "veefore policies test",
8955
- "veefore policies deploy pol_01ABC"
8956
- ]
8957
- },
8958
- {
8959
- name: "logs",
8960
- summary: "Read or stream the decision log.",
8961
- usage: "veefore logs [--follow] [--decision <d>] [--limit <n>]",
8962
- description: "Every line is a server-side decision with the policy and rule that produced it.",
8963
- options: [
8964
- ["--follow, -f", "Stream new decisions as they happen."],
8965
- ["--decision <d>", "Filter by decision, e.g. BLOCK."],
8966
- ["--agent <id>", "Filter by agent."],
8967
- ["--all", "Include every agent in the environment."],
8968
- ["--limit <n>", "Number of recent decisions to show."]
8969
- ]
8970
- },
8971
- {
8972
- name: "audit",
8973
- summary: "Query, verify and export the audit trail.",
8974
- usage: "veefore audit <list|show|verify|export> [eventId]",
8975
- description: "`verify` re-checks the hash chain and exits non-zero if it is broken. `export` returns a JSON or CSV file together with a content hash an auditor can use to prove the file is unaltered.",
8976
- options: [
8977
- ["--severity <s>", "Filter by severity."],
8978
- ["--type <t>", "Filter by event type."],
8979
- ["--from <iso> --to <iso>", "Time range."],
8980
- ["--format <json|csv>", "Export format."],
8981
- ['--reason "<text>"', "Why this export is being taken. Recorded in the audit trail."],
8982
- ["--out <file>", "Write the export to a file (mode 0600)."]
8983
- ],
8984
- examples: [
8985
- "veefore audit verify",
8986
- 'veefore audit export --format csv --reason "Quarterly SOC 2 evidence" --out audit.csv'
8987
- ]
8988
- },
8989
- {
8990
- name: "deploy",
8991
- summary: "Promote the environment to enforcement.",
8992
- usage: "veefore deploy [--mode ENFORCE|SIMULATE|OBSERVE]",
8993
- description: "Runs preflight checks, replays recent observed traffic to show exactly what starts being refused, then requires confirmation, a reason and step-up authentication. Reducing enforcement always requires a reason and is recorded as a CRITICAL audit event.",
8994
- options: [
8995
- ["--mode <mode>", "Target mode. Defaults to ENFORCE."],
8996
- ['--reason "<text>"', "Reason recorded in the audit trail."],
8997
- ["--skip-preflight", "Incident use only. Recorded in the audit reason."]
8998
- ]
8999
- },
9000
- {
9001
- name: "login",
9002
- summary: "Sign in through your browser and store a session token.",
9003
- usage: "veefore login [--device] [--password] [--control-plane <url>]",
9004
- description: "Opens the console in your browser, where you sign in (or create an account) and approve this terminal. The terminal never sees your password. Over SSH, or with --device, it prints a link and a code to enter in any browser instead. Only the resulting session token is stored, in ~/.veefore/credentials.json at mode 0600, and the session can be revoked from your profile. In CI, set VEEFORE_EMAIL and VEEFORE_PASSWORD.",
9005
- options: [
9006
- ["--device", "Show a link and a code instead of opening a browser here."],
9007
- ["--password", "Type email and password in the terminal instead of using the browser."]
8953
+ async function projectHomeLoop(existing, flags) {
8954
+ const source = welcomeSource(flags);
8955
+ let last = 0;
8956
+ for (let round = 0; ; round += 1) {
8957
+ await beginScreen(source, { animate: round === 0 });
8958
+ const state = await agentState(existing);
8959
+ printProjectSummary(existing, state);
8960
+ const choices = homeChoices(existing, state, flags);
8961
+ choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
8962
+ out();
8963
+ let picked;
8964
+ try {
8965
+ picked = await select(
8966
+ "What would you like to do?",
8967
+ choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice })),
8968
+ {
8969
+ // Repainted as soon as a sign-in, sign-out or project change is
8970
+ // noticed, so the panel and the status never sit stale.
8971
+ redraw: () => {
8972
+ drawScreen(source);
8973
+ printProjectSummary(existing, state);
8974
+ out();
8975
+ }
8976
+ }
8977
+ );
8978
+ } catch (error) {
8979
+ if (error instanceof MenuCancelled) return last;
8980
+ throw error;
8981
+ }
8982
+ if (picked.leaves) {
8983
+ return onOrdinaryScreen(async () => {
8984
+ drawScreen(source);
8985
+ try {
8986
+ return await picked.run();
8987
+ } catch (error) {
8988
+ return error instanceof MenuCancelled ? last : reportError(error, flags);
8989
+ }
8990
+ });
8991
+ }
8992
+ last = await runOnScreen(source, flags, () => picked.run(), {
8993
+ label: "Back to menu",
8994
+ live: picked.live
8995
+ });
8996
+ if (!isInteractive()) return last;
8997
+ }
8998
+ }
8999
+ function printProjectSummary(existing, state) {
9000
+ out();
9001
+ details([
9002
+ ["Agent", `${existing.agentName} (${existing.agentId})`],
9003
+ ["Environment", `${existing.environmentName} (${existing.environmentType})`],
9004
+ [
9005
+ "Status",
9006
+ state.kind === "ok" ? decisionStyle(state.status) : state.kind === "signed-out" ? style.amber("not signed in") : state.kind === "session-ended" ? style.amber("signed out") : state.kind === "missing" ? style.red("agent not found") : style.amber("Veefore not reachable")
9008
9007
  ]
9009
- },
9010
- {
9011
- name: "logout",
9012
- summary: "Sign out of Veefore on this machine.",
9013
- usage: "veefore logout [--all]",
9014
- description: 'Ends the session on the server and deletes the stored token. Signing out of the console with "Also sign out of the Veefore CLI" ticked does the same for every terminal you approved; the next command here then says you were signed out and asks you to sign in again.',
9015
- options: [["--all", "Sign out of every control plane this machine is signed in to."]]
9008
+ ]);
9009
+ if (state.kind === "session-ended") {
9010
+ out(`${symbol.warn} ${state.message}`);
9011
+ }
9012
+ }
9013
+ function homeChoices(existing, state, flags) {
9014
+ const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
9015
+ if (ended) {
9016
+ out();
9017
+ out(
9018
+ `${symbol.warn} ${style.amber(
9019
+ state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
9020
+ )} Every tool call wrapped in guard() is now refused.`
9021
+ );
9022
+ return [
9023
+ {
9024
+ label: "Remove Veefore from this project",
9025
+ hint: "take guard() out of the code, disconnect",
9026
+ run: () => removeCommand([], flags, state),
9027
+ leaves: true
9028
+ },
9029
+ {
9030
+ label: "Set it up again with a new agent",
9031
+ hint: "the guarded code keeps working",
9032
+ run: () => initCommand(["--force"], flags),
9033
+ leaves: true
9034
+ }
9035
+ ];
9036
+ }
9037
+ const choices = [];
9038
+ if (state.kind === "signed-out" || state.kind === "session-ended") {
9039
+ choices.push({
9040
+ label: "Sign in",
9041
+ hint: "veefore login",
9042
+ run: async () => {
9043
+ await login(existing.controlPlaneUrl);
9044
+ return 0;
9045
+ }
9046
+ });
9047
+ }
9048
+ choices.push(
9049
+ {
9050
+ label: "Watch decisions live",
9051
+ hint: "veefore dev",
9052
+ run: () => devCommand([], flags),
9053
+ live: true
9054
+ },
9055
+ { label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
9056
+ {
9057
+ label: "Protect tools in the code",
9058
+ hint: "veefore integrate",
9059
+ run: () => integrateCommand([], flags)
9060
+ },
9061
+ {
9062
+ label: "Check every tool is guarded",
9063
+ hint: "veefore integrate --check",
9064
+ run: () => integrateCommand(["--check"], flags)
9065
+ },
9066
+ { label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
9067
+ {
9068
+ label: "Audit the security posture",
9069
+ hint: "veefore doctor",
9070
+ run: () => doctorCommand([], flags)
9071
+ },
9072
+ { label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
9073
+ {
9074
+ label: "Re-run setup for this project",
9075
+ hint: "veefore init --force",
9076
+ run: () => initCommand(["--force"], flags),
9077
+ leaves: true
9078
+ },
9079
+ {
9080
+ label: "Remove Veefore from this project",
9081
+ hint: "veefore remove",
9082
+ run: () => removeCommand([], flags, state),
9083
+ leaves: true
9084
+ }
9085
+ );
9086
+ if (state.kind === "ok" || state.kind === "unreachable") {
9087
+ choices.push({
9088
+ label: "Sign out",
9089
+ hint: "veefore logout",
9090
+ run: async () => {
9091
+ await logout(existing.controlPlaneUrl);
9092
+ return 0;
9093
+ }
9094
+ // Deliberately not `leaves`: signing out comes back to this menu, so the
9095
+ // panel and the status redraw as signed out and the next step on offer
9096
+ // is Sign in. Leaving instead would quit on the one action whose result
9097
+ // a person most wants to see confirmed.
9098
+ });
9016
9099
  }
9017
- ];
9018
- var GLOBAL_OPTIONS = [
9019
- ["--json", "Machine-readable output."],
9020
- ["--yes, -y", "Assume yes for confirmations. Never skips step-up authentication."],
9021
- ["--verbose", "Expanded output."],
9022
- [
9023
- "--control-plane <url>",
9024
- "Use this Veefore instead of the one found automatically (hosted, or the last you signed in to)."
9025
- ],
9026
- ["--gateway <url>", "Override the configured gateway URL."],
9027
- ["--version, -V", "Print the CLI version."],
9028
- ["--help, -h", "Show help."]
9029
- ];
9030
- function printRootHelp(version) {
9031
- out();
9032
- out(
9033
- `${style.bold("veefore")} ${style.grey(version)} \u2014 identity, authority and policy for AI agents`
9100
+ return choices;
9101
+ }
9102
+
9103
+ // src/commands/policies.ts
9104
+ function caseCount(result) {
9105
+ return result.passed + result.failed;
9106
+ }
9107
+ async function policiesCommand(args, flags) {
9108
+ const [subcommand, ...rest] = args;
9109
+ switch (subcommand) {
9110
+ case void 0:
9111
+ case "list":
9112
+ return listPolicies(rest, flags);
9113
+ case "show":
9114
+ return showPolicy(rest, flags);
9115
+ case "test":
9116
+ return testPolicies(rest, flags);
9117
+ case "deploy":
9118
+ return deployPolicy(rest, flags);
9119
+ case "diff":
9120
+ return diffPolicy(rest, flags);
9121
+ default:
9122
+ throw new CliError(
9123
+ `Unknown subcommand \`veefore policies ${subcommand}\`.`,
9124
+ "Available: list, show, test, deploy, diff."
9125
+ );
9126
+ }
9127
+ }
9128
+ async function listPolicies(args, flags) {
9129
+ const context = await resolveContext(flags);
9130
+ const policies = await context.api.get("/v1/policies", {
9131
+ workspace_id: args.includes("--all") ? void 0 : context.config.workspaceId,
9132
+ status: valueFor(args, "--status")
9133
+ });
9134
+ if (flags.json) {
9135
+ printJson({ items: policies });
9136
+ return 0;
9137
+ }
9138
+ heading(`Policies in ${context.config.workspaceName}`);
9139
+ table(
9140
+ [
9141
+ { header: "NAME", key: "name", width: 30 },
9142
+ { header: "KEY", key: "key", width: 24 },
9143
+ { header: "STATUS", key: "status", width: 18 },
9144
+ { header: "ACTIVE v", key: "active", width: 10, align: "right" },
9145
+ { header: "LATEST v", key: "latest", width: 10, align: "right" },
9146
+ { header: "BINDINGS", key: "bindings", width: 9, align: "right" },
9147
+ { header: "24H", key: "evaluations", width: 7, align: "right" }
9148
+ ],
9149
+ policies.map((policy) => ({
9150
+ name: policy.name,
9151
+ key: policy.key,
9152
+ status: decisionStyle(policy.status),
9153
+ active: policy.activeVersion ? String(policy.activeVersion.versionNumber) : "\u2014",
9154
+ latest: policy.latestVersion ? String(policy.latestVersion.versionNumber) : "\u2014",
9155
+ bindings: String(policy.bindings.length),
9156
+ evaluations: String(policy.evaluations24h)
9157
+ }))
9158
+ );
9159
+ const unbound = policies.filter(
9160
+ (policy) => policy.status === "ACTIVE" && policy.bindings.length === 0
9034
9161
  );
9162
+ const drafts = policies.filter((policy) => policy.status === "DRAFT");
9035
9163
  out();
9036
- out(style.bold("USAGE"));
9037
- out(" veefore <command> [options]");
9164
+ if (unbound.length > 0) {
9165
+ out(
9166
+ ` ${symbol.warn} ${style.amber(`${unbound.length} active policy/policies are not bound to any scope, so they evaluate nothing.`)}`
9167
+ );
9168
+ }
9169
+ if (drafts.length > 0) {
9170
+ out(
9171
+ ` ${symbol.info} ${drafts.length} draft(s). Run ${style.bold("veefore policies deploy <policyId>")} once the tests pass.`
9172
+ );
9173
+ }
9174
+ if (policies.length === 0) {
9175
+ out(` ${symbol.warn} No policies exist. Nothing is being evaluated against rules.`);
9176
+ }
9038
9177
  out();
9039
- out(style.bold("COMMANDS"));
9040
- const width = Math.max(...COMMANDS.map((command) => command.name.length));
9041
- for (const command of COMMANDS) {
9042
- out(` ${style.cyan(command.name.padEnd(width))} ${command.summary}`);
9178
+ return 0;
9179
+ }
9180
+ async function showPolicy(args, flags) {
9181
+ const context = await resolveContext(flags);
9182
+ const policyId = positional(args);
9183
+ if (!policyId)
9184
+ throw new CliError("A policy id is required. Usage: veefore policies show <policyId>");
9185
+ const policy = await context.api.get(
9186
+ `/v1/policies/${policyId}`
9187
+ );
9188
+ const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
9189
+ if (flags.json) {
9190
+ printJson({ policy, versions });
9191
+ return 0;
9192
+ }
9193
+ heading(policy.name);
9194
+ details([
9195
+ ["ID", policy.id],
9196
+ ["Key", policy.key],
9197
+ ["Status", decisionStyle(policy.status)],
9198
+ ["Priority", String(policy.priority)],
9199
+ [
9200
+ "Active version",
9201
+ policy.activeVersion ? `v${policy.activeVersion.versionNumber}` : style.grey("none")
9202
+ ],
9203
+ ["Bound agents", String(policy.boundAgentCount)],
9204
+ ["Evaluations (24h)", `${policy.evaluations24h} ${style.grey(`${policy.blocks24h} blocked`)}`],
9205
+ ["Owner", policy.ownerEmail ?? style.grey("unknown")],
9206
+ ["Description", policy.description || style.grey("none")]
9207
+ ]);
9208
+ if (policy.plainEnglish && policy.plainEnglish.length > 0) {
9209
+ heading("In plain English");
9210
+ for (const line of policy.plainEnglish) out(` ${symbol.bullet} ${line}`);
9211
+ }
9212
+ if (policy.bindings.length > 0) {
9213
+ heading("Bindings");
9214
+ table(
9215
+ [
9216
+ { header: "SCOPE", key: "scope", width: 14 },
9217
+ { header: "TARGET", key: "target", width: 34 },
9218
+ { header: "ENABLED", key: "enabled", width: 8 }
9219
+ ],
9220
+ policy.bindings.map((binding) => ({
9221
+ scope: binding.scope,
9222
+ target: binding.scopeId,
9223
+ enabled: binding.enabled ? style.green("yes") : style.red("no")
9224
+ }))
9225
+ );
9043
9226
  }
9227
+ heading("Versions");
9228
+ table(
9229
+ [
9230
+ { header: "v", key: "version", width: 4, align: "right" },
9231
+ { header: "STATUS", key: "status", width: 20 },
9232
+ { header: "CREATED", key: "created", width: 20 },
9233
+ { header: "SUMMARY", key: "summary", width: 40 }
9234
+ ],
9235
+ versions.map((version) => ({
9236
+ version: String(version.versionNumber),
9237
+ status: decisionStyle(version.status),
9238
+ created: version.createdAt.slice(0, 16).replace("T", " "),
9239
+ summary: version.changeSummary ?? ""
9240
+ }))
9241
+ );
9044
9242
  out();
9045
- out(style.bold("GLOBAL OPTIONS"));
9046
- renderOptions(GLOBAL_OPTIONS);
9243
+ return 0;
9244
+ }
9245
+ async function testPolicies(args, flags) {
9246
+ const context = await resolveContext(flags);
9247
+ const explicit = positional(args);
9248
+ const targets = explicit ? [await context.api.get(`/v1/policies/${explicit}`)] : await context.api.get("/v1/policies", {
9249
+ workspace_id: context.config.workspaceId
9250
+ });
9251
+ if (targets.length === 0) {
9252
+ out(`${symbol.warn} No policies to test.`);
9253
+ return 0;
9254
+ }
9255
+ const reports = [];
9256
+ for (const policy of targets) {
9257
+ try {
9258
+ const result = await context.api.post(
9259
+ `/v1/policies/${policy.id}/tests/run`,
9260
+ {}
9261
+ );
9262
+ reports.push({ policy, result });
9263
+ } catch (error) {
9264
+ reports.push({
9265
+ policy,
9266
+ result: null,
9267
+ error: error instanceof Error ? error.message : String(error)
9268
+ });
9269
+ }
9270
+ }
9271
+ if (flags.json) {
9272
+ printJson(reports);
9273
+ } else {
9274
+ renderTestReports(reports);
9275
+ }
9276
+ const failed = reports.filter(
9277
+ (report) => report.error !== void 0 || (report.result?.failed ?? 0) > 0
9278
+ );
9279
+ return failed.length > 0 ? 1 : 0;
9280
+ }
9281
+ function renderTestReports(reports) {
9282
+ heading("Policy tests");
9283
+ let totalCases = 0;
9284
+ let totalFailed = 0;
9285
+ let withoutTests = 0;
9286
+ for (const report of reports) {
9287
+ if (report.error) {
9288
+ out(`${symbol.fail} ${style.bold(report.policy.name)} ${style.grey(report.error)}`);
9289
+ totalFailed += 1;
9290
+ continue;
9291
+ }
9292
+ const result = report.result;
9293
+ if (!result || caseCount(result) === 0) {
9294
+ withoutTests += 1;
9295
+ out(
9296
+ `${symbol.warn} ${style.bold(report.policy.name)} ${style.amber("no test cases recorded")}`
9297
+ );
9298
+ continue;
9299
+ }
9300
+ totalCases += caseCount(result);
9301
+ totalFailed += result.failed;
9302
+ const icon = result.failed === 0 ? symbol.pass : symbol.fail;
9303
+ out(
9304
+ `${icon} ${style.bold(report.policy.name)} ${style.grey(`${result.passed}/${caseCount(result)} passing`)}`
9305
+ );
9306
+ for (const testCase of result.results.filter((entry) => !entry.passed)) {
9307
+ out(
9308
+ ` ${symbol.fail} ${testCase.name}: expected ${decisionStyle(testCase.expected)}, got ${decisionStyle(testCase.actual)}`
9309
+ );
9310
+ if (testCase.reason) out(` ${style.grey(testCase.reason)}`);
9311
+ }
9312
+ }
9047
9313
  out();
9048
- out(style.bold("GETTING STARTED"));
9049
- out(
9050
- ` ${style.grey("1.")} veefore ${style.grey("sign in, scan, set up and protect this project")}`
9051
- );
9052
9314
  out(
9053
- ` ${style.grey("2.")} veefore dev ${style.grey("watch what your agent actually does")}`
9315
+ totalFailed === 0 ? ` ${symbol.pass} ${style.green(`All ${totalCases} test case(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${totalFailed} failure(s) across ${totalCases} test case(s).`)}`
9054
9316
  );
9055
- out(` ${style.grey("3.")} veefore doctor ${style.grey("check the security posture")}`);
9056
- out(` ${style.grey("4.")} veefore deploy ${style.grey("turn enforcement on")}`);
9057
- out();
9058
- out(style.grey(" Run `veefore <command> --help` for detail on any command."));
9317
+ if (withoutTests > 0) {
9318
+ out(
9319
+ ` ${symbol.warn} ${style.amber(`${withoutTests} policy/policies have no test cases.`)} Add cases so activation can be proven safe.`
9320
+ );
9321
+ }
9059
9322
  out();
9060
9323
  }
9061
- function printCommandHelp(name) {
9062
- const command = COMMANDS.find((entry) => entry.name === name);
9063
- if (!command) return false;
9064
- out();
9065
- out(`${style.bold(`veefore ${command.name}`)} \u2014 ${command.summary}`);
9066
- out();
9067
- out(style.bold("USAGE"));
9068
- out(` ${command.usage}`);
9069
- out();
9070
- out(style.bold("DESCRIPTION"));
9071
- for (const line of wrap2(command.description, 76)) out(` ${line}`);
9072
- if (command.options && command.options.length > 0) {
9073
- out();
9074
- out(style.bold("OPTIONS"));
9075
- renderOptions(command.options);
9324
+ async function deployPolicy(args, flags) {
9325
+ const context = await resolveContext(flags);
9326
+ const policyId = positional(args);
9327
+ if (!policyId) {
9328
+ throw new CliError(
9329
+ "A policy id is required.",
9330
+ 'Usage: veefore policies deploy <policyId> [--version <n>] [--reason "\u2026"]'
9331
+ );
9076
9332
  }
9077
- if (command.examples && command.examples.length > 0) {
9078
- out();
9079
- out(style.bold("EXAMPLES"));
9080
- for (const example of command.examples) out(` ${style.grey("$")} ${example}`);
9333
+ const policy = await context.api.get(`/v1/policies/${policyId}`);
9334
+ const versions = await context.api.get(`/v1/policies/${policyId}/versions`);
9335
+ const requested = valueFor(args, "--version");
9336
+ const target = requested ? versions.find((version) => String(version.versionNumber) === requested) : versions.find((version) => version.status === "DRAFT") ?? versions.find((version) => version.status === "TESTED");
9337
+ if (!target) {
9338
+ throw new CliError(
9339
+ requested ? `Version ${requested} was not found for this policy.` : "There is no draft version to deploy.",
9340
+ "Edit the policy in the console to create a new draft, then deploy it."
9341
+ );
9081
9342
  }
9082
- out();
9083
- return true;
9084
- }
9085
- function renderOptions(options) {
9086
- const width = Math.max(...options.map(([flag]) => flag.length));
9087
- for (const [flag, description] of options) {
9088
- out(` ${style.cyan(flag.padEnd(width))} ${description}`);
9343
+ if (target.versionNumber === policy.activeVersion?.versionNumber) {
9344
+ out(`${symbol.info} Version ${target.versionNumber} is already active. Nothing to do.`);
9345
+ return 0;
9089
9346
  }
9090
- }
9091
- function wrap2(text, width) {
9092
- const words2 = text.split(/\s+/);
9093
- const lines = [];
9094
- let current = "";
9095
- for (const word of words2) {
9096
- if (current.length + word.length + 1 > width) {
9097
- lines.push(current);
9098
- current = word;
9099
- } else {
9100
- current = current === "" ? word : `${current} ${word}`;
9347
+ heading(`Deploy ${policy.name} v${target.versionNumber}`);
9348
+ details([
9349
+ ["Environment", `${context.config.environmentName} (${context.config.environmentType})`],
9350
+ ["Policy", `${policy.name} (${policy.id})`],
9351
+ [
9352
+ "Currently active",
9353
+ policy.activeVersion === null ? style.grey("none \u2014 this will be the first active version") : `v${policy.activeVersion.versionNumber}`
9354
+ ],
9355
+ ["Deploying", `v${target.versionNumber}`],
9356
+ ["Bindings", String(policy.bindings.length)]
9357
+ ]);
9358
+ if (policy.bindings.length === 0) {
9359
+ out();
9360
+ out(
9361
+ ` ${symbol.warn} ${style.amber("This policy is not bound to any scope.")} Activating it will not change any decision until you bind it.`
9362
+ );
9363
+ }
9364
+ if (policy.activeVersion !== null) {
9365
+ const diff = await context.api.get(`/v1/policies/${policyId}/diff`, {
9366
+ from: policy.activeVersion.versionNumber,
9367
+ to: target.versionNumber
9368
+ }).catch(() => null);
9369
+ if (diff) {
9370
+ heading("Changes");
9371
+ for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
9372
+ for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
9373
+ for (const rule of diff.changed) {
9374
+ out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
9375
+ }
9376
+ if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
9377
+ out(style.grey(" No rule changes."));
9378
+ }
9379
+ if (diff.riskAssessment) {
9380
+ out();
9381
+ out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
9382
+ }
9101
9383
  }
9102
9384
  }
9103
- if (current !== "") lines.push(current);
9104
- return lines;
9105
- }
9106
-
9107
- // src/home.ts
9108
- async function homeCommand(args, flags) {
9109
- const source = welcomeSource(flags);
9110
- let first = true;
9111
- for (; ; ) {
9112
- if (readProjectConfig()) return initCommand(args, flags);
9113
- await beginScreen(source, { animate: first });
9114
- first = false;
9115
- out();
9116
- out(style.bold("Let's get started."));
9385
+ heading("Tests");
9386
+ const testSpinner = spinner("Running recorded test cases");
9387
+ let testResult = null;
9388
+ try {
9389
+ testResult = await context.api.post(`/v1/policies/${policyId}/tests/run`, {
9390
+ version_id: target.id
9391
+ });
9392
+ testSpinner.succeed(`${testResult.passed}/${caseCount(testResult)} test case(s) passing`);
9393
+ } catch (error) {
9394
+ testSpinner.fail("Could not run the test cases");
9395
+ throw error;
9396
+ }
9397
+ if (testResult.failed > 0) {
9117
9398
  out();
9118
- let action;
9119
- try {
9120
- action = await select("What would you like to do?", [
9121
- { label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
9122
- {
9123
- label: "Preview what setup would find",
9124
- hint: "veefore scan \xB7 changes nothing",
9125
- value: "scan"
9126
- },
9127
- { label: "Sign in", hint: "veefore login", value: "login" },
9128
- { label: "Show every command", hint: "veefore help", value: "help" },
9129
- { label: "Exit", hint: "esc", value: "exit" }
9130
- ]);
9131
- } catch (error) {
9132
- if (error instanceof MenuCancelled) return 0;
9133
- throw error;
9399
+ for (const failure of testResult.results.filter((entry) => !entry.passed)) {
9400
+ out(
9401
+ ` ${symbol.fail} ${failure.name}: expected ${decisionStyle(failure.expected)}, got ${decisionStyle(failure.actual)}`
9402
+ );
9134
9403
  }
9135
- if (action === "exit") return 0;
9136
- if (action === "setup") {
9137
- await runOnScreen(source, flags, () => initCommand(args, flags), {
9138
- // Setup done: "Continue", and the next pass opens the project home
9139
- // with the panel filled in. Otherwise "Back to menu", to read why and
9140
- // choose again.
9141
- label: () => readProjectConfig() ? "Continue" : "Back to menu"
9142
- });
9143
- continue;
9404
+ out();
9405
+ throw new CliError(
9406
+ `${testResult.failed} test case(s) fail. This version will not be deployed.`,
9407
+ "Fix the rules or the expectations, then deploy again. Use --force only if you accept the risk and the server permits it."
9408
+ );
9409
+ }
9410
+ if (caseCount(testResult) === 0) {
9411
+ out();
9412
+ out(
9413
+ ` ${symbol.warn} ${style.amber("This policy has no test cases.")} There is no evidence the new rules behave as intended.`
9414
+ );
9415
+ const proceed = await confirm("Deploy anyway?", { yes: flags.yes });
9416
+ if (!proceed) {
9417
+ out(style.grey("Cancelled. Add test cases first."));
9418
+ return 1;
9144
9419
  }
9145
- const code = await runOnScreen(source, flags, async () => {
9146
- switch (action) {
9147
- case "scan":
9148
- return scanCommand([], flags);
9149
- case "login":
9150
- await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
9151
- return 0;
9152
- case "help":
9153
- printRootHelp(source().version);
9154
- return 0;
9155
- }
9156
- return 0;
9157
- });
9158
- if (!isInteractive()) return code;
9159
9420
  }
9160
- }
9161
- async function initWithWelcome(args, flags) {
9162
- await beginScreen(welcomeSource(flags), { animate: true });
9163
- return initCommand(args, flags);
9164
- }
9165
- async function runOnScreen(source, flags, run2, options = {}) {
9166
- await beginScreen(source, { compact: true });
9167
- let code = 0;
9421
+ const reason = valueFor(args, "--reason") ?? await ask("Reason for this change (recorded in the audit trail, min 10 characters):");
9422
+ if (reason.trim().length < 10) {
9423
+ throw new CliError("A reason of at least 10 characters is required.");
9424
+ }
9425
+ if (context.config.environmentType === "PRODUCTION") {
9426
+ out();
9427
+ out(` ${symbol.warn} ${style.amber("This affects a PRODUCTION environment.")}`);
9428
+ }
9429
+ const confirmed = await confirm(`Activate v${target.versionNumber}?`, { yes: flags.yes });
9430
+ if (!confirmed) {
9431
+ out(style.grey("Cancelled. No change was made."));
9432
+ return 1;
9433
+ }
9434
+ const deploySpinner = spinner(`Activating v${target.versionNumber}`);
9168
9435
  try {
9169
- code = await run2();
9436
+ await withStepUp(
9437
+ context.api,
9438
+ () => context.api.post(`/v1/policies/${policyId}/activate`, {
9439
+ version_id: target.id,
9440
+ reason
9441
+ })
9442
+ );
9443
+ deploySpinner.succeed(`${policy.name} v${target.versionNumber} is ACTIVE`);
9170
9444
  } catch (error) {
9171
- code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
9445
+ deploySpinner.fail("Activation failed");
9446
+ throw error;
9172
9447
  }
9173
- const label = options.label ?? "Back";
9174
- await pressBack(typeof label === "function" ? label() : label);
9175
- return code;
9448
+ out();
9449
+ out(` ${symbol.pass} The previous version is marked SUPERSEDED and can be rolled back.`);
9450
+ out(` ${symbol.pass} The change and its diff are recorded in the audit trail.`);
9451
+ out(style.grey(" Roll back with: veefore policies deploy <policyId> --version <previous>"));
9452
+ out();
9453
+ return 0;
9454
+ }
9455
+ async function diffPolicy(args, flags) {
9456
+ const context = await resolveContext(flags);
9457
+ const policyId = positional(args);
9458
+ if (!policyId) {
9459
+ throw new CliError(
9460
+ "A policy id is required.",
9461
+ "Usage: veefore policies diff <policyId> --from <n> --to <n>"
9462
+ );
9463
+ }
9464
+ const from = valueFor(args, "--from");
9465
+ const to = valueFor(args, "--to");
9466
+ const diff = await context.api.get(`/v1/policies/${policyId}/diff`, { from, to });
9467
+ if (flags.json) {
9468
+ printJson(diff);
9469
+ return 0;
9470
+ }
9471
+ heading(`Policy diff v${diff.from} \u2192 v${diff.to}`);
9472
+ for (const rule of diff.added) out(` ${style.green("+")} ${rule.name}`);
9473
+ for (const rule of diff.removed) out(` ${style.red("-")} ${rule.name}`);
9474
+ for (const rule of diff.changed) {
9475
+ out(` ${style.amber("~")} ${rule.name} ${style.grey(rule.summary)}`);
9476
+ }
9477
+ if (diff.added.length + diff.removed.length + diff.changed.length === 0) {
9478
+ out(style.grey(" No differences."));
9479
+ }
9480
+ if (diff.riskAssessment) {
9481
+ out();
9482
+ out(` ${style.bold("Risk assessment")}: ${diff.riskAssessment}`);
9483
+ }
9484
+ out();
9485
+ return 0;
9176
9486
  }
9177
9487
 
9178
9488
  // src/index.ts