image-context-cascade-cli 0.1.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.
Files changed (2) hide show
  1. package/dist/main.js +226 -0
  2. package/package.json +47 -0
package/dist/main.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, createWriteStream } from "node:fs";
3
+ import { access, copyFile, readFile, rename, stat, unlink } from "node:fs/promises";
4
+ import { basename, dirname, extname, join } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { parseArgs } from "node:util";
7
+ import { cascadeImages } from "image-context-cascade";
8
+
9
+ const historicalStrategy = Object.assign(() => "historical", { cascadeStrategyName: "custom" });
10
+
11
+ function usage() {
12
+ return `image-cascade rescue <file> [--yes] [--all] [--json]\n\nRescue oversized image session files by downgrading historical image blocks.\n\nCommands:\n rescue <file> Analyze or rewrite a .jsonl session or a single JSON document.\n\nOptions:\n --yes Write changes. Default is dry-run and writes nothing.\n --all Downgrade all image blocks, including when no user-message boundary is found.\n --json Print machine-readable JSON statistics.\n --help, -h Show this help.\n\nNotes:\n JSONL mode uses two streaming passes and O(1) memory. Single JSON mode reads the whole file into memory.\n`;
13
+ }
14
+
15
+ function hasUserRole(value) {
16
+ if (!value || typeof value !== "object") return false;
17
+ if (Array.isArray(value)) return value.some(hasUserRole);
18
+ if (value.role === "user") return true;
19
+ return Object.values(value).some(hasUserRole);
20
+ }
21
+
22
+ function emptyStats(file, mode, dryRun) {
23
+ return {
24
+ file,
25
+ mode,
26
+ dryRun,
27
+ lines: 0,
28
+ boundaryLine: null,
29
+ found: 0,
30
+ downgraded: 0,
31
+ estimatedSavedChars: 0,
32
+ skippedLines: 0,
33
+ bytesBefore: 0,
34
+ estimatedBytesAfter: 0,
35
+ bytesAfter: null,
36
+ backup: null,
37
+ changed: false,
38
+ };
39
+ }
40
+
41
+ function addTelemetry(stats, telemetry) {
42
+ stats.found += telemetry.found ?? 0;
43
+ stats.downgraded += telemetry.downgraded ?? 0;
44
+ stats.estimatedSavedChars += telemetry.estimatedSavedChars ?? 0;
45
+ }
46
+
47
+ async function fileExists(path) {
48
+ try { await access(path); return true; } catch { return false; }
49
+ }
50
+
51
+ async function nextBackupPath(file) {
52
+ let candidate = `${file}.icc-backup`;
53
+ if (!(await fileExists(candidate))) return candidate;
54
+ for (let i = 1; ; i++) {
55
+ candidate = `${file}.icc-backup.${i}`;
56
+ if (!(await fileExists(candidate))) return candidate;
57
+ }
58
+ }
59
+
60
+ async function scanJsonlBoundary(file) {
61
+ let lineNo = 0;
62
+ let lastUserLine = 0;
63
+ const rl = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
64
+ for await (const line of rl) {
65
+ lineNo++;
66
+ try {
67
+ const obj = JSON.parse(line);
68
+ if (hasUserRole(obj)) lastUserLine = lineNo;
69
+ } catch {
70
+ // Boundary scan ignores malformed lines; second pass reports skippedLines.
71
+ }
72
+ }
73
+ return { lines: lineNo, boundaryLine: lastUserLine || null };
74
+ }
75
+
76
+ function lineOut(line, obj, result) {
77
+ return result.mutated ? `${JSON.stringify(result.payload)}\n` : `${line}\n`;
78
+ }
79
+
80
+ async function transformJsonl(file, opts, writePath) {
81
+ const first = await scanJsonlBoundary(file);
82
+ const st = await stat(file);
83
+ const stats = emptyStats(file, "jsonl", !opts.yes);
84
+ stats.lines = first.lines;
85
+ stats.boundaryLine = opts.all ? null : first.boundaryLine;
86
+ stats.bytesBefore = st.size;
87
+
88
+ let lineNo = 0;
89
+ let estimatedBytesAfter = 0;
90
+ let changed = false;
91
+ const out = writePath ? createWriteStream(writePath, { flags: "wx" }) : null;
92
+ const rl = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
93
+
94
+ try {
95
+ for await (const line of rl) {
96
+ lineNo++;
97
+ let output = `${line}\n`;
98
+ const shouldProcess = opts.all || (first.boundaryLine !== null && lineNo < first.boundaryLine);
99
+ if (shouldProcess) {
100
+ try {
101
+ const obj = JSON.parse(line);
102
+ const result = cascadeImages(obj, { strategy: historicalStrategy });
103
+ addTelemetry(stats, result.telemetry);
104
+ output = lineOut(line, obj, result);
105
+ if (result.mutated) changed = true;
106
+ } catch {
107
+ stats.skippedLines++;
108
+ }
109
+ }
110
+ estimatedBytesAfter += Buffer.byteLength(output);
111
+ if (out && !out.write(output)) await new Promise((resolve) => out.once("drain", resolve));
112
+ }
113
+ } finally {
114
+ if (out) await new Promise((resolve, reject) => out.end((err) => err ? reject(err) : resolve()));
115
+ }
116
+
117
+ stats.estimatedBytesAfter = changed ? estimatedBytesAfter : stats.bytesBefore;
118
+ stats.changed = changed;
119
+ return stats;
120
+ }
121
+
122
+ async function transformSingleJson(file, opts, writePath) {
123
+ const buf = await readFile(file);
124
+ const text = buf.toString("utf8");
125
+ const stats = emptyStats(file, "json", !opts.yes);
126
+ stats.bytesBefore = buf.length;
127
+ stats.lines = text.length === 0 ? 0 : text.split(/\r\n|\r|\n/).length;
128
+ let obj;
129
+ try {
130
+ obj = JSON.parse(text);
131
+ } catch (err) {
132
+ throw new Error(`Failed to parse JSON document: ${err.message}`);
133
+ }
134
+ const result = cascadeImages(obj, opts.all ? { strategy: historicalStrategy } : {});
135
+ addTelemetry(stats, result.telemetry);
136
+ stats.changed = result.mutated;
137
+ const output = result.mutated ? JSON.stringify(result.payload, null, 2) + "\n" : text;
138
+ stats.estimatedBytesAfter = result.mutated ? Buffer.byteLength(output) : stats.bytesBefore;
139
+ if (writePath) {
140
+ const stream = createWriteStream(writePath, { flags: "wx" });
141
+ await new Promise((resolve, reject) => stream.end(output, (err) => err ? reject(err) : resolve()));
142
+ }
143
+ return stats;
144
+ }
145
+
146
+ async function rescue(file, opts) {
147
+ try { await access(file); } catch { throw new Error(`File not found: ${file}`); }
148
+ const mode = extname(file).toLowerCase() === ".jsonl" ? "jsonl" : "json";
149
+ const dryStats = mode === "jsonl" ? await transformJsonl(file, opts, null) : await transformSingleJson(file, opts, null);
150
+ if (!opts.yes || !dryStats.changed) return dryStats;
151
+
152
+ const temp = join(dirname(file), `.${basename(file)}.icc-tmp-${process.pid}-${Date.now()}`);
153
+ try {
154
+ const writeStats = mode === "jsonl" ? await transformJsonl(file, opts, temp) : await transformSingleJson(file, opts, temp);
155
+ if (!writeStats.changed) {
156
+ await unlink(temp).catch(() => {});
157
+ return { ...writeStats, estimatedBytesAfter: writeStats.bytesBefore };
158
+ }
159
+ const backup = await nextBackupPath(file);
160
+ await copyFile(file, backup);
161
+ await rename(temp, file);
162
+ const after = await stat(file);
163
+ return { ...writeStats, backup, bytesAfter: after.size };
164
+ } catch (err) {
165
+ await unlink(temp).catch(() => {});
166
+ throw err;
167
+ }
168
+ }
169
+
170
+ function printStats(stats, json) {
171
+ if (json) {
172
+ console.log(JSON.stringify(stats, null, 2));
173
+ return;
174
+ }
175
+ console.log(`mode: ${stats.mode}`);
176
+ console.log(`lines: ${stats.lines}`);
177
+ if (stats.mode === "jsonl") console.log(`boundaryLine: ${stats.boundaryLine ?? "none"}`);
178
+ console.log(`found: ${stats.found}`);
179
+ console.log(`downgraded: ${stats.downgraded}`);
180
+ console.log(`skippedLines: ${stats.skippedLines}`);
181
+ console.log(`estimatedSavedChars: ${stats.estimatedSavedChars}`);
182
+ console.log(`bytesBefore: ${stats.bytesBefore}`);
183
+ console.log(`estimatedBytesAfter: ${stats.estimatedBytesAfter}`);
184
+ if (stats.bytesAfter !== null) console.log(`bytesAfter: ${stats.bytesAfter}`);
185
+ if (stats.backup) console.log(`backup: ${stats.backup}`);
186
+ if (stats.mode === "jsonl" && stats.boundaryLine === null && stats.downgraded === 0) {
187
+ console.log("no user-message boundary found; nothing downgraded. Use --all to downgrade all image blocks.");
188
+ }
189
+ if (stats.dryRun) console.log("dry-run: no files written; rerun with --yes to apply changes.");
190
+ else if (!stats.changed) console.log("no changes needed.");
191
+ }
192
+
193
+ export async function main(argv = process.argv.slice(2)) {
194
+ const parsed = parseArgs({
195
+ args: argv,
196
+ allowPositionals: true,
197
+ options: {
198
+ yes: { type: "boolean", default: false },
199
+ all: { type: "boolean", default: false },
200
+ json: { type: "boolean", default: false },
201
+ help: { type: "boolean", short: "h", default: false },
202
+ },
203
+ });
204
+ const [cmd, file] = parsed.positionals;
205
+ if (parsed.values.help || !cmd) {
206
+ console.log(usage());
207
+ return 0;
208
+ }
209
+ if (cmd !== "rescue" || !file) {
210
+ console.error(usage());
211
+ return 1;
212
+ }
213
+ try {
214
+ const stats = await rescue(file, parsed.values);
215
+ printStats(stats, Boolean(parsed.values.json));
216
+ return 0;
217
+ } catch (err) {
218
+ if (parsed.values.json) console.error(JSON.stringify({ error: err.message }));
219
+ else console.error(`error: ${err.message}`);
220
+ return 1;
221
+ }
222
+ }
223
+
224
+ if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}` || process.argv[1]?.endsWith("main.js")) {
225
+ process.exitCode = await main();
226
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "image-context-cascade-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Rescue CLI for oversized image-context-cascade session files.",
7
+ "bin": {
8
+ "image-cascade": "dist/main.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "default": "./dist/main.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/dlgod7/image-context-cascade.git",
28
+ "directory": "packages/cli"
29
+ },
30
+ "homepage": "https://github.com/dlgod7/image-context-cascade#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/dlgod7/image-context-cascade/issues"
33
+ },
34
+ "keywords": [
35
+ "llm",
36
+ "agent",
37
+ "context",
38
+ "image",
39
+ "middleware",
40
+ "cli",
41
+ "rescue"
42
+ ],
43
+ "dependencies": {
44
+ "image-context-cascade": "0.1.0"
45
+ },
46
+ "devDependencies": {}
47
+ }