@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,289 @@
|
|
|
1
|
+
import { encodingForModel } from "js-tiktoken";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import type { ContextBreakdownSegment } from "@lattice/shared";
|
|
6
|
+
import { guessContextWindow } from "./session";
|
|
7
|
+
import { loadConfig } from "../config";
|
|
8
|
+
|
|
9
|
+
var encoder = encodingForModel("gpt-4o");
|
|
10
|
+
|
|
11
|
+
function countTokens(text: string): number {
|
|
12
|
+
if (!text) return 0;
|
|
13
|
+
return encoder.encode(text).length;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readFileSafe(path: string): string {
|
|
17
|
+
try {
|
|
18
|
+
if (existsSync(path)) return readFileSync(path, "utf-8");
|
|
19
|
+
} catch {}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readDirFiles(dirPath: string): string {
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(dirPath)) return "";
|
|
26
|
+
var files = readdirSync(dirPath, { withFileTypes: true });
|
|
27
|
+
var content = "";
|
|
28
|
+
for (var i = 0; i < files.length; i++) {
|
|
29
|
+
if (files[i].isFile()) {
|
|
30
|
+
content += readFileSafe(join(dirPath, files[i].name)) + "\n";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return content;
|
|
34
|
+
} catch {}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectPathToHash(projectPath: string): string {
|
|
39
|
+
return projectPath.replace(/\//g, "-");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getProjectPath(projectSlug: string): string | null {
|
|
43
|
+
var config = loadConfig();
|
|
44
|
+
var project = config.projects.find(function (p) { return p.slug === projectSlug; });
|
|
45
|
+
return project ? project.path : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Known built-in Claude Code tools with approximate per-tool token counts
|
|
49
|
+
// These are the tool definitions sent in every API request
|
|
50
|
+
var BUILTIN_TOOLS: Record<string, number> = {
|
|
51
|
+
"Read": 350,
|
|
52
|
+
"Write": 300,
|
|
53
|
+
"Edit": 400,
|
|
54
|
+
"Bash": 500,
|
|
55
|
+
"Glob": 250,
|
|
56
|
+
"Grep": 450,
|
|
57
|
+
"Agent": 600,
|
|
58
|
+
"WebFetch": 400,
|
|
59
|
+
"WebSearch": 300,
|
|
60
|
+
"TodoWrite": 300,
|
|
61
|
+
"NotebookEdit": 350,
|
|
62
|
+
"AskUserQuestion": 400,
|
|
63
|
+
"Skill": 350,
|
|
64
|
+
"TaskOutput": 200,
|
|
65
|
+
"TaskStop": 150,
|
|
66
|
+
"EnterPlanMode": 200,
|
|
67
|
+
"ExitPlanMode": 150,
|
|
68
|
+
"EnterWorktree": 200,
|
|
69
|
+
"ExitWorktree": 150,
|
|
70
|
+
"CronCreate": 250,
|
|
71
|
+
"CronDelete": 200,
|
|
72
|
+
"CronList": 150,
|
|
73
|
+
"ToolSearch": 250,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Average tokens per MCP tool definition (name + description + JSON schema)
|
|
77
|
+
var MCP_TOOL_AVG_TOKENS = 250;
|
|
78
|
+
|
|
79
|
+
interface ToolCounts {
|
|
80
|
+
builtinTools: string[];
|
|
81
|
+
mcpTools: Map<string, string[]>; // server name -> tool names
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractToolsFromSession(lines: string[]): ToolCounts {
|
|
85
|
+
var builtinSet = new Set<string>();
|
|
86
|
+
var mcpMap = new Map<string, Set<string>>();
|
|
87
|
+
|
|
88
|
+
// First: check compact boundary metadata for preCompactDiscoveredTools
|
|
89
|
+
for (var i = 0; i < lines.length; i++) {
|
|
90
|
+
var line = lines[i].trim();
|
|
91
|
+
if (!line || !line.includes("compactMetadata")) continue;
|
|
92
|
+
try {
|
|
93
|
+
var parsed = JSON.parse(line);
|
|
94
|
+
var tools = parsed.compactMetadata?.preCompactDiscoveredTools;
|
|
95
|
+
if (Array.isArray(tools)) {
|
|
96
|
+
for (var t = 0; t < tools.length; t++) {
|
|
97
|
+
categorizeToolName(tools[t], builtinSet, mcpMap);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Second: scan tool_use blocks from assistant messages for any we missed
|
|
104
|
+
for (var j = 0; j < lines.length; j++) {
|
|
105
|
+
var aLine = lines[j].trim();
|
|
106
|
+
if (!aLine || !aLine.includes("tool_use")) continue;
|
|
107
|
+
try {
|
|
108
|
+
var aParsed = JSON.parse(aLine);
|
|
109
|
+
if (aParsed.type === "assistant" && aParsed.message && Array.isArray(aParsed.message.content)) {
|
|
110
|
+
for (var k = 0; k < aParsed.message.content.length; k++) {
|
|
111
|
+
var block = aParsed.message.content[k];
|
|
112
|
+
if (block.type === "tool_use" && block.name) {
|
|
113
|
+
categorizeToolName(block.name, builtinSet, mcpMap);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If we found no tools at all, use the default known set
|
|
121
|
+
if (builtinSet.size === 0 && mcpMap.size === 0) {
|
|
122
|
+
return {
|
|
123
|
+
builtinTools: Object.keys(BUILTIN_TOOLS),
|
|
124
|
+
mcpTools: new Map(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
builtinTools: Array.from(builtinSet),
|
|
130
|
+
mcpTools: new Map(Array.from(mcpMap.entries()).map(function (entry) {
|
|
131
|
+
return [entry[0], Array.from(entry[1])];
|
|
132
|
+
})),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function categorizeToolName(name: string, builtinSet: Set<string>, mcpMap: Map<string, Set<string>>): void {
|
|
137
|
+
if (name.startsWith("mcp__")) {
|
|
138
|
+
var parts = name.split("__");
|
|
139
|
+
var serverName = parts[1] || "unknown";
|
|
140
|
+
if (!mcpMap.has(serverName)) {
|
|
141
|
+
mcpMap.set(serverName, new Set());
|
|
142
|
+
}
|
|
143
|
+
mcpMap.get(serverName)!.add(name);
|
|
144
|
+
} else {
|
|
145
|
+
builtinSet.add(name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function estimateBuiltinToolTokens(toolNames: string[]): number {
|
|
150
|
+
var total = 0;
|
|
151
|
+
for (var i = 0; i < toolNames.length; i++) {
|
|
152
|
+
total += BUILTIN_TOOLS[toolNames[i]] || 300;
|
|
153
|
+
}
|
|
154
|
+
// Also include tools that are always sent but might not appear in usage
|
|
155
|
+
var knownKeys = Object.keys(BUILTIN_TOOLS);
|
|
156
|
+
for (var j = 0; j < knownKeys.length; j++) {
|
|
157
|
+
if (toolNames.indexOf(knownKeys[j]) === -1) {
|
|
158
|
+
total += BUILTIN_TOOLS[knownKeys[j]];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return total;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ContextBreakdownResult {
|
|
165
|
+
segments: ContextBreakdownSegment[];
|
|
166
|
+
contextWindow: number;
|
|
167
|
+
autocompactAt: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function getContextBreakdown(projectSlug: string, sessionId: string): Promise<ContextBreakdownResult | null> {
|
|
171
|
+
var projectPath = getProjectPath(projectSlug);
|
|
172
|
+
if (!projectPath) return null;
|
|
173
|
+
|
|
174
|
+
var home = homedir();
|
|
175
|
+
var hash = projectPathToHash(projectPath);
|
|
176
|
+
var sessionFile = join(home, ".claude", "projects", hash, sessionId + ".jsonl");
|
|
177
|
+
if (!existsSync(sessionFile)) return null;
|
|
178
|
+
|
|
179
|
+
// Read instruction files
|
|
180
|
+
var globalClaudeMd = readFileSafe(join(home, ".claude", "CLAUDE.md"));
|
|
181
|
+
var globalRulesContent = readDirFiles(join(home, ".claude", "rules"));
|
|
182
|
+
var projectClaudeMd = readFileSafe(join(projectPath, "CLAUDE.md"));
|
|
183
|
+
var projectLocalClaudeMd = readFileSafe(join(home, ".claude", "projects", hash, "CLAUDE.md"));
|
|
184
|
+
|
|
185
|
+
var memoryContent = readDirFiles(join(home, ".claude", "projects", hash, "memory"));
|
|
186
|
+
var memoryIndex = readFileSafe(join(home, ".claude", "projects", hash, "MEMORY.md"));
|
|
187
|
+
|
|
188
|
+
var instructionsTokens = countTokens(globalClaudeMd + globalRulesContent + projectClaudeMd + projectLocalClaudeMd);
|
|
189
|
+
var memoryTokens = countTokens(memoryContent + memoryIndex);
|
|
190
|
+
|
|
191
|
+
// Parse session
|
|
192
|
+
var content = readFileSync(sessionFile, "utf-8");
|
|
193
|
+
var lines = content.trim().split("\n");
|
|
194
|
+
|
|
195
|
+
// Extract tool info
|
|
196
|
+
var toolCounts = extractToolsFromSession(lines);
|
|
197
|
+
var builtinToolTokens = estimateBuiltinToolTokens(toolCounts.builtinTools);
|
|
198
|
+
|
|
199
|
+
// Parse conversation messages
|
|
200
|
+
var userText = "";
|
|
201
|
+
var assistantText = "";
|
|
202
|
+
var toolResultText = "";
|
|
203
|
+
var contextWindow = 0;
|
|
204
|
+
var lastModel = "";
|
|
205
|
+
|
|
206
|
+
for (var i = 0; i < lines.length; i++) {
|
|
207
|
+
var line = lines[i].trim();
|
|
208
|
+
if (!line) continue;
|
|
209
|
+
try {
|
|
210
|
+
var parsed = JSON.parse(line);
|
|
211
|
+
if (parsed.type === "user" && parsed.message) {
|
|
212
|
+
var userContent = parsed.message.content;
|
|
213
|
+
if (typeof userContent === "string") {
|
|
214
|
+
userText += userContent + "\n";
|
|
215
|
+
} else if (Array.isArray(userContent)) {
|
|
216
|
+
for (var j = 0; j < userContent.length; j++) {
|
|
217
|
+
var block = userContent[j];
|
|
218
|
+
if (block.type === "text" && block.text) {
|
|
219
|
+
userText += block.text + "\n";
|
|
220
|
+
} else if (block.type === "tool_result") {
|
|
221
|
+
if (typeof block.content === "string") {
|
|
222
|
+
toolResultText += block.content + "\n";
|
|
223
|
+
} else if (Array.isArray(block.content)) {
|
|
224
|
+
for (var ri = 0; ri < block.content.length; ri++) {
|
|
225
|
+
if (block.content[ri].type === "text" && block.content[ri].text) {
|
|
226
|
+
toolResultText += block.content[ri].text + "\n";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (parsed.type === "assistant" && parsed.message) {
|
|
234
|
+
var aContent = parsed.message.content;
|
|
235
|
+
if (typeof aContent === "string") {
|
|
236
|
+
assistantText += aContent + "\n";
|
|
237
|
+
} else if (Array.isArray(aContent)) {
|
|
238
|
+
for (var k = 0; k < aContent.length; k++) {
|
|
239
|
+
var ab = aContent[k];
|
|
240
|
+
if (ab.type === "text" && ab.text) {
|
|
241
|
+
assistantText += ab.text + "\n";
|
|
242
|
+
} else if (ab.type === "tool_use" && ab.input) {
|
|
243
|
+
assistantText += JSON.stringify(ab.input) + "\n";
|
|
244
|
+
} else if (ab.type === "thinking" && ab.thinking) {
|
|
245
|
+
assistantText += ab.thinking + "\n";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (parsed.message.model) {
|
|
250
|
+
lastModel = parsed.message.model;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
var userTokens = countTokens(userText);
|
|
257
|
+
var assistantTokens = countTokens(assistantText);
|
|
258
|
+
var toolResultTokens = countTokens(toolResultText);
|
|
259
|
+
contextWindow = guessContextWindow(lastModel);
|
|
260
|
+
|
|
261
|
+
var autocompactAt = Math.round(contextWindow * 0.9);
|
|
262
|
+
var systemPromptEstimate = 4500;
|
|
263
|
+
|
|
264
|
+
var segments: ContextBreakdownSegment[] = [
|
|
265
|
+
{ label: "System prompt", tokens: systemPromptEstimate, id: "system", estimated: true },
|
|
266
|
+
{ label: "Built-in tools (" + Object.keys(BUILTIN_TOOLS).length + ")", tokens: builtinToolTokens, id: "builtin_tools", estimated: true },
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Add per-MCP-server segments
|
|
270
|
+
toolCounts.mcpTools.forEach(function (tools, serverName) {
|
|
271
|
+
var mcpTokens = tools.length * MCP_TOOL_AVG_TOKENS;
|
|
272
|
+
segments.push({
|
|
273
|
+
label: serverName + " (" + tools.length + " tools)",
|
|
274
|
+
tokens: mcpTokens,
|
|
275
|
+
id: "mcp_" + serverName,
|
|
276
|
+
estimated: true,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
segments.push(
|
|
281
|
+
{ label: "Instructions", tokens: instructionsTokens, id: "instructions", estimated: false },
|
|
282
|
+
{ label: "Memory", tokens: memoryTokens, id: "memory", estimated: false },
|
|
283
|
+
{ label: "Your messages", tokens: userTokens, id: "user", estimated: false },
|
|
284
|
+
{ label: "Claude responses", tokens: assistantTokens, id: "assistant", estimated: false },
|
|
285
|
+
{ label: "Tool results", tokens: toolResultTokens, id: "tool_results", estimated: false },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return { segments, contextWindow, autocompactAt };
|
|
289
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve, relative } from "node:path";
|
|
3
|
+
import type { FileEntry } from "@lattice/shared";
|
|
4
|
+
|
|
5
|
+
var MAX_FILE_SIZE = 512 * 1024;
|
|
6
|
+
|
|
7
|
+
export function validatePath(projectPath: string, relativePath: string): string | null {
|
|
8
|
+
var resolved = resolve(projectPath, relativePath);
|
|
9
|
+
var normalizedRoot = resolve(projectPath);
|
|
10
|
+
if (!resolved.startsWith(normalizedRoot + "/") && resolved !== normalizedRoot) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function listDirectory(projectPath: string, relativePath: string): FileEntry[] {
|
|
17
|
+
var fullPath = validatePath(projectPath, relativePath);
|
|
18
|
+
if (!fullPath) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var entries: FileEntry[] = [];
|
|
23
|
+
var names: string[];
|
|
24
|
+
try {
|
|
25
|
+
names = readdirSync(fullPath);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (var i = 0; i < names.length; i++) {
|
|
31
|
+
var name = names[i];
|
|
32
|
+
if (name.startsWith(".")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
var entryFull = join(fullPath, name);
|
|
36
|
+
try {
|
|
37
|
+
var stat = statSync(entryFull);
|
|
38
|
+
var entryRelative = relative(projectPath, entryFull);
|
|
39
|
+
entries.push({
|
|
40
|
+
name: name,
|
|
41
|
+
path: entryRelative,
|
|
42
|
+
isDirectory: stat.isDirectory(),
|
|
43
|
+
size: stat.isDirectory() ? 0 : stat.size,
|
|
44
|
+
modifiedAt: stat.mtimeMs,
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
// skip entries we can't stat
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
entries.sort(function (a, b) {
|
|
52
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
53
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
54
|
+
return a.name.localeCompare(b.name);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readFile(projectPath: string, relativePath: string): string | null {
|
|
61
|
+
var fullPath = validatePath(projectPath, relativePath);
|
|
62
|
+
if (!fullPath) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var stat: ReturnType<typeof statSync>;
|
|
67
|
+
try {
|
|
68
|
+
stat = statSync(fullPath);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var buf: Buffer;
|
|
78
|
+
try {
|
|
79
|
+
buf = readFileSync(fullPath);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (var i = 0; i < Math.min(buf.length, 8192); i++) {
|
|
85
|
+
var byte = buf[i];
|
|
86
|
+
if (byte === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return buf.toString("utf-8");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function writeFile(projectPath: string, relativePath: string, content: string): boolean {
|
|
95
|
+
var fullPath = validatePath(projectPath, relativePath);
|
|
96
|
+
if (!fullPath) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export function readProjectClaudeMd(projectPath: string): string {
|
|
6
|
+
var filePath = join(projectPath, "CLAUDE.md");
|
|
7
|
+
if (!existsSync(filePath)) return "";
|
|
8
|
+
try {
|
|
9
|
+
return readFileSync(filePath, "utf-8");
|
|
10
|
+
} catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function writeProjectClaudeMd(projectPath: string, content: string): void {
|
|
16
|
+
writeFileSync(join(projectPath, "CLAUDE.md"), content, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readProjectClaudeSettings(projectPath: string): Record<string, unknown> {
|
|
20
|
+
var filePath = join(projectPath, ".claude", "settings.json");
|
|
21
|
+
if (!existsSync(filePath)) return {};
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mergeProjectClaudeSettings(projectPath: string, updates: Record<string, unknown>): void {
|
|
30
|
+
var existing = readProjectClaudeSettings(projectPath);
|
|
31
|
+
Object.assign(existing, updates);
|
|
32
|
+
var dir = join(projectPath, ".claude");
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
writeFileSync(join(dir, "settings.json"), JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readProjectRules(projectPath: string): Array<{ filename: string; content: string }> {
|
|
38
|
+
var dir = join(projectPath, ".claude", "rules");
|
|
39
|
+
if (!existsSync(dir)) return [];
|
|
40
|
+
var results: Array<{ filename: string; content: string }> = [];
|
|
41
|
+
try {
|
|
42
|
+
var files = readdirSync(dir);
|
|
43
|
+
for (var file of files) {
|
|
44
|
+
if (!file.endsWith(".md")) continue;
|
|
45
|
+
try {
|
|
46
|
+
var content = readFileSync(join(dir, file), "utf-8");
|
|
47
|
+
results.push({ filename: file, content });
|
|
48
|
+
} catch {
|
|
49
|
+
// skip unreadable files
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeProjectRules(projectPath: string, rules: Array<{ filename: string; content: string }>): void {
|
|
59
|
+
var dir = join(projectPath, ".claude", "rules");
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
|
|
62
|
+
var incoming = new Set<string>();
|
|
63
|
+
for (var rule of rules) {
|
|
64
|
+
incoming.add(rule.filename);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
var existing = readdirSync(dir);
|
|
69
|
+
for (var file of existing) {
|
|
70
|
+
if (file.endsWith(".md") && !incoming.has(file)) {
|
|
71
|
+
unlinkSync(join(dir, file));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// dir just created, nothing to delete
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (var rule of rules) {
|
|
79
|
+
writeFileSync(join(dir, rule.filename), rule.content, "utf-8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readProjectMcpServers(projectPath: string): Record<string, unknown> {
|
|
84
|
+
var filePath = join(projectPath, ".mcp.json");
|
|
85
|
+
if (!existsSync(filePath)) return {};
|
|
86
|
+
try {
|
|
87
|
+
var parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
88
|
+
return parsed?.mcpServers ?? {};
|
|
89
|
+
} catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function writeProjectMcpServers(projectPath: string, servers: Record<string, unknown>): void {
|
|
95
|
+
var filePath = join(projectPath, ".mcp.json");
|
|
96
|
+
writeFileSync(filePath, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf-8");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function readProjectSkills(projectPath: string): Array<{ name: string; description: string; path: string }> {
|
|
100
|
+
return scanSkillsDir(join(projectPath, ".claude", "skills"));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function readGlobalRules(): Array<{ filename: string; content: string }> {
|
|
104
|
+
var dir = join(homedir(), ".claude", "rules");
|
|
105
|
+
if (!existsSync(dir)) return [];
|
|
106
|
+
var results: Array<{ filename: string; content: string }> = [];
|
|
107
|
+
try {
|
|
108
|
+
var files = readdirSync(dir);
|
|
109
|
+
for (var file of files) {
|
|
110
|
+
if (!file.endsWith(".md")) continue;
|
|
111
|
+
try {
|
|
112
|
+
var content = readFileSync(join(dir, file), "utf-8");
|
|
113
|
+
results.push({ filename: file, content });
|
|
114
|
+
} catch {
|
|
115
|
+
// skip unreadable files
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function readGlobalPermissions(): { allow: string[]; deny: string[] } {
|
|
125
|
+
var settings = readJsonFile(join(homedir(), ".claude", "settings.json"));
|
|
126
|
+
var local = readJsonFile(join(homedir(), ".claude", "settings.local.json"));
|
|
127
|
+
|
|
128
|
+
var allow = new Set<string>();
|
|
129
|
+
var deny = new Set<string>();
|
|
130
|
+
|
|
131
|
+
collectPermissions(settings, allow, deny);
|
|
132
|
+
collectPermissions(local, allow, deny);
|
|
133
|
+
|
|
134
|
+
return { allow: Array.from(allow), deny: Array.from(deny) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readGlobalMcpServers(): Record<string, unknown> {
|
|
138
|
+
var filePath = join(homedir(), ".claude.json");
|
|
139
|
+
if (!existsSync(filePath)) return {};
|
|
140
|
+
try {
|
|
141
|
+
var parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
142
|
+
return parsed?.mcpServers ?? {};
|
|
143
|
+
} catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function writeGlobalMcpServers(servers: Record<string, unknown>): void {
|
|
149
|
+
var filePath = join(homedir(), ".claude.json");
|
|
150
|
+
var existing: Record<string, unknown> = {};
|
|
151
|
+
if (existsSync(filePath)) {
|
|
152
|
+
try {
|
|
153
|
+
existing = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
existing.mcpServers = servers;
|
|
157
|
+
writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readGlobalSkills(): Array<{ name: string; description: string; path: string }> {
|
|
161
|
+
var results: Array<{ name: string; description: string; path: string }> = [];
|
|
162
|
+
var dirs = [
|
|
163
|
+
join(homedir(), ".claude", "skills"),
|
|
164
|
+
join(homedir(), ".agents", "skills"),
|
|
165
|
+
];
|
|
166
|
+
for (var dir of dirs) {
|
|
167
|
+
var skills = scanSkillsDir(dir);
|
|
168
|
+
for (var skill of skills) {
|
|
169
|
+
results.push(skill);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readJsonFile(filePath: string): Record<string, unknown> {
|
|
176
|
+
if (!existsSync(filePath)) return {};
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
179
|
+
} catch {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function collectPermissions(obj: Record<string, unknown>, allow: Set<string>, deny: Set<string>): void {
|
|
185
|
+
var perms = obj.permissions as Record<string, unknown> | undefined;
|
|
186
|
+
if (!perms) return;
|
|
187
|
+
var allowArr = perms.allow;
|
|
188
|
+
var denyArr = perms.deny;
|
|
189
|
+
if (Array.isArray(allowArr)) {
|
|
190
|
+
for (var item of allowArr) {
|
|
191
|
+
if (typeof item === "string") allow.add(item);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(denyArr)) {
|
|
195
|
+
for (var item of denyArr) {
|
|
196
|
+
if (typeof item === "string") deny.add(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseFrontmatter(content: string): { name: string; description: string } {
|
|
202
|
+
var match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
203
|
+
if (!match) return { name: "", description: "" };
|
|
204
|
+
var yaml = match[1];
|
|
205
|
+
var name = "";
|
|
206
|
+
var desc = "";
|
|
207
|
+
var lines = yaml.split(/\r?\n/);
|
|
208
|
+
for (var i = 0; i < lines.length; i++) {
|
|
209
|
+
var line = lines[i];
|
|
210
|
+
var nameMatch = line.match(/^name:\s*(.+)/);
|
|
211
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
212
|
+
var descMatch = line.match(/^description:\s*(.+)/);
|
|
213
|
+
if (descMatch) desc = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
214
|
+
}
|
|
215
|
+
return { name, description: desc };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function scanSkillsDir(dir: string): Array<{ name: string; description: string; path: string }> {
|
|
219
|
+
if (!existsSync(dir)) return [];
|
|
220
|
+
var results: Array<{ name: string; description: string; path: string }> = [];
|
|
221
|
+
try {
|
|
222
|
+
var entries = readdirSync(dir, { withFileTypes: true });
|
|
223
|
+
for (var entry of entries) {
|
|
224
|
+
var entryPath = join(dir, entry.name);
|
|
225
|
+
if (entry.isDirectory()) {
|
|
226
|
+
try {
|
|
227
|
+
var subFiles = readdirSync(entryPath);
|
|
228
|
+
for (var subFile of subFiles) {
|
|
229
|
+
if (subFile.toUpperCase().startsWith("SKILL")) {
|
|
230
|
+
var skillPath = join(entryPath, subFile);
|
|
231
|
+
var content = readFileSync(skillPath, "utf-8");
|
|
232
|
+
var meta = parseFrontmatter(content);
|
|
233
|
+
var skillName = meta.name || entry.name;
|
|
234
|
+
var description = meta.description || firstNonEmptyLine(content.replace(/^---[\s\S]*?---\s*/, ""));
|
|
235
|
+
results.push({ name: skillName, description, path: skillPath });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// skip unreadable subdirectories
|
|
241
|
+
}
|
|
242
|
+
} else if (entry.name.toUpperCase().startsWith("SKILL")) {
|
|
243
|
+
try {
|
|
244
|
+
var content = readFileSync(entryPath, "utf-8");
|
|
245
|
+
var meta = parseFrontmatter(content);
|
|
246
|
+
var name = meta.name || entry.name.replace(/\.[^.]+$/, "");
|
|
247
|
+
var description = meta.description || firstNonEmptyLine(content.replace(/^---[\s\S]*?---\s*/, ""));
|
|
248
|
+
results.push({ name, description, path: entryPath });
|
|
249
|
+
} catch {
|
|
250
|
+
// skip unreadable files
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function firstNonEmptyLine(text: string): string {
|
|
261
|
+
var lines = text.split("\n");
|
|
262
|
+
for (var line of lines) {
|
|
263
|
+
var trimmed = line.trim();
|
|
264
|
+
if (trimmed.length > 0) return trimmed;
|
|
265
|
+
}
|
|
266
|
+
return "";
|
|
267
|
+
}
|