ai-todo-cli 0.3.0 → 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 (2) hide show
  1. package/dist/index.js +95 -5
  2. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -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
  });
@@ -143,14 +144,14 @@ async function fetchManifest() {
143
144
  }
144
145
 
145
146
  // src/client.ts
146
- async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyParams, fixedBody) {
147
+ async function apiRequest(method, pathTemplate, pathParams2, queryParams, bodyParams, fixedBody) {
147
148
  const creds = loadCredentials();
148
149
  if (!creds) {
149
150
  console.log(JSON.stringify({ error: "Not logged in. Run: ai-todo login" }));
150
151
  process.exit(2);
151
152
  }
152
153
  let path = pathTemplate;
153
- for (const [key, value] of Object.entries(pathParams)) {
154
+ for (const [key, value] of Object.entries(pathParams2)) {
154
155
  path = path.replace(`:${key}`, encodeURIComponent(value));
155
156
  }
156
157
  const url = new URL(path, API_BASE_URL);
@@ -160,7 +161,7 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
160
161
  }
161
162
  }
162
163
  const headers = {
163
- Authorization: `Bearer ${creds.access_token}`
164
+ Authorization: `Bearer ${creds.session_token || creds.access_token}`
164
165
  };
165
166
  let body;
166
167
  const mergedBody = { ...bodyParams, ...fixedBody };
@@ -185,20 +186,62 @@ async function apiRequest(method, pathTemplate, pathParams, queryParams, bodyPar
185
186
  }
186
187
 
187
188
  // src/commands.ts
189
+ function toCamelCase(s) {
190
+ return s.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
191
+ }
188
192
  function registerDynamicCommands(program2, operations) {
189
193
  for (const op of operations) {
190
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();
191
200
  for (const param of op.params) {
192
201
  const flag = `--${param.name} <value>`;
193
202
  const desc = buildParamDesc(param.description, param.enum);
194
- if (param.required) {
203
+ if (param.required && !param.aliases?.length) {
195
204
  cmd.requiredOption(flag, desc);
196
205
  } else {
197
206
  cmd.option(flag, desc);
207
+ if (param.required && param.aliases?.length) {
208
+ requiredParamsWithAliases.add(param.name);
209
+ }
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
+ }
198
220
  }
199
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
+ });
243
+ }
200
244
  cmd.action(async (opts) => {
201
- const pathParams = {};
202
245
  const queryParams = {};
203
246
  const bodyParams = {};
204
247
  for (const param of op.params) {
@@ -235,6 +278,31 @@ function buildParamDesc(desc, enumValues) {
235
278
  }
236
279
  return desc;
237
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
+ }
238
306
  function coerceValue(value, type) {
239
307
  if (type === "number") {
240
308
  const n = Number(value);
@@ -269,6 +337,27 @@ program.command("whoami").description("Show current authenticated user").action(
269
337
  email: creds.email
270
338
  }));
271
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
+ }
272
361
  async function main() {
273
362
  const firstArg = process.argv[2];
274
363
  const skipCommands = ["login", "logout", "whoami"];
@@ -278,6 +367,7 @@ async function main() {
278
367
  try {
279
368
  const manifest = await fetchManifest();
280
369
  registerDynamicCommands(program, manifest.operations);
370
+ setupUnknownCommandHandler(manifest.operations);
281
371
  } catch {
282
372
  const isHelpOrEmpty = !firstArg || ["help", "--help", "-h"].includes(firstArg);
283
373
  if (!isHelpOrEmpty) {
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.2",
4
4
  "description": "CLI tool for AI agents to interact with ai-todo",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,7 +19,9 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "tsup",
22
- "dev": "tsup --watch"
22
+ "dev": "tsup --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
23
25
  },
24
26
  "dependencies": {
25
27
  "commander": "^13.0.0",
@@ -28,7 +30,8 @@
28
30
  "devDependencies": {
29
31
  "@types/node": "^22.0.0",
30
32
  "tsup": "^8.0.0",
31
- "typescript": "^5.9.0"
33
+ "typescript": "^5.9.0",
34
+ "vitest": "^4.1.0"
32
35
  },
33
36
  "engines": {
34
37
  "node": ">=18"