@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.
- package/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- 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
|
+
});
|