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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zelapi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Zelai
|
|
2
|
+
|
|
3
|
+
> Official CLI & SDK buat **Zelapi Agent API** — chat sama AI langsung dari terminal kamu, atau jalanin sebagai bot di Discord & Telegram.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
███████╗███████╗██╗ █████╗ ██╗ by ZELAPI
|
|
7
|
+
╚══███╔╝██╔════╝██║ ██╔══██╗██║
|
|
8
|
+
███╔╝ █████╗ ██║ ███████║██║
|
|
9
|
+
███╔╝ ██╔══╝ ██║ ██╔══██║██║
|
|
10
|
+
███████╗███████╗███████╗██║ ██║██║
|
|
11
|
+
╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`zelai` adalah CLI bergaya Claude/Gemini CLI: tinggal panggil `zelai` di terminal, masuk chat REPL, ngobrol sama model AI Zelapi (`opus`, `sonnet`, `haiku`) lewat satu unified endpoint `/api/v1/agent`. Session persistent (otomatis disimpan di folder `./session`), ada slash command (`/status`, `/usage`, `/activity`, dll), warning kalau quota habis, plus connector Discord & Telegram di folder `connector/`.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Lewat npm (paling gampang):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g zelai-cli
|
|
22
|
+
zelai --version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Setelah install, command `zelai` langsung available di terminal lo.
|
|
26
|
+
|
|
27
|
+
Atau clone dari source:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/zelapi/zelai.git
|
|
31
|
+
cd zelai
|
|
32
|
+
npm install
|
|
33
|
+
npm link # bikin command `zelai` global
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Atau jalanin tanpa link:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node bin/zelai.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
zelai login # masukin API key zel_…
|
|
46
|
+
zelai # masuk chat REPL
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Chat REPL look:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
zelai by ZELAPI · v1.0.0
|
|
53
|
+
● Zelai Agent CLI • online • zelapioffciall.dpdns.org
|
|
54
|
+
Ketik /help buat lihat command. /exit buat keluar.
|
|
55
|
+
|
|
56
|
+
you (haiku) › halo!
|
|
57
|
+
zelai(haiku) ›
|
|
58
|
+
hai juga! ada yang bisa aku bantu?
|
|
59
|
+
|
|
60
|
+
you (haiku) › /status
|
|
61
|
+
─────────────────────────────────────────
|
|
62
|
+
Server Status · https://zelapioffciall.dpdns.org/api/status
|
|
63
|
+
status OK
|
|
64
|
+
uptime 34h12m
|
|
65
|
+
version 1.0.0
|
|
66
|
+
─────────────────────────────────────────
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## One-shot mode
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
zelai -p "ringkasin paper attention is all you need dalam 3 poin"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Slash commands
|
|
76
|
+
|
|
77
|
+
| Command | Fungsi |
|
|
78
|
+
| ------------------ | --------------------------------------------------- |
|
|
79
|
+
| `/help` | Tampilin daftar command |
|
|
80
|
+
| `/status` | Cek `https://zelapioffciall.dpdns.org/api/status` |
|
|
81
|
+
| `/usage` | Cek pemakaian + kuota API key |
|
|
82
|
+
| `/activity` | Activity logs akun |
|
|
83
|
+
| `/model <name>` | Ganti model: `opus`, `sonnet`, `haiku` |
|
|
84
|
+
| `/endpoint <path>` | Ganti AI endpoint (mis. `/ai/claila`, `/ai/spawn`) |
|
|
85
|
+
| `/system <prompt>` | Set system prompt |
|
|
86
|
+
| `/new` | Mulai sesi baru |
|
|
87
|
+
| `/session <id>` | Set / lihat session ID manual |
|
|
88
|
+
| `/sessions` | List semua sesi tersimpan di `./session` |
|
|
89
|
+
| `/load <id│nomor>` | Load sesi tersimpan |
|
|
90
|
+
| `/forget <id│all>` | Hapus sesi tersimpan |
|
|
91
|
+
| `/save` | Paksa simpan sesi sekarang |
|
|
92
|
+
| `/login` | Set / ganti API key |
|
|
93
|
+
| `/logout` | Hapus API key |
|
|
94
|
+
| `/config` | Tampilin path & isi config |
|
|
95
|
+
| `/clear` | Bersihin layar |
|
|
96
|
+
| `/exit` | Keluar (alias: `/quit`, `/q`) |
|
|
97
|
+
|
|
98
|
+
## Session persistence
|
|
99
|
+
|
|
100
|
+
Folder `./session` **otomatis dibikin** pas pertama kali `zelai` jalan, di working directory tempat kamu nge-run. Setiap sesi disimpan sebagai `./session/<session-id>.json` — turn history, model, endpoint, system prompt semuanya lengkap.
|
|
101
|
+
|
|
102
|
+
Reload sesi otomatis: kalau kamu jalanin `zelai` lagi, last session di-resume dari file, jadi context tetap kebawa.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
zelai --session zelapi-026391vehak07 # langsung load sesi tertentu
|
|
106
|
+
zelai sessions # list semua sesi tersimpan
|
|
107
|
+
zelai resume zelapi-026391vehak07 # lanjutin sesi spesifik
|
|
108
|
+
zelai resume 1 # lanjutin sesi nomor 1 dari `zelai sessions`
|
|
109
|
+
zelai resume # lanjutin sesi paling baru (default)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Warning ketika limit habis
|
|
113
|
+
|
|
114
|
+
Kalau API key kamu kena rate limit / quota habis (HTTP 429 atau body yang nyebut `limit`/`quota`/`exhausted`), Zelai bakal nge-render box warning merah dengan info `used`, `limit`, `plan`, dan link upgrade. Plus, di awal sesi interaktif, Zelai cek soft kalau pemakaian udah ≥ 90% — ngasih heads-up.
|
|
115
|
+
|
|
116
|
+
## Programmatic SDK
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const { Zelai } = require('zelai');
|
|
120
|
+
|
|
121
|
+
const z = new Zelai({ apiKey: process.env.ZELAPI_KEY });
|
|
122
|
+
|
|
123
|
+
const { reply, sessionId } = await z.chat('halo!');
|
|
124
|
+
console.log(reply);
|
|
125
|
+
|
|
126
|
+
// Lanjutan sesi:
|
|
127
|
+
const next = await z.chat('lanjutin obrolannya', { sessionId });
|
|
128
|
+
console.log(next.reply);
|
|
129
|
+
|
|
130
|
+
// User endpoints:
|
|
131
|
+
console.log(await z.status());
|
|
132
|
+
console.log(await z.usage());
|
|
133
|
+
console.log(await z.activityLogs());
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Connectors (Discord + Telegram)
|
|
137
|
+
|
|
138
|
+
Lihat `connector/README.md`. Singkatnya:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Discord
|
|
142
|
+
npm install discord.js
|
|
143
|
+
ZELAPI_KEY=… DISCORD_TOKEN=… node connector/discord.js
|
|
144
|
+
|
|
145
|
+
# Telegram (zero extra deps)
|
|
146
|
+
ZELAPI_KEY=… TELEGRAM_TOKEN=… node connector/telegram.js
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Session map antar-channel disimpan di `./session/_connector-map.json` — chat di bot Discord tetap nyambung kalau sebelumnya lo udah ngobrol via CLI.
|
|
150
|
+
|
|
151
|
+
## API spec ringkas
|
|
152
|
+
|
|
153
|
+
| Field | Type | Notes |
|
|
154
|
+
| -------------------- | -------- | ------------------------------------------- |
|
|
155
|
+
| `endpoint` | string | **wajib** — mis. `/ai/claila` |
|
|
156
|
+
| `model` | string | opsional — `opus`/`sonnet`/`haiku` |
|
|
157
|
+
| `system` | string | opsional — system prompt |
|
|
158
|
+
| `messages` | array | **wajib** — `[{role, content, session?}]` |
|
|
159
|
+
| `messages[].session` | string | opsional — lanjutin sesi sebelumnya |
|
|
160
|
+
|
|
161
|
+
Auth: `Authorization: Bearer zel_xxxxxxx`.
|
|
162
|
+
|
|
163
|
+
Docs lengkap: <https://zelapioffciall.dpdns.org>
|
|
164
|
+
|
|
165
|
+
## Env vars
|
|
166
|
+
|
|
167
|
+
| Variable | Fungsi |
|
|
168
|
+
| ------------------ | -------------------------------------------- |
|
|
169
|
+
| `ZELAPI_KEY` | Default API key |
|
|
170
|
+
| `ZELAI_MODEL` | Default model untuk connectors |
|
|
171
|
+
| `ZELAI_ENDPOINT` | Default AI endpoint untuk connectors |
|
|
172
|
+
| `ZELAI_SYSTEM` | Default system prompt untuk connectors |
|
|
173
|
+
| `ZELAI_DEBUG` | Print stack trace kalau error |
|
|
174
|
+
| `DISCORD_TOKEN` | Token bot Discord |
|
|
175
|
+
| `DISCORD_PREFIX` | Prefix command Discord (default `!zelai`) |
|
|
176
|
+
| `TELEGRAM_TOKEN` | Token bot Telegram |
|
|
177
|
+
| `TELEGRAM_ALLOW` | Comma-separated chat/user IDs (whitelist) |
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT © Zelapi
|
package/bin/zelai.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Zelai CLI — Entry point
|
|
4
|
+
* Official command-line client for the Zelapi Agent API.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
require('../src/cli').main(process.argv.slice(2)).catch((err) => {
|
|
9
|
+
// Last-resort error handler so uncaught failures don't leak a stack trace.
|
|
10
|
+
const chalk = (() => {
|
|
11
|
+
try { return require('chalk'); } catch { return null; }
|
|
12
|
+
})();
|
|
13
|
+
const tag = chalk ? chalk.red.bold('✖ fatal') : '✖ fatal';
|
|
14
|
+
const msg = err && err.message ? err.message : String(err);
|
|
15
|
+
process.stderr.write(`${tag} ${msg}\n`);
|
|
16
|
+
if (process.env.ZELAI_DEBUG && err && err.stack) {
|
|
17
|
+
process.stderr.write(err.stack + '\n');
|
|
18
|
+
}
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Zelai Connectors
|
|
2
|
+
|
|
3
|
+
Connector ini bikin Zelai bisa nongol di luar terminal — jadi bot beneran di Discord & Telegram. Semua connector pake SDK yang sama (`require('zelai')` / `require('../src')`), session disimpan di folder `./session` yang sama, jadi `/sessions` di CLI bakal kelihatan juga sesi dari bot.
|
|
4
|
+
|
|
5
|
+
## Discord
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install discord.js
|
|
9
|
+
export ZELAPI_KEY=zel_xxxxxxxx
|
|
10
|
+
export DISCORD_TOKEN=...
|
|
11
|
+
# optional
|
|
12
|
+
export DISCORD_PREFIX="!zelai"
|
|
13
|
+
export ZELAI_MODEL=haiku
|
|
14
|
+
|
|
15
|
+
node connector/discord.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Bot bakal respons kalau:
|
|
19
|
+
- Di-mention (`@YourBot`)
|
|
20
|
+
- Pesan diawali prefix (default `!zelai`)
|
|
21
|
+
- DM langsung ke bot
|
|
22
|
+
|
|
23
|
+
Slash command yang di-support: `/help`, `/status`, `/usage`, `/activity`, `/new`, `/model`.
|
|
24
|
+
|
|
25
|
+
Bot intents yang dibutuhin di Discord Developer Portal:
|
|
26
|
+
- `MESSAGE CONTENT INTENT` ✅
|
|
27
|
+
- `SERVER MEMBERS INTENT` (opsional)
|
|
28
|
+
|
|
29
|
+
## Telegram
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export ZELAPI_KEY=zel_xxxxxxxx
|
|
33
|
+
export TELEGRAM_TOKEN=123456:ABC...
|
|
34
|
+
# optional — whitelist chat / user IDs (koma)
|
|
35
|
+
export TELEGRAM_ALLOW="123456789,987654321"
|
|
36
|
+
|
|
37
|
+
node connector/telegram.js
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Telegram connector pake **HTTP long polling** langsung — gak butuh dependency tambahan, cuma `axios` (yang udah ada). Cocok buat self-host di VPS.
|
|
41
|
+
|
|
42
|
+
Di group, bot cuma respons kalau:
|
|
43
|
+
- Di-mention (`@your_bot`)
|
|
44
|
+
- Reply ke pesan bot
|
|
45
|
+
- Pesan diawali `/`
|
|
46
|
+
|
|
47
|
+
Di DM, semua pesan diproses.
|
|
48
|
+
|
|
49
|
+
## Bagaimana session bekerja?
|
|
50
|
+
|
|
51
|
+
Tiap channel/DM punya **session key**, di-mapping ke session ID Zelapi di file `./session/_connector-map.json`. Begitu user balasan dari bot, server udah inget context-nya — Discord dan Telegram dapet experience yang persistent. Reset pakai `/new`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zelai — Discord connector.
|
|
3
|
+
*
|
|
4
|
+
* ENV:
|
|
5
|
+
* ZELAPI_KEY API key Zelai
|
|
6
|
+
* DISCORD_TOKEN Discord bot token
|
|
7
|
+
* DISCORD_PREFIX (opsional) prefix selain mention. Default: "!zelai"
|
|
8
|
+
* ZELAI_MODEL (opsional) opus|sonnet|haiku
|
|
9
|
+
* ZELAI_ENDPOINT (opsional) default /ai/claila
|
|
10
|
+
*
|
|
11
|
+
* Run:
|
|
12
|
+
* node connector/discord.js
|
|
13
|
+
*
|
|
14
|
+
* Bot harus punya intents: Guilds, GuildMessages, MessageContent, DirectMessages.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const ui = require('../src/ui');
|
|
19
|
+
const { makeSdk, handleSlash, runChat, logBanner } = require('./shared');
|
|
20
|
+
|
|
21
|
+
const PREFIX = process.env.DISCORD_PREFIX || '!zelai';
|
|
22
|
+
|
|
23
|
+
function requireDiscord() {
|
|
24
|
+
try {
|
|
25
|
+
return require('discord.js');
|
|
26
|
+
} catch {
|
|
27
|
+
console.log(ui.errorTag() + ' module `discord.js` belum ke-install. Jalanin:');
|
|
28
|
+
console.log(' ' + ui.chalk.cyan('npm install discord.js'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldHandle(client, message) {
|
|
34
|
+
if (message.author.bot) return false;
|
|
35
|
+
|
|
36
|
+
// DM → selalu handle
|
|
37
|
+
if (!message.guild) return true;
|
|
38
|
+
|
|
39
|
+
// Mention bot → handle
|
|
40
|
+
if (client.user && message.mentions && message.mentions.has(client.user)) return true;
|
|
41
|
+
|
|
42
|
+
// Prefix → handle
|
|
43
|
+
if (message.content && message.content.startsWith(PREFIX)) return true;
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripTrigger(client, message) {
|
|
49
|
+
let text = message.content || '';
|
|
50
|
+
if (client.user) {
|
|
51
|
+
const re = new RegExp(`<@!?${client.user.id}>`, 'g');
|
|
52
|
+
text = text.replace(re, '');
|
|
53
|
+
}
|
|
54
|
+
if (text.startsWith(PREFIX)) text = text.slice(PREFIX.length);
|
|
55
|
+
return text.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function channelKeyFor(message) {
|
|
59
|
+
if (message.guild) {
|
|
60
|
+
return `discord:guild:${message.guild.id}:channel:${message.channelId}`;
|
|
61
|
+
}
|
|
62
|
+
return `discord:dm:${message.author.id}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
const token = process.env.DISCORD_TOKEN;
|
|
67
|
+
if (!token) {
|
|
68
|
+
console.log(ui.errorTag() + ' DISCORD_TOKEN env wajib di-set.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const sdk = makeSdk();
|
|
72
|
+
logBanner('discord');
|
|
73
|
+
|
|
74
|
+
const Discord = requireDiscord();
|
|
75
|
+
const { Client, GatewayIntentBits, Partials, Events } = Discord;
|
|
76
|
+
|
|
77
|
+
const client = new Client({
|
|
78
|
+
intents: [
|
|
79
|
+
GatewayIntentBits.Guilds,
|
|
80
|
+
GatewayIntentBits.GuildMessages,
|
|
81
|
+
GatewayIntentBits.MessageContent,
|
|
82
|
+
GatewayIntentBits.DirectMessages,
|
|
83
|
+
],
|
|
84
|
+
partials: [Partials.Channel, Partials.Message],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
client.once(Events.ClientReady, (c) => {
|
|
88
|
+
console.log(`${ui.okTag()} login as ${ui.chalk.cyan.bold(c.user.tag)}`);
|
|
89
|
+
console.log(`${ui.infoTag()} mention bot atau pakai prefix ${ui.chalk.yellow(PREFIX)} buat ngobrol.`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
93
|
+
try {
|
|
94
|
+
if (!shouldHandle(client, message)) return;
|
|
95
|
+
|
|
96
|
+
const text = stripTrigger(client, message);
|
|
97
|
+
if (!text) {
|
|
98
|
+
await message.reply('halo! ketik pesan kamu setelah mention atau prefix. `/help` buat lihat command.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const channelKey = channelKeyFor(message);
|
|
103
|
+
|
|
104
|
+
// Typing indicator (best effort)
|
|
105
|
+
try { message.channel.sendTyping && message.channel.sendTyping(); } catch {}
|
|
106
|
+
|
|
107
|
+
const slash = await handleSlash(sdk, channelKey, text).catch((err) => {
|
|
108
|
+
return { handled: true, text: '❌ ' + (err.message || 'error') };
|
|
109
|
+
});
|
|
110
|
+
if (slash.handled) {
|
|
111
|
+
await safeReply(message, slash.text);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const reply = await runChat(sdk, channelKey, text);
|
|
116
|
+
await safeReply(message, reply);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
try { await message.reply('❌ error: ' + (err.message || err)); } catch {}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await client.login(token);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function safeReply(message, text) {
|
|
126
|
+
if (!text) text = '(empty)';
|
|
127
|
+
// Discord max message length = 2000. Split kalau panjang.
|
|
128
|
+
const MAX = 1900;
|
|
129
|
+
if (text.length <= MAX) {
|
|
130
|
+
await message.reply(text);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let buf = text;
|
|
135
|
+
while (buf.length > MAX) {
|
|
136
|
+
let cut = buf.lastIndexOf('\n', MAX);
|
|
137
|
+
if (cut < MAX / 2) cut = MAX;
|
|
138
|
+
chunks.push(buf.slice(0, cut));
|
|
139
|
+
buf = buf.slice(cut);
|
|
140
|
+
}
|
|
141
|
+
chunks.push(buf);
|
|
142
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
143
|
+
if (i === 0) await message.reply(chunks[i]);
|
|
144
|
+
else await message.channel.send(chunks[i]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (require.main === module) {
|
|
149
|
+
main().catch((err) => {
|
|
150
|
+
console.log(ui.errorTag() + ' ' + (err.message || err));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { main };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers yang dipakai bareng sama Discord & Telegram connector.
|
|
3
|
+
* Tiap platform punya state per-channel/per-user, jadi kita mapping channelKey → session id.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const { Zelai } = require('../src');
|
|
11
|
+
const session = require('../src/session');
|
|
12
|
+
const ui = require('../src/ui');
|
|
13
|
+
|
|
14
|
+
const STATE_FILE = path.join(session.sessionDir(), '_connector-map.json');
|
|
15
|
+
|
|
16
|
+
function loadMap() {
|
|
17
|
+
if (!fs.existsSync(STATE_FILE)) return {};
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function saveMap(map) {
|
|
26
|
+
session.ensureDir();
|
|
27
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(map, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSessionFor(channelKey) {
|
|
31
|
+
const map = loadMap();
|
|
32
|
+
return map[channelKey] || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setSessionFor(channelKey, sessionId) {
|
|
36
|
+
const map = loadMap();
|
|
37
|
+
map[channelKey] = sessionId;
|
|
38
|
+
saveMap(map);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resetSessionFor(channelKey) {
|
|
42
|
+
const map = loadMap();
|
|
43
|
+
delete map[channelKey];
|
|
44
|
+
saveMap(map);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeSdk(opts = {}) {
|
|
48
|
+
const apiKey = opts.apiKey || process.env.ZELAPI_KEY;
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
throw new Error('ZELAPI_KEY env wajib di-set buat connector.');
|
|
51
|
+
}
|
|
52
|
+
return new Zelai({
|
|
53
|
+
apiKey,
|
|
54
|
+
model: opts.model || process.env.ZELAI_MODEL || 'haiku',
|
|
55
|
+
endpoint: opts.endpoint || process.env.ZELAI_ENDPOINT || '/ai/claila',
|
|
56
|
+
system: opts.system || process.env.ZELAI_SYSTEM
|
|
57
|
+
|| 'You are Zelai, a helpful assistant. Reply in the same language as the user. Keep replies concise for chat.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Universal command handler — dipakai sebelum kita kirim ke LLM.
|
|
63
|
+
* Return { handled: true, text } kalau command direspons, else { handled: false }.
|
|
64
|
+
*
|
|
65
|
+
* Pendekatan: setiap connector kirim msg ke handler ini dulu. Kalau handled,
|
|
66
|
+
* platform tinggal kirim teksnya. Kalau gak, terus ke runChat().
|
|
67
|
+
*/
|
|
68
|
+
async function handleSlash(sdk, channelKey, raw) {
|
|
69
|
+
const text = (raw || '').trim();
|
|
70
|
+
if (!text.startsWith('/')) return { handled: false };
|
|
71
|
+
|
|
72
|
+
const space = text.indexOf(' ');
|
|
73
|
+
const name = (space === -1 ? text : text.slice(0, space)).toLowerCase();
|
|
74
|
+
const args = space === -1 ? '' : text.slice(space + 1).trim();
|
|
75
|
+
|
|
76
|
+
if (name === '/help' || name === '/?') {
|
|
77
|
+
return {
|
|
78
|
+
handled: true,
|
|
79
|
+
text:
|
|
80
|
+
'**Zelai Bot — slash commands**\n' +
|
|
81
|
+
'`/status` cek status server\n' +
|
|
82
|
+
'`/usage` cek pemakaian API key\n' +
|
|
83
|
+
'`/activity` activity logs terakhir\n' +
|
|
84
|
+
'`/new` mulai sesi baru di channel ini\n' +
|
|
85
|
+
'`/model opus|sonnet|haiku` ganti model\n' +
|
|
86
|
+
'`/help` tampilin pesan ini',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (name === '/status') {
|
|
91
|
+
const data = await sdk.status();
|
|
92
|
+
const root = (data && data.data) || data || {};
|
|
93
|
+
const fields = ['status', 'state', 'uptime', 'version', 'region'];
|
|
94
|
+
const lines = ['**Server status**'];
|
|
95
|
+
let found = 0;
|
|
96
|
+
for (const k of fields) {
|
|
97
|
+
if (root[k] !== undefined) { lines.push(`• \`${k}\`: ${root[k]}`); found++; }
|
|
98
|
+
}
|
|
99
|
+
if (!found) lines.push('```json\n' + JSON.stringify(root, null, 2).slice(0, 1500) + '\n```');
|
|
100
|
+
return { handled: true, text: lines.join('\n') };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (name === '/usage' || name === '/quota') {
|
|
104
|
+
try {
|
|
105
|
+
const data = await sdk.usage();
|
|
106
|
+
return { handled: true, text: '**Usage**\n```json\n' + JSON.stringify(data, null, 2).slice(0, 1500) + '\n```' };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err && err.name === 'LimitExceededError') {
|
|
109
|
+
return { handled: true, text: '⚠️ **Limit habis** — kuota API key kamu udah penuh.' };
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === '/activity' || name === '/logs') {
|
|
116
|
+
const data = await sdk.activityLogs();
|
|
117
|
+
return { handled: true, text: '**Activity logs**\n```json\n' + JSON.stringify(data, null, 2).slice(0, 1500) + '\n```' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (name === '/new' || name === '/reset') {
|
|
121
|
+
resetSessionFor(channelKey);
|
|
122
|
+
return { handled: true, text: '🧹 sesi di-reset buat channel ini.' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (name === '/model') {
|
|
126
|
+
const next = args.toLowerCase();
|
|
127
|
+
if (!['opus', 'sonnet', 'haiku'].includes(next)) {
|
|
128
|
+
return { handled: true, text: 'pilih: `opus`, `sonnet`, atau `haiku`.' };
|
|
129
|
+
}
|
|
130
|
+
sdk.config.model = next;
|
|
131
|
+
return { handled: true, text: `✅ model diganti ke \`${next}\` buat sesi runtime ini.` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { handled: true, text: `command \`${name}\` belum dikenal — coba \`/help\`.` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Jalanin chat AI buat satu pesan dari platform.
|
|
139
|
+
* sdk : Zelai SDK instance
|
|
140
|
+
* channelKey : unique key per channel/DM (e.g. "discord:guild:123:user:456")
|
|
141
|
+
* userText : pesan dari user
|
|
142
|
+
*/
|
|
143
|
+
async function runChat(sdk, channelKey, userText) {
|
|
144
|
+
const sessionId = getSessionFor(channelKey);
|
|
145
|
+
|
|
146
|
+
let res;
|
|
147
|
+
try {
|
|
148
|
+
res = await sdk.chat(userText, { sessionId });
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err && err.name === 'LimitExceededError') {
|
|
151
|
+
return '⚠️ **Limit habis** — kuota API key kamu udah penuh. Upgrade di https://zelapioffciall.dpdns.org/pricing';
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (res.sessionId && res.sessionId !== sessionId) {
|
|
157
|
+
setSessionFor(channelKey, res.sessionId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Persist ke folder ./session juga biar kelihatan di `zelai sessions`.
|
|
161
|
+
const sid = res.sessionId || sessionId || session.localId();
|
|
162
|
+
try {
|
|
163
|
+
session.append(sid, { role: 'user', content: userText }, {
|
|
164
|
+
model: sdk.config.model, endpoint: sdk.config.endpoint, system: sdk.config.system,
|
|
165
|
+
});
|
|
166
|
+
session.append(sid, { role: 'assistant', content: res.reply || '' });
|
|
167
|
+
if (!getSessionFor(channelKey)) setSessionFor(channelKey, sid);
|
|
168
|
+
} catch {
|
|
169
|
+
// best-effort — kalau gagal nulis disk, tetap reply
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return res.reply || '(gak ada balasan)';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function logBanner(name) {
|
|
176
|
+
console.log(ui.banner());
|
|
177
|
+
console.log(ui.chalk.gray(' connector: ') + ui.chalk.cyan.bold(name));
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
makeSdk,
|
|
183
|
+
handleSlash,
|
|
184
|
+
runChat,
|
|
185
|
+
getSessionFor,
|
|
186
|
+
setSessionFor,
|
|
187
|
+
resetSessionFor,
|
|
188
|
+
logBanner,
|
|
189
|
+
};
|