@phren/cli 0.0.11 → 0.0.13

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 (76) hide show
  1. package/README.md +9 -9
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +54 -67
  7. package/mcp/dist/cli-config.js +4 -5
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-graph.js +17 -3
  10. package/mcp/dist/cli-hooks-output.js +1 -1
  11. package/mcp/dist/cli-hooks-session.js +1 -1
  12. package/mcp/dist/cli-hooks.js +5 -3
  13. package/mcp/dist/cli.js +1 -1
  14. package/mcp/dist/content-archive.js +21 -12
  15. package/mcp/dist/content-citation.js +13 -2
  16. package/mcp/dist/content-learning.js +6 -4
  17. package/mcp/dist/content-metadata.js +10 -0
  18. package/mcp/dist/core-finding.js +1 -1
  19. package/mcp/dist/data-access.js +10 -31
  20. package/mcp/dist/data-tasks.js +5 -26
  21. package/mcp/dist/embedding.js +0 -1
  22. package/mcp/dist/entrypoint.js +4 -0
  23. package/mcp/dist/finding-impact.js +1 -32
  24. package/mcp/dist/finding-journal.js +1 -1
  25. package/mcp/dist/finding-lifecycle.js +2 -7
  26. package/mcp/dist/governance-locks.js +6 -0
  27. package/mcp/dist/governance-policy.js +1 -7
  28. package/mcp/dist/governance-scores.js +1 -7
  29. package/mcp/dist/hooks.js +23 -0
  30. package/mcp/dist/init-config.js +1 -1
  31. package/mcp/dist/init-preferences.js +1 -1
  32. package/mcp/dist/init-setup.js +1 -50
  33. package/mcp/dist/init-shared.js +53 -1
  34. package/mcp/dist/init.js +21 -6
  35. package/mcp/dist/link-context.js +1 -1
  36. package/mcp/dist/link-doctor.js +11 -54
  37. package/mcp/dist/link.js +4 -53
  38. package/mcp/dist/mcp-extract-facts.js +11 -6
  39. package/mcp/dist/mcp-finding.js +10 -14
  40. package/mcp/dist/mcp-graph.js +6 -6
  41. package/mcp/dist/mcp-hooks.js +1 -1
  42. package/mcp/dist/mcp-search.js +3 -8
  43. package/mcp/dist/mcp-session.js +12 -2
  44. package/mcp/dist/memory-ui-assets.js +1 -36
  45. package/mcp/dist/memory-ui-graph.js +152 -50
  46. package/mcp/dist/memory-ui-page.js +7 -5
  47. package/mcp/dist/memory-ui-scripts.js +42 -36
  48. package/mcp/dist/phren-core.js +2 -0
  49. package/mcp/dist/phren-paths.js +1 -2
  50. package/mcp/dist/proactivity.js +5 -5
  51. package/mcp/dist/project-config.js +1 -1
  52. package/mcp/dist/provider-adapters.js +1 -1
  53. package/mcp/dist/query-correlation.js +22 -19
  54. package/mcp/dist/session-checkpoints.js +14 -14
  55. package/mcp/dist/shared-data-utils.js +28 -0
  56. package/mcp/dist/shared-fragment-graph.js +11 -11
  57. package/mcp/dist/shared-governance.js +1 -1
  58. package/mcp/dist/shared-retrieval.js +2 -10
  59. package/mcp/dist/shared-search-fallback.js +2 -12
  60. package/mcp/dist/shared.js +2 -3
  61. package/mcp/dist/shell-entry.js +1 -1
  62. package/mcp/dist/shell-input.js +62 -52
  63. package/mcp/dist/shell-palette.js +6 -1
  64. package/mcp/dist/shell-render.js +9 -5
  65. package/mcp/dist/shell-state-store.js +1 -4
  66. package/mcp/dist/shell-view.js +4 -4
  67. package/mcp/dist/shell.js +4 -54
  68. package/mcp/dist/status.js +2 -8
  69. package/mcp/dist/utils.js +1 -1
  70. package/package.json +1 -2
  71. package/skills/docs.md +11 -11
  72. package/starter/README.md +1 -1
  73. package/starter/global/CLAUDE.md +2 -2
  74. package/starter/global/skills/audit.md +10 -10
  75. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  76. package/mcp/dist/impact-scoring.js +0 -22
@@ -110,21 +110,21 @@ export function clearTaskCheckpoint(phrenPath, args) {
110
110
  debugLog(`checkpoint clear ${filePath}: ${errorMessage(err)}`);
111
111
  }
112
112
  }
113
- const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
114
- for (const checkpoint of allProjectCheckpoints) {
115
- const idMatch = ids.size > 0 && ids.has(checkpoint.taskId);
116
- const lineMatch = args.taskLine && checkpoint.taskLine === args.taskLine;
117
- if (!idMatch && !lineMatch)
118
- continue;
119
- const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
120
- try {
121
- if (fs.existsSync(filePath)) {
122
- fs.unlinkSync(filePath);
123
- removed++;
113
+ if (args.taskLine) {
114
+ const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
115
+ for (const checkpoint of allProjectCheckpoints) {
116
+ if (checkpoint.taskLine !== args.taskLine)
117
+ continue;
118
+ const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
119
+ try {
120
+ if (fs.existsSync(filePath)) {
121
+ fs.unlinkSync(filePath);
122
+ removed++;
123
+ }
124
+ }
125
+ catch (err) {
126
+ debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
124
127
  }
125
- }
126
- catch (err) {
127
- debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
128
128
  }
129
129
  }
130
130
  return removed;
@@ -0,0 +1,28 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { phrenErr, PhrenError, phrenOk, } from "./shared.js";
4
+ import { withFileLock as withFileLockRaw } from "./shared-governance.js";
5
+ import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
6
+ export function withSafeLock(filePath, fn) {
7
+ try {
8
+ return withFileLockRaw(filePath, fn);
9
+ }
10
+ catch (err) {
11
+ const msg = errorMessage(err);
12
+ if (msg.includes("could not acquire lock")) {
13
+ return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
14
+ }
15
+ throw err;
16
+ }
17
+ }
18
+ export function ensureProject(phrenPath, project) {
19
+ if (!isValidProjectName(project))
20
+ return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
21
+ const dir = safeProjectPath(phrenPath, project);
22
+ if (!dir)
23
+ return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
24
+ if (!fs.existsSync(dir)) {
25
+ return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
26
+ }
27
+ return phrenOk(dir);
28
+ }
@@ -22,7 +22,7 @@ export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
22
22
  * found" -> suggest adding it).
23
23
  */
24
24
  export function logFragmentMiss(phrenPath, name, context, project) {
25
- if (!process.env.PHREN_DEBUG && !(process.env.PHREN_DEBUG))
25
+ if (!process.env.PHREN_DEBUG)
26
26
  return;
27
27
  if (!name || name.length <= 2)
28
28
  return;
@@ -116,7 +116,7 @@ function getOrCreateFragment(db, name, type) {
116
116
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [name, type, new Date().toISOString().slice(0, 10)]);
117
117
  }
118
118
  catch (err) {
119
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
119
+ if (process.env.PHREN_DEBUG)
120
120
  process.stderr.write(`[phren] fragmentInsert: ${errorMessage(err)}\n`);
121
121
  }
122
122
  const result = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [name, type]);
@@ -139,7 +139,7 @@ export function ensureGlobalEntitiesTable(db) {
139
139
  )`);
140
140
  }
141
141
  catch (err) {
142
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
142
+ if (process.env.PHREN_DEBUG)
143
143
  process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${errorMessage(err)}\n`);
144
144
  }
145
145
  }
@@ -186,7 +186,7 @@ export function beginUserFragmentBuildCache(phrenPath, projects) {
186
186
  _buildUserFragmentCache.set(cacheKey, loaded.fragments);
187
187
  }
188
188
  catch (err) {
189
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
189
+ if (process.env.PHREN_DEBUG)
190
190
  process.stderr.write(`[phren] beginUserFragmentBuildCache: ${errorMessage(err)}\n`);
191
191
  _buildUserFragmentCache.set(cacheKey, []);
192
192
  }
@@ -225,7 +225,7 @@ function parseUserDefinedFragments(phrenPath, project) {
225
225
  }
226
226
  }
227
227
  catch (err) {
228
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
228
+ if (process.env.PHREN_DEBUG)
229
229
  process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${errorMessage(err)}\n`);
230
230
  }
231
231
  }
@@ -236,7 +236,7 @@ function parseUserDefinedFragments(phrenPath, project) {
236
236
  return loaded.fragments;
237
237
  }
238
238
  catch (err) {
239
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
239
+ if (process.env.PHREN_DEBUG)
240
240
  process.stderr.write(`[phren] parseUserDefinedFragments: ${errorMessage(err)}\n`);
241
241
  return [];
242
242
  }
@@ -353,7 +353,7 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
353
353
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [docFragmentId, fragmentId, "mentions", sourceDoc]);
354
354
  }
355
355
  catch (err) {
356
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
356
+ if (process.env.PHREN_DEBUG)
357
357
  process.stderr.write(`[phren] fragmentLinksInsert: ${errorMessage(err)}\n`);
358
358
  }
359
359
  // Write to global_entities for cross-project queries
@@ -362,7 +362,7 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
362
362
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, project, sourceDoc]);
363
363
  }
364
364
  catch (err) {
365
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
365
+ if (process.env.PHREN_DEBUG)
366
366
  process.stderr.write(`[phren] globalFragmentsInsert: ${errorMessage(err)}\n`);
367
367
  }
368
368
  }
@@ -391,7 +391,7 @@ export function queryFragmentLinks(db, name) {
391
391
  }
392
392
  }
393
393
  catch (err) {
394
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
394
+ if (process.env.PHREN_DEBUG)
395
395
  process.stderr.write(`[phren] queryFragmentLinks: ${errorMessage(err)}\n`);
396
396
  }
397
397
  return { related };
@@ -427,7 +427,7 @@ export function queryCrossProjectFragments(db, fragmentName, excludeProject) {
427
427
  }
428
428
  }
429
429
  catch (err) {
430
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
430
+ if (process.env.PHREN_DEBUG)
431
431
  process.stderr.write(`[phren] queryCrossProjectFragments: ${errorMessage(err)}\n`);
432
432
  }
433
433
  return results;
@@ -448,7 +448,7 @@ export function getFragmentBoostDocs(db, query) {
448
448
  return boostDocs;
449
449
  }
450
450
  catch (err) {
451
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
451
+ if (process.env.PHREN_DEBUG)
452
452
  process.stderr.write(`[phren] getFragmentBoostDocs: ${errorMessage(err)}\n`);
453
453
  return new Set();
454
454
  }
@@ -1,4 +1,4 @@
1
1
  export * from './governance-policy.js';
2
2
  export * from './governance-scores.js';
3
3
  export * from './governance-audit.js';
4
- export { withFileLock } from './governance-locks.js';
4
+ export { withFileLock, isFiniteNumber, hasValidSchemaVersion } from './governance-locks.js';
@@ -8,7 +8,7 @@ import { buildFtsQueryVariants, buildRelaxedFtsQuery, isFeatureEnabled, STOP_WOR
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
10
  import { getProjectGlobBoost } from "./cli-hooks-globs.js";
11
- import { vectorFallback } from "./shared-search-fallback.js";
11
+ import { vectorFallback, deterministicSeed } from "./shared-search-fallback.js";
12
12
  import { getOllamaUrl, getCloudEmbeddingUrl } from "./shared-ollama.js";
13
13
  import { keywordFallbackSearch } from "./core-search.js";
14
14
  import { debugLog } from "./shared.js";
@@ -161,14 +161,6 @@ function docOverlapScore(queryTokens, doc) {
161
161
  const corpus = `${doc.project} ${doc.filename} ${doc.type} ${doc.path}\n${doc.content.slice(0, 5000)}`;
162
162
  return overlapScore(queryTokens, corpus);
163
163
  }
164
- function semanticFallbackSeed(text) {
165
- let hash = 2166136261;
166
- for (let i = 0; i < text.length; i++) {
167
- hash ^= text.charCodeAt(i);
168
- hash = Math.imul(hash, 16777619);
169
- }
170
- return hash >>> 0;
171
- }
172
164
  function loadSemanticFallbackWindow(db, startRowid, limit, project, wrapBefore) {
173
165
  const where = [
174
166
  project ? "project = ?" : "",
@@ -250,7 +242,7 @@ function semanticFallbackDocs(db, prompt, project) {
250
242
  const windowCount = Math.min(SEMANTIC_FALLBACK_WINDOW_COUNT, cappedLimit);
251
243
  const perWindow = Math.max(1, Math.ceil(cappedLimit / windowCount));
252
244
  const stride = Math.max(1, Math.floor(span / windowCount));
253
- const seed = semanticFallbackSeed(`${project ?? "*"}\n${terms.join(" ")}`);
245
+ const seed = deterministicSeed(`${project ?? "*"}\n${terms.join(" ")}`);
254
246
  for (let i = 0; i < windowCount && docs.length < cappedLimit; i++) {
255
247
  const offset = (seed + i * stride) % span;
256
248
  const startRowid = minRowid + offset;
@@ -72,7 +72,7 @@ function cachedTokenize(text) {
72
72
  tokenCache.set(key, tokens);
73
73
  return tokens;
74
74
  }
75
- function deterministicSeed(text) {
75
+ export function deterministicSeed(text) {
76
76
  let hash = 2166136261;
77
77
  for (let i = 0; i < text.length; i++) {
78
78
  hash ^= text.charCodeAt(i);
@@ -153,18 +153,8 @@ function tfidfCosine(docs, query, corpusN) {
153
153
  return termTf * idf;
154
154
  });
155
155
  }
156
- function cosine(a, b) {
157
- let dot = 0, normA = 0, normB = 0;
158
- for (let i = 0; i < a.length; i++) {
159
- dot += a[i] * b[i];
160
- normA += a[i] * a[i];
161
- normB += b[i] * b[i];
162
- }
163
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
164
- return denom === 0 ? 0 : dot / denom;
165
- }
166
156
  const queryVec = buildVector(queryTokens);
167
- return docTokenLists.map(docTokens => cosine(queryVec, buildVector(docTokens)));
157
+ return docTokenLists.map(docTokens => cosineSimilarity(queryVec, buildVector(docTokens)));
168
158
  }
169
159
  /**
170
160
  * Cosine fallback search: when FTS5 returns fewer than COSINE_FALLBACK_THRESHOLD results,
@@ -3,10 +3,9 @@ import * as path from "path";
3
3
  import { debugLog, runtimeFile } from "./phren-paths.js";
4
4
  import { errorMessage } from "./utils.js";
5
5
  export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
6
- 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, } from "./phren-core.js";
6
+ 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";
7
7
  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, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
8
8
  export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, hasExplicitFindingSignal, hasExplicitTaskSignal, hasExecutionIntent, hasDiscoveryIntent, shouldAutoCaptureFindingsForLevel, shouldAutoCaptureTaskForLevel, } from "./proactivity.js";
9
- const RESERVED_PROJECT_DIR_NAMES = new Set(["profiles", "templates", "global"]);
10
9
  const MEMORY_SCOPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
11
10
  export function normalizeMemoryScope(scope) {
12
11
  if (typeof scope !== "string")
@@ -81,7 +80,7 @@ export function appendAuditLog(phrenPath, event, details) {
81
80
  if (stat.size > 1_000_000) {
82
81
  const content = fs.readFileSync(logPath, "utf8");
83
82
  const lines = content.split("\n");
84
- fs.writeFileSync(logPath, lines.slice(-500).join("\n"));
83
+ fs.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n");
85
84
  }
86
85
  }
87
86
  else {
@@ -147,7 +147,7 @@ export async function startShell(phrenPath, profile) {
147
147
  const shell = new PhrenShell(phrenPath, profile);
148
148
  if (!process.stdin.isTTY) {
149
149
  const { createInterface } = await import("readline");
150
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
150
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY ?? false });
151
151
  const repaint = async () => { clearScreen(); process.stdout.write(await shell.render()); rl.setPrompt(`\n${style.boldCyan(":phren>")} `); rl.prompt(); };
152
152
  const stopPoll = startLiveStatePoller({ phrenPath, shell, repaint });
153
153
  await repaint();
@@ -740,6 +740,67 @@ function showCursorPosition(host) {
740
740
  const short = label.length > 50 ? label.slice(0, 48) + "…" : label;
741
741
  host.setMessage(` ${style.dim(`${cursor + 1} / ${count}`)}${short ? ` ${style.dimItalic(short)}` : ""}`);
742
742
  }
743
+ // ── View shortcut keys (shared between handleInput text mode and handleNavigateKey) ─
744
+ /**
745
+ * Handle p/b/l/m/s/k/h shortcut keys that switch the active view.
746
+ * Returns true if the key was handled.
747
+ */
748
+ export function applyViewShortcut(host, key) {
749
+ if (key === "p") {
750
+ host.setView("Projects");
751
+ host.setMessage(` ${TAB_ICONS.Projects} Projects`);
752
+ return true;
753
+ }
754
+ if (key === "b") {
755
+ if (!host.state.project) {
756
+ host.setMessage(style.dim(" Select a project first (↵)"));
757
+ return true;
758
+ }
759
+ host.setView("Tasks");
760
+ host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
761
+ return true;
762
+ }
763
+ if (key === "l") {
764
+ if (!host.state.project) {
765
+ host.setMessage(style.dim(" Select a project first (↵)"));
766
+ return true;
767
+ }
768
+ host.setView("Findings");
769
+ host.setMessage(` ${TAB_ICONS.Findings} Findings`);
770
+ return true;
771
+ }
772
+ if (key === "m") {
773
+ if (!host.state.project) {
774
+ host.setMessage(style.dim(" Select a project first (↵)"));
775
+ return true;
776
+ }
777
+ host.setView("Review Queue");
778
+ host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
779
+ return true;
780
+ }
781
+ if (key === "s") {
782
+ if (!host.state.project) {
783
+ host.setMessage(style.dim(" Select a project first (↵)"));
784
+ return true;
785
+ }
786
+ host.setView("Skills");
787
+ host.setMessage(` ${TAB_ICONS.Skills} Skills`);
788
+ return true;
789
+ }
790
+ if (key === "k") {
791
+ host.setView("Hooks");
792
+ host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
793
+ return true;
794
+ }
795
+ if (key === "h") {
796
+ host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
797
+ host.healthCache = undefined;
798
+ host.setView("Health");
799
+ host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
800
+ return true;
801
+ }
802
+ return false;
803
+ }
743
804
  // ── Navigate-mode key handler ─────────────────────────────────────────────────
744
805
  export async function handleNavigateKey(host, key) {
745
806
  if (key === "\x1b[A") {
@@ -836,59 +897,8 @@ export async function handleNavigateKey(host, key) {
836
897
  }
837
898
  return true;
838
899
  }
839
- if (key === "p") {
840
- host.setView("Projects");
841
- host.setMessage(` ${TAB_ICONS.Projects} Projects`);
842
- return true;
843
- }
844
- if (key === "b") {
845
- if (!host.state.project) {
846
- host.setMessage(style.dim(" Select a project first (↵)"));
847
- return true;
848
- }
849
- host.setView("Tasks");
850
- host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
900
+ if (applyViewShortcut(host, key))
851
901
  return true;
852
- }
853
- if (key === "l") {
854
- if (!host.state.project) {
855
- host.setMessage(style.dim(" Select a project first (↵)"));
856
- return true;
857
- }
858
- host.setView("Findings");
859
- host.setMessage(` ${TAB_ICONS.Findings} Fragments`);
860
- return true;
861
- }
862
- if (key === "m") {
863
- if (!host.state.project) {
864
- host.setMessage(style.dim(" Select a project first (↵)"));
865
- return true;
866
- }
867
- host.setView("Review Queue");
868
- host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
869
- return true;
870
- }
871
- if (key === "s") {
872
- if (!host.state.project) {
873
- host.setMessage(style.dim(" Select a project first (↵)"));
874
- return true;
875
- }
876
- host.setView("Skills");
877
- host.setMessage(` ${TAB_ICONS.Skills} Skills`);
878
- return true;
879
- }
880
- if (key === "k") {
881
- host.setView("Hooks");
882
- host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
883
- return true;
884
- }
885
- if (key === "h") {
886
- host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
887
- host.healthCache = undefined;
888
- host.setView("Health");
889
- host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
890
- return true;
891
- }
892
902
  if (key === "i" && host.state.view === "Projects") {
893
903
  const next = host.state.introMode === "always" ? "once-per-version" : host.state.introMode === "off" ? "always" : "off";
894
904
  host.state.introMode = next;
@@ -7,7 +7,12 @@ import { EXEC_TIMEOUT_MS, } from "./shared.js";
7
7
  export function resultMsg(r) {
8
8
  if (!r.ok)
9
9
  return r.error;
10
- return typeof r.data === "string" ? r.data : JSON.stringify(r.data);
10
+ if (typeof r.data === "string")
11
+ return r.data;
12
+ if (r.data && typeof r.data === "object" && "message" in r.data && typeof r.data.message === "string") {
13
+ return r.data.message;
14
+ }
15
+ return JSON.stringify(r.data);
11
16
  }
12
17
  export function editDistance(a, b) {
13
18
  const m = a.length;
@@ -1,6 +1,12 @@
1
1
  // ── ANSI utilities ──────────────────────────────────────────────────────────
2
2
  const ESC = "\x1b[";
3
3
  export const RESET = `${ESC}0m`;
4
+ export const BOLD = `${ESC}1m`;
5
+ export const DIM = `${ESC}2m`;
6
+ export const GREEN = `${ESC}32m`;
7
+ export const YELLOW = `${ESC}33m`;
8
+ export const RED = `${ESC}31m`;
9
+ export const CYAN = `${ESC}36m`;
4
10
  export const style = {
5
11
  bold: (s) => `${ESC}1m${s}${RESET}`,
6
12
  dim: (s) => `${ESC}2m${s}${RESET}`,
@@ -144,9 +150,7 @@ const PHREN_LOGO = [
144
150
  "██║ ██║ ██║██║ ██║███████╗██║ ╚████║",
145
151
  "╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝",
146
152
  ];
147
- // Compact phren character for startup (uses PHREN_ART from phren-art.ts via import)
148
153
  import { PHREN_ART as PHREN_STARTUP_ART } from "./phren-art.js";
149
- const PHREN_STARTUP = PHREN_STARTUP_ART;
150
154
  // ── Line-based viewport: edge-triggered scroll (stable, no jumpiness) ─────────
151
155
  export function lineViewport(allLines, cursorFirstLine, cursorLastLine, height, prevStart) {
152
156
  if (allLines.length === 0 || height <= 0)
@@ -181,7 +185,7 @@ export function shellHelpText() {
181
185
  hdr("View-specific keys"),
182
186
  ` ${style.bold("Projects")} ${k("↵")} ${d("open project tasks")} ${k("i")} ${d("cycle intro mode")}`,
183
187
  ` ${style.bold("Tasks")} ${k("a")} ${d("add task")} ${k("d")} ${d("toggle active/queue")} ${k("↵")} ${d("mark complete")}`,
184
- ` ${style.bold("Fragments")} ${k("a")} ${d("tell phren")} ${k("d")} ${d("delete selected")}`,
188
+ ` ${style.bold("Findings")} ${k("a")} ${d("tell phren")} ${k("d")} ${d("delete selected")}`,
185
189
  ` ${style.bold("Review Queue")} ${k("↵")} ${d("inspect selected item")} ${d("(read-only)")}`,
186
190
  ` ${style.bold("Skills")} ${k("t")} ${d("toggle enabled")} ${k("d")} ${d("remove")}`,
187
191
  "",
@@ -221,7 +225,7 @@ export function shellStartupFrames(version) {
221
225
  const versionBadge = badge(`v${version}`, style.boldBlue);
222
226
  if (cols >= 72) {
223
227
  // Side-by-side: phren character on left, logo text on right
224
- const phrenLines = PHREN_STARTUP;
228
+ const phrenLines = PHREN_STARTUP_ART;
225
229
  const logoLines = PHREN_LOGO.map(line => gradient(line));
226
230
  const infoLine = `${gradient("◆")} ${style.bold("phren")} ${versionBadge} ${tagline}`;
227
231
  // Logo is 6 lines, pad to align vertically with character center
@@ -252,7 +256,7 @@ export function shellStartupFrames(version) {
252
256
  ];
253
257
  }
254
258
  // Narrow terminal: progressive text reveal with gradient
255
- const stages = ["c", "cor", "phren"];
259
+ const stages = ["p", "phr", "phren"];
256
260
  const spinners = ["◜", "◠", "◝"];
257
261
  return stages.map((stage, i) => [
258
262
  "",
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { phrenErr, PhrenError, phrenOk, shellStateFile } from "./shared.js";
4
- import { getRuntimeHealth, withFileLock as withFileLockRaw } from "./shared-governance.js";
4
+ import { withFileLock as withFileLockRaw } from "./shared-governance.js";
5
5
  import { errorMessage } from "./utils.js";
6
6
  function withSafeLock(filePath, fn) {
7
7
  try {
@@ -76,6 +76,3 @@ export function resetShellState(phrenPath) {
76
76
  return phrenOk("Shell state reset.");
77
77
  });
78
78
  }
79
- export function readRuntimeHealth(phrenPath) {
80
- return getRuntimeHealth(phrenPath);
81
- }
@@ -67,7 +67,7 @@ function renderBottomBar(state, navMode, inputCtx, inputBuf) {
67
67
  Tasks: [`${k("a")} ${d("add")}`, `${k("↵")} ${d("mark done")}`, `${k("d")} ${d("toggle active")}`],
68
68
  Findings: [`${k("a")} ${d("add")}`, `${k("d")} ${d("remove")}`],
69
69
  "Review Queue": [`${k("↵")} ${d("inspect")}`],
70
- Skills: [`${k("t")} ${d("toggle")}`, `${k("d")} ${d("remove")}`],
70
+ Skills: [`${k("a")} ${d("add")}`, `${k("t")} ${d("toggle")}`, `${k("d")} ${d("remove")}`],
71
71
  Hooks: [`${k("a")} ${d("enable")}`, `${k("d")} ${d("disable")}`],
72
72
  Health: [`${k("↑↓")} ${d("scroll")}`, `${k("esc")} ${d("back")}`],
73
73
  };
@@ -136,9 +136,9 @@ function renderProjectsDashboard(ctx, entries, height) {
136
136
  const findingsPreview = scoped
137
137
  .filter((entry) => entry.findingCount > 0)
138
138
  .slice(0, 3)
139
- .map((entry) => `${style.bold(entry.name)} ${style.dim(`${entry.findingCount} fragments`)}`);
139
+ .map((entry) => `${style.bold(entry.name)} ${style.dim(`${entry.findingCount} findings`)}`);
140
140
  const lines = [
141
- ` ${badge(ctx.profile || "default", style.boldBlue)} ${style.bold(String(scoped.length))} projects ${style.dim("·")} ${style.boldGreen(String(totals.active))} active ${style.dim("·")} ${style.boldYellow(String(totals.queue))} queued ${style.dim("·")} ${style.boldCyan(String(totals.findings))} fragments ${style.dim("·")} ${style.boldMagenta(String(totals.review))} review`,
141
+ ` ${badge(ctx.profile || "default", style.boldBlue)} ${style.bold(String(scoped.length))} projects ${style.dim("·")} ${style.boldGreen(String(totals.active))} active ${style.dim("·")} ${style.boldYellow(String(totals.queue))} queued ${style.dim("·")} ${style.boldCyan(String(totals.findings))} findings ${style.dim("·")} ${style.boldMagenta(String(totals.review))} review`,
142
142
  ctx.state.project
143
143
  ? ` ${style.green("●")} active context ${style.boldCyan(ctx.state.project)} ${style.dim("· ↵ opens selected project tasks")}`
144
144
  : ` ${style.dim("No project selected yet")} ${style.dim("· ↵ sets context and opens tasks")}`,
@@ -517,7 +517,7 @@ function renderSkillsView(ctx, cursor, height) {
517
517
  }
518
518
  const LIFECYCLE_HOOKS = [
519
519
  { event: "UserPromptSubmit", description: "inject context before each prompt" },
520
- { event: "Stop", description: "phren saves fragments after each response" },
520
+ { event: "Stop", description: "phren saves findings after each response" },
521
521
  { event: "SessionStart", description: "git pull at session start" },
522
522
  ];
523
523
  export function getHookEntries(phrenPath, project) {
package/mcp/dist/shell.js CHANGED
@@ -2,11 +2,11 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { addTask, addFinding, loadShellState, saveShellState, } from "./data-access.js";
4
4
  import { style } from "./shell-render.js";
5
- import { MAX_UNDO_STACK, TAB_ICONS, } from "./shell-types.js";
5
+ import { MAX_UNDO_STACK, } from "./shell-types.js";
6
6
  import { resultMsg, defaultRunHooks, defaultRunUpdate, defaultRunRelink, } from "./shell-palette.js";
7
7
  import { runDoctor } from "./link.js";
8
8
  import { renderShell, } from "./shell-view.js";
9
- import { executePalette, completeInput as completeInputFn, getListItems, handleNavigateKey, } from "./shell-input.js";
9
+ import { executePalette, completeInput as completeInputFn, getListItems, handleNavigateKey, applyViewShortcut, } from "./shell-input.js";
10
10
  import { errorMessage } from "./utils.js";
11
11
  // ── Shell class ──────────────────────────────────────────────────────────────
12
12
  export class PhrenShell {
@@ -16,7 +16,7 @@ export class PhrenShell {
16
16
  state;
17
17
  message = ` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`;
18
18
  healthCache;
19
- prevHealthView;
19
+ prevHealthView = undefined;
20
20
  showHelp = false;
21
21
  pendingConfirm;
22
22
  undoStack = [];
@@ -277,58 +277,8 @@ export class PhrenShell {
277
277
  return true;
278
278
  if (["q", "quit", ":q", ":quit", ":exit"].includes(input.toLowerCase()))
279
279
  return false;
280
- if (input === "p") {
281
- this.setView("Projects");
282
- this.setMessage(` ${TAB_ICONS.Projects} Projects`);
280
+ if (applyViewShortcut(this.asNavigationHost(), input))
283
281
  return true;
284
- }
285
- if (input === "b") {
286
- if (!this.state.project) {
287
- this.setMessage(style.dim(" Select a project first (↵)"));
288
- return true;
289
- }
290
- this.setView("Tasks");
291
- this.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
292
- return true;
293
- }
294
- if (input === "l") {
295
- if (!this.state.project) {
296
- this.setMessage(style.dim(" Select a project first (↵)"));
297
- return true;
298
- }
299
- this.setView("Findings");
300
- this.setMessage(` ${TAB_ICONS.Findings} Fragments`);
301
- return true;
302
- }
303
- if (input === "m") {
304
- if (!this.state.project) {
305
- this.setMessage(style.dim(" Select a project first (↵)"));
306
- return true;
307
- }
308
- this.setView("Review Queue");
309
- this.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
310
- return true;
311
- }
312
- if (input === "s") {
313
- if (!this.state.project) {
314
- this.setMessage(style.dim(" Select a project first (↵)"));
315
- return true;
316
- }
317
- this.setView("Skills");
318
- this.setMessage(` ${TAB_ICONS.Skills} Skills`);
319
- return true;
320
- }
321
- if (input === "k") {
322
- this.setView("Hooks");
323
- this.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
324
- return true;
325
- }
326
- if (input === "h") {
327
- this.healthCache = undefined;
328
- this.setView("Health");
329
- this.setMessage(` ${TAB_ICONS.Health} Health`);
330
- return true;
331
- }
332
282
  if (input.startsWith("/")) {
333
283
  this.setFilter(input.slice(1));
334
284
  return true;
@@ -10,6 +10,7 @@ import { runGit as runGitShared, errorMessage } from "./utils.js";
10
10
  import { readRuntimeHealth, resolveTaskFilePath } from "./data-access.js";
11
11
  import { resolveRuntimeProfile } from "./runtime-profile.js";
12
12
  import { renderPhrenArt } from "./phren-art.js";
13
+ import { RESET, BOLD, DIM, GREEN, YELLOW, RED, CYAN } from "./shell-render.js";
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  function readPackageVersion() {
15
16
  try {
@@ -23,13 +24,6 @@ function readPackageVersion() {
23
24
  return "unknown";
24
25
  }
25
26
  }
26
- const RESET = "\x1b[0m";
27
- const BOLD = "\x1b[1m";
28
- const DIM = "\x1b[2m";
29
- const GREEN = "\x1b[32m";
30
- const YELLOW = "\x1b[33m";
31
- const RED = "\x1b[31m";
32
- const CYAN = "\x1b[36m";
33
27
  function check(ok) {
34
28
  return ok ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`;
35
29
  }
@@ -302,7 +296,7 @@ export async function runStatus() {
302
296
  totalTask += countBullets(taskPath);
303
297
  totalQueue += countQueueItems(phrenPath, projName);
304
298
  }
305
- console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings} fragments, ${totalTask} tasks, ${totalQueue} queued`);
299
+ console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings} findings, ${totalTask} tasks, ${totalQueue} queued`);
306
300
  const gitTarget = manifest?.installMode === "project-local" && manifest.workspaceRoot ? manifest.workspaceRoot : phrenPath;
307
301
  const isGitRepo = runGit(gitTarget, ["rev-parse", "--is-inside-work-tree"]) === "true";
308
302
  const hasOriginRemote = isGitRepo && Boolean(runGit(gitTarget, ["remote", "get-url", "origin"]));
package/mcp/dist/utils.js CHANGED
@@ -241,7 +241,7 @@ export function sanitizeFts5Query(raw) {
241
241
  if (raw.length > 500)
242
242
  raw = raw.slice(0, 500);
243
243
  // Whitelist approach: only allow alphanumeric, spaces, hyphens, apostrophes, double quotes, asterisks
244
- let q = raw.replace(/[^a-zA-Z0-9 \-'"*]/g, " ");
244
+ let q = raw.replace(/[^a-zA-Z0-9 \-"*]/g, " ");
245
245
  q = q.replace(/\s+/g, " ");
246
246
  q = q.trim();
247
247
  // Q83: FTS5 only accepts * as a prefix operator directly attached to a token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,6 @@
18
18
  "glob": "^13.0.6",
19
19
  "inquirer": "^12.10.0",
20
20
  "js-yaml": "^4.1.1",
21
- "sharp": "^0.34.5",
22
21
  "sql.js-fts5": "^1.4.0",
23
22
  "zod": "^4.3.6"
24
23
  },