@phren/cli 0.0.10 → 0.0.11

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 (63) hide show
  1. package/README.md +2 -8
  2. package/mcp/dist/cli-actions.js +5 -5
  3. package/mcp/dist/cli-config.js +334 -127
  4. package/mcp/dist/cli-govern.js +35 -63
  5. package/mcp/dist/cli-graph.js +3 -2
  6. package/mcp/dist/cli-hooks-globs.js +2 -1
  7. package/mcp/dist/cli-hooks-output.js +3 -3
  8. package/mcp/dist/cli-hooks.js +39 -32
  9. package/mcp/dist/cli-namespaces.js +15 -5
  10. package/mcp/dist/cli-search.js +2 -2
  11. package/mcp/dist/content-archive.js +2 -2
  12. package/mcp/dist/content-dedup.js +9 -9
  13. package/mcp/dist/embedding.js +7 -7
  14. package/mcp/dist/entrypoint.js +129 -102
  15. package/mcp/dist/governance-locks.js +6 -5
  16. package/mcp/dist/governance-policy.js +155 -2
  17. package/mcp/dist/governance-scores.js +3 -3
  18. package/mcp/dist/hooks.js +39 -18
  19. package/mcp/dist/index.js +4 -4
  20. package/mcp/dist/init-config.js +3 -24
  21. package/mcp/dist/init-setup.js +5 -5
  22. package/mcp/dist/init.js +170 -23
  23. package/mcp/dist/link-checksums.js +3 -2
  24. package/mcp/dist/link-context.js +1 -1
  25. package/mcp/dist/link-doctor.js +3 -3
  26. package/mcp/dist/link-skills.js +98 -12
  27. package/mcp/dist/link.js +17 -27
  28. package/mcp/dist/machine-identity.js +1 -9
  29. package/mcp/dist/mcp-config.js +247 -42
  30. package/mcp/dist/mcp-data.js +9 -9
  31. package/mcp/dist/mcp-extract-facts.js +1 -1
  32. package/mcp/dist/mcp-extract.js +2 -2
  33. package/mcp/dist/mcp-finding.js +6 -6
  34. package/mcp/dist/mcp-graph.js +11 -11
  35. package/mcp/dist/mcp-ops.js +18 -18
  36. package/mcp/dist/mcp-search.js +8 -8
  37. package/mcp/dist/memory-ui-page.js +23 -0
  38. package/mcp/dist/memory-ui-scripts.js +210 -27
  39. package/mcp/dist/memory-ui-server.js +115 -3
  40. package/mcp/dist/phren-paths.js +7 -7
  41. package/mcp/dist/profile-store.js +2 -2
  42. package/mcp/dist/project-config.js +63 -16
  43. package/mcp/dist/session-utils.js +3 -2
  44. package/mcp/dist/shared-fragment-graph.js +22 -21
  45. package/mcp/dist/shared-index.js +144 -105
  46. package/mcp/dist/shared-retrieval.js +19 -13
  47. package/mcp/dist/shared-search-fallback.js +13 -13
  48. package/mcp/dist/shared-sqljs.js +3 -2
  49. package/mcp/dist/shared.js +3 -3
  50. package/mcp/dist/shell-input.js +1 -1
  51. package/mcp/dist/shell-state-store.js +1 -1
  52. package/mcp/dist/shell-view.js +3 -2
  53. package/mcp/dist/shell.js +1 -1
  54. package/mcp/dist/skill-files.js +4 -10
  55. package/mcp/dist/skill-registry.js +3 -0
  56. package/mcp/dist/status.js +41 -13
  57. package/mcp/dist/task-hygiene.js +1 -1
  58. package/mcp/dist/telemetry.js +5 -4
  59. package/mcp/dist/update.js +1 -1
  60. package/mcp/dist/utils.js +3 -3
  61. package/package.json +2 -2
  62. package/starter/global/skills/audit.md +106 -0
  63. package/mcp/dist/shared-paths.js +0 -1
@@ -49,8 +49,8 @@ async function _drainEmbQueue() {
49
49
  await cache.load();
50
50
  }
51
51
  catch (err) {
52
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
53
- process.stderr.write(`[phren] embeddingQueue cacheLoad: ${err instanceof Error ? err.message : String(err)}\n`);
52
+ if ((process.env.PHREN_DEBUG))
53
+ process.stderr.write(`[phren] embeddingQueue cacheLoad: ${errorMessage(err)}\n`);
54
54
  }
55
55
  const model = getEmbeddingModel();
56
56
  for (const { docPath, content } of docs) {
@@ -62,16 +62,16 @@ async function _drainEmbQueue() {
62
62
  cache.set(docPath, getEmbeddingModel(), vec);
63
63
  }
64
64
  catch (err) {
65
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
66
- process.stderr.write(`[phren] embeddingQueue embedText: ${err instanceof Error ? err.message : String(err)}\n`);
65
+ if ((process.env.PHREN_DEBUG))
66
+ process.stderr.write(`[phren] embeddingQueue embedText: ${errorMessage(err)}\n`);
67
67
  }
68
68
  }
69
69
  try {
70
70
  await cache.flush();
71
71
  }
72
72
  catch (err) {
73
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
74
- process.stderr.write(`[phren] embeddingQueue cacheFlush: ${err instanceof Error ? err.message : String(err)}\n`);
73
+ if ((process.env.PHREN_DEBUG))
74
+ process.stderr.write(`[phren] embeddingQueue cacheFlush: ${errorMessage(err)}\n`);
75
75
  }
76
76
  }
77
77
  }
@@ -133,8 +133,8 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
133
133
  normalized = fs.realpathSync.native(resolved);
134
134
  }
135
135
  catch (err) {
136
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
137
- process.stderr.write(`[phren] resolveImports realpath: ${err instanceof Error ? err.message : String(err)}\n`);
136
+ if ((process.env.PHREN_DEBUG))
137
+ process.stderr.write(`[phren] resolveImports realpath: ${errorMessage(err)}\n`);
138
138
  return `<!-- @import not found: ${trimmed} -->`;
139
139
  }
140
140
  let normalizedGlobalRoot = globalRoot;
@@ -158,8 +158,8 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
158
158
  return _resolveImportsRecursive(imported, phrenPath, childSeen, depth + 1);
159
159
  }
160
160
  catch (err) {
161
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
162
- process.stderr.write(`[phren] resolveImports fileRead: ${err instanceof Error ? err.message : String(err)}\n`);
161
+ if ((process.env.PHREN_DEBUG))
162
+ process.stderr.write(`[phren] resolveImports fileRead: ${errorMessage(err)}\n`);
163
163
  return `<!-- @import error: ${trimmed} -->`;
164
164
  }
165
165
  });
@@ -180,8 +180,8 @@ function touchSentinel(phrenPath) {
180
180
  fs.writeFileSync(sentinelPath, Date.now().toString());
181
181
  }
182
182
  catch (err) {
183
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
184
- process.stderr.write(`[phren] touchSentinel: ${err instanceof Error ? err.message : String(err)}\n`);
183
+ if ((process.env.PHREN_DEBUG))
184
+ process.stderr.write(`[phren] touchSentinel: ${errorMessage(err)}\n`);
185
185
  }
186
186
  }
187
187
  function computePhrenHash(phrenPath, profile, preGlobbed) {
@@ -197,8 +197,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
197
197
  hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
198
198
  }
199
199
  catch (err) {
200
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
201
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
200
+ if ((process.env.PHREN_DEBUG))
201
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
202
202
  }
203
203
  }
204
204
  for (const configPath of topicConfigEntries) {
@@ -207,8 +207,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
207
207
  hash.update(`topic-config:${configPath}:${stat.mtimeMs}:${stat.size}`);
208
208
  }
209
209
  catch (err) {
210
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
211
- process.stderr.write(`[phren] computePhrenHash topicConfig: ${err instanceof Error ? err.message : String(err)}\n`);
210
+ if ((process.env.PHREN_DEBUG))
211
+ process.stderr.write(`[phren] computePhrenHash topicConfig: ${errorMessage(err)}\n`);
212
212
  }
213
213
  }
214
214
  }
@@ -239,8 +239,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
239
239
  }
240
240
  }
241
241
  catch (err) {
242
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
243
- process.stderr.write(`[phren] computePhrenHash globDir: ${err instanceof Error ? err.message : String(err)}\n`);
242
+ if ((process.env.PHREN_DEBUG))
243
+ process.stderr.write(`[phren] computePhrenHash globDir: ${errorMessage(err)}\n`);
244
244
  }
245
245
  }
246
246
  files.sort();
@@ -250,8 +250,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
250
250
  hash.update(`${f}:${stat.mtimeMs}:${stat.size}`);
251
251
  }
252
252
  catch (err) {
253
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
254
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
253
+ if ((process.env.PHREN_DEBUG))
254
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
255
255
  }
256
256
  }
257
257
  for (const configPath of topicConfigEntries) {
@@ -260,8 +260,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
260
260
  hash.update(`topic-config:${configPath}:${stat.mtimeMs}:${stat.size}`);
261
261
  }
262
262
  catch (err) {
263
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
264
- process.stderr.write(`[phren] computePhrenHash topicConfig: ${err instanceof Error ? err.message : String(err)}\n`);
263
+ if ((process.env.PHREN_DEBUG))
264
+ process.stderr.write(`[phren] computePhrenHash topicConfig: ${errorMessage(err)}\n`);
265
265
  }
266
266
  }
267
267
  }
@@ -271,8 +271,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
271
271
  hash.update(`native:${mem.fullPath}:${stat.mtimeMs}:${stat.size}`);
272
272
  }
273
273
  catch (err) {
274
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
275
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
274
+ if ((process.env.PHREN_DEBUG))
275
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
276
276
  }
277
277
  }
278
278
  // Include global/ files (pulled via @import) so changes invalidate the cache
@@ -286,8 +286,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
286
286
  hash.update(`global:${f}:${stat.mtimeMs}:${stat.size}`);
287
287
  }
288
288
  catch (err) {
289
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
290
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
289
+ if ((process.env.PHREN_DEBUG))
290
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
291
291
  }
292
292
  }
293
293
  }
@@ -299,8 +299,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
299
299
  hash.update(`manual-links:${stat.mtimeMs}:${stat.size}`);
300
300
  }
301
301
  catch (err) {
302
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
303
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
302
+ if ((process.env.PHREN_DEBUG))
303
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
304
304
  }
305
305
  }
306
306
  const indexPolicyPath = path.join(phrenPath, ".governance", "index-policy.json");
@@ -310,8 +310,8 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
310
310
  hash.update(`index-policy-file:${stat.mtimeMs}:${stat.size}`);
311
311
  }
312
312
  catch (err) {
313
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
314
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
313
+ if ((process.env.PHREN_DEBUG))
314
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
315
315
  }
316
316
  }
317
317
  if (profile)
@@ -334,8 +334,8 @@ function loadHashMap(phrenPath) {
334
334
  }
335
335
  }
336
336
  catch (err) {
337
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
338
- process.stderr.write(`[phren] loadHashMap: ${err instanceof Error ? err.message : String(err)}\n`);
337
+ if ((process.env.PHREN_DEBUG))
338
+ process.stderr.write(`[phren] loadHashMap: ${errorMessage(err)}\n`);
339
339
  }
340
340
  return { hashes: {} };
341
341
  }
@@ -355,8 +355,8 @@ function saveHashMap(phrenPath, hashes) {
355
355
  existing = data.hashes;
356
356
  }
357
357
  catch (err) {
358
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
359
- process.stderr.write(`[phren] saveHashMap readExisting: ${err instanceof Error ? err.message : String(err)}\n`);
358
+ if ((process.env.PHREN_DEBUG))
359
+ process.stderr.write(`[phren] saveHashMap readExisting: ${errorMessage(err)}\n`);
360
360
  }
361
361
  const merged = { ...existing, ...hashes };
362
362
  // Remove entries for paths that no longer exist on disk
@@ -494,8 +494,8 @@ function insertFileIntoIndex(db, entry, phrenPath, opts) {
494
494
  return true;
495
495
  }
496
496
  catch (err) {
497
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
498
- process.stderr.write(`[phren] insertFileIntoIndex: ${err instanceof Error ? err.message : String(err)}\n`);
497
+ if ((process.env.PHREN_DEBUG))
498
+ process.stderr.write(`[phren] insertFileIntoIndex: ${errorMessage(err)}\n`);
499
499
  return false;
500
500
  }
501
501
  }
@@ -575,8 +575,8 @@ function deleteEntityLinksForDocPath(db, phrenPath, docPath, fallbackProject, fa
575
575
  db.run("DELETE FROM global_entities WHERE doc_key = ?", [sourceDoc]);
576
576
  }
577
577
  catch (err) {
578
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
579
- process.stderr.write(`[phren] deleteEntityLinksForDocPath globalEntities: ${err instanceof Error ? err.message : String(err)}\n`);
578
+ if ((process.env.PHREN_DEBUG))
579
+ process.stderr.write(`[phren] deleteEntityLinksForDocPath globalEntities: ${errorMessage(err)}\n`);
580
580
  }
581
581
  }
582
582
  /**
@@ -591,15 +591,15 @@ export function updateFileInIndex(db, filePath, phrenPath) {
591
591
  deleteEntityLinksForDocPath(db, phrenPath, resolvedPath);
592
592
  }
593
593
  catch (err) {
594
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
595
- process.stderr.write(`[phren] updateFileInIndex deleteEntityLinks: ${err instanceof Error ? err.message : String(err)}\n`);
594
+ if ((process.env.PHREN_DEBUG))
595
+ process.stderr.write(`[phren] updateFileInIndex deleteEntityLinks: ${errorMessage(err)}\n`);
596
596
  }
597
597
  try {
598
598
  db.run("DELETE FROM docs WHERE path = ?", [resolvedPath]);
599
599
  }
600
600
  catch (err) {
601
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
602
- process.stderr.write(`[phren] updateFileInIndex deleteDocs: ${err instanceof Error ? err.message : String(err)}\n`);
601
+ if ((process.env.PHREN_DEBUG))
602
+ process.stderr.write(`[phren] updateFileInIndex deleteDocs: ${errorMessage(err)}\n`);
603
603
  }
604
604
  // Re-insert if file still exists
605
605
  if (fs.existsSync(resolvedPath)) {
@@ -618,8 +618,8 @@ export function updateFileInIndex(db, filePath, phrenPath) {
618
618
  extractAndLinkFragments(db, content, getEntrySourceDocKey(entry, phrenPath), phrenPath);
619
619
  }
620
620
  catch (err) {
621
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
622
- process.stderr.write(`[phren] updateFileInIndex entityExtraction: ${err instanceof Error ? err.message : String(err)}\n`);
621
+ if ((process.env.PHREN_DEBUG))
622
+ process.stderr.write(`[phren] updateFileInIndex entityExtraction: ${errorMessage(err)}\n`);
623
623
  }
624
624
  }
625
625
  }
@@ -630,8 +630,8 @@ export function updateFileInIndex(db, filePath, phrenPath) {
630
630
  saveHashMap(phrenPath, hashData.hashes);
631
631
  }
632
632
  catch (err) {
633
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
634
- process.stderr.write(`[phren] updateFileInIndex hashMap: ${err instanceof Error ? err.message : String(err)}\n`);
633
+ if ((process.env.PHREN_DEBUG))
634
+ process.stderr.write(`[phren] updateFileInIndex hashMap: ${errorMessage(err)}\n`);
635
635
  }
636
636
  }
637
637
  else {
@@ -644,8 +644,8 @@ export function updateFileInIndex(db, filePath, phrenPath) {
644
644
  await c.flush();
645
645
  }
646
646
  catch (err) {
647
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
648
- process.stderr.write(`[phren] updateFileInIndex embeddingDelete: ${err instanceof Error ? err.message : String(err)}\n`);
647
+ if ((process.env.PHREN_DEBUG))
648
+ process.stderr.write(`[phren] updateFileInIndex embeddingDelete: ${errorMessage(err)}\n`);
649
649
  }
650
650
  })();
651
651
  }
@@ -664,8 +664,8 @@ function readHashSentinel(phrenPath) {
664
664
  }
665
665
  }
666
666
  catch (err) {
667
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
668
- process.stderr.write(`[phren] readHashSentinel: ${err instanceof Error ? err.message : String(err)}\n`);
667
+ if ((process.env.PHREN_DEBUG))
668
+ process.stderr.write(`[phren] readHashSentinel: ${errorMessage(err)}\n`);
669
669
  }
670
670
  return null;
671
671
  }
@@ -675,8 +675,8 @@ function writeHashSentinel(phrenPath, hash) {
675
675
  fs.writeFileSync(sentinelPath, JSON.stringify({ hash, computedAt: Date.now() }));
676
676
  }
677
677
  catch (err) {
678
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
679
- process.stderr.write(`[phren] writeHashSentinel: ${err instanceof Error ? err.message : String(err)}\n`);
678
+ if ((process.env.PHREN_DEBUG))
679
+ process.stderr.write(`[phren] writeHashSentinel: ${errorMessage(err)}\n`);
680
680
  }
681
681
  }
682
682
  function isSentinelFresh(phrenPath, sentinel) {
@@ -693,8 +693,8 @@ function isSentinelFresh(phrenPath, sentinel) {
693
693
  return false;
694
694
  }
695
695
  catch (err) {
696
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
697
- process.stderr.write(`[phren] isSentinelFresh statDir: ${err instanceof Error ? err.message : String(err)}\n`);
696
+ if ((process.env.PHREN_DEBUG))
697
+ process.stderr.write(`[phren] isSentinelFresh statDir: ${errorMessage(err)}\n`);
698
698
  }
699
699
  }
700
700
  return true;
@@ -715,8 +715,8 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
715
715
  return fs.statSync(f.fullPath).mtimeMs > graphMtime;
716
716
  }
717
717
  catch (err) {
718
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
719
- process.stderr.write(`[phren] loadCachedEntityGraph statFile: ${err instanceof Error ? err.message : String(err)}\n`);
718
+ if ((process.env.PHREN_DEBUG))
719
+ process.stderr.write(`[phren] loadCachedEntityGraph statFile: ${errorMessage(err)}\n`);
720
720
  return true;
721
721
  }
722
722
  });
@@ -743,8 +743,8 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
743
743
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [entity, project, docKey]);
744
744
  }
745
745
  catch (err) {
746
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
747
- process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert2: ${err instanceof Error ? err.message : String(err)}\n`);
746
+ if ((process.env.PHREN_DEBUG))
747
+ process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert2: ${errorMessage(err)}\n`);
748
748
  }
749
749
  }
750
750
  }
@@ -762,23 +762,23 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
762
762
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, proj, sourceDoc]);
763
763
  }
764
764
  catch (err) {
765
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
766
- process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert: ${err instanceof Error ? err.message : String(err)}\n`);
765
+ if ((process.env.PHREN_DEBUG))
766
+ process.stderr.write(`[phren] loadCachedEntityGraph globalEntitiesInsert: ${errorMessage(err)}\n`);
767
767
  }
768
768
  }
769
769
  }
770
770
  }
771
771
  catch (err) {
772
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
773
- process.stderr.write(`[phren] entityGraph globalEntitiesRestore: ${err instanceof Error ? err.message : String(err)}\n`);
772
+ if ((process.env.PHREN_DEBUG))
773
+ process.stderr.write(`[phren] entityGraph globalEntitiesRestore: ${errorMessage(err)}\n`);
774
774
  }
775
775
  }
776
776
  return true;
777
777
  }
778
778
  }
779
779
  catch (err) {
780
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
781
- process.stderr.write(`[phren] entityGraph cacheLoad: ${err instanceof Error ? err.message : String(err)}\n`);
780
+ if ((process.env.PHREN_DEBUG))
781
+ process.stderr.write(`[phren] entityGraph cacheLoad: ${errorMessage(err)}\n`);
782
782
  }
783
783
  return false;
784
784
  }
@@ -797,7 +797,7 @@ function mergeManualLinks(db, phrenPath) {
797
797
  // Validate: skip manual links whose sourceDoc no longer exists in the index
798
798
  const docCheck = queryDocBySourceKey(db, phrenPath, link.sourceDoc);
799
799
  if (!docCheck) {
800
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
800
+ if ((process.env.PHREN_DEBUG))
801
801
  process.stderr.write(`[phren] manualLinks: pruning stale link to "${link.sourceDoc}"\n`);
802
802
  pruned = true;
803
803
  continue;
@@ -819,14 +819,14 @@ function mergeManualLinks(db, phrenPath) {
819
819
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [link.entity, projectMatch[1], link.sourceDoc]);
820
820
  }
821
821
  catch (err) {
822
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
823
- process.stderr.write(`[phren] manualLinks globalEntities: ${err instanceof Error ? err.message : String(err)}\n`);
822
+ if ((process.env.PHREN_DEBUG))
823
+ process.stderr.write(`[phren] manualLinks globalEntities: ${errorMessage(err)}\n`);
824
824
  }
825
825
  }
826
826
  }
827
827
  catch (err) {
828
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
829
- process.stderr.write(`[phren] manualLinks entry: ${err instanceof Error ? err.message : String(err)}\n`);
828
+ if ((process.env.PHREN_DEBUG))
829
+ process.stderr.write(`[phren] manualLinks entry: ${errorMessage(err)}\n`);
830
830
  }
831
831
  }
832
832
  // Rewrite manual-links.json if stale entries were pruned
@@ -839,14 +839,14 @@ function mergeManualLinks(db, phrenPath) {
839
839
  });
840
840
  }
841
841
  catch (err) {
842
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
843
- process.stderr.write(`[phren] manualLinks prune write: ${err instanceof Error ? err.message : String(err)}\n`);
842
+ if ((process.env.PHREN_DEBUG))
843
+ process.stderr.write(`[phren] manualLinks prune write: ${errorMessage(err)}\n`);
844
844
  }
845
845
  }
846
846
  }
847
847
  catch (err) {
848
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
849
- process.stderr.write(`[phren] mergeManualLinks: ${err instanceof Error ? err.message : String(err)}\n`);
848
+ if ((process.env.PHREN_DEBUG))
849
+ process.stderr.write(`[phren] mergeManualLinks: ${errorMessage(err)}\n`);
850
850
  }
851
851
  }
852
852
  async function buildIndexImpl(phrenPath, profile) {
@@ -859,21 +859,21 @@ async function buildIndexImpl(phrenPath, profile) {
859
859
  userSuffix = String(os.userInfo().uid);
860
860
  }
861
861
  catch (err) {
862
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
863
- process.stderr.write(`[phren] buildIndexImpl userInfo: ${err instanceof Error ? err.message : String(err)}\n`);
862
+ if ((process.env.PHREN_DEBUG))
863
+ process.stderr.write(`[phren] buildIndexImpl userInfo: ${errorMessage(err)}\n`);
864
864
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
865
865
  }
866
866
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
867
867
  // Fast path: if the sentinel is fresh, skip the expensive glob + hash computation
868
868
  const sentinel = readHashSentinel(phrenPath);
869
869
  let hash;
870
- let globResult;
870
+ let globResult = null;
871
871
  if (sentinel && isSentinelFresh(phrenPath, sentinel)) {
872
872
  hash = sentinel.hash;
873
873
  const cacheFile = path.join(cacheDir, `${hash}.db`);
874
874
  if (fs.existsSync(cacheFile)) {
875
- // Sentinel cache hit — defer full glob until we actually need it
876
- globResult = globAllFiles(phrenPath, profile);
875
+ // Sentinel cache hit — defer glob; only load it if incremental path needs it
876
+ globResult = null;
877
877
  }
878
878
  else {
879
879
  // Cache file was cleaned up, fall through to full computation
@@ -896,6 +896,39 @@ async function buildIndexImpl(phrenPath, profile) {
896
896
  const schemaChanged = savedHashData.version !== INDEX_SCHEMA_VERSION;
897
897
  // Try loading cached DB for incremental update
898
898
  if (!schemaChanged && fs.existsSync(cacheFile)) {
899
+ // Pure sentinel hit: glob was skipped, return DB without computing file diffs
900
+ if (!globResult) {
901
+ try {
902
+ const cached = fs.readFileSync(cacheFile);
903
+ const db = new SQL.Database(cached);
904
+ const docCountResult = db.exec("SELECT COUNT(*) FROM docs");
905
+ const docCount = docCountResult?.[0]?.values?.[0]?.[0] ?? 0;
906
+ if (docCount > 0) {
907
+ try {
908
+ db.run("ALTER TABLE entities ADD COLUMN first_seen_at TEXT");
909
+ }
910
+ catch { /* column already exists */ }
911
+ debugLog(`Loaded FTS index from cache (${hash.slice(0, 8)}) in ${Date.now() - t0}ms [sentinel-hit]`);
912
+ appendIndexEvent(phrenPath, {
913
+ event: "build_index",
914
+ cache: "hit",
915
+ hash: hash.slice(0, 12),
916
+ elapsedMs: Date.now() - t0,
917
+ profile: profile || "",
918
+ });
919
+ return db;
920
+ }
921
+ // Empty DB — fall through to full rebuild with glob
922
+ db.close();
923
+ }
924
+ catch (err) {
925
+ debugLog(`sentinel-hit DB load failed, falling back to full rebuild: ${errorMessage(err)}`);
926
+ }
927
+ // Need glob for rebuild
928
+ globResult = globAllFiles(phrenPath, profile);
929
+ hash = computePhrenHash(phrenPath, profile, globResult.filePaths);
930
+ writeHashSentinel(phrenPath, hash);
931
+ }
899
932
  try {
900
933
  const cached = fs.readFileSync(cacheFile);
901
934
  let db;
@@ -932,8 +965,8 @@ async function buildIndexImpl(phrenPath, profile) {
932
965
  }
933
966
  }
934
967
  catch (err) {
935
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
936
- process.stderr.write(`[phren] buildIndex hashFile: ${err instanceof Error ? err.message : String(err)}\n`);
968
+ if ((process.env.PHREN_DEBUG))
969
+ process.stderr.write(`[phren] buildIndex hashFile: ${errorMessage(err)}\n`);
937
970
  }
938
971
  }
939
972
  // Check for files missing from the index (deleted files)
@@ -968,27 +1001,27 @@ async function buildIndexImpl(phrenPath, profile) {
968
1001
  deleteEntityLinksForDocPath(db, phrenPath, missingPath);
969
1002
  }
970
1003
  catch (err) {
971
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
972
- process.stderr.write(`[phren] buildIndex deleteEntityLinksForMissing: ${err instanceof Error ? err.message : String(err)}\n`);
1004
+ if ((process.env.PHREN_DEBUG))
1005
+ process.stderr.write(`[phren] buildIndex deleteEntityLinksForMissing: ${errorMessage(err)}\n`);
973
1006
  }
974
1007
  try {
975
1008
  db.run("DELETE FROM docs WHERE path = ?", [missingPath]);
976
1009
  }
977
1010
  catch (err) {
978
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
979
- process.stderr.write(`[phren] buildIndex deleteDocForMissing: ${err instanceof Error ? err.message : String(err)}\n`);
1011
+ if ((process.env.PHREN_DEBUG))
1012
+ process.stderr.write(`[phren] buildIndex deleteDocForMissing: ${errorMessage(err)}\n`);
980
1013
  }
981
1014
  }
982
1015
  db.run("COMMIT");
983
1016
  }
984
1017
  catch (err) {
985
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
986
- process.stderr.write(`[phren] buildIndex incrementalDeleteCommit: ${err instanceof Error ? err.message : String(err)}\n`);
1018
+ if ((process.env.PHREN_DEBUG))
1019
+ process.stderr.write(`[phren] buildIndex incrementalDeleteCommit: ${errorMessage(err)}\n`);
987
1020
  try {
988
1021
  db.run("ROLLBACK");
989
1022
  }
990
1023
  catch (e2) {
991
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1024
+ if ((process.env.PHREN_DEBUG))
992
1025
  process.stderr.write(`[phren] buildIndex incrementalDeleteRollback: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
993
1026
  }
994
1027
  }
@@ -1025,7 +1058,7 @@ async function buildIndexImpl(phrenPath, profile) {
1025
1058
  db.run("ROLLBACK");
1026
1059
  }
1027
1060
  catch (e2) {
1028
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1061
+ if ((process.env.PHREN_DEBUG))
1029
1062
  process.stderr.write(`[phren] buildIndex perFileRollback: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
1030
1063
  }
1031
1064
  throw err;
@@ -1040,8 +1073,8 @@ async function buildIndexImpl(phrenPath, profile) {
1040
1073
  fs.writeFileSync(cacheFile, db.export());
1041
1074
  }
1042
1075
  catch (err) {
1043
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1044
- process.stderr.write(`[phren] buildIndex incrementalCacheSave: ${err instanceof Error ? err.message : String(err)}\n`);
1076
+ if ((process.env.PHREN_DEBUG))
1077
+ process.stderr.write(`[phren] buildIndex incrementalCacheSave: ${errorMessage(err)}\n`);
1045
1078
  }
1046
1079
  const incMs = Date.now() - t0;
1047
1080
  debugLog(`Incremental FTS update: ${updatedCount} changed, ${missingFromIndex.length} removed in ${incMs}ms`);
@@ -1069,6 +1102,12 @@ async function buildIndexImpl(phrenPath, profile) {
1069
1102
  }
1070
1103
  }
1071
1104
  // ── Full rebuild ──────────────────────────────────────────────────────────
1105
+ // Ensure glob data is available for full rebuild (may be null from sentinel fast-path fallback)
1106
+ if (!globResult) {
1107
+ globResult = globAllFiles(phrenPath, profile);
1108
+ hash = computePhrenHash(phrenPath, profile, globResult.filePaths);
1109
+ writeHashSentinel(phrenPath, hash);
1110
+ }
1072
1111
  const db = new SQL.Database();
1073
1112
  db.run(`
1074
1113
  CREATE VIRTUAL TABLE docs USING fts5(
@@ -1092,8 +1131,8 @@ async function buildIndexImpl(phrenPath, profile) {
1092
1131
  newHashes[entry.fullPath] = hashFileContent(entry.fullPath);
1093
1132
  }
1094
1133
  catch (err) {
1095
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1096
- process.stderr.write(`[phren] computePhrenHash skip: ${err instanceof Error ? err.message : String(err)}\n`);
1134
+ if ((process.env.PHREN_DEBUG))
1135
+ process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
1097
1136
  }
1098
1137
  if (insertFileIntoIndex(db, entry, phrenPath, { scheduleEmbeddings: true })) {
1099
1138
  fileCount++;
@@ -1120,8 +1159,8 @@ async function buildIndexImpl(phrenPath, profile) {
1120
1159
  fs.writeFileSync(graphPath, JSON.stringify({ entities: entityRows, links: linkRows, globalEntities: globalEntityRows, ts: Date.now() }));
1121
1160
  }
1122
1161
  catch (err) {
1123
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1124
- process.stderr.write(`[phren] buildIndex entityGraphPersist: ${err instanceof Error ? err.message : String(err)}\n`);
1162
+ if ((process.env.PHREN_DEBUG))
1163
+ process.stderr.write(`[phren] buildIndex entityGraphPersist: ${errorMessage(err)}\n`);
1125
1164
  }
1126
1165
  }
1127
1166
  // Always merge manual links (survive rebuild)
@@ -1132,7 +1171,7 @@ async function buildIndexImpl(phrenPath, profile) {
1132
1171
  invalidateDfCache();
1133
1172
  const buildMs = Date.now() - t0;
1134
1173
  debugLog(`Built FTS index: ${fileCount} files from ${getProjectDirs(phrenPath, profile).length} projects in ${buildMs}ms`);
1135
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1174
+ if ((process.env.PHREN_DEBUG))
1136
1175
  console.error(`Indexed ${fileCount} files from ${getProjectDirs(phrenPath, profile).length} projects`);
1137
1176
  appendIndexEvent(phrenPath, {
1138
1177
  event: "build_index",
@@ -1153,8 +1192,8 @@ async function buildIndexImpl(phrenPath, profile) {
1153
1192
  fs.unlinkSync(path.join(cacheDir, f));
1154
1193
  }
1155
1194
  catch (err) {
1156
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1157
- process.stderr.write(`[phren] buildIndex staleCacheCleanup: ${err instanceof Error ? err.message : String(err)}\n`);
1195
+ if ((process.env.PHREN_DEBUG))
1196
+ process.stderr.write(`[phren] buildIndex staleCacheCleanup: ${errorMessage(err)}\n`);
1158
1197
  }
1159
1198
  }
1160
1199
  debugLog(`Saved FTS index cache (${hash.slice(0, 8)}) — total ${Date.now() - t0}ms`);
@@ -1190,8 +1229,8 @@ function isRebuildLockHeld(phrenPath) {
1190
1229
  return Date.now() - stat.mtimeMs <= staleThreshold;
1191
1230
  }
1192
1231
  catch (err) {
1193
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1194
- process.stderr.write(`[phren] isRebuildLockHeld stat: ${err instanceof Error ? err.message : String(err)}\n`);
1232
+ if ((process.env.PHREN_DEBUG))
1233
+ process.stderr.write(`[phren] isRebuildLockHeld stat: ${errorMessage(err)}\n`);
1195
1234
  return false;
1196
1235
  }
1197
1236
  }
@@ -1202,8 +1241,8 @@ async function loadIndexSnapshotOrEmpty(phrenPath, profile) {
1202
1241
  userSuffix = String(os.userInfo().uid);
1203
1242
  }
1204
1243
  catch (err) {
1205
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1206
- process.stderr.write(`[phren] loadIndexSnapshotOrEmpty userInfo: ${err instanceof Error ? err.message : String(err)}\n`);
1244
+ if ((process.env.PHREN_DEBUG))
1245
+ process.stderr.write(`[phren] loadIndexSnapshotOrEmpty userInfo: ${errorMessage(err)}\n`);
1207
1246
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
1208
1247
  }
1209
1248
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
@@ -1303,8 +1342,8 @@ export function findFtsCacheForPath(phrenPath, profile) {
1303
1342
  userSuffix = String(os.userInfo().uid);
1304
1343
  }
1305
1344
  catch (err) {
1306
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1307
- process.stderr.write(`[phren] findFtsCacheForPath userInfo: ${err instanceof Error ? err.message : String(err)}\n`);
1345
+ if ((process.env.PHREN_DEBUG))
1346
+ process.stderr.write(`[phren] findFtsCacheForPath userInfo: ${errorMessage(err)}\n`);
1308
1347
  userSuffix = crypto.createHash("sha1").update(homeDir()).digest("hex").slice(0, 12);
1309
1348
  }
1310
1349
  const cacheDir = path.join(os.tmpdir(), `phren-fts-${userSuffix}`);
@@ -1318,8 +1357,8 @@ export function findFtsCacheForPath(phrenPath, profile) {
1318
1357
  }
1319
1358
  }
1320
1359
  catch (err) {
1321
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
1322
- process.stderr.write(`[phren] findFtsCacheForPath: ${err instanceof Error ? err.message : String(err)}\n`);
1360
+ if ((process.env.PHREN_DEBUG))
1361
+ process.stderr.write(`[phren] findFtsCacheForPath: ${errorMessage(err)}\n`);
1323
1362
  }
1324
1363
  return { exists: false };
1325
1364
  }