@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.
Files changed (59) hide show
  1. package/mcp/dist/cli/actions.js +3 -0
  2. package/mcp/dist/cli/config.js +3 -3
  3. package/mcp/dist/cli/govern.js +18 -8
  4. package/mcp/dist/cli/hooks-context.js +1 -1
  5. package/mcp/dist/cli/hooks-session.js +18 -62
  6. package/mcp/dist/cli/namespaces.js +1 -1
  7. package/mcp/dist/cli/search.js +5 -5
  8. package/mcp/dist/cli-hooks-prompt.js +7 -3
  9. package/mcp/dist/cli-hooks-session-handlers.js +3 -15
  10. package/mcp/dist/cli-hooks-stop.js +10 -48
  11. package/mcp/dist/content/archive.js +8 -20
  12. package/mcp/dist/content/learning.js +29 -8
  13. package/mcp/dist/data/access.js +13 -4
  14. package/mcp/dist/finding/lifecycle.js +9 -3
  15. package/mcp/dist/governance/audit.js +13 -5
  16. package/mcp/dist/governance/policy.js +13 -0
  17. package/mcp/dist/governance/rbac.js +1 -1
  18. package/mcp/dist/governance/scores.js +2 -1
  19. package/mcp/dist/hooks.js +52 -6
  20. package/mcp/dist/index.js +1 -1
  21. package/mcp/dist/init/init.js +66 -45
  22. package/mcp/dist/init/shared.js +1 -1
  23. package/mcp/dist/init-bootstrap.js +0 -47
  24. package/mcp/dist/init-fresh.js +13 -18
  25. package/mcp/dist/init-uninstall.js +22 -0
  26. package/mcp/dist/init-walkthrough.js +19 -24
  27. package/mcp/dist/link/doctor.js +9 -0
  28. package/mcp/dist/package-metadata.js +1 -1
  29. package/mcp/dist/phren-art.js +4 -120
  30. package/mcp/dist/proactivity.js +1 -1
  31. package/mcp/dist/project-topics.js +16 -46
  32. package/mcp/dist/provider-adapters.js +1 -1
  33. package/mcp/dist/runtime-profile.js +1 -1
  34. package/mcp/dist/shared/data-utils.js +25 -0
  35. package/mcp/dist/shared/fragment-graph.js +4 -18
  36. package/mcp/dist/shared/index.js +14 -10
  37. package/mcp/dist/shared/ollama.js +23 -5
  38. package/mcp/dist/shared/process.js +24 -0
  39. package/mcp/dist/shared/retrieval.js +7 -4
  40. package/mcp/dist/shared/search-fallback.js +1 -0
  41. package/mcp/dist/shared.js +2 -1
  42. package/mcp/dist/shell/render.js +1 -1
  43. package/mcp/dist/skill/registry.js +1 -1
  44. package/mcp/dist/skill/state.js +0 -3
  45. package/mcp/dist/task/github.js +1 -0
  46. package/mcp/dist/task/lifecycle.js +1 -6
  47. package/mcp/dist/tools/config.js +415 -400
  48. package/mcp/dist/tools/finding.js +390 -373
  49. package/mcp/dist/tools/ops.js +372 -365
  50. package/mcp/dist/tools/search.js +495 -487
  51. package/mcp/dist/tools/session.js +3 -2
  52. package/mcp/dist/tools/skills.js +9 -0
  53. package/mcp/dist/ui/page.js +1 -1
  54. package/mcp/dist/ui/server.js +645 -1040
  55. package/mcp/dist/utils.js +12 -8
  56. package/package.json +1 -1
  57. package/mcp/dist/init-dryrun.js +0 -55
  58. package/mcp/dist/init-migrate.js +0 -51
  59. package/mcp/dist/init-walkthrough-merge.js +0 -90
package/mcp/dist/utils.js CHANGED
@@ -169,7 +169,7 @@ export const STOP_WORDS = new Set([
169
169
  export function extractKeywordEntries(text) {
170
170
  const words = text
171
171
  .toLowerCase()
172
- .replace(/[^\w\s-]/g, " ")
172
+ .replace(/[^\p{L}\p{N}\s_-]/gu, " ")
173
173
  .split(/\s+/)
174
174
  .filter(w => w.length > 1 && !STOP_WORDS.has(w));
175
175
  // Build bigrams from adjacent non-stop-words
@@ -254,23 +254,27 @@ export function queueFilePath(phrenPath, project) {
254
254
  }
255
255
  return result;
256
256
  }
257
+ const MAX_FTS_QUERY_LENGTH = 500;
257
258
  // Sanitize user input before passing it to an FTS5 MATCH expression.
258
259
  // Strips FTS5-specific syntax that could cause injection or parse errors.
259
260
  export function sanitizeFts5Query(raw) {
260
261
  if (!raw)
261
262
  return "";
262
- if (raw.length > 500)
263
- raw = raw.slice(0, 500);
264
- // Whitelist approach: only allow alphanumeric, spaces, hyphens, apostrophes, double quotes, asterisks
265
- let q = raw.replace(/[^a-zA-Z0-9 \-"*]/g, " ");
266
- q = q.replace(/\s+/g, " ");
267
- q = q.trim();
263
+ if (raw.length > MAX_FTS_QUERY_LENGTH)
264
+ raw = raw.slice(0, MAX_FTS_QUERY_LENGTH);
265
+ // Whitelist approach: only allow alphanumeric, spaces, hyphens, apostrophes, asterisks
266
+ let q = raw.replace(/[^\p{L}\p{N} \-"*]/gu, " ");
267
+ // Strip all double quotes — buildFtsClauses wraps terms in quotes itself,
268
+ // so user-supplied quotes only risk producing unbalanced FTS5 syntax.
269
+ q = q.replace(/"/g, "");
268
270
  // Q83: see docs/decisions/Q83-fts5-asterisk-validation.md
269
271
  q = q.replace(/(?<!\w)\*/g, "");
270
272
  // Also strip a trailing asterisk that is preceded only by whitespace at word
271
273
  // end of the whole query (handles "foo *" → "foo").
272
274
  q = q.replace(/\s+\*$/g, "");
273
- return q.trim();
275
+ // Normalize spaces after all stripping to avoid double spaces from removed characters
276
+ q = q.replace(/\s+/g, " ").trim();
277
+ return q;
274
278
  }
275
279
  function parseSynonymsYaml(filePath) {
276
280
  if (!fs.existsSync(filePath))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,55 +0,0 @@
1
- /**
2
- * Dry-run output for init.
3
- */
4
- import * as path from "path";
5
- import { getMachineName } from "./machine-identity.js";
6
- import { getPendingBootstrapTarget } from "./init-detect.js";
7
- import { log } from "./init/shared.js";
8
- export function printDryRun(phrenPath, opts, params) {
9
- const { hasExistingInstall, mcpLabel, hooksLabel, hooksEnabled, storageChoice, storageRepoRoot } = params;
10
- const pendingBootstrap = getPendingBootstrapTarget(phrenPath, opts);
11
- log("\nInit dry run. No files will be written.\n");
12
- if (storageChoice) {
13
- log(`Storage location: ${storageChoice} (${phrenPath})`);
14
- if (storageChoice === "project" && storageRepoRoot) {
15
- log(` Would update ${path.join(storageRepoRoot, ".gitignore")} with .phren/`);
16
- log(` Would set PHREN_PATH in ${path.join(storageRepoRoot, ".env")}`);
17
- }
18
- }
19
- if (hasExistingInstall) {
20
- log(`phren install detected at ${phrenPath}`);
21
- log(`Would update configuration for the existing install:\n`);
22
- log(` MCP mode: ${mcpLabel}`);
23
- log(` Hooks mode: ${hooksLabel}`);
24
- log(` Reconfigure Claude Code MCP/hooks`);
25
- log(` Reconfigure VS Code, Cursor, Copilot CLI, and Codex MCP targets`);
26
- if (hooksEnabled) {
27
- log(` Reconfigure lifecycle hooks for detected tools`);
28
- }
29
- if (pendingBootstrap?.mode === "detected") {
30
- log(` Would offer to add current project directory (${pendingBootstrap.path})`);
31
- }
32
- if (opts.applyStarterUpdate) {
33
- log(` Apply starter template updates to global/CLAUDE.md and global skills`);
34
- }
35
- log(` Run post-init verification checks`);
36
- log(`\nDry run complete.\n`);
37
- return;
38
- }
39
- log(`No existing phren install found at ${phrenPath}`);
40
- log(`Would create a new phren install:\n`);
41
- log(` Copy starter files to ${phrenPath} (or create minimal structure)`);
42
- log(` Update machines.yaml for machine "${opts.machine || getMachineName()}"`);
43
- log(` Create/update config files`);
44
- log(` MCP mode: ${mcpLabel}`);
45
- log(` Hooks mode: ${hooksLabel}`);
46
- log(` Configure Claude Code plus detected MCP targets (VS Code/Cursor/Copilot/Codex)`);
47
- if (hooksEnabled) {
48
- log(` Configure lifecycle hooks for detected tools`);
49
- }
50
- if (pendingBootstrap?.mode === "detected") {
51
- log(` Would offer to add current project directory (${pendingBootstrap.path})`);
52
- }
53
- log(` Write install preferences and run post-init verification checks`);
54
- log(`\nDry run complete.\n`);
55
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Legacy migration helpers for init: directory rename, skill rename.
3
- */
4
- import * as fs from "fs";
5
- import * as path from "path";
6
- import { homePath } from "./shared.js";
7
- import { hasInstallMarkers } from "./init-detect.js";
8
- /**
9
- * Migrate the legacy hidden store directory into ~/.phren when upgrading
10
- * from the previous product name.
11
- * @returns true if migration occurred
12
- */
13
- export function migrateLegacyStore(phrenPath, dryRun) {
14
- const legacyPath = path.resolve(homePath(".cortex"));
15
- if (legacyPath === phrenPath || !fs.existsSync(legacyPath) || !hasInstallMarkers(legacyPath)) {
16
- return false;
17
- }
18
- if (!dryRun) {
19
- fs.renameSync(legacyPath, phrenPath);
20
- }
21
- console.log(`Migrated legacy store → ~/.phren`);
22
- return true;
23
- }
24
- /**
25
- * Rename stale legacy skill names left over from the rebrand.
26
- * Runs on every init so users who already migrated the directory still get the fix.
27
- */
28
- export function migrateLegacySkills(phrenPath) {
29
- const skillsMigrateDir = path.join(phrenPath, "global", "skills");
30
- if (!fs.existsSync(skillsMigrateDir))
31
- return;
32
- const legacySkillName = "cortex.md";
33
- const legacySkillPrefix = "cortex-";
34
- for (const entry of fs.readdirSync(skillsMigrateDir)) {
35
- if (!entry.endsWith(".md"))
36
- continue;
37
- if (entry === legacySkillName) {
38
- const dest = path.join(skillsMigrateDir, "phren.md");
39
- if (!fs.existsSync(dest)) {
40
- fs.renameSync(path.join(skillsMigrateDir, entry), dest);
41
- }
42
- }
43
- else if (entry.startsWith(legacySkillPrefix)) {
44
- const newName = `phren-${entry.slice(legacySkillPrefix.length)}`;
45
- const dest = path.join(skillsMigrateDir, newName);
46
- if (!fs.existsSync(dest)) {
47
- fs.renameSync(path.join(skillsMigrateDir, entry), dest);
48
- }
49
- }
50
- }
51
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Merge walkthrough answers back into InitOptions.
3
- */
4
- import { execFileSync } from "child_process";
5
- import { runWalkthrough } from "./init-walkthrough.js";
6
- import { resolveInitPhrenPath, hasInstallMarkers } from "./init-detect.js";
7
- import { log } from "./init/shared.js";
8
- /**
9
- * Run the interactive walkthrough for first-time installs, merging answers into opts.
10
- * Mutates opts in-place and returns the (possibly updated) phrenPath and hasExistingInstall.
11
- */
12
- export async function mergeWalkthroughAnswers(phrenPath, hasExisting, opts) {
13
- const answers = await runWalkthrough(phrenPath);
14
- opts._walkthroughStorageChoice = answers.storageChoice;
15
- opts._walkthroughStoragePath = answers.storagePath;
16
- opts._walkthroughStorageRepoRoot = answers.storageRepoRoot;
17
- const newPhrenPath = resolveInitPhrenPath(opts);
18
- const newHasExisting = hasInstallMarkers(newPhrenPath);
19
- opts.machine = opts.machine || answers.machine;
20
- opts.profile = opts.profile || answers.profile;
21
- opts.mcp = opts.mcp || answers.mcp;
22
- opts.hooks = opts.hooks || answers.hooks;
23
- opts.projectOwnershipDefault = opts.projectOwnershipDefault || answers.projectOwnershipDefault;
24
- opts.findingsProactivity = opts.findingsProactivity || answers.findingsProactivity;
25
- opts.taskProactivity = opts.taskProactivity || answers.taskProactivity;
26
- if (typeof opts.lowConfidenceThreshold !== "number")
27
- opts.lowConfidenceThreshold = answers.lowConfidenceThreshold;
28
- if (!Array.isArray(opts.riskySections))
29
- opts.riskySections = answers.riskySections;
30
- opts.taskMode = opts.taskMode || answers.taskMode;
31
- if (answers.cloneUrl) {
32
- opts._walkthroughCloneUrl = answers.cloneUrl;
33
- }
34
- if (answers.githubRepo) {
35
- opts._walkthroughGithub = { username: answers.githubUsername, repo: answers.githubRepo };
36
- }
37
- opts._walkthroughDomain = answers.domain;
38
- if (answers.inferredScaffold) {
39
- opts._walkthroughInferredScaffold = answers.inferredScaffold;
40
- }
41
- if (!answers.ollamaEnabled) {
42
- process.env._PHREN_WALKTHROUGH_OLLAMA_SKIP = "1";
43
- }
44
- else {
45
- opts._walkthroughSemanticSearch = true;
46
- }
47
- opts._walkthroughAutoCapture = answers.autoCaptureEnabled;
48
- if (answers.semanticDedupEnabled) {
49
- opts._walkthroughSemanticDedup = true;
50
- }
51
- if (answers.semanticConflictEnabled) {
52
- opts._walkthroughSemanticConflict = true;
53
- }
54
- if (answers.findingSensitivity && answers.findingSensitivity !== "balanced") {
55
- opts.findingSensitivity = answers.findingSensitivity;
56
- }
57
- opts._walkthroughBootstrapCurrentProject = answers.bootstrapCurrentProject;
58
- opts._walkthroughBootstrapOwnership = answers.bootstrapOwnership;
59
- return { phrenPath: newPhrenPath, hasExistingInstall: newHasExisting };
60
- }
61
- /**
62
- * Clone an existing phren from a remote URL.
63
- * @returns true if the clone succeeded (existing install), false otherwise
64
- */
65
- export function cloneExistingPhren(phrenPath, cloneUrl) {
66
- log(`\nCloning existing phren from ${cloneUrl}...`);
67
- try {
68
- execFileSync("git", ["clone", cloneUrl, phrenPath], {
69
- stdio: ["ignore", "pipe", "pipe"],
70
- timeout: 60_000,
71
- });
72
- log(` Cloned to ${phrenPath}`);
73
- return true;
74
- }
75
- catch (e) {
76
- log(` Clone failed: ${e instanceof Error ? e.message : String(e)}`);
77
- log("");
78
- log(" ┌──────────────────────────────────────────────────────────────────┐");
79
- log(" │ WARNING: Sync is NOT configured. Your phren data is local-only. │");
80
- log(" │ │");
81
- log(" │ To fix later: │");
82
- log(` │ cd ${phrenPath}`);
83
- log(" │ git remote add origin <YOUR_REPO_URL> │");
84
- log(" │ git push -u origin main │");
85
- log(" └──────────────────────────────────────────────────────────────────┘");
86
- log("");
87
- log(` Continuing with fresh local-only install.`);
88
- return false;
89
- }
90
- }