@liustack/modlens 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/dist/main.js +781 -350
- 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
|
@@ -5,9 +5,10 @@ import * as path from "path";
|
|
|
5
5
|
import * as childProcess from "child_process";
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import * as os from "os";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
8
9
|
import require$$0$1 from "node:assert";
|
|
9
10
|
import require$$1 from "node:net";
|
|
10
|
-
import require$$2
|
|
11
|
+
import require$$2 from "node:http";
|
|
11
12
|
import require$$0$2 from "node:stream";
|
|
12
13
|
import require$$5 from "node:querystring";
|
|
13
14
|
import require$$0 from "node:events";
|
|
@@ -18,44 +19,20 @@ import require$$0$4 from "node:buffer";
|
|
|
18
19
|
import require$$0$5 from "node:zlib";
|
|
19
20
|
import require$$5$1 from "node:perf_hooks";
|
|
20
21
|
import require$$8 from "node:util/types";
|
|
21
|
-
import require$$2$
|
|
22
|
-
import require$$2$
|
|
22
|
+
import require$$2$1 from "node:worker_threads";
|
|
23
|
+
import require$$2$2 from "node:crypto";
|
|
23
24
|
import require$$11 from "node:stream/web";
|
|
24
25
|
import require$$0$6 from "node:url";
|
|
25
26
|
import require$$1$1 from "node:async_hooks";
|
|
26
27
|
import require$$1$2 from "node:console";
|
|
27
28
|
import require$$0$7 from "node:fs/promises";
|
|
28
29
|
import require$$1$3 from "node:path";
|
|
29
|
-
import require$$2$
|
|
30
|
+
import require$$2$3 from "node:timers";
|
|
30
31
|
import require$$1$4 from "node:dns";
|
|
31
32
|
import * as dns$1 from "dns/promises";
|
|
32
33
|
import { isIP } from "net";
|
|
33
34
|
import { createRequire } from "module";
|
|
34
35
|
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
36
|
var undici = { exports: {} };
|
|
60
37
|
var symbols;
|
|
61
38
|
var hasRequiredSymbols;
|
|
@@ -1061,7 +1038,7 @@ function requireUtil$5() {
|
|
|
1061
1038
|
hasRequiredUtil$5 = 1;
|
|
1062
1039
|
const assert = require$$0$1;
|
|
1063
1040
|
const { kDestroyed, kBodyUsed, kListeners, kBody } = requireSymbols();
|
|
1064
|
-
const { IncomingMessage } = require$$2
|
|
1041
|
+
const { IncomingMessage } = require$$2;
|
|
1065
1042
|
const stream = require$$0$2;
|
|
1066
1043
|
const net = require$$1;
|
|
1067
1044
|
const { stringify } = require$$5;
|
|
@@ -4309,7 +4286,7 @@ function requireWebidl() {
|
|
|
4309
4286
|
hasRequiredWebidl = 1;
|
|
4310
4287
|
const assert = require$$0$1;
|
|
4311
4288
|
const { types, inspect } = require$$3;
|
|
4312
|
-
const { markAsUncloneable } = require$$2$
|
|
4289
|
+
const { markAsUncloneable } = require$$2$1;
|
|
4313
4290
|
const UNDEFINED = 1;
|
|
4314
4291
|
const BOOLEAN = 2;
|
|
4315
4292
|
const STRING = 3;
|
|
@@ -5670,20 +5647,14 @@ function requireUtil$4() {
|
|
|
5670
5647
|
return util$4;
|
|
5671
5648
|
}
|
|
5672
5649
|
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
5650
|
var hasRequiredRuntimeFeatures;
|
|
5680
5651
|
function requireRuntimeFeatures() {
|
|
5681
5652
|
if (hasRequiredRuntimeFeatures) return runtimeFeatures;
|
|
5682
5653
|
hasRequiredRuntimeFeatures = 1;
|
|
5683
5654
|
const lazyLoaders = {
|
|
5684
5655
|
__proto__: null,
|
|
5685
|
-
"node:crypto": () => require$$2$
|
|
5686
|
-
"node:sqlite": () => require
|
|
5656
|
+
"node:crypto": () => require$$2$2,
|
|
5657
|
+
"node:sqlite": () => require("node:sqlite")
|
|
5687
5658
|
};
|
|
5688
5659
|
function detectRuntimeFeatureByNodeModule(moduleName) {
|
|
5689
5660
|
try {
|
|
@@ -5760,7 +5731,7 @@ function requireFormdata() {
|
|
|
5760
5731
|
const { webidl } = requireWebidl();
|
|
5761
5732
|
const nodeUtil = require$$3;
|
|
5762
5733
|
const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
|
|
5763
|
-
const random = runtimeFeatures2.has("crypto") ? require$$2$
|
|
5734
|
+
const random = runtimeFeatures2.has("crypto") ? require$$2$2.randomInt : (max) => Math.floor(Math.random() * max);
|
|
5764
5735
|
class FormData {
|
|
5765
5736
|
#state = [];
|
|
5766
5737
|
#boundary = null;
|
|
@@ -9112,7 +9083,7 @@ function requireClient() {
|
|
|
9112
9083
|
hasRequiredClient = 1;
|
|
9113
9084
|
const assert = require$$0$1;
|
|
9114
9085
|
const net = require$$1;
|
|
9115
|
-
const http = require$$2
|
|
9086
|
+
const http = require$$2;
|
|
9116
9087
|
const util2 = requireUtil$5();
|
|
9117
9088
|
const { ClientStats } = requireStats();
|
|
9118
9089
|
const { channels } = requireDiagnostics();
|
|
@@ -13531,7 +13502,7 @@ function requireMockUtils() {
|
|
|
13531
13502
|
kTotalDispatchCount
|
|
13532
13503
|
} = requireMockSymbols();
|
|
13533
13504
|
const { serializePathWithQuery, parseHeaders } = requireUtil$5();
|
|
13534
|
-
const { STATUS_CODES } = require$$2
|
|
13505
|
+
const { STATUS_CODES } = require$$2;
|
|
13535
13506
|
const {
|
|
13536
13507
|
types: {
|
|
13537
13508
|
isPromise
|
|
@@ -14806,7 +14777,7 @@ function requireSnapshotUtils() {
|
|
|
14806
14777
|
match: new Set(matchHeaders.map((header) => caseSensitive ? header : header.toLowerCase()))
|
|
14807
14778
|
};
|
|
14808
14779
|
}
|
|
14809
|
-
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$
|
|
14780
|
+
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$2 : null;
|
|
14810
14781
|
const hashId = crypto2?.hash ? (value) => crypto2.hash("sha256", value, "base64url") : (value) => Buffer.from(value).toString("base64url");
|
|
14811
14782
|
function isUndiciHeaders(headers2) {
|
|
14812
14783
|
return Array.isArray(headers2) && (headers2.length & 1) === 0;
|
|
@@ -14884,7 +14855,7 @@ function requireSnapshotRecorder() {
|
|
|
14884
14855
|
hasRequiredSnapshotRecorder = 1;
|
|
14885
14856
|
const { writeFile, readFile, mkdir } = require$$0$7;
|
|
14886
14857
|
const { dirname, resolve } = require$$1$3;
|
|
14887
|
-
const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$
|
|
14858
|
+
const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$3;
|
|
14888
14859
|
const { InvalidArgumentError, UndiciError } = requireErrors();
|
|
14889
14860
|
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = requireSnapshotUtils();
|
|
14890
14861
|
function normalizeUrlForMatching(url, matchQuery, normalizeQuery) {
|
|
@@ -19535,7 +19506,7 @@ function requireSqliteCacheStore() {
|
|
|
19535
19506
|
}
|
|
19536
19507
|
}
|
|
19537
19508
|
if (!DatabaseSync) {
|
|
19538
|
-
DatabaseSync = require
|
|
19509
|
+
DatabaseSync = require("node:sqlite").DatabaseSync;
|
|
19539
19510
|
}
|
|
19540
19511
|
this.#db = new DatabaseSync(opts?.location ?? ":memory:");
|
|
19541
19512
|
this.#db.exec(`
|
|
@@ -21500,7 +21471,7 @@ function requireSubresourceIntegrity() {
|
|
|
21500
21471
|
const validSRIHashAlgorithmTokenSet = /* @__PURE__ */ new Map([["sha256", 0], ["sha384", 1], ["sha512", 2]]);
|
|
21501
21472
|
let crypto2;
|
|
21502
21473
|
if (runtimeFeatures2.has("crypto")) {
|
|
21503
|
-
crypto2 = require$$2$
|
|
21474
|
+
crypto2 = require$$2$2;
|
|
21504
21475
|
const cryptoHashes = crypto2.getHashes();
|
|
21505
21476
|
if (cryptoHashes.length === 0) {
|
|
21506
21477
|
validSRIHashAlgorithmTokenSet.clear();
|
|
@@ -21694,7 +21665,7 @@ function requireFetch() {
|
|
|
21694
21665
|
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requireDataUrl();
|
|
21695
21666
|
const { getGlobalDispatcher } = requireGlobal();
|
|
21696
21667
|
const { webidl } = requireWebidl();
|
|
21697
|
-
const { STATUS_CODES } = require$$2
|
|
21668
|
+
const { STATUS_CODES } = require$$2;
|
|
21698
21669
|
const { bytesMatch } = requireSubresourceIntegrity();
|
|
21699
21670
|
const { isomorphicEncode } = requireInfra();
|
|
21700
21671
|
const GET_OR_HEAD = ["GET", "HEAD"];
|
|
@@ -24454,7 +24425,7 @@ function requireFrame() {
|
|
|
24454
24425
|
const BUFFER_SIZE = 8 * 1024;
|
|
24455
24426
|
let buffer = null;
|
|
24456
24427
|
let bufIdx = BUFFER_SIZE;
|
|
24457
|
-
const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$
|
|
24428
|
+
const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$2.randomFillSync : null;
|
|
24458
24429
|
function generateMask() {
|
|
24459
24430
|
if (bufIdx === BUFFER_SIZE) {
|
|
24460
24431
|
bufIdx = 0;
|
|
@@ -24559,7 +24530,7 @@ function requireConnection() {
|
|
|
24559
24530
|
const { WebsocketFrameSend } = requireFrame();
|
|
24560
24531
|
const assert = require$$0$1;
|
|
24561
24532
|
const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
|
|
24562
|
-
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$
|
|
24533
|
+
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$2 : null;
|
|
24563
24534
|
let warningEmitted = false;
|
|
24564
24535
|
function establishWebSocketConnection(url, protocols, client2, handler, options) {
|
|
24565
24536
|
const requestURL = url;
|
|
@@ -27537,13 +27508,56 @@ function schemaViolations(schema, value, path2) {
|
|
|
27537
27508
|
}
|
|
27538
27509
|
return [];
|
|
27539
27510
|
}
|
|
27540
|
-
function
|
|
27541
|
-
|
|
27511
|
+
function tryParseJson(text) {
|
|
27512
|
+
try {
|
|
27513
|
+
return JSON.parse(text);
|
|
27514
|
+
} catch {
|
|
27515
|
+
return null;
|
|
27516
|
+
}
|
|
27517
|
+
}
|
|
27518
|
+
function parseJsonOrExplain(raw, origin) {
|
|
27542
27519
|
try {
|
|
27543
|
-
|
|
27520
|
+
return JSON.parse(raw);
|
|
27544
27521
|
} catch (error) {
|
|
27545
27522
|
throw new Error(`${origin} is not valid JSON: ${error.message}`);
|
|
27546
27523
|
}
|
|
27524
|
+
}
|
|
27525
|
+
function parseJsonLoose(text) {
|
|
27526
|
+
const trimmed = text.trim();
|
|
27527
|
+
const direct = tryParseJson(trimmed);
|
|
27528
|
+
if (direct !== null) {
|
|
27529
|
+
return direct;
|
|
27530
|
+
}
|
|
27531
|
+
return parseBraceSlice(trimmed);
|
|
27532
|
+
}
|
|
27533
|
+
function extractJson(text) {
|
|
27534
|
+
const trimmed = text.trim();
|
|
27535
|
+
const direct = tryParseJson(trimmed);
|
|
27536
|
+
if (direct !== null) {
|
|
27537
|
+
return direct;
|
|
27538
|
+
}
|
|
27539
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
|
|
27540
|
+
if (fenced) {
|
|
27541
|
+
const parsed = tryParseJson(fenced[1].trim());
|
|
27542
|
+
if (parsed !== null) {
|
|
27543
|
+
return parsed;
|
|
27544
|
+
}
|
|
27545
|
+
}
|
|
27546
|
+
return parseBraceSlice(trimmed);
|
|
27547
|
+
}
|
|
27548
|
+
function parseBraceSlice(trimmed) {
|
|
27549
|
+
const first = trimmed.indexOf("{");
|
|
27550
|
+
const last = trimmed.lastIndexOf("}");
|
|
27551
|
+
if (first >= 0 && last > first) {
|
|
27552
|
+
return tryParseJson(trimmed.slice(first, last + 1));
|
|
27553
|
+
}
|
|
27554
|
+
return null;
|
|
27555
|
+
}
|
|
27556
|
+
function truncate(text, max = 300) {
|
|
27557
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
27558
|
+
}
|
|
27559
|
+
function parseExtraBody(raw, origin) {
|
|
27560
|
+
const parsed = parseJsonOrExplain(raw, origin);
|
|
27547
27561
|
if (!isPlainObject(parsed)) {
|
|
27548
27562
|
throw new Error(
|
|
27549
27563
|
`${origin} must be a JSON object, for example {"thinking":{"type":"disabled"}}`
|
|
@@ -27585,47 +27599,6 @@ function hasPath(value, dottedPath) {
|
|
|
27585
27599
|
function isPlainObject(value) {
|
|
27586
27600
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27587
27601
|
}
|
|
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
27602
|
const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
27630
27603
|
const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
|
|
27631
27604
|
const TOOL_NAME = "report_vision_evidence";
|
|
@@ -28176,11 +28149,13 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28176
28149
|
const config2 = loadConfigFile(configPath);
|
|
28177
28150
|
if (dottedKey === "provider") {
|
|
28178
28151
|
config2.provider = value;
|
|
28152
|
+
} else if (dottedKey.startsWith("guards.")) {
|
|
28153
|
+
setGuardsValue(config2, dottedKey.slice("guards.".length), value);
|
|
28179
28154
|
} else {
|
|
28180
28155
|
const dot = dottedKey.indexOf(".");
|
|
28181
28156
|
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
28182
28157
|
throw new Error(
|
|
28183
|
-
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
28158
|
+
`Invalid config key: ${dottedKey}. Use "provider", "guards.<denyModels|denyWhenUnknown>", or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
28184
28159
|
);
|
|
28185
28160
|
}
|
|
28186
28161
|
const providerName = dottedKey.slice(0, dot);
|
|
@@ -28214,6 +28189,38 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28214
28189
|
} catch {
|
|
28215
28190
|
}
|
|
28216
28191
|
}
|
|
28192
|
+
function setGuardsValue(config2, field, value) {
|
|
28193
|
+
if (field === "denyModels") {
|
|
28194
|
+
if (value.trim() === "") {
|
|
28195
|
+
delete config2.guards?.denyModels;
|
|
28196
|
+
} else {
|
|
28197
|
+
config2.guards ??= {};
|
|
28198
|
+
config2.guards.denyModels = parseModelList(value);
|
|
28199
|
+
}
|
|
28200
|
+
} else if (field === "denyWhenUnknown") {
|
|
28201
|
+
const normalized = value.trim().toLowerCase();
|
|
28202
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
28203
|
+
throw new Error("guards.denyWhenUnknown must be true or false.");
|
|
28204
|
+
}
|
|
28205
|
+
config2.guards ??= {};
|
|
28206
|
+
config2.guards.denyWhenUnknown = normalized === "true";
|
|
28207
|
+
} else {
|
|
28208
|
+
throw new Error(`Unknown guards field: ${field}. Use denyModels or denyWhenUnknown.`);
|
|
28209
|
+
}
|
|
28210
|
+
if (config2.guards && Object.keys(config2.guards).length === 0) {
|
|
28211
|
+
delete config2.guards;
|
|
28212
|
+
}
|
|
28213
|
+
}
|
|
28214
|
+
function parseModelList(value) {
|
|
28215
|
+
if (value.trim().startsWith("[")) {
|
|
28216
|
+
const parsed = parseJsonOrExplain(value, "guards.denyModels");
|
|
28217
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
28218
|
+
throw new Error("guards.denyModels must be a JSON array of glob strings.");
|
|
28219
|
+
}
|
|
28220
|
+
return parsed;
|
|
28221
|
+
}
|
|
28222
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
28223
|
+
}
|
|
28217
28224
|
const CONFIG_TEMPLATE = {
|
|
28218
28225
|
// Empty means the built-in default provider.
|
|
28219
28226
|
provider: "",
|
|
@@ -28266,6 +28273,18 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28266
28273
|
if (config2.provider?.trim()) {
|
|
28267
28274
|
effective.provider = config2.provider.trim();
|
|
28268
28275
|
}
|
|
28276
|
+
if (config2.guards) {
|
|
28277
|
+
const guards = {};
|
|
28278
|
+
if (config2.guards.denyModels !== void 0) {
|
|
28279
|
+
guards.denyModels = `${JSON.stringify(config2.guards.denyModels)} (file)`;
|
|
28280
|
+
}
|
|
28281
|
+
if (config2.guards.denyWhenUnknown !== void 0) {
|
|
28282
|
+
guards.denyWhenUnknown = `${config2.guards.denyWhenUnknown} (file)`;
|
|
28283
|
+
}
|
|
28284
|
+
if (Object.keys(guards).length > 0) {
|
|
28285
|
+
effective.guards = guards;
|
|
28286
|
+
}
|
|
28287
|
+
}
|
|
28269
28288
|
return JSON.stringify(effective, null, 2);
|
|
28270
28289
|
}
|
|
28271
28290
|
function maskKey(key) {
|
|
@@ -28508,8 +28527,7 @@ function resolveInput(input) {
|
|
|
28508
28527
|
return { source: trimmed, kind: "remote" };
|
|
28509
28528
|
}
|
|
28510
28529
|
if (/^file:\/\//i.test(trimmed)) {
|
|
28511
|
-
|
|
28512
|
-
return { source: path.resolve(localPath), kind: "local" };
|
|
28530
|
+
return { source: path.resolve(fileURLToPath(trimmed)), kind: "local" };
|
|
28513
28531
|
}
|
|
28514
28532
|
return { source: path.resolve(trimmed), kind: "local" };
|
|
28515
28533
|
}
|
|
@@ -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.1");
|
|
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) => {
|