@lanonasis/recall-forge 1.1.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.
Files changed (68) hide show
  1. package/.claw/skills/SKILL.md +347 -0
  2. package/CHANGELOG.md +162 -0
  3. package/LICENSE +21 -0
  4. package/README.md +302 -0
  5. package/SETUP.md +190 -0
  6. package/dist/cli-common.d.ts +25 -0
  7. package/dist/cli-common.js +338 -0
  8. package/dist/cli-memory.d.ts +6 -0
  9. package/dist/cli-memory.js +146 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.js +135 -0
  12. package/dist/client.d.ts +116 -0
  13. package/dist/client.js +643 -0
  14. package/dist/config.d.ts +41 -0
  15. package/dist/config.js +125 -0
  16. package/dist/enrichment/capture-filter.d.ts +4 -0
  17. package/dist/enrichment/capture-filter.js +44 -0
  18. package/dist/enrichment/prompt-safety.d.ts +13 -0
  19. package/dist/enrichment/prompt-safety.js +83 -0
  20. package/dist/enrichment/tag-extractor.d.ts +1 -0
  21. package/dist/enrichment/tag-extractor.js +47 -0
  22. package/dist/enrichment/type-detector.d.ts +2 -0
  23. package/dist/enrichment/type-detector.js +95 -0
  24. package/dist/extraction/cli-extract.d.ts +8 -0
  25. package/dist/extraction/cli-extract.js +66 -0
  26. package/dist/extraction/format-adapters.d.ts +8 -0
  27. package/dist/extraction/format-adapters.js +268 -0
  28. package/dist/extraction/index.d.ts +7 -0
  29. package/dist/extraction/index.js +7 -0
  30. package/dist/extraction/jsonl-extractor.d.ts +32 -0
  31. package/dist/extraction/jsonl-extractor.js +207 -0
  32. package/dist/extraction/markdown-extractor.d.ts +23 -0
  33. package/dist/extraction/markdown-extractor.js +228 -0
  34. package/dist/extraction/secret-redactor.d.ts +7 -0
  35. package/dist/extraction/secret-redactor.js +112 -0
  36. package/dist/extraction/sqlite-extractor.d.ts +15 -0
  37. package/dist/extraction/sqlite-extractor.js +245 -0
  38. package/dist/extraction/types.d.ts +50 -0
  39. package/dist/extraction/types.js +1 -0
  40. package/dist/hooks/capture.d.ts +23 -0
  41. package/dist/hooks/capture.js +162 -0
  42. package/dist/hooks/context-engine.d.ts +4 -0
  43. package/dist/hooks/context-engine.js +54 -0
  44. package/dist/hooks/local-fallback.d.ts +5 -0
  45. package/dist/hooks/local-fallback.js +31 -0
  46. package/dist/hooks/recall.d.ts +21 -0
  47. package/dist/hooks/recall.js +123 -0
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.js +103 -0
  50. package/dist/plugin-sdk-stub.d.ts +53 -0
  51. package/dist/plugin-sdk-stub.js +3 -0
  52. package/dist/privacy/privacy-guard.d.ts +33 -0
  53. package/dist/privacy/privacy-guard.js +130 -0
  54. package/dist/privacy/privacy-log.d.ts +6 -0
  55. package/dist/privacy/privacy-log.js +44 -0
  56. package/dist/tools/memory-forget.d.ts +3 -0
  57. package/dist/tools/memory-forget.js +109 -0
  58. package/dist/tools/memory-get.d.ts +3 -0
  59. package/dist/tools/memory-get.js +46 -0
  60. package/dist/tools/memory-search.d.ts +4 -0
  61. package/dist/tools/memory-search.js +95 -0
  62. package/dist/tools/memory-store.d.ts +5 -0
  63. package/dist/tools/memory-store.js +199 -0
  64. package/openclaw.plugin.json +315 -0
  65. package/package.json +90 -0
  66. package/setup/agents-memory.md +63 -0
  67. package/setup/heartbeat-memory.md +53 -0
  68. package/setup/install.sh +179 -0
@@ -0,0 +1,268 @@
1
+ // Pluggable JSONL format parsers
2
+ // Each adapter detects and extracts content from a specific JSONL format
3
+ // Shared: extract text from Claude-style message content (string or content blocks)
4
+ function extractMessageText(msg) {
5
+ const texts = [];
6
+ const content = msg.content;
7
+ if (typeof content === "string") {
8
+ texts.push(content);
9
+ }
10
+ else if (Array.isArray(content)) {
11
+ for (const block of content) {
12
+ const b = block;
13
+ if (b.type === "text" && typeof b.text === "string") {
14
+ texts.push(b.text);
15
+ }
16
+ }
17
+ }
18
+ return texts;
19
+ }
20
+ const TEXT_BLOCK_TYPES = new Set(["text", "input_text", "output_text", "markdown"]);
21
+ function extractOpenClawSessionText(value) {
22
+ if (typeof value === "string") {
23
+ return value.length > 0 ? [value] : [];
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return value.flatMap((entry) => extractOpenClawSessionText(entry));
27
+ }
28
+ if (!value || typeof value !== "object") {
29
+ return [];
30
+ }
31
+ const record = value;
32
+ const texts = [];
33
+ const blockType = typeof record.type === "string" ? record.type : undefined;
34
+ if (typeof record.text === "string" && (!blockType || TEXT_BLOCK_TYPES.has(blockType))) {
35
+ texts.push(record.text);
36
+ }
37
+ if ("content" in record) {
38
+ texts.push(...extractOpenClawSessionText(record.content));
39
+ }
40
+ if ("message" in record && typeof record.message !== "object") {
41
+ texts.push(...extractOpenClawSessionText(record.message));
42
+ }
43
+ if (typeof record.body === "string") {
44
+ texts.push(record.body);
45
+ }
46
+ if (typeof record.prompt === "string") {
47
+ texts.push(record.prompt);
48
+ }
49
+ return [...new Set(texts.filter((text) => text.length > 0))];
50
+ }
51
+ /**
52
+ * Claude Code session JSONL format
53
+ * Lines have: { role: "user"|"assistant"|"system", content: string|ContentBlock[] }
54
+ */
55
+ const claudeCodeAdapter = {
56
+ name: "claude-code",
57
+ detect(sample) {
58
+ return (typeof sample.role === "string" &&
59
+ ["user", "assistant", "system"].includes(sample.role) &&
60
+ ("content" in sample || "type" in sample));
61
+ },
62
+ extract(line, lineNumber) {
63
+ const role = line.role;
64
+ const normalizedRole = role === "user" || role === "assistant" || role === "system"
65
+ ? role
66
+ : "unknown";
67
+ const texts = extractMessageText(line);
68
+ const timestamp = (line.timestamp ?? line.created_at);
69
+ return texts
70
+ .filter((t) => t.length > 0)
71
+ .map((text) => ({
72
+ text,
73
+ role: normalizedRole,
74
+ sourceFormat: "claude-code",
75
+ lineNumber,
76
+ timestamp,
77
+ }));
78
+ },
79
+ };
80
+ /**
81
+ * OpenClaw cache-trace JSONL format
82
+ * Lines may have request/response payloads, tool outputs, etc.
83
+ */
84
+ const openclawCacheAdapter = {
85
+ name: "openclaw-cache",
86
+ detect(sample) {
87
+ return ("cache_key" in sample ||
88
+ "trace_id" in sample ||
89
+ ("request" in sample && "response" in sample) ||
90
+ ("tool" in sample && "output" in sample));
91
+ },
92
+ extract(line, lineNumber) {
93
+ const records = [];
94
+ const timestamp = (line.timestamp ?? line.ts ?? line.created_at);
95
+ // Extract from request body
96
+ if (line.request && typeof line.request === "object") {
97
+ const req = line.request;
98
+ const texts = extractMessageText(req);
99
+ if (typeof req.body === "string")
100
+ texts.push(req.body);
101
+ if (typeof req.prompt === "string")
102
+ texts.push(req.prompt);
103
+ for (const text of texts.filter((t) => t.length > 0)) {
104
+ records.push({
105
+ text,
106
+ role: "user",
107
+ sourceFormat: "openclaw-cache",
108
+ lineNumber,
109
+ timestamp,
110
+ });
111
+ }
112
+ }
113
+ // Extract from response body
114
+ if (line.response && typeof line.response === "object") {
115
+ const res = line.response;
116
+ const texts = extractMessageText(res);
117
+ if (typeof res.body === "string")
118
+ texts.push(res.body);
119
+ if (typeof res.result === "string")
120
+ texts.push(res.result);
121
+ for (const text of texts.filter((t) => t.length > 0)) {
122
+ records.push({
123
+ text,
124
+ role: "assistant",
125
+ sourceFormat: "openclaw-cache",
126
+ lineNumber,
127
+ timestamp,
128
+ });
129
+ }
130
+ }
131
+ // Extract from tool output
132
+ if (typeof line.output === "string" && line.output.length > 0) {
133
+ records.push({
134
+ text: line.output,
135
+ role: "assistant",
136
+ sourceFormat: "openclaw-cache",
137
+ lineNumber,
138
+ timestamp,
139
+ });
140
+ }
141
+ // Extract from top-level content
142
+ if (typeof line.content === "string" && line.content.length > 0) {
143
+ records.push({
144
+ text: line.content,
145
+ role: "unknown",
146
+ sourceFormat: "openclaw-cache",
147
+ lineNumber,
148
+ timestamp,
149
+ });
150
+ }
151
+ return records;
152
+ },
153
+ };
154
+ /**
155
+ * OpenClaw session JSONL format
156
+ * Lines have: { type: "message", message: { role, content[] } }
157
+ */
158
+ const openclawSessionAdapter = {
159
+ name: "openclaw-session",
160
+ detect(sample) {
161
+ // Session header line (first line of OpenClaw session files)
162
+ if (sample.type === "session" && ("cwd" in sample || "id" in sample))
163
+ return true;
164
+ // Standard message line
165
+ if (sample.type !== "message" || !sample.message || typeof sample.message !== "object") {
166
+ return false;
167
+ }
168
+ const message = sample.message;
169
+ return typeof message.role === "string" && ("content" in message || "text" in message);
170
+ },
171
+ extract(line, lineNumber) {
172
+ // Skip non-message lines (session header, model_change, thinking_level_change, custom, etc.)
173
+ if (line.type !== "message" || !line.message || typeof line.message !== "object") {
174
+ return [];
175
+ }
176
+ const message = line.message;
177
+ const role = message.role;
178
+ const normalizedRole = role === "user" || role === "assistant" || role === "system"
179
+ ? role
180
+ : "unknown";
181
+ const timestamp = (line.timestamp ?? line.ts ?? line.created_at ?? message.timestamp);
182
+ const texts = extractOpenClawSessionText(message.content ?? message.text ?? message);
183
+ return texts.map((text) => ({
184
+ text,
185
+ role: normalizedRole,
186
+ sourceFormat: "openclaw-session",
187
+ lineNumber,
188
+ timestamp,
189
+ }));
190
+ },
191
+ };
192
+ /**
193
+ * Codex session JSONL format
194
+ * Lines have: { type: "message", sender: "user"|"agent", content: string }
195
+ */
196
+ const codexAdapter = {
197
+ name: "codex",
198
+ detect(sample) {
199
+ return ((sample.type === "message" || sample.type === "rollout") &&
200
+ ("sender" in sample || "agent" in sample));
201
+ },
202
+ extract(line, lineNumber) {
203
+ const records = [];
204
+ const timestamp = (line.timestamp ?? line.ts);
205
+ const sender = (line.sender ?? line.agent ?? "unknown");
206
+ const role = sender === "user" ? "user" : sender === "agent" ? "assistant" : "unknown";
207
+ if (typeof line.content === "string" && line.content.length > 0) {
208
+ records.push({ text: line.content, role: role, sourceFormat: "codex", lineNumber, timestamp });
209
+ }
210
+ if (typeof line.message === "string" && line.message.length > 0) {
211
+ records.push({ text: line.message, role: role, sourceFormat: "codex", lineNumber, timestamp });
212
+ }
213
+ return records;
214
+ },
215
+ };
216
+ /**
217
+ * Generic fallback — tries to find text in any JSON structure
218
+ */
219
+ const genericAdapter = {
220
+ name: "generic",
221
+ detect() {
222
+ return true; // Always matches as fallback
223
+ },
224
+ extract(line, lineNumber) {
225
+ const records = [];
226
+ const timestamp = (line.timestamp ?? line.ts ?? line.created_at);
227
+ // Try common text field names
228
+ const textFields = ["content", "text", "message", "body", "prompt", "output", "result"];
229
+ for (const field of textFields) {
230
+ const val = line[field];
231
+ if (typeof val === "string" && val.length > 0) {
232
+ records.push({
233
+ text: val,
234
+ role: "unknown",
235
+ sourceFormat: "generic",
236
+ lineNumber,
237
+ timestamp,
238
+ });
239
+ }
240
+ }
241
+ return records;
242
+ },
243
+ };
244
+ /** All adapters in detection priority order */
245
+ export const FORMAT_ADAPTERS = [
246
+ claudeCodeAdapter,
247
+ openclawCacheAdapter,
248
+ openclawSessionAdapter,
249
+ codexAdapter,
250
+ genericAdapter,
251
+ ];
252
+ /**
253
+ * Auto-detect the format adapter for a JSONL file
254
+ * Uses the first successfully parsed line as a sample
255
+ */
256
+ export function detectFormat(sample, forceFormat) {
257
+ if (forceFormat) {
258
+ const forced = FORMAT_ADAPTERS.find((a) => a.name === forceFormat);
259
+ if (forced)
260
+ return forced;
261
+ }
262
+ for (const adapter of FORMAT_ADAPTERS) {
263
+ if (adapter.detect(sample))
264
+ return adapter;
265
+ }
266
+ // Should never reach here — generic always matches
267
+ return genericAdapter;
268
+ }
@@ -0,0 +1,7 @@
1
+ export { extractJsonl, formatStats } from "./jsonl-extractor.js";
2
+ export { extractMarkdown, isMarkdownFile } from "./markdown-extractor.js";
3
+ export { extractSqlite, isSqliteFile } from "./sqlite-extractor.js";
4
+ export { redactSecrets, containsSecrets } from "./secret-redactor.js";
5
+ export { detectFormat, FORMAT_ADAPTERS } from "./format-adapters.js";
6
+ export { registerExtractCli } from "./cli-extract.js";
7
+ export type { ExtractionOptions, ExtractionStats, ExtractionRecord, RedactionResult, FormatAdapter, } from "./types.js";
@@ -0,0 +1,7 @@
1
+ // Extraction pipeline — barrel export
2
+ export { extractJsonl, formatStats } from "./jsonl-extractor.js";
3
+ export { extractMarkdown, isMarkdownFile } from "./markdown-extractor.js";
4
+ export { extractSqlite, isSqliteFile } from "./sqlite-extractor.js";
5
+ export { redactSecrets, containsSecrets } from "./secret-redactor.js";
6
+ export { detectFormat, FORMAT_ADAPTERS } from "./format-adapters.js";
7
+ export { registerExtractCli } from "./cli-extract.js";
@@ -0,0 +1,32 @@
1
+ import type { LanonasisClient } from "../client.js";
2
+ import type { LanonasisConfig } from "../config.js";
3
+ import type { LocalFallbackWriter } from "../hooks/local-fallback.js";
4
+ import type { ExtractionOptions, ExtractionStats } from "./types.js";
5
+ export interface ExtractionDeps {
6
+ client: LanonasisClient;
7
+ config: LanonasisConfig;
8
+ logger: {
9
+ info(msg: string): void;
10
+ warn(msg: string): void;
11
+ };
12
+ fallback?: LocalFallbackWriter;
13
+ }
14
+ /**
15
+ * Extract memories from a JSONL file with secret redaction
16
+ *
17
+ * Pipeline per record:
18
+ * 1. Parse JSONL line → format adapter → ExtractionRecord
19
+ * 2. Role filter (default: user only)
20
+ * 3. redactSecrets() — strips API keys, tokens, credentials
21
+ * 4. shouldCapture() — filters noise
22
+ * 5. looksLikePromptInjection() — safety check
23
+ * 6. detectMemoryType() + extractTags() — enrichment
24
+ * 7. vector dedup via searchMemories() — skip duplicates
25
+ * 8. createMemory() with idempotency key — store
26
+ * 9. Optional local markdown fallback
27
+ */
28
+ export declare function extractJsonl(options: ExtractionOptions, deps: ExtractionDeps): Promise<ExtractionStats>;
29
+ /**
30
+ * Format extraction stats as a human-readable report
31
+ */
32
+ export declare function formatStats(stats: ExtractionStats, dryRun: boolean): string;
@@ -0,0 +1,207 @@
1
+ // Core streaming JSONL extraction engine
2
+ // Reads any JSONL source, redacts secrets, enriches, deduplicates, stores as memories
3
+ import { createReadStream } from "fs";
4
+ import { createInterface } from "readline";
5
+ import { createHash } from "crypto";
6
+ import { shouldCapture } from "../enrichment/capture-filter.js";
7
+ import { detectMemoryType } from "../enrichment/type-detector.js";
8
+ import { extractTags } from "../enrichment/tag-extractor.js";
9
+ import { looksLikePromptInjection } from "../enrichment/prompt-safety.js";
10
+ import { redactSecrets } from "./secret-redactor.js";
11
+ import { detectFormat } from "./format-adapters.js";
12
+ function idempotencyKey(filePath, lineNumber, textPrefix) {
13
+ return createHash("sha256")
14
+ .update(`${filePath}:${lineNumber}:${textPrefix}`)
15
+ .digest("hex")
16
+ .slice(0, 32);
17
+ }
18
+ /**
19
+ * Extract memories from a JSONL file with secret redaction
20
+ *
21
+ * Pipeline per record:
22
+ * 1. Parse JSONL line → format adapter → ExtractionRecord
23
+ * 2. Role filter (default: user only)
24
+ * 3. redactSecrets() — strips API keys, tokens, credentials
25
+ * 4. shouldCapture() — filters noise
26
+ * 5. looksLikePromptInjection() — safety check
27
+ * 6. detectMemoryType() + extractTags() — enrichment
28
+ * 7. vector dedup via searchMemories() — skip duplicates
29
+ * 8. createMemory() with idempotency key — store
30
+ * 9. Optional local markdown fallback
31
+ */
32
+ export async function extractJsonl(options, deps) {
33
+ const startTime = Date.now();
34
+ const stats = {
35
+ linesRead: 0,
36
+ linesParsed: 0,
37
+ linesSkipped: 0,
38
+ recordsExtracted: 0,
39
+ recordsFiltered: 0,
40
+ recordsDeduped: 0,
41
+ recordsStored: 0,
42
+ secretsRedacted: 0,
43
+ markdownWritten: 0,
44
+ errors: 0,
45
+ durationMs: 0,
46
+ };
47
+ const { filePath, format, channel = "jsonl-extract", dedup = true, dedupThreshold = 0.92, localFallback = false, dryRun = false, limit, strict = false, roles = ["user"], } = options;
48
+ let adapter = null;
49
+ let totalProcessed = 0;
50
+ // Stream the file line by line
51
+ const fileStream = createReadStream(filePath, { encoding: "utf-8" });
52
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
53
+ for await (const line of rl) {
54
+ stats.linesRead++;
55
+ // Check limit
56
+ if (limit && totalProcessed >= limit)
57
+ break;
58
+ // Skip empty lines
59
+ const trimmed = line.trim();
60
+ if (!trimmed) {
61
+ stats.linesSkipped++;
62
+ continue;
63
+ }
64
+ // Parse JSON
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(trimmed);
68
+ }
69
+ catch {
70
+ stats.linesSkipped++;
71
+ stats.errors++;
72
+ continue;
73
+ }
74
+ stats.linesParsed++;
75
+ // Auto-detect format on first valid line
76
+ if (!adapter) {
77
+ adapter = detectFormat(parsed, format);
78
+ deps.logger.info(`Format detected: ${adapter.name}`);
79
+ }
80
+ // Extract records from this line
81
+ const records = adapter.extract(parsed, stats.linesRead);
82
+ stats.recordsExtracted += records.length;
83
+ // Process each record through the pipeline
84
+ for (const record of records) {
85
+ if (limit && totalProcessed >= limit)
86
+ break;
87
+ // Role filter
88
+ if (roles.length > 0 && !roles.includes(record.role))
89
+ continue;
90
+ // Step 1: Redact secrets FIRST — before any other processing
91
+ const redaction = redactSecrets(record.text);
92
+ stats.secretsRedacted += redaction.secretsFound;
93
+ const cleanText = redaction.text;
94
+ // Step 2: Capture filter
95
+ if (!shouldCapture(cleanText, { strict })) {
96
+ stats.recordsFiltered++;
97
+ continue;
98
+ }
99
+ // Step 3: Prompt injection check
100
+ if (looksLikePromptInjection(cleanText)) {
101
+ stats.recordsFiltered++;
102
+ continue;
103
+ }
104
+ // Step 4: Enrichment
105
+ const memoryType = detectMemoryType(cleanText);
106
+ const tags = [
107
+ ...extractTags(cleanText),
108
+ "jsonl-extract",
109
+ record.sourceFormat,
110
+ ];
111
+ // Step 5: Vector dedup (if enabled and not dry run)
112
+ if (dedup && !dryRun) {
113
+ try {
114
+ const existing = await deps.client.searchMemories({
115
+ query: cleanText.slice(0, 500),
116
+ threshold: dedupThreshold,
117
+ limit: 1,
118
+ });
119
+ if (existing && existing.length > 0) {
120
+ stats.recordsDeduped++;
121
+ continue;
122
+ }
123
+ }
124
+ catch (err) {
125
+ // Dedup failure is non-fatal — proceed to store
126
+ deps.logger.warn(`Dedup check failed: ${err instanceof Error ? err.message : "unknown"}`);
127
+ }
128
+ }
129
+ // Step 6: Store
130
+ if (!dryRun) {
131
+ const title = cleanText.slice(0, 80).replace(/\s+/g, " ").trim();
132
+ const params = {
133
+ title,
134
+ content: cleanText,
135
+ type: memoryType,
136
+ tags,
137
+ metadata: {
138
+ agent_id: deps.config.agentId,
139
+ source: record.sourceFormat,
140
+ channel,
141
+ line_number: record.lineNumber,
142
+ captured_at: record.timestamp ?? new Date().toISOString(),
143
+ secrets_redacted: redaction.secretsFound,
144
+ },
145
+ idempotency_key: idempotencyKey(filePath, record.lineNumber, cleanText.slice(0, 200)),
146
+ };
147
+ try {
148
+ await deps.client.createMemory(params);
149
+ stats.recordsStored++;
150
+ }
151
+ catch (err) {
152
+ stats.errors++;
153
+ deps.logger.warn(`Store failed at line ${record.lineNumber}: ${err instanceof Error ? err.message : "unknown"}`);
154
+ }
155
+ }
156
+ else {
157
+ // Dry run — count as "would store"
158
+ stats.recordsStored++;
159
+ }
160
+ // Step 7: Local markdown fallback
161
+ if (localFallback && deps.fallback) {
162
+ try {
163
+ const title = cleanText.slice(0, 80).replace(/\s+/g, " ").trim();
164
+ await deps.fallback.writeMemory(title, cleanText);
165
+ stats.markdownWritten++;
166
+ }
167
+ catch {
168
+ // Non-fatal
169
+ }
170
+ }
171
+ totalProcessed++;
172
+ // Progress indicator every 500 records
173
+ if (totalProcessed % 500 === 0) {
174
+ deps.logger.info(`Progress: ${totalProcessed} records processed, ${stats.recordsStored} stored, ${stats.secretsRedacted} secrets redacted`);
175
+ }
176
+ }
177
+ }
178
+ stats.durationMs = Date.now() - startTime;
179
+ return stats;
180
+ }
181
+ /**
182
+ * Format extraction stats as a human-readable report
183
+ */
184
+ export function formatStats(stats, dryRun) {
185
+ const lines = [
186
+ "",
187
+ dryRun ? "=== DRY RUN REPORT ===" : "=== EXTRACTION REPORT ===",
188
+ "",
189
+ `Lines read: ${stats.linesRead}`,
190
+ `Lines parsed: ${stats.linesParsed}`,
191
+ `Lines skipped: ${stats.linesSkipped}`,
192
+ "",
193
+ `Records extracted: ${stats.recordsExtracted}`,
194
+ `Records filtered: ${stats.recordsFiltered} (noise/injection)`,
195
+ `Records deduped: ${stats.recordsDeduped} (already in memory)`,
196
+ `Records stored: ${stats.recordsStored}${dryRun ? " (would store)" : ""}`,
197
+ "",
198
+ `Secrets redacted: ${stats.secretsRedacted}`,
199
+ `Errors: ${stats.errors}`,
200
+ "",
201
+ `Duration: ${(stats.durationMs / 1000).toFixed(1)}s`,
202
+ ];
203
+ if (stats.markdownWritten > 0) {
204
+ lines.push(`Markdown written: ${stats.markdownWritten}`);
205
+ }
206
+ return lines.join("\n");
207
+ }
@@ -0,0 +1,23 @@
1
+ import type { ExtractionOptions, ExtractionStats } from "./types.js";
2
+ import type { ExtractionDeps } from "./jsonl-extractor.js";
3
+ /**
4
+ * Detect whether a file is markdown based on extension.
5
+ * Used by the CLI to route to extractMarkdown() vs extractJsonl().
6
+ */
7
+ export declare function isMarkdownFile(filePath: string): boolean;
8
+ /**
9
+ * Extract memories from a Markdown document.
10
+ *
11
+ * Pipeline per section:
12
+ * 1. Split file by headings → MarkdownSection[]
13
+ * 2. Convert to ExtractionRecord[]
14
+ * 3. Role filter (default: user only — all sections are "user")
15
+ * 4. redactSecrets()
16
+ * 5. shouldCapture() — filter noise
17
+ * 6. looksLikePromptInjection() — safety check
18
+ * 7. detectMemoryType() + extractTags() — enrichment
19
+ * 8. Vector dedup via searchMemories()
20
+ * 9. createMemory() with idempotency key
21
+ * 10. Optional local markdown fallback
22
+ */
23
+ export declare function extractMarkdown(options: ExtractionOptions, deps: ExtractionDeps): Promise<ExtractionStats>;