@massu/core 0.4.2 → 0.5.0
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/dist/cli.js +1530 -1177
- package/dist/hooks/post-tool-use.js +75 -6
- package/dist/hooks/user-prompt.js +16 -0
- package/package.json +1 -1
- package/src/commands/init.ts +27 -0
- package/src/hooks/post-tool-use.ts +17 -0
- package/src/hooks/user-prompt.ts +21 -0
- package/src/memory-file-ingest.ts +127 -0
- package/src/memory-tools.ts +34 -1
|
@@ -1606,9 +1606,66 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
|
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
1608
|
// src/hooks/post-tool-use.ts
|
|
1609
|
-
import { readFileSync as
|
|
1609
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
1610
1610
|
import { join as join2 } from "path";
|
|
1611
|
+
import { parse as parseYaml3 } from "yaml";
|
|
1612
|
+
|
|
1613
|
+
// src/memory-file-ingest.ts
|
|
1614
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync } from "fs";
|
|
1611
1615
|
import { parse as parseYaml2 } from "yaml";
|
|
1616
|
+
function ingestMemoryFile(db, sessionId, filePath) {
|
|
1617
|
+
if (!existsSync6(filePath)) return "skipped";
|
|
1618
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
1619
|
+
const basename2 = (filePath.split("/").pop() ?? "").replace(".md", "");
|
|
1620
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1621
|
+
let name = basename2;
|
|
1622
|
+
let description = "";
|
|
1623
|
+
let type = "discovery";
|
|
1624
|
+
let confidence;
|
|
1625
|
+
if (frontmatterMatch) {
|
|
1626
|
+
try {
|
|
1627
|
+
const fm = parseYaml2(frontmatterMatch[1]);
|
|
1628
|
+
name = fm.name ?? basename2;
|
|
1629
|
+
description = fm.description ?? "";
|
|
1630
|
+
type = fm.type ?? "discovery";
|
|
1631
|
+
confidence = fm.confidence != null ? Number(fm.confidence) : void 0;
|
|
1632
|
+
} catch {
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const obsType = mapMemoryTypeToObservationType(type);
|
|
1636
|
+
const importance = confidence != null ? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1))) : 4;
|
|
1637
|
+
const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
|
|
1638
|
+
const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : "";
|
|
1639
|
+
const title = `[memory-file] ${name}`;
|
|
1640
|
+
const detail = description ? `${description}
|
|
1641
|
+
|
|
1642
|
+
${body}` : body;
|
|
1643
|
+
const existing = db.prepare(
|
|
1644
|
+
"SELECT id FROM observations WHERE title = ? LIMIT 1"
|
|
1645
|
+
).get(title);
|
|
1646
|
+
if (existing) {
|
|
1647
|
+
db.prepare("UPDATE observations SET detail = ?, importance = ? WHERE id = ?").run(detail, importance, existing.id);
|
|
1648
|
+
return "updated";
|
|
1649
|
+
} else {
|
|
1650
|
+
addObservation(db, sessionId, obsType, title, detail, { importance });
|
|
1651
|
+
return "inserted";
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function mapMemoryTypeToObservationType(memoryType) {
|
|
1655
|
+
switch (memoryType) {
|
|
1656
|
+
case "user":
|
|
1657
|
+
case "feedback":
|
|
1658
|
+
return "decision";
|
|
1659
|
+
case "project":
|
|
1660
|
+
return "feature";
|
|
1661
|
+
case "reference":
|
|
1662
|
+
return "discovery";
|
|
1663
|
+
default:
|
|
1664
|
+
return "discovery";
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/hooks/post-tool-use.ts
|
|
1612
1669
|
var seenReads = /* @__PURE__ */ new Set();
|
|
1613
1670
|
var currentSessionId = null;
|
|
1614
1671
|
async function main() {
|
|
@@ -1705,6 +1762,18 @@ async function main() {
|
|
|
1705
1762
|
}
|
|
1706
1763
|
} catch (_memoryErr) {
|
|
1707
1764
|
}
|
|
1765
|
+
try {
|
|
1766
|
+
if (tool_name === "Edit" || tool_name === "Write") {
|
|
1767
|
+
const filePath = tool_input.file_path ?? "";
|
|
1768
|
+
if (filePath && filePath.includes("/memory/") && filePath.endsWith(".md")) {
|
|
1769
|
+
const basename2 = filePath.split("/").pop() ?? "";
|
|
1770
|
+
if (basename2 !== "MEMORY.md") {
|
|
1771
|
+
ingestMemoryFile(db, session_id, filePath);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
} catch (_memoryIngestErr) {
|
|
1776
|
+
}
|
|
1708
1777
|
try {
|
|
1709
1778
|
if (tool_name === "Edit" || tool_name === "Write") {
|
|
1710
1779
|
const filePath = tool_input.file_path ?? "";
|
|
@@ -1768,9 +1837,9 @@ function readConventions(cwd) {
|
|
|
1768
1837
|
try {
|
|
1769
1838
|
const projectRoot = cwd ?? process.cwd();
|
|
1770
1839
|
const configPath = join2(projectRoot, "massu.config.yaml");
|
|
1771
|
-
if (!
|
|
1772
|
-
const content =
|
|
1773
|
-
const parsed =
|
|
1840
|
+
if (!existsSync7(configPath)) return defaults;
|
|
1841
|
+
const content = readFileSync6(configPath, "utf-8");
|
|
1842
|
+
const parsed = parseYaml3(content);
|
|
1774
1843
|
if (!parsed || typeof parsed !== "object") return defaults;
|
|
1775
1844
|
const conventions = parsed.conventions;
|
|
1776
1845
|
if (!conventions || typeof conventions !== "object") return defaults;
|
|
@@ -1797,11 +1866,11 @@ function isKnowledgeSourceFile(filePath) {
|
|
|
1797
1866
|
function checkMemoryFileIntegrity(filePath) {
|
|
1798
1867
|
const issues = [];
|
|
1799
1868
|
try {
|
|
1800
|
-
if (!
|
|
1869
|
+
if (!existsSync7(filePath)) {
|
|
1801
1870
|
issues.push("MEMORY.md file does not exist after write");
|
|
1802
1871
|
return issues;
|
|
1803
1872
|
}
|
|
1804
|
-
const content =
|
|
1873
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
1805
1874
|
const lines = content.split("\n");
|
|
1806
1875
|
const MAX_LINES = 200;
|
|
1807
1876
|
if (lines.length > MAX_LINES) {
|
|
@@ -951,6 +951,22 @@ async function main() {
|
|
|
951
951
|
}
|
|
952
952
|
} catch (_knowledgeErr) {
|
|
953
953
|
}
|
|
954
|
+
try {
|
|
955
|
+
const significantSignals = ["fix", "implement", "migrate", "refactor", "debug", "decision", "chose", "architecture", "redesign", "rewrite"];
|
|
956
|
+
const promptLower = prompt.toLowerCase();
|
|
957
|
+
const signalCount = significantSignals.filter((s) => promptLower.includes(s)).length;
|
|
958
|
+
if (signalCount >= 2) {
|
|
959
|
+
const memoryFileCount = db.prepare(
|
|
960
|
+
"SELECT COUNT(*) as count FROM observations WHERE session_id = ? AND title LIKE '[memory-file] %'"
|
|
961
|
+
).get(session_id);
|
|
962
|
+
if (memoryFileCount.count === 0) {
|
|
963
|
+
process.stderr.write(
|
|
964
|
+
"\n[MEMORY REMINDER] Significant work detected but no memory files have been written.\nConsider saving learnings to memory/*.md files for future sessions.\n\n"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch (_memoryNagErr) {
|
|
969
|
+
}
|
|
954
970
|
} finally {
|
|
955
971
|
db.close();
|
|
956
972
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 43 workflow commands",
|
|
6
6
|
"main": "src/server.ts",
|
package/src/commands/init.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
|
|
|
17
17
|
import { resolve, basename, dirname } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { homedir } from 'os';
|
|
20
|
+
import { backfillMemoryFiles } from '../memory-file-ingest.ts';
|
|
20
21
|
|
|
21
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
23
|
const __dirname = dirname(__filename);
|
|
@@ -588,6 +589,32 @@ export async function runInit(): Promise<void> {
|
|
|
588
589
|
console.log(' Created initial MEMORY.md');
|
|
589
590
|
}
|
|
590
591
|
|
|
592
|
+
// Step 6b: Auto-backfill existing memory files into database
|
|
593
|
+
try {
|
|
594
|
+
const claudeDirName = '.claude';
|
|
595
|
+
const encodedRoot = projectRoot.replace(/\//g, '-');
|
|
596
|
+
const computedMemoryDir = resolve(homedir(), claudeDirName, 'projects', encodedRoot, 'memory');
|
|
597
|
+
|
|
598
|
+
const memFiles = existsSync(computedMemoryDir)
|
|
599
|
+
? readdirSync(computedMemoryDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md')
|
|
600
|
+
: [];
|
|
601
|
+
|
|
602
|
+
if (memFiles.length > 0) {
|
|
603
|
+
const { getMemoryDb } = await import('../memory-db.ts');
|
|
604
|
+
const db = getMemoryDb();
|
|
605
|
+
try {
|
|
606
|
+
const stats = backfillMemoryFiles(db, computedMemoryDir, `init-${Date.now()}`);
|
|
607
|
+
if (stats.inserted > 0 || stats.updated > 0) {
|
|
608
|
+
console.log(` Backfilled ${stats.inserted + stats.updated} memory files into database (${stats.inserted} new, ${stats.updated} updated)`);
|
|
609
|
+
}
|
|
610
|
+
} finally {
|
|
611
|
+
db.close();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (_backfillErr) {
|
|
615
|
+
// Best-effort: don't fail init if backfill fails
|
|
616
|
+
}
|
|
617
|
+
|
|
591
618
|
// Step 7: Databases info
|
|
592
619
|
console.log(' Databases will auto-create on first session');
|
|
593
620
|
|
|
@@ -17,6 +17,7 @@ import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
|
|
|
17
17
|
import { readFileSync, existsSync } from 'fs';
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
import { ingestMemoryFile } from '../memory-file-ingest.ts';
|
|
20
21
|
|
|
21
22
|
interface HookInput {
|
|
22
23
|
session_id: string;
|
|
@@ -149,6 +150,22 @@ async function main(): Promise<void> {
|
|
|
149
150
|
// Best-effort: never block post-tool-use
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// Memory file auto-ingest: when Claude writes a memory/*.md file,
|
|
154
|
+
// parse frontmatter and ingest into observations table
|
|
155
|
+
try {
|
|
156
|
+
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
157
|
+
const filePath = (tool_input.file_path as string) ?? '';
|
|
158
|
+
if (filePath && filePath.includes('/memory/') && filePath.endsWith('.md')) {
|
|
159
|
+
const basename = filePath.split('/').pop() ?? '';
|
|
160
|
+
if (basename !== 'MEMORY.md') {
|
|
161
|
+
ingestMemoryFile(db, session_id, filePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (_memoryIngestErr) {
|
|
166
|
+
// Best-effort: never block post-tool-use
|
|
167
|
+
}
|
|
168
|
+
|
|
152
169
|
// Knowledge index staleness check on knowledge file edits
|
|
153
170
|
try {
|
|
154
171
|
if (tool_name === 'Edit' || tool_name === 'Write') {
|
package/src/hooks/user-prompt.ts
CHANGED
|
@@ -86,6 +86,27 @@ async function main(): Promise<void> {
|
|
|
86
86
|
} catch (_knowledgeErr) {
|
|
87
87
|
// Best-effort: never block prompt capture
|
|
88
88
|
}
|
|
89
|
+
// 6. Memory enforcement: nag when significant work detected but no memory ingestion
|
|
90
|
+
try {
|
|
91
|
+
const significantSignals = ['fix', 'implement', 'migrate', 'refactor', 'debug', 'decision', 'chose', 'architecture', 'redesign', 'rewrite'];
|
|
92
|
+
const promptLower = prompt.toLowerCase();
|
|
93
|
+
const signalCount = significantSignals.filter(s => promptLower.includes(s)).length;
|
|
94
|
+
|
|
95
|
+
if (signalCount >= 2) {
|
|
96
|
+
const memoryFileCount = db.prepare(
|
|
97
|
+
"SELECT COUNT(*) as count FROM observations WHERE session_id = ? AND title LIKE '[memory-file] %'"
|
|
98
|
+
).get(session_id) as { count: number };
|
|
99
|
+
|
|
100
|
+
if (memoryFileCount.count === 0) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
'\n[MEMORY REMINDER] Significant work detected but no memory files have been written.\n' +
|
|
103
|
+
'Consider saving learnings to memory/*.md files for future sessions.\n\n'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (_memoryNagErr) {
|
|
108
|
+
// Best-effort: never block prompt capture
|
|
109
|
+
}
|
|
89
110
|
} finally {
|
|
90
111
|
db.close();
|
|
91
112
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Memory File Auto-Ingest
|
|
6
|
+
// Shared module for parsing memory/*.md files and ingesting
|
|
7
|
+
// their YAML frontmatter + content into the observations table.
|
|
8
|
+
// Used by: post-tool-use.ts, memory-tools.ts, init.ts
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
import type Database from 'better-sqlite3';
|
|
12
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { parse as parseYaml } from 'yaml';
|
|
15
|
+
import { addObservation } from './memory-db.ts';
|
|
16
|
+
|
|
17
|
+
export type IngestResult = 'inserted' | 'updated' | 'skipped';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a memory/*.md file's YAML frontmatter and ingest it into the
|
|
21
|
+
* observations table. Deduplicates by title prefix `[memory-file] {name}`.
|
|
22
|
+
*
|
|
23
|
+
* @returns 'inserted' | 'updated' | 'skipped'
|
|
24
|
+
*/
|
|
25
|
+
export function ingestMemoryFile(
|
|
26
|
+
db: Database.Database,
|
|
27
|
+
sessionId: string,
|
|
28
|
+
filePath: string,
|
|
29
|
+
): IngestResult {
|
|
30
|
+
if (!existsSync(filePath)) return 'skipped';
|
|
31
|
+
|
|
32
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
33
|
+
const basename = (filePath.split('/').pop() ?? '').replace('.md', '');
|
|
34
|
+
|
|
35
|
+
// Parse YAML frontmatter (between first --- and second ---)
|
|
36
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
37
|
+
|
|
38
|
+
let name = basename;
|
|
39
|
+
let description = '';
|
|
40
|
+
let type = 'discovery';
|
|
41
|
+
let confidence: number | undefined;
|
|
42
|
+
|
|
43
|
+
if (frontmatterMatch) {
|
|
44
|
+
try {
|
|
45
|
+
const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
|
|
46
|
+
name = (fm.name as string) ?? basename;
|
|
47
|
+
description = (fm.description as string) ?? '';
|
|
48
|
+
type = (fm.type as string) ?? 'discovery';
|
|
49
|
+
confidence = fm.confidence != null ? Number(fm.confidence) : undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
// Use defaults if YAML parsing fails
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Map memory types to observation types
|
|
56
|
+
const obsType = mapMemoryTypeToObservationType(type);
|
|
57
|
+
|
|
58
|
+
// Calculate importance from confidence (0.0-1.0 -> 1-5)
|
|
59
|
+
const importance = confidence != null
|
|
60
|
+
? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1)))
|
|
61
|
+
: 4;
|
|
62
|
+
|
|
63
|
+
// Extract body (after second ---)
|
|
64
|
+
const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
|
|
65
|
+
const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : '';
|
|
66
|
+
|
|
67
|
+
const title = `[memory-file] ${name}`;
|
|
68
|
+
const detail = description ? `${description}\n\n${body}` : body;
|
|
69
|
+
|
|
70
|
+
// Deduplicate: check if this exact title exists
|
|
71
|
+
const existing = db.prepare(
|
|
72
|
+
'SELECT id FROM observations WHERE title = ? LIMIT 1'
|
|
73
|
+
).get(title) as { id: number } | undefined;
|
|
74
|
+
|
|
75
|
+
if (existing) {
|
|
76
|
+
db.prepare('UPDATE observations SET detail = ?, importance = ? WHERE id = ?')
|
|
77
|
+
.run(detail, importance, existing.id);
|
|
78
|
+
return 'updated';
|
|
79
|
+
} else {
|
|
80
|
+
addObservation(db, sessionId, obsType, title, detail, { importance });
|
|
81
|
+
return 'inserted';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Bulk-ingest all memory/*.md files from a directory.
|
|
87
|
+
* Skips MEMORY.md (the index file).
|
|
88
|
+
*
|
|
89
|
+
* @returns { inserted, updated, skipped, total }
|
|
90
|
+
*/
|
|
91
|
+
export function backfillMemoryFiles(
|
|
92
|
+
db: Database.Database,
|
|
93
|
+
memoryDir: string,
|
|
94
|
+
sessionId?: string,
|
|
95
|
+
): { inserted: number; updated: number; skipped: number; total: number } {
|
|
96
|
+
const stats = { inserted: 0, updated: 0, skipped: 0, total: 0 };
|
|
97
|
+
|
|
98
|
+
if (!existsSync(memoryDir)) return stats;
|
|
99
|
+
|
|
100
|
+
const files = readdirSync(memoryDir).filter(
|
|
101
|
+
f => f.endsWith('.md') && f !== 'MEMORY.md'
|
|
102
|
+
);
|
|
103
|
+
stats.total = files.length;
|
|
104
|
+
|
|
105
|
+
const sid = sessionId ?? `backfill-${Date.now()}`;
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const result = ingestMemoryFile(db, sid, join(memoryDir, file));
|
|
109
|
+
stats[result]++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return stats;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function mapMemoryTypeToObservationType(memoryType: string): string {
|
|
116
|
+
switch (memoryType) {
|
|
117
|
+
case 'user':
|
|
118
|
+
case 'feedback':
|
|
119
|
+
return 'decision';
|
|
120
|
+
case 'project':
|
|
121
|
+
return 'feature';
|
|
122
|
+
case 'reference':
|
|
123
|
+
return 'discovery';
|
|
124
|
+
default:
|
|
125
|
+
return 'discovery';
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/memory-tools.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
assignImportance,
|
|
14
14
|
createSession,
|
|
15
15
|
} from './memory-db.ts';
|
|
16
|
-
import { getConfig } from './config.ts';
|
|
16
|
+
import { getConfig, getResolvedPaths } from './config.ts';
|
|
17
|
+
import { backfillMemoryFiles } from './memory-file-ingest.ts';
|
|
17
18
|
|
|
18
19
|
/** Prefix a base tool name with the configured tool prefix. */
|
|
19
20
|
function p(baseName: string): string {
|
|
@@ -101,6 +102,16 @@ export function getMemoryToolDefinitions(): ToolDefinition[] {
|
|
|
101
102
|
required: [],
|
|
102
103
|
},
|
|
103
104
|
},
|
|
105
|
+
// P4-007: memory_backfill
|
|
106
|
+
{
|
|
107
|
+
name: p('memory_backfill'),
|
|
108
|
+
description: 'Scan all memory/*.md files and ingest into database. Run after massu init or to recover from DB loss. Parses YAML frontmatter and deduplicates by title.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {},
|
|
112
|
+
required: [],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
104
115
|
// P4-006: memory_ingest
|
|
105
116
|
{
|
|
106
117
|
name: p('memory_ingest'),
|
|
@@ -154,6 +165,8 @@ export function handleMemoryToolCall(
|
|
|
154
165
|
return handleFailures(args, memoryDb);
|
|
155
166
|
case 'memory_ingest':
|
|
156
167
|
return handleIngest(args, memoryDb);
|
|
168
|
+
case 'memory_backfill':
|
|
169
|
+
return handleBackfill(memoryDb);
|
|
157
170
|
default:
|
|
158
171
|
return text(`Unknown memory tool: ${name}`);
|
|
159
172
|
}
|
|
@@ -374,6 +387,26 @@ function handleIngest(args: Record<string, unknown>, db: Database.Database): Too
|
|
|
374
387
|
return text(`Observation #${id} recorded successfully.\nType: ${type}\nTitle: ${title}\nImportance: ${importance}\nSession: ${activeSession.session_id.slice(0, 8)}...`);
|
|
375
388
|
}
|
|
376
389
|
|
|
390
|
+
function handleBackfill(db: Database.Database): ToolResult {
|
|
391
|
+
const memoryDir = getResolvedPaths().memoryDir;
|
|
392
|
+
const stats = backfillMemoryFiles(db, memoryDir);
|
|
393
|
+
|
|
394
|
+
const lines = [
|
|
395
|
+
'## Memory Backfill Results',
|
|
396
|
+
'',
|
|
397
|
+
`- **Total files scanned**: ${stats.total}`,
|
|
398
|
+
`- **Inserted (new)**: ${stats.inserted}`,
|
|
399
|
+
`- **Updated (existing)**: ${stats.updated}`,
|
|
400
|
+
`- **Skipped (not found)**: ${stats.skipped}`,
|
|
401
|
+
'',
|
|
402
|
+
stats.total === 0
|
|
403
|
+
? 'No memory files found in memory directory.'
|
|
404
|
+
: `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`,
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
return text(lines.join('\n'));
|
|
408
|
+
}
|
|
409
|
+
|
|
377
410
|
// ============================================================
|
|
378
411
|
// Helpers
|
|
379
412
|
// ============================================================
|