open-wadah 1.0.0

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 (2) hide show
  1. package/cli.js +579 -0
  2. package/package.json +20 -0
package/cli.js ADDED
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander'
3
+ import chalk from 'chalk'
4
+ import fetch from 'node-fetch'
5
+ import Conf from 'conf'
6
+ import { input, password as promptPassword } from '@inquirer/prompts'
7
+ import { webcrypto } from 'node:crypto'
8
+
9
+ const randomUUID = () => webcrypto.randomUUID()
10
+
11
+ // ── config ────────────────────────────────────────────────────────────────────
12
+
13
+ const conf = new Conf({ projectName: 'open-wadah' })
14
+
15
+ function getApiBase() {
16
+ return (
17
+ (process.env.TASK_MANAGER_API_URL ?? '').trim() ||
18
+ conf.get('api_url') ||
19
+ 'http://localhost:3001'
20
+ )
21
+ }
22
+
23
+ function getAuthToken() {
24
+ if (process.env.TASK_MANAGER_TOKEN) return process.env.TASK_MANAGER_TOKEN
25
+ return conf.get('access_token') ?? null
26
+ }
27
+
28
+ function authHeaders() {
29
+ const token = getAuthToken()
30
+ return {
31
+ 'Content-Type': 'application/json',
32
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
33
+ }
34
+ }
35
+
36
+ // ── token refresh ─────────────────────────────────────────────────────────────
37
+
38
+ async function refreshAccessToken() {
39
+ const refreshToken = conf.get('refresh_token')
40
+ if (!refreshToken) return false
41
+ try {
42
+ const res = await fetch(`${getApiBase()}/api/auth/refresh`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ refresh_token: refreshToken }),
46
+ })
47
+ if (!res.ok) return false
48
+ const data = await res.json()
49
+ conf.set('access_token', data.access_token)
50
+ conf.set('refresh_token', data.refresh_token)
51
+ conf.set('token_expires', data.expires_at)
52
+ return true
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ async function ensureAuth() {
59
+ if (process.env.TASK_MANAGER_TOKEN) return // agent token, no refresh needed
60
+ const expires = conf.get('token_expires')
61
+ // Refresh if within 5 minutes of expiry or already expired
62
+ if (expires && Date.now() > (expires * 1000) - 300_000) {
63
+ await refreshAccessToken()
64
+ }
65
+ }
66
+
67
+ // ── http ──────────────────────────────────────────────────────────────────────
68
+
69
+ async function api(path, options = {}, retry = true) {
70
+ await ensureAuth()
71
+ const url = `${getApiBase()}${path}`
72
+ const res = await fetch(url, { headers: authHeaders(), ...options })
73
+
74
+ // If 401 and we have a refresh token, try once more
75
+ if (res.status === 401 && retry && !process.env.TASK_MANAGER_TOKEN) {
76
+ const refreshed = await refreshAccessToken()
77
+ if (refreshed) return api(path, options, false)
78
+ }
79
+
80
+ const body = await res.json().catch(() => ({}))
81
+ if (!res.ok) throw new Error(body.error ?? body.message ?? res.statusText)
82
+ return body
83
+ }
84
+
85
+ // ── formatting ────────────────────────────────────────────────────────────────
86
+
87
+ function formatDate(ts) {
88
+ if (!ts) return '—'
89
+ const d = new Date(ts)
90
+ const today = new Date(); today.setHours(0, 0, 0, 0)
91
+ const day = new Date(d); day.setHours(0, 0, 0, 0)
92
+ const diff = Math.round((day - today) / 86400000)
93
+ if (diff === 0) return chalk.yellow('Today')
94
+ if (diff < 0) return chalk.red(`${Math.abs(diff)}d overdue`)
95
+ if (diff === 1) return chalk.green('Tomorrow')
96
+ if (diff <= 7) return chalk.green(`${diff}d`)
97
+ return d.toLocaleDateString()
98
+ }
99
+
100
+ function printTaskList(tasks, state) {
101
+ const bucketMap = Object.fromEntries(state.buckets.map((b) => [b.id, b.title]))
102
+ const assigneeMap = Object.fromEntries(state.assignees.map((a) => [a.id, a.name]))
103
+ const boardMap = Object.fromEntries(state.boards.map((b) => [b.id, b.name]))
104
+
105
+ console.log()
106
+ if (tasks.length === 0) {
107
+ console.log(chalk.gray(' No tasks.\n'))
108
+ return
109
+ }
110
+ tasks.forEach((t) => {
111
+ const bucket = bucketMap[t.bucketId] ?? '—'
112
+ const assignee = assigneeMap[t.assigneeId] ?? 'Unassigned'
113
+ const requester = t.requestedById ? assigneeMap[t.requestedById] : null
114
+ const board = boardMap[t.boardId] ?? '—'
115
+ const id = t.id.slice(0, 8)
116
+
117
+ console.log(` ${chalk.bold(t.title)}`)
118
+ console.log(chalk.gray(
119
+ ` ${id} · ${board} / ${bucket} · ${assignee}` +
120
+ (requester ? ` ← ${requester}` : '') +
121
+ (t.dueDate ? ` · ${formatDate(t.dueDate)}` : '') +
122
+ (t.tags?.length ? ` · #${t.tags.join(' #')}` : '')
123
+ ))
124
+ console.log()
125
+ })
126
+ }
127
+
128
+ function handleError(err) {
129
+ const msg = err.message ?? ''
130
+ if (
131
+ msg.includes('401') ||
132
+ msg.toLowerCase().includes('missing token') ||
133
+ msg.toLowerCase().includes('invalid token') ||
134
+ msg.toLowerCase().includes('not authenticated')
135
+ ) {
136
+ console.error(chalk.red('\n✗ Not authenticated.'))
137
+ console.error(chalk.gray(' Humans: wadah login'))
138
+ console.error(chalk.gray(' Agents: TASK_MANAGER_TOKEN=<token> wadah open\n'))
139
+ } else {
140
+ console.error(chalk.red(`\n✗ ${msg}\n`))
141
+ }
142
+ process.exit(1)
143
+ }
144
+
145
+ async function resolveMyAssigneeId(state) {
146
+ try {
147
+ const me = await api('/api/auth/me')
148
+ const userId = me.user?.id
149
+ // Match by user_id field on assignee (if backend returns it)
150
+ const byUserId = state.assignees.find((a) => a.user_id === userId)
151
+ if (byUserId) return byUserId.id
152
+ } catch {}
153
+ // Fallback: 'Me' assignee
154
+ return state.assignees.find((a) => a.name === 'Me')?.id ?? null
155
+ }
156
+
157
+ function findAssignee(assignees, name) {
158
+ const q = name.toLowerCase()
159
+ return assignees.find((a) => a.name.toLowerCase().includes(q)) ?? null
160
+ }
161
+
162
+ // ── program ───────────────────────────────────────────────────────────────────
163
+
164
+ program
165
+ .name('tm')
166
+ .description('Open Wadah CLI — shared task board for humans and agents')
167
+ .version('1.0.0')
168
+
169
+ // ── tm login ──────────────────────────────────────────────────────────────────
170
+
171
+ program
172
+ .command('login')
173
+ .description('Sign in as a human user (email + password)')
174
+ .option('--api-url <url>', 'Override API base URL and save it')
175
+ .action(async (opts) => {
176
+ if (opts.apiUrl) conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
177
+
178
+ console.log(chalk.bold('\nOpen Wadah — Sign in\n'))
179
+ const email = await input({ message: 'Email:' })
180
+ const pass = await promptPassword({ message: 'Password:' })
181
+
182
+ try {
183
+ const data = await fetch(`${getApiBase()}/api/auth/login`, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ email, password: pass }),
187
+ })
188
+ const body = await data.json()
189
+ if (!data.ok) throw new Error(body.error ?? 'Login failed')
190
+
191
+ conf.set('access_token', body.access_token)
192
+ conf.set('refresh_token', body.refresh_token)
193
+ conf.set('token_expires', body.expires_at)
194
+ conf.set('user_email', body.user.email)
195
+ conf.set('user_id', body.user.id)
196
+ console.log(chalk.green(`\n✓ Signed in as ${body.user.email}\n`))
197
+ } catch (err) {
198
+ console.error(chalk.red(`\n✗ ${err.message}\n`))
199
+ process.exit(1)
200
+ }
201
+ })
202
+
203
+ // ── tm signup ─────────────────────────────────────────────────────────────────
204
+
205
+ program
206
+ .command('signup')
207
+ .description('Create a new account')
208
+ .option('--api-url <url>', 'Override API base URL and save it')
209
+ .action(async (opts) => {
210
+ if (opts.apiUrl) conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
211
+
212
+ console.log(chalk.bold('\nOpen Wadah — Create account\n'))
213
+ const name = await input({ message: 'Name:' })
214
+ const email = await input({ message: 'Email:' })
215
+ const pass = await promptPassword({ message: 'Password (min 6 chars):' })
216
+
217
+ try {
218
+ // Sign up
219
+ const signupRes = await fetch(`${getApiBase()}/api/auth/signup`, {
220
+ method: 'POST',
221
+ headers: { 'Content-Type': 'application/json' },
222
+ body: JSON.stringify({ name, email, password: pass }),
223
+ })
224
+ const signupBody = await signupRes.json()
225
+ if (!signupRes.ok) throw new Error(signupBody.error ?? 'Signup failed')
226
+
227
+ // Auto login
228
+ const loginRes = await fetch(`${getApiBase()}/api/auth/login`, {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ email, password: pass }),
232
+ })
233
+ const loginBody = await loginRes.json()
234
+ if (!loginRes.ok) throw new Error(loginBody.error ?? 'Login after signup failed')
235
+
236
+ conf.set('access_token', loginBody.access_token)
237
+ conf.set('refresh_token', loginBody.refresh_token)
238
+ conf.set('token_expires', loginBody.expires_at)
239
+ conf.set('user_email', loginBody.user.email)
240
+ conf.set('user_id', loginBody.user.id)
241
+ console.log(chalk.green(`\n✓ Account created and signed in as ${email}\n`))
242
+ } catch (err) {
243
+ console.error(chalk.red(`\n✗ ${err.message}\n`))
244
+ process.exit(1)
245
+ }
246
+ })
247
+
248
+ // ── tm logout ─────────────────────────────────────────────────────────────────
249
+
250
+ program
251
+ .command('logout')
252
+ .description('Sign out and clear stored credentials')
253
+ .action(() => {
254
+ conf.delete('access_token')
255
+ conf.delete('refresh_token')
256
+ conf.delete('token_expires')
257
+ conf.delete('user_email')
258
+ conf.delete('user_id')
259
+ console.log(chalk.green('✓ Signed out\n'))
260
+ })
261
+
262
+ // ── tm whoami ─────────────────────────────────────────────────────────────────
263
+
264
+ program
265
+ .command('whoami')
266
+ .description('Show current identity and workspace')
267
+ .action(async () => {
268
+ if (process.env.TASK_MANAGER_TOKEN) {
269
+ console.log(chalk.bold('\nAgent') + ' (TASK_MANAGER_TOKEN)')
270
+ } else {
271
+ const email = conf.get('user_email')
272
+ if (!email) {
273
+ console.log(chalk.gray('\nNot signed in. Run: tm login\n'))
274
+ return
275
+ }
276
+ try {
277
+ const me = await api('/api/auth/me')
278
+ const ws = me.workspaces?.[0]
279
+ console.log()
280
+ console.log(chalk.bold(me.user.email))
281
+ if (ws) console.log(chalk.gray(`Workspace: ${ws.name} (${ws.role})`))
282
+ console.log(chalk.gray(`API: ${getApiBase()}`))
283
+ console.log()
284
+ } catch {
285
+ console.log(chalk.gray(`\n${email} (token may be expired — run: tm login)\n`))
286
+ }
287
+ }
288
+ })
289
+
290
+ // ── tm open ───────────────────────────────────────────────────────────────────
291
+
292
+ program
293
+ .command('open')
294
+ .description('List open tasks assigned to me')
295
+ .option('--all', 'Show all tasks in the workspace')
296
+ .option('--assignee <name>', 'Filter by assignee name')
297
+ .option('--requested-by <name>', 'Filter by requester name')
298
+ .option('--board <name>', 'Filter by board name')
299
+ .option('--completed', 'Show completed tasks instead')
300
+ .action(async (opts) => {
301
+ try {
302
+ const state = await api('/api/state')
303
+ let tasks = state.tasks.filter((t) => !!t.completed === !!opts.completed)
304
+
305
+ if (!opts.all && !opts.assignee) {
306
+ const myId = await resolveMyAssigneeId(state)
307
+ if (myId) tasks = tasks.filter((t) => t.assigneeId === myId)
308
+ }
309
+ if (opts.assignee) {
310
+ const a = findAssignee(state.assignees, opts.assignee)
311
+ if (!a) return console.error(chalk.red(`\n✗ Assignee not found: ${opts.assignee}\n`))
312
+ tasks = tasks.filter((t) => t.assigneeId === a.id)
313
+ }
314
+ if (opts.requestedBy) {
315
+ const a = findAssignee(state.assignees, opts.requestedBy)
316
+ if (!a) return console.error(chalk.red(`\n✗ Assignee not found: ${opts.requestedBy}\n`))
317
+ tasks = tasks.filter((t) => t.requestedById === a.id)
318
+ }
319
+ if (opts.board) {
320
+ const b = state.boards.find((b) => b.name.toLowerCase().includes(opts.board.toLowerCase()))
321
+ if (b) tasks = tasks.filter((t) => t.boardId === b.id)
322
+ }
323
+
324
+ printTaskList(tasks, state)
325
+ } catch (err) { handleError(err) }
326
+ })
327
+
328
+ // ── tm requested ──────────────────────────────────────────────────────────────
329
+
330
+ program
331
+ .command('requested')
332
+ .description('List open tasks you requested (assigned to others / agents)')
333
+ .action(async () => {
334
+ try {
335
+ const state = await api('/api/state')
336
+ const myId = await resolveMyAssigneeId(state)
337
+ const tasks = state.tasks.filter((t) => t.requestedById === myId && !t.completed)
338
+ printTaskList(tasks, state)
339
+ } catch (err) { handleError(err) }
340
+ })
341
+
342
+ // ── tm add ────────────────────────────────────────────────────────────────────
343
+
344
+ program
345
+ .command('add <title>')
346
+ .description('Create a new task')
347
+ .option('--assignee <name>', 'Assign to a person or agent')
348
+ .option('--bucket <name>', 'Column name (default: first column)')
349
+ .option('--board <name>', 'Board name (default: current board)')
350
+ .option('--due <YYYY-MM-DD>', 'Due date')
351
+ .option('--repo <owner/repo>','GitHub repo')
352
+ .option('--url <url>', 'Issue / PR URL')
353
+ .option('--notes <text>', 'Notes')
354
+ .action(async (title, opts) => {
355
+ try {
356
+ const state = await api('/api/state')
357
+ const myId = await resolveMyAssigneeId(state)
358
+
359
+ let assigneeId = myId
360
+ let requestedById = null
361
+
362
+ if (opts.assignee) {
363
+ const a = findAssignee(state.assignees, opts.assignee)
364
+ if (!a) return console.error(chalk.red(`\n✗ Assignee not found: ${opts.assignee}\n`))
365
+ assigneeId = a.id
366
+ requestedById = myId
367
+ }
368
+
369
+ const bucket = opts.bucket
370
+ ? state.buckets.find((b) => b.title.toLowerCase().includes(opts.bucket.toLowerCase()))
371
+ : state.buckets[0]
372
+
373
+ const board = opts.board
374
+ ? state.boards.find((b) => b.name.toLowerCase().includes(opts.board.toLowerCase()))
375
+ : state.boards.find((b) => b.id === state.currentBoardId) ?? state.boards[0]
376
+
377
+ const task = await api('/api/tasks', {
378
+ method: 'POST',
379
+ body: JSON.stringify({
380
+ title,
381
+ bucketId: bucket?.id,
382
+ boardId: board?.id,
383
+ assigneeId,
384
+ requestedById,
385
+ dueDate: opts.due ? new Date(opts.due).getTime() : null,
386
+ repo: opts.repo ?? null,
387
+ contextUrl: opts.url ?? null,
388
+ notes: opts.notes ?? '',
389
+ }),
390
+ })
391
+
392
+ const assigneeName = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
393
+ console.log(chalk.green('\n✓ Created') + ` ${chalk.bold(task.title)}`)
394
+ console.log(chalk.gray(` ID: ${task.id} · Assigned to: ${assigneeName}\n`))
395
+ } catch (err) { handleError(err) }
396
+ })
397
+
398
+ // ── tm complete ───────────────────────────────────────────────────────────────
399
+
400
+ program
401
+ .command('complete <id>')
402
+ .description('Mark a task as complete')
403
+ .action(async (id) => {
404
+ try {
405
+ const task = await api(`/api/tasks/${id}`, {
406
+ method: 'PATCH',
407
+ body: JSON.stringify({ completed: true }),
408
+ })
409
+ console.log(chalk.green('\n✓ Completed') + ` ${chalk.bold(task.title)}\n`)
410
+ } catch (err) { handleError(err) }
411
+ })
412
+
413
+ // ── tm reopen ─────────────────────────────────────────────────────────────────
414
+
415
+ program
416
+ .command('reopen <id>')
417
+ .description('Reopen a completed task')
418
+ .action(async (id) => {
419
+ try {
420
+ const task = await api(`/api/tasks/${id}`, {
421
+ method: 'PATCH',
422
+ body: JSON.stringify({ completed: false }),
423
+ })
424
+ console.log(chalk.green('\n✓ Reopened') + ` ${chalk.bold(task.title)}\n`)
425
+ } catch (err) { handleError(err) }
426
+ })
427
+
428
+ // ── tm assign ─────────────────────────────────────────────────────────────────
429
+
430
+ program
431
+ .command('assign <id>')
432
+ .description('Reassign a task to another person or agent')
433
+ .requiredOption('--to <name>', 'Assignee name')
434
+ .action(async (id, opts) => {
435
+ try {
436
+ const state = await api('/api/state')
437
+ const a = findAssignee(state.assignees, opts.to)
438
+ if (!a) return console.error(chalk.red(`\n✗ Assignee not found: ${opts.to}\n`))
439
+
440
+ const myId = await resolveMyAssigneeId(state)
441
+ const task = await api(`/api/tasks/${id}`, {
442
+ method: 'PATCH',
443
+ body: JSON.stringify({ assigneeId: a.id, requestedById: myId }),
444
+ })
445
+ console.log(chalk.green('\n✓ Assigned') + ` ${chalk.bold(task.title)} → ${a.name}\n`)
446
+ } catch (err) { handleError(err) }
447
+ })
448
+
449
+ // ── tm delete ─────────────────────────────────────────────────────────────────
450
+
451
+ program
452
+ .command('delete <id>')
453
+ .description('Delete a task')
454
+ .action(async (id) => {
455
+ try {
456
+ const task = await api(`/api/tasks/${id}`)
457
+ await api(`/api/tasks/${id}`, { method: 'DELETE' })
458
+ console.log(chalk.green('\n✓ Deleted') + ` ${chalk.bold(task.title)}\n`)
459
+ } catch (err) { handleError(err) }
460
+ })
461
+
462
+ // ── tm view ───────────────────────────────────────────────────────────────────
463
+
464
+ program
465
+ .command('view <id>')
466
+ .description('View full details of a task')
467
+ .action(async (id) => {
468
+ try {
469
+ const [task, state] = await Promise.all([api(`/api/tasks/${id}`), api('/api/state')])
470
+ const assignee = state.assignees.find((a) => a.id === task.assigneeId)?.name ?? 'Unassigned'
471
+ const requester = state.assignees.find((a) => a.id === task.requestedById)?.name ?? null
472
+ const bucket = state.buckets.find((b) => b.id === task.bucketId)?.title ?? '—'
473
+ const board = state.boards.find((b) => b.id === task.boardId)?.name ?? '—'
474
+
475
+ console.log()
476
+ console.log(chalk.bold(task.title) + (task.completed ? chalk.green(' ✓ done') : ''))
477
+ console.log(chalk.gray(` ID: ${task.id}`))
478
+ console.log(chalk.gray(` Board: ${board} / ${bucket}`))
479
+ console.log(chalk.gray(` Assigned: ${assignee}`))
480
+ if (requester) console.log(chalk.gray(` Requested: ${requester}`))
481
+ if (task.dueDate) console.log(chalk.gray(` Due: ${formatDate(task.dueDate)}`))
482
+ if (task.repo) console.log(chalk.gray(` Repo: ${task.repo}`))
483
+ if (task.contextUrl) console.log(chalk.gray(` Link: ${task.contextUrl}`))
484
+ if (task.tags?.length) console.log(chalk.gray(` Tags: #${task.tags.join(' #')}`))
485
+ if (task.description) { console.log(); console.log(` ${task.description}`) }
486
+ if (task.notes) { console.log(chalk.gray('\n Notes:')); console.log(` ${task.notes}`) }
487
+ if (task.subtasks?.length) {
488
+ console.log(chalk.gray('\n Subtasks:'))
489
+ task.subtasks.forEach((s) => {
490
+ console.log(` ${s.completed ? chalk.green('✓') : '○'} ${s.title}`)
491
+ })
492
+ }
493
+ if (task.comments?.length) {
494
+ console.log(chalk.gray('\n Comments:'))
495
+ task.comments.forEach((c) => {
496
+ console.log(` ${chalk.gray(new Date(c.createdAt).toLocaleString())} ${c.text}`)
497
+ })
498
+ }
499
+ console.log()
500
+ } catch (err) { handleError(err) }
501
+ })
502
+
503
+ // ── tm comment ────────────────────────────────────────────────────────────────
504
+
505
+ program
506
+ .command('comment <id> <text>')
507
+ .description('Add a comment to a task')
508
+ .action(async (id, text) => {
509
+ try {
510
+ const task = await api(`/api/tasks/${id}`)
511
+ const comment = { id: randomUUID(), text, createdAt: Date.now() }
512
+ await api(`/api/tasks/${id}`, {
513
+ method: 'PATCH',
514
+ body: JSON.stringify({ comments: [...(task.comments ?? []), comment] }),
515
+ })
516
+ console.log(chalk.green('\n✓ Comment added\n'))
517
+ } catch (err) { handleError(err) }
518
+ })
519
+
520
+ // ── tm boards ─────────────────────────────────────────────────────────────────
521
+
522
+ program
523
+ .command('boards')
524
+ .description('List all boards')
525
+ .action(async () => {
526
+ try {
527
+ const state = await api('/api/state')
528
+ console.log()
529
+ state.boards.forEach((b) => {
530
+ const open = state.tasks.filter((t) => t.boardId === b.id && !t.completed).length
531
+ const done = state.tasks.filter((t) => t.boardId === b.id && t.completed).length
532
+ console.log(` ${chalk.bold(b.name)} ${chalk.gray(`${open} open · ${done} done`)}`)
533
+ })
534
+ console.log()
535
+ } catch (err) { handleError(err) }
536
+ })
537
+
538
+ // ── tm invite ─────────────────────────────────────────────────────────────────
539
+
540
+ program
541
+ .command('invite')
542
+ .description('Generate a workspace invite link')
543
+ .option('--email <email>', 'Email address to invite')
544
+ .option('--role <role>', 'Role: member or agent (default: member)', 'member')
545
+ .action(async (opts) => {
546
+ try {
547
+ const data = await api('/api/workspace/invite', {
548
+ method: 'POST',
549
+ body: JSON.stringify({ email: opts.email, role: opts.role }),
550
+ })
551
+ console.log(chalk.green('\n✓ Invite created'))
552
+ console.log(` ${chalk.bold(data.link)}`)
553
+ if (opts.email) console.log(chalk.gray(` For: ${opts.email} (${opts.role})`))
554
+ console.log(chalk.gray(' Expires in 7 days\n'))
555
+ } catch (err) { handleError(err) }
556
+ })
557
+
558
+ // ── tm config ─────────────────────────────────────────────────────────────────
559
+
560
+ program
561
+ .command('config')
562
+ .description('Show or set CLI configuration')
563
+ .option('--api-url <url>', 'Set API base URL')
564
+ .action((opts) => {
565
+ if (opts.apiUrl) {
566
+ conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
567
+ console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
568
+ return
569
+ }
570
+ console.log()
571
+ console.log(chalk.bold('CLI config'))
572
+ console.log(chalk.gray(` API URL: ${getApiBase()}`))
573
+ console.log(chalk.gray(` Signed in as: ${conf.get('user_email') ?? '—'}`))
574
+ console.log(chalk.gray(` Agent mode: ${process.env.TASK_MANAGER_TOKEN ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
575
+ console.log(chalk.gray(` Config file: ${conf.path}`))
576
+ console.log()
577
+ })
578
+
579
+ program.parse()
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "open-wadah",
3
+ "version": "1.0.0",
4
+ "description": "Open Wadah CLI — shared task board for humans and agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "wadah": "cli.js",
8
+ "ow": "cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node cli.js"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.4.1",
15
+ "commander": "^13.1.0",
16
+ "conf": "^13.1.0",
17
+ "inquirer": "^12.5.0",
18
+ "node-fetch": "^3.3.2"
19
+ }
20
+ }