@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,125 @@
1
+ import type { ClientMessage, MeshPairMessage, MeshUnpairMessage, NodeInfo } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo, broadcast } from "../ws/broadcast";
4
+ import { loadConfig } from "../config";
5
+ import { loadOrCreateIdentity } from "../identity";
6
+ import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
7
+ import { addPeer, removePeer, loadPeers } from "../mesh/peers";
8
+ import type { PeerInfo } from "@lattice/shared";
9
+
10
+ export function buildNodesMessage(): NodeInfo[] {
11
+ var peers = loadPeers();
12
+ var config = loadConfig();
13
+ var identity = loadOrCreateIdentity();
14
+
15
+ var local: NodeInfo = {
16
+ id: identity.id,
17
+ name: config.name,
18
+ address: "localhost",
19
+ port: config.port,
20
+ online: true,
21
+ isLocal: true,
22
+ projects: config.projects.map(function (p) {
23
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: identity.id };
24
+ }),
25
+ };
26
+
27
+ var remotes: NodeInfo[] = peers.map(function (peer) {
28
+ return {
29
+ id: peer.id,
30
+ name: peer.name,
31
+ address: peer.addresses[0] ?? "",
32
+ port: 0,
33
+ online: false,
34
+ isLocal: false,
35
+ projects: [],
36
+ };
37
+ });
38
+
39
+ return [local, ...remotes];
40
+ }
41
+
42
+ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
43
+ if (message.type === "mesh:generate_invite") {
44
+ var config = loadConfig();
45
+ generateInviteCode("localhost", config.port).then(function (result) {
46
+ sendTo(clientId, {
47
+ type: "mesh:invite_code",
48
+ code: result.code,
49
+ qrDataUrl: result.qrDataUrl,
50
+ });
51
+ }).catch(function (err) {
52
+ console.error("[lattice] Failed to generate invite code:", err);
53
+ });
54
+ return;
55
+ }
56
+
57
+ if (message.type === "mesh:pair") {
58
+ var pairMsg = message as MeshPairMessage;
59
+ var parsed = parseInviteCode(pairMsg.code);
60
+ if (!parsed) {
61
+ console.warn("[lattice] mesh:pair — invalid invite code");
62
+ return;
63
+ }
64
+ if (!validatePairingToken(parsed.token)) {
65
+ console.warn("[lattice] mesh:pair — invalid or expired token");
66
+ return;
67
+ }
68
+ consumePairingToken(parsed.token);
69
+
70
+ var wsUrl = "ws://" + parsed.address + ":" + parsed.port + "/ws";
71
+ var ws = new WebSocket(wsUrl);
72
+ ws.addEventListener("open", function () {
73
+ var identity = loadOrCreateIdentity();
74
+ ws.send(JSON.stringify({
75
+ type: "mesh:hello",
76
+ nodeId: identity.id,
77
+ name: loadConfig().name,
78
+ projects: [],
79
+ }));
80
+ });
81
+ ws.addEventListener("message", function (event: MessageEvent) {
82
+ try {
83
+ var data = JSON.parse(event.data as string) as { type: string; nodeId?: string; name?: string };
84
+ if (data.type === "mesh:hello" && data.nodeId && data.name) {
85
+ var peer: PeerInfo = {
86
+ id: data.nodeId,
87
+ name: data.name,
88
+ addresses: [parsed!.address],
89
+ publicKey: "",
90
+ pairedAt: Date.now(),
91
+ };
92
+ addPeer(peer);
93
+ ws.close();
94
+
95
+ var nodeInfo: NodeInfo = {
96
+ id: peer.id,
97
+ name: peer.name,
98
+ address: parsed!.address,
99
+ port: parsed!.port,
100
+ online: true,
101
+ isLocal: false,
102
+ projects: [],
103
+ };
104
+ sendTo(clientId, { type: "mesh:paired", node: nodeInfo });
105
+ broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
106
+ }
107
+ } catch {
108
+ console.error("[lattice] mesh:pair — invalid handshake response");
109
+ }
110
+ });
111
+ ws.addEventListener("error", function () {
112
+ console.error("[lattice] mesh:pair — failed to connect to", wsUrl);
113
+ });
114
+ return;
115
+ }
116
+
117
+ if (message.type === "mesh:unpair") {
118
+ var unpairMsg = message as MeshUnpairMessage;
119
+ var removed = removePeer(unpairMsg.nodeId);
120
+ if (removed) {
121
+ broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
122
+ }
123
+ return;
124
+ }
125
+ });
@@ -0,0 +1,45 @@
1
+ import type {
2
+ ClientMessage,
3
+ NotesCreateMessage,
4
+ NotesUpdateMessage,
5
+ NotesDeleteMessage,
6
+ } from "@lattice/shared";
7
+ import { registerHandler } from "../ws/router";
8
+ import { sendTo, broadcast } from "../ws/broadcast";
9
+ import { listNotes, createNote, updateNote, deleteNote } from "../features/sticky-notes";
10
+
11
+ registerHandler("notes", function (clientId: string, message: ClientMessage) {
12
+ if (message.type === "notes:list") {
13
+ sendTo(clientId, { type: "notes:list_result", notes: listNotes() });
14
+ return;
15
+ }
16
+
17
+ if (message.type === "notes:create") {
18
+ var createMsg = message as NotesCreateMessage;
19
+ var note = createNote(createMsg.content);
20
+ broadcast({ type: "notes:created", note });
21
+ return;
22
+ }
23
+
24
+ if (message.type === "notes:update") {
25
+ var updateMsg = message as NotesUpdateMessage;
26
+ var updated = updateNote(updateMsg.id, updateMsg.content);
27
+ if (!updated) {
28
+ sendTo(clientId, { type: "chat:error", message: "Note not found" });
29
+ return;
30
+ }
31
+ broadcast({ type: "notes:updated", note: updated });
32
+ return;
33
+ }
34
+
35
+ if (message.type === "notes:delete") {
36
+ var deleteMsg = message as NotesDeleteMessage;
37
+ var deleted = deleteNote(deleteMsg.id);
38
+ if (!deleted) {
39
+ sendTo(clientId, { type: "chat:error", message: "Note not found" });
40
+ return;
41
+ }
42
+ broadcast({ type: "notes:deleted", id: deleteMsg.id });
43
+ return;
44
+ }
45
+ });
@@ -0,0 +1,174 @@
1
+ import type { ClientMessage, ProjectSettingsGetMessage, ProjectSettingsUpdateMessage, ProjectSettings, McpServerConfig } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo } from "../ws/broadcast";
4
+ import { loadConfig, saveConfig, invalidateConfigCache } from "../config";
5
+ import { getProjectBySlug } from "../project/registry";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import {
10
+ readProjectClaudeMd,
11
+ writeProjectClaudeMd,
12
+ readProjectClaudeSettings,
13
+ mergeProjectClaudeSettings,
14
+ readProjectRules,
15
+ writeProjectRules,
16
+ readProjectMcpServers,
17
+ writeProjectMcpServers,
18
+ readProjectSkills,
19
+ readGlobalRules,
20
+ readGlobalPermissions,
21
+ readGlobalMcpServers,
22
+ readGlobalSkills,
23
+ } from "../project/project-files";
24
+
25
+ function loadGlobalClaudeMd(): string {
26
+ var mdPath = join(homedir(), ".claude", "CLAUDE.md");
27
+ if (existsSync(mdPath)) {
28
+ try {
29
+ return readFileSync(mdPath, "utf-8");
30
+ } catch {}
31
+ }
32
+ return "";
33
+ }
34
+
35
+ function buildProjectSettings(projectSlug: string): ProjectSettings | { error: string } {
36
+ var project = getProjectBySlug(projectSlug);
37
+ if (!project) {
38
+ return { error: "Project not found" };
39
+ }
40
+ if (!existsSync(project.path)) {
41
+ return { error: "Project path does not exist on disk" };
42
+ }
43
+
44
+ var claudeMd = readProjectClaudeMd(project.path);
45
+ var claudeSettings = readProjectClaudeSettings(project.path);
46
+ var lattice = (claudeSettings.lattice ?? {}) as Record<string, unknown>;
47
+ var permissions = (claudeSettings.permissions ?? { allow: [], deny: [] }) as { allow: string[]; deny: string[] };
48
+ var rules = readProjectRules(project.path);
49
+ var mcpServers = readProjectMcpServers(project.path) as Record<string, McpServerConfig>;
50
+ var skills = readProjectSkills(project.path);
51
+
52
+ var config = loadConfig();
53
+
54
+ var settings: ProjectSettings = {
55
+ title: project.title,
56
+ path: project.path,
57
+ icon: (project as Record<string, unknown>).icon as ProjectSettings["icon"],
58
+ claudeMd,
59
+ defaultModel: (lattice.defaultModel as string) || undefined,
60
+ defaultEffort: (lattice.defaultEffort as string) || undefined,
61
+ thinking: lattice.thinking as ProjectSettings["thinking"],
62
+ permissionMode: (lattice.defaultPermissionMode as string) || undefined,
63
+ permissions: {
64
+ allow: Array.isArray(permissions.allow) ? permissions.allow : [],
65
+ deny: Array.isArray(permissions.deny) ? permissions.deny : [],
66
+ },
67
+ env: project.env ?? {},
68
+ mcpServers,
69
+ rules,
70
+ skills,
71
+ global: {
72
+ claudeMd: loadGlobalClaudeMd(),
73
+ defaultModel: "",
74
+ defaultEffort: "",
75
+ env: config.globalEnv ?? {},
76
+ permissions: readGlobalPermissions(),
77
+ rules: readGlobalRules(),
78
+ mcpServers: readGlobalMcpServers() as Record<string, McpServerConfig>,
79
+ skills: readGlobalSkills(),
80
+ },
81
+ };
82
+
83
+ return settings;
84
+ }
85
+
86
+ registerHandler("project-settings", function (clientId: string, message: ClientMessage) {
87
+ if (message.type === "project-settings:get") {
88
+ var getMsg = message as ProjectSettingsGetMessage;
89
+ var result = buildProjectSettings(getMsg.projectSlug);
90
+ if ("error" in result) {
91
+ sendTo(clientId, { type: "project-settings:error", projectSlug: getMsg.projectSlug, message: result.error });
92
+ return;
93
+ }
94
+ sendTo(clientId, { type: "project-settings:data", projectSlug: getMsg.projectSlug, settings: result });
95
+ return;
96
+ }
97
+
98
+ if (message.type === "project-settings:update") {
99
+ var updateMsg = message as ProjectSettingsUpdateMessage;
100
+ var projectSlug = updateMsg.projectSlug;
101
+ var section = updateMsg.section;
102
+ var settings = updateMsg.settings;
103
+
104
+ var project = getProjectBySlug(projectSlug);
105
+ if (!project) {
106
+ sendTo(clientId, { type: "project-settings:error", projectSlug, message: "Project not found" });
107
+ return;
108
+ }
109
+ if (!existsSync(project.path)) {
110
+ sendTo(clientId, { type: "project-settings:error", projectSlug, message: "Project path does not exist on disk" });
111
+ return;
112
+ }
113
+
114
+ try {
115
+ if (section === "general") {
116
+ invalidateConfigCache();
117
+ var config = loadConfig();
118
+ var idx = config.projects.findIndex(function (p) { return p.slug === projectSlug; });
119
+ if (idx !== -1) {
120
+ if (typeof settings.title === "string") {
121
+ config.projects[idx].title = settings.title;
122
+ }
123
+ if (settings.icon !== undefined) {
124
+ config.projects[idx].icon = settings.icon as ProjectSettings["icon"];
125
+ }
126
+ saveConfig(config);
127
+ }
128
+ } else if (section === "claude") {
129
+ if (typeof settings.claudeMd === "string") {
130
+ writeProjectClaudeMd(project.path, settings.claudeMd);
131
+ }
132
+ var latticeUpdates: Record<string, unknown> = {};
133
+ if (settings.defaultModel !== undefined) latticeUpdates.defaultModel = settings.defaultModel;
134
+ if (settings.defaultEffort !== undefined) latticeUpdates.defaultEffort = settings.defaultEffort;
135
+ if (settings.thinking !== undefined) latticeUpdates.thinking = settings.thinking;
136
+ if (settings.defaultPermissionMode !== undefined) latticeUpdates.defaultPermissionMode = settings.defaultPermissionMode;
137
+ if (Object.keys(latticeUpdates).length > 0) {
138
+ mergeProjectClaudeSettings(project.path, { lattice: latticeUpdates });
139
+ }
140
+ } else if (section === "environment") {
141
+ invalidateConfigCache();
142
+ var config = loadConfig();
143
+ var idx = config.projects.findIndex(function (p) { return p.slug === projectSlug; });
144
+ if (idx !== -1) {
145
+ config.projects[idx].env = (settings.env as Record<string, string>) ?? {};
146
+ saveConfig(config);
147
+ }
148
+ } else if (section === "mcp") {
149
+ writeProjectMcpServers(project.path, (settings.mcpServers as Record<string, unknown>) ?? {});
150
+ } else if (section === "rules") {
151
+ writeProjectRules(project.path, (settings.rules as Array<{ filename: string; content: string }>) ?? []);
152
+ } else if (section === "permissions") {
153
+ mergeProjectClaudeSettings(project.path, {
154
+ permissions: {
155
+ allow: (settings.allow as string[]) ?? [],
156
+ deny: (settings.deny as string[]) ?? [],
157
+ },
158
+ });
159
+ }
160
+ } catch (err) {
161
+ var errMsg = err instanceof Error ? err.message : String(err);
162
+ sendTo(clientId, { type: "project-settings:error", projectSlug, message: errMsg });
163
+ return;
164
+ }
165
+
166
+ var updated = buildProjectSettings(projectSlug);
167
+ if ("error" in updated) {
168
+ sendTo(clientId, { type: "project-settings:error", projectSlug, message: updated.error });
169
+ return;
170
+ }
171
+ sendTo(clientId, { type: "project-settings:data", projectSlug, settings: updated });
172
+ return;
173
+ }
174
+ });
@@ -0,0 +1,47 @@
1
+ import type {
2
+ ClientMessage,
3
+ SchedulerCreateMessage,
4
+ SchedulerDeleteMessage,
5
+ SchedulerToggleMessage,
6
+ } from "@lattice/shared";
7
+ import { registerHandler } from "../ws/router";
8
+ import { sendTo } from "../ws/broadcast";
9
+ import { listTasks, createTask, deleteTask, toggleTask } from "../features/scheduler";
10
+
11
+ registerHandler("scheduler", function (clientId: string, message: ClientMessage) {
12
+ if (message.type === "scheduler:list") {
13
+ sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
14
+ return;
15
+ }
16
+
17
+ if (message.type === "scheduler:create") {
18
+ var createMsg = message as SchedulerCreateMessage;
19
+ var task = createTask({
20
+ name: createMsg.name,
21
+ prompt: createMsg.prompt,
22
+ cron: createMsg.cron,
23
+ projectSlug: createMsg.projectSlug,
24
+ });
25
+ if (!task) {
26
+ sendTo(clientId, { type: "chat:error", message: "Invalid cron expression" });
27
+ return;
28
+ }
29
+ sendTo(clientId, { type: "scheduler:task_created", task });
30
+ sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
31
+ return;
32
+ }
33
+
34
+ if (message.type === "scheduler:delete") {
35
+ var deleteMsg = message as SchedulerDeleteMessage;
36
+ deleteTask(deleteMsg.taskId);
37
+ sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
38
+ return;
39
+ }
40
+
41
+ if (message.type === "scheduler:toggle") {
42
+ var toggleMsg = message as SchedulerToggleMessage;
43
+ toggleTask(toggleMsg.taskId);
44
+ sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
45
+ return;
46
+ }
47
+ });
@@ -0,0 +1,159 @@
1
+ import type {
2
+ ClientMessage,
3
+ SessionActivateMessage,
4
+ SessionCreateMessage,
5
+ SessionDeleteMessage,
6
+ SessionListRequestMessage,
7
+ SessionRenameMessage,
8
+ } from "@lattice/shared";
9
+ import { registerHandler } from "../ws/router";
10
+ import { sendTo } from "../ws/broadcast";
11
+ import { loadConfig } from "../config";
12
+ import {
13
+ createSession,
14
+ deleteSession,
15
+ findProjectSlugForSession,
16
+ getSessionTitle,
17
+ getSessionUsage,
18
+ listSessions,
19
+ loadSessionHistory,
20
+ renameSession,
21
+ } from "../project/session";
22
+ import { getContextBreakdown } from "../project/context-breakdown";
23
+ import { setActiveSession } from "./chat";
24
+ import { setActiveProject } from "./fs";
25
+ import { wasSessionInterrupted, clearInterruptedFlag } from "../project/sdk-bridge";
26
+
27
+ registerHandler("session", function (clientId: string, message: ClientMessage) {
28
+ if (message.type === "session:list_request") {
29
+ var listReqMsg = message as SessionListRequestMessage;
30
+ void listSessions(listReqMsg.projectSlug).then(function (sessions) {
31
+ sendTo(clientId, {
32
+ type: "session:list",
33
+ projectSlug: listReqMsg.projectSlug,
34
+ sessions,
35
+ });
36
+ });
37
+ return;
38
+ }
39
+
40
+ if (message.type === "session:list_all_request") {
41
+ var config = loadConfig();
42
+ var allPromises = config.projects.map(function (p) {
43
+ return listSessions(p.slug);
44
+ });
45
+ void Promise.all(allPromises).then(function (results) {
46
+ var merged: typeof results[0] = [];
47
+ for (var i = 0; i < results.length; i++) {
48
+ for (var j = 0; j < results[i].length; j++) {
49
+ merged.push(results[i][j]);
50
+ }
51
+ }
52
+ merged.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
53
+ sendTo(clientId, {
54
+ type: "session:list_all",
55
+ sessions: merged.slice(0, 20),
56
+ });
57
+ });
58
+ return;
59
+ }
60
+
61
+ if (message.type === "session:create") {
62
+ var createMsg = message as SessionCreateMessage;
63
+ var session = createSession(createMsg.projectSlug);
64
+ sendTo(clientId, { type: "session:created", session });
65
+ void listSessions(createMsg.projectSlug).then(function (sessions) {
66
+ sendTo(clientId, {
67
+ type: "session:list",
68
+ projectSlug: createMsg.projectSlug,
69
+ sessions,
70
+ });
71
+ });
72
+ return;
73
+ }
74
+
75
+ if (message.type === "session:activate") {
76
+ var activateMsg = message as SessionActivateMessage;
77
+ setActiveSession(clientId, activateMsg.projectSlug, activateMsg.sessionId);
78
+ setActiveProject(clientId, activateMsg.projectSlug);
79
+ void Promise.all([
80
+ loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId),
81
+ getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId),
82
+ getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId),
83
+ getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId),
84
+ ]).then(function (results) {
85
+ var interrupted = wasSessionInterrupted(activateMsg.sessionId);
86
+ if (interrupted) {
87
+ clearInterruptedFlag(activateMsg.sessionId);
88
+ }
89
+ sendTo(clientId, {
90
+ type: "session:history",
91
+ projectSlug: activateMsg.projectSlug,
92
+ sessionId: activateMsg.sessionId,
93
+ messages: results[0],
94
+ title: results[1],
95
+ interrupted: interrupted || undefined,
96
+ });
97
+ var usage = results[2];
98
+ if (usage) {
99
+ sendTo(clientId, {
100
+ type: "chat:context_usage",
101
+ inputTokens: usage.inputTokens,
102
+ outputTokens: usage.outputTokens,
103
+ cacheReadTokens: usage.cacheReadTokens,
104
+ cacheCreationTokens: usage.cacheCreationTokens,
105
+ contextWindow: usage.contextWindow,
106
+ });
107
+ }
108
+ var breakdown = results[3];
109
+ if (breakdown) {
110
+ sendTo(clientId, {
111
+ type: "chat:context_breakdown",
112
+ segments: breakdown.segments,
113
+ contextWindow: breakdown.contextWindow,
114
+ autocompactAt: breakdown.autocompactAt,
115
+ });
116
+ }
117
+ });
118
+ return;
119
+ }
120
+
121
+ if (message.type === "session:rename") {
122
+ var renameMsg = message as SessionRenameMessage;
123
+ void findProjectSlugForSession(renameMsg.sessionId).then(function (projectSlug) {
124
+ if (!projectSlug) {
125
+ sendTo(clientId, { type: "chat:error", message: "Session not found" });
126
+ return;
127
+ }
128
+ void renameSession(projectSlug, renameMsg.sessionId, renameMsg.title).then(function () {
129
+ void listSessions(projectSlug).then(function (sessions) {
130
+ sendTo(clientId, {
131
+ type: "session:list",
132
+ projectSlug,
133
+ sessions,
134
+ });
135
+ });
136
+ });
137
+ });
138
+ return;
139
+ }
140
+
141
+ if (message.type === "session:delete") {
142
+ var deleteMsg = message as SessionDeleteMessage;
143
+ void findProjectSlugForSession(deleteMsg.sessionId).then(function (deleteProjectSlug) {
144
+ if (!deleteProjectSlug) {
145
+ sendTo(clientId, { type: "chat:error", message: "Session not found" });
146
+ return;
147
+ }
148
+ void deleteSession(deleteProjectSlug, deleteMsg.sessionId).then(function () {
149
+ void listSessions(deleteProjectSlug).then(function (sessions) {
150
+ sendTo(clientId, {
151
+ type: "session:list",
152
+ projectSlug: deleteProjectSlug,
153
+ sessions,
154
+ });
155
+ });
156
+ });
157
+ });
158
+ }
159
+ });
@@ -0,0 +1,109 @@
1
+ import type { ClientMessage, SettingsGetMessage, SettingsUpdateMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo, broadcast } from "../ws/broadcast";
4
+ import { loadConfig, saveConfig } from "../config";
5
+ import { addProject } from "../project/registry";
6
+ import type { LatticeConfig } from "@lattice/shared";
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { readGlobalMcpServers, writeGlobalMcpServers, readGlobalSkills } from "../project/project-files";
11
+ import { loadOrCreateIdentity } from "../identity";
12
+
13
+ function loadGlobalClaudeMd(): string {
14
+ var mdPath = join(homedir(), ".claude", "CLAUDE.md");
15
+ if (existsSync(mdPath)) {
16
+ try {
17
+ return readFileSync(mdPath, "utf-8");
18
+ } catch {}
19
+ }
20
+ return "";
21
+ }
22
+
23
+ function saveGlobalClaudeMd(content: string): void {
24
+ var claudeDir = join(homedir(), ".claude");
25
+ if (!existsSync(claudeDir)) {
26
+ mkdirSync(claudeDir, { recursive: true });
27
+ }
28
+ writeFileSync(join(claudeDir, "CLAUDE.md"), content, "utf-8");
29
+ }
30
+
31
+ registerHandler("settings", function (clientId: string, message: ClientMessage) {
32
+ if (message.type === "settings:get") {
33
+ var config = loadConfig();
34
+ var identity = loadOrCreateIdentity();
35
+ var configWithClaudeMd = { ...config, claudeMd: loadGlobalClaudeMd() };
36
+ sendTo(clientId, {
37
+ type: "settings:data",
38
+ config: configWithClaudeMd,
39
+ mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
40
+ globalSkills: readGlobalSkills(),
41
+ });
42
+ sendTo(clientId, {
43
+ type: "projects:list",
44
+ projects: config.projects.map(function (p) {
45
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: identity.id, nodeName: config.name, isRemote: false };
46
+ }),
47
+ });
48
+ return;
49
+ }
50
+
51
+ if (message.type === "settings:update") {
52
+ var updateMsg = message as SettingsUpdateMessage;
53
+ var current = loadConfig();
54
+
55
+ var incoming = updateMsg.settings as Record<string, unknown>;
56
+ if (typeof incoming.claudeMd === "string") {
57
+ saveGlobalClaudeMd(incoming.claudeMd);
58
+ delete incoming.claudeMd;
59
+ }
60
+
61
+ if (incoming.mcpServers != null) {
62
+ writeGlobalMcpServers(incoming.mcpServers as Record<string, unknown>);
63
+ delete incoming.mcpServers;
64
+ }
65
+
66
+ var incomingProjects = incoming.projects as Array<{ path: string; slug?: string; title: string; env?: Record<string, string> }> | undefined;
67
+ if (incomingProjects && incomingProjects.length > 0) {
68
+ for (var i = 0; i < incomingProjects.length; i++) {
69
+ var proj = incomingProjects[i];
70
+ addProject(proj.path, proj.title);
71
+ }
72
+ }
73
+
74
+ var refreshed = loadConfig();
75
+ var updated: LatticeConfig = {
76
+ ...refreshed,
77
+ ...(incoming as Partial<LatticeConfig>),
78
+ globalEnv: {
79
+ ...refreshed.globalEnv,
80
+ ...((incoming.globalEnv as Record<string, string>) ?? {}),
81
+ },
82
+ projects: refreshed.projects,
83
+ };
84
+ saveConfig(updated);
85
+ var updatedWithClaudeMd = { ...updated, claudeMd: loadGlobalClaudeMd() };
86
+ sendTo(clientId, {
87
+ type: "settings:data",
88
+ config: updatedWithClaudeMd,
89
+ mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
90
+ globalSkills: readGlobalSkills(),
91
+ });
92
+ var updatedIdentity = loadOrCreateIdentity();
93
+ broadcast({
94
+ type: "projects:list",
95
+ projects: updated.projects.map(function (p) {
96
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: updatedIdentity.id, nodeName: updated.name, isRemote: false };
97
+ }),
98
+ });
99
+ return;
100
+ }
101
+
102
+ if (message.type === "settings:restart") {
103
+ console.log("[lattice] Restart requested by client");
104
+ setTimeout(function () {
105
+ process.exit(0);
106
+ }, 200);
107
+ return;
108
+ }
109
+ });