@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/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,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-add example ────────────────────────────────────────────────────
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
  ];
@@ -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
- ## CRITICAL: READ BEFORE YOU WRITE
89
+ ## WHEN TO READ BEFORE WRITING
90
90
 
91
- These rules are mandatory whenever you plan to edit or create a file:
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
- ### 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
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
- ### 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
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
+ };