@cryptiklemur/lattice 1.1.0 → 1.3.0

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.
@@ -3,6 +3,10 @@ import { registerHandler } from "../ws/router";
3
3
  import { sendTo, broadcast } from "../ws/broadcast";
4
4
  import { getProjectBySlug } from "../project/registry";
5
5
  import { listDirectory, readFile, writeFile } from "../project/file-browser";
6
+ import { readdirSync, existsSync, readFileSync, statSync } from "node:fs";
7
+ import { join, basename } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { loadConfig } from "../config";
6
10
 
7
11
  var activeProjectByClient = new Map<string, string>();
8
12
 
@@ -82,3 +86,158 @@ registerHandler("fs", function (clientId: string, message: ClientMessage) {
82
86
  return;
83
87
  }
84
88
  });
89
+
90
+ function resolvePath(path: string): string {
91
+ if (!path || path === "~") return homedir();
92
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
93
+ return path;
94
+ }
95
+
96
+ function detectProjectName(dirPath: string): string | null {
97
+ try {
98
+ var pkgPath = join(dirPath, "package.json");
99
+ if (existsSync(pkgPath)) {
100
+ var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
101
+ if (pkg.name) return pkg.name;
102
+ }
103
+ } catch {}
104
+
105
+ try {
106
+ var cargoPath = join(dirPath, "Cargo.toml");
107
+ if (existsSync(cargoPath)) {
108
+ var cargo = readFileSync(cargoPath, "utf-8");
109
+ var cargoMatch = cargo.match(/\[package\][\s\S]*?name\s*=\s*"([^"]+)"/);
110
+ if (cargoMatch) return cargoMatch[1];
111
+ }
112
+ } catch {}
113
+
114
+ try {
115
+ var composerPath = join(dirPath, "composer.json");
116
+ if (existsSync(composerPath)) {
117
+ var composer = JSON.parse(readFileSync(composerPath, "utf-8"));
118
+ if (composer.name) return composer.name;
119
+ }
120
+ } catch {}
121
+
122
+ try {
123
+ var pyprojectPath = join(dirPath, "pyproject.toml");
124
+ if (existsSync(pyprojectPath)) {
125
+ var pyproject = readFileSync(pyprojectPath, "utf-8");
126
+ var pyMatch = pyproject.match(/\[project\][\s\S]*?name\s*=\s*"([^"]+)"/);
127
+ if (pyMatch) return pyMatch[1];
128
+ }
129
+ } catch {}
130
+
131
+ try {
132
+ var goModPath = join(dirPath, "go.mod");
133
+ if (existsSync(goModPath)) {
134
+ var goMod = readFileSync(goModPath, "utf-8");
135
+ var goMatch = goMod.match(/^module\s+(\S+)/m);
136
+ if (goMatch) {
137
+ var parts = goMatch[1].split("/");
138
+ return parts[parts.length - 1];
139
+ }
140
+ }
141
+ } catch {}
142
+
143
+ try {
144
+ var entries = readdirSync(dirPath);
145
+ for (var i = 0; i < entries.length; i++) {
146
+ if (entries[i].endsWith(".sln") || entries[i].endsWith(".csproj")) {
147
+ return entries[i].replace(/\.[^.]+$/, "");
148
+ }
149
+ }
150
+ } catch {}
151
+
152
+ return null;
153
+ }
154
+
155
+ registerHandler("browse", function (clientId: string, message: ClientMessage) {
156
+ if (message.type === "browse:list") {
157
+ var browseMsg = message as { type: "browse:list"; path: string };
158
+ var resolvedPath = resolvePath(browseMsg.path);
159
+ var home = homedir();
160
+
161
+ if (!existsSync(resolvedPath)) {
162
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
163
+ return;
164
+ }
165
+
166
+ try {
167
+ var stat = statSync(resolvedPath);
168
+ if (!stat.isDirectory()) {
169
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
170
+ return;
171
+ }
172
+ } catch {
173
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
174
+ return;
175
+ }
176
+
177
+ try {
178
+ var dirEntries = readdirSync(resolvedPath, { withFileTypes: true });
179
+ var results: Array<{ name: string; path: string; hasClaudeMd: boolean; projectName: string | null }> = [];
180
+
181
+ for (var i = 0; i < dirEntries.length; i++) {
182
+ var entry = dirEntries[i];
183
+ if (!entry.isDirectory()) continue;
184
+
185
+ var entryPath = join(resolvedPath, entry.name);
186
+ var hasClaudeMd = existsSync(join(entryPath, "CLAUDE.md"));
187
+ var projectName = detectProjectName(entryPath);
188
+
189
+ results.push({
190
+ name: entry.name,
191
+ path: entryPath,
192
+ hasClaudeMd: hasClaudeMd,
193
+ projectName: projectName,
194
+ });
195
+ }
196
+
197
+ results.sort(function (a, b) { return a.name.localeCompare(b.name); });
198
+
199
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: results });
200
+ } catch {
201
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
202
+ }
203
+ return;
204
+ }
205
+
206
+ if (message.type === "browse:suggestions") {
207
+ var claudeProjectsDir = join(homedir(), ".claude", "projects");
208
+ var config = loadConfig();
209
+ var existingPaths = new Set(config.projects.map(function (p) { return p.path; }));
210
+ var suggestions: Array<{ path: string; name: string; hasClaudeMd: boolean }> = [];
211
+
212
+ if (existsSync(claudeProjectsDir)) {
213
+ try {
214
+ var hashDirs = readdirSync(claudeProjectsDir);
215
+ for (var i = 0; i < hashDirs.length; i++) {
216
+ var hashDir = hashDirs[i];
217
+ var candidatePath = "/" + hashDir.slice(1).replace(/-/g, "/");
218
+
219
+ if (!existsSync(candidatePath)) continue;
220
+ if (existingPaths.has(candidatePath)) continue;
221
+
222
+ try {
223
+ var stat = statSync(candidatePath);
224
+ if (!stat.isDirectory()) continue;
225
+ } catch { continue; }
226
+
227
+ var hasClaudeMd = existsSync(join(candidatePath, "CLAUDE.md"));
228
+ var name = candidatePath.split("/").pop() || hashDir;
229
+
230
+ suggestions.push({
231
+ path: candidatePath,
232
+ name: name,
233
+ hasClaudeMd: hasClaudeMd,
234
+ });
235
+ }
236
+ } catch {}
237
+ }
238
+
239
+ suggestions.sort(function (a, b) { return a.name.localeCompare(b.name); });
240
+ sendTo(clientId, { type: "browse:suggestions_result", suggestions: suggestions });
241
+ return;
242
+ }
243
+ });
@@ -0,0 +1,179 @@
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ClientMessage } from "@lattice/shared";
5
+ import { registerHandler } from "../ws/router";
6
+ import { sendTo } from "../ws/broadcast";
7
+ import { loadConfig } from "../config";
8
+
9
+ function getMemoryDir(projectSlug: string): string | null {
10
+ var config = loadConfig();
11
+ var project = config.projects.find(function (p) { return p.slug === projectSlug; });
12
+ if (!project) return null;
13
+ var hash = "-" + project.path.replace(/\//g, "-").replace(/^-/, "");
14
+ return join(homedir(), ".claude", "projects", hash, "memory");
15
+ }
16
+
17
+ function parseFrontmatter(content: string): { name: string; description: string; type: string } {
18
+ var match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
19
+ if (!match) return { name: "", description: "", type: "" };
20
+ var yaml = match[1];
21
+ var name = "";
22
+ var description = "";
23
+ var type = "";
24
+ var lines = yaml.split(/\r?\n/);
25
+ for (var i = 0; i < lines.length; i++) {
26
+ var line = lines[i];
27
+ var nameMatch = line.match(/^name:\s*(.+)/);
28
+ if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
29
+ var descMatch = line.match(/^description:\s*(.+)/);
30
+ if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, "");
31
+ var typeMatch = line.match(/^type:\s*(.+)/);
32
+ if (typeMatch) type = typeMatch[1].trim().replace(/^["']|["']$/g, "");
33
+ }
34
+ return { name, description, type };
35
+ }
36
+
37
+ function regenerateIndex(memoryDir: string): void {
38
+ if (!existsSync(memoryDir)) return;
39
+ var files = readdirSync(memoryDir).filter(function (f) {
40
+ return f.endsWith(".md") && f !== "MEMORY.md";
41
+ });
42
+
43
+ var grouped: Record<string, Array<{ filename: string; name: string; description: string }>> = {};
44
+
45
+ for (var i = 0; i < files.length; i++) {
46
+ try {
47
+ var content = readFileSync(join(memoryDir, files[i]), "utf-8");
48
+ var meta = parseFrontmatter(content);
49
+ var type = meta.type || "other";
50
+ if (!grouped[type]) grouped[type] = [];
51
+ grouped[type].push({ filename: files[i], name: meta.name || files[i], description: meta.description || "" });
52
+ } catch {}
53
+ }
54
+
55
+ var lines: string[] = ["# Memory Index", ""];
56
+ var types = Object.keys(grouped).sort();
57
+ for (var t = 0; t < types.length; t++) {
58
+ lines.push("## " + types[t].charAt(0).toUpperCase() + types[t].slice(1));
59
+ var entries = grouped[types[t]];
60
+ for (var e = 0; e < entries.length; e++) {
61
+ var desc = entries[e].description ? " — " + entries[e].description : "";
62
+ lines.push("- [" + entries[e].filename + "](" + entries[e].filename + ")" + desc);
63
+ }
64
+ lines.push("");
65
+ }
66
+
67
+ writeFileSync(join(memoryDir, "MEMORY.md"), lines.join("\n"), "utf-8");
68
+ }
69
+
70
+ registerHandler("memory", function (clientId: string, message: ClientMessage) {
71
+ if (message.type === "memory:list") {
72
+ var listMsg = message as { type: "memory:list"; projectSlug: string };
73
+ var memDir = getMemoryDir(listMsg.projectSlug);
74
+ if (!memDir || !existsSync(memDir)) {
75
+ sendTo(clientId, { type: "memory:list_result", projectSlug: listMsg.projectSlug, memories: [] });
76
+ return;
77
+ }
78
+
79
+ var files = readdirSync(memDir).filter(function (f) {
80
+ return f.endsWith(".md") && f !== "MEMORY.md";
81
+ });
82
+
83
+ var memories: Array<{ filename: string; name: string; description: string; type: string }> = [];
84
+ for (var i = 0; i < files.length; i++) {
85
+ try {
86
+ var content = readFileSync(join(memDir, files[i]), "utf-8");
87
+ var meta = parseFrontmatter(content);
88
+ memories.push({
89
+ filename: files[i],
90
+ name: meta.name || files[i].replace(/\.md$/, ""),
91
+ description: meta.description,
92
+ type: meta.type || "other",
93
+ });
94
+ } catch {}
95
+ }
96
+
97
+ memories.sort(function (a, b) { return a.name.localeCompare(b.name); });
98
+ sendTo(clientId, { type: "memory:list_result", projectSlug: listMsg.projectSlug, memories: memories });
99
+ return;
100
+ }
101
+
102
+ if (message.type === "memory:view") {
103
+ var viewMsg = message as { type: "memory:view"; projectSlug: string; filename: string };
104
+ var viewDir = getMemoryDir(viewMsg.projectSlug);
105
+ if (!viewDir) {
106
+ sendTo(clientId, { type: "memory:view_result", filename: viewMsg.filename, content: "Project not found." });
107
+ return;
108
+ }
109
+ try {
110
+ var viewContent = readFileSync(join(viewDir, viewMsg.filename), "utf-8");
111
+ sendTo(clientId, { type: "memory:view_result", filename: viewMsg.filename, content: viewContent });
112
+ } catch {
113
+ sendTo(clientId, { type: "memory:view_result", filename: viewMsg.filename, content: "File not found." });
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (message.type === "memory:save") {
119
+ var saveMsg = message as { type: "memory:save"; projectSlug: string; filename: string; content: string };
120
+ var saveDir = getMemoryDir(saveMsg.projectSlug);
121
+ if (!saveDir) {
122
+ sendTo(clientId, { type: "memory:save_result", success: false, message: "Project not found." });
123
+ return;
124
+ }
125
+ try {
126
+ mkdirSync(saveDir, { recursive: true });
127
+ writeFileSync(join(saveDir, saveMsg.filename), saveMsg.content, "utf-8");
128
+ regenerateIndex(saveDir);
129
+ sendTo(clientId, { type: "memory:save_result", success: true });
130
+ var updatedFiles = readdirSync(saveDir).filter(function (f) { return f.endsWith(".md") && f !== "MEMORY.md"; });
131
+ var updatedMemories: Array<{ filename: string; name: string; description: string; type: string }> = [];
132
+ for (var j = 0; j < updatedFiles.length; j++) {
133
+ try {
134
+ var c = readFileSync(join(saveDir, updatedFiles[j]), "utf-8");
135
+ var m = parseFrontmatter(c);
136
+ updatedMemories.push({ filename: updatedFiles[j], name: m.name || updatedFiles[j].replace(/\.md$/, ""), description: m.description, type: m.type || "other" });
137
+ } catch {}
138
+ }
139
+ updatedMemories.sort(function (a, b) { return a.name.localeCompare(b.name); });
140
+ sendTo(clientId, { type: "memory:list_result", projectSlug: saveMsg.projectSlug, memories: updatedMemories });
141
+ } catch (err) {
142
+ sendTo(clientId, { type: "memory:save_result", success: false, message: "Failed to save: " + String(err) });
143
+ }
144
+ return;
145
+ }
146
+
147
+ if (message.type === "memory:delete") {
148
+ var delMsg = message as { type: "memory:delete"; projectSlug: string; filename: string };
149
+ var delDir = getMemoryDir(delMsg.projectSlug);
150
+ if (!delDir) {
151
+ sendTo(clientId, { type: "memory:delete_result", success: false, message: "Project not found." });
152
+ return;
153
+ }
154
+ try {
155
+ var filePath = join(delDir, delMsg.filename);
156
+ if (!existsSync(filePath)) {
157
+ sendTo(clientId, { type: "memory:delete_result", success: false, message: "Memory not found." });
158
+ return;
159
+ }
160
+ unlinkSync(filePath);
161
+ regenerateIndex(delDir);
162
+ sendTo(clientId, { type: "memory:delete_result", success: true });
163
+ var remainingFiles = readdirSync(delDir).filter(function (f) { return f.endsWith(".md") && f !== "MEMORY.md"; });
164
+ var remainingMemories: Array<{ filename: string; name: string; description: string; type: string }> = [];
165
+ for (var k = 0; k < remainingFiles.length; k++) {
166
+ try {
167
+ var rc = readFileSync(join(delDir, remainingFiles[k]), "utf-8");
168
+ var rm = parseFrontmatter(rc);
169
+ remainingMemories.push({ filename: remainingFiles[k], name: rm.name || remainingFiles[k].replace(/\.md$/, ""), description: rm.description, type: rm.type || "other" });
170
+ } catch {}
171
+ }
172
+ remainingMemories.sort(function (a, b) { return a.name.localeCompare(b.name); });
173
+ sendTo(clientId, { type: "memory:list_result", projectSlug: delMsg.projectSlug, memories: remainingMemories });
174
+ } catch (err) {
175
+ sendTo(clientId, { type: "memory:delete_result", success: false, message: "Failed to delete: " + String(err) });
176
+ }
177
+ return;
178
+ }
179
+ });
@@ -2,7 +2,7 @@ import type { ClientMessage, SettingsGetMessage, SettingsUpdateMessage } from "@
2
2
  import { registerHandler } from "../ws/router";
3
3
  import { sendTo, broadcast } from "../ws/broadcast";
4
4
  import { loadConfig, saveConfig } from "../config";
5
- import { addProject } from "../project/registry";
5
+ import { addProject, removeProject } from "../project/registry";
6
6
  import type { LatticeConfig } from "@lattice/shared";
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
8
  import { join } from "node:path";
@@ -63,6 +63,11 @@ registerHandler("settings", function (clientId: string, message: ClientMessage)
63
63
  delete incoming.mcpServers;
64
64
  }
65
65
 
66
+ if (typeof incoming.removeProject === "string") {
67
+ removeProject(incoming.removeProject as string);
68
+ delete incoming.removeProject;
69
+ }
70
+
66
71
  var incomingProjects = incoming.projects as Array<{ path: string; slug?: string; title: string; env?: Record<string, string> }> | undefined;
67
72
  if (incomingProjects && incomingProjects.length > 0) {
68
73
  for (var i = 0; i < incomingProjects.length; i++) {
@@ -223,6 +223,39 @@ export interface SkillsUpdateMessage {
223
223
  source: string;
224
224
  }
225
225
 
226
+ export interface BrowseListMessage {
227
+ type: "browse:list";
228
+ path: string;
229
+ }
230
+
231
+ export interface MemoryListMessage {
232
+ type: "memory:list";
233
+ projectSlug: string;
234
+ }
235
+
236
+ export interface MemoryViewMessage {
237
+ type: "memory:view";
238
+ projectSlug: string;
239
+ filename: string;
240
+ }
241
+
242
+ export interface MemorySaveMessage {
243
+ type: "memory:save";
244
+ projectSlug: string;
245
+ filename: string;
246
+ content: string;
247
+ }
248
+
249
+ export interface MemoryDeleteMessage {
250
+ type: "memory:delete";
251
+ projectSlug: string;
252
+ filename: string;
253
+ }
254
+
255
+ export interface BrowseSuggestionsMessage {
256
+ type: "browse:suggestions";
257
+ }
258
+
226
259
  export interface ProjectSettingsGetMessage {
227
260
  type: "project-settings:get";
228
261
  projectSlug: string;
@@ -289,7 +322,13 @@ export type ClientMessage =
289
322
  | SkillsInstallMessage
290
323
  | SkillsViewMessage
291
324
  | SkillsDeleteMessage
292
- | SkillsUpdateMessage;
325
+ | SkillsUpdateMessage
326
+ | BrowseListMessage
327
+ | MemoryListMessage
328
+ | MemoryViewMessage
329
+ | MemorySaveMessage
330
+ | MemoryDeleteMessage
331
+ | BrowseSuggestionsMessage;
293
332
 
294
333
  export interface SessionListMessage {
295
334
  type: "session:list";
@@ -548,6 +587,56 @@ export interface SkillsDeleteResultMessage {
548
587
  message?: string;
549
588
  }
550
589
 
590
+ export interface BrowseListResultMessage {
591
+ type: "browse:list_result";
592
+ path: string;
593
+ homedir: string;
594
+ entries: Array<{
595
+ name: string;
596
+ path: string;
597
+ hasClaudeMd: boolean;
598
+ projectName: string | null;
599
+ }>;
600
+ }
601
+
602
+ export interface MemoryListResultMessage {
603
+ type: "memory:list_result";
604
+ projectSlug: string;
605
+ memories: Array<{
606
+ filename: string;
607
+ name: string;
608
+ description: string;
609
+ type: string;
610
+ }>;
611
+ }
612
+
613
+ export interface MemoryViewResultMessage {
614
+ type: "memory:view_result";
615
+ filename: string;
616
+ content: string;
617
+ }
618
+
619
+ export interface MemorySaveResultMessage {
620
+ type: "memory:save_result";
621
+ success: boolean;
622
+ message?: string;
623
+ }
624
+
625
+ export interface MemoryDeleteResultMessage {
626
+ type: "memory:delete_result";
627
+ success: boolean;
628
+ message?: string;
629
+ }
630
+
631
+ export interface BrowseSuggestionsResultMessage {
632
+ type: "browse:suggestions_result";
633
+ suggestions: Array<{
634
+ path: string;
635
+ name: string;
636
+ hasClaudeMd: boolean;
637
+ }>;
638
+ }
639
+
551
640
  export type ServerMessage =
552
641
  | SessionListMessage
553
642
  | SessionCreatedMessage
@@ -592,7 +681,13 @@ export type ServerMessage =
592
681
  | SkillsSearchResultsMessage
593
682
  | SkillsInstallResultMessage
594
683
  | SkillsViewResultMessage
595
- | SkillsDeleteResultMessage;
684
+ | SkillsDeleteResultMessage
685
+ | BrowseListResultMessage
686
+ | MemoryListResultMessage
687
+ | MemoryViewResultMessage
688
+ | MemorySaveResultMessage
689
+ | MemoryDeleteResultMessage
690
+ | BrowseSuggestionsResultMessage;
596
691
 
597
692
  export interface MeshHelloMessage {
598
693
  type: "mesh:hello";
@@ -15,7 +15,7 @@ export type McpServerConfig =
15
15
  | { type: "sse"; url: string; headers?: Record<string, string> };
16
16
 
17
17
  export type ProjectSettingsSection =
18
- | "general" | "claude" | "environment" | "mcp" | "skills" | "rules" | "permissions";
18
+ | "general" | "claude" | "environment" | "mcp" | "skills" | "rules" | "permissions" | "memory";
19
19
 
20
20
  export interface ProjectSettings {
21
21
  title: string;