@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/README.md +195 -8
- package/dist/agent.js +183 -8
- package/dist/brief.js +276 -0
- package/dist/cli.js +361 -23
- package/dist/config.js +47 -1
- package/dist/mcp.js +389 -0
- package/dist/prompt.js +4 -4
- package/dist/router.js +186 -12
- package/dist/sesi.js +77 -0
- package/dist/state.js +104 -0
- package/examples/vibekit.config.json +1 -1
- package/package.json +1 -1
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,
|
|
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 {
|
|
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.
|
|
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
|
|
18
|
-
vibekit do "<tugas>"
|
|
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
|
|
30
|
-
--
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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 };
|