@phren/cli 0.0.28 → 0.0.33

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 (153) hide show
  1. package/mcp/dist/capabilities/cli.js +2 -5
  2. package/mcp/dist/capabilities/mcp.js +5 -8
  3. package/mcp/dist/capabilities/types.js +2 -5
  4. package/mcp/dist/capabilities/vscode.js +2 -5
  5. package/mcp/dist/capabilities/web-ui.js +2 -5
  6. package/mcp/dist/{cli-actions.js → cli/actions.js} +25 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
  11. package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
  12. package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
  13. package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
  14. package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
  15. package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
  16. package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +58 -117
  17. package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
  18. package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
  19. package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
  20. package/mcp/dist/{cli-search.js → cli/search.js} +12 -11
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +323 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +337 -0
  24. package/mcp/dist/cli-hooks-stop.js +519 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
  26. package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
  27. package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
  28. package/mcp/dist/{content-learning.js → content/learning.js} +41 -20
  29. package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
  30. package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
  31. package/mcp/dist/{core-project.js → core/project.js} +4 -4
  32. package/mcp/dist/{core-search.js → core/search.js} +2 -2
  33. package/mcp/dist/{data-access.js → data/access.js} +142 -15
  34. package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
  35. package/mcp/dist/embedding.js +9 -14
  36. package/mcp/dist/entrypoint.js +11 -11
  37. package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
  38. package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
  39. package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
  40. package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +13 -7
  41. package/mcp/dist/governance/audit.js +30 -0
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
  46. package/mcp/dist/hooks.js +53 -37
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +54 -30
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +80 -69
  51. package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
  52. package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
  53. package/mcp/dist/{init-shared.js → init/shared.js} +4 -4
  54. package/mcp/dist/init-bootstrap.js +21 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-env.js +114 -0
  57. package/mcp/dist/init-fresh.js +234 -0
  58. package/mcp/dist/init-hooks.js +26 -0
  59. package/mcp/dist/init-mcp.js +65 -0
  60. package/mcp/dist/init-modes.js +135 -0
  61. package/mcp/dist/init-npm.js +37 -0
  62. package/mcp/dist/init-project-local.js +99 -0
  63. package/mcp/dist/init-semantic.js +48 -0
  64. package/mcp/dist/init-types.js +1 -0
  65. package/mcp/dist/init-uninstall.js +504 -0
  66. package/mcp/dist/init-update.js +96 -0
  67. package/mcp/dist/init-walkthrough.js +524 -0
  68. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  69. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  70. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  71. package/mcp/dist/{link.js → link/link.js} +26 -31
  72. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  73. package/mcp/dist/logger.js +11 -3
  74. package/mcp/dist/package-metadata.js +1 -1
  75. package/mcp/dist/phren-art.js +4 -126
  76. package/mcp/dist/phren-paths.js +30 -12
  77. package/mcp/dist/proactivity.js +3 -3
  78. package/mcp/dist/profile-store.js +5 -6
  79. package/mcp/dist/project-config.js +2 -2
  80. package/mcp/dist/project-topics.js +17 -47
  81. package/mcp/dist/provider-adapters.js +1 -1
  82. package/mcp/dist/query-correlation.js +1 -1
  83. package/mcp/dist/runtime-profile.js +1 -1
  84. package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
  85. package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
  86. package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
  87. package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +28 -3
  88. package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
  89. package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +19 -42
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
  93. package/mcp/dist/shared/process.js +24 -0
  94. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
  95. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
  96. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  97. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  98. package/mcp/dist/shared.js +6 -60
  99. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  100. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  101. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  102. package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
  103. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  104. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  105. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  106. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  107. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  108. package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
  109. package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
  110. package/mcp/dist/startup-embedding.js +2 -2
  111. package/mcp/dist/status.js +15 -14
  112. package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
  113. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  114. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
  115. package/mcp/dist/telemetry.js +3 -4
  116. package/mcp/dist/tool-registry.js +29 -17
  117. package/mcp/dist/tools/config.js +530 -0
  118. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  119. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  120. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  121. package/mcp/dist/tools/finding.js +584 -0
  122. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  123. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  124. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  125. package/mcp/dist/tools/ops.js +468 -0
  126. package/mcp/dist/tools/search.js +672 -0
  127. package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
  128. package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
  129. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  130. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  131. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  132. package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
  133. package/mcp/dist/ui/server.js +1024 -0
  134. package/mcp/dist/update.js +2 -2
  135. package/mcp/dist/utils.js +63 -19
  136. package/package.json +2 -2
  137. package/scripts/preuninstall.mjs +31 -0
  138. package/starter/global/CLAUDE.md +3 -2
  139. package/mcp/dist/governance-audit.js +0 -22
  140. package/mcp/dist/mcp-config.js +0 -551
  141. package/mcp/dist/mcp-finding.js +0 -594
  142. package/mcp/dist/mcp-ops.js +0 -363
  143. package/mcp/dist/mcp-search.js +0 -668
  144. package/mcp/dist/memory-ui-server.js +0 -1411
  145. package/mcp/dist/shared-governance.js +0 -4
  146. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  147. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  148. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  149. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  150. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  151. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  152. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  153. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -3,21 +3,22 @@ import * as path from "path";
3
3
  import * as os from "os";
4
4
  import * as crypto from "crypto";
5
5
  import { globSync } from "glob";
6
- import { debugLog, appendIndexEvent, getProjectDirs, collectNativeMemoryFiles, runtimeFile, homeDir, readRootManifest, } from "./shared.js";
7
- import { getIndexPolicy, withFileLock } from "./shared-governance.js";
8
- import { stripTaskDoneSection } from "./shared-content.js";
9
- import { isInactiveFindingLine } from "./finding-lifecycle.js";
10
- import { invalidateDfCache } from "./shared-search-fallback.js";
11
- import { errorMessage } from "./utils.js";
12
- import { beginUserFragmentBuildCache, endUserFragmentBuildCache, extractAndLinkFragments, ensureGlobalEntitiesTable, } from "./shared-fragment-graph.js";
13
- import { bootstrapSqlJs } from "./shared-sqljs.js";
14
- import { getProjectOwnershipMode, getProjectSourcePath, readProjectConfig } from "./project-config.js";
15
- import { buildSourceDocKey, queryDocBySourceKey, queryDocRows, } from "./index-query.js";
16
- import { classifyTopicForText, readProjectTopics, } from "./project-topics.js";
17
- export { porterStem } from "./shared-stemmer.js";
18
- export { cosineFallback } from "./shared-search-fallback.js";
19
- export { queryFragmentLinks, queryFragmentLinks as queryEntityLinks, getFragmentBoostDocs, getFragmentBoostDocs as getEntityBoostDocs, ensureGlobalEntitiesTable, queryCrossProjectFragments, logFragmentMiss, logFragmentMiss as logEntityMiss, extractFragmentNames, extractFragmentNames as extractEntityNames, } from "./shared-fragment-graph.js";
20
- export { buildSourceDocKey, decodeFiniteNumber, decodeStringRow, extractSnippet, getDocSourceKey, normalizeMemoryId, queryDocBySourceKey, queryDocRows, queryRows, rowToDoc, rowToDocWithRowid, } from "./index-query.js";
6
+ import { debugLog, appendIndexEvent, getProjectDirs, collectNativeMemoryFiles, runtimeFile, homeDir, readRootManifest, } from "../shared.js";
7
+ import { getIndexPolicy, withFileLock } from "./governance.js";
8
+ import { stripTaskDoneSection } from "./content.js";
9
+ import { isInactiveFindingLine } from "../finding/lifecycle.js";
10
+ import { invalidateDfCache } from "./search-fallback.js";
11
+ import { errorMessage } from "../utils.js";
12
+ import { logger } from "../logger.js";
13
+ import { beginUserFragmentBuildCache, endUserFragmentBuildCache, extractAndLinkFragments, ensureGlobalEntitiesTable, } from "./fragment-graph.js";
14
+ import { bootstrapSqlJs } from "./sqljs.js";
15
+ import { getProjectOwnershipMode, getProjectSourcePath, readProjectConfig } from "../project-config.js";
16
+ import { buildSourceDocKey, queryDocBySourceKey, queryDocRows, } from "../index-query.js";
17
+ import { classifyTopicForText, readProjectTopics, } from "../project-topics.js";
18
+ export { porterStem } from "./stemmer.js";
19
+ export { cosineFallback } from "./search-fallback.js";
20
+ export { queryFragmentLinks, getFragmentBoostDocs, ensureGlobalEntitiesTable, queryCrossProjectFragments, logFragmentMiss, extractFragmentNames, } from "./fragment-graph.js";
21
+ export { buildSourceDocKey, decodeFiniteNumber, decodeStringRow, extractSnippet, getDocSourceKey, normalizeMemoryId, queryDocBySourceKey, queryDocRows, queryRows, rowToDoc, rowToDocWithRowid, } from "../index-query.js";
21
22
  // ── Async embedding queue ───────────────────────────────────────────────────
22
23
  const _embQueue = new Map();
23
24
  let _embTimer = null;
@@ -32,8 +33,8 @@ function scheduleEmbedding(phrenPath, docPath, content) {
32
33
  async function _drainEmbQueue() {
33
34
  if (_embQueue.size === 0)
34
35
  return;
35
- const { embedText, getEmbeddingModel } = await import("./shared-ollama.js");
36
- const { getEmbeddingCache } = await import("./shared-embedding-cache.js");
36
+ const { embedText, getEmbeddingModel } = await import("./ollama.js");
37
+ const { getEmbeddingCache } = await import("./embedding-cache.js");
37
38
  const entries = [..._embQueue.entries()];
38
39
  _embQueue.clear();
39
40
  // Group by phrenPath so we flush each cache once after all its entries are set.
@@ -49,8 +50,7 @@ async function _drainEmbQueue() {
49
50
  await cache.load();
50
51
  }
51
52
  catch (err) {
52
- if ((process.env.PHREN_DEBUG))
53
- process.stderr.write(`[phren] embeddingQueue cacheLoad: ${errorMessage(err)}\n`);
53
+ logger.debug("embeddingQueue cacheLoad", errorMessage(err));
54
54
  }
55
55
  const model = getEmbeddingModel();
56
56
  for (const { docPath, content } of docs) {
@@ -62,16 +62,14 @@ async function _drainEmbQueue() {
62
62
  cache.set(docPath, getEmbeddingModel(), vec);
63
63
  }
64
64
  catch (err) {
65
- if ((process.env.PHREN_DEBUG))
66
- process.stderr.write(`[phren] embeddingQueue embedText: ${errorMessage(err)}\n`);
65
+ logger.debug("embeddingQueue embedText", errorMessage(err));
67
66
  }
68
67
  }
69
68
  try {
70
69
  await cache.flush();
71
70
  }
72
71
  catch (err) {
73
- if ((process.env.PHREN_DEBUG))
74
- process.stderr.write(`[phren] embeddingQueue cacheFlush: ${errorMessage(err)}\n`);
72
+ logger.debug("embeddingQueue cacheFlush", errorMessage(err));
75
73
  }
76
74
  }
77
75
  }
@@ -133,8 +131,7 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
133
131
  normalized = fs.realpathSync.native(resolved);
134
132
  }
135
133
  catch (err) {
136
- if ((process.env.PHREN_DEBUG))
137
- process.stderr.write(`[phren] resolveImports realpath: ${errorMessage(err)}\n`);
134
+ logger.debug("resolveImports realpath", errorMessage(err));
138
135
  return `<!-- @import not found: ${trimmed} -->`;
139
136
  }
140
137
  let normalizedGlobalRoot = globalRoot;
@@ -158,8 +155,7 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
158
155
  return _resolveImportsRecursive(imported, phrenPath, childSeen, depth + 1);
159
156
  }
160
157
  catch (err) {
161
- if ((process.env.PHREN_DEBUG))
162
- process.stderr.write(`[phren] resolveImports fileRead: ${errorMessage(err)}\n`);
158
+ logger.debug("resolveImports fileRead", errorMessage(err));
163
159
  return `<!-- @import error: ${trimmed} -->`;
164
160
  }
165
161
  });
@@ -169,6 +165,7 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
169
165
  * The import path is resolved relative to the phren root (e.g. `shared/foo.md` -> `~/.phren/global/shared/foo.md`).
170
166
  * Circular imports are detected and skipped. Depth is capped to prevent runaway recursion.
171
167
  */
168
+ /** @internal Exported for tests. */
172
169
  export function resolveImports(content, phrenPath) {
173
170
  return _resolveImportsRecursive(content, phrenPath, new Set(), 0);
174
171
  }
@@ -180,8 +177,7 @@ function touchSentinel(phrenPath) {
180
177
  fs.writeFileSync(sentinelPath, Date.now().toString());
181
178
  }
182
179
  catch (err) {
183
- if ((process.env.PHREN_DEBUG))
184
- process.stderr.write(`[phren] touchSentinel: ${errorMessage(err)}\n`);
180
+ logger.debug("touchSentinel", errorMessage(err));
185
181
  }
186
182
  }
187
183
  function computePhrenHash(phrenPath, profile, preGlobbed) {
@@ -197,8 +193,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
197
193
  hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
198
194
  }
199
195
  catch (err) {
200
- if ((process.env.PHREN_DEBUG))
201
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
196
+ logger.debug("computePhrenHash skip", errorMessage(err));
202
197
  }
203
198
  }
204
199
  for (const configPath of topicConfigEntries) {
@@ -207,8 +202,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
207
202
  hash.update(`topic-config:${configPath}:${stat.mtimeMs}:${stat.size}`);
208
203
  }
209
204
  catch (err) {
210
- if ((process.env.PHREN_DEBUG))
211
- process.stderr.write(`[phren] computePhrenHash topicConfig: ${errorMessage(err)}\n`);
205
+ logger.debug("computePhrenHash topicConfig", errorMessage(err));
212
206
  }
213
207
  }
214
208
  }
@@ -239,8 +233,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
239
233
  }
240
234
  }
241
235
  catch (err) {
242
- if ((process.env.PHREN_DEBUG))
243
- process.stderr.write(`[phren] computePhrenHash globDir: ${errorMessage(err)}\n`);
236
+ logger.debug("computePhrenHash globDir", errorMessage(err));
244
237
  }
245
238
  }
246
239
  files.sort();
@@ -250,8 +243,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
250
243
  hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
251
244
  }
252
245
  catch (err) {
253
- if ((process.env.PHREN_DEBUG))
254
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
246
+ logger.debug("computePhrenHash skip", errorMessage(err));
255
247
  }
256
248
  }
257
249
  for (const configPath of topicConfigEntries) {
@@ -260,8 +252,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
260
252
  hash.update(`topic-config:${configPath}:${stat.mtimeMs}:${stat.size}`);
261
253
  }
262
254
  catch (err) {
263
- if ((process.env.PHREN_DEBUG))
264
- process.stderr.write(`[phren] computePhrenHash topicConfig: ${errorMessage(err)}\n`);
255
+ logger.debug("computePhrenHash topicConfig", errorMessage(err));
265
256
  }
266
257
  }
267
258
  }
@@ -271,8 +262,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
271
262
  hash.update(`native:${mem.fullPath}:${stat.mtimeMs}:${stat.size}`);
272
263
  }
273
264
  catch (err) {
274
- if ((process.env.PHREN_DEBUG))
275
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
265
+ logger.debug("computePhrenHash skip", errorMessage(err));
276
266
  }
277
267
  }
278
268
  // Include global/ files (pulled via @import) so changes invalidate the cache
@@ -286,8 +276,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
286
276
  hash.update(`global:${f}:${stat.mtimeMs}:${stat.size}`);
287
277
  }
288
278
  catch (err) {
289
- if ((process.env.PHREN_DEBUG))
290
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
279
+ logger.debug("computePhrenHash skip", errorMessage(err));
291
280
  }
292
281
  }
293
282
  }
@@ -299,8 +288,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
299
288
  hash.update(`manual-links:${stat.mtimeMs}:${stat.size}`);
300
289
  }
301
290
  catch (err) {
302
- if ((process.env.PHREN_DEBUG))
303
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
291
+ logger.debug("computePhrenHash skip", errorMessage(err));
304
292
  }
305
293
  }
306
294
  const indexPolicyPath = path.join(phrenPath, ".config", "index-policy.json");
@@ -310,8 +298,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
310
298
  hash.update(`index-policy-file:${stat.mtimeMs}:${stat.size}`);
311
299
  }
312
300
  catch (err) {
313
- if ((process.env.PHREN_DEBUG))
314
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
301
+ logger.debug("computePhrenHash skip", errorMessage(err));
315
302
  }
316
303
  }
317
304
  if (profile)
@@ -334,12 +321,11 @@ function loadHashMap(phrenPath) {
334
321
  }
335
322
  }
336
323
  catch (err) {
337
- if ((process.env.PHREN_DEBUG))
338
- process.stderr.write(`[phren] loadHashMap: ${errorMessage(err)}\n`);
324
+ logger.debug("loadHashMap", errorMessage(err));
339
325
  }
340
326
  return { hashes: {} };
341
327
  }
342
- function saveHashMap(phrenPath, hashes) {
328
+ function saveHashMap(phrenPath, hashes, knownPaths) {
343
329
  const runtimeDir = path.join(phrenPath, ".runtime");
344
330
  try {
345
331
  fs.mkdirSync(runtimeDir, { recursive: true });
@@ -355,13 +341,14 @@ function saveHashMap(phrenPath, hashes) {
355
341
  existing = data.hashes;
356
342
  }
357
343
  catch (err) {
358
- if ((process.env.PHREN_DEBUG))
359
- process.stderr.write(`[phren] saveHashMap readExisting: ${errorMessage(err)}\n`);
344
+ logger.debug("saveHashMap readExisting", errorMessage(err));
360
345
  }
361
346
  const merged = { ...existing, ...hashes };
362
- // Remove entries for paths that no longer exist on disk
347
+ // Remove entries for paths that no longer exist. When knownPaths is provided
348
+ // (build passes supply the full file list), use set membership instead of
349
+ // hitting the filesystem — avoids N sync stat calls inside the lock.
363
350
  for (const filePath of Object.keys(merged)) {
364
- if (!fs.existsSync(filePath)) {
351
+ if (knownPaths ? !knownPaths.has(filePath) : !fs.existsSync(filePath)) {
365
352
  delete merged[filePath];
366
353
  }
367
354
  }
@@ -494,8 +481,7 @@ function insertFileIntoIndex(db, entry, phrenPath, opts) {
494
481
  return true;
495
482
  }
496
483
  catch (err) {
497
- if ((process.env.PHREN_DEBUG))
498
- process.stderr.write(`[phren] insertFileIntoIndex: ${errorMessage(err)}\n`);
484
+ logger.debug("insertFileIntoIndex", errorMessage(err));
499
485
  return false;
500
486
  }
501
487
  }
@@ -575,8 +561,7 @@ function deleteEntityLinksForDocPath(db, phrenPath, docPath, fallbackProject, fa
575
561
  db.run("DELETE FROM global_entities WHERE doc_key = ?", [sourceDoc]);
576
562
  }
577
563
  catch (err) {
578
- if ((process.env.PHREN_DEBUG))
579
- process.stderr.write(`[phren] deleteEntityLinksForDocPath globalEntities: ${errorMessage(err)}\n`);
564
+ logger.debug("deleteEntityLinksForDocPath globalEntities", errorMessage(err));
580
565
  }
581
566
  }
582
567
  /**
@@ -591,15 +576,13 @@ export function updateFileInIndex(db, filePath, phrenPath) {
591
576
  deleteEntityLinksForDocPath(db, phrenPath, resolvedPath);
592
577
  }
593
578
  catch (err) {
594
- if ((process.env.PHREN_DEBUG))
595
- process.stderr.write(`[phren] updateFileInIndex deleteEntityLinks: ${errorMessage(err)}\n`);
579
+ logger.debug("updateFileInIndex deleteEntityLinks", errorMessage(err));
596
580
  }
597
581
  try {
598
582
  db.run("DELETE FROM docs WHERE path = ?", [resolvedPath]);
599
583
  }
600
584
  catch (err) {
601
- if ((process.env.PHREN_DEBUG))
602
- process.stderr.write(`[phren] updateFileInIndex deleteDocs: ${errorMessage(err)}\n`);
585
+ logger.debug("updateFileInIndex deleteDocs", errorMessage(err));
603
586
  }
604
587
  // Re-insert if file still exists
605
588
  if (fs.existsSync(resolvedPath)) {
@@ -618,8 +601,7 @@ export function updateFileInIndex(db, filePath, phrenPath) {
618
601
  extractAndLinkFragments(db, content, getEntrySourceDocKey(entry, phrenPath), phrenPath);
619
602
  }
620
603
  catch (err) {
621
- if ((process.env.PHREN_DEBUG))
622
- process.stderr.write(`[phren] updateFileInIndex entityExtraction: ${errorMessage(err)}\n`);
604
+ logger.debug("updateFileInIndex entityExtraction", errorMessage(err));
623
605
  }
624
606
  }
625
607
  }
@@ -630,22 +612,20 @@ export function updateFileInIndex(db, filePath, phrenPath) {
630
612
  saveHashMap(phrenPath, hashData.hashes);
631
613
  }
632
614
  catch (err) {
633
- if ((process.env.PHREN_DEBUG))
634
- process.stderr.write(`[phren] updateFileInIndex hashMap: ${errorMessage(err)}\n`);
615
+ logger.debug("updateFileInIndex hashMap", errorMessage(err));
635
616
  }
636
617
  }
637
618
  else {
638
619
  // Remove stale embedding if file was deleted
639
620
  void (async () => {
640
621
  try {
641
- const { getEmbeddingCache } = await import("./shared-embedding-cache.js");
622
+ const { getEmbeddingCache } = await import("./embedding-cache.js");
642
623
  const c = getEmbeddingCache(phrenPath);
643
624
  c.delete(resolvedPath);
644
625
  await c.flush();
645
626
  }
646
627
  catch (err) {
647
- if ((process.env.PHREN_DEBUG))
648
- process.stderr.write(`[phren] updateFileInIndex embeddingDelete: ${errorMessage(err)}\n`);
628
+ logger.debug("updateFileInIndex embeddingDelete", errorMessage(err));
649
629
  }
650
630
  })();
651
631
  }
@@ -664,8 +644,7 @@ function readHashSentinel(phrenPath) {
664
644
  }
665
645
  }
666
646
  catch (err) {
667
- if ((process.env.PHREN_DEBUG))
668
- process.stderr.write(`[phren] readHashSentinel: ${errorMessage(err)}\n`);
647
+ logger.debug("readHashSentinel", errorMessage(err));
669
648
  }
670
649
  return null;
671
650
  }
@@ -675,8 +654,7 @@ function writeHashSentinel(phrenPath, hash) {
675
654
  fs.writeFileSync(sentinelPath, JSON.stringify({ hash, computedAt: Date.now() }));
676
655
  }
677
656
  catch (err) {
678
- if ((process.env.PHREN_DEBUG))
679
- process.stderr.write(`[phren] writeHashSentinel: ${errorMessage(err)}\n`);
657
+ logger.debug("writeHashSentinel", errorMessage(err));
680
658
  }
681
659
  }
682
660
  function isSentinelFresh(phrenPath, sentinel) {
@@ -693,8 +671,7 @@ function isSentinelFresh(phrenPath, sentinel) {
693
671
  return false;
694
672
  }
695
673
  catch (err) {
696
- if ((process.env.PHREN_DEBUG))
697
- process.stderr.write(`[phren] isSentinelFresh statDir: ${errorMessage(err)}\n`);
674
+ logger.debug("isSentinelFresh statDir", errorMessage(err));
698
675
  }
699
676
  }
700
677
  return true;
@@ -715,8 +692,7 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
715
692
  return fs.statSync(f.fullPath).mtimeMs > graphMtime;
716
693
  }
717
694
  catch (err) {
718
- if ((process.env.PHREN_DEBUG))
719
- process.stderr.write(`[phren] loadCachedEntityGraph statFile: ${errorMessage(err)}\n`);
695
+ logger.debug("loadCachedEntityGraph statFile", errorMessage(err));
720
696
  return true;
721
697
  }
722
698
  });
@@ -743,8 +719,7 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
743
719
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [entity, project, docKey]);
744
720
  }
745
721
  catch (err) {
746
- if ((process.env.PHREN_DEBUG))
747
- process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert2: ${errorMessage(err)}\n`);
722
+ logger.debug("loadCachedEntityGraph globalEntitiesInsert2", errorMessage(err));
748
723
  }
749
724
  }
750
725
  }
@@ -762,23 +737,20 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
762
737
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, proj, sourceDoc]);
763
738
  }
764
739
  catch (err) {
765
- if ((process.env.PHREN_DEBUG))
766
- process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert: ${errorMessage(err)}\n`);
740
+ logger.debug("loadCachedEntityGraph globalEntitiesInsert", errorMessage(err));
767
741
  }
768
742
  }
769
743
  }
770
744
  }
771
745
  catch (err) {
772
- if ((process.env.PHREN_DEBUG))
773
- process.stderr.write(`[phren] entityGraph globalEntitiesRestore: ${errorMessage(err)}\n`);
746
+ logger.debug("entityGraph globalEntitiesRestore", errorMessage(err));
774
747
  }
775
748
  }
776
749
  return true;
777
750
  }
778
751
  }
779
752
  catch (err) {
780
- if ((process.env.PHREN_DEBUG))
781
- process.stderr.write(`[phren] entityGraph cacheLoad: ${errorMessage(err)}\n`);
753
+ logger.debug("entityGraph cacheLoad", errorMessage(err));
782
754
  }
783
755
  return false;
784
756
  }
@@ -797,8 +769,7 @@ function mergeManualLinks(db, phrenPath) {
797
769
  // Validate: skip manual links whose sourceDoc no longer exists in the index
798
770
  const docCheck = queryDocBySourceKey(db, phrenPath, link.sourceDoc);
799
771
  if (!docCheck) {
800
- if ((process.env.PHREN_DEBUG))
801
- process.stderr.write(`[phren] manualLinks: pruning stale link to "${link.sourceDoc}"\n`);
772
+ logger.debug("manualLinks", `pruning stale link to "${link.sourceDoc}"`);
802
773
  pruned = true;
803
774
  continue;
804
775
  }
@@ -819,14 +790,12 @@ function mergeManualLinks(db, phrenPath) {
819
790
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [link.entity, projectMatch[1], link.sourceDoc]);
820
791
  }
821
792
  catch (err) {
822
- if ((process.env.PHREN_DEBUG))
823
- process.stderr.write(`[phren] manualLinks globalEntities: ${errorMessage(err)}\n`);
793
+ logger.debug("manualLinks globalEntities", errorMessage(err));
824
794
  }
825
795
  }
826
796
  }
827
797
  catch (err) {
828
- if ((process.env.PHREN_DEBUG))
829
- process.stderr.write(`[phren] manualLinks entry: ${errorMessage(err)}\n`);
798
+ logger.debug("manualLinks entry", errorMessage(err));
830
799
  }
831
800
  }
832
801
  // Rewrite manual-links.json if stale entries were pruned
@@ -839,19 +808,18 @@ function mergeManualLinks(db, phrenPath) {
839
808
  });
840
809
  }
841
810
  catch (err) {
842
- if ((process.env.PHREN_DEBUG))
843
- process.stderr.write(`[phren] manualLinks prune write: ${errorMessage(err)}\n`);
811
+ logger.debug("manualLinks prune write", errorMessage(err));
844
812
  }
845
813
  }
846
814
  }
847
815
  catch (err) {
848
- if ((process.env.PHREN_DEBUG))
849
- process.stderr.write(`[phren] mergeManualLinks: ${errorMessage(err)}\n`);
816
+ logger.debug("mergeManualLinks", errorMessage(err));
850
817
  }
851
818
  }
852
819
  async function buildIndexImpl(phrenPath, profile) {
853
820
  const t0 = Date.now();
854
- beginUserFragmentBuildCache(phrenPath, getProjectDirs(phrenPath, profile).map(dir => path.basename(dir)));
821
+ const projectDirs = getProjectDirs(phrenPath, profile);
822
+ beginUserFragmentBuildCache(phrenPath, projectDirs.map(dir => path.basename(dir)));
855
823
  try {
856
824
  // ── Cache dir + hash sentinel ─────────────────────────────────────────────
857
825
  let userSuffix;
@@ -859,8 +827,7 @@ async function buildIndexImpl(phrenPath, profile) {
859
827
  userSuffix = String(os.userInfo().uid);
860
828
  }
861
829
  catch (err) {
862
- if ((process.env.PHREN_DEBUG))
863
- process.stderr.write(`[phren] buildIndexImpl userInfo: ${errorMessage(err)}\n`);
830
+ logger.debug("buildIndexImpl userInfo", errorMessage(err));
864
831
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
865
832
  }
866
833
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
@@ -965,8 +932,7 @@ async function buildIndexImpl(phrenPath, profile) {
965
932
  }
966
933
  }
967
934
  catch (err) {
968
- if ((process.env.PHREN_DEBUG))
969
- process.stderr.write(`[phren] buildIndex hashFile: ${errorMessage(err)}\n`);
935
+ logger.debug("buildIndex hashFile", errorMessage(err));
970
936
  }
971
937
  }
972
938
  // Check for files missing from the index (deleted files)
@@ -1001,28 +967,24 @@ async function buildIndexImpl(phrenPath, profile) {
1001
967
  deleteEntityLinksForDocPath(db, phrenPath, missingPath);
1002
968
  }
1003
969
  catch (err) {
1004
- if ((process.env.PHREN_DEBUG))
1005
- process.stderr.write(`[phren] buildIndex deleteEntityLinksForMissing: ${errorMessage(err)}\n`);
970
+ logger.debug("buildIndex deleteEntityLinksForMissing", errorMessage(err));
1006
971
  }
1007
972
  try {
1008
973
  db.run("DELETE FROM docs WHERE path = ?", [missingPath]);
1009
974
  }
1010
975
  catch (err) {
1011
- if ((process.env.PHREN_DEBUG))
1012
- process.stderr.write(`[phren] buildIndex deleteDocForMissing: ${errorMessage(err)}\n`);
976
+ logger.debug("buildIndex deleteDocForMissing", errorMessage(err));
1013
977
  }
1014
978
  }
1015
979
  db.run("COMMIT");
1016
980
  }
1017
981
  catch (err) {
1018
- if ((process.env.PHREN_DEBUG))
1019
- process.stderr.write(`[phren] buildIndex incrementalDeleteCommit: ${errorMessage(err)}\n`);
982
+ logger.debug("buildIndex incrementalDeleteCommit", errorMessage(err));
1020
983
  try {
1021
984
  db.run("ROLLBACK");
1022
985
  }
1023
986
  catch (e2) {
1024
- if ((process.env.PHREN_DEBUG))
1025
- process.stderr.write(`[phren] buildIndex incrementalDeleteRollback: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
987
+ logger.debug("buildIndex incrementalDeleteRollback", e2 instanceof Error ? e2.message : String(e2));
1026
988
  }
1027
989
  }
1028
990
  let updatedCount = 0;
@@ -1058,13 +1020,12 @@ async function buildIndexImpl(phrenPath, profile) {
1058
1020
  db.run("ROLLBACK");
1059
1021
  }
1060
1022
  catch (e2) {
1061
- if ((process.env.PHREN_DEBUG))
1062
- process.stderr.write(`[phren] buildIndex perFileRollback: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
1023
+ logger.debug("buildIndex perFileRollback", e2 instanceof Error ? e2.message : String(e2));
1063
1024
  }
1064
1025
  throw err;
1065
1026
  }
1066
1027
  }
1067
- saveHashMap(phrenPath, currentHashes);
1028
+ saveHashMap(phrenPath, currentHashes, new Set(Object.keys(currentHashes)));
1068
1029
  touchSentinel(phrenPath);
1069
1030
  invalidateDfCache();
1070
1031
  // Save updated cache
@@ -1073,8 +1034,7 @@ async function buildIndexImpl(phrenPath, profile) {
1073
1034
  fs.writeFileSync(cacheFile, db.export());
1074
1035
  }
1075
1036
  catch (err) {
1076
- if ((process.env.PHREN_DEBUG))
1077
- process.stderr.write(`[phren] buildIndex incrementalCacheSave: ${errorMessage(err)}\n`);
1037
+ logger.debug("buildIndex incrementalCacheSave", errorMessage(err));
1078
1038
  }
1079
1039
  const incMs = Date.now() - t0;
1080
1040
  debugLog(`Incremental FTS update: ${updatedCount} changed, ${missingFromIndex.length} removed in ${incMs}ms`);
@@ -1131,8 +1091,7 @@ async function buildIndexImpl(phrenPath, profile) {
1131
1091
  newHashes[entry.fullPath] = hashFileContent(entry.fullPath);
1132
1092
  }
1133
1093
  catch (err) {
1134
- if ((process.env.PHREN_DEBUG))
1135
- process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
1094
+ logger.debug("computePhrenHash skip", errorMessage(err));
1136
1095
  }
1137
1096
  if (insertFileIntoIndex(db, entry, phrenPath, { scheduleEmbeddings: true })) {
1138
1097
  fileCount++;
@@ -1159,26 +1118,25 @@ async function buildIndexImpl(phrenPath, profile) {
1159
1118
  fs.writeFileSync(graphPath, JSON.stringify({ entities: entityRows, links: linkRows, globalEntities: globalEntityRows, ts: Date.now() }));
1160
1119
  }
1161
1120
  catch (err) {
1162
- if ((process.env.PHREN_DEBUG))
1163
- process.stderr.write(`[phren] buildIndex entityGraphPersist: ${errorMessage(err)}\n`);
1121
+ logger.debug("buildIndex entityGraphPersist", errorMessage(err));
1164
1122
  }
1165
1123
  }
1166
1124
  // Always merge manual links (survive rebuild)
1167
1125
  mergeManualLinks(db, phrenPath);
1168
1126
  // ── Finalize: persist hashes, save cache, log ─────────────────────────────
1169
- saveHashMap(phrenPath, newHashes);
1127
+ saveHashMap(phrenPath, newHashes, new Set(Object.keys(newHashes)));
1170
1128
  touchSentinel(phrenPath);
1171
1129
  invalidateDfCache();
1172
1130
  const buildMs = Date.now() - t0;
1173
- debugLog(`Built FTS index: ${fileCount} files from ${getProjectDirs(phrenPath, profile).length} projects in ${buildMs}ms`);
1131
+ debugLog(`Built FTS index: ${fileCount} files from ${projectDirs.length} projects in ${buildMs}ms`);
1174
1132
  if ((process.env.PHREN_DEBUG))
1175
- console.error(`Indexed ${fileCount} files from ${getProjectDirs(phrenPath, profile).length} projects`);
1133
+ console.error(`Indexed ${fileCount} files from ${projectDirs.length} projects`);
1176
1134
  appendIndexEvent(phrenPath, {
1177
1135
  event: "build_index",
1178
1136
  cache: "miss",
1179
1137
  hash: hash.slice(0, 12),
1180
1138
  files: fileCount,
1181
- projects: getProjectDirs(phrenPath, profile).length,
1139
+ projects: projectDirs.length,
1182
1140
  elapsedMs: buildMs,
1183
1141
  profile: profile || "",
1184
1142
  });
@@ -1192,8 +1150,7 @@ async function buildIndexImpl(phrenPath, profile) {
1192
1150
  fs.unlinkSync(path.join(cacheDir, f));
1193
1151
  }
1194
1152
  catch (err) {
1195
- if ((process.env.PHREN_DEBUG))
1196
- process.stderr.write(`[phren] buildIndex staleCacheCleanup: ${errorMessage(err)}\n`);
1153
+ logger.debug("buildIndex staleCacheCleanup", errorMessage(err));
1197
1154
  }
1198
1155
  }
1199
1156
  debugLog(`Saved FTS index cache (${hash.slice(0, 8)}) — total ${Date.now() - t0}ms`);
@@ -1229,8 +1186,7 @@ function isRebuildLockHeld(phrenPath) {
1229
1186
  return Date.now() - stat.mtimeMs <= staleThreshold;
1230
1187
  }
1231
1188
  catch (err) {
1232
- if ((process.env.PHREN_DEBUG))
1233
- process.stderr.write(`[phren] isRebuildLockHeld stat: ${errorMessage(err)}\n`);
1189
+ logger.debug("isRebuildLockHeld stat", errorMessage(err));
1234
1190
  return false;
1235
1191
  }
1236
1192
  }
@@ -1241,8 +1197,7 @@ async function loadIndexSnapshotOrEmpty(phrenPath, profile) {
1241
1197
  userSuffix = String(os.userInfo().uid);
1242
1198
  }
1243
1199
  catch (err) {
1244
- if ((process.env.PHREN_DEBUG))
1245
- process.stderr.write(`[phren] loadIndexSnapshotOrEmpty userInfo: ${errorMessage(err)}\n`);
1200
+ logger.debug("loadIndexSnapshotOrEmpty userInfo", errorMessage(err));
1246
1201
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
1247
1202
  }
1248
1203
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
@@ -1257,7 +1212,27 @@ async function loadIndexSnapshotOrEmpty(phrenPath, profile) {
1257
1212
  debugLog(`Failed to open cached FTS snapshot while rebuild lock held: ${errorMessage(err)}`);
1258
1213
  }
1259
1214
  }
1260
- debugLog("FTS rebuild already in progress; returning empty snapshot");
1215
+ // Before returning empty, try to load any stale-but-usable cache file
1216
+ try {
1217
+ if (fs.existsSync(cacheDir)) {
1218
+ const cacheFiles = fs.readdirSync(cacheDir)
1219
+ .filter(f => f.endsWith(".db"))
1220
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(cacheDir, f)).mtimeMs }))
1221
+ .sort((a, b) => b.mtime - a.mtime);
1222
+ for (const cf of cacheFiles) {
1223
+ try {
1224
+ const staleDb = new SQL.Database(fs.readFileSync(path.join(cacheDir, cf.name)));
1225
+ debugLog(`FTS rebuild in progress; falling back to stale cache: ${cf.name}`);
1226
+ return staleDb;
1227
+ }
1228
+ catch { /* try next */ }
1229
+ }
1230
+ }
1231
+ }
1232
+ catch (err) {
1233
+ debugLog(`Failed to scan stale FTS caches: ${errorMessage(err)}`);
1234
+ }
1235
+ debugLog("FTS rebuild already in progress; no usable cache found, returning empty snapshot");
1261
1236
  return createEmptyIndexDb(SQL);
1262
1237
  }
1263
1238
  // Serialize concurrent in-process buildIndex calls to prevent SQLite corruption
@@ -1342,8 +1317,7 @@ export function findFtsCacheForPath(phrenPath, profile) {
1342
1317
  userSuffix = String(os.userInfo().uid);
1343
1318
  }
1344
1319
  catch (err) {
1345
- if ((process.env.PHREN_DEBUG))
1346
- process.stderr.write(`[phren] findFtsCacheForPath userInfo: ${errorMessage(err)}\n`);
1320
+ logger.debug("findFtsCacheForPath userInfo", errorMessage(err));
1347
1321
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
1348
1322
  }
1349
1323
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
@@ -1357,8 +1331,7 @@ export function findFtsCacheForPath(phrenPath, profile) {
1357
1331
  }
1358
1332
  }
1359
1333
  catch (err) {
1360
- if ((process.env.PHREN_DEBUG))
1361
- process.stderr.write(`[phren] findFtsCacheForPath: ${errorMessage(err)}\n`);
1334
+ logger.debug("findFtsCacheForPath", errorMessage(err));
1362
1335
  }
1363
1336
  return { exists: false };
1364
1337
  }