tribunal-kit 7.0.0 → 8.0.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/.agent/config/claude.json +18 -0
- package/.agent/config/plugin.json +102 -0
- package/.agent/config/slash-commands.json +103 -0
- package/.agent/config/specialist-registry.json +415 -0
- package/.agent/config/system-prompt.md +191 -0
- package/.agent/history/architecture-graph.yaml +302 -3
- package/.agent/history/graph-cache.json +515 -45
- package/.agent/history/memory.db +0 -0
- package/.agent/history/snapshots/bin__adapter-install.js.json +13 -0
- package/.agent/history/snapshots/bin__global-store.js.json +15 -0
- package/.agent/history/snapshots/bin__mcp-server.js.json +19 -0
- package/.agent/history/snapshots/bin__proxy-server.js.json +20 -0
- package/.agent/history/snapshots/bin__spawn-agent.js.json +12 -0
- package/.agent/history/snapshots/bin__tk-proxy.js.json +21 -0
- package/.agent/history/snapshots/bin__tribunal-kit.js.json +9 -7
- package/.agent/history/snapshots/bin__wrapper.js.json +14 -0
- package/.agent/history/snapshots/eslint.config.js.json +1 -2
- package/.agent/history/snapshots/scripts__benchmark.js.json +14 -0
- package/.agent/history/snapshots/scripts__changelog.js.json +1 -2
- package/.agent/history/snapshots/scripts__fix-vbc.js.json +11 -0
- package/.agent/history/snapshots/scripts__stress_benchmark.js.json +15 -0
- package/.agent/history/snapshots/scripts__sync-version.js.json +1 -2
- package/.agent/history/snapshots/scripts__validate-payload.js.json +1 -2
- package/.agent/history/snapshots/scripts__visual_audit.js.json +11 -0
- package/.agent/history/snapshots/test__integration__bridges.test.js.json +1 -2
- package/.agent/history/snapshots/test__integration__graceful_degradation.test.js.json +12 -0
- package/.agent/history/snapshots/test__integration__init.test.js.json +1 -2
- package/.agent/history/snapshots/test__integration__minimal_change_pipeline.test.js.json +13 -0
- package/.agent/history/snapshots/test__integration__parallel_tribunal.test.js.json +12 -0
- package/.agent/history/snapshots/test__integration__routing.test.js.json +1 -2
- package/.agent/history/snapshots/test__integration__swarm_dispatcher.test.js.json +1 -2
- package/.agent/history/snapshots/test__integration__sync_status.test.js.json +13 -0
- package/.agent/history/snapshots/test__integration__wave2.test.js.json +1 -2
- package/.agent/history/snapshots/test__integration__wrapper.test.js.json +13 -0
- package/.agent/history/snapshots/test__unit__align.test.js.json +10 -0
- package/.agent/history/snapshots/test__unit__args.test.js.json +3 -3
- package/.agent/history/snapshots/test__unit__audit_release.test.js.json +16 -0
- package/.agent/history/snapshots/test__unit__case_law_manager.test.js.json +1 -2
- package/.agent/history/snapshots/test__unit__cicd_validator.test.js.json +12 -0
- package/.agent/history/snapshots/test__unit__compile.test.js.json +10 -0
- package/.agent/history/snapshots/test__unit__context_broker.test.js.json +1 -2
- package/.agent/history/snapshots/test__unit__contract_engine.test.js.json +13 -0
- package/.agent/history/snapshots/test__unit__copyDir.test.js.json +3 -3
- package/.agent/history/snapshots/test__unit__graph_tools.test.js.json +1 -2
- package/.agent/history/snapshots/test__unit__guardrail_engine.test.js.json +10 -0
- package/.agent/history/snapshots/test__unit__impact_classifier.test.js.json +12 -0
- package/.agent/history/snapshots/test__unit__init.test.js.json +14 -0
- package/.agent/history/snapshots/test__unit__inner_loop_validator.test.js.json +1 -2
- package/.agent/history/snapshots/test__unit__integrity_manifest.test.js.json +13 -0
- package/.agent/history/snapshots/test__unit__learn.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__marathon.test.js.json +22 -0
- package/.agent/history/snapshots/test__unit__mcp_server.test.js.json +16 -0
- package/.agent/history/snapshots/test__unit__memory.test.js.json +13 -0
- package/.agent/history/snapshots/test__unit__minimal_change.test.js.json +10 -0
- package/.agent/history/snapshots/test__unit__native.test.js.json +10 -0
- package/.agent/history/snapshots/test__unit__optimize.test.js.json +13 -0
- package/.agent/history/snapshots/test__unit__path_resolution.test.js.json +14 -0
- package/.agent/history/snapshots/test__unit__production_readiness_evidence.test.js.json +21 -0
- package/.agent/history/snapshots/test__unit__selfInstall.test.js.json +3 -3
- package/.agent/history/snapshots/test__unit__semver.test.js.json +3 -3
- package/.agent/history/snapshots/test__unit__skill_evolution.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__stress.test.js.json +15 -0
- package/.agent/history/snapshots/test__unit__swarm_dispatcher.test.js.json +3 -3
- package/.agent/history/snapshots/test__unit__utils.test.js.json +10 -0
- package/.agent/scripts/ast_context_loader.js +137 -0
- package/.agent/scripts/memory_engine.js +581 -0
- package/.agent/scripts/payload_schemas.js +146 -0
- package/.agent/scripts/prompt_compiler.js +59 -11
- package/.agent/scripts/swarm_dispatcher.js +295 -67
- package/.agent/scripts/token_budget_broker.js +117 -6
- package/.claude/CLAUDE.md +442 -0
- package/README.md +22 -12
- package/SECURITY.md +3 -3
- package/bin/adapter-install.js +240 -0
- package/bin/global-store.js +48 -0
- package/bin/mcp-server.js +568 -252
- package/bin/proxy-server.js +113 -0
- package/bin/spawn-agent.js +39 -0
- package/bin/tk-proxy.js +34 -0
- package/dist/commands/init.js +8 -51
- package/dist/commands/status.js +61 -36
- package/dist/commands/validate.js +22 -31
- package/dist/tui/banner.js +77 -0
- package/dist/tui/index.js +21 -0
- package/dist/tui/reviewer-grid.js +90 -0
- package/dist/tui/shimmer.js +91 -0
- package/dist/tui/theme.js +130 -0
- package/dist/tui/tree.js +93 -0
- package/dist/tui/wizard.js +148 -0
- package/dist/utils/helpers.js +5 -41
- package/package.json +27 -10
- package/scripts/build-graph.js +71 -0
- package/.agent/history/snapshots/migrate_refs.js.json +0 -11
- package/.agent/scripts/compile_router.py +0 -5
- package/.agent/scripts/migrate_skills_frontmatter.py +0 -5
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* memory_engine.js — Tribunal Kit 4-Type Taxonomy Persistent Memory Engine
|
|
4
|
+
* ==============================================================================
|
|
5
|
+
* SQLite-backed memory storage with budget-constrained recall.
|
|
6
|
+
*
|
|
7
|
+
* Memory Types:
|
|
8
|
+
* - semantic: Permanent facts (no expiration)
|
|
9
|
+
* - procedural: How-to recipes (no expiration)
|
|
10
|
+
* - episodic: Events with 30-day TTL
|
|
11
|
+
* - working: Session scratch (cleared on restart)
|
|
12
|
+
*
|
|
13
|
+
* Recall Algorithm:
|
|
14
|
+
* score = relevance × recency × priority
|
|
15
|
+
* where relevance = token overlap with query
|
|
16
|
+
* recency = -0.1 × days since last access
|
|
17
|
+
* priority = explicit priority (default: 1.0)
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const { MemoryEngine } = require('./memory_engine');
|
|
21
|
+
* const mem = new MemoryEngine(agentDir);
|
|
22
|
+
* mem.store('semantic', 'User prefers TypeScript', ['typescript', 'preference']);
|
|
23
|
+
* const { results } = mem.recall('typescript', 2000);
|
|
24
|
+
*
|
|
25
|
+
* Zero external dependencies beyond better-sqlite3 (already installed).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
// Lazy-load better-sqlite3 (optional dependency for environments without native compilation)
|
|
34
|
+
let Database = null;
|
|
35
|
+
try {
|
|
36
|
+
Database = require('better-sqlite3');
|
|
37
|
+
} catch (_err) {
|
|
38
|
+
// Fallback to in-memory storage if better-sqlite3 not available
|
|
39
|
+
console.warn('[Memory] better-sqlite3 not available, using in-memory fallback');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Memory Types ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const MEMORY_TYPES = {
|
|
45
|
+
SEMANTIC: 'semantic', // Permanent facts
|
|
46
|
+
PROCEDURAL: 'procedural', // How-to recipes
|
|
47
|
+
EPISODIC: 'episodic', // Events (30-day TTL)
|
|
48
|
+
WORKING: 'working', // Session scratch
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const VALID_TYPES = Object.values(MEMORY_TYPES);
|
|
52
|
+
|
|
53
|
+
// ─── In-Memory Fallback Storage ───────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
class InMemoryStore {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.memories = new Map();
|
|
58
|
+
this.nextId = 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
prepare(sql) {
|
|
62
|
+
const self = this;
|
|
63
|
+
|
|
64
|
+
// Simple SQL-like operations on in-memory Map
|
|
65
|
+
return {
|
|
66
|
+
run(...params) {
|
|
67
|
+
// INSERT
|
|
68
|
+
if (sql.includes('INSERT INTO memories')) {
|
|
69
|
+
const [type, content, tags, priority, expiresAt] = params;
|
|
70
|
+
const id = self.nextId++;
|
|
71
|
+
self.memories.set(id, {
|
|
72
|
+
id,
|
|
73
|
+
type,
|
|
74
|
+
content,
|
|
75
|
+
tags: JSON.parse(tags),
|
|
76
|
+
priority,
|
|
77
|
+
created_at: new Date().toISOString(),
|
|
78
|
+
accessed_at: new Date().toISOString(),
|
|
79
|
+
expires_at: expiresAt,
|
|
80
|
+
access_count: 0,
|
|
81
|
+
});
|
|
82
|
+
return { lastInsertRowid: id, changes: 1 };
|
|
83
|
+
}
|
|
84
|
+
// UPDATE
|
|
85
|
+
if (sql.includes('UPDATE memories')) {
|
|
86
|
+
for (const [_id, mem] of self.memories) {
|
|
87
|
+
mem.accessed_at = new Date().toISOString();
|
|
88
|
+
mem.access_count++;
|
|
89
|
+
}
|
|
90
|
+
return { changes: self.memories.size };
|
|
91
|
+
}
|
|
92
|
+
// DELETE
|
|
93
|
+
if (sql.includes('DELETE FROM memories')) {
|
|
94
|
+
const before = self.memories.size;
|
|
95
|
+
for (const [id, mem] of self.memories) {
|
|
96
|
+
if (
|
|
97
|
+
mem.type === 'episodic' &&
|
|
98
|
+
mem.expires_at &&
|
|
99
|
+
new Date(mem.expires_at) < new Date()
|
|
100
|
+
) {
|
|
101
|
+
self.memories.delete(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { changes: before - self.memories.size };
|
|
105
|
+
}
|
|
106
|
+
return { changes: 0 };
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
all(...params) {
|
|
110
|
+
const [query] = params;
|
|
111
|
+
const results = [];
|
|
112
|
+
|
|
113
|
+
for (const [_id, mem] of self.memories) {
|
|
114
|
+
// Filter expired
|
|
115
|
+
if (mem.expires_at && new Date(mem.expires_at) < new Date()) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Calculate score
|
|
120
|
+
const relevance = (
|
|
121
|
+
mem.content.toLowerCase().match(new RegExp(query.toLowerCase(), 'g')) || []
|
|
122
|
+
).length;
|
|
123
|
+
const daysSinceAccess =
|
|
124
|
+
(Date.now() - new Date(mem.accessed_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
125
|
+
const score = relevance * 3.0 + daysSinceAccess * -0.1 + mem.priority;
|
|
126
|
+
|
|
127
|
+
results.push({ ...mem, score });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return results.sort((a, b) => b.score - a.score);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
get(...params) {
|
|
134
|
+
const [id] = params;
|
|
135
|
+
return self.memories.get(id) || undefined;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
exec(_sql) {
|
|
141
|
+
// No-op for in-memory
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close() {
|
|
145
|
+
this.memories.clear();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Memory Engine Class ──────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
class MemoryEngine {
|
|
152
|
+
/**
|
|
153
|
+
* Initialize the Memory Engine.
|
|
154
|
+
* @param {string} agentDir - Path to .agent/ directory
|
|
155
|
+
* @param {object} options - Configuration options
|
|
156
|
+
*/
|
|
157
|
+
constructor(agentDir, options = {}) {
|
|
158
|
+
this.agentDir = agentDir;
|
|
159
|
+
this.dbPath = path.join(agentDir, 'history', 'memory.db');
|
|
160
|
+
this.options = {
|
|
161
|
+
defaultTTL: 30 * 24 * 60 * 60 * 1000, // 30 days for episodic
|
|
162
|
+
maxWorkingMemorySize: 100, // Max working memory entries
|
|
163
|
+
...options,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Ensure history directory exists
|
|
167
|
+
const historyDir = path.dirname(this.dbPath);
|
|
168
|
+
if (!fs.existsSync(historyDir)) {
|
|
169
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Initialize database
|
|
173
|
+
this.db = this.initDatabase();
|
|
174
|
+
this.initSchema();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Initialize database connection (SQLite or in-memory fallback).
|
|
179
|
+
*/
|
|
180
|
+
initDatabase() {
|
|
181
|
+
if (Database) {
|
|
182
|
+
return new Database(this.dbPath);
|
|
183
|
+
}
|
|
184
|
+
return new InMemoryStore();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create database schema if not exists.
|
|
189
|
+
*/
|
|
190
|
+
initSchema() {
|
|
191
|
+
this.db.exec(`
|
|
192
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
193
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
194
|
+
type TEXT NOT NULL CHECK(type IN ('semantic', 'procedural', 'episodic', 'working')),
|
|
195
|
+
content TEXT NOT NULL,
|
|
196
|
+
tags TEXT,
|
|
197
|
+
priority REAL DEFAULT 1.0,
|
|
198
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
199
|
+
accessed_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
200
|
+
expires_at TEXT,
|
|
201
|
+
access_count INTEGER DEFAULT 0
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_memories_accessed ON memories(accessed_at);
|
|
206
|
+
`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Store a new memory entry.
|
|
211
|
+
* @param {string} type - Memory type (semantic, procedural, episodic, working)
|
|
212
|
+
* @param {string} content - Memory content
|
|
213
|
+
* @param {string[]} tags - Searchable tags
|
|
214
|
+
* @param {object} options - Additional options (priority, expiresIn)
|
|
215
|
+
* @returns {{ id: number, type: string, token_estimate: number }}
|
|
216
|
+
*/
|
|
217
|
+
store(type, content, tags = [], options = {}) {
|
|
218
|
+
// Validate type
|
|
219
|
+
if (!VALID_TYPES.includes(type)) {
|
|
220
|
+
throw new Error(`Invalid memory type: "${type}". Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate content
|
|
224
|
+
if (!content || typeof content !== 'string') {
|
|
225
|
+
throw new Error('Memory content must be a non-empty string');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Calculate expiration
|
|
229
|
+
let expiresAt = null;
|
|
230
|
+
if (type === MEMORY_TYPES.EPISODIC) {
|
|
231
|
+
const expiresIn = options.expiresIn || this.options.defaultTTL;
|
|
232
|
+
expiresAt = new Date(Date.now() + expiresIn).toISOString();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Insert into database
|
|
236
|
+
const stmt = this.db.prepare(`
|
|
237
|
+
INSERT INTO memories (type, content, tags, priority, expires_at)
|
|
238
|
+
VALUES (?, ?, ?, ?, ?)
|
|
239
|
+
`);
|
|
240
|
+
|
|
241
|
+
const result = stmt.run(
|
|
242
|
+
type,
|
|
243
|
+
content,
|
|
244
|
+
JSON.stringify(tags),
|
|
245
|
+
options.priority || 1.0,
|
|
246
|
+
expiresAt,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
id: result.lastInsertRowid,
|
|
251
|
+
type,
|
|
252
|
+
token_estimate: this.estimateTokens(content),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Recall memories matching a query, within token budget.
|
|
258
|
+
* @param {string} query - Search query
|
|
259
|
+
* @param {number} budget - Maximum token budget
|
|
260
|
+
* @returns {{ results: array, tokens_used: number }}
|
|
261
|
+
*/
|
|
262
|
+
recall(query, budget = 2000) {
|
|
263
|
+
const _tokens = this.tokenize(query);
|
|
264
|
+
|
|
265
|
+
// Score by relevance × recency × priority
|
|
266
|
+
const memories = this.db
|
|
267
|
+
.prepare(
|
|
268
|
+
`
|
|
269
|
+
SELECT *, (
|
|
270
|
+
(length(content) - length(replace(lower(content), lower(?), ''))) / length(?) * 3.0
|
|
271
|
+
+ (julianday('now') - julianday(accessed_at)) * -0.1
|
|
272
|
+
+ priority
|
|
273
|
+
) as score
|
|
274
|
+
FROM memories
|
|
275
|
+
WHERE expires_at IS NULL OR expires_at > datetime('now')
|
|
276
|
+
ORDER BY score DESC
|
|
277
|
+
`,
|
|
278
|
+
)
|
|
279
|
+
.all(query, query);
|
|
280
|
+
|
|
281
|
+
// Fit within budget
|
|
282
|
+
const selected = [];
|
|
283
|
+
let usedTokens = 0;
|
|
284
|
+
|
|
285
|
+
for (const mem of memories) {
|
|
286
|
+
const tokens = this.estimateTokens(mem.content);
|
|
287
|
+
if (usedTokens + tokens <= budget) {
|
|
288
|
+
selected.push({
|
|
289
|
+
id: mem.id,
|
|
290
|
+
memory_type: mem.type,
|
|
291
|
+
content: mem.content,
|
|
292
|
+
tags: mem.tags ? JSON.parse(mem.tags) : [],
|
|
293
|
+
priority: mem.priority,
|
|
294
|
+
score: mem.score,
|
|
295
|
+
});
|
|
296
|
+
usedTokens += tokens;
|
|
297
|
+
|
|
298
|
+
// Update access stats
|
|
299
|
+
this.db
|
|
300
|
+
.prepare(
|
|
301
|
+
`
|
|
302
|
+
UPDATE memories
|
|
303
|
+
SET accessed_at = CURRENT_TIMESTAMP, access_count = access_count + 1
|
|
304
|
+
WHERE id = ?
|
|
305
|
+
`,
|
|
306
|
+
)
|
|
307
|
+
.run(mem.id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { results: selected, tokens_used: usedTokens };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get a specific memory by ID.
|
|
316
|
+
* @param {number} id - Memory ID
|
|
317
|
+
* @returns {object|null}
|
|
318
|
+
*/
|
|
319
|
+
get(id) {
|
|
320
|
+
const mem = this.db
|
|
321
|
+
.prepare(
|
|
322
|
+
`
|
|
323
|
+
SELECT * FROM memories WHERE id = ?
|
|
324
|
+
`,
|
|
325
|
+
)
|
|
326
|
+
.get(id);
|
|
327
|
+
|
|
328
|
+
if (!mem) return null;
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
id: mem.id,
|
|
332
|
+
memory_type: mem.type,
|
|
333
|
+
content: mem.content,
|
|
334
|
+
tags: mem.tags ? JSON.parse(mem.tags) : [],
|
|
335
|
+
priority: mem.priority,
|
|
336
|
+
created_at: mem.created_at,
|
|
337
|
+
accessed_at: mem.accessed_at,
|
|
338
|
+
access_count: mem.access_count,
|
|
339
|
+
expires_at: mem.expires_at,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Delete a memory by ID.
|
|
345
|
+
* @param {number} id - Memory ID
|
|
346
|
+
* @returns {boolean} True if deleted
|
|
347
|
+
*/
|
|
348
|
+
delete(id) {
|
|
349
|
+
const result = this.db
|
|
350
|
+
.prepare(
|
|
351
|
+
`
|
|
352
|
+
DELETE FROM memories WHERE id = ?
|
|
353
|
+
`,
|
|
354
|
+
)
|
|
355
|
+
.run(id);
|
|
356
|
+
|
|
357
|
+
return result.changes > 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Clear all working memories (session scratch).
|
|
362
|
+
*/
|
|
363
|
+
clearWorking() {
|
|
364
|
+
this.db
|
|
365
|
+
.prepare(
|
|
366
|
+
`
|
|
367
|
+
DELETE FROM memories WHERE type = 'working'
|
|
368
|
+
`,
|
|
369
|
+
)
|
|
370
|
+
.run();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Auto-expire episodic memories.
|
|
375
|
+
*/
|
|
376
|
+
expire() {
|
|
377
|
+
this.db
|
|
378
|
+
.prepare(
|
|
379
|
+
`
|
|
380
|
+
DELETE FROM memories
|
|
381
|
+
WHERE type = 'episodic' AND expires_at < datetime('now')
|
|
382
|
+
`,
|
|
383
|
+
)
|
|
384
|
+
.run();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get memory statistics.
|
|
389
|
+
*/
|
|
390
|
+
stats() {
|
|
391
|
+
const stats = this.db
|
|
392
|
+
.prepare(
|
|
393
|
+
`
|
|
394
|
+
SELECT
|
|
395
|
+
type,
|
|
396
|
+
COUNT(*) as count,
|
|
397
|
+
SUM(length(content)) as total_chars
|
|
398
|
+
FROM memories
|
|
399
|
+
WHERE expires_at IS NULL OR expires_at > datetime('now')
|
|
400
|
+
GROUP BY type
|
|
401
|
+
`,
|
|
402
|
+
)
|
|
403
|
+
.all();
|
|
404
|
+
|
|
405
|
+
const result = {
|
|
406
|
+
total: 0,
|
|
407
|
+
by_type: {},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
for (const stat of stats) {
|
|
411
|
+
result.total += stat.count;
|
|
412
|
+
result.by_type[stat.type] = {
|
|
413
|
+
count: stat.count,
|
|
414
|
+
total_chars: stat.total_chars,
|
|
415
|
+
estimated_tokens: Math.ceil(stat.total_chars / 4),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Close database connection.
|
|
424
|
+
*/
|
|
425
|
+
close() {
|
|
426
|
+
if (this.db && this.db.close) {
|
|
427
|
+
this.db.close();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Estimate token count for text.
|
|
435
|
+
* Uses rough approximation: 1 token ≈ 4 characters.
|
|
436
|
+
*/
|
|
437
|
+
estimateTokens(text) {
|
|
438
|
+
if (!text) return 0;
|
|
439
|
+
return Math.ceil(text.length / 4);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Tokenize text into lowercase words.
|
|
444
|
+
*/
|
|
445
|
+
tokenize(text) {
|
|
446
|
+
if (!text) return [];
|
|
447
|
+
return (text.match(/\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b/g) || []).map(t => t.toLowerCase());
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Module Exports ────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
module.exports = {
|
|
454
|
+
MemoryEngine,
|
|
455
|
+
MEMORY_TYPES,
|
|
456
|
+
VALID_TYPES,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// ─── CLI Entry Point ───────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
if (require.main === module) {
|
|
462
|
+
const args = process.argv.slice(2);
|
|
463
|
+
|
|
464
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
465
|
+
console.log(`
|
|
466
|
+
\x1b[1mmemory_engine.js\x1b[0m — Tribunal Kit 4-Type Taxonomy Persistent Memory Engine
|
|
467
|
+
|
|
468
|
+
\x1b[1mUsage:\x1b[0m
|
|
469
|
+
node .agent/scripts/memory_engine.js store <type> <content> [--tags tag1,tag2]
|
|
470
|
+
node .agent/scripts/memory_engine.js recall <query> [--budget 2000]
|
|
471
|
+
node .agent/scripts/memory_engine.js get <id>
|
|
472
|
+
node .agent/scripts/memory_engine.js delete <id>
|
|
473
|
+
node .agent/scripts/memory_engine.js stats
|
|
474
|
+
node .agent/scripts/memory_engine.js expire
|
|
475
|
+
|
|
476
|
+
\x1b[1mMemory Types:\x1b[0m
|
|
477
|
+
semantic — Permanent facts (no expiration)
|
|
478
|
+
procedural — How-to recipes (no expiration)
|
|
479
|
+
episodic — Events with 30-day TTL
|
|
480
|
+
working — Session scratch (cleared on restart)
|
|
481
|
+
|
|
482
|
+
\x1b[1mExamples:\x1b[0m
|
|
483
|
+
node .agent/scripts/memory_engine.js store semantic "User prefers TypeScript strict mode" --tags typescript,preference
|
|
484
|
+
node .agent/scripts/memory_engine.js recall "typescript" --budget 1000
|
|
485
|
+
node .agent/scripts/memory_engine.js stats
|
|
486
|
+
`);
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const { findAgentDir } = require('./_utils');
|
|
491
|
+
const agentDir = findAgentDir() || path.resolve(__dirname, '..');
|
|
492
|
+
|
|
493
|
+
const mem = new MemoryEngine(agentDir);
|
|
494
|
+
|
|
495
|
+
const command = args[0];
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
switch (command) {
|
|
499
|
+
case 'store': {
|
|
500
|
+
const type = args[1];
|
|
501
|
+
const content = args[2];
|
|
502
|
+
const tagsIdx = args.indexOf('--tags');
|
|
503
|
+
const tags =
|
|
504
|
+
tagsIdx !== -1 && args[tagsIdx + 1]
|
|
505
|
+
? args[tagsIdx + 1].split(',').map(t => t.trim())
|
|
506
|
+
: [];
|
|
507
|
+
|
|
508
|
+
const result = mem.store(type, content, tags);
|
|
509
|
+
console.log(
|
|
510
|
+
`\x1b[32m✓\x1b[0m Memory stored: #${result.id} (${result.type}, ~${result.token_estimate} tokens)`,
|
|
511
|
+
);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case 'recall': {
|
|
516
|
+
const query = args[1];
|
|
517
|
+
const budgetIdx = args.indexOf('--budget');
|
|
518
|
+
const budget =
|
|
519
|
+
budgetIdx !== -1 && args[budgetIdx + 1] ? parseInt(args[budgetIdx + 1], 10) : 2000;
|
|
520
|
+
|
|
521
|
+
const { results, tokens_used } = mem.recall(query, budget);
|
|
522
|
+
console.log(
|
|
523
|
+
`\n## Memory Recall (${results.length} results, ~${tokens_used}/${budget} tokens)\n`,
|
|
524
|
+
);
|
|
525
|
+
for (const entry of results) {
|
|
526
|
+
console.log(`- **[${entry.memory_type.toUpperCase()}]** #${entry.id}: ${entry.content}`);
|
|
527
|
+
if (entry.tags.length > 0) console.log(` _(${entry.tags.join(', ')})_`);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
case 'get': {
|
|
533
|
+
const id = parseInt(args[1], 10);
|
|
534
|
+
const result = mem.get(id);
|
|
535
|
+
if (!result) {
|
|
536
|
+
console.error(`\x1b[31m✖ Memory #${id} not found\x1b[0m`);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
console.log(JSON.stringify(result, null, 2));
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case 'delete': {
|
|
544
|
+
const id = parseInt(args[1], 10);
|
|
545
|
+
const deleted = mem.delete(id);
|
|
546
|
+
if (deleted) {
|
|
547
|
+
console.log(`\x1b[32m✓\x1b[0m Memory #${id} deleted`);
|
|
548
|
+
} else {
|
|
549
|
+
console.error(`\x1b[31m✖ Memory #${id} not found\x1b[0m`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case 'stats': {
|
|
556
|
+
const stats = mem.stats();
|
|
557
|
+
console.log(`\n## Memory Statistics\n`);
|
|
558
|
+
console.log(`Total memories: ${stats.total}`);
|
|
559
|
+
for (const [type, data] of Object.entries(stats.by_type)) {
|
|
560
|
+
console.log(` ${type}: ${data.count} (~${data.estimated_tokens} tokens)`);
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case 'expire': {
|
|
566
|
+
mem.expire();
|
|
567
|
+
console.log(`\x1b[32m✓\x1b[0m Expired episodic memories cleaned up`);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
default:
|
|
572
|
+
console.error(`\x1b[31m✖ Unknown command: ${command}\x1b[0m`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.error(`\x1b[31m✖ Error: ${err.message}\x1b[0m`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
} finally {
|
|
579
|
+
mem.close();
|
|
580
|
+
}
|
|
581
|
+
}
|