@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
package/mcp/dist/init/init.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI orchestrator for phren init, mcp-mode, hooks-mode, and uninstall.
|
|
3
|
-
* Delegates to focused helpers in init-config, init-setup,
|
|
3
|
+
* Delegates to focused helpers in init-config, init-setup, init-preferences,
|
|
4
|
+
* init-walkthrough, init-mcp-mode, init-hooks-mode, and init-uninstall.
|
|
4
5
|
*/
|
|
5
6
|
import * as fs from "fs";
|
|
6
7
|
import * as path from "path";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { getMachineName, machineFilePath, persistMachineName } from "../machine-identity.js";
|
|
11
|
-
import { atomicWriteText, debugLog, isRecord, hookConfigPath, homeDir, homePath, expandHomePath, findPhrenPath, getProjectDirs, readRootManifest, writeRootManifest, } from "../shared.js";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { getMachineName, persistMachineName } from "../machine-identity.js";
|
|
10
|
+
import { atomicWriteText, debugLog, expandHomePath, hookConfigPath, writeRootManifest, } from "../shared.js";
|
|
12
11
|
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
13
|
-
import { codexJsonCandidates, copilotMcpCandidates, cursorMcpCandidates, vscodeMcpCandidates, } from "../provider-adapters.js";
|
|
14
12
|
import { logger } from "../logger.js";
|
|
15
13
|
export { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, resetVSCodeProbeCache, patchJsonFile, } from "./config.js";
|
|
16
14
|
export { getMcpEnabledPreference, setMcpEnabledPreference, getHooksEnabledPreference, setHooksEnabledPreference, } from "./preferences.js";
|
|
17
15
|
export { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode, getProjectOwnershipDefault, } from "../project-config.js";
|
|
18
16
|
export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, } from "../proactivity.js";
|
|
19
17
|
export { ensureGovernanceFiles, repairPreexistingInstall, runPostInitVerify, getVerifyOutcomeNote, listTemplates, detectProjectDir, isProjectTracked, ensureLocalGitRepo, resolvePreferredHomeDir, inferInitScaffoldFromRepo, } from "./setup.js";
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
// Re-export from extracted modules so consumers can still import from init.js
|
|
19
|
+
export { configureMcpTargets, warmSemanticSearch, runProjectLocalInit } from "./init-configure.js";
|
|
20
|
+
export { runMcpMode } from "./init-mcp-mode.js";
|
|
21
|
+
export { runHooksMode } from "./init-hooks-mode.js";
|
|
22
|
+
export { runUninstall } from "./init-uninstall.js";
|
|
23
|
+
// Internal imports from extracted modules (used by runInit)
|
|
24
|
+
import { configureMcpTargets, configureHooksIfEnabled, applyOnboardingPreferences, writeWalkthroughEnvDefaults, collectRepairedAssetLabels, applyProjectStorageBindings, warmSemanticSearch, runProjectLocalInit, } from "./init-configure.js";
|
|
25
|
+
import { runWalkthrough, createWalkthroughPrompts, createWalkthroughStyle } from "./init-walkthrough.js";
|
|
26
|
+
import { getMcpEnabledPreference, getHooksEnabledPreference, writeInstallPreferences, readInstallPreferences, } from "./preferences.js";
|
|
27
|
+
import { ensureGovernanceFiles, repairPreexistingInstall, runPostInitVerify, applyStarterTemplateUpdates, listTemplates, applyTemplate, ensureProjectScaffold, ensureLocalGitRepo, bootstrapFromExisting, updateMachinesYaml, detectProjectDir, isProjectTracked, } from "./setup.js";
|
|
24
28
|
import { DEFAULT_PHREN_PATH, STARTER_DIR, VERSION, log, confirmPrompt } from "./shared.js";
|
|
25
29
|
import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, } from "../project-config.js";
|
|
26
|
-
import { getWorkflowPolicy
|
|
30
|
+
import { getWorkflowPolicy } from "../shared/governance.js";
|
|
27
31
|
import { addProjectToProfile } from "../profile-store.js";
|
|
28
|
-
const PHREN_NPM_PACKAGE_NAME = "@phren/cli";
|
|
29
32
|
function parseVersion(version) {
|
|
30
33
|
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/);
|
|
31
34
|
if (!match)
|
|
@@ -60,53 +63,6 @@ export function isVersionNewer(current, previous) {
|
|
|
60
63
|
return true;
|
|
61
64
|
return c.pre > p.pre;
|
|
62
65
|
}
|
|
63
|
-
function getNpmCommand() {
|
|
64
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
65
|
-
}
|
|
66
|
-
function runSyncCommand(command, args) {
|
|
67
|
-
try {
|
|
68
|
-
const result = spawnSync(command, args, {
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
-
});
|
|
72
|
-
return {
|
|
73
|
-
ok: result.status === 0,
|
|
74
|
-
status: result.status,
|
|
75
|
-
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
76
|
-
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
return {
|
|
81
|
-
ok: false,
|
|
82
|
-
status: null,
|
|
83
|
-
stdout: "",
|
|
84
|
-
stderr: errorMessage(err),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function shouldUninstallCurrentGlobalPackage() {
|
|
89
|
-
// Always attempt to remove the global package if it exists, regardless of
|
|
90
|
-
// whether the uninstaller was invoked from the global install or a local repo.
|
|
91
|
-
const npmRootResult = runSyncCommand(getNpmCommand(), ["root", "-g"]);
|
|
92
|
-
if (!npmRootResult.ok)
|
|
93
|
-
return false;
|
|
94
|
-
const npmRoot = npmRootResult.stdout.trim();
|
|
95
|
-
if (!npmRoot)
|
|
96
|
-
return false;
|
|
97
|
-
const globalPkgPath = path.join(npmRoot, PHREN_NPM_PACKAGE_NAME);
|
|
98
|
-
return fs.existsSync(globalPkgPath);
|
|
99
|
-
}
|
|
100
|
-
function uninstallCurrentGlobalPackage() {
|
|
101
|
-
const result = runSyncCommand(getNpmCommand(), ["uninstall", "-g", PHREN_NPM_PACKAGE_NAME]);
|
|
102
|
-
if (result.ok) {
|
|
103
|
-
log(` Removed global npm package (${PHREN_NPM_PACKAGE_NAME})`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const detail = result.stderr.trim() || result.stdout.trim() || (result.status === null ? "failed to start command" : `exit code ${result.status}`);
|
|
107
|
-
log(` Warning: could not remove global npm package (${PHREN_NPM_PACKAGE_NAME})`);
|
|
108
|
-
debugLog(`uninstall: global npm cleanup failed: ${detail}`);
|
|
109
|
-
}
|
|
110
66
|
export function parseMcpMode(raw) {
|
|
111
67
|
if (!raw)
|
|
112
68
|
return undefined;
|
|
@@ -118,7 +74,7 @@ export function parseMcpMode(raw) {
|
|
|
118
74
|
function normalizedBootstrapProjectName(projectPath) {
|
|
119
75
|
return path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
120
76
|
}
|
|
121
|
-
function getPendingBootstrapTarget(phrenPath, _opts) {
|
|
77
|
+
export function getPendingBootstrapTarget(phrenPath, _opts) {
|
|
122
78
|
const cwdProject = detectProjectDir(process.cwd(), phrenPath);
|
|
123
79
|
if (!cwdProject)
|
|
124
80
|
return null;
|
|
@@ -127,31 +83,6 @@ function getPendingBootstrapTarget(phrenPath, _opts) {
|
|
|
127
83
|
return null;
|
|
128
84
|
return { path: cwdProject, mode: "detected" };
|
|
129
85
|
}
|
|
130
|
-
function parseLowConfidenceThreshold(raw, fallback) {
|
|
131
|
-
if (!raw)
|
|
132
|
-
return fallback;
|
|
133
|
-
const value = Number.parseFloat(raw.trim());
|
|
134
|
-
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
135
|
-
return fallback;
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
function parseRiskySectionsAnswer(raw, fallback) {
|
|
139
|
-
if (!raw)
|
|
140
|
-
return [...fallback];
|
|
141
|
-
const aliases = {
|
|
142
|
-
review: "Review",
|
|
143
|
-
stale: "Stale",
|
|
144
|
-
conflict: "Conflicts",
|
|
145
|
-
conflicts: "Conflicts",
|
|
146
|
-
};
|
|
147
|
-
const parsed = raw
|
|
148
|
-
.split(/[,\s]+/)
|
|
149
|
-
.map((token) => aliases[token.trim().toLowerCase()])
|
|
150
|
-
.filter((section) => Boolean(section));
|
|
151
|
-
if (!parsed.length)
|
|
152
|
-
return [...fallback];
|
|
153
|
-
return Array.from(new Set(parsed));
|
|
154
|
-
}
|
|
155
86
|
function hasInstallMarkers(phrenPath) {
|
|
156
87
|
// Require at least two markers to consider this a real install.
|
|
157
88
|
// A partial clone or failed init may create one directory but not finish.
|
|
@@ -170,814 +101,6 @@ function resolveInitPhrenPath(opts) {
|
|
|
170
101
|
const raw = opts._walkthroughStoragePath || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
171
102
|
return path.resolve(expandHomePath(raw));
|
|
172
103
|
}
|
|
173
|
-
function detectRepoRootForStorage(phrenPath) {
|
|
174
|
-
return detectProjectDir(process.cwd(), phrenPath);
|
|
175
|
-
}
|
|
176
|
-
function withFallbackColors(style) {
|
|
177
|
-
return {
|
|
178
|
-
header: style?.header ?? ((text) => text),
|
|
179
|
-
success: style?.success ?? ((text) => text),
|
|
180
|
-
warning: style?.warning ?? ((text) => text),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
async function createWalkthroughStyle() {
|
|
184
|
-
try {
|
|
185
|
-
const chalkModule = await import(String("chalk"));
|
|
186
|
-
const chalkAny = chalkModule.default
|
|
187
|
-
?? chalkModule.chalk
|
|
188
|
-
?? chalkModule;
|
|
189
|
-
const chalk = chalkAny;
|
|
190
|
-
return withFallbackColors({
|
|
191
|
-
header: (text) => chalk.bold.cyan(text),
|
|
192
|
-
success: (text) => chalk.green(text),
|
|
193
|
-
warning: (text) => chalk.yellow(text),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
return withFallbackColors();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
async function createWalkthroughPrompts() {
|
|
201
|
-
try {
|
|
202
|
-
const inquirerModule = await import(String("inquirer"));
|
|
203
|
-
const maybeFns = inquirerModule;
|
|
204
|
-
if (typeof maybeFns.input === "function"
|
|
205
|
-
&& typeof maybeFns.confirm === "function"
|
|
206
|
-
&& typeof maybeFns.select === "function") {
|
|
207
|
-
return {
|
|
208
|
-
input: async (message, initialValue) => (await maybeFns.input({ message, default: initialValue })).trim(),
|
|
209
|
-
confirm: async (message, defaultValue = false) => Boolean(await maybeFns.confirm({ message, default: defaultValue })),
|
|
210
|
-
select: async (message, choices, defaultValue) => maybeFns.select({
|
|
211
|
-
message,
|
|
212
|
-
choices: choices.map((choice) => ({ value: choice.value, name: choice.name, description: choice.description })),
|
|
213
|
-
default: defaultValue,
|
|
214
|
-
}),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
const prompt = maybeFns.default?.prompt ?? maybeFns.prompt;
|
|
218
|
-
if (typeof prompt === "function") {
|
|
219
|
-
return {
|
|
220
|
-
input: async (message, initialValue) => {
|
|
221
|
-
const answer = await prompt([{ type: "input", name: "value", message, default: initialValue }]);
|
|
222
|
-
return String(answer.value ?? "").trim();
|
|
223
|
-
},
|
|
224
|
-
confirm: async (message, defaultValue = false) => {
|
|
225
|
-
const answer = await prompt([{ type: "confirm", name: "value", message, default: defaultValue }]);
|
|
226
|
-
return Boolean(answer.value);
|
|
227
|
-
},
|
|
228
|
-
select: async (message, choices, defaultValue) => {
|
|
229
|
-
const answer = await prompt([{
|
|
230
|
-
type: "list",
|
|
231
|
-
name: "value",
|
|
232
|
-
message,
|
|
233
|
-
choices: choices.map((choice) => ({ value: choice.value, name: choice.name })),
|
|
234
|
-
default: defaultValue,
|
|
235
|
-
}]);
|
|
236
|
-
return String(answer.value);
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
// fallback below
|
|
243
|
-
}
|
|
244
|
-
const readline = await import("readline");
|
|
245
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
246
|
-
const ask = (message) => new Promise((resolve) => rl.question(message, resolve));
|
|
247
|
-
process.once("exit", () => rl.close());
|
|
248
|
-
return {
|
|
249
|
-
input: async (message, initialValue) => {
|
|
250
|
-
const prompt = initialValue ? `${message} (${initialValue}): ` : `${message}: `;
|
|
251
|
-
const answer = (await ask(prompt)).trim();
|
|
252
|
-
return answer || (initialValue ?? "");
|
|
253
|
-
},
|
|
254
|
-
confirm: async (message, defaultValue = false) => {
|
|
255
|
-
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
256
|
-
const answer = (await ask(`${message} ${suffix}: `)).trim().toLowerCase();
|
|
257
|
-
if (!answer)
|
|
258
|
-
return defaultValue;
|
|
259
|
-
return answer === "y" || answer === "yes";
|
|
260
|
-
},
|
|
261
|
-
select: async (message, choices, defaultValue) => {
|
|
262
|
-
log(`${message}`);
|
|
263
|
-
for (const [index, choice] of choices.entries()) {
|
|
264
|
-
log(` ${index + 1}. ${choice.name}`);
|
|
265
|
-
}
|
|
266
|
-
const selected = (await ask(`Select [1-${choices.length}]${defaultValue ? " (Enter for default)" : ""}: `)).trim();
|
|
267
|
-
if (!selected && defaultValue)
|
|
268
|
-
return defaultValue;
|
|
269
|
-
const idx = Number.parseInt(selected, 10) - 1;
|
|
270
|
-
if (!Number.isNaN(idx) && idx >= 0 && idx < choices.length)
|
|
271
|
-
return choices[idx].value;
|
|
272
|
-
return defaultValue ?? choices[0].value;
|
|
273
|
-
},
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
// Interactive walkthrough for first-time init
|
|
277
|
-
async function runWalkthrough(phrenPath) {
|
|
278
|
-
const prompts = await createWalkthroughPrompts();
|
|
279
|
-
const style = await createWalkthroughStyle();
|
|
280
|
-
const divider = style.header("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
281
|
-
const printSection = (title) => {
|
|
282
|
-
log("");
|
|
283
|
-
log(divider);
|
|
284
|
-
log(style.header(title));
|
|
285
|
-
log(divider);
|
|
286
|
-
};
|
|
287
|
-
const printSummary = (items) => {
|
|
288
|
-
printSection("Configuration Summary");
|
|
289
|
-
for (const item of items) {
|
|
290
|
-
log(style.success(`✓ ${item}`));
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
const { renderPhrenArt } = await import("../phren-art.js");
|
|
294
|
-
log("");
|
|
295
|
-
log(renderPhrenArt(" "));
|
|
296
|
-
log("");
|
|
297
|
-
printSection("Welcome");
|
|
298
|
-
log("Let's set up persistent memory for your AI agents.");
|
|
299
|
-
log("Every option can be changed later.\n");
|
|
300
|
-
printSection("Storage Location");
|
|
301
|
-
log("Where should phren store data?");
|
|
302
|
-
const storageChoice = await prompts.select("Storage location", [
|
|
303
|
-
{
|
|
304
|
-
value: "global",
|
|
305
|
-
name: "global (~/.phren/ - default, shared across projects)",
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
value: "project",
|
|
309
|
-
name: "per-project (<repo>/.phren/ - scoped to this repo, add to .gitignore)",
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
value: "custom",
|
|
313
|
-
name: "custom path",
|
|
314
|
-
},
|
|
315
|
-
], "global");
|
|
316
|
-
let storagePath = path.resolve(homePath(".phren"));
|
|
317
|
-
let storageRepoRoot;
|
|
318
|
-
if (storageChoice === "project") {
|
|
319
|
-
const repoRoot = detectRepoRootForStorage(phrenPath);
|
|
320
|
-
if (!repoRoot) {
|
|
321
|
-
throw new Error("Per-project storage requires running init from a repository directory.");
|
|
322
|
-
}
|
|
323
|
-
storageRepoRoot = repoRoot;
|
|
324
|
-
storagePath = path.join(repoRoot, ".phren");
|
|
325
|
-
}
|
|
326
|
-
else if (storageChoice === "custom") {
|
|
327
|
-
const customInput = await prompts.input("Custom phren path", phrenPath);
|
|
328
|
-
storagePath = path.resolve(expandHomePath(customInput || phrenPath));
|
|
329
|
-
}
|
|
330
|
-
printSection("Existing Phren");
|
|
331
|
-
log("If you've already set up phren on another machine, paste the git clone URL.");
|
|
332
|
-
log("Otherwise, leave blank.");
|
|
333
|
-
const cloneAnswer = await prompts.input("Clone URL (leave blank to skip)");
|
|
334
|
-
if (cloneAnswer) {
|
|
335
|
-
const cloneConfig = {
|
|
336
|
-
storageChoice,
|
|
337
|
-
storagePath,
|
|
338
|
-
storageRepoRoot,
|
|
339
|
-
machine: getMachineName(),
|
|
340
|
-
profile: "personal",
|
|
341
|
-
mcp: "on",
|
|
342
|
-
hooks: "on",
|
|
343
|
-
projectOwnershipDefault: "phren-managed",
|
|
344
|
-
findingsProactivity: "high",
|
|
345
|
-
taskProactivity: "high",
|
|
346
|
-
lowConfidenceThreshold: 0.7,
|
|
347
|
-
riskySections: ["Stale", "Conflicts"],
|
|
348
|
-
taskMode: "auto",
|
|
349
|
-
bootstrapCurrentProject: false,
|
|
350
|
-
ollamaEnabled: false,
|
|
351
|
-
autoCaptureEnabled: false,
|
|
352
|
-
semanticDedupEnabled: false,
|
|
353
|
-
semanticConflictEnabled: false,
|
|
354
|
-
findingSensitivity: "balanced",
|
|
355
|
-
cloneUrl: cloneAnswer,
|
|
356
|
-
domain: "software",
|
|
357
|
-
};
|
|
358
|
-
printSummary([
|
|
359
|
-
`Storage: ${storageChoice} (${storagePath})`,
|
|
360
|
-
`Existing memory clone: ${cloneAnswer}`,
|
|
361
|
-
`Machine: ${cloneConfig.machine}`,
|
|
362
|
-
`Profile: ${cloneConfig.profile}`,
|
|
363
|
-
"MCP: enabled",
|
|
364
|
-
"Hooks: enabled",
|
|
365
|
-
"Project ownership default: phren-managed",
|
|
366
|
-
"Task mode: auto",
|
|
367
|
-
"Domain: software",
|
|
368
|
-
]);
|
|
369
|
-
return cloneConfig;
|
|
370
|
-
}
|
|
371
|
-
const defaultMachine = getMachineName();
|
|
372
|
-
printSection("Identity");
|
|
373
|
-
const machine = await prompts.input("Machine name", defaultMachine);
|
|
374
|
-
const profile = await prompts.input("Profile name", "personal");
|
|
375
|
-
const repoForInference = detectProjectDir(process.cwd(), storagePath);
|
|
376
|
-
const inferredScaffold = repoForInference
|
|
377
|
-
? inferInitScaffoldFromRepo(repoForInference)
|
|
378
|
-
: null;
|
|
379
|
-
const inferredDomain = inferredScaffold?.domain ?? "software";
|
|
380
|
-
printSection("Project Domain");
|
|
381
|
-
log("What kind of project is this?");
|
|
382
|
-
if (repoForInference && inferredScaffold) {
|
|
383
|
-
log(`Detected repo signals from ${repoForInference} (${inferredScaffold.reason}).`);
|
|
384
|
-
if (inferredScaffold.referenceHints.length > 0) {
|
|
385
|
-
log(`Reference hints: ${inferredScaffold.referenceHints.join(", ")}`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// Use inferred domain directly — adaptive init derives domain from repo content.
|
|
389
|
-
// Only ask if inference was weak (fell back to default "software" with no signals).
|
|
390
|
-
let domain = inferredDomain;
|
|
391
|
-
if (inferredDomain === "software" && !inferredScaffold) {
|
|
392
|
-
domain = await prompts.select("Project domain", [
|
|
393
|
-
{ value: "software", name: "software" },
|
|
394
|
-
{ value: "research", name: "research" },
|
|
395
|
-
{ value: "creative", name: "creative" },
|
|
396
|
-
{ value: "other", name: "other" },
|
|
397
|
-
], "software");
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
log(`Domain: ${inferredDomain} (inferred from project content)`);
|
|
401
|
-
}
|
|
402
|
-
printSection("Project Ownership");
|
|
403
|
-
log("Choose who owns repo-facing instruction files for projects you add.");
|
|
404
|
-
log(" phren-managed: Phren may mirror CLAUDE.md / AGENTS.md into the repo");
|
|
405
|
-
log(" detached: Phren keeps its own docs but does not write into the repo");
|
|
406
|
-
log(" repo-managed: keep the repo's existing CLAUDE/AGENTS files as canonical");
|
|
407
|
-
log(" Change later: npx phren config project-ownership <mode>");
|
|
408
|
-
const projectOwnershipDefault = await prompts.select("Default project ownership", [
|
|
409
|
-
{ value: "detached", name: "detached (default)" },
|
|
410
|
-
{ value: "phren-managed", name: "phren-managed" },
|
|
411
|
-
{ value: "repo-managed", name: "repo-managed" },
|
|
412
|
-
], "detached");
|
|
413
|
-
printSection("MCP");
|
|
414
|
-
log("MCP mode registers phren as a tool server so your AI agent can call it");
|
|
415
|
-
log("directly: search memory, manage tasks, save findings, etc.");
|
|
416
|
-
log(" Recommended for: Claude Code, Cursor, Copilot CLI, Codex");
|
|
417
|
-
log(" Alternative: hooks-only mode (read-only context injection, any agent)");
|
|
418
|
-
log(" Change later: npx phren mcp-mode on|off");
|
|
419
|
-
const mcp = (await prompts.confirm("Enable MCP?", true)) ? "on" : "off";
|
|
420
|
-
printSection("Hooks");
|
|
421
|
-
log("Hooks run shell commands at session start, prompt submit, and session end.");
|
|
422
|
-
log(" - SessionStart: git pull (keeps memory in sync across machines)");
|
|
423
|
-
log(" - UserPromptSubmit: searches phren and injects relevant context");
|
|
424
|
-
log(" - Stop: commits and pushes any new findings after each response");
|
|
425
|
-
log(" What they touch: ~/.claude/settings.json (hooks section only)");
|
|
426
|
-
log(" Change later: npx phren hooks-mode on|off");
|
|
427
|
-
const hooks = (await prompts.confirm("Enable hooks?", true)) ? "on" : "off";
|
|
428
|
-
printSection("Semantic Search (Optional)");
|
|
429
|
-
log("Phren can use a local embedding model for semantic (fuzzy) search via Ollama.");
|
|
430
|
-
log(" Best fit: paraphrase-heavy or weak-lexical queries.");
|
|
431
|
-
log(" Skip it if you mostly search by filenames, symbols, commands, or exact phrases.");
|
|
432
|
-
log(" - Model: nomic-embed-text (274 MB, one-time download)");
|
|
433
|
-
log(" - Ollama runs locally, no cloud, no cost");
|
|
434
|
-
log(" - Falls back to FTS5 keyword search if disabled or unavailable");
|
|
435
|
-
log(" Change later: set PHREN_OLLAMA_URL=off to disable");
|
|
436
|
-
let ollamaEnabled = false;
|
|
437
|
-
try {
|
|
438
|
-
const { checkOllamaStatus } = await import("../shared/ollama.js");
|
|
439
|
-
const status = await checkOllamaStatus();
|
|
440
|
-
if (status === "ready") {
|
|
441
|
-
log(" Ollama detected with nomic-embed-text ready.");
|
|
442
|
-
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery?", false);
|
|
443
|
-
}
|
|
444
|
-
else if (status === "no_model") {
|
|
445
|
-
log(" Ollama detected, but nomic-embed-text is not pulled yet.");
|
|
446
|
-
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery? (will pull nomic-embed-text)", false);
|
|
447
|
-
if (ollamaEnabled) {
|
|
448
|
-
log(" Run after init: ollama pull nomic-embed-text");
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
else if (status === "not_running") {
|
|
452
|
-
log(" Ollama not detected. Install it to enable semantic search:");
|
|
453
|
-
log(" https://ollama.com → then: ollama pull nomic-embed-text");
|
|
454
|
-
ollamaEnabled = await prompts.confirm("Enable semantic search (Ollama not installed yet)?", false);
|
|
455
|
-
if (ollamaEnabled) {
|
|
456
|
-
log(style.success(" Semantic search enabled — will activate once Ollama is running."));
|
|
457
|
-
log(" To disable: set PHREN_OLLAMA_URL=off in your shell profile");
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
catch (err) {
|
|
462
|
-
logger.debug("init", `init ollamaCheck: ${errorMessage(err)}`);
|
|
463
|
-
}
|
|
464
|
-
printSection("Auto-Capture (Optional)");
|
|
465
|
-
log("After each session, phren scans the conversation for insight-signal phrases");
|
|
466
|
-
log("(\"always\", \"never\", \"pitfall\", \"gotcha\", etc.) and saves them automatically.");
|
|
467
|
-
log(" - Runs silently in the Stop hook; captured findings go to FINDINGS.md");
|
|
468
|
-
log(" - You can review and remove any auto-captured entry at any time");
|
|
469
|
-
log(" - Can be toggled: set PHREN_FEATURE_AUTO_CAPTURE=0 to disable");
|
|
470
|
-
const autoCaptureEnabled = await prompts.confirm("Enable auto-capture?", true);
|
|
471
|
-
let findingsProactivity = "high";
|
|
472
|
-
if (autoCaptureEnabled) {
|
|
473
|
-
log(" Findings capture level controls how eager phren is to save lessons automatically.");
|
|
474
|
-
log(" Change later: npx phren config proactivity.findings <high|medium|low>");
|
|
475
|
-
findingsProactivity = await prompts.select("Findings capture level", [
|
|
476
|
-
{ value: "high", name: "high (recommended)" },
|
|
477
|
-
{ value: "medium", name: "medium" },
|
|
478
|
-
{ value: "low", name: "low" },
|
|
479
|
-
], "high");
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
findingsProactivity = "low";
|
|
483
|
-
}
|
|
484
|
-
printSection("Task Management");
|
|
485
|
-
log("Choose how phren handles tasks as you work.");
|
|
486
|
-
log(" auto (recommended): captures tasks naturally as you work, links findings to tasks");
|
|
487
|
-
log(" suggest: proposes tasks but waits for approval before writing");
|
|
488
|
-
log(" manual: tasks are fully manual — you add them yourself");
|
|
489
|
-
log(" off: never touch tasks automatically");
|
|
490
|
-
log(" Change later: npx phren config workflow set --taskMode=<mode>");
|
|
491
|
-
const taskMode = await prompts.select("Task mode", [
|
|
492
|
-
{ value: "auto", name: "auto (recommended)" },
|
|
493
|
-
{ value: "suggest", name: "suggest" },
|
|
494
|
-
{ value: "manual", name: "manual" },
|
|
495
|
-
{ value: "off", name: "off" },
|
|
496
|
-
], "auto");
|
|
497
|
-
let taskProactivity = "high";
|
|
498
|
-
if (taskMode === "auto" || taskMode === "suggest") {
|
|
499
|
-
log(" Task proactivity controls how much evidence phren needs before capturing tasks.");
|
|
500
|
-
log(" high (recommended): captures tasks as they come up naturally");
|
|
501
|
-
log(" medium: only when you explicitly mention a task");
|
|
502
|
-
log(" low: minimal auto-capture");
|
|
503
|
-
log(" Change later: npx phren config proactivity.tasks <high|medium|low>");
|
|
504
|
-
taskProactivity = await prompts.select("Task proactivity", [
|
|
505
|
-
{ value: "high", name: "high (recommended)" },
|
|
506
|
-
{ value: "medium", name: "medium" },
|
|
507
|
-
{ value: "low", name: "low" },
|
|
508
|
-
], "high");
|
|
509
|
-
}
|
|
510
|
-
printSection("Workflow Guardrails");
|
|
511
|
-
log("Choose how strict review gates should be for risky or low-confidence writes.");
|
|
512
|
-
log(" lowConfidenceThreshold: confidence cutoff used to mark writes as risky");
|
|
513
|
-
log(" riskySections: sections always treated as risky");
|
|
514
|
-
log(" Change later: npx phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
|
|
515
|
-
const thresholdAnswer = await prompts.input("Low-confidence threshold [0.0-1.0]", "0.7");
|
|
516
|
-
const lowConfidenceThreshold = parseLowConfidenceThreshold(thresholdAnswer, 0.7);
|
|
517
|
-
const riskySectionsAnswer = await prompts.input("Risky sections [Review,Stale,Conflicts]", "Stale,Conflicts");
|
|
518
|
-
const riskySections = parseRiskySectionsAnswer(riskySectionsAnswer, ["Stale", "Conflicts"]);
|
|
519
|
-
// Only offer semantic dedup/conflict when an LLM endpoint is explicitly configured.
|
|
520
|
-
// These features call /chat/completions, not an embedding endpoint, so we gate on
|
|
521
|
-
// PHREN_LLM_ENDPOINT (primary) or the presence of a known API key as a fallback.
|
|
522
|
-
// PHREN_EMBEDDING_API_URL alone is NOT sufficient — it only enables embeddings,
|
|
523
|
-
// not the LLM chat call that callLlm() makes.
|
|
524
|
-
const hasLlmApi = Boolean((process.env.PHREN_LLM_ENDPOINT) ||
|
|
525
|
-
process.env.ANTHROPIC_API_KEY ||
|
|
526
|
-
process.env.OPENAI_API_KEY);
|
|
527
|
-
let semanticDedupEnabled = false;
|
|
528
|
-
let semanticConflictEnabled = false;
|
|
529
|
-
if (hasLlmApi) {
|
|
530
|
-
printSection("LLM-Powered Memory Quality (Optional)");
|
|
531
|
-
log("Phren can use an LLM to catch near-duplicate or conflicting findings.");
|
|
532
|
-
log(" Requires: PHREN_LLM_ENDPOINT or ANTHROPIC_API_KEY/OPENAI_API_KEY set");
|
|
533
|
-
log("");
|
|
534
|
-
log("Semantic dedup: before saving a finding, ask the LLM whether it means the");
|
|
535
|
-
log("same thing as an existing one (catches same idea with different wording).");
|
|
536
|
-
semanticDedupEnabled = await prompts.confirm("Enable LLM-powered duplicate detection?", false);
|
|
537
|
-
log("");
|
|
538
|
-
log("Conflict detection: after saving a finding, check whether it contradicts an");
|
|
539
|
-
log("existing one (e.g. \"always use X\" vs \"never use X\"). Adds an inline annotation.");
|
|
540
|
-
semanticConflictEnabled = await prompts.confirm("Enable LLM-powered conflict detection?", false);
|
|
541
|
-
if (semanticDedupEnabled || semanticConflictEnabled) {
|
|
542
|
-
const currentModel = (process.env.PHREN_LLM_MODEL) || "gpt-4o-mini / claude-haiku-4-5-20251001 (default)";
|
|
543
|
-
log("");
|
|
544
|
-
log(" Cost note: each semantic check is ~80 input + ~5 output tokens, cached 24h.");
|
|
545
|
-
log(` Current model: ${currentModel}`);
|
|
546
|
-
const llmModel = (process.env.PHREN_LLM_MODEL);
|
|
547
|
-
const isExpensive = llmModel && /opus|sonnet|gpt-4(?!o-mini)/i.test(llmModel);
|
|
548
|
-
if (isExpensive) {
|
|
549
|
-
log(style.warning(` Warning: ${llmModel} is 20x more expensive than Haiku for yes/no checks.`));
|
|
550
|
-
log(" Consider: PHREN_LLM_MODEL=claude-haiku-4-5-20251001");
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
log(" With Haiku: fractions of a cent/session. With Opus: ~$0.20/session for heavy use.");
|
|
554
|
-
log(" Tip: set PHREN_LLM_MODEL=claude-haiku-4-5-20251001 to keep costs low.");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
printSection("Finding Sensitivity");
|
|
559
|
-
log("Controls how eagerly agents save findings to memory.");
|
|
560
|
-
log(" minimal — only when you explicitly ask");
|
|
561
|
-
log(" conservative — decisions and pitfalls only");
|
|
562
|
-
log(" balanced — non-obvious patterns, decisions, pitfalls, bugs (recommended)");
|
|
563
|
-
log(" aggressive — everything worth remembering, err on the side of capturing");
|
|
564
|
-
log(" Change later: npx phren config finding-sensitivity <level>");
|
|
565
|
-
const findingSensitivity = await prompts.select("Finding sensitivity", [
|
|
566
|
-
{ value: "balanced", name: "balanced (recommended)" },
|
|
567
|
-
{ value: "conservative", name: "conservative" },
|
|
568
|
-
{ value: "minimal", name: "minimal" },
|
|
569
|
-
{ value: "aggressive", name: "aggressive" },
|
|
570
|
-
], "balanced");
|
|
571
|
-
printSection("GitHub Sync");
|
|
572
|
-
log(`Phren stores memory as plain Markdown files in a git repo (${storagePath}).`);
|
|
573
|
-
log("Push it to a private GitHub repo to sync memory across machines.");
|
|
574
|
-
log(" Hooks will auto-commit + push after every session and pull on start.");
|
|
575
|
-
log(" Skip this if you just want to try phren locally first.");
|
|
576
|
-
const githubAnswer = await prompts.input("GitHub username (leave blank to skip)");
|
|
577
|
-
const githubUsername = githubAnswer || undefined;
|
|
578
|
-
let githubRepo;
|
|
579
|
-
if (githubUsername) {
|
|
580
|
-
const repoAnswer = await prompts.input("Repo name", "my-phren");
|
|
581
|
-
githubRepo = repoAnswer || "my-phren";
|
|
582
|
-
}
|
|
583
|
-
let bootstrapCurrentProject = false;
|
|
584
|
-
let bootstrapOwnership;
|
|
585
|
-
const detectedProject = detectProjectDir(process.cwd(), storagePath);
|
|
586
|
-
if (detectedProject) {
|
|
587
|
-
const detectedProjectName = path.basename(detectedProject);
|
|
588
|
-
printSection("Current Project");
|
|
589
|
-
log(`Detected project: ${detectedProjectName}`);
|
|
590
|
-
bootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
591
|
-
if (!bootstrapCurrentProject) {
|
|
592
|
-
bootstrapCurrentProject = false;
|
|
593
|
-
log(style.warning(` Skipped. Later: cd ${detectedProject} && npx phren add`));
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
|
597
|
-
{ value: projectOwnershipDefault, name: `${projectOwnershipDefault} (default)` },
|
|
598
|
-
...PROJECT_OWNERSHIP_MODES
|
|
599
|
-
.filter((mode) => mode !== projectOwnershipDefault)
|
|
600
|
-
.map((mode) => ({ value: mode, name: mode })),
|
|
601
|
-
], projectOwnershipDefault);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
const summaryItems = [
|
|
605
|
-
`Storage: ${storageChoice} (${storagePath})`,
|
|
606
|
-
`Machine: ${machine}`,
|
|
607
|
-
`Profile: ${profile}`,
|
|
608
|
-
`Domain: ${domain}`,
|
|
609
|
-
`Project ownership default: ${projectOwnershipDefault}`,
|
|
610
|
-
`MCP: ${mcp === "on" ? "enabled" : "disabled"}`,
|
|
611
|
-
`Hooks: ${hooks === "on" ? "enabled" : "disabled"}`,
|
|
612
|
-
`Auto-capture: ${autoCaptureEnabled ? "enabled" : "disabled"}`,
|
|
613
|
-
`Findings capture level: ${findingsProactivity}`,
|
|
614
|
-
`Task mode: ${taskMode}`,
|
|
615
|
-
`Task proactivity: ${taskProactivity}`,
|
|
616
|
-
`Low-confidence threshold: ${lowConfidenceThreshold}`,
|
|
617
|
-
`Risky sections: ${riskySections.join(", ")}`,
|
|
618
|
-
`Finding sensitivity: ${findingSensitivity}`,
|
|
619
|
-
`Semantic search: ${ollamaEnabled ? "enabled" : "disabled"}`,
|
|
620
|
-
`Semantic dedup: ${semanticDedupEnabled ? "enabled" : "disabled"}`,
|
|
621
|
-
`Semantic conflict detection: ${semanticConflictEnabled ? "enabled" : "disabled"}`,
|
|
622
|
-
`GitHub sync: ${githubUsername ? `${githubUsername}/${githubRepo ?? "my-phren"}` : "skipped"}`,
|
|
623
|
-
`Add detected project: ${bootstrapCurrentProject ? `yes (${bootstrapOwnership ?? projectOwnershipDefault})` : "no"}`,
|
|
624
|
-
];
|
|
625
|
-
if (inferredScaffold) {
|
|
626
|
-
summaryItems.push(`Inference: ${inferredScaffold.reason}`);
|
|
627
|
-
}
|
|
628
|
-
printSummary(summaryItems);
|
|
629
|
-
return {
|
|
630
|
-
storageChoice,
|
|
631
|
-
storagePath,
|
|
632
|
-
storageRepoRoot,
|
|
633
|
-
machine,
|
|
634
|
-
profile,
|
|
635
|
-
mcp,
|
|
636
|
-
hooks,
|
|
637
|
-
projectOwnershipDefault,
|
|
638
|
-
findingsProactivity,
|
|
639
|
-
taskProactivity,
|
|
640
|
-
lowConfidenceThreshold,
|
|
641
|
-
riskySections,
|
|
642
|
-
taskMode,
|
|
643
|
-
bootstrapCurrentProject,
|
|
644
|
-
bootstrapOwnership,
|
|
645
|
-
ollamaEnabled,
|
|
646
|
-
autoCaptureEnabled,
|
|
647
|
-
semanticDedupEnabled,
|
|
648
|
-
semanticConflictEnabled,
|
|
649
|
-
findingSensitivity,
|
|
650
|
-
githubUsername,
|
|
651
|
-
githubRepo,
|
|
652
|
-
domain,
|
|
653
|
-
inferredScaffold: inferredScaffold
|
|
654
|
-
? (domain === inferredScaffold.domain
|
|
655
|
-
? inferredScaffold
|
|
656
|
-
: { ...inferredScaffold, domain, topics: [] })
|
|
657
|
-
: undefined,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
export async function warmSemanticSearch(phrenPath, profile) {
|
|
661
|
-
const { checkOllamaAvailable, checkModelAvailable, getOllamaUrl, getEmbeddingModel } = await import("../shared/ollama.js");
|
|
662
|
-
const ollamaUrl = getOllamaUrl();
|
|
663
|
-
if (!ollamaUrl)
|
|
664
|
-
return "Semantic search: disabled.";
|
|
665
|
-
const model = getEmbeddingModel();
|
|
666
|
-
if (!await checkOllamaAvailable()) {
|
|
667
|
-
return `Semantic search not warmed: Ollama offline at ${ollamaUrl}.`;
|
|
668
|
-
}
|
|
669
|
-
if (!await checkModelAvailable()) {
|
|
670
|
-
return `Semantic search not warmed: model ${model} is not pulled yet.`;
|
|
671
|
-
}
|
|
672
|
-
const { buildIndex, listIndexedDocumentPaths } = await import("../shared/index.js");
|
|
673
|
-
const { getEmbeddingCache, formatEmbeddingCoverage } = await import("../shared/embedding-cache.js");
|
|
674
|
-
const { backgroundEmbedMissingDocs } = await import("../startup-embedding.js");
|
|
675
|
-
const { getPersistentVectorIndex } = await import("../shared/vector-index.js");
|
|
676
|
-
const db = await buildIndex(phrenPath, profile);
|
|
677
|
-
try {
|
|
678
|
-
const cache = getEmbeddingCache(phrenPath);
|
|
679
|
-
await cache.load().catch(() => { });
|
|
680
|
-
const allPaths = listIndexedDocumentPaths(phrenPath, profile);
|
|
681
|
-
const before = cache.coverage(allPaths);
|
|
682
|
-
if (before.missing > 0) {
|
|
683
|
-
await backgroundEmbedMissingDocs(db, cache);
|
|
684
|
-
}
|
|
685
|
-
await cache.load().catch(() => { });
|
|
686
|
-
const after = cache.coverage(allPaths);
|
|
687
|
-
if (cache.size() > 0) {
|
|
688
|
-
getPersistentVectorIndex(phrenPath).ensure(cache.getAllEntries());
|
|
689
|
-
}
|
|
690
|
-
if (after.total === 0) {
|
|
691
|
-
return `Semantic search ready (${model}), but there are no indexed docs yet.`;
|
|
692
|
-
}
|
|
693
|
-
const embeddedNow = Math.max(0, after.embedded - before.embedded);
|
|
694
|
-
const prefix = after.state === "warm" ? "Semantic search warmed" : "Semantic search warming";
|
|
695
|
-
const delta = embeddedNow > 0 ? `; embedded ${embeddedNow} new docs during init` : "";
|
|
696
|
-
return `${prefix}: ${model}, ${formatEmbeddingCoverage(after)}${delta}.`;
|
|
697
|
-
}
|
|
698
|
-
finally {
|
|
699
|
-
try {
|
|
700
|
-
db.close();
|
|
701
|
-
}
|
|
702
|
-
catch { /* ignore close errors in init */ }
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
function applyOnboardingPreferences(phrenPath, opts) {
|
|
706
|
-
if (opts.projectOwnershipDefault) {
|
|
707
|
-
writeInstallPreferences(phrenPath, { projectOwnershipDefault: opts.projectOwnershipDefault });
|
|
708
|
-
}
|
|
709
|
-
const runtimePatch = {};
|
|
710
|
-
if (opts.findingsProactivity)
|
|
711
|
-
runtimePatch.proactivityFindings = opts.findingsProactivity;
|
|
712
|
-
if (opts.taskProactivity)
|
|
713
|
-
runtimePatch.proactivityTask = opts.taskProactivity;
|
|
714
|
-
if (Object.keys(runtimePatch).length > 0) {
|
|
715
|
-
writeInstallPreferences(phrenPath, runtimePatch);
|
|
716
|
-
}
|
|
717
|
-
const governancePatch = {};
|
|
718
|
-
if (opts.findingsProactivity)
|
|
719
|
-
governancePatch.proactivityFindings = opts.findingsProactivity;
|
|
720
|
-
if (opts.taskProactivity)
|
|
721
|
-
governancePatch.proactivityTask = opts.taskProactivity;
|
|
722
|
-
if (Object.keys(governancePatch).length > 0) {
|
|
723
|
-
writeGovernanceInstallPreferences(phrenPath, governancePatch);
|
|
724
|
-
}
|
|
725
|
-
const workflowPatch = {};
|
|
726
|
-
if (typeof opts.lowConfidenceThreshold === "number")
|
|
727
|
-
workflowPatch.lowConfidenceThreshold = opts.lowConfidenceThreshold;
|
|
728
|
-
if (Array.isArray(opts.riskySections))
|
|
729
|
-
workflowPatch.riskySections = opts.riskySections;
|
|
730
|
-
if (opts.taskMode)
|
|
731
|
-
workflowPatch.taskMode = opts.taskMode;
|
|
732
|
-
if (opts.findingSensitivity)
|
|
733
|
-
workflowPatch.findingSensitivity = opts.findingSensitivity;
|
|
734
|
-
if (Object.keys(workflowPatch).length > 0) {
|
|
735
|
-
updateWorkflowPolicy(phrenPath, workflowPatch);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
function writeWalkthroughEnvDefaults(phrenPath, opts) {
|
|
739
|
-
const envFile = path.join(phrenPath, ".env");
|
|
740
|
-
let envContent = fs.existsSync(envFile) ? fs.readFileSync(envFile, "utf8") : "# phren feature flags — generated by init\n";
|
|
741
|
-
const envFlags = [];
|
|
742
|
-
const autoCaptureChoice = opts._walkthroughAutoCapture;
|
|
743
|
-
const hasAutoCaptureFlag = /^\s*PHREN_FEATURE_AUTO_CAPTURE=.*$/m.test(envContent);
|
|
744
|
-
if (typeof autoCaptureChoice === "boolean") {
|
|
745
|
-
envFlags.push({
|
|
746
|
-
flag: `PHREN_FEATURE_AUTO_CAPTURE=${autoCaptureChoice ? "1" : "0"}`,
|
|
747
|
-
label: `Auto-capture ${autoCaptureChoice ? "enabled" : "disabled"}`,
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
else if (autoCaptureChoice !== false && !hasAutoCaptureFlag) {
|
|
751
|
-
// Default to enabled on fresh installs and non-walkthrough init.
|
|
752
|
-
envFlags.push({ flag: "PHREN_FEATURE_AUTO_CAPTURE=1", label: "Auto-capture enabled" });
|
|
753
|
-
}
|
|
754
|
-
if (opts._walkthroughSemanticDedup)
|
|
755
|
-
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_DEDUP=1", label: "Semantic dedup" });
|
|
756
|
-
if (opts._walkthroughSemanticConflict)
|
|
757
|
-
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_CONFLICT=1", label: "Conflict detection" });
|
|
758
|
-
if (envFlags.length === 0)
|
|
759
|
-
return [];
|
|
760
|
-
let changed = false;
|
|
761
|
-
const enabledLabels = [];
|
|
762
|
-
for (const { flag, label } of envFlags) {
|
|
763
|
-
const key = flag.split("=")[0];
|
|
764
|
-
const lineRe = new RegExp(`^\\s*${key}=.*$`, "m");
|
|
765
|
-
if (lineRe.test(envContent)) {
|
|
766
|
-
const before = envContent;
|
|
767
|
-
envContent = envContent.replace(lineRe, flag);
|
|
768
|
-
if (envContent !== before) {
|
|
769
|
-
changed = true;
|
|
770
|
-
enabledLabels.push(label);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
else {
|
|
774
|
-
if (!envContent.endsWith("\n"))
|
|
775
|
-
envContent += "\n";
|
|
776
|
-
envContent += `${flag}\n`;
|
|
777
|
-
changed = true;
|
|
778
|
-
enabledLabels.push(label);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
if (changed) {
|
|
782
|
-
const tmpPath = `${envFile}.tmp-${crypto.randomUUID()}`;
|
|
783
|
-
fs.writeFileSync(tmpPath, envContent);
|
|
784
|
-
fs.renameSync(tmpPath, envFile);
|
|
785
|
-
}
|
|
786
|
-
return enabledLabels.map((label) => `${label} (${envFile})`);
|
|
787
|
-
}
|
|
788
|
-
function collectRepairedAssetLabels(repaired) {
|
|
789
|
-
const repairedAssets = [];
|
|
790
|
-
if (repaired.createdContextFile)
|
|
791
|
-
repairedAssets.push("~/.phren-context.md");
|
|
792
|
-
if (repaired.createdRootMemory)
|
|
793
|
-
repairedAssets.push("generated MEMORY.md");
|
|
794
|
-
repairedAssets.push(...repaired.createdGlobalAssets);
|
|
795
|
-
repairedAssets.push(...repaired.createdRuntimeAssets);
|
|
796
|
-
repairedAssets.push(...repaired.createdFeatureDefaults);
|
|
797
|
-
repairedAssets.push(...repaired.createdSkillArtifacts);
|
|
798
|
-
return repairedAssets;
|
|
799
|
-
}
|
|
800
|
-
function applyProjectStorageBindings(repoRoot, phrenPath) {
|
|
801
|
-
const updates = [];
|
|
802
|
-
if (ensureGitignoreEntry(repoRoot, ".phren/")) {
|
|
803
|
-
updates.push(`${path.join(repoRoot, ".gitignore")} (.phren/)`);
|
|
804
|
-
}
|
|
805
|
-
if (upsertProjectEnvVar(repoRoot, "PHREN_PATH", phrenPath)) {
|
|
806
|
-
updates.push(`${path.join(repoRoot, ".env")} (PHREN_PATH=${phrenPath})`);
|
|
807
|
-
}
|
|
808
|
-
return updates;
|
|
809
|
-
}
|
|
810
|
-
async function runProjectLocalInit(opts = {}) {
|
|
811
|
-
const detectedRoot = detectProjectDir(process.cwd(), path.join(process.cwd(), ".phren")) || process.cwd();
|
|
812
|
-
const hasWorkspaceMarker = fs.existsSync(path.join(detectedRoot, ".git")) ||
|
|
813
|
-
fs.existsSync(path.join(detectedRoot, "CLAUDE.md")) ||
|
|
814
|
-
fs.existsSync(path.join(detectedRoot, "AGENTS.md")) ||
|
|
815
|
-
fs.existsSync(path.join(detectedRoot, ".claude", "CLAUDE.md"));
|
|
816
|
-
if (!hasWorkspaceMarker) {
|
|
817
|
-
throw new Error("project-local mode must be run inside a repo or project root");
|
|
818
|
-
}
|
|
819
|
-
const workspaceRoot = path.resolve(detectedRoot);
|
|
820
|
-
const phrenPath = path.join(workspaceRoot, ".phren");
|
|
821
|
-
const existingManifest = readRootManifest(phrenPath);
|
|
822
|
-
if (existingManifest && existingManifest.installMode !== "project-local") {
|
|
823
|
-
throw new Error(`Refusing to reuse non-local phren root at ${phrenPath}`);
|
|
824
|
-
}
|
|
825
|
-
const ownershipDefault = opts.projectOwnershipDefault
|
|
826
|
-
?? (existingManifest ? getProjectOwnershipDefault(phrenPath) : "detached");
|
|
827
|
-
if (!existingManifest && !opts.projectOwnershipDefault) {
|
|
828
|
-
opts.projectOwnershipDefault = ownershipDefault;
|
|
829
|
-
}
|
|
830
|
-
const mcpEnabled = opts.mcp ? opts.mcp === "on" : true;
|
|
831
|
-
const projectName = path.basename(workspaceRoot).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
832
|
-
if (opts.dryRun) {
|
|
833
|
-
log("\nInit dry run. No files will be written.\n");
|
|
834
|
-
log(`Mode: project-local`);
|
|
835
|
-
log(`Workspace root: ${workspaceRoot}`);
|
|
836
|
-
log(`Phren root: ${phrenPath}`);
|
|
837
|
-
log(`Project: ${projectName}`);
|
|
838
|
-
log(`VS Code workspace MCP: ${mcpEnabled ? "on" : "off"}`);
|
|
839
|
-
log(`Hooks: unsupported in project-local mode`);
|
|
840
|
-
log("");
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
fs.mkdirSync(phrenPath, { recursive: true });
|
|
844
|
-
writeRootManifest(phrenPath, {
|
|
845
|
-
version: 1,
|
|
846
|
-
installMode: "project-local",
|
|
847
|
-
syncMode: "workspace-git",
|
|
848
|
-
workspaceRoot,
|
|
849
|
-
primaryProject: projectName,
|
|
850
|
-
});
|
|
851
|
-
ensureGovernanceFiles(phrenPath);
|
|
852
|
-
repairPreexistingInstall(phrenPath);
|
|
853
|
-
fs.mkdirSync(path.join(phrenPath, "global", "skills"), { recursive: true });
|
|
854
|
-
fs.mkdirSync(path.join(phrenPath, ".runtime"), { recursive: true });
|
|
855
|
-
fs.mkdirSync(path.join(phrenPath, ".sessions"), { recursive: true });
|
|
856
|
-
if (!fs.existsSync(path.join(phrenPath, ".gitignore"))) {
|
|
857
|
-
atomicWriteText(path.join(phrenPath, ".gitignore"), [
|
|
858
|
-
".runtime/",
|
|
859
|
-
".sessions/",
|
|
860
|
-
"*.lock",
|
|
861
|
-
"*.tmp-*",
|
|
862
|
-
"",
|
|
863
|
-
].join("\n"));
|
|
864
|
-
}
|
|
865
|
-
if (!fs.existsSync(path.join(phrenPath, "global", "CLAUDE.md"))) {
|
|
866
|
-
atomicWriteText(path.join(phrenPath, "global", "CLAUDE.md"), "# Global Context\n\nRepo-local Phren instructions shared across this workspace.\n");
|
|
867
|
-
}
|
|
868
|
-
const created = bootstrapFromExisting(phrenPath, workspaceRoot, { ownership: ownershipDefault });
|
|
869
|
-
applyOnboardingPreferences(phrenPath, opts);
|
|
870
|
-
writeInstallPreferences(phrenPath, {
|
|
871
|
-
mcpEnabled,
|
|
872
|
-
hooksEnabled: false,
|
|
873
|
-
skillsScope: opts.skillsScope ?? "global",
|
|
874
|
-
installedVersion: VERSION,
|
|
875
|
-
});
|
|
876
|
-
try {
|
|
877
|
-
const vscodeResult = configureVSCode(phrenPath, { mcpEnabled, scope: "workspace" });
|
|
878
|
-
logMcpTargetStatus("VS Code", vscodeResult, existingManifest ? "Updated" : "Configured");
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
debugLog(`configureVSCode(workspace) failed: ${errorMessage(err)}`);
|
|
882
|
-
}
|
|
883
|
-
log(`\n${existingManifest ? "Updated" : "Created"} project-local phren at ${phrenPath}`);
|
|
884
|
-
log(` Workspace root: ${workspaceRoot}`);
|
|
885
|
-
log(` Project: ${created.project}`);
|
|
886
|
-
log(` Ownership: ${created.ownership}`);
|
|
887
|
-
log(` Sync mode: workspace-git`);
|
|
888
|
-
log(` Hooks: off (unsupported in project-local mode)`);
|
|
889
|
-
log(` VS Code MCP: ${mcpEnabled ? "workspace on" : "workspace off"}`);
|
|
890
|
-
const verify = runPostInitVerify(phrenPath);
|
|
891
|
-
log(`\nVerifying setup...`);
|
|
892
|
-
for (const check of verify.checks) {
|
|
893
|
-
log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Configure MCP for all detected AI coding tools (Claude, VS Code, Cursor, Copilot, Codex).
|
|
898
|
-
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
899
|
-
*/
|
|
900
|
-
export function configureMcpTargets(phrenPath, opts, verb = "Configured") {
|
|
901
|
-
let claudeStatus = "no_settings";
|
|
902
|
-
try {
|
|
903
|
-
const status = configureClaude(phrenPath, { mcpEnabled: opts.mcpEnabled, hooksEnabled: opts.hooksEnabled });
|
|
904
|
-
claudeStatus = status ?? "installed";
|
|
905
|
-
if (status === "disabled" || status === "already_disabled") {
|
|
906
|
-
log(` ${verb} Claude Code hooks (MCP disabled)`);
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
log(` ${verb} Claude Code MCP + hooks`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
catch (e) {
|
|
913
|
-
log(` Could not configure Claude Code settings (${e}), add manually`);
|
|
914
|
-
}
|
|
915
|
-
let vsStatus = "no_vscode";
|
|
916
|
-
try {
|
|
917
|
-
vsStatus = configureVSCode(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_vscode";
|
|
918
|
-
logMcpTargetStatus("VS Code", vsStatus, verb);
|
|
919
|
-
}
|
|
920
|
-
catch (err) {
|
|
921
|
-
debugLog(`configureVSCode failed: ${errorMessage(err)}`);
|
|
922
|
-
}
|
|
923
|
-
let cursorStatus = "no_cursor";
|
|
924
|
-
try {
|
|
925
|
-
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_cursor";
|
|
926
|
-
logMcpTargetStatus("Cursor", cursorStatus, verb);
|
|
927
|
-
}
|
|
928
|
-
catch (err) {
|
|
929
|
-
debugLog(`configureCursorMcp failed: ${errorMessage(err)}`);
|
|
930
|
-
}
|
|
931
|
-
let copilotStatus = "no_copilot";
|
|
932
|
-
try {
|
|
933
|
-
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_copilot";
|
|
934
|
-
logMcpTargetStatus("Copilot CLI", copilotStatus, verb);
|
|
935
|
-
}
|
|
936
|
-
catch (err) {
|
|
937
|
-
debugLog(`configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
938
|
-
}
|
|
939
|
-
let codexStatus = "no_codex";
|
|
940
|
-
try {
|
|
941
|
-
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_codex";
|
|
942
|
-
logMcpTargetStatus("Codex", codexStatus, verb);
|
|
943
|
-
}
|
|
944
|
-
catch (err) {
|
|
945
|
-
debugLog(`configureCodexMcp failed: ${errorMessage(err)}`);
|
|
946
|
-
}
|
|
947
|
-
const allStatuses = [claudeStatus, vsStatus, cursorStatus, copilotStatus, codexStatus];
|
|
948
|
-
if (allStatuses.some((s) => s === "installed" || s === "already_configured"))
|
|
949
|
-
return "installed";
|
|
950
|
-
if (allStatuses.some((s) => s === "disabled" || s === "already_disabled"))
|
|
951
|
-
return "disabled";
|
|
952
|
-
return claudeStatus;
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Configure hooks if enabled, or log a disabled message.
|
|
956
|
-
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
957
|
-
*/
|
|
958
|
-
function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
|
|
959
|
-
if (hooksEnabled) {
|
|
960
|
-
try {
|
|
961
|
-
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
962
|
-
if (hooked.length)
|
|
963
|
-
log(` ${verb} hooks: ${hooked.join(", ")}`);
|
|
964
|
-
}
|
|
965
|
-
catch (err) {
|
|
966
|
-
debugLog(`configureAllHooks failed: ${errorMessage(err)}`);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
else {
|
|
970
|
-
log(` Hooks are disabled by preference (run: npx phren hooks-mode on)`);
|
|
971
|
-
}
|
|
972
|
-
// Install phren CLI wrapper at ~/.local/bin/phren so the bare command works
|
|
973
|
-
const wrapperInstalled = installPhrenCliWrapper(phrenPath);
|
|
974
|
-
if (wrapperInstalled) {
|
|
975
|
-
log(` ${verb} CLI wrapper: ~/.local/bin/phren`);
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
log(` Note: phren CLI wrapper not installed (existing non-managed binary, or no entry script found)`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
104
|
export async function runInit(opts = {}) {
|
|
982
105
|
if ((opts.mode || "shared") === "project-local") {
|
|
983
106
|
await runProjectLocalInit(opts);
|
|
@@ -990,7 +113,7 @@ export async function runInit(opts = {}) {
|
|
|
990
113
|
// doesn't exist yet but the legacy directory does.
|
|
991
114
|
if (!opts._walkthroughStoragePath && !fs.existsSync(phrenPath)) {
|
|
992
115
|
// Pre-rebrand directory name — kept as literal for migration
|
|
993
|
-
const legacyPath = path.resolve(homePath(".cortex"));
|
|
116
|
+
const legacyPath = path.resolve((await import("../shared.js")).homePath(".cortex"));
|
|
994
117
|
if (legacyPath !== phrenPath && fs.existsSync(legacyPath) && hasInstallMarkers(legacyPath)) {
|
|
995
118
|
if (!dryRun) {
|
|
996
119
|
fs.renameSync(legacyPath, phrenPath);
|
|
@@ -1523,555 +646,3 @@ export async function runInit(opts = {}) {
|
|
|
1523
646
|
log(`\n Read ${phrenPath}/README.md for a guided tour of each file.`);
|
|
1524
647
|
log(``);
|
|
1525
648
|
}
|
|
1526
|
-
export async function runMcpMode(modeArg) {
|
|
1527
|
-
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1528
|
-
const manifest = readRootManifest(phrenPath);
|
|
1529
|
-
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
1530
|
-
if (!normalizedArg || normalizedArg === "status") {
|
|
1531
|
-
const current = getMcpEnabledPreference(phrenPath);
|
|
1532
|
-
const hooks = getHooksEnabledPreference(phrenPath);
|
|
1533
|
-
log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
|
|
1534
|
-
log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
|
|
1535
|
-
log(`Change mode: npx phren mcp-mode on|off`);
|
|
1536
|
-
log(`Hooks toggle: npx phren hooks-mode on|off`);
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
const mode = parseMcpMode(normalizedArg);
|
|
1540
|
-
if (!mode) {
|
|
1541
|
-
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
1542
|
-
}
|
|
1543
|
-
const enabled = mode === "on";
|
|
1544
|
-
if (manifest?.installMode === "project-local") {
|
|
1545
|
-
const vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled, scope: "workspace" });
|
|
1546
|
-
setMcpEnabledPreference(phrenPath, enabled);
|
|
1547
|
-
log(`MCP mode set to ${mode}.`);
|
|
1548
|
-
log(`VS Code status: ${vscodeStatus}`);
|
|
1549
|
-
log(`Project-local mode only configures workspace VS Code MCP.`);
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
let claudeStatus = "no_settings";
|
|
1553
|
-
let vscodeStatus = "no_vscode";
|
|
1554
|
-
let cursorStatus = "no_cursor";
|
|
1555
|
-
let copilotStatus = "no_copilot";
|
|
1556
|
-
let codexStatus = "no_codex";
|
|
1557
|
-
try {
|
|
1558
|
-
claudeStatus = configureClaude(phrenPath, { mcpEnabled: enabled }) ?? claudeStatus;
|
|
1559
|
-
}
|
|
1560
|
-
catch (err) {
|
|
1561
|
-
debugLog(`mcp-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled }) ?? vscodeStatus;
|
|
1565
|
-
}
|
|
1566
|
-
catch (err) {
|
|
1567
|
-
debugLog(`mcp-mode: configureVSCode failed: ${errorMessage(err)}`);
|
|
1568
|
-
}
|
|
1569
|
-
try {
|
|
1570
|
-
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: enabled }) ?? cursorStatus;
|
|
1571
|
-
}
|
|
1572
|
-
catch (err) {
|
|
1573
|
-
debugLog(`mcp-mode: configureCursorMcp failed: ${errorMessage(err)}`);
|
|
1574
|
-
}
|
|
1575
|
-
try {
|
|
1576
|
-
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: enabled }) ?? copilotStatus;
|
|
1577
|
-
}
|
|
1578
|
-
catch (err) {
|
|
1579
|
-
debugLog(`mcp-mode: configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
1580
|
-
}
|
|
1581
|
-
try {
|
|
1582
|
-
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: enabled }) ?? codexStatus;
|
|
1583
|
-
}
|
|
1584
|
-
catch (err) {
|
|
1585
|
-
debugLog(`mcp-mode: configureCodexMcp failed: ${errorMessage(err)}`);
|
|
1586
|
-
}
|
|
1587
|
-
// Persist preference only after config writes have been attempted
|
|
1588
|
-
setMcpEnabledPreference(phrenPath, enabled);
|
|
1589
|
-
log(`MCP mode set to ${mode}.`);
|
|
1590
|
-
log(`Claude status: ${claudeStatus}`);
|
|
1591
|
-
log(`VS Code status: ${vscodeStatus}`);
|
|
1592
|
-
log(`Cursor status: ${cursorStatus}`);
|
|
1593
|
-
log(`Copilot CLI status: ${copilotStatus}`);
|
|
1594
|
-
log(`Codex status: ${codexStatus}`);
|
|
1595
|
-
log(`Restart your agent to apply changes.`);
|
|
1596
|
-
}
|
|
1597
|
-
export async function runHooksMode(modeArg) {
|
|
1598
|
-
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1599
|
-
const manifest = readRootManifest(phrenPath);
|
|
1600
|
-
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
1601
|
-
if (!normalizedArg || normalizedArg === "status") {
|
|
1602
|
-
const current = getHooksEnabledPreference(phrenPath);
|
|
1603
|
-
log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
|
|
1604
|
-
log(`Change mode: npx phren hooks-mode on|off`);
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
const mode = parseMcpMode(normalizedArg);
|
|
1608
|
-
if (!mode) {
|
|
1609
|
-
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
1610
|
-
}
|
|
1611
|
-
if (manifest?.installMode === "project-local") {
|
|
1612
|
-
throw new Error("hooks-mode is unsupported in project-local mode");
|
|
1613
|
-
}
|
|
1614
|
-
const enabled = mode === "on";
|
|
1615
|
-
let claudeStatus = "no_settings";
|
|
1616
|
-
try {
|
|
1617
|
-
claudeStatus = configureClaude(phrenPath, {
|
|
1618
|
-
mcpEnabled: getMcpEnabledPreference(phrenPath),
|
|
1619
|
-
hooksEnabled: enabled,
|
|
1620
|
-
}) ?? claudeStatus;
|
|
1621
|
-
}
|
|
1622
|
-
catch (err) {
|
|
1623
|
-
debugLog(`hooks-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
1624
|
-
}
|
|
1625
|
-
if (enabled) {
|
|
1626
|
-
try {
|
|
1627
|
-
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
1628
|
-
if (hooked.length)
|
|
1629
|
-
log(`Updated hooks: ${hooked.join(", ")}`);
|
|
1630
|
-
}
|
|
1631
|
-
catch (err) {
|
|
1632
|
-
debugLog(`hooks-mode: configureAllHooks failed: ${errorMessage(err)}`);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
else {
|
|
1636
|
-
log("Hooks will no-op immediately via preference and Claude hooks are removed.");
|
|
1637
|
-
}
|
|
1638
|
-
// Persist preference only after config writes have been attempted
|
|
1639
|
-
setHooksEnabledPreference(phrenPath, enabled);
|
|
1640
|
-
log(`Hooks mode set to ${mode}.`);
|
|
1641
|
-
log(`Claude status: ${claudeStatus}`);
|
|
1642
|
-
log(`Restart your agent to apply changes.`);
|
|
1643
|
-
}
|
|
1644
|
-
// Agent skill directories to sweep for symlinks during uninstall
|
|
1645
|
-
function agentSkillDirs() {
|
|
1646
|
-
const home = homeDir();
|
|
1647
|
-
return [
|
|
1648
|
-
homePath(".claude", "skills"),
|
|
1649
|
-
path.join(home, ".cursor", "skills"),
|
|
1650
|
-
path.join(home, ".copilot", "skills"),
|
|
1651
|
-
path.join(home, ".codex", "skills"),
|
|
1652
|
-
];
|
|
1653
|
-
}
|
|
1654
|
-
// Remove skill symlinks that resolve inside phrenPath. Only touches symlinks, never regular files.
|
|
1655
|
-
function sweepSkillSymlinks(phrenPath) {
|
|
1656
|
-
const resolvedPhren = path.resolve(phrenPath);
|
|
1657
|
-
for (const dir of agentSkillDirs()) {
|
|
1658
|
-
if (!fs.existsSync(dir))
|
|
1659
|
-
continue;
|
|
1660
|
-
let entries;
|
|
1661
|
-
try {
|
|
1662
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1663
|
-
}
|
|
1664
|
-
catch (err) {
|
|
1665
|
-
debugLog(`sweepSkillSymlinks: readdirSync failed for ${dir}: ${errorMessage(err)}`);
|
|
1666
|
-
continue;
|
|
1667
|
-
}
|
|
1668
|
-
for (const entry of entries) {
|
|
1669
|
-
if (!entry.isSymbolicLink())
|
|
1670
|
-
continue;
|
|
1671
|
-
const fullPath = path.join(dir, entry.name);
|
|
1672
|
-
try {
|
|
1673
|
-
const target = fs.realpathSync(fullPath);
|
|
1674
|
-
if (target.startsWith(resolvedPhren + path.sep) || target === resolvedPhren) {
|
|
1675
|
-
fs.unlinkSync(fullPath);
|
|
1676
|
-
log(` Removed skill symlink: ${fullPath}`);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
catch {
|
|
1680
|
-
// Broken symlink (target no longer exists) — clean it up
|
|
1681
|
-
try {
|
|
1682
|
-
fs.unlinkSync(fullPath);
|
|
1683
|
-
log(` Removed broken skill symlink: ${fullPath}`);
|
|
1684
|
-
}
|
|
1685
|
-
catch (err2) {
|
|
1686
|
-
debugLog(`sweepSkillSymlinks: could not remove broken symlink ${fullPath}: ${errorMessage(err2)}`);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
// Remove phren-generated manifest files from the skills parent directory
|
|
1691
|
-
const parentDir = path.dirname(dir);
|
|
1692
|
-
for (const manifestFile of ["skill-manifest.json", "skill-commands.json"]) {
|
|
1693
|
-
const manifestPath = path.join(parentDir, manifestFile);
|
|
1694
|
-
try {
|
|
1695
|
-
if (fs.existsSync(manifestPath)) {
|
|
1696
|
-
fs.unlinkSync(manifestPath);
|
|
1697
|
-
log(` Removed ${manifestFile} (${manifestPath})`);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
catch (err) {
|
|
1701
|
-
debugLog(`sweepSkillSymlinks: could not remove ${manifestPath}: ${errorMessage(err)}`);
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
// Filter phren hook entries from an agent hooks file. Returns true if the file was changed.
|
|
1707
|
-
// Deletes the file if no hooks remain. `commandField` is the JSON key holding the command
|
|
1708
|
-
// string in each hook entry (e.g. "bash" for Copilot, "command" for Codex).
|
|
1709
|
-
function filterAgentHooks(filePath, commandField) {
|
|
1710
|
-
if (!fs.existsSync(filePath))
|
|
1711
|
-
return false;
|
|
1712
|
-
try {
|
|
1713
|
-
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1714
|
-
if (!isRecord(raw) || !isRecord(raw.hooks))
|
|
1715
|
-
return false;
|
|
1716
|
-
const hooks = raw.hooks;
|
|
1717
|
-
let changed = false;
|
|
1718
|
-
for (const event of Object.keys(hooks)) {
|
|
1719
|
-
const entries = hooks[event];
|
|
1720
|
-
if (!Array.isArray(entries))
|
|
1721
|
-
continue;
|
|
1722
|
-
const filtered = entries.filter((e) => !(isRecord(e) && typeof e[commandField] === "string" && isPhrenCommand(e[commandField])));
|
|
1723
|
-
if (filtered.length !== entries.length) {
|
|
1724
|
-
hooks[event] = filtered;
|
|
1725
|
-
changed = true;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
if (!changed)
|
|
1729
|
-
return false;
|
|
1730
|
-
// Remove empty hook event keys
|
|
1731
|
-
for (const event of Object.keys(hooks)) {
|
|
1732
|
-
if (Array.isArray(hooks[event]) && hooks[event].length === 0) {
|
|
1733
|
-
delete hooks[event];
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
if (Object.keys(hooks).length === 0) {
|
|
1737
|
-
fs.unlinkSync(filePath);
|
|
1738
|
-
}
|
|
1739
|
-
else {
|
|
1740
|
-
atomicWriteText(filePath, JSON.stringify(raw, null, 2));
|
|
1741
|
-
}
|
|
1742
|
-
return true;
|
|
1743
|
-
}
|
|
1744
|
-
catch (err) {
|
|
1745
|
-
debugLog(`filterAgentHooks: failed for ${filePath}: ${errorMessage(err)}`);
|
|
1746
|
-
return false;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
async function promptUninstallConfirm(phrenPath) {
|
|
1750
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1751
|
-
return true;
|
|
1752
|
-
// Show summary of what will be deleted
|
|
1753
|
-
try {
|
|
1754
|
-
const projectDirs = getProjectDirs(phrenPath);
|
|
1755
|
-
const projectCount = projectDirs.length;
|
|
1756
|
-
let findingCount = 0;
|
|
1757
|
-
for (const dir of projectDirs) {
|
|
1758
|
-
const findingsFile = path.join(dir, "FINDINGS.md");
|
|
1759
|
-
if (fs.existsSync(findingsFile)) {
|
|
1760
|
-
const content = fs.readFileSync(findingsFile, "utf8");
|
|
1761
|
-
findingCount += content.split("\n").filter((l) => l.startsWith("- ")).length;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
log(`\n Will delete: ${phrenPath}`);
|
|
1765
|
-
log(` Contains: ${projectCount} project(s), ~${findingCount} finding(s)`);
|
|
1766
|
-
}
|
|
1767
|
-
catch (err) {
|
|
1768
|
-
debugLog(`promptUninstallConfirm: summary failed: ${errorMessage(err)}`);
|
|
1769
|
-
log(`\n Will delete: ${phrenPath}`);
|
|
1770
|
-
}
|
|
1771
|
-
const readline = await import("readline");
|
|
1772
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1773
|
-
return new Promise((resolve) => {
|
|
1774
|
-
rl.question(`\nThis will permanently delete ${phrenPath} and all phren data. Type 'yes' to confirm: `, (answer) => {
|
|
1775
|
-
rl.close();
|
|
1776
|
-
resolve(answer.trim().toLowerCase() === "yes");
|
|
1777
|
-
});
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
export async function runUninstall(opts = {}) {
|
|
1781
|
-
const phrenPath = findPhrenPath();
|
|
1782
|
-
const manifest = phrenPath ? readRootManifest(phrenPath) : null;
|
|
1783
|
-
if (manifest?.installMode === "project-local" && phrenPath) {
|
|
1784
|
-
log("\nUninstalling project-local phren...\n");
|
|
1785
|
-
const workspaceRoot = manifest.workspaceRoot || path.dirname(phrenPath);
|
|
1786
|
-
const workspaceMcp = path.join(workspaceRoot, ".vscode", "mcp.json");
|
|
1787
|
-
try {
|
|
1788
|
-
if (removeMcpServerAtPath(workspaceMcp)) {
|
|
1789
|
-
log(` Removed phren from VS Code workspace MCP config (${workspaceMcp})`);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
catch (err) {
|
|
1793
|
-
debugLog(`uninstall local vscode cleanup failed: ${errorMessage(err)}`);
|
|
1794
|
-
}
|
|
1795
|
-
fs.rmSync(phrenPath, { recursive: true, force: true });
|
|
1796
|
-
log(` Removed ${phrenPath}`);
|
|
1797
|
-
log("\nProject-local phren uninstalled.");
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
log("\nUninstalling phren...\n");
|
|
1801
|
-
const shouldRemoveGlobalPackage = shouldUninstallCurrentGlobalPackage();
|
|
1802
|
-
// Confirmation prompt (shared-mode only — project-local is low-stakes)
|
|
1803
|
-
if (!opts.yes) {
|
|
1804
|
-
const confirmed = phrenPath
|
|
1805
|
-
? await promptUninstallConfirm(phrenPath)
|
|
1806
|
-
: (process.stdin.isTTY && process.stdout.isTTY
|
|
1807
|
-
? await (async () => {
|
|
1808
|
-
const readline = await import("readline");
|
|
1809
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1810
|
-
return new Promise((resolve) => {
|
|
1811
|
-
rl.question("This will remove all phren config and hooks. Type 'yes' to confirm: ", (answer) => {
|
|
1812
|
-
rl.close();
|
|
1813
|
-
resolve(answer.trim().toLowerCase() === "yes");
|
|
1814
|
-
});
|
|
1815
|
-
});
|
|
1816
|
-
})()
|
|
1817
|
-
: true);
|
|
1818
|
-
if (!confirmed) {
|
|
1819
|
-
log("Uninstall cancelled.");
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
const home = homeDir();
|
|
1824
|
-
const machineFile = machineFilePath();
|
|
1825
|
-
const settingsPath = hookConfigPath("claude");
|
|
1826
|
-
// Remove from Claude Code ~/.claude.json (where MCP servers are actually read)
|
|
1827
|
-
const claudeJsonPath = homePath(".claude.json");
|
|
1828
|
-
if (fs.existsSync(claudeJsonPath)) {
|
|
1829
|
-
try {
|
|
1830
|
-
if (removeMcpServerAtPath(claudeJsonPath)) {
|
|
1831
|
-
log(` Removed phren MCP server from ~/.claude.json`);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
catch (e) {
|
|
1835
|
-
log(` Warning: could not update ~/.claude.json (${e})`);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
// Remove from Claude Code settings.json
|
|
1839
|
-
if (fs.existsSync(settingsPath)) {
|
|
1840
|
-
try {
|
|
1841
|
-
patchJsonFile(settingsPath, (data) => {
|
|
1842
|
-
const hooksMap = isRecord(data.hooks) ? data.hooks : (data.hooks = {});
|
|
1843
|
-
// Remove MCP server
|
|
1844
|
-
if (data.mcpServers?.phren) {
|
|
1845
|
-
delete data.mcpServers.phren;
|
|
1846
|
-
log(` Removed phren MCP server from Claude Code settings`);
|
|
1847
|
-
}
|
|
1848
|
-
// Remove hooks containing phren references
|
|
1849
|
-
for (const hookEvent of ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"]) {
|
|
1850
|
-
const hooks = hooksMap[hookEvent];
|
|
1851
|
-
if (!Array.isArray(hooks))
|
|
1852
|
-
continue;
|
|
1853
|
-
const before = hooks.length;
|
|
1854
|
-
hooksMap[hookEvent] = hooks.filter((h) => !h.hooks?.some((hook) => typeof hook.command === "string" && isPhrenCommand(hook.command)));
|
|
1855
|
-
const removed = before - hooksMap[hookEvent].length;
|
|
1856
|
-
if (removed > 0)
|
|
1857
|
-
log(` Removed ${removed} phren hook(s) from ${hookEvent}`);
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
catch (e) {
|
|
1862
|
-
log(` Warning: could not update Claude Code settings (${e})`);
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
else {
|
|
1866
|
-
log(` Claude Code settings not found at ${settingsPath} — skipping`);
|
|
1867
|
-
}
|
|
1868
|
-
// Remove from VS Code mcp.json
|
|
1869
|
-
const vsCandidates = vscodeMcpCandidates().map((dir) => path.join(dir, "mcp.json"));
|
|
1870
|
-
for (const mcpFile of vsCandidates) {
|
|
1871
|
-
try {
|
|
1872
|
-
if (removeMcpServerAtPath(mcpFile)) {
|
|
1873
|
-
log(` Removed phren from VS Code MCP config (${mcpFile})`);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
catch (err) {
|
|
1877
|
-
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
// Remove from Cursor MCP config
|
|
1881
|
-
const cursorCandidates = cursorMcpCandidates();
|
|
1882
|
-
for (const mcpFile of cursorCandidates) {
|
|
1883
|
-
try {
|
|
1884
|
-
if (removeMcpServerAtPath(mcpFile)) {
|
|
1885
|
-
log(` Removed phren from Cursor MCP config (${mcpFile})`);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
catch (err) {
|
|
1889
|
-
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
// Remove from Copilot CLI MCP config
|
|
1893
|
-
const copilotCandidates = copilotMcpCandidates();
|
|
1894
|
-
for (const mcpFile of copilotCandidates) {
|
|
1895
|
-
try {
|
|
1896
|
-
if (removeMcpServerAtPath(mcpFile)) {
|
|
1897
|
-
log(` Removed phren from Copilot CLI MCP config (${mcpFile})`);
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
catch (err) {
|
|
1901
|
-
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
// Remove from Codex MCP config (TOML + JSON)
|
|
1905
|
-
const codexToml = path.join(home, ".codex", "config.toml");
|
|
1906
|
-
try {
|
|
1907
|
-
if (removeTomlMcpServer(codexToml)) {
|
|
1908
|
-
log(` Removed phren from Codex MCP config (${codexToml})`);
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
catch (err) {
|
|
1912
|
-
debugLog(`uninstall: cleanup failed for ${codexToml}: ${errorMessage(err)}`);
|
|
1913
|
-
}
|
|
1914
|
-
const codexCandidates = codexJsonCandidates((process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1915
|
-
for (const mcpFile of codexCandidates) {
|
|
1916
|
-
try {
|
|
1917
|
-
if (removeMcpServerAtPath(mcpFile)) {
|
|
1918
|
-
log(` Removed phren from Codex MCP config (${mcpFile})`);
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
catch (err) {
|
|
1922
|
-
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
// Remove phren entries from Copilot hooks file (filter, don't bulk-delete)
|
|
1926
|
-
const copilotHooksFile = hookConfigPath("copilot", (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1927
|
-
try {
|
|
1928
|
-
if (filterAgentHooks(copilotHooksFile, "bash")) {
|
|
1929
|
-
log(` Removed phren entries from Copilot hooks (${copilotHooksFile})`);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
catch (err) {
|
|
1933
|
-
debugLog(`uninstall: cleanup failed for ${copilotHooksFile}: ${errorMessage(err)}`);
|
|
1934
|
-
}
|
|
1935
|
-
// Remove phren entries from Cursor hooks file (may contain non-phren entries)
|
|
1936
|
-
const cursorHooksFile = hookConfigPath("cursor", (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1937
|
-
try {
|
|
1938
|
-
if (fs.existsSync(cursorHooksFile)) {
|
|
1939
|
-
const raw = JSON.parse(fs.readFileSync(cursorHooksFile, "utf8"));
|
|
1940
|
-
let changed = false;
|
|
1941
|
-
for (const key of ["sessionStart", "beforeSubmitPrompt", "stop"]) {
|
|
1942
|
-
if (raw[key]?.command && typeof raw[key].command === "string" && isPhrenCommand(raw[key].command)) {
|
|
1943
|
-
delete raw[key];
|
|
1944
|
-
changed = true;
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
if (changed) {
|
|
1948
|
-
atomicWriteText(cursorHooksFile, JSON.stringify(raw, null, 2));
|
|
1949
|
-
log(` Removed phren entries from Cursor hooks (${cursorHooksFile})`);
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
catch (err) {
|
|
1954
|
-
debugLog(`uninstall: cleanup failed for ${cursorHooksFile}: ${errorMessage(err)}`);
|
|
1955
|
-
}
|
|
1956
|
-
// Remove phren entries from Codex hooks file (filter, don't bulk-delete)
|
|
1957
|
-
const uninstallPhrenPath = (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1958
|
-
const codexHooksFile = hookConfigPath("codex", uninstallPhrenPath);
|
|
1959
|
-
try {
|
|
1960
|
-
if (filterAgentHooks(codexHooksFile, "command")) {
|
|
1961
|
-
log(` Removed phren entries from Codex hooks (${codexHooksFile})`);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
catch (err) {
|
|
1965
|
-
debugLog(`uninstall: cleanup failed for ${codexHooksFile}: ${errorMessage(err)}`);
|
|
1966
|
-
}
|
|
1967
|
-
// Remove session wrapper scripts (written by installSessionWrapper) and CLI wrapper
|
|
1968
|
-
const localBinDir = path.join(home, ".local", "bin");
|
|
1969
|
-
for (const tool of ["copilot", "cursor", "codex", "phren"]) {
|
|
1970
|
-
const wrapperPath = path.join(localBinDir, tool);
|
|
1971
|
-
try {
|
|
1972
|
-
if (fs.existsSync(wrapperPath)) {
|
|
1973
|
-
// Only remove if it's a phren wrapper (check for PHREN_PATH marker)
|
|
1974
|
-
const content = fs.readFileSync(wrapperPath, "utf8");
|
|
1975
|
-
if (content.includes("PHREN_PATH") && content.includes("phren")) {
|
|
1976
|
-
fs.unlinkSync(wrapperPath);
|
|
1977
|
-
log(` Removed ${tool} session wrapper (${wrapperPath})`);
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
catch (err) {
|
|
1982
|
-
debugLog(`uninstall: cleanup failed for ${wrapperPath}: ${errorMessage(err)}`);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
try {
|
|
1986
|
-
if (fs.existsSync(machineFile)) {
|
|
1987
|
-
fs.unlinkSync(machineFile);
|
|
1988
|
-
log(` Removed machine alias (${machineFile})`);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
catch (err) {
|
|
1992
|
-
debugLog(`uninstall: cleanup failed for ${machineFile}: ${errorMessage(err)}`);
|
|
1993
|
-
}
|
|
1994
|
-
const contextFile = homePath(".phren-context.md");
|
|
1995
|
-
try {
|
|
1996
|
-
if (fs.existsSync(contextFile)) {
|
|
1997
|
-
fs.unlinkSync(contextFile);
|
|
1998
|
-
log(` Removed machine context file (${contextFile})`);
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
catch (err) {
|
|
2002
|
-
debugLog(`uninstall: cleanup failed for ${contextFile}: ${errorMessage(err)}`);
|
|
2003
|
-
}
|
|
2004
|
-
// Remove global CLAUDE.md symlink (created by linkGlobal -> ~/.claude/CLAUDE.md)
|
|
2005
|
-
const globalClaudeLink = homePath(".claude", "CLAUDE.md");
|
|
2006
|
-
try {
|
|
2007
|
-
if (fs.lstatSync(globalClaudeLink).isSymbolicLink()) {
|
|
2008
|
-
fs.unlinkSync(globalClaudeLink);
|
|
2009
|
-
log(` Removed global CLAUDE.md symlink (${globalClaudeLink})`);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
catch {
|
|
2013
|
-
// Does not exist or not a symlink — nothing to do
|
|
2014
|
-
}
|
|
2015
|
-
// Remove copilot-instructions.md symlink (created by linkGlobal -> ~/.github/copilot-instructions.md)
|
|
2016
|
-
const copilotInstrLink = homePath(".github", "copilot-instructions.md");
|
|
2017
|
-
try {
|
|
2018
|
-
if (fs.lstatSync(copilotInstrLink).isSymbolicLink()) {
|
|
2019
|
-
fs.unlinkSync(copilotInstrLink);
|
|
2020
|
-
log(` Removed copilot-instructions.md symlink (${copilotInstrLink})`);
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
catch {
|
|
2024
|
-
// Does not exist or not a symlink — nothing to do
|
|
2025
|
-
}
|
|
2026
|
-
// Sweep agent skill directories for symlinks pointing into the phren store
|
|
2027
|
-
if (phrenPath) {
|
|
2028
|
-
try {
|
|
2029
|
-
sweepSkillSymlinks(phrenPath);
|
|
2030
|
-
}
|
|
2031
|
-
catch (err) {
|
|
2032
|
-
debugLog(`uninstall: skill symlink sweep failed: ${errorMessage(err)}`);
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
if (phrenPath && fs.existsSync(phrenPath)) {
|
|
2036
|
-
try {
|
|
2037
|
-
fs.rmSync(phrenPath, { recursive: true, force: true });
|
|
2038
|
-
log(` Removed phren root (${phrenPath})`);
|
|
2039
|
-
}
|
|
2040
|
-
catch (err) {
|
|
2041
|
-
debugLog(`uninstall: cleanup failed for ${phrenPath}: ${errorMessage(err)}`);
|
|
2042
|
-
log(` Warning: could not remove phren root (${phrenPath})`);
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
if (shouldRemoveGlobalPackage) {
|
|
2046
|
-
uninstallCurrentGlobalPackage();
|
|
2047
|
-
}
|
|
2048
|
-
// Remove VS Code extension if installed
|
|
2049
|
-
try {
|
|
2050
|
-
const codeResult = execFileSync("code", ["--list-extensions"], {
|
|
2051
|
-
encoding: "utf8",
|
|
2052
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2053
|
-
timeout: 10_000,
|
|
2054
|
-
});
|
|
2055
|
-
const phrenExts = codeResult.split("\n").filter((ext) => ext.toLowerCase().includes("phren"));
|
|
2056
|
-
for (const ext of phrenExts) {
|
|
2057
|
-
const trimmed = ext.trim();
|
|
2058
|
-
if (!trimmed)
|
|
2059
|
-
continue;
|
|
2060
|
-
try {
|
|
2061
|
-
execFileSync("code", ["--uninstall-extension", trimmed], {
|
|
2062
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2063
|
-
timeout: 15_000,
|
|
2064
|
-
});
|
|
2065
|
-
log(` Removed VS Code extension (${trimmed})`);
|
|
2066
|
-
}
|
|
2067
|
-
catch (err) {
|
|
2068
|
-
debugLog(`uninstall: VS Code extension removal failed for ${trimmed}: ${errorMessage(err)}`);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
catch {
|
|
2073
|
-
// code CLI not available — skip
|
|
2074
|
-
}
|
|
2075
|
-
log(`\nPhren config, hooks, and installed data removed.`);
|
|
2076
|
-
log(`Restart your agent(s) to apply changes.\n`);
|
|
2077
|
-
}
|