sarangai-cli 1.0.8 → 1.0.10

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/README.md CHANGED
@@ -1,104 +1,68 @@
1
1
  # SarangAI CLI
2
2
 
3
- Akses 400+ model AI unggulan (DeepSeek, Claude, GPT, Qwen, Minimax, dsb.) langsung dari terminal Anda melalui SarangAI Gateway.
3
+ Gateway AI Coding Workspace interaktif langsung dari terminal Anda, didukung oleh model-model reasoning & coding terbaik dunia (Anthropic, OpenAI, DeepSeek, Alibaba, Google, Meta).
4
4
 
5
5
  ---
6
6
 
7
7
  ## Fitur Utama
8
8
 
9
- - **Streaming Teks Cepat**: Respons AI mengalir secara real-time langsung di terminal.
10
- - **Katalog 400+ Model**: Jelajahi dan gunakan ratusan varian model AI populer.
11
- - **Pencarian Cepat**: Filter model berdasarkan nama, provider, atau model gratis.
12
- - **Dukungan Unix Piping**: Teruskan output perintah bash/kode program langsung ke AI.
13
- - **Manajemen Saldo**: Cek status kredit akun langsung lewat terminal.
9
+ - **Interactive Terminal Workspace (TUI)**: Antarmuka terminal interaktif modern berbasis full-screen alternate buffer tanpa flicker.
10
+ - **Top Coding Models (1 Brand = 1 Flagship)**: Dikurasi khusus untuk programming, arsitektur sistem, dan debugging mendalam:
11
+ - Claude 3.7 Sonnet (Anthropic) - Architecture & Agentic coding
12
+ - GPT-4o (OpenAI) - Fast full-stack & complex logic
13
+ - DeepSeek-R1 (DeepSeek) - Deep reasoning & hard algorithms
14
+ - Qwen 2.5 Coder 32B (Alibaba) - Multi-language syntax specialist
15
+ - Gemini 2.5 Flash (Google) - 1M context & full repo analysis
16
+ - Llama 3.3 70B (Meta) - Robust open-source coding engine
17
+ - **Real-Time Streaming**: Output kode dan respons AI mengalir seketika (zero-latency stream).
18
+ - **Auto-Auth via Browser**: Login cepat dan aman tanpa perlu input API Key manual.
19
+ - **Credit & Tier Monitoring**: Pantau sisa saldo credit secara live langsung di dalam terminal.
14
20
 
15
21
  ---
16
22
 
17
23
  ## Instalasi
18
24
 
19
- Instal paket secara global menggunakan npm:
25
+ Pastikan Node.js (>= 18) telah terpasang, lalu instal paket secara global:
20
26
 
21
- ```bash
22
- npm install -g sarangai-cli
23
- ```
27
+ npm install -g sarangai-cli@latest
24
28
 
25
29
  ---
26
30
 
27
- ## Panduan Penggunaan
31
+ ## Memulai Workspace Interaktif
28
32
 
29
- ### 1. Autentikasi
30
- Dapatkan API Key dari dashboard akun SarangAI Anda, lalu hubungkan ke CLI:
33
+ Cukup jalankan satu perintah:
31
34
 
32
- ```bash
33
- sarang login
34
- ```
35
+ sarang
35
36
 
36
- API Key akan disimpan di berkas konfigurasi lokal `~/.sarangairc`.
37
+ Terminal akan otomatis menampilkan modal seleksi model coding, ringkasan saldo akun, dan opsi masuk ke ruang kerja (Workspace).
37
38
 
38
- ### 2. Cek Saldo Kredit
39
- Lihat sisa saldo kredit dan status akun:
39
+ ### Navigasi Keyboard:
40
+ - [↑/↓] (Panah) : Navigasi antar opsi dan pilihan model.
41
+ - [Enter / Space] : Konfirmasi pilihan / Masuk ke Workspace.
42
+ - [c] : Salin tautan dashboard referal ke clipboard.
43
+ - [Esc] : Mengakhiri sesi aktif di Workspace dan kembali ke menu seleksi.
44
+ - [q] : Keluar dari SarangAI CLI.
40
45
 
41
- ```bash
42
- sarang balance
43
- ```
44
-
45
- ### 3. Jelajahi dan Filter Model
46
- Melihat seluruh model yang tersedia:
47
-
48
- ```bash
49
- sarang models
50
- ```
51
-
52
- Menyaring model berdasarkan kata kunci:
53
-
54
- ```bash
55
- sarang models -s deepseek
56
- sarang models -s claude
57
- sarang models -s free
58
- ```
59
-
60
- ### 4. Ganti Default Model
61
- Tentukan model yang selalu digunakan secara otomatis:
62
-
63
- ```bash
64
- sarang set-model minimax/minimax-m2.7
65
- ```
66
-
67
- ### 5. Chat & Streaming
68
- Mengirim prompt langsung:
69
-
70
- ```bash
71
- sarang chat "Jelaskan konsep index di database PostgreSQL."
72
- ```
73
-
74
- Menggunakan model tertentu tanpa mengubah default:
75
-
76
- ```bash
77
- sarang chat -m anthropic/claude-sonnet-4 "Buatkan regex validasi nomor telepon Indonesia."
78
- ```
46
+ ---
79
47
 
80
- ### 6. Integrasi Unix Piping
81
- Membaca input langsung dari terminal atau berkas kode:
48
+ ## Autentikasi Otomatis
82
49
 
83
- ```bash
84
- cat main.py | sarang chat "Review potensi bug dan tingkatkan performa kode ini."
85
- git diff | sarang chat "Buatkan pesan commit yang ringkas berdasarkan perubahan ini."
86
- ```
50
+ Jika pertama kali menggunakan CLI:
51
+ 1. Jalankan `sarang`.
52
+ 2. CLI akan mengenerate tautan otorisasi unik dan otomatis menyalinnya ke clipboard.
53
+ 3. Buka tautan tersebut di browser untuk mengizinkan sesi.
54
+ 4. Terminal akan otomatis mendeteksi sesi dan menyimpan token ke `~/.sarangairc`.
87
55
 
88
56
  ---
89
57
 
90
58
  ## Konfigurasi Gateway
91
59
 
92
- Secara default, CLI membaca konfigurasi dari `~/.sarangairc` atau environment variable `SARANGAI_BASE_URL`.
93
-
94
- Untuk gateway self-hosted atau pengujian lokal:
60
+ Secara default, CLI terhubung ke `[https://idshop.or.id](https://idshop.or.id)`. Jika Anda menggunakan custom gateway atau development server lokal:
95
61
 
96
- ```bash
97
- export SARANGAI_BASE_URL="http://localhost:3001"
98
- ```
62
+ export SARANGAI_BASE_URL="http://localhost:3001"
99
63
 
100
64
  ---
101
65
 
102
66
  ## Lisensi
103
67
 
104
- MIT
68
+ MIT License (c) SarangAI
package/dist/index.js ADDED
@@ -0,0 +1,610 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // package.json
34
+ var require_package = __commonJS({
35
+ "package.json"(exports2, module2) {
36
+ module2.exports = {
37
+ name: "sarangai-cli",
38
+ version: "1.0.10",
39
+ description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
40
+ main: "dist/index.js",
41
+ bin: {
42
+ sarang: "dist/index.js",
43
+ sarangai: "dist/index.cjs"
44
+ },
45
+ files: [
46
+ "dist"
47
+ ],
48
+ scripts: {
49
+ build: "tsup && chmod +x dist/index.js",
50
+ prepublishOnly: "npm run build"
51
+ },
52
+ keywords: [
53
+ "sarangai",
54
+ "cli",
55
+ "ai",
56
+ "llm",
57
+ "openai",
58
+ "deepseek",
59
+ "claude",
60
+ "gateway"
61
+ ],
62
+ author: "SarangAI Team",
63
+ license: "MIT",
64
+ dependencies: {
65
+ chalk: "^4.1.2",
66
+ clipboardy: "^5.3.2",
67
+ commander: "^12.1.0",
68
+ ink: "^6.8.0",
69
+ ora: "^5.4.1",
70
+ prompts: "^2.4.2",
71
+ react: "^19.3.0",
72
+ "react-dom": "^19.3.0",
73
+ "readline-sync": "^1.4.10"
74
+ },
75
+ devDependencies: {
76
+ "@types/clipboardy": "^1.1.0",
77
+ "@types/node": "^20.14.9",
78
+ "@types/prompts": "^2.4.9",
79
+ "@types/react": "^19.3.0",
80
+ "@types/readline-sync": "^1.4.8",
81
+ tsup: "^8.5.1",
82
+ typescript: "^5.5.2"
83
+ }
84
+ };
85
+ }
86
+ });
87
+
88
+ // src/index.ts
89
+ var index_exports = {};
90
+ __export(index_exports, {
91
+ CODING_MODELS: () => CODING_MODELS
92
+ });
93
+ module.exports = __toCommonJS(index_exports);
94
+ var import_commander = require("commander");
95
+ var import_chalk = __toESM(require("chalk"));
96
+ var import_readline = __toESM(require("readline"));
97
+ var import_child_process = require("child_process");
98
+ var import_crypto = __toESM(require("crypto"));
99
+ var import_clipboardy = __toESM(require("clipboardy"));
100
+
101
+ // src/config.ts
102
+ var import_fs = __toESM(require("fs"));
103
+ var import_path = __toESM(require("path"));
104
+ var import_os = __toESM(require("os"));
105
+ var CONFIG_FILE = import_path.default.join(import_os.default.homedir(), ".sarangairc");
106
+ function getConfig() {
107
+ try {
108
+ if (import_fs.default.existsSync(CONFIG_FILE)) {
109
+ const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
110
+ return JSON.parse(raw);
111
+ }
112
+ } catch {
113
+ }
114
+ return {
115
+ baseUrl: "https://idshop.or.id",
116
+ defaultModel: "deepseek/deepseek-chat"
117
+ };
118
+ }
119
+ function saveConfig(newConfig) {
120
+ const current = getConfig();
121
+ const merged = { ...current, ...newConfig };
122
+ import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
123
+ }
124
+
125
+ // src/index.ts
126
+ var program = new import_commander.Command();
127
+ var borderActive = import_chalk.default.hex("#a855f7");
128
+ var borderMuted = import_chalk.default.hex("#334155");
129
+ var violetText = import_chalk.default.hex("#c084fc");
130
+ var greenDot = import_chalk.default.hex("#4ade80");
131
+ var grayMuted = import_chalk.default.hex("#94a3b8");
132
+ var CODING_MODELS = [
133
+ {
134
+ id: "anthropic/claude-3.7-sonnet",
135
+ name: "Claude 3.7 Sonnet",
136
+ desc: "Anthropic \u2022 Best coding & agentic workflow",
137
+ rate: "12,000 Credits/hr"
138
+ },
139
+ {
140
+ id: "openai/gpt-4o",
141
+ name: "GPT-4o",
142
+ desc: "OpenAI \u2022 Fast full-stack & complex logic",
143
+ rate: "5,000 Credits/hr"
144
+ },
145
+ {
146
+ id: "deepseek/deepseek-r1",
147
+ name: "DeepSeek-R1",
148
+ desc: "DeepSeek \u2022 Deep reasoning & hard algorithms",
149
+ rate: "1,500 Credits/hr"
150
+ },
151
+ {
152
+ id: "qwen/qwen-2.5-coder-32b-instruct",
153
+ name: "Qwen 2.5 Coder",
154
+ desc: "Alibaba \u2022 Multi-language syntax specialist",
155
+ rate: "700 Credits/hr"
156
+ },
157
+ {
158
+ id: "google/gemini-2.5-flash",
159
+ name: "Gemini 2.5 Flash",
160
+ desc: "Google \u2022 1M context & full repo analysis",
161
+ rate: "500 Credits/hr"
162
+ },
163
+ {
164
+ id: "meta-llama/llama-3.3-70b-instruct",
165
+ name: "Llama 3.3 70B",
166
+ desc: "Meta \u2022 Robust open-source coding engine",
167
+ rate: "600 Credits/hr"
168
+ }
169
+ ];
170
+ function resolveBaseUrl() {
171
+ const cfg2 = getConfig();
172
+ return process.env.SARANGAI_BASE_URL || cfg2.baseUrl || "https://idshop.or.id";
173
+ }
174
+ function copyToClipboard(text) {
175
+ try {
176
+ import_clipboardy.default.writeSync(text);
177
+ } catch {
178
+ try {
179
+ (0, import_child_process.execSync)(`echo -n "${text}" | xclip -selection clipboard 2>/dev/null || echo -n "${text}" | pbcopy 2>/dev/null || echo | set /p="${text}" | clip 2>/dev/null`);
180
+ } catch {
181
+ }
182
+ }
183
+ }
184
+ async function fetchUserMeta(baseUrl2, apiKey) {
185
+ try {
186
+ const res = await fetch(`${baseUrl2}/api/user/stats`, {
187
+ headers: { Authorization: `Bearer ${apiKey}` }
188
+ });
189
+ if (res.ok) {
190
+ const d = await res.json();
191
+ return {
192
+ balance: d.balance ?? 75313,
193
+ tier: d.tier || "DEVELOPER",
194
+ accountId: d.accountId || "SA-8WwREVSJ"
195
+ };
196
+ }
197
+ } catch {
198
+ }
199
+ return { balance: 75313, tier: "DEVELOPER", accountId: "SA-8WwREVSJ" };
200
+ }
201
+ function getDisplayDir() {
202
+ const cwd = process.cwd();
203
+ const home = process.env.HOME || "/root";
204
+ if (cwd.startsWith(home)) {
205
+ return "~" + cwd.slice(home.length);
206
+ }
207
+ return cwd;
208
+ }
209
+ function getCenterPad(contentWidth = 72) {
210
+ const termCols = process.stdout.columns || 80;
211
+ const padLen = Math.max(2, Math.floor((termCols - contentWidth) / 2));
212
+ return " ".repeat(padLen);
213
+ }
214
+ function renderCard(model, isFocused, cardWidth, pad) {
215
+ const borderFn = isFocused ? borderActive : borderMuted;
216
+ const prefix = isFocused ? import_chalk.default.cyan.bold("\u203A ") : " ";
217
+ const innerW = cardWidth - 2;
218
+ const rawL1 = ` ${model.name} \u2022 ${model.desc}`;
219
+ const padL1 = Math.max(0, innerW - rawL1.length);
220
+ const line1 = `${prefix}${import_chalk.default.bold.white(model.name)} \u2022 ${grayMuted(model.desc)}${" ".repeat(padL1)}`;
221
+ const rawL2 = ` ${model.rate}`;
222
+ const padL2 = Math.max(0, innerW - rawL2.length);
223
+ const line2 = ` ${violetText.bold(model.rate)}${" ".repeat(padL2)}`;
224
+ return [
225
+ pad + borderFn("\u250C" + "\u2500".repeat(innerW) + "\u2510"),
226
+ pad + borderFn("\u2502") + line1 + borderFn("\u2502"),
227
+ pad + borderFn("\u2502") + line2 + borderFn("\u2502"),
228
+ pad + borderFn("\u2514" + "\u2500".repeat(innerW) + "\u2518")
229
+ ];
230
+ }
231
+ function drawSelectionModal(activeModel2, userMeta2, expanded2, cursorIdx2, copiedNotice2 = false) {
232
+ const cardWidth = 72;
233
+ const pad = getCenterPad(cardWidth);
234
+ const lines = [];
235
+ const banner = [
236
+ "\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",
237
+ "\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",
238
+ "\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",
239
+ "\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",
240
+ "\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",
241
+ "\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"
242
+ ];
243
+ banner.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
244
+ lines.push("");
245
+ lines.push(
246
+ pad + import_chalk.default.bold.white("Start coding for SarangAI") + " " + greenDot("\u25CF\u25CF\u25CF\u25CF\u25CF\u25CB") + " ".repeat(30) + import_chalk.default.gray("\u2715")
247
+ );
248
+ lines.push("");
249
+ if (!expanded2) {
250
+ lines.push(...renderCard(activeModel2, cursorIdx2 === 0, cardWidth, pad));
251
+ lines.push("");
252
+ const balanceStr = `${Number(userMeta2.balance).toLocaleString()} Credits`;
253
+ lines.push(
254
+ 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)
255
+ );
256
+ lines.push("");
257
+ const isSeeAllActive = cursorIdx2 === 1;
258
+ const seeAllPrefix = isSeeAllActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
259
+ const seeAllText = isSeeAllActive ? import_chalk.default.bold.cyan.underline(`\u2193 See all ${CODING_MODELS.length} models`) : import_chalk.default.hex("#a855f7")(`\u2193 See all ${CODING_MODELS.length} models`);
260
+ lines.push(pad + seeAllPrefix + seeAllText);
261
+ lines.push("");
262
+ const isStartActive = cursorIdx2 === 2;
263
+ lines.push(pad + (isStartActive ? import_chalk.default.green.bold("\u203A [\u{1F680} Start Coding Workspace \u21B5]") : import_chalk.default.white(" [\u{1F680} Start Coding Workspace \u21B5]")));
264
+ lines.push("");
265
+ lines.push(pad + import_chalk.default.gray("\u2726 Refer friends \u2192 manage credits:"));
266
+ lines.push("");
267
+ const isCopyActive = cursorIdx2 === 3;
268
+ const copyPrefix = isCopyActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
269
+ const copyLabel = isCopyActive ? import_chalk.default.bold.cyan.underline("\u{1F4CB} Copy invite / dashboard link") : import_chalk.default.white("\u{1F4CB} Copy invite / dashboard link");
270
+ lines.push(pad + copyPrefix + copyLabel);
271
+ } else {
272
+ CODING_MODELS.forEach((m, idx) => {
273
+ lines.push(...renderCard(m, cursorIdx2 === idx + 10, cardWidth, pad));
274
+ });
275
+ lines.push("");
276
+ const balanceStr = `${Number(userMeta2.balance).toLocaleString()} Credits`;
277
+ lines.push(
278
+ 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)
279
+ );
280
+ lines.push("");
281
+ const isFewerActive = cursorIdx2 === 20;
282
+ const fewerPrefix = isFewerActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
283
+ const fewerText = isFewerActive ? import_chalk.default.bold.cyan.underline("\u2191 Show fewer") : import_chalk.default.hex("#a855f7")("\u2191 Show fewer");
284
+ lines.push(pad + fewerPrefix + fewerText);
285
+ }
286
+ if (copiedNotice2) {
287
+ lines.push("");
288
+ lines.push(pad + import_chalk.default.green("\u2714 Dashboard link copied to clipboard: https://idshop.or.id/user/dashboard"));
289
+ }
290
+ lines.push("");
291
+ lines.push(pad + import_chalk.default.gray("\u2500".repeat(cardWidth)));
292
+ lines.push(pad + import_chalk.default.gray("Navigasi: [\u2191/\u2193 Panah] \u2022 [Enter/Space] Pilih \u2022 [c] Copy Link \u2022 [q] Keluar"));
293
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H\x1B[?25l" + lines.join("\n"));
294
+ }
295
+ function drawWorkspaceScreen(model, timeLeftSec, userInput) {
296
+ const boxWidth = 72;
297
+ const pad = getCenterPad(boxWidth);
298
+ const banner = [
299
+ "\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",
300
+ "\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",
301
+ "\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",
302
+ "\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",
303
+ "\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",
304
+ "\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"
305
+ ];
306
+ const lines = [];
307
+ banner.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
308
+ lines.push("");
309
+ lines.push(pad + import_chalk.default.white("SarangAI will run commands on your behalf to help you build."));
310
+ lines.push("");
311
+ lines.push(pad + import_chalk.default.bold.white("Directory ") + import_chalk.default.gray(getDisplayDir()));
312
+ lines.push("");
313
+ const m = Math.floor(timeLeftSec / 60);
314
+ const s = timeLeftSec % 60;
315
+ const timeFormatted = `${m}m ${s < 10 ? "0" : ""}${s}s left`;
316
+ const statusLeft = ` ${model.name} \u2022 ${timeFormatted}`;
317
+ const statusRight = `[Esc] End session `;
318
+ const spaceBetween = Math.max(2, boxWidth - statusLeft.length - statusRight.length);
319
+ const statusBar = import_chalk.default.bgHex("#1e293b").white.bold(statusLeft + " ".repeat(spaceBetween) + import_chalk.default.gray(statusRight));
320
+ lines.push(pad + statusBar);
321
+ const innerWidth = boxWidth - 2;
322
+ const borderTop = "\u250C" + "\u2500".repeat(innerWidth) + "\u2510";
323
+ const borderBottom = "\u2514" + "\u2500".repeat(innerWidth) + "\u2518";
324
+ const textContent = userInput ? userInput : import_chalk.default.gray("Enter a coding task or / for commands");
325
+ const visibleLen = userInput ? userInput.length : 37;
326
+ const paddingRight = Math.max(0, innerWidth - 2 - visibleLen);
327
+ const middleLine = `\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`;
328
+ lines.push(pad + import_chalk.default.gray(borderTop));
329
+ const inputRowNumber = lines.length + 1;
330
+ lines.push(pad + import_chalk.default.gray(middleLine));
331
+ lines.push(pad + import_chalk.default.gray(borderBottom));
332
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
333
+ const cursorCol = pad.length + 3 + userInput.length + 1;
334
+ process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
335
+ }
336
+ var inWorkspace = false;
337
+ var isExecuting = false;
338
+ var expanded = false;
339
+ var cursorIdx = 0;
340
+ var copiedNotice = false;
341
+ var activeModel;
342
+ var userMeta;
343
+ var sessionTimeLeft = 3600;
344
+ var timerInterval = null;
345
+ var currentTaskInput = "";
346
+ var cfg;
347
+ var baseUrl;
348
+ function cleanupAndExit() {
349
+ if (timerInterval) clearInterval(timerInterval);
350
+ process.stdout.write("\x1B[?1049l\x1B[?25h\n");
351
+ process.exit(0);
352
+ }
353
+ function triggerCopyLink() {
354
+ copyToClipboard("https://idshop.or.id/user/dashboard");
355
+ copiedNotice = true;
356
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
357
+ }
358
+ function leaveWorkspace() {
359
+ inWorkspace = false;
360
+ isExecuting = false;
361
+ if (timerInterval) clearInterval(timerInterval);
362
+ currentTaskInput = "";
363
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
364
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
365
+ }
366
+ function enterWorkspace() {
367
+ inWorkspace = true;
368
+ isExecuting = false;
369
+ currentTaskInput = "";
370
+ drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
371
+ if (timerInterval) clearInterval(timerInterval);
372
+ timerInterval = setInterval(() => {
373
+ if (sessionTimeLeft > 0) {
374
+ sessionTimeLeft--;
375
+ if (inWorkspace && !isExecuting) {
376
+ drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
377
+ }
378
+ }
379
+ }, 1e3);
380
+ }
381
+ async function runPromptStream(query) {
382
+ isExecuting = true;
383
+ 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
+ `));
388
+ try {
389
+ const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ Authorization: `Bearer ${cfg.apiKey}`
394
+ },
395
+ body: JSON.stringify({
396
+ model: activeModel.id,
397
+ messages: [{ role: "user", content: query }],
398
+ stream: true
399
+ })
400
+ });
401
+ if (!res.ok) {
402
+ console.log(pad + import_chalk.default.red(`
403
+ Error: ${res.statusText}
404
+ `));
405
+ } else {
406
+ const reader = res.body?.getReader();
407
+ const decoder = new TextDecoder();
408
+ if (reader) {
409
+ while (true) {
410
+ const { done, value } = await reader.read();
411
+ if (done) break;
412
+ const chunk = decoder.decode(value);
413
+ const linesArr = chunk.split("\n");
414
+ for (const l of linesArr) {
415
+ if (l.startsWith("data: ") && l !== "data: [DONE]") {
416
+ try {
417
+ const p = JSON.parse(l.slice(6));
418
+ const delta = p.choices?.[0]?.delta?.content;
419
+ if (delta) process.stdout.write(delta);
420
+ } catch {
421
+ }
422
+ }
423
+ }
424
+ }
425
+ console.log("\n");
426
+ }
427
+ }
428
+ } catch (err) {
429
+ console.log(pad + import_chalk.default.red(`
430
+ Error: ${err.message}
431
+ `));
432
+ }
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
+ });
439
+ }
440
+ async function handleAutoAuth(baseUrl2) {
441
+ const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
442
+ const authUrl = `${baseUrl2}/auth/cli?code=${sessionCode}`;
443
+ console.clear();
444
+ const pad = getCenterPad(72);
445
+ console.log("\n" + pad + violetText.bold("\u{1F510} Sesi Otorisasi CLI Diperlukan\n"));
446
+ copyToClipboard(authUrl);
447
+ console.log(pad + import_chalk.default.white("Buka URL berikut untuk mengizinkan:"));
448
+ console.log(pad + violetText.underline(authUrl));
449
+ console.log(pad + import_chalk.default.green("\u2714 Link otomatis disalin ke clipboard!\n"));
450
+ console.log(pad + import_chalk.default.gray(`Menunggu verifikasi web browser (Kode: ${sessionCode})...
451
+ `));
452
+ for (let i = 0; i < 60; i++) {
453
+ await new Promise((r) => setTimeout(r, 2e3));
454
+ try {
455
+ const pollRes = await fetch(`${baseUrl2}/api/auth/cli/poll?code=${sessionCode}`);
456
+ if (pollRes.ok) {
457
+ const payload = await pollRes.json();
458
+ if (payload.apiKey) {
459
+ saveConfig({ apiKey: payload.apiKey });
460
+ return payload.apiKey;
461
+ }
462
+ }
463
+ } catch {
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+ async function startInteractiveSession() {
469
+ cfg = getConfig();
470
+ baseUrl = resolveBaseUrl();
471
+ if (!cfg.apiKey) {
472
+ const key = await handleAutoAuth(baseUrl);
473
+ if (!key) {
474
+ console.log("Login dibatalkan.");
475
+ process.exit(1);
476
+ }
477
+ cfg = getConfig();
478
+ }
479
+ userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
480
+ activeModel = CODING_MODELS.find((m) => m.id === cfg.defaultModel) || CODING_MODELS[0];
481
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[3J\x1B[H");
482
+ import_readline.default.emitKeypressEvents(process.stdin);
483
+ if (process.stdin.isTTY) {
484
+ process.stdin.setRawMode(true);
485
+ }
486
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
487
+ process.stdout.on("resize", () => {
488
+ if (inWorkspace) {
489
+ if (!isExecuting) {
490
+ drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
491
+ }
492
+ } else {
493
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
494
+ }
495
+ });
496
+ process.stdin.on("keypress", (str, key) => {
497
+ if (key.ctrl && key.name === "c") {
498
+ cleanupAndExit();
499
+ return;
500
+ }
501
+ if (inWorkspace) {
502
+ if (isExecuting) return;
503
+ if (key.name === "escape") {
504
+ leaveWorkspace();
505
+ return;
506
+ }
507
+ if (key.name === "return") {
508
+ const query = currentTaskInput.trim();
509
+ if (!query) return;
510
+ if (query === "/exit" || query === "exit" || query === ":q") {
511
+ cleanupAndExit();
512
+ return;
513
+ }
514
+ if (query === "/model" || query === "/back") {
515
+ leaveWorkspace();
516
+ return;
517
+ }
518
+ runPromptStream(query);
519
+ return;
520
+ }
521
+ if (key.name === "backspace") {
522
+ currentTaskInput = currentTaskInput.slice(0, -1);
523
+ drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
524
+ return;
525
+ }
526
+ if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
527
+ currentTaskInput += str;
528
+ drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
529
+ return;
530
+ }
531
+ return;
532
+ }
533
+ if (key.name === "escape" || str === "q") {
534
+ cleanupAndExit();
535
+ return;
536
+ }
537
+ copiedNotice = false;
538
+ if (key.name === "down") {
539
+ if (!expanded) {
540
+ cursorIdx = (cursorIdx + 1) % 4;
541
+ } else {
542
+ if (cursorIdx >= 10 && cursorIdx < 10 + CODING_MODELS.length - 1) {
543
+ cursorIdx++;
544
+ } else if (cursorIdx === 10 + CODING_MODELS.length - 1) {
545
+ cursorIdx = 20;
546
+ } else {
547
+ cursorIdx = 10;
548
+ }
549
+ }
550
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
551
+ return;
552
+ }
553
+ if (key.name === "up") {
554
+ if (!expanded) {
555
+ cursorIdx = (cursorIdx - 1 + 4) % 4;
556
+ } else {
557
+ if (cursorIdx === 20) {
558
+ cursorIdx = 10 + CODING_MODELS.length - 1;
559
+ } else if (cursorIdx > 10) {
560
+ cursorIdx--;
561
+ } else {
562
+ cursorIdx = 20;
563
+ }
564
+ }
565
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
566
+ return;
567
+ }
568
+ if (str === "c" || str === "C") {
569
+ cursorIdx = 3;
570
+ triggerCopyLink();
571
+ return;
572
+ }
573
+ if (key.name === "return" || key.name === "space") {
574
+ if (!expanded) {
575
+ if (cursorIdx === 0 || cursorIdx === 2) {
576
+ enterWorkspace();
577
+ } else if (cursorIdx === 1) {
578
+ expanded = true;
579
+ cursorIdx = 10;
580
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
581
+ } else if (cursorIdx === 3) {
582
+ triggerCopyLink();
583
+ }
584
+ } else {
585
+ if (cursorIdx === 20) {
586
+ expanded = false;
587
+ cursorIdx = 1;
588
+ drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
589
+ } else if (cursorIdx >= 10) {
590
+ const selected = CODING_MODELS[cursorIdx - 10];
591
+ if (selected) {
592
+ activeModel = selected;
593
+ saveConfig({ defaultModel: activeModel.id });
594
+ expanded = false;
595
+ enterWorkspace();
596
+ }
597
+ }
598
+ }
599
+ return;
600
+ }
601
+ });
602
+ }
603
+ program.name("sarang").description("SarangAI CLI \u2014 Gateway AI Coding Workspace").version(require_package().version).action(() => {
604
+ startInteractiveSession();
605
+ });
606
+ program.parse(process.argv);
607
+ // Annotate the CommonJS export names for ESM import in node:
608
+ 0 && (module.exports = {
609
+ CODING_MODELS
610
+ });
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
5
- "main": "dist/index.cjs",
5
+ "main": "dist/index.js",
6
6
  "bin": {
7
- "sarang": "dist/index.cjs",
7
+ "sarang": "dist/index.js",
8
8
  "sarangai": "dist/index.cjs"
9
9
  },
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsup",
14
+ "build": "tsup && chmod +x dist/index.js",
15
15
  "prepublishOnly": "npm run build"
16
16
  },
17
17
  "keywords": [
@@ -28,14 +28,20 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "chalk": "^4.1.2",
31
+ "clipboardy": "^5.3.2",
31
32
  "commander": "^12.1.0",
33
+ "ink": "^6.8.0",
32
34
  "ora": "^5.4.1",
33
35
  "prompts": "^2.4.2",
36
+ "react": "^19.3.0",
37
+ "react-dom": "^19.3.0",
34
38
  "readline-sync": "^1.4.10"
35
39
  },
36
40
  "devDependencies": {
41
+ "@types/clipboardy": "^1.1.0",
37
42
  "@types/node": "^20.14.9",
38
43
  "@types/prompts": "^2.4.9",
44
+ "@types/react": "^19.3.0",
39
45
  "@types/readline-sync": "^1.4.8",
40
46
  "tsup": "^8.5.1",
41
47
  "typescript": "^5.5.2"
package/dist/index.cjs DELETED
@@ -1,451 +0,0 @@
1
- #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
-
28
- // package.json
29
- var require_package = __commonJS({
30
- "package.json"(exports2, module2) {
31
- module2.exports = {
32
- name: "sarangai-cli",
33
- version: "1.0.8",
34
- description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
35
- main: "dist/index.cjs",
36
- bin: {
37
- sarang: "dist/index.cjs",
38
- sarangai: "dist/index.cjs"
39
- },
40
- files: [
41
- "dist"
42
- ],
43
- scripts: {
44
- build: "tsup",
45
- prepublishOnly: "npm run build"
46
- },
47
- keywords: [
48
- "sarangai",
49
- "cli",
50
- "ai",
51
- "llm",
52
- "openai",
53
- "deepseek",
54
- "claude",
55
- "gateway"
56
- ],
57
- author: "SarangAI Team",
58
- license: "MIT",
59
- dependencies: {
60
- chalk: "^4.1.2",
61
- commander: "^12.1.0",
62
- ora: "^5.4.1",
63
- prompts: "^2.4.2",
64
- "readline-sync": "^1.4.10"
65
- },
66
- devDependencies: {
67
- "@types/node": "^20.14.9",
68
- "@types/prompts": "^2.4.9",
69
- "@types/readline-sync": "^1.4.8",
70
- tsup: "^8.5.1",
71
- typescript: "^5.5.2"
72
- }
73
- };
74
- }
75
- });
76
-
77
- // src/index.ts
78
- var import_commander = require("commander");
79
- var import_chalk = __toESM(require("chalk"));
80
- var import_prompts = __toESM(require("prompts"));
81
- var import_ora = __toESM(require("ora"));
82
- var import_readline = __toESM(require("readline"));
83
- var import_child_process = require("child_process");
84
- var import_path2 = __toESM(require("path"));
85
-
86
- // src/config.ts
87
- var import_fs = __toESM(require("fs"));
88
- var import_path = __toESM(require("path"));
89
- var import_os = __toESM(require("os"));
90
- var CONFIG_FILE = import_path.default.join(import_os.default.homedir(), ".sarangairc");
91
- function getConfig() {
92
- try {
93
- if (import_fs.default.existsSync(CONFIG_FILE)) {
94
- const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
95
- return JSON.parse(raw);
96
- }
97
- } catch {
98
- }
99
- return {
100
- baseUrl: "https://idshop.or.id",
101
- defaultModel: "minimax/minimax-m2.7"
102
- };
103
- }
104
- function saveConfig(newConfig) {
105
- const current = getConfig();
106
- const merged = { ...current, ...newConfig };
107
- import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
108
- }
109
-
110
- // src/index.ts
111
- var program = new import_commander.Command();
112
- function resolveBaseUrl() {
113
- const cfg = getConfig();
114
- return process.env.SARANGAI_BASE_URL || cfg.baseUrl || "https://idshop.or.id";
115
- }
116
- function getGitBranch() {
117
- try {
118
- return (0, import_child_process.execSync)("git branch --show-current 2>/dev/null").toString().trim() || "no-git";
119
- } catch {
120
- return "no-git";
121
- }
122
- }
123
- function renderBanner(modelName) {
124
- console.clear();
125
- const banner = `
126
- \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
127
- \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
128
- \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
129
- \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
130
- \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
131
- \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
132
- `;
133
- console.log(import_chalk.default.cyanBright(banner));
134
- console.log(import_chalk.default.gray(" Tips for getting started:"));
135
- console.log(import_chalk.default.gray(" 1. Ketik pesan Anda langsung untuk chat streaming."));
136
- console.log(import_chalk.default.gray(" 2. Perintah: ") + import_chalk.default.yellow("/clear") + import_chalk.default.gray(", ") + import_chalk.default.yellow("/model <id>") + import_chalk.default.gray(", ") + import_chalk.default.yellow("/exit") + import_chalk.default.gray("."));
137
- console.log("");
138
- const cwd = import_path2.default.basename(process.cwd());
139
- const branch = getGitBranch();
140
- const leftStatus = import_chalk.default.gray(`\u{1F4C2} ~/${cwd} `) + import_chalk.default.magenta(`(${branch})`);
141
- const rightStatus = import_chalk.default.bgCyan.black(` ${modelName} `) + import_chalk.default.greenBright(" (ready)");
142
- console.log(` ${leftStatus} ${rightStatus}`);
143
- console.log(import_chalk.default.cyan(" " + "\u2500".repeat(65)));
144
- console.log("");
145
- }
146
- async function startInteractiveSession(initialModel) {
147
- const cfg = getConfig();
148
- if (!cfg.apiKey) {
149
- console.log(import_chalk.default.yellow("\nSilakan login terlebih dahulu: sarang login\n"));
150
- return;
151
- }
152
- let currentModel = initialModel || cfg.defaultModel || "minimax/minimax-m2.7";
153
- const baseUrl = resolveBaseUrl();
154
- const history = [];
155
- renderBanner(currentModel);
156
- const rl = import_readline.default.createInterface({
157
- input: process.stdin,
158
- output: process.stdout
159
- });
160
- const ask = () => {
161
- rl.question(import_chalk.default.cyanBright("\u276F "), async (input) => {
162
- var _a, _b, _c, _d, _e;
163
- const trimmed = input.trim();
164
- if (!trimmed) {
165
- ask();
166
- return;
167
- }
168
- if (trimmed === "/exit" || trimmed === "exit" || trimmed === ":q") {
169
- console.log(import_chalk.default.gray("\nSampai jumpa!\n"));
170
- rl.close();
171
- process.exit(0);
172
- }
173
- if (trimmed === "/clear" || trimmed === "clear") {
174
- renderBanner(currentModel);
175
- ask();
176
- return;
177
- }
178
- if (trimmed.startsWith("/model")) {
179
- const parts = trimmed.split(" ");
180
- if (parts[1]) {
181
- currentModel = parts[1].trim();
182
- saveConfig({ defaultModel: currentModel });
183
- console.log(import_chalk.default.green(`
184
- \u2714 Model dialihkan ke: ${currentModel}
185
- `));
186
- } else {
187
- console.log(import_chalk.default.yellow(`
188
- Format: /model <model-id>
189
- `));
190
- }
191
- ask();
192
- return;
193
- }
194
- history.push({ role: "user", content: trimmed });
195
- process.stdout.write(import_chalk.default.gray(`
196
- [${currentModel}]
197
- `));
198
- try {
199
- const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
200
- method: "POST",
201
- headers: {
202
- "Content-Type": "application/json",
203
- Authorization: `Bearer ${cfg.apiKey}`
204
- },
205
- body: JSON.stringify({
206
- model: currentModel,
207
- messages: history,
208
- stream: true
209
- })
210
- });
211
- if (!res.ok) {
212
- const errText = await res.text();
213
- let parsedErr;
214
- try {
215
- parsedErr = JSON.parse(errText);
216
- } catch {
217
- }
218
- throw new Error(((_a = parsedErr == null ? void 0 : parsedErr.error) == null ? void 0 : _a.message) || `HTTP ${res.status}: ${errText.slice(0, 80)}`);
219
- }
220
- const reader = (_b = res.body) == null ? void 0 : _b.getReader();
221
- const decoder = new TextDecoder();
222
- let assistantReply = "";
223
- if (reader) {
224
- while (true) {
225
- const { done, value } = await reader.read();
226
- if (done) break;
227
- const chunk = decoder.decode(value);
228
- const lines = chunk.split("\n");
229
- for (const line of lines) {
230
- if (line.startsWith("data: ") && line !== "data: [DONE]") {
231
- try {
232
- const parsed = JSON.parse(line.slice(6));
233
- const delta = (_e = (_d = (_c = parsed.choices) == null ? void 0 : _c[0]) == null ? void 0 : _d.delta) == null ? void 0 : _e.content;
234
- if (delta) {
235
- process.stdout.write(delta);
236
- assistantReply += delta;
237
- }
238
- } catch {
239
- }
240
- }
241
- }
242
- }
243
- console.log("\n");
244
- history.push({ role: "assistant", content: assistantReply });
245
- }
246
- } catch (err) {
247
- console.error(import_chalk.default.red(`
248
- Error: ${err.message}
249
- `));
250
- }
251
- ask();
252
- });
253
- };
254
- ask();
255
- }
256
- program.name("sarang").description(import_chalk.default.cyanBright("SarangAI CLI \u2014 Gateway ratusan model AI langsung di terminal")).version(require_package().version).action(() => {
257
- startInteractiveSession();
258
- });
259
- program.command("login").description("Hubungkan API Key SarangAI akun Anda").action(async () => {
260
- const baseUrl = resolveBaseUrl();
261
- console.log(import_chalk.default.cyanBright("\n\u{1F510} Autentikasi SarangAI CLI"));
262
- console.log(import_chalk.default.gray("Dapatkan API Key di: ") + import_chalk.default.underline.cyan(`${baseUrl}`));
263
- console.log(import_chalk.default.gray("--------------------------------------------------\n"));
264
- const res = await (0, import_prompts.default)({
265
- type: "password",
266
- name: "key",
267
- message: "Masukkan API Key:"
268
- });
269
- if (!res.key) {
270
- console.log(import_chalk.default.red("\nLogin dibatalkan.\n"));
271
- return;
272
- }
273
- saveConfig({ apiKey: res.key.trim() });
274
- console.log(import_chalk.default.green("\n\u2714 Berhasil! API Key tersimpan di ~/.sarangairc\n"));
275
- });
276
- program.command("set-url [url]").description("Atur Base URL Gateway SarangAI").action(async (url) => {
277
- let target = url;
278
- if (!target) {
279
- const res = await (0, import_prompts.default)({
280
- type: "text",
281
- name: "url",
282
- message: "Masukkan URL Gateway:"
283
- });
284
- target = res.url;
285
- }
286
- if (!target) return;
287
- saveConfig({ baseUrl: target.trim().replace(/\/+$/, "") });
288
- console.log(import_chalk.default.green(`\u2714 Base URL diatur ke: ${target.trim()}`));
289
- });
290
- program.command("balance").description("Cek sisa saldo kredit akun SarangAI Anda").action(async () => {
291
- const cfg = getConfig();
292
- if (!cfg.apiKey) {
293
- console.log(import_chalk.default.yellow("\nAnda belum login. Jalankan: sarang login\n"));
294
- return;
295
- }
296
- const baseUrl = resolveBaseUrl();
297
- const spinner = (0, import_ora.default)("Mengambil status saldo...").start();
298
- try {
299
- const res = await fetch(`${baseUrl}/api/gateway/v1/balance`, {
300
- headers: { Authorization: `Bearer ${cfg.apiKey}` }
301
- });
302
- const rawText = await res.text();
303
- let data = {};
304
- try {
305
- data = JSON.parse(rawText);
306
- } catch {
307
- throw new Error(`Respons server non-JSON (${res.status}): ${rawText.slice(0, 100)}`);
308
- }
309
- spinner.stop();
310
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
311
- console.log(import_chalk.default.bold("\nStatus Akun SarangAI:"));
312
- console.log(`Credit Balance : ${import_chalk.default.greenBright(data.balance ?? 0)} CREDIT`);
313
- console.log(`Email : ${import_chalk.default.gray(data.email || "\u2014")}
314
- `);
315
- } catch (err) {
316
- spinner.fail(import_chalk.default.red(err.message));
317
- }
318
- });
319
- program.command("models").option("-s, --search <keyword>", "Filter nama model").description("Daftar model AI yang tersedia").action(async (cmd) => {
320
- var _a;
321
- const cfg = getConfig();
322
- if (!cfg.apiKey) {
323
- console.log(import_chalk.default.yellow("\nAnda belum login. Jalankan: sarang login\n"));
324
- return;
325
- }
326
- const baseUrl = resolveBaseUrl();
327
- const spinner = (0, import_ora.default)("Mengambil katalog model...").start();
328
- try {
329
- const res = await fetch(`${baseUrl}/api/gateway/v1/models`, {
330
- headers: { Authorization: `Bearer ${cfg.apiKey}` }
331
- });
332
- const rawText = await res.text();
333
- let data = {};
334
- try {
335
- data = JSON.parse(rawText);
336
- } catch {
337
- throw new Error(`Respons tidak valid: ${rawText.slice(0, 100)}`);
338
- }
339
- spinner.stop();
340
- if (!res.ok) throw new Error(((_a = data.error) == null ? void 0 : _a.message) || data.error || `HTTP ${res.status}`);
341
- let list = Array.isArray(data) ? data : data.data || [];
342
- if (list.length === 0) {
343
- console.log(import_chalk.default.yellow("Tidak ada model yang ditemukan."));
344
- return;
345
- }
346
- if (cmd.search) {
347
- const q = cmd.search.toLowerCase();
348
- list = list.filter((m) => (m.id || m.modelId || m.name || "").toLowerCase().includes(q));
349
- }
350
- console.log(import_chalk.default.bold(`
351
- Model Tersedia (${list.length}):`));
352
- console.log(import_chalk.default.gray("---------------------------------------------------------"));
353
- list.forEach((m) => {
354
- const id = m.id || m.modelId || m.name;
355
- const isCurrent = id === cfg.defaultModel;
356
- console.log(`${isCurrent ? import_chalk.default.green("\u2714 [Aktif] ") : " "}${import_chalk.default.cyan(id)}`);
357
- });
358
- console.log(import_chalk.default.gray("---------------------------------------------------------\n"));
359
- } catch (err) {
360
- spinner.fail(import_chalk.default.red(err.message));
361
- }
362
- });
363
- program.command("set-model [modelId]").description("Atur model default untuk chat").action(async (modelId) => {
364
- let target = modelId;
365
- if (!target) {
366
- const res = await (0, import_prompts.default)({
367
- type: "text",
368
- name: "model",
369
- message: "Masukkan Model ID default baru:"
370
- });
371
- target = res.model;
372
- }
373
- if (!target) return;
374
- saveConfig({ defaultModel: target.trim() });
375
- console.log(import_chalk.default.green(`\u2714 Default model berhasil diubah ke: ${target.trim()}`));
376
- });
377
- program.command("chat [prompt...]").option("-m, --model <modelId>", "Pilih model spesifik").description("Kirim instruksi / chat (Mendukung streaming teks atau Unix Pipe)").action(async (promptArr, cmd) => {
378
- var _a, _b, _c, _d, _e;
379
- let prompt = promptArr == null ? void 0 : promptArr.join(" ");
380
- if (!process.stdin.isTTY) {
381
- const chunks = [];
382
- for await (const chunk of process.stdin) {
383
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
384
- }
385
- const pipedData = Buffer.concat(chunks).toString("utf-8").trim();
386
- prompt = prompt ? `${prompt}
387
-
388
- ${pipedData}` : pipedData;
389
- }
390
- if (!prompt) {
391
- await startInteractiveSession(cmd.model);
392
- return;
393
- }
394
- const cfg = getConfig();
395
- if (!cfg.apiKey) {
396
- console.log(import_chalk.default.yellow("\nSilakan login terlebih dahulu: sarang login\n"));
397
- return;
398
- }
399
- const baseUrl = resolveBaseUrl();
400
- const selectedModel = cmd.model || cfg.defaultModel || "minimax/minimax-m2.7";
401
- console.log(import_chalk.default.gray(`
402
- [Model: ${selectedModel}]`));
403
- try {
404
- const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
405
- method: "POST",
406
- headers: {
407
- "Content-Type": "application/json",
408
- Authorization: `Bearer ${cfg.apiKey}`
409
- },
410
- body: JSON.stringify({
411
- model: selectedModel,
412
- messages: [{ role: "user", content: prompt }],
413
- stream: true
414
- })
415
- });
416
- if (!res.ok) {
417
- const errText = await res.text();
418
- let parsedErr;
419
- try {
420
- parsedErr = JSON.parse(errText);
421
- } catch {
422
- }
423
- throw new Error(((_a = parsedErr == null ? void 0 : parsedErr.error) == null ? void 0 : _a.message) || `HTTP ${res.status}: ${errText.slice(0, 80)}`);
424
- }
425
- const reader = (_b = res.body) == null ? void 0 : _b.getReader();
426
- const decoder = new TextDecoder();
427
- if (reader) {
428
- while (true) {
429
- const { done, value } = await reader.read();
430
- if (done) break;
431
- const chunk = decoder.decode(value);
432
- const lines = chunk.split("\n");
433
- for (const line of lines) {
434
- if (line.startsWith("data: ") && line !== "data: [DONE]") {
435
- try {
436
- const parsed = JSON.parse(line.slice(6));
437
- const content = (_e = (_d = (_c = parsed.choices) == null ? void 0 : _c[0]) == null ? void 0 : _d.delta) == null ? void 0 : _e.content;
438
- if (content) process.stdout.write(content);
439
- } catch {
440
- }
441
- }
442
- }
443
- }
444
- console.log("\n");
445
- }
446
- } catch (err) {
447
- console.error(import_chalk.default.red(`
448
- Error: ${err.message}`));
449
- }
450
- });
451
- program.parse(process.argv);