Skip to content

Commit e2dad8f

Browse files
committed
fix: rewrite to inline readline, add error handlers for debugging exit issue
1 parent 7051d91 commit e2dad8f

1 file changed

Lines changed: 107 additions & 53 deletions

File tree

src/index.ts

Lines changed: 107 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
#!/usr/bin/env node
22

33
import path from "node:path";
4+
import readline from "node:readline";
5+
import fs from "node:fs/promises";
46
import { fileURLToPath } from "node:url";
57
import chalk from "chalk";
6-
import ora from "ora";
78
import { loadConfig, saveConfig, resolveWorkspaceDir } from "./config/config.js";
89
import { createOpenAIProvider } from "./llm/provider.js";
910
import { Agent } from "./agent/agent.js";
1011
import { initWorkspace } from "./workspace/init.js";
11-
import { CliInput } from "./cli/input.js";
12-
import readline from "node:readline";
1312

1413
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1514
const TEMPLATE_DIR = path.resolve(__dirname, "../templates");
1615

17-
/** Simple terminal markdown renderer using chalk */
16+
/** Simple terminal markdown renderer */
1817
function 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

221275
main().catch((err) => {

0 commit comments

Comments
 (0)