aiex-cli 0.0.6 → 0.0.7-beta.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.
@@ -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.2";
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,554 @@ 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 mapColumnType(property) {
300
+ switch (property.type) {
301
+ case "string": {
302
+ const format = property.format;
303
+ if (format === "date-time" || property.drizzle?.mode === "timestamp") return {
304
+ class: "integer",
305
+ mode: "timestamp"
306
+ };
307
+ if (format === "json" || property.drizzle?.mode === "json") return {
308
+ class: "text",
309
+ mode: "json"
310
+ };
311
+ return { class: "text" };
312
+ }
313
+ case "integer": {
314
+ const mode = property.drizzle?.mode;
315
+ if (mode === "boolean" || mode === "timestamp" || mode === "timestamp_ms" || mode === "bigint") return {
316
+ class: "integer",
317
+ mode
318
+ };
319
+ return { class: "integer" };
320
+ }
321
+ case "number": return { class: "real" };
322
+ case "boolean": return {
323
+ class: "integer",
324
+ mode: "boolean"
325
+ };
326
+ case "object":
327
+ case "array": return {
328
+ class: "text",
329
+ mode: "json"
330
+ };
331
+ case "null":
332
+ default: return { class: "text" };
333
+ }
334
+ }
335
+ function mapPropertyToColumn(name$1, property, isRequired) {
336
+ return {
337
+ name: toSnakeCase(name$1),
338
+ columnType: mapColumnType(property),
339
+ isPrimary: property.primary ?? false,
340
+ isAutoIncrement: property.autoIncrement ?? false,
341
+ isNullable: !isRequired && !property.primary,
342
+ isUnique: property.unique ?? false,
343
+ default: property.default,
344
+ isForeignKey: property.foreignKey !== void 0,
345
+ foreignKeyRef: property.foreignKey ?? void 0
346
+ };
347
+ }
348
+ function getColumnChecks(prop, colName) {
349
+ const checks = [];
350
+ if (prop.type === "string") {
351
+ if (prop.minLength !== void 0 && prop.minLength > 0) checks.push({
352
+ name: `${colName}_min_length`,
353
+ column: colName,
354
+ kind: "min_length",
355
+ value: prop.minLength
356
+ });
357
+ if (prop.maxLength !== void 0) checks.push({
358
+ name: `${colName}_max_length`,
359
+ column: colName,
360
+ kind: "max_length",
361
+ value: prop.maxLength
362
+ });
363
+ }
364
+ if (prop.type === "integer" || prop.type === "number") {
365
+ if (prop.minimum !== void 0) checks.push({
366
+ name: `${colName}_min`,
367
+ column: colName,
368
+ kind: "min_value",
369
+ value: prop.minimum
370
+ });
371
+ if (prop.maximum !== void 0) checks.push({
372
+ name: `${colName}_max`,
373
+ column: colName,
374
+ kind: "max_value",
375
+ value: prop.maximum
376
+ });
377
+ }
378
+ return checks;
379
+ }
380
+ function parseObjectToTable(schema, _warnings) {
381
+ const tableName = schema.table.name;
382
+ const columns = [];
383
+ const checks = [];
384
+ const requiredFields = new Set(schema.required ?? []);
385
+ const autoColumns = /* @__PURE__ */ new Set();
386
+ if (schema.table.timestamps) {
387
+ autoColumns.add("created_at");
388
+ autoColumns.add("updated_at");
389
+ }
390
+ if (schema.table.softDelete) autoColumns.add("deleted_at");
391
+ for (const [propName, prop] of Object.entries(schema.properties)) {
392
+ if (prop.nested?.enabled && (prop.type === "object" || prop.type === "array")) continue;
393
+ if (prop.type === "array" && prop.items?.nested?.enabled) continue;
394
+ const snakeName = toSnakeCase(propName);
395
+ if (autoColumns.has(snakeName)) continue;
396
+ const column = mapPropertyToColumn(propName, prop, requiredFields.has(propName));
397
+ columns.push(column);
398
+ checks.push(...getColumnChecks(prop, column.name));
399
+ }
400
+ if (schema.table.timestamps) {
401
+ const tsCol = {
402
+ name: "created_at",
403
+ columnType: {
404
+ class: "integer",
405
+ mode: "timestamp"
406
+ },
407
+ isPrimary: false,
408
+ isAutoIncrement: false,
409
+ isNullable: false,
410
+ isUnique: false
411
+ };
412
+ columns.push(tsCol);
413
+ columns.push({
414
+ ...tsCol,
415
+ name: "updated_at"
416
+ });
417
+ }
418
+ if (schema.table.softDelete) columns.push({
419
+ name: "deleted_at",
420
+ columnType: {
421
+ class: "integer",
422
+ mode: "timestamp"
423
+ },
424
+ isPrimary: false,
425
+ isAutoIncrement: false,
426
+ isNullable: true,
427
+ isUnique: false
428
+ });
429
+ return checks.length > 0 ? {
430
+ name: tableName,
431
+ columns,
432
+ checks
433
+ } : {
434
+ name: tableName,
435
+ columns
436
+ };
437
+ }
438
+ function parseNestedObject(propName, property, parentTableName, warnings) {
439
+ const nestedTableName = `${parentTableName}_${toSnakeCase(propName)}`;
440
+ const columns = [];
441
+ const checks = [];
442
+ const relationType = property.nested?.relation === "has-many" ? "has-many" : "has-one";
443
+ columns.push({
444
+ name: "id",
445
+ columnType: { class: "integer" },
446
+ isPrimary: true,
447
+ isAutoIncrement: true,
448
+ isNullable: false,
449
+ isUnique: false
450
+ });
451
+ columns.push({
452
+ name: `${parentTableName}_id`,
453
+ columnType: { class: "integer" },
454
+ isPrimary: false,
455
+ isAutoIncrement: false,
456
+ isNullable: false,
457
+ isUnique: false,
458
+ isForeignKey: true,
459
+ foreignKeyRef: {
460
+ table: parentTableName,
461
+ column: "id"
462
+ }
463
+ });
464
+ if (property.type === "object" && property.properties) for (const [childName, childProp] of Object.entries(property.properties)) {
465
+ if (childProp.nested?.enabled) {
466
+ 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.`);
467
+ continue;
468
+ }
469
+ const column = mapPropertyToColumn(childName, childProp, false);
470
+ columns.push(column);
471
+ checks.push(...getColumnChecks(childProp, column.name));
472
+ }
473
+ const relation = {
474
+ fromTable: nestedTableName,
475
+ fromColumn: `${parentTableName}_id`,
476
+ toTable: parentTableName,
477
+ toColumn: "id",
478
+ name: parentTableName
479
+ };
480
+ const reverseRelation = {
481
+ type: relationType,
482
+ fromTable: parentTableName,
483
+ toTable: nestedTableName,
484
+ name: toSnakeCase(propName)
485
+ };
486
+ return {
487
+ table: checks.length > 0 ? {
488
+ name: nestedTableName,
489
+ columns,
490
+ checks
491
+ } : {
492
+ name: nestedTableName,
493
+ columns
494
+ },
495
+ relation,
496
+ reverseRelation
497
+ };
498
+ }
499
+ function parseJsonSchema(schema) {
500
+ const tables = [];
501
+ const relations = [];
502
+ const reverseRelations = [];
503
+ const warnings = [];
504
+ const mainTable = parseObjectToTable(schema, warnings);
505
+ tables.push(mainTable);
506
+ for (const [propName, prop] of Object.entries(schema.properties)) if (prop.type === "object" && prop.nested?.enabled) {
507
+ const nested = parseNestedObject(propName, prop, mainTable.name, warnings);
508
+ tables.push(nested.table);
509
+ relations.push(nested.relation);
510
+ reverseRelations.push(nested.reverseRelation);
511
+ } else if (prop.type === "array" && prop.items?.nested?.enabled && prop.items?.type === "object" && prop.items.properties) {
512
+ const nested = parseNestedObject(propName, prop.items, mainTable.name, warnings);
513
+ tables.push(nested.table);
514
+ relations.push(nested.relation);
515
+ reverseRelations.push(nested.reverseRelation);
516
+ }
517
+ return {
518
+ tables,
519
+ relations,
520
+ reverseRelations,
521
+ warnings
522
+ };
523
+ }
524
+
525
+ //#endregion
526
+ //#region src/domain/schema/schemas.ts
527
+ const DrizzleModeSchema = z.enum([
528
+ "json",
529
+ "timestamp",
530
+ "timestamp_ms",
531
+ "boolean",
532
+ "bigint"
533
+ ]);
534
+ const DrizzleExtensionSchema = z.object({
535
+ mode: DrizzleModeSchema.optional(),
536
+ customType: z.string().optional()
537
+ }).optional();
538
+ const NestedConfigSchema = z.object({
539
+ enabled: z.literal(true),
540
+ relation: z.enum(["has-one", "has-many"])
541
+ });
542
+ const ForeignKeyRefSchema = z.object({
543
+ table: z.string().min(1),
544
+ column: z.string().min(1)
545
+ });
546
+ const JsonSchemaPropertySchema = z.lazy(() => z.object({
547
+ description: z.string().optional(),
548
+ type: z.enum([
549
+ "string",
550
+ "integer",
551
+ "number",
552
+ "boolean",
553
+ "object",
554
+ "array",
555
+ "null"
556
+ ]),
557
+ format: z.string().optional(),
558
+ pattern: z.string().optional(),
559
+ enum: z.array(z.union([z.string(), z.number()])).optional(),
560
+ primary: z.boolean().optional(),
561
+ autoIncrement: z.boolean().optional(),
562
+ unique: z.boolean().optional(),
563
+ default: z.unknown().optional(),
564
+ maxLength: z.number().int().positive().optional(),
565
+ minLength: z.number().int().nonnegative().optional(),
566
+ minimum: z.number().optional(),
567
+ maximum: z.number().optional(),
568
+ examples: z.array(z.unknown()).optional(),
569
+ xPrompt: z.string().optional(),
570
+ drizzle: DrizzleExtensionSchema,
571
+ nested: NestedConfigSchema.optional(),
572
+ foreignKey: ForeignKeyRefSchema.optional(),
573
+ properties: z.record(z.string(), JsonSchemaPropertySchema).optional(),
574
+ items: JsonSchemaPropertySchema.optional(),
575
+ required: z.array(z.string()).optional()
576
+ }));
577
+ const TableConfigSchema = z.object({
578
+ name: z.string().min(1).regex(/^[a-z][a-z0-9_]*$/, "Table name must be snake_case (lowercase letters, digits, underscores)"),
579
+ timestamps: z.boolean().optional(),
580
+ softDelete: z.boolean().optional()
581
+ });
582
+ const ExamplePairSchema = z.object({
583
+ text: z.string().min(1),
584
+ output: z.record(z.string(), z.unknown())
585
+ });
586
+ const JsonSchemaDefinitionSchema = z.object({
587
+ $schema: z.string().optional(),
588
+ title: z.string().min(1),
589
+ description: z.string().optional(),
590
+ type: z.literal("object"),
591
+ table: TableConfigSchema,
592
+ properties: z.record(z.string(), JsonSchemaPropertySchema),
593
+ required: z.array(z.string()).optional(),
594
+ examples: z.array(ExamplePairSchema).optional()
595
+ }).refine((schema) => Object.keys(schema.properties).length >= 1, {
596
+ message: "At least one property is required",
597
+ path: ["properties"]
598
+ }).refine((schema) => !schema.required || schema.required.every((r) => r in schema.properties), {
599
+ message: "All required fields must be defined in properties",
600
+ path: ["required"]
601
+ }).refine((schema) => {
602
+ return !Object.values(schema.properties).some((p) => p.primary) || Object.values(schema.properties).filter((p) => p.primary).length === 1;
603
+ }, {
604
+ message: "Only one primary key is allowed per table",
605
+ path: ["properties"]
606
+ });
607
+
608
+ //#endregion
609
+ //#region src/domain/ai/schemas.ts
610
+ const ModelCapabilitiesSchema = z.object({
611
+ vision: z.boolean(),
612
+ structuredOutput: z.boolean(),
613
+ maxTokens: z.number().int().positive().optional(),
614
+ maxOutputTokens: z.number().int().positive().optional()
615
+ });
616
+ const AIModelConfigSchema = z.object({
617
+ name: z.string().min(1),
618
+ capabilities: ModelCapabilitiesSchema
619
+ });
620
+ const AIProviderConfigSchema = z.object({
621
+ baseURL: z.string().min(1),
622
+ apiKey: z.string(),
623
+ models: z.array(AIModelConfigSchema).min(1),
624
+ timeout: z.number().int().positive().default(300).optional()
625
+ });
626
+ const PromptConfigSchema = z.object({
627
+ systemTemplate: z.string().min(1),
628
+ userTemplate: z.string().min(1)
629
+ });
630
+ const ExtractionConfigSchema = z.object({ outputDir: z.string().min(1) });
631
+ const ImageOcrFallbackSchema = z.preprocess((value) => [
632
+ "auto",
633
+ "off",
634
+ "local"
635
+ ].includes(String(value)) ? "localAuto" : value, z.literal("localAuto").default("localAuto").optional());
636
+ const ImageOcrConfigSchema = z.object({
637
+ ocrFallback: ImageOcrFallbackSchema,
638
+ ocrLanguages: z.string().min(1).optional(),
639
+ ocrMinConfidence: z.number().min(0).max(1).optional()
640
+ });
641
+ const ExternalPdfConverterConfigSchema = z.object({
642
+ command: z.string().min(1),
643
+ args: z.array(z.string()).min(1).refine((args) => args.some((arg) => arg.includes("{input}")), { message: "args must contain {input} template variable" }),
644
+ outputFile: z.string().min(1).optional(),
645
+ timeout: z.number().int().positive().default(600).optional(),
646
+ fallbackToUnpdf: z.boolean().optional()
647
+ });
648
+ const MineruApiPdfConverterConfigSchema = z.object({
649
+ token: z.string(),
650
+ baseURL: z.string().url().optional(),
651
+ modelVersion: z.string().optional(),
652
+ isOcr: z.boolean().optional(),
653
+ enableFormula: z.boolean().optional(),
654
+ enableTable: z.boolean().optional()
655
+ });
656
+ const PdfConfigSchema = z.preprocess((value) => {
657
+ if (!value || typeof value !== "object") return value;
658
+ const config = { ...value };
659
+ if (config.converter === "markitdown" && config.markitdown) {
660
+ config.converter = "external";
661
+ config.external = config.markitdown;
662
+ } else if (config.converter === "marker" && config.marker) {
663
+ config.converter = "external";
664
+ config.external = config.marker;
665
+ } else if (config.converter === "markitdown" || config.converter === "marker") config.converter = "unpdf";
666
+ delete config.markitdown;
667
+ delete config.marker;
668
+ return config;
669
+ }, z.object({
670
+ converter: z.enum([
671
+ "unpdf",
672
+ "mineru",
673
+ "mineru_api",
674
+ "external"
675
+ ]),
676
+ mineru: ExternalPdfConverterConfigSchema.optional(),
677
+ mineruApi: MineruApiPdfConverterConfigSchema.optional(),
678
+ external: ExternalPdfConverterConfigSchema.optional()
679
+ }));
680
+ const LangfuseConfigSchema = z.object({
681
+ publicKey: z.string(),
682
+ secretKey: z.string(),
683
+ host: z.string().optional()
684
+ });
685
+ const NotionSchemaConfigSchema = z.object({
686
+ databaseId: z.string(),
687
+ titleProperty: z.string().optional(),
688
+ fieldMap: z.record(z.string()).optional()
689
+ });
690
+ const NotionConfigSchema = z.object({
691
+ enabled: z.boolean(),
692
+ token: z.string(),
693
+ schemas: z.record(NotionSchemaConfigSchema).default({})
694
+ });
695
+ const WebhookConfigSchema = z.object({
696
+ enabled: z.boolean(),
697
+ url: z.string(),
698
+ secret: z.string().optional()
699
+ });
700
+ const AIConfigSchema = z.object({
701
+ provider: AIProviderConfigSchema,
702
+ prompt: PromptConfigSchema,
703
+ extraction: ExtractionConfigSchema,
704
+ image: ImageOcrConfigSchema.optional(),
705
+ pdf: PdfConfigSchema.optional(),
706
+ langfuse: LangfuseConfigSchema.optional(),
707
+ notion: NotionConfigSchema.optional(),
708
+ webhook: WebhookConfigSchema.optional()
709
+ });
710
+
711
+ //#endregion
712
+ //#region src/infrastructure/ai/ai-config-store.ts
713
+ const CONFIG_FILE_NAME = "ai-config.json";
714
+ const GITIGNORE_FILE = ".gitignore";
715
+ async function readAIConfig(aiexDir) {
716
+ const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
717
+ try {
718
+ const parsed = await readFile(configPath);
719
+ return AIConfigSchema.parse(parsed);
720
+ } catch {
721
+ return null;
722
+ }
723
+ }
724
+ async function writeAIConfig(aiexDir, config) {
725
+ const configPath = path.join(aiexDir, CONFIG_FILE_NAME);
726
+ await fs.mkdir(aiexDir, { recursive: true });
727
+ await writeFile(configPath, config, {
728
+ spaces: 2,
729
+ EOL: "\n"
730
+ });
731
+ await addToGitignore(aiexDir, CONFIG_FILE_NAME);
732
+ }
733
+ function getDefaultAIConfig() {
734
+ return structuredClone(DEFAULT_AI_CONFIG);
735
+ }
736
+ async function addToGitignore(aiexDir, fileName) {
737
+ const projectRoot = path.dirname(aiexDir);
738
+ const gitignorePath = path.join(projectRoot, GITIGNORE_FILE);
739
+ try {
740
+ const content = await fs.readFile(gitignorePath, "utf-8");
741
+ if (content.split("\n").some((line) => line.trim() === fileName || line.includes(".aiex/"))) return;
742
+ const newContent = content.endsWith("\n") ? `${content}${fileName}\n` : `${content}\n${fileName}\n`;
743
+ await fs.writeFile(gitignorePath, newContent);
744
+ } catch {
745
+ await fs.writeFile(gitignorePath, `${fileName}\n`);
746
+ }
747
+ }
748
+
749
+ //#endregion
750
+ //#region src/locales/en.ts
751
+ const en = {
752
+ common: {
753
+ done: "Done!",
754
+ failed: "Failed!",
755
+ cancelled: "Cancelled",
756
+ save: "Save",
757
+ cancel: "Cancel",
758
+ delete: "Delete",
759
+ close: "Close",
760
+ loading: "Loading...",
761
+ unknownError: "Unknown error"
762
+ },
763
+ cli: { description: "JSON Schema → SQLite with AI-powered data extraction" },
764
+ command: {
765
+ web: {
766
+ description: "Start visual JSON Schema editor",
767
+ args: { port: "Port to listen on" },
768
+ starting: "Starting web server...",
455
769
  serverRunning: "Server running at {{url}}",
456
770
  schemaDir: "Schema directory: {{path}}",
457
771
  pressCtrlC: "Press Ctrl+C to stop",
@@ -959,7 +1273,7 @@ function t(key, options) {
959
1273
  }
960
1274
 
961
1275
  //#endregion
962
- //#region src/core/image-ocr/index.ts
1276
+ //#region src/infrastructure/ocr/system-ocr.ts
963
1277
  const DEFAULT_OCR_LANGUAGES = "en-US, zh-Hans";
964
1278
  const SELF_CHECK_EXPECTED_TEXT = "AIEX";
965
1279
  const defaultRuntime = {
@@ -983,417 +1297,87 @@ function shouldUseImageOcrFallback(aiConfig, modelOverride, runtime = defaultRun
983
1297
  function isLocalOcrPlatform(platform) {
984
1298
  return platform === "darwin" || platform === "win32";
985
1299
  }
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
- }
1064
-
1065
- //#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;
1076
- }
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})`;
1080
- }
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");
1113
- }
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");
1126
- }
1127
-
1128
- //#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()";
1168
- }
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
- };
1180
- }
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
- };
1232
- }
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);
1300
+ function parseOcrLanguages(languages) {
1301
+ return (languages ?? DEFAULT_OCR_LANGUAGES).split(",").map((language) => language.trim()).filter(Boolean);
1302
+ }
1303
+ async function recognizeImageText(imagePath, config, runtime = defaultRuntime) {
1304
+ if (!isLocalOcrPlatform(runtime.platform)) throw new Error(t("errors.ocr.platformUnsupported", { platform: runtime.platform }));
1305
+ let localOcr;
1306
+ try {
1307
+ localOcr = await runtime.loadLocalOcr();
1308
+ } catch (error) {
1309
+ const message = error instanceof Error ? error.message : String(error);
1310
+ throw new Error(t("errors.ocr.unavailable", { error: message }));
1267
1311
  }
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
- };
1312
+ const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, parseOcrLanguages(config?.ocrLanguages));
1313
+ const text = result.text.trim();
1314
+ if (!text) throw new Error(t("errors.ocr.noText"));
1315
+ const confidence = result.confidence;
1316
+ const minConfidence = config?.ocrMinConfidence ?? 0;
1317
+ if (confidence < minConfidence) throw new Error(t("errors.ocr.lowConfidence", {
1318
+ confidence: (confidence * 100).toFixed(1),
1319
+ min: (minConfidence * 100).toFixed(1)
1320
+ }));
1281
1321
  return {
1282
- table: {
1283
- name: nestedTableName,
1284
- columns
1285
- },
1286
- relation,
1287
- reverseRelation
1322
+ text,
1323
+ confidence
1288
1324
  };
1289
1325
  }
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);
1326
+ function normalizeOcrText(text) {
1327
+ return text.replace(/\s+/g, "").trim().toUpperCase();
1328
+ }
1329
+ async function checkImageOcrAvailability(imagePath, runtime = defaultRuntime) {
1330
+ if (!isLocalOcrPlatform(runtime.platform)) return {
1331
+ platformSupported: false,
1332
+ dependencyLoaded: false,
1333
+ ocrOk: null,
1334
+ imagePath,
1335
+ error: t("errors.ocr.platformUnsupported", { platform: runtime.platform })
1336
+ };
1337
+ let localOcr;
1338
+ try {
1339
+ localOcr = await runtime.loadLocalOcr();
1340
+ } catch (error) {
1341
+ return {
1342
+ platformSupported: true,
1343
+ dependencyLoaded: false,
1344
+ ocrOk: null,
1345
+ imagePath,
1346
+ error: error instanceof Error ? error.message : String(error)
1347
+ };
1307
1348
  }
1308
- return {
1309
- tables,
1310
- relations,
1311
- reverseRelations,
1312
- warnings
1349
+ if (!imagePath) return {
1350
+ platformSupported: true,
1351
+ dependencyLoaded: true,
1352
+ ocrOk: null,
1353
+ error: "No OCR self-check image was found."
1313
1354
  };
1355
+ try {
1356
+ const result = await localOcr.recognize(imagePath, localOcr.OcrAccuracy.Accurate, ["en-US"]);
1357
+ const recognizedText = result.text.trim();
1358
+ const ocrOk = normalizeOcrText(recognizedText).includes(SELF_CHECK_EXPECTED_TEXT);
1359
+ return {
1360
+ platformSupported: true,
1361
+ dependencyLoaded: true,
1362
+ ocrOk,
1363
+ imagePath,
1364
+ recognizedText,
1365
+ confidence: result.confidence,
1366
+ error: ocrOk ? void 0 : `Expected OCR text "${SELF_CHECK_EXPECTED_TEXT}" was not recognized.`
1367
+ };
1368
+ } catch (error) {
1369
+ return {
1370
+ platformSupported: true,
1371
+ dependencyLoaded: true,
1372
+ ocrOk: false,
1373
+ imagePath,
1374
+ error: error instanceof Error ? error.message : String(error)
1375
+ };
1376
+ }
1314
1377
  }
1315
1378
 
1316
1379
  //#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
1380
+ //#region src/infrastructure/schema/migration-config.ts
1397
1381
  function createMigrationConfig(cwd) {
1398
1382
  return {
1399
1383
  schemaPath: `${cwd}/.aiex/schema`,
@@ -1416,7 +1400,7 @@ function generateDrizzleConfig() {
1416
1400
  }
1417
1401
 
1418
1402
  //#endregion
1419
- //#region src/core/doctor-collector.ts
1403
+ //#region src/application/doctor/collect-diagnostics.ts
1420
1404
  const V1_SUFFIX_RE = /\/v1\/?$/;
1421
1405
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
1422
1406
  async function checkConnection(baseURL) {
@@ -1593,4 +1577,98 @@ async function collectDoctorDiagnostics(options = {}) {
1593
1577
  }
1594
1578
 
1595
1579
  //#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 };
1580
+ //#region src/infrastructure/schema/generate-drizzle-schema.ts
1581
+ function renderColumnType(ct) {
1582
+ switch (ct.class) {
1583
+ case "text": return ct.mode === "json" ? `text({ mode: 'json' })` : "text()";
1584
+ case "integer": return ct.mode ? `integer({ mode: '${ct.mode}' })` : "integer()";
1585
+ case "real": return "real()";
1586
+ }
1587
+ }
1588
+ function renderDefaultValue(value) {
1589
+ return JSON.stringify(value);
1590
+ }
1591
+ function generateColumnDefinition(column) {
1592
+ if (column.isPrimary && column.isAutoIncrement) return ` ${column.name}: integer().primaryKey({ autoIncrement: true })`;
1593
+ let def = ` ${column.name}: ${renderColumnType(column.columnType)}`;
1594
+ if (column.isPrimary) def += ".primaryKey()";
1595
+ if (!column.isNullable && !column.isPrimary) def += ".notNull()";
1596
+ if (column.isUnique && !column.isPrimary) def += ".unique()";
1597
+ if (column.default !== void 0) def += `.default(${renderDefaultValue(column.default)})`;
1598
+ if (column.isForeignKey && column.foreignKeyRef) def += `.references(() => ${column.foreignKeyRef.table}.${column.foreignKeyRef.column})`;
1599
+ return def;
1600
+ }
1601
+ function renderCheckToDrizzle(check, tableVar) {
1602
+ const colRef = `\${${tableVar}.${check.column}}`;
1603
+ let expr;
1604
+ switch (check.kind) {
1605
+ case "min_length":
1606
+ expr = `length(${colRef}) >= ${check.value}`;
1607
+ break;
1608
+ case "max_length":
1609
+ expr = `length(${colRef}) <= ${check.value}`;
1610
+ break;
1611
+ case "min_value":
1612
+ expr = `${colRef} >= ${check.value}`;
1613
+ break;
1614
+ case "max_value":
1615
+ expr = `${colRef} <= ${check.value}`;
1616
+ break;
1617
+ }
1618
+ return ` ${check.name}: check('${check.name}', sql\`${expr}\`)`;
1619
+ }
1620
+ function generateTableDefinition(table) {
1621
+ const columns = table.columns.map(generateColumnDefinition);
1622
+ if (!table.checks?.length) return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n})`;
1623
+ const checkLines = table.checks.map((c) => renderCheckToDrizzle(c, "table"));
1624
+ return `export const ${table.name} = sqliteTable('${table.name}', {\n${columns.join(",\n")}\n}, (table) => ({\n${checkLines.join(",\n")}\n}))`;
1625
+ }
1626
+ function generateRelationDefinitions(relations, reverseRelations) {
1627
+ if (relations.length === 0 && reverseRelations.length === 0) return "";
1628
+ const definitions = [];
1629
+ const childByTable = /* @__PURE__ */ new Map();
1630
+ for (const rel of relations) {
1631
+ const list = childByTable.get(rel.fromTable) ?? [];
1632
+ list.push(rel);
1633
+ childByTable.set(rel.fromTable, list);
1634
+ }
1635
+ const parentByTable = /* @__PURE__ */ new Map();
1636
+ for (const rel of reverseRelations) {
1637
+ const list = parentByTable.get(rel.fromTable) ?? [];
1638
+ list.push(rel);
1639
+ parentByTable.set(rel.fromTable, list);
1640
+ }
1641
+ const allTableNames = new Set([...childByTable.keys(), ...parentByTable.keys()]);
1642
+ for (const tableName of allTableNames) {
1643
+ const childRels = childByTable.get(tableName) ?? [];
1644
+ const parentRels = parentByTable.get(tableName) ?? [];
1645
+ const needsOne = childRels.length > 0 || parentRels.some((r) => r.type === "has-one");
1646
+ const needsMany = parentRels.some((r) => r.type === "has-many");
1647
+ const imports = [];
1648
+ if (needsOne) imports.push("one");
1649
+ if (needsMany) imports.push("many");
1650
+ const importStr = imports.join(", ");
1651
+ const relDefs = [];
1652
+ 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 })`);
1653
+ for (const rel of parentRels) if (rel.type === "has-many") relDefs.push(` ${rel.name}: many(${rel.toTable})`);
1654
+ else relDefs.push(` ${rel.name}: one(${rel.toTable})`);
1655
+ definitions.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${importStr} }) => ({\n${relDefs.join(",\n")}\n}))`);
1656
+ }
1657
+ return definitions.join("\n\n");
1658
+ }
1659
+ function generateDrizzleSchema(result) {
1660
+ const imports = `import { ${`sqliteTable, text, integer, real${result.tables.some((t$1) => t$1.checks?.length) ? ", check, sql" : ""}`} } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'`;
1661
+ const tableDefs = result.tables.map(generateTableDefinition).join("\n\n");
1662
+ const relationDefs = generateRelationDefinitions(result.relations, result.reverseRelations);
1663
+ const parts = [
1664
+ imports,
1665
+ "",
1666
+ tableDefs
1667
+ ];
1668
+ if (relationDefs) parts.push("", relationDefs);
1669
+ parts.push("");
1670
+ return parts.join("\n");
1671
+ }
1672
+
1673
+ //#endregion
1674
+ 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 };