@phren/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { runtimeDir } from "./shared.js";
|
|
4
|
+
// In-memory buffers keyed by phrenPath to batch disk writes
|
|
5
|
+
// Keeping per-path buffers avoids silently losing events when the active path changes.
|
|
6
|
+
const buffers = new Map();
|
|
7
|
+
const pendingCounts = new Map();
|
|
8
|
+
const FLUSH_THRESHOLD = 10;
|
|
9
|
+
function telemetryPath(phrenPath) {
|
|
10
|
+
return path.join(runtimeDir(phrenPath), "telemetry.json");
|
|
11
|
+
}
|
|
12
|
+
function loadFromDisk(phrenPath) {
|
|
13
|
+
const file = telemetryPath(phrenPath);
|
|
14
|
+
const defaults = {
|
|
15
|
+
config: { enabled: false },
|
|
16
|
+
stats: { toolCalls: {}, cliCommands: {}, errors: 0, sessions: 0, lastActive: "" },
|
|
17
|
+
};
|
|
18
|
+
if (!fs.existsSync(file))
|
|
19
|
+
return defaults;
|
|
20
|
+
try {
|
|
21
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
22
|
+
return {
|
|
23
|
+
config: { ...defaults.config, ...raw.config },
|
|
24
|
+
stats: { ...defaults.stats, ...raw.stats },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
29
|
+
process.stderr.write(`[phren] telemetry loadFromDisk: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
30
|
+
return defaults;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadTelemetry(phrenPath) {
|
|
34
|
+
const cached = buffers.get(phrenPath);
|
|
35
|
+
if (cached)
|
|
36
|
+
return cached;
|
|
37
|
+
const data = loadFromDisk(phrenPath);
|
|
38
|
+
buffers.set(phrenPath, data);
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
function saveTelemetry(phrenPath, data) {
|
|
42
|
+
buffers.set(phrenPath, data);
|
|
43
|
+
const pending = (pendingCounts.get(phrenPath) ?? 0) + 1;
|
|
44
|
+
pendingCounts.set(phrenPath, pending);
|
|
45
|
+
if (pending >= FLUSH_THRESHOLD) {
|
|
46
|
+
flushTelemetryForPath(phrenPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function flushTelemetryForPath(phrenPath) {
|
|
50
|
+
const data = buffers.get(phrenPath);
|
|
51
|
+
const pending = pendingCounts.get(phrenPath) ?? 0;
|
|
52
|
+
if (!data || pending === 0)
|
|
53
|
+
return;
|
|
54
|
+
const file = telemetryPath(phrenPath);
|
|
55
|
+
try {
|
|
56
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
61
|
+
process.stderr.write(`[phren] telemetry flush: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
62
|
+
}
|
|
63
|
+
pendingCounts.set(phrenPath, 0);
|
|
64
|
+
}
|
|
65
|
+
export function flushTelemetry() {
|
|
66
|
+
for (const phrenPath of buffers.keys()) {
|
|
67
|
+
flushTelemetryForPath(phrenPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Register flush on process exit
|
|
71
|
+
let exitHookRegistered = false;
|
|
72
|
+
function ensureExitHook() {
|
|
73
|
+
if (exitHookRegistered)
|
|
74
|
+
return;
|
|
75
|
+
exitHookRegistered = true;
|
|
76
|
+
process.on("exit", () => flushTelemetry());
|
|
77
|
+
}
|
|
78
|
+
export function isTelemetryEnabled(phrenPath) {
|
|
79
|
+
return loadTelemetry(phrenPath).config.enabled;
|
|
80
|
+
}
|
|
81
|
+
export function setTelemetryEnabled(phrenPath, enabled) {
|
|
82
|
+
const data = loadTelemetry(phrenPath);
|
|
83
|
+
data.config.enabled = enabled;
|
|
84
|
+
if (enabled && !data.config.enabledAt) {
|
|
85
|
+
data.config.enabledAt = new Date().toISOString();
|
|
86
|
+
}
|
|
87
|
+
// Config changes flush immediately
|
|
88
|
+
buffers.set(phrenPath, data);
|
|
89
|
+
pendingCounts.set(phrenPath, 1);
|
|
90
|
+
flushTelemetryForPath(phrenPath);
|
|
91
|
+
}
|
|
92
|
+
export function trackToolCall(phrenPath, toolName) {
|
|
93
|
+
const data = loadTelemetry(phrenPath);
|
|
94
|
+
if (!data.config.enabled)
|
|
95
|
+
return;
|
|
96
|
+
ensureExitHook();
|
|
97
|
+
data.stats.toolCalls[toolName] = (data.stats.toolCalls[toolName] || 0) + 1;
|
|
98
|
+
data.stats.lastActive = new Date().toISOString();
|
|
99
|
+
saveTelemetry(phrenPath, data);
|
|
100
|
+
}
|
|
101
|
+
export function trackCliCommand(phrenPath, command) {
|
|
102
|
+
const data = loadTelemetry(phrenPath);
|
|
103
|
+
if (!data.config.enabled)
|
|
104
|
+
return;
|
|
105
|
+
ensureExitHook();
|
|
106
|
+
data.stats.cliCommands[command] = (data.stats.cliCommands[command] || 0) + 1;
|
|
107
|
+
data.stats.lastActive = new Date().toISOString();
|
|
108
|
+
saveTelemetry(phrenPath, data);
|
|
109
|
+
}
|
|
110
|
+
export function trackError(phrenPath) {
|
|
111
|
+
const data = loadTelemetry(phrenPath);
|
|
112
|
+
if (!data.config.enabled)
|
|
113
|
+
return;
|
|
114
|
+
ensureExitHook();
|
|
115
|
+
data.stats.errors += 1;
|
|
116
|
+
saveTelemetry(phrenPath, data);
|
|
117
|
+
}
|
|
118
|
+
export function trackSession(phrenPath) {
|
|
119
|
+
const data = loadTelemetry(phrenPath);
|
|
120
|
+
if (!data.config.enabled)
|
|
121
|
+
return;
|
|
122
|
+
ensureExitHook();
|
|
123
|
+
data.stats.sessions += 1;
|
|
124
|
+
data.stats.lastActive = new Date().toISOString();
|
|
125
|
+
saveTelemetry(phrenPath, data);
|
|
126
|
+
}
|
|
127
|
+
export function getTelemetrySummary(phrenPath) {
|
|
128
|
+
const data = loadTelemetry(phrenPath);
|
|
129
|
+
if (!data.config.enabled)
|
|
130
|
+
return "Telemetry: disabled (opt in with 'phren config telemetry on')";
|
|
131
|
+
const topTools = Object.entries(data.stats.toolCalls)
|
|
132
|
+
.sort(([, a], [, b]) => b - a)
|
|
133
|
+
.slice(0, 5)
|
|
134
|
+
.map(([name, count]) => ` ${name}: ${count}`)
|
|
135
|
+
.join("\n");
|
|
136
|
+
const topCli = Object.entries(data.stats.cliCommands)
|
|
137
|
+
.sort(([, a], [, b]) => b - a)
|
|
138
|
+
.slice(0, 5)
|
|
139
|
+
.map(([name, count]) => ` ${name}: ${count}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
const lines = [
|
|
142
|
+
`Telemetry: enabled (since ${data.config.enabledAt || "unknown"})`,
|
|
143
|
+
`Sessions: ${data.stats.sessions}`,
|
|
144
|
+
`Errors: ${data.stats.errors}`,
|
|
145
|
+
`Last active: ${data.stats.lastActive || "never"}`,
|
|
146
|
+
];
|
|
147
|
+
if (topTools)
|
|
148
|
+
lines.push("Top tools:", topTools);
|
|
149
|
+
if (topCli)
|
|
150
|
+
lines.push("Top CLI commands:", topCli);
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
export function resetTelemetry(phrenPath) {
|
|
154
|
+
const data = loadTelemetry(phrenPath);
|
|
155
|
+
data.stats = { toolCalls: {}, cliCommands: {}, errors: 0, sessions: 0, lastActive: "" };
|
|
156
|
+
// Reset flushes immediately
|
|
157
|
+
buffers.set(phrenPath, data);
|
|
158
|
+
pendingCounts.set(phrenPath, 1);
|
|
159
|
+
flushTelemetryForPath(phrenPath);
|
|
160
|
+
}
|
|
161
|
+
// Reset internal buffer state (for testing)
|
|
162
|
+
export function _resetBuffer() {
|
|
163
|
+
buffers.clear();
|
|
164
|
+
pendingCounts.clear();
|
|
165
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest globalSetup — runs once in the main process before any test workers spawn.
|
|
3
|
+
*
|
|
4
|
+
* Builds mcp/dist if it is missing or stale so every fork sees a complete,
|
|
5
|
+
* consistent dist artifact. Running the build here (rather than inside each
|
|
6
|
+
* fork via ensureCliBuilt) eliminates the race condition where one fork runs
|
|
7
|
+
* `rm -rf mcp/dist` while another fork is checking fs.existsSync(CLI_PATH).
|
|
8
|
+
*
|
|
9
|
+
* `pretest` in package.json already calls `npm run build`, so in normal `npm test`
|
|
10
|
+
* runs this is a fast no-op check. It is the safety net for:
|
|
11
|
+
* - `vitest run` called directly (no pretest hook)
|
|
12
|
+
* - Watch mode re-runs where pretest does not re-fire
|
|
13
|
+
* - CI environments that skip npm lifecycle scripts
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync } from "child_process";
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const REPO_ROOT = path.resolve(__dirname, "../..");
|
|
22
|
+
const CLI_PATH = path.join(REPO_ROOT, "mcp", "dist", "index.js");
|
|
23
|
+
export async function setup() {
|
|
24
|
+
if (fs.existsSync(CLI_PATH)) {
|
|
25
|
+
// Dist already present — skip build. This is the common path when
|
|
26
|
+
// `npm test` is used (pretest already built it) or during watch mode
|
|
27
|
+
// re-runs where the artifact is still fresh.
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
process.stdout.write("[test-global-setup] mcp/dist missing — building...\n");
|
|
31
|
+
execFileSync("npm", ["run", "build"], {
|
|
32
|
+
cwd: REPO_ROOT,
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
timeout: 60_000,
|
|
35
|
+
});
|
|
36
|
+
process.stdout.write("[test-global-setup] build complete.\n");
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const CATEGORY_BY_MODULE = {
|
|
5
|
+
"mcp-search": "Search and browse",
|
|
6
|
+
"mcp-tasks": "Task management",
|
|
7
|
+
"mcp-finding": "Finding capture",
|
|
8
|
+
"mcp-memory": "Memory quality",
|
|
9
|
+
"mcp-data": "Data management",
|
|
10
|
+
"mcp-graph": "Entities and graph",
|
|
11
|
+
"mcp-session": "Session management",
|
|
12
|
+
"mcp-ops": "Operations and review",
|
|
13
|
+
"mcp-skills": "Skills management",
|
|
14
|
+
"mcp-hooks": "Hooks management",
|
|
15
|
+
"mcp-extract": "Extraction",
|
|
16
|
+
};
|
|
17
|
+
const MODULE_ORDER = Object.keys(CATEGORY_BY_MODULE);
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
function decodeStringLiteral(raw) {
|
|
20
|
+
return raw
|
|
21
|
+
.replace(/\\"/g, "\"")
|
|
22
|
+
.replace(/\\'/g, "'")
|
|
23
|
+
.replace(/\\n/g, "\n")
|
|
24
|
+
.replace(/\\\\/g, "\\");
|
|
25
|
+
}
|
|
26
|
+
function sourceDir() {
|
|
27
|
+
return __dirname;
|
|
28
|
+
}
|
|
29
|
+
function parseModuleTools(moduleName, source) {
|
|
30
|
+
const tools = [];
|
|
31
|
+
const pattern = /registerTool\(\s*"([^"]+)"\s*,\s*\{([\s\S]*?)\}\s*,/g;
|
|
32
|
+
for (const match of source.matchAll(pattern)) {
|
|
33
|
+
const [, name, configBlock] = match;
|
|
34
|
+
const descMatch = configBlock.match(/description:\s*"((?:[^"\\]|\\.)*)"/s);
|
|
35
|
+
if (!descMatch)
|
|
36
|
+
continue;
|
|
37
|
+
const titleMatch = configBlock.match(/title:\s*"((?:[^"\\]|\\.)*)"/s);
|
|
38
|
+
tools.push({
|
|
39
|
+
name,
|
|
40
|
+
title: titleMatch ? decodeStringLiteral(titleMatch[1]) : undefined,
|
|
41
|
+
description: decodeStringLiteral(descMatch[1]).replace(/\s+/g, " ").trim(),
|
|
42
|
+
module: moduleName,
|
|
43
|
+
category: CATEGORY_BY_MODULE[moduleName] || "Other",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Handle loop-registered tools: { tool: "name", ... } patterns followed by registerTool(action.tool, ...)
|
|
47
|
+
const loopPattern = /for\s*\(const\s+(\w+)\s+of\s+\[([\s\S]*?)\]\s*(?:as\s+const\s*)?\)\s*\{\s*server\.registerTool\(\s*\1\.tool\s*,\s*\{([\s\S]*?)\}\s*,/g;
|
|
48
|
+
for (const loopMatch of source.matchAll(loopPattern)) {
|
|
49
|
+
const [, , itemsBlock, configBlock] = loopMatch;
|
|
50
|
+
const itemPattern = /\{\s*tool:\s*"([^"]+)"[^}]*verb:\s*"([^"]+)"/g;
|
|
51
|
+
for (const itemMatch of itemsBlock.matchAll(itemPattern)) {
|
|
52
|
+
const [, toolName, verb] = itemMatch;
|
|
53
|
+
const descMatch = configBlock.match(/description:\s*`\$\{[\w.]+\}\s*(.*?)`/s);
|
|
54
|
+
const desc = descMatch ? `${verb} ${descMatch[1]}`.trim() : `${verb} operation`;
|
|
55
|
+
tools.push({
|
|
56
|
+
name: toolName,
|
|
57
|
+
description: desc,
|
|
58
|
+
module: moduleName,
|
|
59
|
+
category: CATEGORY_BY_MODULE[moduleName] || "Other",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return tools;
|
|
64
|
+
}
|
|
65
|
+
export function getRegisteredTools() {
|
|
66
|
+
const dir = sourceDir();
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const moduleName of MODULE_ORDER) {
|
|
69
|
+
const tsPath = path.join(dir, `${moduleName}.ts`);
|
|
70
|
+
const jsPath = path.join(dir, `${moduleName}.js`);
|
|
71
|
+
const sourcePath = fs.existsSync(tsPath) ? tsPath : jsPath;
|
|
72
|
+
if (!fs.existsSync(sourcePath))
|
|
73
|
+
continue;
|
|
74
|
+
entries.push(...parseModuleTools(moduleName, fs.readFileSync(sourcePath, "utf8")));
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
export function getToolCount() {
|
|
79
|
+
return getRegisteredTools().length;
|
|
80
|
+
}
|
|
81
|
+
export function getToolsByCategory() {
|
|
82
|
+
const grouped = new Map();
|
|
83
|
+
for (const tool of getRegisteredTools()) {
|
|
84
|
+
const items = grouped.get(tool.category) || [];
|
|
85
|
+
items.push(tool);
|
|
86
|
+
grouped.set(tool.category, items);
|
|
87
|
+
}
|
|
88
|
+
return MODULE_ORDER
|
|
89
|
+
.map((moduleName) => CATEGORY_BY_MODULE[moduleName])
|
|
90
|
+
.filter((category, index, all) => all.indexOf(category) === index)
|
|
91
|
+
.map((category) => ({
|
|
92
|
+
category,
|
|
93
|
+
tools: (grouped.get(category) || []).sort((a, b) => a.name.localeCompare(b.name)),
|
|
94
|
+
}))
|
|
95
|
+
.filter((entry) => entry.tools.length > 0);
|
|
96
|
+
}
|
|
97
|
+
export function renderToolCatalogMarkdown() {
|
|
98
|
+
return getToolsByCategory()
|
|
99
|
+
.map(({ category, tools }) => {
|
|
100
|
+
const lines = tools.map((tool) => `- \`${tool.name}\`: ${tool.description}`);
|
|
101
|
+
return `**${category}:**\n${lines.join("\n")}`;
|
|
102
|
+
})
|
|
103
|
+
.join("\n\n");
|
|
104
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { errorMessage } from "./utils.js";
|
|
6
|
+
import { PACKAGE_NAME, PACKAGE_SPEC } from "./package-metadata.js";
|
|
7
|
+
function shellCommand(bin) {
|
|
8
|
+
return process.platform === "win32" ? `${bin}.cmd` : bin;
|
|
9
|
+
}
|
|
10
|
+
function packageRootFromRuntime() {
|
|
11
|
+
const current = fileURLToPath(import.meta.url);
|
|
12
|
+
return path.resolve(path.dirname(current), "..", "..");
|
|
13
|
+
}
|
|
14
|
+
function run(cmd, args, cwd) {
|
|
15
|
+
return execFileSync(cmd, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
shell: process.platform === "win32" && cmd.endsWith(".cmd"),
|
|
20
|
+
timeout: 180_000,
|
|
21
|
+
}).trim();
|
|
22
|
+
}
|
|
23
|
+
function cleanupStarterRefreshArtifacts(phrenPath) {
|
|
24
|
+
const runtimeRoot = path.join(phrenPath, ".runtime", "starter-updates");
|
|
25
|
+
if (!fs.existsSync(runtimeRoot))
|
|
26
|
+
return 0;
|
|
27
|
+
let removed = 0;
|
|
28
|
+
for (const entry of fs.readdirSync(runtimeRoot, { recursive: true })) {
|
|
29
|
+
const fullPath = path.join(runtimeRoot, String(entry));
|
|
30
|
+
if (!fs.existsSync(fullPath))
|
|
31
|
+
continue;
|
|
32
|
+
const stat = fs.statSync(fullPath);
|
|
33
|
+
if (!stat.isFile())
|
|
34
|
+
continue;
|
|
35
|
+
if (fullPath.endsWith(".new") || fullPath.endsWith(".current")) {
|
|
36
|
+
fs.unlinkSync(fullPath);
|
|
37
|
+
removed++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return removed;
|
|
41
|
+
}
|
|
42
|
+
function maybeRefreshStarter(root, builtEntry, refreshStarter) {
|
|
43
|
+
if (!refreshStarter) {
|
|
44
|
+
return " Run `phren update --refresh-starter` to refresh global starter assets.";
|
|
45
|
+
}
|
|
46
|
+
run(process.execPath, [builtEntry, "init", "--apply-starter-update", "-y"], root);
|
|
47
|
+
const cleaned = cleanupStarterRefreshArtifacts(root);
|
|
48
|
+
return cleaned > 0
|
|
49
|
+
? ` Refreshed starter assets and cleaned ${cleaned} staged starter artifact(s).`
|
|
50
|
+
: " Refreshed starter assets.";
|
|
51
|
+
}
|
|
52
|
+
export async function runPhrenUpdate(opts = {}) {
|
|
53
|
+
const root = packageRootFromRuntime();
|
|
54
|
+
const hasGit = fs.existsSync(path.join(root, ".git"));
|
|
55
|
+
const builtEntry = path.join(root, "mcp", "dist", "index.js");
|
|
56
|
+
if (hasGit) {
|
|
57
|
+
try {
|
|
58
|
+
// Warn if working tree is dirty (autostash handles it, but good to know)
|
|
59
|
+
try {
|
|
60
|
+
const status = run("git", ["status", "--porcelain"], root);
|
|
61
|
+
if (status) {
|
|
62
|
+
process.stderr.write(`Note: uncommitted changes detected, autostash will preserve them.\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
67
|
+
process.stderr.write(`[phren] runPhrenUpdate gitStatus: ${errorMessage(err)}\n`);
|
|
68
|
+
}
|
|
69
|
+
const pull = run("git", ["pull", "--rebase", "--autostash"], root);
|
|
70
|
+
run(shellCommand("npm"), ["install"], root);
|
|
71
|
+
try {
|
|
72
|
+
run(shellCommand("npm"), ["run", "build"], root);
|
|
73
|
+
run(process.execPath, [builtEntry, "--health"], root);
|
|
74
|
+
const starterMessage = maybeRefreshStarter(root, builtEntry, Boolean(opts.refreshStarter));
|
|
75
|
+
return { ok: true, message: `Updated local phren repo at ${root}${pull ? ` (${pull})` : ""}.${starterMessage} Rebuilt and verified CLI health.` };
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const detail = errorMessage(err);
|
|
79
|
+
return { ok: false, message: `Local repo updated but rebuild/health check failed: ${detail}` };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const detail = errorMessage(err);
|
|
84
|
+
return { ok: false, message: `Local repo update failed: ${detail}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
run(shellCommand("npm"), ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
89
|
+
run(shellCommand("npm"), ["list", "-g", PACKAGE_NAME, "--depth=0"]);
|
|
90
|
+
const starterMessage = maybeRefreshStarter(root, builtEntry, Boolean(opts.refreshStarter));
|
|
91
|
+
return { ok: true, message: `Updated phren via npm global install (@latest) and verified the package is installed.${starterMessage}` };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const detail = errorMessage(err);
|
|
95
|
+
return { ok: false, message: `Global update failed: ${detail}. Try manually: npm install -g ${PACKAGE_SPEC}` };
|
|
96
|
+
}
|
|
97
|
+
}
|