loopwork 0.3.0 → 0.3.1

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/bin/loopwork +0 -0
  2. package/package.json +48 -4
  3. package/src/backends/github.ts +6 -3
  4. package/src/backends/json.ts +28 -10
  5. package/src/commands/run.ts +2 -2
  6. package/src/contracts/config.ts +3 -75
  7. package/src/contracts/index.ts +0 -6
  8. package/src/core/cli.ts +25 -16
  9. package/src/core/state.ts +10 -4
  10. package/src/core/utils.ts +10 -4
  11. package/src/monitor/index.ts +56 -34
  12. package/src/plugins/index.ts +9 -131
  13. package/examples/README.md +0 -70
  14. package/examples/basic-json-backend/.specs/tasks/TASK-001.md +0 -22
  15. package/examples/basic-json-backend/.specs/tasks/TASK-002.md +0 -23
  16. package/examples/basic-json-backend/.specs/tasks/TASK-003.md +0 -37
  17. package/examples/basic-json-backend/.specs/tasks/tasks.json +0 -19
  18. package/examples/basic-json-backend/README.md +0 -32
  19. package/examples/basic-json-backend/TESTING.md +0 -184
  20. package/examples/basic-json-backend/hello.test.ts +0 -9
  21. package/examples/basic-json-backend/hello.ts +0 -3
  22. package/examples/basic-json-backend/loopwork.config.js +0 -35
  23. package/examples/basic-json-backend/math.test.ts +0 -29
  24. package/examples/basic-json-backend/math.ts +0 -3
  25. package/examples/basic-json-backend/package.json +0 -15
  26. package/examples/basic-json-backend/quick-start.sh +0 -80
  27. package/loopwork.config.ts +0 -164
  28. package/src/plugins/asana.ts +0 -192
  29. package/src/plugins/cost-tracking.ts +0 -402
  30. package/src/plugins/discord.ts +0 -269
  31. package/src/plugins/everhour.ts +0 -335
  32. package/src/plugins/telegram/bot.ts +0 -517
  33. package/src/plugins/telegram/index.ts +0 -6
  34. package/src/plugins/telegram/notifications.ts +0 -198
  35. package/src/plugins/todoist.ts +0 -261
  36. package/test/backends.test.ts +0 -929
  37. package/test/cli.test.ts +0 -145
  38. package/test/config.test.ts +0 -90
  39. package/test/e2e.test.ts +0 -458
  40. package/test/github-tasks.test.ts +0 -191
  41. package/test/loopwork-config-types.test.ts +0 -288
  42. package/test/monitor.test.ts +0 -123
  43. package/test/plugins.test.ts +0 -1175
  44. package/test/state.test.ts +0 -295
  45. package/test/utils.test.ts +0 -60
  46. package/tsconfig.json +0 -20
@@ -1,80 +0,0 @@
1
- #!/bin/bash
2
- # Quick Start Script for Basic JSON Backend Example
3
-
4
- set -e
5
-
6
- echo "🚀 Loopwork Basic Example - Quick Start"
7
- echo "========================================"
8
- echo ""
9
-
10
- # Check if we're in the right directory
11
- if [ ! -f "loopwork.config.js" ] && [ ! -f "loopwork.config.ts" ]; then
12
- echo "❌ Error: Please run this script from the examples/basic-json-backend directory"
13
- exit 1
14
- fi
15
-
16
- echo "📋 Current Tasks:"
17
- cat .specs/tasks/tasks.json | grep -A 2 '"id"'
18
- echo ""
19
-
20
- echo "Choose an option:"
21
- echo "1) Dry Run (preview without executing)"
22
- echo "2) Run Loopwork (execute tasks)"
23
- echo "3) Reset tasks to pending"
24
- echo "4) View task details"
25
- echo ""
26
-
27
- read -p "Enter your choice (1-4): " choice
28
-
29
- case $choice in
30
- 1)
31
- echo ""
32
- echo "🔍 Running dry-run..."
33
- bun run ../../src/index.ts --dry-run
34
- ;;
35
- 2)
36
- echo ""
37
- echo "⚡ Running Loopwork..."
38
- bun run ../../src/index.ts
39
- ;;
40
- 3)
41
- echo ""
42
- echo "🔄 Resetting all tasks to pending..."
43
- cat > .specs/tasks/tasks.json << 'EOF'
44
- {
45
- "tasks": [
46
- {
47
- "id": "TASK-001",
48
- "status": "pending",
49
- "priority": "high"
50
- },
51
- {
52
- "id": "TASK-002",
53
- "status": "pending",
54
- "priority": "medium"
55
- },
56
- {
57
- "id": "TASK-003",
58
- "status": "pending",
59
- "priority": "low"
60
- }
61
- ]
62
- }
63
- EOF
64
- echo "✅ Tasks reset!"
65
- ;;
66
- 4)
67
- echo ""
68
- echo "📄 Task Details:"
69
- echo ""
70
- for task in .specs/tasks/TASK-*.md; do
71
- echo "----------------------------------------"
72
- head -5 "$task"
73
- echo ""
74
- done
75
- ;;
76
- *)
77
- echo "❌ Invalid choice"
78
- exit 1
79
- ;;
80
- esac
@@ -1,164 +0,0 @@
1
- import {
2
- defineConfig,
3
- withTelegram,
4
- withCostTracking,
5
- withJSON,
6
- withGitHub,
7
- withPlugin,
8
- withAsana,
9
- withEverhour,
10
- withTodoist,
11
- withDiscord,
12
- compose,
13
- } from './src/loopwork-config-types'
14
- import { withJSONBackend, withGitHubBackend } from './src/backend-plugin'
15
-
16
- /**
17
- * Loopwork Configuration
18
- *
19
- * This file configures the Loopwork task runner.
20
- * Similar to next.config.js, you can use plugin wrappers to add functionality.
21
- *
22
- * Backends are now plugins:
23
- * - withJSONBackend({ tasksFile: 'tasks.json' })
24
- * - withGitHubBackend({ repo: 'owner/repo' })
25
- */
26
-
27
- // =============================================================================
28
- // Full Example with All Plugins
29
- // =============================================================================
30
-
31
- export default compose(
32
- // Backend plugins (choose one)
33
- withJSONBackend({ tasksFile: '.specs/tasks/tasks.json' }),
34
- // withGitHubBackend({ repo: 'owner/repo' }),
35
-
36
- // Legacy backend config (still supported)
37
- // withJSON({ tasksFile: '.specs/tasks/tasks.json' }),
38
- // withGitHub({ repo: 'owner/repo' }),
39
-
40
- // Telegram notifications on task events
41
- withTelegram({
42
- botToken: process.env.TELEGRAM_BOT_TOKEN,
43
- chatId: process.env.TELEGRAM_CHAT_ID,
44
- silent: false,
45
- }),
46
-
47
- // Asana integration: sync task status to Asana project
48
- // Tasks should have metadata.asanaGid set in the tasks file
49
- // withAsana({
50
- // projectId: process.env.ASANA_PROJECT_ID,
51
- // syncStatus: true,
52
- // }),
53
-
54
- // Everhour time tracking: auto-track time spent on tasks
55
- // Uses metadata.everhourId or metadata.asanaGid (auto-prefixed with 'as:')
56
- // withEverhour({
57
- // autoStartTimer: true,
58
- // autoStopTimer: true,
59
- // }),
60
-
61
- // Todoist integration: sync task status to Todoist
62
- // Tasks should have metadata.todoistId set
63
- // withTodoist({
64
- // projectId: process.env.TODOIST_PROJECT_ID,
65
- // syncStatus: true,
66
- // addComments: true,
67
- // }),
68
-
69
- // Discord notifications via webhook
70
- // withDiscord({
71
- // webhookUrl: process.env.DISCORD_WEBHOOK_URL,
72
- // username: 'Loopwork',
73
- // notifyOnComplete: true,
74
- // notifyOnFail: true,
75
- // mentionOnFail: '<@&123456>', // mention role on failures
76
- // }),
77
-
78
- // Cost tracking for token usage
79
- withCostTracking({
80
- enabled: true,
81
- defaultModel: 'claude-3.5-sonnet',
82
- }),
83
-
84
- // Dashboard TUI: live progress display
85
- // Requires: bun add ink react @types/react
86
- // withPlugin(createDashboardPlugin({ totalTasks: 10 })),
87
-
88
- // Custom plugin: Log to console
89
- withPlugin({
90
- name: 'console-logger',
91
- onLoopStart: (namespace) => {
92
- console.log(`\n🚀 Loop starting in namespace: ${namespace}\n`)
93
- },
94
- onTaskStart: (task) => {
95
- console.log(`📋 Starting: ${task.id} - ${task.title}`)
96
- },
97
- onTaskComplete: (task, result) => {
98
- console.log(`✅ Completed: ${task.id} in ${result.duration}s`)
99
- },
100
- onTaskFailed: (task, error) => {
101
- console.log(`❌ Failed: ${task.id} - ${error}`)
102
- },
103
- onLoopEnd: (stats) => {
104
- console.log(`\n📊 Loop finished: ${stats.completed} completed, ${stats.failed} failed\n`)
105
- },
106
- }),
107
-
108
- // Custom plugin: Slack webhook (example)
109
- // withPlugin({
110
- // name: 'slack-notify',
111
- // onTaskComplete: async (task) => {
112
- // await fetch(process.env.SLACK_WEBHOOK_URL, {
113
- // method: 'POST',
114
- // headers: { 'Content-Type': 'application/json' },
115
- // body: JSON.stringify({
116
- // text: `✅ Task completed: ${task.id} - ${task.title}`,
117
- // }),
118
- // })
119
- // },
120
- // }),
121
- )(defineConfig({
122
- // AI CLI tool: 'opencode', 'claude', or 'gemini'
123
- cli: 'opencode',
124
-
125
- // Loop settings
126
- maxIterations: 50,
127
- timeout: 600, // seconds per task
128
- namespace: 'default', // for concurrent loops
129
-
130
- // Behavior
131
- autoConfirm: false, // -y flag
132
- dryRun: false,
133
- debug: false,
134
-
135
- // Retry settings
136
- maxRetries: 3,
137
- circuitBreakerThreshold: 5,
138
- taskDelay: 2000, // ms between tasks
139
- retryDelay: 3000, // ms before retry
140
- }))
141
-
142
- // =============================================================================
143
- // Alternative: Backend Plugins Pattern (recommended)
144
- // =============================================================================
145
-
146
- // export default compose(
147
- // withJSONBackend({ tasksFile: 'tasks.json' }),
148
- // withTelegram(),
149
- // withAsana(),
150
- // withEverhour(),
151
- // )(defineConfig({ cli: 'opencode' }))
152
-
153
- // =============================================================================
154
- // Alternative: GitHub Backend Plugin
155
- // =============================================================================
156
-
157
- // export default compose(
158
- // withGitHubBackend({ repo: 'myorg/myrepo' }),
159
- // withTelegram(),
160
- // withDiscord({ webhookUrl: process.env.DISCORD_WEBHOOK_URL }),
161
- // )(defineConfig({
162
- // cli: 'claude',
163
- // feature: 'auth', // filter by feature label
164
- // }))
@@ -1,192 +0,0 @@
1
- /**
2
- * Asana Plugin for Loopwork
3
- *
4
- * Syncs task status with Asana projects.
5
- * Tasks should have metadata.asanaGid set to the Asana task GID.
6
- *
7
- * Setup:
8
- * 1. Get Personal Access Token from Asana Developer Console
9
- * 2. Set ASANA_ACCESS_TOKEN env var
10
- * 3. Set ASANA_PROJECT_ID env var (from project URL)
11
- * 4. Add asanaGid to task metadata in your tasks file
12
- */
13
-
14
- import type { LoopworkPlugin, PluginTask } from '../contracts'
15
-
16
- export interface AsanaConfig {
17
- accessToken?: string
18
- projectId?: string
19
- workspaceId?: string
20
- /** Create Asana tasks for new Loopwork tasks */
21
- autoCreate?: boolean
22
- /** Sync status changes to Asana */
23
- syncStatus?: boolean
24
- }
25
-
26
- interface AsanaTask {
27
- gid: string
28
- name: string
29
- completed: boolean
30
- notes?: string
31
- }
32
-
33
- interface AsanaResponse<T> {
34
- data: T
35
- }
36
-
37
- export class AsanaClient {
38
- private baseUrl = 'https://app.asana.com/api/1.0'
39
- private accessToken: string
40
-
41
- constructor(accessToken: string) {
42
- this.accessToken = accessToken
43
- }
44
-
45
- private async request<T>(
46
- method: string,
47
- endpoint: string,
48
- body?: Record<string, unknown>
49
- ): Promise<T> {
50
- const url = `${this.baseUrl}${endpoint}`
51
- const response = await fetch(url, {
52
- method,
53
- headers: {
54
- 'Authorization': `Bearer ${this.accessToken}`,
55
- 'Content-Type': 'application/json',
56
- },
57
- body: body ? JSON.stringify({ data: body }) : undefined,
58
- })
59
-
60
- if (!response.ok) {
61
- const error = await response.text()
62
- throw new Error(`Asana API error: ${response.status} - ${error}`)
63
- }
64
-
65
- const result = await response.json() as AsanaResponse<T>
66
- return result.data
67
- }
68
-
69
- async getTask(taskGid: string): Promise<AsanaTask> {
70
- return this.request('GET', `/tasks/${taskGid}`)
71
- }
72
-
73
- async createTask(projectId: string, name: string, notes?: string): Promise<AsanaTask> {
74
- return this.request('POST', '/tasks', {
75
- name,
76
- notes,
77
- projects: [projectId],
78
- })
79
- }
80
-
81
- async updateTask(taskGid: string, updates: Partial<{ name: string; notes: string; completed: boolean }>): Promise<AsanaTask> {
82
- return this.request('PUT', `/tasks/${taskGid}`, updates)
83
- }
84
-
85
- async completeTask(taskGid: string): Promise<AsanaTask> {
86
- return this.updateTask(taskGid, { completed: true })
87
- }
88
-
89
- async addComment(taskGid: string, text: string): Promise<void> {
90
- await this.request('POST', `/tasks/${taskGid}/stories`, { text })
91
- }
92
-
93
- async getProjectTasks(projectId: string): Promise<AsanaTask[]> {
94
- return this.request('GET', `/projects/${projectId}/tasks?opt_fields=gid,name,completed,notes`)
95
- }
96
- }
97
-
98
- /**
99
- * Create Asana plugin wrapper
100
- */
101
- export function withAsana(config: AsanaConfig = {}) {
102
- const accessToken = config.accessToken || process.env.ASANA_ACCESS_TOKEN
103
- const projectId = config.projectId || process.env.ASANA_PROJECT_ID
104
-
105
- return (baseConfig: any) => ({
106
- ...baseConfig,
107
- asana: {
108
- accessToken,
109
- projectId,
110
- autoCreate: config.autoCreate ?? false,
111
- syncStatus: config.syncStatus ?? true,
112
- },
113
- })
114
- }
115
-
116
- /** Helper to get Asana GID from task metadata */
117
- function getAsanaGid(task: PluginTask): string | undefined {
118
- return task.metadata?.asanaGid as string | undefined
119
- }
120
-
121
- /**
122
- * Create Asana hook plugin
123
- *
124
- * Tasks should have metadata.asanaGid set for Asana integration.
125
- */
126
- export function createAsanaPlugin(config: AsanaConfig = {}): LoopworkPlugin {
127
- const accessToken = config.accessToken || process.env.ASANA_ACCESS_TOKEN || ''
128
- const projectId = config.projectId || process.env.ASANA_PROJECT_ID || ''
129
-
130
- if (!accessToken || !projectId) {
131
- return {
132
- name: 'asana',
133
- onConfigLoad: (cfg) => {
134
- console.warn('Asana plugin: Missing ASANA_ACCESS_TOKEN or ASANA_PROJECT_ID')
135
- return cfg
136
- },
137
- }
138
- }
139
-
140
- const client = new AsanaClient(accessToken)
141
-
142
- return {
143
- name: 'asana',
144
-
145
- async onTaskStart(task) {
146
- const asanaGid = getAsanaGid(task)
147
- if (!asanaGid) return
148
-
149
- try {
150
- await client.addComment(asanaGid, `🔄 Loopwork started working on this task`)
151
- } catch (e: any) {
152
- console.warn(`Asana: Failed to add comment: ${e.message}`)
153
- }
154
- },
155
-
156
- async onTaskComplete(task, result) {
157
- const asanaGid = getAsanaGid(task)
158
- if (!asanaGid) return
159
-
160
- try {
161
- if (config.syncStatus !== false) {
162
- await client.completeTask(asanaGid)
163
- }
164
- await client.addComment(
165
- asanaGid,
166
- `✅ Completed by Loopwork in ${Math.round(result.duration)}s`
167
- )
168
- } catch (e: any) {
169
- console.warn(`Asana: Failed to update task: ${e.message}`)
170
- }
171
- },
172
-
173
- async onTaskFailed(task, error) {
174
- const asanaGid = getAsanaGid(task)
175
- if (!asanaGid) return
176
-
177
- try {
178
- await client.addComment(
179
- asanaGid,
180
- `❌ Loopwork failed: ${error.slice(0, 200)}`
181
- )
182
- } catch (e: any) {
183
- console.warn(`Asana: Failed to add comment: ${e.message}`)
184
- }
185
- },
186
-
187
- async onLoopEnd(stats) {
188
- // Could post a summary to a specific task or project
189
- console.log(`📊 Asana sync: ${stats.completed} tasks synced`)
190
- },
191
- }
192
- }