@cryptiklemur/lattice 0.0.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.
Files changed (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,380 @@
1
+ import { readdirSync, readFileSync, existsSync, lstatSync, realpathSync, statSync, rmSync } from "node:fs";
2
+ import { join, sep, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { execSync } from "node:child_process";
5
+ import type { ClientMessage } from "@lattice/shared";
6
+ import type { SkillInfo } from "@lattice/shared";
7
+ import { registerHandler } from "../ws/router";
8
+ import { sendTo } from "../ws/broadcast";
9
+ import { loadConfig } from "../config";
10
+ import { readGlobalMcpServers, readGlobalSkills } from "../project/project-files";
11
+
12
+ var searchCache = new Map<string, { skills: Array<{ id: string; skillId: string; name: string; source: string; installs: number }>; count: number; time: number }>();
13
+
14
+ var skillsCache: SkillInfo[] | null = null;
15
+ var lastScanTime: number = 0;
16
+ var CACHE_TTL_MS = 60000;
17
+
18
+ function parseFrontmatter(content: string): { name: string; description: string } {
19
+ var match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
20
+ if (!match) return { name: "", description: "" };
21
+ var yaml = match[1];
22
+ var name = "";
23
+ var desc = "";
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) desc = descMatch[1].trim().replace(/^["']|["']$/g, "");
31
+ }
32
+ return { name, description: desc };
33
+ }
34
+
35
+ function findSkillFiles(rootDir: string): string[] {
36
+ if (!existsSync(rootDir)) return [];
37
+ try {
38
+ var output = execSync(
39
+ "find " + JSON.stringify(rootDir) + " -name SKILL.md -type f 2>/dev/null",
40
+ { encoding: "utf-8", timeout: 5000 }
41
+ );
42
+ return output.trim().split("\n").filter(function (l) { return l.length > 0; });
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ function parseCommandFile(filePath: string, fileName: string): SkillInfo | null {
49
+ try {
50
+ var content = readFileSync(filePath, "utf-8");
51
+ var name = fileName.replace(/\.md$/, "");
52
+ var desc = "";
53
+ var lines = content.split(/\r?\n/);
54
+ for (var i = 0; i < Math.min(lines.length, 10); i++) {
55
+ var line = lines[i].trim();
56
+ if (line.length > 0 && !line.startsWith("#") && !line.startsWith("---")) {
57
+ desc = line.slice(0, 120);
58
+ break;
59
+ }
60
+ }
61
+ return { name: name, description: desc || "Command: " + name, path: filePath };
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function scanCommandsDir(dirPath: string): SkillInfo[] {
68
+ var results: SkillInfo[] = [];
69
+ if (!existsSync(dirPath)) return results;
70
+ try {
71
+ var entries = readdirSync(dirPath);
72
+ for (var i = 0; i < entries.length; i++) {
73
+ if (!entries[i].endsWith(".md")) continue;
74
+ var filePath = join(dirPath, entries[i]);
75
+ var info = parseCommandFile(filePath, entries[i]);
76
+ if (info) results.push(info);
77
+ }
78
+ } catch {}
79
+ return results;
80
+ }
81
+
82
+ function getSkills(): SkillInfo[] {
83
+ var now = Date.now();
84
+ if (skillsCache && now - lastScanTime < CACHE_TTL_MS) {
85
+ return skillsCache;
86
+ }
87
+
88
+ var home = homedir();
89
+ var skills: SkillInfo[] = [];
90
+
91
+ var skillDirs = [
92
+ join(home, ".claude"),
93
+ join(home, ".agents"),
94
+ join(home, ".superpowers"),
95
+ ];
96
+
97
+ for (var d = 0; d < skillDirs.length; d++) {
98
+ var files = findSkillFiles(skillDirs[d]);
99
+ for (var f = 0; f < files.length; f++) {
100
+ try {
101
+ var content = readFileSync(files[f], "utf-8");
102
+ var meta = parseFrontmatter(content);
103
+ if (meta.name) {
104
+ var skillName = meta.name;
105
+ var filePath = files[f];
106
+
107
+ var pluginMatch = filePath.match(/plugins\/([^/]+)\/skills\/([^/]+)\/SKILL\.md$/);
108
+ if (pluginMatch) {
109
+ var pluginName = pluginMatch[1];
110
+ var rawSkillName = pluginMatch[2];
111
+ if (skillName.indexOf(":") === -1 && skillName === rawSkillName) {
112
+ skillName = pluginName + ":" + skillName;
113
+ }
114
+ }
115
+
116
+ var extPluginMatch = filePath.match(/external_plugins\/([^/]+)\/skills\/([^/]+)\/SKILL\.md$/);
117
+ if (extPluginMatch) {
118
+ var extPluginName = extPluginMatch[1];
119
+ var extSkillName = extPluginMatch[2];
120
+ if (skillName.indexOf(":") === -1 && skillName === extSkillName) {
121
+ skillName = extPluginName + ":" + skillName;
122
+ }
123
+ }
124
+
125
+ var marketplaceSkillMatch = filePath.match(/marketplaces\/([^/]+)\/.claude\/skills\/([^/]+)\/SKILL\.md$/);
126
+ if (marketplaceSkillMatch) {
127
+ var mktName = marketplaceSkillMatch[1];
128
+ var mktSkill = marketplaceSkillMatch[2];
129
+ if (skillName.indexOf(":") === -1 && skillName === mktSkill) {
130
+ skillName = mktName + ":" + skillName;
131
+ }
132
+ }
133
+
134
+ skills.push({
135
+ name: skillName,
136
+ description: meta.description,
137
+ path: filePath,
138
+ });
139
+ }
140
+ } catch {}
141
+ }
142
+ }
143
+
144
+ var config = loadConfig();
145
+ for (var p = 0; p < config.projects.length; p++) {
146
+ var projectPath = config.projects[p].path;
147
+ var projectSkillDirs = [
148
+ join(projectPath, ".claude", "skills"),
149
+ join(projectPath, ".claude", "commands"),
150
+ join(projectPath, ".superpowers", "skills"),
151
+ ];
152
+ for (var pd = 0; pd < projectSkillDirs.length; pd++) {
153
+ if (projectSkillDirs[pd].endsWith("commands")) {
154
+ var cmds = scanCommandsDir(projectSkillDirs[pd]);
155
+ for (var c = 0; c < cmds.length; c++) skills.push(cmds[c]);
156
+ } else {
157
+ var projFiles = findSkillFiles(projectSkillDirs[pd]);
158
+ for (var pf = 0; pf < projFiles.length; pf++) {
159
+ try {
160
+ var projContent = readFileSync(projFiles[pf], "utf-8");
161
+ var projMeta = parseFrontmatter(projContent);
162
+ if (projMeta.name) {
163
+ skills.push({
164
+ name: projMeta.name,
165
+ description: projMeta.description,
166
+ path: projFiles[pf],
167
+ });
168
+ }
169
+ } catch {}
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ var globalCommands = scanCommandsDir(join(home, ".claude", "commands"));
176
+ for (var gc = 0; gc < globalCommands.length; gc++) {
177
+ skills.push(globalCommands[gc]);
178
+ }
179
+
180
+ var namespacedBareNames = new Set<string>();
181
+ for (var n = 0; n < skills.length; n++) {
182
+ var colonIdx = skills[n].name.indexOf(":");
183
+ if (colonIdx !== -1) {
184
+ namespacedBareNames.add(skills[n].name.slice(colonIdx + 1));
185
+ }
186
+ }
187
+
188
+ var filtered: SkillInfo[] = [];
189
+ for (var fi = 0; fi < skills.length; fi++) {
190
+ if (skills[fi].name.indexOf(":") === -1 && namespacedBareNames.has(skills[fi].name)) {
191
+ continue;
192
+ }
193
+ filtered.push(skills[fi]);
194
+ }
195
+
196
+ filtered.sort(function (a, b) { return a.name.localeCompare(b.name); });
197
+
198
+ var seen = new Set<string>();
199
+ var unique: SkillInfo[] = [];
200
+ for (var i = 0; i < filtered.length; i++) {
201
+ if (!seen.has(filtered[i].name)) {
202
+ seen.add(filtered[i].name);
203
+ unique.push(filtered[i]);
204
+ }
205
+ }
206
+
207
+ skillsCache = unique;
208
+ lastScanTime = now;
209
+ return unique;
210
+ }
211
+
212
+ export function resolveSkillContent(skillName: string): string | null {
213
+ var skills = getSkills();
214
+ var match = skills.find(function (s) { return s.name === skillName; });
215
+ if (!match) return null;
216
+ try {
217
+ return readFileSync(match.path, "utf-8");
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ registerHandler("skills", function (clientId: string, message: ClientMessage) {
224
+ if (message.type === "skills:list_request") {
225
+ var skills = getSkills();
226
+ sendTo(clientId, { type: "skills:list", skills: skills });
227
+ return;
228
+ }
229
+
230
+ if (message.type === "skills:search") {
231
+ var searchMsg = message as { type: "skills:search"; query: string };
232
+ var query = searchMsg.query.trim();
233
+ if (!query) {
234
+ sendTo(clientId, { type: "skills:search_results", query: query, skills: [], count: 0 });
235
+ return;
236
+ }
237
+
238
+ var cacheKey = "search:" + query;
239
+ var cached = searchCache.get(cacheKey);
240
+ if (cached && Date.now() - cached.time < 60000) {
241
+ sendTo(clientId, { type: "skills:search_results", query: query, skills: cached.skills, count: cached.count });
242
+ return;
243
+ }
244
+
245
+ void fetch("https://skills.sh/api/search?q=" + encodeURIComponent(query))
246
+ .then(function (res) { return res.json(); })
247
+ .then(function (data: { skills?: Array<{ id: string; skillId: string; name: string; source: string; installs: number }>; count?: number }) {
248
+ var skills = (data.skills ?? []).map(function (s) {
249
+ return { id: s.id, skillId: s.skillId, name: s.name, source: s.source, installs: s.installs };
250
+ });
251
+ var count = data.count ?? skills.length;
252
+ searchCache.set(cacheKey, { skills: skills, count: count, time: Date.now() });
253
+ sendTo(clientId, { type: "skills:search_results", query: query, skills: skills, count: count });
254
+ })
255
+ .catch(function () {
256
+ sendTo(clientId, { type: "skills:search_results", query: query, skills: [], count: 0, error: "Search unavailable" });
257
+ });
258
+ return;
259
+ }
260
+
261
+ if (message.type === "skills:install") {
262
+ var installMsg = message as { type: "skills:install"; source: string; scope: "global" | "project"; projectSlug?: string };
263
+ var cwd = homedir();
264
+ if (installMsg.scope === "project" && installMsg.projectSlug) {
265
+ var installConfig = loadConfig();
266
+ var installProject = installConfig.projects.find(function (p) { return p.slug === installMsg.projectSlug; });
267
+ if (installProject) {
268
+ cwd = installProject.path;
269
+ }
270
+ }
271
+
272
+ try {
273
+ var proc = Bun.spawn(["npx", "skillsadd", installMsg.source], {
274
+ cwd: cwd,
275
+ stdout: "pipe",
276
+ stderr: "pipe",
277
+ });
278
+
279
+ var timeout = setTimeout(function () {
280
+ proc.kill();
281
+ }, 60000);
282
+
283
+ void proc.exited.then(function (code) {
284
+ clearTimeout(timeout);
285
+ skillsCache = null;
286
+ if (code === 0) {
287
+ sendTo(clientId, { type: "skills:install_result", success: true, message: "Installed successfully" });
288
+ } else {
289
+ sendTo(clientId, { type: "skills:install_result", success: false, message: "Install failed (exit code " + code + ")" });
290
+ }
291
+ if (installMsg.scope === "global") {
292
+ var globalConfig = loadConfig();
293
+ sendTo(clientId, {
294
+ type: "settings:data",
295
+ config: globalConfig,
296
+ mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
297
+ globalSkills: readGlobalSkills(),
298
+ });
299
+ }
300
+ });
301
+ } catch (err) {
302
+ sendTo(clientId, { type: "skills:install_result", success: false, message: "Failed to start install: " + String(err) });
303
+ }
304
+ return;
305
+ }
306
+
307
+ if (message.type === "skills:view") {
308
+ var viewMsg = message as { type: "skills:view"; path: string };
309
+ try {
310
+ if (!existsSync(viewMsg.path)) {
311
+ sendTo(clientId, { type: "skills:view_result", path: viewMsg.path, content: "File not found." });
312
+ return;
313
+ }
314
+ var viewContent = readFileSync(viewMsg.path, "utf-8");
315
+ sendTo(clientId, { type: "skills:view_result", path: viewMsg.path, content: viewContent });
316
+ } catch {
317
+ sendTo(clientId, { type: "skills:view_result", path: viewMsg.path, content: "Failed to read file." });
318
+ }
319
+ return;
320
+ }
321
+
322
+ if (message.type === "skills:delete") {
323
+ var deleteMsg = message as { type: "skills:delete"; path: string };
324
+ try {
325
+ var skillDir = dirname(deleteMsg.path);
326
+ if (!existsSync(deleteMsg.path)) {
327
+ sendTo(clientId, { type: "skills:delete_result", success: false, message: "Skill not found." });
328
+ return;
329
+ }
330
+ rmSync(skillDir, { recursive: true, force: true });
331
+ skillsCache = null;
332
+ sendTo(clientId, { type: "skills:delete_result", success: true, message: "Skill deleted." });
333
+ var delConfig = loadConfig();
334
+ sendTo(clientId, {
335
+ type: "settings:data",
336
+ config: delConfig,
337
+ mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
338
+ globalSkills: readGlobalSkills(),
339
+ });
340
+ } catch (err) {
341
+ sendTo(clientId, { type: "skills:delete_result", success: false, message: "Failed to delete: " + String(err) });
342
+ }
343
+ return;
344
+ }
345
+
346
+ if (message.type === "skills:update") {
347
+ var updateMsg = message as { type: "skills:update"; source: string };
348
+ try {
349
+ var updateProc = Bun.spawn(["npx", "skillsadd", updateMsg.source], {
350
+ cwd: homedir(),
351
+ stdout: "pipe",
352
+ stderr: "pipe",
353
+ });
354
+
355
+ var updateTimeout = setTimeout(function () {
356
+ updateProc.kill();
357
+ }, 60000);
358
+
359
+ void updateProc.exited.then(function (code) {
360
+ clearTimeout(updateTimeout);
361
+ skillsCache = null;
362
+ if (code === 0) {
363
+ sendTo(clientId, { type: "skills:install_result", success: true, message: "Updated successfully" });
364
+ } else {
365
+ sendTo(clientId, { type: "skills:install_result", success: false, message: "Update failed (exit code " + code + ")" });
366
+ }
367
+ var updConfig = loadConfig();
368
+ sendTo(clientId, {
369
+ type: "settings:data",
370
+ config: updConfig,
371
+ mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
372
+ globalSkills: readGlobalSkills(),
373
+ });
374
+ });
375
+ } catch (err) {
376
+ sendTo(clientId, { type: "skills:install_result", success: false, message: "Failed to start update: " + String(err) });
377
+ }
378
+ return;
379
+ }
380
+ });
@@ -0,0 +1,70 @@
1
+ import type { ClientMessage, TerminalCreateMessage, TerminalInputMessage, TerminalResizeMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo } from "../ws/broadcast";
4
+ import { createTerminal, destroyTerminal, writeToTerminal, resizeTerminal } from "../project/terminal";
5
+ import { getActiveSession } from "./chat";
6
+ import { getProjectBySlug } from "../project/registry";
7
+ import { homedir } from "node:os";
8
+
9
+ var clientTerminals = new Map<string, Set<string>>();
10
+
11
+ function getOrCreateClientSet(clientId: string): Set<string> {
12
+ var set = clientTerminals.get(clientId);
13
+ if (!set) {
14
+ set = new Set<string>();
15
+ clientTerminals.set(clientId, set);
16
+ }
17
+ return set;
18
+ }
19
+
20
+ export function cleanupClientTerminals(clientId: string): void {
21
+ var set = clientTerminals.get(clientId);
22
+ if (set) {
23
+ set.forEach(function(termId) {
24
+ destroyTerminal(termId);
25
+ });
26
+ clientTerminals.delete(clientId);
27
+ }
28
+ }
29
+
30
+ registerHandler("terminal", function(clientId: string, message: ClientMessage) {
31
+ if (message.type === "terminal:create") {
32
+ var _msg = message as TerminalCreateMessage;
33
+ var cwd = homedir();
34
+
35
+ var active = getActiveSession(clientId);
36
+ if (active) {
37
+ var project = getProjectBySlug(active.projectSlug);
38
+ if (project) {
39
+ cwd = project.path;
40
+ }
41
+ }
42
+
43
+ var termId = createTerminal(
44
+ cwd,
45
+ function(data: string) {
46
+ sendTo(clientId, { type: "terminal:output", termId: termId, data: data });
47
+ },
48
+ function(code: number) {
49
+ sendTo(clientId, { type: "terminal:exited", termId: termId, code: code });
50
+ getOrCreateClientSet(clientId).delete(termId);
51
+ },
52
+ );
53
+
54
+ getOrCreateClientSet(clientId).add(termId);
55
+ sendTo(clientId, { type: "terminal:created", termId: termId });
56
+ return;
57
+ }
58
+
59
+ if (message.type === "terminal:input") {
60
+ var inputMsg = message as TerminalInputMessage;
61
+ writeToTerminal(inputMsg.termId, inputMsg.data);
62
+ return;
63
+ }
64
+
65
+ if (message.type === "terminal:resize") {
66
+ var resizeMsg = message as TerminalResizeMessage;
67
+ resizeTerminal(resizeMsg.termId, resizeMsg.cols, resizeMsg.rows);
68
+ return;
69
+ }
70
+ });
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { getLatticeHome } from "./config";
5
+
6
+ interface NodeIdentity {
7
+ id: string;
8
+ createdAt: number;
9
+ }
10
+
11
+ export function getIdentityPath(): string {
12
+ return join(getLatticeHome(), "identity.json");
13
+ }
14
+
15
+ export function loadOrCreateIdentity(): NodeIdentity {
16
+ var path = getIdentityPath();
17
+ if (existsSync(path)) {
18
+ return JSON.parse(readFileSync(path, "utf-8")) as NodeIdentity;
19
+ }
20
+ var identity: NodeIdentity = {
21
+ id: randomUUID(),
22
+ createdAt: Date.now(),
23
+ };
24
+ writeFileSync(path, JSON.stringify(identity, null, 2), "utf-8");
25
+ return identity;
26
+ }
@@ -0,0 +1,190 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { DAEMON_PID_FILE } from "@lattice/shared";
4
+ import { getLatticeHome, loadConfig } from "./config";
5
+
6
+ var args = process.argv.slice(2);
7
+ var command = "start";
8
+ var portOverride: number | null = null;
9
+
10
+ for (var i = 0; i < args.length; i++) {
11
+ if (args[i] === "--port" && i + 1 < args.length) {
12
+ portOverride = parseInt(args[i + 1], 10);
13
+ i++;
14
+ } else if (args[i].startsWith("--port=")) {
15
+ portOverride = parseInt(args[i].split("=")[1], 10);
16
+ } else if (!args[i].startsWith("-")) {
17
+ command = args[i];
18
+ }
19
+ }
20
+
21
+ function getPidPath(): string {
22
+ return join(getLatticeHome(), DAEMON_PID_FILE);
23
+ }
24
+
25
+ function readPid(): number | null {
26
+ var pidPath = getPidPath();
27
+ if (!existsSync(pidPath)) {
28
+ return null;
29
+ }
30
+ try {
31
+ var raw = readFileSync(pidPath, "utf-8").trim();
32
+ var pid = parseInt(raw, 10);
33
+ return isNaN(pid) ? null : pid;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function writePid(pid: number): void {
40
+ writeFileSync(getPidPath(), String(pid), "utf-8");
41
+ }
42
+
43
+ function removePid(): void {
44
+ var pidPath = getPidPath();
45
+ if (existsSync(pidPath)) {
46
+ try {
47
+ unlinkSync(pidPath);
48
+ } catch {
49
+ // ignore
50
+ }
51
+ }
52
+ }
53
+
54
+ function isDaemonRunning(pid: number): boolean {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ switch (command) {
64
+ case "daemon":
65
+ await runDaemon();
66
+ break;
67
+ case "start":
68
+ await runStart();
69
+ break;
70
+ case "stop":
71
+ runStop();
72
+ break;
73
+ case "status":
74
+ runStatus();
75
+ break;
76
+ default:
77
+ console.log("[lattice] Unknown command: " + command);
78
+ console.log("[lattice] Usage: lattice [start|stop|status|daemon]");
79
+ process.exit(1);
80
+ }
81
+
82
+ async function runDaemon(): Promise<void> {
83
+ var { startDaemon } = await import("./daemon");
84
+ writePid(process.pid);
85
+ process.on("SIGTERM", function () {
86
+ removePid();
87
+ process.exit(0);
88
+ });
89
+ process.on("SIGINT", function () {
90
+ removePid();
91
+ process.exit(0);
92
+ });
93
+ await startDaemon(portOverride);
94
+ }
95
+
96
+ async function runStart(): Promise<void> {
97
+ var pid = readPid();
98
+ if (pid !== null && isDaemonRunning(pid)) {
99
+ console.log("[lattice] Daemon is already running (PID " + pid + ")");
100
+ var config = loadConfig();
101
+ var url = (config.tls ? "https" : "http") + "://localhost:" + config.port;
102
+ openBrowser(url);
103
+ return;
104
+ }
105
+
106
+ removePid();
107
+
108
+ var scriptPath = import.meta.path;
109
+ var logPath = join(getLatticeHome(), "daemon.log");
110
+
111
+ var child = Bun.spawn(["bun", scriptPath, "daemon"], {
112
+ detached: true,
113
+ stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)],
114
+ });
115
+
116
+ child.unref();
117
+
118
+ var childPid = child.pid;
119
+ writePid(childPid);
120
+ console.log("[lattice] Daemon started (PID " + childPid + ")");
121
+ console.log("[lattice] Logs: " + logPath);
122
+
123
+ await new Promise<void>(function (resolve) {
124
+ setTimeout(resolve, 800);
125
+ });
126
+
127
+ var config = loadConfig();
128
+ var url = (config.tls ? "https" : "http") + "://localhost:" + config.port;
129
+ console.log("[lattice] Opening " + url);
130
+ openBrowser(url);
131
+ }
132
+
133
+ function runStop(): void {
134
+ var pid = readPid();
135
+ if (pid === null) {
136
+ console.log("[lattice] No PID file found. Daemon may not be running.");
137
+ process.exit(1);
138
+ }
139
+
140
+ if (!isDaemonRunning(pid)) {
141
+ console.log("[lattice] Daemon is not running (stale PID " + pid + ")");
142
+ removePid();
143
+ process.exit(0);
144
+ }
145
+
146
+ try {
147
+ process.kill(pid, "SIGTERM");
148
+ removePid();
149
+ console.log("[lattice] Daemon stopped (PID " + pid + ")");
150
+ } catch (err) {
151
+ console.error("[lattice] Failed to stop daemon:", err);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ function runStatus(): void {
157
+ var pid = readPid();
158
+ if (pid === null) {
159
+ console.log("[lattice] Status: not running (no PID file)");
160
+ process.exit(0);
161
+ }
162
+
163
+ if (!isDaemonRunning(pid)) {
164
+ console.log("[lattice] Status: not running (stale PID " + pid + ")");
165
+ removePid();
166
+ process.exit(0);
167
+ }
168
+
169
+ var config = loadConfig();
170
+ var url = (config.tls ? "https" : "http") + "://localhost:" + config.port;
171
+ console.log("[lattice] Status: running");
172
+ console.log("[lattice] PID: " + pid);
173
+ console.log("[lattice] Port: " + config.port);
174
+ console.log("[lattice] URL: " + url);
175
+ }
176
+
177
+ function openBrowser(url: string): void {
178
+ var platform = process.platform;
179
+ try {
180
+ if (platform === "win32") {
181
+ Bun.spawn(["cmd", "/c", "start", url], { detached: true, stdio: ["ignore", "ignore", "ignore"] }).unref();
182
+ } else if (platform === "darwin") {
183
+ Bun.spawn(["open", url], { detached: true, stdio: ["ignore", "ignore", "ignore"] }).unref();
184
+ } else {
185
+ Bun.spawn(["xdg-open", url], { detached: true, stdio: ["ignore", "ignore", "ignore"] }).unref();
186
+ }
187
+ } catch {
188
+ console.log("[lattice] Could not open browser. Visit: " + url);
189
+ }
190
+ }