@krl-grn/wande 0.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 ADDED
@@ -0,0 +1,95 @@
1
+ # wande CLI
2
+
3
+ Command-line interface for automating your Wande Task Manager workspace through bot tokens.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18+
8
+ - A valid bot token generated in **Settings -> Bot API**
9
+ - Your Convex HTTP URL (for example: `https://<deployment>.convex.site`)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -g @krl-grn/wande
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ wande login <BOT_TOKEN>
21
+ wande whoami --json
22
+ ```
23
+
24
+ ## Core commands
25
+
26
+ ### Tasks
27
+
28
+ ```bash
29
+ wande tasks:list --limit 20 --json
30
+ wande tasks:create --title "Task from CLI" --status new --json
31
+ wande tasks:status --id <TASK_ID> --status done --json
32
+ ```
33
+
34
+ ### Routines
35
+
36
+ ```bash
37
+ wande routines:list --limit 20 --json
38
+ wande routines:create --title "Routine from CLI" --status new --json
39
+ wande routines:status --id <ROUTINE_ID> --status done --json
40
+ ```
41
+
42
+ ### Projects
43
+
44
+ ```bash
45
+ wande projects:list --json
46
+ wande projects:create --title "CLI Project" --json
47
+ ```
48
+
49
+ ### Queue instances
50
+
51
+ ```bash
52
+ wande queue:list --date 2026-02-23 --json
53
+ wande queue:create --date 2026-02-23 --entity-id e1 --entity-type task --title "Queue item" --status new --json
54
+ wande queue:status --instance-id <INSTANCE_ID> --status done --json
55
+ ```
56
+
57
+ ## Security notes
58
+
59
+ - Bot tokens are shown only once when created. Save them securely.
60
+ - If a token is exposed, revoke it immediately in **Settings -> Bot API** and create a new one.
61
+ - The CLI stores token config locally for your user profile.
62
+
63
+ ## API behavior and limits
64
+
65
+ - Supported entities: `tasks`, `routines`, `projects`, `queueInstances`
66
+ - Bot is allowed to:
67
+ - read tasks/routines/projects/queueInstances
68
+ - create tasks/routines/projects/queueInstances
69
+ - update status for tasks/routines/queueInstances
70
+ - Bot is not allowed to:
71
+ - delete entities
72
+ - reorder entities
73
+ - change date/time/order for existing queue instances
74
+ - Rate limits (server-side):
75
+ - `60 req/min` per token
76
+ - `20 create/min` per token
77
+ - `200 create/day` per token
78
+
79
+ ## Troubleshooting
80
+
81
+ - `401 UNAUTHORIZED`: token is invalid, expired, or revoked.
82
+ - `403 FORBIDDEN`: token is missing required scopes.
83
+ - `429 RATE_LIMITED`: you exceeded rate limits; retry later.
84
+ - `Could not find public function ...`: run Convex dev/deploy so bot functions are available.
85
+ - `login` resolves URL automatically from `https://wande.app/api/cli/config`. If needed, override with `--url`.
86
+
87
+ ## Development
88
+
89
+ From repository root:
90
+
91
+ ```bash
92
+ npm run cli:install
93
+ npm run cli:build
94
+ node packages/cli/dist/index.js --help
95
+ ```
package/dist/client.js ADDED
@@ -0,0 +1,27 @@
1
+ export class BotApiClient {
2
+ baseUrl;
3
+ constructor(baseUrl) {
4
+ this.baseUrl = baseUrl;
5
+ }
6
+ async request(path, options) {
7
+ const headers = {
8
+ Authorization: `Bearer ${options.token}`,
9
+ "Content-Type": "application/json",
10
+ };
11
+ if (options.idempotencyKey) {
12
+ headers["X-Idempotency-Key"] = options.idempotencyKey;
13
+ }
14
+ const response = await fetch(`${this.baseUrl}${path}`, {
15
+ method: options.method ?? "GET",
16
+ headers,
17
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
18
+ });
19
+ const raw = await response.text();
20
+ const parsed = raw ? JSON.parse(raw) : null;
21
+ if (!response.ok) {
22
+ const message = parsed?.error?.message ?? parsed?.message ?? `Request failed (${response.status})`;
23
+ throw new Error(message);
24
+ }
25
+ return parsed;
26
+ }
27
+ }
package/dist/config.js ADDED
@@ -0,0 +1,69 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ function configDir() {
5
+ return path.join(os.homedir(), ".taskmanager-cli");
6
+ }
7
+ function configPath() {
8
+ return path.join(configDir(), "config.json");
9
+ }
10
+ export async function loadConfig() {
11
+ try {
12
+ const raw = await readFile(configPath(), "utf8");
13
+ return JSON.parse(raw);
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ export async function saveConfig(next) {
20
+ await mkdir(configDir(), { recursive: true });
21
+ await writeFile(configPath(), JSON.stringify(next, null, 2), "utf8");
22
+ }
23
+ export async function clearToken() {
24
+ const cfg = await loadConfig();
25
+ delete cfg.token;
26
+ await saveConfig(cfg);
27
+ }
28
+ export function defaultBaseUrl() {
29
+ return process.env.TASKMANAGER_BOT_BASE_URL ?? "http://127.0.0.1:3210";
30
+ }
31
+ function normalizeBaseUrl(input) {
32
+ return input.replace(/\/+$/, "");
33
+ }
34
+ function candidateSiteUrls() {
35
+ const candidates = [
36
+ process.env.TASKMANAGER_SITE_URL,
37
+ "https://wande.app",
38
+ ].filter((value) => Boolean(value && value.trim().length > 0));
39
+ return Array.from(new Set(candidates.map(normalizeBaseUrl)));
40
+ }
41
+ export async function resolveBotBaseUrl(explicitUrl, existingUrl) {
42
+ if (explicitUrl) {
43
+ return normalizeBaseUrl(explicitUrl);
44
+ }
45
+ if (existingUrl) {
46
+ return normalizeBaseUrl(existingUrl);
47
+ }
48
+ if (process.env.TASKMANAGER_BOT_BASE_URL) {
49
+ return normalizeBaseUrl(process.env.TASKMANAGER_BOT_BASE_URL);
50
+ }
51
+ for (const siteUrl of candidateSiteUrls()) {
52
+ try {
53
+ const response = await fetch(`${siteUrl}/api/cli/config`, {
54
+ method: "GET",
55
+ });
56
+ if (!response.ok) {
57
+ continue;
58
+ }
59
+ const json = (await response.json());
60
+ if (json.botBaseUrl && json.botBaseUrl.trim().length > 0) {
61
+ return normalizeBaseUrl(json.botBaseUrl);
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore bootstrap failures and continue trying fallbacks.
66
+ }
67
+ }
68
+ return defaultBaseUrl();
69
+ }
package/dist/index.js ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { randomUUID } from "node:crypto";
4
+ import { BotApiClient } from "./client.js";
5
+ import { clearToken, loadConfig, resolveBotBaseUrl, saveConfig } from "./config.js";
6
+ import { printData } from "./output.js";
7
+ async function getRuntime() {
8
+ const config = await loadConfig();
9
+ const baseUrl = await resolveBotBaseUrl(undefined, config.baseUrl);
10
+ const token = config.token;
11
+ if (!token) {
12
+ throw new Error("Not authenticated. Run: wande login <token>");
13
+ }
14
+ return {
15
+ config,
16
+ token,
17
+ client: new BotApiClient(baseUrl),
18
+ };
19
+ }
20
+ async function performLogin(token, explicitUrl) {
21
+ const current = await loadConfig();
22
+ const baseUrl = await resolveBotBaseUrl(explicitUrl, current.baseUrl);
23
+ await saveConfig({
24
+ ...current,
25
+ token,
26
+ baseUrl,
27
+ });
28
+ console.log(`Saved bot token (base URL: ${baseUrl})`);
29
+ }
30
+ const program = new Command();
31
+ program.name("wande").description("Wande bot CLI");
32
+ program
33
+ .command("login <token>")
34
+ .description("Save bot token and auto-resolve API URL")
35
+ .option("--url <baseUrl>")
36
+ .action(async (token, options) => {
37
+ await performLogin(token, options.url);
38
+ });
39
+ program
40
+ .command("whoami")
41
+ .description("Show authenticated bot identity")
42
+ .option("--json", "JSON output", false)
43
+ .action(async (opts) => {
44
+ const { client, token } = await getRuntime();
45
+ const res = await client.request("/bot/auth/whoami", {
46
+ method: "POST",
47
+ token,
48
+ body: {},
49
+ });
50
+ printData(res.data, Boolean(opts.json));
51
+ });
52
+ program
53
+ .command("logout")
54
+ .description("Remove saved bot token")
55
+ .action(async () => {
56
+ await clearToken();
57
+ console.log("Token removed");
58
+ });
59
+ program
60
+ .command("auth:login")
61
+ .description("Legacy alias for login")
62
+ .requiredOption("--token <token>")
63
+ .option("--url <baseUrl>")
64
+ .action(async (options) => {
65
+ await performLogin(options.token, options.url);
66
+ });
67
+ program.command("auth:logout").description("Legacy alias for logout").action(async () => {
68
+ await clearToken();
69
+ console.log("Token removed");
70
+ });
71
+ program.command("auth:whoami").description("Legacy alias for whoami").option("--json", "JSON output", false).action(async (opts) => {
72
+ const { client, token } = await getRuntime();
73
+ const res = await client.request("/bot/auth/whoami", {
74
+ method: "POST",
75
+ token,
76
+ body: {},
77
+ });
78
+ printData(res.data, Boolean(opts.json));
79
+ });
80
+ program
81
+ .command("tasks:list")
82
+ .option("--status <status>")
83
+ .option("--limit <limit>")
84
+ .option("--json", "JSON output", false)
85
+ .action(async (opts) => {
86
+ const { client, token } = await getRuntime();
87
+ const params = new URLSearchParams();
88
+ if (opts.status)
89
+ params.set("status", String(opts.status));
90
+ if (opts.limit)
91
+ params.set("limit", String(opts.limit));
92
+ const suffix = params.toString() ? `?${params.toString()}` : "";
93
+ const res = await client.request(`/bot/tasks${suffix}`, {
94
+ token,
95
+ });
96
+ printData(res.data, Boolean(opts.json));
97
+ });
98
+ program
99
+ .command("tasks:create")
100
+ .requiredOption("--title <title>")
101
+ .option("--content <content>")
102
+ .option("--project-id <projectId>")
103
+ .option("--status <status>")
104
+ .option("--idempotency-key <key>")
105
+ .option("--json", "JSON output", false)
106
+ .action(async (opts) => {
107
+ const { client, token } = await getRuntime();
108
+ const res = await client.request("/bot/tasks", {
109
+ method: "POST",
110
+ token,
111
+ idempotencyKey: opts.idempotencyKey ?? randomUUID(),
112
+ body: {
113
+ title: opts.title,
114
+ content: opts.content,
115
+ project_id: opts.projectId,
116
+ status: opts.status,
117
+ },
118
+ });
119
+ printData(res.data, Boolean(opts.json));
120
+ });
121
+ program
122
+ .command("tasks:status")
123
+ .requiredOption("--id <taskId>")
124
+ .requiredOption("--status <status>")
125
+ .option("--json", "JSON output", false)
126
+ .action(async (opts) => {
127
+ const { client, token } = await getRuntime();
128
+ const res = await client.request(`/bot/tasks/${opts.id}/status`, {
129
+ method: "POST",
130
+ token,
131
+ body: { status: opts.status },
132
+ });
133
+ printData(res.data, Boolean(opts.json));
134
+ });
135
+ program
136
+ .command("routines:list")
137
+ .option("--status <status>")
138
+ .option("--limit <limit>")
139
+ .option("--json", "JSON output", false)
140
+ .action(async (opts) => {
141
+ const { client, token } = await getRuntime();
142
+ const params = new URLSearchParams();
143
+ if (opts.status)
144
+ params.set("status", String(opts.status));
145
+ if (opts.limit)
146
+ params.set("limit", String(opts.limit));
147
+ const suffix = params.toString() ? `?${params.toString()}` : "";
148
+ const res = await client.request(`/bot/routines${suffix}`, {
149
+ token,
150
+ });
151
+ printData(res.data, Boolean(opts.json));
152
+ });
153
+ program
154
+ .command("routines:create")
155
+ .requiredOption("--title <title>")
156
+ .option("--content <content>")
157
+ .option("--project-id <projectId>")
158
+ .option("--status <status>")
159
+ .option("--idempotency-key <key>")
160
+ .option("--json", "JSON output", false)
161
+ .action(async (opts) => {
162
+ const { client, token } = await getRuntime();
163
+ const res = await client.request("/bot/routines", {
164
+ method: "POST",
165
+ token,
166
+ idempotencyKey: opts.idempotencyKey ?? randomUUID(),
167
+ body: {
168
+ title: opts.title,
169
+ content: opts.content,
170
+ project_id: opts.projectId,
171
+ status: opts.status,
172
+ },
173
+ });
174
+ printData(res.data, Boolean(opts.json));
175
+ });
176
+ program
177
+ .command("routines:status")
178
+ .requiredOption("--id <routineId>")
179
+ .requiredOption("--status <status>")
180
+ .option("--json", "JSON output", false)
181
+ .action(async (opts) => {
182
+ const { client, token } = await getRuntime();
183
+ const res = await client.request(`/bot/routines/${opts.id}/status`, {
184
+ method: "POST",
185
+ token,
186
+ body: { status: opts.status },
187
+ });
188
+ printData(res.data, Boolean(opts.json));
189
+ });
190
+ program
191
+ .command("projects:list")
192
+ .option("--limit <limit>")
193
+ .option("--json", "JSON output", false)
194
+ .action(async (opts) => {
195
+ const { client, token } = await getRuntime();
196
+ const params = new URLSearchParams();
197
+ if (opts.limit)
198
+ params.set("limit", String(opts.limit));
199
+ const suffix = params.toString() ? `?${params.toString()}` : "";
200
+ const res = await client.request(`/bot/projects${suffix}`, {
201
+ token,
202
+ });
203
+ printData(res.data, Boolean(opts.json));
204
+ });
205
+ program
206
+ .command("projects:create")
207
+ .requiredOption("--title <title>")
208
+ .option("--color <color>")
209
+ .option("--idempotency-key <key>")
210
+ .option("--json", "JSON output", false)
211
+ .action(async (opts) => {
212
+ const { client, token } = await getRuntime();
213
+ const res = await client.request("/bot/projects", {
214
+ method: "POST",
215
+ token,
216
+ idempotencyKey: opts.idempotencyKey ?? randomUUID(),
217
+ body: {
218
+ title: opts.title,
219
+ color: opts.color,
220
+ },
221
+ });
222
+ printData(res.data, Boolean(opts.json));
223
+ });
224
+ program
225
+ .command("queue:list")
226
+ .option("--date <date>")
227
+ .option("--limit <limit>")
228
+ .option("--json", "JSON output", false)
229
+ .action(async (opts) => {
230
+ const { client, token } = await getRuntime();
231
+ const params = new URLSearchParams();
232
+ if (opts.date)
233
+ params.set("date", String(opts.date));
234
+ if (opts.limit)
235
+ params.set("limit", String(opts.limit));
236
+ const suffix = params.toString() ? `?${params.toString()}` : "";
237
+ const res = await client.request(`/bot/queue${suffix}`, {
238
+ token,
239
+ });
240
+ printData(res.data, Boolean(opts.json));
241
+ });
242
+ program
243
+ .command("queue:create")
244
+ .requiredOption("--date <date>")
245
+ .requiredOption("--entity-id <entityId>")
246
+ .requiredOption("--entity-type <entityType>")
247
+ .requiredOption("--title <title>")
248
+ .option("--project-id <projectId>")
249
+ .option("--description <description>")
250
+ .option("--status <status>", "new")
251
+ .option("--idempotency-key <key>")
252
+ .option("--json", "JSON output", false)
253
+ .action(async (opts) => {
254
+ const { client, token } = await getRuntime();
255
+ const res = await client.request("/bot/queue/instances", {
256
+ method: "POST",
257
+ token,
258
+ idempotencyKey: opts.idempotencyKey ?? randomUUID(),
259
+ body: {
260
+ date: opts.date,
261
+ instance: {
262
+ entity_id: opts.entityId,
263
+ entity_type: opts.entityType,
264
+ title: opts.title,
265
+ project_id: opts.projectId,
266
+ description: opts.description,
267
+ status: opts.status,
268
+ },
269
+ },
270
+ });
271
+ printData(res.data, Boolean(opts.json));
272
+ });
273
+ program
274
+ .command("queue:status")
275
+ .requiredOption("--instance-id <instanceId>")
276
+ .requiredOption("--status <status>")
277
+ .option("--json", "JSON output", false)
278
+ .action(async (opts) => {
279
+ const { client, token } = await getRuntime();
280
+ const res = await client.request(`/bot/queue/instances/${opts.instanceId}/status`, {
281
+ method: "POST",
282
+ token,
283
+ body: { status: opts.status },
284
+ });
285
+ printData(res.data, Boolean(opts.json));
286
+ });
287
+ program.parseAsync().catch((error) => {
288
+ console.error(error instanceof Error ? error.message : String(error));
289
+ process.exitCode = 1;
290
+ });
package/dist/output.js ADDED
@@ -0,0 +1,17 @@
1
+ export function printData(payload, asJson) {
2
+ if (asJson) {
3
+ console.log(JSON.stringify(payload, null, 2));
4
+ return;
5
+ }
6
+ if (Array.isArray(payload)) {
7
+ if (payload.length === 0) {
8
+ console.log("No items");
9
+ return;
10
+ }
11
+ for (const item of payload) {
12
+ console.log(JSON.stringify(item));
13
+ }
14
+ return;
15
+ }
16
+ console.log(JSON.stringify(payload, null, 2));
17
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@krl-grn/wande",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "bin": {
11
+ "wande": "./dist/index.js",
12
+ "tm": "./dist/index.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "dev": "node --loader ts-node/esm src/index.ts"
17
+ },
18
+ "dependencies": {
19
+ "commander": "^14.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.19.20",
23
+ "ts-node": "^10.9.2",
24
+ "typescript": "^5.9.3"
25
+ }
26
+ }