@phren/cli 0.0.5 → 0.0.7
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 +3 -3
- package/mcp/dist/cli-hooks-session.js +9 -3
- package/mcp/dist/cli-hooks.js +27 -9
- package/mcp/dist/content-citation.js +24 -3
- package/mcp/dist/content-learning.js +28 -5
- package/mcp/dist/data-access.js +96 -53
- package/mcp/dist/finding-impact.js +23 -0
- package/mcp/dist/finding-lifecycle.js +56 -29
- package/mcp/dist/governance-locks.js +11 -6
- package/mcp/dist/index.js +2 -1
- package/mcp/dist/init-preferences.js +18 -3
- package/mcp/dist/init.js +11 -0
- package/mcp/dist/mcp-config.js +0 -8
- package/mcp/dist/mcp-data.js +22 -3
- package/mcp/dist/mcp-extract.js +1 -0
- package/mcp/dist/mcp-finding.js +1 -1
- package/mcp/dist/mcp-hooks.js +36 -16
- package/mcp/dist/mcp-memory.js +0 -1
- package/mcp/dist/mcp-ops.js +5 -10
- package/mcp/dist/mcp-search.js +7 -1
- package/mcp/dist/mcp-session.js +66 -6
- package/mcp/dist/mcp-skills.js +5 -2
- package/mcp/dist/mcp-tasks.js +7 -4
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-data.js +7 -7
- package/mcp/dist/memory-ui-graph.js +178 -23
- package/mcp/dist/project-config.js +37 -18
- package/mcp/dist/shared-content.js +1 -1
- package/mcp/dist/shared-index.js +16 -3
- package/mcp/dist/shared-retrieval.js +64 -34
- package/mcp/dist/shared.js +1 -10
- package/mcp/dist/test-global-setup.js +3 -4
- package/package.json +2 -2
|
@@ -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";
|
|
@@ -12,7 +12,6 @@ import { vectorFallback } 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";
|
|
15
|
-
import { getCorrelatedDocs } from "./query-correlation.js";
|
|
16
15
|
// ── Scoring constants ─────────────────────────────────────────────────────────
|
|
17
16
|
/** Number of docs sampled for token-overlap semantic fallback search. */
|
|
18
17
|
const SEMANTIC_FALLBACK_SAMPLE_LIMIT = 100;
|
|
@@ -32,13 +31,13 @@ const LOW_FOCUS_SNIPPET_CHAR_FRACTION = 0.55;
|
|
|
32
31
|
const TASK_RESCUE_MIN_OVERLAP = 0.3;
|
|
33
32
|
const TASK_RESCUE_OVERLAP_MARGIN = 0.12;
|
|
34
33
|
const TASK_RESCUE_SCORE_MARGIN = 0.6;
|
|
35
|
-
/** Boost applied to docs that correlate with recurring query patterns. */
|
|
36
|
-
const CORRELATION_BOOST = 1.5;
|
|
37
34
|
/** Fraction of bullets that must be low-value before applying the low-value penalty. */
|
|
38
35
|
const LOW_VALUE_BULLET_FRACTION = 0.5;
|
|
39
36
|
// ── Intent and scoring helpers ───────────────────────────────────────────────
|
|
40
37
|
export function detectTaskIntent(prompt) {
|
|
41
38
|
const p = prompt.toLowerCase();
|
|
39
|
+
if (/(^|\s)\/[a-z][a-z0-9_-]{1,63}(?=$|\s|[.,:;!?])/.test(p) || /\b(skill|swarm|lineup|slash command)\b/.test(p))
|
|
40
|
+
return "skill";
|
|
42
41
|
if (/(bug|error|fix|broken|regression|fail|stack trace)/.test(p))
|
|
43
42
|
return "debug";
|
|
44
43
|
if (/(review|audit|pr|pull request|nit|refactor)/.test(p))
|
|
@@ -50,6 +49,8 @@ export function detectTaskIntent(prompt) {
|
|
|
50
49
|
return "general";
|
|
51
50
|
}
|
|
52
51
|
function intentBoost(intent, docType) {
|
|
52
|
+
if (intent === "skill" && docType === "skill")
|
|
53
|
+
return 4;
|
|
53
54
|
if (intent === "debug" && (docType === "findings" || docType === "reference"))
|
|
54
55
|
return 3;
|
|
55
56
|
if (intent === "review" && (docType === "canonical" || docType === "changelog"))
|
|
@@ -346,10 +347,23 @@ export function searchDocuments(db, safeQuery, prompt, keywords, detectedProject
|
|
|
346
347
|
if (ftsDocs.length === 0 && relaxedQuery && relaxedQuery !== safeQuery) {
|
|
347
348
|
runScopedFtsQuery(relaxedQuery);
|
|
348
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
|
+
}
|
|
349
363
|
// Tier 2: Token-overlap semantic — always run, scored independently
|
|
350
364
|
const semanticDocs = semanticFallbackDocs(db, `${prompt}\n${keywords}`, detectedProject);
|
|
351
365
|
// Merge with Reciprocal Rank Fusion so documents found by both tiers rank highest
|
|
352
|
-
const merged = rrfMerge([ftsDocs, semanticDocs]);
|
|
366
|
+
const merged = rrfMerge([ftsDocs, fragmentExpansionDocs, semanticDocs]);
|
|
353
367
|
if (merged.length === 0)
|
|
354
368
|
return null;
|
|
355
369
|
return merged.slice(0, 12);
|
|
@@ -386,7 +400,7 @@ export async function searchDocumentsAsync(db, safeQuery, prompt, keywords, dete
|
|
|
386
400
|
}
|
|
387
401
|
catch (err) {
|
|
388
402
|
// Vector search failure is non-fatal — return sync result
|
|
389
|
-
if (
|
|
403
|
+
if (process.env.PHREN_DEBUG)
|
|
390
404
|
process.stderr.write(`[phren] hybridSearch vectorFallback: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
391
405
|
return syncResult;
|
|
392
406
|
}
|
|
@@ -486,7 +500,7 @@ export async function searchKnowledgeRows(db, options) {
|
|
|
486
500
|
}
|
|
487
501
|
}
|
|
488
502
|
catch (err) {
|
|
489
|
-
if (
|
|
503
|
+
if (process.env.PHREN_DEBUG) {
|
|
490
504
|
process.stderr.write(`[phren] vectorFallback: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
491
505
|
}
|
|
492
506
|
}
|
|
@@ -501,6 +515,7 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
|
|
|
501
515
|
const queueItems = [];
|
|
502
516
|
const auditEntries = [];
|
|
503
517
|
const highImpactFindingIds = phrenPath ? getHighImpactFindings(phrenPath, 3) : undefined;
|
|
518
|
+
const impactCounts = phrenPath ? getImpactSurfaceCounts(phrenPath, 1) : undefined;
|
|
504
519
|
const filtered = rows
|
|
505
520
|
.map((doc) => {
|
|
506
521
|
if (!TRUST_FILTERED_TYPES.has(doc.type))
|
|
@@ -511,6 +526,7 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
|
|
|
511
526
|
decay,
|
|
512
527
|
project: doc.project,
|
|
513
528
|
highImpactFindingIds,
|
|
529
|
+
impactCounts,
|
|
514
530
|
});
|
|
515
531
|
if (trust.issues.length > 0) {
|
|
516
532
|
const stale = trust.issues.filter((i) => i.reason === "stale").map((i) => i.bullet);
|
|
@@ -608,15 +624,13 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
|
|
|
608
624
|
}
|
|
609
625
|
}
|
|
610
626
|
const getRecentDate = (doc) => recentDateCache.get(doc.path || `${doc.project}/${doc.filename}`) ?? "0000-00-00";
|
|
611
|
-
// Query correlation: pre-warm docs that historically correlated with similar queries
|
|
612
|
-
const correlatedDocKeys = query ? new Set(getCorrelatedDocs(phrenPathLocal, query, 5)) : new Set();
|
|
613
627
|
// Precompute per-doc ranking metadata once — avoids recomputing inside sort comparator.
|
|
614
628
|
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
615
629
|
const FILE_MATCH_BOOST = 1.5;
|
|
616
630
|
const scored = ranked.map((doc) => {
|
|
617
631
|
const globBoost = getProjectGlobBoost(phrenPathLocal, doc.project, cwd, gitCtx?.changedFiles);
|
|
618
632
|
const key = entryScoreKey(doc.project, doc.filename, doc.content);
|
|
619
|
-
const entity = entityBoostPaths.has(doc.path) ? 1.
|
|
633
|
+
const entity = entityBoostPaths.has(doc.path) ? 1.5 : 1;
|
|
620
634
|
const date = getRecentDate(doc);
|
|
621
635
|
const fileRel = fileRelevanceBoost(doc.path, changedFiles);
|
|
622
636
|
const branchMat = branchMatchBoost(doc.content, gitCtx?.branch);
|
|
@@ -631,17 +645,19 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
|
|
|
631
645
|
&& queryOverlap < WEAK_CROSS_PROJECT_OVERLAP_MAX
|
|
632
646
|
? WEAK_CROSS_PROJECT_OVERLAP_PENALTY
|
|
633
647
|
: 0;
|
|
634
|
-
|
|
635
|
-
const
|
|
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;
|
|
636
652
|
const score = Math.round((intentBoost(intent, doc.type) +
|
|
653
|
+
skillNameBoost +
|
|
637
654
|
fileRel +
|
|
638
655
|
branchMat +
|
|
639
656
|
globBoost +
|
|
640
657
|
qualityMult +
|
|
641
658
|
entity +
|
|
642
659
|
queryOverlap * queryOverlapWeight +
|
|
643
|
-
recencyBoost(doc.type, date)
|
|
644
|
-
correlationBoost -
|
|
660
|
+
recencyBoost(doc.type, date) -
|
|
645
661
|
weakCrossProjectPenalty -
|
|
646
662
|
lowValuePenalty(doc.content, doc.type)) * crossProjectAgeMultiplier(doc, detectedProject, date) * 10000) / 10000;
|
|
647
663
|
const fileMatch = fileRel > 0 || branchMat > 0;
|
|
@@ -706,24 +722,6 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
|
|
|
706
722
|
}
|
|
707
723
|
return ranked;
|
|
708
724
|
}
|
|
709
|
-
/** Annotate snippet lines that carry contradiction metadata with visible markers. */
|
|
710
|
-
export function annotateContradictions(snippet) {
|
|
711
|
-
return snippet.split('\n').map(line => {
|
|
712
|
-
const conflictMatch = line.match(/<!-- conflicts_with: "(.*?)" -->/);
|
|
713
|
-
const contradictMatch = line.match(/<!-- phren:contradicts "(.*?)" -->/);
|
|
714
|
-
const statusMatch = line.match(/phren:status "contradicted"/);
|
|
715
|
-
if (conflictMatch) {
|
|
716
|
-
return line.replace(conflictMatch[0], '') + ` [CONTRADICTED — conflicts with: "${conflictMatch[1]}"]`;
|
|
717
|
-
}
|
|
718
|
-
if (contradictMatch) {
|
|
719
|
-
return line.replace(contradictMatch[0], '') + ` [CONTRADICTED — see: "${contradictMatch[1]}"]`;
|
|
720
|
-
}
|
|
721
|
-
if (statusMatch) {
|
|
722
|
-
return line + ' [CONTRADICTED]';
|
|
723
|
-
}
|
|
724
|
-
return line;
|
|
725
|
-
}).join('\n');
|
|
726
|
-
}
|
|
727
725
|
/** Mark snippet lines with stale citations (cited file missing or line content changed). */
|
|
728
726
|
export function markStaleCitations(snippet) {
|
|
729
727
|
const lines = snippet.split("\n");
|
|
@@ -756,7 +754,7 @@ export function markStaleCitations(snippet) {
|
|
|
756
754
|
}
|
|
757
755
|
}
|
|
758
756
|
catch (err) {
|
|
759
|
-
if (
|
|
757
|
+
if (process.env.PHREN_DEBUG)
|
|
760
758
|
process.stderr.write(`[phren] applyCitationAnnotations fileRead: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
761
759
|
stale = true;
|
|
762
760
|
}
|
|
@@ -773,10 +771,40 @@ export function markStaleCitations(snippet) {
|
|
|
773
771
|
}
|
|
774
772
|
return result.join("\n");
|
|
775
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
|
+
}
|
|
776
791
|
export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudget) {
|
|
777
792
|
const selected = [];
|
|
778
793
|
let usedTokens = 36;
|
|
779
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
|
+
}
|
|
780
808
|
for (const doc of rows) {
|
|
781
809
|
let snippet = compactSnippet(extractSnippet(doc.content, keywords, 8), lineBudget, charBudget);
|
|
782
810
|
if (!snippet.trim())
|
|
@@ -785,8 +813,10 @@ export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudg
|
|
|
785
813
|
if (TRUST_FILTERED_TYPES.has(doc.type)) {
|
|
786
814
|
snippet = markStaleCitations(snippet);
|
|
787
815
|
}
|
|
788
|
-
// Surface contradiction metadata as visible annotations
|
|
789
816
|
snippet = annotateContradictions(snippet);
|
|
817
|
+
snippet = dedupSnippetBullets(snippet);
|
|
818
|
+
if (!snippet.trim())
|
|
819
|
+
continue;
|
|
790
820
|
let focusScore = queryTokens.length > 0
|
|
791
821
|
? overlapScore(queryTokens, `${doc.filename}\n${snippet}`)
|
|
792
822
|
: 1;
|
package/mcp/dist/shared.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { debugLog, runtimeFile } from "./phren-paths.js";
|
|
4
|
-
import { errorMessage
|
|
4
|
+
import { errorMessage } from "./utils.js";
|
|
5
5
|
export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
|
|
6
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";
|
|
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";
|
|
@@ -28,15 +28,6 @@ export function isMemoryScopeVisible(itemScope, activeScope) {
|
|
|
28
28
|
export function impactLogFile(phrenPath) {
|
|
29
29
|
return runtimeFile(phrenPath, "impact.jsonl");
|
|
30
30
|
}
|
|
31
|
-
function isProjectDirEntry(entry) {
|
|
32
|
-
return entry.isDirectory()
|
|
33
|
-
&& !entry.name.startsWith(".")
|
|
34
|
-
&& !entry.name.endsWith(".archived")
|
|
35
|
-
&& !RESERVED_PROJECT_DIR_NAMES.has(entry.name);
|
|
36
|
-
}
|
|
37
|
-
function isCanonicalProjectDirName(name) {
|
|
38
|
-
return name === name.toLowerCase() && isValidProjectName(name);
|
|
39
|
-
}
|
|
40
31
|
export function appendAuditLog(phrenPath, event, details) {
|
|
41
32
|
const logPath = runtimeFile(phrenPath, "audit.log");
|
|
42
33
|
const line = `[${new Date().toISOString()}] ${event} ${details}\n`;
|
|
@@ -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:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Knowledge layer for AI agents. Claude remembers you. Phren remembers your work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -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",
|