sarangai-cli 1.0.10 → 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 +189 -32
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -35,12 +35,12 @@ var require_package = __commonJS({
35
35
  "package.json"(exports2, module2) {
36
36
  module2.exports = {
37
37
  name: "sarangai-cli",
38
- version: "1.0.10",
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: {
42
42
  sarang: "dist/index.js",
43
- sarangai: "dist/index.cjs"
43
+ sarangai: "dist/index.js"
44
44
  },
45
45
  files: [
46
46
  "dist"
@@ -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");
@@ -131,8 +139,8 @@ var greenDot = import_chalk.default.hex("#4ade80");
131
139
  var grayMuted = import_chalk.default.hex("#94a3b8");
132
140
  var CODING_MODELS = [
133
141
  {
134
- id: "anthropic/claude-3.7-sonnet",
135
- name: "Claude 3.7 Sonnet",
142
+ id: "anthropic/claude-sonnet-4.6",
143
+ name: "Claude Sonnet 4.6",
136
144
  desc: "Anthropic \u2022 Best coding & agentic workflow",
137
145
  rate: "12,000 Credits/hr"
138
146
  },
@@ -183,20 +191,20 @@ function copyToClipboard(text) {
183
191
  }
184
192
  async function fetchUserMeta(baseUrl2, apiKey) {
185
193
  try {
186
- const res = await fetch(`${baseUrl2}/api/user/stats`, {
194
+ const res = await fetch(`${baseUrl2}/api/gateway/v1/balance`, {
187
195
  headers: { Authorization: `Bearer ${apiKey}` }
188
196
  });
189
197
  if (res.ok) {
190
198
  const d = await res.json();
191
199
  return {
192
- balance: d.balance ?? 75313,
200
+ balance: d.balance ?? 0,
193
201
  tier: d.tier || "DEVELOPER",
194
- accountId: d.accountId || "SA-8WwREVSJ"
202
+ accountId: d.accountId ? `SA-${d.accountId}` : "SA-\u2014"
195
203
  };
196
204
  }
197
205
  } catch {
198
206
  }
199
- return { balance: 75313, tier: "DEVELOPER", accountId: "SA-8WwREVSJ" };
207
+ return { balance: 0, tier: "DEVELOPER", accountId: "SA-\u2014" };
200
208
  }
201
209
  function getDisplayDir() {
202
210
  const cwd = process.cwd();
@@ -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
  }
381
- async function runPromptStream(query) {
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
+ }
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",
@@ -399,43 +521,66 @@ async function runPromptStream(query) {
399
521
  })
400
522
  });
401
523
  if (!res.ok) {
402
- console.log(pad + import_chalk.default.red(`
403
- Error: ${res.statusText}
404
- `));
524
+ let detail = res.statusText || `HTTP ${res.status}`;
525
+ try {
526
+ const errBody = await res.json();
527
+ const msg = errBody?.error?.message || errBody?.error || errBody?.message;
528
+ if (msg) detail = typeof msg === "string" ? msg : JSON.stringify(msg);
529
+ } catch {
530
+ }
531
+ respError = `(${res.status}) ${detail}`;
532
+ repaint(false, true);
533
+ if (res.status === 401 && !isRetry) {
534
+ respError = "API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...";
535
+ repaint(false, true);
536
+ try {
537
+ saveConfig({ apiKey: void 0 });
538
+ } catch {
539
+ }
540
+ cfg = getConfig();
541
+ const newKey = await handleAutoAuth(baseUrl);
542
+ if (newKey) {
543
+ cfg = getConfig();
544
+ userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
545
+ return runPromptStream(query, true);
546
+ }
547
+ respError = "Otorisasi ulang dibatalkan.";
548
+ repaint(false, true);
549
+ }
405
550
  } else {
406
551
  const reader = res.body?.getReader();
407
552
  const decoder = new TextDecoder();
408
553
  if (reader) {
554
+ let sseBuffer = "";
409
555
  while (true) {
410
556
  const { done, value } = await reader.read();
411
557
  if (done) break;
412
- const chunk = decoder.decode(value);
413
- const linesArr = chunk.split("\n");
414
- 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")) {
415
564
  if (l.startsWith("data: ") && l !== "data: [DONE]") {
416
565
  try {
417
566
  const p = JSON.parse(l.slice(6));
418
567
  const delta = p.choices?.[0]?.delta?.content;
419
- if (delta) process.stdout.write(delta);
568
+ if (delta) respText += delta;
420
569
  } catch {
421
570
  }
422
571
  }
423
572
  }
573
+ repaint(true);
424
574
  }
425
- console.log("\n");
575
+ repaint(false, true);
426
576
  }
427
577
  }
428
578
  } catch (err) {
429
- console.log(pad + import_chalk.default.red(`
430
- Error: ${err.message}
431
- `));
579
+ respError = err.message;
580
+ repaint(false, true);
432
581
  }
433
- console.log(pad + import_chalk.default.gray("\nTekan sembarang tombol untuk kembali ke workspace..."));
434
- process.stdin.once("keypress", () => {
435
- isExecuting = false;
436
- currentTaskInput = "";
437
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
438
- });
582
+ respStreaming = false;
583
+ waitResponseDismiss();
439
584
  }
440
585
  async function handleAutoAuth(baseUrl2) {
441
586
  const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
@@ -468,6 +613,13 @@ async function handleAutoAuth(baseUrl2) {
468
613
  async function startInteractiveSession() {
469
614
  cfg = getConfig();
470
615
  baseUrl = resolveBaseUrl();
616
+ const MODEL_MIGRATIONS = {
617
+ "anthropic/claude-3.7-sonnet": "anthropic/claude-sonnet-4.6"
618
+ };
619
+ if (cfg.defaultModel && MODEL_MIGRATIONS[cfg.defaultModel]) {
620
+ cfg.defaultModel = MODEL_MIGRATIONS[cfg.defaultModel];
621
+ saveConfig({ defaultModel: cfg.defaultModel });
622
+ }
471
623
  if (!cfg.apiKey) {
472
624
  const key = await handleAutoAuth(baseUrl);
473
625
  if (!key) {
@@ -486,7 +638,9 @@ async function startInteractiveSession() {
486
638
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
487
639
  process.stdout.on("resize", () => {
488
640
  if (inWorkspace) {
489
- if (!isExecuting) {
641
+ if (isExecuting) {
642
+ drawResponseScreen(respQuery, respText, { streaming: respStreaming, error: respError });
643
+ } else {
490
644
  drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
491
645
  }
492
646
  } else {
@@ -499,7 +653,10 @@ async function startInteractiveSession() {
499
653
  return;
500
654
  }
501
655
  if (inWorkspace) {
502
- if (isExecuting) return;
656
+ if (isExecuting) {
657
+ if (key.name === "escape") cleanupAndExit();
658
+ return;
659
+ }
503
660
  if (key.name === "escape") {
504
661
  leaveWorkspace();
505
662
  return;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.10",
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": {
7
7
  "sarang": "dist/index.js",
8
- "sarangai": "dist/index.cjs"
8
+ "sarangai": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist"