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
@@ -1,11 +1,12 @@
1
1
  import { renderPageHeader } from '../components/pageHeader.js';
2
2
  import { renderTextInput } from '../components/formControls.js';
3
- import { escapeHtml } from '../utils/format.js';
3
+ import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
4
4
 
5
5
  function renderSettingsNavigation(activeSection) {
6
6
  const items = [
7
7
  { id: 'information', icon: 'info', label: 'Information' },
8
8
  { id: 'api-tokens', icon: 'key', label: 'API Tokens' },
9
+ { id: 'mcp', icon: 'hub', label: 'MCP' },
9
10
  ];
10
11
 
11
12
  return `
@@ -90,6 +91,262 @@ function renderVersionCheckStatus(settings) {
90
91
  `;
91
92
  }
92
93
 
94
+ function renderMcpTimestamp(value) {
95
+ return value ? formatCompactDateTime(value) : 'Never';
96
+ }
97
+
98
+ function renderMcpStatusSummary(status) {
99
+ if (status?.error) {
100
+ return {
101
+ label: 'Server Error',
102
+ icon: 'error',
103
+ badgeClass: 'status-badge border-error/30 bg-error-container/20 text-error',
104
+ message: status.error,
105
+ };
106
+ }
107
+
108
+ if (status?.connected) {
109
+ return {
110
+ label: 'Agent Connected',
111
+ icon: 'check_circle',
112
+ badgeClass: 'status-badge status-badge--success',
113
+ message: `Last tool call: ${status.lastToolName || 'none'} at ${renderMcpTimestamp(status.lastToolCallAt)}`,
114
+ };
115
+ }
116
+
117
+ if (status?.enabled) {
118
+ return {
119
+ label: 'Waiting For Agent',
120
+ icon: 'radio_button_unchecked',
121
+ badgeClass: 'status-badge',
122
+ message: status.lastConnectedAt
123
+ ? `Last agent connection: ${renderMcpTimestamp(status.lastConnectedAt)}`
124
+ : 'No MCP agent connected yet.',
125
+ };
126
+ }
127
+
128
+ return {
129
+ label: 'Stopped',
130
+ icon: 'block',
131
+ badgeClass: 'status-badge',
132
+ message: 'No MCP agent connected yet.',
133
+ };
134
+ }
135
+
136
+ function renderMcpMetric(label, value) {
137
+ return `
138
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-3">
139
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
140
+ ${escapeHtml(label)}
141
+ </div>
142
+ <div class="mt-2 break-words font-mono text-sm text-on-surface">
143
+ ${escapeHtml(value)}
144
+ </div>
145
+ </div>
146
+ `;
147
+ }
148
+
149
+ function renderMcpSection(settings) {
150
+ if (settings.mcpStatusLoading && !settings.mcpStatus) {
151
+ return `
152
+ <div class="flex min-h-[240px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
153
+ <div class="text-center text-on-surface-variant/40">
154
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
155
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_MCP_STATUS</p>
156
+ </div>
157
+ </div>
158
+ `;
159
+ }
160
+
161
+ if (settings.mcpStatusError) {
162
+ return `
163
+ <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
164
+ <div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
165
+ ${escapeHtml(settings.mcpStatusError.code ?? 'MCP_STATUS_FAILED')}
166
+ </div>
167
+ <div class="mt-2">${escapeHtml(settings.mcpStatusError.message ?? 'MCP status could not be loaded.')}</div>
168
+ </div>
169
+ `;
170
+ }
171
+
172
+ const status = settings.mcpStatus ?? {};
173
+ const summary = renderMcpStatusSummary(status);
174
+ const tools = status.toolDetails?.length
175
+ ? status.toolDetails
176
+ : (status.exposedTools ?? []).map(name => ({ name, description: '' }));
177
+ const codexConfig = status.codexConfig ?? '';
178
+ const command = status.command ?? 'sqlite-hub-mcp';
179
+
180
+ return `
181
+ <div class="space-y-6">
182
+ <section class="shell-section overflow-hidden">
183
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
184
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">MCP Status</span>
185
+ <span class="material-symbols-outlined text-xs text-primary-container">hub</span>
186
+ </div>
187
+ <div class="space-y-5 p-6">
188
+ <div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
189
+ <div class="min-w-0">
190
+ <div class="flex items-center gap-3">
191
+ <span class="material-symbols-outlined text-base text-primary-container">${summary.icon}</span>
192
+ <span class="${summary.badgeClass}">${escapeHtml(summary.label)}</span>
193
+ </div>
194
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant">
195
+ ${escapeHtml(summary.message)}
196
+ </div>
197
+ </div>
198
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/60">
199
+ ${escapeHtml(status.transport ?? 'unknown')}
200
+ </div>
201
+ </div>
202
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-3">
203
+ ${renderMcpMetric('MCP Server', status.serverRunning ? 'Running' : 'Stopped')}
204
+ ${renderMcpMetric('Agent connected', status.connected ? 'Yes' : 'No')}
205
+ ${renderMcpMetric('Active clients', String(status.activeClientCount ?? 0))}
206
+ ${renderMcpMetric('Last connected', renderMcpTimestamp(status.lastConnectedAt))}
207
+ ${renderMcpMetric('Last disconnected', renderMcpTimestamp(status.lastDisconnectedAt))}
208
+ ${renderMcpMetric('Last tool call', status.lastToolCallAt ? renderMcpTimestamp(status.lastToolCallAt) : 'Never')}
209
+ ${renderMcpMetric('Last tool name', status.lastToolName ?? 'None')}
210
+ ${renderMcpMetric('Transport', status.transport ?? 'unknown')}
211
+ ${renderMcpMetric('Exposed tools', String(status.exposedTools?.length ?? tools.length))}
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ <section class="shell-section overflow-hidden">
217
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
218
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Tools</span>
219
+ <span class="material-symbols-outlined text-xs text-primary-container">construction</span>
220
+ </div>
221
+ <div class="grid grid-cols-1 gap-3 p-6 md:grid-cols-2">
222
+ ${tools
223
+ .map(
224
+ tool => `
225
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-3">
226
+ <div class="font-mono text-xs font-bold text-primary-container">${escapeHtml(tool.name)}</div>
227
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant">${escapeHtml(tool.description || 'SQLite Hub MCP tool.')}</div>
228
+ </div>
229
+ `,
230
+ )
231
+ .join('')}
232
+ </div>
233
+ </section>
234
+
235
+ <section class="shell-section overflow-hidden">
236
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
237
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Codex Setup</span>
238
+ <span class="material-symbols-outlined text-xs text-primary-container">terminal</span>
239
+ </div>
240
+ <div class="space-y-4 p-6">
241
+ <div>
242
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
243
+ Local_Command
244
+ </div>
245
+ <div class="mt-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 font-mono text-sm text-primary-container">
246
+ ${escapeHtml(command)}
247
+ </div>
248
+ </div>
249
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
250
+ <textarea class="settings-mcp-config-input custom-scrollbar control-input font-mono text-xs leading-6" readonly data-mcp-config>${escapeHtml(codexConfig)}</textarea>
251
+ <button class="standard-button self-start" data-action="copy-mcp-config" type="button">
252
+ <span class="material-symbols-outlined text-sm">content_copy</span>
253
+ Copy Config
254
+ </button>
255
+ </div>
256
+ </div>
257
+ </section>
258
+
259
+ <section class="shell-section overflow-hidden">
260
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
261
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Security</span>
262
+ <span class="material-symbols-outlined text-xs text-primary-container">shield</span>
263
+ </div>
264
+ <div class="space-y-2 p-6 text-sm leading-6 text-on-surface-variant">
265
+ <div>Read-only queries are limited to SELECT, PRAGMA, and EXPLAIN.</div>
266
+ <div>Mutating SQL statements are blocked server-side before execution.</div>
267
+ <div>Destructive actions must use existing SQLite Hub safety mechanisms such as verified backups.</div>
268
+ <div>Local stdio MCP is allowed because it runs on the same machine as SQLite Hub and does not expose network credentials.</div>
269
+ </div>
270
+ </section>
271
+ </div>
272
+ `;
273
+ }
274
+
275
+ function renderApiTokenRow(token, tokenSaving) {
276
+ const callCount = Number(token.callCount ?? 0);
277
+ const createdAt = token.createdAt ? formatCompactDateTime(token.createdAt) : 'Unknown';
278
+ const lastCallAt = token.lastCallAt ? formatCompactDateTime(token.lastCallAt) : 'Never';
279
+
280
+ return `
281
+ <div class="grid grid-cols-1 gap-4 border-t border-outline-variant/10 bg-surface-container-high px-4 py-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem] xl:items-center">
282
+ <div class="min-w-0">
283
+ <div class="truncate text-sm font-semibold text-on-surface" title="${escapeHtml(token.name)}">
284
+ ${escapeHtml(token.name)}
285
+ </div>
286
+ <div
287
+ class="mt-2 inline-flex max-w-full border border-outline-variant/10 bg-surface-container-lowest px-3 py-1 font-mono text-[11px] text-primary-container"
288
+ title="${escapeHtml(token.tokenPrefix ?? '')}"
289
+ >
290
+ <span class="truncate">${escapeHtml(token.tokenPrefix)}...</span>
291
+ </div>
292
+ </div>
293
+ <div>
294
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Created</div>
295
+ <div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.createdAt ?? 'unknown')}">
296
+ ${escapeHtml(createdAt)}
297
+ </div>
298
+ </div>
299
+ <div>
300
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Calls</div>
301
+ <div class="mt-1 font-mono text-lg font-bold text-on-surface xl:mt-0">
302
+ ${escapeHtml(formatNumber(callCount))}
303
+ </div>
304
+ </div>
305
+ <div>
306
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Last Call</div>
307
+ <div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.lastCallAt ?? 'never')}">
308
+ ${escapeHtml(lastCallAt)}
309
+ </div>
310
+ </div>
311
+ <div class="flex xl:justify-end">
312
+ <button
313
+ class="delete-button"
314
+ data-action="open-delete-api-token-modal"
315
+ data-token-id="${escapeHtml(token.id)}"
316
+ type="button"
317
+ ${tokenSaving ? 'disabled' : ''}
318
+ >
319
+ <span class="material-symbols-outlined text-sm">delete</span>
320
+ Delete
321
+ </button>
322
+ </div>
323
+ </div>
324
+ `;
325
+ }
326
+
327
+ function renderApiTokenRows(apiTokens, tokenSaving) {
328
+ if (!apiTokens.length) {
329
+ return `
330
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-5 text-sm text-on-surface-variant">
331
+ No API tokens exist for this database.
332
+ </div>
333
+ `;
334
+ }
335
+
336
+ return `
337
+ <div class="overflow-hidden border border-outline-variant/10">
338
+ <div class="hidden bg-surface-container-highest px-4 py-3 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55 xl:grid xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem]">
339
+ <div>Token</div>
340
+ <div>Created</div>
341
+ <div>Calls</div>
342
+ <div>Last Call</div>
343
+ <div class="text-right">Actions</div>
344
+ </div>
345
+ ${apiTokens.map(token => renderApiTokenRow(token, tokenSaving)).join('')}
346
+ </div>
347
+ `;
348
+ }
349
+
93
350
  function renderSettingsContent(state) {
94
351
  if (state.settings.loading && !state.settings.appVersion) {
95
352
  return `
@@ -113,7 +370,8 @@ function renderSettingsContent(state) {
113
370
  `;
114
371
  }
115
372
 
116
- const activeSection = state.settings.section === 'api-tokens' ? 'api-tokens' : 'information';
373
+ const activeSection =
374
+ state.settings.section === 'api-tokens' ? 'api-tokens' : state.settings.section === 'mcp' ? 'mcp' : 'information';
117
375
  const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
118
376
  const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
119
377
  const versionCheckLoading = Boolean(state.settings.versionCheckLoading);
@@ -121,32 +379,7 @@ function renderSettingsContent(state) {
121
379
  const apiTokens = state.settings.apiTokens ?? [];
122
380
  const createdApiToken = state.settings.createdApiToken;
123
381
  const tokenSaving = Boolean(state.settings.tokenSaving);
124
- const tokenItems = apiTokens.length
125
- ? apiTokens
126
- .map(
127
- token => `
128
- <div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
129
- <div class="min-w-0">
130
- <div class="truncate text-sm font-semibold text-on-surface">${escapeHtml(token.name)}</div>
131
- <div class="mt-1 font-mono text-[10px] text-on-surface-variant/60">
132
- ${escapeHtml(token.tokenPrefix)}... · Created ${escapeHtml(token.createdAt ?? 'unknown')}
133
- </div>
134
- </div>
135
- <button
136
- class="delete-button"
137
- data-action="open-delete-api-token-modal"
138
- data-token-id="${escapeHtml(token.id)}"
139
- type="button"
140
- ${tokenSaving ? 'disabled' : ''}
141
- >
142
- <span class="material-symbols-outlined text-sm">delete</span>
143
- Delete
144
- </button>
145
- </div>
146
- `,
147
- )
148
- .join('')
149
- : '<div class="text-sm text-on-surface-variant">No API tokens exist for this database.</div>';
382
+ const tokenItems = renderApiTokenRows(apiTokens, tokenSaving);
150
383
  const tokenSection = tokenDatabase
151
384
  ? `
152
385
  <div class="space-y-5">
@@ -327,11 +560,12 @@ function renderSettingsContent(state) {
327
560
  </div>
328
561
  </section>
329
562
  `;
563
+ const mcpSection = renderMcpSection(state.settings);
330
564
 
331
565
  return `
332
566
  <div class="space-y-6">
333
567
  ${renderSettingsNavigation(activeSection)}
334
- ${activeSection === 'api-tokens' ? apiTokensSection : informationSection}
568
+ ${activeSection === 'api-tokens' ? apiTokensSection : activeSection === 'mcp' ? mcpSection : informationSection}
335
569
  </div>
336
570
  `;
337
571
  }
@@ -346,7 +580,9 @@ export function renderSettingsView(state) {
346
580
  subtitle:
347
581
  state.settings.section === 'api-tokens'
348
582
  ? 'Security // Database API access'
349
- : 'Application // Build + Credits',
583
+ : state.settings.section === 'mcp'
584
+ ? 'Agents // Local MCP access'
585
+ : 'Application // Build + Credits',
350
586
  })}
351
587
  ${renderSettingsContent(state)}
352
588
  </div>
@@ -248,6 +248,12 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
248
248
  label: 'Table Designer',
249
249
  target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
250
250
  },
251
+ {
252
+ icon: 'troubleshoot',
253
+ key: 'table-advisor',
254
+ label: 'Table Advisor',
255
+ target: tableName => `/table-advisor/${encodeURIComponent(tableName)}`,
256
+ },
251
257
  {
252
258
  key: 'sql-editor',
253
259
  },