@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/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/dist/main.js +779 -348
- package/docs/troubleshooting.md +14 -0
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +33 -10
- package/skills/modlens/references/configure.md +7 -2
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
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
|
|
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$
|
|
22
|
-
import require$$2$
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
5686
|
-
"node:sqlite": () => require
|
|
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$
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
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$
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
27541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28710
|
-
|
|
28711
|
-
const
|
|
28712
|
-
if (
|
|
28713
|
-
return
|
|
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
|
|
28733
|
+
return bothDirections && resolvedWanted.startsWith(`${resolvedRecorded}${path.sep}`);
|
|
28716
28734
|
}
|
|
28717
|
-
function
|
|
28718
|
-
|
|
28719
|
-
const
|
|
28720
|
-
|
|
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
|
|
28723
|
-
const realEmit = process.emitWarning;
|
|
28724
|
-
process.emitWarning = () => {
|
|
28725
|
-
};
|
|
28755
|
+
function readLines(filePath) {
|
|
28726
28756
|
try {
|
|
28727
|
-
|
|
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
|
|
28745
|
-
|
|
28746
|
-
|
|
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
|
-
|
|
28757
|
-
|
|
28758
|
-
|
|
28759
|
-
|
|
28760
|
-
|
|
28761
|
-
|
|
28762
|
-
|
|
28763
|
-
|
|
28764
|
-
|
|
28765
|
-
|
|
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
|
-
|
|
28768
|
-
|
|
28769
|
-
|
|
28770
|
-
|
|
28771
|
-
|
|
28772
|
-
|
|
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
|
|
28776
|
-
|
|
28777
|
-
|
|
28778
|
-
|
|
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
|
-
|
|
28809
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path.join(dir, name));
|
|
28781
28810
|
} catch {
|
|
28782
|
-
|
|
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
|
|
28788
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
29041
|
-
|
|
29042
|
-
|
|
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
|
|
29054
|
-
let DatabaseSync;
|
|
28908
|
+
function loadNodeSqlite() {
|
|
29055
28909
|
try {
|
|
29056
28910
|
const nodeRequire = createRequire(import.meta.url);
|
|
29057
|
-
|
|
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.
|
|
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) => {
|