@phren/cli 0.0.38 → 0.0.40
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/hooks.js +4 -61
- package/mcp/dist/entrypoint.js +1 -0
- package/mcp/dist/governance/locks.js +27 -4
- package/mcp/dist/init/init-hooks-mode.js +1 -2
- package/mcp/dist/init/init-mcp-mode.js +1 -2
- package/mcp/dist/init/init-walkthrough.js +37 -1
- package/mcp/dist/init/init.js +7 -37
- package/mcp/dist/init/setup.js +9 -2
- package/mcp/dist/init/shared.js +8 -0
- package/package.json +1 -1
package/mcp/dist/cli/hooks.js
CHANGED
|
@@ -9,7 +9,7 @@ import { mergeConfig, } from "../shared/governance.js";
|
|
|
9
9
|
import { buildIndex, detectProject, } from "../shared/index.js";
|
|
10
10
|
import { isProjectHookEnabled } from "../project-config.js";
|
|
11
11
|
import { checkConsolidationNeeded, } from "../shared/content.js";
|
|
12
|
-
import { buildRobustFtsQuery, extractKeywordEntries, isFeatureEnabled, clampInt, errorMessage,
|
|
12
|
+
import { buildRobustFtsQuery, extractKeywordEntries, isFeatureEnabled, clampInt, errorMessage, } from "../utils.js";
|
|
13
13
|
import { getHooksEnabledPreference } from "../init/init.js";
|
|
14
14
|
import { logger } from "../logger.js";
|
|
15
15
|
import { isToolHookEnabled } from "../hooks.js";
|
|
@@ -37,65 +37,9 @@ import { getGitContext, trackSessionMetrics, } from "./hooks-session.js";
|
|
|
37
37
|
import { approximateTokens } from "../shared/retrieval.js";
|
|
38
38
|
import { resolveRuntimeProfile } from "../runtime-profile.js";
|
|
39
39
|
import { handleTaskPromptLifecycle } from "../task/lifecycle.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const values of Object.values(map)) {
|
|
44
|
-
if (values.includes(term))
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
function termAppearsInText(term, text) {
|
|
50
|
-
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
51
|
-
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
52
|
-
return pattern.test(text);
|
|
53
|
-
}
|
|
54
|
-
function autoLearnQuerySynonyms(phrenPath, project, keywordEntries, rows) {
|
|
55
|
-
if (!project)
|
|
56
|
-
return;
|
|
57
|
-
const synonymMap = loadSynonymMap(project, phrenPath);
|
|
58
|
-
const knownTerms = new Set([
|
|
59
|
-
...Object.keys(synonymMap),
|
|
60
|
-
...Object.values(synonymMap).flat(),
|
|
61
|
-
]);
|
|
62
|
-
const queryTerms = [...new Set(keywordEntries
|
|
63
|
-
.map((item) => item.trim().toLowerCase())
|
|
64
|
-
.filter((item) => item.length > 2 && !STOP_WORDS.has(item)))];
|
|
65
|
-
const unknownTerms = queryTerms.filter((term) => !synonymTermKnown(term, synonymMap));
|
|
66
|
-
if (unknownTerms.length === 0)
|
|
67
|
-
return;
|
|
68
|
-
const corpus = rows
|
|
69
|
-
.slice(0, 8)
|
|
70
|
-
.map((row) => row.content.slice(0, 6000))
|
|
71
|
-
.join("\n")
|
|
72
|
-
.toLowerCase();
|
|
73
|
-
if (!corpus.trim())
|
|
74
|
-
return;
|
|
75
|
-
const learned = [];
|
|
76
|
-
for (const unknown of unknownTerms.slice(0, 3)) {
|
|
77
|
-
const related = [...knownTerms]
|
|
78
|
-
.filter((candidate) => candidate.length > 2
|
|
79
|
-
&& candidate !== unknown
|
|
80
|
-
&& !STOP_WORDS.has(candidate)
|
|
81
|
-
&& !queryTerms.includes(candidate)
|
|
82
|
-
&& termAppearsInText(candidate, corpus))
|
|
83
|
-
.slice(0, 4);
|
|
84
|
-
if (related.length === 0)
|
|
85
|
-
continue;
|
|
86
|
-
try {
|
|
87
|
-
learnSynonym(phrenPath, project, unknown, related);
|
|
88
|
-
learned.push({ term: unknown, related });
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
debugLog(`hook-prompt synonym-learn failed for "${unknown}": ${errorMessage(err)}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (learned.length > 0) {
|
|
95
|
-
const details = learned.map((entry) => `${entry.term}->${entry.related.join(",")}`).join("; ");
|
|
96
|
-
debugLog(`hook-prompt learned synonyms project=${project} ${details}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
40
|
+
// Auto-learn from prompts was removed — it learned conversational noise ("bro", "idk", typos)
|
|
41
|
+
// as synonyms for high-frequency terms. Manual `phren config synonyms add` still works.
|
|
42
|
+
// Future: finding-based co-occurrence mining (phren maintain command, not live hook).
|
|
99
43
|
async function readStdin() {
|
|
100
44
|
return new Promise((resolve, reject) => {
|
|
101
45
|
const chunks = [];
|
|
@@ -195,7 +139,6 @@ export async function handleHookPrompt() {
|
|
|
195
139
|
stage.searchMs = Date.now() - tSearch0;
|
|
196
140
|
if (!rows || !rows.length)
|
|
197
141
|
process.exit(0);
|
|
198
|
-
autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
|
|
199
142
|
const tTrust0 = Date.now();
|
|
200
143
|
const policy = resolvedConfig.retentionPolicy;
|
|
201
144
|
const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
|
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -340,6 +340,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
340
340
|
applyStarterUpdate: initArgs.includes("--apply-starter-update"),
|
|
341
341
|
dryRun: initArgs.includes("--dry-run"),
|
|
342
342
|
yes: initArgs.includes("--yes") || initArgs.includes("-y"),
|
|
343
|
+
express: initArgs.includes("--express"),
|
|
343
344
|
_walkthroughCloneUrl: cloneUrl,
|
|
344
345
|
});
|
|
345
346
|
return finish();
|
|
@@ -24,10 +24,33 @@ function acquireFileLock(lockPath) {
|
|
|
24
24
|
try {
|
|
25
25
|
const stat = fs.statSync(lockPath);
|
|
26
26
|
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
27
|
-
// Lock
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
27
|
+
// Lock mtime exceeds stale threshold — but only delete if the
|
|
28
|
+
// owning process is dead. Legitimate long operations (e.g. index
|
|
29
|
+
// rebuilds) can hold locks beyond the threshold.
|
|
30
|
+
let ownerAlive = false;
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(lockPath, "utf-8");
|
|
33
|
+
const pid = Number.parseInt(content.split("\n")[0], 10);
|
|
34
|
+
if (pid > 0 && Number.isFinite(pid)) {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, 0); // signal 0: liveness check, no actual signal sent
|
|
37
|
+
ownerAlive = true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// process.kill throws if PID doesn't exist → owner is dead
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// pid <= 0 or NaN → treat as stale (unparseable / corrupt lock)
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Can't read lock file (deleted between stat and read) → retry
|
|
47
|
+
}
|
|
48
|
+
if (!ownerAlive) {
|
|
49
|
+
try {
|
|
50
|
+
fs.unlinkSync(lockPath);
|
|
51
|
+
}
|
|
52
|
+
catch { /* already gone */ }
|
|
53
|
+
}
|
|
31
54
|
continue;
|
|
32
55
|
}
|
|
33
56
|
}
|
|
@@ -6,8 +6,7 @@ import { errorMessage } from "../utils.js";
|
|
|
6
6
|
import { configureAllHooks } from "../hooks.js";
|
|
7
7
|
import { configureClaude } from "./config.js";
|
|
8
8
|
import { getMcpEnabledPreference, getHooksEnabledPreference, setHooksEnabledPreference, } from "./preferences.js";
|
|
9
|
-
import { DEFAULT_PHREN_PATH, log } from "./shared.js";
|
|
10
|
-
import { parseMcpMode } from "./init.js";
|
|
9
|
+
import { DEFAULT_PHREN_PATH, log, parseMcpMode } from "./shared.js";
|
|
11
10
|
export async function runHooksMode(modeArg) {
|
|
12
11
|
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
13
12
|
const manifest = readRootManifest(phrenPath);
|
|
@@ -5,8 +5,7 @@ import { debugLog, findPhrenPath, readRootManifest } from "../shared.js";
|
|
|
5
5
|
import { errorMessage } from "../utils.js";
|
|
6
6
|
import { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, } from "./config.js";
|
|
7
7
|
import { getMcpEnabledPreference, getHooksEnabledPreference, setMcpEnabledPreference, } from "./preferences.js";
|
|
8
|
-
import { DEFAULT_PHREN_PATH, log } from "./shared.js";
|
|
9
|
-
import { parseMcpMode } from "./init.js";
|
|
8
|
+
import { DEFAULT_PHREN_PATH, log, parseMcpMode } from "./shared.js";
|
|
10
9
|
export async function runMcpMode(modeArg) {
|
|
11
10
|
const phrenPath = findPhrenPath() || (process.env.PHREN_PATH) || DEFAULT_PHREN_PATH;
|
|
12
11
|
const manifest = readRootManifest(phrenPath);
|
|
@@ -114,7 +114,7 @@ function detectRepoRootForStorage(phrenPath) {
|
|
|
114
114
|
return detectProjectDir(process.cwd(), phrenPath);
|
|
115
115
|
}
|
|
116
116
|
// Interactive walkthrough for first-time init
|
|
117
|
-
export async function runWalkthrough(phrenPath) {
|
|
117
|
+
export async function runWalkthrough(phrenPath, options) {
|
|
118
118
|
const prompts = await createWalkthroughPrompts();
|
|
119
119
|
const style = await createWalkthroughStyle();
|
|
120
120
|
const divider = style.header("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
@@ -137,6 +137,42 @@ export async function runWalkthrough(phrenPath) {
|
|
|
137
137
|
printSection("Welcome");
|
|
138
138
|
log("Let's set up persistent memory for your AI agents.");
|
|
139
139
|
log("Every option can be changed later.\n");
|
|
140
|
+
// Express mode: skip the entire walkthrough with recommended defaults
|
|
141
|
+
const useExpress = options?.express === true
|
|
142
|
+
|| (options?.express !== false && await prompts.confirm("Use recommended settings? (global storage, MCP on, hooks on, auto tasks)", true));
|
|
143
|
+
if (useExpress) {
|
|
144
|
+
const expressResult = {
|
|
145
|
+
storageChoice: "global",
|
|
146
|
+
storagePath: path.resolve(homePath(".phren")),
|
|
147
|
+
machine: getMachineName(),
|
|
148
|
+
profile: "personal",
|
|
149
|
+
mcp: "on",
|
|
150
|
+
hooks: "on",
|
|
151
|
+
projectOwnershipDefault: "phren-managed",
|
|
152
|
+
findingsProactivity: "high",
|
|
153
|
+
taskProactivity: "high",
|
|
154
|
+
lowConfidenceThreshold: 0.7,
|
|
155
|
+
riskySections: ["Stale", "Conflicts"],
|
|
156
|
+
taskMode: "auto",
|
|
157
|
+
bootstrapCurrentProject: false,
|
|
158
|
+
ollamaEnabled: false,
|
|
159
|
+
autoCaptureEnabled: false,
|
|
160
|
+
semanticDedupEnabled: false,
|
|
161
|
+
semanticConflictEnabled: false,
|
|
162
|
+
findingSensitivity: "balanced",
|
|
163
|
+
domain: "software",
|
|
164
|
+
};
|
|
165
|
+
printSummary([
|
|
166
|
+
`Storage: global (${expressResult.storagePath})`,
|
|
167
|
+
`Machine: ${expressResult.machine}`,
|
|
168
|
+
"MCP: enabled",
|
|
169
|
+
"Hooks: enabled",
|
|
170
|
+
"Project ownership: phren-managed",
|
|
171
|
+
"Task mode: auto",
|
|
172
|
+
"Domain: software",
|
|
173
|
+
]);
|
|
174
|
+
return expressResult;
|
|
175
|
+
}
|
|
140
176
|
printSection("Storage Location");
|
|
141
177
|
log("Where should phren store data?");
|
|
142
178
|
const storageChoice = await prompts.select("Storage location", [
|
package/mcp/dist/init/init.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "fs";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
9
|
import { getMachineName, persistMachineName } from "../machine-identity.js";
|
|
10
|
-
import { atomicWriteText, debugLog, expandHomePath,
|
|
10
|
+
import { atomicWriteText, debugLog, expandHomePath, writeRootManifest, } from "../shared.js";
|
|
11
11
|
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
12
12
|
import { logger } from "../logger.js";
|
|
13
13
|
export { configureClaude, configureVSCode, configureCursorMcp, configureCopilotMcp, configureCodexMcp, logMcpTargetStatus, resetVSCodeProbeCache, patchJsonFile, } from "./config.js";
|
|
@@ -25,10 +25,11 @@ import { configureMcpTargets, configureHooksIfEnabled, applyOnboardingPreference
|
|
|
25
25
|
import { runWalkthrough, createWalkthroughPrompts, createWalkthroughStyle } from "./init-walkthrough.js";
|
|
26
26
|
import { getMcpEnabledPreference, getHooksEnabledPreference, writeInstallPreferences, readInstallPreferences, } from "./preferences.js";
|
|
27
27
|
import { ensureGovernanceFiles, repairPreexistingInstall, runPostInitVerify, applyStarterTemplateUpdates, listTemplates, applyTemplate, ensureProjectScaffold, ensureLocalGitRepo, bootstrapFromExisting, updateMachinesYaml, detectProjectDir, isProjectTracked, } from "./setup.js";
|
|
28
|
-
import { DEFAULT_PHREN_PATH, STARTER_DIR, VERSION, log
|
|
28
|
+
import { DEFAULT_PHREN_PATH, STARTER_DIR, VERSION, log } from "./shared.js";
|
|
29
29
|
import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, } from "../project-config.js";
|
|
30
30
|
import { getWorkflowPolicy } from "../shared/governance.js";
|
|
31
31
|
import { addProjectToProfile } from "../profile-store.js";
|
|
32
|
+
export { parseMcpMode } from "./shared.js";
|
|
32
33
|
function parseVersion(version) {
|
|
33
34
|
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/);
|
|
34
35
|
if (!match)
|
|
@@ -63,14 +64,6 @@ export function isVersionNewer(current, previous) {
|
|
|
63
64
|
return true;
|
|
64
65
|
return c.pre > p.pre;
|
|
65
66
|
}
|
|
66
|
-
export function parseMcpMode(raw) {
|
|
67
|
-
if (!raw)
|
|
68
|
-
return undefined;
|
|
69
|
-
const normalized = raw.trim().toLowerCase();
|
|
70
|
-
if (normalized === "on" || normalized === "off")
|
|
71
|
-
return normalized;
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
67
|
function normalizedBootstrapProjectName(projectPath) {
|
|
75
68
|
return path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
76
69
|
}
|
|
@@ -147,8 +140,10 @@ export async function runInit(opts = {}) {
|
|
|
147
140
|
}
|
|
148
141
|
let hasExistingInstall = hasInstallMarkers(phrenPath);
|
|
149
142
|
// Interactive walkthrough for first-time installs (skip with --yes or non-TTY)
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
// --express bypasses the TTY check since it skips all interactive prompts
|
|
144
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
145
|
+
if (!hasExistingInstall && !dryRun && !opts.yes && (isTTY || opts.express)) {
|
|
146
|
+
const answers = await runWalkthrough(phrenPath, { express: opts.express });
|
|
152
147
|
opts._walkthroughStorageChoice = answers.storageChoice;
|
|
153
148
|
opts._walkthroughStoragePath = answers.storagePath;
|
|
154
149
|
opts._walkthroughStorageRepoRoot = answers.storageRepoRoot;
|
|
@@ -350,20 +345,6 @@ export async function runInit(opts = {}) {
|
|
|
350
345
|
log(` Default project ownership: ${ownershipDefault}`);
|
|
351
346
|
log(` Task mode: ${getWorkflowPolicy(phrenPath).taskMode}`);
|
|
352
347
|
log(` Git repo: ${existingGitRepo.detail}`);
|
|
353
|
-
// Confirmation prompt before writing config
|
|
354
|
-
if (!opts.yes) {
|
|
355
|
-
const settingsPath = hookConfigPath("claude");
|
|
356
|
-
const modifications = [];
|
|
357
|
-
modifications.push(` ${settingsPath} (update MCP server + hooks)`);
|
|
358
|
-
log(`\nWill modify:`);
|
|
359
|
-
for (const mod of modifications)
|
|
360
|
-
log(mod);
|
|
361
|
-
const confirmed = await confirmPrompt("\nProceed?");
|
|
362
|
-
if (!confirmed) {
|
|
363
|
-
log("Aborted.");
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
348
|
// Always reconfigure MCP and hooks (picks up new features on upgrade)
|
|
368
349
|
configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled }, "Updated");
|
|
369
350
|
configureHooksIfEnabled(phrenPath, hooksEnabled, "Updated");
|
|
@@ -546,17 +527,6 @@ export async function runInit(opts = {}) {
|
|
|
546
527
|
if (repairedAssets.length > 0) {
|
|
547
528
|
log(` Recreated missing generated assets: ${repairedAssets.join(", ")}`);
|
|
548
529
|
}
|
|
549
|
-
// Confirmation prompt before writing agent config
|
|
550
|
-
if (!opts.yes) {
|
|
551
|
-
const settingsPath = hookConfigPath("claude");
|
|
552
|
-
log(`\nWill modify:`);
|
|
553
|
-
log(` ${settingsPath} (add MCP server + hooks)`);
|
|
554
|
-
const confirmed = await confirmPrompt("\nProceed?");
|
|
555
|
-
if (!confirmed) {
|
|
556
|
-
log("Aborted.");
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
530
|
// Configure MCP for all detected AI coding tools and hooks
|
|
561
531
|
configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled }, "Configured");
|
|
562
532
|
configureHooksIfEnabled(phrenPath, hooksEnabled, "Configured");
|
package/mcp/dist/init/setup.js
CHANGED
|
@@ -891,8 +891,15 @@ export function ensureLocalGitRepo(phrenPath) {
|
|
|
891
891
|
stdio: ["ignore", "pipe", "ignore"],
|
|
892
892
|
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
893
893
|
}).trim();
|
|
894
|
-
|
|
895
|
-
|
|
894
|
+
// Normalize both paths: resolve symlinks (macOS /var→/private/var) and
|
|
895
|
+
// on Windows resolve 8.3 short names (RUNNER~1→runneradmin) + case-insensitive
|
|
896
|
+
const realpath = process.platform === "win32" ? fs.realpathSync.native : fs.realpathSync;
|
|
897
|
+
let resolvedTopLevel = realpath(path.resolve(topLevel));
|
|
898
|
+
let resolvedPhrenPath = realpath(path.resolve(phrenPath));
|
|
899
|
+
if (process.platform === "win32") {
|
|
900
|
+
resolvedTopLevel = resolvedTopLevel.toLowerCase();
|
|
901
|
+
resolvedPhrenPath = resolvedPhrenPath.toLowerCase();
|
|
902
|
+
}
|
|
896
903
|
if (resolvedTopLevel === resolvedPhrenPath) {
|
|
897
904
|
// phrenPath IS the repo root — it has its own git repo
|
|
898
905
|
return { ok: true, initialized: false, detail: "existing git repo" };
|
package/mcp/dist/init/shared.js
CHANGED
|
@@ -67,6 +67,14 @@ export function nearestWritableTarget(filePath) {
|
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
export function parseMcpMode(raw) {
|
|
71
|
+
if (!raw)
|
|
72
|
+
return undefined;
|
|
73
|
+
const normalized = raw.trim().toLowerCase();
|
|
74
|
+
if (normalized === "on" || normalized === "off")
|
|
75
|
+
return normalized;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
70
78
|
export async function confirmPrompt(message) {
|
|
71
79
|
if (process.env.CI === "true" || !process.stdin.isTTY)
|
|
72
80
|
return true;
|