@phren/cli 0.0.11 → 0.0.13
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 +9 -9
- package/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-actions.js +54 -67
- package/mcp/dist/cli-config.js +4 -5
- package/mcp/dist/cli-extract.js +3 -2
- package/mcp/dist/cli-graph.js +17 -3
- package/mcp/dist/cli-hooks-output.js +1 -1
- package/mcp/dist/cli-hooks-session.js +1 -1
- package/mcp/dist/cli-hooks.js +5 -3
- package/mcp/dist/cli.js +1 -1
- package/mcp/dist/content-archive.js +21 -12
- package/mcp/dist/content-citation.js +13 -2
- package/mcp/dist/content-learning.js +6 -4
- package/mcp/dist/content-metadata.js +10 -0
- package/mcp/dist/core-finding.js +1 -1
- package/mcp/dist/data-access.js +10 -31
- package/mcp/dist/data-tasks.js +5 -26
- package/mcp/dist/embedding.js +0 -1
- package/mcp/dist/entrypoint.js +4 -0
- package/mcp/dist/finding-impact.js +1 -32
- package/mcp/dist/finding-journal.js +1 -1
- package/mcp/dist/finding-lifecycle.js +2 -7
- package/mcp/dist/governance-locks.js +6 -0
- package/mcp/dist/governance-policy.js +1 -7
- package/mcp/dist/governance-scores.js +1 -7
- package/mcp/dist/hooks.js +23 -0
- package/mcp/dist/init-config.js +1 -1
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +1 -50
- package/mcp/dist/init-shared.js +53 -1
- package/mcp/dist/init.js +21 -6
- package/mcp/dist/link-context.js +1 -1
- package/mcp/dist/link-doctor.js +11 -54
- package/mcp/dist/link.js +4 -53
- package/mcp/dist/mcp-extract-facts.js +11 -6
- package/mcp/dist/mcp-finding.js +10 -14
- package/mcp/dist/mcp-graph.js +6 -6
- package/mcp/dist/mcp-hooks.js +1 -1
- package/mcp/dist/mcp-search.js +3 -8
- package/mcp/dist/mcp-session.js +12 -2
- package/mcp/dist/memory-ui-assets.js +1 -36
- package/mcp/dist/memory-ui-graph.js +152 -50
- package/mcp/dist/memory-ui-page.js +7 -5
- package/mcp/dist/memory-ui-scripts.js +42 -36
- package/mcp/dist/phren-core.js +2 -0
- package/mcp/dist/phren-paths.js +1 -2
- package/mcp/dist/proactivity.js +5 -5
- package/mcp/dist/project-config.js +1 -1
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +22 -19
- package/mcp/dist/session-checkpoints.js +14 -14
- package/mcp/dist/shared-data-utils.js +28 -0
- package/mcp/dist/shared-fragment-graph.js +11 -11
- package/mcp/dist/shared-governance.js +1 -1
- package/mcp/dist/shared-retrieval.js +2 -10
- package/mcp/dist/shared-search-fallback.js +2 -12
- package/mcp/dist/shared.js +2 -3
- package/mcp/dist/shell-entry.js +1 -1
- package/mcp/dist/shell-input.js +62 -52
- package/mcp/dist/shell-palette.js +6 -1
- package/mcp/dist/shell-render.js +9 -5
- package/mcp/dist/shell-state-store.js +1 -4
- package/mcp/dist/shell-view.js +4 -4
- package/mcp/dist/shell.js +4 -54
- package/mcp/dist/status.js +2 -8
- package/mcp/dist/utils.js +1 -1
- package/package.json +1 -2
- package/skills/docs.md +11 -11
- package/starter/README.md +1 -1
- package/starter/global/CLAUDE.md +2 -2
- package/starter/global/skills/audit.md +10 -10
- package/mcp/dist/cli-hooks-retrieval.js +0 -2
- package/mcp/dist/impact-scoring.js +0 -22
package/mcp/dist/link-doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
4
|
import { debugLog, EXEC_TIMEOUT_QUICK_MS, getProjectDirs, isRecord, homeDir, homePath, hookConfigPath, runtimeHealthFile, } from "./shared.js";
|
|
5
|
+
import { commandVersion, versionAtLeast, nearestWritableTarget } from "./init-shared.js";
|
|
5
6
|
import { validateGovernanceJson } from "./shared-governance.js";
|
|
6
7
|
import { errorMessage } from "./utils.js";
|
|
7
8
|
import { buildIndex, queryRows } from "./shared-index.js";
|
|
@@ -34,53 +35,6 @@ function isWrapperActive(tool) {
|
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
function commandVersion(cmd, args = ["--version"]) {
|
|
38
|
-
try {
|
|
39
|
-
return execFileSync(cmd, args, {
|
|
40
|
-
encoding: "utf8",
|
|
41
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
-
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
43
|
-
}).trim();
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
debugLog(`doctor: commandVersion ${cmd} failed: ${errorMessage(err)}`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function parseSemverTriple(raw) {
|
|
51
|
-
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
52
|
-
if (!match)
|
|
53
|
-
return null;
|
|
54
|
-
return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
|
|
55
|
-
}
|
|
56
|
-
function versionAtLeast(raw, major, minor = 0) {
|
|
57
|
-
if (!raw)
|
|
58
|
-
return false;
|
|
59
|
-
const parsed = parseSemverTriple(raw);
|
|
60
|
-
if (!parsed)
|
|
61
|
-
return false;
|
|
62
|
-
const [m, n] = parsed;
|
|
63
|
-
if (m !== major)
|
|
64
|
-
return m > major;
|
|
65
|
-
return n >= minor;
|
|
66
|
-
}
|
|
67
|
-
function nearestWritableTarget(filePath) {
|
|
68
|
-
let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
|
|
69
|
-
while (!fs.existsSync(probe)) {
|
|
70
|
-
const parent = path.dirname(probe);
|
|
71
|
-
if (parent === probe)
|
|
72
|
-
return false;
|
|
73
|
-
probe = parent;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
fs.accessSync(probe, fs.constants.W_OK);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
debugLog(`doctor: writable check failed for ${filePath}: ${errorMessage(err)}`);
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
38
|
function gitRemoteStatus(phrenPath) {
|
|
85
39
|
try {
|
|
86
40
|
execFileSync("git", ["-C", phrenPath, "rev-parse", "--is-inside-work-tree"], {
|
|
@@ -375,6 +329,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
375
329
|
const detected = detectInstalledTools();
|
|
376
330
|
if (detected.has("copilot")) {
|
|
377
331
|
const copilotHooks = hookConfigPath("copilot", phrenPath);
|
|
332
|
+
const copilotWritable = nearestWritableTarget(copilotHooks);
|
|
378
333
|
checks.push({
|
|
379
334
|
name: "copilot-hooks",
|
|
380
335
|
ok: fs.existsSync(copilotHooks),
|
|
@@ -382,12 +337,13 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
382
337
|
});
|
|
383
338
|
checks.push({
|
|
384
339
|
name: "copilot-config-writable",
|
|
385
|
-
ok:
|
|
386
|
-
detail:
|
|
340
|
+
ok: copilotWritable,
|
|
341
|
+
detail: copilotWritable ? `writable: ${copilotHooks}` : `not writable: ${copilotHooks}`,
|
|
387
342
|
});
|
|
388
343
|
}
|
|
389
344
|
if (detected.has("cursor")) {
|
|
390
345
|
const cursorHooks = hookConfigPath("cursor", phrenPath);
|
|
346
|
+
const cursorWritable = nearestWritableTarget(cursorHooks);
|
|
391
347
|
checks.push({
|
|
392
348
|
name: "cursor-hooks",
|
|
393
349
|
ok: fs.existsSync(cursorHooks),
|
|
@@ -395,12 +351,13 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
395
351
|
});
|
|
396
352
|
checks.push({
|
|
397
353
|
name: "cursor-config-writable",
|
|
398
|
-
ok:
|
|
399
|
-
detail:
|
|
354
|
+
ok: cursorWritable,
|
|
355
|
+
detail: cursorWritable ? `writable: ${cursorHooks}` : `not writable: ${cursorHooks}`,
|
|
400
356
|
});
|
|
401
357
|
}
|
|
402
358
|
if (detected.has("codex")) {
|
|
403
359
|
const codexHooks = hookConfigPath("codex", phrenPath);
|
|
360
|
+
const codexWritable = nearestWritableTarget(codexHooks);
|
|
404
361
|
checks.push({
|
|
405
362
|
name: "codex-hooks",
|
|
406
363
|
ok: fs.existsSync(codexHooks),
|
|
@@ -408,8 +365,8 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
408
365
|
});
|
|
409
366
|
checks.push({
|
|
410
367
|
name: "codex-config-writable",
|
|
411
|
-
ok:
|
|
412
|
-
detail:
|
|
368
|
+
ok: codexWritable,
|
|
369
|
+
detail: codexWritable ? `writable: ${codexHooks}` : `not writable: ${codexHooks}`,
|
|
413
370
|
});
|
|
414
371
|
}
|
|
415
372
|
for (const tool of ["copilot", "cursor", "codex"]) {
|
|
@@ -446,7 +403,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
446
403
|
}
|
|
447
404
|
else {
|
|
448
405
|
// Read-only mode: just check if hook configs exist, don't write anything
|
|
449
|
-
const detectedTools =
|
|
406
|
+
const detectedTools = detected;
|
|
450
407
|
const hookChecks = [];
|
|
451
408
|
const missing = [];
|
|
452
409
|
for (const tool of detectedTools) {
|
package/mcp/dist/link.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as readline from "readline";
|
|
|
4
4
|
import * as yaml from "js-yaml";
|
|
5
5
|
import { execFileSync } from "child_process";
|
|
6
6
|
import { ROOT } from "./package-metadata.js";
|
|
7
|
-
import {
|
|
7
|
+
import { configureMcpTargets, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
|
|
8
8
|
import { configureAllHooks, detectInstalledTools } from "./hooks.js";
|
|
9
9
|
import { getMachineName, persistMachineName } from "./machine-identity.js";
|
|
10
10
|
import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "./shared.js";
|
|
@@ -34,7 +34,7 @@ function listProfiles(phrenPath) {
|
|
|
34
34
|
const listed = listProfilesShared(phrenPath);
|
|
35
35
|
if (!listed.ok)
|
|
36
36
|
return [];
|
|
37
|
-
return listed.data.map((profile) => ({ name: profile.name
|
|
37
|
+
return listed.data.map((profile) => ({ name: profile.name }));
|
|
38
38
|
}
|
|
39
39
|
export function findProfileFile(phrenPath, profileName) {
|
|
40
40
|
const profilesDir = path.join(phrenPath, "profiles");
|
|
@@ -94,7 +94,7 @@ async function registerMachine(phrenPath) {
|
|
|
94
94
|
}
|
|
95
95
|
log("\nAvailable profiles:");
|
|
96
96
|
for (const p of listProfiles(phrenPath))
|
|
97
|
-
log(` ${p.name}
|
|
97
|
+
log(` ${p.name}`);
|
|
98
98
|
log("");
|
|
99
99
|
const profile = (await ask("Which profile? ")).trim();
|
|
100
100
|
rl.close();
|
|
@@ -491,56 +491,7 @@ export async function runLink(phrenPath, opts = {}) {
|
|
|
491
491
|
log(` MCP mode: ${mcpEnabled ? "ON (recommended)" : "OFF (hooks-only fallback)"}`);
|
|
492
492
|
log(` Hooks mode: ${hooksEnabled ? "ON (active)" : "OFF (disabled)"}`);
|
|
493
493
|
maybeOfferStarterTemplateUpdate(phrenPath);
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
mcpStatus = configureClaude(phrenPath, { mcpEnabled, hooksEnabled }) ?? "installed";
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
if ((process.env.PHREN_DEBUG))
|
|
500
|
-
process.stderr.write(`[phren] link configureClaude: ${errorMessage(err)}\n`);
|
|
501
|
-
}
|
|
502
|
-
logMcpTargetStatus("Claude", mcpStatus);
|
|
503
|
-
let vsStatus = "no_vscode";
|
|
504
|
-
try {
|
|
505
|
-
vsStatus = configureVSCode(phrenPath, { mcpEnabled }) ?? "no_vscode";
|
|
506
|
-
}
|
|
507
|
-
catch (err) {
|
|
508
|
-
if ((process.env.PHREN_DEBUG))
|
|
509
|
-
process.stderr.write(`[phren] link configureVSCode: ${errorMessage(err)}\n`);
|
|
510
|
-
}
|
|
511
|
-
logMcpTargetStatus("VS Code", vsStatus);
|
|
512
|
-
let cursorStatus = "no_cursor";
|
|
513
|
-
try {
|
|
514
|
-
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled }) ?? "no_cursor";
|
|
515
|
-
}
|
|
516
|
-
catch (err) {
|
|
517
|
-
if ((process.env.PHREN_DEBUG))
|
|
518
|
-
process.stderr.write(`[phren] link configureCursorMcp: ${errorMessage(err)}\n`);
|
|
519
|
-
}
|
|
520
|
-
logMcpTargetStatus("Cursor", cursorStatus);
|
|
521
|
-
let copilotStatus = "no_copilot";
|
|
522
|
-
try {
|
|
523
|
-
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled }) ?? "no_copilot";
|
|
524
|
-
}
|
|
525
|
-
catch (err) {
|
|
526
|
-
if ((process.env.PHREN_DEBUG))
|
|
527
|
-
process.stderr.write(`[phren] link configureCopilotMcp: ${errorMessage(err)}\n`);
|
|
528
|
-
}
|
|
529
|
-
logMcpTargetStatus("Copilot CLI", copilotStatus);
|
|
530
|
-
let codexStatus = "no_codex";
|
|
531
|
-
try {
|
|
532
|
-
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled }) ?? "no_codex";
|
|
533
|
-
}
|
|
534
|
-
catch (err) {
|
|
535
|
-
if ((process.env.PHREN_DEBUG))
|
|
536
|
-
process.stderr.write(`[phren] link configureCodexMcp: ${errorMessage(err)}\n`);
|
|
537
|
-
}
|
|
538
|
-
logMcpTargetStatus("Codex", codexStatus);
|
|
539
|
-
const mcpStatusForContext = [mcpStatus, vsStatus, cursorStatus, copilotStatus, codexStatus].some((s) => s === "installed" || s === "already_configured")
|
|
540
|
-
? "installed"
|
|
541
|
-
: [mcpStatus, vsStatus, cursorStatus, copilotStatus, codexStatus].some((s) => s === "disabled" || s === "already_disabled")
|
|
542
|
-
? "disabled"
|
|
543
|
-
: mcpStatus;
|
|
494
|
+
const mcpStatusForContext = configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled });
|
|
544
495
|
// Register hooks for Copilot CLI, Cursor, Codex
|
|
545
496
|
if (hooksEnabled) {
|
|
546
497
|
const hookedTools = configureAllHooks(phrenPath, { tools: detectedTools });
|
|
@@ -9,6 +9,7 @@ import * as path from "path";
|
|
|
9
9
|
import { debugLog } from "./shared.js";
|
|
10
10
|
import { safeProjectPath, isFeatureEnabled, errorMessage } from "./utils.js";
|
|
11
11
|
import { callLlm } from "./content-dedup.js";
|
|
12
|
+
import { withFileLock } from "./shared-governance.js";
|
|
12
13
|
const FACT_EXTRACT_FLAG = "PHREN_FEATURE_FACT_EXTRACT";
|
|
13
14
|
const MAX_FACTS = 50;
|
|
14
15
|
function preferencesPath(phrenPath, project) {
|
|
@@ -64,13 +65,17 @@ export function extractFactFromFinding(phrenPath, project, finding) {
|
|
|
64
65
|
const fact = raw.replace(/[\r\n]+/g, " ").trim().slice(0, 200);
|
|
65
66
|
if (!fact)
|
|
66
67
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const normalized = fact.toLowerCase();
|
|
70
|
-
if (existing.some(f => f.fact.toLowerCase() === normalized))
|
|
68
|
+
const p = preferencesPath(phrenPath, project);
|
|
69
|
+
if (!p)
|
|
71
70
|
return;
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
withFileLock(p, () => {
|
|
72
|
+
const existing = readExtractedFacts(phrenPath, project);
|
|
73
|
+
const normalized = fact.toLowerCase();
|
|
74
|
+
if (existing.some(f => f.fact.toLowerCase() === normalized))
|
|
75
|
+
return;
|
|
76
|
+
existing.push({ fact, source: finding.slice(0, 120), at: new Date().toISOString() });
|
|
77
|
+
writeExtractedFacts(phrenPath, project, existing);
|
|
78
|
+
});
|
|
74
79
|
})
|
|
75
80
|
.catch((err) => {
|
|
76
81
|
debugLog(`extractFactFromFinding: ${errorMessage(err)}`);
|
package/mcp/dist/mcp-finding.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
|
|
6
6
|
import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "./core-finding.js";
|
|
7
|
-
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, } from "./shared.js";
|
|
7
|
+
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, RESERVED_PROJECT_DIR_NAMES, } from "./shared.js";
|
|
8
8
|
import { addFindingToFile, addFindingsToFile, checkSemanticConflicts, autoMergeConflicts, } from "./shared-content.js";
|
|
9
9
|
import { jaccardTokenize, jaccardSimilarity, stripMetadata } from "./content-dedup.js";
|
|
10
10
|
import { runCustomHooks } from "./hooks.js";
|
|
@@ -17,7 +17,6 @@ import { FINDING_PROVENANCE_SOURCES } from "./content-citation.js";
|
|
|
17
17
|
import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "./finding-lifecycle.js";
|
|
18
18
|
const JACCARD_MAYBE_LOW = 0.30;
|
|
19
19
|
const JACCARD_MAYBE_HIGH = 0.55; // above this isDuplicateFinding already catches it
|
|
20
|
-
const RESERVED_PROJECT_DIRS = new Set(["global", ".runtime", ".sessions", ".governance"]);
|
|
21
20
|
function findJaccardCandidates(phrenPath, project, finding) {
|
|
22
21
|
try {
|
|
23
22
|
const findingsPath = path.join(phrenPath, project, "FINDINGS.md");
|
|
@@ -134,14 +133,11 @@ export function register(server, ctx) {
|
|
|
134
133
|
if (!result.ok) {
|
|
135
134
|
return mcpResponse({ ok: false, error: result.error });
|
|
136
135
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const isAdded = !isSkipped;
|
|
140
|
-
if (isSkipped) {
|
|
141
|
-
return mcpResponse({ ok: true, message: result.data, data: { project, finding: taggedFinding, status: "skipped" } });
|
|
136
|
+
if (result.data.status === "skipped") {
|
|
137
|
+
return mcpResponse({ ok: true, message: result.data.message, data: { project, finding: taggedFinding, status: "skipped" } });
|
|
142
138
|
}
|
|
143
139
|
updateFileInIndex(path.join(phrenPath, project, "FINDINGS.md"));
|
|
144
|
-
if (
|
|
140
|
+
if (result.data.status === "added" || result.data.status === "created") {
|
|
145
141
|
runCustomHooks(phrenPath, "post-finding", { PHREN_PROJECT: project });
|
|
146
142
|
incrementSessionFindings(phrenPath, 1, sessionId, project);
|
|
147
143
|
extractFactFromFinding(phrenPath, project, taggedFinding);
|
|
@@ -178,7 +174,7 @@ export function register(server, ctx) {
|
|
|
178
174
|
}
|
|
179
175
|
const conflictsWithList = semanticConflicts.checked
|
|
180
176
|
? extractConflictsWith(semanticConflicts.annotations)
|
|
181
|
-
: (result.data.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)?.[1] ? [result.data.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)[1]] : []);
|
|
177
|
+
: (result.data.message.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)?.[1] ? [result.data.message.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)[1]] : []);
|
|
182
178
|
const conflictsWith = conflictsWithList[0];
|
|
183
179
|
// Extract fragment hints synchronously from the finding text (regex only, no DB).
|
|
184
180
|
// Full DB fragment linking happens on the next index rebuild via updateFileInIndex →
|
|
@@ -186,11 +182,11 @@ export function register(server, ctx) {
|
|
|
186
182
|
const detectedFragments = extractFragmentNames(taggedFinding);
|
|
187
183
|
return mcpResponse({
|
|
188
184
|
ok: true,
|
|
189
|
-
message: result.data,
|
|
185
|
+
message: result.data.message,
|
|
190
186
|
data: {
|
|
191
187
|
project,
|
|
192
188
|
finding: taggedFinding,
|
|
193
|
-
status:
|
|
189
|
+
status: result.data.status,
|
|
194
190
|
...(conflictsWith ? { conflictsWith } : {}),
|
|
195
191
|
...(conflictsWithList.length > 0 ? { conflicts: conflictsWithList } : {}),
|
|
196
192
|
...(detectedFragments.length > 0 ? { detectedFragments } : {}),
|
|
@@ -356,7 +352,7 @@ export function register(server, ctx) {
|
|
|
356
352
|
const projects = project
|
|
357
353
|
? [project]
|
|
358
354
|
: fs.readdirSync(phrenPath, { withFileTypes: true })
|
|
359
|
-
.filter((entry) => entry.isDirectory() && !
|
|
355
|
+
.filter((entry) => entry.isDirectory() && !RESERVED_PROJECT_DIR_NAMES.has(entry.name) && isValidProjectName(entry.name))
|
|
360
356
|
.map((entry) => entry.name);
|
|
361
357
|
const contradictions = [];
|
|
362
358
|
for (const p of projects) {
|
|
@@ -490,8 +486,8 @@ export function register(server, ctx) {
|
|
|
490
486
|
.filter((name) => name && !name.startsWith(".") && name !== "profiles")));
|
|
491
487
|
const commitMsg = message || `phren: save ${files.length} file(s) across ${projectNames.length} project(s)`;
|
|
492
488
|
runCustomHooks(phrenPath, "pre-save");
|
|
493
|
-
//
|
|
494
|
-
runGit(["add", "
|
|
489
|
+
// Stage all files including untracked (new project dirs, first FINDINGS.md, etc.)
|
|
490
|
+
runGit(["add", "-A"]);
|
|
495
491
|
runGit(["commit", "-m", commitMsg]);
|
|
496
492
|
let hasRemote = false;
|
|
497
493
|
try {
|
package/mcp/dist/mcp-graph.js
CHANGED
|
@@ -9,7 +9,7 @@ import { withFileLock } from "./shared-governance.js";
|
|
|
9
9
|
export function register(server, ctx) {
|
|
10
10
|
// ── search_fragments ──────────────────────────────────────────────────
|
|
11
11
|
server.registerTool("search_fragments", {
|
|
12
|
-
title: "phren
|
|
12
|
+
title: "◆ phren · search fragments",
|
|
13
13
|
description: "Search named fragments in the knowledge graph (libraries, tools, concepts mentioned in findings). " +
|
|
14
14
|
"Returns matching fragment names and how many findings reference each.",
|
|
15
15
|
inputSchema: z.object({
|
|
@@ -209,7 +209,7 @@ export function register(server, ctx) {
|
|
|
209
209
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [fragmentName, resolvedFragmentType, new Date().toISOString().slice(0, 10)]);
|
|
210
210
|
}
|
|
211
211
|
catch (err) {
|
|
212
|
-
if (process.env.PHREN_DEBUG
|
|
212
|
+
if (process.env.PHREN_DEBUG)
|
|
213
213
|
process.stderr.write(`[phren] link_findings fragmentInsert: ${errorMessage(err)}\n`);
|
|
214
214
|
}
|
|
215
215
|
const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [fragmentName, resolvedFragmentType]);
|
|
@@ -232,7 +232,7 @@ export function register(server, ctx) {
|
|
|
232
232
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
|
|
233
233
|
}
|
|
234
234
|
catch (err) {
|
|
235
|
-
if (process.env.PHREN_DEBUG
|
|
235
|
+
if (process.env.PHREN_DEBUG)
|
|
236
236
|
process.stderr.write(`[phren] link_findings docFragmentInsert: ${errorMessage(err)}\n`);
|
|
237
237
|
}
|
|
238
238
|
const docFragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
|
|
@@ -245,7 +245,7 @@ export function register(server, ctx) {
|
|
|
245
245
|
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, relType, sourceDoc]);
|
|
246
246
|
}
|
|
247
247
|
catch (err) {
|
|
248
|
-
if (process.env.PHREN_DEBUG
|
|
248
|
+
if (process.env.PHREN_DEBUG)
|
|
249
249
|
process.stderr.write(`[phren] link_findings linkInsert: ${errorMessage(err)}\n`);
|
|
250
250
|
return mcpResponse({ ok: false, error: "Failed to insert fragment link." });
|
|
251
251
|
}
|
|
@@ -255,7 +255,7 @@ export function register(server, ctx) {
|
|
|
255
255
|
db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [fragmentName, project, sourceDoc]);
|
|
256
256
|
}
|
|
257
257
|
catch (err) {
|
|
258
|
-
if (process.env.PHREN_DEBUG
|
|
258
|
+
if (process.env.PHREN_DEBUG)
|
|
259
259
|
process.stderr.write(`[phren] link_findings globalFragments: ${errorMessage(err)}\n`);
|
|
260
260
|
}
|
|
261
261
|
// 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation)
|
|
@@ -268,7 +268,7 @@ export function register(server, ctx) {
|
|
|
268
268
|
existing = JSON.parse(fs.readFileSync(manualLinksPath, "utf8"));
|
|
269
269
|
}
|
|
270
270
|
catch (err) {
|
|
271
|
-
if (process.env.PHREN_DEBUG
|
|
271
|
+
if (process.env.PHREN_DEBUG)
|
|
272
272
|
process.stderr.write(`[phren] link_findings manualLinksRead: ${errorMessage(err)}\n`);
|
|
273
273
|
}
|
|
274
274
|
}
|
package/mcp/dist/mcp-hooks.js
CHANGED
|
@@ -23,7 +23,7 @@ function validateHookCommand(command) {
|
|
|
23
23
|
return "Command too long (max 1000 characters).";
|
|
24
24
|
// Reject shell metacharacters that allow injection or arbitrary execution
|
|
25
25
|
// when the command is later run via `sh -c`.
|
|
26
|
-
if (/[`$(){}
|
|
26
|
+
if (/[`$(){}&|;<>\n\r#]/.test(trimmed)) {
|
|
27
27
|
return "Command contains disallowed shell characters: ` $ ( ) { } & | ; < >";
|
|
28
28
|
}
|
|
29
29
|
// eval and source can execute arbitrary code
|
package/mcp/dist/mcp-search.js
CHANGED
|
@@ -567,17 +567,12 @@ export function register(server, ctx) {
|
|
|
567
567
|
if (!isValidProjectName(project))
|
|
568
568
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
569
569
|
const includeHistory = include_history ?? include_superseded ?? false;
|
|
570
|
-
|
|
570
|
+
// Always read with archive so we can compute historyCount without a second read
|
|
571
|
+
const result = readFindings(phrenPath, project, { includeArchived: true });
|
|
571
572
|
if (!result.ok)
|
|
572
573
|
return mcpResponse({ ok: false, error: result.error });
|
|
573
574
|
const allItems = result.data;
|
|
574
|
-
|
|
575
|
-
if (!includeHistory) {
|
|
576
|
-
const withArchive = readFindings(phrenPath, project, { includeArchived: true });
|
|
577
|
-
if (withArchive.ok) {
|
|
578
|
-
historyCount = withArchive.data.filter(f => f.tier === "archived" || HISTORY_FINDING_STATUSES.has(f.status)).length;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
575
|
+
const historyCount = allItems.filter(f => f.tier === "archived" || HISTORY_FINDING_STATUSES.has(f.status)).length;
|
|
581
576
|
const visibleItems = includeHistory
|
|
582
577
|
? allItems
|
|
583
578
|
: allItems.filter(f => f.tier !== "archived" && !HISTORY_FINDING_STATUSES.has(f.status));
|
package/mcp/dist/mcp-session.js
CHANGED
|
@@ -90,6 +90,7 @@ function extractResumptionHint(summary, fallbackNextStep, fallbackLastAttempt) {
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
/** Per-connection session map keyed by arbitrary connection ID (if provided). */
|
|
93
|
+
const MAX_SESSION_MAP_ENTRIES = 200;
|
|
93
94
|
const _sessionMap = new Map();
|
|
94
95
|
function sessionsDir(phrenPath) {
|
|
95
96
|
const dir = path.join(phrenPath, ".runtime", "sessions");
|
|
@@ -151,7 +152,10 @@ function lastSummaryPath(phrenPath) {
|
|
|
151
152
|
function writeLastSummary(phrenPath, summary, sessionId, project) {
|
|
152
153
|
try {
|
|
153
154
|
const data = { summary, sessionId, project, endedAt: new Date().toISOString() };
|
|
154
|
-
|
|
155
|
+
const summaryFile = lastSummaryPath(phrenPath);
|
|
156
|
+
const tmpPath = `${summaryFile}.tmp-${crypto.randomUUID()}`;
|
|
157
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
158
|
+
fs.renameSync(tmpPath, summaryFile);
|
|
155
159
|
}
|
|
156
160
|
catch (err) {
|
|
157
161
|
debugError("writeLastSummary", err);
|
|
@@ -417,8 +421,14 @@ export function register(server, ctx) {
|
|
|
417
421
|
};
|
|
418
422
|
const newFile = sessionFileForId(phrenPath, sessionId);
|
|
419
423
|
writeSessionStateFile(newFile, next);
|
|
420
|
-
if (connectionId)
|
|
424
|
+
if (connectionId) {
|
|
421
425
|
_sessionMap.set(connectionId, sessionId);
|
|
426
|
+
if (_sessionMap.size > MAX_SESSION_MAP_ENTRIES) {
|
|
427
|
+
const oldest = _sessionMap.keys().next().value;
|
|
428
|
+
if (oldest !== undefined)
|
|
429
|
+
_sessionMap.delete(oldest);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
422
432
|
const parts = [];
|
|
423
433
|
if (priorSummary) {
|
|
424
434
|
parts.push(`## Last session\n${priorSummary}`);
|