@phren/cli 0.0.41 → 0.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/cli/actions.js +1 -1
- package/mcp/dist/cli/cli.js +3 -1
- package/mcp/dist/cli/hooks-session.js +8 -8
- package/mcp/dist/cli/namespaces.js +110 -3
- package/mcp/dist/cli-hooks-session-handlers.js +8 -8
- package/mcp/dist/content/dedup.js +5 -2
- package/mcp/dist/entrypoint.js +4 -3
- package/mcp/dist/finding/context.js +3 -2
- package/mcp/dist/finding/journal.js +88 -1
- package/mcp/dist/init/config.js +1 -1
- package/mcp/dist/init/init-configure.js +1 -1
- package/mcp/dist/init/init-hooks-mode.js +1 -1
- package/mcp/dist/init/init-mcp-mode.js +2 -2
- package/mcp/dist/init/init-walkthrough.js +9 -9
- package/mcp/dist/init/init.js +8 -8
- package/mcp/dist/init/setup.js +6 -0
- package/mcp/dist/init-fresh.js +4 -4
- package/mcp/dist/init-hooks.js +1 -1
- package/mcp/dist/init-modes.js +3 -3
- package/mcp/dist/init-update.js +3 -3
- package/mcp/dist/init-walkthrough.js +9 -9
- package/mcp/dist/link/link.js +1 -1
- package/mcp/dist/phren-paths.js +2 -2
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/status.js +1 -1
- package/mcp/dist/tools/finding.js +37 -5
- package/mcp/dist/tools/search.js +81 -20
- package/mcp/dist/tools/session.js +10 -4
- package/mcp/dist/tools/tasks.js +78 -8
- package/mcp/dist/tools/types.js +22 -0
- package/package.json +1 -1
- package/skills/sync/SKILL.md +1 -1
- package/starter/README.md +6 -6
- package/starter/machines.yaml +1 -1
- package/starter/my-first-project/tasks.md +1 -1
- package/starter/templates/README.md +1 -1
package/mcp/dist/cli/actions.js
CHANGED
|
@@ -291,7 +291,7 @@ export async function handleUpdate(args) {
|
|
|
291
291
|
process.exitCode = 1;
|
|
292
292
|
}
|
|
293
293
|
else {
|
|
294
|
-
console.log("Run '
|
|
294
|
+
console.log("Run 'phren init' to refresh hooks and config.");
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
export async function handleReview(args) {
|
package/mcp/dist/cli/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { handleExtractMemories } from "./extract.js";
|
|
|
7
7
|
import { handleGovernMemories, handlePruneMemories, handleConsolidateMemories, handleMaintain, handleBackgroundMaintenance, } from "./govern.js";
|
|
8
8
|
import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowPolicy, } from "./config.js";
|
|
9
9
|
import { parseSearchArgs } from "./search.js";
|
|
10
|
-
import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handleStoreNamespace, handleTaskNamespace, } from "./namespaces.js";
|
|
10
|
+
import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handlePromoteNamespace, handleStoreNamespace, handleTaskNamespace, } from "./namespaces.js";
|
|
11
11
|
import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInjection, handleInspectIndex, } from "./ops.js";
|
|
12
12
|
import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./actions.js";
|
|
13
13
|
import { handleGraphNamespace } from "./graph.js";
|
|
@@ -109,6 +109,8 @@ export async function runCliCommand(command, args) {
|
|
|
109
109
|
return handleSessionContext();
|
|
110
110
|
case "store":
|
|
111
111
|
return handleStoreNamespace(args);
|
|
112
|
+
case "promote":
|
|
113
|
+
return handlePromoteNamespace(args);
|
|
112
114
|
default:
|
|
113
115
|
console.error(`Unknown command: ${command}\nRun 'phren --help' for available commands.`);
|
|
114
116
|
process.exit(1);
|
|
@@ -86,13 +86,13 @@ export function getUntrackedProjectNotice(phrenPath, cwd) {
|
|
|
86
86
|
return [
|
|
87
87
|
"<phren-notice>",
|
|
88
88
|
"This project directory is not tracked by phren yet.",
|
|
89
|
-
"Run `
|
|
90
|
-
`Suggested command: \`
|
|
89
|
+
"Run `phren add` to track it now.",
|
|
90
|
+
`Suggested command: \`phren add \"${projectDir}\"\``,
|
|
91
91
|
"Ask the user whether they want to add it to phren now.",
|
|
92
|
-
"If they say no, tell them they can always run `
|
|
92
|
+
"If they say no, tell them they can always run `phren add` later.",
|
|
93
93
|
"If they say yes, also ask whether phren should manage repo instruction files or leave their existing repo-owned CLAUDE/AGENTS files alone.",
|
|
94
|
-
`Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`
|
|
95
|
-
"After onboarding, run `
|
|
94
|
+
`Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`phren add\` from that directory.`,
|
|
95
|
+
"After onboarding, run `phren doctor` if hooks or MCP tools are not responding.",
|
|
96
96
|
"<phren-notice>",
|
|
97
97
|
"",
|
|
98
98
|
].join("\n");
|
|
@@ -127,8 +127,8 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
|
|
|
127
127
|
return [
|
|
128
128
|
"<phren-notice>",
|
|
129
129
|
"Phren onboarding: no tracked projects are active for this workspace yet.",
|
|
130
|
-
"Start in a project repo and run `
|
|
131
|
-
"Run `
|
|
130
|
+
"Start in a project repo and run `phren add` so SessionStart can inject project context.",
|
|
131
|
+
"Run `phren doctor` to verify hooks and MCP wiring after setup.",
|
|
132
132
|
"<phren-notice>",
|
|
133
133
|
"",
|
|
134
134
|
].join("\n");
|
|
@@ -141,7 +141,7 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
|
|
|
141
141
|
"<phren-notice>",
|
|
142
142
|
`Phren onboarding: project "${activeProject}" is tracked but memory is still empty.`,
|
|
143
143
|
"Capture one finding with `add_finding` and one task with `add_task` to seed future SessionStart context.",
|
|
144
|
-
"Run `
|
|
144
|
+
"Run `phren doctor` if setup seems incomplete.",
|
|
145
145
|
"<phren-notice>",
|
|
146
146
|
"",
|
|
147
147
|
].join("\n");
|
|
@@ -594,7 +594,7 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
594
594
|
}
|
|
595
595
|
if (subcommand === "add") {
|
|
596
596
|
console.error("`phren projects add` has been removed from the supported workflow.");
|
|
597
|
-
console.error("Use `cd ~/your-project &&
|
|
597
|
+
console.error("Use `cd ~/your-project && phren add` so enrollment stays path-based.");
|
|
598
598
|
process.exit(1);
|
|
599
599
|
}
|
|
600
600
|
if (subcommand === "remove") {
|
|
@@ -864,7 +864,7 @@ function handleProjectsList(profile) {
|
|
|
864
864
|
.filter((name) => name !== "global")
|
|
865
865
|
.sort();
|
|
866
866
|
if (!projects.length) {
|
|
867
|
-
console.log("No projects found. Run: cd ~/your-project &&
|
|
867
|
+
console.log("No projects found. Run: cd ~/your-project && phren add");
|
|
868
868
|
return;
|
|
869
869
|
}
|
|
870
870
|
console.log(`\nProjects in ${phrenPath}:\n`);
|
|
@@ -888,7 +888,7 @@ function handleProjectsList(profile) {
|
|
|
888
888
|
console.log(` ${name}${tagStr}`);
|
|
889
889
|
}
|
|
890
890
|
console.log(`\n${projects.length} project(s) total.`);
|
|
891
|
-
console.log("Add another project: cd ~/your-project &&
|
|
891
|
+
console.log("Add another project: cd ~/your-project && phren add");
|
|
892
892
|
}
|
|
893
893
|
async function handleProjectsRemove(name, profile) {
|
|
894
894
|
if (!isValidProjectName(name)) {
|
|
@@ -1509,6 +1509,7 @@ function printStoreUsage() {
|
|
|
1509
1509
|
console.log(" phren store add <name> --remote <url> Add a team store");
|
|
1510
1510
|
console.log(" phren store remove <name> Remove a store (local only)");
|
|
1511
1511
|
console.log(" phren store sync Pull all stores");
|
|
1512
|
+
console.log(" phren store activity [--limit N] Recent team findings");
|
|
1512
1513
|
}
|
|
1513
1514
|
export async function handleStoreNamespace(args) {
|
|
1514
1515
|
const subcommand = args[0];
|
|
@@ -1630,6 +1631,47 @@ export async function handleStoreNamespace(args) {
|
|
|
1630
1631
|
}
|
|
1631
1632
|
return;
|
|
1632
1633
|
}
|
|
1634
|
+
if (subcommand === "activity") {
|
|
1635
|
+
const stores = resolveAllStores(phrenPath);
|
|
1636
|
+
const teamStores = stores.filter((s) => s.role === "team");
|
|
1637
|
+
if (teamStores.length === 0) {
|
|
1638
|
+
console.log("No team stores registered. Add one with: phren store add <name> --remote <url>");
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const { readTeamJournalEntries } = await import("../finding/journal.js");
|
|
1642
|
+
const limit = Number(getOptionValue(args.slice(1), "--limit") ?? "20");
|
|
1643
|
+
const allEntries = [];
|
|
1644
|
+
for (const store of teamStores) {
|
|
1645
|
+
if (!fs.existsSync(store.path))
|
|
1646
|
+
continue;
|
|
1647
|
+
const projectDirs = getProjectDirs(store.path);
|
|
1648
|
+
for (const dir of projectDirs) {
|
|
1649
|
+
const projectName = path.basename(dir);
|
|
1650
|
+
const journalEntries = readTeamJournalEntries(store.path, projectName);
|
|
1651
|
+
for (const je of journalEntries) {
|
|
1652
|
+
for (const entry of je.entries) {
|
|
1653
|
+
allEntries.push({ store: store.name, project: projectName, date: je.date, actor: je.actor, entry });
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
allEntries.sort((a, b) => b.date.localeCompare(a.date));
|
|
1659
|
+
const capped = allEntries.slice(0, limit);
|
|
1660
|
+
if (capped.length === 0) {
|
|
1661
|
+
console.log("No team activity yet.");
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
console.log(`Team activity (${capped.length}/${allEntries.length}):\n`);
|
|
1665
|
+
let lastDate = "";
|
|
1666
|
+
for (const e of capped) {
|
|
1667
|
+
if (e.date !== lastDate) {
|
|
1668
|
+
console.log(`## ${e.date}`);
|
|
1669
|
+
lastDate = e.date;
|
|
1670
|
+
}
|
|
1671
|
+
console.log(` [${e.store}/${e.project}] ${e.actor}: ${e.entry}`);
|
|
1672
|
+
}
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1633
1675
|
if (subcommand === "sync") {
|
|
1634
1676
|
const stores = resolveAllStores(phrenPath);
|
|
1635
1677
|
let hasErrors = false;
|
|
@@ -1665,6 +1707,71 @@ export async function handleStoreNamespace(args) {
|
|
|
1665
1707
|
printStoreUsage();
|
|
1666
1708
|
process.exit(1);
|
|
1667
1709
|
}
|
|
1710
|
+
// ── Promote namespace ────────────────────────────────────────────────────────
|
|
1711
|
+
export async function handlePromoteNamespace(args) {
|
|
1712
|
+
if (!args[0] || args[0] === "--help" || args[0] === "-h") {
|
|
1713
|
+
console.log("Usage:");
|
|
1714
|
+
console.log(' phren promote <project> "finding text..." --to <store>');
|
|
1715
|
+
console.log(" Copies a finding from the primary store to a team store.");
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const phrenPath = getPhrenPath();
|
|
1719
|
+
const project = args[0];
|
|
1720
|
+
if (!isValidProjectName(project)) {
|
|
1721
|
+
console.error(`Invalid project name: "${project}"`);
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1724
|
+
const toStore = getOptionValue(args.slice(1), "--to");
|
|
1725
|
+
if (!toStore) {
|
|
1726
|
+
console.error("--to <store> is required. Specify the target team store.");
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
// Everything between project and --to is the finding text
|
|
1730
|
+
const toIdx = args.indexOf("--to");
|
|
1731
|
+
const findingText = args.slice(1, toIdx !== -1 ? toIdx : undefined).join(" ").trim();
|
|
1732
|
+
if (!findingText) {
|
|
1733
|
+
console.error("Finding text is required.");
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
const stores = resolveAllStores(phrenPath);
|
|
1737
|
+
const targetStore = stores.find((s) => s.name === toStore);
|
|
1738
|
+
if (!targetStore) {
|
|
1739
|
+
const available = stores.map((s) => s.name).join(", ");
|
|
1740
|
+
console.error(`Store "${toStore}" not found. Available: ${available}`);
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
if (targetStore.role === "readonly") {
|
|
1744
|
+
console.error(`Store "${toStore}" is read-only.`);
|
|
1745
|
+
process.exit(1);
|
|
1746
|
+
}
|
|
1747
|
+
if (targetStore.role === "primary") {
|
|
1748
|
+
console.error(`Cannot promote to primary store — finding is already there.`);
|
|
1749
|
+
process.exit(1);
|
|
1750
|
+
}
|
|
1751
|
+
// Find the matching finding in the primary store
|
|
1752
|
+
const { readFindings } = await import("../data/access.js");
|
|
1753
|
+
const findingsResult = readFindings(phrenPath, project);
|
|
1754
|
+
if (!findingsResult.ok) {
|
|
1755
|
+
console.error(`Could not read findings for project "${project}".`);
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1758
|
+
const match = findingsResult.data.find((item) => item.text.includes(findingText) || findingText.includes(item.text));
|
|
1759
|
+
if (!match) {
|
|
1760
|
+
console.error(`No finding matching "${findingText.slice(0, 80)}..." found in ${project}.`);
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
// Write to target store
|
|
1764
|
+
const targetProjectDir = path.join(targetStore.path, project);
|
|
1765
|
+
fs.mkdirSync(targetProjectDir, { recursive: true });
|
|
1766
|
+
const { addFindingToFile } = await import("../shared/content.js");
|
|
1767
|
+
const result = addFindingToFile(targetStore.path, project, match.text);
|
|
1768
|
+
if (!result.ok) {
|
|
1769
|
+
console.error(`Failed to add finding to ${toStore}: ${result.error}`);
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
}
|
|
1772
|
+
console.log(`Promoted to ${toStore}/${project}:`);
|
|
1773
|
+
console.log(` "${match.text.slice(0, 120)}${match.text.length > 120 ? "..." : ""}"`);
|
|
1774
|
+
}
|
|
1668
1775
|
function countStoreProjects(store) {
|
|
1669
1776
|
if (!fs.existsSync(store.path))
|
|
1670
1777
|
return 0;
|
|
@@ -60,13 +60,13 @@ export function getUntrackedProjectNotice(phrenPath, cwd) {
|
|
|
60
60
|
return [
|
|
61
61
|
"<phren-notice>",
|
|
62
62
|
"This project directory is not tracked by phren yet.",
|
|
63
|
-
"Run `
|
|
64
|
-
`Suggested command: \`
|
|
63
|
+
"Run `phren add` to track it now.",
|
|
64
|
+
`Suggested command: \`phren add "${projectDir}"\``,
|
|
65
65
|
"Ask the user whether they want to add it to phren now.",
|
|
66
|
-
"If they say no, tell them they can always run `
|
|
66
|
+
"If they say no, tell them they can always run `phren add` later.",
|
|
67
67
|
"If they say yes, also ask whether phren should manage repo instruction files or leave their existing repo-owned CLAUDE/AGENTS files alone.",
|
|
68
|
-
`Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`
|
|
69
|
-
"After onboarding, run `
|
|
68
|
+
`Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`phren add\` from that directory.`,
|
|
69
|
+
"After onboarding, run `phren doctor` if hooks or MCP tools are not responding.",
|
|
70
70
|
"<phren-notice>",
|
|
71
71
|
"",
|
|
72
72
|
].join("\n");
|
|
@@ -83,8 +83,8 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
|
|
|
83
83
|
return [
|
|
84
84
|
"<phren-notice>",
|
|
85
85
|
"Phren onboarding: no tracked projects are active for this workspace yet.",
|
|
86
|
-
"Start in a project repo and run `
|
|
87
|
-
"Run `
|
|
86
|
+
"Start in a project repo and run `phren add` so SessionStart can inject project context.",
|
|
87
|
+
"Run `phren doctor` to verify hooks and MCP wiring after setup.",
|
|
88
88
|
"<phren-notice>",
|
|
89
89
|
"",
|
|
90
90
|
].join("\n");
|
|
@@ -97,7 +97,7 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
|
|
|
97
97
|
"<phren-notice>",
|
|
98
98
|
`Phren onboarding: project "${activeProject}" is tracked but memory is still empty.`,
|
|
99
99
|
"Capture one finding with `add_finding` and one task with `add_task` to seed future SessionStart context.",
|
|
100
|
-
"Run `
|
|
100
|
+
"Run `phren doctor` if setup seems incomplete.",
|
|
101
101
|
"<phren-notice>",
|
|
102
102
|
"",
|
|
103
103
|
].join("\n");
|
|
@@ -338,12 +338,15 @@ export function isDuplicateFinding(existingContent, newLearning, threshold = 0.6
|
|
|
338
338
|
return true;
|
|
339
339
|
}
|
|
340
340
|
// Second pass: Jaccard similarity (strip metadata before comparing)
|
|
341
|
+
// Threshold lowered from 0.55 to 0.40 to catch agent paraphrases —
|
|
342
|
+
// swarm agents often report the same insight with different wording,
|
|
343
|
+
// and 0.55 let too many through.
|
|
341
344
|
const newTokens = jaccardTokenize(stripMetadata(newLearning));
|
|
342
345
|
const existingTokens = jaccardTokenize(stripMetadata(bullet));
|
|
343
346
|
if (newTokens.size < 3 || existingTokens.size < 3)
|
|
344
347
|
continue; // too few tokens for reliable Jaccard
|
|
345
348
|
const jaccard = jaccardSimilarity(newTokens, existingTokens);
|
|
346
|
-
if (jaccard > 0.
|
|
349
|
+
if (jaccard > 0.40) {
|
|
347
350
|
debugLog(`duplicate-detection: Jaccard ${Math.round(jaccard * 100)}% with existing: "${bullet.slice(0, 80)}"`);
|
|
348
351
|
return true;
|
|
349
352
|
}
|
|
@@ -489,7 +492,7 @@ export async function checkSemanticDedup(phrenPath, project, newLearning, signal
|
|
|
489
492
|
if (tokA.size < 3 || tokB.size < 3)
|
|
490
493
|
continue;
|
|
491
494
|
const jaccard = jaccardSimilarity(tokA, tokB);
|
|
492
|
-
if (jaccard >= 0.
|
|
495
|
+
if (jaccard >= 0.40)
|
|
493
496
|
continue; // already caught by sync isDuplicateFinding
|
|
494
497
|
if (jaccard >= 0.3) {
|
|
495
498
|
const isDup = await semanticDedup(a, b, phrenPath, signal);
|
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -175,6 +175,7 @@ const CLI_COMMANDS = [
|
|
|
175
175
|
"consolidation-status",
|
|
176
176
|
"session-context",
|
|
177
177
|
"store",
|
|
178
|
+
"promote",
|
|
178
179
|
];
|
|
179
180
|
async function flushTopLevelOutput() {
|
|
180
181
|
await Promise.all([
|
|
@@ -264,7 +265,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
264
265
|
const phrenPath = defaultPhrenPath();
|
|
265
266
|
const profile = (process.env.PHREN_PROFILE) || undefined;
|
|
266
267
|
if (!fs.existsSync(phrenPath) || !fs.existsSync(path.join(phrenPath, ".config"))) {
|
|
267
|
-
console.log("phren is not set up yet. Run:
|
|
268
|
+
console.log("phren is not set up yet. Run: phren init");
|
|
268
269
|
return finish(1);
|
|
269
270
|
}
|
|
270
271
|
const ownership = ownershipArg
|
|
@@ -380,7 +381,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
380
381
|
const note = getVerifyOutcomeNote(phrenPath, result.checks);
|
|
381
382
|
if (note)
|
|
382
383
|
console.log(`\nNote: ${note}`);
|
|
383
|
-
console.log(`\nRun \`
|
|
384
|
+
console.log(`\nRun \`phren init\` to fix setup issues.`);
|
|
384
385
|
}
|
|
385
386
|
return finish(result.ok ? 0 : 1);
|
|
386
387
|
}
|
|
@@ -407,7 +408,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
407
408
|
}
|
|
408
409
|
}
|
|
409
410
|
if (argvCommand === "link") {
|
|
410
|
-
console.error("`phren link` has been removed. Use `
|
|
411
|
+
console.error("`phren link` has been removed. Use `phren init` instead.");
|
|
411
412
|
return finish(1);
|
|
412
413
|
}
|
|
413
414
|
if (argvCommand === "--health") {
|
|
@@ -167,6 +167,7 @@ export function resolveFindingSessionId(phrenPath, project, explicitSessionId) {
|
|
|
167
167
|
.sort((a, b) => sessionSortValue(b) - sessionSortValue(a));
|
|
168
168
|
if (matchingProject.length > 0)
|
|
169
169
|
return matchingProject[0].sessionId;
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// Do not attribute findings to an unrelated active session from another project.
|
|
171
|
+
// Missing session provenance is safer than cross-project contamination.
|
|
172
|
+
return undefined;
|
|
172
173
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
|
-
import { runtimeDir, phrenOk, phrenErr, PhrenError } from "../shared.js";
|
|
4
|
+
import { runtimeDir, phrenOk, phrenErr, PhrenError, atomicWriteText } from "../shared.js";
|
|
5
5
|
import { withFileLock } from "../shared/governance.js";
|
|
6
6
|
import { addFindingToFile } from "../shared/content.js";
|
|
7
7
|
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
@@ -120,3 +120,90 @@ export function compactFindingJournals(phrenPath, project) {
|
|
|
120
120
|
}
|
|
121
121
|
return result;
|
|
122
122
|
}
|
|
123
|
+
// ── Team store journal (append-only markdown, committed to git) ──────────────
|
|
124
|
+
const TEAM_JOURNAL_DIR = "journal";
|
|
125
|
+
/**
|
|
126
|
+
* Append a finding to a team store's journal.
|
|
127
|
+
* Each actor gets one file per day — no merge conflicts possible.
|
|
128
|
+
* These are markdown files committed to git (not runtime JSONL).
|
|
129
|
+
*/
|
|
130
|
+
export function appendTeamJournal(storePath, project, finding, actor) {
|
|
131
|
+
const resolvedActor = actor || process.env.PHREN_ACTOR || process.env.USER || "unknown";
|
|
132
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
133
|
+
const journalDir = path.join(storePath, project, TEAM_JOURNAL_DIR);
|
|
134
|
+
const journalFile = `${date}-${resolvedActor}.md`;
|
|
135
|
+
const journalPath = path.join(journalDir, journalFile);
|
|
136
|
+
try {
|
|
137
|
+
fs.mkdirSync(journalDir, { recursive: true });
|
|
138
|
+
const entry = `- ${finding}\n`;
|
|
139
|
+
if (fs.existsSync(journalPath)) {
|
|
140
|
+
fs.appendFileSync(journalPath, entry);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
fs.writeFileSync(journalPath, `## ${date} (${resolvedActor})\n\n${entry}`);
|
|
144
|
+
}
|
|
145
|
+
return phrenOk(journalFile);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return phrenErr(`Team journal append failed: ${errorMessage(err)}`, PhrenError.PERMISSION_DENIED);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Read all team journal entries for a project, newest first.
|
|
153
|
+
*/
|
|
154
|
+
export function readTeamJournalEntries(storePath, project) {
|
|
155
|
+
const journalDir = path.join(storePath, project, TEAM_JOURNAL_DIR);
|
|
156
|
+
if (!fs.existsSync(journalDir))
|
|
157
|
+
return [];
|
|
158
|
+
return fs.readdirSync(journalDir)
|
|
159
|
+
.filter((f) => f.endsWith(".md"))
|
|
160
|
+
.sort()
|
|
161
|
+
.reverse()
|
|
162
|
+
.map((file) => {
|
|
163
|
+
const match = file.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
|
|
164
|
+
const date = match?.[1] ?? "unknown";
|
|
165
|
+
const actor = match?.[2] ?? "unknown";
|
|
166
|
+
const content = fs.readFileSync(path.join(journalDir, file), "utf8");
|
|
167
|
+
const entries = content.split("\n")
|
|
168
|
+
.filter((line) => line.startsWith("- "))
|
|
169
|
+
.map((line) => line.slice(2).trim());
|
|
170
|
+
return { file, date, actor, entries };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Materialize FINDINGS.md from team journal entries.
|
|
175
|
+
* Groups by date, includes actor attribution.
|
|
176
|
+
*/
|
|
177
|
+
export function materializeTeamFindings(storePath, project) {
|
|
178
|
+
const journalEntries = readTeamJournalEntries(storePath, project);
|
|
179
|
+
if (journalEntries.length === 0) {
|
|
180
|
+
return phrenErr("No journal entries found", PhrenError.FILE_NOT_FOUND);
|
|
181
|
+
}
|
|
182
|
+
// Group by date, chronological order
|
|
183
|
+
const byDate = new Map();
|
|
184
|
+
for (const entry of [...journalEntries].reverse()) {
|
|
185
|
+
if (!byDate.has(entry.date))
|
|
186
|
+
byDate.set(entry.date, []);
|
|
187
|
+
byDate.get(entry.date).push({ actor: entry.actor, entries: entry.entries });
|
|
188
|
+
}
|
|
189
|
+
const lines = [`# ${project} findings\n`];
|
|
190
|
+
let count = 0;
|
|
191
|
+
for (const [date, actors] of byDate) {
|
|
192
|
+
lines.push(`## ${date}`);
|
|
193
|
+
for (const { actor, entries } of actors) {
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
lines.push(`- ${entry} <!-- author:${actor} -->`);
|
|
196
|
+
count++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
const findingsPath = path.join(storePath, project, "FINDINGS.md");
|
|
202
|
+
try {
|
|
203
|
+
atomicWriteText(findingsPath, lines.join("\n"));
|
|
204
|
+
return phrenOk({ entryCount: count });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
return phrenErr(`Materialize failed: ${errorMessage(err)}`, PhrenError.PERMISSION_DENIED);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/mcp/dist/init/config.js
CHANGED
|
@@ -207,7 +207,7 @@ export function configureClaude(phrenPath, opts = {}) {
|
|
|
207
207
|
eventHooks.push({ matcher: "", hooks: [hookBody] });
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
|
-
const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK",
|
|
210
|
+
const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", true);
|
|
211
211
|
if (hooksEnabled) {
|
|
212
212
|
upsertPhrenHook("UserPromptSubmit", {
|
|
213
213
|
type: "command",
|
|
@@ -89,7 +89,7 @@ export function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
else {
|
|
92
|
-
log(` Hooks are disabled by preference (run:
|
|
92
|
+
log(` Hooks are disabled by preference (run: phren hooks-mode on)`);
|
|
93
93
|
}
|
|
94
94
|
// Install phren CLI wrapper at ~/.local/bin/phren so the bare command works
|
|
95
95
|
const wrapperInstalled = installPhrenCliWrapper(phrenPath);
|
|
@@ -14,7 +14,7 @@ export async function runHooksMode(modeArg) {
|
|
|
14
14
|
if (!normalizedArg || normalizedArg === "status") {
|
|
15
15
|
const current = getHooksEnabledPreference(phrenPath);
|
|
16
16
|
log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
|
|
17
|
-
log(`Change mode:
|
|
17
|
+
log(`Change mode: phren hooks-mode on|off`);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const mode = parseMcpMode(normalizedArg);
|
|
@@ -15,8 +15,8 @@ export async function runMcpMode(modeArg) {
|
|
|
15
15
|
const hooks = getHooksEnabledPreference(phrenPath);
|
|
16
16
|
log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
|
|
17
17
|
log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
|
|
18
|
-
log(`Change mode:
|
|
19
|
-
log(`Hooks toggle:
|
|
18
|
+
log(`Change mode: phren mcp-mode on|off`);
|
|
19
|
+
log(`Hooks toggle: phren hooks-mode on|off`);
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const mode = parseMcpMode(normalizedArg);
|
|
@@ -280,7 +280,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
280
280
|
log(" phren-managed: Phren may mirror CLAUDE.md / AGENTS.md into the repo");
|
|
281
281
|
log(" detached: Phren keeps its own docs but does not write into the repo");
|
|
282
282
|
log(" repo-managed: keep the repo's existing CLAUDE/AGENTS files as canonical");
|
|
283
|
-
log(" Change later:
|
|
283
|
+
log(" Change later: phren config project-ownership <mode>");
|
|
284
284
|
const projectOwnershipDefault = await prompts.select("Default project ownership", [
|
|
285
285
|
{ value: "detached", name: "detached (default)" },
|
|
286
286
|
{ value: "phren-managed", name: "phren-managed" },
|
|
@@ -291,7 +291,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
291
291
|
log("directly: search memory, manage tasks, save findings, etc.");
|
|
292
292
|
log(" Recommended for: Claude Code, Cursor, Copilot CLI, Codex");
|
|
293
293
|
log(" Alternative: hooks-only mode (read-only context injection, any agent)");
|
|
294
|
-
log(" Change later:
|
|
294
|
+
log(" Change later: phren mcp-mode on|off");
|
|
295
295
|
const mcp = (await prompts.confirm("Enable MCP?", true)) ? "on" : "off";
|
|
296
296
|
printSection("Hooks");
|
|
297
297
|
log("Hooks run shell commands at session start, prompt submit, and session end.");
|
|
@@ -299,7 +299,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
299
299
|
log(" - UserPromptSubmit: searches phren and injects relevant context");
|
|
300
300
|
log(" - Stop: commits and pushes any new findings after each response");
|
|
301
301
|
log(" What they touch: ~/.claude/settings.json (hooks section only)");
|
|
302
|
-
log(" Change later:
|
|
302
|
+
log(" Change later: phren hooks-mode on|off");
|
|
303
303
|
const hooks = (await prompts.confirm("Enable hooks?", true)) ? "on" : "off";
|
|
304
304
|
printSection("Semantic Search (Optional)");
|
|
305
305
|
log("Phren can use a local embedding model for semantic (fuzzy) search via Ollama.");
|
|
@@ -347,7 +347,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
347
347
|
let findingsProactivity = "high";
|
|
348
348
|
if (autoCaptureEnabled) {
|
|
349
349
|
log(" Findings capture level controls how eager phren is to save lessons automatically.");
|
|
350
|
-
log(" Change later:
|
|
350
|
+
log(" Change later: phren config proactivity.findings <high|medium|low>");
|
|
351
351
|
findingsProactivity = await prompts.select("Findings capture level", [
|
|
352
352
|
{ value: "high", name: "high (recommended)" },
|
|
353
353
|
{ value: "medium", name: "medium" },
|
|
@@ -363,7 +363,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
363
363
|
log(" suggest: proposes tasks but waits for approval before writing");
|
|
364
364
|
log(" manual: tasks are fully manual — you add them yourself");
|
|
365
365
|
log(" off: never touch tasks automatically");
|
|
366
|
-
log(" Change later:
|
|
366
|
+
log(" Change later: phren config workflow set --taskMode=<mode>");
|
|
367
367
|
const taskMode = await prompts.select("Task mode", [
|
|
368
368
|
{ value: "auto", name: "auto (recommended)" },
|
|
369
369
|
{ value: "suggest", name: "suggest" },
|
|
@@ -376,7 +376,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
376
376
|
log(" high (recommended): captures tasks as they come up naturally");
|
|
377
377
|
log(" medium: only when you explicitly mention a task");
|
|
378
378
|
log(" low: minimal auto-capture");
|
|
379
|
-
log(" Change later:
|
|
379
|
+
log(" Change later: phren config proactivity.tasks <high|medium|low>");
|
|
380
380
|
taskProactivity = await prompts.select("Task proactivity", [
|
|
381
381
|
{ value: "high", name: "high (recommended)" },
|
|
382
382
|
{ value: "medium", name: "medium" },
|
|
@@ -387,7 +387,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
387
387
|
log("Choose how strict review gates should be for risky or low-confidence writes.");
|
|
388
388
|
log(" lowConfidenceThreshold: confidence cutoff used to mark writes as risky");
|
|
389
389
|
log(" riskySections: sections always treated as risky");
|
|
390
|
-
log(" Change later:
|
|
390
|
+
log(" Change later: phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
|
|
391
391
|
const thresholdAnswer = await prompts.input("Low-confidence threshold [0.0-1.0]", "0.7");
|
|
392
392
|
const lowConfidenceThreshold = parseLowConfidenceThreshold(thresholdAnswer, 0.7);
|
|
393
393
|
const riskySectionsAnswer = await prompts.input("Risky sections [Review,Stale,Conflicts]", "Stale,Conflicts");
|
|
@@ -437,7 +437,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
437
437
|
log(" conservative — decisions and pitfalls only");
|
|
438
438
|
log(" balanced — non-obvious patterns, decisions, pitfalls, bugs (recommended)");
|
|
439
439
|
log(" aggressive — everything worth remembering, err on the side of capturing");
|
|
440
|
-
log(" Change later:
|
|
440
|
+
log(" Change later: phren config finding-sensitivity <level>");
|
|
441
441
|
const findingSensitivity = await prompts.select("Finding sensitivity", [
|
|
442
442
|
{ value: "balanced", name: "balanced (recommended)" },
|
|
443
443
|
{ value: "conservative", name: "conservative" },
|
|
@@ -466,7 +466,7 @@ export async function runWalkthrough(phrenPath, options) {
|
|
|
466
466
|
bootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
467
467
|
if (!bootstrapCurrentProject) {
|
|
468
468
|
bootstrapCurrentProject = false;
|
|
469
|
-
log(style.warning(` Skipped. Later: cd ${detectedProject} &&
|
|
469
|
+
log(style.warning(` Skipped. Later: cd ${detectedProject} && phren add`));
|
|
470
470
|
}
|
|
471
471
|
else {
|
|
472
472
|
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
package/mcp/dist/init/init.js
CHANGED
|
@@ -260,7 +260,7 @@ export async function runInit(opts = {}) {
|
|
|
260
260
|
shouldBootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
|
|
261
261
|
if (!shouldBootstrapCurrentProject) {
|
|
262
262
|
shouldBootstrapCurrentProject = false;
|
|
263
|
-
log(style.warning(` Skipped. Later: cd ${pendingBootstrap.path} &&
|
|
263
|
+
log(style.warning(` Skipped. Later: cd ${pendingBootstrap.path} && phren add`));
|
|
264
264
|
}
|
|
265
265
|
else {
|
|
266
266
|
bootstrapOwnership = await prompts.select("Ownership for detected project", [
|
|
@@ -352,7 +352,7 @@ export async function runInit(opts = {}) {
|
|
|
352
352
|
const previousVersion = prefs.installedVersion;
|
|
353
353
|
if (isVersionNewer(VERSION, previousVersion)) {
|
|
354
354
|
log(`\n Starter template update available: v${previousVersion} -> v${VERSION}`);
|
|
355
|
-
log(` Run \`
|
|
355
|
+
log(` Run \`phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
|
|
356
356
|
}
|
|
357
357
|
if (opts.applyStarterUpdate) {
|
|
358
358
|
const updated = applyStarterTemplateUpdates(phrenPath);
|
|
@@ -395,8 +395,8 @@ export async function runInit(opts = {}) {
|
|
|
395
395
|
log(`\n\x1b[95m◆\x1b[0m phren updated successfully`);
|
|
396
396
|
log(`\nNext steps:`);
|
|
397
397
|
log(` 1. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
398
|
-
log(` 2. Run \`
|
|
399
|
-
log(` 3. Change defaults anytime: \`
|
|
398
|
+
log(` 2. Run \`phren doctor\` to verify everything is wired correctly`);
|
|
399
|
+
log(` 3. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
|
|
400
400
|
log(` 4. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
401
401
|
log(` 5. After working across projects, run phren-consolidate to find cross-project patterns`);
|
|
402
402
|
log(``);
|
|
@@ -581,8 +581,8 @@ export async function runInit(opts = {}) {
|
|
|
581
581
|
log(`\nNext steps:`);
|
|
582
582
|
let step = 1;
|
|
583
583
|
log(` ${step++}. Start a new Claude session in your project directory — phren injects context automatically`);
|
|
584
|
-
log(` ${step++}. Run \`
|
|
585
|
-
log(` ${step++}. Change defaults anytime: \`
|
|
584
|
+
log(` ${step++}. Run \`phren doctor\` to verify everything is wired correctly`);
|
|
585
|
+
log(` ${step++}. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
|
|
586
586
|
const gh = opts._walkthroughGithub;
|
|
587
587
|
if (gh) {
|
|
588
588
|
const remote = gh.username
|
|
@@ -607,9 +607,9 @@ export async function runInit(opts = {}) {
|
|
|
607
607
|
log(` git remote add origin git@github.com:YOUR_USERNAME/my-phren.git`);
|
|
608
608
|
log(` git push -u origin main`);
|
|
609
609
|
}
|
|
610
|
-
log(` ${step++}. Add more projects: cd ~/your-project &&
|
|
610
|
+
log(` ${step++}. Add more projects: cd ~/your-project && phren add`);
|
|
611
611
|
if (!mcpEnabled) {
|
|
612
|
-
log(` ${step++}. Turn MCP on:
|
|
612
|
+
log(` ${step++}. Turn MCP on: phren mcp-mode on`);
|
|
613
613
|
}
|
|
614
614
|
log(` ${step++}. After your first week, run phren-discover to surface gaps in your project knowledge`);
|
|
615
615
|
log(` ${step++}. After working across projects, run phren-consolidate to find cross-project patterns`);
|
package/mcp/dist/init/setup.js
CHANGED
|
@@ -1089,9 +1089,15 @@ export function updateMachinesYaml(phrenPath, machine, profile) {
|
|
|
1089
1089
|
*/
|
|
1090
1090
|
export function detectProjectDir(dir, phrenPath) {
|
|
1091
1091
|
const home = os.homedir();
|
|
1092
|
+
const tmpRoot = path.resolve(os.tmpdir());
|
|
1092
1093
|
const resolvedPhrenPath = path.resolve(phrenPath);
|
|
1093
1094
|
let current = path.resolve(dir);
|
|
1094
1095
|
while (true) {
|
|
1096
|
+
// Never treat the shared OS temp root itself as a project. Tools may drop
|
|
1097
|
+
// global instruction files there, which would otherwise hijack detection
|
|
1098
|
+
// for arbitrary temp subdirectories.
|
|
1099
|
+
if (current === tmpRoot)
|
|
1100
|
+
return null;
|
|
1095
1101
|
if (current === home || current === resolvedPhrenPath)
|
|
1096
1102
|
return null;
|
|
1097
1103
|
if (current.startsWith(resolvedPhrenPath + path.sep))
|