@phren/cli 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -17
- 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 +58 -71
- package/mcp/dist/cli-config.js +337 -131
- package/mcp/dist/cli-extract.js +3 -2
- package/mcp/dist/cli-govern.js +35 -63
- package/mcp/dist/cli-graph.js +19 -4
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +4 -4
- package/mcp/dist/cli-hooks-session.js +1 -1
- package/mcp/dist/cli-hooks.js +44 -35
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/cli.js +1 -1
- package/mcp/dist/content-archive.js +23 -14
- package/mcp/dist/content-citation.js +13 -2
- package/mcp/dist/content-dedup.js +9 -9
- 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 +7 -8
- package/mcp/dist/entrypoint.js +133 -102
- 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 +12 -5
- package/mcp/dist/governance-policy.js +156 -9
- package/mcp/dist/governance-scores.js +4 -10
- package/mcp/dist/hooks.js +62 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +4 -25
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +6 -55
- package/mcp/dist/init-shared.js +53 -1
- package/mcp/dist/init.js +191 -29
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +2 -2
- package/mcp/dist/link-doctor.js +14 -57
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +16 -75
- 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 +12 -7
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +16 -20
- package/mcp/dist/mcp-graph.js +12 -12
- package/mcp/dist/mcp-hooks.js +1 -1
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +11 -16
- 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 +30 -5
- package/mcp/dist/memory-ui-scripts.js +252 -63
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-core.js +2 -0
- package/mcp/dist/phren-paths.js +8 -9
- package/mcp/dist/proactivity.js +5 -5
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +64 -17
- 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/session-utils.js +3 -2
- package/mcp/dist/shared-data-utils.js +28 -0
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-governance.js +1 -1
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +21 -23
- package/mcp/dist/shared-search-fallback.js +15 -25
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +5 -6
- package/mcp/dist/shell-entry.js +1 -1
- package/mcp/dist/shell-input.js +63 -53
- package/mcp/dist/shell-palette.js +6 -1
- package/mcp/dist/shell-render.js +9 -5
- package/mcp/dist/shell-state-store.js +2 -5
- package/mcp/dist/shell-view.js +7 -6
- package/mcp/dist/shell.js +5 -55
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +43 -21
- 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 +4 -4
- package/package.json +2 -3
- 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 +106 -0
- package/mcp/dist/cli-hooks-retrieval.js +0 -2
- package/mcp/dist/impact-scoring.js +0 -22
- package/mcp/dist/shared-paths.js +0 -1
|
@@ -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) {
|
|
@@ -24,7 +25,7 @@ export function readExtractedFacts(phrenPath, project) {
|
|
|
24
25
|
return Array.isArray(data) ? data : [];
|
|
25
26
|
}
|
|
26
27
|
catch (err) {
|
|
27
|
-
if ((process.env.PHREN_DEBUG
|
|
28
|
+
if ((process.env.PHREN_DEBUG))
|
|
28
29
|
process.stderr.write(`[phren] readExtractedFacts: ${errorMessage(err)}\n`);
|
|
29
30
|
return [];
|
|
30
31
|
}
|
|
@@ -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-extract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mcpResponse } from "./mcp-types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { isValidProjectName, safeProjectPath } from "./utils.js";
|
|
3
|
+
import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
|
|
4
4
|
import { addFindingsToFile } from "./shared-content.js";
|
|
5
5
|
import { checkOllamaAvailable, checkModelAvailable, generateText, getOllamaUrl, getExtractModel } from "./shared-ollama.js";
|
|
6
6
|
import { debugLog } from "./shared.js";
|
|
@@ -34,7 +34,7 @@ function parseFindings(raw) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
catch (err) {
|
|
37
|
-
debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${
|
|
37
|
+
debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${errorMessage(err)})`);
|
|
38
38
|
}
|
|
39
39
|
return [];
|
|
40
40
|
}
|
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 } : {}),
|
|
@@ -236,7 +232,7 @@ export function register(server, ctx) {
|
|
|
236
232
|
extraAnnotationsByFinding.push(conflicts.checked && conflicts.annotations.length > 0 ? conflicts.annotations : []);
|
|
237
233
|
}
|
|
238
234
|
catch (err) {
|
|
239
|
-
if ((process.env.PHREN_DEBUG
|
|
235
|
+
if ((process.env.PHREN_DEBUG))
|
|
240
236
|
process.stderr.write(`[phren] add_findings semanticConflict: ${errorMessage(err)}\n`);
|
|
241
237
|
extraAnnotationsByFinding.push([]);
|
|
242
238
|
}
|
|
@@ -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 {
|
|
@@ -499,7 +495,7 @@ export function register(server, ctx) {
|
|
|
499
495
|
hasRemote = remotes.length > 0;
|
|
500
496
|
}
|
|
501
497
|
catch (err) {
|
|
502
|
-
if ((process.env.PHREN_DEBUG
|
|
498
|
+
if ((process.env.PHREN_DEBUG))
|
|
503
499
|
process.stderr.write(`[phren] push_changes remoteCheck: ${errorMessage(err)}\n`);
|
|
504
500
|
}
|
|
505
501
|
if (!hasRemote) {
|
|
@@ -523,7 +519,7 @@ export function register(server, ctx) {
|
|
|
523
519
|
runGit(["pull", "--rebase", "--quiet"], { timeout: 15000 });
|
|
524
520
|
}
|
|
525
521
|
catch (pullErr) {
|
|
526
|
-
if ((process.env.PHREN_DEBUG
|
|
522
|
+
if ((process.env.PHREN_DEBUG))
|
|
527
523
|
process.stderr.write(`[phren] push_changes pullRebase: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}\n`);
|
|
528
524
|
const resolved = autoMergeConflicts(phrenPath);
|
|
529
525
|
if (resolved) {
|
|
@@ -534,13 +530,13 @@ export function register(server, ctx) {
|
|
|
534
530
|
});
|
|
535
531
|
}
|
|
536
532
|
catch (continueErr) {
|
|
537
|
-
if ((process.env.PHREN_DEBUG
|
|
533
|
+
if ((process.env.PHREN_DEBUG))
|
|
538
534
|
process.stderr.write(`[phren] push_changes rebaseContinue: ${continueErr instanceof Error ? continueErr.message : String(continueErr)}\n`);
|
|
539
535
|
try {
|
|
540
536
|
runGit(["rebase", "--abort"]);
|
|
541
537
|
}
|
|
542
538
|
catch (abortErr) {
|
|
543
|
-
if ((process.env.PHREN_DEBUG
|
|
539
|
+
if ((process.env.PHREN_DEBUG))
|
|
544
540
|
process.stderr.write(`[phren] push_changes rebaseAbort: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
|
|
545
541
|
}
|
|
546
542
|
break;
|
|
@@ -551,7 +547,7 @@ export function register(server, ctx) {
|
|
|
551
547
|
runGit(["rebase", "--abort"]);
|
|
552
548
|
}
|
|
553
549
|
catch (abortErr) {
|
|
554
|
-
if ((process.env.PHREN_DEBUG
|
|
550
|
+
if ((process.env.PHREN_DEBUG))
|
|
555
551
|
process.stderr.write(`[phren] push_changes rebaseAbort2: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
|
|
556
552
|
}
|
|
557
553
|
break;
|
package/mcp/dist/mcp-graph.js
CHANGED
|
@@ -2,14 +2,14 @@ import { mcpResponse } from "./mcp-types.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as crypto from "crypto";
|
|
5
|
-
import { isValidProjectName } from "./utils.js";
|
|
5
|
+
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
6
6
|
import { queryDocBySourceKey, queryRows, queryFragmentLinks, queryCrossProjectFragments, ensureGlobalEntitiesTable, logFragmentMiss } from "./shared-index.js";
|
|
7
7
|
import { runtimeFile } from "./shared.js";
|
|
8
8
|
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,8 +209,8 @@ 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
|
|
213
|
-
process.stderr.write(`[phren] link_findings fragmentInsert: ${
|
|
212
|
+
if (process.env.PHREN_DEBUG)
|
|
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]);
|
|
216
216
|
if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
|
|
@@ -232,8 +232,8 @@ 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
|
|
236
|
-
process.stderr.write(`[phren] link_findings docFragmentInsert: ${
|
|
235
|
+
if (process.env.PHREN_DEBUG)
|
|
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"]);
|
|
239
239
|
if (!docFragmentResult?.length || !docFragmentResult[0]?.values?.length) {
|
|
@@ -245,8 +245,8 @@ 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
|
|
249
|
-
process.stderr.write(`[phren] link_findings linkInsert: ${
|
|
248
|
+
if (process.env.PHREN_DEBUG)
|
|
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
|
}
|
|
252
252
|
// 4a. Also populate global_entities so manual links appear in cross_project_fragments
|
|
@@ -255,8 +255,8 @@ 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
|
|
259
|
-
process.stderr.write(`[phren] link_findings globalFragments: ${
|
|
258
|
+
if (process.env.PHREN_DEBUG)
|
|
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)
|
|
262
262
|
const manualLinksPath = runtimeFile(ctx.phrenPath, "manual-links.json");
|
|
@@ -268,8 +268,8 @@ 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
|
|
272
|
-
process.stderr.write(`[phren] link_findings manualLinksRead: ${
|
|
271
|
+
if (process.env.PHREN_DEBUG)
|
|
272
|
+
process.stderr.write(`[phren] link_findings manualLinksRead: ${errorMessage(err)}\n`);
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
const newEntry = { entity: fragmentName, entityType: resolvedFragmentType, sourceDoc, relType };
|
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-ops.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { runtimeFile, getProjectDirs } from "./shared.js";
|
|
6
6
|
import { findFtsCacheForPath } from "./shared-index.js";
|
|
7
|
-
import { isValidProjectName } from "./utils.js";
|
|
7
|
+
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
8
8
|
import { readReviewQueue, readReviewQueueAcrossProjects } from "./data-access.js";
|
|
9
9
|
import { addProjectFromPath } from "./core-project.js";
|
|
10
10
|
import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "./project-config.js";
|
|
@@ -44,7 +44,7 @@ export function register(server, ctx) {
|
|
|
44
44
|
catch (err) {
|
|
45
45
|
return mcpResponse({
|
|
46
46
|
ok: false,
|
|
47
|
-
error:
|
|
47
|
+
error: errorMessage(err),
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
});
|
|
@@ -108,8 +108,8 @@ export function register(server, ctx) {
|
|
|
108
108
|
version = pkg.version || "unknown";
|
|
109
109
|
}
|
|
110
110
|
catch (err) {
|
|
111
|
-
if ((process.env.PHREN_DEBUG
|
|
112
|
-
process.stderr.write(`[phren] healthCheck version: ${
|
|
111
|
+
if ((process.env.PHREN_DEBUG))
|
|
112
|
+
process.stderr.write(`[phren] healthCheck version: ${errorMessage(err)}\n`);
|
|
113
113
|
}
|
|
114
114
|
// FTS index (lives in /tmpphren-fts-*/, not .runtime/)
|
|
115
115
|
let indexStatus = { exists: false };
|
|
@@ -117,8 +117,8 @@ export function register(server, ctx) {
|
|
|
117
117
|
indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
|
|
118
118
|
}
|
|
119
119
|
catch (err) {
|
|
120
|
-
if ((process.env.PHREN_DEBUG
|
|
121
|
-
process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${
|
|
120
|
+
if ((process.env.PHREN_DEBUG))
|
|
121
|
+
process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${errorMessage(err)}\n`);
|
|
122
122
|
}
|
|
123
123
|
// Hook registration
|
|
124
124
|
let hooksEnabled = false;
|
|
@@ -127,8 +127,8 @@ export function register(server, ctx) {
|
|
|
127
127
|
hooksEnabled = getHooksEnabledPreference(phrenPath);
|
|
128
128
|
}
|
|
129
129
|
catch (err) {
|
|
130
|
-
if ((process.env.PHREN_DEBUG
|
|
131
|
-
process.stderr.write(`[phren] healthCheck hooksEnabled: ${
|
|
130
|
+
if ((process.env.PHREN_DEBUG))
|
|
131
|
+
process.stderr.write(`[phren] healthCheck hooksEnabled: ${errorMessage(err)}\n`);
|
|
132
132
|
}
|
|
133
133
|
let mcpEnabled = false;
|
|
134
134
|
try {
|
|
@@ -136,8 +136,8 @@ export function register(server, ctx) {
|
|
|
136
136
|
mcpEnabled = getMcpEnabledPreference(phrenPath);
|
|
137
137
|
}
|
|
138
138
|
catch (err) {
|
|
139
|
-
if ((process.env.PHREN_DEBUG
|
|
140
|
-
process.stderr.write(`[phren] healthCheck mcpEnabled: ${
|
|
139
|
+
if ((process.env.PHREN_DEBUG))
|
|
140
|
+
process.stderr.write(`[phren] healthCheck mcpEnabled: ${errorMessage(err)}\n`);
|
|
141
141
|
}
|
|
142
142
|
// Profile/machine info
|
|
143
143
|
const machineName = (() => {
|
|
@@ -145,8 +145,8 @@ export function register(server, ctx) {
|
|
|
145
145
|
return getMachineName();
|
|
146
146
|
}
|
|
147
147
|
catch (err) {
|
|
148
|
-
if ((process.env.PHREN_DEBUG
|
|
149
|
-
process.stderr.write(`[phren] healthCheck machineName: ${
|
|
148
|
+
if ((process.env.PHREN_DEBUG))
|
|
149
|
+
process.stderr.write(`[phren] healthCheck machineName: ${errorMessage(err)}\n`);
|
|
150
150
|
}
|
|
151
151
|
return undefined;
|
|
152
152
|
})();
|
|
@@ -160,8 +160,8 @@ export function register(server, ctx) {
|
|
|
160
160
|
taskMode = workflowPolicy.taskMode;
|
|
161
161
|
}
|
|
162
162
|
catch (err) {
|
|
163
|
-
if ((process.env.PHREN_DEBUG
|
|
164
|
-
process.stderr.write(`[phren] healthCheck taskMode: ${
|
|
163
|
+
if ((process.env.PHREN_DEBUG))
|
|
164
|
+
process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
|
|
165
165
|
}
|
|
166
166
|
try {
|
|
167
167
|
const { readInstallPreferences } = await import("./init-preferences.js");
|
|
@@ -169,8 +169,8 @@ export function register(server, ctx) {
|
|
|
169
169
|
proactivity = prefs.proactivity || "high";
|
|
170
170
|
}
|
|
171
171
|
catch (err) {
|
|
172
|
-
if ((process.env.PHREN_DEBUG
|
|
173
|
-
process.stderr.write(`[phren] healthCheck proactivity: ${
|
|
172
|
+
if ((process.env.PHREN_DEBUG))
|
|
173
|
+
process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
|
|
174
174
|
}
|
|
175
175
|
const lines = [
|
|
176
176
|
`Phren v${version}`,
|
|
@@ -262,8 +262,8 @@ export function register(server, ctx) {
|
|
|
262
262
|
return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
|
|
263
263
|
}
|
|
264
264
|
catch (err) {
|
|
265
|
-
if ((process.env.PHREN_DEBUG
|
|
266
|
-
process.stderr.write(`[phren] readErrorLines: ${
|
|
265
|
+
if ((process.env.PHREN_DEBUG))
|
|
266
|
+
process.stderr.write(`[phren] readErrorLines: ${errorMessage(err)}\n`);
|
|
267
267
|
return [];
|
|
268
268
|
}
|
|
269
269
|
}
|
package/mcp/dist/mcp-search.js
CHANGED
|
@@ -35,8 +35,8 @@ export function logSearchMiss(phrenPath, query, project) {
|
|
|
35
35
|
fs.appendFileSync(missFile, entry + "\n");
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
|
-
if ((process.env.PHREN_DEBUG
|
|
39
|
-
process.stderr.write(`[phren] logSearchMiss: ${
|
|
38
|
+
if ((process.env.PHREN_DEBUG))
|
|
39
|
+
process.stderr.write(`[phren] logSearchMiss: ${errorMessage(err)}\n`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
const HISTORY_FINDING_STATUSES = new Set(["superseded", "retracted"]);
|
|
@@ -160,7 +160,7 @@ export function register(server, ctx) {
|
|
|
160
160
|
createdAt = stat.birthtime.toISOString();
|
|
161
161
|
}
|
|
162
162
|
catch (err) {
|
|
163
|
-
if ((process.env.PHREN_DEBUG
|
|
163
|
+
if ((process.env.PHREN_DEBUG))
|
|
164
164
|
process.stderr.write(`[phren] search_knowledge statFile: ${errorMessage(err)}\n`);
|
|
165
165
|
}
|
|
166
166
|
// Extract tags from content (e.g. [decision], [pitfall], [pattern])
|
|
@@ -393,8 +393,8 @@ export function register(server, ctx) {
|
|
|
393
393
|
relatedFragments = [...new Set(relatedFragments)].slice(0, 10);
|
|
394
394
|
}
|
|
395
395
|
catch (err) {
|
|
396
|
-
if ((process.env.PHREN_DEBUG
|
|
397
|
-
process.stderr.write(`[phren] fragment query: ${
|
|
396
|
+
if ((process.env.PHREN_DEBUG))
|
|
397
|
+
process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
|
|
398
398
|
}
|
|
399
399
|
const formatted = results.map((r) => `### ${r.project}/${r.filename} (${r.type})\n${r.snippet}\n\n\`${r.path}\``);
|
|
400
400
|
// Memory synthesis: generate a concise paragraph from top results when requested
|
|
@@ -408,7 +408,7 @@ export function register(server, ctx) {
|
|
|
408
408
|
synthCache = JSON.parse(fs.readFileSync(synthCachePath, "utf8"));
|
|
409
409
|
}
|
|
410
410
|
catch (err) {
|
|
411
|
-
if ((process.env.PHREN_DEBUG
|
|
411
|
+
if ((process.env.PHREN_DEBUG))
|
|
412
412
|
process.stderr.write(`[phren] search_knowledge synthCacheRead: ${errorMessage(err)}\n`);
|
|
413
413
|
}
|
|
414
414
|
const cached = synthCache[synthKey];
|
|
@@ -433,8 +433,8 @@ export function register(server, ctx) {
|
|
|
433
433
|
fs.writeFileSync(synthCachePath, JSON.stringify(synthCache));
|
|
434
434
|
}
|
|
435
435
|
catch (err) {
|
|
436
|
-
if ((process.env.PHREN_DEBUG
|
|
437
|
-
process.stderr.write(`[phren] synthCache write: ${
|
|
436
|
+
if ((process.env.PHREN_DEBUG))
|
|
437
|
+
process.stderr.write(`[phren] synthCache write: ${errorMessage(err)}\n`);
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
}
|
|
@@ -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}`);
|