@chispear/vibekit 0.2.0 → 0.3.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 +13 -8
- package/dist/agent.js +151 -18
- package/dist/banner.js +36 -0
- package/dist/cli.js +148 -82
- 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 otomatis (maks ±4.000 token, giliran
|
|
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>]...` | 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` | Percakapan interaktif yang juga bisa bertindak di tengah obrolan; riwayat dipangkas otomatis dan sadar-tool (maks ±4.000 token, hanya pada batas giliran utuh); `/keluar` untuk berhenti |
|
|
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,8 @@ 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
|
-
- **Batas 12 langkah** per tugas, dan hasil tiap tool dipangkas (±4.000 karakter) sebelum dikirim balik ke model demi hemat token.
|
|
96
|
+
- **Batas 12 langkah** per giliran/tugas, dan hasil tiap tool dipangkas (±4.000 karakter) sebelum dikirim balik ke model demi hemat token.
|
|
97
|
+
- **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
98
|
|
|
94
99
|
## Cara Failover Bekerja
|
|
95
100
|
|
|
@@ -118,7 +123,7 @@ Keputusan repo: `dist/` masuk `.gitignore` dan dibangun saat publish (`prepublis
|
|
|
118
123
|
|
|
119
124
|
## Status Nama di npm
|
|
120
125
|
|
|
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`.
|
|
126
|
+
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
127
|
|
|
123
128
|
## Lisensi
|
|
124
129
|
|
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.",
|
|
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.
|
|
@@ -256,17 +271,24 @@ export async function eksekusiTool(panggilan, konteks) {
|
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
273
|
/**
|
|
259
|
-
*
|
|
260
|
-
* diminta model lewat eksekutor aman → kirim hasil tool balik
|
|
261
|
-
* model membalas teks tanpa tool_calls
|
|
274
|
+
* Inti loop agent untuk SATU giliran: panggil router (dengan tools) → jalankan
|
|
275
|
+
* tool_calls yang diminta model lewat eksekutor aman → kirim hasil tool balik
|
|
276
|
+
* → ulangi sampai model membalas teks tanpa tool_calls atau langkah habis.
|
|
277
|
+
* Dipakai bersama oleh `do` (lewat jalankanAgent), `ask`, dan `chat`.
|
|
278
|
+
*
|
|
279
|
+
* Catatan kegagalan: bila router gagal di tengah giliran, `messages` mungkin
|
|
280
|
+
* sudah memuat rangkaian parsial (assistant tool_calls + hasil tool tanpa teks
|
|
281
|
+
* akhir). Pemanggil yang menyimpan riwayat (chat) sebaiknya menggulung balik
|
|
282
|
+
* giliran itu; pemanggil sekali jalan (ask/do) bisa mengabaikannya.
|
|
262
283
|
*/
|
|
263
|
-
export async function
|
|
284
|
+
export async function jalankanGiliran(opsi) {
|
|
264
285
|
const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
|
|
286
|
+
const messages = opsi.messages;
|
|
265
287
|
const catatan = [];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
let totalToken = 0;
|
|
289
|
+
let semuaUsage = true;
|
|
290
|
+
let providerTerakhir = null;
|
|
291
|
+
const sumber = () => (semuaUsage ? "usage" : "estimasi");
|
|
270
292
|
let langkah = 0;
|
|
271
293
|
while (langkah < maksLangkah) {
|
|
272
294
|
langkah++;
|
|
@@ -279,18 +301,49 @@ export async function jalankanAgent(opsi) {
|
|
|
279
301
|
});
|
|
280
302
|
catatan.push(...hasil.catatan);
|
|
281
303
|
if (!hasil.ok) {
|
|
282
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
jawabanAkhir: "",
|
|
307
|
+
providerTerakhir,
|
|
308
|
+
totalToken,
|
|
309
|
+
tokenSumber: sumber(),
|
|
310
|
+
jumlahLangkah: langkah,
|
|
311
|
+
catatan,
|
|
312
|
+
pesanGagal: hasil.pesan,
|
|
313
|
+
};
|
|
283
314
|
}
|
|
315
|
+
totalToken += hasil.tokenTerpakai;
|
|
316
|
+
if (hasil.tokenSumber !== "usage")
|
|
317
|
+
semuaUsage = false;
|
|
318
|
+
providerTerakhir = hasil.provider;
|
|
284
319
|
const panggilan = hasil.toolCalls ?? [];
|
|
285
320
|
if (panggilan.length === 0) {
|
|
286
|
-
// Balasan teks murni =
|
|
287
|
-
|
|
321
|
+
// Balasan teks murni = jawaban akhir giliran; simpan ke riwayat agar utuh.
|
|
322
|
+
messages.push({ role: "assistant", content: hasil.jawaban });
|
|
323
|
+
return {
|
|
324
|
+
ok: true,
|
|
325
|
+
jawabanAkhir: hasil.jawaban,
|
|
326
|
+
providerTerakhir,
|
|
327
|
+
totalToken,
|
|
328
|
+
tokenSumber: sumber(),
|
|
329
|
+
jumlahLangkah: langkah,
|
|
330
|
+
catatan,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (hasil.pesanMentah && typeof hasil.pesanMentah === "object") {
|
|
334
|
+
// Teruskan pesan assistant persis seperti yang dikirim penyedia. Gemini 3
|
|
335
|
+
// menyisipkan thought_signature di dalam tool_calls (extra_content) dan
|
|
336
|
+
// menolak permintaan berikutnya dengan HTTP 400 bila tanda tangan itu
|
|
337
|
+
// hilang dari riwayat — jadi pesan mentah tidak boleh dibangun ulang.
|
|
338
|
+
messages.push(hasil.pesanMentah);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
messages.push({
|
|
342
|
+
role: "assistant",
|
|
343
|
+
content: hasil.jawaban ? hasil.jawaban : null,
|
|
344
|
+
tool_calls: panggilan,
|
|
345
|
+
});
|
|
288
346
|
}
|
|
289
|
-
messages.push({
|
|
290
|
-
role: "assistant",
|
|
291
|
-
content: hasil.jawaban ? hasil.jawaban : null,
|
|
292
|
-
tool_calls: panggilan,
|
|
293
|
-
});
|
|
294
347
|
for (const p of panggilan) {
|
|
295
348
|
let argRingkas = "";
|
|
296
349
|
try {
|
|
@@ -312,9 +365,89 @@ export async function jalankanAgent(opsi) {
|
|
|
312
365
|
}
|
|
313
366
|
return {
|
|
314
367
|
ok: false,
|
|
315
|
-
|
|
316
|
-
|
|
368
|
+
jawabanAkhir: "",
|
|
369
|
+
providerTerakhir,
|
|
370
|
+
totalToken,
|
|
371
|
+
tokenSumber: sumber(),
|
|
317
372
|
jumlahLangkah: langkah,
|
|
318
373
|
catatan,
|
|
374
|
+
batasTercapai: true,
|
|
319
375
|
};
|
|
320
376
|
}
|
|
377
|
+
/** Perkiraan token satu pesan: konten teks ditambah argumen tool_calls (bila ada). */
|
|
378
|
+
function tokenPesan(m) {
|
|
379
|
+
let teks = m.content ?? "";
|
|
380
|
+
if (m.tool_calls && m.tool_calls.length > 0)
|
|
381
|
+
teks += "\n" + JSON.stringify(m.tool_calls);
|
|
382
|
+
return hitungToken(teks);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Pangkas riwayat percakapan secara SADAR-TOOL: pemotongan hanya terjadi pada
|
|
386
|
+
* batas giliran utuh. Satu giliran = satu pesan "user" beserta seluruh
|
|
387
|
+
* rangkaian sesudahnya sampai sebelum "user" berikutnya (assistant tool_calls,
|
|
388
|
+
* hasil-hasil tool, assistant teks akhir). Fungsi ini TIDAK PERNAH menyisakan
|
|
389
|
+
* pesan "tool" yatim atau assistant tool_calls tanpa hasil tool-nya.
|
|
390
|
+
*
|
|
391
|
+
* Awalan pesan "system" (bila ada) selalu dipertahankan karena bukan bagian
|
|
392
|
+
* giliran mana pun. Giliran terakhir selalu dipertahankan utuh, jadi hasil
|
|
393
|
+
* boleh tetap melebihi maksToken bila satu giliran terakhir saja sudah
|
|
394
|
+
* sebesar itu. Tidak mengubah array masukan; mengembalikan array baru.
|
|
395
|
+
*/
|
|
396
|
+
export function pangkasRiwayatAman(messages, maksToken) {
|
|
397
|
+
let awal = 0;
|
|
398
|
+
while (awal < messages.length && messages[awal].role === "system")
|
|
399
|
+
awal++;
|
|
400
|
+
const awalan = messages.slice(0, awal);
|
|
401
|
+
const sisa = messages.slice(awal);
|
|
402
|
+
// Kelompokkan sisa menjadi giliran-giliran: grup baru dimulai di setiap
|
|
403
|
+
// pesan "user"; pesan sebelum "user" pertama membentuk grupnya sendiri.
|
|
404
|
+
const grup = [];
|
|
405
|
+
for (const m of sisa) {
|
|
406
|
+
if (m.role === "user" || grup.length === 0)
|
|
407
|
+
grup.push([m]);
|
|
408
|
+
else
|
|
409
|
+
grup[grup.length - 1].push(m);
|
|
410
|
+
}
|
|
411
|
+
const tokenGrup = (g) => g.reduce((s, m) => s + tokenPesan(m), 0);
|
|
412
|
+
let total = awalan.reduce((s, m) => s + tokenPesan(m), 0) + grup.reduce((s, g) => s + tokenGrup(g), 0);
|
|
413
|
+
// Buang giliran tertua secara utuh sampai muat; giliran terakhir tak dibuang.
|
|
414
|
+
while (total > maksToken && grup.length > 1) {
|
|
415
|
+
total -= tokenGrup(grup.shift());
|
|
416
|
+
}
|
|
417
|
+
return [...awalan, ...grup.flat()];
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Mode tugas sekali jalan (`vibekit do`): satu giliran agent dengan system
|
|
421
|
+
* prompt pelaksana tugas, dibangun di atas jalankanGiliran. Perilaku dan
|
|
422
|
+
* bentuk HasilAgent sama seperti sebelum refaktor v0.3.0.
|
|
423
|
+
*/
|
|
424
|
+
export async function jalankanAgent(opsi) {
|
|
425
|
+
const maksLangkah = opsi.maksLangkah ?? MAKS_LANGKAH_BAWAAN;
|
|
426
|
+
const messages = [
|
|
427
|
+
{ role: "system", content: SYSTEM_PROMPT_AGENT },
|
|
428
|
+
{ role: "user", content: opsi.tugas },
|
|
429
|
+
];
|
|
430
|
+
const hasil = await jalankanGiliran({
|
|
431
|
+
config: opsi.config,
|
|
432
|
+
messages,
|
|
433
|
+
lokasiState: opsi.lokasiState,
|
|
434
|
+
cwd: opsi.cwd,
|
|
435
|
+
konfirmasi: opsi.konfirmasi,
|
|
436
|
+
fetchImpl: opsi.fetchImpl,
|
|
437
|
+
maksLangkah: opsi.maksLangkah,
|
|
438
|
+
jejak: opsi.jejak,
|
|
439
|
+
});
|
|
440
|
+
if (hasil.pesanGagal !== undefined) {
|
|
441
|
+
return { ok: false, ringkasan: hasil.pesanGagal, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
|
|
442
|
+
}
|
|
443
|
+
if (hasil.batasTercapai) {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
ringkasan: `Batas langkah tercapai (${maksLangkah} langkah) sebelum tugas selesai. ` +
|
|
447
|
+
"Jalankan lagi perintah yang sama untuk melanjutkan, atau persempit tugasnya.",
|
|
448
|
+
jumlahLangkah: hasil.jumlahLangkah,
|
|
449
|
+
catatan: hasil.catatan,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return { ok: true, ringkasan: hasil.jawabanAkhir, jumlahLangkah: hasil.jumlahLangkah, catatan: hasil.catatan };
|
|
453
|
+
}
|
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, 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.3.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
|
|
@@ -24,11 +24,16 @@ Opsi ask:
|
|
|
24
24
|
--file <path> Lampirkan file sebagai konteks (boleh diulang)
|
|
25
25
|
--dir <path> Lampirkan isi folder sebagai konteks (boleh diulang)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
Kemampuan agent (ask, chat, do):
|
|
28
|
+
Sejak v0.3.0 ketiga perintah itu bisa bertindak, bukan hanya menjawab:
|
|
29
|
+
bila kamu meminta dibuatkan/diubah file atau menjalankan perintah, model
|
|
30
|
+
memakai tool di folder tempat perintah dijalankan. Tool baca (daftar_file,
|
|
31
|
+
baca_file) berjalan langsung; tool yang mengubah sesuatu (tulis_file,
|
|
32
|
+
edit_file, jalankan_perintah) selalu meminta persetujuanmu satu per satu
|
|
33
|
+
— jawaban selain y/ya berarti ditolak. Batas 12 langkah per giliran.
|
|
34
|
+
Perbedaannya: ask = satu pertanyaan yang boleh berakhir dengan aksi,
|
|
35
|
+
chat = percakapan yang bisa bertindak di tengah obrolan,
|
|
36
|
+
do = mode tugas sekali jalan dengan jejak langkah & ringkasan akhir.
|
|
32
37
|
Contoh: vibekit do "buatkan file halo.txt berisi sapaan, lalu tampilkan daftar file di sini"
|
|
33
38
|
|
|
34
39
|
Cara kerja singkat:
|
|
@@ -41,6 +46,7 @@ Cara kerja singkat:
|
|
|
41
46
|
function perintahInit() {
|
|
42
47
|
const dibuat = buatConfigGlobal();
|
|
43
48
|
if (dibuat) {
|
|
49
|
+
console.log(teksBanner());
|
|
44
50
|
console.log(`Konfigurasi dibuat: ${globalConfigPath()}`);
|
|
45
51
|
console.log("Isi kolom \"apiKey\" untuk tiap penyedia yang ingin dipakai (atau pakai sintaks \"env:NAMA_VAR\").");
|
|
46
52
|
console.log("Urutan penyedia = prioritas: yang pertama adalah bawaan, berikutnya cadangan failover.");
|
|
@@ -84,18 +90,46 @@ function uraiArgAsk(args) {
|
|
|
84
90
|
return { error: "Pertanyaan kosong. Contoh: vibekit ask \"apa itu API?\"" };
|
|
85
91
|
return { pertanyaan, files, dirs };
|
|
86
92
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
function buatPembacaBaris() {
|
|
94
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
95
|
+
const antrian = [];
|
|
96
|
+
const penunggu = [];
|
|
97
|
+
let berakhir = false;
|
|
98
|
+
rl.on("line", (baris) => {
|
|
99
|
+
const p = penunggu.shift();
|
|
100
|
+
if (p)
|
|
101
|
+
p(baris);
|
|
102
|
+
else
|
|
103
|
+
antrian.push(baris);
|
|
104
|
+
});
|
|
105
|
+
rl.on("close", () => {
|
|
106
|
+
berakhir = true;
|
|
107
|
+
while (penunggu.length > 0)
|
|
108
|
+
penunggu.shift()(null);
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
tanya: (prompt) => {
|
|
112
|
+
process.stdout.write(prompt);
|
|
113
|
+
const siap = antrian.shift();
|
|
114
|
+
if (siap !== undefined)
|
|
115
|
+
return Promise.resolve(siap);
|
|
116
|
+
if (berakhir)
|
|
117
|
+
return Promise.resolve(null);
|
|
118
|
+
return new Promise((resolve) => penunggu.push(resolve));
|
|
119
|
+
},
|
|
120
|
+
tutup: () => rl.close(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Pembuat callback konfirmasi bersama untuk do/ask/chat di atas satu PembacaBaris. */
|
|
124
|
+
function buatKonfirmasi(pembaca) {
|
|
125
|
+
return async (deskripsi) => {
|
|
126
|
+
console.log(`\n[vibekit] Usulan aksi:\n${deskripsi}`);
|
|
127
|
+
const jawaban = await pembaca.tanya("Jalankan? (y/N) ");
|
|
128
|
+
if (jawaban === null)
|
|
129
|
+
return false; // EOF: tolak demi keamanan
|
|
130
|
+
const j = jawaban.trim().toLowerCase();
|
|
131
|
+
return j === "y" || j === "ya";
|
|
132
|
+
};
|
|
99
133
|
}
|
|
100
134
|
async function perintahAsk(args) {
|
|
101
135
|
const urai = uraiArgAsk(args);
|
|
@@ -125,12 +159,45 @@ async function perintahAsk(args) {
|
|
|
125
159
|
for (const d of konteks.dilewati)
|
|
126
160
|
console.error(`[vibekit] konteks dilewati: ${d}`);
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
// Sejak v0.3.0 ask menjalankan SATU giliran agent: pertanyaan biasa dijawab
|
|
163
|
+
// teks langsung, permintaan membuat file/perintah dijalankan lewat tool
|
|
164
|
+
// dengan konfirmasi — pagar keamanan yang sama seperti `do`.
|
|
165
|
+
const messages = [
|
|
166
|
+
{ role: "system", content: SYSTEM_PROMPT_GABUNGAN },
|
|
167
|
+
{ role: "user", content: isiUser },
|
|
168
|
+
];
|
|
169
|
+
const pembaca = buatPembacaBaris();
|
|
170
|
+
try {
|
|
171
|
+
const hasil = await jalankanGiliran({
|
|
172
|
+
config,
|
|
173
|
+
messages,
|
|
174
|
+
lokasiState: statePath(),
|
|
175
|
+
cwd: process.cwd(),
|
|
176
|
+
konfirmasi: buatKonfirmasi(pembaca),
|
|
177
|
+
jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
|
|
178
|
+
});
|
|
179
|
+
if (!hasil.ok) {
|
|
180
|
+
if (hasil.batasTercapai) {
|
|
181
|
+
console.error(`[vibekit] Batas langkah tercapai (${hasil.jumlahLangkah} langkah) sebelum pertanyaan ini selesai dikerjakan. Persempit permintaanmu, atau gunakan vibekit do untuk mode tugas.`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.error(hasil.pesanGagal ?? "[vibekit] Semua penyedia gagal atau sedang tidak tersedia.");
|
|
185
|
+
}
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
console.log(hasil.jawabanAkhir);
|
|
189
|
+
if (hasil.providerTerakhir) {
|
|
190
|
+
const sumber = hasil.tokenSumber === "usage" ? "dari usage API" : "estimasi";
|
|
191
|
+
console.log(`\n— dijawab oleh ${hasil.providerTerakhir.nama} (${hasil.providerTerakhir.model}) • ${formatAngka(hasil.totalToken)} token (${sumber})`);
|
|
192
|
+
}
|
|
193
|
+
for (const c of hasil.catatan) {
|
|
194
|
+
console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
pembaca.tutup();
|
|
200
|
+
}
|
|
134
201
|
}
|
|
135
202
|
async function perintahChat() {
|
|
136
203
|
let config;
|
|
@@ -142,53 +209,58 @@ async function perintahChat() {
|
|
|
142
209
|
return 2;
|
|
143
210
|
}
|
|
144
211
|
console.log("VibeKit chat — ketik pertanyaan, /keluar untuk berhenti.");
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
212
|
+
console.log("Chat ini juga bisa bertindak: minta saja dibuatkan file atau menjalankan perintah — setiap aksi meminta persetujuanmu dulu.");
|
|
213
|
+
const riwayat = [{ role: "system", content: SYSTEM_PROMPT_GABUNGAN }];
|
|
214
|
+
const pembaca = buatPembacaBaris();
|
|
215
|
+
const konfirmasi = buatKonfirmasi(pembaca);
|
|
216
|
+
try {
|
|
217
|
+
for (;;) {
|
|
218
|
+
const barisMentah = await pembaca.tanya("kamu> ");
|
|
219
|
+
if (barisMentah === null)
|
|
220
|
+
break; // stdin berakhir (EOF)
|
|
221
|
+
const baris = barisMentah.trim();
|
|
222
|
+
if (baris === "/keluar" || baris === "/exit")
|
|
223
|
+
break;
|
|
224
|
+
if (!baris)
|
|
225
|
+
continue;
|
|
226
|
+
riwayat.push({ role: "user", content: baris });
|
|
227
|
+
// Pangkas SADAR-TOOL: hanya pada batas giliran utuh, tidak pernah
|
|
228
|
+
// menyisakan pesan tool yatim atau tool_calls tanpa hasilnya.
|
|
229
|
+
const terpotong = pangkasRiwayatAman(riwayat, BATAS_TOKEN_RIWAYAT);
|
|
230
|
+
riwayat.splice(0, riwayat.length, ...terpotong);
|
|
231
|
+
const hasil = await jalankanGiliran({
|
|
232
|
+
config,
|
|
233
|
+
messages: riwayat,
|
|
234
|
+
lokasiState: statePath(),
|
|
235
|
+
cwd: process.cwd(),
|
|
236
|
+
konfirmasi,
|
|
237
|
+
jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
|
|
238
|
+
});
|
|
239
|
+
if (hasil.ok) {
|
|
240
|
+
console.log(`\nvibekit (${hasil.providerTerakhir?.nama ?? "?"})> ${hasil.jawabanAkhir}\n`);
|
|
155
241
|
}
|
|
156
|
-
|
|
157
|
-
|
|
242
|
+
else {
|
|
243
|
+
// Giliran gagal/batas tercapai: gulung balik SELURUH giliran ini
|
|
244
|
+
// (pesan user + rangkaian parsialnya) agar riwayat tetap bersih.
|
|
245
|
+
let idxUser = -1;
|
|
246
|
+
for (let i = riwayat.length - 1; i >= 0; i--) {
|
|
247
|
+
if (riwayat[i].role === "user") {
|
|
248
|
+
idxUser = i;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (idxUser >= 0)
|
|
253
|
+
riwayat.length = idxUser;
|
|
254
|
+
const pesan = hasil.batasTercapai
|
|
255
|
+
? `Batas langkah tercapai (${hasil.jumlahLangkah} langkah) pada giliran ini; giliran dibatalkan. Persempit permintaanmu.`
|
|
256
|
+
: (hasil.pesanGagal ?? "Semua penyedia gagal atau sedang tidak tersedia.");
|
|
257
|
+
console.error(`\n${pesan}\n`);
|
|
158
258
|
}
|
|
159
259
|
}
|
|
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
260
|
}
|
|
191
|
-
|
|
261
|
+
finally {
|
|
262
|
+
pembaca.tutup();
|
|
263
|
+
}
|
|
192
264
|
return 0;
|
|
193
265
|
}
|
|
194
266
|
async function perintahDo(args) {
|
|
@@ -209,14 +281,8 @@ async function perintahDo(args) {
|
|
|
209
281
|
console.log(`VibeKit agent — folder kerja: ${cwd}`);
|
|
210
282
|
console.log(`Tugas: ${tugas}`);
|
|
211
283
|
console.log("Tool baca berjalan langsung; tulis/edit file dan jalankan perintah meminta persetujuanmu satu per satu.\n");
|
|
212
|
-
const
|
|
213
|
-
const konfirmasi = (
|
|
214
|
-
console.log(`\n[vibekit] Usulan aksi:\n${deskripsi}`);
|
|
215
|
-
rl.question("Jalankan? (y/N) ", (jawaban) => {
|
|
216
|
-
const j = jawaban.trim().toLowerCase();
|
|
217
|
-
resolve(j === "y" || j === "ya");
|
|
218
|
-
});
|
|
219
|
-
});
|
|
284
|
+
const pembaca = buatPembacaBaris();
|
|
285
|
+
const konfirmasi = buatKonfirmasi(pembaca);
|
|
220
286
|
try {
|
|
221
287
|
const hasil = await jalankanAgent({
|
|
222
288
|
config,
|
|
@@ -234,7 +300,7 @@ async function perintahDo(args) {
|
|
|
234
300
|
return hasil.ok ? 0 : 1;
|
|
235
301
|
}
|
|
236
302
|
finally {
|
|
237
|
-
|
|
303
|
+
pembaca.tutup();
|
|
238
304
|
}
|
|
239
305
|
}
|
|
240
306
|
function perintahStatus() {
|
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.3.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("");
|