modelos-secure 0.3.3 → 0.3.4

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.
@@ -49,11 +49,26 @@ function step(name, detail, state = "done") {
49
49
  const right = state === "done" ? "done" : "";
50
50
  say(` ${bold(name.padEnd(STEP_WIDTH))}${detail}${right ? ` ${grey(right)}` : ""}`);
51
51
  }
52
+ var RECOMMENDED = " (recommended)";
53
+ /**
54
+ * The choices as the lines they take on screen, one each and none wider than the window. The
55
+ * picker redraws by moving up exactly this many lines, so a line that wrapped would leave the one
56
+ * above it standing: seen on an 80-column window, where "Yes" appeared twice.
57
+ */
58
+ function choiceLines(choices, index, columns) {
59
+ return choices.map((choice, i) => {
60
+ const tail = choice.recommended ? RECOMMENDED : "";
61
+ const room = Math.max(8, columns - 1 - 4 - tail.length);
62
+ const label = choice.label.length > room ? `${choice.label.slice(0, room - 1)}${unicode ? "…" : "~"}` : choice.label;
63
+ return ` ${i === index ? blue(pointer) : " "} ${label}${tail ? grey(tail) : ""}`;
64
+ });
65
+ }
52
66
  /** One question, one reason, a default. Arrows, Enter and the first letter of each choice. */
53
67
  async function ask(question, why, choices) {
54
68
  const start = Math.max(0, choices.findIndex((c) => c.recommended));
55
69
  say();
56
70
  say(` ${question}`);
71
+ if (why) say(` ${grey(`Why we ask: ${why}`)}`);
57
72
  say();
58
73
  if (!interactive()) {
59
74
  const chosen = choices[start];
@@ -63,14 +78,10 @@ async function ask(question, why, choices) {
63
78
  let index = start;
64
79
  const render = (first) => {
65
80
  if (!first) process.stdout.write(`\u001b[${choices.length.toString()}A`);
66
- for (const [i, choice] of choices.entries()) {
67
- const mark = i === index ? blue(pointer) : " ";
68
- process.stdout.write(`\u001b[2K ${mark} ${choice.label}${choice.recommended ? grey(" (recommended)") : ""}\n`);
69
- }
81
+ const columns = process.stdout.columns || 80;
82
+ for (const line of choiceLines(choices, index, columns)) process.stdout.write(`\u001b[2K${line}\n`);
70
83
  };
71
84
  render(true);
72
- if (why) say(`\n ${grey(`Why we ask: ${why}`)}`);
73
- if (why) process.stdout.write(`\u001b[2A`);
74
85
  return new Promise((resolve, reject) => {
75
86
  emitKeypressEvents(process.stdin);
76
87
  process.stdin.setRawMode(true);
@@ -79,7 +90,6 @@ async function ask(question, why, choices) {
79
90
  process.stdin.off("keypress", onKey);
80
91
  process.stdin.setRawMode(false);
81
92
  process.stdin.pause();
82
- if (why) process.stdout.write(`\u001b[2B\n`);
83
93
  };
84
94
  const onKey = (text, key) => {
85
95
  if (key.ctrl && key.name === "c") {
@@ -89,7 +99,7 @@ async function ask(question, why, choices) {
89
99
  }
90
100
  if (key.name === "up") index = (index - 1 + choices.length) % choices.length;
91
101
  else if (key.name === "down") index = (index + 1) % choices.length;
92
- else if (key.name === "return") {
102
+ else if (key.name === "return" || key.name === "enter") {
93
103
  done();
94
104
  resolve(choices[index].value);
95
105
  return;
@@ -120,7 +130,7 @@ async function askText(question, secret = false) {
120
130
  reject(new Interrupted());
121
131
  return;
122
132
  }
123
- if (key.name === "return") {
133
+ if (key.name === "return" || key.name === "enter") {
124
134
  process.stdin.off("keypress", onKey);
125
135
  process.stdin.setRawMode(false);
126
136
  process.stdin.pause();
@@ -0,0 +1 @@
1
+ export { t as loginCommand } from "./login-DeGa63BQ.js";
package/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d as beforeStopping, f as blue, g as interactive, h as grey, i as IapRefusal, l as ask, m as finishBeforeStopping, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as red, x as step, y as say } from "./assets/login-DeAJGhiN.js";
2
+ import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d as beforeStopping, f as blue, g as interactive, h as grey, i as IapRefusal, l as ask, m as finishBeforeStopping, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as red, x as step, y as say } from "./assets/login-DeGa63BQ.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -23,7 +23,7 @@ if (existsSync(resolve(here, "wasm"))) {
23
23
  }
24
24
  // package.json
25
25
  var name = "modelos-secure";
26
- var version$1 = "0.3.3";
26
+ var version$1 = "0.3.4";
27
27
  var type = "module";
28
28
  var bin = { "modelos-secure": "dist/cli.js" };
29
29
  var exports$1 = { "./hook": "./dist/hook/hook.mjs" };
@@ -424,8 +424,10 @@ async function walk$1(root) {
424
424
  };
425
425
  }
426
426
  /**
427
- * The folders whose manifests describe this one: itself, then each folder above it up to the
428
- * repository's root, for an app kept in a folder of a larger repository.
427
+ * The folders whose manifests describe this one: itself, each folder above it up to the
428
+ * repository's root, for an app kept in a folder of a larger repository, and the folders below
429
+ * it that hold a manifest, for a repository whose app sits in `backend/` or `sidecar/`. Without
430
+ * the ones below, such a repository showed no library, no framework and no route. ADR 0094.
429
431
  */
430
432
  async function manifestFolders(root) {
431
433
  const folders = [root];
@@ -437,6 +439,23 @@ async function manifestFolders(root) {
437
439
  folders.push(up);
438
440
  at = up;
439
441
  }
442
+ const below = async (dir, depth) => {
443
+ if (depth > 3) return;
444
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
445
+ if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith(".")) continue;
446
+ const full = join(dir, entry.name);
447
+ for (const manifest of [
448
+ "package.json",
449
+ "pyproject.toml",
450
+ "Pipfile"
451
+ ]) if (await access(join(full, manifest)).then(() => true, () => false)) {
452
+ folders.push(full);
453
+ break;
454
+ }
455
+ await below(full, depth + 1);
456
+ }
457
+ };
458
+ await below(root, 1);
440
459
  return folders;
441
460
  }
442
461
  /** A TOML file's sections by name, each with its body. Enough for the manifests read here. */
@@ -542,15 +561,25 @@ async function look(root) {
542
561
  command: web,
543
562
  from: "Procfile"
544
563
  });
545
- const composed = (await read("docker-compose.yml"))?.match(/^\s*command:\s*(.+)$/m)?.[1];
546
- if (composed) startCommands.push({
547
- command: composed.replace(/^["']|["']$/g, ""),
548
- from: "docker-compose.yml"
549
- });
550
- const made = (await read("Makefile"))?.match(/^(?:run|start|serve):[^\n]*\n\t(.+)$/m)?.[1];
551
- if (made) startCommands.push({
552
- command: made,
553
- from: "Makefile"
564
+ for (const name of [
565
+ "docker-compose.yml",
566
+ "docker-compose.yaml",
567
+ "compose.yml",
568
+ "compose.yaml"
569
+ ]) {
570
+ const composed = (await read(name))?.match(/^\s*command:\s*(.+)$/m)?.[1];
571
+ if (composed) {
572
+ startCommands.push({
573
+ command: composed.replace(/^["']|["']$/g, ""),
574
+ from: name
575
+ });
576
+ break;
577
+ }
578
+ }
579
+ const target = (await read("Makefile"))?.match(/^(run|start|serve|dev|dev-api|api|server):[^\n]*\n\t(.+)$/m);
580
+ if (target) startCommands.push({
581
+ command: `make ${target[1]}`,
582
+ from: `Makefile, whose ${target[1]} target runs ${target[2].trim().split(/\s+/).slice(0, 3).join(" ")}`
554
583
  });
555
584
  const settingsFiles = [];
556
585
  const settingNames = /* @__PURE__ */ new Set();
@@ -5655,29 +5684,94 @@ function dockerCommand(dockerfile) {
5655
5684
  const full = [entrypoint, last("CMD")].filter((body) => body !== void 0).map((body) => listForm(body)?.join(" ") ?? body).join(" ");
5656
5685
  return full && !full.startsWith("-") ? full : null;
5657
5686
  }
5687
+ /**
5688
+ * Scripts beside the app, not the app: a main block in scripts/, tests/ or eval/, or in a file
5689
+ * named for a check, a benchmark or a one-off job, starts nothing a review could talk to.
5690
+ */
5691
+ var ASIDE_FOLDERS = /(^|\/)(scripts?|tests?|evals?|benchmarks?|training|tools|migrations|examples?|notebooks?|docs?|bin|hooks|experiments?|utils?)(\/|$)/;
5692
+ var ASIDE_NAMES = /(^|\/)(benchmark|check|backfill|stress|verify|test|dump|audit|import|export|migrate|seed|setup|install|build|deploy|smoke|demo|debug|fixture|generate|convert|download|upload|sync|clean|classify|investigate|analy[sz]e|report)([_-][^/]*)?\.py$|(^|\/)conftest\.py$|_(test|tests|audit|smoke|check|benchmark|demo|gates?)\.py$/;
5693
+ function isAside(path) {
5694
+ return ASIDE_FOLDERS.test(path.replace(/\/[^/]+$/, "")) || ASIDE_NAMES.test(path);
5695
+ }
5696
+ /**
5697
+ * The folder a Python file's absolute imports resolve from, which is where the app runs: for
5698
+ * `from app.db import x` in backend/app/main.py, that is backend. Empty for the repository's
5699
+ * root, or when nothing it imports is here.
5700
+ */
5701
+ function importRoot(path, imports, files) {
5702
+ const parts = path.split("/").slice(0, -1);
5703
+ const own = imports.filter((name) => !name.startsWith("."));
5704
+ for (let i = parts.length; i >= 0; i--) {
5705
+ const dir = parts.slice(0, i).join("/");
5706
+ const under = (rest) => dir ? `${dir}/${rest}` : rest;
5707
+ if (own.some((name) => {
5708
+ const head = name.split(".")[0];
5709
+ return files.has(under(`${head}.py`)) || files.has(under(`${head}/__init__.py`));
5710
+ })) return dir;
5711
+ }
5712
+ return "";
5713
+ }
5714
+ /** What is offered first: the framework's own way, then the build, the README, the scripts. */
5715
+ var RANK = [
5716
+ [/^the (FastAPI|Starlette|Flask) (app|factory) /, 0],
5717
+ [/^the Dockerfile's command$/, 1],
5718
+ [/^README/i, 2],
5719
+ [/^(package\.json|pyproject\.toml|Procfile|docker-compose|compose\.|Makefile)/, 3],
5720
+ [/^the main block at ([^/]+\/)*(main|app|server|api|cli|run|serve|wsgi|asgi)\.py:/, 4]
5721
+ ];
5722
+ function rankStart(candidates) {
5723
+ const rank = (c) => RANK.find(([pattern]) => pattern.test(c.from))?.[1] ?? 5;
5724
+ return candidates.map((c, i) => ({
5725
+ c,
5726
+ i
5727
+ })).sort((a, b) => rank(a.c) - rank(b.c) || a.i - b.i).map(({ c }) => c);
5728
+ }
5658
5729
  async function startCandidates(look, project) {
5659
5730
  const files = new Set(project.files.map((f) => f.path));
5660
5731
  const read = (file) => readFile(join(look.root, file), "utf8").catch(() => null);
5661
5732
  const found = [];
5662
5733
  for (const file of project.files) {
5663
5734
  if (file.language !== "python") continue;
5664
- const module = file.path.replace(/\.py$/, "").replace(/\//g, ".");
5735
+ const symbols = symbolsOf(file);
5736
+ const appDir = importRoot(file.path, symbols.imports, files);
5737
+ const module = file.path.slice(appDir ? appDir.length + 1 : 0).replace(/\.py$/, "").replace(/\//g, ".");
5738
+ const dirOption = appDir ? ` --app-dir ${appDir}` : "";
5665
5739
  const standsAlone = module.includes(".") || !/^from \.+\w*\s+import/m.test(file.text);
5666
- for (const d of symbolsOf(file).declarations) {
5740
+ const made = /* @__PURE__ */ new Set();
5741
+ for (const d of symbols.declarations) {
5667
5742
  const callee = d.callee.split(".").at(-1) ?? "";
5668
5743
  if (!d.name) continue;
5669
5744
  if (!standsAlone) continue;
5670
- if (/^(FastAPI|Starlette)$/.test(callee)) found.push({
5671
- command: `uvicorn ${module}:${d.name}`,
5672
- from: `the ${callee} app ${d.name} at ${file.path}:${d.line.toString()}`
5673
- });
5674
- else if (callee === "Flask") found.push({
5675
- command: `flask --app ${module}:${d.name} run`,
5676
- from: `the Flask app ${d.name} at ${file.path}:${d.line.toString()}`
5745
+ if (/^(FastAPI|Starlette)$/.test(callee)) {
5746
+ made.add(callee);
5747
+ found.push({
5748
+ command: `uvicorn ${module}:${d.name}${dirOption}`,
5749
+ from: `the ${callee} app ${d.name} at ${file.path}:${d.line.toString()}`
5750
+ });
5751
+ } else if (callee === "Flask") {
5752
+ made.add(callee);
5753
+ found.push({
5754
+ command: `flask --app ${module}:${d.name} run${dirOption}`,
5755
+ from: `the Flask app ${d.name} at ${file.path}:${d.line.toString()}`
5756
+ });
5757
+ }
5758
+ }
5759
+ const lines = file.text.split("\n");
5760
+ if (standsAlone) for (const s of symbols.symbols) {
5761
+ if (s.kind !== "function") continue;
5762
+ const body = lines.slice(s.line - 1, s.end).join("\n");
5763
+ const callee = /\b(FastAPI|Starlette|Flask)\(/.exec(body)?.[1];
5764
+ if (!callee || made.has(callee) || !/^\s+return\s/m.test(body)) continue;
5765
+ found.push(callee === "Flask" ? {
5766
+ command: `flask --app ${module}:${s.name} run${dirOption}`,
5767
+ from: `the Flask factory ${s.name} at ${file.path}:${s.line.toString()}`
5768
+ } : {
5769
+ command: `uvicorn ${module}:${s.name} --factory${dirOption}`,
5770
+ from: `the ${callee} factory ${s.name} at ${file.path}:${s.line.toString()}`
5677
5771
  });
5678
5772
  }
5679
- const main = file.text.split("\n").findIndex((l) => /^if __name__\s*==\s*["']__main__["']/.test(l));
5680
- if (main >= 0) found.push({
5773
+ const main = lines.findIndex((l) => /^if __name__\s*==\s*["']__main__["']/.test(l));
5774
+ if (main >= 0 && !isAside(file.path)) found.push({
5681
5775
  command: `python ${file.path}`,
5682
5776
  from: `the main block at ${file.path}:${(main + 1).toString()}`
5683
5777
  });
@@ -5707,8 +5801,10 @@ async function startCandidates(look, project) {
5707
5801
  return found.filter((c) => {
5708
5802
  if (seen.has(c.command)) return false;
5709
5803
  seen.add(c.command);
5710
- const file = c.command.split(/\s+/).find((w) => /\.(py|m?js|ts)$/.test(w));
5711
- return !file || files.has(file.replace(/^\.\//, ""));
5804
+ const file = c.command.split(/\s+/).find((w) => /\.(py|m?js|ts)$/.test(w))?.replace(/^\.\//, "");
5805
+ if (!file) return true;
5806
+ if (/^README/i.test(c.from) && isAside(file)) return false;
5807
+ return files.has(file);
5712
5808
  });
5713
5809
  }
5714
5810
  // ../../packages/scan/src/outline.ts
@@ -5749,7 +5845,8 @@ var where$1 = (f) => ({
5749
5845
  file: f.file,
5750
5846
  line: f.line
5751
5847
  });
5752
- async function buildOutline(look, facts, project) {
5848
+ async function buildOutline(look, facts, project, options = {}) {
5849
+ const budget = options.budget ?? BUDGET;
5753
5850
  const outlines = project.files.map(symbolsOf);
5754
5851
  const known = await localValues(look.root, look.settingsFiles);
5755
5852
  const ranked = [...outlines].sort((a, b) => priority(b) - priority(a));
@@ -5758,7 +5855,7 @@ async function buildOutline(look, facts, project) {
5758
5855
  let size = 0;
5759
5856
  for (const file of ranked) {
5760
5857
  const bytes = JSON.stringify(file).length;
5761
- if (size + bytes > BUDGET) omitted.push(file.file);
5858
+ if (size + bytes > budget) omitted.push(file.file);
5762
5859
  else {
5763
5860
  code.push(file);
5764
5861
  size += bytes;
@@ -6162,7 +6259,7 @@ function draftConfig(input) {
6162
6259
  trigger: previous?.ways_in.find((w) => w.kind === "conversation")?.trigger ?? "Hello, can you help me?"
6163
6260
  });
6164
6261
  for (const fact of wayFacts) {
6165
- const before = previous?.ways_in.find((w) => w.name === fact.name || w.function === fact.function);
6262
+ const before = previous?.ways_in.find((w) => w.name === fact.name || !!fact.function && w.function === fact.function);
6166
6263
  const understood = understanding?.waysIn.find((w) => w.name === fact.name);
6167
6264
  if (before) {
6168
6265
  waysIn.push({
@@ -6395,11 +6492,15 @@ function resolveImport(from, name, known) {
6395
6492
  base = up.slice(0, Math.max(0, up.length - (dots - 1))).join("/");
6396
6493
  } else base = "";
6397
6494
  const path = rest ? base ? `${base}/${rest}` : rest : base;
6398
- return first((dots > 0 ? [""] : [
6495
+ const parts = dir.split("/").filter(Boolean);
6496
+ const ancestors = parts.map((_, i) => `${parts.slice(0, parts.length - i).join("/")}/`);
6497
+ return first((dots > 0 ? [""] : [.../* @__PURE__ */ new Set([
6498
+ ...ancestors,
6399
6499
  "",
6400
6500
  "src/",
6401
- "app/"
6402
- ]).flatMap((root) => [`${root}${path}.py`, `${root}${path}/__init__.py`]));
6501
+ "app/",
6502
+ ...ancestors.map((a) => `${a}src/`)
6503
+ ])]).flatMap((root) => [`${root}${path}.py`, `${root}${path}/__init__.py`]));
6403
6504
  }
6404
6505
  function normalise(path) {
6405
6506
  const out = [];
@@ -6974,7 +7075,7 @@ async function answerQuestion(input) {
6974
7075
  async function scanFolder(root) {
6975
7076
  const found = await look(root);
6976
7077
  const { facts, adapters, project } = await readFacts(found);
6977
- found.startCommands.push(...await startCandidates(found, project));
7078
+ found.startCommands = rankStart([...found.startCommands, ...await startCandidates(found, project)]);
6978
7079
  return {
6979
7080
  look: found,
6980
7081
  facts,
@@ -20763,6 +20864,23 @@ async function scriptsIn(dir) {
20763
20864
  return {};
20764
20865
  }
20765
20866
  }
20867
+ var SAME_INTERPRETER = {
20868
+ python: "python3",
20869
+ python3: "python",
20870
+ pip: "pip3",
20871
+ pip3: "pip"
20872
+ };
20873
+ /**
20874
+ * The same command under the interpreter's name this machine has: `python main.py` where only
20875
+ * `python3` is installed, as on most Linux and Mac machines, or the reverse on Windows. A command
20876
+ * whose first word is here already is returned as it is.
20877
+ */
20878
+ async function interpreterHere(command) {
20879
+ const [first = "", ...rest] = command.trim().split(/\s+/);
20880
+ const other = SAME_INTERPRETER[first];
20881
+ if (!other || await onPath(first) || !await onPath(other)) return command;
20882
+ return [other, ...rest].join(" ");
20883
+ }
20766
20884
  /**
20767
20885
  * Why `command` cannot start the app in `dir`, in one sentence, or null when it can as far as can
20768
20886
  * be told without running it.
@@ -21355,7 +21473,7 @@ function toFix(ctx, fix) {
21355
21473
  }
21356
21474
  };
21357
21475
  case "run-command": return {
21358
- label: `${fix.label}: ${fix.command}`,
21476
+ label: fix.label.includes(fix.command) ? fix.label : `${fix.label}: ${fix.command}`,
21359
21477
  needsPerson: true,
21360
21478
  apply: async () => {
21361
21479
  if (!interactive()) return;
@@ -21400,7 +21518,7 @@ var DONT_KNOW = /^(i\s+)?(don'?t|do not)\s+know|^not\s+sure|^no\s+idea|^\?$/i;
21400
21518
  */
21401
21519
  async function typeTheCommand(ctx) {
21402
21520
  for (let tries = 0; tries < 3; tries++) {
21403
- const typed = (await askText("How do you start the app?")).trim();
21521
+ const typed = await interpreterHere((await askText("How do you start the app?")).trim());
21404
21522
  if (!typed || DONT_KNOW.test(typed)) return;
21405
21523
  const why = await whyNotRunnable(ctx.dir, typed);
21406
21524
  if (!why) {
@@ -21417,13 +21535,14 @@ async function typeTheCommand(ctx) {
21417
21535
  async function candidateFixes(ctx) {
21418
21536
  const fixes = [];
21419
21537
  for (const c of ctx.startCommands) {
21420
- if (c.command === ctx.config.start?.command) continue;
21421
- if (await whyNotRunnable(ctx.dir, c.command)) continue;
21538
+ const command = await interpreterHere(c.command);
21539
+ if (command === ctx.config.start?.command) continue;
21540
+ if (await whyNotRunnable(ctx.dir, command)) continue;
21422
21541
  fixes.push({
21423
- label: `${c.command} (from ${c.from})`,
21542
+ label: `${command} (from ${c.from})`,
21424
21543
  apply: () => void (ctx.config.start = {
21425
21544
  ...ctx.config.start,
21426
- command: c.command
21545
+ command
21427
21546
  })
21428
21547
  });
21429
21548
  }
@@ -21439,7 +21558,11 @@ var notKnown = {
21439
21558
  /** Starts the pool, or fails with what happened and how to fix it. */
21440
21559
  async function startApp(ctx, instances) {
21441
21560
  const { config } = ctx;
21442
- const command = config.start?.command;
21561
+ const command = config.start?.command ? await interpreterHere(config.start.command) : void 0;
21562
+ if (command && config.start && command !== config.start.command) config.start = {
21563
+ ...config.start,
21564
+ command
21565
+ };
21443
21566
  const typeIt = {
21444
21567
  label: "Type the command",
21445
21568
  apply: () => typeTheCommand(ctx)
@@ -22208,7 +22331,7 @@ async function scanCommand(dir, options) {
22208
22331
  label: "Enter a token now",
22209
22332
  recommended: true,
22210
22333
  apply: async () => {
22211
- const { loginCommand } = await import("./assets/login-CvPyhFJ7.js");
22334
+ const { loginCommand } = await import("./assets/login-Dz4o-1cM.js");
22212
22335
  await loginCommand();
22213
22336
  const fresh = await readConnection();
22214
22337
  api = fresh ? apiClient(fresh) : null;
@@ -22326,7 +22449,7 @@ async function scanCommand(dir, options) {
22326
22449
  config = treatToolAs(config, tool.name, state.answers[id]);
22327
22450
  }
22328
22451
  if (drafted.config.decided_for_you?.length) say(` ${grey(`I hit the limit of 7 questions, so I made the safer choice on ${plural(drafted.config.decided_for_you.length, "tool")} and recorded it for review in modelos-secure.yaml.`)}`);
22329
- const candidates = [...new Map([...reading?.verified.start ?? [], ...result.look.startCommands].map((c) => [c.command, c])).values()];
22452
+ const candidates = [...new Map([...reading?.verified.start ?? [], ...result.look.startCommands].map((c) => [c.command, c])).values()].slice(0, 6);
22330
22453
  if (!previous?.start?.command && candidates.length > 1 && !options.ci) {
22331
22454
  const command = (state.answers.start ?? (options.yes ? candidates[0].command : await ask("How should the scan start the app?", "It starts the app on this machine, with the hooks attached, to check the connection.", [...candidates.map((c, i) => ({
22332
22455
  label: `${c.command} (${c.from})`,
@@ -22378,12 +22501,13 @@ async function scanCommand(dir, options) {
22378
22501
  const paths = (config.ways_in.length || 1) * (config.rules.length || 1);
22379
22502
  say(`\n ${bold(`${config.system} is connected.`)}\n`);
22380
22503
  say(` ${`${plural(config.ways_in.length, "way")} in`.padEnd(14)} ${config.ways_in.map((w) => w.name).join(", ")}`);
22381
- say(` ${plural(config.tools.length, "tool").padEnd(14)} ${plural(held(config).length, "tool")} held during reviews: ${held(config).join(", ")}`);
22504
+ const heldNow = held(config);
22505
+ say(` ${plural(config.tools.length, "tool").padEnd(14)} ${config.tools.length === 0 ? "none to hold" : heldNow.length === 0 ? "none held during reviews" : `${heldNow.length.toString()} held during reviews: ${heldNow.join(", ")}`}`);
22382
22506
  say(` ${plural(paths, "path").padEnd(14)} none tested yet\n`);
22383
22507
  const url = connection ? `${connection.url}/systems/${system.id}` : "";
22384
22508
  say(` Map: ${url}`);
22385
- if (options.noReview || options.ci) return;
22386
- if (options.yes ? false : await ask("Start a quick first review now? About 15 minutes, 1,200 attempts.", "It starts the app on this machine with the hooks attached, checks they hold what they should, then reviews it.", [{
22509
+ if (options.noReview || options.ci || options.yes || !interactive()) return;
22510
+ if (await ask("Start a quick first review now? About 15 minutes, 1,200 attempts.", "It starts the app on this machine with the hooks attached, checks they hold what they should, then reviews it.", [{
22387
22511
  label: "Yes, start it",
22388
22512
  value: true,
22389
22513
  recommended: true
@@ -22452,8 +22576,8 @@ async function sessionCommand(dir, run) {
22452
22576
  for (;;) {
22453
22577
  say();
22454
22578
  const line = await askText(">");
22455
- const word = line.toLowerCase();
22456
- if (!line || word === "quit" || word === "exit") return;
22579
+ const word = line.toLowerCase().replace(/\(\)$/, "");
22580
+ if (!line || word === "quit" || word === "exit" || word === "q") return;
22457
22581
  if (word === "help") {
22458
22582
  run.help();
22459
22583
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- export { t as loginCommand } from "./login-DeAJGhiN.js";