@phren/cli 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +2 -8
  2. package/mcp/dist/cli-actions.js +5 -5
  3. package/mcp/dist/cli-config.js +334 -127
  4. package/mcp/dist/cli-govern.js +140 -3
  5. package/mcp/dist/cli-graph.js +3 -2
  6. package/mcp/dist/cli-hooks-globs.js +2 -1
  7. package/mcp/dist/cli-hooks-output.js +3 -3
  8. package/mcp/dist/cli-hooks.js +41 -34
  9. package/mcp/dist/cli-namespaces.js +15 -5
  10. package/mcp/dist/cli-search.js +2 -2
  11. package/mcp/dist/content-archive.js +2 -2
  12. package/mcp/dist/content-citation.js +12 -22
  13. package/mcp/dist/content-dedup.js +9 -9
  14. package/mcp/dist/data-access.js +1 -1
  15. package/mcp/dist/data-tasks.js +23 -0
  16. package/mcp/dist/embedding.js +7 -7
  17. package/mcp/dist/entrypoint.js +129 -102
  18. package/mcp/dist/governance-locks.js +6 -5
  19. package/mcp/dist/governance-policy.js +155 -2
  20. package/mcp/dist/governance-scores.js +3 -3
  21. package/mcp/dist/hooks.js +39 -18
  22. package/mcp/dist/index.js +4 -4
  23. package/mcp/dist/init-config.js +3 -24
  24. package/mcp/dist/init-setup.js +5 -5
  25. package/mcp/dist/init.js +170 -23
  26. package/mcp/dist/link-checksums.js +3 -2
  27. package/mcp/dist/link-context.js +1 -1
  28. package/mcp/dist/link-doctor.js +3 -3
  29. package/mcp/dist/link-skills.js +98 -12
  30. package/mcp/dist/link.js +17 -27
  31. package/mcp/dist/machine-identity.js +1 -9
  32. package/mcp/dist/mcp-config.js +247 -42
  33. package/mcp/dist/mcp-data.js +9 -9
  34. package/mcp/dist/mcp-extract-facts.js +1 -1
  35. package/mcp/dist/mcp-extract.js +2 -2
  36. package/mcp/dist/mcp-finding.js +6 -6
  37. package/mcp/dist/mcp-graph.js +11 -11
  38. package/mcp/dist/mcp-ops.js +18 -18
  39. package/mcp/dist/mcp-search.js +8 -8
  40. package/mcp/dist/mcp-tasks.js +21 -1
  41. package/mcp/dist/memory-ui-page.js +23 -0
  42. package/mcp/dist/memory-ui-scripts.js +210 -27
  43. package/mcp/dist/memory-ui-server.js +115 -3
  44. package/mcp/dist/phren-paths.js +7 -7
  45. package/mcp/dist/profile-store.js +2 -2
  46. package/mcp/dist/project-config.js +63 -16
  47. package/mcp/dist/session-utils.js +3 -2
  48. package/mcp/dist/shared-fragment-graph.js +22 -21
  49. package/mcp/dist/shared-index.js +144 -105
  50. package/mcp/dist/shared-retrieval.js +22 -56
  51. package/mcp/dist/shared-search-fallback.js +13 -13
  52. package/mcp/dist/shared-sqljs.js +3 -2
  53. package/mcp/dist/shared.js +3 -3
  54. package/mcp/dist/shell-input.js +1 -1
  55. package/mcp/dist/shell-state-store.js +1 -1
  56. package/mcp/dist/shell-view.js +3 -2
  57. package/mcp/dist/shell.js +1 -1
  58. package/mcp/dist/skill-files.js +4 -10
  59. package/mcp/dist/skill-registry.js +3 -0
  60. package/mcp/dist/status.js +41 -13
  61. package/mcp/dist/task-hygiene.js +1 -1
  62. package/mcp/dist/telemetry.js +5 -4
  63. package/mcp/dist/update.js +1 -1
  64. package/mcp/dist/utils.js +3 -3
  65. package/package.json +2 -2
  66. package/starter/global/skills/audit.md +106 -0
  67. package/mcp/dist/shared-paths.js +0 -1
@@ -108,7 +108,75 @@ export function readSkillManifestHooks(phrenPath) {
108
108
  }
109
109
  return Object.keys(result).length > 0 ? result : null;
110
110
  }
111
- // ── Skill linking helpers ───────────────────────────────────────────────────
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
- const stat = fs.lstatSync(destPath);
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 || 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, path.join(destDir, entry), managedRoot);
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, path.join(destDir, entry), managedRoot);
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();
package/mcp/dist/link.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import * as crypto from "crypto";
4
3
  import * as readline from "readline";
5
4
  import * as yaml from "js-yaml";
6
5
  import { execFileSync } from "child_process";
7
- import { fileURLToPath } from "url";
6
+ import { ROOT } from "./package-metadata.js";
8
7
  import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
9
8
  import { configureAllHooks, detectInstalledTools } from "./hooks.js";
10
9
  import { getMachineName, persistMachineName } from "./machine-identity.js";
11
- import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, } from "./shared.js";
10
+ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "./shared.js";
12
11
  import { errorMessage } from "./utils.js";
12
+ import { log } from "./init-shared.js";
13
13
  import { listMachines as listMachinesShared, listProfiles as listProfilesShared, setMachineProfile, } from "./profile-store.js";
14
- import { writeSkillMd } from "./link-skills.js";
14
+ import { writeSkillMd, isManagedSymlink } from "./link-skills.js";
15
15
  import { syncScopeSkillsToDir } from "./skill-files.js";
16
16
  import { renderSkillInstructionsSection } from "./skill-registry.js";
17
17
  import { findProjectDir } from "./project-locator.js";
@@ -23,14 +23,6 @@ export { updateFileChecksums, verifyFileChecksums } from "./link-checksums.js";
23
23
  export { findProjectDir } from "./project-locator.js";
24
24
  export { parseSkillFrontmatter, validateSkillFrontmatter, validateSkillsDir, readSkillManifestHooks, } from "./link-skills.js";
25
25
  // ── Helpers (exported for link-doctor) ──────────────────────────────────────
26
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
- function log(msg) { process.stdout.write(msg + "\n"); }
28
- function atomicWriteText(filePath, content) {
29
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
31
- fs.writeFileSync(tmpPath, content);
32
- fs.renameSync(tmpPath, filePath);
33
- }
34
26
  export { getMachineName } from "./machine-identity.js";
35
27
  export function lookupProfile(phrenPath, machine) {
36
28
  const listed = listMachinesShared(phrenPath);
@@ -123,7 +115,7 @@ function setupSparseCheckout(phrenPath, projects) {
123
115
  execFileSync("git", ["rev-parse", "--git-dir"], { cwd: phrenPath, stdio: "ignore", timeout: EXEC_TIMEOUT_QUICK_MS });
124
116
  }
125
117
  catch (err) {
126
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
118
+ if ((process.env.PHREN_DEBUG))
127
119
  process.stderr.write(`[phren] setupSparseCheckout notAGitRepo: ${errorMessage(err)}\n`);
128
120
  return;
129
121
  }
@@ -184,10 +176,9 @@ function symlinkFile(src, dest, managedRoot) {
184
176
  if (stat.isSymbolicLink()) {
185
177
  const currentTarget = fs.readlinkSync(dest);
186
178
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
187
- const managedPrefix = path.resolve(managedRoot) + path.sep;
188
179
  if (resolvedTarget === path.resolve(src))
189
180
  return true;
190
- if (!resolvedTarget.startsWith(managedPrefix)) {
181
+ if (!isManagedSymlink(dest, managedRoot)) {
191
182
  log(` preserve existing symlink: ${dest}`);
192
183
  return false;
193
184
  }
@@ -238,8 +229,7 @@ function writeManagedAgentsFile(src, dest, content, managedRoot) {
238
229
  if (stat.isSymbolicLink()) {
239
230
  const currentTarget = fs.readlinkSync(dest);
240
231
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
241
- const managedPrefix = path.resolve(managedRoot) + path.sep;
242
- if (resolvedTarget === path.resolve(src) || resolvedTarget.startsWith(managedPrefix)) {
232
+ if (resolvedTarget === path.resolve(src) || isManagedSymlink(dest, managedRoot)) {
243
233
  fs.unlinkSync(dest);
244
234
  }
245
235
  else {
@@ -278,7 +268,7 @@ function linkGlobal(phrenPath, tools) {
278
268
  symlinkFile(globalClaude, path.join(copilotInstrDir, "copilot-instructions.md"), phrenPath);
279
269
  }
280
270
  catch (err) {
281
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
271
+ if ((process.env.PHREN_DEBUG))
282
272
  process.stderr.write(`[phren] linkGlobal copilotInstructions: ${errorMessage(err)}\n`);
283
273
  }
284
274
  }
@@ -324,7 +314,7 @@ function linkProject(phrenPath, project, tools) {
324
314
  symlinkFile(src, path.join(copilotDir, "copilot-instructions.md"), phrenPath);
325
315
  }
326
316
  catch (err) {
327
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
317
+ if ((process.env.PHREN_DEBUG))
328
318
  process.stderr.write(`[phren] linkProject copilotInstructions: ${errorMessage(err)}\n`);
329
319
  }
330
320
  }
@@ -348,7 +338,7 @@ function linkProject(phrenPath, project, tools) {
348
338
  addTokenAnnotation(claudeFile);
349
339
  }
350
340
  catch (err) {
351
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
341
+ if ((process.env.PHREN_DEBUG))
352
342
  process.stderr.write(`[phren] linkProject tokenAnnotation: ${errorMessage(err)}\n`);
353
343
  }
354
344
  }
@@ -365,7 +355,7 @@ function linkProject(phrenPath, project, tools) {
365
355
  excludeEntries.push("AGENTS.md");
366
356
  }
367
357
  catch (err) {
368
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
358
+ if ((process.env.PHREN_DEBUG))
369
359
  process.stderr.write(`[phren] linkProject agentsMd: ${errorMessage(err)}\n`);
370
360
  }
371
361
  }
@@ -506,7 +496,7 @@ export async function runLink(phrenPath, opts = {}) {
506
496
  mcpStatus = configureClaude(phrenPath, { mcpEnabled, hooksEnabled }) ?? "installed";
507
497
  }
508
498
  catch (err) {
509
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
499
+ if ((process.env.PHREN_DEBUG))
510
500
  process.stderr.write(`[phren] link configureClaude: ${errorMessage(err)}\n`);
511
501
  }
512
502
  logMcpTargetStatus("Claude", mcpStatus);
@@ -515,7 +505,7 @@ export async function runLink(phrenPath, opts = {}) {
515
505
  vsStatus = configureVSCode(phrenPath, { mcpEnabled }) ?? "no_vscode";
516
506
  }
517
507
  catch (err) {
518
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
508
+ if ((process.env.PHREN_DEBUG))
519
509
  process.stderr.write(`[phren] link configureVSCode: ${errorMessage(err)}\n`);
520
510
  }
521
511
  logMcpTargetStatus("VS Code", vsStatus);
@@ -524,7 +514,7 @@ export async function runLink(phrenPath, opts = {}) {
524
514
  cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled }) ?? "no_cursor";
525
515
  }
526
516
  catch (err) {
527
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
517
+ if ((process.env.PHREN_DEBUG))
528
518
  process.stderr.write(`[phren] link configureCursorMcp: ${errorMessage(err)}\n`);
529
519
  }
530
520
  logMcpTargetStatus("Cursor", cursorStatus);
@@ -533,7 +523,7 @@ export async function runLink(phrenPath, opts = {}) {
533
523
  copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled }) ?? "no_copilot";
534
524
  }
535
525
  catch (err) {
536
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
526
+ if ((process.env.PHREN_DEBUG))
537
527
  process.stderr.write(`[phren] link configureCopilotMcp: ${errorMessage(err)}\n`);
538
528
  }
539
529
  logMcpTargetStatus("Copilot CLI", copilotStatus);
@@ -542,7 +532,7 @@ export async function runLink(phrenPath, opts = {}) {
542
532
  codexStatus = configureCodexMcp(phrenPath, { mcpEnabled }) ?? "no_codex";
543
533
  }
544
534
  catch (err) {
545
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
535
+ if ((process.env.PHREN_DEBUG))
546
536
  process.stderr.write(`[phren] link configureCodexMcp: ${errorMessage(err)}\n`);
547
537
  }
548
538
  logMcpTargetStatus("Codex", codexStatus);
@@ -566,7 +556,7 @@ export async function runLink(phrenPath, opts = {}) {
566
556
  log(` phren.SKILL.md written (agentskills-compatible tools)`);
567
557
  }
568
558
  catch (err) {
569
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
559
+ if ((process.env.PHREN_DEBUG))
570
560
  process.stderr.write(`[phren] link writeSkillMd: ${errorMessage(err)}\n`);
571
561
  }
572
562
  log("");
@@ -1,17 +1,9 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
- import * as path from "path";
4
- import * as crypto from "crypto";
5
- import { homePath } from "./shared.js";
3
+ import { homePath, atomicWriteText } from "./shared.js";
6
4
  function phrenMachineFilePath() {
7
5
  return homePath(".phren", ".machine-id");
8
6
  }
9
- function atomicWriteText(filePath, content) {
10
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
12
- fs.writeFileSync(tmpPath, content);
13
- fs.renameSync(tmpPath, filePath);
14
- }
15
7
  export function machineFilePath() {
16
8
  return phrenMachineFilePath();
17
9
  }