@phren/cli 0.0.28 → 0.0.33

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 (153) hide show
  1. package/mcp/dist/capabilities/cli.js +2 -5
  2. package/mcp/dist/capabilities/mcp.js +5 -8
  3. package/mcp/dist/capabilities/types.js +2 -5
  4. package/mcp/dist/capabilities/vscode.js +2 -5
  5. package/mcp/dist/capabilities/web-ui.js +2 -5
  6. package/mcp/dist/{cli-actions.js → cli/actions.js} +25 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
  11. package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
  12. package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
  13. package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
  14. package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
  15. package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
  16. package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +58 -117
  17. package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
  18. package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
  19. package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
  20. package/mcp/dist/{cli-search.js → cli/search.js} +12 -11
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +323 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +337 -0
  24. package/mcp/dist/cli-hooks-stop.js +519 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
  26. package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
  27. package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
  28. package/mcp/dist/{content-learning.js → content/learning.js} +41 -20
  29. package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
  30. package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
  31. package/mcp/dist/{core-project.js → core/project.js} +4 -4
  32. package/mcp/dist/{core-search.js → core/search.js} +2 -2
  33. package/mcp/dist/{data-access.js → data/access.js} +142 -15
  34. package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
  35. package/mcp/dist/embedding.js +9 -14
  36. package/mcp/dist/entrypoint.js +11 -11
  37. package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
  38. package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
  39. package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
  40. package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +13 -7
  41. package/mcp/dist/governance/audit.js +30 -0
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
  46. package/mcp/dist/hooks.js +53 -37
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +54 -30
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +80 -69
  51. package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
  52. package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
  53. package/mcp/dist/{init-shared.js → init/shared.js} +4 -4
  54. package/mcp/dist/init-bootstrap.js +21 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-env.js +114 -0
  57. package/mcp/dist/init-fresh.js +234 -0
  58. package/mcp/dist/init-hooks.js +26 -0
  59. package/mcp/dist/init-mcp.js +65 -0
  60. package/mcp/dist/init-modes.js +135 -0
  61. package/mcp/dist/init-npm.js +37 -0
  62. package/mcp/dist/init-project-local.js +99 -0
  63. package/mcp/dist/init-semantic.js +48 -0
  64. package/mcp/dist/init-types.js +1 -0
  65. package/mcp/dist/init-uninstall.js +504 -0
  66. package/mcp/dist/init-update.js +96 -0
  67. package/mcp/dist/init-walkthrough.js +524 -0
  68. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  69. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  70. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  71. package/mcp/dist/{link.js → link/link.js} +26 -31
  72. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  73. package/mcp/dist/logger.js +11 -3
  74. package/mcp/dist/package-metadata.js +1 -1
  75. package/mcp/dist/phren-art.js +4 -126
  76. package/mcp/dist/phren-paths.js +30 -12
  77. package/mcp/dist/proactivity.js +3 -3
  78. package/mcp/dist/profile-store.js +5 -6
  79. package/mcp/dist/project-config.js +2 -2
  80. package/mcp/dist/project-topics.js +17 -47
  81. package/mcp/dist/provider-adapters.js +1 -1
  82. package/mcp/dist/query-correlation.js +1 -1
  83. package/mcp/dist/runtime-profile.js +1 -1
  84. package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
  85. package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
  86. package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
  87. package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +28 -3
  88. package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
  89. package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +19 -42
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
  93. package/mcp/dist/shared/process.js +24 -0
  94. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
  95. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
  96. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  97. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  98. package/mcp/dist/shared.js +6 -60
  99. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  100. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  101. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  102. package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
  103. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  104. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  105. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  106. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  107. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  108. package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
  109. package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
  110. package/mcp/dist/startup-embedding.js +2 -2
  111. package/mcp/dist/status.js +15 -14
  112. package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
  113. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  114. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
  115. package/mcp/dist/telemetry.js +3 -4
  116. package/mcp/dist/tool-registry.js +29 -17
  117. package/mcp/dist/tools/config.js +530 -0
  118. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  119. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  120. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  121. package/mcp/dist/tools/finding.js +584 -0
  122. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  123. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  124. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  125. package/mcp/dist/tools/ops.js +468 -0
  126. package/mcp/dist/tools/search.js +672 -0
  127. package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
  128. package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
  129. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  130. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  131. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  132. package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
  133. package/mcp/dist/ui/server.js +1024 -0
  134. package/mcp/dist/update.js +2 -2
  135. package/mcp/dist/utils.js +63 -19
  136. package/package.json +2 -2
  137. package/scripts/preuninstall.mjs +31 -0
  138. package/starter/global/CLAUDE.md +3 -2
  139. package/mcp/dist/governance-audit.js +0 -22
  140. package/mcp/dist/mcp-config.js +0 -551
  141. package/mcp/dist/mcp-finding.js +0 -594
  142. package/mcp/dist/mcp-ops.js +0 -363
  143. package/mcp/dist/mcp-search.js +0 -668
  144. package/mcp/dist/memory-ui-server.js +0 -1411
  145. package/mcp/dist/shared-governance.js +0 -4
  146. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  147. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  148. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  149. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  150. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  151. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  152. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  153. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -1,23 +1,24 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { execFileSync } from "child_process";
4
- import { debugLog, EXEC_TIMEOUT_QUICK_MS, getProjectDirs, isRecord, homeDir, homePath, hookConfigPath, runtimeHealthFile, } from "./shared.js";
5
- import { commandVersion, versionAtLeast, nearestWritableTarget } from "./init-shared.js";
6
- import { validateGovernanceJson } from "./shared-governance.js";
7
- import { errorMessage } from "./utils.js";
8
- import { buildIndex, queryRows } from "./shared-index.js";
9
- import { validateTaskFormat, validateFindingsFormat } from "./shared-content.js";
10
- import { detectInstalledTools } from "./hooks.js";
11
- import { validateSkillFrontmatter, validateSkillsDir } from "./link-skills.js";
12
- import { verifyFileChecksums, updateFileChecksums } from "./link-checksums.js";
13
- import { buildSkillManifest } from "./skill-registry.js";
14
- import { inspectTaskHygiene } from "./task-hygiene.js";
15
- import { resolveTaskFilePath, TASK_FILE_ALIASES } from "./data-tasks.js";
16
- import { repairPreexistingInstall } from "./init-setup.js";
4
+ import { debugLog, EXEC_TIMEOUT_QUICK_MS, getProjectDirs, isRecord, homeDir, homePath, hookConfigPath, runtimeHealthFile, } from "../shared.js";
5
+ import { commandVersion, versionAtLeast, nearestWritableTarget } from "../init/shared.js";
6
+ import { validateGovernanceJson } from "../shared/governance.js";
7
+ import { errorMessage } from "../utils.js";
8
+ import { buildIndex, queryRows } from "../shared/index.js";
9
+ import { validateTaskFormat, validateFindingsFormat } from "../shared/content.js";
10
+ import { detectInstalledTools } from "../hooks.js";
11
+ import { validateSkillFrontmatter, validateSkillsDir } from "./skills.js";
12
+ import { verifyFileChecksums, updateFileChecksums } from "./checksums.js";
13
+ import { buildSkillManifest } from "../skill/registry.js";
14
+ import { inspectTaskHygiene } from "../task/hygiene.js";
15
+ import { resolveTaskFilePath, TASK_FILE_ALIASES } from "../data/tasks.js";
16
+ import { repairPreexistingInstall } from "../init/setup.js";
17
17
  import { getMachineName, lookupProfile, findProfileFile, getProfileProjects, findProjectDir, } from "./link.js";
18
- import { claudeProjectKey } from "./link-context.js";
19
- import { getProjectOwnershipMode, readProjectConfig } from "./project-config.js";
20
- import { readInstallPreferences } from "./init-preferences.js";
18
+ import { claudeProjectKey } from "./context.js";
19
+ import { getProjectOwnershipMode, readProjectConfig } from "../project-config.js";
20
+ import { readInstallPreferences } from "../init/preferences.js";
21
+ import { logger } from "../logger.js";
21
22
  // ── Doctor ──────────────────────────────────────────────────────────────────
22
23
  function isWrapperActive(tool) {
23
24
  const wrapperPath = homePath(".local", "bin", tool);
@@ -176,15 +177,13 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
176
177
  fsMs = Date.now() - t0;
177
178
  }
178
179
  catch (err) {
179
- if ((process.env.PHREN_DEBUG))
180
- process.stderr.write(`[phren] doctor fsBenchmark: ${errorMessage(err)}\n`);
180
+ logger.debug("doctor", `doctor fsBenchmark: ${errorMessage(err)}`);
181
181
  fsMs = -1;
182
182
  try {
183
183
  fs.unlinkSync(fsBenchFile);
184
184
  }
185
185
  catch (e2) {
186
- if ((process.env.PHREN_DEBUG))
187
- process.stderr.write(`[phren] doctor fsBenchmarkCleanup: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
186
+ logger.debug("doctor", `doctor fsBenchmarkCleanup: ${e2 instanceof Error ? e2.message : String(e2)}`);
188
187
  }
189
188
  }
190
189
  const fsSlow = fsMs > 500 || fsMs < 0;
@@ -306,8 +305,7 @@ export async function runDoctor(phrenPath, fix = false, checkData = false) {
306
305
  runtime = JSON.parse(fs.readFileSync(runtimeHealthPath, "utf8"));
307
306
  }
308
307
  catch (err) {
309
- if ((process.env.PHREN_DEBUG))
310
- process.stderr.write(`[phren] doctor runtimeHealth: ${errorMessage(err)}\n`);
308
+ logger.debug("doctor", `doctor runtimeHealth: ${errorMessage(err)}`);
311
309
  runtime = null;
312
310
  }
313
311
  }
@@ -3,27 +3,28 @@ import * as path from "path";
3
3
  import * as readline from "readline";
4
4
  import * as yaml from "js-yaml";
5
5
  import { execFileSync } from "child_process";
6
- import { ROOT } from "./package-metadata.js";
7
- import { configureMcpTargets, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
8
- import { configureAllHooks, detectInstalledTools } from "./hooks.js";
9
- import { getMachineName, persistMachineName } from "./machine-identity.js";
10
- import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "./shared.js";
11
- import { errorMessage } from "./utils.js";
12
- import { log } from "./init-shared.js";
13
- import { listMachines as listMachinesShared, listProfiles as listProfilesShared, setMachineProfile, } from "./profile-store.js";
14
- import { writeSkillMd, isManagedSymlink } from "./link-skills.js";
15
- import { syncScopeSkillsToDir } from "./skill-files.js";
16
- import { renderSkillInstructionsSection } from "./skill-registry.js";
17
- import { findProjectDir } from "./project-locator.js";
18
- import { getProjectOwnershipMode, readProjectConfig, } from "./project-config.js";
19
- import { writeContextDefault, writeContextDebugging, writeContextPlanning, writeContextClean, readBackNativeMemory, rebuildMemory, } from "./link-context.js";
6
+ import { ROOT } from "../package-metadata.js";
7
+ import { configureMcpTargets, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, patchJsonFile, setMcpEnabledPreference, } from "../init/init.js";
8
+ import { configureAllHooks, detectInstalledTools } from "../hooks.js";
9
+ import { getMachineName, persistMachineName } from "../machine-identity.js";
10
+ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "../shared.js";
11
+ import { errorMessage } from "../utils.js";
12
+ import { log } from "../init/shared.js";
13
+ import { listMachines as listMachinesShared, listProfiles as listProfilesShared, setMachineProfile, } from "../profile-store.js";
14
+ import { writeSkillMd, isManagedSymlink } from "./skills.js";
15
+ import { syncScopeSkillsToDir } from "../skill/files.js";
16
+ import { renderSkillInstructionsSection } from "../skill/registry.js";
17
+ import { findProjectDir } from "../project-locator.js";
18
+ import { getProjectOwnershipMode, readProjectConfig, } from "../project-config.js";
19
+ import { writeContextDefault, writeContextDebugging, writeContextPlanning, writeContextClean, readBackNativeMemory, rebuildMemory, } from "./context.js";
20
+ import { logger } from "../logger.js";
20
21
  // Re-export sub-modules so existing imports from "./link.js" continue to work
21
- export { runDoctor } from "./link-doctor.js";
22
- export { updateFileChecksums, verifyFileChecksums } from "./link-checksums.js";
23
- export { findProjectDir } from "./project-locator.js";
24
- export { parseSkillFrontmatter, validateSkillFrontmatter, validateSkillsDir, readSkillManifestHooks, } from "./link-skills.js";
22
+ export { runDoctor } from "./doctor.js";
23
+ export { updateFileChecksums, verifyFileChecksums } from "./checksums.js";
24
+ export { findProjectDir } from "../project-locator.js";
25
+ export { parseSkillFrontmatter, validateSkillFrontmatter, validateSkillsDir, readSkillManifestHooks, } from "./skills.js";
25
26
  // ── Helpers (exported for link-doctor) ──────────────────────────────────────
26
- export { getMachineName } from "./machine-identity.js";
27
+ export { getMachineName } from "../machine-identity.js";
27
28
  export function lookupProfile(phrenPath, machine) {
28
29
  const listed = listMachinesShared(phrenPath);
29
30
  if (!listed.ok)
@@ -115,8 +116,7 @@ function setupSparseCheckout(phrenPath, projects) {
115
116
  execFileSync("git", ["rev-parse", "--git-dir"], { cwd: phrenPath, stdio: "ignore", timeout: EXEC_TIMEOUT_QUICK_MS });
116
117
  }
117
118
  catch (err) {
118
- if ((process.env.PHREN_DEBUG))
119
- process.stderr.write(`[phren] setupSparseCheckout notAGitRepo: ${errorMessage(err)}\n`);
119
+ logger.debug("link", `setupSparseCheckout notAGitRepo: ${errorMessage(err)}`);
120
120
  return;
121
121
  }
122
122
  const alwaysInclude = ["profiles", "machines.yaml", "global", "scripts", "link.sh", "README.md", ".gitignore"];
@@ -268,8 +268,7 @@ function linkGlobal(phrenPath, tools) {
268
268
  symlinkFile(globalClaude, path.join(copilotInstrDir, "copilot-instructions.md"), phrenPath);
269
269
  }
270
270
  catch (err) {
271
- if ((process.env.PHREN_DEBUG))
272
- process.stderr.write(`[phren] linkGlobal copilotInstructions: ${errorMessage(err)}\n`);
271
+ logger.debug("link", `linkGlobal copilotInstructions: ${errorMessage(err)}`);
273
272
  }
274
273
  }
275
274
  }
@@ -314,8 +313,7 @@ function linkProject(phrenPath, project, tools) {
314
313
  symlinkFile(src, path.join(copilotDir, "copilot-instructions.md"), phrenPath);
315
314
  }
316
315
  catch (err) {
317
- if ((process.env.PHREN_DEBUG))
318
- process.stderr.write(`[phren] linkProject copilotInstructions: ${errorMessage(err)}\n`);
316
+ logger.debug("link", `linkProject copilotInstructions: ${errorMessage(err)}`);
319
317
  }
320
318
  }
321
319
  }
@@ -338,8 +336,7 @@ function linkProject(phrenPath, project, tools) {
338
336
  addTokenAnnotation(claudeFile);
339
337
  }
340
338
  catch (err) {
341
- if ((process.env.PHREN_DEBUG))
342
- process.stderr.write(`[phren] linkProject tokenAnnotation: ${errorMessage(err)}\n`);
339
+ logger.debug("link", `linkProject tokenAnnotation: ${errorMessage(err)}`);
343
340
  }
344
341
  }
345
342
  // Project-level skills
@@ -355,8 +352,7 @@ function linkProject(phrenPath, project, tools) {
355
352
  excludeEntries.push("AGENTS.md");
356
353
  }
357
354
  catch (err) {
358
- if ((process.env.PHREN_DEBUG))
359
- process.stderr.write(`[phren] linkProject agentsMd: ${errorMessage(err)}\n`);
355
+ logger.debug("link", `linkProject agentsMd: ${errorMessage(err)}`);
360
356
  }
361
357
  }
362
358
  // Auto-exclude phren-managed files from git status
@@ -507,8 +503,7 @@ export async function runLink(phrenPath, opts = {}) {
507
503
  log(` phren.SKILL.md written (agentskills-compatible tools)`);
508
504
  }
509
505
  catch (err) {
510
- if ((process.env.PHREN_DEBUG))
511
- process.stderr.write(`[phren] link writeSkillMd: ${errorMessage(err)}\n`);
506
+ logger.debug("link", `link writeSkillMd: ${errorMessage(err)}`);
512
507
  }
513
508
  log("");
514
509
  // Step 7: Context file
@@ -1,12 +1,13 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as yaml from "js-yaml";
4
- import { debugLog } from "./shared.js";
5
- import { errorMessage } from "./utils.js";
6
- import { buildSharedLifecycleCommands } from "./hooks.js";
7
- import { VERSION } from "./package-metadata.js";
8
- import { getToolCount, renderToolCatalogMarkdown } from "./tool-registry.js";
9
- import { isSkillEnabled } from "./skill-state.js";
4
+ import { debugLog } from "../shared.js";
5
+ import { errorMessage } from "../utils.js";
6
+ import { buildSharedLifecycleCommands } from "../hooks.js";
7
+ import { VERSION } from "../package-metadata.js";
8
+ import { getToolCount, renderToolCatalogMarkdown } from "../tool-registry.js";
9
+ import { isSkillEnabled } from "../skill/state.js";
10
+ import { logger } from "../logger.js";
10
11
  const REQUIRED_SKILL_FIELDS = ["name", "description"];
11
12
  export function parseSkillFrontmatter(rawContent) {
12
13
  // Normalize UTF-8 BOM and Windows-style CRLF line endings before matching frontmatter
@@ -190,8 +191,7 @@ function cleanupManagedSkillLinks(destDir, expectedNames, managedRoot) {
190
191
  fs.unlinkSync(destPath);
191
192
  }
192
193
  catch (err) {
193
- if ((process.env.PHREN_DEBUG))
194
- process.stderr.write(`[phren] cleanupManagedSkillLinks: ${errorMessage(err)}\n`);
194
+ logger.debug("link-skills", `cleanupManagedSkillLinks: ${errorMessage(err)}`);
195
195
  }
196
196
  }
197
197
  }
@@ -217,7 +217,7 @@ export function linkSkillsDir(srcDir, destDir, managedRoot, symlinkFile, opts) {
217
217
  message: `Skipping skill '${skillName}' — user skill already exists at ${destPath}. To use phren's version, rename or remove your skill first.`,
218
218
  };
219
219
  collisions.push(collision);
220
- process.stderr.write(`[phren] ${collision.message}\n`);
220
+ logger.warn("link-skills", collision.message);
221
221
  continue;
222
222
  }
223
223
  expectedNames.add(entry);
@@ -234,7 +234,7 @@ export function linkSkillsDir(srcDir, destDir, managedRoot, symlinkFile, opts) {
234
234
  message: `Skipping skill '${skillName}' — user skill already exists at ${destPath}. To use phren's version, rename or remove your skill first.`,
235
235
  };
236
236
  collisions.push(collision);
237
- process.stderr.write(`[phren] ${collision.message}\n`);
237
+ logger.warn("link-skills", collision.message);
238
238
  continue;
239
239
  }
240
240
  expectedNames.add(entry);
@@ -1,11 +1,13 @@
1
1
  import * as fs from "fs";
2
2
  import { runtimeFile, findPhrenPath } from "./shared.js";
3
+ let _cachedPhrenPath;
3
4
  export function log(level, tool, message, extra) {
4
5
  try {
5
- const phrenPath = findPhrenPath();
6
- if (!phrenPath)
6
+ if (_cachedPhrenPath === undefined)
7
+ _cachedPhrenPath = findPhrenPath();
8
+ if (!_cachedPhrenPath)
7
9
  return;
8
- const logPath = runtimeFile(phrenPath, "debug.log");
10
+ const logPath = runtimeFile(_cachedPhrenPath, "debug.log");
9
11
  const line = JSON.stringify({ ts: new Date().toISOString(), level, tool, message, ...extra });
10
12
  fs.appendFileSync(logPath, line + "\n");
11
13
  }
@@ -13,3 +15,9 @@ export function log(level, tool, message, extra) {
13
15
  // Logging must never throw
14
16
  }
15
17
  }
18
+ export const logger = {
19
+ debug: (tool, message, extra) => log("debug", tool, message, extra),
20
+ info: (tool, message, extra) => log("info", tool, message, extra),
21
+ warn: (tool, message, extra) => log("warn", tool, message, extra),
22
+ error: (tool, message, extra) => log("error", tool, message, extra),
23
+ };
@@ -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
- export const PACKAGE_JSON_PATH = path.join(ROOT, "package.json");
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
  }
@@ -1,24 +1,9 @@
1
1
  /**
2
- * Phren character ASCII/Unicode art, animation engine, and spinner for CLI presence.
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 DIM = `${ESC}2m`;
17
- const DARK_PURPLE = `${ESC}38;5;57m`; // deep purple — shadow/outline
18
- const LIGHT_PURPLE = `${ESC}38;5;141m`; // lavender — brain highlights
19
- const MID_PURPLE = `${ESC}38;5;98m`; // mid tone
20
- const NAVY = `${ESC}38;5;18m`; // darkest outline
21
- // ── Art constants (24px wide, truecolor half-blocks) ─────────────────────────
22
7
  /**
23
8
  * Phren truecolor art (24px wide, generated from phren-transparent.png).
24
9
  * Uses half-block ▀ with RGB foreground+background for pixel-faithful rendering.
@@ -38,35 +23,19 @@ export const PHREN_ART = [
38
23
  " ",
39
24
  " ",
40
25
  ];
41
- /** The art width in visible columns */
42
- const ART_WIDTH = 24;
43
26
  // ── Sparkle row: the cyan pixels at row 2 ────────────────────────────────────
44
- // Sparkle uses ▄ half-blocks with cyan truecolor. For animation we cycle through
45
- // decorative unicode characters at different brightness levels.
46
27
  const SPARKLE_ROW = 2;
47
- const SPARKLE_CHARS = ["\u2726", "\u2727", "\u2736", " "]; // ✦ ✧ ✶ (dim/blank)
48
- // ── Eye detection: dark navy pixels in row 6 (fg R<30, G<45, B<120) ──────────
49
- // Row 6 has two eye pixels at segments 1 and 5 (visual positions 7 and 11).
50
- // 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 ─────────────────────────────────
51
30
  const EYE_ROW = 6;
52
- // Dark-pixel fg threshold
53
31
  const EYE_R_MAX = 30;
54
32
  const EYE_G_MAX = 45;
55
33
  const EYE_B_MAX = 120;
56
- // Body-purple color to use when "closing" eyes (average of surrounding pixels)
57
34
  const BLINK_COLOR = "146;130;250";
58
- /**
59
- * Flip a single art line horizontally. Reverses the order of colored pixel
60
- * segments and swaps leading/trailing whitespace so the character faces right.
61
- * Half-block characters (▀ ▄) are horizontally symmetric so no char swap needed.
62
- */
63
35
  function flipLine(line) {
64
36
  const stripped = line.replace(/\x1b\[[^m]*m/g, "");
65
37
  const leadSpaces = stripped.match(/^( *)/)[1].length;
66
38
  const trailSpaces = stripped.match(/( *)$/)[1].length;
67
- // Parse pixel segments: each is one or two ANSI color codes followed by a block char.
68
- // We strip any leading reset (\x1b[0m) from captured codes — it's an artifact from
69
- // the original per-pixel reset pattern and will be re-added during reassembly.
70
39
  const pixels = [];
71
40
  const pixelRegex = /((?:\x1b\[[^m]*m)+)([\u2580\u2584])/g;
72
41
  let match;
@@ -76,7 +45,7 @@ function flipLine(line) {
76
45
  pixels.push({ codes, char: match[2] });
77
46
  }
78
47
  if (pixels.length === 0)
79
- return line; // blank or space-only line
48
+ return line;
80
49
  const reversed = [...pixels].reverse();
81
50
  const newLead = " ".repeat(trailSpaces);
82
51
  const newTrail = " ".repeat(leadSpaces);
@@ -87,26 +56,14 @@ function flipLine(line) {
87
56
  result += newTrail;
88
57
  return result;
89
58
  }
90
- /**
91
- * Generate horizontally flipped art (facing right).
92
- */
93
59
  function generateFlippedArt(art) {
94
60
  return art.map(flipLine);
95
61
  }
96
- /** Pre-computed right-facing art */
97
62
  export const PHREN_ART_RIGHT = generateFlippedArt(PHREN_ART);
98
- /** Random integer in [min, max] inclusive */
99
63
  function randInt(min, max) {
100
64
  return Math.floor(Math.random() * (max - min + 1)) + min;
101
65
  }
102
- /**
103
- * Replace eye pixels on a single line with the blink color.
104
- * Eye pixels are identified by their dark navy fg color (R<30, G<45, B<120).
105
- * We replace the fg color code while preserving the bg code and character.
106
- */
107
66
  function applyBlinkToLine(line) {
108
- // Match ANSI color sequences: each pixel is \e[38;2;R;G;Bm (optionally with \e[48;2;...m) then a block char
109
- // We scan for fg codes that match the eye threshold and replace them with the blink color
110
67
  return line.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (full, rStr, gStr, bStr) => {
111
68
  const r = Number(rStr);
112
69
  const g = Number(gStr);
@@ -117,44 +74,24 @@ function applyBlinkToLine(line) {
117
74
  return full;
118
75
  });
119
76
  }
120
- /**
121
- * Replace the sparkle pixels on the sparkle row with the current sparkle character.
122
- * The sparkle row has two cyan ▄ half-blocks. During sparkle animation we replace
123
- * them with decorative Unicode characters from SPARKLE_CHARS.
124
- */
125
77
  function applySparkleToLine(line, frame, active) {
126
78
  if (!active)
127
79
  return line;
128
80
  const sparkleChar = SPARKLE_CHARS[frame % SPARKLE_CHARS.length];
129
81
  if (sparkleChar === " ") {
130
- // Dim: replace the cyan-colored segments with spaces (they disappear)
131
82
  return line.replace(/\x1b\[38;2;\d+;2\d\d;2\d\dm[\u2580\u2584]\x1b\[0m/g, " ");
132
83
  }
133
- // Replace the half-block characters with the sparkle unicode char, keeping the cyan color
134
84
  return line.replace(/(\x1b\[38;2;\d+;2\d\d;2\d\dm)[\u2580\u2584](\x1b\[0m)/g, `$1${sparkleChar}$2`);
135
85
  }
136
- /**
137
- * Apply lean (horizontal shift) to a line.
138
- * Positive offset = shift right (prepend spaces, trim from end).
139
- * Negative offset = shift left (trim from start, append spaces).
140
- */
141
86
  function applyLean(line, offset) {
142
87
  if (offset === 0)
143
88
  return line;
144
89
  if (offset > 0) {
145
- // Shift right: prepend spaces
146
90
  return " ".repeat(offset) + line;
147
91
  }
148
- // Shift left: remove leading spaces (up to |offset|)
149
92
  const trimCount = Math.min(-offset, line.match(/^( *)/)[1].length);
150
93
  return line.slice(trimCount);
151
94
  }
152
- /**
153
- * Create an animated phren character controller.
154
- *
155
- * @param options.facing - 'left' (default, original) or 'right' (flipped)
156
- * @param options.size - art width; unused but reserved for future scaling (default 24)
157
- */
158
95
  export function createPhrenAnimator(options) {
159
96
  const facing = options?.facing ?? "left";
160
97
  const baseArt = facing === "right" ? PHREN_ART_RIGHT : PHREN_ART;
@@ -170,22 +107,18 @@ export function createPhrenAnimator(options) {
170
107
  const t = setTimeout(fn, ms);
171
108
  timers.push(t);
172
109
  }
173
- // ── Bob animation: toggles bobUp every ~500ms ──────────────────────────
174
110
  function scheduleBob() {
175
111
  scheduleTimer(() => {
176
112
  state.bobUp = !state.bobUp;
177
113
  scheduleBob();
178
114
  }, 500);
179
115
  }
180
- // ── Blink animation: eyes close for 150ms, random 2-8s intervals ──────
181
116
  function scheduleBlink() {
182
117
  const interval = randInt(2000, 8000);
183
118
  scheduleTimer(() => {
184
- // Perform blink
185
119
  state.isBlinking = true;
186
120
  scheduleTimer(() => {
187
121
  state.isBlinking = false;
188
- // 30% chance of double-blink
189
122
  if (Math.random() < 0.3) {
190
123
  scheduleTimer(() => {
191
124
  state.isBlinking = true;
@@ -201,9 +134,7 @@ export function createPhrenAnimator(options) {
201
134
  }, 150);
202
135
  }, interval);
203
136
  }
204
- // ── Sparkle animation: fast cycle during bursts, long pauses between ───
205
137
  function scheduleSparkle() {
206
- // Wait 1-5 seconds before next sparkle burst
207
138
  const pause = randInt(1000, 5000);
208
139
  scheduleTimer(() => {
209
140
  state.sparkleActive = true;
@@ -213,7 +144,6 @@ export function createPhrenAnimator(options) {
213
144
  }
214
145
  function sparkleStep(step) {
215
146
  if (step >= SPARKLE_CHARS.length) {
216
- // Burst complete
217
147
  state.sparkleActive = false;
218
148
  scheduleSparkle();
219
149
  return;
@@ -223,7 +153,6 @@ export function createPhrenAnimator(options) {
223
153
  sparkleStep(step + 1);
224
154
  }, 200);
225
155
  }
226
- // ── Lean animation: shift 1 col left or right every 4-10s, hold 1-2s ──
227
156
  function scheduleLean() {
228
157
  const interval = randInt(4000, 10000);
229
158
  scheduleTimer(() => {
@@ -240,19 +169,15 @@ export function createPhrenAnimator(options) {
240
169
  getFrame() {
241
170
  let lines = baseArt.map((line, i) => {
242
171
  let result = line;
243
- // Apply blink to the eye row
244
172
  if (state.isBlinking && i === EYE_ROW) {
245
173
  result = applyBlinkToLine(result);
246
174
  }
247
- // Apply sparkle to sparkle row
248
175
  if (i === SPARKLE_ROW) {
249
176
  result = applySparkleToLine(result, state.sparkleFrame, state.sparkleActive);
250
177
  }
251
- // Apply lean
252
178
  result = applyLean(result, state.leanOffset);
253
179
  return result;
254
180
  });
255
- // Apply bob: when bobUp, prepend a blank line (shift everything down visually)
256
181
  if (state.bobUp) {
257
182
  lines = ["", ...lines.slice(0, -1)];
258
183
  }
@@ -272,56 +197,9 @@ export function createPhrenAnimator(options) {
272
197
  },
273
198
  };
274
199
  }
275
- // ── Startup frames (pre-baked, no timers) ────────────────────────────────────
276
- /**
277
- * Returns 4 pre-baked animation frames for shell startup display.
278
- * No timers needed — the caller cycles through them manually.
279
- *
280
- * Frames: [neutral, bob-up, neutral, bob-down(sparkle)]
281
- *
282
- * @param facing - 'left' (default) or 'right'
283
- */
284
- export function getPhrenStartupFrames(facing) {
285
- const art = facing === "right" ? PHREN_ART_RIGHT : PHREN_ART;
286
- // Frame 0: neutral
287
- const frame0 = [...art];
288
- // Frame 1: bob up (prepend blank line, drop last line)
289
- const frame1 = ["", ...art.slice(0, -1)];
290
- // Frame 2: neutral (same as frame 0)
291
- const frame2 = [...art];
292
- // Frame 3: bob down with sparkle burst — shift down by removing first line, append blank
293
- const frame3WithSparkle = art.map((line, i) => {
294
- if (i === SPARKLE_ROW) {
295
- return applySparkleToLine(line, 0, true); // ✦ sparkle
296
- }
297
- return line;
298
- });
299
- const frame3 = [...frame3WithSparkle.slice(1), ""];
300
- return [frame0, frame1, frame2, frame3];
301
- }
302
- // ── Legacy exports (unchanged) ───────────────────────────────────────────────
303
- /** Single-line compact phren for inline use */
304
- export const PHREN_INLINE = `${PURPLE}◆${RESET}`;
305
- /** Phren spinner frames for search/sync operations — cycles through in purple */
306
- export const PHREN_SPINNER_FRAMES = [
307
- `${BRIGHT_PURPLE}◆${RESET}`,
308
- `${PURPLE}◇${RESET}`,
309
- `${CYAN}✦${RESET}`,
310
- `${PURPLE}✧${RESET}`,
311
- `${BRIGHT_PURPLE}◆${RESET}`,
312
- `${DARK_PURPLE}◇${RESET}`,
313
- ];
314
- /** Default spinner interval in ms */
315
- export const PHREN_SPINNER_INTERVAL_MS = 120;
316
200
  /**
317
201
  * Return the phren art as a single string, optionally indented.
318
202
  */
319
203
  export function renderPhrenArt(indent = "") {
320
204
  return PHREN_ART.map(line => indent + line).join("\n");
321
205
  }
322
- /**
323
- * Get a spinner frame by index (wraps around automatically).
324
- */
325
- export function spinnerFrame(tick) {
326
- return PHREN_SPINNER_FRAMES[tick % PHREN_SPINNER_FRAMES.length];
327
- }
@@ -7,6 +7,10 @@ import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
7
7
  import { PhrenError, isRecord, RESERVED_PROJECT_DIR_NAMES } from "./phren-core.js";
8
8
  import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
9
9
  bootstrapPhrenDotEnv();
10
+ function stderrLog(msg) { try {
11
+ process.stderr.write("[phren] " + msg + "\n");
12
+ }
13
+ catch { } }
10
14
  export const ROOT_MANIFEST_FILENAME = "phren.root.yaml";
11
15
  export function homeDir() {
12
16
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -75,7 +79,7 @@ export function readRootManifest(phrenPath) {
75
79
  }
76
80
  catch (err) {
77
81
  if ((process.env.PHREN_DEBUG))
78
- process.stderr.write(`[phren] readRootManifest: ${errorMessage(err)}\n`);
82
+ stderrLog(`readRootManifest: ${errorMessage(err)}`);
79
83
  return null;
80
84
  }
81
85
  }
@@ -263,6 +267,20 @@ export function debugLog(msg) {
263
267
  // debug log is best-effort; logging errors about logging would recurse
264
268
  }
265
269
  }
270
+ /** Always-on structured error log (no PHREN_DEBUG gate). */
271
+ export function errorLog(tool, msg) {
272
+ try {
273
+ const phrenPath = findPhrenPath();
274
+ if (!phrenPath)
275
+ return;
276
+ const logFile = runtimeFile(phrenPath, "debug.log");
277
+ const line = JSON.stringify({ ts: new Date().toISOString(), level: "error", tool, message: msg });
278
+ fs.appendFileSync(logFile, line + "\n");
279
+ }
280
+ catch {
281
+ // Logging must never throw
282
+ }
283
+ }
266
284
  export function appendIndexEvent(phrenPath, event) {
267
285
  try {
268
286
  const file = runtimeFile(phrenPath, "index-events.jsonl");
@@ -270,7 +288,7 @@ export function appendIndexEvent(phrenPath, event) {
270
288
  }
271
289
  catch (err) {
272
290
  if ((process.env.PHREN_DEBUG))
273
- process.stderr.write(`[phren] appendIndexEvent: ${errorMessage(err)}\n`);
291
+ stderrLog(`appendIndexEvent: ${errorMessage(err)}`);
274
292
  }
275
293
  }
276
294
  /** Resolve the canonical findings file for a project directory. */
@@ -301,7 +319,7 @@ export function findProjectNameCaseInsensitive(phrenPath, name) {
301
319
  }
302
320
  catch (err) {
303
321
  if ((process.env.PHREN_DEBUG))
304
- process.stderr.write(`[phren] findProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
322
+ stderrLog(`findProjectNameCaseInsensitive: ${errorMessage(err)}`);
305
323
  }
306
324
  return null;
307
325
  }
@@ -318,7 +336,7 @@ export function findArchivedProjectNameCaseInsensitive(phrenPath, name) {
318
336
  }
319
337
  catch (err) {
320
338
  if ((process.env.PHREN_DEBUG))
321
- process.stderr.write(`[phren] findArchivedProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
339
+ stderrLog(`findArchivedProjectNameCaseInsensitive: ${errorMessage(err)}`);
322
340
  }
323
341
  return null;
324
342
  }
@@ -342,26 +360,26 @@ export function getProjectDirs(phrenPath, profile) {
342
360
  }
343
361
  if (profile) {
344
362
  if (!isValidProjectName(profile)) {
345
- console.error(`${PhrenError.VALIDATION_ERROR}: Invalid PHREN_PROFILE value: ${profile}`);
363
+ errorLog("getProjectDirs", `${PhrenError.VALIDATION_ERROR}: Invalid PHREN_PROFILE value: ${profile}`);
346
364
  return [];
347
365
  }
348
366
  const profilePath = path.join(phrenPath, "profiles", `${profile}.yaml`);
349
367
  if (!fs.existsSync(profilePath)) {
350
- console.error(`${PhrenError.FILE_NOT_FOUND}: Profile file not found: ${profilePath}`);
368
+ errorLog("getProjectDirs", `${PhrenError.FILE_NOT_FOUND}: Profile file not found: ${profilePath}`);
351
369
  return [];
352
370
  }
353
371
  try {
354
372
  const data = yaml.load(fs.readFileSync(profilePath, "utf-8"), { schema: yaml.CORE_SCHEMA });
355
373
  const projects = isRecord(data) ? data.projects : undefined;
356
374
  if (!Array.isArray(projects)) {
357
- console.error(`${PhrenError.MALFORMED_YAML}: Profile YAML missing valid "projects" array: ${profilePath}`);
375
+ errorLog("getProjectDirs", `${PhrenError.MALFORMED_YAML}: Profile YAML missing valid "projects" array: ${profilePath}`);
358
376
  return [];
359
377
  }
360
378
  const listed = projects
361
379
  .map((p) => {
362
380
  const name = String(p);
363
381
  if (!isValidProjectName(name)) {
364
- console.error(`${PhrenError.VALIDATION_ERROR}: Skipping invalid project name in profile: ${name}`);
382
+ errorLog("getProjectDirs", `${PhrenError.VALIDATION_ERROR}: Skipping invalid project name in profile: ${name}`);
365
383
  return null;
366
384
  }
367
385
  return safeProjectPath(phrenPath, name);
@@ -374,8 +392,8 @@ export function getProjectDirs(phrenPath, profile) {
374
392
  }
375
393
  catch (err) {
376
394
  if ((process.env.PHREN_DEBUG))
377
- process.stderr.write(`[phren] getProjectDirs yamlParse: ${errorMessage(err)}\n`);
378
- console.error(`${PhrenError.MALFORMED_YAML}: Malformed profile YAML: ${profilePath}`);
395
+ stderrLog(`getProjectDirs yamlParse: ${errorMessage(err)}`);
396
+ errorLog("getProjectDirs", `${PhrenError.MALFORMED_YAML}: Malformed profile YAML: ${profilePath}`);
379
397
  return [];
380
398
  }
381
399
  }
@@ -386,7 +404,7 @@ export function getProjectDirs(phrenPath, profile) {
386
404
  }
387
405
  catch (err) {
388
406
  if ((process.env.PHREN_DEBUG))
389
- process.stderr.write(`[phren] getProjectDirs: ${errorMessage(err)}\n`);
407
+ stderrLog(`getProjectDirs: ${errorMessage(err)}`);
390
408
  return [];
391
409
  }
392
410
  }
@@ -413,7 +431,7 @@ export function collectNativeMemoryFiles() {
413
431
  }
414
432
  catch (err) {
415
433
  if ((process.env.PHREN_DEBUG))
416
- process.stderr.write(`[phren] collectNativeMemoryFiles: ${errorMessage(err)}\n`);
434
+ stderrLog(`collectNativeMemoryFiles: ${errorMessage(err)}`);
417
435
  }
418
436
  return results;
419
437
  }