kiosapi 0.1.28 → 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 +79 -5
- 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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client — stdio transport only.
|
|
3
|
+
*
|
|
4
|
+
* Protocol: newline-delimited JSON-RPC 2.0 over child process stdin/stdout.
|
|
5
|
+
* No external SDK needed — the protocol is simple enough to implement directly.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { VERSION } from '../help.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Single server connection
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
class McpConnection {
|
|
13
|
+
serverName;
|
|
14
|
+
proc;
|
|
15
|
+
buffer = '';
|
|
16
|
+
pending = new Map();
|
|
17
|
+
nextId = 1;
|
|
18
|
+
dead = false;
|
|
19
|
+
constructor(serverName, config) {
|
|
20
|
+
this.serverName = serverName;
|
|
21
|
+
const env = { ...process.env, ...(config.env ?? {}) };
|
|
22
|
+
// On Windows, npx/npm commands need shell:true to resolve correctly
|
|
23
|
+
const needsShell = process.platform === 'win32' &&
|
|
24
|
+
(config.command === 'npx' || config.command === 'npm' || config.command === 'node');
|
|
25
|
+
this.proc = spawn(config.command, config.args ?? [], {
|
|
26
|
+
env,
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'], // capture stderr so it doesn't pollute CLI output
|
|
28
|
+
shell: needsShell,
|
|
29
|
+
});
|
|
30
|
+
this.proc.stdout?.setEncoding('utf8');
|
|
31
|
+
this.proc.stdout?.on('data', (chunk) => {
|
|
32
|
+
this.buffer += chunk;
|
|
33
|
+
const lines = this.buffer.split('\n');
|
|
34
|
+
this.buffer = lines.pop() ?? '';
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (!line.trim())
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
const msg = JSON.parse(line);
|
|
40
|
+
if (msg.id !== undefined) {
|
|
41
|
+
const p = this.pending.get(msg.id);
|
|
42
|
+
if (p) {
|
|
43
|
+
clearTimeout(p.timer);
|
|
44
|
+
this.pending.delete(msg.id);
|
|
45
|
+
if (msg.error) {
|
|
46
|
+
p.reject(new Error(msg.error.message));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
p.resolve(msg.result);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// non-JSON line (debug output, etc.) — ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const onExit = () => {
|
|
60
|
+
this.dead = true;
|
|
61
|
+
for (const p of this.pending.values()) {
|
|
62
|
+
clearTimeout(p.timer);
|
|
63
|
+
p.reject(new Error(`MCP server "${this.serverName}" exited unexpectedly`));
|
|
64
|
+
}
|
|
65
|
+
this.pending.clear();
|
|
66
|
+
};
|
|
67
|
+
this.proc.on('exit', onExit);
|
|
68
|
+
this.proc.on('error', onExit);
|
|
69
|
+
}
|
|
70
|
+
write(msg) {
|
|
71
|
+
this.proc.stdin?.write(`${JSON.stringify(msg)}\n`);
|
|
72
|
+
}
|
|
73
|
+
request(method, params, timeoutMs = 30_000) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (this.dead) {
|
|
76
|
+
reject(new Error(`MCP server "${this.serverName}" is not running`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const id = this.nextId++;
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.pending.delete(id);
|
|
82
|
+
reject(new Error(`MCP "${this.serverName}" timeout after ${timeoutMs}ms`));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
this.pending.set(id, { resolve: (r) => resolve(r), reject, timer });
|
|
85
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async initialize() {
|
|
89
|
+
await this.request('initialize', {
|
|
90
|
+
protocolVersion: '2024-11-05',
|
|
91
|
+
clientInfo: { name: 'kiosapi', version: VERSION },
|
|
92
|
+
capabilities: { tools: {} },
|
|
93
|
+
}, 10_000);
|
|
94
|
+
// Notification — no response expected
|
|
95
|
+
this.write({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
96
|
+
}
|
|
97
|
+
async listTools() {
|
|
98
|
+
const result = await this.request('tools/list', {}, 10_000);
|
|
99
|
+
return result.tools ?? [];
|
|
100
|
+
}
|
|
101
|
+
async callTool(name, args) {
|
|
102
|
+
const result = await this.request('tools/call', { name, arguments: args }, 60_000);
|
|
103
|
+
const content = result.content ?? [];
|
|
104
|
+
const text = content
|
|
105
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
106
|
+
.map((c) => c.text ?? '')
|
|
107
|
+
.join('\n');
|
|
108
|
+
if (result.isError)
|
|
109
|
+
throw new Error(`MCP tool error: ${text}`);
|
|
110
|
+
return text || '(no output)';
|
|
111
|
+
}
|
|
112
|
+
kill() {
|
|
113
|
+
this.dead = true;
|
|
114
|
+
try {
|
|
115
|
+
this.proc.kill();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// already dead
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
get isAlive() {
|
|
122
|
+
return !this.dead;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Manager: multiple servers, unified tool registry
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
function sanitizeName(s) {
|
|
129
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
130
|
+
}
|
|
131
|
+
export class McpManager {
|
|
132
|
+
connections = new Map();
|
|
133
|
+
registry = [];
|
|
134
|
+
/**
|
|
135
|
+
* Connect to all configured servers in parallel. Failures are logged but do not abort — a
|
|
136
|
+
* partially-connected MCP set is better than no MCP at all.
|
|
137
|
+
*/
|
|
138
|
+
async connect(servers, onWarn) {
|
|
139
|
+
const entries = Object.entries(servers);
|
|
140
|
+
const results = await Promise.allSettled(entries.map(async ([name, config]) => {
|
|
141
|
+
const conn = new McpConnection(name, config);
|
|
142
|
+
await conn.initialize();
|
|
143
|
+
const tools = await conn.listTools();
|
|
144
|
+
this.connections.set(name, conn);
|
|
145
|
+
for (const tool of tools) {
|
|
146
|
+
const qualifiedName = `mcp__${sanitizeName(name)}__${sanitizeName(tool.name)}`;
|
|
147
|
+
this.registry.push({
|
|
148
|
+
qualifiedName,
|
|
149
|
+
serverName: name,
|
|
150
|
+
toolName: tool.name,
|
|
151
|
+
spec: {
|
|
152
|
+
type: 'function',
|
|
153
|
+
function: {
|
|
154
|
+
name: qualifiedName,
|
|
155
|
+
description: `[MCP:${name}] ${tool.description ?? tool.name}`,
|
|
156
|
+
parameters: tool.inputSchema,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
for (const [i, result] of results.entries()) {
|
|
163
|
+
if (result.status === 'rejected') {
|
|
164
|
+
const serverName = entries[i]?.[0] ?? '?';
|
|
165
|
+
onWarn?.(`⚠ MCP server "${serverName}" gagal: ${result.reason.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** ToolSpec[] for all connected MCP tools — merged into the agent's tool list. */
|
|
170
|
+
getToolSpecs() {
|
|
171
|
+
return this.registry.map((r) => r.spec);
|
|
172
|
+
}
|
|
173
|
+
/** Dispatch a tool call by qualified name. */
|
|
174
|
+
async callTool(qualifiedName, args) {
|
|
175
|
+
const entry = this.registry.find((r) => r.qualifiedName === qualifiedName);
|
|
176
|
+
if (!entry)
|
|
177
|
+
throw new Error(`MCP tool "${qualifiedName}" tidak dikenal`);
|
|
178
|
+
const conn = this.connections.get(entry.serverName);
|
|
179
|
+
if (!conn?.isAlive)
|
|
180
|
+
throw new Error(`MCP server "${entry.serverName}" tidak aktif`);
|
|
181
|
+
return conn.callTool(entry.toolName, args);
|
|
182
|
+
}
|
|
183
|
+
get toolCount() {
|
|
184
|
+
return this.registry.length;
|
|
185
|
+
}
|
|
186
|
+
get serverCount() {
|
|
187
|
+
return this.connections.size;
|
|
188
|
+
}
|
|
189
|
+
/** Summary of servers and their tool counts — for display. */
|
|
190
|
+
summary() {
|
|
191
|
+
const byServer = new Map();
|
|
192
|
+
for (const r of this.registry) {
|
|
193
|
+
if (!byServer.has(r.serverName))
|
|
194
|
+
byServer.set(r.serverName, []);
|
|
195
|
+
byServer.get(r.serverName).push(r);
|
|
196
|
+
}
|
|
197
|
+
return [...byServer.entries()].map(([server, tools]) => ({ server, tools }));
|
|
198
|
+
}
|
|
199
|
+
disconnect() {
|
|
200
|
+
for (const conn of this.connections.values())
|
|
201
|
+
conn.kill();
|
|
202
|
+
this.connections.clear();
|
|
203
|
+
this.registry = [];
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/agent/run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { extname, join } from 'node:path';
|
|
3
3
|
import { chatComplete, generateImage, resolveMediaModel, streamVision, } from '../api.js';
|
|
4
|
-
import { CHECKPOINT_PATH } from '../config.js';
|
|
4
|
+
import { CHECKPOINT_PATH, loadMcpServers } from '../config.js';
|
|
5
5
|
import { cyan, dim, green, idn, prompt, red, rupiah, thinking, yellow } from '../ui.js';
|
|
6
|
+
import { McpManager } from './mcp.js';
|
|
6
7
|
import { systemPrompt, toolsForMode } from './schemas.js';
|
|
7
8
|
import { bacaFile, cari, daftarFile, editFile, hapusFile, jalankan, pindahFile, safePath, tulisFile, } from './tools.js';
|
|
8
9
|
const MAX_STEPS = 50;
|
|
@@ -61,6 +62,7 @@ function trimContext(messages) {
|
|
|
61
62
|
const droppedHead = clean.slice(0, clean.length - tail.length + skip);
|
|
62
63
|
const droppedPaths = [];
|
|
63
64
|
const droppedDirs = [];
|
|
65
|
+
const droppedModified = [];
|
|
64
66
|
for (const msg of droppedHead) {
|
|
65
67
|
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
66
68
|
for (const tc of msg.tool_calls) {
|
|
@@ -76,6 +78,13 @@ function trimContext(messages) {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
// Track files that were successfully written/edited/deleted in the dropped context so the
|
|
82
|
+
// model knows it already made those changes and shouldn't repeat them after trimming.
|
|
83
|
+
if (msg.role === 'tool' && msg.content) {
|
|
84
|
+
const m = msg.content.match(/^OK:\s+(.+?)\s+(dibuat|diubah|dihapus|dipindah)/);
|
|
85
|
+
if (m?.[1])
|
|
86
|
+
droppedModified.push(m[1]);
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
const filesNote = droppedPaths.length > 0
|
|
81
90
|
? ` File sudah dibaca: ${[...new Set(droppedPaths)].join(', ')}. JANGAN baca ulang — gunakan cari untuk teks spesifik.`
|
|
@@ -83,9 +92,12 @@ function trimContext(messages) {
|
|
|
83
92
|
const dirsNote = droppedDirs.length > 0
|
|
84
93
|
? ` Direktori sudah terdaftar: ${[...new Set(droppedDirs)].join(', ')}. JANGAN list ulang — gunakan baca_file untuk file spesifik.`
|
|
85
94
|
: '';
|
|
95
|
+
const modifiedNote = droppedModified.length > 0
|
|
96
|
+
? ` File sudah dimodifikasi: ${[...new Set(droppedModified)].join(', ')}. JANGAN tulis ulang kecuali ada perubahan tambahan.`
|
|
97
|
+
: '';
|
|
86
98
|
const note = {
|
|
87
99
|
role: 'system',
|
|
88
|
-
content: `[Kiosapi: ${dropped} pesan awal dihapus dari konteks.${filesNote}${dirsNote}]`,
|
|
100
|
+
content: `[Kiosapi: ${dropped} pesan awal dihapus dari konteks.${filesNote}${dirsNote}${modifiedNote}]`,
|
|
89
101
|
};
|
|
90
102
|
return [system, note, ...tail];
|
|
91
103
|
}
|
|
@@ -476,7 +488,8 @@ export async function runTurn(s, userText) {
|
|
|
476
488
|
}
|
|
477
489
|
}
|
|
478
490
|
s.messages.push({ role: 'user', content: userText });
|
|
479
|
-
|
|
491
|
+
s._lastCompleted = false;
|
|
492
|
+
const tools = [...toolsForMode(s.mode), ...(s.mcpManager?.getToolSpecs() ?? [])];
|
|
480
493
|
let lastText = '';
|
|
481
494
|
let totalIn = 0;
|
|
482
495
|
let totalOut = 0;
|
|
@@ -572,7 +585,14 @@ export async function runTurn(s, userText) {
|
|
|
572
585
|
const total = callCounts.get(loopSig) ?? 0;
|
|
573
586
|
console.log(red(`\n⚠ Loop terdeteksi: "${loopName}" dipanggil ${total}× — agen terjebak.`));
|
|
574
587
|
console.log(yellow('Berikan instruksi baru, atau ketik /bersih untuk mulai ulang.'));
|
|
575
|
-
const loopMsg = `⚠ LOOP: "${loopName}" sudah dipanggil ${total}×
|
|
588
|
+
const loopMsg = `⚠ LOOP TERDETEKSI: "${loopName}" sudah dipanggil ${total}× — BERHENTI SEKARANG.
|
|
589
|
+
|
|
590
|
+
Panggil tool "selesai" dengan ringkasan yang berisi:
|
|
591
|
+
1. Apa yang SUDAH berhasil dikerjakan (file diubah, perintah dijalankan)
|
|
592
|
+
2. Apa yang BELUM selesai dan kendala yang ditemui
|
|
593
|
+
3. Saran langkah berikutnya untuk pengguna
|
|
594
|
+
|
|
595
|
+
JANGAN panggil tool lain selain "selesai".`;
|
|
576
596
|
for (const call of calls) {
|
|
577
597
|
s.messages.push({ role: 'tool', content: loopMsg, tool_call_id: call.id });
|
|
578
598
|
}
|
|
@@ -615,6 +635,45 @@ export async function runTurn(s, userText) {
|
|
|
615
635
|
s.messages.push({ role: 'tool', content: warn, tool_call_id: call.id });
|
|
616
636
|
continue;
|
|
617
637
|
}
|
|
638
|
+
// MCP tool dispatch — routed to the connected MCP server
|
|
639
|
+
if (call.function.name.startsWith('mcp__') && s.mcpManager) {
|
|
640
|
+
let mcpArgs = {};
|
|
641
|
+
try {
|
|
642
|
+
mcpArgs = JSON.parse(call.function.arguments || '{}');
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
s.messages.push({
|
|
646
|
+
role: 'tool',
|
|
647
|
+
content: 'Error: argumen bukan JSON valid.',
|
|
648
|
+
tool_call_id: call.id,
|
|
649
|
+
});
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const [, serverPart, ...toolParts] = call.function.name.split('__');
|
|
653
|
+
console.log(dim(` mcp:${serverPart} → ${toolParts.join('__')}`));
|
|
654
|
+
if (!(await allow(s.otomatis, `Jalankan MCP tool "${call.function.name}"?`))) {
|
|
655
|
+
s.messages.push({
|
|
656
|
+
role: 'tool',
|
|
657
|
+
content: 'Ditolak oleh pengguna.',
|
|
658
|
+
tool_call_id: call.id,
|
|
659
|
+
});
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
let mcpOut;
|
|
663
|
+
try {
|
|
664
|
+
mcpOut = await s.mcpManager.callTool(call.function.name, mcpArgs);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
mcpOut = `Error MCP: ${err instanceof Error ? err.message : String(err)}`;
|
|
668
|
+
}
|
|
669
|
+
const MAX_STORED_RESULT_MCP = 10_000;
|
|
670
|
+
const stored = mcpOut.length > MAX_STORED_RESULT_MCP
|
|
671
|
+
? `${mcpOut.slice(0, MAX_STORED_RESULT_MCP)}\n…[dipotong]`
|
|
672
|
+
: mcpOut;
|
|
673
|
+
s.messages.push({ role: 'tool', content: stored, tool_call_id: call.id });
|
|
674
|
+
saveCheckpoint(s);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
618
677
|
const result = await runTool(call, s.otomatis, s.model);
|
|
619
678
|
// Cap what goes into the conversation history: full output can be 300 lines of directory
|
|
620
679
|
// listing or 100 KB of file content, which quickly overflows small-context-window models
|
|
@@ -644,6 +703,7 @@ export async function runTurn(s, userText) {
|
|
|
644
703
|
console.log(green(`\n✓ Selesai: ${result.output}`));
|
|
645
704
|
showUsage();
|
|
646
705
|
s.totalTokens += totalIn + totalOut;
|
|
706
|
+
s._lastCompleted = true;
|
|
647
707
|
clearCheckpoint();
|
|
648
708
|
return result.output;
|
|
649
709
|
}
|
|
@@ -662,5 +722,19 @@ export async function runTurn(s, userText) {
|
|
|
662
722
|
}
|
|
663
723
|
/** One-shot: run a single task in a mode (used by the `rencana`/`edit`/`buat` subcommands). */
|
|
664
724
|
export async function runAgent(task, mode, opts) {
|
|
665
|
-
|
|
725
|
+
const s = newSession(opts.model, mode, opts.otomatis);
|
|
726
|
+
const mcpServers = loadMcpServers();
|
|
727
|
+
let mcpManager;
|
|
728
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
729
|
+
mcpManager = new McpManager();
|
|
730
|
+
await mcpManager.connect(mcpServers);
|
|
731
|
+
if (mcpManager.toolCount > 0)
|
|
732
|
+
s.mcpManager = mcpManager;
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
await runTurn(s, task);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
mcpManager?.disconnect();
|
|
739
|
+
}
|
|
666
740
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { SKILLS_GLOBAL_DIR, projectSkillsDir } from '../config.js';
|
|
4
|
+
function parseFrontmatter(content) {
|
|
5
|
+
if (!content.startsWith('---\n'))
|
|
6
|
+
return { meta: {}, body: content.trim() };
|
|
7
|
+
const end = content.indexOf('\n---\n', 4);
|
|
8
|
+
if (end === -1)
|
|
9
|
+
return { meta: {}, body: content.trim() };
|
|
10
|
+
const raw = content.slice(4, end);
|
|
11
|
+
const meta = {};
|
|
12
|
+
for (const line of raw.split('\n')) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (trimmed.startsWith('#'))
|
|
15
|
+
continue;
|
|
16
|
+
const match = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
17
|
+
if (match?.[1])
|
|
18
|
+
meta[match[1]] = match[2]?.trim() ?? '';
|
|
19
|
+
}
|
|
20
|
+
return { meta, body: content.slice(end + 5).trim() };
|
|
21
|
+
}
|
|
22
|
+
function readSkillFile(filePath, source) {
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(filePath, 'utf8');
|
|
25
|
+
const { meta, body } = parseFrontmatter(content);
|
|
26
|
+
const name = filePath.split(/[/\\]/).pop()?.replace(/\.md$/, '') ?? '';
|
|
27
|
+
if (!name || !body)
|
|
28
|
+
return null;
|
|
29
|
+
const modeRaw = meta.mode ?? 'buat';
|
|
30
|
+
const mode = modeRaw === 'rencana' || modeRaw === 'edit' ? modeRaw : 'buat';
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
description: meta.description ?? '',
|
|
34
|
+
prompt: body,
|
|
35
|
+
mode,
|
|
36
|
+
otomatis: meta.otomatis === 'true',
|
|
37
|
+
model: meta.model || undefined,
|
|
38
|
+
source,
|
|
39
|
+
filePath,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Load a skill by name: project (.kiosapi/skills/) overrides global (~/.kiosapi/skills/). */
|
|
47
|
+
export function loadSkill(name) {
|
|
48
|
+
const projectPath = join(projectSkillsDir(), `${name}.md`);
|
|
49
|
+
if (existsSync(projectPath))
|
|
50
|
+
return readSkillFile(projectPath, 'project');
|
|
51
|
+
const globalPath = join(SKILLS_GLOBAL_DIR, `${name}.md`);
|
|
52
|
+
if (existsSync(globalPath))
|
|
53
|
+
return readSkillFile(globalPath, 'global');
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** List all available skills (project overrides global on name collision). */
|
|
57
|
+
export function listSkills() {
|
|
58
|
+
const skills = new Map();
|
|
59
|
+
// Load global first so project can override
|
|
60
|
+
for (const { dir, source } of [
|
|
61
|
+
{ dir: SKILLS_GLOBAL_DIR, source: 'global' },
|
|
62
|
+
{ dir: projectSkillsDir(), source: 'project' },
|
|
63
|
+
]) {
|
|
64
|
+
if (!existsSync(dir))
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
for (const file of readdirSync(dir)) {
|
|
68
|
+
if (!file.endsWith('.md'))
|
|
69
|
+
continue;
|
|
70
|
+
const sk = readSkillFile(join(dir, file), source);
|
|
71
|
+
if (sk)
|
|
72
|
+
skills.set(sk.name, sk);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* unreadable dir — ignore */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...skills.values()].sort((a, b) => {
|
|
80
|
+
if (a.source !== b.source)
|
|
81
|
+
return a.source === 'project' ? -1 : 1;
|
|
82
|
+
return a.name.localeCompare(b.name);
|
|
83
|
+
});
|
|
84
|
+
}
|
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() {
|
package/dist/session.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { McpManager } from './agent/mcp.js';
|
|
2
3
|
import { clearCheckpoint, loadCheckpoint, newSession, resetSession, runTurn, undoLastTurn, } from './agent/run.js';
|
|
4
|
+
import { listSkills, loadSkill } from './agent/skills.js';
|
|
3
5
|
import { runCustomTeam, runTeam } from './agent/team.js';
|
|
4
6
|
import { expandAtMentions } from './agent/tools.js';
|
|
5
7
|
import { resolveModel, streamChat } from './api.js';
|
|
6
|
-
import { cmdGambar, cmdIsi, cmdLihat, cmdMasuk, cmdPakai, cmdPerbarui, cmdSaldo, cmdVideo, maybeNotifyUpdate, pickModel, warnIfNoTools, } from './commands.js';
|
|
7
|
-
import { listTeamConfigs, loadConfig, loadProjectConfig, loadTeamConfig, saveTeamConfig, } from './config.js';
|
|
8
|
+
import { cmdGambar, cmdIsi, cmdLihat, cmdMasuk, cmdPakai, cmdPerbarui, cmdSaldo, cmdSkill, cmdVideo, maybeNotifyUpdate, pickModel, warnIfNoTools, } from './commands.js';
|
|
9
|
+
import { clearTimCheckpoint, listTeamConfigs, loadConfig, loadMcpServers, loadProjectConfig, loadTeamConfig, loadTimCheckpoint, saveTeamConfig, } from './config.js';
|
|
8
10
|
import { bold, cyan, dim, green, idn, prompt, red, thinking, yellow } from './ui.js';
|
|
9
11
|
const MODES = ['rencana', 'edit', 'buat'];
|
|
10
12
|
/** The prompt indicator shows the active mode, turn count, accumulated tokens, and ⚡ for auto. */
|
|
@@ -23,6 +25,8 @@ ${dim('Ketik tugasmu langsung. Perintah meta diawali "/". /bantuan untuk daftar,
|
|
|
23
25
|
}
|
|
24
26
|
function slashHelp() {
|
|
25
27
|
console.log(`${bold('Perintah sesi:')}
|
|
28
|
+
/skills Lihat semua skill tersimpan
|
|
29
|
+
/skill <nama> [konteks] Jalankan skill (project atau global)
|
|
26
30
|
/tim <tugas> Multi-agen bawaan (perencana→pengkode→peninjau)
|
|
27
31
|
/tim --pakai <nama> <tugas> Jalankan tim kustom tersimpan
|
|
28
32
|
/peran [peran] [model] Atur model per peran tim (atau lihat/reset)
|
|
@@ -65,6 +69,64 @@ async function runSlash(line, s) {
|
|
|
65
69
|
case '?':
|
|
66
70
|
slashHelp();
|
|
67
71
|
return false;
|
|
72
|
+
case 'skills':
|
|
73
|
+
case 'skill-daftar': {
|
|
74
|
+
const all = listSkills();
|
|
75
|
+
if (all.length === 0) {
|
|
76
|
+
console.log(dim('Belum ada skill. Buat dengan: kiosapi skill buat <nama>'));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (const sk of all) {
|
|
80
|
+
const scopeTag = sk.source === 'project' ? '[project]' : '[global]';
|
|
81
|
+
const desc = sk.description ? ` — ${sk.description}` : '';
|
|
82
|
+
console.log(` ${cyan(sk.name)} ${dim(scopeTag)}${dim(desc)} ${dim(`[${sk.mode}]`)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
case 'skill': {
|
|
88
|
+
const skillName = rest[0];
|
|
89
|
+
if (!skillName) {
|
|
90
|
+
// No name → show list
|
|
91
|
+
const all = listSkills();
|
|
92
|
+
if (all.length === 0) {
|
|
93
|
+
console.log(dim('Belum ada skill. Buat dengan: kiosapi skill buat <nama>'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const sk of all) {
|
|
97
|
+
const desc = sk.description ? dim(` — ${sk.description}`) : '';
|
|
98
|
+
console.log(` ${cyan(sk.name)}${desc} ${dim(`[${sk.mode}]`)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const sk = loadSkill(skillName);
|
|
104
|
+
if (!sk) {
|
|
105
|
+
console.log(red(`Skill "${skillName}" tidak ditemukan.`));
|
|
106
|
+
console.log(dim(' Buat: kiosapi skill buat <nama> | Lihat: /skills'));
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const extra = rest.slice(1).join(' ').trim();
|
|
110
|
+
const rawPrompt = extra ? `${sk.prompt}\n\n${extra}` : sk.prompt;
|
|
111
|
+
const expanded = await expandAtMentions(rawPrompt);
|
|
112
|
+
// Apply skill's model/mode/otomatis to the current session for this turn only
|
|
113
|
+
const prevModel = s.model;
|
|
114
|
+
const prevMode = s.mode;
|
|
115
|
+
const prevOto = s.otomatis;
|
|
116
|
+
if (sk.model)
|
|
117
|
+
s.model = sk.model;
|
|
118
|
+
s.mode = sk.mode;
|
|
119
|
+
if (sk.otomatis)
|
|
120
|
+
s.otomatis = true;
|
|
121
|
+
const scopeLabel = sk.source === 'project' ? dim(' [project]') : dim(' [global]');
|
|
122
|
+
console.log(`${bold(`▶ skill: ${sk.name}`)}${scopeLabel}`);
|
|
123
|
+
await runTurn(s, expanded);
|
|
124
|
+
// Restore session settings after skill run
|
|
125
|
+
s.model = prevModel;
|
|
126
|
+
s.mode = prevMode;
|
|
127
|
+
s.otomatis = prevOto;
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
68
130
|
case 'mode': {
|
|
69
131
|
const m = rest[0]?.toLowerCase();
|
|
70
132
|
if (!m) {
|
|
@@ -483,7 +545,22 @@ export async function startSession() {
|
|
|
483
545
|
}
|
|
484
546
|
banner(s);
|
|
485
547
|
await warnIfNoTools(model);
|
|
486
|
-
|
|
548
|
+
const mcpServers = loadMcpServers();
|
|
549
|
+
let mcpManager;
|
|
550
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
551
|
+
mcpManager = new McpManager();
|
|
552
|
+
await mcpManager.connect(mcpServers, (w) => console.log(yellow(w)));
|
|
553
|
+
if (mcpManager.toolCount > 0) {
|
|
554
|
+
console.log(dim(` MCP: ${mcpManager.toolCount} tool dari ${mcpManager.serverCount} server`));
|
|
555
|
+
}
|
|
556
|
+
s.mcpManager = mcpManager;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
await runSessionLoop(s);
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
mcpManager?.disconnect();
|
|
563
|
+
}
|
|
487
564
|
}
|
|
488
565
|
/**
|
|
489
566
|
* Resume a checkpointed session interactively: load the saved state, show a preamble, ask for a
|
|
@@ -492,6 +569,51 @@ export async function startSession() {
|
|
|
492
569
|
*/
|
|
493
570
|
export async function resumeFromCheckpoint(args) {
|
|
494
571
|
await maybeNotifyUpdate();
|
|
572
|
+
// Tim checkpoint takes priority — it stores multi-step pipeline progress
|
|
573
|
+
const timCp = loadTimCheckpoint();
|
|
574
|
+
if (timCp) {
|
|
575
|
+
console.log(bold('Sesi tim tersimpan ditemukan'));
|
|
576
|
+
console.log(` Tugas : ${timCp.task.slice(0, 80)}${timCp.task.length > 80 ? '…' : ''}`);
|
|
577
|
+
console.log(` Progress : ${timCp.completedStepCount}/${timCp.plan.langkah.length} langkah selesai`);
|
|
578
|
+
console.log(` Disimpan : ${new Date(timCp.savedAt).toLocaleString('id-ID')}`);
|
|
579
|
+
if (timCp.completedStepCount > 0) {
|
|
580
|
+
console.log(dim(' Langkah selesai:'));
|
|
581
|
+
for (const [i, step] of timCp.plan.langkah.entries()) {
|
|
582
|
+
if (i >= timCp.completedStepCount)
|
|
583
|
+
break;
|
|
584
|
+
console.log(dim(` ✓ ${i + 1}. ${step.tugas}`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const remaining = timCp.plan.langkah.length - timCp.completedStepCount;
|
|
588
|
+
console.log(`\n Sisa : ${remaining} langkah belum dikerjakan`);
|
|
589
|
+
if (timCp.cwd !== process.cwd()) {
|
|
590
|
+
console.log(yellow(`\n⚠ Sesi ini disimpan di direktori berbeda: ${timCp.cwd}`));
|
|
591
|
+
}
|
|
592
|
+
if (!loadConfig().apiKey) {
|
|
593
|
+
console.log(dim('Belum masuk — masukkan API key dulu.'));
|
|
594
|
+
await cmdMasuk();
|
|
595
|
+
if (!loadConfig().apiKey)
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const ans = (await prompt('Lanjutkan sesi tim ini? (y/t) ')).trim().toLowerCase();
|
|
599
|
+
if (ans === 'y' || ans === 'ya') {
|
|
600
|
+
const resume = {
|
|
601
|
+
startFromStep: timCp.completedStepCount,
|
|
602
|
+
stepResults: timCp.stepResults,
|
|
603
|
+
plan: timCp.plan,
|
|
604
|
+
};
|
|
605
|
+
await runTeam(timCp.task, { models: timCp.models, otomatis: timCp.otomatis }, resume);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
console.log(dim('Sesi tim dibatalkan.'));
|
|
609
|
+
const clearAns = (await prompt('Hapus checkpoint tim ini? (y/t) ')).trim().toLowerCase();
|
|
610
|
+
if (clearAns === 'y' || clearAns === 'ya') {
|
|
611
|
+
clearTimCheckpoint();
|
|
612
|
+
console.log(dim('Checkpoint tim dihapus.'));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
495
617
|
const cp = loadCheckpoint();
|
|
496
618
|
if (!cp) {
|
|
497
619
|
throw new Error('Tidak ada sesi tersimpan. Mulai dengan: kiosapi buat "tugas"\n' +
|