@liustack/modlens 3.10.0 → 3.12.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 +16 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/dist/main.js +306 -170
- package/docs/cli.md +5 -3
- package/docs/harness-setup.md +3 -3
- package/docs/output-schema.md +1 -1
- package/docs/security.md +9 -1
- package/docs/troubleshooting.md +58 -0
- package/dsh/index.js +109 -17
- package/package.json +2 -2
- package/skills/modlens/SKILL.md +5 -5
- package/skills/modlens/references/configure.md +2 -1
- package/skills/modlens/references/find-image.md +1 -1
- package/skills/modlens/references/onboard.md +4 -4
- 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
|
@@ -13672,7 +13672,7 @@ function requireMockUtils() {
|
|
|
13672
13672
|
}
|
|
13673
13673
|
return normalizedQp;
|
|
13674
13674
|
}
|
|
13675
|
-
function
|
|
13675
|
+
function safeUrl2(path2) {
|
|
13676
13676
|
if (typeof path2 !== "string") {
|
|
13677
13677
|
return path2;
|
|
13678
13678
|
}
|
|
@@ -13710,10 +13710,10 @@ function requireMockUtils() {
|
|
|
13710
13710
|
}
|
|
13711
13711
|
function getMockDispatch(mockDispatches, key) {
|
|
13712
13712
|
const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path;
|
|
13713
|
-
const resolvedPath = typeof basePath === "string" ?
|
|
13713
|
+
const resolvedPath = typeof basePath === "string" ? safeUrl2(basePath) : basePath;
|
|
13714
13714
|
const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath);
|
|
13715
13715
|
let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path2, ignoreTrailingSlash }) => {
|
|
13716
|
-
return ignoreTrailingSlash ? matchValue(removeTrailingSlash(
|
|
13716
|
+
return ignoreTrailingSlash ? matchValue(removeTrailingSlash(safeUrl2(path2)), resolvedPathWithoutTrailingSlash) : matchValue(safeUrl2(path2), resolvedPath);
|
|
13717
13717
|
});
|
|
13718
13718
|
if (matchedMockDispatches.length === 0) {
|
|
13719
13719
|
throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`);
|
|
@@ -27236,15 +27236,6 @@ function parseIpv6Groups(groups) {
|
|
|
27236
27236
|
}
|
|
27237
27237
|
return parsed;
|
|
27238
27238
|
}
|
|
27239
|
-
const MIME_BY_EXT = {
|
|
27240
|
-
".jpg": "image/jpeg",
|
|
27241
|
-
".jpeg": "image/jpeg",
|
|
27242
|
-
".png": "image/png",
|
|
27243
|
-
".webp": "image/webp",
|
|
27244
|
-
".gif": "image/gif",
|
|
27245
|
-
".heic": "image/heic",
|
|
27246
|
-
".heif": "image/heif"
|
|
27247
|
-
};
|
|
27248
27239
|
const MAX_REMOTE_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
27249
27240
|
const ALLOWED_MIME = /* @__PURE__ */ new Set([
|
|
27250
27241
|
"image/png",
|
|
@@ -27270,8 +27261,27 @@ const SNIFFERS = [
|
|
|
27270
27261
|
{
|
|
27271
27262
|
mime: "image/webp",
|
|
27272
27263
|
test: (b) => b.length >= 12 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP"
|
|
27264
|
+
},
|
|
27265
|
+
// ISO BMFF: bytes 4-8 spell "ftyp" and the brand names the format. This
|
|
27266
|
+
// closes the last extension-trust hole: heic/heif must now prove
|
|
27267
|
+
// themselves from the header like every other type.
|
|
27268
|
+
{
|
|
27269
|
+
mime: "image/heic",
|
|
27270
|
+
test: (b) => b.length >= 12 && b.toString("ascii", 4, 8) === "ftyp" && ["heic", "heix", "hevc", "hevx"].includes(b.toString("ascii", 8, 12))
|
|
27271
|
+
},
|
|
27272
|
+
{
|
|
27273
|
+
mime: "image/heif",
|
|
27274
|
+
test: (b) => b.length >= 12 && b.toString("ascii", 4, 8) === "ftyp" && ["mif1", "msf1", "heif"].includes(b.toString("ascii", 8, 12))
|
|
27273
27275
|
}
|
|
27274
27276
|
];
|
|
27277
|
+
function safeUrl(url) {
|
|
27278
|
+
try {
|
|
27279
|
+
const u = new URL(url);
|
|
27280
|
+
return `${u.origin}${u.pathname}`;
|
|
27281
|
+
} catch {
|
|
27282
|
+
return "<unparseable url>";
|
|
27283
|
+
}
|
|
27284
|
+
}
|
|
27275
27285
|
function sniffImageMime(buffer) {
|
|
27276
27286
|
for (const { mime, test } of SNIFFERS) {
|
|
27277
27287
|
if (test(buffer)) {
|
|
@@ -27280,22 +27290,13 @@ function sniffImageMime(buffer) {
|
|
|
27280
27290
|
}
|
|
27281
27291
|
return null;
|
|
27282
27292
|
}
|
|
27283
|
-
function
|
|
27284
|
-
const ext = /^https?:\/\//i.test(source) ? path.extname(new URL(source).pathname).toLowerCase() : path.extname(source).toLowerCase();
|
|
27285
|
-
return MIME_BY_EXT[ext] ?? null;
|
|
27286
|
-
}
|
|
27287
|
-
function resolveImageMime(buffer, source, contentType) {
|
|
27293
|
+
function resolveImageMime(buffer, source, _contentType) {
|
|
27288
27294
|
const sniffed = sniffImageMime(buffer);
|
|
27289
27295
|
if (sniffed) {
|
|
27290
27296
|
return sniffed;
|
|
27291
27297
|
}
|
|
27292
|
-
const declared = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
27293
|
-
const candidate = extMime(source) ?? (declared?.startsWith("image/") ? declared : null);
|
|
27294
|
-
if (candidate && ALLOWED_MIME.has(candidate)) {
|
|
27295
|
-
return candidate;
|
|
27296
|
-
}
|
|
27297
27298
|
throw new Error(
|
|
27298
|
-
`
|
|
27299
|
+
`Content of ${source} does not look like a supported image (its bytes match no known image header). Allowed types: ${[...ALLOWED_MIME].join(", ")}.`
|
|
27299
27300
|
);
|
|
27300
27301
|
}
|
|
27301
27302
|
function readLocalImageBase64(filePath) {
|
|
@@ -27331,32 +27332,34 @@ async function fetchRemoteImageBase64(url, timeoutMs) {
|
|
|
27331
27332
|
const location = response2.headers.get("location");
|
|
27332
27333
|
if (!location) {
|
|
27333
27334
|
throw new Error(
|
|
27334
|
-
`Redirect response (${response2.status}) missing location header: ${current}`
|
|
27335
|
+
`Redirect response (${response2.status}) missing location header: ${safeUrl(current.toString())}`
|
|
27335
27336
|
);
|
|
27336
27337
|
}
|
|
27337
27338
|
await response2.body?.cancel();
|
|
27338
27339
|
if (hop === MAX_REDIRECTS) {
|
|
27339
|
-
throw new Error(`Too many redirects (max ${MAX_REDIRECTS}): ${url}`);
|
|
27340
|
+
throw new Error(`Too many redirects (max ${MAX_REDIRECTS}): ${safeUrl(url)}`);
|
|
27340
27341
|
}
|
|
27341
27342
|
current = normalizeRemoteImageUrl(new URL(location, current).toString());
|
|
27342
27343
|
continue;
|
|
27343
27344
|
}
|
|
27344
27345
|
if (!response2.ok) {
|
|
27345
|
-
throw new Error(
|
|
27346
|
+
throw new Error(
|
|
27347
|
+
`Failed to download image (${response2.status}): ${safeUrl(current.toString())}`
|
|
27348
|
+
);
|
|
27346
27349
|
}
|
|
27347
27350
|
const declaredLength = Number(response2.headers.get("content-length"));
|
|
27348
27351
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_IMAGE_BYTES) {
|
|
27349
27352
|
throw new Error(
|
|
27350
|
-
`Remote image is ${declaredLength} bytes, over the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${current}`
|
|
27353
|
+
`Remote image is ${declaredLength} bytes, over the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${safeUrl(current.toString())}`
|
|
27351
27354
|
);
|
|
27352
27355
|
}
|
|
27353
27356
|
const finalUrl = current.toString();
|
|
27354
27357
|
const buffer = await readCapped(response2, finalUrl);
|
|
27355
27358
|
const contentType = response2.headers.get("content-type") ?? void 0;
|
|
27356
|
-
const mimeType = resolveImageMime(buffer, finalUrl, contentType);
|
|
27359
|
+
const mimeType = resolveImageMime(buffer, safeUrl(finalUrl), contentType);
|
|
27357
27360
|
return { data: buffer.toString("base64"), mimeType };
|
|
27358
27361
|
}
|
|
27359
|
-
throw new Error(`Too many redirects (max ${MAX_REDIRECTS}): ${url}`);
|
|
27362
|
+
throw new Error(`Too many redirects (max ${MAX_REDIRECTS}): ${safeUrl(url)}`);
|
|
27360
27363
|
} finally {
|
|
27361
27364
|
for (const dispatcher2 of dispatchers) {
|
|
27362
27365
|
void dispatcher2.close();
|
|
@@ -27387,7 +27390,7 @@ async function readCapped(response2, url) {
|
|
|
27387
27390
|
const buffer = Buffer.from(await response2.arrayBuffer());
|
|
27388
27391
|
if (buffer.length > MAX_REMOTE_IMAGE_BYTES) {
|
|
27389
27392
|
throw new Error(
|
|
27390
|
-
`Remote image exceeds the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${url}`
|
|
27393
|
+
`Remote image exceeds the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${safeUrl(url)}`
|
|
27391
27394
|
);
|
|
27392
27395
|
}
|
|
27393
27396
|
return buffer;
|
|
@@ -27404,13 +27407,58 @@ async function readCapped(response2, url) {
|
|
|
27404
27407
|
if (total > MAX_REMOTE_IMAGE_BYTES) {
|
|
27405
27408
|
await reader.cancel();
|
|
27406
27409
|
throw new Error(
|
|
27407
|
-
`Remote image exceeds the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${url}`
|
|
27410
|
+
`Remote image exceeds the ${MAX_REMOTE_IMAGE_BYTES}-byte limit: ${safeUrl(url)}`
|
|
27408
27411
|
);
|
|
27409
27412
|
}
|
|
27410
27413
|
chunks.push(Buffer.from(value));
|
|
27411
27414
|
}
|
|
27412
27415
|
return Buffer.concat(chunks);
|
|
27413
27416
|
}
|
|
27417
|
+
function apiProxyDispatcher(explicitProxy, env) {
|
|
27418
|
+
const proxy = explicitProxy?.trim();
|
|
27419
|
+
if (proxy) {
|
|
27420
|
+
return new undiciExports.ProxyAgent(proxy);
|
|
27421
|
+
}
|
|
27422
|
+
if (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy) {
|
|
27423
|
+
return new undiciExports.EnvHttpProxyAgent();
|
|
27424
|
+
}
|
|
27425
|
+
return void 0;
|
|
27426
|
+
}
|
|
27427
|
+
const CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
27428
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
27429
|
+
"ECONNREFUSED",
|
|
27430
|
+
"ECONNRESET",
|
|
27431
|
+
"ENOTFOUND",
|
|
27432
|
+
"EHOSTUNREACH",
|
|
27433
|
+
"ENETUNREACH",
|
|
27434
|
+
"ETIMEDOUT"
|
|
27435
|
+
]);
|
|
27436
|
+
function connectFailureHint(error, url) {
|
|
27437
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
27438
|
+
if (!cause?.code || !CONNECT_CODES.has(cause.code)) {
|
|
27439
|
+
return null;
|
|
27440
|
+
}
|
|
27441
|
+
let host;
|
|
27442
|
+
try {
|
|
27443
|
+
host = new URL(url).host;
|
|
27444
|
+
} catch {
|
|
27445
|
+
return null;
|
|
27446
|
+
}
|
|
27447
|
+
return `Could not connect to ${host} (${cause.code}). The request never reached the network. If this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY, or run: modlens config set proxy <url>`;
|
|
27448
|
+
}
|
|
27449
|
+
async function apiFetch(url, init, proxy, env = process.env) {
|
|
27450
|
+
const dispatcher2 = apiProxyDispatcher(proxy, env);
|
|
27451
|
+
try {
|
|
27452
|
+
return await fetch(
|
|
27453
|
+
url,
|
|
27454
|
+
// `dispatcher` is a Node/undici extension to fetch's options.
|
|
27455
|
+
dispatcher2 ? { ...init, dispatcher: dispatcher2 } : init
|
|
27456
|
+
);
|
|
27457
|
+
} catch (error) {
|
|
27458
|
+
const hint = connectFailureHint(error, url);
|
|
27459
|
+
throw hint ? new Error(hint, { cause: error }) : error;
|
|
27460
|
+
}
|
|
27461
|
+
}
|
|
27414
27462
|
const JSON_TEMPLATE_INSTRUCTION = `Respond with ONE JSON object only, no markdown fences, no commentary. Fill this exact structure with your findings from the image (do not repeat this template literally, replace every value):
|
|
27415
27463
|
{"summary":"one paragraph describing the image","ocr":{"full_text":"all visible text","lines":[{"text":"one line","language":"en"}]},"layout":{"regions":[{"type":"title|subtitle|paragraph|list|table|chart|form|code|image|icon|other","reading_order":1,"text":"region text"}]},"semantics":{"scene":"what kind of scene","intent":"what the image is for","entities":[{"name":"entity","type":"kind","evidence":"where seen"}],"relations":[{"subject":"a","predicate":"relates to","object":"b"}]},"visual":{"dominant_colors":["color"],"style":"visual style","notes":["notable visual detail"]},"uncertainty":["anything unreadable or ambiguous"]}`;
|
|
27416
27464
|
function buildVisionPrompt(options) {
|
|
@@ -27677,6 +27725,33 @@ function hasPath(value, dottedPath) {
|
|
|
27677
27725
|
function isPlainObject(value) {
|
|
27678
27726
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27679
27727
|
}
|
|
27728
|
+
const TOKEN_SHAPES = [
|
|
27729
|
+
// Vendor-prefixed keys (OpenAI/Anthropic sk-, Stripe rk/pk, Slack xox*).
|
|
27730
|
+
/\b(?:sk|rk|pk|xox[a-z])-[A-Za-z0-9_-]{12,}\b/g,
|
|
27731
|
+
// Google API keys.
|
|
27732
|
+
/\bAIza[A-Za-z0-9_-]{20,}\b/g,
|
|
27733
|
+
// GitHub tokens.
|
|
27734
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
27735
|
+
// JWTs (three base64url segments, the first spelling {"alg" or {"typ").
|
|
27736
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\b/g,
|
|
27737
|
+
// Auth headers: "Bearer xyz" / "Authorization: xyz" (space form is real).
|
|
27738
|
+
/\b(?:bearer|authorization)\b[=:\s]+"?[A-Za-z0-9._~+/-]{12,}"?/gi,
|
|
27739
|
+
// Labeled keys need an explicit = or : separator; prose like
|
|
27740
|
+
// "token limit_exceeded" is diagnostics, not a credential.
|
|
27741
|
+
/\b(?:token|api[-_]?key)\b\s*[=:]\s*"?[A-Za-z0-9._~+/-]{12,}"?/gi
|
|
27742
|
+
];
|
|
27743
|
+
function redactSecrets(text, knownSecrets = []) {
|
|
27744
|
+
let out = text;
|
|
27745
|
+
for (const secret of knownSecrets) {
|
|
27746
|
+
if (secret && secret.length >= 6) {
|
|
27747
|
+
out = out.split(secret).join("[redacted]");
|
|
27748
|
+
}
|
|
27749
|
+
}
|
|
27750
|
+
for (const shape of TOKEN_SHAPES) {
|
|
27751
|
+
out = out.replace(shape, "[redacted]");
|
|
27752
|
+
}
|
|
27753
|
+
return out;
|
|
27754
|
+
}
|
|
27680
27755
|
const ANTHROPIC_DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
27681
27756
|
const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
|
|
27682
27757
|
const TOOL_NAME = "report_vision_evidence";
|
|
@@ -27705,46 +27780,52 @@ async function executeAnthropicApi(options) {
|
|
|
27705
27780
|
|
|
27706
27781
|
Report your findings by calling the ${TOOL_NAME} tool.`;
|
|
27707
27782
|
const startedAt = Date.now();
|
|
27708
|
-
const response2 = await
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
"
|
|
27712
|
-
|
|
27713
|
-
|
|
27783
|
+
const response2 = await apiFetch(
|
|
27784
|
+
`${baseUrl}/v1/messages`,
|
|
27785
|
+
{
|
|
27786
|
+
method: "POST",
|
|
27787
|
+
headers: {
|
|
27788
|
+
"x-api-key": apiKey,
|
|
27789
|
+
"anthropic-version": "2023-06-01",
|
|
27790
|
+
"Content-Type": "application/json"
|
|
27791
|
+
},
|
|
27792
|
+
body: JSON.stringify(
|
|
27793
|
+
mergeExtraBody(
|
|
27794
|
+
{
|
|
27795
|
+
model,
|
|
27796
|
+
max_tokens: 4096,
|
|
27797
|
+
tools: [
|
|
27798
|
+
{
|
|
27799
|
+
name: TOOL_NAME,
|
|
27800
|
+
description: "Report the structured visual evidence extracted from the image.",
|
|
27801
|
+
input_schema: VISION_RESULT_SCHEMA
|
|
27802
|
+
}
|
|
27803
|
+
],
|
|
27804
|
+
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27805
|
+
messages: [
|
|
27806
|
+
{
|
|
27807
|
+
role: "user",
|
|
27808
|
+
content: [
|
|
27809
|
+
{ type: "image", source: imageSource },
|
|
27810
|
+
{ type: "text", text: prompt }
|
|
27811
|
+
]
|
|
27812
|
+
}
|
|
27813
|
+
]
|
|
27814
|
+
},
|
|
27815
|
+
options.settings?.extraBody,
|
|
27816
|
+
["model", "messages", "tools", "tool_choice", "stream"],
|
|
27817
|
+
"anthropic"
|
|
27818
|
+
)
|
|
27819
|
+
),
|
|
27820
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27714
27821
|
},
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
{
|
|
27718
|
-
model,
|
|
27719
|
-
max_tokens: 4096,
|
|
27720
|
-
tools: [
|
|
27721
|
-
{
|
|
27722
|
-
name: TOOL_NAME,
|
|
27723
|
-
description: "Report the structured visual evidence extracted from the image.",
|
|
27724
|
-
input_schema: VISION_RESULT_SCHEMA
|
|
27725
|
-
}
|
|
27726
|
-
],
|
|
27727
|
-
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27728
|
-
messages: [
|
|
27729
|
-
{
|
|
27730
|
-
role: "user",
|
|
27731
|
-
content: [
|
|
27732
|
-
{ type: "image", source: imageSource },
|
|
27733
|
-
{ type: "text", text: prompt }
|
|
27734
|
-
]
|
|
27735
|
-
}
|
|
27736
|
-
]
|
|
27737
|
-
},
|
|
27738
|
-
options.settings?.extraBody,
|
|
27739
|
-
["model", "messages", "tools", "tool_choice", "stream"],
|
|
27740
|
-
"anthropic"
|
|
27741
|
-
)
|
|
27742
|
-
),
|
|
27743
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27744
|
-
});
|
|
27822
|
+
options.settings?.proxy
|
|
27823
|
+
);
|
|
27745
27824
|
if (!response2.ok) {
|
|
27746
27825
|
const body2 = await response2.text();
|
|
27747
|
-
throw new Error(
|
|
27826
|
+
throw new Error(
|
|
27827
|
+
`Anthropic API error ${response2.status}: ${truncate(redactSecrets(body2, [apiKey]))}`
|
|
27828
|
+
);
|
|
27748
27829
|
}
|
|
27749
27830
|
const payload = await response2.json();
|
|
27750
27831
|
const toolUse = payload.content?.find((block) => block.type === "tool_use");
|
|
@@ -28000,42 +28081,53 @@ async function executeGeminiApi(options) {
|
|
|
28000
28081
|
extraPrompt: options.extraPrompt
|
|
28001
28082
|
});
|
|
28002
28083
|
const startedAt = Date.now();
|
|
28003
|
-
const response2 = await
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
"
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
28014
|
-
|
|
28015
|
-
|
|
28016
|
-
|
|
28017
|
-
|
|
28084
|
+
const response2 = await apiFetch(
|
|
28085
|
+
`${baseUrl}/v1beta/models/${model}:generateContent`,
|
|
28086
|
+
{
|
|
28087
|
+
method: "POST",
|
|
28088
|
+
headers: {
|
|
28089
|
+
"x-goog-api-key": apiKey,
|
|
28090
|
+
"Content-Type": "application/json"
|
|
28091
|
+
},
|
|
28092
|
+
body: JSON.stringify(
|
|
28093
|
+
mergeExtraBody(
|
|
28094
|
+
{
|
|
28095
|
+
contents: [
|
|
28096
|
+
{
|
|
28097
|
+
parts: [
|
|
28098
|
+
{
|
|
28099
|
+
inline_data: {
|
|
28100
|
+
mime_type: image.mimeType,
|
|
28101
|
+
data: image.data
|
|
28102
|
+
}
|
|
28103
|
+
},
|
|
28104
|
+
{ text: prompt }
|
|
28105
|
+
]
|
|
28106
|
+
}
|
|
28107
|
+
],
|
|
28108
|
+
generationConfig: {
|
|
28109
|
+
responseMimeType: "application/json",
|
|
28110
|
+
responseJsonSchema: VISION_RESULT_SCHEMA
|
|
28018
28111
|
}
|
|
28112
|
+
},
|
|
28113
|
+
options.settings?.extraBody,
|
|
28114
|
+
[
|
|
28115
|
+
"contents",
|
|
28116
|
+
"generationConfig.responseMimeType",
|
|
28117
|
+
"generationConfig.responseJsonSchema"
|
|
28019
28118
|
],
|
|
28020
|
-
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
"contents",
|
|
28028
|
-
"generationConfig.responseMimeType",
|
|
28029
|
-
"generationConfig.responseJsonSchema"
|
|
28030
|
-
],
|
|
28031
|
-
"gemini-api"
|
|
28032
|
-
)
|
|
28033
|
-
),
|
|
28034
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28035
|
-
});
|
|
28119
|
+
"gemini-api"
|
|
28120
|
+
)
|
|
28121
|
+
),
|
|
28122
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28123
|
+
},
|
|
28124
|
+
options.settings?.proxy
|
|
28125
|
+
);
|
|
28036
28126
|
if (!response2.ok) {
|
|
28037
28127
|
const body2 = await response2.text();
|
|
28038
|
-
throw new Error(
|
|
28128
|
+
throw new Error(
|
|
28129
|
+
`Gemini API error ${response2.status}: ${truncate(redactSecrets(body2, [apiKey]))}`
|
|
28130
|
+
);
|
|
28039
28131
|
}
|
|
28040
28132
|
const payload = await response2.json();
|
|
28041
28133
|
const text = payload.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("");
|
|
@@ -28080,36 +28172,42 @@ async function executeOpenaiCompat(options) {
|
|
|
28080
28172
|
|
|
28081
28173
|
${JSON_TEMPLATE_INSTRUCTION}`;
|
|
28082
28174
|
const startedAt = Date.now();
|
|
28083
|
-
const response2 = await
|
|
28084
|
-
|
|
28085
|
-
|
|
28086
|
-
|
|
28087
|
-
|
|
28175
|
+
const response2 = await apiFetch(
|
|
28176
|
+
`${baseUrl}/chat/completions`,
|
|
28177
|
+
{
|
|
28178
|
+
method: "POST",
|
|
28179
|
+
headers: {
|
|
28180
|
+
Authorization: `Bearer ${apiKey}`,
|
|
28181
|
+
"Content-Type": "application/json"
|
|
28182
|
+
},
|
|
28183
|
+
body: JSON.stringify(
|
|
28184
|
+
mergeExtraBody(
|
|
28185
|
+
{
|
|
28186
|
+
model,
|
|
28187
|
+
messages: [
|
|
28188
|
+
{
|
|
28189
|
+
role: "user",
|
|
28190
|
+
content: [
|
|
28191
|
+
{ type: "image_url", image_url: { url: imageUrl } },
|
|
28192
|
+
{ type: "text", text: prompt }
|
|
28193
|
+
]
|
|
28194
|
+
}
|
|
28195
|
+
]
|
|
28196
|
+
},
|
|
28197
|
+
options.settings?.extraBody,
|
|
28198
|
+
["model", "messages", "stream"],
|
|
28199
|
+
"openai"
|
|
28200
|
+
)
|
|
28201
|
+
),
|
|
28202
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28088
28203
|
},
|
|
28089
|
-
|
|
28090
|
-
|
|
28091
|
-
{
|
|
28092
|
-
model,
|
|
28093
|
-
messages: [
|
|
28094
|
-
{
|
|
28095
|
-
role: "user",
|
|
28096
|
-
content: [
|
|
28097
|
-
{ type: "image_url", image_url: { url: imageUrl } },
|
|
28098
|
-
{ type: "text", text: prompt }
|
|
28099
|
-
]
|
|
28100
|
-
}
|
|
28101
|
-
]
|
|
28102
|
-
},
|
|
28103
|
-
options.settings?.extraBody,
|
|
28104
|
-
["model", "messages", "stream"],
|
|
28105
|
-
"openai"
|
|
28106
|
-
)
|
|
28107
|
-
),
|
|
28108
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28109
|
-
});
|
|
28204
|
+
options.settings?.proxy
|
|
28205
|
+
);
|
|
28110
28206
|
if (!response2.ok) {
|
|
28111
28207
|
const body2 = await response2.text();
|
|
28112
|
-
throw new Error(
|
|
28208
|
+
throw new Error(
|
|
28209
|
+
`OpenAI-compatible API error ${response2.status}: ${truncate(redactSecrets(body2, [apiKey]))}`
|
|
28210
|
+
);
|
|
28113
28211
|
}
|
|
28114
28212
|
const payload = await response2.json();
|
|
28115
28213
|
const text = payload.choices?.[0]?.message?.content;
|
|
@@ -28174,7 +28272,7 @@ function providerAliases() {
|
|
|
28174
28272
|
function listProviders() {
|
|
28175
28273
|
return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
|
|
28176
28274
|
}
|
|
28177
|
-
const STRING_FIELDS = ["apiKey", "baseUrl", "model"];
|
|
28275
|
+
const STRING_FIELDS = ["apiKey", "baseUrl", "model", "proxy"];
|
|
28178
28276
|
const REUSE_HARNESSES = ["claude", "codex", "opencode", "pi", "grok"];
|
|
28179
28277
|
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
28180
28278
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
@@ -28215,6 +28313,9 @@ function resolveProviderSettings(providerName, config2, env = process.env) {
|
|
|
28215
28313
|
};
|
|
28216
28314
|
const bindings = ENV_BINDINGS[providerName] ?? {};
|
|
28217
28315
|
const settings = { ...fromFile };
|
|
28316
|
+
if (!settings.proxy && config2.proxy?.trim()) {
|
|
28317
|
+
settings.proxy = config2.proxy.trim();
|
|
28318
|
+
}
|
|
28218
28319
|
for (const [field, envName] of Object.entries(bindings)) {
|
|
28219
28320
|
const value = env[envName]?.trim();
|
|
28220
28321
|
if (value) {
|
|
@@ -28227,6 +28328,12 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28227
28328
|
const config2 = loadConfigFile(configPath);
|
|
28228
28329
|
if (dottedKey === "provider") {
|
|
28229
28330
|
config2.provider = value;
|
|
28331
|
+
} else if (dottedKey === "proxy") {
|
|
28332
|
+
if (value.trim() === "") {
|
|
28333
|
+
delete config2.proxy;
|
|
28334
|
+
} else {
|
|
28335
|
+
config2.proxy = value.trim();
|
|
28336
|
+
}
|
|
28230
28337
|
} else if (dottedKey.startsWith("reuse.")) {
|
|
28231
28338
|
const harness = dottedKey.slice("reuse.".length);
|
|
28232
28339
|
if (!REUSE_HARNESSES.includes(harness)) {
|
|
@@ -28271,7 +28378,7 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28271
28378
|
}
|
|
28272
28379
|
} else if (!STRING_FIELDS.includes(field)) {
|
|
28273
28380
|
throw new Error(
|
|
28274
|
-
`Unknown config field: ${field}. Use apiKey, baseUrl, model, or extraBody.`
|
|
28381
|
+
`Unknown config field: ${field}. Use apiKey, baseUrl, model, proxy, or extraBody.`
|
|
28275
28382
|
);
|
|
28276
28383
|
} else {
|
|
28277
28384
|
config2.providers ??= {};
|
|
@@ -28373,6 +28480,11 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28373
28480
|
if (config2.provider?.trim()) {
|
|
28374
28481
|
effective.provider = config2.provider.trim();
|
|
28375
28482
|
}
|
|
28483
|
+
if (config2.proxy?.trim()) {
|
|
28484
|
+
effective.proxy = `${config2.proxy.trim()} (file)`;
|
|
28485
|
+
} else if (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy) {
|
|
28486
|
+
effective.proxy = `${env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy} (env)`;
|
|
28487
|
+
}
|
|
28376
28488
|
if (config2.guards) {
|
|
28377
28489
|
const guards = {};
|
|
28378
28490
|
if (config2.guards.denyModels !== void 0) {
|
|
@@ -28442,13 +28554,16 @@ const PROVIDER_DESCRIPTORS = [
|
|
|
28442
28554
|
];
|
|
28443
28555
|
function findOnPath(bin, env) {
|
|
28444
28556
|
const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
28557
|
+
const suffixes = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
28445
28558
|
for (const dir of dirs) {
|
|
28446
|
-
const
|
|
28447
|
-
|
|
28448
|
-
|
|
28449
|
-
|
|
28559
|
+
for (const suffix of suffixes) {
|
|
28560
|
+
const full = path.join(dir, bin + suffix);
|
|
28561
|
+
try {
|
|
28562
|
+
if (fs.statSync(full).isFile()) {
|
|
28563
|
+
return full;
|
|
28564
|
+
}
|
|
28565
|
+
} catch {
|
|
28450
28566
|
}
|
|
28451
|
-
} catch {
|
|
28452
28567
|
}
|
|
28453
28568
|
}
|
|
28454
28569
|
return null;
|
|
@@ -28609,7 +28724,10 @@ function probeCodex(env, home) {
|
|
|
28609
28724
|
loggedIn,
|
|
28610
28725
|
visionModels: [],
|
|
28611
28726
|
source: "none",
|
|
28612
|
-
error: error instanceof Error ? error.message : String(error)
|
|
28727
|
+
error: redactSecrets(error instanceof Error ? error.message : String(error)).slice(
|
|
28728
|
+
0,
|
|
28729
|
+
200
|
|
28730
|
+
)
|
|
28613
28731
|
};
|
|
28614
28732
|
}
|
|
28615
28733
|
});
|
|
@@ -28682,7 +28800,10 @@ function probePi(env, home) {
|
|
|
28682
28800
|
cliPath,
|
|
28683
28801
|
visionModels: [],
|
|
28684
28802
|
source: "none",
|
|
28685
|
-
error: error instanceof Error ? error.message : String(error)
|
|
28803
|
+
error: redactSecrets(error instanceof Error ? error.message : String(error)).slice(
|
|
28804
|
+
0,
|
|
28805
|
+
200
|
|
28806
|
+
)
|
|
28686
28807
|
};
|
|
28687
28808
|
}
|
|
28688
28809
|
});
|
|
@@ -28704,7 +28825,10 @@ function probeOpencode(env, runCli) {
|
|
|
28704
28825
|
cliPath,
|
|
28705
28826
|
visionModels: [],
|
|
28706
28827
|
source: "none",
|
|
28707
|
-
error: error instanceof Error ? error.message : String(error)
|
|
28828
|
+
error: redactSecrets(error instanceof Error ? error.message : String(error)).slice(
|
|
28829
|
+
0,
|
|
28830
|
+
200
|
|
28831
|
+
)
|
|
28708
28832
|
};
|
|
28709
28833
|
}
|
|
28710
28834
|
});
|
|
@@ -29221,7 +29345,9 @@ async function analyzeImage(options) {
|
|
|
29221
29345
|
provider: provider.name,
|
|
29222
29346
|
ok: false,
|
|
29223
29347
|
durationSeconds: (Date.now() - startedAt) / 1e3,
|
|
29224
|
-
|
|
29348
|
+
// Providers redact their own errors, but attempts travel into
|
|
29349
|
+
// output and model contexts, so the record gets the belt too.
|
|
29350
|
+
error: redactSecrets(message).slice(0, 300)
|
|
29225
29351
|
});
|
|
29226
29352
|
}
|
|
29227
29353
|
}
|
|
@@ -29444,7 +29570,9 @@ function runCommand(providerName, invocation, timeoutMs, describeFailure) {
|
|
|
29444
29570
|
const explained = describeFailure?.({ stdout, stderr, code, startedAt: runStartedAt }) ?? null;
|
|
29445
29571
|
reject(
|
|
29446
29572
|
new Error(
|
|
29447
|
-
|
|
29573
|
+
redactSecrets(
|
|
29574
|
+
explained ?? `${providerName} provider failed with code ${code}.${stderr ? ` stderr: ${stderr.trim()}` : ""}`
|
|
29575
|
+
)
|
|
29448
29576
|
)
|
|
29449
29577
|
);
|
|
29450
29578
|
return;
|
|
@@ -29572,7 +29700,6 @@ function cwdMatches(recorded, wanted, bothDirections = false) {
|
|
|
29572
29700
|
return bothDirections && resolvedWanted.startsWith(`${resolvedRecorded}${path.sep}`);
|
|
29573
29701
|
}
|
|
29574
29702
|
function transcriptBelongsTo(lines, cwd, bothDirections = false) {
|
|
29575
|
-
let sawCwd = false;
|
|
29576
29703
|
for (const line of lines) {
|
|
29577
29704
|
if (!line.includes('"cwd"')) {
|
|
29578
29705
|
continue;
|
|
@@ -29582,14 +29709,13 @@ function transcriptBelongsTo(lines, cwd, bothDirections = false) {
|
|
|
29582
29709
|
if (typeof recorded !== "string") {
|
|
29583
29710
|
continue;
|
|
29584
29711
|
}
|
|
29585
|
-
sawCwd = true;
|
|
29586
29712
|
if (cwdMatches(recorded, cwd, bothDirections)) {
|
|
29587
29713
|
return true;
|
|
29588
29714
|
}
|
|
29589
29715
|
} catch {
|
|
29590
29716
|
}
|
|
29591
29717
|
}
|
|
29592
|
-
return
|
|
29718
|
+
return false;
|
|
29593
29719
|
}
|
|
29594
29720
|
function readLines(filePath) {
|
|
29595
29721
|
try {
|
|
@@ -29718,16 +29844,19 @@ const claudeAdapter = jsonlAdapter({
|
|
|
29718
29844
|
function opencodeDbPath() {
|
|
29719
29845
|
return path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
|
|
29720
29846
|
}
|
|
29721
|
-
function
|
|
29722
|
-
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
29723
|
-
}
|
|
29724
|
-
function opencodeDirectoryFilter(resolvedCwd) {
|
|
29847
|
+
function opencodeDirectoryFilter(resolvedCwd, caseInsensitive = process.platform === "win32") {
|
|
29725
29848
|
const normalized = resolvedCwd.replace(/\\/g, "/");
|
|
29726
|
-
const
|
|
29727
|
-
const
|
|
29849
|
+
const cwd = caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
29850
|
+
const prefix = `${cwd.replace(/\/+$/, "")}/`;
|
|
29851
|
+
const rawDir = `REPLACE(session.directory, '\\', '/')`;
|
|
29852
|
+
const dir = caseInsensitive ? `LOWER(${rawDir})` : rawDir;
|
|
29853
|
+
const dirPrefix = `RTRIM(${dir}, '/') || '/'`;
|
|
29728
29854
|
return {
|
|
29729
|
-
|
|
29730
|
-
|
|
29855
|
+
// SQLite SUBSTR counts Unicode characters while JS .length counts
|
|
29856
|
+
// UTF-16 units, so the length parameter is measured in code points
|
|
29857
|
+
// ([...str].length) or an emoji in a path would shift the boundary.
|
|
29858
|
+
clause: `(${dir} = ? OR SUBSTR(${dir}, 1, ?) = ? OR SUBSTR(?, 1, LENGTH(${dirPrefix})) = ${dirPrefix})`,
|
|
29859
|
+
params: [cwd, [...prefix].length, prefix, cwd]
|
|
29731
29860
|
};
|
|
29732
29861
|
}
|
|
29733
29862
|
function buildOpencodeQuery(resolvedCwd, sessionId) {
|
|
@@ -30175,8 +30304,13 @@ function inspectProvider(descriptor, config2, env) {
|
|
|
30175
30304
|
name: descriptor.name,
|
|
30176
30305
|
kind: "subprocess",
|
|
30177
30306
|
ready: binaryPath !== null,
|
|
30307
|
+
status: binaryPath !== null ? "installed" : "missing",
|
|
30308
|
+
// "On PATH" proves installation, not a working login: doctor runs
|
|
30309
|
+
// offline and spends nothing, so sign-in state stays unverified
|
|
30310
|
+
// here and the first real read is the auth check.
|
|
30311
|
+
authUnverified: binaryPath !== null,
|
|
30178
30312
|
binaryPath,
|
|
30179
|
-
detail: binaryPath ? `${descriptor.bin} found at ${binaryPath}` : `${descriptor.bin} not on PATH`,
|
|
30313
|
+
detail: binaryPath ? `${descriptor.bin} found at ${binaryPath} (installed; sign-in not verified offline)` : `${descriptor.bin} not on PATH`,
|
|
30180
30314
|
fix: binaryPath ? void 0 : descriptor.install
|
|
30181
30315
|
};
|
|
30182
30316
|
}
|
|
@@ -30194,6 +30328,7 @@ function inspectProvider(descriptor, config2, env) {
|
|
|
30194
30328
|
name: descriptor.name,
|
|
30195
30329
|
kind: "api",
|
|
30196
30330
|
ready,
|
|
30331
|
+
status: ready ? "ready" : "missing",
|
|
30197
30332
|
settings: statuses,
|
|
30198
30333
|
detail,
|
|
30199
30334
|
fix: ready ? void 0 : descriptor.fix
|
|
@@ -30314,7 +30449,8 @@ function renderDoctorReport(report) {
|
|
|
30314
30449
|
lines.push("");
|
|
30315
30450
|
lines.push("Providers");
|
|
30316
30451
|
for (const provider of report.providers) {
|
|
30317
|
-
|
|
30452
|
+
const providerMark = provider.ready && provider.authUnverified ? "[ok?]" : mark(provider.ready);
|
|
30453
|
+
lines.push(` ${providerMark} ${provider.name}: ${provider.detail}`);
|
|
30318
30454
|
if (provider.fix) {
|
|
30319
30455
|
lines.push(` fix: ${provider.fix}`);
|
|
30320
30456
|
}
|
|
@@ -30461,7 +30597,7 @@ function locateSource(cwd, adapters = ADAPTERS) {
|
|
|
30461
30597
|
const blocked = blockers.length > 0 ? `
|
|
30462
30598
|
Blocked: ${blockers.join(" | ")}` : "";
|
|
30463
30599
|
throw new Error(
|
|
30464
|
-
`No pasted images found in any session storage for this directory (looked in: ${dirs}). The user may not have pasted any,
|
|
30600
|
+
`No pasted images found in any session storage for this directory (looked in: ${dirs}). The user may not have pasted any, the storage format changed, or a legacy transcript records no cwd (ownership cannot be proven; an explicit --transcript path bypasses that check). Ask for a file path instead.${blocked}`
|
|
30465
30601
|
);
|
|
30466
30602
|
}
|
|
30467
30603
|
return best.ref;
|
|
@@ -30528,7 +30664,7 @@ function recoverPastedImages(options = {}) {
|
|
|
30528
30664
|
const all = source.extract();
|
|
30529
30665
|
if (all.length === 0) {
|
|
30530
30666
|
throw new Error(
|
|
30531
|
-
`No pasted images found in ${source.location}. The user may not have pasted any,
|
|
30667
|
+
`No pasted images found in ${source.location}. The user may not have pasted any, the storage format changed, or a legacy transcript records no cwd (ownership cannot be proven; an explicit --transcript path bypasses that check). Ask for a file path instead.`
|
|
30532
30668
|
);
|
|
30533
30669
|
}
|
|
30534
30670
|
const outDir = prepareOutDir(options.outDir);
|
|
@@ -30560,16 +30696,19 @@ function recoverPastedImages(options = {}) {
|
|
|
30560
30696
|
return result;
|
|
30561
30697
|
}
|
|
30562
30698
|
const program = new Command();
|
|
30563
|
-
|
|
30699
|
+
function parsePositiveInt(raw, flag) {
|
|
30700
|
+
if (!/^\d+$/.test(raw.trim()) || Number.parseInt(raw, 10) <= 0) {
|
|
30701
|
+
throw new Error(`Invalid ${flag}. Use a positive integer.`);
|
|
30702
|
+
}
|
|
30703
|
+
return Number.parseInt(raw, 10);
|
|
30704
|
+
}
|
|
30705
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.12.0");
|
|
30564
30706
|
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(
|
|
30565
30707
|
"--extra-body <json>",
|
|
30566
30708
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
30567
30709
|
).action(async (options) => {
|
|
30568
30710
|
try {
|
|
30569
|
-
const timeoutMs =
|
|
30570
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
30571
|
-
throw new Error("Invalid --timeout. Use a positive integer in milliseconds.");
|
|
30572
|
-
}
|
|
30711
|
+
const timeoutMs = parsePositiveInt(options.timeout, "--timeout (milliseconds)");
|
|
30573
30712
|
const config2 = loadConfigFile();
|
|
30574
30713
|
if (process.env.MODLENS_MODEL?.trim()) {
|
|
30575
30714
|
const verdict = runGuard(config2.guards, {
|
|
@@ -30607,7 +30746,7 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
|
|
|
30607
30746
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30608
30747
|
`
|
|
30609
30748
|
);
|
|
30610
|
-
process.
|
|
30749
|
+
process.exitCode = 1;
|
|
30611
30750
|
}
|
|
30612
30751
|
});
|
|
30613
30752
|
program.command("recover-paste").description(
|
|
@@ -30620,10 +30759,7 @@ program.command("recover-paste").description(
|
|
|
30620
30759
|
"Force the storage scope: claude-code, pi, opencode, or none (default: auto-detect via process ancestry and env)"
|
|
30621
30760
|
).option("--cwd <path>", "Project directory the image was pasted in", process.cwd()).action(async (options) => {
|
|
30622
30761
|
try {
|
|
30623
|
-
const count =
|
|
30624
|
-
if (!Number.isFinite(count) || count <= 0) {
|
|
30625
|
-
throw new Error("Invalid --count. Use a positive integer.");
|
|
30626
|
-
}
|
|
30762
|
+
const count = parsePositiveInt(options.count, "--count");
|
|
30627
30763
|
const result = recoverPastedImages({
|
|
30628
30764
|
count,
|
|
30629
30765
|
outDir: options.outDir,
|
|
@@ -30639,7 +30775,7 @@ program.command("recover-paste").description(
|
|
|
30639
30775
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30640
30776
|
`
|
|
30641
30777
|
);
|
|
30642
|
-
process.
|
|
30778
|
+
process.exitCode = 1;
|
|
30643
30779
|
}
|
|
30644
30780
|
});
|
|
30645
30781
|
program.command("guard").description(
|
|
@@ -30683,7 +30819,7 @@ program.command("doctor").description(
|
|
|
30683
30819
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30684
30820
|
`
|
|
30685
30821
|
);
|
|
30686
|
-
process.
|
|
30822
|
+
process.exitCode = 1;
|
|
30687
30823
|
}
|
|
30688
30824
|
});
|
|
30689
30825
|
const config = program.command("config").description(`Manage ${CONFIG_PATH} (providers, keys, models)`);
|
|
@@ -30705,7 +30841,7 @@ config.command("init").description(`Create a starter config at ${CONFIG_PATH}`).
|
|
|
30705
30841
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30706
30842
|
`
|
|
30707
30843
|
);
|
|
30708
|
-
process.
|
|
30844
|
+
process.exitCode = 1;
|
|
30709
30845
|
}
|
|
30710
30846
|
});
|
|
30711
30847
|
config.command("set <key> <value>").description("Set a value, e.g. modlens config set gemini-api.apiKey <key>").action((key, value) => {
|
|
@@ -30718,7 +30854,7 @@ config.command("set <key> <value>").description("Set a value, e.g. modlens confi
|
|
|
30718
30854
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30719
30855
|
`
|
|
30720
30856
|
);
|
|
30721
|
-
process.
|
|
30857
|
+
process.exitCode = 1;
|
|
30722
30858
|
}
|
|
30723
30859
|
});
|
|
30724
30860
|
config.command("show").description("Print the effective config (file merged with env vars), API keys masked").action(() => {
|
|
@@ -30730,7 +30866,7 @@ config.command("show").description("Print the effective config (file merged with
|
|
|
30730
30866
|
`Error: ${error instanceof Error ? error.message : String(error)}
|
|
30731
30867
|
`
|
|
30732
30868
|
);
|
|
30733
|
-
process.
|
|
30869
|
+
process.exitCode = 1;
|
|
30734
30870
|
}
|
|
30735
30871
|
});
|
|
30736
|
-
program.
|
|
30872
|
+
await program.parseAsync();
|