recallnest 1.4.1
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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecallNest Doctor — pre-flight checks for installation & configuration.
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* 1. Bun runtime available
|
|
6
|
+
* 2. .env file + JINA_API_KEY set
|
|
7
|
+
* 3. Jina API key valid (test embedding)
|
|
8
|
+
* 4. CC transcript path accessible
|
|
9
|
+
* 5. LanceDB data directory writable
|
|
10
|
+
* 6. Existing index stats (if any)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, accessSync, constants, readFileSync } from "node:fs";
|
|
14
|
+
import { resolve, join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { loadConfig, expandHome, resolveEnv, findConfigPath, loadDotEnv } from "./runtime-config.js";
|
|
17
|
+
import { createEmbedder, type EmbeddingConfig } from "./embedder.js";
|
|
18
|
+
import { extractCanonicalKey, normalizeCanonicalKey } from "./memory-boundaries.js";
|
|
19
|
+
import {
|
|
20
|
+
CaseMemoryInputSchema,
|
|
21
|
+
type CaseMemoryInput,
|
|
22
|
+
StoreMemoryInputSchema,
|
|
23
|
+
type StoreMemoryInput,
|
|
24
|
+
WorkflowPatternInputSchema,
|
|
25
|
+
type WorkflowPatternInput,
|
|
26
|
+
} from "./memory-schema.js";
|
|
27
|
+
import { collectScopeInventory, type ScopeInventoryReport } from "./scope-inventory.js";
|
|
28
|
+
import { MemoryStore, type MemoryEntry, validateStoragePath } from "./store.js";
|
|
29
|
+
|
|
30
|
+
interface CheckResult {
|
|
31
|
+
name: string;
|
|
32
|
+
status: "pass" | "fail" | "warn";
|
|
33
|
+
message: string;
|
|
34
|
+
fix?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ContinuityBaselineSeeds {
|
|
38
|
+
patterns: WorkflowPatternInput[];
|
|
39
|
+
cases: CaseMemoryInput[];
|
|
40
|
+
memories: StoreMemoryInput[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ContinuityBaselineAssessment {
|
|
44
|
+
expected: { patterns: number; cases: number; memories: number };
|
|
45
|
+
found: { patterns: number; cases: number; memories: number };
|
|
46
|
+
missing: { patterns: string[]; cases: string[]; memories: string[] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ContinuityBaselineEntryStore = Pick<MemoryStore, "list">;
|
|
50
|
+
|
|
51
|
+
function pass(name: string, message: string): CheckResult {
|
|
52
|
+
return { name, status: "pass", message };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fail(name: string, message: string, fix?: string): CheckResult {
|
|
56
|
+
return { name, status: "fail", message, fix };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function warn(name: string, message: string, fix?: string): CheckResult {
|
|
60
|
+
return { name, status: "warn", message, fix };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeScopeInventoryLayers(report: ScopeInventoryReport): string {
|
|
64
|
+
return report.layers
|
|
65
|
+
.filter((layer) => layer.anomalyCount > 0 || layer.invalidCount > 0)
|
|
66
|
+
.map((layer) => {
|
|
67
|
+
const parts = [`${layer.layer}:${layer.anomalyCount}`];
|
|
68
|
+
if (layer.invalidCount > 0) {
|
|
69
|
+
parts.push(`invalid ${layer.invalidCount}`);
|
|
70
|
+
}
|
|
71
|
+
return parts.join(" ");
|
|
72
|
+
})
|
|
73
|
+
.join(", ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function assessScopeInventoryReport(report: ScopeInventoryReport): CheckResult {
|
|
77
|
+
const cleanSummary = `0 unresolved anomalies across ${report.totalScannedCount} records`;
|
|
78
|
+
if (report.totalAnomalyCount === 0 && report.totalInvalidCount === 0) {
|
|
79
|
+
const reviewedSuffix = report.totalReviewedCount > 0
|
|
80
|
+
? `; reviewed keeps ${report.totalReviewedCount}`
|
|
81
|
+
: "";
|
|
82
|
+
return pass("Scope inventory", `${cleanSummary}${reviewedSuffix}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const layerSummary = summarizeScopeInventoryLayers(report);
|
|
86
|
+
const firstSample = report.layers.flatMap((layer) => layer.samples).at(0);
|
|
87
|
+
const sampleSummary = firstSample
|
|
88
|
+
? `; sample ${firstSample.layer}:${firstSample.kind}:${firstSample.id.slice(0, 8)}`
|
|
89
|
+
: "";
|
|
90
|
+
|
|
91
|
+
return warn(
|
|
92
|
+
"Scope inventory",
|
|
93
|
+
`${report.totalAnomalyCount} unresolved anomalies, ${report.totalInvalidCount} invalid file(s) across ${report.totalScannedCount} records${layerSummary ? ` (${layerSummary})` : ""}${sampleSummary}`,
|
|
94
|
+
"bun run scope-inventory",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function workflowPatternSeedsPath(): string {
|
|
99
|
+
return resolve(import.meta.dir, "../eval/continuity/pattern-seeds.json");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function caseMemorySeedsPath(): string {
|
|
103
|
+
return resolve(import.meta.dir, "../eval/continuity/case-seeds.json");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function memorySeedsPath(): string {
|
|
107
|
+
return resolve(import.meta.dir, "../eval/continuity/memory-seeds.json");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseMetadata(raw?: string): Record<string, unknown> {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(raw || "{}");
|
|
113
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
114
|
+
? parsed as Record<string, unknown>
|
|
115
|
+
: {};
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeIdentityValue(value?: string): string {
|
|
122
|
+
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveSeedScope(scope?: string, source?: string): string {
|
|
126
|
+
return scope || `memory:${source || "agent"}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function workflowPatternIdentity(title: string, scope: string): string {
|
|
130
|
+
return `${scope.toLowerCase()}::${normalizeIdentityValue(title)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function caseMemoryIdentity(title: string, scope: string): string {
|
|
134
|
+
return `${scope.toLowerCase()}::${normalizeIdentityValue(title)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function storeMemoryIdentity(params: {
|
|
138
|
+
category: string;
|
|
139
|
+
scope: string;
|
|
140
|
+
text: string;
|
|
141
|
+
canonicalKey?: string;
|
|
142
|
+
}): string {
|
|
143
|
+
const identity = params.canonicalKey
|
|
144
|
+
? normalizeCanonicalKey(params.canonicalKey)
|
|
145
|
+
: normalizeIdentityValue(params.text);
|
|
146
|
+
return `${params.scope.toLowerCase()}::${params.category.toLowerCase()}::${identity}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractWorkflowPatternTitle(entry: MemoryEntry): string {
|
|
150
|
+
const metadata = parseMetadata(entry.metadata);
|
|
151
|
+
const workflowPattern = metadata.workflowPattern;
|
|
152
|
+
if (workflowPattern && typeof workflowPattern === "object" && typeof (workflowPattern as any).title === "string") {
|
|
153
|
+
return String((workflowPattern as any).title).trim();
|
|
154
|
+
}
|
|
155
|
+
const match = entry.text.match(/^Workflow pattern:\s*(.+)$/im);
|
|
156
|
+
return match?.[1]?.trim() || "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractCaseMemoryTitle(entry: MemoryEntry): string {
|
|
160
|
+
const metadata = parseMetadata(entry.metadata);
|
|
161
|
+
const caseMemory = metadata.caseMemory;
|
|
162
|
+
if (caseMemory && typeof caseMemory === "object" && typeof (caseMemory as any).title === "string") {
|
|
163
|
+
return String((caseMemory as any).title).trim();
|
|
164
|
+
}
|
|
165
|
+
const match = entry.text.match(/^Case:\s*(.+)$/im);
|
|
166
|
+
return match?.[1]?.trim() || "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeWorkflowPatternSeed(raw: unknown): WorkflowPatternInput {
|
|
170
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
171
|
+
? raw as Record<string, unknown>
|
|
172
|
+
: {};
|
|
173
|
+
return WorkflowPatternInputSchema.parse({
|
|
174
|
+
...record,
|
|
175
|
+
source: typeof record.source === "string" ? record.source : "agent",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeCaseMemorySeed(raw: unknown): CaseMemoryInput {
|
|
180
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
181
|
+
? raw as Record<string, unknown>
|
|
182
|
+
: {};
|
|
183
|
+
return CaseMemoryInputSchema.parse({
|
|
184
|
+
...record,
|
|
185
|
+
source: typeof record.source === "string" ? record.source : "agent",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeStoreMemorySeed(raw: unknown): StoreMemoryInput {
|
|
190
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
191
|
+
? raw as Record<string, unknown>
|
|
192
|
+
: {};
|
|
193
|
+
return StoreMemoryInputSchema.parse({
|
|
194
|
+
...record,
|
|
195
|
+
source: typeof record.source === "string" ? record.source : "agent",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function loadContinuityBaselineSeeds(): ContinuityBaselineSeeds {
|
|
200
|
+
const patternsRaw = JSON.parse(readFileSync(workflowPatternSeedsPath(), "utf-8")) as unknown;
|
|
201
|
+
const casesRaw = JSON.parse(readFileSync(caseMemorySeedsPath(), "utf-8")) as unknown;
|
|
202
|
+
const memoriesRaw = JSON.parse(readFileSync(memorySeedsPath(), "utf-8")) as unknown;
|
|
203
|
+
|
|
204
|
+
if (!Array.isArray(patternsRaw) || !Array.isArray(casesRaw) || !Array.isArray(memoriesRaw)) {
|
|
205
|
+
throw new Error("Continuity seed files must all be JSON arrays.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
patterns: patternsRaw.map((item) => normalizeWorkflowPatternSeed(item)),
|
|
210
|
+
cases: casesRaw.map((item) => normalizeCaseMemorySeed(item)),
|
|
211
|
+
memories: memoriesRaw.map((item) => normalizeStoreMemorySeed(item)),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function loadContinuityBaselineEntries(
|
|
216
|
+
store: ContinuityBaselineEntryStore,
|
|
217
|
+
totalCountHint: number,
|
|
218
|
+
): Promise<MemoryEntry[]> {
|
|
219
|
+
const limit = Number.isFinite(totalCountHint)
|
|
220
|
+
? Math.max(5000, Math.ceil(totalCountHint))
|
|
221
|
+
: 5000;
|
|
222
|
+
return store.list(undefined, undefined, limit, 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function seedPreview(text: string, maxLen = 80): string {
|
|
226
|
+
const compact = String(text || "").replace(/\s+/g, " ").trim();
|
|
227
|
+
return compact.length <= maxLen ? compact : `${compact.slice(0, maxLen - 3).trimEnd()}...`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function assessContinuityBaseline(
|
|
231
|
+
entries: MemoryEntry[],
|
|
232
|
+
seeds: ContinuityBaselineSeeds = loadContinuityBaselineSeeds(),
|
|
233
|
+
): ContinuityBaselineAssessment {
|
|
234
|
+
const patternKeys = new Set(
|
|
235
|
+
entries
|
|
236
|
+
.filter((entry) => entry.category === "patterns")
|
|
237
|
+
.map((entry) => {
|
|
238
|
+
const title = extractWorkflowPatternTitle(entry);
|
|
239
|
+
return title ? workflowPatternIdentity(title, entry.scope) : "";
|
|
240
|
+
})
|
|
241
|
+
.filter(Boolean),
|
|
242
|
+
);
|
|
243
|
+
const caseKeys = new Set(
|
|
244
|
+
entries
|
|
245
|
+
.filter((entry) => entry.category === "cases")
|
|
246
|
+
.map((entry) => {
|
|
247
|
+
const title = extractCaseMemoryTitle(entry);
|
|
248
|
+
return title ? caseMemoryIdentity(title, entry.scope) : "";
|
|
249
|
+
})
|
|
250
|
+
.filter(Boolean),
|
|
251
|
+
);
|
|
252
|
+
const memoryKeys = new Set(
|
|
253
|
+
entries.map((entry) => storeMemoryIdentity({
|
|
254
|
+
category: entry.category,
|
|
255
|
+
scope: entry.scope,
|
|
256
|
+
text: entry.text,
|
|
257
|
+
canonicalKey: extractCanonicalKey(entry.metadata) || undefined,
|
|
258
|
+
})),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const missingPatterns = seeds.patterns
|
|
262
|
+
.filter((seed) => !patternKeys.has(workflowPatternIdentity(seed.title, resolveSeedScope(seed.scope, seed.source))))
|
|
263
|
+
.map((seed) => seed.title);
|
|
264
|
+
const missingCases = seeds.cases
|
|
265
|
+
.filter((seed) => !caseKeys.has(caseMemoryIdentity(seed.title, resolveSeedScope(seed.scope, seed.source))))
|
|
266
|
+
.map((seed) => seed.title);
|
|
267
|
+
const missingMemories = seeds.memories
|
|
268
|
+
.filter((seed) => !memoryKeys.has(storeMemoryIdentity({
|
|
269
|
+
category: seed.category,
|
|
270
|
+
scope: resolveSeedScope(seed.scope, seed.source),
|
|
271
|
+
text: seed.text,
|
|
272
|
+
canonicalKey: seed.canonicalKey,
|
|
273
|
+
})))
|
|
274
|
+
.map((seed) => seedPreview(seed.canonicalKey || seed.text));
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
expected: {
|
|
278
|
+
patterns: seeds.patterns.length,
|
|
279
|
+
cases: seeds.cases.length,
|
|
280
|
+
memories: seeds.memories.length,
|
|
281
|
+
},
|
|
282
|
+
found: {
|
|
283
|
+
patterns: seeds.patterns.length - missingPatterns.length,
|
|
284
|
+
cases: seeds.cases.length - missingCases.length,
|
|
285
|
+
memories: seeds.memories.length - missingMemories.length,
|
|
286
|
+
},
|
|
287
|
+
missing: {
|
|
288
|
+
patterns: missingPatterns,
|
|
289
|
+
cases: missingCases,
|
|
290
|
+
memories: missingMemories,
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function runDoctor(options: { ci?: boolean } = {}): Promise<CheckResult[]> {
|
|
296
|
+
const results: CheckResult[] = [];
|
|
297
|
+
|
|
298
|
+
// 1. Bun runtime
|
|
299
|
+
const bunVersion = typeof Bun !== "undefined" ? Bun.version : null;
|
|
300
|
+
if (bunVersion) {
|
|
301
|
+
results.push(pass("Bun runtime", `v${bunVersion}`));
|
|
302
|
+
} else {
|
|
303
|
+
results.push(fail(
|
|
304
|
+
"Bun runtime",
|
|
305
|
+
"Bun not detected (running under Node?)",
|
|
306
|
+
"Install Bun: curl -fsSL https://bun.sh/install | bash"
|
|
307
|
+
));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. Config file
|
|
311
|
+
let configPath: string | null = null;
|
|
312
|
+
try {
|
|
313
|
+
configPath = findConfigPath();
|
|
314
|
+
results.push(pass("Config file", configPath));
|
|
315
|
+
} catch {
|
|
316
|
+
results.push(fail(
|
|
317
|
+
"Config file",
|
|
318
|
+
"config.json not found",
|
|
319
|
+
"cp config.json.example config.json (or set LOCAL_MEMORY_CONFIG env)"
|
|
320
|
+
));
|
|
321
|
+
return results; // can't continue without config
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 3. .env + JINA_API_KEY
|
|
325
|
+
loadDotEnv();
|
|
326
|
+
const jinaKey = process.env.JINA_API_KEY;
|
|
327
|
+
if (jinaKey && jinaKey !== "your_jina_api_key_here") {
|
|
328
|
+
results.push(pass("JINA_API_KEY", `set (${jinaKey.slice(0, 8)}...)`));
|
|
329
|
+
} else if (options.ci) {
|
|
330
|
+
results.push(warn(
|
|
331
|
+
"JINA_API_KEY",
|
|
332
|
+
jinaKey === "your_jina_api_key_here" ? "still placeholder value" : "not set (CI mode, skipped)",
|
|
333
|
+
));
|
|
334
|
+
} else {
|
|
335
|
+
results.push(fail(
|
|
336
|
+
"JINA_API_KEY",
|
|
337
|
+
jinaKey === "your_jina_api_key_here" ? "still placeholder value" : "not set",
|
|
338
|
+
"Get a free key at https://jina.ai/embeddings/ → paste into .env"
|
|
339
|
+
));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 4. Load config and check paths
|
|
343
|
+
let config;
|
|
344
|
+
try {
|
|
345
|
+
config = loadConfig();
|
|
346
|
+
results.push(pass("Config parse", "valid JSON"));
|
|
347
|
+
} catch (e: any) {
|
|
348
|
+
results.push(fail("Config parse", e.message));
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 5. LanceDB data directory
|
|
353
|
+
const dbPath = resolve(configPath ? join(configPath, "..") : process.cwd(), expandHome(config.dbPath));
|
|
354
|
+
try {
|
|
355
|
+
validateStoragePath(dbPath);
|
|
356
|
+
results.push(pass("Data directory", dbPath));
|
|
357
|
+
} catch (e: any) {
|
|
358
|
+
results.push(fail("Data directory", e.message));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 6. CC transcript path
|
|
362
|
+
const ccSource = config.sources?.cc;
|
|
363
|
+
if (ccSource) {
|
|
364
|
+
if (ccSource.path === "auto") {
|
|
365
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
366
|
+
if (existsSync(projectsDir)) {
|
|
367
|
+
results.push(pass("CC transcripts", `auto-detected: ${projectsDir}`));
|
|
368
|
+
} else {
|
|
369
|
+
results.push(warn(
|
|
370
|
+
"CC transcripts",
|
|
371
|
+
"~/.claude/projects/ not found (auto-detect will fail)",
|
|
372
|
+
`Set sources.cc.path in config.json, e.g.: "${join(homedir(), ".claude", "projects", "-Users-" + homedir().split("/").pop())}"`
|
|
373
|
+
));
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
const ccPath = expandHome(ccSource.path);
|
|
377
|
+
if (existsSync(ccPath)) {
|
|
378
|
+
results.push(pass("CC transcripts", ccPath));
|
|
379
|
+
} else {
|
|
380
|
+
results.push(fail("CC transcripts", `path not found: ${ccPath}`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 7. Codex sessions
|
|
386
|
+
const codexSource = config.sources?.codex;
|
|
387
|
+
if (codexSource) {
|
|
388
|
+
const codexPath = expandHome(codexSource.path);
|
|
389
|
+
if (existsSync(codexPath)) {
|
|
390
|
+
results.push(pass("Codex sessions", codexPath));
|
|
391
|
+
} else {
|
|
392
|
+
results.push(warn("Codex sessions", `path not found: ${codexPath} (optional)`));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 8. Gemini sessions (known limitation)
|
|
397
|
+
const geminiSource = config.sources?.gemini;
|
|
398
|
+
if (geminiSource) {
|
|
399
|
+
results.push(warn(
|
|
400
|
+
"Gemini sessions",
|
|
401
|
+
"Gemini CLI sessions are encrypted protobuf; ingestion not yet supported",
|
|
402
|
+
"This source will be skipped during ingest. No action needed."
|
|
403
|
+
));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 9. Jina API key validation (skip in CI mode)
|
|
407
|
+
if (!options.ci && jinaKey && jinaKey !== "your_jina_api_key_here") {
|
|
408
|
+
try {
|
|
409
|
+
const resolvedKey = resolveEnv(config.embedding.apiKey);
|
|
410
|
+
const embeddingConfig: EmbeddingConfig = {
|
|
411
|
+
provider: "openai-compatible",
|
|
412
|
+
apiKey: resolvedKey,
|
|
413
|
+
model: config.embedding.model,
|
|
414
|
+
baseURL: config.embedding.baseURL,
|
|
415
|
+
dimensions: config.embedding.dimensions,
|
|
416
|
+
taskQuery: config.embedding.taskQuery,
|
|
417
|
+
taskPassage: config.embedding.taskPassage,
|
|
418
|
+
};
|
|
419
|
+
const embedder = createEmbedder(embeddingConfig);
|
|
420
|
+
const testResult = await embedder.test();
|
|
421
|
+
|
|
422
|
+
if (testResult.success) {
|
|
423
|
+
results.push(pass("Embedding API", `${config.embedding.model} (${testResult.dimensions}d)`));
|
|
424
|
+
} else {
|
|
425
|
+
results.push(fail(
|
|
426
|
+
"Embedding API",
|
|
427
|
+
testResult.error || "test embedding failed",
|
|
428
|
+
"Check your JINA_API_KEY at https://jina.ai/embeddings/"
|
|
429
|
+
));
|
|
430
|
+
}
|
|
431
|
+
} catch (e: any) {
|
|
432
|
+
results.push(fail("Embedding API", e.message));
|
|
433
|
+
}
|
|
434
|
+
} else if (options.ci) {
|
|
435
|
+
results.push(warn("Embedding API", "skipped (CI mode)"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 10. Index stats (if data exists)
|
|
439
|
+
try {
|
|
440
|
+
const store = new MemoryStore({ dbPath, vectorDim: config.embedding.dimensions || 1024 });
|
|
441
|
+
const stats = await store.stats();
|
|
442
|
+
if (stats.totalCount > 0) {
|
|
443
|
+
const scopes = Object.entries(stats.scopeCounts)
|
|
444
|
+
.sort((a, b) => b[1] - a[1])
|
|
445
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
446
|
+
.join(", ");
|
|
447
|
+
results.push(pass("Index", `${stats.totalCount} entries (${scopes})`));
|
|
448
|
+
} else {
|
|
449
|
+
results.push(warn(
|
|
450
|
+
"Index",
|
|
451
|
+
"empty — run `lm ingest` to populate",
|
|
452
|
+
"lm ingest --source all"
|
|
453
|
+
));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const entries = await loadContinuityBaselineEntries(store, stats.totalCount);
|
|
458
|
+
const baseline = assessContinuityBaseline(entries);
|
|
459
|
+
const expectedTotal = baseline.expected.patterns + baseline.expected.cases + baseline.expected.memories;
|
|
460
|
+
const foundTotal = baseline.found.patterns + baseline.found.cases + baseline.found.memories;
|
|
461
|
+
const missingTotal = expectedTotal - foundTotal;
|
|
462
|
+
const summary = `patterns ${baseline.found.patterns}/${baseline.expected.patterns}, cases ${baseline.found.cases}/${baseline.expected.cases}, memories ${baseline.found.memories}/${baseline.expected.memories}`;
|
|
463
|
+
|
|
464
|
+
if (missingTotal === 0) {
|
|
465
|
+
results.push(pass("Continuity baseline", summary));
|
|
466
|
+
} else {
|
|
467
|
+
const missingPreview = [
|
|
468
|
+
...baseline.missing.patterns.slice(0, 1),
|
|
469
|
+
...baseline.missing.cases.slice(0, 1),
|
|
470
|
+
...baseline.missing.memories.slice(0, 1),
|
|
471
|
+
].join(" | ");
|
|
472
|
+
results.push(warn(
|
|
473
|
+
"Continuity baseline",
|
|
474
|
+
`${summary}; missing ${missingTotal} canonical seed(s)${missingPreview ? ` (${missingPreview})` : ""}`,
|
|
475
|
+
"bun run seed:continuity"
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
} catch (error: any) {
|
|
479
|
+
results.push(warn(
|
|
480
|
+
"Continuity baseline",
|
|
481
|
+
error?.message || "unable to inspect canonical continuity seeds",
|
|
482
|
+
"bun run seed:continuity"
|
|
483
|
+
));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const report = await collectScopeInventory({
|
|
488
|
+
store,
|
|
489
|
+
sampleLimit: 5,
|
|
490
|
+
});
|
|
491
|
+
results.push(assessScopeInventoryReport(report));
|
|
492
|
+
} catch (error: any) {
|
|
493
|
+
results.push(warn(
|
|
494
|
+
"Scope inventory",
|
|
495
|
+
error?.message || "unable to audit scoped records across memories, pins, checkpoints, and workflow observations",
|
|
496
|
+
"bun run scope-inventory",
|
|
497
|
+
));
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
results.push(warn("Index", "not yet created (will be created on first ingest)"));
|
|
501
|
+
results.push(warn(
|
|
502
|
+
"Continuity baseline",
|
|
503
|
+
"not yet seeded",
|
|
504
|
+
"bun run seed:continuity"
|
|
505
|
+
));
|
|
506
|
+
results.push(warn(
|
|
507
|
+
"Scope inventory",
|
|
508
|
+
"not yet audited",
|
|
509
|
+
"bun run scope-inventory"
|
|
510
|
+
));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return results;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function formatDoctorResults(results: CheckResult[]): string {
|
|
517
|
+
const lines: string[] = ["\n RecallNest Doctor\n"];
|
|
518
|
+
|
|
519
|
+
const icons = { pass: " ✅", fail: " ❌", warn: " ⚠️ " };
|
|
520
|
+
let hasFailure = false;
|
|
521
|
+
let hasWarning = false;
|
|
522
|
+
|
|
523
|
+
for (const r of results) {
|
|
524
|
+
lines.push(`${icons[r.status]} ${r.name}: ${r.message}`);
|
|
525
|
+
if (r.fix) {
|
|
526
|
+
lines.push(` → ${r.fix}`);
|
|
527
|
+
}
|
|
528
|
+
if (r.status === "fail") hasFailure = true;
|
|
529
|
+
if (r.status === "warn") hasWarning = true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
lines.push("");
|
|
533
|
+
if (hasFailure) {
|
|
534
|
+
lines.push(" Fix the ❌ items above before running `lm ingest`.");
|
|
535
|
+
} else if (hasWarning) {
|
|
536
|
+
lines.push(" Review the ⚠️ items above before relying on this environment as a clean baseline.");
|
|
537
|
+
} else {
|
|
538
|
+
lines.push(" All clear. Run `lm ingest --source all` to get started.");
|
|
539
|
+
}
|
|
540
|
+
lines.push("");
|
|
541
|
+
|
|
542
|
+
return lines.join("\n");
|
|
543
|
+
}
|