@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 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>]... [--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 — `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,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
- - **Batas 12 langkah** per tugas, dan hasil tiap tool dipangkas (±4.000 karakter) sebelum dikirim balik ke model demi hemat token.
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 konfirmasi(deskripsi))) {
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 konfirmasi(deskripsi))) {
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 konfirmasi(deskripsi))) {
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
- * 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.
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 jalankanAgent(opsi) {
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
- const messages = [
267
- { role: "system", content: SYSTEM_PROMPT_AGENT },
268
- { role: "user", content: opsi.tugas },
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 { ok: false, ringkasan: hasil.pesan, jumlahLangkah: langkah, catatan };
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 = ringkasan akhir dari model.
287
- return { ok: true, ringkasan: hasil.jawaban, jumlahLangkah: langkah, catatan };
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.jejak?.(`langkah ${langkah}: ${p.function.name}${argRingkas ? ` (${argRingkas})` : ""}`);
303
- const hasilTool = await eksekusiTool(p, { cwd: opsi.cwd, konfirmasi: opsi.konfirmasi });
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
- ringkasan: `Batas langkah tercapai (${maksLangkah} langkah) sebelum tugas selesai. ` +
316
- "Jalankan lagi perintah yang sama untuk melanjutkan, atau persempit tugasnya.",
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, hitungToken } from "./token.js";
9
- const VERSI = "0.2.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 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
@@ -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
- 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.
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 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;
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
- const hasil = await tanyaRouter({
129
- config,
130
- messages: [{ role: "user", content: isiUser }],
131
- lokasiState: statePath(),
132
- });
133
- return tampilkanHasil(hasil);
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
- 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();
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
- catch {
157
- /* readline sudah tertutup (EOF) — abaikan */
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
- rl.close();
301
+ finally {
302
+ pembaca.tutup();
303
+ }
192
304
  return 0;
193
305
  }
194
306
  async function perintahDo(args) {
195
- const tugas = args.join(" ").trim();
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
- 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
- });
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
- rl.close();
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
@@ -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.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("");