@ridit/lens 0.2.1 → 0.2.4

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.
@@ -0,0 +1,324 @@
1
+ import type { Tool, ToolContext } from "@ridit/lens-sdk";
2
+ import {
3
+ fetchUrl,
4
+ searchWeb,
5
+ runShell,
6
+ openUrl,
7
+ readFile,
8
+ readFolder,
9
+ grepFiles,
10
+ writeFile,
11
+ deleteFile,
12
+ deleteFolder,
13
+ generatePdf,
14
+ } from "../../tools";
15
+
16
+ // ── fetch ─────────────────────────────────────────────────────────────────────
17
+
18
+ export const fetchTool: Tool<string> = {
19
+ name: "fetch",
20
+ description: "load a URL",
21
+ safe: true,
22
+ permissionLabel: "fetch",
23
+ systemPromptEntry: (i) =>
24
+ `### ${i}. fetch — load a URL\n<fetch>https://example.com</fetch>`,
25
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
26
+ summariseInput: (url) => url,
27
+ execute: async (url) => {
28
+ try {
29
+ const value = await fetchUrl(url);
30
+ return { kind: "text", value };
31
+ } catch (err) {
32
+ return {
33
+ kind: "error",
34
+ value: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
35
+ };
36
+ }
37
+ },
38
+ };
39
+
40
+ // ── shell ─────────────────────────────────────────────────────────────────────
41
+
42
+ export const shellTool: Tool<string> = {
43
+ name: "shell",
44
+ description: "run a terminal command",
45
+ safe: false,
46
+ permissionLabel: "run",
47
+ systemPromptEntry: (i) =>
48
+ `### ${i}. shell — run a terminal command\n<shell>node -v</shell>`,
49
+ parseInput: (body) => body || null,
50
+ summariseInput: (cmd) => cmd,
51
+ execute: async (cmd, ctx) => {
52
+ const value = await runShell(cmd, ctx.repoPath);
53
+ return { kind: "text", value };
54
+ },
55
+ };
56
+
57
+ // ── read-file ─────────────────────────────────────────────────────────────────
58
+
59
+ export const readFileTool: Tool<string> = {
60
+ name: "read-file",
61
+ description: "read a file from the repo",
62
+ safe: true,
63
+ permissionLabel: "read",
64
+ systemPromptEntry: (i) =>
65
+ `### ${i}. read-file — read a file from the repo\n<read-file>src/foo.ts</read-file>`,
66
+ parseInput: (body) => body || null,
67
+ summariseInput: (p) => p,
68
+ execute: (filePath, ctx) => ({
69
+ kind: "text",
70
+ value: readFile(filePath, ctx.repoPath),
71
+ }),
72
+ };
73
+
74
+ // ── read-folder ───────────────────────────────────────────────────────────────
75
+
76
+ export const readFolderTool: Tool<string> = {
77
+ name: "read-folder",
78
+ description: "list contents of a folder (files + subfolders, one level deep)",
79
+ safe: true,
80
+ permissionLabel: "folder",
81
+ systemPromptEntry: (i) =>
82
+ `### ${i}. read-folder — list contents of a folder (files + subfolders, one level deep)\n<read-folder>src/components</read-folder>`,
83
+ parseInput: (body) => body || null,
84
+ summariseInput: (p) => p,
85
+ execute: (folderPath, ctx) => ({
86
+ kind: "text",
87
+ value: readFolder(folderPath, ctx.repoPath),
88
+ }),
89
+ };
90
+
91
+ // ── grep ──────────────────────────────────────────────────────────────────────
92
+
93
+ interface GrepInput {
94
+ pattern: string;
95
+ glob: string;
96
+ }
97
+
98
+ export const grepTool: Tool<GrepInput> = {
99
+ name: "grep",
100
+ description: "search for a pattern across files in the repo",
101
+ safe: true,
102
+ permissionLabel: "grep",
103
+ systemPromptEntry: (i) =>
104
+ `### ${i}. grep — search for a pattern across files in the repo (cross-platform, no shell needed)\n<grep>\n{"pattern": "ChatRunner", "glob": "src/**/*.tsx"}\n</grep>`,
105
+ parseInput: (body) => {
106
+ try {
107
+ const parsed = JSON.parse(body) as { pattern: string; glob?: string };
108
+ return { pattern: parsed.pattern, glob: parsed.glob ?? "**/*" };
109
+ } catch {
110
+ return { pattern: body, glob: "**/*" };
111
+ }
112
+ },
113
+ summariseInput: ({ pattern, glob }) => `${pattern} — ${glob}`,
114
+ execute: ({ pattern, glob }, ctx) => ({
115
+ kind: "text",
116
+ value: grepFiles(pattern, glob, ctx.repoPath),
117
+ }),
118
+ };
119
+
120
+ // ── write-file ────────────────────────────────────────────────────────────────
121
+
122
+ interface WriteFileInput {
123
+ path: string;
124
+ content: string;
125
+ }
126
+
127
+ export const writeFileTool: Tool<WriteFileInput> = {
128
+ name: "write-file",
129
+ description: "create or overwrite a file",
130
+ safe: false,
131
+ permissionLabel: "write",
132
+ systemPromptEntry: (i) =>
133
+ `### ${i}. write-file — create or overwrite a file\n<write-file>\n{"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}\n</write-file>`,
134
+ parseInput: (body) => {
135
+ try {
136
+ const parsed = JSON.parse(body) as { path: string; content: string };
137
+ if (!parsed.path) return null;
138
+ return parsed;
139
+ } catch {
140
+ return null;
141
+ }
142
+ },
143
+ summariseInput: ({ path, content }) => `${path} (${content.length} bytes)`,
144
+ execute: ({ path: filePath, content }, ctx) => ({
145
+ kind: "text",
146
+ value: writeFile(filePath, content, ctx.repoPath),
147
+ }),
148
+ };
149
+
150
+ // ── delete-file ───────────────────────────────────────────────────────────────
151
+
152
+ export const deleteFileTool: Tool<string> = {
153
+ name: "delete-file",
154
+ description: "permanently delete a single file",
155
+ safe: false,
156
+ permissionLabel: "delete",
157
+ systemPromptEntry: (i) =>
158
+ `### ${i}. delete-file — permanently delete a single file\n<delete-file>src/old-component.tsx</delete-file>`,
159
+ parseInput: (body) => body || null,
160
+ summariseInput: (p) => p,
161
+ execute: (filePath, ctx) => ({
162
+ kind: "text",
163
+ value: deleteFile(filePath, ctx.repoPath),
164
+ }),
165
+ };
166
+
167
+ // ── delete-folder ─────────────────────────────────────────────────────────────
168
+
169
+ export const deleteFolderTool: Tool<string> = {
170
+ name: "delete-folder",
171
+ description: "permanently delete a folder and all its contents",
172
+ safe: false,
173
+ permissionLabel: "delete folder",
174
+ systemPromptEntry: (i) =>
175
+ `### ${i}. delete-folder — permanently delete a folder and all its contents\n<delete-folder>src/legacy</delete-folder>`,
176
+ parseInput: (body) => body || null,
177
+ summariseInput: (p) => p,
178
+ execute: (folderPath, ctx) => ({
179
+ kind: "text",
180
+ value: deleteFolder(folderPath, ctx.repoPath),
181
+ }),
182
+ };
183
+
184
+ // ── open-url ──────────────────────────────────────────────────────────────────
185
+
186
+ export const openUrlTool: Tool<string> = {
187
+ name: "open-url",
188
+ description: "open a URL in the user's default browser",
189
+ safe: true,
190
+ permissionLabel: "open",
191
+ systemPromptEntry: (i) =>
192
+ `### ${i}. open-url — open a URL in the user's default browser\n<open-url>https://github.com/owner/repo</open-url>`,
193
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
194
+ summariseInput: (url) => url,
195
+ execute: (url) => ({ kind: "text", value: openUrl(url) }),
196
+ };
197
+
198
+ // ── generate-pdf ──────────────────────────────────────────────────────────────
199
+
200
+ interface GeneratePdfInput {
201
+ filePath: string;
202
+ content: string;
203
+ }
204
+
205
+ export const generatePdfTool: Tool<GeneratePdfInput> = {
206
+ name: "generate-pdf",
207
+ description: "generate a PDF file from markdown-style content",
208
+ safe: false,
209
+ permissionLabel: "pdf",
210
+ systemPromptEntry: (i) =>
211
+ `### ${i}. generate-pdf — generate a PDF file from markdown-style content\n<generate-pdf>\n{"path": "output/report.pdf", "content": "# Title\\n\\nSome body text."}\n</generate-pdf>`,
212
+ parseInput: (body) => {
213
+ try {
214
+ const parsed = JSON.parse(body) as {
215
+ path?: string;
216
+ filePath?: string;
217
+ content?: string;
218
+ };
219
+ return {
220
+ filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
221
+ content: parsed.content ?? "",
222
+ };
223
+ } catch {
224
+ return null;
225
+ }
226
+ },
227
+ summariseInput: ({ filePath }) => filePath,
228
+ execute: ({ filePath, content }, ctx) => ({
229
+ kind: "text",
230
+ value: generatePdf(filePath, content, ctx.repoPath),
231
+ }),
232
+ };
233
+
234
+ // ── search ────────────────────────────────────────────────────────────────────
235
+
236
+ export const searchTool: Tool<string> = {
237
+ name: "search",
238
+ description: "search the internet for anything you are unsure about",
239
+ safe: true,
240
+ permissionLabel: "search",
241
+ systemPromptEntry: (i) =>
242
+ `### ${i}. search — search the internet for anything you are unsure about\n<search>how to use React useEffect cleanup function</search>`,
243
+ parseInput: (body) => body || null,
244
+ summariseInput: (q) => `"${q}"`,
245
+ execute: async (query) => {
246
+ try {
247
+ const value = await searchWeb(query);
248
+ return { kind: "text", value };
249
+ } catch (err) {
250
+ return {
251
+ kind: "error",
252
+ value: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
253
+ };
254
+ }
255
+ },
256
+ };
257
+
258
+ // ── clone ─────────────────────────────────────────────────────────────────────
259
+
260
+ export const cloneTool: Tool<string> = {
261
+ name: "clone",
262
+ description: "clone a GitHub repo so you can explore and discuss it",
263
+ safe: false,
264
+ permissionLabel: "clone",
265
+ systemPromptEntry: (i) =>
266
+ `### ${i}. clone — clone a GitHub repo so you can explore and discuss it\n<clone>https://github.com/owner/repo</clone>`,
267
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
268
+ summariseInput: (url) => url,
269
+ // Clone is handled specially by ChatRunner (it triggers a UI flow),
270
+ // so execute here is just a fallback that should never run.
271
+ execute: (repoUrl) => ({
272
+ kind: "text",
273
+ value: `Clone of ${repoUrl} was handled by the UI.`,
274
+ }),
275
+ };
276
+
277
+ // ── changes ───────────────────────────────────────────────────────────────────
278
+
279
+ export interface ChangesInput {
280
+ summary: string;
281
+ patches: { path: string; content: string; isNew: boolean }[];
282
+ }
283
+
284
+ export const changesTool: Tool<ChangesInput> = {
285
+ name: "changes",
286
+ description: "propose code edits (shown as a diff for user approval)",
287
+ safe: false,
288
+ permissionLabel: "changes",
289
+ systemPromptEntry: (i) =>
290
+ `### ${i}. changes — propose code edits (shown as a diff for user approval)\n<changes>\n{"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}\n</changes>`,
291
+ parseInput: (body) => {
292
+ try {
293
+ return JSON.parse(body) as ChangesInput;
294
+ } catch {
295
+ return null;
296
+ }
297
+ },
298
+ summariseInput: ({ summary }) => summary,
299
+ // changes is handled specially by ChatRunner (diff preview UI).
300
+ execute: ({ summary }) => ({
301
+ kind: "text",
302
+ value: `Changes proposed: ${summary}`,
303
+ }),
304
+ };
305
+
306
+ // ── registerBuiltins ──────────────────────────────────────────────────────────
307
+
308
+ import { registry } from "./registry";
309
+
310
+ export function registerBuiltins(): void {
311
+ registry.register(fetchTool);
312
+ registry.register(shellTool);
313
+ registry.register(readFileTool);
314
+ registry.register(readFolderTool);
315
+ registry.register(grepTool);
316
+ registry.register(writeFileTool);
317
+ registry.register(deleteFileTool);
318
+ registry.register(deleteFolderTool);
319
+ registry.register(openUrlTool);
320
+ registry.register(generatePdfTool);
321
+ registry.register(searchTool);
322
+ registry.register(cloneTool);
323
+ registry.register(changesTool);
324
+ }
@@ -0,0 +1,63 @@
1
+ // ── Tool Plugin System ────────────────────────────────────────────────────────
2
+ //
3
+ // To create a new tool:
4
+ //
5
+ // 1. Implement the Tool interface
6
+ // 2. Call registry.register(myTool) before the app starts
7
+ //
8
+ // External addon example:
9
+ //
10
+ // import { registry } from "lens/tools/registry";
11
+ // registry.register({ name: "my-tool", ... });
12
+
13
+ import type { Tool } from "@ridit/lens-sdk";
14
+
15
+ // ── Registry ──────────────────────────────────────────────────────────────────
16
+
17
+ class ToolRegistry {
18
+ private tools = new Map<string, Tool<unknown>>();
19
+
20
+ register<T>(tool: Tool<T>): void {
21
+ if (this.tools.has(tool.name)) {
22
+ console.warn(`[ToolRegistry] Overwriting existing tool: "${tool.name}"`);
23
+ }
24
+ this.tools.set(tool.name, tool as Tool<unknown>);
25
+ }
26
+
27
+ unregister(name: string): void {
28
+ this.tools.delete(name);
29
+ }
30
+
31
+ get(name: string): Tool<unknown> | undefined {
32
+ return this.tools.get(name);
33
+ }
34
+
35
+ all(): Tool<unknown>[] {
36
+ return Array.from(this.tools.values());
37
+ }
38
+
39
+ names(): string[] {
40
+ return Array.from(this.tools.keys());
41
+ }
42
+
43
+ /**
44
+ * Build the TOOLS section of the system prompt from all registered tools.
45
+ */
46
+ buildSystemPromptSection(): string {
47
+ const lines: string[] = ["## TOOLS\n"];
48
+ lines.push(
49
+ "You have exactly " +
50
+ this.tools.size +
51
+ " tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.\n",
52
+ );
53
+ let i = 1;
54
+ for (const tool of this.tools.values()) {
55
+ lines.push(tool.systemPromptEntry(i++));
56
+ }
57
+ return lines.join("\n");
58
+ }
59
+ }
60
+
61
+ export const registry = new ToolRegistry();
62
+
63
+ (globalThis as any).__lens_registry = registry;
package/hello.py DELETED
@@ -1,51 +0,0 @@
1
- # hello.py
2
- # A simple script that reads a text file, makes an HTTP request,
3
- # and prints the result in a friendly way.
4
-
5
- import json
6
- import pathlib
7
- import sys
8
- from urllib.request import urlopen
9
-
10
- def read_local_file(path: str) -> str:
11
- """Read the contents of a file and return it as a string."""
12
- file_path = pathlib.Path(path)
13
- if not file_path.is_file():
14
- raise FileNotFoundError(f"❌ File not found: {path}")
15
- return file_path.read_text(encoding="utf-8")
16
-
17
- def fetch_json(url: str) -> dict:
18
- """Fetch JSON from a URL and decode it into a Python dict."""
19
- with urlopen(url) as response:
20
- if response.status != 200:
21
- raise RuntimeError(f"❌ HTTP {response.status} from {url}")
22
- data = response.read()
23
- return json.loads(data)
24
-
25
- def main():
26
- # 1️⃣ Read a local file (optional – you can comment this out)
27
- try:
28
- local_content = read_local_file("example.txt")
29
- print("📄 Contents of example.txt:")
30
- print(local_content)
31
- except FileNotFoundError:
32
- print("⚠️ example.txt not found – skipping that step.")
33
-
34
- # 2️⃣ Fetch some JSON data from a public API
35
- url = "https://api.github.com/repos/python/cpython"
36
- print(f"\n🌐 Fetching data from {url} …")
37
- repo_info = fetch_json(url)
38
-
39
- # 3️⃣ Extract a couple of useful fields and display them
40
- name = repo_info.get("name")
41
- stars = repo_info.get("stargazers_count")
42
- description = repo_info.get("description")
43
- print("\n🔎 Repository info:")
44
- print(f"• Name: {name}")
45
- print(f"• Stars: {stars:,}") # adds commas for readability
46
- print(f"• Description: {description}")
47
-
48
- return 0
49
-
50
- if __name__ == "__main__":
51
- sys.exit(main())