open-wadah 1.2.3 → 1.3.1
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 +105 -53
- package/cli.js +182 -28
- package/package.json +1 -1
- package/test/cli.test.js +7 -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,92 +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`.
|
|
21
|
+
|
|
22
|
+
## 60-second quick start
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
1. Sign in:
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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`.
|
|
45
|
+
|
|
46
|
+
## Core workflows
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|--------|-------------|
|
|
29
|
-
| **Auth** | `login`, `signup`, `logout`, `whoami` |
|
|
30
|
-
| **Tasks** | `open`, `list`, `search`, `requested`, `add`, `complete`, `reopen`, `view`, `update`, `move`, `assign`, `comment`, `subtask list/add/toggle/delete`, `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]` |
|
|
48
|
+
### Daily task operations
|
|
39
49
|
|
|
40
|
-
|
|
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
|
+
```
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
### Dependencies and subtasks
|
|
43
59
|
|
|
44
60
|
```bash
|
|
61
|
+
wadah add "Ship release notes" --blocks <task-id>
|
|
45
62
|
wadah subtask add <task-id> "Write tests"
|
|
46
63
|
wadah subtask list <task-id>
|
|
47
64
|
wadah subtask toggle <task-id> <subtask-id>
|
|
48
|
-
wadah subtask delete <task-id> <subtask-id>
|
|
49
65
|
```
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
**Bash:**
|
|
67
|
+
### Board administration
|
|
54
68
|
|
|
55
69
|
```bash
|
|
56
|
-
wadah
|
|
57
|
-
|
|
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
|
|
58
74
|
```
|
|
59
75
|
|
|
60
|
-
|
|
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:
|
|
61
97
|
|
|
62
98
|
```bash
|
|
63
|
-
wadah
|
|
64
|
-
source ~/.zshrc
|
|
99
|
+
wadah agent-token create "My Agent"
|
|
65
100
|
```
|
|
66
101
|
|
|
67
|
-
|
|
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
|
+
```
|
|
68
109
|
|
|
69
|
-
|
|
70
|
-
- Use `--json` for machine-readable output: `wadah open --json`, `wadah list --json`, `wadah buckets --json`.
|
|
71
|
-
- Natural language: `wadah do "add a task to fix the bug"` (requires `OPENAI_API_KEY`).
|
|
72
|
-
- In Cursor/Claude/Kimi: see **WADAH_CLI.md** in the repo root for a short reference.
|
|
73
|
-
- For autonomous 24/7 agents with GitHub Actions: see **AGENTS.md** in the repo root.
|
|
110
|
+
`wadah do` requires `OPENAI_API_KEY`.
|
|
74
111
|
|
|
75
112
|
## Global flags
|
|
76
113
|
|
|
77
|
-
- `--api <url
|
|
78
|
-
- `--profile <name
|
|
79
|
-
- `--token <token
|
|
80
|
-
- `--json
|
|
81
|
-
- `--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
|
|
82
119
|
|
|
83
|
-
##
|
|
120
|
+
## Environment variables
|
|
84
121
|
|
|
85
|
-
|
|
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
|
|
86
137
|
|
|
87
138
|
```bash
|
|
88
|
-
npm
|
|
89
|
-
|
|
90
|
-
cd task-manager-cli && npm test
|
|
139
|
+
npm test
|
|
140
|
+
node cli.js --help
|
|
91
141
|
```
|
|
92
142
|
|
|
93
|
-
##
|
|
143
|
+
## JSON error codes
|
|
144
|
+
|
|
145
|
+
When running with `--json`, failures include structured error codes:
|
|
94
146
|
|
|
95
|
-
- `auth_error`
|
|
96
|
-
- `validation_error`
|
|
97
|
-
- `network_error`
|
|
98
|
-
- `api_error`
|
|
99
|
-
- `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()}`))
|
|
@@ -429,6 +442,9 @@ Available commands and their args (use these exact command names):
|
|
|
429
442
|
- repo remove: args = [repo id]. Unlink a repository.
|
|
430
443
|
- pr link: args = [task id, pr url]. Attach a PR URL to a task.
|
|
431
444
|
- pr open: args = [task id]. Open the linked PR URL for a task.
|
|
445
|
+
- link add: args = [task id, url]. Optional "--label", "<text>" (e.g. Vercel). Append a non-PR URL.
|
|
446
|
+
- link list: args = [task id]. List task URLs.
|
|
447
|
+
- link remove: args = [task id, link id]. Remove a link (id from link list --json).
|
|
432
448
|
|
|
433
449
|
If the request is unclear or not a valid CLI action, return {"command": "", "args": []}. Output only the JSON object.`
|
|
434
450
|
|
|
@@ -738,7 +754,7 @@ program
|
|
|
738
754
|
.action(async () => {
|
|
739
755
|
if (isMachineOutput()) {
|
|
740
756
|
try {
|
|
741
|
-
if (
|
|
757
|
+
if (envAgentToken() || runtimeToken) {
|
|
742
758
|
const me = await api('/api/auth/me')
|
|
743
759
|
const ws = me.workspaces?.[0] ?? null
|
|
744
760
|
console.log(JSON.stringify({
|
|
@@ -771,8 +787,8 @@ program
|
|
|
771
787
|
return
|
|
772
788
|
}
|
|
773
789
|
|
|
774
|
-
if (
|
|
775
|
-
console.log(chalk.bold('\nAgent') +
|
|
790
|
+
if (envAgentToken()) {
|
|
791
|
+
console.log(chalk.bold('\nAgent') + ` (${envAgentTokenVarName()})`)
|
|
776
792
|
} else {
|
|
777
793
|
const email = confGet('user_email')
|
|
778
794
|
if (!email) {
|
|
@@ -1252,7 +1268,16 @@ program
|
|
|
1252
1268
|
if (requester) console.log(chalk.gray(` Requested: ${requester}`))
|
|
1253
1269
|
if (task.dueDate) console.log(chalk.gray(` Due: ${formatDate(task.dueDate)}`))
|
|
1254
1270
|
if (task.repo) console.log(chalk.gray(` Repo: ${task.repo}`))
|
|
1255
|
-
if (task.
|
|
1271
|
+
if (Array.isArray(task.contextLinks) && task.contextLinks.length > 0) {
|
|
1272
|
+
console.log(chalk.gray('\n Links:'))
|
|
1273
|
+
task.contextLinks.forEach((l) => {
|
|
1274
|
+
const lab = l.label ? `${l.label}: ` : ''
|
|
1275
|
+
console.log(chalk.gray(` ${lab}${l.url}`))
|
|
1276
|
+
})
|
|
1277
|
+
if (task.contextUrl) console.log(chalk.gray(` Primary: ${task.contextUrl}`))
|
|
1278
|
+
} else if (task.contextUrl) {
|
|
1279
|
+
console.log(chalk.gray(` Link: ${task.contextUrl}`))
|
|
1280
|
+
}
|
|
1256
1281
|
if (task.tags?.length) {
|
|
1257
1282
|
const relTags = task.tags.filter((t) => t.startsWith('blocks:') || t.startsWith('blocked-by:'))
|
|
1258
1283
|
const plainTags = task.tags.filter((t) => !t.startsWith('blocks:') && !t.startsWith('blocked-by:'))
|
|
@@ -1471,13 +1496,42 @@ const boardCmd = program.command('board').description('Create or delete a board
|
|
|
1471
1496
|
boardCmd
|
|
1472
1497
|
.command('create <name>')
|
|
1473
1498
|
.description('Create a new board')
|
|
1474
|
-
.
|
|
1499
|
+
.option('--flow <columns>', 'Comma-separated column flow, e.g. "Backlog,Ready,Doing,QA,Done"')
|
|
1500
|
+
.action(async function (name) {
|
|
1475
1501
|
try {
|
|
1502
|
+
const opts = this.opts()
|
|
1476
1503
|
const board = await api('/api/boards', { method: 'POST', body: JSON.stringify({ name: String(name).trim() }) })
|
|
1504
|
+
const createdBuckets = []
|
|
1505
|
+
if (opts.flow) {
|
|
1506
|
+
const flowColumns = String(opts.flow)
|
|
1507
|
+
.split(',')
|
|
1508
|
+
.map((c) => c.trim())
|
|
1509
|
+
.filter(Boolean)
|
|
1510
|
+
if (flowColumns.length === 0) {
|
|
1511
|
+
return fail('Flow is empty. Provide comma-separated columns, e.g. --flow "Backlog,Doing,Done".')
|
|
1512
|
+
}
|
|
1513
|
+
for (const column of flowColumns) {
|
|
1514
|
+
if (!column) continue
|
|
1515
|
+
const bucket = await api('/api/buckets', {
|
|
1516
|
+
method: 'POST',
|
|
1517
|
+
body: JSON.stringify({ title: column, boardId: board.id }),
|
|
1518
|
+
})
|
|
1519
|
+
createdBuckets.push(bucket)
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1477
1522
|
if (isMachineOutput()) {
|
|
1478
|
-
console.log(JSON.stringify(
|
|
1523
|
+
console.log(JSON.stringify(
|
|
1524
|
+
{ ...board, flow: createdBuckets.map((b) => ({ id: b.id, title: b.title })) },
|
|
1525
|
+
null,
|
|
1526
|
+
runtimeJsonOutput ? 2 : 0
|
|
1527
|
+
))
|
|
1479
1528
|
} else {
|
|
1480
1529
|
console.log(chalk.green('\n✓ Board created') + ` ${chalk.bold(board.name)} ${chalk.gray(board.id?.slice(0, 8))}\n`)
|
|
1530
|
+
if (createdBuckets.length > 0) {
|
|
1531
|
+
console.log(chalk.gray(' Flow columns:'))
|
|
1532
|
+
createdBuckets.forEach((b) => console.log(` - ${b.title} ${chalk.gray(b.id?.slice(0, 8))}`))
|
|
1533
|
+
console.log()
|
|
1534
|
+
}
|
|
1481
1535
|
}
|
|
1482
1536
|
} catch (err) { handleError(err) }
|
|
1483
1537
|
})
|
|
@@ -1826,6 +1880,106 @@ prCmd
|
|
|
1826
1880
|
} catch (err) { handleError(err) }
|
|
1827
1881
|
})
|
|
1828
1882
|
|
|
1883
|
+
const linkCmd = program.command('link').description('Attach labeled URLs to tasks (Vercel, Figma, docs — without replacing the PR)')
|
|
1884
|
+
|
|
1885
|
+
function normalizeHttpUrlForLink(input) {
|
|
1886
|
+
let parsed
|
|
1887
|
+
try {
|
|
1888
|
+
parsed = new URL(String(input).trim())
|
|
1889
|
+
} catch {
|
|
1890
|
+
return null
|
|
1891
|
+
}
|
|
1892
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return null
|
|
1893
|
+
return parsed.toString()
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const MAX_TASK_LINKS = 20
|
|
1897
|
+
|
|
1898
|
+
linkCmd
|
|
1899
|
+
.command('add <taskId> <url>')
|
|
1900
|
+
.description('Append a URL to a task (use --label for display)')
|
|
1901
|
+
.option('--label <text>', 'Short label, e.g. Vercel, Figma', '')
|
|
1902
|
+
.action(async function (taskId, url) {
|
|
1903
|
+
const opts = this.opts()
|
|
1904
|
+
try {
|
|
1905
|
+
const normalizedUrl = normalizeHttpUrlForLink(url)
|
|
1906
|
+
if (!normalizedUrl) return fail('Provide a valid http(s) URL')
|
|
1907
|
+
|
|
1908
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1909
|
+
const existing = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1910
|
+
if (existing.length >= MAX_TASK_LINKS) {
|
|
1911
|
+
return fail(`A task can have at most ${MAX_TASK_LINKS} links. Remove one with: wadah link remove <task-id> <link-id>`)
|
|
1912
|
+
}
|
|
1913
|
+
const label = String(opts.label ?? '').trim().slice(0, 64)
|
|
1914
|
+
const next = [...existing, { id: randomUUID(), label, url: normalizedUrl }]
|
|
1915
|
+
|
|
1916
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1917
|
+
method: 'PATCH',
|
|
1918
|
+
body: JSON.stringify({ contextLinks: next }),
|
|
1919
|
+
})
|
|
1920
|
+
|
|
1921
|
+
if (isMachineOutput()) {
|
|
1922
|
+
console.log(JSON.stringify({ taskId, url: normalizedUrl, label, added: true, links: next }, null, runtimeJsonOutput ? 2 : 0))
|
|
1923
|
+
} else {
|
|
1924
|
+
console.log(chalk.green('\n✓ Link added') + ` ${chalk.bold(task.title)}`)
|
|
1925
|
+
console.log(chalk.gray(` ${label ? `${label} · ` : ''}${normalizedUrl}\n`))
|
|
1926
|
+
}
|
|
1927
|
+
} catch (err) { handleError(err) }
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
linkCmd
|
|
1931
|
+
.command('list <taskId>')
|
|
1932
|
+
.description('List URLs attached to a task')
|
|
1933
|
+
.action(async (taskId) => {
|
|
1934
|
+
try {
|
|
1935
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1936
|
+
const links = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1937
|
+
if (isMachineOutput()) {
|
|
1938
|
+
console.log(JSON.stringify({ taskId, links, contextUrl: task.contextUrl ?? null }, null, runtimeJsonOutput ? 2 : 0))
|
|
1939
|
+
return
|
|
1940
|
+
}
|
|
1941
|
+
if (links.length === 0) {
|
|
1942
|
+
console.log(chalk.gray('\nNo links on this task. Add one: wadah link add <task-id> <url> --label Vercel\n'))
|
|
1943
|
+
return
|
|
1944
|
+
}
|
|
1945
|
+
console.log()
|
|
1946
|
+
links.forEach((l) => {
|
|
1947
|
+
const lab = l.label ? chalk.cyan(l.label) : chalk.gray('link')
|
|
1948
|
+
console.log(` ${chalk.dim(l.id)} ${lab}`)
|
|
1949
|
+
console.log(chalk.gray(` ${l.url}`))
|
|
1950
|
+
})
|
|
1951
|
+
if (task.contextUrl) {
|
|
1952
|
+
console.log(chalk.gray(`\n Primary (e.g. wadah pr open): ${task.contextUrl}\n`))
|
|
1953
|
+
} else {
|
|
1954
|
+
console.log()
|
|
1955
|
+
}
|
|
1956
|
+
} catch (err) { handleError(err) }
|
|
1957
|
+
})
|
|
1958
|
+
|
|
1959
|
+
linkCmd
|
|
1960
|
+
.command('remove <taskId> <linkId>')
|
|
1961
|
+
.description('Remove a link by id (from wadah link list --json)')
|
|
1962
|
+
.action(async (taskId, linkId) => {
|
|
1963
|
+
try {
|
|
1964
|
+
const task = await api(`/api/tasks/${taskId}`)
|
|
1965
|
+
const existing = Array.isArray(task.contextLinks) ? task.contextLinks : []
|
|
1966
|
+
const needle = String(linkId).trim()
|
|
1967
|
+
const next = existing.filter((l) => l.id !== needle)
|
|
1968
|
+
if (next.length === existing.length) {
|
|
1969
|
+
return fail(`No link with id matching "${needle}". Use: wadah link list ${taskId} --json`)
|
|
1970
|
+
}
|
|
1971
|
+
await api(`/api/tasks/${taskId}`, {
|
|
1972
|
+
method: 'PATCH',
|
|
1973
|
+
body: JSON.stringify({ contextLinks: next }),
|
|
1974
|
+
})
|
|
1975
|
+
if (isMachineOutput()) {
|
|
1976
|
+
console.log(JSON.stringify({ taskId, removed: needle, links: next }, null, runtimeJsonOutput ? 2 : 0))
|
|
1977
|
+
} else {
|
|
1978
|
+
console.log(chalk.green('\n✓ Link removed') + ` ${chalk.bold(task.title)}\n`)
|
|
1979
|
+
}
|
|
1980
|
+
} catch (err) { handleError(err) }
|
|
1981
|
+
})
|
|
1982
|
+
|
|
1829
1983
|
// ── agent tokens ──────────────────────────────────────────────────────────────
|
|
1830
1984
|
|
|
1831
1985
|
program
|
|
@@ -1858,7 +2012,7 @@ const agentTokenCmd = program.command('agent-token').description('Create or dele
|
|
|
1858
2012
|
|
|
1859
2013
|
agentTokenCmd
|
|
1860
2014
|
.command('create [name]')
|
|
1861
|
-
.description('Create an agent token; token is shown once — store it as
|
|
2015
|
+
.description('Create an agent token; token is shown once — store it as WADAH_AGENT_TOKEN')
|
|
1862
2016
|
.option('--name <text>', 'Token name (if not passed as argument)')
|
|
1863
2017
|
.option('--assignee-name <text>', 'Display name for the agent (default: same as name)')
|
|
1864
2018
|
.action(async function (nameArg) {
|
|
@@ -1876,9 +2030,9 @@ agentTokenCmd
|
|
|
1876
2030
|
console.log(chalk.yellow('\n Token (store it now — shown once):'))
|
|
1877
2031
|
console.log(` ${result.token}`)
|
|
1878
2032
|
console.log(chalk.gray('\n Store it so you don’t lose it:'))
|
|
1879
|
-
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret →
|
|
1880
|
-
console.log(chalk.gray(' · Cursor: Settings → Environment variables →
|
|
1881
|
-
console.log(chalk.gray(' · Shell: export
|
|
2033
|
+
console.log(chalk.gray(' · GitHub Actions: Repo → Settings → Secrets and variables → Actions → New secret → WADAH_AGENT_TOKEN'))
|
|
2034
|
+
console.log(chalk.gray(' · Cursor: Settings → Environment variables → WADAH_AGENT_TOKEN'))
|
|
2035
|
+
console.log(chalk.gray(' · Shell: export WADAH_AGENT_TOKEN="<paste token here>"\n'))
|
|
1882
2036
|
}
|
|
1883
2037
|
} catch (err) { handleError(err) }
|
|
1884
2038
|
})
|
|
@@ -2202,12 +2356,12 @@ program
|
|
|
2202
2356
|
addCheck('api_ping', 'fail', `API ping failed: ${err.message}`)
|
|
2203
2357
|
}
|
|
2204
2358
|
|
|
2205
|
-
const hasEnvToken = Boolean(
|
|
2359
|
+
const hasEnvToken = Boolean(envAgentToken() || runtimeToken)
|
|
2206
2360
|
const hasSavedToken = Boolean(confGet('access_token'))
|
|
2207
2361
|
if (!hasEnvToken && !hasSavedToken) {
|
|
2208
2362
|
addCheck('auth_token', 'warn', 'No token available. Run wadah login.')
|
|
2209
2363
|
} else if (hasEnvToken) {
|
|
2210
|
-
addCheck('auth_token', 'pass',
|
|
2364
|
+
addCheck('auth_token', 'pass', `Using agent token from env/flag (${envAgentTokenVarName()})`)
|
|
2211
2365
|
} else {
|
|
2212
2366
|
addCheck('auth_token', 'pass', 'Using token from profile config')
|
|
2213
2367
|
}
|
|
@@ -2233,7 +2387,7 @@ program
|
|
|
2233
2387
|
addCheck('auth_me', 'warn', `Auth check failed: ${err.message}`)
|
|
2234
2388
|
}
|
|
2235
2389
|
|
|
2236
|
-
// Agent-specific checks (only when
|
|
2390
|
+
// Agent-specific checks (only when an env agent token is set)
|
|
2237
2391
|
if (hasEnvToken && meBody) {
|
|
2238
2392
|
const isAgent = meBody.workspaces?.[0]?.role === 'agent'
|
|
2239
2393
|
if (!isAgent) {
|
|
@@ -2359,7 +2513,7 @@ program
|
|
|
2359
2513
|
const config = {
|
|
2360
2514
|
api_url: getApiBase(),
|
|
2361
2515
|
signed_in_as: confGet('user_email') ?? null,
|
|
2362
|
-
agent_mode: Boolean(
|
|
2516
|
+
agent_mode: Boolean(envAgentToken() || runtimeToken),
|
|
2363
2517
|
profile: getProfile(),
|
|
2364
2518
|
default_board: defaultBoard ?? null,
|
|
2365
2519
|
config_file: conf.path,
|
|
@@ -2372,7 +2526,7 @@ program
|
|
|
2372
2526
|
console.log(chalk.bold('CLI config'))
|
|
2373
2527
|
console.log(chalk.gray(` API URL: ${config.api_url}`))
|
|
2374
2528
|
console.log(chalk.gray(` Signed in as: ${config.signed_in_as ?? '—'}`))
|
|
2375
|
-
console.log(chalk.gray(` Agent mode: ${config.agent_mode ?
|
|
2529
|
+
console.log(chalk.gray(` Agent mode: ${config.agent_mode ? `yes (${envAgentTokenVarName() ?? 'token flag'} set)` : 'no'}`))
|
|
2376
2530
|
console.log(chalk.gray(` Default board: ${defaultBoard ?? '— (not set, use: wadah config --default-board "Name")'}`))
|
|
2377
2531
|
console.log(chalk.gray(` Profile: ${getProfile()}`))
|
|
2378
2532
|
console.log(chalk.gray(` Config file: ${config.config_file}`))
|
|
@@ -2461,7 +2615,7 @@ program
|
|
|
2461
2615
|
.description('Interactive wizard: create an agent token, write it to your shell profile, and verify everything works')
|
|
2462
2616
|
.option('--name <name>', 'Agent name (skip prompt)')
|
|
2463
2617
|
.option('--board <board>', 'Default board name (skip prompt)')
|
|
2464
|
-
.option('--skip-env', 'Skip writing
|
|
2618
|
+
.option('--skip-env', 'Skip writing WADAH_AGENT_TOKEN to shell profile (print instructions only)')
|
|
2465
2619
|
.action(async (opts) => {
|
|
2466
2620
|
const readline = await import('node:readline/promises')
|
|
2467
2621
|
const os = await import('node:os')
|
|
@@ -2525,18 +2679,18 @@ program
|
|
|
2525
2679
|
|
|
2526
2680
|
// Step 4: write env var to shell profile
|
|
2527
2681
|
console.log()
|
|
2528
|
-
console.log(chalk.bold('Step 4/5 Setting up
|
|
2682
|
+
console.log(chalk.bold('Step 4/5 Setting up WADAH_AGENT_TOKEN…'))
|
|
2529
2683
|
|
|
2530
2684
|
const shell = process.env.SHELL ?? ''
|
|
2531
2685
|
const profileMap = { zsh: '.zshrc', bash: '.bashrc', fish: '.config/fish/config.fish' }
|
|
2532
2686
|
const profileKey = Object.keys(profileMap).find((k) => shell.includes(k))
|
|
2533
2687
|
const profileFile = profileKey ? path.join(os.homedir(), profileMap[profileKey]) : null
|
|
2534
2688
|
const marker = '# added by wadah setup-agent'
|
|
2535
|
-
const exportLine = `\nexport
|
|
2689
|
+
const exportLine = `\nexport WADAH_AGENT_TOKEN="${rawToken}" ${marker}\n`
|
|
2536
2690
|
|
|
2537
2691
|
if (opts.skipEnv || !profileFile) {
|
|
2538
2692
|
console.log(chalk.gray(' Skipping automatic profile write. Add this line manually:'))
|
|
2539
|
-
console.log(chalk.gray(` export
|
|
2693
|
+
console.log(chalk.gray(` export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2540
2694
|
const rcGuess = shell.includes('zsh') ? '~/.zshrc' : shell.includes('fish') ? '~/.config/fish/config.fish' : '~/.bashrc'
|
|
2541
2695
|
console.log(chalk.gray(` Then reload: source ${rcGuess}`))
|
|
2542
2696
|
} else {
|
|
@@ -2546,7 +2700,7 @@ program
|
|
|
2546
2700
|
const existing = fs.existsSync(profileFile) ? fs.readFileSync(profileFile, 'utf8') : ''
|
|
2547
2701
|
const filtered = existing
|
|
2548
2702
|
.split('\n')
|
|
2549
|
-
.filter((line) => !line.includes('
|
|
2703
|
+
.filter((line) => !line.includes('WADAH_AGENT_TOKEN=') && !line.includes(marker))
|
|
2550
2704
|
.join('\n')
|
|
2551
2705
|
const next = `${filtered.trimEnd()}${exportLine}`
|
|
2552
2706
|
fs.writeFileSync(profileFile, `${next.endsWith('\n') ? next : `${next}\n`}`)
|
|
@@ -2554,10 +2708,10 @@ program
|
|
|
2554
2708
|
console.log(chalk.gray(` Reload now: source ${profileFile}`))
|
|
2555
2709
|
} catch (e) {
|
|
2556
2710
|
console.log(chalk.yellow(` ⚠ Could not write to ${profileFile}: ${e.message}`))
|
|
2557
|
-
console.log(chalk.gray(` Add manually: export
|
|
2711
|
+
console.log(chalk.gray(` Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2558
2712
|
}
|
|
2559
2713
|
} else {
|
|
2560
|
-
console.log(chalk.gray(` Skipped. Add manually: export
|
|
2714
|
+
console.log(chalk.gray(` Skipped. Add manually: export WADAH_AGENT_TOKEN="${rawToken}"`))
|
|
2561
2715
|
}
|
|
2562
2716
|
}
|
|
2563
2717
|
|
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 = ''
|
|
@@ -47,6 +47,12 @@ test('wadah open --help shows open command help', async () => {
|
|
|
47
47
|
assert.match(stdout, /open|List/)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('wadah link --help shows link subcommands', async () => {
|
|
51
|
+
const { code, stdout } = await runCli(['link', '--help'])
|
|
52
|
+
assert.equal(code, 0)
|
|
53
|
+
assert.match(stdout, /add|list|remove/)
|
|
54
|
+
})
|
|
55
|
+
|
|
50
56
|
test('wadah doctor runs without crashing', async () => {
|
|
51
57
|
const { code } = await runCli(['doctor'])
|
|
52
58
|
// Doctor may exit 0 or 1 depending on API/token; we only assert it doesn't throw
|