luang-cli 0.0.0-stage → 0.1.0
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/README.md +198 -2
- package/bin/luang-cli.js +247 -0
- package/lib/agent.js +424 -0
- package/lib/commands.js +337 -0
- package/lib/core.js +442 -0
- package/package.json +17 -4
package/lib/agent.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// lib/agent.js — daemon agent, task list/create/status, status ringkas,
|
|
4
|
+
// dan mcp-config. Bagian lanjutan dari lib/commands.js.
|
|
5
|
+
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const core = require('./core');
|
|
9
|
+
const c = require('./commands');
|
|
10
|
+
const {
|
|
11
|
+
ApiError,
|
|
12
|
+
apiRequest,
|
|
13
|
+
loadState,
|
|
14
|
+
saveState,
|
|
15
|
+
collectProviders,
|
|
16
|
+
DEFAULT_TEMPLATES,
|
|
17
|
+
STATE_FILE,
|
|
18
|
+
CONFIG_DIR,
|
|
19
|
+
fmtWhen,
|
|
20
|
+
keyPrefix,
|
|
21
|
+
trunc,
|
|
22
|
+
printTable,
|
|
23
|
+
sleep,
|
|
24
|
+
} = core;
|
|
25
|
+
|
|
26
|
+
const STATUS_LABELS = { todo: 'To-Do', in_progress: 'In Progress', review: 'Review', done: 'Done' };
|
|
27
|
+
|
|
28
|
+
// ── agent (daemon) ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function stampNow() {
|
|
31
|
+
const d = new Date();
|
|
32
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
33
|
+
return `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deteksi perubahan lebih luas dari updated_at saja: status, agent_status, note & waktu.
|
|
37
|
+
function taskIdFingerprint(t) {
|
|
38
|
+
return JSON.stringify([t.status || '', t.agent_status || '', t.agent_progress_note || '', t.agent_updated_at || '', t.updated_at || '', t.title || '']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printTaskLine(t, tag) {
|
|
42
|
+
const code = c.taskIdOf(t) || '?';
|
|
43
|
+
let tail = '';
|
|
44
|
+
if (t.agent_status) tail += `agent=${t.agent_status}`;
|
|
45
|
+
if (t.agent_progress_note) tail += (tail ? ' ' : '') + `note="${t.agent_progress_note}"`;
|
|
46
|
+
console.log(`[${stampNow()}] ${code} ${trunc(t.title, 40)} ${t.status || '-'} ${tail}${tag ? ` ${trunc(tag, 20)}` : ''}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildExecCommand(tpl, taskText) {
|
|
50
|
+
if (!tpl || !tpl.includes('{task}')) return null;
|
|
51
|
+
let q;
|
|
52
|
+
if (tpl.includes('"{task}"')) {
|
|
53
|
+
q = taskText.replace(/(["`\\$])/g, '\\$1'); // escape untuk kutip ganda
|
|
54
|
+
} else if (tpl.includes("'{task}'")) {
|
|
55
|
+
q = taskText.replace(/'/g, `'\\''`); // escape untuk kutip tunggal
|
|
56
|
+
} else {
|
|
57
|
+
q = taskText.replace(/(["`\\$\n])/g, ' '); // {task} tanpa kutip → rapikan
|
|
58
|
+
}
|
|
59
|
+
return tpl.replaceAll('{task}', q);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runExecChild(cmd, setChild) {
|
|
63
|
+
const EXEC_TIMEOUT_MS = 15 * 60 * 1000; // exec dibatasi 15 menit agar daemon tidak menggantung
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let child = null;
|
|
66
|
+
try {
|
|
67
|
+
child = exec(cmd, { maxBuffer: 8 * 1024 * 1024, timeout: EXEC_TIMEOUT_MS, windowsHide: true });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
resolve(1);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (setChild) setChild(child);
|
|
73
|
+
const fin = (code) => {
|
|
74
|
+
if (setChild) setChild(null);
|
|
75
|
+
resolve(typeof code === 'number' ? code : 1);
|
|
76
|
+
};
|
|
77
|
+
child.once('close', (code) => fin(code));
|
|
78
|
+
child.once('error', () => fin(1));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function tryExecTask(rt, tasks, state, execCfg, current) {
|
|
83
|
+
const cand = tasks.find((t) => c.statusInputNorm(t.status) === 'todo' && !state.executed[c.taskIdOf(t)]);
|
|
84
|
+
if (!cand) return false;
|
|
85
|
+
const code = c.taskIdOf(cand);
|
|
86
|
+
const taskText = `${cand.title || ''}${cand.description ? ' — ' + cand.description : ''}`;
|
|
87
|
+
const cmdStr = buildExecCommand(execCfg.template, taskText);
|
|
88
|
+
if (!cmdStr) {
|
|
89
|
+
// exec_template kosong / tanpa {task} → hanya notifikasi satu baris (spec)
|
|
90
|
+
state.executed[code] = true; // tandai agar tidak nag lagi
|
|
91
|
+
saveState(state);
|
|
92
|
+
console.log(
|
|
93
|
+
execCfg.template
|
|
94
|
+
? `⏭ ${code} menunggu (To-Do) — exec template "${trunc(execCfg.template, 50)}" tidak memuat {task}, eksekusi dilewati.`
|
|
95
|
+
: `⏭ ${code} menunggu (To-Do) — default provider belum punya exec_template; notifikasi saja.`,
|
|
96
|
+
);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(`🎯 Eksekusi tugas: ${code} — ${trunc(cand.title, 60)}`);
|
|
101
|
+
console.log(` Provider : ${execCfg.type}`);
|
|
102
|
+
console.log(` Perintah : ${trunc(cmdStr, 90)}`);
|
|
103
|
+
const ans = await c.ask(' Jalankan sekarang? (y/N) ');
|
|
104
|
+
state.executed[code] = true;
|
|
105
|
+
if (ans !== 'y') {
|
|
106
|
+
console.log(' Dilewati (tidak dijalankan).');
|
|
107
|
+
saveState(state);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
// Spesifikasi: mulai → body {status:"in_progress"} saja.
|
|
111
|
+
try {
|
|
112
|
+
await apiRequest(rt, 'POST', `/api/v1/agent/tasks/${encodeURIComponent(code)}/status`, {
|
|
113
|
+
body: { status: 'in_progress' },
|
|
114
|
+
});
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error(`⚠️ Gagal set in_progress: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
const exitCode = await runExecChild(cmdStr, (child) => {
|
|
119
|
+
current.child = child;
|
|
120
|
+
});
|
|
121
|
+
const ok = exitCode === 0;
|
|
122
|
+
// Spesifikasi: exit 0 → {status:"done", agent_status:"selesai", progress_note:"exec selesai"};
|
|
123
|
+
// selain itu → {status:"review", agent_status:"gagal"}.
|
|
124
|
+
try {
|
|
125
|
+
await apiRequest(rt, 'POST', `/api/v1/agent/tasks/${encodeURIComponent(code)}/status`, {
|
|
126
|
+
body: ok
|
|
127
|
+
? { status: 'done', agent_status: 'selesai', progress_note: 'exec selesai' }
|
|
128
|
+
: { status: 'review', agent_status: 'gagal' },
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error(`⚠️ Gagal update status tugas: ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
console.log(ok ? `✅ ${code} selesai (exit 0)` : `⚠️ ${code} gagal (exit ${exitCode}) → review`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function cmdAgent(rt, args) {
|
|
138
|
+
if (!rt.key) return c.errExit('Belum login — jalankan `luang-cli login` atau set LUANG_KEY.');
|
|
139
|
+
const flags = args.flags;
|
|
140
|
+
let intervalSec = Number(flags.interval);
|
|
141
|
+
if (!intervalSec || intervalSec < 1) intervalSec = 5;
|
|
142
|
+
if (intervalSec > 3600) intervalSec = 3600;
|
|
143
|
+
const doExec = flags.exec === true;
|
|
144
|
+
|
|
145
|
+
const execCfg = { type: rt.cfg.default_provider, template: null };
|
|
146
|
+
if (doExec) {
|
|
147
|
+
const p = collectProviders(rt.cfg).find((x) => x.provider_type === rt.cfg.default_provider);
|
|
148
|
+
if (p && p.exec_template && p.exec_template.includes('{task}')) {
|
|
149
|
+
execCfg.template = p.exec_template;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const state = loadState();
|
|
154
|
+
const current = { child: null };
|
|
155
|
+
let running = true;
|
|
156
|
+
|
|
157
|
+
console.log('🤖 Agent Luang aktif');
|
|
158
|
+
console.log(` API : ${rt.apiUrl}`);
|
|
159
|
+
console.log(` Poll : setiap ${intervalSec} detik · heartbeat setiap 30 detik`);
|
|
160
|
+
console.log(` Eksekusi : ${doExec ? (execCfg.template ? `aktif (${execCfg.type})` : 'diminta tapi nonaktif — default provider belum punya exec_template berisi {task}') : 'nonaktif'}`);
|
|
161
|
+
console.log(` State : ${STATE_FILE}`);
|
|
162
|
+
console.log(' Hentikan : Ctrl+C');
|
|
163
|
+
console.log('');
|
|
164
|
+
|
|
165
|
+
process.on('SIGINT', () => {
|
|
166
|
+
running = false;
|
|
167
|
+
if (current.child) {
|
|
168
|
+
try {
|
|
169
|
+
current.child.kill('SIGINT');
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// abaikan
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
console.log('\n👋 Agent dihentikan.');
|
|
175
|
+
process.exit(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const heartbeat = async (label) => {
|
|
179
|
+
try {
|
|
180
|
+
await apiRequest(rt, 'POST', '/api/v1/agent/heartbeat', {
|
|
181
|
+
body: { providers: core.heartbeatProviders(collectProviders(rt.cfg)) },
|
|
182
|
+
});
|
|
183
|
+
console.log(`📡 ${label} terkirim`);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error(`⚠️ Heartbeat gagal: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const hbInterval = setInterval(() => {
|
|
190
|
+
if (running) heartbeat('Heartbeat');
|
|
191
|
+
}, 30000);
|
|
192
|
+
|
|
193
|
+
let first = true;
|
|
194
|
+
while (running) {
|
|
195
|
+
let tasks;
|
|
196
|
+
try {
|
|
197
|
+
const res = await apiRequest(rt, 'GET', '/api/v1/agent/tasks');
|
|
198
|
+
tasks = c.normalizeTaskList(res);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
|
|
201
|
+
clearInterval(hbInterval);
|
|
202
|
+
console.error(`❌ Key ditolak server (HTTP ${e.status}): ${e.message}`);
|
|
203
|
+
console.error(' Cek: luang-cli whoami · login ulang: luang-cli login');
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
console.error(`⚠️ Poll gagal: ${e.message} — coba lagi dalam ${intervalSec} detik…`);
|
|
207
|
+
await sleep(intervalSec * 1000);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let dirty = false;
|
|
212
|
+
const seen = new Set();
|
|
213
|
+
for (const t of tasks) {
|
|
214
|
+
const id = c.taskIdOf(t);
|
|
215
|
+
if (!id) continue;
|
|
216
|
+
seen.add(id);
|
|
217
|
+
const cur = t.agent_updated_at || t.updated_at || null;
|
|
218
|
+
const fp = taskIdFingerprint(t);
|
|
219
|
+
const prev = state.tasks[id];
|
|
220
|
+
// berubah bila tidak ada cache, atau fingerprint (status/agent_status/note/waktu) berbeda
|
|
221
|
+
const changed =
|
|
222
|
+
!prev ||
|
|
223
|
+
prev.fp !== fp ||
|
|
224
|
+
(!prev.fp && prev.updated_at !== cur);
|
|
225
|
+
if (changed) {
|
|
226
|
+
state.tasks[id] = { updated_at: cur, fp, title: t.title, status: t.status };
|
|
227
|
+
dirty = true;
|
|
228
|
+
printTaskLine(t, first ? 'baru dipantau' : 'berubah');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// bersihkan cache untuk tugas yang tak lagi dikirim server
|
|
232
|
+
for (const id of Object.keys(state.tasks)) {
|
|
233
|
+
if (!seen.has(id)) {
|
|
234
|
+
delete state.tasks[id];
|
|
235
|
+
dirty = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
first = false;
|
|
239
|
+
if (dirty) saveState(state);
|
|
240
|
+
|
|
241
|
+
if (doExec) {
|
|
242
|
+
const acted = await tryExecTask(rt, tasks, state, execCfg, current);
|
|
243
|
+
if (acted) saveState(state);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await sleep(intervalSec * 1000);
|
|
247
|
+
}
|
|
248
|
+
clearInterval(hbInterval);
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── task list / create / status ──────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
async function cmdTaskList(rt, args) {
|
|
255
|
+
if (!rt.key) return c.errExit('Belum login — jalankan `luang-cli login` atau set LUANG_KEY.');
|
|
256
|
+
let items;
|
|
257
|
+
try {
|
|
258
|
+
items = c.normalizeTaskList(await apiRequest(rt, 'GET', '/api/v1/agent/tasks'));
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return c.authFailMessage(e);
|
|
261
|
+
}
|
|
262
|
+
const stArg = args.flags.status && args.flags.status !== true ? args.flags.status : null;
|
|
263
|
+
if (stArg) {
|
|
264
|
+
const want = c.statusInputNorm(stArg);
|
|
265
|
+
if (!['todo', 'inprogress', 'review', 'done'].includes(want)) {
|
|
266
|
+
return c.errExit(`--status tidak valid: ${stArg}`, 'Pilihan: todo | in_progress | review | done');
|
|
267
|
+
}
|
|
268
|
+
items = items.filter((t) => c.statusInputNorm(t.status) === want);
|
|
269
|
+
}
|
|
270
|
+
if (!items.length) {
|
|
271
|
+
console.log(stArg ? `📭 Tidak ada tugas berstatus "${stArg}".` : '📭 Tidak ada tugas.');
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
printTable(
|
|
275
|
+
['TASKCODE', 'JUDUL', 'STATUS', 'AGENT STATUS', 'UPDATED'],
|
|
276
|
+
items.map((t) => [
|
|
277
|
+
c.taskIdOf(t) || '-',
|
|
278
|
+
trunc(t.title, 40),
|
|
279
|
+
t.status || '-',
|
|
280
|
+
t.agent_status || '-',
|
|
281
|
+
fmtWhen(t.agent_updated_at || t.updated_at),
|
|
282
|
+
])
|
|
283
|
+
);
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(`${items.length} tugas${stArg ? ` (filter ${stArg})` : ''} · update: luang-cli task status <TGS-n> <status>`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function cmdTaskCreate(rt, args) {
|
|
290
|
+
if (!rt.key) return c.errExit('Belum login — jalankan `luang-cli login` atau set LUANG_KEY.');
|
|
291
|
+
const f = args.flags;
|
|
292
|
+
const title = f.title && f.title !== true ? String(f.title).trim() : '';
|
|
293
|
+
if (!title) {
|
|
294
|
+
console.error('Format: luang-cli task create --title "Judul" [--desc "…"] [--priority high|medium|low] [--due 2026-01-02]');
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
const desc = f.desc && f.desc !== true ? String(f.desc) : f.description && f.description !== true ? String(f.description) : undefined;
|
|
298
|
+
const priority = f.priority && f.priority !== true ? String(f.priority).toLowerCase() : undefined;
|
|
299
|
+
if (priority && !['high', 'medium', 'low'].includes(priority)) {
|
|
300
|
+
return c.errExit(`--priority tidak valid: ${priority}`, 'Pilihan: high | medium | low');
|
|
301
|
+
}
|
|
302
|
+
const due = f.due && f.due !== true ? String(f.due).trim() : undefined;
|
|
303
|
+
if (due && !/^\d{4}-\d{2}-\d{2}$/.test(due)) {
|
|
304
|
+
return c.errExit(`--due tidak valid: ${due}`, 'Format tanggal: YYYY-MM-DD, contoh --due 2026-01-02');
|
|
305
|
+
}
|
|
306
|
+
const body = { title };
|
|
307
|
+
if (desc !== undefined && desc !== '') body.description = desc;
|
|
308
|
+
if (priority) body.priority = priority;
|
|
309
|
+
if (due) body.due_date = due;
|
|
310
|
+
let res;
|
|
311
|
+
try {
|
|
312
|
+
res = await apiRequest(rt, 'POST', '/api/v1/agent/tasks', { body });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
if (e instanceof ApiError && (e.status === 403 || /forbid/i.test(e.message))) {
|
|
315
|
+
console.error('❌ Tidak diizinkan membuat tugas. Izin can_create_tasks belum diberikan admin.');
|
|
316
|
+
console.error(' Minta admin aktifkan izin "Bisa-buat-tugas" untuk agent ini di dashboard (Agents → izin Bisa-buat-tugas).');
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
return c.authFailMessage(e);
|
|
320
|
+
}
|
|
321
|
+
const d = (res && res.data) || {};
|
|
322
|
+
console.log(`✅ Tugas dibuat: ${d.taskCode || d.task_code || '-'} — ${title}`);
|
|
323
|
+
if (priority) console.log(` Prioritas : ${priority}`);
|
|
324
|
+
if (due) console.log(` Jatuh tempo: ${due}`);
|
|
325
|
+
if (desc) console.log(` Deskripsi : ${trunc(desc, 80)}`);
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function cmdTaskStatus(rt, args) {
|
|
330
|
+
if (!rt.key) return c.errExit('Belum login — jalankan `luang-cli login` atau set LUANG_KEY.');
|
|
331
|
+
const ref = args.pos[2];
|
|
332
|
+
const st = args.pos[3];
|
|
333
|
+
if (!ref || !st) {
|
|
334
|
+
console.error('Format: luang-cli task status <TGS-12|uuid> <todo|in_progress|review|done> [--note "…"] [--agent-status "…"]');
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
if (!STATUS_LABELS[st]) {
|
|
338
|
+
return c.errExit(`Status tidak valid: ${st}`, `Pilihan: ${Object.keys(STATUS_LABELS).join(' | ')}`);
|
|
339
|
+
}
|
|
340
|
+
const body = { status: st };
|
|
341
|
+
const note = args.flags.note && args.flags.note !== true ? String(args.flags.note) : undefined;
|
|
342
|
+
const agentStatus = args.flags['agent-status'] && args.flags['agent-status'] !== true ? String(args.flags['agent-status']) : undefined;
|
|
343
|
+
if (note !== undefined) body.progress_note = note;
|
|
344
|
+
if (agentStatus !== undefined) body.agent_status = agentStatus;
|
|
345
|
+
try {
|
|
346
|
+
await apiRequest(rt, 'POST', `/api/v1/agent/tasks/${encodeURIComponent(ref)}/status`, { body });
|
|
347
|
+
} catch (e) {
|
|
348
|
+
if (e instanceof ApiError && e.status === 403) {
|
|
349
|
+
console.error('❌ Tidak diizinkan update tugas. Izin can_update_tasks belum diberikan admin (Agents → izin Bisa-update-tugas).');
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
return c.authFailMessage(e);
|
|
353
|
+
}
|
|
354
|
+
console.log(`✅ ${ref} → ${STATUS_LABELS[st]}`);
|
|
355
|
+
if (agentStatus !== undefined) console.log(` Agent status : ${agentStatus}`);
|
|
356
|
+
if (note !== undefined) console.log(` Progres : ${trunc(note, 80)}`);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function cmdTask(rt, args) {
|
|
361
|
+
const sub = args.pos[1] || '';
|
|
362
|
+
if (sub === 'list') return cmdTaskList(rt, args);
|
|
363
|
+
if (sub === 'create') return cmdTaskCreate(rt, args);
|
|
364
|
+
if (sub === 'status') return cmdTaskStatus(rt, args);
|
|
365
|
+
console.error('Format: luang-cli task list [--status todo|in_progress|review|done]');
|
|
366
|
+
console.error(' luang-cli task create --title "…" [--desc …] [--priority …] [--due YYYY-MM-DD]');
|
|
367
|
+
console.error(' luang-cli task status <TGS-12|uuid> <todo|in_progress|review|done> [--note …] [--agent-status …]');
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── status (ringkasan) ───────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
async function cmdStatus(rt) {
|
|
374
|
+
console.log('🔎 Status luang-cli');
|
|
375
|
+
console.log(` API : ${rt.apiUrl}`);
|
|
376
|
+
console.log(` App : ${rt.appUrl}`);
|
|
377
|
+
console.log(` Key : ${rt.key ? keyPrefix(rt.key) : '(belum login)'}`);
|
|
378
|
+
if (process.env.LUANG_KEY && !rt.cfg.key) console.log(' ^ dari env LUANG_KEY');
|
|
379
|
+
if (!rt.key) {
|
|
380
|
+
const list = collectProviders(rt.cfg);
|
|
381
|
+
console.log(` 🛠 Provider lokal terdeteksi: ${list.length ? list.map((p) => p.provider_type).join(', ') : '(tidak ada)'}`);
|
|
382
|
+
console.log(' ➡️ Mulai: luang-cli login');
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const me = await apiRequest(rt, 'GET', '/api/v1/agent/me');
|
|
387
|
+
const d = (me && me.data) || {};
|
|
388
|
+
console.log(` Sesi : ${d.name || '-'} (${d.key_id || 'key-?'}) · status ${d.status || '-'}`);
|
|
389
|
+
console.log(` Tenant: ${d.tenant_slug || d.tenant_id || '-'}`);
|
|
390
|
+
console.log(` Izin : update-tugas ${d.can_update_tasks ? '✅' : '❌'} · buat-tugas ${d.can_create_tasks ? '✅' : '❌'}`);
|
|
391
|
+
console.log(` Provider server: ${c.providersLabel(d.providers)}`);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
console.log(` ⚠️ /me gagal: ${e.message}`);
|
|
394
|
+
}
|
|
395
|
+
const list = collectProviders(rt.cfg);
|
|
396
|
+
console.log(` Default provider: ${rt.cfg.default_provider || '(belum di-set)'}`);
|
|
397
|
+
console.log(` Provider lokal : ${list.length ? list.map((p) => (p.is_default ? '★' + p.provider_type : p.provider_type)).join(', ') : '(tidak ada)'}`);
|
|
398
|
+
console.log(` Config/state : ${CONFIG_DIR.replace(process.env.HOME || '~', '~')}`);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── mcp-config ───────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
async function cmdMcpConfig(rt) {
|
|
405
|
+
const url = `${String(rt.apiUrl).replace(/\/+$/, '')}/api/v1/agent/mcp`;
|
|
406
|
+
const snippet = {
|
|
407
|
+
mcpServers: {
|
|
408
|
+
luang: {
|
|
409
|
+
type: 'http',
|
|
410
|
+
url,
|
|
411
|
+
headers: { 'X-CLI-Key': rt.key || 'lk_xxxxxxxx' },
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
console.log('Snippet konfigurasi MCP (endpoint POST /api/v1/agent/mcp, header X-CLI-Key):');
|
|
416
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
417
|
+
if (!rt.key) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log('⚠️ Ganti "lk_xxxxxxxx" dengan key asli — jalankan luang-cli login dulu.');
|
|
420
|
+
}
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = { cmdAgent, cmdTask, cmdStatus, cmdMcpConfig, buildExecCommand, runExecChild };
|