11#!/usr/bin/env node
22
33import path from "node:path" ;
4+ import readline from "node:readline" ;
5+ import fs from "node:fs/promises" ;
46import { fileURLToPath } from "node:url" ;
57import chalk from "chalk" ;
6- import ora from "ora" ;
78import { loadConfig , saveConfig , resolveWorkspaceDir } from "./config/config.js" ;
89import { createOpenAIProvider } from "./llm/provider.js" ;
910import { Agent } from "./agent/agent.js" ;
1011import { initWorkspace } from "./workspace/init.js" ;
11- import { CliInput } from "./cli/input.js" ;
12- import readline from "node:readline" ;
1312
1413const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1514const TEMPLATE_DIR = path . resolve ( __dirname , "../templates" ) ;
1615
17- /** Simple terminal markdown renderer using chalk */
16+ /** Simple terminal markdown renderer */
1817function renderMarkdown ( text : string ) : string {
19- // 1. Extract fenced code blocks first (before inline code regex eats backticks)
18+ // Extract fenced code blocks first
2019 const codeBlocks : string [ ] = [ ] ;
2120 let processed = text . replace ( / ` ` ` ( \w * ) \n ( [ \s \S ] * ?) ` ` ` / g, ( _m , lang , code ) => {
2221 const label = lang ? chalk . dim ( ` [${ lang } ]` ) : "" ;
@@ -27,7 +26,6 @@ function renderMarkdown(text: string): string {
2726 return `__CODE_BLOCK_${ codeBlocks . length - 1 } __` ;
2827 } ) ;
2928
30- // 2. Apply inline formatting
3129 processed = processed
3230 . replace ( / ^ # # # ( .+ ) $ / gm, ( _m , s ) => chalk . green . bold ( ` ${ s } ` ) )
3331 . replace ( / ^ # # ( .+ ) $ / gm, ( _m , s ) => chalk . green . bold ( ` ${ s } ` ) )
@@ -40,11 +38,9 @@ function renderMarkdown(text: string): string {
4038 . replace ( / ^ > ( .+ ) $ / gm, ( _m , s ) => chalk . gray . italic ( ` │ ${ s } ` ) )
4139 . replace ( / ^ - - - $ / gm, chalk . dim ( "─" . repeat ( 40 ) ) ) ;
4240
43- // 3. Re-insert code blocks
4441 for ( let i = 0 ; i < codeBlocks . length ; i ++ ) {
4542 processed = processed . replace ( `__CODE_BLOCK_${ i } __` , codeBlocks [ i ] ) ;
4643 }
47-
4844 return processed ;
4945}
5046
@@ -53,10 +49,18 @@ async function main() {
5349 const workspaceArg = args . find ( ( a ) => a . startsWith ( "--workspace=" ) ) ?. split ( "=" ) [ 1 ] ;
5450 const workspaceDir = resolveWorkspaceDir ( workspaceArg ) ;
5551
52+ // Debug: catch and log any unhandled errors that might silently kill the process
53+ process . on ( "unhandledRejection" , ( err ) => {
54+ console . error ( chalk . red ( "\n[unhandledRejection]" ) , err ) ;
55+ } ) ;
56+ process . on ( "uncaughtException" , ( err ) => {
57+ console . error ( chalk . red ( "\n[uncaughtException]" ) , err ) ;
58+ } ) ;
59+
5660 console . log ( chalk . cyan . bold ( "\n🦐 ClawCore" ) + chalk . dim ( " — a core version of OpenClaw\n" ) ) ;
5761 console . log ( chalk . dim ( `Workspace: ${ workspaceDir } \n` ) ) ;
5862
59- // Initialize workspace (creates directories + seeds templates if first run)
63+ // Initialize workspace
6064 await initWorkspace ( workspaceDir , TEMPLATE_DIR ) ;
6165
6266 // Load config
@@ -69,7 +73,6 @@ async function main() {
6973 console . log ( chalk . dim ( " Option 1: export OPENAI_API_KEY=sk-..." ) ) ;
7074 console . log ( chalk . dim ( ` Option 2: edit ${ path . join ( workspaceDir , "config.json" ) } \n` ) ) ;
7175
72- // Try env var
7376 const envKey = process . env . OPENAI_API_KEY
7477 ?? process . env . CLAWCORE_API_KEY
7578 ?? process . env . LLM_API_KEY ;
@@ -78,7 +81,6 @@ async function main() {
7881 config = { ...config , llm : { ...config . llm , apiKey : envKey } } ;
7982 console . log ( chalk . green ( "✓ API key found from environment variable.\n" ) ) ;
8083 } else {
81- // Interactive setup (use standard readline for this)
8284 const rl = readline . createInterface ( { input : process . stdin , output : process . stdout } ) ;
8385 const ask = ( q : string ) => new Promise < string > ( ( r ) => rl . question ( q , r ) ) ;
8486
@@ -88,12 +90,8 @@ async function main() {
8890 process . exit ( 1 ) ;
8991 }
9092
91- const baseUrl = await ask (
92- chalk . cyan ( `Base URL (default: ${ config . llm . baseUrl } ): ` ) ,
93- ) ;
94- const model = await ask (
95- chalk . cyan ( `Model (default: ${ config . llm . model } ): ` ) ,
96- ) ;
93+ const baseUrl = await ask ( chalk . cyan ( `Base URL (default: ${ config . llm . baseUrl } ): ` ) ) ;
94+ const model = await ask ( chalk . cyan ( `Model (default: ${ config . llm . model } ): ` ) ) ;
9795
9896 config = {
9997 ...config ,
@@ -113,56 +111,47 @@ async function main() {
113111 // Create LLM provider
114112 const llm = createOpenAIProvider ( config . llm ) ;
115113
116- // Spinner for loading states — must use stderr to avoid conflicting with readline on stdout
117- const spinner = ora ( { spinner : "dots" , color : "cyan" , stream : process . stderr } ) ;
114+ // Streaming state
118115 let streamingStarted = false ;
119116
120- // Create agent with callbacks
117+ // Create agent
121118 const agent = new Agent ( {
122119 llm,
123120 workspaceDir,
124121 callbacks : {
125122 onAssistantText : ( text ) => {
126- spinner . stop ( ) ;
127123 if ( text . trim ( ) === "HEARTBEAT_OK" ) return ;
128-
129124 if ( streamingStarted ) {
130- // Text was already streamed to terminal , just finish with newline
125+ // Text was already streamed, just finish
131126 process . stdout . write ( "\n\n" ) ;
132127 } else {
133- // Non-streamed response (e.g. short tool-only reply) : render with markdown
128+ // Non-streamed response: render markdown
134129 const rendered = renderMarkdown ( text ) ;
135- process . stdout . write ( chalk . green ( "🦐 " ) + rendered + "\n" ) ;
130+ console . log ( chalk . green ( "\n 🦐 " ) + rendered ) ;
136131 }
137132 streamingStarted = false ;
138133 } ,
139134 onTextChunk : ( chunk ) => {
140- spinner . stop ( ) ;
141135 if ( ! streamingStarted ) {
142136 process . stdout . write ( chalk . green ( "\n🦐 " ) ) ;
143137 streamingStarted = true ;
144138 }
145139 process . stdout . write ( chunk ) ;
146140 } ,
147141 onToolCall : ( name , args ) => {
148- spinner . stop ( ) ;
149142 console . log (
150143 chalk . dim ( ` ⚙️ ${ name } (${ Object . entries ( args ) . map ( ( [ k , v ] ) => `${ k } =${ JSON . stringify ( v ) . slice ( 0 , 60 ) } ` ) . join ( ", " ) } )` ) ,
151144 ) ;
152- spinner . start ( "Thinking..." ) ;
153145 } ,
154146 onToolResult : ( name , result ) => {
155- spinner . stop ( ) ;
156147 if ( result . length > 200 ) {
157148 console . log ( chalk . dim ( ` ✓ ${ name } → ${ result . slice ( 0 , 200 ) } ...` ) ) ;
158149 } else {
159150 console . log ( chalk . dim ( ` ✓ ${ name } → ${ result } ` ) ) ;
160151 }
161- spinner . start ( "Thinking..." ) ;
162152 } ,
163153 onHeartbeatStart : ( ) => {
164154 const ts = new Date ( ) . toLocaleString ( ) ;
165- spinner . stop ( ) ;
166155 console . log ( chalk . dim ( `\n💓 Heartbeat scan [${ ts } ]...\n` ) ) ;
167156 } ,
168157 onHeartbeatEnd : ( result ) => {
@@ -178,44 +167,109 @@ async function main() {
178167 console . log ( chalk . dim ( `Model: ${ config . llm . model } ` ) ) ;
179168 console . log ( "" ) ;
180169 console . log ( chalk . cyan ( "📖 Quick Guide:" ) ) ;
181- console . log ( chalk . dim ( " • 输入 exit 或 quit 或 Ctrl+C 退出 " ) ) ;
170+ console . log ( chalk . dim ( " • 输入 exit 或 quit 退出对话 " ) ) ;
182171 console . log ( chalk . dim ( ' • 输入 """ 进入多行模式,再次输入 """ 发送' ) ) ;
183172 console . log ( chalk . dim ( " • 拖拽文件到终端,自动复制到 user/ 文件夹" ) ) ;
184173 console . log ( chalk . dim ( " • 在 skills/ 下添加 SKILL.md 可扩展 AI 的能力" ) ) ;
185174 console . log ( chalk . dim ( "\n" + "─" . repeat ( 60 ) ) + "\n" ) ;
186175
187- // Create input handler
188- const input = new CliInput ( {
176+ // Interactive chat loop — simple readline, proven to work
177+ const rl = readline . createInterface ( {
178+ input : process . stdin ,
179+ output : process . stdout ,
189180 prompt : chalk . cyan ( "You: " ) ,
190- userDir : path . join ( workspaceDir , "user" ) ,
191181 } ) ;
192182
193- input . on ( "message" , async ( text : string ) => {
194- streamingStarted = false ;
195- spinner . start ( "Thinking..." ) ;
196- try {
197- await agent . chat ( text ) ;
198- } catch ( err ) {
199- spinner . stop ( ) ;
200- streamingStarted = false ;
201- console . error ( chalk . red ( `\nError: ${ err instanceof Error ? err . message : String ( err ) } \n` ) ) ;
183+ // Multiline state
184+ let multilineMode = false ;
185+ let multilineBuffer : string [ ] = [ ] ;
186+ const userDir = path . join ( workspaceDir , "user" ) ;
187+
188+ rl . prompt ( ) ;
189+
190+ rl . on ( "line" , async ( line ) => {
191+ // --- Multiline mode ---
192+ if ( multilineMode ) {
193+ if ( line . trim ( ) === '"""' ) {
194+ multilineMode = false ;
195+ const text = multilineBuffer . join ( "\n" ) . trim ( ) ;
196+ multilineBuffer = [ ] ;
197+ if ( text ) {
198+ await handleMessage ( text ) ;
199+ }
200+ rl . prompt ( ) ;
201+ } else {
202+ multilineBuffer . push ( line ) ;
203+ process . stdout . write ( chalk . dim ( "... " ) ) ;
204+ }
205+ return ;
202206 }
203- input . showInputPrompt ( ) ;
204- } ) ;
205207
206- input . on ( "file" , ( ) => {
207- // File was already copied by CliInput
208+ // --- Start multiline ---
209+ if ( line . trim ( ) === '"""' ) {
210+ multilineMode = true ;
211+ multilineBuffer = [ ] ;
212+ console . log ( chalk . dim ( '📝 Multiline mode — type """ on a new line to send' ) ) ;
213+ process . stdout . write ( chalk . dim ( "... " ) ) ;
214+ return ;
215+ }
216+
217+ const input = line . trim ( ) ;
218+ if ( ! input ) {
219+ rl . prompt ( ) ;
220+ return ;
221+ }
222+
223+ // Exit
224+ if ( input . toLowerCase ( ) === "exit" || input . toLowerCase ( ) === "quit" ) {
225+ console . log ( chalk . dim ( "\nGoodbye! 🦐\n" ) ) ;
226+ agent . stop ( ) ;
227+ rl . close ( ) ;
228+ process . exit ( 0 ) ;
229+ }
230+
231+ // File drag-and-drop detection
232+ const cleanPath = input . replace ( / ^ [ ' " ] | [ ' " ] $ / g, "" ) . trim ( ) ;
233+ if ( cleanPath . startsWith ( "/" ) || cleanPath . startsWith ( "~" ) ) {
234+ if ( ! cleanPath . includes ( " " ) || cleanPath . includes ( "\\ " ) ) {
235+ const resolved = cleanPath
236+ . replace ( / ^ ~ / , process . env . HOME ?? "" )
237+ . replace ( / \\ / g, " " ) ;
238+ try {
239+ const stat = await fs . stat ( resolved ) ;
240+ if ( stat . isFile ( ) ) {
241+ const fileName = path . basename ( resolved ) ;
242+ const dest = path . join ( userDir , fileName ) ;
243+ await fs . copyFile ( resolved , dest ) ;
244+ const sizeKb = ( stat . size / 1024 ) . toFixed ( 1 ) ;
245+ console . log ( chalk . green ( `✓ Copied to user/${ fileName } (${ sizeKb } KB)` ) ) ;
246+ rl . prompt ( ) ;
247+ return ;
248+ }
249+ } catch {
250+ // Not a valid path, treat as message
251+ }
252+ }
253+ }
254+
255+ await handleMessage ( input ) ;
256+ rl . prompt ( ) ;
208257 } ) ;
209258
210- input . on ( "exit" , ( ) => {
211- spinner . stop ( ) ;
212- console . log ( chalk . dim ( "\nGoodbye! 🦐\n" ) ) ;
259+ rl . on ( "close" , ( ) => {
213260 agent . stop ( ) ;
214- input . stop ( ) ;
215261 process . exit ( 0 ) ;
216262 } ) ;
217263
218- input . start ( ) ;
264+ async function handleMessage ( text : string ) : Promise < void > {
265+ streamingStarted = false ;
266+ console . log ( chalk . dim ( "⏳ Thinking..." ) ) ;
267+ try {
268+ await agent . chat ( text ) ;
269+ } catch ( err ) {
270+ console . error ( chalk . red ( `\nError: ${ err instanceof Error ? err . message : String ( err ) } \n` ) ) ;
271+ }
272+ }
219273}
220274
221275main ( ) . catch ( ( err ) => {
0 commit comments