aiex-cli 0.0.6 → 0.0.7-beta.1

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.
@@ -9,83 +9,9 @@ import { readFile, writeFile } from "jsonfile";
9
9
  import Conf from "conf";
10
10
  import { z } from "zod";
11
11
 
12
- //#region src/core/doctor.ts
13
- function buildDoctorDiagnostics(input) {
14
- return {
15
- cli: {
16
- name: input.pkg.name,
17
- version: input.pkg.version,
18
- executable: input.executable
19
- },
20
- runtime: {
21
- node: input.node,
22
- platform: input.platform,
23
- arch: input.arch,
24
- shell: input.shell,
25
- packageManager: input.packageManager
26
- },
27
- system: {
28
- os: `${input.osType} ${input.osRelease}`,
29
- cwd: input.cwd
30
- },
31
- imageOcr: { ...input.imageOcr },
32
- config: {
33
- path: input.configPath,
34
- keys: [...input.configStoreKeys].sort()
35
- },
36
- project: { ...input.project }
37
- };
38
- }
39
- function formatDoctorDiagnosticsJson(d) {
40
- return `${JSON.stringify(d, null, 2)}\n`;
41
- }
42
- function doctorDiagnosticsTableRows(d) {
43
- const rows = [
44
- ["node", d.runtime.node],
45
- ["platform", d.runtime.platform],
46
- ["arch", d.runtime.arch],
47
- ["shell", d.runtime.shell],
48
- ["packageManager", d.runtime.packageManager],
49
- ["os", d.system.os],
50
- ["cwd", d.system.cwd],
51
- ["config", d.config.path],
52
- ["configKeys", d.config.keys.join(", ")]
53
- ];
54
- const p = d.project;
55
- rows.push(["aiexDir", p.aiexDir]);
56
- rows.push(["dirExists", String(p.dirExists)]);
57
- rows.push(["schemaFiles", `${p.schemaCount} (${p.schemaFiles.join(", ") || "none"})`]);
58
- rows.push(["aiConfig", String(p.aiConfig)]);
59
- rows.push(["aiApiKeySet", String(p.aiApiKeySet)]);
60
- rows.push(["aiModels", p.aiModelCount ? p.aiModels.join(", ") : "none"]);
61
- rows.push(["aiVisionModels", String(p.aiVisionModelCount)]);
62
- rows.push(["aiStructuredOutputModels", String(p.aiStructuredOutputModelCount)]);
63
- rows.push(["aiProvider", p.aiProvider ?? "none"]);
64
- rows.push(["aiConnectionOk", p.aiConnectionOk === null ? "not tested" : String(p.aiConnectionOk)]);
65
- rows.push(["pdfConverter", p.pdfConverter ?? "none"]);
66
- rows.push(["pdfConverterOk", p.pdfConverterOk === null ? "not tested" : String(p.pdfConverterOk)]);
67
- if (p.pdfConverterError) rows.push(["pdfConverterError", p.pdfConverterError]);
68
- rows.push(["imageOcrPlatform", String(d.imageOcr.platformSupported)]);
69
- rows.push(["imageOcrDependency", String(d.imageOcr.dependencyLoaded)]);
70
- rows.push(["imageOcrOk", d.imageOcr.ocrOk === null ? "not tested" : String(d.imageOcr.ocrOk)]);
71
- if (d.imageOcr.imagePath) rows.push(["imageOcrImage", d.imageOcr.imagePath]);
72
- if (d.imageOcr.recognizedText) rows.push(["imageOcrText", d.imageOcr.recognizedText]);
73
- if (typeof d.imageOcr.confidence === "number") rows.push(["imageOcrConfidence", `${(d.imageOcr.confidence * 100).toFixed(1)}%`]);
74
- if (d.imageOcr.error) rows.push(["imageOcrError", d.imageOcr.error]);
75
- rows.push(["hasDatabase", String(p.hasDatabase)]);
76
- rows.push(["databaseTablesOk", p.databaseTablesOk === null ? "not tested" : String(p.databaseTablesOk)]);
77
- if (p.missingDatabaseTables.length) rows.push(["missingDatabaseTables", p.missingDatabaseTables.join(", ")]);
78
- rows.push(["migrations", String(p.migrationCount)]);
79
- rows.push(["schemaValid", `${p.schemaValidCount}/${p.schemaCount}`]);
80
- for (const invalid of p.invalidSchemas) rows.push(["invalidSchema", `${invalid.file}: ${invalid.error}`]);
81
- for (const err of p.errors) rows.push(["error", err]);
82
- return rows;
83
- }
84
-
85
- //#endregion
86
12
  //#region package.json
87
13
  var name = "aiex-cli";
88
- var version = "0.0.6";
14
+ var version = "0.0.7-beta.1";
89
15
  var description = "JSON Schema → SQLite with AI-powered data extraction";
90
16
  var package_default = {
91
17
  name,
@@ -219,110 +145,7 @@ function seedConfig(config = createConfig()) {
219
145
  }
220
146
 
221
147
  //#endregion
222
- //#region src/core/ai-extraction/schemas.ts
223
- const ModelCapabilitiesSchema = z.object({
224
- vision: z.boolean(),
225
- structuredOutput: z.boolean(),
226
- maxTokens: z.number().int().positive().optional(),
227
- maxOutputTokens: z.number().int().positive().optional()
228
- });
229
- const AIModelConfigSchema = z.object({
230
- name: z.string().min(1),
231
- capabilities: ModelCapabilitiesSchema
232
- });
233
- const AIProviderConfigSchema = z.object({
234
- baseURL: z.string().min(1),
235
- apiKey: z.string(),
236
- models: z.array(AIModelConfigSchema).min(1),
237
- timeout: z.number().int().positive().default(300).optional()
238
- });
239
- const PromptConfigSchema = z.object({
240
- systemTemplate: z.string().min(1),
241
- userTemplate: z.string().min(1)
242
- });
243
- const ExtractionConfigSchema = z.object({ outputDir: z.string().min(1) });
244
- const ImageOcrFallbackSchema = z.preprocess((value) => [
245
- "auto",
246
- "off",
247
- "local"
248
- ].includes(String(value)) ? "localAuto" : value, z.literal("localAuto").default("localAuto").optional());
249
- const ImageOcrConfigSchema = z.object({
250
- ocrFallback: ImageOcrFallbackSchema,
251
- ocrLanguages: z.string().min(1).optional(),
252
- ocrMinConfidence: z.number().min(0).max(1).optional()
253
- });
254
- const ExternalPdfConverterConfigSchema = z.object({
255
- command: z.string().min(1),
256
- args: z.array(z.string()).min(1).refine((args) => args.some((arg) => arg.includes("{input}")), { message: "args must contain {input} template variable" }),
257
- outputFile: z.string().min(1).optional(),
258
- timeout: z.number().int().positive().default(600).optional(),
259
- fallbackToUnpdf: z.boolean().optional()
260
- });
261
- const MineruApiPdfConverterConfigSchema = z.object({
262
- token: z.string(),
263
- baseURL: z.string().url().optional(),
264
- modelVersion: z.string().optional(),
265
- isOcr: z.boolean().optional(),
266
- enableFormula: z.boolean().optional(),
267
- enableTable: z.boolean().optional()
268
- });
269
- const PdfConfigSchema = z.preprocess((value) => {
270
- if (!value || typeof value !== "object") return value;
271
- const config = { ...value };
272
- if (config.converter === "markitdown" && config.markitdown) {
273
- config.converter = "external";
274
- config.external = config.markitdown;
275
- } else if (config.converter === "marker" && config.marker) {
276
- config.converter = "external";
277
- config.external = config.marker;
278
- } else if (config.converter === "markitdown" || config.converter === "marker") config.converter = "unpdf";
279
- delete config.markitdown;
280
- delete config.marker;
281
- return config;
282
- }, z.object({
283
- converter: z.enum([
284
- "unpdf",
285
- "mineru",
286
- "mineru_api",
287
- "external"
288
- ]),
289
- mineru: ExternalPdfConverterConfigSchema.optional(),
290
- mineruApi: MineruApiPdfConverterConfigSchema.optional(),
291
- external: ExternalPdfConverterConfigSchema.optional()
292
- }));
293
- const LangfuseConfigSchema = z.object({
294
- publicKey: z.string(),
295
- secretKey: z.string(),
296
- host: z.string().optional()
297
- });
298
- const NotionSchemaConfigSchema = z.object({
299
- databaseId: z.string(),
300
- titleProperty: z.string().optional(),
301
- fieldMap: z.record(z.string()).optional()
302
- });
303
- const NotionConfigSchema = z.object({
304
- enabled: z.boolean(),
305
- token: z.string(),
306
- schemas: z.record(NotionSchemaConfigSchema).default({})
307
- });
308
- const WebhookConfigSchema = z.object({
309
- enabled: z.boolean(),
310
- url: z.string(),
311
- secret: z.string().optional()
312
- });
313
- const AIConfigSchema = z.object({
314
- provider: AIProviderConfigSchema,
315
- prompt: PromptConfigSchema,
316
- extraction: ExtractionConfigSchema,
317
- image: ImageOcrConfigSchema.optional(),
318
- pdf: PdfConfigSchema.optional(),
319
- langfuse: LangfuseConfigSchema.optional(),
320
- notion: NotionConfigSchema.optional(),
321
- webhook: WebhookConfigSchema.optional()
322
- });
323
-
324
- //#endregion
325
- //#region src/core/ai-extraction/types.ts
148
+ //#region src/domain/ai/types.ts
326
149
  const PLACEHOLDER_SCHEMA = "{schema}";
327
150
  const PLACEHOLDER_TEXT = "{text}";
328
151
  const DEFAULT_MODELS = [{
@@ -395,63 +218,507 @@ const DEFAULT_AI_CONFIG = {
395
218
  };
396
219
 
397
220
  //#endregion
398
- //#region src/core/ai-extraction/config.ts
399
- const CONFIG_FILE_NAME = "ai-config.json";
400
- const GITIGNORE_FILE = ".gitignore";
401
- async function readAIConfig(aiexDir) {
402
- const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
403
- try {
404
- const parsed = await readFile(configPath);
405
- return AIConfigSchema.parse(parsed);
406
- } catch {
407
- return null;
408
- }
409
- }
410
- async function writeAIConfig(aiexDir, config) {
411
- const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
412
- await fs.mkdir(aiexDir, { recursive: true });
413
- await writeFile(configPath, config, {
414
- spaces: 2,
415
- EOL: "\n"
416
- });
417
- await addToGitignore(aiexDir, CONFIG_FILE_NAME);
418
- }
419
- function getDefaultAIConfig() {
420
- return structuredClone(DEFAULT_AI_CONFIG);
221
+ //#region src/domain/doctor/diagnostics.ts
222
+ function buildDoctorDiagnostics(input) {
223
+ return {
224
+ cli: {
225
+ name: input.pkg.name,
226
+ version: input.pkg.version,
227
+ executable: input.executable
228
+ },
229
+ runtime: {
230
+ node: input.node,
231
+ platform: input.platform,
232
+ arch: input.arch,
233
+ shell: input.shell,
234
+ packageManager: input.packageManager
235
+ },
236
+ system: {
237
+ os: `${input.osType} ${input.osRelease}`,
238
+ cwd: input.cwd
239
+ },
240
+ imageOcr: { ...input.imageOcr },
241
+ config: {
242
+ path: input.configPath,
243
+ keys: [...input.configStoreKeys].sort()
244
+ },
245
+ project: { ...input.project }
246
+ };
421
247
  }
422
- async function addToGitignore(aiexDir, fileName) {
423
- const projectRoot = path.dirname(aiexDir);
424
- const gitignorePath = path.join(projectRoot, GITIGNORE_FILE);
425
- try {
426
- const content = await fs.readFile(gitignorePath, "utf-8");
427
- if (content.split("\n").some((line) => line.trim() === fileName || line.includes(".aiex/"))) return;
428
- const newContent = content.endsWith("\n") ? `${content}${fileName}\n` : `${content}\n${fileName}\n`;
429
- await fs.writeFile(gitignorePath, newContent);
430
- } catch {
431
- await fs.writeFile(gitignorePath, `${fileName}\n`);
432
- }
248
+ function formatDoctorDiagnosticsJson(d) {
249
+ return `${JSON.stringify(d, null, 2)}\n`;
433
250
  }
434
-
435
- //#endregion
436
- //#region src/locales/en.ts
437
- const en = {
438
- common: {
439
- done: "Done!",
440
- failed: "Failed!",
441
- cancelled: "Cancelled",
442
- save: "Save",
443
- cancel: "Cancel",
444
- delete: "Delete",
445
- close: "Close",
446
- loading: "Loading...",
447
- unknownError: "Unknown error"
448
- },
449
- cli: { description: "JSON Schema → SQLite with AI-powered data extraction" },
450
- command: {
451
- web: {
452
- description: "Start visual JSON Schema editor",
453
- args: { port: "Port to listen on" },
454
- starting: "Starting web server...",
251
+ function doctorDiagnosticsTableRows(d) {
252
+ const rows = [
253
+ ["node", d.runtime.node],
254
+ ["platform", d.runtime.platform],
255
+ ["arch", d.runtime.arch],
256
+ ["shell", d.runtime.shell],
257
+ ["packageManager", d.runtime.packageManager],
258
+ ["os", d.system.os],
259
+ ["cwd", d.system.cwd],
260
+ ["config", d.config.path],
261
+ ["configKeys", d.config.keys.join(", ")]
262
+ ];
263
+ const p = d.project;
264
+ rows.push(["aiexDir", p.aiexDir]);
265
+ rows.push(["dirExists", String(p.dirExists)]);
266
+ rows.push(["schemaFiles", `${p.schemaCount} (${p.schemaFiles.join(", ") || "none"})`]);
267
+ rows.push(["aiConfig", String(p.aiConfig)]);
268
+ rows.push(["aiApiKeySet", String(p.aiApiKeySet)]);
269
+ rows.push(["aiModels", p.aiModelCount ? p.aiModels.join(", ") : "none"]);
270
+ rows.push(["aiVisionModels", String(p.aiVisionModelCount)]);
271
+ rows.push(["aiStructuredOutputModels", String(p.aiStructuredOutputModelCount)]);
272
+ rows.push(["aiProvider", p.aiProvider ?? "none"]);
273
+ rows.push(["aiConnectionOk", p.aiConnectionOk === null ? "not tested" : String(p.aiConnectionOk)]);
274
+ rows.push(["pdfConverter", p.pdfConverter ?? "none"]);
275
+ rows.push(["pdfConverterOk", p.pdfConverterOk === null ? "not tested" : String(p.pdfConverterOk)]);
276
+ if (p.pdfConverterError) rows.push(["pdfConverterError", p.pdfConverterError]);
277
+ rows.push(["imageOcrPlatform", String(d.imageOcr.platformSupported)]);
278
+ rows.push(["imageOcrDependency", String(d.imageOcr.dependencyLoaded)]);
279
+ rows.push(["imageOcrOk", d.imageOcr.ocrOk === null ? "not tested" : String(d.imageOcr.ocrOk)]);
280
+ if (d.imageOcr.imagePath) rows.push(["imageOcrImage", d.imageOcr.imagePath]);
281
+ if (d.imageOcr.recognizedText) rows.push(["imageOcrText", d.imageOcr.recognizedText]);
282
+ if (typeof d.imageOcr.confidence === "number") rows.push(["imageOcrConfidence", `${(d.imageOcr.confidence * 100).toFixed(1)}%`]);
283
+ if (d.imageOcr.error) rows.push(["imageOcrError", d.imageOcr.error]);
284
+ rows.push(["hasDatabase", String(p.hasDatabase)]);
285
+ rows.push(["databaseTablesOk", p.databaseTablesOk === null ? "not tested" : String(p.databaseTablesOk)]);
286
+ if (p.missingDatabaseTables.length) rows.push(["missingDatabaseTables", p.missingDatabaseTables.join(", ")]);
287
+ rows.push(["migrations", String(p.migrationCount)]);
288
+ rows.push(["schemaValid", `${p.schemaValidCount}/${p.schemaCount}`]);
289
+ for (const invalid of p.invalidSchemas) rows.push(["invalidSchema", `${invalid.file}: ${invalid.error}`]);
290
+ for (const err of p.errors) rows.push(["error", err]);
291
+ return rows;
292
+ }
293
+
294
+ //#endregion
295
+ //#region src/domain/schema/parser.ts
296
+ function toSnakeCase(str) {
297
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
298
+ }
299
+ function mapPropertyToColumn(name$1, property, isRequired) {
300
+ const snakeName = toSnakeCase(name$1);
301
+ let drizzleType;
302
+ const isPrimary = property.primary ?? false;
303
+ const isAutoIncrement = property.autoIncrement ?? false;
304
+ switch (property.type) {
305
+ case "string": {
306
+ const format = property.format;
307
+ if (format === "date-time" || property.drizzle?.mode === "timestamp") drizzleType = `integer({ mode: 'timestamp' })`;
308
+ else if (format === "json" || property.drizzle?.mode === "json") drizzleType = `text({ mode: 'json' })`;
309
+ else drizzleType = "text()";
310
+ break;
311
+ }
312
+ case "integer": {
313
+ const mode = property.drizzle?.mode;
314
+ if (mode === "boolean") drizzleType = `integer({ mode: 'boolean' })`;
315
+ else if (mode === "timestamp" || mode === "timestamp_ms") drizzleType = `integer({ mode: '${mode}' })`;
316
+ else if (mode === "bigint") drizzleType = `integer({ mode: 'bigint' })`;
317
+ else drizzleType = "integer()";
318
+ break;
319
+ }
320
+ case "number":
321
+ drizzleType = "real()";
322
+ break;
323
+ case "boolean":
324
+ drizzleType = `integer({ mode: 'boolean' })`;
325
+ break;
326
+ case "object":
327
+ case "array":
328
+ drizzleType = `text({ mode: 'json' })`;
329
+ break;
330
+ case "null":
331
+ drizzleType = "text()";
332
+ break;
333
+ default: drizzleType = "text()";
334
+ }
335
+ return {
336
+ name: snakeName,
337
+ drizzleType,
338
+ isPrimary,
339
+ isAutoIncrement,
340
+ isNullable: !isRequired && !isPrimary,
341
+ isUnique: property.unique ?? false,
342
+ defaultValue: property.default !== void 0 ? JSON.stringify(property.default) : void 0,
343
+ isForeignKey: property.foreignKey !== void 0,
344
+ foreignKeyRef: property.foreignKey ?? void 0
345
+ };
346
+ }
347
+ function parseObjectToTable(schema, _warnings) {
348
+ const tableName = schema.table.name;
349
+ const columns = [];
350
+ const requiredFields = new Set(schema.required ?? []);
351
+ const autoColumns = /* @__PURE__ */ new Set();
352
+ if (schema.table.timestamps) {
353
+ autoColumns.add("created_at");
354
+ autoColumns.add("updated_at");
355
+ }
356
+ if (schema.table.softDelete) autoColumns.add("deleted_at");
357
+ for (const [propName, prop] of Object.entries(schema.properties)) {
358
+ if (prop.nested?.enabled && (prop.type === "object" || prop.type === "array")) continue;
359
+ if (prop.type === "array" && prop.items?.nested?.enabled) continue;
360
+ const snakeName = toSnakeCase(propName);
361
+ if (autoColumns.has(snakeName)) continue;
362
+ const column = mapPropertyToColumn(propName, prop, requiredFields.has(propName));
363
+ columns.push(column);
364
+ }
365
+ if (schema.table.timestamps) {
366
+ columns.push({
367
+ name: "created_at",
368
+ drizzleType: `integer({ mode: 'timestamp' })`,
369
+ isPrimary: false,
370
+ isAutoIncrement: false,
371
+ isNullable: false,
372
+ isUnique: false,
373
+ defaultValue: void 0
374
+ });
375
+ columns.push({
376
+ name: "updated_at",
377
+ drizzleType: `integer({ mode: 'timestamp' })`,
378
+ isPrimary: false,
379
+ isAutoIncrement: false,
380
+ isNullable: false,
381
+ isUnique: false,
382
+ defaultValue: void 0
383
+ });
384
+ }
385
+ if (schema.table.softDelete) columns.push({
386
+ name: "deleted_at",
387
+ drizzleType: `integer({ mode: 'timestamp' })`,
388
+ isPrimary: false,
389
+ isAutoIncrement: false,
390
+ isNullable: true,
391
+ isUnique: false,
392
+ defaultValue: void 0
393
+ });
394
+ return {
395
+ name: tableName,
396
+ columns
397
+ };
398
+ }
399
+ function parseNestedObject(propName, property, parentTableName, warnings) {
400
+ const nestedTableName = `${parentTableName}_${toSnakeCase(propName)}`;
401
+ const columns = [];
402
+ const relationType = property.nested?.relation === "has-many" ? "has-many" : "has-one";
403
+ columns.push({
404
+ name: "id",
405
+ drizzleType: "integer()",
406
+ isPrimary: true,
407
+ isAutoIncrement: true,
408
+ isNullable: false,
409
+ isUnique: false,
410
+ defaultValue: void 0
411
+ });
412
+ columns.push({
413
+ name: `${parentTableName}_id`,
414
+ drizzleType: "integer()",
415
+ isPrimary: false,
416
+ isAutoIncrement: false,
417
+ isNullable: false,
418
+ isUnique: false,
419
+ defaultValue: void 0,
420
+ isForeignKey: true,
421
+ foreignKeyRef: {
422
+ table: parentTableName,
423
+ column: "id"
424
+ }
425
+ });
426
+ if (property.type === "object" && property.properties) for (const [childName, childProp] of Object.entries(property.properties)) {
427
+ if (childProp.nested?.enabled) {
428
+ warnings.push(`Nested property "${childName}" inside "${nestedTableName}" is skipped — only one level of nesting is supported. Remove nested.enabled or use drizzle.mode: 'json' instead.`);
429
+ continue;
430
+ }
431
+ const column = mapPropertyToColumn(childName, childProp, false);
432
+ columns.push(column);
433
+ }
434
+ const relation = {
435
+ fromTable: nestedTableName,
436
+ fromColumn: `${parentTableName}_id`,
437
+ toTable: parentTableName,
438
+ toColumn: "id",
439
+ name: parentTableName
440
+ };
441
+ const reverseRelation = {
442
+ type: relationType,
443
+ fromTable: parentTableName,
444
+ toTable: nestedTableName,
445
+ name: toSnakeCase(propName)
446
+ };
447
+ return {
448
+ table: {
449
+ name: nestedTableName,
450
+ columns
451
+ },
452
+ relation,
453
+ reverseRelation
454
+ };
455
+ }
456
+ function parseJsonSchema(schema) {
457
+ const tables = [];
458
+ const relations = [];
459
+ const reverseRelations = [];
460
+ const warnings = [];
461
+ const mainTable = parseObjectToTable(schema, warnings);
462
+ tables.push(mainTable);
463
+ for (const [propName, prop] of Object.entries(schema.properties)) if (prop.type === "object" && prop.nested?.enabled) {
464
+ const nested = parseNestedObject(propName, prop, mainTable.name, warnings);
465
+ tables.push(nested.table);
466
+ relations.push(nested.relation);
467
+ reverseRelations.push(nested.reverseRelation);
468
+ } else if (prop.type === "array" && prop.items?.nested?.enabled && prop.items?.type === "object" && prop.items.properties) {
469
+ const nested = parseNestedObject(propName, prop.items, mainTable.name, warnings);
470
+ tables.push(nested.table);
471
+ relations.push(nested.relation);
472
+ reverseRelations.push(nested.reverseRelation);
473
+ }
474
+ return {
475
+ tables,
476
+ relations,
477
+ reverseRelations,
478
+ warnings
479
+ };
480
+ }
481
+
482
+ //#endregion
483
+ //#region src/domain/schema/schemas.ts
484
+ const DrizzleModeSchema = z.enum([
485
+ "json",
486
+ "timestamp",
487
+ "timestamp_ms",
488
+ "boolean",
489
+ "bigint"
490
+ ]);
491
+ const DrizzleExtensionSchema = z.object({
492
+ mode: DrizzleModeSchema.optional(),
493
+ customType: z.string().optional()
494
+ }).optional();
495
+ const NestedConfigSchema = z.object({
496
+ enabled: z.literal(true),
497
+ relation: z.enum(["has-one", "has-many"])
498
+ });
499
+ const ForeignKeyRefSchema = z.object({
500
+ table: z.string().min(1),
501
+ column: z.string().min(1)
502
+ });
503
+ const JsonSchemaPropertySchema = z.lazy(() => z.object({
504
+ description: z.string().optional(),
505
+ type: z.enum([
506
+ "string",
507
+ "integer",
508
+ "number",
509
+ "boolean",
510
+ "object",
511
+ "array",
512
+ "null"
513
+ ]),
514
+ format: z.string().optional(),
515
+ primary: z.boolean().optional(),
516
+ autoIncrement: z.boolean().optional(),
517
+ unique: z.boolean().optional(),
518
+ default: z.unknown().optional(),
519
+ maxLength: z.number().int().positive().optional(),
520
+ minLength: z.number().int().nonnegative().optional(),
521
+ minimum: z.number().optional(),
522
+ maximum: z.number().optional(),
523
+ drizzle: DrizzleExtensionSchema,
524
+ nested: NestedConfigSchema.optional(),
525
+ foreignKey: ForeignKeyRefSchema.optional(),
526
+ properties: z.record(z.string(), JsonSchemaPropertySchema).optional(),
527
+ items: JsonSchemaPropertySchema.optional(),
528
+ required: z.array(z.string()).optional()
529
+ }));
530
+ const TableConfigSchema = z.object({
531
+ name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "Table name must be snake_case (lowercase letters, digits, underscores)"),
532
+ timestamps: z.boolean().optional(),
533
+ softDelete: z.boolean().optional()
534
+ });
535
+ const ExamplePairSchema = z.object({
536
+ text: z.string().min(1),
537
+ output: z.record(z.string(), z.unknown())
538
+ });
539
+ const JsonSchemaDefinitionSchema = z.object({
540
+ $schema: z.string().optional(),
541
+ title: z.string().min(1),
542
+ description: z.string().optional(),
543
+ type: z.literal("object"),
544
+ table: TableConfigSchema,
545
+ properties: z.record(z.string(), JsonSchemaPropertySchema),
546
+ required: z.array(z.string()).optional(),
547
+ examples: z.array(ExamplePairSchema).optional()
548
+ }).refine((schema) => Object.keys(schema.properties).length >= 1, {
549
+ message: "At least one property is required",
550
+ path: ["properties"]
551
+ }).refine((schema) => !schema.required || schema.required.every((r) => r in schema.properties), {
552
+ message: "All required fields must be defined in properties",
553
+ path: ["required"]
554
+ }).refine((schema) => {
555
+ return !Object.values(schema.properties).some((p) => p.primary) || Object.values(schema.properties).filter((p) => p.primary).length === 1;
556
+ }, {
557
+ message: "Only one primary key is allowed per table",
558
+ path: ["properties"]
559
+ });
560
+
561
+ //#endregion
562
+ //#region src/domain/ai/schemas.ts
563
+ const ModelCapabilitiesSchema = z.object({
564
+ vision: z.boolean(),
565
+ structuredOutput: z.boolean(),
566
+ maxTokens: z.number().int().positive().optional(),
567
+ maxOutputTokens: z.number().int().positive().optional()
568
+ });
569
+ const AIModelConfigSchema = z.object({
570
+ name: z.string().min(1),
571
+ capabilities: ModelCapabilitiesSchema
572
+ });
573
+ const AIProviderConfigSchema = z.object({
574
+ baseURL: z.string().min(1),
575
+ apiKey: z.string(),
576
+ models: z.array(AIModelConfigSchema).min(1),
577
+ timeout: z.number().int().positive().default(300).optional()
578
+ });
579
+ const PromptConfigSchema = z.object({
580
+ systemTemplate: z.string().min(1),
581
+ userTemplate: z.string().min(1)
582
+ });
583
+ const ExtractionConfigSchema = z.object({ outputDir: z.string().min(1) });
584
+ const ImageOcrFallbackSchema = z.preprocess((value) => [
585
+ "auto",
586
+ "off",
587
+ "local"
588
+ ].includes(String(value)) ? "localAuto" : value, z.literal("localAuto").default("localAuto").optional());
589
+ const ImageOcrConfigSchema = z.object({
590
+ ocrFallback: ImageOcrFallbackSchema,
591
+ ocrLanguages: z.string().min(1).optional(),
592
+ ocrMinConfidence: z.number().min(0).max(1).optional()
593
+ });
594
+ const ExternalPdfConverterConfigSchema = z.object({
595
+ command: z.string().min(1),
596
+ args: z.array(z.string()).min(1).refine((args) => args.some((arg) => arg.includes("{input}")), { message: "args must contain {input} template variable" }),
597
+ outputFile: z.string().min(1).optional(),
598
+ timeout: z.number().int().positive().default(600).optional(),
599
+ fallbackToUnpdf: z.boolean().optional()
600
+ });
601
+ const MineruApiPdfConverterConfigSchema = z.object({
602
+ token: z.string(),
603
+ baseURL: z.string().url().optional(),
604
+ modelVersion: z.string().optional(),
605
+ isOcr: z.boolean().optional(),
606
+ enableFormula: z.boolean().optional(),
607
+ enableTable: z.boolean().optional()
608
+ });
609
+ const PdfConfigSchema = z.preprocess((value) => {
610
+ if (!value || typeof value !== "object") return value;
611
+ const config = { ...value };
612
+ if (config.converter === "markitdown" && config.markitdown) {
613
+ config.converter = "external";
614
+ config.external = config.markitdown;
615
+ } else if (config.converter === "marker" && config.marker) {
616
+ config.converter = "external";
617
+ config.external = config.marker;
618
+ } else if (config.converter === "markitdown" || config.converter === "marker") config.converter = "unpdf";
619
+ delete config.markitdown;
620
+ delete config.marker;
621
+ return config;
622
+ }, z.object({
623
+ converter: z.enum([
624
+ "unpdf",
625
+ "mineru",
626
+ "mineru_api",
627
+ "external"
628
+ ]),
629
+ mineru: ExternalPdfConverterConfigSchema.optional(),
630
+ mineruApi: MineruApiPdfConverterConfigSchema.optional(),
631
+ external: ExternalPdfConverterConfigSchema.optional()
632
+ }));
633
+ const LangfuseConfigSchema = z.object({
634
+ publicKey: z.string(),
635
+ secretKey: z.string(),
636
+ host: z.string().optional()
637
+ });
638
+ const NotionSchemaConfigSchema = z.object({
639
+ databaseId: z.string(),
640
+ titleProperty: z.string().optional(),
641
+ fieldMap: z.record(z.string()).optional()
642
+ });
643
+ const NotionConfigSchema = z.object({
644
+ enabled: z.boolean(),
645
+ token: z.string(),
646
+ schemas: z.record(NotionSchemaConfigSchema).default({})
647
+ });
648
+ const WebhookConfigSchema = z.object({
649
+ enabled: z.boolean(),
650
+ url: z.string(),
651
+ secret: z.string().optional()
652
+ });
653
+ const AIConfigSchema = z.object({
654
+ provider: AIProviderConfigSchema,
655
+ prompt: PromptConfigSchema,
656
+ extraction: ExtractionConfigSchema,
657
+ image: ImageOcrConfigSchema.optional(),
658
+ pdf: PdfConfigSchema.optional(),
659
+ langfuse: LangfuseConfigSchema.optional(),
660
+ notion: NotionConfigSchema.optional(),
661
+ webhook: WebhookConfigSchema.optional()
662
+ });
663
+
664
+ //#endregion
665
+ //#region src/infrastructure/ai/ai-config-store.ts
666
+ const CONFIG_FILE_NAME = "ai-config.json";
667
+ const GITIGNORE_FILE = ".gitignore";
668
+ async function readAIConfig(aiexDir) {
669
+ const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
670
+ try {
671
+ const parsed = await readFile(configPath);
672
+ return AIConfigSchema.parse(parsed);
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ async function writeAIConfig(aiexDir, config) {
678
+ const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
679
+ await fs.mkdir(aiexDir, { recursive: true });
680
+ await writeFile(configPath, config, {
681
+ spaces: 2,
682
+ EOL: "\n"
683
+ });
684
+ await addToGitignore(aiexDir, CONFIG_FILE_NAME);
685
+ }
686
+ function getDefaultAIConfig() {
687
+ return structuredClone(DEFAULT_AI_CONFIG);
688
+ }
689
+ async function addToGitignore(aiexDir, fileName) {
690
+ const projectRoot = path.dirname(aiexDir);
691
+ const gitignorePath = path.join(projectRoot, GITIGNORE_FILE);
692
+ try {
693
+ const content = await fs.readFile(gitignorePath, "utf-8");
694
+ if (content.split("\n").some((line) => line.trim() === fileName || line.includes(".aiex/"))) return;
695
+ const newContent = content.endsWith("\n") ? `${content}${fileName}\n` : `${content}\n${fileName}\n`;
696
+ await fs.writeFile(gitignorePath, newContent);
697
+ } catch {
698
+ await fs.writeFile(gitignorePath, `${fileName}\n`);
699
+ }
700
+ }
701
+
702
+ //#endregion
703
+ //#region src/locales/en.ts
704
+ const en = {
705
+ common: {
706
+ done: "Done!",
707
+ failed: "Failed!",
708
+ cancelled: "Cancelled",
709
+ save: "Save",
710
+ cancel: "Cancel",
711
+ delete: "Delete",
712
+ close: "Close",
713
+ loading: "Loading...",
714
+ unknownError: "Unknown error"
715
+ },
716
+ cli: { description: "JSON Schema → SQLite with AI-powered data extraction" },
717
+ command: {
718
+ web: {
719
+ description: "Start visual JSON Schema editor",
720
+ args: { port: "Port to listen on" },
721
+ starting: "Starting web server...",
455
722
  serverRunning: "Server running at {{url}}",
456
723
  schemaDir: "Schema directory: {{path}}",
457
724
  pressCtrlC: "Press Ctrl+C to stop",
@@ -901,499 +1168,169 @@ Extraction requirements:
901
1168
  deleteExample: "Delete Example",
902
1169
  inputPlaceholder: "Paste raw unstructured text example here...",
903
1170
  outputPlaceholder: "{ ... }",
904
- mustStartLowercase: "Must start with lowercase letter",
905
- onlyLowercaseAllow: "Only lowercase letters, digits, and underscores allowed",
906
- lessThan: "-"
907
- },
908
- extraction: { status: {
909
- succeeded: "succeeded",
910
- failed: "failed",
911
- stale: "stale",
912
- running: "running"
913
- } }
914
- };
915
-
916
- //#endregion
917
- //#region src/locales/i18n.ts
918
- let tFn = null;
919
- let i18nInstance = null;
920
- let initPromise = null;
921
- function detectLocale() {
922
- if ((process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "").startsWith("zh")) return "zh-CN";
923
- return "en";
924
- }
925
- function resolveFallback(key, options) {
926
- let value = en;
927
- const parts = key.split(".");
928
- for (const part of parts) if (value && typeof value === "object" && part in value) value = value[part];
929
- else return key;
930
- if (typeof value !== "string") return key;
931
- if (options) return Object.entries(options).reduce((str, [k, v]) => str.replace(`{{${k}}}`, String(v)), value);
932
- return value;
933
- }
934
- async function initI18n(lng) {
935
- if (i18nInstance) return;
936
- if (initPromise) return initPromise;
937
- initPromise = (async () => {
938
- const { createInstance } = await import("i18next");
939
- const instance = createInstance();
940
- i18nInstance = instance;
941
- const locale = lng ?? detectLocale();
942
- await instance.init({
943
- lng: locale,
944
- fallbackLng: "en",
945
- resources: {
946
- "en": { translation: en },
947
- "zh-CN": { translation: await import("./zh-CN-wEUNhuHM.mjs").then((m) => m.zhCN) }
948
- },
949
- interpolation: { escapeValue: false },
950
- returnNull: false
951
- });
952
- tFn = instance.t.bind(instance);
953
- })();
954
- return initPromise;
955
- }
956
- function t(key, options) {
957
- if (tFn) return tFn(key, options);
958
- return resolveFallback(key, options);
959
- }
960
-
961
- //#endregion
962
- //#region src/core/image-ocr/index.ts
963
- const DEFAULT_OCR_LANGUAGES = "en-US, zh-Hans";
964
- const SELF_CHECK_EXPECTED_TEXT = "AIEX";
965
- const defaultRuntime = {
966
- platform: process.platform,
967
- async loadLocalOcr() {
968
- return await import("@napi-rs/system-ocr");
969
- }
970
- };
971
- function imageOcrMode(config) {
972
- return config?.ocrFallback ?? "localAuto";
973
- }
974
- function hasVisionModel(aiConfig, modelOverride) {
975
- if (modelOverride) return modelOverride.capabilities.vision;
976
- return aiConfig?.provider.models.some((model) => model.capabilities.vision) ?? true;
977
- }
978
- function shouldUseImageOcrFallback(aiConfig, modelOverride, runtime = defaultRuntime) {
979
- if (hasVisionModel(aiConfig, modelOverride)) return false;
980
- if (imageOcrMode(aiConfig?.image) === "localAuto") return isLocalOcrPlatform(runtime.platform);
981
- return isLocalOcrPlatform(runtime.platform);
982
- }
983
- function isLocalOcrPlatform(platform) {
984
- return platform === "darwin" || platform === "win32";
985
- }
986
- function parseOcrLanguages(languages) {
987
- return (languages ?? DEFAULT_OCR_LANGUAGES).split(",").map((language) => language.trim()).filter(Boolean);
988
- }
989
- async function recognizeImageText(imagePath, config, runtime = defaultRuntime) {
990
- if (!isLocalOcrPlatform(runtime.platform)) throw new Error(t("errors.ocr.platformUnsupported", { platform: runtime.platform }));
991
- let localOcr;
992
- try {
993
- localOcr = await runtime.loadLocalOcr();
994
- } catch (error) {
995
- const message = error instanceof Error ? error.message : String(error);
996
- throw new Error(t("errors.ocr.unavailable", { error: message }));
997
- }
998
- const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, parseOcrLanguages(config?.ocrLanguages));
999
- const text = result.text.trim();
1000
- if (!text) throw new Error(t("errors.ocr.noText"));
1001
- const confidence = result.confidence;
1002
- const minConfidence = config?.ocrMinConfidence ?? 0;
1003
- if (confidence < minConfidence) throw new Error(t("errors.ocr.lowConfidence", {
1004
- confidence: (confidence * 100).toFixed(1),
1005
- min: (minConfidence * 100).toFixed(1)
1006
- }));
1007
- return {
1008
- text,
1009
- confidence
1010
- };
1011
- }
1012
- function normalizeOcrText(text) {
1013
- return text.replace(/\s+/g, "").trim().toUpperCase();
1014
- }
1015
- async function checkImageOcrAvailability(imagePath, runtime = defaultRuntime) {
1016
- if (!isLocalOcrPlatform(runtime.platform)) return {
1017
- platformSupported: false,
1018
- dependencyLoaded: false,
1019
- ocrOk: null,
1020
- imagePath,
1021
- error: t("errors.ocr.platformUnsupported", { platform: runtime.platform })
1022
- };
1023
- let localOcr;
1024
- try {
1025
- localOcr = await runtime.loadLocalOcr();
1026
- } catch (error) {
1027
- return {
1028
- platformSupported: true,
1029
- dependencyLoaded: false,
1030
- ocrOk: null,
1031
- imagePath,
1032
- error: error instanceof Error ? error.message : String(error)
1033
- };
1034
- }
1035
- if (!imagePath) return {
1036
- platformSupported: true,
1037
- dependencyLoaded: true,
1038
- ocrOk: null,
1039
- error: "No OCR self-check image was found."
1040
- };
1041
- try {
1042
- const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, ["en-US"]);
1043
- const recognizedText = result.text.trim();
1044
- const ocrOk = normalizeOcrText(recognizedText).includes(SELF_CHECK_EXPECTED_TEXT);
1045
- return {
1046
- platformSupported: true,
1047
- dependencyLoaded: true,
1048
- ocrOk,
1049
- imagePath,
1050
- recognizedText,
1051
- confidence: result.confidence,
1052
- error: ocrOk ? void 0 : `Expected OCR text "${SELF_CHECK_EXPECTED_TEXT}" was not recognized.`
1053
- };
1054
- } catch (error) {
1055
- return {
1056
- platformSupported: true,
1057
- dependencyLoaded: true,
1058
- ocrOk: false,
1059
- imagePath,
1060
- error: error instanceof Error ? error.message : String(error)
1061
- };
1062
- }
1063
- }
1171
+ mustStartLowercase: "Must start with lowercase letter",
1172
+ onlyLowercaseAllow: "Only lowercase letters, digits, and underscores allowed",
1173
+ lessThan: "-"
1174
+ },
1175
+ extraction: { status: {
1176
+ succeeded: "succeeded",
1177
+ failed: "failed",
1178
+ stale: "stale",
1179
+ running: "running"
1180
+ } }
1181
+ };
1064
1182
 
1065
1183
  //#endregion
1066
- //#region src/core/schema-sqlite/generator.ts
1067
- function generateColumnDefinition(column) {
1068
- if (column.isPrimary && column.isAutoIncrement) return ` ${column.name}: integer().primaryKey({ autoIncrement: true })`;
1069
- let def = ` ${column.name}: ${column.drizzleType}`;
1070
- if (column.isPrimary) def += ".primaryKey()";
1071
- if (!column.isNullable && !column.isPrimary) def += ".notNull()";
1072
- if (column.isUnique && !column.isPrimary) def += ".unique()";
1073
- if (column.defaultValue !== void 0) def += `.default(${column.defaultValue})`;
1074
- if (column.isForeignKey && column.foreignKeyRef) def += `.references(() => ${column.foreignKeyRef.table}.${column.foreignKeyRef.column})`;
1075
- return def;
1184
+ //#region src/locales/i18n.ts
1185
+ let tFn = null;
1186
+ let i18nInstance = null;
1187
+ let initPromise = null;
1188
+ function detectLocale() {
1189
+ if ((process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "").startsWith("zh")) return "zh-CN";
1190
+ return "en";
1076
1191
  }
1077
- function generateTableDefinition(table) {
1078
- const columns = table.columns.map(generateColumnDefinition);
1079
- return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n})`;
1192
+ function resolveFallback(key, options) {
1193
+ let value = en;
1194
+ const parts = key.split(".");
1195
+ for (const part of parts) if (value && typeof value === "object" && part in value) value = value[part];
1196
+ else return key;
1197
+ if (typeof value !== "string") return key;
1198
+ if (options) return Object.entries(options).reduce((str, [k, v]) => str.replace(`{{${k}}}`, String(v)), value);
1199
+ return value;
1080
1200
  }
1081
- function generateRelationDefinitions(relations, reverseRelations) {
1082
- if (relations.length === 0 && reverseRelations.length === 0) return "";
1083
- const definitions = [];
1084
- const childByTable = /* @__PURE__ */ new Map();
1085
- for (const rel of relations) {
1086
- const list = childByTable.get(rel.fromTable) ?? [];
1087
- list.push(rel);
1088
- childByTable.set(rel.fromTable, list);
1089
- }
1090
- const parentByTable = /* @__PURE__ */ new Map();
1091
- for (const rel of reverseRelations) {
1092
- const list = parentByTable.get(rel.fromTable) ?? [];
1093
- list.push(rel);
1094
- parentByTable.set(rel.fromTable, list);
1095
- }
1096
- const allTableNames = new Set([...childByTable.keys(), ...parentByTable.keys()]);
1097
- for (const tableName of allTableNames) {
1098
- const childRels = childByTable.get(tableName) ?? [];
1099
- const parentRels = parentByTable.get(tableName) ?? [];
1100
- const needsOne = childRels.length > 0 || parentRels.some((r) => r.type === "has-one");
1101
- const needsMany = parentRels.some((r) => r.type === "has-many");
1102
- const imports = [];
1103
- if (needsOne) imports.push("one");
1104
- if (needsMany) imports.push("many");
1105
- const importStr = imports.join(", ");
1106
- const relDefs = [];
1107
- for (const rel of childRels) relDefs.push(` ${rel.name}: one(${rel.toTable}, {\n fields: [${rel.fromTable}.${rel.fromColumn}],\n references: [${rel.toTable}.${rel.toColumn}],\n })`);
1108
- for (const rel of parentRels) if (rel.type === "has-many") relDefs.push(` ${rel.name}: many(${rel.toTable})`);
1109
- else relDefs.push(` ${rel.name}: one(${rel.toTable})`);
1110
- definitions.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${importStr} }) => ({\n${relDefs.join(",\n")}\n}))`);
1111
- }
1112
- return definitions.join("\n\n");
1201
+ async function initI18n(lng) {
1202
+ if (i18nInstance) return;
1203
+ if (initPromise) return initPromise;
1204
+ initPromise = (async () => {
1205
+ const { createInstance } = await import("i18next");
1206
+ const instance = createInstance();
1207
+ i18nInstance = instance;
1208
+ const locale = lng ?? detectLocale();
1209
+ await instance.init({
1210
+ lng: locale,
1211
+ fallbackLng: "en",
1212
+ resources: {
1213
+ "en": { translation: en },
1214
+ "zh-CN": { translation: await import("./zh-CN-wEUNhuHM.mjs").then((m) => m.zhCN) }
1215
+ },
1216
+ interpolation: { escapeValue: false },
1217
+ returnNull: false
1218
+ });
1219
+ tFn = instance.t.bind(instance);
1220
+ })();
1221
+ return initPromise;
1113
1222
  }
1114
- function generateDrizzleSchema(result) {
1115
- const imports = `import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'`;
1116
- const tableDefs = result.tables.map(generateTableDefinition).join("\n\n");
1117
- const relationDefs = generateRelationDefinitions(result.relations, result.reverseRelations);
1118
- const parts = [
1119
- imports,
1120
- "",
1121
- tableDefs
1122
- ];
1123
- if (relationDefs) parts.push("", relationDefs);
1124
- parts.push("");
1125
- return parts.join("\n");
1223
+ function t(key, options) {
1224
+ if (tFn) return tFn(key, options);
1225
+ return resolveFallback(key, options);
1126
1226
  }
1127
1227
 
1128
1228
  //#endregion
1129
- //#region src/core/schema-sqlite/parser.ts
1130
- function toSnakeCase(str) {
1131
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
1132
- }
1133
- function mapPropertyToColumn(name$1, property, isRequired) {
1134
- const snakeName = toSnakeCase(name$1);
1135
- let drizzleType;
1136
- const isPrimary = property.primary ?? false;
1137
- const isAutoIncrement = property.autoIncrement ?? false;
1138
- switch (property.type) {
1139
- case "string": {
1140
- const format = property.format;
1141
- if (format === "date-time" || property.drizzle?.mode === "timestamp") drizzleType = `integer({ mode: 'timestamp' })`;
1142
- else if (format === "json" || property.drizzle?.mode === "json") drizzleType = `text({ mode: 'json' })`;
1143
- else drizzleType = "text()";
1144
- break;
1145
- }
1146
- case "integer": {
1147
- const mode = property.drizzle?.mode;
1148
- if (mode === "boolean") drizzleType = `integer({ mode: 'boolean' })`;
1149
- else if (mode === "timestamp" || mode === "timestamp_ms") drizzleType = `integer({ mode: '${mode}' })`;
1150
- else if (mode === "bigint") drizzleType = `integer({ mode: 'bigint' })`;
1151
- else drizzleType = "integer()";
1152
- break;
1153
- }
1154
- case "number":
1155
- drizzleType = "real()";
1156
- break;
1157
- case "boolean":
1158
- drizzleType = `integer({ mode: 'boolean' })`;
1159
- break;
1160
- case "object":
1161
- case "array":
1162
- drizzleType = `text({ mode: 'json' })`;
1163
- break;
1164
- case "null":
1165
- drizzleType = "text()";
1166
- break;
1167
- default: drizzleType = "text()";
1229
+ //#region src/infrastructure/ocr/system-ocr.ts
1230
+ const DEFAULT_OCR_LANGUAGES = "en-US, zh-Hans";
1231
+ const SELF_CHECK_EXPECTED_TEXT = "AIEX";
1232
+ const defaultRuntime = {
1233
+ platform: process.platform,
1234
+ async loadLocalOcr() {
1235
+ return await import("@napi-rs/system-ocr");
1168
1236
  }
1169
- return {
1170
- name: snakeName,
1171
- drizzleType,
1172
- isPrimary,
1173
- isAutoIncrement,
1174
- isNullable: !isRequired && !isPrimary,
1175
- isUnique: property.unique ?? false,
1176
- defaultValue: property.default !== void 0 ? JSON.stringify(property.default) : void 0,
1177
- isForeignKey: property.foreignKey !== void 0,
1178
- foreignKeyRef: property.foreignKey ?? void 0
1179
- };
1237
+ };
1238
+ function imageOcrMode(config) {
1239
+ return config?.ocrFallback ?? "localAuto";
1180
1240
  }
1181
- function parseObjectToTable(schema, _warnings) {
1182
- const tableName = schema.table.name;
1183
- const columns = [];
1184
- const requiredFields = new Set(schema.required ?? []);
1185
- const autoColumns = /* @__PURE__ */ new Set();
1186
- if (schema.table.timestamps) {
1187
- autoColumns.add("created_at");
1188
- autoColumns.add("updated_at");
1189
- }
1190
- if (schema.table.softDelete) autoColumns.add("deleted_at");
1191
- for (const [propName, prop] of Object.entries(schema.properties)) {
1192
- if (prop.nested?.enabled && (prop.type === "object" || prop.type === "array")) continue;
1193
- if (prop.type === "array" && prop.items?.nested?.enabled) continue;
1194
- const snakeName = toSnakeCase(propName);
1195
- if (autoColumns.has(snakeName)) continue;
1196
- const column = mapPropertyToColumn(propName, prop, requiredFields.has(propName));
1197
- columns.push(column);
1198
- }
1199
- if (schema.table.timestamps) {
1200
- columns.push({
1201
- name: "created_at",
1202
- drizzleType: `integer({ mode: 'timestamp' })`,
1203
- isPrimary: false,
1204
- isAutoIncrement: false,
1205
- isNullable: false,
1206
- isUnique: false,
1207
- defaultValue: void 0
1208
- });
1209
- columns.push({
1210
- name: "updated_at",
1211
- drizzleType: `integer({ mode: 'timestamp' })`,
1212
- isPrimary: false,
1213
- isAutoIncrement: false,
1214
- isNullable: false,
1215
- isUnique: false,
1216
- defaultValue: void 0
1217
- });
1218
- }
1219
- if (schema.table.softDelete) columns.push({
1220
- name: "deleted_at",
1221
- drizzleType: `integer({ mode: 'timestamp' })`,
1222
- isPrimary: false,
1223
- isAutoIncrement: false,
1224
- isNullable: true,
1225
- isUnique: false,
1226
- defaultValue: void 0
1227
- });
1228
- return {
1229
- name: tableName,
1230
- columns
1231
- };
1241
+ function hasVisionModel(aiConfig, modelOverride) {
1242
+ if (modelOverride) return modelOverride.capabilities.vision;
1243
+ return aiConfig?.provider.models.some((model) => model.capabilities.vision) ?? true;
1232
1244
  }
1233
- function parseNestedObject(propName, property, parentTableName, warnings) {
1234
- const nestedTableName = `${parentTableName}_${toSnakeCase(propName)}`;
1235
- const columns = [];
1236
- const relationType = property.nested?.relation === "has-many" ? "has-many" : "has-one";
1237
- columns.push({
1238
- name: "id",
1239
- drizzleType: "integer()",
1240
- isPrimary: true,
1241
- isAutoIncrement: true,
1242
- isNullable: false,
1243
- isUnique: false,
1244
- defaultValue: void 0
1245
- });
1246
- columns.push({
1247
- name: `${parentTableName}_id`,
1248
- drizzleType: "integer()",
1249
- isPrimary: false,
1250
- isAutoIncrement: false,
1251
- isNullable: false,
1252
- isUnique: false,
1253
- defaultValue: void 0,
1254
- isForeignKey: true,
1255
- foreignKeyRef: {
1256
- table: parentTableName,
1257
- column: "id"
1258
- }
1259
- });
1260
- if (property.type === "object" && property.properties) for (const [childName, childProp] of Object.entries(property.properties)) {
1261
- if (childProp.nested?.enabled) {
1262
- warnings.push(`Nested property "${childName}" inside "${nestedTableName}" is skipped — only one level of nesting is supported. Remove nested.enabled or use drizzle.mode: 'json' instead.`);
1263
- continue;
1264
- }
1265
- const column = mapPropertyToColumn(childName, childProp, false);
1266
- columns.push(column);
1245
+ function shouldUseImageOcrFallback(aiConfig, modelOverride, runtime = defaultRuntime) {
1246
+ if (hasVisionModel(aiConfig, modelOverride)) return false;
1247
+ if (imageOcrMode(aiConfig?.image) === "localAuto") return isLocalOcrPlatform(runtime.platform);
1248
+ return isLocalOcrPlatform(runtime.platform);
1249
+ }
1250
+ function isLocalOcrPlatform(platform) {
1251
+ return platform === "darwin" || platform === "win32";
1252
+ }
1253
+ function parseOcrLanguages(languages) {
1254
+ return (languages ?? DEFAULT_OCR_LANGUAGES).split(",").map((language) => language.trim()).filter(Boolean);
1255
+ }
1256
+ async function recognizeImageText(imagePath, config, runtime = defaultRuntime) {
1257
+ if (!isLocalOcrPlatform(runtime.platform)) throw new Error(t("errors.ocr.platformUnsupported", { platform: runtime.platform }));
1258
+ let localOcr;
1259
+ try {
1260
+ localOcr = await runtime.loadLocalOcr();
1261
+ } catch (error) {
1262
+ const message = error instanceof Error ? error.message : String(error);
1263
+ throw new Error(t("errors.ocr.unavailable", { error: message }));
1267
1264
  }
1268
- const relation = {
1269
- fromTable: nestedTableName,
1270
- fromColumn: `${parentTableName}_id`,
1271
- toTable: parentTableName,
1272
- toColumn: "id",
1273
- name: parentTableName
1274
- };
1275
- const reverseRelation = {
1276
- type: relationType,
1277
- fromTable: parentTableName,
1278
- toTable: nestedTableName,
1279
- name: toSnakeCase(propName)
1280
- };
1265
+ const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, parseOcrLanguages(config?.ocrLanguages));
1266
+ const text = result.text.trim();
1267
+ if (!text) throw new Error(t("errors.ocr.noText"));
1268
+ const confidence = result.confidence;
1269
+ const minConfidence = config?.ocrMinConfidence ?? 0;
1270
+ if (confidence < minConfidence) throw new Error(t("errors.ocr.lowConfidence", {
1271
+ confidence: (confidence * 100).toFixed(1),
1272
+ min: (minConfidence * 100).toFixed(1)
1273
+ }));
1281
1274
  return {
1282
- table: {
1283
- name: nestedTableName,
1284
- columns
1285
- },
1286
- relation,
1287
- reverseRelation
1275
+ text,
1276
+ confidence
1288
1277
  };
1289
1278
  }
1290
- function parseJsonSchema(schema) {
1291
- const tables = [];
1292
- const relations = [];
1293
- const reverseRelations = [];
1294
- const warnings = [];
1295
- const mainTable = parseObjectToTable(schema, warnings);
1296
- tables.push(mainTable);
1297
- for (const [propName, prop] of Object.entries(schema.properties)) if (prop.type === "object" && prop.nested?.enabled) {
1298
- const nested = parseNestedObject(propName, prop, mainTable.name, warnings);
1299
- tables.push(nested.table);
1300
- relations.push(nested.relation);
1301
- reverseRelations.push(nested.reverseRelation);
1302
- } else if (prop.type === "array" && prop.items?.nested?.enabled && prop.items?.type === "object" && prop.items.properties) {
1303
- const nested = parseNestedObject(propName, prop.items, mainTable.name, warnings);
1304
- tables.push(nested.table);
1305
- relations.push(nested.relation);
1306
- reverseRelations.push(nested.reverseRelation);
1279
+ function normalizeOcrText(text) {
1280
+ return text.replace(/\s+/g, "").trim().toUpperCase();
1281
+ }
1282
+ async function checkImageOcrAvailability(imagePath, runtime = defaultRuntime) {
1283
+ if (!isLocalOcrPlatform(runtime.platform)) return {
1284
+ platformSupported: false,
1285
+ dependencyLoaded: false,
1286
+ ocrOk: null,
1287
+ imagePath,
1288
+ error: t("errors.ocr.platformUnsupported", { platform: runtime.platform })
1289
+ };
1290
+ let localOcr;
1291
+ try {
1292
+ localOcr = await runtime.loadLocalOcr();
1293
+ } catch (error) {
1294
+ return {
1295
+ platformSupported: true,
1296
+ dependencyLoaded: false,
1297
+ ocrOk: null,
1298
+ imagePath,
1299
+ error: error instanceof Error ? error.message : String(error)
1300
+ };
1307
1301
  }
1308
- return {
1309
- tables,
1310
- relations,
1311
- reverseRelations,
1312
- warnings
1302
+ if (!imagePath) return {
1303
+ platformSupported: true,
1304
+ dependencyLoaded: true,
1305
+ ocrOk: null,
1306
+ error: "No OCR self-check image was found."
1313
1307
  };
1308
+ try {
1309
+ const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, ["en-US"]);
1310
+ const recognizedText = result.text.trim();
1311
+ const ocrOk = normalizeOcrText(recognizedText).includes(SELF_CHECK_EXPECTED_TEXT);
1312
+ return {
1313
+ platformSupported: true,
1314
+ dependencyLoaded: true,
1315
+ ocrOk,
1316
+ imagePath,
1317
+ recognizedText,
1318
+ confidence: result.confidence,
1319
+ error: ocrOk ? void 0 : `Expected OCR text "${SELF_CHECK_EXPECTED_TEXT}" was not recognized.`
1320
+ };
1321
+ } catch (error) {
1322
+ return {
1323
+ platformSupported: true,
1324
+ dependencyLoaded: true,
1325
+ ocrOk: false,
1326
+ imagePath,
1327
+ error: error instanceof Error ? error.message : String(error)
1328
+ };
1329
+ }
1314
1330
  }
1315
1331
 
1316
1332
  //#endregion
1317
- //#region src/core/schema-sqlite/schemas.ts
1318
- const DrizzleModeSchema = z.enum([
1319
- "json",
1320
- "timestamp",
1321
- "timestamp_ms",
1322
- "boolean",
1323
- "bigint"
1324
- ]);
1325
- const DrizzleExtensionSchema = z.object({
1326
- mode: DrizzleModeSchema.optional(),
1327
- customType: z.string().optional()
1328
- }).optional();
1329
- const NestedConfigSchema = z.object({
1330
- enabled: z.literal(true),
1331
- relation: z.enum(["has-one", "has-many"])
1332
- });
1333
- const ForeignKeyRefSchema = z.object({
1334
- table: z.string().min(1),
1335
- column: z.string().min(1)
1336
- });
1337
- const JsonSchemaPropertySchema = z.lazy(() => z.object({
1338
- description: z.string().optional(),
1339
- type: z.enum([
1340
- "string",
1341
- "integer",
1342
- "number",
1343
- "boolean",
1344
- "object",
1345
- "array",
1346
- "null"
1347
- ]),
1348
- format: z.string().optional(),
1349
- primary: z.boolean().optional(),
1350
- autoIncrement: z.boolean().optional(),
1351
- unique: z.boolean().optional(),
1352
- default: z.unknown().optional(),
1353
- maxLength: z.number().int().positive().optional(),
1354
- minLength: z.number().int().nonnegative().optional(),
1355
- minimum: z.number().optional(),
1356
- maximum: z.number().optional(),
1357
- drizzle: DrizzleExtensionSchema,
1358
- nested: NestedConfigSchema.optional(),
1359
- foreignKey: ForeignKeyRefSchema.optional(),
1360
- properties: z.record(z.string(), JsonSchemaPropertySchema).optional(),
1361
- items: JsonSchemaPropertySchema.optional(),
1362
- required: z.array(z.string()).optional()
1363
- }));
1364
- const TableConfigSchema = z.object({
1365
- name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "Table name must be snake_case (lowercase letters, digits, underscores)"),
1366
- timestamps: z.boolean().optional(),
1367
- softDelete: z.boolean().optional()
1368
- });
1369
- const ExamplePairSchema = z.object({
1370
- text: z.string().min(1),
1371
- output: z.record(z.string(), z.unknown())
1372
- });
1373
- const JsonSchemaDefinitionSchema = z.object({
1374
- $schema: z.string().optional(),
1375
- title: z.string().min(1),
1376
- description: z.string().optional(),
1377
- type: z.literal("object"),
1378
- table: TableConfigSchema,
1379
- properties: z.record(z.string(), JsonSchemaPropertySchema),
1380
- required: z.array(z.string()).optional(),
1381
- examples: z.array(ExamplePairSchema).optional()
1382
- }).refine((schema) => Object.keys(schema.properties).length >= 1, {
1383
- message: "At least one property is required",
1384
- path: ["properties"]
1385
- }).refine((schema) => !schema.required || schema.required.every((r) => r in schema.properties), {
1386
- message: "All required fields must be defined in properties",
1387
- path: ["required"]
1388
- }).refine((schema) => {
1389
- return !Object.values(schema.properties).some((p) => p.primary) || Object.values(schema.properties).filter((p) => p.primary).length === 1;
1390
- }, {
1391
- message: "Only one primary key is allowed per table",
1392
- path: ["properties"]
1393
- });
1394
-
1395
- //#endregion
1396
- //#region src/core/schema-sqlite/migrator.ts
1333
+ //#region src/infrastructure/schema/migration-config.ts
1397
1334
  function createMigrationConfig(cwd) {
1398
1335
  return {
1399
1336
  schemaPath: `${cwd}/.aiex/schema`,
@@ -1416,7 +1353,7 @@ function generateDrizzleConfig() {
1416
1353
  }
1417
1354
 
1418
1355
  //#endregion
1419
- //#region src/core/doctor-collector.ts
1356
+ //#region src/application/doctor/collect-diagnostics.ts
1420
1357
  const V1_SUFFIX_RE = /\/v1\/?$/;
1421
1358
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
1422
1359
  async function checkConnection(baseURL) {
@@ -1593,4 +1530,67 @@ async function collectDoctorDiagnostics(options = {}) {
1593
1530
  }
1594
1531
 
1595
1532
  //#endregion
1596
- export { description as C, buildDoctorDiagnostics as D, version as E, doctorDiagnosticsTableRows as O, seedConfig as S, package_default as T, DEFAULT_PROMPT_CONFIG as _, parseJsonSchema as a, AIConfigSchema as b, recognizeImageText as c, t as d, getDefaultAIConfig as f, DEFAULT_MINERU_CONFIG as g, DEFAULT_MINERU_API_CONFIG as h, JsonSchemaDefinitionSchema as i, formatDoctorDiagnosticsJson as k, shouldUseImageOcrFallback as l, writeAIConfig as m, createMigrationConfig as n, toSnakeCase as o, readAIConfig as p, generateDrizzleConfig as r, generateDrizzleSchema as s, collectDoctorDiagnostics as t, initI18n as u, PLACEHOLDER_SCHEMA as v, name as w, createConfig as x, PLACEHOLDER_TEXT as y };
1533
+ //#region src/infrastructure/schema/generate-drizzle-schema.ts
1534
+ function generateColumnDefinition(column) {
1535
+ if (column.isPrimary && column.isAutoIncrement) return ` ${column.name}: integer().primaryKey({ autoIncrement: true })`;
1536
+ let def = ` ${column.name}: ${column.drizzleType}`;
1537
+ if (column.isPrimary) def += ".primaryKey()";
1538
+ if (!column.isNullable && !column.isPrimary) def += ".notNull()";
1539
+ if (column.isUnique && !column.isPrimary) def += ".unique()";
1540
+ if (column.defaultValue !== void 0) def += `.default(${column.defaultValue})`;
1541
+ if (column.isForeignKey && column.foreignKeyRef) def += `.references(() => ${column.foreignKeyRef.table}.${column.foreignKeyRef.column})`;
1542
+ return def;
1543
+ }
1544
+ function generateTableDefinition(table) {
1545
+ const columns = table.columns.map(generateColumnDefinition);
1546
+ return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n})`;
1547
+ }
1548
+ function generateRelationDefinitions(relations, reverseRelations) {
1549
+ if (relations.length === 0 && reverseRelations.length === 0) return "";
1550
+ const definitions = [];
1551
+ const childByTable = /* @__PURE__ */ new Map();
1552
+ for (const rel of relations) {
1553
+ const list = childByTable.get(rel.fromTable) ?? [];
1554
+ list.push(rel);
1555
+ childByTable.set(rel.fromTable, list);
1556
+ }
1557
+ const parentByTable = /* @__PURE__ */ new Map();
1558
+ for (const rel of reverseRelations) {
1559
+ const list = parentByTable.get(rel.fromTable) ?? [];
1560
+ list.push(rel);
1561
+ parentByTable.set(rel.fromTable, list);
1562
+ }
1563
+ const allTableNames = new Set([...childByTable.keys(), ...parentByTable.keys()]);
1564
+ for (const tableName of allTableNames) {
1565
+ const childRels = childByTable.get(tableName) ?? [];
1566
+ const parentRels = parentByTable.get(tableName) ?? [];
1567
+ const needsOne = childRels.length > 0 || parentRels.some((r) => r.type === "has-one");
1568
+ const needsMany = parentRels.some((r) => r.type === "has-many");
1569
+ const imports = [];
1570
+ if (needsOne) imports.push("one");
1571
+ if (needsMany) imports.push("many");
1572
+ const importStr = imports.join(", ");
1573
+ const relDefs = [];
1574
+ for (const rel of childRels) relDefs.push(` ${rel.name}: one(${rel.toTable}, {\n fields: [${rel.fromTable}.${rel.fromColumn}],\n references: [${rel.toTable}.${rel.toColumn}],\n })`);
1575
+ for (const rel of parentRels) if (rel.type === "has-many") relDefs.push(` ${rel.name}: many(${rel.toTable})`);
1576
+ else relDefs.push(` ${rel.name}: one(${rel.toTable})`);
1577
+ definitions.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${importStr} }) => ({\n${relDefs.join(",\n")}\n}))`);
1578
+ }
1579
+ return definitions.join("\n\n");
1580
+ }
1581
+ function generateDrizzleSchema(result) {
1582
+ const imports = `import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'`;
1583
+ const tableDefs = result.tables.map(generateTableDefinition).join("\n\n");
1584
+ const relationDefs = generateRelationDefinitions(result.relations, result.reverseRelations);
1585
+ const parts = [
1586
+ imports,
1587
+ "",
1588
+ tableDefs
1589
+ ];
1590
+ if (relationDefs) parts.push("", relationDefs);
1591
+ parts.push("");
1592
+ return parts.join("\n");
1593
+ }
1594
+
1595
+ //#endregion
1596
+ export { PLACEHOLDER_TEXT as C, name as D, description as E, package_default as O, PLACEHOLDER_SCHEMA as S, seedConfig as T, doctorDiagnosticsTableRows as _, recognizeImageText as a, DEFAULT_MINERU_CONFIG as b, t as c, writeAIConfig as d, AIConfigSchema as f, buildDoctorDiagnostics as g, toSnakeCase as h, generateDrizzleConfig as i, version as k, getDefaultAIConfig as l, parseJsonSchema as m, collectDoctorDiagnostics as n, shouldUseImageOcrFallback as o, JsonSchemaDefinitionSchema as p, createMigrationConfig as r, initI18n as s, generateDrizzleSchema as t, readAIConfig as u, formatDoctorDiagnosticsJson as v, createConfig as w, DEFAULT_PROMPT_CONFIG as x, DEFAULT_MINERU_API_CONFIG as y };