@cryptiklemur/lattice 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/bun.lock +776 -2
  2. package/client/index.html +1 -13
  3. package/client/package.json +7 -1
  4. package/client/src/App.tsx +2 -0
  5. package/client/src/commands.ts +36 -0
  6. package/client/src/components/analytics/AnalyticsView.tsx +61 -0
  7. package/client/src/components/analytics/ChartCard.tsx +22 -0
  8. package/client/src/components/analytics/PeriodSelector.tsx +42 -0
  9. package/client/src/components/analytics/QuickStats.tsx +99 -0
  10. package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
  11. package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
  12. package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
  13. package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
  14. package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
  15. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  16. package/client/src/components/chat/ChatInput.tsx +250 -73
  17. package/client/src/components/chat/ChatView.tsx +242 -10
  18. package/client/src/components/chat/CommandPalette.tsx +162 -0
  19. package/client/src/components/chat/Message.tsx +23 -2
  20. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  21. package/client/src/components/chat/TodoCard.tsx +57 -0
  22. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  23. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  24. package/client/src/components/dashboard/DashboardView.tsx +5 -0
  25. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  26. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  27. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  28. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  29. package/client/src/components/settings/Appearance.tsx +1 -0
  30. package/client/src/components/settings/ClaudeSettings.tsx +10 -0
  31. package/client/src/components/settings/Editor.tsx +123 -0
  32. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  33. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  34. package/client/src/components/settings/GlobalRules.tsx +149 -0
  35. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  36. package/client/src/components/settings/Notifications.tsx +88 -0
  37. package/client/src/components/settings/SettingsView.tsx +12 -0
  38. package/client/src/components/settings/skill-shared.tsx +2 -1
  39. package/client/src/components/setup/SetupWizard.tsx +1 -1
  40. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  41. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  42. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  43. package/client/src/components/sidebar/Sidebar.tsx +43 -2
  44. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  45. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  46. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  47. package/client/src/components/workspace/FileTree.tsx +129 -0
  48. package/client/src/components/workspace/FileViewer.tsx +211 -0
  49. package/client/src/components/workspace/NoteCard.tsx +119 -0
  50. package/client/src/components/workspace/NotesView.tsx +102 -0
  51. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  52. package/client/src/components/workspace/SplitPane.tsx +81 -0
  53. package/client/src/components/workspace/TabBar.tsx +185 -0
  54. package/client/src/components/workspace/TaskCard.tsx +158 -0
  55. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  56. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  57. package/client/src/components/workspace/TerminalView.tsx +110 -0
  58. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  59. package/client/src/hooks/useAnalytics.ts +75 -0
  60. package/client/src/hooks/useAttachments.ts +280 -0
  61. package/client/src/hooks/useEditorConfig.ts +28 -0
  62. package/client/src/hooks/useIdleDetection.ts +44 -0
  63. package/client/src/hooks/useInstallPrompt.ts +53 -0
  64. package/client/src/hooks/useNotifications.ts +54 -0
  65. package/client/src/hooks/useOnline.ts +6 -0
  66. package/client/src/hooks/useSession.ts +110 -4
  67. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  68. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  69. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  70. package/client/src/hooks/useWorkspace.ts +48 -0
  71. package/client/src/providers/WebSocketProvider.tsx +18 -0
  72. package/client/src/router.tsx +52 -20
  73. package/client/src/stores/analytics.ts +54 -0
  74. package/client/src/stores/session.ts +136 -0
  75. package/client/src/stores/sidebar.ts +11 -2
  76. package/client/src/stores/workspace.ts +254 -0
  77. package/client/src/styles/global.css +123 -0
  78. package/client/src/utils/editorUrl.ts +62 -0
  79. package/client/vite.config.ts +54 -1
  80. package/package.json +1 -1
  81. package/server/src/analytics/engine.ts +491 -0
  82. package/server/src/daemon.ts +12 -1
  83. package/server/src/features/scheduler.ts +23 -0
  84. package/server/src/features/sticky-notes.ts +5 -3
  85. package/server/src/handlers/analytics.ts +34 -0
  86. package/server/src/handlers/attachment.ts +172 -0
  87. package/server/src/handlers/chat.ts +43 -2
  88. package/server/src/handlers/editor.ts +40 -0
  89. package/server/src/handlers/fs.ts +10 -2
  90. package/server/src/handlers/memory.ts +3 -0
  91. package/server/src/handlers/notes.ts +4 -2
  92. package/server/src/handlers/scheduler.ts +18 -1
  93. package/server/src/handlers/session.ts +14 -8
  94. package/server/src/handlers/settings.ts +37 -2
  95. package/server/src/handlers/terminal.ts +13 -6
  96. package/server/src/project/pty-worker.cjs +83 -0
  97. package/server/src/project/sdk-bridge.ts +266 -11
  98. package/server/src/project/session.ts +4 -4
  99. package/server/src/project/terminal.ts +78 -34
  100. package/shared/src/analytics.ts +24 -0
  101. package/shared/src/index.ts +1 -0
  102. package/shared/src/messages.ts +173 -4
  103. package/shared/src/models.ts +27 -1
  104. package/shared/src/project-settings.ts +1 -1
  105. package/tp.js +19 -0
  106. package/client/public/manifest.json +0 -24
  107. package/client/public/sw.js +0 -61
  108. package/client/src/components/panels/FileBrowser.tsx +0 -241
  109. package/client/src/components/panels/StickyNotes.tsx +0 -187
@@ -0,0 +1,491 @@
1
+ import { readdirSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { AnalyticsPayload, AnalyticsPeriod, AnalyticsScope } from "@lattice/shared";
5
+ import { estimateCost, projectPathToHash } from "../project/session";
6
+ import { loadConfig } from "../config";
7
+
8
+ interface SessionData {
9
+ id: string;
10
+ title: string;
11
+ project: string;
12
+ cost: number;
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ cacheReadTokens: number;
16
+ cacheCreationTokens: number;
17
+ models: Map<string, { cost: number; tokens: number }>;
18
+ tools: Map<string, number>;
19
+ startTime: number;
20
+ endTime: number;
21
+ }
22
+
23
+ interface CacheEntry {
24
+ data: AnalyticsPayload;
25
+ timestamp: number;
26
+ }
27
+
28
+ var cache = new Map<string, CacheEntry>();
29
+ var CACHE_TTL = 5 * 60 * 1000;
30
+
31
+ function bucketModel(model: string): "opus" | "sonnet" | "haiku" | "other" {
32
+ if (model.includes("opus")) return "opus";
33
+ if (model.includes("haiku")) return "haiku";
34
+ if (model.includes("sonnet")) return "sonnet";
35
+ return "other";
36
+ }
37
+
38
+ function getPeriodCutoff(period: AnalyticsPeriod): number {
39
+ if (period === "all") return 0;
40
+ var now = Date.now();
41
+ var hours: Record<string, number> = { "24h": 24, "7d": 168, "30d": 720, "90d": 2160 };
42
+ return now - (hours[period] || 0) * 60 * 60 * 1000;
43
+ }
44
+
45
+ function formatDate(ts: number): string {
46
+ var d = new Date(ts);
47
+ var year = d.getFullYear();
48
+ var month = String(d.getMonth() + 1).padStart(2, "0");
49
+ var day = String(d.getDate()).padStart(2, "0");
50
+ return year + "-" + month + "-" + day;
51
+ }
52
+
53
+ function getCostBucket(cost: number): string {
54
+ if (cost < 0.01) return "$0-0.01";
55
+ if (cost < 0.05) return "$0.01-0.05";
56
+ if (cost < 0.10) return "$0.05-0.10";
57
+ if (cost < 0.50) return "$0.10-0.50";
58
+ if (cost < 1.00) return "$0.50-1.00";
59
+ if (cost < 5.00) return "$1.00-5.00";
60
+ return "$5.00+";
61
+ }
62
+
63
+ function parseSessionFile(filePath: string, sessionId: string, projectSlug: string): SessionData | null {
64
+ try {
65
+ var text: string;
66
+ try {
67
+ text = require("node:fs").readFileSync(filePath, "utf-8");
68
+ } catch {
69
+ return null;
70
+ }
71
+
72
+ var lines = text.split("\n");
73
+ var data: SessionData = {
74
+ id: sessionId,
75
+ title: "",
76
+ project: projectSlug,
77
+ cost: 0,
78
+ inputTokens: 0,
79
+ outputTokens: 0,
80
+ cacheReadTokens: 0,
81
+ cacheCreationTokens: 0,
82
+ models: new Map(),
83
+ tools: new Map(),
84
+ startTime: 0,
85
+ endTime: 0,
86
+ };
87
+
88
+ for (var i = 0; i < lines.length; i++) {
89
+ var line = lines[i].trim();
90
+ if (!line) continue;
91
+
92
+ var parsed: Record<string, unknown>;
93
+ try {
94
+ parsed = JSON.parse(line);
95
+ } catch {
96
+ continue;
97
+ }
98
+
99
+ var timestamp = 0;
100
+ if (typeof parsed.timestamp === "string") {
101
+ var ts = new Date(parsed.timestamp as string).getTime();
102
+ if (!isNaN(ts)) timestamp = ts;
103
+ }
104
+
105
+ if (timestamp > 0) {
106
+ if (data.startTime === 0 || timestamp < data.startTime) data.startTime = timestamp;
107
+ if (timestamp > data.endTime) data.endTime = timestamp;
108
+ }
109
+
110
+ if (parsed.type === "assistant") {
111
+ var message = parsed.message as Record<string, unknown> | undefined;
112
+ if (!message) continue;
113
+
114
+ var usage = message.usage as Record<string, number> | undefined;
115
+ var model = (message.model as string) || "";
116
+
117
+ if (usage) {
118
+ var inTok = usage.input_tokens || 0;
119
+ var outTok = usage.output_tokens || 0;
120
+ var cacheRead = usage.cache_read_input_tokens || 0;
121
+ var cacheCreation = usage.cache_creation_input_tokens || 0;
122
+
123
+ data.inputTokens += inTok;
124
+ data.outputTokens += outTok;
125
+ data.cacheReadTokens += cacheRead;
126
+ data.cacheCreationTokens += cacheCreation;
127
+
128
+ var cost = estimateCost(model, inTok, outTok, cacheRead, cacheCreation);
129
+ data.cost += cost;
130
+
131
+ var bucket = bucketModel(model);
132
+ var existing = data.models.get(bucket);
133
+ if (existing) {
134
+ existing.cost += cost;
135
+ existing.tokens += inTok + outTok;
136
+ } else {
137
+ data.models.set(bucket, { cost: cost, tokens: inTok + outTok });
138
+ }
139
+ }
140
+
141
+ if (!data.title && message.content) {
142
+ if (typeof message.content === "string" && message.content.length > 0) {
143
+ data.title = message.content.slice(0, 80);
144
+ }
145
+ }
146
+ } else if (parsed.type === "user") {
147
+ var userMsg = parsed.message as Record<string, unknown> | undefined;
148
+ if (!userMsg || !Array.isArray(userMsg.content)) continue;
149
+
150
+ var contentArr = userMsg.content as Array<Record<string, unknown>>;
151
+ for (var j = 0; j < contentArr.length; j++) {
152
+ var block = contentArr[j];
153
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
154
+ var toolName = (block.name as string) || "unknown";
155
+ data.tools.set(toolName, (data.tools.get(toolName) || 0) + 1);
156
+ }
157
+ }
158
+
159
+ if (!data.title && Array.isArray(userMsg.content)) {
160
+ for (var k = 0; k < contentArr.length; k++) {
161
+ if (contentArr[k].type === "text" && typeof contentArr[k].text === "string") {
162
+ data.title = (contentArr[k].text as string).slice(0, 80);
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ if (!data.title) data.title = "Session " + sessionId.slice(0, 8);
171
+
172
+ return data;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function getSessionFilesForProject(projectPath: string): Array<{ path: string; id: string }> {
179
+ var hash = projectPathToHash(projectPath);
180
+ var dir = join(homedir(), ".claude", "projects", hash);
181
+ if (!existsSync(dir)) return [];
182
+
183
+ var files: Array<{ path: string; id: string }> = [];
184
+ try {
185
+ var entries = readdirSync(dir);
186
+ for (var i = 0; i < entries.length; i++) {
187
+ if (entries[i].endsWith(".jsonl")) {
188
+ files.push({
189
+ path: join(dir, entries[i]),
190
+ id: entries[i].replace(".jsonl", ""),
191
+ });
192
+ }
193
+ }
194
+ } catch {
195
+ return [];
196
+ }
197
+ return files;
198
+ }
199
+
200
+ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsPayload {
201
+ var cutoff = getPeriodCutoff(period);
202
+ var filtered: SessionData[] = [];
203
+ for (var i = 0; i < sessions.length; i++) {
204
+ var s = sessions[i];
205
+ var sessionTime = s.endTime > 0 ? s.endTime : s.startTime;
206
+ if (sessionTime >= cutoff) filtered.push(s);
207
+ }
208
+
209
+ var totalCost = 0;
210
+ var totalInput = 0;
211
+ var totalOutput = 0;
212
+ var totalCacheRead = 0;
213
+ var totalCacheCreation = 0;
214
+ var totalDuration = 0;
215
+ var durationCount = 0;
216
+
217
+ var dailyCost = new Map<string, { total: number; opus: number; sonnet: number; haiku: number; other: number }>();
218
+ var dailySessions = new Map<string, number>();
219
+ var dailyTokens = new Map<string, { input: number; output: number; cacheRead: number }>();
220
+ var dailyCacheHit = new Map<string, { cacheRead: number; totalInput: number }>();
221
+
222
+ var modelStats = new Map<string, { sessions: number; cost: number; tokens: number }>();
223
+ var projectStats = new Map<string, { cost: number; sessions: number; tokens: number }>();
224
+ var toolStats = new Map<string, { count: number; totalCost: number; sessions: number }>();
225
+
226
+ var costBuckets = new Map<string, number>();
227
+ var bucketOrder = ["$0-0.01", "$0.01-0.05", "$0.05-0.10", "$0.10-0.50", "$0.50-1.00", "$1.00-5.00", "$5.00+"];
228
+ for (var b = 0; b < bucketOrder.length; b++) {
229
+ costBuckets.set(bucketOrder[b], 0);
230
+ }
231
+
232
+ for (var si = 0; si < filtered.length; si++) {
233
+ var sess = filtered[si];
234
+ totalCost += sess.cost;
235
+ totalInput += sess.inputTokens;
236
+ totalOutput += sess.outputTokens;
237
+ totalCacheRead += sess.cacheReadTokens;
238
+ totalCacheCreation += sess.cacheCreationTokens;
239
+
240
+ if (sess.startTime > 0 && sess.endTime > 0 && sess.endTime > sess.startTime) {
241
+ totalDuration += sess.endTime - sess.startTime;
242
+ durationCount++;
243
+ }
244
+
245
+ var date = formatDate(sess.endTime > 0 ? sess.endTime : sess.startTime);
246
+
247
+ var dc = dailyCost.get(date);
248
+ if (!dc) {
249
+ dc = { total: 0, opus: 0, sonnet: 0, haiku: 0, other: 0 };
250
+ dailyCost.set(date, dc);
251
+ }
252
+ dc.total += sess.cost;
253
+ sess.models.forEach(function (val, key) {
254
+ dc![key as "opus" | "sonnet" | "haiku" | "other"] += val.cost;
255
+ });
256
+
257
+ dailySessions.set(date, (dailySessions.get(date) || 0) + 1);
258
+
259
+ var dt = dailyTokens.get(date);
260
+ if (!dt) {
261
+ dt = { input: 0, output: 0, cacheRead: 0 };
262
+ dailyTokens.set(date, dt);
263
+ }
264
+ dt.input += sess.inputTokens;
265
+ dt.output += sess.outputTokens;
266
+ dt.cacheRead += sess.cacheReadTokens;
267
+
268
+ var dch = dailyCacheHit.get(date);
269
+ if (!dch) {
270
+ dch = { cacheRead: 0, totalInput: 0 };
271
+ dailyCacheHit.set(date, dch);
272
+ }
273
+ dch.cacheRead += sess.cacheReadTokens;
274
+ dch.totalInput += sess.inputTokens;
275
+
276
+ sess.models.forEach(function (val, key) {
277
+ var ms = modelStats.get(key);
278
+ if (!ms) {
279
+ ms = { sessions: 0, cost: 0, tokens: 0 };
280
+ modelStats.set(key, ms);
281
+ }
282
+ ms.sessions++;
283
+ ms.cost += val.cost;
284
+ ms.tokens += val.tokens;
285
+ });
286
+
287
+ var ps = projectStats.get(sess.project);
288
+ if (!ps) {
289
+ ps = { cost: 0, sessions: 0, tokens: 0 };
290
+ projectStats.set(sess.project, ps);
291
+ }
292
+ ps.cost += sess.cost;
293
+ ps.sessions++;
294
+ ps.tokens += sess.inputTokens + sess.outputTokens;
295
+
296
+ sess.tools.forEach(function (count, tool) {
297
+ var ts = toolStats.get(tool);
298
+ if (!ts) {
299
+ ts = { count: 0, totalCost: 0, sessions: 0 };
300
+ toolStats.set(tool, ts);
301
+ }
302
+ ts.count += count;
303
+ ts.totalCost += sess.cost;
304
+ ts.sessions++;
305
+ });
306
+
307
+ var bucket = getCostBucket(sess.cost);
308
+ costBuckets.set(bucket, (costBuckets.get(bucket) || 0) + 1);
309
+ }
310
+
311
+ var totalTokensAll = totalInput + totalOutput + totalCacheRead + totalCacheCreation;
312
+ var cacheHitRate = (totalInput + totalCacheRead) > 0 ? totalCacheRead / (totalInput + totalCacheRead) : 0;
313
+
314
+ var dates = Array.from(dailyCost.keys()).sort();
315
+
316
+ var costOverTime: AnalyticsPayload["costOverTime"] = [];
317
+ var cumulativeCost: AnalyticsPayload["cumulativeCost"] = [];
318
+ var sessionsOverTime: AnalyticsPayload["sessionsOverTime"] = [];
319
+ var tokensOverTime: AnalyticsPayload["tokensOverTime"] = [];
320
+ var cacheHitRateOverTime: AnalyticsPayload["cacheHitRateOverTime"] = [];
321
+
322
+ var cumTotal = 0;
323
+ for (var di = 0; di < dates.length; di++) {
324
+ var d = dates[di];
325
+ var dcEntry = dailyCost.get(d)!;
326
+ cumTotal += dcEntry.total;
327
+
328
+ costOverTime.push({
329
+ date: d,
330
+ total: dcEntry.total,
331
+ opus: dcEntry.opus,
332
+ sonnet: dcEntry.sonnet,
333
+ haiku: dcEntry.haiku,
334
+ other: dcEntry.other,
335
+ });
336
+ cumulativeCost.push({ date: d, total: cumTotal });
337
+ sessionsOverTime.push({ date: d, count: dailySessions.get(d) || 0 });
338
+
339
+ var dtEntry = dailyTokens.get(d);
340
+ tokensOverTime.push({
341
+ date: d,
342
+ input: dtEntry ? dtEntry.input : 0,
343
+ output: dtEntry ? dtEntry.output : 0,
344
+ cacheRead: dtEntry ? dtEntry.cacheRead : 0,
345
+ });
346
+
347
+ var dchEntry = dailyCacheHit.get(d);
348
+ var rate = dchEntry && (dchEntry.totalInput + dchEntry.cacheRead) > 0 ? dchEntry.cacheRead / (dchEntry.totalInput + dchEntry.cacheRead) : 0;
349
+ cacheHitRateOverTime.push({ date: d, rate: rate });
350
+ }
351
+
352
+ var costDistribution: AnalyticsPayload["costDistribution"] = [];
353
+ for (var bi = 0; bi < bucketOrder.length; bi++) {
354
+ costDistribution.push({
355
+ bucket: bucketOrder[bi],
356
+ count: costBuckets.get(bucketOrder[bi]) || 0,
357
+ });
358
+ }
359
+
360
+ var sessionBubbles: AnalyticsPayload["sessionBubbles"] = [];
361
+ var sorted = filtered.slice().sort(function (a, b) {
362
+ return (b.endTime || b.startTime) - (a.endTime || a.startTime);
363
+ });
364
+ var bubbleCap = Math.min(sorted.length, 200);
365
+ for (var sbi = 0; sbi < bubbleCap; sbi++) {
366
+ var sb = sorted[sbi];
367
+ sessionBubbles.push({
368
+ id: sb.id,
369
+ title: sb.title,
370
+ cost: sb.cost,
371
+ tokens: sb.inputTokens + sb.outputTokens,
372
+ timestamp: sb.endTime > 0 ? sb.endTime : sb.startTime,
373
+ project: sb.project,
374
+ });
375
+ }
376
+
377
+ var modelUsage: AnalyticsPayload["modelUsage"] = [];
378
+ var totalModelCost = totalCost || 1;
379
+ modelStats.forEach(function (val, key) {
380
+ modelUsage.push({
381
+ model: key,
382
+ sessions: val.sessions,
383
+ cost: val.cost,
384
+ tokens: val.tokens,
385
+ percentage: (val.cost / totalModelCost) * 100,
386
+ });
387
+ });
388
+ modelUsage.sort(function (a, b) { return b.cost - a.cost; });
389
+
390
+ var projectBreakdown: AnalyticsPayload["projectBreakdown"] = [];
391
+ projectStats.forEach(function (val, key) {
392
+ projectBreakdown.push({
393
+ project: key,
394
+ cost: val.cost,
395
+ sessions: val.sessions,
396
+ tokens: val.tokens,
397
+ });
398
+ });
399
+ projectBreakdown.sort(function (a, b) { return b.cost - a.cost; });
400
+
401
+ var toolUsage: AnalyticsPayload["toolUsage"] = [];
402
+ toolStats.forEach(function (val, key) {
403
+ toolUsage.push({
404
+ tool: key,
405
+ count: val.count,
406
+ avgCost: val.sessions > 0 ? val.totalCost / val.sessions : 0,
407
+ });
408
+ });
409
+ toolUsage.sort(function (a, b) { return b.count - a.count; });
410
+
411
+ return {
412
+ totalCost: totalCost,
413
+ totalSessions: filtered.length,
414
+ totalTokens: {
415
+ input: totalInput,
416
+ output: totalOutput,
417
+ cacheRead: totalCacheRead,
418
+ cacheCreation: totalCacheCreation,
419
+ },
420
+ cacheHitRate: cacheHitRate,
421
+ avgSessionCost: filtered.length > 0 ? totalCost / filtered.length : 0,
422
+ avgSessionDuration: durationCount > 0 ? totalDuration / durationCount : 0,
423
+ costOverTime: costOverTime,
424
+ cumulativeCost: cumulativeCost,
425
+ sessionsOverTime: sessionsOverTime,
426
+ tokensOverTime: tokensOverTime,
427
+ cacheHitRateOverTime: cacheHitRateOverTime,
428
+ costDistribution: costDistribution,
429
+ sessionBubbles: sessionBubbles,
430
+ modelUsage: modelUsage,
431
+ projectBreakdown: projectBreakdown,
432
+ toolUsage: toolUsage,
433
+ };
434
+ }
435
+
436
+ export function getAnalytics(
437
+ scope: AnalyticsScope,
438
+ period: AnalyticsPeriod,
439
+ projectSlug?: string,
440
+ sessionId?: string,
441
+ forceRefresh?: boolean,
442
+ ): Promise<AnalyticsPayload> {
443
+ var cacheKey = scope + ":" + period + ":" + (projectSlug || "all");
444
+ if (sessionId) cacheKey += ":" + sessionId;
445
+
446
+ if (!forceRefresh) {
447
+ var cached = cache.get(cacheKey);
448
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
449
+ return Promise.resolve(cached.data);
450
+ }
451
+ }
452
+
453
+ var sessions: SessionData[] = [];
454
+ var config = loadConfig();
455
+
456
+ if (scope === "global") {
457
+ for (var i = 0; i < config.projects.length; i++) {
458
+ var proj = config.projects[i];
459
+ var files = getSessionFilesForProject(proj.path);
460
+ for (var j = 0; j < files.length; j++) {
461
+ var data = parseSessionFile(files[j].path, files[j].id, proj.slug);
462
+ if (data) sessions.push(data);
463
+ }
464
+ }
465
+ } else if (scope === "project" && projectSlug) {
466
+ var project = config.projects.find(function (p) { return p.slug === projectSlug; });
467
+ if (project) {
468
+ var projFiles = getSessionFilesForProject(project.path);
469
+ for (var pf = 0; pf < projFiles.length; pf++) {
470
+ var pData = parseSessionFile(projFiles[pf].path, projFiles[pf].id, projectSlug);
471
+ if (pData) sessions.push(pData);
472
+ }
473
+ }
474
+ } else if (scope === "session" && projectSlug && sessionId) {
475
+ var sessProject = config.projects.find(function (p) { return p.slug === projectSlug; });
476
+ if (sessProject) {
477
+ var hash = projectPathToHash(sessProject.path);
478
+ var filePath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
479
+ if (existsSync(filePath)) {
480
+ var sData = parseSessionFile(filePath, sessionId, projectSlug);
481
+ if (sData) sessions.push(sData);
482
+ }
483
+ }
484
+ }
485
+
486
+ var result = aggregate(sessions, period);
487
+
488
+ cache.set(cacheKey, { data: result, timestamp: Date.now() });
489
+
490
+ return Promise.resolve(result);
491
+ }
@@ -14,7 +14,9 @@ import { ensureCerts } from "./tls";
14
14
  import type { ClientMessage, MeshMessage } from "@lattice/shared";
15
15
  import "./handlers/session";
16
16
  import "./handlers/chat";
17
- import { loadInterruptedSessions } from "./project/sdk-bridge";
17
+ import "./handlers/attachment";
18
+ import { loadInterruptedSessions, unwatchSessionLock } from "./project/sdk-bridge";
19
+ import { clearActiveSession, getActiveSession } from "./handlers/chat";
18
20
  import "./handlers/fs";
19
21
  import "./handlers/terminal";
20
22
  import "./handlers/settings";
@@ -25,9 +27,12 @@ import "./handlers/scheduler";
25
27
  import "./handlers/notes";
26
28
  import "./handlers/skills";
27
29
  import "./handlers/memory";
30
+ import "./handlers/editor";
31
+ import "./handlers/analytics";
28
32
  import { startScheduler } from "./features/scheduler";
29
33
  import { loadNotes } from "./features/sticky-notes";
30
34
  import { cleanupClientTerminals } from "./handlers/terminal";
35
+ import { cleanupClient as cleanupClientAttachments } from "./handlers/attachment";
31
36
 
32
37
  interface WsData {
33
38
  id: string;
@@ -299,8 +304,14 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
299
304
  }
300
305
  },
301
306
  close(ws: ServerWebSocket<WsData>) {
307
+ var activeSession = getActiveSession(ws.data.id);
308
+ if (activeSession) {
309
+ unwatchSessionLock(activeSession.sessionId);
310
+ }
311
+ clearActiveSession(ws.data.id);
302
312
  removeClient(ws.data.id);
303
313
  cleanupClientTerminals(ws.data.id);
314
+ cleanupClientAttachments(ws.data.id);
304
315
  console.log(`[lattice] Client disconnected: ${ws.data.id}`);
305
316
  },
306
317
  },
@@ -245,6 +245,29 @@ export function createTask(data: {
245
245
  return task;
246
246
  }
247
247
 
248
+ export function updateTask(taskId: string, data: { name?: string; prompt?: string; cron?: string }): ScheduledTask | null {
249
+ var task: ScheduledTask | null = null;
250
+ for (var i = 0; i < tasks.length; i++) {
251
+ if (tasks[i].id === taskId) {
252
+ task = tasks[i];
253
+ break;
254
+ }
255
+ }
256
+ if (!task) return null;
257
+
258
+ if (data.cron && data.cron !== task.cron) {
259
+ var parsed = parseCron(data.cron);
260
+ if (!parsed) return null;
261
+ task.cron = data.cron;
262
+ task.nextRunAt = task.enabled ? nextRunTime(data.cron) : null;
263
+ }
264
+ if (data.name !== undefined) task.name = data.name;
265
+ if (data.prompt !== undefined) task.prompt = data.prompt;
266
+ task.updatedAt = Date.now();
267
+ saveSchedules();
268
+ return task;
269
+ }
270
+
248
271
  export function deleteTask(taskId: string): boolean {
249
272
  var idx = -1;
250
273
  for (var i = 0; i < tasks.length; i++) {
@@ -61,17 +61,19 @@ function saveNotes(): void {
61
61
  }
62
62
  }
63
63
 
64
- export function listNotes(): StickyNote[] {
65
- return notes.slice();
64
+ export function listNotes(projectSlug?: string): StickyNote[] {
65
+ if (!projectSlug) return notes.slice();
66
+ return notes.filter(function (n) { return n.projectSlug === projectSlug; });
66
67
  }
67
68
 
68
- export function createNote(content: string): StickyNote {
69
+ export function createNote(content: string, projectSlug?: string): StickyNote {
69
70
  var now = Date.now();
70
71
  var note: StickyNote = {
71
72
  id: "note_" + now + "_" + randomBytes(3).toString("hex"),
72
73
  content,
73
74
  createdAt: now,
74
75
  updatedAt: now,
76
+ projectSlug,
75
77
  };
76
78
  notes.push(note);
77
79
  saveNotes();
@@ -0,0 +1,34 @@
1
+ import type { ClientMessage } from "@lattice/shared";
2
+ import { registerHandler } from "../ws/router";
3
+ import { sendTo } from "../ws/broadcast";
4
+ import { getAnalytics } from "../analytics/engine";
5
+
6
+ registerHandler("analytics", function (clientId: string, message: ClientMessage) {
7
+ if (message.type === "analytics:request") {
8
+ var msg = message as { type: string; requestId: string; scope: string; projectSlug?: string; sessionId?: string; period: string; forceRefresh?: boolean };
9
+
10
+ getAnalytics(
11
+ msg.scope as "global" | "project" | "session",
12
+ msg.period as "24h" | "7d" | "30d" | "90d" | "all",
13
+ msg.projectSlug,
14
+ msg.sessionId,
15
+ msg.forceRefresh
16
+ ).then(function (data) {
17
+ sendTo(clientId, {
18
+ type: "analytics:data",
19
+ requestId: msg.requestId,
20
+ scope: msg.scope,
21
+ period: msg.period,
22
+ data: data,
23
+ });
24
+ }).catch(function (err) {
25
+ sendTo(clientId, {
26
+ type: "analytics:error",
27
+ scope: msg.scope,
28
+ message: err instanceof Error ? err.message : "Analytics computation failed",
29
+ });
30
+ });
31
+
32
+ return;
33
+ }
34
+ });