@phren/cli 0.0.5 → 0.0.7
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 +3 -3
- package/mcp/dist/cli-hooks-session.js +9 -3
- package/mcp/dist/cli-hooks.js +27 -9
- package/mcp/dist/content-citation.js +24 -3
- package/mcp/dist/content-learning.js +28 -5
- package/mcp/dist/data-access.js +96 -53
- package/mcp/dist/finding-impact.js +23 -0
- package/mcp/dist/finding-lifecycle.js +56 -29
- package/mcp/dist/governance-locks.js +11 -6
- package/mcp/dist/index.js +2 -1
- package/mcp/dist/init-preferences.js +18 -3
- package/mcp/dist/init.js +11 -0
- package/mcp/dist/mcp-config.js +0 -8
- package/mcp/dist/mcp-data.js +22 -3
- package/mcp/dist/mcp-extract.js +1 -0
- package/mcp/dist/mcp-finding.js +1 -1
- package/mcp/dist/mcp-hooks.js +36 -16
- package/mcp/dist/mcp-memory.js +0 -1
- package/mcp/dist/mcp-ops.js +5 -10
- package/mcp/dist/mcp-search.js +7 -1
- package/mcp/dist/mcp-session.js +66 -6
- package/mcp/dist/mcp-skills.js +5 -2
- package/mcp/dist/mcp-tasks.js +7 -4
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-data.js +7 -7
- package/mcp/dist/memory-ui-graph.js +178 -23
- package/mcp/dist/project-config.js +37 -18
- package/mcp/dist/shared-content.js +1 -1
- package/mcp/dist/shared-index.js +16 -3
- package/mcp/dist/shared-retrieval.js +64 -34
- package/mcp/dist/shared.js +1 -10
- package/mcp/dist/test-global-setup.js +3 -4
- package/package.json +2 -2
|
@@ -6,6 +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-locks.js";
|
|
9
10
|
function preferencesFile(phrenPath) {
|
|
10
11
|
return installPreferencesFile(phrenPath);
|
|
11
12
|
}
|
|
@@ -24,7 +25,7 @@ function readPreferencesFile(file) {
|
|
|
24
25
|
return {};
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
function
|
|
28
|
+
function writePreferencesFileRaw(file, current, patch) {
|
|
28
29
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
30
|
const tmpPath = `${file}.tmp-${crypto.randomUUID()}`;
|
|
30
31
|
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
@@ -34,6 +35,12 @@ function writePreferencesFile(file, current, patch) {
|
|
|
34
35
|
}, null, 2) + "\n");
|
|
35
36
|
fs.renameSync(tmpPath, file);
|
|
36
37
|
}
|
|
38
|
+
function writePreferencesFile(file, patch) {
|
|
39
|
+
withFileLock(file, () => {
|
|
40
|
+
const current = readPreferencesFile(file);
|
|
41
|
+
writePreferencesFileRaw(file, current, patch);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
37
44
|
export function readInstallPreferences(phrenPath) {
|
|
38
45
|
return readPreferencesFile(preferencesFile(phrenPath));
|
|
39
46
|
}
|
|
@@ -41,10 +48,18 @@ export function readGovernanceInstallPreferences(phrenPath) {
|
|
|
41
48
|
return readPreferencesFile(governanceInstallPreferencesFile(phrenPath));
|
|
42
49
|
}
|
|
43
50
|
export function writeInstallPreferences(phrenPath, patch) {
|
|
44
|
-
writePreferencesFile(preferencesFile(phrenPath),
|
|
51
|
+
writePreferencesFile(preferencesFile(phrenPath), patch);
|
|
45
52
|
}
|
|
46
53
|
export function writeGovernanceInstallPreferences(phrenPath, patch) {
|
|
47
|
-
writePreferencesFile(governanceInstallPreferencesFile(phrenPath),
|
|
54
|
+
writePreferencesFile(governanceInstallPreferencesFile(phrenPath), patch);
|
|
55
|
+
}
|
|
56
|
+
/** Atomically read-modify-write install preferences using a patcher function. */
|
|
57
|
+
export function updateInstallPreferences(phrenPath, patcher) {
|
|
58
|
+
const file = preferencesFile(phrenPath);
|
|
59
|
+
withFileLock(file, () => {
|
|
60
|
+
const current = readPreferencesFile(file);
|
|
61
|
+
writePreferencesFileRaw(file, current, patcher(current));
|
|
62
|
+
});
|
|
48
63
|
}
|
|
49
64
|
export function getMcpEnabledPreference(phrenPath) {
|
|
50
65
|
const prefs = readInstallPreferences(phrenPath);
|
package/mcp/dist/init.js
CHANGED
|
@@ -910,6 +910,17 @@ export async function runInit(opts = {}) {
|
|
|
910
910
|
}
|
|
911
911
|
let phrenPath = resolveInitPhrenPath(opts);
|
|
912
912
|
const dryRun = Boolean(opts.dryRun);
|
|
913
|
+
// Migrate ~/.cortex → ~/.phren when upgrading from the old name.
|
|
914
|
+
// Only runs when the resolved phrenPath doesn't exist yet but ~/.cortex does.
|
|
915
|
+
if (!opts._walkthroughStoragePath && !fs.existsSync(phrenPath)) {
|
|
916
|
+
const cortexPath = path.resolve(homePath(".cortex"));
|
|
917
|
+
if (cortexPath !== phrenPath && fs.existsSync(cortexPath) && hasInstallMarkers(cortexPath)) {
|
|
918
|
+
if (!dryRun) {
|
|
919
|
+
fs.renameSync(cortexPath, phrenPath);
|
|
920
|
+
}
|
|
921
|
+
console.log(`Migrated ~/.cortex → ~/.phren`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
913
924
|
let hasExistingInstall = hasInstallMarkers(phrenPath);
|
|
914
925
|
// Interactive walkthrough for first-time installs (skip with --yes or non-TTY)
|
|
915
926
|
if (!hasExistingInstall && !dryRun && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
package/mcp/dist/mcp-config.js
CHANGED
|
@@ -5,14 +5,6 @@ import { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings
|
|
|
5
5
|
import { readGovernanceInstallPreferences, writeGovernanceInstallPreferences, } from "./init-preferences.js";
|
|
6
6
|
import { FINDING_SENSITIVITY_CONFIG } from "./cli-config.js";
|
|
7
7
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
|
-
function normalizeProactivityLevel(raw) {
|
|
9
|
-
if (!raw)
|
|
10
|
-
return undefined;
|
|
11
|
-
const normalized = raw.trim().toLowerCase();
|
|
12
|
-
return PROACTIVITY_LEVELS.includes(normalized)
|
|
13
|
-
? normalized
|
|
14
|
-
: undefined;
|
|
15
|
-
}
|
|
16
8
|
function proactivitySnapshot(phrenPath) {
|
|
17
9
|
const prefs = readGovernanceInstallPreferences(phrenPath);
|
|
18
10
|
return {
|
package/mcp/dist/mcp-data.js
CHANGED
|
@@ -215,7 +215,14 @@ export function register(server, ctx) {
|
|
|
215
215
|
}
|
|
216
216
|
catch (indexError) {
|
|
217
217
|
// Index rebuild failed — restore backup if we replaced the project dir
|
|
218
|
-
if (overwrite) {
|
|
218
|
+
if (!overwrite) {
|
|
219
|
+
// Non-overwrite case: no backup to restore — remove the orphaned project dir
|
|
220
|
+
try {
|
|
221
|
+
fs.rmSync(projectDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
catch { /* best-effort */ }
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
219
226
|
// Find the backup dir that was created earlier
|
|
220
227
|
try {
|
|
221
228
|
for (const entry of fs.readdirSync(phrenPath)) {
|
|
@@ -287,7 +294,13 @@ export function register(server, ctx) {
|
|
|
287
294
|
return mcpResponse({ ok: false, error: `Archive "${project}.archived" already exists. Unarchive or remove it first.` });
|
|
288
295
|
}
|
|
289
296
|
fs.renameSync(projectDir, archiveDir);
|
|
290
|
-
|
|
297
|
+
try {
|
|
298
|
+
await rebuildIndex();
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
fs.renameSync(archiveDir, projectDir);
|
|
302
|
+
return mcpResponse({ ok: false, error: `Index rebuild failed after archive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
|
|
303
|
+
}
|
|
291
304
|
return mcpResponse({
|
|
292
305
|
ok: true,
|
|
293
306
|
message: `Archived project "${project}". Data preserved at ${archiveDir}.`,
|
|
@@ -304,7 +317,13 @@ export function register(server, ctx) {
|
|
|
304
317
|
return mcpResponse({ ok: false, error: `No archive found for "${project}".`, data: { availableArchives: available } });
|
|
305
318
|
}
|
|
306
319
|
fs.renameSync(archiveDir, projectDir);
|
|
307
|
-
|
|
320
|
+
try {
|
|
321
|
+
await rebuildIndex();
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
fs.renameSync(projectDir, archiveDir);
|
|
325
|
+
return mcpResponse({ ok: false, error: `Index rebuild failed after unarchive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
|
|
326
|
+
}
|
|
308
327
|
return mcpResponse({
|
|
309
328
|
ok: true,
|
|
310
329
|
message: `Unarchived project "${project}". It is now active again.`,
|
package/mcp/dist/mcp-extract.js
CHANGED
|
@@ -125,6 +125,7 @@ export function register(server, ctx) {
|
|
|
125
125
|
}
|
|
126
126
|
return mcpResponse({
|
|
127
127
|
ok: added.length > 0,
|
|
128
|
+
...(added.length === 0 ? { error: `All ${findings.length} finding(s) were skipped (duplicates or errors).` } : {}),
|
|
128
129
|
message: `Extracted ${findings.length} finding(s): ${added.length} added, ${allSkipped.length} skipped (duplicates or errors).`,
|
|
129
130
|
data: { project, extracted: findings, added, skipped: allSkipped, dryRun: false },
|
|
130
131
|
});
|
package/mcp/dist/mcp-finding.js
CHANGED
|
@@ -203,7 +203,7 @@ export function register(server, ctx) {
|
|
|
203
203
|
if (err instanceof Error && err.message.includes("Rejected:")) {
|
|
204
204
|
return mcpResponse({ ok: false, error: errorMessage(err), errorCode: "VALIDATION_ERROR" });
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
return mcpResponse({ ok: false, error: `Unexpected error saving finding: ${errorMessage(err)}` });
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
});
|
package/mcp/dist/mcp-hooks.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mcpResponse } from "./mcp-types.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import { readInstallPreferences,
|
|
5
|
+
import { readInstallPreferences, updateInstallPreferences } from "./init-preferences.js";
|
|
6
6
|
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES } from "./hooks.js";
|
|
7
7
|
import { hookConfigPath } from "./shared.js";
|
|
8
8
|
import { PROJECT_HOOK_EVENTS, isProjectHookEnabled, readProjectConfig, writeProjectHookConfig } from "./project-config.js";
|
|
@@ -161,16 +161,15 @@ export function register(server, ctx) {
|
|
|
161
161
|
if (!normalized) {
|
|
162
162
|
return mcpResponse({ ok: false, error: `Invalid tool "${tool}". Use: ${HOOK_TOOLS.join(", ")}` });
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
writeInstallPreferences(phrenPath, {
|
|
164
|
+
updateInstallPreferences(phrenPath, (prefs) => ({
|
|
166
165
|
hookTools: {
|
|
167
166
|
...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
|
|
168
167
|
[normalized]: enabled,
|
|
169
168
|
},
|
|
170
|
-
});
|
|
169
|
+
}));
|
|
171
170
|
return mcpResponse({ ok: true, message: `${enabled ? "Enabled" : "Disabled"} hooks for ${normalized}.`, data: { tool: normalized, enabled } });
|
|
172
171
|
}
|
|
173
|
-
|
|
172
|
+
updateInstallPreferences(phrenPath, () => ({ hooksEnabled: enabled }));
|
|
174
173
|
return mcpResponse({ ok: true, message: `${enabled ? "Enabled" : "Disabled"} hooks globally.`, data: { global: true, enabled } });
|
|
175
174
|
});
|
|
176
175
|
// ── add_custom_hook ──────────────────────────────────────────────────────
|
|
@@ -202,12 +201,26 @@ export function register(server, ctx) {
|
|
|
202
201
|
const { hostname } = new URL(trimmed);
|
|
203
202
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
204
203
|
const ssrfBlocked = h === "localhost" ||
|
|
205
|
-
|
|
204
|
+
// IPv4 private/loopback ranges
|
|
206
205
|
/^127\./.test(h) ||
|
|
207
206
|
/^10\./.test(h) ||
|
|
208
207
|
/^172\.(1[6-9]|2\d|3[01])\./.test(h) ||
|
|
209
208
|
/^192\.168\./.test(h) ||
|
|
210
209
|
/^169\.254\./.test(h) ||
|
|
210
|
+
// IPv6 loopback
|
|
211
|
+
h === "::1" ||
|
|
212
|
+
// IPv6 ULA (fc00::/7 covers fc:: and fd::)
|
|
213
|
+
h.startsWith("fc") ||
|
|
214
|
+
h.startsWith("fd") ||
|
|
215
|
+
// IPv6 link-local (fe80::/10)
|
|
216
|
+
h.startsWith("fe80:") ||
|
|
217
|
+
// IPv4-mapped IPv6 (::ffff:10.x.x.x, ::ffff:127.x.x.x, etc.)
|
|
218
|
+
/^::ffff:/i.test(h) ||
|
|
219
|
+
// Raw numeric IPv4 forms not normalized by all URL parsers:
|
|
220
|
+
// decimal (2130706433), hex (0x7f000001), octal (0177.0.0.1 prefix)
|
|
221
|
+
/^(0x[0-9a-f]+|0\d+)$/i.test(h) ||
|
|
222
|
+
// Pure decimal integer that encodes an IPv4 address (8+ digits covers 0.0.0.0+)
|
|
223
|
+
/^\d{8,10}$/.test(h) ||
|
|
211
224
|
h.endsWith(".local") ||
|
|
212
225
|
h.endsWith(".internal");
|
|
213
226
|
if (ssrfBlocked) {
|
|
@@ -226,10 +239,13 @@ export function register(server, ctx) {
|
|
|
226
239
|
newHook = { event, command: command, ...(timeout !== undefined ? { timeout } : {}) };
|
|
227
240
|
}
|
|
228
241
|
return ctx.withWriteQueue(async () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
let totalAfter = 0;
|
|
243
|
+
updateInstallPreferences(phrenPath, (prefs) => {
|
|
244
|
+
const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
|
|
245
|
+
totalAfter = existing.length + 1;
|
|
246
|
+
return { customHooks: [...existing, newHook] };
|
|
247
|
+
});
|
|
248
|
+
return mcpResponse({ ok: true, message: `Added custom hook for "${event}": ${"webhook" in newHook ? "[webhook] " : ""}${getHookTarget(newHook)}`, data: { hook: newHook, total: totalAfter } });
|
|
233
249
|
});
|
|
234
250
|
});
|
|
235
251
|
// ── remove_custom_hook ───────────────────────────────────────────────────
|
|
@@ -242,15 +258,19 @@ export function register(server, ctx) {
|
|
|
242
258
|
}),
|
|
243
259
|
}, async ({ event, command }) => {
|
|
244
260
|
return ctx.withWriteQueue(async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
let removed = 0;
|
|
262
|
+
let remainingCount = 0;
|
|
263
|
+
updateInstallPreferences(phrenPath, (prefs) => {
|
|
264
|
+
const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
|
|
265
|
+
const remaining = existing.filter(h => h.event !== event || (command != null && !getHookTarget(h).includes(command)));
|
|
266
|
+
removed = existing.length - remaining.length;
|
|
267
|
+
remainingCount = remaining.length;
|
|
268
|
+
return { customHooks: remaining };
|
|
269
|
+
});
|
|
249
270
|
if (removed === 0) {
|
|
250
271
|
return mcpResponse({ ok: false, error: `No custom hooks matched event="${event}"${command ? ` command containing "${command}"` : ""}.` });
|
|
251
272
|
}
|
|
252
|
-
|
|
253
|
-
return mcpResponse({ ok: true, message: `Removed ${removed} custom hook(s) for "${event}".`, data: { removed, remaining: remaining.length } });
|
|
273
|
+
return mcpResponse({ ok: true, message: `Removed ${removed} custom hook(s) for "${event}".`, data: { removed, remaining: remainingCount } });
|
|
254
274
|
});
|
|
255
275
|
});
|
|
256
276
|
}
|
package/mcp/dist/mcp-memory.js
CHANGED
package/mcp/dist/mcp-ops.js
CHANGED
|
@@ -11,16 +11,9 @@ import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "./project-co
|
|
|
11
11
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
12
12
|
import { getMachineName } from "./machine-identity.js";
|
|
13
13
|
import { getProjectConsolidationStatus, CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
|
|
14
|
-
/** Translate a PhrenResult<string> into a standard McpToolResult shape. */
|
|
15
|
-
function phrenResultToMcp(result) {
|
|
16
|
-
if (result.ok) {
|
|
17
|
-
return { ok: true, message: result.data };
|
|
18
|
-
}
|
|
19
|
-
return { ok: false, error: result.error, errorCode: result.code };
|
|
20
|
-
}
|
|
21
14
|
export function register(server, ctx) {
|
|
22
|
-
const { phrenPath, profile, withWriteQueue
|
|
23
|
-
// ──
|
|
15
|
+
const { phrenPath, profile, withWriteQueue } = ctx;
|
|
16
|
+
// ── add_project ────────────────────────────────────────────────────────────
|
|
24
17
|
server.registerTool("add_project", {
|
|
25
18
|
title: "◆ phren · add project",
|
|
26
19
|
description: "Bootstrap a project into phren from a repo or working directory. " +
|
|
@@ -221,11 +214,13 @@ export function register(server, ctx) {
|
|
|
221
214
|
const { runDoctor } = await import("./link-doctor.js");
|
|
222
215
|
const result = await runDoctor(phrenPath, true, check_data ?? false);
|
|
223
216
|
const lines = result.checks.map((c) => `${c.ok ? "ok" : "FAIL"} ${c.name}: ${c.detail}`);
|
|
217
|
+
const failCount = result.checks.filter((c) => !c.ok).length;
|
|
224
218
|
return mcpResponse({
|
|
225
219
|
ok: result.ok,
|
|
220
|
+
...(result.ok ? {} : { error: `${failCount} check(s) could not be auto-fixed: ${lines.filter((l) => l.startsWith("FAIL")).join("; ")}` }),
|
|
226
221
|
message: result.ok
|
|
227
222
|
? `Doctor fix complete: all ${result.checks.length} checks passed`
|
|
228
|
-
: `Doctor fix complete: ${
|
|
223
|
+
: `Doctor fix complete: ${failCount} issue(s) remain`,
|
|
229
224
|
data: {
|
|
230
225
|
machine: result.machine,
|
|
231
226
|
profile: result.profile,
|
package/mcp/dist/mcp-search.js
CHANGED
|
@@ -131,7 +131,13 @@ export function register(server, ctx) {
|
|
|
131
131
|
}),
|
|
132
132
|
}, async ({ id: rawId }) => {
|
|
133
133
|
// Normalize ID: decode URL encoding and normalize path separators
|
|
134
|
-
|
|
134
|
+
let id;
|
|
135
|
+
try {
|
|
136
|
+
id = normalizeMemoryId(rawId);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return mcpResponse({ ok: false, error: `Invalid memory ID format: "${rawId}" contains malformed URL encoding.` });
|
|
140
|
+
}
|
|
135
141
|
const match = id.match(/^mem:([^/]+)\/(.+)$/);
|
|
136
142
|
if (!match) {
|
|
137
143
|
return mcpResponse({ ok: false, error: `Invalid memory ID format "${rawId}". Expected mem:project/path/to/file.md.` });
|
package/mcp/dist/mcp-session.js
CHANGED
|
@@ -169,7 +169,7 @@ function findMostRecentSummaryWithProject(phrenPath) {
|
|
|
169
169
|
if (fs.existsSync(fastPath)) {
|
|
170
170
|
const data = JSON.parse(fs.readFileSync(fastPath, "utf-8"));
|
|
171
171
|
if (data.summary)
|
|
172
|
-
return { summary: data.summary, project: data.project };
|
|
172
|
+
return { summary: data.summary, project: data.project, endedAt: data.endedAt };
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
catch (err) {
|
|
@@ -181,7 +181,7 @@ function findMostRecentSummaryWithProject(phrenPath) {
|
|
|
181
181
|
if (results.length === 0)
|
|
182
182
|
return { summary: null };
|
|
183
183
|
const best = results[0]; // already sorted newest-mtime-first
|
|
184
|
-
return { summary: best.data.summary, project: best.data.project };
|
|
184
|
+
return { summary: best.data.summary, project: best.data.project, endedAt: best.data.endedAt };
|
|
185
185
|
}
|
|
186
186
|
/** Resolve session file from an explicit sessionId or a previously-bound connectionId. */
|
|
187
187
|
function resolveSessionFile(phrenPath, sessionId, connectionId) {
|
|
@@ -294,7 +294,7 @@ export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
294
294
|
const shortId = sessionId.slice(0, 8);
|
|
295
295
|
try {
|
|
296
296
|
const projectDirs = getProjectDirs(phrenPath);
|
|
297
|
-
const targetProjects = project ? [project] : projectDirs;
|
|
297
|
+
const targetProjects = project ? [project] : projectDirs.map((d) => path.basename(d));
|
|
298
298
|
for (const proj of targetProjects) {
|
|
299
299
|
// Findings with matching sessionId
|
|
300
300
|
const findingsResult = readFindings(phrenPath, proj);
|
|
@@ -338,6 +338,46 @@ function hasCompletedTasksInSession(phrenPath, sessionId, project) {
|
|
|
338
338
|
const artifacts = getSessionArtifacts(phrenPath, sessionId, project);
|
|
339
339
|
return artifacts.tasks.some((task) => task.section === "Done" && task.checked);
|
|
340
340
|
}
|
|
341
|
+
/** Compute what changed since the last session ended. */
|
|
342
|
+
export function computeSessionDiff(phrenPath, project, lastSessionEnd) {
|
|
343
|
+
const projectDir = path.join(phrenPath, project);
|
|
344
|
+
const findingsPath = path.join(projectDir, "FINDINGS.md");
|
|
345
|
+
if (!fs.existsSync(findingsPath))
|
|
346
|
+
return { newFindings: 0, superseded: 0, tasksCompleted: 0 };
|
|
347
|
+
const content = fs.readFileSync(findingsPath, "utf8");
|
|
348
|
+
const lines = content.split("\n");
|
|
349
|
+
let currentDate = null;
|
|
350
|
+
let newFindings = 0;
|
|
351
|
+
let superseded = 0;
|
|
352
|
+
const cutoff = lastSessionEnd.slice(0, 10);
|
|
353
|
+
for (const line of lines) {
|
|
354
|
+
const dateMatch = line.match(/^## (\d{4}-\d{2}-\d{2})$/);
|
|
355
|
+
if (dateMatch) {
|
|
356
|
+
currentDate = dateMatch[1];
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!line.startsWith("- ") || !currentDate)
|
|
360
|
+
continue;
|
|
361
|
+
if (currentDate >= cutoff) {
|
|
362
|
+
newFindings++;
|
|
363
|
+
if (line.includes('status "superseded"'))
|
|
364
|
+
superseded++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Count tasks completed since last session by checking Done items with recent activity dates
|
|
368
|
+
let tasksCompleted = 0;
|
|
369
|
+
const tasksResult = readTasks(phrenPath, project);
|
|
370
|
+
if (tasksResult.ok) {
|
|
371
|
+
for (const item of tasksResult.data.items.Done) {
|
|
372
|
+
// lastActivity is updated on completion; fall back to createdAt
|
|
373
|
+
const itemDate = item.lastActivity?.slice(0, 10) ?? item.createdAt?.slice(0, 10);
|
|
374
|
+
if (itemDate && itemDate >= cutoff) {
|
|
375
|
+
tasksCompleted++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { newFindings, superseded, tasksCompleted };
|
|
380
|
+
}
|
|
341
381
|
export function register(server, ctx) {
|
|
342
382
|
const { phrenPath } = ctx;
|
|
343
383
|
server.registerTool("session_start", {
|
|
@@ -364,6 +404,7 @@ export function register(server, ctx) {
|
|
|
364
404
|
const priorEnded = prior ? null : findMostRecentSummaryWithProject(phrenPath);
|
|
365
405
|
const priorSummary = prior?.summary ?? priorEnded?.summary ?? null;
|
|
366
406
|
const priorProject = prior?.project ?? priorEnded?.project;
|
|
407
|
+
const priorEndedAt = prior?.endedAt ?? priorEnded?.endedAt;
|
|
367
408
|
// Create new session with unique ID in its own file
|
|
368
409
|
const sessionId = crypto.randomUUID();
|
|
369
410
|
const next = {
|
|
@@ -403,12 +444,12 @@ export function register(server, ctx) {
|
|
|
403
444
|
try {
|
|
404
445
|
const tasks = readTasks(phrenPath, activeProject);
|
|
405
446
|
if (tasks.ok) {
|
|
406
|
-
const
|
|
447
|
+
const activeItems = tasks.data.items.Active
|
|
407
448
|
.filter((entry) => isMemoryScopeVisible(normalizeMemoryScope(entry.scope), activeScope))
|
|
408
449
|
.slice(0, 5)
|
|
409
450
|
.map((entry) => `- [ ] ${entry.line}`);
|
|
410
|
-
if (
|
|
411
|
-
parts.push(`## Active task (${activeProject})\n${
|
|
451
|
+
if (activeItems.length > 0) {
|
|
452
|
+
parts.push(`## Active task (${activeProject})\n${activeItems.join("\n")}`);
|
|
412
453
|
}
|
|
413
454
|
}
|
|
414
455
|
}
|
|
@@ -447,6 +488,25 @@ export function register(server, ctx) {
|
|
|
447
488
|
debugError("session_start checkpointsRead", err);
|
|
448
489
|
}
|
|
449
490
|
}
|
|
491
|
+
// Compute context diff since last session
|
|
492
|
+
if (activeProject && isValidProjectName(activeProject) && priorEndedAt) {
|
|
493
|
+
try {
|
|
494
|
+
const diff = computeSessionDiff(phrenPath, activeProject, priorEndedAt);
|
|
495
|
+
if (diff.newFindings > 0 || diff.superseded > 0 || diff.tasksCompleted > 0) {
|
|
496
|
+
const diffParts = [];
|
|
497
|
+
if (diff.newFindings > 0)
|
|
498
|
+
diffParts.push(`${diff.newFindings} new finding${diff.newFindings === 1 ? "" : "s"}`);
|
|
499
|
+
if (diff.superseded > 0)
|
|
500
|
+
diffParts.push(`${diff.superseded} superseded`);
|
|
501
|
+
if (diff.tasksCompleted > 0)
|
|
502
|
+
diffParts.push(`${diff.tasksCompleted} task${diff.tasksCompleted === 1 ? "" : "s"} in done`);
|
|
503
|
+
parts.push(`## Since last session\n${diffParts.join(", ")}.`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
debugError("session_start contextDiff", err);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
450
510
|
const message = parts.length > 0
|
|
451
511
|
? `Session started (${sessionId.slice(0, 8)}).\n\n${parts.join("\n\n")}`
|
|
452
512
|
: `Session started (${sessionId.slice(0, 8)}). No prior context found.`;
|
package/mcp/dist/mcp-skills.js
CHANGED
|
@@ -120,9 +120,12 @@ export function register(server, ctx) {
|
|
|
120
120
|
}
|
|
121
121
|
fs.mkdirSync(destDir, { recursive: true });
|
|
122
122
|
const existing = findLocalSkill(phrenPath, scope, safeName);
|
|
123
|
-
const dest = existing
|
|
123
|
+
const dest = path.resolve(existing
|
|
124
124
|
? existing.path
|
|
125
|
-
: path.join(destDir, `${safeName}.md`);
|
|
125
|
+
: path.join(destDir, `${safeName}.md`));
|
|
126
|
+
if (!dest.startsWith(phrenPath + path.sep) && dest !== phrenPath) {
|
|
127
|
+
return mcpResponse({ ok: false, error: "Skill path escapes phren store." });
|
|
128
|
+
}
|
|
126
129
|
const existed = Boolean(existing) || fs.existsSync(dest);
|
|
127
130
|
fs.writeFileSync(dest, content);
|
|
128
131
|
updateFileInIndex(dest);
|
package/mcp/dist/mcp-tasks.js
CHANGED
|
@@ -159,9 +159,12 @@ export function register(server, ctx) {
|
|
|
159
159
|
data: { project, includedSections: view.includedSections, totalItems: view.totalItems, summary: true },
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
+
const sectionCounts = view.includedSections
|
|
163
|
+
.map((s) => `${s}: ${view.doc.items[s].length}/${doc.items[s].length}`)
|
|
164
|
+
.join(", ");
|
|
162
165
|
const paginationNote = view.truncated
|
|
163
|
-
? `\n\
|
|
164
|
-
: (offset ? `\n\n_Page offset: ${offset}. ${
|
|
166
|
+
? `\n\n_${sectionCounts} (offset ${offset ?? 0}). Use offset/limit to page._`
|
|
167
|
+
: (offset ? `\n\n_Page offset: ${offset}. ${sectionCounts}._` : "");
|
|
165
168
|
return mcpResponse({
|
|
166
169
|
ok: true,
|
|
167
170
|
message: `## ${project}\n${taskMarkdown(view.doc)}${paginationNote}`,
|
|
@@ -231,7 +234,7 @@ export function register(server, ctx) {
|
|
|
231
234
|
const { added, errors } = result.data;
|
|
232
235
|
if (added.length > 0)
|
|
233
236
|
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
234
|
-
return mcpResponse({ ok: added.length > 0, message: `Added ${added.length} of ${items.length} tasks to ${project}`, data: { project, added, errors } });
|
|
237
|
+
return mcpResponse({ ok: added.length > 0, ...(added.length === 0 ? { error: `No tasks added: ${errors.join("; ")}` } : {}), message: `Added ${added.length} of ${items.length} tasks to ${project}`, data: { project, added, errors } });
|
|
235
238
|
});
|
|
236
239
|
});
|
|
237
240
|
server.registerTool("complete_task", {
|
|
@@ -303,7 +306,7 @@ export function register(server, ctx) {
|
|
|
303
306
|
}
|
|
304
307
|
if (completed.length > 0)
|
|
305
308
|
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
306
|
-
return mcpResponse({ ok: completed.length > 0, message: `Completed ${completed.length}/${items.length} items`, data: { project, completed, errors } });
|
|
309
|
+
return mcpResponse({ ok: completed.length > 0, ...(completed.length === 0 ? { error: `No tasks completed: ${errors.join("; ")}` } : {}), message: `Completed ${completed.length}/${items.length} items`, data: { project, completed, errors } });
|
|
307
310
|
});
|
|
308
311
|
});
|
|
309
312
|
server.registerTool("remove_task", {
|