open-wadah 1.2.1 → 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.
- package/README.md +11 -2
- package/cli.js +882 -37
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# open-wadah
|
|
2
2
|
|
|
3
|
-
Open
|
|
3
|
+
Open WADAH CLI — shared task board for humans and agents. Use it from the terminal or from Cursor, Claude Code, and Kimi.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -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
|
@@ -164,7 +164,10 @@ function printTaskList(tasks, state) {
|
|
|
164
164
|
}
|
|
165
165
|
tasks.forEach((t) => {
|
|
166
166
|
const bucket = bucketMap[t.bucketId] ?? '—'
|
|
167
|
-
const
|
|
167
|
+
const assignedIds = taskAssigneeIds(t)
|
|
168
|
+
const assignee = assignedIds.length === 0
|
|
169
|
+
? 'Unassigned'
|
|
170
|
+
: assignedIds.map((id) => assigneeMap[id] ?? id).join(', ')
|
|
168
171
|
const requester = t.requestedById ? assigneeMap[t.requestedById] : null
|
|
169
172
|
const board = boardMap[t.boardId] ?? '—'
|
|
170
173
|
const id = t.id.slice(0, 8)
|
|
@@ -216,6 +219,12 @@ async function resolveMyAssigneeId(state) {
|
|
|
216
219
|
try {
|
|
217
220
|
const me = await api('/api/auth/me')
|
|
218
221
|
if (me.assigneeId) return me.assigneeId
|
|
222
|
+
// For agent tokens the API always returns assigneeId if the token is linked
|
|
223
|
+
// to an assignee. If it's missing, don't fall back to the human "Me" —
|
|
224
|
+
// return null so callers know the agent has no assignee configured.
|
|
225
|
+
if (me.workspaces?.[0]?.role === 'agent') {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
219
228
|
const userId = me.user?.id
|
|
220
229
|
const byUserId = state.assignees.find((a) => a.user_id === userId)
|
|
221
230
|
if (byUserId) return byUserId.id
|
|
@@ -228,6 +237,38 @@ function findAssignee(assignees, name) {
|
|
|
228
237
|
return assignees.find((a) => a.name.toLowerCase().includes(q)) ?? null
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
function taskAssigneeIds(task) {
|
|
241
|
+
if (Array.isArray(task.assigneeIds) && task.assigneeIds.length > 0) return task.assigneeIds
|
|
242
|
+
if (task.assigneeId) return [task.assigneeId]
|
|
243
|
+
return []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function taskHasAssignee(task, assigneeId) {
|
|
247
|
+
return taskAssigneeIds(task).includes(assigneeId)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveAssigneeIdsByNames(assignees, namesCsv) {
|
|
251
|
+
const parts = String(namesCsv ?? '')
|
|
252
|
+
.split(',')
|
|
253
|
+
.map((x) => x.trim())
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
const ids = []
|
|
256
|
+
for (const name of parts) {
|
|
257
|
+
const a = findAssignee(assignees, name)
|
|
258
|
+
if (!a) throw new CliError('validation_error', `Assignee not found: ${name}`)
|
|
259
|
+
ids.push(a.id)
|
|
260
|
+
}
|
|
261
|
+
const uniqueIds = Array.from(new Set(ids))
|
|
262
|
+
const selected = uniqueIds
|
|
263
|
+
.map((id) => assignees.find((a) => a.id === id))
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
const agentCount = selected.filter((a) => a.type === 'agent').length
|
|
266
|
+
if (agentCount > 1) {
|
|
267
|
+
throw new CliError('validation_error', 'Only one agent can be assigned to a task.')
|
|
268
|
+
}
|
|
269
|
+
return uniqueIds
|
|
270
|
+
}
|
|
271
|
+
|
|
231
272
|
function findBoard(boards, query) {
|
|
232
273
|
const q = query.toLowerCase()
|
|
233
274
|
return boards.find((b) => b.id === query || b.name.toLowerCase().includes(q)) ?? null
|
|
@@ -238,6 +279,23 @@ function findBucket(buckets, query) {
|
|
|
238
279
|
return buckets.find((b) => b.id === query || b.title.toLowerCase().includes(q)) ?? null
|
|
239
280
|
}
|
|
240
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
|
+
|
|
241
299
|
function parseCompletedFlag(value) {
|
|
242
300
|
if (typeof value !== 'string') return null
|
|
243
301
|
const v = value.trim().toLowerCase()
|
|
@@ -246,6 +304,46 @@ function parseCompletedFlag(value) {
|
|
|
246
304
|
throw new CliError('validation_error', '--completed must be true or false')
|
|
247
305
|
}
|
|
248
306
|
|
|
307
|
+
function parseGitHubRepoInput(input) {
|
|
308
|
+
const raw = String(input ?? '').trim()
|
|
309
|
+
if (!raw) return null
|
|
310
|
+
const bare = raw.replace(/^git\+/, '').replace(/\.git$/i, '').trim()
|
|
311
|
+
let owner = ''
|
|
312
|
+
let name = ''
|
|
313
|
+
|
|
314
|
+
const sshMatch = bare.match(/^git@github\.com:([^/]+)\/([^/]+)$/i)
|
|
315
|
+
if (sshMatch) {
|
|
316
|
+
owner = sshMatch[1]
|
|
317
|
+
name = sshMatch[2]
|
|
318
|
+
} else if (/^https?:\/\//i.test(bare)) {
|
|
319
|
+
try {
|
|
320
|
+
const u = new URL(bare)
|
|
321
|
+
if (!/^github\.com$/i.test(u.hostname)) return null
|
|
322
|
+
const parts = u.pathname.split('/').filter(Boolean)
|
|
323
|
+
if (parts.length < 2) return null
|
|
324
|
+
owner = parts[0]
|
|
325
|
+
name = parts[1]
|
|
326
|
+
} catch {
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const parts = bare.split('/').filter(Boolean)
|
|
331
|
+
if (parts.length !== 2) return null
|
|
332
|
+
owner = parts[0]
|
|
333
|
+
name = parts[1]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
owner = owner.replace(/[^a-zA-Z0-9-]/g, '')
|
|
337
|
+
name = name.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
338
|
+
if (!owner || !name) return null
|
|
339
|
+
return {
|
|
340
|
+
owner: owner.toLowerCase(),
|
|
341
|
+
name: name.toLowerCase(),
|
|
342
|
+
fullName: `${owner.toLowerCase()}/${name.toLowerCase()}`,
|
|
343
|
+
url: `https://github.com/${owner.toLowerCase()}/${name.toLowerCase()}`,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
249
347
|
function fail(message) {
|
|
250
348
|
throw new CliError('validation_error', message)
|
|
251
349
|
}
|
|
@@ -303,7 +401,7 @@ Available commands and their args (use these exact command names):
|
|
|
303
401
|
- assignees: args = []. List assignees.
|
|
304
402
|
- state: args = []. Full state JSON.
|
|
305
403
|
- move: args = [task id, bucket name or id]. Move task to another column.
|
|
306
|
-
- assign: args = [task id, assignee
|
|
404
|
+
- assign: args = [task id, "--to", "assignee[,assignee2]", "--mode", "set|add|remove"]. Assign one or more people.
|
|
307
405
|
- whoami: args = []. Show current user/workspace.
|
|
308
406
|
- folders: args = []. List folders.
|
|
309
407
|
- files: args = []. List files; optionally include "--folder", "<id>" in args.
|
|
@@ -326,6 +424,11 @@ Available commands and their args (use these exact command names):
|
|
|
326
424
|
- assignee create: args = [name]. Optional "--type", "human" or "agent".
|
|
327
425
|
- assignee update: args = [assignee id]. Optional "--name", "--type".
|
|
328
426
|
- assignee delete: args = [assignee id]. Delete assignee.
|
|
427
|
+
- repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
|
|
428
|
+
- repo list (or repos): args = []. List linked GitHub repositories.
|
|
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.
|
|
329
432
|
|
|
330
433
|
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
331
434
|
|
|
@@ -707,12 +810,12 @@ program
|
|
|
707
810
|
|
|
708
811
|
if (!opts.all && !opts.assignee) {
|
|
709
812
|
const myId = await resolveMyAssigneeId(state)
|
|
710
|
-
if (myId) tasks = tasks.filter((t) => t
|
|
813
|
+
if (myId) tasks = tasks.filter((t) => taskHasAssignee(t, myId))
|
|
711
814
|
}
|
|
712
815
|
if (opts.assignee) {
|
|
713
816
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
714
817
|
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
715
|
-
tasks = tasks.filter((t) => t
|
|
818
|
+
tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
|
|
716
819
|
}
|
|
717
820
|
if (opts.requestedBy) {
|
|
718
821
|
const a = findAssignee(state.assignees, opts.requestedBy)
|
|
@@ -749,7 +852,7 @@ program
|
|
|
749
852
|
if (opts.assignee) {
|
|
750
853
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
751
854
|
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
752
|
-
tasks = tasks.filter((t) => t
|
|
855
|
+
tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
|
|
753
856
|
}
|
|
754
857
|
if (opts.requestedBy) {
|
|
755
858
|
const a = findAssignee(state.assignees, opts.requestedBy)
|
|
@@ -790,7 +893,7 @@ program
|
|
|
790
893
|
if (opts.assignee) {
|
|
791
894
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
792
895
|
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
793
|
-
tasks = tasks.filter((t) => t
|
|
896
|
+
tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
|
|
794
897
|
}
|
|
795
898
|
|
|
796
899
|
tasks = tasks.filter((t) =>
|
|
@@ -828,9 +931,11 @@ program
|
|
|
828
931
|
.command('add <title>')
|
|
829
932
|
.description('Create a new task')
|
|
830
933
|
.option('--assignee <name>', 'Assign to a person or agent')
|
|
934
|
+
.option('--assignees <names>', 'Assign to multiple people (comma-separated names)')
|
|
831
935
|
.option('--bucket <name>', 'Column name (default: first column)')
|
|
832
936
|
.option('--board <name>', 'Board name (default: current board)')
|
|
833
937
|
.option('--due <YYYY-MM-DD>', 'Due date')
|
|
938
|
+
.option('--priority <level>', 'Priority: urgent, high, medium, low')
|
|
834
939
|
.option('--repo <owner/repo>','GitHub repo')
|
|
835
940
|
.option('--url <url>', 'Issue / PR URL')
|
|
836
941
|
.option('--notes <text>', 'Notes')
|
|
@@ -842,20 +947,31 @@ program
|
|
|
842
947
|
const myId = await resolveMyAssigneeId(state)
|
|
843
948
|
|
|
844
949
|
let assigneeId = myId
|
|
950
|
+
let assigneeIds = assigneeId ? [assigneeId] : []
|
|
845
951
|
let requestedById = null
|
|
846
952
|
|
|
953
|
+
if (opts.assignee && opts.assignees) {
|
|
954
|
+
return fail('Use either --assignee or --assignees, not both.')
|
|
955
|
+
}
|
|
847
956
|
if (opts.assignee) {
|
|
848
957
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
849
958
|
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
850
959
|
assigneeId = a.id
|
|
960
|
+
assigneeIds = [a.id]
|
|
961
|
+
requestedById = myId
|
|
962
|
+
} else if (opts.assignees) {
|
|
963
|
+
assigneeIds = resolveAssigneeIdsByNames(state.assignees, opts.assignees)
|
|
964
|
+
assigneeId = assigneeIds[0] ?? null
|
|
851
965
|
requestedById = myId
|
|
852
966
|
}
|
|
853
967
|
|
|
854
|
-
const
|
|
855
|
-
|
|
968
|
+
const defaultBoardName = opts.board ?? confGet('default_board')
|
|
969
|
+
const board = defaultBoardName
|
|
970
|
+
? state.boards.find((b) => b.name.toLowerCase().includes(defaultBoardName.toLowerCase()))
|
|
856
971
|
: state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
|
|
857
972
|
|
|
858
973
|
if (!board && opts.board) return fail(`Board not found: ${opts.board}`)
|
|
974
|
+
if (!board && defaultBoardName && !opts.board) return fail(`Default board not found: "${defaultBoardName}". Update with: wadah config --default-board "<name>"`)
|
|
859
975
|
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
976
|
|
|
861
977
|
const boardBuckets = board ? state.buckets.filter((b) => b.boardId === board.id) : state.buckets
|
|
@@ -871,8 +987,10 @@ program
|
|
|
871
987
|
bucketId: bucket?.id,
|
|
872
988
|
boardId: board?.id,
|
|
873
989
|
assigneeId,
|
|
990
|
+
assigneeIds,
|
|
874
991
|
requestedById,
|
|
875
992
|
dueDate: opts.due ? new Date(opts.due).getTime() : null,
|
|
993
|
+
priority: opts.priority ?? null,
|
|
876
994
|
repo: opts.repo ?? null,
|
|
877
995
|
contextUrl: opts.url ?? null,
|
|
878
996
|
notes: opts.notes ?? '',
|
|
@@ -897,7 +1015,8 @@ program
|
|
|
897
1015
|
} catch {}
|
|
898
1016
|
}
|
|
899
1017
|
|
|
900
|
-
const
|
|
1018
|
+
const taskAssigneeNames = taskAssigneeIds(task).map((id) => state.assignees.find((a) => a.id === id)?.name ?? id)
|
|
1019
|
+
const assigneeName = taskAssigneeNames.length ? taskAssigneeNames.join(', ') : 'Unassigned'
|
|
901
1020
|
console.log(chalk.green('\n✓ Created') + ` ${chalk.bold(task.title)}`)
|
|
902
1021
|
console.log(chalk.gray(` ID: ${task.id} · Assigned to: ${assigneeName}\n`))
|
|
903
1022
|
} catch (err) { handleError(err) }
|
|
@@ -937,20 +1056,35 @@ program
|
|
|
937
1056
|
|
|
938
1057
|
program
|
|
939
1058
|
.command('assign <id>')
|
|
940
|
-
.description('
|
|
941
|
-
.requiredOption('--to <
|
|
1059
|
+
.description('Assign task assignees (set/add/remove)')
|
|
1060
|
+
.requiredOption('--to <names>', 'Assignee name(s), comma-separated')
|
|
1061
|
+
.option('--mode <mode>', 'set | add | remove (default: set)', 'set')
|
|
942
1062
|
.action(async (id, opts) => {
|
|
943
1063
|
try {
|
|
944
1064
|
const state = await getState()
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
1065
|
+
const mode = String(opts.mode || 'set').toLowerCase()
|
|
1066
|
+
if (!['set', 'add', 'remove'].includes(mode)) return fail('--mode must be one of: set, add, remove')
|
|
1067
|
+
const ids = resolveAssigneeIdsByNames(state.assignees, opts.to)
|
|
1068
|
+
if (ids.length === 0) return fail('Provide at least one assignee in --to')
|
|
1069
|
+
const currentTask = await api(`/api/tasks/${id}`)
|
|
1070
|
+
const current = taskAssigneeIds(currentTask)
|
|
1071
|
+
const nextIds = mode === 'set'
|
|
1072
|
+
? ids
|
|
1073
|
+
: mode === 'add'
|
|
1074
|
+
? Array.from(new Set([...current, ...ids]))
|
|
1075
|
+
: current.filter((x) => !ids.includes(x))
|
|
947
1076
|
|
|
948
1077
|
const myId = await resolveMyAssigneeId(state)
|
|
949
|
-
const task
|
|
1078
|
+
const task = await api(`/api/tasks/${id}`, {
|
|
950
1079
|
method: 'PATCH',
|
|
951
|
-
body:
|
|
1080
|
+
body: JSON.stringify({
|
|
1081
|
+
assigneeIds: nextIds,
|
|
1082
|
+
assigneeId: nextIds[0] ?? null,
|
|
1083
|
+
requestedById: myId,
|
|
1084
|
+
}),
|
|
952
1085
|
})
|
|
953
|
-
|
|
1086
|
+
const names = taskAssigneeIds(task).map((aid) => state.assignees.find((a) => a.id === aid)?.name ?? aid).join(', ') || 'Unassigned'
|
|
1087
|
+
console.log(chalk.green('\n✓ Assigned') + ` ${chalk.bold(task.title)} → ${names}\n`)
|
|
954
1088
|
} catch (err) { handleError(err) }
|
|
955
1089
|
})
|
|
956
1090
|
|
|
@@ -984,7 +1118,9 @@ program
|
|
|
984
1118
|
.option('--url <url>', 'Set issue/PR URL')
|
|
985
1119
|
.option('--due <YYYY-MM-DD>', 'Set due date')
|
|
986
1120
|
.option('--clear-due', 'Clear due date')
|
|
1121
|
+
.option('--priority <level>', 'Set priority: urgent, high, medium, low (or clear)')
|
|
987
1122
|
.option('--assignee <name>', 'Set assignee')
|
|
1123
|
+
.option('--assignees <names>', 'Set all assignees (comma-separated names)')
|
|
988
1124
|
.option('--board <name>', 'Set board')
|
|
989
1125
|
.option('--bucket <name>', 'Set bucket')
|
|
990
1126
|
.option('--completed <true|false>', 'Set completion state')
|
|
@@ -1002,6 +1138,12 @@ program
|
|
|
1002
1138
|
if (opts.notes !== undefined) patch.notes = opts.notes
|
|
1003
1139
|
if (opts.repo !== undefined) patch.repo = opts.repo
|
|
1004
1140
|
if (opts.url !== undefined) patch.contextUrl = opts.url
|
|
1141
|
+
if (opts.priority !== undefined) {
|
|
1142
|
+
const VALID_PRIORITIES = ['urgent', 'high', 'medium', 'low', 'clear', 'none']
|
|
1143
|
+
const p = opts.priority.toLowerCase()
|
|
1144
|
+
if (!VALID_PRIORITIES.includes(p)) return fail(`Invalid priority. Use: urgent, high, medium, low (or "clear" to remove)`)
|
|
1145
|
+
patch.priority = (p === 'clear' || p === 'none') ? null : p
|
|
1146
|
+
}
|
|
1005
1147
|
|
|
1006
1148
|
if (opts.due && opts.clearDue) {
|
|
1007
1149
|
return fail('Use either --due or --clear-due, not both.')
|
|
@@ -1013,14 +1155,22 @@ program
|
|
|
1013
1155
|
}
|
|
1014
1156
|
if (opts.clearDue) patch.dueDate = null
|
|
1015
1157
|
|
|
1016
|
-
const needsStateLookup = !!(opts.assignee || opts.board || opts.bucket)
|
|
1158
|
+
const needsStateLookup = !!(opts.assignee || opts.assignees || opts.board || opts.bucket)
|
|
1017
1159
|
const state = needsStateLookup ? await getState() : null
|
|
1018
1160
|
|
|
1019
|
-
|
|
1161
|
+
if (opts.assignee && opts.assignees) {
|
|
1162
|
+
return fail('Use either --assignee or --assignees, not both.')
|
|
1163
|
+
}
|
|
1020
1164
|
if (opts.assignee) {
|
|
1021
1165
|
const a = findAssignee(state.assignees, opts.assignee)
|
|
1022
1166
|
if (!a) return fail(`Assignee not found: ${opts.assignee}`)
|
|
1023
1167
|
patch.assigneeId = a.id
|
|
1168
|
+
patch.assigneeIds = [a.id]
|
|
1169
|
+
}
|
|
1170
|
+
if (opts.assignees) {
|
|
1171
|
+
const ids = resolveAssigneeIdsByNames(state.assignees, opts.assignees)
|
|
1172
|
+
patch.assigneeIds = ids
|
|
1173
|
+
patch.assigneeId = ids[0] ?? null
|
|
1024
1174
|
}
|
|
1025
1175
|
if (opts.board) {
|
|
1026
1176
|
const b = findBoard(state.boards, opts.board)
|
|
@@ -1087,7 +1237,9 @@ program
|
|
|
1087
1237
|
.action(async (id) => {
|
|
1088
1238
|
try {
|
|
1089
1239
|
const [task, state] = await Promise.all([api(`/api/tasks/${id}`), api('/api/state')])
|
|
1090
|
-
const assignee
|
|
1240
|
+
const assignee = taskAssigneeIds(task)
|
|
1241
|
+
.map((id) => state.assignees.find((a) => a.id === id)?.name ?? id)
|
|
1242
|
+
.join(', ') || 'Unassigned'
|
|
1091
1243
|
const requester = state.assignees.find((a) => a.id === task.requestedById)?.name ?? null
|
|
1092
1244
|
const bucket = state.buckets.find((b) => b.id === task.bucketId)?.title ?? '—'
|
|
1093
1245
|
const board = state.boards.find((b) => b.id === task.boardId)?.name ?? '—'
|
|
@@ -1147,6 +1299,87 @@ program
|
|
|
1147
1299
|
} catch (err) { handleError(err) }
|
|
1148
1300
|
})
|
|
1149
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
|
+
|
|
1150
1383
|
// ── tm boards ─────────────────────────────────────────────────────────────────
|
|
1151
1384
|
|
|
1152
1385
|
program
|
|
@@ -1215,16 +1448,16 @@ program
|
|
|
1215
1448
|
id: a.id,
|
|
1216
1449
|
name: a.name,
|
|
1217
1450
|
type: a.type ?? 'human',
|
|
1218
|
-
openCount: state.tasks.filter((t) => t
|
|
1219
|
-
doneCount: state.tasks.filter((t) => t
|
|
1451
|
+
openCount: state.tasks.filter((t) => taskHasAssignee(t, a.id) && !t.completed).length,
|
|
1452
|
+
doneCount: state.tasks.filter((t) => taskHasAssignee(t, a.id) && t.completed).length,
|
|
1220
1453
|
}))
|
|
1221
1454
|
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1222
1455
|
return
|
|
1223
1456
|
}
|
|
1224
1457
|
console.log()
|
|
1225
1458
|
state.assignees.forEach((a) => {
|
|
1226
|
-
const open = state.tasks.filter((t) => t
|
|
1227
|
-
const done = state.tasks.filter((t) => t
|
|
1459
|
+
const open = state.tasks.filter((t) => taskHasAssignee(t, a.id) && !t.completed).length
|
|
1460
|
+
const done = state.tasks.filter((t) => taskHasAssignee(t, a.id) && t.completed).length
|
|
1228
1461
|
console.log(` ${chalk.bold(a.name)} ${chalk.gray(`${a.type ?? 'human'} · ${open} open · ${done} done`)}`)
|
|
1229
1462
|
})
|
|
1230
1463
|
console.log()
|
|
@@ -1249,6 +1482,29 @@ boardCmd
|
|
|
1249
1482
|
} catch (err) { handleError(err) }
|
|
1250
1483
|
})
|
|
1251
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
|
+
|
|
1252
1508
|
boardCmd
|
|
1253
1509
|
.command('delete <id>')
|
|
1254
1510
|
.description('Delete a board (cannot delete the last one)')
|
|
@@ -1418,6 +1674,158 @@ program
|
|
|
1418
1674
|
} catch (err) { handleError(err) }
|
|
1419
1675
|
})
|
|
1420
1676
|
|
|
1677
|
+
program
|
|
1678
|
+
.command('repos')
|
|
1679
|
+
.description('List linked GitHub repositories')
|
|
1680
|
+
.action(async () => {
|
|
1681
|
+
try {
|
|
1682
|
+
const list = await api('/api/github-repos').catch(() => [])
|
|
1683
|
+
const arr = Array.isArray(list) ? list : []
|
|
1684
|
+
if (isMachineOutput()) {
|
|
1685
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1686
|
+
} else if (arr.length === 0) {
|
|
1687
|
+
console.log(chalk.gray('\nNo linked repositories. Add one: wadah repo add https://github.com/owner/repo\n'))
|
|
1688
|
+
} else {
|
|
1689
|
+
console.log()
|
|
1690
|
+
arr.forEach((r) => {
|
|
1691
|
+
const full = r.fullName ?? `${r.owner}/${r.name}`
|
|
1692
|
+
console.log(` ${chalk.bold(full)} ${chalk.gray(`id: ${r.id?.slice(0, 8)}`)}`)
|
|
1693
|
+
console.log(chalk.gray(` ${r.url}`))
|
|
1694
|
+
})
|
|
1695
|
+
console.log()
|
|
1696
|
+
}
|
|
1697
|
+
} catch (err) { handleError(err) }
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
const repoCmd = program.command('repo').description('Link and manage GitHub repositories')
|
|
1701
|
+
|
|
1702
|
+
repoCmd
|
|
1703
|
+
.command('add <repo>')
|
|
1704
|
+
.description('Link a GitHub repo (URL or owner/repo)')
|
|
1705
|
+
.action(async (repoInput) => {
|
|
1706
|
+
try {
|
|
1707
|
+
const parsed = parseGitHubRepoInput(repoInput)
|
|
1708
|
+
if (!parsed) return fail('Provide a valid GitHub repo URL or owner/repo')
|
|
1709
|
+
const linked = await api('/api/github-repos', {
|
|
1710
|
+
method: 'POST',
|
|
1711
|
+
body: JSON.stringify({ url: parsed.url }),
|
|
1712
|
+
})
|
|
1713
|
+
if (isMachineOutput()) {
|
|
1714
|
+
console.log(JSON.stringify(linked, null, runtimeJsonOutput ? 2 : 0))
|
|
1715
|
+
} else {
|
|
1716
|
+
const full = linked.fullName ?? `${linked.owner}/${linked.name}`
|
|
1717
|
+
console.log(chalk.green('\n✓ Repo linked') + ` ${chalk.bold(full)}`)
|
|
1718
|
+
console.log(chalk.gray(` ${linked.url}\n`))
|
|
1719
|
+
}
|
|
1720
|
+
} catch (err) { handleError(err) }
|
|
1721
|
+
})
|
|
1722
|
+
|
|
1723
|
+
repoCmd
|
|
1724
|
+
.command('list')
|
|
1725
|
+
.description('List linked GitHub repositories')
|
|
1726
|
+
.action(async () => {
|
|
1727
|
+
try {
|
|
1728
|
+
const list = await api('/api/github-repos').catch(() => [])
|
|
1729
|
+
const arr = Array.isArray(list) ? list : []
|
|
1730
|
+
if (isMachineOutput()) {
|
|
1731
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1732
|
+
} else if (arr.length === 0) {
|
|
1733
|
+
console.log(chalk.gray('\nNo linked repositories. Add one: wadah repo add https://github.com/owner/repo\n'))
|
|
1734
|
+
} else {
|
|
1735
|
+
console.log()
|
|
1736
|
+
arr.forEach((r) => {
|
|
1737
|
+
const full = r.fullName ?? `${r.owner}/${r.name}`
|
|
1738
|
+
console.log(` ${chalk.bold(full)} ${chalk.gray(`id: ${r.id?.slice(0, 8)}`)}`)
|
|
1739
|
+
console.log(chalk.gray(` ${r.url}`))
|
|
1740
|
+
})
|
|
1741
|
+
console.log()
|
|
1742
|
+
}
|
|
1743
|
+
} catch (err) { handleError(err) }
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1746
|
+
repoCmd
|
|
1747
|
+
.command('remove <id>')
|
|
1748
|
+
.description('Unlink a repository by id (use: wadah repos)')
|
|
1749
|
+
.action(async (id) => {
|
|
1750
|
+
try {
|
|
1751
|
+
await api(`/api/github-repos/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
1752
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Repo unlinked\n'))
|
|
1753
|
+
} catch (err) { handleError(err) }
|
|
1754
|
+
})
|
|
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
|
+
|
|
1421
1829
|
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1422
1830
|
|
|
1423
1831
|
program
|
|
@@ -1434,8 +1842,12 @@ program
|
|
|
1434
1842
|
} else {
|
|
1435
1843
|
console.log()
|
|
1436
1844
|
arr.forEach((t) => {
|
|
1437
|
-
const as
|
|
1438
|
-
|
|
1845
|
+
const as = t.assigneeName ? ` · ${chalk.cyan(t.assigneeName)}` : ''
|
|
1846
|
+
const creator = t.createdBy ? chalk.gray(` · created by ${t.createdBy}`) : ''
|
|
1847
|
+
const used = t.last_used_at ? chalk.gray(` · last used ${new Date(t.last_used_at).toLocaleDateString()}`) : chalk.gray(' · never used')
|
|
1848
|
+
const usage = t.usageLast30d > 0 ? chalk.gray(` · ${t.usageLast30d} call${t.usageLast30d === 1 ? '' : 's'} (30d)`) : ''
|
|
1849
|
+
console.log(` ${chalk.bold(t.name)}${as}`)
|
|
1850
|
+
console.log(chalk.gray(` id: ${t.id?.slice(0, 8)}${used}${usage}${creator}`))
|
|
1439
1851
|
})
|
|
1440
1852
|
console.log()
|
|
1441
1853
|
}
|
|
@@ -1461,9 +1873,12 @@ agentTokenCmd
|
|
|
1461
1873
|
} else {
|
|
1462
1874
|
console.log(chalk.green('\n✓ Agent token created'))
|
|
1463
1875
|
console.log(chalk.gray(` Name: ${result.name} Id: ${result.id?.slice(0, 8)} Acts as: ${result.assigneeName ?? result.name}`))
|
|
1464
|
-
console.log(chalk.yellow('\n Token (
|
|
1876
|
+
console.log(chalk.yellow('\n Token (store it now — shown once):'))
|
|
1465
1877
|
console.log(` ${result.token}`)
|
|
1466
|
-
console.log(chalk.gray('\n
|
|
1878
|
+
console.log(chalk.gray('\n Store it so you don’t lose it:'))
|
|
1879
|
+
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret → TASK_MANAGER_TOKEN'))
|
|
1880
|
+
console.log(chalk.gray(' · Cursor: Settings → Environment variables → TASK_MANAGER_TOKEN'))
|
|
1881
|
+
console.log(chalk.gray(' · Shell: export TASK_MANAGER_TOKEN="<paste token here>"\n'))
|
|
1467
1882
|
}
|
|
1468
1883
|
} catch (err) { handleError(err) }
|
|
1469
1884
|
})
|
|
@@ -1478,6 +1893,32 @@ agentTokenCmd
|
|
|
1478
1893
|
} catch (err) { handleError(err) }
|
|
1479
1894
|
})
|
|
1480
1895
|
|
|
1896
|
+
agentTokenCmd
|
|
1897
|
+
.command('events <id>')
|
|
1898
|
+
.description('Show audit events for an agent token (created / used / deleted)')
|
|
1899
|
+
.action(async (id) => {
|
|
1900
|
+
try {
|
|
1901
|
+
const events = await api(`/api/agent-tokens/${id}/events`)
|
|
1902
|
+
if (isMachineOutput()) {
|
|
1903
|
+
console.log(JSON.stringify(events, null, runtimeJsonOutput ? 2 : 0))
|
|
1904
|
+
return
|
|
1905
|
+
}
|
|
1906
|
+
if (!events?.length) {
|
|
1907
|
+
console.log(chalk.gray('\nNo events recorded for this token.\n'))
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1910
|
+
console.log()
|
|
1911
|
+
events.forEach((e) => {
|
|
1912
|
+
const when = new Date(e.created_at).toLocaleString()
|
|
1913
|
+
const ep = e.endpoint ? chalk.gray(` ${e.endpoint}`) : ''
|
|
1914
|
+
const ip = e.ip_address ? chalk.gray(` from ${e.ip_address}`) : ''
|
|
1915
|
+
const mark = e.event_type === 'created' ? chalk.green('●') : e.event_type === 'deleted' ? chalk.red('●') : chalk.blue('●')
|
|
1916
|
+
console.log(` ${mark} ${chalk.bold(e.event_type.padEnd(8))} ${chalk.gray(when)}${ep}${ip}`)
|
|
1917
|
+
})
|
|
1918
|
+
console.log()
|
|
1919
|
+
} catch (err) { handleError(err) }
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1481
1922
|
// ── calendar events ──────────────────────────────────────────────────────────
|
|
1482
1923
|
|
|
1483
1924
|
const calendarCmd = program
|
|
@@ -1766,17 +2207,23 @@ program
|
|
|
1766
2207
|
if (!hasEnvToken && !hasSavedToken) {
|
|
1767
2208
|
addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
|
|
1768
2209
|
} else if (hasEnvToken) {
|
|
1769
|
-
addCheck('auth_token', 'pass', 'Using token from env/flag')
|
|
2210
|
+
addCheck('auth_token', 'pass', 'Using agent token from env/flag (TASK_MANAGER_TOKEN)')
|
|
1770
2211
|
} else {
|
|
1771
2212
|
addCheck('auth_token', 'pass', 'Using token from profile config')
|
|
1772
2213
|
}
|
|
1773
2214
|
|
|
1774
2215
|
if (pingOk) {
|
|
2216
|
+
let meBody = null
|
|
1775
2217
|
try {
|
|
1776
2218
|
const meRes = await fetch(`${getApiBase()}/api/auth/me`, { headers: authHeaders() })
|
|
1777
2219
|
if (meRes.ok) {
|
|
1778
|
-
|
|
1779
|
-
|
|
2220
|
+
meBody = await meRes.json().catch(() => ({}))
|
|
2221
|
+
const isAgent = meBody.workspaces?.[0]?.role === 'agent'
|
|
2222
|
+
if (isAgent) {
|
|
2223
|
+
addCheck('auth_me', 'pass', `Authenticated as agent (workspace: ${meBody.workspaces?.[0]?.name ?? '?'})`)
|
|
2224
|
+
} else {
|
|
2225
|
+
addCheck('auth_me', 'pass', `Authenticated as ${meBody.user?.email ?? 'unknown user'}`)
|
|
2226
|
+
}
|
|
1780
2227
|
} else if (meRes.status === 401) {
|
|
1781
2228
|
addCheck('auth_me', 'warn', 'Token is missing/expired. Run wadah login.')
|
|
1782
2229
|
} else {
|
|
@@ -1786,6 +2233,41 @@ program
|
|
|
1786
2233
|
addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
|
|
1787
2234
|
}
|
|
1788
2235
|
|
|
2236
|
+
// Agent-specific checks (only when TASK_MANAGER_TOKEN is set)
|
|
2237
|
+
if (hasEnvToken && meBody) {
|
|
2238
|
+
const isAgent = meBody.workspaces?.[0]?.role === 'agent'
|
|
2239
|
+
if (!isAgent) {
|
|
2240
|
+
addCheck('agent_identity', 'warn', 'Token is not an agent token — it authenticated as a human user')
|
|
2241
|
+
} else {
|
|
2242
|
+
addCheck('agent_identity', 'pass', `Token resolves to agent role`)
|
|
2243
|
+
|
|
2244
|
+
if (meBody.assigneeId) {
|
|
2245
|
+
addCheck('agent_assignee', 'pass', `Assignee: ${meBody.assigneeName ?? meBody.assigneeId}`)
|
|
2246
|
+
} else {
|
|
2247
|
+
addCheck('agent_assignee', 'fail', 'No assignee linked to this token. Create a new token via the app (Agent tokens) so it auto-creates an assignee.')
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const defaultBoard = confGet('default_board')
|
|
2251
|
+
if (!defaultBoard) {
|
|
2252
|
+
addCheck('agent_default_board', 'warn', 'No default board set — wadah add will guess. Fix: wadah config --default-board "<name>"')
|
|
2253
|
+
} else {
|
|
2254
|
+
// Verify board exists
|
|
2255
|
+
try {
|
|
2256
|
+
const state = await getState()
|
|
2257
|
+
const board = state.boards.find((b) => b.name.toLowerCase().includes(defaultBoard.toLowerCase()))
|
|
2258
|
+
if (board) {
|
|
2259
|
+
const buckets = state.buckets.filter((b) => b.boardId === board.id)
|
|
2260
|
+
addCheck('agent_default_board', 'pass', `Default board: "${board.name}" (${buckets.length} bucket${buckets.length === 1 ? '' : 's'})`)
|
|
2261
|
+
} else {
|
|
2262
|
+
addCheck('agent_default_board', 'fail', `Default board "${defaultBoard}" not found. Update: wadah config --default-board "<name>"`)
|
|
2263
|
+
}
|
|
2264
|
+
} catch {
|
|
2265
|
+
addCheck('agent_default_board', 'warn', `Could not verify default board "${defaultBoard}"`)
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
1789
2271
|
try {
|
|
1790
2272
|
const deviceRes = await fetch(`${getApiBase()}/api/auth/device/start`, {
|
|
1791
2273
|
method: 'POST',
|
|
@@ -1861,17 +2343,25 @@ program
|
|
|
1861
2343
|
.command('config')
|
|
1862
2344
|
.description('Show or set CLI configuration')
|
|
1863
2345
|
.option('--api-url <url>', 'Set API base URL')
|
|
2346
|
+
.option('--default-board <name>', 'Set default board for wadah add (agents: avoids landing tasks on wrong board)')
|
|
1864
2347
|
.action((opts) => {
|
|
1865
2348
|
if (opts.apiUrl) {
|
|
1866
2349
|
confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
|
|
1867
2350
|
if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
|
|
1868
2351
|
return
|
|
1869
2352
|
}
|
|
2353
|
+
if (opts.defaultBoard) {
|
|
2354
|
+
confSet('default_board', opts.defaultBoard.trim())
|
|
2355
|
+
if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ Default board set to "${opts.defaultBoard.trim()}"\n wadah add will now use this board unless --board is specified.\n`))
|
|
2356
|
+
return
|
|
2357
|
+
}
|
|
2358
|
+
const defaultBoard = confGet('default_board')
|
|
1870
2359
|
const config = {
|
|
1871
2360
|
api_url: getApiBase(),
|
|
1872
2361
|
signed_in_as: confGet('user_email') ?? null,
|
|
1873
2362
|
agent_mode: Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken),
|
|
1874
2363
|
profile: getProfile(),
|
|
2364
|
+
default_board: defaultBoard ?? null,
|
|
1875
2365
|
config_file: conf.path,
|
|
1876
2366
|
}
|
|
1877
2367
|
if (isMachineOutput()) {
|
|
@@ -1880,23 +2370,378 @@ program
|
|
|
1880
2370
|
}
|
|
1881
2371
|
console.log()
|
|
1882
2372
|
console.log(chalk.bold('CLI config'))
|
|
1883
|
-
console.log(chalk.gray(` API URL:
|
|
1884
|
-
console.log(chalk.gray(` Signed in as:
|
|
1885
|
-
console.log(chalk.gray(` Agent mode:
|
|
1886
|
-
console.log(chalk.gray(`
|
|
1887
|
-
console.log(chalk.gray(`
|
|
2373
|
+
console.log(chalk.gray(` API URL: ${config.api_url}`))
|
|
2374
|
+
console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
|
|
2375
|
+
console.log(chalk.gray(` Agent mode: ${config.agent_mode ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
|
|
2376
|
+
console.log(chalk.gray(` Default board: ${defaultBoard ?? '— (not set, use: wadah config --default-board "Name")'}`))
|
|
2377
|
+
console.log(chalk.gray(` Profile: ${getProfile()}`))
|
|
2378
|
+
console.log(chalk.gray(` Config file: ${config.config_file}`))
|
|
1888
2379
|
console.log()
|
|
1889
2380
|
})
|
|
1890
2381
|
|
|
2382
|
+
// ── wadah board-view ──────────────────────────────────────────────────────────
|
|
2383
|
+
|
|
2384
|
+
program
|
|
2385
|
+
.command('board-view')
|
|
2386
|
+
.description('Show a structured view of a board grouped by bucket — lets agents verify task placement')
|
|
2387
|
+
.option('--board <name>', 'Board name (defaults to current/default board)')
|
|
2388
|
+
.option('--assignee <name>', 'Filter to tasks for a specific assignee')
|
|
2389
|
+
.action(async (opts) => {
|
|
2390
|
+
try {
|
|
2391
|
+
const state = await getState()
|
|
2392
|
+
const defaultBoardName = opts.board ?? confGet('default_board')
|
|
2393
|
+
const board = defaultBoardName
|
|
2394
|
+
? state.boards.find((b) => b.name.toLowerCase().includes(defaultBoardName.toLowerCase()))
|
|
2395
|
+
: state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
|
|
2396
|
+
|
|
2397
|
+
if (!board) return fail(opts.board ? `Board not found: ${opts.board}` : 'No boards found')
|
|
2398
|
+
|
|
2399
|
+
const buckets = state.buckets
|
|
2400
|
+
.filter((b) => b.boardId === board.id)
|
|
2401
|
+
.sort((a, b) => a.order - b.order)
|
|
2402
|
+
|
|
2403
|
+
let tasks = state.tasks.filter((t) => t.boardId === board.id && !t.completed)
|
|
2404
|
+
if (opts.assignee) {
|
|
2405
|
+
const a = findAssignee(state.assignees, opts.assignee)
|
|
2406
|
+
if (a) tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
const view = {
|
|
2410
|
+
board: { id: board.id, name: board.name },
|
|
2411
|
+
buckets: buckets.map((bucket) => ({
|
|
2412
|
+
id: bucket.id,
|
|
2413
|
+
name: bucket.title,
|
|
2414
|
+
tasks: tasks
|
|
2415
|
+
.filter((t) => t.bucketId === bucket.id)
|
|
2416
|
+
.sort((a, b) => a.order - b.order)
|
|
2417
|
+
.map((t) => ({
|
|
2418
|
+
id: t.id,
|
|
2419
|
+
title: t.title,
|
|
2420
|
+
assignee: taskAssigneeIds(t).map((aid) => state.assignees.find((a) => a.id === aid)?.name ?? aid).join(', ') || null,
|
|
2421
|
+
priority: t.priority ?? null,
|
|
2422
|
+
})),
|
|
2423
|
+
})),
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
if (isMachineOutput()) {
|
|
2427
|
+
console.log(JSON.stringify(view, null, runtimeJsonOutput ? 2 : 0))
|
|
2428
|
+
return
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
console.log()
|
|
2432
|
+
console.log(chalk.bold(`Board: ${board.name}`))
|
|
2433
|
+
for (const bucket of view.buckets) {
|
|
2434
|
+
console.log(chalk.gray(`\n ${bucket.name} (${bucket.tasks.length})`))
|
|
2435
|
+
if (bucket.tasks.length === 0) {
|
|
2436
|
+
console.log(chalk.gray(' — empty'))
|
|
2437
|
+
} else {
|
|
2438
|
+
bucket.tasks.forEach((t) => {
|
|
2439
|
+
const who = t.assignee ? chalk.gray(` @${t.assignee}`) : ''
|
|
2440
|
+
console.log(` ${t.id.slice(0, 8)} ${t.title.slice(0, 60)}${who}`)
|
|
2441
|
+
})
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
console.log()
|
|
2445
|
+
} catch (err) { handleError(err) }
|
|
2446
|
+
})
|
|
2447
|
+
|
|
1891
2448
|
// ── shell completion ──────────────────────────────────────────────────────────
|
|
1892
2449
|
|
|
1893
2450
|
const CLI_COMMANDS = [
|
|
1894
|
-
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'boards',
|
|
2451
|
+
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
|
|
1895
2452
|
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
1896
2453
|
'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
|
|
1897
|
-
'move', 'open', 'reopen', 'requested', 'search', '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',
|
|
1898
2455
|
]
|
|
1899
2456
|
|
|
2457
|
+
// ── wadah setup-agent ─────────────────────────────────────────────────────────
|
|
2458
|
+
|
|
2459
|
+
program
|
|
2460
|
+
.command('setup-agent')
|
|
2461
|
+
.description('Interactive wizard: create an agent token, write it to your shell profile, and verify everything works')
|
|
2462
|
+
.option('--name <name>', 'Agent name (skip prompt)')
|
|
2463
|
+
.option('--board <board>', 'Default board name (skip prompt)')
|
|
2464
|
+
.option('--skip-env', 'Skip writing TASK_MANAGER_TOKEN to shell profile (print instructions only)')
|
|
2465
|
+
.action(async (opts) => {
|
|
2466
|
+
const readline = await import('node:readline/promises')
|
|
2467
|
+
const os = await import('node:os')
|
|
2468
|
+
const fs = await import('node:fs')
|
|
2469
|
+
const path = await import('node:path')
|
|
2470
|
+
|
|
2471
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
2472
|
+
const ask = (q) => rl.question(q)
|
|
2473
|
+
|
|
2474
|
+
console.log()
|
|
2475
|
+
console.log(chalk.bold('Open Wadah — Agent Setup Wizard'))
|
|
2476
|
+
console.log(chalk.gray(' This wizard will create an agent token and configure your environment.\n'))
|
|
2477
|
+
|
|
2478
|
+
// Step 1: verify auth
|
|
2479
|
+
console.log(chalk.bold('Step 1/5 Checking authentication…'))
|
|
2480
|
+
let me
|
|
2481
|
+
try {
|
|
2482
|
+
me = await api('/api/auth/me')
|
|
2483
|
+
if (me.workspaces?.[0]?.role === 'agent') {
|
|
2484
|
+
console.log(chalk.yellow(' ⚠ You are currently authenticated as an agent, not a human user.'))
|
|
2485
|
+
console.log(chalk.gray(' Run: wadah login then rerun: wadah setup-agent\n'))
|
|
2486
|
+
rl.close(); return
|
|
2487
|
+
}
|
|
2488
|
+
console.log(chalk.green(` ✓ Signed in as ${me.user?.email ?? 'unknown'}`))
|
|
2489
|
+
} catch {
|
|
2490
|
+
console.log(chalk.red(' ✗ Not authenticated. Run: wadah login'))
|
|
2491
|
+
console.log(chalk.gray(' Then rerun: wadah setup-agent\n'))
|
|
2492
|
+
rl.close(); return
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// Step 2: agent name
|
|
2496
|
+
console.log()
|
|
2497
|
+
console.log(chalk.bold('Step 2/5 Name your agent'))
|
|
2498
|
+
let agentName = opts.name
|
|
2499
|
+
if (!agentName) {
|
|
2500
|
+
agentName = (await ask(chalk.gray(' What should your agent be called? (e.g. "Cursor", "Claude") > '))).trim()
|
|
2501
|
+
} else {
|
|
2502
|
+
console.log(chalk.gray(` Using: ${agentName}`))
|
|
2503
|
+
}
|
|
2504
|
+
if (!agentName) agentName = 'My Agent'
|
|
2505
|
+
|
|
2506
|
+
// Step 3: create token
|
|
2507
|
+
console.log()
|
|
2508
|
+
console.log(chalk.bold('Step 3/5 Creating agent token…'))
|
|
2509
|
+
let tokenResult
|
|
2510
|
+
try {
|
|
2511
|
+
tokenResult = await api('/api/agent-tokens', {
|
|
2512
|
+
method: 'POST',
|
|
2513
|
+
body: JSON.stringify({ name: agentName, assigneeName: agentName }),
|
|
2514
|
+
})
|
|
2515
|
+
console.log(chalk.green(` ✓ Token created (assignee: ${tokenResult.assigneeName ?? agentName})`))
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
console.log(chalk.red(` ✗ Failed to create token: ${err.message}`))
|
|
2518
|
+
rl.close(); return
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
const rawToken = tokenResult.token
|
|
2522
|
+
console.log()
|
|
2523
|
+
console.log(chalk.yellow(' Token (shown once — copy it now):'))
|
|
2524
|
+
console.log(` ${chalk.bold(rawToken)}`)
|
|
2525
|
+
|
|
2526
|
+
// Step 4: write env var to shell profile
|
|
2527
|
+
console.log()
|
|
2528
|
+
console.log(chalk.bold('Step 4/5 Setting up TASK_MANAGER_TOKEN…'))
|
|
2529
|
+
|
|
2530
|
+
const shell = process.env.SHELL ?? ''
|
|
2531
|
+
const profileMap = { zsh: '.zshrc', bash: '.bashrc', fish: '.config/fish/config.fish' }
|
|
2532
|
+
const profileKey = Object.keys(profileMap).find((k) => shell.includes(k))
|
|
2533
|
+
const profileFile = profileKey ? path.join(os.homedir(), profileMap[profileKey]) : null
|
|
2534
|
+
const marker = '# added by wadah setup-agent'
|
|
2535
|
+
const exportLine = `\nexport TASK_MANAGER_TOKEN="${rawToken}" ${marker}\n`
|
|
2536
|
+
|
|
2537
|
+
if (opts.skipEnv || !profileFile) {
|
|
2538
|
+
console.log(chalk.gray(' Skipping automatic profile write. Add this line manually:'))
|
|
2539
|
+
console.log(chalk.gray(` export TASK_MANAGER_TOKEN="${rawToken}"`))
|
|
2540
|
+
const rcGuess = shell.includes('zsh') ? '~/.zshrc' : shell.includes('fish') ? '~/.config/fish/config.fish' : '~/.bashrc'
|
|
2541
|
+
console.log(chalk.gray(` Then reload: source ${rcGuess}`))
|
|
2542
|
+
} else {
|
|
2543
|
+
const confirm = await ask(chalk.gray(` Write to ${profileFile}? (Y/n) > `))
|
|
2544
|
+
if (confirm.trim().toLowerCase() !== 'n') {
|
|
2545
|
+
try {
|
|
2546
|
+
const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : ''
|
|
2547
|
+
const filtered = existing
|
|
2548
|
+
.split('\n')
|
|
2549
|
+
.filter((line) => !line.includes('TASK_MANAGER_TOKEN=') && !line.includes(marker))
|
|
2550
|
+
.join('\n')
|
|
2551
|
+
const next = `${filtered.trimEnd()}${exportLine}`
|
|
2552
|
+
fs.writeFileSync(profileFile, `${next.endsWith('\n') ? next : `${next}\n`}`)
|
|
2553
|
+
console.log(chalk.green(` ✓ Written to ${profileFile}`))
|
|
2554
|
+
console.log(chalk.gray(` Reload now: source ${profileFile}`))
|
|
2555
|
+
} catch (e) {
|
|
2556
|
+
console.log(chalk.yellow(` ⚠ Could not write to ${profileFile}: ${e.message}`))
|
|
2557
|
+
console.log(chalk.gray(` Add manually: export TASK_MANAGER_TOKEN="${rawToken}"`))
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
console.log(chalk.gray(` Skipped. Add manually: export TASK_MANAGER_TOKEN="${rawToken}"`))
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// Step 4.5: verify the created token works (without requiring shell reload)
|
|
2565
|
+
console.log()
|
|
2566
|
+
console.log(chalk.bold('Step 4.5/5 Verifying token…'))
|
|
2567
|
+
try {
|
|
2568
|
+
const verifyRes = await fetch(`${getApiBase()}/api/auth/me`, {
|
|
2569
|
+
headers: { Authorization: `Bearer ${rawToken}`, 'Content-Type': 'application/json' },
|
|
2570
|
+
})
|
|
2571
|
+
if (!verifyRes.ok) {
|
|
2572
|
+
console.log(chalk.yellow(` ⚠ Token verification returned ${verifyRes.status}. Continue and run: wadah doctor`))
|
|
2573
|
+
} else {
|
|
2574
|
+
const verifyBody = await verifyRes.json().catch(() => ({}))
|
|
2575
|
+
const role = verifyBody?.workspaces?.[0]?.role ?? '?'
|
|
2576
|
+
const actsAs = verifyBody?.assigneeName ?? tokenResult.assigneeName ?? agentName
|
|
2577
|
+
console.log(chalk.green(` ✓ Token valid (role: ${role}, assignee: ${actsAs})`))
|
|
2578
|
+
}
|
|
2579
|
+
} catch (e) {
|
|
2580
|
+
console.log(chalk.yellow(` ⚠ Could not verify token now: ${e.message}`))
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// Step 5: default board
|
|
2584
|
+
console.log()
|
|
2585
|
+
console.log(chalk.bold('Step 5/5 Default board'))
|
|
2586
|
+
let boardName = opts.board
|
|
2587
|
+
if (!boardName) {
|
|
2588
|
+
try {
|
|
2589
|
+
const state = await getState()
|
|
2590
|
+
if (state.boards?.length > 0) {
|
|
2591
|
+
console.log(chalk.gray(' Available boards:'))
|
|
2592
|
+
state.boards.forEach((b, i) => console.log(chalk.gray(` ${i + 1}. ${b.name}`)))
|
|
2593
|
+
const boardInput = (await ask(chalk.gray(' Which board should new tasks land on by default? (name or number, Enter to skip) > '))).trim()
|
|
2594
|
+
if (boardInput) {
|
|
2595
|
+
const byNum = parseInt(boardInput, 10)
|
|
2596
|
+
const picked = !isNaN(byNum) ? state.boards[byNum - 1] : state.boards.find((b) => b.name.toLowerCase().includes(boardInput.toLowerCase()))
|
|
2597
|
+
if (picked) {
|
|
2598
|
+
confSet('default_board', picked.name)
|
|
2599
|
+
boardName = picked.name
|
|
2600
|
+
console.log(chalk.green(` ✓ Default board: "${picked.name}"`))
|
|
2601
|
+
} else {
|
|
2602
|
+
console.log(chalk.yellow(` ⚠ Board not found — skipping. Set later: wadah config --default-board "<name>"`))
|
|
2603
|
+
}
|
|
2604
|
+
} else {
|
|
2605
|
+
console.log(chalk.gray(' Skipped. Set later: wadah config --default-board "<name>"'))
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
} catch { /* board step is optional */ }
|
|
2609
|
+
} else {
|
|
2610
|
+
confSet('default_board', boardName)
|
|
2611
|
+
console.log(chalk.green(` ✓ Default board: "${boardName}"`))
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
rl.close()
|
|
2615
|
+
|
|
2616
|
+
console.log()
|
|
2617
|
+
console.log(chalk.bold.green('✓ Setup complete!'))
|
|
2618
|
+
console.log()
|
|
2619
|
+
console.log(chalk.gray(' Next steps:'))
|
|
2620
|
+
console.log(chalk.gray(` 1. Reload your shell profile (or open a new terminal)`))
|
|
2621
|
+
console.log(chalk.gray(` 2. Verify everything works: wadah doctor`))
|
|
2622
|
+
console.log(chalk.gray(` 3. Create your first task: wadah add "My first task"`))
|
|
2623
|
+
console.log()
|
|
2624
|
+
})
|
|
2625
|
+
|
|
2626
|
+
// ── agent memory ─────────────────────────────────────────────────────────────
|
|
2627
|
+
|
|
2628
|
+
program
|
|
2629
|
+
.command('memory')
|
|
2630
|
+
.description('Show all memory for this agent, grouped by category')
|
|
2631
|
+
.option('--category <cat>', 'Filter to a specific category')
|
|
2632
|
+
.option('--log', 'Show memory change history instead of current values')
|
|
2633
|
+
.option('--export', 'Export all memory as a JSON blob (for backup or handoff)')
|
|
2634
|
+
.action(async (opts) => {
|
|
2635
|
+
try {
|
|
2636
|
+
if (opts.export) {
|
|
2637
|
+
const data = await api('/api/agent-memory/export')
|
|
2638
|
+
console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
|
|
2639
|
+
return
|
|
2640
|
+
}
|
|
2641
|
+
if (opts.log) {
|
|
2642
|
+
const log = await api('/api/agent-memory/log')
|
|
2643
|
+
if (isMachineOutput()) { console.log(JSON.stringify(log, null, runtimeJsonOutput ? 2 : 0)); return }
|
|
2644
|
+
if (!log?.length) { console.log(chalk.gray('\nNo memory changes recorded yet.\n')); return }
|
|
2645
|
+
console.log()
|
|
2646
|
+
log.forEach((e) => {
|
|
2647
|
+
const when = new Date(e.changed_at).toLocaleString()
|
|
2648
|
+
const key = e.agent_memory?.key ?? '?'
|
|
2649
|
+
console.log(` ${chalk.gray(when)} ${chalk.bold(key)}`)
|
|
2650
|
+
if (e.old_value !== null) console.log(chalk.gray(` was: ${String(e.old_value).slice(0, 80)}`))
|
|
2651
|
+
console.log(chalk.gray(` now: ${String(e.new_value).slice(0, 80)}`))
|
|
2652
|
+
})
|
|
2653
|
+
console.log()
|
|
2654
|
+
return
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
const url = opts.category ? `/api/agent-memory?category=${encodeURIComponent(opts.category)}` : '/api/agent-memory'
|
|
2658
|
+
const entries = await api(url)
|
|
2659
|
+
if (isMachineOutput()) { console.log(JSON.stringify(entries, null, runtimeJsonOutput ? 2 : 0)); return }
|
|
2660
|
+
if (!entries?.length) { console.log(chalk.gray('\nNo memory stored yet. Use: wadah remember <key> <value>\n')); return }
|
|
2661
|
+
|
|
2662
|
+
console.log()
|
|
2663
|
+
const byCategory = {}
|
|
2664
|
+
entries.forEach((e) => {
|
|
2665
|
+
const cat = e.category ?? 'general'
|
|
2666
|
+
if (!byCategory[cat]) byCategory[cat] = []
|
|
2667
|
+
byCategory[cat].push(e)
|
|
2668
|
+
})
|
|
2669
|
+
for (const [cat, items] of Object.entries(byCategory)) {
|
|
2670
|
+
console.log(chalk.bold(`${cat.charAt(0).toUpperCase() + cat.slice(1)}`))
|
|
2671
|
+
items.forEach((e) => {
|
|
2672
|
+
const conf = e.confidence < 1 ? chalk.yellow(` (${Math.round(e.confidence * 100)}% confidence)`) : ''
|
|
2673
|
+
const val = String(e.value).slice(0, 80)
|
|
2674
|
+
console.log(` ${e.key.padEnd(28)} ${val}${conf}`)
|
|
2675
|
+
})
|
|
2676
|
+
console.log()
|
|
2677
|
+
}
|
|
2678
|
+
} catch (err) { handleError(err) }
|
|
2679
|
+
})
|
|
2680
|
+
|
|
2681
|
+
program
|
|
2682
|
+
.command('remember <key> <value>')
|
|
2683
|
+
.description('Write or update a memory entry for this agent')
|
|
2684
|
+
.option('--category <cat>', 'Category (config, preferences, workspace, session, learned)', 'general')
|
|
2685
|
+
.option('--confidence <0-1>', 'Confidence level (default 1.0)', '1.0')
|
|
2686
|
+
.option('--source <text>', 'How this was learned (e.g. user_confirmed, inferred)')
|
|
2687
|
+
.option('--session <id>', 'Session identifier for audit log')
|
|
2688
|
+
.action(async (key, value, opts) => {
|
|
2689
|
+
try {
|
|
2690
|
+
const body = {
|
|
2691
|
+
value,
|
|
2692
|
+
category: opts.category,
|
|
2693
|
+
confidence: parseFloat(opts.confidence || '1'),
|
|
2694
|
+
source: opts.source ?? null,
|
|
2695
|
+
sessionId: opts.session ?? null,
|
|
2696
|
+
}
|
|
2697
|
+
const result = await api(`/api/agent-memory/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(body) })
|
|
2698
|
+
if (isMachineOutput()) { console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0)); return }
|
|
2699
|
+
console.log(chalk.green(`\n✓ Remembered ${chalk.bold(key)} = ${value}\n`))
|
|
2700
|
+
} catch (err) { handleError(err) }
|
|
2701
|
+
})
|
|
2702
|
+
|
|
2703
|
+
program
|
|
2704
|
+
.command('forget <key>')
|
|
2705
|
+
.description('Delete a memory entry')
|
|
2706
|
+
.action(async (key) => {
|
|
2707
|
+
try {
|
|
2708
|
+
await api(`/api/agent-memory/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
|
2709
|
+
if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ Forgotten ${key}\n`))
|
|
2710
|
+
} catch (err) { handleError(err) }
|
|
2711
|
+
})
|
|
2712
|
+
|
|
2713
|
+
program
|
|
2714
|
+
.command('memory-log')
|
|
2715
|
+
.description('Alias for: wadah memory --log')
|
|
2716
|
+
.action(async () => {
|
|
2717
|
+
try {
|
|
2718
|
+
const log = await api('/api/agent-memory/log')
|
|
2719
|
+
if (isMachineOutput()) { console.log(JSON.stringify(log, null, runtimeJsonOutput ? 2 : 0)); return }
|
|
2720
|
+
if (!log?.length) { console.log(chalk.gray('\nNo memory changes recorded yet.\n')); return }
|
|
2721
|
+
console.log()
|
|
2722
|
+
log.forEach((e) => {
|
|
2723
|
+
const when = new Date(e.changed_at).toLocaleString()
|
|
2724
|
+
const key = e.agent_memory?.key ?? '?'
|
|
2725
|
+
console.log(` ${chalk.gray(when)} ${chalk.bold(key)}`)
|
|
2726
|
+
if (e.old_value !== null) console.log(chalk.gray(` was: ${String(e.old_value).slice(0, 80)}`))
|
|
2727
|
+
console.log(chalk.gray(` now: ${String(e.new_value).slice(0, 80)}`))
|
|
2728
|
+
})
|
|
2729
|
+
console.log()
|
|
2730
|
+
} catch (err) { handleError(err) }
|
|
2731
|
+
})
|
|
2732
|
+
|
|
2733
|
+
program
|
|
2734
|
+
.command('memory-export')
|
|
2735
|
+
.description('Alias for: wadah memory --export')
|
|
2736
|
+
.action(async () => {
|
|
2737
|
+
try {
|
|
2738
|
+
const data = await api('/api/agent-memory/export')
|
|
2739
|
+
console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
|
|
2740
|
+
} catch (err) { handleError(err) }
|
|
2741
|
+
})
|
|
2742
|
+
|
|
2743
|
+
// ── shell completion ──────────────────────────────────────────────────────────
|
|
2744
|
+
|
|
1900
2745
|
program
|
|
1901
2746
|
.command('completion [shell]')
|
|
1902
2747
|
.description('Print shell completion script (bash or zsh). Add to your profile: wadah completion bash >> ~/.bashrc')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-wadah",
|
|
3
|
-
"version": "1.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": {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"test": "node --test test/cli.test.js"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@inquirer/prompts": "^8.3.
|
|
16
|
+
"@inquirer/prompts": "^8.3.2",
|
|
17
17
|
"chalk": "^5.4.1",
|
|
18
|
-
"commander": "^
|
|
19
|
-
"conf": "^
|
|
18
|
+
"commander": "^14.0.3",
|
|
19
|
+
"conf": "^15.1.0",
|
|
20
20
|
"form-data": "^4.0.0",
|
|
21
21
|
"node-fetch": "^3.3.2"
|
|
22
22
|
}
|