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.
- package/bin/loopwork +0 -0
- package/package.json +48 -4
- package/src/backends/github.ts +6 -3
- package/src/backends/json.ts +28 -10
- package/src/commands/run.ts +2 -2
- package/src/contracts/config.ts +3 -75
- package/src/contracts/index.ts +0 -6
- package/src/core/cli.ts +25 -16
- package/src/core/state.ts +10 -4
- package/src/core/utils.ts +10 -4
- package/src/monitor/index.ts +56 -34
- package/src/plugins/index.ts +9 -131
- package/examples/README.md +0 -70
- package/examples/basic-json-backend/.specs/tasks/TASK-001.md +0 -22
- package/examples/basic-json-backend/.specs/tasks/TASK-002.md +0 -23
- package/examples/basic-json-backend/.specs/tasks/TASK-003.md +0 -37
- package/examples/basic-json-backend/.specs/tasks/tasks.json +0 -19
- package/examples/basic-json-backend/README.md +0 -32
- package/examples/basic-json-backend/TESTING.md +0 -184
- package/examples/basic-json-backend/hello.test.ts +0 -9
- package/examples/basic-json-backend/hello.ts +0 -3
- package/examples/basic-json-backend/loopwork.config.js +0 -35
- package/examples/basic-json-backend/math.test.ts +0 -29
- package/examples/basic-json-backend/math.ts +0 -3
- package/examples/basic-json-backend/package.json +0 -15
- package/examples/basic-json-backend/quick-start.sh +0 -80
- package/loopwork.config.ts +0 -164
- package/src/plugins/asana.ts +0 -192
- package/src/plugins/cost-tracking.ts +0 -402
- package/src/plugins/discord.ts +0 -269
- package/src/plugins/everhour.ts +0 -335
- package/src/plugins/telegram/bot.ts +0 -517
- package/src/plugins/telegram/index.ts +0 -6
- package/src/plugins/telegram/notifications.ts +0 -198
- package/src/plugins/todoist.ts +0 -261
- package/test/backends.test.ts +0 -929
- package/test/cli.test.ts +0 -145
- package/test/config.test.ts +0 -90
- package/test/e2e.test.ts +0 -458
- package/test/github-tasks.test.ts +0 -191
- package/test/loopwork-config-types.test.ts +0 -288
- package/test/monitor.test.ts +0 -123
- package/test/plugins.test.ts +0 -1175
- package/test/state.test.ts +0 -295
- package/test/utils.test.ts +0 -60
- 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
|
package/loopwork.config.ts
DELETED
|
@@ -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
|
-
// }))
|
package/src/plugins/asana.ts
DELETED
|
@@ -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
|
-
}
|