@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,281 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { getLatticeHome } from "../config";
5
+ import { broadcast } from "../ws/broadcast";
6
+ import type { ScheduledTask } from "@lattice/shared";
7
+
8
+ var schedulesFile = "";
9
+ var tasks: ScheduledTask[] = [];
10
+ var timerId: ReturnType<typeof setInterval> | null = null;
11
+ var lastTriggeredMinute: Record<string, boolean> = {};
12
+
13
+ var CHECK_INTERVAL = 30 * 1000;
14
+
15
+ function getSchedulesPath(): string {
16
+ if (!schedulesFile) {
17
+ schedulesFile = join(getLatticeHome(), "schedules.json");
18
+ }
19
+ return schedulesFile;
20
+ }
21
+
22
+ function parseCronField(field: string, min: number, max: number): number[] {
23
+ var values: number[] = [];
24
+ var parts = field.split(",");
25
+
26
+ for (var i = 0; i < parts.length; i++) {
27
+ var part = parts[i].trim();
28
+
29
+ if (part.indexOf("/") !== -1) {
30
+ var slashParts = part.split("/");
31
+ var step = parseInt(slashParts[1], 10);
32
+ var rangeStr = slashParts[0];
33
+ var rangeMin = min;
34
+ var rangeMax = max;
35
+ if (rangeStr !== "*") {
36
+ var rp = rangeStr.split("-");
37
+ rangeMin = parseInt(rp[0], 10);
38
+ rangeMax = rp.length > 1 ? parseInt(rp[1], 10) : rangeMin;
39
+ }
40
+ for (var v = rangeMin; v <= rangeMax; v += step) {
41
+ values.push(v);
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (part === "*") {
47
+ for (var v = min; v <= max; v++) {
48
+ values.push(v);
49
+ }
50
+ continue;
51
+ }
52
+
53
+ if (part.indexOf("-") !== -1) {
54
+ var rangeParts = part.split("-");
55
+ var from = parseInt(rangeParts[0], 10);
56
+ var to = parseInt(rangeParts[1], 10);
57
+ for (var v = from; v <= to; v++) {
58
+ values.push(v);
59
+ }
60
+ continue;
61
+ }
62
+
63
+ values.push(parseInt(part, 10));
64
+ }
65
+
66
+ return values;
67
+ }
68
+
69
+ interface ParsedCron {
70
+ minutes: number[];
71
+ hours: number[];
72
+ daysOfMonth: number[];
73
+ months: number[];
74
+ daysOfWeek: number[];
75
+ }
76
+
77
+ function parseCron(expr: string): ParsedCron | null {
78
+ var fields = expr.trim().split(/\s+/);
79
+ if (fields.length !== 5) {
80
+ return null;
81
+ }
82
+ return {
83
+ minutes: parseCronField(fields[0], 0, 59),
84
+ hours: parseCronField(fields[1], 0, 23),
85
+ daysOfMonth: parseCronField(fields[2], 1, 31),
86
+ months: parseCronField(fields[3], 1, 12),
87
+ daysOfWeek: parseCronField(fields[4], 0, 6),
88
+ };
89
+ }
90
+
91
+ function cronMatches(parsed: ParsedCron, date: Date): boolean {
92
+ return (
93
+ parsed.minutes.indexOf(date.getMinutes()) !== -1 &&
94
+ parsed.hours.indexOf(date.getHours()) !== -1 &&
95
+ parsed.daysOfMonth.indexOf(date.getDate()) !== -1 &&
96
+ parsed.months.indexOf(date.getMonth() + 1) !== -1 &&
97
+ parsed.daysOfWeek.indexOf(date.getDay()) !== -1
98
+ );
99
+ }
100
+
101
+ function nextRunTime(cronExpr: string, after?: number): number | null {
102
+ var parsed = parseCron(cronExpr);
103
+ if (!parsed) {
104
+ return null;
105
+ }
106
+
107
+ var d = new Date(after || Date.now());
108
+ d.setSeconds(0, 0);
109
+ d.setMinutes(d.getMinutes() + 1);
110
+
111
+ var limit = 366 * 24 * 60;
112
+ for (var i = 0; i < limit; i++) {
113
+ if (cronMatches(parsed, d)) {
114
+ return d.getTime();
115
+ }
116
+ d.setMinutes(d.getMinutes() + 1);
117
+ }
118
+ return null;
119
+ }
120
+
121
+ export function loadSchedules(): void {
122
+ var path = getSchedulesPath();
123
+ if (!existsSync(path)) {
124
+ tasks = [];
125
+ return;
126
+ }
127
+ try {
128
+ var raw = readFileSync(path, "utf-8");
129
+ var parsed = JSON.parse(raw) as { tasks?: ScheduledTask[] };
130
+ tasks = parsed.tasks || [];
131
+ for (var i = 0; i < tasks.length; i++) {
132
+ var task = tasks[i];
133
+ if (task.enabled && task.cron) {
134
+ task.nextRunAt = nextRunTime(task.cron);
135
+ }
136
+ }
137
+ } catch (err) {
138
+ console.error("[scheduler] Failed to load schedules:", err);
139
+ tasks = [];
140
+ }
141
+ }
142
+
143
+ function saveSchedules(): void {
144
+ var path = getSchedulesPath();
145
+ var dir = join(path, "..");
146
+ if (!existsSync(dir)) {
147
+ mkdirSync(dir, { recursive: true });
148
+ }
149
+ var tmp = path + ".tmp";
150
+ try {
151
+ writeFileSync(tmp, JSON.stringify({ tasks }, null, 2));
152
+ renameSync(tmp, path);
153
+ } catch (err) {
154
+ console.error("[scheduler] Failed to save schedules:", err);
155
+ }
156
+ }
157
+
158
+ function tick(): void {
159
+ var now = Date.now();
160
+ var nowMinuteKey = Math.floor(now / 60000);
161
+
162
+ for (var i = 0; i < tasks.length; i++) {
163
+ var task = tasks[i];
164
+ if (!task.enabled || !task.nextRunAt) {
165
+ continue;
166
+ }
167
+ if (task.nextRunAt > now) {
168
+ continue;
169
+ }
170
+
171
+ var triggerKey = task.id + "_" + nowMinuteKey;
172
+ if (lastTriggeredMinute[triggerKey]) {
173
+ continue;
174
+ }
175
+ lastTriggeredMinute[triggerKey] = true;
176
+
177
+ var keys = Object.keys(lastTriggeredMinute);
178
+ for (var k = 0; k < keys.length; k++) {
179
+ var keyParts = keys[k].split("_");
180
+ var keyMinute = parseInt(keyParts[keyParts.length - 1], 10);
181
+ if (keyMinute < nowMinuteKey - 1) {
182
+ delete lastTriggeredMinute[keys[k]];
183
+ }
184
+ }
185
+
186
+ task.lastRunAt = now;
187
+ task.nextRunAt = nextRunTime(task.cron, now);
188
+ task.updatedAt = now;
189
+ saveSchedules();
190
+
191
+ console.log("[scheduler] Triggering task:", task.name, "(", task.id, ")");
192
+ broadcast({ type: "scheduler:tasks", tasks: tasks.slice() });
193
+ }
194
+ }
195
+
196
+ export function startScheduler(): void {
197
+ loadSchedules();
198
+ if (timerId) {
199
+ return;
200
+ }
201
+ timerId = setInterval(function () {
202
+ tick();
203
+ }, CHECK_INTERVAL);
204
+ tick();
205
+ }
206
+
207
+ export function stopScheduler(): void {
208
+ if (timerId) {
209
+ clearInterval(timerId);
210
+ timerId = null;
211
+ }
212
+ }
213
+
214
+ export function listTasks(): ScheduledTask[] {
215
+ return tasks.slice();
216
+ }
217
+
218
+ export function createTask(data: {
219
+ name: string;
220
+ prompt: string;
221
+ cron: string;
222
+ projectSlug: string;
223
+ }): ScheduledTask | null {
224
+ var parsed = parseCron(data.cron);
225
+ if (!parsed) {
226
+ return null;
227
+ }
228
+
229
+ var now = Date.now();
230
+ var task: ScheduledTask = {
231
+ id: "task_" + now + "_" + randomBytes(3).toString("hex"),
232
+ name: data.name,
233
+ prompt: data.prompt,
234
+ cron: data.cron,
235
+ enabled: true,
236
+ projectSlug: data.projectSlug,
237
+ createdAt: now,
238
+ updatedAt: now,
239
+ lastRunAt: null,
240
+ nextRunAt: nextRunTime(data.cron),
241
+ };
242
+
243
+ tasks.push(task);
244
+ saveSchedules();
245
+ return task;
246
+ }
247
+
248
+ export function deleteTask(taskId: string): boolean {
249
+ var idx = -1;
250
+ for (var i = 0; i < tasks.length; i++) {
251
+ if (tasks[i].id === taskId) {
252
+ idx = i;
253
+ break;
254
+ }
255
+ }
256
+ if (idx === -1) {
257
+ return false;
258
+ }
259
+ tasks.splice(idx, 1);
260
+ saveSchedules();
261
+ return true;
262
+ }
263
+
264
+ export function toggleTask(taskId: string): ScheduledTask | null {
265
+ var task: ScheduledTask | null = null;
266
+ for (var i = 0; i < tasks.length; i++) {
267
+ if (tasks[i].id === taskId) {
268
+ task = tasks[i];
269
+ break;
270
+ }
271
+ }
272
+ if (!task) {
273
+ return null;
274
+ }
275
+
276
+ task.enabled = !task.enabled;
277
+ task.updatedAt = Date.now();
278
+ task.nextRunAt = task.enabled ? nextRunTime(task.cron) : null;
279
+ saveSchedules();
280
+ return task;
281
+ }
@@ -0,0 +1,102 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { getLatticeHome } from "../config";
5
+ import type { StickyNote } from "@lattice/shared";
6
+
7
+ var notesFile = "";
8
+ var notes: StickyNote[] = [];
9
+
10
+ function getNotesPath(): string {
11
+ if (!notesFile) {
12
+ notesFile = join(getLatticeHome(), "notes.jsonl");
13
+ }
14
+ return notesFile;
15
+ }
16
+
17
+ export function loadNotes(): void {
18
+ var path = getNotesPath();
19
+ if (!existsSync(path)) {
20
+ notes = [];
21
+ return;
22
+ }
23
+ try {
24
+ var raw = readFileSync(path, "utf-8");
25
+ var lines = raw.trim().split("\n");
26
+ notes = [];
27
+ for (var i = 0; i < lines.length; i++) {
28
+ var line = lines[i].trim();
29
+ if (!line) {
30
+ continue;
31
+ }
32
+ try {
33
+ var note = JSON.parse(line) as StickyNote;
34
+ notes.push(note);
35
+ } catch {
36
+ // skip malformed line
37
+ }
38
+ }
39
+ } catch (err) {
40
+ console.error("[sticky-notes] Failed to load notes:", err);
41
+ notes = [];
42
+ }
43
+ }
44
+
45
+ function saveNotes(): void {
46
+ var path = getNotesPath();
47
+ var dir = join(path, "..");
48
+ if (!existsSync(dir)) {
49
+ mkdirSync(dir, { recursive: true });
50
+ }
51
+ var tmp = path + ".tmp";
52
+ try {
53
+ var lines: string[] = [];
54
+ for (var i = 0; i < notes.length; i++) {
55
+ lines.push(JSON.stringify(notes[i]));
56
+ }
57
+ writeFileSync(tmp, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
58
+ renameSync(tmp, path);
59
+ } catch (err) {
60
+ console.error("[sticky-notes] Failed to save notes:", err);
61
+ }
62
+ }
63
+
64
+ export function listNotes(): StickyNote[] {
65
+ return notes.slice();
66
+ }
67
+
68
+ export function createNote(content: string): StickyNote {
69
+ var now = Date.now();
70
+ var note: StickyNote = {
71
+ id: "note_" + now + "_" + randomBytes(3).toString("hex"),
72
+ content,
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ };
76
+ notes.push(note);
77
+ saveNotes();
78
+ return note;
79
+ }
80
+
81
+ export function updateNote(id: string, content: string): StickyNote | null {
82
+ for (var i = 0; i < notes.length; i++) {
83
+ if (notes[i].id === id) {
84
+ notes[i].content = content;
85
+ notes[i].updatedAt = Date.now();
86
+ saveNotes();
87
+ return notes[i];
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ export function deleteNote(id: string): boolean {
94
+ for (var i = 0; i < notes.length; i++) {
95
+ if (notes[i].id === id) {
96
+ notes.splice(i, 1);
97
+ saveNotes();
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
@@ -0,0 +1,194 @@
1
+ import type { ChatSendMessage, ChatPermissionResponseMessage, ChatSetPermissionModeMessage, ClientMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo } from "../ws/broadcast";
4
+ import { getProjectBySlug } from "../project/registry";
5
+ import { loadConfig } from "../config";
6
+ import { startChatStream, getPendingPermission, deletePendingPermission, addAutoApprovedTool, setSessionPermissionOverride, getActiveStream, buildPermissionRule } from "../project/sdk-bridge";
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ function formatSdkRule(rule: { toolName: string; ruleContent?: string }): string {
11
+ if (!rule.ruleContent) return rule.toolName;
12
+ if (rule.toolName === "Bash") {
13
+ var firstWord = rule.ruleContent.split(/\s+/)[0].replace(/:.*$/, "");
14
+ if (firstWord === "curl" || firstWord === "wget") {
15
+ var urlMatch = rule.ruleContent.match(/https?:\/\/[^\s"']+/);
16
+ if (urlMatch) {
17
+ try {
18
+ var parsed = new URL(urlMatch[0]);
19
+ return rule.toolName + "(" + firstWord + ":" + parsed.hostname + ")";
20
+ } catch {}
21
+ }
22
+ }
23
+ return rule.toolName + "(" + firstWord + ":*)";
24
+ }
25
+ return rule.toolName + "(" + rule.ruleContent + ")";
26
+ }
27
+
28
+ function addProjectAllowRules(projectPath: string, suggestions: Array<{ type: string; rules?: Array<{ toolName: string; ruleContent?: string }>; directories?: string[]; behavior?: string }> | undefined, fallbackToolName: string, fallbackInput: Record<string, unknown>): void {
29
+ var claudeDir = join(projectPath, ".claude");
30
+ var settingsPath = join(claudeDir, "settings.json");
31
+
32
+ var settings: Record<string, unknown> = {};
33
+ if (existsSync(settingsPath)) {
34
+ try {
35
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
36
+ } catch {
37
+ settings = {};
38
+ }
39
+ }
40
+
41
+ if (!settings.permissions) {
42
+ settings.permissions = {};
43
+ }
44
+ var permissions = settings.permissions as Record<string, unknown>;
45
+ if (!Array.isArray(permissions.allow)) {
46
+ permissions.allow = [];
47
+ }
48
+ if (!Array.isArray(permissions.additionalDirectories)) {
49
+ permissions.additionalDirectories = [];
50
+ }
51
+ var allowList = permissions.allow as string[];
52
+ var additionalDirs = permissions.additionalDirectories as string[];
53
+
54
+ if (suggestions && suggestions.length > 0) {
55
+ for (var si = 0; si < suggestions.length; si++) {
56
+ var suggestion = suggestions[si];
57
+ if (suggestion.type === "addRules" && suggestion.behavior === "allow" && suggestion.rules) {
58
+ for (var ri = 0; ri < suggestion.rules.length; ri++) {
59
+ var rule = formatSdkRule(suggestion.rules[ri]);
60
+ if (!allowList.includes(rule)) {
61
+ allowList.push(rule);
62
+ }
63
+ if (suggestion.rules[ri].ruleContent) {
64
+ var ruleDir = suggestion.rules[ri].ruleContent!.replace(/\/\*\*$/, "").replace(/^\//, "");
65
+ if (ruleDir.startsWith("/") && !additionalDirs.includes(ruleDir)) {
66
+ additionalDirs.push(ruleDir);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ if (suggestion.type === "addDirectories" && suggestion.directories) {
72
+ for (var di = 0; di < suggestion.directories.length; di++) {
73
+ if (!additionalDirs.includes(suggestion.directories[di])) {
74
+ additionalDirs.push(suggestion.directories[di]);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ } else {
80
+ var fallbackRule = buildPermissionRule(fallbackToolName, fallbackInput);
81
+ if (!allowList.includes(fallbackRule)) {
82
+ allowList.push(fallbackRule);
83
+ }
84
+ }
85
+
86
+ if (!existsSync(claudeDir)) {
87
+ mkdirSync(claudeDir, { recursive: true });
88
+ }
89
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
90
+ }
91
+
92
+ var activeSessionByClient = new Map<string, { projectSlug: string; sessionId: string }>();
93
+
94
+ export function setActiveSession(clientId: string, projectSlug: string, sessionId: string): void {
95
+ activeSessionByClient.set(clientId, { projectSlug, sessionId });
96
+ }
97
+
98
+ export function clearActiveSession(clientId: string): void {
99
+ activeSessionByClient.delete(clientId);
100
+ }
101
+
102
+ export function getActiveSession(clientId: string): { projectSlug: string; sessionId: string } | undefined {
103
+ return activeSessionByClient.get(clientId);
104
+ }
105
+
106
+ registerHandler("chat", function (clientId: string, message: ClientMessage) {
107
+ if (message.type === "chat:send") {
108
+ var sendMsg = message as ChatSendMessage;
109
+ var active = activeSessionByClient.get(clientId);
110
+
111
+ if (!active) {
112
+ sendTo(clientId, { type: "chat:error", message: "No active session. Activate a session first." });
113
+ return;
114
+ }
115
+
116
+ var project = getProjectBySlug(active.projectSlug);
117
+ if (!project) {
118
+ sendTo(clientId, { type: "chat:error", message: `Project not found: ${active.projectSlug}` });
119
+ return;
120
+ }
121
+
122
+ var config = loadConfig();
123
+ var env = Object.assign({}, config.globalEnv, project.env);
124
+
125
+ startChatStream({
126
+ projectSlug: active.projectSlug,
127
+ sessionId: active.sessionId,
128
+ text: sendMsg.text,
129
+ clientId,
130
+ cwd: project.path,
131
+ env: Object.keys(env).length > 0 ? env : undefined,
132
+ model: sendMsg.model,
133
+ effort: sendMsg.effort as "low" | "medium" | "high" | "max" | undefined,
134
+ });
135
+
136
+ return;
137
+ }
138
+
139
+ if (message.type === "chat:cancel") {
140
+ sendTo(clientId, { type: "chat:error", message: "Cancel not yet implemented." });
141
+ return;
142
+ }
143
+
144
+ if (message.type === "chat:permission_response") {
145
+ var permMsg = message as ChatPermissionResponseMessage;
146
+ var pending = getPendingPermission(permMsg.requestId);
147
+ if (!pending) {
148
+ return;
149
+ }
150
+
151
+ var active = activeSessionByClient.get(clientId);
152
+
153
+ if (permMsg.allow) {
154
+ if (permMsg.alwaysAllow && permMsg.alwaysAllowScope === "session" && active) {
155
+ addAutoApprovedTool(active.sessionId, pending.toolName);
156
+ }
157
+
158
+ if (permMsg.alwaysAllow && permMsg.alwaysAllowScope === "project" && active) {
159
+ var project = getProjectBySlug(active.projectSlug);
160
+ if (project) {
161
+ addProjectAllowRules(project.path, pending.suggestions as any, pending.toolName, pending.input);
162
+ }
163
+ pending.resolve({ behavior: "allow", updatedInput: pending.input, toolUseID: pending.toolUseID });
164
+ } else {
165
+ pending.resolve({ behavior: "allow", updatedInput: pending.input, toolUseID: pending.toolUseID });
166
+ }
167
+
168
+ var resolvedStatus = permMsg.alwaysAllow ? "always_allowed" : "allowed";
169
+ sendTo(clientId, { type: "chat:permission_resolved", requestId: permMsg.requestId, status: resolvedStatus });
170
+ } else {
171
+ pending.resolve({ behavior: "deny", message: "User denied this operation.", toolUseID: pending.toolUseID });
172
+ sendTo(clientId, { type: "chat:permission_resolved", requestId: permMsg.requestId, status: "denied" });
173
+ }
174
+
175
+ deletePendingPermission(permMsg.requestId);
176
+ return;
177
+ }
178
+
179
+ if (message.type === "chat:set_permission_mode") {
180
+ var modeMsg = message as ChatSetPermissionModeMessage;
181
+ var activeSession = activeSessionByClient.get(clientId);
182
+ if (!activeSession) {
183
+ return;
184
+ }
185
+
186
+ var stream = getActiveStream(activeSession.sessionId);
187
+ if (stream) {
188
+ void stream.setPermissionMode(modeMsg.mode);
189
+ } else {
190
+ setSessionPermissionOverride(activeSession.sessionId, modeMsg.mode);
191
+ }
192
+ return;
193
+ }
194
+ });
@@ -0,0 +1,84 @@
1
+ import type { ClientMessage, FsListMessage, FsReadMessage, FsWriteMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo, broadcast } from "../ws/broadcast";
4
+ import { getProjectBySlug } from "../project/registry";
5
+ import { listDirectory, readFile, writeFile } from "../project/file-browser";
6
+
7
+ var activeProjectByClient = new Map<string, string>();
8
+
9
+ export function setActiveProject(clientId: string, projectSlug: string): void {
10
+ activeProjectByClient.set(clientId, projectSlug);
11
+ }
12
+
13
+ export function clearActiveProject(clientId: string): void {
14
+ activeProjectByClient.delete(clientId);
15
+ }
16
+
17
+ registerHandler("fs", function (clientId: string, message: ClientMessage) {
18
+ if (message.type === "fs:list") {
19
+ var listMsg = message as FsListMessage;
20
+ var projectSlug = activeProjectByClient.get(clientId);
21
+ if (!projectSlug) {
22
+ sendTo(clientId, { type: "chat:error", message: "No active project for fs:list" });
23
+ return;
24
+ }
25
+
26
+ var project = getProjectBySlug(projectSlug);
27
+ if (!project) {
28
+ sendTo(clientId, { type: "chat:error", message: "Project not found: " + projectSlug });
29
+ return;
30
+ }
31
+
32
+ var entries = listDirectory(project.path, listMsg.path);
33
+ sendTo(clientId, { type: "fs:list_result", path: listMsg.path, entries });
34
+ return;
35
+ }
36
+
37
+ if (message.type === "fs:read") {
38
+ var readMsg = message as FsReadMessage;
39
+ var projectSlugRead = activeProjectByClient.get(clientId);
40
+ if (!projectSlugRead) {
41
+ sendTo(clientId, { type: "chat:error", message: "No active project for fs:read" });
42
+ return;
43
+ }
44
+
45
+ var projectRead = getProjectBySlug(projectSlugRead);
46
+ if (!projectRead) {
47
+ sendTo(clientId, { type: "chat:error", message: "Project not found: " + projectSlugRead });
48
+ return;
49
+ }
50
+
51
+ var content = readFile(projectRead.path, readMsg.path);
52
+ if (content === null) {
53
+ sendTo(clientId, { type: "chat:error", message: "Cannot read file: " + readMsg.path });
54
+ return;
55
+ }
56
+
57
+ sendTo(clientId, { type: "fs:read_result", path: readMsg.path, content });
58
+ return;
59
+ }
60
+
61
+ if (message.type === "fs:write") {
62
+ var writeMsg = message as FsWriteMessage;
63
+ var projectSlugWrite = activeProjectByClient.get(clientId);
64
+ if (!projectSlugWrite) {
65
+ sendTo(clientId, { type: "chat:error", message: "No active project for fs:write" });
66
+ return;
67
+ }
68
+
69
+ var projectWrite = getProjectBySlug(projectSlugWrite);
70
+ if (!projectWrite) {
71
+ sendTo(clientId, { type: "chat:error", message: "Project not found: " + projectSlugWrite });
72
+ return;
73
+ }
74
+
75
+ var ok = writeFile(projectWrite.path, writeMsg.path, writeMsg.content);
76
+ if (!ok) {
77
+ sendTo(clientId, { type: "chat:error", message: "Cannot write file: " + writeMsg.path });
78
+ return;
79
+ }
80
+
81
+ broadcast({ type: "fs:changed", path: writeMsg.path });
82
+ return;
83
+ }
84
+ });
@@ -0,0 +1,37 @@
1
+ import type { ClientMessage, LoopStartMessage, LoopStopMessage, LoopStatusRequestMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo } from "../ws/broadcast";
4
+ import { startLoop, stopLoop, getLoopStatus } from "../features/ralph-loop";
5
+
6
+ registerHandler("loop", function (clientId: string, message: ClientMessage) {
7
+ if (message.type === "loop:start") {
8
+ var startMsg = message as LoopStartMessage;
9
+ var loop = startLoop(startMsg.projectSlug);
10
+ if (!loop) {
11
+ sendTo(clientId, { type: "chat:error", message: "No PROMPT.md found in .claude/loops/ for this project" });
12
+ return;
13
+ }
14
+ sendTo(clientId, { type: "loop:started", loop });
15
+ return;
16
+ }
17
+
18
+ if (message.type === "loop:stop") {
19
+ var stopMsg = message as LoopStopMessage;
20
+ var stopped = stopLoop(stopMsg.loopId);
21
+ if (!stopped) {
22
+ sendTo(clientId, { type: "chat:error", message: "Loop not found or not running" });
23
+ }
24
+ return;
25
+ }
26
+
27
+ if (message.type === "loop:status") {
28
+ var statusMsg = message as LoopStatusRequestMessage;
29
+ var status = getLoopStatus(statusMsg.loopId);
30
+ if (!status) {
31
+ sendTo(clientId, { type: "chat:error", message: "Loop not found" });
32
+ return;
33
+ }
34
+ sendTo(clientId, { type: "loop:status_update", loop: status });
35
+ return;
36
+ }
37
+ });