ai-todo-cli 0.3.0 → 0.4.3
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/dist/index.js +159 -37
- package/package.json +22 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
4
|
import { createRequire } from "module";
|
|
5
|
+
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
8
|
-
import { createServer } from "http";
|
|
9
8
|
import { randomUUID } from "crypto";
|
|
9
|
+
import { createServer } from "http";
|
|
10
10
|
import open from "open";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
@@ -17,7 +17,7 @@ var CONFIG_DIR = join(homedir(), ".config", "ai-todo");
|
|
|
17
17
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
18
18
|
|
|
19
19
|
// src/credentials.ts
|
|
20
|
-
import {
|
|
20
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
21
21
|
import { dirname } from "path";
|
|
22
22
|
function loadCredentials() {
|
|
23
23
|
try {
|
|
@@ -79,6 +79,7 @@ async function login(tokenDirect) {
|
|
|
79
79
|
}
|
|
80
80
|
saveCredentials({
|
|
81
81
|
access_token: data.access_token,
|
|
82
|
+
session_token: data.session_token,
|
|
82
83
|
user_id: data.user_id,
|
|
83
84
|
email: data.email
|
|
84
85
|
});
|
|
@@ -87,11 +88,13 @@ async function login(tokenDirect) {
|
|
|
87
88
|
"Access-Control-Allow-Origin": new URL(API_BASE_URL).origin
|
|
88
89
|
});
|
|
89
90
|
res.end(JSON.stringify({ success: true }));
|
|
90
|
-
console.log(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
console.log(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
success: true,
|
|
94
|
+
email: data.email,
|
|
95
|
+
message: "Login successful"
|
|
96
|
+
})
|
|
97
|
+
);
|
|
95
98
|
server.close();
|
|
96
99
|
resolve();
|
|
97
100
|
} catch {
|
|
@@ -112,15 +115,19 @@ async function login(tokenDirect) {
|
|
|
112
115
|
}
|
|
113
116
|
const port = addr.port;
|
|
114
117
|
const authUrl = `${API_BASE_URL}/auth/cli?port=${port}&state=${state}`;
|
|
115
|
-
console.log(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}));
|
|
119
|
-
open(authUrl).catch(() => {
|
|
120
|
-
console.log(JSON.stringify({
|
|
121
|
-
message: "Could not open browser. Please visit this URL manually:",
|
|
118
|
+
console.log(
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
message: "Opening browser for login...",
|
|
122
121
|
url: authUrl
|
|
123
|
-
})
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
open(authUrl).catch(() => {
|
|
125
|
+
console.log(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
message: "Could not open browser. Please visit this URL manually:",
|
|
128
|
+
url: authUrl
|
|
129
|
+
})
|
|
130
|
+
);
|
|
124
131
|
});
|
|
125
132
|
});
|
|
126
133
|
const timer = setTimeout(() => {
|
|
@@ -132,16 +139,6 @@ async function login(tokenDirect) {
|
|
|
132
139
|
});
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
// src/manifest.ts
|
|
136
|
-
async function fetchManifest() {
|
|
137
|
-
const res = await fetch(`${API_BASE_URL}/api/manifest`);
|
|
138
|
-
if (!res.ok) {
|
|
139
|
-
console.log(JSON.stringify({ error: "Failed to fetch manifest", status: res.status }));
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
return res.json();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
142
|
// src/client.ts
|
|
146
143
|
async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyParams, fixedBody) {
|
|
147
144
|
const creds = loadCredentials();
|
|
@@ -160,7 +157,7 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
|
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
const headers = {
|
|
163
|
-
Authorization: `Bearer ${creds.access_token}`
|
|
160
|
+
Authorization: `Bearer ${creds.session_token || creds.access_token}`
|
|
164
161
|
};
|
|
165
162
|
let body;
|
|
166
163
|
const mergedBody = { ...bodyParams, ...fixedBody };
|
|
@@ -178,25 +175,75 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
|
|
|
178
175
|
}
|
|
179
176
|
const data = await res.json().catch(() => ({}));
|
|
180
177
|
if (!res.ok) {
|
|
181
|
-
console.log(
|
|
178
|
+
console.log(
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
error: data.error ?? "Request failed",
|
|
181
|
+
status: res.status
|
|
182
|
+
})
|
|
183
|
+
);
|
|
182
184
|
process.exit(1);
|
|
183
185
|
}
|
|
184
186
|
return { data, status: res.status };
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
// src/commands.ts
|
|
190
|
+
function toCamelCase(s) {
|
|
191
|
+
return s.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
192
|
+
}
|
|
188
193
|
function registerDynamicCommands(program2, operations) {
|
|
189
194
|
for (const op of operations) {
|
|
190
195
|
const cmd = program2.command(op.name).description(op.description);
|
|
196
|
+
if (op.aliases?.length) {
|
|
197
|
+
cmd.aliases(op.aliases);
|
|
198
|
+
}
|
|
199
|
+
const paramAliasMap = {};
|
|
200
|
+
const requiredParamsWithAliases = /* @__PURE__ */ new Set();
|
|
191
201
|
for (const param of op.params) {
|
|
192
202
|
const flag = `--${param.name} <value>`;
|
|
193
203
|
const desc = buildParamDesc(param.description, param.enum);
|
|
194
|
-
if (param.required) {
|
|
204
|
+
if (param.required && !param.aliases?.length) {
|
|
195
205
|
cmd.requiredOption(flag, desc);
|
|
196
206
|
} else {
|
|
197
207
|
cmd.option(flag, desc);
|
|
208
|
+
if (param.required && param.aliases?.length) {
|
|
209
|
+
requiredParamsWithAliases.add(param.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (param.aliases?.length) {
|
|
213
|
+
for (const alias of param.aliases) {
|
|
214
|
+
cmd.option(`--${alias} <value>`);
|
|
215
|
+
const camelAlias = toCamelCase(alias);
|
|
216
|
+
paramAliasMap[camelAlias] = param.name;
|
|
217
|
+
if (camelAlias !== alias) {
|
|
218
|
+
paramAliasMap[alias] = param.name;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
198
221
|
}
|
|
199
222
|
}
|
|
223
|
+
if (Object.keys(paramAliasMap).length > 0 || requiredParamsWithAliases.size > 0) {
|
|
224
|
+
cmd.hook("preAction", (thisCommand) => {
|
|
225
|
+
const opts = thisCommand.opts();
|
|
226
|
+
for (const [alias, original] of Object.entries(paramAliasMap)) {
|
|
227
|
+
if (opts[alias] !== void 0 && opts[original] === void 0) {
|
|
228
|
+
thisCommand.setOptionValue(original, opts[alias]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const updatedOpts = thisCommand.opts();
|
|
232
|
+
for (const name of requiredParamsWithAliases) {
|
|
233
|
+
if (updatedOpts[name] === void 0) {
|
|
234
|
+
const param = op.params.find((p) => p.name === name);
|
|
235
|
+
const aliasList = param?.aliases?.map((a) => `--${a}`).join(", ") ?? "";
|
|
236
|
+
console.log(
|
|
237
|
+
JSON.stringify({
|
|
238
|
+
error: `Missing required option: --${name}`,
|
|
239
|
+
aliases: aliasList ? `Also accepts: ${aliasList}` : void 0
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
200
247
|
cmd.action(async (opts) => {
|
|
201
248
|
const pathParams = {};
|
|
202
249
|
const queryParams = {};
|
|
@@ -235,6 +282,34 @@ function buildParamDesc(desc, enumValues) {
|
|
|
235
282
|
}
|
|
236
283
|
return desc;
|
|
237
284
|
}
|
|
285
|
+
function levenshtein(a, b) {
|
|
286
|
+
const m = a.length, n = b.length;
|
|
287
|
+
const dp = Array.from(
|
|
288
|
+
{ length: m + 1 },
|
|
289
|
+
() => Array(n + 1).fill(0)
|
|
290
|
+
);
|
|
291
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
292
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
293
|
+
for (let i = 1; i <= m; i++) {
|
|
294
|
+
for (let j = 1; j <= n; j++) {
|
|
295
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return dp[m][n];
|
|
299
|
+
}
|
|
300
|
+
function findClosestCommand(input, candidates) {
|
|
301
|
+
let best = "";
|
|
302
|
+
let bestDist = Infinity;
|
|
303
|
+
for (const candidate of candidates) {
|
|
304
|
+
const dist = levenshtein(input.toLowerCase(), candidate.toLowerCase());
|
|
305
|
+
if (dist < bestDist) {
|
|
306
|
+
bestDist = dist;
|
|
307
|
+
best = candidate;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const threshold = Math.min(Math.ceil(input.length / 2), 3);
|
|
311
|
+
return bestDist <= threshold ? best : null;
|
|
312
|
+
}
|
|
238
313
|
function coerceValue(value, type) {
|
|
239
314
|
if (type === "number") {
|
|
240
315
|
const n = Number(value);
|
|
@@ -246,12 +321,27 @@ function coerceValue(value, type) {
|
|
|
246
321
|
return value;
|
|
247
322
|
}
|
|
248
323
|
|
|
324
|
+
// src/manifest.ts
|
|
325
|
+
async function fetchManifest() {
|
|
326
|
+
const res = await fetch(`${API_BASE_URL}/api/manifest`);
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
console.log(
|
|
329
|
+
JSON.stringify({ error: "Failed to fetch manifest", status: res.status })
|
|
330
|
+
);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
return res.json();
|
|
334
|
+
}
|
|
335
|
+
|
|
249
336
|
// src/index.ts
|
|
250
337
|
var require2 = createRequire(import.meta.url);
|
|
251
338
|
var { version } = require2("../package.json");
|
|
252
339
|
var program = new Command();
|
|
253
340
|
program.name("ai-todo").description("CLI for AI agents to interact with ai-todo").version(version);
|
|
254
|
-
program.command("login").description("Authenticate with ai-todo via browser").option(
|
|
341
|
+
program.command("login").description("Authenticate with ai-todo via browser").option(
|
|
342
|
+
"--token <jwt>",
|
|
343
|
+
"Directly provide a JWT token (for headless environments)"
|
|
344
|
+
).action(async (opts) => {
|
|
255
345
|
await login(opts.token);
|
|
256
346
|
});
|
|
257
347
|
program.command("logout").description("Clear stored credentials").action(() => {
|
|
@@ -261,14 +351,39 @@ program.command("logout").description("Clear stored credentials").action(() => {
|
|
|
261
351
|
program.command("whoami").description("Show current authenticated user").action(() => {
|
|
262
352
|
const creds = loadCredentials();
|
|
263
353
|
if (!creds) {
|
|
264
|
-
console.log(
|
|
354
|
+
console.log(
|
|
355
|
+
JSON.stringify({ error: "Not logged in. Run: ai-todo login" })
|
|
356
|
+
);
|
|
265
357
|
process.exit(2);
|
|
266
358
|
}
|
|
267
|
-
console.log(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
359
|
+
console.log(
|
|
360
|
+
JSON.stringify({
|
|
361
|
+
user_id: creds.user_id,
|
|
362
|
+
email: creds.email
|
|
363
|
+
})
|
|
364
|
+
);
|
|
271
365
|
});
|
|
366
|
+
function setupUnknownCommandHandler(operations) {
|
|
367
|
+
program.on("command:*", (operands) => {
|
|
368
|
+
const unknown = operands[0];
|
|
369
|
+
const allNames = [];
|
|
370
|
+
for (const op of operations) {
|
|
371
|
+
allNames.push(op.name);
|
|
372
|
+
if (op.aliases) allNames.push(...op.aliases);
|
|
373
|
+
}
|
|
374
|
+
allNames.push("login", "logout", "whoami");
|
|
375
|
+
const suggestion = findClosestCommand(unknown, allNames);
|
|
376
|
+
const result = {
|
|
377
|
+
error: `Unknown command: ${unknown}`
|
|
378
|
+
};
|
|
379
|
+
if (suggestion) {
|
|
380
|
+
result.suggestion = `Did you mean: ai-todo ${suggestion}`;
|
|
381
|
+
}
|
|
382
|
+
result.hint = "Run 'ai-todo --help' to see all available commands";
|
|
383
|
+
console.log(JSON.stringify(result));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
272
387
|
async function main() {
|
|
273
388
|
const firstArg = process.argv[2];
|
|
274
389
|
const skipCommands = ["login", "logout", "whoami"];
|
|
@@ -278,10 +393,13 @@ async function main() {
|
|
|
278
393
|
try {
|
|
279
394
|
const manifest = await fetchManifest();
|
|
280
395
|
registerDynamicCommands(program, manifest.operations);
|
|
396
|
+
setupUnknownCommandHandler(manifest.operations);
|
|
281
397
|
} catch {
|
|
282
398
|
const isHelpOrEmpty = !firstArg || ["help", "--help", "-h"].includes(firstArg);
|
|
283
399
|
if (!isHelpOrEmpty) {
|
|
284
|
-
console.log(
|
|
400
|
+
console.log(
|
|
401
|
+
JSON.stringify({ error: "Failed to load commands from server" })
|
|
402
|
+
);
|
|
285
403
|
process.exit(1);
|
|
286
404
|
}
|
|
287
405
|
}
|
|
@@ -289,6 +407,10 @@ async function main() {
|
|
|
289
407
|
await program.parseAsync(process.argv);
|
|
290
408
|
}
|
|
291
409
|
main().catch((err) => {
|
|
292
|
-
console.log(
|
|
410
|
+
console.log(
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
413
|
+
})
|
|
414
|
+
);
|
|
293
415
|
process.exit(1);
|
|
294
416
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-todo-cli",
|
|
3
|
-
"version": "0.3
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "CLI tool for AI agents to interact with ai-todo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -19,19 +19,37 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsup",
|
|
22
|
-
"dev": "tsup --watch"
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"lint": "biome check src/",
|
|
24
|
+
"lint:fix": "biome check --fix src/",
|
|
25
|
+
"format": "biome format --write src/",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"test:coverage": "vitest run --coverage",
|
|
29
|
+
"prepare": "husky"
|
|
23
30
|
},
|
|
24
31
|
"dependencies": {
|
|
25
32
|
"commander": "^13.0.0",
|
|
26
33
|
"open": "^10.0.0"
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.4.8",
|
|
29
37
|
"@types/node": "^22.0.0",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
39
|
+
"husky": "^9.1.7",
|
|
40
|
+
"lint-staged": "^16.4.0",
|
|
30
41
|
"tsup": "^8.0.0",
|
|
31
|
-
"typescript": "^5.9.0"
|
|
42
|
+
"typescript": "^5.9.0",
|
|
43
|
+
"vitest": "^4.1.0"
|
|
32
44
|
},
|
|
33
45
|
"engines": {
|
|
34
46
|
"node": ">=18"
|
|
35
47
|
},
|
|
36
|
-
"license": "MIT"
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"lint-staged": {
|
|
50
|
+
"src/**/*.ts": [
|
|
51
|
+
"biome check --fix",
|
|
52
|
+
"biome format --write"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
37
55
|
}
|