open-wadah 1.2.2 → 1.2.3

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 (3) hide show
  1. package/README.md +10 -1
  2. package/cli.js +197 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,7 +27,7 @@ wadah complete <task-id>
27
27
  | Command | Description |
28
28
  |--------|-------------|
29
29
  | **Auth** | `login`, `signup`, `logout`, `whoami` |
30
- | **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
30
+ | **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `subtask list/add/toggle/delete`, `delete` |
31
31
  | **Relationships** | `add --blocks <id>`, `add --blocked-by <id>`, `update --blocks <id>` |
32
32
  | **Board** | `boards --json`, `buckets --json`, `assignees --json`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
33
33
  | **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
@@ -39,6 +39,15 @@ wadah complete <task-id>
39
39
 
40
40
  Run `wadah --help` for full list and options.
41
41
 
42
+ Subtask examples:
43
+
44
+ ```bash
45
+ wadah subtask add <task-id> "Write tests"
46
+ wadah subtask list <task-id>
47
+ wadah subtask toggle <task-id> <subtask-id>
48
+ wadah subtask delete <task-id> <subtask-id>
49
+ ```
50
+
42
51
  ## Shell completion
43
52
 
44
53
  **Bash:**
package/cli.js CHANGED
@@ -279,6 +279,23 @@ function findBucket(buckets, query) {
279
279
  return buckets.find((b) => b.id === query || b.title.toLowerCase().includes(q)) ?? null
280
280
  }
281
281
 
282
+ function findSubtask(subtasks, query) {
283
+ const raw = String(query ?? '').trim()
284
+ if (!raw) return null
285
+ const q = raw.toLowerCase()
286
+ const byId = subtasks.filter((s) => s.id === raw || s.id.startsWith(raw))
287
+ if (byId.length === 1) return byId[0]
288
+ if (byId.length > 1) {
289
+ throw new CliError('validation_error', `Ambiguous subtask id prefix: ${raw}`)
290
+ }
291
+ const byTitle = subtasks.filter((s) => String(s.title ?? '').toLowerCase() === q)
292
+ if (byTitle.length === 1) return byTitle[0]
293
+ if (byTitle.length > 1) {
294
+ throw new CliError('validation_error', `Multiple subtasks have title: ${raw}`)
295
+ }
296
+ return null
297
+ }
298
+
282
299
  function parseCompletedFlag(value) {
283
300
  if (typeof value !== 'string') return null
284
301
  const v = value.trim().toLowerCase()
@@ -410,6 +427,8 @@ Available commands and their args (use these exact command names):
410
427
  - repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
411
428
  - repo list (or repos): args = []. List linked GitHub repositories.
412
429
  - repo remove: args = [repo id]. Unlink a repository.
430
+ - pr link: args = [task id, pr url]. Attach a PR URL to a task.
431
+ - pr open: args = [task id]. Open the linked PR URL for a task.
413
432
 
414
433
  If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
415
434
 
@@ -1280,6 +1299,87 @@ program
1280
1299
  } catch (err) { handleError(err) }
1281
1300
  })
1282
1301
 
1302
+ const subtaskCmd = program.command('subtask').description('Manage task subtasks')
1303
+
1304
+ subtaskCmd
1305
+ .command('list <taskId>')
1306
+ .description('List subtasks on a task')
1307
+ .action(async (taskId) => {
1308
+ try {
1309
+ const task = await api(`/api/tasks/${taskId}`)
1310
+ const subtasks = task.subtasks ?? []
1311
+ if (isMachineOutput()) {
1312
+ console.log(JSON.stringify({ taskId, subtasks }, null, runtimeJsonOutput ? 2 : 0))
1313
+ return
1314
+ }
1315
+ console.log()
1316
+ if (subtasks.length === 0) {
1317
+ console.log(chalk.gray(' No subtasks.\n'))
1318
+ return
1319
+ }
1320
+ console.log(chalk.bold(task.title))
1321
+ subtasks.forEach((s) => {
1322
+ const status = s.completed ? chalk.green('✓') : '○'
1323
+ console.log(` ${status} ${s.title} ${chalk.gray(s.id.slice(0, 8))}`)
1324
+ })
1325
+ console.log()
1326
+ } catch (err) { handleError(err) }
1327
+ })
1328
+
1329
+ subtaskCmd
1330
+ .command('add <taskId> <title>')
1331
+ .description('Add a subtask to a task')
1332
+ .action(async (taskId, title) => {
1333
+ try {
1334
+ const trimmedTitle = String(title ?? '').trim()
1335
+ if (!trimmedTitle) return fail('Subtask title is required')
1336
+ const task = await api(`/api/tasks/${taskId}`)
1337
+ const nextSubtask = { id: randomUUID(), title: trimmedTitle, completed: false }
1338
+ await api(`/api/tasks/${taskId}`, {
1339
+ method: 'PATCH',
1340
+ body: JSON.stringify({ subtasks: [...(task.subtasks ?? []), nextSubtask] }),
1341
+ })
1342
+ console.log(chalk.green('\n✓ Subtask added') + ` ${chalk.bold(nextSubtask.title)} ${chalk.gray(`(${nextSubtask.id.slice(0, 8)})`)}\n`)
1343
+ } catch (err) { handleError(err) }
1344
+ })
1345
+
1346
+ subtaskCmd
1347
+ .command('toggle <taskId> <subtaskId>')
1348
+ .description('Toggle subtask completion')
1349
+ .action(async (taskId, subtaskId) => {
1350
+ try {
1351
+ const task = await api(`/api/tasks/${taskId}`)
1352
+ const subtasks = task.subtasks ?? []
1353
+ const target = findSubtask(subtasks, subtaskId)
1354
+ if (!target) return fail(`Subtask not found: ${subtaskId}`)
1355
+ const nextSubtasks = subtasks.map((s) => s.id === target.id ? { ...s, completed: !s.completed } : s)
1356
+ await api(`/api/tasks/${taskId}`, {
1357
+ method: 'PATCH',
1358
+ body: JSON.stringify({ subtasks: nextSubtasks }),
1359
+ })
1360
+ const marker = !target.completed ? chalk.green('✓') : '○'
1361
+ console.log(chalk.green('\n✓ Subtask updated') + ` ${marker} ${chalk.bold(target.title)}\n`)
1362
+ } catch (err) { handleError(err) }
1363
+ })
1364
+
1365
+ subtaskCmd
1366
+ .command('delete <taskId> <subtaskId>')
1367
+ .description('Delete a subtask from a task')
1368
+ .action(async (taskId, subtaskId) => {
1369
+ try {
1370
+ const task = await api(`/api/tasks/${taskId}`)
1371
+ const subtasks = task.subtasks ?? []
1372
+ const target = findSubtask(subtasks, subtaskId)
1373
+ if (!target) return fail(`Subtask not found: ${subtaskId}`)
1374
+ const nextSubtasks = subtasks.filter((s) => s.id !== target.id)
1375
+ await api(`/api/tasks/${taskId}`, {
1376
+ method: 'PATCH',
1377
+ body: JSON.stringify({ subtasks: nextSubtasks }),
1378
+ })
1379
+ console.log(chalk.green('\n✓ Subtask deleted') + ` ${chalk.bold(target.title)}\n`)
1380
+ } catch (err) { handleError(err) }
1381
+ })
1382
+
1283
1383
  // ── tm boards ─────────────────────────────────────────────────────────────────
1284
1384
 
1285
1385
  program
@@ -1382,6 +1482,29 @@ boardCmd
1382
1482
  } catch (err) { handleError(err) }
1383
1483
  })
1384
1484
 
1485
+ boardCmd
1486
+ .command('rename <id> <name>')
1487
+ .description('Rename a board')
1488
+ .action(async (id, name) => {
1489
+ try {
1490
+ const payload = JSON.stringify({ name: String(name).trim() })
1491
+ let board
1492
+ try {
1493
+ board = await api(`/api/boards/${id}`, { method: 'PATCH', body: payload })
1494
+ } catch (err) {
1495
+ // Backward compatibility: some deployments only support PUT for board rename.
1496
+ const notFound = err instanceof CliError && err.code === 'api_error' && String(err.message).toLowerCase().includes('not found')
1497
+ if (!notFound) throw err
1498
+ board = await api(`/api/boards/${id}`, { method: 'PUT', body: payload })
1499
+ }
1500
+ if (isMachineOutput()) {
1501
+ console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
1502
+ } else {
1503
+ console.log(chalk.green('\n✓ Board renamed') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
1504
+ }
1505
+ } catch (err) { handleError(err) }
1506
+ })
1507
+
1385
1508
  boardCmd
1386
1509
  .command('delete <id>')
1387
1510
  .description('Delete a board (cannot delete the last one)')
@@ -1630,6 +1753,79 @@ repoCmd
1630
1753
  } catch (err) { handleError(err) }
1631
1754
  })
1632
1755
 
1756
+ const prCmd = program.command('pr').description('Attach and open pull requests on tasks')
1757
+
1758
+ prCmd
1759
+ .command('link <taskId> <prUrl>')
1760
+ .description('Attach a PR URL to a task')
1761
+ .option('--comment', 'Add a task comment with the PR link')
1762
+ .action(async function (taskId, prUrl) {
1763
+ const opts = this.opts()
1764
+ try {
1765
+ let parsed
1766
+ try {
1767
+ parsed = new URL(String(prUrl).trim())
1768
+ } catch {
1769
+ return fail('Provide a valid PR URL (e.g. https://github.com/owner/repo/pull/123)')
1770
+ }
1771
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
1772
+ return fail('PR URL must start with http:// or https://')
1773
+ }
1774
+ const normalizedUrl = parsed.toString()
1775
+
1776
+ const task = await api(`/api/tasks/${taskId}`)
1777
+ await api(`/api/tasks/${taskId}`, {
1778
+ method: 'PATCH',
1779
+ body: JSON.stringify({ contextUrl: normalizedUrl }),
1780
+ })
1781
+ if (opts.comment) {
1782
+ const comment = {
1783
+ id: randomUUID(),
1784
+ text: `Linked PR: ${normalizedUrl}`,
1785
+ createdAt: Date.now(),
1786
+ }
1787
+ await api(`/api/tasks/${taskId}`, {
1788
+ method: 'PATCH',
1789
+ body: JSON.stringify({ comments: [...(task.comments ?? []), comment] }),
1790
+ })
1791
+ }
1792
+
1793
+ if (isMachineOutput()) {
1794
+ console.log(JSON.stringify({ taskId, prUrl: normalizedUrl, linked: true, commented: Boolean(opts.comment) }, null, runtimeJsonOutput ? 2 : 0))
1795
+ } else {
1796
+ console.log(chalk.green('\n✓ PR linked') + ` ${chalk.bold(task.title)}`)
1797
+ console.log(chalk.gray(` ${normalizedUrl}${opts.comment ? ' · comment added' : ''}\n`))
1798
+ }
1799
+ } catch (err) { handleError(err) }
1800
+ })
1801
+
1802
+ prCmd
1803
+ .command('open <taskId>')
1804
+ .description('Open the linked PR URL for a task')
1805
+ .option('--no-browser', "Don't auto-open browser; print URL only")
1806
+ .action(async function (taskId) {
1807
+ const opts = this.opts()
1808
+ try {
1809
+ const task = await api(`/api/tasks/${taskId}`)
1810
+ const url = task?.contextUrl ? String(task.contextUrl).trim() : ''
1811
+ if (!url) return fail('This task has no linked PR URL. Use: wadah pr link <task-id> <pr-url>')
1812
+
1813
+ if (isMachineOutput()) {
1814
+ console.log(JSON.stringify({ taskId, prUrl: url }, null, runtimeJsonOutput ? 2 : 0))
1815
+ return
1816
+ }
1817
+
1818
+ console.log(chalk.gray(`\nPR: ${url}`))
1819
+ if (opts.browser) {
1820
+ const opened = await openBrowser(url)
1821
+ if (opened) console.log(chalk.green('✓ Opened in browser\n'))
1822
+ else console.log(chalk.yellow('Could not open browser automatically. Open the URL manually.\n'))
1823
+ } else {
1824
+ console.log()
1825
+ }
1826
+ } catch (err) { handleError(err) }
1827
+ })
1828
+
1633
1829
  // ── agent tokens ──────────────────────────────────────────────────────────────
1634
1830
 
1635
1831
  program
@@ -2255,7 +2451,7 @@ const CLI_COMMANDS = [
2255
2451
  'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
2256
2452
  'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
2257
2453
  'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
2258
- 'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
2454
+ 'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'pr', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
2259
2455
  ]
2260
2456
 
2261
2457
  // ── wadah setup-agent ─────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-wadah",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Open Wadah CLI — shared task board for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {