@way_marks/server 0.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,40 @@ exports.getActionsWithFiltering = getActionsWithFiltering;
49
49
  exports.archiveOldActions = archiveOldActions;
50
50
  exports.getArchivedActionsWithFiltering = getArchivedActionsWithFiltering;
51
51
  exports.getSummaryStats = getSummaryStats;
52
+ exports.createSession = createSession;
53
+ exports.getSession = getSession;
54
+ exports.getAllSessions = getAllSessions;
55
+ exports.getSessionActions = getSessionActions;
56
+ exports.rollbackSession = rollbackSession;
57
+ exports.markSessionRolledBack = markSessionRolledBack;
58
+ exports.addTeamMember = addTeamMember;
59
+ exports.getTeamMember = getTeamMember;
60
+ exports.getTeamMemberByEmail = getTeamMemberByEmail;
61
+ exports.getAllTeamMembers = getAllTeamMembers;
62
+ exports.removeTeamMember = removeTeamMember;
63
+ exports.addApprovalRoute = addApprovalRoute;
64
+ exports.getApprovalRoute = getApprovalRoute;
65
+ exports.getAllApprovalRoutes = getAllApprovalRoutes;
66
+ exports.updateApprovalRoute = updateApprovalRoute;
67
+ exports.deleteApprovalRoute = deleteApprovalRoute;
68
+ exports.createApprovalRequest = createApprovalRequest;
69
+ exports.getApprovalRequest = getApprovalRequest;
70
+ exports.getSessionApprovalRequests = getSessionApprovalRequests;
71
+ exports.getPendingApprovals = getPendingApprovals;
72
+ exports.submitApprovalDecision = submitApprovalDecision;
73
+ exports.getApprovalDecisions = getApprovalDecisions;
74
+ exports.addEscalationRule = addEscalationRule;
75
+ exports.getEscalationRule = getEscalationRule;
76
+ exports.getAllEscalationRules = getAllEscalationRules;
77
+ exports.updateEscalationRule = updateEscalationRule;
78
+ exports.deleteEscalationRule = deleteEscalationRule;
79
+ exports.createEscalationRequest = createEscalationRequest;
80
+ exports.getEscalationRequest = getEscalationRequest;
81
+ exports.getPendingEscalations = getPendingEscalations;
82
+ exports.getStaleApprovals = getStaleApprovals;
83
+ exports.submitEscalationDecision = submitEscalationDecision;
84
+ exports.getEscalationDecisions = getEscalationDecisions;
85
+ exports.getEscalationHistory = getEscalationHistory;
52
86
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
53
87
  const fs = __importStar(require("fs"));
54
88
  const path = __importStar(require("path"));
@@ -56,169 +90,403 @@ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
56
90
  const DB_PATH = process.env.WAYMARK_DB_PATH
57
91
  || path.join(PROJECT_ROOT, '.waymark', 'waymark.db');
58
92
  const DB_DIR = path.dirname(DB_PATH);
59
- // Ensure database directory exists
60
- if (!fs.existsSync(DB_DIR)) {
61
- fs.mkdirSync(DB_DIR, { recursive: true });
62
- }
63
- const db = new better_sqlite3_1.default(DB_PATH);
64
- // Create table on startup
65
- db.exec(`
66
- CREATE TABLE IF NOT EXISTS action_log (
67
- id INTEGER PRIMARY KEY AUTOINCREMENT,
68
- action_id TEXT UNIQUE NOT NULL,
69
- session_id TEXT NOT NULL,
70
- tool_name TEXT NOT NULL,
71
- target_path TEXT,
72
- input_payload TEXT NOT NULL,
73
- before_snapshot TEXT,
74
- after_snapshot TEXT,
75
- status TEXT NOT NULL DEFAULT 'pending',
76
- error_message TEXT,
77
- stdout TEXT,
78
- stderr TEXT,
79
- rolled_back INTEGER NOT NULL DEFAULT 0,
80
- rolled_back_at TEXT,
81
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
82
- )
83
- `);
84
- // Migrate existing DBs — add stdout/stderr if not present
85
- try {
86
- db.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
87
- }
88
- catch { }
89
- try {
90
- db.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
91
- }
92
- catch { }
93
- // Migrate v2: policy engine columns
94
- try {
95
- db.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
96
- }
97
- catch { }
98
- try {
99
- db.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
100
- }
101
- catch { }
102
- try {
103
- db.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
104
- }
105
- catch { }
106
- // Migrate v3: approval flow columns
107
- try {
108
- db.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
109
- }
110
- catch { }
111
- try {
112
- db.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
113
- }
114
- catch { }
115
- try {
116
- db.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
117
- }
118
- catch { }
119
- try {
120
- db.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
121
- }
122
- catch { }
123
- // Migrate v4: Phase 1 — plan mode logging visibility
124
- try {
125
- db.exec("ALTER TABLE action_log ADD COLUMN event_type TEXT NOT NULL DEFAULT 'execution'");
126
- }
127
- catch { }
128
- try {
129
- db.exec('ALTER TABLE action_log ADD COLUMN observation_context TEXT');
130
- }
131
- catch { }
132
- try {
133
- db.exec('ALTER TABLE action_log ADD COLUMN request_source TEXT DEFAULT \'direct\'');
134
- }
135
- catch { }
136
- // Migrate v5: Phase 5B — CLI action logging (GitHub Copilot CLI wrapper)
137
- try {
138
- db.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
139
- }
140
- catch { }
141
- // Indexes for query performance
142
- try {
143
- db.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
144
- }
145
- catch { }
146
- try {
147
- db.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
148
- }
149
- catch { }
150
- try {
151
- db.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
152
- }
153
- catch { }
154
- // Phase 3: Add indexes for pagination and filtering
155
- try {
156
- db.exec('CREATE INDEX IF NOT EXISTS idx_tool_name ON action_log(tool_name)');
157
- }
158
- catch { }
159
- try {
160
- db.exec('CREATE INDEX IF NOT EXISTS idx_created_at ON action_log(created_at DESC)');
161
- }
162
- catch { }
163
- try {
164
- db.exec('CREATE INDEX IF NOT EXISTS idx_status_created ON action_log(status, created_at DESC)');
165
- }
166
- catch { }
167
- // Phase 3: Create archive table (same schema as action_log)
168
- db.exec(`
169
- CREATE TABLE IF NOT EXISTS action_archive (
170
- id INTEGER PRIMARY KEY AUTOINCREMENT,
171
- action_id TEXT UNIQUE NOT NULL,
172
- session_id TEXT NOT NULL,
173
- tool_name TEXT NOT NULL,
174
- target_path TEXT,
175
- input_payload TEXT NOT NULL,
176
- before_snapshot TEXT,
177
- after_snapshot TEXT,
178
- status TEXT NOT NULL DEFAULT 'pending',
179
- error_message TEXT,
180
- stdout TEXT,
181
- stderr TEXT,
182
- rolled_back INTEGER NOT NULL DEFAULT 0,
183
- rolled_back_at TEXT,
184
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
185
- decision TEXT NOT NULL DEFAULT 'allow',
186
- policy_reason TEXT,
187
- matched_rule TEXT,
188
- approved_at TEXT,
189
- approved_by TEXT,
190
- rejected_at TEXT,
191
- rejected_reason TEXT,
192
- event_type TEXT NOT NULL DEFAULT 'execution',
193
- observation_context TEXT,
194
- request_source TEXT DEFAULT 'direct',
195
- source TEXT DEFAULT 'mcp'
196
- )
197
- `);
198
- // Phase 3: Add indexes on archive table
199
- try {
200
- db.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
201
- }
202
- catch { }
203
- try {
204
- db.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
205
- }
206
- catch { }
207
- const insertStmt = db.prepare(`
208
- INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
209
- VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
210
- `);
211
- const updateStmt = db.prepare(`
212
- UPDATE action_log
213
- SET status = COALESCE(@status, status),
214
- after_snapshot = COALESCE(@after_snapshot, after_snapshot),
215
- error_message = COALESCE(@error_message, error_message),
216
- stdout = COALESCE(@stdout, stdout),
217
- stderr = COALESCE(@stderr, stderr)
218
- WHERE action_id = @action_id
219
- `);
93
+ // Lazy-load database to allow tests to mock it
94
+ let db = null;
95
+ let initialized = false;
96
+ function initializeSchema(database) {
97
+ if (initialized)
98
+ return;
99
+ initialized = true;
100
+ // Create table on startup
101
+ database.exec(`
102
+ CREATE TABLE IF NOT EXISTS action_log (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ action_id TEXT UNIQUE NOT NULL,
105
+ session_id TEXT NOT NULL,
106
+ tool_name TEXT NOT NULL,
107
+ target_path TEXT,
108
+ input_payload TEXT NOT NULL,
109
+ before_snapshot TEXT,
110
+ after_snapshot TEXT,
111
+ status TEXT NOT NULL DEFAULT 'pending',
112
+ error_message TEXT,
113
+ stdout TEXT,
114
+ stderr TEXT,
115
+ rolled_back INTEGER NOT NULL DEFAULT 0,
116
+ rolled_back_at TEXT,
117
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
118
+ )
119
+ `);
120
+ // Migrate existing DBs add stdout/stderr if not present
121
+ try {
122
+ database.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
123
+ }
124
+ catch { }
125
+ try {
126
+ database.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
127
+ }
128
+ catch { }
129
+ // Migrate v2: policy engine columns
130
+ try {
131
+ database.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
132
+ }
133
+ catch { }
134
+ try {
135
+ database.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
136
+ }
137
+ catch { }
138
+ try {
139
+ database.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
140
+ }
141
+ catch { }
142
+ // Migrate v3: approval flow columns
143
+ try {
144
+ database.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
145
+ }
146
+ catch { }
147
+ try {
148
+ database.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
149
+ }
150
+ catch { }
151
+ try {
152
+ database.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
153
+ }
154
+ catch { }
155
+ try {
156
+ database.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
157
+ }
158
+ catch { }
159
+ // Migrate v4: Phase 1 plan mode logging visibility
160
+ try {
161
+ database.exec("ALTER TABLE action_log ADD COLUMN event_type TEXT NOT NULL DEFAULT 'execution'");
162
+ }
163
+ catch { }
164
+ try {
165
+ database.exec('ALTER TABLE action_log ADD COLUMN observation_context TEXT');
166
+ }
167
+ catch { }
168
+ try {
169
+ database.exec('ALTER TABLE action_log ADD COLUMN request_source TEXT DEFAULT \'direct\'');
170
+ }
171
+ catch { }
172
+ // Migrate v5: Phase 5B CLI action logging (GitHub Copilot CLI wrapper)
173
+ try {
174
+ database.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
175
+ }
176
+ catch { }
177
+ // Migrate v6: Phase 1 Session-level rollback
178
+ try {
179
+ database.exec('ALTER TABLE action_log ADD COLUMN rollback_group TEXT');
180
+ }
181
+ catch { }
182
+ try {
183
+ database.exec('ALTER TABLE action_log ADD COLUMN is_reversible INTEGER DEFAULT 1');
184
+ }
185
+ catch { }
186
+ try {
187
+ database.exec('ALTER TABLE action_log ADD COLUMN revert_action_id TEXT');
188
+ }
189
+ catch { }
190
+ // Phase 1: Create sessions table
191
+ database.exec(`
192
+ CREATE TABLE IF NOT EXISTS sessions (
193
+ session_id TEXT PRIMARY KEY,
194
+ user_id TEXT,
195
+ project_id TEXT,
196
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
197
+ rolled_back_at DATETIME,
198
+ status TEXT NOT NULL DEFAULT 'active'
199
+ )
200
+ `);
201
+ // Indexes for query performance
202
+ try {
203
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
204
+ }
205
+ catch { }
206
+ try {
207
+ database.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
208
+ }
209
+ catch { }
210
+ try {
211
+ database.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
212
+ }
213
+ catch { }
214
+ // Phase 3: Add indexes for pagination and filtering
215
+ try {
216
+ database.exec('CREATE INDEX IF NOT EXISTS idx_tool_name ON action_log(tool_name)');
217
+ }
218
+ catch { }
219
+ try {
220
+ database.exec('CREATE INDEX IF NOT EXISTS idx_created_at ON action_log(created_at DESC)');
221
+ }
222
+ catch { }
223
+ try {
224
+ database.exec('CREATE INDEX IF NOT EXISTS idx_status_created ON action_log(status, created_at DESC)');
225
+ }
226
+ catch { }
227
+ // Phase 3: Create archive table (same schema as action_log)
228
+ database.exec(`
229
+ CREATE TABLE IF NOT EXISTS action_archive (
230
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
231
+ action_id TEXT UNIQUE NOT NULL,
232
+ session_id TEXT NOT NULL,
233
+ tool_name TEXT NOT NULL,
234
+ target_path TEXT,
235
+ input_payload TEXT NOT NULL,
236
+ before_snapshot TEXT,
237
+ after_snapshot TEXT,
238
+ status TEXT NOT NULL DEFAULT 'pending',
239
+ error_message TEXT,
240
+ stdout TEXT,
241
+ stderr TEXT,
242
+ rolled_back INTEGER NOT NULL DEFAULT 0,
243
+ rolled_back_at TEXT,
244
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
245
+ decision TEXT NOT NULL DEFAULT 'allow',
246
+ policy_reason TEXT,
247
+ matched_rule TEXT,
248
+ approved_at TEXT,
249
+ approved_by TEXT,
250
+ rejected_at TEXT,
251
+ rejected_reason TEXT,
252
+ event_type TEXT NOT NULL DEFAULT 'execution',
253
+ observation_context TEXT,
254
+ request_source TEXT DEFAULT 'direct',
255
+ source TEXT DEFAULT 'mcp',
256
+ rollback_group TEXT,
257
+ is_reversible INTEGER DEFAULT 1,
258
+ revert_action_id TEXT
259
+ )
260
+ `);
261
+ // Phase 1: Add same columns to archive (migrations)
262
+ try {
263
+ database.exec('ALTER TABLE action_archive ADD COLUMN rollback_group TEXT');
264
+ }
265
+ catch { }
266
+ try {
267
+ database.exec('ALTER TABLE action_archive ADD COLUMN is_reversible INTEGER DEFAULT 1');
268
+ }
269
+ catch { }
270
+ try {
271
+ database.exec('ALTER TABLE action_archive ADD COLUMN revert_action_id TEXT');
272
+ }
273
+ catch { }
274
+ // Phase 3: Add indexes on archive table
275
+ try {
276
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
277
+ }
278
+ catch { }
279
+ try {
280
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
281
+ }
282
+ catch { }
283
+ // Phase 1: Add indexes for session rollback
284
+ try {
285
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_session ON action_log(session_id, status)');
286
+ }
287
+ catch { }
288
+ try {
289
+ database.exec('CREATE INDEX IF NOT EXISTS idx_action_rollback_group ON action_log(rollback_group)');
290
+ }
291
+ catch { }
292
+ try {
293
+ database.exec('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status, created_at DESC)');
294
+ }
295
+ catch { }
296
+ try {
297
+ database.exec('CREATE INDEX IF NOT EXISTS idx_archive_session ON action_archive(session_id)');
298
+ }
299
+ catch { }
300
+ // Phase 2: Create team_members table
301
+ database.exec(`
302
+ CREATE TABLE IF NOT EXISTS team_members (
303
+ member_id TEXT PRIMARY KEY,
304
+ name TEXT NOT NULL,
305
+ email TEXT NOT NULL UNIQUE,
306
+ slack_id TEXT,
307
+ role TEXT DEFAULT 'approver',
308
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
309
+ added_by TEXT,
310
+ status TEXT DEFAULT 'active'
311
+ )
312
+ `);
313
+ // Phase 2: Create approval_routes table (rules for who approves what)
314
+ database.exec(`
315
+ CREATE TABLE IF NOT EXISTS approval_routes (
316
+ route_id TEXT PRIMARY KEY,
317
+ name TEXT NOT NULL,
318
+ description TEXT,
319
+ condition_type TEXT DEFAULT 'all_sessions',
320
+ condition_json TEXT,
321
+ required_approvers INTEGER DEFAULT 1,
322
+ approver_ids TEXT NOT NULL,
323
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
324
+ created_by TEXT,
325
+ status TEXT DEFAULT 'active'
326
+ )
327
+ `);
328
+ // Phase 2: Create approval_requests table (pending/completed approvals)
329
+ database.exec(`
330
+ CREATE TABLE IF NOT EXISTS approval_requests (
331
+ request_id TEXT PRIMARY KEY,
332
+ session_id TEXT NOT NULL,
333
+ route_id TEXT NOT NULL,
334
+ triggered_by TEXT NOT NULL,
335
+ triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
336
+ status TEXT DEFAULT 'pending',
337
+ completed_at DATETIME,
338
+ approver_ids TEXT NOT NULL,
339
+ approved_count INTEGER DEFAULT 0,
340
+ rejected_count INTEGER DEFAULT 0,
341
+ approval_details TEXT
342
+ )
343
+ `);
344
+ // Phase 2: Create approval_decisions table (audit trail)
345
+ database.exec(`
346
+ CREATE TABLE IF NOT EXISTS approval_decisions (
347
+ decision_id TEXT PRIMARY KEY,
348
+ request_id TEXT NOT NULL,
349
+ approver_id TEXT NOT NULL,
350
+ decision TEXT NOT NULL,
351
+ reason TEXT,
352
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
353
+ )
354
+ `);
355
+ // Phase 2: Add indexes for team tables
356
+ try {
357
+ database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email)');
358
+ }
359
+ catch { }
360
+ try {
361
+ database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status)');
362
+ }
363
+ catch { }
364
+ try {
365
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_routes_status ON approval_routes(status)');
366
+ }
367
+ catch { }
368
+ try {
369
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_session ON approval_requests(session_id)');
370
+ }
371
+ catch { }
372
+ try {
373
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status)');
374
+ }
375
+ catch { }
376
+ try {
377
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_triggered ON approval_requests(triggered_at DESC)');
378
+ }
379
+ catch { }
380
+ try {
381
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_request ON approval_decisions(request_id)');
382
+ }
383
+ catch { }
384
+ try {
385
+ database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_approver ON approval_decisions(approver_id)');
386
+ }
387
+ catch { }
388
+ // Phase 3: Create escalation_rules table
389
+ database.exec(`
390
+ CREATE TABLE IF NOT EXISTS escalation_rules (
391
+ rule_id TEXT PRIMARY KEY,
392
+ name TEXT NOT NULL,
393
+ description TEXT,
394
+ timeout_hours INTEGER DEFAULT 24,
395
+ escalation_targets TEXT NOT NULL,
396
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
397
+ created_by TEXT,
398
+ status TEXT DEFAULT 'active'
399
+ )
400
+ `);
401
+ // Phase 3: Create escalation_requests table
402
+ database.exec(`
403
+ CREATE TABLE IF NOT EXISTS escalation_requests (
404
+ request_id TEXT PRIMARY KEY,
405
+ approval_request_id TEXT NOT NULL,
406
+ session_id TEXT NOT NULL,
407
+ escalation_triggered_at DATETIME,
408
+ escalation_deadline DATETIME,
409
+ escalation_targets TEXT NOT NULL,
410
+ status TEXT DEFAULT 'pending',
411
+ decided_at DATETIME,
412
+ decision TEXT
413
+ )
414
+ `);
415
+ // Phase 3: Create escalation_decisions table
416
+ database.exec(`
417
+ CREATE TABLE IF NOT EXISTS escalation_decisions (
418
+ decision_id TEXT PRIMARY KEY,
419
+ escalation_request_id TEXT NOT NULL,
420
+ target_id TEXT NOT NULL,
421
+ decision TEXT NOT NULL,
422
+ reason TEXT,
423
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
424
+ )
425
+ `);
426
+ // Phase 3: Add indexes for escalation tables
427
+ try {
428
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_rules_status ON escalation_rules(status)');
429
+ }
430
+ catch { }
431
+ try {
432
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_approval ON escalation_requests(approval_request_id)');
433
+ }
434
+ catch { }
435
+ try {
436
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_deadline ON escalation_requests(escalation_deadline)');
437
+ }
438
+ catch { }
439
+ try {
440
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_status ON escalation_requests(status)');
441
+ }
442
+ catch { }
443
+ try {
444
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_request ON escalation_decisions(escalation_request_id)');
445
+ }
446
+ catch { }
447
+ try {
448
+ database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_target ON escalation_decisions(target_id)');
449
+ }
450
+ catch { }
451
+ }
452
+ function getDb() {
453
+ if (!db) {
454
+ // Ensure database directory exists
455
+ if (!fs.existsSync(DB_DIR)) {
456
+ fs.mkdirSync(DB_DIR, { recursive: true });
457
+ }
458
+ db = new better_sqlite3_1.default(DB_PATH);
459
+ initializeSchema(db);
460
+ }
461
+ return db;
462
+ }
463
+ let insertStmt = null;
464
+ let updateStmt = null;
465
+ function getInsertStmt() {
466
+ if (!insertStmt) {
467
+ insertStmt = getDb().prepare(`
468
+ INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
469
+ VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
470
+ `);
471
+ }
472
+ return insertStmt;
473
+ }
474
+ function getUpdateStmt() {
475
+ if (!updateStmt) {
476
+ updateStmt = getDb().prepare(`
477
+ UPDATE action_log
478
+ SET status = COALESCE(@status, status),
479
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot),
480
+ error_message = COALESCE(@error_message, error_message),
481
+ stdout = COALESCE(@stdout, stdout),
482
+ stderr = COALESCE(@stderr, stderr)
483
+ WHERE action_id = @action_id
484
+ `);
485
+ }
486
+ return updateStmt;
487
+ }
220
488
  function insertAction(params) {
221
- insertStmt.run({
489
+ getInsertStmt().run({
222
490
  action_id: params.action_id,
223
491
  session_id: params.session_id,
224
492
  tool_name: params.tool_name,
@@ -236,7 +504,7 @@ function insertAction(params) {
236
504
  });
237
505
  }
238
506
  function updateAction(action_id, params) {
239
- updateStmt.run({
507
+ getUpdateStmt().run({
240
508
  action_id,
241
509
  status: params.status ?? null,
242
510
  after_snapshot: params.after_snapshot ?? null,
@@ -246,24 +514,24 @@ function updateAction(action_id, params) {
246
514
  });
247
515
  }
248
516
  function getActions() {
249
- return db.prepare(`
517
+ return getDb().prepare(`
250
518
  SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
251
519
  `).all();
252
520
  }
253
521
  function getAction(action_id) {
254
- return db.prepare(`
522
+ return getDb().prepare(`
255
523
  SELECT * FROM action_log WHERE action_id = ?
256
524
  `).get(action_id);
257
525
  }
258
526
  function markRolledBack(action_id) {
259
- db.prepare(`
527
+ getDb().prepare(`
260
528
  UPDATE action_log
261
529
  SET rolled_back = 1, rolled_back_at = datetime('now')
262
530
  WHERE action_id = ?
263
531
  `).run(action_id);
264
532
  }
265
533
  function getSessions() {
266
- return db.prepare(`
534
+ return getDb().prepare(`
267
535
  SELECT session_id,
268
536
  COUNT(*) as action_count,
269
537
  MAX(created_at) as latest
@@ -273,7 +541,7 @@ function getSessions() {
273
541
  `).all();
274
542
  }
275
543
  function approveAction(action_id, approved_by, after_snapshot) {
276
- db.prepare(`
544
+ getDb().prepare(`
277
545
  UPDATE action_log
278
546
  SET status = 'success',
279
547
  decision = 'allow',
@@ -284,7 +552,7 @@ function approveAction(action_id, approved_by, after_snapshot) {
284
552
  `).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
285
553
  }
286
554
  function rejectAction(action_id, reason) {
287
- db.prepare(`
555
+ getDb().prepare(`
288
556
  UPDATE action_log
289
557
  SET status = 'rejected',
290
558
  decision = 'rejected',
@@ -294,7 +562,7 @@ function rejectAction(action_id, reason) {
294
562
  `).run({ action_id, reason });
295
563
  }
296
564
  function getPendingCount() {
297
- const row = db.prepare(`
565
+ const row = getDb().prepare(`
298
566
  SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
299
567
  `).get();
300
568
  return row.count;
@@ -318,12 +586,12 @@ function getActionsWithFiltering(filter = {}) {
318
586
  params.search = `%${filter.search}%`;
319
587
  }
320
588
  // Get total count
321
- const countRow = db.prepare(`
589
+ const countRow = getDb().prepare(`
322
590
  SELECT COUNT(*) as count FROM action_log WHERE ${where}
323
591
  `).get(params);
324
592
  const totalCount = countRow.count;
325
593
  // Get paginated results
326
- const actions = db.prepare(`
594
+ const actions = getDb().prepare(`
327
595
  SELECT * FROM action_log WHERE ${where}
328
596
  ORDER BY created_at DESC
329
597
  LIMIT @limit OFFSET @offset
@@ -341,12 +609,12 @@ function archiveOldActions(daysOld = 30) {
341
609
  cutoffDate.setDate(cutoffDate.getDate() - daysOld);
342
610
  const cutoffStr = cutoffDate.toISOString();
343
611
  // Copy old actions to archive
344
- const result = db.prepare(`
612
+ const result = getDb().prepare(`
345
613
  INSERT OR IGNORE INTO action_archive
346
614
  SELECT * FROM action_log WHERE created_at < @cutoff
347
615
  `).run({ cutoff: cutoffStr });
348
616
  // Delete from main table (but keep recent entries)
349
- const deleteStmt = db.prepare(`
617
+ const deleteStmt = getDb().prepare(`
350
618
  DELETE FROM action_log WHERE created_at < @cutoff
351
619
  AND id NOT IN (
352
620
  SELECT id FROM action_log ORDER BY created_at DESC LIMIT 1000
@@ -373,11 +641,11 @@ function getArchivedActionsWithFiltering(filter = {}) {
373
641
  where += ' AND (action_id LIKE @search OR target_path LIKE @search)';
374
642
  params.search = `%${filter.search}%`;
375
643
  }
376
- const countRow = db.prepare(`
644
+ const countRow = getDb().prepare(`
377
645
  SELECT COUNT(*) as count FROM action_archive WHERE ${where}
378
646
  `).get(params);
379
647
  const totalCount = countRow.count;
380
- const actions = db.prepare(`
648
+ const actions = getDb().prepare(`
381
649
  SELECT * FROM action_archive WHERE ${where}
382
650
  ORDER BY created_at DESC
383
651
  LIMIT @limit OFFSET @offset
@@ -395,34 +663,34 @@ function getSummaryStats() {
395
663
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
396
664
  const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
397
665
  const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
398
- const total = db.prepare(`
666
+ const total = getDb().prepare(`
399
667
  SELECT COUNT(*) as count FROM action_log
400
668
  `).get();
401
- const pending = db.prepare(`
669
+ const pending = getDb().prepare(`
402
670
  SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
403
671
  `).get();
404
- const approved = db.prepare(`
672
+ const approved = getDb().prepare(`
405
673
  SELECT COUNT(*) as count FROM action_log WHERE status = 'success' OR decision = 'allow'
406
674
  `).get();
407
- const rejected = db.prepare(`
675
+ const rejected = getDb().prepare(`
408
676
  SELECT COUNT(*) as count FROM action_log WHERE status = 'rejected' OR decision = 'rejected'
409
677
  `).get();
410
- const todayCount = db.prepare(`
678
+ const todayCount = getDb().prepare(`
411
679
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @today
412
680
  `).get({ today });
413
- const weekCount = db.prepare(`
681
+ const weekCount = getDb().prepare(`
414
682
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @weekAgo
415
683
  `).get({ weekAgo });
416
- const monthCount = db.prepare(`
684
+ const monthCount = getDb().prepare(`
417
685
  SELECT COUNT(*) as count FROM action_log WHERE created_at >= @monthAgo
418
686
  `).get({ monthAgo });
419
- const topTools = db.prepare(`
687
+ const topTools = getDb().prepare(`
420
688
  SELECT tool_name as tool, COUNT(*) as count FROM action_log
421
689
  GROUP BY tool_name
422
690
  ORDER BY count DESC
423
691
  LIMIT 5
424
692
  `).all();
425
- const topPaths = db.prepare(`
693
+ const topPaths = getDb().prepare(`
426
694
  SELECT target_path as path, COUNT(*) as count FROM action_log
427
695
  WHERE target_path IS NOT NULL
428
696
  GROUP BY target_path
@@ -441,4 +709,305 @@ function getSummaryStats() {
441
709
  topPaths,
442
710
  };
443
711
  }
712
+ // Phase 1: Session management functions
713
+ function createSession(session_id, user_id, project_id) {
714
+ getDb().prepare(`
715
+ INSERT OR IGNORE INTO sessions (session_id, user_id, project_id, status)
716
+ VALUES (?, ?, ?, 'active')
717
+ `).run(session_id, user_id ?? null, project_id ?? null);
718
+ }
719
+ function getSession(session_id) {
720
+ return getDb().prepare(`
721
+ SELECT * FROM sessions WHERE session_id = ?
722
+ `).get(session_id);
723
+ }
724
+ function getAllSessions() {
725
+ return getDb().prepare(`
726
+ SELECT * FROM sessions ORDER BY created_at DESC
727
+ `).all();
728
+ }
729
+ function getSessionActions(session_id) {
730
+ return getDb().prepare(`
731
+ SELECT * FROM action_log
732
+ WHERE session_id = ?
733
+ ORDER BY created_at ASC
734
+ `).all(session_id);
735
+ }
736
+ function rollbackSession(session_id) {
737
+ try {
738
+ // Start transaction
739
+ const transaction = getDb().transaction(() => {
740
+ // Get all actions in session
741
+ const actions = getSessionActions(session_id);
742
+ if (actions.length === 0) {
743
+ throw new Error(`Session ${session_id} has no actions to rollback`);
744
+ }
745
+ // Check if all actions are reversible
746
+ const nonReversible = actions.filter(a => a.is_reversible === 0);
747
+ if (nonReversible.length > 0) {
748
+ throw new Error(`${nonReversible.length} action(s) in session are not reversible`);
749
+ }
750
+ // For each action, perform rollback
751
+ let rolledBackCount = 0;
752
+ for (const action of actions) {
753
+ // Mark as rolled back
754
+ markRolledBack(action.action_id);
755
+ rolledBackCount++;
756
+ // If before_snapshot exists, restore it (for write_file actions)
757
+ if (action.before_snapshot) {
758
+ try {
759
+ const snapshot = JSON.parse(action.before_snapshot);
760
+ if (snapshot.file_path && snapshot.content !== undefined) {
761
+ // This will be handled by the rollback manager
762
+ // We just mark it as rolled back here
763
+ }
764
+ }
765
+ catch { }
766
+ }
767
+ }
768
+ // Mark session as rolled back
769
+ getDb().prepare(`
770
+ UPDATE sessions
771
+ SET rolled_back_at = datetime('now'), status = 'rolled_back'
772
+ WHERE session_id = ?
773
+ `).run(session_id);
774
+ return rolledBackCount;
775
+ });
776
+ const actionsRolledBack = transaction();
777
+ return {
778
+ success: true,
779
+ session_id,
780
+ actions_rolled_back: actionsRolledBack,
781
+ };
782
+ }
783
+ catch (error) {
784
+ return {
785
+ success: false,
786
+ session_id,
787
+ actions_rolled_back: 0,
788
+ error: error.message,
789
+ };
790
+ }
791
+ }
792
+ function markSessionRolledBack(session_id) {
793
+ getDb().prepare(`
794
+ UPDATE sessions
795
+ SET rolled_back_at = datetime('now'), status = 'rolled_back'
796
+ WHERE session_id = ?
797
+ `).run(session_id);
798
+ }
799
+ // Phase 2: Team member functions
800
+ function addTeamMember(member_id, name, email, added_by, slack_id) {
801
+ getDb().prepare(`
802
+ INSERT OR REPLACE INTO team_members (member_id, name, email, slack_id, added_by, status)
803
+ VALUES (?, ?, ?, ?, ?, 'active')
804
+ `).run(member_id, name, email, slack_id ?? null, added_by);
805
+ }
806
+ function getTeamMember(member_id) {
807
+ return getDb().prepare(`
808
+ SELECT * FROM team_members WHERE member_id = ?
809
+ `).get(member_id);
810
+ }
811
+ function getTeamMemberByEmail(email) {
812
+ return getDb().prepare(`
813
+ SELECT * FROM team_members WHERE email = ?
814
+ `).get(email);
815
+ }
816
+ function getAllTeamMembers() {
817
+ return getDb().prepare(`
818
+ SELECT * FROM team_members WHERE status = 'active' ORDER BY name ASC
819
+ `).all();
820
+ }
821
+ function removeTeamMember(member_id) {
822
+ getDb().prepare(`
823
+ UPDATE team_members SET status = 'inactive' WHERE member_id = ?
824
+ `).run(member_id);
825
+ }
826
+ // Phase 2: Approval route functions
827
+ function addApprovalRoute(route_id, name, approver_ids, created_by, description, condition_type, condition_json) {
828
+ getDb().prepare(`
829
+ INSERT OR REPLACE INTO approval_routes (route_id, name, description, condition_type, condition_json, approver_ids, created_by, status)
830
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
831
+ `).run(route_id, name, description ?? null, condition_type ?? 'all_sessions', condition_json ?? null, JSON.stringify(approver_ids), created_by);
832
+ }
833
+ function getApprovalRoute(route_id) {
834
+ return getDb().prepare(`
835
+ SELECT * FROM approval_routes WHERE route_id = ?
836
+ `).get(route_id);
837
+ }
838
+ function getAllApprovalRoutes() {
839
+ return getDb().prepare(`
840
+ SELECT * FROM approval_routes WHERE status = 'active' ORDER BY name ASC
841
+ `).all();
842
+ }
843
+ function updateApprovalRoute(route_id, updates) {
844
+ const current = getApprovalRoute(route_id);
845
+ if (!current)
846
+ throw new Error(`Route ${route_id} not found`);
847
+ getDb().prepare(`
848
+ UPDATE approval_routes
849
+ SET name = ?, description = ?, approver_ids = ?
850
+ WHERE route_id = ?
851
+ `).run(updates.name ?? current.name, updates.description ?? current.description, updates.approver_ids ? JSON.stringify(updates.approver_ids) : current.approver_ids, route_id);
852
+ }
853
+ function deleteApprovalRoute(route_id) {
854
+ getDb().prepare(`
855
+ UPDATE approval_routes SET status = 'inactive' WHERE route_id = ?
856
+ `).run(route_id);
857
+ }
858
+ // Phase 2: Approval request functions
859
+ function createApprovalRequest(request_id, session_id, route_id, triggered_by, approver_ids) {
860
+ getDb().prepare(`
861
+ INSERT INTO approval_requests (request_id, session_id, route_id, triggered_by, approver_ids, status)
862
+ VALUES (?, ?, ?, ?, ?, 'pending')
863
+ `).run(request_id, session_id, route_id, triggered_by, JSON.stringify(approver_ids));
864
+ }
865
+ function getApprovalRequest(request_id) {
866
+ return getDb().prepare(`
867
+ SELECT * FROM approval_requests WHERE request_id = ?
868
+ `).get(request_id);
869
+ }
870
+ function getSessionApprovalRequests(session_id) {
871
+ return getDb().prepare(`
872
+ SELECT * FROM approval_requests WHERE session_id = ? ORDER BY triggered_at DESC
873
+ `).all(session_id);
874
+ }
875
+ function getPendingApprovals(approver_id) {
876
+ if (approver_id) {
877
+ return getDb().prepare(`
878
+ SELECT * FROM approval_requests
879
+ WHERE status = 'pending' AND approver_ids LIKE ?
880
+ ORDER BY triggered_at DESC
881
+ `).all(`%"${approver_id}"%`);
882
+ }
883
+ return getDb().prepare(`
884
+ SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY triggered_at DESC
885
+ `).all();
886
+ }
887
+ function submitApprovalDecision(decision_id, request_id, approver_id, decision, reason) {
888
+ // Insert decision
889
+ getDb().prepare(`
890
+ INSERT INTO approval_decisions (decision_id, request_id, approver_id, decision, reason)
891
+ VALUES (?, ?, ?, ?, ?)
892
+ `).run(decision_id, request_id, approver_id, decision, reason ?? null);
893
+ // Update approval request counts
894
+ const request = getApprovalRequest(request_id);
895
+ if (!request)
896
+ throw new Error(`Request ${request_id} not found`);
897
+ const decisions = getDb().prepare(`
898
+ SELECT decision FROM approval_decisions WHERE request_id = ?
899
+ `).all(request_id);
900
+ const approvedCount = decisions.filter(d => d.decision === 'approve').length;
901
+ const rejectedCount = decisions.filter(d => d.decision === 'reject').length;
902
+ const approverIds = JSON.parse(request.approver_ids);
903
+ // Determine new status
904
+ let newStatus = 'pending';
905
+ if (rejectedCount > 0) {
906
+ newStatus = 'rejected';
907
+ }
908
+ else if (approvedCount >= approverIds.length) {
909
+ newStatus = 'approved';
910
+ }
911
+ getDb().prepare(`
912
+ UPDATE approval_requests
913
+ SET approved_count = ?, rejected_count = ?, status = ?, completed_at = ?
914
+ WHERE request_id = ?
915
+ `).run(approvedCount, rejectedCount, newStatus, newStatus !== 'pending' ? new Date().toISOString() : null, request_id);
916
+ }
917
+ function getApprovalDecisions(request_id) {
918
+ return getDb().prepare(`
919
+ SELECT * FROM approval_decisions WHERE request_id = ? ORDER BY decided_at ASC
920
+ `).all(request_id);
921
+ }
922
+ // Phase 3: Escalation rule functions
923
+ function addEscalationRule(rule_id, name, escalation_targets, created_by, description, timeout_hours) {
924
+ getDb().prepare(`
925
+ INSERT OR REPLACE INTO escalation_rules (rule_id, name, description, timeout_hours, escalation_targets, created_by, status)
926
+ VALUES (?, ?, ?, ?, ?, ?, 'active')
927
+ `).run(rule_id, name, description ?? null, timeout_hours ?? 24, JSON.stringify(escalation_targets), created_by);
928
+ }
929
+ function getEscalationRule(rule_id) {
930
+ return getDb().prepare(`
931
+ SELECT * FROM escalation_rules WHERE rule_id = ?
932
+ `).get(rule_id);
933
+ }
934
+ function getAllEscalationRules() {
935
+ return getDb().prepare(`
936
+ SELECT * FROM escalation_rules WHERE status = 'active' ORDER BY name ASC
937
+ `).all();
938
+ }
939
+ function updateEscalationRule(rule_id, updates) {
940
+ const current = getEscalationRule(rule_id);
941
+ if (!current)
942
+ throw new Error(`Rule ${rule_id} not found`);
943
+ getDb().prepare(`
944
+ UPDATE escalation_rules
945
+ SET name = ?, description = ?, timeout_hours = ?, escalation_targets = ?
946
+ WHERE rule_id = ?
947
+ `).run(updates.name ?? current.name, updates.description ?? current.description, updates.timeout_hours ?? current.timeout_hours, updates.escalation_targets ? JSON.stringify(updates.escalation_targets) : current.escalation_targets, rule_id);
948
+ }
949
+ function deleteEscalationRule(rule_id) {
950
+ getDb().prepare(`
951
+ UPDATE escalation_rules SET status = 'inactive' WHERE rule_id = ?
952
+ `).run(rule_id);
953
+ }
954
+ // Phase 3: Escalation request functions
955
+ function createEscalationRequest(request_id, approval_request_id, session_id, escalation_targets, deadline) {
956
+ getDb().prepare(`
957
+ INSERT INTO escalation_requests (request_id, approval_request_id, session_id, escalation_triggered_at, escalation_deadline, escalation_targets, status)
958
+ VALUES (?, ?, ?, datetime('now'), ?, ?, 'pending')
959
+ `).run(request_id, approval_request_id, session_id, deadline, JSON.stringify(escalation_targets));
960
+ }
961
+ function getEscalationRequest(request_id) {
962
+ return getDb().prepare(`
963
+ SELECT * FROM escalation_requests WHERE request_id = ?
964
+ `).get(request_id);
965
+ }
966
+ function getPendingEscalations() {
967
+ return getDb().prepare(`
968
+ SELECT * FROM escalation_requests WHERE status = 'pending' ORDER BY escalation_deadline ASC
969
+ `).all();
970
+ }
971
+ function getStaleApprovals(now) {
972
+ return getDb().prepare(`
973
+ SELECT DISTINCT ar.request_id as approval_request_id, ar.session_id
974
+ FROM approval_requests ar
975
+ WHERE ar.status = 'pending'
976
+ AND ar.triggered_at < ?
977
+ AND NOT EXISTS (
978
+ SELECT 1 FROM escalation_requests er
979
+ WHERE er.approval_request_id = ar.request_id
980
+ )
981
+ `).all(now);
982
+ }
983
+ function submitEscalationDecision(decision_id, escalation_request_id, target_id, decision, reason) {
984
+ getDb().prepare(`
985
+ INSERT INTO escalation_decisions (decision_id, escalation_request_id, target_id, decision, reason)
986
+ VALUES (?, ?, ?, ?, ?)
987
+ `).run(decision_id, escalation_request_id, target_id, decision, reason ?? null);
988
+ // Update escalation request status if all targets have decided
989
+ const request = getEscalationRequest(escalation_request_id);
990
+ if (!request)
991
+ return;
992
+ const decisions = getDb().prepare(`
993
+ SELECT DISTINCT decision FROM escalation_decisions WHERE escalation_request_id = ?
994
+ `).all(escalation_request_id);
995
+ const hasBlockDecision = decisions.some(d => d.decision === 'block');
996
+ const newStatus = hasBlockDecision ? 'blocked' : 'proceeded';
997
+ getDb().prepare(`
998
+ UPDATE escalation_requests
999
+ SET status = ?, decided_at = datetime('now'), decision = ?
1000
+ WHERE request_id = ?
1001
+ `).run(newStatus, newStatus, escalation_request_id);
1002
+ }
1003
+ function getEscalationDecisions(escalation_request_id) {
1004
+ return getDb().prepare(`
1005
+ SELECT * FROM escalation_decisions WHERE escalation_request_id = ? ORDER BY decided_at ASC
1006
+ `).all(escalation_request_id);
1007
+ }
1008
+ function getEscalationHistory(session_id) {
1009
+ return getDb().prepare(`
1010
+ SELECT * FROM escalation_requests WHERE session_id = ? ORDER BY escalation_triggered_at DESC
1011
+ `).all(session_id);
1012
+ }
444
1013
  exports.default = db;