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/config.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Config manager — simpan API key & preferensi user di ~/.zelai/config.json
3
+ */
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ const HOME = os.homedir();
11
+ const CONFIG_DIR = path.join(HOME, '.zelai');
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
+
14
+ const DEFAULTS = {
15
+ apiKey: null,
16
+ baseUrl: 'https://api.zelapioffciall.dpdns.org',
17
+ statusUrl: 'https://zelapioffciall.dpdns.org/api/status',
18
+ userBaseUrl: 'https://zelapioffciall.dpdns.org',
19
+ model: 'haiku',
20
+ endpoint: '/ai/claila',
21
+ system: 'You are Zelai, a helpful and concise assistant. Reply in the same language as the user.',
22
+ lastSession: null,
23
+ };
24
+
25
+ function ensureDir() {
26
+ if (!fs.existsSync(CONFIG_DIR)) {
27
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
28
+ }
29
+ }
30
+
31
+ function load() {
32
+ ensureDir();
33
+ if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULTS };
34
+ try {
35
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
36
+ const parsed = JSON.parse(raw);
37
+ return { ...DEFAULTS, ...parsed };
38
+ } catch {
39
+ return { ...DEFAULTS };
40
+ }
41
+ }
42
+
43
+ function save(cfg) {
44
+ ensureDir();
45
+ const merged = { ...DEFAULTS, ...cfg };
46
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
47
+ return merged;
48
+ }
49
+
50
+ function update(patch) {
51
+ const current = load();
52
+ return save({ ...current, ...patch });
53
+ }
54
+
55
+ function clear() {
56
+ if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
57
+ }
58
+
59
+ function configPath() {
60
+ return CONFIG_FILE;
61
+ }
62
+
63
+ module.exports = { load, save, update, clear, configPath, DEFAULTS };
package/src/index.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Programmatic SDK entry point.
3
+ *
4
+ * const { Zelai } = require('zelai');
5
+ * const z = new Zelai({ apiKey: 'zel_...' });
6
+ * const { reply, sessionId } = await z.chat('halo');
7
+ * const next = await z.chat('lanjut', { sessionId });
8
+ */
9
+ 'use strict';
10
+
11
+ const { makeClient } = require('./client');
12
+ const config = require('./config');
13
+
14
+ class Zelai {
15
+ constructor(opts = {}) {
16
+ const defaults = config.DEFAULTS;
17
+ this.config = {
18
+ apiKey: opts.apiKey || process.env.ZELAPI_KEY || null,
19
+ baseUrl: opts.baseUrl || defaults.baseUrl,
20
+ statusUrl: opts.statusUrl || defaults.statusUrl,
21
+ userBaseUrl: opts.userBaseUrl || defaults.userBaseUrl,
22
+ model: opts.model || defaults.model,
23
+ endpoint: opts.endpoint || defaults.endpoint,
24
+ system: opts.system != null ? opts.system : defaults.system,
25
+ };
26
+ this.client = makeClient(this.config);
27
+ }
28
+
29
+ /**
30
+ * Single-turn helper.
31
+ * chat('halo') → kirim 1 user message
32
+ * chat('halo', { sessionId, model, endpoint, system })
33
+ */
34
+ async chat(content, opts = {}) {
35
+ const messages = Array.isArray(content)
36
+ ? content
37
+ : [{ role: 'user', content: String(content) }];
38
+ return this.client.sendAgent({
39
+ messages,
40
+ model: opts.model || this.config.model,
41
+ endpoint: opts.endpoint || this.config.endpoint,
42
+ system: opts.system != null ? opts.system : this.config.system,
43
+ sessionId: opts.sessionId || null,
44
+ });
45
+ }
46
+
47
+ status() {
48
+ return this.client.getStatus();
49
+ }
50
+ activityLogs(apiKey) {
51
+ return this.client.getActivityLogs(apiKey);
52
+ }
53
+ usage(apiKey) {
54
+ return this.client.getUsage(apiKey);
55
+ }
56
+ }
57
+
58
+ module.exports = { Zelai, makeClient, config };
package/src/session.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Session manager.
3
+ *
4
+ * Sessions disimpan sebagai JSON file di ./session/<id>.json (relatif ke cwd
5
+ * tempat user nge-jalanin `zelai`). Folder otomatis dibikin pas pertama kali
6
+ * dipake — gak perlu mkdir manual.
7
+ *
8
+ * File format:
9
+ * {
10
+ * id: "zelapi-…",
11
+ * createdAt: 1700000000000,
12
+ * updatedAt: 1700000000000,
13
+ * model: "haiku",
14
+ * endpoint: "/ai/claila",
15
+ * system: "…",
16
+ * history: [{role, content, ts}, …]
17
+ * }
18
+ */
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const SESSION_DIR_NAME = 'session';
25
+
26
+ function sessionDir() {
27
+ return path.resolve(process.cwd(), SESSION_DIR_NAME);
28
+ }
29
+
30
+ function ensureDir() {
31
+ const dir = sessionDir();
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ return dir;
36
+ }
37
+
38
+ function safeName(id) {
39
+ // Strip everything except alnum, dash, underscore — bikin filename aman.
40
+ return String(id).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120);
41
+ }
42
+
43
+ function sessionFile(id) {
44
+ return path.join(ensureDir(), safeName(id) + '.json');
45
+ }
46
+
47
+ function localId() {
48
+ const r = Math.random().toString(36).slice(2, 10);
49
+ const t = Date.now().toString(36).slice(-6);
50
+ return `local-${t}${r}`;
51
+ }
52
+
53
+ function load(id) {
54
+ if (!id) return null;
55
+ const f = sessionFile(id);
56
+ if (!fs.existsSync(f)) return null;
57
+ try {
58
+ const raw = fs.readFileSync(f, 'utf8');
59
+ return JSON.parse(raw);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function save(record) {
66
+ if (!record || !record.id) throw new Error('session record butuh field `id`');
67
+ ensureDir();
68
+ const f = sessionFile(record.id);
69
+ const next = { ...record, updatedAt: Date.now() };
70
+ fs.writeFileSync(f, JSON.stringify(next, null, 2));
71
+ return next;
72
+ }
73
+
74
+ function create({ id, model, endpoint, system }) {
75
+ const sid = id || localId();
76
+ const record = {
77
+ id: sid,
78
+ createdAt: Date.now(),
79
+ updatedAt: Date.now(),
80
+ model: model || null,
81
+ endpoint: endpoint || null,
82
+ system: system || null,
83
+ history: [],
84
+ };
85
+ return save(record);
86
+ }
87
+
88
+ /**
89
+ * Append entry ke history-nya sebuah session — kalau belum ada, dibikin baru.
90
+ */
91
+ function append(id, entry, meta) {
92
+ let rec = load(id);
93
+ if (!rec) {
94
+ rec = create({ id, ...(meta || {}) });
95
+ }
96
+ rec.history.push({ ...entry, ts: Date.now() });
97
+ if (meta) {
98
+ if (meta.model) rec.model = meta.model;
99
+ if (meta.endpoint) rec.endpoint = meta.endpoint;
100
+ if (meta.system) rec.system = meta.system;
101
+ }
102
+ return save(rec);
103
+ }
104
+
105
+ /**
106
+ * Replace ID — kepake pas server ngasih real session id setelah kita pakai
107
+ * local-… ID dulu. Rename file dan return record yang udah update.
108
+ */
109
+ function rename(oldId, newId) {
110
+ if (!oldId || !newId || oldId === newId) return load(oldId || newId);
111
+ const oldFile = sessionFile(oldId);
112
+ if (!fs.existsSync(oldFile)) return null;
113
+ let rec;
114
+ try {
115
+ rec = JSON.parse(fs.readFileSync(oldFile, 'utf8'));
116
+ } catch {
117
+ return null;
118
+ }
119
+ rec.id = newId;
120
+ const next = save(rec);
121
+ try { fs.unlinkSync(oldFile); } catch {}
122
+ return next;
123
+ }
124
+
125
+ function remove(id) {
126
+ if (!id) return false;
127
+ const f = sessionFile(id);
128
+ if (!fs.existsSync(f)) return false;
129
+ fs.unlinkSync(f);
130
+ return true;
131
+ }
132
+
133
+ function list() {
134
+ const dir = ensureDir();
135
+ const out = [];
136
+ for (const name of fs.readdirSync(dir)) {
137
+ if (!name.endsWith('.json')) continue;
138
+ try {
139
+ const raw = fs.readFileSync(path.join(dir, name), 'utf8');
140
+ const rec = JSON.parse(raw);
141
+ out.push({
142
+ id: rec.id || name.replace(/\.json$/, ''),
143
+ createdAt: rec.createdAt || 0,
144
+ updatedAt: rec.updatedAt || 0,
145
+ model: rec.model || null,
146
+ endpoint: rec.endpoint || null,
147
+ turns: Array.isArray(rec.history) ? rec.history.length : 0,
148
+ preview: previewFor(rec),
149
+ });
150
+ } catch {
151
+ // skip corrupted
152
+ }
153
+ }
154
+ out.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
155
+ return out;
156
+ }
157
+
158
+ function previewFor(rec) {
159
+ if (!rec || !Array.isArray(rec.history)) return '';
160
+ const firstUser = rec.history.find((m) => m && m.role === 'user' && m.content);
161
+ if (!firstUser) return '';
162
+ const s = String(firstUser.content).replace(/\s+/g, ' ').trim();
163
+ return s.length > 60 ? s.slice(0, 59) + '…' : s;
164
+ }
165
+
166
+ module.exports = {
167
+ sessionDir,
168
+ ensureDir,
169
+ sessionFile,
170
+ localId,
171
+ load,
172
+ save,
173
+ create,
174
+ append,
175
+ rename,
176
+ remove,
177
+ list,
178
+ };
package/src/ui.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * UI helpers — banner, prompt, markdown renderer untuk terminal.
3
+ */
4
+ 'use strict';
5
+
6
+ const chalk = require('chalk');
7
+ const { marked } = require('marked');
8
+
9
+ let TerminalRenderer;
10
+ try {
11
+ TerminalRenderer = require('marked-terminal');
12
+ if (TerminalRenderer.default) TerminalRenderer = TerminalRenderer.default;
13
+ } catch {
14
+ TerminalRenderer = null;
15
+ }
16
+
17
+ if (TerminalRenderer) {
18
+ try {
19
+ marked.setOptions({
20
+ renderer: new TerminalRenderer({
21
+ firstHeading: chalk.magentaBright.bold,
22
+ heading: chalk.magenta.bold,
23
+ strong: chalk.bold,
24
+ em: chalk.italic,
25
+ codespan: chalk.yellow,
26
+ code: chalk.yellow,
27
+ blockquote: chalk.gray.italic,
28
+ link: chalk.cyan.underline,
29
+ listitem: chalk.white,
30
+ reflowText: false,
31
+ tab: 2,
32
+ }),
33
+ });
34
+ } catch {
35
+ // ignored — falls back to plain text
36
+ }
37
+ }
38
+
39
+ const VERSION = require('../package.json').version;
40
+
41
+ function banner() {
42
+ const c = chalk.magentaBright;
43
+ const accent = chalk.cyan;
44
+ const dim = chalk.gray;
45
+ const yellow = chalk.yellow;
46
+
47
+ // ASCII ZELAI — block style. "by ZELAPI" subtitle on the side.
48
+ const lines = [
49
+ '',
50
+ c.bold(' ███████╗███████╗██╗ █████╗ ██╗ '),
51
+ c.bold(' ╚══███╔╝██╔════╝██║ ██╔══██╗██║ '),
52
+ c.bold(' ███╔╝ █████╗ ██║ ███████║██║ ') + dim(' by ') + chalk.magenta.bold('ZELAPI'),
53
+ c.bold(' ███╔╝ ██╔══╝ ██║ ██╔══██║██║ ') + dim(' v') + dim(VERSION),
54
+ c.bold(' ███████╗███████╗███████╗██║ ██║██║ ') + dim(' Protocol V1'),
55
+ c.bold(' ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ '),
56
+ '',
57
+ ' ' + accent.bold('● ') + accent.bold('Zelai Agent CLI') + dim(' • ') + chalk.green('online') + dim(' • ') + accent.underline('zelapioffciall.dpdns.org'),
58
+ ' ' + dim('Ketik ') + yellow('/help') + dim(' buat lihat command. ') + yellow('/exit') + dim(' buat keluar. ') + yellow('Ctrl+D') + dim(' juga bisa.'),
59
+ '',
60
+ ];
61
+ return lines.join('\n');
62
+ }
63
+
64
+ function smallBanner() {
65
+ const c = chalk.magentaBright;
66
+ const dim = chalk.gray;
67
+ return c.bold('zelai') + dim(' by ZELAPI · v' + VERSION);
68
+ }
69
+
70
+ function renderMarkdown(text) {
71
+ if (!text) return '';
72
+ try {
73
+ const out = marked.parse(text);
74
+ return typeof out === 'string' ? out.trimEnd() : String(out).trimEnd();
75
+ } catch {
76
+ return text;
77
+ }
78
+ }
79
+
80
+ function userPrompt(model) {
81
+ const m = chalk.gray(`(${model})`);
82
+ return `${chalk.cyan.bold('you')} ${m} ${chalk.gray('›')} `;
83
+ }
84
+
85
+ function assistantTag(model) {
86
+ return chalk.magentaBright.bold('zelai') + chalk.gray(`(${model})`) + chalk.gray(' ›');
87
+ }
88
+
89
+ function systemTag() {
90
+ return chalk.yellow.bold('system') + chalk.gray(' ›');
91
+ }
92
+
93
+ function errorTag() {
94
+ return chalk.red.bold('error') + chalk.gray(' ›');
95
+ }
96
+
97
+ function warnTag() {
98
+ return chalk.yellow.bold('⚠ warning') + chalk.gray(' ›');
99
+ }
100
+
101
+ function infoTag() {
102
+ return chalk.blueBright.bold('info') + chalk.gray(' ›');
103
+ }
104
+
105
+ function okTag() {
106
+ return chalk.green.bold('✓') + chalk.gray(' ›');
107
+ }
108
+
109
+ function hr() {
110
+ const w = Math.min(process.stdout.columns || 80, 100);
111
+ return chalk.gray('─'.repeat(w));
112
+ }
113
+
114
+ function kv(key, value) {
115
+ return `${chalk.gray(key.padEnd(14))} ${chalk.white(value)}`;
116
+ }
117
+
118
+ function pretty(obj) {
119
+ try {
120
+ return JSON.stringify(obj, null, 2);
121
+ } catch {
122
+ return String(obj);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Render a clean "limit hit" box.
128
+ */
129
+ function limitWarning(info) {
130
+ const dim = chalk.gray;
131
+ const red = chalk.red.bold;
132
+ const top = red('┏━━ LIMIT EXCEEDED ' + '━'.repeat(40));
133
+ const bot = red('┗' + '━'.repeat(58));
134
+ const lines = [
135
+ '',
136
+ top,
137
+ red('┃ ') + chalk.yellow('Kuota API kamu udah habis 🚫'),
138
+ red('┃ '),
139
+ ];
140
+ 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)));
145
+ }
146
+ lines.push(red('┃ '));
147
+ lines.push(red('┃ ') + dim('Upgrade plan di ') + chalk.cyan.underline('https://zelapioffciall.dpdns.org/pricing'));
148
+ lines.push(bot);
149
+ lines.push('');
150
+ return lines.join('\n');
151
+ }
152
+
153
+ module.exports = {
154
+ banner,
155
+ smallBanner,
156
+ renderMarkdown,
157
+ userPrompt,
158
+ assistantTag,
159
+ systemTag,
160
+ errorTag,
161
+ warnTag,
162
+ infoTag,
163
+ okTag,
164
+ hr,
165
+ kv,
166
+ pretty,
167
+ limitWarning,
168
+ chalk,
169
+ };