@ridit/lens 0.3.6 → 0.3.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.
- package/CLAUDE.md +50 -0
- package/dist/index.mjs +1967 -1396
- package/package.json +1 -1
- package/src/commands/chat.tsx +14 -20
- package/src/components/chat/ChatMessage.tsx +46 -4
- package/src/components/chat/ChatOverlays.tsx +27 -22
- package/src/components/chat/ChatRunner.tsx +55 -15
- package/src/components/chat/TextArea.tsx +177 -0
- package/src/components/chat/hooks/useChat.ts +417 -226
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -4
- package/src/components/repo/RepoAnalysis.tsx +5 -5
- package/src/components/task/TaskRunner.tsx +3 -3
- package/src/components/timeline/TimelineRunner.tsx +2 -2
- package/src/components/watch/RunRunner.tsx +2 -1
- package/src/index.tsx +13 -2
- package/src/prompts/fewshot.ts +18 -0
- package/src/prompts/system.ts +30 -17
- package/src/types/chat.ts +3 -1
- package/src/utils/chat.ts +73 -8
- package/src/utils/intentClassifier.ts +0 -15
- package/src/utils/memory.ts +103 -26
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/tools/builtins.ts +47 -6
|
@@ -66,10 +66,6 @@ function makeMsg(content: string): Message {
|
|
|
66
66
|
return { role: "assistant", content, type: "text" };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Returns true if the command was handled, false if it should fall through
|
|
71
|
-
* to the normal message flow.
|
|
72
|
-
*/
|
|
73
69
|
export function handleCommand(text: string, ctx: CommandContext): boolean {
|
|
74
70
|
const t = text.trim().toLowerCase();
|
|
75
71
|
|
|
@@ -105,7 +105,7 @@ function AskingFilesStep() {
|
|
|
105
105
|
return (
|
|
106
106
|
<Box gap={1}>
|
|
107
107
|
<Text color={ACCENT}>
|
|
108
|
-
<Spinner />
|
|
108
|
+
<Spinner type="moon" />
|
|
109
109
|
</Text>
|
|
110
110
|
<Text color={ACCENT}>{phrase}</Text>
|
|
111
111
|
</Box>
|
|
@@ -117,7 +117,7 @@ function AnalyzingStep() {
|
|
|
117
117
|
return (
|
|
118
118
|
<Box gap={1}>
|
|
119
119
|
<Text color={ACCENT}>
|
|
120
|
-
<Spinner />
|
|
120
|
+
<Spinner type="earth" />
|
|
121
121
|
</Text>
|
|
122
122
|
<Text color={ACCENT}>{phrase}</Text>
|
|
123
123
|
</Box>
|
|
@@ -174,11 +174,11 @@ function CodebaseQA({
|
|
|
174
174
|
abortRef.current = abort;
|
|
175
175
|
|
|
176
176
|
callChat(provider, systemPrompt, nextAll, abort.signal)
|
|
177
|
-
.then((
|
|
177
|
+
.then((result) => {
|
|
178
178
|
const assistantMsg: Message = {
|
|
179
179
|
role: "assistant",
|
|
180
180
|
type: "text",
|
|
181
|
-
content:
|
|
181
|
+
content: result.text,
|
|
182
182
|
};
|
|
183
183
|
setCommitted((prev) => [...prev, assistantMsg]);
|
|
184
184
|
setAllMessages([...nextAll, assistantMsg]);
|
|
@@ -366,7 +366,7 @@ export const RepoAnalysis = ({
|
|
|
366
366
|
return (
|
|
367
367
|
<Box marginTop={1}>
|
|
368
368
|
<Text color={ACCENT}>
|
|
369
|
-
<Spinner />
|
|
369
|
+
<Spinner type="arc" />
|
|
370
370
|
</Text>
|
|
371
371
|
<Box marginLeft={1}>
|
|
372
372
|
<Text>Writing file...</Text>
|
|
@@ -88,7 +88,7 @@ function ThinkingAboutStep({ prompt }: { prompt: string }) {
|
|
|
88
88
|
return (
|
|
89
89
|
<Box gap={1}>
|
|
90
90
|
<Text color={ACCENT}>
|
|
91
|
-
<Spinner />
|
|
91
|
+
<Spinner type="triangle" />
|
|
92
92
|
</Text>
|
|
93
93
|
<Text color={ACCENT}>{phrase}</Text>
|
|
94
94
|
<Text color="gray">"{prompt}"</Text>
|
|
@@ -270,7 +270,7 @@ export const PromptRunner = ({
|
|
|
270
270
|
return (
|
|
271
271
|
<Box marginTop={1} gap={1}>
|
|
272
272
|
<Text color={ACCENT}>
|
|
273
|
-
<Spinner />
|
|
273
|
+
<Spinner type="dots2" />
|
|
274
274
|
</Text>
|
|
275
275
|
<Text>Reading codebase...</Text>
|
|
276
276
|
</Box>
|
|
@@ -322,7 +322,7 @@ export const PromptRunner = ({
|
|
|
322
322
|
return (
|
|
323
323
|
<Box marginTop={1} gap={1}>
|
|
324
324
|
<Text color={ACCENT}>
|
|
325
|
-
<Spinner />
|
|
325
|
+
<Spinner type="bouncingBar" />
|
|
326
326
|
</Text>
|
|
327
327
|
<Text>Applying changes...</Text>
|
|
328
328
|
</Box>
|
|
@@ -777,9 +777,9 @@ ${summarizeTimeline(commits)}`;
|
|
|
777
777
|
|
|
778
778
|
const runChat = async (history: Message[], signal: AbortSignal) => {
|
|
779
779
|
try {
|
|
780
|
-
const
|
|
780
|
+
const result = await callChat(provider, systemPrompt, history, signal);
|
|
781
781
|
if (signal.aborted) return;
|
|
782
|
-
processResponse(
|
|
782
|
+
processResponse(result.text, history, signal);
|
|
783
783
|
} catch (e: any) {
|
|
784
784
|
if (e?.name === "AbortError") return;
|
|
785
785
|
setMessages((prev) => [
|
|
@@ -602,12 +602,13 @@ ${lensFile.suggestions.length > 0 ? `\nProject suggestions:\n${lensFile.suggesti
|
|
|
602
602
|
|
|
603
603
|
let raw: string;
|
|
604
604
|
try {
|
|
605
|
-
|
|
605
|
+
const result = await callChat(
|
|
606
606
|
provider,
|
|
607
607
|
systemPromptRef.current,
|
|
608
608
|
messages,
|
|
609
609
|
combinedSignal,
|
|
610
610
|
);
|
|
611
|
+
raw = result.text;
|
|
611
612
|
} finally {
|
|
612
613
|
clearTimeout(timeoutId);
|
|
613
614
|
}
|
package/src/index.tsx
CHANGED
|
@@ -16,6 +16,11 @@ import { loadAddons } from "./utils/addons/loadAddons";
|
|
|
16
16
|
registerBuiltins();
|
|
17
17
|
await loadAddons();
|
|
18
18
|
|
|
19
|
+
if (process.stdout.isTTY) {
|
|
20
|
+
process.stdout.write("\x1b[>4;1m");
|
|
21
|
+
process.on("exit", () => process.stdout.write("\x1b[>4;0m"));
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
const program = new Command();
|
|
20
25
|
|
|
21
26
|
program
|
|
@@ -51,8 +56,14 @@ program
|
|
|
51
56
|
.command("chat")
|
|
52
57
|
.description("Chat with your codebase — ask questions or make changes")
|
|
53
58
|
.option("-p, --path <path>", "Path to the repo", ".")
|
|
54
|
-
.
|
|
55
|
-
|
|
59
|
+
.option(
|
|
60
|
+
"--auto-force",
|
|
61
|
+
"Start with force-all mode enabled (auto-approves all tools)",
|
|
62
|
+
)
|
|
63
|
+
.action((opts: { path: string; autoForce: boolean }) => {
|
|
64
|
+
render(
|
|
65
|
+
<ChatCommand path={opts.path} autoForce={opts.autoForce ?? false} />,
|
|
66
|
+
);
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
program
|
package/src/prompts/fewshot.ts
CHANGED
|
@@ -231,4 +231,22 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
231
231
|
content:
|
|
232
232
|
"Done — addons/hello-world.js created using defineTool from @ridit/lens-sdk.",
|
|
233
233
|
},
|
|
234
|
+
{
|
|
235
|
+
role: "user",
|
|
236
|
+
content: "I ran the app and got this error:\n[ERROR] slice(None, 2, None)",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
role: "assistant",
|
|
240
|
+
content: "<read-file>webfetch/parser.py</read-file>",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
role: "user",
|
|
244
|
+
content:
|
|
245
|
+
"Here is the output from read-file of webfetch/parser.py:\n\n# file content here\n\nPlease continue your response based on this output.",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
role: "assistant",
|
|
249
|
+
content:
|
|
250
|
+
'<write-file>\n{"path": "webfetch/parser.py", "content": "...complete fixed content..."}\n</write-file>',
|
|
251
|
+
},
|
|
234
252
|
];
|
package/src/prompts/system.ts
CHANGED
|
@@ -21,34 +21,47 @@ ${tools}
|
|
|
21
21
|
You can save and delete memories at any time by emitting these tags alongside your normal response.
|
|
22
22
|
They are stripped before display — the user will not see the raw tags.
|
|
23
23
|
|
|
24
|
-
### memory-add — save something important to long-term memory
|
|
24
|
+
### memory-add — save something important to long-term memory
|
|
25
25
|
<memory-add>User prefers TypeScript strict mode in all new files</memory-add>
|
|
26
26
|
|
|
27
|
+
Use [global] prefix for things that apply across ALL repos (user preferences, name, coding style):
|
|
28
|
+
<memory-add>[global] User prefers bun over npm for all projects</memory-add>
|
|
29
|
+
|
|
30
|
+
Omit [global] for repo-specific memories (architecture decisions, patterns, agreed conventions):
|
|
31
|
+
<memory-add>This repo uses path aliases defined in tsconfig.json</memory-add>
|
|
32
|
+
|
|
27
33
|
### memory-delete — delete a memory by its ID (shown in brackets like [abc123])
|
|
28
34
|
<memory-delete>abc123</memory-delete>
|
|
29
35
|
|
|
30
|
-
Use memory-add
|
|
36
|
+
Use memory-add ONLY for information that cannot be inferred by reading the codebase:
|
|
37
|
+
- User preferences and coding conventions
|
|
38
|
+
- Decisions made during the session (e.g. "user chose bun over npm")
|
|
39
|
+
- Things the user explicitly asked you to remember
|
|
40
|
+
- Cross-session context that would otherwise be lost
|
|
41
|
+
|
|
42
|
+
NEVER save memories that just describe what files exist or what the project does — that can be read directly from the codebase.
|
|
31
43
|
Use memory-delete when the user asks you to forget something or a memory is outdated.
|
|
32
44
|
|
|
33
45
|
## RULES
|
|
34
46
|
|
|
35
47
|
1. ONE tool per response — emit the XML tag, then stop. Never chain tools in one response except when scaffolding (see below).
|
|
36
48
|
2. NEVER call a tool more than once for the same path in a session. If write-file or shell returned a result, it succeeded. Move on immediately.
|
|
37
|
-
3.
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
40
|
-
6. NEVER
|
|
41
|
-
7.
|
|
42
|
-
8.
|
|
43
|
-
9. When
|
|
44
|
-
10.
|
|
45
|
-
11.
|
|
46
|
-
12.
|
|
47
|
-
13.
|
|
48
|
-
14.
|
|
49
|
-
15. When
|
|
50
|
-
16.
|
|
51
|
-
17.
|
|
49
|
+
3. shell is ONLY for running code, installing packages, building, and testing. NEVER use shell to inspect the filesystem or read files — use read-file, read-folder, or grep instead.
|
|
50
|
+
4. NEVER use shell to run git clone — use the clone tag instead.
|
|
51
|
+
5. NEVER read a file you just wrote. The write output confirms success.
|
|
52
|
+
6. NEVER apologize and redo a tool call — one attempt is enough, trust the output.
|
|
53
|
+
7. When the user asks you to CREATE a brand new file, use write-file immediately — do NOT read first. write-file content must be the COMPLETE file content.
|
|
54
|
+
8. When the user asks you to MODIFY or FIX an existing file, read it first, then propose the edit using changes — NEVER use write-file on an existing file.
|
|
55
|
+
9. When fixing multiple files, use read-files to read ALL of them first, then emit one changes tag with all patches together.
|
|
56
|
+
10. If a read-folder or read-file returns not found, accept it and move on — do NOT retry the same path.
|
|
57
|
+
11. Every shell command runs from the repo root — cd has no persistent effect. Use full paths or combine with && e.g. cd myapp && bun run index.ts
|
|
58
|
+
12. write-file paths are relative to the repo root — use full relative paths e.g. myapp/src/index.tsx not src/index.tsx
|
|
59
|
+
13. When explaining how to use a tool in text, use [tag] bracket notation — NEVER emit a real XML tool tag as part of an explanation.
|
|
60
|
+
14. NEVER use markdown formatting in plain text responses — no bold, no headings, no bullet points. Only use fenced code blocks when showing actual code.
|
|
61
|
+
15. When scaffolding multiple NEW files, emit ONE write-file per response and wait for the result before writing the next file.
|
|
62
|
+
16. When you identify a bug or error, ALWAYS propose the fix immediately using changes. Never describe the fix without proposing it.
|
|
63
|
+
17. NEVER use shell for filesystem inspection or searching — always use grep, read-file, or read-folder instead.
|
|
64
|
+
18. changes patches must include the COMPLETE new file content for each path — never partial content.
|
|
52
65
|
|
|
53
66
|
## ADDON FORMAT
|
|
54
67
|
|
package/src/types/chat.ts
CHANGED
|
@@ -33,7 +33,8 @@ export type Message =
|
|
|
33
33
|
| "delete-folder"
|
|
34
34
|
| "open-url"
|
|
35
35
|
| "generate-pdf"
|
|
36
|
-
| "search"
|
|
36
|
+
| "search"
|
|
37
|
+
| "changes";
|
|
37
38
|
content: string;
|
|
38
39
|
result: string;
|
|
39
40
|
approved: boolean;
|
|
@@ -43,6 +44,7 @@ export type Message =
|
|
|
43
44
|
type: "plan";
|
|
44
45
|
content: string;
|
|
45
46
|
patches: FilePatch[];
|
|
47
|
+
diffLines?: DiffLine[][];
|
|
46
48
|
applied: boolean;
|
|
47
49
|
};
|
|
48
50
|
|
package/src/utils/chat.ts
CHANGED
|
@@ -19,6 +19,11 @@ import { FEW_SHOT_MESSAGES } from "../prompts";
|
|
|
19
19
|
import { registry } from "../utils/tools/registry";
|
|
20
20
|
import type { FilePatch } from "../components/repo/DiffViewer";
|
|
21
21
|
|
|
22
|
+
export type ChatResult = {
|
|
23
|
+
text: string;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
22
27
|
export type ParsedResponse =
|
|
23
28
|
| { kind: "text"; content: string; remainder?: string }
|
|
24
29
|
| {
|
|
@@ -190,12 +195,22 @@ export function toCloneUrl(url: string): string {
|
|
|
190
195
|
return clean.endsWith(".git") ? clean : `${clean}.git`;
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
// Replace buildApiMessages in src/utils/chat.ts
|
|
199
|
+
|
|
193
200
|
function buildApiMessages(
|
|
194
201
|
messages: Message[],
|
|
195
202
|
): { role: string; content: string }[] {
|
|
196
203
|
const recent = messages.slice(-MAX_HISTORY);
|
|
197
204
|
|
|
198
|
-
|
|
205
|
+
const writtenFiles: string[] = [];
|
|
206
|
+
for (const m of recent) {
|
|
207
|
+
if (m.type === "tool" && m.toolName === "write-file" && m.approved) {
|
|
208
|
+
const pathMatch = m.content.match(/^(.+?)\s*\(/);
|
|
209
|
+
if (pathMatch?.[1]) writtenFiles.push(pathMatch[1]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return recent.map((m, idx) => {
|
|
199
214
|
if (m.type === "tool") {
|
|
200
215
|
if (!m.approved) {
|
|
201
216
|
return {
|
|
@@ -204,11 +219,39 @@ function buildApiMessages(
|
|
|
204
219
|
"The tool call was denied by the user. Please respond without using that tool.",
|
|
205
220
|
};
|
|
206
221
|
}
|
|
222
|
+
|
|
223
|
+
if (m.toolName === "write-file") {
|
|
224
|
+
const remaining = writtenFiles.slice(
|
|
225
|
+
writtenFiles.indexOf(m.content.split(" (")[0]!) + 1,
|
|
226
|
+
);
|
|
227
|
+
const doneList = writtenFiles
|
|
228
|
+
.slice(0, writtenFiles.indexOf(m.content.split(" (")[0]!) + 1)
|
|
229
|
+
.map((f) => `- ${f}`)
|
|
230
|
+
.join("\n");
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
role: "user",
|
|
234
|
+
content: [
|
|
235
|
+
`Tool result for write-file (${m.content}):`,
|
|
236
|
+
"",
|
|
237
|
+
m.result,
|
|
238
|
+
"",
|
|
239
|
+
`Files written so far:`,
|
|
240
|
+
doneList || `- ${m.content.split(" (")[0]}`,
|
|
241
|
+
"",
|
|
242
|
+
remaining.length > 0
|
|
243
|
+
? `Continue with the NEXT file. Do NOT rewrite any file already written above.`
|
|
244
|
+
: `All files written. Summarize what was created.`,
|
|
245
|
+
].join("\n"),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
207
249
|
return {
|
|
208
250
|
role: "user",
|
|
209
|
-
content: `
|
|
251
|
+
content: `Tool result for ${m.toolName} (${m.content}):\n\n${m.result}\n\nPlease continue your response based on this output.`,
|
|
210
252
|
};
|
|
211
253
|
}
|
|
254
|
+
|
|
212
255
|
return { role: m.role, content: m.content };
|
|
213
256
|
});
|
|
214
257
|
}
|
|
@@ -219,7 +262,7 @@ export async function callChat(
|
|
|
219
262
|
messages: Message[],
|
|
220
263
|
abortSignal?: AbortSignal,
|
|
221
264
|
retries = 2,
|
|
222
|
-
): Promise<
|
|
265
|
+
): Promise<ChatResult> {
|
|
223
266
|
const apiMessages = [
|
|
224
267
|
...buildFewShotMessages(),
|
|
225
268
|
...buildApiMessages(messages),
|
|
@@ -257,7 +300,7 @@ export async function callChat(
|
|
|
257
300
|
}
|
|
258
301
|
|
|
259
302
|
const controller = new AbortController();
|
|
260
|
-
const timer = setTimeout(() => controller.abort(),
|
|
303
|
+
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
261
304
|
abortSignal?.addEventListener("abort", () => controller.abort());
|
|
262
305
|
|
|
263
306
|
try {
|
|
@@ -288,17 +331,39 @@ export async function callChat(
|
|
|
288
331
|
|
|
289
332
|
if (provider.type === "anthropic") {
|
|
290
333
|
const content = data.content as { type: string; text: string }[];
|
|
291
|
-
|
|
334
|
+
const text = content
|
|
292
335
|
.filter((b) => b.type === "text")
|
|
293
336
|
.map((b) => b.text)
|
|
294
337
|
.join("");
|
|
338
|
+
const truncated = (data as any).stop_reason === "max_tokens";
|
|
339
|
+
return { text, truncated };
|
|
295
340
|
} else {
|
|
296
|
-
const choices = data.choices as {
|
|
297
|
-
|
|
341
|
+
const choices = data.choices as {
|
|
342
|
+
message: { content: string };
|
|
343
|
+
finish_reason?: string;
|
|
344
|
+
}[];
|
|
345
|
+
const text = choices[0]?.message.content ?? "";
|
|
346
|
+
const truncated = choices[0]?.finish_reason === "length";
|
|
347
|
+
return { text, truncated };
|
|
298
348
|
}
|
|
299
349
|
} catch (err) {
|
|
300
350
|
clearTimeout(timer);
|
|
301
|
-
if (err instanceof Error && err.name === "AbortError")
|
|
351
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
352
|
+
if (abortSignal?.aborted) throw err;
|
|
353
|
+
if (retries > 0) {
|
|
354
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
355
|
+
return callChat(
|
|
356
|
+
provider,
|
|
357
|
+
systemPrompt,
|
|
358
|
+
messages,
|
|
359
|
+
abortSignal,
|
|
360
|
+
retries - 1,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
throw new Error(
|
|
364
|
+
"Request timed out. The model took too long to respond (>3 min).",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
302
367
|
if (retries > 0) {
|
|
303
368
|
await new Promise((r) => setTimeout(r, 1500));
|
|
304
369
|
return callChat(
|
|
@@ -1,48 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Classifies user message intent to scope which tools the LLM is allowed to use.
|
|
3
|
-
*
|
|
4
|
-
* readonly → only read/search/fetch tools exposed (no write, delete, shell)
|
|
5
|
-
* mutating → all tools exposed
|
|
6
|
-
* any → all tools exposed (ambiguous / can't tell)
|
|
7
|
-
*/
|
|
8
1
|
export type Intent = "readonly" | "mutating" | "any";
|
|
9
2
|
|
|
10
3
|
const READONLY_PATTERNS: RegExp[] = [
|
|
11
|
-
// listing / exploring
|
|
12
4
|
/\b(list|ls|dir|show|display|print|dump)\b/i,
|
|
13
5
|
/\bwhat(('?s| is| are| does)\b| files| folder)/i,
|
|
14
6
|
/\b(folder|directory|file) (structure|tree|layout|contents?)\b/i,
|
|
15
7
|
/\bexplore\b/i,
|
|
16
8
|
|
|
17
|
-
// reading / explaining
|
|
18
9
|
/\b(read|open|view|look at|check out|inspect|peek)\b/i,
|
|
19
10
|
/\b(explain|describe|summarize|summarise|tell me about|walk me through)\b/i,
|
|
20
11
|
/\bhow does\b/i,
|
|
21
12
|
/\bwhat('?s| is) (in|inside|this|that|the)\b/i,
|
|
22
13
|
|
|
23
|
-
// searching
|
|
24
14
|
/\b(find|search|grep|locate|where is|where are)\b/i,
|
|
25
15
|
/\b(look for|scan|trace)\b/i,
|
|
26
16
|
|
|
27
|
-
// understanding
|
|
28
17
|
/\bunderstand\b/i,
|
|
29
18
|
/\bshow me (how|what|where|why)\b/i,
|
|
30
19
|
];
|
|
31
20
|
|
|
32
21
|
const MUTATING_PATTERNS: RegExp[] = [
|
|
33
|
-
// writing
|
|
34
22
|
/\b(write|create|make|generate|add|build|scaffold|init|initialize|setup|set up)\b/i,
|
|
35
23
|
/\b(new file|new folder|new component|new page|new route)\b/i,
|
|
36
24
|
|
|
37
|
-
// editing
|
|
38
25
|
/\b(edit|modify|update|change|refactor|rename|move|migrate)\b/i,
|
|
39
26
|
/\b(fix|patch|resolve|correct|debug|repair)\b/i,
|
|
40
27
|
/\b(implement|add .+ to|insert|inject|append|prepend)\b/i,
|
|
41
28
|
|
|
42
|
-
// deleting
|
|
43
29
|
/\b(delete|remove|drop|clean ?up|purge|wipe)\b/i,
|
|
44
30
|
|
|
45
|
-
// running
|
|
46
31
|
/\b(run|execute|install|deploy|build|test|start|launch|compile|lint|format)\b/i,
|
|
47
32
|
];
|
|
48
33
|
|
package/src/utils/memory.ts
CHANGED
|
@@ -15,16 +15,18 @@ export type MemoryEntry = {
|
|
|
15
15
|
detail: string;
|
|
16
16
|
summary: string;
|
|
17
17
|
timestamp: string;
|
|
18
|
+
repoPath?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type Memory = {
|
|
21
22
|
id: string;
|
|
22
23
|
content: string;
|
|
23
24
|
timestamp: string;
|
|
25
|
+
repoPath?: string;
|
|
26
|
+
scope: "repo" | "global";
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export type MemoryFile = {
|
|
27
|
-
entries: MemoryEntry[];
|
|
28
30
|
memories: Memory[];
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -32,17 +34,14 @@ const LENS_DIR = path.join(os.homedir(), ".lens");
|
|
|
32
34
|
const MEMORY_PATH = path.join(LENS_DIR, "memory.json");
|
|
33
35
|
|
|
34
36
|
function loadMemoryFile(): MemoryFile {
|
|
35
|
-
if (!existsSync(MEMORY_PATH)) return {
|
|
37
|
+
if (!existsSync(MEMORY_PATH)) return { memories: [] };
|
|
36
38
|
try {
|
|
37
39
|
const data = JSON.parse(
|
|
38
40
|
readFileSync(MEMORY_PATH, "utf-8"),
|
|
39
41
|
) as Partial<MemoryFile>;
|
|
40
|
-
return {
|
|
41
|
-
entries: data.entries ?? [],
|
|
42
|
-
memories: data.memories ?? [],
|
|
43
|
-
};
|
|
42
|
+
return { memories: data.memories ?? [] };
|
|
44
43
|
} catch {
|
|
45
|
-
return {
|
|
44
|
+
return { memories: [] };
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -51,26 +50,48 @@ function saveMemoryFile(m: MemoryFile): void {
|
|
|
51
50
|
writeFileSync(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// ──
|
|
53
|
+
// ── Session-only action entries (in-memory, never written to disk) ────────────
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const sessionEntries: MemoryEntry[] = [];
|
|
56
|
+
|
|
57
|
+
export function appendMemory(
|
|
58
|
+
entry: Omit<MemoryEntry, "timestamp">,
|
|
59
|
+
repoPath?: string,
|
|
60
|
+
): void {
|
|
61
|
+
sessionEntries.push({
|
|
62
|
+
...entry,
|
|
63
|
+
repoPath,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
if (sessionEntries.length > 200)
|
|
67
|
+
sessionEntries.splice(0, sessionEntries.length - 200);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function buildMemorySummary(repoPath: string): string {
|
|
64
71
|
const m = loadMemoryFile();
|
|
65
|
-
const relevant = m.entries.slice(-50);
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const globalMemories = m.memories.filter((mem) => mem.scope === "global");
|
|
74
|
+
const repoMemories = m.memories.filter(
|
|
75
|
+
(mem) => mem.scope === "repo" && mem.repoPath === repoPath,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const relevant = sessionEntries
|
|
79
|
+
.filter((e) => !e.repoPath || e.repoPath === repoPath)
|
|
80
|
+
.slice(-50);
|
|
68
81
|
|
|
69
82
|
const parts: string[] = [];
|
|
70
83
|
|
|
71
|
-
if (
|
|
84
|
+
if (globalMemories.length > 0) {
|
|
85
|
+
parts.push(
|
|
86
|
+
`## GLOBAL MEMORIES (apply to all repos)\n\n${globalMemories
|
|
87
|
+
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
88
|
+
.join("\n")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (repoMemories.length > 0) {
|
|
72
93
|
parts.push(
|
|
73
|
-
`## MEMORIES ABOUT THIS REPO\n\n${
|
|
94
|
+
`## MEMORIES ABOUT THIS REPO\n\n${repoMemories
|
|
74
95
|
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
75
96
|
.join("\n")}`,
|
|
76
97
|
);
|
|
@@ -82,7 +103,7 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
82
103
|
return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
|
|
83
104
|
});
|
|
84
105
|
parts.push(
|
|
85
|
-
`## WHAT YOU HAVE ALREADY DONE
|
|
106
|
+
`## WHAT YOU HAVE ALREADY DONE THIS SESSION\n\nThe following actions have already been completed. Do NOT repeat them unless the user explicitly asks:\n\n${lines.join("\n")}`,
|
|
86
107
|
);
|
|
87
108
|
}
|
|
88
109
|
|
|
@@ -90,27 +111,42 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
export function getRepoMemory(repoPath: string): MemoryEntry[] {
|
|
93
|
-
return
|
|
114
|
+
return sessionEntries.filter((e) => !e.repoPath || e.repoPath === repoPath);
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
export function clearRepoMemory(repoPath: string): void {
|
|
118
|
+
// clear session entries for this repo
|
|
119
|
+
const toRemove = sessionEntries
|
|
120
|
+
.map((e, i) => (e.repoPath === repoPath ? i : -1))
|
|
121
|
+
.filter((i) => i >= 0)
|
|
122
|
+
.reverse();
|
|
123
|
+
for (const i of toRemove) sessionEntries.splice(i, 1);
|
|
124
|
+
|
|
125
|
+
// clear persisted memories for this repo (keep global)
|
|
97
126
|
const m = loadMemoryFile();
|
|
98
|
-
m.
|
|
99
|
-
|
|
127
|
+
m.memories = m.memories.filter(
|
|
128
|
+
(mem) => mem.scope === "global" || mem.repoPath !== repoPath,
|
|
129
|
+
);
|
|
100
130
|
saveMemoryFile(m);
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
// ── User/model memories ───────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
133
|
function generateId(): string {
|
|
106
134
|
return Math.random().toString(36).slice(2, 8);
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
export function addMemory(content: string, repoPath: string): Memory {
|
|
110
138
|
const m = loadMemoryFile();
|
|
139
|
+
|
|
140
|
+
const isGlobal = content.startsWith("[global]");
|
|
141
|
+
const cleanContent = isGlobal
|
|
142
|
+
? content.replace("[global]", "").trim()
|
|
143
|
+
: content;
|
|
144
|
+
|
|
111
145
|
const memory: Memory = {
|
|
112
146
|
id: generateId(),
|
|
113
|
-
content,
|
|
147
|
+
content: cleanContent,
|
|
148
|
+
repoPath: isGlobal ? undefined : repoPath,
|
|
149
|
+
scope: isGlobal ? "global" : "repo",
|
|
114
150
|
timestamp: new Date().toISOString(),
|
|
115
151
|
};
|
|
116
152
|
m.memories.push(memory);
|
|
@@ -121,12 +157,53 @@ export function addMemory(content: string, repoPath: string): Memory {
|
|
|
121
157
|
export function deleteMemory(id: string, repoPath: string): boolean {
|
|
122
158
|
const m = loadMemoryFile();
|
|
123
159
|
const before = m.memories.length;
|
|
124
|
-
m.memories = m.memories.filter((mem) =>
|
|
160
|
+
m.memories = m.memories.filter((mem) => mem.id !== id);
|
|
125
161
|
if (m.memories.length === before) return false;
|
|
126
162
|
saveMemoryFile(m);
|
|
127
163
|
return true;
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
export function listMemories(repoPath: string): Memory[] {
|
|
131
|
-
return loadMemoryFile().memories
|
|
167
|
+
return loadMemoryFile().memories.filter(
|
|
168
|
+
(mem) => mem.scope === "global" || mem.repoPath === repoPath,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type SessionToolLog = {
|
|
173
|
+
toolName: string;
|
|
174
|
+
input: string;
|
|
175
|
+
resultPreview: string;
|
|
176
|
+
timestamp: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const sessionToolLog: SessionToolLog[] = [];
|
|
180
|
+
|
|
181
|
+
export function logToolCall(
|
|
182
|
+
toolName: string,
|
|
183
|
+
input: string,
|
|
184
|
+
result: string,
|
|
185
|
+
repoPath?: string,
|
|
186
|
+
): void {
|
|
187
|
+
sessionToolLog.push({
|
|
188
|
+
toolName,
|
|
189
|
+
input,
|
|
190
|
+
resultPreview: result.slice(0, 120),
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
if (sessionToolLog.length > 100)
|
|
194
|
+
sessionToolLog.splice(0, sessionToolLog.length - 100);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getSessionToolSummary(repoPath: string): string {
|
|
198
|
+
if (sessionToolLog.length === 0) return "";
|
|
199
|
+
const recent = sessionToolLog.slice(-30);
|
|
200
|
+
const lines = recent.map((e) => {
|
|
201
|
+
const input = e.input.length > 60 ? e.input.slice(0, 60) + "…" : e.input;
|
|
202
|
+
return `- ${e.toolName}: ${input}`;
|
|
203
|
+
});
|
|
204
|
+
return `## TOOLS ALREADY USED THIS SESSION\n\nDo NOT call these again unless the user explicitly asks:\n\n${lines.join("\n")}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function clearSessionLog(): void {
|
|
208
|
+
sessionToolLog.splice(0, sessionToolLog.length);
|
|
132
209
|
}
|