@phren/cli 0.0.10 → 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 +35 -63
- 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 +39 -32
- 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-dedup.js +9 -9
- 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/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 +19 -13
- 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/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
|
}
|
package/mcp/dist/link-skills.js
CHANGED
|
@@ -108,7 +108,75 @@ export function readSkillManifestHooks(phrenPath) {
|
|
|
108
108
|
}
|
|
109
109
|
return Object.keys(result).length > 0 ? result : null;
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Returns true if `destPath` is a symlink whose resolved target lives under
|
|
113
|
+
* `managedRoot`. Used to decide whether phren owns a symlink.
|
|
114
|
+
*/
|
|
115
|
+
export function isManagedSymlink(destPath, managedRoot) {
|
|
116
|
+
try {
|
|
117
|
+
const stat = fs.lstatSync(destPath);
|
|
118
|
+
if (!stat.isSymbolicLink())
|
|
119
|
+
return false;
|
|
120
|
+
const target = fs.readlinkSync(destPath);
|
|
121
|
+
const resolvedTarget = path.resolve(path.dirname(destPath), target);
|
|
122
|
+
const managedPrefix = path.resolve(managedRoot) + path.sep;
|
|
123
|
+
return resolvedTarget.startsWith(managedPrefix);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if `destPath` exists and is NOT a symlink that points into
|
|
131
|
+
* managedRoot — i.e. it's a file/dir the user owns.
|
|
132
|
+
*/
|
|
133
|
+
function isUserOwnedFile(destPath, managedRoot) {
|
|
134
|
+
try {
|
|
135
|
+
fs.lstatSync(destPath);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false; // doesn't exist
|
|
139
|
+
}
|
|
140
|
+
return !isManagedSymlink(destPath, managedRoot);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Scan destDir for files that phren would want to link (based on srcDir) but
|
|
144
|
+
* can't because a user-owned file already occupies the destination slot.
|
|
145
|
+
*/
|
|
146
|
+
export function detectSkillCollisions(srcDir, destDir, managedRoot) {
|
|
147
|
+
if (!fs.existsSync(srcDir) || !fs.existsSync(destDir))
|
|
148
|
+
return [];
|
|
149
|
+
const collisions = [];
|
|
150
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
151
|
+
const srcPath = path.join(srcDir, entry);
|
|
152
|
+
const stat = fs.statSync(srcPath);
|
|
153
|
+
if (stat.isFile() && entry.endsWith(".md")) {
|
|
154
|
+
const destPath = path.join(destDir, entry);
|
|
155
|
+
if (isUserOwnedFile(destPath, managedRoot)) {
|
|
156
|
+
const skillName = entry.replace(/\.md$/, "");
|
|
157
|
+
collisions.push({
|
|
158
|
+
skillName,
|
|
159
|
+
destPath,
|
|
160
|
+
message: `Skill '${skillName}' — user file already exists at ${destPath}. Rename or remove it to use phren's version.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (stat.isDirectory()) {
|
|
165
|
+
const skillFile = path.join(srcPath, "SKILL.md");
|
|
166
|
+
if (fs.existsSync(skillFile)) {
|
|
167
|
+
const destPath = path.join(destDir, entry);
|
|
168
|
+
if (isUserOwnedFile(destPath, managedRoot)) {
|
|
169
|
+
collisions.push({
|
|
170
|
+
skillName: entry,
|
|
171
|
+
destPath,
|
|
172
|
+
message: `Skill '${entry}' — user directory already exists at ${destPath}. Rename or remove it to use phren's version.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return collisions;
|
|
179
|
+
}
|
|
112
180
|
function cleanupManagedSkillLinks(destDir, expectedNames, managedRoot) {
|
|
113
181
|
if (!fs.existsSync(destDir))
|
|
114
182
|
return;
|
|
@@ -117,27 +185,22 @@ function cleanupManagedSkillLinks(destDir, expectedNames, managedRoot) {
|
|
|
117
185
|
continue;
|
|
118
186
|
const destPath = path.join(destDir, entry);
|
|
119
187
|
try {
|
|
120
|
-
|
|
121
|
-
if (!stat.isSymbolicLink())
|
|
122
|
-
continue;
|
|
123
|
-
const target = fs.readlinkSync(destPath);
|
|
124
|
-
const resolvedTarget = path.resolve(path.dirname(destPath), target);
|
|
125
|
-
const managedPrefix = path.resolve(managedRoot) + path.sep;
|
|
126
|
-
if (!resolvedTarget.startsWith(managedPrefix))
|
|
188
|
+
if (!isManagedSymlink(destPath, managedRoot))
|
|
127
189
|
continue;
|
|
128
190
|
fs.unlinkSync(destPath);
|
|
129
191
|
}
|
|
130
192
|
catch (err) {
|
|
131
|
-
if ((process.env.PHREN_DEBUG
|
|
193
|
+
if ((process.env.PHREN_DEBUG))
|
|
132
194
|
process.stderr.write(`[phren] cleanupManagedSkillLinks: ${errorMessage(err)}\n`);
|
|
133
195
|
}
|
|
134
196
|
}
|
|
135
197
|
}
|
|
136
198
|
export function linkSkillsDir(srcDir, destDir, managedRoot, symlinkFile, opts) {
|
|
137
199
|
if (!fs.existsSync(srcDir))
|
|
138
|
-
return;
|
|
200
|
+
return [];
|
|
139
201
|
fs.mkdirSync(destDir, { recursive: true });
|
|
140
202
|
const expectedNames = new Set();
|
|
203
|
+
const collisions = [];
|
|
141
204
|
for (const entry of fs.readdirSync(srcDir)) {
|
|
142
205
|
const srcPath = path.join(srcDir, entry);
|
|
143
206
|
const stat = fs.statSync(srcPath);
|
|
@@ -146,20 +209,43 @@ export function linkSkillsDir(srcDir, destDir, managedRoot, symlinkFile, opts) {
|
|
|
146
209
|
continue;
|
|
147
210
|
}
|
|
148
211
|
if (stat.isFile() && entry.endsWith(".md")) {
|
|
212
|
+
const destPath = path.join(destDir, entry);
|
|
213
|
+
if (isUserOwnedFile(destPath, managedRoot)) {
|
|
214
|
+
const collision = {
|
|
215
|
+
skillName,
|
|
216
|
+
destPath,
|
|
217
|
+
message: `Skipping skill '${skillName}' — user skill already exists at ${destPath}. To use phren's version, rename or remove your skill first.`,
|
|
218
|
+
};
|
|
219
|
+
collisions.push(collision);
|
|
220
|
+
process.stderr.write(`[phren] ${collision.message}\n`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
149
223
|
expectedNames.add(entry);
|
|
150
|
-
symlinkFile(srcPath,
|
|
224
|
+
symlinkFile(srcPath, destPath, managedRoot);
|
|
151
225
|
}
|
|
152
226
|
else if (stat.isDirectory()) {
|
|
153
227
|
const skillFile = path.join(srcPath, "SKILL.md");
|
|
154
228
|
if (fs.existsSync(skillFile)) {
|
|
229
|
+
const destPath = path.join(destDir, entry);
|
|
230
|
+
if (isUserOwnedFile(destPath, managedRoot)) {
|
|
231
|
+
const collision = {
|
|
232
|
+
skillName,
|
|
233
|
+
destPath,
|
|
234
|
+
message: `Skipping skill '${skillName}' — user skill already exists at ${destPath}. To use phren's version, rename or remove your skill first.`,
|
|
235
|
+
};
|
|
236
|
+
collisions.push(collision);
|
|
237
|
+
process.stderr.write(`[phren] ${collision.message}\n`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
155
240
|
expectedNames.add(entry);
|
|
156
241
|
// Symlink the entire skill directory so bundled scripts and assets are accessible.
|
|
157
242
|
// Relative paths in the skill body remain valid because the directory structure is preserved.
|
|
158
|
-
symlinkFile(srcPath,
|
|
243
|
+
symlinkFile(srcPath, destPath, managedRoot);
|
|
159
244
|
}
|
|
160
245
|
}
|
|
161
246
|
}
|
|
162
247
|
cleanupManagedSkillLinks(destDir, expectedNames, managedRoot);
|
|
248
|
+
return collisions;
|
|
163
249
|
}
|
|
164
250
|
export function writeSkillMd(phrenPath) {
|
|
165
251
|
const lifecycle = buildSharedLifecycleCommands();
|