@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
package/src/index.tsx CHANGED
@@ -8,6 +8,7 @@ import { ReviewCommand } from "./commands/review";
8
8
  import { TaskCommand } from "./commands/task";
9
9
  import { ChatCommand } from "./commands/chat";
10
10
  import { TimelineCommand } from "./commands/timeline";
11
+ import { CommitCommand } from "./commands/commit";
11
12
  import { registerBuiltins } from "./utils/tools/builtins";
12
13
  import { loadAddons } from "./utils/addons/loadAddons";
13
14
 
@@ -63,4 +64,41 @@ program
63
64
  render(<TimelineCommand path={opts.path} />);
64
65
  });
65
66
 
67
+ program
68
+ .command("commit [files...]")
69
+ .description(
70
+ "Generate a smart conventional commit message from staged changes or specific files",
71
+ )
72
+ .option("-p, --path <path>", "Path to the repo", ".")
73
+ .option(
74
+ "--auto",
75
+ "Stage all changes (or the given files) and commit without confirmation",
76
+ )
77
+ .option("--confirm", "Show preview before committing even when using --auto")
78
+ .option("--preview", "Show the generated message without committing")
79
+ .option("--push", "Push to remote after committing")
80
+ .action(
81
+ (
82
+ files: string[],
83
+ opts: {
84
+ path: string;
85
+ auto: boolean;
86
+ confirm: boolean;
87
+ preview: boolean;
88
+ push: boolean;
89
+ },
90
+ ) => {
91
+ render(
92
+ <CommitCommand
93
+ path={opts.path}
94
+ files={files ?? []}
95
+ auto={opts.auto ?? false}
96
+ confirm={opts.confirm ?? false}
97
+ preview={opts.preview ?? false}
98
+ push={opts.push ?? false}
99
+ />,
100
+ );
101
+ },
102
+ );
103
+
66
104
  program.parse(process.argv);
@@ -1,4 +1,134 @@
1
1
  export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
2
+ // ── create new file directly (no read first) ──────────────────────────────
3
+ {
4
+ role: "user",
5
+ content: "this codebase doesn't have a readme",
6
+ },
7
+ {
8
+ role: "assistant",
9
+ content:
10
+ '<write-file>\n{"path": "README.md", "content": "# Project\\n\\nA brief description of what this project does.\\n\\n## Getting Started\\n\\n```bash\\nbun install\\nbun run dev\\n```"}\n</write-file>',
11
+ },
12
+ {
13
+ role: "user",
14
+ content:
15
+ "Here is the output from write-file to README.md:\n\nWritten: /repo/README.md (10 lines, 128 bytes)\n\nPlease continue your response based on this output.",
16
+ },
17
+ {
18
+ role: "assistant",
19
+ content: "Done — README.md has been created.",
20
+ },
21
+
22
+ // ── expand existing file (read first, then write) ─────────────────────────
23
+ {
24
+ role: "user",
25
+ content: "add more information to the README",
26
+ },
27
+ {
28
+ role: "assistant",
29
+ content: "<read-file>README.md</read-file>",
30
+ },
31
+ {
32
+ role: "user",
33
+ content:
34
+ "Here is the output from read-file of README.md:\n\nFile: README.md (10 lines)\n\n# Project\n\nA brief description.\n\n## Getting Started\n\n```bash\nbun install\n```\n\nPlease continue your response based on this output.",
35
+ },
36
+ {
37
+ role: "assistant",
38
+ content:
39
+ '<write-file>\n{"path": "README.md", "content": "# Project\\n\\nA brief description.\\n\\n## Getting Started\\n\\n```bash\\nbun install\\nbun run dev\\n```\\n\\n## Features\\n\\n- Feature one\\n- Feature two\\n\\n## Contributing\\n\\nPull requests welcome."}\n</write-file>',
40
+ },
41
+ {
42
+ role: "user",
43
+ content:
44
+ "Here is the output from write-file to README.md:\n\nWritten: /repo/README.md (20 lines, 280 bytes)\n\nPlease continue your response based on this output.",
45
+ },
46
+ {
47
+ role: "assistant",
48
+ content: "Done — README.md updated with more detail.",
49
+ },
50
+
51
+ // ── update an existing file ───────────────────────────────────────────────
52
+ {
53
+ role: "user",
54
+ content: "update LENS.md",
55
+ },
56
+ {
57
+ role: "assistant",
58
+ content: "<read-file>LENS.md</read-file>",
59
+ },
60
+ {
61
+ role: "user",
62
+ content:
63
+ "Here is the output from read-file of LENS.md:\n\nFile: LENS.md (33 lines)\n\n# Lens Analysis\n\n## Overview\nA CLI tool for codebase analysis.\n\n## Important Folders\n- src/components\n- src/utils\n\n## Suggestions\n- Add tests\n\nPlease continue your response based on this output.",
64
+ },
65
+ {
66
+ role: "assistant",
67
+ content:
68
+ '<write-file>\n{"path": "LENS.md", "content": "# Lens Analysis\\n\\n## Overview\\nA CLI tool for AI-powered codebase analysis, built with React and Ink.\\n\\n## Important Folders\\n- src/components — UI components rendered in the terminal\\n- src/utils — tools, chat, memory, and registry utilities\\n\\n## Suggestions\\n- Add tests\\n- Add more providers\\n- Improve error handling"}\n</write-file>',
69
+ },
70
+ {
71
+ role: "user",
72
+ content:
73
+ "Here is the output from write-file to LENS.md:\n\nWritten: /repo/LENS.md (15 lines, 420 bytes)\n\nPlease continue your response based on this output.",
74
+ },
75
+ {
76
+ role: "assistant",
77
+ content: "Done — LENS.md updated.",
78
+ },
79
+
80
+ // ── write a new doc file directly ─────────────────────────────────────────
81
+ {
82
+ role: "user",
83
+ content: "write a contributing guide",
84
+ },
85
+ {
86
+ role: "assistant",
87
+ content:
88
+ '<write-file>\n{"path": "CONTRIBUTING.md", "content": "# Contributing\\n\\nPull requests welcome. Please open an issue first to discuss major changes."}\n</write-file>',
89
+ },
90
+ {
91
+ role: "user",
92
+ content:
93
+ "Here is the output from write-file to CONTRIBUTING.md:\n\nWritten: /repo/CONTRIBUTING.md (4 lines, 96 bytes)\n\nPlease continue your response based on this output.",
94
+ },
95
+ {
96
+ role: "assistant",
97
+ content: "Done — CONTRIBUTING.md has been written.",
98
+ },
99
+
100
+ // ── scaffolding a project ─────────────────────────────────────────────────
101
+ {
102
+ role: "user",
103
+ content:
104
+ "scaffold a new project called myapp with package.json and src/index.tsx",
105
+ },
106
+ {
107
+ role: "assistant",
108
+ content:
109
+ '<write-file>\n{"path": "myapp/package.json", "content": "{\\n \\"name\\": \\"myapp\\",\\n \\"version\\": \\"1.0.0\\",\\n \\"scripts\\": { \\"dev\\": \\"bun run src/index.tsx\\" },\\n \\"dependencies\\": { \\"react\\": \\"^18.2.0\\", \\"ink\\": \\"^4.4.1\\" },\\n \\"devDependencies\\": { \\"@types/react\\": \\"^18.2.0\\" }\\n}"}\n</write-file>',
110
+ },
111
+ {
112
+ role: "user",
113
+ content:
114
+ "Here is the output from write-file to myapp/package.json:\n\nWritten: /repo/myapp/package.json (10 lines, 220 bytes)\n\nPlease continue your response based on this output.",
115
+ },
116
+ {
117
+ role: "assistant",
118
+ content:
119
+ '<write-file>\n{"path": "myapp/src/index.tsx", "content": "/** @jsxImportSource react */\\nimport React from \'react\';\\nimport { render, Text } from \'ink\';\\n\\nfunction App() {\\n return <Text>Hello from myapp!</Text>;\\n}\\n\\nrender(<App />);"}\n</write-file>',
120
+ },
121
+ {
122
+ role: "user",
123
+ content:
124
+ "Here is the output from write-file to myapp/src/index.tsx:\n\nWritten: /repo/myapp/src/index.tsx (9 lines, 210 bytes)\n\nPlease continue your response based on this output.",
125
+ },
126
+ {
127
+ role: "assistant",
128
+ content:
129
+ "Done — scaffolded myapp/package.json and myapp/src/index.tsx. Run `cd myapp && bun install` to get started.",
130
+ },
131
+
2
132
  // ── delete / open / pdf ───────────────────────────────────────────────────
3
133
  {
4
134
  role: "user",
@@ -15,7 +145,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
15
145
  },
16
146
  {
17
147
  role: "assistant",
18
- content: "Done — `src/old-component.tsx` has been deleted.",
148
+ content: "Done — src/old-component.tsx has been deleted.",
19
149
  },
20
150
  {
21
151
  role: "user",
@@ -32,8 +162,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
32
162
  },
33
163
  {
34
164
  role: "assistant",
35
- content:
36
- "Done — the `src/legacy` folder and all its contents have been deleted.",
165
+ content: "Done — src/legacy and all its contents have been deleted.",
37
166
  },
38
167
  {
39
168
  role: "user",
@@ -69,7 +198,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
69
198
  },
70
199
  {
71
200
  role: "assistant",
72
- content: "Done — the PDF report has been saved to `docs/report.pdf`.",
201
+ content: "Done — docs/report.pdf has been generated.",
73
202
  },
74
203
 
75
204
  // ── grep ──────────────────────────────────────────────────────────────────
@@ -89,7 +218,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
89
218
  {
90
219
  role: "assistant",
91
220
  content:
92
- "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`.",
221
+ "ChatRunner is defined in src/components/chat/ChatRunner.tsx and imported in src/index.tsx.",
93
222
  },
94
223
  {
95
224
  role: "user",
@@ -107,7 +236,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
107
236
  {
108
237
  role: "assistant",
109
238
  content:
110
- "`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210.",
239
+ "useInput is used in src/components/chat/ChatRunner.tsx — imported on line 5 and called on line 210.",
111
240
  },
112
241
 
113
242
  // ── showing tool usage as an example (bracket notation, NOT real tags) ────
@@ -147,7 +276,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
147
276
  {
148
277
  role: "assistant",
149
278
  content:
150
- "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`.",
279
+ "The src folder contains index.ts, App.tsx, plus subfolders components/ and utils/.",
151
280
  },
152
281
  {
153
282
  role: "user",
@@ -165,7 +294,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
165
294
  {
166
295
  role: "assistant",
167
296
  content:
168
- "The `src/components` folder has 3 files — `Header.tsx`, `Footer.tsx`, `Button.tsx` — plus two subfolders: `ui/` and `forms/`.",
297
+ "src/components has 3 files — Header.tsx, Footer.tsx, Button.tsx — plus two subfolders: ui/ and forms/.",
169
298
  },
170
299
  {
171
300
  role: "user",
@@ -183,10 +312,10 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
183
312
  {
184
313
  role: "assistant",
185
314
  content:
186
- "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code.",
315
+ "The project root has package.json, tsconfig.json, README.md and the src/ folder for source code.",
187
316
  },
188
317
 
189
- // ── fetch → write → done ─────────────────────────────────────────────────
318
+ // ── fetch → write → done ──────────────────────────────────────────────────
190
319
  {
191
320
  role: "user",
192
321
  content: "fetch https://api.github.com/repos/microsoft/typescript",
@@ -221,7 +350,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
221
350
  },
222
351
  {
223
352
  role: "assistant",
224
- content: "Done — `ts-info.json` has been saved.",
353
+ content: "Done — ts-info.json has been saved.",
225
354
  },
226
355
 
227
356
  // ── write it to a file → done ─────────────────────────────────────────────
@@ -241,10 +370,10 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
241
370
  },
242
371
  {
243
372
  role: "assistant",
244
- content: "Done — `hello.py` has been written.",
373
+ content: "Done — hello.py has been written.",
245
374
  },
246
375
 
247
- // ── read before write ─────────────────────────────────────────────────────
376
+ // ── read before write (modifying existing file only) ─────────────────────
248
377
  {
249
378
  role: "user",
250
379
  content: "add a logout button to src/components/Header.tsx",
@@ -320,42 +449,10 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
320
449
  {
321
450
  role: "assistant",
322
451
  content:
323
- "The `??` operator is the nullish coalescing operator. It returns the right side only when the left side is `null` or `undefined`.",
324
- },
325
-
326
- // ── scaffolding ───────────────────────────────────────────────────────────
327
- {
328
- role: "user",
329
- content:
330
- "scaffold a new project called myapp with package.json and src/index.ts",
331
- },
332
- {
333
- role: "assistant",
334
- content:
335
- '<write-file>\n{"path": "myapp/package.json", "content": "{\"name\":\"myapp\",\"version\":\"1.0.0\"}"}\n</write-file>',
336
- },
337
- {
338
- role: "user",
339
- content:
340
- "Here is the output from write-file to myapp/package.json:\n\nWritten: /repo/myapp/package.json (1 lines, 38 bytes)\n\nPlease continue your response based on this output.",
341
- },
342
- {
343
- role: "assistant",
344
- content:
345
- '<write-file>\n{"path": "myapp/src/index.ts", "content": "console.log(\'hello\')"}\n</write-file>',
346
- },
347
- {
348
- role: "user",
349
- content:
350
- "Here is the output from write-file to myapp/src/index.ts:\n\nWritten: /repo/myapp/src/index.ts (1 lines, 20 bytes)\n\nPlease continue your response based on this output.",
351
- },
352
- {
353
- role: "assistant",
354
- content:
355
- "Done — scaffolded `myapp/package.json` and `myapp/src/index.ts`. Run `cd myapp && npm install` to get started.",
452
+ "The ?? operator is the nullish coalescing operator. It returns the right side only when the left side is null or undefined.",
356
453
  },
357
454
 
358
- // ── memory-add example ────────────────────────────────────────────────────
455
+ // ── memory ────────────────────────────────────────────────────────────────
359
456
  {
360
457
  role: "user",
361
458
  content: "remember that we always use bun in this project",
@@ -9,8 +9,6 @@ export function buildSystemPrompt(
9
9
  .map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 2000)}\n\`\`\``)
10
10
  .join("\n\n");
11
11
 
12
- // If a toolsSection is supplied (e.g. from the plugin registry), use it.
13
- // Otherwise fall back to the static built-in section.
14
12
  const tools = toolsSection ?? BUILTIN_TOOLS_SECTION;
15
13
 
16
14
  return `You are an expert software engineer assistant with access to the user's codebase and tools.
@@ -54,6 +52,7 @@ You may emit multiple memory operations in a single response alongside normal co
54
52
  11. write-file content field must be the COMPLETE file content, never empty or placeholder
55
53
  12. After a write-file succeeds, do NOT repeat it — trust the result and move on
56
54
  13. After a write-file succeeds, tell the user it is done immediately — do NOT auto-read the file back to verify
55
+ 13a. NEVER read a file you just wrote — the write output confirms success. Reading back is a wasted tool call and will confuse you.
57
56
  14. NEVER apologize and redo a tool call you already made — if write-file or shell ran and returned a result, it worked, do not run it again
58
57
  15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
59
58
  16. NEVER second-guess yourself mid-response — commit to your answer
@@ -67,6 +66,9 @@ You may emit multiple memory operations in a single response alongside normal co
67
66
  24. When explaining how to use a tool in text, use [tag] bracket notation or a fenced code block — NEVER emit a real XML tool tag as part of an explanation or example
68
67
  25. NEVER read files, list folders, or run tools that were not asked for in the current user message
69
68
  26. NEVER use markdown formatting in plain text responses — no **bold**, no *italics*, no # headings, no bullet points with -, *, or +, no numbered lists, no backtick inline code. Write in plain prose. Only use fenced \`\`\` code blocks when showing actual code.
69
+ 27. When the user asks you to CREATE a new file (e.g. "write a README", "create a config", "add a license", "this codebase doesn't have X"), write it IMMEDIATELY — do NOT read first, even if a stub exists.
70
+ 28. When a tool result is returned, your response must be directly based on that result — never invent or hallucinate content unrelated to the tool output.
71
+ 29. When scaffolding multiple files, emit ONE write-file tag per response — wait for each result before emitting the next. Never chain multiple write-file tags in a single response when file content is complex (more than 20 lines).
70
72
 
71
73
  ## SCAFFOLDING — CHAINING WRITE-FILE CALLS
72
74
 
@@ -86,25 +88,23 @@ in a single response by chaining the tags back-to-back with no text between them
86
88
  The system processes each tag sequentially and automatically continues to the next one.
87
89
  Do NOT wait for a user message between files — emit all tags at once.
88
90
 
89
- ## CRITICAL: READ BEFORE YOU WRITE
91
+ ## WHEN TO READ BEFORE WRITING
90
92
 
91
- These rules are mandatory whenever you plan to edit or create a file:
93
+ Only read a file before writing if ALL of these are true:
94
+ - The file already exists AND has content you need to preserve
95
+ - The user explicitly asked you to modify, edit, or update it (not create it)
96
+ - You do not already have the file content in this conversation
92
97
 
93
- ### Before modifying ANY existing file:
94
- 1. ALWAYS use read-file on the exact file you plan to change FIRST
95
- 2. Study the full current content — understand every import, every export, every type, every existing feature
96
- 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
97
- 4. If you are unsure what other files import from the file you are editing, use read-folder on the parent directory first to see what exists nearby, then read-file the relevant ones
98
+ Never read before writing when:
99
+ - The user asked you to create, write, or add a new file
100
+ - The file is empty, missing, or a stub
101
+ - You already read it earlier in this conversation
98
102
 
99
- ### Before adding a feature that touches multiple files:
100
- 1. Use read-folder on the relevant directory to see what files exist
101
- 2. Use read-file on each file you plan to touch
102
- 3. Only then emit a changes tag with patches that are surgical additions, not wholesale rewrites
103
-
104
- ### The golden rule for write-file and changes:
105
- - The output file must contain EVERYTHING the original had, PLUS your new additions
106
- - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
107
- - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
103
+ When modifying an existing file:
104
+ 1. Use read-file on the exact file first
105
+ 2. Preserve ALL existing content do not remove anything that was not part of the request
106
+ 3. Your write-file must contain EVERYTHING the original had, PLUS your additions
107
+ 4. NEVER produce a file shorter than the original unless explicitly asked to delete things
108
108
 
109
109
  ## CODEBASE
110
110
 
@@ -113,8 +113,6 @@ ${fileList.length > 0 ? fileList : "(no files indexed)"}
113
113
  ${memorySummary}`;
114
114
  }
115
115
 
116
- // ── Static fallback tools section (used when registry is not available) ───────
117
-
118
116
  const BUILTIN_TOOLS_SECTION = `## TOOLS
119
117
 
120
118
  You have exactly thirteen tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
@@ -164,4 +162,10 @@ You have exactly thirteen tools. To use a tool you MUST wrap it in the exact XML
164
162
  ### 13. changes — propose code edits (shown as a diff for user approval)
165
163
  <changes>
166
164
  {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
167
- </changes>`;
165
+ </changes>
166
+
167
+ ### 14. read-files — read multiple files from the repo at once
168
+ <read-files>
169
+ ["src/foo.ts", "src/bar.ts"]
170
+ </read-files>
171
+ `;
@@ -0,0 +1,210 @@
1
+ //
2
+
3
+ //
4
+
5
+ //
6
+
7
+ import type { Tool } from "@ridit/lens-sdk";
8
+
9
+ type ChartType = "bar" | "line" | "sparkline";
10
+
11
+ interface ChartInput {
12
+ type: ChartType;
13
+ title?: string;
14
+ labels?: string[];
15
+ values: number[];
16
+ /** For line charts: height in rows. Default 10. */
17
+ height?: number;
18
+ /** Bar fill character. Default "█" */
19
+ fill?: string;
20
+ }
21
+
22
+ function parseChartInput(body: string): ChartInput | null {
23
+ const trimmed = body.trim();
24
+ if (!trimmed) return null;
25
+ try {
26
+ const parsed = JSON.parse(trimmed) as Partial<ChartInput> & {
27
+ data?: number[];
28
+ series?: number[];
29
+ };
30
+ const values = parsed.values ?? parsed.data ?? parsed.series ?? [];
31
+ if (!Array.isArray(values) || values.length === 0) return null;
32
+ return {
33
+ type: parsed.type ?? "bar",
34
+ title: parsed.title,
35
+ labels: parsed.labels,
36
+ values: values.map(Number),
37
+ height: parsed.height ?? 10,
38
+ fill: parsed.fill ?? "█",
39
+ };
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ const C = {
46
+ reset: "\x1b[0m",
47
+ dim: "\x1b[2m",
48
+ bold: "\x1b[1m",
49
+ orange: "\x1b[38;2;218;119;88m",
50
+ cyan: "\x1b[36m",
51
+ white: "\x1b[37m",
52
+ gray: "\x1b[90m",
53
+ green: "\x1b[32m",
54
+ yellow: "\x1b[33m",
55
+ };
56
+
57
+ const PALETTE = [C.orange, C.cyan, C.green, C.yellow];
58
+ function color(i: number) {
59
+ return PALETTE[i % PALETTE.length]!;
60
+ }
61
+
62
+ function renderBar(input: ChartInput): string {
63
+ const { values, labels, title, fill = "█" } = input;
64
+ const max = Math.max(...values, 1);
65
+ const termW = process.stdout.columns ?? 80;
66
+ const maxLabelLen = labels
67
+ ? Math.max(...labels.map((l) => l.length), 0)
68
+ : String(values.length).length + 1;
69
+ const barMaxW = Math.max(20, termW - maxLabelLen - 12);
70
+
71
+ const lines: string[] = [];
72
+ if (title) lines.push(`${C.bold}${C.white}${title}${C.reset}\n`);
73
+
74
+ values.forEach((v, i) => {
75
+ const label = labels?.[i] ?? String(i + 1);
76
+ const barLen = Math.round((v / max) * barMaxW);
77
+ const bar = fill.repeat(barLen);
78
+ const valueStr = String(v);
79
+ lines.push(
80
+ `${C.gray}${label.padStart(maxLabelLen)}${C.reset} ` +
81
+ `${color(i)}${bar}${C.reset} ` +
82
+ `${C.dim}${valueStr}${C.reset}`,
83
+ );
84
+ });
85
+
86
+ lines.push(
87
+ `${" ".repeat(maxLabelLen + 1)}${C.gray}${"─".repeat(barMaxW)}${C.reset}`,
88
+ );
89
+ lines.push(
90
+ `${" ".repeat(maxLabelLen + 1)}${C.gray}0${" ".repeat(barMaxW - String(max).length)}${max}${C.reset}`,
91
+ );
92
+
93
+ return lines.join("\n");
94
+ }
95
+
96
+ const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
97
+
98
+ function renderSparkline(input: ChartInput): string {
99
+ const { values, title } = input;
100
+ const min = Math.min(...values);
101
+ const max = Math.max(...values, min + 1);
102
+ const range = max - min;
103
+ const spark = values
104
+ .map((v) => {
105
+ const idx = Math.floor(((v - min) / range) * (SPARK_CHARS.length - 1));
106
+ return `${color(0)}${SPARK_CHARS[idx] ?? "▁"}${C.reset}`;
107
+ })
108
+ .join("");
109
+
110
+ const lines: string[] = [];
111
+ if (title) lines.push(`${C.bold}${C.white}${title}${C.reset}`);
112
+ lines.push(spark);
113
+ lines.push(`${C.gray}min ${min} max ${max} n=${values.length}${C.reset}`);
114
+ return lines.join("\n");
115
+ }
116
+
117
+ async function renderLine(input: ChartInput): Promise<string> {
118
+ let asciichart: any;
119
+ try {
120
+ asciichart = await import("asciichart");
121
+ asciichart = asciichart.default ?? asciichart;
122
+ } catch {
123
+ return (
124
+ `${C.yellow}asciichart not installed (npm install asciichart). ` +
125
+ `Falling back to sparkline:${C.reset}\n` +
126
+ renderSparkline(input)
127
+ );
128
+ }
129
+
130
+ const termW = process.stdout.columns ?? 80;
131
+ const height = input.height ?? 10;
132
+
133
+ const lines: string[] = [];
134
+ if (input.title) {
135
+ lines.push(`${C.bold}${C.white}${input.title}${C.reset}\n`);
136
+ }
137
+
138
+ const chart = asciichart.plot(input.values, {
139
+ height,
140
+ width: Math.min(input.values.length, termW - 14),
141
+ colors: [asciichart.cyan],
142
+ });
143
+
144
+ lines.push(chart);
145
+
146
+ if (input.labels && input.labels.length === input.values.length) {
147
+ const step = Math.max(
148
+ 1,
149
+ Math.floor(input.labels.length / Math.min(input.labels.length, 10)),
150
+ );
151
+ const labelRow = input.labels
152
+ .filter((_, i) => i % step === 0)
153
+ .map((l) => l.slice(0, 6).padEnd(6))
154
+ .join(" ");
155
+ lines.push(`${C.gray}${" ".repeat(8)}${labelRow}${C.reset}`);
156
+ }
157
+
158
+ return lines.join("\n");
159
+ }
160
+
161
+ async function renderChart(input: ChartInput): Promise<string> {
162
+ switch (input.type) {
163
+ case "bar":
164
+ return renderBar(input);
165
+ case "sparkline":
166
+ return renderSparkline(input);
167
+ case "line":
168
+ return await renderLine(input);
169
+ default:
170
+ return renderBar(input);
171
+ }
172
+ }
173
+
174
+ export const chartDataTool: Tool<ChartInput> = {
175
+ name: "chart-data",
176
+ description:
177
+ "render a bar, line, or sparkline chart in the terminal from given data",
178
+ safe: true,
179
+ permissionLabel: "chart",
180
+
181
+ systemPromptEntry: (i) =>
182
+ `### ${i}. chart-data — render a chart in the terminal\n` +
183
+ `Types: "bar" (default), "line", "sparkline"\n` +
184
+ `<chart-data>\n` +
185
+ `{"type": "bar", "title": "Commits per month", "labels": ["Jan","Feb","Mar"], "values": [12, 34, 21]}\n` +
186
+ `</chart-data>\n` +
187
+ `<chart-data>\n` +
188
+ `{"type": "line", "title": "Stars over time", "values": [1,3,6,10,15,21,28], "height": 8}\n` +
189
+ `</chart-data>\n` +
190
+ `<chart-data>\n` +
191
+ `{"type": "sparkline", "title": "Daily commits", "values": [2,5,1,8,3,7,4]}\n` +
192
+ `</chart-data>`,
193
+
194
+ parseInput: parseChartInput,
195
+
196
+ summariseInput: ({ type, title }) =>
197
+ title ? `${type} chart — ${title}` : `${type} chart`,
198
+
199
+ execute: async (input, _ctx) => {
200
+ try {
201
+ const rendered = await renderChart(input);
202
+ return { kind: "image" as any, value: rendered };
203
+ } catch (err) {
204
+ return {
205
+ kind: "text",
206
+ value: `Error rendering chart: ${err instanceof Error ? err.message : String(err)}`,
207
+ };
208
+ }
209
+ },
210
+ };