@liustack/modlens 2.6.0 → 2.7.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/main.js +126 -22
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +5 -3
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ One catch: once text-only is declared, the Codex TUI **blocks Ctrl+V image paste
|
|
|
158
158
|
|
|
159
159
|
No setup needed: drag the image file into the terminal, or type its path, and the skill takes over.
|
|
160
160
|
|
|
161
|
-
Paste is trickier. If you run a text-only model behind `ANTHROPIC_BASE_URL`, Claude Code never writes pasted images to a regular temp file and has no modality switch, so a pasted image reaches the model as a pathless `[Unsupported Image]` placeholder (lenient gateways like DeepSeek's Anthropic endpoint) or breaks the request outright ([#62009](https://github.com/anthropics/claude-code/issues/62009)). But the bytes are not gone: Claude Code appends every user message, images included, to the local session transcript before the gateway ever sees it. That is what `modlens recover-paste` exploits: it pulls the most recent pasted images back out of the transcript and prints real file paths, ready for `modlens -i`. The skill runs this automatically when it spots the placeholder. Verified end to end in a real DeepSeek-gateway Claude Code session: paste an image, the model sees only the placeholder, recovers the file by session id, and answers with full image content. Transcripts are per-session files. Skills can pass the exact session via `--session` (Claude Code substitutes `${CLAUDE_SESSION_ID}` into skill text since v2.1.9); without it, recovery picks the transcript holding the newest pasted image by message timestamp, so concurrent sessions in the same project do not confuse it either way. [Pi](https://github.com/earendil-works/pi) stores sessions the same way (`~/.pi/agent/sessions/`, images as base64 in JSONL). [OpenCode](https://github.com/sst/opencode) keeps them in SQLite instead (`~/.local/share/opencode/opencode.db`, images as data URLs; reading it needs Node 22.5+ for node:sqlite). `recover-paste`
|
|
161
|
+
Paste is trickier. If you run a text-only model behind `ANTHROPIC_BASE_URL`, Claude Code never writes pasted images to a regular temp file and has no modality switch, so a pasted image reaches the model as a pathless `[Unsupported Image]` placeholder (lenient gateways like DeepSeek's Anthropic endpoint) or breaks the request outright ([#62009](https://github.com/anthropics/claude-code/issues/62009)). But the bytes are not gone: Claude Code appends every user message, images included, to the local session transcript before the gateway ever sees it. That is what `modlens recover-paste` exploits: it pulls the most recent pasted images back out of the transcript and prints real file paths, ready for `modlens -i`. The skill runs this automatically when it spots the placeholder. Verified end to end in a real DeepSeek-gateway Claude Code session: paste an image, the model sees only the placeholder, recovers the file by session id, and answers with full image content. Transcripts are per-session files. Skills can pass the exact session via `--session` (Claude Code substitutes `${CLAUDE_SESSION_ID}` into skill text since v2.1.9); without it, recovery picks the transcript holding the newest pasted image by message timestamp, so concurrent sessions in the same project do not confuse it either way. [Pi](https://github.com/earendil-works/pi) stores sessions the same way (`~/.pi/agent/sessions/`, images as base64 in JSONL). [OpenCode](https://github.com/sst/opencode) keeps them in SQLite instead (`~/.local/share/opencode/opencode.db`, images as data URLs; reading it needs Node 22.5+ for node:sqlite). `recover-paste` first identifies the harness it is running inside, by walking the process ancestry and checking env fingerprints (`CLAUDECODE`, `PI_CODING_AGENT`, `CODEX_THREAD_ID`), and reads only that harness's storage, so one tool's stale sessions can never hijack another tool's paste; in Claude Code it even targets the exact session from the injected session id, and in Codex it refuses outright and points back to the path tag. Only when detection comes up empty does it fall back to racing all three stores by newest image timestamp. Each harness verified against a real session. One honest caveat: transcript layouts are internal implementation details of those tools with no compatibility promise; if recovery ever breaks, dragging the file still works everywhere.
|
|
162
162
|
|
|
163
163
|
## Why a bridge instead of a multimodal model?
|
|
164
164
|
|
package/README.zh-CN.md
CHANGED
|
@@ -158,7 +158,7 @@ Codex 只认 Responses API,DeepSeek 官方端点原生支持。先照着[官
|
|
|
158
158
|
|
|
159
159
|
不用任何配置:把图片文件拖进终端,或手打路径,skill 直接接手。
|
|
160
160
|
|
|
161
|
-
粘贴要多说两句。走 `ANTHROPIC_BASE_URL` 网关跑纯文本模型时,Claude Code 粘贴的图片从不写普通临时文件,也没有声明模型无视觉的开关,粘贴的图要么变成一个不带路径的 `[Unsupported Image]` 占位符到达模型(DeepSeek 的 Anthropic 兼容端点这类宽容网关),要么直接把请求搞挂([#62009](https://github.com/anthropics/claude-code/issues/62009))。但图片字节没有蒸发:Claude Code 在网关看到消息之前,就把每条用户消息(含图片)原样写进了本地会话记录。`modlens recover-paste` 干的就是这件事:从会话记录里把最近粘贴的图捞回来,落成真实文件路径,直接喂给 `modlens -i`。skill 看到占位符会自动跑这一步。已在真实的 DeepSeek 网关 Claude Code 会话里端到端验证:粘贴一张图,模型只看到占位符,按会话 ID 捞回文件,带着完整图片内容回答。会话记录本来就是一个会话一个文件。skill 可以通过 `--session` 传入精确会话(Claude Code 从 v2.1.9 起会把 `${CLAUDE_SESSION_ID}` 替换进 skill 文本),不传时按消息时间戳挑「持有最新粘贴图」的那份,两条路都不怕同项目并发多开。[Pi](https://github.com/earendil-works/pi) 的会话存储和它同构(`~/.pi/agent/sessions/`,图片以 base64 存 JSONL)。[OpenCode](https://github.com/sst/opencode) 换了个存法,图片以 data URL 塞进 SQLite(`~/.local/share/opencode/opencode.db`,读它需要 Node 22.5+ 的 node:sqlite)。`recover-paste`
|
|
161
|
+
粘贴要多说两句。走 `ANTHROPIC_BASE_URL` 网关跑纯文本模型时,Claude Code 粘贴的图片从不写普通临时文件,也没有声明模型无视觉的开关,粘贴的图要么变成一个不带路径的 `[Unsupported Image]` 占位符到达模型(DeepSeek 的 Anthropic 兼容端点这类宽容网关),要么直接把请求搞挂([#62009](https://github.com/anthropics/claude-code/issues/62009))。但图片字节没有蒸发:Claude Code 在网关看到消息之前,就把每条用户消息(含图片)原样写进了本地会话记录。`modlens recover-paste` 干的就是这件事:从会话记录里把最近粘贴的图捞回来,落成真实文件路径,直接喂给 `modlens -i`。skill 看到占位符会自动跑这一步。已在真实的 DeepSeek 网关 Claude Code 会话里端到端验证:粘贴一张图,模型只看到占位符,按会话 ID 捞回文件,带着完整图片内容回答。会话记录本来就是一个会话一个文件。skill 可以通过 `--session` 传入精确会话(Claude Code 从 v2.1.9 起会把 `${CLAUDE_SESSION_ID}` 替换进 skill 文本),不传时按消息时间戳挑「持有最新粘贴图」的那份,两条路都不怕同项目并发多开。[Pi](https://github.com/earendil-works/pi) 的会话存储和它同构(`~/.pi/agent/sessions/`,图片以 base64 存 JSONL)。[OpenCode](https://github.com/sst/opencode) 换了个存法,图片以 data URL 塞进 SQLite(`~/.local/share/opencode/opencode.db`,读它需要 Node 22.5+ 的 node:sqlite)。`recover-paste` 会先搞清楚自己正跑在哪家宿主里(沿进程祖先链往上找,再核对 `CLAUDECODE`、`PI_CODING_AGENT`、`CODEX_THREAD_ID` 这些环境变量指纹),然后只读那一家的存储,别家的陈年会话再也没机会冒充。在 Claude Code 里还会直接用注入的会话 ID 精确定位,在 Codex 里则干脆拒绝执行并把你指回 path tag。实在识别不出来才退回按最新图片时间戳在三家赛跑。每家都拿真实会话验证过。一句老实话:会话记录格式是这些工具的内部实现,没有兼容承诺,哪天捞不动了,拖文件永远是保底。
|
|
162
162
|
|
|
163
163
|
## 为什么外挂,而不是换多模态模型?
|
|
164
164
|
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
+
import * as childProcess from "child_process";
|
|
5
6
|
import { spawn } from "child_process";
|
|
6
7
|
import * as os from "os";
|
|
7
8
|
import * as crypto from "crypto";
|
|
@@ -991,10 +992,10 @@ function opencodeQuery(dbPath, cwd, sessionId) {
|
|
|
991
992
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
992
993
|
try {
|
|
993
994
|
const resolved = path.resolve(cwd);
|
|
994
|
-
const sessionFilter = sessionId ? `AND (session.id = ? OR session.slug = ?)` : `AND session.directory =
|
|
995
|
-
const params = sessionId ? [sessionId, sessionId] : [resolved];
|
|
995
|
+
const sessionFilter = sessionId ? `AND (session.id = ? OR session.slug = ?)` : `AND (session.directory = ? OR session.directory LIKE ? || '/%' OR ? LIKE session.directory || '/%')`;
|
|
996
|
+
const params = sessionId ? [sessionId, sessionId] : [resolved, resolved, resolved];
|
|
996
997
|
const rows = db.prepare(
|
|
997
|
-
`SELECT part.data AS data, part.time_created AS time_created
|
|
998
|
+
`SELECT part.data AS data, part.time_created AS time_created, part.session_id AS session_id
|
|
998
999
|
FROM part
|
|
999
1000
|
JOIN message ON message.id = part.message_id
|
|
1000
1001
|
JOIN session ON session.id = part.session_id
|
|
@@ -1018,7 +1019,7 @@ function opencodeImagesFromRows(rows) {
|
|
|
1018
1019
|
}
|
|
1019
1020
|
const match = /^data:[^;]+;base64,(.+)$/.exec(part.url ?? "");
|
|
1020
1021
|
if (match) {
|
|
1021
|
-
images.push({ mediaType: part.mime, data: match[1] });
|
|
1022
|
+
images.push({ mediaType: part.mime, data: match[1], filename: part.filename });
|
|
1022
1023
|
}
|
|
1023
1024
|
} catch {
|
|
1024
1025
|
}
|
|
@@ -1033,18 +1034,19 @@ const opencodeAdapter = {
|
|
|
1033
1034
|
if (!fs.existsSync(dbPath)) {
|
|
1034
1035
|
return null;
|
|
1035
1036
|
}
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
if (images.length === 0) {
|
|
1037
|
+
const withImages = opencodeQuery(dbPath, cwd).map((row) => ({ row, images: opencodeImagesFromRows([row]) })).filter((entry) => entry.images.length > 0);
|
|
1038
|
+
if (withImages.length === 0) {
|
|
1039
1039
|
return null;
|
|
1040
1040
|
}
|
|
1041
|
+
const newest = withImages[withImages.length - 1];
|
|
1042
|
+
const scoped = withImages.filter((entry) => entry.row.session_id === newest.row.session_id);
|
|
1041
1043
|
return {
|
|
1042
1044
|
ref: {
|
|
1043
1045
|
harness: "opencode",
|
|
1044
1046
|
location: dbPath,
|
|
1045
|
-
extract: () =>
|
|
1047
|
+
extract: () => scoped.flatMap((entry) => entry.images)
|
|
1046
1048
|
},
|
|
1047
|
-
timestamp:
|
|
1049
|
+
timestamp: newest.row.time_created
|
|
1048
1050
|
};
|
|
1049
1051
|
},
|
|
1050
1052
|
findSession: (cwd, sessionId) => {
|
|
@@ -1063,6 +1065,64 @@ const opencodeAdapter = {
|
|
|
1063
1065
|
};
|
|
1064
1066
|
}
|
|
1065
1067
|
};
|
|
1068
|
+
const HARNESS_BY_BASENAME = {
|
|
1069
|
+
claude: "claude-code",
|
|
1070
|
+
"claude-code": "claude-code",
|
|
1071
|
+
pi: "pi",
|
|
1072
|
+
opencode: "opencode",
|
|
1073
|
+
codex: "codex"
|
|
1074
|
+
};
|
|
1075
|
+
function harnessFromPsTable(psOutput, startPid) {
|
|
1076
|
+
const table = /* @__PURE__ */ new Map();
|
|
1077
|
+
for (const line of psOutput.split("\n")) {
|
|
1078
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
1079
|
+
if (match) {
|
|
1080
|
+
table.set(Number(match[1]), { ppid: Number(match[2]), command: match[3] });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
let pid = table.get(startPid)?.ppid;
|
|
1084
|
+
for (let hops = 0; hops < 50 && pid !== void 0 && pid > 1; hops++) {
|
|
1085
|
+
const proc = table.get(pid);
|
|
1086
|
+
if (!proc) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
for (const token of proc.command.trim().split(/\s+/).slice(0, 8)) {
|
|
1090
|
+
const mapped = HARNESS_BY_BASENAME[path.basename(token)];
|
|
1091
|
+
if (mapped) {
|
|
1092
|
+
return mapped;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
pid = proc.ppid;
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
function detectHarness() {
|
|
1100
|
+
const override = process.env.MODLENS_HARNESS;
|
|
1101
|
+
if (override) {
|
|
1102
|
+
return override === "none" ? null : override;
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const ps = childProcess.execFileSync("ps", ["-Ao", "pid=,ppid=,command="], {
|
|
1106
|
+
encoding: "utf-8",
|
|
1107
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1108
|
+
});
|
|
1109
|
+
const found = harnessFromPsTable(ps, process.pid);
|
|
1110
|
+
if (found) {
|
|
1111
|
+
return found;
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
if (process.env.PI_CODING_AGENT) {
|
|
1116
|
+
return "pi";
|
|
1117
|
+
}
|
|
1118
|
+
if (process.env.CODEX_THREAD_ID || process.env.CODEX_SANDBOX) {
|
|
1119
|
+
return "codex";
|
|
1120
|
+
}
|
|
1121
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_SESSION_ID) {
|
|
1122
|
+
return "claude-code";
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1066
1126
|
const ADAPTERS = [claudeAdapter, piAdapter, opencodeAdapter];
|
|
1067
1127
|
function sourceForExplicitPath(filePath) {
|
|
1068
1128
|
if (filePath.endsWith(".db")) {
|
|
@@ -1077,9 +1137,9 @@ function sourceForExplicitPath(filePath) {
|
|
|
1077
1137
|
}
|
|
1078
1138
|
return jsonlSource("claude-code", filePath, claudeExtractLine);
|
|
1079
1139
|
}
|
|
1080
|
-
function locateSource(cwd) {
|
|
1140
|
+
function locateSource(cwd, adapters = ADAPTERS) {
|
|
1081
1141
|
let best = null;
|
|
1082
|
-
for (const adapter of
|
|
1142
|
+
for (const adapter of adapters) {
|
|
1083
1143
|
let candidate = null;
|
|
1084
1144
|
try {
|
|
1085
1145
|
candidate = adapter.findNewest(cwd);
|
|
@@ -1090,15 +1150,15 @@ function locateSource(cwd) {
|
|
|
1090
1150
|
}
|
|
1091
1151
|
}
|
|
1092
1152
|
if (!best) {
|
|
1093
|
-
const dirs =
|
|
1153
|
+
const dirs = adapters.map((a) => a.describe(cwd)).join(" , ");
|
|
1094
1154
|
throw new Error(
|
|
1095
1155
|
`No pasted images found in any session storage for this directory (looked in: ${dirs}). The user may not have pasted any, or the storage format changed; ask for a file path instead.`
|
|
1096
1156
|
);
|
|
1097
1157
|
}
|
|
1098
1158
|
return best.ref;
|
|
1099
1159
|
}
|
|
1100
|
-
function sourceForSession(cwd, sessionId) {
|
|
1101
|
-
for (const adapter of
|
|
1160
|
+
function sourceForSession(cwd, sessionId, adapters = ADAPTERS) {
|
|
1161
|
+
for (const adapter of adapters) {
|
|
1102
1162
|
try {
|
|
1103
1163
|
const ref = adapter.findSession(cwd, sessionId);
|
|
1104
1164
|
if (ref) {
|
|
@@ -1107,14 +1167,42 @@ function sourceForSession(cwd, sessionId) {
|
|
|
1107
1167
|
} catch {
|
|
1108
1168
|
}
|
|
1109
1169
|
}
|
|
1110
|
-
const dirs =
|
|
1170
|
+
const dirs = adapters.map((a) => a.describe(cwd)).join(" , ");
|
|
1111
1171
|
throw new Error(
|
|
1112
1172
|
`No session ${sessionId} with pasted images under this project (looked in: ${dirs}). Check --cwd, or drop --session to auto-locate by newest pasted image.`
|
|
1113
1173
|
);
|
|
1114
1174
|
}
|
|
1115
1175
|
function recoverPastedImages(options = {}) {
|
|
1116
1176
|
const cwd = options.cwd ?? process.cwd();
|
|
1117
|
-
const
|
|
1177
|
+
const detected = options.transcript ? null : options.harness ?? detectHarness();
|
|
1178
|
+
if (detected === "codex") {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
"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."
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const scoped = detected && detected !== "none" ? detected : null;
|
|
1184
|
+
if (scoped && !ADAPTERS.some((adapter) => adapter.name === scoped)) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`Unknown harness "${scoped}". Supported: claude-code, pi, opencode (or none to scan all).`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
const adapters = scoped ? ADAPTERS.filter((adapter) => adapter.name === scoped) : ADAPTERS;
|
|
1190
|
+
let source = null;
|
|
1191
|
+
if (options.transcript) {
|
|
1192
|
+
source = sourceForExplicitPath(options.transcript);
|
|
1193
|
+
} else if (options.session) {
|
|
1194
|
+
source = sourceForSession(cwd, options.session, adapters);
|
|
1195
|
+
} else {
|
|
1196
|
+
const envSession = detected === "claude-code" ? process.env.CLAUDE_CODE_SESSION_ID : void 0;
|
|
1197
|
+
if (envSession) {
|
|
1198
|
+
try {
|
|
1199
|
+
source = sourceForSession(cwd, envSession, adapters);
|
|
1200
|
+
} catch {
|
|
1201
|
+
source = null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
source ??= locateSource(cwd, adapters);
|
|
1205
|
+
}
|
|
1118
1206
|
const count = Math.max(1, options.count ?? 1);
|
|
1119
1207
|
const outDir = options.outDir ?? path.join(os.tmpdir(), "modlens-paste");
|
|
1120
1208
|
const all = source.extract();
|
|
@@ -1131,12 +1219,24 @@ function recoverPastedImages(options = {}) {
|
|
|
1131
1219
|
const ext = EXT_BY_MIME[image.mediaType] ?? "png";
|
|
1132
1220
|
const filePath = path.join(outDir, `paste-${hash}.${ext}`);
|
|
1133
1221
|
fs.writeFileSync(filePath, buffer);
|
|
1134
|
-
|
|
1222
|
+
const recovered = {
|
|
1223
|
+
path: filePath,
|
|
1224
|
+
mediaType: image.mediaType,
|
|
1225
|
+
bytes: buffer.length
|
|
1226
|
+
};
|
|
1227
|
+
if (image.filename) {
|
|
1228
|
+
recovered.filename = image.filename;
|
|
1229
|
+
}
|
|
1230
|
+
return recovered;
|
|
1135
1231
|
});
|
|
1136
|
-
|
|
1232
|
+
const result = { harness: source.harness, transcript: source.location, images };
|
|
1233
|
+
if (scoped) {
|
|
1234
|
+
result.detected = scoped;
|
|
1235
|
+
}
|
|
1236
|
+
return result;
|
|
1137
1237
|
}
|
|
1138
1238
|
const program = new Command();
|
|
1139
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("2.
|
|
1239
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("2.7.0");
|
|
1140
1240
|
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) => {
|
|
1141
1241
|
try {
|
|
1142
1242
|
const timeoutMs = Number.parseInt(options.timeout, 10);
|
|
@@ -1169,11 +1269,14 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
|
|
|
1169
1269
|
}
|
|
1170
1270
|
});
|
|
1171
1271
|
program.command("recover-paste").description(
|
|
1172
|
-
"Recover images pasted into Claude Code from
|
|
1272
|
+
"Recover images pasted into Claude Code, Pi, or OpenCode from local session storage (they never hit disk otherwise)"
|
|
1173
1273
|
).option("--count <n>", "How many recent pasted images to recover", "1").option("--out-dir <path>", "Directory to write recovered images to").option(
|
|
1174
1274
|
"--session <id>",
|
|
1175
1275
|
"Claude Code session id for exact targeting (skills get it via ${CLAUDE_SESSION_ID})"
|
|
1176
|
-
).option("--transcript <path>", "Explicit transcript .jsonl (overrides --session)").option(
|
|
1276
|
+
).option("--transcript <path>", "Explicit transcript .jsonl or .db (overrides --session)").option(
|
|
1277
|
+
"--harness <name>",
|
|
1278
|
+
"Force the storage scope: claude-code, pi, opencode, or none (default: auto-detect via process ancestry and env)"
|
|
1279
|
+
).option("--cwd <path>", "Project directory the image was pasted in", process.cwd()).action(async (options) => {
|
|
1177
1280
|
try {
|
|
1178
1281
|
const count = Number.parseInt(options.count, 10);
|
|
1179
1282
|
if (!Number.isFinite(count) || count <= 0) {
|
|
@@ -1184,7 +1287,8 @@ program.command("recover-paste").description(
|
|
|
1184
1287
|
outDir: options.outDir,
|
|
1185
1288
|
transcript: options.transcript,
|
|
1186
1289
|
session: options.session,
|
|
1187
|
-
cwd: options.cwd
|
|
1290
|
+
cwd: options.cwd,
|
|
1291
|
+
harness: options.harness
|
|
1188
1292
|
});
|
|
1189
1293
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1190
1294
|
`);
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -65,12 +65,14 @@ Harnesses rarely hand you a clean path. First identify which harness you are in,
|
|
|
65
65
|
|
|
66
66
|
**Codex** (you see a text tag like `<image name=[Image #1] path="/tmp/xxxx.png">`):
|
|
67
67
|
|
|
68
|
-
- Extract the `path` value from the tag and run modlens on it. Pasted images live in a temp file Codex already created; a stripped image keeps its path tag next to the placeholder. Do NOT use `recover-paste` here: it
|
|
68
|
+
- Extract the `path` value from the tag and run modlens on it. Pasted images live in a temp file Codex already created; a stripped image keeps its path tag next to the placeholder. Do NOT use `recover-paste` here: it detects Codex and refuses with this same guidance.
|
|
69
69
|
|
|
70
70
|
**Claude Code, Pi, or OpenCode** (no path tag anywhere; the image reads as `[Unsupported Image]`, a bare `[Image #1]`, or an attachment you simply cannot see):
|
|
71
71
|
|
|
72
|
-
- None of these harnesses writes pasted images to a regular temp file, but all of them persist user messages locally before any gateway strips them: Claude Code and Pi in session JSONL files (`~/.claude/projects/`, `~/.pi/agent/sessions/`), OpenCode in a SQLite database (`~/.local/share/opencode/opencode.db`, read via node:sqlite, needs Node 22.5+). `recover-paste`
|
|
73
|
-
-
|
|
72
|
+
- None of these harnesses writes pasted images to a regular temp file, but all of them persist user messages locally before any gateway strips them: Claude Code and Pi in session JSONL files (`~/.claude/projects/`, `~/.pi/agent/sessions/`), OpenCode in a SQLite database (`~/.local/share/opencode/opencode.db`, read via node:sqlite, needs Node 22.5+). Run `modlens recover-paste` from the project directory the conversation is happening in (add `--count <n>` for several images). It detects which harness it is running inside (process ancestry, then env fingerprints) and reads ONLY that harness's storage, so another tool's old sessions cannot leak in. In Claude Code it also targets your exact session automatically via the injected CLAUDE_CODE_SESSION_ID; `--session <id>` (e.g. from the ${CLAUDE_SESSION_ID} substitution) is only needed to override.
|
|
73
|
+
- The output is JSON with real file paths, ordered oldest to newest, so the LAST path is the user's most recent paste. Analyze that one first. Entries carry `filename` (the original attachment name) when the harness stored one; if the user's message or an error mentions a filename, match on it.
|
|
74
|
+
- Run every command yourself: `recover-paste`, then `modlens -i <path>` on the recovered file, then answer from the JSON. Never ask the user to run modlens or to relay paths.
|
|
75
|
+
- The output's `detected` field names the harness scope that was applied. If it is absent, detection failed and every store was scanned by newest-image timestamp: before describing anything, check that `harness` and `filename` match what you expect, force the scope with `--harness <claude-code|pi|opencode>` if they do not, and when in doubt ask the user for the file instead of describing the wrong image.
|
|
74
76
|
- If recovery fails (session storage is each harness's internals and may change), ask the user to drag the image file into the terminal or type its path.
|
|
75
77
|
|
|
76
78
|
**Any other harness, or nothing matches** (no path tag and `recover-paste` reports no transcripts): do not guess. Ask the user for the image file path, or suggest dragging the file into the terminal.
|