@ridit/lens 0.1.9 → 0.2.1

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.
@@ -0,0 +1,121 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync,
7
+ unlinkSync,
8
+ } from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import type { Message } from "../types/chat";
12
+
13
+ const LENS_DIR = path.join(os.homedir(), ".lens");
14
+ const CHATS_DIR = path.join(LENS_DIR, "chats");
15
+
16
+ export type SavedChat = {
17
+ name: string;
18
+ repoPath: string;
19
+ messages: Message[];
20
+ savedAt: string;
21
+ userMessageCount: number;
22
+ };
23
+
24
+ function ensureChatsDir(): void {
25
+ if (!existsSync(CHATS_DIR)) mkdirSync(CHATS_DIR, { recursive: true });
26
+ }
27
+
28
+ function chatFilePath(name: string): string {
29
+ const safe = name.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
30
+ return path.join(CHATS_DIR, `${safe}.json`);
31
+ }
32
+
33
+ export function saveChat(
34
+ name: string,
35
+ repoPath: string,
36
+ messages: Message[],
37
+ ): void {
38
+ ensureChatsDir();
39
+ const data: SavedChat = {
40
+ name,
41
+ repoPath,
42
+ messages,
43
+ savedAt: new Date().toISOString(),
44
+ userMessageCount: messages.filter((m) => m.role === "user").length,
45
+ };
46
+ writeFileSync(chatFilePath(name), JSON.stringify(data, null, 2), "utf-8");
47
+ }
48
+
49
+ export function loadChat(name: string): SavedChat | null {
50
+ const filePath = chatFilePath(name);
51
+ if (!existsSync(filePath)) return null;
52
+ try {
53
+ return JSON.parse(readFileSync(filePath, "utf-8")) as SavedChat;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function listChats(repoPath?: string): SavedChat[] {
60
+ ensureChatsDir();
61
+ const files = readdirSync(CHATS_DIR).filter((f) => f.endsWith(".json"));
62
+ const chats: SavedChat[] = [];
63
+ for (const file of files) {
64
+ try {
65
+ const data = JSON.parse(
66
+ readFileSync(path.join(CHATS_DIR, file), "utf-8"),
67
+ ) as SavedChat;
68
+ if (!repoPath || data.repoPath === repoPath) chats.push(data);
69
+ } catch {
70
+ // skip corrupt files
71
+ }
72
+ }
73
+ return chats.sort(
74
+ (a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime(),
75
+ );
76
+ }
77
+
78
+ export function deleteChat(name: string): boolean {
79
+ const filePath = chatFilePath(name);
80
+ if (!existsSync(filePath)) return false;
81
+ try {
82
+ unlinkSync(filePath);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ export function getChatNameSuggestions(messages: Message[]): string[] {
90
+ const userMsgs = messages
91
+ .filter((m) => m.role === "user")
92
+ .map((m) => m.content.toLowerCase().trim());
93
+
94
+ const date = new Date().toISOString().slice(0, 10);
95
+
96
+ if (userMsgs.length === 0) {
97
+ return [`chat-${date}`, `session-${date}`, `new-chat`];
98
+ }
99
+
100
+ const suggestions: string[] = [];
101
+
102
+ const toSlug = (s: string) =>
103
+ s
104
+ .replace(/[^a-z0-9\s]/g, "")
105
+ .split(/\s+/)
106
+ .filter(Boolean)
107
+ .slice(0, 4)
108
+ .join("-");
109
+
110
+ const firstSlug = toSlug(userMsgs[0]!);
111
+ if (firstSlug) suggestions.push(firstSlug);
112
+
113
+ if (userMsgs.length > 1) {
114
+ const lastSlug = toSlug(userMsgs[userMsgs.length - 1]!);
115
+ if (lastSlug && lastSlug !== firstSlug) suggestions.push(lastSlug);
116
+ }
117
+
118
+ suggestions.push(`session-${date}`);
119
+
120
+ return suggestions.slice(0, 3);
121
+ }
@@ -71,6 +71,7 @@ export function readImportantFiles(
71
71
  repoPath: string,
72
72
  files: string[],
73
73
  ): ImportantFile[] {
74
+ if (files.length > 100) return [];
74
75
  return files.filter(isImportantFile).flatMap((filePath) => {
75
76
  try {
76
77
  const content = readFileSync(path.join(repoPath, filePath), "utf-8");
@@ -0,0 +1,137 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export type MemoryEntryKind =
6
+ | "file-written"
7
+ | "file-read"
8
+ | "url-fetched"
9
+ | "shell-run"
10
+ | "code-applied"
11
+ | "code-skipped";
12
+
13
+ export type MemoryEntry = {
14
+ kind: MemoryEntryKind;
15
+ detail: string;
16
+ summary: string;
17
+ timestamp: string;
18
+ repoPath: string;
19
+ };
20
+
21
+ export type Memory = {
22
+ id: string;
23
+ content: string;
24
+ timestamp: string;
25
+ repoPath: string;
26
+ };
27
+
28
+ export type MemoryFile = {
29
+ entries: MemoryEntry[];
30
+ memories: Memory[];
31
+ };
32
+
33
+ const LENS_DIR = path.join(os.homedir(), ".lens");
34
+ const MEMORY_PATH = path.join(LENS_DIR, "memory.json");
35
+
36
+ function loadMemoryFile(): MemoryFile {
37
+ if (!existsSync(MEMORY_PATH)) return { entries: [], memories: [] };
38
+ try {
39
+ const data = JSON.parse(
40
+ readFileSync(MEMORY_PATH, "utf-8"),
41
+ ) as Partial<MemoryFile>;
42
+ return {
43
+ entries: data.entries ?? [],
44
+ memories: data.memories ?? [],
45
+ };
46
+ } catch {
47
+ return { entries: [], memories: [] };
48
+ }
49
+ }
50
+
51
+ function saveMemoryFile(m: MemoryFile): void {
52
+ if (!existsSync(LENS_DIR)) mkdirSync(LENS_DIR, { recursive: true });
53
+ writeFileSync(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
54
+ }
55
+
56
+ // ── Action entries (what the model has done) ──────────────────────────────────
57
+
58
+ export function appendMemory(entry: Omit<MemoryEntry, "timestamp">): void {
59
+ const m = loadMemoryFile();
60
+ m.entries.push({ ...entry, timestamp: new Date().toISOString() });
61
+ if (m.entries.length > 500) m.entries = m.entries.slice(-500);
62
+ saveMemoryFile(m);
63
+ }
64
+
65
+ export function buildMemorySummary(repoPath: string): string {
66
+ const m = loadMemoryFile();
67
+ const relevant = m.entries.filter((e) => e.repoPath === repoPath).slice(-50);
68
+
69
+ const memories = m.memories.filter((mem) => mem.repoPath === repoPath);
70
+
71
+ const parts: string[] = [];
72
+
73
+ if (memories.length > 0) {
74
+ parts.push(
75
+ `## MEMORIES ABOUT THIS REPO\n\n${memories
76
+ .map((mem) => `- [${mem.id}] ${mem.content}`)
77
+ .join("\n")}`,
78
+ );
79
+ }
80
+
81
+ if (relevant.length > 0) {
82
+ const lines = relevant.map((e) => {
83
+ const ts = new Date(e.timestamp).toLocaleString();
84
+ return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
85
+ });
86
+ parts.push(
87
+ `## 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")}`,
88
+ );
89
+ }
90
+
91
+ return parts.join("\n\n");
92
+ }
93
+
94
+ export function getRepoMemory(repoPath: string): MemoryEntry[] {
95
+ return loadMemoryFile().entries.filter((e) => e.repoPath === repoPath);
96
+ }
97
+
98
+ export function clearRepoMemory(repoPath: string): void {
99
+ const m = loadMemoryFile();
100
+ m.entries = m.entries.filter((e) => e.repoPath !== repoPath);
101
+ m.memories = m.memories.filter((mem) => mem.repoPath !== repoPath);
102
+ saveMemoryFile(m);
103
+ }
104
+
105
+ // ── User/model memories ───────────────────────────────────────────────────────
106
+
107
+ function generateId(): string {
108
+ return Math.random().toString(36).slice(2, 8);
109
+ }
110
+
111
+ export function addMemory(content: string, repoPath: string): Memory {
112
+ const m = loadMemoryFile();
113
+ const memory: Memory = {
114
+ id: generateId(),
115
+ content,
116
+ timestamp: new Date().toISOString(),
117
+ repoPath,
118
+ };
119
+ m.memories.push(memory);
120
+ saveMemoryFile(m);
121
+ return memory;
122
+ }
123
+
124
+ export function deleteMemory(id: string, repoPath: string): boolean {
125
+ const m = loadMemoryFile();
126
+ const before = m.memories.length;
127
+ m.memories = m.memories.filter(
128
+ (mem) => !(mem.id === id && mem.repoPath === repoPath),
129
+ );
130
+ if (m.memories.length === before) return false;
131
+ saveMemoryFile(m);
132
+ return true;
133
+ }
134
+
135
+ export function listMemories(repoPath: string): Memory[] {
136
+ return loadMemoryFile().memories.filter((mem) => mem.repoPath === repoPath);
137
+ }
@@ -1,86 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import path from "path";
3
- import os from "os";
4
-
5
- export type HistoryEntryKind =
6
- | "file-written"
7
- | "file-read"
8
- | "url-fetched"
9
- | "shell-run"
10
- | "code-applied"
11
- | "code-skipped";
12
-
13
- export type HistoryEntry = {
14
- kind: HistoryEntryKind;
15
- detail: string;
16
- summary: string;
17
- timestamp: string;
18
- repoPath: string;
19
- };
20
-
21
- export type HistoryFile = {
22
- entries: HistoryEntry[];
23
- };
24
-
25
- const LENS_DIR = path.join(os.homedir(), ".lens");
26
- const HISTORY_PATH = path.join(LENS_DIR, "history.json");
27
-
28
- function loadHistory(): HistoryFile {
29
- if (!existsSync(HISTORY_PATH)) return { entries: [] };
30
- try {
31
- return JSON.parse(readFileSync(HISTORY_PATH, "utf-8")) as HistoryFile;
32
- } catch {
33
- return { entries: [] };
34
- }
35
- }
36
-
37
- function saveHistory(h: HistoryFile): void {
38
- if (!existsSync(LENS_DIR)) mkdirSync(LENS_DIR, { recursive: true });
39
- writeFileSync(HISTORY_PATH, JSON.stringify(h, null, 2), "utf-8");
40
- }
41
-
42
- export function appendHistory(entry: Omit<HistoryEntry, "timestamp">): void {
43
- const h = loadHistory();
44
- h.entries.push({ ...entry, timestamp: new Date().toISOString() });
45
-
46
- if (h.entries.length > 500) h.entries = h.entries.slice(-500);
47
- saveHistory(h);
48
- }
49
-
50
- /**
51
- * Returns a compact summary string to inject into the system prompt.
52
- * Only includes entries for the current repo, most recent 50.
53
- */
54
- export function buildHistorySummary(repoPath: string): string {
55
- const h = loadHistory();
56
- const relevant = h.entries.filter((e) => e.repoPath === repoPath).slice(-50);
57
-
58
- if (relevant.length === 0) return "";
59
-
60
- const lines = relevant.map((e) => {
61
- const ts = new Date(e.timestamp).toLocaleString();
62
- return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
63
- });
64
-
65
- return `## WHAT YOU HAVE ALREADY DONE IN THIS REPO
66
-
67
- The following actions have already been completed. Do NOT repeat them unless the user explicitly asks you to redo something:
68
-
69
- ${lines.join("\n")}`;
70
- }
71
-
72
- /**
73
- * Returns all entries for a repo, for display purposes.
74
- */
75
- export function getRepoHistory(repoPath: string): HistoryEntry[] {
76
- return loadHistory().entries.filter((e) => e.repoPath === repoPath);
77
- }
78
-
79
- /**
80
- * Clears all history for a repo.
81
- */
82
- export function clearRepoHistory(repoPath: string): void {
83
- const h = loadHistory();
84
- h.entries = h.entries.filter((e) => e.repoPath !== repoPath);
85
- saveHistory(h);
86
- }