@liustack/modlens 2.7.4 → 2.7.5
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/dist/main.js +213 -151
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -7,128 +7,6 @@ import { spawn } from "child_process";
|
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
import * as crypto from "crypto";
|
|
9
9
|
import { createRequire } from "module";
|
|
10
|
-
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
11
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
12
|
-
const ENV_BINDINGS = {
|
|
13
|
-
"gemini-api": { apiKey: "GEMINI_API_KEY" },
|
|
14
|
-
openai: { apiKey: "OPENAI_API_KEY", baseUrl: "OPENAI_BASE_URL" },
|
|
15
|
-
anthropic: { apiKey: "ANTHROPIC_API_KEY", baseUrl: "ANTHROPIC_BASE_URL" }
|
|
16
|
-
};
|
|
17
|
-
function loadConfigFile(configPath = CONFIG_PATH) {
|
|
18
|
-
let raw;
|
|
19
|
-
try {
|
|
20
|
-
raw = fs.readFileSync(configPath, "utf-8");
|
|
21
|
-
} catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
if (!parsed || typeof parsed !== "object") {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
return parsed;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Failed to parse ${configPath}: ${error.message}. Fix or delete the file.`
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function defaultProviderName(config2) {
|
|
37
|
-
return config2.provider?.trim() || "antigravity-cli";
|
|
38
|
-
}
|
|
39
|
-
const PROVIDER_ALIASES = {
|
|
40
|
-
antigravity: "antigravity-cli",
|
|
41
|
-
agy: "antigravity-cli",
|
|
42
|
-
gemini: "gemini-api",
|
|
43
|
-
claude: "claude-cli"
|
|
44
|
-
};
|
|
45
|
-
function resolveProviderSettings(providerName, config2, env = process.env) {
|
|
46
|
-
const aliasNames = Object.entries(PROVIDER_ALIASES).filter(([, canonical]) => canonical === providerName).map(([alias]) => alias);
|
|
47
|
-
const fromFile = {
|
|
48
|
-
...Object.assign({}, ...aliasNames.map((alias) => config2.providers?.[alias] ?? {})),
|
|
49
|
-
...config2.providers?.[providerName] ?? {}
|
|
50
|
-
};
|
|
51
|
-
const bindings = ENV_BINDINGS[providerName] ?? {};
|
|
52
|
-
const settings = { ...fromFile };
|
|
53
|
-
for (const [field, envName] of Object.entries(bindings)) {
|
|
54
|
-
const value = env[envName]?.trim();
|
|
55
|
-
if (value) {
|
|
56
|
-
settings[field] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return settings;
|
|
60
|
-
}
|
|
61
|
-
function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
62
|
-
const config2 = loadConfigFile(configPath);
|
|
63
|
-
if (dottedKey === "provider") {
|
|
64
|
-
config2.provider = value;
|
|
65
|
-
} else {
|
|
66
|
-
const dot = dottedKey.indexOf(".");
|
|
67
|
-
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model>".`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
const providerName = dottedKey.slice(0, dot);
|
|
73
|
-
const field = dottedKey.slice(dot + 1);
|
|
74
|
-
if (!["apiKey", "baseUrl", "model"].includes(field)) {
|
|
75
|
-
throw new Error(`Unknown config field: ${field}. Use apiKey, baseUrl, or model.`);
|
|
76
|
-
}
|
|
77
|
-
config2.providers ??= {};
|
|
78
|
-
config2.providers[providerName] ??= {};
|
|
79
|
-
config2.providers[providerName][field] = value;
|
|
80
|
-
}
|
|
81
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
82
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
83
|
-
`, { mode: 384 });
|
|
84
|
-
try {
|
|
85
|
-
fs.chmodSync(configPath, 384);
|
|
86
|
-
} catch {
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const CONFIG_TEMPLATE = {
|
|
90
|
-
provider: "antigravity-cli",
|
|
91
|
-
providers: {
|
|
92
|
-
"antigravity-cli": { model: "gemini-3.6-flash-low" },
|
|
93
|
-
"gemini-api": { apiKey: "", model: "gemini-3.6-flash" },
|
|
94
|
-
openai: { baseUrl: "", apiKey: "", model: "" },
|
|
95
|
-
anthropic: { apiKey: "", model: "claude-haiku-4-5-20251001" },
|
|
96
|
-
"claude-cli": { model: "haiku" }
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
function initConfigFile(configPath = CONFIG_PATH, force = false) {
|
|
100
|
-
if (!force && fs.existsSync(configPath)) {
|
|
101
|
-
throw new Error(`${configPath} already exists. Use --force to overwrite.`);
|
|
102
|
-
}
|
|
103
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
104
|
-
fs.writeFileSync(configPath, `${JSON.stringify(CONFIG_TEMPLATE, null, 2)}
|
|
105
|
-
`, { mode: 384 });
|
|
106
|
-
try {
|
|
107
|
-
fs.chmodSync(configPath, 384);
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function renderConfig(config2) {
|
|
112
|
-
const masked = {
|
|
113
|
-
...config2,
|
|
114
|
-
providers: Object.fromEntries(
|
|
115
|
-
Object.entries(config2.providers ?? {}).map(([name, settings]) => [
|
|
116
|
-
name,
|
|
117
|
-
{
|
|
118
|
-
...settings,
|
|
119
|
-
...settings.apiKey ? { apiKey: maskKey(settings.apiKey) } : {}
|
|
120
|
-
}
|
|
121
|
-
])
|
|
122
|
-
)
|
|
123
|
-
};
|
|
124
|
-
return JSON.stringify(masked, null, 2);
|
|
125
|
-
}
|
|
126
|
-
function maskKey(key) {
|
|
127
|
-
if (key.length <= 8) {
|
|
128
|
-
return "****";
|
|
129
|
-
}
|
|
130
|
-
return `${key.slice(0, 6)}...${key.slice(-2)}`;
|
|
131
|
-
}
|
|
132
10
|
function buildVisionPrompt(options) {
|
|
133
11
|
const readInstruction = options.imageKind === "inline" ? "Analyze the image attached to this message." : options.imageKind === "remote" ? `Fetch the image at this URL and analyze it: ${options.imageSource}` : `Read the image file at this path and analyze it: ${options.imageSource}`;
|
|
134
12
|
const basePrompt = `${readInstruction}
|
|
@@ -370,6 +248,23 @@ function agyLogDir() {
|
|
|
370
248
|
return path.join(os.homedir(), ".gemini", "antigravity-cli", "log");
|
|
371
249
|
}
|
|
372
250
|
const LOG_FRESHNESS_MS = 2 * 60 * 1e3;
|
|
251
|
+
function parseAgyLogTime(line, now = /* @__PURE__ */ new Date()) {
|
|
252
|
+
const match = /\b[IWEF](\d{2})(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?/.exec(line);
|
|
253
|
+
if (!match) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const [, month, day, hour, minute, second, fraction] = match;
|
|
257
|
+
const stamp = new Date(
|
|
258
|
+
now.getFullYear(),
|
|
259
|
+
Number(month) - 1,
|
|
260
|
+
Number(day),
|
|
261
|
+
Number(hour),
|
|
262
|
+
Number(minute),
|
|
263
|
+
Number(second),
|
|
264
|
+
fraction ? Number(fraction.slice(0, 3)) : 0
|
|
265
|
+
).getTime();
|
|
266
|
+
return stamp - now.getTime() > 24 * 60 * 60 * 1e3 ? new Date(new Date(stamp).setFullYear(now.getFullYear() - 1)).getTime() : stamp;
|
|
267
|
+
}
|
|
373
268
|
function readRecentAgyLog(since) {
|
|
374
269
|
try {
|
|
375
270
|
const dir = agyLogDir();
|
|
@@ -380,7 +275,11 @@ function readRecentAgyLog(since) {
|
|
|
380
275
|
if (!newest || newest.mtime < since) {
|
|
381
276
|
return "";
|
|
382
277
|
}
|
|
383
|
-
|
|
278
|
+
const recent = fs.readFileSync(newest.full, "utf-8").slice(-64e3).split("\n").filter((line) => {
|
|
279
|
+
const stamp = parseAgyLogTime(line);
|
|
280
|
+
return stamp !== null && stamp >= since;
|
|
281
|
+
});
|
|
282
|
+
return recent.join("\n");
|
|
384
283
|
} catch {
|
|
385
284
|
return "";
|
|
386
285
|
}
|
|
@@ -743,11 +642,8 @@ Respond with ONE JSON object only, no markdown fences, no commentary. Fill this
|
|
|
743
642
|
if (result === null) {
|
|
744
643
|
throw new Error(`OpenAI-compatible API returned non-JSON output: ${truncate(text)}`);
|
|
745
644
|
}
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
(field) => shaped[field] === void 0 || shaped[field] === null
|
|
749
|
-
);
|
|
750
|
-
if (missing.length > 0 || typeof shaped.summary !== "string" || typeof shaped.ocr !== "object" || !Array.isArray(shaped.uncertainty)) {
|
|
645
|
+
const missing = missingSchemaFields(result);
|
|
646
|
+
if (missing.length > 0) {
|
|
751
647
|
throw new Error(
|
|
752
648
|
`OpenAI-compatible API returned JSON that does not match the vision schema${missing.length > 0 ? ` (missing: ${missing.join(", ")})` : ""}. Retry, or switch to gemini-api / anthropic for enforced schemas. Got: ${truncate(text)}`
|
|
753
649
|
);
|
|
@@ -761,6 +657,28 @@ Respond with ONE JSON object only, no markdown fences, no commentary. Fill this
|
|
|
761
657
|
}
|
|
762
658
|
};
|
|
763
659
|
}
|
|
660
|
+
function missingSchemaFields(result) {
|
|
661
|
+
const missing = [];
|
|
662
|
+
const root = result ?? {};
|
|
663
|
+
const child = (key) => root[key] && typeof root[key] === "object" ? root[key] : {};
|
|
664
|
+
const expect = (path2, ok) => {
|
|
665
|
+
if (!ok) {
|
|
666
|
+
missing.push(path2);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
expect("summary", typeof root.summary === "string");
|
|
670
|
+
expect("ocr", typeof root.ocr === "object" && root.ocr !== null);
|
|
671
|
+
expect("ocr.full_text", typeof child("ocr").full_text === "string");
|
|
672
|
+
expect("ocr.lines", Array.isArray(child("ocr").lines));
|
|
673
|
+
expect("layout", typeof root.layout === "object" && root.layout !== null);
|
|
674
|
+
expect("layout.regions", Array.isArray(child("layout").regions));
|
|
675
|
+
expect("semantics", typeof root.semantics === "object" && root.semantics !== null);
|
|
676
|
+
expect("semantics.scene", typeof child("semantics").scene === "string");
|
|
677
|
+
expect("semantics.entities", Array.isArray(child("semantics").entities));
|
|
678
|
+
expect("visual", typeof root.visual === "object" && root.visual !== null);
|
|
679
|
+
expect("uncertainty", Array.isArray(root.uncertainty));
|
|
680
|
+
return missing;
|
|
681
|
+
}
|
|
764
682
|
function toDataUrl(image) {
|
|
765
683
|
return `data:${image.mimeType};base64,${image.data}`;
|
|
766
684
|
}
|
|
@@ -795,9 +713,135 @@ function resolveProvider(providerName = "antigravity-cli") {
|
|
|
795
713
|
}
|
|
796
714
|
return provider;
|
|
797
715
|
}
|
|
716
|
+
function providerAliases() {
|
|
717
|
+
return Object.fromEntries(
|
|
718
|
+
Object.entries(PROVIDERS).map(([alias, provider]) => [alias, provider.name])
|
|
719
|
+
);
|
|
720
|
+
}
|
|
798
721
|
function listProviders() {
|
|
799
722
|
return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
|
|
800
723
|
}
|
|
724
|
+
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
725
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
726
|
+
const ENV_BINDINGS = {
|
|
727
|
+
"gemini-api": { apiKey: "GEMINI_API_KEY" },
|
|
728
|
+
openai: { apiKey: "OPENAI_API_KEY", baseUrl: "OPENAI_BASE_URL" },
|
|
729
|
+
anthropic: { apiKey: "ANTHROPIC_API_KEY", baseUrl: "ANTHROPIC_BASE_URL" }
|
|
730
|
+
};
|
|
731
|
+
function loadConfigFile(configPath = CONFIG_PATH) {
|
|
732
|
+
let raw;
|
|
733
|
+
try {
|
|
734
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error.code === "ENOENT") {
|
|
737
|
+
return {};
|
|
738
|
+
}
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Cannot read ${configPath}: ${error.message}. Fix the file or its permissions.`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const parsed = JSON.parse(raw);
|
|
745
|
+
if (!parsed || typeof parsed !== "object") {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
return parsed;
|
|
749
|
+
} catch (error) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`Failed to parse ${configPath}: ${error.message}. Fix or delete the file.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function defaultProviderName(config2) {
|
|
756
|
+
return config2.provider?.trim() || "antigravity-cli";
|
|
757
|
+
}
|
|
758
|
+
function resolveProviderSettings(providerName, config2, env = process.env) {
|
|
759
|
+
const aliasNames = Object.entries(providerAliases()).filter(([alias, canonical]) => canonical === providerName && alias !== providerName).map(([alias]) => alias);
|
|
760
|
+
const fromFile = {
|
|
761
|
+
...Object.assign({}, ...aliasNames.map((alias) => config2.providers?.[alias] ?? {})),
|
|
762
|
+
...config2.providers?.[providerName] ?? {}
|
|
763
|
+
};
|
|
764
|
+
const bindings = ENV_BINDINGS[providerName] ?? {};
|
|
765
|
+
const settings = { ...fromFile };
|
|
766
|
+
for (const [field, envName] of Object.entries(bindings)) {
|
|
767
|
+
const value = env[envName]?.trim();
|
|
768
|
+
if (value) {
|
|
769
|
+
settings[field] = value;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return settings;
|
|
773
|
+
}
|
|
774
|
+
function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
775
|
+
const config2 = loadConfigFile(configPath);
|
|
776
|
+
if (dottedKey === "provider") {
|
|
777
|
+
config2.provider = value;
|
|
778
|
+
} else {
|
|
779
|
+
const dot = dottedKey.indexOf(".");
|
|
780
|
+
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model>".`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const providerName = dottedKey.slice(0, dot);
|
|
786
|
+
const field = dottedKey.slice(dot + 1);
|
|
787
|
+
if (!["apiKey", "baseUrl", "model"].includes(field)) {
|
|
788
|
+
throw new Error(`Unknown config field: ${field}. Use apiKey, baseUrl, or model.`);
|
|
789
|
+
}
|
|
790
|
+
config2.providers ??= {};
|
|
791
|
+
config2.providers[providerName] ??= {};
|
|
792
|
+
config2.providers[providerName][field] = value;
|
|
793
|
+
}
|
|
794
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
795
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
796
|
+
`, { mode: 384 });
|
|
797
|
+
try {
|
|
798
|
+
fs.chmodSync(configPath, 384);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const CONFIG_TEMPLATE = {
|
|
803
|
+
provider: "antigravity-cli",
|
|
804
|
+
providers: {
|
|
805
|
+
"antigravity-cli": { model: "gemini-3.6-flash-low" },
|
|
806
|
+
"gemini-api": { apiKey: "", model: "gemini-3.6-flash" },
|
|
807
|
+
openai: { baseUrl: "", apiKey: "", model: "" },
|
|
808
|
+
anthropic: { apiKey: "", model: "claude-haiku-4-5-20251001" },
|
|
809
|
+
"claude-cli": { model: "haiku" }
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
function initConfigFile(configPath = CONFIG_PATH, force = false) {
|
|
813
|
+
if (!force && fs.existsSync(configPath)) {
|
|
814
|
+
throw new Error(`${configPath} already exists. Use --force to overwrite.`);
|
|
815
|
+
}
|
|
816
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
817
|
+
fs.writeFileSync(configPath, `${JSON.stringify(CONFIG_TEMPLATE, null, 2)}
|
|
818
|
+
`, { mode: 384 });
|
|
819
|
+
try {
|
|
820
|
+
fs.chmodSync(configPath, 384);
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function renderConfig(config2) {
|
|
825
|
+
const masked = {
|
|
826
|
+
...config2,
|
|
827
|
+
providers: Object.fromEntries(
|
|
828
|
+
Object.entries(config2.providers ?? {}).map(([name, settings]) => [
|
|
829
|
+
name,
|
|
830
|
+
{
|
|
831
|
+
...settings,
|
|
832
|
+
...settings.apiKey ? { apiKey: maskKey(settings.apiKey) } : {}
|
|
833
|
+
}
|
|
834
|
+
])
|
|
835
|
+
)
|
|
836
|
+
};
|
|
837
|
+
return JSON.stringify(masked, null, 2);
|
|
838
|
+
}
|
|
839
|
+
function maskKey(key) {
|
|
840
|
+
if (key.length <= 8) {
|
|
841
|
+
return "****";
|
|
842
|
+
}
|
|
843
|
+
return `${key.slice(0, 6)}...${key.slice(-2)}`;
|
|
844
|
+
}
|
|
801
845
|
const DEFAULT_TIMEOUT_MS = 18e4;
|
|
802
846
|
const KILL_GRACE_MS = 3e4;
|
|
803
847
|
const DRAIN_GRACE_MS = 500;
|
|
@@ -972,37 +1016,44 @@ const EXT_BY_MIME = {
|
|
|
972
1016
|
"image/webp": "webp",
|
|
973
1017
|
"image/gif": "gif"
|
|
974
1018
|
};
|
|
975
|
-
function transcriptBelongsTo(
|
|
1019
|
+
function transcriptBelongsTo(lines, cwd) {
|
|
976
1020
|
const wanted = path.resolve(cwd);
|
|
977
|
-
let
|
|
978
|
-
|
|
979
|
-
raw = fs.readFileSync(filePath, "utf-8");
|
|
980
|
-
} catch {
|
|
981
|
-
return false;
|
|
982
|
-
}
|
|
983
|
-
for (const line of raw.split("\n")) {
|
|
1021
|
+
let sawCwd = false;
|
|
1022
|
+
for (const line of lines) {
|
|
984
1023
|
if (!line.includes('"cwd"')) {
|
|
985
1024
|
continue;
|
|
986
1025
|
}
|
|
987
1026
|
try {
|
|
988
1027
|
const recorded = JSON.parse(line).cwd;
|
|
989
|
-
if (typeof recorded
|
|
990
|
-
|
|
991
|
-
|
|
1028
|
+
if (typeof recorded !== "string") {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
sawCwd = true;
|
|
1032
|
+
const resolved = path.resolve(recorded);
|
|
1033
|
+
if (resolved === wanted || resolved.startsWith(`${wanted}${path.sep}`)) {
|
|
1034
|
+
return true;
|
|
992
1035
|
}
|
|
993
1036
|
} catch {
|
|
994
1037
|
}
|
|
995
1038
|
}
|
|
996
|
-
return
|
|
1039
|
+
return !sawCwd;
|
|
997
1040
|
}
|
|
998
|
-
function
|
|
999
|
-
let raw;
|
|
1041
|
+
function readLines(filePath) {
|
|
1000
1042
|
try {
|
|
1001
|
-
|
|
1043
|
+
return fs.readFileSync(filePath, "utf-8").split("\n");
|
|
1002
1044
|
} catch {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function forEachJsonLine(filePath, visit) {
|
|
1049
|
+
const lines = readLines(filePath);
|
|
1050
|
+
if (!lines) {
|
|
1003
1051
|
return;
|
|
1004
1052
|
}
|
|
1005
|
-
|
|
1053
|
+
forEachParsedLine(lines, visit);
|
|
1054
|
+
}
|
|
1055
|
+
function forEachParsedLine(lines, visit) {
|
|
1056
|
+
for (const line of lines) {
|
|
1006
1057
|
if (!line.includes('"image"')) {
|
|
1007
1058
|
continue;
|
|
1008
1059
|
}
|
|
@@ -1025,9 +1076,9 @@ function jsonlSource(harness, filePath, extractLine) {
|
|
|
1025
1076
|
}
|
|
1026
1077
|
};
|
|
1027
1078
|
}
|
|
1028
|
-
function newestJsonlTimestamp(
|
|
1079
|
+
function newestJsonlTimestamp(lines, extractLine) {
|
|
1029
1080
|
let latest = null;
|
|
1030
|
-
|
|
1081
|
+
forEachParsedLine(lines, (line) => {
|
|
1031
1082
|
if (extractLine(line).length === 0) {
|
|
1032
1083
|
return;
|
|
1033
1084
|
}
|
|
@@ -1054,10 +1105,11 @@ function jsonlAdapter(options) {
|
|
|
1054
1105
|
findNewest: (cwd) => {
|
|
1055
1106
|
let best = null;
|
|
1056
1107
|
for (const file of listJsonl(dirFor(cwd))) {
|
|
1057
|
-
|
|
1108
|
+
const lines = readLines(file);
|
|
1109
|
+
if (!lines || !transcriptBelongsTo(lines, cwd)) {
|
|
1058
1110
|
continue;
|
|
1059
1111
|
}
|
|
1060
|
-
const timestamp = newestJsonlTimestamp(
|
|
1112
|
+
const timestamp = newestJsonlTimestamp(lines, extractLine);
|
|
1061
1113
|
if (timestamp !== null && (!best || timestamp > best.timestamp)) {
|
|
1062
1114
|
best = { ref: jsonlSource(name, file, extractLine), timestamp };
|
|
1063
1115
|
}
|
|
@@ -1066,7 +1118,11 @@ function jsonlAdapter(options) {
|
|
|
1066
1118
|
},
|
|
1067
1119
|
findSession: (cwd, sessionId) => {
|
|
1068
1120
|
for (const file of listJsonl(dirFor(cwd))) {
|
|
1069
|
-
if (matchesSession(path.basename(file), sessionId)
|
|
1121
|
+
if (!matchesSession(path.basename(file), sessionId)) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const lines = readLines(file);
|
|
1125
|
+
if (lines && transcriptBelongsTo(lines, cwd)) {
|
|
1070
1126
|
return jsonlSource(name, file, extractLine);
|
|
1071
1127
|
}
|
|
1072
1128
|
}
|
|
@@ -1240,7 +1296,7 @@ function harnessFromPsTable(psOutput, startPid) {
|
|
|
1240
1296
|
const tokens = proc.command.trim().split(/\s+/);
|
|
1241
1297
|
const candidates = [tokens[0]];
|
|
1242
1298
|
if (/^(node|bun|deno)$/.test(path.basename(tokens[0] ?? ""))) {
|
|
1243
|
-
const script = tokens.slice(1).find((token) => !token.startsWith("-"));
|
|
1299
|
+
const script = tokens.slice(1).find((token) => !token.startsWith("-") && /[/\\]|\.(m|c)?[jt]s$/.test(token));
|
|
1244
1300
|
if (script) {
|
|
1245
1301
|
candidates.push(script);
|
|
1246
1302
|
}
|
|
@@ -1348,6 +1404,12 @@ function recoverPastedImages(options = {}) {
|
|
|
1348
1404
|
"This is a Codex session: pasted images already exist as temp files, and each image tag in the message carries its path. Read the path from the tag instead of running recover-paste."
|
|
1349
1405
|
);
|
|
1350
1406
|
}
|
|
1407
|
+
const requested = options.harness?.trim();
|
|
1408
|
+
if (requested && requested !== "none" && !ADAPTERS.some((a) => a.name === requested)) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`Unknown harness "${requested}". Supported: ${ADAPTERS.map((a) => a.name).join(", ")} (or none to scan all).`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1351
1413
|
const scoped = detected && detected !== "none" ? detected : null;
|
|
1352
1414
|
if (scoped && !ADAPTERS.some((adapter) => adapter.name === scoped)) {
|
|
1353
1415
|
throw new Error(
|
|
@@ -1412,7 +1474,7 @@ function recoverPastedImages(options = {}) {
|
|
|
1412
1474
|
return result;
|
|
1413
1475
|
}
|
|
1414
1476
|
const program = new Command();
|
|
1415
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("2.7.
|
|
1477
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("2.7.5");
|
|
1416
1478
|
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").action(async (options) => {
|
|
1417
1479
|
try {
|
|
1418
1480
|
const timeoutMs = Number.parseInt(options.timeout, 10);
|