@phren/cli 0.0.50 → 0.0.52
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 +17 -2
- package/mcp/dist/cli/cli.js +1 -1
- package/mcp/dist/cli/namespaces.js +35 -10
- package/mcp/dist/cli/ops.js +2 -2
- package/mcp/dist/content/validate.js +18 -0
- package/mcp/dist/data/access.js +27 -0
- package/mcp/dist/data/tasks.js +27 -2
- package/mcp/dist/generated/memory-ui-graph.browser.js +21 -21
- package/mcp/dist/governance/policy.js +3 -1
- package/mcp/dist/memory-ui-graph.runtime.js +21 -21
- package/mcp/dist/phren-core.js +1 -1
- package/mcp/dist/profile-store.js +20 -0
- package/mcp/dist/project-config.js +26 -0
- package/mcp/dist/shared/index.js +1 -1
- package/mcp/dist/shell/view.js +27 -9
- package/mcp/dist/store-routing.js +2 -2
- package/mcp/dist/task/lifecycle.js +11 -0
- package/mcp/dist/tools/config.js +23 -5
- package/mcp/dist/tools/data.js +17 -7
- package/mcp/dist/tools/extract.js +8 -5
- package/mcp/dist/tools/finding.js +14 -9
- package/mcp/dist/tools/graph.js +12 -3
- package/mcp/dist/tools/hooks.js +15 -2
- package/mcp/dist/tools/session.js +58 -18
- package/mcp/dist/tools/tasks.js +58 -44
- package/mcp/dist/ui/data.js +46 -19
- package/mcp/dist/ui/server.js +2 -1
- package/package.json +1 -1
package/mcp/dist/phren-core.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* tools (React, Django, Unity, JUCE, Ansible, ...) are learned dynamically from
|
|
8
8
|
* each project's FINDINGS.md via extractDynamicEntities().
|
|
9
9
|
*/
|
|
10
|
-
export const UNIVERSAL_TECH_TERMS_RE = /\b(Python|Rust|Go|Java|TypeScript|JavaScript|Docker|Kubernetes|AWS|GCP|Azure|SQL|Git)\b/gi;
|
|
10
|
+
export const UNIVERSAL_TECH_TERMS_RE = /\b(Python|Rust|Go|Java|TypeScript|JavaScript|Docker|Kubernetes|AWS|GCP|Azure|SQL|Git|React|Vue|Angular|Svelte|Next\.?js|Nuxt|Vite|esbuild|Webpack|Rollup|Babel|ESLint|Biome|Prettier|Jest|Vitest|Playwright|Cypress|Node\.?js|Deno|Bun|Express|Fastify|Hono|Koa|NestJS|Prisma|Drizzle|Sequelize|TypeORM|Postgres|PostgreSQL|MySQL|SQLite|MongoDB|Redis|Elasticsearch|GraphQL|REST|gRPC|tRPC|Zod|Pydantic|FastAPI|Django|Flask|Rails|Spring|Laravel|Tailwind|Bootstrap|MUI|Material|Fluent|Chakra|Radix|shadcn|SharePoint|SPFx|Teams|OneDrive|Power\s*Apps|Deltek|VantagePoint|Hangfire|ASP\.?NET|MVC|Blazor|MAUI|Electron|VSCode|GitHub|GitLab|Bitbucket|Vercel|Netlify|Railway|Fly\.io|Cloudflare|Lambda|S3|EC2|RDS|DynamoDB|Stripe|Twilio|SendGrid|OpenAI|Anthropic|Claude|LLM|MCP|FTS5|SQLCipher|Sigma|ForceAtlas|Graphology|Playwright|Puppeteer|Selenium|Turbo|Turborepo|Lerna|nx|pnpm|yarn|npm|Bun)\b/gi;
|
|
11
11
|
/**
|
|
12
12
|
* Additional fragment patterns beyond CamelCase and acronyms.
|
|
13
13
|
* Each pattern has a named group so callers can identify the fragment type.
|
|
@@ -305,6 +305,26 @@ function buildProjectCard(dir) {
|
|
|
305
305
|
export function listProjectCards(phrenPath, profile) {
|
|
306
306
|
const dirs = getProjectDirs(phrenPath, profile).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
307
307
|
const cards = dirs.map(buildProjectCard);
|
|
308
|
+
const seen = new Set(dirs.map((d) => path.basename(d)));
|
|
309
|
+
// Include projects from team stores
|
|
310
|
+
try {
|
|
311
|
+
const storeRegistry = require("./store-registry.js");
|
|
312
|
+
const { getNonPrimaryStores } = storeRegistry;
|
|
313
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
314
|
+
if (!fs.existsSync(store.path))
|
|
315
|
+
continue;
|
|
316
|
+
for (const dir of getProjectDirs(store.path)) {
|
|
317
|
+
const name = path.basename(dir);
|
|
318
|
+
if (seen.has(name) || name === "global")
|
|
319
|
+
continue;
|
|
320
|
+
seen.add(name);
|
|
321
|
+
cards.push(buildProjectCard(dir));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// store-registry not available or error loading, continue with primary only
|
|
327
|
+
}
|
|
308
328
|
// Prepend global as a pinned entry so it's always accessible from the shell
|
|
309
329
|
const globalDir = path.join(phrenPath, "global");
|
|
310
330
|
if (fs.existsSync(globalDir)) {
|
|
@@ -130,6 +130,32 @@ export function isProjectHookEnabled(phrenPath, project, event, config) {
|
|
|
130
130
|
return hooks.enabled;
|
|
131
131
|
return true;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Remove a per-project hook override, restoring inheritance from global config.
|
|
135
|
+
* Pass event to clear a specific event override; omit to clear the whole hooks block.
|
|
136
|
+
*/
|
|
137
|
+
export function clearProjectHookOverride(phrenPath, project, event) {
|
|
138
|
+
const configPath = resolveProjectConfigPath(phrenPath, project);
|
|
139
|
+
if (!configPath)
|
|
140
|
+
throw new Error("Project config path escapes phren store");
|
|
141
|
+
return withFileLock(configPath, () => {
|
|
142
|
+
const current = readProjectConfig(phrenPath, project);
|
|
143
|
+
const existingHooks = normalizeHookConfig(current);
|
|
144
|
+
let nextHooks;
|
|
145
|
+
if (event && PROJECT_HOOK_EVENTS.includes(event)) {
|
|
146
|
+
// Delete just this event key
|
|
147
|
+
const { [event]: _removed, ...rest } = existingHooks;
|
|
148
|
+
nextHooks = rest;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Clear all overrides
|
|
152
|
+
nextHooks = {};
|
|
153
|
+
}
|
|
154
|
+
const next = { ...current, hooks: nextHooks };
|
|
155
|
+
writeProjectConfigFile(configPath, next);
|
|
156
|
+
return next;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
133
159
|
export function writeProjectHookConfig(phrenPath, project, patch) {
|
|
134
160
|
// Move read+merge inside the lock so concurrent writers cannot clobber each other.
|
|
135
161
|
const configPath = resolveProjectConfigPath(phrenPath, project);
|
package/mcp/dist/shared/index.js
CHANGED
|
@@ -34,7 +34,7 @@ async function refreshStoreProjectDirs(phrenPath, profile) {
|
|
|
34
34
|
for (const store of otherStores) {
|
|
35
35
|
if (!fs.existsSync(store.path))
|
|
36
36
|
continue;
|
|
37
|
-
dirs.push(...getProjectDirs(store.path
|
|
37
|
+
dirs.push(...getProjectDirs(store.path));
|
|
38
38
|
}
|
|
39
39
|
_cachedStoreProjectDirs = dirs;
|
|
40
40
|
_cachedStorePhrenPath = phrenPath;
|
package/mcp/dist/shell/view.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { canonicalTaskFilePath, listProjectCards, readTasks, readFindings, readReviewQueue, readRuntimeHealth, resolveTaskFilePath, } from "../data/access.js";
|
|
8
|
+
import { getNonPrimaryStores } from "../store-registry.js";
|
|
8
9
|
import { style, badge, separator, stripAnsi, truncateLine, renderWidth, wrapSegments, lineViewport, shellHelpText, gradient, } from "./render.js";
|
|
9
10
|
import { formatSelectableLine, viewportWithStatus, } from "./view-list.js";
|
|
10
11
|
import { SUB_VIEWS, TAB_ICONS, } from "./types.js";
|
|
@@ -15,6 +16,19 @@ import { isProjectHookEnabled, readProjectConfig } from "../project-config.js";
|
|
|
15
16
|
import { getScopedSkills } from "../skill/registry.js";
|
|
16
17
|
import { errorMessage } from "../utils.js";
|
|
17
18
|
import { logger } from "../logger.js";
|
|
19
|
+
/** Resolve which store (primary or team) contains a project */
|
|
20
|
+
function resolveProjectStorePath(phrenPath, project) {
|
|
21
|
+
if (fs.existsSync(path.join(phrenPath, project)))
|
|
22
|
+
return phrenPath;
|
|
23
|
+
try {
|
|
24
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
25
|
+
if (fs.existsSync(path.join(store.path, project)))
|
|
26
|
+
return store.path;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch { /* fall through */ }
|
|
30
|
+
return phrenPath;
|
|
31
|
+
}
|
|
18
32
|
// ── Tab bar ────────────────────────────────────────────────────────────────
|
|
19
33
|
function renderTabBar(state) {
|
|
20
34
|
const cols = renderWidth();
|
|
@@ -108,9 +122,10 @@ function collectProjectDashboardEntries(ctx) {
|
|
|
108
122
|
reviewCount: 0,
|
|
109
123
|
};
|
|
110
124
|
}
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
125
|
+
const storePath = resolveProjectStorePath(ctx.phrenPath, card.name);
|
|
126
|
+
const task = readTasks(storePath, card.name);
|
|
127
|
+
const findings = readFindings(storePath, card.name);
|
|
128
|
+
const review = readReviewQueue(storePath, card.name);
|
|
114
129
|
return {
|
|
115
130
|
...card,
|
|
116
131
|
activeCount: task.ok ? task.data.items.Active.length : 0,
|
|
@@ -262,16 +277,17 @@ function renderTaskView(ctx, cursor, height, subsectionsCache) {
|
|
|
262
277
|
if (!project) {
|
|
263
278
|
return { lines: [style.dim(" No project selected — navigate to Projects (← →) and press ↵")], subsectionsCache };
|
|
264
279
|
}
|
|
265
|
-
const
|
|
280
|
+
const storePath = resolveProjectStorePath(ctx.phrenPath, project);
|
|
281
|
+
const result = readTasks(storePath, project);
|
|
266
282
|
if (!result.ok)
|
|
267
283
|
return { lines: [result.error], subsectionsCache };
|
|
268
284
|
const parsed = result.data;
|
|
269
285
|
const warnings = parsed.issues.length
|
|
270
286
|
? [` ${style.yellow("⚠")} ${style.yellow(parsed.issues.join("; "))}`, ""]
|
|
271
287
|
: [];
|
|
272
|
-
const taskFile = resolveTaskFilePath(
|
|
273
|
-
?? canonicalTaskFilePath(
|
|
274
|
-
?? path.join(
|
|
288
|
+
const taskFile = resolveTaskFilePath(storePath, project)
|
|
289
|
+
?? canonicalTaskFilePath(storePath, project)
|
|
290
|
+
?? path.join(storePath, project, "tasks.md");
|
|
275
291
|
const subsResult = parseSubsections(taskFile, project, subsectionsCache);
|
|
276
292
|
const subsections = subsResult.map;
|
|
277
293
|
const newCache = subsResult.cache;
|
|
@@ -341,7 +357,8 @@ function renderFindingsView(ctx, cursor, height) {
|
|
|
341
357
|
const project = ctx.state.project;
|
|
342
358
|
if (!project)
|
|
343
359
|
return [style.dim(" No project selected.")];
|
|
344
|
-
const
|
|
360
|
+
const storePath = resolveProjectStorePath(ctx.phrenPath, project);
|
|
361
|
+
const result = readFindings(storePath, project);
|
|
345
362
|
if (!result.ok)
|
|
346
363
|
return [result.error];
|
|
347
364
|
const all = result.data;
|
|
@@ -390,7 +407,8 @@ function renderMemoryQueueView(ctx, cursor, height) {
|
|
|
390
407
|
const project = ctx.state.project;
|
|
391
408
|
if (!project)
|
|
392
409
|
return [style.dim(" No project selected.")];
|
|
393
|
-
const
|
|
410
|
+
const storePath = resolveProjectStorePath(ctx.phrenPath, project);
|
|
411
|
+
const result = readReviewQueue(storePath, project);
|
|
394
412
|
if (!result.ok)
|
|
395
413
|
return [result.error];
|
|
396
414
|
const filtered = ctx.state.filter
|
|
@@ -85,7 +85,7 @@ export function listAllProjects(phrenPath, profile) {
|
|
|
85
85
|
const stores = resolveAllStores(phrenPath);
|
|
86
86
|
const results = [];
|
|
87
87
|
for (const store of stores) {
|
|
88
|
-
const dirs = getProjectDirs(store.path, profile);
|
|
88
|
+
const dirs = getProjectDirs(store.path, store.role === "primary" ? profile : undefined);
|
|
89
89
|
for (const dir of dirs) {
|
|
90
90
|
const projectName = path.basename(dir);
|
|
91
91
|
results.push({ store, projectName, projectDir: dir });
|
|
@@ -101,7 +101,7 @@ function isValidStoreName(name) {
|
|
|
101
101
|
function findProjectInStore(store, projectName, profile) {
|
|
102
102
|
if (!fs.existsSync(store.path))
|
|
103
103
|
return null;
|
|
104
|
-
const dirs = getProjectDirs(store.path, profile);
|
|
104
|
+
const dirs = getProjectDirs(store.path, store.role === "primary" ? profile : undefined);
|
|
105
105
|
for (const dir of dirs) {
|
|
106
106
|
if (path.basename(dir) === projectName)
|
|
107
107
|
return dir;
|
|
@@ -9,6 +9,12 @@ import { incrementSessionTasksCompleted } from "../tools/session.js";
|
|
|
9
9
|
const ACTION_PREFIX_RE = /^(?:please\s+|can you\s+|could you\s+|would you\s+|i want you to\s+|i want to\s+|let(?:'|’)s\s+|lets\s+|help me\s+)/i;
|
|
10
10
|
const EXPLICIT_TASK_PREFIX_RE = /^(?:add(?:\s+(?:this|that|it))?\s+(?:to\s+(?:the\s+)?)?(?:task|todo(?:\s+list)?|task(?:\s+list)?)|add\s+(?:a\s+)?task|put(?:\s+(?:this|that|it))?\s+(?:in|on)\s+(?:the\s+)?(?:task|todo(?:\s+list)?|task(?:\s+list)?))\s*(?::|-|,)?\s*/i;
|
|
11
11
|
const NON_ACTIONABLE_RE = /\b(brainstorm|idea|ideas|maybe|what if|should we|could we|would it make sense|question|explain|why is|how does)\b/i;
|
|
12
|
+
// Conversational noise: only matches when the ENTIRE prompt is a short ack/reaction (under 40 chars).
|
|
13
|
+
// This avoids rejecting "sure, go ahead and fix the build" or "great, now update the docs".
|
|
14
|
+
const CONVERSATIONAL_NOISE_RE = /^(ok|okay|yeah|yep|nah|nope|hi|hey|ss|bro|lol|lmao|got it|sounds good|perfect|great|sure|thanks|thank you|ty|np|no problem|alright|cool|nice|damn|wtf|omg|fok)[\s!.?,]*$/i;
|
|
15
|
+
// Raw system/SQL error fragment signals — patterns that only appear in error output, never real task requests.
|
|
16
|
+
// Intentionally does NOT include "line \d+" or "incorrect syntax" alone (too broad — they appear in dev prompts).
|
|
17
|
+
const RAW_MESSAGE_SIGNALS_RE = /\b(msg \d+, level \d+|cannot insert the value null|insufficient result space|uniqueidentifier value to char|pgevision-prod|task-notification|tool-use-id|toolu_0[a-z0-9])\b/i;
|
|
12
18
|
const ACTIONABLE_RE = /\b(add|build|change|complete|continue|create|delete|fix|implement|improve|investigate|make|move|refactor|remove|rename|repair|ship|start|update|wire)\b/i;
|
|
13
19
|
const CONTINUE_RE = /\b(continue|keep going|finish|resume|pick up|work on that|that task)\b/i;
|
|
14
20
|
const GITHUB_URL_RE = /https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/issues\/\d+(?:[?#][^\s]*)?/g;
|
|
@@ -89,6 +95,11 @@ function isActionablePrompt(prompt, intent) {
|
|
|
89
95
|
return false;
|
|
90
96
|
if (NON_ACTIONABLE_RE.test(normalized))
|
|
91
97
|
return false;
|
|
98
|
+
// Always reject conversational noise and raw system/SQL fragments regardless of intent.
|
|
99
|
+
if (CONVERSATIONAL_NOISE_RE.test(normalized))
|
|
100
|
+
return false;
|
|
101
|
+
if (RAW_MESSAGE_SIGNALS_RE.test(normalized))
|
|
102
|
+
return false;
|
|
92
103
|
if (intent === "general")
|
|
93
104
|
return ACTIONABLE_RE.test(normalized);
|
|
94
105
|
return true;
|
package/mcp/dist/tools/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { mcpResponse } from "./types.js";
|
|
3
|
+
import { mcpResponse, resolveStoreForProject } from "./types.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, mergeConfig, VALID_TASK_MODES, VALID_FINDING_SENSITIVITY, } from "../shared/governance.js";
|
|
6
6
|
import { PROACTIVITY_LEVELS, } from "../proactivity.js";
|
|
@@ -75,7 +75,16 @@ async function handleGetConfig(ctx, { domain, project }) {
|
|
|
75
75
|
const err = validateProject(project);
|
|
76
76
|
if (err)
|
|
77
77
|
return mcpResponse({ ok: false, error: err });
|
|
78
|
-
|
|
78
|
+
// Resolve store-qualified project names for team stores
|
|
79
|
+
let resolvedPhrenPath = phrenPath;
|
|
80
|
+
let resolvedProject = project;
|
|
81
|
+
try {
|
|
82
|
+
const resolved = resolveStoreForProject(ctx, project);
|
|
83
|
+
resolvedPhrenPath = resolved.phrenPath;
|
|
84
|
+
resolvedProject = resolved.project;
|
|
85
|
+
}
|
|
86
|
+
catch { /* fall back to primary */ }
|
|
87
|
+
const topicResult = getTopicConfigData(resolvedPhrenPath, resolvedProject);
|
|
79
88
|
if (!topicResult.ok)
|
|
80
89
|
return mcpResponse({ ok: false, error: topicResult.error });
|
|
81
90
|
return mcpResponse({
|
|
@@ -445,9 +454,18 @@ async function handleSetConfig(ctx, { domain, settings, project }) {
|
|
|
445
454
|
const err = validateProject(project);
|
|
446
455
|
if (err)
|
|
447
456
|
return mcpResponse({ ok: false, error: err });
|
|
448
|
-
|
|
457
|
+
// Resolve store-qualified project names for team stores
|
|
458
|
+
let topicPhrenPath = phrenPath;
|
|
459
|
+
let topicProject = project;
|
|
460
|
+
try {
|
|
461
|
+
const resolved = resolveStoreForProject(ctx, project);
|
|
462
|
+
topicPhrenPath = resolved.phrenPath;
|
|
463
|
+
topicProject = resolved.project;
|
|
464
|
+
}
|
|
465
|
+
catch { /* fall back to primary */ }
|
|
466
|
+
const projectDir = safeProjectPath(topicPhrenPath, topicProject);
|
|
449
467
|
if (!projectDir || !fs.existsSync(projectDir)) {
|
|
450
|
-
return mcpResponse({ ok: false, error: `Project "${
|
|
468
|
+
return mcpResponse({ ok: false, error: `Project "${topicProject}" not found in phren.` });
|
|
451
469
|
}
|
|
452
470
|
const topics = settings.topics;
|
|
453
471
|
if (!topics || !Array.isArray(topics)) {
|
|
@@ -480,7 +498,7 @@ async function handleSetConfig(ctx, { domain, settings, project }) {
|
|
|
480
498
|
fs.writeFileSync(configPath, JSON.stringify({ version: 1, domain: topicDomain, topics: [] }, null, 2) + "\n");
|
|
481
499
|
}
|
|
482
500
|
}
|
|
483
|
-
const result = writeProjectTopics(
|
|
501
|
+
const result = writeProjectTopics(topicPhrenPath, topicProject, normalized);
|
|
484
502
|
if (!result.ok) {
|
|
485
503
|
return mcpResponse({ ok: false, error: result.error });
|
|
486
504
|
}
|
package/mcp/dist/tools/data.js
CHANGED
|
@@ -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";
|
|
@@ -35,10 +35,21 @@ export function register(server, ctx) {
|
|
|
35
35
|
inputSchema: z.object({
|
|
36
36
|
project: z.string().describe("Project name to export."),
|
|
37
37
|
}),
|
|
38
|
-
}, async ({ project }) => {
|
|
38
|
+
}, async ({ project: projectInput }) => {
|
|
39
|
+
// Resolve store-qualified project names (e.g. "qualus-shared/arc")
|
|
40
|
+
let resolvedPhrenPath;
|
|
41
|
+
let project;
|
|
42
|
+
try {
|
|
43
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
44
|
+
resolvedPhrenPath = resolved.phrenPath;
|
|
45
|
+
project = resolved.project;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
49
|
+
}
|
|
39
50
|
if (!isValidProjectName(project))
|
|
40
51
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
41
|
-
const projectDir = safeProjectPath(
|
|
52
|
+
const projectDir = safeProjectPath(resolvedPhrenPath, project);
|
|
42
53
|
if (!projectDir || !fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) {
|
|
43
54
|
return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
|
|
44
55
|
}
|
|
@@ -46,17 +57,16 @@ export function register(server, ctx) {
|
|
|
46
57
|
const summaryPath = safeProjectPath(projectDir, "summary.md");
|
|
47
58
|
if (summaryPath && fs.existsSync(summaryPath))
|
|
48
59
|
exported.summary = fs.readFileSync(summaryPath, "utf8");
|
|
49
|
-
const learningsResult = readFindings(
|
|
60
|
+
const learningsResult = readFindings(resolvedPhrenPath, project);
|
|
50
61
|
if (learningsResult.ok)
|
|
51
62
|
exported.learnings = learningsResult.data;
|
|
52
63
|
const findingsPath = safeProjectPath(projectDir, "FINDINGS.md");
|
|
53
64
|
if (findingsPath && fs.existsSync(findingsPath))
|
|
54
65
|
exported.findingsRaw = fs.readFileSync(findingsPath, "utf8");
|
|
55
|
-
const taskResult = readTasks(
|
|
66
|
+
const taskResult = readTasks(resolvedPhrenPath, project);
|
|
56
67
|
if (taskResult.ok) {
|
|
57
68
|
exported.task = taskResult.data.items;
|
|
58
|
-
|
|
59
|
-
const taskRawPath = resolveTaskFilePath(phrenPath, project);
|
|
69
|
+
const taskRawPath = resolveTaskFilePath(resolvedPhrenPath, project);
|
|
60
70
|
if (taskRawPath && fs.existsSync(taskRawPath))
|
|
61
71
|
exported.taskRaw = fs.readFileSync(taskRawPath, "utf8");
|
|
62
72
|
}
|
|
@@ -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 { isValidProjectName, safeProjectPath, errorMessage } from "../utils.js";
|
|
4
4
|
import { addFindingsToFile } from "../shared/content.js";
|
|
@@ -39,7 +39,7 @@ function parseFindings(raw) {
|
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
export function register(server, ctx) {
|
|
42
|
-
const {
|
|
42
|
+
const { withWriteQueue, updateFileInIndex } = ctx;
|
|
43
43
|
server.registerTool("auto_extract_findings", {
|
|
44
44
|
title: "◆ phren · auto-extract findings",
|
|
45
45
|
description: "Use a local Ollama LLM to automatically extract non-obvious findings from text. " +
|
|
@@ -53,7 +53,10 @@ export function register(server, ctx) {
|
|
|
53
53
|
model: z.string().optional().describe("Ollama model to use (overrides PHREN_EXTRACT_MODEL env var)."),
|
|
54
54
|
dryRun: z.boolean().optional().describe("If true, return what would be extracted without saving."),
|
|
55
55
|
}),
|
|
56
|
-
}, async ({ project, text, model, dryRun }) => {
|
|
56
|
+
}, async ({ project: projectInput, text, model, dryRun }) => {
|
|
57
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
58
|
+
const project = resolved.project;
|
|
59
|
+
const targetPath = resolved.phrenPath;
|
|
57
60
|
if (!isValidProjectName(project)) {
|
|
58
61
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
59
62
|
}
|
|
@@ -112,14 +115,14 @@ export function register(server, ctx) {
|
|
|
112
115
|
return withWriteQueue(async () => {
|
|
113
116
|
// Use addFindingsToFile so extracted findings go through the full pipeline:
|
|
114
117
|
// secret scan, dedup check, validation, and index update.
|
|
115
|
-
const result = addFindingsToFile(
|
|
118
|
+
const result = addFindingsToFile(targetPath, project, findings, { source: "extract" });
|
|
116
119
|
if (!result.ok) {
|
|
117
120
|
return mcpResponse({ ok: false, error: result.error });
|
|
118
121
|
}
|
|
119
122
|
const { added, skipped, rejected } = result.data;
|
|
120
123
|
const allSkipped = [...skipped, ...rejected.map(r => r.text)];
|
|
121
124
|
// Update index for the findings file
|
|
122
|
-
const resolvedDir = safeProjectPath(
|
|
125
|
+
const resolvedDir = safeProjectPath(targetPath, project);
|
|
123
126
|
if (resolvedDir) {
|
|
124
127
|
updateFileInIndex(path.join(resolvedDir, "FINDINGS.md"));
|
|
125
128
|
}
|
|
@@ -5,7 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { isValidProjectName, safeProjectPath, errorMessage } from "../utils.js";
|
|
7
7
|
import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "../core/finding.js";
|
|
8
|
-
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope,
|
|
8
|
+
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, } from "../shared.js";
|
|
9
9
|
import { addFindingToFile, addFindingsToFile, checkSemanticConflicts, autoMergeConflicts, } from "../shared/content.js";
|
|
10
10
|
import { jaccardTokenize, jaccardSimilarity, stripMetadata } from "../content/dedup.js";
|
|
11
11
|
import { runCustomHooks } from "../hooks.js";
|
|
@@ -330,17 +330,22 @@ async function handleResolveContradiction(ctx, { project: projectInput, finding_
|
|
|
330
330
|
}));
|
|
331
331
|
}
|
|
332
332
|
async function handleGetContradictions(ctx, { project, finding_text }) {
|
|
333
|
-
const { phrenPath } = ctx;
|
|
334
333
|
if (project && !isValidProjectName(project))
|
|
335
334
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
// Build project list from all stores (primary + team)
|
|
336
|
+
let projectsWithPaths;
|
|
337
|
+
if (project) {
|
|
338
|
+
const resolved = resolveStoreForProject(ctx, project);
|
|
339
|
+
projectsWithPaths = [{ project: resolved.project, storePath: resolved.phrenPath }];
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const { listAllProjects } = await import("../store-routing.js");
|
|
343
|
+
projectsWithPaths = listAllProjects(ctx.phrenPath, ctx.profile)
|
|
344
|
+
.map((p) => ({ project: p.projectName, storePath: p.store.path }));
|
|
345
|
+
}
|
|
341
346
|
const contradictions = [];
|
|
342
|
-
for (const p of
|
|
343
|
-
const result = readFindings(
|
|
347
|
+
for (const { project: p, storePath } of projectsWithPaths) {
|
|
348
|
+
const result = readFindings(storePath, p);
|
|
344
349
|
if (!result.ok)
|
|
345
350
|
continue;
|
|
346
351
|
for (const finding of result.data) {
|
package/mcp/dist/tools/graph.js
CHANGED
|
@@ -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 crypto from "crypto";
|
|
@@ -85,7 +85,13 @@ export function register(server, ctx) {
|
|
|
85
85
|
}
|
|
86
86
|
const results = [];
|
|
87
87
|
for (const doc of relatedDocs) {
|
|
88
|
-
const
|
|
88
|
+
const docProject = doc.split("/")[0];
|
|
89
|
+
let docPhrenPath = ctx.phrenPath;
|
|
90
|
+
try {
|
|
91
|
+
docPhrenPath = resolveStoreForProject(ctx, docProject).phrenPath;
|
|
92
|
+
}
|
|
93
|
+
catch { /* fall back to primary */ }
|
|
94
|
+
const docRow = queryDocBySourceKey(db, docPhrenPath, doc);
|
|
89
95
|
const snippet = docRow?.content ? docRow.content.slice(0, 200) : "";
|
|
90
96
|
results.push({ sourceDoc: doc, snippet });
|
|
91
97
|
}
|
|
@@ -255,7 +261,10 @@ export function register(server, ctx) {
|
|
|
255
261
|
catch (err) {
|
|
256
262
|
logger.debug("graph", `link_findings globalFragments: ${errorMessage(err)}`);
|
|
257
263
|
}
|
|
258
|
-
// 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation)
|
|
264
|
+
// 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation).
|
|
265
|
+
// Note: manual-links.json is intentionally written to the primary phrenPath's runtime directory.
|
|
266
|
+
// During index rebuild, it's read and merged into the unified index (which includes team store docs),
|
|
267
|
+
// so team store projects' manual links persist and are queryable cross-store. Writes are always to primary.
|
|
259
268
|
const manualLinksPath = runtimeFile(ctx.phrenPath, "manual-links.json");
|
|
260
269
|
try {
|
|
261
270
|
withFileLock(manualLinksPath, () => {
|
package/mcp/dist/tools/hooks.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
import { readInstallPreferences, updateInstallPreferences } from "../init/preferences.js";
|
|
6
6
|
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES, validateCustomHookCommand, validateCustomWebhookUrl } from "../hooks.js";
|
|
7
7
|
import { hookConfigPath } from "../shared.js";
|
|
8
|
-
import { PROJECT_HOOK_EVENTS, isProjectHookEnabled, readProjectConfig, writeProjectHookConfig } from "../project-config.js";
|
|
8
|
+
import { PROJECT_HOOK_EVENTS, isProjectHookEnabled, readProjectConfig, writeProjectHookConfig, clearProjectHookOverride } from "../project-config.js";
|
|
9
9
|
import { isValidProjectName } from "../utils.js";
|
|
10
10
|
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
11
11
|
const VALID_CUSTOM_EVENTS = HOOK_EVENT_VALUES;
|
|
@@ -97,14 +97,27 @@ export function register(server, ctx) {
|
|
|
97
97
|
description: "Enable or disable hooks globally, for a specific tool, or for a tracked project.",
|
|
98
98
|
inputSchema: z.object({
|
|
99
99
|
enabled: z.boolean().describe("true to enable, false to disable."),
|
|
100
|
+
clear: z.boolean().optional().describe("When true and project is set, removes the per-project override and restores inheritance from global. Ignores enabled."),
|
|
100
101
|
tool: z.string().optional().describe("Specific tool. Omit to toggle globally."),
|
|
101
102
|
project: z.string().optional().describe("Tracked project name for project-level lifecycle hook overrides."),
|
|
102
103
|
event: z.string().optional().describe("Optional lifecycle event for project-level overrides: UserPromptSubmit, Stop, SessionStart, PostToolUse."),
|
|
103
104
|
}),
|
|
104
|
-
}, async ({ enabled, tool, project, event }) => {
|
|
105
|
+
}, async ({ enabled, clear, tool, project, event }) => {
|
|
105
106
|
if (tool && project) {
|
|
106
107
|
return mcpResponse({ ok: false, error: "Pass either tool or project, not both." });
|
|
107
108
|
}
|
|
109
|
+
// Clear per-project override (restore inheritance)
|
|
110
|
+
if (clear && project) {
|
|
111
|
+
if (!isValidProjectName(project) || !fs.existsSync(path.join(phrenPath, project))) {
|
|
112
|
+
return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
|
|
113
|
+
}
|
|
114
|
+
clearProjectHookOverride(phrenPath, project, event);
|
|
115
|
+
const scope = event ? `${event} hook` : "hooks";
|
|
116
|
+
return mcpResponse({ ok: true, message: `Cleared ${scope} override for ${project} — now inheriting from global.`, data: { project, event, cleared: true } });
|
|
117
|
+
}
|
|
118
|
+
if (clear) {
|
|
119
|
+
return mcpResponse({ ok: false, error: "clear requires project." });
|
|
120
|
+
}
|
|
108
121
|
if (event && !project) {
|
|
109
122
|
return mcpResponse({ ok: false, error: "event requires project." });
|
|
110
123
|
}
|
|
@@ -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";
|
|
@@ -305,16 +305,21 @@ export function listAllSessions(phrenPath, limit = 50) {
|
|
|
305
305
|
entries.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
306
306
|
return entries;
|
|
307
307
|
}
|
|
308
|
-
export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
308
|
+
export async function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
309
309
|
const findings = [];
|
|
310
310
|
const tasks = [];
|
|
311
311
|
const shortId = sessionId.slice(0, 8);
|
|
312
312
|
try {
|
|
313
|
+
// Primary store projects
|
|
313
314
|
const projectDirs = getProjectDirs(phrenPath);
|
|
314
315
|
const targetProjects = project ? [project] : projectDirs.map((d) => path.basename(d));
|
|
315
|
-
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
const readProjectArtifacts = (storePath, proj) => {
|
|
318
|
+
if (seen.has(proj))
|
|
319
|
+
return;
|
|
320
|
+
seen.add(proj);
|
|
316
321
|
// Findings with matching sessionId
|
|
317
|
-
const findingsResult = readFindings(
|
|
322
|
+
const findingsResult = readFindings(storePath, proj);
|
|
318
323
|
if (findingsResult.ok) {
|
|
319
324
|
for (const f of findingsResult.data) {
|
|
320
325
|
if (f.sessionId && (f.sessionId === sessionId || f.sessionId.startsWith(shortId))) {
|
|
@@ -328,7 +333,7 @@ export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
// Tasks with matching sessionId
|
|
331
|
-
const tasksResult = readTasks(
|
|
336
|
+
const tasksResult = readTasks(storePath, proj);
|
|
332
337
|
if (tasksResult.ok) {
|
|
333
338
|
for (const section of ["Active", "Queue", "Done"]) {
|
|
334
339
|
for (const t of tasksResult.data.items[section]) {
|
|
@@ -344,15 +349,32 @@ export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
351
|
}
|
|
352
|
+
};
|
|
353
|
+
for (const proj of targetProjects) {
|
|
354
|
+
readProjectArtifacts(phrenPath, proj);
|
|
355
|
+
}
|
|
356
|
+
// Team store projects
|
|
357
|
+
try {
|
|
358
|
+
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
359
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
360
|
+
if (!fs.existsSync(store.path))
|
|
361
|
+
continue;
|
|
362
|
+
const storeDirs = getProjectDirs(store.path).map(d => path.basename(d)).filter(p => p !== "global");
|
|
363
|
+
const storeTargetProjects = project ? (storeDirs.includes(project) ? [project] : []) : storeDirs;
|
|
364
|
+
for (const proj of storeTargetProjects) {
|
|
365
|
+
readProjectArtifacts(store.path, proj);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
347
368
|
}
|
|
369
|
+
catch { /* store-registry not available */ }
|
|
348
370
|
}
|
|
349
371
|
catch (err) {
|
|
350
372
|
debugLog(`getSessionArtifacts error: ${errorMessage(err)}`);
|
|
351
373
|
}
|
|
352
374
|
return { findings, tasks };
|
|
353
375
|
}
|
|
354
|
-
function hasCompletedTasksInSession(phrenPath, sessionId, project) {
|
|
355
|
-
const artifacts = getSessionArtifacts(phrenPath, sessionId, project);
|
|
376
|
+
async function hasCompletedTasksInSession(phrenPath, sessionId, project) {
|
|
377
|
+
const artifacts = await getSessionArtifacts(phrenPath, sessionId, project);
|
|
356
378
|
return artifacts.tasks.some((task) => task.section === "Done" && task.checked);
|
|
357
379
|
}
|
|
358
380
|
/** Compute what changed since the last session ended. */
|
|
@@ -448,8 +470,16 @@ export function register(server, ctx) {
|
|
|
448
470
|
const activeProject = project ?? priorProject;
|
|
449
471
|
const activeScope = normalizedAgentScope;
|
|
450
472
|
if (activeProject && isValidProjectName(activeProject)) {
|
|
473
|
+
const projectStorePath = (() => {
|
|
474
|
+
try {
|
|
475
|
+
return resolveStoreForProject(ctx, activeProject).phrenPath;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return phrenPath;
|
|
479
|
+
}
|
|
480
|
+
})();
|
|
451
481
|
try {
|
|
452
|
-
const findings = readFindings(
|
|
482
|
+
const findings = readFindings(projectStorePath, activeProject);
|
|
453
483
|
if (findings.ok) {
|
|
454
484
|
const bullets = findings.data
|
|
455
485
|
.filter((entry) => isMemoryScopeVisible(normalizeMemoryScope(entry.scope), activeScope))
|
|
@@ -464,7 +494,7 @@ export function register(server, ctx) {
|
|
|
464
494
|
debugError("session_start findingsRead", err);
|
|
465
495
|
}
|
|
466
496
|
try {
|
|
467
|
-
const tasks = readTasks(
|
|
497
|
+
const tasks = readTasks(projectStorePath, activeProject);
|
|
468
498
|
if (tasks.ok) {
|
|
469
499
|
const activeItems = tasks.data.items.Active
|
|
470
500
|
.filter((entry) => isMemoryScopeVisible(normalizeMemoryScope(entry.scope), activeScope))
|
|
@@ -480,7 +510,7 @@ export function register(server, ctx) {
|
|
|
480
510
|
}
|
|
481
511
|
// Surface extracted preferences/facts for this project
|
|
482
512
|
try {
|
|
483
|
-
const facts = readExtractedFacts(
|
|
513
|
+
const facts = readExtractedFacts(projectStorePath, activeProject).slice(-10);
|
|
484
514
|
if (facts.length > 0) {
|
|
485
515
|
parts.push(`## Preferences (${activeProject})\n${facts.map(f => `- ${f.fact}`).join("\n")}`);
|
|
486
516
|
}
|
|
@@ -489,7 +519,7 @@ export function register(server, ctx) {
|
|
|
489
519
|
debugError("session_start factsRead", err);
|
|
490
520
|
}
|
|
491
521
|
try {
|
|
492
|
-
const checkpoints = listTaskCheckpoints(
|
|
522
|
+
const checkpoints = listTaskCheckpoints(projectStorePath, activeProject).slice(0, 3);
|
|
493
523
|
if (checkpoints.length > 0) {
|
|
494
524
|
const lines = [];
|
|
495
525
|
for (const checkpoint of checkpoints) {
|
|
@@ -600,19 +630,29 @@ export function register(server, ctx) {
|
|
|
600
630
|
writeLastSummary(phrenPath, effectiveSummary, state.sessionId, endedState.project);
|
|
601
631
|
}
|
|
602
632
|
if (endedState.project && isValidProjectName(endedState.project)) {
|
|
633
|
+
const projectStorePath = (() => {
|
|
634
|
+
if (!endedState.project)
|
|
635
|
+
return phrenPath;
|
|
636
|
+
try {
|
|
637
|
+
return resolveStoreForProject(ctx, endedState.project).phrenPath;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return phrenPath;
|
|
641
|
+
}
|
|
642
|
+
})();
|
|
603
643
|
try {
|
|
604
|
-
const trackedActiveTask = getActiveTaskForSession(
|
|
644
|
+
const trackedActiveTask = getActiveTaskForSession(projectStorePath, state.sessionId, endedState.project);
|
|
605
645
|
const activeTask = trackedActiveTask ?? (() => {
|
|
606
|
-
const tasks = readTasks(
|
|
646
|
+
const tasks = readTasks(projectStorePath, endedState.project);
|
|
607
647
|
if (!tasks.ok)
|
|
608
648
|
return null;
|
|
609
649
|
return tasks.data.items.Active[0] ?? null;
|
|
610
650
|
})();
|
|
611
651
|
if (activeTask) {
|
|
612
652
|
const taskId = activeTask.stableId || activeTask.id;
|
|
613
|
-
const projectConfig = readProjectConfig(
|
|
614
|
-
const snapshotRoot = getProjectSourcePath(
|
|
615
|
-
path.join(
|
|
653
|
+
const projectConfig = readProjectConfig(projectStorePath, endedState.project);
|
|
654
|
+
const snapshotRoot = getProjectSourcePath(projectStorePath, endedState.project, projectConfig) ||
|
|
655
|
+
path.join(projectStorePath, endedState.project);
|
|
616
656
|
const { gitStatus, editedFiles } = collectGitStatusSnapshot(snapshotRoot);
|
|
617
657
|
const resumptionHint = extractResumptionHint(effectiveSummary, activeTask.line, activeTask.context || "No prior attempt captured");
|
|
618
658
|
writeTaskCheckpoint(phrenPath, {
|
|
@@ -635,7 +675,7 @@ export function register(server, ctx) {
|
|
|
635
675
|
}
|
|
636
676
|
try {
|
|
637
677
|
const tasksCompleted = Number.isFinite(endedState.tasksCompleted) ? endedState.tasksCompleted : 0;
|
|
638
|
-
if (tasksCompleted > 0 || hasCompletedTasksInSession(phrenPath, state.sessionId, endedState.project)) {
|
|
678
|
+
if (tasksCompleted > 0 || await hasCompletedTasksInSession(phrenPath, state.sessionId, endedState.project)) {
|
|
639
679
|
markImpactEntriesCompletedForSession(phrenPath, state.sessionId, endedState.project);
|
|
640
680
|
}
|
|
641
681
|
}
|
|
@@ -707,7 +747,7 @@ export function register(server, ctx) {
|
|
|
707
747
|
const session = sessions.find(s => s.sessionId === targetSessionId || s.sessionId.startsWith(targetSessionId));
|
|
708
748
|
if (!session)
|
|
709
749
|
return mcpResponse({ ok: false, error: `Session ${targetSessionId} not found.` });
|
|
710
|
-
const artifacts = getSessionArtifacts(phrenPath, session.sessionId, project);
|
|
750
|
+
const artifacts = await getSessionArtifacts(phrenPath, session.sessionId, project);
|
|
711
751
|
const parts = [
|
|
712
752
|
`Session: ${session.sessionId.slice(0, 8)}`,
|
|
713
753
|
`Project: ${session.project ?? "none"}`,
|