@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.4

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 (78) hide show
  1. package/.agent/workflows/dev-workflow-bugfix.md +250 -250
  2. package/.agent/workflows/dev-workflow-feature.md +201 -201
  3. package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
  4. package/.agent/workflows/dev-workflow-refactor.md +238 -238
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
  7. package/.github/pull_request_template.md +22 -22
  8. package/.github/workflows/github-release.yml +56 -0
  9. package/.github/workflows/npm-publish.yml +50 -50
  10. package/.husky/commit-msg +0 -0
  11. package/CHANGELOG.md +759 -741
  12. package/CODE_OF_CONDUCT.md +79 -79
  13. package/CONTRIBUTING.md +60 -60
  14. package/LICENSE +21 -21
  15. package/README.md +632 -632
  16. package/SECURITY.md +20 -20
  17. package/db/index.js +204 -204
  18. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  19. package/docs/PLESK_DEPLOYMENT.md +170 -170
  20. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  21. package/docs/antigravity/EXAMPLES.md +535 -535
  22. package/docs/antigravity/GETTING-STARTED.md +292 -292
  23. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  24. package/docs/antigravity/WALKTHROUGH.md +25 -25
  25. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  26. package/docs/product/PRD.md +67 -67
  27. package/docs/release-notes/RELEASE_NOTES.md +233 -226
  28. package/docs/web-dashboard.md +175 -175
  29. package/exec.js +49 -49
  30. package/index.js +279 -279
  31. package/install.js +73 -73
  32. package/package.json +4 -4
  33. package/release-wrapper.js +192 -192
  34. package/scripts/build-docs.js +63 -63
  35. package/scripts/extract-release-notes.cjs +78 -0
  36. package/scripts/hello-world.js +1 -1
  37. package/scripts/release.cjs +252 -252
  38. package/scripts/release.js +252 -0
  39. package/scripts/update-release-notes.cjs +178 -178
  40. package/scripts/update-release-notes.js +182 -0
  41. package/server.json +23 -23
  42. package/tailwind.config.js +8 -8
  43. package/tests/build.test.js +83 -83
  44. package/tests/exec.test.js +35 -35
  45. package/tests/handlers.test.js +1058 -1058
  46. package/tests/hello-world.test.js +13 -13
  47. package/tests/mcpRegistry.test.js +65 -65
  48. package/tests/patch-release.test.js +7 -0
  49. package/tests/release-wrapper.test.js +107 -107
  50. package/tests/workflowState.test.js +505 -505
  51. package/tools/definitions.js +332 -332
  52. package/tools/handlers.js +1019 -1019
  53. package/vite.config.js +43 -43
  54. package/vite.web.config.js +23 -23
  55. package/web/README.md +36 -36
  56. package/web/app/docs/page.tsx +202 -202
  57. package/web/app/globals.css +90 -90
  58. package/web/app/layout.tsx +36 -36
  59. package/web/app/page.tsx +15 -15
  60. package/web/components/ApiReference.tsx +20 -20
  61. package/web/components/Features.tsx +61 -61
  62. package/web/components/Footer.tsx +9 -9
  63. package/web/components/Hero.tsx +96 -96
  64. package/web/components/History.tsx +143 -143
  65. package/web/components/Navbar.tsx +64 -64
  66. package/web/components/Terminal.tsx +114 -114
  67. package/web/eslint.config.mjs +18 -18
  68. package/web/lib/db.js +131 -131
  69. package/web/lib/docs.js +73 -73
  70. package/web/netlify.toml +3 -3
  71. package/web/next.config.ts +12 -12
  72. package/web/package-lock.json +6560 -6560
  73. package/web/package.json +28 -28
  74. package/web/postcss.config.mjs +9 -9
  75. package/web/server.js +31 -31
  76. package/web/tailwind.config.ts +19 -19
  77. package/web/tsconfig.json +42 -42
  78. package/workflow-state.js +612 -612
@@ -1,114 +1,114 @@
1
- "use client";
2
-
3
- import { useEffect, useState, useRef } from "react";
4
- import { Rocket } from "lucide-react";
5
-
6
- type TerminalLine = {
7
- type: 'command' | 'success' | 'comment' | 'info' | 'final';
8
- text: string;
9
- delay: number;
10
- };
11
-
12
- const terminalLines: TerminalLine[] = [
13
- { type: 'command', text: 'start_task', delay: 500 },
14
- { type: 'success', text: '✓ Task started: Implement user auth', delay: 1500 },
15
- { type: 'comment', text: '# Write code...', delay: 2500 },
16
- { type: 'command', text: 'mark_bug_fixed', delay: 4000 },
17
- { type: 'info', text: '✨ Feature complete! Now create tests...', delay: 5000 },
18
- { type: 'command', text: 'create_tests', delay: 6500 },
19
- { type: 'success', text: '✓ Tests created', delay: 7500 },
20
- { type: 'command', text: 'run_tests', delay: 8500 },
21
- { type: 'success', text: '✓ All tests passed!', delay: 9500 },
22
- { type: 'command', text: 'create_documentation', delay: 10500 },
23
- { type: 'success', text: '✓ Documentation updated', delay: 11500 },
24
- { type: 'command', text: 'commit_and_push', delay: 12500 },
25
- { type: 'final', text: 'Ready to release!', delay: 13500 }
26
- ];
27
-
28
- export default function Terminal() {
29
- const [lines, setLines] = useState<React.ReactElement[]>([]);
30
- const containerRef = useRef<HTMLDivElement>(null);
31
- const hasStarted = useRef(false);
32
-
33
- useEffect(() => {
34
- const observer = new IntersectionObserver((entries) => {
35
- entries.forEach(entry => {
36
- if (entry.isIntersecting && !hasStarted.current) {
37
- hasStarted.current = true;
38
- runAnimation();
39
- }
40
- });
41
- });
42
-
43
- if (containerRef.current) {
44
- observer.observe(containerRef.current);
45
- }
46
-
47
- return () => observer.disconnect();
48
- }, []);
49
-
50
- const runAnimation = async () => {
51
- for (const line of terminalLines) {
52
- await new Promise(r => setTimeout(r, line.delay - (terminalLines[terminalLines.indexOf(line) - 1]?.delay || 0)));
53
-
54
- if (line.type === 'command') {
55
- await typeCommand(line.text);
56
- } else {
57
- addLine(line);
58
- }
59
- }
60
- };
61
-
62
- const typeCommand = async (text: string) => {
63
- const id = Date.now();
64
- setLines(prev => [...prev, (
65
- <div key={id} className="terminal-line visible">
66
- <span className="text-gray-500 mr-2">$</span>
67
- <span className="text-white typing-cursor" id={`cmd-${id}`}></span>
68
- </div>
69
- )]);
70
-
71
- for (let i = 0; i < text.length; i++) {
72
- await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
73
- const el = document.getElementById(`cmd-${id}`);
74
- if (el) el.textContent += text[i];
75
- }
76
-
77
- const el = document.getElementById(`cmd-${id}`);
78
- if (el) el.classList.remove('typing-cursor');
79
- };
80
-
81
- const addLine = (line: TerminalLine) => {
82
- let content: React.ReactElement;
83
- let className = "terminal-line visible";
84
-
85
- switch (line.type) {
86
- case 'success':
87
- className += " text-emerald-400";
88
- content = <>{line.text}</>;
89
- break;
90
- case 'comment':
91
- className += " mt-3";
92
- content = <><span className="text-gray-500 mr-2">$</span><span className="text-white">{line.text}</span></>;
93
- break;
94
- case 'info':
95
- className += " text-purple-400";
96
- content = <>{line.text}</>;
97
- break;
98
- case 'final':
99
- className += " text-pink-400 flex items-center gap-2";
100
- content = <><Rocket className="w-4 h-4" />{line.text}</>;
101
- break;
102
- default:
103
- content = <>{line.text}</>;
104
- }
105
-
106
- setLines(prev => [...prev, <div key={Date.now()} className={className}>{content}</div>]);
107
- };
108
-
109
- return (
110
- <div ref={containerRef} className="font-mono text-sm space-y-2 min-h-[400px]">
111
- {lines}
112
- </div>
113
- );
114
- }
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef } from "react";
4
+ import { Rocket } from "lucide-react";
5
+
6
+ type TerminalLine = {
7
+ type: 'command' | 'success' | 'comment' | 'info' | 'final';
8
+ text: string;
9
+ delay: number;
10
+ };
11
+
12
+ const terminalLines: TerminalLine[] = [
13
+ { type: 'command', text: 'start_task', delay: 500 },
14
+ { type: 'success', text: '✓ Task started: Implement user auth', delay: 1500 },
15
+ { type: 'comment', text: '# Write code...', delay: 2500 },
16
+ { type: 'command', text: 'mark_bug_fixed', delay: 4000 },
17
+ { type: 'info', text: '✨ Feature complete! Now create tests...', delay: 5000 },
18
+ { type: 'command', text: 'create_tests', delay: 6500 },
19
+ { type: 'success', text: '✓ Tests created', delay: 7500 },
20
+ { type: 'command', text: 'run_tests', delay: 8500 },
21
+ { type: 'success', text: '✓ All tests passed!', delay: 9500 },
22
+ { type: 'command', text: 'create_documentation', delay: 10500 },
23
+ { type: 'success', text: '✓ Documentation updated', delay: 11500 },
24
+ { type: 'command', text: 'commit_and_push', delay: 12500 },
25
+ { type: 'final', text: 'Ready to release!', delay: 13500 }
26
+ ];
27
+
28
+ export default function Terminal() {
29
+ const [lines, setLines] = useState<React.ReactElement[]>([]);
30
+ const containerRef = useRef<HTMLDivElement>(null);
31
+ const hasStarted = useRef(false);
32
+
33
+ useEffect(() => {
34
+ const observer = new IntersectionObserver((entries) => {
35
+ entries.forEach(entry => {
36
+ if (entry.isIntersecting && !hasStarted.current) {
37
+ hasStarted.current = true;
38
+ runAnimation();
39
+ }
40
+ });
41
+ });
42
+
43
+ if (containerRef.current) {
44
+ observer.observe(containerRef.current);
45
+ }
46
+
47
+ return () => observer.disconnect();
48
+ }, []);
49
+
50
+ const runAnimation = async () => {
51
+ for (const line of terminalLines) {
52
+ await new Promise(r => setTimeout(r, line.delay - (terminalLines[terminalLines.indexOf(line) - 1]?.delay || 0)));
53
+
54
+ if (line.type === 'command') {
55
+ await typeCommand(line.text);
56
+ } else {
57
+ addLine(line);
58
+ }
59
+ }
60
+ };
61
+
62
+ const typeCommand = async (text: string) => {
63
+ const id = Date.now();
64
+ setLines(prev => [...prev, (
65
+ <div key={id} className="terminal-line visible">
66
+ <span className="text-gray-500 mr-2">$</span>
67
+ <span className="text-white typing-cursor" id={`cmd-${id}`}></span>
68
+ </div>
69
+ )]);
70
+
71
+ for (let i = 0; i < text.length; i++) {
72
+ await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
73
+ const el = document.getElementById(`cmd-${id}`);
74
+ if (el) el.textContent += text[i];
75
+ }
76
+
77
+ const el = document.getElementById(`cmd-${id}`);
78
+ if (el) el.classList.remove('typing-cursor');
79
+ };
80
+
81
+ const addLine = (line: TerminalLine) => {
82
+ let content: React.ReactElement;
83
+ let className = "terminal-line visible";
84
+
85
+ switch (line.type) {
86
+ case 'success':
87
+ className += " text-emerald-400";
88
+ content = <>{line.text}</>;
89
+ break;
90
+ case 'comment':
91
+ className += " mt-3";
92
+ content = <><span className="text-gray-500 mr-2">$</span><span className="text-white">{line.text}</span></>;
93
+ break;
94
+ case 'info':
95
+ className += " text-purple-400";
96
+ content = <>{line.text}</>;
97
+ break;
98
+ case 'final':
99
+ className += " text-pink-400 flex items-center gap-2";
100
+ content = <><Rocket className="w-4 h-4" />{line.text}</>;
101
+ break;
102
+ default:
103
+ content = <>{line.text}</>;
104
+ }
105
+
106
+ setLines(prev => [...prev, <div key={Date.now()} className={className}>{content}</div>]);
107
+ };
108
+
109
+ return (
110
+ <div ref={containerRef} className="font-mono text-sm space-y-2 min-h-[400px]">
111
+ {lines}
112
+ </div>
113
+ );
114
+ }
@@ -1,18 +1,18 @@
1
- import { defineConfig, globalIgnores } from "eslint/config";
2
- import nextVitals from "eslint-config-next/core-web-vitals";
3
- import nextTs from "eslint-config-next/typescript";
4
-
5
- const eslintConfig = defineConfig([
6
- ...nextVitals,
7
- ...nextTs,
8
- // Override default ignores of eslint-config-next.
9
- globalIgnores([
10
- // Default ignores of eslint-config-next:
11
- ".next/**",
12
- "out/**",
13
- "build/**",
14
- "next-env.d.ts",
15
- ]),
16
- ]);
17
-
18
- export default eslintConfig;
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
package/web/lib/db.js CHANGED
@@ -1,131 +1,131 @@
1
- import Database from "better-sqlite3";
2
- import { existsSync } from "fs";
3
- import path from "path";
4
-
5
- const DB_FILENAME = "dev-workflow.db";
6
- const FREQUENCY_FORMATS = {
7
- daily: "%Y-%m-%d",
8
- monthly: "%Y-%m",
9
- yearly: "%Y",
10
- };
11
-
12
- function getDbPath() {
13
- const initCwd = process.env.INIT_CWD || process.cwd();
14
- let projectRoot = initCwd;
15
-
16
- // Look for .state folder specifically.
17
- while (projectRoot !== path.dirname(projectRoot)) {
18
- if (existsSync(path.join(projectRoot, ".state"))) return path.join(projectRoot, ".state", DB_FILENAME);
19
- projectRoot = path.dirname(projectRoot);
20
- }
21
-
22
- // Fallback to relative path from this file if possible
23
- return path.join(process.cwd(), "..", ".state", DB_FILENAME);
24
- }
25
-
26
- let db;
27
- export function getDb() {
28
- if (!db) {
29
- const dbPath = getDbPath();
30
- db = new Database(dbPath);
31
- db.pragma("journal_mode = WAL");
32
- migrate();
33
- }
34
- return db;
35
- }
36
-
37
- function migrate() {
38
- const createHistory = `
39
- CREATE TABLE IF NOT EXISTS workflow_history (
40
- id INTEGER PRIMARY KEY AUTOINCREMENT,
41
- user_id TEXT NOT NULL,
42
- task_description TEXT NOT NULL,
43
- task_type TEXT NOT NULL,
44
- commit_message TEXT,
45
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
46
- forced INTEGER DEFAULT 0,
47
- force_reason TEXT,
48
- dropped INTEGER DEFAULT 0,
49
- drop_reason TEXT,
50
- UNIQUE(user_id, timestamp, task_description)
51
- )
52
- `;
53
- const createSummary = `
54
- CREATE TABLE IF NOT EXISTS project_summary (
55
- user_id TEXT PRIMARY KEY,
56
- total_tasks INTEGER DEFAULT 0,
57
- task_types TEXT DEFAULT '{}',
58
- last_active DATETIME,
59
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
60
- recent_tasks TEXT DEFAULT '[]'
61
- )
62
- `;
63
- const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
64
- const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
65
-
66
- db.exec(createHistory);
67
- db.exec(createSummary);
68
- db.exec(idxHistory);
69
- db.exec(idxSummary);
70
- }
71
-
72
- export function getSummaryForUser(userId) {
73
- const stmt = getDb().prepare(`
74
- SELECT * FROM project_summary WHERE user_id = ?
75
- `);
76
- const row = stmt.get(userId);
77
- if (!row) return null;
78
- return {
79
- totalTasks: row.total_tasks,
80
- taskTypes: JSON.parse(row.task_types),
81
- lastActive: row.last_active,
82
- recentTasks: JSON.parse(row.recent_tasks),
83
- updatedAt: row.updated_at,
84
- };
85
- }
86
-
87
- function buildHistoryFilters({ startDate, endDate }) {
88
- const clauses = [];
89
- const params = [];
90
- if (startDate) {
91
- clauses.push("DATE(timestamp) >= DATE(?)");
92
- params.push(startDate);
93
- }
94
- if (endDate) {
95
- clauses.push("DATE(timestamp) <= DATE(?)");
96
- params.push(endDate);
97
- }
98
- return { clauses, params };
99
- }
100
-
101
- export function getHistoryForUser(userId, options = {}) {
102
- const { page = 1, pageSize = 20, startDate, endDate } = options;
103
- const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
104
- const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
105
- const offset = (sanitizedPage - 1) * sanitizedPageSize;
106
-
107
- const { clauses, params } = buildHistoryFilters({ startDate, endDate });
108
- const where = ["user_id = ?", ...clauses].join(" AND ");
109
- const dbInstance = getDb();
110
-
111
- const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
112
- const total = countStmt.get(userId, ...params).total;
113
-
114
- const rowsStmt = dbInstance.prepare(`
115
- SELECT *
116
- FROM workflow_history
117
- WHERE ${where}
118
- ORDER BY timestamp DESC
119
- LIMIT ?
120
- OFFSET ?
121
- `);
122
- const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
123
-
124
- return {
125
- data: entries,
126
- total,
127
- page: sanitizedPage,
128
- pageSize: sanitizedPageSize,
129
- totalPages: Math.ceil(total / sanitizedPageSize)
130
- };
131
- }
1
+ import Database from "better-sqlite3";
2
+ import { existsSync } from "fs";
3
+ import path from "path";
4
+
5
+ const DB_FILENAME = "dev-workflow.db";
6
+ const FREQUENCY_FORMATS = {
7
+ daily: "%Y-%m-%d",
8
+ monthly: "%Y-%m",
9
+ yearly: "%Y",
10
+ };
11
+
12
+ function getDbPath() {
13
+ const initCwd = process.env.INIT_CWD || process.cwd();
14
+ let projectRoot = initCwd;
15
+
16
+ // Look for .state folder specifically.
17
+ while (projectRoot !== path.dirname(projectRoot)) {
18
+ if (existsSync(path.join(projectRoot, ".state"))) return path.join(projectRoot, ".state", DB_FILENAME);
19
+ projectRoot = path.dirname(projectRoot);
20
+ }
21
+
22
+ // Fallback to relative path from this file if possible
23
+ return path.join(process.cwd(), "..", ".state", DB_FILENAME);
24
+ }
25
+
26
+ let db;
27
+ export function getDb() {
28
+ if (!db) {
29
+ const dbPath = getDbPath();
30
+ db = new Database(dbPath);
31
+ db.pragma("journal_mode = WAL");
32
+ migrate();
33
+ }
34
+ return db;
35
+ }
36
+
37
+ function migrate() {
38
+ const createHistory = `
39
+ CREATE TABLE IF NOT EXISTS workflow_history (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ user_id TEXT NOT NULL,
42
+ task_description TEXT NOT NULL,
43
+ task_type TEXT NOT NULL,
44
+ commit_message TEXT,
45
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
46
+ forced INTEGER DEFAULT 0,
47
+ force_reason TEXT,
48
+ dropped INTEGER DEFAULT 0,
49
+ drop_reason TEXT,
50
+ UNIQUE(user_id, timestamp, task_description)
51
+ )
52
+ `;
53
+ const createSummary = `
54
+ CREATE TABLE IF NOT EXISTS project_summary (
55
+ user_id TEXT PRIMARY KEY,
56
+ total_tasks INTEGER DEFAULT 0,
57
+ task_types TEXT DEFAULT '{}',
58
+ last_active DATETIME,
59
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
60
+ recent_tasks TEXT DEFAULT '[]'
61
+ )
62
+ `;
63
+ const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
64
+ const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
65
+
66
+ db.exec(createHistory);
67
+ db.exec(createSummary);
68
+ db.exec(idxHistory);
69
+ db.exec(idxSummary);
70
+ }
71
+
72
+ export function getSummaryForUser(userId) {
73
+ const stmt = getDb().prepare(`
74
+ SELECT * FROM project_summary WHERE user_id = ?
75
+ `);
76
+ const row = stmt.get(userId);
77
+ if (!row) return null;
78
+ return {
79
+ totalTasks: row.total_tasks,
80
+ taskTypes: JSON.parse(row.task_types),
81
+ lastActive: row.last_active,
82
+ recentTasks: JSON.parse(row.recent_tasks),
83
+ updatedAt: row.updated_at,
84
+ };
85
+ }
86
+
87
+ function buildHistoryFilters({ startDate, endDate }) {
88
+ const clauses = [];
89
+ const params = [];
90
+ if (startDate) {
91
+ clauses.push("DATE(timestamp) >= DATE(?)");
92
+ params.push(startDate);
93
+ }
94
+ if (endDate) {
95
+ clauses.push("DATE(timestamp) <= DATE(?)");
96
+ params.push(endDate);
97
+ }
98
+ return { clauses, params };
99
+ }
100
+
101
+ export function getHistoryForUser(userId, options = {}) {
102
+ const { page = 1, pageSize = 20, startDate, endDate } = options;
103
+ const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
104
+ const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
105
+ const offset = (sanitizedPage - 1) * sanitizedPageSize;
106
+
107
+ const { clauses, params } = buildHistoryFilters({ startDate, endDate });
108
+ const where = ["user_id = ?", ...clauses].join(" AND ");
109
+ const dbInstance = getDb();
110
+
111
+ const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
112
+ const total = countStmt.get(userId, ...params).total;
113
+
114
+ const rowsStmt = dbInstance.prepare(`
115
+ SELECT *
116
+ FROM workflow_history
117
+ WHERE ${where}
118
+ ORDER BY timestamp DESC
119
+ LIMIT ?
120
+ OFFSET ?
121
+ `);
122
+ const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
123
+
124
+ return {
125
+ data: entries,
126
+ total,
127
+ page: sanitizedPage,
128
+ pageSize: sanitizedPageSize,
129
+ totalPages: Math.ceil(total / sanitizedPageSize)
130
+ };
131
+ }