11#!/usr/bin/env node
2- import { exec } from 'node:child_process' ;
32import { resolve } from 'node:path' ;
4- import { promisify } from 'node:util' ;
5- import type { Config } from '@doc-agent/core' ;
6- import { extractDocument } from '@doc-agent/extract' ;
7- import { storage } from '@doc-agent/storage' ;
83import chalk from 'chalk' ;
94import { Command } from 'commander' ;
10- import ora from 'ora' ;
5+ import { render } from 'ink' ;
6+ import React from 'react' ;
7+ import { ExtractApp } from './components/ExtractApp' ;
118
12- const execAsync = promisify ( exec ) ;
9+ // Resolve paths relative to where user ran the command
10+ // INIT_CWD is set by pnpm to original working directory
11+ const cwd = process . env . INIT_CWD || process . cwd ( ) ;
12+ function resolvePath ( filePath : string ) : string {
13+ return resolve ( cwd , filePath ) ;
14+ }
1315
1416const program = new Command ( ) ;
1517
@@ -19,72 +21,39 @@ program
1921 . description ( 'Document extraction and semantic search CLI' )
2022 . version ( '0.1.0' ) ;
2123
22- async function ensureOllamaModel ( model : string ) {
23- const spinner = ora ( `Checking for Ollama model: ${ model } ...` ) . start ( ) ;
24- try {
25- const response = await fetch ( 'http://localhost:11434/api/tags' ) ;
26- if ( ! response . ok ) {
27- throw new Error ( 'Ollama is not running. Please start Ollama first.' ) ;
28- }
29- const data = ( await response . json ( ) ) as { models : { name : string } [ ] } ;
30- const modelExists = data . models . some ( ( m ) => m . name . includes ( model ) ) ;
31-
32- if ( ! modelExists ) {
33- spinner . text = `Pulling Ollama model: ${ model } (this may take a while)...` ;
34- // Use exec to pull so we can potentially see output or just wait
35- // Using the API to pull would be better for progress, but for now CLI is robust
36- await execAsync ( `ollama pull ${ model } ` ) ;
37- spinner . succeed ( `Model ${ model } ready.` ) ;
38- } else {
39- spinner . succeed ( `Model ${ model } found.` ) ;
40- }
41- } catch ( error ) {
42- spinner . fail ( 'Failed to check/pull Ollama model.' ) ;
43- throw error ;
44- }
45- }
46-
4724program
4825 . command ( 'extract <file>' )
4926 . description ( 'Extract structured data from a document' )
5027 . option ( '-p, --provider <provider>' , 'AI provider (gemini|openai|ollama)' , 'ollama' )
51- . option (
52- '-m, --model <model>' ,
53- 'Model to use (default: llama3.2-vision for ollama)' ,
54- 'llama3.2-vision'
55- )
28+ . option ( '-m, --model <model>' , 'Model to use (ollama: llama3.2-vision, gemini: gemini-2.5-flash)' )
5629 . option ( '-d, --dry-run' , 'Print JSON only, do not save to database' , false )
5730 . action ( async ( file : string , options ) => {
58- try {
59- if ( options . provider === 'ollama' ) {
60- await ensureOllamaModel ( options . model ) ;
61- }
62-
63- const spinner = ora ( 'Extracting document data...' ) . start ( ) ;
64-
65- const config : Config = {
66- aiProvider : options . provider ,
67- geminiApiKey : process . env . GEMINI_API_KEY ,
68- openaiApiKey : process . env . OPENAI_API_KEY ,
69- ollamaModel : options . model ,
70- } ;
31+ const absolutePath = resolvePath ( file ) ;
7132
72- const result = await extractDocument ( file , config ) ;
33+ // Set default model based on provider if not specified
34+ const defaultModels : Record < string , string > = {
35+ ollama : 'llama3.2-vision' ,
36+ gemini : 'gemini-2.5-flash' ,
37+ openai : 'gpt-4o' ,
38+ } ;
39+ const model = options . model || defaultModels [ options . provider ] || 'llama3.2-vision' ;
7340
74- if ( options . dryRun ) {
75- spinner . succeed ( chalk . green ( 'Extraction complete (dry run)' ) ) ;
76- } else {
77- const absolutePath = resolve ( file ) ;
78- await storage . saveDocument ( result , absolutePath ) ;
79- spinner . succeed ( chalk . green ( `Saved: ${ result . filename } (ID: ${ result . id } )` ) ) ;
80- }
41+ const { waitUntilExit } = render (
42+ React . createElement ( ExtractApp , {
43+ file : absolutePath ,
44+ provider : options . provider ,
45+ model,
46+ dryRun : options . dryRun ,
47+ onComplete : ( ) => {
48+ // Normal exit
49+ } ,
50+ onError : ( ) => {
51+ process . exitCode = 1 ;
52+ } ,
53+ } )
54+ ) ;
8155
82- console . log ( JSON . stringify ( result , null , 2 ) ) ;
83- } catch ( error ) {
84- console . error ( chalk . red ( '\nExtraction failed:' ) ) ;
85- console . error ( ( error as Error ) . message ) ;
86- process . exit ( 1 ) ;
87- }
56+ await waitUntilExit ( ) ;
8857 } ) ;
8958
9059program
0 commit comments