kiosapi 0.1.3 → 0.1.5

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
@@ -78,7 +78,13 @@ async function runTool(call, otomatis) {
78
78
  }
79
79
  /** Start a fresh session (system prompt seeded from the mode). */
80
80
  export function newSession(model, mode, otomatis) {
81
- return { model, mode, otomatis, messages: [{ role: 'system', content: systemPrompt(mode) }] };
81
+ return {
82
+ model,
83
+ mode,
84
+ otomatis,
85
+ messages: [{ role: 'system', content: systemPrompt(mode) }],
86
+ teamModels: {},
87
+ };
82
88
  }
83
89
  /** Reset the conversation, keeping the current mode/model/otomatis. */
84
90
  export function resetSession(s) {
@@ -94,18 +100,27 @@ export async function runTurn(s, userText) {
94
100
  let lastText = '';
95
101
  for (let step = 0; step < MAX_STEPS; step++) {
96
102
  const stop = thinking();
103
+ let streamed = false;
97
104
  let reply;
98
105
  try {
99
- reply = await chatComplete(s.model, s.messages, tools);
106
+ // Stream the reply live: stop the spinner on the first token and print as it arrives.
107
+ reply = await chatComplete(s.model, s.messages, tools, (delta) => {
108
+ if (!streamed) {
109
+ stop();
110
+ streamed = true;
111
+ }
112
+ process.stdout.write(delta);
113
+ });
100
114
  }
101
115
  finally {
102
- stop();
116
+ if (!streamed)
117
+ stop();
103
118
  }
119
+ if (streamed)
120
+ process.stdout.write('\n');
104
121
  s.messages.push({ role: 'assistant', content: reply.content, tool_calls: reply.tool_calls });
105
- if (reply.content) {
106
- console.log(reply.content);
122
+ if (reply.content)
107
123
  lastText = reply.content;
108
- }
109
124
  const calls = reply.tool_calls ?? [];
110
125
  if (calls.length === 0) {
111
126
  if (step === 0) {
@@ -17,19 +17,20 @@ const ROLES = {
17
17
  brief: 'Peranmu: PENINJAU. Baca file yang relevan dan tinjau hasil implementasi: sebutkan masalah/risiko & saran perbaikan singkat.',
18
18
  },
19
19
  };
20
- /** Run one role as a sub-agent and return its final text. */
21
- async function runRole(role, task, opts) {
22
- console.log(`\n${bold(role.title)}`);
23
- const s = newSession(opts.model, role.mode, opts.otomatis);
20
+ /** Run one role as a sub-agent (with its own model) and return its final text. */
21
+ async function runRole(name, task, opts) {
22
+ const role = ROLES[name];
23
+ const model = opts.models[name];
24
+ console.log(`\n${bold(role.title)} ${dim(`(${model})`)}`);
25
+ const s = newSession(model, role.mode, opts.otomatis);
24
26
  s.messages.push({ role: 'system', content: role.brief });
25
27
  return runTurn(s, task);
26
28
  }
27
29
  /** Orchestrate the perencana → pengkode → peninjau pipeline for a task. */
28
30
  export async function runTeam(task, opts) {
29
31
  console.log(bold('👥 Tim agen: perencana → pengkode → peninjau'));
30
- console.log(dim(`Model: ${opts.model}`));
31
- const plan = await runRole(ROLES.perencana, task, opts);
32
- await runRole(ROLES.pengkode, `Tugas: ${task}\n\nRencana:\n${plan || '(tidak ada rencana eksplisit)'}`, opts);
33
- await runRole(ROLES.peninjau, `Tinjau hasil implementasi untuk tugas: ${task}`, opts);
32
+ const plan = await runRole('perencana', task, opts);
33
+ await runRole('pengkode', `Tugas: ${task}\n\nRencana:\n${plan || '(tidak ada rencana eksplisit)'}`, opts);
34
+ await runRole('peninjau', `Tinjau hasil implementasi untuk tugas: ${task}`, opts);
34
35
  console.log(green('\n✓ Tim selesai.'));
35
36
  }
package/dist/api.js CHANGED
@@ -91,33 +91,73 @@ export async function createTopup(amount) {
91
91
  const res = await authedFetch('/v1/topup', { method: 'POST', body: JSON.stringify({ amount }) });
92
92
  return res.json();
93
93
  }
94
- /** POST /v1/chat/completions (non-stream) with tools — returns the assistant message (for the agent). */
95
- export async function chatComplete(model, messages, tools) {
96
- const { baseUrl, apiKey } = loadConfig();
97
- if (!apiKey)
98
- throw new Error('Belum masuk. Jalankan: kiosapi masuk');
99
- const res = await fetch(`${baseUrl}/v1/chat/completions`, {
94
+ /**
95
+ * One agent step: POST /v1/chat/completions with tools, STREAMED and reassembled into a single
96
+ * assistant reply (content + tool_calls). Streaming keeps bytes flowing so a long "thinking" pause
97
+ * doesn't hit the Worker's sync-request timeout (which surfaced as HTTP 502). An optional onText
98
+ * callback receives content deltas live.
99
+ */
100
+ export async function chatComplete(model, messages, tools, onText) {
101
+ const res = await authedFetch('/v1/chat/completions', {
100
102
  method: 'POST',
101
- headers: {
102
- Authorization: `Bearer ${apiKey}`,
103
- 'Content-Type': 'application/json',
104
- 'X-Title': 'kiosapi-cli',
105
- },
103
+ headers: { 'X-Title': 'kiosapi-cli' },
106
104
  body: JSON.stringify({
107
105
  model,
108
106
  messages,
109
107
  tools,
110
108
  tool_choice: tools && tools.length > 0 ? 'auto' : undefined,
111
- stream: false,
109
+ stream: true,
112
110
  }),
113
111
  });
114
- if (!res.ok)
115
- throw new Error(humanizeError(res.status));
116
- const body = (await res.json());
117
- const msg = body.choices?.[0]?.message;
118
- if (!msg)
119
- throw new Error('Respons model kosong.');
120
- return { content: msg.content ?? null, tool_calls: msg.tool_calls };
112
+ if (!res.body)
113
+ throw new Error('Respons kosong dari server.');
114
+ const reader = res.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ let buffer = '';
117
+ let content = '';
118
+ const calls = new Map();
119
+ while (true) {
120
+ const { done, value } = await reader.read();
121
+ if (done)
122
+ break;
123
+ buffer += decoder.decode(value, { stream: true });
124
+ const lines = buffer.split('\n');
125
+ buffer = lines.pop() ?? '';
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed.startsWith('data:'))
129
+ continue;
130
+ const payload = trimmed.slice(5).trim();
131
+ if (payload === '[DONE]')
132
+ continue;
133
+ try {
134
+ const delta = JSON.parse(payload).choices?.[0]?.delta;
135
+ if (!delta)
136
+ continue;
137
+ if (delta.content) {
138
+ content += delta.content;
139
+ onText?.(delta.content);
140
+ }
141
+ for (const tc of delta.tool_calls ?? []) {
142
+ const cur = calls.get(tc.index) ?? { id: '', name: '', args: '' };
143
+ if (tc.id)
144
+ cur.id = tc.id;
145
+ if (tc.function?.name)
146
+ cur.name = tc.function.name;
147
+ if (tc.function?.arguments)
148
+ cur.args += tc.function.arguments;
149
+ calls.set(tc.index, cur);
150
+ }
151
+ }
152
+ catch {
153
+ // ignore keep-alive / non-JSON lines
154
+ }
155
+ }
156
+ }
157
+ const tool_calls = [...calls.values()]
158
+ .filter((c) => c.name)
159
+ .map((c) => ({ id: c.id, function: { name: c.name, arguments: c.args } }));
160
+ return { content: content || null, tool_calls: tool_calls.length > 0 ? tool_calls : undefined };
121
161
  }
122
162
  /** Parse an OpenAI SSE stream, yielding content deltas. */
123
163
  async function* consumeSSE(res) {
package/dist/commands.js CHANGED
@@ -404,19 +404,34 @@ export async function cmdLihat(args) {
404
404
  stop();
405
405
  process.stdout.write('\n');
406
406
  }
407
- /** tim — multi-agent pipeline (perencana → pengkode → peninjau). */
407
+ /**
408
+ * tim — multi-agent pipeline (perencana → pengkode → peninjau). Each role can use a different model
409
+ * via --perencana/--pengkode/--peninjau; roles without an override fall back to -m / the default.
410
+ */
408
411
  export async function cmdTim(args) {
409
412
  const { values, positionals } = parseArgs({
410
413
  args,
411
- options: { model: { type: 'string', short: 'm' }, otomatis: { type: 'boolean' } },
414
+ options: {
415
+ model: { type: 'string', short: 'm' },
416
+ otomatis: { type: 'boolean' },
417
+ perencana: { type: 'string' },
418
+ pengkode: { type: 'string' },
419
+ peninjau: { type: 'string' },
420
+ },
412
421
  allowPositionals: true,
413
422
  });
414
423
  const task = positionals.join(' ').trim();
415
424
  if (!task)
416
425
  throw new Error('Beri tugas. Contoh: kiosapi tim "bikin endpoint /health + tes"');
417
- const model = await resolveModel(values.model);
418
- await warnIfNoTools(model);
419
- await runTeam(task, { model, otomatis: Boolean(values.otomatis) });
426
+ const base = await resolveModel(values.model);
427
+ const models = {
428
+ perencana: values.perencana ?? base,
429
+ pengkode: values.pengkode ?? base,
430
+ peninjau: values.peninjau ?? base,
431
+ };
432
+ for (const m of new Set(Object.values(models)))
433
+ await warnIfNoTools(m);
434
+ await runTeam(task, { models, otomatis: Boolean(values.otomatis) });
420
435
  }
421
436
  /** saldo — show own balance, bonus tokens, and month-to-date spend. */
422
437
  export async function cmdSaldo() {
package/dist/session.js CHANGED
@@ -19,6 +19,7 @@ ${dim('Ketik tugasmu langsung. Perintah meta diawali "/". /bantuan untuk daftar,
19
19
  function slashHelp() {
20
20
  console.log(`${bold('Perintah sesi:')}
21
21
  /tim <tugas> Multi-agen (perencana→pengkode→peninjau)
22
+ /peran [peran] [model] Atur model per peran tim (atau lihat/reset)
22
23
  /gambar <prompt> Buat gambar → file
23
24
  /video <prompt> Buat video → file
24
25
  /lihat <file> <pertanyaan> Tanya model vision tentang gambar
@@ -99,7 +100,42 @@ async function runSlash(line, s) {
99
100
  if (!task)
100
101
  console.log(red('Beri tugas. Contoh: /tim bikin endpoint /health + tes'));
101
102
  else
102
- await runTeam(task, { model: s.model, otomatis: s.otomatis });
103
+ await runTeam(task, {
104
+ models: {
105
+ perencana: s.teamModels.perencana ?? s.model,
106
+ pengkode: s.teamModels.pengkode ?? s.model,
107
+ peninjau: s.teamModels.peninjau ?? s.model,
108
+ },
109
+ otomatis: s.otomatis,
110
+ });
111
+ return false;
112
+ }
113
+ case 'peran': {
114
+ const [role, ...mrest] = rest;
115
+ const names = ['perencana', 'pengkode', 'peninjau'];
116
+ if (!role) {
117
+ console.log(dim('Model per peran tim:'));
118
+ for (const r of names)
119
+ console.log(` ${r}: ${s.teamModels[r] ?? `${s.model} (default)`}`);
120
+ console.log(dim('Set: /peran <peran> <model> · reset: /peran reset'));
121
+ }
122
+ else if (role === 'reset') {
123
+ s.teamModels = {};
124
+ console.log(dim('Model per peran direset (pakai model sesi).'));
125
+ }
126
+ else if (names.includes(role)) {
127
+ const m = mrest.join(' ').trim();
128
+ if (m) {
129
+ s.teamModels[role] = m;
130
+ console.log(dim(`${role} → ${m}`));
131
+ }
132
+ else {
133
+ console.log(dim(`${role}: ${s.teamModels[role] ?? `${s.model} (default)`}`));
134
+ }
135
+ }
136
+ else {
137
+ console.log(red(`Peran tak dikenal: ${role} (perencana|pengkode|peninjau)`));
138
+ }
103
139
  return false;
104
140
  }
105
141
  case 'gambar':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiosapi",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "CLI Kiosapi.id berbahasa Indonesia — bangun aplikasimu pakai API key Kiosapi (agen + multimodal).",
6
6
  "keywords": [
@@ -28,7 +28,10 @@
28
28
  "bin": {
29
29
  "kiosapi": "./dist/index.js"
30
30
  },
31
- "files": ["dist", "README.md"],
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
32
35
  "engines": {
33
36
  "node": ">=20"
34
37
  },