@liustack/modlens 3.3.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 +11 -0
- package/README.md +17 -0
- package/README.zh-CN.md +17 -0
- package/dist/main.js +828 -265
- package/docs/troubleshooting.md +31 -0
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +35 -10
- package/skills/modlens/references/cli.md +6 -0
- package/skills/modlens/references/configure.md +45 -5
- 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
|
@@ -20,15 +20,14 @@ import require$$5$1 from "node:perf_hooks";
|
|
|
20
20
|
import require$$8 from "node:util/types";
|
|
21
21
|
import require$$2$1 from "node:worker_threads";
|
|
22
22
|
import require$$2$2 from "node:crypto";
|
|
23
|
-
import require$$1$1 from "node:sqlite";
|
|
24
23
|
import require$$11 from "node:stream/web";
|
|
25
24
|
import require$$0$6 from "node:url";
|
|
26
|
-
import require$$1$
|
|
27
|
-
import require$$1$
|
|
25
|
+
import require$$1$1 from "node:async_hooks";
|
|
26
|
+
import require$$1$2 from "node:console";
|
|
28
27
|
import require$$0$7 from "node:fs/promises";
|
|
29
|
-
import require$$1$
|
|
28
|
+
import require$$1$3 from "node:path";
|
|
30
29
|
import require$$2$3 from "node:timers";
|
|
31
|
-
import require$$1$
|
|
30
|
+
import require$$1$4 from "node:dns";
|
|
32
31
|
import * as dns$1 from "dns/promises";
|
|
33
32
|
import { isIP } from "net";
|
|
34
33
|
import { createRequire } from "module";
|
|
@@ -5654,7 +5653,7 @@ function requireRuntimeFeatures() {
|
|
|
5654
5653
|
const lazyLoaders = {
|
|
5655
5654
|
__proto__: null,
|
|
5656
5655
|
"node:crypto": () => require$$2$2,
|
|
5657
|
-
"node:sqlite": () => require
|
|
5656
|
+
"node:sqlite": () => require("node:sqlite")
|
|
5658
5657
|
};
|
|
5659
5658
|
function detectRuntimeFeatureByNodeModule(moduleName) {
|
|
5660
5659
|
try {
|
|
@@ -12557,7 +12556,7 @@ function requireApiRequest() {
|
|
|
12557
12556
|
if (hasRequiredApiRequest) return apiRequest.exports;
|
|
12558
12557
|
hasRequiredApiRequest = 1;
|
|
12559
12558
|
const assert = require$$0$1;
|
|
12560
|
-
const { AsyncResource } = require$$1$
|
|
12559
|
+
const { AsyncResource } = require$$1$1;
|
|
12561
12560
|
const { Readable } = requireReadable();
|
|
12562
12561
|
const { InvalidArgumentError, RequestAbortedError } = requireErrors();
|
|
12563
12562
|
const util2 = requireUtil$5();
|
|
@@ -12810,7 +12809,7 @@ function requireApiStream() {
|
|
|
12810
12809
|
if (hasRequiredApiStream) return apiStream;
|
|
12811
12810
|
hasRequiredApiStream = 1;
|
|
12812
12811
|
const assert = require$$0$1;
|
|
12813
|
-
const { AsyncResource } = require$$1$
|
|
12812
|
+
const { AsyncResource } = require$$1$1;
|
|
12814
12813
|
const { InvalidArgumentError, InvalidReturnValueError } = requireErrors();
|
|
12815
12814
|
const util2 = requireUtil$5();
|
|
12816
12815
|
const { addSignal, removeSignal } = requireAbortSignal();
|
|
@@ -13025,7 +13024,7 @@ function requireApiPipeline() {
|
|
|
13025
13024
|
PassThrough
|
|
13026
13025
|
} = require$$0$2;
|
|
13027
13026
|
const assert = require$$0$1;
|
|
13028
|
-
const { AsyncResource } = require$$1$
|
|
13027
|
+
const { AsyncResource } = require$$1$1;
|
|
13029
13028
|
const {
|
|
13030
13029
|
InvalidArgumentError,
|
|
13031
13030
|
InvalidReturnValueError,
|
|
@@ -13227,7 +13226,7 @@ function requireApiUpgrade() {
|
|
|
13227
13226
|
if (hasRequiredApiUpgrade) return apiUpgrade;
|
|
13228
13227
|
hasRequiredApiUpgrade = 1;
|
|
13229
13228
|
const { InvalidArgumentError, SocketError } = requireErrors();
|
|
13230
|
-
const { AsyncResource } = require$$1$
|
|
13229
|
+
const { AsyncResource } = require$$1$1;
|
|
13231
13230
|
const assert = require$$0$1;
|
|
13232
13231
|
const util2 = requireUtil$5();
|
|
13233
13232
|
const { kHTTP2Stream } = requireSymbols();
|
|
@@ -13327,7 +13326,7 @@ function requireApiConnect() {
|
|
|
13327
13326
|
if (hasRequiredApiConnect) return apiConnect;
|
|
13328
13327
|
hasRequiredApiConnect = 1;
|
|
13329
13328
|
const assert = require$$0$1;
|
|
13330
|
-
const { AsyncResource } = require$$1$
|
|
13329
|
+
const { AsyncResource } = require$$1$1;
|
|
13331
13330
|
const { InvalidArgumentError, SocketError } = requireErrors();
|
|
13332
13331
|
const util2 = requireUtil$5();
|
|
13333
13332
|
const { addSignal, removeSignal } = requireAbortSignal();
|
|
@@ -14543,7 +14542,7 @@ function requirePendingInterceptorsFormatter() {
|
|
|
14543
14542
|
if (hasRequiredPendingInterceptorsFormatter) return pendingInterceptorsFormatter;
|
|
14544
14543
|
hasRequiredPendingInterceptorsFormatter = 1;
|
|
14545
14544
|
const { Transform } = require$$0$2;
|
|
14546
|
-
const { Console } = require$$1$
|
|
14545
|
+
const { Console } = require$$1$2;
|
|
14547
14546
|
const PERSISTENT = process.versions.icu ? "✅" : "Y ";
|
|
14548
14547
|
const NOT_PERSISTENT = process.versions.icu ? "❌" : "N ";
|
|
14549
14548
|
pendingInterceptorsFormatter = class PendingInterceptorsFormatter {
|
|
@@ -14854,7 +14853,7 @@ function requireSnapshotRecorder() {
|
|
|
14854
14853
|
if (hasRequiredSnapshotRecorder) return snapshotRecorder;
|
|
14855
14854
|
hasRequiredSnapshotRecorder = 1;
|
|
14856
14855
|
const { writeFile, readFile, mkdir } = require$$0$7;
|
|
14857
|
-
const { dirname, resolve } = require$$1$
|
|
14856
|
+
const { dirname, resolve } = require$$1$3;
|
|
14858
14857
|
const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$3;
|
|
14859
14858
|
const { InvalidArgumentError, UndiciError } = requireErrors();
|
|
14860
14859
|
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = requireSnapshotUtils();
|
|
@@ -16040,7 +16039,7 @@ function requireDns() {
|
|
|
16040
16039
|
if (hasRequiredDns) return dns;
|
|
16041
16040
|
hasRequiredDns = 1;
|
|
16042
16041
|
const { isIP: isIP2 } = require$$1;
|
|
16043
|
-
const { lookup } = require$$1$
|
|
16042
|
+
const { lookup } = require$$1$4;
|
|
16044
16043
|
const DecoratorHandler = requireDecoratorHandler();
|
|
16045
16044
|
const { InvalidArgumentError, InformationalError } = requireErrors();
|
|
16046
16045
|
const maxInt = Math.pow(2, 31) - 1;
|
|
@@ -19506,7 +19505,7 @@ function requireSqliteCacheStore() {
|
|
|
19506
19505
|
}
|
|
19507
19506
|
}
|
|
19508
19507
|
if (!DatabaseSync) {
|
|
19509
|
-
DatabaseSync = require
|
|
19508
|
+
DatabaseSync = require("node:sqlite").DatabaseSync;
|
|
19510
19509
|
}
|
|
19511
19510
|
this.#db = new DatabaseSync(opts?.location ?? ":memory:");
|
|
19512
19511
|
this.#db.exec(`
|
|
@@ -27515,6 +27514,13 @@ function tryParseJson(text) {
|
|
|
27515
27514
|
return null;
|
|
27516
27515
|
}
|
|
27517
27516
|
}
|
|
27517
|
+
function parseJsonOrExplain(raw, origin) {
|
|
27518
|
+
try {
|
|
27519
|
+
return JSON.parse(raw);
|
|
27520
|
+
} catch (error) {
|
|
27521
|
+
throw new Error(`${origin} is not valid JSON: ${error.message}`);
|
|
27522
|
+
}
|
|
27523
|
+
}
|
|
27518
27524
|
function parseJsonLoose(text) {
|
|
27519
27525
|
const trimmed = text.trim();
|
|
27520
27526
|
const direct = tryParseJson(trimmed);
|
|
@@ -27549,6 +27555,49 @@ function parseBraceSlice(trimmed) {
|
|
|
27549
27555
|
function truncate(text, max = 300) {
|
|
27550
27556
|
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
27551
27557
|
}
|
|
27558
|
+
function parseExtraBody(raw, origin) {
|
|
27559
|
+
const parsed = parseJsonOrExplain(raw, origin);
|
|
27560
|
+
if (!isPlainObject(parsed)) {
|
|
27561
|
+
throw new Error(
|
|
27562
|
+
`${origin} must be a JSON object, for example {"thinking":{"type":"disabled"}}`
|
|
27563
|
+
);
|
|
27564
|
+
}
|
|
27565
|
+
return parsed;
|
|
27566
|
+
}
|
|
27567
|
+
function mergeExtraBody(body2, extra, reserved, providerName) {
|
|
27568
|
+
if (!extra || Object.keys(extra).length === 0) {
|
|
27569
|
+
return body2;
|
|
27570
|
+
}
|
|
27571
|
+
for (const path2 of reserved) {
|
|
27572
|
+
if (hasPath(extra, path2)) {
|
|
27573
|
+
throw new Error(
|
|
27574
|
+
`extraBody cannot override "${path2}" for the ${providerName} provider: it carries the image, the prompt, or the schema this tool depends on. Remove that field from ${providerName}.extraBody (or --extra-body).`
|
|
27575
|
+
);
|
|
27576
|
+
}
|
|
27577
|
+
}
|
|
27578
|
+
return deepMerge(body2, extra);
|
|
27579
|
+
}
|
|
27580
|
+
function deepMerge(base, overlay) {
|
|
27581
|
+
const merged = { ...base };
|
|
27582
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
27583
|
+
const current = merged[key];
|
|
27584
|
+
merged[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
|
|
27585
|
+
}
|
|
27586
|
+
return merged;
|
|
27587
|
+
}
|
|
27588
|
+
function hasPath(value, dottedPath) {
|
|
27589
|
+
let cursor = value;
|
|
27590
|
+
for (const segment of dottedPath.split(".")) {
|
|
27591
|
+
if (!isPlainObject(cursor) || !Object.hasOwn(cursor, segment)) {
|
|
27592
|
+
return false;
|
|
27593
|
+
}
|
|
27594
|
+
cursor = cursor[segment];
|
|
27595
|
+
}
|
|
27596
|
+
return true;
|
|
27597
|
+
}
|
|
27598
|
+
function isPlainObject(value) {
|
|
27599
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27600
|
+
}
|
|
27552
27601
|
const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
27553
27602
|
const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
|
|
27554
27603
|
const TOOL_NAME = "report_vision_evidence";
|
|
@@ -27584,27 +27633,34 @@ Report your findings by calling the ${TOOL_NAME} tool.`;
|
|
|
27584
27633
|
"anthropic-version": "2023-06-01",
|
|
27585
27634
|
"Content-Type": "application/json"
|
|
27586
27635
|
},
|
|
27587
|
-
body: JSON.stringify(
|
|
27588
|
-
|
|
27589
|
-
max_tokens: 4096,
|
|
27590
|
-
tools: [
|
|
27591
|
-
{
|
|
27592
|
-
name: TOOL_NAME,
|
|
27593
|
-
description: "Report the structured visual evidence extracted from the image.",
|
|
27594
|
-
input_schema: VISION_RESULT_SCHEMA
|
|
27595
|
-
}
|
|
27596
|
-
],
|
|
27597
|
-
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27598
|
-
messages: [
|
|
27636
|
+
body: JSON.stringify(
|
|
27637
|
+
mergeExtraBody(
|
|
27599
27638
|
{
|
|
27600
|
-
|
|
27601
|
-
|
|
27602
|
-
|
|
27603
|
-
{
|
|
27639
|
+
model,
|
|
27640
|
+
max_tokens: 4096,
|
|
27641
|
+
tools: [
|
|
27642
|
+
{
|
|
27643
|
+
name: TOOL_NAME,
|
|
27644
|
+
description: "Report the structured visual evidence extracted from the image.",
|
|
27645
|
+
input_schema: VISION_RESULT_SCHEMA
|
|
27646
|
+
}
|
|
27647
|
+
],
|
|
27648
|
+
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27649
|
+
messages: [
|
|
27650
|
+
{
|
|
27651
|
+
role: "user",
|
|
27652
|
+
content: [
|
|
27653
|
+
{ type: "image", source: imageSource },
|
|
27654
|
+
{ type: "text", text: prompt }
|
|
27655
|
+
]
|
|
27656
|
+
}
|
|
27604
27657
|
]
|
|
27605
|
-
}
|
|
27606
|
-
|
|
27607
|
-
|
|
27658
|
+
},
|
|
27659
|
+
options.settings?.extraBody,
|
|
27660
|
+
["model", "messages", "tools", "tool_choice", "stream"],
|
|
27661
|
+
"anthropic"
|
|
27662
|
+
)
|
|
27663
|
+
),
|
|
27608
27664
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27609
27665
|
});
|
|
27610
27666
|
if (!response2.ok) {
|
|
@@ -27871,20 +27927,31 @@ async function executeGeminiApi(options) {
|
|
|
27871
27927
|
"x-goog-api-key": apiKey,
|
|
27872
27928
|
"Content-Type": "application/json"
|
|
27873
27929
|
},
|
|
27874
|
-
body: JSON.stringify(
|
|
27875
|
-
|
|
27930
|
+
body: JSON.stringify(
|
|
27931
|
+
mergeExtraBody(
|
|
27876
27932
|
{
|
|
27877
|
-
|
|
27878
|
-
{
|
|
27879
|
-
|
|
27880
|
-
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27933
|
+
contents: [
|
|
27934
|
+
{
|
|
27935
|
+
parts: [
|
|
27936
|
+
{ inline_data: { mime_type: image.mimeType, data: image.data } },
|
|
27937
|
+
{ text: prompt }
|
|
27938
|
+
]
|
|
27939
|
+
}
|
|
27940
|
+
],
|
|
27941
|
+
generationConfig: {
|
|
27942
|
+
responseMimeType: "application/json",
|
|
27943
|
+
responseJsonSchema: VISION_RESULT_SCHEMA
|
|
27944
|
+
}
|
|
27945
|
+
},
|
|
27946
|
+
options.settings?.extraBody,
|
|
27947
|
+
[
|
|
27948
|
+
"contents",
|
|
27949
|
+
"generationConfig.responseMimeType",
|
|
27950
|
+
"generationConfig.responseJsonSchema"
|
|
27951
|
+
],
|
|
27952
|
+
"gemini-api"
|
|
27953
|
+
)
|
|
27954
|
+
),
|
|
27888
27955
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27889
27956
|
});
|
|
27890
27957
|
if (!response2.ok) {
|
|
@@ -27941,18 +28008,25 @@ Respond with ONE JSON object only, no markdown fences, no commentary. Fill this
|
|
|
27941
28008
|
Authorization: `Bearer ${apiKey}`,
|
|
27942
28009
|
"Content-Type": "application/json"
|
|
27943
28010
|
},
|
|
27944
|
-
body: JSON.stringify(
|
|
27945
|
-
|
|
27946
|
-
messages: [
|
|
28011
|
+
body: JSON.stringify(
|
|
28012
|
+
mergeExtraBody(
|
|
27947
28013
|
{
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
{
|
|
27951
|
-
|
|
28014
|
+
model,
|
|
28015
|
+
messages: [
|
|
28016
|
+
{
|
|
28017
|
+
role: "user",
|
|
28018
|
+
content: [
|
|
28019
|
+
{ type: "image_url", image_url: { url: imageUrl } },
|
|
28020
|
+
{ type: "text", text: prompt }
|
|
28021
|
+
]
|
|
28022
|
+
}
|
|
27952
28023
|
]
|
|
27953
|
-
}
|
|
27954
|
-
|
|
27955
|
-
|
|
28024
|
+
},
|
|
28025
|
+
options.settings?.extraBody,
|
|
28026
|
+
["model", "messages", "stream"],
|
|
28027
|
+
"openai"
|
|
28028
|
+
)
|
|
28029
|
+
),
|
|
27956
28030
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27957
28031
|
});
|
|
27958
28032
|
if (!response2.ok) {
|
|
@@ -28022,6 +28096,7 @@ function providerAliases() {
|
|
|
28022
28096
|
function listProviders() {
|
|
28023
28097
|
return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
|
|
28024
28098
|
}
|
|
28099
|
+
const STRING_FIELDS = ["apiKey", "baseUrl", "model"];
|
|
28025
28100
|
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
28026
28101
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
28027
28102
|
const ENV_BINDINGS = {
|
|
@@ -28073,21 +28148,37 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28073
28148
|
const config2 = loadConfigFile(configPath);
|
|
28074
28149
|
if (dottedKey === "provider") {
|
|
28075
28150
|
config2.provider = value;
|
|
28151
|
+
} else if (dottedKey.startsWith("guards.")) {
|
|
28152
|
+
setGuardsValue(config2, dottedKey.slice("guards.".length), value);
|
|
28076
28153
|
} else {
|
|
28077
28154
|
const dot = dottedKey.indexOf(".");
|
|
28078
28155
|
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
28079
28156
|
throw new Error(
|
|
28080
|
-
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model>".`
|
|
28157
|
+
`Invalid config key: ${dottedKey}. Use "provider", "guards.<denyModels|denyWhenUnknown>", or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
28081
28158
|
);
|
|
28082
28159
|
}
|
|
28083
28160
|
const providerName = dottedKey.slice(0, dot);
|
|
28084
28161
|
const field = dottedKey.slice(dot + 1);
|
|
28085
|
-
if (
|
|
28086
|
-
|
|
28162
|
+
if (field === "extraBody") {
|
|
28163
|
+
config2.providers ??= {};
|
|
28164
|
+
config2.providers[providerName] ??= {};
|
|
28165
|
+
if (value.trim() === "") {
|
|
28166
|
+
delete config2.providers[providerName].extraBody;
|
|
28167
|
+
} else {
|
|
28168
|
+
config2.providers[providerName].extraBody = parseExtraBody(
|
|
28169
|
+
value,
|
|
28170
|
+
`${providerName}.extraBody`
|
|
28171
|
+
);
|
|
28172
|
+
}
|
|
28173
|
+
} else if (!STRING_FIELDS.includes(field)) {
|
|
28174
|
+
throw new Error(
|
|
28175
|
+
`Unknown config field: ${field}. Use apiKey, baseUrl, model, or extraBody.`
|
|
28176
|
+
);
|
|
28177
|
+
} else {
|
|
28178
|
+
config2.providers ??= {};
|
|
28179
|
+
config2.providers[providerName] ??= {};
|
|
28180
|
+
config2.providers[providerName][field] = value;
|
|
28087
28181
|
}
|
|
28088
|
-
config2.providers ??= {};
|
|
28089
|
-
config2.providers[providerName] ??= {};
|
|
28090
|
-
config2.providers[providerName][field] = value;
|
|
28091
28182
|
}
|
|
28092
28183
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
28093
28184
|
fs.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
@@ -28097,6 +28188,38 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28097
28188
|
} catch {
|
|
28098
28189
|
}
|
|
28099
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
|
+
}
|
|
28100
28223
|
const CONFIG_TEMPLATE = {
|
|
28101
28224
|
// Empty means the built-in default provider.
|
|
28102
28225
|
provider: "",
|
|
@@ -28126,7 +28249,7 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28126
28249
|
const fileSettings = config2.providers?.[name] ?? {};
|
|
28127
28250
|
const bindings = ENV_BINDINGS[name] ?? {};
|
|
28128
28251
|
const fields = {};
|
|
28129
|
-
for (const field of
|
|
28252
|
+
for (const field of STRING_FIELDS) {
|
|
28130
28253
|
const envName = bindings[field];
|
|
28131
28254
|
const envValue = envName ? env[envName]?.trim() : void 0;
|
|
28132
28255
|
const value = envValue ?? fileSettings[field];
|
|
@@ -28136,6 +28259,9 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28136
28259
|
fields[field] = `${shown} (${source})`;
|
|
28137
28260
|
}
|
|
28138
28261
|
}
|
|
28262
|
+
if (fileSettings.extraBody !== void 0) {
|
|
28263
|
+
fields.extraBody = `${JSON.stringify(fileSettings.extraBody)} (file)`;
|
|
28264
|
+
}
|
|
28139
28265
|
if (Object.keys(fields).length > 0) {
|
|
28140
28266
|
providers[name] = fields;
|
|
28141
28267
|
}
|
|
@@ -28146,6 +28272,18 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28146
28272
|
if (config2.provider?.trim()) {
|
|
28147
28273
|
effective.provider = config2.provider.trim();
|
|
28148
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
|
+
}
|
|
28149
28287
|
return JSON.stringify(effective, null, 2);
|
|
28150
28288
|
}
|
|
28151
28289
|
function maskKey(key) {
|
|
@@ -28273,7 +28411,8 @@ async function analyzeImage(options) {
|
|
|
28273
28411
|
options,
|
|
28274
28412
|
resolvedInput,
|
|
28275
28413
|
timeoutMs,
|
|
28276
|
-
config2
|
|
28414
|
+
config2,
|
|
28415
|
+
warnings
|
|
28277
28416
|
);
|
|
28278
28417
|
attempts.push({
|
|
28279
28418
|
provider: provider.name,
|
|
@@ -28323,8 +28462,14 @@ async function analyzeImage(options) {
|
|
|
28323
28462
|
`Every configured vision provider failed for this image. ${attempts.map((attempt) => `${attempt.provider}: ${attempt.error}`).join(" | ")}`
|
|
28324
28463
|
);
|
|
28325
28464
|
}
|
|
28326
|
-
async function runProvider(provider, model, options, resolvedInput, timeoutMs, config2) {
|
|
28327
|
-
const
|
|
28465
|
+
async function runProvider(provider, model, options, resolvedInput, timeoutMs, config2, warnings) {
|
|
28466
|
+
const configured = resolveProviderSettings(provider.name, config2);
|
|
28467
|
+
const settings = options.extraBody ? { ...configured, extraBody: options.extraBody } : configured;
|
|
28468
|
+
if (settings.extraBody && !provider.execute) {
|
|
28469
|
+
warnings.push(
|
|
28470
|
+
`${provider.name} is a CLI provider and takes no request body, so extraBody was ignored for this run.`
|
|
28471
|
+
);
|
|
28472
|
+
}
|
|
28328
28473
|
const providerOptions = {
|
|
28329
28474
|
imageSource: resolvedInput.source,
|
|
28330
28475
|
imageKind: resolvedInput.kind,
|
|
@@ -28579,192 +28724,15 @@ function detectHarnessDetailed() {
|
|
|
28579
28724
|
function detectHarness() {
|
|
28580
28725
|
return detectHarnessDetailed().harness;
|
|
28581
28726
|
}
|
|
28582
|
-
|
|
28583
|
-
|
|
28584
|
-
const
|
|
28585
|
-
if (
|
|
28586
|
-
return
|
|
28587
|
-
}
|
|
28588
|
-
return [Number(match[1]), Number(match[2])];
|
|
28589
|
-
}
|
|
28590
|
-
function meetsMinimum(version, minimum) {
|
|
28591
|
-
const [major, minor] = versionParts(version);
|
|
28592
|
-
const [minMajor, minMinor] = versionParts(minimum);
|
|
28593
|
-
return major > minMajor || major === minMajor && minor >= minMinor;
|
|
28594
|
-
}
|
|
28595
|
-
function checkNodeSqlite() {
|
|
28596
|
-
const realEmit = process.emitWarning;
|
|
28597
|
-
process.emitWarning = () => {
|
|
28598
|
-
};
|
|
28599
|
-
try {
|
|
28600
|
-
const mod = createRequire(import.meta.url)("node:sqlite");
|
|
28601
|
-
if (mod?.DatabaseSync) {
|
|
28602
|
-
return {
|
|
28603
|
-
available: true,
|
|
28604
|
-
detail: "node:sqlite is available (OpenCode paste recovery)"
|
|
28605
|
-
};
|
|
28606
|
-
}
|
|
28607
|
-
return { available: false, detail: "node:sqlite loaded but DatabaseSync is missing" };
|
|
28608
|
-
} catch {
|
|
28609
|
-
return {
|
|
28610
|
-
available: false,
|
|
28611
|
-
detail: "node:sqlite unavailable. Upgrade Node to 22.13+ for OpenCode paste recovery"
|
|
28612
|
-
};
|
|
28613
|
-
} finally {
|
|
28614
|
-
process.emitWarning = realEmit;
|
|
28615
|
-
}
|
|
28616
|
-
}
|
|
28617
|
-
function inspectProvider(descriptor, config2, env) {
|
|
28618
|
-
if (descriptor.kind === "subprocess") {
|
|
28619
|
-
const binaryPath = findOnPath(descriptor.bin, env);
|
|
28620
|
-
return {
|
|
28621
|
-
name: descriptor.name,
|
|
28622
|
-
kind: "subprocess",
|
|
28623
|
-
ready: binaryPath !== null,
|
|
28624
|
-
binaryPath,
|
|
28625
|
-
detail: binaryPath ? `${descriptor.bin} found at ${binaryPath}` : `${descriptor.bin} not on PATH`,
|
|
28626
|
-
fix: binaryPath ? void 0 : descriptor.install
|
|
28627
|
-
};
|
|
28628
|
-
}
|
|
28629
|
-
const settings = resolveProviderSettings(descriptor.name, config2, env);
|
|
28630
|
-
const statuses = (descriptor.required ?? []).map((req) => {
|
|
28631
|
-
const envValue = req.env ? env[req.env]?.trim() : void 0;
|
|
28632
|
-
const value = settings[req.field]?.trim();
|
|
28633
|
-
const source = envValue ? "env" : value ? "file" : "missing";
|
|
28634
|
-
return { field: req.field, present: Boolean(value), source, env: req.env };
|
|
28635
|
-
});
|
|
28636
|
-
const missing = statuses.filter((s) => !s.present).map((s) => s.field);
|
|
28637
|
-
const ready = missing.length === 0;
|
|
28638
|
-
const detail = ready ? statuses.map((s) => `${s.field}: ${s.source}`).join(", ") : `missing: ${missing.join(", ")}`;
|
|
28639
|
-
return {
|
|
28640
|
-
name: descriptor.name,
|
|
28641
|
-
kind: "api",
|
|
28642
|
-
ready,
|
|
28643
|
-
settings: statuses,
|
|
28644
|
-
detail,
|
|
28645
|
-
fix: ready ? void 0 : descriptor.fix
|
|
28646
|
-
};
|
|
28647
|
-
}
|
|
28648
|
-
function resolveSelection(config2, providerFlag) {
|
|
28649
|
-
const raw = providerFlag?.trim() || config2.provider?.trim() || "antigravity-cli";
|
|
28650
|
-
const source = providerFlag?.trim() ? "flag" : config2.provider?.trim() ? "config" : "default";
|
|
28651
|
-
let canonical;
|
|
28652
|
-
try {
|
|
28653
|
-
canonical = resolveProvider(raw).name;
|
|
28654
|
-
} catch {
|
|
28655
|
-
canonical = null;
|
|
28656
|
-
}
|
|
28657
|
-
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)";
|
|
28658
|
-
return { provider: raw, canonical, source, reason };
|
|
28659
|
-
}
|
|
28660
|
-
function inspectConfigFile(configPath) {
|
|
28661
|
-
try {
|
|
28662
|
-
const stat = fs.statSync(configPath);
|
|
28663
|
-
const mode = stat.mode & 511;
|
|
28664
|
-
const enforcesPosixPerms = typeof process.getuid === "function";
|
|
28665
|
-
const permissionsOk = !enforcesPosixPerms || (mode & 63) === 0;
|
|
28666
|
-
return {
|
|
28667
|
-
path: configPath,
|
|
28668
|
-
exists: true,
|
|
28669
|
-
mode: mode.toString(8).padStart(3, "0"),
|
|
28670
|
-
permissionsOk,
|
|
28671
|
-
note: permissionsOk ? void 0 : "group/world can read this file. Run: chmod 600 to lock it down"
|
|
28672
|
-
};
|
|
28673
|
-
} catch (error) {
|
|
28674
|
-
if (error.code === "ENOENT") {
|
|
28675
|
-
return {
|
|
28676
|
-
path: configPath,
|
|
28677
|
-
exists: false,
|
|
28678
|
-
mode: null,
|
|
28679
|
-
permissionsOk: true,
|
|
28680
|
-
note: "no config file (using env vars and built-in defaults)"
|
|
28681
|
-
};
|
|
28682
|
-
}
|
|
28683
|
-
return {
|
|
28684
|
-
path: configPath,
|
|
28685
|
-
exists: true,
|
|
28686
|
-
mode: null,
|
|
28687
|
-
permissionsOk: false,
|
|
28688
|
-
note: `cannot stat: ${error.message}`
|
|
28689
|
-
};
|
|
28690
|
-
}
|
|
28691
|
-
}
|
|
28692
|
-
function buildDoctorReport(input) {
|
|
28693
|
-
const env = input.env ?? process.env;
|
|
28694
|
-
const configPath = input.configPath ?? CONFIG_PATH;
|
|
28695
|
-
return {
|
|
28696
|
-
node: {
|
|
28697
|
-
version: process.version,
|
|
28698
|
-
minimum: MIN_NODE,
|
|
28699
|
-
meetsMinimum: meetsMinimum(process.version, MIN_NODE)
|
|
28700
|
-
},
|
|
28701
|
-
nodeSqlite: checkNodeSqlite(),
|
|
28702
|
-
providers: PROVIDER_DESCRIPTORS.map((d) => inspectProvider(d, input.config, env)),
|
|
28703
|
-
selection: resolveSelection(input.config, input.providerFlag),
|
|
28704
|
-
chains: {
|
|
28705
|
-
local: providerChain("local", input.config, env).map((p) => p.name),
|
|
28706
|
-
remote: providerChain("remote", input.config, env).map((p) => p.name)
|
|
28707
|
-
},
|
|
28708
|
-
harness: (() => {
|
|
28709
|
-
const detection = detectHarnessDetailed();
|
|
28710
|
-
return { detected: detection.harness, source: detection.source };
|
|
28711
|
-
})(),
|
|
28712
|
-
config: inspectConfigFile(configPath)
|
|
28713
|
-
};
|
|
28714
|
-
}
|
|
28715
|
-
function mark(ok) {
|
|
28716
|
-
return ok ? "[ok]" : "[!!]";
|
|
28717
|
-
}
|
|
28718
|
-
function renderDoctorReport(report) {
|
|
28719
|
-
const lines = [];
|
|
28720
|
-
lines.push("modlens doctor");
|
|
28721
|
-
lines.push("(local diagnostics only: no network calls, no provider quota spent)");
|
|
28722
|
-
lines.push("");
|
|
28723
|
-
lines.push("Node");
|
|
28724
|
-
lines.push(
|
|
28725
|
-
` ${mark(report.node.meetsMinimum)} ${report.node.version} (minimum ${report.node.minimum})`
|
|
28726
|
-
);
|
|
28727
|
-
lines.push(` ${mark(report.nodeSqlite.available)} ${report.nodeSqlite.detail}`);
|
|
28728
|
-
lines.push("");
|
|
28729
|
-
lines.push("Providers");
|
|
28730
|
-
for (const provider of report.providers) {
|
|
28731
|
-
lines.push(` ${mark(provider.ready)} ${provider.name}: ${provider.detail}`);
|
|
28732
|
-
if (provider.fix) {
|
|
28733
|
-
lines.push(` fix: ${provider.fix}`);
|
|
28734
|
-
}
|
|
28735
|
-
}
|
|
28736
|
-
lines.push("");
|
|
28737
|
-
lines.push("Selected provider");
|
|
28738
|
-
const canonicalNote = report.selection.canonical && report.selection.canonical !== report.selection.provider ? ` (canonical: ${report.selection.canonical})` : report.selection.canonical === null ? " (unknown provider name)" : "";
|
|
28739
|
-
lines.push(` ${report.selection.provider}${canonicalNote}`);
|
|
28740
|
-
lines.push(` reason: ${report.selection.reason}`);
|
|
28741
|
-
lines.push("");
|
|
28742
|
-
lines.push("Failover chains (what a run tries, in order)");
|
|
28743
|
-
const chainLine = (chain) => chain.length > 0 ? chain.join(" -> ") : "(none available)";
|
|
28744
|
-
lines.push(` local: ${chainLine(report.chains.local)}`);
|
|
28745
|
-
lines.push(` remote: ${chainLine(report.chains.remote)}`);
|
|
28746
|
-
lines.push("");
|
|
28747
|
-
lines.push("Harness");
|
|
28748
|
-
lines.push(
|
|
28749
|
-
report.harness.detected ? ` ${report.harness.detected} (via ${report.harness.source})` : ` none detected (${report.harness.source})`
|
|
28750
|
-
);
|
|
28751
|
-
lines.push("");
|
|
28752
|
-
lines.push("Config file");
|
|
28753
|
-
lines.push(` path: ${report.config.path}`);
|
|
28754
|
-
if (report.config.exists) {
|
|
28755
|
-
lines.push(
|
|
28756
|
-
` ${mark(report.config.permissionsOk)} exists, mode ${report.config.mode ?? "?"}`
|
|
28757
|
-
);
|
|
28758
|
-
} else {
|
|
28759
|
-
lines.push(" not present");
|
|
28760
|
-
}
|
|
28761
|
-
if (report.config.note) {
|
|
28762
|
-
lines.push(` note: ${report.config.note}`);
|
|
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;
|
|
28763
28732
|
}
|
|
28764
|
-
return
|
|
28733
|
+
return bothDirections && resolvedWanted.startsWith(`${resolvedRecorded}${path.sep}`);
|
|
28765
28734
|
}
|
|
28766
|
-
function transcriptBelongsTo(lines, cwd) {
|
|
28767
|
-
const wanted = path.resolve(cwd);
|
|
28735
|
+
function transcriptBelongsTo(lines, cwd, bothDirections = false) {
|
|
28768
28736
|
let sawCwd = false;
|
|
28769
28737
|
for (const line of lines) {
|
|
28770
28738
|
if (!line.includes('"cwd"')) {
|
|
@@ -28776,8 +28744,7 @@ function transcriptBelongsTo(lines, cwd) {
|
|
|
28776
28744
|
continue;
|
|
28777
28745
|
}
|
|
28778
28746
|
sawCwd = true;
|
|
28779
|
-
|
|
28780
|
-
if (resolved === wanted || resolved.startsWith(`${wanted}${path.sep}`)) {
|
|
28747
|
+
if (cwdMatches(recorded, cwd, bothDirections)) {
|
|
28781
28748
|
return true;
|
|
28782
28749
|
}
|
|
28783
28750
|
} catch {
|
|
@@ -28844,8 +28811,17 @@ function listJsonl(dir) {
|
|
|
28844
28811
|
return [];
|
|
28845
28812
|
}
|
|
28846
28813
|
}
|
|
28847
|
-
function
|
|
28848
|
-
|
|
28814
|
+
function listJsonlByMtimeDesc(dir) {
|
|
28815
|
+
return listJsonl(dir).map((file) => {
|
|
28816
|
+
try {
|
|
28817
|
+
return { file, mtime: fs.statSync(file).mtimeMs };
|
|
28818
|
+
} catch {
|
|
28819
|
+
return null;
|
|
28820
|
+
}
|
|
28821
|
+
}).filter((entry) => entry !== null).sort((a, b) => b.mtime - a.mtime).map((entry) => entry.file);
|
|
28822
|
+
}
|
|
28823
|
+
function jsonlAdapter(options) {
|
|
28824
|
+
const { name, dirFor, matchesSession, extractLine } = options;
|
|
28849
28825
|
return {
|
|
28850
28826
|
name,
|
|
28851
28827
|
describe: (cwd) => dirFor(cwd),
|
|
@@ -28906,13 +28882,19 @@ function opencodeDbPath() {
|
|
|
28906
28882
|
function escapeLikePattern(value) {
|
|
28907
28883
|
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
28908
28884
|
}
|
|
28909
|
-
function
|
|
28885
|
+
function opencodeDirectoryFilter(resolvedCwd) {
|
|
28910
28886
|
const normalized = resolvedCwd.replace(/\\/g, "/");
|
|
28911
28887
|
const escaped = escapeLikePattern(normalized);
|
|
28912
28888
|
const dir = `REPLACE(session.directory, '\\', '/')`;
|
|
28913
|
-
|
|
28914
|
-
|
|
28915
|
-
|
|
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;
|
|
28916
28898
|
const sql = `SELECT part.data AS data, part.time_created AS time_created, part.session_id AS session_id
|
|
28917
28899
|
FROM part
|
|
28918
28900
|
JOIN message ON message.id = part.message_id
|
|
@@ -28923,12 +28905,17 @@ function buildOpencodeQuery(resolvedCwd, sessionId) {
|
|
|
28923
28905
|
ORDER BY part.time_created ASC`;
|
|
28924
28906
|
return { sql, params };
|
|
28925
28907
|
}
|
|
28926
|
-
function
|
|
28927
|
-
let DatabaseSync;
|
|
28908
|
+
function loadNodeSqlite() {
|
|
28928
28909
|
try {
|
|
28929
28910
|
const nodeRequire = createRequire(import.meta.url);
|
|
28930
|
-
|
|
28911
|
+
return nodeRequire("node:sqlite").DatabaseSync;
|
|
28931
28912
|
} catch {
|
|
28913
|
+
return null;
|
|
28914
|
+
}
|
|
28915
|
+
}
|
|
28916
|
+
function opencodeQuery(dbPath, cwd, sessionId) {
|
|
28917
|
+
const DatabaseSync = loadNodeSqlite();
|
|
28918
|
+
if (!DatabaseSync) {
|
|
28932
28919
|
throw new Error(
|
|
28933
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."
|
|
28934
28921
|
);
|
|
@@ -29029,6 +29016,541 @@ const piAdapter = jsonlAdapter({
|
|
|
29029
29016
|
matchesSession: (fileName, sessionId) => fileName.endsWith(`_${sessionId}.jsonl`),
|
|
29030
29017
|
extractLine: piExtractLine
|
|
29031
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
|
+
}
|
|
29032
29554
|
function extensionFromMediaType(mediaType) {
|
|
29033
29555
|
const subtype = mediaType.split("/")[1]?.split("+")[0]?.replace(/[^a-z0-9]/gi, "");
|
|
29034
29556
|
return subtype ? subtype.toLowerCase() : "bin";
|
|
@@ -29207,13 +29729,28 @@ function recoverPastedImages(options = {}) {
|
|
|
29207
29729
|
return result;
|
|
29208
29730
|
}
|
|
29209
29731
|
const program = new Command();
|
|
29210
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.
|
|
29211
|
-
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").
|
|
29732
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.5.0");
|
|
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(
|
|
29734
|
+
"--extra-body <json>",
|
|
29735
|
+
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
29736
|
+
).action(async (options) => {
|
|
29212
29737
|
try {
|
|
29213
29738
|
const timeoutMs = Number.parseInt(options.timeout, 10);
|
|
29214
29739
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
29215
29740
|
throw new Error("Invalid --timeout. Use a positive integer in milliseconds.");
|
|
29216
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
|
+
}
|
|
29217
29754
|
const result = await analyzeImage({
|
|
29218
29755
|
input: options.input,
|
|
29219
29756
|
provider: options.provider,
|
|
@@ -29221,7 +29758,9 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
|
|
|
29221
29758
|
prompt: options.prompt,
|
|
29222
29759
|
timeoutMs,
|
|
29223
29760
|
providerBin: options.providerBin,
|
|
29224
|
-
workdir: options.workdir
|
|
29761
|
+
workdir: options.workdir,
|
|
29762
|
+
extraBody: options.extraBody ? parseExtraBody(options.extraBody, "--extra-body") : void 0,
|
|
29763
|
+
config: config2
|
|
29225
29764
|
});
|
|
29226
29765
|
const output = JSON.stringify(result, null, 2);
|
|
29227
29766
|
if (options.output) {
|
|
@@ -29271,6 +29810,29 @@ program.command("recover-paste").description(
|
|
|
29271
29810
|
process.exit(1);
|
|
29272
29811
|
}
|
|
29273
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
|
+
});
|
|
29274
29836
|
program.command("doctor").description(
|
|
29275
29837
|
"Diagnose local config and routing (Node, providers, selection, harness) without spending quota or hitting the network"
|
|
29276
29838
|
).option("--json", "Emit the report as JSON").option("-p, --provider <name>", "Show which provider this -p value would select").action((options) => {
|
|
@@ -29302,6 +29864,7 @@ config.command("init").description(`Create a starter config at ${CONFIG_PATH}`).
|
|
|
29302
29864
|
"Everything is optional. Two things you can set:",
|
|
29303
29865
|
" modlens config set provider <name> which provider analyzes images",
|
|
29304
29866
|
" modlens config set <provider>.<apiKey|baseUrl|model> <value> provider credentials",
|
|
29867
|
+
` modlens config set <provider>.extraBody '{"thinking":{"type":"disabled"}}' vendor request fields`,
|
|
29305
29868
|
""
|
|
29306
29869
|
].join("\n")
|
|
29307
29870
|
);
|