@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.
@@ -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((answer) => {
177
+ .then((result) => {
178
178
  const assistantMsg: Message = {
179
179
  role: "assistant",
180
180
  type: "text",
181
- content: answer,
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 raw = await callChat(provider, systemPrompt, history, signal);
780
+ const result = await callChat(provider, systemPrompt, history, signal);
781
781
  if (signal.aborted) return;
782
- processResponse(raw, history, signal);
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
- raw = await callChat(
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
- .action((opts: { path: string }) => {
55
- render(<ChatCommand path={opts.path} />);
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
@@ -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
  ];
@@ -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 for this repo
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 when the user asks you to remember something, or when you learn something project-specific that would be useful in future sessions.
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. NEVER write the same file twice in one session. One write per file, period. If you already wrote it, it is done.
38
- 4. 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.
39
- 5. write-file content must be the COMPLETE file content, never a placeholder or partial.
40
- 6. NEVER read a file you just wrote. The write output confirms success.
41
- 7. NEVER apologize and redo a tool callone attempt is enough, trust the output.
42
- 8. NEVER use shell to run git clone — use the clone tag instead.
43
- 9. When the user asks you to CREATE a new file, write it immediately do NOT read first.
44
- 10. When the user asks you to MODIFY or FIX an existing file, read it first, then write the complete updated version ONCE.
45
- 11. When fixing multiple files, use read-files to read ALL of them first, then write each one ONCE sequentially never rewrite a file already written this session.
46
- 12. If a read-folder or read-file returns not found, accept it and move on do NOT retry the same path.
47
- 13. 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
48
- 14. write-file paths are relative to the repo rootuse full relative paths e.g. myapp/src/index.tsx not src/index.tsx
49
- 15. 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.
50
- 16. NEVER use markdown formatting in plain text responses no bold, no headings, no bullet points. Only use fenced code blocks when showing actual code.
51
- 17. When scaffolding multiple files, emit ONE write-file tag per response and wait for the result before writing the next file.
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 rootuse 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 responsesno 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
- return recent.map((m) => {
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: `Here is the output from the ${m.toolName} of ${m.content}:\n\n${m.result}\n\nPlease continue your response based on this output.`,
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<string> {
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(), 60_000);
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
- return content
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 { message: { content: string } }[];
297
- return choices[0]?.message.content ?? "";
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") throw err;
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
 
@@ -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 { entries: [], memories: [] };
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 { entries: [], memories: [] };
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
- // ── Action entries (what the model has done) ──────────────────────────────────
53
+ // ── Session-only action entries (in-memory, never written to disk) ────────────
55
54
 
56
- export function appendMemory(entry: Omit<MemoryEntry, "timestamp">): void {
57
- const m = loadMemoryFile();
58
- m.entries.push({ ...entry, timestamp: new Date().toISOString() });
59
- if (m.entries.length > 500) m.entries = m.entries.slice(-500);
60
- saveMemoryFile(m);
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 memories = m.memories;
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 (memories.length > 0) {
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${memories
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 IN THIS REPO\n\nThe following actions have already been completed. Do NOT repeat them unless the user explicitly asks you to redo something:\n\n${lines.join("\n")}`,
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 loadMemoryFile().entries;
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.entries = m.entries = [];
99
- m.memories = m.memories = [];
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) => !(mem.id === id));
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
  }