open-wadah 1.0.6 → 1.1.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.
- package/README.md +53 -50
- package/cli.js +657 -4
- package/package.json +4 -2
- package/test/cli.test.js +67 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# open-wadah
|
|
2
2
|
|
|
3
|
-
Open Wadah CLI for
|
|
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
|
|
|
@@ -8,22 +8,13 @@ Open Wadah CLI for managing tasks in a shared workspace.
|
|
|
8
8
|
npm install -g open-wadah
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Commands
|
|
11
|
+
Commands: `wadah`, `tm`, `ow` (same CLI).
|
|
12
12
|
|
|
13
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
|
-
1.
|
|
16
|
-
2. Sign
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
wadah signup
|
|
20
|
-
wadah login
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`wadah login` uses browser sign-in by default.
|
|
24
|
-
Use `wadah login --password` for terminal email/password.
|
|
25
|
-
|
|
26
|
-
3. Use the CLI:
|
|
15
|
+
1. API must be reachable (default: `https://api.openwadah.com`). Override: `TASK_MANAGER_API_URL` or `--api <url>`.
|
|
16
|
+
2. Sign in: `wadah login` (browser) or `wadah login --password` (terminal).
|
|
17
|
+
3. Use the board:
|
|
27
18
|
|
|
28
19
|
```bash
|
|
29
20
|
wadah open
|
|
@@ -31,55 +22,67 @@ wadah add "Fix login issue"
|
|
|
31
22
|
wadah complete <task-id>
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
##
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Description |
|
|
28
|
+
|--------|-------------|
|
|
29
|
+
| **Auth** | `login`, `signup`, `logout`, `whoami` |
|
|
30
|
+
| **Tasks** | `open`, `list`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
|
|
31
|
+
| **Board** | `boards`, `buckets`, `assignees`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
32
|
+
| **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
|
|
33
|
+
| **Calendar** | `calendar` (list; `--assignee`, `--task`, `--from`, `--to`), `calendar add`, `calendar update <id>`, `calendar delete <id>` |
|
|
34
|
+
| **Docs** | `docs` (list), `doc create [title]`, `doc show <id>`, `doc update <id>`, `doc delete <id>` |
|
|
35
|
+
| **Agent tokens** | `agent-tokens` (list), `agent-token create [name]`, `agent-token delete <id>` |
|
|
36
|
+
| **Workspace** | `state`, `members`, `invite`, `config` |
|
|
37
|
+
| **Other** | `do "<sentence>"` (natural language, needs `OPENAI_API_KEY`), `doctor`, `completion [bash\|zsh]` |
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
- `wadah whoami`
|
|
38
|
-
- `wadah doctor`
|
|
39
|
-
- `wadah list --open`
|
|
40
|
-
- `wadah move <task-id> <bucket>`
|
|
41
|
-
- `wadah update <task-id> --title "New title"`
|
|
42
|
-
- `wadah state`
|
|
39
|
+
Run `wadah --help` for full list and options.
|
|
43
40
|
|
|
44
|
-
##
|
|
41
|
+
## Shell completion
|
|
45
42
|
|
|
46
|
-
|
|
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)
|
|
43
|
+
**Bash:**
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
```bash
|
|
46
|
+
wadah completion bash >> ~/.bashrc
|
|
47
|
+
source ~/.bashrc
|
|
48
|
+
```
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
**Zsh:**
|
|
54
51
|
|
|
55
|
-
```
|
|
56
|
-
|
|
52
|
+
```bash
|
|
53
|
+
wadah completion zsh >> ~/.zshrc
|
|
54
|
+
source ~/.zshrc
|
|
57
55
|
```
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
## Agent / AI use
|
|
60
58
|
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
-
|
|
65
|
-
- `server_error`
|
|
66
|
-
- `authorization_pending`
|
|
67
|
-
- `invalid_device_code`
|
|
68
|
-
- `token_expired`
|
|
69
|
-
- `login_timeout`
|
|
59
|
+
- Set `TASK_MANAGER_TOKEN` (create a token in the app under **Agent access**).
|
|
60
|
+
- Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`.
|
|
61
|
+
- Natural language: `wadah do "add a task to fix the bug"` (requires `OPENAI_API_KEY`).
|
|
62
|
+
- In Cursor/Claude/Kimi: see **WADAH_CLI.md** in the repo root for a short reference.
|
|
70
63
|
|
|
71
|
-
##
|
|
64
|
+
## Global flags
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
- `--api <url>` — API base URL
|
|
67
|
+
- `--profile <name>` — config profile
|
|
68
|
+
- `--token <token>` — auth token for this run
|
|
69
|
+
- `--json` — JSON output
|
|
70
|
+
- `--quiet` — minimal output
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
TASK_MANAGER_API_URL=https://api.openwadah.com wadah open
|
|
77
|
-
```
|
|
72
|
+
## Develop & test
|
|
78
73
|
|
|
79
|
-
|
|
74
|
+
From the repo root:
|
|
80
75
|
|
|
81
76
|
```bash
|
|
82
|
-
|
|
77
|
+
npm run install:all
|
|
78
|
+
npm run tm -- --help
|
|
79
|
+
cd task-manager-cli && npm test
|
|
83
80
|
```
|
|
84
81
|
|
|
85
|
-
|
|
82
|
+
## Error codes (with `--json`)
|
|
83
|
+
|
|
84
|
+
- `auth_error` — not authenticated
|
|
85
|
+
- `validation_error` — bad input
|
|
86
|
+
- `network_error` — request failed
|
|
87
|
+
- `api_error` — API returned an error
|
|
88
|
+
- `config` — missing config (e.g. OPENAI_API_KEY for `do`)
|
package/cli.js
CHANGED
|
@@ -6,8 +6,9 @@ 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
|
|
9
|
+
import { readFileSync, existsSync, createReadStream } from 'node:fs'
|
|
10
|
+
import FormData from 'form-data'
|
|
11
|
+
import { dirname, resolve, basename } from 'node:path'
|
|
11
12
|
import { fileURLToPath } from 'node:url'
|
|
12
13
|
|
|
13
14
|
const randomUUID = () => webcrypto.randomUUID()
|
|
@@ -203,12 +204,11 @@ function handleError(err) {
|
|
|
203
204
|
async function resolveMyAssigneeId(state) {
|
|
204
205
|
try {
|
|
205
206
|
const me = await api('/api/auth/me')
|
|
207
|
+
if (me.assigneeId) return me.assigneeId
|
|
206
208
|
const userId = me.user?.id
|
|
207
|
-
// Match by user_id field on assignee (if backend returns it)
|
|
208
209
|
const byUserId = state.assignees.find((a) => a.user_id === userId)
|
|
209
210
|
if (byUserId) return byUserId.id
|
|
210
211
|
} catch {}
|
|
211
|
-
// Fallback: 'Me' assignee
|
|
212
212
|
return state.assignees.find((a) => a.name === 'Me')?.id ?? null
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -266,6 +266,106 @@ function sleep(ms) {
|
|
|
266
266
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// ── natural language (do) ─────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
const DO_SYSTEM_PROMPT = `You are a parser for the "wadah" CLI (task board). Given the user's natural language request, output exactly one JSON object with no markdown or extra text:
|
|
272
|
+
{"command": "<command>", "args": [<array of string arguments>]}
|
|
273
|
+
|
|
274
|
+
Available commands and their args (use these exact command names):
|
|
275
|
+
- add: args = [task title string]. Example: add a task "Fix login" -> {"command": "add", "args": ["Fix login"]}
|
|
276
|
+
- folder create (or mkdir): args = [folder name]. Example: create a folder called Reports -> {"command": "folder create", "args": ["Reports"]}
|
|
277
|
+
- upload: args = [file path]. Optional: include "--folder", "<folder-id>" in args if user says to put in a folder. Example: upload report.pdf -> {"command": "upload", "args": ["report.pdf"]}
|
|
278
|
+
- open: args = []. List open tasks.
|
|
279
|
+
- list: args = []. List all tasks.
|
|
280
|
+
- complete: args = [task id]. Mark task done. Example: complete task abc123 -> {"command": "complete", "args": ["abc123"]}
|
|
281
|
+
- requested: args = []. List tasks I requested.
|
|
282
|
+
- view: args = [task id]. Show task details.
|
|
283
|
+
- boards: args = []. List boards.
|
|
284
|
+
- buckets: args = []. List columns.
|
|
285
|
+
- assignees: args = []. List assignees.
|
|
286
|
+
- state: args = []. Full state JSON.
|
|
287
|
+
- move: args = [task id, bucket name or id]. Move task to another column.
|
|
288
|
+
- assign: args = [task id, assignee name]. Assign a task.
|
|
289
|
+
- whoami: args = []. Show current user/workspace.
|
|
290
|
+
- folders: args = []. List folders.
|
|
291
|
+
- files: args = []. List files; optionally include "--folder", "<id>" in args.
|
|
292
|
+
- members: args = []. List workspace members.
|
|
293
|
+
- calendar: args = []. List calendar events.
|
|
294
|
+
- calendar add: args = []; include "--title", "<t>", "--start", "<iso>", "--end", "<iso>" for new event (start/end required).
|
|
295
|
+
- docs: args = []. List workspace docs.
|
|
296
|
+
- doc create: args = [title]. Create a doc. Optional: "--parent", "<id>", "--emoji", "<char>".
|
|
297
|
+
- doc show: args = [doc id]. Show one doc.
|
|
298
|
+
- doc update: args = [doc id]. Optional: "--title", "<t>", "--content", "<json>", "--emoji", "--parent", "<id>".
|
|
299
|
+
- doc delete: args = [doc id]. Delete a doc.
|
|
300
|
+
- agent-tokens: args = []. List agent tokens.
|
|
301
|
+
- agent-token create: args = [name]. Create agent token; optional "--assignee-name", "<text>".
|
|
302
|
+
- agent-token delete: args = [token id]. Revoke token.
|
|
303
|
+
- board create: args = [board name]. Create board.
|
|
304
|
+
- board delete: args = [board id]. Delete board.
|
|
305
|
+
- bucket create: args = [column title]. Optional "--board", "<id>".
|
|
306
|
+
- bucket update: args = [bucket id]. Optional "--title", "<t>", "--order", "<n>".
|
|
307
|
+
- bucket delete: args = [bucket id]. Delete column.
|
|
308
|
+
- assignee create: args = [name]. Optional "--type", "human" or "agent".
|
|
309
|
+
- assignee update: args = [assignee id]. Optional "--name", "--type".
|
|
310
|
+
- assignee delete: args = [assignee id]. Delete assignee.
|
|
311
|
+
|
|
312
|
+
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
313
|
+
|
|
314
|
+
async function parseNaturalLanguage(message) {
|
|
315
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.WADAH_OPENAI_API_KEY
|
|
316
|
+
if (!apiKey || !apiKey.trim()) {
|
|
317
|
+
throw new CliError('config', 'Set OPENAI_API_KEY or WADAH_OPENAI_API_KEY to use natural language (wadah do "...")')
|
|
318
|
+
}
|
|
319
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: {
|
|
322
|
+
'Content-Type': 'application/json',
|
|
323
|
+
Authorization: `Bearer ${apiKey.trim()}`,
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
model: 'gpt-4o-mini',
|
|
327
|
+
messages: [
|
|
328
|
+
{ role: 'system', content: DO_SYSTEM_PROMPT },
|
|
329
|
+
{ role: 'user', content: String(message).trim() },
|
|
330
|
+
],
|
|
331
|
+
max_tokens: 200,
|
|
332
|
+
temperature: 0,
|
|
333
|
+
}),
|
|
334
|
+
})
|
|
335
|
+
const data = await res.json().catch(() => ({}))
|
|
336
|
+
if (!res.ok) {
|
|
337
|
+
const err = data.error?.message || data.message || res.statusText
|
|
338
|
+
throw new CliError('api_error', `LLM request failed: ${err}`)
|
|
339
|
+
}
|
|
340
|
+
const text = data.choices?.[0]?.message?.content?.trim() || ''
|
|
341
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
|
342
|
+
const raw = jsonMatch ? jsonMatch[0] : text
|
|
343
|
+
let parsed
|
|
344
|
+
try {
|
|
345
|
+
parsed = JSON.parse(raw)
|
|
346
|
+
} catch (_) {
|
|
347
|
+
throw new CliError('api_error', 'Could not parse LLM response as JSON')
|
|
348
|
+
}
|
|
349
|
+
const cmd = parsed.command && String(parsed.command).trim()
|
|
350
|
+
const args = Array.isArray(parsed.args) ? parsed.args.map(String) : []
|
|
351
|
+
return { command: cmd, args }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function runResolvedCommand(command, args) {
|
|
355
|
+
const cliPath = resolve(__dirname, 'cli.js')
|
|
356
|
+
const childArgs = [cliPath]
|
|
357
|
+
if (runtimeApiBase) childArgs.push('--api', runtimeApiBase)
|
|
358
|
+
if (runtimeToken) childArgs.push('--token', runtimeToken)
|
|
359
|
+
if (runtimeProfile) childArgs.push('--profile', runtimeProfile)
|
|
360
|
+
const cmdParts = String(command).split(/\s+/).filter(Boolean)
|
|
361
|
+
childArgs.push(...cmdParts, ...args)
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const child = spawn(process.execPath, childArgs, { stdio: 'inherit', env: process.env, shell: false })
|
|
364
|
+
child.on('error', reject)
|
|
365
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Exit ${code}`))))
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
269
369
|
function openBrowser(url) {
|
|
270
370
|
let cmd
|
|
271
371
|
let args
|
|
@@ -308,6 +408,27 @@ program.hook('preAction', (_, actionCommand) => {
|
|
|
308
408
|
runtimeQuietOutput = Boolean(opts.quiet)
|
|
309
409
|
})
|
|
310
410
|
|
|
411
|
+
// ── do (natural language) ──────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
program
|
|
414
|
+
.command('do <message>')
|
|
415
|
+
.description('Run a command from natural language (e.g. "add a task to fix the login bug")')
|
|
416
|
+
.action(async (message) => {
|
|
417
|
+
try {
|
|
418
|
+
const { command, args } = await parseNaturalLanguage(message)
|
|
419
|
+
if (!command) {
|
|
420
|
+
console.error(chalk.yellow('\nCould not understand. Try a direct command, e.g:\n wadah add "Fix bug"\n wadah folder create "Reports"\n wadah open\n'))
|
|
421
|
+
process.exit(1)
|
|
422
|
+
}
|
|
423
|
+
if (!runtimeQuietOutput && process.stdout.isTTY) {
|
|
424
|
+
console.log(chalk.gray(`Running: wadah ${command} ${args.join(' ')}\n`))
|
|
425
|
+
}
|
|
426
|
+
await runResolvedCommand(command, args)
|
|
427
|
+
} catch (err) {
|
|
428
|
+
handleError(err)
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
|
|
311
432
|
// ── tm login ──────────────────────────────────────────────────────────────────
|
|
312
433
|
|
|
313
434
|
program
|
|
@@ -955,6 +1076,503 @@ program
|
|
|
955
1076
|
} catch (err) { handleError(err) }
|
|
956
1077
|
})
|
|
957
1078
|
|
|
1079
|
+
// ── board CRUD ────────────────────────────────────────────────────────────────
|
|
1080
|
+
|
|
1081
|
+
const boardCmd = program.command('board').description('Create or delete a board (list: wadah boards)')
|
|
1082
|
+
|
|
1083
|
+
boardCmd
|
|
1084
|
+
.command('create <name>')
|
|
1085
|
+
.description('Create a new board')
|
|
1086
|
+
.action(async (name) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const board = await api('/api/boards', { method: 'POST', body: JSON.stringify({ name: String(name).trim() }) })
|
|
1089
|
+
if (isMachineOutput()) {
|
|
1090
|
+
console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
|
|
1091
|
+
} else {
|
|
1092
|
+
console.log(chalk.green('\n✓ Board created') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1093
|
+
}
|
|
1094
|
+
} catch (err) { handleError(err) }
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
boardCmd
|
|
1098
|
+
.command('delete <id>')
|
|
1099
|
+
.description('Delete a board (cannot delete the last one)')
|
|
1100
|
+
.action(async (id) => {
|
|
1101
|
+
try {
|
|
1102
|
+
await api(`/api/boards/${id}`, { method: 'DELETE' })
|
|
1103
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Board deleted\n'))
|
|
1104
|
+
} catch (err) { handleError(err) }
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
// ── bucket CRUD ──────────────────────────────────────────────────────────────
|
|
1108
|
+
|
|
1109
|
+
const bucketCmd = program.command('bucket').description('Create, update, or delete a column/bucket (list: wadah buckets)')
|
|
1110
|
+
|
|
1111
|
+
bucketCmd
|
|
1112
|
+
.command('create <title>')
|
|
1113
|
+
.description('Create a new bucket (column)')
|
|
1114
|
+
.option('--board <id>', 'Board id (optional)')
|
|
1115
|
+
.action(async function (title) {
|
|
1116
|
+
try {
|
|
1117
|
+
const body = { title: String(title).trim() }
|
|
1118
|
+
if (this.opts().board) body.boardId = this.opts().board
|
|
1119
|
+
const bucket = await api('/api/buckets', { method: 'POST', body: JSON.stringify(body) })
|
|
1120
|
+
if (isMachineOutput()) {
|
|
1121
|
+
console.log(JSON.stringify(bucket, null, runtimeJsonOutput ? 2 : 0))
|
|
1122
|
+
} else {
|
|
1123
|
+
console.log(chalk.green('\n✓ Bucket created') + ` ${chalk.bold(bucket.title)} ${chalk.gray(bucket.id?.slice(0, 8))}\n`)
|
|
1124
|
+
}
|
|
1125
|
+
} catch (err) { handleError(err) }
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
bucketCmd
|
|
1129
|
+
.command('update <id>')
|
|
1130
|
+
.description('Update a bucket')
|
|
1131
|
+
.option('--title <text>', 'New title')
|
|
1132
|
+
.option('--order <n>', 'Order (number)', parseInt)
|
|
1133
|
+
.action(async function (id) {
|
|
1134
|
+
const opts = this.opts()
|
|
1135
|
+
try {
|
|
1136
|
+
const patch = {}
|
|
1137
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1138
|
+
if (opts.order !== undefined && !Number.isNaN(opts.order)) patch.order = opts.order
|
|
1139
|
+
if (Object.keys(patch).length === 0) return fail('Provide --title or --order to update')
|
|
1140
|
+
await api(`/api/buckets/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1141
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Bucket updated\n'))
|
|
1142
|
+
} catch (err) { handleError(err) }
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
bucketCmd
|
|
1146
|
+
.command('delete <id>')
|
|
1147
|
+
.description('Delete a bucket (tasks move to another column)')
|
|
1148
|
+
.action(async (id) => {
|
|
1149
|
+
try {
|
|
1150
|
+
await api(`/api/buckets/${id}`, { method: 'DELETE' })
|
|
1151
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Bucket deleted\n'))
|
|
1152
|
+
} catch (err) { handleError(err) }
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
// ── assignee CRUD ────────────────────────────────────────────────────────────
|
|
1156
|
+
|
|
1157
|
+
const assigneeCmd = program.command('assignee').description('Create, update, or delete an assignee (list: wadah assignees)')
|
|
1158
|
+
|
|
1159
|
+
assigneeCmd
|
|
1160
|
+
.command('create <name>')
|
|
1161
|
+
.description('Create a new assignee')
|
|
1162
|
+
.option('--type <type>', 'Type: human or agent', 'human')
|
|
1163
|
+
.action(async function (name) {
|
|
1164
|
+
try {
|
|
1165
|
+
const body = { name: String(name).trim(), type: this.opts().type || 'human' }
|
|
1166
|
+
const assignee = await api('/api/assignees', { method: 'POST', body: JSON.stringify(body) })
|
|
1167
|
+
if (isMachineOutput()) {
|
|
1168
|
+
console.log(JSON.stringify(assignee, null, runtimeJsonOutput ? 2 : 0))
|
|
1169
|
+
} else {
|
|
1170
|
+
console.log(chalk.green('\n✓ Assignee created') + ` ${chalk.bold(assignee.name)} ${chalk.gray(assignee.id?.slice(0, 8))}\n`)
|
|
1171
|
+
}
|
|
1172
|
+
} catch (err) { handleError(err) }
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
assigneeCmd
|
|
1176
|
+
.command('update <id>')
|
|
1177
|
+
.description('Update an assignee')
|
|
1178
|
+
.option('--name <text>', 'New name')
|
|
1179
|
+
.option('--type <type>', 'Type: human or agent')
|
|
1180
|
+
.action(async function (id) {
|
|
1181
|
+
const opts = this.opts()
|
|
1182
|
+
try {
|
|
1183
|
+
const patch = {}
|
|
1184
|
+
if (opts.name !== undefined) patch.name = opts.name
|
|
1185
|
+
if (opts.type !== undefined) patch.type = opts.type
|
|
1186
|
+
if (Object.keys(patch).length === 0) return fail('Provide --name or --type to update')
|
|
1187
|
+
await api(`/api/assignees/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1188
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Assignee updated\n'))
|
|
1189
|
+
} catch (err) { handleError(err) }
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
assigneeCmd
|
|
1193
|
+
.command('delete <id>')
|
|
1194
|
+
.description('Delete an assignee (tasks unassigned)')
|
|
1195
|
+
.action(async (id) => {
|
|
1196
|
+
try {
|
|
1197
|
+
await api(`/api/assignees/${id}`, { method: 'DELETE' })
|
|
1198
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Assignee deleted\n'))
|
|
1199
|
+
} catch (err) { handleError(err) }
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
program
|
|
1203
|
+
.command('folders')
|
|
1204
|
+
.description('List all folders (Files tab)')
|
|
1205
|
+
.action(async () => {
|
|
1206
|
+
try {
|
|
1207
|
+
const folders = await api('/api/folders').catch(() => [])
|
|
1208
|
+
const list = Array.isArray(folders) ? folders : []
|
|
1209
|
+
if (isMachineOutput()) {
|
|
1210
|
+
console.log(JSON.stringify(list, null, runtimeJsonOutput ? 2 : 0))
|
|
1211
|
+
} else if (list.length === 0) {
|
|
1212
|
+
console.log(chalk.gray('\nNo folders. Create one: wadah folder create "Name"\n'))
|
|
1213
|
+
} else {
|
|
1214
|
+
console.log()
|
|
1215
|
+
list.forEach((f) => console.log(` ${chalk.bold(f.name)} ${chalk.gray(`id: ${f.id}`)}`))
|
|
1216
|
+
console.log()
|
|
1217
|
+
}
|
|
1218
|
+
} catch (err) { handleError(err) }
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
program
|
|
1222
|
+
.command('files')
|
|
1223
|
+
.description('List all files in the workspace')
|
|
1224
|
+
.option('--folder <id>', 'Filter by folder id')
|
|
1225
|
+
.action(async function () {
|
|
1226
|
+
const opts = this.opts()
|
|
1227
|
+
try {
|
|
1228
|
+
const state = await api('/api/state')
|
|
1229
|
+
let files = state.files ?? []
|
|
1230
|
+
if (opts.folder) files = files.filter((f) => (f.folderId || f.folder_id) === opts.folder)
|
|
1231
|
+
if (isMachineOutput()) {
|
|
1232
|
+
console.log(JSON.stringify(files, null, runtimeJsonOutput ? 2 : 0))
|
|
1233
|
+
} else if (files.length === 0) {
|
|
1234
|
+
console.log(chalk.gray('\nNo files. Upload one: wadah upload <path>\n'))
|
|
1235
|
+
} else {
|
|
1236
|
+
console.log()
|
|
1237
|
+
files.forEach((f) => {
|
|
1238
|
+
const name = f.name || f.id
|
|
1239
|
+
const url = f.storageUrl || f.storage_url
|
|
1240
|
+
console.log(` ${chalk.bold(name)} ${chalk.gray(url ? url.slice(0, 50) + '…' : f.id)}`)
|
|
1241
|
+
})
|
|
1242
|
+
console.log()
|
|
1243
|
+
}
|
|
1244
|
+
} catch (err) { handleError(err) }
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
program
|
|
1248
|
+
.command('members')
|
|
1249
|
+
.description('List workspace members')
|
|
1250
|
+
.action(async () => {
|
|
1251
|
+
try {
|
|
1252
|
+
const members = await api('/api/workspace/members')
|
|
1253
|
+
const list = Array.isArray(members) ? members : []
|
|
1254
|
+
if (isMachineOutput()) {
|
|
1255
|
+
console.log(JSON.stringify(list, null, runtimeJsonOutput ? 2 : 0))
|
|
1256
|
+
} else if (list.length === 0) {
|
|
1257
|
+
console.log(chalk.gray('\nNo members.\n'))
|
|
1258
|
+
} else {
|
|
1259
|
+
console.log()
|
|
1260
|
+
list.forEach((m) => console.log(` ${chalk.bold(m.role)} ${chalk.gray(m.user_id?.slice(0, 8) ?? '—')}`))
|
|
1261
|
+
console.log()
|
|
1262
|
+
}
|
|
1263
|
+
} catch (err) { handleError(err) }
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1267
|
+
|
|
1268
|
+
program
|
|
1269
|
+
.command('agent-tokens')
|
|
1270
|
+
.description('List agent tokens for this workspace')
|
|
1271
|
+
.action(async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
const list = await api('/api/agent-tokens').catch(() => [])
|
|
1274
|
+
const arr = Array.isArray(list) ? list : []
|
|
1275
|
+
if (isMachineOutput()) {
|
|
1276
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1277
|
+
} else if (arr.length === 0) {
|
|
1278
|
+
console.log(chalk.gray('\nNo agent tokens. Create one: wadah agent-token create "Bot name"\n'))
|
|
1279
|
+
} else {
|
|
1280
|
+
console.log()
|
|
1281
|
+
arr.forEach((t) => {
|
|
1282
|
+
const as = t.assigneeName ? chalk.gray(` · acts as ${t.assigneeName}`) : ''
|
|
1283
|
+
console.log(` ${chalk.bold(t.name)} ${chalk.gray(`id: ${t.id?.slice(0, 8)}${as}`)}`)
|
|
1284
|
+
})
|
|
1285
|
+
console.log()
|
|
1286
|
+
}
|
|
1287
|
+
} catch (err) { handleError(err) }
|
|
1288
|
+
})
|
|
1289
|
+
|
|
1290
|
+
const agentTokenCmd = program.command('agent-token').description('Create or delete an agent token (for CLI/CI)')
|
|
1291
|
+
|
|
1292
|
+
agentTokenCmd
|
|
1293
|
+
.command('create [name]')
|
|
1294
|
+
.description('Create an agent token; token is shown once — store it as TASK_MANAGER_TOKEN')
|
|
1295
|
+
.option('--name <text>', 'Token name (if not passed as argument)')
|
|
1296
|
+
.option('--assignee-name <text>', 'Display name for the agent (default: same as name)')
|
|
1297
|
+
.action(async function (nameArg) {
|
|
1298
|
+
const opts = this.opts()
|
|
1299
|
+
const name = (nameArg || opts.name || 'CLI Agent').trim()
|
|
1300
|
+
try {
|
|
1301
|
+
const body = { name }
|
|
1302
|
+
if (opts.assigneeName) body.assigneeName = opts.assigneeName
|
|
1303
|
+
const result = await api('/api/agent-tokens', { method: 'POST', body: JSON.stringify(body) })
|
|
1304
|
+
if (isMachineOutput()) {
|
|
1305
|
+
console.log(JSON.stringify(result, null, runtimeJsonOutput ? 2 : 0))
|
|
1306
|
+
} else {
|
|
1307
|
+
console.log(chalk.green('\n✓ Agent token created'))
|
|
1308
|
+
console.log(chalk.gray(` Name: ${result.name} Id: ${result.id?.slice(0, 8)} Acts as: ${result.assigneeName ?? result.name}`))
|
|
1309
|
+
console.log(chalk.yellow('\n Token (save it — shown once):'))
|
|
1310
|
+
console.log(` ${result.token}`)
|
|
1311
|
+
console.log(chalk.gray('\n Use: export TASK_MANAGER_TOKEN="<token>"\n'))
|
|
1312
|
+
}
|
|
1313
|
+
} catch (err) { handleError(err) }
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
agentTokenCmd
|
|
1317
|
+
.command('delete <id>')
|
|
1318
|
+
.description('Revoke an agent token')
|
|
1319
|
+
.action(async (id) => {
|
|
1320
|
+
try {
|
|
1321
|
+
await api(`/api/agent-tokens/${id}`, { method: 'DELETE' })
|
|
1322
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Agent token revoked\n'))
|
|
1323
|
+
} catch (err) { handleError(err) }
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
// ── calendar events ──────────────────────────────────────────────────────────
|
|
1327
|
+
|
|
1328
|
+
const calendarCmd = program
|
|
1329
|
+
.command('calendar')
|
|
1330
|
+
.description('List calendar events (optional: --assignee, --task, --from, --to)')
|
|
1331
|
+
.option('--assignee <id>', 'Filter by assignee id')
|
|
1332
|
+
.option('--task <id>', 'Filter by task id')
|
|
1333
|
+
.option('--from <iso>', 'From date (ISO)')
|
|
1334
|
+
.option('--to <iso>', 'To date (ISO)')
|
|
1335
|
+
.action(async function () {
|
|
1336
|
+
const opts = this.opts()
|
|
1337
|
+
try {
|
|
1338
|
+
let path = '/api/calendar-events'
|
|
1339
|
+
const q = []
|
|
1340
|
+
if (opts.assignee) q.push(`assigneeId=${encodeURIComponent(opts.assignee)}`)
|
|
1341
|
+
if (opts.task) q.push(`taskId=${encodeURIComponent(opts.task)}`)
|
|
1342
|
+
if (opts.from) q.push(`from=${encodeURIComponent(opts.from)}`)
|
|
1343
|
+
if (opts.to) q.push(`to=${encodeURIComponent(opts.to)}`)
|
|
1344
|
+
if (q.length) path += '?' + q.join('&')
|
|
1345
|
+
const list = await api(path).catch(() => [])
|
|
1346
|
+
const arr = Array.isArray(list) ? list : []
|
|
1347
|
+
if (isMachineOutput()) {
|
|
1348
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1349
|
+
} else if (arr.length === 0) {
|
|
1350
|
+
console.log(chalk.gray('\nNo calendar events.\n'))
|
|
1351
|
+
} else {
|
|
1352
|
+
console.log()
|
|
1353
|
+
arr.forEach((e) => {
|
|
1354
|
+
const start = e.startTime ? new Date(e.startTime).toLocaleString() : '—'
|
|
1355
|
+
const end = e.endTime ? new Date(e.endTime).toLocaleString() : '—'
|
|
1356
|
+
console.log(` ${chalk.bold(e.title || 'Untitled')} ${chalk.gray(`${start} → ${end}`)} ${chalk.gray(`id: ${e.id?.slice(0, 8)}`)}`)
|
|
1357
|
+
})
|
|
1358
|
+
console.log()
|
|
1359
|
+
}
|
|
1360
|
+
} catch (err) { handleError(err) }
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
calendarCmd
|
|
1364
|
+
.command('add')
|
|
1365
|
+
.description('Create a calendar event (--start and --end required, ISO or YYYY-MM-DD)')
|
|
1366
|
+
.option('--title <text>', 'Event title', 'Work block')
|
|
1367
|
+
.option('--start <iso>', 'Start time (ISO or YYYY-MM-DD)')
|
|
1368
|
+
.option('--end <iso>', 'End time (ISO or YYYY-MM-DD)')
|
|
1369
|
+
.option('--task <id>', 'Task id')
|
|
1370
|
+
.option('--assignee <id>', 'Assignee id')
|
|
1371
|
+
.option('--color <hex>', 'Color')
|
|
1372
|
+
.action(async function () {
|
|
1373
|
+
const opts = this.opts()
|
|
1374
|
+
try {
|
|
1375
|
+
if (!opts.start || !opts.end) return fail('--start and --end are required')
|
|
1376
|
+
const startTime = new Date(opts.start).toISOString()
|
|
1377
|
+
const endTime = new Date(opts.end).toISOString()
|
|
1378
|
+
const body = { title: opts.title, startTime, endTime }
|
|
1379
|
+
if (opts.task) body.taskId = opts.task
|
|
1380
|
+
if (opts.assignee) body.assigneeId = opts.assignee
|
|
1381
|
+
if (opts.color) body.color = opts.color
|
|
1382
|
+
const event = await api('/api/calendar-events', { method: 'POST', body: JSON.stringify(body) })
|
|
1383
|
+
if (isMachineOutput()) {
|
|
1384
|
+
console.log(JSON.stringify(event, null, runtimeJsonOutput ? 2 : 0))
|
|
1385
|
+
} else {
|
|
1386
|
+
console.log(chalk.green('\n✓ Event created') + ` ${chalk.bold(event.title)} ${chalk.gray(event.id?.slice(0, 8))}\n`)
|
|
1387
|
+
}
|
|
1388
|
+
} catch (err) { handleError(err) }
|
|
1389
|
+
})
|
|
1390
|
+
|
|
1391
|
+
calendarCmd
|
|
1392
|
+
.command('update <id>')
|
|
1393
|
+
.description('Update a calendar event')
|
|
1394
|
+
.option('--title <text>', 'Event title')
|
|
1395
|
+
.option('--start <iso>', 'Start time')
|
|
1396
|
+
.option('--end <iso>', 'End time')
|
|
1397
|
+
.option('--task <id>', 'Task id')
|
|
1398
|
+
.option('--assignee <id>', 'Assignee id')
|
|
1399
|
+
.option('--color <hex>', 'Color')
|
|
1400
|
+
.action(async function (id) {
|
|
1401
|
+
const opts = this.opts()
|
|
1402
|
+
try {
|
|
1403
|
+
const patch = {}
|
|
1404
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1405
|
+
if (opts.start !== undefined) patch.startTime = new Date(opts.start).toISOString()
|
|
1406
|
+
if (opts.end !== undefined) patch.endTime = new Date(opts.end).toISOString()
|
|
1407
|
+
if (opts.task !== undefined) patch.taskId = opts.task
|
|
1408
|
+
if (opts.assignee !== undefined) patch.assigneeId = opts.assignee
|
|
1409
|
+
if (opts.color !== undefined) patch.color = opts.color
|
|
1410
|
+
if (Object.keys(patch).length === 0) return fail('Provide at least one option to update')
|
|
1411
|
+
await api(`/api/calendar-events/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1412
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Event updated\n'))
|
|
1413
|
+
} catch (err) { handleError(err) }
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
calendarCmd
|
|
1417
|
+
.command('delete <id>')
|
|
1418
|
+
.description('Delete a calendar event')
|
|
1419
|
+
.action(async (id) => {
|
|
1420
|
+
try {
|
|
1421
|
+
await api(`/api/calendar-events/${id}`, { method: 'DELETE' })
|
|
1422
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Event deleted\n'))
|
|
1423
|
+
} catch (err) { handleError(err) }
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
// ── workspace docs ─────────────────────────────────────────────────────────────
|
|
1427
|
+
|
|
1428
|
+
program
|
|
1429
|
+
.command('docs')
|
|
1430
|
+
.description('List workspace docs')
|
|
1431
|
+
.action(async () => {
|
|
1432
|
+
try {
|
|
1433
|
+
const list = await api('/api/docs').catch(() => [])
|
|
1434
|
+
const arr = Array.isArray(list) ? list : []
|
|
1435
|
+
if (isMachineOutput()) {
|
|
1436
|
+
console.log(JSON.stringify(arr, null, runtimeJsonOutput ? 2 : 0))
|
|
1437
|
+
} else if (arr.length === 0) {
|
|
1438
|
+
console.log(chalk.gray('\nNo docs.\n'))
|
|
1439
|
+
} else {
|
|
1440
|
+
console.log()
|
|
1441
|
+
arr.forEach((d) => console.log(` ${chalk.bold(d.title || 'Untitled')} ${chalk.gray(`id: ${d.id?.slice(0, 8)}`)}`))
|
|
1442
|
+
console.log()
|
|
1443
|
+
}
|
|
1444
|
+
} catch (err) { handleError(err) }
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
const docCmd = program.command('doc').description('Workspace doc (create, show, update, delete)')
|
|
1448
|
+
|
|
1449
|
+
docCmd
|
|
1450
|
+
.command('create [title]')
|
|
1451
|
+
.description('Create a workspace doc')
|
|
1452
|
+
.option('--title <text>', 'Doc title (if not passed as argument)')
|
|
1453
|
+
.option('--parent <id>', 'Parent doc id')
|
|
1454
|
+
.option('--emoji <char>', 'Emoji')
|
|
1455
|
+
.action(async function (titleArg) {
|
|
1456
|
+
const opts = this.opts()
|
|
1457
|
+
const title = (titleArg || opts.title || 'Untitled').trim()
|
|
1458
|
+
try {
|
|
1459
|
+
const body = { title }
|
|
1460
|
+
if (opts.parent) body.parentId = opts.parent
|
|
1461
|
+
if (opts.emoji) body.emoji = opts.emoji
|
|
1462
|
+
const doc = await api('/api/docs', { method: 'POST', body: JSON.stringify(body) })
|
|
1463
|
+
if (isMachineOutput()) {
|
|
1464
|
+
console.log(JSON.stringify(doc, null, runtimeJsonOutput ? 2 : 0))
|
|
1465
|
+
} else {
|
|
1466
|
+
console.log(chalk.green('\n✓ Doc created') + ` ${chalk.bold(doc.title)} ${chalk.gray(doc.id?.slice(0, 8))}\n`)
|
|
1467
|
+
}
|
|
1468
|
+
} catch (err) { handleError(err) }
|
|
1469
|
+
})
|
|
1470
|
+
|
|
1471
|
+
docCmd
|
|
1472
|
+
.command('show <id>')
|
|
1473
|
+
.description('Show a workspace doc')
|
|
1474
|
+
.action(async (id) => {
|
|
1475
|
+
try {
|
|
1476
|
+
const doc = await api(`/api/docs/${id}`)
|
|
1477
|
+
if (isMachineOutput()) {
|
|
1478
|
+
console.log(JSON.stringify(doc, null, runtimeJsonOutput ? 2 : 0))
|
|
1479
|
+
} else {
|
|
1480
|
+
console.log()
|
|
1481
|
+
console.log(chalk.bold(doc.title || 'Untitled'))
|
|
1482
|
+
console.log(chalk.gray(`id: ${doc.id}`))
|
|
1483
|
+
if (doc.content && typeof doc.content === 'object') {
|
|
1484
|
+
const text = JSON.stringify(doc.content)
|
|
1485
|
+
console.log(chalk.gray(text.slice(0, 200) + (text.length > 200 ? '…' : '')))
|
|
1486
|
+
}
|
|
1487
|
+
console.log()
|
|
1488
|
+
}
|
|
1489
|
+
} catch (err) { handleError(err) }
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
docCmd
|
|
1493
|
+
.command('update <id>')
|
|
1494
|
+
.description('Update a workspace doc')
|
|
1495
|
+
.option('--title <text>', 'Doc title')
|
|
1496
|
+
.option('--content <json>', 'Content as JSON string')
|
|
1497
|
+
.option('--emoji <char>', 'Emoji')
|
|
1498
|
+
.option('--parent <id>', 'Parent doc id')
|
|
1499
|
+
.action(async function (id) {
|
|
1500
|
+
const opts = this.opts()
|
|
1501
|
+
try {
|
|
1502
|
+
const patch = {}
|
|
1503
|
+
if (opts.title !== undefined) patch.title = opts.title
|
|
1504
|
+
if (opts.content !== undefined) {
|
|
1505
|
+
try {
|
|
1506
|
+
patch.content = typeof opts.content === 'string' ? JSON.parse(opts.content) : opts.content
|
|
1507
|
+
} catch (_) {
|
|
1508
|
+
return fail('--content must be valid JSON')
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (opts.emoji !== undefined) patch.emoji = opts.emoji
|
|
1512
|
+
if (opts.parent !== undefined) patch.parentId = opts.parent
|
|
1513
|
+
if (Object.keys(patch).length === 0) return fail('Provide at least one option to update')
|
|
1514
|
+
await api(`/api/docs/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
|
|
1515
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Doc updated\n'))
|
|
1516
|
+
} catch (err) { handleError(err) }
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
docCmd
|
|
1520
|
+
.command('delete <id>')
|
|
1521
|
+
.description('Delete a workspace doc')
|
|
1522
|
+
.action(async (id) => {
|
|
1523
|
+
try {
|
|
1524
|
+
await api(`/api/docs/${id}`, { method: 'DELETE' })
|
|
1525
|
+
if (!runtimeQuietOutput) console.log(chalk.green('\n✓ Doc deleted\n'))
|
|
1526
|
+
} catch (err) { handleError(err) }
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
program
|
|
1530
|
+
.command('folder create <name>')
|
|
1531
|
+
.alias('mkdir')
|
|
1532
|
+
.description('Create a folder in the workspace (Files tab)')
|
|
1533
|
+
.action(async (name) => {
|
|
1534
|
+
try {
|
|
1535
|
+
const folder = await api('/api/folders', {
|
|
1536
|
+
method: 'POST',
|
|
1537
|
+
body: JSON.stringify({ name: String(name).trim() }),
|
|
1538
|
+
})
|
|
1539
|
+
if (isMachineOutput()) {
|
|
1540
|
+
console.log(JSON.stringify(folder, null, runtimeJsonOutput ? 2 : 0))
|
|
1541
|
+
} else {
|
|
1542
|
+
console.log(chalk.green('\n✓ Folder created') + ` ${chalk.bold(folder.name)} ${chalk.gray(`(id: ${folder.id})\n`)}`)
|
|
1543
|
+
}
|
|
1544
|
+
} catch (err) { handleError(err) }
|
|
1545
|
+
})
|
|
1546
|
+
|
|
1547
|
+
program
|
|
1548
|
+
.command('upload <path>')
|
|
1549
|
+
.description('Upload a file to the workspace (optional: --folder <id>)')
|
|
1550
|
+
.option('--folder <id>', 'Put the file in this folder id')
|
|
1551
|
+
.action(async (pathArg, opts) => {
|
|
1552
|
+
try {
|
|
1553
|
+
await ensureAuth()
|
|
1554
|
+
const filePath = resolve(pathArg)
|
|
1555
|
+
if (!existsSync(filePath)) return fail(`File not found: ${filePath}`)
|
|
1556
|
+
const form = new FormData()
|
|
1557
|
+
form.append('file', createReadStream(filePath), { filename: basename(filePath) })
|
|
1558
|
+
if (opts.folder) form.append('folderId', String(opts.folder).trim())
|
|
1559
|
+
const headers = { ...authHeaders() }
|
|
1560
|
+
delete headers['Content-Type']
|
|
1561
|
+
const res = await fetch(`${getApiBase()}/api/files/upload`, {
|
|
1562
|
+
method: 'POST',
|
|
1563
|
+
headers: { ...headers, ...form.getHeaders() },
|
|
1564
|
+
body: form,
|
|
1565
|
+
})
|
|
1566
|
+
const data = await res.json().catch(() => ({}))
|
|
1567
|
+
if (!res.ok) throw new CliError(data.error ?? 'Upload failed', data.error ?? res.statusText)
|
|
1568
|
+
if (isMachineOutput()) {
|
|
1569
|
+
console.log(JSON.stringify(data, null, runtimeJsonOutput ? 2 : 0))
|
|
1570
|
+
} else {
|
|
1571
|
+
console.log(chalk.green('\n✓ Uploaded') + ` ${chalk.bold(data.name)} ${chalk.gray(`→ ${data.storageUrl ?? data.id}\n`)}`)
|
|
1572
|
+
}
|
|
1573
|
+
} catch (err) { handleError(err) }
|
|
1574
|
+
})
|
|
1575
|
+
|
|
958
1576
|
program
|
|
959
1577
|
.command('state')
|
|
960
1578
|
.description('Print full workspace state as JSON')
|
|
@@ -1115,4 +1733,39 @@ program
|
|
|
1115
1733
|
console.log()
|
|
1116
1734
|
})
|
|
1117
1735
|
|
|
1736
|
+
// ── shell completion ──────────────────────────────────────────────────────────
|
|
1737
|
+
|
|
1738
|
+
const CLI_COMMANDS = [
|
|
1739
|
+
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'boards',
|
|
1740
|
+
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
1741
|
+
'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
|
|
1742
|
+
'move', 'open', 'reopen', 'requested', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
1743
|
+
]
|
|
1744
|
+
|
|
1745
|
+
program
|
|
1746
|
+
.command('completion [shell]')
|
|
1747
|
+
.description('Print shell completion script (bash or zsh). Add to your profile: wadah completion bash >> ~/.bashrc')
|
|
1748
|
+
.action((shell) => {
|
|
1749
|
+
const sh = (shell || process.env.SHELL || 'bash').toLowerCase()
|
|
1750
|
+
if (sh.includes('zsh')) {
|
|
1751
|
+
console.log(`# zsh completion for wadah
|
|
1752
|
+
_wadah() {
|
|
1753
|
+
local cur
|
|
1754
|
+
cur=\${words[CURRENT]}
|
|
1755
|
+
reply=(${CLI_COMMANDS.join(' ')})
|
|
1756
|
+
}
|
|
1757
|
+
compctl -K _wadah wadah tm ow
|
|
1758
|
+
`)
|
|
1759
|
+
} else {
|
|
1760
|
+
console.log(`# bash completion for wadah
|
|
1761
|
+
_wadah_completion() {
|
|
1762
|
+
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
1763
|
+
local cmds="${CLI_COMMANDS.join(' ')}"
|
|
1764
|
+
COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
|
|
1765
|
+
}
|
|
1766
|
+
complete -F _wadah_completion wadah tm ow
|
|
1767
|
+
`)
|
|
1768
|
+
}
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1118
1771
|
program.parse()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-wadah",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Open Wadah CLI — shared task board for humans and agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
"tm": "cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"start": "node cli.js"
|
|
12
|
+
"start": "node cli.js",
|
|
13
|
+
"test": "node --test test/cli.test.js"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@inquirer/prompts": "^8.3.0",
|
|
16
17
|
"chalk": "^5.4.1",
|
|
17
18
|
"commander": "^13.1.0",
|
|
18
19
|
"conf": "^13.1.0",
|
|
20
|
+
"form-data": "^4.0.0",
|
|
19
21
|
"node-fetch": "^3.3.2"
|
|
20
22
|
}
|
|
21
23
|
}
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { strict as assert } from 'node:assert'
|
|
4
|
+
import { resolve, dirname } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const cliPath = resolve(__dirname, '../cli.js')
|
|
9
|
+
|
|
10
|
+
function runCli(args) {
|
|
11
|
+
return new Promise((resolvePromise, reject) => {
|
|
12
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
env: { ...process.env, TASK_MANAGER_TOKEN: '' },
|
|
15
|
+
})
|
|
16
|
+
let stdout = ''
|
|
17
|
+
let stderr = ''
|
|
18
|
+
child.stdout?.on('data', (d) => { stdout += d })
|
|
19
|
+
child.stderr?.on('data', (d) => { stderr += d })
|
|
20
|
+
child.on('close', (code) => resolvePromise({ code, stdout, stderr }))
|
|
21
|
+
child.on('error', reject)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('wadah --help exits 0 and prints usage', async () => {
|
|
26
|
+
const { code, stdout } = await runCli(['--help'])
|
|
27
|
+
assert.equal(code, 0)
|
|
28
|
+
assert.match(stdout, /Open Wadah|Usage|Commands/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('wadah --version exits 0 and prints version', async () => {
|
|
32
|
+
const { code, stdout } = await runCli(['--version'])
|
|
33
|
+
assert.equal(code, 0)
|
|
34
|
+
assert.match(stdout, /\d+\.\d+\.\d+/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('wadah (no args) prints help or exits', async () => {
|
|
38
|
+
const { code, stdout, stderr } = await runCli([])
|
|
39
|
+
assert(code === 0 || code === 1)
|
|
40
|
+
const out = stdout + stderr
|
|
41
|
+
assert.match(out, /Usage|Commands|Open Wadah/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('wadah open --help shows open command help', async () => {
|
|
45
|
+
const { code, stdout } = await runCli(['open', '--help'])
|
|
46
|
+
assert.equal(code, 0)
|
|
47
|
+
assert.match(stdout, /open|List/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('wadah doctor runs without crashing', async () => {
|
|
51
|
+
const { code } = await runCli(['doctor'])
|
|
52
|
+
// Doctor may exit 0 or 1 depending on API/token; we only assert it doesn't throw
|
|
53
|
+
assert(code === 0 || code === 1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('wadah completion bash prints script', async () => {
|
|
57
|
+
const { code, stdout } = await runCli(['completion', 'bash'])
|
|
58
|
+
assert.equal(code, 0)
|
|
59
|
+
assert.match(stdout, /complete -F _wadah_completion/)
|
|
60
|
+
assert.match(stdout, /wadah tm ow/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('wadah completion zsh prints script', async () => {
|
|
64
|
+
const { code, stdout } = await runCli(['completion', 'zsh'])
|
|
65
|
+
assert.equal(code, 0)
|
|
66
|
+
assert.match(stdout, /compctl|_wadah/)
|
|
67
|
+
})
|