@phren/cli 0.0.10 → 0.0.12

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 (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -1,5 +1,4 @@
1
1
  import * as fs from "fs";
2
- import { statSync } from "fs";
3
2
  import * as path from "path";
4
3
  import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "./shared.js";
5
4
  import { errorMessage, runGitOrThrow } from "./utils.js";
@@ -170,8 +169,16 @@ function resolveCitationFile(citation) {
170
169
  }
171
170
  // Session-scoped caches for git I/O during citation validation.
172
171
  // Keyed by "repo\0commit" and "repo\0file\0line" respectively.
172
+ const MAX_CACHE_ENTRIES = 500;
173
173
  const commitExistsCache = new Map();
174
174
  const blameCache = new Map();
175
+ function evictOldest(cache) {
176
+ if (cache.size <= MAX_CACHE_ENTRIES)
177
+ return;
178
+ const first = cache.keys().next().value;
179
+ if (first !== undefined)
180
+ cache.delete(first);
181
+ }
175
182
  function commitExists(repoPath, commit) {
176
183
  const key = `${repoPath}\0${commit}`;
177
184
  const cached = commitExistsCache.get(key);
@@ -180,11 +187,13 @@ function commitExists(repoPath, commit) {
180
187
  try {
181
188
  runGitOrThrow(repoPath, ["cat-file", "-e", `${commit}^{commit}`], EXEC_TIMEOUT_QUICK_MS);
182
189
  commitExistsCache.set(key, true);
190
+ evictOldest(commitExistsCache);
183
191
  return true;
184
192
  }
185
193
  catch (err) {
186
194
  debugLog(`commitExists: commit ${commit} not found in ${repoPath}: ${errorMessage(err)}`);
187
195
  commitExistsCache.set(key, false);
196
+ evictOldest(commitExistsCache);
188
197
  return false;
189
198
  }
190
199
  }
@@ -197,11 +206,13 @@ function cachedBlame(repoPath, relFile, line) {
197
206
  const out = runGitOrThrow(repoPath, ["blame", "-L", `${line},${line}`, "--porcelain", relFile], 10_000).trim();
198
207
  const first = out.split("\n")[0] || "";
199
208
  blameCache.set(key, first);
209
+ evictOldest(blameCache);
200
210
  return first;
201
211
  }
202
212
  catch (err) {
203
213
  debugLog(`cachedBlame: git blame failed for ${relFile}:${line}: ${errorMessage(err)}`);
204
214
  blameCache.set(key, false);
215
+ evictOldest(blameCache);
205
216
  return false;
206
217
  }
207
218
  }
@@ -274,7 +285,7 @@ function confidenceForAge(ageDays, decay) {
274
285
  }
275
286
  function wasFileModifiedAfter(filePath, findingDate) {
276
287
  try {
277
- const stat = statSync(filePath);
288
+ const stat = fs.statSync(filePath);
278
289
  const fileModified = stat.mtime.toISOString().slice(0, 10);
279
290
  return fileModified > findingDate;
280
291
  }
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
  import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "./shared.js";
5
- import { isFeatureEnabled, safeProjectPath } from "./utils.js";
5
+ import { isFeatureEnabled, safeProjectPath, errorMessage } from "./utils.js";
6
6
  import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "./phren-core.js";
7
7
  import { isInactiveFindingLine } from "./finding-lifecycle.js";
8
8
  // ── LLM provider abstraction ────────────────────────────────────────────────
@@ -50,8 +50,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
50
50
  }
51
51
  }
52
52
  catch (err) {
53
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
54
- process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
53
+ if ((process.env.PHREN_DEBUG))
54
+ process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
55
55
  }
56
56
  const result = await compute();
57
57
  // Persist result
@@ -61,8 +61,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
61
61
  persistCache(cachePath, cache);
62
62
  }
63
63
  catch (err) {
64
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
65
- process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
64
+ if ((process.env.PHREN_DEBUG))
65
+ process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
66
66
  }
67
67
  return result;
68
68
  }
@@ -562,8 +562,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
562
562
  return { name: e.name, mtime: fs.statSync(fp).mtimeMs, fp };
563
563
  }
564
564
  catch (err) {
565
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
566
- process.stderr.write(`[phren] crossProjectScan stat: ${err instanceof Error ? err.message : String(err)}\n`);
565
+ if ((process.env.PHREN_DEBUG))
566
+ process.stderr.write(`[phren] crossProjectScan stat: ${errorMessage(err)}\n`);
567
567
  return null;
568
568
  }
569
569
  })
@@ -577,8 +577,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
577
577
  }
578
578
  }
579
579
  catch (err) {
580
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
581
- process.stderr.write(`[phren] crossProjectScan: ${err instanceof Error ? err.message : String(err)}\n`);
580
+ if ((process.env.PHREN_DEBUG))
581
+ process.stderr.write(`[phren] crossProjectScan: ${errorMessage(err)}\n`);
582
582
  }
583
583
  const annotations = [];
584
584
  const deadline = Date.now() + CONFLICT_CHECK_TOTAL_TIMEOUT_MS;
@@ -378,7 +378,7 @@ export function addFindingToFile(phrenPath, project, learning, citationInput, op
378
378
  if (!result.ok)
379
379
  return result;
380
380
  if (typeof result.data === "string")
381
- return phrenOk(result.data);
381
+ return phrenOk({ message: result.data, status: "skipped" });
382
382
  appendAuditLog(phrenPath, "add_finding", `project=${project}${result.data.created ? " created=true" : ""} citation_commit=${result.data.citation.commit ?? "none"} citation_file=${result.data.citation.file ?? "none"}`);
383
383
  const cap = Number.parseInt((process.env.PHREN_FINDINGS_CAP) || "", 10) || DEFAULT_FINDINGS_CAP;
384
384
  const activeCount = countActiveFindings(result.data.content);
@@ -390,10 +390,12 @@ export function addFindingToFile(phrenPath, project, learning, citationInput, op
390
390
  }
391
391
  if (result.data.created) {
392
392
  const createdMsg = `Created FINDINGS.md for "${project}" and added insight.`;
393
- return phrenOk(result.data.tagWarning ? `${createdMsg} Warning: ${result.data.tagWarning}` : createdMsg);
393
+ const message = result.data.tagWarning ? `${createdMsg} Warning: ${result.data.tagWarning}` : createdMsg;
394
+ return phrenOk({ message, status: "created" });
394
395
  }
395
396
  const addedMsg = `Added finding to ${project}: ${result.data.bullet} (with citation metadata)`;
396
- return phrenOk(result.data.tagWarning ? `${addedMsg} Warning: ${result.data.tagWarning}` : addedMsg);
397
+ const message = result.data.tagWarning ? `${addedMsg} Warning: ${result.data.tagWarning}` : addedMsg;
398
+ return phrenOk({ message, status: "added" });
397
399
  }
398
400
  export function addFindingsToFile(phrenPath, project, learnings, opts) {
399
401
  if (!isValidProjectName(project))
@@ -402,8 +404,8 @@ export function addFindingsToFile(phrenPath, project, learnings, opts) {
402
404
  if (!resolvedDir)
403
405
  return phrenErr(`Invalid project name: "${project}".`, PhrenError.INVALID_PROJECT_NAME);
404
406
  const learningsPath = path.join(resolvedDir, "FINDINGS.md");
405
- const today = new Date().toISOString().slice(0, 10);
406
407
  const nowIso = new Date().toISOString();
408
+ const today = nowIso.slice(0, 10);
407
409
  const resolvedCitationInputResult = resolveFindingCitationInput(phrenPath, project);
408
410
  if (!resolvedCitationInputResult.ok)
409
411
  return resolvedCitationInputResult;
@@ -172,6 +172,16 @@ export function stripAllMetadata(line) {
172
172
  export function stripComments(text) {
173
173
  return text.replace(METADATA_REGEX.anyComment, "").trim();
174
174
  }
175
+ /** Normalize finding text for comparison: strips bullet prefix, HTML comments, confidence tags, normalizes whitespace, lowercases. */
176
+ export function normalizeFindingText(raw) {
177
+ return raw
178
+ .replace(/^-\s+/, "")
179
+ .replace(/<!--.*?-->/g, " ")
180
+ .replace(/\[confidence\s+[01](?:\.\d+)?\]/gi, " ")
181
+ .replace(/\s+/g, " ")
182
+ .trim()
183
+ .toLowerCase();
184
+ }
175
185
  // ---------------------------------------------------------------------------
176
186
  // Add helpers — append metadata comments to a line
177
187
  // ---------------------------------------------------------------------------
@@ -18,7 +18,7 @@ export function addFinding(phrenPath, project, finding, citation, findingType) {
18
18
  if (!result.ok) {
19
19
  return { ok: false, message: result.error };
20
20
  }
21
- return { ok: true, message: result.data, data: { project, finding: taggedFinding } };
21
+ return { ok: true, message: result.data.message, data: { project, finding: taggedFinding } };
22
22
  }
23
23
  /**
24
24
  * Remove a finding by partial text match.
@@ -2,38 +2,17 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as yaml from "js-yaml";
4
4
  import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, isRecord, } from "./shared.js";
5
- import { normalizeQueueEntryText, withFileLock as withFileLockRaw, } from "./shared-governance.js";
5
+ import { normalizeQueueEntryText, } from "./shared-governance.js";
6
6
  import { addFindingToFile, } from "./shared-content.js";
7
- import { isValidProjectName, queueFilePath, safeProjectPath, errorMessage } from "./utils.js";
7
+ import { isValidProjectName, queueFilePath, safeProjectPath } from "./utils.js";
8
8
  import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
9
9
  import { parseFindingLifecycle, } from "./finding-lifecycle.js";
10
- import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
10
+ import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, normalizeFindingText, } from "./content-metadata.js";
11
+ import { withSafeLock, ensureProject } from "./shared-data-utils.js";
11
12
  export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, removeTasks, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
12
13
  export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
13
- export { loadShellState, readRuntimeHealth, resetShellState, saveShellState, } from "./shell-state-store.js";
14
- function withSafeLock(filePath, fn) {
15
- try {
16
- return withFileLockRaw(filePath, fn);
17
- }
18
- catch (err) {
19
- const msg = errorMessage(err);
20
- if (msg.includes("could not acquire lock")) {
21
- return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
22
- }
23
- throw err;
24
- }
25
- }
26
- function ensureProject(phrenPath, project) {
27
- if (!isValidProjectName(project))
28
- return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
29
- const dir = safeProjectPath(phrenPath, project);
30
- if (!dir)
31
- return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
32
- if (!fs.existsSync(dir)) {
33
- return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
34
- }
35
- return phrenOk(dir);
36
- }
14
+ export { loadShellState, resetShellState, saveShellState, } from "./shell-state-store.js";
15
+ export { getRuntimeHealth as readRuntimeHealth } from "./shared-governance.js";
37
16
  function extractDateHeading(line) {
38
17
  const heading = line.match(/^##\s+(.+)$/);
39
18
  if (!heading)
@@ -79,8 +58,8 @@ function findMatchingFindingBullet(bulletLines, needle, match) {
79
58
  const fidMatch = /^[a-z0-9]{8}$/.test(fidNeedle)
80
59
  ? bulletLines.filter(({ line }) => new RegExp(`<!--\\s*fid:${fidNeedle}\\s*-->`).test(line))
81
60
  : [];
82
- const exactMatches = bulletLines.filter(({ line }) => line.replace(/^-\s+/, "").replace(/<!--.*?-->/g, "").trim().toLowerCase() === needle);
83
- const partialMatches = bulletLines.filter(({ line }) => line.toLowerCase().includes(needle));
61
+ const exactMatches = bulletLines.filter(({ line }) => normalizeFindingText(line) === needle);
62
+ const partialMatches = bulletLines.filter(({ line }) => normalizeFindingText(line).includes(needle));
84
63
  if (fidMatch.length === 1)
85
64
  return { kind: "found", idx: fidMatch[0].i };
86
65
  if (exactMatches.length === 1)
@@ -276,7 +255,7 @@ export function removeFinding(phrenPath, project, match) {
276
255
  return phrenErr(`No FINDINGS.md file found for "${project}". Add a finding first with add_finding or :find add.`, PhrenError.FILE_NOT_FOUND);
277
256
  return withSafeLock(filePath, () => {
278
257
  const lines = fs.readFileSync(filePath, "utf8").split("\n");
279
- const needle = match.trim().toLowerCase();
258
+ const needle = normalizeFindingText(match);
280
259
  const bulletLines = collectFindingBulletLines(lines);
281
260
  const activeMatch = findMatchingFindingBullet(bulletLines.filter(({ archived }) => !archived), needle, match);
282
261
  if (activeMatch.kind === "ambiguous") {
@@ -313,7 +292,7 @@ export function editFinding(phrenPath, project, oldText, newText) {
313
292
  return phrenErr(`No FINDINGS.md file found for "${project}".`, PhrenError.FILE_NOT_FOUND);
314
293
  return withSafeLock(findingsPath, () => {
315
294
  const lines = fs.readFileSync(findingsPath, "utf8").split("\n");
316
- const needle = oldText.trim().toLowerCase();
295
+ const needle = normalizeFindingText(oldText);
317
296
  const bulletLines = collectFindingBulletLines(lines);
318
297
  const activeMatch = findMatchingFindingBullet(bulletLines.filter(({ archived }) => !archived), needle, oldText);
319
298
  if (activeMatch.kind === "ambiguous") {
@@ -2,21 +2,9 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { randomBytes, randomUUID } from "crypto";
4
4
  import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, } from "./shared.js";
5
- import { withFileLock as withFileLockRaw } from "./shared-governance.js";
6
5
  import { validateTaskFormat } from "./shared-content.js";
7
- import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
8
- function withSafeLock(filePath, fn) {
9
- try {
10
- return withFileLockRaw(filePath, fn);
11
- }
12
- catch (err) {
13
- const msg = errorMessage(err);
14
- if (msg.includes("could not acquire lock")) {
15
- return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
16
- }
17
- throw err;
18
- }
19
- }
6
+ import { safeProjectPath } from "./utils.js";
7
+ import { withSafeLock, ensureProject } from "./shared-data-utils.js";
20
8
  const ACTIVE_HEADINGS = new Set(["active", "in progress", "in-progress", "current", "wip"]);
21
9
  const QUEUE_HEADINGS = new Set(["queue", "queued", "task", "todo", "upcoming", "next"]);
22
10
  const DONE_HEADINGS = new Set(["done", "completed", "finished", "archived"]);
@@ -108,17 +96,6 @@ function parseContinuation(lines, idx) {
108
96
  }
109
97
  return { context, githubIssue, githubUrl, linesToSkip };
110
98
  }
111
- function ensureProject(phrenPath, project) {
112
- if (!isValidProjectName(project))
113
- return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
114
- const dir = safeProjectPath(phrenPath, project);
115
- if (!dir)
116
- return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
117
- if (!fs.existsSync(dir)) {
118
- return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
119
- }
120
- return phrenOk(dir);
121
- }
122
99
  /** Pattern that matches the task metadata comment embedded in task item lines.
123
100
  * Format: <!-- bid:HASH [rank:N] [lastActivity:ISO] -->
124
101
  */
@@ -808,7 +785,9 @@ export function tidyDoneTasks(phrenPath, project, keep = 30, dryRun) {
808
785
  const lines = archived.map((item) => `- [x] ${item.line}${item.context ? `\n Context: ${item.context}` : ""}`);
809
786
  const block = `## ${stamp}\n\n${lines.join("\n")}\n\n`;
810
787
  const prior = fs.existsSync(archiveFile) ? fs.readFileSync(archiveFile, "utf8") : `# ${project} tasks archive\n\n`;
811
- fs.writeFileSync(archiveFile, prior + block);
788
+ const tmpPath = `${archiveFile}.tmp-${randomUUID()}`;
789
+ fs.writeFileSync(tmpPath, prior + block);
790
+ fs.renameSync(tmpPath, archiveFile);
812
791
  writeTaskDoc(parsed.data);
813
792
  return phrenOk(`Tidied ${project}: archived ${archived.length} done item(s), kept ${safeKeep}.`);
814
793
  });
@@ -79,7 +79,7 @@ async function openCacheDb(phrenPath) {
79
79
  db?.close();
80
80
  }
81
81
  catch (e2) {
82
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
82
+ if ((process.env.PHREN_DEBUG))
83
83
  process.stderr.write(`[phren] embedding openCacheDb dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
84
84
  }
85
85
  throw err;
@@ -126,13 +126,13 @@ function persistDb(phrenPath, db) {
126
126
  }
127
127
  }
128
128
  catch (err) {
129
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
130
- process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${err instanceof Error ? err.message : String(err)}\n`);
129
+ if ((process.env.PHREN_DEBUG))
130
+ process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${errorMessage(err)}\n`);
131
131
  try {
132
132
  onDisk?.close();
133
133
  }
134
134
  catch (e2) {
135
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
135
+ if ((process.env.PHREN_DEBUG))
136
136
  process.stderr.write(`[phren] embedding persistDb onDiskClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
137
137
  }
138
138
  onDisk = null;
@@ -149,7 +149,7 @@ function persistDb(phrenPath, db) {
149
149
  onDisk.close();
150
150
  }
151
151
  catch (e2) {
152
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
152
+ if ((process.env.PHREN_DEBUG))
153
153
  process.stderr.write(`[phren] embedding persistDb onDiskCloseFinally: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
154
154
  }
155
155
  }
@@ -269,7 +269,7 @@ export async function getCachedEmbedding(phrenPath, text, apiKey, model) {
269
269
  db?.close();
270
270
  }
271
271
  catch (e2) {
272
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
+ if ((process.env.PHREN_DEBUG))
273
273
  process.stderr.write(`[phren] embedding getCachedEmbedding dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
274
274
  }
275
275
  }
@@ -320,7 +320,7 @@ export async function getCachedEmbeddings(phrenPath, texts, apiKey, model) {
320
320
  db?.close();
321
321
  }
322
322
  catch (e2) {
323
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
323
+ if ((process.env.PHREN_DEBUG))
324
324
  process.stderr.write(`[phren] embedding getCachedEmbeddings dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
325
325
  }
326
326
  }
@@ -340,5 +340,4 @@ export function cosineSimilarity(a, b) {
340
340
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
341
341
  return denom === 0 ? 0 : dot / denom;
342
342
  }
343
- // Export helpers for testing
344
343
  export { encodeEmbedding, decodeEmbedding, openCacheDb };