recallnest 2.2.1 → 2.5.4
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/CHANGELOG.md +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -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__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -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__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -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-reconstructor.test.ts +65 -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__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -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 +205 -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-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -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 +4898 -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__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obsidian Vault Connector (GB-1)
|
|
3
|
+
*
|
|
4
|
+
* Scans an Obsidian vault directory and produces a ConnectorOutputV1 payload.
|
|
5
|
+
* Recognizes:
|
|
6
|
+
* - .obsidian/ directory (vault detection)
|
|
7
|
+
* - YAML frontmatter (tags, aliases, arbitrary metadata)
|
|
8
|
+
* - [[wikilinks]] (extracted as entity associations via link: tags)
|
|
9
|
+
* - Directory structure (folder: tags for scope/topic mapping)
|
|
10
|
+
* - Content hash (SHA-256 prefix) for incremental sync
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
15
|
+
import { join, relative, basename, dirname } from "node:path";
|
|
16
|
+
import type { ConnectorOutputV1, ConnectorRecord } from "./connector-types.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Vault Detection
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/** Check if a directory is an Obsidian vault (contains .obsidian/ folder) */
|
|
23
|
+
export function isObsidianVault(dirPath: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
return existsSync(join(dirPath, ".obsidian"));
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// YAML Frontmatter Parser
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)(?:\r?\n)?---/;
|
|
36
|
+
|
|
37
|
+
export interface ObsidianFrontmatter {
|
|
38
|
+
tags?: string[];
|
|
39
|
+
aliases?: string[];
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse YAML frontmatter from markdown content.
|
|
45
|
+
* Lightweight parser — handles:
|
|
46
|
+
* - key: value (strings)
|
|
47
|
+
* - key: [inline, list]
|
|
48
|
+
* - key:\n - list\n - items
|
|
49
|
+
* Does NOT handle nested objects, multi-line strings, or anchors.
|
|
50
|
+
* Returns null frontmatter on parse failure (never throws).
|
|
51
|
+
*/
|
|
52
|
+
export function parseFrontmatter(content: string): {
|
|
53
|
+
frontmatter: ObsidianFrontmatter | null;
|
|
54
|
+
body: string;
|
|
55
|
+
} {
|
|
56
|
+
const match = content.match(FRONTMATTER_RE);
|
|
57
|
+
if (!match) return { frontmatter: null, body: content };
|
|
58
|
+
|
|
59
|
+
const yamlBlock = match[1];
|
|
60
|
+
const body = content.slice(match[0].length).trimStart();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const fm: ObsidianFrontmatter = {};
|
|
64
|
+
let currentKey: string | null = null;
|
|
65
|
+
let listAccum: string[] = [];
|
|
66
|
+
|
|
67
|
+
function flushList(): void {
|
|
68
|
+
if (currentKey && listAccum.length > 0) {
|
|
69
|
+
fm[currentKey] = listAccum;
|
|
70
|
+
listAccum = [];
|
|
71
|
+
}
|
|
72
|
+
currentKey = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const line of yamlBlock.split("\n")) {
|
|
76
|
+
// List item: " - value"
|
|
77
|
+
const listItem = line.match(/^\s+-\s+(.+)/);
|
|
78
|
+
if (listItem && currentKey) {
|
|
79
|
+
listAccum.push(listItem[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Key-value: "key: value"
|
|
84
|
+
const kv = line.match(/^([\w][\w-]*):\s*(.*)/);
|
|
85
|
+
if (kv) {
|
|
86
|
+
flushList();
|
|
87
|
+
const [, key, rawVal] = kv;
|
|
88
|
+
const val = rawVal.trim();
|
|
89
|
+
|
|
90
|
+
if (val === "" || val === "[]") {
|
|
91
|
+
// Empty value or empty inline list — start list accumulator
|
|
92
|
+
currentKey = key;
|
|
93
|
+
listAccum = [];
|
|
94
|
+
} else if (val.startsWith("[") && val.endsWith("]")) {
|
|
95
|
+
// Inline list: [a, b, c]
|
|
96
|
+
fm[key] = val
|
|
97
|
+
.slice(1, -1)
|
|
98
|
+
.split(",")
|
|
99
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
} else {
|
|
102
|
+
// Simple string value
|
|
103
|
+
fm[key] = val.replace(/^['"]|['"]$/g, "");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
flushList();
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
frontmatter: Object.keys(fm).length > 0 ? fm : null,
|
|
111
|
+
body,
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
// Parse failure — return body without frontmatter
|
|
115
|
+
return { frontmatter: null, body };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Wikilink Extraction
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
const WIKILINK_PATTERN = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
124
|
+
|
|
125
|
+
export interface WikiLink {
|
|
126
|
+
/** Link target (file name without extension) */
|
|
127
|
+
target: string;
|
|
128
|
+
/** Optional display text (after |) */
|
|
129
|
+
displayText?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract [[wikilinks]] from markdown text.
|
|
134
|
+
* Handles [[target]] and [[target|display text]].
|
|
135
|
+
* Does NOT handle embed syntax ![[embed]] (those are skipped).
|
|
136
|
+
*/
|
|
137
|
+
export function extractWikiLinks(text: string): WikiLink[] {
|
|
138
|
+
const links: WikiLink[] = [];
|
|
139
|
+
// Create a fresh regex to avoid stale lastIndex
|
|
140
|
+
const re = new RegExp(WIKILINK_PATTERN.source, WIKILINK_PATTERN.flags);
|
|
141
|
+
let match: RegExpExecArray | null;
|
|
142
|
+
|
|
143
|
+
while ((match = re.exec(text)) !== null) {
|
|
144
|
+
// Skip embed syntax: character before [[ is !
|
|
145
|
+
if (match.index > 0 && text[match.index - 1] === "!") continue;
|
|
146
|
+
|
|
147
|
+
links.push({
|
|
148
|
+
target: match[1].trim(),
|
|
149
|
+
...(match[2] ? { displayText: match[2].trim() } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return links;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Content Hash
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/** SHA-256 first 16 hex chars — compact but collision-safe for incremental sync */
|
|
161
|
+
export function contentHash(text: string): string {
|
|
162
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Vault Scanner
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
export interface ScanVaultOptions {
|
|
170
|
+
/** Scope prefix, defaults to "vault:<vault-folder-name>" */
|
|
171
|
+
scopePrefix?: string;
|
|
172
|
+
/** Directories to exclude (defaults: .obsidian, .trash, .git, node_modules) */
|
|
173
|
+
excludeDirs?: string[];
|
|
174
|
+
/** Minimum file content length to include (default: 30 chars) */
|
|
175
|
+
minContentLength?: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Scan an Obsidian vault and produce a ConnectorOutputV1 payload.
|
|
180
|
+
*
|
|
181
|
+
* - Recursively walks .md files (skipping excluded directories)
|
|
182
|
+
* - Parses YAML frontmatter -> tags
|
|
183
|
+
* - Extracts [[wikilinks]] -> link:<target> tags
|
|
184
|
+
* - Maps folder structure -> folder:<path> tags
|
|
185
|
+
* - Computes SHA-256 content hash for incremental sync
|
|
186
|
+
*/
|
|
187
|
+
export function scanVault(
|
|
188
|
+
vaultPath: string,
|
|
189
|
+
options?: ScanVaultOptions,
|
|
190
|
+
): ConnectorOutputV1 {
|
|
191
|
+
const vaultName = basename(vaultPath);
|
|
192
|
+
const scopePrefix = options?.scopePrefix ?? `vault:${vaultName}`;
|
|
193
|
+
const defaultExclude = [".obsidian", ".trash", ".git", "node_modules"];
|
|
194
|
+
const excludeDirs = new Set(options?.excludeDirs ?? defaultExclude);
|
|
195
|
+
const minLen = options?.minContentLength ?? 30;
|
|
196
|
+
|
|
197
|
+
const records: ConnectorRecord[] = [];
|
|
198
|
+
|
|
199
|
+
function walk(dir: string): void {
|
|
200
|
+
let entries: ReturnType<typeof readdirSync>;
|
|
201
|
+
try {
|
|
202
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
203
|
+
} catch {
|
|
204
|
+
return; // Permission denied or broken symlink — skip
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (excludeDirs.has(entry.name)) continue;
|
|
209
|
+
|
|
210
|
+
const full = join(dir, entry.name);
|
|
211
|
+
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
walk(full);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
218
|
+
|
|
219
|
+
let raw: string;
|
|
220
|
+
try {
|
|
221
|
+
raw = readFileSync(full, "utf-8");
|
|
222
|
+
} catch {
|
|
223
|
+
continue; // Unreadable file — skip
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (raw.trim().length < minLen) continue;
|
|
227
|
+
|
|
228
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
229
|
+
const wikiLinks = extractWikiLinks(body);
|
|
230
|
+
const relPath = relative(vaultPath, full);
|
|
231
|
+
const folder = dirname(relPath);
|
|
232
|
+
|
|
233
|
+
// --- Build tags ---
|
|
234
|
+
const tags: string[] = [];
|
|
235
|
+
|
|
236
|
+
// Frontmatter tags
|
|
237
|
+
if (frontmatter?.tags) {
|
|
238
|
+
const fmTags = Array.isArray(frontmatter.tags)
|
|
239
|
+
? frontmatter.tags
|
|
240
|
+
: [String(frontmatter.tags)];
|
|
241
|
+
for (const t of fmTags) {
|
|
242
|
+
tags.push(String(t));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Wikilink entity associations
|
|
247
|
+
for (const link of wikiLinks) {
|
|
248
|
+
tags.push(`link:${link.target}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Folder path
|
|
252
|
+
if (folder !== ".") {
|
|
253
|
+
tags.push(`folder:${folder}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
records.push({
|
|
257
|
+
id: `obsidian:${relPath}`,
|
|
258
|
+
text: body,
|
|
259
|
+
title: basename(entry.name, ".md"),
|
|
260
|
+
tags,
|
|
261
|
+
contentHash: contentHash(raw),
|
|
262
|
+
timestamp: new Date(statSync(full).mtimeMs).toISOString(),
|
|
263
|
+
sourceMetadata: {
|
|
264
|
+
vaultPath,
|
|
265
|
+
relativePath: relPath,
|
|
266
|
+
...(frontmatter ? { frontmatter } : {}),
|
|
267
|
+
...(wikiLinks.length > 0 ? { wikiLinks } : {}),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
walk(vaultPath);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
version: "connector-v1",
|
|
277
|
+
source: "obsidian",
|
|
278
|
+
scope: scopePrefix,
|
|
279
|
+
producedAt: new Date().toISOString(),
|
|
280
|
+
records,
|
|
281
|
+
};
|
|
282
|
+
}
|
package/src/pii-detector.ts
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
* F-3: Lightweight PII detection.
|
|
3
3
|
* Scans text for common sensitive patterns (passwords, tokens, IDs).
|
|
4
4
|
* Returns warnings, does not block writes.
|
|
5
|
+
*
|
|
6
|
+
* F-3b (2026-07-10): Deterministic secret redaction on the same rule set.
|
|
7
|
+
* `redactSecrets` scrubs redactable matches BEFORE text reaches any external
|
|
8
|
+
* surface (LLM prompts, embedding API, stderr logs, audit trail). Detection
|
|
9
|
+
* and redaction share one rule table so the two can never drift apart.
|
|
10
|
+
* Deliberately NOT redacted: phone / email (legitimate memory content, warn
|
|
11
|
+
* only) and bare long-hex (would false-positive on git commit hashes, which
|
|
12
|
+
* are high-value memory content; prefixed forms like `secret=<hex>` are
|
|
13
|
+
* caught by the assignment rules).
|
|
5
14
|
*/
|
|
6
15
|
|
|
7
16
|
export type PIISeverity = "high" | "medium" | "low";
|
|
8
17
|
|
|
9
18
|
export interface PIIDetection {
|
|
10
|
-
type: string; // "api_key" | "password" | "id_number" | "email" | "phone" | "credit_card"
|
|
19
|
+
type: string; // "api_key" | "password" | "id_number" | "email" | "phone" | "credit_card" | ...
|
|
11
20
|
severity: PIISeverity;
|
|
12
21
|
match: string; // the matched text (partially masked)
|
|
13
22
|
position: number; // char offset
|
|
@@ -19,42 +28,147 @@ export interface PIIScanResult {
|
|
|
19
28
|
summary: string; // human-readable summary
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
export interface RedactResult {
|
|
32
|
+
text: string; // scrubbed text
|
|
33
|
+
redacted: number; // number of replacements made
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
interface PIIRule {
|
|
23
37
|
type: string;
|
|
24
38
|
severity: PIISeverity;
|
|
25
39
|
pattern: RegExp;
|
|
40
|
+
/** Scrub matches via redactSecrets (detection-only rules leave text intact) */
|
|
41
|
+
redact: boolean;
|
|
42
|
+
/** For key=value style rules: keep the key name, scrub only the value */
|
|
43
|
+
keepPrefix?: RegExp;
|
|
44
|
+
/** Checksum gate for broad numeric patterns: redact ONLY when this returns
|
|
45
|
+
* true (scan still warns regardless). Keeps snowflake IDs / order numbers
|
|
46
|
+
* from being destructively rewritten. */
|
|
47
|
+
validate?: (match: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** GB 11643 checksum (ISO 7064 MOD 11-2) for 18-digit Chinese national IDs. */
|
|
51
|
+
function isValidChineseId(id: string): boolean {
|
|
52
|
+
if (!/^\d{17}[\dXx]$/.test(id)) return false;
|
|
53
|
+
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
|
54
|
+
const checkChars = "10X98765432";
|
|
55
|
+
let sum = 0;
|
|
56
|
+
for (let i = 0; i < 17; i++) sum += Number(id[i]) * weights[i];
|
|
57
|
+
return checkChars[sum % 11] === id[17].toUpperCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Luhn checksum for card-number candidates (separators stripped first). */
|
|
61
|
+
function isValidLuhn(candidate: string): boolean {
|
|
62
|
+
const digits = candidate.replace(/[\s-]/g, "");
|
|
63
|
+
if (!/^\d{16}$/.test(digits)) return false;
|
|
64
|
+
let sum = 0;
|
|
65
|
+
for (let i = 0; i < 16; i++) {
|
|
66
|
+
let d = Number(digits[15 - i]);
|
|
67
|
+
if (i % 2 === 1) {
|
|
68
|
+
d *= 2;
|
|
69
|
+
if (d > 9) d -= 9;
|
|
70
|
+
}
|
|
71
|
+
sum += d;
|
|
72
|
+
}
|
|
73
|
+
return sum % 10 === 0;
|
|
26
74
|
}
|
|
27
75
|
|
|
28
76
|
const PII_RULES: PIIRule[] = [
|
|
77
|
+
// --- Vendor-prefixed token literals (high confidence, full scrub) ---
|
|
78
|
+
{
|
|
79
|
+
type: "anthropic_key",
|
|
80
|
+
severity: "high",
|
|
81
|
+
pattern: /sk-ant-[A-Za-z0-9_\-]{10,}/g,
|
|
82
|
+
redact: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "github_token",
|
|
86
|
+
severity: "high",
|
|
87
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
88
|
+
redact: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "slack_token",
|
|
92
|
+
severity: "high",
|
|
93
|
+
pattern: /xox[baprs]-[A-Za-z0-9\-]{10,}/g,
|
|
94
|
+
redact: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: "aws_access_key",
|
|
98
|
+
severity: "high",
|
|
99
|
+
pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
100
|
+
redact: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "google_api_key",
|
|
104
|
+
severity: "high",
|
|
105
|
+
pattern: /\bAIza[A-Za-z0-9_\-]{35}\b/g,
|
|
106
|
+
redact: true,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "jwt",
|
|
110
|
+
severity: "high",
|
|
111
|
+
pattern: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g,
|
|
112
|
+
redact: true,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "bearer_token",
|
|
116
|
+
severity: "high",
|
|
117
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+\/\-]{16,}=*/g,
|
|
118
|
+
redact: true,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: "pem_private_key",
|
|
122
|
+
severity: "high",
|
|
123
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
124
|
+
redact: true,
|
|
125
|
+
},
|
|
126
|
+
// --- Generic assignment / key styles (keep key name, scrub value) ---
|
|
127
|
+
// Quoted keys and whitespace around the separator are allowed so JSON/YAML/
|
|
128
|
+
// config forms ({"api_key": "..."}, password = ...) are caught too.
|
|
29
129
|
{
|
|
30
130
|
type: "api_key",
|
|
31
131
|
severity: "high",
|
|
32
|
-
pattern: /(?:sk-[A-Za-z0-9_\-]{20,}|(?:api[_-]?key|token|secret)[
|
|
132
|
+
pattern: /(?:sk-[A-Za-z0-9_\-]{20,}|["']?(?:api[_-]?key|token|secret)["']?\s*[=:]\s*["']?[A-Za-z0-9_\-]{20,})/gi,
|
|
133
|
+
redact: true,
|
|
134
|
+
keepPrefix: /^["']?(?:api[_-]?key|token|secret)["']?\s*[=:]\s*["']?/i,
|
|
33
135
|
},
|
|
34
136
|
{
|
|
35
137
|
type: "password",
|
|
36
138
|
severity: "high",
|
|
37
|
-
pattern: /(?:password|passwd|pwd)[
|
|
139
|
+
pattern: /["']?(?:password|passwd|pwd)["']?\s*[=:]\s*["']?[^\s"',;]{8,}/gi,
|
|
140
|
+
redact: true,
|
|
141
|
+
keepPrefix: /^["']?(?:password|passwd|pwd)["']?\s*[=:]\s*["']?/i,
|
|
38
142
|
},
|
|
143
|
+
// --- High-risk personal identifiers ---
|
|
144
|
+
// Broad numeric patterns scan-warn on shape but only REDACT when the
|
|
145
|
+
// checksum verifies — otherwise snowflake IDs / order numbers get rewritten.
|
|
39
146
|
{
|
|
40
147
|
type: "id_number",
|
|
41
148
|
severity: "high",
|
|
42
|
-
pattern:
|
|
149
|
+
pattern: /(?<!\d)\d{17}[\dXx](?!\d)/g,
|
|
150
|
+
redact: true,
|
|
151
|
+
validate: isValidChineseId,
|
|
43
152
|
},
|
|
44
153
|
{
|
|
45
154
|
type: "credit_card",
|
|
46
155
|
severity: "high",
|
|
47
|
-
pattern:
|
|
156
|
+
pattern: /(?<!\d)\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}(?![\d])/g,
|
|
157
|
+
redact: true,
|
|
158
|
+
validate: isValidLuhn,
|
|
48
159
|
},
|
|
160
|
+
// --- Detection-only: legitimate memory content, warn but keep ---
|
|
49
161
|
{
|
|
50
162
|
type: "phone",
|
|
51
163
|
severity: "medium",
|
|
52
164
|
pattern: /1[3-9]\d{9}/g,
|
|
165
|
+
redact: false,
|
|
53
166
|
},
|
|
54
167
|
{
|
|
55
168
|
type: "email",
|
|
56
169
|
severity: "low",
|
|
57
170
|
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
171
|
+
redact: false,
|
|
58
172
|
},
|
|
59
173
|
];
|
|
60
174
|
|
|
@@ -96,3 +210,41 @@ export function scanForPII(text: string): PIIScanResult {
|
|
|
96
210
|
summary,
|
|
97
211
|
};
|
|
98
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Deterministically scrub redactable secrets. Pure regex, no LLM call.
|
|
216
|
+
* Replacement is `[REDACTED:<type>]`; assignment-style rules keep the key
|
|
217
|
+
* name (`password=[REDACTED:password]`) so the memory stays readable.
|
|
218
|
+
*/
|
|
219
|
+
export function redactSecrets(text: string): RedactResult {
|
|
220
|
+
let out = text;
|
|
221
|
+
let redacted = 0;
|
|
222
|
+
|
|
223
|
+
for (const rule of PII_RULES) {
|
|
224
|
+
if (!rule.redact) continue;
|
|
225
|
+
rule.pattern.lastIndex = 0;
|
|
226
|
+
out = out.replace(rule.pattern, (match) => {
|
|
227
|
+
if (rule.validate && !rule.validate(match)) {
|
|
228
|
+
return match; // checksum failed → leave intact (detection-only)
|
|
229
|
+
}
|
|
230
|
+
redacted++;
|
|
231
|
+
if (rule.keepPrefix) {
|
|
232
|
+
const prefixMatch = match.match(rule.keepPrefix);
|
|
233
|
+
if (prefixMatch) {
|
|
234
|
+
return `${prefixMatch[0]}[REDACTED:${rule.type}]`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return `[REDACTED:${rule.type}]`;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { text: out, redacted };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Redact + truncate for log lines. Truncation happens AFTER redaction so a
|
|
246
|
+
* secret can never straddle the cut and leak its prefix.
|
|
247
|
+
*/
|
|
248
|
+
export function redactForLog(text: string, max = 60): string {
|
|
249
|
+
return redactSecrets(text).text.slice(0, max);
|
|
250
|
+
}
|
package/src/query-expander.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* v1.3: P0.3 — User-level alias-map (data/alias-map.json) for short query expansion.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync } from "fs";
|
|
14
|
-
import { join } from "path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { dirname, join } from "path";
|
|
15
15
|
import { resolveQueryEntities } from "./entity-resolver.js";
|
|
16
16
|
|
|
17
17
|
interface SynonymEntry {
|
|
@@ -89,9 +89,14 @@ export function resetAliasMapCache(): void {
|
|
|
89
89
|
aliasMapCache = null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** Resolve the alias-map file path currently in effect. */
|
|
93
|
+
export function aliasMapFilePath(): string {
|
|
94
|
+
return aliasMapPath ?? join(process.cwd(), "data", "alias-map.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
function loadAliasMap(): AliasEntry[] {
|
|
93
98
|
if (aliasMapCache !== null) return aliasMapCache;
|
|
94
|
-
const filePath =
|
|
99
|
+
const filePath = aliasMapFilePath();
|
|
95
100
|
try {
|
|
96
101
|
if (existsSync(filePath)) {
|
|
97
102
|
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
@@ -112,6 +117,75 @@ function loadAliasMap(): AliasEntry[] {
|
|
|
112
117
|
return aliasMapCache;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// User alias management (P1-B manage_alias tool 后端)
|
|
122
|
+
//
|
|
123
|
+
// 这些别名只作用于 BM25 通道(expandQuery 在 hybridRetrieval 内部被调用),
|
|
124
|
+
// 刻意不进 vector embedding——随手加的用户别名若改变语义向量,排查
|
|
125
|
+
// retrieval drift 会非常困难。全通道的 builtin 规则在 src/aliases.ts。
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** 写入校验:trigger 最短 2 字符(防高频泛词),expansions 最多 8 个。 */
|
|
129
|
+
export const USER_ALIAS_TRIGGER_MIN = 2;
|
|
130
|
+
export const USER_ALIAS_EXPANSIONS_MAX = 8;
|
|
131
|
+
|
|
132
|
+
export type AliasUpsertResult =
|
|
133
|
+
| { ok: true; action: "added" | "updated"; entry: AliasEntry }
|
|
134
|
+
| { ok: false; reason: string };
|
|
135
|
+
|
|
136
|
+
/** 新增/更新用户别名(写 alias-map.json;校验后缓存即时失效)。 */
|
|
137
|
+
export function upsertUserAlias(trigger: string, expansions: string[]): AliasUpsertResult {
|
|
138
|
+
const t = trigger.trim();
|
|
139
|
+
const exp = [...new Set(expansions.map(e => e.trim()).filter(e => e.length > 0))];
|
|
140
|
+
|
|
141
|
+
if (t.length < USER_ALIAS_TRIGGER_MIN) {
|
|
142
|
+
return { ok: false, reason: `trigger 太短(<${USER_ALIAS_TRIGGER_MIN} 字符)——高频泛词会污染所有查询` };
|
|
143
|
+
}
|
|
144
|
+
if (exp.length === 0) {
|
|
145
|
+
return { ok: false, reason: "expansions 为空" };
|
|
146
|
+
}
|
|
147
|
+
if (exp.length > USER_ALIAS_EXPANSIONS_MAX) {
|
|
148
|
+
return { ok: false, reason: `expansions 超过 ${USER_ALIAS_EXPANSIONS_MAX} 个——过长扩展会冲淡 BM25 原查询词权重` };
|
|
149
|
+
}
|
|
150
|
+
if (exp.every(e => e === t)) {
|
|
151
|
+
return { ok: false, reason: "expansions 与 trigger 相同,无扩展意义" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rules = loadAliasMap();
|
|
155
|
+
const idx = rules.findIndex(r => r.trigger === t);
|
|
156
|
+
const entry: AliasEntry = { trigger: t, expansions: exp };
|
|
157
|
+
const next = idx >= 0 ? rules.map((r, i) => (i === idx ? entry : r)) : [...rules, entry];
|
|
158
|
+
|
|
159
|
+
const path = aliasMapFilePath();
|
|
160
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
161
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
162
|
+
aliasMapCache = null;
|
|
163
|
+
return { ok: true, action: idx >= 0 ? "updated" : "added", entry };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 删除用户别名;返回是否真的删了。 */
|
|
167
|
+
export function removeUserAlias(trigger: string): boolean {
|
|
168
|
+
const rules = loadAliasMap();
|
|
169
|
+
const next = rules.filter(r => r.trigger !== trigger.trim());
|
|
170
|
+
if (next.length === rules.length) return false;
|
|
171
|
+
const path = aliasMapFilePath();
|
|
172
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
173
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
174
|
+
aliasMapCache = null;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 列出用户别名规则。 */
|
|
179
|
+
export function listUserAliases(): AliasEntry[] {
|
|
180
|
+
return [...loadAliasMap()];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 检视:query 命中了哪些用户别名(BM25-only 通道)。 */
|
|
184
|
+
export function explainUserAliases(query: string): AliasEntry[] {
|
|
185
|
+
const lower = query.toLowerCase();
|
|
186
|
+
return loadAliasMap().filter(e => lower.includes(e.trigger.toLowerCase()));
|
|
187
|
+
}
|
|
188
|
+
|
|
115
189
|
/**
|
|
116
190
|
* Expand a query by appending synonym terms from the dictionary.
|
|
117
191
|
* Returns the original query with additional terms appended.
|
|
@@ -123,7 +197,8 @@ export function expandQuery(query: string): string {
|
|
|
123
197
|
// Entity resolution pre-pass: normalize aliases before synonym expansion
|
|
124
198
|
const resolved = resolveQueryEntities(query);
|
|
125
199
|
const lower = resolved.toLowerCase();
|
|
126
|
-
const
|
|
200
|
+
const synonymAdditions = new Set<string>();
|
|
201
|
+
const aliasAdditions = new Set<string>();
|
|
127
202
|
|
|
128
203
|
// Static synonym map (built-in)
|
|
129
204
|
for (const entry of SYNONYM_MAP) {
|
|
@@ -138,7 +213,7 @@ export function expandQuery(query: string): string {
|
|
|
138
213
|
if (cnMatch || enMatch) {
|
|
139
214
|
for (const exp of entry.expansions) {
|
|
140
215
|
if (!lower.includes(exp.toLowerCase())) {
|
|
141
|
-
|
|
216
|
+
synonymAdditions.add(exp);
|
|
142
217
|
}
|
|
143
218
|
}
|
|
144
219
|
}
|
|
@@ -151,15 +226,21 @@ export function expandQuery(query: string): string {
|
|
|
151
226
|
if (lower.includes(triggerLower)) {
|
|
152
227
|
for (const exp of alias.expansions) {
|
|
153
228
|
if (!lower.includes(exp.toLowerCase())) {
|
|
154
|
-
|
|
229
|
+
aliasAdditions.add(exp);
|
|
155
230
|
}
|
|
156
231
|
}
|
|
157
232
|
}
|
|
158
233
|
}
|
|
159
234
|
|
|
160
|
-
if (
|
|
235
|
+
if (aliasAdditions.size === 0 && synonymAdditions.size === 0) return resolved;
|
|
161
236
|
|
|
162
|
-
// Cap expansion terms to prevent query bloat
|
|
163
|
-
|
|
237
|
+
// Cap expansion terms to prevent query bloat. User aliases rank first inside
|
|
238
|
+
// the cap — 用户为这个 trigger 显式配置的 canonical 名,不应被泛化同义词
|
|
239
|
+
// 挤出 MAX_EXPANSION_TERMS(P1-B:同义词"挂"曾把 alias 的实体名挤掉)。
|
|
240
|
+
const merged = [
|
|
241
|
+
...aliasAdditions,
|
|
242
|
+
...[...synonymAdditions].filter(t => !aliasAdditions.has(t)),
|
|
243
|
+
];
|
|
244
|
+
const limited = merged.slice(0, MAX_EXPANSION_TERMS);
|
|
164
245
|
return `${resolved} ${limited.join(" ")}`;
|
|
165
246
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { LLMClient } from "./llm-client.js";
|
|
14
14
|
import { logWarn } from "./stderr-log.js";
|
|
15
|
+
import * as envConfig from "./env-config.js";
|
|
15
16
|
|
|
16
17
|
// ============================================================================
|
|
17
18
|
// Configuration
|
|
@@ -38,7 +39,7 @@ export const DEFAULT_SYNTHESIZER_CONFIG: SynthesizerConfig = {
|
|
|
38
39
|
* Check if synthesis is enabled via environment variable.
|
|
39
40
|
*/
|
|
40
41
|
export function isSynthesisEnabled(): boolean {
|
|
41
|
-
return
|
|
42
|
+
return envConfig.synthesize();
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
package/src/retention-policy.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import * as envConfig from "./env-config.js";
|
|
10
11
|
|
|
11
12
|
export interface RetentionPolicy {
|
|
12
13
|
/** Auto-archive memories older than this many days (0 = disabled) */
|
|
@@ -29,7 +30,7 @@ function scopeHash(scope: string): string {
|
|
|
29
30
|
|
|
30
31
|
function retentionDir(configDir?: string): string {
|
|
31
32
|
return join(
|
|
32
|
-
configDir ?? (
|
|
33
|
+
configDir ?? envConfig.dataDir(),
|
|
33
34
|
"retention",
|
|
34
35
|
);
|
|
35
36
|
}
|