arc402-cli 0.7.3 → 0.7.4
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.
- package/TUI-SPEC.md +214 -0
- package/dist/index.js +55 -1
- package/dist/index.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +46 -558
- package/dist/repl.js.map +1 -1
- package/dist/tui/App.d.ts +12 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +154 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/Footer.d.ts +11 -0
- package/dist/tui/Footer.d.ts.map +1 -0
- package/dist/tui/Footer.js +13 -0
- package/dist/tui/Footer.js.map +1 -0
- package/dist/tui/Header.d.ts +14 -0
- package/dist/tui/Header.d.ts.map +1 -0
- package/dist/tui/Header.js +19 -0
- package/dist/tui/Header.js.map +1 -0
- package/dist/tui/InputLine.d.ts +11 -0
- package/dist/tui/InputLine.d.ts.map +1 -0
- package/dist/tui/InputLine.js +145 -0
- package/dist/tui/InputLine.js.map +1 -0
- package/dist/tui/Viewport.d.ts +14 -0
- package/dist/tui/Viewport.d.ts.map +1 -0
- package/dist/tui/Viewport.js +48 -0
- package/dist/tui/Viewport.js.map +1 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +55 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/useChat.d.ts +11 -0
- package/dist/tui/useChat.d.ts.map +1 -0
- package/dist/tui/useChat.js +91 -0
- package/dist/tui/useChat.js.map +1 -0
- package/dist/tui/useCommand.d.ts +12 -0
- package/dist/tui/useCommand.d.ts.map +1 -0
- package/dist/tui/useCommand.js +137 -0
- package/dist/tui/useCommand.js.map +1 -0
- package/dist/tui/useScroll.d.ts +17 -0
- package/dist/tui/useScroll.d.ts.map +1 -0
- package/dist/tui/useScroll.js +46 -0
- package/dist/tui/useScroll.js.map +1 -0
- package/package.json +5 -1
- package/src/index.ts +21 -1
- package/src/repl.ts +50 -663
- package/src/tui/App.tsx +214 -0
- package/src/tui/Footer.tsx +18 -0
- package/src/tui/Header.tsx +30 -0
- package/src/tui/InputLine.tsx +164 -0
- package/src/tui/Viewport.tsx +70 -0
- package/src/tui/index.tsx +72 -0
- package/src/tui/useChat.ts +103 -0
- package/src/tui/useCommand.ts +148 -0
- package/src/tui/useScroll.ts +65 -0
- package/tsconfig.json +6 -1
package/src/repl.ts
CHANGED
|
@@ -2,15 +2,14 @@ import chalk from "chalk";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import os from "os";
|
|
5
|
+
import readline from "readline";
|
|
5
6
|
import { createProgram } from "./program";
|
|
6
7
|
import { getBannerLines, BannerConfig } from "./ui/banner";
|
|
7
8
|
import { c } from "./ui/colors";
|
|
8
9
|
|
|
9
|
-
// ─── Config
|
|
10
|
+
// ─── Config helpers ────────────────────────────────────────────────────────────
|
|
10
11
|
|
|
11
12
|
const CONFIG_PATH = path.join(os.homedir(), ".arc402", "config.json");
|
|
12
|
-
const HISTORY_PATH = path.join(os.homedir(), ".arc402", "repl_history");
|
|
13
|
-
const MAX_HISTORY = 1000;
|
|
14
13
|
|
|
15
14
|
async function loadBannerConfig(): Promise<BannerConfig | undefined> {
|
|
16
15
|
if (!fs.existsSync(CONFIG_PATH)) return undefined;
|
|
@@ -18,60 +17,21 @@ async function loadBannerConfig(): Promise<BannerConfig | undefined> {
|
|
|
18
17
|
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as {
|
|
19
18
|
network?: string;
|
|
20
19
|
walletContractAddress?: string;
|
|
21
|
-
rpcUrl?: string;
|
|
22
20
|
};
|
|
23
21
|
const cfg: BannerConfig = { network: raw.network };
|
|
24
22
|
if (raw.walletContractAddress) {
|
|
25
23
|
const w = raw.walletContractAddress;
|
|
26
24
|
cfg.wallet = `${w.slice(0, 6)}...${w.slice(-4)}`;
|
|
27
25
|
}
|
|
28
|
-
if (raw.rpcUrl && raw.walletContractAddress) {
|
|
29
|
-
try {
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
31
|
-
const ethersLib = require("ethers") as typeof import("ethers");
|
|
32
|
-
const provider = new ethersLib.ethers.JsonRpcProvider(raw.rpcUrl);
|
|
33
|
-
const bal = await Promise.race([
|
|
34
|
-
provider.getBalance(raw.walletContractAddress),
|
|
35
|
-
new Promise<never>((_, r) =>
|
|
36
|
-
setTimeout(() => r(new Error("timeout")), 2000)
|
|
37
|
-
),
|
|
38
|
-
]);
|
|
39
|
-
cfg.balance = `${parseFloat(
|
|
40
|
-
ethersLib.ethers.formatEther(bal)
|
|
41
|
-
).toFixed(4)} ETH`;
|
|
42
|
-
} catch {
|
|
43
|
-
/* skip balance on timeout */
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
26
|
return cfg;
|
|
47
27
|
} catch {
|
|
48
28
|
return undefined;
|
|
49
29
|
}
|
|
50
30
|
}
|
|
51
31
|
|
|
52
|
-
// ───
|
|
53
|
-
|
|
54
|
-
const ESC = "\x1b";
|
|
55
|
-
|
|
56
|
-
const ansi = {
|
|
57
|
-
clearScreen: `${ESC}[2J`,
|
|
58
|
-
home: `${ESC}[H`,
|
|
59
|
-
clearLine: `${ESC}[2K`,
|
|
60
|
-
clearToEol: `${ESC}[K`,
|
|
61
|
-
hideCursor: `${ESC}[?25l`,
|
|
62
|
-
showCursor: `${ESC}[?25h`,
|
|
63
|
-
move: (r: number, col: number) => `${ESC}[${r};${col}H`,
|
|
64
|
-
scrollRegion: (top: number, bot: number) => `${ESC}[${top};${bot}r`,
|
|
65
|
-
resetScroll: `${ESC}[r`,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
function write(s: string): void {
|
|
69
|
-
process.stdout.write(s);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ─── Prompt constants ─────────────────────────────────────────────────────────
|
|
32
|
+
// ─── Prompt ────────────────────────────────────────────────────────────────────
|
|
73
33
|
|
|
74
|
-
const
|
|
34
|
+
const PROMPT =
|
|
75
35
|
chalk.cyanBright("◈") +
|
|
76
36
|
" " +
|
|
77
37
|
chalk.dim("arc402") +
|
|
@@ -79,13 +39,6 @@ const PROMPT_TEXT =
|
|
|
79
39
|
chalk.white(">") +
|
|
80
40
|
" ";
|
|
81
41
|
|
|
82
|
-
// Visible character count of "◈ arc402 > "
|
|
83
|
-
const PROMPT_VIS = 11;
|
|
84
|
-
|
|
85
|
-
// ─── Known command detection ──────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
const BUILTIN_CMDS = ["help", "exit", "quit", "clear", "status"];
|
|
88
|
-
|
|
89
42
|
// ─── Shell-style tokenizer ────────────────────────────────────────────────────
|
|
90
43
|
|
|
91
44
|
function parseTokens(input: string): string[] {
|
|
@@ -124,403 +77,60 @@ function parseTokens(input: string): string[] {
|
|
|
124
77
|
return tokens;
|
|
125
78
|
}
|
|
126
79
|
|
|
127
|
-
// ───
|
|
128
|
-
|
|
129
|
-
function getCompletions(
|
|
130
|
-
line: string,
|
|
131
|
-
topCmds: string[],
|
|
132
|
-
subCmds: Map<string, string[]>
|
|
133
|
-
): string[] {
|
|
134
|
-
const allTop = [...BUILTIN_CMDS, ...topCmds];
|
|
135
|
-
const trimmed = line.trimStart();
|
|
136
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
137
|
-
if (spaceIdx === -1) {
|
|
138
|
-
return allTop.filter((cmd) => cmd.startsWith(trimmed));
|
|
139
|
-
}
|
|
140
|
-
const parent = trimmed.slice(0, spaceIdx);
|
|
141
|
-
const rest = trimmed.slice(spaceIdx + 1);
|
|
142
|
-
const subs = subCmds.get(parent) ?? [];
|
|
143
|
-
return subs.filter((s) => s.startsWith(rest)).map((s) => `${parent} ${s}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ─── TUI class ────────────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
class TUI {
|
|
149
|
-
private inputBuffer = "";
|
|
150
|
-
private cursorPos = 0;
|
|
151
|
-
private history: string[] = [];
|
|
152
|
-
private historyIdx = -1;
|
|
153
|
-
private historyTemp = "";
|
|
154
|
-
private bannerLines: string[] = [];
|
|
155
|
-
private topCmds: string[] = [];
|
|
156
|
-
private subCmds = new Map<string, string[]>();
|
|
157
|
-
private bannerCfg?: BannerConfig;
|
|
158
|
-
private commandRunning = false;
|
|
159
|
-
|
|
160
|
-
private get termRows(): number {
|
|
161
|
-
return process.stdout.rows || 24;
|
|
162
|
-
}
|
|
163
|
-
private get termCols(): number {
|
|
164
|
-
return process.stdout.columns || 80;
|
|
165
|
-
}
|
|
166
|
-
private get scrollTop(): number {
|
|
167
|
-
// +1 for separator row after banner
|
|
168
|
-
return this.bannerLines.length + 2;
|
|
169
|
-
}
|
|
170
|
-
private get scrollBot(): number {
|
|
171
|
-
return this.termRows - 1;
|
|
172
|
-
}
|
|
173
|
-
private get inputRow(): number {
|
|
174
|
-
return this.termRows;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
async start(): Promise<void> {
|
|
180
|
-
this.bannerCfg = await loadBannerConfig();
|
|
181
|
-
|
|
182
|
-
// Load persisted history
|
|
183
|
-
try {
|
|
184
|
-
if (fs.existsSync(HISTORY_PATH)) {
|
|
185
|
-
const lines = fs.readFileSync(HISTORY_PATH, "utf-8").split("\n").filter(Boolean);
|
|
186
|
-
this.history = lines.slice(-MAX_HISTORY);
|
|
187
|
-
}
|
|
188
|
-
} catch { /* non-fatal */ }
|
|
189
|
-
|
|
190
|
-
// Build command metadata for completion
|
|
191
|
-
const template = createProgram();
|
|
192
|
-
this.topCmds = template.commands.map((cmd) => cmd.name());
|
|
193
|
-
for (const cmd of template.commands) {
|
|
194
|
-
if (cmd.commands.length > 0) {
|
|
195
|
-
this.subCmds.set(
|
|
196
|
-
cmd.name(),
|
|
197
|
-
cmd.commands.map((s) => s.name())
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Draw initial screen
|
|
203
|
-
this.setupScreen();
|
|
204
|
-
this.drawInputLine();
|
|
205
|
-
|
|
206
|
-
// Enter raw mode
|
|
207
|
-
if (process.stdin.isTTY) {
|
|
208
|
-
process.stdin.setRawMode(true);
|
|
209
|
-
}
|
|
210
|
-
process.stdin.resume();
|
|
211
|
-
process.stdin.setEncoding("utf8");
|
|
212
|
-
process.stdin.on("data", this.boundKeyHandler);
|
|
213
|
-
|
|
214
|
-
// Resize handler
|
|
215
|
-
process.stdout.on("resize", () => {
|
|
216
|
-
this.setupScreen();
|
|
217
|
-
this.drawInputLine();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// SIGINT (shouldn't fire in raw mode, but just in case)
|
|
221
|
-
process.on("SIGINT", () => this.exitGracefully());
|
|
222
|
-
|
|
223
|
-
// Keep alive — process.stdin listener keeps the event loop running
|
|
224
|
-
await new Promise<never>(() => {
|
|
225
|
-
/* never resolves; process.exit() is called on quit */
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── Screen setup ────────────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
private setupScreen(): void {
|
|
232
|
-
this.bannerLines = getBannerLines(this.bannerCfg);
|
|
233
|
-
|
|
234
|
-
// Simple setup — print banner once, no scroll regions (breaks on macOS Terminal)
|
|
235
|
-
for (const line of this.bannerLines) {
|
|
236
|
-
write(line + "\n");
|
|
237
|
-
}
|
|
238
|
-
write(chalk.dim("─".repeat(Math.min(this.termCols, 60))) + "\n\n");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── Banner repaint (in-place, preserves output area) ────────────────────────
|
|
242
|
-
|
|
243
|
-
private repaintBanner(): void {
|
|
244
|
-
write(ansi.hideCursor);
|
|
245
|
-
for (let i = 0; i < this.bannerLines.length; i++) {
|
|
246
|
-
write(ansi.move(i + 1, 1) + ansi.clearToEol + this.bannerLines[i]);
|
|
247
|
-
}
|
|
248
|
-
// Separator
|
|
249
|
-
const sepRow = this.bannerLines.length + 1;
|
|
250
|
-
write(
|
|
251
|
-
ansi.move(sepRow, 1) +
|
|
252
|
-
ansi.clearToEol +
|
|
253
|
-
chalk.dim("─".repeat(this.termCols))
|
|
254
|
-
);
|
|
255
|
-
write(ansi.showCursor);
|
|
256
|
-
}
|
|
80
|
+
// ─── REPL entry point (basic readline fallback) ────────────────────────────────
|
|
257
81
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Place cursor at correct position within the input
|
|
264
|
-
write(ansi.move(this.inputRow, PROMPT_VIS + 1 + this.cursorPos));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ── Key handler ──────────────────────────────────────────────────────────────
|
|
268
|
-
|
|
269
|
-
private readonly boundKeyHandler = (key: string): void => {
|
|
270
|
-
if (this.commandRunning) return;
|
|
271
|
-
this.handleKey(key);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
private handleKey(key: string): void {
|
|
275
|
-
// Ctrl+C
|
|
276
|
-
if (key === "\u0003") {
|
|
277
|
-
this.exitGracefully();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
// Ctrl+L — refresh
|
|
281
|
-
if (key === "\u000C") {
|
|
282
|
-
this.setupScreen();
|
|
283
|
-
this.drawInputLine();
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// Enter
|
|
287
|
-
if (key === "\r" || key === "\n") {
|
|
288
|
-
void this.submit();
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// Backspace
|
|
292
|
-
if (key === "\u007F" || key === "\b") {
|
|
293
|
-
if (this.cursorPos > 0) {
|
|
294
|
-
this.inputBuffer =
|
|
295
|
-
this.inputBuffer.slice(0, this.cursorPos - 1) +
|
|
296
|
-
this.inputBuffer.slice(this.cursorPos);
|
|
297
|
-
this.cursorPos--;
|
|
298
|
-
this.drawInputLine();
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
// Delete (forward)
|
|
303
|
-
if (key === "\x1b[3~") {
|
|
304
|
-
if (this.cursorPos < this.inputBuffer.length) {
|
|
305
|
-
this.inputBuffer =
|
|
306
|
-
this.inputBuffer.slice(0, this.cursorPos) +
|
|
307
|
-
this.inputBuffer.slice(this.cursorPos + 1);
|
|
308
|
-
this.drawInputLine();
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
// Up arrow — history prev
|
|
313
|
-
if (key === "\x1b[A") {
|
|
314
|
-
if (this.historyIdx === -1) {
|
|
315
|
-
this.historyTemp = this.inputBuffer;
|
|
316
|
-
this.historyIdx = this.history.length - 1;
|
|
317
|
-
} else if (this.historyIdx > 0) {
|
|
318
|
-
this.historyIdx--;
|
|
319
|
-
}
|
|
320
|
-
if (this.historyIdx >= 0) {
|
|
321
|
-
this.inputBuffer = this.history[this.historyIdx];
|
|
322
|
-
this.cursorPos = this.inputBuffer.length;
|
|
323
|
-
this.drawInputLine();
|
|
324
|
-
}
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
// Down arrow — history next
|
|
328
|
-
if (key === "\x1b[B") {
|
|
329
|
-
if (this.historyIdx >= 0) {
|
|
330
|
-
this.historyIdx++;
|
|
331
|
-
if (this.historyIdx >= this.history.length) {
|
|
332
|
-
this.historyIdx = -1;
|
|
333
|
-
this.inputBuffer = this.historyTemp;
|
|
334
|
-
} else {
|
|
335
|
-
this.inputBuffer = this.history[this.historyIdx];
|
|
336
|
-
}
|
|
337
|
-
this.cursorPos = this.inputBuffer.length;
|
|
338
|
-
this.drawInputLine();
|
|
339
|
-
}
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
// Right arrow
|
|
343
|
-
if (key === "\x1b[C") {
|
|
344
|
-
if (this.cursorPos < this.inputBuffer.length) {
|
|
345
|
-
this.cursorPos++;
|
|
346
|
-
this.drawInputLine();
|
|
347
|
-
}
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
// Left arrow
|
|
351
|
-
if (key === "\x1b[D") {
|
|
352
|
-
if (this.cursorPos > 0) {
|
|
353
|
-
this.cursorPos--;
|
|
354
|
-
this.drawInputLine();
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
// Home / Ctrl+A
|
|
359
|
-
if (key === "\x1b[H" || key === "\u0001") {
|
|
360
|
-
this.cursorPos = 0;
|
|
361
|
-
this.drawInputLine();
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
// End / Ctrl+E
|
|
365
|
-
if (key === "\x1b[F" || key === "\u0005") {
|
|
366
|
-
this.cursorPos = this.inputBuffer.length;
|
|
367
|
-
this.drawInputLine();
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
// Ctrl+U — clear line
|
|
371
|
-
if (key === "\u0015") {
|
|
372
|
-
this.inputBuffer = "";
|
|
373
|
-
this.cursorPos = 0;
|
|
374
|
-
this.drawInputLine();
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
// Ctrl+K — kill to end
|
|
378
|
-
if (key === "\u000B") {
|
|
379
|
-
this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos);
|
|
380
|
-
this.drawInputLine();
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
// Tab — completion
|
|
384
|
-
if (key === "\t") {
|
|
385
|
-
this.handleTab();
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
// Printable characters
|
|
389
|
-
if (key >= " " && !key.startsWith("\x1b")) {
|
|
390
|
-
this.inputBuffer =
|
|
391
|
-
this.inputBuffer.slice(0, this.cursorPos) +
|
|
392
|
-
key +
|
|
393
|
-
this.inputBuffer.slice(this.cursorPos);
|
|
394
|
-
this.cursorPos += key.length;
|
|
395
|
-
this.drawInputLine();
|
|
82
|
+
export async function startREPL(): Promise<void> {
|
|
83
|
+
if (!process.stdout.isTTY) {
|
|
84
|
+
const bannerCfg = await loadBannerConfig();
|
|
85
|
+
for (const line of getBannerLines(bannerCfg)) {
|
|
86
|
+
process.stdout.write(line + "\n");
|
|
396
87
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// ── Tab completion ───────────────────────────────────────────────────────────
|
|
400
|
-
|
|
401
|
-
private handleTab(): void {
|
|
402
|
-
const completions = getCompletions(
|
|
403
|
-
this.inputBuffer,
|
|
404
|
-
this.topCmds,
|
|
405
|
-
this.subCmds
|
|
88
|
+
process.stdout.write(
|
|
89
|
+
"Interactive TUI requires a TTY. Use arc402 <command> directly.\n"
|
|
406
90
|
);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (completions.length === 1) {
|
|
410
|
-
this.inputBuffer = completions[0] + " ";
|
|
411
|
-
this.cursorPos = this.inputBuffer.length;
|
|
412
|
-
this.drawInputLine();
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Find common prefix
|
|
417
|
-
const common = completions.reduce((a, b) => {
|
|
418
|
-
let i = 0;
|
|
419
|
-
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
420
|
-
return a.slice(0, i);
|
|
421
|
-
});
|
|
422
|
-
if (common.length > this.inputBuffer.trimStart().length) {
|
|
423
|
-
this.inputBuffer = common;
|
|
424
|
-
this.cursorPos = common.length;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Show options in output area
|
|
428
|
-
this.writeOutput("\n" + chalk.dim(completions.join(" ")) + "\n");
|
|
429
|
-
this.drawInputLine();
|
|
91
|
+
return;
|
|
430
92
|
}
|
|
431
93
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// Move cursor to bottom of scroll region to ensure scroll-down works
|
|
436
|
-
write(ansi.move(this.scrollBot, 1));
|
|
437
|
-
write(text);
|
|
94
|
+
const bannerCfg = await loadBannerConfig();
|
|
95
|
+
for (const line of getBannerLines(bannerCfg)) {
|
|
96
|
+
process.stdout.write(line + "\n");
|
|
438
97
|
}
|
|
439
98
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
this.historyIdx = -1;
|
|
447
|
-
|
|
448
|
-
if (!input) {
|
|
449
|
-
this.drawInputLine();
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Add to history
|
|
454
|
-
if (input !== this.history[this.history.length - 1]) {
|
|
455
|
-
this.history.push(input);
|
|
456
|
-
}
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
output: process.stdout,
|
|
102
|
+
prompt: PROMPT,
|
|
103
|
+
terminal: true,
|
|
104
|
+
});
|
|
457
105
|
|
|
458
|
-
|
|
459
|
-
this.writeOutput(
|
|
460
|
-
"\n" + chalk.dim("◈ ") + chalk.white(input) + "\n"
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
464
|
-
|
|
465
|
-
if (input === "exit" || input === "quit") {
|
|
466
|
-
this.exitGracefully();
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
106
|
+
rl.prompt();
|
|
469
107
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.setupScreen();
|
|
473
|
-
this.drawInputLine();
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
108
|
+
rl.on("line", async (input) => {
|
|
109
|
+
const trimmed = input.trim();
|
|
476
110
|
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
this.afterCommand();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
rl.prompt();
|
|
480
113
|
return;
|
|
481
114
|
}
|
|
482
115
|
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
116
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
117
|
+
process.stdout.write(
|
|
118
|
+
" " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n"
|
|
119
|
+
);
|
|
120
|
+
process.exit(0);
|
|
487
121
|
}
|
|
488
122
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (msg) {
|
|
494
|
-
this.commandRunning = true;
|
|
495
|
-
await this.sendChat(msg);
|
|
496
|
-
this.commandRunning = false;
|
|
123
|
+
if (trimmed === "clear") {
|
|
124
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
125
|
+
for (const line of getBannerLines(bannerCfg)) {
|
|
126
|
+
process.stdout.write(line + "\n");
|
|
497
127
|
}
|
|
498
|
-
|
|
128
|
+
rl.prompt();
|
|
499
129
|
return;
|
|
500
130
|
}
|
|
501
131
|
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
const firstWord = input.split(/\s+/)[0];
|
|
505
|
-
const allKnown = [...BUILTIN_CMDS, ...this.topCmds];
|
|
506
|
-
if (!allKnown.includes(firstWord)) {
|
|
507
|
-
this.commandRunning = true;
|
|
508
|
-
await this.sendChat(input);
|
|
509
|
-
this.commandRunning = false;
|
|
510
|
-
this.afterCommand();
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// ── Dispatch to commander ──────────────────────────────────────────────────
|
|
515
|
-
|
|
516
|
-
this.commandRunning = true;
|
|
517
|
-
// Move output cursor to bottom of scroll region
|
|
518
|
-
write(ansi.move(this.scrollBot, 1));
|
|
519
|
-
|
|
520
|
-
// Suspend TUI stdin so interactive commands (prompts, readline) work cleanly
|
|
521
|
-
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
522
|
-
|
|
523
|
-
const tokens = parseTokens(input);
|
|
132
|
+
// Dispatch to commander
|
|
133
|
+
const tokens = parseTokens(trimmed);
|
|
524
134
|
const prog = createProgram();
|
|
525
135
|
prog.exitOverride();
|
|
526
136
|
prog.configureOutput({
|
|
@@ -537,18 +147,12 @@ class TUI {
|
|
|
537
147
|
e.code === "commander.version" ||
|
|
538
148
|
e.code === "commander.executeSubCommandAsync"
|
|
539
149
|
) {
|
|
540
|
-
// already written
|
|
150
|
+
// already written
|
|
541
151
|
} else if (e.code === "commander.unknownCommand") {
|
|
542
152
|
process.stdout.write(
|
|
543
153
|
`\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} \n`
|
|
544
154
|
);
|
|
545
|
-
process.stdout.write(
|
|
546
|
-
chalk.dim(" Type 'help' for available commands\n")
|
|
547
|
-
);
|
|
548
|
-
} else if (e.code?.startsWith("commander.")) {
|
|
549
|
-
process.stdout.write(
|
|
550
|
-
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
551
|
-
);
|
|
155
|
+
process.stdout.write(chalk.dim(" Type 'help' for available commands\n"));
|
|
552
156
|
} else {
|
|
553
157
|
process.stdout.write(
|
|
554
158
|
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
@@ -556,236 +160,19 @@ class TUI {
|
|
|
556
160
|
}
|
|
557
161
|
}
|
|
558
162
|
|
|
559
|
-
// Restore raw mode + our listener (interactive commands may have toggled it)
|
|
560
|
-
if (process.stdin.isTTY) {
|
|
561
|
-
process.stdin.setRawMode(true);
|
|
562
|
-
}
|
|
563
|
-
process.stdin.on("data", this.boundKeyHandler);
|
|
564
|
-
this.commandRunning = false;
|
|
565
|
-
|
|
566
|
-
this.afterCommand();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// ── OpenClaw chat ─────────────────────────────────────────────────────────────
|
|
570
|
-
|
|
571
|
-
private async sendChat(rawMessage: string): Promise<void> {
|
|
572
|
-
const message = rawMessage.trim().slice(0, 10000);
|
|
573
|
-
write(ansi.move(this.scrollBot, 1));
|
|
574
|
-
|
|
575
|
-
let res: Response;
|
|
576
|
-
try {
|
|
577
|
-
res = await fetch("http://localhost:19000/api/agent", {
|
|
578
|
-
method: "POST",
|
|
579
|
-
headers: { "Content-Type": "application/json" },
|
|
580
|
-
body: JSON.stringify({ message, session: "arc402-repl" }),
|
|
581
|
-
signal: AbortSignal.timeout(30000),
|
|
582
|
-
});
|
|
583
|
-
} catch (err: unknown) {
|
|
584
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
585
|
-
const isDown =
|
|
586
|
-
msg.includes("ECONNREFUSED") ||
|
|
587
|
-
msg.includes("fetch failed") ||
|
|
588
|
-
msg.includes("ENOTFOUND") ||
|
|
589
|
-
msg.includes("UND_ERR_SOCKET");
|
|
590
|
-
if (isDown) {
|
|
591
|
-
process.stdout.write(
|
|
592
|
-
"\n " +
|
|
593
|
-
chalk.yellow("⚠") +
|
|
594
|
-
" " +
|
|
595
|
-
chalk.dim("OpenClaw gateway not running. Start with: ") +
|
|
596
|
-
chalk.white("openclaw gateway start") +
|
|
597
|
-
"\n"
|
|
598
|
-
);
|
|
599
|
-
} else {
|
|
600
|
-
process.stdout.write(
|
|
601
|
-
"\n " + c.failure + " " + chalk.red(msg) + "\n"
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (!res.body) {
|
|
608
|
-
process.stdout.write(
|
|
609
|
-
"\n" + chalk.dim(" ◈ ") + chalk.white("(empty response)") + "\n"
|
|
610
|
-
);
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
163
|
process.stdout.write("\n");
|
|
164
|
+
rl.prompt();
|
|
165
|
+
});
|
|
615
166
|
|
|
616
|
-
|
|
617
|
-
// Unwrap SSE data lines
|
|
618
|
-
if (line.startsWith("data: ")) {
|
|
619
|
-
line = line.slice(6);
|
|
620
|
-
if (line === "[DONE]") return;
|
|
621
|
-
try {
|
|
622
|
-
const j = JSON.parse(line) as {
|
|
623
|
-
text?: string;
|
|
624
|
-
content?: string;
|
|
625
|
-
delta?: { text?: string };
|
|
626
|
-
};
|
|
627
|
-
line = j.text ?? j.content ?? j.delta?.text ?? line;
|
|
628
|
-
} catch {
|
|
629
|
-
/* use raw */
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
if (line.trim()) {
|
|
633
|
-
process.stdout.write(chalk.dim(" ◈ ") + chalk.white(line) + "\n");
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
const reader = res.body.getReader();
|
|
638
|
-
const decoder = new TextDecoder();
|
|
639
|
-
let buffer = "";
|
|
640
|
-
|
|
641
|
-
while (true) {
|
|
642
|
-
const { done, value } = await reader.read();
|
|
643
|
-
if (done) break;
|
|
644
|
-
buffer += decoder.decode(value, { stream: true });
|
|
645
|
-
const lines = buffer.split("\n");
|
|
646
|
-
buffer = lines.pop() ?? "";
|
|
647
|
-
for (const line of lines) flushLine(line);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (buffer.trim()) flushLine(buffer);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// ── After each command: show prompt ──────────────────────────────────────────
|
|
654
|
-
|
|
655
|
-
private afterCommand(): void {
|
|
656
|
-
// Simple prompt — no banner repaint (causes scrollback issues on macOS Terminal)
|
|
657
|
-
write("\n");
|
|
658
|
-
this.drawInputLine();
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// ── Built-in: status ─────────────────────────────────────────────────────────
|
|
662
|
-
|
|
663
|
-
private async runStatus(): Promise<void> {
|
|
664
|
-
write(ansi.move(this.scrollBot, 1));
|
|
665
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
666
|
-
process.stdout.write(
|
|
667
|
-
chalk.dim("\n No config found. Run 'config init' to get started.\n")
|
|
668
|
-
);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
try {
|
|
672
|
-
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as {
|
|
673
|
-
network?: string;
|
|
674
|
-
walletContractAddress?: string;
|
|
675
|
-
rpcUrl?: string;
|
|
676
|
-
};
|
|
677
|
-
process.stdout.write("\n");
|
|
678
|
-
if (raw.network)
|
|
679
|
-
process.stdout.write(
|
|
680
|
-
` ${chalk.dim("Network")} ${chalk.white(raw.network)}\n`
|
|
681
|
-
);
|
|
682
|
-
if (raw.walletContractAddress) {
|
|
683
|
-
const w = raw.walletContractAddress;
|
|
684
|
-
process.stdout.write(
|
|
685
|
-
` ${chalk.dim("Wallet")} ${chalk.white(`${w.slice(0, 6)}...${w.slice(-4)}`)}\n`
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
if (raw.rpcUrl && raw.walletContractAddress) {
|
|
689
|
-
try {
|
|
690
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
691
|
-
const ethersLib = require("ethers") as typeof import("ethers");
|
|
692
|
-
const provider = new ethersLib.ethers.JsonRpcProvider(raw.rpcUrl);
|
|
693
|
-
const bal = await Promise.race([
|
|
694
|
-
provider.getBalance(raw.walletContractAddress),
|
|
695
|
-
new Promise<never>((_, r) =>
|
|
696
|
-
setTimeout(() => r(new Error("timeout")), 2000)
|
|
697
|
-
),
|
|
698
|
-
]);
|
|
699
|
-
process.stdout.write(
|
|
700
|
-
` ${chalk.dim("Balance")} ${chalk.white(
|
|
701
|
-
`${parseFloat(ethersLib.ethers.formatEther(bal)).toFixed(4)} ETH`
|
|
702
|
-
)}\n`
|
|
703
|
-
);
|
|
704
|
-
} catch {
|
|
705
|
-
/* skip */
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
process.stdout.write("\n");
|
|
709
|
-
} catch {
|
|
710
|
-
/* skip */
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// ── Built-in: help ────────────────────────────────────────────────────────────
|
|
715
|
-
|
|
716
|
-
private async runHelp(): Promise<void> {
|
|
717
|
-
write(ansi.move(this.scrollBot, 1));
|
|
718
|
-
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
719
|
-
const prog = createProgram();
|
|
720
|
-
prog.exitOverride();
|
|
721
|
-
prog.configureOutput({
|
|
722
|
-
writeOut: (str) => process.stdout.write(str),
|
|
723
|
-
writeErr: (str) => process.stderr.write(str),
|
|
724
|
-
});
|
|
725
|
-
try {
|
|
726
|
-
await prog.parseAsync(["node", "arc402", "--help"]);
|
|
727
|
-
} catch {
|
|
728
|
-
/* commander throws after printing help */
|
|
729
|
-
}
|
|
730
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
731
|
-
process.stdin.on("data", this.boundKeyHandler);
|
|
732
|
-
|
|
733
|
-
process.stdout.write("\n");
|
|
734
|
-
process.stdout.write(chalk.cyanBright("Chat") + "\n");
|
|
735
|
-
process.stdout.write(
|
|
736
|
-
" " +
|
|
737
|
-
chalk.white("<message>") +
|
|
738
|
-
chalk.dim(" Send message to OpenClaw gateway\n")
|
|
739
|
-
);
|
|
167
|
+
rl.on("close", () => {
|
|
740
168
|
process.stdout.write(
|
|
741
|
-
"
|
|
742
|
-
chalk.white("/chat <message>") +
|
|
743
|
-
chalk.dim(" Explicitly route to chat\n")
|
|
169
|
+
"\n " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n"
|
|
744
170
|
);
|
|
745
|
-
process.stdout.write(
|
|
746
|
-
chalk.dim(
|
|
747
|
-
" Gateway: http://localhost:19000 (openclaw gateway start)\n"
|
|
748
|
-
)
|
|
749
|
-
);
|
|
750
|
-
process.stdout.write("\n");
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// ── Exit ──────────────────────────────────────────────────────────────────────
|
|
754
|
-
|
|
755
|
-
private exitGracefully(): void {
|
|
756
|
-
// Save history
|
|
757
|
-
try {
|
|
758
|
-
const toSave = this.history.slice(-MAX_HISTORY);
|
|
759
|
-
fs.mkdirSync(path.dirname(HISTORY_PATH), { recursive: true });
|
|
760
|
-
fs.writeFileSync(HISTORY_PATH, toSave.join("\n") + "\n", { mode: 0o600 });
|
|
761
|
-
} catch { /* non-fatal */ }
|
|
762
|
-
|
|
763
|
-
write(ansi.move(this.inputRow, 1) + ansi.clearLine);
|
|
764
|
-
write(" " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n");
|
|
765
|
-
write(ansi.resetScroll);
|
|
766
|
-
write(ansi.showCursor);
|
|
767
|
-
if (process.stdin.isTTY) {
|
|
768
|
-
process.stdin.setRawMode(false);
|
|
769
|
-
}
|
|
770
171
|
process.exit(0);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// ─── REPL entry point ─────────────────────────────────────────────────────────
|
|
775
|
-
|
|
776
|
-
export async function startREPL(): Promise<void> {
|
|
777
|
-
if (!process.stdout.isTTY) {
|
|
778
|
-
// Non-TTY (piped): fall back to minimal line-mode output
|
|
779
|
-
const bannerCfg = await loadBannerConfig();
|
|
780
|
-
for (const line of getBannerLines(bannerCfg)) {
|
|
781
|
-
process.stdout.write(line + "\n");
|
|
782
|
-
}
|
|
783
|
-
process.stdout.write(
|
|
784
|
-
"Interactive TUI requires a TTY. Use arc402 <command> directly.\n"
|
|
785
|
-
);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
172
|
+
});
|
|
788
173
|
|
|
789
|
-
|
|
790
|
-
await
|
|
174
|
+
// Keep alive
|
|
175
|
+
await new Promise<never>(() => {
|
|
176
|
+
/* readline keeps event loop alive */
|
|
177
|
+
});
|
|
791
178
|
}
|