digital-tasks 2.1.3 → 2.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.
- package/CHANGELOG.md +9 -0
- package/README.md +560 -0
- package/package.json +29 -14
- package/src/client.ts +268 -0
- package/src/index.ts +12 -10
- package/src/markdown.ts +63 -48
- package/src/project.ts +57 -42
- package/src/queue.ts +76 -37
- package/src/task.ts +132 -75
- package/src/types.ts +177 -40
- package/src/worker.ts +959 -0
- package/test/project.test.ts +28 -84
- package/test/queue.test.ts +51 -24
- package/test/task.test.ts +80 -27
- package/test/worker.test.ts +1158 -0
- package/tsconfig.json +2 -13
- package/vitest.config.ts +48 -0
- package/wrangler.jsonc +44 -0
- package/.turbo/turbo-build.log +0 -4
- package/LICENSE +0 -21
- package/dist/function-task.d.ts +0 -319
- package/dist/function-task.d.ts.map +0 -1
- package/dist/function-task.js +0 -286
- package/dist/function-task.js.map +0 -1
- package/dist/index.d.ts +0 -72
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -74
- package/dist/index.js.map +0 -1
- package/dist/markdown.d.ts +0 -112
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js +0 -510
- package/dist/markdown.js.map +0 -1
- package/dist/project.d.ts +0 -259
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -397
- package/dist/project.js.map +0 -1
- package/dist/queue.d.ts +0 -17
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -347
- package/dist/queue.js.map +0 -1
- package/dist/task.d.ts +0 -69
- package/dist/task.d.ts.map +0 -1
- package/dist/task.js +0 -321
- package/dist/task.js.map +0 -1
- package/dist/types.d.ts +0 -292
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -15
- package/dist/types.js.map +0 -1
- package/src/index.js +0 -73
- package/src/markdown.js +0 -509
- package/src/project.js +0 -396
- package/src/queue.js +0 -346
- package/src/task.js +0 -320
- package/src/types.js +0 -14
package/src/client.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client for digital-tasks
|
|
3
|
+
*
|
|
4
|
+
* Connects to a deployed digital-tasks worker via rpc.do,
|
|
5
|
+
* providing a fully typed client for remote task management.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createTaskClient } from 'digital-tasks/client'
|
|
10
|
+
*
|
|
11
|
+
* const tasks = createTaskClient('https://digital-tasks.workers.dev')
|
|
12
|
+
* const task = await tasks.create({ name: 'My Task', description: 'Do something', priority: 'high' })
|
|
13
|
+
* await tasks.execute(task.id)
|
|
14
|
+
* await tasks.complete(task.id, { result: 'done' })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { RPC, http } from 'rpc.do'
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types (mirrored from worker.ts for client-side use)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export type TaskStatus =
|
|
27
|
+
| 'pending'
|
|
28
|
+
| 'scheduled'
|
|
29
|
+
| 'queued'
|
|
30
|
+
| 'blocked'
|
|
31
|
+
| 'in_progress'
|
|
32
|
+
| 'completed'
|
|
33
|
+
| 'failed'
|
|
34
|
+
| 'cancelled'
|
|
35
|
+
|
|
36
|
+
export type TaskPriority = 'low' | 'normal' | 'high' | 'urgent' | 'critical'
|
|
37
|
+
|
|
38
|
+
export interface WorkerRef {
|
|
39
|
+
type: 'agent' | 'human' | 'team' | 'any'
|
|
40
|
+
id: string
|
|
41
|
+
name?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TaskDependency {
|
|
45
|
+
type: 'blocked_by'
|
|
46
|
+
taskId: string
|
|
47
|
+
satisfied: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TaskProgress {
|
|
51
|
+
percent: number
|
|
52
|
+
step?: string
|
|
53
|
+
updatedAt: Date
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TaskAssignment {
|
|
57
|
+
worker: WorkerRef
|
|
58
|
+
assignedAt: Date
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TaskData<TInput = unknown, TOutput = unknown> {
|
|
62
|
+
id: string
|
|
63
|
+
name: string
|
|
64
|
+
description: string
|
|
65
|
+
status: TaskStatus
|
|
66
|
+
priority: TaskPriority
|
|
67
|
+
input?: TInput
|
|
68
|
+
output?: TOutput
|
|
69
|
+
error?: string
|
|
70
|
+
scheduledFor?: Date
|
|
71
|
+
deadline?: Date
|
|
72
|
+
createdAt: Date
|
|
73
|
+
startedAt?: Date
|
|
74
|
+
completedAt?: Date
|
|
75
|
+
progress?: TaskProgress
|
|
76
|
+
dependencies?: TaskDependency[]
|
|
77
|
+
assignment?: TaskAssignment
|
|
78
|
+
metadata?: Record<string, unknown>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CreateTaskOptions<TInput = unknown> {
|
|
82
|
+
id?: string
|
|
83
|
+
name: string
|
|
84
|
+
description: string
|
|
85
|
+
priority?: TaskPriority
|
|
86
|
+
input?: TInput
|
|
87
|
+
scheduledFor?: Date
|
|
88
|
+
deadline?: Date
|
|
89
|
+
tags?: string[]
|
|
90
|
+
metadata?: Record<string, unknown>
|
|
91
|
+
dependencies?: string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ScheduleOptions {
|
|
95
|
+
priority?: TaskPriority
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ExecuteOptions {
|
|
99
|
+
worker?: WorkerRef
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ListOptions {
|
|
103
|
+
status?: TaskStatus | TaskStatus[]
|
|
104
|
+
priority?: TaskPriority
|
|
105
|
+
tags?: string[]
|
|
106
|
+
search?: string
|
|
107
|
+
sortBy?: 'createdAt' | 'priority'
|
|
108
|
+
sortOrder?: 'asc' | 'desc'
|
|
109
|
+
limit?: number
|
|
110
|
+
offset?: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface EnqueueOptions {
|
|
114
|
+
delaySeconds?: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TaskStats {
|
|
118
|
+
total: number
|
|
119
|
+
byStatus: Record<string, number>
|
|
120
|
+
byPriority: Record<string, number>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// TaskServiceAPI - the RPC interface for the client
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The RPC API surface exposed by the digital-tasks worker.
|
|
129
|
+
*
|
|
130
|
+
* This interface mirrors the methods on TaskServiceCore (worker.ts)
|
|
131
|
+
* and is used to type the RPC client proxy.
|
|
132
|
+
*/
|
|
133
|
+
export interface TaskServiceAPI {
|
|
134
|
+
/** Create a new task */
|
|
135
|
+
create(options: CreateTaskOptions): Promise<TaskData>
|
|
136
|
+
|
|
137
|
+
/** Schedule a task for future execution */
|
|
138
|
+
schedule(taskId: string, scheduledFor: Date, options?: ScheduleOptions): Promise<TaskData>
|
|
139
|
+
|
|
140
|
+
/** Start task execution */
|
|
141
|
+
execute(taskId: string, options?: ExecuteOptions): Promise<TaskData>
|
|
142
|
+
|
|
143
|
+
/** Complete a task with output */
|
|
144
|
+
complete(taskId: string, output: unknown): Promise<TaskData>
|
|
145
|
+
|
|
146
|
+
/** Fail a task with error */
|
|
147
|
+
fail(taskId: string, error: string | Error): Promise<TaskData>
|
|
148
|
+
|
|
149
|
+
/** Get task status */
|
|
150
|
+
getStatus(taskId: string): Promise<TaskData | null>
|
|
151
|
+
|
|
152
|
+
/** Update task progress */
|
|
153
|
+
updateProgress(taskId: string, percent: number, step?: string): Promise<TaskData>
|
|
154
|
+
|
|
155
|
+
/** Cancel a task */
|
|
156
|
+
cancel(taskId: string, reason?: string): Promise<boolean>
|
|
157
|
+
|
|
158
|
+
/** List tasks with optional filtering */
|
|
159
|
+
list(options?: ListOptions): Promise<TaskData[]>
|
|
160
|
+
|
|
161
|
+
/** Get task statistics */
|
|
162
|
+
getStats(): Promise<TaskStats>
|
|
163
|
+
|
|
164
|
+
/** Retry a failed task */
|
|
165
|
+
retry(taskId: string): Promise<TaskData>
|
|
166
|
+
|
|
167
|
+
/** Get tasks that are ready for execution */
|
|
168
|
+
getReadyTasks(): Promise<TaskData[]>
|
|
169
|
+
|
|
170
|
+
/** Get tasks that depend on a given task */
|
|
171
|
+
getDependants(taskId: string): Promise<TaskData[]>
|
|
172
|
+
|
|
173
|
+
/** Enqueue a task for background processing */
|
|
174
|
+
enqueue(taskId: string, options?: EnqueueOptions): Promise<TaskData>
|
|
175
|
+
|
|
176
|
+
/** Dequeue and start the next task */
|
|
177
|
+
dequeue(): Promise<TaskData | null>
|
|
178
|
+
|
|
179
|
+
/** Execute a task with AI */
|
|
180
|
+
executeWithAI(taskId: string): Promise<TaskData>
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Client Options
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Options for creating a task client
|
|
189
|
+
*/
|
|
190
|
+
export interface TaskClientOptions {
|
|
191
|
+
/** Authentication token or API key */
|
|
192
|
+
token?: string
|
|
193
|
+
/** Custom headers to include with requests */
|
|
194
|
+
headers?: Record<string, string>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Client Factory
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
/** Default worker URL for digital-tasks */
|
|
202
|
+
const DEFAULT_URL = 'https://digital-tasks.workers.dev'
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a typed RPC client for the digital-tasks worker.
|
|
206
|
+
*
|
|
207
|
+
* @param url - The URL of the deployed digital-tasks worker
|
|
208
|
+
* @param options - Optional configuration (auth token, custom headers)
|
|
209
|
+
* @returns A fully typed RPC client proxy
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* import { createTaskClient } from 'digital-tasks/client'
|
|
214
|
+
*
|
|
215
|
+
* // Connect to a deployed worker
|
|
216
|
+
* const tasks = createTaskClient('https://digital-tasks.workers.dev')
|
|
217
|
+
*
|
|
218
|
+
* // Create and manage tasks
|
|
219
|
+
* const task = await tasks.create({
|
|
220
|
+
* name: 'Analyze data',
|
|
221
|
+
* description: 'Run data analysis pipeline',
|
|
222
|
+
* priority: 'high',
|
|
223
|
+
* })
|
|
224
|
+
*
|
|
225
|
+
* // Execute the task
|
|
226
|
+
* await tasks.execute(task.id)
|
|
227
|
+
*
|
|
228
|
+
* // Track progress
|
|
229
|
+
* await tasks.updateProgress(task.id, 50, 'Processing...')
|
|
230
|
+
*
|
|
231
|
+
* // Complete with output
|
|
232
|
+
* await tasks.complete(task.id, { results: [1, 2, 3] })
|
|
233
|
+
*
|
|
234
|
+
* // List all completed tasks
|
|
235
|
+
* const completed = await tasks.list({ status: 'completed' })
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* // With authentication
|
|
241
|
+
* const tasks = createTaskClient('https://digital-tasks.workers.dev', {
|
|
242
|
+
* token: 'my-api-key',
|
|
243
|
+
* })
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export function createTaskClient(
|
|
247
|
+
url: string = DEFAULT_URL,
|
|
248
|
+
options?: TaskClientOptions
|
|
249
|
+
): TaskServiceAPI {
|
|
250
|
+
return RPC<TaskServiceAPI>(http(url, options?.token))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Default task client connected to digital-tasks.workers.dev
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* import client from 'digital-tasks/client'
|
|
259
|
+
*
|
|
260
|
+
* const task = await client.create({
|
|
261
|
+
* name: 'Quick task',
|
|
262
|
+
* description: 'A task using the default client',
|
|
263
|
+
* })
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
const client: TaskServiceAPI = createTaskClient()
|
|
267
|
+
|
|
268
|
+
export default client
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* digital-tasks - Task management primitives for digital workers
|
|
3
3
|
*
|
|
4
|
-
* Task =
|
|
4
|
+
* Task = Tool + metadata (status, progress, assignment, dependencies)
|
|
5
5
|
*
|
|
6
|
-
* Every task wraps a
|
|
7
|
-
* with lifecycle management, worker assignment, and
|
|
6
|
+
* Every task wraps a Tool (a callable Verb of type code, generative,
|
|
7
|
+
* agentic, or human) with lifecycle management, worker assignment, and
|
|
8
|
+
* dependency tracking. The `function` field is a deprecated alias for
|
|
9
|
+
* `tool` retained for backward compatibility.
|
|
8
10
|
*
|
|
9
11
|
* ## Quick Start
|
|
10
12
|
*
|
|
11
13
|
* ```ts
|
|
12
14
|
* import { Task, createTask, taskQueue } from 'digital-tasks'
|
|
13
15
|
*
|
|
14
|
-
* // Create a task from a
|
|
16
|
+
* // Create a task from a Tool
|
|
15
17
|
* const task = await createTask({
|
|
16
|
-
*
|
|
18
|
+
* tool: {
|
|
17
19
|
* type: 'generative',
|
|
18
20
|
* name: 'summarize',
|
|
19
21
|
* args: { text: 'The text to summarize' },
|
|
@@ -79,13 +81,17 @@ export type {
|
|
|
79
81
|
WorkerRef,
|
|
80
82
|
WorkerType,
|
|
81
83
|
TaskDependency,
|
|
84
|
+
TaskDep,
|
|
82
85
|
DependencyType,
|
|
83
86
|
TaskProgress,
|
|
84
87
|
TaskEvent,
|
|
88
|
+
Comment,
|
|
85
89
|
CreateTaskOptions,
|
|
86
90
|
UpdateTaskOptions,
|
|
87
91
|
TaskQuery,
|
|
88
92
|
TaskQueueStats,
|
|
93
|
+
// Action supertype (re-exported from digital-objects)
|
|
94
|
+
Action,
|
|
89
95
|
// Function types (re-exported from ai-functions)
|
|
90
96
|
FunctionDefinition,
|
|
91
97
|
CodeFunctionDefinition,
|
|
@@ -154,8 +160,4 @@ export {
|
|
|
154
160
|
|
|
155
161
|
export type { SerializeOptions } from './markdown.js'
|
|
156
162
|
|
|
157
|
-
export {
|
|
158
|
-
parseMarkdown,
|
|
159
|
-
toMarkdown,
|
|
160
|
-
syncStatusFromMarkdown,
|
|
161
|
-
} from './markdown.js'
|
|
163
|
+
export { parseMarkdown, toMarkdown, syncStatusFromMarkdown } from './markdown.js'
|
package/src/markdown.ts
CHANGED
|
@@ -49,6 +49,7 @@ import type {
|
|
|
49
49
|
ParallelGroup,
|
|
50
50
|
SequentialGroup,
|
|
51
51
|
ExecutionMode,
|
|
52
|
+
CreateProjectOptions,
|
|
52
53
|
} from './project.js'
|
|
53
54
|
import { task, parallel, sequential, createProject } from './project.js'
|
|
54
55
|
|
|
@@ -61,8 +62,8 @@ import { task, parallel, sequential, createProject } from './project.js'
|
|
|
61
62
|
*/
|
|
62
63
|
const STATUS_MARKERS: Record<string, TaskStatus> = {
|
|
63
64
|
' ': 'pending',
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
x: 'completed',
|
|
66
|
+
X: 'completed',
|
|
66
67
|
'-': 'in_progress',
|
|
67
68
|
'~': 'blocked',
|
|
68
69
|
'!': 'failed',
|
|
@@ -93,7 +94,7 @@ const PRIORITY_MARKERS: Record<string, TaskPriority> = {
|
|
|
93
94
|
'!': 'urgent',
|
|
94
95
|
'^': 'high',
|
|
95
96
|
'': 'normal',
|
|
96
|
-
|
|
97
|
+
v: 'low',
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// ============================================================================
|
|
@@ -135,20 +136,23 @@ function parseLine(line: string): ParsedLine {
|
|
|
135
136
|
const raw = line
|
|
136
137
|
|
|
137
138
|
// Count leading spaces for indent (2 spaces = 1 level)
|
|
138
|
-
const
|
|
139
|
+
const leadingSpacesMatch = line.match(/^(\s*)/)
|
|
140
|
+
const leadingSpaces = leadingSpacesMatch?.[1]?.length ?? 0
|
|
139
141
|
const indent = Math.floor(leadingSpaces / 2)
|
|
140
142
|
const trimmed = line.slice(leadingSpaces)
|
|
141
143
|
|
|
142
144
|
// Check for heading
|
|
143
145
|
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/)
|
|
144
146
|
if (headingMatch) {
|
|
147
|
+
const matchedHashes = headingMatch[1]!
|
|
148
|
+
const matchedTitle = headingMatch[2]!
|
|
145
149
|
return {
|
|
146
150
|
indent,
|
|
147
151
|
isTask: false,
|
|
148
152
|
isOrdered: false,
|
|
149
|
-
title:
|
|
153
|
+
title: matchedTitle.trim(),
|
|
150
154
|
isHeading: true,
|
|
151
|
-
headingLevel:
|
|
155
|
+
headingLevel: matchedHashes.length,
|
|
152
156
|
raw,
|
|
153
157
|
}
|
|
154
158
|
}
|
|
@@ -156,8 +160,8 @@ function parseLine(line: string): ParsedLine {
|
|
|
156
160
|
// Check for unordered task: - [ ] or - [x] etc.
|
|
157
161
|
const unorderedMatch = trimmed.match(/^[-*]\s+\[([^\]]*)\]\s*(.*)$/)
|
|
158
162
|
if (unorderedMatch) {
|
|
159
|
-
const marker = unorderedMatch[1]
|
|
160
|
-
let title = unorderedMatch[2]
|
|
163
|
+
const marker = unorderedMatch[1]!
|
|
164
|
+
let title = unorderedMatch[2]!.trim()
|
|
161
165
|
let priority: TaskPriority = 'normal'
|
|
162
166
|
|
|
163
167
|
// Check for priority marker at start of title
|
|
@@ -179,7 +183,7 @@ function parseLine(line: string): ParsedLine {
|
|
|
179
183
|
indent,
|
|
180
184
|
isTask: true,
|
|
181
185
|
isOrdered: false,
|
|
182
|
-
status: STATUS_MARKERS[marker]
|
|
186
|
+
status: STATUS_MARKERS[marker] ?? 'pending',
|
|
183
187
|
priority,
|
|
184
188
|
title,
|
|
185
189
|
isHeading: false,
|
|
@@ -190,8 +194,9 @@ function parseLine(line: string): ParsedLine {
|
|
|
190
194
|
// Check for ordered task: 1. [ ] or 1. [x] etc.
|
|
191
195
|
const orderedMatch = trimmed.match(/^(\d+)\.\s+\[([^\]]*)\]\s*(.*)$/)
|
|
192
196
|
if (orderedMatch) {
|
|
193
|
-
const
|
|
194
|
-
|
|
197
|
+
const orderNum = orderedMatch[1]!
|
|
198
|
+
const marker = orderedMatch[2]!
|
|
199
|
+
let title = orderedMatch[3]!.trim()
|
|
195
200
|
let priority: TaskPriority = 'normal'
|
|
196
201
|
|
|
197
202
|
// Check for priority marker
|
|
@@ -213,8 +218,8 @@ function parseLine(line: string): ParsedLine {
|
|
|
213
218
|
indent,
|
|
214
219
|
isTask: true,
|
|
215
220
|
isOrdered: true,
|
|
216
|
-
orderNumber: parseInt(
|
|
217
|
-
status: STATUS_MARKERS[marker]
|
|
221
|
+
orderNumber: parseInt(orderNum, 10),
|
|
222
|
+
status: STATUS_MARKERS[marker] ?? 'pending',
|
|
218
223
|
priority,
|
|
219
224
|
title,
|
|
220
225
|
isHeading: false,
|
|
@@ -247,7 +252,7 @@ function parseTasksAtIndent(
|
|
|
247
252
|
let modeSet = false
|
|
248
253
|
|
|
249
254
|
while (index < lines.length) {
|
|
250
|
-
const line = lines[index]
|
|
255
|
+
const line = lines[index]!
|
|
251
256
|
|
|
252
257
|
// Stop if we've gone back to a lower indent level
|
|
253
258
|
if (line.indent < baseIndent && (line.isTask || line.isHeading)) {
|
|
@@ -269,20 +274,21 @@ function parseTasksAtIndent(
|
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
// Parse subtasks
|
|
272
|
-
const { tasks: subtasks, nextIndex } = parseTasksAtIndent(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
baseIndent + 1
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
const taskDef = task(line.title, {
|
|
279
|
-
priority: line.priority,
|
|
280
|
-
subtasks: subtasks.length > 0 ? subtasks : undefined,
|
|
277
|
+
const { tasks: subtasks, nextIndex } = parseTasksAtIndent(lines, index + 1, baseIndent + 1)
|
|
278
|
+
|
|
279
|
+
const taskOptions: Partial<Omit<TaskDefinition, '__type' | 'title'>> = {
|
|
281
280
|
metadata: {
|
|
282
281
|
_originalStatus: line.status,
|
|
283
282
|
_lineNumber: index,
|
|
284
283
|
},
|
|
285
|
-
}
|
|
284
|
+
}
|
|
285
|
+
if (line.priority !== undefined) {
|
|
286
|
+
taskOptions.priority = line.priority
|
|
287
|
+
}
|
|
288
|
+
if (subtasks.length > 0) {
|
|
289
|
+
taskOptions.subtasks = subtasks
|
|
290
|
+
}
|
|
291
|
+
const taskDef = task(line.title, taskOptions)
|
|
286
292
|
|
|
287
293
|
tasks.push(taskDef)
|
|
288
294
|
index = nextIndex
|
|
@@ -324,9 +330,9 @@ export function parseMarkdown(markdown: string): Project {
|
|
|
324
330
|
const allTasks: TaskNode[] = []
|
|
325
331
|
|
|
326
332
|
// Find project name from first h1
|
|
327
|
-
const h1Index = lines.findIndex(l => l.isHeading && l.headingLevel === 1)
|
|
333
|
+
const h1Index = lines.findIndex((l) => l.isHeading && l.headingLevel === 1)
|
|
328
334
|
if (h1Index !== -1) {
|
|
329
|
-
projectName = lines[h1Index]
|
|
335
|
+
projectName = lines[h1Index]!.title
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
// Process sections and tasks
|
|
@@ -334,7 +340,7 @@ export function parseMarkdown(markdown: string): Project {
|
|
|
334
340
|
let index = 0
|
|
335
341
|
|
|
336
342
|
while (index < lines.length) {
|
|
337
|
-
const line = lines[index]
|
|
343
|
+
const line = lines[index]!
|
|
338
344
|
|
|
339
345
|
// New section (h2)
|
|
340
346
|
if (line.isHeading && line.headingLevel === 2) {
|
|
@@ -353,7 +359,7 @@ export function parseMarkdown(markdown: string): Project {
|
|
|
353
359
|
|
|
354
360
|
const modeMatch = sectionName.match(/\((parallel|sequential)\)\s*$/i)
|
|
355
361
|
if (modeMatch) {
|
|
356
|
-
sectionMode = modeMatch[1]
|
|
362
|
+
sectionMode = modeMatch[1]!.toLowerCase() as ExecutionMode
|
|
357
363
|
sectionName = sectionName.replace(/\s*\((parallel|sequential)\)\s*$/i, '')
|
|
358
364
|
}
|
|
359
365
|
|
|
@@ -396,11 +402,15 @@ export function parseMarkdown(markdown: string): Project {
|
|
|
396
402
|
}
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
|
|
405
|
+
const projectOptions: CreateProjectOptions = {
|
|
400
406
|
name: projectName,
|
|
401
|
-
description: projectDescription,
|
|
402
407
|
tasks: allTasks,
|
|
403
|
-
}
|
|
408
|
+
}
|
|
409
|
+
if (projectDescription !== undefined) {
|
|
410
|
+
projectOptions.description = projectDescription
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return createProject(projectOptions)
|
|
404
414
|
}
|
|
405
415
|
|
|
406
416
|
// ============================================================================
|
|
@@ -435,13 +445,13 @@ function serializeTaskNode(
|
|
|
435
445
|
|
|
436
446
|
if (node.__type === 'task') {
|
|
437
447
|
const taskDef = node as TaskDefinition
|
|
438
|
-
const status = (taskDef.metadata?._originalStatus as TaskStatus) || 'pending'
|
|
448
|
+
const status = (taskDef.metadata?.['_originalStatus'] as TaskStatus) || 'pending'
|
|
439
449
|
const marker = options.includeStatus !== false ? STATUS_TO_MARKER[status] : ' '
|
|
440
450
|
|
|
441
451
|
let prefix: string
|
|
442
452
|
if (isSequential) {
|
|
443
453
|
// Use numbered list for sequential
|
|
444
|
-
const num = (taskDef.metadata?._sequenceNumber as number) || 1
|
|
454
|
+
const num = (taskDef.metadata?.['_sequenceNumber'] as number) || 1
|
|
445
455
|
prefix = `${num}. [${marker}]`
|
|
446
456
|
} else {
|
|
447
457
|
// Use bullet for parallel
|
|
@@ -450,11 +460,16 @@ function serializeTaskNode(
|
|
|
450
460
|
|
|
451
461
|
let title = taskDef.title
|
|
452
462
|
if (options.includePriority && taskDef.priority && taskDef.priority !== 'normal') {
|
|
453
|
-
const priorityMarker =
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
463
|
+
const priorityMarker =
|
|
464
|
+
taskDef.priority === 'critical'
|
|
465
|
+
? '!!'
|
|
466
|
+
: taskDef.priority === 'urgent'
|
|
467
|
+
? '!'
|
|
468
|
+
: taskDef.priority === 'high'
|
|
469
|
+
? '^'
|
|
470
|
+
: taskDef.priority === 'low'
|
|
471
|
+
? 'v'
|
|
472
|
+
: ''
|
|
458
473
|
title = `${priorityMarker}${title}`
|
|
459
474
|
}
|
|
460
475
|
|
|
@@ -471,7 +486,7 @@ function serializeTaskNode(
|
|
|
471
486
|
let seqNum = 1
|
|
472
487
|
for (const child of group.tasks) {
|
|
473
488
|
if (child.__type === 'task') {
|
|
474
|
-
(child as TaskDefinition).metadata = {
|
|
489
|
+
;(child as TaskDefinition).metadata = {
|
|
475
490
|
...(child as TaskDefinition).metadata,
|
|
476
491
|
_sequenceNumber: seqNum++,
|
|
477
492
|
}
|
|
@@ -483,7 +498,7 @@ function serializeTaskNode(
|
|
|
483
498
|
let seqNum = 1
|
|
484
499
|
for (const child of group.tasks) {
|
|
485
500
|
if (child.__type === 'task') {
|
|
486
|
-
(child as TaskDefinition).metadata = {
|
|
501
|
+
;(child as TaskDefinition).metadata = {
|
|
487
502
|
...(child as TaskDefinition).metadata,
|
|
488
503
|
_sequenceNumber: seqNum++,
|
|
489
504
|
}
|
|
@@ -558,10 +573,7 @@ export function toMarkdown(project: Project, options: SerializeOptions = {}): st
|
|
|
558
573
|
* Update task statuses in a project from markdown
|
|
559
574
|
* (Useful for syncing when markdown is edited externally)
|
|
560
575
|
*/
|
|
561
|
-
export function syncStatusFromMarkdown(
|
|
562
|
-
project: Project,
|
|
563
|
-
markdown: string
|
|
564
|
-
): Project {
|
|
576
|
+
export function syncStatusFromMarkdown(project: Project, markdown: string): Project {
|
|
565
577
|
const parsed = parseMarkdown(markdown)
|
|
566
578
|
|
|
567
579
|
// Build a map of task titles to statuses from parsed markdown
|
|
@@ -570,7 +582,7 @@ export function syncStatusFromMarkdown(
|
|
|
570
582
|
function collectStatuses(node: TaskNode) {
|
|
571
583
|
if (node.__type === 'task') {
|
|
572
584
|
const taskDef = node as TaskDefinition
|
|
573
|
-
const status = taskDef.metadata?._originalStatus as TaskStatus
|
|
585
|
+
const status = taskDef.metadata?.['_originalStatus'] as TaskStatus
|
|
574
586
|
if (status) {
|
|
575
587
|
statusMap.set(taskDef.title, status)
|
|
576
588
|
}
|
|
@@ -590,14 +602,17 @@ export function syncStatusFromMarkdown(
|
|
|
590
602
|
if (node.__type === 'task') {
|
|
591
603
|
const taskDef = node as TaskDefinition
|
|
592
604
|
const newStatus = statusMap.get(taskDef.title)
|
|
593
|
-
|
|
605
|
+
const result: TaskDefinition = {
|
|
594
606
|
...taskDef,
|
|
595
607
|
metadata: {
|
|
596
608
|
...taskDef.metadata,
|
|
597
|
-
_originalStatus: newStatus || taskDef.metadata?._originalStatus,
|
|
609
|
+
_originalStatus: newStatus || taskDef.metadata?.['_originalStatus'],
|
|
598
610
|
},
|
|
599
|
-
subtasks: taskDef.subtasks?.map(updateStatuses),
|
|
600
611
|
}
|
|
612
|
+
if (taskDef.subtasks) {
|
|
613
|
+
result.subtasks = taskDef.subtasks.map(updateStatuses)
|
|
614
|
+
}
|
|
615
|
+
return result
|
|
601
616
|
} else if (node.__type === 'parallel') {
|
|
602
617
|
const group = node as ParallelGroup
|
|
603
618
|
return {
|