@phren/cli 0.0.28 → 0.0.32
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} +22 -21
- package/mcp/dist/{cli.js → cli/cli.js} +13 -13
- package/mcp/dist/{cli-config.js → cli/config.js} +9 -9
- package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
- package/mcp/dist/{cli-govern.js → cli/govern.js} +10 -9
- 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} +42 -57
- 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} +8 -7
- package/mcp/dist/cli-hooks-git.js +243 -0
- package/mcp/dist/cli-hooks-prompt.js +319 -0
- package/mcp/dist/cli-hooks-session-handlers.js +349 -0
- package/mcp/dist/cli-hooks-stop.js +557 -0
- package/mcp/dist/{content-archive.js → content/archive.js} +8 -9
- 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} +12 -12
- 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} +131 -13
- 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} +4 -4
- package/mcp/dist/{governance-audit.js → governance/audit.js} +2 -2
- package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
- package/mcp/dist/{governance-policy.js → governance/policy.js} +10 -12
- package/mcp/dist/{governance-rbac.js → governance/rbac.js} +3 -3
- package/mcp/dist/{governance-scores.js → governance/scores.js} +8 -10
- package/mcp/dist/hooks.js +39 -31
- package/mcp/dist/index-query.js +4 -1
- package/mcp/dist/index.js +53 -29
- package/mcp/dist/{init-config.js → init/config.js} +6 -6
- package/mcp/dist/{init.js → init/init.js} +28 -29
- 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} +3 -3
- package/mcp/dist/init-bootstrap.js +68 -0
- package/mcp/dist/init-detect.js +38 -0
- package/mcp/dist/init-dryrun.js +55 -0
- package/mcp/dist/init-env.js +114 -0
- package/mcp/dist/init-fresh.js +239 -0
- package/mcp/dist/init-hooks.js +26 -0
- package/mcp/dist/init-mcp.js +65 -0
- package/mcp/dist/init-migrate.js +51 -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 +482 -0
- package/mcp/dist/init-update.js +96 -0
- package/mcp/dist/init-walkthrough-merge.js +90 -0
- package/mcp/dist/init-walkthrough.js +529 -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/phren-art.js +0 -6
- package/mcp/dist/phren-paths.js +30 -12
- package/mcp/dist/proactivity.js +2 -2
- package/mcp/dist/profile-store.js +5 -6
- package/mcp/dist/project-config.js +2 -2
- package/mcp/dist/project-topics.js +1 -1
- package/mcp/dist/query-correlation.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} +3 -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} +15 -24
- package/mcp/dist/shared/governance.js +4 -0
- package/mcp/dist/{shared-index.js → shared/index.js} +92 -123
- package/mcp/dist/{shared-ollama.js → shared/ollama.js} +2 -2
- package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +16 -21
- package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +17 -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 +4 -59
- 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} +1 -1
- 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} +4 -4
- package/mcp/dist/{skill-state.js → skill/state.js} +1 -1
- package/mcp/dist/startup-embedding.js +2 -2
- package/mcp/dist/status.js +15 -14
- package/mcp/dist/{tasks-github.js → task/github.js} +2 -2
- package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
- package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +7 -7
- package/mcp/dist/telemetry.js +3 -4
- package/mcp/dist/tool-registry.js +29 -17
- package/mcp/dist/tools/config.js +515 -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/{mcp-finding.js → tools/finding.js} +97 -124
- 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/{mcp-ops.js → tools/ops.js} +169 -71
- package/mcp/dist/{mcp-search.js → tools/search.js} +19 -23
- package/mcp/dist/{mcp-session.js → tools/session.js} +48 -23
- package/mcp/dist/{mcp-skills.js → tools/skills.js} +33 -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} +4 -6
- package/mcp/dist/{memory-ui-server.js → ui/server.js} +30 -22
- package/mcp/dist/update.js +2 -2
- package/mcp/dist/utils.js +51 -11
- package/package.json +2 -2
- package/scripts/preuninstall.mjs +31 -0
- package/starter/global/CLAUDE.md +3 -2
- package/mcp/dist/mcp-config.js +0 -551
- 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,16 +1,19 @@
|
|
|
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 { runtimeFile, getProjectDirs } from "
|
|
6
|
-
import { findFtsCacheForPath } from "
|
|
7
|
-
import { isValidProjectName, errorMessage } from "
|
|
8
|
-
import { readReviewQueue, readReviewQueueAcrossProjects } from "
|
|
9
|
-
import { addProjectFromPath } from "
|
|
10
|
-
import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "
|
|
11
|
-
import { resolveRuntimeProfile } from "
|
|
12
|
-
import { getMachineName } from "
|
|
13
|
-
import { getProjectConsolidationStatus, CONSOLIDATION_ENTRY_THRESHOLD } from "
|
|
5
|
+
import { runtimeFile, getProjectDirs } from "../shared.js";
|
|
6
|
+
import { findFtsCacheForPath } from "../shared/index.js";
|
|
7
|
+
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
8
|
+
import { readReviewQueue, readReviewQueueAcrossProjects, approveQueueItem, rejectQueueItem, editQueueItem } from "../data/access.js";
|
|
9
|
+
import { addProjectFromPath } from "../core/project.js";
|
|
10
|
+
import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "../project-config.js";
|
|
11
|
+
import { resolveRuntimeProfile } from "../runtime-profile.js";
|
|
12
|
+
import { getMachineName } from "../machine-identity.js";
|
|
13
|
+
import { getProjectConsolidationStatus, CONSOLIDATION_ENTRY_THRESHOLD } from "../content/validate.js";
|
|
14
|
+
import { logger } from "../logger.js";
|
|
15
|
+
import { getRuntimeHealth } from "../governance/policy.js";
|
|
16
|
+
import { countUnsyncedCommits } from "../cli-hooks-git.js";
|
|
14
17
|
export function register(server, ctx) {
|
|
15
18
|
const { phrenPath, profile, withWriteQueue } = ctx;
|
|
16
19
|
// ── add_project ────────────────────────────────────────────────────────────
|
|
@@ -49,49 +52,15 @@ export function register(server, ctx) {
|
|
|
49
52
|
}
|
|
50
53
|
});
|
|
51
54
|
});
|
|
52
|
-
server.registerTool("get_consolidation_status", {
|
|
53
|
-
title: "◆ phren · consolidation status",
|
|
54
|
-
description: "Check whether a project's FINDINGS.md needs consolidation. " +
|
|
55
|
-
"Returns entry count since last consolidation, threshold, and recommendation.",
|
|
56
|
-
inputSchema: z.object({
|
|
57
|
-
project: z.string().optional().describe("Project name. If omitted, checks all projects."),
|
|
58
|
-
}),
|
|
59
|
-
}, async ({ project }) => {
|
|
60
|
-
const projectDirs = project
|
|
61
|
-
? (() => {
|
|
62
|
-
if (!isValidProjectName(project))
|
|
63
|
-
return [];
|
|
64
|
-
const dir = path.join(phrenPath, project);
|
|
65
|
-
return fs.existsSync(dir) ? [dir] : [];
|
|
66
|
-
})()
|
|
67
|
-
: getProjectDirs(phrenPath, profile);
|
|
68
|
-
if (project && projectDirs.length === 0) {
|
|
69
|
-
return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
|
|
70
|
-
}
|
|
71
|
-
const results = [];
|
|
72
|
-
for (const dir of projectDirs) {
|
|
73
|
-
const status = getProjectConsolidationStatus(dir);
|
|
74
|
-
if (!status)
|
|
75
|
-
continue;
|
|
76
|
-
results.push({ ...status, threshold: CONSOLIDATION_ENTRY_THRESHOLD });
|
|
77
|
-
}
|
|
78
|
-
if (results.length === 0) {
|
|
79
|
-
return mcpResponse({ ok: true, message: "No FINDINGS.md files found.", data: { results: [] } });
|
|
80
|
-
}
|
|
81
|
-
const lines = results.map(r => `${r.project}: ${r.entriesSince} entries since${r.lastConsolidated ? ` ${r.lastConsolidated}` : " (never consolidated)"}` +
|
|
82
|
-
`${r.recommended ? " — consolidation recommended" : ""}`);
|
|
83
|
-
return mcpResponse({
|
|
84
|
-
ok: true,
|
|
85
|
-
message: lines.join("\n"),
|
|
86
|
-
data: { results },
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
55
|
// ── health_check ───────────────────────────────────────────────────────────
|
|
90
56
|
server.registerTool("health_check", {
|
|
91
57
|
title: "◆ phren · health",
|
|
92
|
-
description: "Return phren health status: version, FTS index status, hook registration,
|
|
93
|
-
inputSchema: z.object({
|
|
94
|
-
|
|
58
|
+
description: "Return phren health status: version, FTS index status, hook registration, profile/machine info, and consolidation status for all projects.",
|
|
59
|
+
inputSchema: z.object({
|
|
60
|
+
include_consolidation: z.boolean().optional()
|
|
61
|
+
.describe("Include consolidation status for all projects (default true)."),
|
|
62
|
+
}),
|
|
63
|
+
}, async ({ include_consolidation }) => {
|
|
95
64
|
const activeProfile = (() => {
|
|
96
65
|
try {
|
|
97
66
|
return resolveRuntimeProfile(phrenPath);
|
|
@@ -108,8 +77,7 @@ export function register(server, ctx) {
|
|
|
108
77
|
version = pkg.version || "unknown";
|
|
109
78
|
}
|
|
110
79
|
catch (err) {
|
|
111
|
-
|
|
112
|
-
process.stderr.write(`[phren] healthCheck version: ${errorMessage(err)}\n`);
|
|
80
|
+
logger.debug("healthCheck version", errorMessage(err));
|
|
113
81
|
}
|
|
114
82
|
// FTS index (lives in /tmpphren-fts-*/, not .runtime/)
|
|
115
83
|
let indexStatus = { exists: false };
|
|
@@ -117,27 +85,24 @@ export function register(server, ctx) {
|
|
|
117
85
|
indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
|
|
118
86
|
}
|
|
119
87
|
catch (err) {
|
|
120
|
-
|
|
121
|
-
process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${errorMessage(err)}\n`);
|
|
88
|
+
logger.debug("healthCheck ftsCacheCheck", errorMessage(err));
|
|
122
89
|
}
|
|
123
90
|
// Hook registration
|
|
124
91
|
let hooksEnabled = false;
|
|
125
92
|
try {
|
|
126
|
-
const { getHooksEnabledPreference } = await import("
|
|
93
|
+
const { getHooksEnabledPreference } = await import("../init/preferences.js");
|
|
127
94
|
hooksEnabled = getHooksEnabledPreference(phrenPath);
|
|
128
95
|
}
|
|
129
96
|
catch (err) {
|
|
130
|
-
|
|
131
|
-
process.stderr.write(`[phren] healthCheck hooksEnabled: ${errorMessage(err)}\n`);
|
|
97
|
+
logger.debug("healthCheck hooksEnabled", errorMessage(err));
|
|
132
98
|
}
|
|
133
99
|
let mcpEnabled = false;
|
|
134
100
|
try {
|
|
135
|
-
const { getMcpEnabledPreference } = await import("
|
|
101
|
+
const { getMcpEnabledPreference } = await import("../init/preferences.js");
|
|
136
102
|
mcpEnabled = getMcpEnabledPreference(phrenPath);
|
|
137
103
|
}
|
|
138
104
|
catch (err) {
|
|
139
|
-
|
|
140
|
-
process.stderr.write(`[phren] healthCheck mcpEnabled: ${errorMessage(err)}\n`);
|
|
105
|
+
logger.debug("healthCheck mcpEnabled", errorMessage(err));
|
|
141
106
|
}
|
|
142
107
|
// Profile/machine info
|
|
143
108
|
const machineName = (() => {
|
|
@@ -145,8 +110,7 @@ export function register(server, ctx) {
|
|
|
145
110
|
return getMachineName();
|
|
146
111
|
}
|
|
147
112
|
catch (err) {
|
|
148
|
-
|
|
149
|
-
process.stderr.write(`[phren] healthCheck machineName: ${errorMessage(err)}\n`);
|
|
113
|
+
logger.debug("healthCheck machineName", errorMessage(err));
|
|
150
114
|
}
|
|
151
115
|
return undefined;
|
|
152
116
|
})();
|
|
@@ -155,24 +119,22 @@ export function register(server, ctx) {
|
|
|
155
119
|
let proactivity = "high";
|
|
156
120
|
let taskMode = "auto";
|
|
157
121
|
try {
|
|
158
|
-
const { getWorkflowPolicy } = await import("
|
|
122
|
+
const { getWorkflowPolicy } = await import("../governance/policy.js");
|
|
159
123
|
const workflowPolicy = getWorkflowPolicy(phrenPath);
|
|
160
124
|
taskMode = workflowPolicy.taskMode;
|
|
161
125
|
}
|
|
162
126
|
catch (err) {
|
|
163
|
-
|
|
164
|
-
process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
|
|
127
|
+
logger.debug("healthCheck taskMode", errorMessage(err));
|
|
165
128
|
}
|
|
166
129
|
let syncIntent;
|
|
167
130
|
try {
|
|
168
|
-
const { readInstallPreferences } = await import("
|
|
131
|
+
const { readInstallPreferences } = await import("../init/preferences.js");
|
|
169
132
|
const prefs = readInstallPreferences(phrenPath);
|
|
170
133
|
proactivity = prefs.proactivity || "high";
|
|
171
134
|
syncIntent = prefs.syncIntent;
|
|
172
135
|
}
|
|
173
136
|
catch (err) {
|
|
174
|
-
|
|
175
|
-
process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
|
|
137
|
+
logger.debug("healthCheck proactivity", errorMessage(err));
|
|
176
138
|
}
|
|
177
139
|
// Determine sync status from intent + git remote state
|
|
178
140
|
let syncStatus = "local-only";
|
|
@@ -209,6 +171,103 @@ export function register(server, ctx) {
|
|
|
209
171
|
syncDetail = "sync was configured but no remote found";
|
|
210
172
|
}
|
|
211
173
|
}
|
|
174
|
+
let consolidation = null;
|
|
175
|
+
if (include_consolidation !== false) {
|
|
176
|
+
try {
|
|
177
|
+
const projectDirsForConsol = getProjectDirs(phrenPath, activeProfile);
|
|
178
|
+
const consolResults = [];
|
|
179
|
+
for (const dir of projectDirsForConsol) {
|
|
180
|
+
const status = getProjectConsolidationStatus(dir);
|
|
181
|
+
if (!status)
|
|
182
|
+
continue;
|
|
183
|
+
consolResults.push({ ...status, threshold: CONSOLIDATION_ENTRY_THRESHOLD });
|
|
184
|
+
}
|
|
185
|
+
consolidation = consolResults;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
logger.debug("healthCheck consolidation", errorMessage(err));
|
|
189
|
+
consolidation = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const consolSummary = consolidation && consolidation.length > 0
|
|
193
|
+
? consolidation.filter(r => r.recommended).length > 0
|
|
194
|
+
? `Consolidation: ${consolidation.filter(r => r.recommended).length} project(s) need consolidation`
|
|
195
|
+
: `Consolidation: all projects OK`
|
|
196
|
+
: null;
|
|
197
|
+
// ── Surface RuntimeHealth warnings ────────────────────────────────────
|
|
198
|
+
const warnings = [];
|
|
199
|
+
try {
|
|
200
|
+
const health = getRuntimeHealth(phrenPath);
|
|
201
|
+
// Unsynced commits
|
|
202
|
+
const unsynced = health.lastSync?.unsyncedCommits;
|
|
203
|
+
if (typeof unsynced === "number" && unsynced > 0) {
|
|
204
|
+
warnings.push(`Unsynced commits: ${unsynced} (last push: ${health.lastSync?.lastPushStatus ?? "unknown"})`);
|
|
205
|
+
}
|
|
206
|
+
// Last auto-save error
|
|
207
|
+
if (health.lastAutoSave?.status === "error") {
|
|
208
|
+
warnings.push(`Last auto-save failed: ${health.lastAutoSave.detail ?? "unknown error"}`);
|
|
209
|
+
}
|
|
210
|
+
// Last push error
|
|
211
|
+
if (health.lastSync?.lastPushStatus === "error") {
|
|
212
|
+
warnings.push(`Last push failed: ${health.lastSync.lastPushDetail ?? "unknown error"}`);
|
|
213
|
+
}
|
|
214
|
+
// Check live unsynced commit count (may differ from cached value)
|
|
215
|
+
if (syncStatus === "synced" && (!unsynced || unsynced === 0)) {
|
|
216
|
+
try {
|
|
217
|
+
const liveUnsynced = await countUnsyncedCommits(phrenPath);
|
|
218
|
+
if (liveUnsynced > 0) {
|
|
219
|
+
warnings.push(`Unsynced commits: ${liveUnsynced} (not yet pushed to remote)`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
logger.debug("healthCheck liveUnsyncedCount", errorMessage(err));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
logger.debug("healthCheck runtimeHealth", errorMessage(err));
|
|
229
|
+
}
|
|
230
|
+
// Check recent sync warnings from background sync
|
|
231
|
+
try {
|
|
232
|
+
const syncWarningsPath = runtimeFile(phrenPath, "sync-warnings.jsonl");
|
|
233
|
+
if (fs.existsSync(syncWarningsPath)) {
|
|
234
|
+
const lines = fs.readFileSync(syncWarningsPath, "utf8").trim().split("\n").filter(Boolean);
|
|
235
|
+
const recent = lines.slice(-3); // last 3 warnings
|
|
236
|
+
for (const line of recent) {
|
|
237
|
+
try {
|
|
238
|
+
const entry = JSON.parse(line);
|
|
239
|
+
if (entry.error) {
|
|
240
|
+
warnings.push(`Background sync failed (${entry.at?.slice(0, 16) ?? "unknown"}): ${entry.error}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch { /* skip malformed lines */ }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
logger.debug("healthCheck syncWarnings", errorMessage(err));
|
|
249
|
+
}
|
|
250
|
+
// Check embedding/LLM availability
|
|
251
|
+
try {
|
|
252
|
+
const { getOllamaUrl } = await import("../shared/ollama.js");
|
|
253
|
+
const ollamaUrl = getOllamaUrl();
|
|
254
|
+
const hasEmbeddingApi = !!process.env.PHREN_EMBEDDING_API_URL;
|
|
255
|
+
if (!ollamaUrl && !hasEmbeddingApi) {
|
|
256
|
+
warnings.push("Embeddings: unavailable (no Ollama or API endpoint configured)");
|
|
257
|
+
}
|
|
258
|
+
const hasLlmEndpoint = !!process.env.PHREN_LLM_ENDPOINT;
|
|
259
|
+
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
|
260
|
+
const hasOpenAiKey = !!process.env.OPENAI_API_KEY;
|
|
261
|
+
if (!hasLlmEndpoint && !hasAnthropicKey && !hasOpenAiKey) {
|
|
262
|
+
warnings.push("LLM features: unavailable (no API key configured for semantic dedup/conflict detection)");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
logger.debug("healthCheck serviceAvailability", errorMessage(err));
|
|
267
|
+
}
|
|
268
|
+
const warningsSummary = warnings.length > 0
|
|
269
|
+
? `Warnings: ${warnings.length}\n ${warnings.join("\n ")}`
|
|
270
|
+
: null;
|
|
212
271
|
const lines = [
|
|
213
272
|
`Phren v${version}`,
|
|
214
273
|
`Profile: ${activeProfile || "(default)"}`,
|
|
@@ -220,6 +279,8 @@ export function register(server, ctx) {
|
|
|
220
279
|
`Proactivity: ${proactivity}`,
|
|
221
280
|
`Task mode: ${taskMode}`,
|
|
222
281
|
`Sync: ${syncStatus}${syncStatus !== "synced" ? ` (${syncDetail})` : ""}`,
|
|
282
|
+
consolSummary,
|
|
283
|
+
warningsSummary,
|
|
223
284
|
`Path: ${phrenPath}`,
|
|
224
285
|
].filter(Boolean);
|
|
225
286
|
return mcpResponse({
|
|
@@ -237,6 +298,8 @@ export function register(server, ctx) {
|
|
|
237
298
|
taskMode,
|
|
238
299
|
syncStatus,
|
|
239
300
|
syncDetail,
|
|
301
|
+
consolidation,
|
|
302
|
+
warnings,
|
|
240
303
|
phrenPath,
|
|
241
304
|
},
|
|
242
305
|
});
|
|
@@ -251,7 +314,7 @@ export function register(server, ctx) {
|
|
|
251
314
|
.describe("Also validate data files (tasks, findings, governance). Default false."),
|
|
252
315
|
}),
|
|
253
316
|
}, async ({ check_data }) => {
|
|
254
|
-
const { runDoctor } = await import("
|
|
317
|
+
const { runDoctor } = await import("../link/doctor.js");
|
|
255
318
|
const result = await runDoctor(phrenPath, true, check_data ?? false);
|
|
256
319
|
const lines = result.checks.map((c) => `${c.ok ? "ok" : "FAIL"} ${c.name}: ${c.detail}`);
|
|
257
320
|
const failCount = result.checks.filter((c) => !c.ok).length;
|
|
@@ -302,8 +365,7 @@ export function register(server, ctx) {
|
|
|
302
365
|
return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
|
|
303
366
|
}
|
|
304
367
|
catch (err) {
|
|
305
|
-
|
|
306
|
-
process.stderr.write(`[phren] readErrorLines: ${errorMessage(err)}\n`);
|
|
368
|
+
logger.debug("readErrorLines", errorMessage(err));
|
|
307
369
|
return [];
|
|
308
370
|
}
|
|
309
371
|
}
|
|
@@ -360,4 +422,40 @@ export function register(server, ctx) {
|
|
|
360
422
|
data: { items: result.data },
|
|
361
423
|
});
|
|
362
424
|
});
|
|
425
|
+
// ── manage_review_item ──────────────────────────────────────────────────
|
|
426
|
+
server.registerTool("manage_review_item", {
|
|
427
|
+
title: "◆ phren · manage review item",
|
|
428
|
+
description: "Manage a review queue item: approve (removes from queue, finding stays), reject (removes from queue AND FINDINGS.md), or edit (updates text in both).",
|
|
429
|
+
inputSchema: z.object({
|
|
430
|
+
project: z.string().describe("Project name."),
|
|
431
|
+
line: z.string().max(10000).describe("The raw queue line text (as returned by get_review_queue)."),
|
|
432
|
+
action: z.enum(["approve", "reject", "edit"]).describe("Action to perform on the queue item."),
|
|
433
|
+
new_text: z.string().max(10000).optional().describe("Required when action is 'edit'."),
|
|
434
|
+
}),
|
|
435
|
+
}, async ({ project, line, action, new_text }) => {
|
|
436
|
+
if (!isValidProjectName(project)) {
|
|
437
|
+
return mcpResponse({ ok: false, error: `Invalid project name: "${project}".` });
|
|
438
|
+
}
|
|
439
|
+
if (action === "edit" && !new_text) {
|
|
440
|
+
return mcpResponse({ ok: false, error: "new_text is required when action is 'edit'." });
|
|
441
|
+
}
|
|
442
|
+
return withWriteQueue(async () => {
|
|
443
|
+
let result;
|
|
444
|
+
switch (action) {
|
|
445
|
+
case "approve":
|
|
446
|
+
result = approveQueueItem(phrenPath, project, line);
|
|
447
|
+
break;
|
|
448
|
+
case "reject":
|
|
449
|
+
result = rejectQueueItem(phrenPath, project, line);
|
|
450
|
+
break;
|
|
451
|
+
case "edit":
|
|
452
|
+
result = editQueueItem(phrenPath, project, line, new_text);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
if (!result.ok) {
|
|
456
|
+
return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
|
|
457
|
+
}
|
|
458
|
+
return mcpResponse({ ok: true, message: result.data });
|
|
459
|
+
});
|
|
460
|
+
});
|
|
363
461
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
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 { createHash } from "crypto";
|
|
5
|
-
import { isValidProjectName, errorMessage } from "
|
|
6
|
-
import { readFindings } from "
|
|
7
|
-
import { debugLog, runtimeFile, DOC_TYPES, FINDING_TAGS, isMemoryScopeVisible, normalizeMemoryScope, } from "
|
|
8
|
-
import { FINDING_LIFECYCLE_STATUSES, parseFindingLifecycle, } from "
|
|
9
|
-
import { decodeStringRow, queryRows, queryDocRows, queryEntityLinks, logEntityMiss, extractSnippet, queryDocBySourceKey, normalizeMemoryId, } from "
|
|
10
|
-
import { runCustomHooks } from "
|
|
11
|
-
import { entryScoreKey, getQualityMultiplier, getRetentionPolicy } from "
|
|
12
|
-
import { callLlm } from "
|
|
13
|
-
import { rankResults, searchKnowledgeRows, applyTrustFilter, searchFederatedStores } from "
|
|
14
|
-
import { parseSourceComment } from "
|
|
15
|
-
import { resolveActiveSessionScope } from "./
|
|
5
|
+
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
6
|
+
import { readFindings } from "../data/access.js";
|
|
7
|
+
import { debugLog, runtimeFile, DOC_TYPES, FINDING_TAGS, isMemoryScopeVisible, normalizeMemoryScope, } from "../shared.js";
|
|
8
|
+
import { FINDING_LIFECYCLE_STATUSES, parseFindingLifecycle, } from "../shared/content.js";
|
|
9
|
+
import { decodeStringRow, queryRows, queryDocRows, queryEntityLinks, logEntityMiss, extractSnippet, queryDocBySourceKey, normalizeMemoryId, } from "../shared/index.js";
|
|
10
|
+
import { runCustomHooks } from "../hooks.js";
|
|
11
|
+
import { entryScoreKey, getQualityMultiplier, getRetentionPolicy } from "../shared/governance.js";
|
|
12
|
+
import { callLlm } from "../content/dedup.js";
|
|
13
|
+
import { rankResults, searchKnowledgeRows, applyTrustFilter, searchFederatedStores } from "../shared/retrieval.js";
|
|
14
|
+
import { parseSourceComment } from "../content/citation.js";
|
|
15
|
+
import { resolveActiveSessionScope } from "./session.js";
|
|
16
|
+
import { logger } from "../logger.js";
|
|
16
17
|
/**
|
|
17
18
|
* Q30: Log zero-result queries to .runtime/search-misses.jsonl.
|
|
18
19
|
* Strips PII-like tokens (emails, UUIDs, numbers) and keeps only query terms.
|
|
@@ -35,8 +36,7 @@ export function logSearchMiss(phrenPath, query, project) {
|
|
|
35
36
|
fs.appendFileSync(missFile, entry + "\n");
|
|
36
37
|
}
|
|
37
38
|
catch (err) {
|
|
38
|
-
|
|
39
|
-
process.stderr.write(`[phren] logSearchMiss: ${errorMessage(err)}\n`);
|
|
39
|
+
logger.debug("search", `logSearchMiss: ${errorMessage(err)}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
const HISTORY_FINDING_STATUSES = new Set(["superseded", "retracted"]);
|
|
@@ -160,8 +160,7 @@ export function register(server, ctx) {
|
|
|
160
160
|
createdAt = stat.birthtime.toISOString();
|
|
161
161
|
}
|
|
162
162
|
catch (err) {
|
|
163
|
-
|
|
164
|
-
process.stderr.write(`[phren] search_knowledge statFile: ${errorMessage(err)}\n`);
|
|
163
|
+
logger.debug("search", `search_knowledge statFile: ${errorMessage(err)}`);
|
|
165
164
|
}
|
|
166
165
|
// Extract tags from content (e.g. [decision], [pitfall], [pattern])
|
|
167
166
|
const tagMatches = doc.content.match(/\[(decision|pitfall|pattern|tradeoff|architecture|bug)\]/gi);
|
|
@@ -259,7 +258,7 @@ export function register(server, ctx) {
|
|
|
259
258
|
}
|
|
260
259
|
catch (err) {
|
|
261
260
|
if (process.env.PHREN_DEBUG) {
|
|
262
|
-
|
|
261
|
+
logger.debug("search", `search_knowledge federation: ${errorMessage(err)}`);
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
264
|
}
|
|
@@ -421,8 +420,7 @@ export function register(server, ctx) {
|
|
|
421
420
|
relatedFragments = [...new Set(relatedFragments)].slice(0, 10);
|
|
422
421
|
}
|
|
423
422
|
catch (err) {
|
|
424
|
-
|
|
425
|
-
process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
|
|
423
|
+
logger.debug("search", `fragment query: ${errorMessage(err)}`);
|
|
426
424
|
}
|
|
427
425
|
const formatted = results.map((r) => {
|
|
428
426
|
const fedNote = r.federation_source ? ` [from: ${r.federation_source}]` : "";
|
|
@@ -439,8 +437,7 @@ export function register(server, ctx) {
|
|
|
439
437
|
synthCache = JSON.parse(fs.readFileSync(synthCachePath, "utf8"));
|
|
440
438
|
}
|
|
441
439
|
catch (err) {
|
|
442
|
-
|
|
443
|
-
process.stderr.write(`[phren] search_knowledge synthCacheRead: ${errorMessage(err)}\n`);
|
|
440
|
+
logger.debug("search", `search_knowledge synthCacheRead: ${errorMessage(err)}`);
|
|
444
441
|
}
|
|
445
442
|
const cached = synthCache[synthKey];
|
|
446
443
|
const SYNTH_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
@@ -464,8 +461,7 @@ export function register(server, ctx) {
|
|
|
464
461
|
fs.writeFileSync(synthCachePath, JSON.stringify(synthCache));
|
|
465
462
|
}
|
|
466
463
|
catch (err) {
|
|
467
|
-
|
|
468
|
-
process.stderr.write(`[phren] synthCache write: ${errorMessage(err)}\n`);
|
|
464
|
+
logger.debug("search", `synthCache write: ${errorMessage(err)}`);
|
|
469
465
|
}
|
|
470
466
|
}
|
|
471
467
|
}
|
|
@@ -1,22 +1,23 @@
|
|
|
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
5
|
import * as crypto from "crypto";
|
|
6
6
|
import { execFileSync } from "child_process";
|
|
7
|
-
import { debugLog, isMemoryScopeVisible, normalizeMemoryScope } from "
|
|
8
|
-
import { withFileLock } from "
|
|
9
|
-
import { isValidProjectName, errorMessage } from "
|
|
10
|
-
import { runCustomHooks } from "
|
|
11
|
-
import { readExtractedFacts } from "./
|
|
12
|
-
import { resolveFindingSessionId } from "
|
|
13
|
-
import { readTasks } from "
|
|
14
|
-
import { readFindings } from "
|
|
15
|
-
import { getProjectDirs } from "
|
|
16
|
-
import { getActiveTaskForSession } from "
|
|
17
|
-
import { listTaskCheckpoints, writeTaskCheckpoint } from "
|
|
18
|
-
import { markImpactEntriesCompletedForSession } from "
|
|
19
|
-
import { atomicWriteJson, debugError, scanSessionFiles } from "
|
|
7
|
+
import { debugLog, isMemoryScopeVisible, normalizeMemoryScope } from "../shared.js";
|
|
8
|
+
import { withFileLock } from "../shared/governance.js";
|
|
9
|
+
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
10
|
+
import { runCustomHooks } from "../hooks.js";
|
|
11
|
+
import { readExtractedFacts } from "./extract-facts.js";
|
|
12
|
+
import { resolveFindingSessionId } from "../finding/context.js";
|
|
13
|
+
import { readTasks } from "../data/tasks.js";
|
|
14
|
+
import { readFindings } from "../data/access.js";
|
|
15
|
+
import { getProjectDirs } from "../shared.js";
|
|
16
|
+
import { getActiveTaskForSession } from "../task/lifecycle.js";
|
|
17
|
+
import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints.js";
|
|
18
|
+
import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
|
|
19
|
+
import { atomicWriteJson, debugError, scanSessionFiles } from "../session/utils.js";
|
|
20
|
+
import { getRuntimeHealth } from "../governance/policy.js";
|
|
20
21
|
const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
21
22
|
function collectGitStatusSnapshot(cwd) {
|
|
22
23
|
try {
|
|
@@ -215,6 +216,10 @@ function cleanupStaleSessions(phrenPath) {
|
|
|
215
216
|
let cleaned = 0;
|
|
216
217
|
for (const { fullPath, data: state } of results) {
|
|
217
218
|
try {
|
|
219
|
+
// Only clean up sessions that have ended (have endedAt). Active sessions
|
|
220
|
+
// (no endedAt) should never be removed regardless of age.
|
|
221
|
+
if (state && !state.endedAt)
|
|
222
|
+
continue;
|
|
218
223
|
// prefer startedAt from the JSON content over mtime (reliable on noatime mounts)
|
|
219
224
|
const ageMs = state?.startedAt
|
|
220
225
|
? Date.now() - new Date(state.startedAt).getTime()
|
|
@@ -403,16 +408,15 @@ export function register(server, ctx) {
|
|
|
403
408
|
if (agentScope !== undefined && !normalizedAgentScope) {
|
|
404
409
|
return mcpResponse({ ok: false, error: `Invalid agentScope: "${agentScope}". Use lowercase letters/numbers with '-' or '_' (max 64 chars).` });
|
|
405
410
|
}
|
|
406
|
-
// Find most recent prior session for context
|
|
411
|
+
// Find most recent prior session for context.
|
|
412
|
+
// When no explicit project is provided, prefer the last ENDED session's
|
|
413
|
+
// project (completed context) over an active session from a different client.
|
|
414
|
+
const priorEnded = findMostRecentSummaryWithProject(phrenPath);
|
|
407
415
|
const priorResult = findMostRecentSession(phrenPath);
|
|
408
416
|
const prior = priorResult?.state ?? null;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const priorEnded = prior ? null : findMostRecentSummaryWithProject(phrenPath);
|
|
413
|
-
const priorSummary = prior?.summary ?? priorEnded?.summary ?? null;
|
|
414
|
-
const priorProject = prior?.project ?? priorEnded?.project;
|
|
415
|
-
const priorEndedAt = prior?.endedAt ?? priorEnded?.endedAt;
|
|
417
|
+
const priorSummary = priorEnded?.summary ?? prior?.summary ?? null;
|
|
418
|
+
const priorProject = priorEnded?.project ?? prior?.project;
|
|
419
|
+
const priorEndedAt = priorEnded?.endedAt ?? prior?.endedAt;
|
|
416
420
|
// Create new session with unique ID in its own file
|
|
417
421
|
const sessionId = crypto.randomUUID();
|
|
418
422
|
const next = {
|
|
@@ -521,10 +525,31 @@ export function register(server, ctx) {
|
|
|
521
525
|
debugError("session_start contextDiff", err);
|
|
522
526
|
}
|
|
523
527
|
}
|
|
528
|
+
// ── Surface sync/health warnings ────────────────────────────────────
|
|
529
|
+
const sessionWarnings = [];
|
|
530
|
+
try {
|
|
531
|
+
const health = getRuntimeHealth(phrenPath);
|
|
532
|
+
if (health.lastAutoSave?.status === "error") {
|
|
533
|
+
sessionWarnings.push(`Last auto-save failed: ${health.lastAutoSave.detail ?? "unknown"}`);
|
|
534
|
+
}
|
|
535
|
+
if (health.lastSync?.lastPushStatus === "error") {
|
|
536
|
+
sessionWarnings.push(`Last push failed: ${health.lastSync.lastPushDetail ?? "unknown"}`);
|
|
537
|
+
}
|
|
538
|
+
const unsynced = health.lastSync?.unsyncedCommits;
|
|
539
|
+
if (typeof unsynced === "number" && unsynced > 0) {
|
|
540
|
+
sessionWarnings.push(`${unsynced} unsynced commit${unsynced === 1 ? "" : "s"} — run 'phren doctor' or check git remote`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
debugError("session_start runtimeHealth", err);
|
|
545
|
+
}
|
|
546
|
+
if (sessionWarnings.length > 0) {
|
|
547
|
+
parts.push(`## Sync warnings\n${sessionWarnings.map(w => `- ${w}`).join("\n")}`);
|
|
548
|
+
}
|
|
524
549
|
const message = parts.length > 0
|
|
525
550
|
? `Session started (${sessionId.slice(0, 8)}).\n\n${parts.join("\n\n")}`
|
|
526
551
|
: `Session started (${sessionId.slice(0, 8)}). No prior context found.`;
|
|
527
|
-
return mcpResponse({ ok: true, message, data: { sessionId, project: activeProject, agentScope: activeScope } });
|
|
552
|
+
return mcpResponse({ ok: true, message, data: { sessionId, project: activeProject, agentScope: activeScope, warnings: sessionWarnings.length > 0 ? sessionWarnings : undefined } });
|
|
528
553
|
});
|
|
529
554
|
server.registerTool("session_end", {
|
|
530
555
|
title: "◆ phren · session end",
|
|
@@ -1,11 +1,11 @@
|
|
|
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, safeProjectPath } from "
|
|
6
|
-
import { parseSkillFrontmatter, validateSkillFrontmatter } from "
|
|
7
|
-
import { removeSkillPath, setSkillEnabledAndSync } from "
|
|
8
|
-
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "
|
|
5
|
+
import { isValidProjectName, safeProjectPath } from "../utils.js";
|
|
6
|
+
import { parseSkillFrontmatter, validateSkillFrontmatter } from "../link/skills.js";
|
|
7
|
+
import { removeSkillPath, setSkillEnabledAndSync } from "../skill/files.js";
|
|
8
|
+
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "../skill/registry.js";
|
|
9
9
|
export function register(server, ctx) {
|
|
10
10
|
const { phrenPath, profile, withWriteQueue, updateFileInIndex } = ctx;
|
|
11
11
|
// ── list_skills ──────────────────────────────────────────────────────────
|
|
@@ -165,36 +165,34 @@ export function register(server, ctx) {
|
|
|
165
165
|
return mcpResponse({ ok: true, message: `Removed skill "${name}" (${removedPath}).`, data: { path: removedPath } });
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
data: { name: result.name, project, enabled: action.enabled },
|
|
196
|
-
});
|
|
168
|
+
// ── toggle_skill ─────────────────────────────────────────────────────
|
|
169
|
+
server.registerTool("toggle_skill", {
|
|
170
|
+
title: "◆ phren · toggle skill",
|
|
171
|
+
description: "Enable or disable a skill without deleting its file.",
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
name: z.string().describe("Skill name (without .md)."),
|
|
174
|
+
enabled: z.boolean().describe("true to enable, false to disable."),
|
|
175
|
+
project: z.string().describe("Project scope or 'global'."),
|
|
176
|
+
}),
|
|
177
|
+
}, async ({ name, enabled, project }) => {
|
|
178
|
+
if (project.toLowerCase() !== "global" && !isValidProjectName(project)) {
|
|
179
|
+
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
180
|
+
}
|
|
181
|
+
const result = findSkill(phrenPath, profile, project, name);
|
|
182
|
+
if (!result) {
|
|
183
|
+
return mcpResponse({ ok: false, error: `Skill "${name}" not found in "${project}".` });
|
|
184
|
+
}
|
|
185
|
+
if ("error" in result) {
|
|
186
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
187
|
+
}
|
|
188
|
+
const verb = enabled ? "Enable" : "Disable";
|
|
189
|
+
return withWriteQueue(async () => {
|
|
190
|
+
setSkillEnabledAndSync(phrenPath, project, result.name, enabled);
|
|
191
|
+
return mcpResponse({
|
|
192
|
+
ok: true,
|
|
193
|
+
message: `${verb}d skill "${result.name}" in ${project}.`,
|
|
194
|
+
data: { name: result.name, project, enabled },
|
|
197
195
|
});
|
|
198
196
|
});
|
|
199
|
-
}
|
|
197
|
+
});
|
|
200
198
|
}
|