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
@@ -48,6 +48,7 @@ import {
48
48
  initializeApp,
49
49
  insertMarkdownIntoLastOpenDocument,
50
50
  loadMoreQueryHistory,
51
+ loadMoreLogs,
51
52
  openModal,
52
53
  openOverviewInFinder,
53
54
  openQueryHistoryInEditor,
@@ -64,6 +65,7 @@ import {
64
65
  openRestoreBackupModal,
65
66
  openDeleteQueryChartModal,
66
67
  openDataExportModal,
68
+ openGenerateDataModal,
67
69
  openDeleteDocumentModal,
68
70
  openQueryExportModal,
69
71
  openDataRowByIdentity,
@@ -77,6 +79,7 @@ import {
77
79
  openDataRowUpdatePreview,
78
80
  openEditorRowUpdatePreview,
79
81
  refreshCurrentRoute,
82
+ refreshLogs,
80
83
  refreshBackups,
81
84
  refreshMediaTaggingPreview,
82
85
  removeConnection,
@@ -94,6 +97,9 @@ import {
94
97
  selectQueryHistoryItem,
95
98
  selectStructureEntry,
96
99
  setDocumentsSearchQuery,
100
+ setLogFilter,
101
+ setLogSearchInput,
102
+ applyLogSearch,
97
103
  setTableDesignerSearchQuery,
98
104
  setTableDesignerSqlPreviewVisibility,
99
105
  toggleTableDesignerTablesPanel,
@@ -126,7 +132,9 @@ import {
126
132
  submitBackupSafetyChoice,
127
133
  submitCreateBackupConfirmation,
128
134
  submitEditBackupConfirmation,
135
+ submitGenerateDataRows,
129
136
  submitRowUpdatePreviewConfirmation,
137
+ previewGenerateDataRows,
130
138
  setQueryHistoryPanelVisibility,
131
139
  sortDataTableByColumn,
132
140
  sortEditorResultsByColumn,
@@ -170,6 +178,8 @@ import {
170
178
  updateCurrentTableDesignerColumnField,
171
179
  updateCurrentTableDesignerConstraintField,
172
180
  updateCurrentTableDesignerField,
181
+ updateGenerateDataMapping,
182
+ updateGenerateDataModal,
173
183
  updateGenerateTypesModal,
174
184
  addCurrentTableDesignerColumn,
175
185
  applyCurrentMediaTaggingSelection,
@@ -182,10 +192,12 @@ import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
182
192
  import { renderDocumentsView } from './views/documents.js';
183
193
  import { renderEditorView } from './views/editor.js';
184
194
  import { renderLandingView } from './views/landing.js';
195
+ import { renderLogsView, renderLogTable } from './views/logs.js';
185
196
  import { renderMediaTaggingView } from './views/mediaTagging.js';
186
197
  import { renderOverviewView } from './views/overview.js';
187
198
  import { renderSettingsView } from './views/settings.js';
188
199
  import { renderStructureView } from './views/structure.js';
200
+ import { renderTableAdvisorView } from './views/tableAdvisor.js';
189
201
  import { renderTableDesignerView } from './views/tableDesigner.js';
190
202
  import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
191
203
  import {
@@ -200,6 +212,7 @@ import {
200
212
  detectFilePathValue,
201
213
  getPathTypeLabel,
202
214
  } from './utils/filePathPreview.js';
215
+ import { clearInputForEscape } from './utils/inputClear.js';
203
216
  import { formatSqlQuery } from './utils/sqlFormatter.js';
204
217
  import {
205
218
  buildDataRowEditorJsonObject,
@@ -263,6 +276,7 @@ const ROUTE_TITLE_SEGMENTS = {
263
276
  backups: 'Backups',
264
277
  overview: 'Overview',
265
278
  data: 'Data',
279
+ tableAdvisor: 'Table Advisor',
266
280
  structure: 'Structure',
267
281
  editor: 'SQL Editor',
268
282
  editorResults: 'SQL Editor',
@@ -272,6 +286,7 @@ const ROUTE_TITLE_SEGMENTS = {
272
286
  mediaTaggingSetup: 'Media Tagging',
273
287
  mediaTaggingQueue: 'Tagging Queue',
274
288
  settings: 'Settings',
289
+ logs: 'Logs',
275
290
  notFound: 'Not Found',
276
291
  };
277
292
 
@@ -973,6 +988,92 @@ function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
973
988
  return true;
974
989
  }
975
990
 
991
+ function getLogsActiveDatabaseId(state) {
992
+ return String(state.connections?.active?.id ?? state.logs?.metadata?.activeDatabase?.id ?? '');
993
+ }
994
+
995
+ function syncLogsFilterControls(state) {
996
+ const filters = state.logs?.filters ?? {};
997
+
998
+ for (const button of shellRefs.view.querySelectorAll('[data-action="set-log-filter"][data-field][data-value]')) {
999
+ if (!(button instanceof HTMLElement)) {
1000
+ continue;
1001
+ }
1002
+
1003
+ const field = button.dataset.field;
1004
+ const value = button.dataset.value;
1005
+ const active = String(filters[field] ?? '') === String(value ?? '');
1006
+
1007
+ button.classList.toggle('is-active', active);
1008
+ button.setAttribute('aria-pressed', active ? 'true' : 'false');
1009
+ }
1010
+ }
1011
+
1012
+ function syncLogsMetaStrip(state) {
1013
+ const logs = state.logs ?? {};
1014
+ const values = {
1015
+ visible: formatNumber((logs.items ?? []).length),
1016
+ matched: formatNumber(logs.total ?? 0),
1017
+ scope: logs.metadata?.activeDatabase?.label ?? 'Active Database',
1018
+ };
1019
+
1020
+ for (const [key, value] of Object.entries(values)) {
1021
+ const node = shellRefs.view.querySelector(`[data-logs-meta="${key}"]`);
1022
+
1023
+ if (node instanceof HTMLElement) {
1024
+ node.textContent = value;
1025
+ node.title = value;
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ function patchLogsTableUi(state) {
1031
+ const logsView = shellRefs.view.querySelector('[data-logs-view]');
1032
+
1033
+ if (!(logsView instanceof HTMLElement)) {
1034
+ return false;
1035
+ }
1036
+
1037
+ const activeDatabaseId = getLogsActiveDatabaseId(state);
1038
+
1039
+ if (String(logsView.dataset.logsActiveDatabaseId ?? '') !== activeDatabaseId) {
1040
+ return false;
1041
+ }
1042
+
1043
+ const currentTable = logsView.querySelector('[data-logs-table]');
1044
+
1045
+ if (!(currentTable instanceof HTMLElement)) {
1046
+ return false;
1047
+ }
1048
+
1049
+ const currentScrollNode = currentTable.querySelector('[data-logs-table-scroll]');
1050
+ const scrollState =
1051
+ currentScrollNode instanceof HTMLElement
1052
+ ? {
1053
+ left: currentScrollNode.scrollLeft,
1054
+ top: currentScrollNode.scrollTop,
1055
+ }
1056
+ : null;
1057
+ const patched = replaceElementFromRenderedMarkup(currentTable, renderLogTable(state.logs ?? {}));
1058
+
1059
+ if (!patched) {
1060
+ return false;
1061
+ }
1062
+
1063
+ if (scrollState) {
1064
+ const nextScrollNode = logsView.querySelector('[data-logs-table-scroll]');
1065
+
1066
+ if (nextScrollNode instanceof HTMLElement) {
1067
+ nextScrollNode.scrollLeft = scrollState.left;
1068
+ nextScrollNode.scrollTop = scrollState.top;
1069
+ }
1070
+ }
1071
+
1072
+ syncLogsFilterControls(state);
1073
+ syncLogsMetaStrip(state);
1074
+ return true;
1075
+ }
1076
+
976
1077
  function readFileAsBase64(file) {
977
1078
  return new Promise((resolve, reject) => {
978
1079
  const reader = new FileReader();
@@ -1112,12 +1213,16 @@ function resolveView(state) {
1112
1213
  return renderStructureView(state);
1113
1214
  case 'tableDesigner':
1114
1215
  return renderTableDesignerView(state);
1216
+ case 'tableAdvisor':
1217
+ return renderTableAdvisorView(state);
1115
1218
  case 'mediaTaggingSetup':
1116
1219
  return renderMediaTaggingView(state, { subView: 'setup' });
1117
1220
  case 'mediaTaggingQueue':
1118
1221
  return renderMediaTaggingView(state, { subView: 'queue' });
1119
1222
  case 'settings':
1120
1223
  return renderSettingsView(state);
1224
+ case 'logs':
1225
+ return renderLogsView(state);
1121
1226
  default:
1122
1227
  return renderNotFoundView();
1123
1228
  }
@@ -1403,9 +1508,11 @@ function renderApp(state) {
1403
1508
  'editorResults',
1404
1509
  'data',
1405
1510
  'charts',
1511
+ 'logs',
1406
1512
  'documents',
1407
1513
  'structure',
1408
1514
  'tableDesigner',
1515
+ 'tableAdvisor',
1409
1516
  'mediaTaggingSetup',
1410
1517
  'mediaTaggingQueue',
1411
1518
  ].includes(state.route.name);
@@ -1457,8 +1564,8 @@ function renderApp(state) {
1457
1564
  pendingNewTableDesignerAutofocus = false;
1458
1565
  }
1459
1566
 
1460
- const canPatchChartsMain =
1461
- mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1567
+ const canPatchChartsMain = mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1568
+ const canPatchLogsMain = mainChanged && previousRouteName === 'logs' && state.route.name === 'logs';
1462
1569
  let mainPatched = false;
1463
1570
  let preservedChartsDom = false;
1464
1571
 
@@ -1484,6 +1591,10 @@ function renderApp(state) {
1484
1591
  }
1485
1592
  }
1486
1593
 
1594
+ if (!mainPatched && canPatchLogsMain) {
1595
+ mainPatched = patchLogsTableUi(state);
1596
+ }
1597
+
1487
1598
  if (mainChanged) {
1488
1599
  if (!mainPatched) {
1489
1600
  teardownStructureGraph();
@@ -2428,6 +2539,15 @@ async function handleAction(actionNode) {
2428
2539
  case 'refresh-view':
2429
2540
  await refreshCurrentRoute();
2430
2541
  return;
2542
+ case 'refresh-logs':
2543
+ await refreshLogs();
2544
+ return;
2545
+ case 'set-log-filter':
2546
+ await setLogFilter(actionNode.dataset.field, actionNode.dataset.value);
2547
+ return;
2548
+ case 'load-more-logs':
2549
+ await loadMoreLogs();
2550
+ return;
2431
2551
  case 'set-settings-section':
2432
2552
  setSettingsSection(actionNode.dataset.section);
2433
2553
  return;
@@ -2457,6 +2577,16 @@ async function handleAction(actionNode) {
2457
2577
  }
2458
2578
  return;
2459
2579
  }
2580
+ case 'copy-mcp-config': {
2581
+ const configNode = document.querySelector('[data-mcp-config]');
2582
+ const config = configNode instanceof HTMLTextAreaElement ? configNode.value : '';
2583
+
2584
+ if (config && navigator.clipboard?.writeText) {
2585
+ await navigator.clipboard.writeText(config);
2586
+ showToast('MCP config copied.', 'success');
2587
+ }
2588
+ return;
2589
+ }
2460
2590
  case 'open-row-editor-url':
2461
2591
  openRowEditorUrl(actionNode);
2462
2592
  return;
@@ -2878,6 +3008,12 @@ async function handleAction(actionNode) {
2878
3008
  case 'open-data-export-modal':
2879
3009
  openDataExportModal();
2880
3010
  return;
3011
+ case 'open-generate-data-modal':
3012
+ openGenerateDataModal();
3013
+ return;
3014
+ case 'preview-generate-data':
3015
+ await previewGenerateDataRows();
3016
+ return;
2881
3017
  case 'export-data-format': {
2882
3018
  const format = actionNode.dataset.exportFormat;
2883
3019
  const filename = getExportFilenameFromAction(actionNode);
@@ -2948,6 +3084,30 @@ async function handleAction(actionNode) {
2948
3084
  }
2949
3085
  return;
2950
3086
  }
3087
+ case 'copy-table-advisor-sql': {
3088
+ const issueId = actionNode.dataset.issueId;
3089
+ const advisor = getState().tableAdvisor;
3090
+ const issue = (advisor.result?.issues ?? []).find(item => item.id === issueId);
3091
+ const sql = String(issue?.sql ?? '');
3092
+
3093
+ if (!sql.trim()) {
3094
+ showToast('No advisor SQL to copy.', 'alert');
3095
+ return;
3096
+ }
3097
+
3098
+ if (!navigator.clipboard?.writeText) {
3099
+ showToast('Clipboard API is not available.', 'alert');
3100
+ return;
3101
+ }
3102
+
3103
+ try {
3104
+ await navigator.clipboard.writeText(sql);
3105
+ showToast('Advisor SQL copied.', 'success');
3106
+ } catch (error) {
3107
+ showToast('Advisor SQL could not be copied.', 'alert');
3108
+ }
3109
+ return;
3110
+ }
2951
3111
  case 'toggle-table-designer-sql-preview':
2952
3112
  setTableDesignerSqlPreviewVisibility(
2953
3113
  actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
@@ -3236,6 +3396,11 @@ document.addEventListener('keydown', event => {
3236
3396
  return;
3237
3397
  }
3238
3398
 
3399
+ if (target instanceof HTMLInputElement && clearInputForEscape(target)) {
3400
+ event.preventDefault();
3401
+ return;
3402
+ }
3403
+
3239
3404
  if (state.modal) {
3240
3405
  event.preventDefault();
3241
3406
  closeModal();
@@ -3327,6 +3492,20 @@ document.addEventListener('input', event => {
3327
3492
  return;
3328
3493
  }
3329
3494
 
3495
+ if (bindNode.dataset.bind === 'generate-data-field') {
3496
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value, { notify: false });
3497
+ return;
3498
+ }
3499
+
3500
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3501
+ if (bindNode instanceof HTMLSelectElement) {
3502
+ return;
3503
+ }
3504
+
3505
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value, { notify: false });
3506
+ return;
3507
+ }
3508
+
3330
3509
  if (bindNode.dataset.bind === 'copy-column-format-field') {
3331
3510
  updateCopyColumnModalFormatField(
3332
3511
  bindNode.dataset.field,
@@ -3354,6 +3533,11 @@ document.addEventListener('input', event => {
3354
3533
  return;
3355
3534
  }
3356
3535
 
3536
+ if (bindNode.dataset.bind === 'logs-search') {
3537
+ setLogSearchInput(bindNode.value);
3538
+ return;
3539
+ }
3540
+
3357
3541
  if (bindNode.dataset.bind === 'data-search-query') {
3358
3542
  void setDataSearchQuery(bindNode.value);
3359
3543
  return;
@@ -3505,6 +3689,16 @@ document.addEventListener('change', event => {
3505
3689
  return;
3506
3690
  }
3507
3691
 
3692
+ if (bindNode.dataset.bind === 'generate-data-field') {
3693
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value);
3694
+ return;
3695
+ }
3696
+
3697
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3698
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value);
3699
+ return;
3700
+ }
3701
+
3508
3702
  if (bindNode.dataset.bind === 'data-search-column') {
3509
3703
  void setDataSearchColumn(bindNode.value);
3510
3704
  return;
@@ -3649,6 +3843,9 @@ document.addEventListener('submit', async event => {
3649
3843
  const formData = submitter ? new FormData(form, submitter) : new FormData(form);
3650
3844
 
3651
3845
  switch (form.dataset.form) {
3846
+ case 'logs-search':
3847
+ await applyLogSearch(String(formData.get('search') ?? ''));
3848
+ return;
3652
3849
  case 'create-api-token':
3653
3850
  await createSettingsApiToken(String(formData.get('name') ?? ''));
3654
3851
  return;
@@ -3780,6 +3977,9 @@ document.addEventListener('submit', async event => {
3780
3977
  case 'apply-row-update-preview':
3781
3978
  await submitRowUpdatePreviewConfirmation();
3782
3979
  return;
3980
+ case 'generate-data':
3981
+ await submitGenerateDataRows();
3982
+ return;
3783
3983
  case 'create-media-tagging-tag-table':
3784
3984
  await submitCreateMediaTaggingTagTable();
3785
3985
  return;
@@ -56,7 +56,9 @@ export function renderConnectionCard(connection, activeConnectionId) {
56
56
  '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
57
57
  escapeHtml(formatDateTime(connection.lastOpenedAt)),
58
58
  "</div></div>",
59
- '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
59
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold ',
60
+ connection.readOnly ? "text-primary-container" : "text-on-surface",
61
+ '">',
60
62
  connection.readOnly ? "Read only" : "Read / Write",
61
63
  "</div></div></div></div>",
62
64
  '<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',