kiosapi 0.1.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/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/agent/run.js +131 -0
- package/dist/agent/schemas.js +147 -0
- package/dist/agent/team.js +35 -0
- package/dist/agent/tools.js +149 -0
- package/dist/api.js +202 -0
- package/dist/commands.js +432 -0
- package/dist/config.js +49 -0
- package/dist/help.js +45 -0
- package/dist/index.js +76 -0
- package/dist/session.js +166 -0
- package/dist/ui.js +99 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PT Mura Teras Kreatif
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @kiosapi/cli
|
|
2
|
+
|
|
3
|
+
CLI Kiosapi.id berbahasa Indonesia — bangun aplikasimu memakai API key Kiosapi.
|
|
4
|
+
|
|
5
|
+
## Sesi interaktif (seperti Claude Code)
|
|
6
|
+
|
|
7
|
+
Jalankan `kiosapi` tanpa argumen → login (sekali) → masuk sesi. Ketik **tugas langsung** ke agen;
|
|
8
|
+
perintah meta diawali **`/`** (jadi `/isi` ≠ "isi kode di file ini"):
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
$ kiosapi
|
|
12
|
+
buat › bikin file hello.js yang mencetak "Halo Dunia", lalu jalankan
|
|
13
|
+
buat › /mode rencana # ganti mode (rencana | edit | buat)
|
|
14
|
+
rencana › /saldo # perintah meta: /model /otomatis /bersih /pakai /isi /bantuan /keluar
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Riwayat percakapan menyatuh antar giliran. Default mode `buat` (otonom, minta izin sebelum tulis/
|
|
18
|
+
jalankan; `/otomatis` untuk melewati). Subperintah satu-kali di bawah tetap ada untuk skrip/CI.
|
|
19
|
+
|
|
20
|
+
## Subperintah
|
|
21
|
+
|
|
22
|
+
(inti tipis, tanpa dependensi eksternal — hanya Node ≥20 built-ins):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
kiosapi masuk # simpan API key (kios_live_…)
|
|
26
|
+
kiosapi periksa # cek konektivitas + setelan
|
|
27
|
+
kiosapi model --cari coding # daftar model
|
|
28
|
+
kiosapi tanya "halo" # tanya sekali (streaming)
|
|
29
|
+
kiosapi ngobrol # mode percakapan (REPL)
|
|
30
|
+
kiosapi setel model <id> # atur model default
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pipe stdin: `cat error.log | kiosapi tanya "jelaskan error ini"`.
|
|
34
|
+
|
|
35
|
+
**Agen built-in** (butuh model yang mendukung tool calling, mis. `anthropic/claude-sonnet-4-6`):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
kiosapi rencana "telusuri kode & susun rencana" # read-only
|
|
39
|
+
kiosapi edit "ganti judul jadi Halo Dunia" # tulis/edit file (konfirmasi)
|
|
40
|
+
kiosapi buat "bikin REST API Express + tes" # agen otonom (tulis + jalankan)
|
|
41
|
+
kiosapi tim "bikin endpoint /health + tes" # multi-agen: perencana→pengkode→peninjau
|
|
42
|
+
# Opsi: -m <model>, --otomatis (lewati konfirmasi). Tool dibatasi ke direktori kerja;
|
|
43
|
+
# .env & .kiosapiignore dilindungi.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Multimodal:**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
kiosapi gambar "kucing astronot" -o kucing.png # buat gambar → file
|
|
50
|
+
kiosapi video "ombak pantai senja" --detik 6 # buat video (async) → .mp4
|
|
51
|
+
kiosapi lihat foto.png "apa isinya?" # tanya model vision soal gambar
|
|
52
|
+
```
|
|
53
|
+
Agen `edit`/`buat` juga punya tool `buat_gambar` untuk membuat aset.
|
|
54
|
+
|
|
55
|
+
**Sambung ke agen lain** (OpenAI-compatible, vendor-netral):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
kiosapi sambung aider -- --model openai/deepseek/deepseek-v3 # luncurkan aider via Kiosapi
|
|
59
|
+
kiosapi sambung opencode
|
|
60
|
+
kiosapi sambung cursor # tampilkan cara setel editor
|
|
61
|
+
eval "$(kiosapi sambung env)" # export OPENAI_* untuk tool apa pun
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Konfigurasi disimpan di `~/.kiosapi/config.json` (key dapat dioverride lewat `KIOSAPI_API_KEY`).
|
|
65
|
+
Lihat rencana lengkap di `docs/cli-plan.md`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { chatComplete, generateImage, resolveMediaModel, } from '../api.js';
|
|
3
|
+
import { cyan, dim, green, prompt, rupiah, thinking, yellow } from '../ui.js';
|
|
4
|
+
import { systemPrompt, toolsForMode } from './schemas.js';
|
|
5
|
+
import { bacaFile, cari, daftarFile, editFile, jalankan, tulisFile } from './tools.js';
|
|
6
|
+
const MAX_STEPS = 25;
|
|
7
|
+
/** Ask y/t before a destructive action (unless --otomatis). */
|
|
8
|
+
async function allow(otomatis, question) {
|
|
9
|
+
if (otomatis)
|
|
10
|
+
return true;
|
|
11
|
+
const ans = (await prompt(`${yellow('? ')}${question} (y/t) `)).trim().toLowerCase();
|
|
12
|
+
return ans === 'y' || ans === 'ya';
|
|
13
|
+
}
|
|
14
|
+
/** Execute one tool call (with permission gating for destructive tools). */
|
|
15
|
+
async function runTool(call, otomatis) {
|
|
16
|
+
let args = {};
|
|
17
|
+
try {
|
|
18
|
+
args = JSON.parse(call.function.arguments || '{}');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { output: 'Error: argumen bukan JSON valid.' };
|
|
22
|
+
}
|
|
23
|
+
const str = (k) => (typeof args[k] === 'string' ? args[k] : '');
|
|
24
|
+
try {
|
|
25
|
+
switch (call.function.name) {
|
|
26
|
+
case 'baca_file':
|
|
27
|
+
console.log(dim(` baca ${str('path')}`));
|
|
28
|
+
return { output: bacaFile(str('path')) };
|
|
29
|
+
case 'daftar_file':
|
|
30
|
+
console.log(dim(` daftar ${str('path') || '.'}`));
|
|
31
|
+
return { output: daftarFile(str('path') || '.') };
|
|
32
|
+
case 'cari':
|
|
33
|
+
console.log(dim(` cari "${str('pola')}"`));
|
|
34
|
+
return { output: cari(str('pola'), str('ext') || undefined) };
|
|
35
|
+
case 'tulis_file': {
|
|
36
|
+
const p = str('path');
|
|
37
|
+
const isi = str('konten');
|
|
38
|
+
console.log(` ${cyan('tulis')} ${p} ${dim(`(${isi.length} char)`)}`);
|
|
39
|
+
if (!(await allow(otomatis, `Tulis file ${p}?`)))
|
|
40
|
+
return { output: 'Ditolak oleh pengguna.' };
|
|
41
|
+
return { output: tulisFile(p, isi) };
|
|
42
|
+
}
|
|
43
|
+
case 'edit_file': {
|
|
44
|
+
const p = str('path');
|
|
45
|
+
console.log(` ${cyan('edit')} ${p}`);
|
|
46
|
+
if (!(await allow(otomatis, `Edit file ${p}?`)))
|
|
47
|
+
return { output: 'Ditolak oleh pengguna.' };
|
|
48
|
+
return { output: editFile(p, str('cari'), str('ganti')) };
|
|
49
|
+
}
|
|
50
|
+
case 'jalankan': {
|
|
51
|
+
const cmd = str('perintah');
|
|
52
|
+
console.log(` ${cyan('jalankan')}: ${cmd}`);
|
|
53
|
+
if (!(await allow(otomatis, 'Jalankan perintah ini?'))) {
|
|
54
|
+
return { output: 'Ditolak oleh pengguna.' };
|
|
55
|
+
}
|
|
56
|
+
return { output: jalankan(cmd) };
|
|
57
|
+
}
|
|
58
|
+
case 'buat_gambar': {
|
|
59
|
+
const p = str('path');
|
|
60
|
+
console.log(` ${cyan('buat_gambar')} ${p} ${dim(`"${str('prompt').slice(0, 50)}"`)}`);
|
|
61
|
+
if (!(await allow(otomatis, `Buat gambar ${p}? (berbayar)`))) {
|
|
62
|
+
return { output: 'Ditolak oleh pengguna.' };
|
|
63
|
+
}
|
|
64
|
+
const model = await resolveMediaModel(undefined, 'image');
|
|
65
|
+
const img = await generateImage(model, str('prompt'));
|
|
66
|
+
writeFileSync(p, Buffer.from(img.b64, 'base64'));
|
|
67
|
+
return { output: `OK: ${p} dibuat (${rupiah(img.cost)}).` };
|
|
68
|
+
}
|
|
69
|
+
case 'selesai':
|
|
70
|
+
return { output: str('ringkasan') || 'selesai', done: true };
|
|
71
|
+
default:
|
|
72
|
+
return { output: `Error: tool tidak dikenal "${call.function.name}".` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return { output: `Error: ${err instanceof Error ? err.message : String(err)}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Start a fresh session (system prompt seeded from the mode). */
|
|
80
|
+
export function newSession(model, mode, otomatis) {
|
|
81
|
+
return { model, mode, otomatis, messages: [{ role: 'system', content: systemPrompt(mode) }] };
|
|
82
|
+
}
|
|
83
|
+
/** Reset the conversation, keeping the current mode/model/otomatis. */
|
|
84
|
+
export function resetSession(s) {
|
|
85
|
+
s.messages = [{ role: 'system', content: systemPrompt(s.mode) }];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Process one user turn: run the tool-use loop, appending to the session's history. Returns the
|
|
89
|
+
* agent's final text (last answer or the `selesai` summary) — useful for chaining agents in a team.
|
|
90
|
+
*/
|
|
91
|
+
export async function runTurn(s, userText) {
|
|
92
|
+
s.messages.push({ role: 'user', content: userText });
|
|
93
|
+
const tools = toolsForMode(s.mode);
|
|
94
|
+
let lastText = '';
|
|
95
|
+
for (let step = 0; step < MAX_STEPS; step++) {
|
|
96
|
+
const stop = thinking();
|
|
97
|
+
let reply;
|
|
98
|
+
try {
|
|
99
|
+
reply = await chatComplete(s.model, s.messages, tools);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
stop();
|
|
103
|
+
}
|
|
104
|
+
s.messages.push({ role: 'assistant', content: reply.content, tool_calls: reply.tool_calls });
|
|
105
|
+
if (reply.content) {
|
|
106
|
+
console.log(reply.content);
|
|
107
|
+
lastText = reply.content;
|
|
108
|
+
}
|
|
109
|
+
const calls = reply.tool_calls ?? [];
|
|
110
|
+
if (calls.length === 0) {
|
|
111
|
+
if (step === 0) {
|
|
112
|
+
console.log(dim('(Model tidak memakai tool — pastikan model mendukung function calling, mis. /model anthropic/claude-sonnet-4-6.)'));
|
|
113
|
+
}
|
|
114
|
+
return lastText; // final text answer
|
|
115
|
+
}
|
|
116
|
+
for (const call of calls) {
|
|
117
|
+
const result = await runTool(call, s.otomatis);
|
|
118
|
+
s.messages.push({ role: 'tool', content: result.output, tool_call_id: call.id });
|
|
119
|
+
if (result.done) {
|
|
120
|
+
console.log(green(`\n✓ Selesai: ${result.output}`));
|
|
121
|
+
return result.output;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log(yellow(`\nBerhenti setelah ${MAX_STEPS} langkah. Lanjutkan dengan perintah berikutnya.`));
|
|
126
|
+
return lastText;
|
|
127
|
+
}
|
|
128
|
+
/** One-shot: run a single task in a mode (used by the `rencana`/`edit`/`buat` subcommands). */
|
|
129
|
+
export async function runAgent(task, mode, opts) {
|
|
130
|
+
await runTurn(newSession(opts.model, mode, opts.otomatis), task);
|
|
131
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Tool catalog (OpenAI function-calling schemas), described in Indonesian. */
|
|
2
|
+
const TOOLS = {
|
|
3
|
+
baca_file: {
|
|
4
|
+
type: 'function',
|
|
5
|
+
function: {
|
|
6
|
+
name: 'baca_file',
|
|
7
|
+
description: 'Baca isi sebuah file (path relatif ke direktori kerja).',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: { path: { type: 'string', description: 'Path file' } },
|
|
11
|
+
required: ['path'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
daftar_file: {
|
|
16
|
+
type: 'function',
|
|
17
|
+
function: {
|
|
18
|
+
name: 'daftar_file',
|
|
19
|
+
description: 'Daftar isi sebuah folder.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: { path: { type: 'string', description: 'Path folder (default ".")' } },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
cari: {
|
|
27
|
+
type: 'function',
|
|
28
|
+
function: {
|
|
29
|
+
name: 'cari',
|
|
30
|
+
description: 'Cari teks di dalam file-file pada direktori kerja.',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
pola: { type: 'string', description: 'Teks yang dicari' },
|
|
35
|
+
ext: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Batasi ke ekstensi tertentu, mis. ".ts" (opsional)',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ['pola'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
tulis_file: {
|
|
45
|
+
type: 'function',
|
|
46
|
+
function: {
|
|
47
|
+
name: 'tulis_file',
|
|
48
|
+
description: 'Buat atau timpa file dengan konten penuh.',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
path: { type: 'string' },
|
|
53
|
+
konten: { type: 'string', description: 'Isi file lengkap' },
|
|
54
|
+
},
|
|
55
|
+
required: ['path', 'konten'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
edit_file: {
|
|
60
|
+
type: 'function',
|
|
61
|
+
function: {
|
|
62
|
+
name: 'edit_file',
|
|
63
|
+
description: 'Ganti satu potongan teks yang UNIK di sebuah file.',
|
|
64
|
+
parameters: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
path: { type: 'string' },
|
|
68
|
+
cari: { type: 'string', description: 'Teks lama (harus unik di file)' },
|
|
69
|
+
ganti: { type: 'string', description: 'Teks baru' },
|
|
70
|
+
},
|
|
71
|
+
required: ['path', 'cari', 'ganti'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
jalankan: {
|
|
76
|
+
type: 'function',
|
|
77
|
+
function: {
|
|
78
|
+
name: 'jalankan',
|
|
79
|
+
description: 'Jalankan satu perintah shell di direktori kerja.',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: { perintah: { type: 'string' } },
|
|
83
|
+
required: ['perintah'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
buat_gambar: {
|
|
88
|
+
type: 'function',
|
|
89
|
+
function: {
|
|
90
|
+
name: 'buat_gambar',
|
|
91
|
+
description: 'Buat gambar AI dari prompt dan simpan ke file (berbayar).',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
prompt: { type: 'string' },
|
|
96
|
+
path: { type: 'string', description: 'Nama file output, mis. assets/logo.png' },
|
|
97
|
+
},
|
|
98
|
+
required: ['prompt', 'path'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
selesai: {
|
|
103
|
+
type: 'function',
|
|
104
|
+
function: {
|
|
105
|
+
name: 'selesai',
|
|
106
|
+
description: 'Tandai tugas selesai dengan ringkasan singkat.',
|
|
107
|
+
parameters: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: { ringkasan: { type: 'string' } },
|
|
110
|
+
required: ['ringkasan'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const READ_TOOLS = ['baca_file', 'daftar_file', 'cari'];
|
|
116
|
+
const WRITE_TOOLS = ['tulis_file', 'edit_file'];
|
|
117
|
+
const MODE_TOOLS = {
|
|
118
|
+
rencana: [...READ_TOOLS, 'selesai'],
|
|
119
|
+
edit: [...READ_TOOLS, ...WRITE_TOOLS, 'buat_gambar', 'selesai'],
|
|
120
|
+
buat: [...READ_TOOLS, ...WRITE_TOOLS, 'jalankan', 'buat_gambar', 'selesai'],
|
|
121
|
+
};
|
|
122
|
+
/** The tool specs available in a given mode. */
|
|
123
|
+
export function toolsForMode(mode) {
|
|
124
|
+
return MODE_TOOLS[mode].map((name) => {
|
|
125
|
+
const spec = TOOLS[name];
|
|
126
|
+
if (!spec)
|
|
127
|
+
throw new Error(`tool tidak ada: ${name}`);
|
|
128
|
+
return spec;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const MODE_BRIEF = {
|
|
132
|
+
rencana: 'MODE RENCANA (read-only): telusuri kode dan susun rencana langkah. JANGAN mengubah apa pun.',
|
|
133
|
+
edit: 'MODE EDIT: lakukan perubahan kode yang diminta dengan tulis_file/edit_file. Jangan jalankan perintah shell.',
|
|
134
|
+
buat: 'MODE AGEN: bangun/ubah proyek secara otonom. Boleh menulis file dan menjalankan perintah.',
|
|
135
|
+
};
|
|
136
|
+
/** System prompt (Indonesian) that frames the agent's role, mode, and workflow. */
|
|
137
|
+
export function systemPrompt(mode) {
|
|
138
|
+
return `Kamu adalah asisten coding Kiosapi yang bekerja di terminal developer Indonesia.
|
|
139
|
+
${MODE_BRIEF[mode]}
|
|
140
|
+
|
|
141
|
+
Direktori kerja: ${process.cwd()}
|
|
142
|
+
Aturan:
|
|
143
|
+
- Bekerja langkah demi langkah: pakai tool untuk membaca sebelum mengubah.
|
|
144
|
+
- Path selalu relatif ke direktori kerja; akses ke luar ditolak.
|
|
145
|
+
- Buat perubahan kecil dan jelas. Setelah tugas beres, panggil tool "selesai" dengan ringkasan.
|
|
146
|
+
- Jawab dan jelaskan dalam Bahasa Indonesia.`;
|
|
147
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { bold, dim, green } from '../ui.js';
|
|
2
|
+
import { newSession, runTurn } from './run.js';
|
|
3
|
+
const ROLES = {
|
|
4
|
+
perencana: {
|
|
5
|
+
title: '① Perencana',
|
|
6
|
+
mode: 'rencana',
|
|
7
|
+
brief: 'Peranmu: PERENCANA. Telusuri kode seperlunya, lalu susun rencana langkah yang jelas & ringkas. JANGAN menulis kode.',
|
|
8
|
+
},
|
|
9
|
+
pengkode: {
|
|
10
|
+
title: '② Pengkode',
|
|
11
|
+
mode: 'buat',
|
|
12
|
+
brief: 'Peranmu: PENGKODE. Implementasikan tugas mengikuti rencana. Buat perubahan kecil & jelas, lalu panggil selesai.',
|
|
13
|
+
},
|
|
14
|
+
peninjau: {
|
|
15
|
+
title: '③ Peninjau',
|
|
16
|
+
mode: 'rencana',
|
|
17
|
+
brief: 'Peranmu: PENINJAU. Baca file yang relevan dan tinjau hasil implementasi: sebutkan masalah/risiko & saran perbaikan singkat.',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
/** Run one role as a sub-agent and return its final text. */
|
|
21
|
+
async function runRole(role, task, opts) {
|
|
22
|
+
console.log(`\n${bold(role.title)}`);
|
|
23
|
+
const s = newSession(opts.model, role.mode, opts.otomatis);
|
|
24
|
+
s.messages.push({ role: 'system', content: role.brief });
|
|
25
|
+
return runTurn(s, task);
|
|
26
|
+
}
|
|
27
|
+
/** Orchestrate the perencana → pengkode → peninjau pipeline for a task. */
|
|
28
|
+
export async function runTeam(task, opts) {
|
|
29
|
+
console.log(bold('👥 Tim agen: perencana → pengkode → peninjau'));
|
|
30
|
+
console.log(dim(`Model: ${opts.model}`));
|
|
31
|
+
const plan = await runRole(ROLES.perencana, task, opts);
|
|
32
|
+
await runRole(ROLES.pengkode, `Tugas: ${task}\n\nRencana:\n${plan || '(tidak ada rencana eksplisit)'}`, opts);
|
|
33
|
+
await runRole(ROLES.peninjau, `Tinjau hasil implementasi untuk tugas: ${task}`, opts);
|
|
34
|
+
console.log(green('\n✓ Tim selesai.'));
|
|
35
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Local tools the agent can run, all confined to the current working directory and screened by an
|
|
6
|
+
* ignore list (secrets, VCS, deps, build output). These are the only ways the agent touches the
|
|
7
|
+
* machine; permission gating for the destructive ones lives in run.ts.
|
|
8
|
+
*/
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
const MAX_READ = 100_000; // cap a single file read (chars)
|
|
11
|
+
const MAX_MATCHES = 80; // cap search results
|
|
12
|
+
const MAX_OUTPUT = 20_000; // cap captured command output (chars)
|
|
13
|
+
const DEFAULT_IGNORE = ['.git', 'node_modules', '.kiosapi', 'dist', '.next', '.turbo', '.venv'];
|
|
14
|
+
function ignorePatterns() {
|
|
15
|
+
const list = [...DEFAULT_IGNORE];
|
|
16
|
+
const f = join(ROOT, '.kiosapiignore');
|
|
17
|
+
if (existsSync(f)) {
|
|
18
|
+
for (const line of readFileSync(f, 'utf8').split('\n')) {
|
|
19
|
+
const t = line.trim();
|
|
20
|
+
if (t && !t.startsWith('#'))
|
|
21
|
+
list.push(t);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return list;
|
|
25
|
+
}
|
|
26
|
+
/** A path is protected if any segment matches an ignore pattern, or it's a dotenv-style secret. */
|
|
27
|
+
function isProtected(rel, patterns) {
|
|
28
|
+
const segments = rel.split(/[\\/]/);
|
|
29
|
+
if (segments.some((s) => s === '.env' || s.startsWith('.env.')))
|
|
30
|
+
return true;
|
|
31
|
+
return patterns.some((p) => segments.includes(p) || rel === p || rel.startsWith(`${p}/`));
|
|
32
|
+
}
|
|
33
|
+
/** Resolve a user/model-supplied path, rejecting anything outside the working directory or protected. */
|
|
34
|
+
function safePath(p) {
|
|
35
|
+
const abs = resolve(ROOT, p);
|
|
36
|
+
const rel = relative(ROOT, abs);
|
|
37
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
38
|
+
throw new Error(`akses di luar direktori kerja ditolak: ${p}`);
|
|
39
|
+
}
|
|
40
|
+
if (rel !== '' && isProtected(rel, ignorePatterns())) {
|
|
41
|
+
throw new Error(`path dilindungi/diabaikan: ${p}`);
|
|
42
|
+
}
|
|
43
|
+
return { abs, rel: rel || '.' };
|
|
44
|
+
}
|
|
45
|
+
/** baca_file — read a text file. */
|
|
46
|
+
export function bacaFile(path) {
|
|
47
|
+
const { abs } = safePath(path);
|
|
48
|
+
if (!existsSync(abs))
|
|
49
|
+
return `Error: file tidak ada: ${path}`;
|
|
50
|
+
const text = readFileSync(abs, 'utf8');
|
|
51
|
+
return text.length > MAX_READ
|
|
52
|
+
? `${text.slice(0, MAX_READ)}\n…[dipotong, ${text.length} char total]`
|
|
53
|
+
: text;
|
|
54
|
+
}
|
|
55
|
+
/** daftar_file — list a directory (ignored entries hidden). */
|
|
56
|
+
export function daftarFile(path) {
|
|
57
|
+
const { abs, rel } = safePath(path || '.');
|
|
58
|
+
if (!existsSync(abs))
|
|
59
|
+
return `Error: folder tidak ada: ${path}`;
|
|
60
|
+
const patterns = ignorePatterns();
|
|
61
|
+
const entries = readdirSync(abs)
|
|
62
|
+
.filter((name) => !isProtected(rel === '.' ? name : `${rel}/${name}`, patterns))
|
|
63
|
+
.sort()
|
|
64
|
+
.map((name) => {
|
|
65
|
+
const dir = statSync(join(abs, name)).isDirectory();
|
|
66
|
+
return dir ? `${name}/` : name;
|
|
67
|
+
});
|
|
68
|
+
return entries.length > 0 ? entries.join('\n') : '(kosong)';
|
|
69
|
+
}
|
|
70
|
+
/** cari — grep file contents under the working dir (case-insensitive). */
|
|
71
|
+
export function cari(pola, ext) {
|
|
72
|
+
if (!pola)
|
|
73
|
+
return 'Error: pola pencarian kosong.';
|
|
74
|
+
const patterns = ignorePatterns();
|
|
75
|
+
const needle = pola.toLowerCase();
|
|
76
|
+
const hits = [];
|
|
77
|
+
const walk = (dir) => {
|
|
78
|
+
if (hits.length >= MAX_MATCHES)
|
|
79
|
+
return;
|
|
80
|
+
for (const name of readdirSync(dir)) {
|
|
81
|
+
const abs = join(dir, name);
|
|
82
|
+
const rel = relative(ROOT, abs);
|
|
83
|
+
if (isProtected(rel, patterns))
|
|
84
|
+
continue;
|
|
85
|
+
const st = statSync(abs);
|
|
86
|
+
if (st.isDirectory()) {
|
|
87
|
+
walk(abs);
|
|
88
|
+
}
|
|
89
|
+
else if (!ext || name.endsWith(ext)) {
|
|
90
|
+
if (st.size > 2_000_000)
|
|
91
|
+
continue; // skip very large/binary files
|
|
92
|
+
const lines = readFileSync(abs, 'utf8').split('\n');
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i] ?? '';
|
|
95
|
+
if (line.toLowerCase().includes(needle)) {
|
|
96
|
+
hits.push(`${rel}:${i + 1}: ${line.trim().slice(0, 200)}`);
|
|
97
|
+
if (hits.length >= MAX_MATCHES)
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
walk(ROOT);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// unreadable entry — ignore
|
|
109
|
+
}
|
|
110
|
+
return hits.length > 0 ? hits.join('\n') : `(tidak ada hasil untuk "${pola}")`;
|
|
111
|
+
}
|
|
112
|
+
/** tulis_file — create/overwrite a file (parent dirs created). */
|
|
113
|
+
export function tulisFile(path, konten) {
|
|
114
|
+
const { abs } = safePath(path);
|
|
115
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
116
|
+
writeFileSync(abs, konten);
|
|
117
|
+
return `OK: ${path} ditulis (${konten.length} char).`;
|
|
118
|
+
}
|
|
119
|
+
/** edit_file — replace an exact substring (must occur exactly once). */
|
|
120
|
+
export function editFile(path, cari, ganti) {
|
|
121
|
+
const { abs } = safePath(path);
|
|
122
|
+
if (!existsSync(abs))
|
|
123
|
+
return `Error: file tidak ada: ${path}`;
|
|
124
|
+
if (!cari)
|
|
125
|
+
return 'Error: teks "cari" kosong.';
|
|
126
|
+
const text = readFileSync(abs, 'utf8');
|
|
127
|
+
const count = text.split(cari).length - 1;
|
|
128
|
+
if (count === 0)
|
|
129
|
+
return `Error: teks "cari" tidak ditemukan di ${path}.`;
|
|
130
|
+
if (count > 1)
|
|
131
|
+
return `Error: teks "cari" muncul ${count}× di ${path} (harus unik). Perbesar konteks.`;
|
|
132
|
+
writeFileSync(abs, text.replace(cari, ganti));
|
|
133
|
+
return `OK: ${path} diedit.`;
|
|
134
|
+
}
|
|
135
|
+
/** jalankan — run a shell command in the working dir, returning combined output (capped). */
|
|
136
|
+
export function jalankan(perintah) {
|
|
137
|
+
if (!perintah.trim())
|
|
138
|
+
return 'Error: perintah kosong.';
|
|
139
|
+
const res = spawnSync(perintah, {
|
|
140
|
+
cwd: ROOT,
|
|
141
|
+
shell: true,
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
timeout: 120_000,
|
|
144
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
145
|
+
});
|
|
146
|
+
const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.trim();
|
|
147
|
+
const capped = out.length > MAX_OUTPUT ? `${out.slice(0, MAX_OUTPUT)}\n…[dipotong]` : out;
|
|
148
|
+
return `exit=${res.status ?? 'null'}\n${capped || '(tanpa output)'}`;
|
|
149
|
+
}
|