@phren/cli 0.0.9 → 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.
- package/README.md +2 -8
- package/mcp/dist/cli-actions.js +5 -5
- package/mcp/dist/cli-config.js +334 -127
- package/mcp/dist/cli-govern.js +140 -3
- package/mcp/dist/cli-graph.js +3 -2
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +3 -3
- package/mcp/dist/cli-hooks.js +41 -34
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/content-archive.js +2 -2
- package/mcp/dist/content-citation.js +12 -22
- package/mcp/dist/content-dedup.js +9 -9
- package/mcp/dist/data-access.js +1 -1
- package/mcp/dist/data-tasks.js +23 -0
- package/mcp/dist/embedding.js +7 -7
- package/mcp/dist/entrypoint.js +129 -102
- package/mcp/dist/governance-locks.js +6 -5
- package/mcp/dist/governance-policy.js +155 -2
- package/mcp/dist/governance-scores.js +3 -3
- package/mcp/dist/hooks.js +39 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +3 -24
- package/mcp/dist/init-setup.js +5 -5
- package/mcp/dist/init.js +170 -23
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +1 -1
- package/mcp/dist/link-doctor.js +3 -3
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +17 -27
- package/mcp/dist/machine-identity.js +1 -9
- package/mcp/dist/mcp-config.js +247 -42
- package/mcp/dist/mcp-data.js +9 -9
- package/mcp/dist/mcp-extract-facts.js +1 -1
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +6 -6
- package/mcp/dist/mcp-graph.js +11 -11
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +8 -8
- package/mcp/dist/mcp-tasks.js +21 -1
- package/mcp/dist/memory-ui-page.js +23 -0
- package/mcp/dist/memory-ui-scripts.js +210 -27
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-paths.js +7 -7
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +63 -16
- package/mcp/dist/session-utils.js +3 -2
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +22 -56
- package/mcp/dist/shared-search-fallback.js +13 -13
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +3 -3
- package/mcp/dist/shell-input.js +1 -1
- package/mcp/dist/shell-state-store.js +1 -1
- package/mcp/dist/shell-view.js +3 -2
- package/mcp/dist/shell.js +1 -1
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +41 -13
- package/mcp/dist/task-hygiene.js +1 -1
- package/mcp/dist/telemetry.js +5 -4
- package/mcp/dist/update.js +1 -1
- package/mcp/dist/utils.js +3 -3
- package/package.json +2 -2
- package/starter/global/skills/audit.md +106 -0
- 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
|
|
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:
|
|
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;
|
package/mcp/dist/phren-paths.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
29
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
42
|
-
if (!configPath
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
86
|
-
if (!configPath
|
|
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
|
-
|
|
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
|
|
19
|
-
process.stderr.write(`[phren] ${scope}: ${
|
|
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
|
|
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
|
|
119
|
-
process.stderr.write(`[phren] fragmentInsert: ${
|
|
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
|
|
142
|
-
process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${
|
|
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
|
|
189
|
-
process.stderr.write(`[phren] beginUserFragmentBuildCache: ${
|
|
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
|
|
228
|
-
process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${
|
|
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
|
|
239
|
-
process.stderr.write(`[phren] parseUserDefinedFragments: ${
|
|
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
|
|
356
|
-
process.stderr.write(`[phren] fragmentLinksInsert: ${
|
|
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
|
|
365
|
-
process.stderr.write(`[phren] globalFragmentsInsert: ${
|
|
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
|
|
394
|
-
process.stderr.write(`[phren] queryFragmentLinks: ${
|
|
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
|
|
430
|
-
process.stderr.write(`[phren] queryCrossProjectFragments: ${
|
|
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
|
|
451
|
-
process.stderr.write(`[phren] getFragmentBoostDocs: ${
|
|
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
|
}
|