@chispear/vibekit 0.4.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, kompaksiRiwayatAksi, 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.4.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
@@ -25,10 +32,26 @@ Opsi ask:
25
32
  --dir <path> Lampirkan isi folder sebagai konteks (boleh diulang)
26
33
  --auto, --yes Mode auto-allow: aksi tulis/perintah langsung dijalankan
27
34
  tanpa persetujuan (tetap terkunci di folder kerja)
35
+ --tanpa-brief Lewatkan brief proyek (.vibekit/brief.md) untuk pertanyaan ini
28
36
 
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.
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.
32
55
 
33
56
  Kemampuan agent (ask, chat, do):
34
57
  Sejak v0.3.0 ketiga perintah itu bisa bertindak, bukan hanya menjawab:
@@ -45,6 +68,13 @@ Kemampuan agent (ask, chat, do):
45
68
  do = mode tugas sekali jalan dengan jejak langkah & ringkasan akhir.
46
69
  Contoh: vibekit do "buatkan file halo.txt berisi sapaan, lalu tampilkan daftar file di sini"
47
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
+
48
78
  Cara kerja singkat:
49
79
  Penyedia dicoba sesuai urutan di konfigurasi. Bila kuota penyedia terdeteksi
50
80
  habis, permintaan otomatis diulang ke penyedia berikutnya. Setiap jawaban
@@ -70,11 +100,38 @@ function perintahInit() {
70
100
  function cetakPeringatanAuto() {
71
101
  console.error("[vibekit] Mode auto-allow AKTIF: aksi tulis file & menjalankan perintah akan dijalankan TANPA persetujuanmu. Agent tetap terkunci di folder kerja ini.");
72
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
+ }
73
129
  function uraiArgAsk(args) {
74
130
  const files = [];
75
131
  const dirs = [];
76
132
  const sisa = [];
77
133
  let auto = false;
134
+ let tanpaBrief = false;
78
135
  for (let i = 0; i < args.length; i++) {
79
136
  const a = args[i];
80
137
  if (a === "--file") {
@@ -98,6 +155,9 @@ function uraiArgAsk(args) {
98
155
  else if (a === "--auto" || a === "--yes") {
99
156
  auto = true;
100
157
  }
158
+ else if (a === "--tanpa-brief") {
159
+ tanpaBrief = true;
160
+ }
101
161
  else {
102
162
  sisa.push(a);
103
163
  }
@@ -105,7 +165,22 @@ function uraiArgAsk(args) {
105
165
  const pertanyaan = sisa.join(" ").trim();
106
166
  if (!pertanyaan)
107
167
  return { error: "Pertanyaan kosong. Contoh: vibekit ask \"apa itu API?\"" };
108
- return { pertanyaan, files, dirs, auto };
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;
109
184
  }
110
185
  function buatPembacaBaris() {
111
186
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -179,24 +254,39 @@ async function perintahAsk(args) {
179
254
  // Sejak v0.3.0 ask menjalankan SATU giliran agent: pertanyaan biasa dijawab
180
255
  // teks langsung, permintaan membuat file/perintah dijalankan lewat tool
181
256
  // dengan konfirmasi — pagar keamanan yang sama seperti `do`.
257
+ const cwd = process.cwd();
258
+ const konteksBrief = siapkanKonteksBrief(cwd, urai.tanpaBrief);
182
259
  const messages = [
183
260
  { role: "system", content: SYSTEM_PROMPT_GABUNGAN },
261
+ ...(konteksBrief ? [{ role: "system", content: konteksBrief }] : []),
184
262
  { role: "user", content: isiUser },
185
263
  ];
186
264
  if (urai.auto)
187
265
  cetakPeringatanAuto();
266
+ const mcp = await siapkanMcp(config);
188
267
  const pembaca = buatPembacaBaris();
189
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;
190
272
  const hasil = await jalankanGiliran({
191
273
  config,
192
274
  messages,
193
275
  lokasiState: statePath(),
194
- cwd: process.cwd(),
276
+ cwd,
195
277
  konfirmasi: buatKonfirmasi(pembaca),
196
278
  autoAllow: urai.auto,
197
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 } : {}),
198
286
  });
199
287
  if (!hasil.ok) {
288
+ if (sudahMengalir)
289
+ process.stdout.write("\n");
200
290
  if (hasil.batasTercapai) {
201
291
  console.error(`[vibekit] Batas langkah tercapai (${hasil.jumlahLangkah} langkah) sebelum pertanyaan ini selesai dikerjakan. Persempit permintaanmu, atau gunakan vibekit do untuk mode tugas.`);
202
292
  }
@@ -205,7 +295,12 @@ async function perintahAsk(args) {
205
295
  }
206
296
  return 1;
207
297
  }
208
- console.log(hasil.jawabanAkhir);
298
+ if (sudahMengalir) {
299
+ process.stdout.write("\n");
300
+ }
301
+ else {
302
+ console.log(hasil.jawabanAkhir);
303
+ }
209
304
  if (hasil.providerTerakhir) {
210
305
  const sumber = hasil.tokenSumber === "usage" ? "dari usage API" : "estimasi";
211
306
  console.log(`\n— dijawab oleh ${hasil.providerTerakhir.nama} (${hasil.providerTerakhir.model}) • ${formatAngka(hasil.totalToken)} token (${sumber})`);
@@ -217,6 +312,8 @@ async function perintahAsk(args) {
217
312
  }
218
313
  finally {
219
314
  pembaca.tutup();
315
+ if (mcp)
316
+ await mcp.tutup();
220
317
  }
221
318
  }
222
319
  async function perintahChat(args) {
@@ -231,12 +328,49 @@ async function perintahChat(args) {
231
328
  // Mode auto-allow bisa dinyalakan dari flag --auto/--yes, lalu diubah
232
329
  // kapan saja di tengah sesi dengan perintah /auto.
233
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());
234
335
  console.log("VibeKit chat — ketik pertanyaan, /keluar untuk berhenti.");
235
336
  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.");
337
+ console.log("Perintah /auto menyalakan/mematikan mode auto-allow (aksi tanpa persetujuan) di tengah sesi; /sesi menampilkan id sesi aktif.");
237
338
  if (autoAktif)
238
339
  cetakPeringatanAuto();
239
- const riwayat = [{ role: "system", content: SYSTEM_PROMPT_GABUNGAN }];
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);
240
374
  const pembaca = buatPembacaBaris();
241
375
  const konfirmasi = buatKonfirmasi(pembaca);
242
376
  try {
@@ -256,30 +390,74 @@ async function perintahChat(args) {
256
390
  : "[vibekit] Mode auto-allow: NONAKTIF — aksi tulis file & menjalankan perintah kembali meminta persetujuanmu satu per satu.");
257
391
  continue;
258
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
+ }
259
397
  riwayat.push({ role: "user", content: baris });
260
398
  // Kompaksi riwayat aksi DULU: rangkaian tool giliran-giliran lama
261
399
  // diganti ringkasan satu baris agar isi file tidak terkirim ulang.
262
400
  // 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);
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
+ }
266
413
  // Lalu Pangkas SADAR-TOOL: hanya pada batas giliran utuh, tidak pernah
267
414
  // menyisakan pesan tool yatim atau tool_calls tanpa hasilnya.
268
415
  const terpotong = pangkasRiwayatAman(riwayat, BATAS_TOKEN_RIWAYAT);
269
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;
270
421
  const hasil = await jalankanGiliran({
271
422
  config,
272
423
  messages: riwayat,
273
424
  lokasiState: statePath(),
274
- cwd: process.cwd(),
425
+ cwd,
275
426
  konfirmasi,
276
427
  autoAllow: autoAktif,
277
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 } : {}),
278
440
  });
279
441
  if (hasil.ok) {
280
- 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
+ }
281
457
  }
282
458
  else {
459
+ if (sudahMengalir)
460
+ process.stdout.write("\n");
283
461
  // Giliran gagal/batas tercapai: gulung balik SELURUH giliran ini
284
462
  // (pesan user + rangkaian parsialnya) agar riwayat tetap bersih.
285
463
  let idxUser = -1;
@@ -300,13 +478,18 @@ async function perintahChat(args) {
300
478
  }
301
479
  finally {
302
480
  pembaca.tutup();
481
+ if (mcp)
482
+ await mcp.tutup();
303
483
  }
304
484
  return 0;
305
485
  }
306
486
  async function perintahDo(args) {
307
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"]);
308
491
  const tugas = args
309
- .filter((a) => a !== "--auto" && a !== "--yes")
492
+ .filter((a) => !FLAG_DO.has(a))
310
493
  .join(" ")
311
494
  .trim();
312
495
  if (!tugas) {
@@ -324,15 +507,69 @@ async function perintahDo(args) {
324
507
  const cwd = process.cwd();
325
508
  console.log(`VibeKit agent — folder kerja: ${cwd}`);
326
509
  console.log(`Tugas: ${tugas}`);
327
- if (auto) {
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) {
328
514
  cetakPeringatanAuto();
329
515
  }
330
516
  else {
331
517
  console.log("Tool baca berjalan langsung; tulis/edit file dan jalankan perintah meminta persetujuanmu satu per satu.\n");
332
518
  }
519
+ const konteksBrief = siapkanKonteksBrief(cwd, tanpaBrief);
520
+ const mcp = await siapkanMcp(config);
333
521
  const pembaca = buatPembacaBaris();
334
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
+ };
335
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
+ }
336
573
  const hasil = await jalankanAgent({
337
574
  config,
338
575
  tugas,
@@ -341,17 +578,110 @@ async function perintahDo(args) {
341
578
  konfirmasi,
342
579
  autoAllow: auto,
343
580
  jejak: (pesan) => console.error(`[vibekit] ${pesan}`),
581
+ stream: true,
582
+ onToken,
583
+ ...(konteksBrief ? { konteksSistem: konteksBrief } : {}),
584
+ ...(mcp ? { toolEkstra: mcp.toolEkstra, eksekutorEkstra: mcp.eksekutorEkstra } : {}),
344
585
  });
345
586
  for (const c of hasil.catatan) {
346
587
  console.error(`[vibekit] dilewati ${c.nama}: ${c.alasan}`);
347
588
  }
348
- console.log(`\n— ringkasan agent (${hasil.jumlahLangkah} langkah) —`);
349
- 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
+ }
350
598
  return hasil.ok ? 0 : 1;
351
599
  }
352
600
  finally {
353
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;
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;
354
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;
355
685
  }
356
686
  function perintahStatus() {
357
687
  let config;
@@ -393,6 +723,14 @@ async function utama() {
393
723
  return perintahChat(sisa);
394
724
  case "do":
395
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();
396
734
  case "status":
397
735
  return perintahStatus();
398
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 };