open-wadah 1.2.1 → 1.2.2

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 +1 -1
  2. package/cli.js +686 -37
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # open-wadah
2
2
 
3
- Open Wadah CLI — shared task board for humans and agents. Use it from the terminal or from Cursor, Claude Code, and Kimi.
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
 
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 assignee = assigneeMap[t.assigneeId] ?? 'Unassigned'
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
@@ -246,6 +287,46 @@ function parseCompletedFlag(value) {
246
287
  throw new CliError('validation_error', '--completed must be true or false')
247
288
  }
248
289
 
290
+ function parseGitHubRepoInput(input) {
291
+ const raw = String(input ?? '').trim()
292
+ if (!raw) return null
293
+ const bare = raw.replace(/^git\+/, '').replace(/\.git$/i, '').trim()
294
+ let owner = ''
295
+ let name = ''
296
+
297
+ const sshMatch = bare.match(/^git@github\.com:([^/]+)\/([^/]+)$/i)
298
+ if (sshMatch) {
299
+ owner = sshMatch[1]
300
+ name = sshMatch[2]
301
+ } else if (/^https?:\/\//i.test(bare)) {
302
+ try {
303
+ const u = new URL(bare)
304
+ if (!/^github\.com$/i.test(u.hostname)) return null
305
+ const parts = u.pathname.split('/').filter(Boolean)
306
+ if (parts.length < 2) return null
307
+ owner = parts[0]
308
+ name = parts[1]
309
+ } catch {
310
+ return null
311
+ }
312
+ } else {
313
+ const parts = bare.split('/').filter(Boolean)
314
+ if (parts.length !== 2) return null
315
+ owner = parts[0]
316
+ name = parts[1]
317
+ }
318
+
319
+ owner = owner.replace(/[^a-zA-Z0-9-]/g, '')
320
+ name = name.replace(/[^a-zA-Z0-9._-]/g, '')
321
+ if (!owner || !name) return null
322
+ return {
323
+ owner: owner.toLowerCase(),
324
+ name: name.toLowerCase(),
325
+ fullName: `${owner.toLowerCase()}/${name.toLowerCase()}`,
326
+ url: `https://github.com/${owner.toLowerCase()}/${name.toLowerCase()}`,
327
+ }
328
+ }
329
+
249
330
  function fail(message) {
250
331
  throw new CliError('validation_error', message)
251
332
  }
@@ -303,7 +384,7 @@ Available commands and their args (use these exact command names):
303
384
  - assignees: args = []. List assignees.
304
385
  - state: args = []. Full state JSON.
305
386
  - move: args = [task id, bucket name or id]. Move task to another column.
306
- - assign: args = [task id, assignee name]. Assign a task.
387
+ - assign: args = [task id, "--to", "assignee[,assignee2]", "--mode", "set|add|remove"]. Assign one or more people.
307
388
  - whoami: args = []. Show current user/workspace.
308
389
  - folders: args = []. List folders.
309
390
  - files: args = []. List files; optionally include "--folder", "<id>" in args.
@@ -326,6 +407,9 @@ Available commands and their args (use these exact command names):
326
407
  - assignee create: args = [name]. Optional "--type", "human" or "agent".
327
408
  - assignee update: args = [assignee id]. Optional "--name", "--type".
328
409
  - assignee delete: args = [assignee id]. Delete assignee.
410
+ - repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
411
+ - repo list (or repos): args = []. List linked GitHub repositories.
412
+ - repo remove: args = [repo id]. Unlink a repository.
329
413
 
330
414
  If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
331
415
 
@@ -707,12 +791,12 @@ program
707
791
 
708
792
  if (!opts.all && !opts.assignee) {
709
793
  const myId = await resolveMyAssigneeId(state)
710
- if (myId) tasks = tasks.filter((t) => t.assigneeId === myId)
794
+ if (myId) tasks = tasks.filter((t) => taskHasAssignee(t, myId))
711
795
  }
712
796
  if (opts.assignee) {
713
797
  const a = findAssignee(state.assignees, opts.assignee)
714
798
  if (!a) return fail(`Assignee not found: ${opts.assignee}`)
715
- tasks = tasks.filter((t) => t.assigneeId === a.id)
799
+ tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
716
800
  }
717
801
  if (opts.requestedBy) {
718
802
  const a = findAssignee(state.assignees, opts.requestedBy)
@@ -749,7 +833,7 @@ program
749
833
  if (opts.assignee) {
750
834
  const a = findAssignee(state.assignees, opts.assignee)
751
835
  if (!a) return fail(`Assignee not found: ${opts.assignee}`)
752
- tasks = tasks.filter((t) => t.assigneeId === a.id)
836
+ tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
753
837
  }
754
838
  if (opts.requestedBy) {
755
839
  const a = findAssignee(state.assignees, opts.requestedBy)
@@ -790,7 +874,7 @@ program
790
874
  if (opts.assignee) {
791
875
  const a = findAssignee(state.assignees, opts.assignee)
792
876
  if (!a) return fail(`Assignee not found: ${opts.assignee}`)
793
- tasks = tasks.filter((t) => t.assigneeId === a.id)
877
+ tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
794
878
  }
795
879
 
796
880
  tasks = tasks.filter((t) =>
@@ -828,9 +912,11 @@ program
828
912
  .command('add <title>')
829
913
  .description('Create a new task')
830
914
  .option('--assignee <name>', 'Assign to a person or agent')
915
+ .option('--assignees <names>', 'Assign to multiple people (comma-separated names)')
831
916
  .option('--bucket <name>', 'Column name (default: first column)')
832
917
  .option('--board <name>', 'Board name (default: current board)')
833
918
  .option('--due <YYYY-MM-DD>', 'Due date')
919
+ .option('--priority <level>', 'Priority: urgent, high, medium, low')
834
920
  .option('--repo <owner/repo>','GitHub repo')
835
921
  .option('--url <url>', 'Issue / PR URL')
836
922
  .option('--notes <text>', 'Notes')
@@ -842,20 +928,31 @@ program
842
928
  const myId = await resolveMyAssigneeId(state)
843
929
 
844
930
  let assigneeId = myId
931
+ let assigneeIds = assigneeId ? [assigneeId] : []
845
932
  let requestedById = null
846
933
 
934
+ if (opts.assignee && opts.assignees) {
935
+ return fail('Use either --assignee or --assignees, not both.')
936
+ }
847
937
  if (opts.assignee) {
848
938
  const a = findAssignee(state.assignees, opts.assignee)
849
939
  if (!a) return fail(`Assignee not found: ${opts.assignee}`)
850
940
  assigneeId = a.id
941
+ assigneeIds = [a.id]
942
+ requestedById = myId
943
+ } else if (opts.assignees) {
944
+ assigneeIds = resolveAssigneeIdsByNames(state.assignees, opts.assignees)
945
+ assigneeId = assigneeIds[0] ?? null
851
946
  requestedById = myId
852
947
  }
853
948
 
854
- const board = opts.board
855
- ? state.boards.find((b) => b.name.toLowerCase().includes(opts.board.toLowerCase()))
949
+ const defaultBoardName = opts.board ?? confGet('default_board')
950
+ const board = defaultBoardName
951
+ ? state.boards.find((b) => b.name.toLowerCase().includes(defaultBoardName.toLowerCase()))
856
952
  : state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
857
953
 
858
954
  if (!board && opts.board) return fail(`Board not found: ${opts.board}`)
955
+ if (!board && defaultBoardName && !opts.board) return fail(`Default board not found: "${defaultBoardName}". Update with: wadah config --default-board "<name>"`)
859
956
  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
957
 
861
958
  const boardBuckets = board ? state.buckets.filter((b) => b.boardId === board.id) : state.buckets
@@ -871,8 +968,10 @@ program
871
968
  bucketId: bucket?.id,
872
969
  boardId: board?.id,
873
970
  assigneeId,
971
+ assigneeIds,
874
972
  requestedById,
875
973
  dueDate: opts.due ? new Date(opts.due).getTime() : null,
974
+ priority: opts.priority ?? null,
876
975
  repo: opts.repo ?? null,
877
976
  contextUrl: opts.url ?? null,
878
977
  notes: opts.notes ?? '',
@@ -897,7 +996,8 @@ program
897
996
  } catch {}
898
997
  }
899
998
 
900
- const assigneeName = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
999
+ const taskAssigneeNames = taskAssigneeIds(task).map((id) => state.assignees.find((a) => a.id === id)?.name ?? id)
1000
+ const assigneeName = taskAssigneeNames.length ? taskAssigneeNames.join(', ') : 'Unassigned'
901
1001
  console.log(chalk.green('\n✓ Created') + ` ${chalk.bold(task.title)}`)
902
1002
  console.log(chalk.gray(` ID: ${task.id} · Assigned to: ${assigneeName}\n`))
903
1003
  } catch (err) { handleError(err) }
@@ -937,20 +1037,35 @@ program
937
1037
 
938
1038
  program
939
1039
  .command('assign <id>')
940
- .description('Reassign a task to another person or agent')
941
- .requiredOption('--to <name>', 'Assignee name')
1040
+ .description('Assign task assignees (set/add/remove)')
1041
+ .requiredOption('--to <names>', 'Assignee name(s), comma-separated')
1042
+ .option('--mode <mode>', 'set | add | remove (default: set)', 'set')
942
1043
  .action(async (id, opts) => {
943
1044
  try {
944
1045
  const state = await getState()
945
- const a = findAssignee(state.assignees, opts.to)
946
- if (!a) return fail(`Assignee not found: ${opts.to}`)
1046
+ const mode = String(opts.mode || 'set').toLowerCase()
1047
+ if (!['set', 'add', 'remove'].includes(mode)) return fail('--mode must be one of: set, add, remove')
1048
+ const ids = resolveAssigneeIdsByNames(state.assignees, opts.to)
1049
+ if (ids.length === 0) return fail('Provide at least one assignee in --to')
1050
+ const currentTask = await api(`/api/tasks/${id}`)
1051
+ const current = taskAssigneeIds(currentTask)
1052
+ const nextIds = mode === 'set'
1053
+ ? ids
1054
+ : mode === 'add'
1055
+ ? Array.from(new Set([...current, ...ids]))
1056
+ : current.filter((x) => !ids.includes(x))
947
1057
 
948
1058
  const myId = await resolveMyAssigneeId(state)
949
- const task = await api(`/api/tasks/${id}`, {
1059
+ const task = await api(`/api/tasks/${id}`, {
950
1060
  method: 'PATCH',
951
- body: JSON.stringify({ assigneeId: a.id, requestedById: myId }),
1061
+ body: JSON.stringify({
1062
+ assigneeIds: nextIds,
1063
+ assigneeId: nextIds[0] ?? null,
1064
+ requestedById: myId,
1065
+ }),
952
1066
  })
953
- console.log(chalk.green('\n✓ Assigned') + ` ${chalk.bold(task.title)} ${a.name}\n`)
1067
+ const names = taskAssigneeIds(task).map((aid) => state.assignees.find((a) => a.id === aid)?.name ?? aid).join(', ') || 'Unassigned'
1068
+ console.log(chalk.green('\n✓ Assigned') + ` ${chalk.bold(task.title)} → ${names}\n`)
954
1069
  } catch (err) { handleError(err) }
955
1070
  })
956
1071
 
@@ -984,7 +1099,9 @@ program
984
1099
  .option('--url <url>', 'Set issue/PR URL')
985
1100
  .option('--due <YYYY-MM-DD>', 'Set due date')
986
1101
  .option('--clear-due', 'Clear due date')
1102
+ .option('--priority <level>', 'Set priority: urgent, high, medium, low (or clear)')
987
1103
  .option('--assignee <name>', 'Set assignee')
1104
+ .option('--assignees <names>', 'Set all assignees (comma-separated names)')
988
1105
  .option('--board <name>', 'Set board')
989
1106
  .option('--bucket <name>', 'Set bucket')
990
1107
  .option('--completed <true|false>', 'Set completion state')
@@ -1002,6 +1119,12 @@ program
1002
1119
  if (opts.notes !== undefined) patch.notes = opts.notes
1003
1120
  if (opts.repo !== undefined) patch.repo = opts.repo
1004
1121
  if (opts.url !== undefined) patch.contextUrl = opts.url
1122
+ if (opts.priority !== undefined) {
1123
+ const VALID_PRIORITIES = ['urgent', 'high', 'medium', 'low', 'clear', 'none']
1124
+ const p = opts.priority.toLowerCase()
1125
+ if (!VALID_PRIORITIES.includes(p)) return fail(`Invalid priority. Use: urgent, high, medium, low (or "clear" to remove)`)
1126
+ patch.priority = (p === 'clear' || p === 'none') ? null : p
1127
+ }
1005
1128
 
1006
1129
  if (opts.due && opts.clearDue) {
1007
1130
  return fail('Use either --due or --clear-due, not both.')
@@ -1013,14 +1136,22 @@ program
1013
1136
  }
1014
1137
  if (opts.clearDue) patch.dueDate = null
1015
1138
 
1016
- const needsStateLookup = !!(opts.assignee || opts.board || opts.bucket)
1139
+ const needsStateLookup = !!(opts.assignee || opts.assignees || opts.board || opts.bucket)
1017
1140
  const state = needsStateLookup ? await getState() : null
1018
1141
 
1019
-
1142
+ if (opts.assignee && opts.assignees) {
1143
+ return fail('Use either --assignee or --assignees, not both.')
1144
+ }
1020
1145
  if (opts.assignee) {
1021
1146
  const a = findAssignee(state.assignees, opts.assignee)
1022
1147
  if (!a) return fail(`Assignee not found: ${opts.assignee}`)
1023
1148
  patch.assigneeId = a.id
1149
+ patch.assigneeIds = [a.id]
1150
+ }
1151
+ if (opts.assignees) {
1152
+ const ids = resolveAssigneeIdsByNames(state.assignees, opts.assignees)
1153
+ patch.assigneeIds = ids
1154
+ patch.assigneeId = ids[0] ?? null
1024
1155
  }
1025
1156
  if (opts.board) {
1026
1157
  const b = findBoard(state.boards, opts.board)
@@ -1087,7 +1218,9 @@ program
1087
1218
  .action(async (id) => {
1088
1219
  try {
1089
1220
  const [task, state] = await Promise.all([api(`/api/tasks/${id}`), api('/api/state')])
1090
- const assignee = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
1221
+ const assignee = taskAssigneeIds(task)
1222
+ .map((id) => state.assignees.find((a) => a.id === id)?.name ?? id)
1223
+ .join(', ') || 'Unassigned'
1091
1224
  const requester = state.assignees.find((a) => a.id === task.requestedById)?.name ?? null
1092
1225
  const bucket = state.buckets.find((b) => b.id === task.bucketId)?.title ?? '—'
1093
1226
  const board = state.boards.find((b) => b.id === task.boardId)?.name ?? '—'
@@ -1215,16 +1348,16 @@ program
1215
1348
  id: a.id,
1216
1349
  name: a.name,
1217
1350
  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,
1351
+ openCount: state.tasks.filter((t) => taskHasAssignee(t, a.id) && !t.completed).length,
1352
+ doneCount: state.tasks.filter((t) => taskHasAssignee(t, a.id) && t.completed).length,
1220
1353
  }))
1221
1354
  console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
1222
1355
  return
1223
1356
  }
1224
1357
  console.log()
1225
1358
  state.assignees.forEach((a) => {
1226
- const open = state.tasks.filter((t) => t.assigneeId === a.id && !t.completed).length
1227
- const done = state.tasks.filter((t) => t.assigneeId === a.id && t.completed).length
1359
+ const open = state.tasks.filter((t) => taskHasAssignee(t, a.id) && !t.completed).length
1360
+ const done = state.tasks.filter((t) => taskHasAssignee(t, a.id) && t.completed).length
1228
1361
  console.log(` ${chalk.bold(a.name)} ${chalk.gray(`${a.type ?? 'human'} · ${open} open · ${done} done`)}`)
1229
1362
  })
1230
1363
  console.log()
@@ -1418,6 +1551,85 @@ program
1418
1551
  } catch (err) { handleError(err) }
1419
1552
  })
1420
1553
 
1554
+ program
1555
+ .command('repos')
1556
+ .description('List linked GitHub repositories')
1557
+ .action(async () => {
1558
+ try {
1559
+ const list = await api('/api/github-repos').catch(() => [])
1560
+ const arr = Array.isArray(list) ? list : []
1561
+ if (isMachineOutput()) {
1562
+ console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
1563
+ } else if (arr.length === 0) {
1564
+ console.log(chalk.gray('\nNo linked repositories. Add one: wadah repo add https://github.com/owner/repo\n'))
1565
+ } else {
1566
+ console.log()
1567
+ arr.forEach((r) => {
1568
+ const full = r.fullName ?? `${r.owner}/${r.name}`
1569
+ console.log(` ${chalk.bold(full)} ${chalk.gray(`id: ${r.id?.slice(0, 8)}`)}`)
1570
+ console.log(chalk.gray(` ${r.url}`))
1571
+ })
1572
+ console.log()
1573
+ }
1574
+ } catch (err) { handleError(err) }
1575
+ })
1576
+
1577
+ const repoCmd = program.command('repo').description('Link and manage GitHub repositories')
1578
+
1579
+ repoCmd
1580
+ .command('add <repo>')
1581
+ .description('Link a GitHub repo (URL or owner/repo)')
1582
+ .action(async (repoInput) => {
1583
+ try {
1584
+ const parsed = parseGitHubRepoInput(repoInput)
1585
+ if (!parsed) return fail('Provide a valid GitHub repo URL or owner/repo')
1586
+ const linked = await api('/api/github-repos', {
1587
+ method: 'POST',
1588
+ body: JSON.stringify({ url: parsed.url }),
1589
+ })
1590
+ if (isMachineOutput()) {
1591
+ console.log(JSON.stringify(linked, null, runtimeJsonOutput ? 2 : 0))
1592
+ } else {
1593
+ const full = linked.fullName ?? `${linked.owner}/${linked.name}`
1594
+ console.log(chalk.green('\n✓ Repo linked') + ` ${chalk.bold(full)}`)
1595
+ console.log(chalk.gray(` ${linked.url}\n`))
1596
+ }
1597
+ } catch (err) { handleError(err) }
1598
+ })
1599
+
1600
+ repoCmd
1601
+ .command('list')
1602
+ .description('List linked GitHub repositories')
1603
+ .action(async () => {
1604
+ try {
1605
+ const list = await api('/api/github-repos').catch(() => [])
1606
+ const arr = Array.isArray(list) ? list : []
1607
+ if (isMachineOutput()) {
1608
+ console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
1609
+ } else if (arr.length === 0) {
1610
+ console.log(chalk.gray('\nNo linked repositories. Add one: wadah repo add https://github.com/owner/repo\n'))
1611
+ } else {
1612
+ console.log()
1613
+ arr.forEach((r) => {
1614
+ const full = r.fullName ?? `${r.owner}/${r.name}`
1615
+ console.log(` ${chalk.bold(full)} ${chalk.gray(`id: ${r.id?.slice(0, 8)}`)}`)
1616
+ console.log(chalk.gray(` ${r.url}`))
1617
+ })
1618
+ console.log()
1619
+ }
1620
+ } catch (err) { handleError(err) }
1621
+ })
1622
+
1623
+ repoCmd
1624
+ .command('remove <id>')
1625
+ .description('Unlink a repository by id (use: wadah repos)')
1626
+ .action(async (id) => {
1627
+ try {
1628
+ await api(`/api/github-repos/${encodeURIComponent(id)}`, { method: 'DELETE' })
1629
+ if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Repo unlinked\n'))
1630
+ } catch (err) { handleError(err) }
1631
+ })
1632
+
1421
1633
  // ── agent tokens ──────────────────────────────────────────────────────────────
1422
1634
 
1423
1635
  program
@@ -1434,8 +1646,12 @@ program
1434
1646
  } else {
1435
1647
  console.log()
1436
1648
  arr.forEach((t) => {
1437
- const as = t.assigneeName ? chalk.gray(` · acts as ${t.assigneeName}`) : ''
1438
- console.log(` ${chalk.bold(t.name)} ${chalk.gray(`id: ${t.id?.slice(0, 8)}${as}`)}`)
1649
+ const as = t.assigneeName ? ` · ${chalk.cyan(t.assigneeName)}` : ''
1650
+ const creator = t.createdBy ? chalk.gray(` · created by ${t.createdBy}`) : ''
1651
+ const used = t.last_used_at ? chalk.gray(` · last used ${new Date(t.last_used_at).toLocaleDateString()}`) : chalk.gray(' · never used')
1652
+ const usage = t.usageLast30d > 0 ? chalk.gray(` · ${t.usageLast30d} call${t.usageLast30d === 1 ? '' : 's'} (30d)`) : ''
1653
+ console.log(` ${chalk.bold(t.name)}${as}`)
1654
+ console.log(chalk.gray(` id: ${t.id?.slice(0, 8)}${used}${usage}${creator}`))
1439
1655
  })
1440
1656
  console.log()
1441
1657
  }
@@ -1461,9 +1677,12 @@ agentTokenCmd
1461
1677
  } else {
1462
1678
  console.log(chalk.green('\n✓ Agent token created'))
1463
1679
  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 (save it — shown once):'))
1680
+ console.log(chalk.yellow('\n Token (store it now — shown once):'))
1465
1681
  console.log(` ${result.token}`)
1466
- console.log(chalk.gray('\n Use: export TASK_MANAGER_TOKEN="<token>"\n'))
1682
+ console.log(chalk.gray('\n Store it so you don’t lose it:'))
1683
+ console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret → TASK_MANAGER_TOKEN'))
1684
+ console.log(chalk.gray(' · Cursor: Settings → Environment variables → TASK_MANAGER_TOKEN'))
1685
+ console.log(chalk.gray(' · Shell: export TASK_MANAGER_TOKEN="<paste token here>"\n'))
1467
1686
  }
1468
1687
  } catch (err) { handleError(err) }
1469
1688
  })
@@ -1478,6 +1697,32 @@ agentTokenCmd
1478
1697
  } catch (err) { handleError(err) }
1479
1698
  })
1480
1699
 
1700
+ agentTokenCmd
1701
+ .command('events <id>')
1702
+ .description('Show audit events for an agent token (created / used / deleted)')
1703
+ .action(async (id) => {
1704
+ try {
1705
+ const events = await api(`/api/agent-tokens/${id}/events`)
1706
+ if (isMachineOutput()) {
1707
+ console.log(JSON.stringify(events, null, runtimeJsonOutput ? 2 : 0))
1708
+ return
1709
+ }
1710
+ if (!events?.length) {
1711
+ console.log(chalk.gray('\nNo events recorded for this token.\n'))
1712
+ return
1713
+ }
1714
+ console.log()
1715
+ events.forEach((e) => {
1716
+ const when = new Date(e.created_at).toLocaleString()
1717
+ const ep = e.endpoint ? chalk.gray(` ${e.endpoint}`) : ''
1718
+ const ip = e.ip_address ? chalk.gray(` from ${e.ip_address}`) : ''
1719
+ const mark = e.event_type === 'created' ? chalk.green('●') : e.event_type === 'deleted' ? chalk.red('●') : chalk.blue('●')
1720
+ console.log(` ${mark} ${chalk.bold(e.event_type.padEnd(8))} ${chalk.gray(when)}${ep}${ip}`)
1721
+ })
1722
+ console.log()
1723
+ } catch (err) { handleError(err) }
1724
+ })
1725
+
1481
1726
  // ── calendar events ──────────────────────────────────────────────────────────
1482
1727
 
1483
1728
  const calendarCmd = program
@@ -1766,17 +2011,23 @@ program
1766
2011
  if (!hasEnvToken && !hasSavedToken) {
1767
2012
  addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
1768
2013
  } else if (hasEnvToken) {
1769
- addCheck('auth_token', 'pass', 'Using token from env/flag')
2014
+ addCheck('auth_token', 'pass', 'Using agent token from env/flag (TASK_MANAGER_TOKEN)')
1770
2015
  } else {
1771
2016
  addCheck('auth_token', 'pass', 'Using token from profile config')
1772
2017
  }
1773
2018
 
1774
2019
  if (pingOk) {
2020
+ let meBody = null
1775
2021
  try {
1776
2022
  const meRes = await fetch(`${getApiBase()}/api/auth/me`, { headers: authHeaders() })
1777
2023
  if (meRes.ok) {
1778
- const meBody = await meRes.json().catch(() => ({}))
1779
- addCheck('auth_me', 'pass', `Authenticated as ${meBody.user?.email ?? 'unknown user'}`)
2024
+ meBody = await meRes.json().catch(() => ({}))
2025
+ const isAgent = meBody.workspaces?.[0]?.role === 'agent'
2026
+ if (isAgent) {
2027
+ addCheck('auth_me', 'pass', `Authenticated as agent (workspace: ${meBody.workspaces?.[0]?.name ?? '?'})`)
2028
+ } else {
2029
+ addCheck('auth_me', 'pass', `Authenticated as ${meBody.user?.email ?? 'unknown user'}`)
2030
+ }
1780
2031
  } else if (meRes.status === 401) {
1781
2032
  addCheck('auth_me', 'warn', 'Token is missing/expired. Run wadah login.')
1782
2033
  } else {
@@ -1786,6 +2037,41 @@ program
1786
2037
  addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
1787
2038
  }
1788
2039
 
2040
+ // Agent-specific checks (only when TASK_MANAGER_TOKEN is set)
2041
+ if (hasEnvToken && meBody) {
2042
+ const isAgent = meBody.workspaces?.[0]?.role === 'agent'
2043
+ if (!isAgent) {
2044
+ addCheck('agent_identity', 'warn', 'Token is not an agent token — it authenticated as a human user')
2045
+ } else {
2046
+ addCheck('agent_identity', 'pass', `Token resolves to agent role`)
2047
+
2048
+ if (meBody.assigneeId) {
2049
+ addCheck('agent_assignee', 'pass', `Assignee: ${meBody.assigneeName ?? meBody.assigneeId}`)
2050
+ } else {
2051
+ 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.')
2052
+ }
2053
+
2054
+ const defaultBoard = confGet('default_board')
2055
+ if (!defaultBoard) {
2056
+ addCheck('agent_default_board', 'warn', 'No default board set — wadah add will guess. Fix: wadah config --default-board "<name>"')
2057
+ } else {
2058
+ // Verify board exists
2059
+ try {
2060
+ const state = await getState()
2061
+ const board = state.boards.find((b) => b.name.toLowerCase().includes(defaultBoard.toLowerCase()))
2062
+ if (board) {
2063
+ const buckets = state.buckets.filter((b) => b.boardId === board.id)
2064
+ addCheck('agent_default_board', 'pass', `Default board: "${board.name}" (${buckets.length} bucket${buckets.length === 1 ? '' : 's'})`)
2065
+ } else {
2066
+ addCheck('agent_default_board', 'fail', `Default board "${defaultBoard}" not found. Update: wadah config --default-board "<name>"`)
2067
+ }
2068
+ } catch {
2069
+ addCheck('agent_default_board', 'warn', `Could not verify default board "${defaultBoard}"`)
2070
+ }
2071
+ }
2072
+ }
2073
+ }
2074
+
1789
2075
  try {
1790
2076
  const deviceRes = await fetch(`${getApiBase()}/api/auth/device/start`, {
1791
2077
  method: 'POST',
@@ -1861,17 +2147,25 @@ program
1861
2147
  .command('config')
1862
2148
  .description('Show or set CLI configuration')
1863
2149
  .option('--api-url <url>', 'Set API base URL')
2150
+ .option('--default-board <name>', 'Set default board for wadah add (agents: avoids landing tasks on wrong board)')
1864
2151
  .action((opts) => {
1865
2152
  if (opts.apiUrl) {
1866
2153
  confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
1867
2154
  if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
1868
2155
  return
1869
2156
  }
2157
+ if (opts.defaultBoard) {
2158
+ confSet('default_board', opts.defaultBoard.trim())
2159
+ 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`))
2160
+ return
2161
+ }
2162
+ const defaultBoard = confGet('default_board')
1870
2163
  const config = {
1871
2164
  api_url: getApiBase(),
1872
2165
  signed_in_as: confGet('user_email') ?? null,
1873
2166
  agent_mode: Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken),
1874
2167
  profile: getProfile(),
2168
+ default_board: defaultBoard ?? null,
1875
2169
  config_file: conf.path,
1876
2170
  }
1877
2171
  if (isMachineOutput()) {
@@ -1880,23 +2174,378 @@ program
1880
2174
  }
1881
2175
  console.log()
1882
2176
  console.log(chalk.bold('CLI config'))
1883
- console.log(chalk.gray(` API URL: ${config.api_url}`))
1884
- console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
1885
- console.log(chalk.gray(` Agent mode: ${config.agent_mode ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
1886
- console.log(chalk.gray(` Profile: ${getProfile()}`))
1887
- console.log(chalk.gray(` Config file: ${config.config_file}`))
2177
+ console.log(chalk.gray(` API URL: ${config.api_url}`))
2178
+ console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
2179
+ console.log(chalk.gray(` Agent mode: ${config.agent_mode ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
2180
+ console.log(chalk.gray(` Default board: ${defaultBoard ?? '— (not set, use: wadah config --default-board "Name")'}`))
2181
+ console.log(chalk.gray(` Profile: ${getProfile()}`))
2182
+ console.log(chalk.gray(` Config file: ${config.config_file}`))
1888
2183
  console.log()
1889
2184
  })
1890
2185
 
2186
+ // ── wadah board-view ──────────────────────────────────────────────────────────
2187
+
2188
+ program
2189
+ .command('board-view')
2190
+ .description('Show a structured view of a board grouped by bucket — lets agents verify task placement')
2191
+ .option('--board <name>', 'Board name (defaults to current/default board)')
2192
+ .option('--assignee <name>', 'Filter to tasks for a specific assignee')
2193
+ .action(async (opts) => {
2194
+ try {
2195
+ const state = await getState()
2196
+ const defaultBoardName = opts.board ?? confGet('default_board')
2197
+ const board = defaultBoardName
2198
+ ? state.boards.find((b) => b.name.toLowerCase().includes(defaultBoardName.toLowerCase()))
2199
+ : state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
2200
+
2201
+ if (!board) return fail(opts.board ? `Board not found: ${opts.board}` : 'No boards found')
2202
+
2203
+ const buckets = state.buckets
2204
+ .filter((b) => b.boardId === board.id)
2205
+ .sort((a, b) => a.order - b.order)
2206
+
2207
+ let tasks = state.tasks.filter((t) => t.boardId === board.id && !t.completed)
2208
+ if (opts.assignee) {
2209
+ const a = findAssignee(state.assignees, opts.assignee)
2210
+ if (a) tasks = tasks.filter((t) => taskHasAssignee(t, a.id))
2211
+ }
2212
+
2213
+ const view = {
2214
+ board: { id: board.id, name: board.name },
2215
+ buckets: buckets.map((bucket) => ({
2216
+ id: bucket.id,
2217
+ name: bucket.title,
2218
+ tasks: tasks
2219
+ .filter((t) => t.bucketId === bucket.id)
2220
+ .sort((a, b) => a.order - b.order)
2221
+ .map((t) => ({
2222
+ id: t.id,
2223
+ title: t.title,
2224
+ assignee: taskAssigneeIds(t).map((aid) => state.assignees.find((a) => a.id === aid)?.name ?? aid).join(', ') || null,
2225
+ priority: t.priority ?? null,
2226
+ })),
2227
+ })),
2228
+ }
2229
+
2230
+ if (isMachineOutput()) {
2231
+ console.log(JSON.stringify(view, null, runtimeJsonOutput ? 2 : 0))
2232
+ return
2233
+ }
2234
+
2235
+ console.log()
2236
+ console.log(chalk.bold(`Board: ${board.name}`))
2237
+ for (const bucket of view.buckets) {
2238
+ console.log(chalk.gray(`\n ${bucket.name} (${bucket.tasks.length})`))
2239
+ if (bucket.tasks.length === 0) {
2240
+ console.log(chalk.gray(' — empty'))
2241
+ } else {
2242
+ bucket.tasks.forEach((t) => {
2243
+ const who = t.assignee ? chalk.gray(` @${t.assignee}`) : ''
2244
+ console.log(` ${t.id.slice(0, 8)} ${t.title.slice(0, 60)}${who}`)
2245
+ })
2246
+ }
2247
+ }
2248
+ console.log()
2249
+ } catch (err) { handleError(err) }
2250
+ })
2251
+
1891
2252
  // ── shell completion ──────────────────────────────────────────────────────────
1892
2253
 
1893
2254
  const CLI_COMMANDS = [
1894
- 'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'boards',
2255
+ 'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
1895
2256
  'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
1896
2257
  'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
1897
- 'move', 'open', 'reopen', 'requested', 'search', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
2258
+ 'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
1898
2259
  ]
1899
2260
 
2261
+ // ── wadah setup-agent ─────────────────────────────────────────────────────────
2262
+
2263
+ program
2264
+ .command('setup-agent')
2265
+ .description('Interactive wizard: create an agent token, write it to your shell profile, and verify everything works')
2266
+ .option('--name <name>', 'Agent name (skip prompt)')
2267
+ .option('--board <board>', 'Default board name (skip prompt)')
2268
+ .option('--skip-env', 'Skip writing TASK_MANAGER_TOKEN to shell profile (print instructions only)')
2269
+ .action(async (opts) => {
2270
+ const readline = await import('node:readline/promises')
2271
+ const os = await import('node:os')
2272
+ const fs = await import('node:fs')
2273
+ const path = await import('node:path')
2274
+
2275
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
2276
+ const ask = (q) => rl.question(q)
2277
+
2278
+ console.log()
2279
+ console.log(chalk.bold('Open Wadah — Agent Setup Wizard'))
2280
+ console.log(chalk.gray(' This wizard will create an agent token and configure your environment.\n'))
2281
+
2282
+ // Step 1: verify auth
2283
+ console.log(chalk.bold('Step 1/5 Checking authentication…'))
2284
+ let me
2285
+ try {
2286
+ me = await api('/api/auth/me')
2287
+ if (me.workspaces?.[0]?.role === 'agent') {
2288
+ console.log(chalk.yellow(' ⚠ You are currently authenticated as an agent, not a human user.'))
2289
+ console.log(chalk.gray(' Run: wadah login then rerun: wadah setup-agent\n'))
2290
+ rl.close(); return
2291
+ }
2292
+ console.log(chalk.green(` ✓ Signed in as ${me.user?.email ?? 'unknown'}`))
2293
+ } catch {
2294
+ console.log(chalk.red(' ✗ Not authenticated. Run: wadah login'))
2295
+ console.log(chalk.gray(' Then rerun: wadah setup-agent\n'))
2296
+ rl.close(); return
2297
+ }
2298
+
2299
+ // Step 2: agent name
2300
+ console.log()
2301
+ console.log(chalk.bold('Step 2/5 Name your agent'))
2302
+ let agentName = opts.name
2303
+ if (!agentName) {
2304
+ agentName = (await ask(chalk.gray(' What should your agent be called? (e.g. "Cursor", "Claude") > '))).trim()
2305
+ } else {
2306
+ console.log(chalk.gray(` Using: ${agentName}`))
2307
+ }
2308
+ if (!agentName) agentName = 'My Agent'
2309
+
2310
+ // Step 3: create token
2311
+ console.log()
2312
+ console.log(chalk.bold('Step 3/5 Creating agent token…'))
2313
+ let tokenResult
2314
+ try {
2315
+ tokenResult = await api('/api/agent-tokens', {
2316
+ method: 'POST',
2317
+ body: JSON.stringify({ name: agentName, assigneeName: agentName }),
2318
+ })
2319
+ console.log(chalk.green(` ✓ Token created (assignee: ${tokenResult.assigneeName ?? agentName})`))
2320
+ } catch (err) {
2321
+ console.log(chalk.red(` ✗ Failed to create token: ${err.message}`))
2322
+ rl.close(); return
2323
+ }
2324
+
2325
+ const rawToken = tokenResult.token
2326
+ console.log()
2327
+ console.log(chalk.yellow(' Token (shown once — copy it now):'))
2328
+ console.log(` ${chalk.bold(rawToken)}`)
2329
+
2330
+ // Step 4: write env var to shell profile
2331
+ console.log()
2332
+ console.log(chalk.bold('Step 4/5 Setting up TASK_MANAGER_TOKEN…'))
2333
+
2334
+ const shell = process.env.SHELL ?? ''
2335
+ const profileMap = { zsh: '.zshrc', bash: '.bashrc', fish: '.config/fish/config.fish' }
2336
+ const profileKey = Object.keys(profileMap).find((k) => shell.includes(k))
2337
+ const profileFile = profileKey ? path.join(os.homedir(), profileMap[profileKey]) : null
2338
+ const marker = '# added by wadah setup-agent'
2339
+ const exportLine = `\nexport TASK_MANAGER_TOKEN="${rawToken}" ${marker}\n`
2340
+
2341
+ if (opts.skipEnv || !profileFile) {
2342
+ console.log(chalk.gray(' Skipping automatic profile write. Add this line manually:'))
2343
+ console.log(chalk.gray(` export TASK_MANAGER_TOKEN="${rawToken}"`))
2344
+ const rcGuess = shell.includes('zsh') ? '~/.zshrc' : shell.includes('fish') ? '~/.config/fish/config.fish' : '~/.bashrc'
2345
+ console.log(chalk.gray(` Then reload: source ${rcGuess}`))
2346
+ } else {
2347
+ const confirm = await ask(chalk.gray(` Write to ${profileFile}? (Y/n) > `))
2348
+ if (confirm.trim().toLowerCase() !== 'n') {
2349
+ try {
2350
+ const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : ''
2351
+ const filtered = existing
2352
+ .split('\n')
2353
+ .filter((line) => !line.includes('TASK_MANAGER_TOKEN=') && !line.includes(marker))
2354
+ .join('\n')
2355
+ const next = `${filtered.trimEnd()}${exportLine}`
2356
+ fs.writeFileSync(profileFile, `${next.endsWith('\n') ? next : `${next}\n`}`)
2357
+ console.log(chalk.green(` ✓ Written to ${profileFile}`))
2358
+ console.log(chalk.gray(` Reload now: source ${profileFile}`))
2359
+ } catch (e) {
2360
+ console.log(chalk.yellow(` ⚠ Could not write to ${profileFile}: ${e.message}`))
2361
+ console.log(chalk.gray(` Add manually: export TASK_MANAGER_TOKEN="${rawToken}"`))
2362
+ }
2363
+ } else {
2364
+ console.log(chalk.gray(` Skipped. Add manually: export TASK_MANAGER_TOKEN="${rawToken}"`))
2365
+ }
2366
+ }
2367
+
2368
+ // Step 4.5: verify the created token works (without requiring shell reload)
2369
+ console.log()
2370
+ console.log(chalk.bold('Step 4.5/5 Verifying token…'))
2371
+ try {
2372
+ const verifyRes = await fetch(`${getApiBase()}/api/auth/me`, {
2373
+ headers: { Authorization: `Bearer ${rawToken}`, 'Content-Type': 'application/json' },
2374
+ })
2375
+ if (!verifyRes.ok) {
2376
+ console.log(chalk.yellow(` ⚠ Token verification returned ${verifyRes.status}. Continue and run: wadah doctor`))
2377
+ } else {
2378
+ const verifyBody = await verifyRes.json().catch(() => ({}))
2379
+ const role = verifyBody?.workspaces?.[0]?.role ?? '?'
2380
+ const actsAs = verifyBody?.assigneeName ?? tokenResult.assigneeName ?? agentName
2381
+ console.log(chalk.green(` ✓ Token valid (role: ${role}, assignee: ${actsAs})`))
2382
+ }
2383
+ } catch (e) {
2384
+ console.log(chalk.yellow(` ⚠ Could not verify token now: ${e.message}`))
2385
+ }
2386
+
2387
+ // Step 5: default board
2388
+ console.log()
2389
+ console.log(chalk.bold('Step 5/5 Default board'))
2390
+ let boardName = opts.board
2391
+ if (!boardName) {
2392
+ try {
2393
+ const state = await getState()
2394
+ if (state.boards?.length > 0) {
2395
+ console.log(chalk.gray(' Available boards:'))
2396
+ state.boards.forEach((b, i) => console.log(chalk.gray(` ${i + 1}. ${b.name}`)))
2397
+ const boardInput = (await ask(chalk.gray(' Which board should new tasks land on by default? (name or number, Enter to skip) > '))).trim()
2398
+ if (boardInput) {
2399
+ const byNum = parseInt(boardInput, 10)
2400
+ const picked = !isNaN(byNum) ? state.boards[byNum - 1] : state.boards.find((b) => b.name.toLowerCase().includes(boardInput.toLowerCase()))
2401
+ if (picked) {
2402
+ confSet('default_board', picked.name)
2403
+ boardName = picked.name
2404
+ console.log(chalk.green(` ✓ Default board: "${picked.name}"`))
2405
+ } else {
2406
+ console.log(chalk.yellow(` ⚠ Board not found — skipping. Set later: wadah config --default-board "<name>"`))
2407
+ }
2408
+ } else {
2409
+ console.log(chalk.gray(' Skipped. Set later: wadah config --default-board "<name>"'))
2410
+ }
2411
+ }
2412
+ } catch { /* board step is optional */ }
2413
+ } else {
2414
+ confSet('default_board', boardName)
2415
+ console.log(chalk.green(` ✓ Default board: "${boardName}"`))
2416
+ }
2417
+
2418
+ rl.close()
2419
+
2420
+ console.log()
2421
+ console.log(chalk.bold.green('✓ Setup complete!'))
2422
+ console.log()
2423
+ console.log(chalk.gray(' Next steps:'))
2424
+ console.log(chalk.gray(` 1. Reload your shell profile (or open a new terminal)`))
2425
+ console.log(chalk.gray(` 2. Verify everything works: wadah doctor`))
2426
+ console.log(chalk.gray(` 3. Create your first task: wadah add "My first task"`))
2427
+ console.log()
2428
+ })
2429
+
2430
+ // ── agent memory ─────────────────────────────────────────────────────────────
2431
+
2432
+ program
2433
+ .command('memory')
2434
+ .description('Show all memory for this agent, grouped by category')
2435
+ .option('--category <cat>', 'Filter to a specific category')
2436
+ .option('--log', 'Show memory change history instead of current values')
2437
+ .option('--export', 'Export all memory as a JSON blob (for backup or handoff)')
2438
+ .action(async (opts) => {
2439
+ try {
2440
+ if (opts.export) {
2441
+ const data = await api('/api/agent-memory/export')
2442
+ console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
2443
+ return
2444
+ }
2445
+ if (opts.log) {
2446
+ const log = await api('/api/agent-memory/log')
2447
+ if (isMachineOutput()) { console.log(JSON.stringify(log, null, runtimeJsonOutput ? 2 : 0)); return }
2448
+ if (!log?.length) { console.log(chalk.gray('\nNo memory changes recorded yet.\n')); return }
2449
+ console.log()
2450
+ log.forEach((e) => {
2451
+ const when = new Date(e.changed_at).toLocaleString()
2452
+ const key = e.agent_memory?.key ?? '?'
2453
+ console.log(` ${chalk.gray(when)} ${chalk.bold(key)}`)
2454
+ if (e.old_value !== null) console.log(chalk.gray(` was: ${String(e.old_value).slice(0, 80)}`))
2455
+ console.log(chalk.gray(` now: ${String(e.new_value).slice(0, 80)}`))
2456
+ })
2457
+ console.log()
2458
+ return
2459
+ }
2460
+
2461
+ const url = opts.category ? `/api/agent-memory?category=${encodeURIComponent(opts.category)}` : '/api/agent-memory'
2462
+ const entries = await api(url)
2463
+ if (isMachineOutput()) { console.log(JSON.stringify(entries, null, runtimeJsonOutput ? 2 : 0)); return }
2464
+ if (!entries?.length) { console.log(chalk.gray('\nNo memory stored yet. Use: wadah remember <key> <value>\n')); return }
2465
+
2466
+ console.log()
2467
+ const byCategory = {}
2468
+ entries.forEach((e) => {
2469
+ const cat = e.category ?? 'general'
2470
+ if (!byCategory[cat]) byCategory[cat] = []
2471
+ byCategory[cat].push(e)
2472
+ })
2473
+ for (const [cat, items] of Object.entries(byCategory)) {
2474
+ console.log(chalk.bold(`${cat.charAt(0).toUpperCase() + cat.slice(1)}`))
2475
+ items.forEach((e) => {
2476
+ const conf = e.confidence < 1 ? chalk.yellow(` (${Math.round(e.confidence * 100)}% confidence)`) : ''
2477
+ const val = String(e.value).slice(0, 80)
2478
+ console.log(` ${e.key.padEnd(28)} ${val}${conf}`)
2479
+ })
2480
+ console.log()
2481
+ }
2482
+ } catch (err) { handleError(err) }
2483
+ })
2484
+
2485
+ program
2486
+ .command('remember <key> <value>')
2487
+ .description('Write or update a memory entry for this agent')
2488
+ .option('--category <cat>', 'Category (config, preferences, workspace, session, learned)', 'general')
2489
+ .option('--confidence <0-1>', 'Confidence level (default 1.0)', '1.0')
2490
+ .option('--source <text>', 'How this was learned (e.g. user_confirmed, inferred)')
2491
+ .option('--session <id>', 'Session identifier for audit log')
2492
+ .action(async (key, value, opts) => {
2493
+ try {
2494
+ const body = {
2495
+ value,
2496
+ category: opts.category,
2497
+ confidence: parseFloat(opts.confidence || '1'),
2498
+ source: opts.source ?? null,
2499
+ sessionId: opts.session ?? null,
2500
+ }
2501
+ const result = await api(`/api/agent-memory/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(body) })
2502
+ if (isMachineOutput()) { console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0)); return }
2503
+ console.log(chalk.green(`\n✓ Remembered ${chalk.bold(key)} = ${value}\n`))
2504
+ } catch (err) { handleError(err) }
2505
+ })
2506
+
2507
+ program
2508
+ .command('forget <key>')
2509
+ .description('Delete a memory entry')
2510
+ .action(async (key) => {
2511
+ try {
2512
+ await api(`/api/agent-memory/${encodeURIComponent(key)}`, { method: 'DELETE' })
2513
+ if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ Forgotten ${key}\n`))
2514
+ } catch (err) { handleError(err) }
2515
+ })
2516
+
2517
+ program
2518
+ .command('memory-log')
2519
+ .description('Alias for: wadah memory --log')
2520
+ .action(async () => {
2521
+ try {
2522
+ const log = await api('/api/agent-memory/log')
2523
+ if (isMachineOutput()) { console.log(JSON.stringify(log, null, runtimeJsonOutput ? 2 : 0)); return }
2524
+ if (!log?.length) { console.log(chalk.gray('\nNo memory changes recorded yet.\n')); return }
2525
+ console.log()
2526
+ log.forEach((e) => {
2527
+ const when = new Date(e.changed_at).toLocaleString()
2528
+ const key = e.agent_memory?.key ?? '?'
2529
+ console.log(` ${chalk.gray(when)} ${chalk.bold(key)}`)
2530
+ if (e.old_value !== null) console.log(chalk.gray(` was: ${String(e.old_value).slice(0, 80)}`))
2531
+ console.log(chalk.gray(` now: ${String(e.new_value).slice(0, 80)}`))
2532
+ })
2533
+ console.log()
2534
+ } catch (err) { handleError(err) }
2535
+ })
2536
+
2537
+ program
2538
+ .command('memory-export')
2539
+ .description('Alias for: wadah memory --export')
2540
+ .action(async () => {
2541
+ try {
2542
+ const data = await api('/api/agent-memory/export')
2543
+ console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
2544
+ } catch (err) { handleError(err) }
2545
+ })
2546
+
2547
+ // ── shell completion ──────────────────────────────────────────────────────────
2548
+
1900
2549
  program
1901
2550
  .command('completion [shell]')
1902
2551
  .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.1",
3
+ "version": "1.2.2",
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.0",
16
+ "@inquirer/prompts": "^8.3.2",
17
17
  "chalk": "^5.4.1",
18
- "commander": "^13.1.0",
19
- "conf": "^13.1.0",
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
  }