open-wadah 1.2.2 → 1.2.4
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 +111 -50
- package/cli.js +308 -32
- package/package.json +1 -1
- package/test/cli.test.js +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# open-wadah
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line interface for Wadah task boards and autonomous agent workflows.
|
|
4
|
+
|
|
5
|
+
`open-wadah` gives teams and AI agents a fast, scriptable way to plan, assign, and deliver work from the terminal.
|
|
6
|
+
|
|
7
|
+
## Why teams use Wadah CLI
|
|
8
|
+
|
|
9
|
+
- Manage tasks, assignees, and board flow without leaving your terminal.
|
|
10
|
+
- Automate workflows with stable JSON output for scripts and CI.
|
|
11
|
+
- Power autonomous agents with scoped agent tokens.
|
|
12
|
+
- Keep human and agent work on the same board and process.
|
|
4
13
|
|
|
5
14
|
## Install
|
|
6
15
|
|
|
@@ -8,83 +17,135 @@ Open WADAH CLI — shared task board for humans and agents. Use it from the term
|
|
|
8
17
|
npm install -g open-wadah
|
|
9
18
|
```
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
Available commands: `wadah`, `ow`, `tm`.
|
|
12
21
|
|
|
13
|
-
##
|
|
22
|
+
## 60-second quick start
|
|
14
23
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
1. Sign in:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
wadah login
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Open your assigned tasks:
|
|
18
31
|
|
|
19
32
|
```bash
|
|
20
33
|
wadah open
|
|
21
|
-
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Create and move work:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
wadah add "Fix login issue" --board "Main"
|
|
40
|
+
wadah move <task-id> <bucket-id>
|
|
22
41
|
wadah complete <task-id>
|
|
23
42
|
```
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
Default API URL is `https://api.openwadah.com`. Override with `--api <url>` or `WADAH_API_URL`.
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|--------|-------------|
|
|
29
|
-
| **Auth** | `login`, `signup`, `logout`, `whoami` |
|
|
30
|
-
| **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `delete` |
|
|
31
|
-
| **Relationships** | `add --blocks <id>`, `add --blocked-by <id>`, `update --blocks <id>` |
|
|
32
|
-
| **Board** | `boards --json`, `buckets --json`, `assignees --json`; `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
33
|
-
| **Files** | `folders`, `files`, `folder create` / `mkdir`, `upload` |
|
|
34
|
-
| **Calendar** | `calendar` (list; `--assignee`, `--task`, `--from`, `--to`), `calendar add`, `calendar update <id>`, `calendar delete <id>` |
|
|
35
|
-
| **Docs** | `docs` (list), `doc create [title]`, `doc show <id>`, `doc update <id>`, `doc delete <id>` |
|
|
36
|
-
| **Agent tokens** | `agent-tokens` (list), `agent-token create [name]`, `agent-token delete <id>` |
|
|
37
|
-
| **Workspace** | `state`, `members`, `invite`, `config` |
|
|
38
|
-
| **Other** | `do "<sentence>"` (natural language, needs `OPENAI_API_KEY`), `doctor`, `completion [bash\|zsh]` |
|
|
46
|
+
## Core workflows
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
### Daily task operations
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
```bash
|
|
51
|
+
wadah list --board "Main"
|
|
52
|
+
wadah search "billing"
|
|
53
|
+
wadah view <task-id>
|
|
54
|
+
wadah update <task-id> --notes "Root cause + next steps"
|
|
55
|
+
wadah comment <task-id> "Investigating now"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Dependencies and subtasks
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
wadah add "Ship release notes" --blocks <task-id>
|
|
62
|
+
wadah subtask add <task-id> "Write tests"
|
|
63
|
+
wadah subtask list <task-id>
|
|
64
|
+
wadah subtask toggle <task-id> <subtask-id>
|
|
65
|
+
```
|
|
43
66
|
|
|
44
|
-
|
|
67
|
+
### Board administration
|
|
45
68
|
|
|
46
69
|
```bash
|
|
47
|
-
wadah
|
|
48
|
-
|
|
70
|
+
wadah board create "Backend"
|
|
71
|
+
wadah bucket create "In Review" --board <board-id>
|
|
72
|
+
wadah assignee create "Cursor Agent" --type agent
|
|
73
|
+
wadah buckets --board "Backend" --json
|
|
49
74
|
```
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
## Command surface
|
|
77
|
+
|
|
78
|
+
| Area | Commands |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Auth | `login`, `signup`, `logout`, `whoami` |
|
|
81
|
+
| Tasks | `open`, `list`, `search`, `requested`, `add`, `view`, `update`, `move`, `assign`, `comment`, `complete`, `reopen`, `delete` |
|
|
82
|
+
| Subtasks | `subtask list`, `subtask add`, `subtask toggle`, `subtask delete` |
|
|
83
|
+
| Boards & members | `boards`, `buckets`, `assignees`, `board create/delete`, `bucket create/update/delete`, `assignee create/update/delete` |
|
|
84
|
+
| Docs | `docs`, `doc create/show/update/delete` |
|
|
85
|
+
| Calendar | `calendar`, `calendar add/update/delete` |
|
|
86
|
+
| Files | `folders`, `files`, `folder create`, `mkdir`, `upload` |
|
|
87
|
+
| Agent tokens | `agent-tokens`, `agent-token create/delete` |
|
|
88
|
+
| Utilities | `doctor`, `completion bash`, `completion zsh`, `state`, `members`, `invite`, `config`, `do` |
|
|
89
|
+
|
|
90
|
+
Use `wadah --help` or `wadah <command> --help` for full flags and examples.
|
|
91
|
+
|
|
92
|
+
## AI agent mode
|
|
93
|
+
|
|
94
|
+
For autonomous workflows (Cursor, Claude Code, GitHub Actions):
|
|
95
|
+
|
|
96
|
+
- Create an agent token:
|
|
52
97
|
|
|
53
98
|
```bash
|
|
54
|
-
wadah
|
|
55
|
-
source ~/.zshrc
|
|
99
|
+
wadah agent-token create "My Agent"
|
|
56
100
|
```
|
|
57
101
|
|
|
58
|
-
|
|
102
|
+
- Set it as `WADAH_AGENT_TOKEN` in your environment.
|
|
103
|
+
- Use `--json` for deterministic machine output (`wadah open --json`).
|
|
104
|
+
- Optional natural-language command:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
wadah do "add a task to fix onboarding bug"
|
|
108
|
+
```
|
|
59
109
|
|
|
60
|
-
|
|
61
|
-
- Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`, `wadah buckets --json`.
|
|
62
|
-
- Natural language: `wadah do "add a task to fix the bug"` (requires `OPENAI_API_KEY`).
|
|
63
|
-
- In Cursor/Claude/Kimi: see **WADAH_CLI.md** in the repo root for a short reference.
|
|
64
|
-
- For autonomous 24/7 agents with GitHub Actions: see **AGENTS.md** in the repo root.
|
|
110
|
+
`wadah do` requires `OPENAI_API_KEY`.
|
|
65
111
|
|
|
66
112
|
## Global flags
|
|
67
113
|
|
|
68
|
-
- `--api <url
|
|
69
|
-
- `--profile <name
|
|
70
|
-
- `--token <token
|
|
71
|
-
- `--json
|
|
72
|
-
- `--quiet
|
|
114
|
+
- `--api <url>`: API base URL
|
|
115
|
+
- `--profile <name>`: config profile
|
|
116
|
+
- `--token <token>`: token for current command only
|
|
117
|
+
- `--json`: machine-readable output
|
|
118
|
+
- `--quiet`: reduce non-essential output
|
|
73
119
|
|
|
74
|
-
##
|
|
120
|
+
## Environment variables
|
|
75
121
|
|
|
76
|
-
|
|
122
|
+
- `WADAH_AGENT_TOKEN`: preferred auth token for automation
|
|
123
|
+
- `WADAH_API_URL`: override API base URL
|
|
124
|
+
- `OPENAI_API_KEY`: required for `wadah do`
|
|
125
|
+
|
|
126
|
+
## Shell completion
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Bash
|
|
130
|
+
wadah completion bash >> ~/.bashrc && source ~/.bashrc
|
|
131
|
+
|
|
132
|
+
# Zsh
|
|
133
|
+
wadah completion zsh >> ~/.zshrc && source ~/.zshrc
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development
|
|
77
137
|
|
|
78
138
|
```bash
|
|
79
|
-
npm
|
|
80
|
-
|
|
81
|
-
cd task-manager-cli && npm test
|
|
139
|
+
npm test
|
|
140
|
+
node cli.js --help
|
|
82
141
|
```
|
|
83
142
|
|
|
84
|
-
##
|
|
143
|
+
## JSON error codes
|
|
144
|
+
|
|
145
|
+
When running with `--json`, failures include structured error codes:
|
|
85
146
|
|
|
86
|
-
- `auth_error`
|
|
87
|
-
- `validation_error`
|
|
88
|
-
- `network_error`
|
|
89
|
-
- `api_error`
|
|
90
|
-
- `config`
|
|
147
|
+
- `auth_error`
|
|
148
|
+
- `validation_error`
|
|
149
|
+
- `network_error`
|
|
150
|
+
- `api_error`
|
|
151
|
+
- `config`
|
package/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ function normalizeApiUrl(url) {
|
|
|
31
31
|
const conf = new Conf({ projectName: 'open-wadah' })
|
|
32
32
|
|
|
33
33
|
function getProfile() {
|
|
34
|
-
const profile = (process.env.
|
|
34
|
+
const profile = (process.env.WADAH_PROFILE ?? runtimeProfile ?? 'default').trim()
|
|
35
35
|
return profile || 'default'
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -55,15 +55,28 @@ function confDelete(key) {
|
|
|
55
55
|
|
|
56
56
|
function getApiBase() {
|
|
57
57
|
return (
|
|
58
|
-
(process.env.
|
|
58
|
+
(process.env.WADAH_API_URL ?? '').trim() ||
|
|
59
59
|
runtimeApiBase ||
|
|
60
60
|
confGet('api_url') ||
|
|
61
61
|
DEFAULT_API_BASE
|
|
62
62
|
)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Agent token from env: reads WADAH_AGENT_TOKEN. */
|
|
66
|
+
function envAgentToken() {
|
|
67
|
+
const w = (process.env.WADAH_AGENT_TOKEN ?? '').trim()
|
|
68
|
+
return w || null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Which env var supplied the agent token (for status output). */
|
|
72
|
+
function envAgentTokenVarName() {
|
|
73
|
+
if ((process.env.WADAH_AGENT_TOKEN ?? '').trim()) return 'WADAH_AGENT_TOKEN'
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
function getAuthToken() {
|
|
66
|
-
|
|
78
|
+
const fromEnv = envAgentToken()
|
|
79
|
+
if (fromEnv) return fromEnv
|
|
67
80
|
if (runtimeToken) return runtimeToken
|
|
68
81
|
return confGet('access_token') ?? null
|
|
69
82
|
}
|
|
@@ -99,7 +112,7 @@ async function refreshAccessToken() {
|
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
async function ensureAuth() {
|
|
102
|
-
if (
|
|
115
|
+
if (envAgentToken()) return // agent token, no refresh needed
|
|
103
116
|
const expires = confGet('token_expires')
|
|
104
117
|
// Refresh if within 5 minutes of expiry or already expired
|
|
105
118
|
if (expires && Date.now() > (expires * 1000) - 300_000) {
|
|
@@ -120,7 +133,7 @@ async function api(path, options = {}, retry = true) {
|
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
// If 401 and we have a refresh token, try once more
|
|
123
|
-
if (res.status === 401 && retry && !
|
|
136
|
+
if (res.status === 401 && retry && !envAgentToken()) {
|
|
124
137
|
const refreshed = await refreshAccessToken()
|
|
125
138
|
if (refreshed) return api(path, options, false)
|
|
126
139
|
}
|
|
@@ -197,7 +210,7 @@ function handleError(err) {
|
|
|
197
210
|
} else if (authLike) {
|
|
198
211
|
console.error(chalk.red('\n✗ Not authenticated.'))
|
|
199
212
|
console.error(chalk.gray(' Humans: wadah login'))
|
|
200
|
-
console.error(chalk.gray(' Agents:
|
|
213
|
+
console.error(chalk.gray(' Agents: WADAH_AGENT_TOKEN=<token> wadah open\n'))
|
|
201
214
|
} else if (code === 'network_error') {
|
|
202
215
|
console.error(chalk.red('\n✗ Network error — could not reach the API.'))
|
|
203
216
|
console.error(chalk.gray(` Check your connection or API URL: ${getApiBase()}`))
|
|
@@ -279,6 +292,23 @@ function findBucket(buckets, query) {
|
|
|
279
292
|
return buckets.find((b) => b.id === query || b.title.toLowerCase().includes(q)) ?? null
|
|
280
293
|
}
|
|
281
294
|
|
|
295
|
+
function findSubtask(subtasks, query) {
|
|
296
|
+
const raw = String(query ?? '').trim()
|
|
297
|
+
if (!raw) return null
|
|
298
|
+
const q = raw.toLowerCase()
|
|
299
|
+
const byId = subtasks.filter((s) => s.id === raw || s.id.startsWith(raw))
|
|
300
|
+
if (byId.length === 1) return byId[0]
|
|
301
|
+
if (byId.length > 1) {
|
|
302
|
+
throw new CliError('validation_error', `Ambiguous subtask id prefix: ${raw}`)
|
|
303
|
+
}
|
|
304
|
+
const byTitle = subtasks.filter((s) => String(s.title ?? '').toLowerCase() === q)
|
|
305
|
+
if (byTitle.length === 1) return byTitle[0]
|
|
306
|
+
if (byTitle.length > 1) {
|
|
307
|
+
throw new CliError('validation_error', `Multiple subtasks have title: ${raw}`)
|
|
308
|
+
}
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
|
|
282
312
|
function parseCompletedFlag(value) {
|
|
283
313
|
if (typeof value !== 'string') return null
|
|
284
314
|
const v = value.trim().toLowerCase()
|
|
@@ -383,7 +413,7 @@ Available commands and their args (use these exact command names):
|
|
|
383
413
|
- buckets: args = []. List columns.
|
|
384
414
|
- assignees: args = []. List assignees.
|
|
385
415
|
- state: args = []. Full state JSON.
|
|
386
|
-
- move: args = [task id, bucket name or id]. Move task to another column.
|
|
416
|
+
- move: args = [task id, bucket name or id]. Move task to another column. Use --board <name> to move to another board (lands in first column). Add --bucket <name> to specify the column on that board.
|
|
387
417
|
- assign: args = [task id, "--to", "assignee[,assignee2]", "--mode", "set|add|remove"]. Assign one or more people.
|
|
388
418
|
- whoami: args = []. Show current user/workspace.
|
|
389
419
|
- folders: args = []. List folders.
|
|
@@ -410,6 +440,8 @@ Available commands and their args (use these exact command names):
|
|
|
410
440
|
- repo add: args = [github url or owner/repo]. Link a GitHub repository to your workspace.
|
|
411
441
|
- repo list (or repos): args = []. List linked GitHub repositories.
|
|
412
442
|
- repo remove: args = [repo id]. Unlink a repository.
|
|
443
|
+
- pr link: args = [task id, pr url]. Attach a PR URL to a task.
|
|
444
|
+
- pr open: args = [task id]. Open the linked PR URL for a task.
|
|
413
445
|
|
|
414
446
|
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
415
447
|
|
|
@@ -719,7 +751,7 @@ program
|
|
|
719
751
|
.action(async () => {
|
|
720
752
|
if (isMachineOutput()) {
|
|
721
753
|
try {
|
|
722
|
-
if (
|
|
754
|
+
if (envAgentToken() || runtimeToken) {
|
|
723
755
|
const me = await api('/api/auth/me')
|
|
724
756
|
const ws = me.workspaces?.[0] ?? null
|
|
725
757
|
console.log(JSON.stringify({
|
|
@@ -752,8 +784,8 @@ program
|
|
|
752
784
|
return
|
|
753
785
|
}
|
|
754
786
|
|
|
755
|
-
if (
|
|
756
|
-
console.log(chalk.bold('\nAgent') +
|
|
787
|
+
if (envAgentToken()) {
|
|
788
|
+
console.log(chalk.bold('\nAgent') + ` (${envAgentTokenVarName()})`)
|
|
757
789
|
} else {
|
|
758
790
|
const email = confGet('user_email')
|
|
759
791
|
if (!email) {
|
|
@@ -1070,11 +1102,49 @@ program
|
|
|
1070
1102
|
})
|
|
1071
1103
|
|
|
1072
1104
|
program
|
|
1073
|
-
.command('move <id>
|
|
1074
|
-
.description('Move a task to another column')
|
|
1075
|
-
.
|
|
1105
|
+
.command('move <id> [bucket]')
|
|
1106
|
+
.description('Move a task to another column or board')
|
|
1107
|
+
.option('--board <name>', 'Target board (name or id)')
|
|
1108
|
+
.option('--bucket <name>', 'Target column on that board (defaults to first column)')
|
|
1109
|
+
.action(async (id, bucketArg, opts) => {
|
|
1076
1110
|
try {
|
|
1077
1111
|
const state = await getState()
|
|
1112
|
+
|
|
1113
|
+
// --board flag: resolve board then pick column
|
|
1114
|
+
if (opts.board) {
|
|
1115
|
+
const boardQuery = opts.board.toLowerCase()
|
|
1116
|
+
const board = state.boards.find((b) => b.id === opts.board || b.name.toLowerCase().includes(boardQuery))
|
|
1117
|
+
if (!board) return fail(`Board not found: ${opts.board}`)
|
|
1118
|
+
|
|
1119
|
+
const boardBuckets = state.buckets
|
|
1120
|
+
.filter((b) => b.boardId === board.id)
|
|
1121
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
1122
|
+
if (boardBuckets.length === 0) return fail(`Board "${board.name}" has no columns`)
|
|
1123
|
+
|
|
1124
|
+
let bucket
|
|
1125
|
+
if (opts.bucket) {
|
|
1126
|
+
const bq = opts.bucket.toLowerCase()
|
|
1127
|
+
bucket = boardBuckets.find((b) => b.id === opts.bucket || b.title.toLowerCase().includes(bq))
|
|
1128
|
+
if (!bucket) return fail(`Column "${opts.bucket}" not found on board "${board.name}"`)
|
|
1129
|
+
} else if (bucketArg) {
|
|
1130
|
+
const bq = bucketArg.toLowerCase()
|
|
1131
|
+
bucket = boardBuckets.find((b) => b.id === bucketArg || b.title.toLowerCase().includes(bq))
|
|
1132
|
+
if (!bucket) return fail(`Column "${bucketArg}" not found on board "${board.name}"`)
|
|
1133
|
+
} else {
|
|
1134
|
+
bucket = boardBuckets[0]
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const task = await api(`/api/tasks/${id}`, {
|
|
1138
|
+
method: 'PATCH',
|
|
1139
|
+
body: JSON.stringify({ bucketId: bucket.id, boardId: board.id }),
|
|
1140
|
+
})
|
|
1141
|
+
console.log(chalk.green('\n✓ Moved') + ` ${chalk.bold(task.title)} → ${board.name} / ${bucket.title}\n`)
|
|
1142
|
+
return
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// positional bucket arg (original behaviour — cross-board if bucket is on another board)
|
|
1146
|
+
const bucketQuery = bucketArg ?? opts.bucket
|
|
1147
|
+
if (!bucketQuery) return fail('Provide a column name/id, or use --board <name>')
|
|
1078
1148
|
const bucket = findBucket(state.buckets, bucketQuery)
|
|
1079
1149
|
if (!bucket) return fail(`Bucket not found: ${bucketQuery}`)
|
|
1080
1150
|
const patch = { bucketId: bucket.id }
|
|
@@ -1280,6 +1350,87 @@ program
|
|
|
1280
1350
|
} catch (err) { handleError(err) }
|
|
1281
1351
|
})
|
|
1282
1352
|
|
|
1353
|
+
const subtaskCmd = program.command('subtask').description('Manage task subtasks')
|
|
1354
|
+
|
|
1355
|
+
subtaskCmd
|
|
1356
|
+
.command('list <taskId>')
|
|
1357
|
+
.description('List subtasks on a task')
|
|
1358
|
+
.action(async (taskId) => {
|
|
1359
|
+
try {
|
|
1360
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1361
|
+
const subtasks = task.subtasks ?? []
|
|
1362
|
+
if (isMachineOutput()) {
|
|
1363
|
+
console.log(JSON.stringify({ taskId, subtasks }, null, runtimeJsonOutput ? 2 : 0))
|
|
1364
|
+
return
|
|
1365
|
+
}
|
|
1366
|
+
console.log()
|
|
1367
|
+
if (subtasks.length === 0) {
|
|
1368
|
+
console.log(chalk.gray(' No subtasks.\n'))
|
|
1369
|
+
return
|
|
1370
|
+
}
|
|
1371
|
+
console.log(chalk.bold(task.title))
|
|
1372
|
+
subtasks.forEach((s) => {
|
|
1373
|
+
const status = s.completed ? chalk.green('✓') : '○'
|
|
1374
|
+
console.log(` ${status} ${s.title} ${chalk.gray(s.id.slice(0, 8))}`)
|
|
1375
|
+
})
|
|
1376
|
+
console.log()
|
|
1377
|
+
} catch (err) { handleError(err) }
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
subtaskCmd
|
|
1381
|
+
.command('add <taskId> <title>')
|
|
1382
|
+
.description('Add a subtask to a task')
|
|
1383
|
+
.action(async (taskId, title) => {
|
|
1384
|
+
try {
|
|
1385
|
+
const trimmedTitle = String(title ?? '').trim()
|
|
1386
|
+
if (!trimmedTitle) return fail('Subtask title is required')
|
|
1387
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1388
|
+
const nextSubtask = { id: randomUUID(), title: trimmedTitle, completed: false }
|
|
1389
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1390
|
+
method: 'PATCH',
|
|
1391
|
+
body: JSON.stringify({ subtasks: [...(task.subtasks ?? []), nextSubtask] }),
|
|
1392
|
+
})
|
|
1393
|
+
console.log(chalk.green('\n✓ Subtask added') + ` ${chalk.bold(nextSubtask.title)} ${chalk.gray(`(${nextSubtask.id.slice(0, 8)})`)}\n`)
|
|
1394
|
+
} catch (err) { handleError(err) }
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
subtaskCmd
|
|
1398
|
+
.command('toggle <taskId> <subtaskId>')
|
|
1399
|
+
.description('Toggle subtask completion')
|
|
1400
|
+
.action(async (taskId, subtaskId) => {
|
|
1401
|
+
try {
|
|
1402
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1403
|
+
const subtasks = task.subtasks ?? []
|
|
1404
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1405
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1406
|
+
const nextSubtasks = subtasks.map((s) => s.id === target.id ? { ...s, completed: !s.completed } : s)
|
|
1407
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1408
|
+
method: 'PATCH',
|
|
1409
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1410
|
+
})
|
|
1411
|
+
const marker = !target.completed ? chalk.green('✓') : '○'
|
|
1412
|
+
console.log(chalk.green('\n✓ Subtask updated') + ` ${marker} ${chalk.bold(target.title)}\n`)
|
|
1413
|
+
} catch (err) { handleError(err) }
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
subtaskCmd
|
|
1417
|
+
.command('delete <taskId> <subtaskId>')
|
|
1418
|
+
.description('Delete a subtask from a task')
|
|
1419
|
+
.action(async (taskId, subtaskId) => {
|
|
1420
|
+
try {
|
|
1421
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1422
|
+
const subtasks = task.subtasks ?? []
|
|
1423
|
+
const target = findSubtask(subtasks, subtaskId)
|
|
1424
|
+
if (!target) return fail(`Subtask not found: ${subtaskId}`)
|
|
1425
|
+
const nextSubtasks = subtasks.filter((s) => s.id !== target.id)
|
|
1426
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1427
|
+
method: 'PATCH',
|
|
1428
|
+
body: JSON.stringify({ subtasks: nextSubtasks }),
|
|
1429
|
+
})
|
|
1430
|
+
console.log(chalk.green('\n✓ Subtask deleted') + ` ${chalk.bold(target.title)}\n`)
|
|
1431
|
+
} catch (err) { handleError(err) }
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1283
1434
|
// ── tm boards ─────────────────────────────────────────────────────────────────
|
|
1284
1435
|
|
|
1285
1436
|
program
|
|
@@ -1371,13 +1522,65 @@ const boardCmd = program.command('board').description('Create or delete a board
|
|
|
1371
1522
|
boardCmd
|
|
1372
1523
|
.command('create <name>')
|
|
1373
1524
|
.description('Create a new board')
|
|
1374
|
-
.
|
|
1525
|
+
.option('--flow <columns>', 'Comma-separated column flow, e.g. "Backlog,Ready,Doing,QA,Done"')
|
|
1526
|
+
.action(async function (name) {
|
|
1375
1527
|
try {
|
|
1528
|
+
const opts = this.opts()
|
|
1376
1529
|
const board = await api('/api/boards', { method: 'POST', body: JSON.stringify({ name: String(name).trim() }) })
|
|
1530
|
+
const createdBuckets = []
|
|
1531
|
+
if (opts.flow) {
|
|
1532
|
+
const flowColumns = String(opts.flow)
|
|
1533
|
+
.split(',')
|
|
1534
|
+
.map((c) => c.trim())
|
|
1535
|
+
.filter(Boolean)
|
|
1536
|
+
if (flowColumns.length === 0) {
|
|
1537
|
+
return fail('Flow is empty. Provide comma-separated columns, e.g. --flow "Backlog,Doing,Done".')
|
|
1538
|
+
}
|
|
1539
|
+
for (const column of flowColumns) {
|
|
1540
|
+
if (!column) continue
|
|
1541
|
+
const bucket = await api('/api/buckets', {
|
|
1542
|
+
method: 'POST',
|
|
1543
|
+
body: JSON.stringify({ title: column, boardId: board.id }),
|
|
1544
|
+
})
|
|
1545
|
+
createdBuckets.push(bucket)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1377
1548
|
if (isMachineOutput()) {
|
|
1378
|
-
console.log(JSON.stringify(
|
|
1549
|
+
console.log(JSON.stringify(
|
|
1550
|
+
{ ...board, flow: createdBuckets.map((b) => ({ id: b.id, title: b.title })) },
|
|
1551
|
+
null,
|
|
1552
|
+
runtimeJsonOutput ? 2 : 0
|
|
1553
|
+
))
|
|
1379
1554
|
} else {
|
|
1380
1555
|
console.log(chalk.green('\n✓ Board created') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1556
|
+
if (createdBuckets.length > 0) {
|
|
1557
|
+
console.log(chalk.gray(' Flow columns:'))
|
|
1558
|
+
createdBuckets.forEach((b) => console.log(` - ${b.title} ${chalk.gray(b.id?.slice(0, 8))}`))
|
|
1559
|
+
console.log()
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
} catch (err) { handleError(err) }
|
|
1563
|
+
})
|
|
1564
|
+
|
|
1565
|
+
boardCmd
|
|
1566
|
+
.command('rename <id> <name>')
|
|
1567
|
+
.description('Rename a board')
|
|
1568
|
+
.action(async (id, name) => {
|
|
1569
|
+
try {
|
|
1570
|
+
const payload = JSON.stringify({ name: String(name).trim() })
|
|
1571
|
+
let board
|
|
1572
|
+
try {
|
|
1573
|
+
board = await api(`/api/boards/${id}`, { method: 'PATCH', body: payload })
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
// Backward compatibility: some deployments only support PUT for board rename.
|
|
1576
|
+
const notFound = err instanceof CliError && err.code === 'api_error' && String(err.message).toLowerCase().includes('not found')
|
|
1577
|
+
if (!notFound) throw err
|
|
1578
|
+
board = await api(`/api/boards/${id}`, { method: 'PUT', body: payload })
|
|
1579
|
+
}
|
|
1580
|
+
if (isMachineOutput()) {
|
|
1581
|
+
console.log(JSON.stringify(board, null, runtimeJsonOutput ? 2 : 0))
|
|
1582
|
+
} else {
|
|
1583
|
+
console.log(chalk.green('\n✓ Board renamed') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1381
1584
|
}
|
|
1382
1585
|
} catch (err) { handleError(err) }
|
|
1383
1586
|
})
|
|
@@ -1630,6 +1833,79 @@ repoCmd
|
|
|
1630
1833
|
} catch (err) { handleError(err) }
|
|
1631
1834
|
})
|
|
1632
1835
|
|
|
1836
|
+
const prCmd = program.command('pr').description('Attach and open pull requests on tasks')
|
|
1837
|
+
|
|
1838
|
+
prCmd
|
|
1839
|
+
.command('link <taskId> <prUrl>')
|
|
1840
|
+
.description('Attach a PR URL to a task')
|
|
1841
|
+
.option('--comment', 'Add a task comment with the PR link')
|
|
1842
|
+
.action(async function (taskId, prUrl) {
|
|
1843
|
+
const opts = this.opts()
|
|
1844
|
+
try {
|
|
1845
|
+
let parsed
|
|
1846
|
+
try {
|
|
1847
|
+
parsed = new URL(String(prUrl).trim())
|
|
1848
|
+
} catch {
|
|
1849
|
+
return fail('Provide a valid PR URL (e.g. https://github.com/owner/repo/pull/123)')
|
|
1850
|
+
}
|
|
1851
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
1852
|
+
return fail('PR URL must start with http:// or https://')
|
|
1853
|
+
}
|
|
1854
|
+
const normalizedUrl = parsed.toString()
|
|
1855
|
+
|
|
1856
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1857
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1858
|
+
method: 'PATCH',
|
|
1859
|
+
body: JSON.stringify({ contextUrl: normalizedUrl }),
|
|
1860
|
+
})
|
|
1861
|
+
if (opts.comment) {
|
|
1862
|
+
const comment = {
|
|
1863
|
+
id: randomUUID(),
|
|
1864
|
+
text: `Linked PR: ${normalizedUrl}`,
|
|
1865
|
+
createdAt: Date.now(),
|
|
1866
|
+
}
|
|
1867
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1868
|
+
method: 'PATCH',
|
|
1869
|
+
body: JSON.stringify({ comments: [...(task.comments ?? []), comment] }),
|
|
1870
|
+
})
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (isMachineOutput()) {
|
|
1874
|
+
console.log(JSON.stringify({ taskId, prUrl: normalizedUrl, linked: true, commented: Boolean(opts.comment) }, null, runtimeJsonOutput ? 2 : 0))
|
|
1875
|
+
} else {
|
|
1876
|
+
console.log(chalk.green('\n✓ PR linked') + ` ${chalk.bold(task.title)}`)
|
|
1877
|
+
console.log(chalk.gray(` ${normalizedUrl}${opts.comment ? ' · comment added' : ''}\n`))
|
|
1878
|
+
}
|
|
1879
|
+
} catch (err) { handleError(err) }
|
|
1880
|
+
})
|
|
1881
|
+
|
|
1882
|
+
prCmd
|
|
1883
|
+
.command('open <taskId>')
|
|
1884
|
+
.description('Open the linked PR URL for a task')
|
|
1885
|
+
.option('--no-browser', "Don't auto-open browser; print URL only")
|
|
1886
|
+
.action(async function (taskId) {
|
|
1887
|
+
const opts = this.opts()
|
|
1888
|
+
try {
|
|
1889
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1890
|
+
const url = task?.contextUrl ? String(task.contextUrl).trim() : ''
|
|
1891
|
+
if (!url) return fail('This task has no linked PR URL. Use: wadah pr link <task-id> <pr-url>')
|
|
1892
|
+
|
|
1893
|
+
if (isMachineOutput()) {
|
|
1894
|
+
console.log(JSON.stringify({ taskId, prUrl: url }, null, runtimeJsonOutput ? 2 : 0))
|
|
1895
|
+
return
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
console.log(chalk.gray(`\nPR: ${url}`))
|
|
1899
|
+
if (opts.browser) {
|
|
1900
|
+
const opened = await openBrowser(url)
|
|
1901
|
+
if (opened) console.log(chalk.green('✓ Opened in browser\n'))
|
|
1902
|
+
else console.log(chalk.yellow('Could not open browser automatically. Open the URL manually.\n'))
|
|
1903
|
+
} else {
|
|
1904
|
+
console.log()
|
|
1905
|
+
}
|
|
1906
|
+
} catch (err) { handleError(err) }
|
|
1907
|
+
})
|
|
1908
|
+
|
|
1633
1909
|
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1634
1910
|
|
|
1635
1911
|
program
|
|
@@ -1662,7 +1938,7 @@ const agentTokenCmd = program.command('agent-token').description('Create or dele
|
|
|
1662
1938
|
|
|
1663
1939
|
agentTokenCmd
|
|
1664
1940
|
.command('create [name]')
|
|
1665
|
-
.description('Create an agent token; token is shown once — store it as
|
|
1941
|
+
.description('Create an agent token; token is shown once — store it as WADAH_AGENT_TOKEN')
|
|
1666
1942
|
.option('--name <text>', 'Token name (if not passed as argument)')
|
|
1667
1943
|
.option('--assignee-name <text>', 'Display name for the agent (default: same as name)')
|
|
1668
1944
|
.action(async function (nameArg) {
|
|
@@ -1680,9 +1956,9 @@ agentTokenCmd
|
|
|
1680
1956
|
console.log(chalk.yellow('\n Token (store it now — shown once):'))
|
|
1681
1957
|
console.log(` ${result.token}`)
|
|
1682
1958
|
console.log(chalk.gray('\n Store it so you don’t lose it:'))
|
|
1683
|
-
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret →
|
|
1684
|
-
console.log(chalk.gray(' · Cursor: Settings → Environment variables →
|
|
1685
|
-
console.log(chalk.gray(' · Shell: export
|
|
1959
|
+
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret → WADAH_AGENT_TOKEN'))
|
|
1960
|
+
console.log(chalk.gray(' · Cursor: Settings → Environment variables → WADAH_AGENT_TOKEN'))
|
|
1961
|
+
console.log(chalk.gray(' · Shell: export WADAH_AGENT_TOKEN="<paste token here>"\n'))
|
|
1686
1962
|
}
|
|
1687
1963
|
} catch (err) { handleError(err) }
|
|
1688
1964
|
})
|
|
@@ -2006,12 +2282,12 @@ program
|
|
|
2006
2282
|
addCheck('api_ping', 'fail', `API ping failed: ${err.message}`)
|
|
2007
2283
|
}
|
|
2008
2284
|
|
|
2009
|
-
const hasEnvToken = Boolean(
|
|
2285
|
+
const hasEnvToken = Boolean(envAgentToken() || runtimeToken)
|
|
2010
2286
|
const hasSavedToken = Boolean(confGet('access_token'))
|
|
2011
2287
|
if (!hasEnvToken && !hasSavedToken) {
|
|
2012
2288
|
addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
|
|
2013
2289
|
} else if (hasEnvToken) {
|
|
2014
|
-
addCheck('auth_token', 'pass',
|
|
2290
|
+
addCheck('auth_token', 'pass', `Using agent token from env/flag (${envAgentTokenVarName()})`)
|
|
2015
2291
|
} else {
|
|
2016
2292
|
addCheck('auth_token', 'pass', 'Using token from profile config')
|
|
2017
2293
|
}
|
|
@@ -2037,7 +2313,7 @@ program
|
|
|
2037
2313
|
addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
|
|
2038
2314
|
}
|
|
2039
2315
|
|
|
2040
|
-
// Agent-specific checks (only when
|
|
2316
|
+
// Agent-specific checks (only when an env agent token is set)
|
|
2041
2317
|
if (hasEnvToken && meBody) {
|
|
2042
2318
|
const isAgent = meBody.workspaces?.[0]?.role === 'agent'
|
|
2043
2319
|
if (!isAgent) {
|
|
@@ -2163,7 +2439,7 @@ program
|
|
|
2163
2439
|
const config = {
|
|
2164
2440
|
api_url: getApiBase(),
|
|
2165
2441
|
signed_in_as: confGet('user_email') ?? null,
|
|
2166
|
-
agent_mode: Boolean(
|
|
2442
|
+
agent_mode: Boolean(envAgentToken() || runtimeToken),
|
|
2167
2443
|
profile: getProfile(),
|
|
2168
2444
|
default_board: defaultBoard ?? null,
|
|
2169
2445
|
config_file: conf.path,
|
|
@@ -2176,7 +2452,7 @@ program
|
|
|
2176
2452
|
console.log(chalk.bold('CLI config'))
|
|
2177
2453
|
console.log(chalk.gray(` API URL: ${config.api_url}`))
|
|
2178
2454
|
console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
|
|
2179
|
-
console.log(chalk.gray(` Agent mode: ${config.agent_mode ?
|
|
2455
|
+
console.log(chalk.gray(` Agent mode: ${config.agent_mode ? `yes (${envAgentTokenVarName() ?? 'token flag'} set)` : 'no'}`))
|
|
2180
2456
|
console.log(chalk.gray(` Default board: ${defaultBoard ?? '— (not set, use: wadah config --default-board "Name")'}`))
|
|
2181
2457
|
console.log(chalk.gray(` Profile: ${getProfile()}`))
|
|
2182
2458
|
console.log(chalk.gray(` Config file: ${config.config_file}`))
|
|
@@ -2255,7 +2531,7 @@ const CLI_COMMANDS = [
|
|
|
2255
2531
|
'add', 'agent-token', 'agent-tokens', 'assign', 'assignee', 'assignees', 'board', 'board-view', 'boards',
|
|
2256
2532
|
'bucket', 'buckets', 'calendar', 'comment', 'complete', 'config', 'delete', 'doc', 'docs',
|
|
2257
2533
|
'do', 'doctor', 'files', 'folder', 'folders', 'invite', 'list', 'login', 'members', 'mkdir',
|
|
2258
|
-
'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
2534
|
+
'forget', 'memory', 'memory-export', 'memory-log', 'move', 'open', 'pr', 'repo', 'repos', 'reopen', 'remember', 'requested', 'search', 'setup-agent', 'signup', 'state', 'update', 'upload', 'view', 'whoami',
|
|
2259
2535
|
]
|
|
2260
2536
|
|
|
2261
2537
|
// ── wadah setup-agent ─────────────────────────────────────────────────────────
|
|
@@ -2265,7 +2541,7 @@ program
|
|
|
2265
2541
|
.description('Interactive wizard: create an agent token, write it to your shell profile, and verify everything works')
|
|
2266
2542
|
.option('--name <name>', 'Agent name (skip prompt)')
|
|
2267
2543
|
.option('--board <board>', 'Default board name (skip prompt)')
|
|
2268
|
-
.option('--skip-env', 'Skip writing
|
|
2544
|
+
.option('--skip-env', 'Skip writing WADAH_AGENT_TOKEN to shell profile (print instructions only)')
|
|
2269
2545
|
.action(async (opts) => {
|
|
2270
2546
|
const readline = await import('node:readline/promises')
|
|
2271
2547
|
const os = await import('node:os')
|
|
@@ -2329,18 +2605,18 @@ program
|
|
|
2329
2605
|
|
|
2330
2606
|
// Step 4: write env var to shell profile
|
|
2331
2607
|
console.log()
|
|
2332
|
-
console.log(chalk.bold('Step 4/5 Setting up
|
|
2608
|
+
console.log(chalk.bold('Step 4/5 Setting up WADAH_AGENT_TOKEN…'))
|
|
2333
2609
|
|
|
2334
2610
|
const shell = process.env.SHELL ?? ''
|
|
2335
2611
|
const profileMap = { zsh: '.zshrc', bash: '.bashrc', fish: '.config/fish/config.fish' }
|
|
2336
2612
|
const profileKey = Object.keys(profileMap).find((k) => shell.includes(k))
|
|
2337
2613
|
const profileFile = profileKey ? path.join(os.homedir(), profileMap[profileKey]) : null
|
|
2338
2614
|
const marker = '# added by wadah setup-agent'
|
|
2339
|
-
const exportLine = `\nexport
|
|
2615
|
+
const exportLine = `\nexport WADAH_AGENT_TOKEN="${rawToken}" ${marker}\n`
|
|
2340
2616
|
|
|
2341
2617
|
if (opts.skipEnv || !profileFile) {
|
|
2342
2618
|
console.log(chalk.gray(' Skipping automatic profile write. Add this line manually:'))
|
|
2343
|
-
console.log(chalk.gray(` export
|
|
2619
|
+
console.log(chalk.gray(` export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2344
2620
|
const rcGuess = shell.includes('zsh') ? '~/.zshrc' : shell.includes('fish') ? '~/.config/fish/config.fish' : '~/.bashrc'
|
|
2345
2621
|
console.log(chalk.gray(` Then reload: source ${rcGuess}`))
|
|
2346
2622
|
} else {
|
|
@@ -2350,7 +2626,7 @@ program
|
|
|
2350
2626
|
const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : ''
|
|
2351
2627
|
const filtered = existing
|
|
2352
2628
|
.split('\n')
|
|
2353
|
-
.filter((line) => !line.includes('
|
|
2629
|
+
.filter((line) => !line.includes('WADAH_AGENT_TOKEN=') && !line.includes(marker))
|
|
2354
2630
|
.join('\n')
|
|
2355
2631
|
const next = `${filtered.trimEnd()}${exportLine}`
|
|
2356
2632
|
fs.writeFileSync(profileFile, `${next.endsWith('\n') ? next : `${next}\n`}`)
|
|
@@ -2358,10 +2634,10 @@ program
|
|
|
2358
2634
|
console.log(chalk.gray(` Reload now: source ${profileFile}`))
|
|
2359
2635
|
} catch (e) {
|
|
2360
2636
|
console.log(chalk.yellow(` ⚠ Could not write to ${profileFile}: ${e.message}`))
|
|
2361
|
-
console.log(chalk.gray(` Add manually: export
|
|
2637
|
+
console.log(chalk.gray(` Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2362
2638
|
}
|
|
2363
2639
|
} else {
|
|
2364
|
-
console.log(chalk.gray(` Skipped. Add manually: export
|
|
2640
|
+
console.log(chalk.gray(` Skipped. Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2365
2641
|
}
|
|
2366
2642
|
}
|
|
2367
2643
|
|
package/package.json
CHANGED
package/test/cli.test.js
CHANGED
|
@@ -11,7 +11,7 @@ function runCli(args) {
|
|
|
11
11
|
return new Promise((resolvePromise, reject) => {
|
|
12
12
|
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
13
13
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
-
env: { ...process.env,
|
|
14
|
+
env: { ...process.env, WADAH_AGENT_TOKEN: '' },
|
|
15
15
|
})
|
|
16
16
|
let stdout = ''
|
|
17
17
|
let stderr = ''
|