zelai-cli 1.1.0
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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/zelai.js +20 -0
- package/connector/README.md +51 -0
- package/connector/discord.js +155 -0
- package/connector/shared.js +189 -0
- package/connector/telegram.js +176 -0
- package/package.json +76 -0
- package/src/cli.js +563 -0
- package/src/client.js +171 -0
- package/src/commands.js +501 -0
- package/src/config.js +63 -0
- package/src/index.js +58 -0
- package/src/session.js +178 -0
- package/src/ui.js +169 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zelai CLI — main loop.
|
|
3
|
+
* Interactive REPL untuk Zelapi Agent API.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
|
|
10
|
+
const config = require('./config');
|
|
11
|
+
const { makeClient, LimitExceededError } = require('./client');
|
|
12
|
+
const commands = require('./commands');
|
|
13
|
+
const session = require('./session');
|
|
14
|
+
const ui = require('./ui');
|
|
15
|
+
|
|
16
|
+
const VERSION = require('../package.json').version;
|
|
17
|
+
|
|
18
|
+
function parseArgv(argv) {
|
|
19
|
+
const out = { _: [], flags: {} };
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a === '--help' || a === '-h') out.flags.help = true;
|
|
23
|
+
else if (a === '--version' || a === '-v') out.flags.version = true;
|
|
24
|
+
else if (a === '--key' || a === '-k') out.flags.key = argv[++i];
|
|
25
|
+
else if (a === '--model' || a === '-m') out.flags.model = argv[++i];
|
|
26
|
+
else if (a === '--endpoint' || a === '-e') out.flags.endpoint = argv[++i];
|
|
27
|
+
else if (a === '--system' || a === '-s') out.flags.system = argv[++i];
|
|
28
|
+
else if (a === '--session') out.flags.session = argv[++i];
|
|
29
|
+
else if (a === '--no-banner') out.flags.noBanner = true;
|
|
30
|
+
else if (a === '--prompt' || a === '-p') out.flags.prompt = argv[++i];
|
|
31
|
+
else if (a === 'login') out._.push('login');
|
|
32
|
+
else if (a === 'status') out._.push('status');
|
|
33
|
+
else if (a === 'usage') out._.push('usage');
|
|
34
|
+
else if (a === 'activity' || a === 'logs') out._.push('activity');
|
|
35
|
+
else if (a === 'config') out._.push('config');
|
|
36
|
+
else if (a === 'sessions') out._.push('sessions');
|
|
37
|
+
else if (a === 'resume' || a === 'continue') {
|
|
38
|
+
out._.push('resume');
|
|
39
|
+
// optional positional arg: zelai resume <id|nomor>
|
|
40
|
+
if (argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
41
|
+
out.flags.resumeRef = argv[++i];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (a.startsWith('-')) {
|
|
45
|
+
// unknown flag — abaikan biar friendly
|
|
46
|
+
} else out._.push(a);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printHelp() {
|
|
52
|
+
const c = ui.chalk;
|
|
53
|
+
console.log(`
|
|
54
|
+
${c.magentaBright.bold('zelai')} ${c.gray('v' + VERSION)} ${c.gray('—')} official CLI buat ${c.magenta.bold('Zelapi')} Agent API
|
|
55
|
+
|
|
56
|
+
${c.bold('Usage:')}
|
|
57
|
+
${c.cyan('zelai')} masuk ke chat interaktif
|
|
58
|
+
${c.cyan('zelai -p')} ${c.gray('"<pesan>"')} kirim satu pesan terus exit (one-shot)
|
|
59
|
+
${c.cyan('zelai login')} ${c.gray('[zel_…]')} set / ganti API key
|
|
60
|
+
${c.cyan('zelai status')} cek status server
|
|
61
|
+
${c.cyan('zelai usage')} tampilin pemakaian API key
|
|
62
|
+
${c.cyan('zelai activity')} tampilin activity logs
|
|
63
|
+
${c.cyan('zelai sessions')} list sesi tersimpan di ./session
|
|
64
|
+
${c.cyan('zelai resume')} ${c.gray('[id|nomor]')} lanjutin sesi (default: paling baru)
|
|
65
|
+
${c.cyan('zelai config')} tampilin konfigurasi tersimpan
|
|
66
|
+
|
|
67
|
+
${c.bold('Options:')}
|
|
68
|
+
${c.gray('-k, --key <key>')} pakai API key tertentu sekali jalan
|
|
69
|
+
${c.gray('-m, --model <name>')} opus | sonnet | haiku
|
|
70
|
+
${c.gray('-e, --endpoint <path>')} AI endpoint (default /ai/claila)
|
|
71
|
+
${c.gray('-s, --system <prompt>')} custom system prompt
|
|
72
|
+
${c.gray(' --session <id>')} lanjutin sesi lama
|
|
73
|
+
${c.gray(' --no-banner')} sembunyiin banner
|
|
74
|
+
${c.gray('-p, --prompt <msg>')} kirim satu pesan langsung
|
|
75
|
+
${c.gray('-h, --help')} tampilin pesan ini
|
|
76
|
+
${c.gray('-v, --version')} tampilin versi
|
|
77
|
+
|
|
78
|
+
${c.bold('Inside chat:')}
|
|
79
|
+
ketik ${c.yellow('/help')} buat lihat semua slash command (${c.yellow('/status')}, ${c.yellow('/usage')}, ${c.yellow('/activity')}, ${c.yellow('/sessions')}, ${c.yellow('/load')}, …)
|
|
80
|
+
|
|
81
|
+
${c.gray('Docs: https://zelapioffciall.dpdns.org')}
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main(argv) {
|
|
86
|
+
const args = parseArgv(argv);
|
|
87
|
+
|
|
88
|
+
if (args.flags.help) {
|
|
89
|
+
printHelp();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (args.flags.version) {
|
|
93
|
+
console.log(`zelai v${VERSION}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Make sure session folder is ready first thing — user-requested behavior.
|
|
98
|
+
session.ensureDir();
|
|
99
|
+
|
|
100
|
+
// Load config + apply flags / env overrides.
|
|
101
|
+
let cfg = config.load();
|
|
102
|
+
if (process.env.ZELAPI_KEY && !cfg.apiKey) cfg.apiKey = process.env.ZELAPI_KEY;
|
|
103
|
+
if (args.flags.key) cfg.apiKey = args.flags.key;
|
|
104
|
+
if (args.flags.model) cfg.model = args.flags.model;
|
|
105
|
+
if (args.flags.endpoint) cfg.endpoint = args.flags.endpoint;
|
|
106
|
+
if (args.flags.system) cfg.system = args.flags.system;
|
|
107
|
+
|
|
108
|
+
const sub = args._[0];
|
|
109
|
+
|
|
110
|
+
if (sub === 'login') {
|
|
111
|
+
await runLogin(cfg, args._[1]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (sub === 'status') {
|
|
115
|
+
const client = makeClient(cfg);
|
|
116
|
+
await runOneShot(() => commandStatus(client));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (sub === 'usage') {
|
|
120
|
+
if (!cfg.apiKey) return errorExit('Belum login. Jalanin `zelai login` dulu.');
|
|
121
|
+
const client = makeClient(cfg);
|
|
122
|
+
await runOneShot(() => commandUsage(client, cfg.apiKey));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (sub === 'activity') {
|
|
126
|
+
if (!cfg.apiKey) return errorExit('Belum login. Jalanin `zelai login` dulu.');
|
|
127
|
+
const client = makeClient(cfg);
|
|
128
|
+
await runOneShot(() => commandActivity(client, cfg.apiKey));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (sub === 'sessions') {
|
|
132
|
+
listSessions();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (sub === 'resume') {
|
|
136
|
+
// Resolve sesi yang mau di-resume:
|
|
137
|
+
// zelai resume <id> → load id-nya
|
|
138
|
+
// zelai resume <nomor> → load index dari `zelai sessions` (1-based)
|
|
139
|
+
// zelai resume → load sesi yang paling baru
|
|
140
|
+
const ref = args.flags.resumeRef;
|
|
141
|
+
const rows = session.list();
|
|
142
|
+
let target = null;
|
|
143
|
+
if (!ref) {
|
|
144
|
+
target = rows[0]; // paling baru
|
|
145
|
+
} else if (/^\d+$/.test(ref)) {
|
|
146
|
+
target = rows[parseInt(ref, 10) - 1];
|
|
147
|
+
} else {
|
|
148
|
+
const rec = session.load(ref);
|
|
149
|
+
target = rec ? { id: rec.id, turns: (rec.history || []).length } : null;
|
|
150
|
+
}
|
|
151
|
+
if (!target) {
|
|
152
|
+
console.log(`${ui.errorTag()} sesi "${ref || '(none)'}" gak ketemu. Coba ${ui.chalk.cyan('zelai sessions')} dulu.`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const rec = session.load(target.id);
|
|
156
|
+
if (rec) {
|
|
157
|
+
if (rec.model) cfg.model = rec.model;
|
|
158
|
+
if (rec.endpoint) cfg.endpoint = rec.endpoint;
|
|
159
|
+
if (rec.system) cfg.system = rec.system;
|
|
160
|
+
config.update({
|
|
161
|
+
lastSession: target.id,
|
|
162
|
+
model: cfg.model,
|
|
163
|
+
endpoint: cfg.endpoint,
|
|
164
|
+
system: cfg.system,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
args.flags.session = target.id;
|
|
168
|
+
if (!args.flags.noBanner) console.log(ui.banner());
|
|
169
|
+
console.log(`${ui.okTag()} resume sesi ${ui.chalk.cyan(target.id)} ` +
|
|
170
|
+
ui.chalk.gray(`(${(rec && rec.history && rec.history.length) || 0} pesan dimuat)`));
|
|
171
|
+
if (!cfg.apiKey) {
|
|
172
|
+
console.log(`${ui.systemTag()} login dulu yuk biar bisa lanjut chatting.`);
|
|
173
|
+
await runLogin(cfg, null);
|
|
174
|
+
cfg = config.load();
|
|
175
|
+
}
|
|
176
|
+
// skip banner di runInteractive karena udah di-print
|
|
177
|
+
const interactiveArgs = { ...args, flags: { ...args.flags, noBanner: true } };
|
|
178
|
+
await runInteractive(cfg, interactiveArgs);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (sub === 'config') {
|
|
182
|
+
const ctx = { config: cfg, sessionId: args.flags.session || cfg.lastSession || null };
|
|
183
|
+
await commands.dispatch(ctx, '/config', {});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Kalau belum punya API key — jalanin login interaktif dulu.
|
|
188
|
+
if (!cfg.apiKey) {
|
|
189
|
+
if (!args.flags.noBanner) console.log(ui.banner());
|
|
190
|
+
console.log(`${ui.systemTag()} kayaknya kamu belum login. Yuk masukin API key dulu.`);
|
|
191
|
+
await runLogin(cfg, null);
|
|
192
|
+
cfg = config.load();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// One-shot mode lewat -p.
|
|
196
|
+
if (args.flags.prompt) {
|
|
197
|
+
const client = makeClient(cfg);
|
|
198
|
+
const sessionId = args.flags.session || null;
|
|
199
|
+
await runPrompt(cfg, client, args.flags.prompt, sessionId);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await runInteractive(cfg, args);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runLogin(cfg, maybeKey) {
|
|
207
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
208
|
+
try {
|
|
209
|
+
let key = maybeKey;
|
|
210
|
+
if (!key) {
|
|
211
|
+
key = await askMasked(rl, `${ui.systemTag()} masukin API key (zel_…): `, { mask: true });
|
|
212
|
+
}
|
|
213
|
+
key = (key || '').trim();
|
|
214
|
+
if (!key) {
|
|
215
|
+
console.log(`${ui.errorTag()} API key kosong, dibatalin.`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
config.update({ apiKey: key });
|
|
219
|
+
console.log(`${ui.okTag()} tersimpan di ${ui.chalk.gray(config.configPath())}`);
|
|
220
|
+
} finally {
|
|
221
|
+
rl.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runOneShot(fn) {
|
|
226
|
+
try {
|
|
227
|
+
await fn();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
handleError(err);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function commandStatus(client) {
|
|
235
|
+
const spinner = ora({ text: 'ngecek status…', color: 'cyan' }).start();
|
|
236
|
+
try {
|
|
237
|
+
const data = await client.getStatus();
|
|
238
|
+
spinner.stop();
|
|
239
|
+
console.log(ui.pretty(data));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
spinner.stop();
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function commandUsage(client, key) {
|
|
247
|
+
const spinner = ora({ text: 'narik usage…', color: 'cyan' }).start();
|
|
248
|
+
try {
|
|
249
|
+
const data = await client.getUsage(key);
|
|
250
|
+
spinner.stop();
|
|
251
|
+
console.log(ui.pretty(data));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
spinner.stop();
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function commandActivity(client, key) {
|
|
259
|
+
const spinner = ora({ text: 'narik activity logs…', color: 'cyan' }).start();
|
|
260
|
+
try {
|
|
261
|
+
const data = await client.getActivityLogs(key);
|
|
262
|
+
spinner.stop();
|
|
263
|
+
console.log(ui.pretty(data));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
spinner.stop();
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function listSessions() {
|
|
271
|
+
const rows = session.list();
|
|
272
|
+
console.log(ui.hr());
|
|
273
|
+
console.log(ui.chalk.bold('Sessions tersimpan'));
|
|
274
|
+
console.log(ui.chalk.gray('dir: ' + session.sessionDir()));
|
|
275
|
+
if (rows.length === 0) {
|
|
276
|
+
console.log(ui.chalk.gray('(belum ada sesi)'));
|
|
277
|
+
console.log(ui.hr());
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
rows.forEach((r, i) => {
|
|
281
|
+
const when = r.updatedAt ? new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 19) : '—';
|
|
282
|
+
console.log(
|
|
283
|
+
ui.chalk.gray(`#${String(i + 1).padStart(2)} `) +
|
|
284
|
+
ui.chalk.cyan(r.id.padEnd(28)) +
|
|
285
|
+
ui.chalk.gray(' · ') +
|
|
286
|
+
ui.chalk.gray(`${String(r.turns).padStart(3)} turns`) +
|
|
287
|
+
ui.chalk.gray(' · ') +
|
|
288
|
+
ui.chalk.gray(when) +
|
|
289
|
+
(r.preview ? ui.chalk.gray(' — ' + r.preview) : '')
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
console.log(ui.hr());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function runPrompt(cfg, client, prompt, sessionId) {
|
|
296
|
+
const spinner = ora({ text: ' mikir…', color: 'magenta' }).start();
|
|
297
|
+
try {
|
|
298
|
+
const res = await client.sendAgent({
|
|
299
|
+
messages: [{ role: 'user', content: prompt }],
|
|
300
|
+
model: cfg.model,
|
|
301
|
+
endpoint: cfg.endpoint,
|
|
302
|
+
system: cfg.system,
|
|
303
|
+
sessionId: sessionId || null,
|
|
304
|
+
});
|
|
305
|
+
spinner.stop();
|
|
306
|
+
console.log(ui.renderMarkdown(res.reply || '(gak ada balasan)'));
|
|
307
|
+
|
|
308
|
+
const sid = res.sessionId || sessionId || session.localId();
|
|
309
|
+
session.append(sid, { role: 'user', content: prompt }, {
|
|
310
|
+
model: cfg.model, endpoint: cfg.endpoint, system: cfg.system,
|
|
311
|
+
});
|
|
312
|
+
session.append(sid, { role: 'assistant', content: res.reply || '' });
|
|
313
|
+
if (sessionId && res.sessionId && sessionId !== res.sessionId) {
|
|
314
|
+
session.rename(sessionId, res.sessionId);
|
|
315
|
+
}
|
|
316
|
+
config.update({ lastSession: sid });
|
|
317
|
+
console.log(ui.chalk.gray(`\nsession: ${sid}`));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
spinner.stop();
|
|
320
|
+
handleError(err);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function runInteractive(cfg, args) {
|
|
326
|
+
if (!args.flags.noBanner) {
|
|
327
|
+
console.log(ui.banner());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const client = makeClient(cfg);
|
|
331
|
+
const ctx = {
|
|
332
|
+
config: cfg,
|
|
333
|
+
client,
|
|
334
|
+
history: [],
|
|
335
|
+
sessionId: args.flags.session || cfg.lastSession || null,
|
|
336
|
+
rl: null,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Auto-reload history dari file sesi sebelumnya kalau ada.
|
|
340
|
+
if (ctx.sessionId) {
|
|
341
|
+
const rec = session.load(ctx.sessionId);
|
|
342
|
+
if (rec && Array.isArray(rec.history) && rec.history.length > 0) {
|
|
343
|
+
ctx.history = rec.history.map(({ role, content }) => ({ role, content }));
|
|
344
|
+
console.log(`${ui.infoTag()} lanjutin sesi ${ui.chalk.cyan(ctx.sessionId)} ` +
|
|
345
|
+
ui.chalk.gray(`(${rec.history.length} pesan dimuat dari ./session)`));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(`${ui.infoTag()} pakai session id ${ui.chalk.cyan(ctx.sessionId)} ${ui.chalk.gray('(belum ada history lokal)')}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Optional: warn early kalau quota deket habis (best-effort, gak nge-block).
|
|
352
|
+
warnQuotaSoft(client, cfg.apiKey).catch(() => {});
|
|
353
|
+
|
|
354
|
+
const rl = readline.createInterface({
|
|
355
|
+
input: process.stdin,
|
|
356
|
+
output: process.stdout,
|
|
357
|
+
historySize: 200,
|
|
358
|
+
terminal: true,
|
|
359
|
+
});
|
|
360
|
+
ctx.rl = rl;
|
|
361
|
+
|
|
362
|
+
const helpers = {
|
|
363
|
+
ask: (q, opts) => askMasked(rl, q, opts),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const promptFn = () => {
|
|
367
|
+
rl.setPrompt(ui.userPrompt(ctx.config.model));
|
|
368
|
+
rl.prompt();
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
rl.on('SIGINT', () => {
|
|
372
|
+
console.log(`\n${ui.infoTag()} pakai ${ui.chalk.yellow('/exit')} buat keluar, atau Ctrl+D.`);
|
|
373
|
+
promptFn();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
rl.on('close', () => {
|
|
377
|
+
console.log(`\n${ui.infoTag()} bye! 👋`);
|
|
378
|
+
process.exit(0);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
promptFn();
|
|
382
|
+
|
|
383
|
+
rl.on('line', async (raw) => {
|
|
384
|
+
const line = raw.replace(/\r$/, '').trim();
|
|
385
|
+
if (!line) {
|
|
386
|
+
promptFn();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (line.startsWith('/')) {
|
|
391
|
+
try {
|
|
392
|
+
const handled = await commands.dispatch(ctx, line, helpers);
|
|
393
|
+
if (!handled) {
|
|
394
|
+
await handleChat(ctx, line);
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
handleError(err);
|
|
398
|
+
}
|
|
399
|
+
promptFn();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await handleChat(ctx, line);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
handleError(err);
|
|
407
|
+
}
|
|
408
|
+
promptFn();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function handleChat(ctx, content) {
|
|
413
|
+
ctx.history.push({ role: 'user', content });
|
|
414
|
+
|
|
415
|
+
// Strategi: kalau ada session id, kirim cuma user message terbaru — server
|
|
416
|
+
// yang ngurus history. Kalau belum, sertain tail history terakhir biar AI
|
|
417
|
+
// tetap punya konteks.
|
|
418
|
+
let messages;
|
|
419
|
+
if (ctx.sessionId) {
|
|
420
|
+
messages = [{ role: 'user', content }];
|
|
421
|
+
} else {
|
|
422
|
+
messages = ctx.history.slice(-12);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const spinner = ora({ text: ' mikir…', color: 'magenta' }).start();
|
|
426
|
+
let res;
|
|
427
|
+
try {
|
|
428
|
+
res = await ctx.client.sendAgent({
|
|
429
|
+
messages,
|
|
430
|
+
model: ctx.config.model,
|
|
431
|
+
endpoint: ctx.config.endpoint,
|
|
432
|
+
system: ctx.config.system,
|
|
433
|
+
sessionId: ctx.sessionId,
|
|
434
|
+
});
|
|
435
|
+
} catch (err) {
|
|
436
|
+
spinner.stop();
|
|
437
|
+
ctx.history.pop();
|
|
438
|
+
throw err;
|
|
439
|
+
}
|
|
440
|
+
spinner.stop();
|
|
441
|
+
|
|
442
|
+
const reply = res.reply || '(gak ada balasan)';
|
|
443
|
+
ctx.history.push({ role: 'assistant', content: reply });
|
|
444
|
+
|
|
445
|
+
// Persist ke ./session/<id>.json
|
|
446
|
+
const previousId = ctx.sessionId;
|
|
447
|
+
const newId = res.sessionId || ctx.sessionId || session.localId();
|
|
448
|
+
if (previousId && res.sessionId && previousId !== res.sessionId) {
|
|
449
|
+
session.rename(previousId, res.sessionId);
|
|
450
|
+
}
|
|
451
|
+
ctx.sessionId = newId;
|
|
452
|
+
session.append(newId, { role: 'user', content }, {
|
|
453
|
+
model: ctx.config.model,
|
|
454
|
+
endpoint: ctx.config.endpoint,
|
|
455
|
+
system: ctx.config.system,
|
|
456
|
+
});
|
|
457
|
+
session.append(newId, { role: 'assistant', content: reply });
|
|
458
|
+
config.update({ lastSession: newId });
|
|
459
|
+
|
|
460
|
+
console.log(`${ui.assistantTag(ctx.config.model)}`);
|
|
461
|
+
console.log(ui.renderMarkdown(reply));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function warnQuotaSoft(client, apiKey) {
|
|
465
|
+
if (!apiKey) return;
|
|
466
|
+
let usage;
|
|
467
|
+
try {
|
|
468
|
+
usage = await client.getUsage(apiKey);
|
|
469
|
+
} catch {
|
|
470
|
+
return; // soft check — diem aja kalau error
|
|
471
|
+
}
|
|
472
|
+
const info = extractQuotaInfo(usage);
|
|
473
|
+
if (!info) return;
|
|
474
|
+
if (info.exhausted) {
|
|
475
|
+
console.log(ui.limitWarning(info));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (info.used !== undefined && info.limit && info.limit > 0) {
|
|
479
|
+
const ratio = info.used / info.limit;
|
|
480
|
+
if (ratio >= 0.9) {
|
|
481
|
+
console.log(`${ui.warnTag()} pemakaian udah ${(ratio * 100).toFixed(0)}% (${info.used}/${info.limit}). Hati-hati ya.`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function extractQuotaInfo(data) {
|
|
487
|
+
if (!data || typeof data !== 'object') return null;
|
|
488
|
+
const root = data.data && typeof data.data === 'object' ? data.data : data;
|
|
489
|
+
const used = num(root.used) ?? num(root.usage) ?? num(root.count) ?? num(root.requests);
|
|
490
|
+
const limit = num(root.limit) ?? num(root.quota) ?? num(root.max);
|
|
491
|
+
const plan = root.plan || root.tier;
|
|
492
|
+
const resetAt = root.reset || root.resetAt || root.resets_at;
|
|
493
|
+
const exhausted = root.exhausted === true
|
|
494
|
+
|| (used !== undefined && limit !== undefined && used >= limit && limit > 0);
|
|
495
|
+
if (used === undefined && limit === undefined && !exhausted) return null;
|
|
496
|
+
return { used, limit, plan, resetAt, exhausted };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function num(v) {
|
|
500
|
+
if (v === undefined || v === null || v === '') return undefined;
|
|
501
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
502
|
+
return Number.isFinite(n) ? n : undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function handleError(err) {
|
|
506
|
+
if (err instanceof LimitExceededError) {
|
|
507
|
+
console.log(ui.limitWarning(err.info || {}));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
console.log(`${ui.errorTag()} ${err.message}`);
|
|
511
|
+
if (process.env.ZELAI_DEBUG && err.stack) {
|
|
512
|
+
console.log(ui.chalk.gray(err.stack));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* readline `question` dengan optional masking buat API key.
|
|
518
|
+
*/
|
|
519
|
+
function askMasked(rl, prompt, opts = {}) {
|
|
520
|
+
return new Promise((resolve) => {
|
|
521
|
+
if (!opts.mask) {
|
|
522
|
+
rl.question(prompt, (ans) => resolve(ans));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const stdin = process.stdin;
|
|
526
|
+
const stdout = process.stdout;
|
|
527
|
+
stdout.write(prompt);
|
|
528
|
+
let buf = '';
|
|
529
|
+
const onData = (chunk) => {
|
|
530
|
+
const s = chunk.toString('utf8');
|
|
531
|
+
for (const ch of s) {
|
|
532
|
+
if (ch === '\n' || ch === '\r') {
|
|
533
|
+
stdin.removeListener('data', onData);
|
|
534
|
+
stdout.write('\n');
|
|
535
|
+
resolve(buf);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (ch === '') { // Ctrl+C
|
|
539
|
+
stdin.removeListener('data', onData);
|
|
540
|
+
stdout.write('\n');
|
|
541
|
+
process.exit(130);
|
|
542
|
+
}
|
|
543
|
+
if (ch === '' || ch === '\b') {
|
|
544
|
+
if (buf.length > 0) {
|
|
545
|
+
buf = buf.slice(0, -1);
|
|
546
|
+
stdout.write('\b \b');
|
|
547
|
+
}
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
buf += ch;
|
|
551
|
+
stdout.write('•');
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
stdin.on('data', onData);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function errorExit(msg) {
|
|
559
|
+
console.log(`${ui.errorTag()} ${msg}`);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
module.exports = { main };
|