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.
- package/dist/index.js +189 -32
- 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.
|
|
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.
|
|
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-
|
|
135
|
-
name: "Claude
|
|
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/
|
|
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 ??
|
|
200
|
+
balance: d.balance ?? 0,
|
|
193
201
|
tier: d.tier || "DEVELOPER",
|
|
194
|
-
accountId: d.accountId
|
|
202
|
+
accountId: d.accountId ? `SA-${d.accountId}` : "SA-\u2014"
|
|
195
203
|
};
|
|
196
204
|
}
|
|
197
205
|
} catch {
|
|
198
206
|
}
|
|
199
|
-
return { balance:
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
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)
|
|
568
|
+
if (delta) respText += delta;
|
|
420
569
|
} catch {
|
|
421
570
|
}
|
|
422
571
|
}
|
|
423
572
|
}
|
|
573
|
+
repaint(true);
|
|
424
574
|
}
|
|
425
|
-
|
|
575
|
+
repaint(false, true);
|
|
426
576
|
}
|
|
427
577
|
}
|
|
428
578
|
} catch (err) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
`));
|
|
579
|
+
respError = err.message;
|
|
580
|
+
repaint(false, true);
|
|
432
581
|
}
|
|
433
|
-
|
|
434
|
-
|
|
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 (
|
|
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)
|
|
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.
|
|
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.
|
|
8
|
+
"sarangai": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|