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 +34 -70
- package/dist/index.js +610 -0
- package/package.json +10 -4
- package/dist/index.cjs +0 -451
package/README.md
CHANGED
|
@@ -1,104 +1,68 @@
|
|
|
1
1
|
# SarangAI CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
25
|
+
Pastikan Node.js (>= 18) telah terpasang, lalu instal paket secara global:
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
npm install -g sarangai-cli
|
|
23
|
-
```
|
|
27
|
+
npm install -g sarangai-cli@latest
|
|
24
28
|
|
|
25
29
|
---
|
|
26
30
|
|
|
27
|
-
##
|
|
31
|
+
## Memulai Workspace Interaktif
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
Dapatkan API Key dari dashboard akun SarangAI Anda, lalu hubungkan ke CLI:
|
|
33
|
+
Cukup jalankan satu perintah:
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
sarang login
|
|
34
|
-
```
|
|
35
|
+
sarang
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
Terminal akan otomatis menampilkan modal seleksi model coding, ringkasan saldo akun, dan opsi masuk ke ruang kerja (Workspace).
|
|
37
38
|
|
|
38
|
-
###
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
Membaca input langsung dari terminal atau berkas kode:
|
|
48
|
+
## Autentikasi Otomatis
|
|
82
49
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
|
|
5
|
-
"main": "dist/index.
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sarang": "dist/index.
|
|
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);
|