sarangai-cli 1.0.11 → 1.0.12

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 +154 -26
  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.11",
38
+ version: "1.0.12",
39
39
  description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
40
40
  main: "dist/index.js",
41
41
  bin: {
@@ -124,6 +124,14 @@ function saveConfig(newConfig) {
124
124
 
125
125
  // src/index.ts
126
126
  var program = new import_commander.Command();
127
+ var SARANG_BANNER = [
128
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
129
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551",
130
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551",
131
+ "\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551",
132
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551",
133
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D"
134
+ ];
127
135
  var borderActive = import_chalk.default.hex("#a855f7");
128
136
  var borderMuted = import_chalk.default.hex("#334155");
129
137
  var violetText = import_chalk.default.hex("#c084fc");
@@ -345,6 +353,13 @@ var timerInterval = null;
345
353
  var currentTaskInput = "";
346
354
  var cfg;
347
355
  var baseUrl;
356
+ var respQuery = "";
357
+ var respText = "";
358
+ var respError;
359
+ var respStreaming = false;
360
+ var respScroll = null;
361
+ var respTotalLines = 0;
362
+ var respViewRows = 12;
348
363
  function cleanupAndExit() {
349
364
  if (timerInterval) clearInterval(timerInterval);
350
365
  process.stdout.write("\x1B[?1049l\x1B[?25h\n");
@@ -378,13 +393,120 @@ function enterWorkspace() {
378
393
  }
379
394
  }, 1e3);
380
395
  }
396
+ function visibleLength(s) {
397
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
398
+ }
399
+ function wrapWithStyles(input, width) {
400
+ const out = [];
401
+ let inCode = false;
402
+ const text = input.replace(/\r\n?/g, "\n");
403
+ for (const raw of text.split("\n")) {
404
+ const trimmed = raw.trimStart();
405
+ let style = "plain";
406
+ if (trimmed.startsWith("```")) {
407
+ inCode = !inCode;
408
+ style = "code";
409
+ } else if (inCode) {
410
+ style = "code";
411
+ } else if (trimmed.startsWith("#")) {
412
+ style = "heading";
413
+ }
414
+ if (raw === "") {
415
+ out.push({ text: "", style });
416
+ continue;
417
+ }
418
+ let remaining = raw;
419
+ while (remaining.length > width) {
420
+ let cut = remaining.lastIndexOf(" ", width);
421
+ if (cut < Math.floor(width * 0.5)) cut = width;
422
+ out.push({ text: remaining.slice(0, cut), style });
423
+ remaining = remaining.slice(cut).replace(/^ /, "");
424
+ }
425
+ out.push({ text: remaining, style });
426
+ }
427
+ return out;
428
+ }
429
+ function styleBodyLine(l) {
430
+ if (l.style === "heading") return import_chalk.default.bold.hex("#c084fc")(l.text);
431
+ if (l.style === "code") return import_chalk.default.hex("#94a3b8")(l.text);
432
+ if (l.style === "error") return import_chalk.default.red(l.text);
433
+ return import_chalk.default.hex("#e2e8f0")(l.text);
434
+ }
435
+ function drawResponseScreen(query, text, opts) {
436
+ const boxWidth = 72;
437
+ const termRows = process.stdout.rows || 30;
438
+ const pad = getCenterPad(boxWidth);
439
+ const innerW = boxWidth - 2;
440
+ const contentW = innerW - 4;
441
+ const lines = [];
442
+ SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
443
+ 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
+ }
453
+ 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;
458
+ for (const l of visible) {
459
+ const styled = styleBodyLine(l);
460
+ const fill = Math.max(0, contentW - visibleLength(styled));
461
+ lines.push(pad + import_chalk.default.gray("\u2502 ") + styled + " ".repeat(fill) + import_chalk.default.gray(" \u2502"));
462
+ }
463
+ for (let i = visible.length; i < viewRows; i++) {
464
+ lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
465
+ }
466
+ 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"));
473
+ }
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
+ });
493
+ }
381
494
  async function runPromptStream(query, isRetry = false) {
382
495
  isExecuting = true;
496
+ respStreaming = true;
497
+ respQuery = query;
498
+ respText = "";
499
+ respError = void 0;
500
+ respScroll = null;
383
501
  process.stdout.write("\x1B[?25l");
384
- const pad = getCenterPad(72);
385
- console.log("\n\n" + pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${query}`));
386
- console.log(pad + import_chalk.default.gray(`[Generating with ${activeModel.name}...]
387
- `));
502
+ let lastPaint = 0;
503
+ const repaint = (streaming, force = false) => {
504
+ const now = Date.now();
505
+ if (!force && now - lastPaint < 80) return;
506
+ lastPaint = now;
507
+ drawResponseScreen(respQuery, respText, { streaming, error: respError });
508
+ };
509
+ repaint(true, true);
388
510
  try {
389
511
  const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
390
512
  method: "POST",
@@ -406,11 +528,11 @@ async function runPromptStream(query, isRetry = false) {
406
528
  if (msg) detail = typeof msg === "string" ? msg : JSON.stringify(msg);
407
529
  } catch {
408
530
  }
409
- console.log(pad + import_chalk.default.red(`
410
- Error (${res.status}): ${detail}
411
- `));
531
+ respError = `(${res.status}) ${detail}`;
532
+ repaint(false, true);
412
533
  if (res.status === 401 && !isRetry) {
413
- console.log(pad + import_chalk.default.yellow("API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...\n"));
534
+ respError = "API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...";
535
+ repaint(false, true);
414
536
  try {
415
537
  saveConfig({ apiKey: void 0 });
416
538
  } catch {
@@ -422,42 +544,43 @@ Error (${res.status}): ${detail}
422
544
  userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
423
545
  return runPromptStream(query, true);
424
546
  }
425
- console.log(pad + import_chalk.default.red("Otorisasi ulang dibatalkan.\n"));
547
+ respError = "Otorisasi ulang dibatalkan.";
548
+ repaint(false, true);
426
549
  }
427
550
  } else {
428
551
  const reader = res.body?.getReader();
429
552
  const decoder = new TextDecoder();
430
553
  if (reader) {
554
+ let sseBuffer = "";
431
555
  while (true) {
432
556
  const { done, value } = await reader.read();
433
557
  if (done) break;
434
- const chunk = decoder.decode(value);
435
- const linesArr = chunk.split("\n");
436
- for (const l of linesArr) {
558
+ sseBuffer += decoder.decode(value, { stream: true });
559
+ const nlIdx = sseBuffer.lastIndexOf("\n");
560
+ if (nlIdx === -1) continue;
561
+ const complete = sseBuffer.slice(0, nlIdx);
562
+ sseBuffer = sseBuffer.slice(nlIdx + 1);
563
+ for (const l of complete.split("\n")) {
437
564
  if (l.startsWith("data: ") && l !== "data: [DONE]") {
438
565
  try {
439
566
  const p = JSON.parse(l.slice(6));
440
567
  const delta = p.choices?.[0]?.delta?.content;
441
- if (delta) process.stdout.write(delta);
568
+ if (delta) respText += delta;
442
569
  } catch {
443
570
  }
444
571
  }
445
572
  }
573
+ repaint(true);
446
574
  }
447
- console.log("\n");
575
+ repaint(false, true);
448
576
  }
449
577
  }
450
578
  } catch (err) {
451
- console.log(pad + import_chalk.default.red(`
452
- Error: ${err.message}
453
- `));
579
+ respError = err.message;
580
+ repaint(false, true);
454
581
  }
455
- console.log(pad + import_chalk.default.gray("\nTekan sembarang tombol untuk kembali ke workspace..."));
456
- process.stdin.once("keypress", () => {
457
- isExecuting = false;
458
- currentTaskInput = "";
459
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
460
- });
582
+ respStreaming = false;
583
+ waitResponseDismiss();
461
584
  }
462
585
  async function handleAutoAuth(baseUrl2) {
463
586
  const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
@@ -515,7 +638,9 @@ async function startInteractiveSession() {
515
638
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
516
639
  process.stdout.on("resize", () => {
517
640
  if (inWorkspace) {
518
- if (!isExecuting) {
641
+ if (isExecuting) {
642
+ drawResponseScreen(respQuery, respText, { streaming: respStreaming, error: respError });
643
+ } else {
519
644
  drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
520
645
  }
521
646
  } else {
@@ -528,7 +653,10 @@ async function startInteractiveSession() {
528
653
  return;
529
654
  }
530
655
  if (inWorkspace) {
531
- if (isExecuting) return;
656
+ if (isExecuting) {
657
+ if (key.name === "escape") cleanupAndExit();
658
+ return;
659
+ }
532
660
  if (key.name === "escape") {
533
661
  leaveWorkspace();
534
662
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
5
5
  "main": "dist/index.js",
6
6
  "bin": {