@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.
- package/README.md +35 -565
- package/mcp/dist/cli-actions.js +1 -1
- package/mcp/dist/cli-govern.js +2 -2
- package/mcp/dist/cli-hooks.js +25 -1
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/content-citation.js +65 -2
- package/mcp/dist/content-dedup.js +3 -3
- package/mcp/dist/content-learning.js +32 -9
- package/mcp/dist/data-access.js +60 -50
- package/mcp/dist/entrypoint.js +1 -1
- package/mcp/dist/finding-impact.js +23 -0
- package/mcp/dist/finding-lifecycle.js +18 -0
- package/mcp/dist/governance-policy.js +1 -1
- package/mcp/dist/governance-scores.js +9 -1
- package/mcp/dist/link-checksums.js +1 -1
- package/mcp/dist/link-doctor.js +1 -1
- package/mcp/dist/mcp-memory.js +4 -4
- package/mcp/dist/mcp-ops.js +1 -1
- package/mcp/dist/mcp-session.js +61 -2
- package/mcp/dist/memory-ui-data.js +8 -8
- package/mcp/dist/phren-art.js +268 -1
- package/mcp/dist/phren-core.js +4 -4
- package/mcp/dist/phren-paths.js +1 -1
- package/mcp/dist/profile-store.js +1 -1
- package/mcp/dist/query-correlation.js +147 -0
- package/mcp/dist/shared-content.js +2 -2
- package/mcp/dist/shared-index.js +31 -18
- package/mcp/dist/shared-retrieval.js +61 -3
- package/mcp/dist/shell-entry.js +46 -3
- package/mcp/dist/status.js +1 -1
- package/mcp/dist/test-global-setup.js +3 -4
- package/mcp/dist/tool-registry.js +1 -1
- package/mcp/dist/utils.js +1 -1
- package/package.json +3 -3
package/mcp/dist/shared-index.js
CHANGED
|
@@ -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,
|
|
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
|
-
"
|
|
85
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
786
|
-
if (!docCheck
|
|
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(`
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
1107
|
+
debugLog(`fragment extraction failed: ${errorMessage(err)}`);
|
|
1095
1108
|
}
|
|
1096
1109
|
}
|
|
1097
1110
|
}
|
|
1098
1111
|
}
|
|
1099
|
-
// Persist
|
|
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.
|
|
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;
|
package/mcp/dist/shell-entry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/mcp/dist/status.js
CHANGED
|
@@ -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, "
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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": "
|
|
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 = "
|
|
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
|
-
"description": "
|
|
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": "
|
|
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",
|