sarangai-cli 1.0.12 → 1.0.14

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 (2) hide show
  1. package/dist/index.js +341 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ var require_package = __commonJS({
35
35
  "package.json"(exports2, module2) {
36
36
  module2.exports = {
37
37
  name: "sarangai-cli",
38
- version: "1.0.12",
38
+ version: "1.0.14",
39
39
  description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
40
40
  main: "dist/index.js",
41
41
  bin: {
@@ -96,6 +96,8 @@ var import_chalk = __toESM(require("chalk"));
96
96
  var import_readline = __toESM(require("readline"));
97
97
  var import_child_process = require("child_process");
98
98
  var import_crypto = __toESM(require("crypto"));
99
+ var import_fs2 = __toESM(require("fs"));
100
+ var import_path2 = __toESM(require("path"));
99
101
  var import_clipboardy = __toESM(require("clipboardy"));
100
102
 
101
103
  // src/config.ts
@@ -204,7 +206,25 @@ async function fetchUserMeta(baseUrl2, apiKey) {
204
206
  }
205
207
  } catch {
206
208
  }
207
- return { balance: 0, tier: "DEVELOPER", accountId: "SA-\u2014" };
209
+ return null;
210
+ }
211
+ async function refreshUserMeta() {
212
+ const fresh = await fetchUserMeta(baseUrl, cfg?.apiKey || "");
213
+ if (!fresh) return;
214
+ const changed = !userMeta || fresh.balance !== userMeta.balance || fresh.tier !== userMeta.tier || fresh.accountId !== userMeta.accountId;
215
+ userMeta = fresh;
216
+ if (!changed) return;
217
+ if (inWorkspace) {
218
+ if (isExecuting) drawSessionScreen(lastTurn);
219
+ else if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
220
+ else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
221
+ } else {
222
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
223
+ }
224
+ }
225
+ function redrawWithFreshBalance() {
226
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
227
+ refreshUserMeta();
208
228
  }
209
229
  function getDisplayDir() {
210
230
  const cwd = process.cwd();
@@ -257,7 +277,7 @@ function drawSelectionModal(activeModel2, userMeta2, expanded2, cursorIdx2, copi
257
277
  if (!expanded2) {
258
278
  lines.push(...renderCard(activeModel2, cursorIdx2 === 0, cardWidth, pad));
259
279
  lines.push("");
260
- const balanceStr = `${Number(userMeta2.balance).toLocaleString()} Credits`;
280
+ const balanceStr = `${Number(userMeta2?.balance ?? 0).toLocaleString()} Credits`;
261
281
  lines.push(
262
282
  pad + import_chalk.default.bold.white(userMeta2.tier) + import_chalk.default.gray(" \u2022 ") + import_chalk.default.yellow.bold(balanceStr) + import_chalk.default.gray(" remaining \u2022 ") + import_chalk.default.hex("#a855f7")(userMeta2.accountId)
263
283
  );
@@ -281,7 +301,7 @@ function drawSelectionModal(activeModel2, userMeta2, expanded2, cursorIdx2, copi
281
301
  lines.push(...renderCard(m, cursorIdx2 === idx + 10, cardWidth, pad));
282
302
  });
283
303
  lines.push("");
284
- const balanceStr = `${Number(userMeta2.balance).toLocaleString()} Credits`;
304
+ const balanceStr = `${Number(userMeta2?.balance ?? 0).toLocaleString()} Credits`;
285
305
  lines.push(
286
306
  pad + import_chalk.default.bold.white(userMeta2.tier) + import_chalk.default.gray(" \u2022 ") + import_chalk.default.yellow.bold(balanceStr) + import_chalk.default.gray(" remaining \u2022 ") + import_chalk.default.hex("#a855f7")(userMeta2.accountId)
287
307
  );
@@ -347,7 +367,7 @@ var expanded = false;
347
367
  var cursorIdx = 0;
348
368
  var copiedNotice = false;
349
369
  var activeModel;
350
- var userMeta;
370
+ var userMeta = null;
351
371
  var sessionTimeLeft = 3600;
352
372
  var timerInterval = null;
353
373
  var currentTaskInput = "";
@@ -360,9 +380,13 @@ var respStreaming = false;
360
380
  var respScroll = null;
361
381
  var respTotalLines = 0;
362
382
  var respViewRows = 12;
383
+ var conversationHistory = [];
384
+ var sessionScreen = false;
385
+ var respLastNotes = [];
386
+ var keyBuffer = "";
363
387
  function cleanupAndExit() {
364
388
  if (timerInterval) clearInterval(timerInterval);
365
- process.stdout.write("\x1B[?1049l\x1B[?25h\n");
389
+ process.stdout.write("\x1B[?1000l\x1B[?1006l\x1B[?1049l\x1B[?25h\n");
366
390
  process.exit(0);
367
391
  }
368
392
  function triggerCopyLink() {
@@ -373,22 +397,26 @@ function triggerCopyLink() {
373
397
  function leaveWorkspace() {
374
398
  inWorkspace = false;
375
399
  isExecuting = false;
400
+ sessionScreen = false;
376
401
  if (timerInterval) clearInterval(timerInterval);
377
402
  currentTaskInput = "";
403
+ respScroll = null;
404
+ conversationHistory = [];
378
405
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
379
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
406
+ redrawWithFreshBalance();
380
407
  }
381
408
  function enterWorkspace() {
382
409
  inWorkspace = true;
383
410
  isExecuting = false;
384
411
  currentTaskInput = "";
385
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
412
+ sessionScreen = true;
413
+ redrawInputScreen();
386
414
  if (timerInterval) clearInterval(timerInterval);
387
415
  timerInterval = setInterval(() => {
388
416
  if (sessionTimeLeft > 0) {
389
417
  sessionTimeLeft--;
390
418
  if (inWorkspace && !isExecuting) {
391
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
419
+ redrawInputScreen();
392
420
  }
393
421
  }
394
422
  }, 1e3);
@@ -432,7 +460,141 @@ function styleBodyLine(l) {
432
460
  if (l.style === "error") return import_chalk.default.red(l.text);
433
461
  return import_chalk.default.hex("#e2e8f0")(l.text);
434
462
  }
435
- function drawResponseScreen(query, text, opts) {
463
+ var EXT_BY_LANG = {
464
+ python: "py",
465
+ py: "py",
466
+ javascript: "js",
467
+ js: "js",
468
+ typescript: "ts",
469
+ ts: "ts",
470
+ tsx: "tsx",
471
+ jsx: "jsx",
472
+ go: "go",
473
+ rust: "rs",
474
+ rs: "rs",
475
+ java: "java",
476
+ c: "c",
477
+ cpp: "cpp",
478
+ "c++": "cpp",
479
+ csharp: "cs",
480
+ "c#": "cs",
481
+ cs: "cs",
482
+ php: "php",
483
+ ruby: "rb",
484
+ rb: "rb",
485
+ swift: "swift",
486
+ kotlin: "kt",
487
+ kt: "kt",
488
+ bash: "sh",
489
+ sh: "sh",
490
+ shell: "sh",
491
+ zsh: "sh",
492
+ powershell: "ps1",
493
+ ps1: "ps1",
494
+ html: "html",
495
+ css: "css",
496
+ scss: "scss",
497
+ sass: "sass",
498
+ less: "less",
499
+ sql: "sql",
500
+ json: "json",
501
+ yaml: "yaml",
502
+ yml: "yml",
503
+ toml: "toml",
504
+ xml: "xml",
505
+ markdown: "md",
506
+ md: "md",
507
+ dockerfile: "",
508
+ makefile: "",
509
+ ini: "ini",
510
+ env: "",
511
+ txt: "txt",
512
+ text: "txt",
513
+ csv: "csv"
514
+ };
515
+ function extractFilesFromMarkdown(text) {
516
+ const files = [];
517
+ const fence = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]*)(?::[ \t]*([^\n`]+))?[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
518
+ let m;
519
+ while ((m = fence.exec(text)) !== null) {
520
+ const lang = (m[1] || "").toLowerCase().trim();
521
+ const fname = (m[2] || "").trim().replace(/^["']|["']$/g, "");
522
+ if (!fname) continue;
523
+ files.push({ filename: fname, content: m[3], lang: lang || "text" });
524
+ }
525
+ return files;
526
+ }
527
+ function sanitizeFilename(name) {
528
+ const base = import_path2.default.basename(name.trim());
529
+ if (!base || base === "." || base === "..") return null;
530
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) return null;
531
+ if (base.includes("..")) return null;
532
+ return base;
533
+ }
534
+ function defaultFilename(lang) {
535
+ if (lang === "dockerfile") return "Dockerfile";
536
+ if (lang === "makefile") return "Makefile";
537
+ const ext = EXT_BY_LANG[lang];
538
+ return ext === void 0 ? null : `snippet.${ext || "txt"}`;
539
+ }
540
+ function extractAllFiles(text) {
541
+ const extracted = extractFilesFromMarkdown(text);
542
+ if (extracted.length) return extracted;
543
+ const fence2 = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]+)[ \t]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
544
+ let best = null;
545
+ let m;
546
+ while ((m = fence2.exec(text)) !== null) {
547
+ const lang = (m[1] || "").toLowerCase();
548
+ if (!EXT_BY_LANG.hasOwnProperty(lang) || lang === "text") continue;
549
+ const content = m[2];
550
+ if (content.trim().split("\n").length < 2) continue;
551
+ if (!best || content.length > best.content.length) {
552
+ best = { filename: defaultFilename(lang) || "snippet.txt", content, lang };
553
+ }
554
+ }
555
+ return best ? [best] : [];
556
+ }
557
+ function writeExtractedFiles(files) {
558
+ const notes = [];
559
+ for (const f of files) {
560
+ const safe = sanitizeFilename(f.filename);
561
+ if (!safe) {
562
+ notes.push(import_chalk.default.red(`\u2715 Skip nama file tidak valid: ${f.filename}`));
563
+ continue;
564
+ }
565
+ try {
566
+ const target = import_path2.default.join(process.cwd(), safe);
567
+ import_fs2.default.writeFileSync(target, f.content, "utf-8");
568
+ const bytes = Buffer.byteLength(f.content, "utf-8");
569
+ notes.push(import_chalk.default.green(`\u2714 File created: ${safe}`) + import_chalk.default.gray(` (${bytes} bytes)`));
570
+ } catch (err) {
571
+ notes.push(import_chalk.default.red(`\u2715 Gagal menulis ${safe}: ${err.message}`));
572
+ }
573
+ }
574
+ return notes;
575
+ }
576
+ var AGENT_SYSTEM_PROMPT = [
577
+ "Anda adalah asisten coding agent SarangAI yang berjalan di terminal pengguna.",
578
+ `Direktori kerja aktif: ${process.cwd()}.`,
579
+ "Jika permintaan pengguna melibatkan pembuatan atau perubahan file, SELALU output kode dalam fenced code block dengan format ````bahasa:nama-file` pada baris pembuka, contoh: ```python:binary_search.py",
580
+ "Gunakan SATU file per code block dan jangan menambah teks setelah baris penutup ``` milik block tersebut.",
581
+ "Untuk file konfigurasi tanpa ekstensi gunakan ```dockerfile:Dockerfile atau ```makefile:Makefile.",
582
+ "Jelaskan secara singkat sebelum/sesudah code block, tanpa membungkus nama file di backtick atau bold."
583
+ ].join("\n");
584
+ var lastTurn = { query: "", text: "", streaming: false, notes: [] };
585
+ var activeAbort = null;
586
+ function buildTranscript(width) {
587
+ const out = [];
588
+ for (let i = 0; i + 1 < conversationHistory.length; i += 2) {
589
+ const q = conversationHistory[i]?.content ?? "";
590
+ const a = conversationHistory[i + 1]?.content ?? "";
591
+ out.push({ text: `\u203A Anda: ${q}`, style: "query" });
592
+ if (a && a !== "(error)") out.push(...wrapWithStyles(a, width));
593
+ out.push({ text: "", style: "plain" });
594
+ }
595
+ return out;
596
+ }
597
+ function drawSessionScreen(turn, opts = { inputMode: "idle" }) {
436
598
  const boxWidth = 72;
437
599
  const termRows = process.stdout.rows || 30;
438
600
  const pad = getCenterPad(boxWidth);
@@ -441,22 +603,31 @@ function drawResponseScreen(query, text, opts) {
441
603
  const lines = [];
442
604
  SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
443
605
  lines.push("");
444
- lines.push(pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${query}`));
445
- lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
446
- const viewRows = Math.max(6, termRows - 13);
447
- let body;
448
- if (opts.error) {
449
- body = wrapWithStyles(opts.error, contentW).map((l) => ({ ...l, style: "error" }));
450
- } else {
451
- body = wrapWithStyles(text, contentW);
606
+ const mm = Math.floor(sessionTimeLeft / 60);
607
+ const ss = sessionTimeLeft % 60;
608
+ lines.push(pad + import_chalk.default.gray(`${conversationHistory.length} pesan \u2022 ${activeModel.name} \u2022 ${mm}m ${ss < 10 ? "0" : ""}${ss}s left`));
609
+ lines.push("");
610
+ const transcript = buildTranscript(contentW);
611
+ if (!turn.streaming) {
612
+ transcript.push({ text: `\u203A Anda: ${turn.query}`, style: "query" });
613
+ if (turn.error) {
614
+ transcript.push(...wrapWithStyles(turn.error, contentW).map((l) => ({ ...l, style: "error" })));
615
+ } else if (turn.text) {
616
+ transcript.push(...wrapWithStyles(turn.text, contentW));
617
+ }
618
+ for (const note of turn.notes) transcript.push({ text: note, style: "plain" });
452
619
  }
453
- respTotalLines = body.length;
620
+ respTotalLines = transcript.length;
621
+ const headerRows = lines.length;
622
+ const footerRows = opts.inputMode === "typing" ? 8 : 4;
623
+ const viewRows = Math.max(4, termRows - headerRows - footerRows);
454
624
  const maxStart = Math.max(0, respTotalLines - viewRows);
455
- const start = Math.min(respScroll ?? maxStart, maxStart);
456
- const visible = body.slice(start, start + viewRows);
625
+ const start = turn.streaming ? maxStart : Math.min(respScroll ?? maxStart, maxStart);
626
+ const visible = transcript.slice(start, start + viewRows);
457
627
  respViewRows = viewRows;
628
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
458
629
  for (const l of visible) {
459
- const styled = styleBodyLine(l);
630
+ const styled = l.style === "query" ? import_chalk.default.bold.hex("#c084fc")(l.text) : styleBodyLine(l);
460
631
  const fill = Math.max(0, contentW - visibleLength(styled));
461
632
  lines.push(pad + import_chalk.default.gray("\u2502 ") + styled + " ".repeat(fill) + import_chalk.default.gray(" \u2502"));
462
633
  }
@@ -464,32 +635,46 @@ function drawResponseScreen(query, text, opts) {
464
635
  lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
465
636
  }
466
637
  lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
467
- lines.push("");
468
- const range = respTotalLines > viewRows ? ` \u2022 baris ${start + 1}-${Math.min(respTotalLines, start + viewRows)}/${respTotalLines}` : ` \u2022 ${respTotalLines} baris`;
469
- const scrollHint = start > 0 || start < maxStart ? import_chalk.default.gray("[\u2191/\u2193 Scroll] ") : "";
470
- const status = opts.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name}]${range}`) : opts.error ? scrollHint + import_chalk.default.red("\u2715 Selesai dengan error \u2022 tekan sembarang tombol untuk kembali") : scrollHint + import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(`${range} \u2022 tekan sembarang tombol untuk kembali`);
471
- lines.push(pad + status);
472
- process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
638
+ if (opts.inputMode === "typing") {
639
+ lines.push("");
640
+ const scrollTag = start > 0 || start < maxStart ? import_chalk.default.gray(`[\u2191/\u2193/mouse scroll \u2022 ${start + 1}-${Math.min(respTotalLines, start + viewRows)}/${respTotalLines}] `) : "";
641
+ lines.push(pad + scrollTag + import_chalk.default.gray("(/new = sesi baru \u2022 exit = keluar \u2022 Esc = menu)"));
642
+ const textContent = currentTaskInput ? currentTaskInput : import_chalk.default.gray("Ketik follow-up\u2026");
643
+ const visibleLen = currentTaskInput ? currentTaskInput.length : 17;
644
+ const paddingRight = Math.max(0, innerW - 2 - visibleLen);
645
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
646
+ const inputRowNumber = lines.length + 1;
647
+ lines.push(pad + import_chalk.default.gray(`\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`));
648
+ lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
649
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
650
+ const cursorCol = pad.length + 5 + currentTaskInput.length;
651
+ process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
652
+ } else {
653
+ lines.push("");
654
+ const range = start > 0 || start < maxStart ? ` \u2022 ${start + 1}-${Math.min(respTotalLines, start + viewRows)}/${respTotalLines}` : ` \u2022 ${respTotalLines} baris`;
655
+ const status = turn.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name}${range}] Esc = batalkan`) : turn.error ? import_chalk.default.red("\u2715 Error") + import_chalk.default.gray(`${range} \u2022 tekan tombol untuk lanjut`) : import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(`${range} \u2022 ketik follow-up di bawah \u2022 Esc = menu`);
656
+ lines.push(pad + status);
657
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
658
+ }
473
659
  }
474
- function waitResponseDismiss() {
475
- process.stdin.once("keypress", (str, key) => {
476
- if (key?.ctrl && key?.name === "c") {
477
- cleanupAndExit();
478
- return;
479
- }
480
- if (key?.name === "up" || key?.name === "down") {
481
- const maxStart = Math.max(0, respTotalLines - respViewRows);
482
- const current = respScroll ?? maxStart;
483
- respScroll = key.name === "up" ? Math.max(0, current - 3) : Math.min(maxStart, current + 3);
484
- drawResponseScreen(respQuery, respText, { streaming: false, error: respError });
485
- waitResponseDismiss();
486
- return;
487
- }
488
- isExecuting = false;
489
- currentTaskInput = "";
490
- respScroll = null;
491
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
492
- });
660
+ function scrollSession(delta) {
661
+ const maxStart = Math.max(0, respTotalLines - respViewRows);
662
+ const current = respScroll ?? maxStart;
663
+ respScroll = Math.max(0, Math.min(maxStart, current + delta));
664
+ drawSessionScreen(lastTurn, { inputMode: "typing" });
665
+ }
666
+ function redrawInputScreen() {
667
+ if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
668
+ else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
669
+ }
670
+ function finishTurnToInput() {
671
+ refreshUserMeta();
672
+ isExecuting = false;
673
+ sessionScreen = true;
674
+ currentTaskInput = keyBuffer;
675
+ keyBuffer = "";
676
+ respScroll = null;
677
+ drawSessionScreen(lastTurn, { inputMode: "typing" });
493
678
  }
494
679
  async function runPromptStream(query, isRetry = false) {
495
680
  isExecuting = true;
@@ -498,15 +683,18 @@ async function runPromptStream(query, isRetry = false) {
498
683
  respText = "";
499
684
  respError = void 0;
500
685
  respScroll = null;
686
+ activeAbort = new AbortController();
501
687
  process.stdout.write("\x1B[?25l");
688
+ lastTurn = { query, text: "", streaming: true, notes: [] };
689
+ drawSessionScreen(lastTurn);
502
690
  let lastPaint = 0;
503
691
  const repaint = (streaming, force = false) => {
504
692
  const now = Date.now();
505
693
  if (!force && now - lastPaint < 80) return;
506
694
  lastPaint = now;
507
- drawResponseScreen(respQuery, respText, { streaming, error: respError });
695
+ lastTurn = { query, text: respText, streaming, error: respError, notes: lastTurn.notes };
696
+ drawSessionScreen(lastTurn);
508
697
  };
509
- repaint(true, true);
510
698
  try {
511
699
  const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
512
700
  method: "POST",
@@ -514,9 +702,13 @@ async function runPromptStream(query, isRetry = false) {
514
702
  "Content-Type": "application/json",
515
703
  Authorization: `Bearer ${cfg.apiKey}`
516
704
  },
705
+ signal: activeAbort.signal,
517
706
  body: JSON.stringify({
518
707
  model: activeModel.id,
519
- messages: [{ role: "user", content: query }],
708
+ messages: [
709
+ { role: "system", content: AGENT_SYSTEM_PROMPT },
710
+ ...conversationHistory
711
+ ],
520
712
  stream: true
521
713
  })
522
714
  });
@@ -541,7 +733,7 @@ async function runPromptStream(query, isRetry = false) {
541
733
  const newKey = await handleAutoAuth(baseUrl);
542
734
  if (newKey) {
543
735
  cfg = getConfig();
544
- userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
736
+ userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "") ?? userMeta;
545
737
  return runPromptStream(query, true);
546
738
  }
547
739
  respError = "Otorisasi ulang dibatalkan.";
@@ -576,11 +768,25 @@ async function runPromptStream(query, isRetry = false) {
576
768
  }
577
769
  }
578
770
  } catch (err) {
579
- respError = err.message;
580
- repaint(false, true);
771
+ if (err?.name !== "AbortError") {
772
+ respError = err.message;
773
+ repaint(false, true);
774
+ } else {
775
+ respText += "\n[ dibatalkan oleh pengguna ]";
776
+ }
777
+ }
778
+ let notes = [];
779
+ if (respText && !respError) {
780
+ notes = writeExtractedFiles(extractAllFiles(respText));
581
781
  }
582
782
  respStreaming = false;
583
- waitResponseDismiss();
783
+ lastTurn = { query, text: respText, streaming: false, error: respError, notes };
784
+ if (respError && !isRetry) {
785
+ conversationHistory.push({ role: "assistant", content: respText || "(error)" });
786
+ } else if (respText) {
787
+ conversationHistory.push({ role: "assistant", content: respText });
788
+ }
789
+ finishTurnToInput();
584
790
  }
585
791
  async function handleAutoAuth(baseUrl2) {
586
792
  const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
@@ -630,7 +836,7 @@ async function startInteractiveSession() {
630
836
  }
631
837
  userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
632
838
  activeModel = CODING_MODELS.find((m) => m.id === cfg.defaultModel) || CODING_MODELS[0];
633
- process.stdout.write("\x1B[?1049h\x1B[2J\x1B[3J\x1B[H");
839
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[3J\x1B[H\x1B[?1000h\x1B[?1006h");
634
840
  import_readline.default.emitKeypressEvents(process.stdin);
635
841
  if (process.stdin.isTTY) {
636
842
  process.stdin.setRawMode(true);
@@ -639,14 +845,34 @@ async function startInteractiveSession() {
639
845
  process.stdout.on("resize", () => {
640
846
  if (inWorkspace) {
641
847
  if (isExecuting) {
642
- drawResponseScreen(respQuery, respText, { streaming: respStreaming, error: respError });
848
+ drawSessionScreen(lastTurn);
643
849
  } else {
644
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
850
+ redrawInputScreen();
645
851
  }
646
852
  } else {
647
853
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
648
854
  }
649
855
  });
856
+ process.stdin.on("data", (buf) => {
857
+ const s = buf.toString("utf8");
858
+ if (!inWorkspace) return;
859
+ let delta = 0;
860
+ const m = s.match(/^\x1b\[(?:M([\s\S]{3})|<(?:0|64|65);(\d+);(\d+)[Mm])/);
861
+ if (m) {
862
+ if (m[1]) {
863
+ const b = m[1].charCodeAt(0) - 32;
864
+ delta = b === 64 ? -3 : b === 65 ? 3 : 0;
865
+ } else {
866
+ delta = s.includes("<64") ? -3 : s.includes("<65") ? 3 : 0;
867
+ }
868
+ } else if (s === "\x1B[A") delta = -3;
869
+ else if (s === "\x1B[B") delta = 3;
870
+ if (delta !== 0 && !isExecuting && sessionScreen) {
871
+ scrollSession(delta);
872
+ return;
873
+ }
874
+ if (delta !== 0) return;
875
+ });
650
876
  process.stdin.on("keypress", (str, key) => {
651
877
  if (key.ctrl && key.name === "c") {
652
878
  cleanupAndExit();
@@ -654,7 +880,55 @@ async function startInteractiveSession() {
654
880
  }
655
881
  if (inWorkspace) {
656
882
  if (isExecuting) {
657
- if (key.name === "escape") cleanupAndExit();
883
+ if (key.name === "escape" && activeAbort) {
884
+ activeAbort.abort();
885
+ return;
886
+ }
887
+ if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
888
+ keyBuffer += str;
889
+ }
890
+ return;
891
+ }
892
+ if (sessionScreen) {
893
+ if (key.name === "escape") {
894
+ leaveWorkspace();
895
+ return;
896
+ }
897
+ if (key.name === "up" || key.name === "down") {
898
+ scrollSession(key.name === "up" ? -3 : 3);
899
+ return;
900
+ }
901
+ if (key.name === "return") {
902
+ const query = currentTaskInput.trim();
903
+ if (!query) return;
904
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
905
+ leaveWorkspace();
906
+ return;
907
+ }
908
+ if (query === "/model" || query === "/back") {
909
+ leaveWorkspace();
910
+ return;
911
+ }
912
+ if (query === "/new" || query === "/reset") {
913
+ conversationHistory = [];
914
+ respLastNotes = [];
915
+ enterWorkspace();
916
+ return;
917
+ }
918
+ conversationHistory.push({ role: "user", content: query });
919
+ runPromptStream(query);
920
+ return;
921
+ }
922
+ if (key.name === "backspace") {
923
+ currentTaskInput = currentTaskInput.slice(0, -1);
924
+ redrawInputScreen();
925
+ return;
926
+ }
927
+ if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
928
+ currentTaskInput += str;
929
+ redrawInputScreen();
930
+ return;
931
+ }
658
932
  return;
659
933
  }
660
934
  if (key.name === "escape") {
@@ -664,14 +938,21 @@ async function startInteractiveSession() {
664
938
  if (key.name === "return") {
665
939
  const query = currentTaskInput.trim();
666
940
  if (!query) return;
667
- if (query === "/exit" || query === "exit" || query === ":q") {
668
- cleanupAndExit();
941
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
942
+ leaveWorkspace();
669
943
  return;
670
944
  }
671
945
  if (query === "/model" || query === "/back") {
672
946
  leaveWorkspace();
673
947
  return;
674
948
  }
949
+ if (query === "/new" || query === "/reset") {
950
+ conversationHistory = [];
951
+ respLastNotes = [];
952
+ enterWorkspace();
953
+ return;
954
+ }
955
+ conversationHistory.push({ role: "user", content: query });
675
956
  runPromptStream(query);
676
957
  return;
677
958
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
5
5
  "main": "dist/index.js",
6
6
  "bin": {