arc402-cli 0.5.0 → 0.7.0

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