@phren/cli 0.0.28 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/capabilities/cli.js +2 -5
- package/mcp/dist/capabilities/mcp.js +5 -8
- package/mcp/dist/capabilities/types.js +2 -5
- package/mcp/dist/capabilities/vscode.js +2 -5
- package/mcp/dist/capabilities/web-ui.js +2 -5
- package/mcp/dist/{cli-actions.js → cli/actions.js} +25 -21
- package/mcp/dist/{cli.js → cli/cli.js} +13 -13
- package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
- package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
- package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
- package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
- package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
- package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
- package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
- package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
- package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +58 -117
- package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
- package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
- package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
- package/mcp/dist/{cli-search.js → cli/search.js} +12 -11
- package/mcp/dist/cli-hooks-git.js +243 -0
- package/mcp/dist/cli-hooks-prompt.js +323 -0
- package/mcp/dist/cli-hooks-session-handlers.js +337 -0
- package/mcp/dist/cli-hooks-stop.js +519 -0
- package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
- package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
- package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
- package/mcp/dist/{content-learning.js → content/learning.js} +41 -20
- package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
- package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
- package/mcp/dist/{core-project.js → core/project.js} +4 -4
- package/mcp/dist/{core-search.js → core/search.js} +2 -2
- package/mcp/dist/{data-access.js → data/access.js} +142 -15
- package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
- package/mcp/dist/embedding.js +9 -14
- package/mcp/dist/entrypoint.js +11 -11
- package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
- package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
- package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
- package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +13 -7
- package/mcp/dist/governance/audit.js +30 -0
- package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
- package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
- package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
- package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
- package/mcp/dist/hooks.js +53 -37
- package/mcp/dist/index-query.js +4 -1
- package/mcp/dist/index.js +54 -30
- package/mcp/dist/{init-config.js → init/config.js} +6 -6
- package/mcp/dist/{init.js → init/init.js} +80 -69
- package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
- package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
- package/mcp/dist/{init-shared.js → init/shared.js} +4 -4
- package/mcp/dist/init-bootstrap.js +21 -0
- package/mcp/dist/init-detect.js +38 -0
- package/mcp/dist/init-env.js +114 -0
- package/mcp/dist/init-fresh.js +234 -0
- package/mcp/dist/init-hooks.js +26 -0
- package/mcp/dist/init-mcp.js +65 -0
- package/mcp/dist/init-modes.js +135 -0
- package/mcp/dist/init-npm.js +37 -0
- package/mcp/dist/init-project-local.js +99 -0
- package/mcp/dist/init-semantic.js +48 -0
- package/mcp/dist/init-types.js +1 -0
- package/mcp/dist/init-uninstall.js +504 -0
- package/mcp/dist/init-update.js +96 -0
- package/mcp/dist/init-walkthrough.js +524 -0
- package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
- package/mcp/dist/{link-context.js → link/context.js} +4 -4
- package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
- package/mcp/dist/{link.js → link/link.js} +26 -31
- package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
- package/mcp/dist/logger.js +11 -3
- package/mcp/dist/package-metadata.js +1 -1
- package/mcp/dist/phren-art.js +4 -126
- package/mcp/dist/phren-paths.js +30 -12
- package/mcp/dist/proactivity.js +3 -3
- package/mcp/dist/profile-store.js +5 -6
- package/mcp/dist/project-config.js +2 -2
- package/mcp/dist/project-topics.js +17 -47
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +1 -1
- package/mcp/dist/runtime-profile.js +1 -1
- package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
- package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
- package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
- package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +28 -3
- package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
- package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +19 -42
- package/mcp/dist/shared/governance.js +4 -0
- package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
- package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
- package/mcp/dist/shared/process.js +24 -0
- package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
- package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
- package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
- package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
- package/mcp/dist/shared.js +6 -60
- package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
- package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
- package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
- package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
- package/mcp/dist/{shell.js → shell/shell.js} +11 -11
- package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
- package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
- package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
- package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
- package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
- package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
- package/mcp/dist/startup-embedding.js +2 -2
- package/mcp/dist/status.js +15 -14
- package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
- package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
- package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
- package/mcp/dist/telemetry.js +3 -4
- package/mcp/dist/tool-registry.js +29 -17
- package/mcp/dist/tools/config.js +530 -0
- package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
- package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
- package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
- package/mcp/dist/tools/finding.js +584 -0
- package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
- package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
- package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
- package/mcp/dist/tools/ops.js +468 -0
- package/mcp/dist/tools/search.js +672 -0
- package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
- package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
- package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
- package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
- package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
- package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
- package/mcp/dist/ui/server.js +1024 -0
- package/mcp/dist/update.js +2 -2
- package/mcp/dist/utils.js +63 -19
- package/package.json +2 -2
- package/scripts/preuninstall.mjs +31 -0
- package/starter/global/CLAUDE.md +3 -2
- package/mcp/dist/governance-audit.js +0 -22
- package/mcp/dist/mcp-config.js +0 -551
- package/mcp/dist/mcp-finding.js +0 -594
- package/mcp/dist/mcp-ops.js +0 -363
- package/mcp/dist/mcp-search.js +0 -668
- package/mcp/dist/memory-ui-server.js +0 -1411
- package/mcp/dist/shared-governance.js +0 -4
- /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
- /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
- /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
- /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
- /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
- /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
- /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
- /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { PhrenError, phrenErr, phrenOk } from "
|
|
3
|
+
import { PhrenError, phrenErr, phrenOk } from "../phren-core.js";
|
|
4
4
|
// Phren lifecycle comment prefix. No backward compat.
|
|
5
5
|
const LIFECYCLE_PREFIX = "phren";
|
|
6
|
-
import { withFileLock } from "
|
|
7
|
-
import { isValidProjectName, safeProjectPath } from "
|
|
8
|
-
import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "
|
|
6
|
+
import { withFileLock } from "../shared/governance.js";
|
|
7
|
+
import { isValidProjectName, safeProjectPath } from "../utils.js";
|
|
8
|
+
import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "../content/metadata.js";
|
|
9
9
|
export const FINDING_TYPE_DECAY = {
|
|
10
10
|
'pattern': { maxAgeDays: 365, decayMultiplier: 1.0 }, // Slow decay, long-lived
|
|
11
11
|
'decision': { maxAgeDays: Infinity, decayMultiplier: 1.0 }, // Never decays
|
|
@@ -219,7 +219,9 @@ export function supersedeFinding(phrenPath, project, findingText, supersededBy)
|
|
|
219
219
|
const today = new Date().toISOString().slice(0, 10);
|
|
220
220
|
lines[matched.data.index] = applyLifecycle(lines[matched.data.index], { status: "superseded", status_updated: today, status_reason: "superseded_by", status_ref: ref }, today, { supersededBy: ref });
|
|
221
221
|
const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
222
|
-
|
|
222
|
+
const tmpPath = findingsPath + ".tmp." + process.pid;
|
|
223
|
+
fs.writeFileSync(tmpPath, normalized);
|
|
224
|
+
fs.renameSync(tmpPath, findingsPath);
|
|
223
225
|
return phrenOk({ finding: matched.data.text, superseded_by: ref, status: "superseded" });
|
|
224
226
|
});
|
|
225
227
|
}
|
|
@@ -239,7 +241,9 @@ export function retractFinding(phrenPath, project, findingText, reason) {
|
|
|
239
241
|
const today = new Date().toISOString().slice(0, 10);
|
|
240
242
|
lines[matched.data.index] = applyLifecycle(lines[matched.data.index], { status: "retracted", status_updated: today, status_reason: reasonText }, today);
|
|
241
243
|
const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
242
|
-
|
|
244
|
+
const tmpPath = findingsPath + ".tmp." + process.pid;
|
|
245
|
+
fs.writeFileSync(tmpPath, normalized);
|
|
246
|
+
fs.renameSync(tmpPath, findingsPath);
|
|
243
247
|
return phrenOk({ finding: matched.data.text, reason: reasonText, status: "retracted" });
|
|
244
248
|
});
|
|
245
249
|
}
|
|
@@ -289,7 +293,9 @@ export function resolveFindingContradiction(phrenPath, project, findingA, findin
|
|
|
289
293
|
statusB = "retracted";
|
|
290
294
|
}
|
|
291
295
|
const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
292
|
-
|
|
296
|
+
const tmpPath = findingsPath + ".tmp." + process.pid;
|
|
297
|
+
fs.writeFileSync(tmpPath, normalized);
|
|
298
|
+
fs.renameSync(tmpPath, findingsPath);
|
|
293
299
|
return phrenOk({
|
|
294
300
|
resolution,
|
|
295
301
|
finding_a: { text: matchedA.data.text, status: statusA },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { debugLog } from "../shared.js";
|
|
4
|
+
import { errorMessage } from "../utils.js";
|
|
5
|
+
const MAX_LOG_LINES = 1000;
|
|
6
|
+
export function recordRetrieval(phrenPath, file, section) {
|
|
7
|
+
const dir = path.join(phrenPath, ".runtime");
|
|
8
|
+
let logPath;
|
|
9
|
+
try {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
|
+
logPath = path.join(dir, "retrieval-log.jsonl");
|
|
12
|
+
const entry = { file, section, retrievedAt: new Date().toISOString() };
|
|
13
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
debugLog(`recordRetrieval write failed: ${errorMessage(err)}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const stat = fs.statSync(logPath);
|
|
21
|
+
if (stat.size > 500_000) {
|
|
22
|
+
const content = fs.readFileSync(logPath, "utf8");
|
|
23
|
+
const lines = content.split("\n").filter(Boolean);
|
|
24
|
+
fs.writeFileSync(logPath, lines.slice(-MAX_LOG_LINES).join("\n") + "\n");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
debugLog(`recordRetrieval rotation failed: ${errorMessage(err)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { debugLog } from "
|
|
4
|
-
import { errorMessage } from "
|
|
3
|
+
import { debugLog } from "../shared.js";
|
|
4
|
+
import { errorMessage } from "../utils.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
5
6
|
// Acquire the file lock, returning true on success or throwing on timeout.
|
|
6
7
|
function acquireFileLock(lockPath) {
|
|
7
8
|
const maxWait = Number.parseInt(process.env.PHREN_FILE_LOCK_MAX_WAIT_MS || "5000", 10) || 5000;
|
|
@@ -19,8 +20,7 @@ function acquireFileLock(lockPath) {
|
|
|
19
20
|
break;
|
|
20
21
|
}
|
|
21
22
|
catch (err) {
|
|
22
|
-
|
|
23
|
-
process.stderr.write(`[phren] acquireFileLock lockWrite: ${errorMessage(err)}\n`);
|
|
23
|
+
logger.debug("acquireFileLock", `lockWrite: ${errorMessage(err)}`);
|
|
24
24
|
try {
|
|
25
25
|
const stat = fs.statSync(lockPath);
|
|
26
26
|
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
@@ -40,7 +40,14 @@ function acquireFileLock(lockPath) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
|
|
43
|
+
try {
|
|
44
|
+
const result = require('child_process').spawnSync('tasklist', ['/FI', `PID eq ${lockPid}`, '/NH'], { encoding: 'utf8', timeout: 2000 });
|
|
45
|
+
if (result.stdout && result.stdout.includes(String(lockPid)))
|
|
46
|
+
ownerDead = false;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
ownerDead = true;
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
}
|
|
@@ -54,8 +61,7 @@ function acquireFileLock(lockPath) {
|
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
catch (statErr) {
|
|
57
|
-
|
|
58
|
-
process.stderr.write(`[phren] acquireFileLock staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
|
|
64
|
+
logger.debug("acquireFileLock", `staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}`);
|
|
59
65
|
sleep(pollInterval);
|
|
60
66
|
waited += pollInterval;
|
|
61
67
|
continue;
|
|
@@ -75,8 +81,7 @@ function releaseFileLock(lockPath) {
|
|
|
75
81
|
fs.unlinkSync(lockPath);
|
|
76
82
|
}
|
|
77
83
|
catch (err) {
|
|
78
|
-
|
|
79
|
-
process.stderr.write(`[phren] releaseFileLock: ${errorMessage(err)}\n`);
|
|
84
|
+
logger.debug("releaseFileLock", `${errorMessage(err)}`);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
// Q10: withFileLock now accepts both sync and async callbacks.
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "
|
|
5
|
-
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "
|
|
6
|
-
import { errorMessage, isValidProjectName, safeProjectPath } from "
|
|
7
|
-
import { readProjectConfig } from "
|
|
8
|
-
import { getActiveProfileDefaults } from "
|
|
9
|
-
import { runCustomHooks } from "
|
|
10
|
-
import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "
|
|
4
|
+
import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "../shared.js";
|
|
5
|
+
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "../shared/governance.js";
|
|
6
|
+
import { errorMessage, isValidProjectName, safeProjectPath } from "../utils.js";
|
|
7
|
+
import { readProjectConfig } from "../project-config.js";
|
|
8
|
+
import { getActiveProfileDefaults } from "../profile-store.js";
|
|
9
|
+
import { runCustomHooks } from "../hooks.js";
|
|
10
|
+
import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "../content/metadata.js";
|
|
11
|
+
/** @internal Exported for tests. */
|
|
11
12
|
export const MAX_QUEUE_ENTRY_LENGTH = 500;
|
|
13
|
+
export function buildSyncStatus(opts) {
|
|
14
|
+
return {
|
|
15
|
+
...(opts.pullAt !== undefined ? { lastPullAt: opts.pullAt } : {}),
|
|
16
|
+
...(opts.pullStatus !== undefined ? { lastPullStatus: opts.pullStatus } : {}),
|
|
17
|
+
...(opts.pullDetail !== undefined ? { lastPullDetail: opts.pullDetail } : {}),
|
|
18
|
+
...(opts.successfulPullAt !== undefined ? { lastSuccessfulPullAt: opts.successfulPullAt } : {}),
|
|
19
|
+
lastPushAt: opts.now,
|
|
20
|
+
lastPushStatus: opts.pushStatus,
|
|
21
|
+
...(opts.pushDetail !== undefined ? { lastPushDetail: opts.pushDetail } : {}),
|
|
22
|
+
...(opts.unsyncedCommits !== undefined ? { unsyncedCommits: opts.unsyncedCommits } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
12
25
|
export const GOVERNANCE_SCHEMA_VERSION = 1;
|
|
13
26
|
const DEFAULT_POLICY = {
|
|
14
27
|
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
@@ -641,7 +654,7 @@ export function pruneDeadMemories(phrenPath, project, dryRun) {
|
|
|
641
654
|
const file = resolveFindingsPath(dir);
|
|
642
655
|
if (!file)
|
|
643
656
|
continue;
|
|
644
|
-
// Q23:
|
|
657
|
+
// Q23: see docs/decisions/Q23-per-file-lock-concurrent-writers.md
|
|
645
658
|
withFileLock(file, () => {
|
|
646
659
|
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
647
660
|
let currentDate = null;
|
|
@@ -723,13 +736,11 @@ export function consolidateProjectFindings(phrenPath, project, dryRun) {
|
|
|
723
736
|
const file = resolveFindingsPath(path.join(phrenPath, project));
|
|
724
737
|
if (!file)
|
|
725
738
|
return phrenErr(`No FINDINGS.md found for "${project}".`, PhrenError.FILE_NOT_FOUND);
|
|
726
|
-
// Q23:
|
|
739
|
+
// Q23: see docs/decisions/Q23-per-file-lock-concurrent-writers.md
|
|
727
740
|
const result = withFileLock(file, () => {
|
|
728
741
|
const raw = fs.readFileSync(file, "utf8");
|
|
729
742
|
const lines = raw.split("\n");
|
|
730
|
-
// Q12:
|
|
731
|
-
// Archive blocks (<!-- phren:archive:start/end --> and <details>...</details>) are
|
|
732
|
-
// collected verbatim and appended unchanged after the consolidated active section.
|
|
743
|
+
// Q12: see docs/decisions/Q12-active-vs-archive-separation.md
|
|
733
744
|
const archiveBlocks = [];
|
|
734
745
|
const activeLines = [];
|
|
735
746
|
let inArchive = false;
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from "fs";
|
|
20
20
|
import * as path from "path";
|
|
21
|
-
import { debugLog } from "
|
|
22
|
-
import { errorMessage } from "
|
|
23
|
-
import { readProjectConfig } from "
|
|
21
|
+
import { debugLog } from "../shared.js";
|
|
22
|
+
import { errorMessage } from "../utils.js";
|
|
23
|
+
import { readProjectConfig } from "../project-config.js";
|
|
24
24
|
function configDir(phrenPath) {
|
|
25
25
|
return path.join(phrenPath, ".config");
|
|
26
26
|
}
|
|
@@ -104,7 +104,7 @@ function rolePermits(role, action) {
|
|
|
104
104
|
*
|
|
105
105
|
* Returns `{ allowed: true }` when permitted, `{ allowed: false, reason }` when denied.
|
|
106
106
|
*/
|
|
107
|
-
|
|
107
|
+
function checkPermission(phrenPath, action, project) {
|
|
108
108
|
const actor = (process.env.PHREN_ACTOR ?? "").trim() || null;
|
|
109
109
|
const globalAc = readGlobalAccessControl(phrenPath);
|
|
110
110
|
const projectAccess = project
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "
|
|
5
|
-
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "
|
|
6
|
-
import { errorMessage } from "
|
|
4
|
+
import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "../shared.js";
|
|
5
|
+
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "../shared/governance.js";
|
|
6
|
+
import { errorMessage } from "../utils.js";
|
|
7
|
+
import { logger } from "../logger.js";
|
|
8
|
+
const MAX_LOG_LINES = 1000;
|
|
7
9
|
const GOVERNANCE_SCHEMA_VERSION = 1;
|
|
8
10
|
const DEFAULT_MEMORY_SCORES_FILE = {
|
|
9
11
|
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
@@ -114,8 +116,7 @@ function readScoreJournal(phrenPath) {
|
|
|
114
116
|
return JSON.parse(line);
|
|
115
117
|
}
|
|
116
118
|
catch (err) {
|
|
117
|
-
|
|
118
|
-
process.stderr.write(`[phren] readScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
119
|
+
logger.debug("scores", `readScoreJournal parseLine: ${errorMessage(err)}`);
|
|
119
120
|
return null;
|
|
120
121
|
}
|
|
121
122
|
})
|
|
@@ -147,8 +148,7 @@ function claimScoreJournal(phrenPath) {
|
|
|
147
148
|
return JSON.parse(line);
|
|
148
149
|
}
|
|
149
150
|
catch (err) {
|
|
150
|
-
|
|
151
|
-
process.stderr.write(`[phren] claimScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
151
|
+
logger.debug("scores", `claimScoreJournal parseLine: ${errorMessage(err)}`);
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
})
|
|
@@ -163,8 +163,7 @@ function claimScoreJournal(phrenPath) {
|
|
|
163
163
|
fs.unlinkSync(claimedFile);
|
|
164
164
|
}
|
|
165
165
|
catch (err) {
|
|
166
|
-
|
|
167
|
-
process.stderr.write(`[phren] claimScoreJournal unlinkClaim: ${errorMessage(err)}\n`);
|
|
166
|
+
logger.debug("scores", `claimScoreJournal unlinkClaim: ${errorMessage(err)}`);
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
}
|
|
@@ -257,7 +256,7 @@ export function recordInjection(phrenPath, key, sessionId) {
|
|
|
257
256
|
if (stat.size > 1_000_000) {
|
|
258
257
|
const content = fs.readFileSync(logFile, "utf8");
|
|
259
258
|
const lines = content.split("\n");
|
|
260
|
-
fs.writeFileSync(logFile, lines.slice(-
|
|
259
|
+
fs.writeFileSync(logFile, lines.slice(-MAX_LOG_LINES).join("\n"));
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
262
|
catch (err) {
|
|
@@ -276,7 +275,7 @@ export function recordFeedback(phrenPath, key, feedback, sessionId) {
|
|
|
276
275
|
appendAuditLog(phrenPath, "memory_feedback", `key=${key} feedback=${feedback}`);
|
|
277
276
|
// When feedback is "helpful", mark correlated query entries for future boost
|
|
278
277
|
if (feedback === "helpful" && sessionId) {
|
|
279
|
-
import("
|
|
278
|
+
import("../query-correlation.js").then(({ markCorrelationsHelpful: markHelpful }) => {
|
|
280
279
|
const colonIdx = key.indexOf(":");
|
|
281
280
|
const docKey = colonIdx >= 0 ? key.slice(0, colonIdx) : key;
|
|
282
281
|
markHelpful(phrenPath, sessionId, docKey);
|
package/mcp/dist/hooks.js
CHANGED
|
@@ -9,6 +9,8 @@ import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, ins
|
|
|
9
9
|
import { errorMessage } from "./utils.js";
|
|
10
10
|
import { hookConfigPath } from "./provider-adapters.js";
|
|
11
11
|
import { PACKAGE_SPEC } from "./package-metadata.js";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
import { withFileLock } from "./shared/governance.js";
|
|
12
14
|
export function commandExists(cmd) {
|
|
13
15
|
try {
|
|
14
16
|
const whichCmd = process.platform === "win32" ? "where.exe" : "which";
|
|
@@ -67,10 +69,6 @@ function phrenPackageSpec() {
|
|
|
67
69
|
export function shellEscape(s) {
|
|
68
70
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
69
71
|
}
|
|
70
|
-
/** @deprecated Use shellEscape instead */
|
|
71
|
-
function shellSingleQuote(value) {
|
|
72
|
-
return shellEscape(value);
|
|
73
|
-
}
|
|
74
72
|
function buildPackageLifecycleCommands() {
|
|
75
73
|
const packageSpec = phrenPackageSpec();
|
|
76
74
|
return {
|
|
@@ -84,10 +82,10 @@ export function buildLifecycleCommands(phrenPath) {
|
|
|
84
82
|
const entry = resolveCliEntryScript();
|
|
85
83
|
const isWindows = process.platform === "win32";
|
|
86
84
|
const escapedPhren = phrenPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
87
|
-
const quotedPhren =
|
|
85
|
+
const quotedPhren = shellEscape(phrenPath);
|
|
88
86
|
if (entry) {
|
|
89
87
|
const escapedEntry = entry.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
90
|
-
const quotedEntry =
|
|
88
|
+
const quotedEntry = shellEscape(entry);
|
|
91
89
|
if (isWindows) {
|
|
92
90
|
return {
|
|
93
91
|
sessionStart: `set "PHREN_PATH=${escapedPhren}" && node "${escapedEntry}" hook-session-start`,
|
|
@@ -126,7 +124,7 @@ function withHookToolEnv(command, tool) {
|
|
|
126
124
|
if (process.platform === "win32") {
|
|
127
125
|
return `set "PHREN_HOOK_TOOL=${tool}" && ${command}`;
|
|
128
126
|
}
|
|
129
|
-
return `PHREN_HOOK_TOOL=${
|
|
127
|
+
return `PHREN_HOOK_TOOL=${shellEscape(tool)} ${command}`;
|
|
130
128
|
}
|
|
131
129
|
function withHookToolLifecycleCommands(lifecycle, tool) {
|
|
132
130
|
return {
|
|
@@ -153,10 +151,10 @@ function installSessionWrapper(tool, phrenPath) {
|
|
|
153
151
|
const content = `#!/bin/sh
|
|
154
152
|
set -u
|
|
155
153
|
|
|
156
|
-
REAL_BIN=${
|
|
157
|
-
DEFAULT_PHREN_PATH=${
|
|
154
|
+
REAL_BIN=${shellEscape(realBinary)}
|
|
155
|
+
DEFAULT_PHREN_PATH=${shellEscape(phrenPath)}
|
|
158
156
|
PHREN_PATH="\${PHREN_PATH:-$DEFAULT_PHREN_PATH}"
|
|
159
|
-
ENTRY_SCRIPT=${
|
|
157
|
+
ENTRY_SCRIPT=${shellEscape(entry || "")}
|
|
160
158
|
export PHREN_HOOK_TOOL="${tool}"
|
|
161
159
|
|
|
162
160
|
if [ ! -x "$REAL_BIN" ]; then
|
|
@@ -238,7 +236,14 @@ function cachedReadInstallPrefsJson(phrenPath) {
|
|
|
238
236
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
239
237
|
return cached.parsed;
|
|
240
238
|
}
|
|
241
|
-
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
_installPrefsJsonCache.delete(prefsPath);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
242
247
|
_installPrefsJsonCache.set(prefsPath, { mtimeMs, parsed });
|
|
243
248
|
return parsed;
|
|
244
249
|
}
|
|
@@ -274,6 +279,7 @@ export const HOOK_EVENT_VALUES = [
|
|
|
274
279
|
"post-session-end", "post-consolidate",
|
|
275
280
|
];
|
|
276
281
|
const VALID_HOOK_EVENTS = new Set(HOOK_EVENT_VALUES);
|
|
282
|
+
const MAX_HOOK_COMMAND_LENGTH = 1000;
|
|
277
283
|
/** Return the target (URL or shell command) for display or matching. */
|
|
278
284
|
export function getHookTarget(h) {
|
|
279
285
|
return "webhook" in h ? h.webhook : h.command;
|
|
@@ -282,8 +288,8 @@ export function validateCustomHookCommand(command) {
|
|
|
282
288
|
const trimmed = command.trim();
|
|
283
289
|
if (!trimmed)
|
|
284
290
|
return "Command cannot be empty.";
|
|
285
|
-
if (trimmed.length >
|
|
286
|
-
return
|
|
291
|
+
if (trimmed.length > MAX_HOOK_COMMAND_LENGTH)
|
|
292
|
+
return `Command too long (max ${MAX_HOOK_COMMAND_LENGTH} characters).`;
|
|
287
293
|
if (/[`$(){}&|;<>\n\r#]/.test(trimmed)) {
|
|
288
294
|
return "Command contains disallowed shell characters: ` $ ( ) { } & | ; < > # \\n \\r";
|
|
289
295
|
}
|
|
@@ -399,9 +405,7 @@ async function validateAndResolveWebhook(webhook) {
|
|
|
399
405
|
}
|
|
400
406
|
catch (err) {
|
|
401
407
|
debugLog(`validateAndResolveWebhook lookup failed for ${parsed.hostname}: ${errorMessage(err)}`);
|
|
402
|
-
|
|
403
|
-
// (fetch will do its own resolution and may fail with a network error)
|
|
404
|
-
return { resolvedUrl: webhook, host: parsed.host };
|
|
408
|
+
return { error: `webhook hostname "${parsed.hostname}" could not be resolved: ${errorMessage(err)}` };
|
|
405
409
|
}
|
|
406
410
|
}
|
|
407
411
|
const DEFAULT_CUSTOM_HOOK_TIMEOUT = 5000;
|
|
@@ -430,18 +434,24 @@ export function readCustomHooks(phrenPath) {
|
|
|
430
434
|
function appendHookErrorLog(phrenPath, event, message) {
|
|
431
435
|
const logPath = runtimeFile(phrenPath, "hook-errors.log");
|
|
432
436
|
const line = `[${new Date().toISOString()}] [${event}] ${message}\n`;
|
|
433
|
-
fs.appendFileSync(logPath, line);
|
|
434
437
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
withFileLock(logPath, () => {
|
|
439
|
+
fs.appendFileSync(logPath, line);
|
|
440
|
+
try {
|
|
441
|
+
const stat = fs.statSync(logPath);
|
|
442
|
+
if (stat.size > 200_000) {
|
|
443
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
444
|
+
const lines = content.split("\n").filter(Boolean);
|
|
445
|
+
atomicWriteText(logPath, lines.slice(-HOOK_ERROR_LOG_MAX_LINES).join("\n") + "\n");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
logger.debug("appendHookErrorLog rotate", errorMessage(err));
|
|
450
|
+
}
|
|
451
|
+
});
|
|
441
452
|
}
|
|
442
453
|
catch (err) {
|
|
443
|
-
|
|
444
|
-
process.stderr.write(`[phren] appendHookErrorLog rotate: ${errorMessage(err)}\n`);
|
|
454
|
+
logger.debug("appendHookErrorLog lock", errorMessage(err));
|
|
445
455
|
}
|
|
446
456
|
}
|
|
447
457
|
export function runCustomHooks(phrenPath, event, env = {}) {
|
|
@@ -488,8 +498,7 @@ export function runCustomHooks(phrenPath, event, env = {}) {
|
|
|
488
498
|
appendHookErrorLog(phrenPath, event, message);
|
|
489
499
|
}
|
|
490
500
|
catch (logErr) {
|
|
491
|
-
|
|
492
|
-
process.stderr.write(`[phren] runCustomHooks webhookErrorLog: ${errorMessage(logErr)}\n`);
|
|
501
|
+
logger.debug("runCustomHooks webhookErrorLog", errorMessage(logErr));
|
|
493
502
|
}
|
|
494
503
|
});
|
|
495
504
|
continue;
|
|
@@ -503,12 +512,22 @@ export function runCustomHooks(phrenPath, event, env = {}) {
|
|
|
503
512
|
continue;
|
|
504
513
|
}
|
|
505
514
|
const shellArgs = isWindows ? ["/c", hook.command] : ["-c", hook.command];
|
|
515
|
+
// On Windows, cmd /c expands %VAR% in the command string.
|
|
516
|
+
// Sanitize env values to prevent shell metacharacter injection.
|
|
517
|
+
const mergedEnv = { ...process.env, PHREN_PATH: phrenPath, PHREN_HOOK_EVENT: event, ...env };
|
|
518
|
+
if (isWindows) {
|
|
519
|
+
for (const [key, val] of Object.entries(mergedEnv)) {
|
|
520
|
+
if (typeof val === "string") {
|
|
521
|
+
mergedEnv[key] = val.replace(/[&|<>^%]/g, "");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
506
525
|
try {
|
|
507
526
|
execFileSync(shellCmd, shellArgs, {
|
|
508
527
|
cwd: phrenPath,
|
|
509
528
|
encoding: "utf8",
|
|
510
529
|
timeout: hook.timeout ?? DEFAULT_CUSTOM_HOOK_TIMEOUT,
|
|
511
|
-
env:
|
|
530
|
+
env: mergedEnv,
|
|
512
531
|
stdio: ["ignore", "ignore", "pipe"],
|
|
513
532
|
});
|
|
514
533
|
}
|
|
@@ -520,8 +539,7 @@ export function runCustomHooks(phrenPath, event, env = {}) {
|
|
|
520
539
|
appendHookErrorLog(phrenPath, event, errorMessage(err));
|
|
521
540
|
}
|
|
522
541
|
catch (logErr) {
|
|
523
|
-
|
|
524
|
-
process.stderr.write(`[phren] runCustomHooks hookErrorLog: ${errorMessage(logErr)}\n`);
|
|
542
|
+
logger.debug("runCustomHooks hookErrorLog", errorMessage(logErr));
|
|
525
543
|
}
|
|
526
544
|
}
|
|
527
545
|
}
|
|
@@ -556,7 +574,7 @@ export function configureAllHooks(phrenPath, options = {}) {
|
|
|
556
574
|
configured.push("Copilot CLI");
|
|
557
575
|
}
|
|
558
576
|
catch (err) {
|
|
559
|
-
|
|
577
|
+
console.warn(`configureAllHooks: copilot hook config failed: ${errorMessage(err)}`);
|
|
560
578
|
}
|
|
561
579
|
if (isToolHookEnabled(phrenPath, "copilot"))
|
|
562
580
|
installSessionWrapper("copilot", phrenPath);
|
|
@@ -572,8 +590,7 @@ export function configureAllHooks(phrenPath, options = {}) {
|
|
|
572
590
|
existing = JSON.parse(fs.readFileSync(cursorFile, "utf8"));
|
|
573
591
|
}
|
|
574
592
|
catch (err) {
|
|
575
|
-
|
|
576
|
-
process.stderr.write(`[phren] configureAllHooks cursorRead: ${errorMessage(err)}\n`);
|
|
593
|
+
logger.debug("configureAllHooks cursorRead", errorMessage(err));
|
|
577
594
|
}
|
|
578
595
|
const config = {
|
|
579
596
|
...existing,
|
|
@@ -589,7 +606,7 @@ export function configureAllHooks(phrenPath, options = {}) {
|
|
|
589
606
|
configured.push("Cursor");
|
|
590
607
|
}
|
|
591
608
|
catch (err) {
|
|
592
|
-
|
|
609
|
+
console.warn(`configureAllHooks: cursor hook config failed: ${errorMessage(err)}`);
|
|
593
610
|
}
|
|
594
611
|
if (isToolHookEnabled(phrenPath, "cursor"))
|
|
595
612
|
installSessionWrapper("cursor", phrenPath);
|
|
@@ -604,8 +621,7 @@ export function configureAllHooks(phrenPath, options = {}) {
|
|
|
604
621
|
existing = JSON.parse(fs.readFileSync(codexFile, "utf8"));
|
|
605
622
|
}
|
|
606
623
|
catch (err) {
|
|
607
|
-
|
|
608
|
-
process.stderr.write(`[phren] configureAllHooks codexRead: ${errorMessage(err)}\n`);
|
|
624
|
+
logger.debug("configureAllHooks codexRead", errorMessage(err));
|
|
609
625
|
}
|
|
610
626
|
const config = {
|
|
611
627
|
...existing,
|
|
@@ -621,7 +637,7 @@ export function configureAllHooks(phrenPath, options = {}) {
|
|
|
621
637
|
configured.push("Codex");
|
|
622
638
|
}
|
|
623
639
|
catch (err) {
|
|
624
|
-
|
|
640
|
+
console.warn(`configureAllHooks: codex hook config failed: ${errorMessage(err)}`);
|
|
625
641
|
}
|
|
626
642
|
if (isToolHookEnabled(phrenPath, "codex"))
|
|
627
643
|
installSessionWrapper("codex", phrenPath);
|
package/mcp/dist/index-query.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import { debugLog } from "./shared.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
3
4
|
function describeSqlValue(value) {
|
|
4
5
|
if (value === null)
|
|
5
6
|
return "null";
|
|
@@ -78,7 +79,9 @@ export function queryRows(db, sql, params) {
|
|
|
78
79
|
return results[0].values;
|
|
79
80
|
}
|
|
80
81
|
catch (err) {
|
|
81
|
-
|
|
82
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
83
|
+
logger.debug("queryRows", `DB query failed: ${msg}`);
|
|
84
|
+
debugLog(`queryRows failed: ${msg}`);
|
|
82
85
|
return null;
|
|
83
86
|
}
|
|
84
87
|
}
|