@way_marks/server 0.4.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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @way_marks/server
2
+
3
+ MCP server and REST API backend for [Waymark](https://github.com/waymarks/waymark).
4
+
5
+ > **This package is installed automatically** by `@way_marks/cli`.
6
+ > End users should not install it directly.
7
+
8
+ ```bash
9
+ # Correct — install the CLI, which pulls in this package
10
+ npx @way_marks/cli init
11
+ ```
12
+
13
+ ---
14
+
15
+ ## What this package provides
16
+
17
+ Two long-running Node.js processes, both spawned by `waymark start`:
18
+
19
+ | Process | Entry point | What it does |
20
+ |---------|-------------|--------------|
21
+ | MCP server | `dist/mcp/server.js` | stdio MCP server — intercepts Claude Code tool calls, enforces policy, logs actions |
22
+ | API server | `dist/api/server.js` | HTTP server on port 3001 — serves dashboard UI and REST API |
23
+
24
+ Both processes share the same SQLite database (`<project-root>/.waymark/waymark.db`). The MCP process writes; the API process reads. All database calls are synchronous (`better-sqlite3`) so concurrent access is safe.
25
+
26
+ ---
27
+
28
+ ## Environment variables
29
+
30
+ | Variable | Required | Default | Description |
31
+ |----------|----------|---------|-------------|
32
+ | `WAYMARK_PROJECT_ROOT` | Yes | `process.cwd()` | Absolute path to the project being monitored. Sets where `waymark.config.json` and `data/waymark.db` are resolved. |
33
+ | `PORT` | No | `3001` | Port for the API + dashboard server. |
34
+ | `WAYMARK_SLACK_WEBHOOK_URL` | No | — | Incoming webhook URL for Slack notifications on pending actions. |
35
+ | `WAYMARK_SLACK_CHANNEL` | No | — | Slack channel to post to (e.g. `#engineering`). |
36
+ | `WAYMARK_BASE_URL` | No | `http://localhost:3001` | Base URL used in Slack message links back to the dashboard. |
37
+
38
+ ---
39
+
40
+ ## REST API
41
+
42
+ All endpoints are served by `dist/api/server.js` on port 3001.
43
+
44
+ | Method | Path | Description |
45
+ |--------|------|-------------|
46
+ | `GET` | `/api/actions` | List all logged actions. Query: `?count=true` returns `{ count: N }` for pending actions only. |
47
+ | `GET` | `/api/actions/:id` | Get a single action by ID. |
48
+ | `GET` | `/api/actions/:id/status` | Get the current status of an action (`pending`, `approved`, `rejected`, `blocked`, `allowed`). |
49
+ | `POST` | `/api/actions/:id/approve` | Approve a pending action — executes the held write and marks it approved. |
50
+ | `POST` | `/api/actions/:id/reject` | Reject a pending action — marks it rejected, action is not executed. |
51
+ | `POST` | `/api/actions/:id/rollback` | Roll back an approved write_file action to its before-snapshot. |
52
+ | `POST` | `/api/slack/interact` | Slack interactive component handler (approve/reject from Slack message buttons). |
53
+ | `GET` | `/api/sessions` | List all session IDs that have logged actions. |
54
+ | `GET` | `/api/config` | Return the current parsed `waymark.config.json` for the active project. |
55
+ | `GET` | `*` | Catch-all — serves the dashboard `index.html`. |
56
+
57
+ ---
58
+
59
+ ## MCP tools
60
+
61
+ The MCP server exposes three tools to Claude Code:
62
+
63
+ | Tool | Replaces | Description |
64
+ |------|----------|-------------|
65
+ | `waymark:write_file` | `write_file` | Write a file — subject to policy before execution. |
66
+ | `waymark:read_file` | `read_file` | Read a file — always logged. |
67
+ | `waymark:bash` | `bash` | Run a shell command — checked against `blockedCommands` before execution. |
68
+
69
+ ---
70
+
71
+ ## License
72
+
73
+ MIT — see [LICENSE](https://github.com/waymarks/waymark/blob/main/LICENSE)
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ require("dotenv/config");
40
+ const express_1 = __importDefault(require("express"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const database_1 = require("../db/database");
44
+ const engine_1 = require("../policies/engine");
45
+ const handler_1 = require("../approvals/handler");
46
+ const app = (0, express_1.default)();
47
+ const PORT = parseInt(process.env.WAYMARK_PORT || '3001', 10);
48
+ app.use(express_1.default.json());
49
+ app.use(express_1.default.urlencoded({ extended: true }));
50
+ // Serve UI — path works for both ts-node (src/api/) and compiled (dist/api/)
51
+ const UI_DIR = path.resolve(__dirname, '../../src/ui');
52
+ app.use(express_1.default.static(UI_DIR));
53
+ // GET /api/actions — list all actions (or ?count=true for pending count)
54
+ app.get('/api/actions', (req, res) => {
55
+ try {
56
+ if (req.query.count === 'true') {
57
+ return res.json({ count: (0, database_1.getPendingCount)() });
58
+ }
59
+ const actions = (0, database_1.getActions)();
60
+ res.json(actions);
61
+ }
62
+ catch (err) {
63
+ res.status(500).json({ error: err.message });
64
+ }
65
+ });
66
+ // POST /api/actions/:action_id/approve
67
+ app.post('/api/actions/:action_id/approve', async (req, res) => {
68
+ try {
69
+ const result = await (0, handler_1.approvePendingAction)(req.params.action_id, 'ui');
70
+ if (!result.success) {
71
+ const status = result.error === 'Action not found' ? 404 : 400;
72
+ return res.status(status).json({ error: result.error });
73
+ }
74
+ res.json(result);
75
+ }
76
+ catch (err) {
77
+ res.status(500).json({ error: err.message });
78
+ }
79
+ });
80
+ // POST /api/actions/:action_id/reject
81
+ app.post('/api/actions/:action_id/reject', async (req, res) => {
82
+ try {
83
+ const reason = req.body?.reason || 'Rejected';
84
+ const result = await (0, handler_1.rejectPendingAction)(req.params.action_id, reason);
85
+ if (!result.success) {
86
+ const status = result.error === 'Action not found' ? 404 : 400;
87
+ return res.status(status).json({ error: result.error });
88
+ }
89
+ res.json(result);
90
+ }
91
+ catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+ // GET /api/actions/:action_id/status — lightweight status for agent polling
96
+ app.get('/api/actions/:action_id/status', (req, res) => {
97
+ try {
98
+ const action = (0, database_1.getAction)(req.params.action_id);
99
+ if (!action) {
100
+ return res.status(404).json({ error: 'Action not found' });
101
+ }
102
+ res.json({
103
+ status: action.status,
104
+ decision: action.decision,
105
+ approved_by: action.approved_by,
106
+ approved_at: action.approved_at,
107
+ rejected_reason: action.rejected_reason,
108
+ rejected_at: action.rejected_at,
109
+ });
110
+ }
111
+ catch (err) {
112
+ res.status(500).json({ error: err.message });
113
+ }
114
+ });
115
+ // GET /api/actions/:action_id — single action
116
+ app.get('/api/actions/:action_id', (req, res) => {
117
+ try {
118
+ const action = (0, database_1.getAction)(req.params.action_id);
119
+ if (!action) {
120
+ return res.status(404).json({ error: 'Action not found' });
121
+ }
122
+ res.json(action);
123
+ }
124
+ catch (err) {
125
+ res.status(500).json({ error: err.message });
126
+ }
127
+ });
128
+ // POST /api/actions/:action_id/rollback
129
+ app.post('/api/actions/:action_id/rollback', (req, res) => {
130
+ try {
131
+ const action = (0, database_1.getAction)(req.params.action_id);
132
+ if (!action) {
133
+ return res.status(404).json({ error: 'Action not found' });
134
+ }
135
+ if (action.tool_name !== 'write_file') {
136
+ return res.status(400).json({ error: 'Rollback only supported for write_file actions' });
137
+ }
138
+ if (action.rolled_back) {
139
+ return res.status(400).json({ error: 'Action already rolled back' });
140
+ }
141
+ if (!action.target_path) {
142
+ return res.status(400).json({ error: 'No target path on this action' });
143
+ }
144
+ // Bug 3: if no before_snapshot, file was newly created — delete it
145
+ if (!action.before_snapshot) {
146
+ fs.unlinkSync(action.target_path);
147
+ (0, database_1.markRolledBack)(action.action_id);
148
+ return res.json({ success: true, action: 'deleted', message: `Deleted new file: ${action.target_path}` });
149
+ }
150
+ // Restore file to before_snapshot
151
+ fs.mkdirSync(path.dirname(action.target_path), { recursive: true });
152
+ fs.writeFileSync(action.target_path, action.before_snapshot, 'utf8');
153
+ (0, database_1.markRolledBack)(action.action_id);
154
+ res.json({ success: true, action: 'restored', message: `Restored ${action.target_path} to previous state` });
155
+ }
156
+ catch (err) {
157
+ res.status(500).json({ error: err.message });
158
+ }
159
+ });
160
+ // POST /api/slack/interact — Slack interactive components (button clicks)
161
+ // For local development: use ngrok or similar to expose this endpoint publicly: ngrok http 3001
162
+ app.post('/api/slack/interact', async (req, res) => {
163
+ let payload;
164
+ try {
165
+ payload = JSON.parse(req.body.payload);
166
+ }
167
+ catch (err) {
168
+ console.error('Slack interact parse error:', err);
169
+ return res.status(400).json({ error: 'Invalid payload format' });
170
+ }
171
+ try {
172
+ if (!payload?.actions?.[0]) {
173
+ return res.status(400).json({ error: 'No actions in payload' });
174
+ }
175
+ const slackAction = payload.actions[0];
176
+ const actionId = slackAction.action_id;
177
+ const actionValue = slackAction.value; // waymark action_id
178
+ if (actionId === 'waymark_approve') {
179
+ const result = await (0, handler_1.approvePendingAction)(actionValue, 'slack');
180
+ return res.json({ text: result.success ? '✅ Approved by slack' : `❌ Error: ${result.error}` });
181
+ }
182
+ if (actionId === 'waymark_reject') {
183
+ const result = await (0, handler_1.rejectPendingAction)(actionValue, 'Rejected via Slack');
184
+ return res.json({ text: result.success ? '❌ Rejected by slack' : `❌ Error: ${result.error}` });
185
+ }
186
+ res.status(400).json({ error: `Unknown action_id: ${actionId}` });
187
+ }
188
+ catch (err) {
189
+ res.status(500).json({ error: err.message });
190
+ }
191
+ });
192
+ // GET /api/sessions
193
+ app.get('/api/sessions', (req, res) => {
194
+ try {
195
+ const sessions = (0, database_1.getSessions)();
196
+ res.json(sessions);
197
+ }
198
+ catch (err) {
199
+ res.status(500).json({ error: err.message });
200
+ }
201
+ });
202
+ // GET /api/config
203
+ app.get('/api/config', (req, res) => {
204
+ try {
205
+ res.json((0, engine_1.loadConfig)());
206
+ }
207
+ catch (err) {
208
+ res.status(500).json({ error: err.message });
209
+ }
210
+ });
211
+ // Fallback: serve UI for any unmatched route
212
+ app.get('*', (req, res) => {
213
+ res.sendFile(path.join(UI_DIR, 'index.html'));
214
+ });
215
+ app.listen(PORT, () => {
216
+ console.log(`Waymark UI + API running at http://localhost:${PORT}`);
217
+ });
218
+ exports.default = app;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.approvePendingAction = approvePendingAction;
37
+ exports.rejectPendingAction = rejectPendingAction;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const database_1 = require("../db/database");
41
+ const engine_1 = require("../policies/engine");
42
+ async function approvePendingAction(actionId, approvedBy = 'ui') {
43
+ const action = (0, database_1.getAction)(actionId);
44
+ if (!action) {
45
+ return { success: false, error: 'Action not found' };
46
+ }
47
+ if (action.status !== 'pending') {
48
+ return { success: false, error: `Action is not pending (current status: ${action.status})` };
49
+ }
50
+ let after_snapshot;
51
+ if (action.tool_name === 'write_file') {
52
+ const { path: filePath, content } = JSON.parse(action.input_payload);
53
+ const resolvedPath = path.resolve(filePath);
54
+ // Re-check current policies — they may have changed since the action was queued
55
+ const currentConfig = (0, engine_1.loadConfig)();
56
+ const recheck = (0, engine_1.checkFileAction)(resolvedPath, 'write', currentConfig);
57
+ if (recheck.decision === 'block') {
58
+ return { success: false, error: `Approval blocked: policy changed (${recheck.reason})` };
59
+ }
60
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
61
+ fs.writeFileSync(resolvedPath, content, 'utf8');
62
+ after_snapshot = content;
63
+ }
64
+ else if (action.tool_name === 'read_file') {
65
+ // read_file: no re-execution needed, just mark approved
66
+ // (the file read already happened conceptually; approval means "yes, this was ok")
67
+ }
68
+ else {
69
+ return { success: false, error: `Unsupported tool for approval: ${action.tool_name}` };
70
+ }
71
+ (0, database_1.approveAction)(actionId, approvedBy, after_snapshot);
72
+ return { success: true, action: actionId };
73
+ }
74
+ async function rejectPendingAction(actionId, reason) {
75
+ const action = (0, database_1.getAction)(actionId);
76
+ if (!action) {
77
+ return { success: false, error: 'Action not found' };
78
+ }
79
+ if (action.status !== 'pending') {
80
+ return { success: false, error: `Action is not pending (current status: ${action.status})` };
81
+ }
82
+ (0, database_1.rejectAction)(actionId, reason);
83
+ return { success: true, action: actionId };
84
+ }
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.insertAction = insertAction;
40
+ exports.updateAction = updateAction;
41
+ exports.getActions = getActions;
42
+ exports.getAction = getAction;
43
+ exports.markRolledBack = markRolledBack;
44
+ exports.getSessions = getSessions;
45
+ exports.approveAction = approveAction;
46
+ exports.rejectAction = rejectAction;
47
+ exports.getPendingCount = getPendingCount;
48
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
52
+ const DB_PATH = process.env.WAYMARK_DB_PATH
53
+ || path.join(PROJECT_ROOT, '.waymark', 'waymark.db');
54
+ const DB_DIR = path.dirname(DB_PATH);
55
+ // Ensure database directory exists
56
+ if (!fs.existsSync(DB_DIR)) {
57
+ fs.mkdirSync(DB_DIR, { recursive: true });
58
+ }
59
+ const db = new better_sqlite3_1.default(DB_PATH);
60
+ // Create table on startup
61
+ db.exec(`
62
+ CREATE TABLE IF NOT EXISTS action_log (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ action_id TEXT UNIQUE NOT NULL,
65
+ session_id TEXT NOT NULL,
66
+ tool_name TEXT NOT NULL,
67
+ target_path TEXT,
68
+ input_payload TEXT NOT NULL,
69
+ before_snapshot TEXT,
70
+ after_snapshot TEXT,
71
+ status TEXT NOT NULL DEFAULT 'pending',
72
+ error_message TEXT,
73
+ stdout TEXT,
74
+ stderr TEXT,
75
+ rolled_back INTEGER NOT NULL DEFAULT 0,
76
+ rolled_back_at TEXT,
77
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
78
+ )
79
+ `);
80
+ // Migrate existing DBs — add stdout/stderr if not present
81
+ try {
82
+ db.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
83
+ }
84
+ catch { }
85
+ try {
86
+ db.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
87
+ }
88
+ catch { }
89
+ // Migrate v2: policy engine columns
90
+ try {
91
+ db.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
92
+ }
93
+ catch { }
94
+ try {
95
+ db.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
96
+ }
97
+ catch { }
98
+ try {
99
+ db.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
100
+ }
101
+ catch { }
102
+ // Migrate v3: approval flow columns
103
+ try {
104
+ db.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
105
+ }
106
+ catch { }
107
+ try {
108
+ db.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
109
+ }
110
+ catch { }
111
+ try {
112
+ db.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
113
+ }
114
+ catch { }
115
+ try {
116
+ db.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
117
+ }
118
+ catch { }
119
+ // Indexes for query performance
120
+ try {
121
+ db.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
122
+ }
123
+ catch { }
124
+ try {
125
+ db.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
126
+ }
127
+ catch { }
128
+ try {
129
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
130
+ }
131
+ catch { }
132
+ const insertStmt = db.prepare(`
133
+ INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule)
134
+ VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule)
135
+ `);
136
+ const updateStmt = db.prepare(`
137
+ UPDATE action_log
138
+ SET status = COALESCE(@status, status),
139
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot),
140
+ error_message = COALESCE(@error_message, error_message),
141
+ stdout = COALESCE(@stdout, stdout),
142
+ stderr = COALESCE(@stderr, stderr)
143
+ WHERE action_id = @action_id
144
+ `);
145
+ function insertAction(params) {
146
+ insertStmt.run({
147
+ action_id: params.action_id,
148
+ session_id: params.session_id,
149
+ tool_name: params.tool_name,
150
+ target_path: params.target_path ?? null,
151
+ input_payload: params.input_payload,
152
+ before_snapshot: params.before_snapshot ?? null,
153
+ status: params.status,
154
+ decision: params.decision ?? 'pending',
155
+ policy_reason: params.policy_reason ?? null,
156
+ matched_rule: params.matched_rule ?? null,
157
+ });
158
+ }
159
+ function updateAction(action_id, params) {
160
+ updateStmt.run({
161
+ action_id,
162
+ status: params.status ?? null,
163
+ after_snapshot: params.after_snapshot ?? null,
164
+ error_message: params.error_message ?? null,
165
+ stdout: params.stdout ?? null,
166
+ stderr: params.stderr ?? null,
167
+ });
168
+ }
169
+ function getActions() {
170
+ return db.prepare(`
171
+ SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
172
+ `).all();
173
+ }
174
+ function getAction(action_id) {
175
+ return db.prepare(`
176
+ SELECT * FROM action_log WHERE action_id = ?
177
+ `).get(action_id);
178
+ }
179
+ function markRolledBack(action_id) {
180
+ db.prepare(`
181
+ UPDATE action_log
182
+ SET rolled_back = 1, rolled_back_at = datetime('now')
183
+ WHERE action_id = ?
184
+ `).run(action_id);
185
+ }
186
+ function getSessions() {
187
+ return db.prepare(`
188
+ SELECT session_id,
189
+ COUNT(*) as action_count,
190
+ MAX(created_at) as latest
191
+ FROM action_log
192
+ GROUP BY session_id
193
+ ORDER BY latest DESC
194
+ `).all();
195
+ }
196
+ function approveAction(action_id, approved_by, after_snapshot) {
197
+ db.prepare(`
198
+ UPDATE action_log
199
+ SET status = 'success',
200
+ decision = 'allow',
201
+ approved_at = datetime('now'),
202
+ approved_by = @approved_by,
203
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot)
204
+ WHERE action_id = @action_id
205
+ `).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
206
+ }
207
+ function rejectAction(action_id, reason) {
208
+ db.prepare(`
209
+ UPDATE action_log
210
+ SET status = 'rejected',
211
+ decision = 'rejected',
212
+ rejected_at = datetime('now'),
213
+ rejected_reason = @reason
214
+ WHERE action_id = @action_id
215
+ `).run({ action_id, reason });
216
+ }
217
+ function getPendingCount() {
218
+ const row = db.prepare(`
219
+ SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
220
+ `).get();
221
+ return row.count;
222
+ }
223
+ exports.default = db;