@phren/cli 0.0.11 → 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 +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/data-access.js
CHANGED
|
@@ -2,38 +2,17 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as yaml from "js-yaml";
|
|
4
4
|
import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, isRecord, } from "./shared.js";
|
|
5
|
-
import { normalizeQueueEntryText,
|
|
5
|
+
import { normalizeQueueEntryText, } from "./shared-governance.js";
|
|
6
6
|
import { addFindingToFile, } from "./shared-content.js";
|
|
7
|
-
import { isValidProjectName, queueFilePath, safeProjectPath
|
|
7
|
+
import { isValidProjectName, queueFilePath, safeProjectPath } from "./utils.js";
|
|
8
8
|
import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
|
|
9
9
|
import { parseFindingLifecycle, } from "./finding-lifecycle.js";
|
|
10
|
-
import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
|
|
10
|
+
import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, normalizeFindingText, } from "./content-metadata.js";
|
|
11
|
+
import { withSafeLock, ensureProject } from "./shared-data-utils.js";
|
|
11
12
|
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, removeTasks, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
12
13
|
export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
|
|
13
|
-
export { loadShellState,
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
return withFileLockRaw(filePath, fn);
|
|
17
|
-
}
|
|
18
|
-
catch (err) {
|
|
19
|
-
const msg = errorMessage(err);
|
|
20
|
-
if (msg.includes("could not acquire lock")) {
|
|
21
|
-
return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
|
|
22
|
-
}
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function ensureProject(phrenPath, project) {
|
|
27
|
-
if (!isValidProjectName(project))
|
|
28
|
-
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
29
|
-
const dir = safeProjectPath(phrenPath, project);
|
|
30
|
-
if (!dir)
|
|
31
|
-
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
32
|
-
if (!fs.existsSync(dir)) {
|
|
33
|
-
return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
|
|
34
|
-
}
|
|
35
|
-
return phrenOk(dir);
|
|
36
|
-
}
|
|
14
|
+
export { loadShellState, resetShellState, saveShellState, } from "./shell-state-store.js";
|
|
15
|
+
export { getRuntimeHealth as readRuntimeHealth } from "./shared-governance.js";
|
|
37
16
|
function extractDateHeading(line) {
|
|
38
17
|
const heading = line.match(/^##\s+(.+)$/);
|
|
39
18
|
if (!heading)
|
|
@@ -79,8 +58,8 @@ function findMatchingFindingBullet(bulletLines, needle, match) {
|
|
|
79
58
|
const fidMatch = /^[a-z0-9]{8}$/.test(fidNeedle)
|
|
80
59
|
? bulletLines.filter(({ line }) => new RegExp(`<!--\\s*fid:${fidNeedle}\\s*-->`).test(line))
|
|
81
60
|
: [];
|
|
82
|
-
const exactMatches = bulletLines.filter(({ line }) => line
|
|
83
|
-
const partialMatches = bulletLines.filter(({ line }) => line
|
|
61
|
+
const exactMatches = bulletLines.filter(({ line }) => normalizeFindingText(line) === needle);
|
|
62
|
+
const partialMatches = bulletLines.filter(({ line }) => normalizeFindingText(line).includes(needle));
|
|
84
63
|
if (fidMatch.length === 1)
|
|
85
64
|
return { kind: "found", idx: fidMatch[0].i };
|
|
86
65
|
if (exactMatches.length === 1)
|
|
@@ -276,7 +255,7 @@ export function removeFinding(phrenPath, project, match) {
|
|
|
276
255
|
return phrenErr(`No FINDINGS.md file found for "${project}". Add a finding first with add_finding or :find add.`, PhrenError.FILE_NOT_FOUND);
|
|
277
256
|
return withSafeLock(filePath, () => {
|
|
278
257
|
const lines = fs.readFileSync(filePath, "utf8").split("\n");
|
|
279
|
-
const needle = match
|
|
258
|
+
const needle = normalizeFindingText(match);
|
|
280
259
|
const bulletLines = collectFindingBulletLines(lines);
|
|
281
260
|
const activeMatch = findMatchingFindingBullet(bulletLines.filter(({ archived }) => !archived), needle, match);
|
|
282
261
|
if (activeMatch.kind === "ambiguous") {
|
|
@@ -313,7 +292,7 @@ export function editFinding(phrenPath, project, oldText, newText) {
|
|
|
313
292
|
return phrenErr(`No FINDINGS.md file found for "${project}".`, PhrenError.FILE_NOT_FOUND);
|
|
314
293
|
return withSafeLock(findingsPath, () => {
|
|
315
294
|
const lines = fs.readFileSync(findingsPath, "utf8").split("\n");
|
|
316
|
-
const needle = oldText
|
|
295
|
+
const needle = normalizeFindingText(oldText);
|
|
317
296
|
const bulletLines = collectFindingBulletLines(lines);
|
|
318
297
|
const activeMatch = findMatchingFindingBullet(bulletLines.filter(({ archived }) => !archived), needle, oldText);
|
|
319
298
|
if (activeMatch.kind === "ambiguous") {
|
package/mcp/dist/data-tasks.js
CHANGED
|
@@ -2,21 +2,9 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { randomBytes, randomUUID } from "crypto";
|
|
4
4
|
import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, } from "./shared.js";
|
|
5
|
-
import { withFileLock as withFileLockRaw } from "./shared-governance.js";
|
|
6
5
|
import { validateTaskFormat } from "./shared-content.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
return withFileLockRaw(filePath, fn);
|
|
11
|
-
}
|
|
12
|
-
catch (err) {
|
|
13
|
-
const msg = errorMessage(err);
|
|
14
|
-
if (msg.includes("could not acquire lock")) {
|
|
15
|
-
return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
|
|
16
|
-
}
|
|
17
|
-
throw err;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
6
|
+
import { safeProjectPath } from "./utils.js";
|
|
7
|
+
import { withSafeLock, ensureProject } from "./shared-data-utils.js";
|
|
20
8
|
const ACTIVE_HEADINGS = new Set(["active", "in progress", "in-progress", "current", "wip"]);
|
|
21
9
|
const QUEUE_HEADINGS = new Set(["queue", "queued", "task", "todo", "upcoming", "next"]);
|
|
22
10
|
const DONE_HEADINGS = new Set(["done", "completed", "finished", "archived"]);
|
|
@@ -108,17 +96,6 @@ function parseContinuation(lines, idx) {
|
|
|
108
96
|
}
|
|
109
97
|
return { context, githubIssue, githubUrl, linesToSkip };
|
|
110
98
|
}
|
|
111
|
-
function ensureProject(phrenPath, project) {
|
|
112
|
-
if (!isValidProjectName(project))
|
|
113
|
-
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
114
|
-
const dir = safeProjectPath(phrenPath, project);
|
|
115
|
-
if (!dir)
|
|
116
|
-
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
117
|
-
if (!fs.existsSync(dir)) {
|
|
118
|
-
return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
|
|
119
|
-
}
|
|
120
|
-
return phrenOk(dir);
|
|
121
|
-
}
|
|
122
99
|
/** Pattern that matches the task metadata comment embedded in task item lines.
|
|
123
100
|
* Format: <!-- bid:HASH [rank:N] [lastActivity:ISO] -->
|
|
124
101
|
*/
|
|
@@ -808,7 +785,9 @@ export function tidyDoneTasks(phrenPath, project, keep = 30, dryRun) {
|
|
|
808
785
|
const lines = archived.map((item) => `- [x] ${item.line}${item.context ? `\n Context: ${item.context}` : ""}`);
|
|
809
786
|
const block = `## ${stamp}\n\n${lines.join("\n")}\n\n`;
|
|
810
787
|
const prior = fs.existsSync(archiveFile) ? fs.readFileSync(archiveFile, "utf8") : `# ${project} tasks archive\n\n`;
|
|
811
|
-
|
|
788
|
+
const tmpPath = `${archiveFile}.tmp-${randomUUID()}`;
|
|
789
|
+
fs.writeFileSync(tmpPath, prior + block);
|
|
790
|
+
fs.renameSync(tmpPath, archiveFile);
|
|
812
791
|
writeTaskDoc(parsed.data);
|
|
813
792
|
return phrenOk(`Tidied ${project}: archived ${archived.length} done item(s), kept ${safeKeep}.`);
|
|
814
793
|
});
|
package/mcp/dist/embedding.js
CHANGED
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -9,6 +9,7 @@ const HELP_TEXT = `phren - persistent knowledge for your agents
|
|
|
9
9
|
|
|
10
10
|
phren Interactive shell
|
|
11
11
|
phren init Set up phren
|
|
12
|
+
phren quickstart Quick setup: init + project scaffold
|
|
12
13
|
phren add [path] Register a project
|
|
13
14
|
phren search <query> Search what phren knows
|
|
14
15
|
phren status Health check
|
|
@@ -36,6 +37,8 @@ const HELP_TOPICS = {
|
|
|
36
37
|
phren skills resolve <project|global> Print resolved skill manifest
|
|
37
38
|
phren skills doctor <project|global> Diagnose skill visibility
|
|
38
39
|
phren skills sync <project|global> Regenerate skill mirror
|
|
40
|
+
phren skills enable <project|global> <name> Enable a disabled skill
|
|
41
|
+
phren skills disable <project|global> <name> Disable a skill without deleting
|
|
39
42
|
phren skills remove <project> <name> Remove a skill
|
|
40
43
|
phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
|
|
41
44
|
`,
|
|
@@ -109,6 +112,7 @@ function buildFullHelp() {
|
|
|
109
112
|
Usage:
|
|
110
113
|
phren Interactive shell
|
|
111
114
|
phren init Set up phren
|
|
115
|
+
phren quickstart Quick setup: init + project scaffold
|
|
112
116
|
phren add [path] Register a project
|
|
113
117
|
phren search <query> Search what phren knows
|
|
114
118
|
phren status Health check
|
|
@@ -2,19 +2,11 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import { impactLogFile } from "./shared.js";
|
|
4
4
|
import { withFileLock } from "./shared-governance.js";
|
|
5
|
+
import { normalizeFindingText } from "./content-metadata.js";
|
|
5
6
|
let highImpactCache = null;
|
|
6
7
|
function nowIso() {
|
|
7
8
|
return new Date().toISOString();
|
|
8
9
|
}
|
|
9
|
-
function normalizeFindingText(raw) {
|
|
10
|
-
return raw
|
|
11
|
-
.replace(/^-\s+/, "")
|
|
12
|
-
.replace(/<!--.*?-->/g, " ")
|
|
13
|
-
.replace(/\[confidence\s+[01](?:\.\d+)?\]/gi, " ")
|
|
14
|
-
.replace(/\s+/g, " ")
|
|
15
|
-
.trim()
|
|
16
|
-
.toLowerCase();
|
|
17
|
-
}
|
|
18
10
|
export function findingIdFromLine(line) {
|
|
19
11
|
const fid = line.match(/<!--\s*fid:([a-z0-9]{8})\s*-->/i);
|
|
20
12
|
if (fid?.[1])
|
|
@@ -144,29 +136,6 @@ export function getHighImpactFindings(phrenPath, minSurfaceCount = 3) {
|
|
|
144
136
|
};
|
|
145
137
|
return new Set(ids);
|
|
146
138
|
}
|
|
147
|
-
export function getImpactSurfaceCounts(phrenPath, minSurfaces = 1) {
|
|
148
|
-
const file = impactLogFile(phrenPath);
|
|
149
|
-
if (!fs.existsSync(file))
|
|
150
|
-
return new Map();
|
|
151
|
-
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
152
|
-
const counts = new Map();
|
|
153
|
-
for (const line of lines) {
|
|
154
|
-
try {
|
|
155
|
-
const entry = JSON.parse(line);
|
|
156
|
-
if (entry.findingId) {
|
|
157
|
-
counts.set(entry.findingId, (counts.get(entry.findingId) ?? 0) + 1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch { }
|
|
161
|
-
}
|
|
162
|
-
// Filter by minimum
|
|
163
|
-
const filtered = new Map();
|
|
164
|
-
for (const [id, count] of counts) {
|
|
165
|
-
if (count >= minSurfaces)
|
|
166
|
-
filtered.set(id, count);
|
|
167
|
-
}
|
|
168
|
-
return filtered;
|
|
169
|
-
}
|
|
170
139
|
export function markImpactEntriesCompletedForSession(phrenPath, sessionId, project) {
|
|
171
140
|
if (!sessionId)
|
|
172
141
|
return 0;
|
|
@@ -105,7 +105,7 @@ export function compactFindingJournals(phrenPath, project) {
|
|
|
105
105
|
result.failed += 1;
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
108
|
-
if (
|
|
108
|
+
if (write.data.status === "skipped")
|
|
109
109
|
result.skipped += 1;
|
|
110
110
|
else
|
|
111
111
|
result.added += 1;
|
|
@@ -3,9 +3,9 @@ import * as path from "path";
|
|
|
3
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 "./governance
|
|
6
|
+
import { withFileLock } from "./shared-governance.js";
|
|
7
7
|
import { isValidProjectName, safeProjectPath } from "./utils.js";
|
|
8
|
-
import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, } from "./content-metadata.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
|
|
@@ -101,11 +101,6 @@ function findingTextFromLine(line) {
|
|
|
101
101
|
.replace(/<!--.*?-->/g, "")
|
|
102
102
|
.trim();
|
|
103
103
|
}
|
|
104
|
-
function normalizeFindingText(value) {
|
|
105
|
-
return findingTextFromLine(value)
|
|
106
|
-
.replace(/\s+/g, " ")
|
|
107
|
-
.toLowerCase();
|
|
108
|
-
}
|
|
109
104
|
function removeRelationComments(line) {
|
|
110
105
|
return stripRelationMetadata(line);
|
|
111
106
|
}
|
|
@@ -100,3 +100,9 @@ export function withFileLock(filePath, fn) {
|
|
|
100
100
|
releaseFileLock(lockPath);
|
|
101
101
|
return result;
|
|
102
102
|
}
|
|
103
|
+
export function isFiniteNumber(value) {
|
|
104
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
105
|
+
}
|
|
106
|
+
export function hasValidSchemaVersion(data) {
|
|
107
|
+
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
108
|
+
}
|
|
@@ -2,7 +2,7 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "./shared.js";
|
|
5
|
-
import { withFileLock } from "./governance
|
|
5
|
+
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
|
|
6
6
|
import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
|
|
7
7
|
import { readProjectConfig } from "./project-config.js";
|
|
8
8
|
import { runCustomHooks } from "./hooks.js";
|
|
@@ -44,15 +44,9 @@ function governanceDir(phrenPath) {
|
|
|
44
44
|
function govFile(phrenPath, schema) {
|
|
45
45
|
return path.join(governanceDir(phrenPath), GOVERNANCE_REGISTRY[schema].file);
|
|
46
46
|
}
|
|
47
|
-
function hasValidSchemaVersion(data) {
|
|
48
|
-
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
49
|
-
}
|
|
50
47
|
function isStringArray(value) {
|
|
51
48
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
52
49
|
}
|
|
53
|
-
function isFiniteNumber(value) {
|
|
54
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
55
|
-
}
|
|
56
50
|
function pickNumber(value, fallback) {
|
|
57
51
|
return isFiniteNumber(value) ? value : fallback;
|
|
58
52
|
}
|
|
@@ -2,7 +2,7 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "./shared.js";
|
|
5
|
-
import { withFileLock } from "./governance
|
|
5
|
+
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
|
|
6
6
|
import { errorMessage } from "./utils.js";
|
|
7
7
|
const GOVERNANCE_SCHEMA_VERSION = 1;
|
|
8
8
|
const DEFAULT_MEMORY_SCORES_FILE = {
|
|
@@ -15,12 +15,6 @@ function usageLogFile(phrenPath) {
|
|
|
15
15
|
function scoresJournalFile(phrenPath) {
|
|
16
16
|
return runtimeFile(phrenPath, "scores.jsonl");
|
|
17
17
|
}
|
|
18
|
-
function hasValidSchemaVersion(data) {
|
|
19
|
-
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
20
|
-
}
|
|
21
|
-
function isFiniteNumber(value) {
|
|
22
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
23
|
-
}
|
|
24
18
|
function isEntryScore(value) {
|
|
25
19
|
if (!isRecord(value))
|
|
26
20
|
return false;
|
package/mcp/dist/hooks.js
CHANGED
|
@@ -268,6 +268,21 @@ const VALID_HOOK_EVENTS = new Set(HOOK_EVENT_VALUES);
|
|
|
268
268
|
export function getHookTarget(h) {
|
|
269
269
|
return "webhook" in h ? h.webhook : h.command;
|
|
270
270
|
}
|
|
271
|
+
/** Re-validate a command hook at execution time (mirrors mcp-hooks.ts validateHookCommand). */
|
|
272
|
+
function validateCommandAtExecution(command) {
|
|
273
|
+
const trimmed = command.trim();
|
|
274
|
+
if (!trimmed)
|
|
275
|
+
return "Command cannot be empty.";
|
|
276
|
+
if (trimmed.length > 1000)
|
|
277
|
+
return "Command too long (max 1000 characters).";
|
|
278
|
+
if (/[`$(){}&|;<>]/.test(trimmed))
|
|
279
|
+
return "Command contains disallowed shell characters.";
|
|
280
|
+
if (/\b(eval|source)\b/.test(trimmed))
|
|
281
|
+
return "eval and source are not permitted in hook commands.";
|
|
282
|
+
if (!/^[\w./~"'"]/.test(trimmed))
|
|
283
|
+
return "Command must begin with an executable name or path.";
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
271
286
|
const DEFAULT_CUSTOM_HOOK_TIMEOUT = 5000;
|
|
272
287
|
const HOOK_TIMEOUT_MS = parseInt(process.env.PHREN_HOOK_TIMEOUT_MS || '14000', 10);
|
|
273
288
|
const HOOK_ERROR_LOG_MAX_LINES = 1000;
|
|
@@ -342,6 +357,14 @@ export function runCustomHooks(phrenPath, event, env = {}) {
|
|
|
342
357
|
});
|
|
343
358
|
continue;
|
|
344
359
|
}
|
|
360
|
+
const cmdErr = validateCommandAtExecution(hook.command);
|
|
361
|
+
if (cmdErr) {
|
|
362
|
+
const message = `${event}: skipped hook (re-validation failed): ${cmdErr}`;
|
|
363
|
+
debugLog(`runCustomHooks: ${message}`);
|
|
364
|
+
errors.push({ code: PhrenError.VALIDATION_ERROR, message });
|
|
365
|
+
appendHookErrorLog(phrenPath, event, message);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
345
368
|
const shellArgs = isWindows ? ["/c", hook.command] : ["-c", hook.command];
|
|
346
369
|
try {
|
|
347
370
|
execFileSync(shellCmd, shellArgs, {
|
package/mcp/dist/init-config.js
CHANGED
|
@@ -207,7 +207,7 @@ export function configureClaude(phrenPath, opts = {}) {
|
|
|
207
207
|
eventHooks.push({ matcher: "", hooks: [hookBody] });
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
|
-
const toolHookEnabled = hooksEnabled &&
|
|
210
|
+
const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false);
|
|
211
211
|
if (hooksEnabled) {
|
|
212
212
|
upsertPhrenHook("UserPromptSubmit", {
|
|
213
213
|
type: "command",
|
|
@@ -6,7 +6,7 @@ import * as path from "path";
|
|
|
6
6
|
import * as crypto from "crypto";
|
|
7
7
|
import { debugLog, installPreferencesFile } from "./phren-paths.js";
|
|
8
8
|
import { errorMessage } from "./utils.js";
|
|
9
|
-
import { withFileLock } from "./governance
|
|
9
|
+
import { withFileLock } from "./shared-governance.js";
|
|
10
10
|
function preferencesFile(phrenPath) {
|
|
11
11
|
return installPreferencesFile(phrenPath);
|
|
12
12
|
}
|
package/mcp/dist/init-setup.js
CHANGED
|
@@ -11,7 +11,7 @@ import { getMachineName } from "./machine-identity.js";
|
|
|
11
11
|
import { execFileSync } from "child_process";
|
|
12
12
|
import { GOVERNANCE_SCHEMA_VERSION, } from "./shared-governance.js";
|
|
13
13
|
import { STOP_WORDS, errorMessage } from "./utils.js";
|
|
14
|
-
import { ROOT, STARTER_DIR, VERSION, resolveEntryScript } from "./init-shared.js";
|
|
14
|
+
import { ROOT, STARTER_DIR, VERSION, resolveEntryScript, commandVersion, versionAtLeast, nearestWritableTarget } from "./init-shared.js";
|
|
15
15
|
import { readInstallPreferences } from "./init-preferences.js";
|
|
16
16
|
import { TASKS_FILENAME } from "./data-tasks.js";
|
|
17
17
|
import { getProjectOwnershipDefault, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, } from "./project-config.js";
|
|
@@ -311,21 +311,6 @@ export function getVerifyOutcomeNote(phrenPath, checks) {
|
|
|
311
311
|
}
|
|
312
312
|
return "Some reported issues are optional for your chosen install mode; review git-remote / MCP failures separately from hard failures.";
|
|
313
313
|
}
|
|
314
|
-
function commandVersion(cmd, args = ["--version"]) {
|
|
315
|
-
const effectiveCmd = process.platform === "win32" && (cmd === "npm" || cmd === "npx") ? `${cmd}.cmd` : cmd;
|
|
316
|
-
try {
|
|
317
|
-
return execFileSync(effectiveCmd, args, {
|
|
318
|
-
encoding: "utf8",
|
|
319
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
320
|
-
shell: process.platform === "win32" && effectiveCmd.endsWith(".cmd"),
|
|
321
|
-
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
322
|
-
}).trim();
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
debugLog(`commandVersion ${effectiveCmd} failed: ${errorMessage(err)}`);
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
314
|
export function getHookEntrypointCheck(deps = {}) {
|
|
330
315
|
const pathExists = deps.pathExists ?? fs.existsSync;
|
|
331
316
|
const versionReader = deps.versionReader ?? commandVersion;
|
|
@@ -344,40 +329,6 @@ export function getHookEntrypointCheck(deps = {}) {
|
|
|
344
329
|
fix: hookEntrypointOk ? undefined : "Rebuild phren: `npm run build` or reinstall the package, and ensure npm/npx is available for hook fallbacks",
|
|
345
330
|
};
|
|
346
331
|
}
|
|
347
|
-
function parseSemverTriple(raw) {
|
|
348
|
-
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
349
|
-
if (!match)
|
|
350
|
-
return null;
|
|
351
|
-
return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
|
|
352
|
-
}
|
|
353
|
-
function versionAtLeast(raw, major, minor = 0) {
|
|
354
|
-
if (!raw)
|
|
355
|
-
return false;
|
|
356
|
-
const parsed = parseSemverTriple(raw);
|
|
357
|
-
if (!parsed)
|
|
358
|
-
return false;
|
|
359
|
-
const [m, n] = parsed;
|
|
360
|
-
if (m !== major)
|
|
361
|
-
return m > major;
|
|
362
|
-
return n >= minor;
|
|
363
|
-
}
|
|
364
|
-
function nearestWritableTarget(filePath) {
|
|
365
|
-
let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
|
|
366
|
-
while (!fs.existsSync(probe)) {
|
|
367
|
-
const parent = path.dirname(probe);
|
|
368
|
-
if (parent === probe)
|
|
369
|
-
return false;
|
|
370
|
-
probe = parent;
|
|
371
|
-
}
|
|
372
|
-
try {
|
|
373
|
-
fs.accessSync(probe, fs.constants.W_OK);
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
debugLog(`nearestWritableTarget failed for ${filePath}: ${errorMessage(err)}`);
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
332
|
function gitRemoteStatus(phrenPath) {
|
|
382
333
|
try {
|
|
383
334
|
execFileSync("git", ["-C", phrenPath, "rev-parse", "--is-inside-work-tree"], {
|
package/mcp/dist/init-shared.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Shared constants and utilities for init modules.
|
|
3
3
|
* Kept separate to avoid circular dependencies between init-config and init-setup.
|
|
4
4
|
*/
|
|
5
|
+
import * as fs from "fs";
|
|
5
6
|
import * as path from "path";
|
|
6
|
-
import {
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { homePath, EXEC_TIMEOUT_QUICK_MS, debugLog } from "./shared.js";
|
|
9
|
+
import { errorMessage } from "./utils.js";
|
|
7
10
|
import { ROOT as PACKAGE_ROOT, VERSION } from "./package-metadata.js";
|
|
8
11
|
export const ROOT = PACKAGE_ROOT;
|
|
9
12
|
export { VERSION };
|
|
@@ -15,6 +18,55 @@ export function resolveEntryScript() {
|
|
|
15
18
|
export function log(msg) {
|
|
16
19
|
process.stdout.write(msg + "\n");
|
|
17
20
|
}
|
|
21
|
+
export function commandVersion(cmd, args = ["--version"]) {
|
|
22
|
+
const effectiveCmd = process.platform === "win32" && (cmd === "npm" || cmd === "npx") ? `${cmd}.cmd` : cmd;
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync(effectiveCmd, args, {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
shell: process.platform === "win32" && effectiveCmd.endsWith(".cmd"),
|
|
28
|
+
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
debugLog(`commandVersion ${effectiveCmd} failed: ${errorMessage(err)}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function parseSemverTriple(raw) {
|
|
37
|
+
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
38
|
+
if (!match)
|
|
39
|
+
return null;
|
|
40
|
+
return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
|
|
41
|
+
}
|
|
42
|
+
export function versionAtLeast(raw, major, minor = 0) {
|
|
43
|
+
if (!raw)
|
|
44
|
+
return false;
|
|
45
|
+
const parsed = parseSemverTriple(raw);
|
|
46
|
+
if (!parsed)
|
|
47
|
+
return false;
|
|
48
|
+
const [m, n] = parsed;
|
|
49
|
+
if (m !== major)
|
|
50
|
+
return m > major;
|
|
51
|
+
return n >= minor;
|
|
52
|
+
}
|
|
53
|
+
export function nearestWritableTarget(filePath) {
|
|
54
|
+
let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
|
|
55
|
+
while (!fs.existsSync(probe)) {
|
|
56
|
+
const parent = path.dirname(probe);
|
|
57
|
+
if (parent === probe)
|
|
58
|
+
return false;
|
|
59
|
+
probe = parent;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
fs.accessSync(probe, fs.constants.W_OK);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
debugLog(`nearestWritableTarget failed for ${filePath}: ${errorMessage(err)}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
18
70
|
export async function confirmPrompt(message) {
|
|
19
71
|
if (process.env.CI === "true" || !process.stdin.isTTY)
|
|
20
72
|
return true;
|
package/mcp/dist/init.js
CHANGED
|
@@ -845,9 +845,11 @@ async function runProjectLocalInit(opts = {}) {
|
|
|
845
845
|
* Configure MCP for all detected AI coding tools (Claude, VS Code, Cursor, Copilot, Codex).
|
|
846
846
|
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
847
847
|
*/
|
|
848
|
-
function configureMcpTargets(phrenPath, opts, verb) {
|
|
848
|
+
export function configureMcpTargets(phrenPath, opts, verb = "Configured") {
|
|
849
|
+
let claudeStatus = "no_settings";
|
|
849
850
|
try {
|
|
850
851
|
const status = configureClaude(phrenPath, { mcpEnabled: opts.mcpEnabled, hooksEnabled: opts.hooksEnabled });
|
|
852
|
+
claudeStatus = status ?? "installed";
|
|
851
853
|
if (status === "disabled" || status === "already_disabled") {
|
|
852
854
|
log(` ${verb} Claude Code hooks (MCP disabled)`);
|
|
853
855
|
}
|
|
@@ -858,31 +860,44 @@ function configureMcpTargets(phrenPath, opts, verb) {
|
|
|
858
860
|
catch (e) {
|
|
859
861
|
log(` Could not configure Claude Code settings (${e}), add manually`);
|
|
860
862
|
}
|
|
863
|
+
let vsStatus = "no_vscode";
|
|
861
864
|
try {
|
|
862
|
-
|
|
863
|
-
logMcpTargetStatus("VS Code",
|
|
865
|
+
vsStatus = configureVSCode(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_vscode";
|
|
866
|
+
logMcpTargetStatus("VS Code", vsStatus, verb);
|
|
864
867
|
}
|
|
865
868
|
catch (err) {
|
|
866
869
|
debugLog(`configureVSCode failed: ${errorMessage(err)}`);
|
|
867
870
|
}
|
|
871
|
+
let cursorStatus = "no_cursor";
|
|
868
872
|
try {
|
|
869
|
-
|
|
873
|
+
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_cursor";
|
|
874
|
+
logMcpTargetStatus("Cursor", cursorStatus, verb);
|
|
870
875
|
}
|
|
871
876
|
catch (err) {
|
|
872
877
|
debugLog(`configureCursorMcp failed: ${errorMessage(err)}`);
|
|
873
878
|
}
|
|
879
|
+
let copilotStatus = "no_copilot";
|
|
874
880
|
try {
|
|
875
|
-
|
|
881
|
+
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_copilot";
|
|
882
|
+
logMcpTargetStatus("Copilot CLI", copilotStatus, verb);
|
|
876
883
|
}
|
|
877
884
|
catch (err) {
|
|
878
885
|
debugLog(`configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
879
886
|
}
|
|
887
|
+
let codexStatus = "no_codex";
|
|
880
888
|
try {
|
|
881
|
-
|
|
889
|
+
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_codex";
|
|
890
|
+
logMcpTargetStatus("Codex", codexStatus, verb);
|
|
882
891
|
}
|
|
883
892
|
catch (err) {
|
|
884
893
|
debugLog(`configureCodexMcp failed: ${errorMessage(err)}`);
|
|
885
894
|
}
|
|
895
|
+
const allStatuses = [claudeStatus, vsStatus, cursorStatus, copilotStatus, codexStatus];
|
|
896
|
+
if (allStatuses.some((s) => s === "installed" || s === "already_configured"))
|
|
897
|
+
return "installed";
|
|
898
|
+
if (allStatuses.some((s) => s === "disabled" || s === "already_disabled"))
|
|
899
|
+
return "disabled";
|
|
900
|
+
return claudeStatus;
|
|
886
901
|
}
|
|
887
902
|
/**
|
|
888
903
|
* Configure hooks if enabled, or log a disabled message.
|
package/mcp/dist/link-context.js
CHANGED
|
@@ -38,7 +38,7 @@ function writeContextFile(managedContent) {
|
|
|
38
38
|
const existing = fs.readFileSync(contextFile, "utf8");
|
|
39
39
|
if (existing.includes("<!-- phren-managed -->")) {
|
|
40
40
|
const startIdx = existing.indexOf("<!-- phren-managed -->");
|
|
41
|
-
const endIdx = existing.indexOf("<!-- phren-managed -->");
|
|
41
|
+
const endIdx = existing.indexOf("<!-- phren-managed -->", startIdx + "<!-- phren-managed -->".length);
|
|
42
42
|
const before = startIdx > 0 ? existing.slice(0, startIdx).trimEnd() : "";
|
|
43
43
|
const after = endIdx !== -1 ? existing.slice(endIdx + "<!-- phren-managed -->".length).trimStart() : "";
|
|
44
44
|
const parts = [before, wrapped, after].filter(Boolean);
|