agentlas 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -6
- package/bin/agentlas.cjs +55 -8
- package/engine/agentlas-api-agent.cjs +1 -1
- package/engine/agentlas-banner.cjs +40 -56
- package/engine/agentlas-capabilities.cjs +3 -0
- package/engine/agentlas-cloud-runtime.cjs +65 -11
- package/engine/agentlas-composer.cjs +112 -44
- package/engine/agentlas-doctor.cjs +40 -12
- package/engine/agentlas-i18n.cjs +136 -12
- package/engine/agentlas-input.cjs +118 -19
- package/engine/agentlas-native-host.cjs +381 -83
- package/engine/agentlas-parity.cjs +315 -45
- package/engine/agentlas-permissions.cjs +90 -0
- package/engine/agentlas-repl.cjs +239 -70
- package/engine/agentlas-tasks.cjs +111 -0
- package/engine/agentlas-tools.cjs +174 -12
- package/engine/agentlas-ui.cjs +352 -23
- package/engine/agentlas.cjs +2819 -351
- package/engine/semver.cjs +64 -0
- package/package.json +1 -1
- package/test/bootstrap-race.cjs +47 -0
- package/test/capture-runtime-guard.cjs +122 -0
- package/test/cloud-asset-restore.cjs +423 -0
- package/test/cloud-cas-client.cjs +333 -0
- package/test/cloud-owner-restore.cjs +183 -0
- package/test/cloud-runtime-paths.cjs +40 -0
- package/test/cloud-save-publish.cjs +453 -0
- package/test/credential-env-regression.cjs +52 -0
- package/test/login-loopback-security.cjs +115 -0
- package/test/mcp-config-isolation.cjs +36 -0
- package/test/permission-mapping.cjs +180 -0
- package/test/route-regression.cjs +121 -0
- package/test/run-api-regression.cjs +322 -0
- package/test/runtime-env-protection.cjs +45 -0
- package/test/semver-precedence.cjs +39 -0
- package/test/smoke.sh +20 -0
- package/test/sqlite-driver-probe.cjs +22 -0
- package/test/terminal-ui-regression.cjs +472 -0
- package/test/timeout-regression.cjs +218 -0
- package/test/tool-workspace-boundary.cjs +165 -0
- package/test/update-safety.cjs +376 -0
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -16,6 +16,7 @@ const caps = require("./agentlas-capabilities.cjs");
|
|
|
16
16
|
const input = require("./agentlas-input.cjs");
|
|
17
17
|
const i18n = require("./agentlas-i18n.cjs");
|
|
18
18
|
const style = require("./agentlas-style.cjs");
|
|
19
|
+
const permissions = require("./agentlas-permissions.cjs");
|
|
19
20
|
|
|
20
21
|
function runtimeLabel(rt) {
|
|
21
22
|
if (!rt) return "(none)";
|
|
@@ -56,6 +57,8 @@ function makeMemoryGuard(ui, heading) {
|
|
|
56
57
|
};
|
|
57
58
|
return {
|
|
58
59
|
c: ui.c,
|
|
60
|
+
lang: ui.lang,
|
|
61
|
+
t: (...a) => ui.t(...a),
|
|
59
62
|
streamStart: () => ui.streamStart(),
|
|
60
63
|
streamDelta: (t) => {
|
|
61
64
|
if (cut) {
|
|
@@ -89,6 +92,9 @@ function makeMemoryGuard(ui, heading) {
|
|
|
89
92
|
ok: (...a) => ui.ok(...a),
|
|
90
93
|
cost: (...a) => ui.cost(...a),
|
|
91
94
|
line: (...a) => ui.line(...a),
|
|
95
|
+
applyTaskTool: (...a) => ui.applyTaskTool(...a),
|
|
96
|
+
applyTaskResult: (...a) => ui.applyTaskResult(...a),
|
|
97
|
+
replaceTasks: (...a) => ui.replaceTasks(...a),
|
|
92
98
|
};
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -111,10 +117,13 @@ function makeStyleGuard(ui) {
|
|
|
111
117
|
};
|
|
112
118
|
return {
|
|
113
119
|
c: ui.c,
|
|
120
|
+
lang: ui.lang,
|
|
121
|
+
t: (...a) => ui.t(...a),
|
|
114
122
|
streamStart: () => {
|
|
115
123
|
buf = "";
|
|
116
124
|
inCode = false;
|
|
117
|
-
|
|
125
|
+
// This guard emits complete lines, so the persistent turn footer can safely stay visible.
|
|
126
|
+
ui.streamStart(true);
|
|
118
127
|
},
|
|
119
128
|
streamDelta: (text) => {
|
|
120
129
|
if (!text) return;
|
|
@@ -143,6 +152,9 @@ function makeStyleGuard(ui) {
|
|
|
143
152
|
cost: (...a) => ui.cost(...a),
|
|
144
153
|
line: (...a) => ui.line(...a),
|
|
145
154
|
stopSpinner: (...a) => ui.stopSpinner(...a),
|
|
155
|
+
applyTaskTool: (...a) => ui.applyTaskTool(...a),
|
|
156
|
+
applyTaskResult: (...a) => ui.applyTaskResult(...a),
|
|
157
|
+
replaceTasks: (...a) => ui.replaceTasks(...a),
|
|
146
158
|
};
|
|
147
159
|
}
|
|
148
160
|
|
|
@@ -157,7 +169,7 @@ function startRepl(opts) {
|
|
|
157
169
|
const state = {
|
|
158
170
|
subject: opts.subject || null,
|
|
159
171
|
runtime: opts.runtime,
|
|
160
|
-
permission: opts.permission
|
|
172
|
+
permission: opts.permission == null ? "write" : permissions.normalize(opts.permission),
|
|
161
173
|
cwd: opts.cwd,
|
|
162
174
|
history: [],
|
|
163
175
|
native: {}, // kind → { id }
|
|
@@ -197,6 +209,7 @@ function startRepl(opts) {
|
|
|
197
209
|
let closed = false;
|
|
198
210
|
let currentAbort = null;
|
|
199
211
|
let idleExitArmedUntil = 0;
|
|
212
|
+
const permissionCycle = permissions.createCycleController();
|
|
200
213
|
rl.on("close", () => {
|
|
201
214
|
if (handoff) return; // intentionally closed to hand stdin to the raw-mode composer
|
|
202
215
|
closed = true;
|
|
@@ -235,6 +248,19 @@ function startRepl(opts) {
|
|
|
235
248
|
return { projectPath: state.projectPath, agentId: state.subject && state.subject.id, permission: state.permission, cwd: state.cwd, lang: ui.lang };
|
|
236
249
|
}
|
|
237
250
|
|
|
251
|
+
function setPermission(value, options = {}) {
|
|
252
|
+
const notify = options.notify !== false;
|
|
253
|
+
const persist = options.persist !== false;
|
|
254
|
+
const level = permissions.normalize(value);
|
|
255
|
+
state.permission = level;
|
|
256
|
+
if (persist) {
|
|
257
|
+
prefs.permission = level;
|
|
258
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
259
|
+
}
|
|
260
|
+
if (notify) ui.ok(ui.t("permSet", level));
|
|
261
|
+
return level;
|
|
262
|
+
}
|
|
263
|
+
|
|
238
264
|
// Session usage ledger — accumulate per runtime label (host advantage: no single-model CLI can show this).
|
|
239
265
|
function recordCost(label, usage) {
|
|
240
266
|
const e = state.cost[label] || (state.cost[label] = { turns: 0, in: 0, out: 0, cost: 0, ms: 0 });
|
|
@@ -270,22 +296,28 @@ function startRepl(opts) {
|
|
|
270
296
|
// ── run one turn ──
|
|
271
297
|
async function runTurn(prompt, runOptions = {}) {
|
|
272
298
|
busy = true;
|
|
273
|
-
ui.beginTurn(); // 라이브 경과시간 스피너의 턴 시작점
|
|
274
299
|
currentAbort = new AbortController();
|
|
275
300
|
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
301
|
try {
|
|
302
|
+
ui.beginTurn({
|
|
303
|
+
...composerMeta(),
|
|
304
|
+
usage: () => usageSummaryLine(), // 턴 중에도 최신 사용량을 footer에 라이브 반영
|
|
305
|
+
onInterrupt: () => {
|
|
306
|
+
if (!currentAbort || currentAbort.signal.aborted) return;
|
|
307
|
+
currentAbort.abort();
|
|
308
|
+
ui.warn(ui.t("interrupted"));
|
|
309
|
+
},
|
|
310
|
+
}); // 작업 중에도 composer/status bar와 실제 runtime task 목록을 화면 하단에 유지
|
|
311
|
+
const recordHistoryEntry = !runOptions.side;
|
|
312
|
+
const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
|
|
313
|
+
const ctx = { ...ctxNow(), lang: targetLang, uiLang: ui.lang };
|
|
314
|
+
const rt = state.runtime;
|
|
315
|
+
const costLabel = runtimeLabel(rt);
|
|
316
|
+
const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
|
|
317
|
+
ui._lastUsage = null;
|
|
318
|
+
const assistantUi = makeStyleGuard(ui);
|
|
319
|
+
const thinkingText = i18n.t(targetLang, "thinkingWith", costLabel);
|
|
320
|
+
ui.status(thinkingText);
|
|
289
321
|
if (rt.mode === "cli") {
|
|
290
322
|
const bin = H.which(H.RUNTIME_BIN[rt.kind]) || H.RUNTIME_BIN[rt.kind];
|
|
291
323
|
const session = state.native[rt.kind] || (state.native[rt.kind] = {});
|
|
@@ -305,7 +337,7 @@ function startRepl(opts) {
|
|
|
305
337
|
model: rt.model || null, // /model (claude --model, codex -m, gemini -m)
|
|
306
338
|
effort: state.effort || null, // /effort (codex reasoning effort, claude think-keyword)
|
|
307
339
|
mcpServers:
|
|
308
|
-
|
|
340
|
+
state.permission === "full" && H.mcpServers
|
|
309
341
|
? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
|
|
310
342
|
: [],
|
|
311
343
|
env: runEnv,
|
|
@@ -341,7 +373,7 @@ function startRepl(opts) {
|
|
|
341
373
|
apiKey,
|
|
342
374
|
system: sys,
|
|
343
375
|
messages,
|
|
344
|
-
ctx,
|
|
376
|
+
ctx: { ...ctx, env: runEnv },
|
|
345
377
|
ui: guard,
|
|
346
378
|
signal,
|
|
347
379
|
});
|
|
@@ -435,6 +467,23 @@ function startRepl(opts) {
|
|
|
435
467
|
state.routePreambleOnce = null;
|
|
436
468
|
applyRuntimeFor(state.subject);
|
|
437
469
|
}
|
|
470
|
+
// 직답 모드 — 전문 에이전트 확신이 없을 때. 페르소나 없음 + 능력(이미지) 라우팅 없음:
|
|
471
|
+
// 런타임은 세션 기본(baseRuntime) 그대로라, 일반 질문이 gemini 등으로 끌려가지 않는다.
|
|
472
|
+
function setSubjectDirect() {
|
|
473
|
+
if (state.subject && state.subject.kind === "direct") return; // 연속 직답 — 세션/히스토리 유지
|
|
474
|
+
state.subject = {
|
|
475
|
+
kind: "direct",
|
|
476
|
+
id: null,
|
|
477
|
+
slug: "agentlas-direct",
|
|
478
|
+
label: ui.lang === "ko" ? "Agentlas 직답" : "Agentlas direct",
|
|
479
|
+
system: H.directSystemPrompt ? H.directSystemPrompt(ui.lang) : "You are the Agentlas terminal's default assistant.",
|
|
480
|
+
capAgent: null,
|
|
481
|
+
};
|
|
482
|
+
state.history = [];
|
|
483
|
+
state.routePreambleOnce = null;
|
|
484
|
+
state.runtime = baseRuntime;
|
|
485
|
+
state.native = {};
|
|
486
|
+
}
|
|
438
487
|
function setSubjectFirm(firm) {
|
|
439
488
|
const sys = H.firmSystemPrompt(db, firm);
|
|
440
489
|
state.subject = {
|
|
@@ -551,8 +600,8 @@ function startRepl(opts) {
|
|
|
551
600
|
|
|
552
601
|
function printSlashSkills() {
|
|
553
602
|
ui.line("");
|
|
554
|
-
ui.rule("
|
|
555
|
-
for (const entry of input.slashCommandEntries()) {
|
|
603
|
+
ui.rule(ui.t("skills.title"));
|
|
604
|
+
for (const entry of input.slashCommandEntries(ui.lang)) {
|
|
556
605
|
const tag = entry.category ? ui.c.faint(entry.category.padEnd(10)) : "";
|
|
557
606
|
ui.line(" " + ui.c.emerald(entry.command.padEnd(18)) + tag + ui.c.dim(entry.description));
|
|
558
607
|
if (!entry.aliasOf && entry.usage) ui.line(" " + ui.c.faint(" ".repeat(18) + entry.usage));
|
|
@@ -561,14 +610,10 @@ function startRepl(opts) {
|
|
|
561
610
|
|
|
562
611
|
function printPermissions() {
|
|
563
612
|
ui.line("");
|
|
564
|
-
ui.rule("
|
|
565
|
-
ui.line(" " + ui.c.faint("
|
|
566
|
-
const
|
|
567
|
-
|
|
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) {
|
|
613
|
+
ui.rule(ui.t("permissions.title"));
|
|
614
|
+
ui.line(" " + ui.c.faint(ui.t("permissions.current")) + " " + ui.c.emerald(state.permission));
|
|
615
|
+
for (const level of permissions.LEVELS) {
|
|
616
|
+
const description = permissions.copy(level, ui.lang).description;
|
|
572
617
|
const mark = level === state.permission ? "› " : " ";
|
|
573
618
|
ui.line(" " + ui.c.emerald((mark + level).padEnd(10)) + ui.c.dim(description));
|
|
574
619
|
}
|
|
@@ -633,7 +678,8 @@ function startRepl(opts) {
|
|
|
633
678
|
ui.tool("$ " + cmd);
|
|
634
679
|
const r = spawnSync("bash", ["-lc", cmd], { cwd: state.cwd, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
|
|
635
680
|
const out = ((r.stdout || "") + (r.stderr || "")).trim();
|
|
636
|
-
|
|
681
|
+
// `!command` is explicit user output, unlike autonomous runtime traces: keep it inspectable.
|
|
682
|
+
ui.toolResult(out || ("exit " + (r.status == null ? "?" : r.status)), r.status === 0 || r.status == null, { verbose: true });
|
|
637
683
|
}
|
|
638
684
|
|
|
639
685
|
// @path — inline the contents of mentioned files into the prompt as fenced context.
|
|
@@ -693,6 +739,31 @@ function startRepl(opts) {
|
|
|
693
739
|
case "runtime":
|
|
694
740
|
setRuntime(arg);
|
|
695
741
|
return true;
|
|
742
|
+
case "config":
|
|
743
|
+
case "toggles": {
|
|
744
|
+
// 클로드코드 /config 스타일 — 자동 엔진 개입은 전부 명시적 opt-in (기본 off).
|
|
745
|
+
const ENGINE_FLAGS = [
|
|
746
|
+
{ key: "storm", pref: "autoStorm", label: ui.t("config.storm") },
|
|
747
|
+
{ key: "network", pref: "autoNetwork", label: ui.t("config.network") },
|
|
748
|
+
];
|
|
749
|
+
const showConfig = () => {
|
|
750
|
+
ui.line("");
|
|
751
|
+
ui.rule(ui.t("config.title"));
|
|
752
|
+
for (const f of ENGINE_FLAGS) {
|
|
753
|
+
const on = !!prefs[f.pref];
|
|
754
|
+
ui.line(" " + (on ? ui.c.green("● on ") : ui.c.dim("○ off")) + " " + ui.c.emerald(f.key.padEnd(9)) + ui.c.dim(f.label));
|
|
755
|
+
}
|
|
756
|
+
ui.line(" " + ui.c.faint(ui.t("config.usage")));
|
|
757
|
+
};
|
|
758
|
+
const [cfgKey, cfgVal] = arg.trim().split(/\s+/);
|
|
759
|
+
if (!cfgKey) return showConfig(), true;
|
|
760
|
+
const flag = ENGINE_FLAGS.find((f) => f.key === cfgKey.toLowerCase());
|
|
761
|
+
if (!flag || !/^(on|off)$/i.test(cfgVal || "")) return ui.warn(ui.t("config.usage")), true;
|
|
762
|
+
prefs[flag.pref] = /^on$/i.test(cfgVal);
|
|
763
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
764
|
+
ui.ok(ui.t("config.set", flag.key, prefs[flag.pref] ? "on" : "off"));
|
|
765
|
+
return showConfig(), true;
|
|
766
|
+
}
|
|
696
767
|
case "storm": {
|
|
697
768
|
if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
|
|
698
769
|
let goal = arg;
|
|
@@ -801,16 +872,14 @@ function startRepl(opts) {
|
|
|
801
872
|
return true;
|
|
802
873
|
}
|
|
803
874
|
if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
|
|
804
|
-
|
|
805
|
-
ui.ok(ui.t("permSet", p));
|
|
875
|
+
setPermission(p);
|
|
806
876
|
return true;
|
|
807
877
|
}
|
|
808
878
|
case "permissions":
|
|
809
879
|
if (arg) {
|
|
810
880
|
const p = (arg || "").toLowerCase();
|
|
811
881
|
if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
|
|
812
|
-
|
|
813
|
-
ui.ok(ui.t("permSet", p));
|
|
882
|
+
setPermission(p);
|
|
814
883
|
} else {
|
|
815
884
|
printPermissions();
|
|
816
885
|
}
|
|
@@ -913,10 +982,7 @@ function startRepl(opts) {
|
|
|
913
982
|
const servers = H.mcpServers ? H.mcpServers(db) : [];
|
|
914
983
|
ui.line("");
|
|
915
984
|
ui.rule("MCP");
|
|
916
|
-
|
|
917
|
-
ui.info(ui.t("mcp.none"));
|
|
918
|
-
return true;
|
|
919
|
-
}
|
|
985
|
+
ui.line(" " + ui.c.emerald(ui.t("mcp.playwright").padEnd(22)) + ui.c.blue("stdio ") + ui.c.green("on ") + ui.c.dim(" " + ui.t("mcp.fullOnly")));
|
|
920
986
|
for (const s of servers) {
|
|
921
987
|
let envKeys = [];
|
|
922
988
|
try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
|
|
@@ -925,7 +991,7 @@ function startRepl(opts) {
|
|
|
925
991
|
const envStr = envKeys.length ? envKeys.join(", ") : "no key";
|
|
926
992
|
ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
|
|
927
993
|
}
|
|
928
|
-
const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 =
|
|
994
|
+
const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 = full-only Playwright
|
|
929
995
|
ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
|
|
930
996
|
ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
|
|
931
997
|
return true;
|
|
@@ -953,8 +1019,13 @@ function startRepl(opts) {
|
|
|
953
1019
|
const s = n >= 1 && n <= list.length ? list[n - 1] : null;
|
|
954
1020
|
if (!s) return ui.warn(ui.t("resume.noNum")), true;
|
|
955
1021
|
const agent = H.resolveAgent(db, s.agentSlug);
|
|
956
|
-
if (
|
|
957
|
-
|
|
1022
|
+
if (agent) {
|
|
1023
|
+
setSubjectAgent(agent);
|
|
1024
|
+
} else if (s.agentSlug === "agentlas-direct") {
|
|
1025
|
+
setSubjectDirect(); // 직답 세션도 이어서 재개
|
|
1026
|
+
} else {
|
|
1027
|
+
return ui.error(ui.t("noAgent", s.agentSlug)), true;
|
|
1028
|
+
}
|
|
958
1029
|
if (s.kind && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
|
|
959
1030
|
state.runtime = { mode: "cli", kind: s.kind };
|
|
960
1031
|
baseRuntime = state.runtime;
|
|
@@ -1074,10 +1145,14 @@ function startRepl(opts) {
|
|
|
1074
1145
|
if (H.autoRouteAgent) {
|
|
1075
1146
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1076
1147
|
if (choice) {
|
|
1077
|
-
|
|
1148
|
+
if (choice.direct) {
|
|
1149
|
+
setSubjectDirect();
|
|
1150
|
+
} else {
|
|
1151
|
+
setSubjectAgent(choice.agent);
|
|
1152
|
+
}
|
|
1078
1153
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1079
|
-
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${
|
|
1080
|
-
routingNote(state.subject);
|
|
1154
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${state.subject.label}`);
|
|
1155
|
+
if (!choice.direct) routingNote(state.subject);
|
|
1081
1156
|
await runTurn(t);
|
|
1082
1157
|
return ask();
|
|
1083
1158
|
}
|
|
@@ -1097,53 +1172,143 @@ function startRepl(opts) {
|
|
|
1097
1172
|
await handleSlash(t); // /exit 는 내부에서 process.exit
|
|
1098
1173
|
return;
|
|
1099
1174
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1175
|
+
// 직답 모드는 대상 고정이 아니라 "자동 라우팅 유지" — 매 메시지 재라우팅해,
|
|
1176
|
+
// 나중에 전문 요청이 오면 해당 에이전트로 자연스럽게 넘어간다.
|
|
1177
|
+
const directMode = !!(state.subject && state.subject.kind === "direct");
|
|
1178
|
+
if (!state.subject || directMode) {
|
|
1179
|
+
if (!state.subject) {
|
|
1180
|
+
const ags = H.listAgents(db);
|
|
1181
|
+
const single = !/\s/.test(t);
|
|
1182
|
+
if (/^\d+$/.test(t)) {
|
|
1183
|
+
const n = parseInt(t, 10);
|
|
1184
|
+
if (n >= 1 && n <= ags.length) {
|
|
1185
|
+
setSubjectAgent(ags[n - 1]);
|
|
1186
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
1187
|
+
routingNote(state.subject);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (single) {
|
|
1192
|
+
const a = H.resolveAgent(db, t);
|
|
1193
|
+
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1194
|
+
const f = H.resolveFirm(db, t);
|
|
1195
|
+
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1110
1196
|
}
|
|
1111
|
-
}
|
|
1112
|
-
if (single) {
|
|
1113
|
-
const a = H.resolveAgent(db, t);
|
|
1114
|
-
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1115
|
-
const f = H.resolveFirm(db, t);
|
|
1116
|
-
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1117
1197
|
}
|
|
1118
1198
|
if (H.autoRouteAgent) {
|
|
1119
1199
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1120
1200
|
if (choice) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1201
|
+
if (choice.direct) {
|
|
1202
|
+
setSubjectDirect();
|
|
1203
|
+
if (!directMode) {
|
|
1204
|
+
// 첫 직답 진입에만 알림 — 연속 직답 대화에서는 조용히.
|
|
1205
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1206
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
setSubjectAgent(choice.agent);
|
|
1210
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1211
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
1212
|
+
routingNote(state.subject);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// 자동 엔진 개입은 명시적 opt-in 전용 (/config, 기본 off): direct 판정 + 실작업형 프롬프트일 때만.
|
|
1217
|
+
if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
|
|
1218
|
+
if (prefs.autoStorm && H.stormRun) {
|
|
1219
|
+
ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
|
|
1220
|
+
await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (prefs.autoNetwork && H.hepRun) {
|
|
1224
|
+
ui.info(ui.t("config.autoEngage", "hep-network", "network"));
|
|
1225
|
+
ui.line("");
|
|
1226
|
+
await H.hepRun(["hep-network", t, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
1227
|
+
return;
|
|
1125
1228
|
}
|
|
1126
1229
|
}
|
|
1127
1230
|
}
|
|
1128
1231
|
await runTurn(expandMentions(t));
|
|
1129
1232
|
}
|
|
1130
1233
|
|
|
1234
|
+
// 실작업(goal)형 프롬프트 감지 — 질문/잡담에는 자동 엔진(storm/network)을 절대 걸지 않는다.
|
|
1235
|
+
function goalLikePrompt(t) {
|
|
1236
|
+
const s = String(t || "").trim();
|
|
1237
|
+
if (s.length < 12) return false;
|
|
1238
|
+
if (/[??]\s*$/.test(s)) return false;
|
|
1239
|
+
return /(해줘|해라|만들|구현|수정|배포|정리|작성|분석|리팩터|고쳐|추가|빌드|테스트|돌려|실행|자동화|automate|build|implement|fix|refactor|deploy|create|write|run|ship)/i.test(s);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// ── 연결 LLM 세션 사용량 요약 (챗 입력창 아래 상시 표시) ──
|
|
1243
|
+
// 호스트 이점: 단일 모델 CLI는 못 보여주는 멀티 LLM 합산 사용량을 항상 노출한다.
|
|
1244
|
+
function fmtTok(n) {
|
|
1245
|
+
if (!n) return "0";
|
|
1246
|
+
if (n >= 1e6) return (n >= 1e7 ? Math.round(n / 1e6) : (n / 1e6).toFixed(1)) + "m";
|
|
1247
|
+
if (n >= 1e3) return (n >= 1e5 ? Math.round(n / 1e3) : (n / 1e3).toFixed(1)) + "k";
|
|
1248
|
+
return String(n);
|
|
1249
|
+
}
|
|
1250
|
+
function usageSummaryLine() {
|
|
1251
|
+
const shortLabel = (label) => (label === "claude-code" ? "claude" : label);
|
|
1252
|
+
const labels = [];
|
|
1253
|
+
const push = (label) => { if (label && label !== "(none)" && !labels.includes(label)) labels.push(label); };
|
|
1254
|
+
for (const kind of installedKinds()) push(kind); // 연결(설치)된 CLI 런타임 — 미사용이어도 표시
|
|
1255
|
+
push(runtimeLabel(state.runtime)); // 현재 런타임 (BYOK/Ollama 포함)
|
|
1256
|
+
for (const label of Object.keys(state.cost)) push(label); // 세션 중 사용한 나머지
|
|
1257
|
+
const parts = labels.map((label) => {
|
|
1258
|
+
const e = state.cost[label];
|
|
1259
|
+
if (!e) return `${shortLabel(label)} 0`;
|
|
1260
|
+
if (e.in || e.out) return `${shortLabel(label)} ${fmtTok(e.in)}→${fmtTok(e.out)}`;
|
|
1261
|
+
return `${shortLabel(label)} ${e.turns}${ui.lang === "ko" ? "턴" : "t"}`;
|
|
1262
|
+
});
|
|
1263
|
+
return `${ui.t("usageBar")} ${parts.join(" · ")}`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1131
1266
|
// ── composer (raw-mode bottom box) main loop ──
|
|
1132
|
-
function
|
|
1267
|
+
function composerMeta() {
|
|
1133
1268
|
const rt = runtimeLabel(state.runtime);
|
|
1134
1269
|
const subj = state.subject ? state.subject.label : ui.t("composer.autoroute");
|
|
1135
1270
|
const eff = state.effort ? " · " + state.effort : "";
|
|
1136
|
-
|
|
1271
|
+
const permissionLabel = permissions.copy(state.permission, ui.lang).label;
|
|
1272
|
+
return {
|
|
1273
|
+
lang: ui.lang,
|
|
1274
|
+
permission: state.permission,
|
|
1275
|
+
permissionLabel,
|
|
1276
|
+
status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
|
|
1277
|
+
usage: usageSummaryLine(), // 챗 입력창 아래 상시 LLM 사용량 표시줄
|
|
1278
|
+
onCyclePermission: () => {
|
|
1279
|
+
const cycle = permissionCycle.step(state.permission);
|
|
1280
|
+
if (cycle.armed) {
|
|
1281
|
+
return {
|
|
1282
|
+
...composerMeta(),
|
|
1283
|
+
confirmation: ui.t("permFullArm"),
|
|
1284
|
+
confirmationTone: "danger",
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const level = setPermission(cycle.level, { notify: false, persist: false });
|
|
1288
|
+
return {
|
|
1289
|
+
...composerMeta(),
|
|
1290
|
+
confirmation: cycle.enteredFull
|
|
1291
|
+
? ui.t("permFullConfirm")
|
|
1292
|
+
: ui.t("permCycleConfirm", permissions.copy(level, ui.lang).label),
|
|
1293
|
+
confirmationTone: cycle.enteredFull ? "danger" : "normal",
|
|
1294
|
+
};
|
|
1295
|
+
},
|
|
1296
|
+
onPermissionCycleCancel: () => {
|
|
1297
|
+
permissionCycle.cancel();
|
|
1298
|
+
return { ...composerMeta(), confirmation: null, confirmationTone: null };
|
|
1299
|
+
},
|
|
1300
|
+
};
|
|
1137
1301
|
}
|
|
1138
1302
|
async function composerLoop() {
|
|
1139
1303
|
let buffer = "";
|
|
1140
1304
|
while (!closed) {
|
|
1141
1305
|
let r;
|
|
1142
1306
|
try {
|
|
1307
|
+
const meta = composerMeta();
|
|
1143
1308
|
r = await composer.read({
|
|
1144
1309
|
glyph: buffer ? "…" : "›",
|
|
1145
|
-
|
|
1146
|
-
suggest: (l) => input.slashCommandSuggestions(l),
|
|
1310
|
+
...meta,
|
|
1311
|
+
suggest: (l) => input.slashCommandSuggestions(l, 12, ui.lang),
|
|
1147
1312
|
complete: completer,
|
|
1148
1313
|
});
|
|
1149
1314
|
} catch (e) {
|
|
@@ -1176,6 +1341,8 @@ function startRepl(opts) {
|
|
|
1176
1341
|
if (closed) return process.exit(0);
|
|
1177
1342
|
if (slashPalette.setEnabled) slashPalette.setEnabled(true);
|
|
1178
1343
|
const cont = buffer != null;
|
|
1344
|
+
// 클래식 모드에도 사용량 상시 표시 (composer 모드에선 입력박스 아래 줄이 담당)
|
|
1345
|
+
if (!cont && !composer && Object.keys(state.cost).length) ui.line(ui.c.faint(" " + usageSummaryLine()));
|
|
1179
1346
|
rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
|
|
1180
1347
|
if (input.isContinuation(line)) {
|
|
1181
1348
|
return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
|
|
@@ -1236,10 +1403,10 @@ function startRepl(opts) {
|
|
|
1236
1403
|
function printHelp(ui) {
|
|
1237
1404
|
const c = ui.c;
|
|
1238
1405
|
ui.line("");
|
|
1239
|
-
ui.rule("
|
|
1240
|
-
ui.line(" " + c.bold(c.text("
|
|
1406
|
+
ui.rule(ui.t("help.title"));
|
|
1407
|
+
ui.line(" " + c.bold(c.text(ui.t("help.intro"))));
|
|
1241
1408
|
ui.line("");
|
|
1242
|
-
ui.line(" " + c.faint("
|
|
1409
|
+
ui.line(" " + c.faint(ui.t("help.commands")));
|
|
1243
1410
|
const rows = [
|
|
1244
1411
|
[ui.t("help.talkKey"), ui.t("help.talk")],
|
|
1245
1412
|
["/skills", ui.t("help.skills")],
|
|
@@ -1297,10 +1464,12 @@ function printKeybindings(ui) {
|
|
|
1297
1464
|
["!cmd", ui.t("help.bang")],
|
|
1298
1465
|
["\\ + Enter", ui.t("help.multiline")],
|
|
1299
1466
|
["Tab", ui.t("help.tab")],
|
|
1467
|
+
["Shift-Tab", ui.t("help.shiftTab")],
|
|
1468
|
+
["Ctrl-T", ui.t("help.ctrlT")],
|
|
1300
1469
|
["Up / Down", ui.t("help.arrows")],
|
|
1301
1470
|
["Ctrl-C", ui.t("help.ctrlc")],
|
|
1302
1471
|
];
|
|
1303
1472
|
for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
|
|
1304
1473
|
}
|
|
1305
1474
|
|
|
1306
|
-
module.exports = { startRepl, runtimeLabel };
|
|
1475
|
+
module.exports = { startRepl, runtimeLabel, makeMemoryGuard, makeStyleGuard };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/* Normalize only task/todo events emitted by the native runtimes.
|
|
4
|
+
* Ordinary tool activity is intentionally excluded: a command is not a fabricated plan item.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function statusOf(value, completed) {
|
|
8
|
+
if (completed === true) return "completed";
|
|
9
|
+
const status = String(value || "pending").trim().toLowerCase().replace(/[ -]+/g, "_");
|
|
10
|
+
if (["completed", "complete", "done", "success", "succeeded"].includes(status)) return "completed";
|
|
11
|
+
if (["in_progress", "active", "running", "started"].includes(status)) return "in_progress";
|
|
12
|
+
if (["failed", "error", "blocked", "cancelled", "canceled"].includes(status)) return "failed";
|
|
13
|
+
return "pending";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listFrom(payload) {
|
|
17
|
+
if (Array.isArray(payload)) return payload;
|
|
18
|
+
if (!payload || typeof payload !== "object") return [];
|
|
19
|
+
for (const key of ["todos", "items", "tasks"]) {
|
|
20
|
+
if (Array.isArray(payload[key])) return payload[key];
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasExplicitList(payload) {
|
|
26
|
+
if (Array.isArray(payload)) return true;
|
|
27
|
+
if (!payload || typeof payload !== "object") return false;
|
|
28
|
+
return ["todos", "items", "tasks"].some((key) => Array.isArray(payload[key]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeLabel(value, max = 500) {
|
|
32
|
+
let text = String(value || "");
|
|
33
|
+
// Runtime task text is untrusted terminal content. Strip OSC/CSI/escape controls,
|
|
34
|
+
// flatten line breaks, then cap it before it reaches footer row accounting.
|
|
35
|
+
text = text
|
|
36
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
37
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
38
|
+
.replace(/\x1b./g, "")
|
|
39
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
40
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
41
|
+
.replace(/\s+/g, " ")
|
|
42
|
+
.trim();
|
|
43
|
+
return Array.from(text).slice(0, max).join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeTaskList(payload, source = "runtime") {
|
|
47
|
+
return listFrom(payload).flatMap((raw, index) => {
|
|
48
|
+
if (!raw || typeof raw !== "object") return [];
|
|
49
|
+
const label = sanitizeLabel(raw.content || raw.subject || raw.description || raw.text || raw.title || raw.activeForm);
|
|
50
|
+
if (!label) return [];
|
|
51
|
+
return [{
|
|
52
|
+
id: String(raw.id || raw.taskId || `${source}:${index}`),
|
|
53
|
+
label,
|
|
54
|
+
status: statusOf(raw.status, raw.completed),
|
|
55
|
+
source,
|
|
56
|
+
}];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applyTaskTool(current, name, payload, toolId) {
|
|
61
|
+
const tool = String(name || "").toLowerCase();
|
|
62
|
+
if (tool === "todowrite" || tool === "write_todos") {
|
|
63
|
+
return normalizeTaskList(payload, tool);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tool !== "taskcreate" && tool !== "taskupdate") return current;
|
|
67
|
+
|
|
68
|
+
const next = Array.isArray(current) ? current.map((task) => ({ ...task })) : [];
|
|
69
|
+
if (tool === "taskcreate") {
|
|
70
|
+
const label = sanitizeLabel(payload?.subject || payload?.description || payload?.activeForm);
|
|
71
|
+
if (!label) return next;
|
|
72
|
+
next.push({ id: String(payload?.taskId || toolId || `taskcreate:${next.length}`), label, status: "pending", source: "taskcreate" });
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
if (tool === "taskupdate") {
|
|
76
|
+
const id = String(payload?.taskId || toolId || "");
|
|
77
|
+
if (!id) return next;
|
|
78
|
+
if (String(payload?.status || "").toLowerCase() === "deleted") return next.filter((task) => task.id !== id);
|
|
79
|
+
const index = next.findIndex((task) => task.id === id);
|
|
80
|
+
const label = sanitizeLabel(payload?.subject || payload?.description || payload?.activeForm || (index >= 0 && next[index].label) || id);
|
|
81
|
+
const task = { id, label, status: statusOf(payload?.status), source: "taskupdate" };
|
|
82
|
+
if (index >= 0) next[index] = { ...next[index], ...task };
|
|
83
|
+
else next.push(task);
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applyTaskResult(current, name, payload, toolId) {
|
|
89
|
+
const tool = String(name || "").toLowerCase();
|
|
90
|
+
if (!["taskcreate", "taskupdate", "tasklist"].includes(tool)) return current;
|
|
91
|
+
if (!payload || typeof payload !== "object") return current;
|
|
92
|
+
if (tool === "tasklist") {
|
|
93
|
+
return hasExplicitList(payload) ? normalizeTaskList(payload, "tasklist") : current;
|
|
94
|
+
}
|
|
95
|
+
const record = payload.task && typeof payload.task === "object" ? payload.task : payload;
|
|
96
|
+
const id = String(record.id || record.taskId || payload.taskId || "");
|
|
97
|
+
if (!id) return current;
|
|
98
|
+
const next = Array.isArray(current) ? current.map((task) => ({ ...task })) : [];
|
|
99
|
+
const provisional = next.findIndex((task) => task.id === String(toolId || ""));
|
|
100
|
+
const existing = next.findIndex((task) => task.id === id);
|
|
101
|
+
const index = existing >= 0 ? existing : provisional;
|
|
102
|
+
const prior = index >= 0 ? next[index] : null;
|
|
103
|
+
const label = sanitizeLabel(record.content || record.subject || record.description || record.text || record.title || prior?.label || id);
|
|
104
|
+
const task = { id, label, status: statusOf(record.status || prior?.status), source: tool || "task-result" };
|
|
105
|
+
if (index >= 0) next[index] = { ...prior, ...task };
|
|
106
|
+
else next.push(task);
|
|
107
|
+
if (existing >= 0 && provisional >= 0 && provisional !== existing) next.splice(provisional, 1);
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { statusOf, sanitizeLabel, normalizeTaskList, applyTaskTool, applyTaskResult };
|