agentlas 0.4.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/LICENSE +201 -0
- package/README.md +107 -0
- package/bin/agentlas.cjs +176 -0
- package/engine/ENGINE_META.json +8 -0
- package/engine/agentlas-api-agent.cjs +451 -0
- package/engine/agentlas-banner.cjs +108 -0
- package/engine/agentlas-capabilities.cjs +72 -0
- package/engine/agentlas-cloud-runtime.cjs +199 -0
- package/engine/agentlas-composer.cjs +256 -0
- package/engine/agentlas-config.cjs +29 -0
- package/engine/agentlas-doctor.cjs +173 -0
- package/engine/agentlas-i18n.cjs +317 -0
- package/engine/agentlas-input.cjs +437 -0
- package/engine/agentlas-native-host.cjs +612 -0
- package/engine/agentlas-onboard.cjs +71 -0
- package/engine/agentlas-parity.cjs +994 -0
- package/engine/agentlas-repl.cjs +1258 -0
- package/engine/agentlas-style.cjs +100 -0
- package/engine/agentlas-tools.cjs +196 -0
- package/engine/agentlas-ui.cjs +266 -0
- package/engine/agentlas.cjs +7503 -0
- package/engine/architecture.data.json +139 -0
- package/engine/bootstrap-schema.sql +568 -0
- package/install.ps1 +26 -0
- package/install.sh +53 -0
- package/package.json +49 -0
- package/scripts/gen-bootstrap-schema.sh +23 -0
- package/test/smoke.sh +56 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* agentlas-repl: the interactive shell of the agentlas terminal.
|
|
4
|
+
* agentlas is always the host — when the active runtime is claude/codex/gemini it drives them
|
|
5
|
+
* headless and renders inside this TUI (subscription auth preserved); for BYOK/Ollama it runs
|
|
6
|
+
* its own agent loop (api-agent). agentlas.cjs injects DB helpers via the `helpers` object.
|
|
7
|
+
*
|
|
8
|
+
* First launch runs an onboarding wizard (language → runtime → permission), stored in prefs.
|
|
9
|
+
*/
|
|
10
|
+
const readline = require("node:readline");
|
|
11
|
+
const { Ui } = require("./agentlas-ui.cjs");
|
|
12
|
+
const banner = require("./agentlas-banner.cjs");
|
|
13
|
+
const { runNativeTurn } = require("./agentlas-native-host.cjs");
|
|
14
|
+
const { runApiTurn } = require("./agentlas-api-agent.cjs");
|
|
15
|
+
const caps = require("./agentlas-capabilities.cjs");
|
|
16
|
+
const input = require("./agentlas-input.cjs");
|
|
17
|
+
const i18n = require("./agentlas-i18n.cjs");
|
|
18
|
+
const style = require("./agentlas-style.cjs");
|
|
19
|
+
|
|
20
|
+
function runtimeLabel(rt) {
|
|
21
|
+
if (!rt) return "(none)";
|
|
22
|
+
if (rt.mode === "cli") return rt.kind;
|
|
23
|
+
return `${rt.backend}${rt.model ? " · " + rt.model : ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeLang(value) {
|
|
27
|
+
const v = String(value || "").toLowerCase();
|
|
28
|
+
return v === "en" || v === "ko" ? v : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function terminalLang(prefs, opts) {
|
|
32
|
+
const explicit = normalizeLang((opts && opts.lang) || process.env.AGENTLAS_TERMINAL_LANG || process.env.AGENTLAS_LANG);
|
|
33
|
+
if (explicit) return explicit;
|
|
34
|
+
// 온보딩에서 고른 언어(cli-prefs.json lang)를 기본 존중. 없으면 en.
|
|
35
|
+
return normalizeLang(prefs && prefs.lang) || "en";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Hides the trailing "## Memory Events" block from the live stream while keeping the full
|
|
39
|
+
// text for curation. Holds back the last heading.length chars so a split heading is safe too.
|
|
40
|
+
function makeMemoryGuard(ui, heading) {
|
|
41
|
+
const N = heading.length;
|
|
42
|
+
let acc = "";
|
|
43
|
+
let printed = 0;
|
|
44
|
+
let cut = false;
|
|
45
|
+
const flush = () => {
|
|
46
|
+
if (cut) return;
|
|
47
|
+
const idx = acc.indexOf(heading);
|
|
48
|
+
if (idx >= 0) {
|
|
49
|
+
if (idx > printed) ui.streamDelta(acc.slice(printed, idx));
|
|
50
|
+
printed = idx;
|
|
51
|
+
cut = true;
|
|
52
|
+
} else if (acc.length > printed) {
|
|
53
|
+
ui.streamDelta(acc.slice(printed));
|
|
54
|
+
printed = acc.length;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
c: ui.c,
|
|
59
|
+
streamStart: () => ui.streamStart(),
|
|
60
|
+
streamDelta: (t) => {
|
|
61
|
+
if (cut) {
|
|
62
|
+
acc += t;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
acc += t;
|
|
66
|
+
const idx = acc.indexOf(heading);
|
|
67
|
+
if (idx >= 0) {
|
|
68
|
+
if (idx > printed) ui.streamDelta(acc.slice(printed, idx));
|
|
69
|
+
printed = idx;
|
|
70
|
+
cut = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const safe = acc.length - N;
|
|
74
|
+
if (safe > printed) {
|
|
75
|
+
ui.streamDelta(acc.slice(printed, safe));
|
|
76
|
+
printed = safe;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
streamEnd: () => {
|
|
80
|
+
flush();
|
|
81
|
+
ui.streamEnd();
|
|
82
|
+
},
|
|
83
|
+
tool: (...a) => ui.tool(...a),
|
|
84
|
+
toolResult: (...a) => ui.toolResult(...a),
|
|
85
|
+
info: (...a) => ui.info(...a),
|
|
86
|
+
warn: (...a) => ui.warn(...a),
|
|
87
|
+
error: (...a) => ui.error(...a),
|
|
88
|
+
status: (...a) => ui.status(...a),
|
|
89
|
+
ok: (...a) => ui.ok(...a),
|
|
90
|
+
cost: (...a) => ui.cost(...a),
|
|
91
|
+
line: (...a) => ui.line(...a),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 스트리밍 마크다운 렌더(Claude Code 스타일): 줄 단위로 모아 ui.renderInline 으로
|
|
96
|
+
// **bold**/#heading/`code`/불릿/코드펜스를 ANSI로 렌더한다(예전엔 마크다운을 제거했음).
|
|
97
|
+
function makeStyleGuard(ui) {
|
|
98
|
+
let buf = "";
|
|
99
|
+
let inCode = false;
|
|
100
|
+
const emit = (line) => {
|
|
101
|
+
if (/^\s*```/.test(line)) {
|
|
102
|
+
inCode = !inCode;
|
|
103
|
+
ui.write((ui.enabled ? ui.c.faint(line) : line) + "\n");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (inCode) {
|
|
107
|
+
ui.write((ui.enabled ? ui.c.dim(line) : line) + "\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
ui.write((ui.enabled ? ui.renderInline(line) : line) + "\n");
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
c: ui.c,
|
|
114
|
+
streamStart: () => {
|
|
115
|
+
buf = "";
|
|
116
|
+
inCode = false;
|
|
117
|
+
ui.streamStart();
|
|
118
|
+
},
|
|
119
|
+
streamDelta: (text) => {
|
|
120
|
+
if (!text) return;
|
|
121
|
+
ui.stopSpinner();
|
|
122
|
+
buf += text;
|
|
123
|
+
let nl;
|
|
124
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
125
|
+
emit(buf.slice(0, nl));
|
|
126
|
+
buf = buf.slice(nl + 1);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
streamEnd: () => {
|
|
130
|
+
if (buf.length) {
|
|
131
|
+
emit(buf);
|
|
132
|
+
buf = "";
|
|
133
|
+
}
|
|
134
|
+
ui.streamEnd();
|
|
135
|
+
},
|
|
136
|
+
tool: (...a) => ui.tool(...a),
|
|
137
|
+
toolResult: (...a) => ui.toolResult(...a),
|
|
138
|
+
info: (...a) => ui.info(...a),
|
|
139
|
+
warn: (...a) => ui.warn(...a),
|
|
140
|
+
error: (...a) => ui.error(...a),
|
|
141
|
+
status: (...a) => ui.status(...a),
|
|
142
|
+
ok: (...a) => ui.ok(...a),
|
|
143
|
+
cost: (...a) => ui.cost(...a),
|
|
144
|
+
line: (...a) => ui.line(...a),
|
|
145
|
+
stopSpinner: (...a) => ui.stopSpinner(...a),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// startRepl({ db, subject|null, runtime, permission, cwd, helpers, prefs, savePrefs })
|
|
150
|
+
function startRepl(opts) {
|
|
151
|
+
const { db } = opts;
|
|
152
|
+
const H = opts.helpers;
|
|
153
|
+
const prefs = opts.prefs || {};
|
|
154
|
+
prefs.agentRuntime = prefs.agentRuntime || {}; // { agentSlug|firmSlug: runtimeSpec|"auto" }
|
|
155
|
+
let baseRuntime = opts.runtime; // session default; per-agent runtime auto-routes from this
|
|
156
|
+
const ui = new Ui({ lang: terminalLang(prefs, opts) });
|
|
157
|
+
const state = {
|
|
158
|
+
subject: opts.subject || null,
|
|
159
|
+
runtime: opts.runtime,
|
|
160
|
+
permission: opts.permission || "write",
|
|
161
|
+
cwd: opts.cwd,
|
|
162
|
+
history: [],
|
|
163
|
+
native: {}, // kind → { id }
|
|
164
|
+
projectPath: opts.projectPath || null,
|
|
165
|
+
routePreambleOnce: null,
|
|
166
|
+
effort: prefs.effort || null, // /effort: low|medium|high|max → 런타임별 reasoning 강도
|
|
167
|
+
cost: {}, // runtimeLabel → { turns, in, out, cost, ms } — session usage ledger
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function showBanner() {
|
|
171
|
+
banner.renderBanner({
|
|
172
|
+
ui,
|
|
173
|
+
version: opts.version,
|
|
174
|
+
runtimeLabel: runtimeLabel(state.runtime),
|
|
175
|
+
subjectLabel: state.subject ? state.subject.label : null,
|
|
176
|
+
permission: state.permission,
|
|
177
|
+
cwd: state.cwd,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const completer = input.makeCompleter({
|
|
182
|
+
getAgentSlugs: () => { try { return H.listAgents(db).map((a) => a.slug); } catch { return []; } },
|
|
183
|
+
getFirmSlugs: () => { try { return H.listFirms(db).map((f) => f.slug); } catch { return []; } },
|
|
184
|
+
getCwd: () => state.cwd,
|
|
185
|
+
});
|
|
186
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: !!process.stdin.isTTY, completer, historySize: input.HISTORY_MAX });
|
|
187
|
+
input.attachHistory(rl);
|
|
188
|
+
const slashPalette = input.attachSlashPalette(rl, { ui, force: true });
|
|
189
|
+
// Raw-mode bottom input box (Claude Code / Hermes style). TTY only; readline is the fallback.
|
|
190
|
+
const { createComposer } = require("./agentlas-composer.cjs");
|
|
191
|
+
const useComposer = !!process.stdin.isTTY && process.env.AGENTLAS_CLASSIC_INPUT !== "1";
|
|
192
|
+
const composer = useComposer
|
|
193
|
+
? createComposer({ ui, loadHistory: () => input.loadHistory(), saveHistory: (h) => input.saveHistory(h) })
|
|
194
|
+
: null;
|
|
195
|
+
let handoff = false; // set before rl.close() when handing stdin to the composer
|
|
196
|
+
let busy = false;
|
|
197
|
+
let closed = false;
|
|
198
|
+
let currentAbort = null;
|
|
199
|
+
let idleExitArmedUntil = 0;
|
|
200
|
+
rl.on("close", () => {
|
|
201
|
+
if (handoff) return; // intentionally closed to hand stdin to the raw-mode composer
|
|
202
|
+
closed = true;
|
|
203
|
+
if (!busy) process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
if (useComposer) {
|
|
206
|
+
// In composer mode rl is closed; Ctrl-C at the box is a keypress, so process SIGINT only fires mid-turn.
|
|
207
|
+
process.on("SIGINT", () => {
|
|
208
|
+
if (busy && currentAbort) {
|
|
209
|
+
currentAbort.abort();
|
|
210
|
+
ui.warn(ui.t("interrupted"));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
process.on("exit", () => {
|
|
214
|
+
try { if (process.stdin.setRawMode) process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
rl.on("SIGINT", () => {
|
|
218
|
+
if (busy && currentAbort) {
|
|
219
|
+
currentAbort.abort();
|
|
220
|
+
ui.warn(ui.t("interrupted"));
|
|
221
|
+
} else {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
if (now < idleExitArmedUntil) {
|
|
224
|
+
ui.line("");
|
|
225
|
+
ui.line(ui.c.dim(ui.t("bye")));
|
|
226
|
+
rl.close();
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
idleExitArmedUntil = now + 3000;
|
|
230
|
+
ui.warn(ui.t("ctrlcAgain"));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
function ctxNow() {
|
|
235
|
+
return { projectPath: state.projectPath, agentId: state.subject && state.subject.id, permission: state.permission, cwd: state.cwd, lang: ui.lang };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Session usage ledger — accumulate per runtime label (host advantage: no single-model CLI can show this).
|
|
239
|
+
function recordCost(label, usage) {
|
|
240
|
+
const e = state.cost[label] || (state.cost[label] = { turns: 0, in: 0, out: 0, cost: 0, ms: 0 });
|
|
241
|
+
e.turns += 1;
|
|
242
|
+
if (usage) {
|
|
243
|
+
if (usage.input_tokens) e.in += usage.input_tokens;
|
|
244
|
+
if (usage.output_tokens) e.out += usage.output_tokens;
|
|
245
|
+
if (usage.cost_usd) e.cost += usage.cost_usd;
|
|
246
|
+
if (usage.duration_ms) e.ms += usage.duration_ms;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// /resume 용 세션 영속화 — 네이티브 런타임 세션ID + 에이전트/런타임/cwd/제목을 파일에 저장.
|
|
251
|
+
function persistSession(kind, sessionId, titlePrompt) {
|
|
252
|
+
if (!H.sessionsSave || !H.sessionsLoad || !sessionId || !state.subject) return;
|
|
253
|
+
try {
|
|
254
|
+
const list = H.sessionsLoad().filter((s) => !(s.agentSlug === state.subject.slug && s.kind === kind));
|
|
255
|
+
list.unshift({
|
|
256
|
+
ts: Date.now(),
|
|
257
|
+
agentSlug: state.subject.slug,
|
|
258
|
+
agentLabel: state.subject.label,
|
|
259
|
+
kind,
|
|
260
|
+
sessionId,
|
|
261
|
+
cwd: state.cwd,
|
|
262
|
+
title: String(titlePrompt || "").replace(/\s+/g, " ").trim().slice(0, 60),
|
|
263
|
+
});
|
|
264
|
+
H.sessionsSave(list);
|
|
265
|
+
} catch {
|
|
266
|
+
/* ignore */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── run one turn ──
|
|
271
|
+
async function runTurn(prompt, runOptions = {}) {
|
|
272
|
+
busy = true;
|
|
273
|
+
ui.beginTurn(); // 라이브 경과시간 스피너의 턴 시작점
|
|
274
|
+
currentAbort = new AbortController();
|
|
275
|
+
const signal = currentAbort.signal;
|
|
276
|
+
const recordHistoryEntry = !runOptions.side;
|
|
277
|
+
const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
|
|
278
|
+
const ctx = { ...ctxNow(), lang: targetLang, uiLang: ui.lang };
|
|
279
|
+
const rt = state.runtime;
|
|
280
|
+
const costLabel = runtimeLabel(rt);
|
|
281
|
+
const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
|
|
282
|
+
Object.assign(process.env, runEnv);
|
|
283
|
+
ui._lastUsage = null;
|
|
284
|
+
const assistantUi = makeStyleGuard(ui);
|
|
285
|
+
const thinkingText = i18n.t(targetLang, "thinkingWith", costLabel);
|
|
286
|
+
ui.info(thinkingText);
|
|
287
|
+
ui.status(thinkingText);
|
|
288
|
+
try {
|
|
289
|
+
if (rt.mode === "cli") {
|
|
290
|
+
const bin = H.which(H.RUNTIME_BIN[rt.kind]) || H.RUNTIME_BIN[rt.kind];
|
|
291
|
+
const session = state.native[rt.kind] || (state.native[rt.kind] = {});
|
|
292
|
+
const subjectSystem = state.routePreambleOnce
|
|
293
|
+
? `${state.routePreambleOnce}\n\n${state.subject.system}`
|
|
294
|
+
: state.subject.system;
|
|
295
|
+
state.routePreambleOnce = null;
|
|
296
|
+
const sys = H.augmentSystem(db, subjectSystem, ctx, false);
|
|
297
|
+
const res = await runNativeTurn({
|
|
298
|
+
kind: rt.kind,
|
|
299
|
+
bin,
|
|
300
|
+
prompt,
|
|
301
|
+
systemPrompt: session.id ? "" : sys,
|
|
302
|
+
cwd: state.cwd,
|
|
303
|
+
permission: state.permission,
|
|
304
|
+
session,
|
|
305
|
+
model: rt.model || null, // /model (claude --model, codex -m, gemini -m)
|
|
306
|
+
effort: state.effort || null, // /effort (codex reasoning effort, claude think-keyword)
|
|
307
|
+
mcpServers:
|
|
308
|
+
(state.permission === "write" || state.permission === "full") && H.mcpServers
|
|
309
|
+
? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
|
|
310
|
+
: [],
|
|
311
|
+
env: runEnv,
|
|
312
|
+
ui: assistantUi,
|
|
313
|
+
signal,
|
|
314
|
+
});
|
|
315
|
+
const at = (res.text || "").trim();
|
|
316
|
+
if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
|
|
317
|
+
recordCost(costLabel, res.usage);
|
|
318
|
+
if (!runOptions.side && session.id && !res.error) persistSession(rt.kind, session.id, prompt);
|
|
319
|
+
} else {
|
|
320
|
+
const subjectSystem = state.routePreambleOnce
|
|
321
|
+
? `${state.routePreambleOnce}\n\n${state.subject.system}`
|
|
322
|
+
: state.subject.system;
|
|
323
|
+
state.routePreambleOnce = null;
|
|
324
|
+
const sys = H.augmentSystem(db, subjectSystem, ctx, true);
|
|
325
|
+
let apiKey = null;
|
|
326
|
+
if (rt.backend !== "ollama") {
|
|
327
|
+
apiKey = await H.apiKey(rt.backend);
|
|
328
|
+
if (!apiKey) {
|
|
329
|
+
ui.error(ui.t("noKey", rt.backend));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const messages = state.history
|
|
334
|
+
.filter((h) => h.text && h.text.trim())
|
|
335
|
+
.map((h) => ({ role: h.role, content: h.text }))
|
|
336
|
+
.concat([{ role: "user", content: prompt }]);
|
|
337
|
+
const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
|
|
338
|
+
const res = await runApiTurn({
|
|
339
|
+
backend: rt.backend,
|
|
340
|
+
model: rt.model || H.defaultApiModel(rt.backend),
|
|
341
|
+
apiKey,
|
|
342
|
+
system: sys,
|
|
343
|
+
messages,
|
|
344
|
+
ctx,
|
|
345
|
+
ui: guard,
|
|
346
|
+
signal,
|
|
347
|
+
});
|
|
348
|
+
const cleaned = (H.curateCliReply(db, res.text || "", ctx) || "").trim();
|
|
349
|
+
if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
|
|
350
|
+
recordCost(costLabel, ui._lastUsage);
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
ui.stopSpinner();
|
|
354
|
+
if (signal.aborted) {
|
|
355
|
+
// user Ctrl-C — SIGINT handler already printed
|
|
356
|
+
} else if (e && e.name === "AbortError") {
|
|
357
|
+
ui.warn(ui.t("stalled"));
|
|
358
|
+
} else {
|
|
359
|
+
ui.error((e && e.message) || String(e));
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
busy = false;
|
|
363
|
+
ui.endTurn();
|
|
364
|
+
currentAbort = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── slash commands ──
|
|
369
|
+
function setRuntime(arg) {
|
|
370
|
+
const cliKinds = { "claude-code": 1, claude: 1, codex: 1, gemini: 1 };
|
|
371
|
+
const apiBackends = { anthropic: 1, openai: 1, google: 1, ollama: 1, upstage: 1 };
|
|
372
|
+
let a = (arg || "").trim();
|
|
373
|
+
if (a === "claude") a = "claude-code";
|
|
374
|
+
if (cliKinds[a]) {
|
|
375
|
+
const bin = H.which(H.RUNTIME_BIN[a]);
|
|
376
|
+
if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
|
|
377
|
+
state.runtime = { mode: "cli", kind: a };
|
|
378
|
+
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
|
|
379
|
+
state.native = {};
|
|
380
|
+
return ui.ok(ui.t("runtimeSet", a));
|
|
381
|
+
}
|
|
382
|
+
if (apiBackends[a]) {
|
|
383
|
+
state.runtime =
|
|
384
|
+
a === "ollama"
|
|
385
|
+
? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
|
|
386
|
+
: { mode: "api", backend: a, model: null };
|
|
387
|
+
baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
|
|
388
|
+
return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
|
|
389
|
+
}
|
|
390
|
+
ui.warn(ui.t("runtimeUsage"));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Show the English name when the chosen language is English (agents carry name_en).
|
|
394
|
+
function displayName(a) {
|
|
395
|
+
if (!a) return "";
|
|
396
|
+
if (ui.lang === "en" && a.name_en && a.name_en !== a.name) return a.name_en;
|
|
397
|
+
return a.name || a.name_en || "";
|
|
398
|
+
}
|
|
399
|
+
function installedKinds() {
|
|
400
|
+
return caps.CLI_KINDS.filter((k) => H.which(H.RUNTIME_BIN[k]));
|
|
401
|
+
}
|
|
402
|
+
// Resolve the runtime a subject runs on: pinned (prefs) > capability auto-route > session default.
|
|
403
|
+
function applyRuntimeFor(subject) {
|
|
404
|
+
const pinned = prefs.agentRuntime[subject.slug];
|
|
405
|
+
let spec;
|
|
406
|
+
if (pinned && pinned !== "auto") spec = pinned;
|
|
407
|
+
else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
|
|
408
|
+
state.runtime = caps.runtimeFromSpec(spec);
|
|
409
|
+
state.native = {};
|
|
410
|
+
}
|
|
411
|
+
// Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
|
|
412
|
+
function routingNote(subject) {
|
|
413
|
+
if (!subject || !caps.needsImage(subject.capAgent)) return;
|
|
414
|
+
const spec = caps.specOf(state.runtime);
|
|
415
|
+
if (caps.capsFor(spec).image) {
|
|
416
|
+
if (spec !== caps.specOf(baseRuntime)) ui.info(ui.t("routedImage", spec));
|
|
417
|
+
} else {
|
|
418
|
+
ui.warn(ui.t("guard.imageWarn", caps.capsFor(spec).label || spec));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function specToRuntime(spec) {
|
|
422
|
+
return (!spec || spec === "auto") ? null : caps.runtimeFromSpec(spec);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function setSubjectAgent(agent) {
|
|
426
|
+
state.subject = {
|
|
427
|
+
kind: "agent",
|
|
428
|
+
id: agent.id,
|
|
429
|
+
slug: agent.slug,
|
|
430
|
+
label: displayName(agent),
|
|
431
|
+
system: agent.system_prompt || `You are ${agent.name}.`,
|
|
432
|
+
capAgent: agent,
|
|
433
|
+
};
|
|
434
|
+
state.history = [];
|
|
435
|
+
state.routePreambleOnce = null;
|
|
436
|
+
applyRuntimeFor(state.subject);
|
|
437
|
+
}
|
|
438
|
+
function setSubjectFirm(firm) {
|
|
439
|
+
const sys = H.firmSystemPrompt(db, firm);
|
|
440
|
+
state.subject = {
|
|
441
|
+
kind: "firm",
|
|
442
|
+
id: firm.ceo_agent_id,
|
|
443
|
+
slug: firm.slug,
|
|
444
|
+
label: displayName(firm) + " CEO",
|
|
445
|
+
system: sys,
|
|
446
|
+
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
|
|
447
|
+
};
|
|
448
|
+
state.history = [];
|
|
449
|
+
state.routePreambleOnce = null;
|
|
450
|
+
applyRuntimeFor(state.subject);
|
|
451
|
+
}
|
|
452
|
+
function switchSubject(kind, query) {
|
|
453
|
+
if (kind === "agent") {
|
|
454
|
+
const agent = H.resolveAgent(db, query);
|
|
455
|
+
if (!agent) return ui.error(ui.t("noAgent", query));
|
|
456
|
+
setSubjectAgent(agent);
|
|
457
|
+
} else {
|
|
458
|
+
const firm = H.resolveFirm(db, query);
|
|
459
|
+
if (!firm) return ui.error(ui.t("noCompany", query));
|
|
460
|
+
setSubjectFirm(firm);
|
|
461
|
+
}
|
|
462
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
463
|
+
routingNote(state.subject);
|
|
464
|
+
}
|
|
465
|
+
// resolved runtime spec for any agent row (for display in roster / team)
|
|
466
|
+
function resolvedSpec(agentRow, slug) {
|
|
467
|
+
const pinned = prefs.agentRuntime[slug];
|
|
468
|
+
if (pinned && pinned !== "auto") return pinned;
|
|
469
|
+
return caps.autoRuntimeFor(agentRow, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function printRoster() {
|
|
473
|
+
const ags = H.listAgents(db);
|
|
474
|
+
const firms = H.listFirms(db);
|
|
475
|
+
ui.line("");
|
|
476
|
+
ui.line(ui.c.dim(" " + ui.t("picker.agents")));
|
|
477
|
+
ags.forEach((a, i) => {
|
|
478
|
+
const spec = resolvedSpec(a, a.slug);
|
|
479
|
+
const bdg = caps.needsImage(a) ? (caps.capsFor(spec).image ? "[image]" : "[image missing]") : "";
|
|
480
|
+
ui.line(
|
|
481
|
+
" " + ui.c.faint(String(i + 1).padStart(2)) + " " + ui.c.emerald(a.slug.padEnd(26)) + " " +
|
|
482
|
+
ui.c.text((displayName(a) || "").padEnd(16)) + " " + ui.c.blue(spec) + (bdg ? " " + bdg : ""),
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
if (firms.length) {
|
|
486
|
+
ui.line(ui.c.dim(" " + ui.t("picker.companies")));
|
|
487
|
+
firms.forEach((f) =>
|
|
488
|
+
ui.line(" " + ui.c.emerald(("firm " + f.slug).padEnd(26)) + " " + ui.c.text(displayName(f)) + ui.c.dim(" (CEO)")),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (!ags.length && !firms.length) ui.line(" " + ui.c.dim(ui.t("picker.none")));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// /team — show or assign each agent's runtime (LLM). Auto-routed by capability unless pinned.
|
|
495
|
+
function printTeam() {
|
|
496
|
+
const ags = H.listAgents(db);
|
|
497
|
+
ui.line("");
|
|
498
|
+
ui.line(ui.c.dim(" " + ui.t("team.title")));
|
|
499
|
+
for (const a of ags) {
|
|
500
|
+
const pinned = prefs.agentRuntime[a.slug] && prefs.agentRuntime[a.slug] !== "auto";
|
|
501
|
+
const spec = resolvedSpec(a, a.slug);
|
|
502
|
+
const bdg = caps.needsImage(a) ? (caps.capsFor(spec).image ? "[image]" : "[image missing]") : "";
|
|
503
|
+
ui.line(
|
|
504
|
+
" " + ui.c.emerald(a.slug.padEnd(28)) + ui.c.blue((spec + (bdg ? " " + bdg : "")).padEnd(14)) +
|
|
505
|
+
ui.c.faint(pinned ? ui.t("team.pinned") : ui.t("team.auto")),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
ui.line(" " + ui.c.faint(ui.t("team.usage")));
|
|
509
|
+
}
|
|
510
|
+
function setTeam(arg) {
|
|
511
|
+
const parts = arg.trim().split(/\s+/);
|
|
512
|
+
const who = parts[0];
|
|
513
|
+
let spec = (parts[1] || "").trim();
|
|
514
|
+
if (spec === "claude") spec = "claude-code";
|
|
515
|
+
const agent = H.resolveAgent(db, who);
|
|
516
|
+
const firm = agent ? null : H.resolveFirm(db, who);
|
|
517
|
+
const slug = agent ? agent.slug : firm ? firm.slug : null;
|
|
518
|
+
if (!slug) return ui.error(ui.t("noAgent", who));
|
|
519
|
+
if (!spec) return printTeam();
|
|
520
|
+
const valid = ["auto", "claude-code", "codex", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
|
|
521
|
+
if (!valid.includes(spec)) return ui.warn(ui.t("team.usage"));
|
|
522
|
+
prefs.agentRuntime[slug] = spec;
|
|
523
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
524
|
+
ui.ok(ui.t("team.set", slug, spec === "auto" ? ui.t("team.auto") : spec));
|
|
525
|
+
if (state.subject && state.subject.slug === slug) {
|
|
526
|
+
applyRuntimeFor(state.subject);
|
|
527
|
+
routingNote(state.subject);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function printCost() {
|
|
532
|
+
const labels = Object.keys(state.cost);
|
|
533
|
+
ui.line("");
|
|
534
|
+
if (!labels.length) return ui.info(ui.t("noCost"));
|
|
535
|
+
ui.line(ui.c.dim(" " + ui.t("cost.title")));
|
|
536
|
+
let tIn = 0, tOut = 0, tCost = 0, tMs = 0, tTurns = 0;
|
|
537
|
+
const fmt = (e) => {
|
|
538
|
+
const bits = [e.turns + (e.turns === 1 ? " turn" : " turns")];
|
|
539
|
+
if (e.in || e.out) bits.push(e.in + "→" + e.out + " tok");
|
|
540
|
+
if (e.cost) bits.push("$" + e.cost.toFixed(4));
|
|
541
|
+
if (e.ms) bits.push((e.ms / 1000).toFixed(1) + "s");
|
|
542
|
+
return bits.join(" · ");
|
|
543
|
+
};
|
|
544
|
+
for (const label of labels) {
|
|
545
|
+
const e = state.cost[label];
|
|
546
|
+
tIn += e.in; tOut += e.out; tCost += e.cost; tMs += e.ms; tTurns += e.turns;
|
|
547
|
+
ui.line(" " + ui.c.blue(label.padEnd(22)) + ui.c.faint(fmt(e)));
|
|
548
|
+
}
|
|
549
|
+
ui.line(" " + ui.c.emerald(ui.t("cost.total").padEnd(22)) + ui.c.text(fmt({ turns: tTurns, in: tIn, out: tOut, cost: tCost, ms: tMs })));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function printSlashSkills() {
|
|
553
|
+
ui.line("");
|
|
554
|
+
ui.rule("Skills");
|
|
555
|
+
for (const entry of input.slashCommandEntries()) {
|
|
556
|
+
const tag = entry.category ? ui.c.faint(entry.category.padEnd(10)) : "";
|
|
557
|
+
ui.line(" " + ui.c.emerald(entry.command.padEnd(18)) + tag + ui.c.dim(entry.description));
|
|
558
|
+
if (!entry.aliasOf && entry.usage) ui.line(" " + ui.c.faint(" ".repeat(18) + entry.usage));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function printPermissions() {
|
|
563
|
+
ui.line("");
|
|
564
|
+
ui.rule("Permissions");
|
|
565
|
+
ui.line(" " + ui.c.faint("Current") + " " + ui.c.emerald(state.permission));
|
|
566
|
+
const rows = [
|
|
567
|
+
["read", "inspect files and answer; no file writes or shell automation"],
|
|
568
|
+
["write", "read plus create/edit files in the current work area"],
|
|
569
|
+
["full", "write plus shell commands and external local automation"],
|
|
570
|
+
];
|
|
571
|
+
for (const [level, description] of rows) {
|
|
572
|
+
const mark = level === state.permission ? "› " : " ";
|
|
573
|
+
ui.line(" " + ui.c.emerald((mark + level).padEnd(10)) + ui.c.dim(description));
|
|
574
|
+
}
|
|
575
|
+
ui.line(" " + ui.c.faint("usage: /permission read|write|full"));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function rerunSetup() {
|
|
579
|
+
try {
|
|
580
|
+
const { runOnboard } = require("./agentlas-onboard.cjs");
|
|
581
|
+
const result = await runOnboard({ ui, rl, helpers: H });
|
|
582
|
+
Object.assign(prefs, result);
|
|
583
|
+
ui.lang = terminalLang(prefs, opts);
|
|
584
|
+
state.permission = prefs.permission || state.permission;
|
|
585
|
+
if (prefs.runtime && prefs.runtime !== "auto" && H.RUNTIME_BIN[prefs.runtime] && H.which(H.RUNTIME_BIN[prefs.runtime])) {
|
|
586
|
+
state.runtime = { mode: "cli", kind: prefs.runtime };
|
|
587
|
+
baseRuntime = state.runtime;
|
|
588
|
+
state.native = {};
|
|
589
|
+
}
|
|
590
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
ui.error((e && e.message) || String(e));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function compactHistory() {
|
|
597
|
+
const before = state.history.length;
|
|
598
|
+
const keep = 10;
|
|
599
|
+
if (before <= keep) {
|
|
600
|
+
ui.info(ui.t("compact.noop", String(before)));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
state.history = state.history.slice(-keep);
|
|
604
|
+
ui.ok(ui.t("compact.done", String(before), String(state.history.length)));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function showDiff() {
|
|
608
|
+
const { spawnSync } = require("node:child_process");
|
|
609
|
+
const opt = { cwd: state.cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 };
|
|
610
|
+
const stat = spawnSync("git", ["-C", state.cwd, "--no-pager", "diff", "--stat"], opt);
|
|
611
|
+
if (stat.status !== 0 && /not a git repository/i.test(stat.stderr || "")) return ui.warn(ui.t("diffNoGit"));
|
|
612
|
+
const body = spawnSync("git", ["-C", state.cwd, "--no-pager", "diff"], opt);
|
|
613
|
+
const statTxt = (stat.stdout || "").trim();
|
|
614
|
+
const bodyTxt = (body.stdout || "").trim();
|
|
615
|
+
ui.line("");
|
|
616
|
+
if (!statTxt && !bodyTxt) return ui.info(ui.t("diffClean"));
|
|
617
|
+
if (statTxt) ui.markdown(statTxt);
|
|
618
|
+
if (bodyTxt) {
|
|
619
|
+
ui.line("");
|
|
620
|
+
for (const ln of bodyTxt.split("\n").slice(0, 500)) {
|
|
621
|
+
if (ln.startsWith("+") && !ln.startsWith("+++")) ui.line(ui.c.green(ln));
|
|
622
|
+
else if (ln.startsWith("-") && !ln.startsWith("---")) ui.line(ui.c.paw(ln));
|
|
623
|
+
else if (ln.startsWith("@@")) ui.line(ui.c.blue(ln));
|
|
624
|
+
else ui.line(ui.c.dim(ln));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// !cmd — run a shell command in the working folder and show its output (display-only).
|
|
630
|
+
function runShell(cmd) {
|
|
631
|
+
if (!cmd) return;
|
|
632
|
+
const { spawnSync } = require("node:child_process");
|
|
633
|
+
ui.tool("$ " + cmd);
|
|
634
|
+
const r = spawnSync("bash", ["-lc", cmd], { cwd: state.cwd, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
|
|
635
|
+
const out = ((r.stdout || "") + (r.stderr || "")).trim();
|
|
636
|
+
ui.toolResult(out || ("exit " + (r.status == null ? "?" : r.status)), r.status === 0 || r.status == null);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// @path — inline the contents of mentioned files into the prompt as fenced context.
|
|
640
|
+
function expandMentions(text) {
|
|
641
|
+
const fs = require("node:fs");
|
|
642
|
+
const path = require("node:path");
|
|
643
|
+
const seen = new Set();
|
|
644
|
+
const blocks = [];
|
|
645
|
+
const re = /(^|\s)@([^\s]+)/g;
|
|
646
|
+
let m;
|
|
647
|
+
while ((m = re.exec(text))) {
|
|
648
|
+
const p = m[2];
|
|
649
|
+
if (seen.has(p)) continue;
|
|
650
|
+
seen.add(p);
|
|
651
|
+
try {
|
|
652
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(state.cwd, p);
|
|
653
|
+
const st = fs.statSync(abs);
|
|
654
|
+
if (st.isFile() && st.size <= 256 * 1024) {
|
|
655
|
+
blocks.push("File: " + p + "\n```\n" + fs.readFileSync(abs, "utf8").slice(0, 20000) + "\n```");
|
|
656
|
+
}
|
|
657
|
+
} catch { /* not a readable file — leave the @token as plain text */ }
|
|
658
|
+
}
|
|
659
|
+
return blocks.length ? text + "\n\n" + blocks.join("\n\n") : text;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function handleSlash(line) {
|
|
663
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
664
|
+
const arg = rest.join(" ");
|
|
665
|
+
switch (cmd) {
|
|
666
|
+
case "help":
|
|
667
|
+
case "?":
|
|
668
|
+
printHelp(ui);
|
|
669
|
+
return true;
|
|
670
|
+
case "agents":
|
|
671
|
+
printRoster();
|
|
672
|
+
return true;
|
|
673
|
+
case "skills":
|
|
674
|
+
printSlashSkills();
|
|
675
|
+
return true;
|
|
676
|
+
case "team":
|
|
677
|
+
arg ? setTeam(arg) : printTeam();
|
|
678
|
+
return true;
|
|
679
|
+
case "firms": {
|
|
680
|
+
const fs = H.listFirms(db);
|
|
681
|
+
ui.line("");
|
|
682
|
+
for (const f of fs) ui.line(" " + ui.c.emerald(f.slug.padEnd(28)) + ui.c.text(f.name) + ui.c.dim(" (CEO)"));
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
case "agent":
|
|
686
|
+
if (!arg) return ui.warn(ui.t("agentUsage")), true;
|
|
687
|
+
switchSubject("agent", arg);
|
|
688
|
+
return true;
|
|
689
|
+
case "firm":
|
|
690
|
+
if (!arg) return ui.warn(ui.t("firmUsage")), true;
|
|
691
|
+
switchSubject("firm", arg);
|
|
692
|
+
return true;
|
|
693
|
+
case "runtime":
|
|
694
|
+
setRuntime(arg);
|
|
695
|
+
return true;
|
|
696
|
+
case "storm": {
|
|
697
|
+
if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
|
|
698
|
+
let goal = arg;
|
|
699
|
+
let research = false;
|
|
700
|
+
if (/\s--research(-evidence)?\b/.test(" " + goal)) {
|
|
701
|
+
research = true;
|
|
702
|
+
goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
|
|
703
|
+
}
|
|
704
|
+
await H.stormRun(db, goal, { ui, cwd: state.cwd, research });
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
case "build": {
|
|
708
|
+
if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
|
|
709
|
+
ui.line("");
|
|
710
|
+
await H.hepRun(["hep-build", arg], { cwd: state.cwd });
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
case "route": {
|
|
714
|
+
if (!arg) return ui.warn("usage: /route <요청>"), true;
|
|
715
|
+
ui.line("");
|
|
716
|
+
await H.hepRun(["route", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
case "research": {
|
|
720
|
+
if (!arg) return ui.warn("usage: /research status|gather|search|read … (예: /research search \"쿼리\")"), true;
|
|
721
|
+
ui.line("");
|
|
722
|
+
await H.hepRun(["research", ...arg.split(/\s+/)], { cwd: state.cwd });
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
case "swarm": {
|
|
726
|
+
if (!arg) return ui.warn("usage: /swarm <goal> [--parallel N]"), true;
|
|
727
|
+
let goal = arg;
|
|
728
|
+
let concurrency;
|
|
729
|
+
const m = goal.match(/\s--parallel\s+(\d+)\b/);
|
|
730
|
+
if (m) {
|
|
731
|
+
concurrency = Number(m[1]);
|
|
732
|
+
goal = goal.replace(m[0], "").trim();
|
|
733
|
+
}
|
|
734
|
+
await H.swarmRun(db, goal, {
|
|
735
|
+
ui,
|
|
736
|
+
cwd: state.cwd,
|
|
737
|
+
permission: state.permission,
|
|
738
|
+
runtime: state.runtime,
|
|
739
|
+
concurrency,
|
|
740
|
+
agent: state.subject && state.subject.capAgent,
|
|
741
|
+
projectPath: state.projectPath,
|
|
742
|
+
});
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
case "model":
|
|
746
|
+
// CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
|
|
747
|
+
state.runtime.model = arg || null;
|
|
748
|
+
state.native = {}; // 새 모델로 세션 리셋
|
|
749
|
+
ui.ok(ui.t("modelSet", state.runtime.model || ui.t("modelDefault")));
|
|
750
|
+
return true;
|
|
751
|
+
case "effort": {
|
|
752
|
+
const lv = (arg || "").toLowerCase().trim();
|
|
753
|
+
if (!lv) {
|
|
754
|
+
ui.info(ui.t("effortCurrent", state.effort || ui.t("modelDefault")));
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
|
|
758
|
+
return ui.warn(ui.t("effortUsage")), true;
|
|
759
|
+
}
|
|
760
|
+
state.effort = lv === "auto" || lv === "off" ? null : lv;
|
|
761
|
+
state.native = {};
|
|
762
|
+
ui.ok(ui.t("effortSet", state.effort || ui.t("modelDefault")));
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
case "permission":
|
|
766
|
+
case "perm": {
|
|
767
|
+
const p = (arg || "").toLowerCase();
|
|
768
|
+
if (!p) {
|
|
769
|
+
printPermissions();
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
|
|
773
|
+
state.permission = p;
|
|
774
|
+
ui.ok(ui.t("permSet", p));
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
case "permissions":
|
|
778
|
+
if (arg) {
|
|
779
|
+
const p = (arg || "").toLowerCase();
|
|
780
|
+
if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
|
|
781
|
+
state.permission = p;
|
|
782
|
+
ui.ok(ui.t("permSet", p));
|
|
783
|
+
} else {
|
|
784
|
+
printPermissions();
|
|
785
|
+
}
|
|
786
|
+
return true;
|
|
787
|
+
case "setup":
|
|
788
|
+
await rerunSetup();
|
|
789
|
+
return true;
|
|
790
|
+
case "cwd":
|
|
791
|
+
if (arg) {
|
|
792
|
+
const path = require("node:path");
|
|
793
|
+
const fs = require("node:fs");
|
|
794
|
+
const next = path.resolve(state.cwd, arg);
|
|
795
|
+
if (!fs.existsSync(next)) return ui.error(ui.t("cwdNoPath", next)), true;
|
|
796
|
+
state.cwd = next;
|
|
797
|
+
state.native = {};
|
|
798
|
+
if (H.projectPathFor) state.projectPath = H.projectPathFor(db, next);
|
|
799
|
+
ui.ok(ui.t("cwdSet", banner.shorten(next)));
|
|
800
|
+
} else {
|
|
801
|
+
ui.info(state.cwd);
|
|
802
|
+
}
|
|
803
|
+
return true;
|
|
804
|
+
case "memory": {
|
|
805
|
+
const mem = H.cliMemoryContext(db, state.projectPath);
|
|
806
|
+
ui.line("");
|
|
807
|
+
ui.markdown(mem || ui.t("noMemory"));
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
case "ontology": {
|
|
811
|
+
if (!H.ontologyCommand) return ui.warn("ontology command unavailable"), true;
|
|
812
|
+
try {
|
|
813
|
+
const lines = H.ontologyCommand(arg, { cwd: state.cwd, projectPath: state.projectPath });
|
|
814
|
+
ui.line("");
|
|
815
|
+
for (const item of lines || []) ui.line(" " + ui.c.text(String(item)));
|
|
816
|
+
} catch (e) {
|
|
817
|
+
ui.error((e && e.message) || String(e));
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
case "side":
|
|
822
|
+
case "btw":
|
|
823
|
+
if (!arg) return ui.warn(ui.t("sideUsage")), true;
|
|
824
|
+
if (!state.subject) return ui.warn(ui.t("sideNeedsSubject")), true;
|
|
825
|
+
ui.info(ui.t("sideStart"));
|
|
826
|
+
await runTurn(expandMentions(arg), { side: true });
|
|
827
|
+
ui.info(ui.t("sideDone"));
|
|
828
|
+
return true;
|
|
829
|
+
case "clear":
|
|
830
|
+
state.history = [];
|
|
831
|
+
state.native = {};
|
|
832
|
+
if (ui.enabled) process.stdout.write("\x1b[2J\x1b[H");
|
|
833
|
+
showBanner();
|
|
834
|
+
return true;
|
|
835
|
+
case "import":
|
|
836
|
+
if (!arg) return ui.warn(ui.t("importUsage")), true;
|
|
837
|
+
try {
|
|
838
|
+
const r = H.importLocal(db, arg);
|
|
839
|
+
ui.ok(ui.t(r.updated ? "updated" : "imported", r.name, r.kind));
|
|
840
|
+
} catch (e) {
|
|
841
|
+
ui.error((e && e.message) || String(e));
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
case "install": {
|
|
845
|
+
if (!arg) return ui.warn(ui.t("installUsage")), true;
|
|
846
|
+
if (!H.cloudInstall) return ui.warn("install unavailable"), true;
|
|
847
|
+
ui.status(ui.t("installing", arg.trim()));
|
|
848
|
+
try {
|
|
849
|
+
const agent = await H.cloudInstall(db, arg.trim());
|
|
850
|
+
ui.ok(ui.t("cloudInstalled", agent.name || agent.slug));
|
|
851
|
+
if (agent.localPath) ui.info(agent.localPath);
|
|
852
|
+
} catch (e) {
|
|
853
|
+
ui.stopSpinner();
|
|
854
|
+
ui.error((e && e.message) || String(e));
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
case "marketplace":
|
|
859
|
+
case "market": {
|
|
860
|
+
ui.line("");
|
|
861
|
+
ui.rule(ui.t("market.title"));
|
|
862
|
+
ui.line(" " + ui.c.dim(ui.t("market.help")));
|
|
863
|
+
ui.line(" " + ui.c.emerald("/install <slug>".padEnd(18)) + ui.c.dim(ui.t("market.installHint")));
|
|
864
|
+
ui.line(" " + ui.c.emerald("/import <path>".padEnd(18)) + ui.c.dim(ui.t("market.importHint")));
|
|
865
|
+
const logged = H.hasCloudSession ? await H.hasCloudSession() : false;
|
|
866
|
+
ui.line(" " + ui.c.faint(ui.t(logged ? "market.loggedIn" : "market.loggedOut")));
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
case "mcp": {
|
|
870
|
+
const servers = H.mcpServers ? H.mcpServers(db) : [];
|
|
871
|
+
ui.line("");
|
|
872
|
+
ui.rule("MCP");
|
|
873
|
+
if (!servers.length) {
|
|
874
|
+
ui.info(ui.t("mcp.none"));
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
for (const s of servers) {
|
|
878
|
+
let envKeys = [];
|
|
879
|
+
try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
|
|
880
|
+
const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
|
|
881
|
+
const on = s.enabled ? ui.c.green("on ") : ui.c.faint("off");
|
|
882
|
+
const envStr = envKeys.length ? envKeys.join(", ") : "no key";
|
|
883
|
+
ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
|
|
884
|
+
}
|
|
885
|
+
const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 = playwright(항상)
|
|
886
|
+
ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
|
|
887
|
+
ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
case "resume": {
|
|
891
|
+
const list = H.sessionsLoad ? H.sessionsLoad() : [];
|
|
892
|
+
if (!arg) {
|
|
893
|
+
ui.line("");
|
|
894
|
+
ui.rule(ui.t("resume.title"));
|
|
895
|
+
if (!list.length) {
|
|
896
|
+
ui.info(ui.t("resume.none"));
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
list.slice(0, 10).forEach((s, i) =>
|
|
900
|
+
ui.line(
|
|
901
|
+
" " + ui.c.faint(String(i + 1).padStart(2)) + " " +
|
|
902
|
+
ui.c.emerald(String(s.agentLabel || s.agentSlug || "?").padEnd(20)) +
|
|
903
|
+
ui.c.blue(String(s.kind || "").padEnd(12)) + ui.c.dim(s.title || ""),
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
ui.line(" " + ui.c.faint(ui.t("resume.usage")));
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const n = parseInt(arg, 10);
|
|
910
|
+
const s = n >= 1 && n <= list.length ? list[n - 1] : null;
|
|
911
|
+
if (!s) return ui.warn(ui.t("resume.noNum")), true;
|
|
912
|
+
const agent = H.resolveAgent(db, s.agentSlug);
|
|
913
|
+
if (!agent) return ui.error(ui.t("noAgent", s.agentSlug)), true;
|
|
914
|
+
setSubjectAgent(agent);
|
|
915
|
+
if (s.kind && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
|
|
916
|
+
state.runtime = { mode: "cli", kind: s.kind };
|
|
917
|
+
baseRuntime = state.runtime;
|
|
918
|
+
}
|
|
919
|
+
state.native = { [s.kind]: { id: s.sessionId } };
|
|
920
|
+
try {
|
|
921
|
+
const fs2 = require("node:fs");
|
|
922
|
+
if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
|
|
923
|
+
} catch {
|
|
924
|
+
/* ignore */
|
|
925
|
+
}
|
|
926
|
+
ui.ok(ui.t("resume.ok", s.agentLabel || s.agentSlug || "?"));
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
case "doctor":
|
|
930
|
+
await H.doctor(db, ui);
|
|
931
|
+
return true;
|
|
932
|
+
case "status":
|
|
933
|
+
banner.renderStatus({ ui, runtimeLabel: runtimeLabel(state.runtime), subjectLabel: state.subject && state.subject.label, permission: state.permission, cwd: state.cwd });
|
|
934
|
+
return true;
|
|
935
|
+
case "cost":
|
|
936
|
+
printCost();
|
|
937
|
+
return true;
|
|
938
|
+
case "multimodal": {
|
|
939
|
+
const [sub, modality, providerId] = arg.trim().split(/\s+/);
|
|
940
|
+
if (sub === "set") {
|
|
941
|
+
if (!H.setMultimodal) return ui.warn("multimodal settings unavailable"), true;
|
|
942
|
+
try {
|
|
943
|
+
H.setMultimodal(db, modality, providerId);
|
|
944
|
+
ui.ok(ui.t("multimodal.set", modality || "", providerId || ""));
|
|
945
|
+
} catch (e) {
|
|
946
|
+
ui.error((e && e.message) || String(e));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (H.multimodalStatus) {
|
|
950
|
+
const rows = await H.multimodalStatus(db);
|
|
951
|
+
ui.line("");
|
|
952
|
+
ui.line(ui.c.dim(" " + ui.t("multimodal.title")));
|
|
953
|
+
for (const row of rows) {
|
|
954
|
+
const env = row.env.length
|
|
955
|
+
? row.env.map((e) => `${e.key}:${e.hasValue ? "set" : "missing"}`).join(" ")
|
|
956
|
+
: "no key";
|
|
957
|
+
ui.line(" " + ui.c.blue(row.modality.padEnd(7)) + ui.c.text(row.provider.id.padEnd(22)) + ui.c.dim(env));
|
|
958
|
+
}
|
|
959
|
+
ui.line(" " + ui.c.faint(ui.t("multimodal.usage")));
|
|
960
|
+
}
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
case "diff":
|
|
964
|
+
showDiff();
|
|
965
|
+
return true;
|
|
966
|
+
case "history": {
|
|
967
|
+
const items = input.loadHistory().slice(0, 30);
|
|
968
|
+
ui.line("");
|
|
969
|
+
if (!items.length) { ui.info(ui.t("noHistory")); return true; }
|
|
970
|
+
for (let i = 0; i < items.length; i++) ui.line(" " + ui.c.faint(String(i + 1).padStart(3)) + " " + ui.c.text(items[i]));
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
case "compact":
|
|
974
|
+
compactHistory();
|
|
975
|
+
return true;
|
|
976
|
+
case "keybindings":
|
|
977
|
+
printKeybindings(ui);
|
|
978
|
+
return true;
|
|
979
|
+
case "exit":
|
|
980
|
+
case "quit":
|
|
981
|
+
case "q":
|
|
982
|
+
ui.line(ui.c.dim(ui.t("bye")));
|
|
983
|
+
rl.close();
|
|
984
|
+
process.exit(0);
|
|
985
|
+
return false;
|
|
986
|
+
default:
|
|
987
|
+
ui.warn(ui.t("unknownCmd", cmd));
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ── interactive picker (when no agent was given) ──
|
|
993
|
+
function chooseAndStart(setter, row) {
|
|
994
|
+
setter(row);
|
|
995
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
996
|
+
routingNote(state.subject);
|
|
997
|
+
ask();
|
|
998
|
+
}
|
|
999
|
+
// skipRoster=true → 명령/오입력 뒤 재호출 시 전체 로스터를 다시 그리지 않는다(노이즈 제거; Claude Code처럼 조용히).
|
|
1000
|
+
function pick(skipRoster) {
|
|
1001
|
+
if (closed) return process.exit(0);
|
|
1002
|
+
if (slashPalette.setEnabled) slashPalette.setEnabled(false);
|
|
1003
|
+
if (!skipRoster) printRoster();
|
|
1004
|
+
rl.question("\n " + ui.c.emerald(ui.t("picker.prompt")), async (line) => {
|
|
1005
|
+
const t = (line || "").trim();
|
|
1006
|
+
if (!t) return pick(true);
|
|
1007
|
+
if (t.startsWith("/") && !input.isAbsolutePathTask(t)) {
|
|
1008
|
+
const handled = await handleSlash(t);
|
|
1009
|
+
if (handled === false) return;
|
|
1010
|
+
// /agent·/firm·/resume 처럼 대화 대상을 정한 명령이면 픽커를 빠져나가 대화 루프로 전환한다.
|
|
1011
|
+
if (state.subject) return ask();
|
|
1012
|
+
return pick(true);
|
|
1013
|
+
}
|
|
1014
|
+
const ags = H.listAgents(db);
|
|
1015
|
+
if (/^\d+$/.test(t)) {
|
|
1016
|
+
const n = parseInt(t, 10);
|
|
1017
|
+
if (n >= 1 && n <= ags.length) return chooseAndStart(setSubjectAgent, ags[n - 1]);
|
|
1018
|
+
ui.warn(ui.t("picker.noNum"));
|
|
1019
|
+
return pick(true);
|
|
1020
|
+
}
|
|
1021
|
+
if (/^firm\s+/i.test(t)) {
|
|
1022
|
+
const f = H.resolveFirm(db, t.replace(/^firm\s+/i, "").trim());
|
|
1023
|
+
if (f) return chooseAndStart(setSubjectFirm, f);
|
|
1024
|
+
ui.warn(ui.t("picker.noFirm"));
|
|
1025
|
+
return pick(true);
|
|
1026
|
+
}
|
|
1027
|
+
const a = H.resolveAgent(db, t);
|
|
1028
|
+
if (a) return chooseAndStart(setSubjectAgent, a);
|
|
1029
|
+
const f = H.resolveFirm(db, t);
|
|
1030
|
+
if (f) return chooseAndStart(setSubjectFirm, f);
|
|
1031
|
+
if (H.autoRouteAgent) {
|
|
1032
|
+
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1033
|
+
if (choice) {
|
|
1034
|
+
setSubjectAgent(choice.agent);
|
|
1035
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1036
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
1037
|
+
routingNote(state.subject);
|
|
1038
|
+
await runTurn(t);
|
|
1039
|
+
return ask();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
ui.warn(ui.t("picker.noMatch", t));
|
|
1043
|
+
return pick(true);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 한 줄 입력 처리(공용): ! 셸 · / 명령 · 미선택 시 번호/이름/자동라우팅 · 그 외 턴 실행. readline·composer 양쪽이 호출.
|
|
1048
|
+
async function processLine(t) {
|
|
1049
|
+
if (t.startsWith("!")) {
|
|
1050
|
+
runShell(t.slice(1).trim());
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (t.startsWith("/") && !input.isAbsolutePathTask(t)) {
|
|
1054
|
+
await handleSlash(t); // /exit 는 내부에서 process.exit
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (!state.subject) {
|
|
1058
|
+
const ags = H.listAgents(db);
|
|
1059
|
+
const single = !/\s/.test(t);
|
|
1060
|
+
if (/^\d+$/.test(t)) {
|
|
1061
|
+
const n = parseInt(t, 10);
|
|
1062
|
+
if (n >= 1 && n <= ags.length) {
|
|
1063
|
+
setSubjectAgent(ags[n - 1]);
|
|
1064
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
1065
|
+
routingNote(state.subject);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (single) {
|
|
1070
|
+
const a = H.resolveAgent(db, t);
|
|
1071
|
+
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1072
|
+
const f = H.resolveFirm(db, t);
|
|
1073
|
+
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1074
|
+
}
|
|
1075
|
+
if (H.autoRouteAgent) {
|
|
1076
|
+
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1077
|
+
if (choice) {
|
|
1078
|
+
setSubjectAgent(choice.agent);
|
|
1079
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1080
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
1081
|
+
routingNote(state.subject);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
await runTurn(expandMentions(t));
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ── composer (raw-mode bottom box) main loop ──
|
|
1089
|
+
function composerStatus() {
|
|
1090
|
+
const rt = runtimeLabel(state.runtime);
|
|
1091
|
+
const subj = state.subject ? state.subject.label : ui.t("composer.autoroute");
|
|
1092
|
+
const eff = state.effort ? " · " + state.effort : "";
|
|
1093
|
+
return `${rt} · ${state.permission}${eff} · ${subj} · ${ui.t("composer.hint")}`;
|
|
1094
|
+
}
|
|
1095
|
+
async function composerLoop() {
|
|
1096
|
+
let buffer = "";
|
|
1097
|
+
while (!closed) {
|
|
1098
|
+
let r;
|
|
1099
|
+
try {
|
|
1100
|
+
r = await composer.read({
|
|
1101
|
+
glyph: buffer ? "…" : "›",
|
|
1102
|
+
status: composerStatus(),
|
|
1103
|
+
suggest: (l) => input.slashCommandSuggestions(l),
|
|
1104
|
+
complete: completer,
|
|
1105
|
+
});
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
ui.error((e && e.message) || String(e));
|
|
1108
|
+
r = { value: "" };
|
|
1109
|
+
}
|
|
1110
|
+
if (r.exit || r.eof) {
|
|
1111
|
+
ui.line(ui.c.dim(ui.t("bye")));
|
|
1112
|
+
return process.exit(0);
|
|
1113
|
+
}
|
|
1114
|
+
const line = r.value || "";
|
|
1115
|
+
if (input.isContinuation(line)) {
|
|
1116
|
+
buffer += input.stripContinuation(line) + "\n";
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const t = (buffer + line).trim();
|
|
1120
|
+
buffer = "";
|
|
1121
|
+
if (!t) continue;
|
|
1122
|
+
try {
|
|
1123
|
+
await processLine(t);
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
ui.error((e && e.message) || String(e));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
process.exit(0);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// ── readline fallback loop (non-TTY / AGENTLAS_CLASSIC_INPUT=1) ── (multiline: trailing "\\" continues)
|
|
1132
|
+
function ask(buffer) {
|
|
1133
|
+
if (closed) return process.exit(0);
|
|
1134
|
+
if (slashPalette.setEnabled) slashPalette.setEnabled(true);
|
|
1135
|
+
const cont = buffer != null;
|
|
1136
|
+
rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
|
|
1137
|
+
if (input.isContinuation(line)) {
|
|
1138
|
+
return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
|
|
1139
|
+
}
|
|
1140
|
+
const full = (cont ? buffer + "\n" : "") + (line || "");
|
|
1141
|
+
const t = full.trim();
|
|
1142
|
+
if (!t) return ask();
|
|
1143
|
+
slashPalette.clear();
|
|
1144
|
+
if (rl.terminal && rl.history && rl.history[0] !== t) rl.history.unshift(t);
|
|
1145
|
+
input.persistHistory(rl);
|
|
1146
|
+
await processLine(t);
|
|
1147
|
+
if (!closed) ask();
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ── boot: first-run wizard, then banner + picker/loop ──
|
|
1152
|
+
async function bootstrap() {
|
|
1153
|
+
if (!prefs.onboarded) {
|
|
1154
|
+
try {
|
|
1155
|
+
const { runOnboard } = require("./agentlas-onboard.cjs");
|
|
1156
|
+
const result = await runOnboard({ ui, rl, helpers: H });
|
|
1157
|
+
Object.assign(prefs, result);
|
|
1158
|
+
ui.lang = terminalLang(prefs, opts);
|
|
1159
|
+
state.permission = prefs.permission || state.permission;
|
|
1160
|
+
if (prefs.runtime && prefs.runtime !== "auto" && H.RUNTIME_BIN[prefs.runtime] && H.which(H.RUNTIME_BIN[prefs.runtime])) {
|
|
1161
|
+
state.runtime = { mode: "cli", kind: prefs.runtime };
|
|
1162
|
+
}
|
|
1163
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
1164
|
+
ui.line("");
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
ui.error((e && e.message) || String(e));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
baseRuntime = state.runtime; // lock in the session default (post-wizard) before per-agent routing
|
|
1170
|
+
if (state.subject && state.subject.capAgent) {
|
|
1171
|
+
applyRuntimeFor(state.subject);
|
|
1172
|
+
// refresh the label for the chosen language (initial subject came pre-built from the entry)
|
|
1173
|
+
state.subject.label = displayName(state.subject.capAgent) + (state.subject.kind === "firm" ? " CEO" : "");
|
|
1174
|
+
}
|
|
1175
|
+
showBanner();
|
|
1176
|
+
if (state.subject) {
|
|
1177
|
+
routingNote(state.subject);
|
|
1178
|
+
} else {
|
|
1179
|
+
// Claude Code처럼 번호 픽커 없이 바로 입력. 할 일을 입력하면 자동 라우팅된다.
|
|
1180
|
+
ui.line(" " + ui.c.dim(ui.t("picker.hint")));
|
|
1181
|
+
}
|
|
1182
|
+
if (composer) {
|
|
1183
|
+
handoff = true;
|
|
1184
|
+
try { rl.close(); } catch { /* ignore */ } // hand stdin to the raw-mode composer
|
|
1185
|
+
composerLoop();
|
|
1186
|
+
} else {
|
|
1187
|
+
ask();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
bootstrap();
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function printHelp(ui) {
|
|
1194
|
+
const c = ui.c;
|
|
1195
|
+
ui.line("");
|
|
1196
|
+
ui.rule("Help");
|
|
1197
|
+
ui.line(" " + c.bold(c.text("Agentlas runs local agents from this terminal, with runtime, permission, files, shell, and history controls.")));
|
|
1198
|
+
ui.line("");
|
|
1199
|
+
ui.line(" " + c.faint("Commands"));
|
|
1200
|
+
const rows = [
|
|
1201
|
+
[ui.t("help.talkKey"), ui.t("help.talk")],
|
|
1202
|
+
["/skills", ui.t("help.skills")],
|
|
1203
|
+
["/agents", ui.t("help.agents")],
|
|
1204
|
+
["/team [agent rt]", ui.t("help.team")],
|
|
1205
|
+
["/agent <name>", ui.t("help.agent")],
|
|
1206
|
+
["/firms · /firm <name>", ui.t("help.firms")],
|
|
1207
|
+
["/runtime <kind>", ui.t("help.runtime")],
|
|
1208
|
+
["/model <id>", ui.t("help.model")],
|
|
1209
|
+
["/effort <lvl>", ui.t("help.effort")],
|
|
1210
|
+
["/permission <lvl>", ui.t("help.permission")],
|
|
1211
|
+
["/permissions", ui.t("help.permissions")],
|
|
1212
|
+
["/setup", ui.t("help.setup")],
|
|
1213
|
+
["/cwd [path]", ui.t("help.cwd")],
|
|
1214
|
+
["/memory", ui.t("help.memory")],
|
|
1215
|
+
["/ontology [text]", ui.t("help.ontology")],
|
|
1216
|
+
["/side <question>", ui.t("help.side")],
|
|
1217
|
+
["/status", ui.t("help.status")],
|
|
1218
|
+
["/cost", ui.t("help.cost")],
|
|
1219
|
+
["/multimodal", ui.t("help.multimodal")],
|
|
1220
|
+
["/mcp", ui.t("help.mcp")],
|
|
1221
|
+
["/diff", ui.t("help.diff")],
|
|
1222
|
+
["/history", ui.t("help.history")],
|
|
1223
|
+
["/resume [n]", ui.t("help.resume")],
|
|
1224
|
+
["/compact", ui.t("help.compact")],
|
|
1225
|
+
["/import <path>", ui.t("help.import")],
|
|
1226
|
+
["/storm <goal>", ui.t("help.storm")],
|
|
1227
|
+
["/swarm <goal>", ui.t("help.swarm")],
|
|
1228
|
+
["/build <request>", ui.t("help.build")],
|
|
1229
|
+
["/route <request>", ui.t("help.route")],
|
|
1230
|
+
["/research <sub>", ui.t("help.research")],
|
|
1231
|
+
["/marketplace", ui.t("help.market")],
|
|
1232
|
+
["/install <slug>", ui.t("help.install")],
|
|
1233
|
+
["/clear", ui.t("help.clear")],
|
|
1234
|
+
["/doctor", ui.t("help.doctor")],
|
|
1235
|
+
["/keybindings", ui.t("help.keybindings")],
|
|
1236
|
+
["/exit", ui.t("help.exit")],
|
|
1237
|
+
];
|
|
1238
|
+
for (const [k, v] of rows) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
|
|
1239
|
+
ui.line("");
|
|
1240
|
+
printKeybindings(ui);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function printKeybindings(ui) {
|
|
1244
|
+
const c = ui.c;
|
|
1245
|
+
ui.line(" " + c.faint(ui.t("help.tipsTitle")));
|
|
1246
|
+
const tips = [
|
|
1247
|
+
["/", ui.t("help.slash")],
|
|
1248
|
+
["@path", ui.t("help.atfile")],
|
|
1249
|
+
["!cmd", ui.t("help.bang")],
|
|
1250
|
+
["\\ + Enter", ui.t("help.multiline")],
|
|
1251
|
+
["Tab", ui.t("help.tab")],
|
|
1252
|
+
["Up / Down", ui.t("help.arrows")],
|
|
1253
|
+
["Ctrl-C", ui.t("help.ctrlc")],
|
|
1254
|
+
];
|
|
1255
|
+
for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
module.exports = { startRepl, runtimeLabel };
|