@phren/cli 0.0.32 → 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 (58) 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 +14 -6
  20. package/mcp/dist/index.js +1 -1
  21. package/mcp/dist/init/init.js +54 -42
  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/package-metadata.js +1 -1
  28. package/mcp/dist/phren-art.js +4 -120
  29. package/mcp/dist/proactivity.js +1 -1
  30. package/mcp/dist/project-topics.js +16 -46
  31. package/mcp/dist/provider-adapters.js +1 -1
  32. package/mcp/dist/runtime-profile.js +1 -1
  33. package/mcp/dist/shared/data-utils.js +25 -0
  34. package/mcp/dist/shared/fragment-graph.js +4 -18
  35. package/mcp/dist/shared/index.js +14 -10
  36. package/mcp/dist/shared/ollama.js +23 -5
  37. package/mcp/dist/shared/process.js +24 -0
  38. package/mcp/dist/shared/retrieval.js +7 -4
  39. package/mcp/dist/shared/search-fallback.js +1 -0
  40. package/mcp/dist/shared.js +2 -1
  41. package/mcp/dist/shell/render.js +1 -1
  42. package/mcp/dist/skill/registry.js +1 -1
  43. package/mcp/dist/skill/state.js +0 -3
  44. package/mcp/dist/task/github.js +1 -0
  45. package/mcp/dist/task/lifecycle.js +1 -6
  46. package/mcp/dist/tools/config.js +415 -400
  47. package/mcp/dist/tools/finding.js +390 -373
  48. package/mcp/dist/tools/ops.js +372 -365
  49. package/mcp/dist/tools/search.js +495 -487
  50. package/mcp/dist/tools/session.js +3 -2
  51. package/mcp/dist/tools/skills.js +9 -0
  52. package/mcp/dist/ui/page.js +1 -1
  53. package/mcp/dist/ui/server.js +645 -1040
  54. package/mcp/dist/utils.js +12 -8
  55. package/package.json +1 -1
  56. package/mcp/dist/init-dryrun.js +0 -55
  57. package/mcp/dist/init-migrate.js +0 -51
  58. package/mcp/dist/init-walkthrough-merge.js +0 -90
@@ -3,6 +3,11 @@ const DEFAULT_OLLAMA_URL = "http://localhost:11434";
3
3
  const DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
4
4
  const DEFAULT_EXTRACT_MODEL = "llama3.2";
5
5
  const MAX_EMBED_INPUT_CHARS = 6000;
6
+ const CLOUD_EMBEDDING_TIMEOUT_MS = 15_000;
7
+ const OLLAMA_HEALTH_TIMEOUT_MS = 2_000;
8
+ const OLLAMA_EMBEDDING_TIMEOUT_MS = 10_000;
9
+ const OLLAMA_GENERATE_TIMEOUT_MS = 60_000;
10
+ /** @internal Exported for tests. */
6
11
  export function prepareEmbeddingInput(text) {
7
12
  return text
8
13
  .replace(/<!--[\s\S]*?-->/g, " ")
@@ -44,7 +49,7 @@ async function embedTextCloud(input, baseUrl, model, apiKey) {
44
49
  headers["Authorization"] = `Bearer ${apiKey}`;
45
50
  try {
46
51
  const controller = new AbortController();
47
- const id = setTimeout(() => controller.abort(), 15000);
52
+ const id = setTimeout(() => controller.abort(), CLOUD_EMBEDDING_TIMEOUT_MS);
48
53
  const res = await fetch(`${baseUrl}/embeddings`, {
49
54
  method: "POST",
50
55
  headers,
@@ -85,7 +90,7 @@ export async function checkOllamaAvailable(url) {
85
90
  return false;
86
91
  try {
87
92
  const controller = new AbortController();
88
- const id = setTimeout(() => controller.abort(), 2000);
93
+ const id = setTimeout(() => controller.abort(), OLLAMA_HEALTH_TIMEOUT_MS);
89
94
  const res = await fetch(`${baseUrl}/api/tags`, { signal: controller.signal });
90
95
  clearTimeout(id);
91
96
  return res.ok;
@@ -104,7 +109,7 @@ export async function checkModelAvailable(model, url) {
104
109
  const modelName = model ?? getEmbeddingModel();
105
110
  try {
106
111
  const controller = new AbortController();
107
- const id = setTimeout(() => controller.abort(), 2000);
112
+ const id = setTimeout(() => controller.abort(), OLLAMA_HEALTH_TIMEOUT_MS);
108
113
  const res = await fetch(`${baseUrl}/api/tags`, { signal: controller.signal });
109
114
  clearTimeout(id);
110
115
  if (!res.ok)
@@ -131,7 +136,7 @@ export async function embedText(text, model, url) {
131
136
  return null;
132
137
  try {
133
138
  const controller = new AbortController();
134
- const id = setTimeout(() => controller.abort(), 10000);
139
+ const id = setTimeout(() => controller.abort(), OLLAMA_EMBEDDING_TIMEOUT_MS);
135
140
  const res = await fetch(`${baseUrl}/api/embed`, {
136
141
  method: "POST",
137
142
  headers: { "Content-Type": "application/json" },
@@ -158,7 +163,7 @@ export async function generateText(prompt, model, url) {
158
163
  const modelName = model ?? getExtractModel();
159
164
  try {
160
165
  const controller = new AbortController();
161
- const id = setTimeout(() => controller.abort(), 60000);
166
+ const id = setTimeout(() => controller.abort(), OLLAMA_GENERATE_TIMEOUT_MS);
162
167
  const res = await fetch(`${baseUrl}/api/generate`, {
163
168
  method: "POST",
164
169
  headers: { "Content-Type": "application/json" },
@@ -178,4 +183,17 @@ export async function generateText(prompt, model, url) {
178
183
  return null;
179
184
  }
180
185
  }
186
+ /**
187
+ * Probe Ollama availability and model readiness in one call.
188
+ * Returns a status enum so callers can branch on it without repeating the check logic.
189
+ */
190
+ export async function checkOllamaStatus() {
191
+ if (!getOllamaUrl())
192
+ return "disabled";
193
+ const ollamaUp = await checkOllamaAvailable();
194
+ if (!ollamaUp)
195
+ return "not_running";
196
+ const modelReady = await checkModelAvailable();
197
+ return modelReady ? "ready" : "no_model";
198
+ }
181
199
  export { cosineSimilarity } from "../embedding.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared process helpers for spawning detached child processes.
3
+ */
4
+ import { spawn } from "child_process";
5
+ import { resolveRuntimeProfile } from "../runtime-profile.js";
6
+ /**
7
+ * Spawn a detached child process with the standard phren environment.
8
+ * When logFd is provided, stdout/stderr are redirected to that fd.
9
+ * When omitted, all stdio is ignored.
10
+ * Returns the ChildProcess so callers can attach `.unref()` or `.on("exit", ...)`.
11
+ */
12
+ export function spawnDetachedChild(args, opts) {
13
+ return spawn(process.execPath, args, {
14
+ cwd: opts.cwd ?? process.cwd(),
15
+ detached: true,
16
+ stdio: opts.logFd !== undefined ? ["ignore", opts.logFd, opts.logFd] : "ignore",
17
+ env: {
18
+ ...process.env,
19
+ PHREN_PATH: opts.phrenPath,
20
+ PHREN_PROFILE: resolveRuntimeProfile(opts.phrenPath),
21
+ ...opts.extraEnv,
22
+ },
23
+ });
24
+ }
@@ -1,6 +1,6 @@
1
1
  // shared-retrieval.ts — shared retrieval core used by hooks and MCP search.
2
2
  import { getQualityMultiplier, entryScoreKey, } from "./governance.js";
3
- import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getEntityBoostDocs, decodeFiniteNumber, rowToDocWithRowid, buildIndex, } from "./index.js";
3
+ import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getFragmentBoostDocs, decodeFiniteNumber, rowToDocWithRowid, buildIndex, } from "./index.js";
4
4
  import { filterTrustedFindingsDetailed, } from "./content.js";
5
5
  import { parseCitationComment } from "../content/citation.js";
6
6
  import { getHighImpactFindings } from "../finding/impact.js";
@@ -183,6 +183,7 @@ const RRF_K = 60;
183
183
  * Documents appearing in multiple tiers get a higher combined score.
184
184
  * Formula: score(d) = Σ 1/(k + rank_i) for each tier i containing d, where k=60 (standard).
185
185
  */
186
+ /** @internal Exported for tests. */
186
187
  export function rrfMerge(tiers, k = RRF_K) {
187
188
  const scores = new Map();
188
189
  const docs = new Map();
@@ -267,6 +268,7 @@ function semanticFallbackDocs(db, prompt, project) {
267
268
  .map((x) => x.doc);
268
269
  return scored;
269
270
  }
271
+ /** @internal Exported for tests. */
270
272
  export function shouldRunVectorExpansion(rows, prompt, desiredResults = VECTOR_FALLBACK_SKIP_COUNT) {
271
273
  if (!rows || rows.length === 0)
272
274
  return true;
@@ -492,7 +494,7 @@ export async function searchKnowledgeRows(db, options) {
492
494
  * Parse PHREN_FEDERATION_PATHS env var and return valid, distinct paths.
493
495
  * Paths are colon-separated. The local phrenPath is excluded to avoid duplicate results.
494
496
  */
495
- export function parseFederationPaths(localPhrenPath) {
497
+ function parseFederationPaths(localPhrenPath) {
496
498
  const raw = process.env.PHREN_FEDERATION_PATHS ?? "";
497
499
  if (!raw.trim())
498
500
  return [];
@@ -624,7 +626,7 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
624
626
  if (canonicalRows)
625
627
  ranked = [...canonicalRows, ...ranked];
626
628
  }
627
- const entityBoost = query ? getEntityBoostDocs(db, query) : new Set();
629
+ const entityBoost = query ? getFragmentBoostDocs(db, query) : new Set();
628
630
  const entityBoostPaths = new Set();
629
631
  for (const doc of ranked) {
630
632
  // Use getDocSourceKey to build the full project/relFile key, matching what
@@ -735,7 +737,8 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
735
737
  }
736
738
  return ranked;
737
739
  }
738
- /** Mark snippet lines with stale citations (cited file missing or line content changed). */
740
+ /** Mark snippet lines with stale citations (cited file missing or line content changed).
741
+ * @internal Exported for tests. */
739
742
  export function markStaleCitations(snippet) {
740
743
  const lines = snippet.split("\n");
741
744
  const result = [];
@@ -17,6 +17,7 @@ const COSINE_WINDOW_COUNT = 4;
17
17
  function splitPathSegments(filePath) {
18
18
  return filePath.split(/[\\/]+/).filter(Boolean);
19
19
  }
20
+ /** @internal Exported for tests. */
20
21
  export function deriveVectorDocIdentity(phrenPath, fullPath) {
21
22
  const normalizedPhrenPath = phrenPath.replace(/[\\/]+/g, "/").replace(/\/+$/, "");
22
23
  const normalizedFullPath = fullPath.replace(/[\\/]+/g, "/");
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import { debugLog, runtimeFile } from "./phren-paths.js";
4
4
  import { errorMessage } from "./utils.js";
5
5
  import { withFileLock } from "./governance/locks.js";
6
+ const MAX_LOG_LINES = 1000;
6
7
  export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
7
8
  export { EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, PhrenError, phrenOk, phrenErr, forwardErr, parsePhrenErrorCode, isRecord, withDefaults, FINDING_TYPES, FINDING_TAGS, KNOWN_OBSERVATION_TAGS, DOC_TYPES, capCache, RESERVED_PROJECT_DIR_NAMES, } from "./phren-core.js";
8
9
  export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, findArchivedProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
@@ -39,7 +40,7 @@ export function appendAuditLog(phrenPath, event, details) {
39
40
  if (stat.size > 1_000_000) {
40
41
  const content = fs.readFileSync(logPath, "utf8");
41
42
  const lines = content.split("\n");
42
- fs.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n");
43
+ fs.writeFileSync(logPath, lines.slice(-MAX_LOG_LINES).join("\n") + "\n");
43
44
  }
44
45
  });
45
46
  }
@@ -37,7 +37,7 @@ export function separator(width = 50) {
37
37
  export function stripAnsi(s) {
38
38
  return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
39
39
  }
40
- export function visibleWidth(s) {
40
+ function visibleWidth(s) {
41
41
  return stripAnsi(s).length;
42
42
  }
43
43
  export function padToWidth(s, width) {
@@ -223,7 +223,7 @@ export function getAllSkills(phrenPath, profile) {
223
223
  }
224
224
  return all;
225
225
  }
226
- export function getLocalSkills(phrenPath, scope) {
226
+ function getLocalSkills(phrenPath, scope) {
227
227
  if (scope.toLowerCase() === "global")
228
228
  return getGlobalSkills(phrenPath);
229
229
  return getProjectLocalSkills(phrenPath, scope);
@@ -23,6 +23,3 @@ export function setSkillEnabled(phrenPath, scope, name, enabled) {
23
23
  disabledSkills: Object.keys(disabled).length ? disabled : undefined,
24
24
  });
25
25
  }
26
- export function getSkillStateKey(scope, name) {
27
- return skillStateKey(scope, name);
28
- }
@@ -16,6 +16,7 @@ export function parseGithubIssueUrl(url) {
16
16
  url: match[0],
17
17
  };
18
18
  }
19
+ /** @internal Exported for tests. */
19
20
  export function extractGithubRepoFromText(content) {
20
21
  const match = content.match(GITHUB_REPO_URL);
21
22
  return match?.[1];
@@ -80,7 +80,7 @@ function clearTaskSessionState(phrenPath, sessionId) {
80
80
  debugLog(`task lifecycle clear session ${sessionId}: ${errorMessage(err)}`);
81
81
  }
82
82
  }
83
- export function getTaskMode(phrenPath) {
83
+ function getTaskMode(phrenPath) {
84
84
  return getWorkflowPolicy(phrenPath).taskMode;
85
85
  }
86
86
  function isActionablePrompt(prompt, intent) {
@@ -330,11 +330,6 @@ export function finalizeTaskSession(args) {
330
330
  return;
331
331
  }
332
332
  }
333
- export function clearTaskSession(phrenPath, sessionId) {
334
- if (!sessionId)
335
- return;
336
- clearTaskSessionState(phrenPath, sessionId);
337
- }
338
333
  /**
339
334
  * Return the active TaskItem tracked for a session+project, if any.
340
335
  * Used by mcp-finding.ts to link findings to active tasks.