open-wadah 1.0.5 → 1.0.6

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 +28 -0
  2. package/cli.js +296 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -35,11 +35,39 @@ wadah complete <task-id>
35
35
 
36
36
  - `wadah --help`
37
37
  - `wadah whoami`
38
+ - `wadah doctor`
38
39
  - `wadah list --open`
39
40
  - `wadah move <task-id> <bucket>`
40
41
  - `wadah update <task-id> --title "New title"`
41
42
  - `wadah state`
42
43
 
44
+ ## Global Flags
45
+
46
+ - `--profile <name>`: use a named profile (defaults to `default`)
47
+ - `--token <token>`: use a token for this command only
48
+ - `--json`: force machine-readable JSON output
49
+ - `--quiet`: silence non-error output (also machine-friendly)
50
+
51
+ ## Stable Error Codes
52
+
53
+ When `--json` is enabled (or output is non-interactive), errors return:
54
+
55
+ ```json
56
+ {"error":{"code":"auth_error","message":"Not authenticated"}}
57
+ ```
58
+
59
+ Common codes:
60
+
61
+ - `auth_error`
62
+ - `validation_error`
63
+ - `network_error`
64
+ - `api_error`
65
+ - `server_error`
66
+ - `authorization_pending`
67
+ - `invalid_device_code`
68
+ - `token_expired`
69
+ - `login_timeout`
70
+
43
71
  ## API URL Override
44
72
 
45
73
  Use env var:
package/cli.js CHANGED
@@ -6,10 +6,20 @@ import Conf from 'conf'
6
6
  import { input, password as promptPassword } from '@inquirer/prompts'
7
7
  import { webcrypto } from 'node:crypto'
8
8
  import { spawn } from 'node:child_process'
9
+ import { readFileSync } from 'node:fs'
10
+ import { dirname, resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
9
12
 
10
13
  const randomUUID = () => webcrypto.randomUUID()
11
14
  let runtimeApiBase = null
12
15
  const DEFAULT_API_BASE = 'https://api.openwadah.com'
16
+ let runtimeProfile = null
17
+ let runtimeToken = null
18
+ let runtimeJsonOutput = false
19
+ let runtimeQuietOutput = false
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url))
22
+ const CLI_VERSION = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8')).version
13
23
 
14
24
  function normalizeApiUrl(url) {
15
25
  return String(url ?? '').trim().replace(/\/$/, '')
@@ -19,18 +29,42 @@ function normalizeApiUrl(url) {
19
29
 
20
30
  const conf = new Conf({ projectName: 'open-wadah' })
21
31
 
32
+ function getProfile() {
33
+ const profile = (process.env.TASK_MANAGER_PROFILE ?? runtimeProfile ?? 'default').trim()
34
+ return profile || 'default'
35
+ }
36
+
37
+ function profileKey(key) {
38
+ return `profiles.${getProfile()}.${key}`
39
+ }
40
+
41
+ function confGet(key) {
42
+ const scoped = conf.get(profileKey(key))
43
+ if (scoped !== undefined) return scoped
44
+ return conf.get(key)
45
+ }
46
+
47
+ function confSet(key, value) {
48
+ conf.set(profileKey(key), value)
49
+ }
50
+
51
+ function confDelete(key) {
52
+ conf.delete(profileKey(key))
53
+ }
54
+
22
55
  function getApiBase() {
23
56
  return (
24
57
  (process.env.TASK_MANAGER_API_URL ?? '').trim() ||
25
58
  runtimeApiBase ||
26
- conf.get('api_url') ||
59
+ confGet('api_url') ||
27
60
  DEFAULT_API_BASE
28
61
  )
29
62
  }
30
63
 
31
64
  function getAuthToken() {
32
65
  if (process.env.TASK_MANAGER_TOKEN) return process.env.TASK_MANAGER_TOKEN
33
- return conf.get('access_token') ?? null
66
+ if (runtimeToken) return runtimeToken
67
+ return confGet('access_token') ?? null
34
68
  }
35
69
 
36
70
  function authHeaders() {
@@ -44,7 +78,7 @@ function authHeaders() {
44
78
  // ── token refresh ─────────────────────────────────────────────────────────────
45
79
 
46
80
  async function refreshAccessToken() {
47
- const refreshToken = conf.get('refresh_token')
81
+ const refreshToken = confGet('refresh_token')
48
82
  if (!refreshToken) return false
49
83
  try {
50
84
  const res = await fetch(`${getApiBase()}/api/auth/refresh`, {
@@ -54,9 +88,9 @@ async function refreshAccessToken() {
54
88
  })
55
89
  if (!res.ok) return false
56
90
  const data = await res.json()
57
- conf.set('access_token', data.access_token)
58
- conf.set('refresh_token', data.refresh_token)
59
- conf.set('token_expires', data.expires_at)
91
+ confSet('access_token', data.access_token)
92
+ confSet('refresh_token', data.refresh_token)
93
+ confSet('token_expires', data.expires_at)
60
94
  return true
61
95
  } catch {
62
96
  return false
@@ -65,7 +99,7 @@ async function refreshAccessToken() {
65
99
 
66
100
  async function ensureAuth() {
67
101
  if (process.env.TASK_MANAGER_TOKEN) return // agent token, no refresh needed
68
- const expires = conf.get('token_expires')
102
+ const expires = confGet('token_expires')
69
103
  // Refresh if within 5 minutes of expiry or already expired
70
104
  if (expires && Date.now() > (expires * 1000) - 300_000) {
71
105
  await refreshAccessToken()
@@ -77,7 +111,12 @@ async function ensureAuth() {
77
111
  async function api(path, options = {}, retry = true) {
78
112
  await ensureAuth()
79
113
  const url = `${getApiBase()}${path}`
80
- const res = await fetch(url, { headers: authHeaders(), ...options })
114
+ let res
115
+ try {
116
+ res = await fetch(url, { headers: authHeaders(), ...options })
117
+ } catch (err) {
118
+ throw new CliError('network_error', err.message ?? 'Network request failed')
119
+ }
81
120
 
82
121
  // If 401 and we have a refresh token, try once more
83
122
  if (res.status === 401 && retry && !process.env.TASK_MANAGER_TOKEN) {
@@ -86,7 +125,9 @@ async function api(path, options = {}, retry = true) {
86
125
  }
87
126
 
88
127
  const body = await res.json().catch(() => ({}))
89
- if (!res.ok) throw new Error(body.error ?? body.message ?? res.statusText)
128
+ if (!res.ok) {
129
+ throw new CliError(mapApiErrorCode(res.status, body.error ?? body.message ?? res.statusText), body.error ?? body.message ?? res.statusText)
130
+ }
90
131
  return body
91
132
  }
92
133
 
@@ -106,6 +147,11 @@ function formatDate(ts) {
106
147
  }
107
148
 
108
149
  function printTaskList(tasks, state) {
150
+ if (isMachineOutput()) {
151
+ console.log(JSON.stringify({ tasks }, null, runtimeJsonOutput ? 2 : 0))
152
+ return
153
+ }
154
+
109
155
  const bucketMap = Object.fromEntries(state.buckets.map((b) => [b.id, b.title]))
110
156
  const assigneeMap = Object.fromEntries(state.assignees.map((a) => [a.id, a.name]))
111
157
  const boardMap = Object.fromEntries(state.boards.map((b) => [b.id, b.name]))
@@ -134,13 +180,17 @@ function printTaskList(tasks, state) {
134
180
  }
135
181
 
136
182
  function handleError(err) {
137
- const msg = err.message ?? ''
138
- if (
139
- msg.includes('401') ||
183
+ const msg = err.message ?? 'Unexpected error'
184
+ const code = err.code ?? 'unknown_error'
185
+ const authLike =
186
+ code === 'auth_error' ||
140
187
  msg.toLowerCase().includes('missing token') ||
141
188
  msg.toLowerCase().includes('invalid token') ||
142
189
  msg.toLowerCase().includes('not authenticated')
143
- ) {
190
+
191
+ if (isMachineOutput()) {
192
+ console.log(JSON.stringify({ error: { code, message: msg } }))
193
+ } else if (authLike) {
144
194
  console.error(chalk.red('\n✗ Not authenticated.'))
145
195
  console.error(chalk.gray(' Humans: wadah login'))
146
196
  console.error(chalk.gray(' Agents: TASK_MANAGER_TOKEN=<token> wadah open\n'))
@@ -182,12 +232,34 @@ function parseCompletedFlag(value) {
182
232
  const v = value.trim().toLowerCase()
183
233
  if (['true', '1', 'yes', 'y'].includes(v)) return true
184
234
  if (['false', '0', 'no', 'n'].includes(v)) return false
185
- throw new Error('--completed must be true or false')
235
+ throw new CliError('validation_error', '--completed must be true or false')
186
236
  }
187
237
 
188
238
  function fail(message) {
189
- console.error(chalk.red(`\n✗ ${message}\n`))
190
- process.exitCode = 1
239
+ throw new CliError('validation_error', message)
240
+ }
241
+
242
+ class CliError extends Error {
243
+ constructor(code, message) {
244
+ super(message)
245
+ this.code = code
246
+ }
247
+ }
248
+
249
+ function mapApiErrorCode(status, message) {
250
+ const msg = String(message ?? '').toLowerCase()
251
+ if (status === 401 || msg.includes('missing token') || msg.includes('invalid token') || msg.includes('not authenticated')) {
252
+ return 'auth_error'
253
+ }
254
+ if (status === 404 && msg.includes('invalid_device_code')) return 'invalid_device_code'
255
+ if (status === 410 && msg.includes('expired')) return 'token_expired'
256
+ if (status === 428 && msg.includes('authorization_pending')) return 'authorization_pending'
257
+ if (status >= 500) return 'server_error'
258
+ return 'api_error'
259
+ }
260
+
261
+ function isMachineOutput() {
262
+ return runtimeJsonOutput || runtimeQuietOutput || !process.stdout.isTTY
191
263
  }
192
264
 
193
265
  function sleep(ms) {
@@ -220,12 +292,20 @@ function openBrowser(url) {
220
292
  program
221
293
  .name('wadah')
222
294
  .description('Open Wadah CLI — shared task board for humans and agents')
223
- .version('1.0.0')
295
+ .version(CLI_VERSION)
224
296
  .option('--api <url>', 'Override API base URL for this command')
297
+ .option('--profile <name>', 'Use a named profile (default: default)')
298
+ .option('--token <token>', 'Use a token for this command only')
299
+ .option('--json', 'Print machine-readable JSON output')
300
+ .option('--quiet', 'Silence non-error output')
225
301
 
226
302
  program.hook('preAction', (_, actionCommand) => {
227
303
  const opts = actionCommand.optsWithGlobals()
228
304
  if (opts.api) runtimeApiBase = normalizeApiUrl(opts.api)
305
+ if (opts.profile) runtimeProfile = String(opts.profile).trim()
306
+ if (opts.token) runtimeToken = String(opts.token).trim()
307
+ runtimeJsonOutput = Boolean(opts.json)
308
+ runtimeQuietOutput = Boolean(opts.quiet)
229
309
  })
230
310
 
231
311
  // ── tm login ──────────────────────────────────────────────────────────────────
@@ -237,7 +317,7 @@ program
237
317
  .option('--password', 'Use terminal email/password instead of browser login')
238
318
  .option('--no-browser', "Don't auto-open browser; print URL only")
239
319
  .action(async (opts) => {
240
- if (opts.apiUrl) conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
320
+ if (opts.apiUrl) confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
241
321
 
242
322
  // Browser-based login (device flow) is default so password managers can autofill.
243
323
  if (!opts.password) {
@@ -248,7 +328,12 @@ program
248
328
  body: JSON.stringify({ client: 'open-wadah-cli' }),
249
329
  })
250
330
  const startBody = await startRes.json().catch(() => ({}))
251
- if (!startRes.ok) throw new Error(startBody.error ?? 'Could not start browser login')
331
+ if (!startRes.ok) {
332
+ throw new CliError(
333
+ mapApiErrorCode(startRes.status, startBody.error ?? 'Could not start browser login'),
334
+ startBody.error ?? 'Could not start browser login'
335
+ )
336
+ }
252
337
 
253
338
  const verificationUrl = startBody.verification_uri_complete ?? startBody.verification_uri
254
339
  const pollEveryMs = Math.max(1000, Number(startBody.interval ?? 2) * 1000)
@@ -276,11 +361,11 @@ program
276
361
  const pollBody = await pollRes.json().catch(() => ({}))
277
362
 
278
363
  if (pollRes.ok && pollBody.access_token) {
279
- conf.set('access_token', pollBody.access_token)
280
- conf.set('refresh_token', pollBody.refresh_token)
281
- conf.set('token_expires', pollBody.expires_at)
282
- conf.set('user_email', pollBody.user?.email ?? null)
283
- conf.set('user_id', pollBody.user?.id ?? null)
364
+ confSet('access_token', pollBody.access_token)
365
+ confSet('refresh_token', pollBody.refresh_token)
366
+ confSet('token_expires', pollBody.expires_at)
367
+ confSet('user_email', pollBody.user?.email ?? null)
368
+ confSet('user_id', pollBody.user?.id ?? null)
284
369
  console.log(chalk.green(`\n✓ Signed in${pollBody.user?.email ? ` as ${pollBody.user.email}` : ''}\n`))
285
370
  return
286
371
  }
@@ -291,21 +376,24 @@ program
291
376
  const pending = pollRes.status === 428 || pollBody.error === 'authorization_pending' || invalidCodeRace
292
377
  if (!pending) {
293
378
  if (pollRes.status === 410 || pollBody.error === 'expired_token') {
294
- throw new Error('Browser login expired. Please run wadah login again.')
379
+ throw new CliError('token_expired', 'Browser login expired. Please run wadah login again.')
295
380
  }
296
- throw new Error(pollBody.error ?? 'Browser login failed')
381
+ throw new CliError(mapApiErrorCode(pollRes.status, pollBody.error ?? 'Browser login failed'), pollBody.error ?? 'Browser login failed')
297
382
  }
298
383
  await sleep(pollEveryMs)
299
384
  }
300
385
 
301
- throw new Error('Browser login timed out. Please run wadah login again.')
386
+ throw new CliError('login_timeout', 'Browser login timed out. Please run wadah login again.')
302
387
  } catch (err) {
303
- console.error(chalk.red(`\n✗ ${err.message}\n`))
304
- process.exit(1)
388
+ handleError(err)
305
389
  }
306
390
  return
307
391
  }
308
392
 
393
+ if (!process.stdin.isTTY) {
394
+ fail('Password login requires an interactive terminal')
395
+ }
396
+
309
397
  console.log(chalk.bold('\nOpen Wadah — Sign in\n'))
310
398
  const email = await input({ message: 'Email:' })
311
399
  const pass = await promptPassword({ message: 'Password:' })
@@ -317,17 +405,16 @@ program
317
405
  body: JSON.stringify({ email, password: pass }),
318
406
  })
319
407
  const body = await data.json()
320
- if (!data.ok) throw new Error(body.error ?? 'Login failed')
408
+ if (!data.ok) throw new CliError(mapApiErrorCode(data.status, body.error ?? 'Login failed'), body.error ?? 'Login failed')
321
409
 
322
- conf.set('access_token', body.access_token)
323
- conf.set('refresh_token', body.refresh_token)
324
- conf.set('token_expires', body.expires_at)
325
- conf.set('user_email', body.user.email)
326
- conf.set('user_id', body.user.id)
410
+ confSet('access_token', body.access_token)
411
+ confSet('refresh_token', body.refresh_token)
412
+ confSet('token_expires', body.expires_at)
413
+ confSet('user_email', body.user.email)
414
+ confSet('user_id', body.user.id)
327
415
  console.log(chalk.green(`\n✓ Signed in as ${body.user.email}\n`))
328
416
  } catch (err) {
329
- console.error(chalk.red(`\n✗ ${err.message}\n`))
330
- process.exit(1)
417
+ handleError(err)
331
418
  }
332
419
  })
333
420
 
@@ -338,7 +425,11 @@ program
338
425
  .description('Create a new account')
339
426
  .option('--api-url <url>', 'Override API base URL and save it')
340
427
  .action(async (opts) => {
341
- if (opts.apiUrl) conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
428
+ if (opts.apiUrl) confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
429
+
430
+ if (!process.stdin.isTTY) {
431
+ fail('Signup requires an interactive terminal')
432
+ }
342
433
 
343
434
  console.log(chalk.bold('\nOpen Wadah — Create account\n'))
344
435
  const name = await input({ message: 'Name:' })
@@ -353,7 +444,7 @@ program
353
444
  body: JSON.stringify({ name, email, password: pass }),
354
445
  })
355
446
  const signupBody = await signupRes.json()
356
- if (!signupRes.ok) throw new Error(signupBody.error ?? 'Signup failed')
447
+ if (!signupRes.ok) throw new CliError(mapApiErrorCode(signupRes.status, signupBody.error ?? 'Signup failed'), signupBody.error ?? 'Signup failed')
357
448
 
358
449
  // Auto login
359
450
  const loginRes = await fetch(`${getApiBase()}/api/auth/login`, {
@@ -369,18 +460,17 @@ program
369
460
  console.log(chalk.gray(' Check your inbox and confirm your email, then run: wadah login\n'))
370
461
  return
371
462
  }
372
- throw new Error(msg)
463
+ throw new CliError(mapApiErrorCode(loginRes.status, msg), msg)
373
464
  }
374
465
 
375
- conf.set('access_token', loginBody.access_token)
376
- conf.set('refresh_token', loginBody.refresh_token)
377
- conf.set('token_expires', loginBody.expires_at)
378
- conf.set('user_email', loginBody.user.email)
379
- conf.set('user_id', loginBody.user.id)
466
+ confSet('access_token', loginBody.access_token)
467
+ confSet('refresh_token', loginBody.refresh_token)
468
+ confSet('token_expires', loginBody.expires_at)
469
+ confSet('user_email', loginBody.user.email)
470
+ confSet('user_id', loginBody.user.id)
380
471
  console.log(chalk.green(`\n✓ Account created and signed in as ${email}\n`))
381
472
  } catch (err) {
382
- console.error(chalk.red(`\n✗ ${err.message}\n`))
383
- process.exit(1)
473
+ handleError(err)
384
474
  }
385
475
  })
386
476
 
@@ -390,12 +480,12 @@ program
390
480
  .command('logout')
391
481
  .description('Sign out and clear stored credentials')
392
482
  .action(() => {
393
- conf.delete('access_token')
394
- conf.delete('refresh_token')
395
- conf.delete('token_expires')
396
- conf.delete('user_email')
397
- conf.delete('user_id')
398
- console.log(chalk.green('✓ Signed out\n'))
483
+ confDelete('access_token')
484
+ confDelete('refresh_token')
485
+ confDelete('token_expires')
486
+ confDelete('user_email')
487
+ confDelete('user_id')
488
+ if (!runtimeQuietOutput) console.log(chalk.green('✓ Signed out\n'))
399
489
  })
400
490
 
401
491
  // ── tm whoami ─────────────────────────────────────────────────────────────────
@@ -404,12 +494,47 @@ program
404
494
  .command('whoami')
405
495
  .description('Show current identity and workspace')
406
496
  .action(async () => {
497
+ if (isMachineOutput()) {
498
+ try {
499
+ if (process.env.TASK_MANAGER_TOKEN || runtimeToken) {
500
+ const me = await api('/api/auth/me')
501
+ const ws = me.workspaces?.[0] ?? null
502
+ console.log(JSON.stringify({
503
+ mode: 'agent',
504
+ api: getApiBase(),
505
+ email: me.user?.email ?? null,
506
+ workspace: ws ? { id: ws.id, name: ws.name, role: ws.role } : null,
507
+ profile: getProfile(),
508
+ }, null, runtimeJsonOutput ? 2 : 0))
509
+ return
510
+ }
511
+
512
+ const email = confGet('user_email')
513
+ if (!email) {
514
+ console.log(JSON.stringify({ signed_in: false, api: getApiBase(), profile: getProfile() }, null, runtimeJsonOutput ? 2 : 0))
515
+ return
516
+ }
517
+ const me = await api('/api/auth/me')
518
+ const ws = me.workspaces?.[0] ?? null
519
+ console.log(JSON.stringify({
520
+ signed_in: true,
521
+ api: getApiBase(),
522
+ email: me.user?.email ?? email,
523
+ workspace: ws ? { id: ws.id, name: ws.name, role: ws.role } : null,
524
+ profile: getProfile(),
525
+ }, null, runtimeJsonOutput ? 2 : 0))
526
+ } catch (err) {
527
+ handleError(err)
528
+ }
529
+ return
530
+ }
531
+
407
532
  if (process.env.TASK_MANAGER_TOKEN) {
408
533
  console.log(chalk.bold('\nAgent') + ' (TASK_MANAGER_TOKEN)')
409
534
  } else {
410
- const email = conf.get('user_email')
535
+ const email = confGet('user_email')
411
536
  if (!email) {
412
- console.log(chalk.gray('\nNot signed in. Run: tm login\n'))
537
+ console.log(chalk.gray('\nNot signed in. Run: wadah login\n'))
413
538
  return
414
539
  }
415
540
  try {
@@ -421,7 +546,7 @@ program
421
546
  console.log(chalk.gray(`API: ${getApiBase()}`))
422
547
  console.log()
423
548
  } catch {
424
- console.log(chalk.gray(`\n${email} (token may be expired — run: tm login)\n`))
549
+ console.log(chalk.gray(`\n${email} (token may be expired — run: wadah login)\n`))
425
550
  }
426
551
  }
427
552
  })
@@ -840,6 +965,103 @@ program
840
965
  } catch (err) { handleError(err) }
841
966
  })
842
967
 
968
+ program
969
+ .command('doctor')
970
+ .description('Run CLI diagnostics for API/auth/profile setup')
971
+ .action(async () => {
972
+ const checks = []
973
+ const addCheck = (id, status, message, details = null) => checks.push({ id, status, message, details })
974
+
975
+ addCheck('profile', 'pass', `Using profile "${getProfile()}"`)
976
+ addCheck('api_base', 'pass', `API base is ${getApiBase()}`)
977
+
978
+ let pingOk = false
979
+ try {
980
+ const pingRes = await fetch(`${getApiBase()}/api/ping`, { headers: { 'Content-Type': 'application/json' } })
981
+ if (pingRes.ok) {
982
+ pingOk = true
983
+ addCheck('api_ping', 'pass', 'API is reachable')
984
+ } else {
985
+ addCheck('api_ping', 'fail', `API ping failed with status ${pingRes.status}`)
986
+ }
987
+ } catch (err) {
988
+ addCheck('api_ping', 'fail', `API ping failed: ${err.message}`)
989
+ }
990
+
991
+ const hasEnvToken = Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken)
992
+ const hasSavedToken = Boolean(confGet('access_token'))
993
+ if (!hasEnvToken && !hasSavedToken) {
994
+ addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
995
+ } else if (hasEnvToken) {
996
+ addCheck('auth_token', 'pass', 'Using token from env/flag')
997
+ } else {
998
+ addCheck('auth_token', 'pass', 'Using token from profile config')
999
+ }
1000
+
1001
+ if (pingOk) {
1002
+ try {
1003
+ const meRes = await fetch(`${getApiBase()}/api/auth/me`, { headers: authHeaders() })
1004
+ if (meRes.ok) {
1005
+ const meBody = await meRes.json().catch(() => ({}))
1006
+ addCheck('auth_me', 'pass', `Authenticated as ${meBody.user?.email ?? 'unknown user'}`)
1007
+ } else if (meRes.status === 401) {
1008
+ addCheck('auth_me', 'warn', 'Token is missing/expired. Run wadah login.')
1009
+ } else {
1010
+ addCheck('auth_me', 'warn', `Auth check returned status ${meRes.status}`)
1011
+ }
1012
+ } catch (err) {
1013
+ addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
1014
+ }
1015
+
1016
+ try {
1017
+ const deviceRes = await fetch(`${getApiBase()}/api/auth/device/start`, {
1018
+ method: 'POST',
1019
+ headers: { 'Content-Type': 'application/json' },
1020
+ body: JSON.stringify({ client: 'open-wadah-cli-doctor' }),
1021
+ })
1022
+ if (deviceRes.ok) {
1023
+ addCheck('device_login', 'pass', 'Browser login endpoint is available')
1024
+ } else {
1025
+ addCheck('device_login', 'warn', `Browser login endpoint returned ${deviceRes.status}`)
1026
+ }
1027
+ } catch (err) {
1028
+ addCheck('device_login', 'warn', `Browser login endpoint failed: ${err.message}`)
1029
+ }
1030
+ }
1031
+
1032
+ const failed = checks.filter((c) => c.status === 'fail').length
1033
+ const warned = checks.filter((c) => c.status === 'warn').length
1034
+ const report = {
1035
+ ok: failed === 0,
1036
+ summary: { failed, warned, total: checks.length },
1037
+ checks,
1038
+ }
1039
+
1040
+ if (isMachineOutput()) {
1041
+ console.log(JSON.stringify(report, null, runtimeJsonOutput ? 2 : 0))
1042
+ if (failed > 0) process.exit(1)
1043
+ return
1044
+ }
1045
+
1046
+ console.log()
1047
+ console.log(chalk.bold('Open Wadah Doctor'))
1048
+ checks.forEach((check) => {
1049
+ const mark = check.status === 'pass' ? chalk.green('✓') : check.status === 'warn' ? chalk.yellow('!') : chalk.red('✗')
1050
+ console.log(` ${mark} ${check.id}: ${check.message}`)
1051
+ })
1052
+ console.log()
1053
+ if (failed > 0) {
1054
+ console.log(chalk.red(` ${failed} check(s) failed. Resolve failures and rerun wadah doctor.`))
1055
+ console.log()
1056
+ process.exit(1)
1057
+ } else if (warned > 0) {
1058
+ console.log(chalk.yellow(` Completed with ${warned} warning(s).`))
1059
+ console.log()
1060
+ } else {
1061
+ console.log(chalk.green(' All checks passed.\n'))
1062
+ }
1063
+ })
1064
+
843
1065
  // ── tm invite ─────────────────────────────────────────────────────────────────
844
1066
 
845
1067
  program
@@ -868,16 +1090,28 @@ program
868
1090
  .option('--api-url <url>', 'Set API base URL')
869
1091
  .action((opts) => {
870
1092
  if (opts.apiUrl) {
871
- conf.set('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
872
- console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
1093
+ confSet('api_url', opts.apiUrl.trim().replace(/\/$/, ''))
1094
+ if (!runtimeQuietOutput) console.log(chalk.green(`\n✓ API URL set to ${opts.apiUrl}\n`))
1095
+ return
1096
+ }
1097
+ const config = {
1098
+ api_url: getApiBase(),
1099
+ signed_in_as: confGet('user_email') ?? null,
1100
+ agent_mode: Boolean(process.env.TASK_MANAGER_TOKEN || runtimeToken),
1101
+ profile: getProfile(),
1102
+ config_file: conf.path,
1103
+ }
1104
+ if (isMachineOutput()) {
1105
+ console.log(JSON.stringify(config, null, runtimeJsonOutput ? 2 : 0))
873
1106
  return
874
1107
  }
875
1108
  console.log()
876
1109
  console.log(chalk.bold('CLI config'))
877
- console.log(chalk.gray(` API URL: ${getApiBase()}`))
878
- console.log(chalk.gray(` Signed in as: ${conf.get('user_email') ?? '—'}`))
879
- console.log(chalk.gray(` Agent mode: ${process.env.TASK_MANAGER_TOKEN ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
880
- console.log(chalk.gray(` Config file: ${conf.path}`))
1110
+ console.log(chalk.gray(` API URL: ${config.api_url}`))
1111
+ console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
1112
+ console.log(chalk.gray(` Agent mode: ${config.agent_mode ? 'yes (TASK_MANAGER_TOKEN set)' : 'no'}`))
1113
+ console.log(chalk.gray(` Profile: ${getProfile()}`))
1114
+ console.log(chalk.gray(` Config file: ${config.config_file}`))
881
1115
  console.log()
882
1116
  })
883
1117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-wadah",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Open Wadah CLI — shared task board for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {