loopwork 0.3.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 (62) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +528 -0
  3. package/bin/loopwork +0 -0
  4. package/examples/README.md +70 -0
  5. package/examples/basic-json-backend/.specs/tasks/TASK-001.md +22 -0
  6. package/examples/basic-json-backend/.specs/tasks/TASK-002.md +23 -0
  7. package/examples/basic-json-backend/.specs/tasks/TASK-003.md +37 -0
  8. package/examples/basic-json-backend/.specs/tasks/tasks.json +19 -0
  9. package/examples/basic-json-backend/README.md +32 -0
  10. package/examples/basic-json-backend/TESTING.md +184 -0
  11. package/examples/basic-json-backend/hello.test.ts +9 -0
  12. package/examples/basic-json-backend/hello.ts +3 -0
  13. package/examples/basic-json-backend/loopwork.config.js +35 -0
  14. package/examples/basic-json-backend/math.test.ts +29 -0
  15. package/examples/basic-json-backend/math.ts +3 -0
  16. package/examples/basic-json-backend/package.json +15 -0
  17. package/examples/basic-json-backend/quick-start.sh +80 -0
  18. package/loopwork.config.ts +164 -0
  19. package/package.json +26 -0
  20. package/src/backends/github.ts +426 -0
  21. package/src/backends/index.ts +86 -0
  22. package/src/backends/json.ts +598 -0
  23. package/src/backends/plugin.ts +317 -0
  24. package/src/backends/types.ts +19 -0
  25. package/src/commands/init.ts +100 -0
  26. package/src/commands/run.ts +365 -0
  27. package/src/contracts/backend.ts +127 -0
  28. package/src/contracts/config.ts +129 -0
  29. package/src/contracts/index.ts +43 -0
  30. package/src/contracts/plugin.ts +82 -0
  31. package/src/contracts/task.ts +78 -0
  32. package/src/core/cli.ts +275 -0
  33. package/src/core/config.ts +165 -0
  34. package/src/core/state.ts +154 -0
  35. package/src/core/utils.ts +125 -0
  36. package/src/dashboard/cli.ts +449 -0
  37. package/src/dashboard/index.ts +6 -0
  38. package/src/dashboard/kanban.tsx +226 -0
  39. package/src/dashboard/tui.tsx +372 -0
  40. package/src/index.ts +19 -0
  41. package/src/mcp/server.ts +451 -0
  42. package/src/monitor/index.ts +420 -0
  43. package/src/plugins/asana.ts +192 -0
  44. package/src/plugins/cost-tracking.ts +402 -0
  45. package/src/plugins/discord.ts +269 -0
  46. package/src/plugins/everhour.ts +335 -0
  47. package/src/plugins/index.ts +253 -0
  48. package/src/plugins/telegram/bot.ts +517 -0
  49. package/src/plugins/telegram/index.ts +6 -0
  50. package/src/plugins/telegram/notifications.ts +198 -0
  51. package/src/plugins/todoist.ts +261 -0
  52. package/test/backends.test.ts +929 -0
  53. package/test/cli.test.ts +145 -0
  54. package/test/config.test.ts +90 -0
  55. package/test/e2e.test.ts +458 -0
  56. package/test/github-tasks.test.ts +191 -0
  57. package/test/loopwork-config-types.test.ts +288 -0
  58. package/test/monitor.test.ts +123 -0
  59. package/test/plugins.test.ts +1175 -0
  60. package/test/state.test.ts +295 -0
  61. package/test/utils.test.ts +60 -0
  62. package/tsconfig.json +20 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Todoist Plugin for Loopwork
3
+ *
4
+ * Syncs task status with Todoist projects.
5
+ * Tasks should have metadata.todoistId set to the Todoist task ID.
6
+ *
7
+ * Setup:
8
+ * 1. Get API token from Todoist Settings > Integrations > Developer
9
+ * 2. Set TODOIST_API_TOKEN env var
10
+ * 3. Add todoistId to task metadata in your tasks file
11
+ */
12
+
13
+ import type { LoopworkPlugin, PluginTask } from '../contracts'
14
+
15
+ export interface TodoistConfig {
16
+ apiToken?: string
17
+ projectId?: string
18
+ /** Sync status changes to Todoist (complete tasks) */
19
+ syncStatus?: boolean
20
+ /** Add comments to tasks on events */
21
+ addComments?: boolean
22
+ }
23
+
24
+ interface TodoistTask {
25
+ id: string
26
+ content: string
27
+ description: string
28
+ is_completed: boolean
29
+ project_id: string
30
+ priority: number
31
+ labels: string[]
32
+ }
33
+
34
+ interface TodoistComment {
35
+ id: string
36
+ task_id: string
37
+ content: string
38
+ posted_at: string
39
+ }
40
+
41
+ export class TodoistClient {
42
+ private baseUrl = 'https://api.todoist.com/rest/v2'
43
+ private apiToken: string
44
+
45
+ constructor(apiToken: string) {
46
+ this.apiToken = apiToken
47
+ }
48
+
49
+ private async request<T>(
50
+ method: string,
51
+ endpoint: string,
52
+ body?: Record<string, unknown>
53
+ ): Promise<T> {
54
+ const url = `${this.baseUrl}${endpoint}`
55
+ const response = await fetch(url, {
56
+ method,
57
+ headers: {
58
+ 'Authorization': `Bearer ${this.apiToken}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: body ? JSON.stringify(body) : undefined,
62
+ })
63
+
64
+ if (!response.ok) {
65
+ const error = await response.text()
66
+ throw new Error(`Todoist API error: ${response.status} - ${error}`)
67
+ }
68
+
69
+ // Some endpoints return empty response (204)
70
+ if (response.status === 204) {
71
+ return {} as T
72
+ }
73
+
74
+ return response.json() as Promise<T>
75
+ }
76
+
77
+ /**
78
+ * Get a task by ID
79
+ */
80
+ async getTask(taskId: string): Promise<TodoistTask> {
81
+ return this.request('GET', `/tasks/${taskId}`)
82
+ }
83
+
84
+ /**
85
+ * Create a new task
86
+ */
87
+ async createTask(content: string, options?: {
88
+ description?: string
89
+ projectId?: string
90
+ priority?: 1 | 2 | 3 | 4
91
+ labels?: string[]
92
+ }): Promise<TodoistTask> {
93
+ return this.request('POST', '/tasks', {
94
+ content,
95
+ description: options?.description,
96
+ project_id: options?.projectId,
97
+ priority: options?.priority,
98
+ labels: options?.labels,
99
+ })
100
+ }
101
+
102
+ /**
103
+ * Update a task
104
+ */
105
+ async updateTask(taskId: string, updates: {
106
+ content?: string
107
+ description?: string
108
+ priority?: 1 | 2 | 3 | 4
109
+ labels?: string[]
110
+ }): Promise<TodoistTask> {
111
+ return this.request('POST', `/tasks/${taskId}`, updates)
112
+ }
113
+
114
+ /**
115
+ * Complete (close) a task
116
+ */
117
+ async completeTask(taskId: string): Promise<void> {
118
+ await this.request('POST', `/tasks/${taskId}/close`)
119
+ }
120
+
121
+ /**
122
+ * Reopen a completed task
123
+ */
124
+ async reopenTask(taskId: string): Promise<void> {
125
+ await this.request('POST', `/tasks/${taskId}/reopen`)
126
+ }
127
+
128
+ /**
129
+ * Delete a task
130
+ */
131
+ async deleteTask(taskId: string): Promise<void> {
132
+ await this.request('DELETE', `/tasks/${taskId}`)
133
+ }
134
+
135
+ /**
136
+ * Get all tasks in a project
137
+ */
138
+ async getProjectTasks(projectId: string): Promise<TodoistTask[]> {
139
+ return this.request('GET', `/tasks?project_id=${projectId}`)
140
+ }
141
+
142
+ /**
143
+ * Add a comment to a task
144
+ */
145
+ async addComment(taskId: string, content: string): Promise<TodoistComment> {
146
+ return this.request('POST', '/comments', {
147
+ task_id: taskId,
148
+ content,
149
+ })
150
+ }
151
+
152
+ /**
153
+ * Get comments for a task
154
+ */
155
+ async getComments(taskId: string): Promise<TodoistComment[]> {
156
+ return this.request('GET', `/comments?task_id=${taskId}`)
157
+ }
158
+
159
+ /**
160
+ * Get all projects
161
+ */
162
+ async getProjects(): Promise<Array<{ id: string; name: string }>> {
163
+ return this.request('GET', '/projects')
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Create Todoist plugin wrapper
169
+ */
170
+ export function withTodoist(config: TodoistConfig = {}) {
171
+ const apiToken = config.apiToken || process.env.TODOIST_API_TOKEN
172
+
173
+ return (baseConfig: any) => ({
174
+ ...baseConfig,
175
+ todoist: {
176
+ apiToken,
177
+ projectId: config.projectId || process.env.TODOIST_PROJECT_ID,
178
+ syncStatus: config.syncStatus ?? true,
179
+ addComments: config.addComments ?? true,
180
+ },
181
+ })
182
+ }
183
+
184
+ /** Helper to get Todoist ID from task metadata */
185
+ function getTodoistId(task: PluginTask): string | undefined {
186
+ return task.metadata?.todoistId as string | undefined
187
+ }
188
+
189
+ /**
190
+ * Create Todoist hook plugin
191
+ *
192
+ * Tasks should have metadata.todoistId set for Todoist integration.
193
+ */
194
+ export function createTodoistPlugin(config: TodoistConfig = {}): LoopworkPlugin {
195
+ const apiToken = config.apiToken || process.env.TODOIST_API_TOKEN || ''
196
+ const addComments = config.addComments ?? true
197
+
198
+ if (!apiToken) {
199
+ return {
200
+ name: 'todoist',
201
+ onConfigLoad: (cfg) => {
202
+ console.warn('Todoist plugin: Missing TODOIST_API_TOKEN')
203
+ return cfg
204
+ },
205
+ }
206
+ }
207
+
208
+ const client = new TodoistClient(apiToken)
209
+
210
+ return {
211
+ name: 'todoist',
212
+
213
+ async onTaskStart(task) {
214
+ const todoistId = getTodoistId(task)
215
+ if (!todoistId || !addComments) return
216
+
217
+ try {
218
+ await client.addComment(todoistId, `🔄 Loopwork started working on this task`)
219
+ } catch (e: any) {
220
+ console.warn(`Todoist: Failed to add comment: ${e.message}`)
221
+ }
222
+ },
223
+
224
+ async onTaskComplete(task, result) {
225
+ const todoistId = getTodoistId(task)
226
+ if (!todoistId) return
227
+
228
+ try {
229
+ if (config.syncStatus !== false) {
230
+ await client.completeTask(todoistId)
231
+ }
232
+ if (addComments) {
233
+ await client.addComment(
234
+ todoistId,
235
+ `✅ Completed by Loopwork in ${Math.round(result.duration)}s`
236
+ )
237
+ }
238
+ } catch (e: any) {
239
+ console.warn(`Todoist: Failed to update task: ${e.message}`)
240
+ }
241
+ },
242
+
243
+ async onTaskFailed(task, error) {
244
+ const todoistId = getTodoistId(task)
245
+ if (!todoistId || !addComments) return
246
+
247
+ try {
248
+ await client.addComment(
249
+ todoistId,
250
+ `❌ Loopwork failed: ${error.slice(0, 200)}`
251
+ )
252
+ } catch (e: any) {
253
+ console.warn(`Todoist: Failed to add comment: ${e.message}`)
254
+ }
255
+ },
256
+
257
+ async onLoopEnd(stats) {
258
+ console.log(`📋 Todoist sync: ${stats.completed} tasks synced`)
259
+ },
260
+ }
261
+ }