@phren/cli 0.0.36 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/cli-hooks-stop.js +28 -0
- package/mcp/dist/content/learning.js +2 -2
- package/mcp/dist/governance/locks.js +5 -34
- package/mcp/dist/governance/policy.js +2 -2
- package/mcp/dist/init/init-configure.js +338 -0
- package/mcp/dist/init/init-hooks-mode.js +57 -0
- package/mcp/dist/init/init-mcp-mode.js +80 -0
- package/mcp/dist/init/init-uninstall.js +493 -0
- package/mcp/dist/init/init-walkthrough.js +524 -0
- package/mcp/dist/init/init.js +18 -1447
- package/mcp/dist/init/setup.js +15 -5
- package/mcp/dist/init-uninstall.js +11 -2
- package/mcp/dist/phren-paths.js +20 -3
- package/mcp/dist/shared/index.js +8 -0
- package/mcp/dist/task/lifecycle.js +1 -1
- package/package.json +2 -1
|
@@ -434,6 +434,34 @@ export async function handleHookStop() {
|
|
|
434
434
|
}
|
|
435
435
|
return;
|
|
436
436
|
}
|
|
437
|
+
// Check if HEAD has an upstream tracking branch before attempting sync.
|
|
438
|
+
// Detached HEAD or branches without upstream would cause silent push failures.
|
|
439
|
+
const upstream = await runBestEffortGit(["rev-parse", "--abbrev-ref", "@{upstream}"], phrenPath);
|
|
440
|
+
if (!upstream.ok || !upstream.output) {
|
|
441
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
442
|
+
const noUpstreamDetail = "commit created; no upstream tracking branch";
|
|
443
|
+
finalizeTaskSession({
|
|
444
|
+
phrenPath,
|
|
445
|
+
sessionId: taskSessionId,
|
|
446
|
+
status: "no-upstream",
|
|
447
|
+
detail: noUpstreamDetail,
|
|
448
|
+
});
|
|
449
|
+
updateRuntimeHealth(phrenPath, {
|
|
450
|
+
lastStopAt: now,
|
|
451
|
+
lastAutoSave: { at: now, status: "no-upstream", detail: noUpstreamDetail },
|
|
452
|
+
lastSync: {
|
|
453
|
+
lastPushAt: now,
|
|
454
|
+
lastPushStatus: "no-upstream",
|
|
455
|
+
lastPushDetail: noUpstreamDetail,
|
|
456
|
+
unsyncedCommits,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
appendAuditLog(phrenPath, "hook_stop", "status=no-upstream");
|
|
460
|
+
if (unsyncedCommits > 3) {
|
|
461
|
+
process.stderr.write(`phren: ${unsyncedCommits} unsynced commits — no upstream tracking branch.\n`);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
437
465
|
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
438
466
|
const scheduled = scheduleBackgroundSync(phrenPath);
|
|
439
467
|
const syncDetail = scheduled
|
|
@@ -305,7 +305,7 @@ export function addFindingToFile(phrenPath, project, learning, citationInput, op
|
|
|
305
305
|
return phrenOk(`Skipped duplicate finding for "${project}": already exists with similar wording.`);
|
|
306
306
|
}
|
|
307
307
|
const newContent = `# ${project} Findings\n\n## ${today}\n\n${preparedForNewFile.finding.bullet}\n${preparedForNewFile.finding.citationComment}\n`;
|
|
308
|
-
const tmpPath = learningsPath +
|
|
308
|
+
const tmpPath = learningsPath + `.tmp-${crypto.randomUUID()}`;
|
|
309
309
|
fs.writeFileSync(tmpPath, newContent);
|
|
310
310
|
fs.renameSync(tmpPath, learningsPath);
|
|
311
311
|
return phrenOk({
|
|
@@ -461,7 +461,7 @@ export function addFindingsToFile(phrenPath, project, learnings, opts) {
|
|
|
461
461
|
added.push(learning);
|
|
462
462
|
}
|
|
463
463
|
if (added.length > 0) {
|
|
464
|
-
const tmpPath = learningsPath +
|
|
464
|
+
const tmpPath = learningsPath + `.tmp-${crypto.randomUUID()}`;
|
|
465
465
|
fs.writeFileSync(tmpPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
466
466
|
fs.renameSync(tmpPath, learningsPath);
|
|
467
467
|
}
|
|
@@ -24,40 +24,11 @@ function acquireFileLock(lockPath) {
|
|
|
24
24
|
try {
|
|
25
25
|
const stat = fs.statSync(lockPath);
|
|
26
26
|
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (Number.isFinite(lockPid) && lockPid > 0) {
|
|
33
|
-
if (process.platform !== 'win32') {
|
|
34
|
-
try {
|
|
35
|
-
process.kill(lockPid, 0);
|
|
36
|
-
ownerDead = false;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
ownerDead = true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
try {
|
|
44
|
-
const result = require('child_process').spawnSync('tasklist', ['/FI', `PID eq ${lockPid}`, '/NH'], { encoding: 'utf8', timeout: 2000 });
|
|
45
|
-
if (result.stdout && result.stdout.includes(String(lockPid)))
|
|
46
|
-
ownerDead = false;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
ownerDead = true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
ownerDead = true; // Can't read lock file, treat as dead
|
|
56
|
-
}
|
|
57
|
-
if (ownerDead) {
|
|
58
|
-
fs.unlinkSync(lockPath);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
27
|
+
// Lock file is older than stale threshold — delete unconditionally.
|
|
28
|
+
// This handles zombie processes, crashed hooks, and any case where
|
|
29
|
+
// the owning process failed to clean up.
|
|
30
|
+
fs.unlinkSync(lockPath);
|
|
31
|
+
continue;
|
|
61
32
|
}
|
|
62
33
|
}
|
|
63
34
|
catch (statErr) {
|
|
@@ -143,7 +143,7 @@ function normalizeRuntimeHealth(data) {
|
|
|
143
143
|
normalized.lastPromptAt = data.lastPromptAt;
|
|
144
144
|
if (typeof data.lastStopAt === "string")
|
|
145
145
|
normalized.lastStopAt = data.lastStopAt;
|
|
146
|
-
if (isRecord(data.lastAutoSave) && typeof data.lastAutoSave.at === "string" && ["clean", "saved-local", "saved-pushed", "error"].includes(String(data.lastAutoSave.status))) {
|
|
146
|
+
if (isRecord(data.lastAutoSave) && typeof data.lastAutoSave.at === "string" && ["clean", "saved-local", "saved-pushed", "no-upstream", "error"].includes(String(data.lastAutoSave.status))) {
|
|
147
147
|
normalized.lastAutoSave = {
|
|
148
148
|
at: data.lastAutoSave.at,
|
|
149
149
|
status: data.lastAutoSave.status,
|
|
@@ -169,7 +169,7 @@ function normalizeRuntimeHealth(data) {
|
|
|
169
169
|
normalized.lastSync.lastSuccessfulPullAt = data.lastSync.lastSuccessfulPullAt;
|
|
170
170
|
if (typeof data.lastSync.lastPushAt === "string")
|
|
171
171
|
normalized.lastSync.lastPushAt = data.lastSync.lastPushAt;
|
|
172
|
-
if (["saved-local", "saved-pushed", "error"].includes(String(data.lastSync.lastPushStatus)))
|
|
172
|
+
if (["saved-local", "saved-pushed", "no-upstream", "error"].includes(String(data.lastSync.lastPushStatus)))
|
|
173
173
|
normalized.lastSync.lastPushStatus = data.lastSync.lastPushStatus;
|
|
174
174
|
if (typeof data.lastSync.lastPushDetail === "string")
|
|
175
175
|
normalized.lastSync.lastPushDetail = data.lastSync.lastPushDetail;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP target configuration, hooks setup, and onboarding preference helpers.
|
|
3
|
+
* Extracted from init.ts to keep the orchestrator focused on flow control.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as crypto from "crypto";
|
|
8
|
+
import { getProjectOwnershipDefault } from "../project-config.js";
|
|
9
|
+
import { atomicWriteText, debugLog, readRootManifest, writeRootManifest, } from "../shared.js";
|
|
10
|
+
import { errorMessage } from "../utils.js";
|
|
11
|
+
import { configureAllHooks, installPhrenCliWrapper } from "../hooks.js";
|
|
12
|
+
import { updateWorkflowPolicy } from "../shared/governance.js";
|
|
13
|
+
import { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, } from "./config.js";
|
|
14
|
+
import { VERSION } from "./shared.js";
|
|
15
|
+
import { writeInstallPreferences, writeGovernanceInstallPreferences, } from "./preferences.js";
|
|
16
|
+
import { repairPreexistingInstall, ensureGovernanceFiles, ensureGitignoreEntry, upsertProjectEnvVar, detectProjectDir, bootstrapFromExisting, runPostInitVerify, } from "./setup.js";
|
|
17
|
+
import { log } from "./shared.js";
|
|
18
|
+
/**
|
|
19
|
+
* Configure MCP for all detected AI coding tools (Claude, VS Code, Cursor, Copilot, Codex).
|
|
20
|
+
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
21
|
+
*/
|
|
22
|
+
export function configureMcpTargets(phrenPath, opts, verb = "Configured") {
|
|
23
|
+
let claudeStatus = "no_settings";
|
|
24
|
+
try {
|
|
25
|
+
const status = configureClaude(phrenPath, { mcpEnabled: opts.mcpEnabled, hooksEnabled: opts.hooksEnabled });
|
|
26
|
+
claudeStatus = status ?? "installed";
|
|
27
|
+
if (status === "disabled" || status === "already_disabled") {
|
|
28
|
+
log(` ${verb} Claude Code hooks (MCP disabled)`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
log(` ${verb} Claude Code MCP + hooks`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
log(` Could not configure Claude Code settings (${e}), add manually`);
|
|
36
|
+
}
|
|
37
|
+
let vsStatus = "no_vscode";
|
|
38
|
+
try {
|
|
39
|
+
vsStatus = configureVSCode(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_vscode";
|
|
40
|
+
logMcpTargetStatus("VS Code", vsStatus, verb);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
debugLog(`configureVSCode failed: ${errorMessage(err)}`);
|
|
44
|
+
}
|
|
45
|
+
let cursorStatus = "no_cursor";
|
|
46
|
+
try {
|
|
47
|
+
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_cursor";
|
|
48
|
+
logMcpTargetStatus("Cursor", cursorStatus, verb);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
debugLog(`configureCursorMcp failed: ${errorMessage(err)}`);
|
|
52
|
+
}
|
|
53
|
+
let copilotStatus = "no_copilot";
|
|
54
|
+
try {
|
|
55
|
+
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_copilot";
|
|
56
|
+
logMcpTargetStatus("Copilot CLI", copilotStatus, verb);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
debugLog(`configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
60
|
+
}
|
|
61
|
+
let codexStatus = "no_codex";
|
|
62
|
+
try {
|
|
63
|
+
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_codex";
|
|
64
|
+
logMcpTargetStatus("Codex", codexStatus, verb);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
debugLog(`configureCodexMcp failed: ${errorMessage(err)}`);
|
|
68
|
+
}
|
|
69
|
+
const allStatuses = [claudeStatus, vsStatus, cursorStatus, copilotStatus, codexStatus];
|
|
70
|
+
if (allStatuses.some((s) => s === "installed" || s === "already_configured"))
|
|
71
|
+
return "installed";
|
|
72
|
+
if (allStatuses.some((s) => s === "disabled" || s === "already_disabled"))
|
|
73
|
+
return "disabled";
|
|
74
|
+
return claudeStatus;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Configure hooks if enabled, or log a disabled message.
|
|
78
|
+
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
79
|
+
*/
|
|
80
|
+
export function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
|
|
81
|
+
if (hooksEnabled) {
|
|
82
|
+
try {
|
|
83
|
+
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
84
|
+
if (hooked.length)
|
|
85
|
+
log(` ${verb} hooks: ${hooked.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
debugLog(`configureAllHooks failed: ${errorMessage(err)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
log(` Hooks are disabled by preference (run: npx phren hooks-mode on)`);
|
|
93
|
+
}
|
|
94
|
+
// Install phren CLI wrapper at ~/.local/bin/phren so the bare command works
|
|
95
|
+
const wrapperInstalled = installPhrenCliWrapper(phrenPath);
|
|
96
|
+
if (wrapperInstalled) {
|
|
97
|
+
log(` ${verb} CLI wrapper: ~/.local/bin/phren`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
log(` Note: phren CLI wrapper not installed (existing non-managed binary, or no entry script found)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function applyOnboardingPreferences(phrenPath, opts) {
|
|
104
|
+
if (opts.projectOwnershipDefault) {
|
|
105
|
+
writeInstallPreferences(phrenPath, { projectOwnershipDefault: opts.projectOwnershipDefault });
|
|
106
|
+
}
|
|
107
|
+
const runtimePatch = {};
|
|
108
|
+
if (opts.findingsProactivity)
|
|
109
|
+
runtimePatch.proactivityFindings = opts.findingsProactivity;
|
|
110
|
+
if (opts.taskProactivity)
|
|
111
|
+
runtimePatch.proactivityTask = opts.taskProactivity;
|
|
112
|
+
if (Object.keys(runtimePatch).length > 0) {
|
|
113
|
+
writeInstallPreferences(phrenPath, runtimePatch);
|
|
114
|
+
}
|
|
115
|
+
const governancePatch = {};
|
|
116
|
+
if (opts.findingsProactivity)
|
|
117
|
+
governancePatch.proactivityFindings = opts.findingsProactivity;
|
|
118
|
+
if (opts.taskProactivity)
|
|
119
|
+
governancePatch.proactivityTask = opts.taskProactivity;
|
|
120
|
+
if (Object.keys(governancePatch).length > 0) {
|
|
121
|
+
writeGovernanceInstallPreferences(phrenPath, governancePatch);
|
|
122
|
+
}
|
|
123
|
+
const workflowPatch = {};
|
|
124
|
+
if (typeof opts.lowConfidenceThreshold === "number")
|
|
125
|
+
workflowPatch.lowConfidenceThreshold = opts.lowConfidenceThreshold;
|
|
126
|
+
if (Array.isArray(opts.riskySections))
|
|
127
|
+
workflowPatch.riskySections = opts.riskySections;
|
|
128
|
+
if (opts.taskMode)
|
|
129
|
+
workflowPatch.taskMode = opts.taskMode;
|
|
130
|
+
if (opts.findingSensitivity)
|
|
131
|
+
workflowPatch.findingSensitivity = opts.findingSensitivity;
|
|
132
|
+
if (Object.keys(workflowPatch).length > 0) {
|
|
133
|
+
updateWorkflowPolicy(phrenPath, workflowPatch);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function writeWalkthroughEnvDefaults(phrenPath, opts) {
|
|
137
|
+
const envFile = path.join(phrenPath, ".env");
|
|
138
|
+
let envContent = fs.existsSync(envFile) ? fs.readFileSync(envFile, "utf8") : "# phren feature flags — generated by init\n";
|
|
139
|
+
const envFlags = [];
|
|
140
|
+
const autoCaptureChoice = opts._walkthroughAutoCapture;
|
|
141
|
+
const hasAutoCaptureFlag = /^\s*PHREN_FEATURE_AUTO_CAPTURE=.*$/m.test(envContent);
|
|
142
|
+
if (typeof autoCaptureChoice === "boolean") {
|
|
143
|
+
envFlags.push({
|
|
144
|
+
flag: `PHREN_FEATURE_AUTO_CAPTURE=${autoCaptureChoice ? "1" : "0"}`,
|
|
145
|
+
label: `Auto-capture ${autoCaptureChoice ? "enabled" : "disabled"}`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else if (autoCaptureChoice !== false && !hasAutoCaptureFlag) {
|
|
149
|
+
// Default to enabled on fresh installs and non-walkthrough init.
|
|
150
|
+
envFlags.push({ flag: "PHREN_FEATURE_AUTO_CAPTURE=1", label: "Auto-capture enabled" });
|
|
151
|
+
}
|
|
152
|
+
if (opts._walkthroughSemanticDedup)
|
|
153
|
+
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_DEDUP=1", label: "Semantic dedup" });
|
|
154
|
+
if (opts._walkthroughSemanticConflict)
|
|
155
|
+
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_CONFLICT=1", label: "Conflict detection" });
|
|
156
|
+
if (envFlags.length === 0)
|
|
157
|
+
return [];
|
|
158
|
+
let changed = false;
|
|
159
|
+
const enabledLabels = [];
|
|
160
|
+
for (const { flag, label } of envFlags) {
|
|
161
|
+
const key = flag.split("=")[0];
|
|
162
|
+
const lineRe = new RegExp(`^\\s*${key}=.*$`, "m");
|
|
163
|
+
if (lineRe.test(envContent)) {
|
|
164
|
+
const before = envContent;
|
|
165
|
+
envContent = envContent.replace(lineRe, flag);
|
|
166
|
+
if (envContent !== before) {
|
|
167
|
+
changed = true;
|
|
168
|
+
enabledLabels.push(label);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
if (!envContent.endsWith("\n"))
|
|
173
|
+
envContent += "\n";
|
|
174
|
+
envContent += `${flag}\n`;
|
|
175
|
+
changed = true;
|
|
176
|
+
enabledLabels.push(label);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (changed) {
|
|
180
|
+
const tmpPath = `${envFile}.tmp-${crypto.randomUUID()}`;
|
|
181
|
+
fs.writeFileSync(tmpPath, envContent);
|
|
182
|
+
fs.renameSync(tmpPath, envFile);
|
|
183
|
+
}
|
|
184
|
+
return enabledLabels.map((label) => `${label} (${envFile})`);
|
|
185
|
+
}
|
|
186
|
+
export function collectRepairedAssetLabels(repaired) {
|
|
187
|
+
const repairedAssets = [];
|
|
188
|
+
if (repaired.createdContextFile)
|
|
189
|
+
repairedAssets.push("~/.phren-context.md");
|
|
190
|
+
if (repaired.createdRootMemory)
|
|
191
|
+
repairedAssets.push("generated MEMORY.md");
|
|
192
|
+
repairedAssets.push(...repaired.createdGlobalAssets);
|
|
193
|
+
repairedAssets.push(...repaired.createdRuntimeAssets);
|
|
194
|
+
repairedAssets.push(...repaired.createdFeatureDefaults);
|
|
195
|
+
repairedAssets.push(...repaired.createdSkillArtifacts);
|
|
196
|
+
return repairedAssets;
|
|
197
|
+
}
|
|
198
|
+
export function applyProjectStorageBindings(repoRoot, phrenPath) {
|
|
199
|
+
const updates = [];
|
|
200
|
+
if (ensureGitignoreEntry(repoRoot, ".phren/")) {
|
|
201
|
+
updates.push(`${path.join(repoRoot, ".gitignore")} (.phren/)`);
|
|
202
|
+
}
|
|
203
|
+
if (upsertProjectEnvVar(repoRoot, "PHREN_PATH", phrenPath)) {
|
|
204
|
+
updates.push(`${path.join(repoRoot, ".env")} (PHREN_PATH=${phrenPath})`);
|
|
205
|
+
}
|
|
206
|
+
return updates;
|
|
207
|
+
}
|
|
208
|
+
export async function warmSemanticSearch(phrenPath, profile) {
|
|
209
|
+
const { checkOllamaAvailable, checkModelAvailable, getOllamaUrl, getEmbeddingModel } = await import("../shared/ollama.js");
|
|
210
|
+
const ollamaUrl = getOllamaUrl();
|
|
211
|
+
if (!ollamaUrl)
|
|
212
|
+
return "Semantic search: disabled.";
|
|
213
|
+
const model = getEmbeddingModel();
|
|
214
|
+
if (!await checkOllamaAvailable()) {
|
|
215
|
+
return `Semantic search not warmed: Ollama offline at ${ollamaUrl}.`;
|
|
216
|
+
}
|
|
217
|
+
if (!await checkModelAvailable()) {
|
|
218
|
+
return `Semantic search not warmed: model ${model} is not pulled yet.`;
|
|
219
|
+
}
|
|
220
|
+
const { buildIndex, listIndexedDocumentPaths } = await import("../shared/index.js");
|
|
221
|
+
const { getEmbeddingCache, formatEmbeddingCoverage } = await import("../shared/embedding-cache.js");
|
|
222
|
+
const { backgroundEmbedMissingDocs } = await import("../startup-embedding.js");
|
|
223
|
+
const { getPersistentVectorIndex } = await import("../shared/vector-index.js");
|
|
224
|
+
const db = await buildIndex(phrenPath, profile);
|
|
225
|
+
try {
|
|
226
|
+
const cache = getEmbeddingCache(phrenPath);
|
|
227
|
+
await cache.load().catch(() => { });
|
|
228
|
+
const allPaths = listIndexedDocumentPaths(phrenPath, profile);
|
|
229
|
+
const before = cache.coverage(allPaths);
|
|
230
|
+
if (before.missing > 0) {
|
|
231
|
+
await backgroundEmbedMissingDocs(db, cache);
|
|
232
|
+
}
|
|
233
|
+
await cache.load().catch(() => { });
|
|
234
|
+
const after = cache.coverage(allPaths);
|
|
235
|
+
if (cache.size() > 0) {
|
|
236
|
+
getPersistentVectorIndex(phrenPath).ensure(cache.getAllEntries());
|
|
237
|
+
}
|
|
238
|
+
if (after.total === 0) {
|
|
239
|
+
return `Semantic search ready (${model}), but there are no indexed docs yet.`;
|
|
240
|
+
}
|
|
241
|
+
const embeddedNow = Math.max(0, after.embedded - before.embedded);
|
|
242
|
+
const prefix = after.state === "warm" ? "Semantic search warmed" : "Semantic search warming";
|
|
243
|
+
const delta = embeddedNow > 0 ? `; embedded ${embeddedNow} new docs during init` : "";
|
|
244
|
+
return `${prefix}: ${model}, ${formatEmbeddingCoverage(after)}${delta}.`;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
try {
|
|
248
|
+
db.close();
|
|
249
|
+
}
|
|
250
|
+
catch { /* ignore close errors in init */ }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export async function runProjectLocalInit(opts = {}) {
|
|
254
|
+
const detectedRoot = detectProjectDir(process.cwd(), path.join(process.cwd(), ".phren")) || process.cwd();
|
|
255
|
+
const hasWorkspaceMarker = fs.existsSync(path.join(detectedRoot, ".git")) ||
|
|
256
|
+
fs.existsSync(path.join(detectedRoot, "CLAUDE.md")) ||
|
|
257
|
+
fs.existsSync(path.join(detectedRoot, "AGENTS.md")) ||
|
|
258
|
+
fs.existsSync(path.join(detectedRoot, ".claude", "CLAUDE.md"));
|
|
259
|
+
if (!hasWorkspaceMarker) {
|
|
260
|
+
throw new Error("project-local mode must be run inside a repo or project root");
|
|
261
|
+
}
|
|
262
|
+
const workspaceRoot = path.resolve(detectedRoot);
|
|
263
|
+
const phrenPath = path.join(workspaceRoot, ".phren");
|
|
264
|
+
const existingManifest = readRootManifest(phrenPath);
|
|
265
|
+
if (existingManifest && existingManifest.installMode !== "project-local") {
|
|
266
|
+
throw new Error(`Refusing to reuse non-local phren root at ${phrenPath}`);
|
|
267
|
+
}
|
|
268
|
+
const ownershipDefault = opts.projectOwnershipDefault
|
|
269
|
+
?? (existingManifest ? getProjectOwnershipDefault(phrenPath) : "detached");
|
|
270
|
+
if (!existingManifest && !opts.projectOwnershipDefault) {
|
|
271
|
+
opts.projectOwnershipDefault = ownershipDefault;
|
|
272
|
+
}
|
|
273
|
+
const mcpEnabled = opts.mcp ? opts.mcp === "on" : true;
|
|
274
|
+
const projectName = path.basename(workspaceRoot).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
275
|
+
if (opts.dryRun) {
|
|
276
|
+
log("\nInit dry run. No files will be written.\n");
|
|
277
|
+
log(`Mode: project-local`);
|
|
278
|
+
log(`Workspace root: ${workspaceRoot}`);
|
|
279
|
+
log(`Phren root: ${phrenPath}`);
|
|
280
|
+
log(`Project: ${projectName}`);
|
|
281
|
+
log(`VS Code workspace MCP: ${mcpEnabled ? "on" : "off"}`);
|
|
282
|
+
log(`Hooks: unsupported in project-local mode`);
|
|
283
|
+
log("");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
fs.mkdirSync(phrenPath, { recursive: true });
|
|
287
|
+
writeRootManifest(phrenPath, {
|
|
288
|
+
version: 1,
|
|
289
|
+
installMode: "project-local",
|
|
290
|
+
syncMode: "workspace-git",
|
|
291
|
+
workspaceRoot,
|
|
292
|
+
primaryProject: projectName,
|
|
293
|
+
});
|
|
294
|
+
ensureGovernanceFiles(phrenPath);
|
|
295
|
+
repairPreexistingInstall(phrenPath);
|
|
296
|
+
fs.mkdirSync(path.join(phrenPath, "global", "skills"), { recursive: true });
|
|
297
|
+
fs.mkdirSync(path.join(phrenPath, ".runtime"), { recursive: true });
|
|
298
|
+
fs.mkdirSync(path.join(phrenPath, ".sessions"), { recursive: true });
|
|
299
|
+
if (!fs.existsSync(path.join(phrenPath, ".gitignore"))) {
|
|
300
|
+
atomicWriteText(path.join(phrenPath, ".gitignore"), [
|
|
301
|
+
".runtime/",
|
|
302
|
+
".sessions/",
|
|
303
|
+
"*.lock",
|
|
304
|
+
"*.tmp-*",
|
|
305
|
+
"",
|
|
306
|
+
].join("\n"));
|
|
307
|
+
}
|
|
308
|
+
if (!fs.existsSync(path.join(phrenPath, "global", "CLAUDE.md"))) {
|
|
309
|
+
atomicWriteText(path.join(phrenPath, "global", "CLAUDE.md"), "# Global Context\n\nRepo-local Phren instructions shared across this workspace.\n");
|
|
310
|
+
}
|
|
311
|
+
const created = bootstrapFromExisting(phrenPath, workspaceRoot, { ownership: ownershipDefault });
|
|
312
|
+
applyOnboardingPreferences(phrenPath, opts);
|
|
313
|
+
writeInstallPreferences(phrenPath, {
|
|
314
|
+
mcpEnabled,
|
|
315
|
+
hooksEnabled: false,
|
|
316
|
+
skillsScope: opts.skillsScope ?? "global",
|
|
317
|
+
installedVersion: VERSION,
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
const vscodeResult = configureVSCode(phrenPath, { mcpEnabled, scope: "workspace" });
|
|
321
|
+
logMcpTargetStatus("VS Code", vscodeResult, existingManifest ? "Updated" : "Configured");
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
debugLog(`configureVSCode(workspace) failed: ${errorMessage(err)}`);
|
|
325
|
+
}
|
|
326
|
+
log(`\n${existingManifest ? "Updated" : "Created"} project-local phren at ${phrenPath}`);
|
|
327
|
+
log(` Workspace root: ${workspaceRoot}`);
|
|
328
|
+
log(` Project: ${created.project}`);
|
|
329
|
+
log(` Ownership: ${created.ownership}`);
|
|
330
|
+
log(` Sync mode: workspace-git`);
|
|
331
|
+
log(` Hooks: off (unsupported in project-local mode)`);
|
|
332
|
+
log(` VS Code MCP: ${mcpEnabled ? "workspace on" : "workspace off"}`);
|
|
333
|
+
const verify = runPostInitVerify(phrenPath);
|
|
334
|
+
log(`\nVerifying setup...`);
|
|
335
|
+
for (const check of verify.checks) {
|
|
336
|
+
log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks mode toggle: enables/disables phren lifecycle hooks for Claude and other tools.
|
|
3
|
+
*/
|
|
4
|
+
import { debugLog, findPhrenPath, readRootManifest } from "../shared.js";
|
|
5
|
+
import { errorMessage } from "../utils.js";
|
|
6
|
+
import { configureAllHooks } from "../hooks.js";
|
|
7
|
+
import { configureClaude } from "./config.js";
|
|
8
|
+
import { getMcpEnabledPreference, getHooksEnabledPreference, setHooksEnabledPreference, } from "./preferences.js";
|
|
9
|
+
import { DEFAULT_PHREN_PATH, log } from "./shared.js";
|
|
10
|
+
import { parseMcpMode } from "./init.js";
|
|
11
|
+
export async function runHooksMode(modeArg) {
|
|
12
|
+
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
13
|
+
const manifest = readRootManifest(phrenPath);
|
|
14
|
+
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
15
|
+
if (!normalizedArg || normalizedArg === "status") {
|
|
16
|
+
const current = getHooksEnabledPreference(phrenPath);
|
|
17
|
+
log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
|
|
18
|
+
log(`Change mode: npx phren hooks-mode on|off`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const mode = parseMcpMode(normalizedArg);
|
|
22
|
+
if (!mode) {
|
|
23
|
+
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
24
|
+
}
|
|
25
|
+
if (manifest?.installMode === "project-local") {
|
|
26
|
+
throw new Error("hooks-mode is unsupported in project-local mode");
|
|
27
|
+
}
|
|
28
|
+
const enabled = mode === "on";
|
|
29
|
+
let claudeStatus = "no_settings";
|
|
30
|
+
try {
|
|
31
|
+
claudeStatus = configureClaude(phrenPath, {
|
|
32
|
+
mcpEnabled: getMcpEnabledPreference(phrenPath),
|
|
33
|
+
hooksEnabled: enabled,
|
|
34
|
+
}) ?? claudeStatus;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
debugLog(`hooks-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
38
|
+
}
|
|
39
|
+
if (enabled) {
|
|
40
|
+
try {
|
|
41
|
+
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
42
|
+
if (hooked.length)
|
|
43
|
+
log(`Updated hooks: ${hooked.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
debugLog(`hooks-mode: configureAllHooks failed: ${errorMessage(err)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
log("Hooks will no-op immediately via preference and Claude hooks are removed.");
|
|
51
|
+
}
|
|
52
|
+
// Persist preference only after config writes have been attempted
|
|
53
|
+
setHooksEnabledPreference(phrenPath, enabled);
|
|
54
|
+
log(`Hooks mode set to ${mode}.`);
|
|
55
|
+
log(`Claude status: ${claudeStatus}`);
|
|
56
|
+
log(`Restart your agent to apply changes.`);
|
|
57
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP mode toggle: enables/disables phren as an MCP server across all detected tools.
|
|
3
|
+
*/
|
|
4
|
+
import { debugLog, findPhrenPath, readRootManifest } from "../shared.js";
|
|
5
|
+
import { errorMessage } from "../utils.js";
|
|
6
|
+
import { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, } from "./config.js";
|
|
7
|
+
import { getMcpEnabledPreference, getHooksEnabledPreference, setMcpEnabledPreference, } from "./preferences.js";
|
|
8
|
+
import { DEFAULT_PHREN_PATH, log } from "./shared.js";
|
|
9
|
+
import { parseMcpMode } from "./init.js";
|
|
10
|
+
export async function runMcpMode(modeArg) {
|
|
11
|
+
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
12
|
+
const manifest = readRootManifest(phrenPath);
|
|
13
|
+
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
14
|
+
if (!normalizedArg || normalizedArg === "status") {
|
|
15
|
+
const current = getMcpEnabledPreference(phrenPath);
|
|
16
|
+
const hooks = getHooksEnabledPreference(phrenPath);
|
|
17
|
+
log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
|
|
18
|
+
log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
|
|
19
|
+
log(`Change mode: npx phren mcp-mode on|off`);
|
|
20
|
+
log(`Hooks toggle: npx phren hooks-mode on|off`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const mode = parseMcpMode(normalizedArg);
|
|
24
|
+
if (!mode) {
|
|
25
|
+
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
26
|
+
}
|
|
27
|
+
const enabled = mode === "on";
|
|
28
|
+
if (manifest?.installMode === "project-local") {
|
|
29
|
+
const vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled, scope: "workspace" });
|
|
30
|
+
setMcpEnabledPreference(phrenPath, enabled);
|
|
31
|
+
log(`MCP mode set to ${mode}.`);
|
|
32
|
+
log(`VS Code status: ${vscodeStatus}`);
|
|
33
|
+
log(`Project-local mode only configures workspace VS Code MCP.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let claudeStatus = "no_settings";
|
|
37
|
+
let vscodeStatus = "no_vscode";
|
|
38
|
+
let cursorStatus = "no_cursor";
|
|
39
|
+
let copilotStatus = "no_copilot";
|
|
40
|
+
let codexStatus = "no_codex";
|
|
41
|
+
try {
|
|
42
|
+
claudeStatus = configureClaude(phrenPath, { mcpEnabled: enabled }) ?? claudeStatus;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
debugLog(`mcp-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled }) ?? vscodeStatus;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
debugLog(`mcp-mode: configureVSCode failed: ${errorMessage(err)}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: enabled }) ?? cursorStatus;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
debugLog(`mcp-mode: configureCursorMcp failed: ${errorMessage(err)}`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: enabled }) ?? copilotStatus;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
debugLog(`mcp-mode: configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: enabled }) ?? codexStatus;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
debugLog(`mcp-mode: configureCodexMcp failed: ${errorMessage(err)}`);
|
|
70
|
+
}
|
|
71
|
+
// Persist preference only after config writes have been attempted
|
|
72
|
+
setMcpEnabledPreference(phrenPath, enabled);
|
|
73
|
+
log(`MCP mode set to ${mode}.`);
|
|
74
|
+
log(`Claude status: ${claudeStatus}`);
|
|
75
|
+
log(`VS Code status: ${vscodeStatus}`);
|
|
76
|
+
log(`Cursor status: ${cursorStatus}`);
|
|
77
|
+
log(`Copilot CLI status: ${copilotStatus}`);
|
|
78
|
+
log(`Codex status: ${codexStatus}`);
|
|
79
|
+
log(`Restart your agent to apply changes.`);
|
|
80
|
+
}
|