@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 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>]...` | Tanya sekali; 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; riwayat dipangkas otomatis (maks ±4.000 token, giliran tertua dibuang); `/keluar` untuk berhenti |
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 — `vibekit do`
70
+ ## Mode Agent — `ask`, `chat`, dan `do`
71
71
 
72
- Selain menjawab pertanyaan, VibeKit bisa **mengerjakan tugas** di folder proyek Anda:
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 ringkasan akhir. Ada **5 tool**:
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
- * Loop mode agent: panggil router (dengan tools) → jalankan tool_calls yang
260
- * diminta model lewat eksekutor aman → kirim hasil tool balik → ulangi sampai
261
- * model membalas teks tanpa tool_calls (ringkasan akhir) atau langkah habis.
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 jalankanAgent(opsi) {
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
- const messages = [
267
- { role: "system", content: SYSTEM_PROMPT_AGENT },
268
- { role: "user", content: opsi.tugas },
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 { ok: false, ringkasan: hasil.pesan, jumlahLangkah: langkah, catatan };
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 = ringkasan akhir dari model.
287
- return { ok: true, ringkasan: hasil.jawaban, jumlahLangkah: langkah, catatan };
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
- ringkasan: `Batas langkah tercapai (${maksLangkah} langkah) sebelum tugas selesai. ` +
316
- "Jalankan lagi perintah yang sama untuk melanjutkan, atau persempit tugasnya.",
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, hitungToken } from "./token.js";
9
- const VERSI = "0.2.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 lewat router
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
- Mode agent (do):
28
- Agent bekerja HANYA di folder tempat perintah dijalankan. Tool baca
29
- (daftar_file, baca_file) berjalan langsung; tool yang mengubah sesuatu
30
- (tulis_file, edit_file, jalankan_perintah) selalu meminta persetujuanmu
31
- satu per satu — jawaban selain y/ya berarti ditolak. Batas 12 langkah.
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 tampilkanHasil(hasil) {
88
- if (hasil.ok) {
89
- console.log(hasil.jawaban);
90
- const sumber = hasil.tokenSumber === "usage" ? "dari usage API" : "estimasi";
91
- console.log(`\n— dijawab oleh ${hasil.provider.nama} (${hasil.provider.model}) • ${formatAngka(hasil.tokenTerpakai)} token (${sumber})`);
92
- for (const c of hasil.catatan) {
93
- console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
94
- }
95
- return 0;
96
- }
97
- console.error(hasil.pesan);
98
- return 1;
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
- const hasil = await tanyaRouter({
129
- config,
130
- messages: [{ role: "user", content: isiUser }],
131
- lokasiState: statePath(),
132
- });
133
- return tampilkanHasil(hasil);
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
- const riwayat = [];
146
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "kamu> " });
147
- let rlDitutup = false;
148
- rl.on("close", () => {
149
- rlDitutup = true;
150
- });
151
- const promptAman = () => {
152
- if (!rlDitutup) {
153
- try {
154
- rl.prompt();
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
- catch {
157
- /* readline sudah tertutup (EOF) — abaikan */
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
- rl.close();
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 rl = readline.createInterface({ input: process.stdin, output: process.stdout });
213
- const konfirmasi = (deskripsi) => new Promise((resolve) => {
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
- rl.close();
303
+ pembaca.tutup();
238
304
  }
239
305
  }
240
306
  function perintahStatus() {
package/dist/router.js CHANGED
@@ -209,7 +209,7 @@ export async function tanyaRouter(opsi) {
209
209
  return {
210
210
  ok: true,
211
211
  jawaban,
212
- ...(toolCalls.length > 0 ? { toolCalls } : {}),
212
+ ...(toolCalls.length > 0 ? { toolCalls, pesanMentah } : {}),
213
213
  provider,
214
214
  tokenTerpakai,
215
215
  tokenSumber,
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.2.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("");