@phren/cli 0.0.32 → 0.0.34
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 +3 -0
- package/mcp/dist/cli/config.js +3 -3
- package/mcp/dist/cli/govern.js +18 -8
- package/mcp/dist/cli/hooks-context.js +1 -1
- package/mcp/dist/cli/hooks-session.js +18 -62
- package/mcp/dist/cli/namespaces.js +1 -1
- package/mcp/dist/cli/search.js +5 -5
- package/mcp/dist/cli-hooks-prompt.js +7 -3
- package/mcp/dist/cli-hooks-session-handlers.js +3 -15
- package/mcp/dist/cli-hooks-stop.js +10 -48
- package/mcp/dist/content/archive.js +8 -20
- package/mcp/dist/content/learning.js +29 -8
- package/mcp/dist/data/access.js +13 -4
- package/mcp/dist/finding/lifecycle.js +9 -3
- package/mcp/dist/governance/audit.js +13 -5
- package/mcp/dist/governance/policy.js +13 -0
- package/mcp/dist/governance/rbac.js +1 -1
- package/mcp/dist/governance/scores.js +2 -1
- package/mcp/dist/hooks.js +52 -6
- package/mcp/dist/index.js +1 -1
- package/mcp/dist/init/init.js +66 -45
- package/mcp/dist/init/shared.js +1 -1
- package/mcp/dist/init-bootstrap.js +0 -47
- package/mcp/dist/init-fresh.js +13 -18
- package/mcp/dist/init-uninstall.js +22 -0
- package/mcp/dist/init-walkthrough.js +19 -24
- package/mcp/dist/link/doctor.js +9 -0
- package/mcp/dist/package-metadata.js +1 -1
- package/mcp/dist/phren-art.js +4 -120
- package/mcp/dist/proactivity.js +1 -1
- package/mcp/dist/project-topics.js +16 -46
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/runtime-profile.js +1 -1
- package/mcp/dist/shared/data-utils.js +25 -0
- package/mcp/dist/shared/fragment-graph.js +4 -18
- package/mcp/dist/shared/index.js +14 -10
- package/mcp/dist/shared/ollama.js +23 -5
- package/mcp/dist/shared/process.js +24 -0
- package/mcp/dist/shared/retrieval.js +7 -4
- package/mcp/dist/shared/search-fallback.js +1 -0
- package/mcp/dist/shared.js +2 -1
- package/mcp/dist/shell/render.js +1 -1
- package/mcp/dist/skill/registry.js +1 -1
- package/mcp/dist/skill/state.js +0 -3
- package/mcp/dist/task/github.js +1 -0
- package/mcp/dist/task/lifecycle.js +1 -6
- package/mcp/dist/tools/config.js +415 -400
- package/mcp/dist/tools/finding.js +390 -373
- package/mcp/dist/tools/ops.js +372 -365
- package/mcp/dist/tools/search.js +495 -487
- package/mcp/dist/tools/session.js +3 -2
- package/mcp/dist/tools/skills.js +9 -0
- package/mcp/dist/ui/page.js +1 -1
- package/mcp/dist/ui/server.js +645 -1040
- package/mcp/dist/utils.js +12 -8
- package/package.json +1 -1
- package/mcp/dist/init-dryrun.js +0 -55
- package/mcp/dist/init-migrate.js +0 -51
- package/mcp/dist/init-walkthrough-merge.js +0 -90
|
@@ -428,6 +428,28 @@ export async function runUninstall(opts = {}) {
|
|
|
428
428
|
catch (err) {
|
|
429
429
|
debugLog(`uninstall: cleanup failed for ${contextFile}: ${errorMessage(err)}`);
|
|
430
430
|
}
|
|
431
|
+
// Remove global CLAUDE.md symlink (created by linkGlobal -> ~/.claude/CLAUDE.md)
|
|
432
|
+
const globalClaudeLink = homePath(".claude", "CLAUDE.md");
|
|
433
|
+
try {
|
|
434
|
+
if (fs.lstatSync(globalClaudeLink).isSymbolicLink()) {
|
|
435
|
+
fs.unlinkSync(globalClaudeLink);
|
|
436
|
+
log(` Removed global CLAUDE.md symlink (${globalClaudeLink})`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Does not exist or not a symlink — nothing to do
|
|
441
|
+
}
|
|
442
|
+
// Remove copilot-instructions.md symlink (created by linkGlobal -> ~/.github/copilot-instructions.md)
|
|
443
|
+
const copilotInstrLink = homePath(".github", "copilot-instructions.md");
|
|
444
|
+
try {
|
|
445
|
+
if (fs.lstatSync(copilotInstrLink).isSymbolicLink()) {
|
|
446
|
+
fs.unlinkSync(copilotInstrLink);
|
|
447
|
+
log(` Removed copilot-instructions.md symlink (${copilotInstrLink})`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Does not exist or not a symlink — nothing to do
|
|
452
|
+
}
|
|
431
453
|
// Sweep agent skill directories for symlinks pointing into the phren store
|
|
432
454
|
if (phrenPath) {
|
|
433
455
|
try {
|
|
@@ -300,31 +300,26 @@ export async function runWalkthrough(phrenPath) {
|
|
|
300
300
|
log(" Change later: set PHREN_OLLAMA_URL=off to disable");
|
|
301
301
|
let ollamaEnabled = false;
|
|
302
302
|
try {
|
|
303
|
-
const {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery? (will pull nomic-embed-text)", false);
|
|
315
|
-
if (ollamaEnabled) {
|
|
316
|
-
log(" Run after init: ollama pull nomic-embed-text");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
303
|
+
const { checkOllamaStatus } = await import("./shared/ollama.js");
|
|
304
|
+
const status = await checkOllamaStatus();
|
|
305
|
+
if (status === "ready") {
|
|
306
|
+
log(" Ollama detected with nomic-embed-text ready.");
|
|
307
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery?", false);
|
|
308
|
+
}
|
|
309
|
+
else if (status === "no_model") {
|
|
310
|
+
log(" Ollama detected, but nomic-embed-text is not pulled yet.");
|
|
311
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search for fuzzy/paraphrase recovery? (will pull nomic-embed-text)", false);
|
|
312
|
+
if (ollamaEnabled) {
|
|
313
|
+
log(" Run after init: ollama pull nomic-embed-text");
|
|
319
314
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
315
|
+
}
|
|
316
|
+
else if (status === "not_running") {
|
|
317
|
+
log(" Ollama not detected. Install it to enable semantic search:");
|
|
318
|
+
log(" https://ollama.com → then: ollama pull nomic-embed-text");
|
|
319
|
+
ollamaEnabled = await prompts.confirm("Enable semantic search (Ollama not installed yet)?", false);
|
|
320
|
+
if (ollamaEnabled) {
|
|
321
|
+
log(style.success(" Semantic search enabled — will activate once Ollama is running."));
|
|
322
|
+
log(" To disable: set PHREN_OLLAMA_URL=off in your shell profile");
|
|
328
323
|
}
|
|
329
324
|
}
|
|
330
325
|
}
|
package/mcp/dist/link/doctor.js
CHANGED
|
@@ -402,6 +402,15 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
|
|
|
402
402
|
: `${tool} wrapper missing or not first in PATH`,
|
|
403
403
|
});
|
|
404
404
|
}
|
|
405
|
+
// Check phren CLI wrapper
|
|
406
|
+
const phrenCliActive = isWrapperActive("phren");
|
|
407
|
+
checks.push({
|
|
408
|
+
name: "wrapper:phren-cli",
|
|
409
|
+
ok: phrenCliActive,
|
|
410
|
+
detail: phrenCliActive
|
|
411
|
+
? "phren CLI wrapper active via ~/.local/bin/phren"
|
|
412
|
+
: "phren CLI wrapper missing — run init to install, or npm i -g @phren/cli",
|
|
413
|
+
});
|
|
405
414
|
if (fix) {
|
|
406
415
|
const repaired = repairPreexistingInstall(phrenPath);
|
|
407
416
|
const details = [];
|
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
export const ROOT = path.join(__dirname, "..", "..");
|
|
6
|
-
|
|
6
|
+
const PACKAGE_JSON_PATH = path.join(ROOT, "package.json");
|
|
7
7
|
function readPackageJson() {
|
|
8
8
|
return JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf8"));
|
|
9
9
|
}
|
package/mcp/dist/phren-art.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phren character ASCII/Unicode art
|
|
2
|
+
* Phren character ASCII/Unicode art for CLI presence.
|
|
3
3
|
*
|
|
4
4
|
* Based on the pixel art: purple 8-bit brain with diamond eyes,
|
|
5
5
|
* smile, little legs, and cyan sparkle.
|
|
6
|
-
*
|
|
7
|
-
* Animation system provides lifelike movement through composable effects:
|
|
8
|
-
* bob, blink, sparkle, and lean — all driven by setTimeout with randomized
|
|
9
|
-
* intervals for organic timing.
|
|
10
6
|
*/
|
|
11
|
-
const ESC = "\x1b[";
|
|
12
|
-
const RESET = `${ESC}0m`;
|
|
13
|
-
const PURPLE = `${ESC}35m`; // magenta — body
|
|
14
|
-
const BRIGHT_PURPLE = `${ESC}95m`; // bright magenta — highlights
|
|
15
|
-
const CYAN = `${ESC}96m`; // bright cyan — sparkle
|
|
16
|
-
const DARK_PURPLE = `${ESC}38;5;57m`; // deep purple — shadow/outline
|
|
17
|
-
// ── Art constants (24px wide, truecolor half-blocks) ─────────────────────────
|
|
18
7
|
/**
|
|
19
8
|
* Phren truecolor art (24px wide, generated from phren-transparent.png).
|
|
20
9
|
* Uses half-block ▀ with RGB foreground+background for pixel-faithful rendering.
|
|
@@ -35,32 +24,18 @@ export const PHREN_ART = [
|
|
|
35
24
|
" ",
|
|
36
25
|
];
|
|
37
26
|
// ── Sparkle row: the cyan pixels at row 2 ────────────────────────────────────
|
|
38
|
-
// Sparkle uses ▄ half-blocks with cyan truecolor. For animation we cycle through
|
|
39
|
-
// decorative unicode characters at different brightness levels.
|
|
40
27
|
const SPARKLE_ROW = 2;
|
|
41
|
-
const SPARKLE_CHARS = ["\u2726", "\u2727", "\u2736", " "];
|
|
42
|
-
// ── Eye detection: dark navy pixels in row 6
|
|
43
|
-
// Row 6 has two eye pixels at segments 1 and 5 (visual positions 7 and 11).
|
|
44
|
-
// When blinking, we replace their dark fg color with the surrounding body purple.
|
|
28
|
+
const SPARKLE_CHARS = ["\u2726", "\u2727", "\u2736", " "];
|
|
29
|
+
// ── Eye detection: dark navy pixels in row 6 ─────────────────────────────────
|
|
45
30
|
const EYE_ROW = 6;
|
|
46
|
-
// Dark-pixel fg threshold
|
|
47
31
|
const EYE_R_MAX = 30;
|
|
48
32
|
const EYE_G_MAX = 45;
|
|
49
33
|
const EYE_B_MAX = 120;
|
|
50
|
-
// Body-purple color to use when "closing" eyes (average of surrounding pixels)
|
|
51
34
|
const BLINK_COLOR = "146;130;250";
|
|
52
|
-
/**
|
|
53
|
-
* Flip a single art line horizontally. Reverses the order of colored pixel
|
|
54
|
-
* segments and swaps leading/trailing whitespace so the character faces right.
|
|
55
|
-
* Half-block characters (▀ ▄) are horizontally symmetric so no char swap needed.
|
|
56
|
-
*/
|
|
57
35
|
function flipLine(line) {
|
|
58
36
|
const stripped = line.replace(/\x1b\[[^m]*m/g, "");
|
|
59
37
|
const leadSpaces = stripped.match(/^( *)/)[1].length;
|
|
60
38
|
const trailSpaces = stripped.match(/( *)$/)[1].length;
|
|
61
|
-
// Parse pixel segments: each is one or two ANSI color codes followed by a block char.
|
|
62
|
-
// We strip any leading reset (\x1b[0m) from captured codes — it's an artifact from
|
|
63
|
-
// the original per-pixel reset pattern and will be re-added during reassembly.
|
|
64
39
|
const pixels = [];
|
|
65
40
|
const pixelRegex = /((?:\x1b\[[^m]*m)+)([\u2580\u2584])/g;
|
|
66
41
|
let match;
|
|
@@ -70,7 +45,7 @@ function flipLine(line) {
|
|
|
70
45
|
pixels.push({ codes, char: match[2] });
|
|
71
46
|
}
|
|
72
47
|
if (pixels.length === 0)
|
|
73
|
-
return line;
|
|
48
|
+
return line;
|
|
74
49
|
const reversed = [...pixels].reverse();
|
|
75
50
|
const newLead = " ".repeat(trailSpaces);
|
|
76
51
|
const newTrail = " ".repeat(leadSpaces);
|
|
@@ -81,26 +56,14 @@ function flipLine(line) {
|
|
|
81
56
|
result += newTrail;
|
|
82
57
|
return result;
|
|
83
58
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Generate horizontally flipped art (facing right).
|
|
86
|
-
*/
|
|
87
59
|
function generateFlippedArt(art) {
|
|
88
60
|
return art.map(flipLine);
|
|
89
61
|
}
|
|
90
|
-
/** Pre-computed right-facing art */
|
|
91
62
|
export const PHREN_ART_RIGHT = generateFlippedArt(PHREN_ART);
|
|
92
|
-
/** Random integer in [min, max] inclusive */
|
|
93
63
|
function randInt(min, max) {
|
|
94
64
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
95
65
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Replace eye pixels on a single line with the blink color.
|
|
98
|
-
* Eye pixels are identified by their dark navy fg color (R<30, G<45, B<120).
|
|
99
|
-
* We replace the fg color code while preserving the bg code and character.
|
|
100
|
-
*/
|
|
101
66
|
function applyBlinkToLine(line) {
|
|
102
|
-
// Match ANSI color sequences: each pixel is \e[38;2;R;G;Bm (optionally with \e[48;2;...m) then a block char
|
|
103
|
-
// We scan for fg codes that match the eye threshold and replace them with the blink color
|
|
104
67
|
return line.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (full, rStr, gStr, bStr) => {
|
|
105
68
|
const r = Number(rStr);
|
|
106
69
|
const g = Number(gStr);
|
|
@@ -111,44 +74,24 @@ function applyBlinkToLine(line) {
|
|
|
111
74
|
return full;
|
|
112
75
|
});
|
|
113
76
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Replace the sparkle pixels on the sparkle row with the current sparkle character.
|
|
116
|
-
* The sparkle row has two cyan ▄ half-blocks. During sparkle animation we replace
|
|
117
|
-
* them with decorative Unicode characters from SPARKLE_CHARS.
|
|
118
|
-
*/
|
|
119
77
|
function applySparkleToLine(line, frame, active) {
|
|
120
78
|
if (!active)
|
|
121
79
|
return line;
|
|
122
80
|
const sparkleChar = SPARKLE_CHARS[frame % SPARKLE_CHARS.length];
|
|
123
81
|
if (sparkleChar === " ") {
|
|
124
|
-
// Dim: replace the cyan-colored segments with spaces (they disappear)
|
|
125
82
|
return line.replace(/\x1b\[38;2;\d+;2\d\d;2\d\dm[\u2580\u2584]\x1b\[0m/g, " ");
|
|
126
83
|
}
|
|
127
|
-
// Replace the half-block characters with the sparkle unicode char, keeping the cyan color
|
|
128
84
|
return line.replace(/(\x1b\[38;2;\d+;2\d\d;2\d\dm)[\u2580\u2584](\x1b\[0m)/g, `$1${sparkleChar}$2`);
|
|
129
85
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Apply lean (horizontal shift) to a line.
|
|
132
|
-
* Positive offset = shift right (prepend spaces, trim from end).
|
|
133
|
-
* Negative offset = shift left (trim from start, append spaces).
|
|
134
|
-
*/
|
|
135
86
|
function applyLean(line, offset) {
|
|
136
87
|
if (offset === 0)
|
|
137
88
|
return line;
|
|
138
89
|
if (offset > 0) {
|
|
139
|
-
// Shift right: prepend spaces
|
|
140
90
|
return " ".repeat(offset) + line;
|
|
141
91
|
}
|
|
142
|
-
// Shift left: remove leading spaces (up to |offset|)
|
|
143
92
|
const trimCount = Math.min(-offset, line.match(/^( *)/)[1].length);
|
|
144
93
|
return line.slice(trimCount);
|
|
145
94
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Create an animated phren character controller.
|
|
148
|
-
*
|
|
149
|
-
* @param options.facing - 'left' (default, original) or 'right' (flipped)
|
|
150
|
-
* @param options.size - art width; unused but reserved for future scaling (default 24)
|
|
151
|
-
*/
|
|
152
95
|
export function createPhrenAnimator(options) {
|
|
153
96
|
const facing = options?.facing ?? "left";
|
|
154
97
|
const baseArt = facing === "right" ? PHREN_ART_RIGHT : PHREN_ART;
|
|
@@ -164,22 +107,18 @@ export function createPhrenAnimator(options) {
|
|
|
164
107
|
const t = setTimeout(fn, ms);
|
|
165
108
|
timers.push(t);
|
|
166
109
|
}
|
|
167
|
-
// ── Bob animation: toggles bobUp every ~500ms ──────────────────────────
|
|
168
110
|
function scheduleBob() {
|
|
169
111
|
scheduleTimer(() => {
|
|
170
112
|
state.bobUp = !state.bobUp;
|
|
171
113
|
scheduleBob();
|
|
172
114
|
}, 500);
|
|
173
115
|
}
|
|
174
|
-
// ── Blink animation: eyes close for 150ms, random 2-8s intervals ──────
|
|
175
116
|
function scheduleBlink() {
|
|
176
117
|
const interval = randInt(2000, 8000);
|
|
177
118
|
scheduleTimer(() => {
|
|
178
|
-
// Perform blink
|
|
179
119
|
state.isBlinking = true;
|
|
180
120
|
scheduleTimer(() => {
|
|
181
121
|
state.isBlinking = false;
|
|
182
|
-
// 30% chance of double-blink
|
|
183
122
|
if (Math.random() < 0.3) {
|
|
184
123
|
scheduleTimer(() => {
|
|
185
124
|
state.isBlinking = true;
|
|
@@ -195,9 +134,7 @@ export function createPhrenAnimator(options) {
|
|
|
195
134
|
}, 150);
|
|
196
135
|
}, interval);
|
|
197
136
|
}
|
|
198
|
-
// ── Sparkle animation: fast cycle during bursts, long pauses between ───
|
|
199
137
|
function scheduleSparkle() {
|
|
200
|
-
// Wait 1-5 seconds before next sparkle burst
|
|
201
138
|
const pause = randInt(1000, 5000);
|
|
202
139
|
scheduleTimer(() => {
|
|
203
140
|
state.sparkleActive = true;
|
|
@@ -207,7 +144,6 @@ export function createPhrenAnimator(options) {
|
|
|
207
144
|
}
|
|
208
145
|
function sparkleStep(step) {
|
|
209
146
|
if (step >= SPARKLE_CHARS.length) {
|
|
210
|
-
// Burst complete
|
|
211
147
|
state.sparkleActive = false;
|
|
212
148
|
scheduleSparkle();
|
|
213
149
|
return;
|
|
@@ -217,7 +153,6 @@ export function createPhrenAnimator(options) {
|
|
|
217
153
|
sparkleStep(step + 1);
|
|
218
154
|
}, 200);
|
|
219
155
|
}
|
|
220
|
-
// ── Lean animation: shift 1 col left or right every 4-10s, hold 1-2s ──
|
|
221
156
|
function scheduleLean() {
|
|
222
157
|
const interval = randInt(4000, 10000);
|
|
223
158
|
scheduleTimer(() => {
|
|
@@ -234,19 +169,15 @@ export function createPhrenAnimator(options) {
|
|
|
234
169
|
getFrame() {
|
|
235
170
|
let lines = baseArt.map((line, i) => {
|
|
236
171
|
let result = line;
|
|
237
|
-
// Apply blink to the eye row
|
|
238
172
|
if (state.isBlinking && i === EYE_ROW) {
|
|
239
173
|
result = applyBlinkToLine(result);
|
|
240
174
|
}
|
|
241
|
-
// Apply sparkle to sparkle row
|
|
242
175
|
if (i === SPARKLE_ROW) {
|
|
243
176
|
result = applySparkleToLine(result, state.sparkleFrame, state.sparkleActive);
|
|
244
177
|
}
|
|
245
|
-
// Apply lean
|
|
246
178
|
result = applyLean(result, state.leanOffset);
|
|
247
179
|
return result;
|
|
248
180
|
});
|
|
249
|
-
// Apply bob: when bobUp, prepend a blank line (shift everything down visually)
|
|
250
181
|
if (state.bobUp) {
|
|
251
182
|
lines = ["", ...lines.slice(0, -1)];
|
|
252
183
|
}
|
|
@@ -266,56 +197,9 @@ export function createPhrenAnimator(options) {
|
|
|
266
197
|
},
|
|
267
198
|
};
|
|
268
199
|
}
|
|
269
|
-
// ── Startup frames (pre-baked, no timers) ────────────────────────────────────
|
|
270
|
-
/**
|
|
271
|
-
* Returns 4 pre-baked animation frames for shell startup display.
|
|
272
|
-
* No timers needed — the caller cycles through them manually.
|
|
273
|
-
*
|
|
274
|
-
* Frames: [neutral, bob-up, neutral, bob-down(sparkle)]
|
|
275
|
-
*
|
|
276
|
-
* @param facing - 'left' (default) or 'right'
|
|
277
|
-
*/
|
|
278
|
-
export function getPhrenStartupFrames(facing) {
|
|
279
|
-
const art = facing === "right" ? PHREN_ART_RIGHT : PHREN_ART;
|
|
280
|
-
// Frame 0: neutral
|
|
281
|
-
const frame0 = [...art];
|
|
282
|
-
// Frame 1: bob up (prepend blank line, drop last line)
|
|
283
|
-
const frame1 = ["", ...art.slice(0, -1)];
|
|
284
|
-
// Frame 2: neutral (same as frame 0)
|
|
285
|
-
const frame2 = [...art];
|
|
286
|
-
// Frame 3: bob down with sparkle burst — shift down by removing first line, append blank
|
|
287
|
-
const frame3WithSparkle = art.map((line, i) => {
|
|
288
|
-
if (i === SPARKLE_ROW) {
|
|
289
|
-
return applySparkleToLine(line, 0, true); // ✦ sparkle
|
|
290
|
-
}
|
|
291
|
-
return line;
|
|
292
|
-
});
|
|
293
|
-
const frame3 = [...frame3WithSparkle.slice(1), ""];
|
|
294
|
-
return [frame0, frame1, frame2, frame3];
|
|
295
|
-
}
|
|
296
|
-
// ── Legacy exports (unchanged) ───────────────────────────────────────────────
|
|
297
|
-
/** Single-line compact phren for inline use */
|
|
298
|
-
export const PHREN_INLINE = `${PURPLE}◆${RESET}`;
|
|
299
|
-
/** Phren spinner frames for search/sync operations — cycles through in purple */
|
|
300
|
-
export const PHREN_SPINNER_FRAMES = [
|
|
301
|
-
`${BRIGHT_PURPLE}◆${RESET}`,
|
|
302
|
-
`${PURPLE}◇${RESET}`,
|
|
303
|
-
`${CYAN}✦${RESET}`,
|
|
304
|
-
`${PURPLE}✧${RESET}`,
|
|
305
|
-
`${BRIGHT_PURPLE}◆${RESET}`,
|
|
306
|
-
`${DARK_PURPLE}◇${RESET}`,
|
|
307
|
-
];
|
|
308
|
-
/** Default spinner interval in ms */
|
|
309
|
-
export const PHREN_SPINNER_INTERVAL_MS = 120;
|
|
310
200
|
/**
|
|
311
201
|
* Return the phren art as a single string, optionally indented.
|
|
312
202
|
*/
|
|
313
203
|
export function renderPhrenArt(indent = "") {
|
|
314
204
|
return PHREN_ART.map(line => indent + line).join("\n");
|
|
315
205
|
}
|
|
316
|
-
/**
|
|
317
|
-
* Get a spinner frame by index (wraps around automatically).
|
|
318
|
-
*/
|
|
319
|
-
export function spinnerFrame(tick) {
|
|
320
|
-
return PHREN_SPINNER_FRAMES[tick % PHREN_SPINNER_FRAMES.length];
|
|
321
|
-
}
|
package/mcp/dist/proactivity.js
CHANGED
|
@@ -21,7 +21,7 @@ function resolveProactivityPhrenPath(explicitPhrenPath) {
|
|
|
21
21
|
return explicitPhrenPath ?? findPhrenPath();
|
|
22
22
|
}
|
|
23
23
|
/** Read per-user preferences from ~/.phren/.users/<actor>/preferences.json. Actor from PHREN_ACTOR env var. */
|
|
24
|
-
|
|
24
|
+
function readUserPreferences(explicitPhrenPath) {
|
|
25
25
|
const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
|
|
26
26
|
if (!phrenPath)
|
|
27
27
|
return {};
|
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import { debugLog } from "./shared.js";
|
|
5
5
|
import { withFileLock } from "./shared/governance.js";
|
|
6
6
|
import { STOP_WORDS, errorMessage, extractKeywords, isValidProjectName, safeProjectPath } from "./utils.js";
|
|
7
|
+
import { walkDirectory } from "./shared/data-utils.js";
|
|
7
8
|
const TOPIC_CONFIG_FILENAME = "topic-config.json";
|
|
8
9
|
const AUTO_TOPIC_MARKER_RE = /^<!--\s*phren:auto-topic(?:\s+slug=([a-z0-9_-]+))?\s*-->$/;
|
|
9
10
|
const ARCHIVED_SECTION_RE = /^## Archived (\d{4}-\d{2}-\d{2})$/;
|
|
@@ -303,7 +304,7 @@ function normalizeKeyword(raw) {
|
|
|
303
304
|
.replace(/\s+/g, " ")
|
|
304
305
|
.trim();
|
|
305
306
|
}
|
|
306
|
-
|
|
307
|
+
function normalizeTopicSlug(raw) {
|
|
307
308
|
return raw
|
|
308
309
|
.trim()
|
|
309
310
|
.toLowerCase()
|
|
@@ -384,10 +385,7 @@ function topicConfigPath(phrenPath, project) {
|
|
|
384
385
|
function projectDirPath(phrenPath, project) {
|
|
385
386
|
return safeProjectPath(phrenPath, project);
|
|
386
387
|
}
|
|
387
|
-
|
|
388
|
-
return safeProjectPath(phrenPath, project, "reference", "topics");
|
|
389
|
-
}
|
|
390
|
-
export function topicReferenceRelativePath(slug) {
|
|
388
|
+
function topicReferenceRelativePath(slug) {
|
|
391
389
|
return path.posix.join("reference", "topics", `${slug}.md`);
|
|
392
390
|
}
|
|
393
391
|
export function topicReferencePath(phrenPath, project, slug) {
|
|
@@ -575,7 +573,7 @@ export function readProjectTopics(phrenPath, project) {
|
|
|
575
573
|
}
|
|
576
574
|
return { source: "custom", topics: normalized, domain: typeof parsed.domain === "string" ? parsed.domain : undefined };
|
|
577
575
|
}
|
|
578
|
-
|
|
576
|
+
function readPinnedTopics(phrenPath, project) {
|
|
579
577
|
const configPath = topicConfigPath(phrenPath, project);
|
|
580
578
|
if (!configPath || !fs.existsSync(configPath))
|
|
581
579
|
return [];
|
|
@@ -675,27 +673,14 @@ function normalizeBullet(line) {
|
|
|
675
673
|
}
|
|
676
674
|
function collectArchivedBulletsRecursively(dirPath) {
|
|
677
675
|
const bullets = new Set();
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const current = stack.pop();
|
|
683
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
684
|
-
const fullPath = path.join(current, entry.name);
|
|
685
|
-
if (entry.isDirectory()) {
|
|
686
|
-
stack.push(fullPath);
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
676
|
+
for (const filePath of walkDirectory(dirPath)) {
|
|
677
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
678
|
+
for (const line of content.split("\n")) {
|
|
679
|
+
if (!line.startsWith("- "))
|
|
690
680
|
continue;
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
continue;
|
|
695
|
-
const normalized = normalizeBullet(line);
|
|
696
|
-
if (normalized)
|
|
697
|
-
bullets.add(normalized);
|
|
698
|
-
}
|
|
681
|
+
const normalized = normalizeBullet(line);
|
|
682
|
+
if (normalized)
|
|
683
|
+
bullets.add(normalized);
|
|
699
684
|
}
|
|
700
685
|
}
|
|
701
686
|
return bullets;
|
|
@@ -797,23 +782,7 @@ function parseLegacyTopicEntries(content, project) {
|
|
|
797
782
|
return { slug: fallbackSlug, entries };
|
|
798
783
|
}
|
|
799
784
|
function readReferenceMarkdownFiles(referenceDir) {
|
|
800
|
-
|
|
801
|
-
return [];
|
|
802
|
-
const files = [];
|
|
803
|
-
const stack = [referenceDir];
|
|
804
|
-
while (stack.length > 0) {
|
|
805
|
-
const current = stack.pop();
|
|
806
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
807
|
-
const fullPath = path.join(current, entry.name);
|
|
808
|
-
if (entry.isDirectory()) {
|
|
809
|
-
stack.push(fullPath);
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
if (entry.isFile() && entry.name.endsWith(".md"))
|
|
813
|
-
files.push(fullPath);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
return files.sort();
|
|
785
|
+
return walkDirectory(referenceDir).sort();
|
|
817
786
|
}
|
|
818
787
|
function relativeToProject(projectDir, filePath) {
|
|
819
788
|
return path.relative(projectDir, filePath).replace(/\\/g, "/");
|
|
@@ -835,7 +804,7 @@ function safeStatIso(filePath) {
|
|
|
835
804
|
return "";
|
|
836
805
|
}
|
|
837
806
|
}
|
|
838
|
-
|
|
807
|
+
function listProjectTopicDocs(phrenPath, project, topics) {
|
|
839
808
|
const projectDir = projectDirPath(phrenPath, project);
|
|
840
809
|
if (!projectDir)
|
|
841
810
|
return [];
|
|
@@ -885,7 +854,7 @@ export function listProjectReferenceDocs(phrenPath, project, topics) {
|
|
|
885
854
|
}
|
|
886
855
|
return { topicDocs, otherDocs };
|
|
887
856
|
}
|
|
888
|
-
|
|
857
|
+
function listLegacyTopicDocs(phrenPath, project) {
|
|
889
858
|
const projectDir = projectDirPath(phrenPath, project);
|
|
890
859
|
const referenceDir = safeProjectPath(phrenPath, project, "reference");
|
|
891
860
|
if (!projectDir || !referenceDir || !fs.existsSync(referenceDir))
|
|
@@ -1042,6 +1011,7 @@ export function suggestTopics(phrenPath, project, topics) {
|
|
|
1042
1011
|
}
|
|
1043
1012
|
return deduped;
|
|
1044
1013
|
}
|
|
1014
|
+
/** @internal Exported for tests. */
|
|
1045
1015
|
export const suggestProjectTopics = suggestTopics;
|
|
1046
1016
|
export function getProjectTopicsResponse(phrenPath, project) {
|
|
1047
1017
|
const { source, topics } = readProjectTopics(phrenPath, project);
|
|
@@ -1054,7 +1024,7 @@ export function getProjectTopicsResponse(phrenPath, project) {
|
|
|
1054
1024
|
topicDocs: listProjectTopicDocs(phrenPath, project, topics),
|
|
1055
1025
|
};
|
|
1056
1026
|
}
|
|
1057
|
-
|
|
1027
|
+
function resolveReferenceContentPath(phrenPath, project, file) {
|
|
1058
1028
|
if (!isValidProjectName(project) || !file || file.includes("\0"))
|
|
1059
1029
|
return null;
|
|
1060
1030
|
if (!file.endsWith(".md"))
|
|
@@ -35,7 +35,7 @@ function normalizeWindowsPathToWsl(input) {
|
|
|
35
35
|
function uniqStrings(values) {
|
|
36
36
|
return Array.from(new Set(values.filter((value) => Boolean(value && value.trim()))));
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
function pickExistingFile(candidates) {
|
|
39
39
|
for (const candidate of candidates) {
|
|
40
40
|
if (fs.existsSync(candidate))
|
|
41
41
|
return candidate;
|
|
@@ -15,6 +15,31 @@ export function withSafeLock(filePath, fn) {
|
|
|
15
15
|
throw err;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Recursively walk a directory and return paths of files matching an optional filter.
|
|
20
|
+
* Defaults to `.md` files only. Uses an iterative stack to avoid recursion limits.
|
|
21
|
+
*/
|
|
22
|
+
export function walkDirectory(root, filter) {
|
|
23
|
+
const accept = filter ?? ((name) => name.endsWith(".md"));
|
|
24
|
+
const results = [];
|
|
25
|
+
if (!fs.existsSync(root))
|
|
26
|
+
return results;
|
|
27
|
+
const stack = [root];
|
|
28
|
+
while (stack.length > 0) {
|
|
29
|
+
const current = stack.pop();
|
|
30
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
31
|
+
const fullPath = path.join(current, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
stack.push(fullPath);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.isFile() && accept(entry.name)) {
|
|
37
|
+
results.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
18
43
|
export function ensureProject(phrenPath, project) {
|
|
19
44
|
if (!isValidProjectName(project))
|
|
20
45
|
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
@@ -4,8 +4,10 @@ import { runtimeFile } from "../shared.js";
|
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
import { UNIVERSAL_TECH_TERMS_RE } from "../phren-core.js";
|
|
6
6
|
import { errorMessage } from "../utils.js";
|
|
7
|
+
/** @internal Exported for tests. */
|
|
7
8
|
export function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
8
|
-
/** Escape SQL LIKE wildcard characters so user input is treated literally.
|
|
9
|
+
/** Escape SQL LIKE wildcard characters so user input is treated literally.
|
|
10
|
+
* @internal Exported for tests. */
|
|
9
11
|
export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
|
|
10
12
|
/**
|
|
11
13
|
* Log fragment resolution misses to .runtime/fragment-misses.jsonl.
|
|
@@ -41,8 +43,6 @@ export function logFragmentMiss(phrenPath, name, context, project) {
|
|
|
41
43
|
// Best-effort logging; don't let miss tracking break the caller.
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
/** @deprecated Use logFragmentMiss instead */
|
|
45
|
-
export const logEntityMiss = logFragmentMiss;
|
|
46
46
|
// Use the shared universal starter set. Framework/tool specifics are learned
|
|
47
47
|
// dynamically per project via extractDynamicFragments() in content-dedup.ts.
|
|
48
48
|
const PROSE_FRAGMENT_PATTERN = UNIVERSAL_TECH_TERMS_RE;
|
|
@@ -110,8 +110,6 @@ export function extractFragmentNames(content) {
|
|
|
110
110
|
}
|
|
111
111
|
return [...found];
|
|
112
112
|
}
|
|
113
|
-
/** @deprecated Use extractFragmentNames instead */
|
|
114
|
-
export const extractEntityNames = extractFragmentNames;
|
|
115
113
|
function getOrCreateFragment(db, name, type) {
|
|
116
114
|
try {
|
|
117
115
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [name, type, new Date().toISOString().slice(0, 10)]);
|
|
@@ -190,8 +188,6 @@ export function beginUserFragmentBuildCache(phrenPath, projects) {
|
|
|
190
188
|
}
|
|
191
189
|
}
|
|
192
190
|
}
|
|
193
|
-
/** @deprecated Use beginUserFragmentBuildCache instead */
|
|
194
|
-
export const beginUserEntityBuildCache = beginUserFragmentBuildCache;
|
|
195
191
|
/** End a build-scoped cache created by beginUserFragmentBuildCache(). */
|
|
196
192
|
export function endUserFragmentBuildCache(phrenPath) {
|
|
197
193
|
const prefix = `${phrenPath}/`;
|
|
@@ -202,8 +198,6 @@ export function endUserFragmentBuildCache(phrenPath) {
|
|
|
202
198
|
if (_activeBuildCacheKeyPrefix === prefix)
|
|
203
199
|
_activeBuildCacheKeyPrefix = null;
|
|
204
200
|
}
|
|
205
|
-
/** @deprecated Use endUserFragmentBuildCache instead */
|
|
206
|
-
export const endUserEntityBuildCache = endUserFragmentBuildCache;
|
|
207
201
|
function parseUserDefinedFragments(phrenPath, project) {
|
|
208
202
|
const claudeMdPath = `${phrenPath}/${project}/CLAUDE.md`;
|
|
209
203
|
const cacheKey = `${phrenPath}/${project}`;
|
|
@@ -238,13 +232,11 @@ function parseUserDefinedFragments(phrenPath, project) {
|
|
|
238
232
|
}
|
|
239
233
|
}
|
|
240
234
|
/** Clear the user fragment cache (call between index builds). */
|
|
241
|
-
|
|
235
|
+
function clearUserFragmentCache() {
|
|
242
236
|
_userFragmentCache.clear();
|
|
243
237
|
_buildUserFragmentCache.clear();
|
|
244
238
|
_activeBuildCacheKeyPrefix = null;
|
|
245
239
|
}
|
|
246
|
-
/** @deprecated Use clearUserFragmentCache instead */
|
|
247
|
-
export const clearUserEntityCache = clearUserFragmentCache;
|
|
248
240
|
// Words that commonly start sentences or appear in titles — not fragment names
|
|
249
241
|
const SENTENCE_START_WORDS = new Set([
|
|
250
242
|
"the", "this", "that", "these", "those", "when", "where", "which", "while",
|
|
@@ -362,8 +354,6 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
|
|
|
362
354
|
}
|
|
363
355
|
}
|
|
364
356
|
}
|
|
365
|
-
/** @deprecated Use extractAndLinkFragments instead */
|
|
366
|
-
export const extractAndLinkEntities = extractAndLinkFragments;
|
|
367
357
|
/**
|
|
368
358
|
* Query related fragments for a given name.
|
|
369
359
|
*/
|
|
@@ -389,8 +379,6 @@ export function queryFragmentLinks(db, name) {
|
|
|
389
379
|
}
|
|
390
380
|
return { related };
|
|
391
381
|
}
|
|
392
|
-
/** @deprecated Use queryFragmentLinks instead */
|
|
393
|
-
export const queryEntityLinks = queryFragmentLinks;
|
|
394
382
|
/**
|
|
395
383
|
* Query cross-project fragment relationships.
|
|
396
384
|
* Returns projects and docs that share fragments with the given query.
|
|
@@ -444,5 +432,3 @@ export function getFragmentBoostDocs(db, query) {
|
|
|
444
432
|
return new Set();
|
|
445
433
|
}
|
|
446
434
|
}
|
|
447
|
-
/** @deprecated Use getFragmentBoostDocs instead */
|
|
448
|
-
export const getEntityBoostDocs = getFragmentBoostDocs;
|