@phren/cli 0.0.28 → 0.0.33
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/capabilities/cli.js +2 -5
- package/mcp/dist/capabilities/mcp.js +5 -8
- package/mcp/dist/capabilities/types.js +2 -5
- package/mcp/dist/capabilities/vscode.js +2 -5
- package/mcp/dist/capabilities/web-ui.js +2 -5
- package/mcp/dist/{cli-actions.js → cli/actions.js} +25 -21
- package/mcp/dist/{cli.js → cli/cli.js} +13 -13
- package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
- package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
- package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
- package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
- package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
- package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
- package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
- package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
- package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +58 -117
- package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
- package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
- package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
- package/mcp/dist/{cli-search.js → cli/search.js} +12 -11
- package/mcp/dist/cli-hooks-git.js +243 -0
- package/mcp/dist/cli-hooks-prompt.js +323 -0
- package/mcp/dist/cli-hooks-session-handlers.js +337 -0
- package/mcp/dist/cli-hooks-stop.js +519 -0
- package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
- package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
- package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
- package/mcp/dist/{content-learning.js → content/learning.js} +41 -20
- package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
- package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
- package/mcp/dist/{core-project.js → core/project.js} +4 -4
- package/mcp/dist/{core-search.js → core/search.js} +2 -2
- package/mcp/dist/{data-access.js → data/access.js} +142 -15
- package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
- package/mcp/dist/embedding.js +9 -14
- package/mcp/dist/entrypoint.js +11 -11
- package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
- package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
- package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
- package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +13 -7
- package/mcp/dist/governance/audit.js +30 -0
- package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
- package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
- package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
- package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
- package/mcp/dist/hooks.js +53 -37
- package/mcp/dist/index-query.js +4 -1
- package/mcp/dist/index.js +54 -30
- package/mcp/dist/{init-config.js → init/config.js} +6 -6
- package/mcp/dist/{init.js → init/init.js} +80 -69
- package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
- package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
- package/mcp/dist/{init-shared.js → init/shared.js} +4 -4
- package/mcp/dist/init-bootstrap.js +21 -0
- package/mcp/dist/init-detect.js +38 -0
- package/mcp/dist/init-env.js +114 -0
- package/mcp/dist/init-fresh.js +234 -0
- package/mcp/dist/init-hooks.js +26 -0
- package/mcp/dist/init-mcp.js +65 -0
- package/mcp/dist/init-modes.js +135 -0
- package/mcp/dist/init-npm.js +37 -0
- package/mcp/dist/init-project-local.js +99 -0
- package/mcp/dist/init-semantic.js +48 -0
- package/mcp/dist/init-types.js +1 -0
- package/mcp/dist/init-uninstall.js +504 -0
- package/mcp/dist/init-update.js +96 -0
- package/mcp/dist/init-walkthrough.js +524 -0
- package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
- package/mcp/dist/{link-context.js → link/context.js} +4 -4
- package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
- package/mcp/dist/{link.js → link/link.js} +26 -31
- package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
- package/mcp/dist/logger.js +11 -3
- package/mcp/dist/package-metadata.js +1 -1
- package/mcp/dist/phren-art.js +4 -126
- package/mcp/dist/phren-paths.js +30 -12
- package/mcp/dist/proactivity.js +3 -3
- package/mcp/dist/profile-store.js +5 -6
- package/mcp/dist/project-config.js +2 -2
- package/mcp/dist/project-topics.js +17 -47
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +1 -1
- package/mcp/dist/runtime-profile.js +1 -1
- package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
- package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
- package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
- package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +28 -3
- package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
- package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +19 -42
- package/mcp/dist/shared/governance.js +4 -0
- package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
- package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
- package/mcp/dist/shared/process.js +24 -0
- package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
- package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
- package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
- package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
- package/mcp/dist/shared.js +6 -60
- package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
- package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
- package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
- package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
- package/mcp/dist/{shell.js → shell/shell.js} +11 -11
- package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
- package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
- package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
- package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
- package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
- package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
- package/mcp/dist/startup-embedding.js +2 -2
- package/mcp/dist/status.js +15 -14
- package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
- package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
- package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
- package/mcp/dist/telemetry.js +3 -4
- package/mcp/dist/tool-registry.js +29 -17
- package/mcp/dist/tools/config.js +530 -0
- package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
- package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
- package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
- package/mcp/dist/tools/finding.js +584 -0
- package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
- package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
- package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
- package/mcp/dist/tools/ops.js +468 -0
- package/mcp/dist/tools/search.js +672 -0
- package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
- package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
- package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
- package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
- package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
- package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
- package/mcp/dist/ui/server.js +1024 -0
- package/mcp/dist/update.js +2 -2
- package/mcp/dist/utils.js +63 -19
- package/package.json +2 -2
- package/scripts/preuninstall.mjs +31 -0
- package/starter/global/CLAUDE.md +3 -2
- package/mcp/dist/governance-audit.js +0 -22
- package/mcp/dist/mcp-config.js +0 -551
- package/mcp/dist/mcp-finding.js +0 -594
- package/mcp/dist/mcp-ops.js +0 -363
- package/mcp/dist/mcp-search.js +0 -668
- package/mcp/dist/memory-ui-server.js +0 -1411
- package/mcp/dist/shared-governance.js +0 -4
- /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
- /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
- /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
- /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
- /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
- /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
- /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
- /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import { isValidProjectName } from "
|
|
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 "
|
|
7
|
-
import { applyGravity } from "
|
|
8
|
-
import {
|
|
9
|
-
import { clearTaskCheckpoint } from "
|
|
10
|
-
import { incrementSessionTasksCompleted } from "./
|
|
11
|
-
import { normalizeMemoryScope } from "
|
|
12
|
-
import { permissionDeniedError } from "
|
|
5
|
+
import { isValidProjectName } from "../utils.js";
|
|
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
|
+
import { applyGravity } from "../data/tasks.js";
|
|
8
|
+
import { parseGithubIssueUrl, } from "../task/github.js";
|
|
9
|
+
import { clearTaskCheckpoint } from "../session/checkpoints.js";
|
|
10
|
+
import { incrementSessionTasksCompleted } from "./session.js";
|
|
11
|
+
import { normalizeMemoryScope } from "../shared.js";
|
|
12
|
+
import { permissionDeniedError } from "../governance/rbac.js";
|
|
13
13
|
const TASK_SECTION_ORDER = ["Active", "Queue", "Done"];
|
|
14
14
|
const DEFAULT_TASK_LIMIT = 20;
|
|
15
15
|
/** Done items are historical — cap tightly by default to avoid large responses. */
|
|
@@ -198,10 +198,13 @@ export function register(server, ctx) {
|
|
|
198
198
|
});
|
|
199
199
|
server.registerTool("add_task", {
|
|
200
200
|
title: "◆ phren · add task",
|
|
201
|
-
description: "Append
|
|
201
|
+
description: "Append one or more tasks to a project's tasks.md file. Adds to the Queue section. Pass a single string or an array of strings.",
|
|
202
202
|
inputSchema: z.object({
|
|
203
203
|
project: z.string().describe("Project name (must match a directory in your phren)."),
|
|
204
|
-
item: z.
|
|
204
|
+
item: z.union([
|
|
205
|
+
z.string().describe("A single task to add."),
|
|
206
|
+
z.array(z.string()).describe("Multiple tasks to add in one call."),
|
|
207
|
+
]).describe("The task(s) to add. Pass a string for one task, or an array for bulk."),
|
|
205
208
|
scope: z.string().optional().describe("Optional memory scope label. Defaults to 'shared'. Example: 'researcher' or 'builder'."),
|
|
206
209
|
}),
|
|
207
210
|
}, async ({ project, item, scope }) => {
|
|
@@ -213,6 +216,17 @@ export function register(server, ctx) {
|
|
|
213
216
|
const normalizedScope = normalizeMemoryScope(scope ?? "shared");
|
|
214
217
|
if (!normalizedScope)
|
|
215
218
|
return mcpResponse({ ok: false, error: `Invalid scope: "${scope}". Use lowercase letters/numbers with '-' or '_' (max 64 chars), e.g. "researcher".` });
|
|
219
|
+
if (Array.isArray(item)) {
|
|
220
|
+
return withWriteQueue(async () => {
|
|
221
|
+
const result = addTasksBatch(phrenPath, project, item, { scope: normalizedScope });
|
|
222
|
+
if (!result.ok)
|
|
223
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
224
|
+
const { added, errors } = result.data;
|
|
225
|
+
if (added.length > 0)
|
|
226
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
227
|
+
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
|
+
});
|
|
229
|
+
}
|
|
216
230
|
return withWriteQueue(async () => {
|
|
217
231
|
const result = addTaskStore(phrenPath, project, item, { scope: normalizedScope });
|
|
218
232
|
if (!result.ok)
|
|
@@ -221,35 +235,15 @@ export function register(server, ctx) {
|
|
|
221
235
|
return mcpResponse({ ok: true, message: `Task added: ${result.data.line}`, data: { project, item, scope: normalizedScope } });
|
|
222
236
|
});
|
|
223
237
|
});
|
|
224
|
-
server.registerTool("add_tasks", {
|
|
225
|
-
title: "◆ phren · add tasks (bulk)",
|
|
226
|
-
description: "Append multiple tasks to a project's tasks.md file in one call. Adds to the Queue section.",
|
|
227
|
-
inputSchema: z.object({
|
|
228
|
-
project: z.string().describe("Project name."),
|
|
229
|
-
items: z.array(z.string()).describe("List of tasks to add."),
|
|
230
|
-
}),
|
|
231
|
-
}, async ({ project, items }) => {
|
|
232
|
-
if (!isValidProjectName(project))
|
|
233
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
234
|
-
const addTasksDenied = permissionDeniedError(phrenPath, "add_task", project);
|
|
235
|
-
if (addTasksDenied)
|
|
236
|
-
return mcpResponse({ ok: false, error: addTasksDenied });
|
|
237
|
-
return withWriteQueue(async () => {
|
|
238
|
-
const result = addTasksBatch(phrenPath, project, items);
|
|
239
|
-
if (!result.ok)
|
|
240
|
-
return mcpResponse({ ok: false, error: result.error });
|
|
241
|
-
const { added, errors } = result.data;
|
|
242
|
-
if (added.length > 0)
|
|
243
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
244
|
-
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 } });
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
238
|
server.registerTool("complete_task", {
|
|
248
239
|
title: "◆ phren · done",
|
|
249
|
-
description: "Move
|
|
240
|
+
description: "Move one or more tasks to the Done section by matching text. Pass a single string or an array of strings.",
|
|
250
241
|
inputSchema: z.object({
|
|
251
242
|
project: z.string().describe("Project name."),
|
|
252
|
-
item: z.
|
|
243
|
+
item: z.union([
|
|
244
|
+
z.string().describe("Exact or partial text of the item to complete."),
|
|
245
|
+
z.array(z.string()).describe("List of partial item texts to complete."),
|
|
246
|
+
]).describe("The task(s) to complete. Pass a string for one, or an array for bulk."),
|
|
253
247
|
sessionId: z.string().optional().describe("Optional session ID from session_start. Pass this to track per-session task completion metrics."),
|
|
254
248
|
}),
|
|
255
249
|
}, async ({ project, item, sessionId }) => {
|
|
@@ -258,6 +252,38 @@ export function register(server, ctx) {
|
|
|
258
252
|
const completeTaskDenied = permissionDeniedError(phrenPath, "complete_task", project);
|
|
259
253
|
if (completeTaskDenied)
|
|
260
254
|
return mcpResponse({ ok: false, error: completeTaskDenied });
|
|
255
|
+
if (Array.isArray(item)) {
|
|
256
|
+
return withWriteQueue(async () => {
|
|
257
|
+
const resolvedItems = item
|
|
258
|
+
.map((match) => {
|
|
259
|
+
const resolved = resolveTaskItem(phrenPath, project, match);
|
|
260
|
+
return resolved.ok ? resolved.data : null;
|
|
261
|
+
})
|
|
262
|
+
.filter((task) => task !== null);
|
|
263
|
+
const result = completeTasksBatch(phrenPath, project, item);
|
|
264
|
+
if (!result.ok)
|
|
265
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
266
|
+
const { completed, errors } = result.data;
|
|
267
|
+
if (completed.length > 0) {
|
|
268
|
+
const completedSet = new Set(completed);
|
|
269
|
+
for (const task of resolvedItems) {
|
|
270
|
+
if (!completedSet.has(task.line))
|
|
271
|
+
continue;
|
|
272
|
+
clearTaskCheckpoint(phrenPath, {
|
|
273
|
+
project,
|
|
274
|
+
taskId: task.stableId ?? task.id,
|
|
275
|
+
stableId: task.stableId,
|
|
276
|
+
positionalId: task.id,
|
|
277
|
+
taskLine: task.line,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
incrementSessionTasksCompleted(phrenPath, completed.length, sessionId, project);
|
|
281
|
+
}
|
|
282
|
+
if (completed.length > 0)
|
|
283
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
284
|
+
return mcpResponse({ ok: completed.length > 0, ...(completed.length === 0 ? { error: `No tasks completed: ${errors.join("; ")}` } : {}), message: `Completed ${completed.length}/${item.length} items`, data: { project, completed, errors } });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
261
287
|
return withWriteQueue(async () => {
|
|
262
288
|
const before = resolveTaskItem(phrenPath, project, item);
|
|
263
289
|
const result = completeTaskStore(phrenPath, project, item);
|
|
@@ -277,57 +303,15 @@ export function register(server, ctx) {
|
|
|
277
303
|
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
278
304
|
});
|
|
279
305
|
});
|
|
280
|
-
server.registerTool("complete_tasks", {
|
|
281
|
-
title: "◆ phren · done (bulk)",
|
|
282
|
-
description: "Move multiple tasks to Done in one call. Pass an array of partial item texts.",
|
|
283
|
-
inputSchema: z.object({
|
|
284
|
-
project: z.string().describe("Project name."),
|
|
285
|
-
items: z.array(z.string()).describe("List of partial item texts to complete."),
|
|
286
|
-
sessionId: z.string().optional().describe("Optional session ID from session_start. Pass this to track per-session task completion metrics."),
|
|
287
|
-
}),
|
|
288
|
-
}, async ({ project, items, sessionId }) => {
|
|
289
|
-
if (!isValidProjectName(project))
|
|
290
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
291
|
-
const completeTasksDenied = permissionDeniedError(phrenPath, "complete_task", project);
|
|
292
|
-
if (completeTasksDenied)
|
|
293
|
-
return mcpResponse({ ok: false, error: completeTasksDenied });
|
|
294
|
-
return withWriteQueue(async () => {
|
|
295
|
-
const resolvedItems = items
|
|
296
|
-
.map((match) => {
|
|
297
|
-
const resolved = resolveTaskItem(phrenPath, project, match);
|
|
298
|
-
return resolved.ok ? resolved.data : null;
|
|
299
|
-
})
|
|
300
|
-
.filter((task) => task !== null);
|
|
301
|
-
const result = completeTasksBatch(phrenPath, project, items);
|
|
302
|
-
if (!result.ok)
|
|
303
|
-
return mcpResponse({ ok: false, error: result.error });
|
|
304
|
-
const { completed, errors } = result.data;
|
|
305
|
-
if (completed.length > 0) {
|
|
306
|
-
const completedSet = new Set(completed);
|
|
307
|
-
for (const task of resolvedItems) {
|
|
308
|
-
if (!completedSet.has(task.line))
|
|
309
|
-
continue;
|
|
310
|
-
clearTaskCheckpoint(phrenPath, {
|
|
311
|
-
project,
|
|
312
|
-
taskId: task.stableId ?? task.id,
|
|
313
|
-
stableId: task.stableId,
|
|
314
|
-
positionalId: task.id,
|
|
315
|
-
taskLine: task.line,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
incrementSessionTasksCompleted(phrenPath, completed.length, sessionId, project);
|
|
319
|
-
}
|
|
320
|
-
if (completed.length > 0)
|
|
321
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
322
|
-
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 } });
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
306
|
server.registerTool("remove_task", {
|
|
326
307
|
title: "◆ phren · remove task",
|
|
327
|
-
description: "Remove
|
|
308
|
+
description: "Remove one or more tasks from a project's tasks.md file by matching text or ID. Pass a single string or an array of strings.",
|
|
328
309
|
inputSchema: z.object({
|
|
329
310
|
project: z.string().describe("Project name."),
|
|
330
|
-
item: z.
|
|
311
|
+
item: z.union([
|
|
312
|
+
z.string().describe("Exact or partial text of the task, or a task ID like A1/Q3/D2."),
|
|
313
|
+
z.array(z.string()).describe("List of partial item texts or IDs to remove."),
|
|
314
|
+
]).describe("The task(s) to remove. Pass a string for one, or an array for bulk."),
|
|
331
315
|
}),
|
|
332
316
|
}, async ({ project, item }) => {
|
|
333
317
|
if (!isValidProjectName(project))
|
|
@@ -335,6 +319,17 @@ export function register(server, ctx) {
|
|
|
335
319
|
const removeTaskDenied = permissionDeniedError(phrenPath, "remove_task", project);
|
|
336
320
|
if (removeTaskDenied)
|
|
337
321
|
return mcpResponse({ ok: false, error: removeTaskDenied });
|
|
322
|
+
if (Array.isArray(item)) {
|
|
323
|
+
return withWriteQueue(async () => {
|
|
324
|
+
const result = removeTasksBatch(phrenPath, project, item);
|
|
325
|
+
if (!result.ok)
|
|
326
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
327
|
+
const { removed, errors } = result.data;
|
|
328
|
+
if (removed.length > 0)
|
|
329
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
330
|
+
return mcpResponse({ ok: removed.length > 0, ...(removed.length === 0 ? { error: `No tasks removed: ${errors.join("; ")}` } : {}), message: `Removed ${removed.length}/${item.length} items`, data: { project, removed, errors } });
|
|
331
|
+
});
|
|
332
|
+
}
|
|
338
333
|
return withWriteQueue(async () => {
|
|
339
334
|
const result = removeTaskStore(phrenPath, project, item);
|
|
340
335
|
if (!result.ok)
|
|
@@ -343,35 +338,14 @@ export function register(server, ctx) {
|
|
|
343
338
|
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
344
339
|
});
|
|
345
340
|
});
|
|
346
|
-
server.registerTool("remove_tasks", {
|
|
347
|
-
title: "◆ phren · remove tasks (bulk)",
|
|
348
|
-
description: "Remove multiple tasks in one call. Pass an array of partial item texts or IDs.",
|
|
349
|
-
inputSchema: z.object({
|
|
350
|
-
project: z.string().describe("Project name."),
|
|
351
|
-
items: z.array(z.string()).describe("List of partial item texts or IDs to remove."),
|
|
352
|
-
}),
|
|
353
|
-
}, async ({ project, items }) => {
|
|
354
|
-
if (!isValidProjectName(project))
|
|
355
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
356
|
-
const removeTasksDenied = permissionDeniedError(phrenPath, "remove_task", project);
|
|
357
|
-
if (removeTasksDenied)
|
|
358
|
-
return mcpResponse({ ok: false, error: removeTasksDenied });
|
|
359
|
-
return withWriteQueue(async () => {
|
|
360
|
-
const result = removeTasksBatch(phrenPath, project, items);
|
|
361
|
-
if (!result.ok)
|
|
362
|
-
return mcpResponse({ ok: false, error: result.error });
|
|
363
|
-
const { removed, errors } = result.data;
|
|
364
|
-
if (removed.length > 0)
|
|
365
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
366
|
-
return mcpResponse({ ok: removed.length > 0, ...(removed.length === 0 ? { error: `No tasks removed: ${errors.join("; ")}` } : {}), message: `Removed ${removed.length}/${items.length} items`, data: { project, removed, errors } });
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
341
|
server.registerTool("update_task", {
|
|
370
342
|
title: "◆ phren · update task",
|
|
371
|
-
description: "Update a task's text, priority, context, section,
|
|
343
|
+
description: "Update a task's text, priority, context, section, GitHub metadata, pin status, or promote it. " +
|
|
344
|
+
"Also supports work_next (pick highest-priority Queue item) and promote (clear speculative flag). " +
|
|
345
|
+
"When work_next is true, item is not needed.",
|
|
372
346
|
inputSchema: z.object({
|
|
373
347
|
project: z.string().describe("Project name."),
|
|
374
|
-
item: z.string().describe("Partial text to match against existing tasks."),
|
|
348
|
+
item: z.string().optional().describe("Partial text to match against existing tasks. Required unless work_next is true."),
|
|
375
349
|
updates: z.object({
|
|
376
350
|
text: z.string().optional().describe("Replacement text for the task line."),
|
|
377
351
|
priority: z.enum(["high", "medium", "low"]).optional().describe("New priority tag: high, medium, or low."),
|
|
@@ -381,6 +355,10 @@ export function register(server, ctx) {
|
|
|
381
355
|
github_issue: z.union([z.number().int().positive(), z.string()]).optional().describe("GitHub issue number (for example 14 or '#14')."),
|
|
382
356
|
github_url: z.string().optional().describe("GitHub issue URL to associate with the task item."),
|
|
383
357
|
unlink_github: z.boolean().optional().describe("If true, remove any linked GitHub issue metadata from the item."),
|
|
358
|
+
pin: z.boolean().optional().describe("If true, pin the task so it floats to the top of its section."),
|
|
359
|
+
promote: z.boolean().optional().describe("If true, clear the speculative flag on this task (confirm the user wants it)."),
|
|
360
|
+
move_to_active: z.boolean().optional().describe("Used with promote: also move the task to the Active section."),
|
|
361
|
+
work_next: z.boolean().optional().describe("If true, pick the highest-priority Queue item and move it to Active. Ignores item param."),
|
|
384
362
|
}).describe("Fields to update. All are optional."),
|
|
385
363
|
}),
|
|
386
364
|
}, async ({ project, item, updates }) => {
|
|
@@ -389,189 +367,84 @@ export function register(server, ctx) {
|
|
|
389
367
|
const updateTaskDenied = permissionDeniedError(phrenPath, "update_task", project);
|
|
390
368
|
if (updateTaskDenied)
|
|
391
369
|
return mcpResponse({ ok: false, error: updateTaskDenied });
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
item: z.string().describe("Task text, ID, or stable bid to link."),
|
|
406
|
-
issue_number: z.union([z.number().int().positive(), z.string()]).optional().describe("Existing GitHub issue number (for example 14 or '#14')."),
|
|
407
|
-
issue_url: z.string().optional().describe("Existing GitHub issue URL."),
|
|
408
|
-
unlink: z.boolean().optional().describe("If true, remove any linked issue from the task item."),
|
|
409
|
-
}),
|
|
410
|
-
}, async ({ project, item, issue_number, issue_url, unlink }) => {
|
|
411
|
-
if (!isValidProjectName(project))
|
|
412
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
413
|
-
return withWriteQueue(async () => {
|
|
414
|
-
if (unlink && (issue_number !== undefined || issue_url)) {
|
|
415
|
-
return mcpResponse({ ok: false, error: "Use either unlink=true or issue_number/issue_url, not both." });
|
|
416
|
-
}
|
|
417
|
-
if (!unlink && issue_number === undefined && !issue_url) {
|
|
418
|
-
return mcpResponse({ ok: false, error: "Provide issue_number or issue_url to link, or unlink=true to remove the link." });
|
|
419
|
-
}
|
|
420
|
-
if (issue_url) {
|
|
421
|
-
const parsed = parseGithubIssueUrl(issue_url);
|
|
422
|
-
if (!parsed)
|
|
423
|
-
return mcpResponse({ ok: false, error: "issue_url must be a valid GitHub issue URL." });
|
|
424
|
-
if (issue_number !== undefined) {
|
|
425
|
-
const normalizedIssue = Number.parseInt(String(issue_number).replace(/^#/, ""), 10);
|
|
426
|
-
if (normalizedIssue !== parsed.issueNumber) {
|
|
427
|
-
return mcpResponse({ ok: false, error: "issue_number and issue_url refer to different issues." });
|
|
428
|
-
}
|
|
370
|
+
// Runtime validation: item is required unless work_next is true
|
|
371
|
+
if (!updates.work_next && !item) {
|
|
372
|
+
return mcpResponse({ ok: false, error: "item is required unless updates.work_next is true." });
|
|
373
|
+
}
|
|
374
|
+
// Cross-validate github_issue and github_url
|
|
375
|
+
if (updates.github_url) {
|
|
376
|
+
const parsed = parseGithubIssueUrl(updates.github_url);
|
|
377
|
+
if (!parsed)
|
|
378
|
+
return mcpResponse({ ok: false, error: "github_url must be a valid GitHub issue URL." });
|
|
379
|
+
if (updates.github_issue !== undefined) {
|
|
380
|
+
const normalizedIssue = Number.parseInt(String(updates.github_issue).replace(/^#/, ""), 10);
|
|
381
|
+
if (normalizedIssue !== parsed.issueNumber) {
|
|
382
|
+
return mcpResponse({ ok: false, error: "github_issue and github_url refer to different issues." });
|
|
429
383
|
}
|
|
430
384
|
}
|
|
431
|
-
|
|
432
|
-
github_issue: issue_number,
|
|
433
|
-
github_url: issue_url,
|
|
434
|
-
unlink: unlink ?? false,
|
|
435
|
-
});
|
|
436
|
-
if (!result.ok)
|
|
437
|
-
return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
|
|
438
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
439
|
-
return mcpResponse({
|
|
440
|
-
ok: true,
|
|
441
|
-
message: unlink
|
|
442
|
-
? `Removed GitHub link from ${project} task.`
|
|
443
|
-
: `Linked ${project} task to ${result.data.githubIssue ? `#${result.data.githubIssue}` : result.data.githubUrl}.`,
|
|
444
|
-
data: {
|
|
445
|
-
project,
|
|
446
|
-
item,
|
|
447
|
-
stableId: result.data.stableId || null,
|
|
448
|
-
githubIssue: result.data.githubIssue ?? null,
|
|
449
|
-
githubUrl: result.data.githubUrl || null,
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
server.registerTool("promote_task_to_issue", {
|
|
455
|
-
title: "◆ phren · promote task",
|
|
456
|
-
description: "Create a GitHub issue from a task and link it back into the task list.",
|
|
457
|
-
inputSchema: z.object({
|
|
458
|
-
project: z.string().describe("Project name."),
|
|
459
|
-
item: z.string().describe("Task text, ID, or stable bid to promote."),
|
|
460
|
-
repo: z.string().optional().describe("Target GitHub repo in owner/name form. If omitted, phren tries to infer it from CLAUDE.md or summary.md."),
|
|
461
|
-
title: z.string().optional().describe("Optional GitHub issue title. Defaults to the task text."),
|
|
462
|
-
body: z.string().optional().describe("Optional GitHub issue body. Defaults to a body built from the task plus context."),
|
|
463
|
-
mark_done: z.boolean().optional().describe("If true, mark the task Done after creating and linking the issue."),
|
|
464
|
-
}),
|
|
465
|
-
}, async ({ project, item, repo, title, body, mark_done }) => {
|
|
466
|
-
if (!isValidProjectName(project))
|
|
467
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
385
|
+
}
|
|
468
386
|
return withWriteQueue(async () => {
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
387
|
+
// Handle work_next: pick highest-priority Queue item, move to Active
|
|
388
|
+
if (updates.work_next) {
|
|
389
|
+
const result = workNextTask(phrenPath, project);
|
|
390
|
+
if (!result.ok)
|
|
391
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
392
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
393
|
+
return mcpResponse({ ok: true, message: result.data, data: { project } });
|
|
475
394
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return mcpResponse({ ok:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
clearTaskCheckpoint(phrenPath, {
|
|
495
|
-
project,
|
|
496
|
-
taskId: match.data.stableId ?? match.data.id,
|
|
497
|
-
stableId: match.data.stableId,
|
|
498
|
-
positionalId: match.data.id,
|
|
499
|
-
taskLine: match.data.line,
|
|
395
|
+
// Handle pin
|
|
396
|
+
if (updates.pin) {
|
|
397
|
+
const result = pinTask(phrenPath, project, item);
|
|
398
|
+
if (!result.ok)
|
|
399
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
400
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
401
|
+
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
402
|
+
}
|
|
403
|
+
// Handle promote (clear speculative flag)
|
|
404
|
+
if (updates.promote) {
|
|
405
|
+
const result = promoteTask(phrenPath, project, item, updates.move_to_active ?? false);
|
|
406
|
+
if (!result.ok)
|
|
407
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
408
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
409
|
+
return mcpResponse({
|
|
410
|
+
ok: true,
|
|
411
|
+
message: `Promoted task "${result.data.line}" in ${project}${updates.move_to_active ? " (moved to Active)" : ""}.`,
|
|
412
|
+
data: { project, item: result.data },
|
|
500
413
|
});
|
|
501
414
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
532
|
-
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
server.registerTool("work_next_task", {
|
|
536
|
-
title: "◆ phren · work next",
|
|
537
|
-
description: "Move the highest-priority Queue item to Active so it becomes the next task to work on.",
|
|
538
|
-
inputSchema: z.object({
|
|
539
|
-
project: z.string().describe("Project name."),
|
|
540
|
-
}),
|
|
541
|
-
}, async ({ project }) => {
|
|
542
|
-
if (!isValidProjectName(project))
|
|
543
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
544
|
-
return withWriteQueue(async () => {
|
|
545
|
-
const result = workNextTask(phrenPath, project);
|
|
546
|
-
if (!result.ok)
|
|
547
|
-
return mcpResponse({ ok: false, error: result.error });
|
|
548
|
-
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
549
|
-
return mcpResponse({ ok: true, message: result.data, data: { project } });
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
server.registerTool("promote_task", {
|
|
553
|
-
title: "◆ phren · promote task",
|
|
554
|
-
description: "Promote a speculative task to committed by clearing the speculative flag. " +
|
|
555
|
-
"Use this when the user says 'yes do it', 'let's work on that', or otherwise confirms " +
|
|
556
|
-
"they want to commit to a suggested task. Optionally moves it to Active.",
|
|
557
|
-
inputSchema: z.object({
|
|
558
|
-
project: z.string().describe("Project name."),
|
|
559
|
-
item: z.string().describe("Partial text, stable ID (bid:XXXX), or positional ID of the speculative task."),
|
|
560
|
-
move_to_active: z.boolean().optional().describe("If true, also move the task to the Active section. Default false."),
|
|
561
|
-
}),
|
|
562
|
-
}, async ({ project, item, move_to_active }) => {
|
|
563
|
-
if (!isValidProjectName(project))
|
|
564
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
565
|
-
return withWriteQueue(async () => {
|
|
566
|
-
const result = promoteTask(phrenPath, project, item, move_to_active ?? false);
|
|
415
|
+
// Handle github issue linking via update_task when github_issue or github_url is set (and no other field updates)
|
|
416
|
+
if ((updates.github_issue !== undefined || updates.github_url || updates.unlink_github) && !updates.text && !updates.priority && !updates.context && !updates.section) {
|
|
417
|
+
if (updates.unlink_github && (updates.github_issue !== undefined || updates.github_url)) {
|
|
418
|
+
return mcpResponse({ ok: false, error: "Use either unlink_github=true or github_issue/github_url, not both." });
|
|
419
|
+
}
|
|
420
|
+
const result = linkTaskIssue(phrenPath, project, item, {
|
|
421
|
+
github_issue: updates.github_issue,
|
|
422
|
+
github_url: updates.github_url,
|
|
423
|
+
unlink: updates.unlink_github ?? false,
|
|
424
|
+
});
|
|
425
|
+
if (!result.ok)
|
|
426
|
+
return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
|
|
427
|
+
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
428
|
+
return mcpResponse({
|
|
429
|
+
ok: true,
|
|
430
|
+
message: updates.unlink_github
|
|
431
|
+
? `Removed GitHub link from ${project} task.`
|
|
432
|
+
: `Linked ${project} task to ${result.data.githubIssue ? `#${result.data.githubIssue}` : result.data.githubUrl}.`,
|
|
433
|
+
data: {
|
|
434
|
+
project,
|
|
435
|
+
item,
|
|
436
|
+
stableId: result.data.stableId || null,
|
|
437
|
+
githubIssue: result.data.githubIssue ?? null,
|
|
438
|
+
githubUrl: result.data.githubUrl || null,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// Standard update path
|
|
443
|
+
const result = updateTaskStore(phrenPath, project, item, updates);
|
|
567
444
|
if (!result.ok)
|
|
568
445
|
return mcpResponse({ ok: false, error: result.error });
|
|
569
446
|
refreshTaskIndex(updateFileInIndex, phrenPath, project);
|
|
570
|
-
return mcpResponse({
|
|
571
|
-
ok: true,
|
|
572
|
-
message: `Promoted task "${result.data.line}" in ${project}${move_to_active ? " (moved to Active)" : ""}.`,
|
|
573
|
-
data: { project, item: result.data },
|
|
574
|
-
});
|
|
447
|
+
return mcpResponse({ ok: true, message: result.data, data: { project, item, updates } });
|
|
575
448
|
});
|
|
576
449
|
});
|
|
577
450
|
server.registerTool("tidy_done_tasks", {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
-
import { getProjectDirs, runtimeDir, runtimeHealthFile, memoryUsageLogFile, homePath, } from "
|
|
5
|
-
import { errorMessage } from "
|
|
6
|
-
import { readInstallPreferences } from "
|
|
7
|
-
import { readCustomHooks } from "
|
|
8
|
-
import { hookConfigPaths, hookConfigRoots } from "
|
|
9
|
-
import { readProjectConfig, isProjectHookEnabled, PROJECT_HOOK_EVENTS } from "
|
|
10
|
-
import { getAllSkills } from "
|
|
11
|
-
import { resolveTaskFilePath, readTasks, TASKS_FILENAME } from "
|
|
12
|
-
import { buildIndex, queryDocBySourceKey, queryRows } from "
|
|
13
|
-
import { readProjectTopics, classifyTopicForText } from "
|
|
14
|
-
import { entryScoreKey } from "
|
|
4
|
+
import { getProjectDirs, runtimeDir, runtimeHealthFile, memoryUsageLogFile, homePath, } from "../shared.js";
|
|
5
|
+
import { errorMessage } from "../utils.js";
|
|
6
|
+
import { readInstallPreferences } from "../init/preferences.js";
|
|
7
|
+
import { readCustomHooks } from "../hooks.js";
|
|
8
|
+
import { hookConfigPaths, hookConfigRoots } from "../provider-adapters.js";
|
|
9
|
+
import { readProjectConfig, isProjectHookEnabled, PROJECT_HOOK_EVENTS } from "../project-config.js";
|
|
10
|
+
import { getAllSkills } from "../skill/registry.js";
|
|
11
|
+
import { resolveTaskFilePath, readTasks, TASKS_FILENAME } from "../data/tasks.js";
|
|
12
|
+
import { buildIndex, queryDocBySourceKey, queryRows } from "../shared/index.js";
|
|
13
|
+
import { readProjectTopics, classifyTopicForText } from "../project-topics.js";
|
|
14
|
+
import { entryScoreKey } from "../governance/scores.js";
|
|
15
|
+
import { logger } from "../logger.js";
|
|
15
16
|
function extractGithubUrl(content) {
|
|
16
17
|
const match = content.match(/https?:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/);
|
|
17
18
|
return match ? match[0] : undefined;
|
|
@@ -94,6 +95,18 @@ export function isAllowedFilePath(filePath, phrenPath) {
|
|
|
94
95
|
});
|
|
95
96
|
return allowedRealRoots.some((root) => realResolved === root || realResolved.startsWith(root + path.sep));
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Stricter path check for skill endpoints — only allows files under skills/ directories,
|
|
100
|
+
* not the entire phren store.
|
|
101
|
+
*/
|
|
102
|
+
export function isAllowedSkillPath(filePath, phrenPath) {
|
|
103
|
+
if (!isAllowedFilePath(filePath, phrenPath))
|
|
104
|
+
return false;
|
|
105
|
+
const resolved = path.resolve(filePath);
|
|
106
|
+
// Must be under a "skills" directory segment
|
|
107
|
+
const segments = resolved.split(path.sep);
|
|
108
|
+
return segments.some((seg) => seg === "skills");
|
|
109
|
+
}
|
|
97
110
|
export function collectSkillsForUI(phrenPath, profile = "") {
|
|
98
111
|
return getAllSkills(phrenPath, profile).map((skill) => ({
|
|
99
112
|
name: skill.name,
|
|
@@ -136,7 +149,7 @@ export function getHooksData(phrenPath, profile) {
|
|
|
136
149
|
}
|
|
137
150
|
return { globalEnabled, tools, customHooks: readCustomHooks(phrenPath), projectOverrides };
|
|
138
151
|
}
|
|
139
|
-
export async function buildGraph(phrenPath, profile, focusProject) {
|
|
152
|
+
export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
140
153
|
const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
|
|
141
154
|
const nodes = [];
|
|
142
155
|
const links = [];
|
|
@@ -287,9 +300,11 @@ export async function buildGraph(phrenPath, profile, focusProject) {
|
|
|
287
300
|
// task loading failed — continue with other data sources
|
|
288
301
|
}
|
|
289
302
|
// ── Fragments (fragment graph) ──────────────────────────────────────
|
|
290
|
-
|
|
303
|
+
const ownDb = !existingDb;
|
|
304
|
+
let db = existingDb ?? null;
|
|
291
305
|
try {
|
|
292
|
-
|
|
306
|
+
if (!db)
|
|
307
|
+
db = await buildIndex(phrenPath, profile);
|
|
293
308
|
const rows = queryRows(db, `SELECT e.id, e.name, e.type, COUNT(DISTINCT el.source_doc) as ref_count
|
|
294
309
|
FROM entities e JOIN entity_links el ON el.target_id = e.id WHERE e.type != 'document'
|
|
295
310
|
GROUP BY e.id, e.name, e.type ORDER BY ref_count DESC LIMIT 5000`, []);
|
|
@@ -364,7 +379,7 @@ export async function buildGraph(phrenPath, profile, focusProject) {
|
|
|
364
379
|
// fragment loading failed — continue with other data sources
|
|
365
380
|
}
|
|
366
381
|
finally {
|
|
367
|
-
if (db) {
|
|
382
|
+
if (ownDb && db) {
|
|
368
383
|
try {
|
|
369
384
|
db.close();
|
|
370
385
|
}
|
|
@@ -467,8 +482,7 @@ export function collectProjectsForUI(phrenPath, profile) {
|
|
|
467
482
|
}
|
|
468
483
|
}
|
|
469
484
|
catch (err) {
|
|
470
|
-
|
|
471
|
-
process.stderr.write(`[phren] memory-ui filterByProfile: ${errorMessage(err)}\n`);
|
|
485
|
+
logger.debug("memory-ui", `memory-ui filterByProfile: ${errorMessage(err)}`);
|
|
472
486
|
}
|
|
473
487
|
const results = [];
|
|
474
488
|
for (const project of projects) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createWebUiHttpServer, startWebUiServer, } from "./
|
|
2
|
-
import { renderWebUiPage } from "./
|
|
3
|
-
export { renderPageForTests } from "./
|
|
1
|
+
import { createWebUiHttpServer, startWebUiServer, } from "./server.js";
|
|
2
|
+
import { renderWebUiPage } from "./page.js";
|
|
3
|
+
export { renderPageForTests } from "./page.js";
|
|
4
4
|
export function createWebUiServer(phrenPath, opts, profile) {
|
|
5
5
|
return createWebUiHttpServer(phrenPath, renderWebUiPage, profile, opts);
|
|
6
6
|
}
|