luckerr 0.41.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 (156) hide show
  1. package/README.md +267 -0
  2. package/README.zh-CN.md +237 -0
  3. package/dashboard/app.css +3022 -0
  4. package/dashboard/dist/app.js +30137 -0
  5. package/dashboard/dist/app.js.map +1 -0
  6. package/dashboard/dist/vendor-hljs.css +10 -0
  7. package/dashboard/dist/vendor-uplot.css +1 -0
  8. package/dashboard/index.html +19 -0
  9. package/data/deepseek-tokenizer.json.gz +0 -0
  10. package/dist/cli/acp-EOOAI4F5.js +712 -0
  11. package/dist/cli/acp-EOOAI4F5.js.map +1 -0
  12. package/dist/cli/chat-7J6GJXL2.js +51 -0
  13. package/dist/cli/chat-7J6GJXL2.js.map +1 -0
  14. package/dist/cli/chunk-2425HK6U.js +54 -0
  15. package/dist/cli/chunk-2425HK6U.js.map +1 -0
  16. package/dist/cli/chunk-25T6CVUP.js +172 -0
  17. package/dist/cli/chunk-25T6CVUP.js.map +1 -0
  18. package/dist/cli/chunk-2UQP6H6T.js +31 -0
  19. package/dist/cli/chunk-2UQP6H6T.js.map +1 -0
  20. package/dist/cli/chunk-56OAJILV.js +47 -0
  21. package/dist/cli/chunk-56OAJILV.js.map +1 -0
  22. package/dist/cli/chunk-5FTI4KXH.js +150 -0
  23. package/dist/cli/chunk-5FTI4KXH.js.map +1 -0
  24. package/dist/cli/chunk-5TWQD73O.js +2846 -0
  25. package/dist/cli/chunk-5TWQD73O.js.map +1 -0
  26. package/dist/cli/chunk-653BOCMK.js +40 -0
  27. package/dist/cli/chunk-653BOCMK.js.map +1 -0
  28. package/dist/cli/chunk-6ALJTWWQ.js +2663 -0
  29. package/dist/cli/chunk-6ALJTWWQ.js.map +1 -0
  30. package/dist/cli/chunk-6DRKA2IL.js +341 -0
  31. package/dist/cli/chunk-6DRKA2IL.js.map +1 -0
  32. package/dist/cli/chunk-6LV63NJV.js +634 -0
  33. package/dist/cli/chunk-6LV63NJV.js.map +1 -0
  34. package/dist/cli/chunk-74EX7SUH.js +25293 -0
  35. package/dist/cli/chunk-74EX7SUH.js.map +1 -0
  36. package/dist/cli/chunk-74U5RKTX.js +60611 -0
  37. package/dist/cli/chunk-74U5RKTX.js.map +1 -0
  38. package/dist/cli/chunk-ANJSUESV.js +143 -0
  39. package/dist/cli/chunk-ANJSUESV.js.map +1 -0
  40. package/dist/cli/chunk-DB2Z3DKZ.js +54 -0
  41. package/dist/cli/chunk-DB2Z3DKZ.js.map +1 -0
  42. package/dist/cli/chunk-DDIH3ZAA.js +400 -0
  43. package/dist/cli/chunk-DDIH3ZAA.js.map +1 -0
  44. package/dist/cli/chunk-ELN3Z3B2.js +621 -0
  45. package/dist/cli/chunk-ELN3Z3B2.js.map +1 -0
  46. package/dist/cli/chunk-F6BSQJGV.js +200 -0
  47. package/dist/cli/chunk-F6BSQJGV.js.map +1 -0
  48. package/dist/cli/chunk-FET2UAG5.js +246 -0
  49. package/dist/cli/chunk-FET2UAG5.js.map +1 -0
  50. package/dist/cli/chunk-FFJ342IJ.js +190 -0
  51. package/dist/cli/chunk-FFJ342IJ.js.map +1 -0
  52. package/dist/cli/chunk-GB3247B6.js +130 -0
  53. package/dist/cli/chunk-GB3247B6.js.map +1 -0
  54. package/dist/cli/chunk-HC2J4U3G.js +373 -0
  55. package/dist/cli/chunk-HC2J4U3G.js.map +1 -0
  56. package/dist/cli/chunk-HRUZAIHQ.js +42 -0
  57. package/dist/cli/chunk-HRUZAIHQ.js.map +1 -0
  58. package/dist/cli/chunk-J3ZJFUDL.js +308 -0
  59. package/dist/cli/chunk-J3ZJFUDL.js.map +1 -0
  60. package/dist/cli/chunk-J5XJHLWM.js +55 -0
  61. package/dist/cli/chunk-J5XJHLWM.js.map +1 -0
  62. package/dist/cli/chunk-JFGLMRZ6.js +160 -0
  63. package/dist/cli/chunk-JFGLMRZ6.js.map +1 -0
  64. package/dist/cli/chunk-JMBMLOBP.js +26 -0
  65. package/dist/cli/chunk-JMBMLOBP.js.map +1 -0
  66. package/dist/cli/chunk-JMWHXZEL.js +551 -0
  67. package/dist/cli/chunk-JMWHXZEL.js.map +1 -0
  68. package/dist/cli/chunk-KEQGPJBO.js +209 -0
  69. package/dist/cli/chunk-KEQGPJBO.js.map +1 -0
  70. package/dist/cli/chunk-M4K6U37F.js +232 -0
  71. package/dist/cli/chunk-M4K6U37F.js.map +1 -0
  72. package/dist/cli/chunk-MIJI2WMN.js +95 -0
  73. package/dist/cli/chunk-MIJI2WMN.js.map +1 -0
  74. package/dist/cli/chunk-MPAO3JNR.js +128 -0
  75. package/dist/cli/chunk-MPAO3JNR.js.map +1 -0
  76. package/dist/cli/chunk-PZOFBEDC.js +873 -0
  77. package/dist/cli/chunk-PZOFBEDC.js.map +1 -0
  78. package/dist/cli/chunk-RAILYQLN.js +46 -0
  79. package/dist/cli/chunk-RAILYQLN.js.map +1 -0
  80. package/dist/cli/chunk-RR35VQVT.js +90 -0
  81. package/dist/cli/chunk-RR35VQVT.js.map +1 -0
  82. package/dist/cli/chunk-RRA7VPW4.js +417 -0
  83. package/dist/cli/chunk-RRA7VPW4.js.map +1 -0
  84. package/dist/cli/chunk-RU36QVN3.js +452 -0
  85. package/dist/cli/chunk-RU36QVN3.js.map +1 -0
  86. package/dist/cli/chunk-RUBIINXR.js +1819 -0
  87. package/dist/cli/chunk-RUBIINXR.js.map +1 -0
  88. package/dist/cli/chunk-S4XVGLRW.js +499 -0
  89. package/dist/cli/chunk-S4XVGLRW.js.map +1 -0
  90. package/dist/cli/chunk-TUK7OWJA.js +51 -0
  91. package/dist/cli/chunk-TUK7OWJA.js.map +1 -0
  92. package/dist/cli/chunk-VALDDV76.js +580 -0
  93. package/dist/cli/chunk-VALDDV76.js.map +1 -0
  94. package/dist/cli/chunk-WQOGPYGN.js +11390 -0
  95. package/dist/cli/chunk-WQOGPYGN.js.map +1 -0
  96. package/dist/cli/chunk-WREKDFXT.js +34320 -0
  97. package/dist/cli/chunk-WREKDFXT.js.map +1 -0
  98. package/dist/cli/chunk-Y7XQU2EL.js +270 -0
  99. package/dist/cli/chunk-Y7XQU2EL.js.map +1 -0
  100. package/dist/cli/chunk-YBVCZJU4.js +54 -0
  101. package/dist/cli/chunk-YBVCZJU4.js.map +1 -0
  102. package/dist/cli/chunk-YLIHDXUQ.js +749 -0
  103. package/dist/cli/chunk-YLIHDXUQ.js.map +1 -0
  104. package/dist/cli/chunk-YV5XXFD7.js +767 -0
  105. package/dist/cli/chunk-YV5XXFD7.js.map +1 -0
  106. package/dist/cli/chunk-ZRCNIYRQ.js +101 -0
  107. package/dist/cli/chunk-ZRCNIYRQ.js.map +1 -0
  108. package/dist/cli/code-CRKVCMFZ.js +155 -0
  109. package/dist/cli/code-CRKVCMFZ.js.map +1 -0
  110. package/dist/cli/commands-QLMD3T7B.js +356 -0
  111. package/dist/cli/commands-QLMD3T7B.js.map +1 -0
  112. package/dist/cli/commit-53PP32NC.js +293 -0
  113. package/dist/cli/commit-53PP32NC.js.map +1 -0
  114. package/dist/cli/desktop-R6W5CLJ5.js +1046 -0
  115. package/dist/cli/desktop-R6W5CLJ5.js.map +1 -0
  116. package/dist/cli/devtools-YECO25QO.js +3719 -0
  117. package/dist/cli/devtools-YECO25QO.js.map +1 -0
  118. package/dist/cli/diff-LYNRCJZE.js +166 -0
  119. package/dist/cli/diff-LYNRCJZE.js.map +1 -0
  120. package/dist/cli/doctor-5IBP4R5J.js +28 -0
  121. package/dist/cli/doctor-5IBP4R5J.js.map +1 -0
  122. package/dist/cli/events-QN6KLN2V.js +340 -0
  123. package/dist/cli/events-QN6KLN2V.js.map +1 -0
  124. package/dist/cli/index.js +3500 -0
  125. package/dist/cli/index.js.map +1 -0
  126. package/dist/cli/mcp-FGKEH7RG.js +277 -0
  127. package/dist/cli/mcp-FGKEH7RG.js.map +1 -0
  128. package/dist/cli/mcp-browse-YCND4NWT.js +178 -0
  129. package/dist/cli/mcp-browse-YCND4NWT.js.map +1 -0
  130. package/dist/cli/mcp-inspect-V34J3VX5.js +143 -0
  131. package/dist/cli/mcp-inspect-V34J3VX5.js.map +1 -0
  132. package/dist/cli/package.json +3 -0
  133. package/dist/cli/prompt-I775PNKT.js +16 -0
  134. package/dist/cli/prompt-I775PNKT.js.map +1 -0
  135. package/dist/cli/prune-sessions-KGIIYD3P.js +44 -0
  136. package/dist/cli/prune-sessions-KGIIYD3P.js.map +1 -0
  137. package/dist/cli/replay-RDXLUAOE.js +292 -0
  138. package/dist/cli/replay-RDXLUAOE.js.map +1 -0
  139. package/dist/cli/run-RCAC2RYW.js +223 -0
  140. package/dist/cli/run-RCAC2RYW.js.map +1 -0
  141. package/dist/cli/server-FFU6TLYJ.js +3658 -0
  142. package/dist/cli/server-FFU6TLYJ.js.map +1 -0
  143. package/dist/cli/sessions-QT26MQAE.js +107 -0
  144. package/dist/cli/sessions-QT26MQAE.js.map +1 -0
  145. package/dist/cli/setup-VV4WKXHV.js +767 -0
  146. package/dist/cli/setup-VV4WKXHV.js.map +1 -0
  147. package/dist/cli/stats-JVZPQWAN.js +15 -0
  148. package/dist/cli/stats-JVZPQWAN.js.map +1 -0
  149. package/dist/cli/update-KYI3OVJP.js +15 -0
  150. package/dist/cli/update-KYI3OVJP.js.map +1 -0
  151. package/dist/cli/version-ANYORXTI.js +34 -0
  152. package/dist/cli/version-ANYORXTI.js.map +1 -0
  153. package/dist/index.d.ts +2557 -0
  154. package/dist/index.js +15000 -0
  155. package/dist/index.js.map +1 -0
  156. package/package.json +106 -0
@@ -0,0 +1,3658 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
3
+ import {
4
+ readEventLogFile,
5
+ recentEventFiles
6
+ } from "./chunk-J5XJHLWM.js";
7
+ import {
8
+ SLASH_COMMANDS,
9
+ createCheckpoint,
10
+ deleteCheckpoint,
11
+ fmtAgo,
12
+ listCheckpoints,
13
+ listPlanArchives,
14
+ loadCheckpoint,
15
+ restoreCheckpoint
16
+ } from "./chunk-YLIHDXUQ.js";
17
+ import "./chunk-DB2Z3DKZ.js";
18
+ import {
19
+ fetchSmitheryDetail,
20
+ handleToFetchResult,
21
+ loadMorePages,
22
+ openRegistry,
23
+ specStringFor
24
+ } from "./chunk-6DRKA2IL.js";
25
+ import {
26
+ registerSemanticSearchTool
27
+ } from "./chunk-MIJI2WMN.js";
28
+ import {
29
+ BUILTIN_ALLOWLIST,
30
+ lineDiff
31
+ } from "./chunk-RUBIINXR.js";
32
+ import {
33
+ PROJECT_MEMORY_FILE,
34
+ SKILLS_DIRNAME,
35
+ SKILL_FILE,
36
+ findProjectMemoryPath,
37
+ parseFrontmatter,
38
+ resolveProjectMemoryWritePath,
39
+ validateSkillFrontmatter
40
+ } from "./chunk-VALDDV76.js";
41
+ import "./chunk-56OAJILV.js";
42
+ import {
43
+ checkOllamaStatus,
44
+ pullOllamaModel,
45
+ startOllamaDaemon
46
+ } from "./chunk-ZRCNIYRQ.js";
47
+ import {
48
+ INDEX_DIR_NAME,
49
+ buildIndex,
50
+ compareIndexIdentity,
51
+ indexExists,
52
+ querySemantic,
53
+ readIndexMeta,
54
+ walkChunks
55
+ } from "./chunk-PZOFBEDC.js";
56
+ import {
57
+ HOOK_EVENTS,
58
+ globalSettingsPath,
59
+ loadHooks,
60
+ projectSettingsPath
61
+ } from "./chunk-KEQGPJBO.js";
62
+ import "./chunk-S4XVGLRW.js";
63
+ import {
64
+ OpenAICompatClient
65
+ } from "./chunk-DDIH3ZAA.js";
66
+ import "./chunk-25T6CVUP.js";
67
+ import {
68
+ listSessions,
69
+ sessionPath,
70
+ sessionsDir
71
+ } from "./chunk-Y7XQU2EL.js";
72
+ import {
73
+ getLanguage,
74
+ getSupportedLanguages,
75
+ setLanguage
76
+ } from "./chunk-5TWQD73O.js";
77
+ import {
78
+ DEFAULT_INDEX_EXCLUDES,
79
+ DEFAULT_MAX_FILE_BYTES,
80
+ DEFAULT_RESPECT_GITIGNORE,
81
+ addProjectShellAllowed,
82
+ clearProjectShellAllowed,
83
+ isPlausibleKey,
84
+ loadIndexConfig,
85
+ loadIndexUserConfig,
86
+ loadProjectShellAllowed,
87
+ loadProviderApiKey,
88
+ loadProviderBaseUrl,
89
+ loadSemanticEmbeddingUserConfig,
90
+ readConfig,
91
+ redactKey,
92
+ redactSemanticEmbeddingConfig,
93
+ removeProjectShellAllowed,
94
+ resolveIndexConfig,
95
+ resolveSemanticEmbeddingConfig,
96
+ saveSemanticEmbeddingConfig,
97
+ writeConfig
98
+ } from "./chunk-6ALJTWWQ.js";
99
+ import {
100
+ aggregateUsage,
101
+ bucketCacheHitRatio,
102
+ formatLogSize,
103
+ readUsageLog
104
+ } from "./chunk-M4K6U37F.js";
105
+ import {
106
+ DEEPSEEK_PRICING,
107
+ cacheSavingsUsd
108
+ } from "./chunk-ANJSUESV.js";
109
+ import {
110
+ contextTokensFor,
111
+ listProviders,
112
+ pricingFor,
113
+ resolveProvider
114
+ } from "./chunk-JMWHXZEL.js";
115
+ import {
116
+ VERSION
117
+ } from "./chunk-MPAO3JNR.js";
118
+ import "./chunk-TUK7OWJA.js";
119
+
120
+ // src/server/index.ts
121
+ import { randomBytes } from "crypto";
122
+ import { createServer } from "http";
123
+
124
+ // src/server/api/events.ts
125
+ var PING_INTERVAL_MS = 25e3;
126
+ function handleEvents(req, res, ctx) {
127
+ if (!ctx.subscribeEvents) {
128
+ res.writeHead(503, { "content-type": "application/json" });
129
+ res.end(JSON.stringify({ error: "event stream requires an attached dashboard session." }));
130
+ return;
131
+ }
132
+ res.writeHead(200, {
133
+ "content-type": "text/event-stream",
134
+ "cache-control": "no-cache",
135
+ connection: "keep-alive",
136
+ "x-accel-buffering": "no"
137
+ // disable Nginx-style buffering if anything proxies us
138
+ });
139
+ const writeEvent = (event) => {
140
+ if (res.writableEnded) return;
141
+ try {
142
+ res.write(`data: ${JSON.stringify(event)}
143
+
144
+ `);
145
+ } catch {
146
+ }
147
+ };
148
+ if (ctx.isBusy) writeEvent({ kind: "busy-change", busy: ctx.isBusy() });
149
+ const unsubscribe = ctx.subscribeEvents(writeEvent);
150
+ const ping = setInterval(() => writeEvent({ kind: "ping" }), PING_INTERVAL_MS);
151
+ ping.unref?.();
152
+ const cleanup = () => {
153
+ clearInterval(ping);
154
+ try {
155
+ unsubscribe();
156
+ } catch {
157
+ }
158
+ if (!res.writableEnded) {
159
+ try {
160
+ res.end();
161
+ } catch {
162
+ }
163
+ }
164
+ };
165
+ req.on("close", cleanup);
166
+ req.on("error", cleanup);
167
+ res.on("close", cleanup);
168
+ }
169
+
170
+ // src/server/assets.ts
171
+ import { closeSync, fstatSync, openSync, readFileSync, readSync } from "fs";
172
+ import { dirname, join } from "path";
173
+ import { fileURLToPath } from "url";
174
+ function resolveAssetDir() {
175
+ const here = dirname(fileURLToPath(import.meta.url));
176
+ const candidates = [
177
+ join(here, "..", "..", "dashboard"),
178
+ join(here, "..", "dashboard"),
179
+ join(here, "dashboard")
180
+ ];
181
+ for (const c of candidates) {
182
+ try {
183
+ readFileSync(join(c, "index.html"), "utf8");
184
+ return c;
185
+ } catch {
186
+ }
187
+ }
188
+ return candidates[0];
189
+ }
190
+ var ASSET_DIR = resolveAssetDir();
191
+ var fileCache = /* @__PURE__ */ new Map();
192
+ function loadCachedFile(path) {
193
+ const fd = openSync(path, "r");
194
+ try {
195
+ const stat = fstatSync(fd);
196
+ const cached = fileCache.get(path);
197
+ if (cached && cached.mtimeMs === stat.mtimeMs) return cached.body;
198
+ const buf = Buffer.alloc(stat.size);
199
+ let read = 0;
200
+ while (read < stat.size) {
201
+ const n = readSync(fd, buf, read, stat.size - read, read);
202
+ if (n <= 0) break;
203
+ read += n;
204
+ }
205
+ const body = buf.toString("utf8", 0, read);
206
+ fileCache.set(path, { body, mtimeMs: stat.mtimeMs });
207
+ return body;
208
+ } finally {
209
+ closeSync(fd);
210
+ }
211
+ }
212
+ function loadIndexTemplate() {
213
+ return loadCachedFile(join(ASSET_DIR, "index.html"));
214
+ }
215
+ function loadApp() {
216
+ return loadCachedFile(join(ASSET_DIR, "dist", "app.js"));
217
+ }
218
+ function loadAppMap() {
219
+ try {
220
+ return loadCachedFile(join(ASSET_DIR, "dist", "app.js.map"));
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ function loadCss() {
226
+ return loadCachedFile(join(ASSET_DIR, "app.css"));
227
+ }
228
+ function renderIndexHtml(token, mode) {
229
+ const tpl = loadIndexTemplate();
230
+ const safeToken = token.replace(/[^a-zA-Z0-9]/g, "");
231
+ return tpl.replaceAll("__LUCKERR_TOKEN__", safeToken).replaceAll("__LUCKERR_MODE__", mode);
232
+ }
233
+ var VENDOR_CSS_NAMES = /* @__PURE__ */ new Set(["vendor-hljs.css", "vendor-uplot.css"]);
234
+ function loadVendorCss(name) {
235
+ return loadCachedFile(join(ASSET_DIR, "dist", name));
236
+ }
237
+ function serveAsset(name) {
238
+ if (name === "app.js") {
239
+ return { body: loadApp(), contentType: "application/javascript; charset=utf-8" };
240
+ }
241
+ if (name === "app.js.map") {
242
+ const body = loadAppMap();
243
+ return body == null ? null : { body, contentType: "application/json; charset=utf-8" };
244
+ }
245
+ if (name === "app.css") {
246
+ return { body: loadCss(), contentType: "text/css; charset=utf-8" };
247
+ }
248
+ if (VENDOR_CSS_NAMES.has(name)) {
249
+ return { body: loadVendorCss(name), contentType: "text/css; charset=utf-8" };
250
+ }
251
+ return null;
252
+ }
253
+
254
+ // src/server/api/abort.ts
255
+ async function handleAbort(method, _rest, _body, ctx) {
256
+ if (method !== "POST") {
257
+ return { status: 405, body: { error: "POST only" } };
258
+ }
259
+ if (!ctx.abortTurn) {
260
+ return {
261
+ status: 503,
262
+ body: { error: "abort requires an attached dashboard session." }
263
+ };
264
+ }
265
+ ctx.abortTurn();
266
+ ctx.audit?.({ ts: Date.now(), action: "abort-turn" });
267
+ return { status: 202, body: { aborted: true } };
268
+ }
269
+
270
+ // src/server/api/checkpoint-create.ts
271
+ async function handleCheckpointCreate(method, _rest, body, ctx) {
272
+ if (method !== "POST") return { status: 405, body: { error: "POST only" } };
273
+ const rootDir = ctx.getCurrentCwd?.();
274
+ if (!rootDir) return { status: 400, body: { error: "no active workspace" } };
275
+ let parsed;
276
+ try {
277
+ parsed = JSON.parse(body);
278
+ } catch {
279
+ return { status: 400, body: { error: "invalid JSON" } };
280
+ }
281
+ if (!parsed.name) return { status: 400, body: { error: "missing name" } };
282
+ let paths;
283
+ try {
284
+ const { execSync: execSync2 } = await import("child_process");
285
+ const stdout = execSync2("git ls-files --cached --others --exclude-standard", {
286
+ cwd: rootDir,
287
+ encoding: "utf8",
288
+ maxBuffer: 10 * 1024 * 1024
289
+ });
290
+ paths = stdout.split("\n").filter(Boolean);
291
+ } catch (err) {
292
+ const msg = err instanceof Error ? err.message : String(err);
293
+ if (msg.includes("ENOENT") || msg.includes("not a git repository") || msg.includes("fatal")) {
294
+ return {
295
+ status: 400,
296
+ body: {
297
+ error: `Cannot snapshot \u2014 not a git repository or git is unavailable: ${msg}`
298
+ }
299
+ };
300
+ }
301
+ return {
302
+ status: 500,
303
+ body: { error: `git ls-files failed: ${msg}` }
304
+ };
305
+ }
306
+ const meta = createCheckpoint({
307
+ rootDir,
308
+ name: parsed.name,
309
+ paths
310
+ });
311
+ return {
312
+ status: 200,
313
+ body: {
314
+ id: meta.id,
315
+ name: meta.name,
316
+ fileCount: meta.fileCount,
317
+ bytes: meta.bytes
318
+ }
319
+ };
320
+ }
321
+
322
+ // src/server/api/checkpoint-delete.ts
323
+ async function handleCheckpointDelete(method, _rest, body, ctx) {
324
+ if (method !== "POST") return { status: 405, body: { error: "POST only" } };
325
+ const rootDir = ctx.getCurrentCwd?.();
326
+ if (!rootDir) return { status: 400, body: { error: "no active workspace" } };
327
+ let parsed;
328
+ try {
329
+ parsed = JSON.parse(body);
330
+ } catch {
331
+ return { status: 400, body: { error: "invalid JSON" } };
332
+ }
333
+ if (!parsed.id) return { status: 400, body: { error: "missing id" } };
334
+ const ok = deleteCheckpoint(rootDir, parsed.id);
335
+ return ok ? { status: 200, body: { deleted: parsed.id } } : { status: 500, body: { error: "delete failed" } };
336
+ }
337
+
338
+ // src/server/api/checkpoint-diffs.ts
339
+ import { readFileSync as readFileSync2 } from "fs";
340
+ import { resolve } from "path";
341
+ async function handleCheckpointDiffs(method, _rest, _body, ctx, query = new URLSearchParams()) {
342
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
343
+ const rootDir = ctx.getCurrentCwd?.();
344
+ if (!rootDir) return { status: 200, body: [] };
345
+ const checkpointId = query.get("id");
346
+ if (!checkpointId) return { status: 400, body: { error: "missing id" } };
347
+ const checkpoint = loadCheckpoint(rootDir, checkpointId);
348
+ if (!checkpoint) return { status: 404, body: { error: "checkpoint not found" } };
349
+ const diffs = [];
350
+ for (const snap of checkpoint.files) {
351
+ const absPath = resolve(rootDir, snap.path);
352
+ let currentContent = null;
353
+ try {
354
+ currentContent = readFileSync2(absPath, "utf8");
355
+ } catch {
356
+ currentContent = null;
357
+ }
358
+ if (snap.content !== null) {
359
+ if (currentContent === null) {
360
+ diffs.push({
361
+ file: snap.path,
362
+ additions: 0,
363
+ deletions: snap.content.split("\n").length,
364
+ status: "deleted"
365
+ });
366
+ } else if (currentContent !== snap.content) {
367
+ const rows = lineDiff(snap.content.split("\n"), currentContent.split("\n"));
368
+ const additions = rows.filter((r) => r.op === "+").length;
369
+ const deletions = rows.filter((r) => r.op === "-").length;
370
+ let patch = `--- a/${snap.path}
371
+ +++ b/${snap.path}
372
+ `;
373
+ const ctx2 = 3;
374
+ let i = 0;
375
+ while (i < rows.length) {
376
+ while (i < rows.length && rows[i].op === " ") i++;
377
+ if (i >= rows.length) break;
378
+ const hunkStart = Math.max(0, i - ctx2);
379
+ let hunkEnd = i;
380
+ while (hunkEnd < rows.length && rows[hunkEnd].op !== " ") hunkEnd++;
381
+ hunkEnd = Math.min(rows.length, hunkEnd + ctx2);
382
+ const oldCount = rows.slice(hunkStart, hunkEnd).filter((r) => r.op !== "+").length;
383
+ const newCount = rows.slice(hunkStart, hunkEnd).filter((r) => r.op !== "-").length;
384
+ patch += `@@ -${hunkStart + 1},${oldCount} +${hunkStart + 1},${newCount} @@
385
+ `;
386
+ for (let j = hunkStart; j < hunkEnd; j++) {
387
+ patch += `${rows[j].op}${rows[j].line}
388
+ `;
389
+ }
390
+ i = hunkEnd;
391
+ }
392
+ diffs.push({
393
+ file: snap.path,
394
+ additions,
395
+ deletions,
396
+ patch,
397
+ status: "modified"
398
+ });
399
+ }
400
+ } else {
401
+ if (currentContent !== null) {
402
+ const additions = currentContent.split("\n").length;
403
+ diffs.push({
404
+ file: snap.path,
405
+ additions,
406
+ deletions: 0,
407
+ status: "added"
408
+ });
409
+ }
410
+ }
411
+ }
412
+ return { status: 200, body: diffs };
413
+ }
414
+
415
+ // src/server/api/checkpoint-restore.ts
416
+ async function handleCheckpointRestore(method, _rest, body, ctx) {
417
+ if (method !== "POST") return { status: 405, body: { error: "POST only" } };
418
+ const rootDir = ctx.getCurrentCwd?.();
419
+ if (!rootDir) return { status: 400, body: { error: "no active workspace" } };
420
+ let parsed;
421
+ try {
422
+ parsed = JSON.parse(body);
423
+ } catch {
424
+ return { status: 400, body: { error: "invalid JSON" } };
425
+ }
426
+ if (!parsed.id) return { status: 400, body: { error: "missing id" } };
427
+ const result = restoreCheckpoint(rootDir, parsed.id);
428
+ return { status: 200, body: result };
429
+ }
430
+
431
+ // src/server/api/checkpoints.ts
432
+ async function handleCheckpoints(method, _rest, _body, ctx) {
433
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
434
+ const rootDir = ctx.getCurrentCwd?.();
435
+ if (!rootDir) return { status: 200, body: [] };
436
+ const metas = listCheckpoints(rootDir);
437
+ const items = metas.map((m) => ({
438
+ id: m.id,
439
+ name: m.name,
440
+ createdAt: m.createdAt,
441
+ source: m.source,
442
+ fileCount: m.fileCount,
443
+ bytes: m.bytes,
444
+ ago: fmtAgo(m.createdAt)
445
+ }));
446
+ return { status: 200, body: items };
447
+ }
448
+
449
+ // src/server/api/edit-mode.ts
450
+ function parseBody(raw) {
451
+ if (!raw) return {};
452
+ try {
453
+ const parsed = JSON.parse(raw);
454
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
455
+ } catch {
456
+ return {};
457
+ }
458
+ }
459
+ var VALID = /* @__PURE__ */ new Set(["review", "auto", "yolo"]);
460
+ async function handleEditMode(method, _rest, body, ctx) {
461
+ if (method === "GET") {
462
+ return {
463
+ status: 200,
464
+ body: { mode: ctx.getEditMode?.() ?? null }
465
+ };
466
+ }
467
+ if (method === "POST") {
468
+ if (!ctx.setEditMode) {
469
+ return {
470
+ status: 503,
471
+ body: { error: "edit-mode mutation requires an attached `luckerr code` session." }
472
+ };
473
+ }
474
+ const { mode } = parseBody(body);
475
+ if (typeof mode !== "string" || !VALID.has(mode)) {
476
+ return { status: 400, body: { error: "mode must be review | auto | yolo" } };
477
+ }
478
+ const resolved = ctx.setEditMode(mode);
479
+ ctx.audit?.({ ts: Date.now(), action: "set-edit-mode", payload: { mode: resolved } });
480
+ return { status: 200, body: { mode: resolved } };
481
+ }
482
+ return { status: 405, body: { error: "GET or POST only" } };
483
+ }
484
+
485
+ // src/server/api/file-read.ts
486
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readSync as readSync2 } from "fs";
487
+ import { extname, join as join2, resolve as resolve2, sep } from "path";
488
+ var MAX_FILE_SIZE = 500 * 1024;
489
+ var BINARY_EXTS = /* @__PURE__ */ new Set([
490
+ ".png",
491
+ ".jpg",
492
+ ".jpeg",
493
+ ".gif",
494
+ ".webp",
495
+ ".ico",
496
+ ".pdf",
497
+ ".zip",
498
+ ".tar",
499
+ ".gz",
500
+ ".7z",
501
+ ".woff",
502
+ ".woff2",
503
+ ".ttf",
504
+ ".eot",
505
+ ".mp4",
506
+ ".webm",
507
+ ".mp3",
508
+ ".wav",
509
+ ".ogg",
510
+ ".exe",
511
+ ".dll",
512
+ ".so",
513
+ ".dylib",
514
+ ".class",
515
+ ".pyc",
516
+ ".o",
517
+ ".obj"
518
+ ]);
519
+ async function handleFileRead(method, rest, _body, ctx) {
520
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
521
+ const filePath = decodeURIComponent(rest.join("/"));
522
+ if (!filePath) return { status: 400, body: { error: "file path required" } };
523
+ const cwd = ctx.getCurrentCwd?.();
524
+ if (!cwd) return { status: 503, body: { error: "no project directory available" } };
525
+ const resolved = resolve2(join2(cwd, filePath));
526
+ const normalizedCwd = resolve2(cwd);
527
+ if (!resolved.startsWith(normalizedCwd + sep) && resolved !== normalizedCwd) {
528
+ return { status: 403, body: { error: "path escapes workspace" } };
529
+ }
530
+ const ext = extname(filePath).toLowerCase();
531
+ if (BINARY_EXTS.has(ext)) {
532
+ return { status: 400, body: { error: "binary file not supported" } };
533
+ }
534
+ let fd;
535
+ try {
536
+ fd = openSync2(resolved, "r");
537
+ } catch (err) {
538
+ const code = err.code;
539
+ if (code === "ENOENT") {
540
+ return { status: 404, body: { error: `file not found: ${filePath}` } };
541
+ }
542
+ return { status: 500, body: { error: "cannot open file" } };
543
+ }
544
+ try {
545
+ const st = fstatSync2(fd);
546
+ if (!st.isFile()) {
547
+ return { status: 400, body: { error: "not a file" } };
548
+ }
549
+ if (st.size > MAX_FILE_SIZE) {
550
+ return {
551
+ status: 413,
552
+ body: { error: `file too large (${st.size} bytes, max ${MAX_FILE_SIZE})` }
553
+ };
554
+ }
555
+ const buf = Buffer.alloc(st.size);
556
+ readSync2(fd, buf, 0, st.size, 0);
557
+ return { status: 200, body: { content: buf.toString("utf-8"), path: filePath, size: st.size } };
558
+ } finally {
559
+ closeSync2(fd);
560
+ }
561
+ }
562
+
563
+ // src/server/api/files.ts
564
+ import { existsSync, readdirSync, statSync } from "fs";
565
+ import { extname as extname2, join as join3, relative, sep as sep2 } from "path";
566
+ var RESULT_CAP = 50;
567
+ var MAX_DEPTH = 4;
568
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
569
+ "node_modules",
570
+ ".git",
571
+ ".luckerr",
572
+ "dist",
573
+ "build",
574
+ "out",
575
+ ".next",
576
+ "coverage",
577
+ ".cache",
578
+ "__pycache__",
579
+ ".venv",
580
+ ".pytest_cache"
581
+ ]);
582
+ var SKIP_EXTS = /* @__PURE__ */ new Set([
583
+ ".png",
584
+ ".jpg",
585
+ ".jpeg",
586
+ ".gif",
587
+ ".webp",
588
+ ".ico",
589
+ ".pdf",
590
+ ".zip",
591
+ ".tar",
592
+ ".gz",
593
+ ".lock",
594
+ ".woff",
595
+ ".woff2",
596
+ ".ttf"
597
+ ]);
598
+ async function handleFiles(method, _rest, body, ctx) {
599
+ if (method !== "POST") return { status: 405, body: { error: "POST only" } };
600
+ const cwd = ctx.getCurrentCwd?.();
601
+ if (!cwd || !existsSync(cwd)) {
602
+ return { status: 503, body: { error: "@-mention picker requires a code-mode session" } };
603
+ }
604
+ let parsed;
605
+ try {
606
+ parsed = JSON.parse(body || "{}");
607
+ } catch {
608
+ return { status: 400, body: { error: "body must be JSON" } };
609
+ }
610
+ const prefix = typeof parsed.prefix === "string" ? parsed.prefix.trim().toLowerCase() : "";
611
+ const matches = walk(cwd, prefix);
612
+ return { status: 200, body: { files: matches } };
613
+ }
614
+ function walk(root, prefix) {
615
+ const out = [];
616
+ const stack = [{ path: root, depth: 0 }];
617
+ while (stack.length > 0 && out.length < RESULT_CAP) {
618
+ const { path, depth } = stack.pop();
619
+ if (depth > MAX_DEPTH) continue;
620
+ let names;
621
+ try {
622
+ names = readdirSync(path);
623
+ } catch {
624
+ continue;
625
+ }
626
+ for (const name of names) {
627
+ if (out.length >= RESULT_CAP) break;
628
+ if (name.startsWith(".") && depth === 0) continue;
629
+ if (SKIP_DIRS.has(name)) continue;
630
+ const full = join3(path, name);
631
+ let st;
632
+ try {
633
+ st = statSync(full);
634
+ } catch {
635
+ continue;
636
+ }
637
+ if (st.isDirectory()) {
638
+ stack.push({ path: full, depth: depth + 1 });
639
+ continue;
640
+ }
641
+ if (!st.isFile()) continue;
642
+ if (SKIP_EXTS.has(extname2(name).toLowerCase())) continue;
643
+ const rel = relative(root, full).split(sep2).join("/");
644
+ if (prefix && !rel.toLowerCase().includes(prefix)) continue;
645
+ out.push(rel);
646
+ }
647
+ }
648
+ return out.sort((a, b) => a.localeCompare(b));
649
+ }
650
+
651
+ // src/server/api/git-diffs.ts
652
+ import { execSync } from "child_process";
653
+ function parseGitDiff(stdout) {
654
+ const files = [];
655
+ const blocks = stdout.split(/\ndiff --git /).filter(Boolean);
656
+ for (const block of blocks) {
657
+ const fullBlock = block.startsWith("diff --git ") ? block : `diff --git ${block}`;
658
+ const bPath = fullBlock.match(/^diff --git a\/.+ b\/(.+)$/m)?.[1];
659
+ if (!bPath) continue;
660
+ const patchContent = block;
661
+ const additions = (patchContent.match(/^\+/gm) || []).length;
662
+ const deletions = (patchContent.match(/^-/gm) || []).length;
663
+ const isNew = /^new file mode/.test(patchContent);
664
+ const isDeleted = /^deleted file mode/.test(patchContent);
665
+ const status = isNew ? "added" : isDeleted ? "deleted" : "modified";
666
+ files.push({
667
+ file: bPath,
668
+ additions,
669
+ deletions,
670
+ patch: fullBlock,
671
+ status
672
+ });
673
+ }
674
+ return files;
675
+ }
676
+ async function handleGitDiffs(method, _rest, _body, _ctx) {
677
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
678
+ let diffStdout;
679
+ let stagedStdout;
680
+ let untracked;
681
+ try {
682
+ diffStdout = execSync("git diff --no-color --unified=3 HEAD", {
683
+ encoding: "utf8",
684
+ maxBuffer: 10 * 1024 * 1024,
685
+ windowsHide: true
686
+ });
687
+ stagedStdout = execSync("git diff --no-color --unified=3 --cached", {
688
+ encoding: "utf8",
689
+ maxBuffer: 10 * 1024 * 1024,
690
+ windowsHide: true
691
+ });
692
+ untracked = execSync("git ls-files --others --exclude-standard", {
693
+ encoding: "utf8",
694
+ maxBuffer: 1024 * 1024,
695
+ windowsHide: true
696
+ });
697
+ } catch {
698
+ return { status: 200, body: [] };
699
+ }
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const allDiffs = [];
702
+ const combined = diffStdout + (stagedStdout ? `
703
+ ${stagedStdout}` : "");
704
+ for (const f of parseGitDiff(combined)) {
705
+ if (!seen.has(f.file)) {
706
+ seen.add(f.file);
707
+ allDiffs.push(f);
708
+ }
709
+ }
710
+ for (const file of untracked.split("\n").filter(Boolean)) {
711
+ if (!seen.has(file)) {
712
+ seen.add(file);
713
+ allDiffs.push({
714
+ file,
715
+ additions: 0,
716
+ deletions: 0,
717
+ status: "added"
718
+ });
719
+ }
720
+ }
721
+ return { status: 200, body: allDiffs };
722
+ }
723
+
724
+ // src/server/api/health.ts
725
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
726
+ import { homedir } from "os";
727
+ import { join as join4 } from "path";
728
+ function dirSize(path) {
729
+ if (!existsSync2(path)) return { path, exists: false, fileCount: 0, totalBytes: 0 };
730
+ let fileCount = 0;
731
+ let totalBytes = 0;
732
+ try {
733
+ const entries = readdirSync2(path);
734
+ for (const name of entries) {
735
+ const full = join4(path, name);
736
+ try {
737
+ const s = statSync2(full);
738
+ if (s.isFile()) {
739
+ fileCount++;
740
+ totalBytes += s.size;
741
+ } else if (s.isDirectory()) {
742
+ try {
743
+ const inner = readdirSync2(full);
744
+ for (const child of inner) {
745
+ try {
746
+ const cs = statSync2(join4(full, child));
747
+ if (cs.isFile()) {
748
+ fileCount++;
749
+ totalBytes += cs.size;
750
+ }
751
+ } catch {
752
+ }
753
+ }
754
+ } catch {
755
+ }
756
+ }
757
+ } catch {
758
+ }
759
+ }
760
+ } catch {
761
+ return { path, exists: true, fileCount: 0, totalBytes: 0 };
762
+ }
763
+ return { path, exists: true, fileCount, totalBytes };
764
+ }
765
+ async function handleHealth(method, _rest, _body, ctx) {
766
+ if (method !== "GET") {
767
+ return { status: 405, body: { error: "GET only" } };
768
+ }
769
+ const home = homedir();
770
+ const luckerrHome = join4(home, ".luckerr");
771
+ const sessionsStat = dirSize(join4(luckerrHome, "sessions"));
772
+ const memoryStat = dirSize(join4(luckerrHome, "memory"));
773
+ const semanticStat = dirSize(join4(luckerrHome, "semantic"));
774
+ let usageBytes = 0;
775
+ if (existsSync2(ctx.usageLogPath)) {
776
+ try {
777
+ usageBytes = statSync2(ctx.usageLogPath).size;
778
+ } catch {
779
+ }
780
+ }
781
+ const sessions = listSessions();
782
+ return {
783
+ status: 200,
784
+ body: {
785
+ version: VERSION,
786
+ latestVersion: ctx.getLatestVersion?.() ?? null,
787
+ luckerrHome,
788
+ sessions: {
789
+ path: sessionsStat.path,
790
+ count: sessions.length,
791
+ totalBytes: sessionsStat.totalBytes
792
+ },
793
+ memory: {
794
+ path: memoryStat.path,
795
+ fileCount: memoryStat.fileCount,
796
+ totalBytes: memoryStat.totalBytes
797
+ },
798
+ semantic: {
799
+ path: semanticStat.path,
800
+ exists: semanticStat.exists,
801
+ fileCount: semanticStat.fileCount,
802
+ totalBytes: semanticStat.totalBytes
803
+ },
804
+ usageLog: {
805
+ path: ctx.usageLogPath,
806
+ bytes: usageBytes
807
+ },
808
+ jobs: ctx.jobs ? ctx.jobs.list().length : null
809
+ }
810
+ };
811
+ }
812
+
813
+ // src/server/api/hooks.ts
814
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
815
+ import { dirname as dirname2 } from "path";
816
+
817
+ // src/server/api/hooks-events.ts
818
+ import { existsSync as existsSync3 } from "fs";
819
+ var HOOK_LOG_CAP = 12;
820
+ function readRecentHookRuns(now = Date.now(), sessionsDirOverride) {
821
+ const dir = sessionsDirOverride ?? sessionsDir();
822
+ if (!existsSync3(dir)) return null;
823
+ const files = recentEventFiles(dir, now);
824
+ if (files.length === 0) return null;
825
+ const rows = [];
826
+ for (const file of files) {
827
+ const events = readEventLogFile(file);
828
+ for (const ev of events) {
829
+ if (ev.type !== "hook.fired") continue;
830
+ const ts = Date.parse(ev.ts);
831
+ if (!Number.isFinite(ts)) continue;
832
+ rows.push({
833
+ hookName: ev.hookName,
834
+ phase: ev.phase,
835
+ outcome: ev.outcome,
836
+ whenMs: ts
837
+ });
838
+ }
839
+ }
840
+ rows.sort((a, b) => b.whenMs - a.whenMs);
841
+ return rows.slice(0, HOOK_LOG_CAP);
842
+ }
843
+
844
+ // src/server/api/hooks.ts
845
+ function parseBody2(raw) {
846
+ if (!raw) return {};
847
+ try {
848
+ const parsed = JSON.parse(raw);
849
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
850
+ } catch {
851
+ return {};
852
+ }
853
+ }
854
+ function readSettingsFile(path) {
855
+ if (!existsSync4(path)) return {};
856
+ try {
857
+ const raw = readFileSync3(path, "utf8");
858
+ const parsed = JSON.parse(raw);
859
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
860
+ } catch {
861
+ return {};
862
+ }
863
+ }
864
+ function writeSettingsFile(path, hooksBlock) {
865
+ const existing = readSettingsFile(path);
866
+ existing.hooks = hooksBlock;
867
+ mkdirSync(dirname2(path), { recursive: true });
868
+ writeFileSync(path, `${JSON.stringify(existing, null, 2)}
869
+ `, "utf8");
870
+ }
871
+ async function handleHooks(method, rest, body, ctx) {
872
+ if (method === "GET" && rest.length === 0) {
873
+ const projectPath = ctx.getCurrentCwd ? projectSettingsPath(ctx.getCurrentCwd() ?? "") : null;
874
+ const globalPath = globalSettingsPath();
875
+ const projectFile = projectPath ? readSettingsFile(projectPath) : {};
876
+ const globalFile = readSettingsFile(globalPath);
877
+ const resolved = loadHooks({ projectRoot: ctx.getCurrentCwd?.() });
878
+ return {
879
+ status: 200,
880
+ body: {
881
+ project: {
882
+ path: projectPath,
883
+ hooks: projectFile.hooks ?? {}
884
+ },
885
+ global: {
886
+ path: globalPath,
887
+ hooks: globalFile.hooks ?? {}
888
+ },
889
+ resolved,
890
+ events: HOOK_EVENTS,
891
+ recentRuns: readRecentHookRuns(void 0, ctx.sessionsDir)
892
+ }
893
+ };
894
+ }
895
+ if (method === "POST" && rest[0] === "save") {
896
+ const { scope, hooks } = parseBody2(body);
897
+ if (scope !== "project" && scope !== "global") {
898
+ return { status: 400, body: { error: "scope must be project | global" } };
899
+ }
900
+ if (typeof hooks !== "object" || hooks === null) {
901
+ return { status: 400, body: { error: "hooks must be an object keyed by event name" } };
902
+ }
903
+ let path;
904
+ if (scope === "project") {
905
+ const cwd = ctx.getCurrentCwd?.();
906
+ if (!cwd) {
907
+ return {
908
+ status: 503,
909
+ body: { error: "no active project \u2014 open `/dashboard` from inside `luckerr code`" }
910
+ };
911
+ }
912
+ path = projectSettingsPath(cwd);
913
+ } else {
914
+ path = globalSettingsPath();
915
+ }
916
+ if (!path) {
917
+ return { status: 500, body: { error: "could not resolve settings path" } };
918
+ }
919
+ writeSettingsFile(path, hooks);
920
+ ctx.audit?.({ ts: Date.now(), action: "save-hooks", payload: { scope, path } });
921
+ return { status: 200, body: { saved: true, path } };
922
+ }
923
+ if (method === "POST" && rest[0] === "reload") {
924
+ if (!ctx.reloadHooks) {
925
+ return {
926
+ status: 503,
927
+ body: { error: "reload requires an attached session \u2014 App.tsx wires the callback" }
928
+ };
929
+ }
930
+ const count = ctx.reloadHooks();
931
+ ctx.audit?.({ ts: Date.now(), action: "reload-hooks", payload: { count } });
932
+ return { status: 200, body: { reloaded: true, count } };
933
+ }
934
+ return { status: 405, body: { error: `method ${method} not supported on this path` } };
935
+ }
936
+
937
+ // src/server/api/index-config.ts
938
+ var PREVIEW_INCLUDED_CAP = 50;
939
+ var PREVIEW_PER_REASON_CAP = 10;
940
+ function parseBody3(raw) {
941
+ if (!raw) return {};
942
+ try {
943
+ const parsed = JSON.parse(raw);
944
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
945
+ } catch {
946
+ return {};
947
+ }
948
+ }
949
+ function isStringArray(v) {
950
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
951
+ }
952
+ async function handleIndexConfig(method, rest, body, ctx) {
953
+ if (rest[0] === "preview" && method === "POST") {
954
+ return await handlePreview(body, ctx);
955
+ }
956
+ if (method === "GET") {
957
+ const user = loadIndexUserConfig(ctx.configPath);
958
+ const resolved = resolveIndexConfig(user);
959
+ return {
960
+ status: 200,
961
+ body: {
962
+ user,
963
+ resolved,
964
+ defaults: {
965
+ excludeDirs: [...DEFAULT_INDEX_EXCLUDES.dirs],
966
+ excludeFiles: [...DEFAULT_INDEX_EXCLUDES.files],
967
+ excludeExts: [...DEFAULT_INDEX_EXCLUDES.exts],
968
+ excludePatterns: [],
969
+ respectGitignore: DEFAULT_RESPECT_GITIGNORE,
970
+ maxFileBytes: DEFAULT_MAX_FILE_BYTES
971
+ }
972
+ }
973
+ };
974
+ }
975
+ if (method === "POST") {
976
+ const fields = parseBody3(body);
977
+ const next = {};
978
+ const changed = [];
979
+ if (fields.excludeDirs !== void 0) {
980
+ if (!isStringArray(fields.excludeDirs)) {
981
+ return { status: 400, body: { error: "excludeDirs must be string[]" } };
982
+ }
983
+ next.excludeDirs = fields.excludeDirs;
984
+ changed.push("excludeDirs");
985
+ }
986
+ if (fields.excludeFiles !== void 0) {
987
+ if (!isStringArray(fields.excludeFiles)) {
988
+ return { status: 400, body: { error: "excludeFiles must be string[]" } };
989
+ }
990
+ next.excludeFiles = fields.excludeFiles;
991
+ changed.push("excludeFiles");
992
+ }
993
+ if (fields.excludeExts !== void 0) {
994
+ if (!isStringArray(fields.excludeExts)) {
995
+ return { status: 400, body: { error: "excludeExts must be string[]" } };
996
+ }
997
+ next.excludeExts = fields.excludeExts;
998
+ changed.push("excludeExts");
999
+ }
1000
+ if (fields.excludePatterns !== void 0) {
1001
+ if (!isStringArray(fields.excludePatterns)) {
1002
+ return { status: 400, body: { error: "excludePatterns must be string[]" } };
1003
+ }
1004
+ next.excludePatterns = fields.excludePatterns;
1005
+ changed.push("excludePatterns");
1006
+ }
1007
+ if (fields.respectGitignore !== void 0) {
1008
+ if (typeof fields.respectGitignore !== "boolean") {
1009
+ return { status: 400, body: { error: "respectGitignore must be boolean" } };
1010
+ }
1011
+ next.respectGitignore = fields.respectGitignore;
1012
+ changed.push("respectGitignore");
1013
+ }
1014
+ if (fields.maxFileBytes !== void 0) {
1015
+ if (typeof fields.maxFileBytes !== "number" || fields.maxFileBytes <= 0) {
1016
+ return { status: 400, body: { error: "maxFileBytes must be a positive number" } };
1017
+ }
1018
+ next.maxFileBytes = fields.maxFileBytes;
1019
+ changed.push("maxFileBytes");
1020
+ }
1021
+ const cfg = readConfig(ctx.configPath);
1022
+ cfg.index = { ...cfg.index ?? {}, ...next };
1023
+ writeConfig(cfg, ctx.configPath);
1024
+ if (changed.length > 0) {
1025
+ ctx.audit?.({ ts: Date.now(), action: "set-index-config", payload: { fields: changed } });
1026
+ }
1027
+ return { status: 200, body: { changed, resolved: resolveIndexConfig(cfg.index) } };
1028
+ }
1029
+ return { status: 405, body: { error: "GET or POST only" } };
1030
+ }
1031
+ async function handlePreview(body, ctx) {
1032
+ const root = ctx.getCurrentCwd?.();
1033
+ if (!root) {
1034
+ return {
1035
+ status: 400,
1036
+ body: { error: "preview requires a code-mode session (no project root attached)" }
1037
+ };
1038
+ }
1039
+ const fields = parseBody3(body);
1040
+ const draft = {};
1041
+ if (isStringArray(fields.excludeDirs)) draft.excludeDirs = fields.excludeDirs;
1042
+ if (isStringArray(fields.excludeFiles)) draft.excludeFiles = fields.excludeFiles;
1043
+ if (isStringArray(fields.excludeExts)) draft.excludeExts = fields.excludeExts;
1044
+ if (isStringArray(fields.excludePatterns)) draft.excludePatterns = fields.excludePatterns;
1045
+ if (typeof fields.respectGitignore === "boolean")
1046
+ draft.respectGitignore = fields.respectGitignore;
1047
+ if (typeof fields.maxFileBytes === "number" && fields.maxFileBytes > 0) {
1048
+ draft.maxFileBytes = fields.maxFileBytes;
1049
+ }
1050
+ const resolved = resolveIndexConfig(draft);
1051
+ const skipBuckets = {
1052
+ defaultDir: 0,
1053
+ defaultFile: 0,
1054
+ binaryExt: 0,
1055
+ binaryContent: 0,
1056
+ tooLarge: 0,
1057
+ gitignore: 0,
1058
+ pattern: 0,
1059
+ readError: 0
1060
+ };
1061
+ const skipSamples = {
1062
+ defaultDir: [],
1063
+ defaultFile: [],
1064
+ binaryExt: [],
1065
+ binaryContent: [],
1066
+ tooLarge: [],
1067
+ gitignore: [],
1068
+ pattern: [],
1069
+ readError: []
1070
+ };
1071
+ const includedFiles = /* @__PURE__ */ new Set();
1072
+ const sampleIncluded = [];
1073
+ for await (const chunk of walkChunks(root, {
1074
+ config: resolved,
1075
+ onSkip: (rel, reason) => {
1076
+ skipBuckets[reason]++;
1077
+ const bucket = skipSamples[reason];
1078
+ if (bucket.length < PREVIEW_PER_REASON_CAP) bucket.push(rel);
1079
+ }
1080
+ })) {
1081
+ if (!includedFiles.has(chunk.path)) {
1082
+ includedFiles.add(chunk.path);
1083
+ if (sampleIncluded.length < PREVIEW_INCLUDED_CAP) sampleIncluded.push(chunk.path);
1084
+ }
1085
+ }
1086
+ return {
1087
+ status: 200,
1088
+ body: {
1089
+ filesIncluded: includedFiles.size,
1090
+ sampleIncluded,
1091
+ skipBuckets,
1092
+ skipSamples,
1093
+ resolved
1094
+ }
1095
+ };
1096
+ }
1097
+
1098
+ // src/server/api/loop.ts
1099
+ function parseBody4(raw) {
1100
+ if (!raw) return {};
1101
+ try {
1102
+ const parsed = JSON.parse(raw);
1103
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1104
+ } catch {
1105
+ return {};
1106
+ }
1107
+ }
1108
+ var MIN_INTERVAL_MS = 5e3;
1109
+ var MAX_INTERVAL_MS = 6 * 60 * 60 * 1e3;
1110
+ async function handleLoop(method, rest, body, ctx) {
1111
+ if (method === "GET" && rest[0] === "status") {
1112
+ if (!ctx.getLoopRunStatus) {
1113
+ return { status: 503, body: { error: "auto-loop not available \u2014 attach to a chat session" } };
1114
+ }
1115
+ return { status: 200, body: { status: ctx.getLoopRunStatus() } };
1116
+ }
1117
+ if (method === "POST" && rest[0] === "start") {
1118
+ if (!ctx.startAutoLoop) {
1119
+ return { status: 503, body: { error: "auto-loop start not wired" } };
1120
+ }
1121
+ const { intervalMs, prompt } = parseBody4(body);
1122
+ if (typeof prompt !== "string" || !prompt.trim()) {
1123
+ return { status: 400, body: { error: "prompt must be a non-empty string" } };
1124
+ }
1125
+ if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS) {
1126
+ return {
1127
+ status: 400,
1128
+ body: {
1129
+ error: `intervalMs must be a number in [${MIN_INTERVAL_MS}, ${MAX_INTERVAL_MS}] (5s..6h)`
1130
+ }
1131
+ };
1132
+ }
1133
+ ctx.startAutoLoop(intervalMs, prompt.trim());
1134
+ ctx.audit?.({ ts: Date.now(), action: "auto-loop-start", payload: { intervalMs } });
1135
+ return { status: 200, body: { started: true } };
1136
+ }
1137
+ if (method === "POST" && rest[0] === "stop") {
1138
+ if (!ctx.stopAutoLoop) {
1139
+ return { status: 503, body: { error: "auto-loop stop not wired" } };
1140
+ }
1141
+ ctx.stopAutoLoop();
1142
+ ctx.audit?.({ ts: Date.now(), action: "auto-loop-stop" });
1143
+ return { status: 200, body: { stopped: true } };
1144
+ }
1145
+ return {
1146
+ status: 405,
1147
+ body: { error: `method ${method} not supported on /api/loop/${rest[0] ?? ""}` }
1148
+ };
1149
+ }
1150
+
1151
+ // src/server/api/mcp.ts
1152
+ function parseBody5(raw) {
1153
+ if (!raw) return {};
1154
+ try {
1155
+ const parsed = JSON.parse(raw);
1156
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1157
+ } catch {
1158
+ return {};
1159
+ }
1160
+ }
1161
+ function clampInt(raw, min, max, fallback) {
1162
+ if (raw == null) return fallback;
1163
+ const n = Number.parseInt(raw, 10);
1164
+ if (!Number.isFinite(n)) return fallback;
1165
+ return Math.max(min, Math.min(max, n));
1166
+ }
1167
+ function findRegistryEntry(entries, name) {
1168
+ const exact = entries.find((e) => e.name === name);
1169
+ if (exact) return exact;
1170
+ const lower = name.toLowerCase();
1171
+ const ci = entries.find((e) => e.name.toLowerCase() === lower);
1172
+ if (ci) return ci;
1173
+ const tail = entries.find((e) => e.name.toLowerCase().endsWith(`/${lower}`));
1174
+ if (tail) return tail;
1175
+ return null;
1176
+ }
1177
+ async function handleMcp(method, rest, body, ctx, query = new URLSearchParams()) {
1178
+ if (method === "GET" && rest.length === 0) {
1179
+ const servers = (ctx.mcpServers ?? []).map((s) => ({
1180
+ label: s.label,
1181
+ spec: s.spec,
1182
+ toolCount: s.toolCount,
1183
+ protocolVersion: s.report.protocolVersion,
1184
+ serverInfo: s.report.serverInfo,
1185
+ capabilities: s.report.capabilities,
1186
+ tools: s.report.tools.supported ? s.report.tools.items : [],
1187
+ resources: s.report.resources.supported ? s.report.resources.items : [],
1188
+ prompts: s.report.prompts.supported ? s.report.prompts.items : [],
1189
+ instructions: s.report.instructions ?? null
1190
+ }));
1191
+ return {
1192
+ status: 200,
1193
+ body: {
1194
+ servers,
1195
+ canHotReload: Boolean(ctx.reloadMcp),
1196
+ canInvoke: Boolean(ctx.invokeMcpTool)
1197
+ }
1198
+ };
1199
+ }
1200
+ if (method === "GET" && rest[0] === "specs") {
1201
+ const cfg = readConfig(ctx.configPath);
1202
+ return { status: 200, body: { specs: cfg.mcp ?? [] } };
1203
+ }
1204
+ if (method === "POST" && rest[0] === "specs") {
1205
+ const { spec } = parseBody5(body);
1206
+ if (typeof spec !== "string" || !spec.trim()) {
1207
+ return { status: 400, body: { error: "spec (non-empty string) required" } };
1208
+ }
1209
+ const cfg = readConfig(ctx.configPath);
1210
+ const list = cfg.mcp ?? [];
1211
+ if (list.includes(spec)) {
1212
+ return { status: 200, body: { added: false, alreadyPresent: true } };
1213
+ }
1214
+ cfg.mcp = [...list, spec.trim()];
1215
+ writeConfig(cfg, ctx.configPath);
1216
+ ctx.audit?.({ ts: Date.now(), action: "add-mcp-spec", payload: { spec } });
1217
+ let bridged = false;
1218
+ if (ctx.reloadMcp) {
1219
+ try {
1220
+ await ctx.reloadMcp();
1221
+ bridged = true;
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ return { status: 200, body: { added: true, requiresRestart: !bridged, bridged } };
1226
+ }
1227
+ if (method === "DELETE" && rest[0] === "specs") {
1228
+ const { spec } = parseBody5(body);
1229
+ if (typeof spec !== "string") {
1230
+ return { status: 400, body: { error: "spec (string) required" } };
1231
+ }
1232
+ const cfg = readConfig(ctx.configPath);
1233
+ const list = cfg.mcp ?? [];
1234
+ if (!list.includes(spec)) {
1235
+ return { status: 200, body: { removed: false } };
1236
+ }
1237
+ cfg.mcp = list.filter((s) => s !== spec);
1238
+ writeConfig(cfg, ctx.configPath);
1239
+ ctx.audit?.({ ts: Date.now(), action: "remove-mcp-spec", payload: { spec } });
1240
+ let bridged = false;
1241
+ if (ctx.reloadMcp) {
1242
+ try {
1243
+ await ctx.reloadMcp();
1244
+ bridged = true;
1245
+ } catch {
1246
+ }
1247
+ }
1248
+ return { status: 200, body: { removed: true, requiresRestart: !bridged, bridged } };
1249
+ }
1250
+ if (method === "POST" && rest[0] === "reload") {
1251
+ if (!ctx.reloadMcp) {
1252
+ return {
1253
+ status: 503,
1254
+ body: {
1255
+ error: "live MCP reload not wired in this session \u2014 restart `luckerr code` to apply spec edits."
1256
+ }
1257
+ };
1258
+ }
1259
+ const count = await ctx.reloadMcp();
1260
+ return { status: 200, body: { reloaded: true, count } };
1261
+ }
1262
+ if (method === "GET" && rest[0] === "registry" && (rest[1] === void 0 || rest[1] === "list")) {
1263
+ const pagesWanted = clampInt(query.get("pages"), 1, 200, 1);
1264
+ const maxPages = clampInt(query.get("maxPages"), 1, 200, 20);
1265
+ const limit = clampInt(query.get("limit"), 1, 1e3, 30);
1266
+ const refreshRaw = query.get("refresh");
1267
+ const refresh = refreshRaw === "1" || refreshRaw === "true";
1268
+ const q = (query.get("q") ?? "").trim().toLowerCase();
1269
+ try {
1270
+ const handle = await openRegistry({ noCache: refresh });
1271
+ const target = q ? maxPages : pagesWanted;
1272
+ const additional = Math.max(0, target - handle.cache.pagination.pagesLoaded);
1273
+ if (additional > 0) {
1274
+ await loadMorePages(handle, {
1275
+ pages: additional,
1276
+ matchTarget: q ? limit : void 0,
1277
+ filter: q ? (e) => `${e.name} ${e.title} ${e.description}`.toLowerCase().includes(q) : void 0
1278
+ });
1279
+ }
1280
+ const result = handleToFetchResult(handle);
1281
+ const matched = q ? result.entries.filter(
1282
+ (e) => `${e.name} ${e.title} ${e.description}`.toLowerCase().includes(q)
1283
+ ) : result.entries;
1284
+ const ranked = matched.slice().sort((a, b) => {
1285
+ const ap = a.popularity ?? -1;
1286
+ const bp = b.popularity ?? -1;
1287
+ if (ap !== bp) return bp - ap;
1288
+ return a.name.localeCompare(b.name);
1289
+ });
1290
+ return {
1291
+ status: 200,
1292
+ body: {
1293
+ source: result.source,
1294
+ fromCache: result.fromCache,
1295
+ fetchedAt: result.fetchedAt,
1296
+ loaded: result.entries.length,
1297
+ hasMore: result.hasMore,
1298
+ matched: matched.length,
1299
+ entries: ranked.slice(0, limit),
1300
+ errors: result.errors
1301
+ }
1302
+ };
1303
+ } catch (err) {
1304
+ return { status: 500, body: { error: err.message } };
1305
+ }
1306
+ }
1307
+ if (method === "POST" && rest[0] === "registry" && rest[1] === "install") {
1308
+ const { name, maxPages } = parseBody5(body);
1309
+ if (typeof name !== "string" || !name.trim()) {
1310
+ return { status: 400, body: { error: "name (string) required" } };
1311
+ }
1312
+ const cap = typeof maxPages === "number" && maxPages > 0 ? maxPages : 30;
1313
+ try {
1314
+ const handle = await openRegistry({});
1315
+ const target = name.trim();
1316
+ const lower = target.toLowerCase();
1317
+ const filter = (e) => {
1318
+ const n = e.name.toLowerCase();
1319
+ return n === lower || n.endsWith(`/${lower}`) || n.includes(lower);
1320
+ };
1321
+ const additional = Math.max(0, cap - handle.cache.pagination.pagesLoaded);
1322
+ if (additional > 0) {
1323
+ await loadMorePages(handle, { pages: additional, matchTarget: 1, filter });
1324
+ }
1325
+ const entry = findRegistryEntry(handle.cache.entries, target);
1326
+ if (!entry) {
1327
+ return {
1328
+ status: 404,
1329
+ body: {
1330
+ error: `no MCP server named "${target}" found in ${handle.cache.pagination.pagesLoaded} page(s)`
1331
+ }
1332
+ };
1333
+ }
1334
+ if (!entry.install && entry.source === "smithery") {
1335
+ const fetched = await fetchSmitheryDetail(entry.name);
1336
+ if (fetched) entry.install = fetched;
1337
+ }
1338
+ if (!entry.install) {
1339
+ return {
1340
+ status: 422,
1341
+ body: {
1342
+ error: `Could not derive install metadata for ${entry.name}`,
1343
+ hint: `npx -y @smithery/cli install ${entry.name}`
1344
+ }
1345
+ };
1346
+ }
1347
+ const spec = specStringFor(entry.name, entry.install);
1348
+ const cfg = readConfig(ctx.configPath);
1349
+ const existing = cfg.mcp ?? [];
1350
+ if (existing.includes(spec)) {
1351
+ return { status: 200, body: { added: false, alreadyPresent: true, spec, entry } };
1352
+ }
1353
+ cfg.mcp = [...existing, spec];
1354
+ writeConfig(cfg, ctx.configPath);
1355
+ ctx.audit?.({
1356
+ ts: Date.now(),
1357
+ action: "install-mcp-from-registry",
1358
+ payload: { name: entry.name, spec }
1359
+ });
1360
+ let bridged = false;
1361
+ let bridgeError;
1362
+ if (ctx.reloadMcp) {
1363
+ try {
1364
+ await ctx.reloadMcp();
1365
+ bridged = true;
1366
+ } catch (err) {
1367
+ bridgeError = err.message;
1368
+ }
1369
+ }
1370
+ return {
1371
+ status: 200,
1372
+ body: {
1373
+ added: true,
1374
+ requiresRestart: !ctx.reloadMcp || !!bridgeError,
1375
+ bridged,
1376
+ bridgeError,
1377
+ spec,
1378
+ entry
1379
+ }
1380
+ };
1381
+ } catch (err) {
1382
+ return { status: 500, body: { error: err.message } };
1383
+ }
1384
+ }
1385
+ if (method === "POST" && rest[0] === "invoke") {
1386
+ if (!ctx.invokeMcpTool) {
1387
+ return {
1388
+ status: 503,
1389
+ body: { error: "MCP invocation requires an attached session." }
1390
+ };
1391
+ }
1392
+ const { server, tool, args } = parseBody5(body);
1393
+ if (typeof server !== "string" || typeof tool !== "string") {
1394
+ return { status: 400, body: { error: "server + tool (strings) required" } };
1395
+ }
1396
+ try {
1397
+ const result = await ctx.invokeMcpTool(
1398
+ server,
1399
+ tool,
1400
+ typeof args === "object" && args !== null ? args : {}
1401
+ );
1402
+ return { status: 200, body: { result } };
1403
+ } catch (err) {
1404
+ return { status: 500, body: { error: err.message } };
1405
+ }
1406
+ }
1407
+ return { status: 405, body: { error: `method ${method} not supported on this path` } };
1408
+ }
1409
+
1410
+ // src/server/api/memory.ts
1411
+ import { createHash } from "crypto";
1412
+ import {
1413
+ existsSync as existsSync5,
1414
+ mkdirSync as mkdirSync2,
1415
+ readFileSync as readFileSync4,
1416
+ readdirSync as readdirSync3,
1417
+ statSync as statSync3,
1418
+ unlinkSync,
1419
+ writeFileSync as writeFileSync2
1420
+ } from "fs";
1421
+ import { homedir as homedir2 } from "os";
1422
+ import { basename, dirname as dirname3, join as join5, resolve as resolvePath } from "path";
1423
+ function projectHash(rootDir) {
1424
+ return createHash("sha1").update(resolvePath(rootDir)).digest("hex").slice(0, 16);
1425
+ }
1426
+ function globalMemoryDir() {
1427
+ return join5(homedir2(), ".luckerr", "memory", "global");
1428
+ }
1429
+ function projectMemoryDir(rootDir) {
1430
+ return join5(homedir2(), ".luckerr", "memory", projectHash(rootDir));
1431
+ }
1432
+ function parseBody6(raw) {
1433
+ if (!raw) return {};
1434
+ try {
1435
+ const parsed = JSON.parse(raw);
1436
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1437
+ } catch {
1438
+ return {};
1439
+ }
1440
+ }
1441
+ var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
1442
+ function listMemoryFiles(dir) {
1443
+ if (!existsSync5(dir)) return [];
1444
+ try {
1445
+ return readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => {
1446
+ const stat = statSync3(join5(dir, f));
1447
+ return {
1448
+ name: f.replace(/\.md$/, ""),
1449
+ size: stat.size,
1450
+ mtime: stat.mtime.getTime()
1451
+ };
1452
+ }).sort((a, b) => b.mtime - a.mtime);
1453
+ } catch {
1454
+ return [];
1455
+ }
1456
+ }
1457
+ async function handleMemory(method, rest, body, ctx) {
1458
+ const cwd = ctx.getCurrentCwd?.();
1459
+ const globalDir = globalMemoryDir();
1460
+ const projectMemDir = cwd ? projectMemoryDir(cwd) : "";
1461
+ if (method === "GET" && rest.length === 0) {
1462
+ const existingProjectMemory = cwd ? findProjectMemoryPath(cwd) : null;
1463
+ const projectMemoryPath = existingProjectMemory ?? (cwd ? join5(cwd, PROJECT_MEMORY_FILE) : null);
1464
+ const projectMemoryExists = existingProjectMemory !== null;
1465
+ return {
1466
+ status: 200,
1467
+ body: {
1468
+ project: {
1469
+ path: projectMemoryPath,
1470
+ exists: projectMemoryExists,
1471
+ file: projectMemoryPath ? basename(projectMemoryPath) : PROJECT_MEMORY_FILE
1472
+ },
1473
+ global: {
1474
+ path: globalDir,
1475
+ files: listMemoryFiles(globalDir)
1476
+ },
1477
+ projectMem: {
1478
+ path: projectMemDir,
1479
+ files: projectMemDir ? listMemoryFiles(projectMemDir) : []
1480
+ }
1481
+ }
1482
+ };
1483
+ }
1484
+ const [scope, ...nameParts] = rest;
1485
+ const name = nameParts.join("/");
1486
+ if (method === "GET") {
1487
+ if (scope === "project") {
1488
+ if (!cwd) return { status: 503, body: { error: "no active project" } };
1489
+ const path = findProjectMemoryPath(cwd);
1490
+ if (!path) return { status: 404, body: { error: "project memory file not found" } };
1491
+ return { status: 200, body: { path, body: readFileSync4(path, "utf8") } };
1492
+ }
1493
+ if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
1494
+ const dir = scope === "global" ? globalDir : projectMemDir;
1495
+ if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
1496
+ const path = join5(dir, `${name}.md`);
1497
+ if (!existsSync5(path)) return { status: 404, body: { error: "not found" } };
1498
+ return { status: 200, body: { path, body: readFileSync4(path, "utf8") } };
1499
+ }
1500
+ return { status: 400, body: { error: "bad scope or name" } };
1501
+ }
1502
+ if (method === "POST") {
1503
+ const { body: contents } = parseBody6(body);
1504
+ if (typeof contents !== "string") {
1505
+ return { status: 400, body: { error: "body (string) required" } };
1506
+ }
1507
+ if (scope === "project") {
1508
+ if (!cwd) return { status: 503, body: { error: "no active project" } };
1509
+ const path = resolveProjectMemoryWritePath(cwd);
1510
+ mkdirSync2(dirname3(path), { recursive: true });
1511
+ writeFileSync2(path, contents, "utf8");
1512
+ ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, path } });
1513
+ return { status: 200, body: { saved: true, path } };
1514
+ }
1515
+ if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
1516
+ const dir = scope === "global" ? globalDir : projectMemDir;
1517
+ if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
1518
+ mkdirSync2(dir, { recursive: true });
1519
+ const path = join5(dir, `${name}.md`);
1520
+ writeFileSync2(path, contents, "utf8");
1521
+ ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, name, path } });
1522
+ return { status: 200, body: { saved: true, path } };
1523
+ }
1524
+ return { status: 400, body: { error: "bad scope or name" } };
1525
+ }
1526
+ if (method === "DELETE") {
1527
+ if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
1528
+ const dir = scope === "global" ? globalDir : projectMemDir;
1529
+ if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
1530
+ const path = join5(dir, `${name}.md`);
1531
+ if (existsSync5(path)) {
1532
+ unlinkSync(path);
1533
+ ctx.audit?.({ ts: Date.now(), action: "delete-memory", payload: { scope, name, path } });
1534
+ return { status: 200, body: { deleted: true } };
1535
+ }
1536
+ return { status: 404, body: { error: "not found" } };
1537
+ }
1538
+ if (scope === "project") {
1539
+ if (!cwd) return { status: 503, body: { error: "no active project" } };
1540
+ const path = findProjectMemoryPath(cwd);
1541
+ if (path) {
1542
+ unlinkSync(path);
1543
+ ctx.audit?.({ ts: Date.now(), action: "delete-memory", payload: { scope, path } });
1544
+ return { status: 200, body: { deleted: true } };
1545
+ }
1546
+ return { status: 404, body: { error: "not found" } };
1547
+ }
1548
+ return { status: 400, body: { error: "bad scope or name" } };
1549
+ }
1550
+ return { status: 405, body: { error: `method ${method} not supported` } };
1551
+ }
1552
+
1553
+ // src/server/api/messages.ts
1554
+ async function handleMessages(method, _rest, _body, ctx) {
1555
+ if (method !== "GET") {
1556
+ return { status: 405, body: { error: "GET only" } };
1557
+ }
1558
+ const messages = ctx.getMessages ? ctx.getMessages() : [];
1559
+ return {
1560
+ status: 200,
1561
+ body: {
1562
+ messages,
1563
+ busy: ctx.isBusy ? ctx.isBusy() : false
1564
+ }
1565
+ };
1566
+ }
1567
+
1568
+ // src/server/api/modal.ts
1569
+ function parsePickerResolution(body) {
1570
+ const { action, id, text, query } = body;
1571
+ if (typeof action !== "string") return { error: "picker action required" };
1572
+ switch (action) {
1573
+ case "pick":
1574
+ case "delete":
1575
+ case "install":
1576
+ case "uninstall":
1577
+ if (typeof id !== "string" || !id) return { error: `picker ${action} requires id` };
1578
+ return { action, id };
1579
+ case "rename":
1580
+ if (typeof id !== "string" || !id) return { error: "picker rename requires id" };
1581
+ if (typeof text !== "string") return { error: "picker rename requires text" };
1582
+ return { action: "rename", id, text };
1583
+ case "new":
1584
+ return typeof text === "string" && text ? { action: "new", text } : { action: "new" };
1585
+ case "load-more":
1586
+ return { action: "load-more" };
1587
+ case "refine":
1588
+ if (typeof query !== "string") return { error: "picker refine requires query" };
1589
+ return { action: "refine", query };
1590
+ case "cancel":
1591
+ return { action: "cancel" };
1592
+ default:
1593
+ return { error: `unknown picker action: ${action}` };
1594
+ }
1595
+ }
1596
+ function parseBody7(raw) {
1597
+ if (!raw) return {};
1598
+ try {
1599
+ const parsed = JSON.parse(raw);
1600
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1601
+ } catch {
1602
+ return {};
1603
+ }
1604
+ }
1605
+ async function handleModal(method, rest, body, ctx) {
1606
+ if (method === "GET" && rest.length === 0) {
1607
+ return {
1608
+ status: 200,
1609
+ body: { modal: ctx.getActiveModal ? ctx.getActiveModal() : null }
1610
+ };
1611
+ }
1612
+ if (method === "POST" && rest[0] === "resolve") {
1613
+ const parsed = parseBody7(body);
1614
+ const { kind, choice, text } = parsed;
1615
+ if (kind === "shell") {
1616
+ if (!ctx.resolveShellConfirm) {
1617
+ return { status: 503, body: { error: "shell modal resolution not wired" } };
1618
+ }
1619
+ if (choice !== "run_once" && choice !== "always_allow" && choice !== "deny") {
1620
+ return {
1621
+ status: 400,
1622
+ body: { error: "shell choice must be run_once / always_allow / deny" }
1623
+ };
1624
+ }
1625
+ ctx.resolveShellConfirm(choice);
1626
+ return { status: 200, body: { resolved: true } };
1627
+ }
1628
+ if (kind === "choice") {
1629
+ if (!ctx.resolveChoiceConfirm) {
1630
+ return { status: 503, body: { error: "choice modal resolution not wired" } };
1631
+ }
1632
+ const c = choice;
1633
+ if (!c || typeof c !== "object") {
1634
+ return { status: 400, body: { error: "choice must be an object with a kind field" } };
1635
+ }
1636
+ if (c.kind === "pick" && typeof c.optionId === "string") {
1637
+ ctx.resolveChoiceConfirm({ kind: "pick", optionId: c.optionId });
1638
+ return { status: 200, body: { resolved: true } };
1639
+ }
1640
+ if (c.kind === "custom" && typeof c.text === "string") {
1641
+ ctx.resolveChoiceConfirm({ kind: "custom", text: c.text });
1642
+ return { status: 200, body: { resolved: true } };
1643
+ }
1644
+ if (c.kind === "cancel") {
1645
+ ctx.resolveChoiceConfirm({ kind: "cancel" });
1646
+ return { status: 200, body: { resolved: true } };
1647
+ }
1648
+ return { status: 400, body: { error: "unknown choice resolution shape" } };
1649
+ }
1650
+ if (kind === "plan") {
1651
+ if (!ctx.resolvePlanConfirm) {
1652
+ return { status: 503, body: { error: "plan modal resolution not wired" } };
1653
+ }
1654
+ if (choice !== "approve" && choice !== "refine" && choice !== "cancel") {
1655
+ return { status: 400, body: { error: "plan choice must be approve / refine / cancel" } };
1656
+ }
1657
+ ctx.resolvePlanConfirm(choice, typeof text === "string" && text.trim() ? text : void 0);
1658
+ return { status: 200, body: { resolved: true } };
1659
+ }
1660
+ if (kind === "edit-review") {
1661
+ if (!ctx.resolveEditReview) {
1662
+ return { status: 503, body: { error: "edit-review modal resolution not wired" } };
1663
+ }
1664
+ if (choice !== "apply" && choice !== "reject" && choice !== "apply-rest-of-turn" && choice !== "flip-to-auto") {
1665
+ return { status: 400, body: { error: "edit-review choice invalid" } };
1666
+ }
1667
+ ctx.resolveEditReview(choice);
1668
+ return { status: 200, body: { resolved: true } };
1669
+ }
1670
+ if (kind === "checkpoint") {
1671
+ if (!ctx.resolveCheckpointConfirm) {
1672
+ return { status: 503, body: { error: "checkpoint modal resolution not wired" } };
1673
+ }
1674
+ if (choice !== "continue" && choice !== "revise" && choice !== "stop") {
1675
+ return {
1676
+ status: 400,
1677
+ body: { error: "checkpoint choice must be continue / revise / stop" }
1678
+ };
1679
+ }
1680
+ ctx.resolveCheckpointConfirm(
1681
+ choice,
1682
+ typeof text === "string" && text.trim() ? text : void 0
1683
+ );
1684
+ return { status: 200, body: { resolved: true } };
1685
+ }
1686
+ if (kind === "revision") {
1687
+ if (!ctx.resolveReviseConfirm) {
1688
+ return { status: 503, body: { error: "revision modal resolution not wired" } };
1689
+ }
1690
+ if (choice !== "accept" && choice !== "reject") {
1691
+ return { status: 400, body: { error: "revision choice must be accept / reject" } };
1692
+ }
1693
+ ctx.resolveReviseConfirm(choice);
1694
+ return { status: 200, body: { resolved: true } };
1695
+ }
1696
+ if (kind === "picker") {
1697
+ if (!ctx.resolvePicker) {
1698
+ return { status: 503, body: { error: "picker modal resolution not wired" } };
1699
+ }
1700
+ const resolution = parsePickerResolution(parsed);
1701
+ if ("error" in resolution) {
1702
+ return { status: 400, body: { error: resolution.error } };
1703
+ }
1704
+ ctx.resolvePicker(resolution);
1705
+ return { status: 200, body: { resolved: true } };
1706
+ }
1707
+ if (kind === "viewer") {
1708
+ if (!ctx.resolveViewer) {
1709
+ return { status: 503, body: { error: "viewer modal resolution not wired" } };
1710
+ }
1711
+ if (parsed.action !== "close") {
1712
+ return { status: 400, body: { error: "viewer action must be close" } };
1713
+ }
1714
+ ctx.resolveViewer({ action: "close" });
1715
+ return { status: 200, body: { resolved: true } };
1716
+ }
1717
+ return { status: 400, body: { error: `unknown modal kind: ${String(kind)}` } };
1718
+ }
1719
+ return { status: 405, body: { error: `method ${method} not supported on this path` } };
1720
+ }
1721
+
1722
+ // src/server/api/models.ts
1723
+ async function handleModels(method, rest, body, ctx) {
1724
+ if (method === "POST" && rest[0] === "fetch") {
1725
+ return handleFetchModels(body, ctx);
1726
+ }
1727
+ if (method !== "GET") return { status: 405, body: { error: "GET or POST /fetch only" } };
1728
+ const cfg = readConfig(ctx.configPath);
1729
+ const currentModel = ctx.loop?.model ?? null;
1730
+ const currentProvider = currentModel ? resolveProvider(currentModel) : void 0;
1731
+ const activeProviderId = currentProvider?.id ?? cfg.provider ?? "deepseek";
1732
+ const pricingSnapshot = {};
1733
+ const providers = {};
1734
+ const seen = /* @__PURE__ */ new Set();
1735
+ const displayModels = [];
1736
+ const addModel = (mid, p) => {
1737
+ if (seen.has(mid)) return;
1738
+ seen.add(mid);
1739
+ displayModels.push(mid);
1740
+ providers[mid] = {
1741
+ providerId: p.id,
1742
+ providerLabel: p.label,
1743
+ defaultModel: p.defaultModel,
1744
+ contextTokens: contextTokensFor(mid)
1745
+ };
1746
+ pricingSnapshot[mid] = pricingFor(mid);
1747
+ };
1748
+ if (currentModel && !seen.has(currentModel)) {
1749
+ const p = resolveProvider(currentModel);
1750
+ if (p) addModel(currentModel, p);
1751
+ }
1752
+ const allProviders = listProviders();
1753
+ const providersWithKeys = allProviders.filter(
1754
+ (p) => Boolean(process.env[p.auth.envKey] ?? cfg.providerKeys?.[p.id])
1755
+ );
1756
+ const fetchResults = await Promise.allSettled(
1757
+ providersWithKeys.map((p) => fetchLiveModels(p, ctx.configPath))
1758
+ );
1759
+ for (let i = 0; i < providersWithKeys.length; i++) {
1760
+ const p = providersWithKeys[i];
1761
+ const result = fetchResults[i];
1762
+ if (result.status === "fulfilled" && result.value.length > 0) {
1763
+ for (const mid of result.value) addModel(mid, p);
1764
+ } else {
1765
+ for (const mid of p.models) addModel(mid, p);
1766
+ }
1767
+ }
1768
+ const providersList = allProviders.map((p) => ({
1769
+ id: p.id,
1770
+ label: p.label,
1771
+ defaultModel: p.defaultModel,
1772
+ hasKey: Boolean(process.env[p.auth.envKey] ?? cfg.providerKeys?.[p.id]),
1773
+ modelsEndpoint: Boolean(p.endpoints.models)
1774
+ }));
1775
+ return {
1776
+ status: 200,
1777
+ body: {
1778
+ models: displayModels,
1779
+ current: currentModel,
1780
+ activeProviderId,
1781
+ pricing: Object.keys(pricingSnapshot).length > 0 ? pricingSnapshot : DEEPSEEK_PRICING,
1782
+ providers,
1783
+ providersList,
1784
+ apiKeySet: providersWithKeys.length > 0
1785
+ }
1786
+ };
1787
+ }
1788
+ async function fetchLiveModels(profile, configPath) {
1789
+ if (!profile.endpoints.models) return [];
1790
+ const apiKey = loadProviderApiKey(profile.id, profile.auth.envKey, configPath);
1791
+ if (!apiKey) return [];
1792
+ const baseUrl = loadProviderBaseUrl(profile.id, configPath) ?? profile.endpoints.chat;
1793
+ try {
1794
+ const client = new OpenAICompatClient({
1795
+ provider: profile,
1796
+ apiKey,
1797
+ baseUrl,
1798
+ timeoutMs: 1e4
1799
+ });
1800
+ const list = await client.listModels();
1801
+ if (!list || !Array.isArray(list.data) || list.data.length === 0) {
1802
+ return [];
1803
+ }
1804
+ return list.data.map((m) => m.id);
1805
+ } catch {
1806
+ return [];
1807
+ }
1808
+ }
1809
+ function parseFetchBody(raw) {
1810
+ if (!raw) return {};
1811
+ try {
1812
+ const parsed = JSON.parse(raw);
1813
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1814
+ } catch {
1815
+ return {};
1816
+ }
1817
+ }
1818
+ async function handleFetchModels(body, ctx) {
1819
+ const fields = parseFetchBody(body);
1820
+ const providerId = fields.providerId;
1821
+ if (!providerId || typeof providerId !== "string") {
1822
+ return { status: 400, body: { error: "providerId is required" } };
1823
+ }
1824
+ const profile = listProviders().find((p) => p.id === providerId);
1825
+ if (!profile) {
1826
+ return { status: 404, body: { error: `unknown provider: ${providerId}` } };
1827
+ }
1828
+ let apiKey;
1829
+ if (fields.apiKey && typeof fields.apiKey === "string" && fields.apiKey.trim()) {
1830
+ apiKey = fields.apiKey.trim();
1831
+ } else {
1832
+ apiKey = loadProviderApiKey(providerId, profile.auth.envKey, ctx.configPath);
1833
+ }
1834
+ if (!apiKey) {
1835
+ return {
1836
+ status: 400,
1837
+ body: {
1838
+ error: `No API key for ${profile.label}. Set ${profile.auth.envKey} or enter one below.`,
1839
+ needsKey: true
1840
+ }
1841
+ };
1842
+ }
1843
+ const baseUrl = fields.baseUrl && typeof fields.baseUrl === "string" && fields.baseUrl.trim() ? fields.baseUrl.trim() : loadProviderBaseUrl(providerId, ctx.configPath) ?? profile.endpoints.chat;
1844
+ const liveModels = await fetchLiveModels(profile, ctx.configPath);
1845
+ if (liveModels.length > 0) {
1846
+ return {
1847
+ status: 200,
1848
+ body: {
1849
+ models: liveModels,
1850
+ source: "live",
1851
+ providerId,
1852
+ providerLabel: profile.label
1853
+ }
1854
+ };
1855
+ }
1856
+ return {
1857
+ status: 200,
1858
+ body: {
1859
+ models: profile.models,
1860
+ source: "static",
1861
+ providerId,
1862
+ providerLabel: profile.label,
1863
+ note: profile.endpoints.models ? "API fetch failed; showing static catalog." : "Provider has no /models endpoint; showing static catalog."
1864
+ }
1865
+ };
1866
+ }
1867
+
1868
+ // src/server/api/cockpit-events.ts
1869
+ import { existsSync as existsSync6 } from "fs";
1870
+ var DAY_MS = 864e5;
1871
+ var RECENT_FILES_CAP = 8;
1872
+ var PLAN_FEED_CAP = 4;
1873
+ var TOOL_FEED_CAP = 6;
1874
+ function computeEventsCockpit(now = Date.now(), sessionsDirOverride) {
1875
+ const dir = sessionsDirOverride ?? sessionsDir();
1876
+ if (!existsSync6(dir)) {
1877
+ return { toolCalls24h: null, recentPlans: null, toolActivity: null };
1878
+ }
1879
+ const files = recentEventFiles(dir, now, RECENT_FILES_CAP);
1880
+ if (files.length === 0) {
1881
+ return { toolCalls24h: null, recentPlans: null, toolActivity: null };
1882
+ }
1883
+ let calls24h = 0;
1884
+ let callsPrior24h = 0;
1885
+ const cutoff24h = now - DAY_MS;
1886
+ const cutoff48h = now - 2 * DAY_MS;
1887
+ const allTools = [];
1888
+ const allPlans = [];
1889
+ for (const file of files) {
1890
+ const events = readEventLogFile(file);
1891
+ if (events.length === 0) continue;
1892
+ countToolCalls(events, cutoff24h, cutoff48h, (in24h) => {
1893
+ if (in24h) calls24h++;
1894
+ else callsPrior24h++;
1895
+ });
1896
+ collectToolActivity(events, allTools);
1897
+ collectPlans(events, allPlans);
1898
+ }
1899
+ allTools.sort((a, b) => b.whenMs - a.whenMs);
1900
+ allPlans.sort((a, b) => b.whenMs - a.whenMs);
1901
+ return {
1902
+ toolCalls24h: { total: calls24h, delta: calls24h - callsPrior24h },
1903
+ recentPlans: allPlans.slice(0, PLAN_FEED_CAP),
1904
+ toolActivity: allTools.slice(0, TOOL_FEED_CAP)
1905
+ };
1906
+ }
1907
+ function countToolCalls(events, cutoff24h, cutoff48h, onCall) {
1908
+ for (const ev of events) {
1909
+ if (ev.type !== "tool.intent") continue;
1910
+ const ts = parseTs(ev.ts);
1911
+ if (ts === null) continue;
1912
+ if (ts >= cutoff24h) onCall(true);
1913
+ else if (ts >= cutoff48h) onCall(false);
1914
+ }
1915
+ }
1916
+ function collectToolActivity(events, into) {
1917
+ const intentByCallId = /* @__PURE__ */ new Map();
1918
+ for (const ev of events) {
1919
+ if (ev.type === "tool.intent") {
1920
+ const ts = parseTs(ev.ts);
1921
+ if (ts !== null) intentByCallId.set(ev.callId, { name: ev.name, args: ev.args, ts });
1922
+ } else if (ev.type === "tool.result") {
1923
+ const intent = intentByCallId.get(ev.callId);
1924
+ if (!intent) continue;
1925
+ into.push({
1926
+ name: intent.name,
1927
+ args: summarizeArgs(intent.args),
1928
+ level: ev.ok ? "ok" : "err",
1929
+ whenMs: intent.ts
1930
+ });
1931
+ } else if (ev.type === "tool.denied") {
1932
+ const intent = intentByCallId.get(ev.callId);
1933
+ if (!intent) continue;
1934
+ into.push({
1935
+ name: intent.name,
1936
+ args: summarizeArgs(intent.args),
1937
+ level: "warn",
1938
+ whenMs: intent.ts
1939
+ });
1940
+ }
1941
+ }
1942
+ }
1943
+ function collectPlans(events, into) {
1944
+ let current = null;
1945
+ let completed = /* @__PURE__ */ new Set();
1946
+ for (const ev of events) {
1947
+ if (ev.type === "plan.submitted") {
1948
+ if (current) {
1949
+ into.push(buildPlan(current, completed));
1950
+ }
1951
+ const ts = parseTs(ev.ts);
1952
+ if (ts === null) {
1953
+ current = null;
1954
+ continue;
1955
+ }
1956
+ current = {
1957
+ id: `${ev.id}`,
1958
+ title: planTitle(ev.body, ev.steps),
1959
+ totalSteps: ev.steps.length,
1960
+ whenMs: ts
1961
+ };
1962
+ completed = /* @__PURE__ */ new Set();
1963
+ } else if (ev.type === "plan.step.completed") {
1964
+ if (!current) continue;
1965
+ completed.add(ev.stepId);
1966
+ }
1967
+ }
1968
+ if (current) into.push(buildPlan(current, completed));
1969
+ }
1970
+ function buildPlan(current, completed) {
1971
+ return {
1972
+ id: current.id,
1973
+ title: current.title,
1974
+ totalSteps: current.totalSteps,
1975
+ completedSteps: completed.size,
1976
+ status: completed.size >= current.totalSteps && current.totalSteps > 0 ? "done" : "active",
1977
+ whenMs: current.whenMs
1978
+ };
1979
+ }
1980
+ function planTitle(body, steps) {
1981
+ const firstBodyLine = body.split(/\r?\n/).find((l) => l.trim().length > 0);
1982
+ if (firstBodyLine)
1983
+ return firstBodyLine.replace(/^#+\s*/, "").trim().slice(0, 80);
1984
+ if (steps.length > 0 && steps[0]) return steps[0].title.slice(0, 80);
1985
+ return "(plan)";
1986
+ }
1987
+ function summarizeArgs(args) {
1988
+ if (!args) return "";
1989
+ let parsed;
1990
+ try {
1991
+ parsed = JSON.parse(args);
1992
+ } catch {
1993
+ return args.slice(0, 60);
1994
+ }
1995
+ if (parsed && typeof parsed === "object") {
1996
+ const obj = parsed;
1997
+ const path = obj.path ?? obj.file_path ?? obj.filename;
1998
+ const command = obj.command;
1999
+ if (typeof command === "string")
2000
+ return command.length > 60 ? `${command.slice(0, 60)}\u2026` : command;
2001
+ if (typeof path === "string") return path;
2002
+ }
2003
+ return args.slice(0, 60);
2004
+ }
2005
+ function parseTs(ts) {
2006
+ const n = Date.parse(ts);
2007
+ return Number.isFinite(n) ? n : null;
2008
+ }
2009
+
2010
+ // src/server/api/cockpit.ts
2011
+ var TTL_MS = 3e4;
2012
+ var cache = /* @__PURE__ */ new Map();
2013
+ function computeCockpit(ctx, now = Date.now()) {
2014
+ return {
2015
+ balance: extractBalance(ctx.getStats?.() ?? null),
2016
+ currentSession: extractCurrentSession(ctx),
2017
+ ...readWarmCached(ctx.usageLogPath, now, ctx.sessionsDir)
2018
+ };
2019
+ }
2020
+ function extractBalance(stats) {
2021
+ const first = stats?.balance?.[0];
2022
+ if (!first) return null;
2023
+ return { currency: first.currency, total: first.total_balance };
2024
+ }
2025
+ function extractCurrentSession(ctx) {
2026
+ const id = ctx.getSessionName?.() ?? null;
2027
+ const stats = ctx.getStats?.() ?? null;
2028
+ const loop = ctx.loop;
2029
+ if (!id || !stats || !loop) return null;
2030
+ let completion = 0;
2031
+ for (const t of loop.stats.turns) completion += t.usage.completionTokens;
2032
+ return {
2033
+ id,
2034
+ turns: stats.turns,
2035
+ totalCostUsd: stats.totalCostUsd,
2036
+ lastPromptTokens: stats.lastPromptTokens,
2037
+ completionTokens: completion
2038
+ };
2039
+ }
2040
+ function readWarmCached(usageLogPath, now, sessionsDir2) {
2041
+ const cacheKey = `${usageLogPath}::${sessionsDir2 ?? ""}`;
2042
+ const hit = cache.get(cacheKey);
2043
+ if (hit && now - hit.ts < TTL_MS) return hit.data;
2044
+ const data = computeWarm(usageLogPath, now, sessionsDir2);
2045
+ cache.set(cacheKey, { ts: now, data });
2046
+ return data;
2047
+ }
2048
+ function computeWarm(usageLogPath, now, sessionsDir2) {
2049
+ const events = computeEventsCockpit(now, sessionsDir2);
2050
+ const records = readUsageLog(usageLogPath);
2051
+ if (records.length === 0) {
2052
+ return { tokens7d: null, cacheHit7d: null, costTrend14d: null, ...events };
2053
+ }
2054
+ const week = aggregateUsage(records, { now }).buckets[1];
2055
+ const priorWeekRecords = records.filter(
2056
+ (r) => r.ts < week.since && r.ts >= week.since - 7 * 864e5
2057
+ );
2058
+ const priorWeek = aggregateUsage(priorWeekRecords, { now: week.since }).buckets[1];
2059
+ const tokens7dTotal = week.promptTokens + week.completionTokens;
2060
+ const tokens7dPrior = priorWeek.promptTokens + priorWeek.completionTokens;
2061
+ const tokens7d = {
2062
+ total: tokens7dTotal,
2063
+ deltaPct: tokens7dPrior > 0 ? (tokens7dTotal - tokens7dPrior) / tokens7dPrior * 100 : null
2064
+ };
2065
+ const cacheHitRatio = bucketCacheHitRatio(week);
2066
+ const cacheHit7d = {
2067
+ ratio: cacheHitRatio,
2068
+ deltaPp: priorWeek.cacheHitTokens + priorWeek.cacheMissTokens > 0 ? (cacheHitRatio - bucketCacheHitRatio(priorWeek)) * 100 : null
2069
+ };
2070
+ return {
2071
+ tokens7d,
2072
+ cacheHit7d,
2073
+ costTrend14d: rollupDailyCost(records, now, 14),
2074
+ ...events
2075
+ };
2076
+ }
2077
+ function rollupDailyCost(records, now, days) {
2078
+ const since = now - days * 864e5;
2079
+ const buckets = /* @__PURE__ */ new Map();
2080
+ for (let i = 0; i < days; i++) {
2081
+ buckets.set(localDateKey(now - i * 864e5), 0);
2082
+ }
2083
+ for (const r of records) {
2084
+ if (r.ts < since) continue;
2085
+ const key = localDateKey(r.ts);
2086
+ if (!buckets.has(key)) continue;
2087
+ buckets.set(key, (buckets.get(key) ?? 0) + r.costUsd);
2088
+ }
2089
+ return Array.from(buckets.entries()).map(([date, usd]) => ({ date, usd })).sort((a, b) => a.date < b.date ? -1 : 1);
2090
+ }
2091
+ function localDateKey(ts) {
2092
+ const d = new Date(ts);
2093
+ const y = d.getFullYear();
2094
+ const m = String(d.getMonth() + 1).padStart(2, "0");
2095
+ const day = String(d.getDate()).padStart(2, "0");
2096
+ return `${y}-${m}-${day}`;
2097
+ }
2098
+
2099
+ // src/server/api/overview.ts
2100
+ async function handleOverview(method, _rest, _body, ctx) {
2101
+ if (method !== "GET") {
2102
+ return { status: 405, body: { error: "GET only" } };
2103
+ }
2104
+ const cfg = readConfig(ctx.configPath);
2105
+ const cwd = ctx.getCurrentCwd?.() ?? null;
2106
+ const semanticIndexExists = cwd ? await indexExists(cwd).catch(() => false) : null;
2107
+ const currentModel = ctx.loop?.model ?? null;
2108
+ const currentProvider = currentModel ? resolveProvider(currentModel) : void 0;
2109
+ const providerId = currentProvider?.id ?? cfg.provider ?? "deepseek";
2110
+ let thinkingSupported = false;
2111
+ if (currentProvider && currentModel) {
2112
+ const t = currentProvider.thinking;
2113
+ if (t.transport !== "none") {
2114
+ thinkingSupported = !t.thinkingModels || t.thinkingModels.includes(currentModel);
2115
+ }
2116
+ }
2117
+ const overview = {
2118
+ version: VERSION,
2119
+ mode: ctx.mode,
2120
+ latestVersion: ctx.getLatestVersion?.() ?? null,
2121
+ session: ctx.getSessionName?.() ?? null,
2122
+ cwd,
2123
+ model: ctx.loop?.model ?? null,
2124
+ providerId,
2125
+ editMode: ctx.getEditMode?.() ?? null,
2126
+ planMode: ctx.getPlanMode?.() ?? null,
2127
+ pendingEdits: ctx.getPendingEditCount?.() ?? null,
2128
+ mcpServerCount: ctx.mcpServers?.length ?? null,
2129
+ toolCount: ctx.tools ? ctx.tools.size : null,
2130
+ preset: cfg.preset ?? "auto",
2131
+ reasoningEffort: cfg.reasoningEffort ?? "max",
2132
+ budgetUsd: ctx.loop?.budgetUsd ?? null,
2133
+ stats: ctx.getStats?.() ?? null,
2134
+ semanticIndexExists,
2135
+ thinkingSupported,
2136
+ cockpit: computeCockpit(ctx)
2137
+ };
2138
+ return { status: 200, body: overview };
2139
+ }
2140
+
2141
+ // src/server/api/permissions.ts
2142
+ function parseBody8(raw) {
2143
+ if (!raw) return {};
2144
+ try {
2145
+ const parsed = JSON.parse(raw);
2146
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
2147
+ } catch {
2148
+ return {};
2149
+ }
2150
+ }
2151
+ async function handlePermissions(method, rest, body, ctx) {
2152
+ if (method === "GET" && rest.length === 0) {
2153
+ const cwd2 = ctx.getCurrentCwd?.();
2154
+ return {
2155
+ status: 200,
2156
+ body: {
2157
+ currentCwd: cwd2 ?? null,
2158
+ editMode: ctx.getEditMode?.() ?? null,
2159
+ builtin: [...BUILTIN_ALLOWLIST],
2160
+ project: cwd2 ? loadProjectShellAllowed(cwd2, ctx.configPath) : []
2161
+ }
2162
+ };
2163
+ }
2164
+ const cwd = ctx.getCurrentCwd?.();
2165
+ if (!cwd) {
2166
+ return {
2167
+ status: 503,
2168
+ body: {
2169
+ error: "no active project \u2014 mutations require an attached dashboard session (run `/dashboard` from inside `luckerr code`)."
2170
+ }
2171
+ };
2172
+ }
2173
+ if (method === "POST" && rest.length === 0) {
2174
+ const { prefix } = parseBody8(body);
2175
+ if (typeof prefix !== "string" || !prefix.trim()) {
2176
+ return { status: 400, body: { error: "prefix (string) required" } };
2177
+ }
2178
+ const trimmed = prefix.trim();
2179
+ if (BUILTIN_ALLOWLIST.includes(trimmed)) {
2180
+ return {
2181
+ status: 409,
2182
+ body: {
2183
+ error: `\`${trimmed}\` is already in the builtin allowlist \u2014 no project entry needed.`
2184
+ }
2185
+ };
2186
+ }
2187
+ const before = loadProjectShellAllowed(cwd, ctx.configPath);
2188
+ if (before.includes(trimmed)) {
2189
+ return { status: 200, body: { added: false, prefix: trimmed, alreadyPresent: true } };
2190
+ }
2191
+ addProjectShellAllowed(cwd, trimmed, ctx.configPath);
2192
+ ctx.audit?.({
2193
+ ts: Date.now(),
2194
+ action: "add-allowlist",
2195
+ payload: { prefix: trimmed, project: cwd }
2196
+ });
2197
+ return { status: 200, body: { added: true, prefix: trimmed } };
2198
+ }
2199
+ if (method === "DELETE" && rest.length === 0) {
2200
+ const { prefix } = parseBody8(body);
2201
+ if (typeof prefix !== "string" || !prefix.trim()) {
2202
+ return { status: 400, body: { error: "prefix (string) required" } };
2203
+ }
2204
+ const trimmed = prefix.trim();
2205
+ if (BUILTIN_ALLOWLIST.includes(trimmed)) {
2206
+ return {
2207
+ status: 409,
2208
+ body: {
2209
+ error: `\`${trimmed}\` is in the builtin allowlist (read-only); builtin entries can't be removed at runtime.`
2210
+ }
2211
+ };
2212
+ }
2213
+ const removed = removeProjectShellAllowed(cwd, trimmed, ctx.configPath);
2214
+ if (removed) {
2215
+ ctx.audit?.({
2216
+ ts: Date.now(),
2217
+ action: "remove-allowlist",
2218
+ payload: { prefix: trimmed, project: cwd }
2219
+ });
2220
+ }
2221
+ return { status: 200, body: { removed, prefix: trimmed } };
2222
+ }
2223
+ if (method === "POST" && rest[0] === "clear") {
2224
+ const { confirm } = parseBody8(body);
2225
+ if (confirm !== true) {
2226
+ return {
2227
+ status: 400,
2228
+ body: {
2229
+ error: "clear requires { confirm: true } in the body \u2014 guards against accidental wipe."
2230
+ }
2231
+ };
2232
+ }
2233
+ const dropped = clearProjectShellAllowed(cwd, ctx.configPath);
2234
+ if (dropped > 0) {
2235
+ ctx.audit?.({
2236
+ ts: Date.now(),
2237
+ action: "clear-allowlist",
2238
+ payload: { dropped, project: cwd }
2239
+ });
2240
+ }
2241
+ return { status: 200, body: { dropped } };
2242
+ }
2243
+ return { status: 405, body: { error: `method ${method} not supported on this path` } };
2244
+ }
2245
+
2246
+ // src/server/api/plans.ts
2247
+ async function handlePlans(method, _rest, _body, _ctx) {
2248
+ if (method !== "GET") {
2249
+ return { status: 405, body: { error: "GET only" } };
2250
+ }
2251
+ const out = [];
2252
+ for (const session of listSessions()) {
2253
+ const archives = listPlanArchives(session.name);
2254
+ for (const a of archives) {
2255
+ const total = a.steps.length;
2256
+ const done = a.completedStepIds.length;
2257
+ const row = {
2258
+ session: session.name,
2259
+ path: a.path,
2260
+ completedAt: a.completedAt,
2261
+ totalSteps: total,
2262
+ completedSteps: done,
2263
+ completionRatio: total > 0 ? done / total : 0,
2264
+ steps: a.steps,
2265
+ completedStepIds: a.completedStepIds
2266
+ };
2267
+ if (a.summary) row.summary = a.summary;
2268
+ out.push(row);
2269
+ }
2270
+ }
2271
+ out.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
2272
+ return { status: 200, body: { plans: out } };
2273
+ }
2274
+
2275
+ // src/server/api/project-tree.ts
2276
+ import { existsSync as existsSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2277
+ import { extname as extname3, join as join6, relative as relative2, sep as sep3 } from "path";
2278
+ var MAX_DEPTH2 = 6;
2279
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
2280
+ "node_modules",
2281
+ ".git",
2282
+ ".luckerr",
2283
+ "dist",
2284
+ "build",
2285
+ "out",
2286
+ ".next",
2287
+ "coverage",
2288
+ ".cache",
2289
+ "__pycache__",
2290
+ ".venv",
2291
+ ".pytest_cache"
2292
+ ]);
2293
+ var SKIP_EXTS2 = /* @__PURE__ */ new Set([
2294
+ ".png",
2295
+ ".jpg",
2296
+ ".jpeg",
2297
+ ".gif",
2298
+ ".webp",
2299
+ ".ico",
2300
+ ".pdf",
2301
+ ".zip",
2302
+ ".tar",
2303
+ ".gz",
2304
+ ".lock",
2305
+ ".woff",
2306
+ ".woff2",
2307
+ ".ttf"
2308
+ ]);
2309
+ async function handleProjectTree(method, _rest, _body, ctx) {
2310
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
2311
+ const cwd = ctx.getCurrentCwd?.();
2312
+ if (!cwd || !existsSync7(cwd)) {
2313
+ return { status: 503, body: { error: "no project directory available" } };
2314
+ }
2315
+ const tree = buildTree(cwd, cwd, 0);
2316
+ return { status: 200, body: { tree } };
2317
+ }
2318
+ function buildTree(root, dirPath, depth) {
2319
+ if (depth > MAX_DEPTH2) return [];
2320
+ let names;
2321
+ try {
2322
+ names = readdirSync4(dirPath);
2323
+ } catch {
2324
+ return [];
2325
+ }
2326
+ const nodes = [];
2327
+ const dirs = [];
2328
+ const files = [];
2329
+ for (const name of names) {
2330
+ if (SKIP_DIRS2.has(name)) continue;
2331
+ const full = join6(dirPath, name);
2332
+ let st;
2333
+ try {
2334
+ st = statSync4(full);
2335
+ } catch {
2336
+ continue;
2337
+ }
2338
+ if (st.isDirectory()) {
2339
+ dirs.push(name);
2340
+ } else if (st.isFile() && !SKIP_EXTS2.has(extname3(name).toLowerCase())) {
2341
+ files.push(name);
2342
+ }
2343
+ }
2344
+ dirs.sort();
2345
+ files.sort();
2346
+ for (const name of dirs) {
2347
+ const full = join6(dirPath, name);
2348
+ const rel = relative2(root, full).split(sep3).join("/");
2349
+ const children = buildTree(root, full, depth + 1);
2350
+ nodes.push({ name, path: rel, isDir: true, children });
2351
+ }
2352
+ for (const name of files) {
2353
+ const full = join6(dirPath, name);
2354
+ const rel = relative2(root, full).split(sep3).join("/");
2355
+ nodes.push({ name, path: rel, isDir: false });
2356
+ }
2357
+ return nodes;
2358
+ }
2359
+
2360
+ // src/server/api/review-diffs.ts
2361
+ async function handleReviewDiffs(method, _rest, _body, _ctx) {
2362
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
2363
+ return { status: 200, body: [] };
2364
+ }
2365
+
2366
+ // src/server/api/semantic.ts
2367
+ import { closeSync as closeSync3, fstatSync as fstatSync3, openSync as openSync3, readSync as readSync3 } from "fs";
2368
+ import { join as join7 } from "path";
2369
+ var JOBS = /* @__PURE__ */ new Map();
2370
+ var PULLS = /* @__PURE__ */ new Map();
2371
+ function getRoot(ctx) {
2372
+ const cwd = ctx.getCurrentCwd?.();
2373
+ return cwd ?? null;
2374
+ }
2375
+ async function handleSemantic(method, rest, body, ctx) {
2376
+ const sub = rest[0] ?? "";
2377
+ if (sub === "" && method === "GET") return await getStatus(ctx);
2378
+ if (sub === "config" && method === "GET") return getSemanticConfig(ctx);
2379
+ if (sub === "config" && method === "POST") return saveSemanticConfigApi(body, ctx);
2380
+ if (sub === "start" && method === "POST") return await startJob(body, ctx);
2381
+ if (sub === "stop" && method === "POST") return await stopJob(ctx);
2382
+ if (sub === "ollama" && method === "POST") {
2383
+ const action = rest[1] ?? "";
2384
+ if (action === "start") return await startDaemon(ctx);
2385
+ if (action === "pull") return await startPull(body, ctx);
2386
+ }
2387
+ if (sub === "search" && method === "POST") return await runSearch(body, ctx);
2388
+ return { status: 404, body: { error: "no such semantic endpoint" } };
2389
+ }
2390
+ async function runSearch(rawBody, ctx) {
2391
+ const root = getRoot(ctx);
2392
+ if (!root) {
2393
+ return { status: 503, body: { error: "search requires an attached code-mode session" } };
2394
+ }
2395
+ let parsed;
2396
+ try {
2397
+ parsed = JSON.parse(rawBody || "{}");
2398
+ } catch {
2399
+ return { status: 400, body: { error: "body must be JSON" } };
2400
+ }
2401
+ const query = typeof parsed.query === "string" ? parsed.query.trim() : "";
2402
+ if (!query) return { status: 400, body: { error: "query required" } };
2403
+ const topK = typeof parsed.topK === "number" && Number.isFinite(parsed.topK) ? Math.max(1, Math.min(16, Math.floor(parsed.topK))) : 8;
2404
+ const minScore = typeof parsed.minScore === "number" && Number.isFinite(parsed.minScore) ? Math.max(0, Math.min(1, parsed.minScore)) : 0.3;
2405
+ const startedAt = Date.now();
2406
+ const embedding = resolveSemanticEmbeddingConfig(ctx.configPath);
2407
+ try {
2408
+ const hits = await querySemantic(root, query, {
2409
+ topK,
2410
+ minScore,
2411
+ configPath: ctx.configPath
2412
+ });
2413
+ if (hits === null) {
2414
+ return { status: 404, body: { error: "no semantic index for this project" } };
2415
+ }
2416
+ return {
2417
+ status: 200,
2418
+ body: {
2419
+ hits: hits.map((h) => ({
2420
+ path: h.entry.path,
2421
+ startLine: h.entry.startLine,
2422
+ endLine: h.entry.endLine,
2423
+ score: h.score,
2424
+ snippet: h.entry.text
2425
+ })),
2426
+ elapsedMs: Date.now() - startedAt,
2427
+ provider: embedding.provider,
2428
+ model: embedding.model
2429
+ }
2430
+ };
2431
+ } catch (err) {
2432
+ return { status: 500, body: { error: err.message } };
2433
+ }
2434
+ }
2435
+ async function getStatus(ctx) {
2436
+ const root = getRoot(ctx);
2437
+ if (!root) {
2438
+ return {
2439
+ status: 200,
2440
+ body: {
2441
+ attached: false,
2442
+ reason: "Semantic indexing requires a code-mode session \u2014 run `/dashboard` from inside `luckerr code` instead of standalone `luckerr dashboard`."
2443
+ }
2444
+ };
2445
+ }
2446
+ const config = loadSemanticEmbeddingUserConfig(ctx.configPath);
2447
+ const configView = redactSemanticEmbeddingConfig(config);
2448
+ const resolved = resolveSemanticEmbeddingConfig(ctx.configPath);
2449
+ const [hasIndex, providerStatus, index] = await Promise.all([
2450
+ indexExists(root),
2451
+ getProviderStatusFromConfig(configView),
2452
+ readIndexMeta2(root, { provider: resolved.provider, model: resolved.model })
2453
+ ]);
2454
+ const job = JOBS.get(root) ?? null;
2455
+ const pull = providerStatus.kind === "ollama" ? PULLS.get(providerStatus.modelName) ?? null : null;
2456
+ return {
2457
+ status: 200,
2458
+ body: {
2459
+ attached: true,
2460
+ root,
2461
+ provider: configView.provider,
2462
+ providerConfig: configView,
2463
+ providerStatus,
2464
+ index: hasIndex ? index : { exists: false },
2465
+ ollama: providerStatus.kind === "ollama" ? providerStatus : void 0,
2466
+ job: job ? snapshotJob(job) : null,
2467
+ pull: pull ? snapshotPull(pull) : null
2468
+ }
2469
+ };
2470
+ }
2471
+ async function readIndexMeta2(root, current) {
2472
+ const dir = join7(root, INDEX_DIR_NAME);
2473
+ const dataPath = join7(dir, "index.jsonl");
2474
+ const diskMeta = await readIndexMeta(dir);
2475
+ if (!diskMeta) return { exists: false };
2476
+ let chunks = 0;
2477
+ const files = /* @__PURE__ */ new Set();
2478
+ let sizeBytes = 0;
2479
+ try {
2480
+ const fd = openSync3(dataPath, "r");
2481
+ let raw;
2482
+ try {
2483
+ const stat = fstatSync3(fd);
2484
+ sizeBytes = stat.size;
2485
+ const buf = Buffer.alloc(stat.size);
2486
+ let read = 0;
2487
+ while (read < stat.size) {
2488
+ const n = readSync3(fd, buf, read, stat.size - read, read);
2489
+ if (n <= 0) break;
2490
+ read += n;
2491
+ }
2492
+ raw = buf.toString("utf8", 0, read);
2493
+ } finally {
2494
+ closeSync3(fd);
2495
+ }
2496
+ for (const line of raw.split(/\r?\n/)) {
2497
+ if (!line.trim()) continue;
2498
+ chunks++;
2499
+ try {
2500
+ const rec = JSON.parse(line);
2501
+ if (typeof rec.p === "string") files.add(rec.p);
2502
+ } catch {
2503
+ }
2504
+ }
2505
+ } catch {
2506
+ }
2507
+ const mismatch = compareIndexIdentity(diskMeta, current);
2508
+ return {
2509
+ exists: true,
2510
+ provider: diskMeta.provider,
2511
+ chunks,
2512
+ files: files.size,
2513
+ dim: diskMeta.dim ?? 0,
2514
+ sizeBytes,
2515
+ lastBuiltMs: diskMeta.updatedAt ? Date.parse(diskMeta.updatedAt) || 0 : 0,
2516
+ model: diskMeta.model ?? "",
2517
+ builtWith: { provider: diskMeta.provider, model: diskMeta.model },
2518
+ current,
2519
+ compatible: mismatch === null,
2520
+ mismatch
2521
+ };
2522
+ }
2523
+ function snapshotPull(p) {
2524
+ return {
2525
+ startedAt: p.startedAt,
2526
+ status: p.status,
2527
+ lastLine: p.lastLine,
2528
+ exitCode: p.exitCode
2529
+ };
2530
+ }
2531
+ async function startDaemon(ctx) {
2532
+ const resolved = resolveSemanticEmbeddingConfig(ctx.configPath);
2533
+ if (resolved.provider !== "ollama") {
2534
+ return { status: 409, body: { error: "ollama actions require provider=ollama" } };
2535
+ }
2536
+ const r = await startOllamaDaemon({ baseUrl: resolved.baseUrl, timeoutMs: 15e3 }).catch(
2537
+ (err) => ({
2538
+ ready: false,
2539
+ pid: null,
2540
+ error: err.message
2541
+ })
2542
+ );
2543
+ if ("error" in r) return { status: 500, body: { ready: false, error: r.error } };
2544
+ return { status: r.ready ? 200 : 504, body: r };
2545
+ }
2546
+ async function startPull(body, ctx) {
2547
+ const resolved = resolveSemanticEmbeddingConfig(ctx.configPath);
2548
+ if (resolved.provider !== "ollama") {
2549
+ return { status: 409, body: { error: "ollama actions require provider=ollama" } };
2550
+ }
2551
+ let parsed = {};
2552
+ if (body) {
2553
+ try {
2554
+ parsed = JSON.parse(body);
2555
+ } catch {
2556
+ return { status: 400, body: { error: "invalid JSON body" } };
2557
+ }
2558
+ }
2559
+ const model = typeof parsed.model === "string" && parsed.model ? parsed.model : resolved.model;
2560
+ const existing = PULLS.get(model);
2561
+ if (existing && existing.status === "pulling") {
2562
+ return {
2563
+ status: 409,
2564
+ body: { error: `${model} is already pulling`, pull: snapshotPull(existing) }
2565
+ };
2566
+ }
2567
+ const rec = {
2568
+ startedAt: Date.now(),
2569
+ status: "pulling",
2570
+ lastLine: `pulling ${model}\u2026`,
2571
+ exitCode: null
2572
+ };
2573
+ PULLS.set(model, rec);
2574
+ void pullOllamaModel(model, {
2575
+ onLine: (line) => {
2576
+ if (line.trim().length > 0) rec.lastLine = line.trim();
2577
+ }
2578
+ }).then((code) => {
2579
+ rec.exitCode = code;
2580
+ rec.status = code === 0 ? "done" : "error";
2581
+ if (code !== 0 && (!rec.lastLine || !rec.lastLine.toLowerCase().includes("error"))) {
2582
+ rec.lastLine = `ollama pull exited with code ${code}`;
2583
+ }
2584
+ }).catch((err) => {
2585
+ rec.status = "error";
2586
+ rec.lastLine = err.message;
2587
+ });
2588
+ return { status: 202, body: { started: true, pull: snapshotPull(rec) } };
2589
+ }
2590
+ function snapshotJob(j) {
2591
+ return {
2592
+ startedAt: j.startedAt,
2593
+ finishedAt: j.finishedAt ?? null,
2594
+ cancelledAt: j.cancelledAt ?? null,
2595
+ phase: j.phase,
2596
+ lastPhase: j.lastPhase ?? null,
2597
+ rebuild: j.rebuild,
2598
+ filesScanned: j.filesScanned ?? null,
2599
+ filesChanged: j.filesChanged ?? null,
2600
+ filesSkipped: j.filesSkipped ?? null,
2601
+ chunksTotal: j.chunksTotal ?? null,
2602
+ chunksDone: j.chunksDone ?? null,
2603
+ aborted: j.aborted,
2604
+ result: j.result ?? null,
2605
+ error: j.error ?? null
2606
+ };
2607
+ }
2608
+ async function startJob(body, ctx) {
2609
+ const root = getRoot(ctx);
2610
+ if (!root) {
2611
+ return {
2612
+ status: 400,
2613
+ body: { error: "no project root \u2014 only available in attached (code-mode) dashboards" }
2614
+ };
2615
+ }
2616
+ const existing = JOBS.get(root);
2617
+ if (existing && (existing.phase === "setup" || existing.phase === "scan" || existing.phase === "embed" || existing.phase === "write")) {
2618
+ return {
2619
+ status: 409,
2620
+ body: { error: "an indexing job is already running", job: snapshotJob(existing) }
2621
+ };
2622
+ }
2623
+ let parsed = {};
2624
+ if (body) {
2625
+ try {
2626
+ parsed = JSON.parse(body);
2627
+ } catch {
2628
+ return { status: 400, body: { error: "invalid JSON body" } };
2629
+ }
2630
+ }
2631
+ const rebuild = parsed.rebuild === true;
2632
+ const job = {
2633
+ startedAt: Date.now(),
2634
+ phase: "setup",
2635
+ lastPhase: "setup",
2636
+ rebuild,
2637
+ aborted: false,
2638
+ controller: new AbortController()
2639
+ };
2640
+ JOBS.set(root, job);
2641
+ void runIndex(root, job, ctx).catch((err) => {
2642
+ job.phase = "error";
2643
+ job.finishedAt = Date.now();
2644
+ job.error = err instanceof Error ? err.message : String(err);
2645
+ });
2646
+ const resolved = resolveSemanticEmbeddingConfig(ctx.configPath);
2647
+ return {
2648
+ status: 202,
2649
+ body: {
2650
+ started: true,
2651
+ provider: resolved.provider,
2652
+ model: resolved.model,
2653
+ job: snapshotJob(job)
2654
+ }
2655
+ };
2656
+ }
2657
+ async function runIndex(root, job, ctx) {
2658
+ try {
2659
+ const resolved = resolveSemanticEmbeddingConfig(ctx.configPath);
2660
+ const result = await buildIndex(root, {
2661
+ rebuild: job.rebuild,
2662
+ configPath: ctx.configPath,
2663
+ signal: job.controller.signal,
2664
+ indexConfig: loadIndexConfig(ctx.configPath),
2665
+ onProgress: (p) => {
2666
+ job.phase = p.phase;
2667
+ if (p.phase !== "done") job.lastPhase = p.phase;
2668
+ if (p.filesScanned !== void 0) job.filesScanned = p.filesScanned;
2669
+ if (p.filesChanged !== void 0) job.filesChanged = p.filesChanged;
2670
+ if (p.filesSkipped !== void 0) job.filesSkipped = p.filesSkipped;
2671
+ if (p.chunksTotal !== void 0) job.chunksTotal = p.chunksTotal;
2672
+ if (p.chunksDone !== void 0) job.chunksDone = p.chunksDone;
2673
+ }
2674
+ });
2675
+ job.phase = "done";
2676
+ job.finishedAt = Date.now();
2677
+ job.result = result;
2678
+ if (ctx.tools && ctx.addToolToPrefix) {
2679
+ try {
2680
+ const added = await registerSemanticSearchTool(ctx.tools, { root, ...resolved });
2681
+ if (added) {
2682
+ const spec = ctx.tools.specs().find((s) => s.function.name === "semantic_search");
2683
+ if (spec) ctx.addToolToPrefix(spec);
2684
+ }
2685
+ } catch {
2686
+ }
2687
+ }
2688
+ } catch (err) {
2689
+ if (isAbortError(err)) {
2690
+ job.phase = "cancelled";
2691
+ job.cancelledAt = Date.now();
2692
+ job.finishedAt = job.cancelledAt;
2693
+ job.error = void 0;
2694
+ return;
2695
+ }
2696
+ job.phase = "error";
2697
+ job.finishedAt = Date.now();
2698
+ job.error = err instanceof Error ? err.message : String(err);
2699
+ }
2700
+ }
2701
+ async function stopJob(ctx) {
2702
+ const root = getRoot(ctx);
2703
+ if (!root) return { status: 400, body: { error: "no project root" } };
2704
+ const job = JOBS.get(root);
2705
+ if (!job || job.phase === "done" || job.phase === "error" || job.phase === "cancelled") {
2706
+ return { status: 404, body: { error: "no running job" } };
2707
+ }
2708
+ job.aborted = true;
2709
+ job.controller.abort(new Error("semantic indexing aborted"));
2710
+ return { status: 202, body: { stopping: true, job: snapshotJob(job) } };
2711
+ }
2712
+ function getSemanticConfig(ctx) {
2713
+ return {
2714
+ status: 200,
2715
+ body: redactSemanticEmbeddingConfig(loadSemanticEmbeddingUserConfig(ctx.configPath))
2716
+ };
2717
+ }
2718
+ function saveSemanticConfigApi(rawBody, ctx) {
2719
+ let parsed;
2720
+ try {
2721
+ parsed = JSON.parse(rawBody || "{}");
2722
+ } catch {
2723
+ return { status: 400, body: { error: "body must be JSON" } };
2724
+ }
2725
+ const existing = loadSemanticEmbeddingUserConfig(ctx.configPath);
2726
+ const next = {
2727
+ provider: parsed.provider === "openai-compat" ? "openai-compat" : "ollama",
2728
+ ollama: {
2729
+ baseUrl: typeof parsed.ollama?.baseUrl === "string" ? parsed.ollama.baseUrl : existing.ollama?.baseUrl,
2730
+ model: typeof parsed.ollama?.model === "string" ? parsed.ollama.model : existing.ollama?.model
2731
+ },
2732
+ openaiCompat: {
2733
+ baseUrl: typeof parsed.openaiCompat?.baseUrl === "string" ? parsed.openaiCompat.baseUrl : existing.openaiCompat?.baseUrl,
2734
+ apiKey: typeof parsed.openaiCompat?.apiKey === "string" ? parsed.openaiCompat.apiKey.trim() || existing.openaiCompat?.apiKey : existing.openaiCompat?.apiKey,
2735
+ model: typeof parsed.openaiCompat?.model === "string" ? parsed.openaiCompat.model : existing.openaiCompat?.model,
2736
+ extraBody: parsed.openaiCompat?.extraBody === void 0 ? existing.openaiCompat?.extraBody : parsed.openaiCompat.extraBody
2737
+ }
2738
+ };
2739
+ try {
2740
+ saveSemanticEmbeddingConfig(next, ctx.configPath);
2741
+ } catch (err) {
2742
+ return { status: 400, body: { error: err.message } };
2743
+ }
2744
+ ctx.audit?.({
2745
+ ts: Date.now(),
2746
+ action: "set-semantic-config",
2747
+ payload: { provider: next.provider }
2748
+ });
2749
+ return {
2750
+ status: 200,
2751
+ body: {
2752
+ changed: collectSemanticConfigChanges(existing, next),
2753
+ config: redactSemanticEmbeddingConfig(loadSemanticEmbeddingUserConfig(ctx.configPath))
2754
+ }
2755
+ };
2756
+ }
2757
+ function collectSemanticConfigChanges(before, after) {
2758
+ const left = JSON.stringify(before);
2759
+ const right = JSON.stringify(after);
2760
+ if (left === right) return [];
2761
+ return ["semantic"];
2762
+ }
2763
+ async function getProviderStatusFromConfig(config) {
2764
+ if (config.provider === "openai-compat") {
2765
+ return {
2766
+ kind: "openai-compat",
2767
+ ready: Boolean(
2768
+ config.openaiCompat.baseUrl && config.openaiCompat.apiKeySet && config.openaiCompat.model
2769
+ ),
2770
+ baseUrl: config.openaiCompat.baseUrl,
2771
+ apiKeySet: config.openaiCompat.apiKeySet,
2772
+ model: config.openaiCompat.model,
2773
+ extraBodyKeys: Object.keys(config.openaiCompat.extraBody)
2774
+ };
2775
+ }
2776
+ const ollama = await checkOllamaStatus(config.ollama.model, config.ollama.baseUrl).catch(
2777
+ (err) => ({
2778
+ binaryFound: false,
2779
+ daemonRunning: false,
2780
+ modelPulled: false,
2781
+ modelName: config.ollama.model,
2782
+ installedModels: [],
2783
+ error: err instanceof Error ? err.message : String(err)
2784
+ })
2785
+ );
2786
+ return {
2787
+ kind: "ollama",
2788
+ ready: ollama.daemonRunning && ollama.modelPulled,
2789
+ baseUrl: config.ollama.baseUrl,
2790
+ ...ollama
2791
+ };
2792
+ }
2793
+ function isAbortError(err) {
2794
+ if (err instanceof Error) {
2795
+ if (err.name === "AbortError") return true;
2796
+ if (/aborted/i.test(err.message)) return true;
2797
+ }
2798
+ return false;
2799
+ }
2800
+
2801
+ // src/server/api/sessions.ts
2802
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2803
+ function parseTranscript(path, maxBytes = 4 * 1024 * 1024) {
2804
+ let raw;
2805
+ try {
2806
+ raw = readFileSync5(path, "utf8");
2807
+ } catch {
2808
+ return [];
2809
+ }
2810
+ if (raw.length > maxBytes) raw = raw.slice(0, maxBytes);
2811
+ const out = [];
2812
+ for (const line of raw.split(/\r?\n/)) {
2813
+ if (!line.trim()) continue;
2814
+ try {
2815
+ const rec = JSON.parse(line);
2816
+ const role = typeof rec.role === "string" ? rec.role : "unknown";
2817
+ const msg = { role };
2818
+ if (typeof rec.content === "string") msg.content = rec.content;
2819
+ else if (rec.content !== void 0) msg.content = JSON.stringify(rec.content);
2820
+ if (typeof rec.tool_name === "string") msg.toolName = rec.tool_name;
2821
+ if (typeof rec.toolName === "string") msg.toolName = rec.toolName;
2822
+ out.push(msg);
2823
+ } catch {
2824
+ }
2825
+ }
2826
+ return out;
2827
+ }
2828
+ async function handleSessions(method, rest, _body, _ctx) {
2829
+ if (method !== "GET") {
2830
+ return { status: 405, body: { error: "GET only" } };
2831
+ }
2832
+ if (rest.length === 0) {
2833
+ const sessions = listSessions();
2834
+ return {
2835
+ status: 200,
2836
+ body: {
2837
+ sessions: sessions.map((s) => ({
2838
+ name: s.name,
2839
+ path: s.path,
2840
+ size: s.size,
2841
+ messageCount: s.messageCount,
2842
+ mtime: s.mtime.getTime()
2843
+ }))
2844
+ }
2845
+ };
2846
+ }
2847
+ const name = decodeURIComponent(rest[0]);
2848
+ const path = sessionPath(name);
2849
+ if (!existsSync8(path)) {
2850
+ return { status: 404, body: { error: `no such session: ${name}` } };
2851
+ }
2852
+ const messages = parseTranscript(path);
2853
+ return {
2854
+ status: 200,
2855
+ body: {
2856
+ name,
2857
+ path,
2858
+ messages,
2859
+ messageCount: messages.length
2860
+ }
2861
+ };
2862
+ }
2863
+
2864
+ // src/server/api/settings.ts
2865
+ function parseBody9(raw) {
2866
+ if (!raw) return {};
2867
+ try {
2868
+ const parsed = JSON.parse(raw);
2869
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
2870
+ } catch {
2871
+ return {};
2872
+ }
2873
+ }
2874
+ var VALID_PRESETS = /* @__PURE__ */ new Set(["auto", "flash", "pro", "fast", "smart", "max"]);
2875
+ var VALID_EFFORTS = /* @__PURE__ */ new Set(["high", "max"]);
2876
+ async function handleSettings(method, _rest, body, ctx) {
2877
+ if (method === "GET") {
2878
+ const cfg = readConfig(ctx.configPath);
2879
+ if (cfg.search === void 0) {
2880
+ cfg.search = true;
2881
+ writeConfig(cfg, ctx.configPath);
2882
+ }
2883
+ const live = ctx.loop;
2884
+ const currentModel = live?.model ?? null;
2885
+ const currentProvider = currentModel ? resolveProvider(currentModel) : void 0;
2886
+ const providerId = currentProvider?.id ?? cfg.provider ?? "deepseek";
2887
+ const defaultModel = currentProvider?.defaultModel ?? null;
2888
+ const escalationModel = currentProvider?.escalationModel ?? null;
2889
+ let thinkingSupported = false;
2890
+ if (currentProvider && currentModel) {
2891
+ const t = currentProvider.thinking;
2892
+ if (t.transport !== "none") {
2893
+ thinkingSupported = !t.thinkingModels || t.thinkingModels.includes(currentModel);
2894
+ }
2895
+ }
2896
+ return {
2897
+ status: 200,
2898
+ body: {
2899
+ apiKey: cfg.apiKey ? redactKey(cfg.apiKey) : null,
2900
+ apiKeySet: Boolean(cfg.apiKey),
2901
+ baseUrl: cfg.baseUrl ?? null,
2902
+ lang: getLanguage(),
2903
+ preset: cfg.preset ?? "auto",
2904
+ reasoningEffort: cfg.reasoningEffort ?? "max",
2905
+ search: cfg.search !== false,
2906
+ editMode: cfg.editMode ?? "review",
2907
+ session: cfg.session ?? null,
2908
+ model: currentModel,
2909
+ proNext: live?.proArmed ?? false,
2910
+ budgetUsd: live?.budgetUsd ?? null,
2911
+ sessionSpendUsd: ctx.getStats?.()?.totalCostUsd ?? null,
2912
+ // Provider context for dynamic UI.
2913
+ providerId,
2914
+ defaultModel,
2915
+ escalationModel,
2916
+ thinkingSupported,
2917
+ // Hint to the SPA which fields require restart.
2918
+ appliesAt: {
2919
+ apiKey: "next-session",
2920
+ baseUrl: "next-session",
2921
+ preset: "next-session",
2922
+ reasoningEffort: "next-turn",
2923
+ search: "next-session",
2924
+ model: "next-turn",
2925
+ proNext: "next-turn",
2926
+ budgetUsd: "live"
2927
+ }
2928
+ }
2929
+ };
2930
+ }
2931
+ if (method === "POST") {
2932
+ const fields = parseBody9(body);
2933
+ const cfg = readConfig(ctx.configPath);
2934
+ const changed = [];
2935
+ let langPending = null;
2936
+ let presetPendingLive = null;
2937
+ let effortPendingLive = null;
2938
+ if (fields.lang !== void 0) {
2939
+ const raw = String(fields.lang);
2940
+ const supported = getSupportedLanguages();
2941
+ const langCode = supported.find((l) => l.toLowerCase() === raw.toLowerCase());
2942
+ if (!langCode) {
2943
+ return { status: 400, body: { error: `lang must be one of: ${supported.join(", ")}` } };
2944
+ }
2945
+ cfg.lang = langCode;
2946
+ langPending = langCode;
2947
+ changed.push("lang");
2948
+ }
2949
+ if (fields.apiKey !== void 0) {
2950
+ if (typeof fields.apiKey !== "string" || !isPlausibleKey(fields.apiKey)) {
2951
+ return { status: 400, body: { error: "apiKey must be 16+ chars with no whitespace" } };
2952
+ }
2953
+ cfg.apiKey = fields.apiKey.trim();
2954
+ changed.push("apiKey");
2955
+ }
2956
+ if (fields.baseUrl !== void 0) {
2957
+ if (typeof fields.baseUrl !== "string" || !fields.baseUrl.trim()) {
2958
+ return { status: 400, body: { error: "baseUrl must be a non-empty string" } };
2959
+ }
2960
+ cfg.baseUrl = fields.baseUrl.trim();
2961
+ changed.push("baseUrl");
2962
+ }
2963
+ if (fields.preset !== void 0) {
2964
+ if (typeof fields.preset !== "string" || !VALID_PRESETS.has(fields.preset)) {
2965
+ return { status: 400, body: { error: "preset must be auto | flash | pro" } };
2966
+ }
2967
+ cfg.preset = fields.preset;
2968
+ presetPendingLive = fields.preset;
2969
+ changed.push("preset");
2970
+ }
2971
+ if (fields.reasoningEffort !== void 0) {
2972
+ if (typeof fields.reasoningEffort !== "string" || !VALID_EFFORTS.has(fields.reasoningEffort)) {
2973
+ return { status: 400, body: { error: "reasoningEffort must be high | max" } };
2974
+ }
2975
+ cfg.reasoningEffort = fields.reasoningEffort;
2976
+ effortPendingLive = fields.reasoningEffort;
2977
+ changed.push("reasoningEffort");
2978
+ }
2979
+ if (fields.search !== void 0) {
2980
+ if (typeof fields.search !== "boolean") {
2981
+ return { status: 400, body: { error: "search must be a boolean" } };
2982
+ }
2983
+ cfg.search = fields.search;
2984
+ changed.push("search");
2985
+ }
2986
+ let modelPendingLive = null;
2987
+ let proNextPending = null;
2988
+ let budgetPending;
2989
+ if (fields.model !== void 0) {
2990
+ if (typeof fields.model !== "string" || !fields.model.trim()) {
2991
+ return { status: 400, body: { error: "model must be a non-empty string" } };
2992
+ }
2993
+ modelPendingLive = fields.model.trim();
2994
+ changed.push("model");
2995
+ }
2996
+ if (fields.proNext !== void 0) {
2997
+ if (typeof fields.proNext !== "boolean") {
2998
+ return { status: 400, body: { error: "proNext must be a boolean" } };
2999
+ }
3000
+ proNextPending = fields.proNext;
3001
+ changed.push("proNext");
3002
+ }
3003
+ if (fields.budgetUsd !== void 0) {
3004
+ if (fields.budgetUsd === null) {
3005
+ budgetPending = null;
3006
+ } else if (typeof fields.budgetUsd === "number" && fields.budgetUsd > 0 && Number.isFinite(fields.budgetUsd)) {
3007
+ budgetPending = fields.budgetUsd;
3008
+ } else {
3009
+ return {
3010
+ status: 400,
3011
+ body: { error: "budgetUsd must be null or a positive finite number" }
3012
+ };
3013
+ }
3014
+ changed.push("budgetUsd");
3015
+ }
3016
+ if (fields.providerKey !== void 0) {
3017
+ const pk = fields.providerKey;
3018
+ if (!pk || typeof pk.providerId !== "string" || typeof pk.key !== "string" || !pk.key.trim()) {
3019
+ return { status: 400, body: { error: "providerKey must be { providerId: string, key: string }" } };
3020
+ }
3021
+ if (!isPlausibleKey(pk.key)) {
3022
+ return { status: 400, body: { error: "providerKey.key must be 16+ chars with no whitespace" } };
3023
+ }
3024
+ if (!cfg.providerKeys) cfg.providerKeys = {};
3025
+ cfg.providerKeys[pk.providerId] = pk.key.trim();
3026
+ changed.push("providerKey");
3027
+ }
3028
+ if (fields.providerBaseUrl !== void 0) {
3029
+ const pu = fields.providerBaseUrl;
3030
+ if (!pu || typeof pu.providerId !== "string" || typeof pu.url !== "string" || !pu.url.trim()) {
3031
+ return { status: 400, body: { error: "providerBaseUrl must be { providerId: string, url: string }" } };
3032
+ }
3033
+ if (!cfg.providerBaseUrls) cfg.providerBaseUrls = {};
3034
+ cfg.providerBaseUrls[pu.providerId] = pu.url.trim();
3035
+ changed.push("providerBaseUrl");
3036
+ }
3037
+ if (changed.length > 0) {
3038
+ writeConfig(cfg, ctx.configPath);
3039
+ if (langPending) setLanguage(langPending);
3040
+ if (presetPendingLive) ctx.applyPresetLive?.(presetPendingLive);
3041
+ if (effortPendingLive) ctx.applyEffortLive?.(effortPendingLive);
3042
+ if (modelPendingLive) ctx.applyModelLive?.(modelPendingLive);
3043
+ if (proNextPending !== null) ctx.setProNextLive?.(proNextPending);
3044
+ if (budgetPending !== void 0) ctx.setBudgetUsdLive?.(budgetPending);
3045
+ ctx.audit?.({ ts: Date.now(), action: "set-settings", payload: { fields: changed } });
3046
+ }
3047
+ return { status: 200, body: { changed } };
3048
+ }
3049
+ return { status: 405, body: { error: "GET or POST only" } };
3050
+ }
3051
+
3052
+ // src/server/api/skills.ts
3053
+ import {
3054
+ closeSync as closeSync4,
3055
+ existsSync as existsSync9,
3056
+ fstatSync as fstatSync4,
3057
+ mkdirSync as mkdirSync3,
3058
+ openSync as openSync4,
3059
+ readFileSync as readFileSync6,
3060
+ readSync as readSync4,
3061
+ readdirSync as readdirSync5,
3062
+ rmSync,
3063
+ statSync as statSync5,
3064
+ writeFileSync as writeFileSync3
3065
+ } from "fs";
3066
+ import { homedir as homedir3 } from "os";
3067
+ import { dirname as dirname4, join as join8 } from "path";
3068
+ function parseBody10(raw) {
3069
+ if (!raw) return {};
3070
+ try {
3071
+ const parsed = JSON.parse(raw);
3072
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
3073
+ } catch {
3074
+ return {};
3075
+ }
3076
+ }
3077
+ var SAFE_NAME2 = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
3078
+ function globalSkillsDir() {
3079
+ return join8(homedir3(), ".luckerr", SKILLS_DIRNAME);
3080
+ }
3081
+ function projectSkillsDir(rootDir) {
3082
+ return join8(rootDir, ".luckerr", SKILLS_DIRNAME);
3083
+ }
3084
+ function parseFrontmatterDescription(raw) {
3085
+ const desc = parseFrontmatter(raw).data.description?.trim();
3086
+ return desc ? desc : void 0;
3087
+ }
3088
+ function readSkillListEntry(skillPath, name, scope) {
3089
+ try {
3090
+ const fd = openSync4(skillPath, "r");
3091
+ let stat;
3092
+ let raw;
3093
+ try {
3094
+ stat = fstatSync4(fd);
3095
+ if (!stat.isFile()) return null;
3096
+ const buf = Buffer.alloc(stat.size);
3097
+ let read = 0;
3098
+ while (read < stat.size) {
3099
+ const n = readSync4(fd, buf, read, stat.size - read, read);
3100
+ if (n <= 0) break;
3101
+ read += n;
3102
+ }
3103
+ raw = buf.toString("utf8", 0, read);
3104
+ } finally {
3105
+ closeSync4(fd);
3106
+ }
3107
+ const item = {
3108
+ name,
3109
+ scope,
3110
+ path: skillPath,
3111
+ size: stat.size,
3112
+ mtime: stat.mtime.getTime()
3113
+ };
3114
+ const desc = parseFrontmatterDescription(raw);
3115
+ if (desc) item.description = desc;
3116
+ return item;
3117
+ } catch {
3118
+ return null;
3119
+ }
3120
+ }
3121
+ function resolveSkillPath(dir, name) {
3122
+ const folderPath = join8(dir, name, SKILL_FILE);
3123
+ try {
3124
+ if (statSync5(folderPath).isFile()) return { path: folderPath, layout: "folder" };
3125
+ } catch {
3126
+ }
3127
+ const flatPath = join8(dir, `${name}.md`);
3128
+ try {
3129
+ if (statSync5(flatPath).isFile()) return { path: flatPath, layout: "flat" };
3130
+ } catch {
3131
+ }
3132
+ return null;
3133
+ }
3134
+ function defaultSkillPath(dir, name) {
3135
+ return { path: join8(dir, name, SKILL_FILE), layout: "folder" };
3136
+ }
3137
+ function listSkills(dir, scope) {
3138
+ if (!existsSync9(dir)) return [];
3139
+ const out = [];
3140
+ try {
3141
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3142
+ let name;
3143
+ let skillPath;
3144
+ if (entry.isDirectory()) {
3145
+ name = entry.name;
3146
+ if (!SAFE_NAME2.test(name)) continue;
3147
+ skillPath = join8(dir, name, SKILL_FILE);
3148
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
3149
+ name = entry.name.slice(0, -3);
3150
+ if (!SAFE_NAME2.test(name)) continue;
3151
+ skillPath = join8(dir, entry.name);
3152
+ } else {
3153
+ continue;
3154
+ }
3155
+ const item = readSkillListEntry(skillPath, name, scope);
3156
+ if (item) out.push(item);
3157
+ }
3158
+ } catch {
3159
+ }
3160
+ return out.sort((a, b) => a.name.localeCompare(b.name));
3161
+ }
3162
+ function countSubagentRuns(usageLogPath) {
3163
+ const cutoff = Date.now() - 7 * 864e5;
3164
+ const counts = /* @__PURE__ */ new Map();
3165
+ for (const r of readUsageLog(usageLogPath)) {
3166
+ if (r.kind !== "subagent" || r.ts < cutoff) continue;
3167
+ const skill = r.subagent?.skillName?.trim();
3168
+ if (!skill) continue;
3169
+ counts.set(skill, (counts.get(skill) ?? 0) + 1);
3170
+ }
3171
+ return counts;
3172
+ }
3173
+ async function handleSkills(method, rest, body, ctx) {
3174
+ const cwd = ctx.getCurrentCwd?.();
3175
+ if (method === "GET" && rest.length === 0) {
3176
+ const runs7d = countSubagentRuns(ctx.usageLogPath);
3177
+ const tag = (rows) => rows.map((r) => ({ ...r, runs7d: runs7d.get(r.name) ?? 0 }));
3178
+ return {
3179
+ status: 200,
3180
+ body: {
3181
+ global: tag(listSkills(globalSkillsDir(), "global")),
3182
+ project: cwd ? tag(listSkills(projectSkillsDir(cwd), "project")) : [],
3183
+ builtin: [
3184
+ {
3185
+ name: "explore",
3186
+ scope: "builtin",
3187
+ description: "subagent \u2014 broad codebase survey",
3188
+ runs7d: runs7d.get("explore") ?? 0
3189
+ },
3190
+ {
3191
+ name: "research",
3192
+ scope: "builtin",
3193
+ description: "subagent \u2014 deep web + repo research",
3194
+ runs7d: runs7d.get("research") ?? 0
3195
+ }
3196
+ ],
3197
+ paths: {
3198
+ global: globalSkillsDir(),
3199
+ project: cwd ? projectSkillsDir(cwd) : null
3200
+ }
3201
+ }
3202
+ };
3203
+ }
3204
+ const [scope, ...nameParts] = rest;
3205
+ const name = nameParts.join("/");
3206
+ if (!scope || !name || !SAFE_NAME2.test(name)) {
3207
+ return { status: 400, body: { error: "expected /api/skills/<scope>/<name>" } };
3208
+ }
3209
+ if (scope !== "project" && scope !== "global") {
3210
+ return {
3211
+ status: 400,
3212
+ body: { error: "scope must be project | global (builtin is read-only)" }
3213
+ };
3214
+ }
3215
+ let dir;
3216
+ if (scope === "project") {
3217
+ if (!cwd) {
3218
+ return {
3219
+ status: 503,
3220
+ body: { error: "no active project \u2014 open `/dashboard` from `luckerr code`" }
3221
+ };
3222
+ }
3223
+ dir = projectSkillsDir(cwd);
3224
+ } else {
3225
+ dir = globalSkillsDir();
3226
+ }
3227
+ const resolved = resolveSkillPath(dir, name);
3228
+ if (method === "GET") {
3229
+ if (!resolved) return { status: 404, body: { error: "skill not found" } };
3230
+ return {
3231
+ status: 200,
3232
+ body: { path: resolved.path, body: readFileSync6(resolved.path, "utf8") }
3233
+ };
3234
+ }
3235
+ if (method === "POST") {
3236
+ const { body: contents } = parseBody10(body);
3237
+ if (typeof contents !== "string") {
3238
+ return { status: 400, body: { error: "body (string) required" } };
3239
+ }
3240
+ const fm = validateSkillFrontmatter(contents);
3241
+ if ("error" in fm) {
3242
+ return { status: 400, body: { error: fm.error } };
3243
+ }
3244
+ const target = resolved ?? defaultSkillPath(dir, name);
3245
+ mkdirSync3(dirname4(target.path), { recursive: true });
3246
+ writeFileSync3(target.path, contents, "utf8");
3247
+ ctx.audit?.({
3248
+ ts: Date.now(),
3249
+ action: "save-skill",
3250
+ payload: { scope, name, path: target.path }
3251
+ });
3252
+ return { status: 200, body: { saved: true, path: target.path } };
3253
+ }
3254
+ if (method === "DELETE") {
3255
+ if (!resolved) return { status: 404, body: { error: "skill not found" } };
3256
+ rmSync(resolved.layout === "folder" ? dirname4(resolved.path) : resolved.path, {
3257
+ recursive: true,
3258
+ force: true
3259
+ });
3260
+ ctx.audit?.({ ts: Date.now(), action: "delete-skill", payload: { scope, name } });
3261
+ return { status: 200, body: { deleted: true } };
3262
+ }
3263
+ return { status: 405, body: { error: `method ${method} not supported` } };
3264
+ }
3265
+
3266
+ // src/server/api/slash.ts
3267
+ async function handleSlash(method, _rest, _body, ctx) {
3268
+ if (method !== "GET") return { status: 405, body: { error: "GET only" } };
3269
+ const codeMode = ctx.getCurrentCwd?.() != null;
3270
+ const commands = SLASH_COMMANDS.filter((c) => c.contextual !== "code" || codeMode).map((c) => ({
3271
+ cmd: c.cmd,
3272
+ summary: c.summary,
3273
+ argsHint: c.argsHint,
3274
+ contextual: c.contextual,
3275
+ aliases: c.aliases
3276
+ }));
3277
+ return { status: 200, body: { commands, codeMode } };
3278
+ }
3279
+
3280
+ // src/server/api/submit.ts
3281
+ function parseBody11(raw) {
3282
+ if (!raw) return {};
3283
+ try {
3284
+ const parsed = JSON.parse(raw);
3285
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
3286
+ } catch {
3287
+ return {};
3288
+ }
3289
+ }
3290
+ async function handleSubmit(method, _rest, body, ctx) {
3291
+ if (method !== "POST") {
3292
+ return { status: 405, body: { error: "POST only" } };
3293
+ }
3294
+ if (!ctx.submitPrompt) {
3295
+ return {
3296
+ status: 503,
3297
+ body: {
3298
+ error: "submit requires an attached dashboard session \u2014 open `/dashboard` from inside `luckerr code` or `luckerr chat`."
3299
+ }
3300
+ };
3301
+ }
3302
+ const { prompt } = parseBody11(body);
3303
+ if (typeof prompt !== "string" || !prompt.trim()) {
3304
+ return { status: 400, body: { error: "prompt (non-empty string) required" } };
3305
+ }
3306
+ const result = ctx.submitPrompt(prompt);
3307
+ if (!result.accepted) {
3308
+ return {
3309
+ status: 409,
3310
+ body: { accepted: false, reason: result.reason ?? "loop is busy" }
3311
+ };
3312
+ }
3313
+ ctx.audit?.({
3314
+ ts: Date.now(),
3315
+ action: "submit-prompt",
3316
+ payload: { length: prompt.length }
3317
+ });
3318
+ return { status: 202, body: { accepted: true } };
3319
+ }
3320
+
3321
+ // src/server/api/tools.ts
3322
+ async function handleTools(method, _rest, _body, ctx) {
3323
+ if (method !== "GET") {
3324
+ return { status: 405, body: { error: "GET only" } };
3325
+ }
3326
+ if (!ctx.tools) {
3327
+ return {
3328
+ status: 503,
3329
+ body: {
3330
+ error: "live tools view requires an attached session \u2014 run `/dashboard` from inside `luckerr code` instead of standalone `luckerr dashboard`.",
3331
+ available: false
3332
+ }
3333
+ };
3334
+ }
3335
+ const specs = ctx.tools.specs();
3336
+ const items = specs.map((s) => {
3337
+ const def = ctx.tools.get(s.function.name);
3338
+ return {
3339
+ name: s.function.name,
3340
+ description: s.function.description,
3341
+ schema: s.function.parameters,
3342
+ readOnly: Boolean(def?.readOnly),
3343
+ flattened: ctx.tools.wasFlattened(s.function.name)
3344
+ };
3345
+ });
3346
+ return {
3347
+ status: 200,
3348
+ body: {
3349
+ planMode: ctx.tools.planMode,
3350
+ total: items.length,
3351
+ tools: items
3352
+ }
3353
+ };
3354
+ }
3355
+
3356
+ // src/server/api/usage.ts
3357
+ function dayKey(ts) {
3358
+ return new Date(ts).toISOString().slice(0, 10);
3359
+ }
3360
+ function buildSeries(records) {
3361
+ const map = /* @__PURE__ */ new Map();
3362
+ for (const r of records) {
3363
+ const day = dayKey(r.ts);
3364
+ let b = map.get(day);
3365
+ if (!b) {
3366
+ b = {
3367
+ day,
3368
+ turns: 0,
3369
+ promptTokens: 0,
3370
+ completionTokens: 0,
3371
+ cacheHitTokens: 0,
3372
+ cacheMissTokens: 0,
3373
+ costUsd: 0,
3374
+ cacheSavingsUsd: 0
3375
+ };
3376
+ map.set(day, b);
3377
+ }
3378
+ b.turns += 1;
3379
+ b.promptTokens += r.promptTokens;
3380
+ b.completionTokens += r.completionTokens;
3381
+ b.cacheHitTokens += r.cacheHitTokens;
3382
+ b.cacheMissTokens += r.cacheMissTokens;
3383
+ b.costUsd += r.costUsd;
3384
+ b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);
3385
+ }
3386
+ return Array.from(map.values()).sort((a, b) => a.day.localeCompare(b.day));
3387
+ }
3388
+ async function handleUsage(method, rest, _body, ctx) {
3389
+ if (method !== "GET") {
3390
+ return { status: 405, body: { error: "GET only" } };
3391
+ }
3392
+ const records = readUsageLog(ctx.usageLogPath);
3393
+ if (rest[0] === "series") {
3394
+ return {
3395
+ status: 200,
3396
+ body: {
3397
+ days: buildSeries(records),
3398
+ recordCount: records.length
3399
+ }
3400
+ };
3401
+ }
3402
+ const agg = aggregateUsage(records);
3403
+ return {
3404
+ status: 200,
3405
+ body: {
3406
+ logPath: ctx.usageLogPath,
3407
+ logSize: formatLogSize(ctx.usageLogPath),
3408
+ recordCount: records.length,
3409
+ buckets: agg.buckets,
3410
+ byModel: agg.byModel,
3411
+ bySession: agg.bySession,
3412
+ firstSeen: agg.firstSeen,
3413
+ lastSeen: agg.lastSeen,
3414
+ subagents: agg.subagents ?? null
3415
+ }
3416
+ };
3417
+ }
3418
+
3419
+ // src/server/router.ts
3420
+ async function handleApi(pathTail, method, body, ctx, query = new URLSearchParams()) {
3421
+ const normalized = pathTail.replace(/\/+$/, "");
3422
+ const [head, ...rest] = normalized.split("/");
3423
+ try {
3424
+ switch (head) {
3425
+ case "overview":
3426
+ return await handleOverview(method, rest, body, ctx);
3427
+ case "usage":
3428
+ return await handleUsage(method, rest, body, ctx);
3429
+ case "tools":
3430
+ return await handleTools(method, rest, body, ctx);
3431
+ case "permissions":
3432
+ return await handlePermissions(method, rest, body, ctx);
3433
+ case "messages":
3434
+ return await handleMessages(method, rest, body, ctx);
3435
+ case "submit":
3436
+ return await handleSubmit(method, rest, body, ctx);
3437
+ case "abort":
3438
+ return await handleAbort(method, rest, body, ctx);
3439
+ case "health":
3440
+ return await handleHealth(method, rest, body, ctx);
3441
+ case "sessions":
3442
+ return await handleSessions(method, rest, body, ctx);
3443
+ case "plans":
3444
+ return await handlePlans(method, rest, body, ctx);
3445
+ case "modal":
3446
+ return await handleModal(method, rest, body, ctx);
3447
+ case "edit-mode":
3448
+ return await handleEditMode(method, rest, body, ctx);
3449
+ case "settings":
3450
+ return await handleSettings(method, rest, body, ctx);
3451
+ case "hooks":
3452
+ return await handleHooks(method, rest, body, ctx);
3453
+ case "memory":
3454
+ return await handleMemory(method, rest, body, ctx);
3455
+ case "skills":
3456
+ return await handleSkills(method, rest, body, ctx);
3457
+ case "mcp":
3458
+ return await handleMcp(method, rest, body, ctx, query);
3459
+ case "semantic":
3460
+ return await handleSemantic(method, rest, body, ctx);
3461
+ case "index-config":
3462
+ return await handleIndexConfig(method, rest, body, ctx);
3463
+ case "slash":
3464
+ return await handleSlash(method, rest, body, ctx);
3465
+ case "files":
3466
+ return await handleFiles(method, rest, body, ctx);
3467
+ case "project-tree":
3468
+ return await handleProjectTree(method, rest, body, ctx);
3469
+ case "git-diffs":
3470
+ return await handleGitDiffs(method, rest, body, ctx);
3471
+ case "checkpoints":
3472
+ return await handleCheckpoints(method, rest, body, ctx);
3473
+ case "checkpoint-diffs":
3474
+ return await handleCheckpointDiffs(method, rest, body, ctx, query);
3475
+ case "checkpoint-restore":
3476
+ return await handleCheckpointRestore(method, rest, body, ctx);
3477
+ case "checkpoint-create":
3478
+ return await handleCheckpointCreate(method, rest, body, ctx);
3479
+ case "checkpoint-delete":
3480
+ return await handleCheckpointDelete(method, rest, body, ctx);
3481
+ case "review-diffs":
3482
+ return await handleReviewDiffs(method, rest, body, ctx);
3483
+ case "file":
3484
+ return await handleFileRead(method, rest, body, ctx);
3485
+ case "loop":
3486
+ return await handleLoop(method, rest, body, ctx);
3487
+ case "models":
3488
+ return await handleModels(method, rest, body, ctx);
3489
+ default:
3490
+ return { status: 404, body: { error: `no such endpoint: /${head}` } };
3491
+ }
3492
+ } catch (err) {
3493
+ return {
3494
+ status: 500,
3495
+ body: { error: `handler crashed: ${err.message}` }
3496
+ };
3497
+ }
3498
+ }
3499
+
3500
+ // src/server/index.ts
3501
+ function mintToken() {
3502
+ return randomBytes(32).toString("hex");
3503
+ }
3504
+ function constantTimeEquals(a, b) {
3505
+ if (a.length !== b.length) return false;
3506
+ let mismatch = 0;
3507
+ for (let i = 0; i < a.length; i++) {
3508
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
3509
+ }
3510
+ return mismatch === 0;
3511
+ }
3512
+ function checkAuth(req, expectedToken, isMutation) {
3513
+ const url = new URL(req.url ?? "/", "http://localhost");
3514
+ const queryToken = url.searchParams.get("token") ?? "";
3515
+ const headerToken = typeof req.headers["x-luckerr-token"] === "string" ? req.headers["x-luckerr-token"] : "";
3516
+ if (isMutation) {
3517
+ if (!headerToken || !constantTimeEquals(headerToken, expectedToken)) {
3518
+ return {
3519
+ status: 403,
3520
+ body: JSON.stringify({
3521
+ error: "mutation requires X-Luckerr-Token header (CSRF defence \u2014 query token alone is rejected for POST/DELETE)."
3522
+ })
3523
+ };
3524
+ }
3525
+ return null;
3526
+ }
3527
+ if (queryToken && constantTimeEquals(queryToken, expectedToken) || headerToken && constantTimeEquals(headerToken, expectedToken)) {
3528
+ return null;
3529
+ }
3530
+ return {
3531
+ status: 401,
3532
+ body: JSON.stringify({ error: "missing or invalid token" })
3533
+ };
3534
+ }
3535
+ var MAX_BODY_BYTES = 256 * 1024;
3536
+ async function readBody(req) {
3537
+ let total = 0;
3538
+ const chunks = [];
3539
+ return new Promise((resolve3, reject) => {
3540
+ req.on("data", (chunk) => {
3541
+ total += chunk.length;
3542
+ if (total > MAX_BODY_BYTES) {
3543
+ reject(new Error(`body exceeds ${MAX_BODY_BYTES} bytes`));
3544
+ req.destroy();
3545
+ return;
3546
+ }
3547
+ chunks.push(chunk);
3548
+ });
3549
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
3550
+ req.on("error", reject);
3551
+ });
3552
+ }
3553
+ async function dispatch(req, res, ctx, expectedToken) {
3554
+ const url = new URL(req.url ?? "/", "http://localhost");
3555
+ const path = url.pathname;
3556
+ const method = (req.method ?? "GET").toUpperCase();
3557
+ const isMutation = method === "POST" || method === "DELETE" || method === "PUT";
3558
+ if (path === "/" || path === "/index.html") {
3559
+ const fail = checkAuth(req, expectedToken, false);
3560
+ if (fail) {
3561
+ res.writeHead(fail.status, { "content-type": "text/plain" });
3562
+ res.end("unauthorized \u2014 open the URL printed by /dashboard, including ?token=\u2026");
3563
+ return;
3564
+ }
3565
+ const html = renderIndexHtml(expectedToken, ctx.mode);
3566
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
3567
+ res.end(html);
3568
+ return;
3569
+ }
3570
+ if (path.startsWith("/assets/")) {
3571
+ const fail = checkAuth(req, expectedToken, false);
3572
+ if (fail) {
3573
+ res.writeHead(fail.status);
3574
+ res.end();
3575
+ return;
3576
+ }
3577
+ const asset = serveAsset(path.slice("/assets/".length));
3578
+ if (!asset) {
3579
+ res.writeHead(404);
3580
+ res.end("not found");
3581
+ return;
3582
+ }
3583
+ res.writeHead(200, { "content-type": asset.contentType });
3584
+ res.end(asset.body);
3585
+ return;
3586
+ }
3587
+ if (path === "/api/events") {
3588
+ const fail = checkAuth(req, expectedToken, false);
3589
+ if (fail) {
3590
+ res.writeHead(fail.status, { "content-type": "application/json" });
3591
+ res.end(fail.body);
3592
+ return;
3593
+ }
3594
+ handleEvents(req, res, ctx);
3595
+ return;
3596
+ }
3597
+ if (path.startsWith("/api/")) {
3598
+ const fail = checkAuth(req, expectedToken, isMutation);
3599
+ if (fail) {
3600
+ res.writeHead(fail.status, { "content-type": "application/json" });
3601
+ res.end(fail.body);
3602
+ return;
3603
+ }
3604
+ let body = "";
3605
+ if (isMutation) {
3606
+ try {
3607
+ body = await readBody(req);
3608
+ } catch (err) {
3609
+ res.writeHead(413, { "content-type": "application/json" });
3610
+ res.end(JSON.stringify({ error: err.message }));
3611
+ return;
3612
+ }
3613
+ }
3614
+ const result = await handleApi(path.slice("/api/".length), method, body, ctx, url.searchParams);
3615
+ res.writeHead(result.status, { "content-type": "application/json" });
3616
+ res.end(JSON.stringify(result.body));
3617
+ return;
3618
+ }
3619
+ res.writeHead(404, { "content-type": "text/plain" });
3620
+ res.end("not found");
3621
+ }
3622
+ function startDashboardServer(ctx, opts = {}) {
3623
+ const token = opts.token ?? mintToken();
3624
+ const host = opts.host ?? "127.0.0.1";
3625
+ const port = opts.port ?? 0;
3626
+ return new Promise((resolve3, reject) => {
3627
+ const server = createServer((req, res) => {
3628
+ dispatch(req, res, ctx, token).catch((err) => {
3629
+ if (!res.headersSent) {
3630
+ res.writeHead(500, { "content-type": "application/json" });
3631
+ }
3632
+ res.end(JSON.stringify({ error: err.message }));
3633
+ });
3634
+ });
3635
+ server.on("error", reject);
3636
+ server.listen(port, host, () => {
3637
+ const addr = server.address();
3638
+ const finalPort = addr.port;
3639
+ const url = `http://${host}:${finalPort}/?token=${token}`;
3640
+ let closed = false;
3641
+ const close = () => new Promise((doneResolve) => {
3642
+ if (closed) return doneResolve();
3643
+ closed = true;
3644
+ server.close(() => doneResolve());
3645
+ setTimeout(() => server.closeAllConnections?.(), 1e3).unref();
3646
+ });
3647
+ resolve3({ url, token, port: finalPort, close });
3648
+ });
3649
+ });
3650
+ }
3651
+ export {
3652
+ checkAuth,
3653
+ constantTimeEquals,
3654
+ dispatch,
3655
+ readBody,
3656
+ startDashboardServer
3657
+ };
3658
+ //# sourceMappingURL=server-FFU6TLYJ.js.map