@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.
Files changed (41) hide show
  1. package/LENS.md +32 -68
  2. package/README.md +91 -0
  3. package/addons/README.md +3 -0
  4. package/addons/run-tests.js +127 -0
  5. package/dist/index.mjs +226459 -2638
  6. package/package.json +13 -4
  7. package/src/colors.ts +5 -0
  8. package/src/commands/commit.tsx +686 -0
  9. package/src/commands/provider.tsx +36 -22
  10. package/src/components/__tests__/Header.test.tsx +9 -0
  11. package/src/components/chat/ChatMessage.tsx +6 -6
  12. package/src/components/chat/ChatOverlays.tsx +20 -10
  13. package/src/components/chat/ChatRunner.tsx +197 -31
  14. package/src/components/provider/ApiKeyStep.tsx +77 -121
  15. package/src/components/provider/ModelStep.tsx +35 -20
  16. package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
  17. package/src/components/provider/ProviderTypeStep.tsx +12 -5
  18. package/src/components/provider/RemoveProviderStep.tsx +7 -8
  19. package/src/components/repo/RepoAnalysis.tsx +1 -1
  20. package/src/components/task/TaskRunner.tsx +1 -1
  21. package/src/components/timeline/CommitDetail.tsx +2 -4
  22. package/src/components/timeline/CommitList.tsx +2 -14
  23. package/src/components/timeline/TimelineChat.tsx +1 -2
  24. package/src/components/timeline/TimelineRunner.tsx +506 -423
  25. package/src/index.tsx +38 -0
  26. package/src/prompts/fewshot.ts +144 -47
  27. package/src/prompts/system.ts +25 -21
  28. package/src/tools/chart.ts +210 -0
  29. package/src/tools/convert-image.ts +312 -0
  30. package/src/tools/files.ts +1 -9
  31. package/src/tools/git.ts +577 -0
  32. package/src/tools/index.ts +17 -13
  33. package/src/tools/pdf.ts +136 -78
  34. package/src/tools/view-image.ts +335 -0
  35. package/src/tools/web.ts +0 -4
  36. package/src/utils/addons/loadAddons.ts +6 -3
  37. package/src/utils/chat.ts +38 -23
  38. package/src/utils/thinking.tsx +275 -162
  39. package/src/utils/tools/builtins.ts +39 -32
  40. package/src/utils/tools/registry.ts +0 -14
  41. 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
+ };
@@ -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,