@chispear/vibekit 0.3.0 → 0.6.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/dist/cli.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import readline from "node:readline";
3
- import { buatConfigGlobal, globalConfigPath, muatConfig, statePath, } from "./config.js";
4
- import { jalankanAgent, jalankanGiliran, pangkasRiwayatAman, SYSTEM_PROMPT_GABUNGAN, } from "./agent.js";
3
+ import { buatConfigGlobal, configDir, globalConfigPath, muatConfig, statePath, } from "./config.js";
4
+ import { jalankanAgent, jalankanAgentRencana, jalankanGiliran, kompaksiRiwayatAksiTerukur, pangkasRiwayatAman, SYSTEM_PROMPT_GABUNGAN, } from "./agent.js";
5
5
  import { teksBanner } from "./banner.js";
6
+ import { AWALAN_KONTEKS_BRIEF, buatBrief, muatKonteksBrief } from "./brief.js";
6
7
  import { bangunKonteks } from "./context.js";
7
- import { formatJam, muatState, sedangCooldown, statePenyedia, } from "./state.js";
8
+ import { sambungkanMcpServers } from "./mcp.js";
9
+ import { buatIdSesi, daftarSesi, folderSesi, muatSesiTerakhir, simpanSesi, } from "./sesi.js";
10
+ import { bangunLaporanHemat, catatHematKompaksi, formatJam, muatState, sedangCooldown, statePenyedia, } from "./state.js";
8
11
  import { formatAngka } from "./token.js";
9
- const VERSI = "0.3.0";
12
+ const VERSI = "0.6.0";
10
13
  const BATAS_TOKEN_RIWAYAT = 4000;
11
14
  function bantuan() {
12
15
  console.log(`VibeKit CLI v${VERSI} — router AI hemat token dengan failover otomatis
@@ -14,8 +17,12 @@ function bantuan() {
14
17
  Penggunaan:
15
18
  vibekit init Buat ~/.vibekit/config.json dari template
16
19
  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
20
+ vibekit chat [opsi] Percakapan interaktif yang juga bisa bertindak (perintah /keluar untuk berhenti)
21
+ vibekit do "<tugas>" [opsi] Mode agent sekali jalan: kerjakan tugas dengan tool file/perintah di folder ini
22
+ vibekit mcp Daftar tool dari server MCP pada konfigurasi (field mcpServers)
23
+ vibekit brief Susun ringkasan konteks proyek ke .vibekit/brief.md (dipakai otomatis sebagai konteks)
24
+ vibekit hemat Laporan pemakaian token (hari ini & 7 hari) + token yang dihemat kompaksi riwayat
25
+ vibekit sesi Daftar sesi chat tersimpan (bisa dilanjutkan dengan chat --lanjut)
19
26
  vibekit status Status & pemakaian token tiap penyedia hari ini
20
27
  vibekit --help Tampilkan bantuan ini
21
28
  vibekit --version Tampilkan versi
@@ -23,6 +30,28 @@ Penggunaan:
23
30
  Opsi ask:
24
31
  --file <path> Lampirkan file sebagai konteks (boleh diulang)
25
32
  --dir <path> Lampirkan isi folder sebagai konteks (boleh diulang)
33
+ --auto, --yes Mode auto-allow: aksi tulis/perintah langsung dijalankan
34
+ tanpa persetujuan (tetap terkunci di folder kerja)
35
+ --tanpa-brief Lewatkan brief proyek (.vibekit/brief.md) untuk pertanyaan ini
36
+
37
+ Opsi chat:
38
+ --lanjut Lanjutkan sesi chat terakhir yang tersimpan (riwayat dipulihkan)
39
+ --auto, --yes Mode auto-allow seperti di atas; di tengah sesi, perintah
40
+ /auto menyalakan/mematikannya kapan saja, /sesi menampilkan
41
+ id sesi aktif, /keluar untuk berhenti
42
+ --tanpa-brief Lewatkan brief proyek untuk sesi ini
43
+
44
+ Opsi do:
45
+ --auto, --yes Mode auto-allow seperti di atas
46
+ --rencana Susun rencana langkah dulu, minta persetujuanmu SATU kali,
47
+ lalu jalankan rencana itu tanpa konfirmasi per langkah
48
+ --tanpa-brief Lewatkan brief proyek untuk tugas ini
49
+
50
+ Streaming:
51
+ Jawaban ask/chat dan ringkasan akhir do tampil mengalir (streaming) begitu
52
+ model menulisnya. Bila aliran terputus di TENGAH jalan setelah sebagian
53
+ jawaban tampil, permintaan tidak diulang ke penyedia lain agar keluaran
54
+ tidak ganda; error sebelum token pertama tetap di-failover seperti biasa.
26
55
 
27
56
  Kemampuan agent (ask, chat, do):
28
57
  Sejak v0.3.0 ketiga perintah itu bisa bertindak, bukan hanya menjawab:
@@ -31,11 +60,21 @@ Kemampuan agent (ask, chat, do):
31
60
  baca_file) berjalan langsung; tool yang mengubah sesuatu (tulis_file,
32
61
  edit_file, jalankan_perintah) selalu meminta persetujuanmu satu per satu
33
62
  — jawaban selain y/ya berarti ditolak. Batas 12 langkah per giliran.
63
+ Mode auto-allow (--auto/--yes, atau /auto di chat) melewati persetujuan
64
+ itu: semua aksi langsung dijalankan, pagar folder kerja tetap berlaku.
65
+ Hanya aktifkan di folder yang aman dan untuk tugas yang kamu percayai.
34
66
  Perbedaannya: ask = satu pertanyaan yang boleh berakhir dengan aksi,
35
67
  chat = percakapan yang bisa bertindak di tengah obrolan,
36
68
  do = mode tugas sekali jalan dengan jejak langkah & ringkasan akhir.
37
69
  Contoh: vibekit do "buatkan file halo.txt berisi sapaan, lalu tampilkan daftar file di sini"
38
70
 
71
+ Dukungan MCP (Model Context Protocol):
72
+ Bila konfigurasi memuat field "mcpServers", VibeKit menyambung ke server
73
+ MCP lokal (stdio) maupun remote (HTTP) itu dan tool mereka tampil
74
+ berdampingan dengan tool bawaan, bernama mcp__<server>__<tool>. Tool MCP
75
+ diperlakukan sebagai aksi: selalu meminta persetujuanmu, kecuali mode
76
+ auto-allow. Perintah "vibekit mcp" menampilkan tool yang tersedia.
77
+
39
78
  Cara kerja singkat:
40
79
  Penyedia dicoba sesuai urutan di konfigurasi. Bila kuota penyedia terdeteksi
41
80
  habis, permintaan otomatis diulang ke penyedia berikutnya. Setiap jawaban
@@ -57,10 +96,42 @@ function perintahInit() {
57
96
  }
58
97
  return 0;
59
98
  }
99
+ /** Satu baris peringatan yang dicetak di awal perintah saat mode auto-allow aktif. */
100
+ function cetakPeringatanAuto() {
101
+ console.error("[vibekit] Mode auto-allow AKTIF: aksi tulis file & menjalankan perintah akan dijalankan TANPA persetujuanmu. Agent tetap terkunci di folder kerja ini.");
102
+ }
103
+ function potongTeks(teks, maks) {
104
+ return teks.length > maks ? teks.slice(0, maks) + "…" : teks;
105
+ }
106
+ /**
107
+ * Sambungkan server MCP dari konfigurasi (bila ada) dan siapkan tool ekstra
108
+ * untuk loop agent. Satu baris ringkas dicetak per server (`MCP <nama>: N
109
+ * tool`); kegagalan per server tampil sebagai peringatan dan tidak
110
+ * menggagalkan perintah. Kembalikan null bila konfigurasi tanpa mcpServers.
111
+ * Pemanggil WAJIB memanggil `tutup()` saat selesai (try/finally).
112
+ */
113
+ async function siapkanMcp(config) {
114
+ if (!config.mcpServers || config.mcpServers.length === 0)
115
+ return null;
116
+ const pengelola = await sambungkanMcpServers(config.mcpServers);
117
+ for (const r of pengelola.rincianServer) {
118
+ console.error(`[vibekit] MCP ${r.nama}: ${r.tools.length} tool`);
119
+ }
120
+ for (const p of pengelola.peringatan) {
121
+ console.error(`[vibekit] MCP peringatan: ${p}`);
122
+ }
123
+ return {
124
+ toolEkstra: pengelola.tools,
125
+ eksekutorEkstra: (nama, argumen) => pengelola.panggilTool(nama, argumen),
126
+ tutup: () => pengelola.tutupSemua(),
127
+ };
128
+ }
60
129
  function uraiArgAsk(args) {
61
130
  const files = [];
62
131
  const dirs = [];
63
132
  const sisa = [];
133
+ let auto = false;
134
+ let tanpaBrief = false;
64
135
  for (let i = 0; i < args.length; i++) {
65
136
  const a = args[i];
66
137
  if (a === "--file") {
@@ -81,6 +152,12 @@ function uraiArgAsk(args) {
81
152
  else if (a.startsWith("--dir=")) {
82
153
  dirs.push(a.slice("--dir=".length));
83
154
  }
155
+ else if (a === "--auto" || a === "--yes") {
156
+ auto = true;
157
+ }
158
+ else if (a === "--tanpa-brief") {
159
+ tanpaBrief = true;
160
+ }
84
161
  else {
85
162
  sisa.push(a);
86
163
  }
@@ -88,7 +165,22 @@ function uraiArgAsk(args) {
88
165
  const pertanyaan = sisa.join(" ").trim();
89
166
  if (!pertanyaan)
90
167
  return { error: "Pertanyaan kosong. Contoh: vibekit ask \"apa itu API?\"" };
91
- return { pertanyaan, files, dirs };
168
+ return { pertanyaan, files, dirs, auto, tanpaBrief };
169
+ }
170
+ /**
171
+ * Siapkan pesan konteks brief proyek (.vibekit/brief.md di cwd) untuk
172
+ * disuntikkan setelah system prompt. Mengembalikan teks pesan system, atau
173
+ * null bila brief tidak ada / dilewatkan lewat --tanpa-brief. Satu baris
174
+ * info dicetak ke stderr saat brief dipakai.
175
+ */
176
+ function siapkanKonteksBrief(cwd, tanpaBrief) {
177
+ if (tanpaBrief)
178
+ return null;
179
+ const brief = muatKonteksBrief(cwd);
180
+ if (!brief)
181
+ return null;
182
+ console.error(`[vibekit] (memakai brief proyek: ±${formatAngka(brief.token)} token)`);
183
+ return brief.teks;
92
184
  }
93
185
  function buatPembacaBaris() {
94
186
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -162,21 +254,39 @@ async function perintahAsk(args) {
162
254
  // Sejak v0.3.0 ask menjalankan SATU giliran agent: pertanyaan biasa dijawab
163
255
  // teks langsung, permintaan membuat file/perintah dijalankan lewat tool
164
256
  // dengan konfirmasi — pagar keamanan yang sama seperti `do`.
257
+ const cwd = process.cwd();
258
+ const konteksBrief = siapkanKonteksBrief(cwd, urai.tanpaBrief);
165
259
  const messages = [
166
260
  { role: "system", content: SYSTEM_PROMPT_GABUNGAN },
261
+ ...(konteksBrief ? [{ role: "system", content: konteksBrief }] : []),
167
262
  { role: "user", content: isiUser },
168
263
  ];
264
+ if (urai.auto)
265
+ cetakPeringatanAuto();
266
+ const mcp = await siapkanMcp(config);
169
267
  const pembaca = buatPembacaBaris();
170
268
  try {
269
+ // Jawaban dialirkan (streaming) ke stdout potongan demi potongan; jejak
270
+ // langkah tetap ke stderr agar stdout bersih untuk jawaban.
271
+ let sudahMengalir = false;
171
272
  const hasil = await jalankanGiliran({
172
273
  config,
173
274
  messages,
174
275
  lokasiState: statePath(),
175
- cwd: process.cwd(),
276
+ cwd,
176
277
  konfirmasi: buatKonfirmasi(pembaca),
278
+ autoAllow: urai.auto,
177
279
  jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
280
+ stream: true,
281
+ onToken: (potongan) => {
282
+ sudahMengalir = true;
283
+ process.stdout.write(potongan);
284
+ },
285
+ ...(mcp ? { toolEkstra: mcp.toolEkstra, eksekutorEkstra: mcp.eksekutorEkstra } : {}),
178
286
  });
179
287
  if (!hasil.ok) {
288
+ if (sudahMengalir)
289
+ process.stdout.write("\n");
180
290
  if (hasil.batasTercapai) {
181
291
  console.error(`[vibekit] Batas langkah tercapai (${hasil.jumlahLangkah} langkah) sebelum pertanyaan ini selesai dikerjakan. Persempit permintaanmu, atau gunakan vibekit do untuk mode tugas.`);
182
292
  }
@@ -185,7 +295,12 @@ async function perintahAsk(args) {
185
295
  }
186
296
  return 1;
187
297
  }
188
- console.log(hasil.jawabanAkhir);
298
+ if (sudahMengalir) {
299
+ process.stdout.write("\n");
300
+ }
301
+ else {
302
+ console.log(hasil.jawabanAkhir);
303
+ }
189
304
  if (hasil.providerTerakhir) {
190
305
  const sumber = hasil.tokenSumber === "usage" ? "dari usage API" : "estimasi";
191
306
  console.log(`\n— dijawab oleh ${hasil.providerTerakhir.nama} (${hasil.providerTerakhir.model}) • ${formatAngka(hasil.totalToken)} token (${sumber})`);
@@ -197,9 +312,11 @@ async function perintahAsk(args) {
197
312
  }
198
313
  finally {
199
314
  pembaca.tutup();
315
+ if (mcp)
316
+ await mcp.tutup();
200
317
  }
201
318
  }
202
- async function perintahChat() {
319
+ async function perintahChat(args) {
203
320
  let config;
204
321
  try {
205
322
  config = muatConfig().config;
@@ -208,9 +325,52 @@ async function perintahChat() {
208
325
  console.error(`[vibekit] ${e.message}`);
209
326
  return 2;
210
327
  }
328
+ // Mode auto-allow bisa dinyalakan dari flag --auto/--yes, lalu diubah
329
+ // kapan saja di tengah sesi dengan perintah /auto.
330
+ let autoAktif = args.includes("--auto") || args.includes("--yes");
331
+ const lanjut = args.includes("--lanjut");
332
+ const tanpaBrief = args.includes("--tanpa-brief");
333
+ const cwd = process.cwd();
334
+ const dirSesi = folderSesi(configDir());
211
335
  console.log("VibeKit chat — ketik pertanyaan, /keluar untuk berhenti.");
212
336
  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 }];
337
+ console.log("Perintah /auto menyalakan/mematikan mode auto-allow (aksi tanpa persetujuan) di tengah sesi; /sesi menampilkan id sesi aktif.");
338
+ if (autoAktif)
339
+ cetakPeringatanAuto();
340
+ // Sesi: --lanjut memulihkan sesi terakhir yang tersimpan (riwayat internal
341
+ // apa adanya, termasuk pesan mentah tool agar thought_signature utuh);
342
+ // tanpa --lanjut, sesi baru dimulai. Sesi disimpan SETELAH setiap giliran
343
+ // berhasil — sesi yang belum pernah berhasil tidak meninggalkan file.
344
+ let sesiAktif;
345
+ const riwayat = [];
346
+ if (lanjut) {
347
+ const tersimpan = muatSesiTerakhir(dirSesi);
348
+ if (tersimpan) {
349
+ sesiAktif = tersimpan;
350
+ riwayat.push(...tersimpan.pesan);
351
+ const jumlahIsi = tersimpan.pesan.filter((m) => m.role !== "system").length;
352
+ console.log(`[vibekit] Sesi dilanjutkan: ${tersimpan.id} — ${jumlahIsi} pesan dipulihkan (terakhir diperbarui ${new Date(tersimpan.diperbarui).toLocaleString("id-ID")}).`);
353
+ }
354
+ else {
355
+ console.log("[vibekit] Belum ada sesi tersimpan — memulai sesi chat baru.");
356
+ sesiAktif = { id: buatIdSesi(), dibuat: new Date().toISOString(), diperbarui: new Date().toISOString(), pesan: [] };
357
+ riwayat.push({ role: "system", content: SYSTEM_PROMPT_GABUNGAN });
358
+ }
359
+ }
360
+ else {
361
+ sesiAktif = { id: buatIdSesi(), dibuat: new Date().toISOString(), diperbarui: new Date().toISOString(), pesan: [] };
362
+ riwayat.push({ role: "system", content: SYSTEM_PROMPT_GABUNGAN });
363
+ }
364
+ // Brief proyek disuntikkan SEKALI di awal sesi (setelah system prompt),
365
+ // kecuali sudah ada di riwayat sesi yang dilanjutkan.
366
+ const briefSudahAda = riwayat.some((m) => typeof m.content === "string" && m.content.startsWith(AWALAN_KONTEKS_BRIEF));
367
+ if (!briefSudahAda) {
368
+ const konteksBrief = siapkanKonteksBrief(cwd, tanpaBrief);
369
+ if (konteksBrief)
370
+ riwayat.splice(1, 0, { role: "system", content: konteksBrief });
371
+ }
372
+ // Server MCP disambungkan SEKALI untuk seluruh sesi chat ini.
373
+ const mcp = await siapkanMcp(config);
214
374
  const pembaca = buatPembacaBaris();
215
375
  const konfirmasi = buatKonfirmasi(pembaca);
216
376
  try {
@@ -223,23 +383,81 @@ async function perintahChat() {
223
383
  break;
224
384
  if (!baris)
225
385
  continue;
386
+ if (baris === "/auto") {
387
+ autoAktif = !autoAktif;
388
+ console.log(autoAktif
389
+ ? "[vibekit] Mode auto-allow: AKTIF — aksi tulis file & menjalankan perintah berikutnya dijalankan TANPA persetujuan. Agent tetap terkunci di folder kerja ini."
390
+ : "[vibekit] Mode auto-allow: NONAKTIF — aksi tulis file & menjalankan perintah kembali meminta persetujuanmu satu per satu.");
391
+ continue;
392
+ }
393
+ if (baris === "/sesi") {
394
+ console.log(`[vibekit] Sesi aktif: ${sesiAktif.id} (tersimpan otomatis setelah setiap giliran berhasil; lanjutkan kapan pun dengan: vibekit chat --lanjut)`);
395
+ continue;
396
+ }
226
397
  riwayat.push({ role: "user", content: baris });
227
- // Pangkas SADAR-TOOL: hanya pada batas giliran utuh, tidak pernah
398
+ // Kompaksi riwayat aksi DULU: rangkaian tool giliran-giliran lama
399
+ // diganti ringkasan satu baris agar isi file tidak terkirim ulang.
400
+ // Giliran terakhir (yang baru saja ditambahkan pesan user-nya ini
401
+ // menjadi giliran terakhir) tidak disentuh kompaksi. Token yang
402
+ // terbuang DIUKUR dan diakumulasikan ke state (laporan `vibekit hemat`).
403
+ const terkompaksi = kompaksiRiwayatAksiTerukur(riwayat);
404
+ riwayat.splice(0, riwayat.length, ...terkompaksi.riwayat);
405
+ if (terkompaksi.tokenTerbuang > 0) {
406
+ try {
407
+ catatHematKompaksi(statePath(), terkompaksi.tokenTerbuang);
408
+ }
409
+ catch {
410
+ /* pencatatan hemat gagal tidak boleh menggagalkan giliran */
411
+ }
412
+ }
413
+ // Lalu Pangkas SADAR-TOOL: hanya pada batas giliran utuh, tidak pernah
228
414
  // menyisakan pesan tool yatim atau tool_calls tanpa hasilnya.
229
415
  const terpotong = pangkasRiwayatAman(riwayat, BATAS_TOKEN_RIWAYAT);
230
416
  riwayat.splice(0, riwayat.length, ...terpotong);
417
+ // Jawaban dialirkan (streaming): header dicetak saat token pertama
418
+ // tiba dari penyedia pemenang (bukan sebelumnya, agar nama penyedia
419
+ // tidak salah saat failover), lalu potongan teks menyusul apa adanya.
420
+ let sudahMengalir = false;
231
421
  const hasil = await jalankanGiliran({
232
422
  config,
233
423
  messages: riwayat,
234
424
  lokasiState: statePath(),
235
- cwd: process.cwd(),
425
+ cwd,
236
426
  konfirmasi,
427
+ autoAllow: autoAktif,
237
428
  jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
429
+ stream: true,
430
+ onTokenPertama: (provider) => {
431
+ // Header hanya dicetak SEKALI per giliran, pada token pertama dari
432
+ // panggilan router mana pun yang lebih dulu mengeluarkan teks.
433
+ if (sudahMengalir)
434
+ return;
435
+ sudahMengalir = true;
436
+ process.stdout.write(`\nvibekit (${provider.nama})> `);
437
+ },
438
+ onToken: (potongan) => process.stdout.write(potongan),
439
+ ...(mcp ? { toolEkstra: mcp.toolEkstra, eksekutorEkstra: mcp.eksekutorEkstra } : {}),
238
440
  });
239
441
  if (hasil.ok) {
240
- console.log(`\nvibekit (${hasil.providerTerakhir?.nama ?? "?"})> ${hasil.jawabanAkhir}\n`);
442
+ if (sudahMengalir) {
443
+ process.stdout.write("\n\n");
444
+ }
445
+ else {
446
+ console.log(`\nvibekit (${hasil.providerTerakhir?.nama ?? "?"})> ${hasil.jawabanAkhir}\n`);
447
+ }
448
+ // Simpan sesi SETELAH giliran berhasil (riwayat sudah terkompaksi).
449
+ sesiAktif.diperbarui = new Date().toISOString();
450
+ sesiAktif.pesan = [...riwayat];
451
+ try {
452
+ simpanSesi(dirSesi, sesiAktif);
453
+ }
454
+ catch {
455
+ /* kegagalan menyimpan sesi tidak menggagalkan percakapan */
456
+ }
241
457
  }
242
458
  else {
459
+ if (sudahMengalir)
460
+ process.stdout.write("\n");
243
461
  // Giliran gagal/batas tercapai: gulung balik SELURUH giliran ini
244
462
  // (pesan user + rangkaian parsialnya) agar riwayat tetap bersih.
245
463
  let idxUser = -1;
@@ -260,11 +478,20 @@ async function perintahChat() {
260
478
  }
261
479
  finally {
262
480
  pembaca.tutup();
481
+ if (mcp)
482
+ await mcp.tutup();
263
483
  }
264
484
  return 0;
265
485
  }
266
486
  async function perintahDo(args) {
267
- const tugas = args.join(" ").trim();
487
+ const auto = args.includes("--auto") || args.includes("--yes");
488
+ const rencana = args.includes("--rencana");
489
+ const tanpaBrief = args.includes("--tanpa-brief");
490
+ const FLAG_DO = new Set(["--auto", "--yes", "--rencana", "--tanpa-brief"]);
491
+ const tugas = args
492
+ .filter((a) => !FLAG_DO.has(a))
493
+ .join(" ")
494
+ .trim();
268
495
  if (!tugas) {
269
496
  console.error("[vibekit] Tugas kosong. Contoh: vibekit do \"buatkan file halo.txt berisi sapaan\"");
270
497
  return 2;
@@ -280,28 +507,181 @@ async function perintahDo(args) {
280
507
  const cwd = process.cwd();
281
508
  console.log(`VibeKit agent — folder kerja: ${cwd}`);
282
509
  console.log(`Tugas: ${tugas}`);
283
- console.log("Tool baca berjalan langsung; tulis/edit file dan jalankan perintah meminta persetujuanmu satu per satu.\n");
510
+ if (rencana) {
511
+ console.log("Mode rencana: agent menyusun rencana dulu dan meminta persetujuanmu SATU kali sebelum menjalankan apa pun.\n");
512
+ }
513
+ else if (auto) {
514
+ cetakPeringatanAuto();
515
+ }
516
+ else {
517
+ console.log("Tool baca berjalan langsung; tulis/edit file dan jalankan perintah meminta persetujuanmu satu per satu.\n");
518
+ }
519
+ const konteksBrief = siapkanKonteksBrief(cwd, tanpaBrief);
520
+ const mcp = await siapkanMcp(config);
284
521
  const pembaca = buatPembacaBaris();
285
522
  const konfirmasi = buatKonfirmasi(pembaca);
523
+ // Ringkasan akhir dialirkan (streaming) ke stdout; jejak tetap ke stderr.
524
+ let sudahMengalir = false;
525
+ const onToken = (potongan) => {
526
+ sudahMengalir = true;
527
+ process.stdout.write(potongan);
528
+ };
286
529
  try {
530
+ if (rencana) {
531
+ const hasil = await jalankanAgentRencana({
532
+ config,
533
+ tugas,
534
+ lokasiState: statePath(),
535
+ cwd,
536
+ konfirmasi,
537
+ konfirmasiRencana: async (teksRencana) => {
538
+ console.log("\n— rencana agent —");
539
+ console.log(teksRencana);
540
+ const jawaban = await pembaca.tanya("\nJalankan rencana ini? (y/N) ");
541
+ if (jawaban === null)
542
+ return false; // EOF: tolak demi keamanan
543
+ const j = jawaban.trim().toLowerCase();
544
+ if (j !== "y" && j !== "ya")
545
+ return false;
546
+ console.error("[vibekit] Rencana disetujui — eksekusi berjalan TANPA konfirmasi per langkah (mode auto-allow khusus rencana ini). Pagar folder kerja tetap berlaku.");
547
+ return true;
548
+ },
549
+ jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
550
+ stream: true,
551
+ onToken,
552
+ ...(konteksBrief ? { konteksSistem: konteksBrief } : {}),
553
+ ...(mcp ? { toolEkstra: mcp.toolEkstra, eksekutorEkstra: mcp.eksekutorEkstra } : {}),
554
+ });
555
+ for (const c of hasil.catatan) {
556
+ console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
557
+ }
558
+ if (hasil.status === "dibatalkan") {
559
+ console.log("\nDibatalkan — rencana tidak dijalankan, tidak ada perubahan apa pun.");
560
+ return 0;
561
+ }
562
+ if (sudahMengalir) {
563
+ process.stdout.write("\n");
564
+ if (hasil.status !== "selesai")
565
+ console.error(`[vibekit] ${hasil.ringkasan}`);
566
+ }
567
+ else {
568
+ console.log(`\n— ringkasan agent (${hasil.jumlahLangkah} langkah) —`);
569
+ console.log(hasil.ringkasan);
570
+ }
571
+ return hasil.status === "selesai" ? 0 : 1;
572
+ }
287
573
  const hasil = await jalankanAgent({
288
574
  config,
289
575
  tugas,
290
576
  lokasiState: statePath(),
291
577
  cwd,
292
578
  konfirmasi,
579
+ autoAllow: auto,
293
580
  jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
581
+ stream: true,
582
+ onToken,
583
+ ...(konteksBrief ? { konteksSistem: konteksBrief } : {}),
584
+ ...(mcp ? { toolEkstra: mcp.toolEkstra, eksekutorEkstra: mcp.eksekutorEkstra } : {}),
294
585
  });
295
586
  for (const c of hasil.catatan) {
296
587
  console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
297
588
  }
298
- console.log(`\n— ringkasan agent (${hasil.jumlahLangkah} langkah) —`);
299
- console.log(hasil.ringkasan);
589
+ if (sudahMengalir) {
590
+ process.stdout.write("\n");
591
+ if (!hasil.ok)
592
+ console.error(`[vibekit] ${hasil.ringkasan}`);
593
+ }
594
+ else {
595
+ console.log(`\n— ringkasan agent (${hasil.jumlahLangkah} langkah) —`);
596
+ console.log(hasil.ringkasan);
597
+ }
300
598
  return hasil.ok ? 0 : 1;
301
599
  }
302
600
  finally {
303
601
  pembaca.tutup();
602
+ if (mcp)
603
+ await mcp.tutup();
604
+ }
605
+ }
606
+ /**
607
+ * `vibekit mcp`: sambungkan semua server MCP dari konfigurasi, cetak daftar
608
+ * tool per server (nama ter-namespace + deskripsi singkat), lalu putuskan.
609
+ */
610
+ async function perintahMcp() {
611
+ let config;
612
+ try {
613
+ config = muatConfig().config;
614
+ }
615
+ catch (e) {
616
+ console.error(`[vibekit] ${e.message}`);
617
+ return 2;
304
618
  }
619
+ const daftar = config.mcpServers ?? [];
620
+ if (daftar.length === 0) {
621
+ console.log("Belum ada server MCP di konfigurasi.");
622
+ console.log("Tambahkan field \"mcpServers\" pada file konfigurasi — tiap entri adalah server lokal (stdio, lewat \"perintah\") atau remote (HTTP, lewat \"url\"). Contoh minimal:");
623
+ console.log(JSON.stringify({
624
+ mcpServers: [
625
+ {
626
+ nama: "filesystem",
627
+ perintah: "npx",
628
+ argumen: ["-y", "@modelcontextprotocol/server-filesystem", "."],
629
+ },
630
+ ],
631
+ }, null, 2));
632
+ console.log("Lihat bagian \"Dukungan MCP\" di README untuk contoh lengkap, termasuk server remote.");
633
+ return 0;
634
+ }
635
+ const pengelola = await sambungkanMcpServers(daftar);
636
+ try {
637
+ for (const r of pengelola.rincianServer) {
638
+ console.log(`\nMCP ${r.nama} — ${r.tools.length} tool:`);
639
+ for (const t of r.tools) {
640
+ console.log(` ${t.namaLengkap}${t.deskripsi ? ` — ${potongTeks(t.deskripsi, 80)}` : ""}`);
641
+ }
642
+ }
643
+ for (const p of pengelola.peringatan) {
644
+ console.error(`[vibekit] MCP peringatan: ${p}`);
645
+ }
646
+ if (pengelola.rincianServer.length === 0) {
647
+ console.error("[vibekit] Tidak ada server MCP yang berhasil tersambung.");
648
+ return 1;
649
+ }
650
+ return 0;
651
+ }
652
+ finally {
653
+ await pengelola.tutupSemua();
654
+ }
655
+ }
656
+ /** `vibekit hemat`: laporan pemakaian token + penghematan terukur dari state lokal. */
657
+ function perintahHemat() {
658
+ const state = muatState(statePath());
659
+ console.log(bangunLaporanHemat(state, new Date()));
660
+ return 0;
661
+ }
662
+ /** `vibekit sesi`: daftar sesi chat tersimpan, terbaru lebih dulu. */
663
+ function perintahSesi() {
664
+ const daftar = daftarSesi(folderSesi(configDir()));
665
+ if (daftar.length === 0) {
666
+ console.log("Belum ada sesi chat tersimpan. Sesi tersimpan otomatis setelah giliran chat pertama yang berhasil.");
667
+ console.log("Lanjutkan sesi terakhir kapan pun dengan: vibekit chat --lanjut");
668
+ return 0;
669
+ }
670
+ console.log(`Sesi chat tersimpan (${daftar.length}):`);
671
+ for (const s of daftar) {
672
+ const waktu = new Date(s.diperbarui).toLocaleString("id-ID");
673
+ console.log(` ${s.id} • ${waktu} • ${s.jumlahPesan} pesan${s.cuplikan ? ` • "${s.cuplikan}"` : ""}`);
674
+ }
675
+ console.log("\nLanjutkan sesi terakhir dengan: vibekit chat --lanjut");
676
+ return 0;
677
+ }
678
+ /** `vibekit brief`: pindai folder kerja saat ini dan tulis .vibekit/brief.md. */
679
+ function perintahBrief() {
680
+ const hasil = buatBrief(process.cwd());
681
+ console.log(`Brief proyek ditulis: ${hasil.lokasi}`);
682
+ console.log(`Isi: ${hasil.jumlahFile} file terpindai • ±${formatAngka(hasil.token)} token.`);
683
+ console.log("Brief ini dipakai otomatis sebagai konteks pada ask/chat/do berikutnya (lewatkan dengan --tanpa-brief).");
684
+ return 0;
305
685
  }
306
686
  function perintahStatus() {
307
687
  let config;
@@ -340,9 +720,17 @@ async function utama() {
340
720
  case "ask":
341
721
  return perintahAsk(sisa);
342
722
  case "chat":
343
- return perintahChat();
723
+ return perintahChat(sisa);
344
724
  case "do":
345
725
  return perintahDo(sisa);
726
+ case "mcp":
727
+ return perintahMcp();
728
+ case "brief":
729
+ return perintahBrief();
730
+ case "hemat":
731
+ return perintahHemat();
732
+ case "sesi":
733
+ return perintahSesi();
346
734
  case "status":
347
735
  return perintahStatus();
348
736
  case "--version":
package/dist/config.js CHANGED
@@ -68,6 +68,48 @@ function normalisasiProvider(p) {
68
68
  extraBody: p.extraBody,
69
69
  };
70
70
  }
71
+ /** Saring rekaman bebas menjadi Record<string,string> (hanya nilai string yang dipertahankan). */
72
+ function rekamanString(mentah) {
73
+ const hasil = {};
74
+ if (mentah && typeof mentah === "object") {
75
+ for (const [k, v] of Object.entries(mentah)) {
76
+ if (typeof v === "string")
77
+ hasil[k] = v;
78
+ }
79
+ }
80
+ return hasil;
81
+ }
82
+ /**
83
+ * Normalisasi daftar mcpServers dari konfigurasi mentah (validasi ringan):
84
+ * entri wajib punya "nama" dan salah satu dari "perintah" (stdio) / "url"
85
+ * (HTTP); entri yang tidak memenuhi syarat dilewati, bukan menggagalkan muat.
86
+ */
87
+ function normalisasiMcpServers(mentah) {
88
+ if (!Array.isArray(mentah))
89
+ return undefined;
90
+ const hasil = [];
91
+ for (const entri of mentah) {
92
+ if (!entri || typeof entri !== "object")
93
+ continue;
94
+ const o = entri;
95
+ const nama = typeof o.nama === "string" ? o.nama.trim() : "";
96
+ const perintah = typeof o.perintah === "string" && o.perintah.trim() !== "" ? o.perintah : undefined;
97
+ const url = typeof o.url === "string" && o.url.trim() !== "" ? o.url : undefined;
98
+ if (!nama || (!perintah && !url))
99
+ continue;
100
+ hasil.push({
101
+ nama,
102
+ ...(perintah ? { perintah } : {}),
103
+ ...(Array.isArray(o.argumen)
104
+ ? { argumen: o.argumen.filter((a) => typeof a === "string") }
105
+ : {}),
106
+ ...(o.env && typeof o.env === "object" ? { env: rekamanString(o.env) } : {}),
107
+ ...(url ? { url } : {}),
108
+ ...(o.headers && typeof o.headers === "object" ? { headers: rekamanString(o.headers) } : {}),
109
+ });
110
+ }
111
+ return hasil.length > 0 ? hasil : undefined;
112
+ }
71
113
  /**
72
114
  * Muat konfigurasi: file proyek ./vibekit.config.json menimpa global ~/.vibekit/config.json.
73
115
  * Lempar Error dengan pesan Bahasa Indonesia bila tidak ada konfigurasi / tidak valid.
@@ -91,11 +133,15 @@ export function muatConfig(cwd = process.cwd()) {
91
133
  if (!obj || !Array.isArray(obj.providers) || obj.providers.length === 0) {
92
134
  throw new Error(`Konfigurasi di ${sumber} harus berisi array "providers" yang tidak kosong.`);
93
135
  }
136
+ const mcpServers = normalisasiMcpServers(obj.mcpServers);
94
137
  const config = {
95
138
  providers: obj.providers.map((p) => normalisasiProvider(p)),
139
+ ...(mcpServers ? { mcpServers } : {}),
96
140
  gaya: {
97
141
  jawaban: obj.gaya?.jawaban ?? "poin-penting",
98
- maksTokenJawaban: typeof obj.gaya?.maksTokenJawaban === "number" ? obj.gaya.maksTokenJawaban : 800,
142
+ // Angka dipertahankan apa adanya (termasuk 0 = tanpa batas); tidak diisi/null
143
+ // dibiarkan undefined agar router tidak mengirim max_tokens sama sekali.
144
+ maksTokenJawaban: typeof obj.gaya?.maksTokenJawaban === "number" ? obj.gaya.maksTokenJawaban : undefined,
99
145
  },
100
146
  };
101
147
  return { config, sumber };