@goondocks/myco 0.5.1 → 0.6.1

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 (138) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +14 -11
  4. package/dist/{chunk-JJL6AMDA.js → chunk-24DOZEUJ.js} +255 -6
  5. package/dist/chunk-24DOZEUJ.js.map +1 -0
  6. package/dist/{chunk-ZWUFTOG3.js → chunk-2GSX3BK2.js} +4 -4
  7. package/dist/{chunk-FIRMTYFH.js → chunk-2YBUL3IL.js} +4 -37
  8. package/dist/chunk-2YBUL3IL.js.map +1 -0
  9. package/dist/{chunk-HL2S5QZG.js → chunk-2ZBB3MQT.js} +319 -40
  10. package/dist/chunk-2ZBB3MQT.js.map +1 -0
  11. package/dist/{chunk-HJG7Z6SJ.js → chunk-3EM23DMD.js} +2 -2
  12. package/dist/{chunk-XQXXF6MU.js → chunk-4RMSHZE4.js} +12 -1
  13. package/dist/{chunk-XQXXF6MU.js.map → chunk-4RMSHZE4.js.map} +1 -1
  14. package/dist/{chunk-T7OC6GH5.js → chunk-5FNZ7AMX.js} +2 -2
  15. package/dist/{chunk-X6TKHO22.js → chunk-5QWZT4AB.js} +2 -2
  16. package/dist/{chunk-B6WVNDA5.js → chunk-6BSDCZ5Q.js} +8 -2
  17. package/dist/{chunk-B6WVNDA5.js.map → chunk-6BSDCZ5Q.js.map} +1 -1
  18. package/dist/{chunk-R6LQT3U7.js → chunk-B5UZSHQV.js} +8 -12
  19. package/dist/{chunk-R6LQT3U7.js.map → chunk-B5UZSHQV.js.map} +1 -1
  20. package/dist/{chunk-7KQB22DP.js → chunk-E7OBRBCQ.js} +2 -2
  21. package/dist/{chunk-RCV2I4AI.js → chunk-GDYYJTTT.js} +5 -3
  22. package/dist/{chunk-RCV2I4AI.js.map → chunk-GDYYJTTT.js.map} +1 -1
  23. package/dist/{chunk-MIU3DKLN.js → chunk-GNR3QAER.js} +2 -2
  24. package/dist/{chunk-BMJX2IDQ.js → chunk-H7PRCVGQ.js} +2 -2
  25. package/dist/{chunk-6LTNFMXO.js → chunk-KC7ENQTN.js} +2 -2
  26. package/dist/chunk-KUMVJIJW.js +117 -0
  27. package/dist/chunk-KUMVJIJW.js.map +1 -0
  28. package/dist/{chunk-ND4VK6C7.js → chunk-L25U7PIG.js} +2 -2
  29. package/dist/{chunk-6UJWI4IW.js → chunk-MQSYSQ6T.js} +7 -5
  30. package/dist/{chunk-6UJWI4IW.js.map → chunk-MQSYSQ6T.js.map} +1 -1
  31. package/dist/{chunk-TBRZAJ7W.js → chunk-P3WO3N3I.js} +11 -3
  32. package/dist/chunk-P3WO3N3I.js.map +1 -0
  33. package/dist/{chunk-JI6M2L2W.js → chunk-QGJ2ZIUZ.js} +7 -4
  34. package/dist/chunk-QGJ2ZIUZ.js.map +1 -0
  35. package/dist/{chunk-5EZ7QF6J.js → chunk-QLUE3BUL.js} +66 -1
  36. package/dist/chunk-QLUE3BUL.js.map +1 -0
  37. package/dist/{chunk-AK6GNLPV.js → chunk-TWSTAVLO.js} +17 -1
  38. package/dist/{chunk-AK6GNLPV.js.map → chunk-TWSTAVLO.js.map} +1 -1
  39. package/dist/{chunk-FIA5NTRH.js → chunk-UVGAVYWZ.js} +11 -13
  40. package/dist/chunk-UVGAVYWZ.js.map +1 -0
  41. package/dist/{chunk-UKWO26VI.js → chunk-YTANWAGE.js} +2 -2
  42. package/dist/chunk-ZMYNRTTD.js +64 -0
  43. package/dist/chunk-ZMYNRTTD.js.map +1 -0
  44. package/dist/{cli-BLYNNKGJ.js → cli-K7SUTP7A.js} +22 -22
  45. package/dist/{client-5GB4WVXE.js → client-YJMNTITQ.js} +5 -5
  46. package/dist/{config-5FGLQGCW.js → config-G5GGT5A6.js} +3 -3
  47. package/dist/curate-6T5NKVXK.js +80 -0
  48. package/dist/curate-6T5NKVXK.js.map +1 -0
  49. package/dist/{detect-providers-BIHYFK5M.js → detect-providers-S3M5TAMW.js} +3 -3
  50. package/dist/{digest-7NKYXM6G.js → digest-O35VHYFP.js} +31 -40
  51. package/dist/digest-O35VHYFP.js.map +1 -0
  52. package/dist/{init-HPQ77WWF.js → init-TFLSATB3.js} +9 -11
  53. package/dist/init-TFLSATB3.js.map +1 -0
  54. package/dist/{logs-BSTBZHDR.js → logs-IENORIYR.js} +3 -3
  55. package/dist/{main-NFQ4II75.js → main-JEUQS3BY.js} +1218 -294
  56. package/dist/main-JEUQS3BY.js.map +1 -0
  57. package/dist/rebuild-7SH5GSNX.js +66 -0
  58. package/dist/rebuild-7SH5GSNX.js.map +1 -0
  59. package/dist/{reprocess-ZL4HKTSC.js → reprocess-Q4YH2ZBK.js} +20 -22
  60. package/dist/{reprocess-ZL4HKTSC.js.map → reprocess-Q4YH2ZBK.js.map} +1 -1
  61. package/dist/{restart-FYW662DR.js → restart-NLJLB52D.js} +7 -6
  62. package/dist/{restart-FYW662DR.js.map → restart-NLJLB52D.js.map} +1 -1
  63. package/dist/{search-E5JQMTXV.js → search-2BVRF54H.js} +10 -10
  64. package/dist/{server-TV3D35HZ.js → server-4AMZNP4F.js} +51 -97
  65. package/dist/{server-TV3D35HZ.js.map → server-4AMZNP4F.js.map} +1 -1
  66. package/dist/{session-QF6MILAC.js → session-F326AWCH.js} +2 -2
  67. package/dist/{session-start-5MFEOVQ5.js → session-start-AZAF3DTE.js} +10 -10
  68. package/dist/setup-digest-YLZZGSSR.js +15 -0
  69. package/dist/setup-llm-JOXBSLXC.js +15 -0
  70. package/dist/src/cli.js +4 -4
  71. package/dist/src/daemon/main.js +4 -4
  72. package/dist/src/hooks/post-tool-use.js +5 -5
  73. package/dist/src/hooks/session-end.js +5 -5
  74. package/dist/src/hooks/session-start.js +4 -4
  75. package/dist/src/hooks/stop.js +7 -7
  76. package/dist/src/hooks/user-prompt-submit.js +5 -5
  77. package/dist/src/mcp/server.js +4 -4
  78. package/dist/src/prompts/consolidation.md +46 -0
  79. package/dist/src/templates/portal.md +5 -0
  80. package/dist/stats-MKDIZFIQ.js +58 -0
  81. package/dist/stats-MKDIZFIQ.js.map +1 -0
  82. package/dist/templates-XPRBOWCE.js +38 -0
  83. package/dist/templates-XPRBOWCE.js.map +1 -0
  84. package/dist/ui/assets/index-D37IoDXS.css +1 -0
  85. package/dist/ui/assets/index-DA61Ial2.js +289 -0
  86. package/dist/ui/favicon.svg +11 -0
  87. package/dist/ui/fonts/GeistMono-LICENSE.txt +92 -0
  88. package/dist/ui/fonts/GeistMono-Variable.woff2 +0 -0
  89. package/dist/ui/index.html +14 -0
  90. package/dist/{verify-RACBFT2P.js → verify-7DW7LAND.js} +6 -6
  91. package/dist/{version-HJTVNPOO.js → version-RQLD7VBP.js} +4 -4
  92. package/package.json +3 -2
  93. package/dist/chunk-2AMAOSRF.js +0 -105
  94. package/dist/chunk-2AMAOSRF.js.map +0 -1
  95. package/dist/chunk-5EZ7QF6J.js.map +0 -1
  96. package/dist/chunk-FIA5NTRH.js.map +0 -1
  97. package/dist/chunk-FIRMTYFH.js.map +0 -1
  98. package/dist/chunk-HL2S5QZG.js.map +0 -1
  99. package/dist/chunk-IURC35BF.js +0 -49
  100. package/dist/chunk-IURC35BF.js.map +0 -1
  101. package/dist/chunk-JI6M2L2W.js.map +0 -1
  102. package/dist/chunk-JJL6AMDA.js.map +0 -1
  103. package/dist/chunk-KYL67SKZ.js +0 -150
  104. package/dist/chunk-KYL67SKZ.js.map +0 -1
  105. package/dist/chunk-TBRZAJ7W.js.map +0 -1
  106. package/dist/curate-S4HOYWXA.js +0 -231
  107. package/dist/curate-S4HOYWXA.js.map +0 -1
  108. package/dist/digest-7NKYXM6G.js.map +0 -1
  109. package/dist/init-HPQ77WWF.js.map +0 -1
  110. package/dist/main-NFQ4II75.js.map +0 -1
  111. package/dist/rebuild-KQ6G2GZM.js +0 -86
  112. package/dist/rebuild-KQ6G2GZM.js.map +0 -1
  113. package/dist/setup-digest-DZAFIBEF.js +0 -15
  114. package/dist/setup-llm-4BZM33YT.js +0 -15
  115. package/dist/stats-ZIIJ2GB3.js +0 -77
  116. package/dist/stats-ZIIJ2GB3.js.map +0 -1
  117. /package/dist/{chunk-ZWUFTOG3.js.map → chunk-2GSX3BK2.js.map} +0 -0
  118. /package/dist/{chunk-HJG7Z6SJ.js.map → chunk-3EM23DMD.js.map} +0 -0
  119. /package/dist/{chunk-T7OC6GH5.js.map → chunk-5FNZ7AMX.js.map} +0 -0
  120. /package/dist/{chunk-X6TKHO22.js.map → chunk-5QWZT4AB.js.map} +0 -0
  121. /package/dist/{chunk-7KQB22DP.js.map → chunk-E7OBRBCQ.js.map} +0 -0
  122. /package/dist/{chunk-MIU3DKLN.js.map → chunk-GNR3QAER.js.map} +0 -0
  123. /package/dist/{chunk-BMJX2IDQ.js.map → chunk-H7PRCVGQ.js.map} +0 -0
  124. /package/dist/{chunk-6LTNFMXO.js.map → chunk-KC7ENQTN.js.map} +0 -0
  125. /package/dist/{chunk-ND4VK6C7.js.map → chunk-L25U7PIG.js.map} +0 -0
  126. /package/dist/{chunk-UKWO26VI.js.map → chunk-YTANWAGE.js.map} +0 -0
  127. /package/dist/{cli-BLYNNKGJ.js.map → cli-K7SUTP7A.js.map} +0 -0
  128. /package/dist/{client-5GB4WVXE.js.map → client-YJMNTITQ.js.map} +0 -0
  129. /package/dist/{config-5FGLQGCW.js.map → config-G5GGT5A6.js.map} +0 -0
  130. /package/dist/{detect-providers-BIHYFK5M.js.map → detect-providers-S3M5TAMW.js.map} +0 -0
  131. /package/dist/{logs-BSTBZHDR.js.map → logs-IENORIYR.js.map} +0 -0
  132. /package/dist/{search-E5JQMTXV.js.map → search-2BVRF54H.js.map} +0 -0
  133. /package/dist/{session-QF6MILAC.js.map → session-F326AWCH.js.map} +0 -0
  134. /package/dist/{session-start-5MFEOVQ5.js.map → session-start-AZAF3DTE.js.map} +0 -0
  135. /package/dist/{setup-digest-DZAFIBEF.js.map → setup-digest-YLZZGSSR.js.map} +0 -0
  136. /package/dist/{setup-llm-4BZM33YT.js.map → setup-llm-JOXBSLXC.js.map} +0 -0
  137. /package/dist/{verify-RACBFT2P.js.map → verify-7DW7LAND.js.map} +0 -0
  138. /package/dist/{version-HJTVNPOO.js.map → version-RQLD7VBP.js.map} +0 -0
@@ -4,14 +4,24 @@ import {
4
4
  TranscriptMiner,
5
5
  extractTurnsFromBuffer,
6
6
  writeObservationNotes
7
- } from "./chunk-R6LQT3U7.js";
7
+ } from "./chunk-B5UZSHQV.js";
8
+ import {
9
+ gatherStats
10
+ } from "./chunk-ZMYNRTTD.js";
8
11
  import {
9
12
  DigestEngine,
10
- Metabolism
11
- } from "./chunk-HL2S5QZG.js";
13
+ Metabolism,
14
+ appendTraceRecord,
15
+ readLastTimestamp,
16
+ runCuration,
17
+ runDigest,
18
+ runRebuild
19
+ } from "./chunk-2ZBB3MQT.js";
20
+ import "./chunk-3JCXYLHD.js";
12
21
  import {
22
+ consolidateSpores,
13
23
  handleMycoContext
14
- } from "./chunk-IURC35BF.js";
24
+ } from "./chunk-KUMVJIJW.js";
15
25
  import {
16
26
  VaultWriter,
17
27
  bareSessionId,
@@ -19,60 +29,72 @@ import {
19
29
  sessionNoteId,
20
30
  sessionRelativePath,
21
31
  sessionWikilink
22
- } from "./chunk-6LTNFMXO.js";
32
+ } from "./chunk-KC7ENQTN.js";
23
33
  import {
24
34
  DaemonLogger
25
- } from "./chunk-5EZ7QF6J.js";
35
+ } from "./chunk-QLUE3BUL.js";
36
+ import {
37
+ VectorIndex
38
+ } from "./chunk-4RMSHZE4.js";
26
39
  import {
27
- checkSupersession
28
- } from "./chunk-FIA5NTRH.js";
40
+ checkSupersession,
41
+ isActiveSpore
42
+ } from "./chunk-UVGAVYWZ.js";
29
43
  import {
44
+ buildSimilarityPrompt,
45
+ extractNumber,
46
+ formatNotesForPrompt,
30
47
  indexNote,
31
- rebuildIndex
32
- } from "./chunk-JJL6AMDA.js";
48
+ loadPrompt,
49
+ rebuildIndex,
50
+ stripReasoningTokens
51
+ } from "./chunk-24DOZEUJ.js";
33
52
  import {
34
53
  generateEmbedding
35
54
  } from "./chunk-RGVBGTD6.js";
36
- import {
37
- VectorIndex
38
- } from "./chunk-XQXXF6MU.js";
39
- import {
40
- buildSimilarityPrompt,
41
- extractNumber
42
- } from "./chunk-KYL67SKZ.js";
43
- import "./chunk-2AMAOSRF.js";
44
55
  import {
45
56
  createEmbeddingProvider,
46
57
  createLlmProvider
47
- } from "./chunk-RCV2I4AI.js";
48
- import "./chunk-MIU3DKLN.js";
58
+ } from "./chunk-GDYYJTTT.js";
59
+ import "./chunk-GNR3QAER.js";
49
60
  import {
50
61
  initFts
51
62
  } from "./chunk-6FQISQNA.js";
52
63
  import {
53
64
  MycoIndex
54
- } from "./chunk-AK6GNLPV.js";
55
- import "./chunk-JI6M2L2W.js";
65
+ } from "./chunk-TWSTAVLO.js";
66
+ import "./chunk-2YBUL3IL.js";
67
+ import "./chunk-SAKJMNSR.js";
56
68
  import {
57
- loadConfig
58
- } from "./chunk-TBRZAJ7W.js";
69
+ LmStudioBackend,
70
+ OllamaBackend
71
+ } from "./chunk-QGJ2ZIUZ.js";
59
72
  import {
73
+ CONFIG_FILENAME,
74
+ loadConfig,
75
+ saveConfig
76
+ } from "./chunk-P3WO3N3I.js";
77
+ import {
78
+ MycoConfigSchema,
60
79
  external_exports,
61
80
  require_dist
62
- } from "./chunk-6UJWI4IW.js";
81
+ } from "./chunk-MQSYSQ6T.js";
63
82
  import {
64
83
  EventBuffer
65
84
  } from "./chunk-HIN3UVOG.js";
66
85
  import {
67
86
  getPluginVersion
68
- } from "./chunk-7KQB22DP.js";
87
+ } from "./chunk-E7OBRBCQ.js";
69
88
  import {
70
89
  claudeCodeAdapter,
71
90
  createPerProjectAdapter,
72
91
  extensionForMimeType
73
- } from "./chunk-X6TKHO22.js";
92
+ } from "./chunk-5QWZT4AB.js";
74
93
  import {
75
94
  CANDIDATE_CONTENT_PREVIEW,
95
+ CONSOLIDATION_MAX_TOKENS,
96
+ CONSOLIDATION_MIN_CLUSTER_SIZE,
97
+ CONSOLIDATION_VECTOR_FETCH_LIMIT,
76
98
  CONTENT_SNIPPET_CHARS,
77
99
  CONTEXT_SESSION_PREVIEW_CHARS,
78
100
  EMBEDDING_INPUT_LIMIT,
@@ -86,40 +108,138 @@ import {
86
108
  RELATED_SPORES_LIMIT,
87
109
  SESSION_CONTEXT_MAX_PLANS,
88
110
  STALE_BUFFER_MAX_AGE_MS
89
- } from "./chunk-B6WVNDA5.js";
111
+ } from "./chunk-6BSDCZ5Q.js";
90
112
  import {
91
113
  __toESM
92
114
  } from "./chunk-PZUWP5VK.js";
93
115
 
94
116
  // src/daemon/server.ts
95
117
  import http from "http";
118
+ import fs2 from "fs";
119
+ import path2 from "path";
120
+
121
+ // src/daemon/router.ts
122
+ var Router = class {
123
+ routes = [];
124
+ add(method, pattern, handler) {
125
+ const type = pattern.includes(":") ? "param" : pattern.endsWith("/*") ? "prefix" : "exact";
126
+ const segments = type === "param" ? pattern.split("/") : void 0;
127
+ this.routes.push({ method, pattern, handler, type, segments });
128
+ }
129
+ /**
130
+ * Match a request against registered routes.
131
+ * Priority: exact > parameterized > prefix. Within parameterized routes,
132
+ * first-registered wins if multiple patterns match at the same depth.
133
+ */
134
+ match(method, rawUrl) {
135
+ const url = new URL(rawUrl, "http://localhost");
136
+ const pathname = url.pathname;
137
+ const query = {};
138
+ url.searchParams.forEach((v, k) => {
139
+ query[k] = v;
140
+ });
141
+ let paramMatch;
142
+ let prefixMatch;
143
+ for (const route of this.routes) {
144
+ if (route.method !== method) continue;
145
+ if (route.type === "exact" && route.pattern === pathname) {
146
+ return { handler: route.handler, params: {}, query, pathname };
147
+ }
148
+ if (route.type === "param" && !paramMatch && route.segments) {
149
+ const parts = pathname.split("/");
150
+ if (parts.length === route.segments.length) {
151
+ const params = {};
152
+ let matched = true;
153
+ for (let i = 0; i < route.segments.length; i++) {
154
+ if (route.segments[i].startsWith(":")) {
155
+ params[route.segments[i].slice(1)] = parts[i];
156
+ } else if (route.segments[i] !== parts[i]) {
157
+ matched = false;
158
+ break;
159
+ }
160
+ }
161
+ if (matched) {
162
+ paramMatch = { handler: route.handler, params, query, pathname };
163
+ }
164
+ }
165
+ }
166
+ if (route.type === "prefix" && !prefixMatch) {
167
+ const prefix = route.pattern.slice(0, -1);
168
+ if (pathname.startsWith(prefix)) {
169
+ prefixMatch = { handler: route.handler, params: {}, query, pathname };
170
+ }
171
+ }
172
+ }
173
+ return paramMatch ?? prefixMatch;
174
+ }
175
+ };
176
+
177
+ // src/daemon/static.ts
96
178
  import fs from "fs";
97
179
  import path from "path";
180
+ var HASHED_ASSET_PREFIX = "/assets/";
181
+ var IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
182
+ var NO_CACHE = "no-cache";
183
+ var MIME_TYPES = {
184
+ ".html": "text/html",
185
+ ".js": "application/javascript",
186
+ ".css": "text/css",
187
+ ".json": "application/json",
188
+ ".svg": "image/svg+xml",
189
+ ".png": "image/png",
190
+ ".ico": "image/x-icon",
191
+ ".woff": "font/woff",
192
+ ".woff2": "font/woff2",
193
+ ".ttf": "font/ttf"
194
+ };
195
+ function resolveStaticFile(uiDir, pathname) {
196
+ const relative3 = pathname.startsWith("/") ? pathname.slice(1) : pathname;
197
+ const resolved = path.resolve(uiDir, relative3 || "index.html");
198
+ if (!resolved.startsWith(path.resolve(uiDir))) {
199
+ return void 0;
200
+ }
201
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
202
+ const ext = path.extname(resolved);
203
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
204
+ const cacheControl = pathname.startsWith(HASHED_ASSET_PREFIX) ? IMMUTABLE_CACHE : NO_CACHE;
205
+ return { filePath: resolved, contentType, cacheControl };
206
+ }
207
+ const indexPath = path.join(uiDir, "index.html");
208
+ if (fs.existsSync(indexPath)) {
209
+ return { filePath: indexPath, contentType: "text/html", cacheControl: NO_CACHE };
210
+ }
211
+ return void 0;
212
+ }
213
+
214
+ // src/daemon/server.ts
215
+ var DEFAULT_STATUS = 200;
98
216
  var DaemonServer = class {
99
217
  port = 0;
100
218
  version;
219
+ uiDir;
101
220
  server = null;
102
221
  vaultDir;
103
222
  logger;
104
- routes = /* @__PURE__ */ new Map();
223
+ router = new Router();
105
224
  constructor(config) {
106
225
  this.vaultDir = config.vaultDir;
107
226
  this.logger = config.logger;
227
+ this.uiDir = config.uiDir ?? null;
108
228
  this.version = getPluginVersion();
109
229
  this.registerDefaultRoutes();
110
230
  }
111
231
  registerRoute(method, routePath, handler) {
112
- this.routes.set(`${method} ${routePath}`, { method, handler });
232
+ this.router.add(method, routePath, handler);
113
233
  }
114
- async start() {
234
+ async start(port = 0) {
115
235
  return new Promise((resolve3, reject) => {
116
236
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
117
237
  this.server.on("error", reject);
118
- this.server.listen(0, "127.0.0.1", () => {
238
+ this.server.listen(port, "127.0.0.1", () => {
119
239
  const addr = this.server.address();
120
240
  this.port = addr.port;
121
241
  this.writeDaemonJson();
122
- this.logger.info("daemon", "Server started", { port: this.port });
242
+ this.logger.info("daemon", "Server started", { port: this.port, dashboard: `http://localhost:${this.port}/` });
123
243
  resolve3();
124
244
  });
125
245
  });
@@ -139,40 +259,66 @@ var DaemonServer = class {
139
259
  }
140
260
  registerDefaultRoutes() {
141
261
  this.registerRoute("GET", "/health", async () => ({
142
- myco: true,
143
- version: this.version,
144
- pid: process.pid,
145
- uptime: process.uptime()
262
+ body: {
263
+ myco: true,
264
+ version: this.version,
265
+ pid: process.pid,
266
+ uptime: process.uptime()
267
+ }
146
268
  }));
147
269
  }
148
270
  async handleRequest(req, res) {
149
- const key = `${req.method} ${req.url}`;
150
- const route = this.routes.get(key);
151
- if (!route) {
152
- res.writeHead(404, { "Content-Type": "application/json" });
153
- res.end(JSON.stringify({ error: "not found" }));
271
+ const match = this.router.match(req.method, req.url);
272
+ if (match) {
273
+ try {
274
+ const body = req.method === "POST" || req.method === "PUT" ? await readBody(req) : void 0;
275
+ const result = await match.handler({
276
+ body,
277
+ query: match.query,
278
+ params: match.params,
279
+ pathname: match.pathname
280
+ });
281
+ const status = result.status ?? DEFAULT_STATUS;
282
+ const headers = { "Content-Type": "application/json", ...result.headers };
283
+ res.writeHead(status, headers);
284
+ res.end(JSON.stringify(result.body));
285
+ } catch (error) {
286
+ this.logger.error("daemon", "Request handler error", {
287
+ path: req.url,
288
+ error: error.message
289
+ });
290
+ res.writeHead(500, { "Content-Type": "application/json" });
291
+ res.end(JSON.stringify({ error: error.message }));
292
+ }
154
293
  return;
155
294
  }
156
- try {
157
- const body = req.method === "POST" ? await readBody(req) : void 0;
158
- const result = await route.handler(body);
159
- res.writeHead(200, { "Content-Type": "application/json" });
160
- res.end(JSON.stringify(result));
161
- } catch (error) {
162
- this.logger.error("daemon", "Request handler error", {
163
- path: req.url,
164
- error: error.message
165
- });
166
- res.writeHead(500, { "Content-Type": "application/json" });
167
- res.end(JSON.stringify({ error: error.message }));
295
+ if (this.uiDir && req.method === "GET") {
296
+ const pathname = new URL(req.url, "http://localhost").pathname;
297
+ const result = resolveStaticFile(this.uiDir, pathname);
298
+ if (result) {
299
+ try {
300
+ const content = await fs2.promises.readFile(result.filePath);
301
+ res.writeHead(200, {
302
+ "Content-Type": result.contentType,
303
+ "Cache-Control": result.cacheControl
304
+ });
305
+ res.end(content);
306
+ } catch {
307
+ res.writeHead(404, { "Content-Type": "application/json" });
308
+ res.end(JSON.stringify({ error: "not found" }));
309
+ }
310
+ return;
311
+ }
168
312
  }
313
+ res.writeHead(404, { "Content-Type": "application/json" });
314
+ res.end(JSON.stringify({ error: "not found" }));
169
315
  }
170
316
  updateDaemonJsonSessions(sessions) {
171
- const jsonPath = path.join(this.vaultDir, "daemon.json");
317
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
172
318
  try {
173
- const info = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
319
+ const info = JSON.parse(fs2.readFileSync(jsonPath, "utf-8"));
174
320
  info.sessions = sessions;
175
- fs.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
321
+ fs2.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
176
322
  } catch {
177
323
  }
178
324
  }
@@ -183,13 +329,13 @@ var DaemonServer = class {
183
329
  started: (/* @__PURE__ */ new Date()).toISOString(),
184
330
  sessions: []
185
331
  };
186
- const jsonPath = path.join(this.vaultDir, "daemon.json");
187
- fs.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
332
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
333
+ fs2.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
188
334
  }
189
335
  removeDaemonJson() {
190
- const jsonPath = path.join(this.vaultDir, "daemon.json");
336
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
191
337
  try {
192
- fs.unlinkSync(jsonPath);
338
+ fs2.unlinkSync(jsonPath);
193
339
  } catch {
194
340
  }
195
341
  }
@@ -299,8 +445,8 @@ var BatchManager = class {
299
445
  };
300
446
 
301
447
  // src/daemon/lineage.ts
302
- import fs2 from "fs";
303
- import path2 from "path";
448
+ import fs3 from "fs";
449
+ import path3 from "path";
304
450
  var LINEAGE_IMMEDIATE_GAP_SECONDS = 5;
305
451
  var LINEAGE_FALLBACK_MAX_HOURS = 24;
306
452
  var LINEAGE_SIMILARITY_THRESHOLD = 0.7;
@@ -313,8 +459,8 @@ var LineageGraph = class {
313
459
  state;
314
460
  filePath;
315
461
  constructor(vaultDir) {
316
- this.filePath = path2.join(vaultDir, "lineage.json");
317
- fs2.mkdirSync(path2.dirname(this.filePath), { recursive: true });
462
+ this.filePath = path3.join(vaultDir, "lineage.json");
463
+ fs3.mkdirSync(path3.dirname(this.filePath), { recursive: true });
318
464
  this.state = this.load();
319
465
  }
320
466
  addLink(link) {
@@ -394,7 +540,7 @@ var LineageGraph = class {
394
540
  }
395
541
  load() {
396
542
  try {
397
- const raw = JSON.parse(fs2.readFileSync(this.filePath, "utf-8"));
543
+ const raw = JSON.parse(fs3.readFileSync(this.filePath, "utf-8"));
398
544
  const sessionArtifacts = raw.sessionArtifacts ?? raw.sessionPlans ?? {};
399
545
  return { links: raw.links ?? [], sessionArtifacts };
400
546
  } catch {
@@ -403,8 +549,8 @@ var LineageGraph = class {
403
549
  }
404
550
  persist() {
405
551
  const tmp = this.filePath + ".tmp";
406
- fs2.writeFileSync(tmp, JSON.stringify(this.state, null, 2));
407
- fs2.renameSync(tmp, this.filePath);
552
+ fs3.writeFileSync(tmp, JSON.stringify(this.state, null, 2));
553
+ fs3.renameSync(tmp, this.filePath);
408
554
  }
409
555
  };
410
556
 
@@ -498,7 +644,7 @@ var ReaddirpStream = class extends Readable {
498
644
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
499
645
  const statMethod = opts.lstat ? lstat : stat;
500
646
  if (wantBigintFsStats) {
501
- this._stat = (path7) => statMethod(path7, { bigint: true });
647
+ this._stat = (path10) => statMethod(path10, { bigint: true });
502
648
  } else {
503
649
  this._stat = statMethod;
504
650
  }
@@ -523,8 +669,8 @@ var ReaddirpStream = class extends Readable {
523
669
  const par = this.parent;
524
670
  const fil = par && par.files;
525
671
  if (fil && fil.length > 0) {
526
- const { path: path7, depth } = par;
527
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path7));
672
+ const { path: path10, depth } = par;
673
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path10));
528
674
  const awaited = await Promise.all(slice);
529
675
  for (const entry of awaited) {
530
676
  if (!entry)
@@ -564,20 +710,20 @@ var ReaddirpStream = class extends Readable {
564
710
  this.reading = false;
565
711
  }
566
712
  }
567
- async _exploreDir(path7, depth) {
713
+ async _exploreDir(path10, depth) {
568
714
  let files;
569
715
  try {
570
- files = await readdir(path7, this._rdOptions);
716
+ files = await readdir(path10, this._rdOptions);
571
717
  } catch (error) {
572
718
  this._onError(error);
573
719
  }
574
- return { files, depth, path: path7 };
720
+ return { files, depth, path: path10 };
575
721
  }
576
- async _formatEntry(dirent, path7) {
722
+ async _formatEntry(dirent, path10) {
577
723
  let entry;
578
724
  const basename3 = this._isDirent ? dirent.name : dirent;
579
725
  try {
580
- const fullPath = presolve(pjoin(path7, basename3));
726
+ const fullPath = presolve(pjoin(path10, basename3));
581
727
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
582
728
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
583
729
  } catch (err) {
@@ -977,16 +1123,16 @@ var delFromSet = (main2, prop, item) => {
977
1123
  };
978
1124
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
979
1125
  var FsWatchInstances = /* @__PURE__ */ new Map();
980
- function createFsWatchInstance(path7, options, listener, errHandler, emitRaw) {
1126
+ function createFsWatchInstance(path10, options, listener, errHandler, emitRaw) {
981
1127
  const handleEvent = (rawEvent, evPath) => {
982
- listener(path7);
983
- emitRaw(rawEvent, evPath, { watchedPath: path7 });
984
- if (evPath && path7 !== evPath) {
985
- fsWatchBroadcast(sp.resolve(path7, evPath), KEY_LISTENERS, sp.join(path7, evPath));
1128
+ listener(path10);
1129
+ emitRaw(rawEvent, evPath, { watchedPath: path10 });
1130
+ if (evPath && path10 !== evPath) {
1131
+ fsWatchBroadcast(sp.resolve(path10, evPath), KEY_LISTENERS, sp.join(path10, evPath));
986
1132
  }
987
1133
  };
988
1134
  try {
989
- return fs_watch(path7, {
1135
+ return fs_watch(path10, {
990
1136
  persistent: options.persistent
991
1137
  }, handleEvent);
992
1138
  } catch (error) {
@@ -1002,12 +1148,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
1002
1148
  listener(val1, val2, val3);
1003
1149
  });
1004
1150
  };
1005
- var setFsWatchListener = (path7, fullPath, options, handlers) => {
1151
+ var setFsWatchListener = (path10, fullPath, options, handlers) => {
1006
1152
  const { listener, errHandler, rawEmitter } = handlers;
1007
1153
  let cont = FsWatchInstances.get(fullPath);
1008
1154
  let watcher;
1009
1155
  if (!options.persistent) {
1010
- watcher = createFsWatchInstance(path7, options, listener, errHandler, rawEmitter);
1156
+ watcher = createFsWatchInstance(path10, options, listener, errHandler, rawEmitter);
1011
1157
  if (!watcher)
1012
1158
  return;
1013
1159
  return watcher.close.bind(watcher);
@@ -1018,7 +1164,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
1018
1164
  addAndConvert(cont, KEY_RAW, rawEmitter);
1019
1165
  } else {
1020
1166
  watcher = createFsWatchInstance(
1021
- path7,
1167
+ path10,
1022
1168
  options,
1023
1169
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
1024
1170
  errHandler,
@@ -1033,7 +1179,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
1033
1179
  cont.watcherUnusable = true;
1034
1180
  if (isWindows && error.code === "EPERM") {
1035
1181
  try {
1036
- const fd = await open(path7, "r");
1182
+ const fd = await open(path10, "r");
1037
1183
  await fd.close();
1038
1184
  broadcastErr(error);
1039
1185
  } catch (err) {
@@ -1064,7 +1210,7 @@ var setFsWatchListener = (path7, fullPath, options, handlers) => {
1064
1210
  };
1065
1211
  };
1066
1212
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
1067
- var setFsWatchFileListener = (path7, fullPath, options, handlers) => {
1213
+ var setFsWatchFileListener = (path10, fullPath, options, handlers) => {
1068
1214
  const { listener, rawEmitter } = handlers;
1069
1215
  let cont = FsWatchFileInstances.get(fullPath);
1070
1216
  const copts = cont && cont.options;
@@ -1086,7 +1232,7 @@ var setFsWatchFileListener = (path7, fullPath, options, handlers) => {
1086
1232
  });
1087
1233
  const currmtime = curr.mtimeMs;
1088
1234
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
1089
- foreach(cont.listeners, (listener2) => listener2(path7, curr));
1235
+ foreach(cont.listeners, (listener2) => listener2(path10, curr));
1090
1236
  }
1091
1237
  })
1092
1238
  };
@@ -1116,13 +1262,13 @@ var NodeFsHandler = class {
1116
1262
  * @param listener on fs change
1117
1263
  * @returns closer for the watcher instance
1118
1264
  */
1119
- _watchWithNodeFs(path7, listener) {
1265
+ _watchWithNodeFs(path10, listener) {
1120
1266
  const opts = this.fsw.options;
1121
- const directory = sp.dirname(path7);
1122
- const basename3 = sp.basename(path7);
1267
+ const directory = sp.dirname(path10);
1268
+ const basename3 = sp.basename(path10);
1123
1269
  const parent = this.fsw._getWatchedDir(directory);
1124
1270
  parent.add(basename3);
1125
- const absolutePath = sp.resolve(path7);
1271
+ const absolutePath = sp.resolve(path10);
1126
1272
  const options = {
1127
1273
  persistent: opts.persistent
1128
1274
  };
@@ -1132,12 +1278,12 @@ var NodeFsHandler = class {
1132
1278
  if (opts.usePolling) {
1133
1279
  const enableBin = opts.interval !== opts.binaryInterval;
1134
1280
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
1135
- closer = setFsWatchFileListener(path7, absolutePath, options, {
1281
+ closer = setFsWatchFileListener(path10, absolutePath, options, {
1136
1282
  listener,
1137
1283
  rawEmitter: this.fsw._emitRaw
1138
1284
  });
1139
1285
  } else {
1140
- closer = setFsWatchListener(path7, absolutePath, options, {
1286
+ closer = setFsWatchListener(path10, absolutePath, options, {
1141
1287
  listener,
1142
1288
  errHandler: this._boundHandleError,
1143
1289
  rawEmitter: this.fsw._emitRaw
@@ -1159,7 +1305,7 @@ var NodeFsHandler = class {
1159
1305
  let prevStats = stats;
1160
1306
  if (parent.has(basename3))
1161
1307
  return;
1162
- const listener = async (path7, newStats) => {
1308
+ const listener = async (path10, newStats) => {
1163
1309
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
1164
1310
  return;
1165
1311
  if (!newStats || newStats.mtimeMs === 0) {
@@ -1173,11 +1319,11 @@ var NodeFsHandler = class {
1173
1319
  this.fsw._emit(EV.CHANGE, file, newStats2);
1174
1320
  }
1175
1321
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
1176
- this.fsw._closeFile(path7);
1322
+ this.fsw._closeFile(path10);
1177
1323
  prevStats = newStats2;
1178
1324
  const closer2 = this._watchWithNodeFs(file, listener);
1179
1325
  if (closer2)
1180
- this.fsw._addPathCloser(path7, closer2);
1326
+ this.fsw._addPathCloser(path10, closer2);
1181
1327
  } else {
1182
1328
  prevStats = newStats2;
1183
1329
  }
@@ -1209,7 +1355,7 @@ var NodeFsHandler = class {
1209
1355
  * @param item basename of this item
1210
1356
  * @returns true if no more processing is needed for this entry.
1211
1357
  */
1212
- async _handleSymlink(entry, directory, path7, item) {
1358
+ async _handleSymlink(entry, directory, path10, item) {
1213
1359
  if (this.fsw.closed) {
1214
1360
  return;
1215
1361
  }
@@ -1219,7 +1365,7 @@ var NodeFsHandler = class {
1219
1365
  this.fsw._incrReadyCount();
1220
1366
  let linkPath;
1221
1367
  try {
1222
- linkPath = await fsrealpath(path7);
1368
+ linkPath = await fsrealpath(path10);
1223
1369
  } catch (e) {
1224
1370
  this.fsw._emitReady();
1225
1371
  return true;
@@ -1229,12 +1375,12 @@ var NodeFsHandler = class {
1229
1375
  if (dir.has(item)) {
1230
1376
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
1231
1377
  this.fsw._symlinkPaths.set(full, linkPath);
1232
- this.fsw._emit(EV.CHANGE, path7, entry.stats);
1378
+ this.fsw._emit(EV.CHANGE, path10, entry.stats);
1233
1379
  }
1234
1380
  } else {
1235
1381
  dir.add(item);
1236
1382
  this.fsw._symlinkPaths.set(full, linkPath);
1237
- this.fsw._emit(EV.ADD, path7, entry.stats);
1383
+ this.fsw._emit(EV.ADD, path10, entry.stats);
1238
1384
  }
1239
1385
  this.fsw._emitReady();
1240
1386
  return true;
@@ -1264,9 +1410,9 @@ var NodeFsHandler = class {
1264
1410
  return;
1265
1411
  }
1266
1412
  const item = entry.path;
1267
- let path7 = sp.join(directory, item);
1413
+ let path10 = sp.join(directory, item);
1268
1414
  current.add(item);
1269
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path7, item)) {
1415
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path10, item)) {
1270
1416
  return;
1271
1417
  }
1272
1418
  if (this.fsw.closed) {
@@ -1275,8 +1421,8 @@ var NodeFsHandler = class {
1275
1421
  }
1276
1422
  if (item === target || !target && !previous.has(item)) {
1277
1423
  this.fsw._incrReadyCount();
1278
- path7 = sp.join(dir, sp.relative(dir, path7));
1279
- this._addToNodeFs(path7, initialAdd, wh, depth + 1);
1424
+ path10 = sp.join(dir, sp.relative(dir, path10));
1425
+ this._addToNodeFs(path10, initialAdd, wh, depth + 1);
1280
1426
  }
1281
1427
  }).on(EV.ERROR, this._boundHandleError);
1282
1428
  return new Promise((resolve3, reject) => {
@@ -1345,13 +1491,13 @@ var NodeFsHandler = class {
1345
1491
  * @param depth Child path actually targeted for watch
1346
1492
  * @param target Child path actually targeted for watch
1347
1493
  */
1348
- async _addToNodeFs(path7, initialAdd, priorWh, depth, target) {
1494
+ async _addToNodeFs(path10, initialAdd, priorWh, depth, target) {
1349
1495
  const ready = this.fsw._emitReady;
1350
- if (this.fsw._isIgnored(path7) || this.fsw.closed) {
1496
+ if (this.fsw._isIgnored(path10) || this.fsw.closed) {
1351
1497
  ready();
1352
1498
  return false;
1353
1499
  }
1354
- const wh = this.fsw._getWatchHelpers(path7);
1500
+ const wh = this.fsw._getWatchHelpers(path10);
1355
1501
  if (priorWh) {
1356
1502
  wh.filterPath = (entry) => priorWh.filterPath(entry);
1357
1503
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -1367,8 +1513,8 @@ var NodeFsHandler = class {
1367
1513
  const follow = this.fsw.options.followSymlinks;
1368
1514
  let closer;
1369
1515
  if (stats.isDirectory()) {
1370
- const absPath = sp.resolve(path7);
1371
- const targetPath = follow ? await fsrealpath(path7) : path7;
1516
+ const absPath = sp.resolve(path10);
1517
+ const targetPath = follow ? await fsrealpath(path10) : path10;
1372
1518
  if (this.fsw.closed)
1373
1519
  return;
1374
1520
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -1378,29 +1524,29 @@ var NodeFsHandler = class {
1378
1524
  this.fsw._symlinkPaths.set(absPath, targetPath);
1379
1525
  }
1380
1526
  } else if (stats.isSymbolicLink()) {
1381
- const targetPath = follow ? await fsrealpath(path7) : path7;
1527
+ const targetPath = follow ? await fsrealpath(path10) : path10;
1382
1528
  if (this.fsw.closed)
1383
1529
  return;
1384
1530
  const parent = sp.dirname(wh.watchPath);
1385
1531
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
1386
1532
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
1387
- closer = await this._handleDir(parent, stats, initialAdd, depth, path7, wh, targetPath);
1533
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path10, wh, targetPath);
1388
1534
  if (this.fsw.closed)
1389
1535
  return;
1390
1536
  if (targetPath !== void 0) {
1391
- this.fsw._symlinkPaths.set(sp.resolve(path7), targetPath);
1537
+ this.fsw._symlinkPaths.set(sp.resolve(path10), targetPath);
1392
1538
  }
1393
1539
  } else {
1394
1540
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
1395
1541
  }
1396
1542
  ready();
1397
1543
  if (closer)
1398
- this.fsw._addPathCloser(path7, closer);
1544
+ this.fsw._addPathCloser(path10, closer);
1399
1545
  return false;
1400
1546
  } catch (error) {
1401
1547
  if (this.fsw._handleError(error)) {
1402
1548
  ready();
1403
- return path7;
1549
+ return path10;
1404
1550
  }
1405
1551
  }
1406
1552
  }
@@ -1443,24 +1589,24 @@ function createPattern(matcher) {
1443
1589
  }
1444
1590
  return () => false;
1445
1591
  }
1446
- function normalizePath(path7) {
1447
- if (typeof path7 !== "string")
1592
+ function normalizePath(path10) {
1593
+ if (typeof path10 !== "string")
1448
1594
  throw new Error("string expected");
1449
- path7 = sp2.normalize(path7);
1450
- path7 = path7.replace(/\\/g, "/");
1595
+ path10 = sp2.normalize(path10);
1596
+ path10 = path10.replace(/\\/g, "/");
1451
1597
  let prepend = false;
1452
- if (path7.startsWith("//"))
1598
+ if (path10.startsWith("//"))
1453
1599
  prepend = true;
1454
- path7 = path7.replace(DOUBLE_SLASH_RE, "/");
1600
+ path10 = path10.replace(DOUBLE_SLASH_RE, "/");
1455
1601
  if (prepend)
1456
- path7 = "/" + path7;
1457
- return path7;
1602
+ path10 = "/" + path10;
1603
+ return path10;
1458
1604
  }
1459
1605
  function matchPatterns(patterns, testString, stats) {
1460
- const path7 = normalizePath(testString);
1606
+ const path10 = normalizePath(testString);
1461
1607
  for (let index = 0; index < patterns.length; index++) {
1462
1608
  const pattern = patterns[index];
1463
- if (pattern(path7, stats)) {
1609
+ if (pattern(path10, stats)) {
1464
1610
  return true;
1465
1611
  }
1466
1612
  }
@@ -1498,19 +1644,19 @@ var toUnix = (string) => {
1498
1644
  }
1499
1645
  return str;
1500
1646
  };
1501
- var normalizePathToUnix = (path7) => toUnix(sp2.normalize(toUnix(path7)));
1502
- var normalizeIgnored = (cwd = "") => (path7) => {
1503
- if (typeof path7 === "string") {
1504
- return normalizePathToUnix(sp2.isAbsolute(path7) ? path7 : sp2.join(cwd, path7));
1647
+ var normalizePathToUnix = (path10) => toUnix(sp2.normalize(toUnix(path10)));
1648
+ var normalizeIgnored = (cwd = "") => (path10) => {
1649
+ if (typeof path10 === "string") {
1650
+ return normalizePathToUnix(sp2.isAbsolute(path10) ? path10 : sp2.join(cwd, path10));
1505
1651
  } else {
1506
- return path7;
1652
+ return path10;
1507
1653
  }
1508
1654
  };
1509
- var getAbsolutePath = (path7, cwd) => {
1510
- if (sp2.isAbsolute(path7)) {
1511
- return path7;
1655
+ var getAbsolutePath = (path10, cwd) => {
1656
+ if (sp2.isAbsolute(path10)) {
1657
+ return path10;
1512
1658
  }
1513
- return sp2.join(cwd, path7);
1659
+ return sp2.join(cwd, path10);
1514
1660
  };
1515
1661
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
1516
1662
  var DirEntry = class {
@@ -1575,10 +1721,10 @@ var WatchHelper = class {
1575
1721
  dirParts;
1576
1722
  followSymlinks;
1577
1723
  statMethod;
1578
- constructor(path7, follow, fsw) {
1724
+ constructor(path10, follow, fsw) {
1579
1725
  this.fsw = fsw;
1580
- const watchPath = path7;
1581
- this.path = path7 = path7.replace(REPLACER_RE, "");
1726
+ const watchPath = path10;
1727
+ this.path = path10 = path10.replace(REPLACER_RE, "");
1582
1728
  this.watchPath = watchPath;
1583
1729
  this.fullWatchPath = sp2.resolve(watchPath);
1584
1730
  this.dirParts = [];
@@ -1718,20 +1864,20 @@ var FSWatcher = class extends EventEmitter {
1718
1864
  this._closePromise = void 0;
1719
1865
  let paths = unifyPaths(paths_);
1720
1866
  if (cwd) {
1721
- paths = paths.map((path7) => {
1722
- const absPath = getAbsolutePath(path7, cwd);
1867
+ paths = paths.map((path10) => {
1868
+ const absPath = getAbsolutePath(path10, cwd);
1723
1869
  return absPath;
1724
1870
  });
1725
1871
  }
1726
- paths.forEach((path7) => {
1727
- this._removeIgnoredPath(path7);
1872
+ paths.forEach((path10) => {
1873
+ this._removeIgnoredPath(path10);
1728
1874
  });
1729
1875
  this._userIgnored = void 0;
1730
1876
  if (!this._readyCount)
1731
1877
  this._readyCount = 0;
1732
1878
  this._readyCount += paths.length;
1733
- Promise.all(paths.map(async (path7) => {
1734
- const res = await this._nodeFsHandler._addToNodeFs(path7, !_internal, void 0, 0, _origAdd);
1879
+ Promise.all(paths.map(async (path10) => {
1880
+ const res = await this._nodeFsHandler._addToNodeFs(path10, !_internal, void 0, 0, _origAdd);
1735
1881
  if (res)
1736
1882
  this._emitReady();
1737
1883
  return res;
@@ -1753,17 +1899,17 @@ var FSWatcher = class extends EventEmitter {
1753
1899
  return this;
1754
1900
  const paths = unifyPaths(paths_);
1755
1901
  const { cwd } = this.options;
1756
- paths.forEach((path7) => {
1757
- if (!sp2.isAbsolute(path7) && !this._closers.has(path7)) {
1902
+ paths.forEach((path10) => {
1903
+ if (!sp2.isAbsolute(path10) && !this._closers.has(path10)) {
1758
1904
  if (cwd)
1759
- path7 = sp2.join(cwd, path7);
1760
- path7 = sp2.resolve(path7);
1905
+ path10 = sp2.join(cwd, path10);
1906
+ path10 = sp2.resolve(path10);
1761
1907
  }
1762
- this._closePath(path7);
1763
- this._addIgnoredPath(path7);
1764
- if (this._watched.has(path7)) {
1908
+ this._closePath(path10);
1909
+ this._addIgnoredPath(path10);
1910
+ if (this._watched.has(path10)) {
1765
1911
  this._addIgnoredPath({
1766
- path: path7,
1912
+ path: path10,
1767
1913
  recursive: true
1768
1914
  });
1769
1915
  }
@@ -1827,38 +1973,38 @@ var FSWatcher = class extends EventEmitter {
1827
1973
  * @param stats arguments to be passed with event
1828
1974
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
1829
1975
  */
1830
- async _emit(event, path7, stats) {
1976
+ async _emit(event, path10, stats) {
1831
1977
  if (this.closed)
1832
1978
  return;
1833
1979
  const opts = this.options;
1834
1980
  if (isWindows)
1835
- path7 = sp2.normalize(path7);
1981
+ path10 = sp2.normalize(path10);
1836
1982
  if (opts.cwd)
1837
- path7 = sp2.relative(opts.cwd, path7);
1838
- const args = [path7];
1983
+ path10 = sp2.relative(opts.cwd, path10);
1984
+ const args = [path10];
1839
1985
  if (stats != null)
1840
1986
  args.push(stats);
1841
1987
  const awf = opts.awaitWriteFinish;
1842
1988
  let pw;
1843
- if (awf && (pw = this._pendingWrites.get(path7))) {
1989
+ if (awf && (pw = this._pendingWrites.get(path10))) {
1844
1990
  pw.lastChange = /* @__PURE__ */ new Date();
1845
1991
  return this;
1846
1992
  }
1847
1993
  if (opts.atomic) {
1848
1994
  if (event === EVENTS.UNLINK) {
1849
- this._pendingUnlinks.set(path7, [event, ...args]);
1995
+ this._pendingUnlinks.set(path10, [event, ...args]);
1850
1996
  setTimeout(() => {
1851
- this._pendingUnlinks.forEach((entry, path8) => {
1997
+ this._pendingUnlinks.forEach((entry, path11) => {
1852
1998
  this.emit(...entry);
1853
1999
  this.emit(EVENTS.ALL, ...entry);
1854
- this._pendingUnlinks.delete(path8);
2000
+ this._pendingUnlinks.delete(path11);
1855
2001
  });
1856
2002
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
1857
2003
  return this;
1858
2004
  }
1859
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path7)) {
2005
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path10)) {
1860
2006
  event = EVENTS.CHANGE;
1861
- this._pendingUnlinks.delete(path7);
2007
+ this._pendingUnlinks.delete(path10);
1862
2008
  }
1863
2009
  }
1864
2010
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -1876,16 +2022,16 @@ var FSWatcher = class extends EventEmitter {
1876
2022
  this.emitWithAll(event, args);
1877
2023
  }
1878
2024
  };
1879
- this._awaitWriteFinish(path7, awf.stabilityThreshold, event, awfEmit);
2025
+ this._awaitWriteFinish(path10, awf.stabilityThreshold, event, awfEmit);
1880
2026
  return this;
1881
2027
  }
1882
2028
  if (event === EVENTS.CHANGE) {
1883
- const isThrottled = !this._throttle(EVENTS.CHANGE, path7, 50);
2029
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path10, 50);
1884
2030
  if (isThrottled)
1885
2031
  return this;
1886
2032
  }
1887
2033
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
1888
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path7) : path7;
2034
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path10) : path10;
1889
2035
  let stats2;
1890
2036
  try {
1891
2037
  stats2 = await stat3(fullPath);
@@ -1916,23 +2062,23 @@ var FSWatcher = class extends EventEmitter {
1916
2062
  * @param timeout duration of time to suppress duplicate actions
1917
2063
  * @returns tracking object or false if action should be suppressed
1918
2064
  */
1919
- _throttle(actionType, path7, timeout) {
2065
+ _throttle(actionType, path10, timeout) {
1920
2066
  if (!this._throttled.has(actionType)) {
1921
2067
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
1922
2068
  }
1923
2069
  const action = this._throttled.get(actionType);
1924
2070
  if (!action)
1925
2071
  throw new Error("invalid throttle");
1926
- const actionPath = action.get(path7);
2072
+ const actionPath = action.get(path10);
1927
2073
  if (actionPath) {
1928
2074
  actionPath.count++;
1929
2075
  return false;
1930
2076
  }
1931
2077
  let timeoutObject;
1932
2078
  const clear = () => {
1933
- const item = action.get(path7);
2079
+ const item = action.get(path10);
1934
2080
  const count = item ? item.count : 0;
1935
- action.delete(path7);
2081
+ action.delete(path10);
1936
2082
  clearTimeout(timeoutObject);
1937
2083
  if (item)
1938
2084
  clearTimeout(item.timeoutObject);
@@ -1940,7 +2086,7 @@ var FSWatcher = class extends EventEmitter {
1940
2086
  };
1941
2087
  timeoutObject = setTimeout(clear, timeout);
1942
2088
  const thr = { timeoutObject, clear, count: 0 };
1943
- action.set(path7, thr);
2089
+ action.set(path10, thr);
1944
2090
  return thr;
1945
2091
  }
1946
2092
  _incrReadyCount() {
@@ -1954,44 +2100,44 @@ var FSWatcher = class extends EventEmitter {
1954
2100
  * @param event
1955
2101
  * @param awfEmit Callback to be called when ready for event to be emitted.
1956
2102
  */
1957
- _awaitWriteFinish(path7, threshold, event, awfEmit) {
2103
+ _awaitWriteFinish(path10, threshold, event, awfEmit) {
1958
2104
  const awf = this.options.awaitWriteFinish;
1959
2105
  if (typeof awf !== "object")
1960
2106
  return;
1961
2107
  const pollInterval = awf.pollInterval;
1962
2108
  let timeoutHandler;
1963
- let fullPath = path7;
1964
- if (this.options.cwd && !sp2.isAbsolute(path7)) {
1965
- fullPath = sp2.join(this.options.cwd, path7);
2109
+ let fullPath = path10;
2110
+ if (this.options.cwd && !sp2.isAbsolute(path10)) {
2111
+ fullPath = sp2.join(this.options.cwd, path10);
1966
2112
  }
1967
2113
  const now = /* @__PURE__ */ new Date();
1968
2114
  const writes = this._pendingWrites;
1969
2115
  function awaitWriteFinishFn(prevStat) {
1970
2116
  statcb(fullPath, (err, curStat) => {
1971
- if (err || !writes.has(path7)) {
2117
+ if (err || !writes.has(path10)) {
1972
2118
  if (err && err.code !== "ENOENT")
1973
2119
  awfEmit(err);
1974
2120
  return;
1975
2121
  }
1976
2122
  const now2 = Number(/* @__PURE__ */ new Date());
1977
2123
  if (prevStat && curStat.size !== prevStat.size) {
1978
- writes.get(path7).lastChange = now2;
2124
+ writes.get(path10).lastChange = now2;
1979
2125
  }
1980
- const pw = writes.get(path7);
2126
+ const pw = writes.get(path10);
1981
2127
  const df = now2 - pw.lastChange;
1982
2128
  if (df >= threshold) {
1983
- writes.delete(path7);
2129
+ writes.delete(path10);
1984
2130
  awfEmit(void 0, curStat);
1985
2131
  } else {
1986
2132
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
1987
2133
  }
1988
2134
  });
1989
2135
  }
1990
- if (!writes.has(path7)) {
1991
- writes.set(path7, {
2136
+ if (!writes.has(path10)) {
2137
+ writes.set(path10, {
1992
2138
  lastChange: now,
1993
2139
  cancelWait: () => {
1994
- writes.delete(path7);
2140
+ writes.delete(path10);
1995
2141
  clearTimeout(timeoutHandler);
1996
2142
  return event;
1997
2143
  }
@@ -2002,8 +2148,8 @@ var FSWatcher = class extends EventEmitter {
2002
2148
  /**
2003
2149
  * Determines whether user has asked to ignore this path.
2004
2150
  */
2005
- _isIgnored(path7, stats) {
2006
- if (this.options.atomic && DOT_RE.test(path7))
2151
+ _isIgnored(path10, stats) {
2152
+ if (this.options.atomic && DOT_RE.test(path10))
2007
2153
  return true;
2008
2154
  if (!this._userIgnored) {
2009
2155
  const { cwd } = this.options;
@@ -2013,17 +2159,17 @@ var FSWatcher = class extends EventEmitter {
2013
2159
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
2014
2160
  this._userIgnored = anymatch(list, void 0);
2015
2161
  }
2016
- return this._userIgnored(path7, stats);
2162
+ return this._userIgnored(path10, stats);
2017
2163
  }
2018
- _isntIgnored(path7, stat4) {
2019
- return !this._isIgnored(path7, stat4);
2164
+ _isntIgnored(path10, stat4) {
2165
+ return !this._isIgnored(path10, stat4);
2020
2166
  }
2021
2167
  /**
2022
2168
  * Provides a set of common helpers and properties relating to symlink handling.
2023
2169
  * @param path file or directory pattern being watched
2024
2170
  */
2025
- _getWatchHelpers(path7) {
2026
- return new WatchHelper(path7, this.options.followSymlinks, this);
2171
+ _getWatchHelpers(path10) {
2172
+ return new WatchHelper(path10, this.options.followSymlinks, this);
2027
2173
  }
2028
2174
  // Directory helpers
2029
2175
  // -----------------
@@ -2055,63 +2201,63 @@ var FSWatcher = class extends EventEmitter {
2055
2201
  * @param item base path of item/directory
2056
2202
  */
2057
2203
  _remove(directory, item, isDirectory) {
2058
- const path7 = sp2.join(directory, item);
2059
- const fullPath = sp2.resolve(path7);
2060
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path7) || this._watched.has(fullPath);
2061
- if (!this._throttle("remove", path7, 100))
2204
+ const path10 = sp2.join(directory, item);
2205
+ const fullPath = sp2.resolve(path10);
2206
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path10) || this._watched.has(fullPath);
2207
+ if (!this._throttle("remove", path10, 100))
2062
2208
  return;
2063
2209
  if (!isDirectory && this._watched.size === 1) {
2064
2210
  this.add(directory, item, true);
2065
2211
  }
2066
- const wp = this._getWatchedDir(path7);
2212
+ const wp = this._getWatchedDir(path10);
2067
2213
  const nestedDirectoryChildren = wp.getChildren();
2068
- nestedDirectoryChildren.forEach((nested) => this._remove(path7, nested));
2214
+ nestedDirectoryChildren.forEach((nested) => this._remove(path10, nested));
2069
2215
  const parent = this._getWatchedDir(directory);
2070
2216
  const wasTracked = parent.has(item);
2071
2217
  parent.remove(item);
2072
2218
  if (this._symlinkPaths.has(fullPath)) {
2073
2219
  this._symlinkPaths.delete(fullPath);
2074
2220
  }
2075
- let relPath = path7;
2221
+ let relPath = path10;
2076
2222
  if (this.options.cwd)
2077
- relPath = sp2.relative(this.options.cwd, path7);
2223
+ relPath = sp2.relative(this.options.cwd, path10);
2078
2224
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
2079
2225
  const event = this._pendingWrites.get(relPath).cancelWait();
2080
2226
  if (event === EVENTS.ADD)
2081
2227
  return;
2082
2228
  }
2083
- this._watched.delete(path7);
2229
+ this._watched.delete(path10);
2084
2230
  this._watched.delete(fullPath);
2085
2231
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
2086
- if (wasTracked && !this._isIgnored(path7))
2087
- this._emit(eventName, path7);
2088
- this._closePath(path7);
2232
+ if (wasTracked && !this._isIgnored(path10))
2233
+ this._emit(eventName, path10);
2234
+ this._closePath(path10);
2089
2235
  }
2090
2236
  /**
2091
2237
  * Closes all watchers for a path
2092
2238
  */
2093
- _closePath(path7) {
2094
- this._closeFile(path7);
2095
- const dir = sp2.dirname(path7);
2096
- this._getWatchedDir(dir).remove(sp2.basename(path7));
2239
+ _closePath(path10) {
2240
+ this._closeFile(path10);
2241
+ const dir = sp2.dirname(path10);
2242
+ this._getWatchedDir(dir).remove(sp2.basename(path10));
2097
2243
  }
2098
2244
  /**
2099
2245
  * Closes only file-specific watchers
2100
2246
  */
2101
- _closeFile(path7) {
2102
- const closers = this._closers.get(path7);
2247
+ _closeFile(path10) {
2248
+ const closers = this._closers.get(path10);
2103
2249
  if (!closers)
2104
2250
  return;
2105
2251
  closers.forEach((closer) => closer());
2106
- this._closers.delete(path7);
2252
+ this._closers.delete(path10);
2107
2253
  }
2108
- _addPathCloser(path7, closer) {
2254
+ _addPathCloser(path10, closer) {
2109
2255
  if (!closer)
2110
2256
  return;
2111
- let list = this._closers.get(path7);
2257
+ let list = this._closers.get(path10);
2112
2258
  if (!list) {
2113
2259
  list = [];
2114
- this._closers.set(path7, list);
2260
+ this._closers.set(path10, list);
2115
2261
  }
2116
2262
  list.push(closer);
2117
2263
  }
@@ -2140,7 +2286,7 @@ function watch(paths, options = {}) {
2140
2286
  }
2141
2287
 
2142
2288
  // src/daemon/watcher.ts
2143
- import path3 from "path";
2289
+ import path4 from "path";
2144
2290
  var PlanWatcher = class {
2145
2291
  config;
2146
2292
  fsWatcher = null;
@@ -2166,7 +2312,7 @@ var PlanWatcher = class {
2166
2312
  source: "tool",
2167
2313
  filePath,
2168
2314
  sessionId: event.session_id,
2169
- detail: `${event.tool_name} on plan file: ${path3.basename(filePath)}`,
2315
+ detail: `${event.tool_name} on plan file: ${path4.basename(filePath)}`,
2170
2316
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2171
2317
  });
2172
2318
  }
@@ -2174,7 +2320,7 @@ var PlanWatcher = class {
2174
2320
  }
2175
2321
  startFileWatcher() {
2176
2322
  const absPaths = this.config.watchPaths.map(
2177
- (p) => path3.resolve(this.config.projectRoot, p)
2323
+ (p) => path4.resolve(this.config.projectRoot, p)
2178
2324
  );
2179
2325
  this.fsWatcher = watch(absPaths, {
2180
2326
  ignoreInitial: true,
@@ -2190,7 +2336,7 @@ var PlanWatcher = class {
2190
2336
  this.fsWatcher = null;
2191
2337
  }
2192
2338
  onFileChange(absolutePath, action) {
2193
- const rel = path3.relative(this.config.projectRoot, absolutePath);
2339
+ const rel = path4.relative(this.config.projectRoot, absolutePath);
2194
2340
  this.knownPlans.add(absolutePath);
2195
2341
  this.config.onPlan({
2196
2342
  source: "filesystem",
@@ -2200,17 +2346,299 @@ var PlanWatcher = class {
2200
2346
  });
2201
2347
  }
2202
2348
  isInPlanDirectory(filePath) {
2203
- const abs = path3.isAbsolute(filePath) ? filePath : path3.resolve(this.config.projectRoot, filePath);
2349
+ const abs = path4.isAbsolute(filePath) ? filePath : path4.resolve(this.config.projectRoot, filePath);
2204
2350
  return this.config.watchPaths.some(
2205
- (wp) => abs.startsWith(path3.resolve(this.config.projectRoot, wp))
2351
+ (wp) => abs.startsWith(path4.resolve(this.config.projectRoot, wp))
2206
2352
  );
2207
2353
  }
2208
2354
  };
2209
2355
 
2356
+ // src/daemon/consolidation.ts
2357
+ import path5 from "path";
2358
+ var consolidationResponseSchema = external_exports.discriminatedUnion("consolidate", [
2359
+ external_exports.object({ consolidate: external_exports.literal(false), reason: external_exports.string().optional() }),
2360
+ external_exports.object({
2361
+ consolidate: external_exports.literal(true),
2362
+ title: external_exports.string(),
2363
+ content: external_exports.string(),
2364
+ source_ids: external_exports.array(external_exports.string()),
2365
+ tags: external_exports.array(external_exports.string()).optional()
2366
+ })
2367
+ ]);
2368
+ var CONSOLIDATION_TRACE_FILENAME = "consolidation-trace.jsonl";
2369
+ var ConsolidationEngine = class {
2370
+ deps;
2371
+ log;
2372
+ lastTimestampCache = void 0;
2373
+ constructor(config) {
2374
+ this.deps = {
2375
+ vaultDir: config.vaultDir,
2376
+ index: config.index,
2377
+ vectorIndex: config.vectorIndex,
2378
+ llmProvider: config.llmProvider,
2379
+ embeddingProvider: config.embeddingProvider
2380
+ };
2381
+ this.log = config.log ?? (() => {
2382
+ });
2383
+ }
2384
+ /**
2385
+ * Read the timestamp of the last consolidation pass from the trace file.
2386
+ * Cached in memory after first read — subsequent calls are O(1).
2387
+ */
2388
+ getLastTimestamp() {
2389
+ if (this.lastTimestampCache !== void 0) return this.lastTimestampCache;
2390
+ const tracePath = path5.join(this.deps.vaultDir, "digest", CONSOLIDATION_TRACE_FILENAME);
2391
+ this.lastTimestampCache = readLastTimestamp(tracePath);
2392
+ return this.lastTimestampCache;
2393
+ }
2394
+ /**
2395
+ * Append a consolidation pass result as a JSON line to the trace file.
2396
+ * Updates the in-memory timestamp cache.
2397
+ */
2398
+ appendTrace(record) {
2399
+ const tracePath = path5.join(this.deps.vaultDir, "digest", CONSOLIDATION_TRACE_FILENAME);
2400
+ appendTraceRecord(tracePath, record);
2401
+ this.lastTimestampCache = record.timestamp;
2402
+ }
2403
+ /**
2404
+ * Run one consolidation pass.
2405
+ *
2406
+ * Algorithm:
2407
+ * 1. Early-exit if vectorIndex or llmProvider is absent.
2408
+ * 2. Query index for spores created since lastTimestamp.
2409
+ * 3. Filter for active spores.
2410
+ * 4. For each new spore (skip if already processed):
2411
+ * a. Embed it.
2412
+ * b. Vector-search for similar spores.
2413
+ * c. Fetch candidate notes; filter same-type + active + not-processed.
2414
+ * d. Include trigger spore in cluster if not already present.
2415
+ * e. Skip if cluster < CONSOLIDATION_MIN_CLUSTER_SIZE.
2416
+ * f. Ask LLM whether to consolidate.
2417
+ * g. If approved, call consolidateSpores(); track counts.
2418
+ * h. Mark cluster members as processed regardless of outcome.
2419
+ * 5. Append trace record, return result.
2420
+ *
2421
+ * Returns null if no new spores were found or dependencies are absent.
2422
+ */
2423
+ async runPass() {
2424
+ const { vaultDir, index, vectorIndex, llmProvider, embeddingProvider } = this.deps;
2425
+ if (!vectorIndex || !llmProvider || !embeddingProvider) {
2426
+ this.log("debug", "ConsolidationEngine: skipped \u2014 vectorIndex, llmProvider, or embeddingProvider unavailable");
2427
+ return null;
2428
+ }
2429
+ const startTime = Date.now();
2430
+ const lastTimestamp = this.getLastTimestamp();
2431
+ const allSpores = index.query({ type: "spore", since: lastTimestamp ?? void 0 });
2432
+ const newSpores = allSpores.filter((n) => isActiveSpore(n.frontmatter));
2433
+ this.log("debug", "ConsolidationEngine: spores to check", {
2434
+ count: newSpores.length,
2435
+ lastTimestamp: lastTimestamp ?? "never"
2436
+ });
2437
+ if (newSpores.length === 0) {
2438
+ return null;
2439
+ }
2440
+ const processedIds = /* @__PURE__ */ new Set();
2441
+ let clustersFound = 0;
2442
+ let consolidated = 0;
2443
+ let sporesSuperseded = 0;
2444
+ const template = loadPrompt("consolidation");
2445
+ for (const triggerSpore of newSpores) {
2446
+ if (processedIds.has(triggerSpore.id)) continue;
2447
+ const observationType = triggerSpore.frontmatter["observation_type"];
2448
+ let embedding;
2449
+ const storedEmbedding = vectorIndex.getEmbedding(triggerSpore.id);
2450
+ if (storedEmbedding) {
2451
+ embedding = storedEmbedding;
2452
+ } else {
2453
+ try {
2454
+ const embeddingText = triggerSpore.content.slice(0, EMBEDDING_INPUT_LIMIT);
2455
+ const result = await generateEmbedding(embeddingProvider, embeddingText);
2456
+ embedding = result.embedding;
2457
+ } catch (err) {
2458
+ this.log("warn", "ConsolidationEngine: embedding failed", { id: triggerSpore.id, error: String(err) });
2459
+ processedIds.add(triggerSpore.id);
2460
+ continue;
2461
+ }
2462
+ }
2463
+ const vectorResults = vectorIndex.search(embedding, {
2464
+ type: "spore",
2465
+ limit: CONSOLIDATION_VECTOR_FETCH_LIMIT
2466
+ });
2467
+ if (vectorResults.length === 0) {
2468
+ processedIds.add(triggerSpore.id);
2469
+ continue;
2470
+ }
2471
+ const candidateIds = vectorResults.map((r) => r.id);
2472
+ const candidateNotes = index.queryByIds(candidateIds);
2473
+ const filtered = candidateNotes.filter((note) => {
2474
+ if (processedIds.has(note.id)) return false;
2475
+ if (!isActiveSpore(note.frontmatter)) return false;
2476
+ if (observationType && note.frontmatter["observation_type"] !== observationType) return false;
2477
+ return true;
2478
+ });
2479
+ const clusterMap = new Map(filtered.map((n) => [n.id, n]));
2480
+ if (!clusterMap.has(triggerSpore.id)) {
2481
+ clusterMap.set(triggerSpore.id, triggerSpore);
2482
+ }
2483
+ const cluster = Array.from(clusterMap.values());
2484
+ if (cluster.length < CONSOLIDATION_MIN_CLUSTER_SIZE) {
2485
+ this.log("debug", "ConsolidationEngine: cluster too small \u2014 skipping", {
2486
+ triggerId: triggerSpore.id,
2487
+ clusterSize: cluster.length,
2488
+ minRequired: CONSOLIDATION_MIN_CLUSTER_SIZE
2489
+ });
2490
+ cluster.forEach((n) => processedIds.add(n.id));
2491
+ continue;
2492
+ }
2493
+ clustersFound++;
2494
+ const candidatesText = formatNotesForPrompt(cluster);
2495
+ const prompt = template.replace("{{count}}", String(cluster.length)).replace("{{observation_type}}", observationType ?? "unknown").replace("{{candidates}}", candidatesText);
2496
+ let responseText;
2497
+ try {
2498
+ const response = await llmProvider.summarize(prompt, {
2499
+ maxTokens: CONSOLIDATION_MAX_TOKENS,
2500
+ reasoning: LLM_REASONING_MODE
2501
+ });
2502
+ responseText = stripReasoningTokens(response.text);
2503
+ } catch (err) {
2504
+ this.log("warn", "ConsolidationEngine: LLM call failed", {
2505
+ triggerId: triggerSpore.id,
2506
+ error: String(err)
2507
+ });
2508
+ cluster.forEach((n) => processedIds.add(n.id));
2509
+ continue;
2510
+ }
2511
+ let parsed;
2512
+ try {
2513
+ const raw = JSON.parse(responseText);
2514
+ const result = consolidationResponseSchema.safeParse(raw);
2515
+ if (!result.success) {
2516
+ this.log("warn", "ConsolidationEngine: LLM response failed schema validation", {
2517
+ triggerId: triggerSpore.id,
2518
+ responseText
2519
+ });
2520
+ cluster.forEach((n) => processedIds.add(n.id));
2521
+ continue;
2522
+ }
2523
+ parsed = result.data;
2524
+ } catch {
2525
+ this.log("warn", "ConsolidationEngine: failed to parse LLM response", {
2526
+ triggerId: triggerSpore.id,
2527
+ responseText
2528
+ });
2529
+ cluster.forEach((n) => processedIds.add(n.id));
2530
+ continue;
2531
+ }
2532
+ if (!parsed.consolidate) {
2533
+ this.log("debug", "ConsolidationEngine: LLM declined to consolidate", {
2534
+ triggerId: triggerSpore.id,
2535
+ reason: parsed.reason
2536
+ });
2537
+ cluster.forEach((n) => processedIds.add(n.id));
2538
+ continue;
2539
+ }
2540
+ const clusterIdSet = new Set(cluster.map((n) => n.id));
2541
+ const validSourceIds = parsed.source_ids.filter((id) => clusterIdSet.has(id));
2542
+ if (validSourceIds.length < CONSOLIDATION_MIN_CLUSTER_SIZE) {
2543
+ this.log("warn", "ConsolidationEngine: insufficient valid source_ids after validation", {
2544
+ triggerId: triggerSpore.id,
2545
+ sourceIds: parsed.source_ids,
2546
+ validCount: validSourceIds.length
2547
+ });
2548
+ cluster.forEach((n) => processedIds.add(n.id));
2549
+ continue;
2550
+ }
2551
+ try {
2552
+ const consolidateResult = await consolidateSpores(
2553
+ {
2554
+ sourceSporeIds: validSourceIds,
2555
+ consolidatedContent: parsed.content,
2556
+ observationType: observationType ?? "gotcha",
2557
+ tags: parsed.tags
2558
+ },
2559
+ {
2560
+ vaultDir,
2561
+ index,
2562
+ vectorIndex,
2563
+ embeddingProvider: embeddingProvider ?? null
2564
+ }
2565
+ );
2566
+ consolidated++;
2567
+ sporesSuperseded += consolidateResult.sources_archived;
2568
+ this.log("info", "ConsolidationEngine: consolidated cluster", {
2569
+ wisdomId: consolidateResult.wisdom_id,
2570
+ sourcesArchived: consolidateResult.sources_archived,
2571
+ clusterSize: cluster.length
2572
+ });
2573
+ } catch (err) {
2574
+ this.log("warn", "ConsolidationEngine: consolidateSpores failed", {
2575
+ triggerId: triggerSpore.id,
2576
+ error: String(err)
2577
+ });
2578
+ }
2579
+ cluster.forEach((n) => processedIds.add(n.id));
2580
+ }
2581
+ const passTimestamp = (/* @__PURE__ */ new Date()).toISOString();
2582
+ const durationMs = Date.now() - startTime;
2583
+ const passResult = {
2584
+ timestamp: passTimestamp,
2585
+ sporesChecked: newSpores.length,
2586
+ clustersFound,
2587
+ consolidated,
2588
+ sporesSuperseded,
2589
+ durationMs
2590
+ };
2591
+ if (consolidated > 0) {
2592
+ this.appendTrace(passResult);
2593
+ } else {
2594
+ this.lastTimestampCache = passTimestamp;
2595
+ }
2596
+ this.log("info", "ConsolidationEngine: pass complete", {
2597
+ sporesChecked: newSpores.length,
2598
+ clustersFound,
2599
+ consolidated,
2600
+ sporesSuperseded,
2601
+ durationMs
2602
+ });
2603
+ return passResult;
2604
+ }
2605
+ };
2606
+
2607
+ // src/daemon/port.ts
2608
+ import { createHash } from "crypto";
2609
+ import net from "net";
2610
+ var PORT_RANGE_START = 19200;
2611
+ var PORT_RANGE_SIZE = 1e4;
2612
+ var PORT_RETRY_COUNT = 10;
2613
+ function derivePort(vaultPath) {
2614
+ const hash = createHash("md5").update(vaultPath).digest();
2615
+ const num = hash.readUInt16LE(0);
2616
+ return PORT_RANGE_START + num % PORT_RANGE_SIZE;
2617
+ }
2618
+ async function resolvePort(configPort, vaultPath) {
2619
+ const basePort = configPort ?? derivePort(vaultPath);
2620
+ for (let offset = 0; offset < PORT_RETRY_COUNT; offset++) {
2621
+ const candidate = basePort + offset;
2622
+ if (candidate > 65535) break;
2623
+ if (await isPortAvailable(candidate)) return candidate;
2624
+ }
2625
+ return 0;
2626
+ }
2627
+ function isPortAvailable(port) {
2628
+ return new Promise((resolve3) => {
2629
+ const server = net.createServer();
2630
+ server.once("error", () => resolve3(false));
2631
+ server.once("listening", () => {
2632
+ server.close(() => resolve3(true));
2633
+ });
2634
+ server.listen(port, "127.0.0.1");
2635
+ });
2636
+ }
2637
+
2210
2638
  // src/artifacts/candidates.ts
2211
2639
  import { execFileSync } from "child_process";
2212
- import fs3 from "fs";
2213
- import path4 from "path";
2640
+ import fs4 from "fs";
2641
+ import path6 from "path";
2214
2642
  var EXCLUDED_FILENAMES = /* @__PURE__ */ new Set([
2215
2643
  "claude.md",
2216
2644
  "agents.md",
@@ -2231,7 +2659,7 @@ var EXCLUDED_PREFIXES = [
2231
2659
  ".github/"
2232
2660
  ];
2233
2661
  function isExcludedPath(relativePath) {
2234
- const basename3 = path4.basename(relativePath).toLowerCase();
2662
+ const basename3 = path6.basename(relativePath).toLowerCase();
2235
2663
  if (EXCLUDED_FILENAMES.has(basename3)) return true;
2236
2664
  const normalized = relativePath.replace(/\\/g, "/");
2237
2665
  return EXCLUDED_PREFIXES.some((prefix) => normalized.startsWith(prefix));
@@ -2239,7 +2667,7 @@ function isExcludedPath(relativePath) {
2239
2667
  function collectArtifactCandidates(filePaths, config, projectRoot) {
2240
2668
  if (filePaths.size === 0) return [];
2241
2669
  const extFiltered = [...filePaths].filter(
2242
- (absPath) => config.artifact_extensions.includes(path4.extname(absPath))
2670
+ (absPath) => config.artifact_extensions.includes(path6.extname(absPath))
2243
2671
  );
2244
2672
  if (extFiltered.length === 0) return [];
2245
2673
  const ignoredSet = getGitIgnored(extFiltered, projectRoot);
@@ -2247,8 +2675,8 @@ function collectArtifactCandidates(filePaths, config, projectRoot) {
2247
2675
  for (const absPath of extFiltered) {
2248
2676
  if (ignoredSet.has(absPath)) continue;
2249
2677
  try {
2250
- const content = fs3.readFileSync(absPath, "utf-8");
2251
- const relativePath = path4.relative(projectRoot, absPath);
2678
+ const content = fs4.readFileSync(absPath, "utf-8");
2679
+ const relativePath = path6.relative(projectRoot, absPath);
2252
2680
  if (isExcludedPath(relativePath)) continue;
2253
2681
  candidates.push({ path: relativePath, content });
2254
2682
  } catch {
@@ -2271,9 +2699,9 @@ function getGitIgnored(filePaths, cwd) {
2271
2699
 
2272
2700
  // src/artifacts/slugify.ts
2273
2701
  import crypto from "crypto";
2274
- import path5 from "path";
2702
+ import path7 from "path";
2275
2703
  function slugifyPath(relativePath) {
2276
- const ext = path5.extname(relativePath);
2704
+ const ext = path7.extname(relativePath);
2277
2705
  const withoutExt = ext ? relativePath.slice(0, -ext.length) : relativePath;
2278
2706
  let slug = withoutExt.replace(/[/\\]/g, "-").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
2279
2707
  if (slug.length > MAX_SLUG_LENGTH) {
@@ -2283,10 +2711,416 @@ function slugifyPath(relativePath) {
2283
2711
  return slug;
2284
2712
  }
2285
2713
 
2714
+ // src/daemon/api/config.ts
2715
+ async function handleGetConfig(vaultDir) {
2716
+ const config = loadConfig(vaultDir);
2717
+ return { body: config };
2718
+ }
2719
+ async function handlePutConfig(vaultDir, body) {
2720
+ const result = MycoConfigSchema.safeParse(body);
2721
+ if (!result.success) {
2722
+ return {
2723
+ status: 400,
2724
+ body: { error: "validation_failed", issues: result.error.issues }
2725
+ };
2726
+ }
2727
+ saveConfig(vaultDir, result.data);
2728
+ return { body: result.data };
2729
+ }
2730
+
2731
+ // src/daemon/api/stats.ts
2732
+ import { createHash as createHash2 } from "crypto";
2733
+ import fs5 from "fs";
2734
+ import path8 from "path";
2735
+ async function handleGetStats(deps) {
2736
+ const baseStats = gatherStats(deps.vaultDir, deps.index, deps.vectorIndex ?? void 0);
2737
+ const digestConfig = deps.config.digest;
2738
+ const digest = {
2739
+ enabled: digestConfig.enabled,
2740
+ consolidation_enabled: digestConfig.consolidation,
2741
+ metabolism_state: deps.metabolism?.state ?? null,
2742
+ last_cycle: null,
2743
+ substrate_queue: 0
2744
+ };
2745
+ return {
2746
+ body: {
2747
+ daemon: {
2748
+ ...baseStats.daemon,
2749
+ pid: baseStats.daemon?.pid ?? process.pid,
2750
+ port: baseStats.daemon?.port ?? 0,
2751
+ version: deps.version,
2752
+ uptime_seconds: process.uptime(),
2753
+ active_sessions: baseStats.daemon?.active_sessions ?? [],
2754
+ config_hash: deps.configHash
2755
+ },
2756
+ vault: baseStats.vault,
2757
+ index: baseStats.index,
2758
+ digest,
2759
+ intelligence: {
2760
+ processor: {
2761
+ provider: deps.config.intelligence.llm.provider,
2762
+ model: deps.config.intelligence.llm.model
2763
+ },
2764
+ digest: digestConfig.intelligence.provider ? {
2765
+ provider: digestConfig.intelligence.provider,
2766
+ model: digestConfig.intelligence.model ?? deps.config.intelligence.llm.model
2767
+ } : null,
2768
+ embedding: {
2769
+ provider: deps.config.intelligence.embedding.provider,
2770
+ model: deps.config.intelligence.embedding.model
2771
+ }
2772
+ }
2773
+ }
2774
+ };
2775
+ }
2776
+ function computeConfigHash(vaultDir) {
2777
+ try {
2778
+ const configPath = path8.join(vaultDir, CONFIG_FILENAME);
2779
+ const raw = fs5.readFileSync(configPath, "utf-8");
2780
+ return createHash2("md5").update(raw).digest("hex");
2781
+ } catch {
2782
+ return "";
2783
+ }
2784
+ }
2785
+
2786
+ // src/daemon/api/logs.ts
2787
+ async function handleGetLogs(ringBuffer, query) {
2788
+ const since = query.since || null;
2789
+ const level = query.level;
2790
+ const limit = query.limit ? parseInt(query.limit, 10) : void 0;
2791
+ const result = ringBuffer.since(since, { level, limit: isNaN(limit) ? void 0 : limit });
2792
+ const entries = result.entries.map((entry) => {
2793
+ const { component, ...rest } = entry;
2794
+ return { ...rest, category: component };
2795
+ });
2796
+ return {
2797
+ body: {
2798
+ entries,
2799
+ cursor: result.cursor,
2800
+ ...result.cursor_reset ? { cursor_reset: true } : {}
2801
+ }
2802
+ };
2803
+ }
2804
+
2805
+ // src/daemon/api/restart.ts
2806
+ import { spawn } from "child_process";
2807
+ var RestartBodySchema = external_exports.object({
2808
+ force: external_exports.boolean().optional()
2809
+ }).optional();
2810
+ var RESTART_RESPONSE_FLUSH_MS = 500;
2811
+ var RESTART_CHILD_DELAY_SECONDS = 3;
2812
+ async function handleRestart(deps, body) {
2813
+ const parsed = RestartBodySchema.safeParse(body);
2814
+ const force = parsed.success ? parsed.data?.force : false;
2815
+ if (!force && deps.progressTracker.hasActiveOperations()) {
2816
+ return {
2817
+ status: 409,
2818
+ body: { status: "busy", message: "Active operations in progress. Use force=true to override." }
2819
+ };
2820
+ }
2821
+ const daemonScript = process.argv[1];
2822
+ const shellCmd = `sleep ${RESTART_CHILD_DELAY_SECONDS} && ${process.execPath} ${daemonScript} --vault ${deps.vaultDir}`;
2823
+ const child = spawn("/bin/sh", ["-c", shellCmd], {
2824
+ detached: true,
2825
+ stdio: "ignore"
2826
+ });
2827
+ child.unref();
2828
+ setTimeout(() => {
2829
+ process.kill(process.pid, "SIGTERM");
2830
+ }, RESTART_RESPONSE_FLUSH_MS);
2831
+ return { body: { status: "restarting" } };
2832
+ }
2833
+
2834
+ // src/daemon/api/progress.ts
2835
+ import { randomUUID } from "crypto";
2836
+ var MAX_CONCURRENT_OPERATIONS = 10;
2837
+ var PROGRESS_TTL_MS = 5 * 60 * 1e3;
2838
+ var ProgressTracker = class {
2839
+ entries = /* @__PURE__ */ new Map();
2840
+ /**
2841
+ * Create a new tracked operation. Returns the existing token if an
2842
+ * operation of the same type is already running (duplicate prevention).
2843
+ * Throws if the maximum concurrent operations limit is reached.
2844
+ */
2845
+ /**
2846
+ * Create a new tracked operation or return existing one.
2847
+ * Returns `{ token, isNew }` — if `isNew` is false, the operation
2848
+ * was already running and the caller should NOT launch it again.
2849
+ * Throws if the maximum concurrent operations limit is reached.
2850
+ */
2851
+ create(type) {
2852
+ this.cleanup();
2853
+ for (const entry of this.entries.values()) {
2854
+ if (entry.type === type && entry.status === "running") {
2855
+ return { token: entry.token, isNew: false };
2856
+ }
2857
+ }
2858
+ const runningCount = [...this.entries.values()].filter((e) => e.status === "running").length;
2859
+ if (runningCount >= MAX_CONCURRENT_OPERATIONS) {
2860
+ throw new Error(`Maximum concurrent operations reached (${MAX_CONCURRENT_OPERATIONS})`);
2861
+ }
2862
+ const token = randomUUID();
2863
+ const now = Date.now();
2864
+ this.entries.set(token, {
2865
+ token,
2866
+ type,
2867
+ status: "running",
2868
+ created: now,
2869
+ updated: now
2870
+ });
2871
+ return { token, isNew: true };
2872
+ }
2873
+ /**
2874
+ * Update progress for a tracked operation.
2875
+ */
2876
+ update(token, data) {
2877
+ const entry = this.entries.get(token);
2878
+ if (!entry) return;
2879
+ if (data.percent !== void 0) entry.percent = data.percent;
2880
+ if (data.message !== void 0) entry.message = data.message;
2881
+ if (data.status !== void 0) entry.status = data.status;
2882
+ entry.updated = Date.now();
2883
+ }
2884
+ /**
2885
+ * Get the current state of a tracked operation.
2886
+ */
2887
+ get(token) {
2888
+ return this.entries.get(token);
2889
+ }
2890
+ /**
2891
+ * Check whether any operations are currently running.
2892
+ */
2893
+ hasActiveOperations() {
2894
+ for (const entry of this.entries.values()) {
2895
+ if (entry.status === "running") return true;
2896
+ }
2897
+ return false;
2898
+ }
2899
+ /**
2900
+ * Remove completed/failed entries older than PROGRESS_TTL_MS.
2901
+ */
2902
+ cleanup() {
2903
+ const cutoff = Date.now() - PROGRESS_TTL_MS;
2904
+ for (const [token, entry] of this.entries) {
2905
+ if (entry.status !== "running" && entry.updated < cutoff) {
2906
+ this.entries.delete(token);
2907
+ }
2908
+ }
2909
+ }
2910
+ };
2911
+ async function handleGetProgress(tracker, token) {
2912
+ const entry = tracker.get(token);
2913
+ if (!entry) {
2914
+ return { status: 404, body: { error: "not_found", message: "Progress token not found" } };
2915
+ }
2916
+ return { body: entry };
2917
+ }
2918
+
2919
+ // src/daemon/api/operations.ts
2920
+ var PROGRESS_COMPLETE = 100;
2921
+ var CurateBody = external_exports.object({
2922
+ dry_run: external_exports.boolean().optional()
2923
+ }).optional();
2924
+ var DigestBody = external_exports.object({
2925
+ tier: external_exports.number().int().positive().optional(),
2926
+ full: external_exports.boolean().optional()
2927
+ }).optional();
2928
+ async function handleRebuild(deps) {
2929
+ const { token, isNew } = deps.progressTracker.create("rebuild");
2930
+ if (!isNew) {
2931
+ return { body: { token, status: "already_running" } };
2932
+ }
2933
+ runRebuild(
2934
+ {
2935
+ vaultDir: deps.vaultDir,
2936
+ config: deps.config,
2937
+ index: deps.index,
2938
+ vectorIndex: deps.vectorIndex ?? void 0,
2939
+ log: deps.log
2940
+ },
2941
+ deps.embeddingProvider,
2942
+ (done, total) => {
2943
+ const percent = total > 0 ? Math.round(done / total * PROGRESS_COMPLETE) : 0;
2944
+ deps.progressTracker.update(token, {
2945
+ percent,
2946
+ message: `Embedded ${done}/${total} notes`
2947
+ });
2948
+ }
2949
+ ).then((result) => {
2950
+ deps.progressTracker.update(token, {
2951
+ status: "completed",
2952
+ percent: PROGRESS_COMPLETE,
2953
+ message: `FTS: ${result.ftsCount}, embedded: ${result.embeddedCount}, failed: ${result.failedCount}, skipped: ${result.skippedCount}`
2954
+ });
2955
+ deps.log("info", "Rebuild completed via API", {
2956
+ fts: result.ftsCount,
2957
+ embedded: result.embeddedCount,
2958
+ failed: result.failedCount
2959
+ });
2960
+ }).catch((err) => {
2961
+ deps.progressTracker.update(token, {
2962
+ status: "failed",
2963
+ message: err.message
2964
+ });
2965
+ deps.log("warn", "Rebuild failed via API", { error: err.message });
2966
+ });
2967
+ return { body: { token } };
2968
+ }
2969
+ async function handleDigest(deps, body) {
2970
+ const parsed = DigestBody.safeParse(body);
2971
+ if (!parsed.success) {
2972
+ return { status: 400, body: { error: "validation_failed", issues: parsed.error.issues } };
2973
+ }
2974
+ if (!deps.config.digest.enabled) {
2975
+ return { status: 400, body: { error: "digest_disabled", message: "Digest is not enabled in myco.yaml" } };
2976
+ }
2977
+ const options = parsed.data;
2978
+ const { token, isNew } = deps.progressTracker.create("digest");
2979
+ if (!isNew) {
2980
+ return { body: { token, status: "already_running" } };
2981
+ }
2982
+ runDigest(
2983
+ {
2984
+ vaultDir: deps.vaultDir,
2985
+ config: deps.config,
2986
+ index: deps.index,
2987
+ vectorIndex: deps.vectorIndex ?? void 0,
2988
+ log: deps.log
2989
+ },
2990
+ deps.llmProvider,
2991
+ options ?? void 0
2992
+ ).then((result) => {
2993
+ if (result) {
2994
+ deps.progressTracker.update(token, {
2995
+ status: "completed",
2996
+ percent: PROGRESS_COMPLETE,
2997
+ message: `Tiers: [${result.tiersGenerated.join(", ")}], substrate: ${Object.values(result.substrate).flat().length} notes, ${(result.durationMs / 1e3).toFixed(1)}s`
2998
+ });
2999
+ deps.log("info", "Digest completed via API", {
3000
+ tiers: result.tiersGenerated,
3001
+ duration: result.durationMs
3002
+ });
3003
+ } else {
3004
+ deps.progressTracker.update(token, {
3005
+ status: "completed",
3006
+ percent: PROGRESS_COMPLETE,
3007
+ message: "No substrate found \u2014 nothing to digest"
3008
+ });
3009
+ }
3010
+ }).catch((err) => {
3011
+ deps.progressTracker.update(token, {
3012
+ status: "failed",
3013
+ message: err.message
3014
+ });
3015
+ deps.log("warn", "Digest failed via API", { error: err.message });
3016
+ });
3017
+ return { body: { token } };
3018
+ }
3019
+ async function handleCurate(deps, body, runCuration2) {
3020
+ const parsed = CurateBody.safeParse(body);
3021
+ if (!parsed.success) {
3022
+ return { status: 400, body: { error: "validation_failed", issues: parsed.error.issues } };
3023
+ }
3024
+ const isDryRun = parsed.data?.dry_run ?? false;
3025
+ if (!deps.vectorIndex) {
3026
+ return { status: 400, body: { error: "vector_index_unavailable", message: "Curate requires a working embedding provider" } };
3027
+ }
3028
+ const curationDeps = {
3029
+ vaultDir: deps.vaultDir,
3030
+ config: deps.config,
3031
+ index: deps.index,
3032
+ vectorIndex: deps.vectorIndex,
3033
+ llmProvider: deps.llmProvider,
3034
+ embeddingProvider: deps.embeddingProvider,
3035
+ log: deps.log
3036
+ };
3037
+ if (isDryRun) {
3038
+ try {
3039
+ const result = await runCuration2(curationDeps, true);
3040
+ return { body: { dry_run: true, ...result } };
3041
+ } catch (err) {
3042
+ return { status: 500, body: { error: "curation_failed", message: err.message } };
3043
+ }
3044
+ }
3045
+ const { token, isNew } = deps.progressTracker.create("curate");
3046
+ if (!isNew) {
3047
+ return { body: { token, status: "already_running" } };
3048
+ }
3049
+ runCuration2(curationDeps, false).then((result) => {
3050
+ deps.progressTracker.update(token, {
3051
+ status: "completed",
3052
+ percent: PROGRESS_COMPLETE,
3053
+ message: `Scanned: ${result.scanned}, clusters: ${result.clustersEvaluated}, superseded: ${result.superseded}`
3054
+ });
3055
+ deps.log("info", "Curation completed via API", { ...result });
3056
+ }).catch((err) => {
3057
+ deps.progressTracker.update(token, {
3058
+ status: "failed",
3059
+ message: err.message
3060
+ });
3061
+ deps.log("warn", "Curation failed via API", { error: err.message });
3062
+ });
3063
+ return { body: { token } };
3064
+ }
3065
+
3066
+ // src/daemon/api/models.ts
3067
+ var MODEL_LIST_TIMEOUT_MS = 5e3;
3068
+ var ANTHROPIC_MODELS = [
3069
+ "claude-opus-4-6",
3070
+ "claude-sonnet-4-6",
3071
+ "claude-haiku-4-5-20251001"
3072
+ ];
3073
+ var EMBEDDING_PATTERNS = [
3074
+ "embed",
3075
+ "bge-",
3076
+ "nomic-embed",
3077
+ "e5-",
3078
+ "gte-",
3079
+ "granite-embedding"
3080
+ ];
3081
+ function filterEmbeddingModels(models) {
3082
+ return models.filter((m) => {
3083
+ const name = m.toLowerCase();
3084
+ return EMBEDDING_PATTERNS.some((p) => name.includes(p));
3085
+ });
3086
+ }
3087
+ function filterLlmModels(models) {
3088
+ return models.filter((m) => {
3089
+ const name = m.toLowerCase();
3090
+ return !EMBEDDING_PATTERNS.some((p) => name.includes(p));
3091
+ });
3092
+ }
3093
+ async function handleGetModels(req) {
3094
+ const provider = req.query.provider;
3095
+ const type = req.query.type;
3096
+ if (!provider) {
3097
+ return { status: 400, body: { error: "provider query parameter required" } };
3098
+ }
3099
+ let models = [];
3100
+ try {
3101
+ if (provider === "ollama") {
3102
+ const backend = new OllamaBackend({ base_url: req.query.base_url });
3103
+ models = await backend.listModels(MODEL_LIST_TIMEOUT_MS);
3104
+ } else if (provider === "lm-studio" || provider === "openai-compatible") {
3105
+ const backend = new LmStudioBackend({ base_url: req.query.base_url });
3106
+ models = await backend.listModels(MODEL_LIST_TIMEOUT_MS);
3107
+ } else if (provider === "anthropic") {
3108
+ models = ANTHROPIC_MODELS;
3109
+ }
3110
+ } catch {
3111
+ }
3112
+ if (type === "embedding") {
3113
+ models = filterEmbeddingModels(models);
3114
+ } else if (type === "llm") {
3115
+ models = filterLlmModels(models);
3116
+ }
3117
+ return { body: { provider, models } };
3118
+ }
3119
+
2286
3120
  // src/daemon/main.ts
2287
3121
  var import_yaml = __toESM(require_dist(), 1);
2288
- import fs4 from "fs";
2289
- import path6 from "path";
3122
+ import fs6 from "fs";
3123
+ import path9 from "path";
2290
3124
  function indexAndEmbed(relativePath, noteId, embeddingText, metadata, deps) {
2291
3125
  indexNote(deps.index, deps.vaultDir, relativePath);
2292
3126
  if (deps.vectorIndex && embeddingText) {
@@ -2358,28 +3192,28 @@ ${candidate.content}`,
2358
3192
  }
2359
3193
  }
2360
3194
  function migrateSporeFiles(vaultDir) {
2361
- const sporesDir = path6.join(vaultDir, "spores");
2362
- if (!fs4.existsSync(sporesDir)) return 0;
3195
+ const sporesDir = path9.join(vaultDir, "spores");
3196
+ if (!fs6.existsSync(sporesDir)) return 0;
2363
3197
  let moved = 0;
2364
- const entries = fs4.readdirSync(sporesDir);
3198
+ const entries = fs6.readdirSync(sporesDir);
2365
3199
  for (const entry of entries) {
2366
- const fullPath = path6.join(sporesDir, entry);
3200
+ const fullPath = path9.join(sporesDir, entry);
2367
3201
  if (!entry.endsWith(".md")) continue;
2368
- if (fs4.statSync(fullPath).isDirectory()) continue;
3202
+ if (fs6.statSync(fullPath).isDirectory()) continue;
2369
3203
  try {
2370
- const content = fs4.readFileSync(fullPath, "utf-8");
3204
+ const content = fs6.readFileSync(fullPath, "utf-8");
2371
3205
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
2372
3206
  if (!fmMatch) continue;
2373
3207
  const parsed = import_yaml.default.parse(fmMatch[1]);
2374
3208
  const obsType = parsed.observation_type;
2375
3209
  if (!obsType) continue;
2376
3210
  const normalizedType = obsType.replace(/_/g, "-");
2377
- const targetDir = path6.join(sporesDir, normalizedType);
2378
- fs4.mkdirSync(targetDir, { recursive: true });
2379
- const targetPath = path6.join(targetDir, entry);
2380
- fs4.renameSync(fullPath, targetPath);
3211
+ const targetDir = path9.join(sporesDir, normalizedType);
3212
+ fs6.mkdirSync(targetDir, { recursive: true });
3213
+ const targetPath = path9.join(targetDir, entry);
3214
+ fs6.renameSync(fullPath, targetPath);
2381
3215
  const now = /* @__PURE__ */ new Date();
2382
- fs4.utimesSync(targetPath, now, now);
3216
+ fs6.utimesSync(targetPath, now, now);
2383
3217
  moved++;
2384
3218
  } catch {
2385
3219
  }
@@ -2392,13 +3226,28 @@ async function main() {
2392
3226
  process.stderr.write("Usage: mycod --vault <path>\n");
2393
3227
  process.exit(1);
2394
3228
  }
2395
- const vaultDir = path6.resolve(vaultArg);
3229
+ const vaultDir = path9.resolve(vaultArg);
2396
3230
  const config = loadConfig(vaultDir);
2397
- const logger = new DaemonLogger(path6.join(vaultDir, "logs"), {
3231
+ const logger = new DaemonLogger(path9.join(vaultDir, "logs"), {
2398
3232
  level: config.daemon.log_level,
2399
3233
  maxSize: config.daemon.max_log_size
2400
3234
  });
2401
- const server = new DaemonServer({ vaultDir, logger });
3235
+ let uiDir = null;
3236
+ {
3237
+ let dir = path9.dirname(new URL(import.meta.url).pathname);
3238
+ for (let i = 0; i < 5; i++) {
3239
+ const candidate = path9.join(dir, "dist", "ui");
3240
+ if (fs6.existsSync(path9.join(dir, "package.json")) && fs6.existsSync(candidate)) {
3241
+ uiDir = candidate;
3242
+ break;
3243
+ }
3244
+ dir = path9.dirname(dir);
3245
+ }
3246
+ }
3247
+ if (uiDir) {
3248
+ logger.debug("daemon", "Static UI directory found", { path: uiDir });
3249
+ }
3250
+ const server = new DaemonServer({ vaultDir, logger, uiDir: uiDir ?? void 0 });
2402
3251
  const registry = new SessionRegistry({
2403
3252
  gracePeriod: config.daemon.grace_period,
2404
3253
  onEmpty: async () => {
@@ -2417,14 +3266,14 @@ async function main() {
2417
3266
  let vectorIndex = null;
2418
3267
  try {
2419
3268
  const testEmbed = await embeddingProvider.embed("test");
2420
- vectorIndex = new VectorIndex(path6.join(vaultDir, "vectors.db"), testEmbed.dimensions);
3269
+ vectorIndex = new VectorIndex(path9.join(vaultDir, "vectors.db"), testEmbed.dimensions);
2421
3270
  logger.info("embeddings", "Vector index initialized", { dimensions: testEmbed.dimensions });
2422
3271
  } catch (error) {
2423
3272
  logger.warn("embeddings", "Vector index unavailable", { error: error.message });
2424
3273
  }
2425
3274
  const processor = new BufferProcessor(llmProvider, config.intelligence.llm.context_window, config.capture);
2426
3275
  const vault = new VaultWriter(vaultDir);
2427
- const index = new MycoIndex(path6.join(vaultDir, "index.db"));
3276
+ const index = new MycoIndex(path9.join(vaultDir, "index.db"));
2428
3277
  const lineageGraph = new LineageGraph(vaultDir);
2429
3278
  const transcriptMiner = new TranscriptMiner({
2430
3279
  additionalAdapters: config.capture.transcript_paths.map(
@@ -2433,17 +3282,17 @@ async function main() {
2433
3282
  });
2434
3283
  let activeStopProcessing = null;
2435
3284
  const indexDeps = { index, vaultDir, vectorIndex, embeddingProvider, llmProvider, logger };
2436
- const bufferDir = path6.join(vaultDir, "buffer");
3285
+ const bufferDir = path9.join(vaultDir, "buffer");
2437
3286
  const sessionBuffers = /* @__PURE__ */ new Map();
2438
3287
  const sessionFilePaths = /* @__PURE__ */ new Map();
2439
3288
  const capturedArtifactPaths = /* @__PURE__ */ new Map();
2440
- if (fs4.existsSync(bufferDir)) {
3289
+ if (fs6.existsSync(bufferDir)) {
2441
3290
  const cutoff = Date.now() - STALE_BUFFER_MAX_AGE_MS;
2442
- for (const file of fs4.readdirSync(bufferDir)) {
2443
- const filePath = path6.join(bufferDir, file);
2444
- const stat4 = fs4.statSync(filePath);
3291
+ for (const file of fs6.readdirSync(bufferDir)) {
3292
+ const filePath = path9.join(bufferDir, file);
3293
+ const stat4 = fs6.statSync(filePath);
2445
3294
  if (stat4.mtimeMs < cutoff) {
2446
- fs4.unlinkSync(filePath);
3295
+ fs6.unlinkSync(filePath);
2447
3296
  logger.debug("daemon", "Cleaned stale buffer", { file });
2448
3297
  }
2449
3298
  }
@@ -2480,10 +3329,10 @@ async function main() {
2480
3329
  logger.info("watcher", "Plan detected", { source: event.source, file: event.filePath });
2481
3330
  if (event.filePath) {
2482
3331
  try {
2483
- const content = fs4.readFileSync(event.filePath, "utf-8");
2484
- const relativePath = path6.relative(vaultDir, event.filePath);
2485
- const title = content.match(/^#\s+(.+)$/m)?.[1] ?? path6.basename(event.filePath);
2486
- const planId = `plan-${path6.basename(event.filePath, ".md")}`;
3332
+ const content = fs6.readFileSync(event.filePath, "utf-8");
3333
+ const relativePath = path9.relative(vaultDir, event.filePath);
3334
+ const title = content.match(/^#\s+(.+)$/m)?.[1] ?? path9.basename(event.filePath);
3335
+ const planId = `plan-${path9.basename(event.filePath, ".md")}`;
2487
3336
  indexAndEmbed(
2488
3337
  relativePath,
2489
3338
  planId,
@@ -2522,6 +3371,23 @@ ${content}`,
2522
3371
  config,
2523
3372
  log: (level, message, data) => logger[level]("digest", message, data)
2524
3373
  });
3374
+ if (config.digest.consolidation) {
3375
+ const consolidationEngine = new ConsolidationEngine({
3376
+ vaultDir,
3377
+ index,
3378
+ vectorIndex,
3379
+ embeddingProvider,
3380
+ llmProvider: digestLlm,
3381
+ log: (level, message, data) => logger[level]("consolidation", message, data)
3382
+ });
3383
+ digestEngine.registerPrePass("consolidation", async () => {
3384
+ const result = await consolidationEngine.runPass();
3385
+ if (result && result.consolidated > 0) {
3386
+ logger.info("consolidation", `Consolidation pass: ${result.consolidated} wisdom notes, ${result.sporesSuperseded} spores superseded`);
3387
+ }
3388
+ });
3389
+ logger.info("consolidation", "Auto-consolidation enabled as digest pre-pass");
3390
+ }
2525
3391
  metabolism = new Metabolism(config.digest.metabolism);
2526
3392
  logger.debug("digest", "Firing initial digest cycle (background)");
2527
3393
  digestEngine.runCycle().then((result) => {
@@ -2580,7 +3446,7 @@ ${content}`,
2580
3446
  }
2581
3447
  const captured = capturedArtifactPaths.get(sessionId);
2582
3448
  for (const c of candidates) {
2583
- const absPath = path6.resolve(process.cwd(), c.path);
3449
+ const absPath = path9.resolve(process.cwd(), c.path);
2584
3450
  captured.add(absPath);
2585
3451
  }
2586
3452
  }).catch((err) => logger.warn("processor", "Incremental artifact capture failed", {
@@ -2590,8 +3456,8 @@ ${content}`,
2590
3456
  }
2591
3457
  }
2592
3458
  });
2593
- server.registerRoute("POST", "/sessions/register", async (body) => {
2594
- const { session_id, branch, started_at } = RegisterBody.parse(body);
3459
+ server.registerRoute("POST", "/sessions/register", async (req) => {
3460
+ const { session_id, branch, started_at } = RegisterBody.parse(req.body);
2595
3461
  const resolvedStartedAt = started_at ?? (/* @__PURE__ */ new Date()).toISOString();
2596
3462
  registry.register(session_id, { started_at: resolvedStartedAt, branch });
2597
3463
  server.updateDaemonJsonSessions(registry.sessions);
@@ -2613,21 +3479,21 @@ ${content}`,
2613
3479
  }
2614
3480
  metabolism?.activate();
2615
3481
  logger.info("lifecycle", "Session registered", { session_id, branch });
2616
- return { ok: true, sessions: registry.sessions };
3482
+ return { body: { ok: true, sessions: registry.sessions } };
2617
3483
  });
2618
- server.registerRoute("POST", "/sessions/unregister", async (body) => {
2619
- const { session_id } = UnregisterBody.parse(body);
3484
+ server.registerRoute("POST", "/sessions/unregister", async (req) => {
3485
+ const { session_id } = UnregisterBody.parse(req.body);
2620
3486
  registry.unregister(session_id);
2621
3487
  try {
2622
3488
  const cutoff = Date.now() - STALE_BUFFER_MAX_AGE_MS;
2623
- for (const file of fs4.readdirSync(bufferDir)) {
3489
+ for (const file of fs6.readdirSync(bufferDir)) {
2624
3490
  if (!file.endsWith(".jsonl")) continue;
2625
3491
  const bufferSessionId = file.replace(".jsonl", "");
2626
3492
  if (bufferSessionId === session_id) continue;
2627
- const filePath = path6.join(bufferDir, file);
2628
- const stat4 = fs4.statSync(filePath);
3493
+ const filePath = path9.join(bufferDir, file);
3494
+ const stat4 = fs6.statSync(filePath);
2629
3495
  if (stat4.mtimeMs < cutoff) {
2630
- fs4.unlinkSync(filePath);
3496
+ fs6.unlinkSync(filePath);
2631
3497
  logger.debug("daemon", "Cleaned stale buffer", { file });
2632
3498
  }
2633
3499
  }
@@ -2638,10 +3504,10 @@ ${content}`,
2638
3504
  capturedArtifactPaths.delete(session_id);
2639
3505
  server.updateDaemonJsonSessions(registry.sessions);
2640
3506
  logger.info("lifecycle", "Session unregistered", { session_id });
2641
- return { ok: true, sessions: registry.sessions };
3507
+ return { body: { ok: true, sessions: registry.sessions } };
2642
3508
  });
2643
- server.registerRoute("POST", "/events", async (body) => {
2644
- const validated = EventBody.parse(body);
3509
+ server.registerRoute("POST", "/events", async (req) => {
3510
+ const validated = EventBody.parse(req.body);
2645
3511
  const event = { ...validated, timestamp: validated.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
2646
3512
  logger.debug("hooks", "Event received", { type: event.type, session_id: event.session_id });
2647
3513
  if (!registry.getSession(event.session_id)) {
@@ -2668,10 +3534,10 @@ ${content}`,
2668
3534
  }
2669
3535
  }
2670
3536
  }
2671
- return { ok: true };
3537
+ return { body: { ok: true } };
2672
3538
  });
2673
- server.registerRoute("POST", "/events/stop", async (body) => {
2674
- const { session_id: sessionId, user, transcript_path: hookTranscriptPath, last_assistant_message: lastAssistantMessage } = StopBody.parse(body);
3539
+ server.registerRoute("POST", "/events/stop", async (req) => {
3540
+ const { session_id: sessionId, user, transcript_path: hookTranscriptPath, last_assistant_message: lastAssistantMessage } = StopBody.parse(req.body);
2675
3541
  if (!registry.getSession(sessionId)) {
2676
3542
  registry.register(sessionId, { started_at: (/* @__PURE__ */ new Date()).toISOString() });
2677
3543
  logger.debug("lifecycle", "Auto-registered session from stop event", { session_id: sessionId });
@@ -2685,7 +3551,7 @@ ${content}`,
2685
3551
  activeStopProcessing = prev.then(run).finally(() => {
2686
3552
  activeStopProcessing = null;
2687
3553
  });
2688
- return { ok: true };
3554
+ return { body: { ok: true } };
2689
3555
  });
2690
3556
  async function processStopEvent(sessionId, user, sessionMeta, hookTranscriptPath, lastAssistantMessage) {
2691
3557
  const lastBatch = batchManager.finalize(sessionId);
@@ -2722,15 +3588,15 @@ ${content}`,
2722
3588
  }
2723
3589
  const ended = (/* @__PURE__ */ new Date()).toISOString();
2724
3590
  let started = allTurns.length > 0 && allTurns[0].timestamp ? allTurns[0].timestamp : ended;
2725
- const sessionsDir = path6.join(vaultDir, "sessions");
3591
+ const sessionsDir = path9.join(vaultDir, "sessions");
2726
3592
  const sessionFileName = `${sessionNoteId(sessionId)}.md`;
2727
3593
  let existingContent;
2728
3594
  const duplicatePaths = [];
2729
3595
  try {
2730
- for (const dateDir of fs4.readdirSync(sessionsDir)) {
2731
- const candidate = path6.join(sessionsDir, dateDir, sessionFileName);
3596
+ for (const dateDir of fs6.readdirSync(sessionsDir)) {
3597
+ const candidate = path9.join(sessionsDir, dateDir, sessionFileName);
2732
3598
  try {
2733
- const content = fs4.readFileSync(candidate, "utf-8");
3599
+ const content = fs6.readFileSync(candidate, "utf-8");
2734
3600
  if (!existingContent || content.length > existingContent.length) {
2735
3601
  existingContent = content;
2736
3602
  }
@@ -2793,20 +3659,20 @@ ${conversationText}`;
2793
3659
  }
2794
3660
  const date = started.slice(0, 10);
2795
3661
  const relativePath = sessionRelativePath(sessionId, date);
2796
- const targetFullPath = path6.join(vaultDir, relativePath);
3662
+ const targetFullPath = path9.join(vaultDir, relativePath);
2797
3663
  for (const dup of duplicatePaths) {
2798
3664
  if (dup !== targetFullPath) {
2799
3665
  try {
2800
- fs4.unlinkSync(dup);
3666
+ fs6.unlinkSync(dup);
2801
3667
  logger.debug("lifecycle", "Removed duplicate session file", { path: dup });
2802
3668
  } catch {
2803
3669
  }
2804
3670
  }
2805
3671
  }
2806
- const attachmentsDir = path6.join(vaultDir, "attachments");
3672
+ const attachmentsDir = path9.join(vaultDir, "attachments");
2807
3673
  const hasImages = allTurns.some((t) => t.images?.length);
2808
3674
  if (hasImages) {
2809
- fs4.mkdirSync(attachmentsDir, { recursive: true });
3675
+ fs6.mkdirSync(attachmentsDir, { recursive: true });
2810
3676
  }
2811
3677
  const turnImageNames = /* @__PURE__ */ new Map();
2812
3678
  for (let i = 0; i < allTurns.length; i++) {
@@ -2817,9 +3683,9 @@ ${conversationText}`;
2817
3683
  const img = turn.images[j];
2818
3684
  const ext = extensionForMimeType(img.mediaType);
2819
3685
  const filename = `${bareSessionId(sessionId)}-t${i + 1}-${j + 1}.${ext}`;
2820
- const filePath = path6.join(attachmentsDir, filename);
2821
- if (!fs4.existsSync(filePath)) {
2822
- fs4.writeFileSync(filePath, Buffer.from(img.data, "base64"));
3686
+ const filePath = path9.join(attachmentsDir, filename);
3687
+ if (!fs6.existsSync(filePath)) {
3688
+ fs6.writeFileSync(filePath, Buffer.from(img.data, "base64"));
2823
3689
  logger.debug("processor", "Image saved", { filename, turn: i + 1 });
2824
3690
  }
2825
3691
  names.push(filename);
@@ -2920,8 +3786,8 @@ ${conversationText}`;
2920
3786
  }
2921
3787
  logger.info("processor", "Session note written", { session_id: sessionId, path: relativePath });
2922
3788
  }
2923
- server.registerRoute("POST", "/context", async (body) => {
2924
- const { session_id, branch } = ContextBody.parse(body);
3789
+ server.registerRoute("POST", "/context", async (req) => {
3790
+ const { session_id, branch } = ContextBody.parse(req.body);
2925
3791
  logger.debug("hooks", "Session context query", { session_id });
2926
3792
  try {
2927
3793
  if (config.digest.enabled && config.digest.inject_tier) {
@@ -2932,7 +3798,7 @@ ${conversationText}`;
2932
3798
  Branch:: \`${branch}\``);
2933
3799
  meta.push(`Session:: \`${session_id}\``);
2934
3800
  logger.debug("context", `Injecting digest extract (tier ${result.tier})`, { session_id, fallback: result.fallback });
2935
- return { text: meta.join("\n\n"), source: "digest", tier: result.tier };
3801
+ return { body: { text: meta.join("\n\n"), source: "digest", tier: result.tier } };
2936
3802
  }
2937
3803
  }
2938
3804
  const parts = [];
@@ -2965,22 +3831,22 @@ ${planLines.join("\n")}`);
2965
3831
  }
2966
3832
  parts.push(`Session:: \`${session_id}\``);
2967
3833
  if (parts.length > 0) {
2968
- return { text: parts.join("\n\n") };
3834
+ return { body: { text: parts.join("\n\n") } };
2969
3835
  }
2970
- return { text: "" };
3836
+ return { body: { text: "" } };
2971
3837
  } catch (error) {
2972
3838
  logger.error("daemon", "Session context failed", { error: error.message });
2973
- return { text: "" };
3839
+ return { body: { text: "" } };
2974
3840
  }
2975
3841
  });
2976
3842
  const PromptContextBody = external_exports.object({
2977
3843
  prompt: external_exports.string(),
2978
3844
  session_id: external_exports.string().optional()
2979
3845
  });
2980
- server.registerRoute("POST", "/context/prompt", async (body) => {
2981
- const { prompt, session_id } = PromptContextBody.parse(body);
3846
+ server.registerRoute("POST", "/context/prompt", async (req) => {
3847
+ const { prompt, session_id } = PromptContextBody.parse(req.body);
2982
3848
  if (!prompt || prompt.length < PROMPT_CONTEXT_MIN_LENGTH || !vectorIndex) {
2983
- return { text: "" };
3849
+ return { body: { text: "" } };
2984
3850
  }
2985
3851
  try {
2986
3852
  const emb = await generateEmbedding(embeddingProvider, prompt.slice(0, EMBEDDING_INPUT_LIMIT));
@@ -2989,7 +3855,7 @@ ${planLines.join("\n")}`);
2989
3855
  type: "spore",
2990
3856
  relativeThreshold: PROMPT_CONTEXT_MIN_SIMILARITY
2991
3857
  });
2992
- if (results.length === 0) return { text: "" };
3858
+ if (results.length === 0) return { body: { text: "" } };
2993
3859
  const noteMap = new Map(
2994
3860
  index.queryByIds(results.map((r) => r.id)).map((n) => [n.id, n])
2995
3861
  );
@@ -3002,7 +3868,7 @@ ${planLines.join("\n")}`);
3002
3868
  const obsType = fm.observation_type ?? "note";
3003
3869
  lines.push(`- [${obsType}] ${note.title}: ${note.content.slice(0, CONTENT_SNIPPET_CHARS)} \`[${note.id}]\``);
3004
3870
  }
3005
- if (lines.length === 0) return { text: "" };
3871
+ if (lines.length === 0) return { body: { text: "" } };
3006
3872
  const injected = `**Relevant spores for this task:**
3007
3873
  ${lines.join("\n")}`;
3008
3874
  logger.debug("context", "Prompt context injected", {
@@ -3010,14 +3876,72 @@ ${lines.join("\n")}`;
3010
3876
  spores: lines.length,
3011
3877
  prompt_preview: prompt.slice(0, 50)
3012
3878
  });
3013
- return { text: injected };
3879
+ return { body: { text: injected } };
3014
3880
  } catch (err) {
3015
3881
  logger.debug("context", "Prompt context failed", { error: err.message });
3016
- return { text: "" };
3882
+ return { body: { text: "" } };
3017
3883
  }
3018
3884
  });
3019
- await server.start();
3885
+ const progressTracker = new ProgressTracker();
3886
+ let configHash = computeConfigHash(vaultDir);
3887
+ server.registerRoute("GET", "/api/config", async () => handleGetConfig(vaultDir));
3888
+ server.registerRoute("PUT", "/api/config", async (req) => {
3889
+ const result = await handlePutConfig(vaultDir, req.body);
3890
+ if (!result.status || result.status < 400) {
3891
+ configHash = computeConfigHash(vaultDir);
3892
+ }
3893
+ return result;
3894
+ });
3895
+ server.registerRoute("GET", "/api/stats", async () => handleGetStats({
3896
+ vaultDir,
3897
+ index,
3898
+ vectorIndex,
3899
+ version: server.version,
3900
+ config,
3901
+ configHash,
3902
+ metabolism
3903
+ }));
3904
+ server.registerRoute("GET", "/api/logs", async (req) => handleGetLogs(logger.getRingBuffer(), req.query));
3905
+ server.registerRoute("GET", "/api/models", async (req) => handleGetModels(req));
3906
+ server.registerRoute("POST", "/api/restart", async (req) => handleRestart({ vaultDir, progressTracker }, req.body));
3907
+ server.registerRoute("GET", "/api/progress/:token", async (req) => handleGetProgress(progressTracker, req.params.token));
3908
+ const operationDeps = {
3909
+ vaultDir,
3910
+ config,
3911
+ index,
3912
+ vectorIndex,
3913
+ llmProvider,
3914
+ embeddingProvider,
3915
+ progressTracker,
3916
+ log: (level, message, data) => {
3917
+ const fn = logger[level];
3918
+ if (typeof fn === "function") fn.call(logger, "operations", message, data);
3919
+ }
3920
+ };
3921
+ server.registerRoute("POST", "/api/rebuild", async () => handleRebuild(operationDeps));
3922
+ server.registerRoute("POST", "/api/digest", async (req) => handleDigest(operationDeps, req.body));
3923
+ server.registerRoute("POST", "/api/curate", async (req) => handleCurate(operationDeps, req.body, runCuration));
3924
+ const resolvedPort = await resolvePort(config.daemon.port, vaultDir);
3925
+ if (resolvedPort === 0) {
3926
+ logger.warn("daemon", "All preferred ports occupied, using ephemeral port");
3927
+ }
3928
+ await server.start(resolvedPort);
3020
3929
  logger.info("daemon", "Daemon ready", { vault: vaultDir, port: server.port });
3930
+ if (config.daemon.port === null && resolvedPort !== 0) {
3931
+ try {
3932
+ config.daemon.port = resolvedPort;
3933
+ saveConfig(vaultDir, config);
3934
+ logger.info("daemon", "Persisted auto-derived port to myco.yaml", { port: resolvedPort });
3935
+ } catch (err) {
3936
+ logger.warn("daemon", "Failed to persist auto-derived port", { error: err.message });
3937
+ }
3938
+ }
3939
+ try {
3940
+ const { loadTemplate } = await import("./templates-XPRBOWCE.js");
3941
+ const portalContent = loadTemplate("portal", { port: String(server.port) });
3942
+ fs6.writeFileSync(path9.join(vaultDir, "_portal.md"), portalContent, "utf-8");
3943
+ } catch {
3944
+ }
3021
3945
  if (needsMigrationReindex) {
3022
3946
  setImmediate(() => {
3023
3947
  logger.info("daemon", "Rebuilding index after memories\u2192spores migration (background)");
@@ -3053,4 +3977,4 @@ export {
3053
3977
  chokidar/index.js:
3054
3978
  (*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) *)
3055
3979
  */
3056
- //# sourceMappingURL=main-NFQ4II75.js.map
3980
+ //# sourceMappingURL=main-JEUQS3BY.js.map