@ridit/lens 0.2.4 → 0.2.6
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/LENS.md +32 -68
- package/README.md +91 -0
- package/addons/README.md +3 -0
- package/addons/run-tests.js +127 -0
- package/dist/index.mjs +226459 -2638
- package/package.json +13 -4
- package/src/colors.ts +5 -0
- package/src/commands/commit.tsx +686 -0
- package/src/commands/provider.tsx +36 -22
- package/src/components/__tests__/Header.test.tsx +9 -0
- package/src/components/chat/ChatMessage.tsx +6 -6
- package/src/components/chat/ChatOverlays.tsx +20 -10
- package/src/components/chat/ChatRunner.tsx +197 -31
- package/src/components/provider/ApiKeyStep.tsx +77 -121
- package/src/components/provider/ModelStep.tsx +35 -20
- package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
- package/src/components/provider/ProviderTypeStep.tsx +12 -5
- package/src/components/provider/RemoveProviderStep.tsx +7 -8
- package/src/components/repo/RepoAnalysis.tsx +1 -1
- package/src/components/task/TaskRunner.tsx +1 -1
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +506 -423
- package/src/index.tsx +38 -0
- package/src/prompts/fewshot.ts +144 -47
- package/src/prompts/system.ts +25 -21
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/pdf.ts +136 -78
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +6 -3
- package/src/utils/chat.ts +38 -23
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +39 -32
- package/src/utils/tools/registry.ts +0 -14
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// this tool will be used by view-image tool for conversion of unsupported image formats
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import type { Tool } from "@ridit/lens-sdk";
|
|
7
|
+
|
|
8
|
+
interface ConvertImageInput {
|
|
9
|
+
input: string | string[];
|
|
10
|
+
|
|
11
|
+
output: string;
|
|
12
|
+
|
|
13
|
+
resize?: string;
|
|
14
|
+
|
|
15
|
+
crop?: string;
|
|
16
|
+
|
|
17
|
+
rotate?: 90 | 180 | 270;
|
|
18
|
+
|
|
19
|
+
flip?: "h" | "v" | "both";
|
|
20
|
+
|
|
21
|
+
quality?: number;
|
|
22
|
+
|
|
23
|
+
grayscale?: boolean;
|
|
24
|
+
|
|
25
|
+
blur?: number;
|
|
26
|
+
|
|
27
|
+
sharpen?: number;
|
|
28
|
+
|
|
29
|
+
strip?: boolean;
|
|
30
|
+
|
|
31
|
+
watermark?: string;
|
|
32
|
+
|
|
33
|
+
frames?: number;
|
|
34
|
+
|
|
35
|
+
gifDelay?: number;
|
|
36
|
+
|
|
37
|
+
gifLoop?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseInput(body: string): ConvertImageInput | null {
|
|
41
|
+
const trimmed = body.trim();
|
|
42
|
+
if (!trimmed) return null;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(trimmed) as ConvertImageInput;
|
|
45
|
+
if (!parsed.input || !parsed.output) return null;
|
|
46
|
+
return parsed;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ffmpegAvailable(): boolean {
|
|
53
|
+
try {
|
|
54
|
+
execSync("ffmpeg -version", { stdio: "pipe", timeout: 5000 });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolve(p: string, repoPath: string): string {
|
|
62
|
+
return path.isAbsolute(p) ? p : path.join(repoPath, p);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ensureDir(filePath: string): void {
|
|
66
|
+
const dir = path.dirname(filePath);
|
|
67
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildVfFilters(input: ConvertImageInput): string[] {
|
|
71
|
+
const filters: string[] = [];
|
|
72
|
+
|
|
73
|
+
if (input.resize) {
|
|
74
|
+
const [w, h] = input.resize.split("x");
|
|
75
|
+
const fw = w === "-1" ? "-2" : (w ?? "-2");
|
|
76
|
+
const fh = h === "-1" ? "-2" : (h ?? "-2");
|
|
77
|
+
filters.push(`scale=${fw}:${fh}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (input.crop) {
|
|
81
|
+
const parts = input.crop.split(/[x:]/);
|
|
82
|
+
const [cw, ch, cx = "0", cy = "0"] = parts;
|
|
83
|
+
filters.push(`crop=${cw}:${ch}:${cx}:${cy}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input.rotate) {
|
|
87
|
+
const transposeMap: Record<number, string> = {
|
|
88
|
+
90: "transpose=1",
|
|
89
|
+
180: "transpose=2,transpose=2",
|
|
90
|
+
270: "transpose=2",
|
|
91
|
+
};
|
|
92
|
+
if (transposeMap[input.rotate]) filters.push(transposeMap[input.rotate]!);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (input.flip) {
|
|
96
|
+
if (input.flip === "h" || input.flip === "both") filters.push("hflip");
|
|
97
|
+
if (input.flip === "v" || input.flip === "both") filters.push("vflip");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (input.grayscale) {
|
|
101
|
+
filters.push("hue=s=0");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (input.blur && input.blur > 0) {
|
|
105
|
+
filters.push(`gblur=sigma=${input.blur}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (input.sharpen && input.sharpen > 0) {
|
|
109
|
+
filters.push(`unsharp=5:5:${input.sharpen}:5:5:0`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return filters;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildArgs(
|
|
116
|
+
input: ConvertImageInput,
|
|
117
|
+
resolvedInput: string,
|
|
118
|
+
resolvedOutput: string,
|
|
119
|
+
repoPath: string,
|
|
120
|
+
): string[] {
|
|
121
|
+
const args: string[] = ["-y"];
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(input.input)) {
|
|
124
|
+
const delay = input.gifDelay ?? 10;
|
|
125
|
+
const loop = input.gifLoop ?? 0;
|
|
126
|
+
|
|
127
|
+
for (const src of input.input) {
|
|
128
|
+
args.push("-i", resolve(src, repoPath));
|
|
129
|
+
}
|
|
130
|
+
args.push(
|
|
131
|
+
"-filter_complex",
|
|
132
|
+
`concat=n=${input.input.length}:v=1:a=0,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
|
|
133
|
+
"-loop",
|
|
134
|
+
String(loop),
|
|
135
|
+
resolvedOutput,
|
|
136
|
+
);
|
|
137
|
+
return args;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (input.frames) {
|
|
141
|
+
args.push("-i", resolvedInput);
|
|
142
|
+
args.push("-vframes", String(input.frames));
|
|
143
|
+
|
|
144
|
+
args.push(resolvedOutput);
|
|
145
|
+
return args;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (input.watermark) {
|
|
149
|
+
const wmPath = resolve(input.watermark, repoPath);
|
|
150
|
+
args.push("-i", resolvedInput, "-i", wmPath);
|
|
151
|
+
args.push(
|
|
152
|
+
"-filter_complex",
|
|
153
|
+
"[1:v]scale=iw/4:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10",
|
|
154
|
+
);
|
|
155
|
+
const vf = buildVfFilters(input);
|
|
156
|
+
if (vf.length) args.push("-vf", vf.join(","));
|
|
157
|
+
} else {
|
|
158
|
+
args.push("-i", resolvedInput);
|
|
159
|
+
const vf = buildVfFilters(input);
|
|
160
|
+
if (vf.length) args.push("-vf", vf.join(","));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ext = path.extname(resolvedOutput).toLowerCase();
|
|
164
|
+
if (input.quality !== undefined) {
|
|
165
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
166
|
+
const q = Math.round(2 + ((100 - input.quality) / 100) * 29);
|
|
167
|
+
args.push("-q:v", String(q));
|
|
168
|
+
} else if (ext === ".webp") {
|
|
169
|
+
args.push("-quality", String(input.quality));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (input.strip) {
|
|
174
|
+
args.push("-map_metadata", "-1");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const imageExts = [
|
|
178
|
+
".png",
|
|
179
|
+
".jpg",
|
|
180
|
+
".jpeg",
|
|
181
|
+
".webp",
|
|
182
|
+
".avif",
|
|
183
|
+
".bmp",
|
|
184
|
+
".tiff",
|
|
185
|
+
".gif",
|
|
186
|
+
];
|
|
187
|
+
if (imageExts.includes(ext) && !input.frames) {
|
|
188
|
+
args.push("-frames:v", "1");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
args.push(resolvedOutput);
|
|
192
|
+
return args;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function runConvert(input: ConvertImageInput, repoPath: string): string {
|
|
196
|
+
if (!ffmpegAvailable()) {
|
|
197
|
+
return (
|
|
198
|
+
"Error: ffmpeg is not installed or not on PATH.\n" +
|
|
199
|
+
"Install it from https://ffmpeg.org/download.html or:\n" +
|
|
200
|
+
" Windows: winget install ffmpeg\n" +
|
|
201
|
+
" macOS: brew install ffmpeg\n" +
|
|
202
|
+
" Linux: sudo apt install ffmpeg"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const resolvedOutput = resolve(input.output, repoPath);
|
|
207
|
+
ensureDir(resolvedOutput);
|
|
208
|
+
|
|
209
|
+
const resolvedInput = Array.isArray(input.input)
|
|
210
|
+
? resolve(input.input[0]!, repoPath)
|
|
211
|
+
: resolve(input.input, repoPath);
|
|
212
|
+
|
|
213
|
+
if (!Array.isArray(input.input) && !input.input.startsWith("http")) {
|
|
214
|
+
if (!existsSync(resolvedInput)) {
|
|
215
|
+
return `Error: input file not found — ${resolvedInput}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const args = buildArgs(input, resolvedInput, resolvedOutput, repoPath);
|
|
220
|
+
const cmd = `ffmpeg ${args.map((a) => `"${a}"`).join(" ")}`;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const stderr = execSync(cmd, {
|
|
224
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
225
|
+
timeout: 120_000,
|
|
226
|
+
encoding: "utf-8",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const lines = (stderr || "")
|
|
230
|
+
.split("\n")
|
|
231
|
+
.filter((l) => l.includes("video:") || l.includes("frame="))
|
|
232
|
+
.slice(-3)
|
|
233
|
+
.join("\n");
|
|
234
|
+
|
|
235
|
+
const inputLabel = Array.isArray(input.input)
|
|
236
|
+
? `${input.input.length} files`
|
|
237
|
+
: path.basename(input.input as string);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
`✓ converted ${inputLabel} → ${input.output}\n` +
|
|
241
|
+
(lines ? `\n${lines}` : "")
|
|
242
|
+
).trim();
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
const errOut = (e.stderr ?? e.stdout ?? e.message ?? String(e))
|
|
245
|
+
.split("\n")
|
|
246
|
+
.filter((l: string) => l.trim() && !l.startsWith("ffmpeg version"))
|
|
247
|
+
.slice(-6)
|
|
248
|
+
.join("\n");
|
|
249
|
+
return `Error: ${errOut}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const convertImageTool: Tool<ConvertImageInput> = {
|
|
254
|
+
name: "convert-image",
|
|
255
|
+
description:
|
|
256
|
+
"convert, resize, crop, rotate, compress, or reformat images using ffmpeg",
|
|
257
|
+
safe: false,
|
|
258
|
+
permissionLabel: "convert image",
|
|
259
|
+
|
|
260
|
+
systemPromptEntry: (i) =>
|
|
261
|
+
[
|
|
262
|
+
`### ${i}. convert-image — image conversion and manipulation via ffmpeg`,
|
|
263
|
+
`<convert-image>`,
|
|
264
|
+
`{"input": "photo.png", "output": "photo.webp", "quality": 85, "resize": "1280x-1"}`,
|
|
265
|
+
`</convert-image>`,
|
|
266
|
+
`Fields (all optional except input/output):`,
|
|
267
|
+
` input — source path or array of paths (for gif assembly)`,
|
|
268
|
+
` output — destination path; extension sets format (png/jpg/webp/avif/gif/bmp/tiff)`,
|
|
269
|
+
` resize — "WxH" or "Wx-1" (keep aspect) e.g. "800x-1" or "400x300"`,
|
|
270
|
+
` crop — "WxH:X:Y" e.g. "400x300:100:50"`,
|
|
271
|
+
` rotate — 90 | 180 | 270`,
|
|
272
|
+
` flip — "h" | "v" | "both"`,
|
|
273
|
+
` quality — 1–100 (jpg/webp)`,
|
|
274
|
+
` grayscale — true`,
|
|
275
|
+
` blur — gaussian blur radius e.g. 5`,
|
|
276
|
+
` sharpen — unsharp strength e.g. 1.5`,
|
|
277
|
+
` strip — true to remove EXIF metadata`,
|
|
278
|
+
` watermark — path to overlay image (bottom-right, 25% size)`,
|
|
279
|
+
` frames — extract N frames from video/gif (output must contain %04d e.g. frame%04d.png)`,
|
|
280
|
+
` gifDelay — frame delay in centiseconds for gif assembly (default 10)`,
|
|
281
|
+
` gifLoop — gif loop count, 0 = infinite (default 0)`,
|
|
282
|
+
`Examples:`,
|
|
283
|
+
` Convert format: {"input":"a.png","output":"a.jpg","quality":90}`,
|
|
284
|
+
` Resize: {"input":"big.jpg","output":"thumb.jpg","resize":"400x-1"}`,
|
|
285
|
+
` Rotate + grayscale:{"input":"photo.jpg","output":"out.jpg","rotate":90,"grayscale":true}`,
|
|
286
|
+
` Strip EXIF: {"input":"photo.jpg","output":"clean.jpg","strip":true}`,
|
|
287
|
+
` Assemble gif: {"input":["f1.png","f2.png","f3.png"],"output":"anim.gif","gifDelay":15}`,
|
|
288
|
+
` Extract frames: {"input":"clip.gif","output":"frame%04d.png","frames":10}`,
|
|
289
|
+
` Watermark: {"input":"photo.jpg","output":"marked.jpg","watermark":"logo.png"}`,
|
|
290
|
+
].join("\n"),
|
|
291
|
+
|
|
292
|
+
parseInput,
|
|
293
|
+
|
|
294
|
+
summariseInput: ({ input, output }) => {
|
|
295
|
+
const src = Array.isArray(input)
|
|
296
|
+
? `${input.length} files`
|
|
297
|
+
: path.basename(input as string);
|
|
298
|
+
return `${src} → ${path.basename(output)}`;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
execute: async (input, ctx) => {
|
|
302
|
+
try {
|
|
303
|
+
const result = runConvert(input, ctx.repoPath);
|
|
304
|
+
return { kind: "text", value: result };
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
kind: "text",
|
|
308
|
+
value: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
};
|
package/src/tools/files.ts
CHANGED
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
} from "fs";
|
|
10
10
|
import type { FilePatch } from "../components/repo/DiffViewer";
|
|
11
11
|
|
|
12
|
-
// ── Walk ──────────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
12
|
const SKIP_DIRS = new Set([
|
|
15
13
|
"node_modules",
|
|
16
14
|
".git",
|
|
@@ -61,8 +59,6 @@ export function applyPatches(repoPath: string, patches: FilePatch[]): void {
|
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
// ── Read ──────────────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
62
|
export function readFile(filePath: string, repoPath: string): string {
|
|
67
63
|
const candidates = path.isAbsolute(filePath)
|
|
68
64
|
? [filePath]
|
|
@@ -120,9 +116,7 @@ export function readFolder(folderPath: string, repoPath: string): string {
|
|
|
120
116
|
try {
|
|
121
117
|
if (statSync(full).isDirectory()) subfolders.push(`${entry}/`);
|
|
122
118
|
else files.push(entry);
|
|
123
|
-
} catch {
|
|
124
|
-
// skip
|
|
125
|
-
}
|
|
119
|
+
} catch {}
|
|
126
120
|
}
|
|
127
121
|
|
|
128
122
|
const total = files.length + subfolders.length;
|
|
@@ -211,8 +205,6 @@ export function grepFiles(
|
|
|
211
205
|
return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)\n\n${results.join("\n\n")}`;
|
|
212
206
|
}
|
|
213
207
|
|
|
214
|
-
// ── Write / Delete ────────────────────────────────────────────────────────────
|
|
215
|
-
|
|
216
208
|
export function writeFile(
|
|
217
209
|
filePath: string,
|
|
218
210
|
content: string,
|