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/cli.ts
ADDED
|
@@ -0,0 +1,1917 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* RecallNest CLI — MCP-native memory search and distillation
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* lm search "query keywords"
|
|
7
|
+
* lm ingest --source cc --limit 5
|
|
8
|
+
* lm ingest --source all
|
|
9
|
+
* lm stats
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
|
|
17
|
+
import type { RetrievalResult } from "./retriever.js";
|
|
18
|
+
import { applyRetrievalProfile, listRetrievalProfiles } from "./retrieval-profiles.js";
|
|
19
|
+
import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
20
|
+
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, getExportsDir, listDirtyBriefAssets, listMemoryAssets, listPinAssets, pinSummaryLine, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
|
|
21
|
+
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
|
|
22
|
+
import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, type LocalMemoryConfig } from "./runtime-config.js";
|
|
23
|
+
import {
|
|
24
|
+
formatDedupReasonSummary,
|
|
25
|
+
getDedupSkipRate,
|
|
26
|
+
type IngestResult,
|
|
27
|
+
ingestCCTranscripts,
|
|
28
|
+
ingestCodexSessions,
|
|
29
|
+
ingestGeminiSessions,
|
|
30
|
+
ingestMarkdownFiles,
|
|
31
|
+
} from "./ingest.js";
|
|
32
|
+
import { runDoctor, formatDoctorResults } from "./doctor.js";
|
|
33
|
+
import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
|
|
34
|
+
import {
|
|
35
|
+
CaseMemoryInputSchema,
|
|
36
|
+
type CaseMemoryInput,
|
|
37
|
+
StoreMemoryInputSchema,
|
|
38
|
+
type StoreMemoryInput,
|
|
39
|
+
WorkflowPatternInputSchema,
|
|
40
|
+
type WorkflowPatternInput,
|
|
41
|
+
} from "./memory-schema.js";
|
|
42
|
+
import { classifyLegacyScope, type MemoryEntry } from "./store.js";
|
|
43
|
+
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
44
|
+
import { resolveConflictCandidate } from "./conflict-engine.js";
|
|
45
|
+
import { escalateConflicts } from "./conflict-escalation.js";
|
|
46
|
+
import { ConflictCandidateStore } from "./conflict-store.js";
|
|
47
|
+
import { formatConflictAudit, formatConflictAuditMarkdown, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
|
|
48
|
+
import { CONFLICT_ATTENTION_LEVELS, parseConflictAttention, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
|
|
49
|
+
import { buildConflictAuditSummary, clusterConflicts, summarizeConflictAdvice } from "./conflict-advisor.js";
|
|
50
|
+
import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.js";
|
|
51
|
+
import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
|
|
52
|
+
import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
|
|
53
|
+
import { WorkflowObservationStore } from "./workflow-observation-store.js";
|
|
54
|
+
import {
|
|
55
|
+
applyWorkflowObservationScopeSuggestions,
|
|
56
|
+
formatWorkflowObservationScopeReview,
|
|
57
|
+
keepWorkflowObservationScopesGlobal,
|
|
58
|
+
parseWorkflowObservationScopeCue,
|
|
59
|
+
reviewWorkflowObservationScopes,
|
|
60
|
+
} from "./workflow-observation-scope-review.js";
|
|
61
|
+
import { collectScopeInventory, formatScopeInventoryReport } from "./scope-inventory.js";
|
|
62
|
+
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Auto-detect Claude Code transcript directory.
|
|
66
|
+
* Scans ~/.claude/projects/ for subdirectories containing .jsonl files.
|
|
67
|
+
*/
|
|
68
|
+
function detectCCPath(): string | null {
|
|
69
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
70
|
+
if (!existsSync(projectsDir)) return null;
|
|
71
|
+
try {
|
|
72
|
+
const dirs = readdirSync(projectsDir, { withFileTypes: true })
|
|
73
|
+
.filter(d => d.isDirectory())
|
|
74
|
+
.map(d => join(projectsDir, d.name));
|
|
75
|
+
// Pick the directory with most .jsonl files
|
|
76
|
+
let best = "";
|
|
77
|
+
let bestCount = 0;
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
const jsonlCount = readdirSync(dir).filter(f => f.endsWith(".jsonl")).length;
|
|
80
|
+
if (jsonlCount > bestCount) {
|
|
81
|
+
bestCount = jsonlCount;
|
|
82
|
+
best = dir;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return best || null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve "auto" paths in config to actual directories.
|
|
93
|
+
*/
|
|
94
|
+
function resolveSourcePath(source: string, key: string): string {
|
|
95
|
+
if (source !== "auto") return expandHome(source);
|
|
96
|
+
|
|
97
|
+
if (key === "cc") {
|
|
98
|
+
const detected = detectCCPath();
|
|
99
|
+
if (!detected) throw new Error("Could not auto-detect Claude Code transcript path. Set sources.cc.path in config.json.");
|
|
100
|
+
return detected;
|
|
101
|
+
}
|
|
102
|
+
if (key === "memory") {
|
|
103
|
+
const ccPath = detectCCPath();
|
|
104
|
+
if (!ccPath) throw new Error("Could not auto-detect memory path. Set sources.memory.path in config.json.");
|
|
105
|
+
return join(ccPath, "memory");
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Cannot auto-detect path for source "${key}". Set it manually in config.json.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatIngestSummary(result: IngestResult): string {
|
|
111
|
+
return `${result.filesProcessed} files, ${result.chunksIngested} ingested, ${result.chunksDeduped} deduped (${formatDedupReasonSummary(result)}), ${result.errors.length} errors`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function maybeWarnHighCCDedupRate(result: IngestResult): void {
|
|
115
|
+
if (result.source !== "cc") return;
|
|
116
|
+
const dedupRate = getDedupSkipRate(result);
|
|
117
|
+
if (dedupRate <= 0.85) return;
|
|
118
|
+
console.log(` ⚠️ dedup 率异常: ${(dedupRate * 100).toFixed(1)}%`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// CLI
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
const program = new Command();
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.name("recallnest")
|
|
129
|
+
.description("本地优先 AI 对话记忆搜索与蒸馏层")
|
|
130
|
+
.version("1.4.0");
|
|
131
|
+
|
|
132
|
+
async function runRetrievalView(
|
|
133
|
+
view: "search" | "explain" | "distill",
|
|
134
|
+
query: string,
|
|
135
|
+
options: {
|
|
136
|
+
limit?: string;
|
|
137
|
+
scope?: string;
|
|
138
|
+
sessionId?: string;
|
|
139
|
+
allScopes?: boolean;
|
|
140
|
+
json?: boolean;
|
|
141
|
+
profile?: string;
|
|
142
|
+
explain?: boolean;
|
|
143
|
+
trace?: boolean;
|
|
144
|
+
},
|
|
145
|
+
) {
|
|
146
|
+
const config = loadConfig();
|
|
147
|
+
const { retriever, profile } = createComponents(config, options.profile);
|
|
148
|
+
|
|
149
|
+
const limit = parseLimitOption(options.limit, 5, 1, 20);
|
|
150
|
+
|
|
151
|
+
// Build trace collector when --trace is set
|
|
152
|
+
let trace: import("./retrieval-trace.js").TraceCollector | undefined;
|
|
153
|
+
if (options.trace) {
|
|
154
|
+
const { TraceCollector } = await import("./retrieval-trace.js");
|
|
155
|
+
trace = new TraceCollector();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const results = await retriever.retrieve(buildRetrievalContext({
|
|
159
|
+
query,
|
|
160
|
+
limit,
|
|
161
|
+
scope: options.scope,
|
|
162
|
+
sessionId: options.sessionId,
|
|
163
|
+
allScopes: Boolean(options.allScopes),
|
|
164
|
+
source: "cli",
|
|
165
|
+
trace,
|
|
166
|
+
}, {
|
|
167
|
+
operation: `cli:${view}`,
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
if (view === "search" && options.json) {
|
|
171
|
+
console.log(
|
|
172
|
+
JSON.stringify(
|
|
173
|
+
{
|
|
174
|
+
query,
|
|
175
|
+
profile: profile.name,
|
|
176
|
+
results: results.map((r) => ({
|
|
177
|
+
score: r.score,
|
|
178
|
+
scope: r.entry.scope,
|
|
179
|
+
text: r.entry.text,
|
|
180
|
+
metadata: r.entry.metadata,
|
|
181
|
+
retrievalPath: Object.keys(r.sources).filter((key) => Boolean((r.sources as any)[key])),
|
|
182
|
+
})),
|
|
183
|
+
},
|
|
184
|
+
null,
|
|
185
|
+
2,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Print trace summary before results when --trace is set
|
|
192
|
+
if (trace) {
|
|
193
|
+
const mode = retriever.getConfig().mode === "vector" ? "vector" as const : "hybrid" as const;
|
|
194
|
+
console.log(trace.summarize(query, mode));
|
|
195
|
+
console.log(""); // blank separator
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const context = { query, profile: profile.name };
|
|
199
|
+
const rendered = view === "explain"
|
|
200
|
+
? formatExplainResults(results, context)
|
|
201
|
+
: view === "distill"
|
|
202
|
+
? distillResults(results, context)
|
|
203
|
+
: formatSearchResults(results, context);
|
|
204
|
+
|
|
205
|
+
console.log(rendered);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseLimitOption(value: string | undefined, fallback: number, min = 1, max = 50): number {
|
|
209
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
210
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
211
|
+
return Math.min(max, Math.max(min, parsed));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseRequiredLimitOption(value: string | undefined, field: string, min = 1, max = 50): number {
|
|
215
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
216
|
+
if (!Number.isFinite(parsed)) {
|
|
217
|
+
throw new Error(`${field} must be an integer`);
|
|
218
|
+
}
|
|
219
|
+
return Math.min(max, Math.max(min, parsed));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function toScopeFilter(scope?: string): string[] | undefined {
|
|
223
|
+
const trimmed = typeof scope === "string" ? scope.trim() : "";
|
|
224
|
+
return trimmed ? [trimmed] : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function errorMessage(error: unknown): string {
|
|
228
|
+
return error instanceof Error ? error.message : String(error);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseExportFormat(value: string | undefined, fallback: "md" | "json" = "md"): "md" | "json" {
|
|
232
|
+
if (!value) return fallback;
|
|
233
|
+
if (value === "md" || value === "json") return value;
|
|
234
|
+
throw new Error(`Invalid export format: ${value}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function splitCsvOption(value?: string): string[] {
|
|
238
|
+
return String(value || "")
|
|
239
|
+
.split(",")
|
|
240
|
+
.map((item) => item.trim())
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function defaultConflictAuditExportPath(format: "md" | "json", canonicalKey?: string): string {
|
|
245
|
+
const timestamp = new Date().toISOString().replace(/[:]/g, "-").replace(/\..+/, "");
|
|
246
|
+
const stem = canonicalKey
|
|
247
|
+
? canonicalKey
|
|
248
|
+
.toLowerCase()
|
|
249
|
+
.replace(/[^a-z0-9\p{Script=Han}]+/giu, "-")
|
|
250
|
+
.replace(/^-+|-+$/g, "")
|
|
251
|
+
.slice(0, 48) || "conflict-audit"
|
|
252
|
+
: "conflict-audit";
|
|
253
|
+
return join(getExportsDir(), `${timestamp}-${stem}.${format}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseConflictStatusOption(value?: string): "open" | "accepted-incoming" | "kept-existing" | undefined {
|
|
257
|
+
if (!value) return undefined;
|
|
258
|
+
const parsed = ConflictStatusSchema.safeParse(value);
|
|
259
|
+
if (!parsed.success) {
|
|
260
|
+
throw new Error(`Invalid conflict status: ${value}`);
|
|
261
|
+
}
|
|
262
|
+
return parsed.data;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function filterConflictsByAttention<T extends { status: string; createdAt: string; updatedAt: string; reopenCount?: number; lastReopenedAt?: string }>(
|
|
266
|
+
records: T[],
|
|
267
|
+
attention?: string,
|
|
268
|
+
staleOnly?: boolean,
|
|
269
|
+
): T[] {
|
|
270
|
+
if (!attention && !staleOnly) return records;
|
|
271
|
+
return records.filter((record) => {
|
|
272
|
+
const lifecycle = summarizeConflictLifecycle(record as any);
|
|
273
|
+
if (staleOnly) {
|
|
274
|
+
return lifecycle.attention === "stale" || lifecycle.attention === "escalated";
|
|
275
|
+
}
|
|
276
|
+
return lifecycle.attention === attention;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function workflowPatternSeedsPath(file?: string): string {
|
|
281
|
+
return file
|
|
282
|
+
? resolve(file)
|
|
283
|
+
: resolve(import.meta.dir, "../eval/continuity/pattern-seeds.json");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function caseMemorySeedsPath(file?: string): string {
|
|
287
|
+
return file
|
|
288
|
+
? resolve(file)
|
|
289
|
+
: resolve(import.meta.dir, "../eval/continuity/case-seeds.json");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function memorySeedsPath(file?: string): string {
|
|
293
|
+
return file
|
|
294
|
+
? resolve(file)
|
|
295
|
+
: resolve(import.meta.dir, "../eval/continuity/memory-seeds.json");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseMetadata(raw?: string): Record<string, unknown> {
|
|
299
|
+
try {
|
|
300
|
+
return JSON.parse(raw || "{}") as Record<string, unknown>;
|
|
301
|
+
} catch {
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function extractWorkflowPatternTitle(entry: MemoryEntry): string {
|
|
307
|
+
const metadata = parseMetadata(entry.metadata);
|
|
308
|
+
const workflowPattern = metadata.workflowPattern;
|
|
309
|
+
if (workflowPattern && typeof workflowPattern === "object" && typeof (workflowPattern as any).title === "string") {
|
|
310
|
+
return String((workflowPattern as any).title).trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const match = entry.text.match(/^Workflow pattern:\s*(.+)$/im);
|
|
314
|
+
return match?.[1]?.trim() || "";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractCaseMemoryTitle(entry: MemoryEntry): string {
|
|
318
|
+
const metadata = parseMetadata(entry.metadata);
|
|
319
|
+
const caseMemory = metadata.caseMemory;
|
|
320
|
+
if (caseMemory && typeof caseMemory === "object" && typeof (caseMemory as any).title === "string") {
|
|
321
|
+
return String((caseMemory as any).title).trim();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const match = entry.text.match(/^Case:\s*(.+)$/im);
|
|
325
|
+
return match?.[1]?.trim() || "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function workflowPatternIdentity(title: string, scope: string): string {
|
|
329
|
+
return `${scope.toLowerCase()}::${title.trim().toLowerCase()}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function caseMemoryIdentity(title: string, scope: string): string {
|
|
333
|
+
return `${scope.toLowerCase()}::${title.trim().toLowerCase()}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeSeedIdentityValue(value?: string): string {
|
|
337
|
+
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function extractStoreMemoryCanonicalKey(entry: MemoryEntry): string {
|
|
341
|
+
const metadata = parseMetadata(entry.metadata);
|
|
342
|
+
return typeof metadata.canonicalKey === "string" ? metadata.canonicalKey.trim() : "";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function storeMemoryIdentity(params: {
|
|
346
|
+
category: string;
|
|
347
|
+
scope: string;
|
|
348
|
+
text: string;
|
|
349
|
+
canonicalKey?: string;
|
|
350
|
+
}): string {
|
|
351
|
+
const identity = normalizeSeedIdentityValue(params.canonicalKey) || normalizeSeedIdentityValue(params.text);
|
|
352
|
+
return `${params.scope.toLowerCase()}::${params.category.toLowerCase()}::${identity}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeWorkflowPatternSeed(raw: unknown, defaults: {
|
|
356
|
+
scope?: string;
|
|
357
|
+
source?: string;
|
|
358
|
+
}): WorkflowPatternInput {
|
|
359
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
360
|
+
? raw as Record<string, unknown>
|
|
361
|
+
: {};
|
|
362
|
+
return WorkflowPatternInputSchema.parse({
|
|
363
|
+
...record,
|
|
364
|
+
scope: typeof record.scope === "string" ? record.scope : defaults.scope,
|
|
365
|
+
source: typeof record.source === "string" ? record.source : defaults.source,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeCaseMemorySeed(raw: unknown, defaults: {
|
|
370
|
+
scope?: string;
|
|
371
|
+
source?: string;
|
|
372
|
+
}): CaseMemoryInput {
|
|
373
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
374
|
+
? raw as Record<string, unknown>
|
|
375
|
+
: {};
|
|
376
|
+
return CaseMemoryInputSchema.parse({
|
|
377
|
+
...record,
|
|
378
|
+
scope: typeof record.scope === "string" ? record.scope : defaults.scope,
|
|
379
|
+
source: typeof record.source === "string" ? record.source : defaults.source,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeStoreMemorySeed(raw: unknown, defaults: {
|
|
384
|
+
scope?: string;
|
|
385
|
+
source?: string;
|
|
386
|
+
}): StoreMemoryInput {
|
|
387
|
+
const record = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
388
|
+
? raw as Record<string, unknown>
|
|
389
|
+
: {};
|
|
390
|
+
return StoreMemoryInputSchema.parse({
|
|
391
|
+
...record,
|
|
392
|
+
scope: typeof record.scope === "string" ? record.scope : defaults.scope,
|
|
393
|
+
source: typeof record.source === "string" ? record.source : defaults.source,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function seedTextPreview(text: string, maxLen = 72): string {
|
|
398
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
399
|
+
return compact.length <= maxLen ? compact : `${compact.slice(0, maxLen - 3).trimEnd()}...`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function formatLegacyScope(value: string | null): string {
|
|
403
|
+
const kind = classifyLegacyScope(value);
|
|
404
|
+
if (kind === "missing") return "<missing>";
|
|
405
|
+
if (kind === "empty") return "<empty>";
|
|
406
|
+
return value ?? "<missing>";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function collectRepeatedOption(value: string, previous: string[] = []): string[] {
|
|
410
|
+
return [...previous, value];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
|
|
414
|
+
if (!entry) {
|
|
415
|
+
throw new Error("Memory entry not found.");
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
entry,
|
|
419
|
+
score: entry.importance || 0.7,
|
|
420
|
+
sources: {
|
|
421
|
+
fused: { score: entry.importance || 0.7 },
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─── search ──────────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
program
|
|
429
|
+
.command("search <query>")
|
|
430
|
+
.description("搜索记忆(支持 profile / explain)")
|
|
431
|
+
.option("-n, --limit <n>", "返回结果数", "5")
|
|
432
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
433
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
434
|
+
.option("--all-scopes", "显式允许跨 scope 搜索")
|
|
435
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "default")
|
|
436
|
+
.option("--explain", "显示命中原因与检索路径")
|
|
437
|
+
.option("--trace", "显示检索管道各阶段 trace(调试用)")
|
|
438
|
+
.option("--json", "JSON 格式输出")
|
|
439
|
+
.action(async (query: string, options) => {
|
|
440
|
+
await runRetrievalView(options.explain ? "explain" : "search", query, options);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
program
|
|
444
|
+
.command("explain <query>")
|
|
445
|
+
.description("解释为什么召回这些记忆")
|
|
446
|
+
.option("-n, --limit <n>", "返回结果数", "5")
|
|
447
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
448
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
449
|
+
.option("--all-scopes", "显式允许跨 scope 搜索")
|
|
450
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "default")
|
|
451
|
+
.action(async (query: string, options) => {
|
|
452
|
+
await runRetrievalView("explain", query, options);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
program
|
|
456
|
+
.command("distill <query>")
|
|
457
|
+
.description("把命中结果蒸馏成可复用 briefing")
|
|
458
|
+
.option("-n, --limit <n>", "返回结果数", "8")
|
|
459
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
460
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
461
|
+
.option("--all-scopes", "显式允许跨 scope 搜索")
|
|
462
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "writing")
|
|
463
|
+
.action(async (query: string, options) => {
|
|
464
|
+
await runRetrievalView("distill", query, options);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
program
|
|
468
|
+
.command("profiles")
|
|
469
|
+
.description("显示可用检索画像")
|
|
470
|
+
.action(() => {
|
|
471
|
+
const rows = [
|
|
472
|
+
"Profile Label What It Optimizes",
|
|
473
|
+
"----------- -------------- -----------------------------------------------",
|
|
474
|
+
];
|
|
475
|
+
for (const profile of listRetrievalProfiles()) {
|
|
476
|
+
rows.push(
|
|
477
|
+
`${profile.name.padEnd(11)} ${profile.label.padEnd(14)} ${profile.description}`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
console.log(rows.join("\n"));
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
program
|
|
484
|
+
.command("pin <memoryId>")
|
|
485
|
+
.description("把一条命中记忆提升为 pinned asset")
|
|
486
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
487
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
488
|
+
.option("--all-scopes", "显式允许跨 scope 读取")
|
|
489
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "default")
|
|
490
|
+
.option("-q, --query <query>", "记录这条记忆来自哪个查询")
|
|
491
|
+
.option("-t, --title <title>", "自定义 asset 标题")
|
|
492
|
+
.option("--summary <summary>", "自定义 asset 摘要")
|
|
493
|
+
.action(async (memoryId: string, options) => {
|
|
494
|
+
const config = loadConfig();
|
|
495
|
+
const { store, embedder } = createComponents(config, options.profile);
|
|
496
|
+
const scopeSelection = resolveScopeSelection({
|
|
497
|
+
scope: options.scope,
|
|
498
|
+
sessionId: options.sessionId,
|
|
499
|
+
allScopes: Boolean(options.allScopes),
|
|
500
|
+
operation: "cli:pin",
|
|
501
|
+
});
|
|
502
|
+
const entry = await store.get(memoryId, scopeSelection.scopeFilter);
|
|
503
|
+
if (!entry) {
|
|
504
|
+
console.log(`Memory not found: ${memoryId}`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const pinnedImportance = Math.max(entry.importance || 0.7, 0.95);
|
|
509
|
+
await store.update(entry.id, { importance: pinnedImportance }, scopeSelection.scopeFilter);
|
|
510
|
+
|
|
511
|
+
const asset = buildPinAsset(entryToRetrievalResult(entry), {
|
|
512
|
+
title: options.title,
|
|
513
|
+
summary: options.summary,
|
|
514
|
+
query: options.query,
|
|
515
|
+
profile: options.profile,
|
|
516
|
+
});
|
|
517
|
+
const path = savePinAsset(asset);
|
|
518
|
+
await indexPinnedAsset(store, embedder, asset);
|
|
519
|
+
|
|
520
|
+
console.log([
|
|
521
|
+
`Pinned : ${asset.id.slice(0, 8)}`,
|
|
522
|
+
`Memory : ${entry.id.slice(0, 8)} (${entry.scope})`,
|
|
523
|
+
`Title : ${asset.title}`,
|
|
524
|
+
`Path : ${path}`,
|
|
525
|
+
].join("\n"));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
program
|
|
529
|
+
.command("brief <query>")
|
|
530
|
+
.description("把一组召回结果沉淀成 structured memory brief")
|
|
531
|
+
.option("-n, --limit <n>", "返回结果数", "8")
|
|
532
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
533
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
534
|
+
.option("--all-scopes", "显式允许跨 scope 搜索")
|
|
535
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "writing")
|
|
536
|
+
.option("-t, --title <title>", "自定义 brief 标题")
|
|
537
|
+
.action(async (query: string, options) => {
|
|
538
|
+
const config = loadConfig();
|
|
539
|
+
const { retriever, profile, store, embedder } = createComponents(config, options.profile);
|
|
540
|
+
const limit = parseLimitOption(options.limit, 8, 1, 20);
|
|
541
|
+
const results = await retriever.retrieve(buildRetrievalContext({
|
|
542
|
+
query,
|
|
543
|
+
limit,
|
|
544
|
+
scope: options.scope,
|
|
545
|
+
sessionId: options.sessionId,
|
|
546
|
+
allScopes: Boolean(options.allScopes),
|
|
547
|
+
source: "cli",
|
|
548
|
+
}, {
|
|
549
|
+
operation: "cli:brief",
|
|
550
|
+
}));
|
|
551
|
+
if (results.length === 0) {
|
|
552
|
+
console.log("No results found.");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const briefSeedResults = selectBriefSeedResults(results);
|
|
557
|
+
const summary = summarizeResults(briefSeedResults, { query, profile: profile.name });
|
|
558
|
+
const asset = buildBriefAsset(summary, { title: options.title });
|
|
559
|
+
const path = saveBriefAsset(asset);
|
|
560
|
+
await indexAsset(store, embedder, asset);
|
|
561
|
+
|
|
562
|
+
console.log([
|
|
563
|
+
`Brief : ${asset.id.slice(0, 8)}`,
|
|
564
|
+
`Title : ${asset.title}`,
|
|
565
|
+
`Hits : ${asset.hits}`,
|
|
566
|
+
`Path : ${path}`,
|
|
567
|
+
].join("\n"));
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
program
|
|
571
|
+
.command("pins")
|
|
572
|
+
.description("列出最近的 pinned assets")
|
|
573
|
+
.option("-n, --limit <n>", "返回结果数", "10")
|
|
574
|
+
.action((options) => {
|
|
575
|
+
const limit = parseLimitOption(options.limit, 10, 1, 50);
|
|
576
|
+
const rows = listPinAssets(limit);
|
|
577
|
+
if (rows.length === 0) {
|
|
578
|
+
console.log("No pinned assets yet.");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log("Pin ID Title Scope Date");
|
|
582
|
+
console.log("-------- ----- ----- ----------");
|
|
583
|
+
for (const row of rows) {
|
|
584
|
+
console.log(pinSummaryLine(row));
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
program
|
|
589
|
+
.command("assets")
|
|
590
|
+
.description("列出最近的 structured memory assets")
|
|
591
|
+
.option("-n, --limit <n>", "返回结果数", "12")
|
|
592
|
+
.action((options) => {
|
|
593
|
+
const limit = parseLimitOption(options.limit, 12, 1, 50);
|
|
594
|
+
const rows = listMemoryAssets(limit);
|
|
595
|
+
if (rows.length === 0) {
|
|
596
|
+
console.log("No assets yet.");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
console.log("Asset ID Kind Title Scope / Sources Date");
|
|
600
|
+
console.log("-------- ----- ----- --------------- ----------");
|
|
601
|
+
for (const row of rows) {
|
|
602
|
+
console.log(assetSummaryLine(row));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
program
|
|
607
|
+
.command("clean-briefs")
|
|
608
|
+
.description("归档旧规则生成的脏 brief,并从索引删除对应 asset scope")
|
|
609
|
+
.option("--apply", "执行清理;默认只预览")
|
|
610
|
+
.action(async (options) => {
|
|
611
|
+
const dirty = listDirtyBriefAssets();
|
|
612
|
+
if (dirty.length === 0) {
|
|
613
|
+
console.log("No dirty briefs found.");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!options.apply) {
|
|
618
|
+
console.log("Dirty briefs detected:");
|
|
619
|
+
for (const item of dirty) {
|
|
620
|
+
console.log(`${item.id.slice(0, 8)} ${item.title} [${item.scope}] ${item.reasons.join("; ")}`);
|
|
621
|
+
}
|
|
622
|
+
console.log("\nRun with --apply to archive files and delete their indexed asset scopes.");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const config = loadConfig();
|
|
627
|
+
const { store } = createComponents(config);
|
|
628
|
+
let archived = 0;
|
|
629
|
+
let deleted = 0;
|
|
630
|
+
|
|
631
|
+
for (const item of dirty) {
|
|
632
|
+
archiveDirtyBriefAsset(item);
|
|
633
|
+
archived += 1;
|
|
634
|
+
deleted += await store.bulkDelete([item.scope]);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log([
|
|
638
|
+
`Dirty briefs: ${dirty.length}`,
|
|
639
|
+
`Archived : ${archived}`,
|
|
640
|
+
`Index rows : ${deleted}`,
|
|
641
|
+
].join("\n"));
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
program
|
|
645
|
+
.command("export <query>")
|
|
646
|
+
.description("导出检索与蒸馏结果到 markdown/json")
|
|
647
|
+
.option("-n, --limit <n>", "返回结果数", "8")
|
|
648
|
+
.option("-s, --scope <scope>", "显式限定 scope")
|
|
649
|
+
.option("--session-id <sessionId>", "当前 sessionId;未传 scope 时会推断为 session:<id>")
|
|
650
|
+
.option("--all-scopes", "显式允许跨 scope 搜索")
|
|
651
|
+
.option("-p, --profile <profile>", "检索画像:default / writing / debug / fact-check", "writing")
|
|
652
|
+
.option("-f, --format <format>", "导出格式:md / json", "md")
|
|
653
|
+
.action(async (query: string, options) => {
|
|
654
|
+
const format = options.format === "json" ? "json" : "md";
|
|
655
|
+
const config = loadConfig();
|
|
656
|
+
const { retriever, profile } = createComponents(config, options.profile);
|
|
657
|
+
const limit = parseLimitOption(options.limit, 8, 1, 20);
|
|
658
|
+
const results = await retriever.retrieve(buildRetrievalContext({
|
|
659
|
+
query,
|
|
660
|
+
limit,
|
|
661
|
+
scope: options.scope,
|
|
662
|
+
sessionId: options.sessionId,
|
|
663
|
+
allScopes: Boolean(options.allScopes),
|
|
664
|
+
source: "cli",
|
|
665
|
+
}, {
|
|
666
|
+
operation: "cli:export",
|
|
667
|
+
}));
|
|
668
|
+
const summary = distillResults(results, { query, profile: profile.name });
|
|
669
|
+
const artifact = writeExportArtifact({
|
|
670
|
+
query,
|
|
671
|
+
profile: profile.name,
|
|
672
|
+
results,
|
|
673
|
+
summary,
|
|
674
|
+
format,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
console.log([
|
|
678
|
+
`Exported : ${artifact.id.slice(0, 8)}`,
|
|
679
|
+
`Format : ${artifact.format}`,
|
|
680
|
+
`Profile : ${artifact.profile}`,
|
|
681
|
+
`Path : ${artifact.outputPath}`,
|
|
682
|
+
].join("\n"));
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const conflictsCommand = program
|
|
686
|
+
.command("conflicts")
|
|
687
|
+
.description("查看和裁决 memory promotion conflicts");
|
|
688
|
+
|
|
689
|
+
conflictsCommand
|
|
690
|
+
.command("list")
|
|
691
|
+
.description("列出近期 conflict candidates")
|
|
692
|
+
.option("-n, --limit <n>", "返回结果数", "20")
|
|
693
|
+
.option("--status <status>", "状态过滤:open / accepted-incoming / kept-existing")
|
|
694
|
+
.option("--attention <attention>", `生命周期过滤:${CONFLICT_ATTENTION_LEVELS.join(" / ")}`)
|
|
695
|
+
.option("--stale", "只看 stale / escalated 的 open conflicts")
|
|
696
|
+
.option("-k, --canonical-key <canonicalKey>", "按 canonicalKey 过滤")
|
|
697
|
+
.option("--group-by <mode>", "输出模式:record / cluster", "record")
|
|
698
|
+
.option("--json", "JSON 格式输出")
|
|
699
|
+
.action(async (options) => {
|
|
700
|
+
const limit = parseLimitOption(options.limit, 20, 1, 50);
|
|
701
|
+
const status = parseConflictStatusOption(options.status) || (
|
|
702
|
+
options.attention || options.stale ? undefined : "open"
|
|
703
|
+
);
|
|
704
|
+
const attention = parseConflictAttention(options.attention);
|
|
705
|
+
if (options.attention && !attention) {
|
|
706
|
+
throw new Error(`Invalid attention level: ${options.attention}`);
|
|
707
|
+
}
|
|
708
|
+
const conflictStore = new ConflictCandidateStore();
|
|
709
|
+
const records = filterConflictsByAttention(await conflictStore.listRecent({
|
|
710
|
+
status,
|
|
711
|
+
canonicalKey: options.canonicalKey,
|
|
712
|
+
limit: Math.max(limit * 2, limit),
|
|
713
|
+
}), attention, Boolean(options.stale)).slice(0, limit);
|
|
714
|
+
const groupBy = options.groupBy === "cluster" ? "cluster" : "record";
|
|
715
|
+
|
|
716
|
+
if (options.json) {
|
|
717
|
+
if (groupBy === "cluster") {
|
|
718
|
+
const clusters = clusterConflicts(records);
|
|
719
|
+
console.log(JSON.stringify({
|
|
720
|
+
groupBy,
|
|
721
|
+
clusters,
|
|
722
|
+
count: clusters.length,
|
|
723
|
+
}, null, 2));
|
|
724
|
+
} else {
|
|
725
|
+
console.log(JSON.stringify({
|
|
726
|
+
groupBy,
|
|
727
|
+
conflicts: records.map((record) => ({
|
|
728
|
+
...record,
|
|
729
|
+
lifecycle: summarizeConflictLifecycle(record),
|
|
730
|
+
advice: summarizeConflictAdvice(record),
|
|
731
|
+
})),
|
|
732
|
+
count: records.length,
|
|
733
|
+
}, null, 2));
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
console.log(groupBy === "cluster" ? formatConflictClusters(clusterConflicts(records)) : formatConflictList(records));
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
conflictsCommand
|
|
742
|
+
.command("show <conflictId>")
|
|
743
|
+
.description("查看单条 conflict 的完整详情")
|
|
744
|
+
.option("--json", "JSON 格式输出")
|
|
745
|
+
.action(async (conflictId: string, options) => {
|
|
746
|
+
const conflictStore = new ConflictCandidateStore();
|
|
747
|
+
const record = await conflictStore.getById(conflictId);
|
|
748
|
+
if (!record) {
|
|
749
|
+
console.log(`Conflict not found: ${conflictId}`);
|
|
750
|
+
process.exitCode = 1;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (options.json) {
|
|
755
|
+
console.log(JSON.stringify({
|
|
756
|
+
...record,
|
|
757
|
+
lifecycle: summarizeConflictLifecycle(record),
|
|
758
|
+
advice: summarizeConflictAdvice(record),
|
|
759
|
+
}, null, 2));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
console.log(formatConflictRecord(record));
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
conflictsCommand
|
|
767
|
+
.command("audit")
|
|
768
|
+
.description("生成一份面向终端的 conflict audit 摘要,优先指出 stale / escalated clusters")
|
|
769
|
+
.option("-n, --limit <n>", "扫描的 conflict 记录数", "100")
|
|
770
|
+
.option("--top <n>", "展示的 priority cluster 数", "5")
|
|
771
|
+
.option("--status <status>", "状态过滤:open / accepted-incoming / kept-existing / merged")
|
|
772
|
+
.option("-k, --canonical-key <canonicalKey>", "按 canonicalKey 过滤")
|
|
773
|
+
.option("--export", "把 audit 报告导出到文件")
|
|
774
|
+
.option("--format <format>", "导出格式:md / json", "md")
|
|
775
|
+
.option("--output <path>", "导出文件路径;默认写到 data/exports")
|
|
776
|
+
.option("--json", "JSON 格式输出")
|
|
777
|
+
.action(async (options) => {
|
|
778
|
+
const limit = parseLimitOption(options.limit, 100, 1, 500);
|
|
779
|
+
const top = parseLimitOption(options.top, 5, 1, 20);
|
|
780
|
+
const status = parseConflictStatusOption(options.status);
|
|
781
|
+
const conflictStore = new ConflictCandidateStore();
|
|
782
|
+
const records = await conflictStore.listRecent({
|
|
783
|
+
status,
|
|
784
|
+
canonicalKey: options.canonicalKey,
|
|
785
|
+
limit,
|
|
786
|
+
});
|
|
787
|
+
const summary = buildConflictAuditSummary(records, top);
|
|
788
|
+
const exportRequested = Boolean(options.export || options.output);
|
|
789
|
+
|
|
790
|
+
if (options.json) {
|
|
791
|
+
const payload = {
|
|
792
|
+
generatedAt: new Date().toISOString(),
|
|
793
|
+
filters: {
|
|
794
|
+
status: status || "all",
|
|
795
|
+
canonicalKey: options.canonicalKey || "all",
|
|
796
|
+
limit,
|
|
797
|
+
top,
|
|
798
|
+
},
|
|
799
|
+
summary,
|
|
800
|
+
};
|
|
801
|
+
if (exportRequested) {
|
|
802
|
+
const format = parseExportFormat(options.format);
|
|
803
|
+
const outputPath = options.output
|
|
804
|
+
? resolve(options.output)
|
|
805
|
+
: defaultConflictAuditExportPath(format, options.canonicalKey);
|
|
806
|
+
const rendered = format === "json"
|
|
807
|
+
? JSON.stringify(payload, null, 2) + "\n"
|
|
808
|
+
: formatConflictAuditMarkdown(summary, {
|
|
809
|
+
generatedAt: payload.generatedAt,
|
|
810
|
+
limit,
|
|
811
|
+
top,
|
|
812
|
+
...(status ? { status } : {}),
|
|
813
|
+
...(options.canonicalKey ? { canonicalKey: options.canonicalKey } : {}),
|
|
814
|
+
});
|
|
815
|
+
writeFileSync(outputPath, rendered);
|
|
816
|
+
console.log(JSON.stringify({ ...payload, export: { format, outputPath } }, null, 2));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const rendered = formatConflictAudit(summary);
|
|
824
|
+
if (!exportRequested) {
|
|
825
|
+
console.log(rendered);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const format = parseExportFormat(options.format);
|
|
830
|
+
const generatedAt = new Date().toISOString();
|
|
831
|
+
const outputPath = options.output
|
|
832
|
+
? resolve(options.output)
|
|
833
|
+
: defaultConflictAuditExportPath(format, options.canonicalKey);
|
|
834
|
+
const exported = format === "json"
|
|
835
|
+
? JSON.stringify({
|
|
836
|
+
generatedAt,
|
|
837
|
+
filters: {
|
|
838
|
+
status: status || "all",
|
|
839
|
+
canonicalKey: options.canonicalKey || "all",
|
|
840
|
+
limit,
|
|
841
|
+
top,
|
|
842
|
+
},
|
|
843
|
+
summary,
|
|
844
|
+
}, null, 2) + "\n"
|
|
845
|
+
: formatConflictAuditMarkdown(summary, {
|
|
846
|
+
generatedAt,
|
|
847
|
+
limit,
|
|
848
|
+
top,
|
|
849
|
+
...(status ? { status } : {}),
|
|
850
|
+
...(options.canonicalKey ? { canonicalKey: options.canonicalKey } : {}),
|
|
851
|
+
});
|
|
852
|
+
writeFileSync(outputPath, exported);
|
|
853
|
+
console.log(`${rendered}\n\nExported: ${outputPath}`);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
conflictsCommand
|
|
857
|
+
.command("escalate")
|
|
858
|
+
.description("预览或应用 conflict aging / escalation policy")
|
|
859
|
+
.option("--attention <attention>", "仅处理 stale / escalated", "stale")
|
|
860
|
+
.option("-k, --canonical-key <canonicalKey>", "按 canonicalKey 过滤")
|
|
861
|
+
.option("-n, --limit <n>", "扫描的 open conflict 数", "100")
|
|
862
|
+
.option("--top <n>", "最多处理多少条 eligible conflict", "10")
|
|
863
|
+
.option("--apply", "真正写回 escalation metadata;默认只预览")
|
|
864
|
+
.option("--notes <notes>", "写回时附带备注")
|
|
865
|
+
.option("--json", "JSON 格式输出")
|
|
866
|
+
.action(async (options) => {
|
|
867
|
+
const attention = options.attention === "escalated"
|
|
868
|
+
? "escalated"
|
|
869
|
+
: options.attention === "stale"
|
|
870
|
+
? "stale"
|
|
871
|
+
: null;
|
|
872
|
+
if (!attention) {
|
|
873
|
+
throw new Error("Invalid escalation attention. Use stale or escalated.");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const conflictStore = new ConflictCandidateStore();
|
|
877
|
+
const result = await escalateConflicts({
|
|
878
|
+
conflictStore,
|
|
879
|
+
}, {
|
|
880
|
+
attention,
|
|
881
|
+
canonicalKey: options.canonicalKey,
|
|
882
|
+
limit: parseLimitOption(options.limit, 100, 1, 500),
|
|
883
|
+
top: parseLimitOption(options.top, 10, 1, 20),
|
|
884
|
+
apply: Boolean(options.apply),
|
|
885
|
+
notes: options.notes,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (options.json) {
|
|
889
|
+
console.log(JSON.stringify(result, null, 2));
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
console.log(formatConflictEscalation(result));
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
conflictsCommand
|
|
897
|
+
.command("resolve [conflictId]")
|
|
898
|
+
.description("解决一条或一批 conflict candidates")
|
|
899
|
+
.option("--keep-existing", "保留现有 durable memory")
|
|
900
|
+
.option("--accept-incoming", "接受 incoming promoted text")
|
|
901
|
+
.option("--merge", "合并 existing 与 incoming wording")
|
|
902
|
+
.option("--merged-text <text>", "merge 时可选覆盖默认 merge suggestion 的文本")
|
|
903
|
+
.option("--all", "批量处理符合过滤条件的 conflicts")
|
|
904
|
+
.option("--status <status>", "批量过滤状态:open / accepted-incoming / kept-existing / merged", "open")
|
|
905
|
+
.option("--attention <attention>", `批量过滤生命周期:${CONFLICT_ATTENTION_LEVELS.join(" / ")}`)
|
|
906
|
+
.option("--stale", "批量只处理 stale / escalated 的 open conflicts")
|
|
907
|
+
.option("-k, --canonical-key <canonicalKey>", "批量时按 canonicalKey 过滤")
|
|
908
|
+
.option("-n, --limit <n>", "批量处理上限", "20")
|
|
909
|
+
.option("--notes <notes>", "可选备注")
|
|
910
|
+
.option("--json", "JSON 格式输出")
|
|
911
|
+
.action(async (conflictId: string | undefined, options) => {
|
|
912
|
+
const keepExisting = Boolean(options.keepExisting);
|
|
913
|
+
const acceptIncoming = Boolean(options.acceptIncoming);
|
|
914
|
+
const merge = Boolean(options.merge);
|
|
915
|
+
const selectedCount = [keepExisting, acceptIncoming, merge].filter(Boolean).length;
|
|
916
|
+
if (selectedCount !== 1) {
|
|
917
|
+
throw new Error("Choose exactly one of --keep-existing, --accept-incoming, or --merge");
|
|
918
|
+
}
|
|
919
|
+
if (options.all && conflictId) {
|
|
920
|
+
throw new Error("Use either a conflictId or --all, not both");
|
|
921
|
+
}
|
|
922
|
+
if (!options.all && !conflictId) {
|
|
923
|
+
throw new Error("Provide a conflictId or use --all");
|
|
924
|
+
}
|
|
925
|
+
if (options.mergedText && !merge) {
|
|
926
|
+
throw new Error("--merged-text can only be used with --merge");
|
|
927
|
+
}
|
|
928
|
+
if (options.all && options.mergedText) {
|
|
929
|
+
throw new Error("--merged-text is only supported when resolving a single conflict");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const resolution = keepExisting
|
|
933
|
+
? "keep_existing"
|
|
934
|
+
: acceptIncoming
|
|
935
|
+
? "accept_incoming"
|
|
936
|
+
: "merge";
|
|
937
|
+
const config = loadConfig();
|
|
938
|
+
const { store, embedder } = createComponents(config);
|
|
939
|
+
const conflictStore = new ConflictCandidateStore();
|
|
940
|
+
const deps = { store, embedder, conflictStore };
|
|
941
|
+
|
|
942
|
+
if (options.all) {
|
|
943
|
+
const limit = parseLimitOption(options.limit, 20, 1, 100);
|
|
944
|
+
const status = parseConflictStatusOption(options.status) || "open";
|
|
945
|
+
const attention = parseConflictAttention(options.attention);
|
|
946
|
+
if (options.attention && !attention) {
|
|
947
|
+
throw new Error(`Invalid attention level: ${options.attention}`);
|
|
948
|
+
}
|
|
949
|
+
const records = filterConflictsByAttention(await conflictStore.listRecent({
|
|
950
|
+
status,
|
|
951
|
+
canonicalKey: options.canonicalKey,
|
|
952
|
+
limit: Math.max(limit * 2, limit),
|
|
953
|
+
}), attention, Boolean(options.stale)).slice(0, limit);
|
|
954
|
+
|
|
955
|
+
const results: Array<{ conflictId: string; status?: string; error?: string }> = [];
|
|
956
|
+
for (const record of records) {
|
|
957
|
+
try {
|
|
958
|
+
const resolved = await resolveConflictCandidate(deps, {
|
|
959
|
+
conflictId: record.conflictId,
|
|
960
|
+
resolution,
|
|
961
|
+
notes: options.notes,
|
|
962
|
+
});
|
|
963
|
+
results.push({ conflictId: resolved.conflictId, status: resolved.status });
|
|
964
|
+
} catch (error) {
|
|
965
|
+
results.push({ conflictId: record.conflictId, error: errorMessage(error) });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (options.json) {
|
|
970
|
+
console.log(JSON.stringify({
|
|
971
|
+
total: records.length,
|
|
972
|
+
resolved: results.filter((item) => item.status).length,
|
|
973
|
+
failed: results.filter((item) => item.error).length,
|
|
974
|
+
results,
|
|
975
|
+
}, null, 2));
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const lines = [
|
|
980
|
+
`Batch resolve: ${records.length} conflict(s)`,
|
|
981
|
+
`Resolution : ${resolution}`,
|
|
982
|
+
];
|
|
983
|
+
if (options.canonicalKey) {
|
|
984
|
+
lines.push(`Canonical : ${options.canonicalKey}`);
|
|
985
|
+
}
|
|
986
|
+
lines.push("");
|
|
987
|
+
for (const item of results) {
|
|
988
|
+
lines.push(
|
|
989
|
+
item.error
|
|
990
|
+
? `${item.conflictId.slice(0, 8)} ERROR ${item.error}`
|
|
991
|
+
: `${item.conflictId.slice(0, 8)} ${item.status}`,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
console.log(lines.join("\n"));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const result = await resolveConflictCandidate(deps, {
|
|
999
|
+
conflictId,
|
|
1000
|
+
resolution,
|
|
1001
|
+
...(options.mergedText ? { mergedText: options.mergedText } : {}),
|
|
1002
|
+
notes: options.notes,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
if (options.json) {
|
|
1006
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
console.log(formatConflictResolution(result));
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
program
|
|
1014
|
+
.command("seed-patterns [file]")
|
|
1015
|
+
.description("批量写入 continuity / workflow pattern seeds")
|
|
1016
|
+
.option("-s, --scope <scope>", "默认 scope(仅覆盖 seed 里未显式提供的项)")
|
|
1017
|
+
.option("--source <source>", "默认 source(manual / agent / api)", "agent")
|
|
1018
|
+
.option("--json", "JSON 输出")
|
|
1019
|
+
.action(async (file: string | undefined, options) => {
|
|
1020
|
+
const path = workflowPatternSeedsPath(file);
|
|
1021
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
1022
|
+
if (!Array.isArray(raw)) {
|
|
1023
|
+
throw new Error(`Pattern seeds file must be a JSON array: ${path}`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const config = loadConfig();
|
|
1027
|
+
const { store, embedder } = createComponents(config);
|
|
1028
|
+
const conflictStore = new ConflictCandidateStore();
|
|
1029
|
+
const existing = await store.list(undefined, "patterns", 1000, 0);
|
|
1030
|
+
const existingKeys = new Set(
|
|
1031
|
+
existing
|
|
1032
|
+
.map((entry) => {
|
|
1033
|
+
const title = extractWorkflowPatternTitle(entry);
|
|
1034
|
+
return title ? workflowPatternIdentity(title, entry.scope) : "";
|
|
1035
|
+
})
|
|
1036
|
+
.filter(Boolean),
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const stored: Array<{ title: string; id: string; scope: string }> = [];
|
|
1040
|
+
const skipped: Array<{ title: string; scope: string }> = [];
|
|
1041
|
+
|
|
1042
|
+
for (const seed of raw) {
|
|
1043
|
+
const input = normalizeWorkflowPatternSeed(seed, {
|
|
1044
|
+
scope: options.scope,
|
|
1045
|
+
source: options.source,
|
|
1046
|
+
});
|
|
1047
|
+
const scope = input.scope;
|
|
1048
|
+
const key = workflowPatternIdentity(input.title, scope);
|
|
1049
|
+
if (existingKeys.has(key)) {
|
|
1050
|
+
skipped.push({ title: input.title, scope });
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const record = await persistWorkflowPattern({ store, embedder, conflictStore }, input);
|
|
1055
|
+
existingKeys.add(key);
|
|
1056
|
+
stored.push({ title: record.title, id: record.id, scope: record.resolvedScope });
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (options.json) {
|
|
1060
|
+
console.log(JSON.stringify({ path, stored, skipped }, null, 2));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const lines = [
|
|
1065
|
+
`Pattern seeds: ${path}`,
|
|
1066
|
+
`Stored : ${stored.length}`,
|
|
1067
|
+
`Skipped : ${skipped.length}`,
|
|
1068
|
+
];
|
|
1069
|
+
|
|
1070
|
+
if (stored.length > 0) {
|
|
1071
|
+
lines.push("");
|
|
1072
|
+
lines.push("Stored patterns:");
|
|
1073
|
+
for (const item of stored) {
|
|
1074
|
+
lines.push(` ${item.id.slice(0, 8)} ${item.scope} ${item.title}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (skipped.length > 0) {
|
|
1079
|
+
lines.push("");
|
|
1080
|
+
lines.push("Skipped existing:");
|
|
1081
|
+
for (const item of skipped) {
|
|
1082
|
+
lines.push(` ${item.scope} ${item.title}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
console.log(lines.join("\n"));
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
program
|
|
1090
|
+
.command("seed-cases [file]")
|
|
1091
|
+
.description("批量写入 continuity / reusable case seeds")
|
|
1092
|
+
.option("-s, --scope <scope>", "默认 scope(仅覆盖 seed 里未显式提供的项)")
|
|
1093
|
+
.option("--source <source>", "默认 source(manual / agent / api)", "agent")
|
|
1094
|
+
.option("--json", "JSON 输出")
|
|
1095
|
+
.action(async (file: string | undefined, options) => {
|
|
1096
|
+
const path = caseMemorySeedsPath(file);
|
|
1097
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
1098
|
+
if (!Array.isArray(raw)) {
|
|
1099
|
+
throw new Error(`Case seeds file must be a JSON array: ${path}`);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const config = loadConfig();
|
|
1103
|
+
const { store, embedder } = createComponents(config);
|
|
1104
|
+
const conflictStore = new ConflictCandidateStore();
|
|
1105
|
+
const existing = await store.list(undefined, "cases", 1000, 0);
|
|
1106
|
+
const existingKeys = new Set(
|
|
1107
|
+
existing
|
|
1108
|
+
.map((entry) => {
|
|
1109
|
+
const title = extractCaseMemoryTitle(entry);
|
|
1110
|
+
return title ? caseMemoryIdentity(title, entry.scope) : "";
|
|
1111
|
+
})
|
|
1112
|
+
.filter(Boolean),
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
const stored: Array<{ title: string; id: string; scope: string }> = [];
|
|
1116
|
+
const skipped: Array<{ title: string; scope: string }> = [];
|
|
1117
|
+
|
|
1118
|
+
for (const seed of raw) {
|
|
1119
|
+
const input = normalizeCaseMemorySeed(seed, {
|
|
1120
|
+
scope: options.scope,
|
|
1121
|
+
source: options.source,
|
|
1122
|
+
});
|
|
1123
|
+
const scope = input.scope;
|
|
1124
|
+
const key = caseMemoryIdentity(input.title, scope);
|
|
1125
|
+
if (existingKeys.has(key)) {
|
|
1126
|
+
skipped.push({ title: input.title, scope });
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const record = await persistCaseMemory({ store, embedder, conflictStore }, input);
|
|
1131
|
+
existingKeys.add(key);
|
|
1132
|
+
stored.push({ title: record.title, id: record.id, scope: record.resolvedScope });
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (options.json) {
|
|
1136
|
+
console.log(JSON.stringify({ path, stored, skipped }, null, 2));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const lines = [
|
|
1141
|
+
`Case seeds: ${path}`,
|
|
1142
|
+
`Stored : ${stored.length}`,
|
|
1143
|
+
`Skipped : ${skipped.length}`,
|
|
1144
|
+
];
|
|
1145
|
+
|
|
1146
|
+
if (stored.length > 0) {
|
|
1147
|
+
lines.push("");
|
|
1148
|
+
lines.push("Stored cases:");
|
|
1149
|
+
for (const item of stored) {
|
|
1150
|
+
lines.push(` ${item.id.slice(0, 8)} ${item.scope} ${item.title}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (skipped.length > 0) {
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push("Skipped existing:");
|
|
1157
|
+
for (const item of skipped) {
|
|
1158
|
+
lines.push(` ${item.scope} ${item.title}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
console.log(lines.join("\n"));
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
program
|
|
1166
|
+
.command("seed-memories [file]")
|
|
1167
|
+
.description("批量写入 durable project / continuity memory seeds")
|
|
1168
|
+
.option("-s, --scope <scope>", "默认 scope(仅覆盖 seed 里未显式提供的项)")
|
|
1169
|
+
.option("--source <source>", "默认 source(manual / agent / api)", "agent")
|
|
1170
|
+
.option("--json", "JSON 输出")
|
|
1171
|
+
.action(async (file: string | undefined, options) => {
|
|
1172
|
+
const path = memorySeedsPath(file);
|
|
1173
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
1174
|
+
if (!Array.isArray(raw)) {
|
|
1175
|
+
throw new Error(`Memory seeds file must be a JSON array: ${path}`);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const config = loadConfig();
|
|
1179
|
+
const { store, embedder } = createComponents(config);
|
|
1180
|
+
const conflictStore = new ConflictCandidateStore();
|
|
1181
|
+
const existing = await store.list(undefined, undefined, 2000, 0);
|
|
1182
|
+
const existingKeys = new Set(
|
|
1183
|
+
existing
|
|
1184
|
+
.map((entry) => storeMemoryIdentity({
|
|
1185
|
+
category: entry.category,
|
|
1186
|
+
scope: entry.scope,
|
|
1187
|
+
text: entry.text,
|
|
1188
|
+
canonicalKey: extractStoreMemoryCanonicalKey(entry),
|
|
1189
|
+
}))
|
|
1190
|
+
.filter(Boolean),
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
const stored: Array<{ category: string; id: string; scope: string; text: string }> = [];
|
|
1194
|
+
const skipped: Array<{ category: string; scope: string; text: string }> = [];
|
|
1195
|
+
|
|
1196
|
+
for (const seed of raw) {
|
|
1197
|
+
const input = normalizeStoreMemorySeed(seed, {
|
|
1198
|
+
scope: options.scope,
|
|
1199
|
+
source: options.source,
|
|
1200
|
+
});
|
|
1201
|
+
const scope = input.scope;
|
|
1202
|
+
const key = storeMemoryIdentity(input);
|
|
1203
|
+
if (existingKeys.has(key)) {
|
|
1204
|
+
skipped.push({ category: input.category, scope, text: input.text });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const record = await persistMemory({ store, embedder, conflictStore }, input);
|
|
1209
|
+
existingKeys.add(storeMemoryIdentity({
|
|
1210
|
+
category: input.category,
|
|
1211
|
+
scope: record.resolvedScope,
|
|
1212
|
+
text: input.text,
|
|
1213
|
+
canonicalKey: record.canonicalKey,
|
|
1214
|
+
}));
|
|
1215
|
+
stored.push({
|
|
1216
|
+
category: record.category,
|
|
1217
|
+
id: record.id,
|
|
1218
|
+
scope: record.resolvedScope,
|
|
1219
|
+
text: record.text,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (options.json) {
|
|
1224
|
+
console.log(JSON.stringify({ path, stored, skipped }, null, 2));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const lines = [
|
|
1229
|
+
`Memory seeds: ${path}`,
|
|
1230
|
+
`Stored : ${stored.length}`,
|
|
1231
|
+
`Skipped : ${skipped.length}`,
|
|
1232
|
+
];
|
|
1233
|
+
|
|
1234
|
+
if (stored.length > 0) {
|
|
1235
|
+
lines.push("");
|
|
1236
|
+
lines.push("Stored memories:");
|
|
1237
|
+
for (const item of stored) {
|
|
1238
|
+
lines.push(` ${item.id.slice(0, 8)} ${item.scope} [${item.category}] ${seedTextPreview(item.text)}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (skipped.length > 0) {
|
|
1243
|
+
lines.push("");
|
|
1244
|
+
lines.push("Skipped existing:");
|
|
1245
|
+
for (const item of skipped) {
|
|
1246
|
+
lines.push(` ${item.scope} [${item.category}] ${seedTextPreview(item.text)}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
console.log(lines.join("\n"));
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// ─── demo ────────────────────────────────────────────────────────────────────
|
|
1254
|
+
|
|
1255
|
+
program
|
|
1256
|
+
.command("demo")
|
|
1257
|
+
.description("运行示例搜索,展示 RecallNest 工作效果")
|
|
1258
|
+
.action(async () => {
|
|
1259
|
+
const demoQueries = [
|
|
1260
|
+
"how to debug a failing bot",
|
|
1261
|
+
"telegram bridge setup",
|
|
1262
|
+
"memory search architecture",
|
|
1263
|
+
];
|
|
1264
|
+
|
|
1265
|
+
console.log("\n RecallNest Demo\n");
|
|
1266
|
+
console.log(" Running sample queries to show what RecallNest can do.\n");
|
|
1267
|
+
|
|
1268
|
+
const config = loadConfig();
|
|
1269
|
+
const { store } = createComponents(config);
|
|
1270
|
+
const stats = await store.stats();
|
|
1271
|
+
|
|
1272
|
+
if (stats.totalCount === 0) {
|
|
1273
|
+
console.log(" Index is empty. Run `lm ingest --source all` first.\n");
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
console.log(` Index: ${stats.totalCount} entries across ${Object.keys(stats.scopeCounts).length} scopes\n`);
|
|
1278
|
+
|
|
1279
|
+
const { retriever, profile } = createComponents(config);
|
|
1280
|
+
for (const query of demoQueries) {
|
|
1281
|
+
console.log(` Query: "${query}"`);
|
|
1282
|
+
const results = await retriever.retrieve({ query, limit: 3 });
|
|
1283
|
+
if (results.length === 0) {
|
|
1284
|
+
console.log(" (no results)\n");
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
for (const r of results) {
|
|
1288
|
+
const preview = r.entry.text.slice(0, 80).replace(/\n/g, " ");
|
|
1289
|
+
console.log(` [${r.score.toFixed(2)}] ${r.entry.scope} — ${preview}...`);
|
|
1290
|
+
}
|
|
1291
|
+
console.log();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
console.log(" Try your own: lm search \"your question here\"\n");
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// ─── ingest ──────────────────────────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
program
|
|
1300
|
+
.command("ingest")
|
|
1301
|
+
.description("导入对话记录到索引")
|
|
1302
|
+
.option("-s, --source <source>", "数据源: cc / codex / memory / desktop / all", "all")
|
|
1303
|
+
.option("-l, --limit <n>", "限制处理文件数(调试用)")
|
|
1304
|
+
.option("-v, --verbose", "详细输出")
|
|
1305
|
+
.option("--no-dedup", "跳过向量去重")
|
|
1306
|
+
.option("--no-llm", "禁用 LLM 提取(原始对话将跳过不存入,排入待处理队列)")
|
|
1307
|
+
.option("--recent [hours]", "快速模式:只处理最近 N 小时修改的文件(默认 2 小时),跳过 ingested-files 检查")
|
|
1308
|
+
.action(async (options) => {
|
|
1309
|
+
const config = loadConfig();
|
|
1310
|
+
const { store, embedder, llm } = createComponents(config);
|
|
1311
|
+
|
|
1312
|
+
const source = options.source || "all";
|
|
1313
|
+
const limit = options.limit
|
|
1314
|
+
? parseRequiredLimitOption(options.limit, "--limit", 1, Number.MAX_SAFE_INTEGER)
|
|
1315
|
+
: undefined;
|
|
1316
|
+
const verbose = options.verbose || false;
|
|
1317
|
+
const noDedup = options.dedup === false; // --no-dedup
|
|
1318
|
+
// --recent: quick mode — only process files modified within N hours, skip ingested-files check
|
|
1319
|
+
const recentHours: number | undefined = options.recent !== undefined
|
|
1320
|
+
? (options.recent === true ? 2 : Number(options.recent))
|
|
1321
|
+
: undefined;
|
|
1322
|
+
if (recentHours !== undefined && (isNaN(recentHours) || recentHours <= 0)) {
|
|
1323
|
+
console.error("❌ --recent 参数必须是正数(小时数)");
|
|
1324
|
+
process.exitCode = 1;
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const results: any[] = [];
|
|
1328
|
+
|
|
1329
|
+
// Pre-flight: validate embedding API before processing any files
|
|
1330
|
+
console.log("\n🔑 验证 Embedding API...");
|
|
1331
|
+
const testResult = await embedder.test();
|
|
1332
|
+
if (!testResult.success) {
|
|
1333
|
+
console.error(`\n❌ Embedding API 验证失败: ${testResult.error}`);
|
|
1334
|
+
console.error(" → 检查 .env 中的 JINA_API_KEY 是否正确");
|
|
1335
|
+
console.error(" → 获取免费 key: https://jina.ai/embeddings/");
|
|
1336
|
+
console.error(" → 运行 lm doctor 查看完整诊断\n");
|
|
1337
|
+
process.exitCode = 1;
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
console.log(` ✅ ${config.embedding.model} (${testResult.dimensions}d)`);
|
|
1341
|
+
|
|
1342
|
+
// LLM status — respect --no-llm flag
|
|
1343
|
+
const effectiveLlm = options.noLlm ? null : llm;
|
|
1344
|
+
if (options.noLlm) {
|
|
1345
|
+
console.log(" ⚠️ LLM: 已通过 --no-llm 禁用,原始对话将跳过不存入");
|
|
1346
|
+
} else if (llm) {
|
|
1347
|
+
const llmTest = await llm.test();
|
|
1348
|
+
console.log(llmTest.success
|
|
1349
|
+
? ` ✅ LLM: ${config.llm?.model} (L0 摘要 + 语义去重)`
|
|
1350
|
+
: ` ⚠️ LLM: ${config.llm?.model} 连接失败,原始对话将跳过不存入`);
|
|
1351
|
+
} else {
|
|
1352
|
+
console.log(" ⚠️ LLM: 未配置,原始对话将跳过不存入(配置 config.json → llm 以启用)");
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Drain pending queue if LLM is available
|
|
1356
|
+
if (effectiveLlm) {
|
|
1357
|
+
const { drainPendingQueue } = await import("./ingest.js");
|
|
1358
|
+
const drained = await drainPendingQueue(store, embedder, effectiveLlm);
|
|
1359
|
+
if (drained.processed > 0) {
|
|
1360
|
+
console.log(` ♻️ Pending queue: ${drained.processed} processed, ${drained.errors} errors`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
console.log(` ${noDedup ? "⚠️ 去重已禁用 (--no-dedup)" : "✅ 两阶段去重: vector + LLM 语义判断"}`);
|
|
1365
|
+
if (recentHours !== undefined) {
|
|
1366
|
+
console.log(` ⚡ 快速模式: 只处理最近 ${recentHours} 小时修改的文件,跳过 ingested-files 检查`);
|
|
1367
|
+
}
|
|
1368
|
+
console.log();
|
|
1369
|
+
console.log(`🔄 开始导入记忆 (source: ${source})...\n`);
|
|
1370
|
+
|
|
1371
|
+
const ingestOpts = { limit, verbose, noDedup, llm: effectiveLlm, recentHours };
|
|
1372
|
+
|
|
1373
|
+
// CC Transcripts
|
|
1374
|
+
if (source === "all" || source === "cc") {
|
|
1375
|
+
console.log("📝 导入 Claude Code 对话...");
|
|
1376
|
+
const ccSource = config.sources.cc;
|
|
1377
|
+
if (ccSource) {
|
|
1378
|
+
const ccPath = resolveSourcePath(ccSource.path, "cc");
|
|
1379
|
+
const r = await ingestCCTranscripts(store, embedder, ccPath, ingestOpts);
|
|
1380
|
+
results.push(r);
|
|
1381
|
+
console.log(` ✅ CC: ${formatIngestSummary(r)}`);
|
|
1382
|
+
maybeWarnHighCCDedupRate(r);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Codex Sessions
|
|
1387
|
+
if (source === "all" || source === "codex") {
|
|
1388
|
+
console.log("🤖 导入 Codex 对话...");
|
|
1389
|
+
const r = await ingestCodexSessions(store, embedder, ingestOpts);
|
|
1390
|
+
results.push(r);
|
|
1391
|
+
console.log(` ✅ Codex: ${formatIngestSummary(r)}`);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Gemini Sessions (JSON format under ~/.gemini/tmp/*/chats/)
|
|
1395
|
+
if (source === "all" || source === "gemini") {
|
|
1396
|
+
console.log("💎 导入 Gemini 对话...");
|
|
1397
|
+
const r = await ingestGeminiSessions(store, embedder, {
|
|
1398
|
+
limit,
|
|
1399
|
+
verbose,
|
|
1400
|
+
noDedup,
|
|
1401
|
+
llm,
|
|
1402
|
+
recentHours,
|
|
1403
|
+
});
|
|
1404
|
+
results.push(r);
|
|
1405
|
+
console.log(` ✅ Gemini: ${formatIngestSummary(r)}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Claude Desktop / claude.ai web conversations (CC transcript format)
|
|
1409
|
+
if (source === "all" || source === "desktop") {
|
|
1410
|
+
const desktopSource = config.sources.desktop;
|
|
1411
|
+
if (desktopSource) {
|
|
1412
|
+
const desktopPath = resolve(import.meta.dir, "..", desktopSource.path);
|
|
1413
|
+
if (existsSync(desktopPath)) {
|
|
1414
|
+
console.log("🖥️ 导入 Claude Desktop 对话...");
|
|
1415
|
+
const r = await ingestCCTranscripts(store, embedder, desktopPath, ingestOpts);
|
|
1416
|
+
results.push(r);
|
|
1417
|
+
console.log(` ✅ Desktop: ${formatIngestSummary(r)}`);
|
|
1418
|
+
maybeWarnHighCCDedupRate(r);
|
|
1419
|
+
} else if (source === "desktop") {
|
|
1420
|
+
console.log("⚠️ Desktop 目录不存在,请先运行: bun scripts/pull-claude-desktop.ts");
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Memory markdown files
|
|
1426
|
+
if (source === "all" || source === "memory") {
|
|
1427
|
+
console.log("📚 导入记忆文件...");
|
|
1428
|
+
const memSource = config.sources.memory;
|
|
1429
|
+
if (memSource) {
|
|
1430
|
+
const memPath = resolveSourcePath(memSource.path, "memory");
|
|
1431
|
+
const r = await ingestMarkdownFiles(store, embedder, memPath, "memory", {
|
|
1432
|
+
verbose,
|
|
1433
|
+
noDedup,
|
|
1434
|
+
llm,
|
|
1435
|
+
recentHours,
|
|
1436
|
+
});
|
|
1437
|
+
results.push(r);
|
|
1438
|
+
console.log(` ✅ Memory: ${formatIngestSummary(r)}`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Summary
|
|
1443
|
+
console.log("\n📊 导入汇总:");
|
|
1444
|
+
let totalChunks = 0;
|
|
1445
|
+
let totalDeduped = 0;
|
|
1446
|
+
let totalErrors = 0;
|
|
1447
|
+
const totalDedupReasons = {
|
|
1448
|
+
hard: 0,
|
|
1449
|
+
exact: 0,
|
|
1450
|
+
"llm-skip": 0,
|
|
1451
|
+
"llm-merge": 0,
|
|
1452
|
+
unique: 0,
|
|
1453
|
+
} as IngestResult["dedupReasonCounts"];
|
|
1454
|
+
for (const r of results) {
|
|
1455
|
+
totalChunks += r.chunksIngested;
|
|
1456
|
+
totalDeduped += r.chunksDeduped;
|
|
1457
|
+
totalErrors += r.errors.length;
|
|
1458
|
+
for (const reason of Object.keys(totalDedupReasons) as Array<keyof typeof totalDedupReasons>) {
|
|
1459
|
+
totalDedupReasons[reason] += r.dedupReasonCounts[reason];
|
|
1460
|
+
}
|
|
1461
|
+
if (r.errors.length > 0 && verbose) {
|
|
1462
|
+
console.log(` ⚠️ ${r.source} errors:`);
|
|
1463
|
+
for (const e of r.errors.slice(0, 5)) {
|
|
1464
|
+
console.log(` ${e}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
console.log(
|
|
1469
|
+
` 总计: ${totalChunks} chunks 已索引, ${totalDeduped} chunks 去重 (hard:${totalDedupReasons.hard}, exact:${totalDedupReasons.exact}, llm-skip:${totalDedupReasons["llm-skip"]}, llm-merge:${totalDedupReasons["llm-merge"]}), ${totalErrors} errors`,
|
|
1470
|
+
);
|
|
1471
|
+
console.log();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// ─── stats ───────────────────────────────────────────────────────────────────
|
|
1475
|
+
|
|
1476
|
+
program
|
|
1477
|
+
.command("stats")
|
|
1478
|
+
.description("显示索引统计")
|
|
1479
|
+
.action(async () => {
|
|
1480
|
+
const config = loadConfig();
|
|
1481
|
+
const { store } = createComponents(config);
|
|
1482
|
+
|
|
1483
|
+
const stats = await store.stats();
|
|
1484
|
+
|
|
1485
|
+
console.log("\n📊 记忆索引统计:\n");
|
|
1486
|
+
console.log(` 总条目: ${stats.totalCount}`);
|
|
1487
|
+
console.log();
|
|
1488
|
+
|
|
1489
|
+
console.log(" 按来源:");
|
|
1490
|
+
for (const [scope, count] of Object.entries(stats.scopeCounts).sort(
|
|
1491
|
+
(a, b) => b[1] - a[1],
|
|
1492
|
+
)) {
|
|
1493
|
+
console.log(` ${scope}: ${count}`);
|
|
1494
|
+
}
|
|
1495
|
+
console.log();
|
|
1496
|
+
|
|
1497
|
+
console.log(" 按类别:");
|
|
1498
|
+
for (const [cat, count] of Object.entries(stats.categoryCounts)) {
|
|
1499
|
+
console.log(` ${cat}: ${count}`);
|
|
1500
|
+
}
|
|
1501
|
+
console.log();
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
program
|
|
1505
|
+
.command("scope-audit")
|
|
1506
|
+
.description("扫描 legacy scope 异常行(missing / empty / global)")
|
|
1507
|
+
.option("-n, --limit <n>", "最多显示多少条示例", "20")
|
|
1508
|
+
.option("--json", "JSON 格式输出")
|
|
1509
|
+
.action(async (options) => {
|
|
1510
|
+
const config = loadConfig();
|
|
1511
|
+
const store = createStoreOnly(config);
|
|
1512
|
+
const limit = parseLimitOption(options.limit, 20, 1, 200);
|
|
1513
|
+
const audit = await store.auditLegacyScopes(limit);
|
|
1514
|
+
|
|
1515
|
+
if (options.json) {
|
|
1516
|
+
console.log(JSON.stringify(audit, null, 2));
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (audit.totalCount === 0) {
|
|
1521
|
+
console.log("No legacy scope anomalies found.");
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
console.log("Legacy scope audit\n");
|
|
1526
|
+
console.log(` Total anomalies: ${audit.totalCount}`);
|
|
1527
|
+
console.log(` Missing scope : ${audit.counts.missing}`);
|
|
1528
|
+
console.log(` Empty scope : ${audit.counts.empty}`);
|
|
1529
|
+
console.log(` Global scope : ${audit.counts.global}`);
|
|
1530
|
+
|
|
1531
|
+
if (audit.samples.length === 0) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
console.log("\n Sample rows:");
|
|
1536
|
+
console.log(" ID Kind Scope Category Date Text");
|
|
1537
|
+
console.log(" -------- ------- ----------- ------------ ---------- ----");
|
|
1538
|
+
for (const sample of audit.samples) {
|
|
1539
|
+
const date = new Date(sample.timestamp).toISOString().slice(0, 10);
|
|
1540
|
+
console.log(
|
|
1541
|
+
` ${sample.id.slice(0, 8)} ${sample.kind.padEnd(7)} ${formatLegacyScope(sample.scope).padEnd(11)} ${sample.category.padEnd(12)} ${date} ${seedTextPreview(sample.text, 56)}`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
console.log("\n Use this report to migrate or delete historical unscoped rows before removing compatibility shims.");
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
program
|
|
1549
|
+
.command("scope-inventory")
|
|
1550
|
+
.description("跨 memories / pins / checkpoints / workflow observations 统一盘点 legacy scope 异常")
|
|
1551
|
+
.option("-n, --limit <n>", "每层最多显示多少条示例", "20")
|
|
1552
|
+
.option("--json", "JSON 格式输出")
|
|
1553
|
+
.action(async (options) => {
|
|
1554
|
+
const config = loadConfig();
|
|
1555
|
+
const store = createStoreOnly(config);
|
|
1556
|
+
const limit = parseLimitOption(options.limit, 20, 1, 200);
|
|
1557
|
+
const report = await collectScopeInventory({
|
|
1558
|
+
store,
|
|
1559
|
+
sampleLimit: limit,
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
if (options.json) {
|
|
1563
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
console.log(formatScopeInventoryReport(report));
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
// ─── reset ───────────────────────────────────────────────────────────────────
|
|
1571
|
+
|
|
1572
|
+
program
|
|
1573
|
+
.command("reset")
|
|
1574
|
+
.description("清空索引(危险!)")
|
|
1575
|
+
.option("--yes", "跳过确认")
|
|
1576
|
+
.action(async (options) => {
|
|
1577
|
+
if (!options.yes) {
|
|
1578
|
+
console.log("⚠️ 这会清空所有已索引的记忆。确认请加 --yes");
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const config = loadConfig();
|
|
1583
|
+
const dbPath = resolve(import.meta.dir, "..", expandHome(config.dbPath));
|
|
1584
|
+
|
|
1585
|
+
if (existsSync(dbPath)) {
|
|
1586
|
+
const { rmSync } = await import("node:fs");
|
|
1587
|
+
rmSync(dbPath, { recursive: true });
|
|
1588
|
+
console.log("✅ 索引已清空。");
|
|
1589
|
+
} else {
|
|
1590
|
+
console.log("索引目录不存在,无需清空。");
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
// ─── export ──────────────────────────────────────────────────────────────────
|
|
1595
|
+
|
|
1596
|
+
program
|
|
1597
|
+
.command("export-memories")
|
|
1598
|
+
.description("导出最近记忆为 markdown(供 digital-clone 等下游消费)")
|
|
1599
|
+
.option("-d, --days <n>", "导出最近 N 天的记忆", "7")
|
|
1600
|
+
.option("-n, --limit <n>", "最多导出条数", "200")
|
|
1601
|
+
.option("-o, --output <path>", "输出文件路径(默认 stdout)")
|
|
1602
|
+
.option("-s, --scope <scope>", "限定来源")
|
|
1603
|
+
.option("--json", "输出 JSONL 格式")
|
|
1604
|
+
.action(async (options) => {
|
|
1605
|
+
const config = loadConfig();
|
|
1606
|
+
const { store } = createComponents(config);
|
|
1607
|
+
const scopeFilter = toScopeFilter(options.scope);
|
|
1608
|
+
const limit = parseLimitOption(options.limit, 200, 1, 1000);
|
|
1609
|
+
const days = parseLimitOption(options.days, 7, 1, 365);
|
|
1610
|
+
const cutoff = Date.now() - days * 86_400_000;
|
|
1611
|
+
|
|
1612
|
+
// List all entries, filter by time
|
|
1613
|
+
const entries = await store.list(scopeFilter, undefined, limit * 2, 0);
|
|
1614
|
+
const recent = entries
|
|
1615
|
+
.filter(e => e.timestamp >= cutoff)
|
|
1616
|
+
.slice(0, limit);
|
|
1617
|
+
|
|
1618
|
+
if (recent.length === 0) {
|
|
1619
|
+
console.log(`最近 ${days} 天没有新记忆。`);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
let output: string;
|
|
1624
|
+
|
|
1625
|
+
if (options.json) {
|
|
1626
|
+
output = recent.map(e => JSON.stringify({
|
|
1627
|
+
id: e.id,
|
|
1628
|
+
text: e.text,
|
|
1629
|
+
category: e.category,
|
|
1630
|
+
scope: e.scope,
|
|
1631
|
+
importance: e.importance,
|
|
1632
|
+
timestamp: e.timestamp,
|
|
1633
|
+
})).join("\n") + "\n";
|
|
1634
|
+
} else {
|
|
1635
|
+
// Markdown format: grouped by scope, sorted by time
|
|
1636
|
+
const grouped: Record<string, typeof recent> = {};
|
|
1637
|
+
for (const e of recent) {
|
|
1638
|
+
const key = e.scope || "<missing>";
|
|
1639
|
+
if (!grouped[key]) grouped[key] = [];
|
|
1640
|
+
grouped[key].push(e);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const lines: string[] = [
|
|
1644
|
+
`# RecallNest 记忆导出`,
|
|
1645
|
+
``,
|
|
1646
|
+
`> 导出时间: ${new Date().toISOString().slice(0, 10)}`,
|
|
1647
|
+
`> 范围: 最近 ${days} 天, ${recent.length} 条`,
|
|
1648
|
+
``,
|
|
1649
|
+
];
|
|
1650
|
+
|
|
1651
|
+
for (const [scope, entries] of Object.entries(grouped)) {
|
|
1652
|
+
lines.push(`## ${scope}`);
|
|
1653
|
+
lines.push(``);
|
|
1654
|
+
for (const e of entries) {
|
|
1655
|
+
const date = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
1656
|
+
const cat = e.category || "other";
|
|
1657
|
+
lines.push(`### [${cat}] ${date}`);
|
|
1658
|
+
lines.push(``);
|
|
1659
|
+
lines.push(e.text);
|
|
1660
|
+
lines.push(``);
|
|
1661
|
+
lines.push(`---`);
|
|
1662
|
+
lines.push(``);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
output = lines.join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (options.output) {
|
|
1670
|
+
const { writeFileSync } = require("node:fs");
|
|
1671
|
+
const { mkdirSync } = require("node:fs");
|
|
1672
|
+
const { dirname } = require("node:path");
|
|
1673
|
+
mkdirSync(dirname(options.output), { recursive: true });
|
|
1674
|
+
writeFileSync(options.output, output, "utf-8");
|
|
1675
|
+
console.log(`✅ 导出 ${recent.length} 条记忆 → ${options.output}`);
|
|
1676
|
+
} else {
|
|
1677
|
+
console.log(output);
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
// ─── doctor ──────────────────────────────────────────────────────────────────
|
|
1682
|
+
|
|
1683
|
+
program
|
|
1684
|
+
.command("workflow-observe")
|
|
1685
|
+
.description("记录一条 workflow primitive observation,作为自进化观测层的 append-only 输入")
|
|
1686
|
+
.argument("<workflowId>", "workflow primitive id,如 resume_context / checkpoint_session")
|
|
1687
|
+
.argument("<summary>", "本次 observation 的简短说明")
|
|
1688
|
+
.option("--outcome <outcome>", "success | failure | corrected | missed", "success")
|
|
1689
|
+
.option("--scope <scope>", "可选 scope,如 project:recallnest")
|
|
1690
|
+
.option("--source <source>", "来源标签", "manual")
|
|
1691
|
+
.option("--signal <signal>", "失败/纠正信号标签")
|
|
1692
|
+
.option("--task <task>", "相关任务")
|
|
1693
|
+
.option("--tags <tags>", "逗号分隔 tags")
|
|
1694
|
+
.option("--tools <tools>", "逗号分隔 tools")
|
|
1695
|
+
.option("--json", "输出 JSON")
|
|
1696
|
+
.action(async (workflowId, summary, options) => {
|
|
1697
|
+
const store = new WorkflowObservationStore();
|
|
1698
|
+
const record = buildWorkflowObservationRecord({
|
|
1699
|
+
workflowId,
|
|
1700
|
+
summary,
|
|
1701
|
+
outcome: WorkflowObservationOutcomeSchema.parse(options.outcome),
|
|
1702
|
+
scope: options.scope,
|
|
1703
|
+
source: options.source,
|
|
1704
|
+
signal: options.signal,
|
|
1705
|
+
task: options.task,
|
|
1706
|
+
tags: splitCsvOption(options.tags),
|
|
1707
|
+
tools: splitCsvOption(options.tools),
|
|
1708
|
+
});
|
|
1709
|
+
const stored = await store.save(record);
|
|
1710
|
+
if (options.json) {
|
|
1711
|
+
console.log(JSON.stringify(stored, null, 2));
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
console.log(formatWorkflowObservationSaved(stored));
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
program
|
|
1718
|
+
.command("workflow-health")
|
|
1719
|
+
.description("查看 workflow primitive 的健康度;不带 workflowId 时输出全局 dashboard")
|
|
1720
|
+
.argument("[workflowId]", "可选 workflow primitive id")
|
|
1721
|
+
.option("--scope <scope>", "可选 scope")
|
|
1722
|
+
.option("--limit <n>", "dashboard 返回数量上限")
|
|
1723
|
+
.option("--json", "输出 JSON")
|
|
1724
|
+
.action(async (workflowId, options) => {
|
|
1725
|
+
const store = new WorkflowObservationStore();
|
|
1726
|
+
if (workflowId) {
|
|
1727
|
+
const report = await inspectWorkflowHealth(store, {
|
|
1728
|
+
workflowId,
|
|
1729
|
+
scope: options.scope,
|
|
1730
|
+
});
|
|
1731
|
+
if (options.json) {
|
|
1732
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
console.log(formatWorkflowHealthReport(report));
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const dashboard = await inspectWorkflowDashboard(store, {
|
|
1740
|
+
scope: options.scope,
|
|
1741
|
+
limit: parseLimitOption(options.limit, 10, 1, 30),
|
|
1742
|
+
});
|
|
1743
|
+
if (options.json) {
|
|
1744
|
+
console.log(JSON.stringify(dashboard, null, 2));
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
console.log(formatWorkflowHealthDashboard(dashboard, options.scope));
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
program
|
|
1751
|
+
.command("workflow-evidence")
|
|
1752
|
+
.description("为某个 workflow primitive 生成 evidence pack,方便后续做规则/测试收口")
|
|
1753
|
+
.argument("<workflowId>", "workflow primitive id")
|
|
1754
|
+
.option("--scope <scope>", "可选 scope")
|
|
1755
|
+
.option("--limit <n>", "最近 issue observation 数量上限")
|
|
1756
|
+
.option("--json", "输出 JSON")
|
|
1757
|
+
.action(async (workflowId, options) => {
|
|
1758
|
+
const store = new WorkflowObservationStore();
|
|
1759
|
+
const pack = await buildWorkflowEvidence(store, {
|
|
1760
|
+
workflowId,
|
|
1761
|
+
scope: options.scope,
|
|
1762
|
+
limit: parseLimitOption(options.limit, 5, 1, 20),
|
|
1763
|
+
});
|
|
1764
|
+
if (options.json) {
|
|
1765
|
+
console.log(JSON.stringify(pack, null, 2));
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
console.log(formatWorkflowEvidencePack(pack));
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
program
|
|
1772
|
+
.command("workflow-scope-review")
|
|
1773
|
+
.description("审查指定 scope(默认 global)的 workflow observations,并按 cue 预览/回填目标 scope")
|
|
1774
|
+
.option("--current-scope <scope>", "要审查的当前 scope", "global")
|
|
1775
|
+
.option("--source <source>", "可选 source 过滤,如 managed")
|
|
1776
|
+
.option("--workflow-id <workflowId>", "可选 workflowId 过滤,如 resume_context")
|
|
1777
|
+
.option("--cue <needle=scope>", "可重复;把命中的 task/summary cue 映射到目标 scope", collectRepeatedOption, [])
|
|
1778
|
+
.option("--neighbor-window-seconds <n>", "允许从邻近 observation 继承唯一 scope 的时间窗(秒)", "90")
|
|
1779
|
+
.option("--apply", "把有唯一建议 scope 的记录直接回填到文件")
|
|
1780
|
+
.option("--keep-global-id <id>", "可重复;把指定 manual-review 记录标记为已审阅且允许保留当前 global", collectRepeatedOption, [])
|
|
1781
|
+
.option("--keep-global-reason <reason>", "给 --keep-global-id 使用的审阅原因")
|
|
1782
|
+
.option("--json", "输出 JSON")
|
|
1783
|
+
.action(async (options) => {
|
|
1784
|
+
const cues = (options.cue as string[]).map(parseWorkflowObservationScopeCue);
|
|
1785
|
+
const keepGlobalIds = (options.keepGlobalId as string[]) || [];
|
|
1786
|
+
if (keepGlobalIds.length > 0 && typeof options.keepGlobalReason !== "string") {
|
|
1787
|
+
throw new Error("workflow-scope-review --keep-global-id requires --keep-global-reason.");
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const review = reviewWorkflowObservationScopes({
|
|
1791
|
+
currentScope: options.currentScope,
|
|
1792
|
+
source: options.source,
|
|
1793
|
+
workflowId: options.workflowId,
|
|
1794
|
+
cues,
|
|
1795
|
+
neighborWindowSeconds: parseLimitOption(options.neighborWindowSeconds, 90, 0, 3600),
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
const keepGlobalResult = keepGlobalIds.length > 0
|
|
1799
|
+
? keepWorkflowObservationScopesGlobal(review, {
|
|
1800
|
+
ids: keepGlobalIds,
|
|
1801
|
+
reason: options.keepGlobalReason,
|
|
1802
|
+
})
|
|
1803
|
+
: undefined;
|
|
1804
|
+
|
|
1805
|
+
if (options.apply || keepGlobalResult) {
|
|
1806
|
+
const result = options.apply
|
|
1807
|
+
? applyWorkflowObservationScopeSuggestions(review)
|
|
1808
|
+
: undefined;
|
|
1809
|
+
const latestReview = reviewWorkflowObservationScopes({
|
|
1810
|
+
currentScope: options.currentScope,
|
|
1811
|
+
source: options.source,
|
|
1812
|
+
workflowId: options.workflowId,
|
|
1813
|
+
cues,
|
|
1814
|
+
neighborWindowSeconds: parseLimitOption(options.neighborWindowSeconds, 90, 0, 3600),
|
|
1815
|
+
});
|
|
1816
|
+
if (options.json) {
|
|
1817
|
+
console.log(JSON.stringify({ review: latestReview, result, keepGlobalResult }, null, 2));
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
console.log(formatWorkflowObservationScopeReview(latestReview));
|
|
1822
|
+
if (keepGlobalResult) {
|
|
1823
|
+
console.log();
|
|
1824
|
+
console.log(`Reviewed keep-global : ${keepGlobalResult.reviewedCount}`);
|
|
1825
|
+
console.log(`Skipped keep-global : ${keepGlobalResult.skippedCount}`);
|
|
1826
|
+
if (keepGlobalResult.unmatchedIds.length > 0) {
|
|
1827
|
+
console.log(`Unmatched ids : ${keepGlobalResult.unmatchedIds.join(", ")}`);
|
|
1828
|
+
}
|
|
1829
|
+
if (keepGlobalResult.ambiguousIds.length > 0) {
|
|
1830
|
+
console.log(`Ambiguous ids : ${keepGlobalResult.ambiguousIds.join(", ")}`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (options.apply) {
|
|
1834
|
+
console.log();
|
|
1835
|
+
console.log(`Applied : ${result?.updatedCount || 0}`);
|
|
1836
|
+
console.log(`Skipped : ${result?.skippedCount || 0}`);
|
|
1837
|
+
}
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (options.json) {
|
|
1842
|
+
console.log(JSON.stringify(review, null, 2));
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
console.log(formatWorkflowObservationScopeReview(review));
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
program
|
|
1849
|
+
.command("checkpoint-gc")
|
|
1850
|
+
.description("Clean up old session checkpoints: archive stale rich ones, delete expired minimal ones")
|
|
1851
|
+
.option("--dry-run", "Preview actions without modifying files")
|
|
1852
|
+
.option("--keep-per-scope <n>", "Rich checkpoints to keep per scope (default: 5)")
|
|
1853
|
+
.option("--archive-after-days <n>", "Archive rich checkpoints older than N days (default: 30)")
|
|
1854
|
+
.option("--minimal-ttl-days <n>", "Delete minimal checkpoints older than N days (default: 7)")
|
|
1855
|
+
.option("--archive-ttl-days <n>", "Hard-delete archived files older than N days (default: 90)")
|
|
1856
|
+
.action(async (options) => {
|
|
1857
|
+
const { SessionCheckpointStore } = await import("./session-store.js");
|
|
1858
|
+
const store = new SessionCheckpointStore();
|
|
1859
|
+
const result = store.gc({
|
|
1860
|
+
dryRun: Boolean(options.dryRun),
|
|
1861
|
+
keepPerScope: options.keepPerScope ? parseLimitOption(options.keepPerScope, 5, 1, 50) : undefined,
|
|
1862
|
+
archiveAfterDays: options.archiveAfterDays ? parseLimitOption(options.archiveAfterDays, 30, 1, 365) : undefined,
|
|
1863
|
+
minimalTtlDays: options.minimalTtlDays ? parseLimitOption(options.minimalTtlDays, 7, 1, 365) : undefined,
|
|
1864
|
+
archiveTtlDays: options.archiveTtlDays ? parseLimitOption(options.archiveTtlDays, 90, 1, 730) : undefined,
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
console.log(options.dryRun ? "=== Checkpoint GC (dry run) ===\n" : "=== Checkpoint GC ===\n");
|
|
1868
|
+
|
|
1869
|
+
const archives = result.actions.filter((a) => a.action === "archive");
|
|
1870
|
+
const deletes = result.actions.filter((a) => a.action === "delete");
|
|
1871
|
+
|
|
1872
|
+
if (archives.length > 0) {
|
|
1873
|
+
console.log(`Archive (${archives.length}):`);
|
|
1874
|
+
for (const a of archives) {
|
|
1875
|
+
console.log(` ${a.checkpointId.slice(0, 8)} ${a.resolvedScope} ${a.updatedAt.slice(0, 10)} ${a.reason}`);
|
|
1876
|
+
}
|
|
1877
|
+
console.log();
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
if (deletes.length > 0) {
|
|
1881
|
+
console.log(`Delete (${deletes.length}):`);
|
|
1882
|
+
for (const a of deletes) {
|
|
1883
|
+
console.log(` ${a.checkpointId.slice(0, 8)} ${a.resolvedScope} ${a.updatedAt.slice(0, 10)} ${a.reason}`);
|
|
1884
|
+
}
|
|
1885
|
+
console.log();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
console.log([
|
|
1889
|
+
`Kept : ${result.kept}`,
|
|
1890
|
+
`Archived : ${result.archived}`,
|
|
1891
|
+
`Deleted : ${result.deleted}`,
|
|
1892
|
+
`Errors : ${result.errors.length}`,
|
|
1893
|
+
].join("\n"));
|
|
1894
|
+
|
|
1895
|
+
if (result.errors.length > 0) {
|
|
1896
|
+
console.log("\nErrors:");
|
|
1897
|
+
for (const e of result.errors) console.log(` ${e}`);
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
program
|
|
1902
|
+
.command("doctor")
|
|
1903
|
+
.description("检查安装状态和配置")
|
|
1904
|
+
.option("--ci", "跳过 API key 在线验证(CI 模式)")
|
|
1905
|
+
.action(async (options) => {
|
|
1906
|
+
const results = await runDoctor({ ci: options.ci });
|
|
1907
|
+
console.log(formatDoctorResults(results));
|
|
1908
|
+
const hasFailure = results.some(r => r.status === "fail");
|
|
1909
|
+
if (hasFailure) process.exitCode = 1;
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
// Run
|
|
1913
|
+
loadDotEnv();
|
|
1914
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1915
|
+
console.error(`RecallNest CLI error: ${errorMessage(error)}`);
|
|
1916
|
+
process.exitCode = 1;
|
|
1917
|
+
});
|