claudit 0.1.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 (40) hide show
  1. package/README.md +139 -0
  2. package/bin/claudit-mcp.js +3 -0
  3. package/bin/claudit.js +11 -0
  4. package/client/dist/assets/index-DhjH_2Wd.css +32 -0
  5. package/client/dist/assets/index-Dwom-XdC.js +98 -0
  6. package/client/dist/index.html +13 -0
  7. package/package.json +40 -0
  8. package/server/dist/server/src/index.js +170 -0
  9. package/server/dist/server/src/mcp-server.js +144 -0
  10. package/server/dist/server/src/routes/cron.js +101 -0
  11. package/server/dist/server/src/routes/filesystem.js +71 -0
  12. package/server/dist/server/src/routes/groups.js +60 -0
  13. package/server/dist/server/src/routes/sessions.js +206 -0
  14. package/server/dist/server/src/routes/todo.js +93 -0
  15. package/server/dist/server/src/routes/todoProviders.js +179 -0
  16. package/server/dist/server/src/services/claudeProcess.js +220 -0
  17. package/server/dist/server/src/services/cronScheduler.js +163 -0
  18. package/server/dist/server/src/services/cronStorage.js +154 -0
  19. package/server/dist/server/src/services/database.js +103 -0
  20. package/server/dist/server/src/services/eventBus.js +11 -0
  21. package/server/dist/server/src/services/groupStorage.js +52 -0
  22. package/server/dist/server/src/services/historyIndex.js +100 -0
  23. package/server/dist/server/src/services/jsonStore.js +41 -0
  24. package/server/dist/server/src/services/managedSessions.js +96 -0
  25. package/server/dist/server/src/services/providerConfigStorage.js +80 -0
  26. package/server/dist/server/src/services/providers/TodoProvider.js +1 -0
  27. package/server/dist/server/src/services/providers/larkDocsProvider.js +75 -0
  28. package/server/dist/server/src/services/providers/mcpClient.js +151 -0
  29. package/server/dist/server/src/services/providers/meegoProvider.js +99 -0
  30. package/server/dist/server/src/services/providers/registry.js +17 -0
  31. package/server/dist/server/src/services/providers/supabaseProvider.js +172 -0
  32. package/server/dist/server/src/services/ptyManager.js +263 -0
  33. package/server/dist/server/src/services/sessionIndexCache.js +24 -0
  34. package/server/dist/server/src/services/sessionParser.js +98 -0
  35. package/server/dist/server/src/services/sessionScanner.js +244 -0
  36. package/server/dist/server/src/services/todoStorage.js +112 -0
  37. package/server/dist/server/src/services/todoSyncEngine.js +170 -0
  38. package/server/dist/server/src/types.js +1 -0
  39. package/server/dist/shared/src/index.js +1 -0
  40. package/server/dist/shared/src/types.js +2 -0
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Claude Session Manager</title>
7
+ <script type="module" crossorigin src="/assets/index-Dwom-XdC.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DhjH_2Wd.css">
9
+ </head>
10
+ <body class="bg-gray-950 text-gray-100">
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "claudit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A web dashboard for managing Claude Code sessions, cron tasks, and todos",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "claude",
9
+ "cli",
10
+ "dashboard",
11
+ "claude-code"
12
+ ],
13
+ "bin": {
14
+ "claudit": "./bin/claudit.js",
15
+ "claudit-mcp": "./bin/claudit-mcp.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "server/dist/",
20
+ "client/dist/"
21
+ ],
22
+ "scripts": {
23
+ "dev": "concurrently -n server,client -c blue,green \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
24
+ "build": "npm run build --prefix client && npm run build --prefix server",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.27.1",
29
+ "better-sqlite3": "^11.10.0",
30
+ "cors": "^2.8.5",
31
+ "express": "^4.21.0",
32
+ "node-cron": "^4.2.1",
33
+ "node-pty": "^1.1.0",
34
+ "ws": "^8.18.0",
35
+ "zod": "^4.3.6"
36
+ },
37
+ "devDependencies": {
38
+ "concurrently": "^9.1.0"
39
+ }
40
+ }
@@ -0,0 +1,170 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import path from 'path';
4
+ import { createServer } from 'http';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ import sessionRoutes from './routes/sessions.js';
7
+ import cronRoutes from './routes/cron.js';
8
+ import todoRoutes from './routes/todo.js';
9
+ import groupRoutes from './routes/groups.js';
10
+ import todoProviderRoutes from './routes/todoProviders.js';
11
+ import filesystemRoutes from './routes/filesystem.js';
12
+ import { ClaudeProcess } from './services/claudeProcess.js';
13
+ import { initScheduler, stopAllJobs } from './services/cronScheduler.js';
14
+ import { initProviderSync, stopProviderSync } from './services/todoSyncEngine.js';
15
+ import { closeMcpConnections } from './services/providers/mcpClient.js';
16
+ import { handleTerminalConnection } from './services/ptyManager.js';
17
+ import { eventBus } from './services/eventBus.js';
18
+ import { closeDb } from './services/database.js';
19
+ const app = express();
20
+ const PORT = parseInt(process.env.PORT || '3001', 10);
21
+ const CLAUDIT_ROOT = process.env.CLAUDIT_ROOT;
22
+ app.use(cors());
23
+ app.use(express.json());
24
+ // REST routes
25
+ app.use('/api/sessions', sessionRoutes);
26
+ app.use('/api/cron', cronRoutes);
27
+ app.use('/api/todo/providers', todoProviderRoutes);
28
+ app.use('/api/todo/groups', groupRoutes);
29
+ app.use('/api/todo', todoRoutes);
30
+ app.use('/api/filesystem', filesystemRoutes);
31
+ // Health check
32
+ app.get('/api/health', (_req, res) => {
33
+ res.json({ status: 'ok' });
34
+ });
35
+ // Production mode: serve client static files
36
+ if (CLAUDIT_ROOT) {
37
+ const clientDist = path.join(CLAUDIT_ROOT, 'client', 'dist');
38
+ app.use(express.static(clientDist));
39
+ // SPA fallback: non-API/WS requests return index.html
40
+ app.get('*', (_req, res) => {
41
+ res.sendFile(path.join(clientDist, 'index.html'));
42
+ });
43
+ }
44
+ const server = createServer(app);
45
+ // WebSocket servers (noServer mode to avoid multiple upgrade handler conflicts)
46
+ const wss = new WebSocketServer({ noServer: true });
47
+ const wssTerminal = new WebSocketServer({ noServer: true });
48
+ const wssEvents = new WebSocketServer({ noServer: true });
49
+ server.on('upgrade', (request, socket, head) => {
50
+ const { pathname } = new URL(request.url || '', `http://${request.headers.host}`);
51
+ if (pathname === '/ws/chat') {
52
+ wss.handleUpgrade(request, socket, head, (ws) => {
53
+ wss.emit('connection', ws, request);
54
+ });
55
+ }
56
+ else if (pathname === '/ws/terminal') {
57
+ wssTerminal.handleUpgrade(request, socket, head, (ws) => {
58
+ wssTerminal.emit('connection', ws, request);
59
+ });
60
+ }
61
+ else if (pathname === '/ws/events') {
62
+ wssEvents.handleUpgrade(request, socket, head, (ws) => {
63
+ wssEvents.emit('connection', ws, request);
64
+ });
65
+ }
66
+ else {
67
+ socket.destroy();
68
+ }
69
+ });
70
+ wssTerminal.on('connection', (ws) => {
71
+ handleTerminalConnection(ws);
72
+ });
73
+ wssEvents.on('connection', (ws) => {
74
+ const unsubscribe = eventBus.onSessionEvent((event) => {
75
+ if (ws.readyState === WebSocket.OPEN) {
76
+ ws.send(JSON.stringify(event));
77
+ }
78
+ });
79
+ ws.on('close', () => unsubscribe());
80
+ });
81
+ wss.on('connection', (ws) => {
82
+ console.log('[ws] Client connected');
83
+ let claude = null;
84
+ const safeSend = (data) => {
85
+ if (ws.readyState === WebSocket.OPEN) {
86
+ ws.send(JSON.stringify(data));
87
+ }
88
+ };
89
+ ws.on('message', (raw) => {
90
+ let msg;
91
+ try {
92
+ msg = JSON.parse(raw.toString());
93
+ }
94
+ catch {
95
+ safeSend({ type: 'error', message: 'Invalid JSON' });
96
+ return;
97
+ }
98
+ console.log(`[ws] Received: ${msg.type}`);
99
+ switch (msg.type) {
100
+ case 'resume': {
101
+ if (claude)
102
+ claude.stop();
103
+ claude = new ClaudeProcess(msg.sessionId, msg.projectPath);
104
+ claude.on('ready', () => {
105
+ console.log('[ws] Claude CLI ready');
106
+ });
107
+ claude.on('assistant_text', (text) => {
108
+ safeSend({ type: 'assistant_text', text });
109
+ });
110
+ claude.on('assistant_thinking', (text) => {
111
+ safeSend({ type: 'assistant_thinking', text });
112
+ });
113
+ claude.on('tool_use', (data) => {
114
+ safeSend({ type: 'tool_use', ...data });
115
+ });
116
+ claude.on('tool_result', (data) => {
117
+ safeSend({ type: 'tool_result', ...data });
118
+ });
119
+ claude.on('done', () => {
120
+ safeSend({ type: 'done' });
121
+ });
122
+ claude.on('error', (message) => {
123
+ safeSend({ type: 'error', message });
124
+ });
125
+ claude.start();
126
+ // Send connected immediately — CLI waits for stdin so no init event until message sent
127
+ safeSend({ type: 'connected', sessionId: msg.sessionId });
128
+ break;
129
+ }
130
+ case 'message': {
131
+ if (!claude) {
132
+ safeSend({ type: 'error', message: 'No active session. Send "resume" first.' });
133
+ return;
134
+ }
135
+ claude.sendMessage(msg.content);
136
+ break;
137
+ }
138
+ case 'stop': {
139
+ if (claude) {
140
+ claude.stop();
141
+ claude = null;
142
+ }
143
+ break;
144
+ }
145
+ }
146
+ });
147
+ ws.on('close', () => {
148
+ console.log('[ws] Client disconnected');
149
+ if (claude) {
150
+ claude.stop();
151
+ claude = null;
152
+ }
153
+ });
154
+ });
155
+ server.listen(PORT, () => {
156
+ console.log(`Server running on http://localhost:${PORT}`);
157
+ initScheduler();
158
+ initProviderSync();
159
+ });
160
+ // Graceful shutdown for tsx watch restarts
161
+ for (const sig of ['SIGTERM', 'SIGINT']) {
162
+ process.on(sig, () => {
163
+ stopAllJobs();
164
+ stopProviderSync();
165
+ closeMcpConnections();
166
+ closeDb();
167
+ server.close();
168
+ process.exit(0);
169
+ });
170
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { getAllTodos, getTodo, createTodo, updateTodo, deleteTodo, } from './services/todoStorage.js';
6
+ const server = new McpServer({
7
+ name: 'claudit-todos',
8
+ version: '0.1.0',
9
+ });
10
+ // --- list_todos ---
11
+ server.tool('list_todos', 'List all todos. Optionally filter by status (pending/completed) and priority (low/medium/high).', {
12
+ status: z.enum(['pending', 'completed', 'all']).optional().describe('Filter by completion status. Default: all'),
13
+ priority: z.enum(['low', 'medium', 'high']).optional().describe('Filter by priority level'),
14
+ groupId: z.string().optional().describe('Filter by group ID. Use "ungrouped" for todos without a group.'),
15
+ }, async ({ status, priority, groupId }) => {
16
+ let todos = getAllTodos();
17
+ if (status === 'pending')
18
+ todos = todos.filter(t => !t.completed);
19
+ else if (status === 'completed')
20
+ todos = todos.filter(t => t.completed);
21
+ if (priority)
22
+ todos = todos.filter(t => t.priority === priority);
23
+ if (groupId === 'ungrouped')
24
+ todos = todos.filter(t => !t.groupId);
25
+ else if (groupId)
26
+ todos = todos.filter(t => t.groupId === groupId);
27
+ const summary = todos.map(t => ({
28
+ id: t.id,
29
+ title: t.title,
30
+ description: t.description,
31
+ completed: t.completed,
32
+ priority: t.priority,
33
+ groupId: t.groupId,
34
+ position: t.position,
35
+ createdAt: t.createdAt,
36
+ completedAt: t.completedAt,
37
+ sessionId: t.sessionId,
38
+ externalUrl: t.provider?.externalUrl,
39
+ }));
40
+ return {
41
+ content: [{
42
+ type: 'text',
43
+ text: todos.length === 0
44
+ ? 'No todos found.'
45
+ : JSON.stringify(summary, null, 2),
46
+ }],
47
+ };
48
+ });
49
+ // --- get_todo ---
50
+ server.tool('get_todo', 'Get full details of a specific todo by ID.', {
51
+ id: z.string().describe('The todo ID'),
52
+ }, async ({ id }) => {
53
+ const todo = getTodo(id);
54
+ if (!todo) {
55
+ return {
56
+ content: [{ type: 'text', text: `Todo not found: ${id}` }],
57
+ isError: true,
58
+ };
59
+ }
60
+ return {
61
+ content: [{ type: 'text', text: JSON.stringify(todo, null, 2) }],
62
+ };
63
+ });
64
+ // --- create_todo ---
65
+ server.tool('create_todo', 'Create a new todo item.', {
66
+ title: z.string().describe('Title of the todo'),
67
+ description: z.string().optional().describe('Detailed description'),
68
+ priority: z.enum(['low', 'medium', 'high']).optional().describe('Priority level. Default: medium'),
69
+ sessionId: z.string().optional().describe('Link to a Claude session ID'),
70
+ sessionLabel: z.string().optional().describe('Display label for the linked session'),
71
+ groupId: z.string().optional().describe('Group ID to assign the todo to'),
72
+ }, async ({ title, description, priority, sessionId, sessionLabel, groupId }) => {
73
+ const todo = createTodo({
74
+ title,
75
+ description,
76
+ completed: false,
77
+ priority: priority || 'medium',
78
+ sessionId,
79
+ sessionLabel,
80
+ groupId,
81
+ position: 0, // auto-computed
82
+ });
83
+ return {
84
+ content: [{ type: 'text', text: `Created todo: ${todo.id}\n${JSON.stringify(todo, null, 2)}` }],
85
+ };
86
+ });
87
+ // --- update_todo ---
88
+ server.tool('update_todo', 'Update an existing todo. Only provided fields will be changed.', {
89
+ id: z.string().describe('The todo ID to update'),
90
+ title: z.string().optional().describe('New title'),
91
+ description: z.string().optional().describe('New description'),
92
+ completed: z.boolean().optional().describe('Mark as completed (true) or pending (false)'),
93
+ priority: z.enum(['low', 'medium', 'high']).optional().describe('New priority level'),
94
+ groupId: z.string().nullable().optional().describe('Group ID to move the todo to (null to ungroup)'),
95
+ }, async ({ id, title, description, completed, priority, groupId }) => {
96
+ const updates = {};
97
+ if (title !== undefined)
98
+ updates.title = title;
99
+ if (description !== undefined)
100
+ updates.description = description;
101
+ if (priority !== undefined)
102
+ updates.priority = priority;
103
+ if (groupId !== undefined)
104
+ updates.groupId = groupId;
105
+ if (completed !== undefined) {
106
+ updates.completed = completed;
107
+ if (completed)
108
+ updates.completedAt = new Date().toISOString();
109
+ }
110
+ const todo = updateTodo(id, updates);
111
+ if (!todo) {
112
+ return {
113
+ content: [{ type: 'text', text: `Todo not found: ${id}` }],
114
+ isError: true,
115
+ };
116
+ }
117
+ return {
118
+ content: [{ type: 'text', text: `Updated todo: ${todo.id}\n${JSON.stringify(todo, null, 2)}` }],
119
+ };
120
+ });
121
+ // --- delete_todo ---
122
+ server.tool('delete_todo', 'Delete a todo by ID.', {
123
+ id: z.string().describe('The todo ID to delete'),
124
+ }, async ({ id }) => {
125
+ const deleted = deleteTodo(id);
126
+ if (!deleted) {
127
+ return {
128
+ content: [{ type: 'text', text: `Todo not found: ${id}` }],
129
+ isError: true,
130
+ };
131
+ }
132
+ return {
133
+ content: [{ type: 'text', text: `Deleted todo: ${id}` }],
134
+ };
135
+ });
136
+ // --- Start server ---
137
+ async function main() {
138
+ const transport = new StdioServerTransport();
139
+ await server.connect(transport);
140
+ }
141
+ main().catch((err) => {
142
+ console.error('MCP server error:', err);
143
+ process.exit(1);
144
+ });
@@ -0,0 +1,101 @@
1
+ import { Router } from 'express';
2
+ import { getAllTasks, getTask, createTask, updateTask, deleteTask, getTaskExecutions, } from '../services/cronStorage.js';
3
+ import { refreshTask, removeTask, runTaskNow } from '../services/cronScheduler.js';
4
+ const router = Router();
5
+ // GET /api/cron — list all tasks
6
+ router.get('/', (_req, res) => {
7
+ try {
8
+ res.json(getAllTasks());
9
+ }
10
+ catch (err) {
11
+ console.error('Error fetching cron tasks:', err);
12
+ res.status(500).json({ error: 'Failed to fetch cron tasks' });
13
+ }
14
+ });
15
+ // POST /api/cron — create task
16
+ router.post('/', (req, res) => {
17
+ try {
18
+ const { name, cronExpression, prompt, enabled, projectPath } = req.body;
19
+ if (!name || !cronExpression || !prompt) {
20
+ res.status(400).json({ error: 'name, cronExpression, and prompt are required' });
21
+ return;
22
+ }
23
+ const task = createTask({
24
+ name,
25
+ cronExpression,
26
+ prompt,
27
+ enabled: enabled ?? true,
28
+ projectPath,
29
+ });
30
+ refreshTask(task.id);
31
+ res.status(201).json(task);
32
+ }
33
+ catch (err) {
34
+ console.error('Error creating cron task:', err);
35
+ res.status(500).json({ error: 'Failed to create cron task' });
36
+ }
37
+ });
38
+ // PUT /api/cron/:id — update task
39
+ router.put('/:id', (req, res) => {
40
+ try {
41
+ const task = updateTask(req.params.id, req.body);
42
+ if (!task) {
43
+ res.status(404).json({ error: 'Task not found' });
44
+ return;
45
+ }
46
+ refreshTask(task.id);
47
+ res.json(task);
48
+ }
49
+ catch (err) {
50
+ console.error('Error updating cron task:', err);
51
+ res.status(500).json({ error: 'Failed to update cron task' });
52
+ }
53
+ });
54
+ // DELETE /api/cron/:id — delete task
55
+ router.delete('/:id', (req, res) => {
56
+ try {
57
+ const ok = deleteTask(req.params.id);
58
+ if (!ok) {
59
+ res.status(404).json({ error: 'Task not found' });
60
+ return;
61
+ }
62
+ removeTask(req.params.id);
63
+ res.json({ success: true });
64
+ }
65
+ catch (err) {
66
+ console.error('Error deleting cron task:', err);
67
+ res.status(500).json({ error: 'Failed to delete cron task' });
68
+ }
69
+ });
70
+ // POST /api/cron/:id/run — manual trigger
71
+ router.post('/:id/run', (req, res) => {
72
+ try {
73
+ const task = getTask(req.params.id);
74
+ if (!task) {
75
+ res.status(404).json({ error: 'Task not found' });
76
+ return;
77
+ }
78
+ runTaskNow(task.id);
79
+ res.json({ success: true, message: 'Task triggered' });
80
+ }
81
+ catch (err) {
82
+ console.error('Error running cron task:', err);
83
+ res.status(500).json({ error: 'Failed to run cron task' });
84
+ }
85
+ });
86
+ // GET /api/cron/:id/executions — execution history
87
+ router.get('/:id/executions', (req, res) => {
88
+ try {
89
+ const task = getTask(req.params.id);
90
+ if (!task) {
91
+ res.status(404).json({ error: 'Task not found' });
92
+ return;
93
+ }
94
+ res.json(getTaskExecutions(task.id));
95
+ }
96
+ catch (err) {
97
+ console.error('Error fetching executions:', err);
98
+ res.status(500).json({ error: 'Failed to fetch executions' });
99
+ }
100
+ });
101
+ export default router;
@@ -0,0 +1,71 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { execSync } from 'child_process';
6
+ const router = Router();
7
+ /** Get git branch and repo name for a directory */
8
+ function getGitInfo(dirPath) {
9
+ try {
10
+ if (!fs.existsSync(path.join(dirPath, '.git')))
11
+ return null;
12
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
13
+ cwd: dirPath, encoding: 'utf-8', timeout: 3000,
14
+ }).trim();
15
+ // Repo name: top-level directory name of the git repo
16
+ const topLevel = execSync('git rev-parse --show-toplevel', {
17
+ cwd: dirPath, encoding: 'utf-8', timeout: 3000,
18
+ }).trim();
19
+ const repoName = path.basename(topLevel);
20
+ return { branch, repoName };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ // GET /api/filesystem/list?path=...
27
+ router.get('/list', (req, res) => {
28
+ try {
29
+ const targetPath = req.query.path || os.homedir();
30
+ const resolved = path.resolve(targetPath);
31
+ if (!fs.existsSync(resolved)) {
32
+ res.status(400).json({ error: 'Path does not exist' });
33
+ return;
34
+ }
35
+ if (!fs.statSync(resolved).isDirectory()) {
36
+ res.status(400).json({ error: 'Path is not a directory' });
37
+ return;
38
+ }
39
+ const parentPath = path.dirname(resolved) !== resolved ? path.dirname(resolved) : null;
40
+ // Check if current dir is a git repo
41
+ const isGitRepo = fs.existsSync(path.join(resolved, '.git'));
42
+ const gitInfo = isGitRepo ? getGitInfo(resolved) : null;
43
+ const dirents = fs.readdirSync(resolved, { withFileTypes: true });
44
+ const entries = [];
45
+ for (const d of dirents) {
46
+ // Skip hidden files and node_modules
47
+ if (d.name.startsWith('.') || d.name === 'node_modules')
48
+ continue;
49
+ if (!d.isDirectory())
50
+ continue;
51
+ const entryPath = path.join(resolved, d.name);
52
+ const entryIsGitRepo = fs.existsSync(path.join(entryPath, '.git'));
53
+ const entryGitInfo = entryIsGitRepo ? getGitInfo(entryPath) : null;
54
+ entries.push({
55
+ name: d.name,
56
+ path: entryPath,
57
+ isDirectory: true,
58
+ isGitRepo: entryIsGitRepo || undefined,
59
+ gitInfo: entryGitInfo || undefined,
60
+ });
61
+ }
62
+ // Sort alphabetically
63
+ entries.sort((a, b) => a.name.localeCompare(b.name));
64
+ res.json({ currentPath: resolved, parentPath, isGitRepo, gitInfo, entries });
65
+ }
66
+ catch (err) {
67
+ console.error('Error listing directory:', err);
68
+ res.status(500).json({ error: err.message || 'Failed to list directory' });
69
+ }
70
+ });
71
+ export default router;
@@ -0,0 +1,60 @@
1
+ import { Router } from 'express';
2
+ import { getAllGroups, createGroup, updateGroup, deleteGroup, } from '../services/groupStorage.js';
3
+ const router = Router();
4
+ // GET /api/todo/groups — list all groups
5
+ router.get('/', (_req, res) => {
6
+ try {
7
+ res.json(getAllGroups());
8
+ }
9
+ catch (err) {
10
+ console.error('Error fetching groups:', err);
11
+ res.status(500).json({ error: 'Failed to fetch groups' });
12
+ }
13
+ });
14
+ // POST /api/todo/groups — create group
15
+ router.post('/', (req, res) => {
16
+ try {
17
+ const { name } = req.body;
18
+ if (!name) {
19
+ res.status(400).json({ error: 'name is required' });
20
+ return;
21
+ }
22
+ const group = createGroup(name);
23
+ res.status(201).json(group);
24
+ }
25
+ catch (err) {
26
+ console.error('Error creating group:', err);
27
+ res.status(500).json({ error: 'Failed to create group' });
28
+ }
29
+ });
30
+ // PUT /api/todo/groups/:id — update group
31
+ router.put('/:id', (req, res) => {
32
+ try {
33
+ const group = updateGroup(req.params.id, req.body);
34
+ if (!group) {
35
+ res.status(404).json({ error: 'Group not found' });
36
+ return;
37
+ }
38
+ res.json(group);
39
+ }
40
+ catch (err) {
41
+ console.error('Error updating group:', err);
42
+ res.status(500).json({ error: 'Failed to update group' });
43
+ }
44
+ });
45
+ // DELETE /api/todo/groups/:id — delete group
46
+ router.delete('/:id', (req, res) => {
47
+ try {
48
+ const ok = deleteGroup(req.params.id);
49
+ if (!ok) {
50
+ res.status(404).json({ error: 'Group not found' });
51
+ return;
52
+ }
53
+ res.json({ success: true });
54
+ }
55
+ catch (err) {
56
+ console.error('Error deleting group:', err);
57
+ res.status(500).json({ error: 'Failed to delete group' });
58
+ }
59
+ });
60
+ export default router;