zelai-cli 1.1.0 → 1.2.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/bin/zelai.js +29 -8
- package/package.json +1 -1
- package/src/cli.js +94 -10
- package/src/client.js +1 -0
- package/src/commands.js +270 -44
- package/src/config.js +12 -1
- package/src/theme.js +71 -0
- package/src/ui.js +142 -44
- package/src/version.js +85 -0
package/bin/zelai.js
CHANGED
|
@@ -5,16 +5,37 @@
|
|
|
5
5
|
*/
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
// Track interactive mode — kalau true, error apapun jangan exit, cukup print.
|
|
9
|
+
global.__ZELAI_INTERACTIVE__ = false;
|
|
10
|
+
|
|
11
|
+
function safeStringify(err) {
|
|
12
|
+
if (!err) return 'unknown error';
|
|
13
|
+
if (err.message) return err.message;
|
|
14
|
+
try { return JSON.stringify(err); } catch { return String(err); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function logErr(label, err) {
|
|
18
|
+
let chalk;
|
|
19
|
+
try { chalk = require('chalk'); } catch {}
|
|
20
|
+
const tag = chalk ? chalk.red.bold('✖ ' + label) : '✖ ' + label;
|
|
21
|
+
const msg = safeStringify(err);
|
|
15
22
|
process.stderr.write(`${tag} ${msg}\n`);
|
|
16
23
|
if (process.env.ZELAI_DEBUG && err && err.stack) {
|
|
17
24
|
process.stderr.write(err.stack + '\n');
|
|
18
25
|
}
|
|
19
|
-
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process.on('uncaughtException', (err) => {
|
|
29
|
+
logErr('uncaught', err);
|
|
30
|
+
if (!global.__ZELAI_INTERACTIVE__) process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
process.on('unhandledRejection', (err) => {
|
|
34
|
+
logErr('rejection', err);
|
|
35
|
+
if (!global.__ZELAI_INTERACTIVE__) process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
require('../src/cli').main(process.argv.slice(2)).catch((err) => {
|
|
39
|
+
logErr('fatal', err);
|
|
40
|
+
if (!global.__ZELAI_INTERACTIVE__) process.exit(1);
|
|
20
41
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zelai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Zelai — official CLI & SDK for the Zelapi Agent API. Chat with AI right from your terminal, Discord, or Telegram. Multi-turn sessions, slash commands, limit warnings, and a built-in Discord/Telegram connector.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zelai",
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,8 @@ const config = require('./config');
|
|
|
11
11
|
const { makeClient, LimitExceededError } = require('./client');
|
|
12
12
|
const commands = require('./commands');
|
|
13
13
|
const session = require('./session');
|
|
14
|
+
const themeMod = require('./theme');
|
|
15
|
+
const versionMod = require('./version');
|
|
14
16
|
const ui = require('./ui');
|
|
15
17
|
|
|
16
18
|
const VERSION = require('../package.json').version;
|
|
@@ -105,6 +107,9 @@ async function main(argv) {
|
|
|
105
107
|
if (args.flags.endpoint) cfg.endpoint = args.flags.endpoint;
|
|
106
108
|
if (args.flags.system) cfg.system = args.flags.system;
|
|
107
109
|
|
|
110
|
+
// Apply theme yang udah dipilih user (atau default 'dark').
|
|
111
|
+
themeMod.setTheme(cfg.theme || 'dark');
|
|
112
|
+
|
|
108
113
|
const sub = args._[0];
|
|
109
114
|
|
|
110
115
|
if (sub === 'login') {
|
|
@@ -184,12 +189,18 @@ async function main(argv) {
|
|
|
184
189
|
return;
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
// Kalau belum punya API key — jalanin login interaktif dulu.
|
|
192
|
+
// Kalau belum punya API key — jalanin login interaktif dulu, terus theme picker.
|
|
193
|
+
const isFirstLogin = !cfg.apiKey;
|
|
188
194
|
if (!cfg.apiKey) {
|
|
189
195
|
if (!args.flags.noBanner) console.log(ui.banner());
|
|
190
196
|
console.log(`${ui.systemTag()} kayaknya kamu belum login. Yuk masukin API key dulu.`);
|
|
191
197
|
await runLogin(cfg, null);
|
|
192
198
|
cfg = config.load();
|
|
199
|
+
if (cfg.apiKey) {
|
|
200
|
+
await pickThemeInteractive();
|
|
201
|
+
cfg = config.load();
|
|
202
|
+
themeMod.setTheme(cfg.theme || 'dark');
|
|
203
|
+
}
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
// One-shot mode lewat -p.
|
|
@@ -227,7 +238,64 @@ async function runOneShot(fn) {
|
|
|
227
238
|
await fn();
|
|
228
239
|
} catch (err) {
|
|
229
240
|
handleError(err);
|
|
230
|
-
process.exit(1);
|
|
241
|
+
if (!global.__ZELAI_INTERACTIVE__) process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Tanya user mau theme apa setelah login pertama.
|
|
247
|
+
* Pilihan: dark (default), night.
|
|
248
|
+
*/
|
|
249
|
+
async function pickThemeInteractive() {
|
|
250
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
251
|
+
try {
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(' ' + ui.chalk.bold('🎨 Pilih warna terminal:'));
|
|
254
|
+
const themes = themeMod.list();
|
|
255
|
+
themes.forEach((name, i) => {
|
|
256
|
+
const lbl = themeMod.describe(name);
|
|
257
|
+
console.log(' ' + ui.chalk.yellow(`${i + 1}.`) + ' ' + ui.chalk.cyan.bold(name.padEnd(8)) + ui.chalk.gray('— ' + lbl));
|
|
258
|
+
});
|
|
259
|
+
const ans = await askMasked(rl, ` ${ui.systemTag()} pilih [${themes.join('/')}] (default: dark): `);
|
|
260
|
+
const choice = (ans || '').trim().toLowerCase();
|
|
261
|
+
let pick = 'dark';
|
|
262
|
+
if (themes.includes(choice)) pick = choice;
|
|
263
|
+
else if (/^\d+$/.test(choice)) {
|
|
264
|
+
const idx = parseInt(choice, 10) - 1;
|
|
265
|
+
if (themes[idx]) pick = themes[idx];
|
|
266
|
+
}
|
|
267
|
+
themeMod.setTheme(pick);
|
|
268
|
+
config.update({ theme: pick });
|
|
269
|
+
console.log(` ${ui.okTag()} theme ${ui.chalk.cyan(pick)} ${ui.chalk.gray('— ' + themeMod.describe(pick))}`);
|
|
270
|
+
console.log('');
|
|
271
|
+
} finally {
|
|
272
|
+
rl.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Cek versi terbaru dari npm registry (best-effort, async + cached 24h).
|
|
278
|
+
* Kalau current behind → print warning vulnerable.
|
|
279
|
+
*/
|
|
280
|
+
async function checkVersionWarning() {
|
|
281
|
+
const cur = VERSION;
|
|
282
|
+
const cfg = config.load();
|
|
283
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
284
|
+
let latest = cfg.latestKnownVersion;
|
|
285
|
+
const stale = !cfg.lastVersionCheck || (Date.now() - cfg.lastVersionCheck) > DAY;
|
|
286
|
+
if (stale) {
|
|
287
|
+
try {
|
|
288
|
+
const fetched = await versionMod.getLatest(3000);
|
|
289
|
+
if (fetched) {
|
|
290
|
+
latest = fetched;
|
|
291
|
+
config.update({ latestKnownVersion: fetched, lastVersionCheck: Date.now() });
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// ignored
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (latest && versionMod.isBehind(cur, latest)) {
|
|
298
|
+
console.log(ui.vulnerableWarning(cur, latest));
|
|
231
299
|
}
|
|
232
300
|
}
|
|
233
301
|
|
|
@@ -323,10 +391,16 @@ async function runPrompt(cfg, client, prompt, sessionId) {
|
|
|
323
391
|
}
|
|
324
392
|
|
|
325
393
|
async function runInteractive(cfg, args) {
|
|
394
|
+
// Mark interactive mode so global error guards in bin/zelai.js gak ngebunuh process.
|
|
395
|
+
global.__ZELAI_INTERACTIVE__ = true;
|
|
396
|
+
|
|
326
397
|
if (!args.flags.noBanner) {
|
|
327
398
|
console.log(ui.banner());
|
|
328
399
|
}
|
|
329
400
|
|
|
401
|
+
// Version check warning (vulnerable kalo behind) — async, non-blocking.
|
|
402
|
+
checkVersionWarning().catch(() => {});
|
|
403
|
+
|
|
330
404
|
const client = makeClient(cfg);
|
|
331
405
|
const ctx = {
|
|
332
406
|
config: cfg,
|
|
@@ -334,6 +408,7 @@ async function runInteractive(cfg, args) {
|
|
|
334
408
|
history: [],
|
|
335
409
|
sessionId: args.flags.session || cfg.lastSession || null,
|
|
336
410
|
rl: null,
|
|
411
|
+
connectors: {},
|
|
337
412
|
};
|
|
338
413
|
|
|
339
414
|
// Auto-reload history dari file sesi sebelumnya kalau ada.
|
|
@@ -364,7 +439,10 @@ async function runInteractive(cfg, args) {
|
|
|
364
439
|
};
|
|
365
440
|
|
|
366
441
|
const promptFn = () => {
|
|
367
|
-
|
|
442
|
+
const modelLabel = ctx.config.modelDisabled ? 'auto' : ctx.config.model;
|
|
443
|
+
// Chat box: tulis baris atas dulu, terus prompt
|
|
444
|
+
console.log(ui.userBoxOpen(modelLabel, { model: modelLabel }));
|
|
445
|
+
rl.setPrompt(ui.userPrompt(modelLabel, { disabledHint: ctx.config.modelDisabled || ctx.config.systemDisabled }));
|
|
368
446
|
rl.prompt();
|
|
369
447
|
};
|
|
370
448
|
|
|
@@ -387,6 +465,7 @@ async function runInteractive(cfg, args) {
|
|
|
387
465
|
return;
|
|
388
466
|
}
|
|
389
467
|
|
|
468
|
+
// SEMUA branch wrapped — REPL never exit on error.
|
|
390
469
|
if (line.startsWith('/')) {
|
|
391
470
|
try {
|
|
392
471
|
const handled = await commands.dispatch(ctx, line, helpers);
|
|
@@ -427,15 +506,17 @@ async function handleChat(ctx, content) {
|
|
|
427
506
|
try {
|
|
428
507
|
res = await ctx.client.sendAgent({
|
|
429
508
|
messages,
|
|
430
|
-
|
|
509
|
+
// Kalau di-disable, kirim null biar payload gak include field-nya.
|
|
510
|
+
model: ctx.config.modelDisabled ? null : ctx.config.model,
|
|
431
511
|
endpoint: ctx.config.endpoint,
|
|
432
|
-
system: ctx.config.system,
|
|
512
|
+
system: ctx.config.systemDisabled ? null : ctx.config.system,
|
|
433
513
|
sessionId: ctx.sessionId,
|
|
434
514
|
});
|
|
435
515
|
} catch (err) {
|
|
436
516
|
spinner.stop();
|
|
437
517
|
ctx.history.pop();
|
|
438
|
-
|
|
518
|
+
handleError(err);
|
|
519
|
+
return; // INTERACTIVE: gak exit, balik ke prompt
|
|
439
520
|
}
|
|
440
521
|
spinner.stop();
|
|
441
522
|
|
|
@@ -450,15 +531,18 @@ async function handleChat(ctx, content) {
|
|
|
450
531
|
}
|
|
451
532
|
ctx.sessionId = newId;
|
|
452
533
|
session.append(newId, { role: 'user', content }, {
|
|
453
|
-
model: ctx.config.model,
|
|
534
|
+
model: ctx.config.modelDisabled ? null : ctx.config.model,
|
|
454
535
|
endpoint: ctx.config.endpoint,
|
|
455
|
-
system: ctx.config.system,
|
|
536
|
+
system: ctx.config.systemDisabled ? null : ctx.config.system,
|
|
456
537
|
});
|
|
457
538
|
session.append(newId, { role: 'assistant', content: reply });
|
|
458
539
|
config.update({ lastSession: newId });
|
|
459
540
|
|
|
460
|
-
|
|
461
|
-
|
|
541
|
+
// Render assistant bubble.
|
|
542
|
+
const modelLabel = ctx.config.modelDisabled ? 'auto' : ctx.config.model;
|
|
543
|
+
console.log(ui.assistantBoxOpen(modelLabel));
|
|
544
|
+
console.log(ui.gutterIndent(ui.renderMarkdown(reply)));
|
|
545
|
+
console.log(ui.assistantBoxClose());
|
|
462
546
|
}
|
|
463
547
|
|
|
464
548
|
async function warnQuotaSoft(client, apiKey) {
|
package/src/client.js
CHANGED
|
@@ -90,6 +90,7 @@ function makeClient(config) {
|
|
|
90
90
|
endpoint: endpoint || '/ai/claila',
|
|
91
91
|
messages: msgs,
|
|
92
92
|
};
|
|
93
|
+
// model & system di-omit dari payload kalau null/empty (lihat /model:disable, /system:disable).
|
|
93
94
|
if (model) payload.model = model;
|
|
94
95
|
if (system) payload.system = system;
|
|
95
96
|
|
package/src/commands.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slash command registry.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Parser support 2 syntax:
|
|
5
|
+
* /cmd arg1 arg2 — normal
|
|
6
|
+
* /cmd:arg1 arg2 arg3 — colon-form (e.g. /system:disable, /telegram:<token> <chatid>)
|
|
7
|
+
*
|
|
8
|
+
* Setiap handler dipanggil dengan (ctx, args, helpers).
|
|
9
|
+
* ctx : session state dari cli.js (config, history, sessionId, client, rl, …)
|
|
10
|
+
* args : string sisa setelah nama command
|
|
11
|
+
* helpers : { ask, …, spawnTelegram, spawnDiscord }
|
|
4
12
|
*/
|
|
5
13
|
'use strict';
|
|
6
14
|
|
|
@@ -8,27 +16,38 @@ const ora = require('ora');
|
|
|
8
16
|
const ui = require('./ui');
|
|
9
17
|
const config = require('./config');
|
|
10
18
|
const session = require('./session');
|
|
19
|
+
const versionMod = require('./version');
|
|
20
|
+
const themeMod = require('./theme');
|
|
11
21
|
const { LimitExceededError } = require('./client');
|
|
12
22
|
|
|
13
23
|
const HELP_ROWS = [
|
|
14
|
-
['/help',
|
|
15
|
-
['/status',
|
|
16
|
-
['/activity',
|
|
17
|
-
['/usage',
|
|
18
|
-
['/model',
|
|
19
|
-
['/
|
|
20
|
-
['/
|
|
21
|
-
['/
|
|
22
|
-
['/
|
|
23
|
-
['/
|
|
24
|
-
['/
|
|
25
|
-
['/
|
|
26
|
-
['/
|
|
27
|
-
['/
|
|
28
|
-
['/
|
|
29
|
-
['/
|
|
30
|
-
['/
|
|
31
|
-
['/
|
|
24
|
+
['/help', 'tampilin daftar command'],
|
|
25
|
+
['/status', 'cek status server (/api/status)'],
|
|
26
|
+
['/activity', 'tampilin activity logs akun kamu'],
|
|
27
|
+
['/usage', 'tampilin pemakaian / kuota API key'],
|
|
28
|
+
['/model', 'ganti model: /model opus|sonnet|haiku'],
|
|
29
|
+
['/model:disable', 'matikan model di payload (server default)'],
|
|
30
|
+
['/model:enable', 'nyalain lagi model di payload'],
|
|
31
|
+
['/endpoint', 'ganti AI endpoint, mis: /ai/claila, /ai/spawn'],
|
|
32
|
+
['/system', 'set system prompt; kosong → tampilin current'],
|
|
33
|
+
['/system:disable', 'matikan system prompt di payload'],
|
|
34
|
+
['/system:enable', 'nyalain lagi system prompt di payload'],
|
|
35
|
+
['/theme', 'ganti tema: /theme dark|night'],
|
|
36
|
+
['/new', 'mulai sesi baru (lupain history)'],
|
|
37
|
+
['/session', 'tampilin / set session ID secara manual'],
|
|
38
|
+
['/sessions', 'list semua sesi di folder ./session'],
|
|
39
|
+
['/load', 'load sesi tersimpan: /load <id|nomor>'],
|
|
40
|
+
['/forget', 'hapus sesi tersimpan: /forget <id|nomor|all>'],
|
|
41
|
+
['/save', 'paksa simpan sesi sekarang ke ./session'],
|
|
42
|
+
['/telegram:<token>','konek bot Telegram: /telegram:<TOKEN> [chatid]'],
|
|
43
|
+
['/discord:<token>', 'konek bot Discord: /discord:<TOKEN>'],
|
|
44
|
+
['/disconnect', 'matiin semua connector aktif'],
|
|
45
|
+
['/update', 'update zelai-cli ke versi terbaru lewat npm'],
|
|
46
|
+
['/login', 'set / ganti API key (zel_…)'],
|
|
47
|
+
['/logout', 'hapus API key tersimpan'],
|
|
48
|
+
['/config', 'tampilin lokasi & isi config'],
|
|
49
|
+
['/clear', 'bersihin layar'],
|
|
50
|
+
['/exit', 'keluar (alias: /quit, /q)'],
|
|
32
51
|
];
|
|
33
52
|
|
|
34
53
|
async function cmdHelp() {
|
|
@@ -36,7 +55,7 @@ async function cmdHelp() {
|
|
|
36
55
|
console.log(ui.chalk.bold('Slash Commands'));
|
|
37
56
|
console.log('');
|
|
38
57
|
for (const [k, v] of HELP_ROWS) {
|
|
39
|
-
console.log(' ' + ui.chalk.yellow(k.padEnd(
|
|
58
|
+
console.log(' ' + ui.chalk.yellow(k.padEnd(20)) + ui.chalk.gray('· ') + ui.chalk.white(v));
|
|
40
59
|
}
|
|
41
60
|
console.log('');
|
|
42
61
|
console.log(ui.chalk.gray(' Tip: ketik biasa tanpa "/" buat ngobrol sama AI.'));
|
|
@@ -49,14 +68,14 @@ async function cmdStatus(ctx) {
|
|
|
49
68
|
const data = await ctx.client.getStatus();
|
|
50
69
|
spinner.stop();
|
|
51
70
|
console.log(ui.hr());
|
|
52
|
-
console.log(ui.chalk.bold('Server Status') + ui.chalk.gray(' · ') + ui.
|
|
71
|
+
console.log(ui.chalk.bold('Server Status') + ui.chalk.gray(' · ') + ui.theme.current().link('https://zelapioffciall.dpdns.org/api/status'));
|
|
53
72
|
console.log('');
|
|
54
73
|
if (typeof data === 'object' && data !== null) {
|
|
55
|
-
const known = ['status', 'state', 'health', 'uptime', 'version', 'region', 'latency', 'node', 'env'];
|
|
74
|
+
const known = ['status', 'state', 'health', 'uptime', 'version', 'region', 'latency', 'node', 'env', 'runtime'];
|
|
56
75
|
const root = data.data && typeof data.data === 'object' ? data.data : data;
|
|
57
76
|
let printed = 0;
|
|
58
77
|
for (const k of known) {
|
|
59
|
-
if (root[k] !== undefined) {
|
|
78
|
+
if (root[k] !== undefined && typeof root[k] !== 'object') {
|
|
60
79
|
const v = root[k];
|
|
61
80
|
const value = (k === 'status' || k === 'state' || k === 'health')
|
|
62
81
|
? (/ok|up|online|healthy|stable/i.test(String(v)) ? ui.chalk.green.bold(String(v)) : ui.chalk.yellow(String(v)))
|
|
@@ -65,13 +84,18 @@ async function cmdStatus(ctx) {
|
|
|
65
84
|
printed++;
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
} else {
|
|
87
|
+
if (Array.isArray(root.targets) || Array.isArray(data.targets)) {
|
|
88
|
+
const targets = root.targets || data.targets;
|
|
71
89
|
console.log('');
|
|
72
|
-
console.log(ui.chalk.
|
|
73
|
-
|
|
90
|
+
console.log(' ' + ui.chalk.bold('targets:'));
|
|
91
|
+
for (const t of targets) {
|
|
92
|
+
const dot = t.online ? ui.chalk.green('●') : ui.chalk.red('●');
|
|
93
|
+
console.log(' ' + dot + ' ' + ui.chalk.white((t.name || t.id || '').padEnd(20)) +
|
|
94
|
+
ui.chalk.gray(' · ping ') + ui.chalk.cyan(String(t.ping ?? '—') + 'ms'));
|
|
95
|
+
}
|
|
96
|
+
printed++;
|
|
74
97
|
}
|
|
98
|
+
if (printed === 0) console.log(ui.chalk.gray(ui.pretty(data)));
|
|
75
99
|
} else {
|
|
76
100
|
console.log(String(data));
|
|
77
101
|
}
|
|
@@ -155,15 +179,26 @@ function maybeShowQuotaBar(flat) {
|
|
|
155
179
|
|
|
156
180
|
async function cmdModel(ctx, args) {
|
|
157
181
|
const next = (args || '').trim().toLowerCase();
|
|
182
|
+
if (next === 'disable') {
|
|
183
|
+
ctx.config = config.update({ modelDisabled: true });
|
|
184
|
+
console.log(`${ui.okTag()} model param dimatikan — payload bakal di-kirim tanpa field model.`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (next === 'enable') {
|
|
188
|
+
ctx.config = config.update({ modelDisabled: false });
|
|
189
|
+
console.log(`${ui.okTag()} model param dinyalain lagi — sekarang pake ${ui.chalk.cyan(ctx.config.model)}.`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
158
192
|
if (!next) {
|
|
159
|
-
|
|
193
|
+
const status = ctx.config.modelDisabled ? ui.chalk.gray('(disabled)') : '';
|
|
194
|
+
console.log(`${ui.infoTag()} model: ${ui.chalk.cyan(ctx.config.model)} ${status} ${ui.chalk.gray('— pilihan: opus, sonnet, haiku · /model:disable buat matiin')}`);
|
|
160
195
|
return;
|
|
161
196
|
}
|
|
162
197
|
if (!['opus', 'sonnet', 'haiku'].includes(next)) {
|
|
163
198
|
console.log(`${ui.errorTag()} model gak dikenal. Pilih: opus | sonnet | haiku`);
|
|
164
199
|
return;
|
|
165
200
|
}
|
|
166
|
-
ctx.config = config.update({ model: next });
|
|
201
|
+
ctx.config = config.update({ model: next, modelDisabled: false });
|
|
167
202
|
console.log(`${ui.okTag()} model diganti ke ${ui.chalk.cyan(next)}`);
|
|
168
203
|
}
|
|
169
204
|
|
|
@@ -183,15 +218,41 @@ async function cmdEndpoint(ctx, args) {
|
|
|
183
218
|
|
|
184
219
|
async function cmdSystem(ctx, args) {
|
|
185
220
|
const next = (args || '').trim();
|
|
221
|
+
if (next === 'disable') {
|
|
222
|
+
ctx.config = config.update({ systemDisabled: true });
|
|
223
|
+
console.log(`${ui.okTag()} system prompt dimatikan — payload bakal di-kirim tanpa field system.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (next === 'enable') {
|
|
227
|
+
ctx.config = config.update({ systemDisabled: false });
|
|
228
|
+
console.log(`${ui.okTag()} system prompt dinyalain lagi.`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
186
231
|
if (!next) {
|
|
187
|
-
|
|
232
|
+
const status = ctx.config.systemDisabled ? ui.chalk.gray('(disabled)') : '';
|
|
233
|
+
console.log(`${ui.infoTag()} system prompt ${status}:`);
|
|
188
234
|
console.log(ui.chalk.gray(' ' + (ctx.config.system || '(kosong)')));
|
|
189
235
|
return;
|
|
190
236
|
}
|
|
191
|
-
ctx.config = config.update({ system: next });
|
|
237
|
+
ctx.config = config.update({ system: next, systemDisabled: false });
|
|
192
238
|
console.log(`${ui.okTag()} system prompt updated.`);
|
|
193
239
|
}
|
|
194
240
|
|
|
241
|
+
async function cmdTheme(ctx, args) {
|
|
242
|
+
const next = (args || '').trim().toLowerCase();
|
|
243
|
+
if (!next) {
|
|
244
|
+
const cur = themeMod.current().name;
|
|
245
|
+
console.log(`${ui.infoTag()} theme sekarang: ${ui.chalk.cyan(cur)} ${ui.chalk.gray('— pilihan: ' + themeMod.list().join(', '))}`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!themeMod.setTheme(next)) {
|
|
249
|
+
console.log(`${ui.errorTag()} theme "${next}" gak dikenal. Pilih: ${themeMod.list().join(' | ')}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
ctx.config = config.update({ theme: next });
|
|
253
|
+
console.log(`${ui.okTag()} theme diganti ke ${ui.chalk.cyan(next)} ${ui.chalk.gray('— ' + themeMod.describe(next))}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
195
256
|
async function cmdNew(ctx) {
|
|
196
257
|
ctx.history = [];
|
|
197
258
|
ctx.sessionId = null;
|
|
@@ -249,7 +310,6 @@ function resolveSessionRef(ctx, ref) {
|
|
|
249
310
|
if (!ref) return null;
|
|
250
311
|
const trimmed = ref.trim();
|
|
251
312
|
if (!trimmed) return null;
|
|
252
|
-
// Number → from last listing
|
|
253
313
|
if (/^\d+$/.test(trimmed)) {
|
|
254
314
|
const idx = parseInt(trimmed, 10) - 1;
|
|
255
315
|
const rows = ctx._lastSessionsList || session.list();
|
|
@@ -357,11 +417,12 @@ async function cmdConfig(ctx) {
|
|
|
357
417
|
console.log(ui.kv('path', config.configPath()));
|
|
358
418
|
console.log(ui.kv('sessionDir', session.sessionDir()));
|
|
359
419
|
console.log(ui.kv('apiKey', ctx.config.apiKey ? maskKey(ctx.config.apiKey) : '(belum di-set)'));
|
|
360
|
-
console.log(ui.kv('
|
|
420
|
+
console.log(ui.kv('theme', ctx.config.theme || 'dark'));
|
|
421
|
+
console.log(ui.kv('model', ctx.config.modelDisabled ? '(disabled)' : ctx.config.model));
|
|
361
422
|
console.log(ui.kv('endpoint', ctx.config.endpoint));
|
|
362
423
|
console.log(ui.kv('baseUrl', ctx.config.baseUrl));
|
|
363
424
|
console.log(ui.kv('session', ctx.sessionId || '(belum ada)'));
|
|
364
|
-
console.log(ui.kv('system', truncate(ctx.config.system || '', 60)));
|
|
425
|
+
console.log(ui.kv('system', ctx.config.systemDisabled ? '(disabled)' : truncate(ctx.config.system || '', 60)));
|
|
365
426
|
console.log(ui.hr());
|
|
366
427
|
}
|
|
367
428
|
|
|
@@ -371,10 +432,154 @@ async function cmdClear() {
|
|
|
371
432
|
|
|
372
433
|
async function cmdExit(ctx) {
|
|
373
434
|
console.log(`${ui.infoTag()} bye! 👋`);
|
|
435
|
+
if (ctx.connectors) {
|
|
436
|
+
for (const c of Object.values(ctx.connectors)) {
|
|
437
|
+
try { c.child && c.child.kill(); } catch {}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
374
440
|
if (ctx.rl) ctx.rl.close();
|
|
375
441
|
process.exit(0);
|
|
376
442
|
}
|
|
377
443
|
|
|
444
|
+
/* ----- /update ----- */
|
|
445
|
+
|
|
446
|
+
async function cmdUpdate(ctx) {
|
|
447
|
+
console.log(`${ui.infoTag()} ngecek versi terbaru di npm…`);
|
|
448
|
+
const latest = await versionMod.getLatest();
|
|
449
|
+
const cur = require('../package.json').version;
|
|
450
|
+
if (!latest) {
|
|
451
|
+
console.log(`${ui.warnTag()} gak bisa ngambil versi terbaru — coba cek koneksi.`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (!versionMod.isBehind(cur, latest)) {
|
|
455
|
+
console.log(`${ui.okTag()} kamu udah pakai versi terbaru: ${ui.chalk.green.bold('v' + cur)}.`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
console.log(`${ui.infoTag()} update ${ui.chalk.gray('v' + cur)} → ${ui.chalk.green.bold('v' + latest)}`);
|
|
459
|
+
console.log(ui.chalk.gray(' running: npm i -g zelai-cli@latest'));
|
|
460
|
+
console.log('');
|
|
461
|
+
const result = await versionMod.runUpdate((line) => {
|
|
462
|
+
if (line) console.log(ui.chalk.gray(' ' + line));
|
|
463
|
+
});
|
|
464
|
+
console.log('');
|
|
465
|
+
if (result.ok) {
|
|
466
|
+
console.log(`${ui.okTag()} update selesai. Restart ${ui.chalk.yellow('zelai')} buat pake versi baru.`);
|
|
467
|
+
} else {
|
|
468
|
+
console.log(`${ui.errorTag()} update gagal (exit ${result.code}). Coba manual: ${ui.chalk.yellow('npm i -g zelai-cli@latest')}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ----- connectors (in-process spawn) ----- */
|
|
473
|
+
|
|
474
|
+
async function cmdTelegram(ctx, args) {
|
|
475
|
+
// args = "<token> [chatid]" — sebelumnya colon-form udah di-flatten parser.
|
|
476
|
+
const parts = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
477
|
+
const token = parts[0];
|
|
478
|
+
const chatId = parts[1];
|
|
479
|
+
if (!token || !token.includes(':')) {
|
|
480
|
+
console.log(`${ui.errorTag()} format: ${ui.chalk.yellow('/telegram:<BOT_TOKEN> [chat_id]')}`);
|
|
481
|
+
console.log(ui.chalk.gray(' contoh: /telegram:1234567:AAAA…token 987654321'));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!ctx.config.apiKey) {
|
|
485
|
+
console.log(`${ui.errorTag()} login dulu (/login zel_…)`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!ctx.connectors) ctx.connectors = {};
|
|
489
|
+
if (ctx.connectors.telegram && !ctx.connectors.telegram.child.killed) {
|
|
490
|
+
console.log(`${ui.warnTag()} connector Telegram udah jalan — pakai /disconnect dulu.`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const { spawn } = require('child_process');
|
|
495
|
+
const path = require('path');
|
|
496
|
+
const env = {
|
|
497
|
+
...process.env,
|
|
498
|
+
ZELAPI_KEY: ctx.config.apiKey,
|
|
499
|
+
TELEGRAM_TOKEN: token,
|
|
500
|
+
ZELAI_MODEL: ctx.config.modelDisabled ? '' : ctx.config.model,
|
|
501
|
+
ZELAI_ENDPOINT: ctx.config.endpoint,
|
|
502
|
+
ZELAI_SYSTEM: ctx.config.systemDisabled ? '' : ctx.config.system,
|
|
503
|
+
};
|
|
504
|
+
if (chatId) env.TELEGRAM_ALLOW = chatId;
|
|
505
|
+
|
|
506
|
+
const script = path.join(__dirname, '..', 'connector', 'telegram.js');
|
|
507
|
+
const child = spawn(process.execPath, [script], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
508
|
+
ctx.connectors.telegram = { child, token: maskKey(token), chatId };
|
|
509
|
+
|
|
510
|
+
console.log(`${ui.infoTag()} starting Telegram connector ${ui.chalk.gray('pid=' + child.pid)} ${ui.chalk.gray('token=' + maskKey(token))}${chatId ? ui.chalk.gray(' allow=' + chatId) : ''}`);
|
|
511
|
+
|
|
512
|
+
const prefix = ui.chalk.magenta('[telegram] ');
|
|
513
|
+
child.stdout.on('data', (b) => process.stdout.write(prefix + b.toString().replace(/\n(?!$)/g, '\n' + prefix)));
|
|
514
|
+
child.stderr.on('data', (b) => process.stdout.write(prefix + ui.chalk.red(b.toString()).replace(/\n(?!$)/g, '\n' + prefix)));
|
|
515
|
+
child.on('close', (code) => {
|
|
516
|
+
console.log(`${ui.infoTag()} Telegram connector exit (code ${code})`);
|
|
517
|
+
if (ctx.connectors) delete ctx.connectors.telegram;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function cmdDiscord(ctx, args) {
|
|
522
|
+
const parts = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
523
|
+
const token = parts[0];
|
|
524
|
+
if (!token) {
|
|
525
|
+
console.log(`${ui.errorTag()} format: ${ui.chalk.yellow('/discord:<BOT_TOKEN>')}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!ctx.config.apiKey) {
|
|
529
|
+
console.log(`${ui.errorTag()} login dulu (/login zel_…)`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (!ctx.connectors) ctx.connectors = {};
|
|
533
|
+
if (ctx.connectors.discord && !ctx.connectors.discord.child.killed) {
|
|
534
|
+
console.log(`${ui.warnTag()} connector Discord udah jalan — pakai /disconnect dulu.`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const { spawn } = require('child_process');
|
|
539
|
+
const path = require('path');
|
|
540
|
+
const env = {
|
|
541
|
+
...process.env,
|
|
542
|
+
ZELAPI_KEY: ctx.config.apiKey,
|
|
543
|
+
DISCORD_TOKEN: token,
|
|
544
|
+
ZELAI_MODEL: ctx.config.modelDisabled ? '' : ctx.config.model,
|
|
545
|
+
ZELAI_ENDPOINT: ctx.config.endpoint,
|
|
546
|
+
ZELAI_SYSTEM: ctx.config.systemDisabled ? '' : ctx.config.system,
|
|
547
|
+
};
|
|
548
|
+
const script = path.join(__dirname, '..', 'connector', 'discord.js');
|
|
549
|
+
const child = spawn(process.execPath, [script], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
550
|
+
ctx.connectors.discord = { child, token: maskKey(token) };
|
|
551
|
+
|
|
552
|
+
console.log(`${ui.infoTag()} starting Discord connector ${ui.chalk.gray('pid=' + child.pid)} ${ui.chalk.gray('token=' + maskKey(token))}`);
|
|
553
|
+
|
|
554
|
+
const prefix = ui.chalk.cyan('[discord] ');
|
|
555
|
+
child.stdout.on('data', (b) => process.stdout.write(prefix + b.toString().replace(/\n(?!$)/g, '\n' + prefix)));
|
|
556
|
+
child.stderr.on('data', (b) => process.stdout.write(prefix + ui.chalk.red(b.toString()).replace(/\n(?!$)/g, '\n' + prefix)));
|
|
557
|
+
child.on('close', (code) => {
|
|
558
|
+
console.log(`${ui.infoTag()} Discord connector exit (code ${code})`);
|
|
559
|
+
if (ctx.connectors) delete ctx.connectors.discord;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function cmdDisconnect(ctx, args) {
|
|
564
|
+
const target = (args || '').trim().toLowerCase();
|
|
565
|
+
if (!ctx.connectors || Object.keys(ctx.connectors).length === 0) {
|
|
566
|
+
console.log(`${ui.infoTag()} gak ada connector yang aktif.`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
let killed = 0;
|
|
570
|
+
for (const [name, info] of Object.entries(ctx.connectors)) {
|
|
571
|
+
if (target && target !== name) continue;
|
|
572
|
+
try { info.child.kill(); killed++; } catch {}
|
|
573
|
+
}
|
|
574
|
+
if (killed === 0) {
|
|
575
|
+
console.log(`${ui.warnTag()} gak ada yang dimatiin ${target ? `(target "${target}")` : ''}.`);
|
|
576
|
+
} else {
|
|
577
|
+
console.log(`${ui.okTag()} ${killed} connector dimatiin.`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* ----- registry ----- */
|
|
582
|
+
|
|
378
583
|
const REGISTRY = {
|
|
379
584
|
'/help': cmdHelp,
|
|
380
585
|
'/?': cmdHelp,
|
|
@@ -386,6 +591,7 @@ const REGISTRY = {
|
|
|
386
591
|
'/model': cmdModel,
|
|
387
592
|
'/endpoint': cmdEndpoint,
|
|
388
593
|
'/system': cmdSystem,
|
|
594
|
+
'/theme': cmdTheme,
|
|
389
595
|
'/new': cmdNew,
|
|
390
596
|
'/reset': cmdNew,
|
|
391
597
|
'/session': cmdSession,
|
|
@@ -394,6 +600,10 @@ const REGISTRY = {
|
|
|
394
600
|
'/load': cmdLoad,
|
|
395
601
|
'/forget': cmdForget,
|
|
396
602
|
'/save': cmdSave,
|
|
603
|
+
'/telegram': cmdTelegram,
|
|
604
|
+
'/discord': cmdDiscord,
|
|
605
|
+
'/disconnect': cmdDisconnect,
|
|
606
|
+
'/update': cmdUpdate,
|
|
397
607
|
'/login': cmdLogin,
|
|
398
608
|
'/logout': cmdLogout,
|
|
399
609
|
'/config': cmdConfig,
|
|
@@ -404,13 +614,28 @@ const REGISTRY = {
|
|
|
404
614
|
'/q': cmdExit,
|
|
405
615
|
};
|
|
406
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Parse a slash command.
|
|
619
|
+
*
|
|
620
|
+
* /cmd → { name: '/cmd', args: '' }
|
|
621
|
+
* /cmd foo bar → { name: '/cmd', args: 'foo bar' }
|
|
622
|
+
* /cmd:foo → { name: '/cmd', args: 'foo' }
|
|
623
|
+
* /cmd:foo:bar → { name: '/cmd', args: 'foo:bar' }
|
|
624
|
+
* /cmd:foo:bar baz qux → { name: '/cmd', args: 'foo:bar baz qux' }
|
|
625
|
+
*/
|
|
407
626
|
function parse(line) {
|
|
408
627
|
const trimmed = line.trim();
|
|
409
628
|
if (!trimmed.startsWith('/')) return null;
|
|
629
|
+
// /word:rest... → split di colon pertama
|
|
630
|
+
const m = trimmed.match(/^\/([a-z?][a-z0-9?_-]*):(.*)$/i);
|
|
631
|
+
if (m) {
|
|
632
|
+
return { name: '/' + m[1].toLowerCase(), args: m[2].trim() };
|
|
633
|
+
}
|
|
634
|
+
// /cmd args
|
|
410
635
|
const space = trimmed.indexOf(' ');
|
|
411
|
-
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
636
|
+
const name = (space === -1 ? trimmed : trimmed.slice(0, space)).toLowerCase();
|
|
412
637
|
const args = space === -1 ? '' : trimmed.slice(space + 1);
|
|
413
|
-
return { name
|
|
638
|
+
return { name, args };
|
|
414
639
|
}
|
|
415
640
|
|
|
416
641
|
async function dispatch(ctx, line, helpers) {
|
|
@@ -421,10 +646,16 @@ async function dispatch(ctx, line, helpers) {
|
|
|
421
646
|
console.log(`${ui.errorTag()} command gak dikenal: ${parsed.name} — coba ${ui.chalk.yellow('/help')}`);
|
|
422
647
|
return true;
|
|
423
648
|
}
|
|
424
|
-
|
|
649
|
+
try {
|
|
650
|
+
await handler(ctx, parsed.args, helpers || {});
|
|
651
|
+
} catch (err) {
|
|
652
|
+
showError(err);
|
|
653
|
+
}
|
|
425
654
|
return true;
|
|
426
655
|
}
|
|
427
656
|
|
|
657
|
+
/* ----- shared helpers ----- */
|
|
658
|
+
|
|
428
659
|
function renderListy(data, preferredKeys) {
|
|
429
660
|
const items = Array.isArray(data)
|
|
430
661
|
? data
|
|
@@ -467,7 +698,7 @@ function showError(err) {
|
|
|
467
698
|
console.log(ui.limitWarning(err.info || {}));
|
|
468
699
|
return;
|
|
469
700
|
}
|
|
470
|
-
console.log(`${ui.errorTag()} ${err.message}`);
|
|
701
|
+
console.log(`${ui.errorTag()} ${err.message || err}`);
|
|
471
702
|
}
|
|
472
703
|
|
|
473
704
|
function formatVal(v) {
|
|
@@ -493,9 +724,4 @@ function maskKey(key) {
|
|
|
493
724
|
return key.slice(0, 6) + '…' + key.slice(-4);
|
|
494
725
|
}
|
|
495
726
|
|
|
496
|
-
function indent(s, n) {
|
|
497
|
-
const pad = ' '.repeat(n);
|
|
498
|
-
return s.split('\n').map((l) => pad + l).join('\n');
|
|
499
|
-
}
|
|
500
|
-
|
|
501
727
|
module.exports = { dispatch, parse, REGISTRY };
|
package/src/config.js
CHANGED
|
@@ -13,13 +13,20 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
13
13
|
|
|
14
14
|
const DEFAULTS = {
|
|
15
15
|
apiKey: null,
|
|
16
|
-
baseUrl: 'https://
|
|
16
|
+
baseUrl: 'https://zelapioffciall.dpdns.org',
|
|
17
17
|
statusUrl: 'https://zelapioffciall.dpdns.org/api/status',
|
|
18
18
|
userBaseUrl: 'https://zelapioffciall.dpdns.org',
|
|
19
19
|
model: 'haiku',
|
|
20
20
|
endpoint: '/ai/claila',
|
|
21
21
|
system: 'You are Zelai, a helpful and concise assistant. Reply in the same language as the user.',
|
|
22
|
+
// Toggle flag — kalau true, field gak dikirim ke payload meskipun ada value-nya.
|
|
23
|
+
modelDisabled: false,
|
|
24
|
+
systemDisabled: false,
|
|
22
25
|
lastSession: null,
|
|
26
|
+
theme: 'dark',
|
|
27
|
+
// Soft cache buat version-check supaya gak hit registry setiap startup.
|
|
28
|
+
lastVersionCheck: 0,
|
|
29
|
+
latestKnownVersion: null,
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
function ensureDir() {
|
|
@@ -34,6 +41,10 @@ function load() {
|
|
|
34
41
|
try {
|
|
35
42
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
36
43
|
const parsed = JSON.parse(raw);
|
|
44
|
+
// Migrate baseUrl yang ngarah ke subdomain `api.` lama → root.
|
|
45
|
+
if (parsed.baseUrl === 'https://api.zelapioffciall.dpdns.org') {
|
|
46
|
+
parsed.baseUrl = 'https://zelapioffciall.dpdns.org';
|
|
47
|
+
}
|
|
37
48
|
return { ...DEFAULTS, ...parsed };
|
|
38
49
|
} catch {
|
|
39
50
|
return { ...DEFAULTS };
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme palette buat terminal — dipakai di seluruh UI helpers.
|
|
3
|
+
*
|
|
4
|
+
* Tiap theme nge-define palette warna untuk:
|
|
5
|
+
* primary, accent, dim, user, assistant, system, error, warn, ok, link
|
|
6
|
+
*
|
|
7
|
+
* Active theme dipilih lewat config (`theme: 'dark' | 'night'`).
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
const THEMES = {
|
|
14
|
+
dark: {
|
|
15
|
+
name: 'dark',
|
|
16
|
+
label: 'Dark — magenta neon, classic Zelapi',
|
|
17
|
+
primary: chalk.magentaBright.bold,
|
|
18
|
+
accent: chalk.cyan,
|
|
19
|
+
dim: chalk.gray,
|
|
20
|
+
user: chalk.cyan.bold,
|
|
21
|
+
assistant: chalk.magentaBright.bold,
|
|
22
|
+
system: chalk.yellow.bold,
|
|
23
|
+
error: chalk.red.bold,
|
|
24
|
+
warn: chalk.yellow.bold,
|
|
25
|
+
ok: chalk.green.bold,
|
|
26
|
+
info: chalk.blueBright.bold,
|
|
27
|
+
link: chalk.cyan.underline,
|
|
28
|
+
box: chalk.magentaBright,
|
|
29
|
+
boxAccent: chalk.cyan,
|
|
30
|
+
},
|
|
31
|
+
night: {
|
|
32
|
+
name: 'night',
|
|
33
|
+
label: 'Night — soft blue & purple, easy on eyes',
|
|
34
|
+
primary: chalk.blueBright.bold,
|
|
35
|
+
accent: chalk.magenta,
|
|
36
|
+
dim: chalk.gray,
|
|
37
|
+
user: chalk.blueBright.bold,
|
|
38
|
+
assistant: chalk.magenta.bold,
|
|
39
|
+
system: chalk.cyan.bold,
|
|
40
|
+
error: chalk.red.bold,
|
|
41
|
+
warn: chalk.yellow,
|
|
42
|
+
ok: chalk.green,
|
|
43
|
+
info: chalk.cyan.bold,
|
|
44
|
+
link: chalk.blueBright.underline,
|
|
45
|
+
box: chalk.blue,
|
|
46
|
+
boxAccent: chalk.magenta,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let CURRENT = THEMES.dark;
|
|
51
|
+
|
|
52
|
+
function setTheme(name) {
|
|
53
|
+
const next = THEMES[name];
|
|
54
|
+
if (!next) return false;
|
|
55
|
+
CURRENT = next;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function current() {
|
|
60
|
+
return CURRENT;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function list() {
|
|
64
|
+
return Object.keys(THEMES);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function describe(name) {
|
|
68
|
+
return THEMES[name] ? THEMES[name].label : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { setTheme, current, list, describe, THEMES };
|
package/src/ui.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UI helpers — banner, prompt, markdown renderer untuk terminal.
|
|
2
|
+
* UI helpers — banner, prompt, markdown renderer, chat box untuk terminal.
|
|
3
|
+
*
|
|
4
|
+
* Semua warna ngambil dari src/theme.js — jadi `dark`/`night` keubah secara
|
|
5
|
+
* konsisten di seluruh UI.
|
|
3
6
|
*/
|
|
4
7
|
'use strict';
|
|
5
8
|
|
|
6
9
|
const chalk = require('chalk');
|
|
7
10
|
const { marked } = require('marked');
|
|
11
|
+
const theme = require('./theme');
|
|
8
12
|
|
|
9
13
|
let TerminalRenderer;
|
|
10
14
|
try {
|
|
@@ -32,29 +36,33 @@ if (TerminalRenderer) {
|
|
|
32
36
|
}),
|
|
33
37
|
});
|
|
34
38
|
} catch {
|
|
35
|
-
// ignored —
|
|
39
|
+
// ignored — fall back to plain text
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const VERSION = require('../package.json').version;
|
|
40
44
|
|
|
45
|
+
function termWidth() {
|
|
46
|
+
return Math.min(process.stdout.columns || 80, 100);
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
function banner() {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
50
|
+
const t = theme.current();
|
|
51
|
+
const c = t.primary;
|
|
52
|
+
const accent = t.accent;
|
|
53
|
+
const dim = t.dim;
|
|
45
54
|
const yellow = chalk.yellow;
|
|
46
55
|
|
|
47
|
-
// ASCII ZELAI — block style. "by ZELAPI" subtitle on the side.
|
|
48
56
|
const lines = [
|
|
49
57
|
'',
|
|
50
|
-
c
|
|
51
|
-
c
|
|
52
|
-
c
|
|
53
|
-
c
|
|
54
|
-
c
|
|
55
|
-
c
|
|
58
|
+
c(' ███████╗███████╗██╗ █████╗ ██╗ '),
|
|
59
|
+
c(' ╚══███╔╝██╔════╝██║ ██╔══██╗██║ '),
|
|
60
|
+
c(' ███╔╝ █████╗ ██║ ███████║██║ ') + dim(' by ') + t.accent.bold('ZELAPI'),
|
|
61
|
+
c(' ███╔╝ ██╔══╝ ██║ ██╔══██║██║ ') + dim(' v') + dim(VERSION),
|
|
62
|
+
c(' ███████╗███████╗███████╗██║ ██║██║ ') + dim(' Protocol V1'),
|
|
63
|
+
c(' ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ '),
|
|
56
64
|
'',
|
|
57
|
-
' ' + accent.bold('● ') + accent.bold('Zelai Agent CLI') + dim(' • ') + chalk.green('online') + dim(' • ') +
|
|
65
|
+
' ' + accent.bold('● ') + accent.bold('Zelai Agent CLI') + dim(' • ') + chalk.green('online') + dim(' • ') + t.link('zelapioffciall.dpdns.org'),
|
|
58
66
|
' ' + dim('Ketik ') + yellow('/help') + dim(' buat lihat command. ') + yellow('/exit') + dim(' buat keluar. ') + yellow('Ctrl+D') + dim(' juga bisa.'),
|
|
59
67
|
'',
|
|
60
68
|
];
|
|
@@ -62,9 +70,8 @@ function banner() {
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
function smallBanner() {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
return c.bold('zelai') + dim(' by ZELAPI · v' + VERSION);
|
|
73
|
+
const t = theme.current();
|
|
74
|
+
return t.primary('zelai') + t.dim(' by ZELAPI · v' + VERSION);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
function renderMarkdown(text) {
|
|
@@ -77,42 +84,98 @@ function renderMarkdown(text) {
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
/* ----- chat box ----- */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Top of an input box. Render duluan sebelum readline prompt.
|
|
91
|
+
* ╭─[ you (haiku) ]──────────────────────
|
|
92
|
+
* │ »
|
|
93
|
+
*/
|
|
94
|
+
function userBoxOpen(model, opts = {}) {
|
|
95
|
+
const t = theme.current();
|
|
96
|
+
const w = termWidth();
|
|
97
|
+
const label = ` you (${opts.model || model || 'haiku'}) `;
|
|
98
|
+
const filler = Math.max(w - label.length - 4, 0);
|
|
99
|
+
const top = t.boxAccent('╭─[') + t.user(label) + t.boxAccent(']' + '─'.repeat(filler));
|
|
100
|
+
return top;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function userPrompt(model, opts = {}) {
|
|
104
|
+
const t = theme.current();
|
|
105
|
+
const arrow = opts.disabledHint ? t.warn('»') : t.user('»');
|
|
106
|
+
return `${t.boxAccent('│')} ${arrow} `;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Open of an assistant bubble.
|
|
111
|
+
* ╭─[ zelai (haiku) ]──────────────────────
|
|
112
|
+
* │ markdown content here
|
|
113
|
+
* │ ...
|
|
114
|
+
* ╰──────────────────────────────────────
|
|
115
|
+
*/
|
|
116
|
+
function assistantBoxOpen(model) {
|
|
117
|
+
const t = theme.current();
|
|
118
|
+
const w = termWidth();
|
|
119
|
+
const label = ` zelai (${model}) `;
|
|
120
|
+
const filler = Math.max(w - label.length - 4, 0);
|
|
121
|
+
return t.box('╭─[') + t.assistant(label) + t.box(']' + '─'.repeat(filler));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assistantBoxClose() {
|
|
125
|
+
const t = theme.current();
|
|
126
|
+
return t.box('╰' + '─'.repeat(Math.max(termWidth() - 1, 0)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Indent each non-empty line with the "│ " gutter.
|
|
131
|
+
*/
|
|
132
|
+
function gutterIndent(text) {
|
|
133
|
+
const t = theme.current();
|
|
134
|
+
const g = t.box('│') + ' ';
|
|
135
|
+
if (!text) return g;
|
|
136
|
+
return text.split('\n').map((l) => g + l).join('\n');
|
|
83
137
|
}
|
|
84
138
|
|
|
139
|
+
/* ----- tags / hints ----- */
|
|
140
|
+
|
|
85
141
|
function assistantTag(model) {
|
|
86
|
-
|
|
142
|
+
const t = theme.current();
|
|
143
|
+
return t.assistant('zelai') + t.dim(`(${model})`) + t.dim(' ›');
|
|
87
144
|
}
|
|
88
145
|
|
|
89
146
|
function systemTag() {
|
|
90
|
-
|
|
147
|
+
const t = theme.current();
|
|
148
|
+
return t.system('system') + t.dim(' ›');
|
|
91
149
|
}
|
|
92
150
|
|
|
93
151
|
function errorTag() {
|
|
94
|
-
|
|
152
|
+
const t = theme.current();
|
|
153
|
+
return t.error('error') + t.dim(' ›');
|
|
95
154
|
}
|
|
96
155
|
|
|
97
156
|
function warnTag() {
|
|
98
|
-
|
|
157
|
+
const t = theme.current();
|
|
158
|
+
return t.warn('⚠ warning') + t.dim(' ›');
|
|
99
159
|
}
|
|
100
160
|
|
|
101
161
|
function infoTag() {
|
|
102
|
-
|
|
162
|
+
const t = theme.current();
|
|
163
|
+
return t.info('info') + t.dim(' ›');
|
|
103
164
|
}
|
|
104
165
|
|
|
105
166
|
function okTag() {
|
|
106
|
-
|
|
167
|
+
const t = theme.current();
|
|
168
|
+
return t.ok('✓') + t.dim(' ›');
|
|
107
169
|
}
|
|
108
170
|
|
|
109
171
|
function hr() {
|
|
110
|
-
const
|
|
111
|
-
return
|
|
172
|
+
const t = theme.current();
|
|
173
|
+
return t.dim('─'.repeat(termWidth()));
|
|
112
174
|
}
|
|
113
175
|
|
|
114
176
|
function kv(key, value) {
|
|
115
|
-
|
|
177
|
+
const t = theme.current();
|
|
178
|
+
return `${t.dim(key.padEnd(14))} ${chalk.white(value)}`;
|
|
116
179
|
}
|
|
117
180
|
|
|
118
181
|
function pretty(obj) {
|
|
@@ -123,38 +186,68 @@ function pretty(obj) {
|
|
|
123
186
|
}
|
|
124
187
|
}
|
|
125
188
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*/
|
|
189
|
+
/* ----- limit warning ----- */
|
|
190
|
+
|
|
129
191
|
function limitWarning(info) {
|
|
130
|
-
const
|
|
131
|
-
const red =
|
|
192
|
+
const t = theme.current();
|
|
193
|
+
const red = t.error;
|
|
132
194
|
const top = red('┏━━ LIMIT EXCEEDED ' + '━'.repeat(40));
|
|
133
195
|
const bot = red('┗' + '━'.repeat(58));
|
|
134
|
-
const lines = [
|
|
135
|
-
'',
|
|
136
|
-
top,
|
|
137
|
-
red('┃ ') + chalk.yellow('Kuota API kamu udah habis 🚫'),
|
|
138
|
-
red('┃ '),
|
|
139
|
-
];
|
|
196
|
+
const lines = ['', top, red('┃ ') + chalk.yellow('Kuota API kamu udah habis 🚫'), red('┃ ')];
|
|
140
197
|
if (info && typeof info === 'object') {
|
|
141
|
-
if (info.used !== undefined) lines.push(red('┃ ') + dim('used : ') + chalk.white(String(info.used)));
|
|
142
|
-
if (info.limit !== undefined) lines.push(red('┃ ') + dim('limit : ') + chalk.white(String(info.limit)));
|
|
143
|
-
if (info.resetAt) lines.push(red('┃ ') + dim('reset : ') + chalk.white(String(info.resetAt)));
|
|
144
|
-
if (info.plan) lines.push(red('┃ ') + dim('plan : ') + chalk.white(String(info.plan)));
|
|
198
|
+
if (info.used !== undefined) lines.push(red('┃ ') + t.dim('used : ') + chalk.white(String(info.used)));
|
|
199
|
+
if (info.limit !== undefined) lines.push(red('┃ ') + t.dim('limit : ') + chalk.white(String(info.limit)));
|
|
200
|
+
if (info.resetAt) lines.push(red('┃ ') + t.dim('reset : ') + chalk.white(String(info.resetAt)));
|
|
201
|
+
if (info.plan) lines.push(red('┃ ') + t.dim('plan : ') + chalk.white(String(info.plan)));
|
|
145
202
|
}
|
|
146
203
|
lines.push(red('┃ '));
|
|
147
|
-
lines.push(red('┃ ') + dim('Upgrade plan di ') +
|
|
204
|
+
lines.push(red('┃ ') + t.dim('Upgrade plan di ') + t.link('https://zelapioffciall.dpdns.org/pricing'));
|
|
148
205
|
lines.push(bot);
|
|
149
206
|
lines.push('');
|
|
150
207
|
return lines.join('\n');
|
|
151
208
|
}
|
|
152
209
|
|
|
210
|
+
/* ----- version vulnerable warning ----- */
|
|
211
|
+
|
|
212
|
+
function vulnerableWarning(current, latest) {
|
|
213
|
+
const t = theme.current();
|
|
214
|
+
const yellow = chalk.yellow.bold;
|
|
215
|
+
const dim = t.dim;
|
|
216
|
+
const top = yellow('┏━━ ⚠ UPDATE AVAILABLE ' + '━'.repeat(35));
|
|
217
|
+
const bot = yellow('┗' + '━'.repeat(58));
|
|
218
|
+
return [
|
|
219
|
+
'',
|
|
220
|
+
top,
|
|
221
|
+
yellow('┃ ') + chalk.white('Your version is too vulnerable to problems,'),
|
|
222
|
+
yellow('┃ ') + chalk.white('please update by typing ') + chalk.cyan.bold('/update'),
|
|
223
|
+
yellow('┃ '),
|
|
224
|
+
yellow('┃ ') + dim('current : ') + chalk.white(current),
|
|
225
|
+
yellow('┃ ') + dim('latest : ') + chalk.green.bold(latest),
|
|
226
|
+
bot,
|
|
227
|
+
'',
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ----- generic box ----- */
|
|
232
|
+
|
|
233
|
+
function box(title, body) {
|
|
234
|
+
const t = theme.current();
|
|
235
|
+
const w = termWidth();
|
|
236
|
+
const label = ` ${title} `;
|
|
237
|
+
const filler = Math.max(w - label.length - 4, 0);
|
|
238
|
+
const top = t.boxAccent('╭─[') + t.user(label) + t.boxAccent(']' + '─'.repeat(filler));
|
|
239
|
+
const bot = t.boxAccent('╰' + '─'.repeat(Math.max(w - 1, 0)));
|
|
240
|
+
return [top, gutterIndent(body || ''), bot].join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
153
243
|
module.exports = {
|
|
154
244
|
banner,
|
|
155
245
|
smallBanner,
|
|
156
246
|
renderMarkdown,
|
|
247
|
+
userBoxOpen,
|
|
157
248
|
userPrompt,
|
|
249
|
+
assistantBoxOpen,
|
|
250
|
+
assistantBoxClose,
|
|
158
251
|
assistantTag,
|
|
159
252
|
systemTag,
|
|
160
253
|
errorTag,
|
|
@@ -165,5 +258,10 @@ module.exports = {
|
|
|
165
258
|
kv,
|
|
166
259
|
pretty,
|
|
167
260
|
limitWarning,
|
|
261
|
+
vulnerableWarning,
|
|
262
|
+
box,
|
|
263
|
+
gutterIndent,
|
|
264
|
+
termWidth,
|
|
168
265
|
chalk,
|
|
266
|
+
theme,
|
|
169
267
|
};
|
package/src/version.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version check + /update helper.
|
|
3
|
+
*
|
|
4
|
+
* - getLatest() → ngambil versi terbaru dari npm registry (zelai-cli)
|
|
5
|
+
* - isBehind(a, b) → true kalau a (current) < b (latest)
|
|
6
|
+
* - runUpdate() → spawn `npm i -g zelai-cli@latest`, stream output ke stdout
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const axios = require('axios');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
14
|
+
const PKG = 'zelai-cli';
|
|
15
|
+
|
|
16
|
+
async function getLatest(timeoutMs = 4000) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await axios.get(`${REGISTRY}/${PKG}/latest`, { timeout: timeoutMs });
|
|
19
|
+
if (res.status >= 200 && res.status < 300 && res.data && res.data.version) {
|
|
20
|
+
return String(res.data.version);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Diem aja — version check itu best effort.
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseSemver(v) {
|
|
29
|
+
const m = String(v || '').replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
30
|
+
if (!m) return null;
|
|
31
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compareSemver(a, b) {
|
|
35
|
+
const av = parseSemver(a);
|
|
36
|
+
const bv = parseSemver(b);
|
|
37
|
+
if (!av || !bv) return 0;
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
if (av[i] < bv[i]) return -1;
|
|
40
|
+
if (av[i] > bv[i]) return 1;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isBehind(current, latest) {
|
|
46
|
+
if (!current || !latest) return false;
|
|
47
|
+
return compareSemver(current, latest) < 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Jalanin `npm i -g zelai-cli@latest`. Stream stdout/stderr live ke onLine,
|
|
52
|
+
* supaya CLI bisa render pesan progress (warnaan).
|
|
53
|
+
*
|
|
54
|
+
* Returns Promise<{ ok, code }>.
|
|
55
|
+
*/
|
|
56
|
+
function runUpdate(onLine) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const args = ['i', '-g', `${PKG}@latest`];
|
|
59
|
+
const child = spawn('npm', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
60
|
+
|
|
61
|
+
let buf = '';
|
|
62
|
+
const onData = (chunk) => {
|
|
63
|
+
buf += chunk.toString('utf8');
|
|
64
|
+
let nl;
|
|
65
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
66
|
+
const line = buf.slice(0, nl);
|
|
67
|
+
buf = buf.slice(nl + 1);
|
|
68
|
+
if (typeof onLine === 'function') onLine(line);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
child.stdout.on('data', onData);
|
|
72
|
+
child.stderr.on('data', onData);
|
|
73
|
+
|
|
74
|
+
child.on('close', (code) => {
|
|
75
|
+
if (buf && typeof onLine === 'function') onLine(buf);
|
|
76
|
+
resolve({ ok: code === 0, code });
|
|
77
|
+
});
|
|
78
|
+
child.on('error', (err) => {
|
|
79
|
+
if (typeof onLine === 'function') onLine('error: ' + err.message);
|
|
80
|
+
resolve({ ok: false, code: -1 });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { getLatest, isBehind, compareSemver, parseSemver, runUpdate, PKG };
|