sqlite-hub 1.4.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.
Files changed (46) hide show
  1. package/README.md +14 -1
  2. package/bin/sqlite-hub-mcp.js +8 -0
  3. package/bin/sqlite-hub.js +555 -122
  4. package/docs/API.md +30 -0
  5. package/docs/CLI.md +22 -0
  6. package/docs/CLI_API_PARITY.md +61 -0
  7. package/docs/MCP.md +85 -0
  8. package/docs/changelog.md +13 -1
  9. package/docs/guidelines/AGENTS.md +32 -0
  10. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  11. package/docs/todo.md +1 -14
  12. package/frontend/js/api.js +49 -0
  13. package/frontend/js/app.js +202 -2
  14. package/frontend/js/components/connectionCard.js +3 -1
  15. package/frontend/js/components/modal.js +356 -15
  16. package/frontend/js/components/sidebar.js +41 -7
  17. package/frontend/js/components/topNav.js +2 -14
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +483 -7
  20. package/frontend/js/utils/inputClear.js +36 -0
  21. package/frontend/js/utils/syntheticData.js +240 -0
  22. package/frontend/js/views/backups.js +52 -0
  23. package/frontend/js/views/data.js +16 -0
  24. package/frontend/js/views/logs.js +339 -0
  25. package/frontend/js/views/settings.js +266 -30
  26. package/frontend/js/views/structure.js +6 -0
  27. package/frontend/js/views/tableAdvisor.js +385 -0
  28. package/frontend/js/views/tableDesigner.js +6 -0
  29. package/frontend/styles/components.css +149 -0
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/views.css +31 -0
  32. package/package.json +4 -2
  33. package/server/mcp/stdioServer.js +272 -0
  34. package/server/routes/data.js +45 -0
  35. package/server/routes/externalApi.js +285 -2
  36. package/server/routes/logs.js +127 -0
  37. package/server/routes/settings.js +47 -0
  38. package/server/server.js +3 -0
  39. package/server/services/databaseCommandService.js +284 -2
  40. package/server/services/mcpStatusService.js +140 -0
  41. package/server/services/mcpToolService.js +274 -0
  42. package/server/services/sqlite/dataBrowserService.js +36 -0
  43. package/server/services/sqlite/introspection.js +226 -22
  44. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  45. package/server/services/sqlite/tableAdvisor.js +790 -0
  46. package/server/services/storage/appStateStore.js +821 -169
@@ -29,6 +29,232 @@ function readStoreName(req) {
29
29
  ).trim();
30
30
  }
31
31
 
32
+ function decodePathPart(value = "") {
33
+ try {
34
+ return decodeURIComponent(value);
35
+ } catch {
36
+ return value;
37
+ }
38
+ }
39
+
40
+ function readPathParts(req) {
41
+ return String(req.path ?? "")
42
+ .split("/")
43
+ .map((part) => part.trim())
44
+ .filter(Boolean)
45
+ .map(decodePathPart);
46
+ }
47
+
48
+ function buildExternalApiAccessDescriptor(req) {
49
+ const method = String(req.method ?? "GET").toUpperCase();
50
+ const parts = readPathParts(req);
51
+
52
+ if (parts[0] === "info") {
53
+ return {
54
+ action: "api.info.get",
55
+ targetType: "app",
56
+ targetName: "info",
57
+ };
58
+ }
59
+
60
+ if (parts[0] === "query") {
61
+ return {
62
+ action: "api.query.execute",
63
+ databaseKey: readDatabaseId(req),
64
+ targetType: "query",
65
+ targetName: readStoreName(req) || "raw query",
66
+ };
67
+ }
68
+
69
+ if (parts[0] !== "databases") {
70
+ return {
71
+ action: `api.${method.toLowerCase()}`,
72
+ targetType: "request",
73
+ targetName: req.path,
74
+ };
75
+ }
76
+
77
+ const databaseKey = parts[1] ?? "";
78
+ const collection = parts[2] ?? "";
79
+ const itemName = parts[3] ?? "";
80
+ const subAction = parts[4] ?? "";
81
+
82
+ if (!collection) {
83
+ return {
84
+ action: "api.database.get",
85
+ databaseKey,
86
+ targetType: "database",
87
+ targetName: databaseKey,
88
+ };
89
+ }
90
+
91
+ if (collection === "tables") {
92
+ if (!itemName) {
93
+ return {
94
+ action: "api.tables.list",
95
+ databaseKey,
96
+ targetType: "database",
97
+ targetName: databaseKey,
98
+ };
99
+ }
100
+
101
+ if (subAction === "row") {
102
+ return {
103
+ action: "api.table.row.export",
104
+ databaseKey,
105
+ targetType: "table",
106
+ targetName: itemName,
107
+ };
108
+ }
109
+
110
+ if (subAction === "types") {
111
+ return {
112
+ action: "api.table.types.generate",
113
+ databaseKey,
114
+ targetType: "table",
115
+ targetName: itemName,
116
+ };
117
+ }
118
+
119
+ return {
120
+ action: "api.table.get",
121
+ databaseKey,
122
+ targetType: "table",
123
+ targetName: itemName,
124
+ };
125
+ }
126
+
127
+ if (collection === "backups") {
128
+ return {
129
+ action: method === "GET" ? "api.backups.list" : "api.backup.create",
130
+ databaseKey,
131
+ targetType: "database",
132
+ targetName: databaseKey,
133
+ };
134
+ }
135
+
136
+ if (collection === "queries") {
137
+ if (!itemName) {
138
+ return {
139
+ action: "api.queries.list",
140
+ databaseKey,
141
+ targetType: "database",
142
+ targetName: databaseKey,
143
+ };
144
+ }
145
+
146
+ if (subAction === "execute") {
147
+ return {
148
+ action: "api.query.execute.saved",
149
+ databaseKey,
150
+ targetType: "query",
151
+ targetName: itemName,
152
+ };
153
+ }
154
+
155
+ if (subAction === "export") {
156
+ return {
157
+ action: "api.query.export",
158
+ databaseKey,
159
+ targetType: "query",
160
+ targetName: itemName,
161
+ };
162
+ }
163
+
164
+ if (subAction === "notes") {
165
+ return {
166
+ action: "api.query.notes.get",
167
+ databaseKey,
168
+ targetType: "query",
169
+ targetName: itemName,
170
+ };
171
+ }
172
+
173
+ return {
174
+ action: "api.query.get",
175
+ databaseKey,
176
+ targetType: "query",
177
+ targetName: itemName,
178
+ };
179
+ }
180
+
181
+ if (collection === "documents") {
182
+ if (!itemName) {
183
+ return {
184
+ action: "api.documents.list",
185
+ databaseKey,
186
+ targetType: "database",
187
+ targetName: databaseKey,
188
+ };
189
+ }
190
+
191
+ if (subAction === "export") {
192
+ return {
193
+ action: "api.document.export",
194
+ databaseKey,
195
+ targetType: "document",
196
+ targetName: itemName,
197
+ };
198
+ }
199
+
200
+ return {
201
+ action: "api.document.get",
202
+ databaseKey,
203
+ targetType: "document",
204
+ targetName: itemName,
205
+ };
206
+ }
207
+
208
+ return {
209
+ action: `api.${collection}.${method.toLowerCase()}`,
210
+ databaseKey,
211
+ targetType: collection,
212
+ targetName: itemName || databaseKey,
213
+ };
214
+ }
215
+
216
+ function createExternalApiAccessLogger(appStateStore) {
217
+ return (req, res, next) => {
218
+ const startedAtMs = Date.now();
219
+ const startedAt = new Date(startedAtMs).toISOString();
220
+
221
+ res.on("finish", () => {
222
+ if (!appStateStore?.recordAccessLog) {
223
+ return;
224
+ }
225
+
226
+ const descriptor = buildExternalApiAccessDescriptor(req);
227
+ const failed = res.statusCode >= 400;
228
+
229
+ try {
230
+ appStateStore.recordAccessLog({
231
+ source: "api",
232
+ action: descriptor.action,
233
+ databaseKey: descriptor.databaseKey,
234
+ targetType: descriptor.targetType,
235
+ targetName: descriptor.targetName,
236
+ status: failed ? "error" : "success",
237
+ startedAt,
238
+ durationMs: Date.now() - startedAtMs,
239
+ errorMessage: failed ? req.accessLogError || `HTTP ${res.statusCode}` : null,
240
+ metadata: {
241
+ method: req.method,
242
+ path: req.path,
243
+ route: req.route?.path ? String(req.route.path) : null,
244
+ statusCode: res.statusCode,
245
+ apiTokenId: req.apiToken?.id ?? null,
246
+ apiTokenName: req.apiToken?.name ?? null,
247
+ },
248
+ });
249
+ } catch {
250
+ // Access logging must not change API behavior.
251
+ }
252
+ });
253
+
254
+ next();
255
+ };
256
+ }
257
+
32
258
  function authenticateDatabaseRequest(req, tokenService, databaseId) {
33
259
  const token = readBearerToken(req.get("authorization"));
34
260
 
@@ -38,12 +264,24 @@ function authenticateDatabaseRequest(req, tokenService, databaseId) {
38
264
  });
39
265
  }
40
266
 
41
- return tokenService.authenticate(databaseId, token);
267
+ const apiToken = tokenService.authenticate(databaseId, token);
268
+
269
+ req.apiToken = apiToken;
270
+ return apiToken;
42
271
  }
43
272
 
44
- function createExternalApiRouter({ databaseService, tokenService, appInfoService = buildAppInfo }) {
273
+ function createExternalApiRouter({
274
+ databaseService,
275
+ tokenService,
276
+ appStateStore = null,
277
+ appInfoService = buildAppInfo,
278
+ }) {
45
279
  const router = express.Router();
46
280
 
281
+ if (appStateStore?.recordAccessLog) {
282
+ router.use(createExternalApiAccessLogger(appStateStore));
283
+ }
284
+
47
285
  router.get(
48
286
  "/info",
49
287
  route(async (req, res) => {
@@ -131,6 +369,46 @@ function createExternalApiRouter({ databaseService, tokenService, appInfoService
131
369
  })
132
370
  );
133
371
 
372
+ router.get(
373
+ "/databases/:databaseId/backups",
374
+ route((req, res) => {
375
+ const backups = databaseService.listBackups(req.params.databaseId);
376
+
377
+ res.json(
378
+ successResponse({
379
+ data: {
380
+ items: backups,
381
+ },
382
+ metadata: {
383
+ databaseId: req.params.databaseId,
384
+ total: backups.length,
385
+ },
386
+ })
387
+ );
388
+ })
389
+ );
390
+
391
+ router.post(
392
+ "/databases/:databaseId/backups",
393
+ route(async (req, res) => {
394
+ const backup = await databaseService.createBackup(req.params.databaseId, {
395
+ name: req.body?.name,
396
+ notes: req.body?.notes,
397
+ context: req.body?.context ?? "api",
398
+ });
399
+
400
+ res.json(
401
+ successResponse({
402
+ message: "Backup created and verified.",
403
+ data: backup,
404
+ metadata: {
405
+ databaseId: req.params.databaseId,
406
+ },
407
+ })
408
+ );
409
+ })
410
+ );
411
+
134
412
  router.post(
135
413
  "/databases/:databaseId/tables/:tableName/row",
136
414
  route((req, res) => {
@@ -336,6 +614,11 @@ function createExternalApiRouter({ databaseService, tokenService, appInfoService
336
614
  })
337
615
  );
338
616
 
617
+ router.use((error, req, res, next) => {
618
+ req.accessLogError = error?.message ?? null;
619
+ next(error);
620
+ });
621
+
339
622
  return router;
340
623
  }
341
624
 
@@ -0,0 +1,127 @@
1
+ const express = require("express");
2
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
+
4
+ const RANGE_MS = {
5
+ "1h": 60 * 60 * 1000,
6
+ "24h": 24 * 60 * 60 * 1000,
7
+ "7d": 7 * 24 * 60 * 60 * 1000,
8
+ "30d": 30 * 24 * 60 * 60 * 1000,
9
+ };
10
+
11
+ function normalizeOption(value, allowedValues, fallback = "all") {
12
+ const normalized = String(value ?? fallback).trim().toLowerCase();
13
+ return allowedValues.includes(normalized) ? normalized : fallback;
14
+ }
15
+
16
+ function parseLimit(value) {
17
+ const numeric = Number(value);
18
+ return Number.isInteger(numeric) && numeric > 0 ? Math.min(numeric, 200) : 100;
19
+ }
20
+
21
+ function parseOffset(value) {
22
+ const numeric = Number(value);
23
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : 0;
24
+ }
25
+
26
+ function normalizeTimestamp(value) {
27
+ const normalized = String(value ?? "").trim();
28
+
29
+ if (!normalized) {
30
+ return null;
31
+ }
32
+
33
+ const timestamp = new Date(normalized).getTime();
34
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
35
+ }
36
+
37
+ function resolveTimeWindow(query = {}, now = new Date()) {
38
+ const explicitFrom = normalizeTimestamp(query.from);
39
+ const explicitTo = normalizeTimestamp(query.to);
40
+ const range = normalizeOption(query.range, ["1h", "24h", "7d", "30d", "all"], "all");
41
+
42
+ if (explicitFrom || explicitTo) {
43
+ return {
44
+ range,
45
+ from: explicitFrom,
46
+ to: explicitTo,
47
+ };
48
+ }
49
+
50
+ if (range === "all") {
51
+ return {
52
+ range,
53
+ from: null,
54
+ to: null,
55
+ };
56
+ }
57
+
58
+ return {
59
+ range,
60
+ from: new Date(now.getTime() - RANGE_MS[range]).toISOString(),
61
+ to: now.toISOString(),
62
+ };
63
+ }
64
+
65
+ function resolveActiveDatabase(connectionManager) {
66
+ const activeDatabase = connectionManager.getActiveConnection() ?? null;
67
+
68
+ if (!activeDatabase?.id) {
69
+ throw new DatabaseRequiredError();
70
+ }
71
+
72
+ return activeDatabase;
73
+ }
74
+
75
+ function createLogsRouter({ appStateStore, connectionManager, now = () => new Date() }) {
76
+ const router = express.Router();
77
+
78
+ router.get(
79
+ "/",
80
+ route((req, res) => {
81
+ const timeWindow = resolveTimeWindow(req.query, now());
82
+ const activeDatabase = resolveActiveDatabase(connectionManager);
83
+ const kind = normalizeOption(req.query.kind, ["all", "query", "access"], "all");
84
+ const actor = normalizeOption(req.query.actor, ["all", "user", "cli", "api", "mcp"], "all");
85
+ const status = normalizeOption(req.query.status, ["all", "success", "error"], "all");
86
+ const queryType = normalizeOption(
87
+ req.query.queryType,
88
+ ["all", "select", "insert", "update", "delete", "pragma", "create", "alter", "drop", "other"],
89
+ "all"
90
+ );
91
+ const destructive = normalizeOption(req.query.destructive, ["all", "yes", "no"], "all");
92
+ const search = String(req.query.search ?? "").trim();
93
+ const result = appStateStore.listActivityLogs({
94
+ kind,
95
+ actor: actor === "all" ? null : actor,
96
+ status: status === "all" ? null : status,
97
+ databaseKey: activeDatabase.id,
98
+ queryType: queryType === "all" ? null : queryType,
99
+ destructive,
100
+ from: timeWindow.from,
101
+ to: timeWindow.to,
102
+ search,
103
+ limit: parseLimit(req.query.limit),
104
+ offset: parseOffset(req.query.offset),
105
+ });
106
+
107
+ res.json(
108
+ successResponse({
109
+ data: result,
110
+ metadata: {
111
+ ...result.filters,
112
+ range: timeWindow.range,
113
+ activeDatabase,
114
+ },
115
+ })
116
+ );
117
+ })
118
+ );
119
+
120
+ return router;
121
+ }
122
+
123
+ module.exports = {
124
+ createLogsRouter,
125
+ resolveActiveDatabase,
126
+ resolveTimeWindow,
127
+ };
@@ -1,4 +1,5 @@
1
1
  const express = require("express");
2
+ const path = require("node:path");
2
3
  const { AppError, DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
4
  const {
4
5
  checkLatestAppVersion,
@@ -7,6 +8,8 @@ const {
7
8
  readSettingsMetadata,
8
9
  readSqliteVersion,
9
10
  } = require("../services/appInfoService");
11
+ const { McpStatusService } = require("../services/mcpStatusService");
12
+ const { MCP_TOOL_DEFINITIONS } = require("../services/mcpToolService");
10
13
 
11
14
  function getActiveTokenContext({ connectionManager, tokenService }) {
12
15
  const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
@@ -34,6 +37,37 @@ function buildSettingsMetadata(context) {
34
37
  };
35
38
  }
36
39
 
40
+ function buildMcpCodexConfig() {
41
+ const serverPath = path.resolve(__dirname, "../../bin/sqlite-hub-mcp.js");
42
+
43
+ return [
44
+ "[mcp_servers.sqlitehub]",
45
+ 'command = "node"',
46
+ `args = [${JSON.stringify(serverPath)}]`,
47
+ "startup_timeout_sec = 10",
48
+ "tool_timeout_sec = 60",
49
+ ].join("\n");
50
+ }
51
+
52
+ function buildMcpSettingsStatus(appStateStore) {
53
+ const statusService = new McpStatusService({
54
+ appStateStore,
55
+ exposedTools: MCP_TOOL_DEFINITIONS,
56
+ transport: "stdio",
57
+ });
58
+ const status = statusService.getStatus();
59
+
60
+ return {
61
+ ...status,
62
+ toolDetails: MCP_TOOL_DEFINITIONS.map((tool) => ({
63
+ name: tool.name,
64
+ description: tool.description,
65
+ })),
66
+ command: `node ${path.resolve(__dirname, "../../bin/sqlite-hub-mcp.js")}`,
67
+ codexConfig: buildMcpCodexConfig(),
68
+ };
69
+ }
70
+
37
71
  function createSettingsRouter({ appStateStore, connectionManager, tokenService, versionCheckService }) {
38
72
  const router = express.Router();
39
73
  const context = { connectionManager, tokenService };
@@ -89,6 +123,17 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
89
123
  })
90
124
  );
91
125
 
126
+ router.get(
127
+ "/mcp",
128
+ route((req, res) => {
129
+ res.json(
130
+ successResponse({
131
+ data: buildMcpSettingsStatus(appStateStore),
132
+ })
133
+ );
134
+ })
135
+ );
136
+
92
137
  router.post(
93
138
  "/api-tokens",
94
139
  route((req, res) => {
@@ -126,6 +171,8 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
126
171
 
127
172
  module.exports = {
128
173
  createSettingsRouter,
174
+ buildMcpCodexConfig,
175
+ buildMcpSettingsStatus,
129
176
  buildSettingsMetadata,
130
177
  checkLatestAppVersion,
131
178
  compareSemver,
package/server/server.js CHANGED
@@ -36,6 +36,7 @@ const { createSettingsRouter } = require("./routes/settings");
36
36
  const { createExportRouter } = require("./routes/export");
37
37
  const { createDocumentsRouter } = require("./routes/documents");
38
38
  const { createExternalApiRouter } = require("./routes/externalApi");
39
+ const { createLogsRouter } = require("./routes/logs");
39
40
 
40
41
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
41
42
  const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
@@ -133,11 +134,13 @@ app.use(
133
134
  );
134
135
  app.use("/api/export", createExportRouter({ exportService }));
135
136
  app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
137
+ app.use("/api/logs", createLogsRouter({ appStateStore, connectionManager }));
136
138
  app.use(
137
139
  "/api/v1",
138
140
  createExternalApiRouter({
139
141
  databaseService: databaseCommandService,
140
142
  tokenService: apiTokenService,
143
+ appStateStore,
141
144
  })
142
145
  );
143
146