@ridit/lens 0.3.6 → 0.3.7

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/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
  | {
@@ -219,7 +224,7 @@ export async function callChat(
219
224
  messages: Message[],
220
225
  abortSignal?: AbortSignal,
221
226
  retries = 2,
222
- ): Promise<string> {
227
+ ): Promise<ChatResult> {
223
228
  const apiMessages = [
224
229
  ...buildFewShotMessages(),
225
230
  ...buildApiMessages(messages),
@@ -288,13 +293,20 @@ export async function callChat(
288
293
 
289
294
  if (provider.type === "anthropic") {
290
295
  const content = data.content as { type: string; text: string }[];
291
- return content
296
+ const text = content
292
297
  .filter((b) => b.type === "text")
293
298
  .map((b) => b.text)
294
299
  .join("");
300
+ const truncated = (data as any).stop_reason === "max_tokens";
301
+ return { text, truncated };
295
302
  } else {
296
- const choices = data.choices as { message: { content: string } }[];
297
- return choices[0]?.message.content ?? "";
303
+ const choices = data.choices as {
304
+ message: { content: string };
305
+ finish_reason?: string;
306
+ }[];
307
+ const text = choices[0]?.message.content ?? "";
308
+ const truncated = choices[0]?.finish_reason === "length";
309
+ return { text, truncated };
298
310
  }
299
311
  } catch (err) {
300
312
  clearTimeout(timer);
@@ -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
  }
@@ -136,13 +136,39 @@ export const writeFileTool: Tool<WriteFileInput> = {
136
136
  systemPromptEntry: (i) =>
137
137
  `### ${i}. write-file — create or overwrite a file\n<write-file>\n{"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}\n</write-file>`,
138
138
  parseInput: (body) => {
139
+ const tryParse = (s: string) => {
140
+ try {
141
+ const parsed = JSON.parse(s) as { path: string; content: string };
142
+ if (!parsed.path || parsed.content === undefined) return null;
143
+ return { ...parsed, path: parsed.path.replace(/\\/g, "/") };
144
+ } catch {
145
+ return null;
146
+ }
147
+ };
148
+
149
+ const first = tryParse(body.trim());
150
+ if (first) return first;
151
+
139
152
  try {
140
- const parsed = JSON.parse(body) as { path: string; content: string };
141
- if (!parsed.path) return null;
142
- return { ...parsed, path: parsed.path.replace(/\\/g, "/") };
143
- } catch {
144
- return null;
153
+ const sanitized = body
154
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
155
+ .replace(/\n/g, "\\n")
156
+ .replace(/\r/g, "\\r")
157
+ .replace(/\t/g, "\\t");
158
+ const second = tryParse(sanitized);
159
+ if (second) return second;
160
+ } catch {}
161
+
162
+ const pathMatch = body.match(/"path"\s*:\s*"([^"]+)"/);
163
+ const contentMatch = body.match(/"content"\s*:\s*"([\s\S]*)"\s*}?\s*$/);
164
+ if (pathMatch && contentMatch && contentMatch[1] !== undefined) {
165
+ return {
166
+ path: pathMatch[1]!.replace(/\\/g, "/"),
167
+ content: contentMatch[1]!.replace(/\\n/g, "\n").replace(/\\t/g, "\t"),
168
+ };
145
169
  }
170
+
171
+ return null;
146
172
  },
147
173
  summariseInput: ({ path, content }) => `${path} (${content.length} bytes)`,
148
174
  execute: ({ path: filePath, content }, ctx) => ({