buddy-workbench 0.1.9 → 0.1.10

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.
@@ -0,0 +1,177 @@
1
+ import { Router } from 'express';
2
+ import { listTodos, saveTodos } from '../repositories/todos.js';
3
+
4
+ const router = Router();
5
+
6
+ function validTodo(body) {
7
+ const title = String(body?.title || '').trim();
8
+ if (!title) return null;
9
+
10
+ const priority = ['high', 'medium', 'low'].includes(body?.priority) ? body.priority : 'medium';
11
+ const category = String(body?.category || 'work').trim() || 'work';
12
+ const description = String(body?.description || '').trim();
13
+ const dueDate = body?.dueDate ? String(body.dueDate).trim() : null;
14
+ const completed = Boolean(body?.completed);
15
+ const archived = Boolean(body?.archived);
16
+
17
+ return {
18
+ title,
19
+ description,
20
+ priority,
21
+ category,
22
+ dueDate,
23
+ completed,
24
+ archived
25
+ };
26
+ }
27
+
28
+ router.get('/', (_req, res) => {
29
+ res.json(listTodos());
30
+ });
31
+
32
+ router.post('/', (req, res) => {
33
+ const data = validTodo(req.body);
34
+ if (!data) {
35
+ return res.status(400).json({ error: 'Title is required.' });
36
+ }
37
+
38
+ const now = new Date().toISOString();
39
+ const newTodo = {
40
+ id: crypto.randomUUID(),
41
+ ...data,
42
+ completedAt: data.completed ? now : null,
43
+ archivedAt: data.archived ? now : null,
44
+ createdAt: now,
45
+ updatedAt: now
46
+ };
47
+
48
+ const todos = listTodos();
49
+ todos.unshift(newTodo);
50
+ saveTodos(todos);
51
+
52
+ res.status(201).json(newTodo);
53
+ });
54
+
55
+ router.put('/:id', (req, res) => {
56
+ const { id } = req.params;
57
+ const todos = listTodos();
58
+ const index = todos.findIndex((item) => item.id === id);
59
+
60
+ if (index < 0) {
61
+ return res.status(404).json({ error: 'Todo item not found.' });
62
+ }
63
+
64
+ const data = validTodo(req.body);
65
+ if (!data) {
66
+ return res.status(400).json({ error: 'Title is required.' });
67
+ }
68
+
69
+ const existing = todos[index];
70
+ const now = new Date().toISOString();
71
+ const completedAt = data.completed
72
+ ? (existing.completed ? existing.completedAt : now)
73
+ : null;
74
+ const archivedAt = data.archived
75
+ ? (existing.archived ? existing.archivedAt : now)
76
+ : null;
77
+
78
+ const updated = {
79
+ ...existing,
80
+ ...data,
81
+ completedAt,
82
+ archivedAt,
83
+ updatedAt: now
84
+ };
85
+
86
+ todos[index] = updated;
87
+ saveTodos(todos);
88
+
89
+ res.json(updated);
90
+ });
91
+
92
+ router.post('/:id/archive', (req, res) => {
93
+ const { id } = req.params;
94
+ const todos = listTodos();
95
+ const index = todos.findIndex((item) => item.id === id);
96
+
97
+ if (index < 0) {
98
+ return res.status(404).json({ error: 'Todo item not found.' });
99
+ }
100
+
101
+ const now = new Date().toISOString();
102
+ const updated = {
103
+ ...todos[index],
104
+ archived: true,
105
+ archivedAt: now,
106
+ updatedAt: now
107
+ };
108
+
109
+ todos[index] = updated;
110
+ saveTodos(todos);
111
+ res.json(updated);
112
+ });
113
+
114
+ router.post('/:id/unarchive', (req, res) => {
115
+ const { id } = req.params;
116
+ const todos = listTodos();
117
+ const index = todos.findIndex((item) => item.id === id);
118
+
119
+ if (index < 0) {
120
+ return res.status(404).json({ error: 'Todo item not found.' });
121
+ }
122
+
123
+ const now = new Date().toISOString();
124
+ const updated = {
125
+ ...todos[index],
126
+ archived: false,
127
+ archivedAt: null,
128
+ updatedAt: now
129
+ };
130
+
131
+ todos[index] = updated;
132
+ saveTodos(todos);
133
+ res.json(updated);
134
+ });
135
+
136
+ router.delete('/:id', (req, res) => {
137
+ const { id } = req.params;
138
+ const todos = listTodos();
139
+ const next = todos.filter((item) => item.id !== id);
140
+
141
+ if (next.length === todos.length) {
142
+ return res.status(404).json({ error: 'Todo item not found.' });
143
+ }
144
+
145
+ saveTodos(next);
146
+ res.status(204).end();
147
+ });
148
+
149
+ router.post('/batch', (req, res) => {
150
+ const { action, ids } = req.body || {};
151
+ let todos = listTodos();
152
+ const now = new Date().toISOString();
153
+ const idSet = new Set(Array.isArray(ids) ? ids : []);
154
+
155
+ if (action === 'markCompleted') {
156
+ todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: true, completedAt: item.completedAt || now, updatedAt: now } : item));
157
+ } else if (action === 'markPending') {
158
+ todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: false, completedAt: null, updatedAt: now } : item));
159
+ } else if (action === 'archive') {
160
+ todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: true, archivedAt: now, updatedAt: now } : item));
161
+ } else if (action === 'unarchive') {
162
+ todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: false, archivedAt: null, updatedAt: now } : item));
163
+ } else if (action === 'delete') {
164
+ todos = todos.filter((item) => !idSet.has(item.id));
165
+ } else if (action === 'clearCompleted') {
166
+ todos = todos.filter((item) => !item.completed || item.archived);
167
+ } else if (action === 'clearArchived') {
168
+ todos = todos.filter((item) => !item.archived);
169
+ } else {
170
+ return res.status(400).json({ error: 'Invalid batch action.' });
171
+ }
172
+
173
+ saveTodos(todos);
174
+ res.json(todos);
175
+ });
176
+
177
+ export default router;
@@ -0,0 +1,28 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readSettings } from '../repositories/settings.js';
3
+
4
+ export function openInSystemBrowser(url) {
5
+ if (!url || typeof url !== 'string') return;
6
+ const settings = readSettings();
7
+ const browser = settings.defaultBrowser || 'chrome';
8
+
9
+ if (process.platform === 'darwin') {
10
+ const macAppMap = {
11
+ chrome: 'Google Chrome',
12
+ edge: 'Microsoft Edge',
13
+ safari: 'Safari'
14
+ };
15
+ const appName = macAppMap[browser] || 'Google Chrome';
16
+ execFile('open', ['-a', appName, url]);
17
+ } else if (process.platform === 'win32') {
18
+ const winCmdMap = {
19
+ chrome: 'chrome',
20
+ edge: 'msedge',
21
+ safari: 'msedge'
22
+ };
23
+ const cmd = winCmdMap[browser] || 'chrome';
24
+ execFile('cmd', ['/c', 'start', '', cmd, url]);
25
+ } else {
26
+ execFile('xdg-open', [url]);
27
+ }
28
+ }
@@ -19,8 +19,74 @@ export function clipboardDates() {
19
19
  return readdirSync(paths.clipboardDir, { withFileTypes: true }).filter((year) => year.isDirectory() && /^\d{4}$/.test(year.name)).flatMap((year) => readdirSync(join(paths.clipboardDir, year.name), { withFileTypes: true }).filter((month) => month.isDirectory() && /^\d{2}$/.test(month.name)).flatMap((month) => readdirSync(join(paths.clipboardDir, year.name, month.name), { withFileTypes: true }).filter((day) => day.isFile() && /^\d{2}\.json$/.test(day.name)).map((day) => `${year.name}-${month.name}-${day.name.slice(0, 2)}`))).sort().reverse();
20
20
  }
21
21
 
22
+ function getEditorUrl(editor = 'vscode', path = '') {
23
+ if (editor === 'idea') return `idea://open?file=${encodeURIComponent(path)}`;
24
+ if (editor === 'devin') return `devin://file${encodeURI(path)}`;
25
+ return `vscode://file${encodeURI(path)}`;
26
+ }
27
+
22
28
  function dayItems(date) { return readJson(dayFile(date), []); }
23
- export function clipboardItems(date = today()) { return dayItems(date).map((item) => item.contentFile ? { ...item, editorUrl: `vscode://file${encodeURI(join(paths.clipboardDir, 'content', item.contentFile))}` } : item); }
29
+ export function clipboardItems(date = today()) {
30
+ const settings = readSettings();
31
+ const editor = settings.defaultEditor || 'vscode';
32
+ const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
33
+ return dayItems(date).map((item) => item.contentFile ? { ...item, editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)), editorName } : item);
34
+ }
35
+ export function allTaggedClipboardItems() {
36
+ const settings = readSettings();
37
+ const editor = settings.defaultEditor || 'vscode';
38
+ const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
39
+ const dates = clipboardDates();
40
+ const result = [];
41
+
42
+ for (const date of dates) {
43
+ const items = dayItems(date);
44
+ for (const item of items) {
45
+ if (Array.isArray(item.tags) && item.tags.length > 0) {
46
+ result.push(
47
+ item.contentFile
48
+ ? {
49
+ ...item,
50
+ date,
51
+ editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
52
+ editorName
53
+ }
54
+ : { ...item, date }
55
+ );
56
+ }
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+
62
+ export function updateClipboardItemTags(date, id, tags) {
63
+ let targetDate = date;
64
+ let items = dayItems(targetDate);
65
+ let item = items.find((entry) => entry.id === id);
66
+
67
+ if (!item) {
68
+ const dates = clipboardDates();
69
+ for (const d of dates) {
70
+ const dayList = dayItems(d);
71
+ const found = dayList.find((entry) => entry.id === id);
72
+ if (found) {
73
+ targetDate = d;
74
+ items = dayList;
75
+ item = found;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+
81
+ if (!item) return null;
82
+
83
+ const validTags = Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [];
84
+ const updatedItem = { ...item, tags: validTags };
85
+ const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
86
+ writeJson(dayFile(targetDate), nextItems);
87
+ return updatedItem;
88
+ }
89
+
24
90
  export function clipboardOriginal(date, id) { const item = dayItems(date).find((entry) => entry.id === id); if (!item?.contentFile) return item?.text || null; try { return readFileSync(join(paths.clipboardDir, 'content', item.contentFile), 'utf8'); } catch { return null; } }
25
91
  export function deleteClipboardItem(date, id) {
26
92
  const items = dayItems(date); const item = items.find((entry) => entry.id === id);
@@ -82,18 +148,18 @@ export async function captureClipboard() {
82
148
  if (alreadyExists) return;
83
149
 
84
150
  const id = crypto.randomUUID(); const isLong = text.length > previewLimit;
85
- if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', `${id}.txt`), text, 'utf8'); }
86
- const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: `${id}.txt`, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
151
+ if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', id), text, 'utf8'); }
152
+ const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: id, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
87
153
  writeJson(dayFile(date), [item, ...items].slice(0, 200));
88
154
  } catch {}
89
155
  }
90
156
 
91
157
  export function startClipboardCapture() { captureClipboard(); const timer = setInterval(captureClipboard, 2000); timer.unref(); }
92
158
 
93
- export function saveClipboardImage(buffer) {
159
+ export function saveClipboardImage(buffer, ext = 'png') {
94
160
  const date = today();
95
161
  const id = crypto.randomUUID();
96
- const filename = `${id}.png`;
162
+ const filename = `${id}.${ext}`;
97
163
 
98
164
  mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
99
165
  writeFileSync(join(paths.clipboardDir, 'content', filename), buffer);
package/server.js CHANGED
@@ -13,6 +13,9 @@ import groupTaskRoutes from './server/routes/group-tasks.js';
13
13
  import portDiagnosticsRoutes from './server/routes/port-diagnostics.js';
14
14
  import settingsRoutes from './server/routes/settings.js';
15
15
  import prReviewRoutes from './server/routes/pr-review.js';
16
+ import jiraFiltersRoutes from './server/routes/jira-filters.js';
17
+ import todoRoutes from './server/routes/todos.js';
18
+ import staticPagesRoutes from './server/routes/static-pages.js';
16
19
  import { startClipboardCapture } from './server/services/clipboard-history.js';
17
20
 
18
21
  const port = Number(process.env.PORT || 3100);
@@ -27,6 +30,9 @@ app.use('/api/group-tasks', groupTaskRoutes);
27
30
  app.use('/api/port-diagnostics', portDiagnosticsRoutes);
28
31
  app.use('/api/settings', settingsRoutes);
29
32
  app.use('/api/pr-review', prReviewRoutes);
33
+ app.use('/api/jira-filters', jiraFiltersRoutes);
34
+ app.use('/api/todos', todoRoutes);
35
+ app.use('/api/static-pages', staticPagesRoutes);
30
36
 
31
37
  await freePort(port);
32
38
  const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });