ai-todo-cli 0.1.2 → 0.4.2

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 +11 -0
  2. package/dist/index.js +104 -7
  3. package/package.json +15 -4
package/README.md CHANGED
@@ -36,6 +36,17 @@ ai-todo spaces:list
36
36
 
37
37
  Run `ai-todo --help` to see all available commands (fetched from server).
38
38
 
39
+ ## Release
40
+
41
+ This package is published to npm via GitHub Actions when a tag like `v0.1.3` is pushed.
42
+
43
+ ```bash
44
+ npm version patch
45
+ git push origin main --follow-tags
46
+ ```
47
+
48
+ The workflow will verify that the Git tag matches `package.json` before publishing.
49
+
39
50
  ## For AI Agents
40
51
 
41
52
  This CLI is designed for AI agent integration. Key features:
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { createRequire } from "module";
5
6
 
6
7
  // src/auth.ts
7
8
  import { createServer } from "http";
@@ -78,6 +79,7 @@ async function login(tokenDirect) {
78
79
  }
79
80
  saveCredentials({
80
81
  access_token: data.access_token,
82
+ session_token: data.session_token,
81
83
  user_id: data.user_id,
82
84
  email: data.email
83
85
  });
@@ -142,14 +144,14 @@ async function fetchManifest() {
142
144
  }
143
145
 
144
146
  // src/client.ts
145
- async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyParams, fixedBody) {
147
+ async function apiRequest(method, pathTemplate, pathParams2, queryParams, bodyParams, fixedBody) {
146
148
  const creds = loadCredentials();
147
149
  if (!creds) {
148
150
  console.log(JSON.stringify({ error: "Not logged in. Run: ai-todo login" }));
149
151
  process.exit(2);
150
152
  }
151
153
  let path = pathTemplate;
152
- for (const [key, value] of Object.entries(pathParams)) {
154
+ for (const [key, value] of Object.entries(pathParams2)) {
153
155
  path = path.replace(`:${key}`, encodeURIComponent(value));
154
156
  }
155
157
  const url = new URL(path, API_BASE_URL);
@@ -159,7 +161,7 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
159
161
  }
160
162
  }
161
163
  const headers = {
162
- Authorization: `Bearer ${creds.access_token}`
164
+ Authorization: `Bearer ${creds.session_token || creds.access_token}`
163
165
  };
164
166
  let body;
165
167
  const mergedBody = { ...bodyParams, ...fixedBody };
@@ -184,20 +186,62 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
184
186
  }
185
187
 
186
188
  // src/commands.ts
189
+ function toCamelCase(s) {
190
+ return s.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
191
+ }
187
192
  function registerDynamicCommands(program2, operations) {
188
193
  for (const op of operations) {
189
194
  const cmd = program2.command(op.name).description(op.description);
195
+ if (op.aliases?.length) {
196
+ cmd.aliases(op.aliases);
197
+ }
198
+ const paramAliasMap = {};
199
+ const requiredParamsWithAliases = /* @__PURE__ */ new Set();
190
200
  for (const param of op.params) {
191
201
  const flag = `--${param.name} <value>`;
192
202
  const desc = buildParamDesc(param.description, param.enum);
193
- if (param.required) {
203
+ if (param.required && !param.aliases?.length) {
194
204
  cmd.requiredOption(flag, desc);
195
205
  } else {
196
206
  cmd.option(flag, desc);
207
+ if (param.required && param.aliases?.length) {
208
+ requiredParamsWithAliases.add(param.name);
209
+ }
197
210
  }
211
+ if (param.aliases?.length) {
212
+ for (const alias of param.aliases) {
213
+ cmd.option(`--${alias} <value>`);
214
+ const camelAlias = toCamelCase(alias);
215
+ paramAliasMap[camelAlias] = param.name;
216
+ if (camelAlias !== alias) {
217
+ paramAliasMap[alias] = param.name;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ if (Object.keys(paramAliasMap).length > 0 || requiredParamsWithAliases.size > 0) {
223
+ cmd.hook("preAction", (thisCommand) => {
224
+ const opts = thisCommand.opts();
225
+ for (const [alias, original] of Object.entries(paramAliasMap)) {
226
+ if (opts[alias] !== void 0 && opts[original] === void 0) {
227
+ thisCommand.setOptionValue(original, opts[alias]);
228
+ }
229
+ }
230
+ const updatedOpts = thisCommand.opts();
231
+ for (const name of requiredParamsWithAliases) {
232
+ if (updatedOpts[name] === void 0) {
233
+ const param = op.params.find((p) => p.name === name);
234
+ const aliasList = param?.aliases?.map((a) => `--${a}`).join(", ") ?? "";
235
+ console.log(JSON.stringify({
236
+ error: `Missing required option: --${name}`,
237
+ aliases: aliasList ? `Also accepts: ${aliasList}` : void 0
238
+ }));
239
+ process.exit(1);
240
+ }
241
+ }
242
+ });
198
243
  }
199
244
  cmd.action(async (opts) => {
200
- const pathParams = {};
201
245
  const queryParams = {};
202
246
  const bodyParams = {};
203
247
  for (const param of op.params) {
@@ -219,7 +263,11 @@ function registerDynamicCommands(program2, operations) {
219
263
  bodyParams,
220
264
  op.fixed_body
221
265
  );
222
- console.log(JSON.stringify(data, null, 2));
266
+ if (op.format === "text" && typeof data === "object" && data !== null && "output" in data) {
267
+ console.log(data.output);
268
+ } else {
269
+ console.log(JSON.stringify(data, null, 2));
270
+ }
223
271
  });
224
272
  }
225
273
  }
@@ -230,6 +278,31 @@ function buildParamDesc(desc, enumValues) {
230
278
  }
231
279
  return desc;
232
280
  }
281
+ function levenshtein(a, b) {
282
+ const m = a.length, n = b.length;
283
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
284
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
285
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
286
+ for (let i = 1; i <= m; i++) {
287
+ for (let j = 1; j <= n; j++) {
288
+ 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]);
289
+ }
290
+ }
291
+ return dp[m][n];
292
+ }
293
+ function findClosestCommand(input, candidates) {
294
+ let best = "";
295
+ let bestDist = Infinity;
296
+ for (const candidate of candidates) {
297
+ const dist = levenshtein(input.toLowerCase(), candidate.toLowerCase());
298
+ if (dist < bestDist) {
299
+ bestDist = dist;
300
+ best = candidate;
301
+ }
302
+ }
303
+ const threshold = Math.min(Math.ceil(input.length / 2), 3);
304
+ return bestDist <= threshold ? best : null;
305
+ }
233
306
  function coerceValue(value, type) {
234
307
  if (type === "number") {
235
308
  const n = Number(value);
@@ -242,8 +315,10 @@ function coerceValue(value, type) {
242
315
  }
243
316
 
244
317
  // src/index.ts
318
+ var require2 = createRequire(import.meta.url);
319
+ var { version } = require2("../package.json");
245
320
  var program = new Command();
246
- program.name("ai-todo").description("CLI for AI agents to interact with ai-todo").version("0.1.0");
321
+ program.name("ai-todo").description("CLI for AI agents to interact with ai-todo").version(version);
247
322
  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
323
  await login(opts.token);
249
324
  });
@@ -262,6 +337,27 @@ program.command("whoami").description("Show current authenticated user").action(
262
337
  email: creds.email
263
338
  }));
264
339
  });
340
+ function setupUnknownCommandHandler(operations) {
341
+ program.on("command:*", (operands) => {
342
+ const unknown = operands[0];
343
+ const allNames = [];
344
+ for (const op of operations) {
345
+ allNames.push(op.name);
346
+ if (op.aliases) allNames.push(...op.aliases);
347
+ }
348
+ allNames.push("login", "logout", "whoami");
349
+ const suggestion = findClosestCommand(unknown, allNames);
350
+ const result = {
351
+ error: `Unknown command: ${unknown}`
352
+ };
353
+ if (suggestion) {
354
+ result.suggestion = `Did you mean: ai-todo ${suggestion}`;
355
+ }
356
+ result.hint = "Run 'ai-todo --help' to see all available commands";
357
+ console.log(JSON.stringify(result));
358
+ process.exit(1);
359
+ });
360
+ }
265
361
  async function main() {
266
362
  const firstArg = process.argv[2];
267
363
  const skipCommands = ["login", "logout", "whoami"];
@@ -271,6 +367,7 @@ async function main() {
271
367
  try {
272
368
  const manifest = await fetchManifest();
273
369
  registerDynamicCommands(program, manifest.operations);
370
+ setupUnknownCommandHandler(manifest.operations);
274
371
  } catch {
275
372
  const isHelpOrEmpty = !firstArg || ["help", "--help", "-h"].includes(firstArg);
276
373
  if (!isHelpOrEmpty) {
package/package.json CHANGED
@@ -1,26 +1,37 @@
1
1
  {
2
2
  "name": "ai-todo-cli",
3
- "version": "0.1.2",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for AI agents to interact with ai-todo",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/strzhao/ai-todo-cli.git"
9
+ },
10
+ "homepage": "https://github.com/strzhao/ai-todo-cli#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/strzhao/ai-todo-cli/issues"
13
+ },
6
14
  "bin": {
7
- "ai-todo": "./dist/index.js"
15
+ "ai-todo": "dist/index.js"
8
16
  },
9
17
  "files": [
10
18
  "dist"
11
19
  ],
12
20
  "scripts": {
13
21
  "build": "tsup",
14
- "dev": "tsup --watch"
22
+ "dev": "tsup --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
15
25
  },
16
26
  "dependencies": {
17
27
  "commander": "^13.0.0",
18
28
  "open": "^10.0.0"
19
29
  },
20
30
  "devDependencies": {
31
+ "@types/node": "^22.0.0",
21
32
  "tsup": "^8.0.0",
22
33
  "typescript": "^5.9.0",
23
- "@types/node": "^22.0.0"
34
+ "vitest": "^4.1.0"
24
35
  },
25
36
  "engines": {
26
37
  "node": ">=18"