agentlas 0.9.10 → 1.0.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -1,1780 +0,0 @@
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 crypto = require("node:crypto");
12
- const { Ui } = require("./agentlas-ui.cjs");
13
- const banner = require("./agentlas-banner.cjs");
14
- const { runNativeTurn } = require("./agentlas-native-host.cjs");
15
- const { runApiTurn } = require("./agentlas-api-agent.cjs");
16
- const caps = require("./agentlas-capabilities.cjs");
17
- const input = require("./agentlas-input.cjs");
18
- const i18n = require("./agentlas-i18n.cjs");
19
- const style = require("./agentlas-style.cjs");
20
- const permissions = require("./agentlas-permissions.cjs");
21
-
22
- function runtimeLabel(rt) {
23
- if (!rt) return "(none)";
24
- if (rt.mode === "cli") return rt.kind;
25
- return `${rt.backend}${rt.model ? " · " + rt.model : ""}`;
26
- }
27
-
28
- function runtimePromptForSession(prompt, ctx, rt, session, helpers) {
29
- const resumesServerSide = rt && (rt.kind === "claude-code" || rt.kind === "codex");
30
- if (!session?.id || !resumesServerSide || typeof helpers?.memoryEmitterPrompt !== "function") return prompt;
31
- return `${prompt}\n\n${helpers.memoryEmitterPrompt(prompt, ctx)}`;
32
- }
33
-
34
- function normalizeLang(value) {
35
- const v = String(value || "").toLowerCase();
36
- return v === "en" || v === "ko" ? v : null;
37
- }
38
-
39
- function terminalLang(prefs, opts) {
40
- const explicit = normalizeLang((opts && opts.lang) || process.env.AGENTLAS_TERMINAL_LANG || process.env.AGENTLAS_LANG);
41
- if (explicit) return explicit;
42
- // 온보딩에서 고른 언어(cli-prefs.json lang)를 기본 존중. 없으면 en.
43
- return normalizeLang(prefs && prefs.lang) || "en";
44
- }
45
-
46
- // Hides the trailing "## Memory Events" block from the live stream while keeping the full
47
- // text for curation. Holds back the last heading.length chars so a split heading is safe too.
48
- function makeMemoryGuard(ui, heading) {
49
- const N = heading.length;
50
- let acc = "";
51
- let printed = 0;
52
- let cut = false;
53
- const flush = () => {
54
- if (cut) return;
55
- const idx = acc.indexOf(heading);
56
- if (idx >= 0) {
57
- if (idx > printed) ui.streamDelta(acc.slice(printed, idx));
58
- printed = idx;
59
- cut = true;
60
- } else if (acc.length > printed) {
61
- ui.streamDelta(acc.slice(printed));
62
- printed = acc.length;
63
- }
64
- };
65
- return {
66
- c: ui.c,
67
- lang: ui.lang,
68
- t: (...a) => ui.t(...a),
69
- streamStart: () => ui.streamStart(),
70
- streamDelta: (t) => {
71
- if (cut) {
72
- acc += t;
73
- return;
74
- }
75
- acc += t;
76
- const idx = acc.indexOf(heading);
77
- if (idx >= 0) {
78
- if (idx > printed) ui.streamDelta(acc.slice(printed, idx));
79
- printed = idx;
80
- cut = true;
81
- return;
82
- }
83
- const safe = acc.length - N;
84
- if (safe > printed) {
85
- ui.streamDelta(acc.slice(printed, safe));
86
- printed = safe;
87
- }
88
- },
89
- streamEnd: () => {
90
- flush();
91
- ui.streamEnd();
92
- },
93
- stopSpinner: (...a) => ui.stopSpinner(...a),
94
- tool: (...a) => ui.tool(...a),
95
- toolResult: (...a) => ui.toolResult(...a),
96
- info: (...a) => ui.info(...a),
97
- warn: (...a) => ui.warn(...a),
98
- error: (...a) => ui.error(...a),
99
- status: (...a) => ui.status(...a),
100
- ok: (...a) => ui.ok(...a),
101
- cost: (...a) => ui.cost(...a),
102
- line: (...a) => ui.line(...a),
103
- applyTaskTool: (...a) => ui.applyTaskTool(...a),
104
- applyTaskResult: (...a) => ui.applyTaskResult(...a),
105
- replaceTasks: (...a) => ui.replaceTasks(...a),
106
- };
107
- }
108
-
109
- // 스트리밍 마크다운 렌더(Claude Code 스타일): 줄 단위로 모아 ui.renderInline 으로
110
- // **bold**/#heading/`code`/불릿/코드펜스를 ANSI로 렌더한다(예전엔 마크다운을 제거했음).
111
- function makeStyleGuard(ui) {
112
- let buf = "";
113
- let inCode = false;
114
- const emit = (line) => {
115
- if (/^\s*```/.test(line)) {
116
- inCode = !inCode;
117
- ui.write((ui.enabled ? ui.c.faint(line) : line) + "\n");
118
- return;
119
- }
120
- if (inCode) {
121
- ui.write((ui.enabled ? ui.c.dim(line) : line) + "\n");
122
- return;
123
- }
124
- ui.write((ui.enabled ? ui.renderInline(line) : line) + "\n");
125
- };
126
- return {
127
- c: ui.c,
128
- lang: ui.lang,
129
- t: (...a) => ui.t(...a),
130
- streamStart: () => {
131
- buf = "";
132
- inCode = false;
133
- // This guard emits complete lines, so the persistent turn footer can safely stay visible.
134
- ui.streamStart(true);
135
- },
136
- streamDelta: (text) => {
137
- if (!text) return;
138
- ui.stopSpinner();
139
- buf += text;
140
- let nl;
141
- while ((nl = buf.indexOf("\n")) >= 0) {
142
- emit(buf.slice(0, nl));
143
- buf = buf.slice(nl + 1);
144
- }
145
- },
146
- streamEnd: () => {
147
- if (buf.length) {
148
- emit(buf);
149
- buf = "";
150
- }
151
- ui.streamEnd();
152
- },
153
- tool: (...a) => ui.tool(...a),
154
- toolResult: (...a) => ui.toolResult(...a),
155
- info: (...a) => ui.info(...a),
156
- warn: (...a) => ui.warn(...a),
157
- error: (...a) => ui.error(...a),
158
- status: (...a) => ui.status(...a),
159
- ok: (...a) => ui.ok(...a),
160
- cost: (...a) => ui.cost(...a),
161
- line: (...a) => ui.line(...a),
162
- stopSpinner: (...a) => ui.stopSpinner(...a),
163
- applyTaskTool: (...a) => ui.applyTaskTool(...a),
164
- applyTaskResult: (...a) => ui.applyTaskResult(...a),
165
- replaceTasks: (...a) => ui.replaceTasks(...a),
166
- };
167
- }
168
-
169
- // startRepl({ db, subject|null, runtime, permission, cwd, helpers, prefs, savePrefs })
170
- function startRepl(opts) {
171
- const { db } = opts;
172
- const H = opts.helpers;
173
- const prefs = applyPreferenceDefaults(opts.prefs || {});
174
- prefs.agentRuntime = prefs.agentRuntime || {}; // { agentSlug|firmSlug: runtimeSpec|"auto" }
175
- let baseRuntime = opts.runtime; // session default; per-agent runtime auto-routes from this
176
- const ui = new Ui({ lang: terminalLang(prefs, opts) });
177
- const state = {
178
- subject: opts.subject || null,
179
- runtime: opts.runtime,
180
- permission: opts.permission == null ? "write" : permissions.normalize(opts.permission),
181
- cwd: opts.cwd,
182
- history: [],
183
- native: {}, // kind → { id }
184
- projectPath: opts.projectPath || null,
185
- routePreambleOnce: null,
186
- effort: prefs.effort || null, // /effort: low|medium|high|max → 런타임별 reasoning 강도
187
- modelPinned: false, // true only after an explicit /model <id>; /model auto clears it
188
- effortPinned: prefs.effortPinned === true || Boolean(prefs.effort), // /effort auto is the only automatic mode
189
- cost: {}, // runtimeLabel → { turns, in, out, cost, ms } — session usage ledger
190
- };
191
-
192
- function showBanner() {
193
- banner.renderBanner({
194
- ui,
195
- version: opts.version,
196
- runtimeLabel: runtimeLabel(state.runtime),
197
- subjectLabel: state.subject ? state.subject.label : null,
198
- permission: state.permission,
199
- cwd: state.cwd,
200
- });
201
- }
202
-
203
- const completer = input.makeCompleter({
204
- getAgentSlugs: () => { try { return H.listAgents(db).map((a) => a.slug); } catch { return []; } },
205
- getFirmSlugs: () => { try { return H.listFirms(db).map((f) => f.slug); } catch { return []; } },
206
- getCwd: () => state.cwd,
207
- });
208
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: !!process.stdin.isTTY, completer, historySize: input.HISTORY_MAX });
209
- input.attachHistory(rl);
210
- const slashPalette = input.attachSlashPalette(rl, { ui, force: true });
211
- // Raw-mode bottom input box (Claude Code / Hermes style). TTY only; readline is the fallback.
212
- const { createComposer } = require("./agentlas-composer.cjs");
213
- const useComposer = !!process.stdin.isTTY && process.env.AGENTLAS_CLASSIC_INPUT !== "1";
214
- const composer = useComposer
215
- ? createComposer({ ui, loadHistory: () => input.loadHistory(), saveHistory: (h) => input.saveHistory(h) })
216
- : null;
217
- let handoff = false; // set before rl.close() when handing stdin to the composer
218
- let busy = false;
219
- let closed = false;
220
- let currentAbort = null;
221
- let idleExitArmedUntil = 0;
222
- const permissionCycle = permissions.createCycleController();
223
- rl.on("close", () => {
224
- if (handoff) return; // intentionally closed to hand stdin to the raw-mode composer
225
- closed = true;
226
- if (!busy) process.exit(0);
227
- });
228
- if (useComposer) {
229
- // In composer mode rl is closed; Ctrl-C at the box is a keypress, so process SIGINT only fires mid-turn.
230
- process.on("SIGINT", () => {
231
- if (busy && currentAbort) {
232
- currentAbort.abort();
233
- ui.warn(ui.t("interrupted"));
234
- }
235
- });
236
- process.on("exit", () => {
237
- try { if (process.stdin.setRawMode) process.stdin.setRawMode(false); } catch { /* ignore */ }
238
- });
239
- }
240
- rl.on("SIGINT", () => {
241
- if (busy && currentAbort) {
242
- currentAbort.abort();
243
- ui.warn(ui.t("interrupted"));
244
- } else {
245
- const now = Date.now();
246
- if (now < idleExitArmedUntil) {
247
- ui.line("");
248
- ui.line(ui.c.dim(ui.t("bye")));
249
- rl.close();
250
- process.exit(0);
251
- }
252
- idleExitArmedUntil = now + 3000;
253
- ui.warn(ui.t("ctrlcAgain"));
254
- }
255
- });
256
-
257
- function ctxNow() {
258
- return { projectPath: state.projectPath, agentId: state.subject && state.subject.id, permission: state.permission, cwd: state.cwd, lang: ui.lang };
259
- }
260
-
261
- function setPermission(value, options = {}) {
262
- const notify = options.notify !== false;
263
- const persist = options.persist !== false;
264
- const level = permissions.normalize(value);
265
- state.permission = level;
266
- if (persist) {
267
- prefs.permission = level;
268
- if (opts.savePrefs) opts.savePrefs(prefs);
269
- }
270
- if (notify) ui.ok(ui.t("permSet", level));
271
- return level;
272
- }
273
-
274
- // Session usage ledger — accumulate per runtime label (host advantage: no single-model CLI can show this).
275
- function recordCost(label, usage) {
276
- const e = state.cost[label] || (state.cost[label] = { turns: 0, in: 0, out: 0, cost: 0, ms: 0 });
277
- e.turns += 1;
278
- if (usage) {
279
- if (usage.input_tokens) e.in += usage.input_tokens;
280
- if (usage.output_tokens) e.out += usage.output_tokens;
281
- if (usage.cost_usd) e.cost += usage.cost_usd;
282
- if (usage.duration_ms) e.ms += usage.duration_ms;
283
- }
284
- }
285
-
286
- // /resume 용 세션 영속화 — 네이티브 런타임 세션ID + 에이전트/런타임/cwd/제목을 파일에 저장.
287
- function persistSession(kind, sessionId, titlePrompt) {
288
- if (!H.sessionsSave || !H.sessionsLoad || !sessionId || !state.subject) return;
289
- try {
290
- const list = H.sessionsLoad().filter((s) => !(s.agentSlug === state.subject.slug && s.kind === kind));
291
- list.unshift({
292
- ts: Date.now(),
293
- agentSlug: state.subject.slug,
294
- agentLabel: state.subject.label,
295
- kind,
296
- sessionId,
297
- cwd: state.cwd,
298
- title: String(titlePrompt || "").replace(/\s+/g, " ").trim().slice(0, 60),
299
- });
300
- H.sessionsSave(list);
301
- } catch {
302
- /* ignore */
303
- }
304
- }
305
-
306
- // ── run one turn ──
307
- async function runTurn(prompt, runOptions = {}) {
308
- busy = true;
309
- currentAbort = new AbortController();
310
- const signal = currentAbort.signal;
311
- let experienceRunId = `terminal-run:${crypto.randomUUID()}`;
312
- const runStartedAt = Date.now();
313
- let experienceFinalized = false;
314
- let experienceContext = null;
315
- let memorySettled = false;
316
- let memoryContext = null;
317
- let memoryRuntime = null;
318
- let memoryModel = null;
319
- let memoryOutput = "";
320
- let memoryOutcome = "failed";
321
- try {
322
- ui.beginTurn({
323
- ...composerMeta(),
324
- usage: () => usageSummaryLine(), // 턴 중에도 최신 사용량을 footer에 라이브 반영
325
- onInterrupt: () => {
326
- if (!currentAbort || currentAbort.signal.aborted) return;
327
- currentAbort.abort();
328
- ui.warn(ui.t("interrupted"));
329
- },
330
- }); // 작업 중에도 composer/status bar와 실제 runtime task 목록을 화면 하단에 유지
331
- if (H.ensureProjectForExecution) {
332
- state.projectPath = H.ensureProjectForExecution(
333
- db,
334
- state.cwd,
335
- state.permission,
336
- "terminal-interactive-turn",
337
- );
338
- }
339
- const recordHistoryEntry = !runOptions.side;
340
- const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
341
- const curatedMemories = [];
342
- const rt = state.runtime;
343
- const priorContextDigest = crypto.createHash("sha256")
344
- .update(JSON.stringify(state.history.map((item) => ({ role: item.role, text: item.text }))))
345
- .digest("hex");
346
- const memoryTurn = typeof H.beginMemoryTurn === "function"
347
- ? H.beginMemoryTurn(db, prompt, {
348
- ...ctxNow(),
349
- surface: runOptions.side ? "terminal-side-turn" : "terminal-interactive-turn",
350
- conversationRef: rt.mode === "cli" ? state.native[rt.kind]?.id : null,
351
- priorContextDigest,
352
- // Provider session resume is conversation context, not turn
353
- // identity. Every submitted host turn receives a fresh id.
354
- stableTurnId: `terminal-turn:${crypto.randomUUID()}`,
355
- })
356
- : null;
357
- if (memoryTurn?.turnId) experienceRunId = `terminal-run:${memoryTurn.turnId}`;
358
- const ctx = {
359
- ...ctxNow(),
360
- lang: targetLang,
361
- uiLang: ui.lang,
362
- curatedMemories,
363
- memoryTurn,
364
- turnId: memoryTurn?.turnId || null,
365
- };
366
- memoryContext = ctx;
367
- memoryRuntime = rt;
368
- let workloadResolution = null;
369
- if (state.subject && state.subject.kind === "firm" && typeof H.allocateWorkload === "function") {
370
- ui.status(targetLang === "ko" ? "상위 AI가 팀 작업의 모델 비용을 배정 중…" : "Higher-level AI is allocating the team model…");
371
- const planned = await H.allocateWorkload(db, prompt, {
372
- runtime: rt,
373
- cwd: state.cwd,
374
- projectPath: state.projectPath,
375
- agentId: state.subject.id,
376
- lang: targetLang,
377
- mode: "team",
378
- modelPin: state.modelPinned ? rt.model : null,
379
- effortPin: state.effortPinned ? (state.effort || "none") : undefined,
380
- onWarning: (message) => ui.warn(message),
381
- });
382
- workloadResolution = planned && planned.resolution;
383
- if (workloadResolution && workloadResolution.fallbackReason) {
384
- ui.info(`model route: ${workloadResolution.source} · ${workloadResolution.model || runtimeLabel(rt)} · ${workloadResolution.fallbackReason}`);
385
- }
386
- }
387
- const selectedModel = (workloadResolution && workloadResolution.model) || rt.model || null;
388
- memoryModel = selectedModel;
389
- const selectedEffort = workloadResolution ? workloadResolution.effort : state.effort || null;
390
- const costLabel = selectedModel ? `${runtimeLabel(rt)} · ${selectedModel}` : runtimeLabel(rt);
391
- experienceContext = { ctx, rt, selectedModel, curatedMemories, targetLang };
392
- const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
393
- ui._lastUsage = null;
394
- const assistantUi = makeStyleGuard(ui);
395
- const thinkingText = i18n.t(targetLang, "thinkingWith", costLabel);
396
- ui.status(thinkingText);
397
- if (rt.mode === "cli") {
398
- const bin = H.which(H.RUNTIME_BIN[rt.kind]) || H.RUNTIME_BIN[rt.kind];
399
- const session = state.native[rt.kind] || (state.native[rt.kind] = {});
400
- const subjectSystem = state.routePreambleOnce
401
- ? `${state.routePreambleOnce}\n\n${state.subject.system}`
402
- : state.subject.system;
403
- state.routePreambleOnce = null;
404
- const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
405
- // claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
406
- // 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
407
- // 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
408
- // 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
409
- const resumesServerSide = rt.kind === "claude-code" || rt.kind === "codex";
410
- // Claude/Codex resume keeps the first turn's system prompt. Append the
411
- // current host contract to this turn's user payload so a resumed model
412
- // cannot reuse a stale Memory Ticket id from an earlier turn.
413
- const runtimePrompt = runtimePromptForSession(prompt, ctx, rt, session, H);
414
- const memoryGuard = makeMemoryGuard(assistantUi, H.eventsHeading());
415
- const connectedMcpServers = state.permission === "full" && H.mcpServers
416
- ? H.mcpServers(db).filter((server) =>
417
- server.enabled && server.transport === "stdio" && server.runtimeEligible === true && server.runtimeConsented === true)
418
- : [];
419
- const res = await runNativeTurn({
420
- kind: rt.kind,
421
- bin,
422
- prompt: runtimePrompt,
423
- systemPrompt: session.id && resumesServerSide ? "" : sys,
424
- cwd: state.cwd,
425
- permission: state.permission,
426
- session,
427
- model: selectedModel, // explicit /model pin or higher-level AI allocation
428
- effort: selectedEffort, // explicit /effort pin or higher-level AI allocation
429
- mcpServers: connectedMcpServers,
430
- env: runEnv,
431
- ui: memoryGuard,
432
- signal,
433
- });
434
- memoryOutput = String(res.text || "");
435
- memoryOutcome = res.error ? "failed" : "succeeded";
436
- let at = "";
437
- let memoryResult = null;
438
- if (typeof H.completeMemoryTurn === "function") {
439
- try {
440
- memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
441
- model: selectedModel,
442
- outcome: memoryOutcome,
443
- requestText: prompt,
444
- invokeCurator: !res.error,
445
- });
446
- memorySettled = true;
447
- } catch {
448
- // finally retries once without semantic Curator authority.
449
- }
450
- for (const memory of memoryResult?.curatedMemories || []) {
451
- if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
452
- }
453
- at = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
454
- } else {
455
- at = (H.sanitizeAssistantText(res.text || "") || "").trim();
456
- }
457
- if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
458
- recordCost(costLabel, res.usage);
459
- if (!runOptions.side && session.id && !res.error) persistSession(rt.kind, session.id, prompt);
460
- if (typeof H.finalizeExperienceRun === "function") {
461
- H.finalizeExperienceRun(db, {
462
- agentId: ctx.agentId,
463
- projectPath: ctx.projectPath,
464
- cwd: state.cwd,
465
- runtime: rt,
466
- permission: state.permission,
467
- model: selectedModel,
468
- mcpServers: connectedMcpServers,
469
- curatedMemories,
470
- taskHint: prompt,
471
- outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
472
- usage: res.usage,
473
- durationMs: Date.now() - runStartedAt,
474
- runId: experienceRunId,
475
- lang: targetLang,
476
- });
477
- experienceFinalized = true;
478
- }
479
- } else {
480
- const subjectSystem = state.routePreambleOnce
481
- ? `${state.routePreambleOnce}\n\n${state.subject.system}`
482
- : state.subject.system;
483
- state.routePreambleOnce = null;
484
- const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
485
- let apiKey = null;
486
- if (rt.backend !== "ollama") {
487
- apiKey = await H.apiKey(rt.backend);
488
- if (!apiKey) {
489
- ui.error(ui.t("noKey", rt.backend));
490
- return;
491
- }
492
- }
493
- const messages = state.history
494
- .filter((h) => h.text && h.text.trim())
495
- .map((h) => ({ role: h.role, content: h.text }))
496
- .concat([{ role: "user", content: prompt }]);
497
- const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
498
- const res = await runApiTurn({
499
- backend: rt.backend,
500
- model: selectedModel || H.defaultApiModel(rt.backend),
501
- apiKey,
502
- system: sys,
503
- messages,
504
- ctx: { ...ctx, env: runEnv },
505
- ui: guard,
506
- signal,
507
- });
508
- memoryOutput = String(res.text || "");
509
- memoryOutcome = "succeeded";
510
- let cleaned = "";
511
- if (typeof H.completeMemoryTurn === "function") {
512
- let memoryResult = null;
513
- try {
514
- memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
515
- model: selectedModel,
516
- outcome: memoryOutcome,
517
- requestText: prompt,
518
- });
519
- memorySettled = true;
520
- } catch {
521
- // finally retries once without semantic Curator authority.
522
- }
523
- for (const memory of memoryResult?.curatedMemories || []) {
524
- if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
525
- }
526
- cleaned = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
527
- } else {
528
- cleaned = (H.sanitizeAssistantText(res.text || "") || "").trim();
529
- }
530
- if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
531
- recordCost(costLabel, ui._lastUsage);
532
- if (typeof H.finalizeExperienceRun === "function") {
533
- H.finalizeExperienceRun(db, {
534
- agentId: ctx.agentId,
535
- projectPath: ctx.projectPath,
536
- cwd: state.cwd,
537
- runtime: rt,
538
- permission: state.permission,
539
- model: selectedModel,
540
- curatedMemories,
541
- taskHint: prompt,
542
- outcome: { status: "succeeded", failureCode: null },
543
- usage: ui._lastUsage,
544
- durationMs: Date.now() - runStartedAt,
545
- runId: experienceRunId,
546
- lang: targetLang,
547
- });
548
- experienceFinalized = true;
549
- }
550
- }
551
- } catch (e) {
552
- ui.stopSpinner();
553
- memoryOutcome = signal.aborted ? "cancelled" : "failed";
554
- if (!experienceFinalized && experienceContext && typeof H.finalizeExperienceRun === "function") {
555
- H.finalizeExperienceRun(db, {
556
- agentId: experienceContext.ctx.agentId,
557
- projectPath: experienceContext.ctx.projectPath,
558
- cwd: state.cwd,
559
- runtime: experienceContext.rt,
560
- permission: experienceContext.ctx.permission,
561
- model: experienceContext.selectedModel,
562
- curatedMemories: experienceContext.curatedMemories,
563
- taskHint: prompt,
564
- outcome: { status: signal.aborted ? "cancelled" : "failed", failureCode: signal.aborted ? "operator-cancelled" : "runtime-error" },
565
- durationMs: Date.now() - runStartedAt,
566
- runId: experienceRunId,
567
- lang: experienceContext.targetLang,
568
- });
569
- experienceFinalized = true;
570
- }
571
- if (signal.aborted) {
572
- // user Ctrl-C — SIGINT handler already printed
573
- } else if (e && e.name === "AbortError") {
574
- ui.warn(ui.t("stalled"));
575
- } else {
576
- ui.error((e && e.message) || String(e));
577
- }
578
- } finally {
579
- if (!memorySettled && memoryContext?.turnId && typeof H.completeMemoryTurn === "function") {
580
- try {
581
- await H.completeMemoryTurn(db, memoryOutput, memoryContext, memoryRuntime, {
582
- model: memoryModel,
583
- outcome: memoryOutcome,
584
- requestText: prompt,
585
- invokeCurator: false,
586
- });
587
- memorySettled = true;
588
- } catch {
589
- ui.warn(memoryContext.lang === "ko"
590
- ? "메모리 영수증을 저장하지 못했습니다. 응답 실행은 유지됩니다."
591
- : "The memory receipt could not be persisted; the response run is preserved.");
592
- }
593
- }
594
- busy = false;
595
- ui.endTurn();
596
- currentAbort = null;
597
- }
598
- }
599
-
600
- // ── slash commands ──
601
- function setRuntime(arg) {
602
- const cliKinds = { "claude-code": 1, claude: 1, codex: 1, gemini: 1 };
603
- const apiBackends = { anthropic: 1, openai: 1, google: 1, ollama: 1, upstage: 1 };
604
- let a = (arg || "").trim();
605
- if (a === "claude") a = "claude-code";
606
- if (cliKinds[a]) {
607
- const bin = H.which(H.RUNTIME_BIN[a]);
608
- if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
609
- state.runtime = { mode: "cli", kind: a };
610
- state.modelPinned = false;
611
- baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
612
- state.native = {};
613
- return ui.ok(ui.t("runtimeSet", a));
614
- }
615
- if (apiBackends[a]) {
616
- state.runtime =
617
- a === "ollama"
618
- ? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
619
- : { mode: "api", backend: a, model: null };
620
- state.modelPinned = false;
621
- baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
622
- return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
623
- }
624
- ui.warn(ui.t("runtimeUsage"));
625
- }
626
-
627
- // Show the English name when the chosen language is English (agents carry name_en).
628
- function displayName(a) {
629
- if (!a) return "";
630
- if (ui.lang === "en" && a.name_en && a.name_en !== a.name) return a.name_en;
631
- return a.name || a.name_en || "";
632
- }
633
- function installedKinds() {
634
- return caps.CLI_KINDS.filter((k) => H.which(H.RUNTIME_BIN[k]));
635
- }
636
- // Resolve the runtime a subject runs on: pinned (prefs) > capability auto-route > session default.
637
- function applyRuntimeFor(subject) {
638
- const pinned = prefs.agentRuntime[subject.slug];
639
- let spec;
640
- if (pinned && pinned !== "auto") spec = pinned;
641
- else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
642
- state.runtime = caps.runtimeFromSpec(spec);
643
- state.modelPinned = false;
644
- state.native = {};
645
- }
646
- // 이미지 능력 판정 warm-cache — 동기 호출자(applyRuntimeFor/배지)가 읽기 전에 비동기
647
- // 경로에서 상주 판정 서비스를 먼저 데운다. 러너 없음/실패는 어휘 폴백(라벨은 routingNote가 찍음).
648
- async function warmImageJudgment(agentRow) {
649
- if (!agentRow || typeof caps.resolveNeedsImage !== "function") return;
650
- try {
651
- await caps.resolveNeedsImage(agentRow);
652
- } catch {
653
- /* lexical fallback */
654
- }
655
- }
656
- // Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
657
- function routingNote(subject) {
658
- if (!subject || !caps.needsImage(subject.capAgent)) return;
659
- const spec = caps.specOf(state.runtime);
660
- if (caps.capsFor(spec).image) {
661
- if (spec !== caps.specOf(baseRuntime)) {
662
- // 판정 주체 라벨 — 모델 판정인지 결정적 폴백인지 반드시 밝힌다(조용한 폴백 금지).
663
- const judged = typeof caps.imageJudgmentSource === "function" && caps.imageJudgmentSource(subject.capAgent) === "llm";
664
- ui.info(ui.t("routedImage", spec) + " — " + ui.t(judged ? "judge.source.llm" : "judge.source.fallback"));
665
- }
666
- } else {
667
- ui.warn(ui.t("guard.imageWarn", caps.capsFor(spec).label || spec));
668
- }
669
- }
670
- function specToRuntime(spec) {
671
- return (!spec || spec === "auto") ? null : caps.runtimeFromSpec(spec);
672
- }
673
-
674
- function setSubjectAgent(agent) {
675
- state.subject = {
676
- kind: "agent",
677
- id: agent.id,
678
- slug: agent.slug,
679
- label: displayName(agent),
680
- system: agent.system_prompt || `You are ${agent.name}.`,
681
- capAgent: agent,
682
- };
683
- state.history = [];
684
- state.routePreambleOnce = null;
685
- applyRuntimeFor(state.subject);
686
- }
687
- // 직답 모드 — 전문 에이전트 확신이 없을 때. 페르소나 없음 + 능력(이미지) 라우팅 없음:
688
- // 런타임은 세션 기본(baseRuntime) 그대로라, 일반 질문이 gemini 등으로 끌려가지 않는다.
689
- function setSubjectDirect() {
690
- if (state.subject && state.subject.kind === "direct") return; // 연속 직답 — 세션/히스토리 유지
691
- state.subject = {
692
- kind: "direct",
693
- id: null,
694
- slug: "agentlas-direct",
695
- label: ui.lang === "ko" ? "Agentlas 직답" : "Agentlas direct",
696
- system: H.directSystemPrompt ? H.directSystemPrompt(ui.lang) : "You are the Agentlas terminal's default assistant.",
697
- capAgent: null,
698
- };
699
- state.history = [];
700
- state.routePreambleOnce = null;
701
- state.runtime = baseRuntime;
702
- state.native = {};
703
- }
704
- function setSubjectFirm(firm) {
705
- const sys = H.firmSystemPrompt(db, firm);
706
- state.subject = {
707
- kind: "firm",
708
- id: firm.ceo_agent_id,
709
- slug: firm.slug,
710
- label: displayName(firm) + " CEO",
711
- system: sys,
712
- capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
713
- };
714
- state.history = [];
715
- state.routePreambleOnce = null;
716
- applyRuntimeFor(state.subject);
717
- }
718
- async function switchSubject(kind, query) {
719
- if (kind === "agent") {
720
- const agent = H.resolveAgent(db, query);
721
- if (!agent) return ui.error(ui.t("noAgent", query));
722
- await warmImageJudgment(agent); // 런타임 자동 배정 전에 모델 판정을 데운다
723
- setSubjectAgent(agent);
724
- } else {
725
- const firm = H.resolveFirm(db, query);
726
- if (!firm) return ui.error(ui.t("noCompany", query));
727
- setSubjectFirm(firm);
728
- }
729
- ui.ok(ui.t("switched", state.subject.label));
730
- routingNote(state.subject);
731
- }
732
- // resolved runtime spec for any agent row (for display in roster / team)
733
- function resolvedSpec(agentRow, slug) {
734
- const pinned = prefs.agentRuntime[slug];
735
- if (pinned && pinned !== "auto") return pinned;
736
- return caps.autoRuntimeFor(agentRow, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
737
- }
738
-
739
- function printRoster() {
740
- const ags = H.listAgents(db);
741
- const firms = H.listFirms(db);
742
- ui.line("");
743
- ui.line(ui.c.dim(" " + ui.t("picker.agents")));
744
- ags.forEach((a, i) => {
745
- const spec = resolvedSpec(a, a.slug);
746
- const bdg = caps.needsImage(a) ? (caps.capsFor(spec).image ? "[image]" : "[image missing]") : "";
747
- ui.line(
748
- " " + ui.c.faint(String(i + 1).padStart(2)) + " " + ui.c.emerald(a.slug.padEnd(26)) + " " +
749
- ui.c.text((displayName(a) || "").padEnd(16)) + " " + ui.c.blue(spec) + (bdg ? " " + bdg : ""),
750
- );
751
- });
752
- if (firms.length) {
753
- ui.line(ui.c.dim(" " + ui.t("picker.companies")));
754
- firms.forEach((f) =>
755
- ui.line(" " + ui.c.emerald(("firm " + f.slug).padEnd(26)) + " " + ui.c.text(displayName(f)) + ui.c.dim(" (CEO)")),
756
- );
757
- }
758
- if (!ags.length && !firms.length) ui.line(" " + ui.c.dim(ui.t("picker.none")));
759
- }
760
-
761
- // /team — show or assign each agent's runtime (LLM). Auto-routed by capability unless pinned.
762
- function printTeam() {
763
- const ags = H.listAgents(db);
764
- ui.line("");
765
- ui.line(ui.c.dim(" " + ui.t("team.title")));
766
- for (const a of ags) {
767
- const pinned = prefs.agentRuntime[a.slug] && prefs.agentRuntime[a.slug] !== "auto";
768
- const spec = resolvedSpec(a, a.slug);
769
- const bdg = caps.needsImage(a) ? (caps.capsFor(spec).image ? "[image]" : "[image missing]") : "";
770
- ui.line(
771
- " " + ui.c.emerald(a.slug.padEnd(28)) + ui.c.blue((spec + (bdg ? " " + bdg : "")).padEnd(14)) +
772
- ui.c.faint(pinned ? ui.t("team.pinned") : ui.t("team.auto")),
773
- );
774
- }
775
- ui.line(" " + ui.c.faint(ui.t("team.usage")));
776
- }
777
- function setTeam(arg) {
778
- const parts = arg.trim().split(/\s+/);
779
- const who = parts[0];
780
- let spec = (parts[1] || "").trim();
781
- if (spec === "claude") spec = "claude-code";
782
- const agent = H.resolveAgent(db, who);
783
- const firm = agent ? null : H.resolveFirm(db, who);
784
- const slug = agent ? agent.slug : firm ? firm.slug : null;
785
- if (!slug) return ui.error(ui.t("noAgent", who));
786
- if (!spec) return printTeam();
787
- const valid = ["auto", "claude-code", "codex", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
788
- if (!valid.includes(spec)) return ui.warn(ui.t("team.usage"));
789
- prefs.agentRuntime[slug] = spec;
790
- if (opts.savePrefs) opts.savePrefs(prefs);
791
- ui.ok(ui.t("team.set", slug, spec === "auto" ? ui.t("team.auto") : spec));
792
- if (state.subject && state.subject.slug === slug) {
793
- applyRuntimeFor(state.subject);
794
- routingNote(state.subject);
795
- }
796
- }
797
-
798
- function printCost() {
799
- const labels = Object.keys(state.cost);
800
- ui.line("");
801
- if (!labels.length) return ui.info(ui.t("noCost"));
802
- ui.line(ui.c.dim(" " + ui.t("cost.title")));
803
- let tIn = 0, tOut = 0, tCost = 0, tMs = 0, tTurns = 0;
804
- const fmt = (e) => {
805
- const bits = [e.turns + (e.turns === 1 ? " turn" : " turns")];
806
- if (e.in || e.out) bits.push(e.in + "→" + e.out + " tok");
807
- // 달러 비용 미표시 — 토큰 수만.
808
- if (e.ms) bits.push((e.ms / 1000).toFixed(1) + "s");
809
- return bits.join(" · ");
810
- };
811
- for (const label of labels) {
812
- const e = state.cost[label];
813
- tIn += e.in; tOut += e.out; tCost += e.cost; tMs += e.ms; tTurns += e.turns;
814
- ui.line(" " + ui.c.blue(label.padEnd(22)) + ui.c.faint(fmt(e)));
815
- }
816
- 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 })));
817
- }
818
-
819
- function printSlashSkills() {
820
- ui.line("");
821
- ui.rule(ui.t("skills.title"));
822
- for (const entry of input.slashCommandEntries(ui.lang)) {
823
- const tag = entry.category ? ui.c.faint(entry.category.padEnd(10)) : "";
824
- ui.line(" " + ui.c.emerald(entry.command.padEnd(18)) + tag + ui.c.dim(entry.description));
825
- if (!entry.aliasOf && entry.usage) ui.line(" " + ui.c.faint(" ".repeat(18) + entry.usage));
826
- }
827
- }
828
-
829
- function printPermissions() {
830
- ui.line("");
831
- ui.rule(ui.t("permissions.title"));
832
- ui.line(" " + ui.c.faint(ui.t("permissions.current")) + " " + ui.c.emerald(state.permission));
833
- for (const level of permissions.LEVELS) {
834
- const description = permissions.copy(level, ui.lang).description;
835
- const mark = level === state.permission ? "› " : " ";
836
- ui.line(" " + ui.c.emerald((mark + level).padEnd(10)) + ui.c.dim(description));
837
- }
838
- ui.line(" " + ui.c.faint("usage: /permission read|write|full"));
839
- }
840
-
841
- async function rerunSetup() {
842
- try {
843
- const { runOnboard } = require("./agentlas-onboard.cjs");
844
- const result = await runOnboard({ ui, rl, helpers: H });
845
- Object.assign(prefs, result);
846
- ui.lang = terminalLang(prefs, opts);
847
- state.permission = prefs.permission || state.permission;
848
- if (prefs.runtime && prefs.runtime !== "auto" && H.RUNTIME_BIN[prefs.runtime] && H.which(H.RUNTIME_BIN[prefs.runtime])) {
849
- state.runtime = { mode: "cli", kind: prefs.runtime };
850
- baseRuntime = state.runtime;
851
- state.native = {};
852
- }
853
- if (opts.savePrefs) opts.savePrefs(prefs);
854
- } catch (e) {
855
- ui.error((e && e.message) || String(e));
856
- }
857
- }
858
-
859
- function compactHistory() {
860
- const before = state.history.length;
861
- const keep = 10;
862
- if (before <= keep) {
863
- ui.info(ui.t("compact.noop", String(before)));
864
- return;
865
- }
866
- state.history = state.history.slice(-keep);
867
- ui.ok(ui.t("compact.done", String(before), String(state.history.length)));
868
- }
869
-
870
- function showDiff() {
871
- const { spawnSync } = require("node:child_process");
872
- const opt = { cwd: state.cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 };
873
- const stat = spawnSync("git", ["-C", state.cwd, "--no-pager", "diff", "--stat"], opt);
874
- if (stat.status !== 0 && /not a git repository/i.test(stat.stderr || "")) return ui.warn(ui.t("diffNoGit"));
875
- const body = spawnSync("git", ["-C", state.cwd, "--no-pager", "diff"], opt);
876
- const statTxt = (stat.stdout || "").trim();
877
- const bodyTxt = (body.stdout || "").trim();
878
- ui.line("");
879
- if (!statTxt && !bodyTxt) return ui.info(ui.t("diffClean"));
880
- if (statTxt) ui.markdown(statTxt);
881
- if (bodyTxt) {
882
- ui.line("");
883
- for (const ln of bodyTxt.split("\n").slice(0, 500)) {
884
- if (ln.startsWith("+") && !ln.startsWith("+++")) ui.line(ui.c.green(ln));
885
- else if (ln.startsWith("-") && !ln.startsWith("---")) ui.line(ui.c.paw(ln));
886
- else if (ln.startsWith("@@")) ui.line(ui.c.blue(ln));
887
- else ui.line(ui.c.dim(ln));
888
- }
889
- }
890
- }
891
-
892
- // !cmd — run a shell command in the working folder and show its output (display-only).
893
- function runShell(cmd) {
894
- if (!cmd) return;
895
- const { spawnSync } = require("node:child_process");
896
- ui.tool("$ " + cmd);
897
- const r = spawnSync("bash", ["-lc", cmd], { cwd: state.cwd, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
898
- const out = ((r.stdout || "") + (r.stderr || "")).trim();
899
- // `!command` is explicit user output, unlike autonomous runtime traces: keep it inspectable.
900
- ui.toolResult(out || ("exit " + (r.status == null ? "?" : r.status)), r.status === 0 || r.status == null, { verbose: true });
901
- }
902
-
903
- // @path — inline the contents of mentioned files into the prompt as fenced context.
904
- function expandMentions(text) {
905
- const fs = require("node:fs");
906
- const path = require("node:path");
907
- const seen = new Set();
908
- const blocks = [];
909
- const re = /(^|\s)@([^\s]+)/g;
910
- let m;
911
- while ((m = re.exec(text))) {
912
- const p = m[2];
913
- if (seen.has(p)) continue;
914
- seen.add(p);
915
- try {
916
- const abs = path.isAbsolute(p) ? p : path.resolve(state.cwd, p);
917
- const st = fs.statSync(abs);
918
- if (st.isFile() && st.size <= 256 * 1024) {
919
- blocks.push("File: " + p + "\n```\n" + fs.readFileSync(abs, "utf8").slice(0, 20000) + "\n```");
920
- }
921
- } catch { /* not a readable file — leave the @token as plain text */ }
922
- }
923
- return blocks.length ? text + "\n\n" + blocks.join("\n\n") : text;
924
- }
925
-
926
- async function handleSlash(line) {
927
- const [cmd, ...rest] = line.slice(1).split(/\s+/);
928
- const arg = rest.join(" ");
929
- switch (cmd) {
930
- case "help":
931
- case "?":
932
- printHelp(ui);
933
- return true;
934
- case "agents":
935
- printRoster();
936
- return true;
937
- case "skills":
938
- printSlashSkills();
939
- return true;
940
- case "team":
941
- arg ? setTeam(arg) : printTeam();
942
- return true;
943
- case "firms": {
944
- const fs = H.listFirms(db);
945
- ui.line("");
946
- for (const f of fs) ui.line(" " + ui.c.emerald(f.slug.padEnd(28)) + ui.c.text(f.name) + ui.c.dim(" (CEO)"));
947
- return true;
948
- }
949
- case "agent":
950
- if (!arg) return ui.warn(ui.t("agentUsage")), true;
951
- await switchSubject("agent", arg);
952
- return true;
953
- case "firm":
954
- if (!arg) return ui.warn(ui.t("firmUsage")), true;
955
- await switchSubject("firm", arg);
956
- return true;
957
- case "runtime":
958
- setRuntime(arg);
959
- return true;
960
- case "config":
961
- case "toggles": {
962
- // 클로드코드 /config 스타일 — 자동 엔진 개입은 전부 명시적 opt-in (기본 off).
963
- const ENGINE_FLAGS = [
964
- { key: "storm", pref: "autoStorm", label: ui.t("config.storm") },
965
- { key: "network", pref: "autoNetwork", label: ui.t("config.network") },
966
- ];
967
- const showConfig = () => {
968
- ui.line("");
969
- ui.rule(ui.t("config.title"));
970
- for (const f of ENGINE_FLAGS) {
971
- const on = !!prefs[f.pref];
972
- ui.line(" " + (on ? ui.c.green("● on ") : ui.c.dim("○ off")) + " " + ui.c.emerald(f.key.padEnd(9)) + ui.c.dim(f.label));
973
- }
974
- ui.line(" " + ui.c.faint(ui.t("config.usage")));
975
- };
976
- const [cfgKey, cfgVal] = arg.trim().split(/\s+/);
977
- if (!cfgKey) return showConfig(), true;
978
- const flag = ENGINE_FLAGS.find((f) => f.key === cfgKey.toLowerCase());
979
- if (!flag || !/^(on|off)$/i.test(cfgVal || "")) return ui.warn(ui.t("config.usage")), true;
980
- prefs[flag.pref] = /^on$/i.test(cfgVal);
981
- if (opts.savePrefs) opts.savePrefs(prefs);
982
- ui.ok(ui.t("config.set", flag.key, prefs[flag.pref] ? "on" : "off"));
983
- return showConfig(), true;
984
- }
985
- case "storm": {
986
- if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
987
- let goal = arg;
988
- let research = false;
989
- if (/\s--research(-evidence)?\b/.test(" " + goal)) {
990
- research = true;
991
- goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
992
- }
993
- if (H.ensureProjectForExecution) {
994
- state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-storm");
995
- }
996
- await H.stormRun(db, goal, {
997
- ui,
998
- cwd: state.cwd,
999
- permission: state.permission,
1000
- projectPath: state.projectPath,
1001
- research,
1002
- });
1003
- return true;
1004
- }
1005
- case "build": {
1006
- if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
1007
- ui.line("");
1008
- if (typeof H.terminalBuild !== "function") throw new Error("Terminal MCP build preflight is unavailable");
1009
- if (H.ensureProjectForExecution) {
1010
- state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-build");
1011
- }
1012
- await H.terminalBuild(db, arg, {
1013
- cwd: state.cwd,
1014
- modelPin: state.modelPinned ? state.runtime.model : null,
1015
- effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1016
- input: process.stdin,
1017
- promptOutput: process.stderr,
1018
- out: (line) => ui.line(String(line)),
1019
- });
1020
- return true;
1021
- }
1022
- case "route": {
1023
- if (!arg) return ui.warn("usage: /route <요청>"), true;
1024
- ui.line("");
1025
- await H.hepRun(["route", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
1026
- return true;
1027
- }
1028
- case "research": {
1029
- if (!arg) return ui.warn("usage: /research status|gather|search|read … (예: /research search \"쿼리\")"), true;
1030
- ui.line("");
1031
- await H.hepRun(["research", ...arg.split(/\s+/)], { cwd: state.cwd });
1032
- return true;
1033
- }
1034
- case "search": {
1035
- if (!arg) return ui.warn("usage: /search <할 일>"), true;
1036
- if (H.cloudSearch) await H.cloudSearch(db, [arg]);
1037
- else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
1038
- return true;
1039
- }
1040
- case "workforce":
1041
- case "network":
1042
- case "taskforce": {
1043
- if (!arg) return ui.warn("usage: /network <요청> [--benchmark] (Agent Workforce Ontology)"), true;
1044
- let goal = arg;
1045
- const benchmark = /(?:^|\s)--benchmark(?:\s|$)/.test(goal);
1046
- goal = goal.replace(/(?:^|\s)--benchmark(?=\s|$)/g, " ").trim();
1047
- let concurrency;
1048
- const parallel = goal.match(/(?:^|\s)--parallel\s+(\d+)\b/);
1049
- if (parallel) {
1050
- concurrency = Number(parallel[1]);
1051
- goal = goal.replace(parallel[0], "").trim();
1052
- }
1053
- if (H.ensureProjectForExecution) {
1054
- state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce");
1055
- }
1056
- ui.line("");
1057
- await H.workforceRun(db, goal, {
1058
- ui,
1059
- cwd: state.cwd,
1060
- permission: state.permission,
1061
- runtime: state.runtime,
1062
- projectPath: state.projectPath,
1063
- modelPin: state.modelPinned ? state.runtime.model : null,
1064
- effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1065
- concurrency,
1066
- benchmark,
1067
- });
1068
- return true;
1069
- }
1070
- case "legacy-network": {
1071
- if (!arg) return ui.warn("usage: /legacy-network <요청>"), true;
1072
- ui.line("");
1073
- await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
1074
- return true;
1075
- }
1076
- case "browser": {
1077
- ui.line("");
1078
- await H.hepRun(["hep-browser", ...(arg ? arg.split(/\s+/) : [])], { cwd: state.cwd });
1079
- return true;
1080
- }
1081
- case "connect": {
1082
- ui.line("");
1083
- await H.hepRun(["hep-connect", ...(arg ? arg.split(/\s+/) : [])], { cwd: state.cwd });
1084
- return true;
1085
- }
1086
- case "swarm": {
1087
- if (!arg) return ui.warn("usage: /swarm <goal> [--parallel N]"), true;
1088
- let goal = arg;
1089
- let concurrency;
1090
- const m = goal.match(/\s--parallel\s+(\d+)\b/);
1091
- if (m) {
1092
- concurrency = Number(m[1]);
1093
- goal = goal.replace(m[0], "").trim();
1094
- }
1095
- if (H.ensureProjectForExecution) {
1096
- state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-swarm");
1097
- }
1098
- await H.swarmRun(db, goal, {
1099
- ui,
1100
- cwd: state.cwd,
1101
- permission: state.permission,
1102
- runtime: state.runtime,
1103
- concurrency,
1104
- agent: state.subject && state.subject.capAgent,
1105
- projectPath: state.projectPath,
1106
- modelPin: state.modelPinned ? state.runtime.model : null,
1107
- effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1108
- });
1109
- return true;
1110
- }
1111
- case "model": {
1112
- // CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
1113
- const requested = (arg || "").trim();
1114
- state.runtime.model = !requested || requested.toLowerCase() === "auto" ? null : requested;
1115
- state.modelPinned = Boolean(state.runtime.model);
1116
- state.native = {}; // 새 모델로 세션 리셋
1117
- ui.ok(ui.t("modelSet", state.runtime.model || "auto"));
1118
- return true;
1119
- }
1120
- case "effort": {
1121
- const lv = (arg || "").toLowerCase().trim();
1122
- if (!lv) {
1123
- ui.info(ui.t("effortCurrent", state.effortPinned ? (state.effort || "off") : "auto"));
1124
- return true;
1125
- }
1126
- if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
1127
- return ui.warn(ui.t("effortUsage")), true;
1128
- }
1129
- state.effort = lv === "auto" || lv === "off" ? null : lv;
1130
- state.effortPinned = lv !== "auto";
1131
- prefs.effort = state.effort;
1132
- prefs.effortPinned = state.effortPinned;
1133
- if (opts.savePrefs) opts.savePrefs(prefs);
1134
- state.native = {};
1135
- ui.ok(ui.t("effortSet", state.effortPinned ? (state.effort || "off") : "auto"));
1136
- return true;
1137
- }
1138
- case "permission":
1139
- case "perm": {
1140
- const p = (arg || "").toLowerCase();
1141
- if (!p) {
1142
- printPermissions();
1143
- return true;
1144
- }
1145
- if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
1146
- setPermission(p);
1147
- return true;
1148
- }
1149
- case "permissions":
1150
- if (arg) {
1151
- const p = (arg || "").toLowerCase();
1152
- if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
1153
- setPermission(p);
1154
- } else {
1155
- printPermissions();
1156
- }
1157
- return true;
1158
- case "setup":
1159
- await rerunSetup();
1160
- return true;
1161
- case "cwd":
1162
- if (arg) {
1163
- const path = require("node:path");
1164
- const fs = require("node:fs");
1165
- const next = path.resolve(state.cwd, arg);
1166
- if (!fs.existsSync(next)) return ui.error(ui.t("cwdNoPath", next)), true;
1167
- state.cwd = next;
1168
- state.native = {};
1169
- if (H.projectPathFor) state.projectPath = H.projectPathFor(db, next);
1170
- ui.ok(ui.t("cwdSet", banner.shorten(next)));
1171
- } else {
1172
- ui.info(state.cwd);
1173
- }
1174
- return true;
1175
- case "memory": {
1176
- const mem = H.cliMemoryContext(db, state.projectPath);
1177
- ui.line("");
1178
- ui.markdown(mem || ui.t("noMemory"));
1179
- return true;
1180
- }
1181
- case "ontology": {
1182
- if (!H.ontologyCommand) return ui.warn("ontology command unavailable"), true;
1183
- try {
1184
- const lines = H.ontologyCommand(arg, { cwd: state.cwd, projectPath: state.projectPath });
1185
- ui.line("");
1186
- for (const item of lines || []) ui.line(" " + ui.c.text(String(item)));
1187
- } catch (e) {
1188
- ui.error((e && e.message) || String(e));
1189
- }
1190
- return true;
1191
- }
1192
- case "career-graph":
1193
- case "graph": {
1194
- if (!H.careerGraphCommand) return ui.warn("career graph command unavailable"), true;
1195
- try {
1196
- const lines = H.careerGraphCommand(arg, { cwd: state.cwd, projectPath: state.projectPath });
1197
- ui.line("");
1198
- for (const item of lines || []) ui.line(" " + ui.c.text(String(item)));
1199
- } catch (e) {
1200
- ui.error((e && e.message) || String(e));
1201
- }
1202
- return true;
1203
- }
1204
- case "side":
1205
- case "btw":
1206
- if (!arg) return ui.warn(ui.t("sideUsage")), true;
1207
- if (!state.subject) return ui.warn(ui.t("sideNeedsSubject")), true;
1208
- ui.info(ui.t("sideStart"));
1209
- await runTurn(expandMentions(arg), { side: true });
1210
- ui.info(ui.t("sideDone"));
1211
- return true;
1212
- case "clear":
1213
- state.history = [];
1214
- state.native = {};
1215
- if (ui.enabled) process.stdout.write("\x1b[2J\x1b[H");
1216
- showBanner();
1217
- return true;
1218
- case "import":
1219
- if (!arg) return ui.warn(ui.t("importUsage")), true;
1220
- try {
1221
- const r = H.importLocal(db, arg);
1222
- ui.ok(ui.t(r.updated ? "updated" : "imported", r.name, r.kind));
1223
- } catch (e) {
1224
- ui.error((e && e.message) || String(e));
1225
- }
1226
- return true;
1227
- case "install": {
1228
- if (!arg) return ui.warn(ui.t("installUsage")), true;
1229
- if (!H.cloudInstall) return ui.warn("install unavailable"), true;
1230
- ui.status(ui.t("installing", arg.trim()));
1231
- try {
1232
- const agent = await H.cloudInstall(db, arg.trim());
1233
- ui.ok(ui.t("cloudInstalled", agent.name || agent.slug));
1234
- if (agent.localPath) ui.info(agent.localPath);
1235
- } catch (e) {
1236
- ui.stopSpinner();
1237
- ui.error((e && e.message) || String(e));
1238
- }
1239
- return true;
1240
- }
1241
- case "marketplace":
1242
- case "market": {
1243
- ui.line("");
1244
- ui.rule(ui.t("market.title"));
1245
- ui.line(" " + ui.c.dim(ui.t("market.help")));
1246
- ui.line(" " + ui.c.emerald("/install <slug>".padEnd(18)) + ui.c.dim(ui.t("market.installHint")));
1247
- ui.line(" " + ui.c.emerald("/import <path>".padEnd(18)) + ui.c.dim(ui.t("market.importHint")));
1248
- const logged = H.hasCloudSession ? await H.hasCloudSession() : false;
1249
- ui.line(" " + ui.c.faint(ui.t(logged ? "market.loggedIn" : "market.loggedOut")));
1250
- return true;
1251
- }
1252
- case "mcp": {
1253
- const servers = H.mcpServers ? H.mcpServers(db) : [];
1254
- ui.line("");
1255
- ui.rule("MCP");
1256
- for (const s of servers) {
1257
- let envKeys = [];
1258
- try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
1259
- const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
1260
- const on = s.enabled && s.runtimeConsented ? ui.c.green("on ") : s.enabled ? ui.c.faint("consent") : ui.c.faint("off");
1261
- const envStr = envKeys.length ? envKeys.join(", ") : "no key";
1262
- ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
1263
- }
1264
- const wired = servers.filter((s) => s.enabled && s.transport === "stdio" && s.runtimeEligible === true && s.runtimeConsented === true).length;
1265
- ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
1266
- ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
1267
- return true;
1268
- }
1269
- case "resume": {
1270
- const list = H.sessionsLoad ? H.sessionsLoad() : [];
1271
- if (!arg) {
1272
- ui.line("");
1273
- ui.rule(ui.t("resume.title"));
1274
- if (!list.length) {
1275
- ui.info(ui.t("resume.none"));
1276
- return true;
1277
- }
1278
- list.slice(0, 10).forEach((s, i) =>
1279
- ui.line(
1280
- " " + ui.c.faint(String(i + 1).padStart(2)) + " " +
1281
- ui.c.emerald(String(s.agentLabel || s.agentSlug || "?").padEnd(20)) +
1282
- ui.c.blue(String(s.kind || "").padEnd(12)) + ui.c.dim(s.title || ""),
1283
- ),
1284
- );
1285
- ui.line(" " + ui.c.faint(ui.t("resume.usage")));
1286
- return true;
1287
- }
1288
- const n = parseInt(arg, 10);
1289
- const s = n >= 1 && n <= list.length ? list[n - 1] : null;
1290
- if (!s) return ui.warn(ui.t("resume.noNum")), true;
1291
- const agent = H.resolveAgent(db, s.agentSlug);
1292
- if (agent) {
1293
- setSubjectAgent(agent);
1294
- } else if (s.agentSlug === "agentlas-direct") {
1295
- setSubjectDirect(); // 직답 세션도 이어서 재개
1296
- } else {
1297
- return ui.error(ui.t("noAgent", s.agentSlug)), true;
1298
- }
1299
- // 저장된 CLI 런타임이 실제로 설치돼 있을 때만 그 세션을 복원한다. s.kind가 없거나
1300
- // (손상된 resume 레코드) 런타임이 미설치면, 잘못된/"undefined" 키로 native 맵을
1301
- // 덮어써 현재 세션을 잃는 것을 막고 새 대화로 시작한다.
1302
- if (s.kind && s.sessionId && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
1303
- state.runtime = { mode: "cli", kind: s.kind };
1304
- baseRuntime = state.runtime;
1305
- state.native = { [s.kind]: { id: s.sessionId } };
1306
- } else {
1307
- state.native = {};
1308
- }
1309
- try {
1310
- const fs2 = require("node:fs");
1311
- if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
1312
- } catch {
1313
- /* ignore */
1314
- }
1315
- ui.ok(ui.t("resume.ok", s.agentLabel || s.agentSlug || "?"));
1316
- return true;
1317
- }
1318
- case "doctor":
1319
- await H.doctor(db, ui);
1320
- return true;
1321
- case "status":
1322
- banner.renderStatus({ ui, runtimeLabel: runtimeLabel(state.runtime), subjectLabel: state.subject && state.subject.label, permission: state.permission, cwd: state.cwd });
1323
- return true;
1324
- case "cost":
1325
- printCost();
1326
- return true;
1327
- case "multimodal": {
1328
- const [sub, modality, providerId] = arg.trim().split(/\s+/);
1329
- if (sub === "set") {
1330
- if (!H.setMultimodal) return ui.warn("multimodal settings unavailable"), true;
1331
- try {
1332
- H.setMultimodal(db, modality, providerId);
1333
- ui.ok(ui.t("multimodal.set", modality || "", providerId || ""));
1334
- } catch (e) {
1335
- ui.error((e && e.message) || String(e));
1336
- }
1337
- }
1338
- if (H.multimodalStatus) {
1339
- const rows = await H.multimodalStatus(db);
1340
- ui.line("");
1341
- ui.line(ui.c.dim(" " + ui.t("multimodal.title")));
1342
- for (const row of rows) {
1343
- const env = row.env.length
1344
- ? row.env.map((e) => `${e.key}:${e.hasValue ? "set" : "missing"}`).join(" ")
1345
- : "no key";
1346
- ui.line(" " + ui.c.blue(row.modality.padEnd(7)) + ui.c.text(row.provider.id.padEnd(22)) + ui.c.dim(env));
1347
- }
1348
- ui.line(" " + ui.c.faint(ui.t("multimodal.usage")));
1349
- }
1350
- return true;
1351
- }
1352
- case "diff":
1353
- showDiff();
1354
- return true;
1355
- case "history": {
1356
- const items = input.loadHistory().slice(0, 30);
1357
- ui.line("");
1358
- if (!items.length) { ui.info(ui.t("noHistory")); return true; }
1359
- for (let i = 0; i < items.length; i++) ui.line(" " + ui.c.faint(String(i + 1).padStart(3)) + " " + ui.c.text(items[i]));
1360
- return true;
1361
- }
1362
- case "compact":
1363
- compactHistory();
1364
- return true;
1365
- case "keybindings":
1366
- printKeybindings(ui);
1367
- return true;
1368
- case "exit":
1369
- case "quit":
1370
- case "q":
1371
- ui.line(ui.c.dim(ui.t("bye")));
1372
- rl.close();
1373
- process.exit(0);
1374
- return false;
1375
- default:
1376
- ui.warn(ui.t("unknownCmd", cmd));
1377
- return true;
1378
- }
1379
- }
1380
-
1381
- // ── interactive picker (when no agent was given) ──
1382
- async function chooseAndStart(setter, row) {
1383
- if (setter === setSubjectAgent) await warmImageJudgment(row); // firm은 팀 베토라 판정 대상 아님
1384
- setter(row);
1385
- ui.ok(ui.t("switched", state.subject.label));
1386
- routingNote(state.subject);
1387
- ask();
1388
- }
1389
- // skipRoster=true → 명령/오입력 뒤 재호출 시 전체 로스터를 다시 그리지 않는다(노이즈 제거; Claude Code처럼 조용히).
1390
- function pick(skipRoster) {
1391
- if (closed) return process.exit(0);
1392
- if (slashPalette.setEnabled) slashPalette.setEnabled(false);
1393
- if (!skipRoster) printRoster();
1394
- rl.question("\n " + ui.c.emerald(ui.t("picker.prompt")), async (line) => {
1395
- const t = (line || "").trim();
1396
- if (!t) return pick(true);
1397
- if (t.startsWith("/") && !input.isAbsolutePathTask(t)) {
1398
- const handled = await handleSlash(t);
1399
- if (handled === false) return;
1400
- // /agent·/firm·/resume 처럼 대화 대상을 정한 명령이면 픽커를 빠져나가 대화 루프로 전환한다.
1401
- if (state.subject) return ask();
1402
- return pick(true);
1403
- }
1404
- const ags = H.listAgents(db);
1405
- if (/^\d+$/.test(t)) {
1406
- const n = parseInt(t, 10);
1407
- if (n >= 1 && n <= ags.length) return chooseAndStart(setSubjectAgent, ags[n - 1]);
1408
- ui.warn(ui.t("picker.noNum"));
1409
- return pick(true);
1410
- }
1411
- if (/^firm\s+/i.test(t)) {
1412
- const f = H.resolveFirm(db, t.replace(/^firm\s+/i, "").trim());
1413
- if (f) return chooseAndStart(setSubjectFirm, f);
1414
- ui.warn(ui.t("picker.noFirm"));
1415
- return pick(true);
1416
- }
1417
- const a = H.resolveAgent(db, t);
1418
- if (a) return chooseAndStart(setSubjectAgent, a);
1419
- const f = H.resolveFirm(db, t);
1420
- if (f) return chooseAndStart(setSubjectFirm, f);
1421
- if (H.autoRouteAgent || H.resolveAutoRoute) {
1422
- // 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
1423
- const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
1424
- if (choice) {
1425
- if (choice.direct) {
1426
- setSubjectDirect();
1427
- } else {
1428
- await warmImageJudgment(choice.agent);
1429
- setSubjectAgent(choice.agent);
1430
- }
1431
- state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1432
- ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${state.subject.label}`);
1433
- if (!choice.direct) routingNote(state.subject);
1434
- await runTurn(t);
1435
- return ask();
1436
- }
1437
- }
1438
- ui.warn(ui.t("picker.noMatch", t));
1439
- return pick(true);
1440
- });
1441
- }
1442
-
1443
- // 한 줄 입력 처리(공용): ! 셸 · / 명령 · 미선택 시 번호/이름/자동라우팅 · 그 외 턴 실행. readline·composer 양쪽이 호출.
1444
- async function processLine(t) {
1445
- if (t.startsWith("!")) {
1446
- runShell(t.slice(1).trim());
1447
- return;
1448
- }
1449
- if (t.startsWith("/") && !input.isAbsolutePathTask(t)) {
1450
- await handleSlash(t); // /exit 는 내부에서 process.exit
1451
- return;
1452
- }
1453
- // 직답 모드는 대상 고정이 아니라 "자동 라우팅 유지" — 매 메시지 재라우팅해,
1454
- // 나중에 전문 요청이 오면 해당 에이전트로 자연스럽게 넘어간다.
1455
- const directMode = !!(state.subject && state.subject.kind === "direct");
1456
- if (!state.subject || directMode) {
1457
- if (!state.subject) {
1458
- const ags = H.listAgents(db);
1459
- const single = !/\s/.test(t);
1460
- if (/^\d+$/.test(t)) {
1461
- const n = parseInt(t, 10);
1462
- if (n >= 1 && n <= ags.length) {
1463
- await warmImageJudgment(ags[n - 1]);
1464
- setSubjectAgent(ags[n - 1]);
1465
- ui.ok(ui.t("switched", state.subject.label));
1466
- routingNote(state.subject);
1467
- return;
1468
- }
1469
- }
1470
- if (single) {
1471
- const a = H.resolveAgent(db, t);
1472
- if (a) { await warmImageJudgment(a); setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1473
- const f = H.resolveFirm(db, t);
1474
- if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1475
- }
1476
- }
1477
- if (H.autoRouteAgent || H.resolveAutoRoute) {
1478
- // 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
1479
- const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
1480
- if (choice) {
1481
- if (choice.direct) {
1482
- setSubjectDirect();
1483
- if (!directMode) {
1484
- // 첫 직답 진입에만 알림 — 연속 직답 대화에서는 조용히.
1485
- state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1486
- ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
1487
- }
1488
- } else {
1489
- await warmImageJudgment(choice.agent);
1490
- setSubjectAgent(choice.agent);
1491
- state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1492
- ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
1493
- routingNote(state.subject);
1494
- }
1495
- }
1496
- }
1497
- // Workforce is the default for direct, goal-like work. An explicit
1498
- // `/config network off` remains a durable opt-out; Storm stays opt-in.
1499
- if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
1500
- if (prefs.autoStorm && H.stormRun) {
1501
- ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
1502
- await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
1503
- return;
1504
- }
1505
- if (prefs.autoNetwork && H.workforceRun) {
1506
- ui.info(ui.t("config.autoEngage", "workforce ontology", "network"));
1507
- if (H.ensureProjectForExecution) {
1508
- state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce-auto");
1509
- }
1510
- ui.line("");
1511
- await H.workforceRun(db, t, {
1512
- ui,
1513
- cwd: state.cwd,
1514
- permission: state.permission,
1515
- runtime: state.runtime,
1516
- projectPath: state.projectPath,
1517
- modelPin: state.modelPinned ? state.runtime.model : null,
1518
- effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1519
- });
1520
- return;
1521
- }
1522
- }
1523
- }
1524
- await runTurn(expandMentions(t));
1525
- }
1526
-
1527
- // 실작업(goal)형 프롬프트 감지 — 질문/잡담에는 자동 엔진(storm/network)을 절대 걸지 않는다.
1528
- function goalLikePrompt(t) {
1529
- const s = String(t || "").trim();
1530
- if (s.length < 12) return false;
1531
- if (/[??]\s*$/.test(s)) return false;
1532
- return /(해줘|해라|만들|구현|수정|배포|정리|작성|분석|리팩터|고쳐|추가|빌드|테스트|돌려|실행|자동화|automate|build|implement|fix|refactor|deploy|create|write|run|ship)/i.test(s);
1533
- }
1534
-
1535
- // ── 연결 LLM 세션 사용량 요약 (챗 입력창 아래 상시 표시) ──
1536
- // 호스트 이점: 단일 모델 CLI는 못 보여주는 멀티 LLM 합산 사용량을 항상 노출한다.
1537
- function fmtTok(n) {
1538
- if (!n) return "0";
1539
- if (n >= 1e6) return (n >= 1e7 ? Math.round(n / 1e6) : (n / 1e6).toFixed(1)) + "m";
1540
- if (n >= 1e3) return (n >= 1e5 ? Math.round(n / 1e3) : (n / 1e3).toFixed(1)) + "k";
1541
- return String(n);
1542
- }
1543
- function usageSummaryLine() {
1544
- const shortLabel = (label) => (label === "claude-code" ? "claude" : label);
1545
- const labels = [];
1546
- const push = (label) => { if (label && label !== "(none)" && !labels.includes(label)) labels.push(label); };
1547
- for (const kind of installedKinds()) push(kind); // 연결(설치)된 CLI 런타임 — 미사용이어도 표시
1548
- push(runtimeLabel(state.runtime)); // 현재 런타임 (BYOK/Ollama 포함)
1549
- for (const label of Object.keys(state.cost)) push(label); // 세션 중 사용한 나머지
1550
- const parts = labels.map((label) => {
1551
- const e = state.cost[label];
1552
- if (!e) return `${shortLabel(label)} 0`;
1553
- if (e.in || e.out) return `${shortLabel(label)} ${fmtTok(e.in)}→${fmtTok(e.out)}`;
1554
- return `${shortLabel(label)} ${e.turns}${ui.lang === "ko" ? "턴" : "t"}`;
1555
- });
1556
- return `${ui.t("usageBar")} ${parts.join(" · ")}`;
1557
- }
1558
-
1559
- // ── composer (raw-mode bottom box) main loop ──
1560
- function composerMeta() {
1561
- const rt = runtimeLabel(state.runtime);
1562
- const subj = state.subject ? state.subject.label : ui.t("composer.autoroute");
1563
- const eff = state.effort ? " · " + state.effort : "";
1564
- const permissionLabel = permissions.copy(state.permission, ui.lang).label;
1565
- return {
1566
- lang: ui.lang,
1567
- permission: state.permission,
1568
- permissionLabel,
1569
- status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
1570
- usage: usageSummaryLine(), // 챗 입력창 아래 상시 LLM 사용량 표시줄
1571
- onCyclePermission: () => {
1572
- const cycle = permissionCycle.step(state.permission);
1573
- if (cycle.armed) {
1574
- return {
1575
- ...composerMeta(),
1576
- confirmation: ui.t("permFullArm"),
1577
- confirmationTone: "danger",
1578
- };
1579
- }
1580
- const level = setPermission(cycle.level, { notify: false, persist: false });
1581
- return {
1582
- ...composerMeta(),
1583
- confirmation: cycle.enteredFull
1584
- ? ui.t("permFullConfirm")
1585
- : ui.t("permCycleConfirm", permissions.copy(level, ui.lang).label),
1586
- confirmationTone: cycle.enteredFull ? "danger" : "normal",
1587
- };
1588
- },
1589
- onPermissionCycleCancel: () => {
1590
- permissionCycle.cancel();
1591
- return { ...composerMeta(), confirmation: null, confirmationTone: null };
1592
- },
1593
- };
1594
- }
1595
- async function composerLoop() {
1596
- let buffer = "";
1597
- while (!closed) {
1598
- let r;
1599
- try {
1600
- const meta = composerMeta();
1601
- r = await composer.read({
1602
- glyph: buffer ? "…" : "›",
1603
- ...meta,
1604
- suggest: (l) => input.slashCommandSuggestions(l, 12, ui.lang),
1605
- complete: completer,
1606
- });
1607
- } catch (e) {
1608
- ui.error((e && e.message) || String(e));
1609
- r = { value: "" };
1610
- }
1611
- if (r.exit || r.eof) {
1612
- ui.line(ui.c.dim(ui.t("bye")));
1613
- return process.exit(0);
1614
- }
1615
- const line = r.value || "";
1616
- if (input.isContinuation(line)) {
1617
- buffer += input.stripContinuation(line) + "\n";
1618
- continue;
1619
- }
1620
- const t = (buffer + line).trim();
1621
- buffer = "";
1622
- if (!t) continue;
1623
- try {
1624
- await processLine(t);
1625
- } catch (e) {
1626
- ui.error((e && e.message) || String(e));
1627
- }
1628
- }
1629
- process.exit(0);
1630
- }
1631
-
1632
- // ── readline fallback loop (non-TTY / AGENTLAS_CLASSIC_INPUT=1) ── (multiline: trailing "\\" continues)
1633
- function ask(buffer) {
1634
- if (closed) return process.exit(0);
1635
- if (slashPalette.setEnabled) slashPalette.setEnabled(true);
1636
- const cont = buffer != null;
1637
- // 클래식 모드에도 사용량 상시 표시 (composer 모드에선 입력박스 아래 줄이 담당)
1638
- if (!cont && !composer && Object.keys(state.cost).length) ui.line(ui.c.faint(" " + usageSummaryLine()));
1639
- rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
1640
- if (input.isContinuation(line)) {
1641
- return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
1642
- }
1643
- const full = (cont ? buffer + "\n" : "") + (line || "");
1644
- const t = full.trim();
1645
- if (!t) return ask();
1646
- slashPalette.clear();
1647
- if (rl.terminal && rl.history && rl.history[0] !== t) rl.history.unshift(t);
1648
- input.persistHistory(rl);
1649
- await processLine(t);
1650
- if (!closed) ask();
1651
- });
1652
- }
1653
-
1654
- // ── boot: first-run wizard, then banner + picker/loop ──
1655
- async function bootstrap() {
1656
- if (!prefs.onboarded) {
1657
- try {
1658
- const { runOnboard } = require("./agentlas-onboard.cjs");
1659
- const result = await runOnboard({ ui, rl, helpers: H });
1660
- Object.assign(prefs, result);
1661
- ui.lang = terminalLang(prefs, opts);
1662
- state.permission = prefs.permission || state.permission;
1663
- if (prefs.runtime && prefs.runtime !== "auto" && H.RUNTIME_BIN[prefs.runtime] && H.which(H.RUNTIME_BIN[prefs.runtime])) {
1664
- state.runtime = { mode: "cli", kind: prefs.runtime };
1665
- }
1666
- if (opts.savePrefs) opts.savePrefs(prefs);
1667
- ui.line("");
1668
- } catch (e) {
1669
- ui.error((e && e.message) || String(e));
1670
- }
1671
- }
1672
- baseRuntime = state.runtime; // lock in the session default (post-wizard) before per-agent routing
1673
- if (state.subject && state.subject.capAgent) {
1674
- applyRuntimeFor(state.subject);
1675
- // refresh the label for the chosen language (initial subject came pre-built from the entry)
1676
- state.subject.label = displayName(state.subject.capAgent) + (state.subject.kind === "firm" ? " CEO" : "");
1677
- }
1678
- showBanner();
1679
- if (state.subject) {
1680
- routingNote(state.subject);
1681
- } else {
1682
- // Claude Code처럼 번호 픽커 없이 바로 입력. 할 일을 입력하면 자동 라우팅된다.
1683
- ui.line(" " + ui.c.dim(ui.t("picker.hint")));
1684
- }
1685
- if (composer) {
1686
- handoff = true;
1687
- try { rl.close(); } catch { /* ignore */ } // hand stdin to the raw-mode composer
1688
- composerLoop();
1689
- } else {
1690
- ask();
1691
- }
1692
- }
1693
- bootstrap();
1694
- }
1695
-
1696
- function applyPreferenceDefaults(prefs) {
1697
- if (!Object.prototype.hasOwnProperty.call(prefs, "autoNetwork")) prefs.autoNetwork = true;
1698
- return prefs;
1699
- }
1700
-
1701
- function printHelp(ui) {
1702
- const c = ui.c;
1703
- ui.line("");
1704
- ui.rule(ui.t("help.title"));
1705
- ui.line(" " + c.bold(c.text(ui.t("help.intro"))));
1706
- ui.line("");
1707
- ui.line(" " + c.faint(ui.t("help.commands")));
1708
- const rows = [
1709
- [ui.t("help.talkKey"), ui.t("help.talk")],
1710
- ["/skills", ui.t("help.skills")],
1711
- ["/agents", ui.t("help.agents")],
1712
- ["/team [agent rt]", ui.t("help.team")],
1713
- ["/agent <name>", ui.t("help.agent")],
1714
- ["/firms · /firm <name>", ui.t("help.firms")],
1715
- ["/runtime <kind>", ui.t("help.runtime")],
1716
- ["/model <id>", ui.t("help.model")],
1717
- ["/effort <lvl>", ui.t("help.effort")],
1718
- ["/permission <lvl>", ui.t("help.permission")],
1719
- ["/permissions", ui.t("help.permissions")],
1720
- ["/setup", ui.t("help.setup")],
1721
- ["/cwd [path]", ui.t("help.cwd")],
1722
- ["/memory", ui.t("help.memory")],
1723
- ["/career-graph [text]", ui.t("help.careerGraph")],
1724
- ["/ontology [text]", ui.t("help.ontology")],
1725
- ["/side <question>", ui.t("help.side")],
1726
- ["/status", ui.t("help.status")],
1727
- ["/cost", ui.t("help.cost")],
1728
- ["/multimodal", ui.t("help.multimodal")],
1729
- ["/mcp", ui.t("help.mcp")],
1730
- ["/diff", ui.t("help.diff")],
1731
- ["/history", ui.t("help.history")],
1732
- ["/resume [n]", ui.t("help.resume")],
1733
- ["/compact", ui.t("help.compact")],
1734
- ["/import <path>", ui.t("help.import")],
1735
- ["/storm <goal>", ui.t("help.storm")],
1736
- ["/swarm <goal>", ui.t("help.swarm")],
1737
- ["/build <request>", ui.t("help.build")],
1738
- ["/route <request>", ui.t("help.route")],
1739
- ["/research <sub>", ui.t("help.research")],
1740
- ["/search <task>", ui.t("help.search")],
1741
- ["/install <slug>", ui.t("help.install")],
1742
- ["/network <req>", ui.t("help.network")],
1743
- ["/browser", ui.t("help.browser")],
1744
- ["/connect", ui.t("help.connect")],
1745
- ["/marketplace", ui.t("help.market")],
1746
- ["/clear", ui.t("help.clear")],
1747
- ["/doctor", ui.t("help.doctor")],
1748
- ["/keybindings", ui.t("help.keybindings")],
1749
- ["/exit", ui.t("help.exit")],
1750
- ];
1751
- for (const [k, v] of rows) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
1752
- ui.line("");
1753
- printKeybindings(ui);
1754
- }
1755
-
1756
- function printKeybindings(ui) {
1757
- const c = ui.c;
1758
- ui.line(" " + c.faint(ui.t("help.tipsTitle")));
1759
- const tips = [
1760
- ["/", ui.t("help.slash")],
1761
- ["@path", ui.t("help.atfile")],
1762
- ["!cmd", ui.t("help.bang")],
1763
- ["\\ + Enter", ui.t("help.multiline")],
1764
- ["Tab", ui.t("help.tab")],
1765
- ["Shift-Tab", ui.t("help.shiftTab")],
1766
- ["Ctrl-T", ui.t("help.ctrlT")],
1767
- ["Up / Down", ui.t("help.arrows")],
1768
- ["Ctrl-C", ui.t("help.ctrlc")],
1769
- ];
1770
- for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
1771
- }
1772
-
1773
- module.exports = {
1774
- startRepl,
1775
- runtimeLabel,
1776
- runtimePromptForSession,
1777
- makeMemoryGuard,
1778
- makeStyleGuard,
1779
- applyPreferenceDefaults,
1780
- };