kiosapi 0.1.7 → 0.1.9
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/run.js +338 -16
- package/dist/agent/schemas.js +58 -4
- package/dist/agent/team.js +38 -1
- package/dist/agent/tools.js +186 -15
- package/dist/api.js +15 -2
- package/dist/commands.js +254 -8
- package/dist/config.js +61 -3
- package/dist/help.js +46 -28
- package/dist/index.js +18 -2
- package/dist/session.js +393 -55
- package/dist/ui.js +5 -0
- package/package.json +1 -1
package/dist/agent/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* Local tools the agent can run, all confined to the current working directory and screened by an
|
|
@@ -24,14 +24,14 @@ function ignorePatterns() {
|
|
|
24
24
|
return list;
|
|
25
25
|
}
|
|
26
26
|
/** A path is protected if any segment matches an ignore pattern, or it's a dotenv-style secret. */
|
|
27
|
-
function isProtected(rel, patterns) {
|
|
27
|
+
export function isProtected(rel, patterns) {
|
|
28
28
|
const segments = rel.split(/[\\/]/);
|
|
29
29
|
if (segments.some((s) => s === '.env' || s.startsWith('.env.')))
|
|
30
30
|
return true;
|
|
31
31
|
return patterns.some((p) => segments.includes(p) || rel === p || rel.startsWith(`${p}/`));
|
|
32
32
|
}
|
|
33
33
|
/** Resolve a user/model-supplied path, rejecting anything outside the working directory or protected. */
|
|
34
|
-
function safePath(p) {
|
|
34
|
+
export function safePath(p) {
|
|
35
35
|
const abs = resolve(ROOT, p);
|
|
36
36
|
const rel = relative(ROOT, abs);
|
|
37
37
|
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
@@ -132,18 +132,189 @@ export function editFile(path, cari, ganti) {
|
|
|
132
132
|
writeFileSync(abs, text.replace(cari, ganti));
|
|
133
133
|
return `OK: ${path} diedit.`;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Glob expansion helpers for @pattern/* mentions
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
function globToRegex(pattern) {
|
|
139
|
+
const re = pattern
|
|
140
|
+
.replace(/\\/g, '/')
|
|
141
|
+
.replace(/[.+^${}()|[\]]/g, '\\$&') // escape regex chars, not * ?
|
|
142
|
+
.replace(/\*\*\//g, '(?:.+/)?') // **/ → optional dir prefix
|
|
143
|
+
.replace(/\*\*/g, '.+') // ** → any path (fallback)
|
|
144
|
+
.replace(/\*/g, '[^/]+') // * → single segment
|
|
145
|
+
.replace(/\?/g, '[^/]'); // ? → single char
|
|
146
|
+
return new RegExp(`^${re}$`);
|
|
147
|
+
}
|
|
148
|
+
const MAX_GLOB_FILES = 30;
|
|
149
|
+
const MAX_GLOB_CHARS = 150_000;
|
|
150
|
+
function globFiles(pattern) {
|
|
151
|
+
const patterns = ignorePatterns();
|
|
152
|
+
const re = globToRegex(pattern);
|
|
153
|
+
const hits = [];
|
|
154
|
+
const walk = (dir) => {
|
|
155
|
+
if (hits.length >= MAX_GLOB_FILES)
|
|
156
|
+
return;
|
|
157
|
+
for (const name of readdirSync(dir)) {
|
|
158
|
+
const abs = join(dir, name);
|
|
159
|
+
const rel = relative(ROOT, abs).replace(/\\/g, '/');
|
|
160
|
+
if (isProtected(rel, patterns))
|
|
161
|
+
continue;
|
|
162
|
+
const st = statSync(abs);
|
|
163
|
+
if (st.isDirectory()) {
|
|
164
|
+
walk(abs);
|
|
165
|
+
}
|
|
166
|
+
else if (re.test(rel)) {
|
|
167
|
+
hits.push(rel);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
walk(ROOT);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// ignore unreadable dirs
|
|
176
|
+
}
|
|
177
|
+
return hits.sort();
|
|
178
|
+
}
|
|
179
|
+
/** @git:subcommand shortcuts available in user messages. */
|
|
180
|
+
const GIT_CMDS = {
|
|
181
|
+
diff: ['git', 'diff', 'HEAD'],
|
|
182
|
+
status: ['git', 'status', '--short'],
|
|
183
|
+
log: ['git', 'log', '--oneline', '-10'],
|
|
184
|
+
staged: ['git', 'diff', '--staged'],
|
|
185
|
+
branch: ['git', 'branch', '-a'],
|
|
186
|
+
stash: ['git', 'stash', 'list'],
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Expand @mentions in a user message:
|
|
190
|
+
* @git:diff / @git:status / @git:log → inject git output in <git:X> tags
|
|
191
|
+
* @src/**\/*.ts → inject all matched files in <file> tags
|
|
192
|
+
* @src/app.ts → inject single file in <file> tags
|
|
193
|
+
* Unresolvable tokens are left as-is.
|
|
194
|
+
*/
|
|
195
|
+
export function expandAtMentions(text) {
|
|
196
|
+
return text.replace(/@([\w./\-:*?]+)/g, (match, p) => {
|
|
197
|
+
if (p.startsWith('git:')) {
|
|
198
|
+
const sub = p.slice(4);
|
|
199
|
+
const gitArgs = GIT_CMDS[sub];
|
|
200
|
+
if (!gitArgs) {
|
|
201
|
+
process.stderr.write(` [@${p}: tidak dikenal. Tersedia: ${Object.keys(GIT_CMDS)
|
|
202
|
+
.map((k) => `@git:${k}`)
|
|
203
|
+
.join(', ')}]\n`);
|
|
204
|
+
return match;
|
|
205
|
+
}
|
|
206
|
+
const res = spawnSync(gitArgs[0], gitArgs.slice(1), {
|
|
207
|
+
cwd: ROOT,
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
timeout: 10_000,
|
|
210
|
+
});
|
|
211
|
+
const out = res.error
|
|
212
|
+
? `Error: ${res.error.message}`
|
|
213
|
+
: (res.stdout ?? '').trim() || (res.status !== 0 ? (res.stderr ?? '').trim() : '(kosong)');
|
|
214
|
+
process.stderr.write(` [@${p}: injected]\n`);
|
|
215
|
+
return `\n<git:${sub}>\n${out}\n</git:${sub}>\n`;
|
|
216
|
+
}
|
|
217
|
+
// Glob pattern: @src/**/*.ts or @*.json etc.
|
|
218
|
+
if (p.includes('*') || p.includes('?')) {
|
|
219
|
+
const files = globFiles(p);
|
|
220
|
+
if (files.length === 0) {
|
|
221
|
+
process.stderr.write(` [@${p}: tidak ada file yang cocok]\n`);
|
|
222
|
+
return match;
|
|
223
|
+
}
|
|
224
|
+
const parts = [];
|
|
225
|
+
let total = 0;
|
|
226
|
+
for (const f of files) {
|
|
227
|
+
try {
|
|
228
|
+
const content = bacaFile(f);
|
|
229
|
+
total += content.length;
|
|
230
|
+
if (total > MAX_GLOB_CHARS) {
|
|
231
|
+
process.stderr.write(` [@${p}: limit chars tercapai, berhenti di ${parts.length} file]\n`);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
parts.push(`\n<file path="${f}">\n${content}\n</file>`);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// skip unreadable
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
process.stderr.write(` [@${p}: ${parts.length} file diinjeksi]\n`);
|
|
241
|
+
return parts.join('\n');
|
|
242
|
+
}
|
|
243
|
+
// Only expand if the token looks like a file path (contains / or .)
|
|
244
|
+
if (!p.includes('/') && !p.includes('.'))
|
|
245
|
+
return match;
|
|
246
|
+
try {
|
|
247
|
+
const content = bacaFile(p);
|
|
248
|
+
process.stderr.write(` [@${p}: injected]\n`);
|
|
249
|
+
return `\n<file path="${p}">\n${content}\n</file>\n`;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return match;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** hapus_file — delete a single file. */
|
|
257
|
+
export function hapusFile(path) {
|
|
258
|
+
const { abs } = safePath(path);
|
|
259
|
+
if (!existsSync(abs))
|
|
260
|
+
return `Error: file tidak ada: ${path}`;
|
|
261
|
+
unlinkSync(abs);
|
|
262
|
+
return `OK: ${path} dihapus.`;
|
|
263
|
+
}
|
|
264
|
+
/** pindah_file — rename or move a file/directory (creates parent dirs if needed). */
|
|
265
|
+
export function pindahFile(dari, ke) {
|
|
266
|
+
const { abs: absFrom } = safePath(dari);
|
|
267
|
+
const { abs: absTo } = safePath(ke);
|
|
268
|
+
if (!existsSync(absFrom))
|
|
269
|
+
return `Error: path tidak ada: ${dari}`;
|
|
270
|
+
mkdirSync(dirname(absTo), { recursive: true });
|
|
271
|
+
renameSync(absFrom, absTo);
|
|
272
|
+
return `OK: ${dari} → ${ke}`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* jalankan — run a shell command, streaming output live to stdout so long installs
|
|
276
|
+
* (npm install, cargo build, etc.) don't leave the terminal silent for 30+ seconds.
|
|
277
|
+
* Output is also captured (capped at MAX_OUTPUT) and returned for the model.
|
|
278
|
+
*/
|
|
279
|
+
export async function jalankan(perintah) {
|
|
137
280
|
if (!perintah.trim())
|
|
138
281
|
return 'Error: perintah kosong.';
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
let output = '';
|
|
284
|
+
let limitHit = false;
|
|
285
|
+
process.stdout.write('\n');
|
|
286
|
+
const proc = spawn(perintah, { cwd: ROOT, shell: true });
|
|
287
|
+
const onData = (chunk) => {
|
|
288
|
+
const text = chunk.toString();
|
|
289
|
+
output += text;
|
|
290
|
+
if (!limitHit) {
|
|
291
|
+
if (output.length > MAX_OUTPUT) {
|
|
292
|
+
limitHit = true;
|
|
293
|
+
process.stdout.write('\n ··· output dipotong ···\n');
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
process.stdout.write(text);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const timer = setTimeout(() => {
|
|
301
|
+
proc.kill();
|
|
302
|
+
process.stdout.write('\n');
|
|
303
|
+
resolve('exit=null\nError: timeout (120 detik).');
|
|
304
|
+
}, 120_000);
|
|
305
|
+
proc.stdout?.on('data', onData);
|
|
306
|
+
proc.stderr?.on('data', onData);
|
|
307
|
+
proc.on('close', (code) => {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
process.stdout.write('\n');
|
|
310
|
+
const trimmed = output.trim();
|
|
311
|
+
const capped = trimmed.length > MAX_OUTPUT ? `${trimmed.slice(0, MAX_OUTPUT)}\n…[dipotong]` : trimmed;
|
|
312
|
+
resolve(`exit=${code ?? 'null'}\n${capped || '(tanpa output)'}`);
|
|
313
|
+
});
|
|
314
|
+
proc.on('error', (err) => {
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
process.stdout.write('\n');
|
|
317
|
+
resolve(`exit=null\nError: ${err.message}`);
|
|
318
|
+
});
|
|
145
319
|
});
|
|
146
|
-
const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.trim();
|
|
147
|
-
const capped = out.length > MAX_OUTPUT ? `${out.slice(0, MAX_OUTPUT)}\n…[dipotong]` : out;
|
|
148
|
-
return `exit=${res.status ?? 'null'}\n${capped || '(tanpa output)'}`;
|
|
149
320
|
}
|
package/dist/api.js
CHANGED
|
@@ -139,6 +139,8 @@ export async function chatComplete(model, messages, tools, onText) {
|
|
|
139
139
|
let buffer = '';
|
|
140
140
|
let content = '';
|
|
141
141
|
const calls = new Map();
|
|
142
|
+
let promptTokens = 0;
|
|
143
|
+
let completionTokens = 0;
|
|
142
144
|
while (true) {
|
|
143
145
|
const { done, value } = await reader.read();
|
|
144
146
|
if (done)
|
|
@@ -154,7 +156,13 @@ export async function chatComplete(model, messages, tools, onText) {
|
|
|
154
156
|
if (payload === '[DONE]')
|
|
155
157
|
continue;
|
|
156
158
|
try {
|
|
157
|
-
const
|
|
159
|
+
const chunk = JSON.parse(payload);
|
|
160
|
+
// Capture usage when the gateway includes it (typically in the final chunk)
|
|
161
|
+
if (chunk.usage) {
|
|
162
|
+
promptTokens = chunk.usage.prompt_tokens ?? promptTokens;
|
|
163
|
+
completionTokens = chunk.usage.completion_tokens ?? completionTokens;
|
|
164
|
+
}
|
|
165
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
158
166
|
if (!delta)
|
|
159
167
|
continue;
|
|
160
168
|
if (delta.content) {
|
|
@@ -184,7 +192,12 @@ export async function chatComplete(model, messages, tools, onText) {
|
|
|
184
192
|
type: 'function',
|
|
185
193
|
function: { name: c.name, arguments: c.args },
|
|
186
194
|
}));
|
|
187
|
-
|
|
195
|
+
const usage = promptTokens + completionTokens > 0 ? { promptTokens, completionTokens } : undefined;
|
|
196
|
+
return {
|
|
197
|
+
content: content || null,
|
|
198
|
+
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
|
|
199
|
+
usage,
|
|
200
|
+
};
|
|
188
201
|
}
|
|
189
202
|
/** Parse an OpenAI SSE stream, yielding content deltas. */
|
|
190
203
|
async function* consumeSSE(res) {
|
package/dist/commands.js
CHANGED
|
@@ -3,10 +3,11 @@ import { 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 { runAgent } from './agent/run.js';
|
|
7
|
-
import { runTeam } from './agent/team.js';
|
|
6
|
+
import { loadCheckpoint, runAgent } from './agent/run.js';
|
|
7
|
+
import { runCustomTeam, runTeam } from './agent/team.js';
|
|
8
|
+
import { expandAtMentions } from './agent/tools.js';
|
|
8
9
|
import { createTopup, fetchBytesAuthed, fetchLatestVersion, fetchModels, fetchPemakaian, fetchSaldo, generateImage, modelSupportsTools, pollJob, resolveMediaModel, resolveModel, streamChat, streamVision, submitVideo, } from './api.js';
|
|
9
|
-
import { clearKey, fileConfig, loadConfig, saveConfig } from './config.js';
|
|
10
|
+
import { clearKey, fileConfig, initProjectConfig, listTeamConfigs, loadConfig, loadProjectConfig, loadTeamConfig, saveConfig, saveTeamConfig, } from './config.js';
|
|
10
11
|
import { VERSION } from './help.js';
|
|
11
12
|
import { bold, cyan, dim, green, idn, prompt, promptHidden, readStdin, red, rupiah, sleep, thinking, yellow, } from './ui.js';
|
|
12
13
|
const IMAGE_MIME = {
|
|
@@ -27,7 +28,15 @@ export async function cmdMasuk() {
|
|
|
27
28
|
}
|
|
28
29
|
saveConfig({ apiKey: key });
|
|
29
30
|
console.log(green('✓ Tersimpan di ~/.kiosapi/config.json'));
|
|
30
|
-
|
|
31
|
+
process.stdout.write(' Memverifikasi saldo… ');
|
|
32
|
+
try {
|
|
33
|
+
const s = await fetchSaldo();
|
|
34
|
+
console.log(green(`Saldo: ${rupiah(s.balance_rupiah)}`));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
console.log(dim('(tidak bisa cek saldo — periksa koneksi atau coba: kiosapi saldo)'));
|
|
38
|
+
}
|
|
39
|
+
console.log(dim('Siap! Coba: kiosapi tanya "halo"'));
|
|
31
40
|
}
|
|
32
41
|
/** keluar — forget the stored key. */
|
|
33
42
|
export function cmdKeluar() {
|
|
@@ -49,6 +58,16 @@ export async function cmdPeriksa() {
|
|
|
49
58
|
catch {
|
|
50
59
|
console.log(red('tidak terhubung'));
|
|
51
60
|
}
|
|
61
|
+
if (cfg.apiKey) {
|
|
62
|
+
process.stdout.write(' API key saldo: ');
|
|
63
|
+
try {
|
|
64
|
+
const s = await fetchSaldo();
|
|
65
|
+
console.log(green(`valid — saldo ${rupiah(s.balance_rupiah)}`));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
console.log(red('invalid atau expired — coba: kiosapi masuk'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
const latest = await fetchLatestVersion();
|
|
53
72
|
const upd = latest && isNewerVersion(latest, VERSION)
|
|
54
73
|
? yellow(` → versi ${latest} tersedia (kiosapi perbarui)`)
|
|
@@ -56,6 +75,19 @@ export async function cmdPeriksa() {
|
|
|
56
75
|
? green(' ✓ terbaru')
|
|
57
76
|
: '';
|
|
58
77
|
console.log(` Versi : ${VERSION}${upd}`);
|
|
78
|
+
// Tim tersimpan
|
|
79
|
+
const teams = listTeamConfigs();
|
|
80
|
+
console.log(` Tim tersimpan: ${teams.length > 0 ? teams.map((t) => cyan(t)).join(', ') : dim('(belum ada — lihat: kiosapi tim --buat)')}`);
|
|
81
|
+
// Checkpoint
|
|
82
|
+
const cp = loadCheckpoint();
|
|
83
|
+
if (cp) {
|
|
84
|
+
const when = new Date(cp.savedAt).toLocaleString('id-ID');
|
|
85
|
+
const turns = cp.messages.filter((m) => m.role === 'user').length;
|
|
86
|
+
console.log(` Checkpoint : ${yellow(`${cp.mode} · ${turns} giliran · ${when}`)} ${dim('(kiosapi lanjut)')}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(` Checkpoint : ${dim('(tidak ada)')}`);
|
|
90
|
+
}
|
|
59
91
|
}
|
|
60
92
|
/** Compare two dotted versions; true if `latest` is strictly newer than `current`. */
|
|
61
93
|
function isNewerVersion(latest, current) {
|
|
@@ -105,7 +137,7 @@ export async function maybeNotifyUpdate() {
|
|
|
105
137
|
saveConfig({ updateCheckedAt: now, latestVersion: latest });
|
|
106
138
|
}
|
|
107
139
|
if (latest && isNewerVersion(latest, VERSION)) {
|
|
108
|
-
console.log(yellow(`Versi baru kiosapi ${latest} tersedia (sekarang ${VERSION}). Jalankan: kiosapi perbarui`));
|
|
140
|
+
console.log(yellow(`Versi baru kiosapi ${latest} tersedia (sekarang ${VERSION}). Jalankan: kiosapi perbarui (atau /perbarui dalam sesi)`));
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
143
|
/** model — list available models. `--cari q` filters; `--tools` shows only tool-capable models. */
|
|
@@ -152,6 +184,7 @@ export async function cmdTanya(args) {
|
|
|
152
184
|
text = text ? `${text}\n\n${piped}` : piped;
|
|
153
185
|
if (!text)
|
|
154
186
|
throw new Error('Beri pertanyaan. Contoh: kiosapi tanya "halo"');
|
|
187
|
+
text = expandAtMentions(text);
|
|
155
188
|
const model = await resolveModel(values.model);
|
|
156
189
|
const stop = thinking();
|
|
157
190
|
let first = true;
|
|
@@ -290,7 +323,11 @@ async function runAgentCommand(args, mode) {
|
|
|
290
323
|
options: { model: { type: 'string', short: 'm' }, otomatis: { type: 'boolean' } },
|
|
291
324
|
allowPositionals: true,
|
|
292
325
|
});
|
|
293
|
-
|
|
326
|
+
let task = positionals.join(' ').trim();
|
|
327
|
+
// Accept piped stdin as additional context (e.g. `npm test 2>&1 | kiosapi buat "fix errors"`)
|
|
328
|
+
const piped = await readStdin();
|
|
329
|
+
if (piped)
|
|
330
|
+
task = task ? `${task}\n\n${piped}` : piped;
|
|
294
331
|
if (!task)
|
|
295
332
|
throw new Error('Beri tugas. Contoh: kiosapi buat "bikin REST API Express + tes"');
|
|
296
333
|
const model = await resolveModel(values.model);
|
|
@@ -404,9 +441,151 @@ export async function cmdLihat(args) {
|
|
|
404
441
|
stop();
|
|
405
442
|
process.stdout.write('\n');
|
|
406
443
|
}
|
|
444
|
+
/** Interactive wizard to create a custom TeamConfig from scratch. */
|
|
445
|
+
async function timBuatWizard(nama) {
|
|
446
|
+
if (!nama.trim())
|
|
447
|
+
throw new Error('Beri nama tim. Contoh: kiosapi tim --buat backend-team');
|
|
448
|
+
const DEFAULT_MODEL = 'deepseek/deepseek-v3';
|
|
449
|
+
console.log(bold(`Membuat tim "${nama}".`));
|
|
450
|
+
console.log(dim('Tambahkan peran satu per satu. Ketik Enter kosong untuk nama peran = selesai.\n'));
|
|
451
|
+
const peran = {};
|
|
452
|
+
const alurInput = [];
|
|
453
|
+
let idx = 1;
|
|
454
|
+
while (true) {
|
|
455
|
+
const namaPr = (await prompt(`Peran ${idx} — nama (kosong = selesai): `)).trim();
|
|
456
|
+
if (!namaPr)
|
|
457
|
+
break;
|
|
458
|
+
const modelPr = (await prompt(` Model (Enter = ${DEFAULT_MODEL}): `)).trim() || DEFAULT_MODEL;
|
|
459
|
+
const modeRaw = (await prompt(' Mode [rencana/edit/buat] (Enter = buat): '))
|
|
460
|
+
.trim()
|
|
461
|
+
.toLowerCase();
|
|
462
|
+
const modePr = modeRaw === 'rencana' || modeRaw === 'edit' ? modeRaw : 'buat';
|
|
463
|
+
const briefPr = (await prompt(' Brief/instruksi peran ini (Enter = skip): ')).trim();
|
|
464
|
+
peran[namaPr] = { model: modelPr, mode: modePr, brief: briefPr || undefined };
|
|
465
|
+
alurInput.push(namaPr);
|
|
466
|
+
console.log(dim(` ✓ Peran "${namaPr}" ditambahkan.\n`));
|
|
467
|
+
idx++;
|
|
468
|
+
}
|
|
469
|
+
if (alurInput.length === 0) {
|
|
470
|
+
console.log(yellow('Tidak ada peran — tim tidak disimpan.'));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const alurRaw = (await prompt(`Urutan alur (Enter = ${alurInput.join(',')}): `)).trim();
|
|
474
|
+
const alur = alurRaw
|
|
475
|
+
? alurRaw
|
|
476
|
+
.split(',')
|
|
477
|
+
.map((s) => s.trim())
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
: alurInput;
|
|
480
|
+
const config = { nama, peran, alur };
|
|
481
|
+
saveTeamConfig(config);
|
|
482
|
+
console.log(green(`\n✓ Tim "${nama}" tersimpan (${alur.length} peran: ${alur.join(' → ')}).`));
|
|
483
|
+
console.log(dim(` Edit : ~/.kiosapi/tim/${nama}.json`));
|
|
484
|
+
console.log(dim(` Jalankan: kiosapi tim --pakai ${nama} "tugas"`));
|
|
485
|
+
}
|
|
486
|
+
const ALL_COMMANDS = [
|
|
487
|
+
'masuk',
|
|
488
|
+
'keluar',
|
|
489
|
+
'periksa',
|
|
490
|
+
'perbarui',
|
|
491
|
+
'model',
|
|
492
|
+
'tanya',
|
|
493
|
+
'ngobrol',
|
|
494
|
+
'setel',
|
|
495
|
+
'sambung',
|
|
496
|
+
'saldo',
|
|
497
|
+
'pakai',
|
|
498
|
+
'isi',
|
|
499
|
+
'rencana',
|
|
500
|
+
'edit',
|
|
501
|
+
'buat',
|
|
502
|
+
'tim',
|
|
503
|
+
'gambar',
|
|
504
|
+
'video',
|
|
505
|
+
'lihat',
|
|
506
|
+
'lanjut',
|
|
507
|
+
'init',
|
|
508
|
+
'completion',
|
|
509
|
+
// English aliases
|
|
510
|
+
'login',
|
|
511
|
+
'logout',
|
|
512
|
+
'doctor',
|
|
513
|
+
'update',
|
|
514
|
+
'models',
|
|
515
|
+
'ask',
|
|
516
|
+
'chat',
|
|
517
|
+
'config',
|
|
518
|
+
'connect',
|
|
519
|
+
'balance',
|
|
520
|
+
'usage',
|
|
521
|
+
'topup',
|
|
522
|
+
'plan',
|
|
523
|
+
'team',
|
|
524
|
+
'image',
|
|
525
|
+
'vision',
|
|
526
|
+
'resume',
|
|
527
|
+
];
|
|
528
|
+
/** completion — output shell completion script for the given shell. */
|
|
529
|
+
export function cmdCompletion(args) {
|
|
530
|
+
const shell = args[0]?.toLowerCase();
|
|
531
|
+
const cmds = ALL_COMMANDS.join(' ');
|
|
532
|
+
if (shell === 'bash') {
|
|
533
|
+
console.log(`# Bash completion for kiosapi
|
|
534
|
+
# Add to ~/.bashrc: source <(kiosapi completion bash)
|
|
535
|
+
_kiosapi_completion() {
|
|
536
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
537
|
+
COMPREPLY=( $(compgen -W "${cmds}" -- "$cur") )
|
|
538
|
+
}
|
|
539
|
+
complete -F _kiosapi_completion kiosapi`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (shell === 'zsh') {
|
|
543
|
+
console.log(`# Zsh completion for kiosapi
|
|
544
|
+
# Add to ~/.zshrc: source <(kiosapi completion zsh)
|
|
545
|
+
_kiosapi() {
|
|
546
|
+
local -a commands
|
|
547
|
+
commands=(${ALL_COMMANDS.map((c) => `'${c}'`).join(' ')})
|
|
548
|
+
_describe 'command' commands
|
|
549
|
+
}
|
|
550
|
+
compdef _kiosapi kiosapi`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (shell === 'fish') {
|
|
554
|
+
console.log(`# Fish completion for kiosapi
|
|
555
|
+
# Save to ~/.config/fish/completions/kiosapi.fish
|
|
556
|
+
complete -c kiosapi -f -a "${cmds}"`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (shell === 'pwsh' || shell === 'powershell') {
|
|
560
|
+
console.log(`# PowerShell completion for kiosapi
|
|
561
|
+
# Add to $PROFILE:
|
|
562
|
+
Register-ArgumentCompleter -Native -CommandName kiosapi -ScriptBlock {
|
|
563
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
564
|
+
@(${ALL_COMMANDS.map((c) => `'${c}'`).join(',')}) |
|
|
565
|
+
Where-Object { $_ -like "$wordToComplete*" } |
|
|
566
|
+
ForEach-Object {
|
|
567
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
568
|
+
}
|
|
569
|
+
}`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
console.log(`${bold('kiosapi completion')} — pasang shell completion
|
|
573
|
+
|
|
574
|
+
kiosapi completion bash → Bash
|
|
575
|
+
kiosapi completion zsh → Zsh
|
|
576
|
+
kiosapi completion fish → Fish
|
|
577
|
+
kiosapi completion pwsh → PowerShell
|
|
578
|
+
|
|
579
|
+
${bold('Cara pasang:')}
|
|
580
|
+
${dim('Bash:')} echo 'source <(kiosapi completion bash)' >> ~/.bashrc
|
|
581
|
+
${dim('Zsh:')} echo 'source <(kiosapi completion zsh)' >> ~/.zshrc
|
|
582
|
+
${dim('Fish:')} kiosapi completion fish > ~/.config/fish/completions/kiosapi.fish
|
|
583
|
+
${dim('Pwsh:')} echo '. <(kiosapi completion pwsh | Out-String)' >> $PROFILE`);
|
|
584
|
+
}
|
|
407
585
|
/**
|
|
408
|
-
* tim — multi-agent pipeline
|
|
409
|
-
*
|
|
586
|
+
* tim — multi-agent pipeline. Default: perencana → pengkode → peninjau.
|
|
587
|
+
* --pakai <nama> loads a saved custom team config (~/.kiosapi/tim/<nama>.json).
|
|
588
|
+
* --daftar lists all saved teams.
|
|
410
589
|
*/
|
|
411
590
|
export async function cmdTim(args) {
|
|
412
591
|
const { values, positionals } = parseArgs({
|
|
@@ -417,9 +596,44 @@ export async function cmdTim(args) {
|
|
|
417
596
|
perencana: { type: 'string' },
|
|
418
597
|
pengkode: { type: 'string' },
|
|
419
598
|
peninjau: { type: 'string' },
|
|
599
|
+
pakai: { type: 'string' },
|
|
600
|
+
daftar: { type: 'boolean' },
|
|
601
|
+
buat: { type: 'string' },
|
|
420
602
|
},
|
|
421
603
|
allowPositionals: true,
|
|
422
604
|
});
|
|
605
|
+
if (values.daftar) {
|
|
606
|
+
const teams = listTeamConfigs();
|
|
607
|
+
if (teams.length === 0) {
|
|
608
|
+
console.log(dim('Belum ada tim tersimpan.'));
|
|
609
|
+
console.log(dim('Buat baru: kiosapi tim --buat <nama>'));
|
|
610
|
+
console.log(dim('Atau dari sesi interaktif: /simpan-tim <nama>'));
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
console.log(bold('Tim tersimpan:'));
|
|
614
|
+
for (const t of teams) {
|
|
615
|
+
const cfg = loadTeamConfig(t);
|
|
616
|
+
const alur = cfg?.alur.join(' → ') ?? '?';
|
|
617
|
+
console.log(` ${cyan(t)} ${dim(`(${alur})`)}`);
|
|
618
|
+
}
|
|
619
|
+
console.log(dim(`\n${teams.length} tim. Jalankan: kiosapi tim --pakai <nama> "tugas"`));
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (values.buat) {
|
|
624
|
+
await timBuatWizard(values.buat);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (values.pakai) {
|
|
628
|
+
const config = loadTeamConfig(values.pakai);
|
|
629
|
+
if (!config)
|
|
630
|
+
throw new Error(`Tim "${values.pakai}" tidak ditemukan. Lihat: kiosapi tim --daftar`);
|
|
631
|
+
const task = positionals.join(' ').trim();
|
|
632
|
+
if (!task)
|
|
633
|
+
throw new Error(`Beri tugas. Contoh: kiosapi tim --pakai ${values.pakai} "tambah endpoint login"`);
|
|
634
|
+
await runCustomTeam(task, { config, otomatis: Boolean(values.otomatis) });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
423
637
|
const task = positionals.join(' ').trim();
|
|
424
638
|
if (!task)
|
|
425
639
|
throw new Error('Beri tugas. Contoh: kiosapi tim "bikin endpoint /health + tes"');
|
|
@@ -470,6 +684,38 @@ export async function cmdIsi(args) {
|
|
|
470
684
|
console.log(green(`✓ Tagihan dibuat untuk ${rupiah(amount)}.`));
|
|
471
685
|
console.log(`Bayar di: ${cyan(invoice_url)}`);
|
|
472
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* init — scaffold a kiosapi.json project config in the current directory.
|
|
689
|
+
* If one already exists, prints it and exits so the user can edit it manually.
|
|
690
|
+
*/
|
|
691
|
+
export async function cmdInit() {
|
|
692
|
+
const existing = loadProjectConfig();
|
|
693
|
+
if (existing) {
|
|
694
|
+
console.log(yellow('kiosapi.json sudah ada (mungkin di direktori induk). Edit langsung untuk mengubah:'));
|
|
695
|
+
console.log(JSON.stringify(existing, null, 2));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
console.log(bold('Buat kiosapi.json — config per-project'));
|
|
699
|
+
console.log(dim('Kosongkan untuk pakai default global.\n'));
|
|
700
|
+
const chosenModel = await pickModel(undefined, fileConfig().defaultModel);
|
|
701
|
+
const modeRaw = (await prompt('Mode default [rencana/edit/buat] (Enter = buat): '))
|
|
702
|
+
.trim()
|
|
703
|
+
.toLowerCase();
|
|
704
|
+
const mode = modeRaw === 'rencana' || modeRaw === 'edit' ? modeRaw : 'buat';
|
|
705
|
+
const otoRaw = (await prompt('Auto-approve semua tool secara default? (y/t, Enter = t): '))
|
|
706
|
+
.trim()
|
|
707
|
+
.toLowerCase();
|
|
708
|
+
const config = {
|
|
709
|
+
...(chosenModel ? { model: chosenModel } : {}),
|
|
710
|
+
mode,
|
|
711
|
+
otomatis: otoRaw === 'y' || otoRaw === 'ya',
|
|
712
|
+
};
|
|
713
|
+
const p = initProjectConfig(config);
|
|
714
|
+
console.log(green(`\n✓ ${p}`));
|
|
715
|
+
console.log(JSON.stringify(config, null, 2));
|
|
716
|
+
console.log(dim('\nTambahkan ke .gitignore jika config ini bersifat personal.'));
|
|
717
|
+
console.log(dim('Edit langsung untuk mengubah, atau hapus untuk kembali ke default global.'));
|
|
718
|
+
}
|
|
473
719
|
/** Editor/IDE tools that can't be launched from a terminal — we print paste-config instead. */
|
|
474
720
|
const EDITOR_TOOLS = new Set(['cursor', 'cline', 'continue', 'vscode', 'windsurf']);
|
|
475
721
|
/**
|
package/dist/config.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
const DEFAULT_BASE_URL = 'https://api.kiosapi.id';
|
|
5
|
-
const DIR = join(homedir(), '.kiosapi');
|
|
5
|
+
export const DIR = join(homedir(), '.kiosapi');
|
|
6
6
|
export const CONFIG_PATH = join(DIR, 'config.json');
|
|
7
|
+
export const CHECKPOINT_PATH = join(DIR, 'checkpoint.json');
|
|
8
|
+
export const TIM_DIR = join(DIR, 'tim');
|
|
7
9
|
/** Read only the on-disk config (ignores env overrides) — used before writing. */
|
|
8
10
|
function readFile() {
|
|
9
11
|
if (!existsSync(CONFIG_PATH))
|
|
@@ -47,3 +49,59 @@ export function clearKey() {
|
|
|
47
49
|
export function fileConfig() {
|
|
48
50
|
return readFile();
|
|
49
51
|
}
|
|
52
|
+
/** Persist a team config to ~/.kiosapi/tim/<nama>.json. */
|
|
53
|
+
export function saveTeamConfig(config) {
|
|
54
|
+
mkdirSync(TIM_DIR, { recursive: true });
|
|
55
|
+
writeFileSync(join(TIM_DIR, `${config.nama}.json`), `${JSON.stringify(config, null, 2)}\n`);
|
|
56
|
+
}
|
|
57
|
+
/** Load a team config by name; null if not found or invalid. */
|
|
58
|
+
export function loadTeamConfig(nama) {
|
|
59
|
+
const p = join(TIM_DIR, `${nama}.json`);
|
|
60
|
+
if (!existsSync(p))
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const PROJECT_CONFIG_NAME = 'kiosapi.json';
|
|
70
|
+
/**
|
|
71
|
+
* Walk up from cwd looking for kiosapi.json. Returns the first one found, or null.
|
|
72
|
+
* Walking up lets a project root config apply to all sub-directories.
|
|
73
|
+
*/
|
|
74
|
+
export function loadProjectConfig() {
|
|
75
|
+
let dir = process.cwd();
|
|
76
|
+
while (true) {
|
|
77
|
+
const p = join(dir, PROJECT_CONFIG_NAME);
|
|
78
|
+
if (existsSync(p)) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const parent = dirname(dir);
|
|
87
|
+
if (parent === dir)
|
|
88
|
+
break; // filesystem root
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** Write a kiosapi.json into the current directory. Returns the full path written. */
|
|
94
|
+
export function initProjectConfig(config) {
|
|
95
|
+
const p = join(process.cwd(), PROJECT_CONFIG_NAME);
|
|
96
|
+
writeFileSync(p, `${JSON.stringify(config, null, 2)}\n`);
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
/** List all saved team names. */
|
|
100
|
+
export function listTeamConfigs() {
|
|
101
|
+
if (!existsSync(TIM_DIR))
|
|
102
|
+
return [];
|
|
103
|
+
return readdirSync(TIM_DIR)
|
|
104
|
+
.filter((f) => f.endsWith('.json'))
|
|
105
|
+
.map((f) => f.slice(0, -5))
|
|
106
|
+
.sort();
|
|
107
|
+
}
|