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.
- package/README.md +1 -1
- package/cli.js +686 -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
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
855
|
-
|
|
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
|
|
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('
|
|
941
|
-
.requiredOption('--to <
|
|
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
|
|
946
|
-
if (!
|
|
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
|
|
1059
|
+
const task = await api(`/api/tasks/${id}`, {
|
|
950
1060
|
method: 'PATCH',
|
|
951
|
-
body:
|
|
1061
|
+
body: JSON.stringify({
|
|
1062
|
+
assigneeIds: nextIds,
|
|
1063
|
+
assigneeId: nextIds[0] ?? null,
|
|
1064
|
+
requestedById: myId,
|
|
1065
|
+
}),
|
|
952
1066
|
})
|
|
953
|
-
|
|
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
|
|
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
|
|
1219
|
-
doneCount: state.tasks.filter((t) => t
|
|
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
|
|
1227
|
-
const done = state.tasks.filter((t) => t
|
|
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
|
|
1438
|
-
|
|
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 (
|
|
1680
|
+
console.log(chalk.yellow('\n Token (store it now — shown once):'))
|
|
1465
1681
|
console.log(` ${result.token}`)
|
|
1466
|
-
console.log(chalk.gray('\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
|
-
|
|
1779
|
-
|
|
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:
|
|
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(`
|
|
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.
|
|
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.
|
|
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
|
}
|