arc402-cli 0.6.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.
package/src/repl.ts CHANGED
@@ -6,17 +6,11 @@ import { createProgram } from "./program";
6
6
  import { getBannerLines, BannerConfig } from "./ui/banner";
7
7
  import { c } from "./ui/colors";
8
8
 
9
- // ─── Sentinel to intercept process.exit() from commands ──────────────────────
10
-
11
- class REPLExitSignal extends Error {
12
- constructor(public readonly code: number = 0) {
13
- super("repl-exit-signal");
14
- }
15
- }
16
-
17
9
  // ─── Config / banner helpers ──────────────────────────────────────────────────
18
10
 
19
11
  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;
20
14
 
21
15
  async function loadBannerConfig(): Promise<BannerConfig | undefined> {
22
16
  if (!fs.existsSync(CONFIG_PATH)) return undefined;
@@ -99,10 +93,21 @@ function parseTokens(input: string): string[] {
99
93
  let current = "";
100
94
  let inQuote = false;
101
95
  let quoteChar = "";
96
+ let escape = false;
102
97
  for (const ch of input) {
98
+ if (escape) {
99
+ current += ch;
100
+ escape = false;
101
+ continue;
102
+ }
103
103
  if (inQuote) {
104
- if (ch === quoteChar) inQuote = false;
105
- else current += ch;
104
+ if (quoteChar === '"' && ch === "\\") {
105
+ escape = true;
106
+ } else if (ch === quoteChar) {
107
+ inQuote = false;
108
+ } else {
109
+ current += ch;
110
+ }
106
111
  } else if (ch === '"' || ch === "'") {
107
112
  inQuote = true;
108
113
  quoteChar = ch;
@@ -174,6 +179,14 @@ class TUI {
174
179
  async start(): Promise<void> {
175
180
  this.bannerCfg = await loadBannerConfig();
176
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
+
177
190
  // Build command metadata for completion
178
191
  const template = createProgram();
179
192
  this.topCmds = template.commands.map((cmd) => cmd.name());
@@ -529,42 +542,32 @@ class TUI {
529
542
  writeErr: (str) => process.stderr.write(str),
530
543
  });
531
544
 
532
- const origExit = process.exit;
533
- (process as NodeJS.Process).exit = ((code?: number) => {
534
- throw new REPLExitSignal(code ?? 0);
535
- }) as typeof process.exit;
536
-
537
545
  try {
538
546
  await prog.parseAsync(["node", "arc402", ...tokens]);
539
547
  } catch (err) {
540
- if (err instanceof REPLExitSignal) {
541
- // Command called process.exit() — normal
548
+ const e = err as { code?: string; message?: string };
549
+ if (
550
+ e.code === "commander.helpDisplayed" ||
551
+ e.code === "commander.version" ||
552
+ e.code === "commander.executeSubCommandAsync"
553
+ ) {
554
+ // already written or normal exit
555
+ } else if (e.code === "commander.unknownCommand") {
556
+ process.stdout.write(
557
+ `\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} \n`
558
+ );
559
+ process.stdout.write(
560
+ chalk.dim(" Type 'help' for available commands\n")
561
+ );
562
+ } else if (e.code?.startsWith("commander.")) {
563
+ process.stdout.write(
564
+ `\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
565
+ );
542
566
  } else {
543
- const e = err as { code?: string; message?: string };
544
- if (
545
- e.code === "commander.helpDisplayed" ||
546
- e.code === "commander.version"
547
- ) {
548
- // already written
549
- } else if (e.code === "commander.unknownCommand") {
550
- process.stdout.write(
551
- `\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} \n`
552
- );
553
- process.stdout.write(
554
- chalk.dim(" Type 'help' for available commands\n")
555
- );
556
- } else if (e.code?.startsWith("commander.")) {
557
- process.stdout.write(
558
- `\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
559
- );
560
- } else {
561
- process.stdout.write(
562
- `\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
563
- );
564
- }
567
+ process.stdout.write(
568
+ `\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
569
+ );
565
570
  }
566
- } finally {
567
- (process as NodeJS.Process).exit = origExit;
568
571
  }
569
572
 
570
573
  // Restore raw mode + our listener (interactive commands may have toggled it)
@@ -579,7 +582,8 @@ class TUI {
579
582
 
580
583
  // ── OpenClaw chat ─────────────────────────────────────────────────────────────
581
584
 
582
- private async sendChat(message: string): Promise<void> {
585
+ private async sendChat(rawMessage: string): Promise<void> {
586
+ const message = rawMessage.trim().slice(0, 10000);
583
587
  write(ansi.move(this.scrollBot, 1));
584
588
 
585
589
  let res: Response;
@@ -762,6 +766,13 @@ class TUI {
762
766
  // ── Exit ──────────────────────────────────────────────────────────────────────
763
767
 
764
768
  private exitGracefully(): void {
769
+ // Save history
770
+ try {
771
+ const toSave = this.history.slice(-MAX_HISTORY);
772
+ fs.mkdirSync(path.dirname(HISTORY_PATH), { recursive: true });
773
+ fs.writeFileSync(HISTORY_PATH, toSave.join("\n") + "\n", { mode: 0o600 });
774
+ } catch { /* non-fatal */ }
775
+
765
776
  write(ansi.move(this.inputRow, 1) + ansi.clearLine);
766
777
  write(" " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n");
767
778
  write(ansi.resetScroll);
package/src/ui/format.ts CHANGED
@@ -42,6 +42,7 @@ export function formatTimeAgo(timestampSeconds: number): string {
42
42
  const now = Math.floor(Date.now() / 1000);
43
43
  const delta = now - timestampSeconds;
44
44
 
45
+ if (delta < 0) return "in " + formatDuration(Math.abs(delta));
45
46
  if (delta < 60) return "just now";
46
47
  if (delta < 3600) {
47
48
  const m = Math.floor(delta / 60);