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.
@@ -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. Telusuri kode seperlunya lalu buat rencana langkah kecil & terstruktur.
37
+ const PLANNER_BRIEF = `Peranmu: PERENCANA. Baca SELURUH spesifikasi terlebih dahulu.
37
38
 
38
- WAJIB: panggil tool "selesai" dengan parameter ringkasan berisi JSON dalam format TEPAT ini (tidak ada teks lain sebelum/sesudah JSON):
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, bisa selesai dalam ~15 tool calls.
43
- - Maksimal 10 langkah pecah yang kompleks, gabung yang trivial.
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
- export async function runTeam(task, opts) {
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
- // Planner
50
- console.log(`\n${bold('① Perencana')} ${dim(`(${opts.models.perencana})`)}`);
51
- const plannerSession = newSession(opts.models.perencana, 'rencana', opts.otomatis);
52
- plannerSession.messages.push({ role: 'system', content: PLANNER_BRIEF });
53
- const planOutput = await runTurn(plannerSession, task);
54
- const plan = tryParsePlan(planOutput);
55
- if (!plan) {
56
- console.log(yellow('⚠ Perencana tidak menghasilkan rencana terstruktur jalankan sebagai sesi pengkode tunggal.'));
57
- const s = newSession(opts.models.pengkode, 'buat', opts.otomatis);
58
- s.messages.push({
59
- role: 'system',
60
- content: 'Peranmu: PENGKODE. Implementasikan tugas mengikuti rencana. Buat perubahan kecil & jelas, lalu panggil selesai.',
61
- });
62
- await runTurn(s, `Tugas: ${task}\n\nRencana:\n${planOutput || '(tidak ada rencana eksplisit)'}`);
63
- console.log(`\n${bold('③ Peninjau')} ${dim(`(${opts.models.peninjau})`)}`);
64
- const rev = newSession(opts.models.peninjau, 'rencana', opts.otomatis);
65
- rev.messages.push({ role: 'system', content: REVIEWER_BRIEF });
66
- await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}`);
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
- // Show plan
71
- console.log(bold(`\n📋 ${plan.ringkasan}`));
72
- for (const [i, step] of plan.langkah.entries()) {
73
- console.log(` ${dim(`${i + 1}.`)} ${step.tugas}`);
74
- console.log(dim(` 📄 ${step.file.join(', ')}`));
75
- if (step.catatan)
76
- console.log(dim(` 💬 ${step.catatan}`));
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.messages.push({
86
- role: 'system',
87
- content: [
88
- `Peranmu: PENGKODE — langkah ${label} dari rencana keseluruhan.`,
89
- `TUGAS: ${step.tugas}`,
90
- `File relevan: ${step.file.join(', ')}`,
91
- step.catatan ? `Catatan: ${step.catatan}` : '',
92
- 'Fokus HANYA pada tugas ini. Baca file relevan, buat perubahan, panggil selesai.',
93
- 'Jangan baca file lain kecuali diperlukan langsung untuk memahami konteks.',
94
- ]
95
- .filter(Boolean)
96
- .join('\n'),
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
- await runTurn(s, stepPrompt);
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 — pass the plan so it knows which files were touched
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.messages.push({ role: 'system', content: REVIEWER_BRIEF });
114
- await runTurn(rev, `Tinjau hasil implementasi untuk tugas: ${task}\n\nLangkah yang sudah dikerjakan:\n${stepSummary}`);
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.messages.push({ role: 'system', content: roleCfg.brief });
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(task, { models, otomatis: Boolean(values.otomatis) });
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() {