@cryptiklemur/lattice 0.0.0 → 1.1.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.
- package/.github/workflows/release.yml +4 -4
- package/.releaserc.json +2 -1
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
- package/client/src/components/mesh/NodeBadge.tsx +24 -24
- package/client/src/components/mesh/PairingDialog.tsx +281 -281
- package/client/src/components/panels/FileBrowser.tsx +241 -241
- package/client/src/components/panels/StickyNotes.tsx +187 -187
- package/client/src/components/settings/Appearance.tsx +151 -151
- package/client/src/components/settings/MeshStatus.tsx +145 -145
- package/client/src/components/settings/SettingsView.tsx +57 -57
- package/client/src/components/setup/SetupWizard.tsx +750 -750
- package/client/src/components/ui/ErrorBoundary.tsx +56 -56
- package/client/src/router.tsx +391 -391
- package/client/vite.config.ts +20 -20
- package/package.json +1 -1
- package/server/src/handlers/chat.ts +194 -194
- package/server/src/handlers/settings.ts +109 -109
- package/themes/amoled.json +20 -20
- package/themes/ayu-light.json +9 -9
- package/themes/catppuccin-latte.json +9 -9
- package/themes/catppuccin-mocha.json +9 -9
- package/themes/clay-light.json +10 -10
- package/themes/clay.json +10 -10
- package/themes/dracula.json +9 -9
- package/themes/everforest-light.json +9 -9
- package/themes/everforest.json +9 -9
- package/themes/github-light.json +9 -9
- package/themes/gruvbox-dark.json +9 -9
- package/themes/gruvbox-light.json +9 -9
- package/themes/monokai.json +9 -9
- package/themes/nord-light.json +9 -9
- package/themes/nord.json +9 -9
- package/themes/one-dark.json +9 -9
- package/themes/one-light.json +9 -9
- package/themes/rose-pine-dawn.json +9 -9
- package/themes/rose-pine.json +9 -9
- package/themes/solarized-dark.json +9 -9
- package/themes/solarized-light.json +9 -9
- package/themes/tokyo-night-light.json +9 -9
- package/themes/tokyo-night.json +9 -9
- package/.serena/project.yml +0 -138
package/client/vite.config.ts
CHANGED
|
@@ -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": "
|
|
3
|
+
"version": "1.1.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>",
|
|
@@ -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
|
+
});
|
|
@@ -1,109 +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
|
-
});
|
|
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
|
+
});
|