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,1721 @@
|
|
|
1
|
+
// --- 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 crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = process.env.CTM_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
|
|
8
|
+
const DEFAULT_DB_PATH = path.join(DATA_DIR, 'task-manager.db');
|
|
9
|
+
const DEFAULT_IMAGES_DIR = path.join(DATA_DIR, 'images');
|
|
10
|
+
const BACKUP_DIR = path.join(DATA_DIR, 'backups');
|
|
11
|
+
|
|
12
|
+
let db = null;
|
|
13
|
+
let currentDbPath = null;
|
|
14
|
+
let backupIntervalId = null;
|
|
15
|
+
|
|
16
|
+
function getDb() {
|
|
17
|
+
if (db) return db;
|
|
18
|
+
throw new Error('Database not initialized. Call initDb() first.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function initDb(dbPath) {
|
|
22
|
+
dbPath = dbPath || DEFAULT_DB_PATH;
|
|
23
|
+
currentDbPath = dbPath;
|
|
24
|
+
const dir = path.dirname(dbPath);
|
|
25
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
if (!fs.existsSync(DEFAULT_IMAGES_DIR)) fs.mkdirSync(DEFAULT_IMAGES_DIR, { recursive: true });
|
|
27
|
+
if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
28
|
+
|
|
29
|
+
db = new Database(dbPath);
|
|
30
|
+
db.pragma('journal_mode = WAL');
|
|
31
|
+
db.pragma('busy_timeout = 5000');
|
|
32
|
+
db.pragma('foreign_keys = ON');
|
|
33
|
+
|
|
34
|
+
createTables();
|
|
35
|
+
runMigrations();
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createTables() {
|
|
40
|
+
db.exec(`
|
|
41
|
+
-- Settings (key-value store)
|
|
42
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
43
|
+
key TEXT PRIMARY KEY,
|
|
44
|
+
value TEXT,
|
|
45
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Folders for organizing prompts
|
|
49
|
+
CREATE TABLE IF NOT EXISTS folders (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
parent_id INTEGER REFERENCES folders(id) ON DELETE SET NULL,
|
|
53
|
+
color TEXT DEFAULT '#7aa2f7',
|
|
54
|
+
sort_order INTEGER DEFAULT 0,
|
|
55
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- Prompts
|
|
59
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
title TEXT NOT NULL,
|
|
62
|
+
content TEXT NOT NULL DEFAULT '',
|
|
63
|
+
content_html TEXT NOT NULL DEFAULT '',
|
|
64
|
+
folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL,
|
|
65
|
+
context_type TEXT DEFAULT 'general',
|
|
66
|
+
tags TEXT DEFAULT '[]',
|
|
67
|
+
is_template INTEGER DEFAULT 0,
|
|
68
|
+
template_vars TEXT DEFAULT '[]',
|
|
69
|
+
starred INTEGER DEFAULT 0,
|
|
70
|
+
sort_order INTEGER DEFAULT 0,
|
|
71
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
72
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
-- Prompt versions (history)
|
|
76
|
+
CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
prompt_id INTEGER NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,
|
|
79
|
+
version INTEGER NOT NULL,
|
|
80
|
+
title TEXT NOT NULL,
|
|
81
|
+
content TEXT NOT NULL DEFAULT '',
|
|
82
|
+
content_html TEXT NOT NULL DEFAULT '',
|
|
83
|
+
tags TEXT DEFAULT '[]',
|
|
84
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
85
|
+
message TEXT DEFAULT ''
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
-- Images (stored as blobs or file references)
|
|
89
|
+
CREATE TABLE IF NOT EXISTS images (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
prompt_id INTEGER REFERENCES prompts(id) ON DELETE CASCADE,
|
|
92
|
+
filename TEXT NOT NULL,
|
|
93
|
+
mime_type TEXT DEFAULT 'image/png',
|
|
94
|
+
file_path TEXT NOT NULL,
|
|
95
|
+
annotations TEXT DEFAULT '[]',
|
|
96
|
+
width INTEGER,
|
|
97
|
+
height INTEGER,
|
|
98
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- Prompt chains
|
|
102
|
+
CREATE TABLE IF NOT EXISTS chains (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
name TEXT NOT NULL,
|
|
105
|
+
description TEXT DEFAULT '',
|
|
106
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
107
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
-- Chain nodes (steps in a chain)
|
|
111
|
+
CREATE TABLE IF NOT EXISTS chain_nodes (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
chain_id INTEGER NOT NULL REFERENCES chains(id) ON DELETE CASCADE,
|
|
114
|
+
prompt_id INTEGER REFERENCES prompts(id) ON DELETE SET NULL,
|
|
115
|
+
node_type TEXT DEFAULT 'prompt',
|
|
116
|
+
position_x REAL DEFAULT 0,
|
|
117
|
+
position_y REAL DEFAULT 0,
|
|
118
|
+
config TEXT DEFAULT '{}',
|
|
119
|
+
sort_order INTEGER DEFAULT 0
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
-- Chain edges (connections between nodes)
|
|
123
|
+
CREATE TABLE IF NOT EXISTS chain_edges (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
chain_id INTEGER NOT NULL REFERENCES chains(id) ON DELETE CASCADE,
|
|
126
|
+
from_node_id INTEGER NOT NULL REFERENCES chain_nodes(id) ON DELETE CASCADE,
|
|
127
|
+
to_node_id INTEGER NOT NULL REFERENCES chain_nodes(id) ON DELETE CASCADE,
|
|
128
|
+
condition TEXT DEFAULT '',
|
|
129
|
+
label TEXT DEFAULT ''
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
-- Prompt usage tracking
|
|
133
|
+
CREATE TABLE IF NOT EXISTS prompt_usage (
|
|
134
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
135
|
+
prompt_id INTEGER NOT NULL REFERENCES prompts(id) ON DELETE CASCADE,
|
|
136
|
+
session_id TEXT,
|
|
137
|
+
used_at TEXT DEFAULT (datetime('now')),
|
|
138
|
+
result TEXT DEFAULT '',
|
|
139
|
+
effectiveness_score REAL
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
-- Permission rules
|
|
143
|
+
CREATE TABLE IF NOT EXISTS permission_rules (
|
|
144
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
145
|
+
tool_name TEXT NOT NULL,
|
|
146
|
+
pattern TEXT DEFAULT '*',
|
|
147
|
+
action TEXT DEFAULT 'ask',
|
|
148
|
+
project_path TEXT DEFAULT '*',
|
|
149
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
150
|
+
UNIQUE(tool_name, pattern, project_path)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
-- Permission audit log (from session parsing)
|
|
154
|
+
CREATE TABLE IF NOT EXISTS permission_log (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
session_id TEXT,
|
|
157
|
+
tool_name TEXT NOT NULL,
|
|
158
|
+
action_taken TEXT,
|
|
159
|
+
details TEXT DEFAULT '',
|
|
160
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- Session conversations (imported from JSONL)
|
|
164
|
+
CREATE TABLE IF NOT EXISTS session_conversations (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
session_id TEXT NOT NULL,
|
|
167
|
+
project_path TEXT,
|
|
168
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
169
|
+
user_msg_count INTEGER DEFAULT 0,
|
|
170
|
+
assistant_msg_count INTEGER DEFAULT 0,
|
|
171
|
+
title TEXT,
|
|
172
|
+
first_message TEXT,
|
|
173
|
+
git_branch TEXT,
|
|
174
|
+
file_size INTEGER DEFAULT 0,
|
|
175
|
+
session_created_at TEXT,
|
|
176
|
+
imported_at TEXT DEFAULT (datetime('now')),
|
|
177
|
+
UNIQUE(session_id)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
-- Prompt templates
|
|
181
|
+
CREATE TABLE IF NOT EXISTS templates (
|
|
182
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
183
|
+
name TEXT NOT NULL,
|
|
184
|
+
description TEXT DEFAULT '',
|
|
185
|
+
content TEXT NOT NULL DEFAULT '',
|
|
186
|
+
content_html TEXT NOT NULL DEFAULT '',
|
|
187
|
+
variables TEXT DEFAULT '[]',
|
|
188
|
+
category TEXT DEFAULT 'general',
|
|
189
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
-- Create indexes
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_folder ON prompts(folder_id);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_context ON prompts(context_type);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_prompts_updated ON prompts(updated_at);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_versions_prompt ON prompt_versions(prompt_id);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_images_prompt ON images(prompt_id);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_chain_nodes_chain ON chain_nodes(chain_id);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_chain_edges_chain ON chain_edges(chain_id);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_usage_prompt ON prompt_usage(prompt_id);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_session_conversations_session ON session_conversations(session_id);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_permission_log_session ON permission_log(session_id);
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function runMigrations() {
|
|
207
|
+
// Add pinned column to prompts if not exists
|
|
208
|
+
const cols = getDb().prepare("PRAGMA table_info(prompts)").all();
|
|
209
|
+
if (!cols.find(c => c.name === 'pinned')) {
|
|
210
|
+
getDb().exec('ALTER TABLE prompts ADD COLUMN pinned INTEGER DEFAULT 0');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add session_titles table for AI-generated titles
|
|
214
|
+
const hasTitles = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_titles'").get();
|
|
215
|
+
if (!hasTitles) {
|
|
216
|
+
getDb().exec(`
|
|
217
|
+
CREATE TABLE session_titles (
|
|
218
|
+
session_id TEXT PRIMARY KEY,
|
|
219
|
+
title TEXT NOT NULL,
|
|
220
|
+
user_renamed INTEGER DEFAULT 0,
|
|
221
|
+
generated_at TEXT DEFAULT (datetime('now'))
|
|
222
|
+
);
|
|
223
|
+
`);
|
|
224
|
+
} else {
|
|
225
|
+
// Migration: add user_renamed column if missing
|
|
226
|
+
const cols = getDb().prepare("PRAGMA table_info(session_titles)").all();
|
|
227
|
+
if (!cols.find(c => c.name === 'user_renamed')) {
|
|
228
|
+
getDb().exec("ALTER TABLE session_titles ADD COLUMN user_renamed INTEGER DEFAULT 0");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add session_folders table for user-overridden project folders
|
|
233
|
+
const hasFolders = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_folders'").get();
|
|
234
|
+
if (!hasFolders) {
|
|
235
|
+
getDb().exec(`
|
|
236
|
+
CREATE TABLE session_folders (
|
|
237
|
+
session_id TEXT PRIMARY KEY,
|
|
238
|
+
folder TEXT NOT NULL,
|
|
239
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
240
|
+
);
|
|
241
|
+
`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Add prompt_queues table
|
|
245
|
+
const hasQueues = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='prompt_queues'").get();
|
|
246
|
+
if (!hasQueues) {
|
|
247
|
+
getDb().exec(`
|
|
248
|
+
CREATE TABLE prompt_queues (
|
|
249
|
+
session_id TEXT PRIMARY KEY,
|
|
250
|
+
mode TEXT DEFAULT 'manual',
|
|
251
|
+
status TEXT DEFAULT 'idle',
|
|
252
|
+
current_index INTEGER DEFAULT -1,
|
|
253
|
+
idle_timeout_ms INTEGER DEFAULT 10000,
|
|
254
|
+
items TEXT DEFAULT '[]',
|
|
255
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
256
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
257
|
+
);
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add perm_rules table (source of truth for all permission rules)
|
|
262
|
+
const hasPermRules = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='perm_rules'").get();
|
|
263
|
+
if (!hasPermRules) {
|
|
264
|
+
getDb().exec(`
|
|
265
|
+
CREATE TABLE perm_rules (
|
|
266
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
267
|
+
rule TEXT NOT NULL,
|
|
268
|
+
list_type TEXT NOT NULL DEFAULT 'allow',
|
|
269
|
+
scope TEXT NOT NULL DEFAULT 'global',
|
|
270
|
+
project TEXT NOT NULL DEFAULT '__global__',
|
|
271
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
272
|
+
UNIQUE(rule, list_type, project)
|
|
273
|
+
);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_perm_rules_project ON perm_rules(project);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_perm_rules_type ON perm_rules(list_type);
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add always_ask column to perm_rules (migration)
|
|
280
|
+
const hasAlwaysAsk = getDb().prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='perm_rules'").get();
|
|
281
|
+
if (hasAlwaysAsk && !hasAlwaysAsk.sql.includes('always_ask')) {
|
|
282
|
+
getDb().exec('ALTER TABLE perm_rules ADD COLUMN always_ask INTEGER DEFAULT 0');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Migrate tool_permissions_cache data into perm_rules
|
|
286
|
+
const hasTPC = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tool_permissions_cache'").get();
|
|
287
|
+
if (hasTPC) {
|
|
288
|
+
const rows = getDb().prepare('SELECT * FROM tool_permissions_cache').all();
|
|
289
|
+
if (rows.length > 0) {
|
|
290
|
+
const ins = getDb().prepare(
|
|
291
|
+
`INSERT OR IGNORE INTO perm_rules (rule, list_type, scope, project, always_ask)
|
|
292
|
+
VALUES (?, 'allow', ?, ?, ?)`
|
|
293
|
+
);
|
|
294
|
+
for (const r of rows) {
|
|
295
|
+
if (r.project_path === '__always_ask__') {
|
|
296
|
+
// always_ask flag — update existing rule or insert
|
|
297
|
+
getDb().prepare(
|
|
298
|
+
`UPDATE perm_rules SET always_ask = 1 WHERE rule = ? AND list_type = 'allow'`
|
|
299
|
+
).run(r.tool_name);
|
|
300
|
+
// If no row was updated (rule doesn't exist as broad rule), insert it as global
|
|
301
|
+
ins.run(r.tool_name, 'global', '__global__', 1);
|
|
302
|
+
} else {
|
|
303
|
+
const scope = r.project_path === '__global__' ? 'global' : 'project';
|
|
304
|
+
ins.run(r.tool_name, scope, r.project_path, 0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
console.log(` Migrated ${rows.length} entries from tool_permissions_cache → perm_rules`);
|
|
308
|
+
}
|
|
309
|
+
getDb().exec('DROP TABLE tool_permissions_cache');
|
|
310
|
+
console.log(' Dropped tool_permissions_cache table');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add parent_id column to prompts for tree grouping
|
|
314
|
+
const promptsSql = getDb().prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='prompts'").get();
|
|
315
|
+
if (promptsSql && !promptsSql.sql.includes('parent_id')) {
|
|
316
|
+
getDb().exec('ALTER TABLE prompts ADD COLUMN parent_id INTEGER REFERENCES prompts(id) ON DELETE SET NULL');
|
|
317
|
+
getDb().exec('CREATE INDEX IF NOT EXISTS idx_prompts_parent ON prompts(parent_id)');
|
|
318
|
+
console.log(' Added parent_id column to prompts table');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Add auto_approvals table
|
|
322
|
+
const hasAutoApprovals = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='auto_approvals'").get();
|
|
323
|
+
if (!hasAutoApprovals) {
|
|
324
|
+
getDb().exec(`
|
|
325
|
+
CREATE TABLE auto_approvals (
|
|
326
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
327
|
+
pattern TEXT NOT NULL,
|
|
328
|
+
response TEXT NOT NULL DEFAULT 'yes',
|
|
329
|
+
label TEXT NOT NULL DEFAULT '',
|
|
330
|
+
category TEXT NOT NULL DEFAULT '',
|
|
331
|
+
enabled INTEGER DEFAULT 1,
|
|
332
|
+
occurrences INTEGER DEFAULT 0,
|
|
333
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
334
|
+
UNIQUE(pattern)
|
|
335
|
+
);
|
|
336
|
+
`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Add approval_rules table (learned rules from AI agent)
|
|
340
|
+
const hasApprovalRules = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='approval_rules'").get();
|
|
341
|
+
if (!hasApprovalRules) {
|
|
342
|
+
getDb().exec(`
|
|
343
|
+
CREATE TABLE approval_rules (
|
|
344
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
345
|
+
pattern TEXT NOT NULL,
|
|
346
|
+
label TEXT NOT NULL DEFAULT '',
|
|
347
|
+
description TEXT DEFAULT '',
|
|
348
|
+
category TEXT DEFAULT '',
|
|
349
|
+
risk_level TEXT DEFAULT 'low',
|
|
350
|
+
enabled INTEGER DEFAULT 1,
|
|
351
|
+
times_matched INTEGER DEFAULT 0,
|
|
352
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
353
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
354
|
+
UNIQUE(pattern)
|
|
355
|
+
);
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Rename generic "Local dev operations" rules with descriptive labels
|
|
360
|
+
{
|
|
361
|
+
const labelMap = [
|
|
362
|
+
{ pattern: 'echo\\s+.*>\\s*\\/tmp\\/', label: 'Write to /tmp', desc: 'Echo output to temp files' },
|
|
363
|
+
{ pattern: 'cat\\s', label: 'Read file contents', desc: 'View file contents with cat' },
|
|
364
|
+
{ pattern: 'ls\\s', label: 'List directory', desc: 'List files and directories' },
|
|
365
|
+
{ pattern: 'pwd', label: 'Print working directory', desc: 'Show current directory path' },
|
|
366
|
+
{ pattern: 'git\\s+(status|log|diff|branch|show)', label: 'Git read operations', desc: 'Read-only git commands' },
|
|
367
|
+
{ pattern: 'node\\s+-e', label: 'Node one-liner', desc: 'Run inline Node.js expression' },
|
|
368
|
+
{ pattern: 'python3?\\s+-c', label: 'Python one-liner', desc: 'Run inline Python expression' },
|
|
369
|
+
{ pattern: 'npm\\s+(run|test|start)', label: 'npm script', desc: 'Run npm scripts (run, test, start)' },
|
|
370
|
+
{ pattern: 'mkdir\\s+-?p?\\s', label: 'Create directory', desc: 'Create directories with mkdir' },
|
|
371
|
+
{ pattern: '>\\s*\\/tmp\\/', label: 'Write to /tmp', desc: 'Redirect output to temp files' },
|
|
372
|
+
{ pattern: 'touch\\s', label: 'Create empty file', desc: 'Create or update file timestamps' },
|
|
373
|
+
{ pattern: 'cp\\s', label: 'Copy files', desc: 'Copy files or directories' },
|
|
374
|
+
{ pattern: 'mv\\s', label: 'Move/rename files', desc: 'Move or rename files' },
|
|
375
|
+
];
|
|
376
|
+
const stmt = getDb().prepare(
|
|
377
|
+
`UPDATE approval_rules SET label = ?, description = ? WHERE pattern = ? AND label = 'Local dev operations'`
|
|
378
|
+
);
|
|
379
|
+
for (const { pattern, label, desc } of labelMap) {
|
|
380
|
+
stmt.run(label, desc, pattern);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add approval_decisions table (audit log of all decisions)
|
|
385
|
+
const hasApprovalDecisions = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='approval_decisions'").get();
|
|
386
|
+
if (!hasApprovalDecisions) {
|
|
387
|
+
getDb().exec(`
|
|
388
|
+
CREATE TABLE approval_decisions (
|
|
389
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
390
|
+
session_id TEXT,
|
|
391
|
+
tool_name TEXT DEFAULT '',
|
|
392
|
+
command_summary TEXT NOT NULL DEFAULT '',
|
|
393
|
+
full_context TEXT DEFAULT '',
|
|
394
|
+
warning_text TEXT DEFAULT '',
|
|
395
|
+
decision TEXT NOT NULL DEFAULT 'escalated',
|
|
396
|
+
reasoning TEXT DEFAULT '',
|
|
397
|
+
decided_by TEXT DEFAULT 'ai',
|
|
398
|
+
rule_id INTEGER,
|
|
399
|
+
risk_level TEXT DEFAULT 'low',
|
|
400
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
401
|
+
);
|
|
402
|
+
CREATE INDEX IF NOT EXISTS idx_approval_decisions_session ON approval_decisions(session_id);
|
|
403
|
+
CREATE INDEX IF NOT EXISTS idx_approval_decisions_created ON approval_decisions(created_at);
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Add code_reviews tables
|
|
408
|
+
const hasCodeReviews = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='code_reviews'").get();
|
|
409
|
+
if (!hasCodeReviews) {
|
|
410
|
+
getDb().exec(`
|
|
411
|
+
CREATE TABLE code_reviews (
|
|
412
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
413
|
+
session_id TEXT,
|
|
414
|
+
project_path TEXT NOT NULL,
|
|
415
|
+
base_ref TEXT DEFAULT '',
|
|
416
|
+
status TEXT DEFAULT 'draft',
|
|
417
|
+
summary TEXT DEFAULT '',
|
|
418
|
+
file_count INTEGER DEFAULT 0,
|
|
419
|
+
comment_count INTEGER DEFAULT 0,
|
|
420
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
421
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
422
|
+
submitted_at TEXT
|
|
423
|
+
);
|
|
424
|
+
CREATE INDEX IF NOT EXISTS idx_code_reviews_session ON code_reviews(session_id);
|
|
425
|
+
CREATE INDEX IF NOT EXISTS idx_code_reviews_project ON code_reviews(project_path);
|
|
426
|
+
|
|
427
|
+
CREATE TABLE review_comments (
|
|
428
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
429
|
+
review_id INTEGER NOT NULL REFERENCES code_reviews(id) ON DELETE CASCADE,
|
|
430
|
+
file_path TEXT NOT NULL,
|
|
431
|
+
line_start INTEGER NOT NULL,
|
|
432
|
+
line_end INTEGER,
|
|
433
|
+
side TEXT DEFAULT 'new',
|
|
434
|
+
body TEXT NOT NULL,
|
|
435
|
+
severity TEXT DEFAULT 'comment',
|
|
436
|
+
status TEXT DEFAULT 'open',
|
|
437
|
+
ai_generated INTEGER DEFAULT 0,
|
|
438
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
439
|
+
);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_review_comments_review ON review_comments(review_id);
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Add insights tables (session analysis, groups, skills, recommendations)
|
|
445
|
+
const hasSessionAnalyses = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_analyses'").get();
|
|
446
|
+
if (!hasSessionAnalyses) {
|
|
447
|
+
getDb().exec(`
|
|
448
|
+
CREATE TABLE session_analyses (
|
|
449
|
+
session_id TEXT PRIMARY KEY,
|
|
450
|
+
project TEXT,
|
|
451
|
+
title TEXT,
|
|
452
|
+
category TEXT,
|
|
453
|
+
topics TEXT DEFAULT '[]',
|
|
454
|
+
skills_used TEXT DEFAULT '[]',
|
|
455
|
+
complexity TEXT,
|
|
456
|
+
summary TEXT,
|
|
457
|
+
pattern TEXT,
|
|
458
|
+
first_message TEXT,
|
|
459
|
+
session_modified_at TEXT,
|
|
460
|
+
analyzed_at TEXT DEFAULT (datetime('now')),
|
|
461
|
+
token_count INTEGER DEFAULT 0,
|
|
462
|
+
message_count INTEGER DEFAULT 0,
|
|
463
|
+
prompt_efficiency TEXT DEFAULT '{}'
|
|
464
|
+
);
|
|
465
|
+
CREATE INDEX IF NOT EXISTS idx_session_analyses_category ON session_analyses(category);
|
|
466
|
+
CREATE INDEX IF NOT EXISTS idx_session_analyses_analyzed ON session_analyses(analyzed_at);
|
|
467
|
+
|
|
468
|
+
CREATE TABLE insight_groups (
|
|
469
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
470
|
+
name TEXT NOT NULL,
|
|
471
|
+
description TEXT,
|
|
472
|
+
category TEXT,
|
|
473
|
+
is_internal INTEGER DEFAULT 0,
|
|
474
|
+
session_ids TEXT DEFAULT '[]',
|
|
475
|
+
session_count INTEGER DEFAULT 0,
|
|
476
|
+
generated_at TEXT DEFAULT (datetime('now'))
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
CREATE TABLE insight_skills (
|
|
480
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
481
|
+
name TEXT NOT NULL,
|
|
482
|
+
title TEXT,
|
|
483
|
+
description TEXT,
|
|
484
|
+
trigger_pattern TEXT,
|
|
485
|
+
category TEXT,
|
|
486
|
+
is_internal INTEGER DEFAULT 0,
|
|
487
|
+
based_on TEXT DEFAULT '[]',
|
|
488
|
+
priority TEXT DEFAULT 'low',
|
|
489
|
+
example_sessions TEXT DEFAULT '[]',
|
|
490
|
+
generated_at TEXT DEFAULT (datetime('now'))
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
CREATE TABLE insight_recommendations (
|
|
494
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
495
|
+
type TEXT NOT NULL,
|
|
496
|
+
category TEXT,
|
|
497
|
+
title TEXT NOT NULL,
|
|
498
|
+
description TEXT,
|
|
499
|
+
evidence TEXT DEFAULT '{}',
|
|
500
|
+
impact TEXT DEFAULT 'medium',
|
|
501
|
+
actionable TEXT,
|
|
502
|
+
generated_at TEXT DEFAULT (datetime('now'))
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
CREATE TABLE analysis_runs (
|
|
506
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
507
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
508
|
+
completed_at TEXT,
|
|
509
|
+
sessions_analyzed INTEGER DEFAULT 0,
|
|
510
|
+
sessions_skipped INTEGER DEFAULT 0,
|
|
511
|
+
status TEXT DEFAULT 'running'
|
|
512
|
+
);
|
|
513
|
+
`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Add prompt_executions table (harvested session messages)
|
|
517
|
+
const hasPromptExecs = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='prompt_executions'").get();
|
|
518
|
+
if (!hasPromptExecs) {
|
|
519
|
+
getDb().exec(`
|
|
520
|
+
CREATE TABLE prompt_executions (
|
|
521
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
522
|
+
session_id TEXT NOT NULL,
|
|
523
|
+
message_text TEXT NOT NULL,
|
|
524
|
+
message_index INTEGER DEFAULT 0,
|
|
525
|
+
role TEXT DEFAULT 'user',
|
|
526
|
+
tool_uses TEXT DEFAULT '[]',
|
|
527
|
+
image_refs TEXT DEFAULT '[]',
|
|
528
|
+
project_path TEXT DEFAULT '',
|
|
529
|
+
cwd TEXT DEFAULT '',
|
|
530
|
+
source TEXT DEFAULT 'harvested',
|
|
531
|
+
outcome TEXT,
|
|
532
|
+
outcome_notes TEXT,
|
|
533
|
+
executed_at TEXT,
|
|
534
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
535
|
+
UNIQUE(session_id, message_index)
|
|
536
|
+
);
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_pe_session ON prompt_executions(session_id);
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_pe_role ON prompt_executions(role);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_pe_project ON prompt_executions(project_path);
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_pe_executed ON prompt_executions(executed_at);
|
|
541
|
+
`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Add prompt_patterns table (detected reusable patterns)
|
|
545
|
+
const hasPromptPatterns = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='prompt_patterns'").get();
|
|
546
|
+
if (!hasPromptPatterns) {
|
|
547
|
+
getDb().exec(`
|
|
548
|
+
CREATE TABLE prompt_patterns (
|
|
549
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
550
|
+
normalized_text TEXT NOT NULL,
|
|
551
|
+
frequency INTEGER DEFAULT 1,
|
|
552
|
+
example_text TEXT,
|
|
553
|
+
sessions TEXT DEFAULT '[]',
|
|
554
|
+
projects TEXT DEFAULT '[]',
|
|
555
|
+
last_used_at TEXT,
|
|
556
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
557
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
558
|
+
UNIQUE(normalized_text)
|
|
559
|
+
);
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Add harvest_state table (singleton watermark for incremental scanning)
|
|
564
|
+
const hasHarvestState = getDb().prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='harvest_state'").get();
|
|
565
|
+
if (!hasHarvestState) {
|
|
566
|
+
getDb().exec(`
|
|
567
|
+
CREATE TABLE harvest_state (
|
|
568
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
569
|
+
last_scan_at TEXT,
|
|
570
|
+
files_scanned INTEGER DEFAULT 0,
|
|
571
|
+
prompts_harvested INTEGER DEFAULT 0
|
|
572
|
+
);
|
|
573
|
+
`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Add lifecycle_status column to prompts for prompt lifecycle
|
|
577
|
+
const promptsCols2 = getDb().prepare("PRAGMA table_info(prompts)").all();
|
|
578
|
+
if (!promptsCols2.find(c => c.name === 'lifecycle_status')) {
|
|
579
|
+
getDb().exec("ALTER TABLE prompts ADD COLUMN lifecycle_status TEXT DEFAULT 'draft'");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Rename lifecycle statuses: tested→used, proven→frequent
|
|
583
|
+
getDb().exec("UPDATE prompts SET lifecycle_status = 'used' WHERE lifecycle_status = 'tested'");
|
|
584
|
+
getDb().exec("UPDATE prompts SET lifecycle_status = 'frequent' WHERE lifecycle_status = 'proven'");
|
|
585
|
+
|
|
586
|
+
// Add last_lifecycle_refresh_at column to harvest_state
|
|
587
|
+
const hsCols = getDb().prepare("PRAGMA table_info(harvest_state)").all();
|
|
588
|
+
if (!hsCols.find(c => c.name === 'last_lifecycle_refresh_at')) {
|
|
589
|
+
getDb().exec("ALTER TABLE harvest_state ADD COLUMN last_lifecycle_refresh_at TEXT");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// --- Settings CRUD ---
|
|
594
|
+
function getSetting(key, defaultValue) {
|
|
595
|
+
const row = getDb().prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
|
596
|
+
return row ? JSON.parse(row.value) : defaultValue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function getSettingsByPrefix(prefix) {
|
|
600
|
+
return getDb().prepare('SELECT key, value FROM settings WHERE key LIKE ?').all(prefix + '%').map(row => ({
|
|
601
|
+
key: row.key,
|
|
602
|
+
value: JSON.parse(row.value),
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function setSetting(key, value) {
|
|
607
|
+
getDb().prepare(
|
|
608
|
+
"INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
|
|
609
|
+
).run(key, JSON.stringify(value));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// --- Prompts CRUD ---
|
|
613
|
+
function createPrompt({ title, content, content_html, folder_id, context_type, tags, is_template, template_vars, starred, parent_id }) {
|
|
614
|
+
const result = getDb().prepare(
|
|
615
|
+
`INSERT INTO prompts (title, content, content_html, folder_id, context_type, tags, is_template, template_vars, starred, parent_id)
|
|
616
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
617
|
+
).run(
|
|
618
|
+
title, content || '', content_html || '', folder_id || null,
|
|
619
|
+
context_type || 'general', JSON.stringify(tags || []),
|
|
620
|
+
is_template ? 1 : 0, JSON.stringify(template_vars || []),
|
|
621
|
+
starred ? 1 : 0, parent_id || null
|
|
622
|
+
);
|
|
623
|
+
// Create initial version
|
|
624
|
+
getDb().prepare(
|
|
625
|
+
`INSERT INTO prompt_versions (prompt_id, version, title, content, content_html, tags, message)
|
|
626
|
+
VALUES (?, 1, ?, ?, ?, ?, 'Initial version')`
|
|
627
|
+
).run(result.lastInsertRowid, title, content || '', content_html || '', JSON.stringify(tags || []));
|
|
628
|
+
return result.lastInsertRowid;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function updatePrompt(id, { title, content, content_html, folder_id, context_type, tags, is_template, template_vars, starred, pinned, sort_order, parent_id, version_message, lifecycle_status }) {
|
|
632
|
+
const existing = getDb().prepare('SELECT * FROM prompts WHERE id = ?').get(id);
|
|
633
|
+
if (!existing) throw new Error('Prompt not found');
|
|
634
|
+
|
|
635
|
+
getDb().prepare(
|
|
636
|
+
`UPDATE prompts SET title=?, content=?, content_html=?, folder_id=?, context_type=?, tags=?,
|
|
637
|
+
is_template=?, template_vars=?, starred=?, pinned=?, sort_order=?, parent_id=?, lifecycle_status=?, updated_at=datetime('now') WHERE id=?`
|
|
638
|
+
).run(
|
|
639
|
+
title ?? existing.title, content ?? existing.content, content_html ?? existing.content_html,
|
|
640
|
+
folder_id !== undefined ? folder_id : existing.folder_id,
|
|
641
|
+
context_type ?? existing.context_type, tags ? JSON.stringify(tags) : existing.tags,
|
|
642
|
+
is_template !== undefined ? (is_template ? 1 : 0) : existing.is_template,
|
|
643
|
+
template_vars ? JSON.stringify(template_vars) : existing.template_vars,
|
|
644
|
+
starred !== undefined ? (starred ? 1 : 0) : existing.starred,
|
|
645
|
+
pinned !== undefined ? (pinned ? 1 : 0) : (existing.pinned || 0),
|
|
646
|
+
sort_order !== undefined ? sort_order : (existing.sort_order || 0),
|
|
647
|
+
parent_id !== undefined ? parent_id : (existing.parent_id || null),
|
|
648
|
+
lifecycle_status ?? (existing.lifecycle_status || 'draft'),
|
|
649
|
+
id
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Create new version
|
|
653
|
+
const lastVersion = getDb().prepare(
|
|
654
|
+
'SELECT MAX(version) as v FROM prompt_versions WHERE prompt_id = ?'
|
|
655
|
+
).get(id);
|
|
656
|
+
getDb().prepare(
|
|
657
|
+
`INSERT INTO prompt_versions (prompt_id, version, title, content, content_html, tags, message)
|
|
658
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
659
|
+
).run(
|
|
660
|
+
id, (lastVersion?.v || 0) + 1, title ?? existing.title,
|
|
661
|
+
content ?? existing.content, content_html ?? existing.content_html,
|
|
662
|
+
tags ? JSON.stringify(tags) : existing.tags, version_message || ''
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function getPrompt(id) {
|
|
667
|
+
return getDb().prepare('SELECT p.*, (SELECT COUNT(*) FROM prompts c WHERE c.parent_id = p.id) as child_count FROM prompts p WHERE p.id = ?').get(id);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function listPrompts({ folder_id, context_type, search, starred, limit, offset, include_children, lifecycle_status } = {}) {
|
|
671
|
+
let sql = `SELECT p.*, (SELECT COUNT(*) FROM prompts c WHERE c.parent_id = p.id) as child_count FROM prompts p WHERE 1=1`;
|
|
672
|
+
const params = [];
|
|
673
|
+
|
|
674
|
+
// By default, only show top-level prompts (parent_id IS NULL)
|
|
675
|
+
// When searching, show all matches regardless of nesting
|
|
676
|
+
if (!search && !include_children) {
|
|
677
|
+
sql += ' AND p.parent_id IS NULL';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (folder_id !== undefined) { sql += ' AND p.folder_id = ?'; params.push(folder_id); }
|
|
681
|
+
if (context_type) { sql += ' AND p.context_type = ?'; params.push(context_type); }
|
|
682
|
+
if (lifecycle_status) { sql += ' AND (p.lifecycle_status = ? OR (? = \'draft\' AND p.lifecycle_status IS NULL))'; params.push(lifecycle_status, lifecycle_status); }
|
|
683
|
+
if (starred) { sql += ' AND p.starred = 1'; }
|
|
684
|
+
if (search) {
|
|
685
|
+
sql += ' AND (p.title LIKE ? OR p.content LIKE ? OR p.tags LIKE ?)';
|
|
686
|
+
const q = `%${search}%`;
|
|
687
|
+
params.push(q, q, q);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
sql += ' ORDER BY p.pinned DESC, p.sort_order ASC, p.updated_at DESC';
|
|
691
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
692
|
+
if (offset) { sql += ' OFFSET ?'; params.push(offset); }
|
|
693
|
+
|
|
694
|
+
return getDb().prepare(sql).all(...params);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function listChildPrompts(parentId) {
|
|
698
|
+
return getDb().prepare(
|
|
699
|
+
'SELECT * FROM prompts WHERE parent_id = ? ORDER BY sort_order ASC, updated_at DESC'
|
|
700
|
+
).all(parentId);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function setPromptParent(id, parentId) {
|
|
704
|
+
// Prevent circular: can't set parent to self or to own child
|
|
705
|
+
if (parentId === id) throw new Error('Cannot parent to self');
|
|
706
|
+
if (parentId) {
|
|
707
|
+
const target = getDb().prepare('SELECT parent_id FROM prompts WHERE id = ?').get(parentId);
|
|
708
|
+
if (target && target.parent_id) throw new Error('Cannot nest more than 1 level deep');
|
|
709
|
+
// If the dragged prompt has children, prevent it from becoming a child
|
|
710
|
+
const childCount = getDb().prepare('SELECT COUNT(*) as n FROM prompts WHERE parent_id = ?').get(id);
|
|
711
|
+
if (childCount.n > 0) throw new Error('Cannot nest a group inside another group');
|
|
712
|
+
}
|
|
713
|
+
getDb().prepare('UPDATE prompts SET parent_id = ?, updated_at = datetime(\'now\') WHERE id = ?').run(parentId || null, id);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function createGroupFromPrompts(promptIds, { title, content, contentHtml } = {}) {
|
|
717
|
+
if (!promptIds || promptIds.length < 2) throw new Error('Need at least 2 prompts to create a group');
|
|
718
|
+
const prompts = promptIds.map(id => {
|
|
719
|
+
const p = getDb().prepare('SELECT * FROM prompts WHERE id = ?').get(id);
|
|
720
|
+
if (!p) throw new Error(`Prompt ${id} not found`);
|
|
721
|
+
if (p.parent_id) throw new Error(`Prompt "${p.title}" is already in a group`);
|
|
722
|
+
const childCount = getDb().prepare('SELECT COUNT(*) as n FROM prompts WHERE parent_id = ?').get(id);
|
|
723
|
+
if (childCount.n > 0) throw new Error(`Prompt "${p.title}" is already a group parent`);
|
|
724
|
+
return p;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Fallback if no AI-generated title/content provided
|
|
728
|
+
if (!title) {
|
|
729
|
+
title = prompts.map(p => p.title || 'Untitled').join(' + ');
|
|
730
|
+
}
|
|
731
|
+
if (!content) {
|
|
732
|
+
const summaryLines = prompts.map(p => `- ${p.title || 'Untitled'}`).join('\n');
|
|
733
|
+
content = `Group of ${prompts.length} prompts:\n${summaryLines}`;
|
|
734
|
+
}
|
|
735
|
+
if (!contentHtml) {
|
|
736
|
+
contentHtml = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const folderId = prompts[0].folder_id;
|
|
740
|
+
const contextType = prompts[0].context_type || 'general';
|
|
741
|
+
const sortOrder = prompts[0].sort_order || 0;
|
|
742
|
+
|
|
743
|
+
const txn = getDb().transaction(() => {
|
|
744
|
+
const result = getDb().prepare(
|
|
745
|
+
`INSERT INTO prompts (title, content, content_html, folder_id, context_type, tags, sort_order)
|
|
746
|
+
VALUES (?, ?, ?, ?, ?, '[]', ?)`
|
|
747
|
+
).run(title, content, contentHtml, folderId, contextType, sortOrder);
|
|
748
|
+
const groupId = result.lastInsertRowid;
|
|
749
|
+
|
|
750
|
+
getDb().prepare(
|
|
751
|
+
`INSERT INTO prompt_versions (prompt_id, version, title, content, content_html, tags, message)
|
|
752
|
+
VALUES (?, 1, ?, ?, ?, '[]', 'Group created')`
|
|
753
|
+
).run(groupId, title, content, contentHtml);
|
|
754
|
+
|
|
755
|
+
const setParent = getDb().prepare('UPDATE prompts SET parent_id = ?, updated_at = datetime(\'now\') WHERE id = ?');
|
|
756
|
+
for (const p of prompts) {
|
|
757
|
+
setParent.run(groupId, p.id);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return groupId;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
return txn();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function deletePrompt(id) {
|
|
767
|
+
// Delete children first (group sub-prompts), then the prompt itself
|
|
768
|
+
getDb().prepare('DELETE FROM prompts WHERE parent_id = ?').run(id);
|
|
769
|
+
getDb().prepare('DELETE FROM prompts WHERE id = ?').run(id);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function duplicatePrompt(id) {
|
|
773
|
+
const existing = getDb().prepare('SELECT * FROM prompts WHERE id = ?').get(id);
|
|
774
|
+
if (!existing) throw new Error('Prompt not found');
|
|
775
|
+
|
|
776
|
+
const insertStmt = getDb().prepare(
|
|
777
|
+
`INSERT INTO prompts (title, content, content_html, folder_id, context_type, tags, is_template, template_vars, starred, pinned, sort_order, parent_id)
|
|
778
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const txn = getDb().transaction(() => {
|
|
782
|
+
const result = insertStmt.run(
|
|
783
|
+
existing.title + ' (copy)', existing.content, existing.content_html,
|
|
784
|
+
existing.folder_id, existing.context_type, existing.tags,
|
|
785
|
+
existing.is_template, existing.template_vars, 0, 0, 0,
|
|
786
|
+
existing.parent_id || null
|
|
787
|
+
);
|
|
788
|
+
const newId = result.lastInsertRowid;
|
|
789
|
+
|
|
790
|
+
// Recursively duplicate children
|
|
791
|
+
const children = listChildPrompts(id);
|
|
792
|
+
for (const child of children) {
|
|
793
|
+
insertStmt.run(
|
|
794
|
+
child.title, child.content, child.content_html,
|
|
795
|
+
child.folder_id, child.context_type, child.tags,
|
|
796
|
+
child.is_template, child.template_vars, 0, 0, child.sort_order,
|
|
797
|
+
newId
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return newId;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return txn();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function reorderPrompts(orderedIds) {
|
|
808
|
+
const stmt = getDb().prepare('UPDATE prompts SET sort_order = ? WHERE id = ?');
|
|
809
|
+
const txn = getDb().transaction(() => {
|
|
810
|
+
for (let i = 0; i < orderedIds.length; i++) {
|
|
811
|
+
stmt.run(i, orderedIds[i]);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
txn();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function reorderFolders(orderedIds) {
|
|
818
|
+
const stmt = getDb().prepare('UPDATE folders SET sort_order = ? WHERE id = ?');
|
|
819
|
+
const txn = getDb().transaction(() => {
|
|
820
|
+
for (let i = 0; i < orderedIds.length; i++) {
|
|
821
|
+
stmt.run(i, orderedIds[i]);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
txn();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function getPromptVersions(promptId) {
|
|
828
|
+
return getDb().prepare('SELECT * FROM prompt_versions WHERE prompt_id = ? ORDER BY version DESC').all(promptId);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function restorePromptVersion(promptId, versionId) {
|
|
832
|
+
const version = getDb().prepare('SELECT * FROM prompt_versions WHERE id = ? AND prompt_id = ?').get(versionId, promptId);
|
|
833
|
+
if (!version) throw new Error('Version not found');
|
|
834
|
+
updatePrompt(promptId, {
|
|
835
|
+
title: version.title, content: version.content, content_html: version.content_html,
|
|
836
|
+
tags: JSON.parse(version.tags || '[]'), version_message: `Restored from v${version.version}`
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// --- Folders CRUD ---
|
|
841
|
+
function createFolder({ name, parent_id, color }) {
|
|
842
|
+
const result = getDb().prepare(
|
|
843
|
+
'INSERT INTO folders (name, parent_id, color) VALUES (?, ?, ?)'
|
|
844
|
+
).run(name, parent_id || null, color || '#7aa2f7');
|
|
845
|
+
return result.lastInsertRowid;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function listFolders() {
|
|
849
|
+
return getDb().prepare('SELECT * FROM folders ORDER BY sort_order, name').all();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function updateFolder(id, { name, parent_id, color, sort_order }) {
|
|
853
|
+
const existing = getDb().prepare('SELECT * FROM folders WHERE id = ?').get(id);
|
|
854
|
+
if (!existing) throw new Error('Folder not found');
|
|
855
|
+
getDb().prepare('UPDATE folders SET name=?, parent_id=?, color=?, sort_order=? WHERE id=?')
|
|
856
|
+
.run(name ?? existing.name, parent_id !== undefined ? parent_id : existing.parent_id,
|
|
857
|
+
color ?? existing.color, sort_order ?? existing.sort_order, id);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function deleteFolder(id) {
|
|
861
|
+
getDb().prepare('DELETE FROM folders WHERE id = ?').run(id);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// --- Images ---
|
|
865
|
+
function saveImage(promptId, buffer, filename, mimeType) {
|
|
866
|
+
const ext = path.extname(filename) || '.png';
|
|
867
|
+
const hash = crypto.createHash('md5').update(buffer).digest('hex');
|
|
868
|
+
const storedFilename = `${hash}${ext}`;
|
|
869
|
+
const filePath = path.join(DEFAULT_IMAGES_DIR, storedFilename);
|
|
870
|
+
|
|
871
|
+
if (!fs.existsSync(filePath)) {
|
|
872
|
+
fs.writeFileSync(filePath, buffer);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Use NULL for prompt_id=0 (queue panel images not tied to a prompt) to avoid FK constraint
|
|
876
|
+
const pid = promptId && promptId > 0 ? promptId : null;
|
|
877
|
+
const result = getDb().prepare(
|
|
878
|
+
'INSERT INTO images (prompt_id, filename, mime_type, file_path) VALUES (?, ?, ?, ?)'
|
|
879
|
+
).run(pid, filename, mimeType || 'image/png', filePath);
|
|
880
|
+
return { id: result.lastInsertRowid, filename: storedFilename, path: filePath };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function getImage(id) {
|
|
884
|
+
return getDb().prepare('SELECT * FROM images WHERE id = ?').get(id);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function updateImageAnnotations(id, annotations) {
|
|
888
|
+
getDb().prepare('UPDATE images SET annotations = ? WHERE id = ?').run(JSON.stringify(annotations), id);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function listImages(promptId) {
|
|
892
|
+
return getDb().prepare('SELECT * FROM images WHERE prompt_id = ? ORDER BY created_at').all(promptId);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function deleteImage(id) {
|
|
896
|
+
const img = getDb().prepare('SELECT * FROM images WHERE id = ?').get(id);
|
|
897
|
+
if (img) {
|
|
898
|
+
try { fs.unlinkSync(img.file_path); } catch {}
|
|
899
|
+
getDb().prepare('DELETE FROM images WHERE id = ?').run(id);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// --- Chains ---
|
|
904
|
+
function createChain({ name, description }) {
|
|
905
|
+
const result = getDb().prepare('INSERT INTO chains (name, description) VALUES (?, ?)').run(name, description || '');
|
|
906
|
+
return result.lastInsertRowid;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function getChain(id) {
|
|
910
|
+
const chain = getDb().prepare('SELECT * FROM chains WHERE id = ?').get(id);
|
|
911
|
+
if (!chain) return null;
|
|
912
|
+
chain.nodes = getDb().prepare('SELECT * FROM chain_nodes WHERE chain_id = ? ORDER BY sort_order').all(id);
|
|
913
|
+
chain.edges = getDb().prepare('SELECT * FROM chain_edges WHERE chain_id = ?').all(id);
|
|
914
|
+
return chain;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function listChains() {
|
|
918
|
+
return getDb().prepare('SELECT * FROM chains ORDER BY updated_at DESC').all();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function updateChain(id, { name, description, nodes, edges }) {
|
|
922
|
+
getDb().prepare('UPDATE chains SET name=?, description=?, updated_at=datetime("now") WHERE id=?')
|
|
923
|
+
.run(name, description || '', id);
|
|
924
|
+
|
|
925
|
+
if (nodes) {
|
|
926
|
+
getDb().prepare('DELETE FROM chain_nodes WHERE chain_id = ?').run(id);
|
|
927
|
+
const insertNode = getDb().prepare(
|
|
928
|
+
'INSERT INTO chain_nodes (chain_id, prompt_id, node_type, position_x, position_y, config, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
929
|
+
);
|
|
930
|
+
for (const n of nodes) {
|
|
931
|
+
insertNode.run(id, n.prompt_id || null, n.node_type || 'prompt', n.position_x || 0, n.position_y || 0, JSON.stringify(n.config || {}), n.sort_order || 0);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (edges) {
|
|
936
|
+
getDb().prepare('DELETE FROM chain_edges WHERE chain_id = ?').run(id);
|
|
937
|
+
const insertEdge = getDb().prepare(
|
|
938
|
+
'INSERT INTO chain_edges (chain_id, from_node_id, to_node_id, condition, label) VALUES (?, ?, ?, ?, ?)'
|
|
939
|
+
);
|
|
940
|
+
for (const e of edges) {
|
|
941
|
+
insertEdge.run(id, e.from_node_id, e.to_node_id, e.condition || '', e.label || '');
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function deleteChain(id) {
|
|
947
|
+
getDb().prepare('DELETE FROM chains WHERE id = ?').run(id);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// --- Perm Rules (source of truth for Claude Code permissions) ---
|
|
951
|
+
function listPermRules({ project, listType } = {}) {
|
|
952
|
+
let sql = 'SELECT * FROM perm_rules WHERE 1=1';
|
|
953
|
+
const params = [];
|
|
954
|
+
if (project) { sql += ' AND project = ?'; params.push(project); }
|
|
955
|
+
if (listType) { sql += ' AND list_type = ?'; params.push(listType); }
|
|
956
|
+
sql += ' ORDER BY project, rule';
|
|
957
|
+
return getDb().prepare(sql).all(...params);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function addPermRule({ rule, listType, scope, project }) {
|
|
961
|
+
getDb().prepare(
|
|
962
|
+
`INSERT OR IGNORE INTO perm_rules (rule, list_type, scope, project)
|
|
963
|
+
VALUES (?, ?, ?, ?)`
|
|
964
|
+
).run(rule, listType || 'allow', scope || 'global', project || '__global__');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function removePermRule({ rule, listType, project }) {
|
|
968
|
+
getDb().prepare(
|
|
969
|
+
'DELETE FROM perm_rules WHERE rule = ? AND list_type = ? AND project = ?'
|
|
970
|
+
).run(rule, listType || 'allow', project || '__global__');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function bulkSetPermRules(rules) {
|
|
974
|
+
// rules: [{ rule, listType, scope, project }]
|
|
975
|
+
const txn = getDb().transaction(() => {
|
|
976
|
+
getDb().exec('DELETE FROM perm_rules');
|
|
977
|
+
const ins = getDb().prepare(
|
|
978
|
+
'INSERT OR IGNORE INTO perm_rules (rule, list_type, scope, project) VALUES (?, ?, ?, ?)'
|
|
979
|
+
);
|
|
980
|
+
for (const r of rules) {
|
|
981
|
+
ins.run(r.rule, r.listType || 'allow', r.scope || 'global', r.project || '__global__');
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
txn();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function getPermRulesByProject() {
|
|
988
|
+
const rows = getDb().prepare('SELECT * FROM perm_rules ORDER BY project, list_type, rule').all();
|
|
989
|
+
const result = {};
|
|
990
|
+
for (const r of rows) {
|
|
991
|
+
if (!result[r.project]) result[r.project] = { allow: [], deny: [] };
|
|
992
|
+
result[r.project][r.list_type].push(r.rule);
|
|
993
|
+
}
|
|
994
|
+
return result;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// --- Legacy Permission Rules (old table, kept for backwards compat) ---
|
|
998
|
+
function listPermissionRules() {
|
|
999
|
+
return getDb().prepare('SELECT * FROM permission_rules ORDER BY tool_name, pattern').all();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function upsertPermissionRule({ tool_name, pattern, action, project_path }) {
|
|
1003
|
+
getDb().prepare(
|
|
1004
|
+
`INSERT INTO permission_rules (tool_name, pattern, action, project_path)
|
|
1005
|
+
VALUES (?, ?, ?, ?)
|
|
1006
|
+
ON CONFLICT(tool_name, pattern, project_path) DO UPDATE SET action = excluded.action`
|
|
1007
|
+
).run(tool_name, pattern || '*', action || 'ask', project_path || '*');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function deletePermissionRule(id) {
|
|
1011
|
+
getDb().prepare('DELETE FROM permission_rules WHERE id = ?').run(id);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function listPermissionLog({ session_id, tool_name, limit } = {}) {
|
|
1015
|
+
let sql = 'SELECT * FROM permission_log WHERE 1=1';
|
|
1016
|
+
const params = [];
|
|
1017
|
+
if (session_id) { sql += ' AND session_id = ?'; params.push(session_id); }
|
|
1018
|
+
if (tool_name) { sql += ' AND tool_name = ?'; params.push(tool_name); }
|
|
1019
|
+
sql += ' ORDER BY timestamp DESC';
|
|
1020
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
1021
|
+
return getDb().prepare(sql).all(...params);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function addPermissionLog({ session_id, tool_name, action_taken, details }) {
|
|
1025
|
+
getDb().prepare(
|
|
1026
|
+
'INSERT INTO permission_log (session_id, tool_name, action_taken, details) VALUES (?, ?, ?, ?)'
|
|
1027
|
+
).run(session_id || '', tool_name, action_taken || '', details || '');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// --- Session Titles ---
|
|
1031
|
+
function getSessionTitle(sessionId) {
|
|
1032
|
+
const row = getDb().prepare('SELECT title, user_renamed FROM session_titles WHERE session_id = ?').get(sessionId);
|
|
1033
|
+
return row || null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function setSessionTitle(sessionId, title, userRenamed = false) {
|
|
1037
|
+
getDb().prepare(
|
|
1038
|
+
`INSERT INTO session_titles (session_id, title, user_renamed) VALUES (?, ?, ?)
|
|
1039
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1040
|
+
title = CASE WHEN session_titles.user_renamed AND NOT excluded.user_renamed THEN session_titles.title ELSE excluded.title END,
|
|
1041
|
+
user_renamed = CASE WHEN excluded.user_renamed THEN 1 ELSE session_titles.user_renamed END,
|
|
1042
|
+
generated_at = datetime('now')`
|
|
1043
|
+
).run(sessionId, title, userRenamed ? 1 : 0);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function isSessionUserRenamed(sessionId) {
|
|
1047
|
+
const row = getDb().prepare('SELECT user_renamed FROM session_titles WHERE session_id = ?').get(sessionId);
|
|
1048
|
+
return row ? !!row.user_renamed : false;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function getAllSessionTitles() {
|
|
1052
|
+
const rows = getDb().prepare('SELECT session_id, title, user_renamed FROM session_titles').all();
|
|
1053
|
+
const map = {};
|
|
1054
|
+
for (const r of rows) map[r.session_id] = { title: r.title, userRenamed: !!r.user_renamed };
|
|
1055
|
+
return map;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
// --- Always Ask (uses perm_rules.always_ask column) ---
|
|
1060
|
+
function getAlwaysAskTools() {
|
|
1061
|
+
return getDb().prepare('SELECT DISTINCT rule FROM perm_rules WHERE always_ask = 1').all()
|
|
1062
|
+
.map(r => r.rule);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function setAlwaysAsk(toolName, alwaysAsk) {
|
|
1066
|
+
if (alwaysAsk) {
|
|
1067
|
+
// Set always_ask=1 on all matching rules across all projects
|
|
1068
|
+
const updated = getDb().prepare(
|
|
1069
|
+
`UPDATE perm_rules SET always_ask = 1 WHERE rule = ?`
|
|
1070
|
+
).run(toolName);
|
|
1071
|
+
// If no matching rule exists, insert as global allow with always_ask
|
|
1072
|
+
if (updated.changes === 0) {
|
|
1073
|
+
getDb().prepare(
|
|
1074
|
+
`INSERT OR IGNORE INTO perm_rules (rule, list_type, scope, project, always_ask)
|
|
1075
|
+
VALUES (?, 'allow', 'global', '__global__', 1)`
|
|
1076
|
+
).run(toolName);
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
getDb().prepare(
|
|
1080
|
+
`UPDATE perm_rules SET always_ask = 0 WHERE rule = ?`
|
|
1081
|
+
).run(toolName);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// --- Session Conversations ---
|
|
1086
|
+
function importSessionConversation({ session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at }) {
|
|
1087
|
+
getDb().prepare(
|
|
1088
|
+
`INSERT INTO session_conversations (session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at)
|
|
1089
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1090
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1091
|
+
messages=excluded.messages, user_msg_count=excluded.user_msg_count,
|
|
1092
|
+
assistant_msg_count=excluded.assistant_msg_count, title=excluded.title,
|
|
1093
|
+
first_message=excluded.first_message, git_branch=excluded.git_branch,
|
|
1094
|
+
file_size=excluded.file_size, imported_at=datetime('now')`
|
|
1095
|
+
).run(
|
|
1096
|
+
session_id, project_path || '', JSON.stringify(messages || []),
|
|
1097
|
+
user_msg_count || 0, assistant_msg_count || 0, title || '',
|
|
1098
|
+
first_message || '', git_branch || '', file_size || 0, session_created_at || ''
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function listSessionConversations({ search, limit, offset } = {}) {
|
|
1103
|
+
let sql = 'SELECT id, session_id, project_path, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at, imported_at FROM session_conversations WHERE 1=1';
|
|
1104
|
+
const params = [];
|
|
1105
|
+
if (search) {
|
|
1106
|
+
sql += ' AND (title LIKE ? OR first_message LIKE ? OR project_path LIKE ?)';
|
|
1107
|
+
const q = `%${search}%`;
|
|
1108
|
+
params.push(q, q, q);
|
|
1109
|
+
}
|
|
1110
|
+
sql += ' ORDER BY imported_at DESC';
|
|
1111
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
1112
|
+
if (offset) { sql += ' OFFSET ?'; params.push(offset); }
|
|
1113
|
+
return getDb().prepare(sql).all(...params);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function getSessionConversation(sessionId) {
|
|
1117
|
+
return getDb().prepare('SELECT * FROM session_conversations WHERE session_id = ?').get(sessionId);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// --- Templates ---
|
|
1121
|
+
function createTemplate({ name, description, content, content_html, variables, category }) {
|
|
1122
|
+
const result = getDb().prepare(
|
|
1123
|
+
'INSERT INTO templates (name, description, content, content_html, variables, category) VALUES (?, ?, ?, ?, ?, ?)'
|
|
1124
|
+
).run(name, description || '', content || '', content_html || '', JSON.stringify(variables || []), category || 'general');
|
|
1125
|
+
return result.lastInsertRowid;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function listTemplates() {
|
|
1129
|
+
return getDb().prepare('SELECT * FROM templates ORDER BY category, name').all();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function getTemplate(id) {
|
|
1133
|
+
return getDb().prepare('SELECT * FROM templates WHERE id = ?').get(id);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function deleteTemplate(id) {
|
|
1137
|
+
getDb().prepare('DELETE FROM templates WHERE id = ?').run(id);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// --- Prompt Usage ---
|
|
1141
|
+
function trackPromptUsage({ prompt_id, session_id, result, effectiveness_score }) {
|
|
1142
|
+
getDb().prepare(
|
|
1143
|
+
'INSERT INTO prompt_usage (prompt_id, session_id, result, effectiveness_score) VALUES (?, ?, ?, ?)'
|
|
1144
|
+
).run(prompt_id, session_id || '', result || '', effectiveness_score || null);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function getPromptUsageStats(promptId) {
|
|
1148
|
+
return getDb().prepare(
|
|
1149
|
+
`SELECT COUNT(*) as use_count, AVG(effectiveness_score) as avg_score,
|
|
1150
|
+
MAX(used_at) as last_used FROM prompt_usage WHERE prompt_id = ?`
|
|
1151
|
+
).get(promptId);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// --- Backup & Recovery ---
|
|
1155
|
+
function checkpointWal() {
|
|
1156
|
+
if (!db) return;
|
|
1157
|
+
try {
|
|
1158
|
+
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
1159
|
+
} catch {}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function createBackup(label) {
|
|
1163
|
+
if (!db || !currentDbPath) throw new Error('Database not initialized');
|
|
1164
|
+
checkpointWal();
|
|
1165
|
+
|
|
1166
|
+
const now = new Date();
|
|
1167
|
+
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1168
|
+
const tag = label ? `-${label.replace(/[^a-zA-Z0-9_-]/g, '')}` : '';
|
|
1169
|
+
const backupName = `task-manager-${ts}${tag}.db`;
|
|
1170
|
+
const backupPath = path.join(BACKUP_DIR, backupName);
|
|
1171
|
+
|
|
1172
|
+
// Use SQLite backup API via better-sqlite3
|
|
1173
|
+
db.backup(backupPath).then(() => {}).catch(() => {
|
|
1174
|
+
// Fallback: file copy
|
|
1175
|
+
fs.copyFileSync(currentDbPath, backupPath);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Also copy images dir as a tarball if it has content
|
|
1179
|
+
const imagesBackup = path.join(BACKUP_DIR, `images-${ts}${tag}.tar.gz`);
|
|
1180
|
+
try {
|
|
1181
|
+
|
|
1182
|
+
const imageFiles = fs.readdirSync(DEFAULT_IMAGES_DIR);
|
|
1183
|
+
if (imageFiles.length > 0) {
|
|
1184
|
+
require('child_process').spawnSync('tar', ['-czf', imagesBackup, '-C', path.dirname(DEFAULT_IMAGES_DIR), path.basename(DEFAULT_IMAGES_DIR)], { timeout: 30000 });
|
|
1185
|
+
}
|
|
1186
|
+
} catch {}
|
|
1187
|
+
|
|
1188
|
+
cleanOldBackups();
|
|
1189
|
+
return { backupName, backupPath, timestamp: now.toISOString() };
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function createBackupSync(label) {
|
|
1193
|
+
if (!db || !currentDbPath) throw new Error('Database not initialized');
|
|
1194
|
+
checkpointWal();
|
|
1195
|
+
|
|
1196
|
+
const now = new Date();
|
|
1197
|
+
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1198
|
+
const tag = label ? `-${label.replace(/[^a-zA-Z0-9_-]/g, '')}` : '';
|
|
1199
|
+
const backupName = `task-manager-${ts}${tag}.db`;
|
|
1200
|
+
const backupPath = path.join(BACKUP_DIR, backupName);
|
|
1201
|
+
|
|
1202
|
+
fs.copyFileSync(currentDbPath, backupPath);
|
|
1203
|
+
|
|
1204
|
+
// Also copy images
|
|
1205
|
+
const imagesBackup = path.join(BACKUP_DIR, `images-${ts}${tag}.tar.gz`);
|
|
1206
|
+
try {
|
|
1207
|
+
|
|
1208
|
+
const imageFiles = fs.readdirSync(DEFAULT_IMAGES_DIR);
|
|
1209
|
+
if (imageFiles.length > 0) {
|
|
1210
|
+
require('child_process').spawnSync('tar', ['-czf', imagesBackup, '-C', path.dirname(DEFAULT_IMAGES_DIR), path.basename(DEFAULT_IMAGES_DIR)], { timeout: 30000 });
|
|
1211
|
+
}
|
|
1212
|
+
} catch {}
|
|
1213
|
+
|
|
1214
|
+
cleanOldBackups();
|
|
1215
|
+
return { backupName, backupPath, timestamp: now.toISOString() };
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function listBackups() {
|
|
1219
|
+
if (!fs.existsSync(BACKUP_DIR)) return [];
|
|
1220
|
+
const files = fs.readdirSync(BACKUP_DIR)
|
|
1221
|
+
.filter(f => f.startsWith('task-manager-') && f.endsWith('.db'))
|
|
1222
|
+
.sort()
|
|
1223
|
+
.reverse();
|
|
1224
|
+
|
|
1225
|
+
return files.map(f => {
|
|
1226
|
+
const stat = fs.statSync(path.join(BACKUP_DIR, f));
|
|
1227
|
+
// Check for matching images archive
|
|
1228
|
+
const imagesFile = f.replace('task-manager-', 'images-').replace('.db', '.tar.gz');
|
|
1229
|
+
const hasImages = fs.existsSync(path.join(BACKUP_DIR, imagesFile));
|
|
1230
|
+
return {
|
|
1231
|
+
name: f,
|
|
1232
|
+
path: path.join(BACKUP_DIR, f),
|
|
1233
|
+
size: stat.size,
|
|
1234
|
+
createdAt: stat.mtime.toISOString(),
|
|
1235
|
+
hasImages,
|
|
1236
|
+
imagesFile: hasImages ? imagesFile : null,
|
|
1237
|
+
};
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function restoreBackup(backupName) {
|
|
1242
|
+
const backupPath = path.join(BACKUP_DIR, path.basename(backupName));
|
|
1243
|
+
if (!fs.existsSync(backupPath)) throw new Error('Backup not found');
|
|
1244
|
+
if (!backupName.endsWith('.db')) throw new Error('Invalid backup file');
|
|
1245
|
+
|
|
1246
|
+
// Create a safety backup of current state before restoring
|
|
1247
|
+
createBackupSync('pre-restore');
|
|
1248
|
+
|
|
1249
|
+
// Close current DB
|
|
1250
|
+
checkpointWal();
|
|
1251
|
+
if (db) { db.close(); db = null; }
|
|
1252
|
+
|
|
1253
|
+
// Copy backup over current DB
|
|
1254
|
+
fs.copyFileSync(backupPath, currentDbPath);
|
|
1255
|
+
|
|
1256
|
+
// Restore images if archive exists
|
|
1257
|
+
const imagesFile = backupName.replace('task-manager-', 'images-').replace('.db', '.tar.gz');
|
|
1258
|
+
const imagesArchive = path.join(BACKUP_DIR, imagesFile);
|
|
1259
|
+
if (fs.existsSync(imagesArchive)) {
|
|
1260
|
+
try {
|
|
1261
|
+
require('child_process').spawnSync('tar', ['-xzf', imagesArchive, '-C', path.dirname(DEFAULT_IMAGES_DIR)], { timeout: 30000 });
|
|
1262
|
+
} catch {}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Re-open DB
|
|
1266
|
+
db = new Database(currentDbPath);
|
|
1267
|
+
db.pragma('journal_mode = WAL');
|
|
1268
|
+
db.pragma('busy_timeout = 5000');
|
|
1269
|
+
db.pragma('foreign_keys = ON');
|
|
1270
|
+
|
|
1271
|
+
return { ok: true, restored: backupName };
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function deleteBackup(backupName) {
|
|
1275
|
+
const backupPath = path.join(BACKUP_DIR, path.basename(backupName));
|
|
1276
|
+
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
|
|
1277
|
+
// Also delete images archive
|
|
1278
|
+
const imagesFile = backupName.replace('task-manager-', 'images-').replace('.db', '.tar.gz');
|
|
1279
|
+
const imagesPath = path.join(BACKUP_DIR, imagesFile);
|
|
1280
|
+
if (fs.existsSync(imagesPath)) fs.unlinkSync(imagesPath);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function cleanOldBackups(maxAge = 30) {
|
|
1284
|
+
// Keep backups for maxAge days, but always keep at least 5
|
|
1285
|
+
const backups = listBackups();
|
|
1286
|
+
if (backups.length <= 5) return;
|
|
1287
|
+
|
|
1288
|
+
const cutoff = Date.now() - (maxAge * 24 * 60 * 60 * 1000);
|
|
1289
|
+
const toDelete = backups.slice(5).filter(b => new Date(b.createdAt).getTime() < cutoff);
|
|
1290
|
+
for (const b of toDelete) {
|
|
1291
|
+
deleteBackup(b.name);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function startDailyBackup() {
|
|
1296
|
+
// Run backup immediately if none today
|
|
1297
|
+
const backups = listBackups();
|
|
1298
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1299
|
+
const hasToday = backups.some(b => b.createdAt.startsWith(today));
|
|
1300
|
+
if (!hasToday && db) {
|
|
1301
|
+
try { createBackupSync('daily'); } catch (e) { console.error('Daily backup failed:', e.message); }
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Schedule daily check every hour
|
|
1305
|
+
backupIntervalId = setInterval(() => {
|
|
1306
|
+
const bks = listBackups();
|
|
1307
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
1308
|
+
const hasBk = bks.some(b => b.createdAt.startsWith(day));
|
|
1309
|
+
if (!hasBk && db) {
|
|
1310
|
+
try { createBackupSync('daily'); } catch (e) { console.error('Daily backup failed:', e.message); }
|
|
1311
|
+
}
|
|
1312
|
+
}, 60 * 60 * 1000); // check every hour
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function closeDb() {
|
|
1316
|
+
if (backupIntervalId) { clearInterval(backupIntervalId); backupIntervalId = null; }
|
|
1317
|
+
if (db) {
|
|
1318
|
+
checkpointWal();
|
|
1319
|
+
db.close();
|
|
1320
|
+
db = null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// --- Prompt Queues CRUD ---
|
|
1325
|
+
|
|
1326
|
+
function saveQueue(sessionId, { mode, status, currentIndex, items, idleTimeoutMs }) {
|
|
1327
|
+
getDb().prepare(
|
|
1328
|
+
`INSERT INTO prompt_queues (session_id, mode, status, current_index, idle_timeout_ms, items, updated_at)
|
|
1329
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
1330
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1331
|
+
mode = excluded.mode, status = excluded.status, current_index = excluded.current_index,
|
|
1332
|
+
idle_timeout_ms = excluded.idle_timeout_ms, items = excluded.items, updated_at = excluded.updated_at`
|
|
1333
|
+
).run(sessionId, mode, status, currentIndex, idleTimeoutMs, JSON.stringify(items));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function loadQueue(sessionId) {
|
|
1337
|
+
const row = getDb().prepare('SELECT * FROM prompt_queues WHERE session_id = ?').get(sessionId);
|
|
1338
|
+
if (!row) return null;
|
|
1339
|
+
return {
|
|
1340
|
+
sessionId: row.session_id,
|
|
1341
|
+
mode: row.mode,
|
|
1342
|
+
status: row.status,
|
|
1343
|
+
currentIndex: row.current_index,
|
|
1344
|
+
items: JSON.parse(row.items || '[]'),
|
|
1345
|
+
idleTimeoutMs: row.idle_timeout_ms,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function loadAllQueues() {
|
|
1350
|
+
const rows = getDb().prepare('SELECT * FROM prompt_queues WHERE status != ?').all('done');
|
|
1351
|
+
return rows.map(row => ({
|
|
1352
|
+
sessionId: row.session_id,
|
|
1353
|
+
mode: row.mode,
|
|
1354
|
+
status: row.status,
|
|
1355
|
+
currentIndex: row.current_index,
|
|
1356
|
+
items: JSON.parse(row.items || '[]'),
|
|
1357
|
+
idleTimeoutMs: row.idle_timeout_ms,
|
|
1358
|
+
}));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function deleteQueueDb(sessionId) {
|
|
1362
|
+
getDb().prepare('DELETE FROM prompt_queues WHERE session_id = ?').run(sessionId);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// --- Auto Approvals ---
|
|
1366
|
+
function listAutoApprovals() {
|
|
1367
|
+
return getDb().prepare('SELECT * FROM auto_approvals ORDER BY category, label').all();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function upsertAutoApproval({ pattern, response, label, category, enabled, occurrences }) {
|
|
1371
|
+
getDb().prepare(
|
|
1372
|
+
`INSERT INTO auto_approvals (pattern, response, label, category, enabled, occurrences)
|
|
1373
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1374
|
+
ON CONFLICT(pattern) DO UPDATE SET
|
|
1375
|
+
response = excluded.response,
|
|
1376
|
+
label = excluded.label,
|
|
1377
|
+
category = excluded.category,
|
|
1378
|
+
enabled = CASE WHEN excluded.enabled IS NOT NULL THEN excluded.enabled ELSE auto_approvals.enabled END,
|
|
1379
|
+
occurrences = CASE WHEN excluded.occurrences > 0 THEN excluded.occurrences ELSE auto_approvals.occurrences END`
|
|
1380
|
+
).run(pattern, response || 'yes', label || '', category || '', enabled ?? 1, occurrences || 0);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function toggleAutoApproval(id, enabled) {
|
|
1384
|
+
getDb().prepare('UPDATE auto_approvals SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, id);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function deleteAutoApproval(id) {
|
|
1388
|
+
getDb().prepare('DELETE FROM auto_approvals WHERE id = ?').run(id);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function getEnabledAutoApprovals() {
|
|
1392
|
+
return getDb().prepare('SELECT * FROM auto_approvals WHERE enabled = 1').all();
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// --- Approval Rules (learned by AI agent) ---
|
|
1396
|
+
function listApprovalRules() {
|
|
1397
|
+
return getDb().prepare('SELECT * FROM approval_rules ORDER BY times_matched DESC, label').all();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function upsertApprovalRule({ pattern, label, description, category, riskLevel, enabled }) {
|
|
1401
|
+
getDb().prepare(
|
|
1402
|
+
`INSERT INTO approval_rules (pattern, label, description, category, risk_level, enabled)
|
|
1403
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1404
|
+
ON CONFLICT(pattern) DO UPDATE SET
|
|
1405
|
+
label = excluded.label,
|
|
1406
|
+
description = CASE WHEN excluded.description != '' THEN excluded.description ELSE approval_rules.description END,
|
|
1407
|
+
category = CASE WHEN excluded.category != '' THEN excluded.category ELSE approval_rules.category END,
|
|
1408
|
+
risk_level = excluded.risk_level,
|
|
1409
|
+
times_matched = approval_rules.times_matched + 1,
|
|
1410
|
+
updated_at = datetime('now')`
|
|
1411
|
+
).run(pattern, label || '', description || '', category || '', riskLevel || 'low', enabled ? 1 : 0);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function toggleApprovalRule(id, enabled) {
|
|
1415
|
+
getDb().prepare('UPDATE approval_rules SET enabled = ?, updated_at = datetime("now") WHERE id = ?').run(enabled ? 1 : 0, id);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function deleteApprovalRule(id) {
|
|
1419
|
+
getDb().prepare('DELETE FROM approval_rules WHERE id = ?').run(id);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function incrementApprovalRuleMatch(id) {
|
|
1423
|
+
getDb().prepare('UPDATE approval_rules SET times_matched = times_matched + 1, updated_at = datetime("now") WHERE id = ?').run(id);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// --- Approval Decisions (audit log) ---
|
|
1427
|
+
function addApprovalDecision({ sessionId, toolName, commandSummary, fullContext, warning, decision, reasoning, decidedBy, ruleId, riskLevel }) {
|
|
1428
|
+
const result = getDb().prepare(
|
|
1429
|
+
`INSERT INTO approval_decisions (session_id, tool_name, command_summary, full_context, warning_text, decision, reasoning, decided_by, rule_id, risk_level)
|
|
1430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1431
|
+
).run(sessionId || '', toolName || '', commandSummary || '', fullContext || '', warning || '', decision, reasoning || '', decidedBy || 'ai', ruleId || null, riskLevel || 'low');
|
|
1432
|
+
return result.lastInsertRowid;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function listApprovalDecisions({ limit, sessionId } = {}) {
|
|
1436
|
+
let sql = 'SELECT * FROM approval_decisions WHERE 1=1';
|
|
1437
|
+
const params = [];
|
|
1438
|
+
if (sessionId) { sql += ' AND session_id = ?'; params.push(sessionId); }
|
|
1439
|
+
sql += ' ORDER BY created_at DESC';
|
|
1440
|
+
if (limit) { sql += ' LIMIT ?'; params.push(limit); }
|
|
1441
|
+
return getDb().prepare(sql).all(...params);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function resolveApprovalDecision(id, decision) {
|
|
1445
|
+
getDb().prepare('UPDATE approval_decisions SET decision = ?, decided_by = ? WHERE id = ?')
|
|
1446
|
+
.run(decision, 'user', id);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function getPendingEscalations(sessionId) {
|
|
1450
|
+
let sql = "SELECT * FROM approval_decisions WHERE decision = 'escalated'";
|
|
1451
|
+
const params = [];
|
|
1452
|
+
if (sessionId) { sql += ' AND session_id = ?'; params.push(sessionId); }
|
|
1453
|
+
sql += ' ORDER BY created_at DESC';
|
|
1454
|
+
return getDb().prepare(sql).all(...params);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// --- Code Reviews CRUD ---
|
|
1458
|
+
function createReview({ session_id, project_path, base_ref }) {
|
|
1459
|
+
const result = getDb().prepare(
|
|
1460
|
+
`INSERT INTO code_reviews (session_id, project_path, base_ref) VALUES (?, ?, ?)`
|
|
1461
|
+
).run(session_id || null, project_path, base_ref || '');
|
|
1462
|
+
return result.lastInsertRowid;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function getReview(id) {
|
|
1466
|
+
const review = getDb().prepare('SELECT * FROM code_reviews WHERE id = ?').get(id);
|
|
1467
|
+
if (!review) return null;
|
|
1468
|
+
review.comments = getDb().prepare('SELECT * FROM review_comments WHERE review_id = ? ORDER BY file_path, line_start').all(id);
|
|
1469
|
+
return review;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function listReviews({ project_path, session_id, limit = 50 } = {}) {
|
|
1473
|
+
let sql = 'SELECT * FROM code_reviews WHERE 1=1';
|
|
1474
|
+
const params = [];
|
|
1475
|
+
if (project_path) { sql += ' AND project_path = ?'; params.push(project_path); }
|
|
1476
|
+
if (session_id) { sql += ' AND session_id = ?'; params.push(session_id); }
|
|
1477
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
1478
|
+
params.push(limit);
|
|
1479
|
+
return getDb().prepare(sql).all(...params);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function updateReview(id, { status, summary, file_count, base_ref }) {
|
|
1483
|
+
const sets = ["updated_at = datetime('now')"];
|
|
1484
|
+
const params = [];
|
|
1485
|
+
if (status !== undefined) {
|
|
1486
|
+
sets.push('status = ?'); params.push(status);
|
|
1487
|
+
if (status === 'submitted') { sets.push("submitted_at = datetime('now')"); }
|
|
1488
|
+
}
|
|
1489
|
+
if (summary !== undefined) { sets.push('summary = ?'); params.push(summary); }
|
|
1490
|
+
if (file_count !== undefined) { sets.push('file_count = ?'); params.push(file_count); }
|
|
1491
|
+
if (base_ref !== undefined) { sets.push('base_ref = ?'); params.push(base_ref); }
|
|
1492
|
+
params.push(id);
|
|
1493
|
+
getDb().prepare(`UPDATE code_reviews SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function deleteReview(id) {
|
|
1497
|
+
getDb().prepare('DELETE FROM code_reviews WHERE id = ?').run(id);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function addReviewComment({ review_id, file_path, line_start, line_end, side, body, severity, ai_generated }) {
|
|
1501
|
+
const result = getDb().prepare(
|
|
1502
|
+
`INSERT INTO review_comments (review_id, file_path, line_start, line_end, side, body, severity, ai_generated)
|
|
1503
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1504
|
+
).run(review_id, file_path, line_start, line_end || line_start, side || 'new', body, severity || 'comment', ai_generated ? 1 : 0);
|
|
1505
|
+
// Update comment count
|
|
1506
|
+
const count = getDb().prepare('SELECT COUNT(*) as n FROM review_comments WHERE review_id = ?').get(review_id);
|
|
1507
|
+
getDb().prepare('UPDATE code_reviews SET comment_count = ? WHERE id = ?').run(count.n, review_id);
|
|
1508
|
+
return result.lastInsertRowid;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function updateReviewComment(id, { body, severity, status }) {
|
|
1512
|
+
const sets = [];
|
|
1513
|
+
const params = [];
|
|
1514
|
+
if (body !== undefined) { sets.push('body = ?'); params.push(body); }
|
|
1515
|
+
if (severity !== undefined) { sets.push('severity = ?'); params.push(severity); }
|
|
1516
|
+
if (status !== undefined) { sets.push('status = ?'); params.push(status); }
|
|
1517
|
+
if (sets.length === 0) return;
|
|
1518
|
+
params.push(id);
|
|
1519
|
+
getDb().prepare(`UPDATE review_comments SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function deleteReviewComment(id) {
|
|
1523
|
+
const comment = getDb().prepare('SELECT review_id FROM review_comments WHERE id = ?').get(id);
|
|
1524
|
+
getDb().prepare('DELETE FROM review_comments WHERE id = ?').run(id);
|
|
1525
|
+
if (comment) {
|
|
1526
|
+
const count = getDb().prepare('SELECT COUNT(*) as n FROM review_comments WHERE review_id = ?').get(comment.review_id);
|
|
1527
|
+
getDb().prepare('UPDATE code_reviews SET comment_count = ? WHERE id = ?').run(count.n, comment.review_id);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// --- Session Analyses (Insights) ---
|
|
1532
|
+
function upsertSessionAnalysis(data) {
|
|
1533
|
+
getDb().prepare(`
|
|
1534
|
+
INSERT INTO session_analyses (session_id, project, title, category, topics, skills_used, complexity, summary, pattern, first_message, session_modified_at, analyzed_at, token_count, message_count, prompt_efficiency)
|
|
1535
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, ?)
|
|
1536
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1537
|
+
project=excluded.project, title=excluded.title, category=excluded.category,
|
|
1538
|
+
topics=excluded.topics, skills_used=excluded.skills_used, complexity=excluded.complexity,
|
|
1539
|
+
summary=excluded.summary, pattern=excluded.pattern, first_message=excluded.first_message,
|
|
1540
|
+
session_modified_at=excluded.session_modified_at, analyzed_at=excluded.analyzed_at,
|
|
1541
|
+
token_count=excluded.token_count, message_count=excluded.message_count,
|
|
1542
|
+
prompt_efficiency=excluded.prompt_efficiency
|
|
1543
|
+
`).run(
|
|
1544
|
+
data.session_id, data.project || '', data.title || '', data.category || '',
|
|
1545
|
+
JSON.stringify(data.topics || []), JSON.stringify(data.skills_used || []),
|
|
1546
|
+
data.complexity || '', data.summary || '', data.pattern || '',
|
|
1547
|
+
data.first_message || '', data.session_modified_at || '',
|
|
1548
|
+
data.token_count || 0, data.message_count || 0,
|
|
1549
|
+
JSON.stringify(data.prompt_efficiency || {})
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function getSessionAnalysis(sessionId) {
|
|
1554
|
+
return getDb().prepare('SELECT * FROM session_analyses WHERE session_id = ?').get(sessionId);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function getAllSessionAnalyses() {
|
|
1558
|
+
return getDb().prepare('SELECT * FROM session_analyses ORDER BY analyzed_at DESC').all();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function getSessionAnalysisCount() {
|
|
1562
|
+
return getDb().prepare('SELECT COUNT(*) as count FROM session_analyses').get().count;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function deleteSessionAnalysis(sessionId) {
|
|
1566
|
+
getDb().prepare('DELETE FROM session_analyses WHERE session_id = ?').run(sessionId);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function deleteStaleAnalyses(validSessionIds) {
|
|
1570
|
+
const existing = getDb().prepare('SELECT session_id FROM session_analyses').all().map(r => r.session_id);
|
|
1571
|
+
const validSet = new Set(validSessionIds);
|
|
1572
|
+
let removed = 0;
|
|
1573
|
+
for (const id of existing) {
|
|
1574
|
+
if (!validSet.has(id)) {
|
|
1575
|
+
getDb().prepare('DELETE FROM session_analyses WHERE session_id = ?').run(id);
|
|
1576
|
+
removed++;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return removed;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// --- Insight Groups ---
|
|
1583
|
+
function replaceInsightGroups(groups) {
|
|
1584
|
+
getDb().prepare('DELETE FROM insight_groups').run();
|
|
1585
|
+
const ins = getDb().prepare(`
|
|
1586
|
+
INSERT INTO insight_groups (name, description, category, is_internal, session_ids, session_count, generated_at)
|
|
1587
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
1588
|
+
`);
|
|
1589
|
+
for (const g of groups) {
|
|
1590
|
+
ins.run(g.name, g.description || '', g.category || '', g.is_internal ? 1 : 0,
|
|
1591
|
+
JSON.stringify(g.session_ids || []), g.count || g.session_count || 0);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function listInsightGroups(includeInternal) {
|
|
1596
|
+
if (includeInternal) return getDb().prepare('SELECT * FROM insight_groups ORDER BY session_count DESC').all();
|
|
1597
|
+
return getDb().prepare('SELECT * FROM insight_groups WHERE is_internal = 0 ORDER BY session_count DESC').all();
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// --- Insight Skills ---
|
|
1601
|
+
function replaceInsightSkills(skills) {
|
|
1602
|
+
getDb().prepare('DELETE FROM insight_skills').run();
|
|
1603
|
+
const ins = getDb().prepare(`
|
|
1604
|
+
INSERT INTO insight_skills (name, title, description, trigger_pattern, category, is_internal, based_on, priority, example_sessions, generated_at)
|
|
1605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
1606
|
+
`);
|
|
1607
|
+
for (const s of skills) {
|
|
1608
|
+
ins.run(s.name, s.title || '', s.description || '', s.trigger || s.trigger_pattern || '',
|
|
1609
|
+
s.category || '', s.is_internal ? 1 : 0, JSON.stringify(s.based_on || []),
|
|
1610
|
+
s.priority || 'low', JSON.stringify(s.example_sessions || []));
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function listInsightSkills(includeInternal) {
|
|
1615
|
+
if (includeInternal) return getDb().prepare('SELECT * FROM insight_skills ORDER BY priority DESC').all();
|
|
1616
|
+
return getDb().prepare('SELECT * FROM insight_skills WHERE is_internal = 0 ORDER BY priority DESC').all();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// --- Insight Recommendations ---
|
|
1620
|
+
function replaceInsightRecommendations(recs) {
|
|
1621
|
+
getDb().prepare('DELETE FROM insight_recommendations').run();
|
|
1622
|
+
const ins = getDb().prepare(`
|
|
1623
|
+
INSERT INTO insight_recommendations (type, category, title, description, evidence, impact, actionable, generated_at)
|
|
1624
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
1625
|
+
`);
|
|
1626
|
+
for (const r of recs) {
|
|
1627
|
+
ins.run(r.type, r.category || '', r.title, r.description || '',
|
|
1628
|
+
JSON.stringify(r.evidence || {}), r.impact || 'medium', r.actionable || '');
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function listInsightRecommendations() {
|
|
1633
|
+
return getDb().prepare('SELECT * FROM insight_recommendations ORDER BY impact DESC').all();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// --- Analysis Runs ---
|
|
1637
|
+
function startAnalysisRun() {
|
|
1638
|
+
return getDb().prepare(`INSERT INTO analysis_runs (status) VALUES ('running')`).run().lastInsertRowid;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function completeAnalysisRun(id, { sessions_analyzed, sessions_skipped, status }) {
|
|
1642
|
+
getDb().prepare(`UPDATE analysis_runs SET completed_at = datetime('now'), sessions_analyzed = ?, sessions_skipped = ?, status = ? WHERE id = ?`)
|
|
1643
|
+
.run(sessions_analyzed || 0, sessions_skipped || 0, status || 'completed', id);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function getLastAnalysisRun() {
|
|
1647
|
+
return getDb().prepare('SELECT * FROM analysis_runs ORDER BY id DESC LIMIT 1').get();
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// --- Full Insights Data (for API) ---
|
|
1651
|
+
function getInsightsData(includeInternal) {
|
|
1652
|
+
const groups = listInsightGroups(includeInternal).map(g => ({
|
|
1653
|
+
...g, session_ids: JSON.parse(g.session_ids || '[]'),
|
|
1654
|
+
topics: undefined, is_internal: !!g.is_internal,
|
|
1655
|
+
}));
|
|
1656
|
+
const skills = listInsightSkills(includeInternal).map(s => ({
|
|
1657
|
+
...s, based_on: JSON.parse(s.based_on || '[]'),
|
|
1658
|
+
example_sessions: JSON.parse(s.example_sessions || '[]'),
|
|
1659
|
+
is_internal: !!s.is_internal,
|
|
1660
|
+
}));
|
|
1661
|
+
const recommendations = listInsightRecommendations().map(r => ({
|
|
1662
|
+
...r, evidence: JSON.parse(r.evidence || '{}'),
|
|
1663
|
+
}));
|
|
1664
|
+
const sessionCount = getSessionAnalysisCount();
|
|
1665
|
+
const lastRun = getLastAnalysisRun();
|
|
1666
|
+
|
|
1667
|
+
// Compute summary stats
|
|
1668
|
+
const analyses = getAllSessionAnalyses();
|
|
1669
|
+
const totalMessages = analyses.reduce((sum, a) => sum + (a.message_count || 0), 0);
|
|
1670
|
+
const avgMessages = sessionCount > 0 ? (totalMessages / sessionCount).toFixed(1) : 0;
|
|
1671
|
+
const categories = {};
|
|
1672
|
+
for (const a of analyses) {
|
|
1673
|
+
categories[a.category] = (categories[a.category] || 0) + 1;
|
|
1674
|
+
}
|
|
1675
|
+
const topCategory = Object.entries(categories).sort((a, b) => b[1] - a[1])[0];
|
|
1676
|
+
|
|
1677
|
+
return {
|
|
1678
|
+
sessionCount,
|
|
1679
|
+
stats: {
|
|
1680
|
+
avgMessages: parseFloat(avgMessages),
|
|
1681
|
+
totalMessages,
|
|
1682
|
+
topCategory: topCategory ? { name: topCategory[0], count: topCategory[1] } : null,
|
|
1683
|
+
},
|
|
1684
|
+
groups, skills, recommendations,
|
|
1685
|
+
updatedAt: lastRun?.completed_at || null,
|
|
1686
|
+
lastRunStatus: lastRun?.status || null,
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
module.exports = {
|
|
1691
|
+
initDb, getDb, closeDb, getDbPath: () => currentDbPath,
|
|
1692
|
+
getSetting, getSettingsByPrefix, setSetting,
|
|
1693
|
+
createPrompt, updatePrompt, getPrompt, listPrompts, listChildPrompts, setPromptParent, createGroupFromPrompts, deletePrompt, duplicatePrompt, reorderPrompts,
|
|
1694
|
+
getPromptVersions, restorePromptVersion,
|
|
1695
|
+
createFolder, listFolders, updateFolder, deleteFolder, reorderFolders,
|
|
1696
|
+
saveImage, getImage, updateImageAnnotations, listImages, deleteImage,
|
|
1697
|
+
createChain, getChain, listChains, updateChain, deleteChain,
|
|
1698
|
+
listPermissionRules, upsertPermissionRule, deletePermissionRule,
|
|
1699
|
+
listPermissionLog, addPermissionLog,
|
|
1700
|
+
getAlwaysAskTools, setAlwaysAsk,
|
|
1701
|
+
importSessionConversation, listSessionConversations, getSessionConversation,
|
|
1702
|
+
getSessionTitle, setSessionTitle, isSessionUserRenamed, getAllSessionTitles,
|
|
1703
|
+
createTemplate, listTemplates, getTemplate, deleteTemplate,
|
|
1704
|
+
trackPromptUsage, getPromptUsageStats,
|
|
1705
|
+
checkpointWal, createBackup, createBackupSync, listBackups, restoreBackup, deleteBackup, startDailyBackup,
|
|
1706
|
+
saveQueue, loadQueue, loadAllQueues, deleteQueueDb,
|
|
1707
|
+
listPermRules, addPermRule, removePermRule, bulkSetPermRules, getPermRulesByProject,
|
|
1708
|
+
listAutoApprovals, upsertAutoApproval, toggleAutoApproval, deleteAutoApproval, getEnabledAutoApprovals,
|
|
1709
|
+
listApprovalRules, upsertApprovalRule, toggleApprovalRule, deleteApprovalRule, incrementApprovalRuleMatch,
|
|
1710
|
+
addApprovalDecision, listApprovalDecisions, resolveApprovalDecision, getPendingEscalations,
|
|
1711
|
+
createReview, getReview, listReviews, updateReview, deleteReview,
|
|
1712
|
+
addReviewComment, updateReviewComment, deleteReviewComment,
|
|
1713
|
+
upsertSessionAnalysis, getSessionAnalysis, getAllSessionAnalyses, getSessionAnalysisCount,
|
|
1714
|
+
deleteSessionAnalysis, deleteStaleAnalyses,
|
|
1715
|
+
replaceInsightGroups, listInsightGroups,
|
|
1716
|
+
replaceInsightSkills, listInsightSkills,
|
|
1717
|
+
replaceInsightRecommendations, listInsightRecommendations,
|
|
1718
|
+
startAnalysisRun, completeAnalysisRun, getLastAnalysisRun,
|
|
1719
|
+
getInsightsData,
|
|
1720
|
+
DEFAULT_IMAGES_DIR, BACKUP_DIR,
|
|
1721
|
+
};
|