verybot 0.1.3
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.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
const DEFAULT_LIMIT = 10;
|
|
6
|
+
export class MemoryStore {
|
|
7
|
+
db;
|
|
8
|
+
vectorEnabled = false;
|
|
9
|
+
vecTableCreated = false;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
/** Async factory — loads sqlite-vec extension if available. */
|
|
14
|
+
static async create(dbPath) {
|
|
15
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
16
|
+
const db = new Database(dbPath);
|
|
17
|
+
db.pragma("journal_mode = WAL");
|
|
18
|
+
const store = new MemoryStore(db);
|
|
19
|
+
store.createSchema();
|
|
20
|
+
await store.loadVecExtension();
|
|
21
|
+
return store;
|
|
22
|
+
}
|
|
23
|
+
get hasVectorSearch() {
|
|
24
|
+
return this.vectorEnabled;
|
|
25
|
+
}
|
|
26
|
+
// --- Schema ---
|
|
27
|
+
createSchema() {
|
|
28
|
+
this.db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
fact TEXT NOT NULL,
|
|
32
|
+
source TEXT NOT NULL,
|
|
33
|
+
timestamp INTEGER NOT NULL,
|
|
34
|
+
embedding BLOB
|
|
35
|
+
);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp DESC);
|
|
38
|
+
`);
|
|
39
|
+
// FTS5 external content table (synced manually in save/delete, no triggers)
|
|
40
|
+
// Porter tokenizer enables stemming: "like" matches "likes", "running" matches "run"
|
|
41
|
+
this.db.exec(`
|
|
42
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
43
|
+
fact,
|
|
44
|
+
content='memories',
|
|
45
|
+
content_rowid='rowid',
|
|
46
|
+
tokenize='porter unicode61'
|
|
47
|
+
);
|
|
48
|
+
`);
|
|
49
|
+
// Idempotent migration: add team_id column for team-scoped memories
|
|
50
|
+
this.migrateTeamId();
|
|
51
|
+
}
|
|
52
|
+
/** Add team_id column if it doesn't exist yet (idempotent). */
|
|
53
|
+
migrateTeamId() {
|
|
54
|
+
const columns = this.db.pragma("table_info(memories)");
|
|
55
|
+
const hasTeamId = columns.some((c) => c.name === "team_id");
|
|
56
|
+
if (!hasTeamId) {
|
|
57
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN team_id TEXT");
|
|
58
|
+
logger.info("Migrated memories table: added team_id column");
|
|
59
|
+
}
|
|
60
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_memories_team_id ON memories(team_id)");
|
|
61
|
+
}
|
|
62
|
+
async loadVecExtension() {
|
|
63
|
+
try {
|
|
64
|
+
const sqliteVec = await import("sqlite-vec");
|
|
65
|
+
// Handle both ESM default export and CJS module patterns
|
|
66
|
+
const loader = sqliteVec.default?.load ?? sqliteVec.load;
|
|
67
|
+
loader(this.db);
|
|
68
|
+
this.vectorEnabled = true;
|
|
69
|
+
// Check if vec table already exists from a previous run
|
|
70
|
+
const vecExists = this.db
|
|
71
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_vec'")
|
|
72
|
+
.get();
|
|
73
|
+
if (vecExists)
|
|
74
|
+
this.vecTableCreated = true;
|
|
75
|
+
logger.info(`sqlite-vec extension loaded (vec table: ${this.vecTableCreated ? "exists" : "pending"})`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
logger.info("sqlite-vec not available — vector search disabled");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ensureVecTable(dims) {
|
|
82
|
+
if (this.vecTableCreated || !this.vectorEnabled)
|
|
83
|
+
return;
|
|
84
|
+
this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(embedding float[${dims}])`);
|
|
85
|
+
this.vecTableCreated = true;
|
|
86
|
+
logger.info(`Vector table created (${dims} dimensions)`);
|
|
87
|
+
}
|
|
88
|
+
// --- CRUD ---
|
|
89
|
+
/** Save a memory entry. Returns false if duplicate or near-duplicate fact exists. */
|
|
90
|
+
save(entry) {
|
|
91
|
+
// Normalize: trim whitespace and trailing punctuation
|
|
92
|
+
entry = { ...entry, fact: normalizeFact(entry.fact) };
|
|
93
|
+
// Skip exact duplicate (scoped to same team)
|
|
94
|
+
const teamFilter = entry.teamId
|
|
95
|
+
? "AND team_id = ?"
|
|
96
|
+
: "AND team_id IS NULL";
|
|
97
|
+
const exactParams = entry.teamId
|
|
98
|
+
? [entry.fact, entry.teamId]
|
|
99
|
+
: [entry.fact];
|
|
100
|
+
const exact = this.db
|
|
101
|
+
.prepare(`SELECT id FROM memories WHERE fact = ? ${teamFilter}`)
|
|
102
|
+
.get(...exactParams);
|
|
103
|
+
if (exact)
|
|
104
|
+
return false;
|
|
105
|
+
// Skip near-duplicate via FTS5 (e.g. "lives in US" vs "lives in United States")
|
|
106
|
+
// Scope dedup to same team
|
|
107
|
+
const ftsQuery = buildFtsQuery(entry.fact);
|
|
108
|
+
if (ftsQuery) {
|
|
109
|
+
const dedupTeamFilter = entry.teamId
|
|
110
|
+
? "AND m.team_id = ?"
|
|
111
|
+
: "AND m.team_id IS NULL";
|
|
112
|
+
const dedupParams = entry.teamId
|
|
113
|
+
? [ftsQuery, entry.source, entry.teamId]
|
|
114
|
+
: [ftsQuery, entry.source];
|
|
115
|
+
const similar = this.db
|
|
116
|
+
.prepare(`SELECT m.fact FROM memories_fts f
|
|
117
|
+
JOIN memories m ON m.rowid = f.rowid
|
|
118
|
+
WHERE memories_fts MATCH ? AND m.source = ? ${dedupTeamFilter}
|
|
119
|
+
LIMIT 3`)
|
|
120
|
+
.all(...dedupParams);
|
|
121
|
+
for (const row of similar) {
|
|
122
|
+
if (isSimilarFact(row.fact, entry.fact))
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const embeddingBlob = entry.embedding
|
|
127
|
+
? Buffer.from(new Float32Array(entry.embedding).buffer)
|
|
128
|
+
: null;
|
|
129
|
+
const info = this.db
|
|
130
|
+
.prepare(`INSERT INTO memories (id, fact, source, timestamp, embedding, team_id)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
132
|
+
.run(entry.id, entry.fact, entry.source, entry.timestamp, embeddingBlob, entry.teamId ?? null);
|
|
133
|
+
// Keep as BigInt — better-sqlite3 binds BigInt as INTEGER, but Number as REAL.
|
|
134
|
+
// sqlite-vec rejects REAL primary keys on virtual tables.
|
|
135
|
+
const rowid = info.lastInsertRowid;
|
|
136
|
+
// Sync FTS5 index
|
|
137
|
+
this.db
|
|
138
|
+
.prepare("INSERT INTO memories_fts(rowid, fact) VALUES (?, ?)")
|
|
139
|
+
.run(rowid, entry.fact);
|
|
140
|
+
// Insert into vector table if we have an embedding
|
|
141
|
+
if (this.vectorEnabled && entry.embedding) {
|
|
142
|
+
this.ensureVecTable(entry.embedding.length);
|
|
143
|
+
this.db
|
|
144
|
+
.prepare("INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)")
|
|
145
|
+
.run(rowid, embeddingBlob);
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
/** Full-text search using FTS5/BM25. Optionally scoped to a source and/or team. */
|
|
150
|
+
searchByText(query, limit = DEFAULT_LIMIT, source, teamId) {
|
|
151
|
+
const ftsQuery = buildFtsQuery(query);
|
|
152
|
+
if (!ftsQuery)
|
|
153
|
+
return [];
|
|
154
|
+
const conditions = ["memories_fts MATCH ?"];
|
|
155
|
+
const params = [ftsQuery];
|
|
156
|
+
if (source) {
|
|
157
|
+
conditions.push("m.source = ?");
|
|
158
|
+
params.push(source);
|
|
159
|
+
}
|
|
160
|
+
// Team filter: return only team-specific memories
|
|
161
|
+
if (teamId) {
|
|
162
|
+
conditions.push("m.team_id = ?");
|
|
163
|
+
params.push(teamId);
|
|
164
|
+
}
|
|
165
|
+
params.push(limit);
|
|
166
|
+
const rows = this.db
|
|
167
|
+
.prepare(`SELECT m.id, m.fact, m.source, m.timestamp, m.team_id, rank
|
|
168
|
+
FROM memories_fts f
|
|
169
|
+
JOIN memories m ON m.rowid = f.rowid
|
|
170
|
+
WHERE ${conditions.join(" AND ")}
|
|
171
|
+
ORDER BY rank
|
|
172
|
+
LIMIT ?`)
|
|
173
|
+
.all(...params);
|
|
174
|
+
return rows.map((r) => ({
|
|
175
|
+
id: r.id, fact: r.fact, source: r.source, timestamp: r.timestamp,
|
|
176
|
+
...(r.team_id ? { teamId: r.team_id } : {}),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
/** Vector similarity search using sqlite-vec. Optionally scoped to a source and/or team. */
|
|
180
|
+
searchByVector(embedding, limit = DEFAULT_LIMIT, source, teamId) {
|
|
181
|
+
if (!this.vectorEnabled || !this.vecTableCreated)
|
|
182
|
+
return [];
|
|
183
|
+
const buf = Buffer.from(new Float32Array(embedding).buffer);
|
|
184
|
+
// Fetch more than needed so we can filter by source/team and still fill the limit
|
|
185
|
+
const needsPostFilter = !!source || !!teamId;
|
|
186
|
+
const fetchLimit = needsPostFilter ? limit * 3 : limit;
|
|
187
|
+
const vecRows = this.db
|
|
188
|
+
.prepare("SELECT rowid, distance FROM memories_vec WHERE embedding MATCH ? ORDER BY distance LIMIT ?")
|
|
189
|
+
.all(buf, fetchLimit);
|
|
190
|
+
if (vecRows.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
const rowids = vecRows.map((r) => r.rowid);
|
|
193
|
+
const placeholders = rowids.map(() => "?").join(",");
|
|
194
|
+
const conditions = [`rowid IN (${placeholders})`];
|
|
195
|
+
const params = [...rowids];
|
|
196
|
+
if (source) {
|
|
197
|
+
conditions.push("source = ?");
|
|
198
|
+
params.push(source);
|
|
199
|
+
}
|
|
200
|
+
if (teamId) {
|
|
201
|
+
conditions.push("team_id = ?");
|
|
202
|
+
params.push(teamId);
|
|
203
|
+
}
|
|
204
|
+
params.push(limit);
|
|
205
|
+
const memRows = this.db
|
|
206
|
+
.prepare(`SELECT id, fact, source, timestamp, team_id FROM memories
|
|
207
|
+
WHERE ${conditions.join(" AND ")}
|
|
208
|
+
LIMIT ?`)
|
|
209
|
+
.all(...params);
|
|
210
|
+
return memRows.map((r) => ({
|
|
211
|
+
id: r.id, fact: r.fact, source: r.source, timestamp: r.timestamp,
|
|
212
|
+
...(r.team_id ? { teamId: r.team_id } : {}),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
/** Delete a single memory by its ID. Optionally verify team ownership. Returns true if found and deleted. */
|
|
216
|
+
deleteById(id, teamId) {
|
|
217
|
+
// Verify ownership when teamId is provided
|
|
218
|
+
const query = teamId
|
|
219
|
+
? "SELECT rowid, fact FROM memories WHERE id = ? AND team_id = ?"
|
|
220
|
+
: "SELECT rowid, fact FROM memories WHERE id = ?";
|
|
221
|
+
const params = teamId ? [id, teamId] : [id];
|
|
222
|
+
const row = this.db
|
|
223
|
+
.prepare(query)
|
|
224
|
+
.get(...params);
|
|
225
|
+
if (!row)
|
|
226
|
+
return false;
|
|
227
|
+
const doDelete = this.db.transaction(() => {
|
|
228
|
+
// Remove from FTS5 index
|
|
229
|
+
this.db
|
|
230
|
+
.prepare("INSERT INTO memories_fts(memories_fts, rowid, fact) VALUES('delete', ?, ?)")
|
|
231
|
+
.run(row.rowid, row.fact);
|
|
232
|
+
// Remove from vector table
|
|
233
|
+
if (this.vectorEnabled && this.vecTableCreated) {
|
|
234
|
+
this.db.prepare("DELETE FROM memories_vec WHERE rowid = ?").run(row.rowid);
|
|
235
|
+
}
|
|
236
|
+
// Remove from main table
|
|
237
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
238
|
+
});
|
|
239
|
+
doDelete();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Paginated list of memories for a specific team (or global if teamId is null).
|
|
244
|
+
* Intended for CRUD management (listing/deleting).
|
|
245
|
+
*/
|
|
246
|
+
listByTeam(teamId, limit = DEFAULT_LIMIT, offset = 0) {
|
|
247
|
+
const filter = teamId ? "WHERE team_id = ?" : "WHERE team_id IS NULL";
|
|
248
|
+
const params = teamId ? [teamId] : [];
|
|
249
|
+
const countRow = this.db
|
|
250
|
+
.prepare(`SELECT COUNT(*) AS cnt FROM memories ${filter}`)
|
|
251
|
+
.get(...params);
|
|
252
|
+
const rows = this.db
|
|
253
|
+
.prepare(`SELECT id, fact, source, timestamp, team_id FROM memories
|
|
254
|
+
${filter}
|
|
255
|
+
ORDER BY timestamp DESC
|
|
256
|
+
LIMIT ? OFFSET ?`)
|
|
257
|
+
.all(...params, limit, offset);
|
|
258
|
+
return {
|
|
259
|
+
total: countRow.cnt,
|
|
260
|
+
entries: rows.map((r) => ({
|
|
261
|
+
id: r.id, fact: r.fact, source: r.source, timestamp: r.timestamp,
|
|
262
|
+
...(r.team_id ? { teamId: r.team_id } : {}),
|
|
263
|
+
})),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Delete all memories from a given session source. Returns count deleted. */
|
|
267
|
+
deleteBySource(source) {
|
|
268
|
+
const rows = this.db
|
|
269
|
+
.prepare("SELECT rowid, fact FROM memories WHERE source = ?")
|
|
270
|
+
.all(source);
|
|
271
|
+
if (rows.length === 0)
|
|
272
|
+
return 0;
|
|
273
|
+
const rowids = rows.map((r) => r.rowid);
|
|
274
|
+
const placeholders = rowids.map(() => "?").join(",");
|
|
275
|
+
// Remove from FTS5 index
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
this.db
|
|
278
|
+
.prepare("INSERT INTO memories_fts(memories_fts, rowid, fact) VALUES('delete', ?, ?)")
|
|
279
|
+
.run(row.rowid, row.fact);
|
|
280
|
+
}
|
|
281
|
+
// Remove from vector table
|
|
282
|
+
if (this.vectorEnabled && this.vecTableCreated) {
|
|
283
|
+
this.db
|
|
284
|
+
.prepare(`DELETE FROM memories_vec WHERE rowid IN (${placeholders})`)
|
|
285
|
+
.run(...rowids);
|
|
286
|
+
}
|
|
287
|
+
// Remove from main table
|
|
288
|
+
const info = this.db
|
|
289
|
+
.prepare(`DELETE FROM memories WHERE rowid IN (${placeholders})`)
|
|
290
|
+
.run(...rowids);
|
|
291
|
+
return info.changes;
|
|
292
|
+
}
|
|
293
|
+
close() {
|
|
294
|
+
this.db.close();
|
|
295
|
+
logger.info("Memory store closed");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Build an FTS5 query from raw text. Returns null if no usable tokens. */
|
|
299
|
+
function buildFtsQuery(raw) {
|
|
300
|
+
const tokens = raw
|
|
301
|
+
.match(/[A-Za-z0-9_]+/g)
|
|
302
|
+
?.map((t) => t.trim())
|
|
303
|
+
.filter(Boolean) ?? [];
|
|
304
|
+
if (tokens.length === 0)
|
|
305
|
+
return null;
|
|
306
|
+
// Use OR for broader matching (AND would be too strict for memory recall)
|
|
307
|
+
return tokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
308
|
+
}
|
|
309
|
+
/** Trim whitespace and trailing punctuation from a fact. */
|
|
310
|
+
function normalizeFact(fact) {
|
|
311
|
+
return fact.trim().replace(/[.\s]+$/, "");
|
|
312
|
+
}
|
|
313
|
+
/** Check if two facts are near-duplicates by comparing their word overlap. */
|
|
314
|
+
function isSimilarFact(a, b) {
|
|
315
|
+
const wordsA = new Set(a.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
316
|
+
const wordsB = new Set(b.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
317
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
318
|
+
return false;
|
|
319
|
+
let overlap = 0;
|
|
320
|
+
for (const w of wordsA) {
|
|
321
|
+
if (wordsB.has(w))
|
|
322
|
+
overlap++;
|
|
323
|
+
}
|
|
324
|
+
// If 80%+ of words overlap in both directions, it's a near-duplicate
|
|
325
|
+
const ratioA = overlap / wordsA.size;
|
|
326
|
+
const ratioB = overlap / wordsB.size;
|
|
327
|
+
return ratioA >= 0.8 && ratioB >= 0.8;
|
|
328
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Root data directory: ~/.verybot */
|
|
2
|
+
export declare const BASE_DIR: string;
|
|
3
|
+
/** Conversation history (JSONL files): ~/.verybot/sessions/ */
|
|
4
|
+
export declare const SESSIONS_DIR: string;
|
|
5
|
+
/** Long-term memory + schedules (SQLite): ~/.verybot/memory.db */
|
|
6
|
+
export declare const MEMORY_DB_PATH: string;
|
|
7
|
+
/** Chromium user data: ~/.verybot/browser/ */
|
|
8
|
+
export declare const BROWSER_PROFILE_DIR: string;
|
|
9
|
+
/** Named browser profiles: ~/.verybot/browser-profiles/ */
|
|
10
|
+
export declare const BROWSER_PROFILES_DIR: string;
|
|
11
|
+
/** User skill definitions: ~/.verybot/skills/ */
|
|
12
|
+
export declare const SKILLS_DIR: string;
|
|
13
|
+
/** User integration definitions: ~/.verybot/integrations/ */
|
|
14
|
+
export declare const INTEGRATIONS_DIR: string;
|
|
15
|
+
/** Docker sandbox workspace: ~/.verybot/workspace/ */
|
|
16
|
+
export declare const SANDBOX_WORKSPACE: string;
|
|
17
|
+
/** WhatsApp Baileys auth state: ~/.verybot/whatsapp-auth/ */
|
|
18
|
+
export declare const WHATSAPP_AUTH_DIR: string;
|
|
19
|
+
/** Ensure all data directories exist. Call once at startup. */
|
|
20
|
+
export declare function ensureDirs(): void;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
/** Root data directory: ~/.verybot */
|
|
5
|
+
export const BASE_DIR = join(homedir(), ".verybot");
|
|
6
|
+
/** Conversation history (JSONL files): ~/.verybot/sessions/ */
|
|
7
|
+
export const SESSIONS_DIR = join(BASE_DIR, "sessions");
|
|
8
|
+
/** Long-term memory + schedules (SQLite): ~/.verybot/memory.db */
|
|
9
|
+
export const MEMORY_DB_PATH = join(BASE_DIR, "memory.db");
|
|
10
|
+
/** Chromium user data: ~/.verybot/browser/ */
|
|
11
|
+
export const BROWSER_PROFILE_DIR = join(BASE_DIR, "browser");
|
|
12
|
+
/** Named browser profiles: ~/.verybot/browser-profiles/ */
|
|
13
|
+
export const BROWSER_PROFILES_DIR = join(BASE_DIR, "browser-profiles");
|
|
14
|
+
/** User skill definitions: ~/.verybot/skills/ */
|
|
15
|
+
export const SKILLS_DIR = join(BASE_DIR, "skills");
|
|
16
|
+
/** User integration definitions: ~/.verybot/integrations/ */
|
|
17
|
+
export const INTEGRATIONS_DIR = join(BASE_DIR, "integrations");
|
|
18
|
+
/** Docker sandbox workspace: ~/.verybot/workspace/ */
|
|
19
|
+
export const SANDBOX_WORKSPACE = join(BASE_DIR, "workspace");
|
|
20
|
+
/** WhatsApp Baileys auth state: ~/.verybot/whatsapp-auth/ */
|
|
21
|
+
export const WHATSAPP_AUTH_DIR = join(BASE_DIR, "whatsapp-auth");
|
|
22
|
+
/** All directories that should exist at startup. */
|
|
23
|
+
const REQUIRED_DIRS = [BASE_DIR, SESSIONS_DIR, BROWSER_PROFILE_DIR, BROWSER_PROFILES_DIR, SKILLS_DIR, INTEGRATIONS_DIR, WHATSAPP_AUTH_DIR];
|
|
24
|
+
/** Ensure all data directories exist. Call once at startup. */
|
|
25
|
+
export function ensureDirs() {
|
|
26
|
+
for (const dir of REQUIRED_DIRS) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Stable IDs for built-in templates — never change these. */
|
|
2
|
+
const ID_CODE_REVIEW_PIPELINE = "builtin-code-review-pipeline";
|
|
3
|
+
const ID_CODE_REVIEWER = "builtin-code-reviewer";
|
|
4
|
+
const ID_SECURITY_REVIEWER = "builtin-security-reviewer";
|
|
5
|
+
const ID_RESEARCHER = "builtin-researcher";
|
|
6
|
+
export const BUILTIN_TEMPLATES = [
|
|
7
|
+
{
|
|
8
|
+
id: ID_CODE_REVIEW_PIPELINE,
|
|
9
|
+
name: "Code Review Pipeline",
|
|
10
|
+
description: "Orchestrator that coordinates a multi-pass code review across workers.",
|
|
11
|
+
role: "orchestrator",
|
|
12
|
+
builtin: true,
|
|
13
|
+
content: `You are a code review orchestrator. Your job is to coordinate a thorough review of code changes by delegating to specialist workers.
|
|
14
|
+
|
|
15
|
+
When reviewing code:
|
|
16
|
+
1. First delegate to the Code Reviewer for general quality, readability, and correctness.
|
|
17
|
+
2. Then delegate to the Security Reviewer for vulnerability analysis.
|
|
18
|
+
3. Synthesize the findings into a single, actionable review summary.
|
|
19
|
+
|
|
20
|
+
Prioritize issues by severity: critical > major > minor > suggestion.
|
|
21
|
+
Always include specific file paths and line references.`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: ID_CODE_REVIEWER,
|
|
25
|
+
name: "Code Reviewer",
|
|
26
|
+
description: "Worker that reviews code for quality, readability, and correctness.",
|
|
27
|
+
role: "worker",
|
|
28
|
+
builtin: true,
|
|
29
|
+
content: `You are a senior code reviewer. Analyze code changes for:
|
|
30
|
+
|
|
31
|
+
- **Correctness**: Logic errors, edge cases, off-by-one errors, race conditions.
|
|
32
|
+
- **Readability**: Naming, structure, comments where non-obvious.
|
|
33
|
+
- **Maintainability**: DRY violations, coupling, complexity.
|
|
34
|
+
- **Performance**: Unnecessary allocations, N+1 queries, missing indexes.
|
|
35
|
+
- **Testing**: Missing test coverage, brittle tests, untested edge cases.
|
|
36
|
+
|
|
37
|
+
Be specific — reference file paths and line numbers. Suggest concrete improvements, not vague advice.`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: ID_SECURITY_REVIEWER,
|
|
41
|
+
name: "Security Reviewer",
|
|
42
|
+
description: "Worker that reviews code for security vulnerabilities.",
|
|
43
|
+
role: "worker",
|
|
44
|
+
builtin: true,
|
|
45
|
+
content: `You are a security reviewer. Analyze code for vulnerabilities including:
|
|
46
|
+
|
|
47
|
+
- **Injection**: SQL injection, command injection, XSS, template injection.
|
|
48
|
+
- **Authentication/Authorization**: Broken access control, missing auth checks.
|
|
49
|
+
- **Data Exposure**: Leaked secrets, verbose errors, insecure logging.
|
|
50
|
+
- **Input Validation**: Missing or insufficient validation at trust boundaries.
|
|
51
|
+
- **Cryptography**: Weak algorithms, hardcoded keys, insecure randomness.
|
|
52
|
+
- **Dependencies**: Known vulnerable packages, supply chain risks.
|
|
53
|
+
|
|
54
|
+
Rate each finding by severity (Critical/High/Medium/Low) and provide a remediation recommendation.`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: ID_RESEARCHER,
|
|
58
|
+
name: "Researcher",
|
|
59
|
+
description: "Worker that gathers information from the web and documentation.",
|
|
60
|
+
role: "worker",
|
|
61
|
+
builtin: true,
|
|
62
|
+
content: `You are a research specialist. When given a research task:
|
|
63
|
+
|
|
64
|
+
1. Break the question into sub-questions if needed.
|
|
65
|
+
2. Use web search and documentation to gather authoritative sources.
|
|
66
|
+
3. Cross-reference multiple sources to verify accuracy.
|
|
67
|
+
4. Summarize findings with citations and source links.
|
|
68
|
+
5. Clearly distinguish facts from opinions and note any conflicting information.
|
|
69
|
+
|
|
70
|
+
Prefer primary sources (official docs, RFCs, academic papers) over secondary ones.`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PromptTemplate } from "./types.js";
|
|
2
|
+
/** Max allowed length for template names. */
|
|
3
|
+
export declare const MAX_TEMPLATE_NAME_LENGTH = 128;
|
|
4
|
+
/** Max allowed length for template content. */
|
|
5
|
+
export declare const MAX_TEMPLATE_CONTENT_LENGTH = 50000;
|
|
6
|
+
export interface CreatePromptTemplateInput {
|
|
7
|
+
id?: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
role: "orchestrator" | "worker";
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
export interface UpdatePromptTemplateInput {
|
|
14
|
+
name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
role?: "orchestrator" | "worker";
|
|
17
|
+
content?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* SQLite-backed persistence for prompt templates.
|
|
21
|
+
* Shares the same DB file as TeamStore, TaskStore, etc.
|
|
22
|
+
*/
|
|
23
|
+
export declare class PromptTemplateStore {
|
|
24
|
+
private db;
|
|
25
|
+
private constructor();
|
|
26
|
+
static create(dbPath: string): Promise<PromptTemplateStore>;
|
|
27
|
+
private createSchema;
|
|
28
|
+
createPromptTemplate(input: CreatePromptTemplateInput): PromptTemplate;
|
|
29
|
+
updatePromptTemplate(id: string, input: UpdatePromptTemplateInput): PromptTemplate | null;
|
|
30
|
+
deletePromptTemplate(id: string): boolean;
|
|
31
|
+
getPromptTemplateById(id: string): PromptTemplate | null;
|
|
32
|
+
listPromptTemplates(): PromptTemplate[];
|
|
33
|
+
/**
|
|
34
|
+
* Upsert built-in templates by id on boot.
|
|
35
|
+
* Only updates name/description/role/content for existing builtins.
|
|
36
|
+
*/
|
|
37
|
+
seedBuiltins(templates: Omit<PromptTemplate, "createdAt" | "updatedAt">[]): void;
|
|
38
|
+
close(): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
/** Max allowed length for template names. */
|
|
7
|
+
export const MAX_TEMPLATE_NAME_LENGTH = 128;
|
|
8
|
+
/** Max allowed length for template content. */
|
|
9
|
+
export const MAX_TEMPLATE_CONTENT_LENGTH = 50_000;
|
|
10
|
+
const VALID_ROLES = new Set(["orchestrator", "worker"]);
|
|
11
|
+
/**
|
|
12
|
+
* SQLite-backed persistence for prompt templates.
|
|
13
|
+
* Shares the same DB file as TeamStore, TaskStore, etc.
|
|
14
|
+
*/
|
|
15
|
+
export class PromptTemplateStore {
|
|
16
|
+
db;
|
|
17
|
+
constructor(db) {
|
|
18
|
+
this.db = db;
|
|
19
|
+
}
|
|
20
|
+
static async create(dbPath) {
|
|
21
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
22
|
+
const db = new Database(dbPath);
|
|
23
|
+
db.pragma("journal_mode = WAL");
|
|
24
|
+
const store = new PromptTemplateStore(db);
|
|
25
|
+
store.createSchema();
|
|
26
|
+
return store;
|
|
27
|
+
}
|
|
28
|
+
createSchema() {
|
|
29
|
+
this.db.pragma("foreign_keys = ON");
|
|
30
|
+
this.db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS prompt_templates (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
name TEXT NOT NULL UNIQUE,
|
|
34
|
+
description TEXT NOT NULL DEFAULT '',
|
|
35
|
+
role TEXT NOT NULL,
|
|
36
|
+
content TEXT NOT NULL DEFAULT '',
|
|
37
|
+
builtin INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
created_at INTEGER NOT NULL,
|
|
39
|
+
updated_at INTEGER NOT NULL
|
|
40
|
+
);
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
createPromptTemplate(input) {
|
|
44
|
+
validateName(input.name);
|
|
45
|
+
validateContent(input.content);
|
|
46
|
+
validateRole(input.role);
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const id = input.id ?? randomUUID();
|
|
49
|
+
try {
|
|
50
|
+
this.db.prepare(`INSERT INTO prompt_templates (id, name, description, role, content, builtin, created_at, updated_at)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)`).run(id, input.name, input.description ?? "", input.role, input.content, now, now);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
55
|
+
throw new Error(`A template named "${input.name}" already exists`);
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
id, name: input.name, description: input.description ?? "",
|
|
61
|
+
role: input.role, content: input.content, builtin: false,
|
|
62
|
+
createdAt: now, updatedAt: now,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
updatePromptTemplate(id, input) {
|
|
66
|
+
const existing = this.getPromptTemplateById(id);
|
|
67
|
+
if (!existing)
|
|
68
|
+
return null;
|
|
69
|
+
if (existing.builtin)
|
|
70
|
+
throw new Error("Cannot modify a built-in template");
|
|
71
|
+
if (input.name !== undefined)
|
|
72
|
+
validateName(input.name);
|
|
73
|
+
if (input.content !== undefined)
|
|
74
|
+
validateContent(input.content);
|
|
75
|
+
if (input.role !== undefined)
|
|
76
|
+
validateRole(input.role);
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const updated = {
|
|
79
|
+
...existing,
|
|
80
|
+
...(input.name !== undefined && { name: input.name }),
|
|
81
|
+
...(input.description !== undefined && { description: input.description }),
|
|
82
|
+
...(input.role !== undefined && { role: input.role }),
|
|
83
|
+
...(input.content !== undefined && { content: input.content }),
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
this.db.prepare(`UPDATE prompt_templates SET name = ?, description = ?, role = ?, content = ?, updated_at = ? WHERE id = ?`).run(updated.name, updated.description, updated.role, updated.content, now, id);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
91
|
+
throw new Error(`A template named "${input.name}" already exists`);
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
return updated;
|
|
96
|
+
}
|
|
97
|
+
deletePromptTemplate(id) {
|
|
98
|
+
const existing = this.getPromptTemplateById(id);
|
|
99
|
+
if (existing?.builtin)
|
|
100
|
+
throw new Error("Cannot delete a built-in template");
|
|
101
|
+
const info = this.db.prepare("DELETE FROM prompt_templates WHERE id = ?").run(id);
|
|
102
|
+
return info.changes > 0;
|
|
103
|
+
}
|
|
104
|
+
getPromptTemplateById(id) {
|
|
105
|
+
const row = this.db.prepare("SELECT * FROM prompt_templates WHERE id = ?").get(id);
|
|
106
|
+
return row ? toPromptTemplate(row) : null;
|
|
107
|
+
}
|
|
108
|
+
listPromptTemplates() {
|
|
109
|
+
const rows = this.db.prepare("SELECT * FROM prompt_templates ORDER BY builtin DESC, created_at ASC").all();
|
|
110
|
+
return rows.map(toPromptTemplate);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Upsert built-in templates by id on boot.
|
|
114
|
+
* Only updates name/description/role/content for existing builtins.
|
|
115
|
+
*/
|
|
116
|
+
seedBuiltins(templates) {
|
|
117
|
+
const upsert = this.db.prepare(`
|
|
118
|
+
INSERT INTO prompt_templates (id, name, description, role, content, builtin, created_at, updated_at)
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)
|
|
120
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
121
|
+
name = excluded.name,
|
|
122
|
+
description = excluded.description,
|
|
123
|
+
role = excluded.role,
|
|
124
|
+
content = excluded.content,
|
|
125
|
+
builtin = 1,
|
|
126
|
+
updated_at = excluded.updated_at
|
|
127
|
+
`);
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const runAll = this.db.transaction(() => {
|
|
130
|
+
for (const t of templates) {
|
|
131
|
+
upsert.run(t.id, t.name, t.description, t.role, t.content, now, now);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
runAll();
|
|
135
|
+
logger.info(`Seeded ${templates.length} built-in prompt templates`);
|
|
136
|
+
}
|
|
137
|
+
close() {
|
|
138
|
+
this.db.close();
|
|
139
|
+
logger.info("Prompt template store closed");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function toPromptTemplate(row) {
|
|
143
|
+
return {
|
|
144
|
+
id: row.id,
|
|
145
|
+
name: row.name,
|
|
146
|
+
description: row.description ?? "",
|
|
147
|
+
role: row.role,
|
|
148
|
+
content: row.content ?? "",
|
|
149
|
+
builtin: row.builtin === 1,
|
|
150
|
+
createdAt: row.created_at,
|
|
151
|
+
updatedAt: row.updated_at,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function validateName(name) {
|
|
155
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
156
|
+
throw new Error("Template name is required");
|
|
157
|
+
}
|
|
158
|
+
if (name.length > MAX_TEMPLATE_NAME_LENGTH) {
|
|
159
|
+
throw new Error(`Template name exceeds maximum length of ${MAX_TEMPLATE_NAME_LENGTH}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function validateContent(content) {
|
|
163
|
+
if (typeof content !== "string") {
|
|
164
|
+
throw new Error("Template content must be a string");
|
|
165
|
+
}
|
|
166
|
+
if (content.length > MAX_TEMPLATE_CONTENT_LENGTH) {
|
|
167
|
+
throw new Error(`Template content exceeds maximum length of ${MAX_TEMPLATE_CONTENT_LENGTH}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function validateRole(role) {
|
|
171
|
+
if (!VALID_ROLES.has(role)) {
|
|
172
|
+
throw new Error("role must be 'orchestrator' or 'worker'");
|
|
173
|
+
}
|
|
174
|
+
}
|