agentlas 0.5.5 → 0.7.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.
@@ -111,6 +111,14 @@ const STRINGS = {
111
111
  "team.usage": "usage: /team · /team <agent> <claude-code|codex|gemini|auto>",
112
112
  "team.set": "%s → %s",
113
113
  "routedImage": "routed to %s for image support",
114
+ "usageBar": "tokens",
115
+ "config.title": "Engine auto engagement — explicit on/off (default: off)",
116
+ "config.storm": "Stormbreaker auto-engage on direct-routed real work",
117
+ "config.network": "hep-network auto-borrow on direct-routed real work",
118
+ "config.usage": "usage: /config storm|network on|off · explicit /storm and /network always work",
119
+ "config.set": "%s → %s",
120
+ "config.autoEngage": "auto-engaging %s (opted in via /config — turn off with /config %s off)",
121
+ "help.config": "explicit on/off for Stormbreaker/hep-network auto engagement",
114
122
  "guard.imageWarn": "this agent works with images, but %s can't generate them — try /runtime gemini (or codex), or /team to pin one",
115
123
  "multimodal.title": "Multimodal fallback",
116
124
  "multimodal.set": "multimodal %s → %s",
@@ -317,6 +325,14 @@ const STRINGS = {
317
325
  "team.usage": "사용법: /team · /team <에이전트> <claude-code|codex|gemini|auto>",
318
326
  "team.set": "%s → %s",
319
327
  "routedImage": "이미지 지원을 위해 %s로 라우팅",
328
+ "usageBar": "토큰",
329
+ "config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
330
+ "config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
331
+ "config.network": "직답 라우팅된 실작업에 hep-network 자동 빌림",
332
+ "config.usage": "사용법: /config storm|network on|off · /storm·/network 명시 실행은 항상 가능",
333
+ "config.set": "%s → %s",
334
+ "config.autoEngage": "%s 자동 개입 (/config로 켜져 있음 — 해제: /config %s off)",
335
+ "help.config": "Stormbreaker/hep-network 자동 개입 명시적 on/off",
320
336
  "guard.imageWarn": "이 에이전트는 이미지 작업인데 %s는 이미지 생성 불가 — /runtime gemini(또는 codex), 또는 /team으로 고정",
321
337
  "multimodal.title": "멀티모달 fallback",
322
338
  "multimodal.set": "멀티모달 %s → %s",
@@ -89,6 +89,7 @@ const SLASH_COMMAND_META = [
89
89
  { command: "/permission", description: "Set read/write/full permission", category: "Settings", usage: "/permission full", detail: "No argument shows what read, write, and full mean.", aliases: ["/perm"] },
90
90
  { command: "/permissions", description: "Show or set current permission", category: "Settings", usage: "/permissions", detail: "Codex-style permission screen for Agentlas read/write/full." },
91
91
  { command: "/setup", description: "Run first-time setup again", category: "Settings", usage: "/setup", detail: "Re-run language, runtime, and default permission setup in-place." },
92
+ { command: "/config", description: "Explicit on/off for Stormbreaker and hep-network auto engagement", category: "Settings", usage: "/config [storm|network] [on|off]", detail: "Both engines default to off — no automatic activation. Turn one on to let direct-routed real-work prompts engage it; /storm and /network stay available explicitly either way.", examples: ["/config", "/config storm on", "/config network off"], aliases: ["/toggles"] },
92
93
  { command: "/cwd", description: "Show or change the working folder", category: "Files", usage: "/cwd <path>", detail: "Change the folder used for tools, file mentions, and local commands." },
93
94
  { command: "/memory", description: "Show the memory injected into this run", category: "Context", usage: "/memory", detail: "Print the project memory that Agentlas adds to agent turns." },
94
95
  { command: "/side", description: "Ask a side question without saving it to chat context", category: "Context", usage: "/side <question>", detail: "Runs a one-off answer using current context, then returns without appending to chat history.", aliases: ["/btw"] },
@@ -137,6 +138,7 @@ const HELP_KEY_BY_COMMAND = {
137
138
  "/permission": "help.permission",
138
139
  "/permissions": "help.permissions",
139
140
  "/setup": "help.setup",
141
+ "/config": "help.config",
140
142
  "/cwd": "help.cwd",
141
143
  "/memory": "help.memory",
142
144
  "/side": "help.side",
@@ -301,6 +301,7 @@ function startRepl(opts) {
301
301
  try {
302
302
  ui.beginTurn({
303
303
  ...composerMeta(),
304
+ usage: () => usageSummaryLine(), // 턴 중에도 최신 사용량을 footer에 라이브 반영
304
305
  onInterrupt: () => {
305
306
  if (!currentAbort || currentAbort.signal.aborted) return;
306
307
  currentAbort.abort();
@@ -325,11 +326,16 @@ function startRepl(opts) {
325
326
  : state.subject.system;
326
327
  state.routePreambleOnce = null;
327
328
  const sys = H.augmentSystem(db, subjectSystem, ctx, false);
329
+ // claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
330
+ // 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
331
+ // 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
332
+ // 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
333
+ const resumesServerSide = rt.kind === "claude-code" || rt.kind === "codex";
328
334
  const res = await runNativeTurn({
329
335
  kind: rt.kind,
330
336
  bin,
331
337
  prompt,
332
- systemPrompt: session.id ? "" : sys,
338
+ systemPrompt: session.id && resumesServerSide ? "" : sys,
333
339
  cwd: state.cwd,
334
340
  permission: state.permission,
335
341
  session,
@@ -466,6 +472,23 @@ function startRepl(opts) {
466
472
  state.routePreambleOnce = null;
467
473
  applyRuntimeFor(state.subject);
468
474
  }
475
+ // 직답 모드 — 전문 에이전트 확신이 없을 때. 페르소나 없음 + 능력(이미지) 라우팅 없음:
476
+ // 런타임은 세션 기본(baseRuntime) 그대로라, 일반 질문이 gemini 등으로 끌려가지 않는다.
477
+ function setSubjectDirect() {
478
+ if (state.subject && state.subject.kind === "direct") return; // 연속 직답 — 세션/히스토리 유지
479
+ state.subject = {
480
+ kind: "direct",
481
+ id: null,
482
+ slug: "agentlas-direct",
483
+ label: ui.lang === "ko" ? "Agentlas 직답" : "Agentlas direct",
484
+ system: H.directSystemPrompt ? H.directSystemPrompt(ui.lang) : "You are the Agentlas terminal's default assistant.",
485
+ capAgent: null,
486
+ };
487
+ state.history = [];
488
+ state.routePreambleOnce = null;
489
+ state.runtime = baseRuntime;
490
+ state.native = {};
491
+ }
469
492
  function setSubjectFirm(firm) {
470
493
  const sys = H.firmSystemPrompt(db, firm);
471
494
  state.subject = {
@@ -474,7 +497,7 @@ function startRepl(opts) {
474
497
  slug: firm.slug,
475
498
  label: displayName(firm) + " CEO",
476
499
  system: sys,
477
- capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
500
+ 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 },
478
501
  };
479
502
  state.history = [];
480
503
  state.routePreambleOnce = null;
@@ -721,6 +744,31 @@ function startRepl(opts) {
721
744
  case "runtime":
722
745
  setRuntime(arg);
723
746
  return true;
747
+ case "config":
748
+ case "toggles": {
749
+ // 클로드코드 /config 스타일 — 자동 엔진 개입은 전부 명시적 opt-in (기본 off).
750
+ const ENGINE_FLAGS = [
751
+ { key: "storm", pref: "autoStorm", label: ui.t("config.storm") },
752
+ { key: "network", pref: "autoNetwork", label: ui.t("config.network") },
753
+ ];
754
+ const showConfig = () => {
755
+ ui.line("");
756
+ ui.rule(ui.t("config.title"));
757
+ for (const f of ENGINE_FLAGS) {
758
+ const on = !!prefs[f.pref];
759
+ ui.line(" " + (on ? ui.c.green("● on ") : ui.c.dim("○ off")) + " " + ui.c.emerald(f.key.padEnd(9)) + ui.c.dim(f.label));
760
+ }
761
+ ui.line(" " + ui.c.faint(ui.t("config.usage")));
762
+ };
763
+ const [cfgKey, cfgVal] = arg.trim().split(/\s+/);
764
+ if (!cfgKey) return showConfig(), true;
765
+ const flag = ENGINE_FLAGS.find((f) => f.key === cfgKey.toLowerCase());
766
+ if (!flag || !/^(on|off)$/i.test(cfgVal || "")) return ui.warn(ui.t("config.usage")), true;
767
+ prefs[flag.pref] = /^on$/i.test(cfgVal);
768
+ if (opts.savePrefs) opts.savePrefs(prefs);
769
+ ui.ok(ui.t("config.set", flag.key, prefs[flag.pref] ? "on" : "off"));
770
+ return showConfig(), true;
771
+ }
724
772
  case "storm": {
725
773
  if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
726
774
  let goal = arg;
@@ -735,7 +783,13 @@ function startRepl(opts) {
735
783
  case "build": {
736
784
  if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
737
785
  ui.line("");
738
- await H.hepRun(["hep-build", arg], { cwd: state.cwd });
786
+ if (typeof H.terminalBuild !== "function") throw new Error("Terminal MCP build preflight is unavailable");
787
+ await H.terminalBuild(db, arg, {
788
+ cwd: state.cwd,
789
+ input: process.stdin,
790
+ promptOutput: process.stderr,
791
+ out: (line) => ui.line(String(line)),
792
+ });
739
793
  return true;
740
794
  }
741
795
  case "route": {
@@ -756,14 +810,6 @@ function startRepl(opts) {
756
810
  else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
757
811
  return true;
758
812
  }
759
- case "install": {
760
- if (!arg) return ui.warn("usage: /install <slug> (/search 로 먼저 찾으세요)"), true;
761
- if (H.cloudInstall) {
762
- try { const r = await H.cloudInstall(db, arg.trim()); ui.ok((r && r.name) ? `설치됨: ${r.name}` : `설치됨: ${arg.trim()}`); }
763
- catch (e) { ui.error((e && e.message) || String(e)); }
764
- } else ui.warn("설치 기능을 사용할 수 없습니다.");
765
- return true;
766
- }
767
813
  case "network":
768
814
  case "taskforce": {
769
815
  if (!arg) return ui.warn("usage: /network <요청> (A2A 태스크포스)"), true;
@@ -976,13 +1022,23 @@ function startRepl(opts) {
976
1022
  const s = n >= 1 && n <= list.length ? list[n - 1] : null;
977
1023
  if (!s) return ui.warn(ui.t("resume.noNum")), true;
978
1024
  const agent = H.resolveAgent(db, s.agentSlug);
979
- if (!agent) return ui.error(ui.t("noAgent", s.agentSlug)), true;
980
- setSubjectAgent(agent);
981
- if (s.kind && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
1025
+ if (agent) {
1026
+ setSubjectAgent(agent);
1027
+ } else if (s.agentSlug === "agentlas-direct") {
1028
+ setSubjectDirect(); // 직답 세션도 이어서 재개
1029
+ } else {
1030
+ return ui.error(ui.t("noAgent", s.agentSlug)), true;
1031
+ }
1032
+ // 저장된 CLI 런타임이 실제로 설치돼 있을 때만 그 세션을 복원한다. s.kind가 없거나
1033
+ // (손상된 resume 레코드) 런타임이 미설치면, 잘못된/"undefined" 키로 native 맵을
1034
+ // 덮어써 현재 세션을 잃는 것을 막고 새 대화로 시작한다.
1035
+ if (s.kind && s.sessionId && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
982
1036
  state.runtime = { mode: "cli", kind: s.kind };
983
1037
  baseRuntime = state.runtime;
1038
+ state.native = { [s.kind]: { id: s.sessionId } };
1039
+ } else {
1040
+ state.native = {};
984
1041
  }
985
- state.native = { [s.kind]: { id: s.sessionId } };
986
1042
  try {
987
1043
  const fs2 = require("node:fs");
988
1044
  if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
@@ -1097,10 +1153,14 @@ function startRepl(opts) {
1097
1153
  if (H.autoRouteAgent) {
1098
1154
  const choice = H.autoRouteAgent(db, t, ui.lang);
1099
1155
  if (choice) {
1100
- setSubjectAgent(choice.agent);
1156
+ if (choice.direct) {
1157
+ setSubjectDirect();
1158
+ } else {
1159
+ setSubjectAgent(choice.agent);
1160
+ }
1101
1161
  state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1102
- ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
1103
- routingNote(state.subject);
1162
+ ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${state.subject.label}`);
1163
+ if (!choice.direct) routingNote(state.subject);
1104
1164
  await runTurn(t);
1105
1165
  return ask();
1106
1166
  }
@@ -1120,37 +1180,97 @@ function startRepl(opts) {
1120
1180
  await handleSlash(t); // /exit 는 내부에서 process.exit
1121
1181
  return;
1122
1182
  }
1123
- if (!state.subject) {
1124
- const ags = H.listAgents(db);
1125
- const single = !/\s/.test(t);
1126
- if (/^\d+$/.test(t)) {
1127
- const n = parseInt(t, 10);
1128
- if (n >= 1 && n <= ags.length) {
1129
- setSubjectAgent(ags[n - 1]);
1130
- ui.ok(ui.t("switched", state.subject.label));
1131
- routingNote(state.subject);
1132
- return;
1183
+ // 직답 모드는 대상 고정이 아니라 "자동 라우팅 유지" — 매 메시지 재라우팅해,
1184
+ // 나중에 전문 요청이 오면 해당 에이전트로 자연스럽게 넘어간다.
1185
+ const directMode = !!(state.subject && state.subject.kind === "direct");
1186
+ if (!state.subject || directMode) {
1187
+ if (!state.subject) {
1188
+ const ags = H.listAgents(db);
1189
+ const single = !/\s/.test(t);
1190
+ if (/^\d+$/.test(t)) {
1191
+ const n = parseInt(t, 10);
1192
+ if (n >= 1 && n <= ags.length) {
1193
+ setSubjectAgent(ags[n - 1]);
1194
+ ui.ok(ui.t("switched", state.subject.label));
1195
+ routingNote(state.subject);
1196
+ return;
1197
+ }
1198
+ }
1199
+ if (single) {
1200
+ const a = H.resolveAgent(db, t);
1201
+ if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1202
+ const f = H.resolveFirm(db, t);
1203
+ if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1133
1204
  }
1134
- }
1135
- if (single) {
1136
- const a = H.resolveAgent(db, t);
1137
- if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1138
- const f = H.resolveFirm(db, t);
1139
- if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
1140
1205
  }
1141
1206
  if (H.autoRouteAgent) {
1142
1207
  const choice = H.autoRouteAgent(db, t, ui.lang);
1143
1208
  if (choice) {
1144
- setSubjectAgent(choice.agent);
1145
- state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1146
- ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
1147
- routingNote(state.subject);
1209
+ if (choice.direct) {
1210
+ setSubjectDirect();
1211
+ if (!directMode) {
1212
+ // 첫 직답 진입에만 알림 — 연속 직답 대화에서는 조용히.
1213
+ state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1214
+ ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
1215
+ }
1216
+ } else {
1217
+ setSubjectAgent(choice.agent);
1218
+ state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
1219
+ ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
1220
+ routingNote(state.subject);
1221
+ }
1222
+ }
1223
+ }
1224
+ // 자동 엔진 개입은 명시적 opt-in 전용 (/config, 기본 off): direct 판정 + 실작업형 프롬프트일 때만.
1225
+ if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
1226
+ if (prefs.autoStorm && H.stormRun) {
1227
+ ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
1228
+ await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
1229
+ return;
1230
+ }
1231
+ if (prefs.autoNetwork && H.hepRun) {
1232
+ ui.info(ui.t("config.autoEngage", "hep-network", "network"));
1233
+ ui.line("");
1234
+ await H.hepRun(["hep-network", t, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
1235
+ return;
1148
1236
  }
1149
1237
  }
1150
1238
  }
1151
1239
  await runTurn(expandMentions(t));
1152
1240
  }
1153
1241
 
1242
+ // 실작업(goal)형 프롬프트 감지 — 질문/잡담에는 자동 엔진(storm/network)을 절대 걸지 않는다.
1243
+ function goalLikePrompt(t) {
1244
+ const s = String(t || "").trim();
1245
+ if (s.length < 12) return false;
1246
+ if (/[??]\s*$/.test(s)) return false;
1247
+ return /(해줘|해라|만들|구현|수정|배포|정리|작성|분석|리팩터|고쳐|추가|빌드|테스트|돌려|실행|자동화|automate|build|implement|fix|refactor|deploy|create|write|run|ship)/i.test(s);
1248
+ }
1249
+
1250
+ // ── 연결 LLM 세션 사용량 요약 (챗 입력창 아래 상시 표시) ──
1251
+ // 호스트 이점: 단일 모델 CLI는 못 보여주는 멀티 LLM 합산 사용량을 항상 노출한다.
1252
+ function fmtTok(n) {
1253
+ if (!n) return "0";
1254
+ if (n >= 1e6) return (n >= 1e7 ? Math.round(n / 1e6) : (n / 1e6).toFixed(1)) + "m";
1255
+ if (n >= 1e3) return (n >= 1e5 ? Math.round(n / 1e3) : (n / 1e3).toFixed(1)) + "k";
1256
+ return String(n);
1257
+ }
1258
+ function usageSummaryLine() {
1259
+ const shortLabel = (label) => (label === "claude-code" ? "claude" : label);
1260
+ const labels = [];
1261
+ const push = (label) => { if (label && label !== "(none)" && !labels.includes(label)) labels.push(label); };
1262
+ for (const kind of installedKinds()) push(kind); // 연결(설치)된 CLI 런타임 — 미사용이어도 표시
1263
+ push(runtimeLabel(state.runtime)); // 현재 런타임 (BYOK/Ollama 포함)
1264
+ for (const label of Object.keys(state.cost)) push(label); // 세션 중 사용한 나머지
1265
+ const parts = labels.map((label) => {
1266
+ const e = state.cost[label];
1267
+ if (!e) return `${shortLabel(label)} 0`;
1268
+ if (e.in || e.out) return `${shortLabel(label)} ${fmtTok(e.in)}→${fmtTok(e.out)}`;
1269
+ return `${shortLabel(label)} ${e.turns}${ui.lang === "ko" ? "턴" : "t"}`;
1270
+ });
1271
+ return `${ui.t("usageBar")} ${parts.join(" · ")}`;
1272
+ }
1273
+
1154
1274
  // ── composer (raw-mode bottom box) main loop ──
1155
1275
  function composerMeta() {
1156
1276
  const rt = runtimeLabel(state.runtime);
@@ -1162,6 +1282,7 @@ function startRepl(opts) {
1162
1282
  permission: state.permission,
1163
1283
  permissionLabel,
1164
1284
  status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
1285
+ usage: usageSummaryLine(), // 챗 입력창 아래 상시 LLM 사용량 표시줄
1165
1286
  onCyclePermission: () => {
1166
1287
  const cycle = permissionCycle.step(state.permission);
1167
1288
  if (cycle.armed) {
@@ -1228,6 +1349,8 @@ function startRepl(opts) {
1228
1349
  if (closed) return process.exit(0);
1229
1350
  if (slashPalette.setEnabled) slashPalette.setEnabled(true);
1230
1351
  const cont = buffer != null;
1352
+ // 클래식 모드에도 사용량 상시 표시 (composer 모드에선 입력박스 아래 줄이 담당)
1353
+ if (!cont && !composer && Object.keys(state.cost).length) ui.line(ui.c.faint(" " + usageSummaryLine()));
1231
1354
  rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
1232
1355
  if (input.isContinuation(line)) {
1233
1356
  return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
@@ -280,7 +280,11 @@ class Ui {
280
280
  }
281
281
  }
282
282
  }
283
- return [...taskLines, rule, prompt, rule, statusLine];
283
+ // 중에도 사용량 표시줄 유지 (chrome.usage: 문자열 또는 라이브 getter)
284
+ const usageSrc = this._turnChrome.usage;
285
+ const usageText = typeof usageSrc === "function" ? usageSrc() : usageSrc;
286
+ const usageLine = usageText ? this.c.faint(truncateCells(String(usageText), width)) : null;
287
+ return [...taskLines, rule, prompt, rule, statusLine, ...(usageLine ? [usageLine] : [])];
284
288
  }
285
289
 
286
290
  _eraseFooter() {