open-wadah 1.1.0 → 1.2.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 (3) hide show
  1. package/README.md +6 -4
  2. package/cli.js +178 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,8 +27,9 @@ wadah complete <task-id>
27
27
  | Command | Description |
28
28
  |--------|-------------|
29
29
  | **Auth** | `login`, `signup`, `logout`, `whoami` |
30
- | **Tasks** | `open`, `list`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
31
- | **Board** | `boards`, `buckets`, `assignees`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
30
+ | **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
31
+ | **Relationships** | `add --blocks <id>`, `add --blocked-by <id>`, `update --blocks <id>` |
32
+ | **Board** | `boards --json`, `buckets --json`, `assignees --json`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
32
33
  | **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
33
34
  | **Calendar** | `calendar` (list; `--assignee`, `--task`, `--from`, `--to`), `calendar add`, `calendar update <id>`, `calendar delete <id>` |
34
35
  | **Docs** | `docs` (list), `doc create [title]`, `doc show <id>`, `doc update <id>`, `doc delete <id>` |
@@ -56,10 +57,11 @@ source ~/.zshrc
56
57
 
57
58
  ## Agent / AI use
58
59
 
59
- - Set `TASK_MANAGER_TOKEN` (create a token in the app under **Agent access**).
60
- - Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`.
60
+ - Set `TASK_MANAGER_TOKEN` (create a token: `wadah agent-token create "My Agent"`).
61
+ - Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`, `wadah buckets --json`.
61
62
  - Natural language: `wadah do "add a task to fix the bug"` (requires `OPENAI_API_KEY`).
62
63
  - In Cursor/Claude/Kimi: see **WADAH_CLI.md** in the repo root for a short reference.
64
+ - For autonomous 24/7 agents with GitHub Actions: see **AGENTS.md** in the repo root.
63
65
 
64
66
  ## Global flags
65
67
 
package/cli.js CHANGED
@@ -181,8 +181,8 @@ function printTaskList(tasks, state) {
181
181
  }
182
182
 
183
183
  function handleError(err) {
184
- const msg = err.message ?? 'Unexpected error'
185
- const code = err.code ?? 'unknown_error'
184
+ const msg = err.message ?? 'Unexpected error'
185
+ const code = err.code ?? 'unknown_error'
186
186
  const authLike =
187
187
  code === 'auth_error' ||
188
188
  msg.toLowerCase().includes('missing token') ||
@@ -195,6 +195,17 @@ function handleError(err) {
195
195
  console.error(chalk.red('\n✗ Not authenticated.'))
196
196
  console.error(chalk.gray(' Humans: wadah login'))
197
197
  console.error(chalk.gray(' Agents: TASK_MANAGER_TOKEN=<token> wadah open\n'))
198
+ } else if (code === 'network_error') {
199
+ console.error(chalk.red('\n✗ Network error — could not reach the API.'))
200
+ console.error(chalk.gray(` Check your connection or API URL: ${getApiBase()}`))
201
+ console.error(chalk.gray(' Run: wadah doctor\n'))
202
+ } else if (code === 'server_error') {
203
+ console.error(chalk.red('\n✗ Server error — something went wrong on the API side.'))
204
+ console.error(chalk.gray(' Try again in a moment. If it persists, check api.openwadah.com status.\n'))
205
+ } else if (code === 'api_error' && msg.toLowerCase().includes('not found')) {
206
+ console.error(chalk.red('\n✗ Not found.'))
207
+ console.error(chalk.gray(' The resource may not exist, or this endpoint may not be available for agent tokens.'))
208
+ console.error(chalk.gray(' Run: wadah doctor\n'))
198
209
  } else {
199
210
  console.error(chalk.red(`\n✗ ${msg}\n`))
200
211
  }
@@ -262,6 +273,13 @@ function isMachineOutput() {
262
273
  return runtimeJsonOutput || runtimeQuietOutput || !process.stdout.isTTY
263
274
  }
264
275
 
276
+ // ── session state cache ───────────────────────────────────────────────────────
277
+ let _cachedState = null
278
+ async function getState() {
279
+ if (!_cachedState) _cachedState = await api('/api/state')
280
+ return _cachedState
281
+ }
282
+
265
283
  function sleep(ms) {
266
284
  return new Promise((resolve) => setTimeout(resolve, ms))
267
285
  }
@@ -684,7 +702,7 @@ program
684
702
  .option('--completed', 'Show completed tasks instead')
685
703
  .action(async (opts) => {
686
704
  try {
687
- const state = await api('/api/state')
705
+ const state = await getState()
688
706
  let tasks = state.tasks.filter((t) => !!t.completed === !!opts.completed)
689
707
 
690
708
  if (!opts.all && !opts.assignee) {
@@ -723,7 +741,7 @@ program
723
741
  if (opts.open && opts.completed) {
724
742
  return fail('Use either --open or --completed, not both.')
725
743
  }
726
- const state = await api('/api/state')
744
+ const state = await getState()
727
745
  let tasks = state.tasks
728
746
  if (opts.open) tasks = tasks.filter((t) => !t.completed)
729
747
  if (opts.completed) tasks = tasks.filter((t) => !!t.completed)
@@ -748,6 +766,48 @@ program
748
766
  } catch (err) { handleError(err) }
749
767
  })
750
768
 
769
+ // ── tm search ─────────────────────────────────────────────────────────────────
770
+
771
+ program
772
+ .command('search <query>')
773
+ .description('Search tasks by title, notes or tags')
774
+ .option('--board <name>', 'Limit search to a specific board')
775
+ .option('--assignee <name>', 'Limit search to a specific assignee')
776
+ .option('--open', 'Only open tasks (default)')
777
+ .option('--all', 'Include completed tasks')
778
+ .action(async (query, opts) => {
779
+ try {
780
+ const state = await getState()
781
+ const q = query.toLowerCase()
782
+ let tasks = state.tasks
783
+
784
+ if (!opts.all) tasks = tasks.filter((t) => !t.completed)
785
+ if (opts.board) {
786
+ const b = findBoard(state.boards, opts.board)
787
+ if (!b) return fail(`Board not found: ${opts.board}`)
788
+ tasks = tasks.filter((t) => t.boardId === b.id)
789
+ }
790
+ if (opts.assignee) {
791
+ const a = findAssignee(state.assignees, opts.assignee)
792
+ if (!a) return fail(`Assignee not found: ${opts.assignee}`)
793
+ tasks = tasks.filter((t) => t.assigneeId === a.id)
794
+ }
795
+
796
+ tasks = tasks.filter((t) =>
797
+ t.title.toLowerCase().includes(q) ||
798
+ (t.notes ?? '').toLowerCase().includes(q) ||
799
+ (t.description ?? '').toLowerCase().includes(q) ||
800
+ (t.tags ?? []).some((tag) => tag.toLowerCase().includes(q))
801
+ )
802
+
803
+ if (!isMachineOutput() && tasks.length === 0) {
804
+ console.log(chalk.gray(`\n No tasks matching "${query}".\n`))
805
+ return
806
+ }
807
+ printTaskList(tasks, state)
808
+ } catch (err) { handleError(err) }
809
+ })
810
+
751
811
  // ── tm requested ──────────────────────────────────────────────────────────────
752
812
 
753
813
  program
@@ -755,7 +815,7 @@ program
755
815
  .description('List open tasks you requested (assigned to others / agents)')
756
816
  .action(async () => {
757
817
  try {
758
- const state = await api('/api/state')
818
+ const state = await getState()
759
819
  const myId = await resolveMyAssigneeId(state)
760
820
  const tasks = state.tasks.filter((t) => t.requestedById === myId && !t.completed)
761
821
  printTaskList(tasks, state)
@@ -774,9 +834,11 @@ program
774
834
  .option('--repo <owner/repo>','GitHub repo')
775
835
  .option('--url <url>', 'Issue / PR URL')
776
836
  .option('--notes <text>', 'Notes')
837
+ .option('--blocks <id>', 'This task blocks another task (provide task id)')
838
+ .option('--blocked-by <id>', 'This task is blocked by another task (provide task id)')
777
839
  .action(async (title, opts) => {
778
840
  try {
779
- const state = await api('/api/state')
841
+ const state = await getState()
780
842
  const myId = await resolveMyAssigneeId(state)
781
843
 
782
844
  let assigneeId = myId
@@ -789,14 +851,19 @@ program
789
851
  requestedById = myId
790
852
  }
791
853
 
792
- const bucket = opts.bucket
793
- ? state.buckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
794
- : state.buckets[0]
795
-
796
854
  const board = opts.board
797
855
  ? state.boards.find((b) => b.name.toLowerCase().includes(opts.board.toLowerCase()))
798
856
  : state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
799
857
 
858
+ if (!board && opts.board) return fail(`Board not found: ${opts.board}`)
859
+ if (!board) console.warn(chalk.yellow(` ⚠ No board specified — defaulting to "${state.boards[0]?.name}". Set a default with: wadah config --default-board "<name>"`))
860
+
861
+ const boardBuckets = board ? state.buckets.filter((b) => b.boardId === board.id) : state.buckets
862
+ const bucket = opts.bucket
863
+ ? boardBuckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
864
+ ?? state.buckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
865
+ : boardBuckets[0] ?? state.buckets[0]
866
+
800
867
  const task = await api('/api/tasks', {
801
868
  method: 'POST',
802
869
  body: JSON.stringify({
@@ -809,9 +876,27 @@ program
809
876
  repo: opts.repo ?? null,
810
877
  contextUrl: opts.url ?? null,
811
878
  notes: opts.notes ?? '',
879
+ tags: [
880
+ ...(opts.blocks ? [`blocks:${opts.blocks.slice(0, 8)}`] : []),
881
+ ...(opts.blockedBy ? [`blocked-by:${opts.blockedBy.slice(0, 8)}`] : []),
882
+ ],
812
883
  }),
813
884
  })
814
885
 
886
+ // If this task blocks another, tag the other task back
887
+ if (opts.blocks) {
888
+ try {
889
+ const other = await api(`/api/tasks/${opts.blocks}`)
890
+ const existingTags = other.tags ?? []
891
+ if (!existingTags.some((t) => t.startsWith('blocked-by:'))) {
892
+ await api(`/api/tasks/${opts.blocks}`, {
893
+ method: 'PATCH',
894
+ body: JSON.stringify({ tags: [...existingTags, `blocked-by:${task.id.slice(0, 8)}`] }),
895
+ })
896
+ }
897
+ } catch {}
898
+ }
899
+
815
900
  const assigneeName = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
816
901
  console.log(chalk.green('\n✓ Created') + ` ${chalk.bold(task.title)}`)
817
902
  console.log(chalk.gray(` ID: ${task.id} · Assigned to: ${assigneeName}\n`))
@@ -856,7 +941,7 @@ program
856
941
  .requiredOption('--to <name>', 'Assignee name')
857
942
  .action(async (id, opts) => {
858
943
  try {
859
- const state = await api('/api/state')
944
+ const state = await getState()
860
945
  const a = findAssignee(state.assignees, opts.to)
861
946
  if (!a) return fail(`Assignee not found: ${opts.to}`)
862
947
 
@@ -874,14 +959,18 @@ program
874
959
  .description('Move a task to another column')
875
960
  .action(async (id, bucketQuery) => {
876
961
  try {
877
- const state = await api('/api/state')
962
+ const state = await getState()
878
963
  const bucket = findBucket(state.buckets, bucketQuery)
879
964
  if (!bucket) return fail(`Bucket not found: ${bucketQuery}`)
965
+ const patch = { bucketId: bucket.id }
966
+ if (bucket.boardId) patch.boardId = bucket.boardId
880
967
  const task = await api(`/api/tasks/${id}`, {
881
968
  method: 'PATCH',
882
- body: JSON.stringify({ bucketId: bucket.id }),
969
+ body: JSON.stringify(patch),
883
970
  })
884
- console.log(chalk.green('\n✓ Moved') + ` ${chalk.bold(task.title)} ${bucket.title}\n`)
971
+ const boardName = state.boards.find((b) => b.id === bucket.boardId)?.name
972
+ const dest = boardName ? `${boardName} / ${bucket.title}` : bucket.title
973
+ console.log(chalk.green('\n✓ Moved') + ` ${chalk.bold(task.title)} → ${dest}\n`)
885
974
  } catch (err) { handleError(err) }
886
975
  })
887
976
 
@@ -899,6 +988,8 @@ program
899
988
  .option('--board <name>', 'Set board')
900
989
  .option('--bucket <name>', 'Set bucket')
901
990
  .option('--completed <true|false>', 'Set completion state')
991
+ .option('--blocks <id>', 'Mark this task as blocking another task')
992
+ .option('--blocked-by <id>', 'Mark this task as blocked by another task')
902
993
  .action(async (id, opts) => {
903
994
  try {
904
995
  const patch = {}
@@ -923,7 +1014,8 @@ program
923
1014
  if (opts.clearDue) patch.dueDate = null
924
1015
 
925
1016
  const needsStateLookup = !!(opts.assignee || opts.board || opts.bucket)
926
- const state = needsStateLookup ? await api('/api/state') : null
1017
+ const state = needsStateLookup ? await getState() : null
1018
+
927
1019
 
928
1020
  if (opts.assignee) {
929
1021
  const a = findAssignee(state.assignees, opts.assignee)
@@ -936,12 +1028,32 @@ program
936
1028
  patch.boardId = b.id
937
1029
  }
938
1030
  if (opts.bucket) {
939
- const b = findBucket(state.buckets, opts.bucket)
1031
+ const targetBoardId = patch.boardId ?? null
1032
+ const scopedBuckets = targetBoardId
1033
+ ? state.buckets.filter((b) => b.boardId === targetBoardId)
1034
+ : state.buckets
1035
+ const b = scopedBuckets.find((b) => b.id === opts.bucket || b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
1036
+ ?? state.buckets.find((b) => b.id === opts.bucket || b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
940
1037
  if (!b) return fail(`Bucket not found: ${opts.bucket}`)
941
1038
  patch.bucketId = b.id
1039
+ if (!patch.boardId && b.boardId) patch.boardId = b.boardId
942
1040
  }
943
1041
  if (opts.completed !== undefined) patch.completed = parsedCompleted
944
1042
 
1043
+ // Handle relationship tags (blocks / blocked-by)
1044
+ if (opts.blocks || opts.blockedBy) {
1045
+ const currentTask = await api(`/api/tasks/${id}`)
1046
+ const currentTags = currentTask.tags ?? []
1047
+ const newTags = [...currentTags]
1048
+ if (opts.blocks && !newTags.some((t) => t.startsWith('blocks:'))) {
1049
+ newTags.push(`blocks:${opts.blocks.slice(0, 8)}`)
1050
+ }
1051
+ if (opts.blockedBy && !newTags.some((t) => t.startsWith('blocked-by:'))) {
1052
+ newTags.push(`blocked-by:${opts.blockedBy.slice(0, 8)}`)
1053
+ }
1054
+ patch.tags = newTags
1055
+ }
1056
+
945
1057
  if (Object.keys(patch).length === 0) {
946
1058
  return fail('Nothing to update. Run wadah update --help')
947
1059
  }
@@ -989,7 +1101,17 @@ program
989
1101
  if (task.dueDate) console.log(chalk.gray(` Due: ${formatDate(task.dueDate)}`))
990
1102
  if (task.repo) console.log(chalk.gray(` Repo: ${task.repo}`))
991
1103
  if (task.contextUrl) console.log(chalk.gray(` Link: ${task.contextUrl}`))
992
- if (task.tags?.length) console.log(chalk.gray(` Tags: #${task.tags.join(' #')}`))
1104
+ if (task.tags?.length) {
1105
+ const relTags = task.tags.filter((t) => t.startsWith('blocks:') || t.startsWith('blocked-by:'))
1106
+ const plainTags = task.tags.filter((t) => !t.startsWith('blocks:') && !t.startsWith('blocked-by:'))
1107
+ if (plainTags.length) console.log(chalk.gray(` Tags: #${plainTags.join(' #')}`))
1108
+ if (relTags.length) {
1109
+ const blocking = relTags.filter((t) => t.startsWith('blocks:')).map((t) => t.replace('blocks:', ''))
1110
+ const blockedBy = relTags.filter((t) => t.startsWith('blocked-by:')).map((t) => t.replace('blocked-by:', ''))
1111
+ if (blocking.length) console.log(chalk.red(` Blocks: ${blocking.join(', ')}`))
1112
+ if (blockedBy.length) console.log(chalk.yellow(` Blocked by: ${blockedBy.join(', ')}`))
1113
+ }
1114
+ }
993
1115
  if (task.description) { console.log(); console.log(` ${task.description}`) }
994
1116
  if (task.notes) { console.log(chalk.gray('\n Notes:')); console.log(` ${task.notes}`) }
995
1117
  if (task.subtasks?.length) {
@@ -1032,7 +1154,17 @@ program
1032
1154
  .description('List all boards')
1033
1155
  .action(async () => {
1034
1156
  try {
1035
- const state = await api('/api/state')
1157
+ const state = await getState()
1158
+ if (isMachineOutput()) {
1159
+ const result = state.boards.map((b) => ({
1160
+ id: b.id,
1161
+ name: b.name,
1162
+ openCount: state.tasks.filter((t) => t.boardId === b.id && !t.completed).length,
1163
+ doneCount: state.tasks.filter((t) => t.boardId === b.id && t.completed).length,
1164
+ }))
1165
+ console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
1166
+ return
1167
+ }
1036
1168
  console.log()
1037
1169
  state.boards.forEach((b) => {
1038
1170
  const open = state.tasks.filter((t) => t.boardId === b.id && !t.completed).length
@@ -1048,8 +1180,20 @@ program
1048
1180
  .description('List all columns (buckets)')
1049
1181
  .action(async () => {
1050
1182
  try {
1051
- const state = await api('/api/state')
1183
+ const state = await getState()
1052
1184
  const boardMap = Object.fromEntries(state.boards.map((b) => [b.id, b.name]))
1185
+ if (isMachineOutput()) {
1186
+ const result = state.buckets.map((b) => ({
1187
+ id: b.id,
1188
+ title: b.title,
1189
+ boardId: b.boardId,
1190
+ boardName: boardMap[b.boardId] ?? '—',
1191
+ openCount: state.tasks.filter((t) => t.bucketId === b.id && !t.completed).length,
1192
+ doneCount: state.tasks.filter((t) => t.bucketId === b.id && t.completed).length,
1193
+ }))
1194
+ console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
1195
+ return
1196
+ }
1053
1197
  console.log()
1054
1198
  state.buckets.forEach((b) => {
1055
1199
  const open = state.tasks.filter((t) => t.bucketId === b.id && !t.completed).length
@@ -1065,7 +1209,18 @@ program
1065
1209
  .description('List all assignees')
1066
1210
  .action(async () => {
1067
1211
  try {
1068
- const state = await api('/api/state')
1212
+ const state = await getState()
1213
+ if (isMachineOutput()) {
1214
+ const result = state.assignees.map((a) => ({
1215
+ id: a.id,
1216
+ name: a.name,
1217
+ type: a.type ?? 'human',
1218
+ openCount: state.tasks.filter((t) => t.assigneeId === a.id && !t.completed).length,
1219
+ doneCount: state.tasks.filter((t) => t.assigneeId === a.id && t.completed).length,
1220
+ }))
1221
+ console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
1222
+ return
1223
+ }
1069
1224
  console.log()
1070
1225
  state.assignees.forEach((a) => {
1071
1226
  const open = state.tasks.filter((t) => t.assigneeId === a.id && !t.completed).length
@@ -1225,7 +1380,7 @@ program
1225
1380
  .action(async function () {
1226
1381
  const opts = this.opts()
1227
1382
  try {
1228
- const state = await api('/api/state')
1383
+ const state = await getState()
1229
1384
  let files = state.files ?? []
1230
1385
  if (opts.folder) files = files.filter((f) => (f.folderId || f.folder_id) === opts.folder)
1231
1386
  if (isMachineOutput()) {
@@ -1578,7 +1733,7 @@ program
1578
1733
  .description('Print full workspace state as JSON')
1579
1734
  .action(async () => {
1580
1735
  try {
1581
- const state = await api('/api/state')
1736
+ const state = await getState()
1582
1737
  console.log(JSON.stringify(state, null, 2))
1583
1738
  } catch (err) { handleError(err) }
1584
1739
  })
@@ -1739,7 +1894,7 @@ const CLI_COMMANDS = [
1739
1894
  'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'boards',
1740
1895
  'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
1741
1896
  'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
1742
- 'move', 'open', 'reopen', 'requested', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
1897
+ 'move', 'open', 'reopen', 'requested', 'search', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
1743
1898
  ]
1744
1899
 
1745
1900
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-wadah",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Open Wadah CLI — shared task board for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {