@cryptiklemur/lattice 0.0.0 → 1.2.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 (53) hide show
  1. package/.github/workflows/release.yml +4 -4
  2. package/.releaserc.json +2 -1
  3. package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
  4. package/client/src/components/mesh/NodeBadge.tsx +24 -24
  5. package/client/src/components/mesh/PairingDialog.tsx +281 -281
  6. package/client/src/components/panels/FileBrowser.tsx +241 -241
  7. package/client/src/components/panels/StickyNotes.tsx +187 -187
  8. package/client/src/components/project-settings/ProjectMemory.tsx +471 -0
  9. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  10. package/client/src/components/settings/Appearance.tsx +151 -151
  11. package/client/src/components/settings/MeshStatus.tsx +145 -145
  12. package/client/src/components/settings/SettingsView.tsx +57 -57
  13. package/client/src/components/setup/SetupWizard.tsx +750 -750
  14. package/client/src/components/sidebar/AddProjectModal.tsx +432 -0
  15. package/client/src/components/sidebar/ProjectRail.tsx +8 -4
  16. package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
  17. package/client/src/components/ui/ErrorBoundary.tsx +56 -56
  18. package/client/src/hooks/useSidebar.ts +16 -0
  19. package/client/src/router.tsx +453 -391
  20. package/client/src/stores/sidebar.ts +28 -0
  21. package/client/vite.config.ts +20 -20
  22. package/package.json +1 -1
  23. package/server/src/daemon.ts +1 -0
  24. package/server/src/handlers/chat.ts +194 -194
  25. package/server/src/handlers/fs.ts +159 -0
  26. package/server/src/handlers/memory.ts +179 -0
  27. package/server/src/handlers/settings.ts +114 -109
  28. package/shared/src/messages.ts +97 -2
  29. package/shared/src/project-settings.ts +1 -1
  30. package/themes/amoled.json +20 -20
  31. package/themes/ayu-light.json +9 -9
  32. package/themes/catppuccin-latte.json +9 -9
  33. package/themes/catppuccin-mocha.json +9 -9
  34. package/themes/clay-light.json +10 -10
  35. package/themes/clay.json +10 -10
  36. package/themes/dracula.json +9 -9
  37. package/themes/everforest-light.json +9 -9
  38. package/themes/everforest.json +9 -9
  39. package/themes/github-light.json +9 -9
  40. package/themes/gruvbox-dark.json +9 -9
  41. package/themes/gruvbox-light.json +9 -9
  42. package/themes/monokai.json +9 -9
  43. package/themes/nord-light.json +9 -9
  44. package/themes/nord.json +9 -9
  45. package/themes/one-dark.json +9 -9
  46. package/themes/one-light.json +9 -9
  47. package/themes/rose-pine-dawn.json +9 -9
  48. package/themes/rose-pine.json +9 -9
  49. package/themes/solarized-dark.json +9 -9
  50. package/themes/solarized-light.json +9 -9
  51. package/themes/tokyo-night-light.json +9 -9
  52. package/themes/tokyo-night.json +9 -9
  53. package/.serena/project.yml +0 -138
@@ -26,6 +26,8 @@ export interface SidebarState {
26
26
  projectDropdownOpen: boolean;
27
27
  drawerOpen: boolean;
28
28
  nodeSettingsOpen: boolean;
29
+ addProjectOpen: boolean;
30
+ confirmRemoveSlug: string | null;
29
31
  }
30
32
 
31
33
  var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes"];
@@ -70,6 +72,8 @@ var sidebarStore = new Store<SidebarState>({
70
72
  projectDropdownOpen: false,
71
73
  drawerOpen: false,
72
74
  nodeSettingsOpen: false,
75
+ addProjectOpen: false,
76
+ confirmRemoveSlug: null,
73
77
  });
74
78
 
75
79
  function pushUrl(projectSlug: string | null, sessionId: string | null): void {
@@ -334,3 +338,27 @@ export function closeNodeSettings(): void {
334
338
  return { ...state, nodeSettingsOpen: false };
335
339
  });
336
340
  }
341
+
342
+ export function openAddProject(): void {
343
+ sidebarStore.setState(function (state) {
344
+ return { ...state, addProjectOpen: true };
345
+ });
346
+ }
347
+
348
+ export function closeAddProject(): void {
349
+ sidebarStore.setState(function (state) {
350
+ return { ...state, addProjectOpen: false };
351
+ });
352
+ }
353
+
354
+ export function openConfirmRemove(slug: string): void {
355
+ sidebarStore.setState(function (state) {
356
+ return { ...state, confirmRemoveSlug: slug };
357
+ });
358
+ }
359
+
360
+ export function closeConfirmRemove(): void {
361
+ sidebarStore.setState(function (state) {
362
+ return { ...state, confirmRemoveSlug: null };
363
+ });
364
+ }
@@ -1,20 +1,20 @@
1
- import { defineConfig } from "vite";
2
- import react from "@vitejs/plugin-react";
3
- import tailwindcss from "@tailwindcss/vite";
4
-
5
- export default defineConfig({
6
- plugins: [tailwindcss(), react()],
7
- server: {
8
- host: "0.0.0.0",
9
- open: true,
10
- proxy: {
11
- "/ws": {
12
- target: "ws://localhost:7654",
13
- ws: true,
14
- },
15
- "/api": {
16
- target: "http://localhost:7654",
17
- },
18
- },
19
- },
20
- });
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [tailwindcss(), react()],
7
+ server: {
8
+ host: "0.0.0.0",
9
+ open: true,
10
+ proxy: {
11
+ "/ws": {
12
+ target: "ws://localhost:7654",
13
+ ws: true,
14
+ },
15
+ "/api": {
16
+ target: "http://localhost:7654",
17
+ },
18
+ },
19
+ },
20
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "0.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
5
5
  "license": "MIT",
6
6
  "author": "Aaron Scherer <me@aaronscherer.me>",
@@ -24,6 +24,7 @@ import "./handlers/loop";
24
24
  import "./handlers/scheduler";
25
25
  import "./handlers/notes";
26
26
  import "./handlers/skills";
27
+ import "./handlers/memory";
27
28
  import { startScheduler } from "./features/scheduler";
28
29
  import { loadNotes } from "./features/sticky-notes";
29
30
  import { cleanupClientTerminals } from "./handlers/terminal";
@@ -1,194 +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
- });
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
+ });
@@ -3,6 +3,10 @@ import { registerHandler } from "../ws/router";
3
3
  import { sendTo, broadcast } from "../ws/broadcast";
4
4
  import { getProjectBySlug } from "../project/registry";
5
5
  import { listDirectory, readFile, writeFile } from "../project/file-browser";
6
+ import { readdirSync, existsSync, readFileSync, statSync } from "node:fs";
7
+ import { join, basename } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { loadConfig } from "../config";
6
10
 
7
11
  var activeProjectByClient = new Map<string, string>();
8
12
 
@@ -82,3 +86,158 @@ registerHandler("fs", function (clientId: string, message: ClientMessage) {
82
86
  return;
83
87
  }
84
88
  });
89
+
90
+ function resolvePath(path: string): string {
91
+ if (!path || path === "~") return homedir();
92
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
93
+ return path;
94
+ }
95
+
96
+ function detectProjectName(dirPath: string): string | null {
97
+ try {
98
+ var pkgPath = join(dirPath, "package.json");
99
+ if (existsSync(pkgPath)) {
100
+ var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
101
+ if (pkg.name) return pkg.name;
102
+ }
103
+ } catch {}
104
+
105
+ try {
106
+ var cargoPath = join(dirPath, "Cargo.toml");
107
+ if (existsSync(cargoPath)) {
108
+ var cargo = readFileSync(cargoPath, "utf-8");
109
+ var cargoMatch = cargo.match(/\[package\][\s\S]*?name\s*=\s*"([^"]+)"/);
110
+ if (cargoMatch) return cargoMatch[1];
111
+ }
112
+ } catch {}
113
+
114
+ try {
115
+ var composerPath = join(dirPath, "composer.json");
116
+ if (existsSync(composerPath)) {
117
+ var composer = JSON.parse(readFileSync(composerPath, "utf-8"));
118
+ if (composer.name) return composer.name;
119
+ }
120
+ } catch {}
121
+
122
+ try {
123
+ var pyprojectPath = join(dirPath, "pyproject.toml");
124
+ if (existsSync(pyprojectPath)) {
125
+ var pyproject = readFileSync(pyprojectPath, "utf-8");
126
+ var pyMatch = pyproject.match(/\[project\][\s\S]*?name\s*=\s*"([^"]+)"/);
127
+ if (pyMatch) return pyMatch[1];
128
+ }
129
+ } catch {}
130
+
131
+ try {
132
+ var goModPath = join(dirPath, "go.mod");
133
+ if (existsSync(goModPath)) {
134
+ var goMod = readFileSync(goModPath, "utf-8");
135
+ var goMatch = goMod.match(/^module\s+(\S+)/m);
136
+ if (goMatch) {
137
+ var parts = goMatch[1].split("/");
138
+ return parts[parts.length - 1];
139
+ }
140
+ }
141
+ } catch {}
142
+
143
+ try {
144
+ var entries = readdirSync(dirPath);
145
+ for (var i = 0; i < entries.length; i++) {
146
+ if (entries[i].endsWith(".sln") || entries[i].endsWith(".csproj")) {
147
+ return entries[i].replace(/\.[^.]+$/, "");
148
+ }
149
+ }
150
+ } catch {}
151
+
152
+ return null;
153
+ }
154
+
155
+ registerHandler("browse", function (clientId: string, message: ClientMessage) {
156
+ if (message.type === "browse:list") {
157
+ var browseMsg = message as { type: "browse:list"; path: string };
158
+ var resolvedPath = resolvePath(browseMsg.path);
159
+ var home = homedir();
160
+
161
+ if (!existsSync(resolvedPath)) {
162
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
163
+ return;
164
+ }
165
+
166
+ try {
167
+ var stat = statSync(resolvedPath);
168
+ if (!stat.isDirectory()) {
169
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
170
+ return;
171
+ }
172
+ } catch {
173
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
174
+ return;
175
+ }
176
+
177
+ try {
178
+ var dirEntries = readdirSync(resolvedPath, { withFileTypes: true });
179
+ var results: Array<{ name: string; path: string; hasClaudeMd: boolean; projectName: string | null }> = [];
180
+
181
+ for (var i = 0; i < dirEntries.length; i++) {
182
+ var entry = dirEntries[i];
183
+ if (!entry.isDirectory()) continue;
184
+
185
+ var entryPath = join(resolvedPath, entry.name);
186
+ var hasClaudeMd = existsSync(join(entryPath, "CLAUDE.md"));
187
+ var projectName = detectProjectName(entryPath);
188
+
189
+ results.push({
190
+ name: entry.name,
191
+ path: entryPath,
192
+ hasClaudeMd: hasClaudeMd,
193
+ projectName: projectName,
194
+ });
195
+ }
196
+
197
+ results.sort(function (a, b) { return a.name.localeCompare(b.name); });
198
+
199
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: results });
200
+ } catch {
201
+ sendTo(clientId, { type: "browse:list_result", path: resolvedPath, homedir: home, entries: [] });
202
+ }
203
+ return;
204
+ }
205
+
206
+ if (message.type === "browse:suggestions") {
207
+ var claudeProjectsDir = join(homedir(), ".claude", "projects");
208
+ var config = loadConfig();
209
+ var existingPaths = new Set(config.projects.map(function (p) { return p.path; }));
210
+ var suggestions: Array<{ path: string; name: string; hasClaudeMd: boolean }> = [];
211
+
212
+ if (existsSync(claudeProjectsDir)) {
213
+ try {
214
+ var hashDirs = readdirSync(claudeProjectsDir);
215
+ for (var i = 0; i < hashDirs.length; i++) {
216
+ var hashDir = hashDirs[i];
217
+ var candidatePath = "/" + hashDir.slice(1).replace(/-/g, "/");
218
+
219
+ if (!existsSync(candidatePath)) continue;
220
+ if (existingPaths.has(candidatePath)) continue;
221
+
222
+ try {
223
+ var stat = statSync(candidatePath);
224
+ if (!stat.isDirectory()) continue;
225
+ } catch { continue; }
226
+
227
+ var hasClaudeMd = existsSync(join(candidatePath, "CLAUDE.md"));
228
+ var name = candidatePath.split("/").pop() || hashDir;
229
+
230
+ suggestions.push({
231
+ path: candidatePath,
232
+ name: name,
233
+ hasClaudeMd: hasClaudeMd,
234
+ });
235
+ }
236
+ } catch {}
237
+ }
238
+
239
+ suggestions.sort(function (a, b) { return a.name.localeCompare(b.name); });
240
+ sendTo(clientId, { type: "browse:suggestions_result", suggestions: suggestions });
241
+ return;
242
+ }
243
+ });