@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/README.md +196 -7
- package/dist/agent.js +299 -7
- package/dist/brief.js +276 -0
- package/dist/cli.js +407 -19
- 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, 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 {
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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 };
|