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.
Files changed (2) hide show
  1. package/dist/index.js +159 -37
  2. 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 { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
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(JSON.stringify({
91
- success: true,
92
- email: data.email,
93
- message: "Login successful"
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(JSON.stringify({
116
- message: "Opening browser for login...",
117
- url: authUrl
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(JSON.stringify({ error: data.error ?? "Request failed", status: res.status }));
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("--token <jwt>", "Directly provide a JWT token (for headless environments)").action(async (opts) => {
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(JSON.stringify({ error: "Not logged in. Run: ai-todo login" }));
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(JSON.stringify({
268
- user_id: creds.user_id,
269
- email: creds.email
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(JSON.stringify({ error: "Failed to load commands from server" }));
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(JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }));
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.0",
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
  }