@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
package/mcp/dist/cli-govern.js
CHANGED
|
@@ -144,7 +144,7 @@ export async function handlePruneMemories(args = []) {
|
|
|
144
144
|
.filter((e) => e !== null);
|
|
145
145
|
}
|
|
146
146
|
catch (err) {
|
|
147
|
-
if ((process.env.PHREN_DEBUG
|
|
147
|
+
if ((process.env.PHREN_DEBUG))
|
|
148
148
|
process.stderr.write(`[phren] cli-govern retrievalLog readParse: ${errorMessage(err)}\n`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
@@ -255,7 +255,7 @@ export async function handleGcMaintain(args = []) {
|
|
|
255
255
|
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
|
|
256
256
|
let oldCommits = [];
|
|
257
257
|
try {
|
|
258
|
-
const raw = execSync(`git log --
|
|
258
|
+
const raw = execSync(`git log --before="${sevenDaysAgo}" --format="%H %ci %s"`, { cwd: phrenPath, encoding: "utf8" }).trim();
|
|
259
259
|
if (raw) {
|
|
260
260
|
oldCommits = raw.split("\n").filter((l) => l.includes("auto-save:") || l.includes("[auto]"));
|
|
261
261
|
}
|
|
@@ -271,23 +271,21 @@ export async function handleGcMaintain(args = []) {
|
|
|
271
271
|
report.commitsSquashed = oldCommits.length;
|
|
272
272
|
}
|
|
273
273
|
else {
|
|
274
|
-
// Group by ISO week
|
|
274
|
+
// Group by ISO week based on commit timestamp (already in the log output)
|
|
275
275
|
const commitsByWeek = new Map();
|
|
276
276
|
for (const line of oldCommits) {
|
|
277
277
|
const hash = line.split(" ")[0];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// Skip commits we can't resolve
|
|
290
|
-
}
|
|
278
|
+
// Format: "HASH YYYY-MM-DD HH:MM:SS +ZZZZ subject..."
|
|
279
|
+
const dateMatch = line.match(/^[a-f0-9]+ (\d{4}-\d{2}-\d{2})/);
|
|
280
|
+
if (!dateMatch)
|
|
281
|
+
continue;
|
|
282
|
+
const date = new Date(dateMatch[1]);
|
|
283
|
+
const weekStart = new Date(date);
|
|
284
|
+
weekStart.setDate(date.getDate() - date.getDay());
|
|
285
|
+
const weekKey = weekStart.toISOString().slice(0, 10);
|
|
286
|
+
if (!commitsByWeek.has(weekKey))
|
|
287
|
+
commitsByWeek.set(weekKey, []);
|
|
288
|
+
commitsByWeek.get(weekKey).push(hash);
|
|
291
289
|
}
|
|
292
290
|
// For each week with multiple commits, soft-reset to oldest and amend into a summary
|
|
293
291
|
for (const [weekKey, hashes] of commitsByWeek.entries()) {
|
|
@@ -320,64 +318,38 @@ export async function handleGcMaintain(args = []) {
|
|
|
320
318
|
console.log("Commit squash: all old auto-save weeks have only one commit, nothing to squash.");
|
|
321
319
|
}
|
|
322
320
|
}
|
|
323
|
-
// 3. Prune stale
|
|
324
|
-
const sessionsDir = path.join(phrenPath, ".sessions");
|
|
321
|
+
// 3–4. Prune stale files from .sessions/ and .runtime/
|
|
325
322
|
const thirtyDaysAgo = Date.now() - 30 * 86400000;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (stat.mtimeMs < thirtyDaysAgo) {
|
|
333
|
-
if (dryRun) {
|
|
334
|
-
console.log(`[dry-run] Would remove session marker: .sessions/${entry}`);
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
fs.unlinkSync(fullPath);
|
|
338
|
-
}
|
|
339
|
-
report.sessionsRemoved++;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Skip unreadable entries
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const sessionsVerb = dryRun ? "Would remove" : "Removed";
|
|
348
|
-
console.log(`${sessionsVerb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
|
|
349
|
-
// 4. Trim runtime logs from ~/.phren/.runtime/ older than 30 days
|
|
350
|
-
const runtimeDir = path.join(phrenPath, ".runtime");
|
|
351
|
-
const logExtensions = new Set([".log", ".jsonl", ".json"]);
|
|
352
|
-
if (fs.existsSync(runtimeDir)) {
|
|
353
|
-
const entries = fs.readdirSync(runtimeDir);
|
|
354
|
-
for (const entry of entries) {
|
|
355
|
-
const ext = path.extname(entry);
|
|
356
|
-
if (!logExtensions.has(ext))
|
|
357
|
-
continue;
|
|
358
|
-
// Never trim the active audit log or telemetry config
|
|
359
|
-
if (entry === "audit.log" || entry === "telemetry.json")
|
|
323
|
+
function pruneStaleFiles(dir, label, filter) {
|
|
324
|
+
let removed = 0;
|
|
325
|
+
if (!fs.existsSync(dir))
|
|
326
|
+
return removed;
|
|
327
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
328
|
+
if (filter && !filter(entry))
|
|
360
329
|
continue;
|
|
361
|
-
const fullPath = path.join(
|
|
330
|
+
const fullPath = path.join(dir, entry);
|
|
362
331
|
try {
|
|
363
|
-
|
|
364
|
-
if (stat.mtimeMs < thirtyDaysAgo) {
|
|
332
|
+
if (fs.statSync(fullPath).mtimeMs < thirtyDaysAgo) {
|
|
365
333
|
if (dryRun) {
|
|
366
|
-
console.log(`[dry-run] Would remove
|
|
334
|
+
console.log(`[dry-run] Would remove: ${label}/${entry}`);
|
|
367
335
|
}
|
|
368
336
|
else {
|
|
369
337
|
fs.unlinkSync(fullPath);
|
|
370
338
|
}
|
|
371
|
-
|
|
339
|
+
removed++;
|
|
372
340
|
}
|
|
373
341
|
}
|
|
374
|
-
catch {
|
|
375
|
-
// Skip unreadable entries
|
|
376
|
-
}
|
|
342
|
+
catch { /* skip unreadable */ }
|
|
377
343
|
}
|
|
344
|
+
return removed;
|
|
378
345
|
}
|
|
379
|
-
const
|
|
380
|
-
|
|
346
|
+
const logExtensions = new Set([".log", ".jsonl", ".json"]);
|
|
347
|
+
const protectedFiles = new Set(["audit.log", "telemetry.json"]);
|
|
348
|
+
report.sessionsRemoved = pruneStaleFiles(path.join(phrenPath, ".sessions"), ".sessions");
|
|
349
|
+
report.runtimeLogsRemoved = pruneStaleFiles(path.join(phrenPath, ".runtime"), ".runtime", (entry) => logExtensions.has(path.extname(entry)) && !protectedFiles.has(entry));
|
|
350
|
+
const verb = dryRun ? "Would remove" : "Removed";
|
|
351
|
+
console.log(`${verb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
|
|
352
|
+
console.log(`${verb} ${report.runtimeLogsRemoved} stale runtime log(s) from .runtime/`);
|
|
381
353
|
// 5. Summary
|
|
382
354
|
if (!dryRun) {
|
|
383
355
|
appendAuditLog(phrenPath, "maintain_gc", `gitGc=${report.gitGcRan} squashed=${report.commitsSquashed} sessions=${report.sessionsRemoved} logs=${report.runtimeLogsRemoved}`);
|
|
@@ -529,7 +501,7 @@ export async function handleBackgroundMaintenance(projectArg) {
|
|
|
529
501
|
fs.unlinkSync(markers.lock);
|
|
530
502
|
}
|
|
531
503
|
catch (err) {
|
|
532
|
-
if ((process.env.PHREN_DEBUG
|
|
504
|
+
if ((process.env.PHREN_DEBUG))
|
|
533
505
|
process.stderr.write(`[phren] cli-govern backgroundMaintenance unlockFinal: ${errorMessage(err)}\n`);
|
|
534
506
|
}
|
|
535
507
|
}
|
package/mcp/dist/cli-graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPhrenPath } from "./shared.js";
|
|
2
2
|
import { buildIndex, queryRows } from "./shared-index.js";
|
|
3
3
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
4
|
+
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* CLI: phren graph [--project <name>] [--limit <n>]
|
|
6
7
|
* Displays the fragment knowledge graph as a table.
|
|
@@ -18,6 +19,10 @@ export async function handleGraphRead(args) {
|
|
|
18
19
|
limit = Math.min(Math.max(parseInt(args[++i], 10) || 20, 1), 200);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
if (project && !isValidProjectName(project)) {
|
|
23
|
+
console.error(`Invalid project name: "${project}".`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
21
26
|
const db = await buildIndex(phrenPath, profile);
|
|
22
27
|
let sql;
|
|
23
28
|
let params;
|
|
@@ -74,6 +79,10 @@ export async function handleGraphLink(args) {
|
|
|
74
79
|
process.exit(1);
|
|
75
80
|
}
|
|
76
81
|
const [project, findingText, fragmentName] = args;
|
|
82
|
+
if (!isValidProjectName(project)) {
|
|
83
|
+
console.error(`Invalid project name: "${project}".`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
77
86
|
const phrenPath = getPhrenPath();
|
|
78
87
|
const profile = resolveRuntimeProfile(phrenPath);
|
|
79
88
|
const db = await buildIndex(phrenPath, profile);
|
|
@@ -94,7 +103,10 @@ export async function handleGraphLink(args) {
|
|
|
94
103
|
try {
|
|
95
104
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [normalizedFragment, "fragment", new Date().toISOString().slice(0, 10)]);
|
|
96
105
|
}
|
|
97
|
-
catch {
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (process.env.PHREN_DEBUG)
|
|
108
|
+
process.stderr.write(`[phren] graph link insert fragment: ${errorMessage(err)}\n`);
|
|
109
|
+
}
|
|
98
110
|
const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [normalizedFragment, "fragment"]);
|
|
99
111
|
if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
|
|
100
112
|
console.error("Failed to create fragment.");
|
|
@@ -104,7 +116,10 @@ export async function handleGraphLink(args) {
|
|
|
104
116
|
try {
|
|
105
117
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
|
|
106
118
|
}
|
|
107
|
-
catch {
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (process.env.PHREN_DEBUG)
|
|
121
|
+
process.stderr.write(`[phren] graph link insert document: ${errorMessage(err)}\n`);
|
|
122
|
+
}
|
|
108
123
|
const docResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
|
|
109
124
|
if (!docResult?.length || !docResult[0]?.values?.length) {
|
|
110
125
|
console.error("Failed to create document fragment.");
|
|
@@ -115,7 +130,7 @@ export async function handleGraphLink(args) {
|
|
|
115
130
|
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, "mentions", sourceDoc]);
|
|
116
131
|
}
|
|
117
132
|
catch (err) {
|
|
118
|
-
console.error(`Failed to link: ${
|
|
133
|
+
console.error(`Failed to link: ${errorMessage(err)}`);
|
|
119
134
|
process.exit(1);
|
|
120
135
|
}
|
|
121
136
|
// Persist to manual-links.json
|
|
@@ -144,7 +159,7 @@ export async function handleGraphLink(args) {
|
|
|
144
159
|
});
|
|
145
160
|
}
|
|
146
161
|
catch (err) {
|
|
147
|
-
console.error(`Failed to persist manual link: ${
|
|
162
|
+
console.error(`Failed to persist manual link: ${errorMessage(err)}`);
|
|
148
163
|
process.exit(1);
|
|
149
164
|
}
|
|
150
165
|
console.log(`Linked "${fragmentName}" to ${sourceDoc}.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { capCache } from "./shared.js";
|
|
4
|
+
import { errorMessage } from "./utils.js";
|
|
4
5
|
// ── Glob matching and project frontmatter ────────────────────────────────────
|
|
5
6
|
const projectGlobCache = new Map();
|
|
6
7
|
export function clearProjectGlobCache() {
|
|
@@ -45,7 +46,7 @@ function parseProjectGlobs(phrenPathLocal, project) {
|
|
|
45
46
|
}
|
|
46
47
|
catch (err) {
|
|
47
48
|
if (process.env.PHREN_DEBUG)
|
|
48
|
-
process.stderr.write(`[phren] getProjectGlobs: ${
|
|
49
|
+
process.stderr.write(`[phren] getProjectGlobs: ${errorMessage(err)}\n`);
|
|
49
50
|
}
|
|
50
51
|
projectGlobCache.set(project, globs);
|
|
51
52
|
capCache(projectGlobCache);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordInjection, recordRetrieval, } from "./shared-governance.js";
|
|
2
2
|
import { getDocSourceKey, } from "./shared-index.js";
|
|
3
3
|
import { logImpact, extractFindingIdsFromSnippet, } from "./finding-impact.js";
|
|
4
|
-
import { isFeatureEnabled } from "./utils.js";
|
|
4
|
+
import { isFeatureEnabled, errorMessage } from "./utils.js";
|
|
5
5
|
import { annotateStale } from "./cli-hooks-citations.js";
|
|
6
6
|
import { approximateTokens, fileRelevanceBoost, branchMatchBoost } from "./shared-retrieval.js";
|
|
7
7
|
// ── Progressive disclosure helpers ────────────────────────────────────────────
|
|
@@ -56,7 +56,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
56
56
|
}
|
|
57
57
|
catch (err) {
|
|
58
58
|
if (process.env.PHREN_DEBUG)
|
|
59
|
-
process.stderr.write(`[phren] injectContext recordRetrieval: ${
|
|
59
|
+
process.stderr.write(`[phren] injectContext recordRetrieval: ${errorMessage(err)}\n`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -108,7 +108,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
108
108
|
}
|
|
109
109
|
catch (err) {
|
|
110
110
|
if (process.env.PHREN_DEBUG)
|
|
111
|
-
process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${
|
|
111
|
+
process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${errorMessage(err)}\n`);
|
|
112
112
|
}
|
|
113
113
|
parts.push(`[${getDocSourceKey(doc, phrenPathLocal)}] (${doc.type})`);
|
|
114
114
|
parts.push(annotateStale(snippet));
|
|
@@ -116,7 +116,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
logImpact(phrenPathLocal, impactEntries);
|
|
119
|
-
parts.push("
|
|
119
|
+
parts.push("</phren-context>");
|
|
120
120
|
const changedCount = gitCtx?.changedFiles.size ?? 0;
|
|
121
121
|
if (gitCtx) {
|
|
122
122
|
const fileHits = selected.filter((r) => fileRelevanceBoost(r.doc.path, gitCtx.changedFiles) > 0).length;
|
|
@@ -749,7 +749,7 @@ export async function handleHookStop() {
|
|
|
749
749
|
}
|
|
750
750
|
catch (err) {
|
|
751
751
|
if (process.env.PHREN_DEBUG)
|
|
752
|
-
process.stderr.write(`[phren]
|
|
752
|
+
process.stderr.write(`[phren] hookStop transcriptParse: ${errorMessage(err)}\n`);
|
|
753
753
|
}
|
|
754
754
|
}
|
|
755
755
|
captureInput = assistantTexts.join("\n");
|
package/mcp/dist/cli-hooks.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// cli-hooks-output.ts — hook output formatting
|
|
6
6
|
// cli-hooks-globs.ts — project glob matching
|
|
7
7
|
import { debugLog, sessionMarker, sessionsDir, getPhrenPath, } from "./shared.js";
|
|
8
|
-
import {
|
|
8
|
+
import { mergeConfig, } from "./shared-governance.js";
|
|
9
9
|
import { buildIndex, detectProject, } from "./shared-index.js";
|
|
10
10
|
import { isProjectHookEnabled } from "./project-config.js";
|
|
11
11
|
import { checkConsolidationNeeded, } from "./shared-content.js";
|
|
@@ -126,7 +126,7 @@ export async function handleHookPrompt() {
|
|
|
126
126
|
}
|
|
127
127
|
catch (err) {
|
|
128
128
|
if (process.env.PHREN_DEBUG)
|
|
129
|
-
process.stderr.write(`[phren] hookPrompt stdinRead: ${
|
|
129
|
+
process.stderr.write(`[phren] hookPrompt stdinRead: ${errorMessage(err)}\n`);
|
|
130
130
|
process.exit(0);
|
|
131
131
|
}
|
|
132
132
|
const input = parseHookInput(raw);
|
|
@@ -164,7 +164,9 @@ export async function handleHookPrompt() {
|
|
|
164
164
|
for (const kw of keywordEntries) {
|
|
165
165
|
sessionTopics[kw] = (sessionTopics[kw] ?? 0) + 1;
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
const topicTmp = `${topicFile}.tmp-${process.pid}`;
|
|
168
|
+
fs.writeFileSync(topicTmp, JSON.stringify(sessionTopics));
|
|
169
|
+
fs.renameSync(topicTmp, topicFile);
|
|
168
170
|
// Find hot topics (3+ mentions this session)
|
|
169
171
|
hotTopics = Object.entries(sessionTopics)
|
|
170
172
|
.filter(([, count]) => count >= 3)
|
|
@@ -182,6 +184,7 @@ export async function handleHookPrompt() {
|
|
|
182
184
|
appendAuditLog(getPhrenPath(), "hook_prompt", `status=project_disabled project=${detectedProject}`);
|
|
183
185
|
process.exit(0);
|
|
184
186
|
}
|
|
187
|
+
const resolvedConfig = mergeConfig(getPhrenPath(), detectedProject ?? undefined);
|
|
185
188
|
const safeQuery = buildRobustFtsQuery(keywords, detectedProject, getPhrenPath());
|
|
186
189
|
if (!safeQuery)
|
|
187
190
|
process.exit(0);
|
|
@@ -193,14 +196,17 @@ export async function handleHookPrompt() {
|
|
|
193
196
|
process.exit(0);
|
|
194
197
|
autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
|
|
195
198
|
const tTrust0 = Date.now();
|
|
196
|
-
const policy =
|
|
199
|
+
const policy = resolvedConfig.retentionPolicy;
|
|
197
200
|
const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
|
|
198
201
|
const trustResult = applyTrustFilter(rows, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay, getPhrenPath());
|
|
199
202
|
rows = trustResult.rows;
|
|
200
203
|
stage.trustMs = Date.now() - tTrust0;
|
|
201
204
|
if (!rows.length)
|
|
202
205
|
process.exit(0);
|
|
203
|
-
|
|
206
|
+
const findingsProactivity = resolvedConfig.proactivity.findings
|
|
207
|
+
?? resolvedConfig.proactivity.base
|
|
208
|
+
?? getProactivityLevelForFindings(getPhrenPath());
|
|
209
|
+
if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && findingsProactivity !== "low" && sessionId && detectedProject && cwd) {
|
|
204
210
|
const marker = sessionMarker(getPhrenPath(), `extracted-${sessionId}-${detectedProject}`);
|
|
205
211
|
if (!fs.existsSync(marker)) {
|
|
206
212
|
try {
|
|
@@ -260,7 +266,9 @@ export async function handleHookPrompt() {
|
|
|
260
266
|
debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
|
|
261
267
|
}
|
|
262
268
|
const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getPhrenPath(), sessionId);
|
|
263
|
-
const taskLevel =
|
|
269
|
+
const taskLevel = resolvedConfig.proactivity.tasks
|
|
270
|
+
?? resolvedConfig.proactivity.base
|
|
271
|
+
?? getProactivityLevelForTask(getPhrenPath());
|
|
264
272
|
const taskLifecycle = handleTaskPromptLifecycle({
|
|
265
273
|
phrenPath: getPhrenPath(),
|
|
266
274
|
prompt,
|
|
@@ -275,8 +283,7 @@ export async function handleHookPrompt() {
|
|
|
275
283
|
}
|
|
276
284
|
// Inject finding sensitivity agent instruction
|
|
277
285
|
try {
|
|
278
|
-
const
|
|
279
|
-
const sensitivity = workflowPolicy.findingSensitivity ?? "balanced";
|
|
286
|
+
const sensitivity = resolvedConfig.findingSensitivity ?? "balanced";
|
|
280
287
|
const sensitivityConfig = FINDING_SENSITIVITY_CONFIG[sensitivity];
|
|
281
288
|
if (sensitivityConfig) {
|
|
282
289
|
parts.push("");
|
|
@@ -304,36 +311,38 @@ export async function handleHookPrompt() {
|
|
|
304
311
|
const noticeFile = sessionId ? sessionMarker(getPhrenPath(), `noticed-${sessionId}`) : null;
|
|
305
312
|
const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
|
|
306
313
|
if (!alreadyNoticed) {
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
// Defer stale session marker cleanup to after output — it's I/O-heavy and not needed for results
|
|
315
|
+
setImmediate(() => {
|
|
316
|
+
try {
|
|
317
|
+
const cutoff = Date.now() - 86400000;
|
|
318
|
+
const sessDir = sessionsDir(getPhrenPath());
|
|
319
|
+
if (fs.existsSync(sessDir)) {
|
|
320
|
+
for (const f of fs.readdirSync(sessDir)) {
|
|
321
|
+
if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
|
|
322
|
+
continue;
|
|
323
|
+
const fp = `${sessDir}/${f}`;
|
|
324
|
+
try {
|
|
325
|
+
if (fs.statSync(fp).mtimeMs < cutoff)
|
|
326
|
+
fs.unlinkSync(fp);
|
|
327
|
+
}
|
|
328
|
+
catch { /* ignore per-file errors */ }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Also clean stale markers from the phren root
|
|
332
|
+
for (const f of fs.readdirSync(getPhrenPath())) {
|
|
333
|
+
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
314
334
|
continue;
|
|
315
|
-
const fp = `${
|
|
316
|
-
|
|
335
|
+
const fp = `${getPhrenPath()}/${f}`;
|
|
336
|
+
try {
|
|
317
337
|
fs.unlinkSync(fp);
|
|
338
|
+
}
|
|
339
|
+
catch { /* ignore */ }
|
|
318
340
|
}
|
|
319
341
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
323
|
-
continue;
|
|
324
|
-
const fp = `${getPhrenPath()}/${f}`;
|
|
325
|
-
try {
|
|
326
|
-
fs.unlinkSync(fp);
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
if (process.env.PHREN_DEBUG)
|
|
330
|
-
process.stderr.write(`[phren] hookPrompt staleNoticeUnlink: ${errorMessage(err)}\n`);
|
|
331
|
-
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
|
|
332
344
|
}
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
|
|
336
|
-
}
|
|
345
|
+
});
|
|
337
346
|
const needed = checkConsolidationNeeded(getPhrenPath(), profile);
|
|
338
347
|
if (needed.length > 0) {
|
|
339
348
|
const notices = needed.map((n) => {
|
|
@@ -345,7 +354,7 @@ export async function handleHookPrompt() {
|
|
|
345
354
|
parts.push(`Findings ready for consolidation:`);
|
|
346
355
|
parts.push(notices.join("\n"));
|
|
347
356
|
parts.push(`Run phren-consolidate when ready.`);
|
|
348
|
-
parts.push(
|
|
357
|
+
parts.push(`</phren-notice>`);
|
|
349
358
|
}
|
|
350
359
|
if (noticeFile) {
|
|
351
360
|
try {
|
|
@@ -367,7 +376,7 @@ export async function handleHookPrompt() {
|
|
|
367
376
|
}
|
|
368
377
|
catch (err) {
|
|
369
378
|
const msg = errorMessage(err);
|
|
370
|
-
process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details
|
|
379
|
+
process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details.</phren-error>\n`);
|
|
371
380
|
debugLog(`hook-prompt error: ${msg}`);
|
|
372
381
|
process.exit(0);
|
|
373
382
|
}
|
|
@@ -5,6 +5,7 @@ import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjec
|
|
|
5
5
|
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
6
6
|
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
7
7
|
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
|
|
8
|
+
import { detectSkillCollisions } from "./link-skills.js";
|
|
8
9
|
import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
|
|
9
10
|
import { findProjectDir } from "./project-locator.js";
|
|
10
11
|
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
|
|
@@ -76,7 +77,7 @@ function openInEditor(filePath) {
|
|
|
76
77
|
execFileSync(editor, [filePath], { stdio: "inherit" });
|
|
77
78
|
}
|
|
78
79
|
catch (err) {
|
|
79
|
-
if ((process.env.PHREN_DEBUG
|
|
80
|
+
if ((process.env.PHREN_DEBUG))
|
|
80
81
|
process.stderr.write(`[phren] openInEditor: ${errorMessage(err)}\n`);
|
|
81
82
|
console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
|
|
82
83
|
process.exit(1);
|
|
@@ -145,7 +146,7 @@ export function handleSkillsNamespace(args, profile) {
|
|
|
145
146
|
console.log(`Linked skill ${fileName} into ${project}.`);
|
|
146
147
|
}
|
|
147
148
|
catch (err) {
|
|
148
|
-
if ((process.env.PHREN_DEBUG
|
|
149
|
+
if ((process.env.PHREN_DEBUG))
|
|
149
150
|
process.stderr.write(`[phren] skill add symlinkFailed: ${errorMessage(err)}\n`);
|
|
150
151
|
fs.copyFileSync(source, dest);
|
|
151
152
|
console.log(`Copied skill ${fileName} into ${project}.`);
|
|
@@ -450,7 +451,7 @@ export function handleDetectSkills(args, profile) {
|
|
|
450
451
|
continue;
|
|
451
452
|
}
|
|
452
453
|
catch (err) {
|
|
453
|
-
if ((process.env.PHREN_DEBUG
|
|
454
|
+
if ((process.env.PHREN_DEBUG))
|
|
454
455
|
process.stderr.write(`[phren] skillList lstat: ${errorMessage(err)}\n`);
|
|
455
456
|
}
|
|
456
457
|
const name = entry.replace(/\.md$/, "");
|
|
@@ -548,6 +549,15 @@ function printSkillDoctor(scope, manifest, destDir) {
|
|
|
548
549
|
problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
|
|
549
550
|
}
|
|
550
551
|
}
|
|
552
|
+
// Check for user-owned files blocking phren skill links
|
|
553
|
+
const phrenPath = getPhrenPath();
|
|
554
|
+
const srcDir = scope.toLowerCase() === "global"
|
|
555
|
+
? path.join(phrenPath, "global", "skills")
|
|
556
|
+
: path.join(phrenPath, scope, "skills");
|
|
557
|
+
const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
|
|
558
|
+
for (const collision of collisions) {
|
|
559
|
+
problems.push(`Skill collision: ${collision.message}`);
|
|
560
|
+
}
|
|
551
561
|
}
|
|
552
562
|
if (!manifest.problems.length && !problems.length) {
|
|
553
563
|
console.log("\nDoctor: no skill pipeline issues detected.");
|
|
@@ -853,7 +863,7 @@ function handleProjectsList(profile) {
|
|
|
853
863
|
dirFiles = new Set(fs.readdirSync(projectDir));
|
|
854
864
|
}
|
|
855
865
|
catch (err) {
|
|
856
|
-
if ((process.env.PHREN_DEBUG
|
|
866
|
+
if ((process.env.PHREN_DEBUG))
|
|
857
867
|
process.stderr.write(`[phren] projects list readdir: ${errorMessage(err)}\n`);
|
|
858
868
|
dirFiles = new Set();
|
|
859
869
|
}
|
|
@@ -896,7 +906,7 @@ async function handleProjectsRemove(name, profile) {
|
|
|
896
906
|
countFiles(projectDir);
|
|
897
907
|
}
|
|
898
908
|
catch (err) {
|
|
899
|
-
if ((process.env.PHREN_DEBUG
|
|
909
|
+
if ((process.env.PHREN_DEBUG))
|
|
900
910
|
process.stderr.write(`[phren] projects remove countFiles: ${errorMessage(err)}\n`);
|
|
901
911
|
}
|
|
902
912
|
const readline = await import("readline");
|
package/mcp/dist/cli-search.js
CHANGED
|
@@ -34,7 +34,7 @@ export function readSearchHistory(phrenPath) {
|
|
|
34
34
|
.map((line) => JSON.parse(line));
|
|
35
35
|
}
|
|
36
36
|
catch (err) {
|
|
37
|
-
if ((process.env.PHREN_DEBUG
|
|
37
|
+
if ((process.env.PHREN_DEBUG))
|
|
38
38
|
process.stderr.write(`[phren] readSearchHistory: ${errorMessage(err)}\n`);
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
@@ -253,7 +253,7 @@ export async function runSearch(opts, phrenPath, profile) {
|
|
|
253
253
|
logSearchMiss(phrenPath, opts.query, opts.project);
|
|
254
254
|
}
|
|
255
255
|
catch (err) {
|
|
256
|
-
if ((process.env.PHREN_DEBUG
|
|
256
|
+
if ((process.env.PHREN_DEBUG))
|
|
257
257
|
process.stderr.write(`[phren] search logSearchMiss: ${errorMessage(err)}\n`);
|
|
258
258
|
}
|
|
259
259
|
}
|
package/mcp/dist/cli.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function runCliCommand(command, args) {
|
|
|
108
108
|
case "session-context":
|
|
109
109
|
return handleSessionContext();
|
|
110
110
|
default:
|
|
111
|
-
console.error(`Unknown command: ${command}
|
|
111
|
+
console.error(`Unknown command: ${command}\nRun 'phren --help' for available commands.`);
|
|
112
112
|
process.exit(1);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -77,12 +77,11 @@ function parseActiveEntries(content) {
|
|
|
77
77
|
/**
|
|
78
78
|
* Check whether a bullet already exists in a reference file (already archived).
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
/** Build a Set of normalized bullet strings from all .md files in referenceDir. */
|
|
81
|
+
function buildArchivedBulletSet(referenceDir) {
|
|
82
|
+
const bulletSet = new Set();
|
|
81
83
|
if (!fs.existsSync(referenceDir))
|
|
82
|
-
return
|
|
83
|
-
const normalizedBullet = stripComments(bullet).replace(/^-\s+/, "").trim().toLowerCase();
|
|
84
|
-
if (!normalizedBullet)
|
|
85
|
-
return false;
|
|
84
|
+
return bulletSet;
|
|
86
85
|
try {
|
|
87
86
|
const stack = [referenceDir];
|
|
88
87
|
while (stack.length > 0) {
|
|
@@ -100,17 +99,23 @@ function isAlreadyArchived(referenceDir, bullet) {
|
|
|
100
99
|
if (!line.startsWith("- "))
|
|
101
100
|
continue;
|
|
102
101
|
const normalizedLine = stripComments(line).replace(/^-\s+/, "").trim().toLowerCase();
|
|
103
|
-
if (normalizedLine
|
|
104
|
-
|
|
102
|
+
if (normalizedLine)
|
|
103
|
+
bulletSet.add(normalizedLine);
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
catch (err) {
|
|
110
|
-
if ((process.env.PHREN_DEBUG
|
|
111
|
-
process.stderr.write(`[phren]
|
|
109
|
+
if ((process.env.PHREN_DEBUG))
|
|
110
|
+
process.stderr.write(`[phren] buildArchivedBulletSet: ${errorMessage(err)}\n`);
|
|
112
111
|
}
|
|
113
|
-
return
|
|
112
|
+
return bulletSet;
|
|
113
|
+
}
|
|
114
|
+
function isAlreadyArchived(archivedSet, bullet) {
|
|
115
|
+
const normalizedBullet = stripComments(bullet).replace(/^-\s+/, "").trim().toLowerCase();
|
|
116
|
+
if (!normalizedBullet)
|
|
117
|
+
return false;
|
|
118
|
+
return archivedSet.has(normalizedBullet);
|
|
114
119
|
}
|
|
115
120
|
/**
|
|
116
121
|
* Archive the oldest entries from FINDINGS.md into reference/{topic}.md files.
|
|
@@ -152,8 +157,11 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
|
|
|
152
157
|
throw wxErr;
|
|
153
158
|
}
|
|
154
159
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
160
|
+
catch (innerErr) {
|
|
161
|
+
if (innerErr.code === "EEXIST" || innerErr.code === "ENOENT") {
|
|
162
|
+
return phrenErr("Consolidation already running", PhrenError.LOCK_TIMEOUT);
|
|
163
|
+
}
|
|
164
|
+
throw innerErr;
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
else {
|
|
@@ -173,9 +181,10 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
|
|
|
173
181
|
const referenceDir = path.join(resolvedDir, "reference");
|
|
174
182
|
const { topics } = readProjectTopics(phrenPath, project);
|
|
175
183
|
const today = new Date().toISOString().slice(0, 10);
|
|
184
|
+
const archivedSet = buildArchivedBulletSet(referenceDir);
|
|
176
185
|
const actuallyArchived = [];
|
|
177
186
|
for (const entry of toArchive) {
|
|
178
|
-
if (isAlreadyArchived(
|
|
187
|
+
if (isAlreadyArchived(archivedSet, entry.bullet)) {
|
|
179
188
|
debugLog(`auto_archive: skipping already-archived entry: "${entry.bullet.slice(0, 60)}"`);
|
|
180
189
|
continue;
|
|
181
190
|
}
|
|
@@ -271,7 +280,7 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
|
|
|
271
280
|
fs.unlinkSync(lockFile);
|
|
272
281
|
}
|
|
273
282
|
catch (err) {
|
|
274
|
-
if ((process.env.PHREN_DEBUG
|
|
283
|
+
if ((process.env.PHREN_DEBUG))
|
|
275
284
|
process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
|
|
276
285
|
}
|
|
277
286
|
}
|