@liustack/modlens 3.11.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 +4 -0
- package/dist/main.js +172 -96
- package/docs/cli.md +1 -1
- package/docs/troubleshooting.md +23 -0
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/configure.md +1 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.12.0 - 2026-08-14
|
|
4
|
+
|
|
5
|
+
- **The API providers work behind a proxy ([#20](https://github.com/liustack/modlens/issues/20)).** Node's fetch ignores `HTTP_PROXY`/`HTTPS_PROXY` entirely, so machines that reach the internet through a proxy could not use `gemini-api` at all, and the failure surfaced as a bare `fetch failed`. The three inline API providers now honor the standard environment variables (`NO_PROXY` included, via undici's `EnvHttpProxyAgent`), with an explicit setting as the escape hatch: `modlens config set proxy <url>` for all API providers, `<provider>.proxy` to scope it to one. A connect-level failure now names the unreachable host and points at both knobs instead of saying `fetch failed`. Scope is deliberate and documented: the proxy applies to API requests only, while the remote-image download path keeps its direct, IP-pinned connection, because its SSRF guards validate the exact address being contacted and a proxy would blind them. Thanks to @soloyu for a report that arrived with the diagnosis, the fix direction, and the security boundary already thought through.
|
|
6
|
+
|
|
3
7
|
## 3.11.0 - 2026-08-14
|
|
4
8
|
|
|
5
9
|
- **A full-project audit, all ten findings fixed, then re-reviewed until clean.** An independent deep review of the whole repository (P0: none) surfaced ten conditional-but-real defects. Every fix went back through further independent review rounds, which caught real bugs in the first fixes themselves (case and Unicode boundaries, a cancellation regression); the final round accepted with no blocking findings. Each item below carries a regression test — the suite grew by 32 cases.
|
package/dist/main.js
CHANGED
|
@@ -27414,6 +27414,51 @@ async function readCapped(response2, url) {
|
|
|
27414
27414
|
}
|
|
27415
27415
|
return Buffer.concat(chunks);
|
|
27416
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
|
+
}
|
|
27417
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):
|
|
27418
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"]}`;
|
|
27419
27464
|
function buildVisionPrompt(options) {
|
|
@@ -27735,43 +27780,47 @@ async function executeAnthropicApi(options) {
|
|
|
27735
27780
|
|
|
27736
27781
|
Report your findings by calling the ${TOOL_NAME} tool.`;
|
|
27737
27782
|
const startedAt = Date.now();
|
|
27738
|
-
const response2 = await
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
"
|
|
27742
|
-
|
|
27743
|
-
|
|
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)
|
|
27744
27821
|
},
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
{
|
|
27748
|
-
model,
|
|
27749
|
-
max_tokens: 4096,
|
|
27750
|
-
tools: [
|
|
27751
|
-
{
|
|
27752
|
-
name: TOOL_NAME,
|
|
27753
|
-
description: "Report the structured visual evidence extracted from the image.",
|
|
27754
|
-
input_schema: VISION_RESULT_SCHEMA
|
|
27755
|
-
}
|
|
27756
|
-
],
|
|
27757
|
-
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27758
|
-
messages: [
|
|
27759
|
-
{
|
|
27760
|
-
role: "user",
|
|
27761
|
-
content: [
|
|
27762
|
-
{ type: "image", source: imageSource },
|
|
27763
|
-
{ type: "text", text: prompt }
|
|
27764
|
-
]
|
|
27765
|
-
}
|
|
27766
|
-
]
|
|
27767
|
-
},
|
|
27768
|
-
options.settings?.extraBody,
|
|
27769
|
-
["model", "messages", "tools", "tool_choice", "stream"],
|
|
27770
|
-
"anthropic"
|
|
27771
|
-
)
|
|
27772
|
-
),
|
|
27773
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27774
|
-
});
|
|
27822
|
+
options.settings?.proxy
|
|
27823
|
+
);
|
|
27775
27824
|
if (!response2.ok) {
|
|
27776
27825
|
const body2 = await response2.text();
|
|
27777
27826
|
throw new Error(
|
|
@@ -28032,39 +28081,48 @@ async function executeGeminiApi(options) {
|
|
|
28032
28081
|
extraPrompt: options.extraPrompt
|
|
28033
28082
|
});
|
|
28034
28083
|
const startedAt = Date.now();
|
|
28035
|
-
const response2 = await
|
|
28036
|
-
|
|
28037
|
-
|
|
28038
|
-
"
|
|
28039
|
-
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
28043
|
-
|
|
28044
|
-
|
|
28045
|
-
|
|
28046
|
-
|
|
28047
|
-
|
|
28048
|
-
|
|
28049
|
-
|
|
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
|
|
28050
28111
|
}
|
|
28112
|
+
},
|
|
28113
|
+
options.settings?.extraBody,
|
|
28114
|
+
[
|
|
28115
|
+
"contents",
|
|
28116
|
+
"generationConfig.responseMimeType",
|
|
28117
|
+
"generationConfig.responseJsonSchema"
|
|
28051
28118
|
],
|
|
28052
|
-
|
|
28053
|
-
|
|
28054
|
-
|
|
28055
|
-
|
|
28056
|
-
|
|
28057
|
-
|
|
28058
|
-
|
|
28059
|
-
"contents",
|
|
28060
|
-
"generationConfig.responseMimeType",
|
|
28061
|
-
"generationConfig.responseJsonSchema"
|
|
28062
|
-
],
|
|
28063
|
-
"gemini-api"
|
|
28064
|
-
)
|
|
28065
|
-
),
|
|
28066
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28067
|
-
});
|
|
28119
|
+
"gemini-api"
|
|
28120
|
+
)
|
|
28121
|
+
),
|
|
28122
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28123
|
+
},
|
|
28124
|
+
options.settings?.proxy
|
|
28125
|
+
);
|
|
28068
28126
|
if (!response2.ok) {
|
|
28069
28127
|
const body2 = await response2.text();
|
|
28070
28128
|
throw new Error(
|
|
@@ -28114,33 +28172,37 @@ async function executeOpenaiCompat(options) {
|
|
|
28114
28172
|
|
|
28115
28173
|
${JSON_TEMPLATE_INSTRUCTION}`;
|
|
28116
28174
|
const startedAt = Date.now();
|
|
28117
|
-
const response2 = await
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
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)
|
|
28122
28203
|
},
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
{
|
|
28126
|
-
model,
|
|
28127
|
-
messages: [
|
|
28128
|
-
{
|
|
28129
|
-
role: "user",
|
|
28130
|
-
content: [
|
|
28131
|
-
{ type: "image_url", image_url: { url: imageUrl } },
|
|
28132
|
-
{ type: "text", text: prompt }
|
|
28133
|
-
]
|
|
28134
|
-
}
|
|
28135
|
-
]
|
|
28136
|
-
},
|
|
28137
|
-
options.settings?.extraBody,
|
|
28138
|
-
["model", "messages", "stream"],
|
|
28139
|
-
"openai"
|
|
28140
|
-
)
|
|
28141
|
-
),
|
|
28142
|
-
signal: AbortSignal.timeout(options.timeoutMs)
|
|
28143
|
-
});
|
|
28204
|
+
options.settings?.proxy
|
|
28205
|
+
);
|
|
28144
28206
|
if (!response2.ok) {
|
|
28145
28207
|
const body2 = await response2.text();
|
|
28146
28208
|
throw new Error(
|
|
@@ -28210,7 +28272,7 @@ function providerAliases() {
|
|
|
28210
28272
|
function listProviders() {
|
|
28211
28273
|
return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
|
|
28212
28274
|
}
|
|
28213
|
-
const STRING_FIELDS = ["apiKey", "baseUrl", "model"];
|
|
28275
|
+
const STRING_FIELDS = ["apiKey", "baseUrl", "model", "proxy"];
|
|
28214
28276
|
const REUSE_HARNESSES = ["claude", "codex", "opencode", "pi", "grok"];
|
|
28215
28277
|
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
28216
28278
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
@@ -28251,6 +28313,9 @@ function resolveProviderSettings(providerName, config2, env = process.env) {
|
|
|
28251
28313
|
};
|
|
28252
28314
|
const bindings = ENV_BINDINGS[providerName] ?? {};
|
|
28253
28315
|
const settings = { ...fromFile };
|
|
28316
|
+
if (!settings.proxy && config2.proxy?.trim()) {
|
|
28317
|
+
settings.proxy = config2.proxy.trim();
|
|
28318
|
+
}
|
|
28254
28319
|
for (const [field, envName] of Object.entries(bindings)) {
|
|
28255
28320
|
const value = env[envName]?.trim();
|
|
28256
28321
|
if (value) {
|
|
@@ -28263,6 +28328,12 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28263
28328
|
const config2 = loadConfigFile(configPath);
|
|
28264
28329
|
if (dottedKey === "provider") {
|
|
28265
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
|
+
}
|
|
28266
28337
|
} else if (dottedKey.startsWith("reuse.")) {
|
|
28267
28338
|
const harness = dottedKey.slice("reuse.".length);
|
|
28268
28339
|
if (!REUSE_HARNESSES.includes(harness)) {
|
|
@@ -28307,7 +28378,7 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28307
28378
|
}
|
|
28308
28379
|
} else if (!STRING_FIELDS.includes(field)) {
|
|
28309
28380
|
throw new Error(
|
|
28310
|
-
`Unknown config field: ${field}. Use apiKey, baseUrl, model, or extraBody.`
|
|
28381
|
+
`Unknown config field: ${field}. Use apiKey, baseUrl, model, proxy, or extraBody.`
|
|
28311
28382
|
);
|
|
28312
28383
|
} else {
|
|
28313
28384
|
config2.providers ??= {};
|
|
@@ -28409,6 +28480,11 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28409
28480
|
if (config2.provider?.trim()) {
|
|
28410
28481
|
effective.provider = config2.provider.trim();
|
|
28411
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
|
+
}
|
|
28412
28488
|
if (config2.guards) {
|
|
28413
28489
|
const guards = {};
|
|
28414
28490
|
if (config2.guards.denyModels !== void 0) {
|
|
@@ -30626,7 +30702,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
30626
30702
|
}
|
|
30627
30703
|
return Number.parseInt(raw, 10);
|
|
30628
30704
|
}
|
|
30629
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.
|
|
30705
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.12.0");
|
|
30630
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(
|
|
30631
30707
|
"--extra-body <json>",
|
|
30632
30708
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
package/docs/cli.md
CHANGED
|
@@ -95,6 +95,6 @@ Five providers: `antigravity-cli` (no key), `gemini-api` (fastest free route), `
|
|
|
95
95
|
Other subcommands:
|
|
96
96
|
|
|
97
97
|
- `modlens guard [--model <id>]`: should the engine run for the active model at all? Exit 0 allow, 1 deny, verdict as JSON.
|
|
98
|
-
- `modlens config <init|set|show>`: keys are `provider`, `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|extraBody>`.
|
|
98
|
+
- `modlens config <init|set|show>`: keys are `provider`, `proxy` (HTTP/HTTPS proxy for the API providers, `HTTPS_PROXY`/`HTTP_PROXY` also honored), `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`.
|
|
99
99
|
- `modlens doctor`: Node and node:sqlite, provider readiness, the failover chains for this machine, the detected harness, the guard's rules with a live verdict, and the Reuse section with per-harness grant decisions and discovered vision. Spends no quota; `--json` for a machine-readable report.
|
|
100
100
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -154,6 +154,29 @@ The trade-off is honest either way: an explicit `@latest` (or the exclusion)
|
|
|
154
154
|
opts modlens out of pnpm's supply-chain cooling-off window, so new releases
|
|
155
155
|
install immediately.
|
|
156
156
|
|
|
157
|
+
## fetch failed, or could not connect
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Could not connect to generativelanguage.googleapis.com (UND_ERR_CONNECT_TIMEOUT). The request never reached the network. ...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The API request never left the machine. On networks that reach the internet
|
|
164
|
+
through a proxy this is expected: Node's fetch ignores the proxy environment
|
|
165
|
+
variables by default. modlens honors them once you ask it to route that way,
|
|
166
|
+
in either form:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
HTTPS_PROXY=http://127.0.0.1:7890 modlens -i shot.png -p gemini-api # env (NO_PROXY honored too)
|
|
170
|
+
modlens config set proxy http://127.0.0.1:7890 # persistent, all API providers
|
|
171
|
+
modlens config set openai.proxy http://127.0.0.1:7890 # one provider only
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The proxy applies to API provider requests only. The remote-image download
|
|
175
|
+
path keeps its direct, IP-pinned connection on purpose: its SSRF guards
|
|
176
|
+
validate the exact address being contacted, and a proxy would blind them. On
|
|
177
|
+
a proxied machine, prefer local files or let the failover chain hand remote
|
|
178
|
+
URLs to a provider that fetches them upstream.
|
|
179
|
+
|
|
157
180
|
## Config file problems
|
|
158
181
|
|
|
159
182
|
```
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -20,11 +20,11 @@ powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 <args>
|
|
|
20
20
|
|
|
21
21
|
It resolves a working runtime (PATH `modlens`, then `npx`, then `bunx`) and forwards your arguments unchanged. Exit 78 means no runtime: relay the `nextSteps` from its stderr JSON instead of retrying.
|
|
22
22
|
|
|
23
|
-
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.
|
|
23
|
+
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.12.0):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.12.0: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.12.0 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.12.0 <args>`.
|
|
28
28
|
4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.13+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
29
29
|
|
|
30
30
|
`references/runtime.md` documents the pin and the diagnostic fields.
|
|
@@ -22,6 +22,7 @@ Everything lives under four top-level keys, all optional. This example shows eve
|
|
|
22
22
|
```json
|
|
23
23
|
{
|
|
24
24
|
"provider": "gemini-api",
|
|
25
|
+
"proxy": "http://127.0.0.1:7890",
|
|
25
26
|
"reuse": { "claude": true, "codex": true, "opencode": false, "pi": true, "grok": true },
|
|
26
27
|
"guards": {
|
|
27
28
|
"allowModels": ["deepseek-v4-*", "glm-5.*", "minimax-m2.5*", "qwen3-coder*"],
|
|
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
24
24
|
# package.json version, and the release script rewrites it on every bump.
|
|
25
25
|
$Package = '@liustack/modlens'
|
|
26
26
|
$Bin = 'modlens'
|
|
27
|
-
$Pinned = '3.
|
|
27
|
+
$Pinned = '3.12.0'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -22,7 +22,7 @@ set -eu
|
|
|
22
22
|
# package.json version, and the release script rewrites it on every bump.
|
|
23
23
|
PKG="@liustack/modlens"
|
|
24
24
|
BIN="modlens"
|
|
25
|
-
PINNED="3.
|
|
25
|
+
PINNED="3.12.0"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|