kiosapi 0.1.13 → 0.1.16

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { extname } from 'node:path';
2
+ import { extname, join } from 'node:path';
3
3
  import { chatComplete, generateImage, resolveMediaModel, streamVision, } from '../api.js';
4
4
  import { CHECKPOINT_PATH } from '../config.js';
5
5
  import { cyan, dim, green, idn, prompt, red, rupiah, thinking, yellow } from '../ui.js';
@@ -235,9 +235,12 @@ async function runTool(call, otomatis, model) {
235
235
  case 'baca_file':
236
236
  console.log(dim(` baca ${str('path')}`));
237
237
  return { output: bacaFile(str('path')) };
238
- case 'daftar_file':
239
- console.log(dim(` daftar ${str('path') || '.'}`));
240
- return { output: daftarFile(str('path') || '.') };
238
+ case 'daftar_file': {
239
+ const ked = typeof args['kedalaman'] === 'number' ? args['kedalaman'] : undefined;
240
+ const dPath = str('path') || '.';
241
+ console.log(dim(` daftar ${dPath}${ked != null ? ` (kedalaman ${ked})` : ''}`));
242
+ return { output: daftarFile(dPath, ked) };
243
+ }
241
244
  case 'cari':
242
245
  console.log(dim(` cari "${str('pola')}"`));
243
246
  return { output: cari(str('pola'), str('ext') || undefined) };
@@ -370,6 +373,32 @@ export function undoLastTurn(s) {
370
373
  * agent's final text (last answer or the `selesai` summary) — useful for chaining agents in a team.
371
374
  */
372
375
  export async function runTurn(s, userText) {
376
+ // On the very first turn of a fresh session, auto-inject project metadata so the model
377
+ // starts oriented without needing to call daftar_file(".") for basic orientation.
378
+ // Injected: root directory tree + key manifest files (package.json, README, etc.).
379
+ if (s.messages.filter((m) => m.role === 'user').length === 0) {
380
+ const snippets = [];
381
+ try {
382
+ snippets.push(`<root-directory>\n${daftarFile('.')}\n</root-directory>`);
383
+ }
384
+ catch { /* ignore */ }
385
+ for (const f of ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'README.md']) {
386
+ const abs = join(process.cwd(), f);
387
+ if (existsSync(abs)) {
388
+ try {
389
+ const content = readFileSync(abs, 'utf8');
390
+ snippets.push(`<file path="${f}">\n${content.slice(0, 4000)}\n</file>`);
391
+ }
392
+ catch { /* unreadable — skip */ }
393
+ }
394
+ }
395
+ if (snippets.length > 0) {
396
+ s.messages.push({
397
+ role: 'system',
398
+ content: `## Konteks Proyek (auto-injected — jangan panggil daftar_file(".") lagi)\n${snippets.join('\n\n')}`,
399
+ });
400
+ }
401
+ }
373
402
  s.messages.push({ role: 'user', content: userText });
374
403
  const tools = toolsForMode(s.mode);
375
404
  let lastText = '';
@@ -387,6 +416,11 @@ export async function runTurn(s, userText) {
387
416
  const lastSigs = []; // last 3 sigs for consecutive detection
388
417
  const COUNT_LIMIT = 4; // same tool+args called 4× total → loop
389
418
  const CONSEC_LIMIT = 3; // same sig 3× in a row → loop
419
+ // Read-only tool cache: on 2nd+ identical call, return the previous result with a warning
420
+ // instead of re-running. This gives the model early feedback so it can self-correct before
421
+ // COUNT_LIMIT is reached, preventing the common "list root, list root, list root" pattern.
422
+ const READ_ONLY_TOOLS = new Set(['baca_file', 'daftar_file', 'cari', 'lihat_gambar']);
423
+ const toolCache = new Map(); // sig → first output
390
424
  const stepLimit = s.maxSteps ?? MAX_STEPS;
391
425
  for (let step = 0; step < stepLimit; step++) {
392
426
  const stop = thinking();
@@ -466,10 +500,30 @@ export async function runTurn(s, userText) {
466
500
  }
467
501
  const stepModified = new Set();
468
502
  for (const call of calls) {
503
+ const sig = `${call.function.name}:${call.function.arguments}`;
504
+ const count = callCounts.get(sig) ?? 1;
505
+ // For read-only tools called more than once with the same args: return the cached result
506
+ // with an escalating warning instead of re-running. The model gets immediate feedback so
507
+ // it can self-correct (pick a subfolder, go deeper) rather than looping to COUNT_LIMIT.
508
+ if (count > 1 && READ_ONLY_TOOLS.has(call.function.name) && toolCache.has(sig)) {
509
+ const toolName = call.function.name;
510
+ const cachedOut = toolCache.get(sig) ?? '';
511
+ const warn = count >= COUNT_LIMIT - 1
512
+ ? `⚠ STOP: "${toolName}" sudah dipanggil ${count}× dengan argumen yang sama — ` +
513
+ `hasilnya tidak berubah. WAJIB gunakan tool "selesai" atau eksplorasi path BERBEDA.`
514
+ : `[Cache — identik dengan panggilan sebelumnya]\n${cachedOut}\n\n` +
515
+ `⚠ Kamu sudah memanggil "${toolName}" dengan argumen yang sama ${count}×. ` +
516
+ `Jangan ulangi — masuk ke subfolder spesifik atau gunakan "selesai".`;
517
+ console.log(dim(` ↩ ${toolName} (cache ke-${count})`));
518
+ s.messages.push({ role: 'tool', content: warn, tool_call_id: call.id });
519
+ continue;
520
+ }
469
521
  const result = await runTool(call, s.otomatis, s.model);
470
522
  s.messages.push({ role: 'tool', content: result.output, tool_call_id: call.id });
471
523
  if (result.modifiedPath)
472
524
  stepModified.add(result.modifiedPath);
525
+ if (READ_ONLY_TOOLS.has(call.function.name))
526
+ toolCache.set(sig, result.output);
473
527
  if (result.done) {
474
528
  if (stepModified.size > 0)
475
529
  console.log(dim(` ✎ ${[...stepModified].join(' · ')}`));
@@ -16,10 +16,18 @@ const TOOLS = {
16
16
  type: 'function',
17
17
  function: {
18
18
  name: 'daftar_file',
19
- description: 'Daftar isi sebuah folder.',
19
+ description: 'Daftar isi sebuah folder sebagai tree. Default: 3 level untuk ".", 1 level untuk subfolder. Gunakan kedalaman=2-4 untuk subfolder yang dalam.',
20
20
  parameters: {
21
21
  type: 'object',
22
- properties: { path: { type: 'string', description: 'Path folder (default ".")' } },
22
+ properties: {
23
+ path: { type: 'string', description: 'Path folder (default ".")' },
24
+ kedalaman: {
25
+ type: 'integer',
26
+ description: 'Kedalaman tree (1–5). Default: 3 untuk ".", 1 untuk subfolder.',
27
+ minimum: 1,
28
+ maximum: 5,
29
+ },
30
+ },
23
31
  },
24
32
  },
25
33
  },
@@ -192,10 +200,11 @@ Direktori kerja: ${process.cwd()}
192
200
  OS: ${osName} · ${shellNote}
193
201
  Aturan:
194
202
  - Bekerja langkah demi langkah: pakai tool untuk membaca sebelum mengubah.
203
+ - Strategi eksplorasi: daftar_file(".") sudah tersedia di konteks awal — identifikasi file yang relevan LANGSUNG, lalu baca dengan baca_file. Jangan ulangi daftar_file pada path yang sama.
204
+ - JANGAN memanggil tool APAPUN dengan argumen identik lebih dari 1× dalam satu sesi. Jika tool mengembalikan peringatan cache "⚠", langsung ganti ke path atau argumen BERBEDA.
195
205
  - Path selalu relatif ke direktori kerja; akses ke luar ditolak.
196
206
  - Gunakan hapus_file/pindah_file untuk menghapus/memindahkan file (lebih aman dari jalankan del/rm).
197
207
  - Buat perubahan kecil dan jelas. Setelah tugas beres, panggil tool "selesai" dengan ringkasan.
198
- - JANGAN memanggil tool yang sama dengan argumen yang sama lebih dari berturut-turut. Jika sudah mendapat hasil daftar_file atau baca_file, langsung lanjutkan — JANGAN ulangi panggilan yang sama.
199
- - Jika tidak tahu harus berbuat apa selanjutnya, gunakan tool "selesai" dan jelaskan apa yang sudah ditemukan.
208
+ - Jika tidak tahu harus berbuat apa: gunakan tool "selesai" dan jelaskan apa yang sudah ditemukan.
200
209
  - Jawab dan jelaskan dalam Bahasa Indonesia.`;
201
210
  }
@@ -52,20 +52,43 @@ export function bacaFile(path) {
52
52
  ? `${text.slice(0, MAX_READ)}\n…[dipotong, ${text.length} char total]`
53
53
  : text;
54
54
  }
55
- /** daftar_file — list a directory (ignored entries hidden). */
56
- export function daftarFile(path) {
55
+ /** daftar_file — list a directory (ignored entries hidden).
56
+ * kedalaman controls tree depth (1–5). Defaults: 3 for root ".", 1 for subdirs.
57
+ * Output is capped at 300 lines to keep API payloads reasonable. */
58
+ export function daftarFile(path, kedalaman) {
57
59
  const { abs, rel } = safePath(path || '.');
58
60
  if (!existsSync(abs))
59
61
  return `Error: folder tidak ada: ${path}`;
60
62
  const patterns = ignorePatterns();
61
- const entries = readdirSync(abs)
62
- .filter((name) => !isProtected(rel === '.' ? name : `${rel}/${name}`, patterns))
63
- .sort()
64
- .map((name) => {
65
- const dir = statSync(join(abs, name)).isDirectory();
66
- return dir ? `${name}/` : name;
67
- });
68
- return entries.length > 0 ? entries.join('\n') : '(kosong)';
63
+ // Default depth: 3 for root (wide orientation), 1 for any subdir (focused)
64
+ const depth = kedalaman != null ? Math.min(Math.max(1, kedalaman), 5) : rel === '.' ? 3 : 1;
65
+ const listDir = (dirAbs, dirRel, d, indent, lines) => {
66
+ let entries;
67
+ try {
68
+ entries = readdirSync(dirAbs)
69
+ .filter((name) => !isProtected(dirRel === '.' ? name : `${dirRel}/${name}`, patterns))
70
+ .sort()
71
+ .map((name) => ({ name, isDir: statSync(join(dirAbs, name)).isDirectory() }));
72
+ }
73
+ catch {
74
+ return; // unreadable dir — skip silently
75
+ }
76
+ for (const { name, isDir } of entries) {
77
+ if (lines.length >= 300) {
78
+ lines.push(' … (dipotong — panggil lagi dengan subfolder spesifik)');
79
+ return;
80
+ }
81
+ lines.push(`${indent}${name}${isDir ? '/' : ''}`);
82
+ if (isDir && d > 1) {
83
+ const subAbs = join(dirAbs, name);
84
+ const subRel = dirRel === '.' ? name : `${dirRel}/${name}`;
85
+ listDir(subAbs, subRel, d - 1, `${indent} `, lines);
86
+ }
87
+ }
88
+ };
89
+ const lines = [];
90
+ listDir(abs, rel, depth, '', lines);
91
+ return lines.length > 0 ? lines.join('\n') : '(kosong)';
69
92
  }
70
93
  /** cari — grep file contents under the working dir (case-insensitive). */
71
94
  export function cari(pola, ext) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiosapi",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "description": "CLI Kiosapi.id berbahasa Indonesia — bangun aplikasimu pakai API key Kiosapi (agen + multimodal).",
6
6
  "keywords": [