@phren/cli 0.0.4 → 0.0.6

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.
@@ -6,12 +6,13 @@ import { globSync } from "glob";
6
6
  import { debugLog, appendIndexEvent, getProjectDirs, collectNativeMemoryFiles, runtimeFile, homeDir, readRootManifest, } from "./shared.js";
7
7
  import { getIndexPolicy, withFileLock } from "./shared-governance.js";
8
8
  import { stripTaskDoneSection } from "./shared-content.js";
9
+ import { isInactiveFindingLine } from "./finding-lifecycle.js";
9
10
  import { invalidateDfCache } from "./shared-search-fallback.js";
10
11
  import { errorMessage } from "./utils.js";
11
12
  import { beginUserFragmentBuildCache, endUserFragmentBuildCache, extractAndLinkFragments, ensureGlobalEntitiesTable, } from "./shared-fragment-graph.js";
12
13
  import { bootstrapSqlJs } from "./shared-sqljs.js";
13
14
  import { getProjectOwnershipMode, getProjectSourcePath, readProjectConfig } from "./project-config.js";
14
- import { buildSourceDocKey, queryDocRows, queryRows, } from "./index-query.js";
15
+ import { buildSourceDocKey, queryDocBySourceKey, queryDocRows, } from "./index-query.js";
15
16
  import { classifyTopicForText, readProjectTopics, } from "./project-topics.js";
16
17
  export { porterStem } from "./shared-stemmer.js";
17
18
  export { cosineFallback } from "./shared-search-fallback.js";
@@ -81,8 +82,8 @@ const FILE_TYPE_MAP = {
81
82
  "reference.md": "reference",
82
83
  "tasks.md": "task",
83
84
  "changelog.md": "changelog",
84
- "canonical_memories.md": "canonical",
85
- "memory_queue.md": "memory-queue",
85
+ "truths.md": "canonical",
86
+ "review.md": "review-queue",
86
87
  };
87
88
  function pathHasSegment(relPath, segment) {
88
89
  const parts = relPath.replace(/\\/g, "/").split("/").filter(Boolean);
@@ -101,6 +102,11 @@ export function classifyFile(filename, relPath) {
101
102
  }
102
103
  const IMPORT_RE = /^@import\s+(.+)$/gm;
103
104
  const MAX_IMPORT_DEPTH = 5;
105
+ const IMPORT_ROOT_PREFIX = "shared/";
106
+ function isAllowedImportPath(importPath) {
107
+ const normalized = importPath.replace(/\\/g, "/");
108
+ return normalized.startsWith(IMPORT_ROOT_PREFIX) && normalized.toLowerCase().endsWith(".md");
109
+ }
104
110
  /**
105
111
  * Internal recursive helper for resolveImports. Tracks `seen` (cycle detection) and `depth` (runaway
106
112
  * recursion guard) — callers should never pass these; use the public `resolveImports` instead.
@@ -110,6 +116,9 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
110
116
  return content;
111
117
  return content.replace(IMPORT_RE, (_match, importPath) => {
112
118
  const trimmed = importPath.trim();
119
+ if (!isAllowedImportPath(trimmed)) {
120
+ return "<!-- @import blocked: only shared/*.md allowed -->";
121
+ }
113
122
  const globalRoot = path.resolve(phrenPath, "global");
114
123
  const resolved = path.join(globalRoot, trimmed);
115
124
  // Use lexical resolution first for the prefix check
@@ -282,7 +291,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
282
291
  }
283
292
  }
284
293
  }
285
- // Include manual entity links so graph changes invalidate the cache
294
+ // Include manual fragment links so graph changes invalidate the cache
286
295
  const manualLinksPath = runtimeFile(phrenPath, "manual-links.json");
287
296
  if (fs.existsSync(manualLinksPath)) {
288
297
  try {
@@ -464,6 +473,10 @@ export function normalizeIndexedContent(content, type, phrenPath, maxChars) {
464
473
  if (type === "task") {
465
474
  normalized = stripTaskDoneSection(normalized);
466
475
  }
476
+ if (type === "findings") {
477
+ const lines = normalized.split("\n");
478
+ normalized = lines.filter(line => !isInactiveFindingLine(line)).join("\n");
479
+ }
467
480
  if (typeof maxChars === "number" && maxChars >= 0) {
468
481
  normalized = normalized.slice(0, maxChars);
469
482
  }
@@ -598,7 +611,7 @@ export function updateFileInIndex(db, filePath, phrenPath) {
598
611
  const type = classifyFile(filename, relFile);
599
612
  const entry = { fullPath: resolvedPath, project, filename, type, relFile };
600
613
  if (insertFileIntoIndex(db, entry, phrenPath, { scheduleEmbeddings: true })) {
601
- // Re-extract entities for finding files
614
+ // Re-extract fragments for finding files
602
615
  if (type === "findings") {
603
616
  try {
604
617
  const content = fs.readFileSync(resolvedPath, "utf-8");
@@ -687,7 +700,7 @@ function isSentinelFresh(phrenPath, sentinel) {
687
700
  return true;
688
701
  }
689
702
  /**
690
- * Attempt to restore the entity graph (entities, entity_links, global_entities) from a
703
+ * Attempt to restore the fragment graph (entities, entity_links, global_entities) from a
691
704
  * previously persisted JSON snapshot. Returns true if the graph was loaded, false if the
692
705
  * caller must run full extraction instead.
693
706
  */
@@ -723,7 +736,7 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
723
736
  // is not empty after a cached-graph rebuild path.
724
737
  if (Array.isArray(graph.globalEntities)) {
725
738
  for (const [entity, project, docKey] of graph.globalEntities) {
726
- // Skip global entities whose source doc no longer exists
739
+ // Skip global fragments whose source doc no longer exists
727
740
  if (docKey && !validDocKeys.has(docKey))
728
741
  continue;
729
742
  try {
@@ -736,7 +749,7 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
736
749
  }
737
750
  }
738
751
  else {
739
- // Older cache without globalEntities: re-derive from entity_links + entities
752
+ // Older cache without globalEntities: re-derive from entity_links + entities tables
740
753
  try {
741
754
  const rows = db.exec(`SELECT e.name, el.source_doc FROM entity_links el
742
755
  JOIN entities e ON el.target_id = e.id
@@ -769,7 +782,7 @@ function loadCachedEntityGraph(db, graphPath, allFiles, phrenPath) {
769
782
  }
770
783
  return false;
771
784
  }
772
- /** Merge manual entity links (written by link_findings tool) into the live DB. Always runs on
785
+ /** Merge manual fragment links (written by link_findings tool) into the live DB. Always runs on
773
786
  * every build so hand-authored links survive a full index rebuild. */
774
787
  function mergeManualLinks(db, phrenPath) {
775
788
  const manualLinksPath = runtimeFile(phrenPath, 'manual-links.json');
@@ -782,8 +795,8 @@ function mergeManualLinks(db, phrenPath) {
782
795
  for (const link of manualLinks) {
783
796
  try {
784
797
  // Validate: skip manual links whose sourceDoc no longer exists in the index
785
- const docCheck = queryRows(db, "SELECT 1 FROM docs WHERE source_key = ? LIMIT 1", [link.sourceDoc]);
786
- if (!docCheck || docCheck.length === 0) {
798
+ const docCheck = queryDocBySourceKey(db, phrenPath, link.sourceDoc);
799
+ if (!docCheck) {
787
800
  if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
788
801
  process.stderr.write(`[phren] manualLinks: pruning stale link to "${link.sourceDoc}"\n`);
789
802
  pruned = true;
@@ -1001,7 +1014,7 @@ async function buildIndexImpl(phrenPath, profile) {
1001
1014
  extractAndLinkFragments(db, content, getEntrySourceDocKey(entry, phrenPath), phrenPath);
1002
1015
  }
1003
1016
  catch (err) {
1004
- debugLog(`entity extraction failed: ${errorMessage(err)}`);
1017
+ debugLog(`fragment extraction failed: ${errorMessage(err)}`);
1005
1018
  }
1006
1019
  }
1007
1020
  }
@@ -1063,15 +1076,15 @@ async function buildIndexImpl(phrenPath, profile) {
1063
1076
  tokenize = "porter unicode61"
1064
1077
  );
1065
1078
  `);
1066
- // Entity graph tables for lightweight reference graph
1079
+ // Fragment graph tables for lightweight reference graph
1067
1080
  db.run(`CREATE TABLE IF NOT EXISTS entities (id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, first_seen_at TEXT, UNIQUE(name, type))`);
1068
1081
  db.run(`CREATE TABLE IF NOT EXISTS entity_links (source_id INTEGER REFERENCES entities(id), target_id INTEGER REFERENCES entities(id), rel_type TEXT NOT NULL, source_doc TEXT, PRIMARY KEY (source_id, target_id, rel_type))`);
1069
- // Q20: Cross-project entity index
1082
+ // Q20: Cross-project fragment index
1070
1083
  ensureGlobalEntitiesTable(db);
1071
1084
  const allFiles = globResult.entries;
1072
1085
  const newHashes = {};
1073
1086
  let fileCount = 0;
1074
- // Try loading cached entity graph
1087
+ // Try loading cached fragment graph
1075
1088
  const graphPath = runtimeFile(phrenPath, 'entity-graph.json');
1076
1089
  const entityGraphLoaded = loadCachedEntityGraph(db, graphPath, allFiles, phrenPath);
1077
1090
  for (const entry of allFiles) {
@@ -1084,19 +1097,19 @@ async function buildIndexImpl(phrenPath, profile) {
1084
1097
  }
1085
1098
  if (insertFileIntoIndex(db, entry, phrenPath, { scheduleEmbeddings: true })) {
1086
1099
  fileCount++;
1087
- // Extract entities from finding files (if not loaded from cache)
1100
+ // Extract fragments from finding files (if not loaded from cache)
1088
1101
  if (!entityGraphLoaded && entry.type === "findings") {
1089
1102
  try {
1090
1103
  const content = fs.readFileSync(entry.fullPath, "utf-8");
1091
1104
  extractAndLinkFragments(db, content, getEntrySourceDocKey(entry, phrenPath), phrenPath);
1092
1105
  }
1093
1106
  catch (err) {
1094
- debugLog(`entity extraction failed: ${errorMessage(err)}`);
1107
+ debugLog(`fragment extraction failed: ${errorMessage(err)}`);
1095
1108
  }
1096
1109
  }
1097
1110
  }
1098
1111
  }
1099
- // Persist entity graph for next build
1112
+ // Persist fragment graph for next build
1100
1113
  if (!entityGraphLoaded) {
1101
1114
  try {
1102
1115
  const entityRows = db.exec("SELECT id, name, type FROM entities")[0]?.values ?? [];
@@ -3,7 +3,7 @@ import { getQualityMultiplier, entryScoreKey, } from "./shared-governance.js";
3
3
  import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getEntityBoostDocs, decodeFiniteNumber, rowToDocWithRowid, } from "./shared-index.js";
4
4
  import { filterTrustedFindingsDetailed, } from "./shared-content.js";
5
5
  import { parseCitationComment } from "./content-citation.js";
6
- import { getHighImpactFindings } from "./finding-impact.js";
6
+ import { getHighImpactFindings, getImpactSurfaceCounts } from "./finding-impact.js";
7
7
  import { buildFtsQueryVariants, buildRelaxedFtsQuery, isFeatureEnabled, STOP_WORDS } from "./utils.js";
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
@@ -36,6 +36,8 @@ const LOW_VALUE_BULLET_FRACTION = 0.5;
36
36
  // ── Intent and scoring helpers ───────────────────────────────────────────────
37
37
  export function detectTaskIntent(prompt) {
38
38
  const p = prompt.toLowerCase();
39
+ if (/\/\w+/.test(p) || /\b(skill|swarm|command|lineup|slash command)\b/.test(p))
40
+ return "skill";
39
41
  if (/(bug|error|fix|broken|regression|fail|stack trace)/.test(p))
40
42
  return "debug";
41
43
  if (/(review|audit|pr|pull request|nit|refactor)/.test(p))
@@ -47,6 +49,8 @@ export function detectTaskIntent(prompt) {
47
49
  return "general";
48
50
  }
49
51
  function intentBoost(intent, docType) {
52
+ if (intent === "skill" && docType === "skill")
53
+ return 4;
50
54
  if (intent === "debug" && (docType === "findings" || docType === "reference"))
51
55
  return 3;
52
56
  if (intent === "review" && (docType === "canonical" || docType === "changelog"))
@@ -343,10 +347,23 @@ export function searchDocuments(db, safeQuery, prompt, keywords, detectedProject
343
347
  if (ftsDocs.length === 0 && relaxedQuery && relaxedQuery !== safeQuery) {
344
348
  runScopedFtsQuery(relaxedQuery);
345
349
  }
350
+ // Tier 1.5: Fragment graph expansion
351
+ const fragmentExpansionDocs = [];
352
+ const queryLower = (prompt + " " + keywords).toLowerCase();
353
+ const fragmentBoostDocKeys = getEntityBoostDocs(db, queryLower);
354
+ for (const docKey of fragmentBoostDocKeys) {
355
+ if (ftsSeenKeys.has(docKey))
356
+ continue;
357
+ const rows = queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE path = ? LIMIT 1", [docKey]);
358
+ if (rows?.length) {
359
+ ftsSeenKeys.add(docKey);
360
+ fragmentExpansionDocs.push(rows[0]);
361
+ }
362
+ }
346
363
  // Tier 2: Token-overlap semantic — always run, scored independently
347
364
  const semanticDocs = semanticFallbackDocs(db, `${prompt}\n${keywords}`, detectedProject);
348
365
  // Merge with Reciprocal Rank Fusion so documents found by both tiers rank highest
349
- const merged = rrfMerge([ftsDocs, semanticDocs]);
366
+ const merged = rrfMerge([ftsDocs, fragmentExpansionDocs, semanticDocs]);
350
367
  if (merged.length === 0)
351
368
  return null;
352
369
  return merged.slice(0, 12);
@@ -498,6 +515,7 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
498
515
  const queueItems = [];
499
516
  const auditEntries = [];
500
517
  const highImpactFindingIds = phrenPath ? getHighImpactFindings(phrenPath, 3) : undefined;
518
+ const impactCounts = phrenPath ? getImpactSurfaceCounts(phrenPath, 1) : undefined;
501
519
  const filtered = rows
502
520
  .map((doc) => {
503
521
  if (!TRUST_FILTERED_TYPES.has(doc.type))
@@ -508,6 +526,7 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
508
526
  decay,
509
527
  project: doc.project,
510
528
  highImpactFindingIds,
529
+ impactCounts,
511
530
  });
512
531
  if (trust.issues.length > 0) {
513
532
  const stale = trust.issues.filter((i) => i.reason === "stale").map((i) => i.bullet);
@@ -611,7 +630,7 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
611
630
  const scored = ranked.map((doc) => {
612
631
  const globBoost = getProjectGlobBoost(phrenPathLocal, doc.project, cwd, gitCtx?.changedFiles);
613
632
  const key = entryScoreKey(doc.project, doc.filename, doc.content);
614
- const entity = entityBoostPaths.has(doc.path) ? 1.3 : 1;
633
+ const entity = entityBoostPaths.has(doc.path) ? 1.5 : 1;
615
634
  const date = getRecentDate(doc);
616
635
  const fileRel = fileRelevanceBoost(doc.path, changedFiles);
617
636
  const branchMat = branchMatchBoost(doc.content, gitCtx?.branch);
@@ -626,7 +645,12 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
626
645
  && queryOverlap < WEAK_CROSS_PROJECT_OVERLAP_MAX
627
646
  ? WEAK_CROSS_PROJECT_OVERLAP_PENALTY
628
647
  : 0;
648
+ // Boost skills whose filename matches a query token (e.g. "swarm" matches swarm.md)
649
+ const skillNameBoost = doc.type === "skill" && queryTokens.length > 0
650
+ ? queryTokens.some((t) => doc.filename.replace(/\.md$/i, "").toLowerCase() === t) ? 4 : 0
651
+ : 0;
629
652
  const score = Math.round((intentBoost(intent, doc.type) +
653
+ skillNameBoost +
630
654
  fileRel +
631
655
  branchMat +
632
656
  globBoost +
@@ -747,10 +771,40 @@ export function markStaleCitations(snippet) {
747
771
  }
748
772
  return result.join("\n");
749
773
  }
774
+ function annotateContradictions(snippet) {
775
+ return snippet.split('\n').map(line => {
776
+ const conflictMatch = line.match(/<!-- conflicts_with: "(.*?)" -->/);
777
+ const contradictMatch = line.match(/<!-- phren:contradicts "(.*?)" -->/);
778
+ const statusMatch = line.match(/phren:status "contradicted"/);
779
+ if (conflictMatch) {
780
+ return line.replace(conflictMatch[0], '') + ` [CONTRADICTED — conflicts with: "${conflictMatch[1]}"]`;
781
+ }
782
+ if (contradictMatch) {
783
+ return line.replace(contradictMatch[0], '') + ` [CONTRADICTED — see: "${contradictMatch[1]}"]`;
784
+ }
785
+ if (statusMatch) {
786
+ return line + ' [CONTRADICTED]';
787
+ }
788
+ return line;
789
+ }).join('\n');
790
+ }
750
791
  export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudget) {
751
792
  const selected = [];
752
793
  let usedTokens = 36;
753
794
  const queryTokens = tokenizeForOverlap(keywords);
795
+ const seenBullets = new Set();
796
+ // For each snippet being added, hash its bullet lines and skip duplicates
797
+ function dedupSnippetBullets(snippet) {
798
+ return snippet.split('\n').filter(line => {
799
+ if (!line.startsWith('- '))
800
+ return true; // Keep non-bullet lines (headers, etc)
801
+ const normalized = line.replace(/<!--.*?-->/g, '').trim().toLowerCase();
802
+ if (seenBullets.has(normalized))
803
+ return false;
804
+ seenBullets.add(normalized);
805
+ return true;
806
+ }).join('\n');
807
+ }
754
808
  for (const doc of rows) {
755
809
  let snippet = compactSnippet(extractSnippet(doc.content, keywords, 8), lineBudget, charBudget);
756
810
  if (!snippet.trim())
@@ -759,6 +813,10 @@ export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudg
759
813
  if (TRUST_FILTERED_TYPES.has(doc.type)) {
760
814
  snippet = markStaleCitations(snippet);
761
815
  }
816
+ snippet = annotateContradictions(snippet);
817
+ snippet = dedupSnippetBullets(snippet);
818
+ if (!snippet.trim())
819
+ continue;
762
820
  let focusScore = queryTokens.length > 0
763
821
  ? overlapScore(queryTokens, `${doc.filename}\n${snippet}`)
764
822
  : 1;
@@ -3,7 +3,8 @@
3
3
  * Extracted from shell.ts to keep the orchestrator under 300 lines.
4
4
  */
5
5
  import { PhrenShell } from "./shell.js";
6
- import { style, clearScreen, clearToEnd, shellStartupFrames } from "./shell-render.js";
6
+ import { style, clearScreen, clearToEnd, shellStartupFrames, gradient, badge } from "./shell-render.js";
7
+ import { createPhrenAnimator } from "./phren-art.js";
7
8
  import { errorMessage } from "./utils.js";
8
9
  import { computePhrenLiveStateToken } from "./shared.js";
9
10
  import { VERSION } from "./init-shared.js";
@@ -59,13 +60,55 @@ async function playStartupIntro(phrenPath, plan = resolveStartupIntroPlan(phrenP
59
60
  await sleep(160);
60
61
  }
61
62
  }
62
- renderIntroFrame(frames[frames.length - 1], renderHint);
63
+ // Start animated phren during loading
64
+ const animator = createPhrenAnimator({ facing: "right" });
65
+ animator.start();
66
+ const cols = process.stdout.columns || 80;
67
+ const tagline = style.dim("local memory for working agents");
68
+ const versionBadge = badge(`v${VERSION}`, style.boldBlue);
69
+ const logoLines = [
70
+ "██████╗ ██╗ ██╗██████╗ ███████╗███╗ ██╗",
71
+ "██╔══██╗██║ ██║██╔══██╗██╔════╝████╗ ██║",
72
+ "██████╔╝███████║██████╔╝█████╗ ██╔██╗ ██║",
73
+ "██╔═══╝ ██╔══██║██╔══██╗██╔══╝ ██║╚██╗██║",
74
+ "██║ ██║ ██║██║ ██║███████╗██║ ╚████║",
75
+ "╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝",
76
+ ].map(l => gradient(l));
77
+ const infoLine = `${gradient("◆")} ${style.bold("phren")} ${versionBadge} ${tagline}`;
78
+ function renderAnimatedFrame(hint) {
79
+ const phrenLines = animator.getFrame();
80
+ const rightSide = ["", "", ...logoLines, "", infoLine];
81
+ const charWidth = 26;
82
+ const maxLines = Math.max(phrenLines.length, rightSide.length);
83
+ const merged = [""];
84
+ for (let i = 0; i < maxLines; i++) {
85
+ const left = (i < phrenLines.length ? phrenLines[i] : "").padEnd(charWidth);
86
+ const right = i < rightSide.length ? rightSide[i] : "";
87
+ merged.push(left + right);
88
+ }
89
+ if (hint)
90
+ merged.push("", ` ${hint}`);
91
+ merged.push("");
92
+ renderIntroFrame(merged.join("\n"));
93
+ }
94
+ // Animate during dwell/loading period
63
95
  if (plan.holdForKeypress) {
96
+ const animInterval = setInterval(() => renderAnimatedFrame(renderHint), 200);
97
+ renderAnimatedFrame(renderHint);
64
98
  await waitForAnyKeypress();
99
+ clearInterval(animInterval);
65
100
  }
66
101
  else if (plan.dwellMs > 0) {
67
- await sleep(plan.dwellMs);
102
+ const startTime = Date.now();
103
+ while (Date.now() - startTime < plan.dwellMs) {
104
+ renderAnimatedFrame(renderHint);
105
+ await sleep(200);
106
+ }
107
+ }
108
+ else {
109
+ renderAnimatedFrame(renderHint);
68
110
  }
111
+ animator.stop();
69
112
  if (plan.markSeen) {
70
113
  markStartupIntroSeen(phrenPath);
71
114
  }
@@ -39,7 +39,7 @@ function countBullets(filePath) {
39
39
  return content.split("\n").filter((l) => l.startsWith("- ")).length;
40
40
  }
41
41
  function countQueueItems(phrenPath, project) {
42
- const queueFile = path.join(phrenPath, project, "MEMORY_QUEUE.md");
42
+ const queueFile = path.join(phrenPath, project, "review.md");
43
43
  return countBullets(queueFile);
44
44
  }
45
45
  function runGit(cwd, args) {
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Vitest globalSetup — runs once in the main process before any test workers spawn.
3
3
  *
4
- * Builds mcp/dist if it is missing or stale so every fork sees a complete,
5
- * consistent dist artifact. Running the build here (rather than inside each
6
- * fork via ensureCliBuilt) eliminates the race condition where one fork runs
7
- * `rm -rf mcp/dist` while another fork is checking fs.existsSync(CLI_PATH).
4
+ * Builds mcp/dist if it is missing so every fork sees a complete, consistent
5
+ * dist artifact before tests begin. Individual subprocess helpers can still
6
+ * repair a missing artifact later under a lock if some test mutates dist.
8
7
  *
9
8
  * `pretest` in package.json already calls `npm run build`, so in normal `npm test`
10
9
  * runs this is a fast no-op check. It is the safety net for:
@@ -7,7 +7,7 @@ const CATEGORY_BY_MODULE = {
7
7
  "mcp-finding": "Finding capture",
8
8
  "mcp-memory": "Memory quality",
9
9
  "mcp-data": "Data management",
10
- "mcp-graph": "Entities and graph",
10
+ "mcp-graph": "Fragments and graph",
11
11
  "mcp-session": "Session management",
12
12
  "mcp-ops": "Operations and review",
13
13
  "mcp-skills": "Skills management",
package/mcp/dist/utils.js CHANGED
@@ -222,7 +222,7 @@ export function safeProjectPath(base, ...segments) {
222
222
  }
223
223
  return resolved;
224
224
  }
225
- const QUEUE_FILENAME = "MEMORY_QUEUE.md";
225
+ const QUEUE_FILENAME = "review.md";
226
226
  export function queueFilePath(phrenPath, project) {
227
227
  if (!isValidProjectName(project)) {
228
228
  throw new Error(`Invalid project name: ${project}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.4",
4
- "description": "Long-term memory for AI agents. Stored as markdown in a git repo you own.",
3
+ "version": "0.0.6",
4
+ "description": "Knowledge layer for AI agents. Claude remembers you. Phren remembers your work.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "phren": "mcp/dist/index.js"
@@ -34,7 +34,7 @@
34
34
  "vitest": "^4.0.18"
35
35
  },
36
36
  "scripts": {
37
- "build": "rm -rf mcp/dist && tsc -p mcp/tsconfig.json && chmod +x mcp/dist/index.js && cp mcp/src/synonyms*.json mcp/dist/",
37
+ "build": "node scripts/build.mjs",
38
38
  "dev": "tsx mcp/src/index.ts",
39
39
  "lint": "eslint mcp/src/ --ignore-pattern '*.test.ts'",
40
40
  "validate-docs": "bash scripts/validate-docs.sh",