@chispear/vibekit 0.2.0 → 0.4.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/README.md +15 -8
- package/dist/agent.js +273 -23
- package/dist/banner.js +36 -0
- package/dist/cli.js +203 -87
- package/dist/router.js +1 -1
- package/package.json +3 -1
- package/scripts/banner.js +34 -0
package/README.md
CHANGED
|
@@ -61,21 +61,25 @@ vibekit status
|
|
|
61
61
|
| Perintah | Fungsi |
|
|
62
62
|
|---|---|
|
|
63
63
|
| `vibekit init` | Buat `~/.vibekit/config.json` dari template (tidak menimpa yang sudah ada) |
|
|
64
|
-
| `vibekit ask "<pertanyaan>" [--file <path>]... [--dir <path>]
|
|
65
|
-
| `vibekit chat` | Percakapan interaktif; riwayat dipangkas
|
|
66
|
-
| `vibekit do "<tugas>"` | Mode agent: model mengerjakan tugas nyata dengan tool file/perintah di folder saat ini (lihat bagian Mode Agent di bawah) |
|
|
64
|
+
| `vibekit ask "<pertanyaan>" [--file <path>]... [--dir <path>]... [--auto]` | Satu pertanyaan yang boleh berakhir dengan aksi; lampirkan file/folder sebagai konteks terseleksi. Ringkasan hemat token ditulis ke stderr, jawaban bersih di stdout, ditutup baris info model + token terpakai |
|
|
65
|
+
| `vibekit chat [--auto]` | Percakapan interaktif yang juga bisa bertindak di tengah obrolan; riwayat aksi lama dipadatkan jadi ringkasan lalu dipangkas sadar-tool (maks ±4.000 token, hanya pada batas giliran utuh); `/keluar` untuk berhenti, `/auto` untuk menyala-matikan mode auto-allow |
|
|
66
|
+
| `vibekit do "<tugas>"` | Mode agent sekali jalan: model mengerjakan tugas nyata dengan tool file/perintah di folder saat ini (lihat bagian Mode Agent di bawah) |
|
|
67
67
|
| `vibekit status` | Tabel per penyedia: model, status (aktif / cooldown s.d. jam / anggaran tercapai), token terpakai hari ini |
|
|
68
68
|
| `vibekit --help` / `--version` | Bantuan / versi |
|
|
69
69
|
|
|
70
|
-
## Mode Agent — `
|
|
70
|
+
## Mode Agent — `ask`, `chat`, dan `do`
|
|
71
71
|
|
|
72
|
-
Selain menjawab pertanyaan, VibeKit bisa **
|
|
72
|
+
Selain menjawab pertanyaan, VibeKit bisa **bertindak** di folder proyek Anda. Sejak v0.3.0 kemampuan agent ini menyatu ke semua perintah — pertanyaan biasa dijawab teks seperti biasa, sedangkan permintaan membuat file atau menjalankan perintah dikerjakan lewat tool dengan pagar keamanan yang sama:
|
|
73
|
+
|
|
74
|
+
- **`vibekit ask`** — satu pertanyaan yang boleh berakhir dengan aksi: `vibekit ask "buatkan file halo.txt berisi sapaan"`.
|
|
75
|
+
- **`vibekit chat`** — ngobrol yang bisa bertindak di tengah percakapan: minta dibuatkan file di giliran mana pun, model memakai tool lalu melanjutkan obrolan dengan riwayat yang utuh (termasuk rangkaian tool-nya).
|
|
76
|
+
- **`vibekit do`** — mode tugas sekali jalan: jejak langkah dicetak, ditutup ringkasan akhir, dan kode keluar mencerminkan berhasil/tidaknya tugas:
|
|
73
77
|
|
|
74
78
|
```bash
|
|
75
79
|
vibekit do "buatkan file halo.txt berisi sapaan, lalu tampilkan daftar file di sini"
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yang sama, failover router yang sama), sampai ia membalas
|
|
82
|
+
Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yang sama, failover router yang sama), sampai ia membalas teks akhir. Ada **5 tool**:
|
|
79
83
|
|
|
80
84
|
| Tool | Fungsi | Konfirmasi? |
|
|
81
85
|
|---|---|---|
|
|
@@ -89,7 +93,9 @@ Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yan
|
|
|
89
93
|
|
|
90
94
|
- **Hanya folder kerja (cwd).** Semua path di-resolve terhadap folder tempat perintah dijalankan; path yang keluar dari folder itu (mis. `../`) ditolak eksekutor, bahkan bila pengguna menyetujui.
|
|
91
95
|
- **Konfirmasi per aksi.** Setiap aksi yang mengubah file atau menjalankan perintah menampilkan deskripsi aksinya (path + pratinjau konten untuk tulis, potongan teks lama→baru untuk edit, perintah lengkap untuk shell) dan hanya berjalan bila Anda menjawab `y`/`ya` — selain itu dianggap menolak, dan penolakan itu dilaporkan ke model agar ia menyesuaikan rencana.
|
|
92
|
-
- **
|
|
96
|
+
- **Mode auto-allow (opsional, sejak v0.4.0).** Flag `--auto` (alias `--yes`) pada `ask`, `chat`, dan `do` — atau perintah `/auto` di tengah sesi chat — melewati konfirmasi per aksi: semua aksi tulis/perintah langsung dijalankan. Pagar folder kerja TIDAK ikut dilepas (path traversal tetap ditolak), tetapi risikonya nyata: model bisa menulis atau menjalankan sesuatu tanpa sempat Anda periksa. Hanya aktifkan di folder yang aman (folder uji, proyek sekali pakai) dan untuk tugas yang Anda percayai.
|
|
97
|
+
- **Batas 12 langkah** per giliran/tugas, dan hasil tiap tool dipangkas (±4.000 karakter) sebelum dikirim balik ke model demi hemat token.
|
|
98
|
+
- **Riwayat chat yang aman untuk tool.** Pemangkasan riwayat chat hanya terjadi pada batas giliran utuh (satu pesan pengguna + seluruh rangkaian tool-nya), sehingga tidak pernah ada pesan hasil tool yang yatim atau panggilan tool yang kehilangan hasilnya.
|
|
93
99
|
|
|
94
100
|
## Cara Failover Bekerja
|
|
95
101
|
|
|
@@ -104,6 +110,7 @@ Model bekerja langkah demi langkah lewat tool calling (API OpenAI-compatible yan
|
|
|
104
110
|
- **System prompt ringkas** — gaya `poin-penting` memerintahkan model menjawab hanya poin penting tanpa basa-basi; tersedia juga `normal` dan `detail`.
|
|
105
111
|
- **Batas jawaban keras** — `maksTokenJawaban` menjadi `max_tokens` di setiap permintaan.
|
|
106
112
|
- **Konteks terseleksi** — `--file/--dir` melewati file biner & file > 256 KB, memangkas baris komentar penuh dan baris kosong berlebih, dan membatasi total konteks (±16.000 token) dengan memotong berkas terbesar. Selisih token mentah vs terseleksi selalu ditampilkan.
|
|
113
|
+
- **Kompaksi riwayat aksi (chat)** — rangkaian tool giliran-giliran lama dipadatkan menjadi ringkasan satu baris (`[Aksi selesai: tulis_file index.html — OK]`) sebelum giliran baru dimulai, jadi isi file yang pernah ditulis/dibaca model tidak dikirim ulang ke API di setiap giliran. Giliran terakhir tidak pernah disentuh kompaksi, sehingga `thought_signature` Gemini tetap utuh selama dibutuhkan.
|
|
107
114
|
- **Anggaran & pencatatan** — pemakaian per penyedia per hari terlihat di `vibekit status`, sehingga boros selalu kelihatan.
|
|
108
115
|
|
|
109
116
|
## Pengembangan
|
|
@@ -118,7 +125,7 @@ Keputusan repo: `dist/` masuk `.gitignore` dan dibangun saat publish (`prepublis
|
|
|
118
125
|
|
|
119
126
|
## Status Nama di npm
|
|
120
127
|
|
|
121
|
-
Package ini terbit di npm sebagai **`@chispear/vibekit`** (v0.1.0, 25 Oktober 2026; v0.2.0 menambahkan mode agent `vibekit do`) karena nama tak-scoped `vibekit` sudah dipakai proyek lain. Nama perintah CLI tetap `vibekit`.
|
|
128
|
+
Package ini terbit di npm sebagai **`@chispear/vibekit`** (v0.1.0, 25 Oktober 2026; v0.2.0 menambahkan mode agent `vibekit do`; v0.3.0 menggabungkan kemampuan agent itu ke `ask` dan `chat`) karena nama tak-scoped `vibekit` sudah dipakai proyek lain. Nama perintah CLI tetap `vibekit`.
|
|
122
129
|
|
|
123
130
|
## Lisensi
|
|
124
131
|
|
package/dist/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { exec } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tanyaRouter } from "./router.js";
|
|
5
|
+
import { hitungToken } from "./token.js";
|
|
5
6
|
/** Hasil tool dipangkas sampai ±karakter ini sebelum dikirim balik ke model (hemat token). */
|
|
6
7
|
const BATAS_HASIL_TOOL = 4000;
|
|
7
8
|
/** Batas waktu jalankan_perintah. */
|
|
@@ -99,6 +100,20 @@ const SYSTEM_PROMPT_AGENT = [
|
|
|
99
100
|
"Jangan mengarang isi file: gunakan daftar_file/baca_file dulu bila perlu mengetahui isi folder/file.",
|
|
100
101
|
"Bila tugas selesai (atau tidak bisa dilanjutkan), balas TANPA memanggil tool, berisi ringkasan akhir yang singkat dalam poin-poin penting: apa yang dilakukan dan hasilnya.",
|
|
101
102
|
].join(" ");
|
|
103
|
+
/**
|
|
104
|
+
* System prompt gabungan untuk `ask` dan `chat` (sejak v0.3.0): model adalah
|
|
105
|
+
* lawan bicara yang menjawab pertanyaan biasa langsung sebagai teks, tetapi
|
|
106
|
+
* juga bisa bertindak memakai tool yang sama seperti mode agent bila diminta.
|
|
107
|
+
*/
|
|
108
|
+
export const SYSTEM_PROMPT_GABUNGAN = [
|
|
109
|
+
"Kamu adalah lawan bicara VibeKit yang bekerja di dalam SATU folder kerja pengguna.",
|
|
110
|
+
"Jawab pertanyaan biasa langsung sebagai teks, singkat dan hanya poin-poin penting.",
|
|
111
|
+
"Bila pengguna meminta dibuatkan/diubah file atau menjalankan perintah, gunakan tool yang tersedia: daftar_file, baca_file, tulis_file, edit_file, jalankan_perintah.",
|
|
112
|
+
"Semua path bersifat relatif terhadap folder kerja; jangan pernah mencoba mengakses apa pun di luar folder itu.",
|
|
113
|
+
"Aksi tulis_file, edit_file, dan jalankan_perintah selalu meminta persetujuan pengguna dulu; bila pengguna menolak, jangan memaksa dan jangan mengulang aksi yang sama.",
|
|
114
|
+
"Jangan mengarang isi file: gunakan daftar_file/baca_file dulu bila perlu mengetahui isi folder/file.",
|
|
115
|
+
"Setelah bertindak, tutup giliran dengan balasan teks (tanpa tool) berisi ringkasan singkat: apa yang dilakukan dan hasilnya. Ringkasan itu TIDAK BOLEH mengulang isi file yang ditulis atau dibaca — cukup sebutkan nama file dan hasilnya.",
|
|
116
|
+
].join(" ");
|
|
102
117
|
/**
|
|
103
118
|
* Resolve path masukan terhadap cwd. Mengembalikan null bila hasil resolve
|
|
104
119
|
* keluar dari cwd (path traversal) — kunci keamanan mode agent.
|
|
@@ -137,6 +152,8 @@ function argumenRingkas(nama, arg) {
|
|
|
137
152
|
return potong(JSON.stringify(arg), 80);
|
|
138
153
|
}
|
|
139
154
|
}
|
|
155
|
+
/** Nama tool yang mengubah sesuatu — wajib konfirmasi kecuali mode auto-allow. */
|
|
156
|
+
const TOOL_MUTASI = new Set(["tulis_file", "edit_file", "jalankan_perintah"]);
|
|
140
157
|
/**
|
|
141
158
|
* Jalankan satu panggilan tool. Selalu mengembalikan teks hasil untuk model
|
|
142
159
|
* (tidak pernah throw): keberhasilan diawali "OK:", kegagalan "ERROR:",
|
|
@@ -152,6 +169,10 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
152
169
|
return `ERROR: argumen untuk tool ${nama} bukan JSON yang valid.`;
|
|
153
170
|
}
|
|
154
171
|
const { cwd, konfirmasi } = konteks;
|
|
172
|
+
// Mode auto-allow: callback konfirmasi pengguna tidak dipanggil sama sekali —
|
|
173
|
+
// aksi mutasi langsung disetujui. Pagar cwd (resolusiAman) TIDAK berubah dan
|
|
174
|
+
// tetap menolak path traversal walau auto-allow aktif.
|
|
175
|
+
const konfirmasiEfektif = konteks.autoAllow ? async () => true : konfirmasi;
|
|
155
176
|
const ambilPath = (mentah) => {
|
|
156
177
|
const masukan = typeof mentah === "string" && mentah.trim() !== "" ? mentah : ".";
|
|
157
178
|
const tujuan = resolusiAman(cwd, masukan);
|
|
@@ -188,7 +209,7 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
188
209
|
const konten = typeof arg.konten === "string" ? arg.konten : "";
|
|
189
210
|
const deskripsi = `tulis_file → ${p.rel} (${konten.length} karakter)\n` +
|
|
190
211
|
`Pratinjau konten:\n${potong(konten, 300)}`;
|
|
191
|
-
if (!(await
|
|
212
|
+
if (!(await konfirmasiEfektif(deskripsi))) {
|
|
192
213
|
return `DITOLAK: pengguna menolak aksi tulis_file ke ${p.rel}. File tidak ditulis. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
|
|
193
214
|
}
|
|
194
215
|
await fs.mkdir(path.dirname(p.tujuan), { recursive: true });
|
|
@@ -209,7 +230,7 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
209
230
|
const deskripsi = `edit_file → ${p.rel}\n` +
|
|
210
231
|
`Teks lama: ${potong(teksLama, 150)}\n` +
|
|
211
232
|
`Teks baru: ${potong(teksBaru, 150)}`;
|
|
212
|
-
if (!(await
|
|
233
|
+
if (!(await konfirmasiEfektif(deskripsi))) {
|
|
213
234
|
return `DITOLAK: pengguna menolak aksi edit_file di ${p.rel}. File tidak diubah. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
|
|
214
235
|
}
|
|
215
236
|
const baru = isi.slice(0, idx) + teksBaru + isi.slice(idx + teksLama.length);
|
|
@@ -221,7 +242,7 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
221
242
|
if (!perintah)
|
|
222
243
|
return "ERROR: argumen perintah kosong.";
|
|
223
244
|
const deskripsi = `jalankan_perintah → ${perintah}`;
|
|
224
|
-
if (!(await
|
|
245
|
+
if (!(await konfirmasiEfektif(deskripsi))) {
|
|
225
246
|
return `DITOLAK: pengguna menolak menjalankan perintah "${perintah}". Perintah tidak dijalankan. Jangan mengulang aksi yang sama; sesuaikan rencana atau akhiri dengan ringkasan.`;
|
|
226
247
|
}
|
|
227
248
|
const hasil = await new Promise((resolve) => {
|
|
@@ -256,17 +277,24 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
279
|
/**
|
|
259
|
-
*
|
|
260
|
-
* diminta model lewat eksekutor aman → kirim hasil tool balik
|
|
261
|
-
* model membalas teks tanpa tool_calls
|
|
280
|
+
* Inti loop agent untuk SATU giliran: panggil router (dengan tools) → jalankan
|
|
281
|
+
* tool_calls yang diminta model lewat eksekutor aman → kirim hasil tool balik
|
|
282
|
+
* → ulangi sampai model membalas teks tanpa tool_calls atau langkah habis.
|
|
283
|
+
* Dipakai bersama oleh `do` (lewat jalankanAgent), `ask`, dan `chat`.
|
|
284
|
+
*
|
|
285
|
+
* Catatan kegagalan: bila router gagal di tengah giliran, `messages` mungkin
|
|
286
|
+
* sudah memuat rangkaian parsial (assistant tool_calls + hasil tool tanpa teks
|
|
287
|
+
* akhir). Pemanggil yang menyimpan riwayat (chat) sebaiknya menggulung balik
|
|
288
|
+
* giliran itu; pemanggil sekali jalan (ask/do) bisa mengabaikannya.
|
|
262
289
|
*/
|
|
263
|
-
export async function
|
|
290
|
+
export async function jalankanGiliran(opsi) {
|
|
264
291
|
const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
|
|
292
|
+
const messages = opsi.messages;
|
|
265
293
|
const catatan = [];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
let totalToken = 0;
|
|
295
|
+
let semuaUsage = true;
|
|
296
|
+
let providerTerakhir = null;
|
|
297
|
+
const sumber = () => (semuaUsage ? "usage" : "estimasi");
|
|
270
298
|
let langkah = 0;
|
|
271
299
|
while (langkah < maksLangkah) {
|
|
272
300
|
langkah++;
|
|
@@ -279,18 +307,49 @@ export async function jalankanAgent(opsi) {
|
|
|
279
307
|
});
|
|
280
308
|
catatan.push(...hasil.catatan);
|
|
281
309
|
if (!hasil.ok) {
|
|
282
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
jawabanAkhir: "",
|
|
313
|
+
providerTerakhir,
|
|
314
|
+
totalToken,
|
|
315
|
+
tokenSumber: sumber(),
|
|
316
|
+
jumlahLangkah: langkah,
|
|
317
|
+
catatan,
|
|
318
|
+
pesanGagal: hasil.pesan,
|
|
319
|
+
};
|
|
283
320
|
}
|
|
321
|
+
totalToken += hasil.tokenTerpakai;
|
|
322
|
+
if (hasil.tokenSumber !== "usage")
|
|
323
|
+
semuaUsage = false;
|
|
324
|
+
providerTerakhir = hasil.provider;
|
|
284
325
|
const panggilan = hasil.toolCalls ?? [];
|
|
285
326
|
if (panggilan.length === 0) {
|
|
286
|
-
// Balasan teks murni =
|
|
287
|
-
|
|
327
|
+
// Balasan teks murni = jawaban akhir giliran; simpan ke riwayat agar utuh.
|
|
328
|
+
messages.push({ role: "assistant", content: hasil.jawaban });
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
jawabanAkhir: hasil.jawaban,
|
|
332
|
+
providerTerakhir,
|
|
333
|
+
totalToken,
|
|
334
|
+
tokenSumber: sumber(),
|
|
335
|
+
jumlahLangkah: langkah,
|
|
336
|
+
catatan,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (hasil.pesanMentah && typeof hasil.pesanMentah === "object") {
|
|
340
|
+
// Teruskan pesan assistant persis seperti yang dikirim penyedia. Gemini 3
|
|
341
|
+
// menyisipkan thought_signature di dalam tool_calls (extra_content) dan
|
|
342
|
+
// menolak permintaan berikutnya dengan HTTP 400 bila tanda tangan itu
|
|
343
|
+
// hilang dari riwayat — jadi pesan mentah tidak boleh dibangun ulang.
|
|
344
|
+
messages.push(hasil.pesanMentah);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
messages.push({
|
|
348
|
+
role: "assistant",
|
|
349
|
+
content: hasil.jawaban ? hasil.jawaban : null,
|
|
350
|
+
tool_calls: panggilan,
|
|
351
|
+
});
|
|
288
352
|
}
|
|
289
|
-
messages.push({
|
|
290
|
-
role: "assistant",
|
|
291
|
-
content: hasil.jawaban ? hasil.jawaban : null,
|
|
292
|
-
tool_calls: panggilan,
|
|
293
|
-
});
|
|
294
353
|
for (const p of panggilan) {
|
|
295
354
|
let argRingkas = "";
|
|
296
355
|
try {
|
|
@@ -299,8 +358,13 @@ export async function jalankanAgent(opsi) {
|
|
|
299
358
|
catch {
|
|
300
359
|
argRingkas = "";
|
|
301
360
|
}
|
|
302
|
-
opsi.
|
|
303
|
-
|
|
361
|
+
const penandaAuto = opsi.autoAllow && TOOL_MUTASI.has(p.function.name) ? " (auto)" : "";
|
|
362
|
+
opsi.jejak?.(`langkah ${langkah}: ${p.function.name}${argRingkas ? ` (${argRingkas})` : ""}${penandaAuto}`);
|
|
363
|
+
const hasilTool = await eksekusiTool(p, {
|
|
364
|
+
cwd: opsi.cwd,
|
|
365
|
+
konfirmasi: opsi.konfirmasi,
|
|
366
|
+
autoAllow: opsi.autoAllow,
|
|
367
|
+
});
|
|
304
368
|
opsi.jejak?.(` → ${potong(hasilTool.split("\n")[0], 120)}`);
|
|
305
369
|
messages.push({
|
|
306
370
|
role: "tool",
|
|
@@ -312,9 +376,195 @@ export async function jalankanAgent(opsi) {
|
|
|
312
376
|
}
|
|
313
377
|
return {
|
|
314
378
|
ok: false,
|
|
315
|
-
|
|
316
|
-
|
|
379
|
+
jawabanAkhir: "",
|
|
380
|
+
providerTerakhir,
|
|
381
|
+
totalToken,
|
|
382
|
+
tokenSumber: sumber(),
|
|
317
383
|
jumlahLangkah: langkah,
|
|
318
384
|
catatan,
|
|
385
|
+
batasTercapai: true,
|
|
319
386
|
};
|
|
320
387
|
}
|
|
388
|
+
/** Perkiraan token satu pesan: konten teks ditambah argumen tool_calls (bila ada). */
|
|
389
|
+
function tokenPesan(m) {
|
|
390
|
+
let teks = m.content ?? "";
|
|
391
|
+
if (m.tool_calls && m.tool_calls.length > 0)
|
|
392
|
+
teks += "\n" + JSON.stringify(m.tool_calls);
|
|
393
|
+
return hitungToken(teks);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Pangkas riwayat percakapan secara SADAR-TOOL: pemotongan hanya terjadi pada
|
|
397
|
+
* batas giliran utuh. Satu giliran = satu pesan "user" beserta seluruh
|
|
398
|
+
* rangkaian sesudahnya sampai sebelum "user" berikutnya (assistant tool_calls,
|
|
399
|
+
* hasil-hasil tool, assistant teks akhir). Fungsi ini TIDAK PERNAH menyisakan
|
|
400
|
+
* pesan "tool" yatim atau assistant tool_calls tanpa hasil tool-nya.
|
|
401
|
+
*
|
|
402
|
+
* Awalan pesan "system" (bila ada) selalu dipertahankan karena bukan bagian
|
|
403
|
+
* giliran mana pun. Giliran terakhir selalu dipertahankan utuh, jadi hasil
|
|
404
|
+
* boleh tetap melebihi maksToken bila satu giliran terakhir saja sudah
|
|
405
|
+
* sebesar itu. Tidak mengubah array masukan; mengembalikan array baru.
|
|
406
|
+
*/
|
|
407
|
+
export function pangkasRiwayatAman(messages, maksToken) {
|
|
408
|
+
let awal = 0;
|
|
409
|
+
while (awal < messages.length && messages[awal].role === "system")
|
|
410
|
+
awal++;
|
|
411
|
+
const awalan = messages.slice(0, awal);
|
|
412
|
+
const sisa = messages.slice(awal);
|
|
413
|
+
// Kelompokkan sisa menjadi giliran-giliran: grup baru dimulai di setiap
|
|
414
|
+
// pesan "user"; pesan sebelum "user" pertama membentuk grupnya sendiri.
|
|
415
|
+
const grup = [];
|
|
416
|
+
for (const m of sisa) {
|
|
417
|
+
if (m.role === "user" || grup.length === 0)
|
|
418
|
+
grup.push([m]);
|
|
419
|
+
else
|
|
420
|
+
grup[grup.length - 1].push(m);
|
|
421
|
+
}
|
|
422
|
+
const tokenGrup = (g) => g.reduce((s, m) => s + tokenPesan(m), 0);
|
|
423
|
+
let total = awalan.reduce((s, m) => s + tokenPesan(m), 0) + grup.reduce((s, g) => s + tokenGrup(g), 0);
|
|
424
|
+
// Buang giliran tertua secara utuh sampai muat; giliran terakhir tak dibuang.
|
|
425
|
+
while (total > maksToken && grup.length > 1) {
|
|
426
|
+
total -= tokenGrup(grup.shift());
|
|
427
|
+
}
|
|
428
|
+
return [...awalan, ...grup.flat()];
|
|
429
|
+
}
|
|
430
|
+
/** Target ringkas satu panggilan tool untuk pesan ringkasan kompaksi. */
|
|
431
|
+
function targetRingkasTool(panggilan) {
|
|
432
|
+
let arg = {};
|
|
433
|
+
try {
|
|
434
|
+
arg = JSON.parse(panggilan.function.arguments || "{}");
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Argumen rusak: target dikosongkan, ringkasan tetap dibuat dari nama tool.
|
|
438
|
+
}
|
|
439
|
+
if (panggilan.function.name === "jalankan_perintah") {
|
|
440
|
+
return potong(String(arg.perintah ?? ""), 40);
|
|
441
|
+
}
|
|
442
|
+
return String(arg.path ?? ".");
|
|
443
|
+
}
|
|
444
|
+
/** Status satu hasil tool dari awalannya: "OK" | "ERROR" | "DITOLAK" | kata pertama. */
|
|
445
|
+
function statusHasilTool(konten) {
|
|
446
|
+
const t = (konten ?? "").trimStart();
|
|
447
|
+
if (t.startsWith("OK"))
|
|
448
|
+
return "OK";
|
|
449
|
+
if (t.startsWith("ERROR"))
|
|
450
|
+
return "ERROR";
|
|
451
|
+
if (t.startsWith("DITOLAK"))
|
|
452
|
+
return "DITOLAK";
|
|
453
|
+
return t.split(/[:\s]/)[0] || "?";
|
|
454
|
+
}
|
|
455
|
+
/** Padatkan satu giliran LAMA (bukan giliran terakhir) — lihat kompaksiRiwayatAksi. */
|
|
456
|
+
function kompaksiSatuGiliran(giliran) {
|
|
457
|
+
const keluar = [];
|
|
458
|
+
let i = 0;
|
|
459
|
+
while (i < giliran.length) {
|
|
460
|
+
const m = giliran[i];
|
|
461
|
+
if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
|
|
462
|
+
// Kumpulkan pesan-pesan tool milik rangkaian ini.
|
|
463
|
+
const hasilTool = [];
|
|
464
|
+
let j = i + 1;
|
|
465
|
+
while (j < giliran.length && giliran[j].role === "tool") {
|
|
466
|
+
hasilTool.push(giliran[j]);
|
|
467
|
+
j++;
|
|
468
|
+
}
|
|
469
|
+
// Teks pendamping pada pesan assistant (jarang ada) dipertahankan.
|
|
470
|
+
if (typeof m.content === "string" && m.content.trim() !== "") {
|
|
471
|
+
keluar.push({ role: "assistant", content: m.content });
|
|
472
|
+
}
|
|
473
|
+
// Satu pesan ringkasan per panggilan tool, menggantikan seluruh
|
|
474
|
+
// rangkaian — argumen lengkap (termasuk isi file) dan thought_signature
|
|
475
|
+
// ikut terbuang di sini, memang itu tujuannya (hemat token).
|
|
476
|
+
for (const p of m.tool_calls) {
|
|
477
|
+
const pasangan = hasilTool.find((t) => t.tool_call_id === p.id) ?? hasilTool[0];
|
|
478
|
+
const status = statusHasilTool(pasangan?.content ?? null);
|
|
479
|
+
const target = targetRingkasTool(p);
|
|
480
|
+
keluar.push({
|
|
481
|
+
role: "assistant",
|
|
482
|
+
content: `[Aksi selesai: ${p.function.name}${target ? ` ${target}` : ""} — ${status}]`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
i = j;
|
|
486
|
+
}
|
|
487
|
+
else if (m.role === "tool") {
|
|
488
|
+
// Pesan tool yatim tanpa rangkaian assistant pendahulu dalam giliran
|
|
489
|
+
// yang sama: buang (tidak sah dikirim ke API tanpa pendahulunya).
|
|
490
|
+
i++;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
keluar.push(m);
|
|
494
|
+
i++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return keluar;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Kompaksi riwayat aksi untuk chat: ganti rangkaian tool pada giliran-giliran
|
|
501
|
+
* LAMA menjadi ringkasan teks satu baris, agar isi file yang ditulis model
|
|
502
|
+
* (argumen tool_calls) tidak ikut dikirim ulang ke API pada setiap giliran
|
|
503
|
+
* berikutnya. Bentuk ringkasan: `[Aksi selesai: <tool> <target> — <status>]`.
|
|
504
|
+
*
|
|
505
|
+
* Aturan keras:
|
|
506
|
+
* - Giliran TERAKHIR tidak disentuh sama sekali — pesan assistant mentahnya
|
|
507
|
+
* (termasuk thought_signature Gemini di dalam tool_calls) wajib utuh
|
|
508
|
+
* selama giliran itu masih aktif/baru selesai.
|
|
509
|
+
* - Pesan sebelum "user" pertama (mis. system) dipertahankan apa adanya.
|
|
510
|
+
* - Pesan user dan assistant teks biasa dipertahankan apa adanya.
|
|
511
|
+
* - Mengembalikan array BARU; array masukan tidak diubah.
|
|
512
|
+
*/
|
|
513
|
+
export function kompaksiRiwayatAksi(messages) {
|
|
514
|
+
let awal = 0;
|
|
515
|
+
while (awal < messages.length && messages[awal].role !== "user")
|
|
516
|
+
awal++;
|
|
517
|
+
const awalan = messages.slice(0, awal);
|
|
518
|
+
const sisa = messages.slice(awal);
|
|
519
|
+
const grup = [];
|
|
520
|
+
for (const m of sisa) {
|
|
521
|
+
if (m.role === "user" || grup.length === 0)
|
|
522
|
+
grup.push([m]);
|
|
523
|
+
else
|
|
524
|
+
grup[grup.length - 1].push(m);
|
|
525
|
+
}
|
|
526
|
+
const hasil = [...awalan];
|
|
527
|
+
grup.forEach((giliran, gi) => {
|
|
528
|
+
if (gi === grup.length - 1)
|
|
529
|
+
hasil.push(...giliran);
|
|
530
|
+
else
|
|
531
|
+
hasil.push(...kompaksiSatuGiliran(giliran));
|
|
532
|
+
});
|
|
533
|
+
return hasil;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Mode tugas sekali jalan (`vibekit do`): satu giliran agent dengan system
|
|
537
|
+
* prompt pelaksana tugas, dibangun di atas jalankanGiliran. Perilaku dan
|
|
538
|
+
* bentuk HasilAgent sama seperti sebelum refaktor v0.3.0.
|
|
539
|
+
*/
|
|
540
|
+
export async function jalankanAgent(opsi) {
|
|
541
|
+
const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
|
|
542
|
+
const messages = [
|
|
543
|
+
{ role: "system", content: SYSTEM_PROMPT_AGENT },
|
|
544
|
+
{ role: "user", content: opsi.tugas },
|
|
545
|
+
];
|
|
546
|
+
const hasil = await jalankanGiliran({
|
|
547
|
+
config: opsi.config,
|
|
548
|
+
messages,
|
|
549
|
+
lokasiState: opsi.lokasiState,
|
|
550
|
+
cwd: opsi.cwd,
|
|
551
|
+
konfirmasi: opsi.konfirmasi,
|
|
552
|
+
autoAllow: opsi.autoAllow,
|
|
553
|
+
fetchImpl: opsi.fetchImpl,
|
|
554
|
+
maksLangkah: opsi.maksLangkah,
|
|
555
|
+
jejak: opsi.jejak,
|
|
556
|
+
});
|
|
557
|
+
if (hasil.pesanGagal !== undefined) {
|
|
558
|
+
return { ok: false, ringkasan: hasil.pesanGagal, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
|
|
559
|
+
}
|
|
560
|
+
if (hasil.batasTercapai) {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
ringkasan: `Batas langkah tercapai (${maksLangkah} langkah) sebelum tugas selesai. ` +
|
|
564
|
+
"Jalankan lagi perintah yang sama untuk melanjutkan, atau persempit tugasnya.",
|
|
565
|
+
jumlahLangkah: hasil.jumlahLangkah,
|
|
566
|
+
catatan: hasil.catatan,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return { ok: true, ringkasan: hasil.jawabanAkhir, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
|
|
570
|
+
}
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Banner CHISPEAR hijau besar — ditampilkan saat `vibekit init` pertama kali
|
|
2
|
+
// (momen "berhasil terpasang & diatur" yang pasti terlihat pengguna, karena
|
|
3
|
+
// keluaran skrip postinstall npm tidak selalu ditampilkan npm).
|
|
4
|
+
// CATATAN: scripts/banner.js adalah salinan mandiri dari seni ini untuk
|
|
5
|
+
// postinstall — jaga keduanya tetap sinkron.
|
|
6
|
+
const GLIF = {
|
|
7
|
+
C: [" █████ ", "██ ██", "██ ", "██ ", "██ ██", " █████ "],
|
|
8
|
+
H: ["██ ██", "██ ██", "███████", "██ ██", "██ ██", "██ ██"],
|
|
9
|
+
I: ["███", " █ ", " █ ", " █ ", " █ ", "███"],
|
|
10
|
+
S: ["███████", "██ ", "██████ ", " ██", " ██", "██████ "],
|
|
11
|
+
P: ["██████ ", "██ ██", "██████ ", "██ ", "██ ", "██ "],
|
|
12
|
+
E: ["███████", "██ ", "█████ ", "█████ ", "██ ", "███████"],
|
|
13
|
+
A: [" █████ ", "██ ██", "███████", "██ ██", "██ ██", "██ ██"],
|
|
14
|
+
R: ["██████ ", "██ ██", "██████ ", "██ ██ ", "██ ██", "██ ██"],
|
|
15
|
+
};
|
|
16
|
+
/** Teks banner CHISPEAR (hijau terang bila NO_COLOR tidak diset). */
|
|
17
|
+
export function teksBanner() {
|
|
18
|
+
const baris = ["", "", "", "", "", ""];
|
|
19
|
+
for (const huruf of "CHISPEAR") {
|
|
20
|
+
const g = GLIF[huruf];
|
|
21
|
+
if (!g)
|
|
22
|
+
continue;
|
|
23
|
+
for (let i = 0; i < 6; i++)
|
|
24
|
+
baris[i] += (baris[i] ? " " : "") + g[i];
|
|
25
|
+
}
|
|
26
|
+
const pakaiWarna = !process.env.NO_COLOR;
|
|
27
|
+
const HIJAU = pakaiWarna ? "\x1b[1;92m" : "";
|
|
28
|
+
const RESET = pakaiWarna ? "\x1b[0m" : "";
|
|
29
|
+
return [
|
|
30
|
+
"",
|
|
31
|
+
...baris.map((b) => HIJAU + b + RESET),
|
|
32
|
+
"",
|
|
33
|
+
HIJAU + " VibeKit CLI terpasang — router AI hemat tokenmu siap dipakai." + RESET,
|
|
34
|
+
"",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import { buatConfigGlobal, globalConfigPath, muatConfig, statePath, } from "./config.js";
|
|
4
|
-
import { jalankanAgent } from "./agent.js";
|
|
4
|
+
import { jalankanAgent, jalankanGiliran, kompaksiRiwayatAksi, pangkasRiwayatAman, SYSTEM_PROMPT_GABUNGAN, } from "./agent.js";
|
|
5
|
+
import { teksBanner } from "./banner.js";
|
|
5
6
|
import { bangunKonteks } from "./context.js";
|
|
6
|
-
import { tanyaRouter } from "./router.js";
|
|
7
7
|
import { formatJam, muatState, sedangCooldown, statePenyedia, } from "./state.js";
|
|
8
|
-
import { formatAngka
|
|
9
|
-
const VERSI = "0.
|
|
8
|
+
import { formatAngka } from "./token.js";
|
|
9
|
+
const VERSI = "0.4.0";
|
|
10
10
|
const BATAS_TOKEN_RIWAYAT = 4000;
|
|
11
11
|
function bantuan() {
|
|
12
12
|
console.log(`VibeKit CLI v${VERSI} — router AI hemat token dengan failover otomatis
|
|
13
13
|
|
|
14
14
|
Penggunaan:
|
|
15
15
|
vibekit init Buat ~/.vibekit/config.json dari template
|
|
16
|
-
vibekit ask "<pertanyaan>" [opsi] Tanya sekali
|
|
17
|
-
vibekit chat Percakapan interaktif (perintah /keluar untuk berhenti)
|
|
18
|
-
vibekit do "<tugas>" Mode agent: kerjakan tugas dengan tool file/perintah di folder ini
|
|
16
|
+
vibekit ask "<pertanyaan>" [opsi] Tanya sekali (bisa bertindak: membuat file & menjalankan perintah dengan persetujuanmu)
|
|
17
|
+
vibekit chat Percakapan interaktif yang juga bisa bertindak (perintah /keluar untuk berhenti)
|
|
18
|
+
vibekit do "<tugas>" Mode agent sekali jalan: kerjakan tugas dengan tool file/perintah di folder ini
|
|
19
19
|
vibekit status Status & pemakaian token tiap penyedia hari ini
|
|
20
20
|
vibekit --help Tampilkan bantuan ini
|
|
21
21
|
vibekit --version Tampilkan versi
|
|
@@ -23,12 +23,26 @@ Penggunaan:
|
|
|
23
23
|
Opsi ask:
|
|
24
24
|
--file <path> Lampirkan file sebagai konteks (boleh diulang)
|
|
25
25
|
--dir <path> Lampirkan isi folder sebagai konteks (boleh diulang)
|
|
26
|
+
--auto, --yes Mode auto-allow: aksi tulis/perintah langsung dijalankan
|
|
27
|
+
tanpa persetujuan (tetap terkunci di folder kerja)
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
Opsi chat & do:
|
|
30
|
+
--auto, --yes Mode auto-allow seperti di atas. Di tengah sesi chat,
|
|
31
|
+
perintah /auto menyalakan/mematikan mode ini kapan saja.
|
|
32
|
+
|
|
33
|
+
Kemampuan agent (ask, chat, do):
|
|
34
|
+
Sejak v0.3.0 ketiga perintah itu bisa bertindak, bukan hanya menjawab:
|
|
35
|
+
bila kamu meminta dibuatkan/diubah file atau menjalankan perintah, model
|
|
36
|
+
memakai tool di folder tempat perintah dijalankan. Tool baca (daftar_file,
|
|
37
|
+
baca_file) berjalan langsung; tool yang mengubah sesuatu (tulis_file,
|
|
38
|
+
edit_file, jalankan_perintah) selalu meminta persetujuanmu satu per satu
|
|
39
|
+
— jawaban selain y/ya berarti ditolak. Batas 12 langkah per giliran.
|
|
40
|
+
Mode auto-allow (--auto/--yes, atau /auto di chat) melewati persetujuan
|
|
41
|
+
itu: semua aksi langsung dijalankan, pagar folder kerja tetap berlaku.
|
|
42
|
+
Hanya aktifkan di folder yang aman dan untuk tugas yang kamu percayai.
|
|
43
|
+
Perbedaannya: ask = satu pertanyaan yang boleh berakhir dengan aksi,
|
|
44
|
+
chat = percakapan yang bisa bertindak di tengah obrolan,
|
|
45
|
+
do = mode tugas sekali jalan dengan jejak langkah & ringkasan akhir.
|
|
32
46
|
Contoh: vibekit do "buatkan file halo.txt berisi sapaan, lalu tampilkan daftar file di sini"
|
|
33
47
|
|
|
34
48
|
Cara kerja singkat:
|
|
@@ -41,6 +55,7 @@ Cara kerja singkat:
|
|
|
41
55
|
function perintahInit() {
|
|
42
56
|
const dibuat = buatConfigGlobal();
|
|
43
57
|
if (dibuat) {
|
|
58
|
+
console.log(teksBanner());
|
|
44
59
|
console.log(`Konfigurasi dibuat: ${globalConfigPath()}`);
|
|
45
60
|
console.log("Isi kolom \"apiKey\" untuk tiap penyedia yang ingin dipakai (atau pakai sintaks \"env:NAMA_VAR\").");
|
|
46
61
|
console.log("Urutan penyedia = prioritas: yang pertama adalah bawaan, berikutnya cadangan failover.");
|
|
@@ -51,10 +66,15 @@ function perintahInit() {
|
|
|
51
66
|
}
|
|
52
67
|
return 0;
|
|
53
68
|
}
|
|
69
|
+
/** Satu baris peringatan yang dicetak di awal perintah saat mode auto-allow aktif. */
|
|
70
|
+
function cetakPeringatanAuto() {
|
|
71
|
+
console.error("[vibekit] Mode auto-allow AKTIF: aksi tulis file & menjalankan perintah akan dijalankan TANPA persetujuanmu. Agent tetap terkunci di folder kerja ini.");
|
|
72
|
+
}
|
|
54
73
|
function uraiArgAsk(args) {
|
|
55
74
|
const files = [];
|
|
56
75
|
const dirs = [];
|
|
57
76
|
const sisa = [];
|
|
77
|
+
let auto = false;
|
|
58
78
|
for (let i = 0; i < args.length; i++) {
|
|
59
79
|
const a = args[i];
|
|
60
80
|
if (a === "--file") {
|
|
@@ -75,6 +95,9 @@ function uraiArgAsk(args) {
|
|
|
75
95
|
else if (a.startsWith("--dir=")) {
|
|
76
96
|
dirs.push(a.slice("--dir=".length));
|
|
77
97
|
}
|
|
98
|
+
else if (a === "--auto" || a === "--yes") {
|
|
99
|
+
auto = true;
|
|
100
|
+
}
|
|
78
101
|
else {
|
|
79
102
|
sisa.push(a);
|
|
80
103
|
}
|
|
@@ -82,20 +105,48 @@ function uraiArgAsk(args) {
|
|
|
82
105
|
const pertanyaan = sisa.join(" ").trim();
|
|
83
106
|
if (!pertanyaan)
|
|
84
107
|
return { error: "Pertanyaan kosong. Contoh: vibekit ask \"apa itu API?\"" };
|
|
85
|
-
return { pertanyaan, files, dirs };
|
|
108
|
+
return { pertanyaan, files, dirs, auto };
|
|
86
109
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
function buatPembacaBaris() {
|
|
111
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
112
|
+
const antrian = [];
|
|
113
|
+
const penunggu = [];
|
|
114
|
+
let berakhir = false;
|
|
115
|
+
rl.on("line", (baris) => {
|
|
116
|
+
const p = penunggu.shift();
|
|
117
|
+
if (p)
|
|
118
|
+
p(baris);
|
|
119
|
+
else
|
|
120
|
+
antrian.push(baris);
|
|
121
|
+
});
|
|
122
|
+
rl.on("close", () => {
|
|
123
|
+
berakhir = true;
|
|
124
|
+
while (penunggu.length > 0)
|
|
125
|
+
penunggu.shift()(null);
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
tanya: (prompt) => {
|
|
129
|
+
process.stdout.write(prompt);
|
|
130
|
+
const siap = antrian.shift();
|
|
131
|
+
if (siap !== undefined)
|
|
132
|
+
return Promise.resolve(siap);
|
|
133
|
+
if (berakhir)
|
|
134
|
+
return Promise.resolve(null);
|
|
135
|
+
return new Promise((resolve) => penunggu.push(resolve));
|
|
136
|
+
},
|
|
137
|
+
tutup: () => rl.close(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Pembuat callback konfirmasi bersama untuk do/ask/chat di atas satu PembacaBaris. */
|
|
141
|
+
function buatKonfirmasi(pembaca) {
|
|
142
|
+
return async (deskripsi) => {
|
|
143
|
+
console.log(`\n[vibekit] Usulan aksi:\n${deskripsi}`);
|
|
144
|
+
const jawaban = await pembaca.tanya("Jalankan? (y/N) ");
|
|
145
|
+
if (jawaban === null)
|
|
146
|
+
return false; // EOF: tolak demi keamanan
|
|
147
|
+
const j = jawaban.trim().toLowerCase();
|
|
148
|
+
return j === "y" || j === "ya";
|
|
149
|
+
};
|
|
99
150
|
}
|
|
100
151
|
async function perintahAsk(args) {
|
|
101
152
|
const urai = uraiArgAsk(args);
|
|
@@ -125,14 +176,50 @@ async function perintahAsk(args) {
|
|
|
125
176
|
for (const d of konteks.dilewati)
|
|
126
177
|
console.error(`[vibekit] konteks dilewati: ${d}`);
|
|
127
178
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
// Sejak v0.3.0 ask menjalankan SATU giliran agent: pertanyaan biasa dijawab
|
|
180
|
+
// teks langsung, permintaan membuat file/perintah dijalankan lewat tool
|
|
181
|
+
// dengan konfirmasi — pagar keamanan yang sama seperti `do`.
|
|
182
|
+
const messages = [
|
|
183
|
+
{ role: "system", content: SYSTEM_PROMPT_GABUNGAN },
|
|
184
|
+
{ role: "user", content: isiUser },
|
|
185
|
+
];
|
|
186
|
+
if (urai.auto)
|
|
187
|
+
cetakPeringatanAuto();
|
|
188
|
+
const pembaca = buatPembacaBaris();
|
|
189
|
+
try {
|
|
190
|
+
const hasil = await jalankanGiliran({
|
|
191
|
+
config,
|
|
192
|
+
messages,
|
|
193
|
+
lokasiState: statePath(),
|
|
194
|
+
cwd: process.cwd(),
|
|
195
|
+
konfirmasi: buatKonfirmasi(pembaca),
|
|
196
|
+
autoAllow: urai.auto,
|
|
197
|
+
jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
|
|
198
|
+
});
|
|
199
|
+
if (!hasil.ok) {
|
|
200
|
+
if (hasil.batasTercapai) {
|
|
201
|
+
console.error(`[vibekit] Batas langkah tercapai (${hasil.jumlahLangkah} langkah) sebelum pertanyaan ini selesai dikerjakan. Persempit permintaanmu, atau gunakan vibekit do untuk mode tugas.`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.error(hasil.pesanGagal ?? "[vibekit] Semua penyedia gagal atau sedang tidak tersedia.");
|
|
205
|
+
}
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
console.log(hasil.jawabanAkhir);
|
|
209
|
+
if (hasil.providerTerakhir) {
|
|
210
|
+
const sumber = hasil.tokenSumber === "usage" ? "dari usage API" : "estimasi";
|
|
211
|
+
console.log(`\n— dijawab oleh ${hasil.providerTerakhir.nama} (${hasil.providerTerakhir.model}) • ${formatAngka(hasil.totalToken)} token (${sumber})`);
|
|
212
|
+
}
|
|
213
|
+
for (const c of hasil.catatan) {
|
|
214
|
+
console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
|
|
215
|
+
}
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
pembaca.tutup();
|
|
220
|
+
}
|
|
134
221
|
}
|
|
135
|
-
async function perintahChat() {
|
|
222
|
+
async function perintahChat(args) {
|
|
136
223
|
let config;
|
|
137
224
|
try {
|
|
138
225
|
config = muatConfig().config;
|
|
@@ -141,58 +228,87 @@ async function perintahChat() {
|
|
|
141
228
|
console.error(`[vibekit] ${e.message}`);
|
|
142
229
|
return 2;
|
|
143
230
|
}
|
|
231
|
+
// Mode auto-allow bisa dinyalakan dari flag --auto/--yes, lalu diubah
|
|
232
|
+
// kapan saja di tengah sesi dengan perintah /auto.
|
|
233
|
+
let autoAktif = args.includes("--auto") || args.includes("--yes");
|
|
144
234
|
console.log("VibeKit chat — ketik pertanyaan, /keluar untuk berhenti.");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
235
|
+
console.log("Chat ini juga bisa bertindak: minta saja dibuatkan file atau menjalankan perintah — setiap aksi meminta persetujuanmu dulu.");
|
|
236
|
+
console.log("Perintah /auto menyalakan/mematikan mode auto-allow (aksi tanpa persetujuan) di tengah sesi.");
|
|
237
|
+
if (autoAktif)
|
|
238
|
+
cetakPeringatanAuto();
|
|
239
|
+
const riwayat = [{ role: "system", content: SYSTEM_PROMPT_GABUNGAN }];
|
|
240
|
+
const pembaca = buatPembacaBaris();
|
|
241
|
+
const konfirmasi = buatKonfirmasi(pembaca);
|
|
242
|
+
try {
|
|
243
|
+
for (;;) {
|
|
244
|
+
const barisMentah = await pembaca.tanya("kamu> ");
|
|
245
|
+
if (barisMentah === null)
|
|
246
|
+
break; // stdin berakhir (EOF)
|
|
247
|
+
const baris = barisMentah.trim();
|
|
248
|
+
if (baris === "/keluar" || baris === "/exit")
|
|
249
|
+
break;
|
|
250
|
+
if (!baris)
|
|
251
|
+
continue;
|
|
252
|
+
if (baris === "/auto") {
|
|
253
|
+
autoAktif = !autoAktif;
|
|
254
|
+
console.log(autoAktif
|
|
255
|
+
? "[vibekit] Mode auto-allow: AKTIF — aksi tulis file & menjalankan perintah berikutnya dijalankan TANPA persetujuan. Agent tetap terkunci di folder kerja ini."
|
|
256
|
+
: "[vibekit] Mode auto-allow: NONAKTIF — aksi tulis file & menjalankan perintah kembali meminta persetujuanmu satu per satu.");
|
|
257
|
+
continue;
|
|
155
258
|
}
|
|
156
|
-
|
|
157
|
-
|
|
259
|
+
riwayat.push({ role: "user", content: baris });
|
|
260
|
+
// Kompaksi riwayat aksi DULU: rangkaian tool giliran-giliran lama
|
|
261
|
+
// diganti ringkasan satu baris agar isi file tidak terkirim ulang.
|
|
262
|
+
// Giliran terakhir (yang baru saja ditambahkan pesan user-nya ini
|
|
263
|
+
// menjadi giliran terakhir) tidak disentuh kompaksi.
|
|
264
|
+
const terkompaksi = kompaksiRiwayatAksi(riwayat);
|
|
265
|
+
riwayat.splice(0, riwayat.length, ...terkompaksi);
|
|
266
|
+
// Lalu Pangkas SADAR-TOOL: hanya pada batas giliran utuh, tidak pernah
|
|
267
|
+
// menyisakan pesan tool yatim atau tool_calls tanpa hasilnya.
|
|
268
|
+
const terpotong = pangkasRiwayatAman(riwayat, BATAS_TOKEN_RIWAYAT);
|
|
269
|
+
riwayat.splice(0, riwayat.length, ...terpotong);
|
|
270
|
+
const hasil = await jalankanGiliran({
|
|
271
|
+
config,
|
|
272
|
+
messages: riwayat,
|
|
273
|
+
lokasiState: statePath(),
|
|
274
|
+
cwd: process.cwd(),
|
|
275
|
+
konfirmasi,
|
|
276
|
+
autoAllow: autoAktif,
|
|
277
|
+
jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
|
|
278
|
+
});
|
|
279
|
+
if (hasil.ok) {
|
|
280
|
+
console.log(`\nvibekit (${hasil.providerTerakhir?.nama ?? "?"})> ${hasil.jawabanAkhir}\n`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Giliran gagal/batas tercapai: gulung balik SELURUH giliran ini
|
|
284
|
+
// (pesan user + rangkaian parsialnya) agar riwayat tetap bersih.
|
|
285
|
+
let idxUser = -1;
|
|
286
|
+
for (let i = riwayat.length - 1; i >= 0; i--) {
|
|
287
|
+
if (riwayat[i].role === "user") {
|
|
288
|
+
idxUser = i;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (idxUser >= 0)
|
|
293
|
+
riwayat.length = idxUser;
|
|
294
|
+
const pesan = hasil.batasTercapai
|
|
295
|
+
? `Batas langkah tercapai (${hasil.jumlahLangkah} langkah) pada giliran ini; giliran dibatalkan. Persempit permintaanmu.`
|
|
296
|
+
: (hasil.pesanGagal ?? "Semua penyedia gagal atau sedang tidak tersedia.");
|
|
297
|
+
console.error(`\n${pesan}\n`);
|
|
158
298
|
}
|
|
159
299
|
}
|
|
160
|
-
};
|
|
161
|
-
promptAman();
|
|
162
|
-
for await (const barisMentah of rl) {
|
|
163
|
-
const baris = barisMentah.trim();
|
|
164
|
-
if (baris === "/keluar" || baris === "/exit")
|
|
165
|
-
break;
|
|
166
|
-
if (!baris) {
|
|
167
|
-
promptAman();
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
riwayat.push({ role: "user", content: baris });
|
|
171
|
-
// Jaga riwayat: buang giliran tertua sampai total token riwayat masuk akal.
|
|
172
|
-
while (riwayat.length > 2 &&
|
|
173
|
-
hitungToken(riwayat.map((m) => m.content).join("\n")) > BATAS_TOKEN_RIWAYAT) {
|
|
174
|
-
riwayat.splice(0, 2);
|
|
175
|
-
}
|
|
176
|
-
const hasil = await tanyaRouter({
|
|
177
|
-
config,
|
|
178
|
-
messages: [...riwayat],
|
|
179
|
-
lokasiState: statePath(),
|
|
180
|
-
});
|
|
181
|
-
if (hasil.ok) {
|
|
182
|
-
riwayat.push({ role: "assistant", content: hasil.jawaban });
|
|
183
|
-
console.log(`\nvibekit (${hasil.provider.nama})> ${hasil.jawaban}\n`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
riwayat.pop(); // pertanyaan yang gagal tidak disimpan sebagai giliran selesai
|
|
187
|
-
console.error(`\n${hasil.pesan}\n`);
|
|
188
|
-
}
|
|
189
|
-
promptAman();
|
|
190
300
|
}
|
|
191
|
-
|
|
301
|
+
finally {
|
|
302
|
+
pembaca.tutup();
|
|
303
|
+
}
|
|
192
304
|
return 0;
|
|
193
305
|
}
|
|
194
306
|
async function perintahDo(args) {
|
|
195
|
-
const
|
|
307
|
+
const auto = args.includes("--auto") || args.includes("--yes");
|
|
308
|
+
const tugas = args
|
|
309
|
+
.filter((a) => a !== "--auto" && a !== "--yes")
|
|
310
|
+
.join(" ")
|
|
311
|
+
.trim();
|
|
196
312
|
if (!tugas) {
|
|
197
313
|
console.error("[vibekit] Tugas kosong. Contoh: vibekit do \"buatkan file halo.txt berisi sapaan\"");
|
|
198
314
|
return 2;
|
|
@@ -208,15 +324,14 @@ async function perintahDo(args) {
|
|
|
208
324
|
const cwd = process.cwd();
|
|
209
325
|
console.log(`VibeKit agent — folder kerja: ${cwd}`);
|
|
210
326
|
console.log(`Tugas: ${tugas}`);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
327
|
+
if (auto) {
|
|
328
|
+
cetakPeringatanAuto();
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log("Tool baca berjalan langsung; tulis/edit file dan jalankan perintah meminta persetujuanmu satu per satu.\n");
|
|
332
|
+
}
|
|
333
|
+
const pembaca = buatPembacaBaris();
|
|
334
|
+
const konfirmasi = buatKonfirmasi(pembaca);
|
|
220
335
|
try {
|
|
221
336
|
const hasil = await jalankanAgent({
|
|
222
337
|
config,
|
|
@@ -224,6 +339,7 @@ async function perintahDo(args) {
|
|
|
224
339
|
lokasiState: statePath(),
|
|
225
340
|
cwd,
|
|
226
341
|
konfirmasi,
|
|
342
|
+
autoAllow: auto,
|
|
227
343
|
jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
|
|
228
344
|
});
|
|
229
345
|
for (const c of hasil.catatan) {
|
|
@@ -234,7 +350,7 @@ async function perintahDo(args) {
|
|
|
234
350
|
return hasil.ok ? 0 : 1;
|
|
235
351
|
}
|
|
236
352
|
finally {
|
|
237
|
-
|
|
353
|
+
pembaca.tutup();
|
|
238
354
|
}
|
|
239
355
|
}
|
|
240
356
|
function perintahStatus() {
|
|
@@ -274,7 +390,7 @@ async function utama() {
|
|
|
274
390
|
case "ask":
|
|
275
391
|
return perintahAsk(sisa);
|
|
276
392
|
case "chat":
|
|
277
|
-
return perintahChat();
|
|
393
|
+
return perintahChat(sisa);
|
|
278
394
|
case "do":
|
|
279
395
|
return perintahDo(sisa);
|
|
280
396
|
case "status":
|
package/dist/router.js
CHANGED
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
+
"scripts",
|
|
18
19
|
"README.md",
|
|
19
20
|
"examples"
|
|
20
21
|
],
|
|
@@ -23,12 +24,13 @@
|
|
|
23
24
|
"name": "@chispear/vibekit",
|
|
24
25
|
"scripts": {
|
|
25
26
|
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"postinstall": "node scripts/banner.js",
|
|
26
28
|
"prepublishOnly": "npm run build",
|
|
27
29
|
"start": "node dist/cli.js",
|
|
28
30
|
"test": "npm run build && node --test test/*.test.mjs"
|
|
29
31
|
},
|
|
30
32
|
"type": "module",
|
|
31
|
-
"version": "0.
|
|
33
|
+
"version": "0.4.0",
|
|
32
34
|
"publishConfig": {
|
|
33
35
|
"access": "public"
|
|
34
36
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Banner CHISPEAR — tampil otomatis setelah package berhasil terpasang
|
|
2
|
+
// (npm postinstall). Tanpa dependensi; hormati NO_COLOR.
|
|
3
|
+
// Salinan mandiri dari seni di src/banner.ts (skrip ini tidak boleh bergantung
|
|
4
|
+
// pada dist/ karena postinstall juga berjalan sebelum build di repo clone) —
|
|
5
|
+
// jaga keduanya tetap sinkron.
|
|
6
|
+
const GLIF = {
|
|
7
|
+
C: [" █████ ", "██ ██", "██ ", "██ ", "██ ██", " █████ "],
|
|
8
|
+
H: ["██ ██", "██ ██", "███████", "██ ██", "██ ██", "██ ██"],
|
|
9
|
+
I: ["███", " █ ", " █ ", " █ ", " █ ", "███"],
|
|
10
|
+
S: ["███████", "██ ", "██████ ", " ██", " ██", "██████ "],
|
|
11
|
+
P: ["██████ ", "██ ██", "██████ ", "██ ", "██ ", "██ "],
|
|
12
|
+
E: ["███████", "██ ", "█████ ", "█████ ", "██ ", "███████"],
|
|
13
|
+
A: [" █████ ", "██ ██", "███████", "██ ██", "██ ██", "██ ██"],
|
|
14
|
+
R: ["██████ ", "██ ██", "██████ ", "██ ██ ", "██ ██", "██ ██"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const kata = "CHISPEAR";
|
|
18
|
+
const baris = ["", "", "", "", "", ""];
|
|
19
|
+
for (const huruf of kata) {
|
|
20
|
+
const g = GLIF[huruf];
|
|
21
|
+
if (!g) continue;
|
|
22
|
+
for (let i = 0; i < 6; i++) baris[i] += (baris[i] ? " " : "") + g[i];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pakaiWarna = !process.env.NO_COLOR;
|
|
26
|
+
const HIJAU = pakaiWarna ? "\x1b[1;92m" : "";
|
|
27
|
+
const RESET = pakaiWarna ? "\x1b[0m" : "";
|
|
28
|
+
|
|
29
|
+
console.log("");
|
|
30
|
+
for (const b of baris) console.log(HIJAU + b + RESET);
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log(HIJAU + " VibeKit CLI terpasang — router AI hemat tokenmu siap dipakai." + RESET);
|
|
33
|
+
console.log(" Mulai dengan: vibekit init • Bantuan: vibekit --help");
|
|
34
|
+
console.log("");
|