@phren/cli 0.0.41 → 0.0.43
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/mcp/dist/cli/actions.js +1 -1
- package/mcp/dist/cli/cli.js +3 -1
- package/mcp/dist/cli/hooks-session.js +8 -8
- package/mcp/dist/cli/namespaces.js +110 -3
- package/mcp/dist/cli-hooks-session-handlers.js +8 -8
- package/mcp/dist/content/dedup.js +5 -2
- package/mcp/dist/entrypoint.js +4 -3
- package/mcp/dist/finding/context.js +3 -2
- package/mcp/dist/finding/journal.js +88 -1
- package/mcp/dist/init/config.js +1 -1
- package/mcp/dist/init/init-configure.js +1 -1
- package/mcp/dist/init/init-hooks-mode.js +1 -1
- package/mcp/dist/init/init-mcp-mode.js +2 -2
- package/mcp/dist/init/init-walkthrough.js +9 -9
- package/mcp/dist/init/init.js +8 -8
- package/mcp/dist/init/setup.js +6 -0
- package/mcp/dist/init-fresh.js +4 -4
- package/mcp/dist/init-hooks.js +1 -1
- package/mcp/dist/init-modes.js +3 -3
- package/mcp/dist/init-update.js +3 -3
- package/mcp/dist/init-walkthrough.js +9 -9
- package/mcp/dist/link/link.js +1 -1
- package/mcp/dist/phren-paths.js +2 -2
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/status.js +1 -1
- package/mcp/dist/tools/finding.js +37 -5
- package/mcp/dist/tools/search.js +81 -20
- package/mcp/dist/tools/session.js +10 -4
- package/mcp/dist/tools/tasks.js +78 -8
- package/mcp/dist/tools/types.js +22 -0
- package/package.json +1 -1
- package/skills/sync/SKILL.md +1 -1
- package/starter/README.md +6 -6
- package/starter/machines.yaml +1 -1
- package/starter/my-first-project/tasks.md +1 -1
- package/starter/templates/README.md +1 -1
package/mcp/dist/init-fresh.js
CHANGED
|
@@ -197,8 +197,8 @@ export async function runFreshInstall(phrenPath, opts, params) {
|
|
|
197
197
|
log(`\nNext steps:`);
|
|
198
198
|
let step = 1;
|
|
199
199
|
log(` ${step++}. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
200
|
-
log(` ${step++}. Run \`
|
|
201
|
-
log(` ${step++}. Change defaults anytime: \`
|
|
200
|
+
log(` ${step++}. Run \`phren doctor\` to verify everything is wired correctly`);
|
|
201
|
+
log(` ${step++}. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
|
|
202
202
|
const gh = opts._walkthroughGithub;
|
|
203
203
|
if (gh) {
|
|
204
204
|
const remote = gh.username
|
|
@@ -223,9 +223,9 @@ export async function runFreshInstall(phrenPath, opts, params) {
|
|
|
223
223
|
log(` git remote add origin git@github.com:YOUR_USERNAME/my-phren.git`);
|
|
224
224
|
log(` git push -u origin main`);
|
|
225
225
|
}
|
|
226
|
-
log(` ${step++}. Add more projects: cd ~/your-project &&
|
|
226
|
+
log(` ${step++}. Add more projects: cd ~/your-project && phren add`);
|
|
227
227
|
if (!mcpEnabled) {
|
|
228
|
-
log(` ${step++}. Turn MCP on:
|
|
228
|
+
log(` ${step++}. Turn MCP on: phren mcp-mode on`);
|
|
229
229
|
}
|
|
230
230
|
log(` ${step++}. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
231
231
|
log(` ${step++}. After working across projects, run phren-consolidate to find cross-project patterns`);
|
package/mcp/dist/init-hooks.js
CHANGED
package/mcp/dist/init-modes.js
CHANGED
|
@@ -24,8 +24,8 @@ export async function runMcpMode(modeArg) {
|
|
|
24
24
|
const hooks = getHooksEnabledPreference(phrenPath);
|
|
25
25
|
log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
|
|
26
26
|
log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
|
|
27
|
-
log(`Change mode:
|
|
28
|
-
log(`Hooks toggle:
|
|
27
|
+
log(`Change mode: phren mcp-mode on|off`);
|
|
28
|
+
log(`Hooks toggle: phren hooks-mode on|off`);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
const mode = parseMcpMode(normalizedArg);
|
|
@@ -93,7 +93,7 @@ export async function runHooksMode(modeArg) {
|
|
|
93
93
|
if (!normalizedArg || normalizedArg === "status") {
|
|
94
94
|
const current = getHooksEnabledPreference(phrenPath);
|
|
95
95
|
log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
|
|
96
|
-
log(`Change mode:
|
|
96
|
+
log(`Change mode: phren hooks-mode on|off`);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
const mode = parseMcpMode(normalizedArg);
|
package/mcp/dist/init-update.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function runExistingInstallUpdate(phrenPath, opts, params) {
|
|
|
53
53
|
const previousVersion = prefs.installedVersion;
|
|
54
54
|
if (isVersionNewer(VERSION, previousVersion)) {
|
|
55
55
|
log(`\n Starter template update available: v${previousVersion} -> v${VERSION}`);
|
|
56
|
-
log(` Run \`
|
|
56
|
+
log(` Run \`phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
|
|
57
57
|
}
|
|
58
58
|
if (opts.applyStarterUpdate) {
|
|
59
59
|
const updated = applyStarterTemplateUpdates(phrenPath);
|
|
@@ -88,8 +88,8 @@ export async function runExistingInstallUpdate(phrenPath, opts, params) {
|
|
|
88
88
|
log(`\n\x1b[95m◆\x1b[0m phren updated successfully`);
|
|
89
89
|
log(`\nNext steps:`);
|
|
90
90
|
log(` 1. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
91
|
-
log(` 2. Run \`
|
|
92
|
-
log(` 3. Change defaults anytime: \`
|
|
91
|
+
log(` 2. Run \`phren doctor\` to verify everything is wired correctly`);
|
|
92
|
+
log(` 3. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
|
|
93
93
|
log(` 4. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
94
94
|
log(` 5. After working across projects, run phren-consolidate to find cross-project patterns`);
|
|
95
95
|
log(``);
|
|
@@ -269,7 +269,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
269
269
|
log(" phren-managed: Phren may mirror CLAUDE.md / AGENTS.md into the repo");
|
|
270
270
|
log(" detached: Phren keeps its own docs but does not write into the repo");
|
|
271
271
|
log(" repo-managed: keep the repo's existing CLAUDE/AGENTS files as canonical");
|
|
272
|
-
log(" Change later:
|
|
272
|
+
log(" Change later: phren config project-ownership <mode>");
|
|
273
273
|
const projectOwnershipDefault = await prompts.select("Default project ownership", [
|
|
274
274
|
{ value: "detached", name: "detached (default)" },
|
|
275
275
|
{ value: "phren-managed", name: "phren-managed" },
|
|
@@ -280,7 +280,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
280
280
|
log("directly: search memory, manage tasks, save findings, etc.");
|
|
281
281
|
log(" Recommended for: Claude Code, Cursor, Copilot CLI, Codex");
|
|
282
282
|
log(" Alternative: hooks-only mode (read-only context injection, any agent)");
|
|
283
|
-
log(" Change later:
|
|
283
|
+
log(" Change later: phren mcp-mode on|off");
|
|
284
284
|
const mcp = (await prompts.confirm("Enable MCP?", true)) ? "on" : "off";
|
|
285
285
|
printSection("Hooks");
|
|
286
286
|
log("Hooks run shell commands at session start, prompt submit, and session end.");
|
|
@@ -288,7 +288,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
288
288
|
log(" - UserPromptSubmit: searches phren and injects relevant context");
|
|
289
289
|
log(" - Stop: commits and pushes any new findings after each response");
|
|
290
290
|
log(" What they touch: ~/.claude/settings.json (hooks section only)");
|
|
291
|
-
log(" Change later:
|
|
291
|
+
log(" Change later: phren hooks-mode on|off");
|
|
292
292
|
const hooks = (await prompts.confirm("Enable hooks?", true)) ? "on" : "off";
|
|
293
293
|
printSection("Semantic Search (Optional)");
|
|
294
294
|
log("Phren can use a local embedding model for semantic (fuzzy) search via Ollama.");
|
|
@@ -336,7 +336,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
336
336
|
let findingsProactivity = "high";
|
|
337
337
|
if (autoCaptureEnabled) {
|
|
338
338
|
log(" Findings capture level controls how eager phren is to save lessons automatically.");
|
|
339
|
-
log(" Change later:
|
|
339
|
+
log(" Change later: phren config proactivity.findings <high|medium|low>");
|
|
340
340
|
findingsProactivity = await prompts.select("Findings capture level", [
|
|
341
341
|
{ value: "high", name: "high (recommended)" },
|
|
342
342
|
{ value: "medium", name: "medium" },
|
|
@@ -352,7 +352,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
352
352
|
log(" suggest: proposes tasks but waits for approval before writing");
|
|
353
353
|
log(" manual: tasks are fully manual — you add them yourself");
|
|
354
354
|
log(" off: never touch tasks automatically");
|
|
355
|
-
log(" Change later:
|
|
355
|
+
log(" Change later: phren config workflow set --taskMode=<mode>");
|
|
356
356
|
const taskMode = await prompts.select("Task mode", [
|
|
357
357
|
{ value: "auto", name: "auto (recommended)" },
|
|
358
358
|
{ value: "suggest", name: "suggest" },
|
|
@@ -365,7 +365,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
365
365
|
log(" high (recommended): captures tasks as they come up naturally");
|
|
366
366
|
log(" medium: only when you explicitly mention a task");
|
|
367
367
|
log(" low: minimal auto-capture");
|
|
368
|
-
log(" Change later:
|
|
368
|
+
log(" Change later: phren config proactivity.tasks <high|medium|low>");
|
|
369
369
|
taskProactivity = await prompts.select("Task proactivity", [
|
|
370
370
|
{ value: "high", name: "high (recommended)" },
|
|
371
371
|
{ value: "medium", name: "medium" },
|
|
@@ -376,7 +376,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
376
376
|
log("Choose how strict review gates should be for risky or low-confidence writes.");
|
|
377
377
|
log(" lowConfidenceThreshold: confidence cutoff used to mark writes as risky");
|
|
378
378
|
log(" riskySections: sections always treated as risky");
|
|
379
|
-
log(" Change later:
|
|
379
|
+
log(" Change later: phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
|
|
380
380
|
const thresholdAnswer = await prompts.input("Low-confidence threshold [0.0-1.0]", "0.7");
|
|
381
381
|
const lowConfidenceThreshold = parseLowConfidenceThreshold(thresholdAnswer, 0.7);
|
|
382
382
|
const riskySectionsAnswer = await prompts.input("Risky sections [Review,Stale,Conflicts]", "Stale,Conflicts");
|
|
@@ -426,7 +426,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
426
426
|
log(" conservative — decisions and pitfalls only");
|
|
427
427
|
log(" balanced — non-obvious patterns, decisions, pitfalls, bugs (recommended)");
|
|
428
428
|
log(" aggressive — everything worth remembering, err on the side of capturing");
|
|
429
|
-
log(" Change later:
|
|
429
|
+
log(" Change later: phren config finding-sensitivity <level>");
|
|
430
430
|
const findingSensitivity = await prompts.select("Finding sensitivity", [
|
|
431
431
|
{ value: "balanced", name: "balanced (recommended)" },
|
|
432
432
|
{ value: "conservative", name: "conservative" },
|
|
@@ -455,7 +455,7 @@ export async function runWalkthrough(phrenPath) {
|
|
|
455
455
|
bootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
456
456
|
if (!bootstrapCurrentProject) {
|
|
457
457
|
bootstrapCurrentProject = false;
|
|
458
|
-
log(style.warning(` Skipped. Later: cd ${detectedProject} &&
|
|
458
|
+
log(style.warning(` Skipped. Later: cd ${detectedProject} && phren add`));
|
|
459
459
|
}
|
|
460
460
|
else {
|
|
461
461
|
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
package/mcp/dist/link/link.js
CHANGED
|
@@ -76,7 +76,7 @@ function maybeOfferStarterTemplateUpdate(phrenPath) {
|
|
|
76
76
|
const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
|
|
77
77
|
if (isVersionNewer(current, prefs.installedVersion)) {
|
|
78
78
|
log(` Starter template update available: v${prefs.installedVersion} -> v${current}`);
|
|
79
|
-
log(` Run \`
|
|
79
|
+
log(` Run \`phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
catch (err) {
|
package/mcp/dist/phren-paths.js
CHANGED
|
@@ -209,7 +209,7 @@ export function findPhrenPathWithArg(arg) {
|
|
|
209
209
|
const existing = findPhrenPath();
|
|
210
210
|
if (existing)
|
|
211
211
|
return existing;
|
|
212
|
-
throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run '
|
|
212
|
+
throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run 'phren init'.`);
|
|
213
213
|
}
|
|
214
214
|
export function isProjectLocalMode(phrenPath) {
|
|
215
215
|
try {
|
|
@@ -513,7 +513,7 @@ export function getPhrenPath() {
|
|
|
513
513
|
if (!lazyPhrenPath) {
|
|
514
514
|
const existing = findPhrenPath();
|
|
515
515
|
if (!existing)
|
|
516
|
-
throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run '
|
|
516
|
+
throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run 'phren init'.`);
|
|
517
517
|
lazyPhrenPath = existing;
|
|
518
518
|
}
|
|
519
519
|
return lazyPhrenPath;
|
|
@@ -45,7 +45,7 @@ export function resolveActiveProfile(phrenPath, requestedProfile) {
|
|
|
45
45
|
export function listMachines(phrenPath) {
|
|
46
46
|
const machinesPath = path.join(phrenPath, "machines.yaml");
|
|
47
47
|
if (!fs.existsSync(machinesPath))
|
|
48
|
-
return phrenErr(`machines.yaml not found. Run '
|
|
48
|
+
return phrenErr(`machines.yaml not found. Run 'phren init' to set up your phren.`, PhrenError.FILE_NOT_FOUND);
|
|
49
49
|
try {
|
|
50
50
|
const raw = fs.readFileSync(machinesPath, "utf8");
|
|
51
51
|
const parsed = yaml.load(raw, { schema: yaml.CORE_SCHEMA });
|
|
@@ -205,7 +205,7 @@ export function getActiveProfileDefaults(phrenPath, profile) {
|
|
|
205
205
|
export function listProfiles(phrenPath) {
|
|
206
206
|
const profilesDir = path.join(phrenPath, "profiles");
|
|
207
207
|
if (!fs.existsSync(profilesDir))
|
|
208
|
-
return phrenErr(`No profiles/ directory found. Run '
|
|
208
|
+
return phrenErr(`No profiles/ directory found. Run 'phren init' to set up your phren.`, PhrenError.FILE_NOT_FOUND);
|
|
209
209
|
const files = fs.readdirSync(profilesDir).filter((file) => file.endsWith(".yaml")).sort();
|
|
210
210
|
const profiles = [];
|
|
211
211
|
for (const file of files) {
|
package/mcp/dist/status.js
CHANGED
|
@@ -53,7 +53,7 @@ function hasCommandHook(value) {
|
|
|
53
53
|
export async function runStatus() {
|
|
54
54
|
const phrenPath = findPhrenPath();
|
|
55
55
|
if (!phrenPath) {
|
|
56
|
-
console.log(`${RED}phren not found${RESET}. Run ${CYAN}npx phren init${RESET} to set up.`);
|
|
56
|
+
console.log(`${RED}phren not found${RESET}. Run ${CYAN}npx @phren/cli init${RESET} to set up.`);
|
|
57
57
|
process.exit(1);
|
|
58
58
|
}
|
|
59
59
|
const cwd = process.cwd();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mcpResponse } from "./types.js";
|
|
1
|
+
import { mcpResponse, resolveStoreForProject } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -17,8 +17,8 @@ import { getActiveTaskForSession } from "../task/lifecycle.js";
|
|
|
17
17
|
import { FINDING_PROVENANCE_SOURCES } from "../content/citation.js";
|
|
18
18
|
import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "../finding/lifecycle.js";
|
|
19
19
|
import { permissionDeniedError } from "../governance/rbac.js";
|
|
20
|
-
const JACCARD_MAYBE_LOW = 0.
|
|
21
|
-
const JACCARD_MAYBE_HIGH = 0.
|
|
20
|
+
const JACCARD_MAYBE_LOW = 0.25;
|
|
21
|
+
const JACCARD_MAYBE_HIGH = 0.40; // above this isDuplicateFinding already catches it
|
|
22
22
|
function findJaccardCandidates(phrenPath, project, finding) {
|
|
23
23
|
try {
|
|
24
24
|
const findingsPath = path.join(phrenPath, project, "FINDINGS.md");
|
|
@@ -84,13 +84,45 @@ function withLifecycleMutation(phrenPath, project, writeQueue, updateIndex, hand
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
87
|
-
async function handleAddFinding(ctx,
|
|
88
|
-
const {
|
|
87
|
+
async function handleAddFinding(ctx, params) {
|
|
88
|
+
const { finding, citation, sessionId, source, findingType, scope } = params;
|
|
89
|
+
// Resolve store-qualified project names (e.g., "team/arc" → store path + "arc")
|
|
90
|
+
let phrenPath;
|
|
91
|
+
let project;
|
|
92
|
+
try {
|
|
93
|
+
const resolved = resolveStoreForProject(ctx, params.project);
|
|
94
|
+
phrenPath = resolved.phrenPath;
|
|
95
|
+
project = resolved.project;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
99
|
+
}
|
|
100
|
+
const { withWriteQueue, updateFileInIndex } = ctx;
|
|
89
101
|
if (!isValidProjectName(project))
|
|
90
102
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
91
103
|
const addFindingDenied = permissionDeniedError(phrenPath, "add_finding", project);
|
|
92
104
|
if (addFindingDenied)
|
|
93
105
|
return mcpResponse({ ok: false, error: addFindingDenied });
|
|
106
|
+
// Team stores: use append-only journal (no FINDINGS.md mutation, no merge conflicts)
|
|
107
|
+
{
|
|
108
|
+
const storeResolved = resolveStoreForProject(ctx, params.project);
|
|
109
|
+
if (storeResolved.storeRole === "team") {
|
|
110
|
+
const { appendTeamJournal } = await import("../finding/journal.js");
|
|
111
|
+
const findings = Array.isArray(finding) ? finding : [finding];
|
|
112
|
+
const added = [];
|
|
113
|
+
for (const f of findings) {
|
|
114
|
+
const taggedFinding = findingType ? `[${findingType}] ${f}` : f;
|
|
115
|
+
const result = appendTeamJournal(phrenPath, project, taggedFinding);
|
|
116
|
+
if (result.ok)
|
|
117
|
+
added.push(taggedFinding);
|
|
118
|
+
}
|
|
119
|
+
return mcpResponse({
|
|
120
|
+
ok: added.length > 0,
|
|
121
|
+
message: `Added ${added.length} finding(s) to ${params.project} journal`,
|
|
122
|
+
data: { project: params.project, added, journalMode: true },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
94
126
|
if (Array.isArray(finding)) {
|
|
95
127
|
const findings = finding;
|
|
96
128
|
if (findings.length > 100)
|
package/mcp/dist/tools/search.js
CHANGED
|
@@ -458,8 +458,28 @@ async function handleSearchKnowledge(ctx, { query, limit, project, type, tag, si
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
async function handleGetProjectSummary(ctx, { name }) {
|
|
461
|
+
// Support store-qualified names (e.g., "team/arc")
|
|
462
|
+
const { parseStoreQualified } = await import("../store-routing.js");
|
|
463
|
+
const { storeName, projectName } = parseStoreQualified(name);
|
|
464
|
+
const lookupName = projectName;
|
|
461
465
|
const db = ctx.db();
|
|
462
|
-
|
|
466
|
+
let docs = queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ?", [lookupName]);
|
|
467
|
+
// If not in primary index and store-qualified, try reading from the store's filesystem
|
|
468
|
+
if (!docs && storeName) {
|
|
469
|
+
const store = resolveAllStores(ctx.phrenPath).find((s) => s.name === storeName);
|
|
470
|
+
if (store && fs.existsSync(path.join(store.path, lookupName))) {
|
|
471
|
+
const projDir = path.join(store.path, lookupName);
|
|
472
|
+
const fsDocs = [];
|
|
473
|
+
for (const [file, type] of [["summary.md", "summary"], ["CLAUDE.md", "claude"], ["FINDINGS.md", "findings"], ["tasks.md", "task"]]) {
|
|
474
|
+
const filePath = path.join(projDir, file);
|
|
475
|
+
if (fs.existsSync(filePath)) {
|
|
476
|
+
fsDocs.push({ filename: file, type, content: fs.readFileSync(filePath, "utf8").slice(0, 8000), path: filePath });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (fsDocs.length > 0)
|
|
480
|
+
docs = fsDocs;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
463
483
|
if (!docs) {
|
|
464
484
|
const projectRows = queryRows(db, "SELECT DISTINCT project FROM docs ORDER BY project", []);
|
|
465
485
|
const names = projectRows ? projectRows.map(row => decodeStringRow(row, 1, "get_project_summary.projects")[0]) : [];
|
|
@@ -494,42 +514,83 @@ async function handleGetProjectSummary(ctx, { name }) {
|
|
|
494
514
|
async function handleListProjects(ctx, { page, page_size }) {
|
|
495
515
|
const { phrenPath, profile } = ctx;
|
|
496
516
|
const db = ctx.db();
|
|
517
|
+
// Gather projects from primary store index
|
|
497
518
|
const projectRows = queryRows(db, "SELECT DISTINCT project FROM docs ORDER BY project", []);
|
|
498
|
-
|
|
519
|
+
const primaryProjects = projectRows
|
|
520
|
+
? projectRows.map(row => decodeStringRow(row, 1, "list_projects.projects")[0])
|
|
521
|
+
: [];
|
|
522
|
+
// Gather projects from non-primary stores
|
|
523
|
+
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
524
|
+
const { getProjectDirs } = await import("../phren-paths.js");
|
|
525
|
+
const nonPrimaryStores = getNonPrimaryStores(phrenPath);
|
|
526
|
+
const storeProjects = [];
|
|
527
|
+
for (const store of nonPrimaryStores) {
|
|
528
|
+
if (!fs.existsSync(store.path))
|
|
529
|
+
continue;
|
|
530
|
+
const dirs = getProjectDirs(store.path);
|
|
531
|
+
for (const dir of dirs) {
|
|
532
|
+
const projName = path.basename(dir);
|
|
533
|
+
storeProjects.push({ name: projName, store: store.name });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Combine: primary projects (no store prefix) + non-primary (with store prefix)
|
|
537
|
+
const allProjects = [
|
|
538
|
+
...primaryProjects.map((p) => ({ name: p, store: undefined })),
|
|
539
|
+
...storeProjects,
|
|
540
|
+
];
|
|
541
|
+
if (allProjects.length === 0)
|
|
499
542
|
return mcpResponse({ ok: true, message: "No projects indexed.", data: { projects: [], total: 0 } });
|
|
500
|
-
const projects = projectRows.map(row => decodeStringRow(row, 1, "list_projects.projects")[0]);
|
|
501
543
|
const pageSize = page_size ?? 20;
|
|
502
544
|
const pageNum = page ?? 1;
|
|
503
545
|
const start = Math.max(0, (pageNum - 1) * pageSize);
|
|
504
546
|
const end = start + pageSize;
|
|
505
|
-
const pageProjects =
|
|
506
|
-
const totalPages = Math.max(1, Math.ceil(
|
|
547
|
+
const pageProjects = allProjects.slice(start, end);
|
|
548
|
+
const totalPages = Math.max(1, Math.ceil(allProjects.length / pageSize));
|
|
507
549
|
if (pageNum > totalPages) {
|
|
508
550
|
return mcpResponse({ ok: false, error: `Page ${pageNum} out of range. Total pages: ${totalPages}.` });
|
|
509
551
|
}
|
|
510
552
|
const badgeTypes = ["claude", "findings", "summary", "task"];
|
|
511
553
|
const badgeLabels = { claude: "CLAUDE.md", findings: "FINDINGS", summary: "summary", task: "task" };
|
|
512
|
-
const projectList = pageProjects.map((
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
554
|
+
const projectList = pageProjects.map((entry) => {
|
|
555
|
+
// Primary store projects: query the DB for badge info
|
|
556
|
+
if (!entry.store) {
|
|
557
|
+
const rows = queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ?", [entry.name]) ?? [];
|
|
558
|
+
const types = rows.map(row => row.type);
|
|
559
|
+
const summaryRow = rows.find(row => row.type === "summary");
|
|
560
|
+
const claudeRow = rows.find(row => row.type === "claude");
|
|
561
|
+
const source = summaryRow?.content ?? claudeRow?.content;
|
|
562
|
+
let brief = "";
|
|
563
|
+
if (source) {
|
|
564
|
+
const firstLine = source.split("\n").find(l => l.trim() && !l.startsWith("#"));
|
|
565
|
+
brief = firstLine?.trim() || "";
|
|
566
|
+
}
|
|
567
|
+
const badges = badgeTypes.filter(t => types.includes(t)).map(t => badgeLabels[t]);
|
|
568
|
+
return { name: entry.name, store: undefined, brief, badges, fileCount: rows.length };
|
|
569
|
+
}
|
|
570
|
+
// Non-primary store projects: basic info (no DB query, just check file existence)
|
|
571
|
+
const store = nonPrimaryStores.find((s) => s.name === entry.store);
|
|
572
|
+
const projDir = store ? path.join(store.path, entry.name) : "";
|
|
573
|
+
const badges = [];
|
|
574
|
+
if (projDir) {
|
|
575
|
+
if (fs.existsSync(path.join(projDir, "CLAUDE.md")))
|
|
576
|
+
badges.push("CLAUDE.md");
|
|
577
|
+
if (fs.existsSync(path.join(projDir, "FINDINGS.md")))
|
|
578
|
+
badges.push("FINDINGS");
|
|
579
|
+
if (fs.existsSync(path.join(projDir, "summary.md")))
|
|
580
|
+
badges.push("summary");
|
|
581
|
+
if (fs.existsSync(path.join(projDir, "tasks.md")))
|
|
582
|
+
badges.push("task");
|
|
522
583
|
}
|
|
523
|
-
|
|
524
|
-
return { name: proj, brief, badges, fileCount: rows.length };
|
|
584
|
+
return { name: entry.name, store: entry.store, brief: "", badges, fileCount: badges.length };
|
|
525
585
|
});
|
|
526
|
-
const lines = [`# Phren Projects (${
|
|
586
|
+
const lines = [`# Phren Projects (${allProjects.length})`];
|
|
527
587
|
if (profile)
|
|
528
588
|
lines.push(`Profile: ${profile}`);
|
|
529
589
|
lines.push(`Page: ${pageNum}/${totalPages} (page_size=${pageSize})`);
|
|
530
590
|
lines.push(`Path: ${phrenPath}\n`);
|
|
531
591
|
for (const p of projectList) {
|
|
532
|
-
|
|
592
|
+
const storeTag = p.store ? ` (${p.store})` : "";
|
|
593
|
+
lines.push(`## ${p.name}${storeTag}`);
|
|
533
594
|
if (p.brief)
|
|
534
595
|
lines.push(p.brief);
|
|
535
596
|
lines.push(`[${p.badges.join(" | ")}] - ${p.fileCount} file(s)\n`);
|
|
@@ -537,7 +598,7 @@ async function handleListProjects(ctx, { page, page_size }) {
|
|
|
537
598
|
return mcpResponse({
|
|
538
599
|
ok: true,
|
|
539
600
|
message: lines.join("\n"),
|
|
540
|
-
data: { projects: projectList, total:
|
|
601
|
+
data: { projects: projectList, total: allProjects.length, page: pageNum, totalPages, pageSize },
|
|
541
602
|
});
|
|
542
603
|
}
|
|
543
604
|
async function handleGetFindings(ctx, { project, limit, include_superseded, include_history, status }) {
|
|
@@ -18,6 +18,7 @@ import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints
|
|
|
18
18
|
import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
|
|
19
19
|
import { atomicWriteJson, debugError, scanSessionFiles } from "../session/utils.js";
|
|
20
20
|
import { getRuntimeHealth } from "../governance/policy.js";
|
|
21
|
+
import { getProjectSourcePath, readProjectConfig } from "../project-config.js";
|
|
21
22
|
const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
23
|
function collectGitStatusSnapshot(cwd) {
|
|
23
24
|
try {
|
|
@@ -221,9 +222,11 @@ function cleanupStaleSessions(phrenPath) {
|
|
|
221
222
|
// (no endedAt) should never be removed regardless of age.
|
|
222
223
|
if (state && !state.endedAt)
|
|
223
224
|
continue;
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
// For ended sessions, age out by end time rather than start time so
|
|
226
|
+
// long-running sessions do not disappear immediately after they finish.
|
|
227
|
+
const expirationAnchor = state?.endedAt || state?.startedAt;
|
|
228
|
+
const ageMs = expirationAnchor
|
|
229
|
+
? Date.now() - new Date(expirationAnchor).getTime()
|
|
227
230
|
: Date.now() - fs.statSync(fullPath).mtimeMs;
|
|
228
231
|
if (ageMs > STALE_SESSION_MS) {
|
|
229
232
|
fs.unlinkSync(fullPath);
|
|
@@ -607,7 +610,10 @@ export function register(server, ctx) {
|
|
|
607
610
|
})();
|
|
608
611
|
if (activeTask) {
|
|
609
612
|
const taskId = activeTask.stableId || activeTask.id;
|
|
610
|
-
const
|
|
613
|
+
const projectConfig = readProjectConfig(phrenPath, endedState.project);
|
|
614
|
+
const snapshotRoot = getProjectSourcePath(phrenPath, endedState.project, projectConfig) ||
|
|
615
|
+
path.join(phrenPath, endedState.project);
|
|
616
|
+
const { gitStatus, editedFiles } = collectGitStatusSnapshot(snapshotRoot);
|
|
611
617
|
const resumptionHint = extractResumptionHint(effectiveSummary, activeTask.line, activeTask.context || "No prior attempt captured");
|
|
612
618
|
writeTaskCheckpoint(phrenPath, {
|
|
613
619
|
project: endedState.project,
|
package/mcp/dist/tools/tasks.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mcpResponse } from "./types.js";
|
|
1
|
+
import { mcpResponse, resolveStoreForProject } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { isValidProjectName } from "../utils.js";
|
|
6
6
|
import { addTask as addTaskStore, addTasks as addTasksBatch, taskMarkdown, completeTask as completeTaskStore, completeTasks as completeTasksBatch, removeTask as removeTaskStore, removeTasks as removeTasksBatch, linkTaskIssue, pinTask, workNextTask, tidyDoneTasks, readTasks, readTasksAcrossProjects, resolveTaskItem, TASKS_FILENAME, updateTask as updateTaskStore, promoteTask, } from "../data/access.js";
|
|
7
7
|
import { applyGravity } from "../data/tasks.js";
|
|
8
|
-
import { parseGithubIssueUrl, } from "../task/github.js";
|
|
8
|
+
import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo, } from "../task/github.js";
|
|
9
9
|
import { clearTaskCheckpoint } from "../session/checkpoints.js";
|
|
10
10
|
import { incrementSessionTasksCompleted } from "./session.js";
|
|
11
11
|
import { normalizeMemoryScope } from "../shared.js";
|
|
@@ -207,10 +207,21 @@ export function register(server, ctx) {
|
|
|
207
207
|
]).describe("The task(s) to add. Pass a string for one task, or an array for bulk."),
|
|
208
208
|
scope: z.string().optional().describe("Optional memory scope label. Defaults to 'shared'. Example: 'researcher' or 'builder'."),
|
|
209
209
|
}),
|
|
210
|
-
}, async ({ project, item, scope }) => {
|
|
210
|
+
}, async ({ project: projectInput, item, scope }) => {
|
|
211
|
+
// Resolve store-qualified project names (e.g., "team/arc")
|
|
212
|
+
let targetPhrenPath;
|
|
213
|
+
let project;
|
|
214
|
+
try {
|
|
215
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
216
|
+
targetPhrenPath = resolved.phrenPath;
|
|
217
|
+
project = resolved.project;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
221
|
+
}
|
|
211
222
|
if (!isValidProjectName(project))
|
|
212
223
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
213
|
-
const addTaskDenied = permissionDeniedError(
|
|
224
|
+
const addTaskDenied = permissionDeniedError(targetPhrenPath, "add_task", project);
|
|
214
225
|
if (addTaskDenied)
|
|
215
226
|
return mcpResponse({ ok: false, error: addTaskDenied });
|
|
216
227
|
const normalizedScope = normalizeMemoryScope(scope ?? "shared");
|
|
@@ -218,20 +229,20 @@ export function register(server, ctx) {
|
|
|
218
229
|
return mcpResponse({ ok: false, error: `Invalid scope: "${scope}". Use lowercase letters/numbers with '-' or '_' (max 64 chars), e.g. "researcher".` });
|
|
219
230
|
if (Array.isArray(item)) {
|
|
220
231
|
return withWriteQueue(async () => {
|
|
221
|
-
const result = addTasksBatch(
|
|
232
|
+
const result = addTasksBatch(targetPhrenPath, project, item, { scope: normalizedScope });
|
|
222
233
|
if (!result.ok)
|
|
223
234
|
return mcpResponse({ ok: false, error: result.error });
|
|
224
235
|
const { added, errors } = result.data;
|
|
225
236
|
if (added.length > 0)
|
|
226
|
-
refreshTaskIndex(updateFileInIndex,
|
|
237
|
+
refreshTaskIndex(updateFileInIndex, targetPhrenPath, project);
|
|
227
238
|
return mcpResponse({ ok: added.length > 0, ...(added.length === 0 ? { error: `No tasks added: ${errors.join("; ")}` } : {}), message: `Added ${added.length} of ${item.length} tasks to ${project}`, data: { project, added, errors } });
|
|
228
239
|
});
|
|
229
240
|
}
|
|
230
241
|
return withWriteQueue(async () => {
|
|
231
|
-
const result = addTaskStore(
|
|
242
|
+
const result = addTaskStore(targetPhrenPath, project, item, { scope: normalizedScope });
|
|
232
243
|
if (!result.ok)
|
|
233
244
|
return mcpResponse({ ok: false, error: result.error });
|
|
234
|
-
refreshTaskIndex(updateFileInIndex,
|
|
245
|
+
refreshTaskIndex(updateFileInIndex, targetPhrenPath, project);
|
|
235
246
|
return mcpResponse({ ok: true, message: `Task added: ${result.data.line}`, data: { project, item, scope: normalizedScope } });
|
|
236
247
|
});
|
|
237
248
|
});
|
|
@@ -355,6 +366,7 @@ export function register(server, ctx) {
|
|
|
355
366
|
github_issue: z.union([z.number().int().positive(), z.string()]).optional().describe("GitHub issue number (for example 14 or '#14')."),
|
|
356
367
|
github_url: z.string().optional().describe("GitHub issue URL to associate with the task item."),
|
|
357
368
|
unlink_github: z.boolean().optional().describe("If true, remove any linked GitHub issue metadata from the item."),
|
|
369
|
+
create_issue: z.boolean().optional().describe("If true, create a GitHub issue for this task and link it."),
|
|
358
370
|
pin: z.boolean().optional().describe("If true, pin the task so it floats to the top of its section."),
|
|
359
371
|
promote: z.boolean().optional().describe("If true, clear the speculative flag on this task (confirm the user wants it)."),
|
|
360
372
|
move_to_active: z.boolean().optional().describe("Used with promote: also move the task to the Active section."),
|
|
@@ -371,6 +383,25 @@ export function register(server, ctx) {
|
|
|
371
383
|
if (!updates.work_next && !item) {
|
|
372
384
|
return mcpResponse({ ok: false, error: "item is required unless updates.work_next is true." });
|
|
373
385
|
}
|
|
386
|
+
if (updates.create_issue) {
|
|
387
|
+
const extraUpdates = [
|
|
388
|
+
updates.text,
|
|
389
|
+
updates.priority,
|
|
390
|
+
updates.context,
|
|
391
|
+
updates.section,
|
|
392
|
+
updates.github_issue,
|
|
393
|
+
updates.github_url,
|
|
394
|
+
updates.unlink_github,
|
|
395
|
+
updates.pin,
|
|
396
|
+
updates.promote,
|
|
397
|
+
updates.move_to_active,
|
|
398
|
+
updates.work_next,
|
|
399
|
+
updates.replace_context,
|
|
400
|
+
].some((value) => value !== undefined);
|
|
401
|
+
if (extraUpdates) {
|
|
402
|
+
return mcpResponse({ ok: false, error: "create_issue must be used by itself." });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
374
405
|
// Cross-validate github_issue and github_url
|
|
375
406
|
if (updates.github_url) {
|
|
376
407
|
const parsed = parseGithubIssueUrl(updates.github_url);
|
|
@@ -412,6 +443,45 @@ export function register(server, ctx) {
|
|
|
412
443
|
data: { project, item: result.data },
|
|
413
444
|
});
|
|
414
445
|
}
|
|
446
|
+
if (updates.create_issue) {
|
|
447
|
+
const resolved = resolveTaskItem(phrenPath, project, item);
|
|
448
|
+
if (!resolved.ok)
|
|
449
|
+
return mcpResponse({ ok: false, error: resolved.error });
|
|
450
|
+
const repo = resolveProjectGithubRepo(phrenPath, project);
|
|
451
|
+
if (!repo) {
|
|
452
|
+
return mcpResponse({
|
|
453
|
+
ok: false,
|
|
454
|
+
error: "Could not infer a GitHub repo. Add a GitHub URL to CLAUDE.md or summary.md, or link an existing issue instead.",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const created = createGithubIssueForTask({
|
|
458
|
+
repo,
|
|
459
|
+
title: resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
|
|
460
|
+
body: buildTaskIssueBody(project, resolved.data),
|
|
461
|
+
});
|
|
462
|
+
if (!created.ok)
|
|
463
|
+
return mcpResponse({ ok: false, error: created.error, errorCode: created.code });
|
|
464
|
+
const linked = linkTaskIssue(phrenPath, project, item, {
|
|
465
|
+
github_issue: created.data.issueNumber,
|
|
466
|
+
github_url: created.data.url,
|
|
467
|
+
});
|
|
468
|
+
if (!linked.ok)
|
|
469
|
+
return mcpResponse({ ok: false, error: linked.error, errorCode: linked.code });
|
|
470
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
471
|
+
return mcpResponse({
|
|
472
|
+
ok: true,
|
|
473
|
+
message: `Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`,
|
|
474
|
+
data: {
|
|
475
|
+
project,
|
|
476
|
+
item,
|
|
477
|
+
issue_number: created.data.issueNumber ?? null,
|
|
478
|
+
issue_url: created.data.url,
|
|
479
|
+
githubIssue: linked.data.githubIssue ?? null,
|
|
480
|
+
githubUrl: linked.data.githubUrl || null,
|
|
481
|
+
stableId: linked.data.stableId || null,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
415
485
|
// Handle github issue linking via update_task when github_issue or github_url is set (and no other field updates)
|
|
416
486
|
if ((updates.github_issue !== undefined || updates.github_url || updates.unlink_github) && !updates.text && !updates.priority && !updates.context && !updates.section) {
|
|
417
487
|
if (updates.unlink_github && (updates.github_issue !== undefined || updates.github_url)) {
|
package/mcp/dist/tools/types.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
import { parseStoreQualified } from "../store-routing.js";
|
|
2
|
+
import { resolveAllStores } from "../store-registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the effective phrenPath and bare project name for a project input.
|
|
5
|
+
* Handles store-qualified names ("store/project") by routing to the correct store.
|
|
6
|
+
* Returns the primary store path for bare names.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveStoreForProject(ctx, projectInput) {
|
|
9
|
+
const { storeName, projectName } = parseStoreQualified(projectInput);
|
|
10
|
+
if (!storeName) {
|
|
11
|
+
return { phrenPath: ctx.phrenPath, project: projectName, storeRole: "primary" };
|
|
12
|
+
}
|
|
13
|
+
const stores = resolveAllStores(ctx.phrenPath);
|
|
14
|
+
const store = stores.find((s) => s.name === storeName);
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error(`Store "${storeName}" not found`);
|
|
17
|
+
}
|
|
18
|
+
if (store.role === "readonly") {
|
|
19
|
+
throw new Error(`Store "${storeName}" is read-only`);
|
|
20
|
+
}
|
|
21
|
+
return { phrenPath: store.path, project: projectName, storeRole: store.role };
|
|
22
|
+
}
|
|
1
23
|
/**
|
|
2
24
|
* Convert an McpToolResult into the MCP SDK response format.
|
|
3
25
|
* Single shared implementation — replaces the per-file jsonResponse() duplicates.
|