aiex-cli 0.0.1-beta.6 → 0.0.1-beta.8

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 CHANGED
@@ -100,6 +100,26 @@ By default, aiex automatically selects a model based on your input type (vision-
100
100
  | `aiex extract -s <name> -f <file> --db` | Extract and insert into SQLite database |
101
101
  | `aiex extract -s <name> -f <file> -m <model>` | Extract with a specific AI model |
102
102
  | `aiex doctor` | System and configuration diagnostics |
103
+ | `aiex completion bash\|zsh\|fish` | Generate shell completion scripts |
104
+
105
+ ### Shell Completions
106
+
107
+ Enable tab completion for commands and options:
108
+
109
+ ```bash
110
+ # bash
111
+ source <(aiex completion bash)
112
+
113
+ # zsh
114
+ source <(aiex completion zsh)
115
+
116
+ # fish
117
+ aiex completion fish | source
118
+ ```
119
+
120
+ To make it permanent, add the `source` line to your shell config file (`~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`).
121
+
122
+ > Completions are dynamically generated from the command definitions — no manual updates needed when commands or options change.
103
123
 
104
124
  <br>
105
125
 
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as doctorDiagnosticsTableRows, a as writeAIConfig, b as toSnakeCase, c as PLACEHOLDER_TEXT, d as seedConfig, f as description, g as createMigrationConfig, h as version, i as readAIConfig, l as AIConfigSchema, m as package_default, n as getDefaultAIConfig, o as DEFAULT_PROMPT_CONFIG, p as name, r as maskApiKey, s as PLACEHOLDER_SCHEMA, t as collectDoctorDiagnostics, u as createConfig, v as JsonSchemaDefinitionSchema, w as formatDoctorDiagnosticsJson, x as generateDrizzleSchema, y as parseJsonSchema } from "./doctor-BTfByg-I.mjs";
1
+ import { C as doctorDiagnosticsTableRows, a as writeAIConfig, b as toSnakeCase, c as PLACEHOLDER_TEXT, d as seedConfig, f as description, g as createMigrationConfig, h as version, i as readAIConfig, l as AIConfigSchema, m as package_default, n as getDefaultAIConfig, o as DEFAULT_PROMPT_CONFIG, p as name, r as maskApiKey, s as PLACEHOLDER_SCHEMA, t as collectDoctorDiagnostics, u as createConfig, v as JsonSchemaDefinitionSchema, w as formatDoctorDiagnosticsJson, x as generateDrizzleSchema, y as parseJsonSchema } from "./doctor-DxG7uaGR.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
@@ -6,14 +6,14 @@ import { fileURLToPath } from "node:url";
6
6
  import { ZodError } from "zod";
7
7
  import fs from "node:fs/promises";
8
8
  import { defineCommand, runMain } from "citty";
9
+ import { consola } from "consola";
9
10
  import updateNotifier from "update-notifier";
10
11
  import CliTable3 from "cli-table3";
11
- import { consola } from "consola";
12
12
  import { intro, outro, spinner } from "@clack/prompts";
13
13
  import Database from "better-sqlite3";
14
14
  import pc from "picocolors";
15
15
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
16
- import { Output, generateText, jsonSchema } from "ai";
16
+ import { APICallError, Output, generateText, jsonSchema } from "ai";
17
17
  import { exec, execFile } from "node:child_process";
18
18
  import { promisify } from "node:util";
19
19
  import { serve } from "@hono/node-server";
@@ -95,6 +95,64 @@ function parseAllSchemas(entries) {
95
95
  };
96
96
  }
97
97
 
98
+ //#endregion
99
+ //#region src/commands/completion.ts
100
+ function bashScript(name$1) {
101
+ return `# ${name$1} bash completion
102
+ _${name$1}() {
103
+ local IFS=\\$'\\n'
104
+ COMPREPLY=($(${name$1} _complete "\${COMP_WORDS[@]}" 2>/dev/null))
105
+ }
106
+ complete -F _${name$1} ${name$1}
107
+ `;
108
+ }
109
+ function zshScript(name$1) {
110
+ return `# ${name$1} zsh completion
111
+ #compdef ${name$1}
112
+
113
+ _${name$1}() {
114
+ local -a completions
115
+ completions=("\${(@f)$(${name$1} _complete "\${words[@]}" 2>/dev/null)}")
116
+ _describe '${name$1}' completions
117
+ }
118
+ compdef _${name$1} ${name$1}
119
+ `;
120
+ }
121
+ function fishScript(name$1) {
122
+ return `# ${name$1} fish completion
123
+ complete -c ${name$1} -f -a '(${name$1} _complete (commandline -cp) 2>/dev/null)'
124
+ `;
125
+ }
126
+ function generateScript(name$1, shell) {
127
+ switch (shell) {
128
+ case "bash": return bashScript(name$1);
129
+ case "zsh": return zshScript(name$1);
130
+ case "fish": return fishScript(name$1);
131
+ default: throw new Error(`Unsupported shell: ${shell}. Use bash, zsh, or fish.`);
132
+ }
133
+ }
134
+ const completionCommand = defineCommand({
135
+ meta: {
136
+ name: "completion",
137
+ description: "Generate shell completion scripts (bash|zsh|fish)\n\nUsage:\n aiex completion bash # source <(aiex completion bash)\n aiex completion zsh # source <(aiex completion zsh)\n aiex completion fish # aiex completion fish | source"
138
+ },
139
+ args: { shell: {
140
+ type: "string",
141
+ description: "Shell type: bash, zsh, fish",
142
+ required: true
143
+ } },
144
+ async run({ args }) {
145
+ const name$1 = "aiex";
146
+ const shell = args.shell;
147
+ try {
148
+ process.stdout.write(generateScript(name$1, shell));
149
+ } catch (error) {
150
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
151
+ process.exit(1);
152
+ }
153
+ }
154
+ });
155
+
98
156
  //#endregion
99
157
  //#region src/commands/doctor.ts
100
158
  const doctorCommand = defineCommand({
@@ -12702,6 +12760,28 @@ function lookupModelCapabilities(modelName) {
12702
12760
  return null;
12703
12761
  }
12704
12762
 
12763
+ //#endregion
12764
+ //#region src/utils/retry.ts
12765
+ async function withRetry(fn, onRetry, maxRetries = 5) {
12766
+ let lastError;
12767
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
12768
+ return await fn();
12769
+ } catch (error) {
12770
+ const err = error instanceof Error ? error : new Error(String(error));
12771
+ lastError = err;
12772
+ if (!(err instanceof APICallError && err.isRetryable && attempt < maxRetries)) throw err;
12773
+ const delayMs = 1e3 * 2 ** attempt + Math.round(Math.random() * 500);
12774
+ onRetry?.({
12775
+ attempt: attempt + 1,
12776
+ maxRetries,
12777
+ delayMs,
12778
+ statusCode: err.statusCode
12779
+ });
12780
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
12781
+ }
12782
+ throw lastError ?? /* @__PURE__ */ new Error("Retry failed after all attempts");
12783
+ }
12784
+
12705
12785
  //#endregion
12706
12786
  //#region src/core/ai-extraction/json-utils.ts
12707
12787
  function stripFences(text) {
@@ -12876,18 +12956,18 @@ function detectMimeType(filePath) {
12876
12956
  }
12877
12957
  async function readFilePart(filePath) {
12878
12958
  const mime = detectMimeType(filePath);
12879
- const dataUri = `data:${mime};base64,${(await fs.readFile(filePath)).toString("base64")}`;
12959
+ const buffer = await fs.readFile(filePath);
12880
12960
  const name$1 = path.basename(filePath);
12881
12961
  if (mime.startsWith("image/")) return {
12882
12962
  type: "image",
12883
- name: name$1,
12884
- image: dataUri
12963
+ image: buffer,
12964
+ mimeType: mime
12885
12965
  };
12886
12966
  return {
12887
12967
  type: "file",
12888
- name: name$1,
12889
- data: dataUri,
12890
- mimeType: mime
12968
+ data: buffer,
12969
+ mediaType: mime,
12970
+ filename: name$1
12891
12971
  };
12892
12972
  }
12893
12973
  function nullableType(type) {
@@ -13038,42 +13118,32 @@ async function extractStructuredData(input) {
13038
13118
  let result;
13039
13119
  if (useFileContent) {
13040
13120
  const filePart = await readFilePart(file);
13041
- const fileName = filePart.name;
13042
13121
  const contentParts = [{
13043
13122
  type: "text",
13044
- text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text || `Data is contained in the attached file: ${fileName}`) : user
13045
- }];
13046
- if (filePart.type === "image") contentParts.push({
13047
- type: "image",
13048
- image: filePart.image
13049
- });
13050
- else contentParts.push({
13051
- type: "file",
13052
- data: filePart.data,
13053
- mimeType: filePart.mimeType
13054
- });
13123
+ text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text || `Data is contained in the attached file: ${filePart.filename || path.basename(file)}`) : user
13124
+ }, filePart];
13055
13125
  const fileOpts = {
13056
13126
  model: provider.chatModel(selected.name),
13127
+ system,
13057
13128
  messages: [{
13058
- role: "system",
13059
- content: system
13060
- }, {
13061
13129
  role: "user",
13062
13130
  content: contentParts
13063
13131
  }],
13064
- abortSignal: AbortSignal.timeout(12e4)
13132
+ abortSignal: AbortSignal.timeout(12e4),
13133
+ maxRetries: 0
13065
13134
  };
13066
13135
  if (useStructuredOutput) fileOpts.output = Output.object({ schema: outputSchema });
13067
- result = await generateText(fileOpts);
13136
+ result = await withRetry(() => generateText(fileOpts), input.onRetry);
13068
13137
  } else {
13069
13138
  const textOpts = {
13070
13139
  model: provider.chatModel(selected.name),
13071
13140
  system,
13072
13141
  prompt: user,
13073
- abortSignal: AbortSignal.timeout(6e4)
13142
+ abortSignal: AbortSignal.timeout(6e4),
13143
+ maxRetries: 0
13074
13144
  };
13075
13145
  if (useStructuredOutput) textOpts.output = Output.object({ schema: outputSchema });
13076
- result = await generateText(textOpts);
13146
+ result = await withRetry(() => generateText(textOpts), input.onRetry);
13077
13147
  }
13078
13148
  let data;
13079
13149
  if (useStructuredOutput) data = result.output;
@@ -13401,7 +13471,10 @@ const extractCommand = defineCommand({
13401
13471
  text,
13402
13472
  aiexDir,
13403
13473
  file: filePath,
13404
- modelOverride
13474
+ modelOverride,
13475
+ onRetry(info) {
13476
+ s.message(`API responded with ${info.statusCode}, retrying in ${info.delayMs / 1e3}s (${info.attempt}/${info.maxRetries})...`);
13477
+ }
13405
13478
  });
13406
13479
  if (!result.success) {
13407
13480
  s.stop("Extraction failed");
@@ -14050,6 +14123,16 @@ function schemaRoutes(config) {
14050
14123
  const filePath = resolveSchemaFile(schemaDir, c.req.param("name"));
14051
14124
  if (!filePath) return c.json({ error: "Invalid schema file name" }, 400);
14052
14125
  try {
14126
+ const aiexDir = path.dirname(schemaDir);
14127
+ try {
14128
+ const content = await fs.readFile(filePath, "utf-8");
14129
+ const parsed = JsonSchemaDefinitionSchema.safeParse(JSON.parse(content));
14130
+ if (parsed.success) {
14131
+ const tableName = parsed.data.table.name;
14132
+ const snapshotPath = path.join(aiexDir, "extracted", `${tableName}.prompt.md`);
14133
+ await fs.unlink(snapshotPath).catch(() => {});
14134
+ }
14135
+ } catch {}
14053
14136
  await fs.unlink(filePath);
14054
14137
  return c.json({ success: true });
14055
14138
  } catch {
@@ -14204,6 +14287,7 @@ const subCommands = {
14204
14287
  web: webCommand,
14205
14288
  schema: schemaCommand,
14206
14289
  extract: extractCommand,
14290
+ completion: completionCommand,
14207
14291
  doctor: doctorCommand
14208
14292
  };
14209
14293
 
@@ -14211,6 +14295,21 @@ const subCommands = {
14211
14295
  //#region src/cli.ts
14212
14296
  seedConfig(createConfig());
14213
14297
  updateNotifier({ pkg: package_default }).notify();
14298
+ process.on("uncaughtException", (error) => {
14299
+ consola.error(error);
14300
+ process.exit(1);
14301
+ });
14302
+ process.on("unhandledRejection", (reason) => {
14303
+ const error = reason instanceof Error ? reason : new Error(String(reason));
14304
+ consola.error(error);
14305
+ process.exit(1);
14306
+ });
14307
+ if (process.argv[2] === "_complete") {
14308
+ const { getCompletions } = await import("./completions-ygS1okck.mjs");
14309
+ const suggestions = getCompletions(subCommands, process.argv.slice(3));
14310
+ for (const s of suggestions) process.stdout.write(`${s}\n`);
14311
+ process.exit(0);
14312
+ }
14214
14313
  runMain(defineCommand({
14215
14314
  meta: {
14216
14315
  name,
@@ -0,0 +1,90 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import fs from "node:fs";
4
+
5
+ //#region src/core/completions.ts
6
+ const LEADING_DASHES = /^-+/;
7
+ function getArgNames(cmd) {
8
+ if (!cmd.args) return [];
9
+ return Object.entries(cmd.args).flatMap(([key, arg]) => {
10
+ const names = [`--${key}`];
11
+ if (arg.alias) names.push(`-${arg.alias}`);
12
+ return names;
13
+ });
14
+ }
15
+ function getCommandNames(cmds) {
16
+ return Object.keys(cmds).filter((c) => !c.startsWith("_"));
17
+ }
18
+ function getFileCompletions(pattern) {
19
+ try {
20
+ const files = fs.readdirSync(path.dirname(pattern));
21
+ const ext = path.extname(pattern);
22
+ const prefix = path.basename(pattern).replace(ext, "");
23
+ return files.filter((f) => f.endsWith(ext) && f.startsWith(prefix)).map((f) => f.replace(ext, ""));
24
+ } catch {
25
+ return [];
26
+ }
27
+ }
28
+ function getJsonModelNames(configPath) {
29
+ try {
30
+ const content = fs.readFileSync(configPath, "utf-8");
31
+ const config = JSON.parse(content);
32
+ if (config.provider?.models) return config.provider.models.map((m) => m.name);
33
+ } catch {}
34
+ return [];
35
+ }
36
+ function getValueCompletions(prevArg) {
37
+ switch (prevArg) {
38
+ case "--schema":
39
+ case "-s": {
40
+ const cwd = process.cwd();
41
+ return getFileCompletions(path.join(cwd, ".aiex/schema/*.json"));
42
+ }
43
+ case "--model":
44
+ case "-m": {
45
+ const cwd = process.cwd();
46
+ return getJsonModelNames(path.join(cwd, ".aiex/ai-config.json"));
47
+ }
48
+ case "--file":
49
+ case "-f":
50
+ case "--text":
51
+ case "-t":
52
+ case "--name":
53
+ case "--port":
54
+ case "-p": return [];
55
+ default: return [];
56
+ }
57
+ }
58
+ function getCompletions(subCommands, args) {
59
+ const cmds = getCommandNames(subCommands);
60
+ if (args.length <= 1) {
61
+ const word = args[0] ?? "";
62
+ if (!word) return cmds;
63
+ return cmds.filter((c) => c.startsWith(word));
64
+ }
65
+ const cmdName = args[0];
66
+ const cmd = subCommands[cmdName];
67
+ const rest = args.slice(1);
68
+ if (!cmd) {
69
+ const matched = cmds.filter((c) => c.startsWith(cmdName));
70
+ if (matched.length > 0) return matched;
71
+ return cmds;
72
+ }
73
+ const current = rest[rest.length - 1] ?? "";
74
+ const prev = rest.length > 1 ? rest[rest.length - 2] : void 0;
75
+ if (current.startsWith("-")) {
76
+ const names = getArgNames(cmd);
77
+ if (!current) return names;
78
+ const key = current.replace(LEADING_DASHES, "");
79
+ if (!key) return names;
80
+ return names.filter((n) => n.replace(LEADING_DASHES, "").startsWith(key));
81
+ }
82
+ if (!current && prev) {
83
+ const values = getValueCompletions(prev);
84
+ if (values.length > 0) return values;
85
+ }
86
+ return getArgNames(cmd);
87
+ }
88
+
89
+ //#endregion
90
+ export { getCompletions };
@@ -411,7 +411,7 @@ function generateDrizzleConfig() {
411
411
  //#endregion
412
412
  //#region package.json
413
413
  var name = "aiex-cli";
414
- var version = "0.0.1-beta.6";
414
+ var version = "0.0.1-beta.8";
415
415
  var description = "JSON Schema → SQLite with AI-powered data extraction";
416
416
  var package_default = {
417
417
  name,
@@ -452,13 +452,15 @@ var package_default = {
452
452
  files: [
453
453
  "bin",
454
454
  "dist",
455
- "src/core/schema-sqlite/migrate-helper.ts"
455
+ "src/core/schema-sqlite/migrate-helper.ts",
456
+ "src/core/schema-sqlite/migration-name.ts"
456
457
  ],
457
458
  scripts: {
458
459
  "build": "tsdown && pnpm --filter aiex-web build",
459
460
  "dev": "tsdown --watch",
460
461
  "start": "tsx src/index.ts",
461
462
  "test": "vitest",
463
+ "coverage": "vitest --coverage",
462
464
  "typecheck": "tsc",
463
465
  "lint": "eslint .",
464
466
  "prepublishOnly": "cp ../../README.md . && pnpm run build",
@@ -491,6 +493,7 @@ var package_default = {
491
493
  "@types/better-sqlite3": "catalog:types",
492
494
  "@types/node": "catalog:types",
493
495
  "@types/update-notifier": "catalog:",
496
+ "@vitest/coverage-v8": "catalog:testing",
494
497
  "eslint": "catalog:cli",
495
498
  "publint": "catalog:cli",
496
499
  "tsdown": "catalog:cli",
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as doctorDiagnosticsTableRows, S as buildDoctorDiagnostics, _ as generateDrizzleConfig, g as createMigrationConfig, t as collectDoctorDiagnostics, v as JsonSchemaDefinitionSchema, w as formatDoctorDiagnosticsJson, x as generateDrizzleSchema, y as parseJsonSchema } from "./doctor-BTfByg-I.mjs";
1
+ import { C as doctorDiagnosticsTableRows, S as buildDoctorDiagnostics, _ as generateDrizzleConfig, g as createMigrationConfig, t as collectDoctorDiagnostics, v as JsonSchemaDefinitionSchema, w as formatDoctorDiagnosticsJson, x as generateDrizzleSchema, y as parseJsonSchema } from "./doctor-DxG7uaGR.mjs";
2
2
 
3
3
  export { JsonSchemaDefinitionSchema, buildDoctorDiagnostics, collectDoctorDiagnostics, createMigrationConfig, doctorDiagnosticsTableRows, formatDoctorDiagnosticsJson, generateDrizzleConfig, generateDrizzleSchema, parseJsonSchema };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiex-cli",
3
3
  "type": "module",
4
- "version": "0.0.1-beta.6",
4
+ "version": "0.0.1-beta.8",
5
5
  "description": "JSON Schema → SQLite with AI-powered data extraction",
6
6
  "author": "OSpoon <zxin088@gmail.com>",
7
7
  "license": "MIT",
@@ -39,7 +39,8 @@
39
39
  "files": [
40
40
  "bin",
41
41
  "dist",
42
- "src/core/schema-sqlite/migrate-helper.ts"
42
+ "src/core/schema-sqlite/migrate-helper.ts",
43
+ "src/core/schema-sqlite/migration-name.ts"
43
44
  ],
44
45
  "dependencies": {
45
46
  "@ai-sdk/openai-compatible": "^2.0.47",
@@ -68,6 +69,7 @@
68
69
  "@types/better-sqlite3": "^7.6.0",
69
70
  "@types/node": "^25.6.0",
70
71
  "@types/update-notifier": "^6.0.8",
72
+ "@vitest/coverage-v8": "^4.1.4",
71
73
  "eslint": "^10.2.0",
72
74
  "publint": "^0.3.18",
73
75
  "tsdown": "^0.17.3",
@@ -80,6 +82,7 @@
80
82
  "dev": "tsdown --watch",
81
83
  "start": "tsx src/index.ts",
82
84
  "test": "vitest",
85
+ "coverage": "vitest --coverage",
83
86
  "typecheck": "tsc",
84
87
  "lint": "eslint ."
85
88
  }
@@ -0,0 +1,14 @@
1
+ export function sanitizeMigrationName(name?: string): string | undefined {
2
+ if (!name)
3
+ return undefined
4
+
5
+ const slug = name
6
+ .trim()
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9_\s-]/g, '_')
9
+ .replace(/[\s-]+/g, '_')
10
+ .replace(/_+/g, '_')
11
+ .replace(/^_+|_+$/g, '')
12
+
13
+ return slug || undefined
14
+ }