@ridit/lens 0.1.6 → 0.1.8

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.
@@ -48,6 +48,30 @@ export function PermissionPrompt({
48
48
  icon = "r";
49
49
  label = "read";
50
50
  value = tool.filePath;
51
+ } else if (tool.type === "read-folder") {
52
+ icon = "d";
53
+ label = "folder";
54
+ value = tool.folderPath;
55
+ } else if (tool.type === "grep") {
56
+ icon = "/";
57
+ label = "grep";
58
+ value = `${tool.pattern} ${tool.glob}`;
59
+ } else if (tool.type === "delete-file") {
60
+ icon = "x";
61
+ label = "delete";
62
+ value = tool.filePath;
63
+ } else if (tool.type === "delete-folder") {
64
+ icon = "X";
65
+ label = "delete folder";
66
+ value = tool.folderPath;
67
+ } else if (tool.type === "open-url") {
68
+ icon = "↗";
69
+ label = "open";
70
+ value = tool.url;
71
+ } else if (tool.type === "generate-pdf") {
72
+ icon = "P";
73
+ label = "pdf";
74
+ value = tool.filePath;
51
75
  } else if (tool.type === "write-file") {
52
76
  icon = "w";
53
77
  label = "write";
@@ -128,12 +152,15 @@ export function TypewriterText({
128
152
  return <Text color={color}>{displayed}</Text>;
129
153
  }
130
154
 
131
- export function ShortcutBar() {
155
+ export function ShortcutBar({ autoApprove }: { autoApprove?: boolean }) {
132
156
  return (
133
157
  <Box gap={3} marginTop={0}>
134
158
  <Text color="gray" dimColor>
135
159
  enter send · ^v paste · ^c exit
136
160
  </Text>
161
+ <Text color={autoApprove ? "green" : "gray"} dimColor={!autoApprove}>
162
+ {autoApprove ? "⚡ auto" : "/auto"}
163
+ </Text>
137
164
  </Box>
138
165
  );
139
166
  }
@@ -20,6 +20,12 @@ import {
20
20
  runShell,
21
21
  fetchUrl,
22
22
  readFile,
23
+ readFolder,
24
+ grepFiles,
25
+ deleteFile,
26
+ deleteFolder,
27
+ openUrl,
28
+ generatePdf,
23
29
  writeFile,
24
30
  buildSystemPrompt,
25
31
  parseResponse,
@@ -55,6 +61,7 @@ const COMMANDS = [
55
61
  { cmd: "/timeline", desc: "browse commit history" },
56
62
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
57
63
  { cmd: "/review", desc: "review current codebsae" },
64
+ { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
58
65
  ];
59
66
 
60
67
  function CommandPalette({
@@ -98,6 +105,15 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
98
105
  const [clonedUrls, setClonedUrls] = useState<Set<string>>(new Set());
99
106
  const [showTimeline, setShowTimeline] = useState(false);
100
107
  const [showReview, setShowReview] = useState(false);
108
+ const [autoApprove, setAutoApprove] = useState(false);
109
+
110
+ // Abort controller for the currently in-flight API call.
111
+ // Pressing ESC while thinking aborts the request and drops the response.
112
+ const abortControllerRef = useRef<AbortController | null>(null);
113
+
114
+ // Cache of tool results within a single conversation turn to prevent
115
+ // the model from re-calling tools it already ran with the same args
116
+ const toolResultCache = useRef<Map<string, string>>(new Map());
101
117
 
102
118
  const inputBuffer = useRef("");
103
119
  const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -119,6 +135,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
119
135
  };
120
136
 
121
137
  const handleError = (currentAll: Message[]) => (err: unknown) => {
138
+ // Silently drop aborted requests — user pressed ESC intentionally
139
+ if (err instanceof Error && err.name === "AbortError") {
140
+ setStage({ type: "idle" });
141
+ return;
142
+ }
122
143
  const errMsg: Message = {
123
144
  role: "assistant",
124
145
  content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
@@ -129,7 +150,17 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
129
150
  setStage({ type: "idle" });
130
151
  };
131
152
 
132
- const processResponse = (raw: string, currentAll: Message[]) => {
153
+ const processResponse = (
154
+ raw: string,
155
+ currentAll: Message[],
156
+ signal: AbortSignal,
157
+ ) => {
158
+ // If ESC was pressed before we got here, silently drop the response
159
+ if (signal.aborted) {
160
+ setStage({ type: "idle" });
161
+ return;
162
+ }
163
+
133
164
  const parsed = parseResponse(raw);
134
165
 
135
166
  if (parsed.kind === "changes") {
@@ -169,7 +200,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
169
200
  parsed.kind === "shell" ||
170
201
  parsed.kind === "fetch" ||
171
202
  parsed.kind === "read-file" ||
203
+ parsed.kind === "read-folder" ||
204
+ parsed.kind === "grep" ||
172
205
  parsed.kind === "write-file" ||
206
+ parsed.kind === "delete-file" ||
207
+ parsed.kind === "delete-folder" ||
208
+ parsed.kind === "open-url" ||
209
+ parsed.kind === "generate-pdf" ||
173
210
  parsed.kind === "search"
174
211
  ) {
175
212
  let tool: Parameters<typeof PermissionPrompt>[0]["tool"];
@@ -179,6 +216,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
179
216
  tool = { type: "fetch", url: parsed.url };
180
217
  } else if (parsed.kind === "read-file") {
181
218
  tool = { type: "read-file", filePath: parsed.filePath };
219
+ } else if (parsed.kind === "read-folder") {
220
+ tool = { type: "read-folder", folderPath: parsed.folderPath };
221
+ } else if (parsed.kind === "grep") {
222
+ tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
223
+ } else if (parsed.kind === "delete-file") {
224
+ tool = { type: "delete-file", filePath: parsed.filePath };
225
+ } else if (parsed.kind === "delete-folder") {
226
+ tool = { type: "delete-folder", folderPath: parsed.folderPath };
227
+ } else if (parsed.kind === "open-url") {
228
+ tool = { type: "open-url", url: parsed.url };
229
+ } else if (parsed.kind === "generate-pdf") {
230
+ tool = {
231
+ type: "generate-pdf",
232
+ filePath: parsed.filePath,
233
+ content: parsed.pdfContent,
234
+ };
182
235
  } else if (parsed.kind === "search") {
183
236
  tool = { type: "search", query: parsed.query };
184
237
  } else {
@@ -199,13 +252,32 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
199
252
  setCommitted((prev) => [...prev, preambleMsg]);
200
253
  }
201
254
 
202
- setStage({
203
- type: "permission",
204
- tool,
205
- pendingMessages: currentAll,
206
- resolve: async (approved: boolean) => {
207
- let result = "(denied by user)";
208
- if (approved) {
255
+ // Safe tools that can be auto-approved (no side effects)
256
+ const isSafeTool =
257
+ parsed.kind === "read-file" ||
258
+ parsed.kind === "read-folder" ||
259
+ parsed.kind === "grep" ||
260
+ parsed.kind === "fetch" ||
261
+ parsed.kind === "open-url" ||
262
+ parsed.kind === "search";
263
+
264
+ const executeAndContinue = async (approved: boolean) => {
265
+ let result = "(denied by user)";
266
+ if (approved) {
267
+ const cacheKey =
268
+ parsed.kind === "read-file"
269
+ ? `read-file:${parsed.filePath}`
270
+ : parsed.kind === "read-folder"
271
+ ? `read-folder:${parsed.folderPath}`
272
+ : parsed.kind === "grep"
273
+ ? `grep:${parsed.pattern}:${parsed.glob}`
274
+ : null;
275
+
276
+ if (cacheKey && toolResultCache.current.has(cacheKey)) {
277
+ result =
278
+ toolResultCache.current.get(cacheKey)! +
279
+ "\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
280
+ } else {
209
281
  try {
210
282
  setStage({ type: "thinking" });
211
283
  if (parsed.kind === "shell") {
@@ -214,6 +286,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
214
286
  result = await fetchUrl(parsed.url);
215
287
  } else if (parsed.kind === "read-file") {
216
288
  result = readFile(parsed.filePath, repoPath);
289
+ } else if (parsed.kind === "read-folder") {
290
+ result = readFolder(parsed.folderPath, repoPath);
291
+ } else if (parsed.kind === "grep") {
292
+ result = grepFiles(parsed.pattern, parsed.glob, repoPath);
293
+ } else if (parsed.kind === "delete-file") {
294
+ result = deleteFile(parsed.filePath, repoPath);
295
+ } else if (parsed.kind === "delete-folder") {
296
+ result = deleteFolder(parsed.folderPath, repoPath);
297
+ } else if (parsed.kind === "open-url") {
298
+ result = openUrl(parsed.url);
299
+ } else if (parsed.kind === "generate-pdf") {
300
+ result = generatePdf(
301
+ parsed.filePath,
302
+ parsed.pdfContent,
303
+ repoPath,
304
+ );
217
305
  } else if (parsed.kind === "write-file") {
218
306
  result = writeFile(
219
307
  parsed.filePath,
@@ -223,72 +311,133 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
223
311
  } else if (parsed.kind === "search") {
224
312
  result = await searchWeb(parsed.query);
225
313
  }
314
+ if (cacheKey) {
315
+ toolResultCache.current.set(cacheKey, result);
316
+ }
226
317
  } catch (err: unknown) {
227
318
  result = `Error: ${err instanceof Error ? err.message : "failed"}`;
228
319
  }
229
320
  }
321
+ }
230
322
 
231
- if (approved && !result.startsWith("Error:")) {
232
- const kindMap = {
233
- shell: "shell-run",
234
- fetch: "url-fetched",
235
- "read-file": "file-read",
236
- "write-file": "file-written",
237
- search: "url-fetched",
238
- } as const;
239
- appendHistory({
240
- kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
241
- detail:
242
- parsed.kind === "shell"
243
- ? parsed.command
244
- : parsed.kind === "fetch"
245
- ? parsed.url
246
- : parsed.kind === "search"
247
- ? parsed.query
248
- : parsed.filePath,
249
- summary: result.split("\n")[0]?.slice(0, 120) ?? "",
250
- repoPath,
251
- });
252
- }
253
-
254
- const toolName =
255
- parsed.kind === "shell"
256
- ? "shell"
257
- : parsed.kind === "fetch"
258
- ? "fetch"
259
- : parsed.kind === "read-file"
260
- ? "read-file"
323
+ if (approved && !result.startsWith("Error:")) {
324
+ const kindMap = {
325
+ shell: "shell-run",
326
+ fetch: "url-fetched",
327
+ "read-file": "file-read",
328
+ "read-folder": "file-read",
329
+ grep: "file-read",
330
+ "delete-file": "file-written",
331
+ "delete-folder": "file-written",
332
+ "open-url": "url-fetched",
333
+ "generate-pdf": "file-written",
334
+ "write-file": "file-written",
335
+ search: "url-fetched",
336
+ } as const;
337
+ appendHistory({
338
+ kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
339
+ detail:
340
+ parsed.kind === "shell"
341
+ ? parsed.command
342
+ : parsed.kind === "fetch"
343
+ ? parsed.url
261
344
  : parsed.kind === "search"
262
- ? "search"
263
- : "write-file";
264
-
265
- const toolContent =
266
- parsed.kind === "shell"
267
- ? parsed.command
268
- : parsed.kind === "fetch"
269
- ? parsed.url
270
- : parsed.kind === "search"
271
- ? parsed.query
272
- : parsed.filePath;
273
-
274
- const toolMsg: Message = {
275
- role: "assistant",
276
- type: "tool",
277
- toolName,
278
- content: toolContent,
279
- result,
280
- approved,
281
- };
345
+ ? parsed.query
346
+ : parsed.kind === "read-folder"
347
+ ? parsed.folderPath
348
+ : parsed.kind === "grep"
349
+ ? `${parsed.pattern} ${parsed.glob}`
350
+ : parsed.kind === "delete-file"
351
+ ? parsed.filePath
352
+ : parsed.kind === "delete-folder"
353
+ ? parsed.folderPath
354
+ : parsed.kind === "open-url"
355
+ ? parsed.url
356
+ : parsed.kind === "generate-pdf"
357
+ ? parsed.filePath
358
+ : parsed.filePath,
359
+ summary: result.split("\n")[0]?.slice(0, 120) ?? "",
360
+ repoPath,
361
+ });
362
+ }
363
+
364
+ const toolName =
365
+ parsed.kind === "shell"
366
+ ? "shell"
367
+ : parsed.kind === "fetch"
368
+ ? "fetch"
369
+ : parsed.kind === "read-file"
370
+ ? "read-file"
371
+ : parsed.kind === "read-folder"
372
+ ? "read-folder"
373
+ : parsed.kind === "grep"
374
+ ? "grep"
375
+ : parsed.kind === "delete-file"
376
+ ? "delete-file"
377
+ : parsed.kind === "delete-folder"
378
+ ? "delete-folder"
379
+ : parsed.kind === "open-url"
380
+ ? "open-url"
381
+ : parsed.kind === "generate-pdf"
382
+ ? "generate-pdf"
383
+ : parsed.kind === "search"
384
+ ? "search"
385
+ : "write-file";
386
+
387
+ const toolContent =
388
+ parsed.kind === "shell"
389
+ ? parsed.command
390
+ : parsed.kind === "fetch"
391
+ ? parsed.url
392
+ : parsed.kind === "search"
393
+ ? parsed.query
394
+ : parsed.kind === "read-folder"
395
+ ? parsed.folderPath
396
+ : parsed.kind === "grep"
397
+ ? `${parsed.pattern} — ${parsed.glob}`
398
+ : parsed.kind === "delete-file"
399
+ ? parsed.filePath
400
+ : parsed.kind === "delete-folder"
401
+ ? parsed.folderPath
402
+ : parsed.kind === "open-url"
403
+ ? parsed.url
404
+ : parsed.kind === "generate-pdf"
405
+ ? parsed.filePath
406
+ : parsed.filePath;
407
+
408
+ const toolMsg: Message = {
409
+ role: "assistant",
410
+ type: "tool",
411
+ toolName,
412
+ content: toolContent,
413
+ result,
414
+ approved,
415
+ };
416
+
417
+ const withTool = [...currentAll, toolMsg];
418
+ setAllMessages(withTool);
419
+ setCommitted((prev) => [...prev, toolMsg]);
282
420
 
283
- const withTool = [...currentAll, toolMsg];
284
- setAllMessages(withTool);
285
- setCommitted((prev) => [...prev, toolMsg]);
421
+ // Create a fresh abort controller for the follow-up call
422
+ const nextAbort = new AbortController();
423
+ abortControllerRef.current = nextAbort;
286
424
 
287
- setStage({ type: "thinking" });
288
- callChat(provider!, systemPrompt, withTool)
289
- .then((r: string) => processResponse(r, withTool))
290
- .catch(handleError(withTool));
291
- },
425
+ setStage({ type: "thinking" });
426
+ callChat(provider!, systemPrompt, withTool, nextAbort.signal)
427
+ .then((r: string) => processResponse(r, withTool, nextAbort.signal))
428
+ .catch(handleError(withTool));
429
+ };
430
+
431
+ if (autoApprove && isSafeTool) {
432
+ executeAndContinue(true);
433
+ return;
434
+ }
435
+
436
+ setStage({
437
+ type: "permission",
438
+ tool,
439
+ pendingMessages: currentAll,
440
+ resolve: executeAndContinue,
292
441
  });
293
442
  return;
294
443
  }
@@ -349,6 +498,21 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
349
498
  return;
350
499
  }
351
500
 
501
+ if (text.trim().toLowerCase() === "/auto") {
502
+ const next = !autoApprove;
503
+ setAutoApprove(next);
504
+ const msg: Message = {
505
+ role: "assistant",
506
+ content: next
507
+ ? "Auto-approve ON — read, search, grep and folder tools will run without asking. Write and code changes still require approval."
508
+ : "Auto-approve OFF — all tools will ask for permission.",
509
+ type: "text",
510
+ };
511
+ setCommitted((prev) => [...prev, msg]);
512
+ setAllMessages((prev) => [...prev, msg]);
513
+ return;
514
+ }
515
+
352
516
  if (text.trim().toLowerCase() === "/clear history") {
353
517
  clearRepoHistory(repoPath);
354
518
  const clearedMsg: Message = {
@@ -365,15 +529,29 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
365
529
  const nextAll = [...allMessages, userMsg];
366
530
  setCommitted((prev) => [...prev, userMsg]);
367
531
  setAllMessages(nextAll);
532
+ toolResultCache.current.clear();
533
+
534
+ // Create a fresh abort controller for this request
535
+ const abort = new AbortController();
536
+ abortControllerRef.current = abort;
537
+
368
538
  setStage({ type: "thinking" });
369
- callChat(provider, systemPrompt, nextAll)
370
- .then((raw: string) => processResponse(raw, nextAll))
539
+ callChat(provider, systemPrompt, nextAll, abort.signal)
540
+ .then((raw: string) => processResponse(raw, nextAll, abort.signal))
371
541
  .catch(handleError(nextAll));
372
542
  };
373
543
 
374
544
  useInput((input, key) => {
375
545
  if (showTimeline) return;
376
546
 
547
+ // ESC while thinking → abort the in-flight request and go idle
548
+ if (stage.type === "thinking" && key.escape) {
549
+ abortControllerRef.current?.abort();
550
+ abortControllerRef.current = null;
551
+ setStage({ type: "idle" });
552
+ return;
553
+ }
554
+
377
555
  if (stage.type === "idle") {
378
556
  if (key.ctrl && input === "c") {
379
557
  process.exit(0);
@@ -699,6 +877,9 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
699
877
  <Box gap={1}>
700
878
  <Text color={ACCENT}>●</Text>
701
879
  <TypewriterText text={thinkingPhrase} />
880
+ <Text color="gray" dimColor>
881
+ · esc cancel
882
+ </Text>
702
883
  </Box>
703
884
  )}
704
885
 
@@ -724,7 +905,7 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
724
905
  setInputValue("");
725
906
  }}
726
907
  />
727
- <ShortcutBar />
908
+ <ShortcutBar autoApprove={autoApprove} />
728
909
  </Box>
729
910
  )}
730
911
  </Box>
package/src/index.tsx CHANGED
@@ -1,60 +1,60 @@
1
- import React from "react";
2
- import { render } from "ink";
3
- import { Command } from "commander";
4
- import { RepoCommand } from "./commands/repo";
5
- import { InitCommand } from "./commands/provider";
6
- import { ReviewCommand } from "./commands/review";
7
- import { TaskCommand } from "./commands/task";
8
- import { ChatCommand } from "./commands/chat";
9
- import { TimelineCommand } from "./commands/timeline";
10
-
11
- const program = new Command();
12
-
13
- program
14
- .command("repo <url>")
15
- .description("Analyze a remote repository")
16
- .action((url) => {
17
- render(<RepoCommand url={url} />);
18
- });
19
-
20
- program
21
- .command("provider")
22
- .description("Configure AI providers")
23
- .action(() => {
24
- render(<InitCommand />);
25
- });
26
-
27
- program
28
- .command("review [path]")
29
- .description("Review a local codebase")
30
- .action((inputPath) => {
31
- render(<ReviewCommand path={inputPath ?? "."} />);
32
- });
33
-
34
- program
35
- .command("task <text>")
36
- .description("Apply a natural language change to the codebase")
37
- .option("-p, --path <path>", "Path to the repo", ".")
38
- .action((text: string, opts: { path: string }) => {
39
- render(<TaskCommand prompt={text} path={opts.path} />);
40
- });
41
-
42
- program
43
- .command("chat")
44
- .description("Chat with your codebase — ask questions or make changes")
45
- .option("-p, --path <path>", "Path to the repo", ".")
46
- .action((opts: { path: string }) => {
47
- render(<ChatCommand path={opts.path} />);
48
- });
49
-
50
- program
51
- .command("timeline")
52
- .description(
53
- "Explore your code history — see commits, changes, and evolution",
54
- )
55
- .option("-p, --path <path>", "Path to the repo", ".")
56
- .action((opts: { path: string }) => {
57
- render(<TimelineCommand path={opts.path} />);
58
- });
59
-
60
- program.parse(process.argv);
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { Command } from "commander";
4
+ import { RepoCommand } from "./commands/repo";
5
+ import { InitCommand } from "./commands/provider";
6
+ import { ReviewCommand } from "./commands/review";
7
+ import { TaskCommand } from "./commands/task";
8
+ import { ChatCommand } from "./commands/chat";
9
+ import { TimelineCommand } from "./commands/timeline";
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .command("repo <url>")
15
+ .description("Analyze a remote repository")
16
+ .action((url) => {
17
+ render(<RepoCommand url={url} />);
18
+ });
19
+
20
+ program
21
+ .command("provider")
22
+ .description("Configure AI providers")
23
+ .action(() => {
24
+ render(<InitCommand />);
25
+ });
26
+
27
+ program
28
+ .command("review [path]")
29
+ .description("Review a local codebase")
30
+ .action((inputPath) => {
31
+ render(<ReviewCommand path={inputPath ?? "."} />);
32
+ });
33
+
34
+ program
35
+ .command("task <text>")
36
+ .description("Apply a natural language change to the codebase")
37
+ .option("-p, --path <path>", "Path to the repo", ".")
38
+ .action((text: string, opts: { path: string }) => {
39
+ render(<TaskCommand prompt={text} path={opts.path} />);
40
+ });
41
+
42
+ program
43
+ .command("chat")
44
+ .description("Chat with your codebase — ask questions or make changes")
45
+ .option("-p, --path <path>", "Path to the repo", ".")
46
+ .action((opts: { path: string }) => {
47
+ render(<ChatCommand path={opts.path} />);
48
+ });
49
+
50
+ program
51
+ .command("timeline")
52
+ .description(
53
+ "Explore your code history — see commits, changes, and evolution",
54
+ )
55
+ .option("-p, --path <path>", "Path to the repo", ".")
56
+ .action((opts: { path: string }) => {
57
+ render(<TimelineCommand path={opts.path} />);
58
+ });
59
+
60
+ program.parse(process.argv);
package/src/types/chat.ts CHANGED
@@ -6,7 +6,13 @@ export type ToolCall =
6
6
  | { type: "shell"; command: string }
7
7
  | { type: "fetch"; url: string }
8
8
  | { type: "read-file"; filePath: string }
9
+ | { type: "read-folder"; folderPath: string }
10
+ | { type: "grep"; pattern: string; glob: string }
9
11
  | { type: "write-file"; filePath: string; fileContent: string }
12
+ | { type: "delete-file"; filePath: string }
13
+ | { type: "delete-folder"; folderPath: string }
14
+ | { type: "open-url"; url: string }
15
+ | { type: "generate-pdf"; filePath: string; content: string }
10
16
  | { type: "search"; query: string };
11
17
 
12
18
  // ── Messages ──────────────────────────────────────────────────────────────────
@@ -16,7 +22,18 @@ export type Message =
16
22
  | {
17
23
  role: "assistant";
18
24
  type: "tool";
19
- toolName: "shell" | "fetch" | "read-file" | "write-file" | "search";
25
+ toolName:
26
+ | "shell"
27
+ | "fetch"
28
+ | "read-file"
29
+ | "read-folder"
30
+ | "grep"
31
+ | "write-file"
32
+ | "delete-file"
33
+ | "delete-folder"
34
+ | "open-url"
35
+ | "generate-pdf"
36
+ | "search";
20
37
  content: string;
21
38
  result: string;
22
39
  approved: boolean;