@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,432 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listSessions as sdkListSessions,
|
|
3
|
+
getSessionInfo,
|
|
4
|
+
getSessionMessages,
|
|
5
|
+
renameSession as sdkRenameSession,
|
|
6
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
import type { SDKSessionInfo, SessionMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
+
import { existsSync, unlinkSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import type { HistoryMessage, SessionSummary } from "@lattice/shared";
|
|
13
|
+
import { loadConfig } from "../config";
|
|
14
|
+
|
|
15
|
+
function getProjectPath(projectSlug: string): string | null {
|
|
16
|
+
var config = loadConfig();
|
|
17
|
+
var project = config.projects.find(function (p) { return p.slug === projectSlug; });
|
|
18
|
+
return project ? project.path : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function projectPathToHash(projectPath: string): string {
|
|
22
|
+
return projectPath.replace(/\//g, "-");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mapSDKSession(info: SDKSessionInfo, projectSlug: string): SessionSummary {
|
|
26
|
+
return {
|
|
27
|
+
id: info.sessionId,
|
|
28
|
+
projectSlug,
|
|
29
|
+
title: info.customTitle || info.summary || info.firstPrompt || "Untitled",
|
|
30
|
+
createdAt: info.createdAt || info.lastModified,
|
|
31
|
+
updatedAt: info.lastModified,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
36
|
+
|
|
37
|
+
var pricingCache: Record<string, { input: number; output: number; cacheRead?: number; cacheCreation?: number }> = {};
|
|
38
|
+
var pricingLoaded = false;
|
|
39
|
+
|
|
40
|
+
var FALLBACK_PRICING: Record<string, { input: number; output: number }> = {
|
|
41
|
+
"claude-opus-4-6": { input: 15, output: 75 },
|
|
42
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
43
|
+
"claude-haiku-4-5": { input: 0.80, output: 4 },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function loadPricing(): void {
|
|
47
|
+
if (pricingLoaded) return;
|
|
48
|
+
pricingLoaded = true;
|
|
49
|
+
fetch(LITELLM_PRICING_URL).then(function (res) {
|
|
50
|
+
return res.json();
|
|
51
|
+
}).then(function (data: Record<string, Record<string, unknown>>) {
|
|
52
|
+
for (var key in data) {
|
|
53
|
+
if (!key.includes("claude")) continue;
|
|
54
|
+
var entry = data[key];
|
|
55
|
+
var inputCost = entry.input_cost_per_token as number | undefined;
|
|
56
|
+
var outputCost = entry.output_cost_per_token as number | undefined;
|
|
57
|
+
if (inputCost == null || outputCost == null) continue;
|
|
58
|
+
var modelId = key.replace("anthropic/", "").replace("claude-", "claude-");
|
|
59
|
+
pricingCache[modelId] = {
|
|
60
|
+
input: inputCost * 1000000,
|
|
61
|
+
output: outputCost * 1000000,
|
|
62
|
+
cacheRead: entry.cache_read_input_token_cost != null ? (entry.cache_read_input_token_cost as number) * 1000000 : undefined,
|
|
63
|
+
cacheCreation: entry.cache_creation_input_token_cost != null ? (entry.cache_creation_input_token_cost as number) * 1000000 : undefined,
|
|
64
|
+
};
|
|
65
|
+
pricingCache[key] = pricingCache[modelId];
|
|
66
|
+
}
|
|
67
|
+
}).catch(function () {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
loadPricing();
|
|
71
|
+
|
|
72
|
+
function getPricing(model: string): { input: number; output: number; cacheRead?: number; cacheCreation?: number } {
|
|
73
|
+
if (pricingCache[model]) return pricingCache[model];
|
|
74
|
+
for (var key in pricingCache) {
|
|
75
|
+
if (key.includes(model) || model.includes(key)) return pricingCache[key];
|
|
76
|
+
}
|
|
77
|
+
var shortModel = model.replace("claude-", "").split("-")[0];
|
|
78
|
+
for (var key2 in pricingCache) {
|
|
79
|
+
if (key2.includes(shortModel)) return pricingCache[key2];
|
|
80
|
+
}
|
|
81
|
+
if (FALLBACK_PRICING[model]) {
|
|
82
|
+
return FALLBACK_PRICING[model];
|
|
83
|
+
}
|
|
84
|
+
return FALLBACK_PRICING["claude-sonnet-4-6"];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function estimateCost(model: string, inputTokens: number, outputTokens: number, cacheRead: number, cacheCreation: number): number {
|
|
88
|
+
var pricing = getPricing(model);
|
|
89
|
+
var normalInput = inputTokens - cacheRead - cacheCreation;
|
|
90
|
+
var inputCost = (normalInput * pricing.input) / 1000000;
|
|
91
|
+
var cacheCost = pricing.cacheRead != null
|
|
92
|
+
? (cacheRead * pricing.cacheRead) / 1000000
|
|
93
|
+
: (cacheRead * pricing.input * 0.1) / 1000000;
|
|
94
|
+
var cacheCreateCost = pricing.cacheCreation != null
|
|
95
|
+
? (cacheCreation * pricing.cacheCreation) / 1000000
|
|
96
|
+
: (cacheCreation * pricing.input * 1.25) / 1000000;
|
|
97
|
+
var outputCost = (outputTokens * pricing.output) / 1000000;
|
|
98
|
+
return Math.max(0, inputCost + cacheCost + cacheCreateCost + outputCost);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseTimestamp(msg: SessionMessage): number {
|
|
102
|
+
var raw = (msg as unknown as { timestamp?: string }).timestamp;
|
|
103
|
+
if (raw) {
|
|
104
|
+
var parsed = new Date(raw).getTime();
|
|
105
|
+
if (!isNaN(parsed)) return parsed;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stripXmlTags(text: string): string {
|
|
111
|
+
return text.replace(/<[^>]+>/g, "").trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isSystemContent(text: string): boolean {
|
|
115
|
+
return text.startsWith("<local-command-caveat>")
|
|
116
|
+
|| text.startsWith("<system-reminder>")
|
|
117
|
+
|| text.startsWith("<local-command-stdout>")
|
|
118
|
+
|| text.startsWith("<command-name>")
|
|
119
|
+
|| text.startsWith("<command-message>");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractUserText(content: unknown): string {
|
|
123
|
+
if (typeof content === "string") {
|
|
124
|
+
if (isSystemContent(content)) return "";
|
|
125
|
+
return stripXmlTags(content);
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(content)) {
|
|
128
|
+
for (var i = 0; i < content.length; i++) {
|
|
129
|
+
var block = content[i] as { type?: string; text?: string };
|
|
130
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
131
|
+
if (isSystemContent(block.text)) continue;
|
|
132
|
+
var cleaned = stripXmlTags(block.text);
|
|
133
|
+
if (cleaned) return cleaned;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function convertSessionMessages(messages: SessionMessage[]): HistoryMessage[] {
|
|
141
|
+
var result: HistoryMessage[] = [];
|
|
142
|
+
|
|
143
|
+
for (var i = 0; i < messages.length; i++) {
|
|
144
|
+
var msg = messages[i];
|
|
145
|
+
var ts = parseTimestamp(msg);
|
|
146
|
+
var apiMsg = msg.message as { role?: string; content?: unknown };
|
|
147
|
+
|
|
148
|
+
if (msg.type === "user") {
|
|
149
|
+
if (Array.isArray(apiMsg.content)) {
|
|
150
|
+
var hadToolResult = false;
|
|
151
|
+
for (var j = 0; j < apiMsg.content.length; j++) {
|
|
152
|
+
var block = apiMsg.content[j] as { type?: string; text?: string; tool_use_id?: string; content?: unknown };
|
|
153
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
154
|
+
hadToolResult = true;
|
|
155
|
+
var resultContent = "";
|
|
156
|
+
if (typeof block.content === "string") {
|
|
157
|
+
resultContent = block.content;
|
|
158
|
+
} else if (Array.isArray(block.content)) {
|
|
159
|
+
var texts: string[] = [];
|
|
160
|
+
for (var ri = 0; ri < block.content.length; ri++) {
|
|
161
|
+
var rb = block.content[ri] as { type?: string; text?: string };
|
|
162
|
+
if (rb.type === "text" && rb.text) texts.push(rb.text);
|
|
163
|
+
}
|
|
164
|
+
resultContent = texts.join("\n");
|
|
165
|
+
} else {
|
|
166
|
+
resultContent = JSON.stringify(block.content ?? "");
|
|
167
|
+
}
|
|
168
|
+
result.push({
|
|
169
|
+
type: "tool_result",
|
|
170
|
+
toolId: block.tool_use_id,
|
|
171
|
+
content: resultContent,
|
|
172
|
+
timestamp: ts,
|
|
173
|
+
});
|
|
174
|
+
} else if (block.type === "text" && block.text) {
|
|
175
|
+
if (isSystemContent(block.text)) continue;
|
|
176
|
+
var cleaned = stripXmlTags(block.text);
|
|
177
|
+
if (cleaned) {
|
|
178
|
+
result.push({
|
|
179
|
+
type: "user",
|
|
180
|
+
uuid: msg.uuid,
|
|
181
|
+
text: cleaned,
|
|
182
|
+
timestamp: ts,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
var text = extractUserText(apiMsg.content);
|
|
189
|
+
if (text) {
|
|
190
|
+
result.push({
|
|
191
|
+
type: "user",
|
|
192
|
+
uuid: msg.uuid,
|
|
193
|
+
text,
|
|
194
|
+
timestamp: ts,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (msg.type === "assistant") {
|
|
199
|
+
var msgUsage = (apiMsg as Record<string, unknown>).usage as { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number } | undefined;
|
|
200
|
+
var msgModel = ((apiMsg as Record<string, unknown>).model as string) || "";
|
|
201
|
+
var lastAssistantIdx = -1;
|
|
202
|
+
if (Array.isArray(apiMsg.content)) {
|
|
203
|
+
for (var k = 0; k < apiMsg.content.length; k++) {
|
|
204
|
+
var aBlock = apiMsg.content[k] as { type?: string; text?: string; id?: string; name?: string; input?: unknown };
|
|
205
|
+
if (aBlock.type === "text" && aBlock.text) {
|
|
206
|
+
lastAssistantIdx = result.length;
|
|
207
|
+
result.push({
|
|
208
|
+
type: "assistant",
|
|
209
|
+
uuid: msg.uuid + "-text-" + k,
|
|
210
|
+
text: aBlock.text,
|
|
211
|
+
timestamp: ts,
|
|
212
|
+
});
|
|
213
|
+
} else if (aBlock.type === "tool_use" && aBlock.id && aBlock.name) {
|
|
214
|
+
result.push({
|
|
215
|
+
type: "tool_start",
|
|
216
|
+
toolId: aBlock.id,
|
|
217
|
+
name: aBlock.name,
|
|
218
|
+
args: JSON.stringify(aBlock.input ?? {}),
|
|
219
|
+
timestamp: ts,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (msgUsage && lastAssistantIdx >= 0) {
|
|
224
|
+
var inTok = msgUsage.input_tokens || 0;
|
|
225
|
+
var outTok = msgUsage.output_tokens || 0;
|
|
226
|
+
var cacheRead = msgUsage.cache_read_input_tokens || 0;
|
|
227
|
+
var cacheCreate = msgUsage.cache_creation_input_tokens || 0;
|
|
228
|
+
result[lastAssistantIdx].inputTokens = inTok;
|
|
229
|
+
result[lastAssistantIdx].outputTokens = outTok;
|
|
230
|
+
result[lastAssistantIdx].model = msgModel;
|
|
231
|
+
result[lastAssistantIdx].costEstimate = estimateCost(msgModel, inTok, outTok, cacheRead, cacheCreate);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
var aText = typeof apiMsg.content === "string" ? apiMsg.content : "";
|
|
235
|
+
if (aText) {
|
|
236
|
+
lastAssistantIdx = result.length;
|
|
237
|
+
result.push({
|
|
238
|
+
type: "assistant",
|
|
239
|
+
uuid: msg.uuid,
|
|
240
|
+
text: aText,
|
|
241
|
+
timestamp: ts,
|
|
242
|
+
});
|
|
243
|
+
if (msgUsage) {
|
|
244
|
+
var inTok2 = msgUsage.input_tokens || 0;
|
|
245
|
+
var outTok2 = msgUsage.output_tokens || 0;
|
|
246
|
+
var cacheRead2 = msgUsage.cache_read_input_tokens || 0;
|
|
247
|
+
var cacheCreate2 = msgUsage.cache_creation_input_tokens || 0;
|
|
248
|
+
result[lastAssistantIdx].inputTokens = inTok2;
|
|
249
|
+
result[lastAssistantIdx].outputTokens = outTok2;
|
|
250
|
+
result[lastAssistantIdx].model = msgModel;
|
|
251
|
+
result[lastAssistantIdx].costEstimate = estimateCost(msgModel, inTok2, outTok2, cacheRead2, cacheCreate2);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface SessionUsageInfo {
|
|
262
|
+
inputTokens: number;
|
|
263
|
+
outputTokens: number;
|
|
264
|
+
cacheReadTokens: number;
|
|
265
|
+
cacheCreationTokens: number;
|
|
266
|
+
contextWindow: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
270
|
+
"claude-opus-4-6": 1048576,
|
|
271
|
+
"claude-sonnet-4-6": 1048576,
|
|
272
|
+
"claude-sonnet-4-5-20250514": 1048576,
|
|
273
|
+
"claude-haiku-4-5-20251001": 1048576,
|
|
274
|
+
"claude-sonnet-4-20250514": 200000,
|
|
275
|
+
"claude-3-5-sonnet-20241022": 200000,
|
|
276
|
+
"claude-3-5-haiku-20241022": 200000,
|
|
277
|
+
"claude-3-opus-20240229": 200000,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export function guessContextWindow(model: string): number {
|
|
281
|
+
if (MODEL_CONTEXT_WINDOWS[model]) return MODEL_CONTEXT_WINDOWS[model];
|
|
282
|
+
if (model.includes("opus-4") || model.includes("sonnet-4")) return 1048576;
|
|
283
|
+
if (model.includes("haiku-4")) return 1048576;
|
|
284
|
+
return 200000;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function getSessionUsage(projectSlug: string, sessionId: string): Promise<SessionUsageInfo | null> {
|
|
288
|
+
var projectPath = getProjectPath(projectSlug);
|
|
289
|
+
if (!projectPath) return null;
|
|
290
|
+
|
|
291
|
+
var hash = projectPathToHash(projectPath);
|
|
292
|
+
var sessionFile = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
293
|
+
if (!existsSync(sessionFile)) return null;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
var content = readFileSync(sessionFile, "utf-8");
|
|
297
|
+
var lines = content.trim().split("\n");
|
|
298
|
+
|
|
299
|
+
for (var i = lines.length - 1; i >= 0; i--) {
|
|
300
|
+
var line = lines[i].trim();
|
|
301
|
+
if (!line) continue;
|
|
302
|
+
try {
|
|
303
|
+
var parsed = JSON.parse(line);
|
|
304
|
+
if (parsed.type === "assistant" && parsed.message && parsed.message.usage) {
|
|
305
|
+
var usage = parsed.message.usage;
|
|
306
|
+
var model = parsed.message.model || "";
|
|
307
|
+
return {
|
|
308
|
+
inputTokens: usage.input_tokens || 0,
|
|
309
|
+
outputTokens: usage.output_tokens || 0,
|
|
310
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
311
|
+
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
312
|
+
contextWindow: guessContextWindow(model),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function listSessions(projectSlug: string): Promise<SessionSummary[]> {
|
|
325
|
+
var projectPath = getProjectPath(projectSlug);
|
|
326
|
+
if (!projectPath) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
var sdkSessions = await sdkListSessions({ dir: projectPath });
|
|
332
|
+
var summaries = sdkSessions.map(function (s) {
|
|
333
|
+
return mapSDKSession(s, projectSlug);
|
|
334
|
+
});
|
|
335
|
+
summaries.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
336
|
+
return summaries;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.warn("[lattice] Failed to list SDK sessions:", err);
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function getSessionTitle(projectSlug: string, sessionId: string): Promise<string> {
|
|
344
|
+
var projectPath = getProjectPath(projectSlug);
|
|
345
|
+
var options = projectPath ? { dir: projectPath } : undefined;
|
|
346
|
+
try {
|
|
347
|
+
var info = await getSessionInfo(sessionId, options);
|
|
348
|
+
if (info) {
|
|
349
|
+
return info.customTitle || info.summary || info.firstPrompt || "Untitled";
|
|
350
|
+
}
|
|
351
|
+
} catch {}
|
|
352
|
+
return "Untitled";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function loadSessionHistory(projectSlug: string, sessionId: string): Promise<HistoryMessage[]> {
|
|
356
|
+
var projectPath = getProjectPath(projectSlug);
|
|
357
|
+
var options = projectPath ? { dir: projectPath } : undefined;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
var messages = await getSessionMessages(sessionId, options);
|
|
361
|
+
return convertSessionMessages(messages);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.warn("[lattice] Failed to load session history:", err);
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function createSession(projectSlug: string): SessionSummary {
|
|
369
|
+
var sessionId = randomUUID();
|
|
370
|
+
var now = Date.now();
|
|
371
|
+
return {
|
|
372
|
+
id: sessionId,
|
|
373
|
+
projectSlug,
|
|
374
|
+
title: "Session " + new Date(now).toLocaleString(),
|
|
375
|
+
createdAt: now,
|
|
376
|
+
updatedAt: now,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function renameSession(projectSlug: string, sessionId: string, title: string): Promise<boolean> {
|
|
381
|
+
var projectPath = getProjectPath(projectSlug);
|
|
382
|
+
var options = projectPath ? { dir: projectPath } : undefined;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await sdkRenameSession(sessionId, title, options);
|
|
386
|
+
return true;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.warn("[lattice] Failed to rename session:", err);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function deleteSession(projectSlug: string, sessionId: string): Promise<boolean> {
|
|
394
|
+
var projectPath = getProjectPath(projectSlug);
|
|
395
|
+
if (!projectPath) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
var hash = projectPathToHash(projectPath);
|
|
400
|
+
var sessionFile = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
401
|
+
|
|
402
|
+
if (!existsSync(sessionFile)) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
unlinkSync(sessionFile);
|
|
408
|
+
return true;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.warn("[lattice] Failed to delete session:", err);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function findProjectSlugForSession(sessionId: string): Promise<string | null> {
|
|
416
|
+
try {
|
|
417
|
+
var info = await getSessionInfo(sessionId);
|
|
418
|
+
if (!info || !info.cwd) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
var config = loadConfig();
|
|
423
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
424
|
+
if (info.cwd.startsWith(config.projects[i].path)) {
|
|
425
|
+
return config.projects[i].slug;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { IPty } from "node-pty";
|
|
3
|
+
|
|
4
|
+
var terminals = new Map<string, IPty>();
|
|
5
|
+
var pty: typeof import("node-pty") | null = null;
|
|
6
|
+
|
|
7
|
+
function getPty(): typeof import("node-pty") {
|
|
8
|
+
if (!pty) {
|
|
9
|
+
pty = require("node-pty") as typeof import("node-pty");
|
|
10
|
+
}
|
|
11
|
+
return pty;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createTerminal(
|
|
15
|
+
cwd: string,
|
|
16
|
+
onData: (data: string) => void,
|
|
17
|
+
onExit: (code: number) => void,
|
|
18
|
+
): string {
|
|
19
|
+
var termId = randomUUID();
|
|
20
|
+
var shell = process.env.SHELL || "bash";
|
|
21
|
+
var lib = getPty();
|
|
22
|
+
|
|
23
|
+
var term = lib.spawn(shell, [], {
|
|
24
|
+
name: "xterm-256color",
|
|
25
|
+
cols: 80,
|
|
26
|
+
rows: 24,
|
|
27
|
+
cwd: cwd,
|
|
28
|
+
env: process.env as Record<string, string>,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
term.onData(onData);
|
|
32
|
+
term.onExit(function(e) {
|
|
33
|
+
terminals.delete(termId);
|
|
34
|
+
onExit(e.exitCode ?? 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
terminals.set(termId, term);
|
|
38
|
+
return termId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeToTerminal(termId: string, data: string): void {
|
|
42
|
+
var term = terminals.get(termId);
|
|
43
|
+
if (term) {
|
|
44
|
+
term.write(data);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resizeTerminal(termId: string, cols: number, rows: number): void {
|
|
49
|
+
var term = terminals.get(termId);
|
|
50
|
+
if (term) {
|
|
51
|
+
term.resize(cols, rows);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function destroyTerminal(termId: string): void {
|
|
56
|
+
var term = terminals.get(termId);
|
|
57
|
+
if (term) {
|
|
58
|
+
try {
|
|
59
|
+
term.kill();
|
|
60
|
+
} catch {
|
|
61
|
+
// already dead
|
|
62
|
+
}
|
|
63
|
+
terminals.delete(termId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getTerminal(termId: string): IPty | undefined {
|
|
68
|
+
return terminals.get(termId);
|
|
69
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { getLatticeHome } from "./config";
|
|
5
|
+
|
|
6
|
+
export interface CertPaths {
|
|
7
|
+
cert: string;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCertsDir(): string {
|
|
12
|
+
var certsDir = join(getLatticeHome(), "certs");
|
|
13
|
+
if (!existsSync(certsDir)) {
|
|
14
|
+
mkdirSync(certsDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
return certsDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ensureCerts(): CertPaths {
|
|
20
|
+
var certsDir = getCertsDir();
|
|
21
|
+
var certPath = join(certsDir, "cert.pem");
|
|
22
|
+
var keyPath = join(certsDir, "key.pem");
|
|
23
|
+
|
|
24
|
+
if (existsSync(certPath) && existsSync(keyPath)) {
|
|
25
|
+
return { cert: certPath, key: keyPath };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log("[lattice] Generating self-signed TLS certificate...");
|
|
29
|
+
|
|
30
|
+
var result = spawnSync(
|
|
31
|
+
"openssl",
|
|
32
|
+
[
|
|
33
|
+
"req", "-x509",
|
|
34
|
+
"-newkey", "rsa:2048",
|
|
35
|
+
"-keyout", keyPath,
|
|
36
|
+
"-out", certPath,
|
|
37
|
+
"-days", "365",
|
|
38
|
+
"-nodes",
|
|
39
|
+
"-subj", "/CN=lattice",
|
|
40
|
+
],
|
|
41
|
+
{ encoding: "utf-8" }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
throw new Error("[lattice] Failed to generate TLS certificates: " + (result.stderr || result.error?.message || "unknown error"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log("[lattice] TLS certificates generated at " + certsDir);
|
|
49
|
+
|
|
50
|
+
return { cert: certPath, key: keyPath };
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
|
|
3
|
+
var clients = new Map<string, ServerWebSocket<{ id: string }>>();
|
|
4
|
+
|
|
5
|
+
export function addClient(ws: ServerWebSocket<{ id: string }>): void {
|
|
6
|
+
clients.set(ws.data.id, ws);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function removeClient(id: string): void {
|
|
10
|
+
clients.delete(id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function broadcast(message: object, excludeId?: string): void {
|
|
14
|
+
var text = JSON.stringify(message);
|
|
15
|
+
for (var [id, ws] of clients) {
|
|
16
|
+
if (id !== excludeId) {
|
|
17
|
+
ws.send(text);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sendTo(id: string, message: object): void {
|
|
23
|
+
var ws = clients.get(id);
|
|
24
|
+
if (ws) {
|
|
25
|
+
ws.send(JSON.stringify(message));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getClientCount(): number {
|
|
30
|
+
return clients.size;
|
|
31
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ClientMessage } from "@lattice/shared";
|
|
2
|
+
import { sendTo } from "./broadcast";
|
|
3
|
+
|
|
4
|
+
type Handler = (clientId: string, message: ClientMessage) => void | Promise<void>;
|
|
5
|
+
|
|
6
|
+
var handlers = new Map<string, Handler>();
|
|
7
|
+
var clientRemoteNode = new Map<string, { nodeId: string; projectSlug: string }>();
|
|
8
|
+
|
|
9
|
+
var PROXIED_PREFIXES = new Set(["session", "chat", "fs", "terminal"]);
|
|
10
|
+
|
|
11
|
+
export function registerHandler(prefix: string, handler: Handler): void {
|
|
12
|
+
handlers.set(prefix, handler);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setClientRemoteNode(clientId: string, nodeId: string, projectSlug: string): void {
|
|
16
|
+
clientRemoteNode.set(clientId, { nodeId, projectSlug });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearClientRemoteNode(clientId: string): void {
|
|
20
|
+
clientRemoteNode.delete(clientId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getClientRemoteNode(clientId: string): { nodeId: string; projectSlug: string } | undefined {
|
|
24
|
+
return clientRemoteNode.get(clientId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
28
|
+
var prefix = message.type.split(":")[0];
|
|
29
|
+
|
|
30
|
+
if (PROXIED_PREFIXES.has(prefix)) {
|
|
31
|
+
var remote = clientRemoteNode.get(clientId);
|
|
32
|
+
|
|
33
|
+
if (message.type === "session:activate") {
|
|
34
|
+
var activateMsg = message as { type: string; projectSlug: string; sessionId: string };
|
|
35
|
+
var localProject = getLocalProject(activateMsg.projectSlug);
|
|
36
|
+
if (!localProject) {
|
|
37
|
+
var remoteEntry = getRemoteNodeForProject(activateMsg.projectSlug);
|
|
38
|
+
if (remoteEntry) {
|
|
39
|
+
setClientRemoteNode(clientId, remoteEntry.nodeId, activateMsg.projectSlug);
|
|
40
|
+
proxyMessage(clientId, remoteEntry.nodeId, activateMsg.projectSlug, message);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
clearClientRemoteNode(clientId);
|
|
45
|
+
}
|
|
46
|
+
} else if (remote) {
|
|
47
|
+
proxyMessage(clientId, remote.nodeId, remote.projectSlug, message);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var handler = handlers.get(prefix);
|
|
53
|
+
if (handler) {
|
|
54
|
+
try {
|
|
55
|
+
var result = handler(clientId, message);
|
|
56
|
+
if (result && typeof result.then === "function") {
|
|
57
|
+
result.then(undefined, function (err: unknown) {
|
|
58
|
+
var stack = err instanceof Error ? err.stack : String(err);
|
|
59
|
+
console.error("[lattice] Async handler error for " + message.type + ":", stack);
|
|
60
|
+
sendTo(clientId, { type: "chat:error", message: "Internal server error processing " + message.type });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
var stack = err instanceof Error ? (err as Error).stack : String(err);
|
|
65
|
+
console.error("[lattice] Handler error for " + message.type + ":", stack);
|
|
66
|
+
sendTo(clientId, { type: "chat:error", message: "Internal server error processing " + message.type });
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.warn(`[lattice] No handler for message type: ${message.type}`);
|
|
71
|
+
sendTo(clientId, { type: "error", message: `Unknown message type: ${message.type}` });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getLocalProject(slug: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
var { getProjectBySlug } = require("../project/registry") as typeof import("../project/registry");
|
|
77
|
+
return getProjectBySlug(slug) !== undefined;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getRemoteNodeForProject(slug: string): { nodeId: string } | undefined {
|
|
84
|
+
try {
|
|
85
|
+
var { findNodeForProject } = require("../mesh/connector") as typeof import("../mesh/connector");
|
|
86
|
+
var nodeId = findNodeForProject(slug);
|
|
87
|
+
if (nodeId) {
|
|
88
|
+
return { nodeId: nodeId };
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function proxyMessage(clientId: string, nodeId: string, projectSlug: string, message: ClientMessage): void {
|
|
97
|
+
try {
|
|
98
|
+
var { proxyToRemoteNode } = require("../mesh/proxy") as typeof import("../mesh/proxy");
|
|
99
|
+
proxyToRemoteNode(nodeId, projectSlug, clientId, message);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error("[router] Failed to proxy message:", err);
|
|
102
|
+
sendTo(clientId, { type: "chat:error", message: "Failed to proxy message to remote node" });
|
|
103
|
+
}
|
|
104
|
+
}
|