@phren/cli 0.0.9 → 0.0.11
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/README.md +2 -8
- package/mcp/dist/cli-actions.js +5 -5
- package/mcp/dist/cli-config.js +334 -127
- package/mcp/dist/cli-govern.js +140 -3
- package/mcp/dist/cli-graph.js +3 -2
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +3 -3
- package/mcp/dist/cli-hooks.js +41 -34
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/content-archive.js +2 -2
- package/mcp/dist/content-citation.js +12 -22
- package/mcp/dist/content-dedup.js +9 -9
- package/mcp/dist/data-access.js +1 -1
- package/mcp/dist/data-tasks.js +23 -0
- package/mcp/dist/embedding.js +7 -7
- package/mcp/dist/entrypoint.js +129 -102
- package/mcp/dist/governance-locks.js +6 -5
- package/mcp/dist/governance-policy.js +155 -2
- package/mcp/dist/governance-scores.js +3 -3
- package/mcp/dist/hooks.js +39 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +3 -24
- package/mcp/dist/init-setup.js +5 -5
- package/mcp/dist/init.js +170 -23
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +1 -1
- package/mcp/dist/link-doctor.js +3 -3
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +17 -27
- package/mcp/dist/machine-identity.js +1 -9
- package/mcp/dist/mcp-config.js +247 -42
- package/mcp/dist/mcp-data.js +9 -9
- package/mcp/dist/mcp-extract-facts.js +1 -1
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +6 -6
- package/mcp/dist/mcp-graph.js +11 -11
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +8 -8
- package/mcp/dist/mcp-tasks.js +21 -1
- package/mcp/dist/memory-ui-page.js +23 -0
- package/mcp/dist/memory-ui-scripts.js +210 -27
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-paths.js +7 -7
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +63 -16
- package/mcp/dist/session-utils.js +3 -2
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +22 -56
- package/mcp/dist/shared-search-fallback.js +13 -13
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +3 -3
- package/mcp/dist/shell-input.js +1 -1
- package/mcp/dist/shell-state-store.js +1 -1
- package/mcp/dist/shell-view.js +3 -2
- package/mcp/dist/shell.js +1 -1
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +41 -13
- package/mcp/dist/task-hygiene.js +1 -1
- package/mcp/dist/telemetry.js +5 -4
- package/mcp/dist/update.js +1 -1
- package/mcp/dist/utils.js +3 -3
- package/package.json +2 -2
- package/starter/global/skills/audit.md +106 -0
- package/mcp/dist/shared-paths.js +0 -1
package/mcp/dist/hooks.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { createHmac
|
|
3
|
+
import { createHmac } from "crypto";
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, installPreferencesFile } from "./shared.js";
|
|
6
|
+
import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, installPreferencesFile, atomicWriteText } from "./shared.js";
|
|
7
7
|
import { errorMessage } from "./utils.js";
|
|
8
8
|
import { hookConfigPath } from "./provider-adapters.js";
|
|
9
9
|
import { PACKAGE_SPEC } from "./package-metadata.js";
|
|
10
|
-
function atomicWriteText(filePath, content) {
|
|
11
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
|
-
const tmpPath = `${filePath}.tmp-${randomUUID()}`;
|
|
13
|
-
fs.writeFileSync(tmpPath, content);
|
|
14
|
-
fs.renameSync(tmpPath, filePath);
|
|
15
|
-
}
|
|
16
10
|
export function commandExists(cmd) {
|
|
17
11
|
try {
|
|
18
12
|
const whichCmd = process.platform === "win32" ? "where.exe" : "which";
|
|
@@ -215,10 +209,34 @@ function validateCodexConfig(config) {
|
|
|
215
209
|
Array.isArray(config.hooks?.UserPromptSubmit) &&
|
|
216
210
|
Array.isArray(config.hooks?.Stop));
|
|
217
211
|
}
|
|
212
|
+
// ── mtime-based install-preferences cache (shared by readHookPreferences + readCustomHooks) ──
|
|
213
|
+
const _installPrefsJsonCache = new Map();
|
|
214
|
+
export function clearHookPrefsCache() {
|
|
215
|
+
_installPrefsJsonCache.clear();
|
|
216
|
+
}
|
|
217
|
+
function cachedReadInstallPrefsJson(phrenPath) {
|
|
218
|
+
const prefsPath = installPreferencesFile(phrenPath);
|
|
219
|
+
let mtimeMs;
|
|
220
|
+
try {
|
|
221
|
+
mtimeMs = fs.statSync(prefsPath).mtimeMs;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
_installPrefsJsonCache.delete(prefsPath);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const cached = _installPrefsJsonCache.get(prefsPath);
|
|
228
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
229
|
+
return cached.parsed;
|
|
230
|
+
}
|
|
231
|
+
const parsed = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
|
|
232
|
+
_installPrefsJsonCache.set(prefsPath, { mtimeMs, parsed });
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
218
235
|
function readHookPreferences(phrenPath) {
|
|
219
236
|
try {
|
|
220
|
-
const
|
|
221
|
-
|
|
237
|
+
const prefs = cachedReadInstallPrefsJson(phrenPath);
|
|
238
|
+
if (!prefs)
|
|
239
|
+
return { enabled: true, toolPrefs: {} };
|
|
222
240
|
const enabled = prefs.hooksEnabled !== false;
|
|
223
241
|
const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object"
|
|
224
242
|
? prefs.hookTools
|
|
@@ -255,15 +273,18 @@ const HOOK_TIMEOUT_MS = parseInt(process.env.PHREN_HOOK_TIMEOUT_MS || '14000', 1
|
|
|
255
273
|
const HOOK_ERROR_LOG_MAX_LINES = 1000;
|
|
256
274
|
export function readCustomHooks(phrenPath) {
|
|
257
275
|
try {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
if (!Array.isArray(prefs.customHooks))
|
|
276
|
+
const prefs = cachedReadInstallPrefsJson(phrenPath);
|
|
277
|
+
if (!prefs || !Array.isArray(prefs.customHooks))
|
|
261
278
|
return [];
|
|
262
|
-
return prefs.customHooks.filter((h) =>
|
|
263
|
-
typeof h
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
279
|
+
return prefs.customHooks.filter((h) => {
|
|
280
|
+
if (!h || typeof h !== "object")
|
|
281
|
+
return false;
|
|
282
|
+
const rec = h;
|
|
283
|
+
return (typeof rec.event === "string" &&
|
|
284
|
+
VALID_HOOK_EVENTS.has(rec.event) &&
|
|
285
|
+
((typeof rec.command === "string" && rec.command.trim().length > 0) ||
|
|
286
|
+
(typeof rec.webhook === "string" && rec.webhook.trim().length > 0)));
|
|
287
|
+
});
|
|
267
288
|
}
|
|
268
289
|
catch (err) {
|
|
269
290
|
debugLog(`readCustomHooks: ${errorMessage(err)}`);
|
package/mcp/dist/index.js
CHANGED
|
@@ -48,13 +48,13 @@ function cleanStaleLocks(phrenPath) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
catch (err) {
|
|
51
|
-
if ((process.env.PHREN_DEBUG
|
|
51
|
+
if ((process.env.PHREN_DEBUG))
|
|
52
52
|
process.stderr.write(`[phren] cleanStaleLocks statFile: ${errorMessage(err)}\n`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
catch (err) {
|
|
57
|
-
if ((process.env.PHREN_DEBUG
|
|
57
|
+
if ((process.env.PHREN_DEBUG))
|
|
58
58
|
process.stderr.write(`[phren] cleanStaleLocks readdir: ${errorMessage(err)}\n`);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -88,7 +88,7 @@ async function main() {
|
|
|
88
88
|
db?.close();
|
|
89
89
|
}
|
|
90
90
|
catch (err) {
|
|
91
|
-
if ((process.env.PHREN_DEBUG
|
|
91
|
+
if ((process.env.PHREN_DEBUG))
|
|
92
92
|
process.stderr.write(`[phren] rebuildIndex dbClose: ${errorMessage(err)}\n`);
|
|
93
93
|
}
|
|
94
94
|
db = await buildIndex(phrenPath, profile);
|
|
@@ -158,7 +158,7 @@ async function main() {
|
|
|
158
158
|
trackToolCall(phrenPath, registeredName);
|
|
159
159
|
}
|
|
160
160
|
catch (err) {
|
|
161
|
-
if ((process.env.PHREN_DEBUG
|
|
161
|
+
if ((process.env.PHREN_DEBUG))
|
|
162
162
|
process.stderr.write(`[phren] trackToolCall: ${errorMessage(err)}\n`);
|
|
163
163
|
}
|
|
164
164
|
return handler(...args);
|
package/mcp/dist/init-config.js
CHANGED
|
@@ -4,27 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { buildLifecycleCommands } from "./hooks.js";
|
|
10
|
-
import { EXEC_TIMEOUT_QUICK_MS, isRecord, hookConfigPath, homePath, readRootManifest, } from "./shared.js";
|
|
7
|
+
import { buildLifecycleCommands, commandExists } from "./hooks.js";
|
|
8
|
+
import { isRecord, hookConfigPath, homePath, readRootManifest, atomicWriteText, } from "./shared.js";
|
|
11
9
|
import { isFeatureEnabled, errorMessage } from "./utils.js";
|
|
12
10
|
import { probeVsCodeConfig, resolveCodexMcpConfig, resolveCopilotMcpConfig, resolveCursorMcpConfig, } from "./provider-adapters.js";
|
|
13
11
|
import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init-preferences.js";
|
|
14
|
-
import { resolveEntryScript, VERSION } from "./init-shared.js";
|
|
15
|
-
function log(msg) {
|
|
16
|
-
process.stdout.write(msg + "\n");
|
|
17
|
-
}
|
|
12
|
+
import { resolveEntryScript, log, VERSION } from "./init-shared.js";
|
|
18
13
|
function getObjectProp(value, key) {
|
|
19
14
|
const candidate = value[key];
|
|
20
15
|
return isRecord(candidate) ? candidate : undefined;
|
|
21
16
|
}
|
|
22
|
-
function atomicWriteText(filePath, content) {
|
|
23
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
24
|
-
const tmpPath = `${filePath}.tmp-${randomUUID()}`;
|
|
25
|
-
fs.writeFileSync(tmpPath, content);
|
|
26
|
-
fs.renameSync(tmpPath, filePath);
|
|
27
|
-
}
|
|
28
17
|
export function patchJsonFile(filePath, patch) {
|
|
29
18
|
let data = {};
|
|
30
19
|
if (fs.existsSync(filePath)) {
|
|
@@ -44,16 +33,6 @@ export function patchJsonFile(filePath, patch) {
|
|
|
44
33
|
patch(data);
|
|
45
34
|
atomicWriteText(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
46
35
|
}
|
|
47
|
-
function commandExists(cmd) {
|
|
48
|
-
try {
|
|
49
|
-
const whichCmd = process.platform === "win32" ? "where.exe" : "which";
|
|
50
|
-
execFileSync(whichCmd, [cmd], { stdio: ["ignore", "ignore", "ignore"], timeout: EXEC_TIMEOUT_QUICK_MS });
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
36
|
function buildMcpServerConfig(phrenPath) {
|
|
58
37
|
const entryScript = resolveEntryScript();
|
|
59
38
|
if (entryScript && fs.existsSync(entryScript)) {
|
package/mcp/dist/init-setup.js
CHANGED
|
@@ -1087,14 +1087,14 @@ export function updateMachinesYaml(phrenPath, machine, profile) {
|
|
|
1087
1087
|
}
|
|
1088
1088
|
}
|
|
1089
1089
|
catch (err) {
|
|
1090
|
-
if ((process.env.PHREN_DEBUG
|
|
1091
|
-
process.stderr.write(`[phren] updateMachinesYaml parse: ${
|
|
1090
|
+
if ((process.env.PHREN_DEBUG))
|
|
1091
|
+
process.stderr.write(`[phren] updateMachinesYaml parse: ${errorMessage(err)}\n`);
|
|
1092
1092
|
}
|
|
1093
1093
|
// Passive init/link refreshes should keep an existing mapping; explicit overrides can remap.
|
|
1094
1094
|
if (hasExistingMapping && !machine && !profile)
|
|
1095
1095
|
return;
|
|
1096
1096
|
const mapping = setMachineProfile(phrenPath, machineName, profileName);
|
|
1097
|
-
if (!mapping.ok && (process.env.PHREN_DEBUG
|
|
1097
|
+
if (!mapping.ok && (process.env.PHREN_DEBUG)) {
|
|
1098
1098
|
process.stderr.write(`[phren] updateMachinesYaml setMachineProfile: ${mapping.error}\n`);
|
|
1099
1099
|
}
|
|
1100
1100
|
}
|
|
@@ -1289,8 +1289,8 @@ export function runPostInitVerify(phrenPath) {
|
|
|
1289
1289
|
ftsOk = entries.some(d => d.isDirectory() && !d.name.startsWith("."));
|
|
1290
1290
|
}
|
|
1291
1291
|
catch (err) {
|
|
1292
|
-
if ((process.env.PHREN_DEBUG
|
|
1293
|
-
process.stderr.write(`[phren] runPostInitVerify projectScan: ${
|
|
1292
|
+
if ((process.env.PHREN_DEBUG))
|
|
1293
|
+
process.stderr.write(`[phren] runPostInitVerify projectScan: ${errorMessage(err)}\n`);
|
|
1294
1294
|
ftsOk = false;
|
|
1295
1295
|
}
|
|
1296
1296
|
checks.push({
|
package/mcp/dist/init.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as crypto from "crypto";
|
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
9
|
import { configureAllHooks } from "./hooks.js";
|
|
10
10
|
import { getMachineName, machineFilePath, persistMachineName } from "./machine-identity.js";
|
|
11
|
-
import { atomicWriteText, debugLog, isRecord, hookConfigPath, homeDir, homePath, expandHomePath, findPhrenPath, readRootManifest, writeRootManifest, } from "./shared.js";
|
|
11
|
+
import { atomicWriteText, debugLog, isRecord, hookConfigPath, homeDir, homePath, expandHomePath, findPhrenPath, getProjectDirs, readRootManifest, writeRootManifest, } from "./shared.js";
|
|
12
12
|
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
13
13
|
import { codexJsonCandidates, copilotMcpCandidates, cursorMcpCandidates, vscodeMcpCandidates, } from "./provider-adapters.js";
|
|
14
14
|
export { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, resetVSCodeProbeCache, patchJsonFile, } from "./init-config.js";
|
|
@@ -406,7 +406,7 @@ async function runWalkthrough(phrenPath) {
|
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
catch (err) {
|
|
409
|
-
if ((process.env.PHREN_DEBUG
|
|
409
|
+
if ((process.env.PHREN_DEBUG))
|
|
410
410
|
process.stderr.write(`[phren] init ollamaCheck: ${errorMessage(err)}\n`);
|
|
411
411
|
}
|
|
412
412
|
printSection("Auto-Capture (Optional)");
|
|
@@ -910,32 +910,36 @@ export async function runInit(opts = {}) {
|
|
|
910
910
|
}
|
|
911
911
|
let phrenPath = resolveInitPhrenPath(opts);
|
|
912
912
|
const dryRun = Boolean(opts.dryRun);
|
|
913
|
-
// Migrate
|
|
914
|
-
// Only runs when the resolved phrenPath
|
|
913
|
+
// Migrate the legacy hidden store directory into ~/.phren when upgrading
|
|
914
|
+
// from the previous product name. Only runs when the resolved phrenPath
|
|
915
|
+
// doesn't exist yet but the legacy directory does.
|
|
915
916
|
if (!opts._walkthroughStoragePath && !fs.existsSync(phrenPath)) {
|
|
916
|
-
|
|
917
|
-
|
|
917
|
+
// Pre-rebrand directory name — kept as literal for migration
|
|
918
|
+
const legacyPath = path.resolve(homePath(".cortex"));
|
|
919
|
+
if (legacyPath !== phrenPath && fs.existsSync(legacyPath) && hasInstallMarkers(legacyPath)) {
|
|
918
920
|
if (!dryRun) {
|
|
919
|
-
fs.renameSync(
|
|
921
|
+
fs.renameSync(legacyPath, phrenPath);
|
|
920
922
|
}
|
|
921
|
-
console.log(`Migrated
|
|
923
|
+
console.log(`Migrated legacy store → ~/.phren`);
|
|
922
924
|
}
|
|
923
925
|
}
|
|
924
|
-
// Rename stale
|
|
925
|
-
//
|
|
926
|
+
// Rename stale legacy skill names left over from the rebrand. Runs on every
|
|
927
|
+
// init so users who already migrated the directory still get the fix.
|
|
926
928
|
const skillsMigrateDir = path.join(phrenPath, "global", "skills");
|
|
927
929
|
if (!dryRun && fs.existsSync(skillsMigrateDir)) {
|
|
930
|
+
const legacySkillName = "cortex.md";
|
|
931
|
+
const legacySkillPrefix = "cortex-";
|
|
928
932
|
for (const entry of fs.readdirSync(skillsMigrateDir)) {
|
|
929
933
|
if (!entry.endsWith(".md"))
|
|
930
934
|
continue;
|
|
931
|
-
if (entry ===
|
|
935
|
+
if (entry === legacySkillName) {
|
|
932
936
|
const dest = path.join(skillsMigrateDir, "phren.md");
|
|
933
937
|
if (!fs.existsSync(dest)) {
|
|
934
938
|
fs.renameSync(path.join(skillsMigrateDir, entry), dest);
|
|
935
939
|
}
|
|
936
940
|
}
|
|
937
|
-
else if (entry.startsWith(
|
|
938
|
-
const newName = entry.
|
|
941
|
+
else if (entry.startsWith(legacySkillPrefix)) {
|
|
942
|
+
const newName = `phren-${entry.slice(legacySkillPrefix.length)}`;
|
|
939
943
|
const dest = path.join(skillsMigrateDir, newName);
|
|
940
944
|
if (!fs.existsSync(dest)) {
|
|
941
945
|
fs.renameSync(path.join(skillsMigrateDir, entry), dest);
|
|
@@ -1381,7 +1385,7 @@ export async function runInit(opts = {}) {
|
|
|
1381
1385
|
}
|
|
1382
1386
|
}
|
|
1383
1387
|
catch (err) {
|
|
1384
|
-
if ((process.env.PHREN_DEBUG
|
|
1388
|
+
if ((process.env.PHREN_DEBUG))
|
|
1385
1389
|
process.stderr.write(`[phren] init ollamaInstallHint: ${errorMessage(err)}\n`);
|
|
1386
1390
|
}
|
|
1387
1391
|
}
|
|
@@ -1554,7 +1558,122 @@ export async function runHooksMode(modeArg) {
|
|
|
1554
1558
|
log(`Claude status: ${claudeStatus}`);
|
|
1555
1559
|
log(`Restart your agent to apply changes.`);
|
|
1556
1560
|
}
|
|
1557
|
-
|
|
1561
|
+
// Agent skill directories to sweep for symlinks during uninstall
|
|
1562
|
+
function agentSkillDirs() {
|
|
1563
|
+
const home = homeDir();
|
|
1564
|
+
return [
|
|
1565
|
+
homePath(".claude", "skills"),
|
|
1566
|
+
path.join(home, ".cursor", "skills"),
|
|
1567
|
+
path.join(home, ".copilot", "skills"),
|
|
1568
|
+
path.join(home, ".codex", "skills"),
|
|
1569
|
+
];
|
|
1570
|
+
}
|
|
1571
|
+
// Remove skill symlinks that resolve inside phrenPath. Only touches symlinks, never regular files.
|
|
1572
|
+
function sweepSkillSymlinks(phrenPath) {
|
|
1573
|
+
const resolvedPhren = path.resolve(phrenPath);
|
|
1574
|
+
for (const dir of agentSkillDirs()) {
|
|
1575
|
+
if (!fs.existsSync(dir))
|
|
1576
|
+
continue;
|
|
1577
|
+
let entries;
|
|
1578
|
+
try {
|
|
1579
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1580
|
+
}
|
|
1581
|
+
catch (err) {
|
|
1582
|
+
debugLog(`sweepSkillSymlinks: readdirSync failed for ${dir}: ${errorMessage(err)}`);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
for (const entry of entries) {
|
|
1586
|
+
if (!entry.isSymbolicLink())
|
|
1587
|
+
continue;
|
|
1588
|
+
const fullPath = path.join(dir, entry.name);
|
|
1589
|
+
try {
|
|
1590
|
+
const target = fs.realpathSync(fullPath);
|
|
1591
|
+
if (target.startsWith(resolvedPhren + path.sep) || target === resolvedPhren) {
|
|
1592
|
+
fs.unlinkSync(fullPath);
|
|
1593
|
+
log(` Removed skill symlink: ${fullPath}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
catch (err) {
|
|
1597
|
+
debugLog(`sweepSkillSymlinks: could not check/remove ${fullPath}: ${errorMessage(err)}`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
// Filter phren hook entries from an agent hooks file. Returns true if the file was changed.
|
|
1603
|
+
// Deletes the file if no hooks remain. `commandField` is the JSON key holding the command
|
|
1604
|
+
// string in each hook entry (e.g. "bash" for Copilot, "command" for Codex).
|
|
1605
|
+
function filterAgentHooks(filePath, commandField) {
|
|
1606
|
+
if (!fs.existsSync(filePath))
|
|
1607
|
+
return false;
|
|
1608
|
+
try {
|
|
1609
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1610
|
+
if (!isRecord(raw) || !isRecord(raw.hooks))
|
|
1611
|
+
return false;
|
|
1612
|
+
const hooks = raw.hooks;
|
|
1613
|
+
let changed = false;
|
|
1614
|
+
for (const event of Object.keys(hooks)) {
|
|
1615
|
+
const entries = hooks[event];
|
|
1616
|
+
if (!Array.isArray(entries))
|
|
1617
|
+
continue;
|
|
1618
|
+
const filtered = entries.filter((e) => !(isRecord(e) && typeof e[commandField] === "string" && isPhrenCommand(e[commandField])));
|
|
1619
|
+
if (filtered.length !== entries.length) {
|
|
1620
|
+
hooks[event] = filtered;
|
|
1621
|
+
changed = true;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (!changed)
|
|
1625
|
+
return false;
|
|
1626
|
+
// Remove empty hook event keys
|
|
1627
|
+
for (const event of Object.keys(hooks)) {
|
|
1628
|
+
if (Array.isArray(hooks[event]) && hooks[event].length === 0) {
|
|
1629
|
+
delete hooks[event];
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (Object.keys(hooks).length === 0) {
|
|
1633
|
+
fs.unlinkSync(filePath);
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
atomicWriteText(filePath, JSON.stringify(raw, null, 2));
|
|
1637
|
+
}
|
|
1638
|
+
return true;
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
debugLog(`filterAgentHooks: failed for ${filePath}: ${errorMessage(err)}`);
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async function promptUninstallConfirm(phrenPath) {
|
|
1646
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1647
|
+
return true;
|
|
1648
|
+
// Show summary of what will be deleted
|
|
1649
|
+
try {
|
|
1650
|
+
const projectDirs = getProjectDirs(phrenPath);
|
|
1651
|
+
const projectCount = projectDirs.length;
|
|
1652
|
+
let findingCount = 0;
|
|
1653
|
+
for (const dir of projectDirs) {
|
|
1654
|
+
const findingsFile = path.join(dir, "FINDINGS.md");
|
|
1655
|
+
if (fs.existsSync(findingsFile)) {
|
|
1656
|
+
const content = fs.readFileSync(findingsFile, "utf8");
|
|
1657
|
+
findingCount += content.split("\n").filter((l) => l.startsWith("- ")).length;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
log(`\n Will delete: ${phrenPath}`);
|
|
1661
|
+
log(` Contains: ${projectCount} project(s), ~${findingCount} finding(s)`);
|
|
1662
|
+
}
|
|
1663
|
+
catch (err) {
|
|
1664
|
+
debugLog(`promptUninstallConfirm: summary failed: ${errorMessage(err)}`);
|
|
1665
|
+
log(`\n Will delete: ${phrenPath}`);
|
|
1666
|
+
}
|
|
1667
|
+
const readline = await import("readline");
|
|
1668
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1669
|
+
return new Promise((resolve) => {
|
|
1670
|
+
rl.question(`\nThis will permanently delete ${phrenPath} and all phren data. Type 'yes' to confirm: `, (answer) => {
|
|
1671
|
+
rl.close();
|
|
1672
|
+
resolve(answer.trim().toLowerCase() === "yes");
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
export async function runUninstall(opts = {}) {
|
|
1558
1677
|
const phrenPath = findPhrenPath();
|
|
1559
1678
|
const manifest = phrenPath ? readRootManifest(phrenPath) : null;
|
|
1560
1679
|
if (manifest?.installMode === "project-local" && phrenPath) {
|
|
@@ -1575,6 +1694,27 @@ export async function runUninstall() {
|
|
|
1575
1694
|
return;
|
|
1576
1695
|
}
|
|
1577
1696
|
log("\nUninstalling phren...\n");
|
|
1697
|
+
// Confirmation prompt (shared-mode only — project-local is low-stakes)
|
|
1698
|
+
if (!opts.yes) {
|
|
1699
|
+
const confirmed = phrenPath
|
|
1700
|
+
? await promptUninstallConfirm(phrenPath)
|
|
1701
|
+
: (process.stdin.isTTY && process.stdout.isTTY
|
|
1702
|
+
? await (async () => {
|
|
1703
|
+
const readline = await import("readline");
|
|
1704
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1705
|
+
return new Promise((resolve) => {
|
|
1706
|
+
rl.question("This will remove all phren config and hooks. Type 'yes' to confirm: ", (answer) => {
|
|
1707
|
+
rl.close();
|
|
1708
|
+
resolve(answer.trim().toLowerCase() === "yes");
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
})()
|
|
1712
|
+
: true);
|
|
1713
|
+
if (!confirmed) {
|
|
1714
|
+
log("Uninstall cancelled.");
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1578
1718
|
const home = homeDir();
|
|
1579
1719
|
const machineFile = machineFilePath();
|
|
1580
1720
|
const settingsPath = hookConfigPath("claude");
|
|
@@ -1677,12 +1817,11 @@ export async function runUninstall() {
|
|
|
1677
1817
|
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1678
1818
|
}
|
|
1679
1819
|
}
|
|
1680
|
-
// Remove Copilot hooks file (
|
|
1820
|
+
// Remove phren entries from Copilot hooks file (filter, don't bulk-delete)
|
|
1681
1821
|
const copilotHooksFile = hookConfigPath("copilot", (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1682
1822
|
try {
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
log(` Removed Copilot hooks file (${copilotHooksFile})`);
|
|
1823
|
+
if (filterAgentHooks(copilotHooksFile, "bash")) {
|
|
1824
|
+
log(` Removed phren entries from Copilot hooks (${copilotHooksFile})`);
|
|
1686
1825
|
}
|
|
1687
1826
|
}
|
|
1688
1827
|
catch (err) {
|
|
@@ -1709,13 +1848,12 @@ export async function runUninstall() {
|
|
|
1709
1848
|
catch (err) {
|
|
1710
1849
|
debugLog(`uninstall: cleanup failed for ${cursorHooksFile}: ${errorMessage(err)}`);
|
|
1711
1850
|
}
|
|
1712
|
-
// Remove Codex hooks file
|
|
1851
|
+
// Remove phren entries from Codex hooks file (filter, don't bulk-delete)
|
|
1713
1852
|
const uninstallPhrenPath = (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1714
1853
|
const codexHooksFile = hookConfigPath("codex", uninstallPhrenPath);
|
|
1715
1854
|
try {
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1718
|
-
log(` Removed Codex hooks file (${codexHooksFile})`);
|
|
1855
|
+
if (filterAgentHooks(codexHooksFile, "command")) {
|
|
1856
|
+
log(` Removed phren entries from Codex hooks (${codexHooksFile})`);
|
|
1719
1857
|
}
|
|
1720
1858
|
}
|
|
1721
1859
|
catch (err) {
|
|
@@ -1748,6 +1886,15 @@ export async function runUninstall() {
|
|
|
1748
1886
|
catch (err) {
|
|
1749
1887
|
debugLog(`uninstall: cleanup failed for ${machineFile}: ${errorMessage(err)}`);
|
|
1750
1888
|
}
|
|
1889
|
+
// Sweep agent skill directories for symlinks pointing into the phren store
|
|
1890
|
+
if (phrenPath) {
|
|
1891
|
+
try {
|
|
1892
|
+
sweepSkillSymlinks(phrenPath);
|
|
1893
|
+
}
|
|
1894
|
+
catch (err) {
|
|
1895
|
+
debugLog(`uninstall: skill symlink sweep failed: ${errorMessage(err)}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1751
1898
|
if (phrenPath && fs.existsSync(phrenPath)) {
|
|
1752
1899
|
try {
|
|
1753
1900
|
fs.rmSync(phrenPath, { recursive: true, force: true });
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
4
|
import { getProjectDirs } from "./shared.js";
|
|
5
5
|
import { TASK_FILE_ALIASES } from "./data-tasks.js";
|
|
6
|
+
import { errorMessage } from "./utils.js";
|
|
6
7
|
function fileChecksum(filePath) {
|
|
7
8
|
const content = fs.readFileSync(filePath);
|
|
8
9
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
@@ -18,8 +19,8 @@ function loadChecksums(phrenPath) {
|
|
|
18
19
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
19
20
|
}
|
|
20
21
|
catch (err) {
|
|
21
|
-
if ((process.env.PHREN_DEBUG
|
|
22
|
-
process.stderr.write(`[phren] loadChecksums: ${
|
|
22
|
+
if ((process.env.PHREN_DEBUG))
|
|
23
|
+
process.stderr.write(`[phren] loadChecksums: ${errorMessage(err)}\n`);
|
|
23
24
|
return {};
|
|
24
25
|
}
|
|
25
26
|
}
|
package/mcp/dist/link-context.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as yaml from "js-yaml";
|
|
|
4
4
|
import { isValidProjectName } from "./utils.js";
|
|
5
5
|
import { homeDir, homePath } from "./shared.js";
|
|
6
6
|
import { resolveTaskFilePath } from "./data-tasks.js";
|
|
7
|
-
|
|
7
|
+
import { log } from "./init-shared.js";
|
|
8
8
|
function contextFilePath() {
|
|
9
9
|
return homePath(".phren-context.md");
|
|
10
10
|
}
|
package/mcp/dist/link-doctor.js
CHANGED
|
@@ -199,14 +199,14 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
199
199
|
fsMs = Date.now() - t0;
|
|
200
200
|
}
|
|
201
201
|
catch (err) {
|
|
202
|
-
if ((process.env.PHREN_DEBUG
|
|
202
|
+
if ((process.env.PHREN_DEBUG))
|
|
203
203
|
process.stderr.write(`[phren] doctor fsBenchmark: ${errorMessage(err)}\n`);
|
|
204
204
|
fsMs = -1;
|
|
205
205
|
try {
|
|
206
206
|
fs.unlinkSync(fsBenchFile);
|
|
207
207
|
}
|
|
208
208
|
catch (e2) {
|
|
209
|
-
if ((process.env.PHREN_DEBUG
|
|
209
|
+
if ((process.env.PHREN_DEBUG))
|
|
210
210
|
process.stderr.write(`[phren] doctor fsBenchmarkCleanup: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
211
211
|
}
|
|
212
212
|
}
|
|
@@ -329,7 +329,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
329
329
|
runtime = JSON.parse(fs.readFileSync(runtimeHealthPath, "utf8"));
|
|
330
330
|
}
|
|
331
331
|
catch (err) {
|
|
332
|
-
if ((process.env.PHREN_DEBUG
|
|
332
|
+
if ((process.env.PHREN_DEBUG))
|
|
333
333
|
process.stderr.write(`[phren] doctor runtimeHealth: ${errorMessage(err)}\n`);
|
|
334
334
|
runtime = null;
|
|
335
335
|
}
|