@phren/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
package/mcp/dist/init.js
ADDED
|
@@ -0,0 +1,1730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI orchestrator for phren init, mcp-mode, hooks-mode, and uninstall.
|
|
3
|
+
* Delegates to focused helpers in init-config, init-setup, and init-preferences.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as crypto from "crypto";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { configureAllHooks } from "./hooks.js";
|
|
10
|
+
import { getMachineName, machineFilePath, persistMachineName } from "./machine-identity.js";
|
|
11
|
+
import { atomicWriteText, debugLog, isRecord, hookConfigPath, homeDir, homePath, expandHomePath, findPhrenPath, readRootManifest, writeRootManifest, } from "./shared.js";
|
|
12
|
+
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
13
|
+
import { codexJsonCandidates, copilotMcpCandidates, cursorMcpCandidates, vscodeMcpCandidates, } from "./provider-adapters.js";
|
|
14
|
+
export { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, resetVSCodeProbeCache, patchJsonFile, } from "./init-config.js";
|
|
15
|
+
export { getMcpEnabledPreference, setMcpEnabledPreference, getHooksEnabledPreference, setHooksEnabledPreference, } from "./init-preferences.js";
|
|
16
|
+
export { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode, getProjectOwnershipDefault, } from "./project-config.js";
|
|
17
|
+
export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, } from "./proactivity.js";
|
|
18
|
+
export { ensureGovernanceFiles, repairPreexistingInstall, runPostInitVerify, getVerifyOutcomeNote, listTemplates, detectProjectDir, isProjectTracked, ensureLocalGitRepo, resolvePreferredHomeDir, inferInitScaffoldFromRepo, } from "./init-setup.js";
|
|
19
|
+
// Imports from helpers (used internally in this file)
|
|
20
|
+
import { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, removeMcpServerAtPath, removeTomlMcpServer, isPhrenCommand, patchJsonFile, } from "./init-config.js";
|
|
21
|
+
import { getMcpEnabledPreference, getHooksEnabledPreference, setMcpEnabledPreference, setHooksEnabledPreference, writeInstallPreferences, writeGovernanceInstallPreferences, readInstallPreferences, } from "./init-preferences.js";
|
|
22
|
+
import { ensureGovernanceFiles, repairPreexistingInstall, runPostInitVerify, applyStarterTemplateUpdates, listTemplates, applyTemplate, ensureProjectScaffold, ensureLocalGitRepo, bootstrapFromExisting, ensureGitignoreEntry, upsertProjectEnvVar, updateMachinesYaml, detectProjectDir, isProjectTracked, inferInitScaffoldFromRepo, } from "./init-setup.js";
|
|
23
|
+
import { DEFAULT_PHREN_PATH, STARTER_DIR, VERSION, log, confirmPrompt } from "./init-shared.js";
|
|
24
|
+
import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, } from "./project-config.js";
|
|
25
|
+
import { getWorkflowPolicy, updateWorkflowPolicy } from "./shared-governance.js";
|
|
26
|
+
import { addProjectToProfile } from "./profile-store.js";
|
|
27
|
+
function parseVersion(version) {
|
|
28
|
+
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return { major: 0, minor: 0, patch: 0, pre: "" };
|
|
31
|
+
return {
|
|
32
|
+
major: Number.parseInt(match[1], 10) || 0,
|
|
33
|
+
minor: Number.parseInt(match[2], 10) || 0,
|
|
34
|
+
patch: Number.parseInt(match[3], 10) || 0,
|
|
35
|
+
pre: match[4] || "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compare two semver strings. Returns true when `current` is strictly newer
|
|
40
|
+
* than `previous`. Pre-release versions (e.g. 1.2.3-rc.1) sort before the
|
|
41
|
+
* corresponding release (1.2.3). Among pre-release tags, comparison is
|
|
42
|
+
* lexicographic.
|
|
43
|
+
*/
|
|
44
|
+
export function isVersionNewer(current, previous) {
|
|
45
|
+
if (!previous)
|
|
46
|
+
return false;
|
|
47
|
+
const c = parseVersion(current);
|
|
48
|
+
const p = parseVersion(previous);
|
|
49
|
+
if (c.major !== p.major)
|
|
50
|
+
return c.major > p.major;
|
|
51
|
+
if (c.minor !== p.minor)
|
|
52
|
+
return c.minor > p.minor;
|
|
53
|
+
if (c.patch !== p.patch)
|
|
54
|
+
return c.patch > p.patch;
|
|
55
|
+
if (c.pre && !p.pre)
|
|
56
|
+
return false;
|
|
57
|
+
if (!c.pre && p.pre)
|
|
58
|
+
return true;
|
|
59
|
+
return c.pre > p.pre;
|
|
60
|
+
}
|
|
61
|
+
export function parseMcpMode(raw) {
|
|
62
|
+
if (!raw)
|
|
63
|
+
return undefined;
|
|
64
|
+
const normalized = raw.trim().toLowerCase();
|
|
65
|
+
if (normalized === "on" || normalized === "off")
|
|
66
|
+
return normalized;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function normalizedBootstrapProjectName(projectPath) {
|
|
70
|
+
return path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
71
|
+
}
|
|
72
|
+
function getPendingBootstrapTarget(phrenPath, opts) {
|
|
73
|
+
const cwdProject = detectProjectDir(process.cwd(), phrenPath);
|
|
74
|
+
if (!cwdProject)
|
|
75
|
+
return null;
|
|
76
|
+
const projectName = normalizedBootstrapProjectName(cwdProject);
|
|
77
|
+
if (isProjectTracked(phrenPath, projectName))
|
|
78
|
+
return null;
|
|
79
|
+
return { path: cwdProject, mode: "detected" };
|
|
80
|
+
}
|
|
81
|
+
function parseLowConfidenceThreshold(raw, fallback) {
|
|
82
|
+
if (!raw)
|
|
83
|
+
return fallback;
|
|
84
|
+
const value = Number.parseFloat(raw.trim());
|
|
85
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
86
|
+
return fallback;
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function parseRiskySectionsAnswer(raw, fallback) {
|
|
90
|
+
if (!raw)
|
|
91
|
+
return [...fallback];
|
|
92
|
+
const aliases = {
|
|
93
|
+
review: "Review",
|
|
94
|
+
stale: "Stale",
|
|
95
|
+
conflict: "Conflicts",
|
|
96
|
+
conflicts: "Conflicts",
|
|
97
|
+
};
|
|
98
|
+
const parsed = raw
|
|
99
|
+
.split(/[,\s]+/)
|
|
100
|
+
.map((token) => aliases[token.trim().toLowerCase()])
|
|
101
|
+
.filter((section) => Boolean(section));
|
|
102
|
+
if (!parsed.length)
|
|
103
|
+
return [...fallback];
|
|
104
|
+
return Array.from(new Set(parsed));
|
|
105
|
+
}
|
|
106
|
+
function hasInstallMarkers(phrenPath) {
|
|
107
|
+
return fs.existsSync(phrenPath) && (fs.existsSync(path.join(phrenPath, "machines.yaml")) ||
|
|
108
|
+
fs.existsSync(path.join(phrenPath, ".governance")) ||
|
|
109
|
+
fs.existsSync(path.join(phrenPath, "global")));
|
|
110
|
+
}
|
|
111
|
+
function resolveInitPhrenPath(opts) {
|
|
112
|
+
const raw = opts._walkthroughStoragePath || (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
113
|
+
return path.resolve(expandHomePath(raw));
|
|
114
|
+
}
|
|
115
|
+
function detectRepoRootForStorage(phrenPath) {
|
|
116
|
+
return detectProjectDir(process.cwd(), phrenPath);
|
|
117
|
+
}
|
|
118
|
+
function withFallbackColors(style) {
|
|
119
|
+
return {
|
|
120
|
+
header: style?.header ?? ((text) => text),
|
|
121
|
+
success: style?.success ?? ((text) => text),
|
|
122
|
+
warning: style?.warning ?? ((text) => text),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function createWalkthroughStyle() {
|
|
126
|
+
try {
|
|
127
|
+
const chalkModule = await import(String("chalk"));
|
|
128
|
+
const chalkAny = chalkModule.default
|
|
129
|
+
?? chalkModule.chalk
|
|
130
|
+
?? chalkModule;
|
|
131
|
+
const chalk = chalkAny;
|
|
132
|
+
return withFallbackColors({
|
|
133
|
+
header: (text) => chalk.bold.cyan(text),
|
|
134
|
+
success: (text) => chalk.green(text),
|
|
135
|
+
warning: (text) => chalk.yellow(text),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return withFallbackColors();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function createWalkthroughPrompts() {
|
|
143
|
+
try {
|
|
144
|
+
const inquirerModule = await import(String("inquirer"));
|
|
145
|
+
const maybeFns = inquirerModule;
|
|
146
|
+
if (typeof maybeFns.input === "function"
|
|
147
|
+
&& typeof maybeFns.confirm === "function"
|
|
148
|
+
&& typeof maybeFns.select === "function") {
|
|
149
|
+
return {
|
|
150
|
+
input: async (message, initialValue) => (await maybeFns.input({ message, default: initialValue })).trim(),
|
|
151
|
+
confirm: async (message, defaultValue = false) => Boolean(await maybeFns.confirm({ message, default: defaultValue })),
|
|
152
|
+
select: async (message, choices, defaultValue) => maybeFns.select({
|
|
153
|
+
message,
|
|
154
|
+
choices: choices.map((choice) => ({ value: choice.value, name: choice.name, description: choice.description })),
|
|
155
|
+
default: defaultValue,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const prompt = maybeFns.default?.prompt ?? maybeFns.prompt;
|
|
160
|
+
if (typeof prompt === "function") {
|
|
161
|
+
return {
|
|
162
|
+
input: async (message, initialValue) => {
|
|
163
|
+
const answer = await prompt([{ type: "input", name: "value", message, default: initialValue }]);
|
|
164
|
+
return String(answer.value ?? "").trim();
|
|
165
|
+
},
|
|
166
|
+
confirm: async (message, defaultValue = false) => {
|
|
167
|
+
const answer = await prompt([{ type: "confirm", name: "value", message, default: defaultValue }]);
|
|
168
|
+
return Boolean(answer.value);
|
|
169
|
+
},
|
|
170
|
+
select: async (message, choices, defaultValue) => {
|
|
171
|
+
const answer = await prompt([{
|
|
172
|
+
type: "list",
|
|
173
|
+
name: "value",
|
|
174
|
+
message,
|
|
175
|
+
choices: choices.map((choice) => ({ value: choice.value, name: choice.name })),
|
|
176
|
+
default: defaultValue,
|
|
177
|
+
}]);
|
|
178
|
+
return String(answer.value);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// fallback below
|
|
185
|
+
}
|
|
186
|
+
const readline = await import("readline");
|
|
187
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
188
|
+
const ask = (message) => new Promise((resolve) => rl.question(message, resolve));
|
|
189
|
+
process.once("exit", () => rl.close());
|
|
190
|
+
return {
|
|
191
|
+
input: async (message, initialValue) => {
|
|
192
|
+
const prompt = initialValue ? `${message} (${initialValue}): ` : `${message}: `;
|
|
193
|
+
const answer = (await ask(prompt)).trim();
|
|
194
|
+
return answer || (initialValue ?? "");
|
|
195
|
+
},
|
|
196
|
+
confirm: async (message, defaultValue = false) => {
|
|
197
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
198
|
+
const answer = (await ask(`${message} ${suffix}: `)).trim().toLowerCase();
|
|
199
|
+
if (!answer)
|
|
200
|
+
return defaultValue;
|
|
201
|
+
return answer === "y" || answer === "yes";
|
|
202
|
+
},
|
|
203
|
+
select: async (message, choices, defaultValue) => {
|
|
204
|
+
log(`${message}`);
|
|
205
|
+
for (const [index, choice] of choices.entries()) {
|
|
206
|
+
log(` ${index + 1}. ${choice.name}`);
|
|
207
|
+
}
|
|
208
|
+
const selected = (await ask(`Select [1-${choices.length}]${defaultValue ? " (Enter for default)" : ""}: `)).trim();
|
|
209
|
+
if (!selected && defaultValue)
|
|
210
|
+
return defaultValue;
|
|
211
|
+
const idx = Number.parseInt(selected, 10) - 1;
|
|
212
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < choices.length)
|
|
213
|
+
return choices[idx].value;
|
|
214
|
+
return defaultValue ?? choices[0].value;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Interactive walkthrough for first-time init
|
|
219
|
+
async function runWalkthrough(phrenPath) {
|
|
220
|
+
const prompts = await createWalkthroughPrompts();
|
|
221
|
+
const style = await createWalkthroughStyle();
|
|
222
|
+
const divider = style.header("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
223
|
+
const printSection = (title) => {
|
|
224
|
+
log("");
|
|
225
|
+
log(divider);
|
|
226
|
+
log(style.header(title));
|
|
227
|
+
log(divider);
|
|
228
|
+
};
|
|
229
|
+
const printSummary = (items) => {
|
|
230
|
+
printSection("Configuration Summary");
|
|
231
|
+
for (const item of items) {
|
|
232
|
+
log(style.success(`✓ ${item}`));
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const { renderPhrenArt } = await import("./phren-art.js");
|
|
236
|
+
log("");
|
|
237
|
+
log(renderPhrenArt(" "));
|
|
238
|
+
log("");
|
|
239
|
+
printSection("Welcome");
|
|
240
|
+
log("Let's set up persistent memory for your AI agents.");
|
|
241
|
+
log("Every option can be changed later.\n");
|
|
242
|
+
printSection("Storage Location");
|
|
243
|
+
log("Where should phren store data?");
|
|
244
|
+
const storageChoice = await prompts.select("Storage location", [
|
|
245
|
+
{
|
|
246
|
+
value: "global",
|
|
247
|
+
name: "global (~/.phren/ - default, shared across projects)",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
value: "project",
|
|
251
|
+
name: "per-project (<repo>/.phren/ - scoped to this repo, add to .gitignore)",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
value: "custom",
|
|
255
|
+
name: "custom path",
|
|
256
|
+
},
|
|
257
|
+
], "global");
|
|
258
|
+
let storagePath = path.resolve(homePath(".phren"));
|
|
259
|
+
let storageRepoRoot;
|
|
260
|
+
if (storageChoice === "project") {
|
|
261
|
+
const repoRoot = detectRepoRootForStorage(phrenPath);
|
|
262
|
+
if (!repoRoot) {
|
|
263
|
+
throw new Error("Per-project storage requires running init from a repository directory.");
|
|
264
|
+
}
|
|
265
|
+
storageRepoRoot = repoRoot;
|
|
266
|
+
storagePath = path.join(repoRoot, ".phren");
|
|
267
|
+
}
|
|
268
|
+
else if (storageChoice === "custom") {
|
|
269
|
+
const customInput = await prompts.input("Custom phren path", phrenPath);
|
|
270
|
+
storagePath = path.resolve(expandHomePath(customInput || phrenPath));
|
|
271
|
+
}
|
|
272
|
+
printSection("Existing Phren");
|
|
273
|
+
log("If you've already set up phren on another machine, paste the git clone URL.");
|
|
274
|
+
log("Otherwise, leave blank.");
|
|
275
|
+
const cloneAnswer = await prompts.input("Clone URL (leave blank to skip)");
|
|
276
|
+
if (cloneAnswer) {
|
|
277
|
+
const cloneConfig = {
|
|
278
|
+
storageChoice,
|
|
279
|
+
storagePath,
|
|
280
|
+
storageRepoRoot,
|
|
281
|
+
machine: getMachineName(),
|
|
282
|
+
profile: "personal",
|
|
283
|
+
mcp: "on",
|
|
284
|
+
hooks: "on",
|
|
285
|
+
projectOwnershipDefault: "phren-managed",
|
|
286
|
+
findingsProactivity: "high",
|
|
287
|
+
taskProactivity: "high",
|
|
288
|
+
lowConfidenceThreshold: 0.7,
|
|
289
|
+
riskySections: ["Stale", "Conflicts"],
|
|
290
|
+
taskMode: "auto",
|
|
291
|
+
bootstrapCurrentProject: false,
|
|
292
|
+
ollamaEnabled: false,
|
|
293
|
+
autoCaptureEnabled: false,
|
|
294
|
+
semanticDedupEnabled: false,
|
|
295
|
+
semanticConflictEnabled: false,
|
|
296
|
+
findingSensitivity: "balanced",
|
|
297
|
+
cloneUrl: cloneAnswer,
|
|
298
|
+
domain: "software",
|
|
299
|
+
};
|
|
300
|
+
printSummary([
|
|
301
|
+
`Storage: ${storageChoice} (${storagePath})`,
|
|
302
|
+
`Existing memory clone: ${cloneAnswer}`,
|
|
303
|
+
`Machine: ${cloneConfig.machine}`,
|
|
304
|
+
`Profile: ${cloneConfig.profile}`,
|
|
305
|
+
"MCP: enabled",
|
|
306
|
+
"Hooks: enabled",
|
|
307
|
+
"Project ownership default: phren-managed",
|
|
308
|
+
"Task mode: auto",
|
|
309
|
+
"Domain: software",
|
|
310
|
+
]);
|
|
311
|
+
return cloneConfig;
|
|
312
|
+
}
|
|
313
|
+
const defaultMachine = getMachineName();
|
|
314
|
+
printSection("Identity");
|
|
315
|
+
const machine = await prompts.input("Machine name", defaultMachine);
|
|
316
|
+
const profile = await prompts.input("Profile name", "personal");
|
|
317
|
+
const repoForInference = detectProjectDir(process.cwd(), storagePath);
|
|
318
|
+
const inferredScaffold = repoForInference
|
|
319
|
+
? inferInitScaffoldFromRepo(repoForInference)
|
|
320
|
+
: null;
|
|
321
|
+
const inferredDomain = inferredScaffold?.domain ?? "software";
|
|
322
|
+
printSection("Project Domain");
|
|
323
|
+
log("What kind of project is this?");
|
|
324
|
+
if (repoForInference && inferredScaffold) {
|
|
325
|
+
log(`Detected repo signals from ${repoForInference} (${inferredScaffold.reason}).`);
|
|
326
|
+
if (inferredScaffold.referenceHints.length > 0) {
|
|
327
|
+
log(`Reference hints: ${inferredScaffold.referenceHints.join(", ")}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Use inferred domain directly — adaptive init derives domain from repo content.
|
|
331
|
+
// Only ask if inference was weak (fell back to default "software" with no signals).
|
|
332
|
+
let domain = inferredDomain;
|
|
333
|
+
if (inferredDomain === "software" && !inferredScaffold) {
|
|
334
|
+
domain = await prompts.select("Project domain", [
|
|
335
|
+
{ value: "software", name: "software" },
|
|
336
|
+
{ value: "research", name: "research" },
|
|
337
|
+
{ value: "creative", name: "creative" },
|
|
338
|
+
{ value: "other", name: "other" },
|
|
339
|
+
], "software");
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
log(`Domain: ${inferredDomain} (inferred from project content)`);
|
|
343
|
+
}
|
|
344
|
+
printSection("Project Ownership");
|
|
345
|
+
log("Choose who owns repo-facing instruction files for projects you add.");
|
|
346
|
+
log(" phren-managed: Phren may mirror CLAUDE.md / AGENTS.md into the repo");
|
|
347
|
+
log(" detached: Phren keeps its own docs but does not write into the repo");
|
|
348
|
+
log(" repo-managed: keep the repo's existing CLAUDE/AGENTS files as canonical");
|
|
349
|
+
log(" Change later: npx phren config project-ownership <mode>");
|
|
350
|
+
const projectOwnershipDefault = await prompts.select("Default project ownership", [
|
|
351
|
+
{ value: "detached", name: "detached (default)" },
|
|
352
|
+
{ value: "phren-managed", name: "phren-managed" },
|
|
353
|
+
{ value: "repo-managed", name: "repo-managed" },
|
|
354
|
+
], "detached");
|
|
355
|
+
printSection("MCP");
|
|
356
|
+
log("MCP mode registers phren as a tool server so your AI agent can call it");
|
|
357
|
+
log("directly: search memory, manage tasks, save findings, etc.");
|
|
358
|
+
log(" Recommended for: Claude Code, Cursor, Copilot CLI, Codex");
|
|
359
|
+
log(" Alternative: hooks-only mode (read-only context injection, any agent)");
|
|
360
|
+
log(" Change later: npx phren mcp-mode on|off");
|
|
361
|
+
const mcp = (await prompts.confirm("Enable MCP?", true)) ? "on" : "off";
|
|
362
|
+
printSection("Hooks");
|
|
363
|
+
log("Hooks run shell commands at session start, prompt submit, and session end.");
|
|
364
|
+
log(" - SessionStart: git pull (keeps memory in sync across machines)");
|
|
365
|
+
log(" - UserPromptSubmit: searches phren and injects relevant context");
|
|
366
|
+
log(" - Stop: commits and pushes any new findings after each response");
|
|
367
|
+
log(" What they touch: ~/.claude/settings.json (hooks section only)");
|
|
368
|
+
log(" Change later: npx phren hooks-mode on|off");
|
|
369
|
+
const hooks = (await prompts.confirm("Enable hooks?", true)) ? "on" : "off";
|
|
370
|
+
printSection("Semantic Search (Optional)");
|
|
371
|
+
log("Phren can use a local embedding model for semantic (fuzzy) search via Ollama.");
|
|
372
|
+
log(" Best fit: paraphrase-heavy or weak-lexical queries.");
|
|
373
|
+
log(" Skip it if you mostly search by filenames, symbols, commands, or exact phrases.");
|
|
374
|
+
log(" - Model: nomic-embed-text (274 MB, one-time download)");
|
|
375
|
+
log(" - Ollama runs locally, no cloud, no cost");
|
|
376
|
+
log(" - Falls back to FTS5 keyword search if disabled or unavailable");
|
|
377
|
+
log(" Change later: set PHREN_OLLAMA_URL=off to disable");
|
|
378
|
+
let ollamaEnabled = false;
|
|
379
|
+
try {
|
|
380
|
+
const { checkOllamaAvailable, checkModelAvailable, getOllamaUrl } = await import("./shared-ollama.js");
|
|
381
|
+
if (getOllamaUrl()) {
|
|
382
|
+
const ollamaUp = await checkOllamaAvailable();
|
|
383
|
+
if (ollamaUp) {
|
|
384
|
+
const modelReady = await checkModelAvailable();
|
|
385
|
+
if (modelReady) {
|
|
386
|
+
log(" Ollama detected with nomic-embed-text ready.");
|
|
387
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery?", false);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
log(" Ollama detected, but nomic-embed-text is not pulled yet.");
|
|
391
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery? (will pull nomic-embed-text)", false);
|
|
392
|
+
if (ollamaEnabled) {
|
|
393
|
+
log(" Run after init: ollama pull nomic-embed-text");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
log(" Ollama not detected. Install it to enable semantic search:");
|
|
399
|
+
log(" https://ollama.com → then: ollama pull nomic-embed-text");
|
|
400
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search (Ollama not installed yet)?", false);
|
|
401
|
+
if (ollamaEnabled) {
|
|
402
|
+
log(style.success(" Semantic search enabled — will activate once Ollama is running."));
|
|
403
|
+
log(" To disable: set PHREN_OLLAMA_URL=off in your shell profile");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
410
|
+
process.stderr.write(`[phren] init ollamaCheck: ${errorMessage(err)}\n`);
|
|
411
|
+
}
|
|
412
|
+
printSection("Auto-Capture (Optional)");
|
|
413
|
+
log("After each session, phren scans the conversation for insight-signal phrases");
|
|
414
|
+
log("(\"always\", \"never\", \"pitfall\", \"gotcha\", etc.) and saves them automatically.");
|
|
415
|
+
log(" - Runs silently in the Stop hook; captured findings go to FINDINGS.md");
|
|
416
|
+
log(" - You can review and remove any auto-captured entry at any time");
|
|
417
|
+
log(" - Can be toggled: set PHREN_FEATURE_AUTO_CAPTURE=0 to disable");
|
|
418
|
+
const autoCaptureEnabled = await prompts.confirm("Enable auto-capture?", true);
|
|
419
|
+
let findingsProactivity = "high";
|
|
420
|
+
if (autoCaptureEnabled) {
|
|
421
|
+
log(" Findings capture level controls how eager phren is to save lessons automatically.");
|
|
422
|
+
log(" Change later: npx phren config proactivity.findings <high|medium|low>");
|
|
423
|
+
findingsProactivity = await prompts.select("Findings capture level", [
|
|
424
|
+
{ value: "high", name: "high (recommended)" },
|
|
425
|
+
{ value: "medium", name: "medium" },
|
|
426
|
+
{ value: "low", name: "low" },
|
|
427
|
+
], "high");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
findingsProactivity = "low";
|
|
431
|
+
}
|
|
432
|
+
printSection("Task Management");
|
|
433
|
+
log("Choose how phren handles tasks as you work.");
|
|
434
|
+
log(" auto (recommended): captures tasks naturally as you work, links findings to tasks");
|
|
435
|
+
log(" suggest: proposes tasks but waits for approval before writing");
|
|
436
|
+
log(" manual: tasks are fully manual — you add them yourself");
|
|
437
|
+
log(" off: never touch tasks automatically");
|
|
438
|
+
log(" Change later: npx phren config workflow set --taskMode=<mode>");
|
|
439
|
+
const taskMode = await prompts.select("Task mode", [
|
|
440
|
+
{ value: "auto", name: "auto (recommended)" },
|
|
441
|
+
{ value: "suggest", name: "suggest" },
|
|
442
|
+
{ value: "manual", name: "manual" },
|
|
443
|
+
{ value: "off", name: "off" },
|
|
444
|
+
], "auto");
|
|
445
|
+
let taskProactivity = "high";
|
|
446
|
+
if (taskMode === "auto" || taskMode === "suggest") {
|
|
447
|
+
log(" Task proactivity controls how much evidence phren needs before capturing tasks.");
|
|
448
|
+
log(" high (recommended): captures tasks as they come up naturally");
|
|
449
|
+
log(" medium: only when you explicitly mention a task");
|
|
450
|
+
log(" low: minimal auto-capture");
|
|
451
|
+
log(" Change later: npx phren config proactivity.tasks <high|medium|low>");
|
|
452
|
+
taskProactivity = await prompts.select("Task proactivity", [
|
|
453
|
+
{ value: "high", name: "high (recommended)" },
|
|
454
|
+
{ value: "medium", name: "medium" },
|
|
455
|
+
{ value: "low", name: "low" },
|
|
456
|
+
], "high");
|
|
457
|
+
}
|
|
458
|
+
printSection("Workflow Guardrails");
|
|
459
|
+
log("Choose how strict review gates should be for risky or low-confidence writes.");
|
|
460
|
+
log(" lowConfidenceThreshold: confidence cutoff used to mark writes as risky");
|
|
461
|
+
log(" riskySections: sections always treated as risky");
|
|
462
|
+
log(" Change later: npx phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
|
|
463
|
+
const thresholdAnswer = await prompts.input("Low-confidence threshold [0.0-1.0]", "0.7");
|
|
464
|
+
const lowConfidenceThreshold = parseLowConfidenceThreshold(thresholdAnswer, 0.7);
|
|
465
|
+
const riskySectionsAnswer = await prompts.input("Risky sections [Review,Stale,Conflicts]", "Stale,Conflicts");
|
|
466
|
+
const riskySections = parseRiskySectionsAnswer(riskySectionsAnswer, ["Stale", "Conflicts"]);
|
|
467
|
+
// Only offer semantic dedup/conflict when an LLM endpoint is explicitly configured.
|
|
468
|
+
// These features call /chat/completions, not an embedding endpoint, so we gate on
|
|
469
|
+
// PHREN_LLM_ENDPOINT (primary) or the presence of a known API key as a fallback.
|
|
470
|
+
// PHREN_EMBEDDING_API_URL alone is NOT sufficient — it only enables embeddings,
|
|
471
|
+
// not the LLM chat call that callLlm() makes.
|
|
472
|
+
const hasLlmApi = Boolean((process.env.PHREN_LLM_ENDPOINT) ||
|
|
473
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
474
|
+
process.env.OPENAI_API_KEY);
|
|
475
|
+
let semanticDedupEnabled = false;
|
|
476
|
+
let semanticConflictEnabled = false;
|
|
477
|
+
if (hasLlmApi) {
|
|
478
|
+
printSection("LLM-Powered Memory Quality (Optional)");
|
|
479
|
+
log("Phren can use an LLM to catch near-duplicate or conflicting findings.");
|
|
480
|
+
log(" Requires: PHREN_LLM_ENDPOINT or ANTHROPIC_API_KEY/OPENAI_API_KEY set");
|
|
481
|
+
log("");
|
|
482
|
+
log("Semantic dedup: before saving a finding, ask the LLM whether it means the");
|
|
483
|
+
log("same thing as an existing one (catches same idea with different wording).");
|
|
484
|
+
semanticDedupEnabled = await prompts.confirm("Enable LLM-powered duplicate detection?", false);
|
|
485
|
+
log("");
|
|
486
|
+
log("Conflict detection: after saving a finding, check whether it contradicts an");
|
|
487
|
+
log("existing one (e.g. \"always use X\" vs \"never use X\"). Adds an inline annotation.");
|
|
488
|
+
semanticConflictEnabled = await prompts.confirm("Enable LLM-powered conflict detection?", false);
|
|
489
|
+
if (semanticDedupEnabled || semanticConflictEnabled) {
|
|
490
|
+
const currentModel = (process.env.PHREN_LLM_MODEL) || "gpt-4o-mini / claude-haiku-4-5-20251001 (default)";
|
|
491
|
+
log("");
|
|
492
|
+
log(" Cost note: each semantic check is ~80 input + ~5 output tokens, cached 24h.");
|
|
493
|
+
log(` Current model: ${currentModel}`);
|
|
494
|
+
const llmModel = (process.env.PHREN_LLM_MODEL);
|
|
495
|
+
const isExpensive = llmModel && /opus|sonnet|gpt-4(?!o-mini)/i.test(llmModel);
|
|
496
|
+
if (isExpensive) {
|
|
497
|
+
log(style.warning(` Warning: ${llmModel} is 20x more expensive than Haiku for yes/no checks.`));
|
|
498
|
+
log(" Consider: PHREN_LLM_MODEL=claude-haiku-4-5-20251001");
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
log(" With Haiku: fractions of a cent/session. With Opus: ~$0.20/session for heavy use.");
|
|
502
|
+
log(" Tip: set PHREN_LLM_MODEL=claude-haiku-4-5-20251001 to keep costs low.");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
printSection("Finding Sensitivity");
|
|
507
|
+
log("Controls how eagerly agents save findings to memory.");
|
|
508
|
+
log(" minimal — only when you explicitly ask");
|
|
509
|
+
log(" conservative — decisions and pitfalls only");
|
|
510
|
+
log(" balanced — non-obvious patterns, decisions, pitfalls, bugs (recommended)");
|
|
511
|
+
log(" aggressive — everything worth remembering, err on the side of capturing");
|
|
512
|
+
log(" Change later: npx phren config finding-sensitivity <level>");
|
|
513
|
+
const findingSensitivity = await prompts.select("Finding sensitivity", [
|
|
514
|
+
{ value: "balanced", name: "balanced (recommended)" },
|
|
515
|
+
{ value: "conservative", name: "conservative" },
|
|
516
|
+
{ value: "minimal", name: "minimal" },
|
|
517
|
+
{ value: "aggressive", name: "aggressive" },
|
|
518
|
+
], "balanced");
|
|
519
|
+
printSection("GitHub Sync");
|
|
520
|
+
log(`Phren stores memory as plain Markdown files in a git repo (${storagePath}).`);
|
|
521
|
+
log("Push it to a private GitHub repo to sync memory across machines.");
|
|
522
|
+
log(" Hooks will auto-commit + push after every session and pull on start.");
|
|
523
|
+
log(" Skip this if you just want to try phren locally first.");
|
|
524
|
+
const githubAnswer = await prompts.input("GitHub username (leave blank to skip)");
|
|
525
|
+
const githubUsername = githubAnswer || undefined;
|
|
526
|
+
let githubRepo;
|
|
527
|
+
if (githubUsername) {
|
|
528
|
+
const repoAnswer = await prompts.input("Repo name", "my-phren");
|
|
529
|
+
githubRepo = repoAnswer || "my-phren";
|
|
530
|
+
}
|
|
531
|
+
let bootstrapCurrentProject = false;
|
|
532
|
+
let bootstrapOwnership;
|
|
533
|
+
const detectedProject = detectProjectDir(process.cwd(), storagePath);
|
|
534
|
+
if (detectedProject) {
|
|
535
|
+
const detectedProjectName = path.basename(detectedProject);
|
|
536
|
+
printSection("Current Project");
|
|
537
|
+
log(`Detected project: ${detectedProjectName}`);
|
|
538
|
+
bootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
539
|
+
if (!bootstrapCurrentProject) {
|
|
540
|
+
bootstrapCurrentProject = false;
|
|
541
|
+
log(style.warning(` Skipped. Later: cd ${detectedProject} && npx phren add`));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
|
545
|
+
{ value: projectOwnershipDefault, name: `${projectOwnershipDefault} (default)` },
|
|
546
|
+
...PROJECT_OWNERSHIP_MODES
|
|
547
|
+
.filter((mode) => mode !== projectOwnershipDefault)
|
|
548
|
+
.map((mode) => ({ value: mode, name: mode })),
|
|
549
|
+
], projectOwnershipDefault);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const summaryItems = [
|
|
553
|
+
`Storage: ${storageChoice} (${storagePath})`,
|
|
554
|
+
`Machine: ${machine}`,
|
|
555
|
+
`Profile: ${profile}`,
|
|
556
|
+
`Domain: ${domain}`,
|
|
557
|
+
`Project ownership default: ${projectOwnershipDefault}`,
|
|
558
|
+
`MCP: ${mcp === "on" ? "enabled" : "disabled"}`,
|
|
559
|
+
`Hooks: ${hooks === "on" ? "enabled" : "disabled"}`,
|
|
560
|
+
`Auto-capture: ${autoCaptureEnabled ? "enabled" : "disabled"}`,
|
|
561
|
+
`Findings capture level: ${findingsProactivity}`,
|
|
562
|
+
`Task mode: ${taskMode}`,
|
|
563
|
+
`Task proactivity: ${taskProactivity}`,
|
|
564
|
+
`Low-confidence threshold: ${lowConfidenceThreshold}`,
|
|
565
|
+
`Risky sections: ${riskySections.join(", ")}`,
|
|
566
|
+
`Finding sensitivity: ${findingSensitivity}`,
|
|
567
|
+
`Semantic search: ${ollamaEnabled ? "enabled" : "disabled"}`,
|
|
568
|
+
`Semantic dedup: ${semanticDedupEnabled ? "enabled" : "disabled"}`,
|
|
569
|
+
`Semantic conflict detection: ${semanticConflictEnabled ? "enabled" : "disabled"}`,
|
|
570
|
+
`GitHub sync: ${githubUsername ? `${githubUsername}/${githubRepo ?? "my-phren"}` : "skipped"}`,
|
|
571
|
+
`Add detected project: ${bootstrapCurrentProject ? `yes (${bootstrapOwnership ?? projectOwnershipDefault})` : "no"}`,
|
|
572
|
+
];
|
|
573
|
+
if (inferredScaffold) {
|
|
574
|
+
summaryItems.push(`Inference: ${inferredScaffold.reason}`);
|
|
575
|
+
}
|
|
576
|
+
printSummary(summaryItems);
|
|
577
|
+
return {
|
|
578
|
+
storageChoice,
|
|
579
|
+
storagePath,
|
|
580
|
+
storageRepoRoot,
|
|
581
|
+
machine,
|
|
582
|
+
profile,
|
|
583
|
+
mcp,
|
|
584
|
+
hooks,
|
|
585
|
+
projectOwnershipDefault,
|
|
586
|
+
findingsProactivity,
|
|
587
|
+
taskProactivity,
|
|
588
|
+
lowConfidenceThreshold,
|
|
589
|
+
riskySections,
|
|
590
|
+
taskMode,
|
|
591
|
+
bootstrapCurrentProject,
|
|
592
|
+
bootstrapOwnership,
|
|
593
|
+
ollamaEnabled,
|
|
594
|
+
autoCaptureEnabled,
|
|
595
|
+
semanticDedupEnabled,
|
|
596
|
+
semanticConflictEnabled,
|
|
597
|
+
findingSensitivity,
|
|
598
|
+
githubUsername,
|
|
599
|
+
githubRepo,
|
|
600
|
+
domain,
|
|
601
|
+
inferredScaffold: inferredScaffold
|
|
602
|
+
? (domain === inferredScaffold.domain
|
|
603
|
+
? inferredScaffold
|
|
604
|
+
: { ...inferredScaffold, domain, topics: [] })
|
|
605
|
+
: undefined,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
export async function warmSemanticSearch(phrenPath, profile) {
|
|
609
|
+
const { checkOllamaAvailable, checkModelAvailable, getOllamaUrl, getEmbeddingModel } = await import("./shared-ollama.js");
|
|
610
|
+
const ollamaUrl = getOllamaUrl();
|
|
611
|
+
if (!ollamaUrl)
|
|
612
|
+
return "Semantic search: disabled.";
|
|
613
|
+
const model = getEmbeddingModel();
|
|
614
|
+
if (!await checkOllamaAvailable()) {
|
|
615
|
+
return `Semantic search not warmed: Ollama offline at ${ollamaUrl}.`;
|
|
616
|
+
}
|
|
617
|
+
if (!await checkModelAvailable()) {
|
|
618
|
+
return `Semantic search not warmed: model ${model} is not pulled yet.`;
|
|
619
|
+
}
|
|
620
|
+
const { buildIndex, listIndexedDocumentPaths } = await import("./shared-index.js");
|
|
621
|
+
const { getEmbeddingCache, formatEmbeddingCoverage } = await import("./shared-embedding-cache.js");
|
|
622
|
+
const { backgroundEmbedMissingDocs } = await import("./startup-embedding.js");
|
|
623
|
+
const { getPersistentVectorIndex } = await import("./shared-vector-index.js");
|
|
624
|
+
const db = await buildIndex(phrenPath, profile);
|
|
625
|
+
try {
|
|
626
|
+
const cache = getEmbeddingCache(phrenPath);
|
|
627
|
+
await cache.load().catch(() => { });
|
|
628
|
+
const allPaths = listIndexedDocumentPaths(phrenPath, profile);
|
|
629
|
+
const before = cache.coverage(allPaths);
|
|
630
|
+
if (before.missing > 0) {
|
|
631
|
+
await backgroundEmbedMissingDocs(db, cache);
|
|
632
|
+
}
|
|
633
|
+
await cache.load().catch(() => { });
|
|
634
|
+
const after = cache.coverage(allPaths);
|
|
635
|
+
if (cache.size() > 0) {
|
|
636
|
+
getPersistentVectorIndex(phrenPath).ensure(cache.getAllEntries());
|
|
637
|
+
}
|
|
638
|
+
if (after.total === 0) {
|
|
639
|
+
return `Semantic search ready (${model}), but there are no indexed docs yet.`;
|
|
640
|
+
}
|
|
641
|
+
const embeddedNow = Math.max(0, after.embedded - before.embedded);
|
|
642
|
+
const prefix = after.state === "warm" ? "Semantic search warmed" : "Semantic search warming";
|
|
643
|
+
const delta = embeddedNow > 0 ? `; embedded ${embeddedNow} new docs during init` : "";
|
|
644
|
+
return `${prefix}: ${model}, ${formatEmbeddingCoverage(after)}${delta}.`;
|
|
645
|
+
}
|
|
646
|
+
finally {
|
|
647
|
+
try {
|
|
648
|
+
db.close();
|
|
649
|
+
}
|
|
650
|
+
catch { /* ignore close errors in init */ }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function applyOnboardingPreferences(phrenPath, opts) {
|
|
654
|
+
if (opts.projectOwnershipDefault) {
|
|
655
|
+
writeInstallPreferences(phrenPath, { projectOwnershipDefault: opts.projectOwnershipDefault });
|
|
656
|
+
}
|
|
657
|
+
const runtimePatch = {};
|
|
658
|
+
if (opts.findingsProactivity)
|
|
659
|
+
runtimePatch.proactivityFindings = opts.findingsProactivity;
|
|
660
|
+
if (opts.taskProactivity)
|
|
661
|
+
runtimePatch.proactivityTask = opts.taskProactivity;
|
|
662
|
+
if (Object.keys(runtimePatch).length > 0) {
|
|
663
|
+
writeInstallPreferences(phrenPath, runtimePatch);
|
|
664
|
+
}
|
|
665
|
+
const governancePatch = {};
|
|
666
|
+
if (opts.findingsProactivity)
|
|
667
|
+
governancePatch.proactivityFindings = opts.findingsProactivity;
|
|
668
|
+
if (opts.taskProactivity)
|
|
669
|
+
governancePatch.proactivityTask = opts.taskProactivity;
|
|
670
|
+
if (Object.keys(governancePatch).length > 0) {
|
|
671
|
+
writeGovernanceInstallPreferences(phrenPath, governancePatch);
|
|
672
|
+
}
|
|
673
|
+
const workflowPatch = {};
|
|
674
|
+
if (typeof opts.lowConfidenceThreshold === "number")
|
|
675
|
+
workflowPatch.lowConfidenceThreshold = opts.lowConfidenceThreshold;
|
|
676
|
+
if (Array.isArray(opts.riskySections))
|
|
677
|
+
workflowPatch.riskySections = opts.riskySections;
|
|
678
|
+
if (opts.taskMode)
|
|
679
|
+
workflowPatch.taskMode = opts.taskMode;
|
|
680
|
+
if (opts.findingSensitivity)
|
|
681
|
+
workflowPatch.findingSensitivity = opts.findingSensitivity;
|
|
682
|
+
if (Object.keys(workflowPatch).length > 0) {
|
|
683
|
+
updateWorkflowPolicy(phrenPath, workflowPatch);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function writeWalkthroughEnvDefaults(phrenPath, opts) {
|
|
687
|
+
const envFile = path.join(phrenPath, ".env");
|
|
688
|
+
let envContent = fs.existsSync(envFile) ? fs.readFileSync(envFile, "utf8") : "# phren feature flags — generated by init\n";
|
|
689
|
+
const envFlags = [];
|
|
690
|
+
const autoCaptureChoice = opts._walkthroughAutoCapture;
|
|
691
|
+
const hasAutoCaptureFlag = /^\s*PHREN_FEATURE_AUTO_CAPTURE=.*$/m.test(envContent);
|
|
692
|
+
if (typeof autoCaptureChoice === "boolean") {
|
|
693
|
+
envFlags.push({
|
|
694
|
+
flag: `PHREN_FEATURE_AUTO_CAPTURE=${autoCaptureChoice ? "1" : "0"}`,
|
|
695
|
+
label: `Auto-capture ${autoCaptureChoice ? "enabled" : "disabled"}`,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
else if (autoCaptureChoice !== false && !hasAutoCaptureFlag) {
|
|
699
|
+
// Default to enabled on fresh installs and non-walkthrough init.
|
|
700
|
+
envFlags.push({ flag: "PHREN_FEATURE_AUTO_CAPTURE=1", label: "Auto-capture enabled" });
|
|
701
|
+
}
|
|
702
|
+
if (opts._walkthroughSemanticDedup)
|
|
703
|
+
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_DEDUP=1", label: "Semantic dedup" });
|
|
704
|
+
if (opts._walkthroughSemanticConflict)
|
|
705
|
+
envFlags.push({ flag: "PHREN_FEATURE_SEMANTIC_CONFLICT=1", label: "Conflict detection" });
|
|
706
|
+
if (envFlags.length === 0)
|
|
707
|
+
return [];
|
|
708
|
+
let changed = false;
|
|
709
|
+
const enabledLabels = [];
|
|
710
|
+
for (const { flag, label } of envFlags) {
|
|
711
|
+
const key = flag.split("=")[0];
|
|
712
|
+
const lineRe = new RegExp(`^\\s*${key}=.*$`, "m");
|
|
713
|
+
if (lineRe.test(envContent)) {
|
|
714
|
+
const before = envContent;
|
|
715
|
+
envContent = envContent.replace(lineRe, flag);
|
|
716
|
+
if (envContent !== before) {
|
|
717
|
+
changed = true;
|
|
718
|
+
enabledLabels.push(label);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
if (!envContent.endsWith("\n"))
|
|
723
|
+
envContent += "\n";
|
|
724
|
+
envContent += `${flag}\n`;
|
|
725
|
+
changed = true;
|
|
726
|
+
enabledLabels.push(label);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (changed) {
|
|
730
|
+
const tmpPath = `${envFile}.tmp-${crypto.randomUUID()}`;
|
|
731
|
+
fs.writeFileSync(tmpPath, envContent);
|
|
732
|
+
fs.renameSync(tmpPath, envFile);
|
|
733
|
+
}
|
|
734
|
+
return enabledLabels.map((label) => `${label} (${envFile})`);
|
|
735
|
+
}
|
|
736
|
+
function collectRepairedAssetLabels(repaired) {
|
|
737
|
+
const repairedAssets = [];
|
|
738
|
+
if (repaired.createdContextFile)
|
|
739
|
+
repairedAssets.push("~/.phren-context.md");
|
|
740
|
+
if (repaired.createdRootMemory)
|
|
741
|
+
repairedAssets.push("generated MEMORY.md");
|
|
742
|
+
repairedAssets.push(...repaired.createdGlobalAssets);
|
|
743
|
+
repairedAssets.push(...repaired.createdRuntimeAssets);
|
|
744
|
+
repairedAssets.push(...repaired.createdFeatureDefaults);
|
|
745
|
+
repairedAssets.push(...repaired.createdSkillArtifacts);
|
|
746
|
+
return repairedAssets;
|
|
747
|
+
}
|
|
748
|
+
function applyProjectStorageBindings(repoRoot, phrenPath) {
|
|
749
|
+
const updates = [];
|
|
750
|
+
if (ensureGitignoreEntry(repoRoot, ".phren/")) {
|
|
751
|
+
updates.push(`${path.join(repoRoot, ".gitignore")} (.phren/)`);
|
|
752
|
+
}
|
|
753
|
+
if (upsertProjectEnvVar(repoRoot, "PHREN_PATH", phrenPath)) {
|
|
754
|
+
updates.push(`${path.join(repoRoot, ".env")} (PHREN_PATH=${phrenPath})`);
|
|
755
|
+
}
|
|
756
|
+
return updates;
|
|
757
|
+
}
|
|
758
|
+
async function runProjectLocalInit(opts = {}) {
|
|
759
|
+
const detectedRoot = detectProjectDir(process.cwd(), path.join(process.cwd(), ".phren")) || process.cwd();
|
|
760
|
+
const hasWorkspaceMarker = fs.existsSync(path.join(detectedRoot, ".git")) ||
|
|
761
|
+
fs.existsSync(path.join(detectedRoot, "CLAUDE.md")) ||
|
|
762
|
+
fs.existsSync(path.join(detectedRoot, "AGENTS.md")) ||
|
|
763
|
+
fs.existsSync(path.join(detectedRoot, ".claude", "CLAUDE.md"));
|
|
764
|
+
if (!hasWorkspaceMarker) {
|
|
765
|
+
throw new Error("project-local mode must be run inside a repo or project root");
|
|
766
|
+
}
|
|
767
|
+
const workspaceRoot = path.resolve(detectedRoot);
|
|
768
|
+
const phrenPath = path.join(workspaceRoot, ".phren");
|
|
769
|
+
const existingManifest = readRootManifest(phrenPath);
|
|
770
|
+
if (existingManifest && existingManifest.installMode !== "project-local") {
|
|
771
|
+
throw new Error(`Refusing to reuse non-local phren root at ${phrenPath}`);
|
|
772
|
+
}
|
|
773
|
+
const ownershipDefault = opts.projectOwnershipDefault
|
|
774
|
+
?? (existingManifest ? getProjectOwnershipDefault(phrenPath) : "detached");
|
|
775
|
+
if (!existingManifest && !opts.projectOwnershipDefault) {
|
|
776
|
+
opts.projectOwnershipDefault = ownershipDefault;
|
|
777
|
+
}
|
|
778
|
+
const mcpEnabled = opts.mcp ? opts.mcp === "on" : true;
|
|
779
|
+
const projectName = path.basename(workspaceRoot).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
780
|
+
if (opts.dryRun) {
|
|
781
|
+
log("\nInit dry run. No files will be written.\n");
|
|
782
|
+
log(`Mode: project-local`);
|
|
783
|
+
log(`Workspace root: ${workspaceRoot}`);
|
|
784
|
+
log(`Phren root: ${phrenPath}`);
|
|
785
|
+
log(`Project: ${projectName}`);
|
|
786
|
+
log(`VS Code workspace MCP: ${mcpEnabled ? "on" : "off"}`);
|
|
787
|
+
log(`Hooks: unsupported in project-local mode`);
|
|
788
|
+
log("");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
fs.mkdirSync(phrenPath, { recursive: true });
|
|
792
|
+
writeRootManifest(phrenPath, {
|
|
793
|
+
version: 1,
|
|
794
|
+
installMode: "project-local",
|
|
795
|
+
syncMode: "workspace-git",
|
|
796
|
+
workspaceRoot,
|
|
797
|
+
primaryProject: projectName,
|
|
798
|
+
});
|
|
799
|
+
ensureGovernanceFiles(phrenPath);
|
|
800
|
+
repairPreexistingInstall(phrenPath);
|
|
801
|
+
fs.mkdirSync(path.join(phrenPath, "global", "skills"), { recursive: true });
|
|
802
|
+
fs.mkdirSync(path.join(phrenPath, ".runtime"), { recursive: true });
|
|
803
|
+
fs.mkdirSync(path.join(phrenPath, ".sessions"), { recursive: true });
|
|
804
|
+
if (!fs.existsSync(path.join(phrenPath, ".gitignore"))) {
|
|
805
|
+
atomicWriteText(path.join(phrenPath, ".gitignore"), [
|
|
806
|
+
".runtime/",
|
|
807
|
+
".sessions/",
|
|
808
|
+
"*.lock",
|
|
809
|
+
"*.tmp-*",
|
|
810
|
+
"",
|
|
811
|
+
].join("\n"));
|
|
812
|
+
}
|
|
813
|
+
if (!fs.existsSync(path.join(phrenPath, "global", "CLAUDE.md"))) {
|
|
814
|
+
atomicWriteText(path.join(phrenPath, "global", "CLAUDE.md"), "# Global Context\n\nRepo-local Phren instructions shared across this workspace.\n");
|
|
815
|
+
}
|
|
816
|
+
const created = bootstrapFromExisting(phrenPath, workspaceRoot, { ownership: ownershipDefault });
|
|
817
|
+
applyOnboardingPreferences(phrenPath, opts);
|
|
818
|
+
writeInstallPreferences(phrenPath, {
|
|
819
|
+
mcpEnabled,
|
|
820
|
+
hooksEnabled: false,
|
|
821
|
+
skillsScope: opts.skillsScope ?? "global",
|
|
822
|
+
installedVersion: VERSION,
|
|
823
|
+
});
|
|
824
|
+
try {
|
|
825
|
+
const vscodeResult = configureVSCode(phrenPath, { mcpEnabled, scope: "workspace" });
|
|
826
|
+
logMcpTargetStatus("VS Code", vscodeResult, existingManifest ? "Updated" : "Configured");
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
debugLog(`configureVSCode(workspace) failed: ${errorMessage(err)}`);
|
|
830
|
+
}
|
|
831
|
+
log(`\n${existingManifest ? "Updated" : "Created"} project-local phren at ${phrenPath}`);
|
|
832
|
+
log(` Workspace root: ${workspaceRoot}`);
|
|
833
|
+
log(` Project: ${created.project}`);
|
|
834
|
+
log(` Ownership: ${created.ownership}`);
|
|
835
|
+
log(` Sync mode: workspace-git`);
|
|
836
|
+
log(` Hooks: off (unsupported in project-local mode)`);
|
|
837
|
+
log(` VS Code MCP: ${mcpEnabled ? "workspace on" : "workspace off"}`);
|
|
838
|
+
const verify = runPostInitVerify(phrenPath);
|
|
839
|
+
log(`\nVerifying setup...`);
|
|
840
|
+
for (const check of verify.checks) {
|
|
841
|
+
log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Configure MCP for all detected AI coding tools (Claude, VS Code, Cursor, Copilot, Codex).
|
|
846
|
+
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
847
|
+
*/
|
|
848
|
+
function configureMcpTargets(phrenPath, opts, verb) {
|
|
849
|
+
try {
|
|
850
|
+
const status = configureClaude(phrenPath, { mcpEnabled: opts.mcpEnabled, hooksEnabled: opts.hooksEnabled });
|
|
851
|
+
if (status === "disabled" || status === "already_disabled") {
|
|
852
|
+
log(` ${verb} Claude Code hooks (MCP disabled)`);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
log(` ${verb} Claude Code MCP + hooks`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
log(` Could not configure Claude Code settings (${e}), add manually`);
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const vscodeResult = configureVSCode(phrenPath, { mcpEnabled: opts.mcpEnabled });
|
|
863
|
+
logMcpTargetStatus("VS Code", vscodeResult, verb);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
debugLog(`configureVSCode failed: ${errorMessage(err)}`);
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
logMcpTargetStatus("Cursor", configureCursorMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }), verb);
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
debugLog(`configureCursorMcp failed: ${errorMessage(err)}`);
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
logMcpTargetStatus("Copilot CLI", configureCopilotMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }), verb);
|
|
876
|
+
}
|
|
877
|
+
catch (err) {
|
|
878
|
+
debugLog(`configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
logMcpTargetStatus("Codex", configureCodexMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }), verb);
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
debugLog(`configureCodexMcp failed: ${errorMessage(err)}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Configure hooks if enabled, or log a disabled message.
|
|
889
|
+
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
890
|
+
*/
|
|
891
|
+
function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
|
|
892
|
+
if (hooksEnabled) {
|
|
893
|
+
try {
|
|
894
|
+
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
895
|
+
if (hooked.length)
|
|
896
|
+
log(` ${verb} hooks: ${hooked.join(", ")}`);
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
debugLog(`configureAllHooks failed: ${errorMessage(err)}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
log(` Hooks are disabled by preference (run: npx phren hooks-mode on)`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
export async function runInit(opts = {}) {
|
|
907
|
+
if ((opts.mode || "shared") === "project-local") {
|
|
908
|
+
await runProjectLocalInit(opts);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
let phrenPath = resolveInitPhrenPath(opts);
|
|
912
|
+
const dryRun = Boolean(opts.dryRun);
|
|
913
|
+
let hasExistingInstall = hasInstallMarkers(phrenPath);
|
|
914
|
+
// Interactive walkthrough for first-time installs (skip with --yes or non-TTY)
|
|
915
|
+
if (!hasExistingInstall && !dryRun && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
916
|
+
const answers = await runWalkthrough(phrenPath);
|
|
917
|
+
opts._walkthroughStorageChoice = answers.storageChoice;
|
|
918
|
+
opts._walkthroughStoragePath = answers.storagePath;
|
|
919
|
+
opts._walkthroughStorageRepoRoot = answers.storageRepoRoot;
|
|
920
|
+
phrenPath = resolveInitPhrenPath(opts);
|
|
921
|
+
hasExistingInstall = hasInstallMarkers(phrenPath);
|
|
922
|
+
opts.machine = opts.machine || answers.machine;
|
|
923
|
+
opts.profile = opts.profile || answers.profile;
|
|
924
|
+
opts.mcp = opts.mcp || answers.mcp;
|
|
925
|
+
opts.hooks = opts.hooks || answers.hooks;
|
|
926
|
+
opts.projectOwnershipDefault = opts.projectOwnershipDefault || answers.projectOwnershipDefault;
|
|
927
|
+
opts.findingsProactivity = opts.findingsProactivity || answers.findingsProactivity;
|
|
928
|
+
opts.taskProactivity = opts.taskProactivity || answers.taskProactivity;
|
|
929
|
+
if (typeof opts.lowConfidenceThreshold !== "number")
|
|
930
|
+
opts.lowConfidenceThreshold = answers.lowConfidenceThreshold;
|
|
931
|
+
if (!Array.isArray(opts.riskySections))
|
|
932
|
+
opts.riskySections = answers.riskySections;
|
|
933
|
+
opts.taskMode = opts.taskMode || answers.taskMode;
|
|
934
|
+
if (answers.cloneUrl) {
|
|
935
|
+
opts._walkthroughCloneUrl = answers.cloneUrl;
|
|
936
|
+
}
|
|
937
|
+
if (answers.githubRepo) {
|
|
938
|
+
opts._walkthroughGithub = { username: answers.githubUsername, repo: answers.githubRepo };
|
|
939
|
+
}
|
|
940
|
+
opts._walkthroughDomain = answers.domain;
|
|
941
|
+
if (answers.inferredScaffold) {
|
|
942
|
+
opts._walkthroughInferredScaffold = answers.inferredScaffold;
|
|
943
|
+
}
|
|
944
|
+
if (!answers.ollamaEnabled) {
|
|
945
|
+
// User explicitly declined Ollama — note it but don't set env (they can set it themselves)
|
|
946
|
+
process.env._PHREN_WALKTHROUGH_OLLAMA_SKIP = "1";
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
opts._walkthroughSemanticSearch = true;
|
|
950
|
+
}
|
|
951
|
+
// Persist the walkthrough choice so init writes an explicit .env default.
|
|
952
|
+
opts._walkthroughAutoCapture = answers.autoCaptureEnabled;
|
|
953
|
+
if (answers.semanticDedupEnabled) {
|
|
954
|
+
opts._walkthroughSemanticDedup = true;
|
|
955
|
+
}
|
|
956
|
+
if (answers.semanticConflictEnabled) {
|
|
957
|
+
opts._walkthroughSemanticConflict = true;
|
|
958
|
+
}
|
|
959
|
+
if (answers.findingSensitivity && answers.findingSensitivity !== "balanced") {
|
|
960
|
+
opts.findingSensitivity = answers.findingSensitivity;
|
|
961
|
+
}
|
|
962
|
+
opts._walkthroughBootstrapCurrentProject = answers.bootstrapCurrentProject;
|
|
963
|
+
opts._walkthroughBootstrapOwnership = answers.bootstrapOwnership;
|
|
964
|
+
}
|
|
965
|
+
// If the walkthrough provided a clone URL, clone it and treat as existing install
|
|
966
|
+
if (opts._walkthroughCloneUrl) {
|
|
967
|
+
log(`\nCloning existing phren from ${opts._walkthroughCloneUrl}...`);
|
|
968
|
+
try {
|
|
969
|
+
execFileSync("git", ["clone", opts._walkthroughCloneUrl, phrenPath], {
|
|
970
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
971
|
+
timeout: 60_000,
|
|
972
|
+
});
|
|
973
|
+
log(` Cloned to ${phrenPath}`);
|
|
974
|
+
// Re-check: the cloned repo should now be treated as an existing install
|
|
975
|
+
hasExistingInstall = true;
|
|
976
|
+
}
|
|
977
|
+
catch (e) {
|
|
978
|
+
log(` Clone failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
979
|
+
log(` Continuing with fresh install instead.`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const mcpEnabled = opts.mcp ? opts.mcp === "on" : getMcpEnabledPreference(phrenPath);
|
|
983
|
+
const hooksEnabled = opts.hooks ? opts.hooks === "on" : getHooksEnabledPreference(phrenPath);
|
|
984
|
+
const skillsScope = opts.skillsScope ?? "global";
|
|
985
|
+
const storageChoice = opts._walkthroughStorageChoice;
|
|
986
|
+
const storageRepoRoot = opts._walkthroughStorageRepoRoot;
|
|
987
|
+
const ownershipDefault = opts.projectOwnershipDefault
|
|
988
|
+
?? (hasExistingInstall ? getProjectOwnershipDefault(phrenPath) : "detached");
|
|
989
|
+
if (!hasExistingInstall && !opts.projectOwnershipDefault) {
|
|
990
|
+
opts.projectOwnershipDefault = ownershipDefault;
|
|
991
|
+
}
|
|
992
|
+
const mcpLabel = mcpEnabled ? "ON (recommended)" : "OFF (hooks-only fallback)";
|
|
993
|
+
const hooksLabel = hooksEnabled ? "ON (active)" : "OFF (disabled)";
|
|
994
|
+
const pendingBootstrap = getPendingBootstrapTarget(phrenPath, opts);
|
|
995
|
+
let shouldBootstrapCurrentProject = opts._walkthroughBootstrapCurrentProject === true;
|
|
996
|
+
let bootstrapOwnership = opts._walkthroughBootstrapOwnership ?? ownershipDefault;
|
|
997
|
+
if (pendingBootstrap && !dryRun) {
|
|
998
|
+
const walkthroughAlreadyHandled = opts._walkthroughBootstrapCurrentProject !== undefined;
|
|
999
|
+
if (walkthroughAlreadyHandled) {
|
|
1000
|
+
shouldBootstrapCurrentProject = opts._walkthroughBootstrapCurrentProject === true;
|
|
1001
|
+
bootstrapOwnership = opts._walkthroughBootstrapOwnership ?? ownershipDefault;
|
|
1002
|
+
}
|
|
1003
|
+
else if (opts.yes || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1004
|
+
shouldBootstrapCurrentProject = true;
|
|
1005
|
+
bootstrapOwnership = ownershipDefault;
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
const prompts = await createWalkthroughPrompts();
|
|
1009
|
+
const style = await createWalkthroughStyle();
|
|
1010
|
+
const detectedProjectName = path.basename(pendingBootstrap.path);
|
|
1011
|
+
log("");
|
|
1012
|
+
log(style.header("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
1013
|
+
log(style.header("Current Project"));
|
|
1014
|
+
log(style.header("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
1015
|
+
log(`Detected project: ${detectedProjectName}`);
|
|
1016
|
+
shouldBootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
1017
|
+
if (!shouldBootstrapCurrentProject) {
|
|
1018
|
+
shouldBootstrapCurrentProject = false;
|
|
1019
|
+
log(style.warning(` Skipped. Later: cd ${pendingBootstrap.path} && npx phren add`));
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
|
1023
|
+
{ value: ownershipDefault, name: `${ownershipDefault} (default)` },
|
|
1024
|
+
...PROJECT_OWNERSHIP_MODES
|
|
1025
|
+
.filter((mode) => mode !== ownershipDefault)
|
|
1026
|
+
.map((mode) => ({ value: mode, name: mode })),
|
|
1027
|
+
], ownershipDefault);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (dryRun) {
|
|
1032
|
+
log("\nInit dry run. No files will be written.\n");
|
|
1033
|
+
if (storageChoice) {
|
|
1034
|
+
log(`Storage location: ${storageChoice} (${phrenPath})`);
|
|
1035
|
+
if (storageChoice === "project" && storageRepoRoot) {
|
|
1036
|
+
log(` Would update ${path.join(storageRepoRoot, ".gitignore")} with .phren/`);
|
|
1037
|
+
log(` Would set PHREN_PATH in ${path.join(storageRepoRoot, ".env")}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (hasExistingInstall) {
|
|
1041
|
+
log(`phren install detected at ${phrenPath}`);
|
|
1042
|
+
log(`Would update configuration for the existing install:\n`);
|
|
1043
|
+
log(` MCP mode: ${mcpLabel}`);
|
|
1044
|
+
log(` Hooks mode: ${hooksLabel}`);
|
|
1045
|
+
log(` Reconfigure Claude Code MCP/hooks`);
|
|
1046
|
+
log(` Reconfigure VS Code, Cursor, Copilot CLI, and Codex MCP targets`);
|
|
1047
|
+
if (hooksEnabled) {
|
|
1048
|
+
log(` Reconfigure lifecycle hooks for detected tools`);
|
|
1049
|
+
}
|
|
1050
|
+
if (pendingBootstrap?.mode === "detected") {
|
|
1051
|
+
log(` Would offer to add current project directory (${pendingBootstrap.path})`);
|
|
1052
|
+
}
|
|
1053
|
+
if (opts.applyStarterUpdate) {
|
|
1054
|
+
log(` Apply starter template updates to global/CLAUDE.md and global skills`);
|
|
1055
|
+
}
|
|
1056
|
+
log(` Run post-init verification checks`);
|
|
1057
|
+
log(`\nDry run complete.\n`);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
log(`No existing phren install found at ${phrenPath}`);
|
|
1061
|
+
log(`Would create a new phren install:\n`);
|
|
1062
|
+
log(` Copy starter files to ${phrenPath} (or create minimal structure)`);
|
|
1063
|
+
log(` Update machines.yaml for machine "${opts.machine || getMachineName()}"`);
|
|
1064
|
+
log(` Create/update config files`);
|
|
1065
|
+
log(` MCP mode: ${mcpLabel}`);
|
|
1066
|
+
log(` Hooks mode: ${hooksLabel}`);
|
|
1067
|
+
log(` Configure Claude Code plus detected MCP targets (VS Code/Cursor/Copilot/Codex)`);
|
|
1068
|
+
if (hooksEnabled) {
|
|
1069
|
+
log(` Configure lifecycle hooks for detected tools`);
|
|
1070
|
+
}
|
|
1071
|
+
if (pendingBootstrap?.mode === "detected") {
|
|
1072
|
+
log(` Would offer to add current project directory (${pendingBootstrap.path})`);
|
|
1073
|
+
}
|
|
1074
|
+
log(` Write install preferences and run post-init verification checks`);
|
|
1075
|
+
log(`\nDry run complete.\n`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (storageChoice === "project") {
|
|
1079
|
+
if (!storageRepoRoot) {
|
|
1080
|
+
throw new Error("Per-project storage requires a detected repository root.");
|
|
1081
|
+
}
|
|
1082
|
+
const storageChanges = applyProjectStorageBindings(storageRepoRoot, phrenPath);
|
|
1083
|
+
for (const change of storageChanges) {
|
|
1084
|
+
log(` Updated storage binding: ${change}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (hasExistingInstall) {
|
|
1088
|
+
writeRootManifest(phrenPath, {
|
|
1089
|
+
version: 1,
|
|
1090
|
+
installMode: "shared",
|
|
1091
|
+
syncMode: "managed-git",
|
|
1092
|
+
});
|
|
1093
|
+
ensureGovernanceFiles(phrenPath);
|
|
1094
|
+
const repaired = repairPreexistingInstall(phrenPath);
|
|
1095
|
+
applyOnboardingPreferences(phrenPath, opts);
|
|
1096
|
+
const existingGitRepo = ensureLocalGitRepo(phrenPath);
|
|
1097
|
+
log(`\nphren already exists at ${phrenPath}`);
|
|
1098
|
+
log(`Updating configuration...\n`);
|
|
1099
|
+
log(` MCP mode: ${mcpLabel}`);
|
|
1100
|
+
log(` Hooks mode: ${hooksLabel}`);
|
|
1101
|
+
log(` Default project ownership: ${ownershipDefault}`);
|
|
1102
|
+
log(` Task mode: ${getWorkflowPolicy(phrenPath).taskMode}`);
|
|
1103
|
+
log(` Git repo: ${existingGitRepo.detail}`);
|
|
1104
|
+
// Confirmation prompt before writing config
|
|
1105
|
+
if (!opts.yes) {
|
|
1106
|
+
const settingsPath = hookConfigPath("claude");
|
|
1107
|
+
const modifications = [];
|
|
1108
|
+
modifications.push(` ${settingsPath} (update MCP server + hooks)`);
|
|
1109
|
+
log(`\nWill modify:`);
|
|
1110
|
+
for (const mod of modifications)
|
|
1111
|
+
log(mod);
|
|
1112
|
+
const confirmed = await confirmPrompt("\nProceed?");
|
|
1113
|
+
if (!confirmed) {
|
|
1114
|
+
log("Aborted.");
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Always reconfigure MCP and hooks (picks up new features on upgrade)
|
|
1119
|
+
configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled }, "Updated");
|
|
1120
|
+
configureHooksIfEnabled(phrenPath, hooksEnabled, "Updated");
|
|
1121
|
+
const prefs = readInstallPreferences(phrenPath);
|
|
1122
|
+
const previousVersion = prefs.installedVersion;
|
|
1123
|
+
if (isVersionNewer(VERSION, previousVersion)) {
|
|
1124
|
+
log(`\n Starter template update available: v${previousVersion} -> v${VERSION}`);
|
|
1125
|
+
log(` Run \`npx phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
|
|
1126
|
+
}
|
|
1127
|
+
if (opts.applyStarterUpdate) {
|
|
1128
|
+
const updated = applyStarterTemplateUpdates(phrenPath);
|
|
1129
|
+
if (updated.length) {
|
|
1130
|
+
log(` Applied starter template updates (${updated.length} file${updated.length === 1 ? "" : "s"}).`);
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
log(` No starter template updates were applied (starter files not found).`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION });
|
|
1137
|
+
if (repaired.removedLegacyProjects > 0) {
|
|
1138
|
+
log(` Removed ${repaired.removedLegacyProjects} legacy starter project entr${repaired.removedLegacyProjects === 1 ? "y" : "ies"} from profiles.`);
|
|
1139
|
+
}
|
|
1140
|
+
const repairedAssets = collectRepairedAssetLabels(repaired);
|
|
1141
|
+
if (repairedAssets.length > 0) {
|
|
1142
|
+
log(` Recreated missing generated assets: ${repairedAssets.join(", ")}`);
|
|
1143
|
+
}
|
|
1144
|
+
// Post-update verification
|
|
1145
|
+
log(`\nVerifying setup...`);
|
|
1146
|
+
const verify = runPostInitVerify(phrenPath);
|
|
1147
|
+
for (const check of verify.checks) {
|
|
1148
|
+
log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (pendingBootstrap && shouldBootstrapCurrentProject) {
|
|
1151
|
+
try {
|
|
1152
|
+
const created = bootstrapFromExisting(phrenPath, pendingBootstrap.path, {
|
|
1153
|
+
profile: opts.profile,
|
|
1154
|
+
ownership: bootstrapOwnership,
|
|
1155
|
+
});
|
|
1156
|
+
log(`\nAdded current project "${created.project}" (${created.ownership})`);
|
|
1157
|
+
}
|
|
1158
|
+
catch (e) {
|
|
1159
|
+
debugLog(`Bootstrap from CWD failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
for (const envLabel of writeWalkthroughEnvDefaults(phrenPath, opts)) {
|
|
1163
|
+
log(` ${envLabel}`);
|
|
1164
|
+
}
|
|
1165
|
+
log(`\n\x1b[95m◆\x1b[0m phren updated successfully`);
|
|
1166
|
+
log(`\nNext steps:`);
|
|
1167
|
+
log(` 1. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
1168
|
+
log(` 2. Run \`npx phren doctor\` to verify everything is wired correctly`);
|
|
1169
|
+
log(` 3. Change defaults anytime: \`npx phren config project-ownership\`, \`npx phren config workflow\`, \`npx phren config proactivity.findings\`, \`npx phren config proactivity.tasks\``);
|
|
1170
|
+
log(` 4. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
1171
|
+
log(` 5. After working across projects, run phren-consolidate to find cross-project patterns`);
|
|
1172
|
+
log(``);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
log("\nSetting up phren...\n");
|
|
1176
|
+
const walkthroughProject = opts._walkthroughProject;
|
|
1177
|
+
if (walkthroughProject) {
|
|
1178
|
+
if (!walkthroughProject.trim()) {
|
|
1179
|
+
console.error("Error: project name cannot be empty.");
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
if (walkthroughProject.length > 100) {
|
|
1183
|
+
console.error("Error: project name must be 100 characters or fewer.");
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
if (!isValidProjectName(walkthroughProject)) {
|
|
1187
|
+
console.error(`Error: invalid project name "${walkthroughProject}". Use lowercase letters, numbers, and hyphens.`);
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// Determine if CWD is a project that should be bootstrapped instead of
|
|
1192
|
+
// creating a dummy "my-first-project".
|
|
1193
|
+
const cwdProjectPath = !walkthroughProject ? detectProjectDir(process.cwd(), phrenPath) : null;
|
|
1194
|
+
const useTemplateProject = Boolean(walkthroughProject) || Boolean(opts.template);
|
|
1195
|
+
const firstProjectName = walkthroughProject || "my-first-project";
|
|
1196
|
+
const firstProjectDomain = opts._walkthroughDomain ?? "software";
|
|
1197
|
+
// Copy bundled starter to ~/.phren
|
|
1198
|
+
function copyDir(src, dest) {
|
|
1199
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1200
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
1201
|
+
if (src === STARTER_DIR && entry.isDirectory() && ["my-api", "my-frontend", "my-first-project"].includes(entry.name)) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const srcPath = path.join(src, entry.name);
|
|
1205
|
+
const destPath = path.join(dest, entry.name);
|
|
1206
|
+
if (entry.isDirectory()) {
|
|
1207
|
+
copyDir(srcPath, destPath);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
fs.copyFileSync(srcPath, destPath);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (fs.existsSync(STARTER_DIR)) {
|
|
1215
|
+
copyDir(STARTER_DIR, phrenPath);
|
|
1216
|
+
writeRootManifest(phrenPath, {
|
|
1217
|
+
version: 1,
|
|
1218
|
+
installMode: "shared",
|
|
1219
|
+
syncMode: "managed-git",
|
|
1220
|
+
});
|
|
1221
|
+
if (useTemplateProject) {
|
|
1222
|
+
const targetProject = walkthroughProject || firstProjectName;
|
|
1223
|
+
const projectDir = path.join(phrenPath, targetProject);
|
|
1224
|
+
const templateApplied = Boolean(opts.template && applyTemplate(projectDir, opts.template, targetProject));
|
|
1225
|
+
if (templateApplied) {
|
|
1226
|
+
log(` Applied "${opts.template}" template to ${targetProject}`);
|
|
1227
|
+
}
|
|
1228
|
+
ensureProjectScaffold(projectDir, targetProject, firstProjectDomain, opts._walkthroughInferredScaffold);
|
|
1229
|
+
const targetProfile = opts.profile || "default";
|
|
1230
|
+
const addToProfile = addProjectToProfile(phrenPath, targetProfile, targetProject);
|
|
1231
|
+
if (!addToProfile.ok) {
|
|
1232
|
+
debugLog(`fresh init addProjectToProfile failed for ${targetProfile}/${targetProject}: ${addToProfile.error}`);
|
|
1233
|
+
}
|
|
1234
|
+
if (opts.template && !templateApplied) {
|
|
1235
|
+
log(` Template "${opts.template}" not found. Available: ${listTemplates().join(", ") || "none"}`);
|
|
1236
|
+
}
|
|
1237
|
+
log(` Seeded project "${targetProject}"`);
|
|
1238
|
+
}
|
|
1239
|
+
log(` Created phren v${VERSION} \u2192 ${phrenPath}`);
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
log(` Starter not found in package, creating minimal structure...`);
|
|
1243
|
+
writeRootManifest(phrenPath, {
|
|
1244
|
+
version: 1,
|
|
1245
|
+
installMode: "shared",
|
|
1246
|
+
syncMode: "managed-git",
|
|
1247
|
+
});
|
|
1248
|
+
fs.mkdirSync(path.join(phrenPath, "global", "skills"), { recursive: true });
|
|
1249
|
+
fs.mkdirSync(path.join(phrenPath, "profiles"), { recursive: true });
|
|
1250
|
+
atomicWriteText(path.join(phrenPath, "global", "CLAUDE.md"), `# Global Context\n\nThis file is loaded in every project.\n\n## General preferences\n\n<!-- Your coding style, preferred tools, things Claude should always know -->\n`);
|
|
1251
|
+
if (useTemplateProject) {
|
|
1252
|
+
const projectDir = path.join(phrenPath, firstProjectName);
|
|
1253
|
+
if (opts.template && applyTemplate(projectDir, opts.template, firstProjectName)) {
|
|
1254
|
+
log(` Applied "${opts.template}" template to ${firstProjectName}`);
|
|
1255
|
+
}
|
|
1256
|
+
ensureProjectScaffold(projectDir, firstProjectName, firstProjectDomain, opts._walkthroughInferredScaffold);
|
|
1257
|
+
}
|
|
1258
|
+
const profileName = opts.profile || "default";
|
|
1259
|
+
const profileProjects = useTemplateProject
|
|
1260
|
+
? ` - global\n - ${firstProjectName}`
|
|
1261
|
+
: ` - global`;
|
|
1262
|
+
atomicWriteText(path.join(phrenPath, "profiles", `${profileName}.yaml`), `name: ${profileName}\ndescription: Default profile\nprojects:\n${profileProjects}\n`);
|
|
1263
|
+
}
|
|
1264
|
+
// If CWD is a project dir, bootstrap it now when onboarding or defaults allow it.
|
|
1265
|
+
if (cwdProjectPath && shouldBootstrapCurrentProject) {
|
|
1266
|
+
try {
|
|
1267
|
+
const created = bootstrapFromExisting(phrenPath, cwdProjectPath, {
|
|
1268
|
+
profile: opts.profile,
|
|
1269
|
+
ownership: bootstrapOwnership,
|
|
1270
|
+
});
|
|
1271
|
+
log(` Added current project "${created.project}" (${created.ownership})`);
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
// Fresh-install bootstrap is best-effort. If it fails, the install
|
|
1275
|
+
// still succeeded and the user can add the project explicitly later.
|
|
1276
|
+
debugLog(`Bootstrap from CWD during fresh install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Persist the local machine alias and map it to the selected profile.
|
|
1280
|
+
const effectiveMachine = opts.machine?.trim() || getMachineName();
|
|
1281
|
+
persistMachineName(effectiveMachine);
|
|
1282
|
+
updateMachinesYaml(phrenPath, effectiveMachine, opts.profile);
|
|
1283
|
+
ensureGovernanceFiles(phrenPath);
|
|
1284
|
+
const repaired = repairPreexistingInstall(phrenPath);
|
|
1285
|
+
applyOnboardingPreferences(phrenPath, opts);
|
|
1286
|
+
const localGitRepo = ensureLocalGitRepo(phrenPath);
|
|
1287
|
+
log(` Updated machines.yaml with machine "${effectiveMachine}"`);
|
|
1288
|
+
log(` MCP mode: ${mcpLabel}`);
|
|
1289
|
+
log(` Hooks mode: ${hooksLabel}`);
|
|
1290
|
+
log(` Default project ownership: ${ownershipDefault}`);
|
|
1291
|
+
log(` Task mode: ${getWorkflowPolicy(phrenPath).taskMode}`);
|
|
1292
|
+
log(` Git repo: ${localGitRepo.detail}`);
|
|
1293
|
+
if (repaired.removedLegacyProjects > 0) {
|
|
1294
|
+
log(` Removed ${repaired.removedLegacyProjects} legacy starter project entr${repaired.removedLegacyProjects === 1 ? "y" : "ies"} from profiles.`);
|
|
1295
|
+
}
|
|
1296
|
+
const repairedAssets = collectRepairedAssetLabels(repaired);
|
|
1297
|
+
if (repairedAssets.length > 0) {
|
|
1298
|
+
log(` Recreated missing generated assets: ${repairedAssets.join(", ")}`);
|
|
1299
|
+
}
|
|
1300
|
+
// Confirmation prompt before writing agent config
|
|
1301
|
+
if (!opts.yes) {
|
|
1302
|
+
const settingsPath = hookConfigPath("claude");
|
|
1303
|
+
log(`\nWill modify:`);
|
|
1304
|
+
log(` ${settingsPath} (add MCP server + hooks)`);
|
|
1305
|
+
const confirmed = await confirmPrompt("\nProceed?");
|
|
1306
|
+
if (!confirmed) {
|
|
1307
|
+
log("Aborted.");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// Configure MCP for all detected AI coding tools and hooks
|
|
1312
|
+
configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled }, "Configured");
|
|
1313
|
+
configureHooksIfEnabled(phrenPath, hooksEnabled, "Configured");
|
|
1314
|
+
writeInstallPreferences(phrenPath, { mcpEnabled, hooksEnabled, skillsScope, installedVersion: VERSION });
|
|
1315
|
+
// Post-init verification
|
|
1316
|
+
log(`\nVerifying setup...`);
|
|
1317
|
+
const verify = runPostInitVerify(phrenPath);
|
|
1318
|
+
for (const check of verify.checks) {
|
|
1319
|
+
log(` ${check.ok ? "pass" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
1320
|
+
}
|
|
1321
|
+
log(`\nWhat was created:`);
|
|
1322
|
+
log(` ${phrenPath}/global/CLAUDE.md Global instructions loaded in every session`);
|
|
1323
|
+
log(` ${phrenPath}/global/skills/ Phren slash commands`);
|
|
1324
|
+
log(` ${phrenPath}/profiles/ Machine-to-project mappings`);
|
|
1325
|
+
log(` ${phrenPath}/.governance/ Memory quality settings and config`);
|
|
1326
|
+
// Ollama status summary (skip if already covered in walkthrough)
|
|
1327
|
+
const walkthroughCoveredOllama = Boolean(process.env._PHREN_WALKTHROUGH_OLLAMA_SKIP) || (!hasExistingInstall && !opts.yes);
|
|
1328
|
+
if (!walkthroughCoveredOllama) {
|
|
1329
|
+
try {
|
|
1330
|
+
const { checkOllamaAvailable, checkModelAvailable, getOllamaUrl } = await import("./shared-ollama.js");
|
|
1331
|
+
if (getOllamaUrl()) {
|
|
1332
|
+
const ollamaUp = await checkOllamaAvailable();
|
|
1333
|
+
if (ollamaUp) {
|
|
1334
|
+
const modelReady = await checkModelAvailable();
|
|
1335
|
+
if (modelReady) {
|
|
1336
|
+
log("\n Semantic search: Ollama + nomic-embed-text ready.");
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
log("\n Semantic search: Ollama running, but nomic-embed-text not pulled.");
|
|
1340
|
+
log(" Run: ollama pull nomic-embed-text");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
log("\n Tip: Install Ollama for semantic search (optional).");
|
|
1345
|
+
log(" https://ollama.com → then: ollama pull nomic-embed-text");
|
|
1346
|
+
log(" (Set PHREN_OLLAMA_URL=off to hide this message)");
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
catch (err) {
|
|
1351
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
1352
|
+
process.stderr.write(`[phren] init ollamaInstallHint: ${errorMessage(err)}\n`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
for (const envLabel of writeWalkthroughEnvDefaults(phrenPath, opts)) {
|
|
1356
|
+
log(` ${envLabel}`);
|
|
1357
|
+
}
|
|
1358
|
+
if (opts._walkthroughSemanticSearch) {
|
|
1359
|
+
log(`\nWarming semantic search...`);
|
|
1360
|
+
try {
|
|
1361
|
+
log(` ${await warmSemanticSearch(phrenPath, opts.profile)}`);
|
|
1362
|
+
}
|
|
1363
|
+
catch (err) {
|
|
1364
|
+
log(` Semantic search warmup failed: ${errorMessage(err)}`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
log(`\n\x1b[95m◆\x1b[0m phren initialized`);
|
|
1368
|
+
log(`\nNext steps:`);
|
|
1369
|
+
let step = 1;
|
|
1370
|
+
log(` ${step++}. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
1371
|
+
log(` ${step++}. Run \`npx phren doctor\` to verify everything is wired correctly`);
|
|
1372
|
+
log(` ${step++}. Change defaults anytime: \`npx phren config project-ownership\`, \`npx phren config workflow\`, \`npx phren config proactivity.findings\`, \`npx phren config proactivity.tasks\``);
|
|
1373
|
+
const gh = opts._walkthroughGithub;
|
|
1374
|
+
if (gh) {
|
|
1375
|
+
const remote = gh.username
|
|
1376
|
+
? `git@github.com:${gh.username}/${gh.repo}.git`
|
|
1377
|
+
: `git@github.com:YOUR_USERNAME/${gh.repo}.git`;
|
|
1378
|
+
log(` ${step++}. Push your phren to GitHub (private repo recommended):`);
|
|
1379
|
+
log(` cd ${phrenPath}`);
|
|
1380
|
+
log(` git add . && git commit -m "Initial phren setup"`);
|
|
1381
|
+
if (gh.username) {
|
|
1382
|
+
log(` gh repo create ${gh.username}/${gh.repo} --private --source=. --push`);
|
|
1383
|
+
log(` # or manually: git remote add origin ${remote} && git push -u origin main`);
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
log(` git remote add origin ${remote}`);
|
|
1387
|
+
log(` git push -u origin main`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
log(` ${step++}. Push to GitHub for cross-machine sync (private repo recommended):`);
|
|
1392
|
+
log(` cd ${phrenPath}`);
|
|
1393
|
+
log(` git add . && git commit -m "Initial phren setup"`);
|
|
1394
|
+
log(` git remote add origin git@github.com:YOUR_USERNAME/my-phren.git`);
|
|
1395
|
+
log(` git push -u origin main`);
|
|
1396
|
+
}
|
|
1397
|
+
log(` ${step++}. Add more projects: cd ~/your-project && npx phren add`);
|
|
1398
|
+
if (!mcpEnabled) {
|
|
1399
|
+
log(` ${step++}. Turn MCP on: npx phren mcp-mode on`);
|
|
1400
|
+
}
|
|
1401
|
+
log(` ${step++}. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
1402
|
+
log(` ${step++}. After working across projects, run phren-consolidate to find cross-project patterns`);
|
|
1403
|
+
log(`\n Read ${phrenPath}/README.md for a guided tour of each file.`);
|
|
1404
|
+
log(``);
|
|
1405
|
+
}
|
|
1406
|
+
export async function runMcpMode(modeArg) {
|
|
1407
|
+
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1408
|
+
const manifest = readRootManifest(phrenPath);
|
|
1409
|
+
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
1410
|
+
if (!normalizedArg || normalizedArg === "status") {
|
|
1411
|
+
const current = getMcpEnabledPreference(phrenPath);
|
|
1412
|
+
const hooks = getHooksEnabledPreference(phrenPath);
|
|
1413
|
+
log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
|
|
1414
|
+
log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
|
|
1415
|
+
log(`Change mode: npx phren mcp-mode on|off`);
|
|
1416
|
+
log(`Hooks toggle: npx phren hooks-mode on|off`);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const mode = parseMcpMode(normalizedArg);
|
|
1420
|
+
if (!mode) {
|
|
1421
|
+
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
1422
|
+
}
|
|
1423
|
+
const enabled = mode === "on";
|
|
1424
|
+
if (manifest?.installMode === "project-local") {
|
|
1425
|
+
const vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled, scope: "workspace" });
|
|
1426
|
+
setMcpEnabledPreference(phrenPath, enabled);
|
|
1427
|
+
log(`MCP mode set to ${mode}.`);
|
|
1428
|
+
log(`VS Code status: ${vscodeStatus}`);
|
|
1429
|
+
log(`Project-local mode only configures workspace VS Code MCP.`);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
let claudeStatus = "no_settings";
|
|
1433
|
+
let vscodeStatus = "no_vscode";
|
|
1434
|
+
let cursorStatus = "no_cursor";
|
|
1435
|
+
let copilotStatus = "no_copilot";
|
|
1436
|
+
let codexStatus = "no_codex";
|
|
1437
|
+
try {
|
|
1438
|
+
claudeStatus = configureClaude(phrenPath, { mcpEnabled: enabled }) ?? claudeStatus;
|
|
1439
|
+
}
|
|
1440
|
+
catch (err) {
|
|
1441
|
+
debugLog(`mcp-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
vscodeStatus = configureVSCode(phrenPath, { mcpEnabled: enabled }) ?? vscodeStatus;
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
debugLog(`mcp-mode: configureVSCode failed: ${errorMessage(err)}`);
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: enabled }) ?? cursorStatus;
|
|
1451
|
+
}
|
|
1452
|
+
catch (err) {
|
|
1453
|
+
debugLog(`mcp-mode: configureCursorMcp failed: ${errorMessage(err)}`);
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: enabled }) ?? copilotStatus;
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
debugLog(`mcp-mode: configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: enabled }) ?? codexStatus;
|
|
1463
|
+
}
|
|
1464
|
+
catch (err) {
|
|
1465
|
+
debugLog(`mcp-mode: configureCodexMcp failed: ${errorMessage(err)}`);
|
|
1466
|
+
}
|
|
1467
|
+
// Persist preference only after config writes have been attempted
|
|
1468
|
+
setMcpEnabledPreference(phrenPath, enabled);
|
|
1469
|
+
log(`MCP mode set to ${mode}.`);
|
|
1470
|
+
log(`Claude status: ${claudeStatus}`);
|
|
1471
|
+
log(`VS Code status: ${vscodeStatus}`);
|
|
1472
|
+
log(`Cursor status: ${cursorStatus}`);
|
|
1473
|
+
log(`Copilot CLI status: ${copilotStatus}`);
|
|
1474
|
+
log(`Codex status: ${codexStatus}`);
|
|
1475
|
+
log(`Restart your agent to apply changes.`);
|
|
1476
|
+
}
|
|
1477
|
+
export async function runHooksMode(modeArg) {
|
|
1478
|
+
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1479
|
+
const manifest = readRootManifest(phrenPath);
|
|
1480
|
+
const normalizedArg = modeArg?.trim().toLowerCase();
|
|
1481
|
+
if (!normalizedArg || normalizedArg === "status") {
|
|
1482
|
+
const current = getHooksEnabledPreference(phrenPath);
|
|
1483
|
+
log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
|
|
1484
|
+
log(`Change mode: npx phren hooks-mode on|off`);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const mode = parseMcpMode(normalizedArg);
|
|
1488
|
+
if (!mode) {
|
|
1489
|
+
throw new Error(`Invalid mode "${modeArg}". Use: on | off | status`);
|
|
1490
|
+
}
|
|
1491
|
+
if (manifest?.installMode === "project-local") {
|
|
1492
|
+
throw new Error("hooks-mode is unsupported in project-local mode");
|
|
1493
|
+
}
|
|
1494
|
+
const enabled = mode === "on";
|
|
1495
|
+
let claudeStatus = "no_settings";
|
|
1496
|
+
try {
|
|
1497
|
+
claudeStatus = configureClaude(phrenPath, {
|
|
1498
|
+
mcpEnabled: getMcpEnabledPreference(phrenPath),
|
|
1499
|
+
hooksEnabled: enabled,
|
|
1500
|
+
}) ?? claudeStatus;
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
debugLog(`hooks-mode: configureClaude failed: ${errorMessage(err)}`);
|
|
1504
|
+
}
|
|
1505
|
+
if (enabled) {
|
|
1506
|
+
try {
|
|
1507
|
+
const hooked = configureAllHooks(phrenPath, { allTools: true });
|
|
1508
|
+
if (hooked.length)
|
|
1509
|
+
log(`Updated hooks: ${hooked.join(", ")}`);
|
|
1510
|
+
}
|
|
1511
|
+
catch (err) {
|
|
1512
|
+
debugLog(`hooks-mode: configureAllHooks failed: ${errorMessage(err)}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
log("Hooks will no-op immediately via preference and Claude hooks are removed.");
|
|
1517
|
+
}
|
|
1518
|
+
// Persist preference only after config writes have been attempted
|
|
1519
|
+
setHooksEnabledPreference(phrenPath, enabled);
|
|
1520
|
+
log(`Hooks mode set to ${mode}.`);
|
|
1521
|
+
log(`Claude status: ${claudeStatus}`);
|
|
1522
|
+
log(`Restart your agent to apply changes.`);
|
|
1523
|
+
}
|
|
1524
|
+
export async function runUninstall() {
|
|
1525
|
+
const phrenPath = findPhrenPath();
|
|
1526
|
+
const manifest = phrenPath ? readRootManifest(phrenPath) : null;
|
|
1527
|
+
if (manifest?.installMode === "project-local" && phrenPath) {
|
|
1528
|
+
log("\nUninstalling project-local phren...\n");
|
|
1529
|
+
const workspaceRoot = manifest.workspaceRoot || path.dirname(phrenPath);
|
|
1530
|
+
const workspaceMcp = path.join(workspaceRoot, ".vscode", "mcp.json");
|
|
1531
|
+
try {
|
|
1532
|
+
if (removeMcpServerAtPath(workspaceMcp)) {
|
|
1533
|
+
log(` Removed phren from VS Code workspace MCP config (${workspaceMcp})`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
catch (err) {
|
|
1537
|
+
debugLog(`uninstall local vscode cleanup failed: ${errorMessage(err)}`);
|
|
1538
|
+
}
|
|
1539
|
+
fs.rmSync(phrenPath, { recursive: true, force: true });
|
|
1540
|
+
log(` Removed ${phrenPath}`);
|
|
1541
|
+
log("\nProject-local phren uninstalled.");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
log("\nUninstalling phren...\n");
|
|
1545
|
+
const home = homeDir();
|
|
1546
|
+
const machineFile = machineFilePath();
|
|
1547
|
+
const settingsPath = hookConfigPath("claude");
|
|
1548
|
+
// Remove from Claude Code ~/.claude.json (where MCP servers are actually read)
|
|
1549
|
+
const claudeJsonPath = homePath(".claude.json");
|
|
1550
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
1551
|
+
try {
|
|
1552
|
+
if (removeMcpServerAtPath(claudeJsonPath)) {
|
|
1553
|
+
log(` Removed phren MCP server from ~/.claude.json`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
catch (e) {
|
|
1557
|
+
log(` Warning: could not update ~/.claude.json (${e})`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
// Remove from Claude Code settings.json
|
|
1561
|
+
if (fs.existsSync(settingsPath)) {
|
|
1562
|
+
try {
|
|
1563
|
+
patchJsonFile(settingsPath, (data) => {
|
|
1564
|
+
const hooksMap = isRecord(data.hooks) ? data.hooks : (data.hooks = {});
|
|
1565
|
+
// Remove MCP server
|
|
1566
|
+
if (data.mcpServers?.phren) {
|
|
1567
|
+
delete data.mcpServers.phren;
|
|
1568
|
+
log(` Removed phren MCP server from Claude Code settings`);
|
|
1569
|
+
}
|
|
1570
|
+
// Remove hooks containing phren references
|
|
1571
|
+
for (const hookEvent of ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"]) {
|
|
1572
|
+
const hooks = hooksMap[hookEvent];
|
|
1573
|
+
if (!Array.isArray(hooks))
|
|
1574
|
+
continue;
|
|
1575
|
+
const before = hooks.length;
|
|
1576
|
+
hooksMap[hookEvent] = hooks.filter((h) => !h.hooks?.some((hook) => typeof hook.command === "string" && isPhrenCommand(hook.command)));
|
|
1577
|
+
const removed = before - hooksMap[hookEvent].length;
|
|
1578
|
+
if (removed > 0)
|
|
1579
|
+
log(` Removed ${removed} phren hook(s) from ${hookEvent}`);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
catch (e) {
|
|
1584
|
+
log(` Warning: could not update Claude Code settings (${e})`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
log(` Claude Code settings not found at ${settingsPath} — skipping`);
|
|
1589
|
+
}
|
|
1590
|
+
// Remove from VS Code mcp.json
|
|
1591
|
+
const vsCandidates = vscodeMcpCandidates().map((dir) => path.join(dir, "mcp.json"));
|
|
1592
|
+
for (const mcpFile of vsCandidates) {
|
|
1593
|
+
try {
|
|
1594
|
+
if (removeMcpServerAtPath(mcpFile)) {
|
|
1595
|
+
log(` Removed phren from VS Code MCP config (${mcpFile})`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
catch (err) {
|
|
1599
|
+
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
// Remove from Cursor MCP config
|
|
1603
|
+
const cursorCandidates = cursorMcpCandidates();
|
|
1604
|
+
for (const mcpFile of cursorCandidates) {
|
|
1605
|
+
try {
|
|
1606
|
+
if (removeMcpServerAtPath(mcpFile)) {
|
|
1607
|
+
log(` Removed phren from Cursor MCP config (${mcpFile})`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
catch (err) {
|
|
1611
|
+
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
// Remove from Copilot CLI MCP config
|
|
1615
|
+
const copilotCandidates = copilotMcpCandidates();
|
|
1616
|
+
for (const mcpFile of copilotCandidates) {
|
|
1617
|
+
try {
|
|
1618
|
+
if (removeMcpServerAtPath(mcpFile)) {
|
|
1619
|
+
log(` Removed phren from Copilot CLI MCP config (${mcpFile})`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
catch (err) {
|
|
1623
|
+
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
// Remove from Codex MCP config (TOML + JSON)
|
|
1627
|
+
const codexToml = path.join(home, ".codex", "config.toml");
|
|
1628
|
+
try {
|
|
1629
|
+
if (removeTomlMcpServer(codexToml)) {
|
|
1630
|
+
log(` Removed phren from Codex MCP config (${codexToml})`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
catch (err) {
|
|
1634
|
+
debugLog(`uninstall: cleanup failed for ${codexToml}: ${errorMessage(err)}`);
|
|
1635
|
+
}
|
|
1636
|
+
const codexCandidates = codexJsonCandidates((process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1637
|
+
for (const mcpFile of codexCandidates) {
|
|
1638
|
+
try {
|
|
1639
|
+
if (removeMcpServerAtPath(mcpFile)) {
|
|
1640
|
+
log(` Removed phren from Codex MCP config (${mcpFile})`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch (err) {
|
|
1644
|
+
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// Remove Copilot hooks file (written by configureAllHooks)
|
|
1648
|
+
const copilotHooksFile = hookConfigPath("copilot", (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1649
|
+
try {
|
|
1650
|
+
if (fs.existsSync(copilotHooksFile)) {
|
|
1651
|
+
fs.unlinkSync(copilotHooksFile);
|
|
1652
|
+
log(` Removed Copilot hooks file (${copilotHooksFile})`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
catch (err) {
|
|
1656
|
+
debugLog(`uninstall: cleanup failed for ${copilotHooksFile}: ${errorMessage(err)}`);
|
|
1657
|
+
}
|
|
1658
|
+
// Remove phren entries from Cursor hooks file (may contain non-phren entries)
|
|
1659
|
+
const cursorHooksFile = hookConfigPath("cursor", (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1660
|
+
try {
|
|
1661
|
+
if (fs.existsSync(cursorHooksFile)) {
|
|
1662
|
+
const raw = JSON.parse(fs.readFileSync(cursorHooksFile, "utf8"));
|
|
1663
|
+
let changed = false;
|
|
1664
|
+
for (const key of ["sessionStart", "beforeSubmitPrompt", "stop"]) {
|
|
1665
|
+
if (raw[key]?.command && typeof raw[key].command === "string" && isPhrenCommand(raw[key].command)) {
|
|
1666
|
+
delete raw[key];
|
|
1667
|
+
changed = true;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (changed) {
|
|
1671
|
+
atomicWriteText(cursorHooksFile, JSON.stringify(raw, null, 2));
|
|
1672
|
+
log(` Removed phren entries from Cursor hooks (${cursorHooksFile})`);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
catch (err) {
|
|
1677
|
+
debugLog(`uninstall: cleanup failed for ${cursorHooksFile}: ${errorMessage(err)}`);
|
|
1678
|
+
}
|
|
1679
|
+
// Remove Codex hooks file in phren path
|
|
1680
|
+
const uninstallPhrenPath = (process.env.PHREN_PATH || process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1681
|
+
const codexHooksFile = hookConfigPath("codex", uninstallPhrenPath);
|
|
1682
|
+
try {
|
|
1683
|
+
if (fs.existsSync(codexHooksFile)) {
|
|
1684
|
+
fs.unlinkSync(codexHooksFile);
|
|
1685
|
+
log(` Removed Codex hooks file (${codexHooksFile})`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
catch (err) {
|
|
1689
|
+
debugLog(`uninstall: cleanup failed for ${codexHooksFile}: ${errorMessage(err)}`);
|
|
1690
|
+
}
|
|
1691
|
+
// Remove session wrapper scripts (written by installSessionWrapper)
|
|
1692
|
+
const localBinDir = path.join(home, ".local", "bin");
|
|
1693
|
+
for (const tool of ["copilot", "cursor", "codex"]) {
|
|
1694
|
+
const wrapperPath = path.join(localBinDir, tool);
|
|
1695
|
+
try {
|
|
1696
|
+
if (fs.existsSync(wrapperPath)) {
|
|
1697
|
+
// Only remove if it's a phren wrapper (check for PHREN_PATH marker)
|
|
1698
|
+
const content = fs.readFileSync(wrapperPath, "utf8");
|
|
1699
|
+
if (content.includes("PHREN_PATH") && content.includes("phren")) {
|
|
1700
|
+
fs.unlinkSync(wrapperPath);
|
|
1701
|
+
log(` Removed ${tool} session wrapper (${wrapperPath})`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
catch (err) {
|
|
1706
|
+
debugLog(`uninstall: cleanup failed for ${wrapperPath}: ${errorMessage(err)}`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
if (fs.existsSync(machineFile)) {
|
|
1711
|
+
fs.unlinkSync(machineFile);
|
|
1712
|
+
log(` Removed machine alias (${machineFile})`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
catch (err) {
|
|
1716
|
+
debugLog(`uninstall: cleanup failed for ${machineFile}: ${errorMessage(err)}`);
|
|
1717
|
+
}
|
|
1718
|
+
if (phrenPath && fs.existsSync(phrenPath)) {
|
|
1719
|
+
try {
|
|
1720
|
+
fs.rmSync(phrenPath, { recursive: true, force: true });
|
|
1721
|
+
log(` Removed phren root (${phrenPath})`);
|
|
1722
|
+
}
|
|
1723
|
+
catch (err) {
|
|
1724
|
+
debugLog(`uninstall: cleanup failed for ${phrenPath}: ${errorMessage(err)}`);
|
|
1725
|
+
log(` Warning: could not remove phren root (${phrenPath})`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
log(`\nPhren config, hooks, and installed data removed.`);
|
|
1729
|
+
log(`Restart your agent(s) to apply changes.\n`);
|
|
1730
|
+
}
|