kiosapi 0.1.27 → 0.1.29
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/agent/mcp.js +205 -0
- package/dist/agent/run.js +110 -25
- package/dist/agent/skills.js +84 -0
- package/dist/agent/team.js +136 -56
- package/dist/commands.js +223 -3
- package/dist/config.js +73 -1
- package/dist/help.js +19 -0
- package/dist/index.js +4 -1
- package/dist/session.js +125 -3
- package/package.json +1 -1
package/dist/agent/team.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { modelSupportsTools } from '../api.js';
|
|
2
|
+
import { clearTimCheckpoint, saveTimCheckpoint } from '../config.js';
|
|
2
3
|
import { bold, dim, green, yellow } from '../ui.js';
|
|
3
4
|
import { newSession, runTurn } from './run.js';
|
|
4
5
|
/**
|
|
@@ -33,85 +34,164 @@ function tryParsePlan(output) {
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Built-in team: planner-executor
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const PLANNER_BRIEF = `Peranmu: PERENCANA.
|
|
37
|
+
const PLANNER_BRIEF = `Peranmu: PERENCANA. Baca SELURUH spesifikasi terlebih dahulu.
|
|
37
38
|
|
|
38
|
-
WAJIB
|
|
39
|
+
LANGKAH WAJIB SEBELUM MERENCANAKAN:
|
|
40
|
+
1. Cari dan baca file spesifikasi/blueprint: blueprint.md, SPEC.md, README.md, atau file .md di root
|
|
41
|
+
2. Pelajari struktur proyek yang sudah ada (daftar_file)
|
|
42
|
+
3. Baru buat rencana langkah yang KOMPREHENSIF mencakup SEMUA fitur yang diminta
|
|
43
|
+
|
|
44
|
+
WAJIB: panggil tool "selesai" dengan parameter ringkasan berisi JSON dalam format TEPAT ini:
|
|
39
45
|
{"ringkasan":"<ringkasan singkat>","langkah":[{"tugas":"<deskripsi spesifik & actionable>","file":["path/file1.ts"],"catatan":"<konteks tambahan, opsional>"},...]}
|
|
40
46
|
|
|
41
47
|
Aturan rencana:
|
|
42
|
-
- Masing-masing langkah: 1–3 file, satu perubahan jelas
|
|
43
|
-
- Maksimal
|
|
44
|
-
- Sebutkan path file LENGKAP dan AKURAT (gunakan daftar_file untuk memastikan)
|
|
45
|
-
- JANGAN menulis kode — hanya rencanakan
|
|
48
|
+
- Masing-masing langkah: 1–3 file, satu perubahan jelas
|
|
49
|
+
- Maksimal 15 langkah (untuk aplikasi besar buat lebih banyak langkah kecil)
|
|
50
|
+
- Sebutkan path file LENGKAP dan AKURAT (gunakan daftar_file untuk memastikan)
|
|
51
|
+
- JANGAN menulis kode — hanya rencanakan
|
|
52
|
+
- Pastikan rencana LENGKAP — tidak ada fitur yang terlewat dari spesifikasi`;
|
|
46
53
|
const REVIEWER_BRIEF = 'Peranmu: PENINJAU. Baca file yang relevan dan tinjau hasil implementasi: sebutkan masalah/risiko & saran perbaikan singkat.';
|
|
47
|
-
|
|
54
|
+
/** Append role brief to the existing system message (index 0) instead of pushing a second
|
|
55
|
+
* system message. Many providers reject or ignore a second {role:'system'} entry before the
|
|
56
|
+
* first user turn, causing the agent to ignore its role instructions entirely. */
|
|
57
|
+
function appendBrief(session, brief) {
|
|
58
|
+
const sys = session.messages[0];
|
|
59
|
+
if (sys?.role === 'system') {
|
|
60
|
+
sys.content += `\n\n${brief}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function runTeam(task, opts, resume) {
|
|
48
64
|
console.log(bold('👥 Tim agen — mode perencana-eksekutor'));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(green('\n✓ Tim selesai.'));
|
|
68
|
-
return;
|
|
65
|
+
let plan = null;
|
|
66
|
+
// If resuming with a saved plan, skip the planner phase entirely
|
|
67
|
+
if (resume?.plan) {
|
|
68
|
+
plan = resume.plan;
|
|
69
|
+
console.log(yellow(`↩ Melanjutkan dari rencana tersimpan (${plan.langkah.length} langkah)`));
|
|
70
|
+
console.log(bold(`\n📋 ${plan.ringkasan}`));
|
|
71
|
+
for (const [i, step] of plan.langkah.entries()) {
|
|
72
|
+
const done = i < (resume.startFromStep ?? 0);
|
|
73
|
+
const marker = done ? dim('✓') : dim(`${i + 1}.`);
|
|
74
|
+
const label = done ? dim(step.tugas) : step.tugas;
|
|
75
|
+
console.log(` ${marker} ${label}`);
|
|
76
|
+
if (!done) {
|
|
77
|
+
console.log(dim(` 📄 ${step.file.join(', ')}`));
|
|
78
|
+
if (step.catatan)
|
|
79
|
+
console.log(dim(` 💬 ${step.catatan}`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
69
83
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
else {
|
|
85
|
+
// ① Planner
|
|
86
|
+
console.log(`\n${bold('① Perencana')} ${dim(`(${opts.models.perencana})`)}`);
|
|
87
|
+
const plannerSession = newSession(opts.models.perencana, 'rencana', opts.otomatis);
|
|
88
|
+
appendBrief(plannerSession, PLANNER_BRIEF);
|
|
89
|
+
const planOutput = await runTurn(plannerSession, task);
|
|
90
|
+
plan = tryParsePlan(planOutput);
|
|
91
|
+
// Retry once if the planner didn't output valid JSON — it may have just forgotten the format.
|
|
92
|
+
if (!plan && planOutput.trim()) {
|
|
93
|
+
console.log(yellow('⚠ Rencana tidak valid — mencoba ulang sekali...'));
|
|
94
|
+
const retryOutput = await runTurn(plannerSession, 'Output sebelumnya tidak valid. Keluarkan HANYA JSON ini tanpa teks lain:\n{"ringkasan":"...","langkah":[{"tugas":"...","file":["path/file.ts"]}]}');
|
|
95
|
+
plan = tryParsePlan(retryOutput);
|
|
96
|
+
}
|
|
97
|
+
if (!plan) {
|
|
98
|
+
console.log(yellow('⚠ Perencana tidak menghasilkan rencana terstruktur — jalankan sebagai sesi pengkode tunggal.'));
|
|
99
|
+
const s = newSession(opts.models.pengkode, 'buat', opts.otomatis);
|
|
100
|
+
appendBrief(s, 'Peranmu: PENGKODE. Implementasikan tugas mengikuti rencana. Buat perubahan kecil & jelas, lalu panggil selesai.');
|
|
101
|
+
await runTurn(s, `Tugas: ${task}\n\nRencana:\n${planOutput || '(tidak ada rencana eksplisit)'}`);
|
|
102
|
+
clearTimCheckpoint();
|
|
103
|
+
console.log(`\n${bold('③ Peninjau')} ${dim(`(${opts.models.peninjau})`)}`);
|
|
104
|
+
const rev = newSession(opts.models.peninjau, 'rencana', opts.otomatis);
|
|
105
|
+
appendBrief(rev, REVIEWER_BRIEF);
|
|
106
|
+
await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}`);
|
|
107
|
+
console.log(green('\n✓ Tim selesai.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Show plan
|
|
111
|
+
console.log(bold(`\n📋 ${plan.ringkasan}`));
|
|
112
|
+
for (const [i, step] of plan.langkah.entries()) {
|
|
113
|
+
console.log(` ${dim(`${i + 1}.`)} ${step.tugas}`);
|
|
114
|
+
console.log(dim(` 📄 ${step.file.join(', ')}`));
|
|
115
|
+
if (step.catatan)
|
|
116
|
+
console.log(dim(` 💬 ${step.catatan}`));
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
// Save checkpoint as soon as the plan is established — if execution is interrupted,
|
|
120
|
+
// `kiosapi lanjut` can resume from step 0 with the existing plan.
|
|
121
|
+
saveTimCheckpoint({
|
|
122
|
+
task,
|
|
123
|
+
plan,
|
|
124
|
+
completedStepCount: 0,
|
|
125
|
+
stepResults: [],
|
|
126
|
+
models: opts.models,
|
|
127
|
+
otomatis: opts.otomatis,
|
|
128
|
+
});
|
|
77
129
|
}
|
|
78
|
-
console.log('');
|
|
79
130
|
// ② Execute each step in a fresh focused session
|
|
131
|
+
const startFromStep = resume?.startFromStep ?? 0;
|
|
132
|
+
const completedSteps = resume?.stepResults ? [...resume.stepResults] : [];
|
|
133
|
+
if (startFromStep > 0) {
|
|
134
|
+
console.log(yellow(`↩ Melanjutkan dari langkah ${startFromStep + 1}/${plan.langkah.length}`));
|
|
135
|
+
}
|
|
80
136
|
for (const [i, step] of plan.langkah.entries()) {
|
|
137
|
+
if (i < startFromStep)
|
|
138
|
+
continue; // skip already-completed steps
|
|
81
139
|
const label = `${i + 1}/${plan.langkah.length}`;
|
|
82
140
|
console.log(bold(`\n② Pengkode — Langkah ${label}`) + dim(`: ${step.tugas}`));
|
|
83
141
|
console.log(dim(` 📄 ${step.file.join(', ')}`));
|
|
84
142
|
const s = newSession(opts.models.pengkode, 'buat', opts.otomatis);
|
|
85
|
-
s.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
s.maxSteps = 30; // executor steps are focused (1-3 files) — keep tight
|
|
144
|
+
appendBrief(s, [
|
|
145
|
+
`Peranmu: PENGKODE — langkah ${label} dari ${plan.langkah.length}.`,
|
|
146
|
+
`TUJUAN KESELURUHAN: ${task.slice(0, 400)}`,
|
|
147
|
+
`RINGKASAN RENCANA: ${plan.ringkasan}`,
|
|
148
|
+
'',
|
|
149
|
+
`TUGAS LANGKAH INI: ${step.tugas}`,
|
|
150
|
+
`File relevan: ${step.file.join(', ')}`,
|
|
151
|
+
step.catatan ? `Catatan: ${step.catatan}` : '',
|
|
152
|
+
'',
|
|
153
|
+
'Fokus HANYA pada tugas ini. Baca file relevan, buat perubahan, panggil selesai.',
|
|
154
|
+
'Jangan baca file lain kecuali diperlukan langsung untuk memahami konteks.',
|
|
155
|
+
]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join('\n'));
|
|
158
|
+
const previousContext = completedSteps.length > 0
|
|
159
|
+
? `\n\n[Progress sebelumnya (${completedSteps.length} langkah selesai):\n${completedSteps.slice(-5).join('\n')}]`
|
|
160
|
+
: '';
|
|
98
161
|
const stepPrompt = [
|
|
99
162
|
step.tugas,
|
|
100
163
|
`\nFile: ${step.file.join(', ')}`,
|
|
101
164
|
step.catatan ? `\nCatatan: ${step.catatan}` : '',
|
|
165
|
+
previousContext,
|
|
102
166
|
]
|
|
103
167
|
.filter(Boolean)
|
|
104
168
|
.join('');
|
|
105
|
-
|
|
169
|
+
const stepStart = Date.now();
|
|
170
|
+
const stepResult = await runTurn(s, stepPrompt);
|
|
171
|
+
const elapsed = Math.round((Date.now() - stepStart) / 1000);
|
|
172
|
+
const didComplete = s._lastCompleted === true;
|
|
173
|
+
const outcome = stepResult?.trim().slice(0, 500) || '(tidak ada output eksplisit)';
|
|
174
|
+
completedSteps.push(`Langkah ${i + 1} "${step.tugas}" [${didComplete ? '✓' : '⚠'}]: ${outcome}`);
|
|
175
|
+
const statusLine = didComplete
|
|
176
|
+
? green(`✓ Langkah ${label} selesai`)
|
|
177
|
+
: yellow(`⚠ Langkah ${label} mungkin tidak tuntas`);
|
|
178
|
+
console.log(`${statusLine} ${dim(`(${elapsed}s)`)}`);
|
|
179
|
+
// Persist progress after every step — a crash here loses at most one step
|
|
180
|
+
saveTimCheckpoint({
|
|
181
|
+
task,
|
|
182
|
+
plan,
|
|
183
|
+
completedStepCount: i + 1,
|
|
184
|
+
stepResults: completedSteps,
|
|
185
|
+
models: opts.models,
|
|
186
|
+
otomatis: opts.otomatis,
|
|
187
|
+
});
|
|
106
188
|
}
|
|
107
|
-
// ③ Reviewer —
|
|
189
|
+
// ③ Reviewer — receives the plan + step outcomes so it knows what actually happened
|
|
108
190
|
console.log(`\n${bold('③ Peninjau')} ${dim(`(${opts.models.peninjau})`)}`);
|
|
109
|
-
const stepSummary = plan.langkah
|
|
110
|
-
.map((s, i) => ` ${i + 1}. ${s.tugas} [${s.file.join(', ')}]`)
|
|
111
|
-
.join('\n');
|
|
112
191
|
const rev = newSession(opts.models.peninjau, 'rencana', opts.otomatis);
|
|
113
|
-
rev
|
|
114
|
-
await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}\n\
|
|
192
|
+
appendBrief(rev, REVIEWER_BRIEF);
|
|
193
|
+
await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}\n\nHasil per langkah:\n${completedSteps.join('\n')}`);
|
|
194
|
+
clearTimCheckpoint();
|
|
115
195
|
console.log(green('\n✓ Tim selesai.'));
|
|
116
196
|
}
|
|
117
197
|
// ---------------------------------------------------------------------------
|
|
@@ -144,7 +224,7 @@ export async function runCustomTeam(task, opts) {
|
|
|
144
224
|
console.log(`\n${bold(`${String(i + 1).padStart(2)}. ${roleName}`)} ${dim(`(${model}, mode: ${mode})`)}`);
|
|
145
225
|
const s = newSession(model, mode, otomatis);
|
|
146
226
|
if (roleCfg.brief)
|
|
147
|
-
s
|
|
227
|
+
appendBrief(s, roleCfg.brief);
|
|
148
228
|
const result = await runTurn(s, context);
|
|
149
229
|
const trimmed = (result ?? '').trim();
|
|
150
230
|
const capped = trimmed.length > MAX_ROLE_OUTPUT
|
package/dist/commands.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
+
import { McpManager } from './agent/mcp.js';
|
|
6
7
|
import { loadCheckpoint, runAgent } from './agent/run.js';
|
|
8
|
+
import { listSkills, loadSkill } from './agent/skills.js';
|
|
7
9
|
import { runCustomTeam, runTeam } from './agent/team.js';
|
|
8
10
|
import { expandAtMentions } from './agent/tools.js';
|
|
9
11
|
import { createTopup, fetchBytesAuthed, fetchLatestVersion, fetchModels, fetchPemakaian, fetchSaldo, generateImage, modelSupportsTools, pollJob, resolveMediaModel, resolveModel, streamChat, streamVision, submitVideo, } from './api.js';
|
|
10
|
-
import { clearKey, fileConfig, initProjectConfig, listTeamConfigs, loadConfig, loadProjectConfig, loadTeamConfig, saveConfig, saveTeamConfig, } from './config.js';
|
|
12
|
+
import { SKILLS_GLOBAL_DIR, clearKey, fileConfig, initProjectConfig, listTeamConfigs, loadConfig, loadMcpServers, loadProjectConfig, loadRawMcpConfig, loadTeamConfig, projectSkillsDir, saveConfig, saveMcpConfig, saveTeamConfig, } from './config.js';
|
|
11
13
|
import { VERSION } from './help.js';
|
|
12
14
|
import { bold, cyan, dim, green, idn, prompt, promptHidden, readStdin, red, rupiah, sleep, thinking, yellow, } from './ui.js';
|
|
13
15
|
const IMAGE_MIME = {
|
|
@@ -483,6 +485,219 @@ async function timBuatWizard(nama) {
|
|
|
483
485
|
console.log(dim(` Edit : ~/.kiosapi/tim/${nama}.json`));
|
|
484
486
|
console.log(dim(` Jalankan: kiosapi tim --pakai ${nama} "tugas"`));
|
|
485
487
|
}
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// mcp — manage MCP server connections
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
const MCP_TEMPLATE = {
|
|
492
|
+
mcpServers: {
|
|
493
|
+
filesystem: {
|
|
494
|
+
command: 'npx',
|
|
495
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
|
|
496
|
+
},
|
|
497
|
+
github: {
|
|
498
|
+
command: 'npx',
|
|
499
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
500
|
+
env: { GITHUB_TOKEN: 'ghp_YOUR_TOKEN_HERE' },
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* mcp — list, test, or scaffold MCP server configuration.
|
|
506
|
+
* kiosapi mcp List configured servers
|
|
507
|
+
* kiosapi mcp sambung Connect to all servers and show available tools
|
|
508
|
+
* kiosapi mcp init Scaffold ~/.kiosapi/mcp.json with examples
|
|
509
|
+
*/
|
|
510
|
+
export async function cmdMcp(args) {
|
|
511
|
+
const sub = (args[0] ?? '').toLowerCase();
|
|
512
|
+
// kiosapi mcp init — scaffold config file
|
|
513
|
+
if (sub === 'init') {
|
|
514
|
+
const { DIR } = await import('./config.js');
|
|
515
|
+
const path = `${DIR}/mcp.json`;
|
|
516
|
+
const { existsSync } = await import('node:fs');
|
|
517
|
+
if (existsSync(path)) {
|
|
518
|
+
console.log(yellow('~/.kiosapi/mcp.json sudah ada. Edit langsung:'));
|
|
519
|
+
console.log(path);
|
|
520
|
+
console.log(JSON.stringify(loadRawMcpConfig(), null, 2));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
saveMcpConfig(MCP_TEMPLATE.mcpServers);
|
|
524
|
+
console.log(green('✓ ~/.kiosapi/mcp.json dibuat'));
|
|
525
|
+
console.log(dim(' Edit untuk sesuaikan server dan path, lalu jalankan: kiosapi mcp sambung'));
|
|
526
|
+
console.log(JSON.stringify(MCP_TEMPLATE, null, 2));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const servers = loadMcpServers();
|
|
530
|
+
const serverCount = Object.keys(servers).length;
|
|
531
|
+
// kiosapi mcp — list configured servers (no connection)
|
|
532
|
+
if (!sub || sub === 'daftar' || sub === 'list') {
|
|
533
|
+
if (serverCount === 0) {
|
|
534
|
+
console.log(dim('Belum ada MCP server dikonfigurasi.'));
|
|
535
|
+
console.log(dim(' Scaffold: kiosapi mcp init'));
|
|
536
|
+
console.log(dim(' Config : ~/.kiosapi/mcp.json atau kiosapi.json → mcpServers'));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
console.log(bold(`MCP Servers (${serverCount}):`));
|
|
540
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
541
|
+
const cmd = [cfg.command, ...(cfg.args ?? [])].join(' ');
|
|
542
|
+
console.log(` ${cyan(name)} ${dim(cmd)}`);
|
|
543
|
+
}
|
|
544
|
+
console.log(dim('\nTest koneksi: kiosapi mcp sambung'));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// kiosapi mcp sambung — connect and list tools
|
|
548
|
+
if (sub === 'sambung' || sub === 'connect' || sub === 'test') {
|
|
549
|
+
if (serverCount === 0) {
|
|
550
|
+
console.log(dim('Belum ada MCP server dikonfigurasi. Jalankan: kiosapi mcp init'));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
console.log(dim(`Menghubungkan ke ${serverCount} server MCP…`));
|
|
554
|
+
const mgr = new McpManager();
|
|
555
|
+
try {
|
|
556
|
+
await mgr.connect(servers, (warn) => console.log(yellow(warn)));
|
|
557
|
+
if (mgr.toolCount === 0) {
|
|
558
|
+
console.log(yellow('Tidak ada tools yang tersedia dari server yang terhubung.'));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
console.log(green(`\n✓ ${mgr.serverCount} server · ${mgr.toolCount} tools\n`));
|
|
562
|
+
for (const { server, tools } of mgr.summary()) {
|
|
563
|
+
console.log(bold(` ${server}`));
|
|
564
|
+
for (const t of tools) {
|
|
565
|
+
const desc = (t.spec.function.description ?? '').replace(`[MCP:${server}] `, '');
|
|
566
|
+
console.log(` ${cyan(t.toolName)} ${dim(desc.slice(0, 70))}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
console.log(dim('\nTools ini tersedia otomatis di semua sesi agen.'));
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
mgr.disconnect();
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
console.log(red(`Sub-perintah tidak dikenal: mcp ${sub}`));
|
|
577
|
+
console.log(dim(' kiosapi mcp — lihat server dikonfigurasi'));
|
|
578
|
+
console.log(dim(' kiosapi mcp sambung — test koneksi + lihat tools'));
|
|
579
|
+
console.log(dim(' kiosapi mcp init — scaffold ~/.kiosapi/mcp.json'));
|
|
580
|
+
}
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// skill — reusable saved agent workflows
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
const SKILL_TEMPLATE = `---
|
|
585
|
+
mode: buat
|
|
586
|
+
# otomatis: false
|
|
587
|
+
# model: deepseek/deepseek-v4-flash
|
|
588
|
+
description: Deskripsi singkat skill ini
|
|
589
|
+
---
|
|
590
|
+
Tulis prompt skillmu di sini.
|
|
591
|
+
|
|
592
|
+
Tips — gunakan @-mention untuk menyertakan konteks:
|
|
593
|
+
@git:diff git diff saat ini
|
|
594
|
+
@git:status status working tree
|
|
595
|
+
@src/**/*.ts semua file .ts di src/
|
|
596
|
+
@path/to/file.ts file spesifik
|
|
597
|
+
|
|
598
|
+
Contoh:
|
|
599
|
+
Review @git:diff, identifikasi masalah potensial, dan buat laporan ringkas.
|
|
600
|
+
`;
|
|
601
|
+
/** Try to open a file in the user's preferred editor; falls back to printing the path. */
|
|
602
|
+
function openInEditor(filePath) {
|
|
603
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
604
|
+
if (!editor) {
|
|
605
|
+
console.log(dim(` Buka di editor: ${filePath}`));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const res = spawnSync(editor, [filePath], { stdio: 'inherit' });
|
|
609
|
+
if (res.error)
|
|
610
|
+
console.log(dim(` Edit manual: ${filePath}`));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* skill — create, list, or run saved skill files.
|
|
614
|
+
* kiosapi skill buat [nama] Buat skill baru (project scope)
|
|
615
|
+
* kiosapi skill buat --global [nama] Buat skill di ~/.kiosapi/skills/
|
|
616
|
+
* kiosapi skill <nama> [konteks] Jalankan skill
|
|
617
|
+
* kiosapi skills Sama dengan kiosapi skill --daftar
|
|
618
|
+
*/
|
|
619
|
+
export async function cmdSkill(args) {
|
|
620
|
+
const sub = args[0]?.toLowerCase();
|
|
621
|
+
// kiosapi skill buat [--global] [nama]
|
|
622
|
+
if (sub === 'buat' || sub === 'create' || sub === 'new') {
|
|
623
|
+
const rest = args.slice(1);
|
|
624
|
+
const isGlobal = rest[0] === '--global' || rest[0] === '-g';
|
|
625
|
+
const nameArg = isGlobal ? rest[1] : rest[0];
|
|
626
|
+
const name = nameArg?.trim() || (await prompt('Nama skill (huruf kecil, tanda hubung): ')).trim();
|
|
627
|
+
if (!name)
|
|
628
|
+
throw new Error('Nama skill tidak boleh kosong.');
|
|
629
|
+
if (!/^[\w-]+$/.test(name))
|
|
630
|
+
throw new Error('Nama hanya boleh huruf, angka, dan tanda hubung.');
|
|
631
|
+
const dir = isGlobal ? SKILLS_GLOBAL_DIR : projectSkillsDir();
|
|
632
|
+
mkdirSync(dir, { recursive: true });
|
|
633
|
+
const filePath = `${dir}/${name}.md`;
|
|
634
|
+
writeFileSync(filePath, SKILL_TEMPLATE);
|
|
635
|
+
const scope = isGlobal ? 'global (~/.kiosapi/skills/)' : 'project (.kiosapi/skills/)';
|
|
636
|
+
console.log(green(`\n✓ Skill "${name}" dibuat (${scope})`));
|
|
637
|
+
console.log(dim(` File: ${filePath}`));
|
|
638
|
+
console.log(dim(` Jalankan: kiosapi skill ${name}`));
|
|
639
|
+
console.log(dim(' Edit prompt & frontmatter lalu simpan.\n'));
|
|
640
|
+
openInEditor(filePath);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// kiosapi skill --daftar | kiosapi skills
|
|
644
|
+
if (!sub || sub === '--daftar' || sub === '--list' || sub === 'daftar' || sub === 'list') {
|
|
645
|
+
return cmdSkills();
|
|
646
|
+
}
|
|
647
|
+
// kiosapi skill <nama> [-m model] [--otomatis] [extra context…]
|
|
648
|
+
const skillName = sub;
|
|
649
|
+
const { values: flagValues, positionals: extraPositionals } = parseArgs({
|
|
650
|
+
args: args.slice(1),
|
|
651
|
+
options: { model: { type: 'string', short: 'm' }, otomatis: { type: 'boolean' } },
|
|
652
|
+
allowPositionals: true,
|
|
653
|
+
strict: false,
|
|
654
|
+
});
|
|
655
|
+
const extraArgs = extraPositionals.join(' ').trim();
|
|
656
|
+
const skill = loadSkill(skillName);
|
|
657
|
+
if (!skill) {
|
|
658
|
+
console.error(red(`Skill "${skillName}" tidak ditemukan.`));
|
|
659
|
+
console.log(dim(' Buat baru: kiosapi skill buat <nama>'));
|
|
660
|
+
console.log(dim(' Lihat daftar: kiosapi skills'));
|
|
661
|
+
process.exitCode = 1;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const cfg = loadConfig();
|
|
665
|
+
const model = flagValues.model ?? skill.model ?? cfg.defaultModel;
|
|
666
|
+
const resolvedModel = await resolveModel(model);
|
|
667
|
+
const otomatis = flagValues.otomatis ?? skill.otomatis;
|
|
668
|
+
const scopeLabel = skill.source === 'project' ? dim(' [project]') : dim(' [global]');
|
|
669
|
+
console.log(`${bold(`▶ ${skill.name}`)}${scopeLabel}${skill.description ? ` — ${dim(skill.description)}` : ''}`);
|
|
670
|
+
const prompt_ = extraArgs ? `${skill.prompt}\n\n${extraArgs}` : skill.prompt;
|
|
671
|
+
const expanded = await expandAtMentions(prompt_);
|
|
672
|
+
await runAgent(expanded, skill.mode, { model: resolvedModel, otomatis });
|
|
673
|
+
}
|
|
674
|
+
/** skills — list all available skills. */
|
|
675
|
+
export async function cmdSkills() {
|
|
676
|
+
const all = listSkills();
|
|
677
|
+
if (all.length === 0) {
|
|
678
|
+
console.log(dim('Belum ada skill.'));
|
|
679
|
+
console.log(dim(' Buat baru: kiosapi skill buat <nama>'));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const projectSkills = all.filter((s) => s.source === 'project');
|
|
683
|
+
const globalSkills = all.filter((s) => s.source === 'global');
|
|
684
|
+
if (projectSkills.length > 0) {
|
|
685
|
+
console.log(bold('\nSkill project (.kiosapi/skills/):'));
|
|
686
|
+
for (const sk of projectSkills) {
|
|
687
|
+
const desc = sk.description ? dim(` — ${sk.description}`) : '';
|
|
688
|
+
console.log(` ${cyan(sk.name)}${desc} ${dim(`[${sk.mode}]`)}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (globalSkills.length > 0) {
|
|
692
|
+
console.log(bold('\nSkill global (~/.kiosapi/skills/):'));
|
|
693
|
+
for (const sk of globalSkills) {
|
|
694
|
+
const desc = sk.description ? dim(` — ${sk.description}`) : '';
|
|
695
|
+
console.log(` ${cyan(sk.name)}${desc} ${dim(`[${sk.mode}]`)}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
console.log(dim('\nJalankan: kiosapi skill <nama>'));
|
|
699
|
+
console.log(dim('Buat baru: kiosapi skill buat <nama>'));
|
|
700
|
+
}
|
|
486
701
|
const ALL_COMMANDS = [
|
|
487
702
|
'masuk',
|
|
488
703
|
'keluar',
|
|
@@ -505,6 +720,9 @@ const ALL_COMMANDS = [
|
|
|
505
720
|
'lihat',
|
|
506
721
|
'lanjut',
|
|
507
722
|
'init',
|
|
723
|
+
'skill',
|
|
724
|
+
'skills',
|
|
725
|
+
'mcp',
|
|
508
726
|
'completion',
|
|
509
727
|
// English aliases
|
|
510
728
|
'login',
|
|
@@ -637,6 +855,8 @@ export async function cmdTim(args) {
|
|
|
637
855
|
const task = positionals.join(' ').trim();
|
|
638
856
|
if (!task)
|
|
639
857
|
throw new Error('Beri tugas. Contoh: kiosapi tim "bikin endpoint /health + tes"');
|
|
858
|
+
// Expand @file mentions so users can pass: kiosapi tim "@blueprint.md"
|
|
859
|
+
const expandedTask = await expandAtMentions(task);
|
|
640
860
|
const base = await resolveModel(values.model);
|
|
641
861
|
const models = {
|
|
642
862
|
perencana: values.perencana ?? base,
|
|
@@ -645,7 +865,7 @@ export async function cmdTim(args) {
|
|
|
645
865
|
};
|
|
646
866
|
for (const m of new Set(Object.values(models)))
|
|
647
867
|
await warnIfNoTools(m);
|
|
648
|
-
await runTeam(
|
|
868
|
+
await runTeam(expandedTask, { models, otomatis: Boolean(values.otomatis) });
|
|
649
869
|
}
|
|
650
870
|
/** saldo — show own balance, bonus tokens, and month-to-date spend. */
|
|
651
871
|
export async function cmdSaldo() {
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
const DEFAULT_BASE_URL = 'https://api.kiosapi.id';
|
|
@@ -6,6 +6,13 @@ export const DIR = join(homedir(), '.kiosapi');
|
|
|
6
6
|
export const CONFIG_PATH = join(DIR, 'config.json');
|
|
7
7
|
export const CHECKPOINT_PATH = join(DIR, 'checkpoint.json');
|
|
8
8
|
export const TIM_DIR = join(DIR, 'tim');
|
|
9
|
+
export const TIM_CHECKPOINT_PATH = join(DIR, 'tim-checkpoint.json');
|
|
10
|
+
export const SKILLS_GLOBAL_DIR = join(DIR, 'skills');
|
|
11
|
+
const MCP_CONFIG_PATH = join(DIR, 'mcp.json');
|
|
12
|
+
/** Returns the project-level skills directory (.kiosapi/skills/ under cwd). */
|
|
13
|
+
export function projectSkillsDir() {
|
|
14
|
+
return join(process.cwd(), '.kiosapi', 'skills');
|
|
15
|
+
}
|
|
9
16
|
/** Read only the on-disk config (ignores env overrides) — used before writing. */
|
|
10
17
|
function readFile() {
|
|
11
18
|
if (!existsSync(CONFIG_PATH))
|
|
@@ -96,6 +103,71 @@ export function initProjectConfig(config) {
|
|
|
96
103
|
writeFileSync(p, `${JSON.stringify(config, null, 2)}\n`);
|
|
97
104
|
return p;
|
|
98
105
|
}
|
|
106
|
+
export function saveTimCheckpoint(data) {
|
|
107
|
+
mkdirSync(DIR, { recursive: true });
|
|
108
|
+
const full = {
|
|
109
|
+
type: 'tim',
|
|
110
|
+
...data,
|
|
111
|
+
savedAt: new Date().toISOString(),
|
|
112
|
+
cwd: process.cwd(),
|
|
113
|
+
};
|
|
114
|
+
writeFileSync(TIM_CHECKPOINT_PATH, `${JSON.stringify(full, null, 2)}\n`);
|
|
115
|
+
}
|
|
116
|
+
export function loadTimCheckpoint() {
|
|
117
|
+
if (!existsSync(TIM_CHECKPOINT_PATH))
|
|
118
|
+
return null;
|
|
119
|
+
try {
|
|
120
|
+
const data = JSON.parse(readFileSync(TIM_CHECKPOINT_PATH, 'utf8'));
|
|
121
|
+
if (data.type !== 'tim')
|
|
122
|
+
return null;
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function clearTimCheckpoint() {
|
|
130
|
+
try {
|
|
131
|
+
unlinkSync(TIM_CHECKPOINT_PATH);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// already gone
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Load all MCP server configs: global (~/.kiosapi/mcp.json) merged with project (kiosapi.json).
|
|
139
|
+
* Project entries override global ones on name collision.
|
|
140
|
+
*/
|
|
141
|
+
export function loadMcpServers() {
|
|
142
|
+
let global = {};
|
|
143
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
144
|
+
try {
|
|
145
|
+
const data = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf8'));
|
|
146
|
+
global = data.mcpServers ?? {};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* corrupt file — ignore */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const project = loadProjectConfig()?.mcpServers ?? {};
|
|
153
|
+
return { ...global, ...project };
|
|
154
|
+
}
|
|
155
|
+
/** Write the global MCP config file. */
|
|
156
|
+
export function saveMcpConfig(servers) {
|
|
157
|
+
mkdirSync(DIR, { recursive: true });
|
|
158
|
+
writeFileSync(MCP_CONFIG_PATH, `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`);
|
|
159
|
+
}
|
|
160
|
+
/** Read the raw global MCP config (for display / editing). */
|
|
161
|
+
export function loadRawMcpConfig() {
|
|
162
|
+
if (!existsSync(MCP_CONFIG_PATH))
|
|
163
|
+
return { mcpServers: {} };
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf8'));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { mcpServers: {} };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
99
171
|
/** List all saved team names. */
|
|
100
172
|
export function listTeamConfigs() {
|
|
101
173
|
if (!existsSync(TIM_DIR))
|
package/dist/help.js
CHANGED
|
@@ -47,6 +47,20 @@ ${bold('Agen (butuh model 🔧):')}
|
|
|
47
47
|
init Buat kiosapi.json (config per-project: model, mode, otomatis)
|
|
48
48
|
${dim('Opsi: -m <model>, --otomatis (lewati konfirmasi). Mendukung pipe stdin.')}
|
|
49
49
|
|
|
50
|
+
${bold('Skills (workflow tersimpan):')}
|
|
51
|
+
skill buat <nama> Buat skill baru di project (.kiosapi/skills/)
|
|
52
|
+
skill buat --global <n> Buat skill global (~/.kiosapi/skills/)
|
|
53
|
+
skill <nama> [konteks] Jalankan skill; konteks opsional ditambahkan ke prompt
|
|
54
|
+
skills Lihat semua skill tersimpan (project + global)
|
|
55
|
+
${dim('Dalam sesi: /skills · /skill <nama> [konteks]')}
|
|
56
|
+
|
|
57
|
+
${bold('MCP (Model Context Protocol):')}
|
|
58
|
+
mcp Lihat server MCP dikonfigurasi
|
|
59
|
+
mcp sambung Test koneksi + lihat tools tersedia
|
|
60
|
+
mcp init Buat ~/.kiosapi/mcp.json dengan contoh konfigurasi
|
|
61
|
+
${dim('Config: ~/.kiosapi/mcp.json (global) atau kiosapi.json → mcpServers (per-project)')}
|
|
62
|
+
${dim('Tools MCP otomatis tersedia di semua sesi agen dan tim mode.')}
|
|
63
|
+
|
|
50
64
|
${bold('Tim multi-agen:')}
|
|
51
65
|
tim "…" Jalankan tim bawaan (perencana → pengkode → peninjau)
|
|
52
66
|
tim --pakai <nama> "…" Jalankan tim kustom tersimpan
|
|
@@ -72,6 +86,11 @@ ${bold('Contoh:')}
|
|
|
72
86
|
kiosapi lanjut
|
|
73
87
|
kiosapi tim --buat fullstack-team
|
|
74
88
|
kiosapi tim --pakai fullstack-team "bikin halaman login"
|
|
89
|
+
kiosapi skill buat fix-tests
|
|
90
|
+
kiosapi skill fix-tests
|
|
91
|
+
kiosapi skills
|
|
92
|
+
kiosapi mcp init
|
|
93
|
+
kiosapi mcp sambung
|
|
75
94
|
kiosapi completion bash >> ~/.bashrc
|
|
76
95
|
|
|
77
96
|
${dim('Sesi interaktif: /undo batalkan giliran terakhir · /ringkas padatkan riwayat · /model ganti model.')}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cmdBuat, cmdCompletion, cmdEdit, cmdGambar, cmdInit, cmdIsi, cmdKeluar, cmdLihat, cmdMasuk, cmdModel, cmdNgobrol, cmdPakai, cmdPerbarui, cmdPeriksa, cmdRencana, cmdSaldo, cmdSambung, cmdSetel, cmdTanya, cmdTim, cmdVideo, maybeNotifyUpdate, } from './commands.js';
|
|
2
|
+
import { cmdBuat, cmdCompletion, cmdEdit, cmdGambar, cmdInit, cmdIsi, cmdKeluar, cmdLihat, cmdMasuk, cmdMcp, cmdModel, cmdNgobrol, cmdPakai, cmdPerbarui, cmdPeriksa, cmdRencana, cmdSaldo, cmdSambung, cmdSetel, cmdSkill, cmdSkills, cmdTanya, cmdTim, cmdVideo, maybeNotifyUpdate, } from './commands.js';
|
|
3
3
|
import { printHelp, printVersion } from './help.js';
|
|
4
4
|
import { resumeFromCheckpoint, startSession } from './session.js';
|
|
5
5
|
import { red } from './ui.js';
|
|
@@ -44,6 +44,9 @@ const COMMANDS = {
|
|
|
44
44
|
lanjut: resumeFromCheckpoint,
|
|
45
45
|
resume: resumeFromCheckpoint,
|
|
46
46
|
init: cmdInit,
|
|
47
|
+
skill: cmdSkill,
|
|
48
|
+
skills: cmdSkills,
|
|
49
|
+
mcp: cmdMcp,
|
|
47
50
|
completion: cmdCompletion,
|
|
48
51
|
};
|
|
49
52
|
async function main() {
|