@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
@@ -12,7 +12,8 @@ import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstall
12
12
  import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isAllowedFilePath, readSyncSnapshot, recentAccepted, recentUsage, } from "./memory-ui-data.js";
13
13
  import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
14
14
  import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
15
- import { getWorkflowPolicy, updateWorkflowPolicy } from "./governance-policy.js";
15
+ import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides } from "./governance-policy.js";
16
+ import { updateProjectConfigOverrides } from "./project-config.js";
16
17
  import { findSkill } from "./skill-registry.js";
17
18
  import { setSkillEnabledAndSync } from "./skill-files.js";
18
19
  import { listAllSessions, getSessionArtifacts } from "./mcp-session.js";
@@ -290,7 +291,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
290
291
  repairPreexistingInstall(phrenPath);
291
292
  }
292
293
  catch (err) {
293
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
294
+ if ((process.env.PHREN_DEBUG))
294
295
  process.stderr.write(`[phren] web-ui repair: ${errorMessage(err)}\n`);
295
296
  }
296
297
  const authToken = opts?.authToken;
@@ -365,7 +366,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
365
366
  }
366
367
  catch (err) {
367
368
  res.writeHead(200, { "content-type": "application/json" });
368
- res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
369
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
369
370
  }
370
371
  });
371
372
  return;
@@ -856,8 +857,13 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
856
857
  try {
857
858
  const prefs = readInstallPreferences(phrenPath);
858
859
  const workflowPolicy = getWorkflowPolicy(phrenPath);
860
+ const retentionPolicy = getRetentionPolicy(phrenPath);
859
861
  const hooksData = getHooksData(phrenPath);
860
862
  const proactivityFindings = prefs.proactivityFindings || prefs.proactivity || "high";
863
+ const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
864
+ const settingsProject = String(qs.project || "");
865
+ const merged = settingsProject && isValidProjectName(settingsProject) ? mergeConfig(phrenPath, settingsProject) : null;
866
+ const overrides = settingsProject && isValidProjectName(settingsProject) ? getProjectConfigOverrides(phrenPath, settingsProject) : null;
861
867
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
862
868
  res.end(JSON.stringify({
863
869
  ok: true,
@@ -871,6 +877,10 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
871
877
  hooksEnabled: hooksData.globalEnabled,
872
878
  mcpEnabled: prefs.mcpEnabled !== false,
873
879
  hookTools: hooksData.tools,
880
+ retentionPolicy,
881
+ workflowPolicy,
882
+ merged,
883
+ overrides,
874
884
  }));
875
885
  }
876
886
  catch (err) {
@@ -975,6 +985,108 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
975
985
  });
976
986
  return;
977
987
  }
988
+ // POST /api/settings/project-overrides — write per-project config overrides
989
+ if (req.method === "POST" && pathname === "/api/settings/project-overrides") {
990
+ void readFormBody(req, res).then((parsed) => {
991
+ if (!parsed)
992
+ return;
993
+ if (!requirePostAuth(req, res, url, parsed, authToken, true))
994
+ return;
995
+ if (!requireCsrf(res, parsed, csrfTokens, true))
996
+ return;
997
+ const project = String(parsed.project || "");
998
+ const field = String(parsed.field || "");
999
+ const value = String(parsed.value || "");
1000
+ const clearField = String(parsed.clear || "") === "true";
1001
+ if (!project || !isValidProjectName(project)) {
1002
+ res.writeHead(400, { "content-type": "application/json" });
1003
+ res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1004
+ return;
1005
+ }
1006
+ const registeredProjects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1007
+ const isRegistered = registeredProjects.includes(project);
1008
+ const registrationWarning = isRegistered ? undefined : `Project '${project}' is not registered in the active profile. Config was saved but it will have no effect until the project is added with 'phren add'.`;
1009
+ const VALID_FIELDS = {
1010
+ findingSensitivity: ["minimal", "conservative", "balanced", "aggressive"],
1011
+ proactivity: ["high", "medium", "low"],
1012
+ proactivityFindings: ["high", "medium", "low"],
1013
+ proactivityTask: ["high", "medium", "low"],
1014
+ taskMode: ["off", "manual", "suggest", "auto"],
1015
+ };
1016
+ const NUMERIC_RETENTION_FIELDS = ["ttlDays", "retentionDays", "autoAcceptThreshold", "minInjectConfidence"];
1017
+ const NUMERIC_WORKFLOW_FIELDS = ["lowConfidenceThreshold"];
1018
+ try {
1019
+ updateProjectConfigOverrides(phrenPath, project, (current) => {
1020
+ const next = { ...current };
1021
+ if (clearField) {
1022
+ if (field in VALID_FIELDS)
1023
+ delete next[field];
1024
+ else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1025
+ if (next.retentionPolicy)
1026
+ delete next.retentionPolicy[field];
1027
+ }
1028
+ else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1029
+ if (next.workflowPolicy)
1030
+ delete next.workflowPolicy[field];
1031
+ }
1032
+ return next;
1033
+ }
1034
+ if (field in VALID_FIELDS) {
1035
+ const allowed = VALID_FIELDS[field];
1036
+ if (!allowed.includes(value))
1037
+ throw new Error(`Invalid value "${value}" for ${field}`);
1038
+ next[field] = value;
1039
+ }
1040
+ else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1041
+ const num = parseFloat(value);
1042
+ if (!Number.isFinite(num) || num < 0)
1043
+ throw new Error(`Invalid numeric value for ${field}`);
1044
+ next.retentionPolicy = { ...next.retentionPolicy, [field]: num };
1045
+ }
1046
+ else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1047
+ const num = parseFloat(value);
1048
+ if (!Number.isFinite(num) || num < 0 || num > 1)
1049
+ throw new Error(`Invalid value for ${field} (must be 0–1)`);
1050
+ next.workflowPolicy = { ...next.workflowPolicy, [field]: num };
1051
+ }
1052
+ else {
1053
+ throw new Error(`Unknown config field: ${field}`);
1054
+ }
1055
+ return next;
1056
+ });
1057
+ const merged = mergeConfig(phrenPath, project);
1058
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1059
+ res.end(JSON.stringify({ ok: true, config: merged }));
1060
+ }
1061
+ catch (err) {
1062
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1063
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1064
+ }
1065
+ });
1066
+ return;
1067
+ }
1068
+ if (req.method === "GET" && pathname === "/api/config") {
1069
+ if (!requireGetAuth(req, res, url, authToken, true))
1070
+ return;
1071
+ const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
1072
+ const project = String(qs.project || "");
1073
+ if (project && !isValidProjectName(project)) {
1074
+ res.writeHead(400, { "content-type": "application/json" });
1075
+ res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1076
+ return;
1077
+ }
1078
+ try {
1079
+ const config = mergeConfig(phrenPath, project || undefined);
1080
+ const projects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1081
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1082
+ res.end(JSON.stringify({ ok: true, config, projects }));
1083
+ }
1084
+ catch (err) {
1085
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1086
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1087
+ }
1088
+ return;
1089
+ }
978
1090
  if (req.method === "GET" && pathname === "/api/csrf-token") {
979
1091
  if (!requireGetAuth(req, res, url, authToken, true))
980
1092
  return;
@@ -74,7 +74,7 @@ export function readRootManifest(phrenPath) {
74
74
  return normalizeManifest(parsed);
75
75
  }
76
76
  catch (err) {
77
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
77
+ if ((process.env.PHREN_DEBUG))
78
78
  process.stderr.write(`[phren] readRootManifest: ${errorMessage(err)}\n`);
79
79
  return null;
80
80
  }
@@ -250,7 +250,7 @@ export function sessionMarker(phrenPath, name) {
250
250
  }
251
251
  // Debug logging is best-effort and only writes when a phren root already exists.
252
252
  export function debugLog(msg) {
253
- if (!(process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
253
+ if (!(process.env.PHREN_DEBUG))
254
254
  return;
255
255
  const phrenPath = findPhrenPath();
256
256
  if (!phrenPath)
@@ -269,7 +269,7 @@ export function appendIndexEvent(phrenPath, event) {
269
269
  fs.appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...event }) + "\n");
270
270
  }
271
271
  catch (err) {
272
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
+ if ((process.env.PHREN_DEBUG))
273
273
  process.stderr.write(`[phren] appendIndexEvent: ${errorMessage(err)}\n`);
274
274
  }
275
275
  }
@@ -301,7 +301,7 @@ export function findProjectNameCaseInsensitive(phrenPath, name) {
301
301
  }
302
302
  }
303
303
  catch (err) {
304
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
304
+ if ((process.env.PHREN_DEBUG))
305
305
  process.stderr.write(`[phren] findProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
306
306
  }
307
307
  return null;
@@ -357,7 +357,7 @@ export function getProjectDirs(phrenPath, profile) {
357
357
  return [...new Set([...listed, ...sharedDirs])];
358
358
  }
359
359
  catch (err) {
360
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
360
+ if ((process.env.PHREN_DEBUG))
361
361
  process.stderr.write(`[phren] getProjectDirs yamlParse: ${errorMessage(err)}\n`);
362
362
  console.error(`${PhrenError.MALFORMED_YAML}: Malformed profile YAML: ${profilePath}`);
363
363
  return [];
@@ -369,7 +369,7 @@ export function getProjectDirs(phrenPath, profile) {
369
369
  .map((entry) => path.join(phrenPath, entry.name));
370
370
  }
371
371
  catch (err) {
372
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
372
+ if ((process.env.PHREN_DEBUG))
373
373
  process.stderr.write(`[phren] getProjectDirs: ${errorMessage(err)}\n`);
374
374
  return [];
375
375
  }
@@ -396,7 +396,7 @@ export function collectNativeMemoryFiles() {
396
396
  }
397
397
  }
398
398
  catch (err) {
399
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
399
+ if ((process.env.PHREN_DEBUG))
400
400
  process.stderr.write(`[phren] collectNativeMemoryFiles: ${errorMessage(err)}\n`);
401
401
  }
402
402
  return results;
@@ -73,7 +73,7 @@ export function listMachines(phrenPath) {
73
73
  return phrenOk(cleaned);
74
74
  }
75
75
  catch (err) {
76
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
76
+ if ((process.env.PHREN_DEBUG))
77
77
  process.stderr.write(`[phren] listMachines yaml parse: ${errorMessage(err)}\n`);
78
78
  return phrenErr(`Could not parse machines.yaml. Check the file for syntax errors or run 'phren doctor --fix'.`, PhrenError.MALFORMED_YAML);
79
79
  }
@@ -139,7 +139,7 @@ export function listProfiles(phrenPath) {
139
139
  profiles.push({ name, file: full, projects });
140
140
  }
141
141
  catch (err) {
142
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
142
+ if ((process.env.PHREN_DEBUG))
143
143
  process.stderr.write(`[phren] listProfiles yamlParse: ${errorMessage(err)}\n`);
144
144
  return phrenErr(`profiles/${file}`, PhrenError.MALFORMED_YAML);
145
145
  }
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import * as yaml from "js-yaml";
5
5
  import { readInstallPreferences } from "./init-preferences.js";
6
6
  import { debugLog } from "./shared.js";
7
- import { errorMessage } from "./utils.js";
7
+ import { errorMessage, safeProjectPath } from "./utils.js";
8
8
  import { withFileLock } from "./governance-locks.js";
9
9
  export const PROJECT_OWNERSHIP_MODES = ["phren-managed", "detached", "repo-managed"];
10
10
  export const PROJECT_HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"];
@@ -24,22 +24,58 @@ export function parseProjectOwnershipMode(raw) {
24
24
  export function projectConfigPath(phrenPath, project) {
25
25
  return path.join(phrenPath, project, "phren.project.yaml");
26
26
  }
27
+ function resolveProjectConfigPath(phrenPath, project) {
28
+ return safeProjectPath(phrenPath, project, "phren.project.yaml");
29
+ }
30
+ function writeProjectConfigFile(configPath, next) {
31
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
32
+ const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
33
+ fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
34
+ fs.renameSync(tmpPath, configPath);
35
+ _projectConfigCache.delete(configPath);
36
+ }
37
+ function normalizeProjectOverrides(raw) {
38
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
39
+ }
40
+ // ── mtime-based config cache ─────────────────────────────────────────────────
41
+ const _projectConfigCache = new Map();
42
+ export function clearProjectConfigCache() {
43
+ _projectConfigCache.clear();
44
+ }
27
45
  export function readProjectConfig(phrenPath, project) {
28
- const configPath = projectConfigPath(phrenPath, project);
29
- if (!fs.existsSync(configPath))
46
+ const configPath = resolveProjectConfigPath(phrenPath, project);
47
+ if (!configPath) {
48
+ debugLog(`readProjectConfig: rejected path for project "${project}"`);
49
+ return {};
50
+ }
51
+ let mtimeMs;
52
+ try {
53
+ mtimeMs = fs.statSync(configPath).mtimeMs;
54
+ }
55
+ catch {
56
+ // File doesn't exist or can't be stat'd
57
+ _projectConfigCache.delete(configPath);
30
58
  return {};
59
+ }
60
+ const cached = _projectConfigCache.get(configPath);
61
+ if (cached && cached.mtimeMs === mtimeMs) {
62
+ return cached.config;
63
+ }
31
64
  try {
32
65
  const parsed = yaml.load(fs.readFileSync(configPath, "utf8"), { schema: yaml.CORE_SCHEMA });
33
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
66
+ const config = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
67
+ _projectConfigCache.set(configPath, { mtimeMs, config });
68
+ return config;
34
69
  }
35
70
  catch (err) {
36
71
  debugLog(`readProjectConfig: failed to parse ${configPath}: ${errorMessage(err)}`);
72
+ _projectConfigCache.delete(configPath);
37
73
  return {};
38
74
  }
39
75
  }
40
76
  export function writeProjectConfig(phrenPath, project, patch) {
41
- const configPath = path.resolve(projectConfigPath(phrenPath, project));
42
- if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
77
+ const configPath = resolveProjectConfigPath(phrenPath, project);
78
+ if (!configPath) {
43
79
  throw new Error(`Project config path escapes phren store`);
44
80
  }
45
81
  return withFileLock(configPath, () => {
@@ -48,10 +84,24 @@ export function writeProjectConfig(phrenPath, project, patch) {
48
84
  ...current,
49
85
  ...patch,
50
86
  };
51
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
52
- const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
53
- fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
54
- fs.renameSync(tmpPath, configPath);
87
+ writeProjectConfigFile(configPath, next);
88
+ return next;
89
+ });
90
+ }
91
+ export function updateProjectConfigOverrides(phrenPath, project, updater) {
92
+ const configPath = resolveProjectConfigPath(phrenPath, project);
93
+ if (!configPath) {
94
+ throw new Error(`Project config path escapes phren store`);
95
+ }
96
+ return withFileLock(configPath, () => {
97
+ const current = readProjectConfig(phrenPath, project);
98
+ const currentConfig = normalizeProjectOverrides(current.config);
99
+ const nextOverrides = normalizeProjectOverrides(updater(currentConfig));
100
+ const next = {
101
+ ...current,
102
+ config: nextOverrides,
103
+ };
104
+ writeProjectConfigFile(configPath, next);
55
105
  return next;
56
106
  });
57
107
  }
@@ -82,8 +132,8 @@ export function isProjectHookEnabled(phrenPath, project, event, config) {
82
132
  }
83
133
  export function writeProjectHookConfig(phrenPath, project, patch) {
84
134
  // Move read+merge inside the lock so concurrent writers cannot clobber each other.
85
- const configPath = path.resolve(projectConfigPath(phrenPath, project));
86
- if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
135
+ const configPath = resolveProjectConfigPath(phrenPath, project);
136
+ if (!configPath) {
87
137
  throw new Error(`Project config path escapes phren store`);
88
138
  }
89
139
  return withFileLock(configPath, () => {
@@ -95,10 +145,7 @@ export function writeProjectHookConfig(phrenPath, project, patch) {
95
145
  ...patch,
96
146
  },
97
147
  };
98
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
99
- const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
100
- fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
101
- fs.renameSync(tmpPath, configPath);
148
+ writeProjectConfigFile(configPath, next);
102
149
  return next;
103
150
  });
104
151
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import { errorMessage } from "./utils.js";
3
4
  /**
4
5
  * Write JSON to a file atomically using temp-file + rename.
5
6
  * Ensures the parent directory exists before writing.
@@ -15,8 +16,8 @@ export function atomicWriteJson(filePath, data) {
15
16
  * Centralises the repeated `if (PHREN_DEBUG) stderr.write(...)` pattern.
16
17
  */
17
18
  export function debugError(scope, err) {
18
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
19
- process.stderr.write(`[phren] ${scope}: ${err instanceof Error ? err.message : String(err)}\n`);
19
+ if ((process.env.PHREN_DEBUG)) {
20
+ process.stderr.write(`[phren] ${scope}: ${errorMessage(err)}\n`);
20
21
  }
21
22
  }
22
23
  /**
@@ -2,6 +2,7 @@ import { decodeStringRow } from "./shared-index.js";
2
2
  import * as fs from "fs";
3
3
  import { runtimeFile } from "./shared.js";
4
4
  import { UNIVERSAL_TECH_TERMS_RE } from "./phren-core.js";
5
+ import { errorMessage } from "./utils.js";
5
6
  export function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
6
7
  /** Escape SQL LIKE wildcard characters so user input is treated literally. */
7
8
  export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
@@ -21,7 +22,7 @@ export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
21
22
  * found" -> suggest adding it).
22
23
  */
23
24
  export function logFragmentMiss(phrenPath, name, context, project) {
24
- if (!process.env.PHREN_DEBUG && !(process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
25
+ if (!process.env.PHREN_DEBUG && !(process.env.PHREN_DEBUG))
25
26
  return;
26
27
  if (!name || name.length <= 2)
27
28
  return;
@@ -115,8 +116,8 @@ function getOrCreateFragment(db, name, type) {
115
116
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [name, type, new Date().toISOString().slice(0, 10)]);
116
117
  }
117
118
  catch (err) {
118
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
119
- process.stderr.write(`[phren] fragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
119
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
120
+ process.stderr.write(`[phren] fragmentInsert: ${errorMessage(err)}\n`);
120
121
  }
121
122
  const result = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [name, type]);
122
123
  if (result?.length && result[0]?.values?.length) {
@@ -138,8 +139,8 @@ export function ensureGlobalEntitiesTable(db) {
138
139
  )`);
139
140
  }
140
141
  catch (err) {
141
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
142
- process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${err instanceof Error ? err.message : String(err)}\n`);
142
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
143
+ process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${errorMessage(err)}\n`);
143
144
  }
144
145
  }
145
146
  /**
@@ -185,8 +186,8 @@ export function beginUserFragmentBuildCache(phrenPath, projects) {
185
186
  _buildUserFragmentCache.set(cacheKey, loaded.fragments);
186
187
  }
187
188
  catch (err) {
188
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
189
- process.stderr.write(`[phren] beginUserFragmentBuildCache: ${err instanceof Error ? err.message : String(err)}\n`);
189
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
190
+ process.stderr.write(`[phren] beginUserFragmentBuildCache: ${errorMessage(err)}\n`);
190
191
  _buildUserFragmentCache.set(cacheKey, []);
191
192
  }
192
193
  }
@@ -224,8 +225,8 @@ function parseUserDefinedFragments(phrenPath, project) {
224
225
  }
225
226
  }
226
227
  catch (err) {
227
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
228
- process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${err instanceof Error ? err.message : String(err)}\n`);
228
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
229
+ process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${errorMessage(err)}\n`);
229
230
  }
230
231
  }
231
232
  const loaded = readUserDefinedFragmentsFromDisk(claudeMdPath);
@@ -235,8 +236,8 @@ function parseUserDefinedFragments(phrenPath, project) {
235
236
  return loaded.fragments;
236
237
  }
237
238
  catch (err) {
238
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
239
- process.stderr.write(`[phren] parseUserDefinedFragments: ${err instanceof Error ? err.message : String(err)}\n`);
239
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
240
+ process.stderr.write(`[phren] parseUserDefinedFragments: ${errorMessage(err)}\n`);
240
241
  return [];
241
242
  }
242
243
  }
@@ -352,8 +353,8 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
352
353
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [docFragmentId, fragmentId, "mentions", sourceDoc]);
353
354
  }
354
355
  catch (err) {
355
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
356
- process.stderr.write(`[phren] fragmentLinksInsert: ${err instanceof Error ? err.message : String(err)}\n`);
356
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
357
+ process.stderr.write(`[phren] fragmentLinksInsert: ${errorMessage(err)}\n`);
357
358
  }
358
359
  // Write to global_entities for cross-project queries
359
360
  if (project) {
@@ -361,8 +362,8 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
361
362
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, project, sourceDoc]);
362
363
  }
363
364
  catch (err) {
364
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
365
- process.stderr.write(`[phren] globalFragmentsInsert: ${err instanceof Error ? err.message : String(err)}\n`);
365
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
366
+ process.stderr.write(`[phren] globalFragmentsInsert: ${errorMessage(err)}\n`);
366
367
  }
367
368
  }
368
369
  }
@@ -390,8 +391,8 @@ export function queryFragmentLinks(db, name) {
390
391
  }
391
392
  }
392
393
  catch (err) {
393
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
394
- process.stderr.write(`[phren] queryFragmentLinks: ${err instanceof Error ? err.message : String(err)}\n`);
394
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
395
+ process.stderr.write(`[phren] queryFragmentLinks: ${errorMessage(err)}\n`);
395
396
  }
396
397
  return { related };
397
398
  }
@@ -426,8 +427,8 @@ export function queryCrossProjectFragments(db, fragmentName, excludeProject) {
426
427
  }
427
428
  }
428
429
  catch (err) {
429
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
430
- process.stderr.write(`[phren] queryCrossProjectFragments: ${err instanceof Error ? err.message : String(err)}\n`);
430
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
431
+ process.stderr.write(`[phren] queryCrossProjectFragments: ${errorMessage(err)}\n`);
431
432
  }
432
433
  return results;
433
434
  }
@@ -447,8 +448,8 @@ export function getFragmentBoostDocs(db, query) {
447
448
  return boostDocs;
448
449
  }
449
450
  catch (err) {
450
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
451
- process.stderr.write(`[phren] getFragmentBoostDocs: ${err instanceof Error ? err.message : String(err)}\n`);
451
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
452
+ process.stderr.write(`[phren] getFragmentBoostDocs: ${errorMessage(err)}\n`);
452
453
  return new Set();
453
454
  }
454
455
  }