@phren/cli 0.0.10 → 0.0.12
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 +11 -17
- package/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-actions.js +58 -71
- package/mcp/dist/cli-config.js +337 -131
- package/mcp/dist/cli-extract.js +3 -2
- package/mcp/dist/cli-govern.js +35 -63
- package/mcp/dist/cli-graph.js +19 -4
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +4 -4
- package/mcp/dist/cli-hooks-session.js +1 -1
- package/mcp/dist/cli-hooks.js +44 -35
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/cli.js +1 -1
- package/mcp/dist/content-archive.js +23 -14
- package/mcp/dist/content-citation.js +13 -2
- package/mcp/dist/content-dedup.js +9 -9
- package/mcp/dist/content-learning.js +6 -4
- package/mcp/dist/content-metadata.js +10 -0
- package/mcp/dist/core-finding.js +1 -1
- package/mcp/dist/data-access.js +10 -31
- package/mcp/dist/data-tasks.js +5 -26
- package/mcp/dist/embedding.js +7 -8
- package/mcp/dist/entrypoint.js +133 -102
- package/mcp/dist/finding-impact.js +1 -32
- package/mcp/dist/finding-journal.js +1 -1
- package/mcp/dist/finding-lifecycle.js +2 -7
- package/mcp/dist/governance-locks.js +12 -5
- package/mcp/dist/governance-policy.js +156 -9
- package/mcp/dist/governance-scores.js +4 -10
- package/mcp/dist/hooks.js +62 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +4 -25
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +6 -55
- package/mcp/dist/init-shared.js +53 -1
- package/mcp/dist/init.js +191 -29
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +2 -2
- package/mcp/dist/link-doctor.js +14 -57
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +16 -75
- 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 +12 -7
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +16 -20
- package/mcp/dist/mcp-graph.js +12 -12
- package/mcp/dist/mcp-hooks.js +1 -1
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +11 -16
- package/mcp/dist/mcp-session.js +12 -2
- package/mcp/dist/memory-ui-assets.js +1 -36
- package/mcp/dist/memory-ui-graph.js +152 -50
- package/mcp/dist/memory-ui-page.js +30 -5
- package/mcp/dist/memory-ui-scripts.js +252 -63
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-core.js +2 -0
- package/mcp/dist/phren-paths.js +8 -9
- package/mcp/dist/proactivity.js +5 -5
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +64 -17
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +22 -19
- package/mcp/dist/session-checkpoints.js +14 -14
- package/mcp/dist/session-utils.js +3 -2
- package/mcp/dist/shared-data-utils.js +28 -0
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-governance.js +1 -1
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +21 -23
- package/mcp/dist/shared-search-fallback.js +15 -25
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +5 -6
- package/mcp/dist/shell-entry.js +1 -1
- package/mcp/dist/shell-input.js +63 -53
- package/mcp/dist/shell-palette.js +6 -1
- package/mcp/dist/shell-render.js +9 -5
- package/mcp/dist/shell-state-store.js +2 -5
- package/mcp/dist/shell-view.js +7 -6
- package/mcp/dist/shell.js +5 -55
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +43 -21
- 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 +4 -4
- package/package.json +2 -3
- package/skills/docs.md +11 -11
- package/starter/README.md +1 -1
- package/starter/global/CLAUDE.md +2 -2
- package/starter/global/skills/audit.md +106 -0
- package/mcp/dist/cli-hooks-retrieval.js +0 -2
- package/mcp/dist/impact-scoring.js +0 -22
- package/mcp/dist/shared-paths.js +0 -1
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)");
|
|
@@ -845,9 +845,11 @@ async function runProjectLocalInit(opts = {}) {
|
|
|
845
845
|
* Configure MCP for all detected AI coding tools (Claude, VS Code, Cursor, Copilot, Codex).
|
|
846
846
|
* @param verb - label used in log messages, e.g. "Updated" or "Configured"
|
|
847
847
|
*/
|
|
848
|
-
function configureMcpTargets(phrenPath, opts, verb) {
|
|
848
|
+
export function configureMcpTargets(phrenPath, opts, verb = "Configured") {
|
|
849
|
+
let claudeStatus = "no_settings";
|
|
849
850
|
try {
|
|
850
851
|
const status = configureClaude(phrenPath, { mcpEnabled: opts.mcpEnabled, hooksEnabled: opts.hooksEnabled });
|
|
852
|
+
claudeStatus = status ?? "installed";
|
|
851
853
|
if (status === "disabled" || status === "already_disabled") {
|
|
852
854
|
log(` ${verb} Claude Code hooks (MCP disabled)`);
|
|
853
855
|
}
|
|
@@ -858,31 +860,44 @@ function configureMcpTargets(phrenPath, opts, verb) {
|
|
|
858
860
|
catch (e) {
|
|
859
861
|
log(` Could not configure Claude Code settings (${e}), add manually`);
|
|
860
862
|
}
|
|
863
|
+
let vsStatus = "no_vscode";
|
|
861
864
|
try {
|
|
862
|
-
|
|
863
|
-
logMcpTargetStatus("VS Code",
|
|
865
|
+
vsStatus = configureVSCode(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_vscode";
|
|
866
|
+
logMcpTargetStatus("VS Code", vsStatus, verb);
|
|
864
867
|
}
|
|
865
868
|
catch (err) {
|
|
866
869
|
debugLog(`configureVSCode failed: ${errorMessage(err)}`);
|
|
867
870
|
}
|
|
871
|
+
let cursorStatus = "no_cursor";
|
|
868
872
|
try {
|
|
869
|
-
|
|
873
|
+
cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_cursor";
|
|
874
|
+
logMcpTargetStatus("Cursor", cursorStatus, verb);
|
|
870
875
|
}
|
|
871
876
|
catch (err) {
|
|
872
877
|
debugLog(`configureCursorMcp failed: ${errorMessage(err)}`);
|
|
873
878
|
}
|
|
879
|
+
let copilotStatus = "no_copilot";
|
|
874
880
|
try {
|
|
875
|
-
|
|
881
|
+
copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_copilot";
|
|
882
|
+
logMcpTargetStatus("Copilot CLI", copilotStatus, verb);
|
|
876
883
|
}
|
|
877
884
|
catch (err) {
|
|
878
885
|
debugLog(`configureCopilotMcp failed: ${errorMessage(err)}`);
|
|
879
886
|
}
|
|
887
|
+
let codexStatus = "no_codex";
|
|
880
888
|
try {
|
|
881
|
-
|
|
889
|
+
codexStatus = configureCodexMcp(phrenPath, { mcpEnabled: opts.mcpEnabled }) ?? "no_codex";
|
|
890
|
+
logMcpTargetStatus("Codex", codexStatus, verb);
|
|
882
891
|
}
|
|
883
892
|
catch (err) {
|
|
884
893
|
debugLog(`configureCodexMcp failed: ${errorMessage(err)}`);
|
|
885
894
|
}
|
|
895
|
+
const allStatuses = [claudeStatus, vsStatus, cursorStatus, copilotStatus, codexStatus];
|
|
896
|
+
if (allStatuses.some((s) => s === "installed" || s === "already_configured"))
|
|
897
|
+
return "installed";
|
|
898
|
+
if (allStatuses.some((s) => s === "disabled" || s === "already_disabled"))
|
|
899
|
+
return "disabled";
|
|
900
|
+
return claudeStatus;
|
|
886
901
|
}
|
|
887
902
|
/**
|
|
888
903
|
* Configure hooks if enabled, or log a disabled message.
|
|
@@ -910,32 +925,36 @@ export async function runInit(opts = {}) {
|
|
|
910
925
|
}
|
|
911
926
|
let phrenPath = resolveInitPhrenPath(opts);
|
|
912
927
|
const dryRun = Boolean(opts.dryRun);
|
|
913
|
-
// Migrate
|
|
914
|
-
// Only runs when the resolved phrenPath
|
|
928
|
+
// Migrate the legacy hidden store directory into ~/.phren when upgrading
|
|
929
|
+
// from the previous product name. Only runs when the resolved phrenPath
|
|
930
|
+
// doesn't exist yet but the legacy directory does.
|
|
915
931
|
if (!opts._walkthroughStoragePath && !fs.existsSync(phrenPath)) {
|
|
916
|
-
|
|
917
|
-
|
|
932
|
+
// Pre-rebrand directory name — kept as literal for migration
|
|
933
|
+
const legacyPath = path.resolve(homePath(".cortex"));
|
|
934
|
+
if (legacyPath !== phrenPath && fs.existsSync(legacyPath) && hasInstallMarkers(legacyPath)) {
|
|
918
935
|
if (!dryRun) {
|
|
919
|
-
fs.renameSync(
|
|
936
|
+
fs.renameSync(legacyPath, phrenPath);
|
|
920
937
|
}
|
|
921
|
-
console.log(`Migrated
|
|
938
|
+
console.log(`Migrated legacy store → ~/.phren`);
|
|
922
939
|
}
|
|
923
940
|
}
|
|
924
|
-
// Rename stale
|
|
925
|
-
//
|
|
941
|
+
// Rename stale legacy skill names left over from the rebrand. Runs on every
|
|
942
|
+
// init so users who already migrated the directory still get the fix.
|
|
926
943
|
const skillsMigrateDir = path.join(phrenPath, "global", "skills");
|
|
927
944
|
if (!dryRun && fs.existsSync(skillsMigrateDir)) {
|
|
945
|
+
const legacySkillName = "cortex.md";
|
|
946
|
+
const legacySkillPrefix = "cortex-";
|
|
928
947
|
for (const entry of fs.readdirSync(skillsMigrateDir)) {
|
|
929
948
|
if (!entry.endsWith(".md"))
|
|
930
949
|
continue;
|
|
931
|
-
if (entry ===
|
|
950
|
+
if (entry === legacySkillName) {
|
|
932
951
|
const dest = path.join(skillsMigrateDir, "phren.md");
|
|
933
952
|
if (!fs.existsSync(dest)) {
|
|
934
953
|
fs.renameSync(path.join(skillsMigrateDir, entry), dest);
|
|
935
954
|
}
|
|
936
955
|
}
|
|
937
|
-
else if (entry.startsWith(
|
|
938
|
-
const newName = entry.
|
|
956
|
+
else if (entry.startsWith(legacySkillPrefix)) {
|
|
957
|
+
const newName = `phren-${entry.slice(legacySkillPrefix.length)}`;
|
|
939
958
|
const dest = path.join(skillsMigrateDir, newName);
|
|
940
959
|
if (!fs.existsSync(dest)) {
|
|
941
960
|
fs.renameSync(path.join(skillsMigrateDir, entry), dest);
|
|
@@ -1381,7 +1400,7 @@ export async function runInit(opts = {}) {
|
|
|
1381
1400
|
}
|
|
1382
1401
|
}
|
|
1383
1402
|
catch (err) {
|
|
1384
|
-
if ((process.env.PHREN_DEBUG
|
|
1403
|
+
if ((process.env.PHREN_DEBUG))
|
|
1385
1404
|
process.stderr.write(`[phren] init ollamaInstallHint: ${errorMessage(err)}\n`);
|
|
1386
1405
|
}
|
|
1387
1406
|
}
|
|
@@ -1554,7 +1573,122 @@ export async function runHooksMode(modeArg) {
|
|
|
1554
1573
|
log(`Claude status: ${claudeStatus}`);
|
|
1555
1574
|
log(`Restart your agent to apply changes.`);
|
|
1556
1575
|
}
|
|
1557
|
-
|
|
1576
|
+
// Agent skill directories to sweep for symlinks during uninstall
|
|
1577
|
+
function agentSkillDirs() {
|
|
1578
|
+
const home = homeDir();
|
|
1579
|
+
return [
|
|
1580
|
+
homePath(".claude", "skills"),
|
|
1581
|
+
path.join(home, ".cursor", "skills"),
|
|
1582
|
+
path.join(home, ".copilot", "skills"),
|
|
1583
|
+
path.join(home, ".codex", "skills"),
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1586
|
+
// Remove skill symlinks that resolve inside phrenPath. Only touches symlinks, never regular files.
|
|
1587
|
+
function sweepSkillSymlinks(phrenPath) {
|
|
1588
|
+
const resolvedPhren = path.resolve(phrenPath);
|
|
1589
|
+
for (const dir of agentSkillDirs()) {
|
|
1590
|
+
if (!fs.existsSync(dir))
|
|
1591
|
+
continue;
|
|
1592
|
+
let entries;
|
|
1593
|
+
try {
|
|
1594
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1595
|
+
}
|
|
1596
|
+
catch (err) {
|
|
1597
|
+
debugLog(`sweepSkillSymlinks: readdirSync failed for ${dir}: ${errorMessage(err)}`);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
for (const entry of entries) {
|
|
1601
|
+
if (!entry.isSymbolicLink())
|
|
1602
|
+
continue;
|
|
1603
|
+
const fullPath = path.join(dir, entry.name);
|
|
1604
|
+
try {
|
|
1605
|
+
const target = fs.realpathSync(fullPath);
|
|
1606
|
+
if (target.startsWith(resolvedPhren + path.sep) || target === resolvedPhren) {
|
|
1607
|
+
fs.unlinkSync(fullPath);
|
|
1608
|
+
log(` Removed skill symlink: ${fullPath}`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
catch (err) {
|
|
1612
|
+
debugLog(`sweepSkillSymlinks: could not check/remove ${fullPath}: ${errorMessage(err)}`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// Filter phren hook entries from an agent hooks file. Returns true if the file was changed.
|
|
1618
|
+
// Deletes the file if no hooks remain. `commandField` is the JSON key holding the command
|
|
1619
|
+
// string in each hook entry (e.g. "bash" for Copilot, "command" for Codex).
|
|
1620
|
+
function filterAgentHooks(filePath, commandField) {
|
|
1621
|
+
if (!fs.existsSync(filePath))
|
|
1622
|
+
return false;
|
|
1623
|
+
try {
|
|
1624
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1625
|
+
if (!isRecord(raw) || !isRecord(raw.hooks))
|
|
1626
|
+
return false;
|
|
1627
|
+
const hooks = raw.hooks;
|
|
1628
|
+
let changed = false;
|
|
1629
|
+
for (const event of Object.keys(hooks)) {
|
|
1630
|
+
const entries = hooks[event];
|
|
1631
|
+
if (!Array.isArray(entries))
|
|
1632
|
+
continue;
|
|
1633
|
+
const filtered = entries.filter((e) => !(isRecord(e) && typeof e[commandField] === "string" && isPhrenCommand(e[commandField])));
|
|
1634
|
+
if (filtered.length !== entries.length) {
|
|
1635
|
+
hooks[event] = filtered;
|
|
1636
|
+
changed = true;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (!changed)
|
|
1640
|
+
return false;
|
|
1641
|
+
// Remove empty hook event keys
|
|
1642
|
+
for (const event of Object.keys(hooks)) {
|
|
1643
|
+
if (Array.isArray(hooks[event]) && hooks[event].length === 0) {
|
|
1644
|
+
delete hooks[event];
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (Object.keys(hooks).length === 0) {
|
|
1648
|
+
fs.unlinkSync(filePath);
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
atomicWriteText(filePath, JSON.stringify(raw, null, 2));
|
|
1652
|
+
}
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
catch (err) {
|
|
1656
|
+
debugLog(`filterAgentHooks: failed for ${filePath}: ${errorMessage(err)}`);
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
async function promptUninstallConfirm(phrenPath) {
|
|
1661
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1662
|
+
return true;
|
|
1663
|
+
// Show summary of what will be deleted
|
|
1664
|
+
try {
|
|
1665
|
+
const projectDirs = getProjectDirs(phrenPath);
|
|
1666
|
+
const projectCount = projectDirs.length;
|
|
1667
|
+
let findingCount = 0;
|
|
1668
|
+
for (const dir of projectDirs) {
|
|
1669
|
+
const findingsFile = path.join(dir, "FINDINGS.md");
|
|
1670
|
+
if (fs.existsSync(findingsFile)) {
|
|
1671
|
+
const content = fs.readFileSync(findingsFile, "utf8");
|
|
1672
|
+
findingCount += content.split("\n").filter((l) => l.startsWith("- ")).length;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
log(`\n Will delete: ${phrenPath}`);
|
|
1676
|
+
log(` Contains: ${projectCount} project(s), ~${findingCount} finding(s)`);
|
|
1677
|
+
}
|
|
1678
|
+
catch (err) {
|
|
1679
|
+
debugLog(`promptUninstallConfirm: summary failed: ${errorMessage(err)}`);
|
|
1680
|
+
log(`\n Will delete: ${phrenPath}`);
|
|
1681
|
+
}
|
|
1682
|
+
const readline = await import("readline");
|
|
1683
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1684
|
+
return new Promise((resolve) => {
|
|
1685
|
+
rl.question(`\nThis will permanently delete ${phrenPath} and all phren data. Type 'yes' to confirm: `, (answer) => {
|
|
1686
|
+
rl.close();
|
|
1687
|
+
resolve(answer.trim().toLowerCase() === "yes");
|
|
1688
|
+
});
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
export async function runUninstall(opts = {}) {
|
|
1558
1692
|
const phrenPath = findPhrenPath();
|
|
1559
1693
|
const manifest = phrenPath ? readRootManifest(phrenPath) : null;
|
|
1560
1694
|
if (manifest?.installMode === "project-local" && phrenPath) {
|
|
@@ -1575,6 +1709,27 @@ export async function runUninstall() {
|
|
|
1575
1709
|
return;
|
|
1576
1710
|
}
|
|
1577
1711
|
log("\nUninstalling phren...\n");
|
|
1712
|
+
// Confirmation prompt (shared-mode only — project-local is low-stakes)
|
|
1713
|
+
if (!opts.yes) {
|
|
1714
|
+
const confirmed = phrenPath
|
|
1715
|
+
? await promptUninstallConfirm(phrenPath)
|
|
1716
|
+
: (process.stdin.isTTY && process.stdout.isTTY
|
|
1717
|
+
? await (async () => {
|
|
1718
|
+
const readline = await import("readline");
|
|
1719
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1720
|
+
return new Promise((resolve) => {
|
|
1721
|
+
rl.question("This will remove all phren config and hooks. Type 'yes' to confirm: ", (answer) => {
|
|
1722
|
+
rl.close();
|
|
1723
|
+
resolve(answer.trim().toLowerCase() === "yes");
|
|
1724
|
+
});
|
|
1725
|
+
});
|
|
1726
|
+
})()
|
|
1727
|
+
: true);
|
|
1728
|
+
if (!confirmed) {
|
|
1729
|
+
log("Uninstall cancelled.");
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1578
1733
|
const home = homeDir();
|
|
1579
1734
|
const machineFile = machineFilePath();
|
|
1580
1735
|
const settingsPath = hookConfigPath("claude");
|
|
@@ -1677,12 +1832,11 @@ export async function runUninstall() {
|
|
|
1677
1832
|
debugLog(`uninstall: cleanup failed for ${mcpFile}: ${errorMessage(err)}`);
|
|
1678
1833
|
}
|
|
1679
1834
|
}
|
|
1680
|
-
// Remove Copilot hooks file (
|
|
1835
|
+
// Remove phren entries from Copilot hooks file (filter, don't bulk-delete)
|
|
1681
1836
|
const copilotHooksFile = hookConfigPath("copilot", (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH);
|
|
1682
1837
|
try {
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
log(` Removed Copilot hooks file (${copilotHooksFile})`);
|
|
1838
|
+
if (filterAgentHooks(copilotHooksFile, "bash")) {
|
|
1839
|
+
log(` Removed phren entries from Copilot hooks (${copilotHooksFile})`);
|
|
1686
1840
|
}
|
|
1687
1841
|
}
|
|
1688
1842
|
catch (err) {
|
|
@@ -1709,13 +1863,12 @@ export async function runUninstall() {
|
|
|
1709
1863
|
catch (err) {
|
|
1710
1864
|
debugLog(`uninstall: cleanup failed for ${cursorHooksFile}: ${errorMessage(err)}`);
|
|
1711
1865
|
}
|
|
1712
|
-
// Remove Codex hooks file
|
|
1866
|
+
// Remove phren entries from Codex hooks file (filter, don't bulk-delete)
|
|
1713
1867
|
const uninstallPhrenPath = (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
1714
1868
|
const codexHooksFile = hookConfigPath("codex", uninstallPhrenPath);
|
|
1715
1869
|
try {
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1718
|
-
log(` Removed Codex hooks file (${codexHooksFile})`);
|
|
1870
|
+
if (filterAgentHooks(codexHooksFile, "command")) {
|
|
1871
|
+
log(` Removed phren entries from Codex hooks (${codexHooksFile})`);
|
|
1719
1872
|
}
|
|
1720
1873
|
}
|
|
1721
1874
|
catch (err) {
|
|
@@ -1748,6 +1901,15 @@ export async function runUninstall() {
|
|
|
1748
1901
|
catch (err) {
|
|
1749
1902
|
debugLog(`uninstall: cleanup failed for ${machineFile}: ${errorMessage(err)}`);
|
|
1750
1903
|
}
|
|
1904
|
+
// Sweep agent skill directories for symlinks pointing into the phren store
|
|
1905
|
+
if (phrenPath) {
|
|
1906
|
+
try {
|
|
1907
|
+
sweepSkillSymlinks(phrenPath);
|
|
1908
|
+
}
|
|
1909
|
+
catch (err) {
|
|
1910
|
+
debugLog(`uninstall: skill symlink sweep failed: ${errorMessage(err)}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1751
1913
|
if (phrenPath && fs.existsSync(phrenPath)) {
|
|
1752
1914
|
try {
|
|
1753
1915
|
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
|
}
|
|
@@ -38,7 +38,7 @@ function writeContextFile(managedContent) {
|
|
|
38
38
|
const existing = fs.readFileSync(contextFile, "utf8");
|
|
39
39
|
if (existing.includes("<!-- phren-managed -->")) {
|
|
40
40
|
const startIdx = existing.indexOf("<!-- phren-managed -->");
|
|
41
|
-
const endIdx = existing.indexOf("<!-- phren-managed -->");
|
|
41
|
+
const endIdx = existing.indexOf("<!-- phren-managed -->", startIdx + "<!-- phren-managed -->".length);
|
|
42
42
|
const before = startIdx > 0 ? existing.slice(0, startIdx).trimEnd() : "";
|
|
43
43
|
const after = endIdx !== -1 ? existing.slice(endIdx + "<!-- phren-managed -->".length).trimStart() : "";
|
|
44
44
|
const parts = [before, wrapped, after].filter(Boolean);
|
package/mcp/dist/link-doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
4
|
import { debugLog, EXEC_TIMEOUT_QUICK_MS, getProjectDirs, isRecord, homeDir, homePath, hookConfigPath, runtimeHealthFile, } from "./shared.js";
|
|
5
|
+
import { commandVersion, versionAtLeast, nearestWritableTarget } from "./init-shared.js";
|
|
5
6
|
import { validateGovernanceJson } from "./shared-governance.js";
|
|
6
7
|
import { errorMessage } from "./utils.js";
|
|
7
8
|
import { buildIndex, queryRows } from "./shared-index.js";
|
|
@@ -34,53 +35,6 @@ function isWrapperActive(tool) {
|
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
function commandVersion(cmd, args = ["--version"]) {
|
|
38
|
-
try {
|
|
39
|
-
return execFileSync(cmd, args, {
|
|
40
|
-
encoding: "utf8",
|
|
41
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
-
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
43
|
-
}).trim();
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
debugLog(`doctor: commandVersion ${cmd} failed: ${errorMessage(err)}`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function parseSemverTriple(raw) {
|
|
51
|
-
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
52
|
-
if (!match)
|
|
53
|
-
return null;
|
|
54
|
-
return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
|
|
55
|
-
}
|
|
56
|
-
function versionAtLeast(raw, major, minor = 0) {
|
|
57
|
-
if (!raw)
|
|
58
|
-
return false;
|
|
59
|
-
const parsed = parseSemverTriple(raw);
|
|
60
|
-
if (!parsed)
|
|
61
|
-
return false;
|
|
62
|
-
const [m, n] = parsed;
|
|
63
|
-
if (m !== major)
|
|
64
|
-
return m > major;
|
|
65
|
-
return n >= minor;
|
|
66
|
-
}
|
|
67
|
-
function nearestWritableTarget(filePath) {
|
|
68
|
-
let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
|
|
69
|
-
while (!fs.existsSync(probe)) {
|
|
70
|
-
const parent = path.dirname(probe);
|
|
71
|
-
if (parent === probe)
|
|
72
|
-
return false;
|
|
73
|
-
probe = parent;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
fs.accessSync(probe, fs.constants.W_OK);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
debugLog(`doctor: writable check failed for ${filePath}: ${errorMessage(err)}`);
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
38
|
function gitRemoteStatus(phrenPath) {
|
|
85
39
|
try {
|
|
86
40
|
execFileSync("git", ["-C", phrenPath, "rev-parse", "--is-inside-work-tree"], {
|
|
@@ -199,14 +153,14 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
199
153
|
fsMs = Date.now() - t0;
|
|
200
154
|
}
|
|
201
155
|
catch (err) {
|
|
202
|
-
if ((process.env.PHREN_DEBUG
|
|
156
|
+
if ((process.env.PHREN_DEBUG))
|
|
203
157
|
process.stderr.write(`[phren] doctor fsBenchmark: ${errorMessage(err)}\n`);
|
|
204
158
|
fsMs = -1;
|
|
205
159
|
try {
|
|
206
160
|
fs.unlinkSync(fsBenchFile);
|
|
207
161
|
}
|
|
208
162
|
catch (e2) {
|
|
209
|
-
if ((process.env.PHREN_DEBUG
|
|
163
|
+
if ((process.env.PHREN_DEBUG))
|
|
210
164
|
process.stderr.write(`[phren] doctor fsBenchmarkCleanup: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
211
165
|
}
|
|
212
166
|
}
|
|
@@ -329,7 +283,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
329
283
|
runtime = JSON.parse(fs.readFileSync(runtimeHealthPath, "utf8"));
|
|
330
284
|
}
|
|
331
285
|
catch (err) {
|
|
332
|
-
if ((process.env.PHREN_DEBUG
|
|
286
|
+
if ((process.env.PHREN_DEBUG))
|
|
333
287
|
process.stderr.write(`[phren] doctor runtimeHealth: ${errorMessage(err)}\n`);
|
|
334
288
|
runtime = null;
|
|
335
289
|
}
|
|
@@ -375,6 +329,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
375
329
|
const detected = detectInstalledTools();
|
|
376
330
|
if (detected.has("copilot")) {
|
|
377
331
|
const copilotHooks = hookConfigPath("copilot", phrenPath);
|
|
332
|
+
const copilotWritable = nearestWritableTarget(copilotHooks);
|
|
378
333
|
checks.push({
|
|
379
334
|
name: "copilot-hooks",
|
|
380
335
|
ok: fs.existsSync(copilotHooks),
|
|
@@ -382,12 +337,13 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
382
337
|
});
|
|
383
338
|
checks.push({
|
|
384
339
|
name: "copilot-config-writable",
|
|
385
|
-
ok:
|
|
386
|
-
detail:
|
|
340
|
+
ok: copilotWritable,
|
|
341
|
+
detail: copilotWritable ? `writable: ${copilotHooks}` : `not writable: ${copilotHooks}`,
|
|
387
342
|
});
|
|
388
343
|
}
|
|
389
344
|
if (detected.has("cursor")) {
|
|
390
345
|
const cursorHooks = hookConfigPath("cursor", phrenPath);
|
|
346
|
+
const cursorWritable = nearestWritableTarget(cursorHooks);
|
|
391
347
|
checks.push({
|
|
392
348
|
name: "cursor-hooks",
|
|
393
349
|
ok: fs.existsSync(cursorHooks),
|
|
@@ -395,12 +351,13 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
395
351
|
});
|
|
396
352
|
checks.push({
|
|
397
353
|
name: "cursor-config-writable",
|
|
398
|
-
ok:
|
|
399
|
-
detail:
|
|
354
|
+
ok: cursorWritable,
|
|
355
|
+
detail: cursorWritable ? `writable: ${cursorHooks}` : `not writable: ${cursorHooks}`,
|
|
400
356
|
});
|
|
401
357
|
}
|
|
402
358
|
if (detected.has("codex")) {
|
|
403
359
|
const codexHooks = hookConfigPath("codex", phrenPath);
|
|
360
|
+
const codexWritable = nearestWritableTarget(codexHooks);
|
|
404
361
|
checks.push({
|
|
405
362
|
name: "codex-hooks",
|
|
406
363
|
ok: fs.existsSync(codexHooks),
|
|
@@ -408,8 +365,8 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
408
365
|
});
|
|
409
366
|
checks.push({
|
|
410
367
|
name: "codex-config-writable",
|
|
411
|
-
ok:
|
|
412
|
-
detail:
|
|
368
|
+
ok: codexWritable,
|
|
369
|
+
detail: codexWritable ? `writable: ${codexHooks}` : `not writable: ${codexHooks}`,
|
|
413
370
|
});
|
|
414
371
|
}
|
|
415
372
|
for (const tool of ["copilot", "cursor", "codex"]) {
|
|
@@ -446,7 +403,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
446
403
|
}
|
|
447
404
|
else {
|
|
448
405
|
// Read-only mode: just check if hook configs exist, don't write anything
|
|
449
|
-
const detectedTools =
|
|
406
|
+
const detectedTools = detected;
|
|
450
407
|
const hookChecks = [];
|
|
451
408
|
const missing = [];
|
|
452
409
|
for (const tool of detectedTools) {
|
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();
|