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/src/client.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * HTTP client untuk Zelapi.
3
+ * Wrapper tipis di atas axios — semua endpoint AI / status / user di-route lewat sini.
4
+ */
5
+ 'use strict';
6
+
7
+ const axios = require('axios');
8
+
9
+ const USER_AGENT = `zelai-cli/${require('../package.json').version} (+https://github.com/zelapi/zelai)`;
10
+
11
+ class LimitExceededError extends Error {
12
+ constructor(message, info) {
13
+ super(message || 'Limit terlampaui.');
14
+ this.name = 'LimitExceededError';
15
+ this.status = 429;
16
+ this.info = info || {};
17
+ }
18
+ }
19
+
20
+ class ApiError extends Error {
21
+ constructor(message, status, body) {
22
+ super(message);
23
+ this.name = 'ApiError';
24
+ this.status = status;
25
+ this.body = body;
26
+ }
27
+ }
28
+
29
+ function makeClient(config) {
30
+ const http = axios.create({
31
+ timeout: 120000,
32
+ headers: { 'User-Agent': USER_AGENT },
33
+ validateStatus: () => true,
34
+ });
35
+
36
+ function authHeaders() {
37
+ if (!config.apiKey) {
38
+ const err = new Error('API key belum di-set. Jalanin /login dulu di dalam chat, atau set env ZELAPI_KEY.');
39
+ err.code = 'NO_API_KEY';
40
+ throw err;
41
+ }
42
+ return {
43
+ Authorization: `Bearer ${config.apiKey}`,
44
+ 'Content-Type': 'application/json',
45
+ };
46
+ }
47
+
48
+ function unwrap(res, endpoint) {
49
+ if (res.status >= 200 && res.status < 300) return res.data;
50
+
51
+ const body = res.data;
52
+ const text = JSON.stringify(body || '').toLowerCase();
53
+ const looksLikeLimit =
54
+ res.status === 429 ||
55
+ /limit (exceed|reach|hit)|quota|exhaust|too many request|rate.?limit/.test(text);
56
+
57
+ if (looksLikeLimit) {
58
+ const info = {
59
+ used: body && (body.used ?? (body.data && body.data.used)),
60
+ limit: body && (body.limit ?? (body.data && body.data.limit)),
61
+ resetAt: body && (body.reset || body.resetAt || (body.data && (body.data.reset || body.data.resetAt))),
62
+ plan: body && (body.plan || (body.data && body.data.plan)),
63
+ };
64
+ const msg = (body && (body.message || body.error || body.detail)) || 'Kuota API kamu udah habis.';
65
+ throw new LimitExceededError(msg, info);
66
+ }
67
+
68
+ const msg =
69
+ (body && (body.message || body.error || body.detail)) ||
70
+ `HTTP ${res.status} dari ${endpoint}`;
71
+ throw new ApiError(msg, res.status, body);
72
+ }
73
+
74
+ /**
75
+ * Kirim pesan ke Agent API.
76
+ * Returns: { reply, sessionId, raw }
77
+ */
78
+ async function sendAgent({ messages, model, endpoint, system, sessionId }) {
79
+ const url = `${config.baseUrl.replace(/\/$/, '')}/api/v1/agent`;
80
+
81
+ // Sesuai docs: `session` di-attach ke item message terakhir.
82
+ const msgs = messages.map((m, i) => {
83
+ if (i === messages.length - 1 && sessionId) {
84
+ return { ...m, session: sessionId };
85
+ }
86
+ return m;
87
+ });
88
+
89
+ const payload = {
90
+ endpoint: endpoint || '/ai/claila',
91
+ messages: msgs,
92
+ };
93
+ if (model) payload.model = model;
94
+ if (system) payload.system = system;
95
+
96
+ const res = await http.post(url, payload, { headers: authHeaders() });
97
+ const data = unwrap(res, '/api/v1/agent');
98
+
99
+ const reply = pickReply(data);
100
+ const session = pickSession(data) || sessionId || null;
101
+ return { reply, sessionId: session, raw: data };
102
+ }
103
+
104
+ async function getStatus() {
105
+ const url = config.statusUrl;
106
+ const res = await http.get(url);
107
+ return unwrap(res, '/api/status');
108
+ }
109
+
110
+ async function getActivityLogs(apiKey) {
111
+ const key = apiKey || config.apiKey;
112
+ if (!key) throw new Error('API key dibutuhkan buat ngambil activity logs.');
113
+ const url = `${config.userBaseUrl.replace(/\/$/, '')}/api/user/activity-logs`;
114
+ const res = await http.get(url, { params: { apikey: key } });
115
+ return unwrap(res, '/api/user/activity-logs');
116
+ }
117
+
118
+ async function getUsage(apiKey) {
119
+ const key = apiKey || config.apiKey;
120
+ if (!key) throw new Error('API key dibutuhkan buat ngambil usage.');
121
+ const url = `${config.userBaseUrl.replace(/\/$/, '')}/api/user/usage`;
122
+ const res = await http.get(url, { params: { apikey: key } });
123
+ return unwrap(res, '/api/user/usage');
124
+ }
125
+
126
+ return { sendAgent, getStatus, getActivityLogs, getUsage };
127
+ }
128
+
129
+ function pickReply(data) {
130
+ if (data == null) return '';
131
+ if (typeof data === 'string') return data;
132
+
133
+ const candidates = [
134
+ data.reply,
135
+ data.response,
136
+ data.message,
137
+ data.answer,
138
+ data.content,
139
+ data.result,
140
+ data.text,
141
+ data.output,
142
+ data.data && data.data.reply,
143
+ data.data && data.data.response,
144
+ data.data && data.data.message,
145
+ data.data && data.data.content,
146
+ data.data && data.data.text,
147
+ data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,
148
+ data.choices && data.choices[0] && data.choices[0].text,
149
+ ];
150
+ for (const c of candidates) {
151
+ if (typeof c === 'string' && c.trim()) return c;
152
+ }
153
+ try {
154
+ return '```json\n' + JSON.stringify(data, null, 2) + '\n```';
155
+ } catch {
156
+ return String(data);
157
+ }
158
+ }
159
+
160
+ function pickSession(data) {
161
+ if (!data || typeof data !== 'object') return null;
162
+ return (
163
+ data.session ||
164
+ data.sessionId ||
165
+ data.session_id ||
166
+ (data.data && (data.data.session || data.data.sessionId || data.data.session_id)) ||
167
+ null
168
+ );
169
+ }
170
+
171
+ module.exports = { makeClient, LimitExceededError, ApiError };
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Slash command registry.
3
+ * Setiap handler dipanggil dengan (ctx, args, helpers) — ctx = session state dari cli.js.
4
+ */
5
+ 'use strict';
6
+
7
+ const ora = require('ora');
8
+ const ui = require('./ui');
9
+ const config = require('./config');
10
+ const session = require('./session');
11
+ const { LimitExceededError } = require('./client');
12
+
13
+ const HELP_ROWS = [
14
+ ['/help', 'tampilin daftar command'],
15
+ ['/status', 'cek status server (/api/status)'],
16
+ ['/activity', 'tampilin activity logs akun kamu'],
17
+ ['/usage', 'tampilin pemakaian / kuota API key'],
18
+ ['/model', 'ganti model — opus | sonnet | haiku'],
19
+ ['/endpoint', 'ganti AI endpoint, mis: /ai/claila, /ai/spawn'],
20
+ ['/system', 'set system prompt; kosong → tampilin current'],
21
+ ['/new', 'mulai sesi baru (lupain history)'],
22
+ ['/session', 'tampilin / set session ID secara manual'],
23
+ ['/sessions', 'list semua sesi di folder ./session'],
24
+ ['/load', 'load sesi tersimpan: /load <id|nomor>'],
25
+ ['/forget', 'hapus sesi tersimpan: /forget <id|nomor|all>'],
26
+ ['/save', 'paksa simpan sesi sekarang ke ./session'],
27
+ ['/login', 'set / ganti API key (zel_…)'],
28
+ ['/logout', 'hapus API key tersimpan'],
29
+ ['/config', 'tampilin lokasi & isi config'],
30
+ ['/clear', 'bersihin layar'],
31
+ ['/exit', 'keluar (alias: /quit, /q)'],
32
+ ];
33
+
34
+ async function cmdHelp() {
35
+ console.log('');
36
+ console.log(ui.chalk.bold('Slash Commands'));
37
+ console.log('');
38
+ for (const [k, v] of HELP_ROWS) {
39
+ console.log(' ' + ui.chalk.yellow(k.padEnd(11)) + ui.chalk.gray('· ') + ui.chalk.white(v));
40
+ }
41
+ console.log('');
42
+ console.log(ui.chalk.gray(' Tip: ketik biasa tanpa "/" buat ngobrol sama AI.'));
43
+ console.log('');
44
+ }
45
+
46
+ async function cmdStatus(ctx) {
47
+ const spinner = ora({ text: 'ngecek status server…', color: 'cyan' }).start();
48
+ try {
49
+ const data = await ctx.client.getStatus();
50
+ spinner.stop();
51
+ console.log(ui.hr());
52
+ console.log(ui.chalk.bold('Server Status') + ui.chalk.gray(' · ') + ui.chalk.cyan.underline('https://zelapioffciall.dpdns.org/api/status'));
53
+ console.log('');
54
+ if (typeof data === 'object' && data !== null) {
55
+ const known = ['status', 'state', 'health', 'uptime', 'version', 'region', 'latency', 'node', 'env'];
56
+ const root = data.data && typeof data.data === 'object' ? data.data : data;
57
+ let printed = 0;
58
+ for (const k of known) {
59
+ if (root[k] !== undefined) {
60
+ const v = root[k];
61
+ const value = (k === 'status' || k === 'state' || k === 'health')
62
+ ? (/ok|up|online|healthy|stable/i.test(String(v)) ? ui.chalk.green.bold(String(v)) : ui.chalk.yellow(String(v)))
63
+ : String(v);
64
+ console.log(' ' + ui.kv(k, value));
65
+ printed++;
66
+ }
67
+ }
68
+ if (printed === 0) {
69
+ console.log(ui.chalk.gray(ui.pretty(data)));
70
+ } else {
71
+ console.log('');
72
+ console.log(ui.chalk.gray(' raw:'));
73
+ console.log(ui.chalk.gray(indent(ui.pretty(data), 2)));
74
+ }
75
+ } else {
76
+ console.log(String(data));
77
+ }
78
+ console.log(ui.hr());
79
+ } catch (err) {
80
+ spinner.stop();
81
+ showError(err);
82
+ }
83
+ }
84
+
85
+ async function cmdActivity(ctx, args) {
86
+ const key = (args && args.trim()) || ctx.config.apiKey;
87
+ if (!key) {
88
+ console.log(`${ui.errorTag()} API key kosong. /login dulu.`);
89
+ return;
90
+ }
91
+ const spinner = ora({ text: 'narik activity logs…', color: 'cyan' }).start();
92
+ try {
93
+ const data = await ctx.client.getActivityLogs(key);
94
+ spinner.stop();
95
+ console.log(ui.hr());
96
+ console.log(ui.chalk.bold('Activity Logs') + ui.chalk.gray(` · apikey=${maskKey(key)}`));
97
+ renderListy(data, ['timestamp', 'time', 'date', 'action', 'event', 'endpoint', 'ip', 'status']);
98
+ console.log(ui.hr());
99
+ } catch (err) {
100
+ spinner.stop();
101
+ showError(err);
102
+ }
103
+ }
104
+
105
+ async function cmdUsage(ctx, args) {
106
+ const key = (args && args.trim()) || ctx.config.apiKey;
107
+ if (!key) {
108
+ console.log(`${ui.errorTag()} API key kosong. /login dulu.`);
109
+ return;
110
+ }
111
+ const spinner = ora({ text: 'narik usage…', color: 'cyan' }).start();
112
+ try {
113
+ const data = await ctx.client.getUsage(key);
114
+ spinner.stop();
115
+ console.log(ui.hr());
116
+ console.log(ui.chalk.bold('Usage') + ui.chalk.gray(` · apikey=${maskKey(key)}`));
117
+ if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
118
+ const flat = data.data && typeof data.data === 'object' ? data.data : data;
119
+ for (const [k, v] of Object.entries(flat)) {
120
+ if (typeof v === 'object' && v !== null) {
121
+ console.log(ui.chalk.gray(`\n${k}:`));
122
+ for (const [k2, v2] of Object.entries(v)) {
123
+ console.log(' ' + ui.kv(k2, formatVal(v2)));
124
+ }
125
+ } else {
126
+ console.log(ui.kv(k, formatVal(v)));
127
+ }
128
+ }
129
+ maybeShowQuotaBar(flat);
130
+ } else {
131
+ renderListy(data, ['endpoint', 'count', 'tokens', 'requests']);
132
+ }
133
+ console.log(ui.hr());
134
+ } catch (err) {
135
+ spinner.stop();
136
+ showError(err);
137
+ }
138
+ }
139
+
140
+ function maybeShowQuotaBar(flat) {
141
+ const used = num(flat.used) ?? num(flat.usage) ?? num(flat.count) ?? num(flat.requests);
142
+ const limit = num(flat.limit) ?? num(flat.quota) ?? num(flat.max);
143
+ if (used === undefined || limit === undefined || limit <= 0) return;
144
+ const ratio = Math.min(used / limit, 1);
145
+ const width = 30;
146
+ const filled = Math.round(ratio * width);
147
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
148
+ const color = ratio >= 1 ? ui.chalk.red : ratio >= 0.9 ? ui.chalk.yellow : ui.chalk.green;
149
+ console.log('');
150
+ console.log(' ' + color(bar) + ' ' + color(`${used}/${limit}`) + ui.chalk.gray(` (${(ratio * 100).toFixed(1)}%)`));
151
+ if (ratio >= 1) {
152
+ console.log(ui.limitWarning({ used, limit, plan: flat.plan, resetAt: flat.reset || flat.resetAt }));
153
+ }
154
+ }
155
+
156
+ async function cmdModel(ctx, args) {
157
+ const next = (args || '').trim().toLowerCase();
158
+ if (!next) {
159
+ console.log(`${ui.infoTag()} model sekarang: ${ui.chalk.cyan(ctx.config.model)} ${ui.chalk.gray('— pilihan: opus, sonnet, haiku')}`);
160
+ return;
161
+ }
162
+ if (!['opus', 'sonnet', 'haiku'].includes(next)) {
163
+ console.log(`${ui.errorTag()} model gak dikenal. Pilih: opus | sonnet | haiku`);
164
+ return;
165
+ }
166
+ ctx.config = config.update({ model: next });
167
+ console.log(`${ui.okTag()} model diganti ke ${ui.chalk.cyan(next)}`);
168
+ }
169
+
170
+ async function cmdEndpoint(ctx, args) {
171
+ const next = (args || '').trim();
172
+ if (!next) {
173
+ console.log(`${ui.infoTag()} endpoint sekarang: ${ui.chalk.cyan(ctx.config.endpoint)}`);
174
+ return;
175
+ }
176
+ if (!next.startsWith('/')) {
177
+ console.log(`${ui.errorTag()} endpoint harus diawali ${ui.chalk.yellow('/')} — contoh: /ai/claila`);
178
+ return;
179
+ }
180
+ ctx.config = config.update({ endpoint: next });
181
+ console.log(`${ui.okTag()} endpoint diganti ke ${ui.chalk.cyan(next)}`);
182
+ }
183
+
184
+ async function cmdSystem(ctx, args) {
185
+ const next = (args || '').trim();
186
+ if (!next) {
187
+ console.log(`${ui.infoTag()} system prompt saat ini:`);
188
+ console.log(ui.chalk.gray(' ' + (ctx.config.system || '(kosong)')));
189
+ return;
190
+ }
191
+ ctx.config = config.update({ system: next });
192
+ console.log(`${ui.okTag()} system prompt updated.`);
193
+ }
194
+
195
+ async function cmdNew(ctx) {
196
+ ctx.history = [];
197
+ ctx.sessionId = null;
198
+ config.update({ lastSession: null });
199
+ console.log(`${ui.okTag()} sesi baru dimulai — history & session ID di-reset.`);
200
+ }
201
+
202
+ async function cmdSession(ctx, args) {
203
+ const next = (args || '').trim();
204
+ if (!next) {
205
+ console.log(`${ui.infoTag()} session ID sekarang: ${ui.chalk.cyan(ctx.sessionId || '(belum ada)')}`);
206
+ return;
207
+ }
208
+ ctx.sessionId = next;
209
+ const rec = session.load(next);
210
+ if (rec && Array.isArray(rec.history)) {
211
+ ctx.history = rec.history.map(({ role, content }) => ({ role, content }));
212
+ console.log(`${ui.okTag()} session ID di-set ke ${ui.chalk.cyan(next)} ${ui.chalk.gray(`(${rec.history.length} pesan dimuat)`)}`);
213
+ } else {
214
+ console.log(`${ui.okTag()} session ID di-set ke ${ui.chalk.cyan(next)}`);
215
+ }
216
+ config.update({ lastSession: next });
217
+ }
218
+
219
+ async function cmdSessions(ctx) {
220
+ const rows = session.list();
221
+ ctx._lastSessionsList = rows;
222
+ console.log(ui.hr());
223
+ console.log(ui.chalk.bold('Sessions tersimpan') + ui.chalk.gray(' · ' + session.sessionDir()));
224
+ if (rows.length === 0) {
225
+ console.log(ui.chalk.gray('(belum ada sesi)'));
226
+ console.log(ui.hr());
227
+ return;
228
+ }
229
+ rows.forEach((r, i) => {
230
+ const when = r.updatedAt ? new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 19) : '—';
231
+ const isCurrent = r.id === ctx.sessionId;
232
+ const marker = isCurrent ? ui.chalk.green.bold('● ') : ui.chalk.gray(' ');
233
+ console.log(
234
+ marker +
235
+ ui.chalk.gray(`#${String(i + 1).padStart(2)} `) +
236
+ ui.chalk.cyan(r.id.padEnd(28)) +
237
+ ui.chalk.gray(' · ') +
238
+ ui.chalk.gray(`${String(r.turns).padStart(3)} turns`) +
239
+ ui.chalk.gray(' · ') +
240
+ ui.chalk.gray(when) +
241
+ (r.preview ? ui.chalk.gray(' — ' + r.preview) : '')
242
+ );
243
+ });
244
+ console.log(ui.chalk.gray('\n /load <nomor|id> · /forget <nomor|id|all>'));
245
+ console.log(ui.hr());
246
+ }
247
+
248
+ function resolveSessionRef(ctx, ref) {
249
+ if (!ref) return null;
250
+ const trimmed = ref.trim();
251
+ if (!trimmed) return null;
252
+ // Number → from last listing
253
+ if (/^\d+$/.test(trimmed)) {
254
+ const idx = parseInt(trimmed, 10) - 1;
255
+ const rows = ctx._lastSessionsList || session.list();
256
+ return rows[idx] ? rows[idx].id : null;
257
+ }
258
+ return trimmed;
259
+ }
260
+
261
+ async function cmdLoad(ctx, args) {
262
+ const ref = (args || '').trim();
263
+ if (!ref) {
264
+ console.log(`${ui.errorTag()} kasih session ID atau nomor — contoh: /load 1`);
265
+ return;
266
+ }
267
+ const id = resolveSessionRef(ctx, ref);
268
+ if (!id) {
269
+ console.log(`${ui.errorTag()} session "${ref}" gak ketemu. Coba /sessions dulu.`);
270
+ return;
271
+ }
272
+ const rec = session.load(id);
273
+ if (!rec) {
274
+ console.log(`${ui.errorTag()} sesi ${id} gak ada di folder ./session.`);
275
+ return;
276
+ }
277
+ ctx.sessionId = id;
278
+ ctx.history = (rec.history || []).map(({ role, content }) => ({ role, content }));
279
+ if (rec.model) ctx.config = config.update({ model: rec.model });
280
+ if (rec.endpoint) ctx.config = config.update({ endpoint: rec.endpoint });
281
+ if (rec.system) ctx.config = config.update({ system: rec.system });
282
+ config.update({ lastSession: id });
283
+ console.log(`${ui.okTag()} di-load sesi ${ui.chalk.cyan(id)} ${ui.chalk.gray(`(${ctx.history.length} pesan)`)}`);
284
+ }
285
+
286
+ async function cmdForget(ctx, args) {
287
+ const ref = (args || '').trim();
288
+ if (!ref) {
289
+ console.log(`${ui.errorTag()} kasih session ID, nomor, atau "all" — contoh: /forget 2`);
290
+ return;
291
+ }
292
+ if (ref.toLowerCase() === 'all') {
293
+ const rows = session.list();
294
+ let n = 0;
295
+ for (const r of rows) if (session.remove(r.id)) n++;
296
+ if (ctx.sessionId) ctx.sessionId = null;
297
+ if (ctx.history) ctx.history = [];
298
+ config.update({ lastSession: null });
299
+ console.log(`${ui.okTag()} dihapus ${n} sesi.`);
300
+ return;
301
+ }
302
+ const id = resolveSessionRef(ctx, ref);
303
+ if (!id) {
304
+ console.log(`${ui.errorTag()} session "${ref}" gak ketemu.`);
305
+ return;
306
+ }
307
+ const ok = session.remove(id);
308
+ if (!ok) {
309
+ console.log(`${ui.errorTag()} gagal hapus sesi ${id}.`);
310
+ return;
311
+ }
312
+ if (ctx.sessionId === id) {
313
+ ctx.sessionId = null;
314
+ ctx.history = [];
315
+ config.update({ lastSession: null });
316
+ }
317
+ console.log(`${ui.okTag()} sesi ${ui.chalk.cyan(id)} dihapus.`);
318
+ }
319
+
320
+ async function cmdSave(ctx) {
321
+ const id = ctx.sessionId || session.localId();
322
+ ctx.sessionId = id;
323
+ const rec = session.load(id) || session.create({
324
+ id, model: ctx.config.model, endpoint: ctx.config.endpoint, system: ctx.config.system,
325
+ });
326
+ rec.history = (ctx.history || []).map(({ role, content }) => ({ role, content, ts: Date.now() }));
327
+ rec.model = ctx.config.model;
328
+ rec.endpoint = ctx.config.endpoint;
329
+ rec.system = ctx.config.system;
330
+ session.save(rec);
331
+ config.update({ lastSession: id });
332
+ console.log(`${ui.okTag()} sesi disimpan: ${ui.chalk.cyan(id)} ${ui.chalk.gray(`(${rec.history.length} pesan)`)}`);
333
+ }
334
+
335
+ async function cmdLogin(ctx, args, { ask }) {
336
+ let key = (args || '').trim();
337
+ if (!key) {
338
+ key = await ask(`${ui.systemTag()} masukin API key (zel_…): `, { mask: true });
339
+ }
340
+ key = key.trim();
341
+ if (!key) {
342
+ console.log(`${ui.errorTag()} API key kosong, dibatalin.`);
343
+ return;
344
+ }
345
+ ctx.config = config.update({ apiKey: key });
346
+ console.log(`${ui.okTag()} API key kesimpan di ${ui.chalk.gray(config.configPath())}`);
347
+ }
348
+
349
+ async function cmdLogout(ctx) {
350
+ ctx.config = config.update({ apiKey: null });
351
+ console.log(`${ui.okTag()} API key dihapus dari config.`);
352
+ }
353
+
354
+ async function cmdConfig(ctx) {
355
+ console.log(ui.hr());
356
+ console.log(ui.chalk.bold('Config'));
357
+ console.log(ui.kv('path', config.configPath()));
358
+ console.log(ui.kv('sessionDir', session.sessionDir()));
359
+ console.log(ui.kv('apiKey', ctx.config.apiKey ? maskKey(ctx.config.apiKey) : '(belum di-set)'));
360
+ console.log(ui.kv('model', ctx.config.model));
361
+ console.log(ui.kv('endpoint', ctx.config.endpoint));
362
+ console.log(ui.kv('baseUrl', ctx.config.baseUrl));
363
+ console.log(ui.kv('session', ctx.sessionId || '(belum ada)'));
364
+ console.log(ui.kv('system', truncate(ctx.config.system || '', 60)));
365
+ console.log(ui.hr());
366
+ }
367
+
368
+ async function cmdClear() {
369
+ process.stdout.write('\x1Bc');
370
+ }
371
+
372
+ async function cmdExit(ctx) {
373
+ console.log(`${ui.infoTag()} bye! 👋`);
374
+ if (ctx.rl) ctx.rl.close();
375
+ process.exit(0);
376
+ }
377
+
378
+ const REGISTRY = {
379
+ '/help': cmdHelp,
380
+ '/?': cmdHelp,
381
+ '/status': cmdStatus,
382
+ '/activity': cmdActivity,
383
+ '/logs': cmdActivity,
384
+ '/usage': cmdUsage,
385
+ '/quota': cmdUsage,
386
+ '/model': cmdModel,
387
+ '/endpoint': cmdEndpoint,
388
+ '/system': cmdSystem,
389
+ '/new': cmdNew,
390
+ '/reset': cmdNew,
391
+ '/session': cmdSession,
392
+ '/sessions': cmdSessions,
393
+ '/ls': cmdSessions,
394
+ '/load': cmdLoad,
395
+ '/forget': cmdForget,
396
+ '/save': cmdSave,
397
+ '/login': cmdLogin,
398
+ '/logout': cmdLogout,
399
+ '/config': cmdConfig,
400
+ '/clear': cmdClear,
401
+ '/cls': cmdClear,
402
+ '/exit': cmdExit,
403
+ '/quit': cmdExit,
404
+ '/q': cmdExit,
405
+ };
406
+
407
+ function parse(line) {
408
+ const trimmed = line.trim();
409
+ if (!trimmed.startsWith('/')) return null;
410
+ const space = trimmed.indexOf(' ');
411
+ const name = space === -1 ? trimmed : trimmed.slice(0, space);
412
+ const args = space === -1 ? '' : trimmed.slice(space + 1);
413
+ return { name: name.toLowerCase(), args };
414
+ }
415
+
416
+ async function dispatch(ctx, line, helpers) {
417
+ const parsed = parse(line);
418
+ if (!parsed) return false;
419
+ const handler = REGISTRY[parsed.name];
420
+ if (!handler) {
421
+ console.log(`${ui.errorTag()} command gak dikenal: ${parsed.name} — coba ${ui.chalk.yellow('/help')}`);
422
+ return true;
423
+ }
424
+ await handler(ctx, parsed.args, helpers || {});
425
+ return true;
426
+ }
427
+
428
+ function renderListy(data, preferredKeys) {
429
+ const items = Array.isArray(data)
430
+ ? data
431
+ : (data && Array.isArray(data.data) ? data.data
432
+ : (data && Array.isArray(data.logs) ? data.logs
433
+ : (data && Array.isArray(data.activities) ? data.activities : null)));
434
+ if (items && items.length === 0) {
435
+ console.log(ui.chalk.gray('(kosong)'));
436
+ return;
437
+ }
438
+ if (items) {
439
+ items.slice(0, 50).forEach((it, idx) => {
440
+ console.log(ui.chalk.gray(`\n#${idx + 1}`));
441
+ if (typeof it !== 'object' || it === null) {
442
+ console.log(' ' + String(it));
443
+ return;
444
+ }
445
+ const keys = preferredKeys.filter((k) => k in it).concat(
446
+ Object.keys(it).filter((k) => !preferredKeys.includes(k))
447
+ );
448
+ for (const k of keys) {
449
+ const v = it[k];
450
+ if (typeof v === 'object' && v !== null) {
451
+ console.log(' ' + ui.kv(k, JSON.stringify(v)));
452
+ } else {
453
+ console.log(' ' + ui.kv(k, formatVal(v)));
454
+ }
455
+ }
456
+ });
457
+ if (items.length > 50) {
458
+ console.log(ui.chalk.gray(`\n…dan ${items.length - 50} entri lagi (di-truncate).`));
459
+ }
460
+ return;
461
+ }
462
+ console.log(ui.chalk.gray(ui.pretty(data)));
463
+ }
464
+
465
+ function showError(err) {
466
+ if (err instanceof LimitExceededError) {
467
+ console.log(ui.limitWarning(err.info || {}));
468
+ return;
469
+ }
470
+ console.log(`${ui.errorTag()} ${err.message}`);
471
+ }
472
+
473
+ function formatVal(v) {
474
+ if (v === null || v === undefined) return ui.chalk.gray('—');
475
+ if (typeof v === 'boolean') return v ? ui.chalk.green('true') : ui.chalk.red('false');
476
+ return String(v);
477
+ }
478
+
479
+ function num(v) {
480
+ if (v === undefined || v === null || v === '') return undefined;
481
+ const n = typeof v === 'number' ? v : parseFloat(v);
482
+ return Number.isFinite(n) ? n : undefined;
483
+ }
484
+
485
+ function truncate(s, n) {
486
+ if (!s) return '';
487
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
488
+ }
489
+
490
+ function maskKey(key) {
491
+ if (!key) return '';
492
+ if (key.length <= 10) return key.slice(0, 3) + '…' + key.slice(-2);
493
+ return key.slice(0, 6) + '…' + key.slice(-4);
494
+ }
495
+
496
+ function indent(s, n) {
497
+ const pad = ' '.repeat(n);
498
+ return s.split('\n').map((l) => pad + l).join('\n');
499
+ }
500
+
501
+ module.exports = { dispatch, parse, REGISTRY };