luang-cli 0.2.0 → 0.3.2
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 +150 -21
- package/bin/luang-cli.js +118 -19
- package/lib/agent.js +869 -81
- package/lib/commands.js +225 -3
- package/lib/core.js +38 -0
- package/lib/models.js +343 -0
- package/lib/ws.js +552 -0
- package/package.json +4 -1
- package/test/bin/opencode +10 -0
- package/test/http-stub.js +77 -0
- package/test/run-e2e.js +806 -0
- package/test/ws-stub.js +202 -0
package/lib/commands.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// lib/commands.js — implementasi semua command luang-cli.
|
|
4
4
|
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
6
7
|
const readline = require('readline');
|
|
7
8
|
|
|
8
9
|
const core = require('./core');
|
|
@@ -14,6 +15,8 @@ const {
|
|
|
14
15
|
saveConfig,
|
|
15
16
|
collectProviders,
|
|
16
17
|
heartbeatProviders,
|
|
18
|
+
resolveWorkspace,
|
|
19
|
+
modelsForProvider,
|
|
17
20
|
detectProviders,
|
|
18
21
|
PROVIDER_TYPES,
|
|
19
22
|
DISPLAY_NAMES,
|
|
@@ -94,6 +97,129 @@ function permLine(label, val) {
|
|
|
94
97
|
return `${val ? '✅' : '❌'} ${label}`;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
// ── Telemetry (konsent ganda: lokal config + flag server) ────────────────────
|
|
101
|
+
|
|
102
|
+
// Label satu baris status konsent ganda. serverOn: true/false/null (tak tahu).
|
|
103
|
+
function telemetryConsentLabel(localOn, serverOn) {
|
|
104
|
+
const l = localOn ? 'lokal ✅' : 'lokal ❌';
|
|
105
|
+
const s = serverOn === true ? 'server ✅' : serverOn === false ? 'server ❌' : 'server ?';
|
|
106
|
+
const on = localOn && serverOn === true;
|
|
107
|
+
return `${l} · ${s} → efektif ${on ? '✅ DIKIRIM' : '❌ tidak dikirim'}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function cmdTelemetry(rt, args) {
|
|
111
|
+
const sub = String(args.pos[1] || 'status').toLowerCase();
|
|
112
|
+
if (sub === 'on') {
|
|
113
|
+
rt.cfg.telemetry = true;
|
|
114
|
+
saveConfig(rt.cfg);
|
|
115
|
+
console.log('✅ Telemetry lokal: ON (config.json telemetry=true)');
|
|
116
|
+
console.log('⚡ Kirim hanya setelah flag server ikut ON (wajib dua-duanya).');
|
|
117
|
+
} else if (sub === 'off') {
|
|
118
|
+
rt.cfg.telemetry = false;
|
|
119
|
+
saveConfig(rt.cfg);
|
|
120
|
+
console.log('✅ Telemetry lokal: OFF');
|
|
121
|
+
console.log('🤐 Data run (telemetry.run) TIDAK akan dikirim dari mesin ini.');
|
|
122
|
+
} else if (sub !== 'status') {
|
|
123
|
+
return errExit('Pemakaian: luang-cli telemetry on|off|status');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Flag server: agent_keys.telemetry_enabled (diubah dari dashboard).
|
|
127
|
+
let serverOn = null;
|
|
128
|
+
let note = null;
|
|
129
|
+
if (rt.key) {
|
|
130
|
+
try {
|
|
131
|
+
const me = await apiRequest(rt, 'GET', '/api/v1/agent/me');
|
|
132
|
+
serverOn = ((me && me.data) || {}).telemetry_enabled === true;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
note = e.message;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
note = 'belum login — flag server tidak bisa diperiksa (luang-cli login)';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('📊 Telemetry — konsent ganda (opt-in, bukan opt-out)');
|
|
142
|
+
console.log(` Lokal : config.telemetry = ${rt.cfg.telemetry === true ? 'true ✅' : 'false ❌'} (${CONFIG_FILE})`);
|
|
143
|
+
if (serverOn === null) console.log(` Server : ? — ${note || 'gagal cek /api/v1/agent/me'}`);
|
|
144
|
+
else console.log(` Server : telemetry_enabled = ${serverOn ? 'true ✅' : 'false ❌'} (Agents → toggle Telemetry di dashboard)`);
|
|
145
|
+
console.log(` Efektif: ${telemetryConsentLabel(rt.cfg.telemetry === true, serverOn)}`);
|
|
146
|
+
console.log('');
|
|
147
|
+
if (rt.cfg.telemetry === true && serverOn !== true) {
|
|
148
|
+
console.log('ℹ️ Bebannya harus ON bersamaan: lokal di sini, server di dashboard.');
|
|
149
|
+
}
|
|
150
|
+
console.log('🔐 Kirim pakai header X-CLI-Key (bukan JWT) via frame WS telemetry.run.');
|
|
151
|
+
console.log(' Bila salah satu OFF → CLI tidak mengirim apa pun (dan server pun men-drop).');
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Workspace folder (folder kerja provider; sinkron dashboard ↔ CLI) ──────
|
|
156
|
+
// Server adalah sumber kebenaran (dashboard mengatur); CLI menyimpan salinan
|
|
157
|
+
// lokal + MEMAKAI sebagai cwd saat --exec. Endpoint CLI: POST /agent/workspace
|
|
158
|
+
// (X-CLI-Key, milik key sendiri) — dashboard memakai PUT /keys/:id/workspace (JWT).
|
|
159
|
+
async function workspaceShow(rt, serverVal) {
|
|
160
|
+
const local = typeof rt.cfg.workspace === 'string' ? rt.cfg.workspace.trim() : '';
|
|
161
|
+
const server = typeof serverVal === 'string' ? serverVal.trim() : '';
|
|
162
|
+
const effective = server || local;
|
|
163
|
+
const resolved = resolveWorkspace(effective);
|
|
164
|
+
let exists = null;
|
|
165
|
+
if (resolved) {
|
|
166
|
+
try {
|
|
167
|
+
exists = fs.statSync(resolved).isDirectory();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
exists = false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log('📁 Workspace folder — folder kerja provider agent (opencode, claude, …)');
|
|
174
|
+
console.log(` Server : ${server ? server : '(belum diset — atur di dashboard Agents atau: luang-cli workspace <path>)'}`);
|
|
175
|
+
console.log(` Lokal : ${local ? local : '(mengikuti server)'}`);
|
|
176
|
+
if (resolved) {
|
|
177
|
+
console.log(` Efektif: ${resolved}${exists === true ? '' : ' ⚠️ tidak ada/bukan folder → exec pakai folder saat ini'}`);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(` Efektif: folder saat ini (${process.cwd()})`);
|
|
180
|
+
}
|
|
181
|
+
console.log(' Atur : luang-cli workspace <path> · reset: luang-cli workspace --reset');
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function cmdWorkspace(rt, args) {
|
|
186
|
+
const raw = String(args.pos[1] || '').trim();
|
|
187
|
+
if (!rt.key) return errExit('Belum login — jalankan `luang-cli login` atau set LUANG_KEY.');
|
|
188
|
+
const flagReset = args.flags && (args.flags.reset === true || args.flags.reset === 'true' || args.flags.reset === '');
|
|
189
|
+
const reset = raw === 'reset' || raw === '-' || raw === '--reset' || flagReset;
|
|
190
|
+
if ((!raw || raw === 'status' || raw === 'show') && !reset) {
|
|
191
|
+
let serverVal = null;
|
|
192
|
+
try {
|
|
193
|
+
const me = await apiRequest(rt, 'GET', '/api/v1/agent/me');
|
|
194
|
+
serverVal = me && me.data ? me.data.workspace : null;
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.log(` ⚠️ /me gagal: ${e.message} — tampilkan nilai lokal saja.`);
|
|
197
|
+
}
|
|
198
|
+
return workspaceShow(rt, serverVal);
|
|
199
|
+
}
|
|
200
|
+
const value = reset ? '' : raw;
|
|
201
|
+
if (!reset) {
|
|
202
|
+
const resolved = resolveWorkspace(value);
|
|
203
|
+
if (!resolved) return errExit(`Path tidak valid: ${value}`);
|
|
204
|
+
try {
|
|
205
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
206
|
+
return errExit(`Bukan folder: ${resolved}`, 'Buat dulu foldernya, atau cek penulisannya.');
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return errExit(`Folder tidak ada: ${resolved}`, 'Buat dulu foldernya, atau cek penulisannya.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await apiRequest(rt, 'POST', '/api/v1/agent/workspace', { body: { workspace: value } });
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return errExit(`Gagal menyimpan ke server: ${e.message}`);
|
|
216
|
+
}
|
|
217
|
+
rt.cfg.workspace = value;
|
|
218
|
+
saveConfig(rt.cfg);
|
|
219
|
+
console.log(reset ? '📁 Workspace direset — exec memakai folder saat ini.' : `📁 Workspace disimpan: ${resolveWorkspace(value)} (server + lokal sinkron)`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
97
223
|
// ── login (device flow) ──────────────────────────────────────────────────────
|
|
98
224
|
|
|
99
225
|
async function cmdLogin(rt, args) {
|
|
@@ -211,6 +337,12 @@ async function cmdWhoami(rt) {
|
|
|
211
337
|
return authFailMessage(e);
|
|
212
338
|
}
|
|
213
339
|
const d = (me && me.data) || {};
|
|
340
|
+
// Sinkron default provider dari dashboard bila diset di sana.
|
|
341
|
+
const sdp = typeof d.default_provider === 'string' ? d.default_provider.trim() : '';
|
|
342
|
+
if (sdp && rt.cfg.default_provider !== sdp) {
|
|
343
|
+
rt.cfg.default_provider = sdp;
|
|
344
|
+
saveConfig(rt.cfg);
|
|
345
|
+
}
|
|
214
346
|
console.log('👤 Sesi Agent');
|
|
215
347
|
console.log(` Nama : ${d.name || '-'}`);
|
|
216
348
|
console.log(` Status : ${d.status || '-'}`);
|
|
@@ -222,7 +354,11 @@ async function cmdWhoami(rt) {
|
|
|
222
354
|
console.log(` ${permLine('Bisa update tugas (can_update_tasks)', !!d.can_update_tasks)}`);
|
|
223
355
|
console.log(` ${permLine('Bisa buat tugas (can_create_tasks)', !!d.can_create_tasks)}`);
|
|
224
356
|
console.log('');
|
|
357
|
+
console.log(`📊 Telemetry: ${telemetryConsentLabel(rt.cfg.telemetry === true, d.telemetry_enabled === true)}`);
|
|
358
|
+
console.log(' (ubah lokal: luang-cli telemetry on|off · flag server: dashboard Agents)');
|
|
359
|
+
console.log('');
|
|
225
360
|
console.log(`🛠 Provider terdaftar (server): ${providersLabel(d.providers)}`);
|
|
361
|
+
console.log(`⭐ Default provider: ${rt.cfg.default_provider || '(belum di-set)'}${sdp ? ' (disetel dashboard)' : ''}`);
|
|
226
362
|
return 0;
|
|
227
363
|
}
|
|
228
364
|
|
|
@@ -260,22 +396,24 @@ async function cmdProvider(rt, args) {
|
|
|
260
396
|
return 0;
|
|
261
397
|
}
|
|
262
398
|
printTable(
|
|
263
|
-
['Provider', 'Path', 'Exec Template'],
|
|
399
|
+
['Provider', 'Path', 'Exec Template', 'Model'],
|
|
264
400
|
list.map((p) => [
|
|
265
401
|
(p.is_default ? '★ ' : '') + p.provider_type,
|
|
266
402
|
p.path || '-',
|
|
267
403
|
p.exec_template || '-',
|
|
404
|
+
p.model || '-',
|
|
268
405
|
])
|
|
269
406
|
);
|
|
270
407
|
console.log('');
|
|
271
408
|
console.log('★ = default provider · deteksi: ~/.claude, ~/.codex, ~/.config/opencode/~/.opencode, ~/.gemini + which(1)');
|
|
409
|
+
console.log(' {model} di exec_template diisi dari kolom Model (atau env LUANG_MODEL) saat mode --exec.');
|
|
272
410
|
await syncIfKey(rt);
|
|
273
411
|
return 0;
|
|
274
412
|
}
|
|
275
413
|
|
|
276
414
|
if (sub === 'add') {
|
|
277
415
|
const type = args.pos[2];
|
|
278
|
-
if (!type) return errExit('Format: luang-cli provider add <type> [--path P] [--exec_template T]', `type: ${PROVIDER_TYPES.join(', ')}`);
|
|
416
|
+
if (!type) return errExit('Format: luang-cli provider add <type> [--path P] [--exec_template T] [--model M]', `type: ${PROVIDER_TYPES.join(', ')}`);
|
|
279
417
|
if (!PROVIDER_TYPES.includes(type)) return errExit(`Tipe tidak dikenal: ${type}`, `Pilihan: ${PROVIDER_TYPES.join(', ')}`);
|
|
280
418
|
const existing = collectProviders(rt.cfg).find((p) => p.provider_type === type) || {};
|
|
281
419
|
const tPath = flags.path && flags.path !== true ? String(flags.path) : existing.path || null;
|
|
@@ -283,16 +421,22 @@ async function cmdProvider(rt, args) {
|
|
|
283
421
|
flags.exec_template && flags.exec_template !== true
|
|
284
422
|
? String(flags.exec_template)
|
|
285
423
|
: existing.exec_template || DEFAULT_TEMPLATES[type] || null;
|
|
424
|
+
let tModel = flags.model && flags.model !== true ? String(flags.model).trim() : existing.model || null;
|
|
425
|
+
if (tModel && !/^[\w.:+/-]+$/.test(tModel)) {
|
|
426
|
+
return errExit(`--model tidak valid: ${tModel}`, 'Karakter yang diizinkan: huruf, angka, `.` `_` `:` `+` `-` `/`');
|
|
427
|
+
}
|
|
286
428
|
rt.cfg.providers[type] = {
|
|
287
429
|
provider_type: type,
|
|
288
430
|
display_name: DISPLAY_NAMES[type] || type,
|
|
289
431
|
path: tPath,
|
|
290
432
|
exec_template: tTpl,
|
|
433
|
+
...(tModel ? { model: tModel } : {}),
|
|
291
434
|
};
|
|
292
435
|
saveConfig(rt.cfg);
|
|
293
436
|
console.log(`➕ Provider ${type} tersimpan di config:`);
|
|
294
437
|
console.log(` Path : ${tPath || '-'}`);
|
|
295
438
|
console.log(` Exec Template : ${tTpl || '-'}`);
|
|
439
|
+
if (tModel) console.log(` Model : ${tModel}`);
|
|
296
440
|
if (!tTpl || !tTpl.includes('{task}')) {
|
|
297
441
|
console.log('⚠️ Exec template tanpa {task} — tugas tidak bisa dieksekusi otomatis.');
|
|
298
442
|
}
|
|
@@ -334,4 +478,82 @@ async function cmdProvider(rt, args) {
|
|
|
334
478
|
return errExit(`Sub-command provider tidak dikenal: ${sub}`, 'Gunakan: list | add | remove | default');
|
|
335
479
|
}
|
|
336
480
|
|
|
337
|
-
|
|
481
|
+
// ── models (daftar model per provider) ───────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
// Lebar kolom daftar model & maks baris wrap (spec: maks 6 baris + "…+n lagi").
|
|
484
|
+
const MODELS_LIST_WIDTH = 44;
|
|
485
|
+
const MODELS_MAX_LINES = 6;
|
|
486
|
+
|
|
487
|
+
function wrapModelList(models, width, maxLines) {
|
|
488
|
+
const rows = [];
|
|
489
|
+
let cur = '';
|
|
490
|
+
let cnt = 0;
|
|
491
|
+
for (const m of models) {
|
|
492
|
+
const cand = cur ? `${cur}, ${m}` : m;
|
|
493
|
+
if (cur && cand.length > width) {
|
|
494
|
+
rows.push({ text: cur, cnt });
|
|
495
|
+
cur = m;
|
|
496
|
+
cnt = 1;
|
|
497
|
+
} else {
|
|
498
|
+
cur = cand;
|
|
499
|
+
cnt += 1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (cur) rows.push({ text: cur, cnt });
|
|
503
|
+
if (rows.length <= maxLines) return { lines: rows.map((r) => r.text), remaining: 0 };
|
|
504
|
+
const keep = rows.slice(0, maxLines);
|
|
505
|
+
const shown = keep.reduce((a, r) => a + r.cnt, 0);
|
|
506
|
+
return { lines: keep.map((r) => r.text).concat([`…+${models.length - shown} lagi`]), remaining: models.length - shown };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function cmdModels(rt, args) {
|
|
510
|
+
const arg = args.pos[1] && !String(args.pos[1]).startsWith('-') ? String(args.pos[1]) : null;
|
|
511
|
+
const allTypes = Object.keys(core.MODEL_REGISTRY);
|
|
512
|
+
if (arg && !allTypes.includes(arg)) {
|
|
513
|
+
return errExit(`Provider tidak dikenal: ${arg}`, `Pilihan: ${allTypes.join(', ')}`);
|
|
514
|
+
}
|
|
515
|
+
const types = arg ? [arg] : allTypes;
|
|
516
|
+
const detected = new Map(collectProviders(rt.cfg).map((p) => [p.provider_type, p]));
|
|
517
|
+
|
|
518
|
+
const result = {};
|
|
519
|
+
for (const t of types) {
|
|
520
|
+
const p = detected.get(t);
|
|
521
|
+
// refresh: true → deteksi dijalankan ulang (bypass cache) saat user minta.
|
|
522
|
+
result[t] = modelsForProvider(t, { path: p && p.path, refresh: true });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (args.flags.json === true) {
|
|
526
|
+
console.log(JSON.stringify(result, null, 2));
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!types.length) {
|
|
531
|
+
console.log('📭 Tidak ada provider.');
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const wProv = Math.max('Provider'.length, ...types.map((t) => t.length));
|
|
536
|
+
const counts = types.map((t) => String(result[t].models.length));
|
|
537
|
+
const wCnt = Math.max('Jumlah'.length, ...counts.map((s) => s.length));
|
|
538
|
+
const pad = (s, w) => String(s).padEnd(w, ' ');
|
|
539
|
+
const padL = (s, w) => String(s).padStart(w, ' ');
|
|
540
|
+
|
|
541
|
+
console.log(`${pad('Provider', wProv)} ${padL('Jumlah', wCnt)} Models`);
|
|
542
|
+
console.log(`${'─'.repeat(wProv)} ${'─'.repeat(wCnt)} ${'─'.repeat(Math.min(MODELS_LIST_WIDTH, 20))}`);
|
|
543
|
+
types.forEach((t, i) => {
|
|
544
|
+
const r = result[t];
|
|
545
|
+
const w = wrapModelList(r.models, MODELS_LIST_WIDTH, MODELS_MAX_LINES);
|
|
546
|
+
const head = `${pad(t, wProv)} ${padL(r.models.length, wCnt)} ${w.lines[0] || '-'}`;
|
|
547
|
+
console.log(head);
|
|
548
|
+
for (const line of w.lines.slice(1)) {
|
|
549
|
+
console.log(`${' '.repeat(wProv)} ${' '.repeat(wCnt)} ${line}`);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
console.log('');
|
|
553
|
+
console.log('Sumber: static = registry saran lokal · binary = perintah `opencode models` · config = ~/.config/opencode');
|
|
554
|
+
console.log('Daftar ini saran/datalist — model bisa diketik manual; `claude`/`codex`/`gemini` tidak pernah dieksekusi.');
|
|
555
|
+
console.log('JSON: luang-cli models [provider] --json');
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
module.exports = { cmdLogin, cmdLogout, cmdWhoami, cmdTelemetry, cmdWorkspace, cmdProvider, cmdModels, errExit, authFailMessage, taskIdOf, statusInputNorm, normalizeTaskList, ask, providersLabel, syncIfKey, permLine, telemetryConsentLabel };
|
package/lib/core.js
CHANGED
|
@@ -8,6 +8,25 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { spawnSync } = require('child_process');
|
|
10
10
|
|
|
11
|
+
const { modelsForProvider, sanitizeModels, MODEL_REGISTRY } = require('./models');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve workspace folder mentah (dari server / config / arg CLI) menjadi
|
|
15
|
+
* path absolut, atau null bila kosong. Aturan:
|
|
16
|
+
* - "" / null → null (pakai cwd proses)
|
|
17
|
+
* - "~" / "~/x" → home dir user
|
|
18
|
+
* - relatif → resolve terhadap cwd proses saat dipanggil
|
|
19
|
+
* - absolut → apa adanya (dinormalisasi)
|
|
20
|
+
* Tidak menyentuh filesystem (cek eksistensi di pemanggil agar bisa warning).
|
|
21
|
+
*/
|
|
22
|
+
function resolveWorkspace(raw) {
|
|
23
|
+
const s = typeof raw === 'string' ? raw.trim() : '';
|
|
24
|
+
if (!s) return null;
|
|
25
|
+
if (s === '~' || s.startsWith('~/')) return path.normalize(path.join(os.homedir(), s.slice(s === '~' ? 1 : 2)));
|
|
26
|
+
if (path.isAbsolute(s)) return path.normalize(s);
|
|
27
|
+
return path.resolve(process.cwd(), s);
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
// ── Lokasi file ──────────────────────────────────────────────────────────────
|
|
12
31
|
|
|
13
32
|
const CONFIG_DIR = path.join(os.homedir(), '.luang-cli');
|
|
@@ -20,6 +39,18 @@ const DEFAULT_CONFIG = {
|
|
|
20
39
|
key: null,
|
|
21
40
|
default_provider: null,
|
|
22
41
|
providers: {},
|
|
42
|
+
// Telemetry konsent (lihat docs/agent-realtime-ws.md):
|
|
43
|
+
// false = jangan kirim apa pun. Kirim hanya bila ini true DAN server flag
|
|
44
|
+
// telemetry_enabled=true (dari GET /agent/me / hello.ok WS).
|
|
45
|
+
telemetry: false,
|
|
46
|
+
// Opsional: jumlah token diisi manual saat exec (jika user tahu) — hanya
|
|
47
|
+
// ikut dikirim bila consent aktif; selain itu biarkan null.
|
|
48
|
+
tokens_in: null,
|
|
49
|
+
tokens_out: null,
|
|
50
|
+
// Mode auto agent (`luang-cli agent start`): true = eksekusi tugas To-Do
|
|
51
|
+
// tanpa konfirmasi y/N (agent berjalan di background / tanpa terminal).
|
|
52
|
+
// Ditulis oleh `agent start` (default true, `--no-auto` menulis false).
|
|
53
|
+
auto_exec: false,
|
|
23
54
|
};
|
|
24
55
|
|
|
25
56
|
// ── Config ───────────────────────────────────────────────────────────────────
|
|
@@ -390,6 +421,9 @@ function heartbeatProviders(list) {
|
|
|
390
421
|
path: p.path || null,
|
|
391
422
|
exec_template: p.exec_template || null,
|
|
392
423
|
version: probeVersion(p.path),
|
|
424
|
+
// Kontrak baru (opsional): daftar model per provider — maks 50 item,
|
|
425
|
+
// tiap item ≤120 karakter (lihat lib/models.js).
|
|
426
|
+
models: modelsForProvider(p.provider_type, { path: p.path }).models,
|
|
393
427
|
}));
|
|
394
428
|
}
|
|
395
429
|
|
|
@@ -438,5 +472,9 @@ module.exports = {
|
|
|
438
472
|
probeVersion,
|
|
439
473
|
collectProviders,
|
|
440
474
|
heartbeatProviders,
|
|
475
|
+
resolveWorkspace,
|
|
476
|
+
modelsForProvider,
|
|
477
|
+
sanitizeModels,
|
|
478
|
+
MODEL_REGISTRY,
|
|
441
479
|
openBrowser,
|
|
442
480
|
};
|