sarangai-cli 1.0.12 → 1.0.13

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 +257 -44
  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.13",
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
@@ -359,7 +361,9 @@ var respError;
359
361
  var respStreaming = false;
360
362
  var respScroll = null;
361
363
  var respTotalLines = 0;
362
- var respViewRows = 12;
364
+ var conversationHistory = [];
365
+ var sessionScreen = false;
366
+ var respLastNotes = [];
363
367
  function cleanupAndExit() {
364
368
  if (timerInterval) clearInterval(timerInterval);
365
369
  process.stdout.write("\x1B[?1049l\x1B[?25h\n");
@@ -373,8 +377,10 @@ function triggerCopyLink() {
373
377
  function leaveWorkspace() {
374
378
  inWorkspace = false;
375
379
  isExecuting = false;
380
+ sessionScreen = false;
376
381
  if (timerInterval) clearInterval(timerInterval);
377
382
  currentTaskInput = "";
383
+ conversationHistory = [];
378
384
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
379
385
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
380
386
  }
@@ -432,7 +438,130 @@ function styleBodyLine(l) {
432
438
  if (l.style === "error") return import_chalk.default.red(l.text);
433
439
  return import_chalk.default.hex("#e2e8f0")(l.text);
434
440
  }
435
- function drawResponseScreen(query, text, opts) {
441
+ var EXT_BY_LANG = {
442
+ python: "py",
443
+ py: "py",
444
+ javascript: "js",
445
+ js: "js",
446
+ typescript: "ts",
447
+ ts: "ts",
448
+ tsx: "tsx",
449
+ jsx: "jsx",
450
+ go: "go",
451
+ rust: "rs",
452
+ rs: "rs",
453
+ java: "java",
454
+ c: "c",
455
+ cpp: "cpp",
456
+ "c++": "cpp",
457
+ csharp: "cs",
458
+ "c#": "cs",
459
+ cs: "cs",
460
+ php: "php",
461
+ ruby: "rb",
462
+ rb: "rb",
463
+ swift: "swift",
464
+ kotlin: "kt",
465
+ kt: "kt",
466
+ bash: "sh",
467
+ sh: "sh",
468
+ shell: "sh",
469
+ zsh: "sh",
470
+ powershell: "ps1",
471
+ ps1: "ps1",
472
+ html: "html",
473
+ css: "css",
474
+ scss: "scss",
475
+ sass: "sass",
476
+ less: "less",
477
+ sql: "sql",
478
+ json: "json",
479
+ yaml: "yaml",
480
+ yml: "yml",
481
+ toml: "toml",
482
+ xml: "xml",
483
+ markdown: "md",
484
+ md: "md",
485
+ dockerfile: "",
486
+ makefile: "",
487
+ ini: "ini",
488
+ env: "",
489
+ txt: "txt",
490
+ text: "txt",
491
+ csv: "csv"
492
+ };
493
+ function extractFilesFromMarkdown(text) {
494
+ const files = [];
495
+ const fence = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]*)(?::[ \t]*([^\n`]+))?[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
496
+ let m;
497
+ while ((m = fence.exec(text)) !== null) {
498
+ const lang = (m[1] || "").toLowerCase().trim();
499
+ const fname = (m[2] || "").trim().replace(/^["']|["']$/g, "");
500
+ if (!fname) continue;
501
+ files.push({ filename: fname, content: m[3], lang: lang || "text" });
502
+ }
503
+ return files;
504
+ }
505
+ function sanitizeFilename(name) {
506
+ const base = import_path2.default.basename(name.trim());
507
+ if (!base || base === "." || base === "..") return null;
508
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) return null;
509
+ if (base.includes("..")) return null;
510
+ return base;
511
+ }
512
+ function defaultFilename(lang) {
513
+ if (lang === "dockerfile") return "Dockerfile";
514
+ if (lang === "makefile") return "Makefile";
515
+ const ext = EXT_BY_LANG[lang];
516
+ return ext === void 0 ? null : `snippet.${ext || "txt"}`;
517
+ }
518
+ function extractAllFiles(text) {
519
+ const extracted = extractFilesFromMarkdown(text);
520
+ if (extracted.length) return extracted;
521
+ const fence2 = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]+)[ \t]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
522
+ let best = null;
523
+ let m;
524
+ while ((m = fence2.exec(text)) !== null) {
525
+ const lang = (m[1] || "").toLowerCase();
526
+ if (!EXT_BY_LANG.hasOwnProperty(lang) || lang === "text") continue;
527
+ const content = m[2];
528
+ if (content.trim().split("\n").length < 2) continue;
529
+ if (!best || content.length > best.content.length) {
530
+ best = { filename: defaultFilename(lang) || "snippet.txt", content, lang };
531
+ }
532
+ }
533
+ return best ? [best] : [];
534
+ }
535
+ function writeExtractedFiles(files) {
536
+ const notes = [];
537
+ for (const f of files) {
538
+ const safe = sanitizeFilename(f.filename);
539
+ if (!safe) {
540
+ notes.push(import_chalk.default.red(`\u2715 Skip nama file tidak valid: ${f.filename}`));
541
+ continue;
542
+ }
543
+ try {
544
+ const target = import_path2.default.join(process.cwd(), safe);
545
+ import_fs2.default.writeFileSync(target, f.content, "utf-8");
546
+ const bytes = Buffer.byteLength(f.content, "utf-8");
547
+ notes.push(import_chalk.default.green(`\u2714 File created: ${safe}`) + import_chalk.default.gray(` (${bytes} bytes)`));
548
+ } catch (err) {
549
+ notes.push(import_chalk.default.red(`\u2715 Gagal menulis ${safe}: ${err.message}`));
550
+ }
551
+ }
552
+ return notes;
553
+ }
554
+ var AGENT_SYSTEM_PROMPT = [
555
+ "Anda adalah asisten coding agent SarangAI yang berjalan di terminal pengguna.",
556
+ `Direktori kerja aktif: ${process.cwd()}.`,
557
+ "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",
558
+ "Gunakan SATU file per code block dan jangan menambah teks setelah baris penutup ``` milik block tersebut.",
559
+ "Untuk file konfigurasi tanpa ekstensi gunakan ```dockerfile:Dockerfile atau ```makefile:Makefile.",
560
+ "Jelaskan secara singkat sebelum/sesudah code block, tanpa membungkus nama file di backtick atau bold."
561
+ ].join("\n");
562
+ var lastTurn = { query: "", text: "", streaming: false, notes: [] };
563
+ var activeAbort = null;
564
+ function drawSessionScreen(turn, opts = { inputMode: "idle" }) {
436
565
  const boxWidth = 72;
437
566
  const termRows = process.stdout.rows || 30;
438
567
  const pad = getCenterPad(boxWidth);
@@ -441,20 +570,19 @@ function drawResponseScreen(query, text, opts) {
441
570
  const lines = [];
442
571
  SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
443
572
  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);
452
- }
573
+ const mm = Math.floor(sessionTimeLeft / 60);
574
+ const ss = sessionTimeLeft % 60;
575
+ lines.push(pad + import_chalk.default.gray(`${conversationHistory.length} pesan \u2022 ${activeModel.name} \u2022 ${mm}m ${ss < 10 ? "0" : ""}${ss}s left`));
576
+ lines.push("");
577
+ const qLabel = turn.query.length > 64 ? turn.query.slice(0, 61) + "\u2026" : turn.query;
578
+ lines.push(pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${qLabel}`));
579
+ const headerRows = lines.length + 1;
580
+ const footerRows = opts.inputMode === "typing" ? 7 : 3;
581
+ const viewRows = Math.max(4, termRows - headerRows - footerRows);
582
+ const body = turn.error ? wrapWithStyles(turn.error, contentW).map((l) => ({ ...l, style: "error" })) : wrapWithStyles(turn.text, contentW);
453
583
  respTotalLines = body.length;
454
- const maxStart = Math.max(0, respTotalLines - viewRows);
455
- const start = Math.min(respScroll ?? maxStart, maxStart);
456
- const visible = body.slice(start, start + viewRows);
457
- respViewRows = viewRows;
584
+ const visible = turn.streaming ? body.slice(0, viewRows) : body.slice(Math.max(0, body.length - viewRows));
585
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
458
586
  for (const l of visible) {
459
587
  const styled = styleBodyLine(l);
460
588
  const fill = Math.max(0, contentW - visibleLength(styled));
@@ -464,31 +592,47 @@ function drawResponseScreen(query, text, opts) {
464
592
  lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
465
593
  }
466
594
  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"));
595
+ for (const note of turn.notes) lines.push(pad + " " + note);
596
+ if (opts.inputMode === "typing") {
597
+ lines.push("");
598
+ const textContent = currentTaskInput ? currentTaskInput : import_chalk.default.gray("Ketik follow-up\u2026 (/new = sesi baru, exit = keluar)");
599
+ const visibleLen = currentTaskInput ? currentTaskInput.length : 55;
600
+ const paddingRight = Math.max(0, innerW - 2 - visibleLen);
601
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
602
+ const inputRowNumber = lines.length + 1;
603
+ lines.push(pad + import_chalk.default.gray(`\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`));
604
+ lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
605
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
606
+ const cursorCol = pad.length + 5 + currentTaskInput.length;
607
+ process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
608
+ } else {
609
+ lines.push("");
610
+ const status = turn.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name} \u2022 ${respTotalLines} baris] Esc = batalkan`) : turn.error ? import_chalk.default.red("\u2715 Error") + import_chalk.default.gray(" \u2022 tekan tombol untuk lanjut") : import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(` \u2022 ${respTotalLines} baris \u2022 ketik follow-up di bawah \u2022 Esc = menu`);
611
+ lines.push(pad + status);
612
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
613
+ }
614
+ }
615
+ function enterSessionInput() {
616
+ currentTaskInput = "";
617
+ sessionScreen = true;
618
+ drawSessionScreen(lastTurn, { inputMode: "typing" });
619
+ }
620
+ function redrawInputScreen() {
621
+ if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
622
+ else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
473
623
  }
474
- function waitResponseDismiss() {
624
+ function waitSessionKeypress() {
475
625
  process.stdin.once("keypress", (str, key) => {
476
626
  if (key?.ctrl && key?.name === "c") {
477
627
  cleanupAndExit();
478
628
  return;
479
629
  }
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();
630
+ isExecuting = false;
631
+ if (key?.name === "escape") {
632
+ leaveWorkspace();
486
633
  return;
487
634
  }
488
- isExecuting = false;
489
- currentTaskInput = "";
490
- respScroll = null;
491
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
635
+ enterSessionInput();
492
636
  });
493
637
  }
494
638
  async function runPromptStream(query, isRetry = false) {
@@ -498,15 +642,18 @@ async function runPromptStream(query, isRetry = false) {
498
642
  respText = "";
499
643
  respError = void 0;
500
644
  respScroll = null;
645
+ activeAbort = new AbortController();
501
646
  process.stdout.write("\x1B[?25l");
647
+ lastTurn = { query, text: "", streaming: true, notes: [] };
648
+ drawSessionScreen(lastTurn);
502
649
  let lastPaint = 0;
503
650
  const repaint = (streaming, force = false) => {
504
651
  const now = Date.now();
505
652
  if (!force && now - lastPaint < 80) return;
506
653
  lastPaint = now;
507
- drawResponseScreen(respQuery, respText, { streaming, error: respError });
654
+ lastTurn = { query, text: respText, streaming, error: respError, notes: lastTurn.notes };
655
+ drawSessionScreen(lastTurn);
508
656
  };
509
- repaint(true, true);
510
657
  try {
511
658
  const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
512
659
  method: "POST",
@@ -514,9 +661,13 @@ async function runPromptStream(query, isRetry = false) {
514
661
  "Content-Type": "application/json",
515
662
  Authorization: `Bearer ${cfg.apiKey}`
516
663
  },
664
+ signal: activeAbort.signal,
517
665
  body: JSON.stringify({
518
666
  model: activeModel.id,
519
- messages: [{ role: "user", content: query }],
667
+ messages: [
668
+ { role: "system", content: AGENT_SYSTEM_PROMPT },
669
+ ...conversationHistory
670
+ ],
520
671
  stream: true
521
672
  })
522
673
  });
@@ -576,11 +727,26 @@ async function runPromptStream(query, isRetry = false) {
576
727
  }
577
728
  }
578
729
  } catch (err) {
579
- respError = err.message;
580
- repaint(false, true);
730
+ if (err?.name !== "AbortError") {
731
+ respError = err.message;
732
+ repaint(false, true);
733
+ } else {
734
+ respText += "\n[ dibatalkan oleh pengguna ]";
735
+ }
736
+ }
737
+ let notes = [];
738
+ if (respText && !respError) {
739
+ notes = writeExtractedFiles(extractAllFiles(respText));
581
740
  }
582
741
  respStreaming = false;
583
- waitResponseDismiss();
742
+ lastTurn = { query, text: respText, streaming: false, error: respError, notes };
743
+ drawSessionScreen(lastTurn);
744
+ if (respError && !isRetry) {
745
+ conversationHistory.push({ role: "assistant", content: respText || "(error)" });
746
+ } else if (respText) {
747
+ conversationHistory.push({ role: "assistant", content: respText });
748
+ }
749
+ waitSessionKeypress();
584
750
  }
585
751
  async function handleAutoAuth(baseUrl2) {
586
752
  const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
@@ -639,9 +805,9 @@ async function startInteractiveSession() {
639
805
  process.stdout.on("resize", () => {
640
806
  if (inWorkspace) {
641
807
  if (isExecuting) {
642
- drawResponseScreen(respQuery, respText, { streaming: respStreaming, error: respError });
808
+ drawSessionScreen(lastTurn);
643
809
  } else {
644
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
810
+ redrawInputScreen();
645
811
  }
646
812
  } else {
647
813
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
@@ -654,7 +820,47 @@ async function startInteractiveSession() {
654
820
  }
655
821
  if (inWorkspace) {
656
822
  if (isExecuting) {
657
- if (key.name === "escape") cleanupAndExit();
823
+ if (key.name === "escape" && activeAbort) {
824
+ activeAbort.abort();
825
+ }
826
+ return;
827
+ }
828
+ if (sessionScreen) {
829
+ if (key.name === "escape") {
830
+ leaveWorkspace();
831
+ return;
832
+ }
833
+ if (key.name === "return") {
834
+ const query = currentTaskInput.trim();
835
+ if (!query) return;
836
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
837
+ leaveWorkspace();
838
+ return;
839
+ }
840
+ if (query === "/model" || query === "/back") {
841
+ leaveWorkspace();
842
+ return;
843
+ }
844
+ if (query === "/new" || query === "/reset") {
845
+ conversationHistory = [];
846
+ respLastNotes = [];
847
+ enterWorkspace();
848
+ return;
849
+ }
850
+ conversationHistory.push({ role: "user", content: query });
851
+ runPromptStream(query);
852
+ return;
853
+ }
854
+ if (key.name === "backspace") {
855
+ currentTaskInput = currentTaskInput.slice(0, -1);
856
+ redrawInputScreen();
857
+ return;
858
+ }
859
+ if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
860
+ currentTaskInput += str;
861
+ redrawInputScreen();
862
+ return;
863
+ }
658
864
  return;
659
865
  }
660
866
  if (key.name === "escape") {
@@ -664,14 +870,21 @@ async function startInteractiveSession() {
664
870
  if (key.name === "return") {
665
871
  const query = currentTaskInput.trim();
666
872
  if (!query) return;
667
- if (query === "/exit" || query === "exit" || query === ":q") {
668
- cleanupAndExit();
873
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
874
+ leaveWorkspace();
669
875
  return;
670
876
  }
671
877
  if (query === "/model" || query === "/back") {
672
878
  leaveWorkspace();
673
879
  return;
674
880
  }
881
+ if (query === "/new" || query === "/reset") {
882
+ conversationHistory = [];
883
+ respLastNotes = [];
884
+ enterWorkspace();
885
+ return;
886
+ }
887
+ conversationHistory.push({ role: "user", content: query });
675
888
  runPromptStream(query);
676
889
  return;
677
890
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
5
5
  "main": "dist/index.js",
6
6
  "bin": {