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.
- package/README.md +50 -0
- package/dist/index.js +284 -0
- 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
|
+
}
|