@ridit/lens 0.3.3 → 0.3.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 +48 -0
- package/README.md +16 -14
- package/dist/index.mjs +4741 -4367
- package/package.json +1 -2
- package/src/commands/{watch.tsx → run.tsx} +3 -3
- package/src/components/chat/ChatRunner.tsx +131 -877
- package/src/components/chat/hooks/useChat.ts +531 -0
- package/src/components/chat/hooks/useChatInput.ts +79 -0
- package/src/components/chat/hooks/useCommandHandlers.ts +327 -0
- package/src/components/repo/LensFileMenu.tsx +2 -9
- package/src/components/repo/RepoAnalysis.tsx +241 -50
- package/src/components/watch/{WatchRunner.tsx → RunRunner.tsx} +1 -2
- package/src/index.tsx +12 -17
- package/src/tools/chart.ts +0 -8
- package/src/types/repo.ts +15 -3
- package/src/utils/ai.ts +108 -20
- package/src/utils/lensfile.ts +83 -18
package/src/utils/ai.ts
CHANGED
|
@@ -19,8 +19,8 @@ Your job is to select the files you need to read to fully understand what this p
|
|
|
19
19
|
Rules:
|
|
20
20
|
- ALWAYS include package.json, tsconfig.json, README.md if they exist
|
|
21
21
|
- ALWAYS include ALL files inside src/ — especially index files, main entry points, and any files that reveal the project's purpose (components, hooks, utilities, exports)
|
|
22
|
-
- Include config files: vite.config, eslint.config, tailwind.config, etc.
|
|
23
|
-
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
22
|
+
- Include config files: vite.config, eslint.config, tailwind.config, bun.lockb, .nvmrc, etc.
|
|
23
|
+
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
24
24
|
- Do NOT skip source files just because there are many — pick up to 30 files
|
|
25
25
|
- Prefer breadth: pick at least one file from every folder under src/
|
|
26
26
|
|
|
@@ -36,36 +36,96 @@ export function buildAnalysisPrompt(
|
|
|
36
36
|
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
37
37
|
.join("\n\n");
|
|
38
38
|
|
|
39
|
-
return `You are a senior software engineer
|
|
39
|
+
return `You are a senior software engineer building a persistent knowledge base about a codebase. Your output will be stored and incrementally updated over time — it must be durable, structural knowledge, not ephemeral warnings.
|
|
40
|
+
|
|
40
41
|
Repository URL: ${repoUrl}
|
|
41
42
|
|
|
42
43
|
Here are the file contents:
|
|
43
44
|
|
|
44
45
|
${fileList}
|
|
45
46
|
|
|
46
|
-
Analyze this repository
|
|
47
|
+
Analyze this repository and extract permanent, structural understanding. Focus on WHAT the codebase IS and HOW it works — not linting issues or missing configs.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
- Read
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
49
|
+
Rules:
|
|
50
|
+
- Read source code carefully. Reference real file names, real function names, real patterns.
|
|
51
|
+
- tooling: detect from package.json, lockfiles, config files. Keys: packageManager (npm/yarn/pnpm/bun), language, runtime, bundler, framework, testRunner, linter, formatter — only include what you actually found evidence of.
|
|
52
|
+
- keyFiles: list the most important files with a one-line description of what they do. Format: "src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai"
|
|
53
|
+
- patterns: list recurring idioms, design patterns, or conventions actually used in the code. E.g. "Discriminated union state machines for multi-stage UI flows", "React + Ink for terminal rendering"
|
|
54
|
+
- architecture: 2-3 sentences describing the high-level structure and how data flows through the system.
|
|
55
|
+
- importantFolders: describe EVERY folder with specifics — what files are in it and what they do.
|
|
56
|
+
- suggestions: specific, actionable improvements referencing real file names and real patterns you saw. No generic advice.
|
|
57
|
+
- overview: 3-5 sentences naming actual components, features, exports. Be specific.
|
|
56
58
|
|
|
57
|
-
Respond ONLY with a JSON object (no markdown, no explanation)
|
|
59
|
+
Respond ONLY with a JSON object (no markdown, no explanation):
|
|
58
60
|
{
|
|
59
|
-
"overview": "
|
|
61
|
+
"overview": "...",
|
|
62
|
+
"architecture": "...",
|
|
63
|
+
"tooling": {
|
|
64
|
+
"packageManager": "bun",
|
|
65
|
+
"language": "TypeScript",
|
|
66
|
+
"runtime": "Node.js",
|
|
67
|
+
"bundler": "tsup",
|
|
68
|
+
"framework": "Ink"
|
|
69
|
+
},
|
|
60
70
|
"importantFolders": [
|
|
61
|
-
"src/
|
|
71
|
+
"src/commands: contains chat.tsx, commit.tsx, review.tsx — each exports an Ink component that is the top-level renderer for that CLI command"
|
|
72
|
+
],
|
|
73
|
+
"keyFiles": [
|
|
74
|
+
"src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai providers via a unified Provider type"
|
|
75
|
+
],
|
|
76
|
+
"patterns": [
|
|
77
|
+
"Discriminated union state machines (type + stage fields) for multi-step UI flows in every command component"
|
|
62
78
|
],
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
"suggestions": [
|
|
80
|
+
"In src/utils/ai.ts, callModel has no retry logic — adding exponential backoff would improve reliability for ollama which can be slow to start"
|
|
81
|
+
]
|
|
66
82
|
}`;
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
export function buildToolingPatchPrompt(
|
|
86
|
+
repoUrl: string,
|
|
87
|
+
files: ImportantFile[],
|
|
88
|
+
): string {
|
|
89
|
+
const relevant = files.filter((f) =>
|
|
90
|
+
[
|
|
91
|
+
"package.json",
|
|
92
|
+
"bun.lockb",
|
|
93
|
+
"yarn.lock",
|
|
94
|
+
"pnpm-lock.yaml",
|
|
95
|
+
"package-lock.json",
|
|
96
|
+
"tsconfig.json",
|
|
97
|
+
".nvmrc",
|
|
98
|
+
".node-version",
|
|
99
|
+
].includes(path.basename(f.path)),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (relevant.length === 0) return "";
|
|
103
|
+
|
|
104
|
+
const fileList = relevant
|
|
105
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 2000)}\n\`\`\``)
|
|
106
|
+
.join("\n\n");
|
|
107
|
+
|
|
108
|
+
return `You are analyzing a repository's tooling configuration.
|
|
109
|
+
Repository: ${repoUrl}
|
|
110
|
+
|
|
111
|
+
${fileList}
|
|
112
|
+
|
|
113
|
+
Extract only tooling information. Respond ONLY with a JSON object:
|
|
114
|
+
{
|
|
115
|
+
"tooling": {
|
|
116
|
+
"packageManager": "bun | npm | yarn | pnpm",
|
|
117
|
+
"language": "TypeScript | JavaScript | ...",
|
|
118
|
+
"runtime": "Node.js | Bun | Deno | ...",
|
|
119
|
+
"bundler": "tsup | esbuild | vite | webpack | ...",
|
|
120
|
+
"framework": "React | Ink | Next.js | ...",
|
|
121
|
+
"testRunner": "vitest | jest | ...",
|
|
122
|
+
"linter": "eslint | biome | ...",
|
|
123
|
+
"formatter": "prettier | biome | ..."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
Only include keys where you found actual evidence. No markdown, no explanation.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
function parseStringArray(text: string): string[] {
|
|
70
130
|
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
71
131
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
@@ -87,12 +147,25 @@ function parseResult(text: string): AnalysisResult {
|
|
|
87
147
|
return {
|
|
88
148
|
overview: parsed.overview ?? "No overview provided",
|
|
89
149
|
importantFolders: parsed.importantFolders ?? [],
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
tooling: parsed.tooling ?? {},
|
|
151
|
+
keyFiles: parsed.keyFiles ?? [],
|
|
152
|
+
patterns: parsed.patterns ?? [],
|
|
153
|
+
architecture: parsed.architecture ?? "",
|
|
92
154
|
suggestions: parsed.suggestions ?? [],
|
|
93
155
|
};
|
|
94
156
|
}
|
|
95
157
|
|
|
158
|
+
function parseToolingPatch(text: string): Partial<AnalysisResult> | null {
|
|
159
|
+
try {
|
|
160
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
161
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
return JSON.parse(match[0]) as Partial<AnalysisResult>;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
96
169
|
export function checkOllamaInstalled(): Promise<boolean> {
|
|
97
170
|
return new Promise((resolve) => {
|
|
98
171
|
exec("ollama --version", (err) => resolve(!err));
|
|
@@ -220,6 +293,21 @@ export async function requestFileList(
|
|
|
220
293
|
return files;
|
|
221
294
|
}
|
|
222
295
|
|
|
296
|
+
export async function extractToolingPatch(
|
|
297
|
+
repoUrl: string,
|
|
298
|
+
files: ImportantFile[],
|
|
299
|
+
provider: Provider,
|
|
300
|
+
): Promise<Partial<AnalysisResult> | null> {
|
|
301
|
+
const prompt = buildToolingPatchPrompt(repoUrl, files);
|
|
302
|
+
if (!prompt) return null;
|
|
303
|
+
try {
|
|
304
|
+
const text = await callModel(provider, prompt);
|
|
305
|
+
return parseToolingPatch(text);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
223
311
|
export async function analyzeRepo(
|
|
224
312
|
repoUrl: string,
|
|
225
313
|
files: ImportantFile[],
|
package/src/utils/lensfile.ts
CHANGED
|
@@ -7,10 +7,13 @@ export const LENS_FILENAME = "LENS.md";
|
|
|
7
7
|
export type LensFile = {
|
|
8
8
|
overview: string;
|
|
9
9
|
importantFolders: string[];
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
tooling: Record<string, string>;
|
|
11
|
+
keyFiles: string[];
|
|
12
|
+
patterns: string[];
|
|
13
|
+
architecture: string;
|
|
12
14
|
suggestions: string[];
|
|
13
15
|
generatedAt: string;
|
|
16
|
+
lastUpdated: string;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export function lensFilePath(repoPath: string): string {
|
|
@@ -21,36 +24,96 @@ export function lensFileExists(repoPath: string): boolean {
|
|
|
21
24
|
return existsSync(lensFilePath(repoPath));
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
27
|
+
function renderLensFile(data: LensFile): string {
|
|
28
|
+
const toolingLines = Object.entries(data.tooling)
|
|
29
|
+
.map(([k, v]) => `- **${k}**: ${v}`)
|
|
30
|
+
.join("\n");
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
> Generated: ${data.generatedAt}
|
|
32
|
+
return `# Lens
|
|
33
|
+
> Generated: ${data.generatedAt}${data.lastUpdated !== data.generatedAt ? ` | Updated: ${data.lastUpdated}` : ""}
|
|
32
34
|
|
|
33
35
|
## Overview
|
|
34
36
|
${data.overview}
|
|
35
37
|
|
|
38
|
+
## Architecture
|
|
39
|
+
${data.architecture}
|
|
40
|
+
|
|
41
|
+
## Tooling & Conventions
|
|
42
|
+
${toolingLines || "- Not yet determined"}
|
|
43
|
+
|
|
36
44
|
## Important Folders
|
|
37
|
-
${data.importantFolders.map((f) => `- ${f}`).join("\n")}
|
|
45
|
+
${data.importantFolders.map((f) => `- ${f}`).join("\n") || "- None"}
|
|
38
46
|
|
|
39
|
-
##
|
|
40
|
-
${data.
|
|
47
|
+
## Key Files
|
|
48
|
+
${data.keyFiles.map((f) => `- ${f}`).join("\n") || "- None"}
|
|
41
49
|
|
|
42
|
-
##
|
|
43
|
-
${data.
|
|
50
|
+
## Patterns & Idioms
|
|
51
|
+
${data.patterns.map((p) => `- ${p}`).join("\n") || "- None"}
|
|
44
52
|
|
|
45
53
|
## Suggestions
|
|
46
|
-
${data.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
54
|
+
${data.suggestions.map((s) => `- ${s}`).join("\n") || "- None"}
|
|
47
55
|
|
|
48
56
|
<!--lens-json
|
|
49
57
|
${JSON.stringify(data)}
|
|
50
58
|
lens-json-->
|
|
51
59
|
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeLensFile(repoPath: string, result: AnalysisResult): void {
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const data: LensFile = {
|
|
65
|
+
overview: result.overview,
|
|
66
|
+
importantFolders: result.importantFolders,
|
|
67
|
+
tooling: result.tooling ?? {},
|
|
68
|
+
keyFiles: result.keyFiles ?? [],
|
|
69
|
+
patterns: result.patterns ?? [],
|
|
70
|
+
architecture: result.architecture ?? "",
|
|
71
|
+
suggestions: result.suggestions,
|
|
72
|
+
generatedAt: now,
|
|
73
|
+
lastUpdated: now,
|
|
74
|
+
};
|
|
75
|
+
writeFileSync(lensFilePath(repoPath), renderLensFile(data), "utf-8");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function patchLensFile(
|
|
79
|
+
repoPath: string,
|
|
80
|
+
patch: Partial<AnalysisResult>,
|
|
81
|
+
): void {
|
|
82
|
+
const existing = readLensFile(repoPath);
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
|
|
85
|
+
const base: LensFile = existing ?? {
|
|
86
|
+
overview: "",
|
|
87
|
+
importantFolders: [],
|
|
88
|
+
tooling: {},
|
|
89
|
+
keyFiles: [],
|
|
90
|
+
patterns: [],
|
|
91
|
+
architecture: "",
|
|
92
|
+
suggestions: [],
|
|
93
|
+
generatedAt: now,
|
|
94
|
+
lastUpdated: now,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const merged: LensFile = {
|
|
98
|
+
...base,
|
|
99
|
+
lastUpdated: now,
|
|
100
|
+
overview: patch.overview ?? base.overview,
|
|
101
|
+
architecture: patch.architecture ?? base.architecture,
|
|
102
|
+
tooling: { ...base.tooling, ...(patch.tooling ?? {}) },
|
|
103
|
+
importantFolders: dedup([
|
|
104
|
+
...base.importantFolders,
|
|
105
|
+
...(patch.importantFolders ?? []),
|
|
106
|
+
]),
|
|
107
|
+
keyFiles: dedup([...base.keyFiles, ...(patch.keyFiles ?? [])]),
|
|
108
|
+
patterns: dedup([...base.patterns, ...(patch.patterns ?? [])]),
|
|
109
|
+
suggestions: dedup([...base.suggestions, ...(patch.suggestions ?? [])]),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
writeFileSync(lensFilePath(repoPath), renderLensFile(merged), "utf-8");
|
|
113
|
+
}
|
|
52
114
|
|
|
53
|
-
|
|
115
|
+
function dedup(arr: string[]): string[] {
|
|
116
|
+
return [...new Map(arr.map((s) => [s.trim().toLowerCase(), s])).values()];
|
|
54
117
|
}
|
|
55
118
|
|
|
56
119
|
export function readLensFile(repoPath: string): LensFile | null {
|
|
@@ -70,8 +133,10 @@ export function lensFileToAnalysisResult(lf: LensFile): AnalysisResult {
|
|
|
70
133
|
return {
|
|
71
134
|
overview: lf.overview,
|
|
72
135
|
importantFolders: lf.importantFolders,
|
|
73
|
-
|
|
74
|
-
|
|
136
|
+
tooling: lf.tooling,
|
|
137
|
+
keyFiles: lf.keyFiles,
|
|
138
|
+
patterns: lf.patterns,
|
|
139
|
+
architecture: lf.architecture,
|
|
75
140
|
suggestions: lf.suggestions,
|
|
76
141
|
};
|
|
77
142
|
}
|