@liustack/modlens 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { spawn } from "child_process";
7
7
  import * as os from "os";
8
8
  import require$$0$1 from "node:assert";
9
9
  import require$$1 from "node:net";
10
- import require$$2$1 from "node:http";
10
+ import require$$2 from "node:http";
11
11
  import require$$0$2 from "node:stream";
12
12
  import require$$5 from "node:querystring";
13
13
  import require$$0 from "node:events";
@@ -18,44 +18,20 @@ import require$$0$4 from "node:buffer";
18
18
  import require$$0$5 from "node:zlib";
19
19
  import require$$5$1 from "node:perf_hooks";
20
20
  import require$$8 from "node:util/types";
21
- import require$$2$2 from "node:worker_threads";
22
- import require$$2$3 from "node:crypto";
21
+ import require$$2$1 from "node:worker_threads";
22
+ import require$$2$2 from "node:crypto";
23
23
  import require$$11 from "node:stream/web";
24
24
  import require$$0$6 from "node:url";
25
25
  import require$$1$1 from "node:async_hooks";
26
26
  import require$$1$2 from "node:console";
27
27
  import require$$0$7 from "node:fs/promises";
28
28
  import require$$1$3 from "node:path";
29
- import require$$2$4 from "node:timers";
29
+ import require$$2$3 from "node:timers";
30
30
  import require$$1$4 from "node:dns";
31
31
  import * as dns$1 from "dns/promises";
32
32
  import { isIP } from "net";
33
33
  import { createRequire } from "module";
34
34
  import * as crypto from "crypto";
35
- function getAugmentedNamespace(n) {
36
- if (Object.prototype.hasOwnProperty.call(n, "__esModule")) return n;
37
- var f = n.default;
38
- if (typeof f == "function") {
39
- var a = function a2() {
40
- if (this instanceof a2) {
41
- return Reflect.construct(f, arguments, this.constructor);
42
- }
43
- return f.apply(this, arguments);
44
- };
45
- a.prototype = f.prototype;
46
- } else a = {};
47
- Object.defineProperty(a, "__esModule", { value: true });
48
- Object.keys(n).forEach(function(k) {
49
- var d = Object.getOwnPropertyDescriptor(n, k);
50
- Object.defineProperty(a, k, d.get ? d : {
51
- enumerable: true,
52
- get: function() {
53
- return n[k];
54
- }
55
- });
56
- });
57
- return a;
58
- }
59
35
  var undici = { exports: {} };
60
36
  var symbols;
61
37
  var hasRequiredSymbols;
@@ -1061,7 +1037,7 @@ function requireUtil$5() {
1061
1037
  hasRequiredUtil$5 = 1;
1062
1038
  const assert = require$$0$1;
1063
1039
  const { kDestroyed, kBodyUsed, kListeners, kBody } = requireSymbols();
1064
- const { IncomingMessage } = require$$2$1;
1040
+ const { IncomingMessage } = require$$2;
1065
1041
  const stream = require$$0$2;
1066
1042
  const net = require$$1;
1067
1043
  const { stringify } = require$$5;
@@ -4309,7 +4285,7 @@ function requireWebidl() {
4309
4285
  hasRequiredWebidl = 1;
4310
4286
  const assert = require$$0$1;
4311
4287
  const { types, inspect } = require$$3;
4312
- const { markAsUncloneable } = require$$2$2;
4288
+ const { markAsUncloneable } = require$$2$1;
4313
4289
  const UNDEFINED = 1;
4314
4290
  const BOOLEAN = 2;
4315
4291
  const STRING = 3;
@@ -5670,20 +5646,14 @@ function requireUtil$4() {
5670
5646
  return util$4;
5671
5647
  }
5672
5648
  var runtimeFeatures = {};
5673
- const __viteBrowserExternal = {};
5674
- const __viteBrowserExternal$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
5675
- __proto__: null,
5676
- default: __viteBrowserExternal
5677
- }, Symbol.toStringTag, { value: "Module" }));
5678
- const require$$2 = /* @__PURE__ */ getAugmentedNamespace(__viteBrowserExternal$1);
5679
5649
  var hasRequiredRuntimeFeatures;
5680
5650
  function requireRuntimeFeatures() {
5681
5651
  if (hasRequiredRuntimeFeatures) return runtimeFeatures;
5682
5652
  hasRequiredRuntimeFeatures = 1;
5683
5653
  const lazyLoaders = {
5684
5654
  __proto__: null,
5685
- "node:crypto": () => require$$2$3,
5686
- "node:sqlite": () => require$$2
5655
+ "node:crypto": () => require$$2$2,
5656
+ "node:sqlite": () => require("node:sqlite")
5687
5657
  };
5688
5658
  function detectRuntimeFeatureByNodeModule(moduleName) {
5689
5659
  try {
@@ -5760,7 +5730,7 @@ function requireFormdata() {
5760
5730
  const { webidl } = requireWebidl();
5761
5731
  const nodeUtil = require$$3;
5762
5732
  const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
5763
- const random = runtimeFeatures2.has("crypto") ? require$$2$3.randomInt : (max) => Math.floor(Math.random() * max);
5733
+ const random = runtimeFeatures2.has("crypto") ? require$$2$2.randomInt : (max) => Math.floor(Math.random() * max);
5764
5734
  class FormData {
5765
5735
  #state = [];
5766
5736
  #boundary = null;
@@ -9112,7 +9082,7 @@ function requireClient() {
9112
9082
  hasRequiredClient = 1;
9113
9083
  const assert = require$$0$1;
9114
9084
  const net = require$$1;
9115
- const http = require$$2$1;
9085
+ const http = require$$2;
9116
9086
  const util2 = requireUtil$5();
9117
9087
  const { ClientStats } = requireStats();
9118
9088
  const { channels } = requireDiagnostics();
@@ -13531,7 +13501,7 @@ function requireMockUtils() {
13531
13501
  kTotalDispatchCount
13532
13502
  } = requireMockSymbols();
13533
13503
  const { serializePathWithQuery, parseHeaders } = requireUtil$5();
13534
- const { STATUS_CODES } = require$$2$1;
13504
+ const { STATUS_CODES } = require$$2;
13535
13505
  const {
13536
13506
  types: {
13537
13507
  isPromise
@@ -14806,7 +14776,7 @@ function requireSnapshotUtils() {
14806
14776
  match: new Set(matchHeaders.map((header) => caseSensitive ? header : header.toLowerCase()))
14807
14777
  };
14808
14778
  }
14809
- const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$3 : null;
14779
+ const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$2 : null;
14810
14780
  const hashId = crypto2?.hash ? (value) => crypto2.hash("sha256", value, "base64url") : (value) => Buffer.from(value).toString("base64url");
14811
14781
  function isUndiciHeaders(headers2) {
14812
14782
  return Array.isArray(headers2) && (headers2.length & 1) === 0;
@@ -14884,7 +14854,7 @@ function requireSnapshotRecorder() {
14884
14854
  hasRequiredSnapshotRecorder = 1;
14885
14855
  const { writeFile, readFile, mkdir } = require$$0$7;
14886
14856
  const { dirname, resolve } = require$$1$3;
14887
- const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$4;
14857
+ const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$3;
14888
14858
  const { InvalidArgumentError, UndiciError } = requireErrors();
14889
14859
  const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = requireSnapshotUtils();
14890
14860
  function normalizeUrlForMatching(url, matchQuery, normalizeQuery) {
@@ -19535,7 +19505,7 @@ function requireSqliteCacheStore() {
19535
19505
  }
19536
19506
  }
19537
19507
  if (!DatabaseSync) {
19538
- DatabaseSync = require$$2.DatabaseSync;
19508
+ DatabaseSync = require("node:sqlite").DatabaseSync;
19539
19509
  }
19540
19510
  this.#db = new DatabaseSync(opts?.location ?? ":memory:");
19541
19511
  this.#db.exec(`
@@ -21500,7 +21470,7 @@ function requireSubresourceIntegrity() {
21500
21470
  const validSRIHashAlgorithmTokenSet = /* @__PURE__ */ new Map([["sha256", 0], ["sha384", 1], ["sha512", 2]]);
21501
21471
  let crypto2;
21502
21472
  if (runtimeFeatures2.has("crypto")) {
21503
- crypto2 = require$$2$3;
21473
+ crypto2 = require$$2$2;
21504
21474
  const cryptoHashes = crypto2.getHashes();
21505
21475
  if (cryptoHashes.length === 0) {
21506
21476
  validSRIHashAlgorithmTokenSet.clear();
@@ -21694,7 +21664,7 @@ function requireFetch() {
21694
21664
  const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requireDataUrl();
21695
21665
  const { getGlobalDispatcher } = requireGlobal();
21696
21666
  const { webidl } = requireWebidl();
21697
- const { STATUS_CODES } = require$$2$1;
21667
+ const { STATUS_CODES } = require$$2;
21698
21668
  const { bytesMatch } = requireSubresourceIntegrity();
21699
21669
  const { isomorphicEncode } = requireInfra();
21700
21670
  const GET_OR_HEAD = ["GET", "HEAD"];
@@ -24454,7 +24424,7 @@ function requireFrame() {
24454
24424
  const BUFFER_SIZE = 8 * 1024;
24455
24425
  let buffer = null;
24456
24426
  let bufIdx = BUFFER_SIZE;
24457
- const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$3.randomFillSync : null;
24427
+ const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$2.randomFillSync : null;
24458
24428
  function generateMask() {
24459
24429
  if (bufIdx === BUFFER_SIZE) {
24460
24430
  bufIdx = 0;
@@ -24559,7 +24529,7 @@ function requireConnection() {
24559
24529
  const { WebsocketFrameSend } = requireFrame();
24560
24530
  const assert = require$$0$1;
24561
24531
  const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
24562
- const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$3 : null;
24532
+ const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$2 : null;
24563
24533
  let warningEmitted = false;
24564
24534
  function establishWebSocketConnection(url, protocols, client2, handler, options) {
24565
24535
  const requestURL = url;
@@ -27537,13 +27507,56 @@ function schemaViolations(schema, value, path2) {
27537
27507
  }
27538
27508
  return [];
27539
27509
  }
27540
- function parseExtraBody(raw, origin) {
27541
- let parsed;
27510
+ function tryParseJson(text) {
27511
+ try {
27512
+ return JSON.parse(text);
27513
+ } catch {
27514
+ return null;
27515
+ }
27516
+ }
27517
+ function parseJsonOrExplain(raw, origin) {
27542
27518
  try {
27543
- parsed = JSON.parse(raw);
27519
+ return JSON.parse(raw);
27544
27520
  } catch (error) {
27545
27521
  throw new Error(`${origin} is not valid JSON: ${error.message}`);
27546
27522
  }
27523
+ }
27524
+ function parseJsonLoose(text) {
27525
+ const trimmed = text.trim();
27526
+ const direct = tryParseJson(trimmed);
27527
+ if (direct !== null) {
27528
+ return direct;
27529
+ }
27530
+ return parseBraceSlice(trimmed);
27531
+ }
27532
+ function extractJson(text) {
27533
+ const trimmed = text.trim();
27534
+ const direct = tryParseJson(trimmed);
27535
+ if (direct !== null) {
27536
+ return direct;
27537
+ }
27538
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
27539
+ if (fenced) {
27540
+ const parsed = tryParseJson(fenced[1].trim());
27541
+ if (parsed !== null) {
27542
+ return parsed;
27543
+ }
27544
+ }
27545
+ return parseBraceSlice(trimmed);
27546
+ }
27547
+ function parseBraceSlice(trimmed) {
27548
+ const first = trimmed.indexOf("{");
27549
+ const last = trimmed.lastIndexOf("}");
27550
+ if (first >= 0 && last > first) {
27551
+ return tryParseJson(trimmed.slice(first, last + 1));
27552
+ }
27553
+ return null;
27554
+ }
27555
+ function truncate(text, max = 300) {
27556
+ return text.length > max ? `${text.slice(0, max)}...` : text;
27557
+ }
27558
+ function parseExtraBody(raw, origin) {
27559
+ const parsed = parseJsonOrExplain(raw, origin);
27547
27560
  if (!isPlainObject(parsed)) {
27548
27561
  throw new Error(
27549
27562
  `${origin} must be a JSON object, for example {"thinking":{"type":"disabled"}}`
@@ -27585,47 +27598,6 @@ function hasPath(value, dottedPath) {
27585
27598
  function isPlainObject(value) {
27586
27599
  return typeof value === "object" && value !== null && !Array.isArray(value);
27587
27600
  }
27588
- function tryParseJson(text) {
27589
- try {
27590
- return JSON.parse(text);
27591
- } catch {
27592
- return null;
27593
- }
27594
- }
27595
- function parseJsonLoose(text) {
27596
- const trimmed = text.trim();
27597
- const direct = tryParseJson(trimmed);
27598
- if (direct !== null) {
27599
- return direct;
27600
- }
27601
- return parseBraceSlice(trimmed);
27602
- }
27603
- function extractJson(text) {
27604
- const trimmed = text.trim();
27605
- const direct = tryParseJson(trimmed);
27606
- if (direct !== null) {
27607
- return direct;
27608
- }
27609
- const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
27610
- if (fenced) {
27611
- const parsed = tryParseJson(fenced[1].trim());
27612
- if (parsed !== null) {
27613
- return parsed;
27614
- }
27615
- }
27616
- return parseBraceSlice(trimmed);
27617
- }
27618
- function parseBraceSlice(trimmed) {
27619
- const first = trimmed.indexOf("{");
27620
- const last = trimmed.lastIndexOf("}");
27621
- if (first >= 0 && last > first) {
27622
- return tryParseJson(trimmed.slice(first, last + 1));
27623
- }
27624
- return null;
27625
- }
27626
- function truncate(text, max = 300) {
27627
- return text.length > max ? `${text.slice(0, max)}...` : text;
27628
- }
27629
27601
  const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
27630
27602
  const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
27631
27603
  const TOOL_NAME = "report_vision_evidence";
@@ -28176,11 +28148,13 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
28176
28148
  const config2 = loadConfigFile(configPath);
28177
28149
  if (dottedKey === "provider") {
28178
28150
  config2.provider = value;
28151
+ } else if (dottedKey.startsWith("guards.")) {
28152
+ setGuardsValue(config2, dottedKey.slice("guards.".length), value);
28179
28153
  } else {
28180
28154
  const dot = dottedKey.indexOf(".");
28181
28155
  if (dot <= 0 || dot === dottedKey.length - 1) {
28182
28156
  throw new Error(
28183
- `Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model|extraBody>".`
28157
+ `Invalid config key: ${dottedKey}. Use "provider", "guards.<denyModels|denyWhenUnknown>", or "<provider>.<apiKey|baseUrl|model|extraBody>".`
28184
28158
  );
28185
28159
  }
28186
28160
  const providerName = dottedKey.slice(0, dot);
@@ -28214,6 +28188,38 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
28214
28188
  } catch {
28215
28189
  }
28216
28190
  }
28191
+ function setGuardsValue(config2, field, value) {
28192
+ if (field === "denyModels") {
28193
+ if (value.trim() === "") {
28194
+ delete config2.guards?.denyModels;
28195
+ } else {
28196
+ config2.guards ??= {};
28197
+ config2.guards.denyModels = parseModelList(value);
28198
+ }
28199
+ } else if (field === "denyWhenUnknown") {
28200
+ const normalized = value.trim().toLowerCase();
28201
+ if (normalized !== "true" && normalized !== "false") {
28202
+ throw new Error("guards.denyWhenUnknown must be true or false.");
28203
+ }
28204
+ config2.guards ??= {};
28205
+ config2.guards.denyWhenUnknown = normalized === "true";
28206
+ } else {
28207
+ throw new Error(`Unknown guards field: ${field}. Use denyModels or denyWhenUnknown.`);
28208
+ }
28209
+ if (config2.guards && Object.keys(config2.guards).length === 0) {
28210
+ delete config2.guards;
28211
+ }
28212
+ }
28213
+ function parseModelList(value) {
28214
+ if (value.trim().startsWith("[")) {
28215
+ const parsed = parseJsonOrExplain(value, "guards.denyModels");
28216
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
28217
+ throw new Error("guards.denyModels must be a JSON array of glob strings.");
28218
+ }
28219
+ return parsed;
28220
+ }
28221
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
28222
+ }
28217
28223
  const CONFIG_TEMPLATE = {
28218
28224
  // Empty means the built-in default provider.
28219
28225
  provider: "",
@@ -28266,6 +28272,18 @@ function renderEffectiveConfig(config2, env = process.env) {
28266
28272
  if (config2.provider?.trim()) {
28267
28273
  effective.provider = config2.provider.trim();
28268
28274
  }
28275
+ if (config2.guards) {
28276
+ const guards = {};
28277
+ if (config2.guards.denyModels !== void 0) {
28278
+ guards.denyModels = `${JSON.stringify(config2.guards.denyModels)} (file)`;
28279
+ }
28280
+ if (config2.guards.denyWhenUnknown !== void 0) {
28281
+ guards.denyWhenUnknown = `${config2.guards.denyWhenUnknown} (file)`;
28282
+ }
28283
+ if (Object.keys(guards).length > 0) {
28284
+ effective.guards = guards;
28285
+ }
28286
+ }
28269
28287
  return JSON.stringify(effective, null, 2);
28270
28288
  }
28271
28289
  function maskKey(key) {
@@ -28706,270 +28724,101 @@ function detectHarnessDetailed() {
28706
28724
  function detectHarness() {
28707
28725
  return detectHarnessDetailed().harness;
28708
28726
  }
28709
- const MIN_NODE = "22.13";
28710
- function versionParts(version) {
28711
- const match = /(\d+)\.(\d+)/.exec(version.replace(/^v/, ""));
28712
- if (!match) {
28713
- return [0, 0];
28727
+ function cwdMatches(recorded, wanted, bothDirections = false) {
28728
+ const resolvedRecorded = path.resolve(recorded);
28729
+ const resolvedWanted = path.resolve(wanted);
28730
+ if (resolvedRecorded === resolvedWanted || resolvedRecorded.startsWith(`${resolvedWanted}${path.sep}`)) {
28731
+ return true;
28714
28732
  }
28715
- return [Number(match[1]), Number(match[2])];
28733
+ return bothDirections && resolvedWanted.startsWith(`${resolvedRecorded}${path.sep}`);
28716
28734
  }
28717
- function meetsMinimum(version, minimum) {
28718
- const [major, minor] = versionParts(version);
28719
- const [minMajor, minMinor] = versionParts(minimum);
28720
- return major > minMajor || major === minMajor && minor >= minMinor;
28735
+ function transcriptBelongsTo(lines, cwd, bothDirections = false) {
28736
+ let sawCwd = false;
28737
+ for (const line of lines) {
28738
+ if (!line.includes('"cwd"')) {
28739
+ continue;
28740
+ }
28741
+ try {
28742
+ const recorded = JSON.parse(line).cwd;
28743
+ if (typeof recorded !== "string") {
28744
+ continue;
28745
+ }
28746
+ sawCwd = true;
28747
+ if (cwdMatches(recorded, cwd, bothDirections)) {
28748
+ return true;
28749
+ }
28750
+ } catch {
28751
+ }
28752
+ }
28753
+ return !sawCwd;
28721
28754
  }
28722
- function checkNodeSqlite() {
28723
- const realEmit = process.emitWarning;
28724
- process.emitWarning = () => {
28725
- };
28755
+ function readLines(filePath) {
28726
28756
  try {
28727
- const mod = createRequire(import.meta.url)("node:sqlite");
28728
- if (mod?.DatabaseSync) {
28729
- return {
28730
- available: true,
28731
- detail: "node:sqlite is available (OpenCode paste recovery)"
28732
- };
28733
- }
28734
- return { available: false, detail: "node:sqlite loaded but DatabaseSync is missing" };
28757
+ return fs.readFileSync(filePath, "utf-8").split("\n");
28735
28758
  } catch {
28736
- return {
28737
- available: false,
28738
- detail: "node:sqlite unavailable. Upgrade Node to 22.13+ for OpenCode paste recovery"
28739
- };
28740
- } finally {
28741
- process.emitWarning = realEmit;
28759
+ return null;
28742
28760
  }
28743
28761
  }
28744
- function inspectProvider(descriptor, config2, env) {
28745
- if (descriptor.kind === "subprocess") {
28746
- const binaryPath = findOnPath(descriptor.bin, env);
28747
- return {
28748
- name: descriptor.name,
28749
- kind: "subprocess",
28750
- ready: binaryPath !== null,
28751
- binaryPath,
28752
- detail: binaryPath ? `${descriptor.bin} found at ${binaryPath}` : `${descriptor.bin} not on PATH`,
28753
- fix: binaryPath ? void 0 : descriptor.install
28754
- };
28762
+ function forEachJsonLine(filePath, visit) {
28763
+ const lines = readLines(filePath);
28764
+ if (!lines) {
28765
+ return;
28755
28766
  }
28756
- const settings = resolveProviderSettings(descriptor.name, config2, env);
28757
- const statuses = (descriptor.required ?? []).map((req) => {
28758
- const envValue = req.env ? env[req.env]?.trim() : void 0;
28759
- const value = settings[req.field]?.trim();
28760
- const source = envValue ? "env" : value ? "file" : "missing";
28761
- return { field: req.field, present: Boolean(value), source, env: req.env };
28762
- });
28763
- const missing = statuses.filter((s) => !s.present).map((s) => s.field);
28764
- const ready = missing.length === 0;
28765
- const detail = ready ? statuses.map((s) => `${s.field}: ${s.source}`).join(", ") : `missing: ${missing.join(", ")}`;
28767
+ forEachParsedLine(lines, visit);
28768
+ }
28769
+ function forEachParsedLine(lines, visit) {
28770
+ for (const line of lines) {
28771
+ if (!line.includes('"image"')) {
28772
+ continue;
28773
+ }
28774
+ try {
28775
+ visit(JSON.parse(line));
28776
+ } catch {
28777
+ }
28778
+ }
28779
+ }
28780
+ function jsonlSource(harness, filePath, extractLine) {
28766
28781
  return {
28767
- name: descriptor.name,
28768
- kind: "api",
28769
- ready,
28770
- settings: statuses,
28771
- detail,
28772
- fix: ready ? void 0 : descriptor.fix
28782
+ harness,
28783
+ location: filePath,
28784
+ extract: () => {
28785
+ const images = [];
28786
+ forEachJsonLine(filePath, (line) => {
28787
+ images.push(...extractLine(line));
28788
+ });
28789
+ return images;
28790
+ }
28773
28791
  };
28774
28792
  }
28775
- function resolveSelection(config2, providerFlag) {
28776
- const raw = providerFlag?.trim() || config2.provider?.trim() || "antigravity-cli";
28777
- const source = providerFlag?.trim() ? "flag" : config2.provider?.trim() ? "config" : "default";
28778
- let canonical;
28793
+ function newestJsonlTimestamp(lines, extractLine) {
28794
+ let latest = null;
28795
+ forEachParsedLine(lines, (line) => {
28796
+ if (extractLine(line).length === 0) {
28797
+ return;
28798
+ }
28799
+ const ts = line.timestamp;
28800
+ const ms = typeof ts === "string" ? Date.parse(ts) : NaN;
28801
+ if (Number.isFinite(ms) && (latest === null || ms > latest)) {
28802
+ latest = ms;
28803
+ }
28804
+ });
28805
+ return latest;
28806
+ }
28807
+ function listJsonl(dir) {
28779
28808
  try {
28780
- canonical = resolveProvider(raw).name;
28809
+ return fs.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path.join(dir, name));
28781
28810
  } catch {
28782
- canonical = null;
28811
+ return [];
28783
28812
  }
28784
- const reason = source === "flag" ? `-p ${raw} on the command line` : source === "config" ? "provider set in the config file" : "built-in default (no -p flag and no provider in the config file)";
28785
- return { provider: raw, canonical, source, reason };
28786
28813
  }
28787
- function inspectConfigFile(configPath) {
28788
- try {
28789
- const stat = fs.statSync(configPath);
28790
- const mode = stat.mode & 511;
28791
- const enforcesPosixPerms = typeof process.getuid === "function";
28792
- const permissionsOk = !enforcesPosixPerms || (mode & 63) === 0;
28793
- return {
28794
- path: configPath,
28795
- exists: true,
28796
- mode: mode.toString(8).padStart(3, "0"),
28797
- permissionsOk,
28798
- note: permissionsOk ? void 0 : "group/world can read this file. Run: chmod 600 to lock it down"
28799
- };
28800
- } catch (error) {
28801
- if (error.code === "ENOENT") {
28802
- return {
28803
- path: configPath,
28804
- exists: false,
28805
- mode: null,
28806
- permissionsOk: true,
28807
- note: "no config file (using env vars and built-in defaults)"
28808
- };
28809
- }
28810
- return {
28811
- path: configPath,
28812
- exists: true,
28813
- mode: null,
28814
- permissionsOk: false,
28815
- note: `cannot stat: ${error.message}`
28816
- };
28817
- }
28818
- }
28819
- function buildDoctorReport(input) {
28820
- const env = input.env ?? process.env;
28821
- const configPath = input.configPath ?? CONFIG_PATH;
28822
- return {
28823
- node: {
28824
- version: process.version,
28825
- minimum: MIN_NODE,
28826
- meetsMinimum: meetsMinimum(process.version, MIN_NODE)
28827
- },
28828
- nodeSqlite: checkNodeSqlite(),
28829
- providers: PROVIDER_DESCRIPTORS.map((d) => inspectProvider(d, input.config, env)),
28830
- selection: resolveSelection(input.config, input.providerFlag),
28831
- chains: {
28832
- local: providerChain("local", input.config, env).map((p) => p.name),
28833
- remote: providerChain("remote", input.config, env).map((p) => p.name)
28834
- },
28835
- harness: (() => {
28836
- const detection = detectHarnessDetailed();
28837
- return { detected: detection.harness, source: detection.source };
28838
- })(),
28839
- config: inspectConfigFile(configPath)
28840
- };
28841
- }
28842
- function mark(ok) {
28843
- return ok ? "[ok]" : "[!!]";
28844
- }
28845
- function renderDoctorReport(report) {
28846
- const lines = [];
28847
- lines.push("modlens doctor");
28848
- lines.push("(local diagnostics only: no network calls, no provider quota spent)");
28849
- lines.push("");
28850
- lines.push("Node");
28851
- lines.push(
28852
- ` ${mark(report.node.meetsMinimum)} ${report.node.version} (minimum ${report.node.minimum})`
28853
- );
28854
- lines.push(` ${mark(report.nodeSqlite.available)} ${report.nodeSqlite.detail}`);
28855
- lines.push("");
28856
- lines.push("Providers");
28857
- for (const provider of report.providers) {
28858
- lines.push(` ${mark(provider.ready)} ${provider.name}: ${provider.detail}`);
28859
- if (provider.fix) {
28860
- lines.push(` fix: ${provider.fix}`);
28861
- }
28862
- }
28863
- lines.push("");
28864
- lines.push("Selected provider");
28865
- const canonicalNote = report.selection.canonical && report.selection.canonical !== report.selection.provider ? ` (canonical: ${report.selection.canonical})` : report.selection.canonical === null ? " (unknown provider name)" : "";
28866
- lines.push(` ${report.selection.provider}${canonicalNote}`);
28867
- lines.push(` reason: ${report.selection.reason}`);
28868
- lines.push("");
28869
- lines.push("Failover chains (what a run tries, in order)");
28870
- const chainLine = (chain) => chain.length > 0 ? chain.join(" -> ") : "(none available)";
28871
- lines.push(` local: ${chainLine(report.chains.local)}`);
28872
- lines.push(` remote: ${chainLine(report.chains.remote)}`);
28873
- lines.push("");
28874
- lines.push("Harness");
28875
- lines.push(
28876
- report.harness.detected ? ` ${report.harness.detected} (via ${report.harness.source})` : ` none detected (${report.harness.source})`
28877
- );
28878
- lines.push("");
28879
- lines.push("Config file");
28880
- lines.push(` path: ${report.config.path}`);
28881
- if (report.config.exists) {
28882
- lines.push(
28883
- ` ${mark(report.config.permissionsOk)} exists, mode ${report.config.mode ?? "?"}`
28884
- );
28885
- } else {
28886
- lines.push(" not present");
28887
- }
28888
- if (report.config.note) {
28889
- lines.push(` note: ${report.config.note}`);
28890
- }
28891
- return lines.join("\n");
28892
- }
28893
- function transcriptBelongsTo(lines, cwd) {
28894
- const wanted = path.resolve(cwd);
28895
- let sawCwd = false;
28896
- for (const line of lines) {
28897
- if (!line.includes('"cwd"')) {
28898
- continue;
28899
- }
28900
- try {
28901
- const recorded = JSON.parse(line).cwd;
28902
- if (typeof recorded !== "string") {
28903
- continue;
28904
- }
28905
- sawCwd = true;
28906
- const resolved = path.resolve(recorded);
28907
- if (resolved === wanted || resolved.startsWith(`${wanted}${path.sep}`)) {
28908
- return true;
28909
- }
28910
- } catch {
28911
- }
28912
- }
28913
- return !sawCwd;
28914
- }
28915
- function readLines(filePath) {
28916
- try {
28917
- return fs.readFileSync(filePath, "utf-8").split("\n");
28918
- } catch {
28919
- return null;
28920
- }
28921
- }
28922
- function forEachJsonLine(filePath, visit) {
28923
- const lines = readLines(filePath);
28924
- if (!lines) {
28925
- return;
28926
- }
28927
- forEachParsedLine(lines, visit);
28928
- }
28929
- function forEachParsedLine(lines, visit) {
28930
- for (const line of lines) {
28931
- if (!line.includes('"image"')) {
28932
- continue;
28933
- }
28814
+ function listJsonlByMtimeDesc(dir) {
28815
+ return listJsonl(dir).map((file) => {
28934
28816
  try {
28935
- visit(JSON.parse(line));
28817
+ return { file, mtime: fs.statSync(file).mtimeMs };
28936
28818
  } catch {
28819
+ return null;
28937
28820
  }
28938
- }
28939
- }
28940
- function jsonlSource(harness, filePath, extractLine) {
28941
- return {
28942
- harness,
28943
- location: filePath,
28944
- extract: () => {
28945
- const images = [];
28946
- forEachJsonLine(filePath, (line) => {
28947
- images.push(...extractLine(line));
28948
- });
28949
- return images;
28950
- }
28951
- };
28952
- }
28953
- function newestJsonlTimestamp(lines, extractLine) {
28954
- let latest = null;
28955
- forEachParsedLine(lines, (line) => {
28956
- if (extractLine(line).length === 0) {
28957
- return;
28958
- }
28959
- const ts = line.timestamp;
28960
- const ms = typeof ts === "string" ? Date.parse(ts) : NaN;
28961
- if (Number.isFinite(ms) && (latest === null || ms > latest)) {
28962
- latest = ms;
28963
- }
28964
- });
28965
- return latest;
28966
- }
28967
- function listJsonl(dir) {
28968
- try {
28969
- return fs.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path.join(dir, name));
28970
- } catch {
28971
- return [];
28972
- }
28821
+ }).filter((entry) => entry !== null).sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
28973
28822
  }
28974
28823
  function jsonlAdapter(options) {
28975
28824
  const { name, dirFor, matchesSession, extractLine } = options;
@@ -29033,13 +28882,19 @@ function opencodeDbPath() {
29033
28882
  function escapeLikePattern(value) {
29034
28883
  return value.replace(/[\\%_]/g, (char) => `\\${char}`);
29035
28884
  }
29036
- function buildOpencodeQuery(resolvedCwd, sessionId) {
28885
+ function opencodeDirectoryFilter(resolvedCwd) {
29037
28886
  const normalized = resolvedCwd.replace(/\\/g, "/");
29038
28887
  const escaped = escapeLikePattern(normalized);
29039
28888
  const dir = `REPLACE(session.directory, '\\', '/')`;
29040
- const directoryFilter = `(${dir} = ? OR ${dir} LIKE ? || '/%' ESCAPE '\\' OR ? LIKE ${dir} || '/%' ESCAPE '\\')`;
29041
- const sessionFilter = sessionId ? `AND ${directoryFilter} AND (session.id = ? OR session.slug = ?)` : `AND ${directoryFilter}`;
29042
- const params = sessionId ? [normalized, escaped, normalized, sessionId, sessionId] : [normalized, escaped, normalized];
28889
+ return {
28890
+ clause: `(${dir} = ? OR ${dir} LIKE ? || '/%' ESCAPE '\\' OR ? LIKE ${dir} || '/%' ESCAPE '\\')`,
28891
+ params: [normalized, escaped, normalized]
28892
+ };
28893
+ }
28894
+ function buildOpencodeQuery(resolvedCwd, sessionId) {
28895
+ const directory = opencodeDirectoryFilter(resolvedCwd);
28896
+ const sessionFilter = sessionId ? `AND ${directory.clause} AND (session.id = ? OR session.slug = ?)` : `AND ${directory.clause}`;
28897
+ const params = sessionId ? [...directory.params, sessionId, sessionId] : directory.params;
29043
28898
  const sql = `SELECT part.data AS data, part.time_created AS time_created, part.session_id AS session_id
29044
28899
  FROM part
29045
28900
  JOIN message ON message.id = part.message_id
@@ -29050,12 +28905,17 @@ function buildOpencodeQuery(resolvedCwd, sessionId) {
29050
28905
  ORDER BY part.time_created ASC`;
29051
28906
  return { sql, params };
29052
28907
  }
29053
- function opencodeQuery(dbPath, cwd, sessionId) {
29054
- let DatabaseSync;
28908
+ function loadNodeSqlite() {
29055
28909
  try {
29056
28910
  const nodeRequire = createRequire(import.meta.url);
29057
- ({ DatabaseSync } = nodeRequire("node:sqlite"));
28911
+ return nodeRequire("node:sqlite").DatabaseSync;
29058
28912
  } catch {
28913
+ return null;
28914
+ }
28915
+ }
28916
+ function opencodeQuery(dbPath, cwd, sessionId) {
28917
+ const DatabaseSync = loadNodeSqlite();
28918
+ if (!DatabaseSync) {
29059
28919
  throw new Error(
29060
28920
  "Reading opencode storage needs the node:sqlite module (unflagged on Node 22.13+). Upgrade Node, or pass --transcript/--session for a JSONL-based harness."
29061
28921
  );
@@ -29156,6 +29016,541 @@ const piAdapter = jsonlAdapter({
29156
29016
  matchesSession: (fileName, sessionId) => fileName.endsWith(`_${sessionId}.jsonl`),
29157
29017
  extractLine: piExtractLine
29158
29018
  });
29019
+ const WINDOW_BYTES = 512 * 1024;
29020
+ function readWindowedLines(file, maxBytes = WINDOW_BYTES) {
29021
+ let fd;
29022
+ try {
29023
+ fd = fs.openSync(file, "r");
29024
+ } catch {
29025
+ return null;
29026
+ }
29027
+ try {
29028
+ const size = fs.fstatSync(fd).size;
29029
+ if (size <= 2 * maxBytes) {
29030
+ const whole = Buffer.alloc(size);
29031
+ fs.readSync(fd, whole, 0, size, 0);
29032
+ return whole.toString("utf-8").split("\n");
29033
+ }
29034
+ const head = Buffer.alloc(maxBytes);
29035
+ fs.readSync(fd, head, 0, maxBytes, 0);
29036
+ const tail = Buffer.alloc(maxBytes);
29037
+ fs.readSync(fd, tail, 0, maxBytes, size - maxBytes);
29038
+ const headLines = head.toString("utf-8").split("\n");
29039
+ headLines.pop();
29040
+ const tailLines = tail.toString("utf-8").split("\n");
29041
+ tailLines.shift();
29042
+ return [...headLines, ...tailLines];
29043
+ } catch {
29044
+ return null;
29045
+ } finally {
29046
+ fs.closeSync(fd);
29047
+ }
29048
+ }
29049
+ function lastAssistantModelFromLines(lines) {
29050
+ for (let i = lines.length - 1; i >= 0; i--) {
29051
+ const line = lines[i];
29052
+ if (!line.includes('"model"')) {
29053
+ continue;
29054
+ }
29055
+ try {
29056
+ const message = JSON.parse(line).message;
29057
+ if (message?.role === "assistant" && typeof message.model === "string") {
29058
+ const found = { model: message.model };
29059
+ if (typeof message.provider === "string") {
29060
+ found.provider = message.provider;
29061
+ }
29062
+ return found;
29063
+ }
29064
+ } catch {
29065
+ }
29066
+ }
29067
+ return null;
29068
+ }
29069
+ function lastCodexModelFromLines(lines) {
29070
+ for (let i = lines.length - 1; i >= 0; i--) {
29071
+ const line = lines[i];
29072
+ if (!line.includes('"turn_context"')) {
29073
+ continue;
29074
+ }
29075
+ try {
29076
+ const parsed = JSON.parse(line);
29077
+ if (parsed.type === "turn_context" && typeof parsed.payload?.model === "string") {
29078
+ return parsed.payload.model;
29079
+ }
29080
+ } catch {
29081
+ }
29082
+ }
29083
+ return null;
29084
+ }
29085
+ function codexTranscriptBelongsTo(lines, cwd) {
29086
+ for (const line of lines) {
29087
+ if (!line.includes('"cwd"')) {
29088
+ continue;
29089
+ }
29090
+ try {
29091
+ const recorded = JSON.parse(line).payload?.cwd;
29092
+ if (typeof recorded === "string" && cwdMatches(recorded, cwd, true)) {
29093
+ return true;
29094
+ }
29095
+ } catch {
29096
+ }
29097
+ }
29098
+ return false;
29099
+ }
29100
+ function* cwdAncestors(cwd) {
29101
+ let current = path.resolve(cwd);
29102
+ for (; ; ) {
29103
+ yield current;
29104
+ const parent = path.dirname(current);
29105
+ if (parent === current) {
29106
+ return;
29107
+ }
29108
+ current = parent;
29109
+ }
29110
+ }
29111
+ function newestAssistantModelInDir(dir, cwd) {
29112
+ for (const file of listJsonlByMtimeDesc(dir)) {
29113
+ const lines = readWindowedLines(file);
29114
+ if (!lines || !transcriptBelongsTo(lines, cwd, true)) {
29115
+ continue;
29116
+ }
29117
+ const found = lastAssistantModelFromLines(lines);
29118
+ if (found) {
29119
+ return found;
29120
+ }
29121
+ }
29122
+ return null;
29123
+ }
29124
+ function existingSlugDirs(root, cwd, slugFor) {
29125
+ const dirs = [];
29126
+ for (const ancestor of cwdAncestors(cwd)) {
29127
+ const dir = path.join(root, slugFor(ancestor));
29128
+ if (fs.existsSync(dir)) {
29129
+ dirs.push(dir);
29130
+ }
29131
+ }
29132
+ return dirs;
29133
+ }
29134
+ function sniffClaudeModel(cwd, env, projectsDir = path.join(os.homedir(), ".claude", "projects")) {
29135
+ const dirs = existingSlugDirs(projectsDir, cwd, claudeProjectSlug);
29136
+ const sessionId = env.CLAUDE_CODE_SESSION_ID?.trim();
29137
+ if (sessionId) {
29138
+ for (const dir of dirs) {
29139
+ const lines = readWindowedLines(path.join(dir, `${sessionId}.jsonl`));
29140
+ const pinned = lines ? lastAssistantModelFromLines(lines) : null;
29141
+ if (pinned) {
29142
+ return pinned;
29143
+ }
29144
+ }
29145
+ }
29146
+ for (const dir of dirs) {
29147
+ const found = newestAssistantModelInDir(dir, cwd);
29148
+ if (found) {
29149
+ return found;
29150
+ }
29151
+ }
29152
+ return null;
29153
+ }
29154
+ function sniffPiModel(cwd, sessionsRoot = path.join(os.homedir(), ".pi", "agent", "sessions")) {
29155
+ for (const dir of existingSlugDirs(sessionsRoot, cwd, piSessionSlug)) {
29156
+ const found = newestAssistantModelInDir(dir, cwd);
29157
+ if (found) {
29158
+ return found;
29159
+ }
29160
+ }
29161
+ return null;
29162
+ }
29163
+ const CODEX_SCAN_LIMIT = 20;
29164
+ const CODEX_STAT_LIMIT = 200;
29165
+ function sniffCodexModel(cwd, env, sessionsRoot = path.join(os.homedir(), ".codex", "sessions")) {
29166
+ let names;
29167
+ try {
29168
+ names = fs.readdirSync(sessionsRoot, { recursive: true }).filter((name) => name.endsWith(".jsonl")).sort().reverse();
29169
+ } catch {
29170
+ return null;
29171
+ }
29172
+ const threadId = env.CODEX_THREAD_ID?.trim();
29173
+ if (threadId) {
29174
+ const pinned = names.find((name) => path.basename(name).endsWith(`-${threadId}.jsonl`));
29175
+ if (pinned) {
29176
+ const lines = readWindowedLines(path.join(sessionsRoot, pinned));
29177
+ const model = lines ? lastCodexModelFromLines(lines) : null;
29178
+ if (model) {
29179
+ return model;
29180
+ }
29181
+ }
29182
+ }
29183
+ const byMtimeDesc = names.slice(0, CODEX_STAT_LIMIT).map((name) => {
29184
+ const file = path.join(sessionsRoot, name);
29185
+ try {
29186
+ return { file, mtime: fs.statSync(file).mtimeMs };
29187
+ } catch {
29188
+ return null;
29189
+ }
29190
+ }).filter((entry) => entry !== null).sort((a, b) => b.mtime - a.mtime);
29191
+ for (const entry of byMtimeDesc.slice(0, CODEX_SCAN_LIMIT)) {
29192
+ const lines = readWindowedLines(entry.file);
29193
+ if (!lines || !codexTranscriptBelongsTo(lines, cwd)) {
29194
+ continue;
29195
+ }
29196
+ const model = lastCodexModelFromLines(lines);
29197
+ if (model) {
29198
+ return model;
29199
+ }
29200
+ }
29201
+ return null;
29202
+ }
29203
+ function opencodeModelForCwd(cwd, dbPath = opencodeDbPath()) {
29204
+ if (!fs.existsSync(dbPath)) {
29205
+ return null;
29206
+ }
29207
+ const DatabaseSync = loadNodeSqlite();
29208
+ if (!DatabaseSync) {
29209
+ return null;
29210
+ }
29211
+ const directory = opencodeDirectoryFilter(path.resolve(cwd));
29212
+ const sql = `SELECT message.data AS data
29213
+ FROM message
29214
+ JOIN session ON session.id = message.session_id
29215
+ WHERE json_extract(message.data, '$.role') = 'assistant'
29216
+ AND ${directory.clause}
29217
+ ORDER BY message.time_created DESC
29218
+ LIMIT 1`;
29219
+ const db = new DatabaseSync(dbPath, { readOnly: true });
29220
+ try {
29221
+ const rows = db.prepare(sql).all(...directory.params);
29222
+ if (rows.length === 0) {
29223
+ return null;
29224
+ }
29225
+ const data = JSON.parse(rows[0].data);
29226
+ if (typeof data.modelID !== "string") {
29227
+ return null;
29228
+ }
29229
+ const found = { model: data.modelID };
29230
+ if (typeof data.providerID === "string") {
29231
+ found.provider = data.providerID;
29232
+ }
29233
+ return found;
29234
+ } catch {
29235
+ return null;
29236
+ } finally {
29237
+ db.close();
29238
+ }
29239
+ }
29240
+ function sniffModel(harness, cwd, env, roots = {}) {
29241
+ try {
29242
+ switch (harness) {
29243
+ case "claude-code":
29244
+ return sniffClaudeModel(cwd, env, roots.claudeProjectsDir);
29245
+ case "pi":
29246
+ return sniffPiModel(cwd, roots.piSessionsRoot);
29247
+ case "codex": {
29248
+ const model = sniffCodexModel(cwd, env, roots.codexSessionsRoot);
29249
+ return model ? { model } : null;
29250
+ }
29251
+ case "opencode":
29252
+ return opencodeModelForCwd(cwd, roots.opencodeDb);
29253
+ default:
29254
+ return null;
29255
+ }
29256
+ } catch {
29257
+ return null;
29258
+ }
29259
+ }
29260
+ function denyPatterns(guards) {
29261
+ const raw = guards?.denyModels;
29262
+ if (!Array.isArray(raw)) {
29263
+ return [];
29264
+ }
29265
+ return raw.filter((pattern) => typeof pattern === "string");
29266
+ }
29267
+ function globMatch(pattern, value) {
29268
+ const regex = pattern.split(/([*?])/).map((part) => {
29269
+ if (part === "*") {
29270
+ return ".*";
29271
+ }
29272
+ if (part === "?") {
29273
+ return ".";
29274
+ }
29275
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29276
+ }).join("");
29277
+ return new RegExp(`^${regex}$`, "i").test(value);
29278
+ }
29279
+ function evaluateGuard(guards, detection) {
29280
+ const patterns = denyPatterns(guards);
29281
+ if (!detection.model) {
29282
+ if (guards?.denyWhenUnknown === true) {
29283
+ return {
29284
+ ...detection,
29285
+ guard: "deny",
29286
+ reason: "model unknown and denyWhenUnknown is set"
29287
+ };
29288
+ }
29289
+ return {
29290
+ ...detection,
29291
+ guard: "allow",
29292
+ reason: patterns.length === 0 ? "no deny rules configured" : "model unknown, failing open"
29293
+ };
29294
+ }
29295
+ if (patterns.length === 0) {
29296
+ return { ...detection, guard: "allow", reason: "no deny rules configured" };
29297
+ }
29298
+ const candidates = [detection.model];
29299
+ if (detection.provider) {
29300
+ candidates.push(`${detection.provider}/${detection.model}`);
29301
+ }
29302
+ for (const pattern of patterns) {
29303
+ if (candidates.some((candidate) => globMatch(pattern, candidate))) {
29304
+ return {
29305
+ ...detection,
29306
+ guard: "deny",
29307
+ matched: pattern,
29308
+ reason: "model has native vision per guards.denyModels"
29309
+ };
29310
+ }
29311
+ }
29312
+ return { ...detection, guard: "allow", reason: "not on the deny list" };
29313
+ }
29314
+ function detectActiveModel(options) {
29315
+ const env = options.env ?? process.env;
29316
+ const envModel = env.MODLENS_MODEL?.trim();
29317
+ if (envModel) {
29318
+ return envModel.toLowerCase() === "none" ? { model: null, source: "env" } : { model: envModel, source: "env" };
29319
+ }
29320
+ const forced = env.MODLENS_HARNESS;
29321
+ const harness = forced === "none" ? null : forced || (options.harness !== void 0 ? options.harness : detectHarnessDetailed().harness);
29322
+ const sniffed = harness ? sniffModel(harness, options.cwd, env, options.roots) : null;
29323
+ if (sniffed) {
29324
+ const detection = {
29325
+ model: sniffed.model,
29326
+ source: "storage",
29327
+ harness
29328
+ };
29329
+ if (sniffed.provider) {
29330
+ detection.provider = sniffed.provider;
29331
+ }
29332
+ if (options.selfReported && options.selfReported.toLowerCase() !== sniffed.model.toLowerCase()) {
29333
+ detection.selfReported = options.selfReported;
29334
+ }
29335
+ return detection;
29336
+ }
29337
+ if (options.selfReported) {
29338
+ return { model: options.selfReported, source: "self-report", harness };
29339
+ }
29340
+ return { model: null, source: "none", harness };
29341
+ }
29342
+ function runGuard(guards, options) {
29343
+ if (denyPatterns(guards).length === 0 && guards?.denyWhenUnknown !== true) {
29344
+ return { model: null, source: "none", guard: "allow", reason: "no deny rules configured" };
29345
+ }
29346
+ return evaluateGuard(guards, detectActiveModel(options));
29347
+ }
29348
+ const MIN_NODE = "22.13";
29349
+ function versionParts(version) {
29350
+ const match = /(\d+)\.(\d+)/.exec(version.replace(/^v/, ""));
29351
+ if (!match) {
29352
+ return [0, 0];
29353
+ }
29354
+ return [Number(match[1]), Number(match[2])];
29355
+ }
29356
+ function meetsMinimum(version, minimum) {
29357
+ const [major, minor] = versionParts(version);
29358
+ const [minMajor, minMinor] = versionParts(minimum);
29359
+ return major > minMajor || major === minMajor && minor >= minMinor;
29360
+ }
29361
+ function checkNodeSqlite() {
29362
+ const realEmit = process.emitWarning;
29363
+ process.emitWarning = () => {
29364
+ };
29365
+ try {
29366
+ const mod = createRequire(import.meta.url)("node:sqlite");
29367
+ if (mod?.DatabaseSync) {
29368
+ return {
29369
+ available: true,
29370
+ detail: "node:sqlite is available (OpenCode paste recovery)"
29371
+ };
29372
+ }
29373
+ return { available: false, detail: "node:sqlite loaded but DatabaseSync is missing" };
29374
+ } catch {
29375
+ return {
29376
+ available: false,
29377
+ detail: "node:sqlite unavailable. Upgrade Node to 22.13+ for OpenCode paste recovery"
29378
+ };
29379
+ } finally {
29380
+ process.emitWarning = realEmit;
29381
+ }
29382
+ }
29383
+ function inspectProvider(descriptor, config2, env) {
29384
+ if (descriptor.kind === "subprocess") {
29385
+ const binaryPath = findOnPath(descriptor.bin, env);
29386
+ return {
29387
+ name: descriptor.name,
29388
+ kind: "subprocess",
29389
+ ready: binaryPath !== null,
29390
+ binaryPath,
29391
+ detail: binaryPath ? `${descriptor.bin} found at ${binaryPath}` : `${descriptor.bin} not on PATH`,
29392
+ fix: binaryPath ? void 0 : descriptor.install
29393
+ };
29394
+ }
29395
+ const settings = resolveProviderSettings(descriptor.name, config2, env);
29396
+ const statuses = (descriptor.required ?? []).map((req) => {
29397
+ const envValue = req.env ? env[req.env]?.trim() : void 0;
29398
+ const value = settings[req.field]?.trim();
29399
+ const source = envValue ? "env" : value ? "file" : "missing";
29400
+ return { field: req.field, present: Boolean(value), source, env: req.env };
29401
+ });
29402
+ const missing = statuses.filter((s) => !s.present).map((s) => s.field);
29403
+ const ready = missing.length === 0;
29404
+ const detail = ready ? statuses.map((s) => `${s.field}: ${s.source}`).join(", ") : `missing: ${missing.join(", ")}`;
29405
+ return {
29406
+ name: descriptor.name,
29407
+ kind: "api",
29408
+ ready,
29409
+ settings: statuses,
29410
+ detail,
29411
+ fix: ready ? void 0 : descriptor.fix
29412
+ };
29413
+ }
29414
+ function resolveSelection(config2, providerFlag) {
29415
+ const raw = providerFlag?.trim() || config2.provider?.trim() || "antigravity-cli";
29416
+ const source = providerFlag?.trim() ? "flag" : config2.provider?.trim() ? "config" : "default";
29417
+ let canonical;
29418
+ try {
29419
+ canonical = resolveProvider(raw).name;
29420
+ } catch {
29421
+ canonical = null;
29422
+ }
29423
+ const reason = source === "flag" ? `-p ${raw} on the command line` : source === "config" ? "provider set in the config file" : "built-in default (no -p flag and no provider in the config file)";
29424
+ return { provider: raw, canonical, source, reason };
29425
+ }
29426
+ function inspectConfigFile(configPath) {
29427
+ try {
29428
+ const stat = fs.statSync(configPath);
29429
+ const mode = stat.mode & 511;
29430
+ const enforcesPosixPerms = typeof process.getuid === "function";
29431
+ const permissionsOk = !enforcesPosixPerms || (mode & 63) === 0;
29432
+ return {
29433
+ path: configPath,
29434
+ exists: true,
29435
+ mode: mode.toString(8).padStart(3, "0"),
29436
+ permissionsOk,
29437
+ note: permissionsOk ? void 0 : "group/world can read this file. Run: chmod 600 to lock it down"
29438
+ };
29439
+ } catch (error) {
29440
+ if (error.code === "ENOENT") {
29441
+ return {
29442
+ path: configPath,
29443
+ exists: false,
29444
+ mode: null,
29445
+ permissionsOk: true,
29446
+ note: "no config file (using env vars and built-in defaults)"
29447
+ };
29448
+ }
29449
+ return {
29450
+ path: configPath,
29451
+ exists: true,
29452
+ mode: null,
29453
+ permissionsOk: false,
29454
+ note: `cannot stat: ${error.message}`
29455
+ };
29456
+ }
29457
+ }
29458
+ function buildDoctorReport(input) {
29459
+ const env = input.env ?? process.env;
29460
+ const configPath = input.configPath ?? CONFIG_PATH;
29461
+ const harnessDetection = detectHarnessDetailed();
29462
+ const guardDetection = detectActiveModel({
29463
+ cwd: process.cwd(),
29464
+ env,
29465
+ harness: harnessDetection.harness
29466
+ });
29467
+ const guardVerdict = evaluateGuard(input.config.guards, guardDetection);
29468
+ return {
29469
+ node: {
29470
+ version: process.version,
29471
+ minimum: MIN_NODE,
29472
+ meetsMinimum: meetsMinimum(process.version, MIN_NODE)
29473
+ },
29474
+ nodeSqlite: checkNodeSqlite(),
29475
+ providers: PROVIDER_DESCRIPTORS.map((d) => inspectProvider(d, input.config, env)),
29476
+ selection: resolveSelection(input.config, input.providerFlag),
29477
+ chains: {
29478
+ local: providerChain("local", input.config, env).map((p) => p.name),
29479
+ remote: providerChain("remote", input.config, env).map((p) => p.name)
29480
+ },
29481
+ harness: { detected: harnessDetection.harness, source: harnessDetection.source },
29482
+ guard: {
29483
+ rules: denyPatterns(input.config.guards).length,
29484
+ denyWhenUnknown: input.config.guards?.denyWhenUnknown ?? false,
29485
+ model: guardVerdict.model,
29486
+ source: guardVerdict.source,
29487
+ verdict: guardVerdict.guard,
29488
+ matched: guardVerdict.matched,
29489
+ reason: guardVerdict.reason
29490
+ },
29491
+ config: inspectConfigFile(configPath)
29492
+ };
29493
+ }
29494
+ function mark(ok) {
29495
+ return ok ? "[ok]" : "[!!]";
29496
+ }
29497
+ function renderDoctorReport(report) {
29498
+ const lines = [];
29499
+ lines.push("modlens doctor");
29500
+ lines.push("(local diagnostics only: no network calls, no provider quota spent)");
29501
+ lines.push("");
29502
+ lines.push("Node");
29503
+ lines.push(
29504
+ ` ${mark(report.node.meetsMinimum)} ${report.node.version} (minimum ${report.node.minimum})`
29505
+ );
29506
+ lines.push(` ${mark(report.nodeSqlite.available)} ${report.nodeSqlite.detail}`);
29507
+ lines.push("");
29508
+ lines.push("Providers");
29509
+ for (const provider of report.providers) {
29510
+ lines.push(` ${mark(provider.ready)} ${provider.name}: ${provider.detail}`);
29511
+ if (provider.fix) {
29512
+ lines.push(` fix: ${provider.fix}`);
29513
+ }
29514
+ }
29515
+ lines.push("");
29516
+ lines.push("Selected provider");
29517
+ const canonicalNote = report.selection.canonical && report.selection.canonical !== report.selection.provider ? ` (canonical: ${report.selection.canonical})` : report.selection.canonical === null ? " (unknown provider name)" : "";
29518
+ lines.push(` ${report.selection.provider}${canonicalNote}`);
29519
+ lines.push(` reason: ${report.selection.reason}`);
29520
+ lines.push("");
29521
+ lines.push("Failover chains (what a run tries, in order)");
29522
+ const chainLine = (chain) => chain.length > 0 ? chain.join(" -> ") : "(none available)";
29523
+ lines.push(` local: ${chainLine(report.chains.local)}`);
29524
+ lines.push(` remote: ${chainLine(report.chains.remote)}`);
29525
+ lines.push("");
29526
+ lines.push("Harness");
29527
+ lines.push(
29528
+ report.harness.detected ? ` ${report.harness.detected} (via ${report.harness.source})` : ` none detected (${report.harness.source})`
29529
+ );
29530
+ lines.push("");
29531
+ lines.push("Guard (should the vision engine run for the active model?)");
29532
+ lines.push(
29533
+ ` rules: ${report.guard.rules} deny pattern(s), denyWhenUnknown: ${report.guard.denyWhenUnknown}`
29534
+ );
29535
+ lines.push(` active model: ${report.guard.model ?? "unknown"} (via ${report.guard.source})`);
29536
+ lines.push(
29537
+ ` verdict: ${report.guard.verdict}${report.guard.matched ? ` (matched "${report.guard.matched}")` : ""}, ${report.guard.reason}`
29538
+ );
29539
+ lines.push("");
29540
+ lines.push("Config file");
29541
+ lines.push(` path: ${report.config.path}`);
29542
+ if (report.config.exists) {
29543
+ lines.push(
29544
+ ` ${mark(report.config.permissionsOk)} exists, mode ${report.config.mode ?? "?"}`
29545
+ );
29546
+ } else {
29547
+ lines.push(" not present");
29548
+ }
29549
+ if (report.config.note) {
29550
+ lines.push(` note: ${report.config.note}`);
29551
+ }
29552
+ return lines.join("\n");
29553
+ }
29159
29554
  function extensionFromMediaType(mediaType) {
29160
29555
  const subtype = mediaType.split("/")[1]?.split("+")[0]?.replace(/[^a-z0-9]/gi, "");
29161
29556
  return subtype ? subtype.toLowerCase() : "bin";
@@ -29334,7 +29729,7 @@ function recoverPastedImages(options = {}) {
29334
29729
  return result;
29335
29730
  }
29336
29731
  const program = new Command();
29337
- program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.4.0");
29732
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.5.0");
29338
29733
  program.command("analyze", { isDefault: true }).description("Analyze an image into structured JSON evidence (default command)").requiredOption("-i, --input <path|url>", "Input image path or https URL").option("-o, --output <path>", "Write result JSON to a file").option("-m, --model <name>", "Provider model name").option("-p, --provider <name>", `Vision provider (${listProviders().join(", ")})`).option("--prompt <text>", "Extra focus for this image").option("--timeout <ms>", "Provider timeout in milliseconds", "180000").option("--provider-bin <path>", "Provider binary path (default: agy)").option("--workdir <path>", "Working directory for the provider").option(
29339
29734
  "--extra-body <json>",
29340
29735
  `JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
@@ -29344,6 +29739,18 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
29344
29739
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
29345
29740
  throw new Error("Invalid --timeout. Use a positive integer in milliseconds.");
29346
29741
  }
29742
+ const config2 = loadConfigFile();
29743
+ if (process.env.MODLENS_MODEL?.trim()) {
29744
+ const verdict = runGuard(config2.guards, {
29745
+ cwd: process.cwd(),
29746
+ env: process.env
29747
+ });
29748
+ if (verdict.guard === "deny" && verdict.matched) {
29749
+ throw new Error(
29750
+ `Invocation guard denied this read: active model "${verdict.model}" matches guards.denyModels pattern "${verdict.matched}". A model with native vision should read the image itself. To override, unset MODLENS_MODEL or edit guards in ${CONFIG_PATH}.`
29751
+ );
29752
+ }
29753
+ }
29347
29754
  const result = await analyzeImage({
29348
29755
  input: options.input,
29349
29756
  provider: options.provider,
@@ -29352,7 +29759,8 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
29352
29759
  timeoutMs,
29353
29760
  providerBin: options.providerBin,
29354
29761
  workdir: options.workdir,
29355
- extraBody: options.extraBody ? parseExtraBody(options.extraBody, "--extra-body") : void 0
29762
+ extraBody: options.extraBody ? parseExtraBody(options.extraBody, "--extra-body") : void 0,
29763
+ config: config2
29356
29764
  });
29357
29765
  const output = JSON.stringify(result, null, 2);
29358
29766
  if (options.output) {
@@ -29402,6 +29810,29 @@ program.command("recover-paste").description(
29402
29810
  process.exit(1);
29403
29811
  }
29404
29812
  });
29813
+ program.command("guard").description(
29814
+ "Check whether the vision engine should run for the active model (exit 0 allow, 1 deny, 2 error)"
29815
+ ).option(
29816
+ "--model <name>",
29817
+ "The calling agent's own model name; weakest signal, used when env and session storage say nothing"
29818
+ ).option("--cwd <path>", "Project directory of the session", process.cwd()).action((options) => {
29819
+ try {
29820
+ const verdict = runGuard(loadConfigFile().guards, {
29821
+ cwd: options.cwd,
29822
+ env: process.env,
29823
+ selfReported: options.model
29824
+ });
29825
+ process.stdout.write(`${JSON.stringify(verdict, null, 2)}
29826
+ `);
29827
+ process.exitCode = verdict.guard === "deny" ? 1 : 0;
29828
+ } catch (error) {
29829
+ process.stderr.write(
29830
+ `Error: ${error instanceof Error ? error.message : String(error)}
29831
+ `
29832
+ );
29833
+ process.exitCode = 2;
29834
+ }
29835
+ });
29405
29836
  program.command("doctor").description(
29406
29837
  "Diagnose local config and routing (Node, providers, selection, harness) without spending quota or hitting the network"
29407
29838
  ).option("--json", "Emit the report as JSON").option("-p, --provider <name>", "Show which provider this -p value would select").action((options) => {