create-walle 0.1.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/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
// --- WALL-E Brain: SQLite Database Layer (WAL mode, better-sqlite3) ---
|
|
2
|
+
const Database = require('better-sqlite3');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
|
|
7
|
+
// Load .env from project root (ensures standalone scripts get env vars too)
|
|
8
|
+
try {
|
|
9
|
+
const _envPath = path.resolve(__dirname, '..', '.env');
|
|
10
|
+
fs.readFileSync(_envPath, 'utf8').split('\n').forEach(line => {
|
|
11
|
+
const m = line.match(/^\s*([^#=]+?)\s*=\s*(.*?)\s*$/);
|
|
12
|
+
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
|
|
13
|
+
});
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
|
|
17
|
+
const DEFAULT_DB_PATH = path.join(DATA_DIR, 'wall-e-brain.db');
|
|
18
|
+
const BACKUP_DIR = path.join(DATA_DIR, 'backups');
|
|
19
|
+
|
|
20
|
+
let db = null;
|
|
21
|
+
let currentDbPath = null;
|
|
22
|
+
|
|
23
|
+
function getDb() {
|
|
24
|
+
if (db) return db;
|
|
25
|
+
throw new Error('Database not initialized. Call initDb() first.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDbPath() {
|
|
29
|
+
return currentDbPath || DEFAULT_DB_PATH;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function initDb(dbPath) {
|
|
33
|
+
dbPath = dbPath || DEFAULT_DB_PATH;
|
|
34
|
+
currentDbPath = dbPath;
|
|
35
|
+
const dir = path.dirname(dbPath);
|
|
36
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const newDb = new Database(dbPath);
|
|
40
|
+
try {
|
|
41
|
+
newDb.pragma('journal_mode = WAL');
|
|
42
|
+
newDb.pragma('busy_timeout = 5000');
|
|
43
|
+
newDb.pragma('foreign_keys = ON');
|
|
44
|
+
db = newDb;
|
|
45
|
+
createTables();
|
|
46
|
+
// Migrations: add skill columns to tasks table if missing
|
|
47
|
+
try { newDb.prepare("SELECT skill FROM tasks LIMIT 0").run(); } catch (_) {
|
|
48
|
+
newDb.prepare("ALTER TABLE tasks ADD COLUMN skill TEXT").run();
|
|
49
|
+
}
|
|
50
|
+
try { newDb.prepare("SELECT skill_config FROM tasks LIMIT 0").run(); } catch (_) {
|
|
51
|
+
newDb.prepare("ALTER TABLE tasks ADD COLUMN skill_config TEXT").run();
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
newDb.close();
|
|
55
|
+
db = null;
|
|
56
|
+
currentDbPath = null;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return db;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createTables() {
|
|
63
|
+
db.exec(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS owner (
|
|
65
|
+
key TEXT PRIMARY KEY,
|
|
66
|
+
value TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
source TEXT NOT NULL,
|
|
72
|
+
source_id TEXT,
|
|
73
|
+
source_channel TEXT,
|
|
74
|
+
memory_type TEXT NOT NULL,
|
|
75
|
+
direction TEXT,
|
|
76
|
+
participants TEXT,
|
|
77
|
+
subject TEXT,
|
|
78
|
+
content TEXT NOT NULL,
|
|
79
|
+
content_raw TEXT,
|
|
80
|
+
metadata TEXT,
|
|
81
|
+
importance REAL DEFAULT 0.5,
|
|
82
|
+
timestamp TEXT NOT NULL,
|
|
83
|
+
ingested_at TEXT DEFAULT (datetime('now')),
|
|
84
|
+
archived_at TEXT,
|
|
85
|
+
extraction_status TEXT DEFAULT 'pending'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
category TEXT NOT NULL,
|
|
91
|
+
subject TEXT NOT NULL,
|
|
92
|
+
predicate TEXT NOT NULL,
|
|
93
|
+
object TEXT NOT NULL,
|
|
94
|
+
confidence REAL DEFAULT 0.5,
|
|
95
|
+
source_memory_ids TEXT,
|
|
96
|
+
first_seen TEXT NOT NULL,
|
|
97
|
+
last_confirmed TEXT NOT NULL,
|
|
98
|
+
times_confirmed INTEGER DEFAULT 1,
|
|
99
|
+
superseded_by TEXT REFERENCES knowledge(id),
|
|
100
|
+
status TEXT DEFAULT 'active'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
pattern_type TEXT NOT NULL,
|
|
106
|
+
domain TEXT,
|
|
107
|
+
description TEXT NOT NULL,
|
|
108
|
+
pattern_data TEXT NOT NULL,
|
|
109
|
+
sample_size INTEGER DEFAULT 0,
|
|
110
|
+
confidence REAL DEFAULT 0.5,
|
|
111
|
+
first_observed TEXT NOT NULL,
|
|
112
|
+
last_observed TEXT NOT NULL,
|
|
113
|
+
status TEXT DEFAULT 'active'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE IF NOT EXISTS code_identity (
|
|
117
|
+
id TEXT PRIMARY KEY,
|
|
118
|
+
aspect TEXT NOT NULL,
|
|
119
|
+
description TEXT NOT NULL,
|
|
120
|
+
evidence TEXT,
|
|
121
|
+
evidence_sources TEXT,
|
|
122
|
+
strength REAL DEFAULT 0.5,
|
|
123
|
+
repos_observed TEXT,
|
|
124
|
+
first_seen TEXT,
|
|
125
|
+
last_seen TEXT
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS people (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
name TEXT NOT NULL,
|
|
131
|
+
aliases TEXT,
|
|
132
|
+
identities TEXT,
|
|
133
|
+
relationship TEXT,
|
|
134
|
+
trust_level REAL DEFAULT 0.5,
|
|
135
|
+
trust_reason TEXT,
|
|
136
|
+
interaction_freq TEXT,
|
|
137
|
+
last_interaction TEXT,
|
|
138
|
+
notes TEXT
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS personas (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
person_id TEXT REFERENCES people(id),
|
|
144
|
+
persona_type TEXT,
|
|
145
|
+
sharing_rules TEXT,
|
|
146
|
+
tone TEXT,
|
|
147
|
+
boundaries TEXT,
|
|
148
|
+
examples TEXT,
|
|
149
|
+
last_updated TEXT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE TABLE IF NOT EXISTS current_state (
|
|
153
|
+
id TEXT PRIMARY KEY,
|
|
154
|
+
state_type TEXT NOT NULL,
|
|
155
|
+
value TEXT NOT NULL,
|
|
156
|
+
priority INTEGER DEFAULT 0,
|
|
157
|
+
started_at TEXT,
|
|
158
|
+
expires_at TEXT,
|
|
159
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
CREATE TABLE IF NOT EXISTS loop_checkpoints (
|
|
163
|
+
loop_name TEXT PRIMARY KEY,
|
|
164
|
+
last_run_at TEXT NOT NULL,
|
|
165
|
+
last_memory_at TEXT,
|
|
166
|
+
metadata TEXT,
|
|
167
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
CREATE TABLE IF NOT EXISTS agent_actions (
|
|
171
|
+
id TEXT PRIMARY KEY,
|
|
172
|
+
action_type TEXT NOT NULL,
|
|
173
|
+
tier INTEGER NOT NULL,
|
|
174
|
+
domain TEXT,
|
|
175
|
+
trigger_memory_id TEXT REFERENCES memories(id),
|
|
176
|
+
description TEXT NOT NULL,
|
|
177
|
+
content TEXT,
|
|
178
|
+
target TEXT,
|
|
179
|
+
confidence REAL NOT NULL,
|
|
180
|
+
status TEXT DEFAULT 'pending',
|
|
181
|
+
review_note TEXT,
|
|
182
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
183
|
+
acted_at TEXT,
|
|
184
|
+
reviewed_at TEXT
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE TABLE IF NOT EXISTS domain_confidence (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
domain TEXT NOT NULL UNIQUE,
|
|
190
|
+
current_tier INTEGER DEFAULT 1,
|
|
191
|
+
confidence REAL DEFAULT 0.3,
|
|
192
|
+
total_actions INTEGER DEFAULT 0,
|
|
193
|
+
approved_actions INTEGER DEFAULT 0,
|
|
194
|
+
rejected_actions INTEGER DEFAULT 0,
|
|
195
|
+
last_promotion TEXT,
|
|
196
|
+
last_demotion TEXT,
|
|
197
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS pending_questions (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
question_type TEXT NOT NULL,
|
|
203
|
+
question TEXT NOT NULL,
|
|
204
|
+
context TEXT,
|
|
205
|
+
options TEXT,
|
|
206
|
+
priority TEXT DEFAULT 'normal',
|
|
207
|
+
status TEXT DEFAULT 'pending',
|
|
208
|
+
answer TEXT,
|
|
209
|
+
resolution_type TEXT,
|
|
210
|
+
resolution_evidence TEXT,
|
|
211
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
212
|
+
answered_at TEXT
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE TABLE IF NOT EXISTS agent_exchanges (
|
|
216
|
+
id TEXT PRIMARY KEY,
|
|
217
|
+
direction TEXT NOT NULL,
|
|
218
|
+
remote_agent_id TEXT NOT NULL,
|
|
219
|
+
person_id TEXT REFERENCES people(id),
|
|
220
|
+
persona_id TEXT REFERENCES personas(id),
|
|
221
|
+
topic TEXT,
|
|
222
|
+
message_in TEXT,
|
|
223
|
+
message_out TEXT,
|
|
224
|
+
shared_knowledge TEXT,
|
|
225
|
+
withheld TEXT,
|
|
226
|
+
confidence REAL,
|
|
227
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
-- Indexes
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source, source_id);
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type, timestamp);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_memories_time ON memories(timestamp);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_memories_extraction ON memories(extraction_status);
|
|
236
|
+
|
|
237
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_subject ON knowledge(subject, predicate);
|
|
238
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge(category);
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_status ON knowledge(status);
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_active_subject ON knowledge(status, subject, predicate);
|
|
241
|
+
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_type_domain ON patterns(pattern_type, domain);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status);
|
|
244
|
+
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_code_identity_aspect ON code_identity(aspect);
|
|
246
|
+
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_people_name ON people(name);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_people_relationship ON people(relationship);
|
|
249
|
+
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_personas_person ON personas(person_id);
|
|
251
|
+
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_state_type ON current_state(state_type);
|
|
253
|
+
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_actions_status ON agent_actions(status, created_at);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_actions_type ON agent_actions(action_type);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_actions_trigger ON agent_actions(trigger_memory_id);
|
|
257
|
+
|
|
258
|
+
CREATE INDEX IF NOT EXISTS idx_questions_status ON pending_questions(status);
|
|
259
|
+
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_exchanges_person ON agent_exchanges(person_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_exchanges_remote ON agent_exchanges(remote_agent_id);
|
|
262
|
+
|
|
263
|
+
CREATE TABLE IF NOT EXISTS daily_summaries (
|
|
264
|
+
id TEXT PRIMARY KEY,
|
|
265
|
+
date TEXT NOT NULL UNIQUE,
|
|
266
|
+
summary TEXT NOT NULL,
|
|
267
|
+
stats TEXT,
|
|
268
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
CREATE INDEX IF NOT EXISTS idx_daily_summaries_date ON daily_summaries(date);
|
|
272
|
+
|
|
273
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
274
|
+
id TEXT PRIMARY KEY,
|
|
275
|
+
role TEXT NOT NULL,
|
|
276
|
+
content TEXT NOT NULL,
|
|
277
|
+
channel TEXT DEFAULT 'ctm',
|
|
278
|
+
session_id TEXT,
|
|
279
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
280
|
+
);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, created_at);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_time ON chat_messages(created_at);
|
|
283
|
+
|
|
284
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
285
|
+
id TEXT PRIMARY KEY,
|
|
286
|
+
name TEXT NOT NULL UNIQUE,
|
|
287
|
+
description TEXT,
|
|
288
|
+
tool_definitions TEXT,
|
|
289
|
+
trigger_type TEXT DEFAULT 'interval',
|
|
290
|
+
trigger_config TEXT,
|
|
291
|
+
prompt_template TEXT,
|
|
292
|
+
enabled INTEGER DEFAULT 1,
|
|
293
|
+
success_count INTEGER DEFAULT 0,
|
|
294
|
+
failure_count INTEGER DEFAULT 0,
|
|
295
|
+
last_run TEXT,
|
|
296
|
+
last_result TEXT,
|
|
297
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
298
|
+
);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_skills_enabled ON skills(enabled);
|
|
300
|
+
|
|
301
|
+
CREATE TABLE IF NOT EXISTS skill_executions (
|
|
302
|
+
id TEXT PRIMARY KEY,
|
|
303
|
+
skill_id TEXT REFERENCES skills(id),
|
|
304
|
+
status TEXT NOT NULL,
|
|
305
|
+
tool_calls TEXT,
|
|
306
|
+
tool_results TEXT,
|
|
307
|
+
memories_created INTEGER DEFAULT 0,
|
|
308
|
+
error TEXT,
|
|
309
|
+
duration_ms INTEGER,
|
|
310
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_skill_exec_skill ON skill_executions(skill_id, created_at);
|
|
313
|
+
|
|
314
|
+
CREATE TABLE IF NOT EXISTS briefing_items (
|
|
315
|
+
id TEXT PRIMARY KEY,
|
|
316
|
+
task_id TEXT,
|
|
317
|
+
skill TEXT,
|
|
318
|
+
title TEXT NOT NULL,
|
|
319
|
+
category TEXT,
|
|
320
|
+
owner TEXT,
|
|
321
|
+
urgency TEXT DEFAULT 'low',
|
|
322
|
+
status TEXT DEFAULT 'open',
|
|
323
|
+
snooze_until TEXT,
|
|
324
|
+
context TEXT,
|
|
325
|
+
first_seen TEXT NOT NULL,
|
|
326
|
+
last_seen TEXT NOT NULL,
|
|
327
|
+
times_seen INTEGER DEFAULT 1,
|
|
328
|
+
resolved_at TEXT,
|
|
329
|
+
notes TEXT,
|
|
330
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
331
|
+
);
|
|
332
|
+
CREATE INDEX IF NOT EXISTS idx_briefing_items_status ON briefing_items(status);
|
|
333
|
+
CREATE INDEX IF NOT EXISTS idx_briefing_items_skill ON briefing_items(skill, status);
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_briefing_items_task ON briefing_items(task_id);
|
|
335
|
+
|
|
336
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
337
|
+
id TEXT PRIMARY KEY,
|
|
338
|
+
title TEXT NOT NULL,
|
|
339
|
+
description TEXT,
|
|
340
|
+
status TEXT DEFAULT 'pending',
|
|
341
|
+
priority TEXT DEFAULT 'normal',
|
|
342
|
+
type TEXT DEFAULT 'once',
|
|
343
|
+
execution TEXT DEFAULT 'chat',
|
|
344
|
+
script TEXT,
|
|
345
|
+
schedule TEXT,
|
|
346
|
+
due_at TEXT,
|
|
347
|
+
next_run_at TEXT,
|
|
348
|
+
started_at TEXT,
|
|
349
|
+
completed_at TEXT,
|
|
350
|
+
last_run_at TEXT,
|
|
351
|
+
run_count INTEGER DEFAULT 0,
|
|
352
|
+
result TEXT,
|
|
353
|
+
error TEXT,
|
|
354
|
+
checkpoint TEXT,
|
|
355
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
356
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
357
|
+
);
|
|
358
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
359
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_at);
|
|
360
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_next ON tasks(next_run_at);
|
|
361
|
+
`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function closeDb() {
|
|
365
|
+
if (backupIntervalId) {
|
|
366
|
+
clearInterval(backupIntervalId);
|
|
367
|
+
backupIntervalId = null;
|
|
368
|
+
}
|
|
369
|
+
if (db) {
|
|
370
|
+
try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch (_) {}
|
|
371
|
+
db.close();
|
|
372
|
+
db = null;
|
|
373
|
+
currentDbPath = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// -- Owner config --
|
|
378
|
+
|
|
379
|
+
function setOwner(key, value) {
|
|
380
|
+
getDb().prepare(
|
|
381
|
+
'INSERT INTO owner (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
|
382
|
+
).run(key, value);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getOwner(key) {
|
|
386
|
+
const row = getDb().prepare('SELECT value FROM owner WHERE key = ?').get(key);
|
|
387
|
+
return row ? row.value : null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getOwnerName() {
|
|
391
|
+
return getOwner('name');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// -- Memory CRUD --
|
|
395
|
+
|
|
396
|
+
function insertMemory(mem) {
|
|
397
|
+
if (!mem.source) throw new Error('Memory requires source');
|
|
398
|
+
if (!mem.memory_type) throw new Error('Memory requires memory_type');
|
|
399
|
+
if (!mem.content) throw new Error('Memory requires content');
|
|
400
|
+
if (!mem.timestamp) throw new Error('Memory requires timestamp');
|
|
401
|
+
|
|
402
|
+
// Dedup: if source+source_id already exists, skip
|
|
403
|
+
if (mem.source_id) {
|
|
404
|
+
const existing = getDb().prepare(
|
|
405
|
+
'SELECT id FROM memories WHERE source = ? AND source_id = ?'
|
|
406
|
+
).get(mem.source, mem.source_id);
|
|
407
|
+
if (existing) return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const id = uuidv4();
|
|
411
|
+
getDb().prepare(`
|
|
412
|
+
INSERT INTO memories (id, source, source_id, source_channel, memory_type, direction, participants, subject, content, content_raw, metadata, importance, timestamp)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
414
|
+
`).run(
|
|
415
|
+
id, mem.source, mem.source_id || null, mem.source_channel || null,
|
|
416
|
+
mem.memory_type, mem.direction || null, mem.participants || null,
|
|
417
|
+
mem.subject || null, mem.content, mem.content_raw || null,
|
|
418
|
+
mem.metadata || null, mem.importance ?? 0.5, mem.timestamp
|
|
419
|
+
);
|
|
420
|
+
return { id };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getMemory(id) {
|
|
424
|
+
return getDb().prepare('SELECT * FROM memories WHERE id = ?').get(id) || null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function listMemories({ source, since, extractionStatus, limit } = {}) {
|
|
428
|
+
const conditions = [];
|
|
429
|
+
const params = [];
|
|
430
|
+
|
|
431
|
+
if (source) {
|
|
432
|
+
conditions.push('source = ?');
|
|
433
|
+
params.push(source);
|
|
434
|
+
}
|
|
435
|
+
if (since) {
|
|
436
|
+
conditions.push('timestamp >= ?');
|
|
437
|
+
params.push(since);
|
|
438
|
+
}
|
|
439
|
+
if (extractionStatus) {
|
|
440
|
+
conditions.push('extraction_status = ?');
|
|
441
|
+
params.push(extractionStatus);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let sql = 'SELECT * FROM memories';
|
|
445
|
+
if (conditions.length > 0) {
|
|
446
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
447
|
+
}
|
|
448
|
+
sql += ' ORDER BY timestamp DESC';
|
|
449
|
+
if (limit) {
|
|
450
|
+
sql += ' LIMIT ?';
|
|
451
|
+
params.push(limit);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return getDb().prepare(sql).all(...params);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function updateMemoryExtraction(id, status) {
|
|
458
|
+
getDb().prepare('UPDATE memories SET extraction_status = ? WHERE id = ?').run(status, id);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// -- Knowledge CRUD --
|
|
462
|
+
|
|
463
|
+
function insertKnowledge(k) {
|
|
464
|
+
if (!k.category) throw new Error('Knowledge requires category');
|
|
465
|
+
if (!k.subject) throw new Error('Knowledge requires subject');
|
|
466
|
+
if (!k.predicate) throw new Error('Knowledge requires predicate');
|
|
467
|
+
if (!k.object) throw new Error('Knowledge requires object');
|
|
468
|
+
|
|
469
|
+
const id = uuidv4();
|
|
470
|
+
const now = new Date().toISOString();
|
|
471
|
+
getDb().prepare(`
|
|
472
|
+
INSERT INTO knowledge (id, category, subject, predicate, object, confidence, source_memory_ids, first_seen, last_confirmed)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
474
|
+
`).run(
|
|
475
|
+
id, k.category, k.subject, k.predicate, k.object,
|
|
476
|
+
k.confidence ?? 0.5, k.source_memory_ids || null, now, now
|
|
477
|
+
);
|
|
478
|
+
return { id };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getKnowledge(id) {
|
|
482
|
+
return getDb().prepare('SELECT * FROM knowledge WHERE id = ?').get(id) || null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function findKnowledge({ subject, predicate, status, category } = {}) {
|
|
486
|
+
const conditions = [];
|
|
487
|
+
const params = [];
|
|
488
|
+
|
|
489
|
+
if (subject) {
|
|
490
|
+
conditions.push('subject = ?');
|
|
491
|
+
params.push(subject);
|
|
492
|
+
}
|
|
493
|
+
if (predicate) {
|
|
494
|
+
conditions.push('predicate = ?');
|
|
495
|
+
params.push(predicate);
|
|
496
|
+
}
|
|
497
|
+
if (status) {
|
|
498
|
+
conditions.push('status = ?');
|
|
499
|
+
params.push(status);
|
|
500
|
+
}
|
|
501
|
+
if (category) {
|
|
502
|
+
conditions.push('category = ?');
|
|
503
|
+
params.push(category);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let sql = 'SELECT * FROM knowledge';
|
|
507
|
+
if (conditions.length > 0) {
|
|
508
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
509
|
+
}
|
|
510
|
+
sql += ' ORDER BY last_confirmed DESC';
|
|
511
|
+
|
|
512
|
+
return getDb().prepare(sql).all(...params);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// -- Checkpoint CRUD --
|
|
516
|
+
|
|
517
|
+
function upsertCheckpoint(loopName, data) {
|
|
518
|
+
const now = new Date().toISOString();
|
|
519
|
+
getDb().prepare(`
|
|
520
|
+
INSERT INTO loop_checkpoints (loop_name, last_run_at, last_memory_at, metadata, updated_at)
|
|
521
|
+
VALUES (?, ?, ?, ?, ?)
|
|
522
|
+
ON CONFLICT(loop_name) DO UPDATE SET
|
|
523
|
+
last_run_at = excluded.last_run_at,
|
|
524
|
+
last_memory_at = excluded.last_memory_at,
|
|
525
|
+
metadata = excluded.metadata,
|
|
526
|
+
updated_at = excluded.updated_at
|
|
527
|
+
`).run(
|
|
528
|
+
loopName, now, data.last_memory_at || null, data.metadata || null, now
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function getCheckpoint(loopName) {
|
|
533
|
+
return getDb().prepare('SELECT * FROM loop_checkpoints WHERE loop_name = ?').get(loopName) || null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// -- Backup --
|
|
537
|
+
|
|
538
|
+
let backupIntervalId = null;
|
|
539
|
+
|
|
540
|
+
function createBackup(label) {
|
|
541
|
+
getDb(); // throws if not initialized
|
|
542
|
+
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
543
|
+
|
|
544
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
|
545
|
+
const backupName = `wall-e-brain-${timestamp}-${label}.db`;
|
|
546
|
+
const backupPath = path.join(BACKUP_DIR, backupName);
|
|
547
|
+
|
|
548
|
+
if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
549
|
+
fs.copyFileSync(currentDbPath, backupPath);
|
|
550
|
+
|
|
551
|
+
cleanOldBackups();
|
|
552
|
+
|
|
553
|
+
return { backupName, backupPath, timestamp };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function cleanOldBackups() {
|
|
557
|
+
if (!fs.existsSync(BACKUP_DIR)) return;
|
|
558
|
+
const files = fs.readdirSync(BACKUP_DIR);
|
|
559
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
560
|
+
|
|
561
|
+
for (const file of files) {
|
|
562
|
+
if (file.startsWith('wall-e-brain-') && file.endsWith('.db')) {
|
|
563
|
+
const filePath = path.join(BACKUP_DIR, file);
|
|
564
|
+
const stat = fs.statSync(filePath);
|
|
565
|
+
if (stat.mtimeMs < thirtyDaysAgo) {
|
|
566
|
+
fs.unlinkSync(filePath);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function startDailyBackup() {
|
|
573
|
+
// Check if backup exists for today
|
|
574
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
575
|
+
let needsBackup = true;
|
|
576
|
+
|
|
577
|
+
if (fs.existsSync(BACKUP_DIR)) {
|
|
578
|
+
const files = fs.readdirSync(BACKUP_DIR);
|
|
579
|
+
const todayPrefix = `wall-e-brain-${today}`;
|
|
580
|
+
needsBackup = !files.some(f => f.startsWith(todayPrefix) && f.endsWith('.db'));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (needsBackup) {
|
|
584
|
+
try { createBackup('daily'); } catch (_) {}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check hourly
|
|
588
|
+
backupIntervalId = setInterval(() => {
|
|
589
|
+
const todayNow = new Date().toISOString().slice(0, 10);
|
|
590
|
+
let exists = false;
|
|
591
|
+
if (fs.existsSync(BACKUP_DIR)) {
|
|
592
|
+
const files = fs.readdirSync(BACKUP_DIR);
|
|
593
|
+
const prefix = `wall-e-brain-${todayNow}`;
|
|
594
|
+
exists = files.some(f => f.startsWith(prefix) && f.endsWith('.db'));
|
|
595
|
+
}
|
|
596
|
+
if (!exists) {
|
|
597
|
+
try { createBackup('daily'); } catch (_) {}
|
|
598
|
+
}
|
|
599
|
+
}, 60 * 60 * 1000);
|
|
600
|
+
|
|
601
|
+
// Don't keep process alive just for backups
|
|
602
|
+
if (backupIntervalId.unref) backupIntervalId.unref();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// -- People CRUD --
|
|
606
|
+
|
|
607
|
+
function insertPerson({ name, relationship, aliases, identities, trust_level, notes }) {
|
|
608
|
+
if (!name) throw new Error('Person requires name');
|
|
609
|
+
const id = uuidv4();
|
|
610
|
+
getDb().prepare(`
|
|
611
|
+
INSERT INTO people (id, name, relationship, aliases, identities, trust_level, notes)
|
|
612
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
613
|
+
`).run(id, name, relationship || null, aliases || null, identities || null, trust_level ?? 0.5, notes || null);
|
|
614
|
+
return { id };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function getPerson(id) {
|
|
618
|
+
return getDb().prepare('SELECT * FROM people WHERE id = ?').get(id) || null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function listPeople({ relationship, limit } = {}) {
|
|
622
|
+
const conditions = [];
|
|
623
|
+
const params = [];
|
|
624
|
+
if (relationship) {
|
|
625
|
+
conditions.push('relationship = ?');
|
|
626
|
+
params.push(relationship);
|
|
627
|
+
}
|
|
628
|
+
let sql = 'SELECT * FROM people';
|
|
629
|
+
if (conditions.length > 0) {
|
|
630
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
631
|
+
}
|
|
632
|
+
sql += ' ORDER BY name';
|
|
633
|
+
if (limit) {
|
|
634
|
+
sql += ' LIMIT ?';
|
|
635
|
+
params.push(limit);
|
|
636
|
+
}
|
|
637
|
+
return getDb().prepare(sql).all(...params);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const PERSON_ALLOWED_FIELDS = ['trust_level', 'trust_reason', 'relationship', 'notes', 'aliases', 'identities', 'interaction_freq', 'last_interaction'];
|
|
641
|
+
|
|
642
|
+
function updatePerson(id, updates) {
|
|
643
|
+
if (!updates || typeof updates !== 'object') throw new Error('Updates must be an object');
|
|
644
|
+
const keys = Object.keys(updates);
|
|
645
|
+
if (keys.length === 0) throw new Error('No updates provided');
|
|
646
|
+
const invalid = keys.filter(k => !PERSON_ALLOWED_FIELDS.includes(k));
|
|
647
|
+
if (invalid.length > 0) throw new Error(`Invalid update fields: ${invalid.join(', ')}`);
|
|
648
|
+
|
|
649
|
+
const sets = keys.map(k => `${k} = ?`);
|
|
650
|
+
const params = keys.map(k => updates[k]);
|
|
651
|
+
params.push(id);
|
|
652
|
+
const result = getDb().prepare(`UPDATE people SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
653
|
+
if (result.changes === 0) throw new Error(`Person not found: ${id}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// -- Pending Questions --
|
|
657
|
+
|
|
658
|
+
function insertQuestion({ question_type, question, context, options, priority }) {
|
|
659
|
+
if (!question_type) throw new Error('Question requires question_type');
|
|
660
|
+
if (!question) throw new Error('Question requires question');
|
|
661
|
+
const id = uuidv4();
|
|
662
|
+
getDb().prepare(`
|
|
663
|
+
INSERT INTO pending_questions (id, question_type, question, context, options, priority)
|
|
664
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
665
|
+
`).run(id, question_type, question, context || null, options || null, priority || 'normal');
|
|
666
|
+
return { id };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function listQuestions({ status, question_type, limit } = {}) {
|
|
670
|
+
const conditions = [];
|
|
671
|
+
const params = [];
|
|
672
|
+
if (status) {
|
|
673
|
+
conditions.push('status = ?');
|
|
674
|
+
params.push(status);
|
|
675
|
+
}
|
|
676
|
+
if (question_type) {
|
|
677
|
+
conditions.push('question_type = ?');
|
|
678
|
+
params.push(question_type);
|
|
679
|
+
}
|
|
680
|
+
let sql = 'SELECT * FROM pending_questions';
|
|
681
|
+
if (conditions.length > 0) {
|
|
682
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
683
|
+
}
|
|
684
|
+
sql += ' ORDER BY created_at DESC';
|
|
685
|
+
if (limit) {
|
|
686
|
+
sql += ' LIMIT ?';
|
|
687
|
+
params.push(limit);
|
|
688
|
+
}
|
|
689
|
+
return getDb().prepare(sql).all(...params);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function answerQuestion(id, { answer, resolution_type, resolution_evidence }) {
|
|
693
|
+
if (!answer) throw new Error('Answer is required');
|
|
694
|
+
if (!resolution_type) throw new Error('Resolution type is required');
|
|
695
|
+
|
|
696
|
+
const statusMap = {
|
|
697
|
+
answered: 'answered',
|
|
698
|
+
inferred: 'inferred',
|
|
699
|
+
dismissed: 'dismissed',
|
|
700
|
+
expired: 'expired',
|
|
701
|
+
};
|
|
702
|
+
const newStatus = statusMap[resolution_type];
|
|
703
|
+
if (!newStatus) throw new Error(`Invalid resolution_type: ${resolution_type}. Must be one of: ${Object.keys(statusMap).join(', ')}`);
|
|
704
|
+
|
|
705
|
+
const now = new Date().toISOString();
|
|
706
|
+
const result = getDb().prepare(`
|
|
707
|
+
UPDATE pending_questions SET answer = ?, resolution_type = ?, resolution_evidence = ?, status = ?, answered_at = ?
|
|
708
|
+
WHERE id = ?
|
|
709
|
+
`).run(answer, resolution_type, resolution_evidence || null, newStatus, now, id);
|
|
710
|
+
if (result.changes === 0) throw new Error(`Question not found: ${id}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// -- Knowledge updates --
|
|
714
|
+
|
|
715
|
+
function supersedeKnowledge(oldId, newId) {
|
|
716
|
+
const result = getDb().prepare(
|
|
717
|
+
"UPDATE knowledge SET superseded_by = ?, status = 'superseded' WHERE id = ?"
|
|
718
|
+
).run(newId, oldId);
|
|
719
|
+
if (result.changes === 0) throw new Error(`Knowledge not found: ${oldId}`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const VALID_KNOWLEDGE_STATUSES = ['active', 'superseded', 'disputed', 'retracted'];
|
|
723
|
+
|
|
724
|
+
function updateKnowledgeStatus(id, status) {
|
|
725
|
+
if (!VALID_KNOWLEDGE_STATUSES.includes(status)) {
|
|
726
|
+
throw new Error(`Invalid knowledge status: ${status}. Must be one of: ${VALID_KNOWLEDGE_STATUSES.join(', ')}`);
|
|
727
|
+
}
|
|
728
|
+
const result = getDb().prepare('UPDATE knowledge SET status = ? WHERE id = ?').run(status, id);
|
|
729
|
+
if (result.changes === 0) throw new Error(`Knowledge not found: ${id}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// -- Stats --
|
|
733
|
+
|
|
734
|
+
function getBrainStats() {
|
|
735
|
+
const d = getDb();
|
|
736
|
+
const memory_count = d.prepare('SELECT count(*) as cnt FROM memories').get().cnt;
|
|
737
|
+
const knowledge_count = d.prepare('SELECT count(*) as cnt FROM knowledge').get().cnt;
|
|
738
|
+
const people_count = d.prepare('SELECT count(*) as cnt FROM people').get().cnt;
|
|
739
|
+
const pattern_count = d.prepare('SELECT count(*) as cnt FROM patterns').get().cnt;
|
|
740
|
+
const pending_question_count = d.prepare("SELECT count(*) as cnt FROM pending_questions WHERE status = 'pending'").get().cnt;
|
|
741
|
+
return { memory_count, knowledge_count, people_count, pattern_count, pending_question_count };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// -- Daily Summaries --
|
|
745
|
+
|
|
746
|
+
function insertDailySummary(date, summary, stats) {
|
|
747
|
+
if (!date) throw new Error('Daily summary requires date');
|
|
748
|
+
if (!summary) throw new Error('Daily summary requires summary');
|
|
749
|
+
const id = uuidv4();
|
|
750
|
+
getDb().prepare(`
|
|
751
|
+
INSERT INTO daily_summaries (id, date, summary, stats)
|
|
752
|
+
VALUES (?, ?, ?, ?)
|
|
753
|
+
`).run(id, date, summary, stats ? JSON.stringify(stats) : null);
|
|
754
|
+
return { id };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function getDailySummary(date) {
|
|
758
|
+
return getDb().prepare('SELECT * FROM daily_summaries WHERE date = ?').get(date) || null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function listDailySummaries(limit) {
|
|
762
|
+
let sql = 'SELECT * FROM daily_summaries ORDER BY date DESC';
|
|
763
|
+
if (limit) {
|
|
764
|
+
sql += ' LIMIT ?';
|
|
765
|
+
return getDb().prepare(sql).all(limit);
|
|
766
|
+
}
|
|
767
|
+
return getDb().prepare(sql).all();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// -- Agent Actions CRUD --
|
|
771
|
+
|
|
772
|
+
function insertAction({ action_type, tier, domain, trigger_memory_id, description, content, target, confidence }) {
|
|
773
|
+
if (!action_type) throw new Error('Action requires action_type');
|
|
774
|
+
if (!description) throw new Error('Action requires description');
|
|
775
|
+
if (confidence == null) throw new Error('Action requires confidence');
|
|
776
|
+
if (tier == null) throw new Error('Action requires tier');
|
|
777
|
+
|
|
778
|
+
const id = uuidv4();
|
|
779
|
+
getDb().prepare(`
|
|
780
|
+
INSERT INTO agent_actions (id, action_type, tier, domain, trigger_memory_id, description, content, target, confidence)
|
|
781
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
782
|
+
`).run(id, action_type, tier, domain || null, trigger_memory_id || null, description, content || null, target || null, confidence);
|
|
783
|
+
return { id };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function getAction(id) {
|
|
787
|
+
return getDb().prepare('SELECT * FROM agent_actions WHERE id = ?').get(id) || null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function listActions({ status, domain, limit } = {}) {
|
|
791
|
+
const conditions = [];
|
|
792
|
+
const params = [];
|
|
793
|
+
if (status) {
|
|
794
|
+
conditions.push('status = ?');
|
|
795
|
+
params.push(status);
|
|
796
|
+
}
|
|
797
|
+
if (domain) {
|
|
798
|
+
conditions.push('domain = ?');
|
|
799
|
+
params.push(domain);
|
|
800
|
+
}
|
|
801
|
+
let sql = 'SELECT * FROM agent_actions';
|
|
802
|
+
if (conditions.length > 0) {
|
|
803
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
804
|
+
}
|
|
805
|
+
sql += ' ORDER BY created_at DESC';
|
|
806
|
+
if (limit) {
|
|
807
|
+
sql += ' LIMIT ?';
|
|
808
|
+
params.push(limit);
|
|
809
|
+
}
|
|
810
|
+
return getDb().prepare(sql).all(...params);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function updateActionStatus(id, status, review_note) {
|
|
814
|
+
const now = new Date().toISOString();
|
|
815
|
+
let sql, params;
|
|
816
|
+
if (status === 'approved') {
|
|
817
|
+
sql = 'UPDATE agent_actions SET status = ?, acted_at = ?, reviewed_at = ? WHERE id = ?';
|
|
818
|
+
params = [status, now, now, id];
|
|
819
|
+
} else if (status === 'rejected') {
|
|
820
|
+
sql = 'UPDATE agent_actions SET status = ?, review_note = ?, reviewed_at = ? WHERE id = ?';
|
|
821
|
+
params = [status, review_note || null, now, id];
|
|
822
|
+
} else {
|
|
823
|
+
sql = 'UPDATE agent_actions SET status = ? WHERE id = ?';
|
|
824
|
+
params = [status, id];
|
|
825
|
+
}
|
|
826
|
+
const result = getDb().prepare(sql).run(...params);
|
|
827
|
+
if (result.changes === 0) throw new Error(`Action not found: ${id}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// -- Persona CRUD --
|
|
831
|
+
|
|
832
|
+
function insertPersona({ person_id, persona_type, sharing_rules, tone, boundaries, examples }) {
|
|
833
|
+
if (!person_id) throw new Error('Persona requires person_id');
|
|
834
|
+
const id = uuidv4();
|
|
835
|
+
const now = new Date().toISOString();
|
|
836
|
+
getDb().prepare(`
|
|
837
|
+
INSERT INTO personas (id, person_id, persona_type, sharing_rules, tone, boundaries, examples, last_updated)
|
|
838
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
839
|
+
`).run(
|
|
840
|
+
id, person_id, persona_type || null,
|
|
841
|
+
typeof sharing_rules === 'string' ? sharing_rules : JSON.stringify(sharing_rules || {}),
|
|
842
|
+
tone || 'professional',
|
|
843
|
+
typeof boundaries === 'string' ? boundaries : JSON.stringify(boundaries || {}),
|
|
844
|
+
typeof examples === 'string' ? examples : JSON.stringify(examples || []),
|
|
845
|
+
now
|
|
846
|
+
);
|
|
847
|
+
return { id };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function getPersonaForPerson(personId) {
|
|
851
|
+
return getDb().prepare('SELECT * FROM personas WHERE person_id = ?').get(personId) || null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function listPersonas() {
|
|
855
|
+
return getDb().prepare(`
|
|
856
|
+
SELECT personas.*, people.name AS person_name, people.relationship AS person_relationship
|
|
857
|
+
FROM personas
|
|
858
|
+
LEFT JOIN people ON personas.person_id = people.id
|
|
859
|
+
ORDER BY people.name
|
|
860
|
+
`).all();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const PERSONA_ALLOWED_FIELDS = ['persona_type', 'sharing_rules', 'tone', 'boundaries', 'examples'];
|
|
864
|
+
|
|
865
|
+
function updatePersona(id, updates) {
|
|
866
|
+
if (!updates || typeof updates !== 'object') throw new Error('Updates must be an object');
|
|
867
|
+
const keys = Object.keys(updates);
|
|
868
|
+
if (keys.length === 0) throw new Error('No updates provided');
|
|
869
|
+
const invalid = keys.filter(k => !PERSONA_ALLOWED_FIELDS.includes(k));
|
|
870
|
+
if (invalid.length > 0) throw new Error(`Invalid update fields: ${invalid.join(', ')}`);
|
|
871
|
+
|
|
872
|
+
// Stringify objects
|
|
873
|
+
const processed = {};
|
|
874
|
+
for (const k of keys) {
|
|
875
|
+
const v = updates[k];
|
|
876
|
+
processed[k] = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : v;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const sets = keys.map(k => `${k} = ?`);
|
|
880
|
+
sets.push('last_updated = ?');
|
|
881
|
+
const params = keys.map(k => processed[k]);
|
|
882
|
+
params.push(new Date().toISOString());
|
|
883
|
+
params.push(id);
|
|
884
|
+
const result = getDb().prepare(`UPDATE personas SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
885
|
+
if (result.changes === 0) throw new Error(`Persona not found: ${id}`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// -- Agent Exchanges query --
|
|
889
|
+
|
|
890
|
+
function listExchanges({ limit } = {}) {
|
|
891
|
+
let sql = 'SELECT * FROM agent_exchanges ORDER BY created_at DESC';
|
|
892
|
+
if (limit) {
|
|
893
|
+
sql += ' LIMIT ?';
|
|
894
|
+
return getDb().prepare(sql).all(limit);
|
|
895
|
+
}
|
|
896
|
+
return getDb().prepare(sql).all();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ── Chat Messages ──
|
|
900
|
+
|
|
901
|
+
function insertChatMessage({ role, content, channel, session_id }) {
|
|
902
|
+
const id = uuidv4();
|
|
903
|
+
getDb().prepare(`
|
|
904
|
+
INSERT INTO chat_messages (id, role, content, channel, session_id, created_at)
|
|
905
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
906
|
+
`).run(id, role, content, channel || 'ctm', session_id || null);
|
|
907
|
+
return { id };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function listChatMessages({ session_id, channel, limit } = {}) {
|
|
911
|
+
const params = [];
|
|
912
|
+
let where = ' WHERE 1=1';
|
|
913
|
+
if (session_id) { where += ' AND session_id = ?'; params.push(session_id); }
|
|
914
|
+
if (channel) { where += ' AND channel = ?'; params.push(channel); }
|
|
915
|
+
|
|
916
|
+
if (limit) {
|
|
917
|
+
// Get the most recent N messages, then return in chronological order
|
|
918
|
+
// Use id as tiebreaker for same-second timestamps (UUIDs sort by insertion order)
|
|
919
|
+
const sql = `SELECT * FROM (SELECT * FROM chat_messages${where} ORDER BY created_at DESC, id DESC LIMIT ?) ORDER BY created_at ASC, id ASC`;
|
|
920
|
+
params.push(limit);
|
|
921
|
+
return getDb().prepare(sql).all(...params);
|
|
922
|
+
}
|
|
923
|
+
return getDb().prepare(`SELECT * FROM chat_messages${where} ORDER BY created_at ASC, id ASC`).all(...params);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function searchChatMessages({ query, limit } = {}) {
|
|
927
|
+
if (!query || !query.trim()) return [];
|
|
928
|
+
const terms = query.trim().split(/\s+/).filter(Boolean);
|
|
929
|
+
const conditions = terms.map(() => 'content LIKE ?');
|
|
930
|
+
const params = terms.map(t => '%' + t + '%');
|
|
931
|
+
const lim = Math.min(Math.max(limit || 50, 1), 200);
|
|
932
|
+
params.push(lim);
|
|
933
|
+
return getDb().prepare(
|
|
934
|
+
`SELECT * FROM chat_messages WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
935
|
+
).all(...params);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function deleteChatMessages({ session_id, user_content, assistant_content }) {
|
|
939
|
+
const db = getDb();
|
|
940
|
+
if (user_content) {
|
|
941
|
+
db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
|
|
942
|
+
.run(session_id || 'default', 'user', user_content);
|
|
943
|
+
}
|
|
944
|
+
if (assistant_content) {
|
|
945
|
+
db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
|
|
946
|
+
.run(session_id || 'default', 'assistant', assistant_content);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function clearChatSession(session_id) {
|
|
951
|
+
getDb().prepare('DELETE FROM chat_messages WHERE session_id = ?').run(session_id);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// -- Skills CRUD --
|
|
955
|
+
|
|
956
|
+
function insertSkill({ name, description, tool_definitions, trigger_type, trigger_config, prompt_template }) {
|
|
957
|
+
if (!name) throw new Error('Skill requires name');
|
|
958
|
+
const id = uuidv4();
|
|
959
|
+
getDb().prepare(`
|
|
960
|
+
INSERT INTO skills (id, name, description, tool_definitions, trigger_type, trigger_config, prompt_template)
|
|
961
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
962
|
+
`).run(
|
|
963
|
+
id, name, description || null,
|
|
964
|
+
tool_definitions || null,
|
|
965
|
+
trigger_type || 'interval',
|
|
966
|
+
trigger_config || null,
|
|
967
|
+
prompt_template || null
|
|
968
|
+
);
|
|
969
|
+
return { id };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function getSkill(id) {
|
|
973
|
+
return getDb().prepare('SELECT * FROM skills WHERE id = ?').get(id) || null;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function getSkillByName(name) {
|
|
977
|
+
return getDb().prepare('SELECT * FROM skills WHERE name = ?').get(name) || null;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function listSkills({ enabled } = {}) {
|
|
981
|
+
if (enabled !== undefined && enabled !== null) {
|
|
982
|
+
return getDb().prepare('SELECT * FROM skills WHERE enabled = ? ORDER BY name').all(enabled);
|
|
983
|
+
}
|
|
984
|
+
return getDb().prepare('SELECT * FROM skills ORDER BY name').all();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const SKILL_ALLOWED_FIELDS = ['name', 'description', 'tool_definitions', 'trigger_type', 'trigger_config', 'prompt_template', 'enabled'];
|
|
988
|
+
|
|
989
|
+
function updateSkill(id, updates) {
|
|
990
|
+
if (!updates || typeof updates !== 'object') throw new Error('Updates must be an object');
|
|
991
|
+
const keys = Object.keys(updates);
|
|
992
|
+
if (keys.length === 0) throw new Error('No updates provided');
|
|
993
|
+
const invalid = keys.filter(k => !SKILL_ALLOWED_FIELDS.includes(k));
|
|
994
|
+
if (invalid.length > 0) throw new Error(`Invalid update fields: ${invalid.join(', ')}`);
|
|
995
|
+
|
|
996
|
+
const sets = keys.map(k => `${k} = ?`);
|
|
997
|
+
const params = keys.map(k => updates[k]);
|
|
998
|
+
params.push(id);
|
|
999
|
+
const result = getDb().prepare(`UPDATE skills SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
1000
|
+
if (result.changes === 0) throw new Error(`Skill not found: ${id}`);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function insertSkillExecution({ skill_id, status, tool_calls, tool_results, memories_created, error, duration_ms }) {
|
|
1004
|
+
if (!skill_id) throw new Error('Skill execution requires skill_id');
|
|
1005
|
+
if (!status) throw new Error('Skill execution requires status');
|
|
1006
|
+
const id = uuidv4();
|
|
1007
|
+
getDb().prepare(`
|
|
1008
|
+
INSERT INTO skill_executions (id, skill_id, status, tool_calls, tool_results, memories_created, error, duration_ms)
|
|
1009
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1010
|
+
`).run(id, skill_id, status, tool_calls || null, tool_results || null, memories_created || 0, error || null, duration_ms || null);
|
|
1011
|
+
return { id };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function listSkillExecutions({ skill_id, limit } = {}) {
|
|
1015
|
+
const conditions = [];
|
|
1016
|
+
const params = [];
|
|
1017
|
+
if (skill_id) {
|
|
1018
|
+
conditions.push('skill_id = ?');
|
|
1019
|
+
params.push(skill_id);
|
|
1020
|
+
}
|
|
1021
|
+
let sql = 'SELECT * FROM skill_executions';
|
|
1022
|
+
if (conditions.length > 0) {
|
|
1023
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
1024
|
+
}
|
|
1025
|
+
sql += ' ORDER BY created_at DESC';
|
|
1026
|
+
if (limit) {
|
|
1027
|
+
sql += ' LIMIT ?';
|
|
1028
|
+
params.push(limit);
|
|
1029
|
+
}
|
|
1030
|
+
return getDb().prepare(sql).all(...params);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function updateSkillStats(skillId, success) {
|
|
1034
|
+
const now = new Date().toISOString();
|
|
1035
|
+
const field = success ? 'success_count' : 'failure_count';
|
|
1036
|
+
const lastResult = success ? 'success' : 'failure';
|
|
1037
|
+
const result = getDb().prepare(
|
|
1038
|
+
`UPDATE skills SET ${field} = ${field} + 1, last_run = ?, last_result = ? WHERE id = ?`
|
|
1039
|
+
).run(now, lastResult, skillId);
|
|
1040
|
+
if (result.changes === 0) throw new Error(`Skill not found: ${skillId}`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ── Tasks ──
|
|
1044
|
+
|
|
1045
|
+
function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config }) {
|
|
1046
|
+
const id = uuidv4();
|
|
1047
|
+
getDb().prepare(`
|
|
1048
|
+
INSERT INTO tasks (id, title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config)
|
|
1049
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1050
|
+
`).run(id, title, description || null, priority || 'normal', type || 'once',
|
|
1051
|
+
execution || 'chat', script || null, schedule || null, due_at || null, next_run_at || due_at || null,
|
|
1052
|
+
skill || null, skill_config || null);
|
|
1053
|
+
return { id };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function getTask(id) {
|
|
1057
|
+
return getDb().prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function listTasks({ status, limit } = {}) {
|
|
1061
|
+
const params = [];
|
|
1062
|
+
let sql = 'SELECT * FROM tasks';
|
|
1063
|
+
if (status) { sql += ' WHERE status = ?'; params.push(status); }
|
|
1064
|
+
sql += ' ORDER BY CASE priority WHEN \'urgent\' THEN 0 WHEN \'high\' THEN 1 WHEN \'normal\' THEN 2 WHEN \'low\' THEN 3 END, created_at DESC';
|
|
1065
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
1066
|
+
return getDb().prepare(sql).all(...params);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function updateTask(id, updates) {
|
|
1070
|
+
const allowed = ['title', 'description', 'status', 'priority', 'type', 'execution', 'script', 'schedule', 'due_at', 'next_run_at', 'started_at', 'completed_at', 'last_run_at', 'run_count', 'result', 'error', 'checkpoint', 'skill', 'skill_config'];
|
|
1071
|
+
const sets = [];
|
|
1072
|
+
const params = [];
|
|
1073
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1074
|
+
if (allowed.includes(k)) { sets.push(`${k} = ?`); params.push(v); }
|
|
1075
|
+
}
|
|
1076
|
+
if (sets.length === 0) return;
|
|
1077
|
+
sets.push('updated_at = datetime(\'now\')');
|
|
1078
|
+
params.push(id);
|
|
1079
|
+
getDb().prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function deleteTask(id) {
|
|
1083
|
+
getDb().prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function getDueTasks() {
|
|
1087
|
+
const now = new Date().toISOString();
|
|
1088
|
+
return getDb().prepare(`
|
|
1089
|
+
SELECT * FROM tasks WHERE status = 'pending' AND (next_run_at IS NULL OR next_run_at <= ?) ORDER BY next_run_at ASC
|
|
1090
|
+
`).all(now);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ── Briefing Items ──
|
|
1094
|
+
|
|
1095
|
+
function insertBriefingItem({ task_id, skill, title, category, owner, urgency, status, context, first_seen, last_seen }) {
|
|
1096
|
+
if (!title) throw new Error('Briefing item requires title');
|
|
1097
|
+
const id = uuidv4();
|
|
1098
|
+
const now = new Date().toISOString();
|
|
1099
|
+
getDb().prepare(`
|
|
1100
|
+
INSERT INTO briefing_items (id, task_id, skill, title, category, owner, urgency, status, context, first_seen, last_seen)
|
|
1101
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1102
|
+
`).run(id, task_id || null, skill || null, title, category || null, owner || null,
|
|
1103
|
+
urgency || 'low', status || 'open', context || null, first_seen || now, last_seen || now);
|
|
1104
|
+
return { id };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function listBriefingItems({ skill, status, limit } = {}) {
|
|
1108
|
+
const conditions = [];
|
|
1109
|
+
const params = [];
|
|
1110
|
+
if (skill) { conditions.push('skill = ?'); params.push(skill); }
|
|
1111
|
+
if (status) { conditions.push('status = ?'); params.push(status); }
|
|
1112
|
+
// Un-snooze items whose snooze has expired
|
|
1113
|
+
const now = new Date().toISOString();
|
|
1114
|
+
let sql = 'SELECT * FROM briefing_items';
|
|
1115
|
+
if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND ');
|
|
1116
|
+
sql += ' ORDER BY CASE urgency WHEN \'critical\' THEN 0 WHEN \'today\' THEN 1 WHEN \'this_week\' THEN 2 WHEN \'async\' THEN 3 ELSE 4 END, last_seen DESC';
|
|
1117
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
1118
|
+
return getDb().prepare(sql).all(...params);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function getBriefingItem(id) {
|
|
1122
|
+
return getDb().prepare('SELECT * FROM briefing_items WHERE id = ?').get(id) || null;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function updateBriefingItem(id, updates) {
|
|
1126
|
+
const allowed = ['status', 'urgency', 'owner', 'category', 'snooze_until', 'notes', 'resolved_at', 'last_seen', 'times_seen', 'context'];
|
|
1127
|
+
const sets = [];
|
|
1128
|
+
const params = [];
|
|
1129
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1130
|
+
if (allowed.includes(k)) { sets.push(`${k} = ?`); params.push(v); }
|
|
1131
|
+
}
|
|
1132
|
+
if (sets.length === 0) return;
|
|
1133
|
+
params.push(id);
|
|
1134
|
+
const result = getDb().prepare(`UPDATE briefing_items SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
1135
|
+
if (result.changes === 0) throw new Error(`Briefing item not found: ${id}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function findBriefingItemByTitle(skill, title) {
|
|
1139
|
+
return getDb().prepare(
|
|
1140
|
+
'SELECT * FROM briefing_items WHERE skill = ? AND title = ? AND status NOT IN (\'done\', \'dismissed\')'
|
|
1141
|
+
).get(skill, title) || null;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function listOpenBriefingItems(skill) {
|
|
1145
|
+
return getDb().prepare(
|
|
1146
|
+
'SELECT * FROM briefing_items WHERE skill = ? AND status NOT IN (\'done\', \'dismissed\') ORDER BY last_seen DESC'
|
|
1147
|
+
).all(skill);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
module.exports = {
|
|
1151
|
+
initDb,
|
|
1152
|
+
getDb,
|
|
1153
|
+
closeDb,
|
|
1154
|
+
getDbPath,
|
|
1155
|
+
DATA_DIR,
|
|
1156
|
+
BACKUP_DIR,
|
|
1157
|
+
// Owner
|
|
1158
|
+
setOwner,
|
|
1159
|
+
getOwner,
|
|
1160
|
+
getOwnerName,
|
|
1161
|
+
// Memory
|
|
1162
|
+
insertMemory,
|
|
1163
|
+
getMemory,
|
|
1164
|
+
listMemories,
|
|
1165
|
+
updateMemoryExtraction,
|
|
1166
|
+
// Knowledge
|
|
1167
|
+
insertKnowledge,
|
|
1168
|
+
getKnowledge,
|
|
1169
|
+
findKnowledge,
|
|
1170
|
+
// Checkpoints
|
|
1171
|
+
upsertCheckpoint,
|
|
1172
|
+
getCheckpoint,
|
|
1173
|
+
// People
|
|
1174
|
+
insertPerson,
|
|
1175
|
+
getPerson,
|
|
1176
|
+
listPeople,
|
|
1177
|
+
updatePerson,
|
|
1178
|
+
// Personas
|
|
1179
|
+
insertPersona,
|
|
1180
|
+
getPersonaForPerson,
|
|
1181
|
+
listPersonas,
|
|
1182
|
+
updatePersona,
|
|
1183
|
+
// Questions
|
|
1184
|
+
insertQuestion,
|
|
1185
|
+
listQuestions,
|
|
1186
|
+
answerQuestion,
|
|
1187
|
+
// Knowledge updates
|
|
1188
|
+
supersedeKnowledge,
|
|
1189
|
+
updateKnowledgeStatus,
|
|
1190
|
+
// Stats
|
|
1191
|
+
getBrainStats,
|
|
1192
|
+
// Daily Summaries
|
|
1193
|
+
insertDailySummary,
|
|
1194
|
+
getDailySummary,
|
|
1195
|
+
listDailySummaries,
|
|
1196
|
+
// Agent Actions
|
|
1197
|
+
insertAction,
|
|
1198
|
+
getAction,
|
|
1199
|
+
listActions,
|
|
1200
|
+
updateActionStatus,
|
|
1201
|
+
// Agent Exchanges
|
|
1202
|
+
listExchanges,
|
|
1203
|
+
// Chat Messages
|
|
1204
|
+
insertChatMessage,
|
|
1205
|
+
listChatMessages,
|
|
1206
|
+
searchChatMessages,
|
|
1207
|
+
deleteChatMessages,
|
|
1208
|
+
clearChatSession,
|
|
1209
|
+
// Backup
|
|
1210
|
+
createBackup,
|
|
1211
|
+
startDailyBackup,
|
|
1212
|
+
// Skills
|
|
1213
|
+
insertSkill,
|
|
1214
|
+
getSkill,
|
|
1215
|
+
getSkillByName,
|
|
1216
|
+
listSkills,
|
|
1217
|
+
updateSkill,
|
|
1218
|
+
insertSkillExecution,
|
|
1219
|
+
listSkillExecutions,
|
|
1220
|
+
updateSkillStats,
|
|
1221
|
+
// Tasks
|
|
1222
|
+
insertTask,
|
|
1223
|
+
getTask,
|
|
1224
|
+
listTasks,
|
|
1225
|
+
updateTask,
|
|
1226
|
+
deleteTask,
|
|
1227
|
+
getDueTasks,
|
|
1228
|
+
// Briefing Items
|
|
1229
|
+
insertBriefingItem,
|
|
1230
|
+
getBriefingItem,
|
|
1231
|
+
listBriefingItems,
|
|
1232
|
+
updateBriefingItem,
|
|
1233
|
+
findBriefingItemByTitle,
|
|
1234
|
+
listOpenBriefingItems,
|
|
1235
|
+
};
|