ai-todo-cli 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.
Files changed (3) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +284 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # ai-todo-cli
2
+
3
+ CLI tool for AI agents to interact with [ai-todo](https://ai-todo.stringzhao.life).
4
+
5
+ All commands are dynamically discovered from the server. All output is JSON.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g ai-todo-cli
11
+ ```
12
+
13
+ ## Login
14
+
15
+ ```bash
16
+ ai-todo login
17
+ ```
18
+
19
+ For headless environments:
20
+
21
+ ```bash
22
+ ai-todo login --token <jwt>
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ ai-todo tasks:list
29
+ ai-todo tasks:list --filter today
30
+ ai-todo tasks:create --title "Review PR" --priority 1
31
+ ai-todo tasks:complete --id <task-id>
32
+ ai-todo tasks:delete --id <task-id>
33
+ ai-todo tasks:add-log --id <task-id> --content "Done with phase 1"
34
+ ai-todo spaces:list
35
+ ```
36
+
37
+ Run `ai-todo --help` to see all available commands (fetched from server).
38
+
39
+ ## For AI Agents
40
+
41
+ This CLI is designed for AI agent integration. Key features:
42
+
43
+ - All output is structured JSON
44
+ - Exit codes: 0 = success, 1 = error, 2 = auth required
45
+ - Commands are dynamically loaded from `/api/manifest`
46
+ - No interactive prompts — all input via flags
47
+
48
+ ## License
49
+
50
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/auth.ts
7
+ import { createServer } from "http";
8
+ import { randomUUID } from "crypto";
9
+ import open from "open";
10
+
11
+ // src/config.ts
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ var API_BASE_URL = process.env.AI_TODO_API_URL ?? "https://ai-todo.stringzhao.life";
15
+ var CONFIG_DIR = join(homedir(), ".config", "ai-todo");
16
+ var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
17
+
18
+ // src/credentials.ts
19
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
20
+ import { dirname } from "path";
21
+ function loadCredentials() {
22
+ try {
23
+ const data = readFileSync(CREDENTIALS_PATH, "utf-8");
24
+ return JSON.parse(data);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function saveCredentials(creds) {
30
+ mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true, mode: 448 });
31
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), {
32
+ mode: 384
33
+ });
34
+ }
35
+ function clearCredentials() {
36
+ try {
37
+ unlinkSync(CREDENTIALS_PATH);
38
+ } catch {
39
+ }
40
+ }
41
+
42
+ // src/auth.ts
43
+ var TIMEOUT_MS = 12e4;
44
+ async function login(tokenDirect) {
45
+ if (tokenDirect) {
46
+ saveCredentials({ access_token: tokenDirect, user_id: "", email: "" });
47
+ console.log(JSON.stringify({ success: true, message: "Token saved" }));
48
+ return;
49
+ }
50
+ const state = randomUUID();
51
+ return new Promise((resolve, reject) => {
52
+ const server = createServer((req, res) => {
53
+ if (req.method === "OPTIONS") {
54
+ res.writeHead(204, {
55
+ "Access-Control-Allow-Origin": new URL(API_BASE_URL).origin,
56
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
57
+ "Access-Control-Allow-Headers": "Content-Type",
58
+ "Access-Control-Max-Age": "86400"
59
+ });
60
+ res.end();
61
+ return;
62
+ }
63
+ if (req.method === "POST" && req.url === "/callback") {
64
+ let body = "";
65
+ req.on("data", (chunk) => {
66
+ body += chunk;
67
+ });
68
+ req.on("end", () => {
69
+ try {
70
+ const data = JSON.parse(body);
71
+ if (data.state !== state) {
72
+ res.writeHead(400, {
73
+ "Content-Type": "application/json",
74
+ "Access-Control-Allow-Origin": new URL(API_BASE_URL).origin
75
+ });
76
+ res.end(JSON.stringify({ error: "State mismatch" }));
77
+ return;
78
+ }
79
+ saveCredentials({
80
+ access_token: data.access_token,
81
+ user_id: data.user_id,
82
+ email: data.email
83
+ });
84
+ res.writeHead(200, {
85
+ "Content-Type": "application/json",
86
+ "Access-Control-Allow-Origin": new URL(API_BASE_URL).origin
87
+ });
88
+ res.end(JSON.stringify({ success: true }));
89
+ console.log(JSON.stringify({
90
+ success: true,
91
+ email: data.email,
92
+ message: "Login successful"
93
+ }));
94
+ server.close();
95
+ resolve();
96
+ } catch {
97
+ res.writeHead(400, { "Content-Type": "application/json" });
98
+ res.end(JSON.stringify({ error: "Invalid request" }));
99
+ }
100
+ });
101
+ return;
102
+ }
103
+ res.writeHead(404);
104
+ res.end();
105
+ });
106
+ server.listen(0, "127.0.0.1", () => {
107
+ const addr = server.address();
108
+ if (!addr || typeof addr === "string") {
109
+ reject(new Error("Failed to start local server"));
110
+ return;
111
+ }
112
+ const port = addr.port;
113
+ const authUrl = `${API_BASE_URL}/auth/cli?port=${port}&state=${state}`;
114
+ console.log(JSON.stringify({
115
+ message: "Opening browser for login...",
116
+ url: authUrl
117
+ }));
118
+ open(authUrl).catch(() => {
119
+ console.log(JSON.stringify({
120
+ message: "Could not open browser. Please visit this URL manually:",
121
+ url: authUrl
122
+ }));
123
+ });
124
+ });
125
+ const timer = setTimeout(() => {
126
+ server.close();
127
+ console.log(JSON.stringify({ error: "Login timed out" }));
128
+ process.exit(1);
129
+ }, TIMEOUT_MS);
130
+ server.on("close", () => clearTimeout(timer));
131
+ });
132
+ }
133
+
134
+ // src/manifest.ts
135
+ async function fetchManifest() {
136
+ const res = await fetch(`${API_BASE_URL}/api/manifest`);
137
+ if (!res.ok) {
138
+ console.log(JSON.stringify({ error: "Failed to fetch manifest", status: res.status }));
139
+ process.exit(1);
140
+ }
141
+ return res.json();
142
+ }
143
+
144
+ // src/client.ts
145
+ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyParams, fixedBody) {
146
+ const creds = loadCredentials();
147
+ if (!creds) {
148
+ console.log(JSON.stringify({ error: "Not logged in. Run: ai-todo login" }));
149
+ process.exit(2);
150
+ }
151
+ let path = pathTemplate;
152
+ for (const [key, value] of Object.entries(pathParams)) {
153
+ path = path.replace(`:${key}`, encodeURIComponent(value));
154
+ }
155
+ const url = new URL(path, API_BASE_URL);
156
+ for (const [key, value] of Object.entries(queryParams)) {
157
+ if (value !== void 0 && value !== "") {
158
+ url.searchParams.set(key, value);
159
+ }
160
+ }
161
+ const headers = {
162
+ Authorization: `Bearer ${creds.access_token}`
163
+ };
164
+ let body;
165
+ const mergedBody = { ...bodyParams, ...fixedBody };
166
+ if (method !== "GET" && method !== "DELETE" && Object.keys(mergedBody).length > 0) {
167
+ headers["Content-Type"] = "application/json";
168
+ body = JSON.stringify(mergedBody);
169
+ }
170
+ const res = await fetch(url.toString(), { method, headers, body });
171
+ if (res.status === 401) {
172
+ console.log(JSON.stringify({ error: "Unauthorized. Run: ai-todo login" }));
173
+ process.exit(2);
174
+ }
175
+ if (res.status === 204) {
176
+ return { data: { success: true }, status: 204 };
177
+ }
178
+ const data = await res.json().catch(() => ({}));
179
+ if (!res.ok) {
180
+ console.log(JSON.stringify({ error: data.error ?? "Request failed", status: res.status }));
181
+ process.exit(1);
182
+ }
183
+ return { data, status: res.status };
184
+ }
185
+
186
+ // src/commands.ts
187
+ function registerDynamicCommands(program2, operations) {
188
+ for (const op of operations) {
189
+ const cmd = program2.command(op.name).description(op.description);
190
+ for (const param of op.params) {
191
+ const flag = `--${param.name} <value>`;
192
+ const desc = buildParamDesc(param.description, param.enum);
193
+ if (param.required) {
194
+ cmd.requiredOption(flag, desc);
195
+ } else {
196
+ cmd.option(flag, desc);
197
+ }
198
+ }
199
+ cmd.action(async (opts) => {
200
+ const pathParams = {};
201
+ const queryParams = {};
202
+ const bodyParams = {};
203
+ for (const param of op.params) {
204
+ const value = opts[param.name];
205
+ if (value === void 0) continue;
206
+ if (param.in === "path") {
207
+ pathParams[param.name] = value;
208
+ } else if (param.in === "query") {
209
+ queryParams[param.name] = value;
210
+ } else {
211
+ bodyParams[param.name] = coerceValue(value, param.type);
212
+ }
213
+ }
214
+ const { data } = await apiRequest(
215
+ op.method,
216
+ op.path,
217
+ pathParams,
218
+ queryParams,
219
+ bodyParams,
220
+ op.fixed_body
221
+ );
222
+ console.log(JSON.stringify(data, null, 2));
223
+ });
224
+ }
225
+ }
226
+ function buildParamDesc(desc, enumValues) {
227
+ if (!desc) return "";
228
+ if (enumValues?.length) {
229
+ return `${desc} [${enumValues.join("|")}]`;
230
+ }
231
+ return desc;
232
+ }
233
+ function coerceValue(value, type) {
234
+ if (type === "number") {
235
+ const n = Number(value);
236
+ return Number.isNaN(n) ? value : n;
237
+ }
238
+ if (type === "string[]") {
239
+ return value.split(",").map((s) => s.trim());
240
+ }
241
+ return value;
242
+ }
243
+
244
+ // src/index.ts
245
+ var program = new Command();
246
+ program.name("ai-todo").description("CLI for AI agents to interact with ai-todo").version("0.1.0");
247
+ program.command("login").description("Authenticate with ai-todo via browser").option("--token <jwt>", "Directly provide a JWT token (for headless environments)").action(async (opts) => {
248
+ await login(opts.token);
249
+ });
250
+ program.command("logout").description("Clear stored credentials").action(() => {
251
+ clearCredentials();
252
+ console.log(JSON.stringify({ success: true, message: "Logged out" }));
253
+ });
254
+ program.command("whoami").description("Show current authenticated user").action(() => {
255
+ const creds = loadCredentials();
256
+ if (!creds) {
257
+ console.log(JSON.stringify({ error: "Not logged in. Run: ai-todo login" }));
258
+ process.exit(2);
259
+ }
260
+ console.log(JSON.stringify({
261
+ user_id: creds.user_id,
262
+ email: creds.email
263
+ }));
264
+ });
265
+ async function main() {
266
+ const authCommands = ["login", "logout", "whoami", "help", "--help", "-h", "--version", "-V"];
267
+ const firstArg = process.argv[2];
268
+ if (!firstArg || authCommands.includes(firstArg)) {
269
+ await program.parseAsync(process.argv);
270
+ return;
271
+ }
272
+ try {
273
+ const manifest = await fetchManifest();
274
+ registerDynamicCommands(program, manifest.operations);
275
+ } catch {
276
+ console.log(JSON.stringify({ error: "Failed to load commands from server" }));
277
+ process.exit(1);
278
+ }
279
+ await program.parseAsync(process.argv);
280
+ }
281
+ main().catch((err) => {
282
+ console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }));
283
+ process.exit(1);
284
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ai-todo-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for AI agents to interact with ai-todo",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-todo": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch"
15
+ },
16
+ "dependencies": {
17
+ "commander": "^13.0.0",
18
+ "open": "^10.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.9.0",
23
+ "@types/node": "^22.0.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "license": "MIT"
29
+ }