@ridit/lens 0.2.4 → 0.2.5
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/dist/index.mjs +69211 -2205
- package/package.json +6 -3
- package/src/commands/commit.tsx +713 -0
- package/src/components/chat/ChatOverlays.tsx +14 -4
- package/src/components/chat/ChatRunner.tsx +197 -30
- 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 +505 -422
- package/src/index.tsx +38 -0
- package/src/prompts/fewshot.ts +100 -3
- package/src/prompts/system.ts +16 -20
- 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/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/chat.ts +7 -17
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +8 -31
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);
|
package/src/prompts/fewshot.ts
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
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
|
+
// ── write a new doc file directly ─────────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
role: "user",
|
|
54
|
+
content: "write a contributing guide",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
role: "assistant",
|
|
58
|
+
content:
|
|
59
|
+
'<write-file>\n{"path": "CONTRIBUTING.md", "content": "# Contributing\\n\\nPull requests welcome. Please open an issue first to discuss major changes."}\n</write-file>',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
role: "user",
|
|
63
|
+
content:
|
|
64
|
+
"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.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: "Done — CONTRIBUTING.md has been written.",
|
|
69
|
+
},
|
|
70
|
+
|
|
2
71
|
// ── delete / open / pdf ───────────────────────────────────────────────────
|
|
3
72
|
{
|
|
4
73
|
role: "user",
|
|
@@ -186,7 +255,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
186
255
|
"The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code.",
|
|
187
256
|
},
|
|
188
257
|
|
|
189
|
-
// ── fetch → write → done
|
|
258
|
+
// ── fetch → write → done ──────────────────────────────────────────────────
|
|
190
259
|
{
|
|
191
260
|
role: "user",
|
|
192
261
|
content: "fetch https://api.github.com/repos/microsoft/typescript",
|
|
@@ -244,7 +313,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
244
313
|
content: "Done — `hello.py` has been written.",
|
|
245
314
|
},
|
|
246
315
|
|
|
247
|
-
// ── read before write
|
|
316
|
+
// ── read before write (modifying existing file only) ─────────────────────
|
|
248
317
|
{
|
|
249
318
|
role: "user",
|
|
250
319
|
content: "add a logout button to src/components/Header.tsx",
|
|
@@ -355,7 +424,7 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
355
424
|
"Done — scaffolded `myapp/package.json` and `myapp/src/index.ts`. Run `cd myapp && npm install` to get started.",
|
|
356
425
|
},
|
|
357
426
|
|
|
358
|
-
// ── memory
|
|
427
|
+
// ── memory ────────────────────────────────────────────────────────────────
|
|
359
428
|
{
|
|
360
429
|
role: "user",
|
|
361
430
|
content: "remember that we always use bun in this project",
|
|
@@ -374,4 +443,32 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
374
443
|
content:
|
|
375
444
|
"<memory-delete>MEMORY_ID</memory-delete>\n\nDone — removed that memory.",
|
|
376
445
|
},
|
|
446
|
+
// ── make existing file more descriptive ───────────────────────────────────
|
|
447
|
+
{
|
|
448
|
+
role: "user",
|
|
449
|
+
content: "make the README.md more descriptive",
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
role: "assistant",
|
|
453
|
+
content: "<read-file>README.md</read-file>",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
role: "user",
|
|
457
|
+
content:
|
|
458
|
+
"Here is the output from read-file of README.md:\n\nFile: README.md (50 lines)\n\n# Lens\n\nA CLI tool for codebase analysis.\n\n## Getting Started\n\n```bash\nbun install\nbun run dev\n```\n\nPlease continue your response based on this output.",
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
role: "assistant",
|
|
462
|
+
content:
|
|
463
|
+
'<write-file>\n{"path": "README.md", "content": "# Lens\\n\\nLens is a CLI tool that brings AI-powered codebase analysis directly to your terminal. It lets you explore, understand, and modify any repository through natural language.\\n\\n## Features\\n\\n- Chat with your codebase using any LLM provider\\n- Read, write, and edit files with AI assistance\\n- Browse commit history with /timeline\\n- Auto-approve safe tool calls with /auto\\n- Persistent memory across sessions\\n\\n## Getting Started\\n\\n```bash\\nbun install\\nbun run dev\\n```\\n\\n## Commands\\n\\n- /timeline — browse commit history\\n- /auto — toggle auto-approve for safe tools\\n- /review — review the current codebase\\n- /memory list — list stored memories\\n- /chat list — list saved chat sessions"}\n</write-file>',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
role: "user",
|
|
467
|
+
content:
|
|
468
|
+
"Here is the output from write-file to README.md:\n\nWritten: /repo/README.md (30 lines, 800 bytes)\n\nPlease continue your response based on this output.",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
role: "assistant",
|
|
472
|
+
content: "Done — README.md updated with a full description of Lens.",
|
|
473
|
+
},
|
|
377
474
|
];
|
package/src/prompts/system.ts
CHANGED
|
@@ -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,7 @@ 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
70
|
|
|
71
71
|
## SCAFFOLDING — CHAINING WRITE-FILE CALLS
|
|
72
72
|
|
|
@@ -86,25 +86,23 @@ in a single response by chaining the tags back-to-back with no text between them
|
|
|
86
86
|
The system processes each tag sequentially and automatically continues to the next one.
|
|
87
87
|
Do NOT wait for a user message between files — emit all tags at once.
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## WHEN TO READ BEFORE WRITING
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Only read a file before writing if ALL of these are true:
|
|
92
|
+
- The file already exists AND has content you need to preserve
|
|
93
|
+
- The user explicitly asked you to modify, edit, or update it (not create it)
|
|
94
|
+
- You do not already have the file content in this conversation
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
96
|
+
Never read before writing when:
|
|
97
|
+
- The user asked you to create, write, or add a new file
|
|
98
|
+
- The file is empty, missing, or a stub
|
|
99
|
+
- You already read it earlier in this conversation
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
1. Use read-
|
|
101
|
-
2.
|
|
102
|
-
3.
|
|
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
|
|
101
|
+
When modifying an existing file:
|
|
102
|
+
1. Use read-file on the exact file first
|
|
103
|
+
2. Preserve ALL existing content — do not remove anything that was not part of the request
|
|
104
|
+
3. Your write-file must contain EVERYTHING the original had, PLUS your additions
|
|
105
|
+
4. NEVER produce a file shorter than the original unless explicitly asked to delete things
|
|
108
106
|
|
|
109
107
|
## CODEBASE
|
|
110
108
|
|
|
@@ -113,8 +111,6 @@ ${fileList.length > 0 ? fileList : "(no files indexed)"}
|
|
|
113
111
|
${memorySummary}`;
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
// ── Static fallback tools section (used when registry is not available) ───────
|
|
117
|
-
|
|
118
114
|
const BUILTIN_TOOLS_SECTION = `## TOOLS
|
|
119
115
|
|
|
120
116
|
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.
|
|
@@ -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
|
+
};
|