agentlas 1.0.44 → 1.0.46

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.46 — 2026-08-11
4
+
5
+ Pick from a list instead of retyping a slug.
6
+
7
+ - `/search` in the shell now shows its results as a list you move through with the
8
+ arrow keys; Enter installs the one you chose. `/graph` does the same and runs the
9
+ graph you pick. Escape cancels either.
10
+ - Search results say what the listing actually means for you — "callable without
11
+ installing" or "install to use" — rather than repeating the server's own enum.
12
+ - Commands now know which surface you are standing on, so the next step they print is
13
+ one you can actually type there. Inside the shell, `search` ends with `/install <slug>`
14
+ instead of `agentlas install <slug>`, which the shell would have refused.
15
+
16
+ ## 1.0.45 — 2026-08-11
17
+
18
+ The command surface was rebuilt from one catalog, and failures stopped printing JSON.
19
+
20
+ - **Commands.** The same list used to be maintained by hand in four places, and they
21
+ disagreed: an English screen advertised a Korean argument hint, one feature was sold
22
+ twice under two names, and a de-duplication pass then hid `/switch`, `/list` and
23
+ `/exit` entirely. There is now a single catalog. Aliases are a field on a command,
24
+ never a row of their own, so a duplicate can no longer appear or be silently dropped.
25
+ - **Help.** `/help` is grouped and short — 25 lines in the terminal instead of 133 —
26
+ ordered by what a new user needs first. `help all` lists everything; `help <command>`
27
+ answers about that one command instead of dumping the whole list. The CLI, the classic
28
+ REPL and the interactive shell now call the same renderer, so they cannot drift apart.
29
+ - **Removed.** `journal` reported success for runs that did not exist and read the wrong
30
+ folder; `career-graph`'s read commands silently created project state; `plugins` was a
31
+ second name for `plugin`. All three now stop with the exact replacement command instead
32
+ of leaking into a paid model turn.
33
+ - **Failures.** A failed staffing run used to print a machine code followed by a raw JSON
34
+ object, with the one useful sentence — run `agentlas login` — buried inside it and cut
35
+ mid-escape by two stacked truncations. The sentence now comes first and the code last;
36
+ no JSON reaches a human. Sign-in expiry is relayed intact instead of being re-wrapped.
37
+ - **Shell layout.** Output now stays above the input box instead of below it,
38
+ the box shows what to type, the whole terminal width is used, blank lines survive,
39
+ and `/permission` `/model` `/runtime` `/effort` work where they were only autocompleted.
40
+
3
41
  ## 1.0.44 — 2026-08-11
4
42
 
5
43
  Turn the interactive shell on once and keep it.
@@ -2916,6 +2916,12 @@ const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reaso
2916
2916
  try {
2917
2917
  runtimeContext = await D.loadWorkforceGoalRuntime(cwd, ctx.goalId || null);
2918
2918
  } catch (error) {
2919
+ /*
2920
+ * 저자·호스트가 이미 한 문장으로 쓴 안내(로그인 필요, 정직 정지)는 그대로 올린다.
2921
+ * 여기서 다시 감싸면 code/honestStop 이 떨어져 나가고, 유일하게 실행 가능한
2922
+ * 문장이 JSON 의 cause 키 밑으로 들어가 사용자에게 blob 으로 보인다(실사고).
2923
+ */
2924
+ if (error && (error.honestStop || error.code)) throw error;
2919
2925
  fail(
2920
2926
  "workforce_goal_runtime_unavailable",
2921
2927
  "the active account/project Workforce binding could not be inspected",
@@ -3291,6 +3297,12 @@ const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reaso
3291
3297
  hostRuntime: identity.runtimeId,
3292
3298
  });
3293
3299
  } catch (error) {
3300
+ /*
3301
+ * 저자·호스트가 이미 한 문장으로 쓴 안내(로그인 필요, 정직 정지)는 그대로 올린다.
3302
+ * 여기서 다시 감싸면 code/honestStop 이 떨어져 나가고, 유일하게 실행 가능한
3303
+ * 문장이 JSON 의 cause 키 밑으로 들어가 사용자에게 blob 으로 보인다(실사고).
3304
+ */
3305
+ if (error && (error.honestStop || error.code)) throw error;
3294
3306
  fail(
3295
3307
  "workforce_goal_binding_failed",
3296
3308
  "prepared execution was blocked because the durable goal binding could not be committed",
@@ -4552,14 +4564,22 @@ const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reaso
4552
4564
  const otherDetails = details && typeof details === "object" && !Array.isArray(details)
4553
4565
  ? Object.fromEntries(Object.entries(details).filter(([key]) => key !== "issues"))
4554
4566
  : details;
4555
- const detailText = otherDetails && (typeof otherDetails !== "object" || Object.keys(otherDetails).length)
4556
- ? ` — ${JSON.stringify(otherDetails).slice(0, 1_200)}`
4557
- : "";
4558
- ui.error(`${receipt.failure.code}: ${receipt.failure.message}${detailText}`, { reveal: true });
4567
+ // otherDetails 사람 줄에 쓰지 않는다 영수증에 그대로 남는다.
4568
+ void otherDetails;
4569
+ /*
4570
+ * 사람이 읽는 줄에 JSON 을 찍지 않는다(2026-08-11 오너 지적). 예전에는
4571
+ * `code: message — {"cause":"…"}` 형태로 나가서, 유일하게 실행 가능한 문장
4572
+ * ("agentlas login")이 JSON 안에 묻히고 두 번의 절단(1200자→800자)에 걸려
4573
+ * 이스케이프 중간에서 잘렸다. 지금은 문장이 먼저, 기계 코드는 맨 끝 한 줄.
4574
+ */
4575
+ const failureError = receipt.failure.error;
4576
+ if (failureError && (failureError.honestStop || failureError.code)) ui.error(failureError);
4577
+ else ui.error(String(receipt.failure.message || ""), { reveal: true });
4559
4578
  if (issues) {
4560
- for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`, { reveal: true });
4561
- if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`, { reveal: true });
4579
+ for (const issue of issues.slice(0, 16)) ui.line(ui.c.dim(` · ${String(issue).slice(0, 400)}`));
4580
+ if (issues.length > 16) ui.line(ui.c.dim(` · … ${issues.length - 16} more in the persisted receipt`));
4562
4581
  }
4582
+ ui.line(ui.c.faint(` ${receipt.failure.code}`));
4563
4583
  // 실패한 실행이야말로 토큰이 어디로 갔는지 알아야 하는 순간이다. issues 유무와
4564
4584
  // 무관하게 낸다 — 첫 배선이 이 블록 안에 들어가는 바람에 issues 없는 실패에서는
4565
4585
  // 장부가 통째로 사라졌다.
@@ -153,8 +153,8 @@ function listGraphs(ctx) {
153
153
  }
154
154
  ctx.out("");
155
155
  ctx.out(ctx.ui.dim(en
156
- ? "Run one with: agentlas graph run \"<name>\""
157
- : "실행하려면: agentlas graph run \"<이름>\""));
156
+ ? `Run one with: ${ctx.surface === "shell" ? "/graph" : "agentlas graph run \"<name>\""}`
157
+ : `실행하려면: ${ctx.surface === "shell" ? "/graph" : "agentlas graph run \"<이름>\""}`));
158
158
  return 0;
159
159
  }
160
160
 
@@ -278,12 +278,11 @@ function graphProblems(graph, en) {
278
278
  /** 노드 한 줄. 무엇인지, 바깥을 바꾸는지, 어떤 값을 만들고 쓰는지. */
279
279
  function nodeLine(ctx, node, en) {
280
280
  const effect = node.config?.effect;
281
- const approval = node.config?.approval;
281
+ // ★승인 게이트 폐지(오너 이사회 결정 2026-08-10) — 데스크탑 커널은 실행 중에 멈춰
282
+ // 묻지 않는다. "확인 후 실행" 표시는 거짓이므로 없앴다. 옛 그래프의 approval
283
+ // 선언이 남아 있어도 마찬가지다. 사실 그대로의 고지는 "바깥을 바꿈" 하나다.
282
284
  const marks = [
283
285
  effect === "mutation" ? (en ? "changes things outside" : "바깥을 바꿈") : null,
284
- approval === "ask" || (effect === "mutation" && approval !== "auto")
285
- ? (en ? "asks first" : "확인 후 실행")
286
- : null,
287
286
  node.config?.consumes ? `${en ? "uses" : "사용"} {{${node.config.consumes}}}` : null,
288
287
  node.config?.produces ? `${en ? "makes" : "생성"} {{${node.config.produces}}}` : null,
289
288
  ].filter(Boolean);
@@ -470,7 +469,9 @@ async function runGraph(ctx, needle, flags) {
470
469
  if (row?.ambiguous) { reportAmbiguous(ctx, needle, row.ambiguous, en); return 1; }
471
470
  if (!row) {
472
471
  ctx.err(en ? `No graph matches "${needle}".` : `"${needle}"와 맞는 그래프가 없습니다.`);
473
- ctx.err(en ? "See what is saved with: agentlas graph list" : "저장된 목록: agentlas graph list");
472
+ ctx.err(en
473
+ ? `See what is saved with: ${ctx.surface === "shell" ? "/graph" : "agentlas graph list"}`
474
+ : `저장된 목록: ${ctx.surface === "shell" ? "/graph" : "agentlas graph list"}`);
474
475
  return 1;
475
476
  }
476
477
  const graph = parseGraph(row);
@@ -746,7 +747,11 @@ async function newGraph(ctx, request, flags) {
746
747
  const mutations = built.graph.nodes.filter((n) => n.config && n.config.effect === "mutation");
747
748
  if (mutations.length) {
748
749
  ctx.out("");
749
- ctx.out(en ? "Steps that go outside (locked to ask first):" : "바깥으로 나가는 단계 (실행 전에 확인하도록 잠급니다):");
750
+ // ★사실 그대로의 고지 단계들은 실행 중에 멈춰 묻지 않는다(승인 게이트 폐지,
751
+ // 오너 이사회 결정 2026-08-10). "잠근다"는 거짓말 대신 무엇이 나가는지를 알린다.
752
+ ctx.out(en
753
+ ? "Steps that change things outside (they run without stopping to ask):"
754
+ : "바깥을 바꾸는 단계 (실행 중에 멈춰 묻지 않습니다):");
750
755
  for (const n of mutations) ctx.out(` · ${n.label}`);
751
756
  }
752
757
  ctx.out("");
@@ -1077,9 +1082,10 @@ function installPackage(ctx, filePath, flags = {}) {
1077
1082
  if (mutations.length) {
1078
1083
  ctx.out(en ? "It changes things outside at:" : "바깥을 바꾸는 지점:");
1079
1084
  for (const m of mutations) ctx.out(` · ${m.label}`);
1085
+ // ★승인 게이트 폐지(2026-08-10) — "멈추고 묻는다"는 거짓말이었다. 사실만 말한다.
1080
1086
  ctx.out(ctx.ui.dim(en
1081
- ? "Those steps stop and ask before they run unless you set them to automatic."
1082
- : "그 단계들은 자동 허용으로 바꾸지 않는 실행 전에 멈추고 묻습니다."));
1087
+ ? "Those steps do not stop to ask once this is switched on, they really change things outside."
1088
+ : "그 단계들은 실행 중에 멈춰 묻지 않습니다 켜면 실제로 바깥을 바꿉니다."));
1083
1089
  }
1084
1090
  ctx.out(ctx.ui.dim(en
1085
1091
  ? "Nothing runs until you switch it on. The desktop app can simulate it first — the terminal cannot."
@@ -1,164 +1,45 @@
1
1
  "use strict";
2
- /* help / usage — v2 명령 표면. 재구축이 진행되며 이 표가 곧 진실이다. */
3
-
4
- const HELP = `agentlas — the operating system for agents, in your terminal
5
-
6
- agentlas open the terminal (REPL)
7
- agentlas "<task>" run once with this project's controller
8
- graph new "<what you want run for you>" build an automation by talking it through
9
-
10
- PROJECT WORK
11
- run [agent] [prompt] project-first one-shot; exact agent is an explicit advanced override
12
- firm <firm> [task] delegate to a CEO (--runtime · --model · --effort)
13
-
14
- AGENTS & HUB
15
- search "<what you need>" discover agents in the Hub
16
- install <slug> install an agent from the Hub
17
- plugin add <slug> · plugin list Hub plugins (MCP servers)
18
- build "<request>" build an installable agent locally (auto-installs)
19
- upload <path> save owner-private in Agent Cloud (--visibility marketplace to publish)
20
- import <path> · cd · native prepare local folder agents
21
- list installed agents/companies + orchestrator/worker runtimes
22
- roles [set <role> <rt>] show or set orchestrator/worker model roles
23
- experience <sub> portable Experience: list|inspect|validate|save|publish|status|export|unpublish
24
- variant resolve --base-release <id> local variant selection (variant help)
25
-
26
- EXECUTE
27
- storm <goal> Goal+UltraCode harness: plan → allocate → execute → verify [--research]
28
- swarm <goal> emergent agent swarm [--parallel N]
29
- workforce | network <request> Agent Workforce Ontology route (public Hub menu)
30
- hep-network "<request>" staff across Local + owner Cloud + public Hub (local Core federation)
31
- hep-local | hep-cloud | hep-hub "<request>" same, restricted to one source scope
32
- call "a,b" "<ctx>" · browser <url|status|sites|login> · route "<req>" [--json] · research <sub>
33
-
34
- KNOWLEDGE
35
- memory import · evolve memory & prompt-evolution proposals
36
- ontology · career-graph project knowledge & source routing
37
- journal <sub> Stormbreaker run journal
38
- project <sub> connect this folder + set an ordered team, standalone
39
- (status · init · use <agent> · team <agent>…)
40
- context <sub> dependency map: refresh|locate|refs|slice|impact|verify
41
-
42
- ACCOUNT & OPS
43
- login | logout | whoami Agentlas Cloud sign-in (browser flow)
44
- cloud <sub> cloud assets: save|publish|package|list|restore|field-test
45
- automation <sub> list|add|on|off|remove|run <id>|runs|daemon
46
- graph <sub> new "<what you want>"|list|show|run <name>|export|inspect|install
47
- creds <sub> · env credentials and shared env keys
48
- usage · telegram · mcp local usage · telegram bindings · MCP servers (mcp probe <id>)
49
- multimodal image/video/audio provider settings
50
- doctor · setup · update health check · first-run wizard · npm update check
51
- oberon | film <sub> AI film render (scaffold|render|list|open)
52
- hep <sub…> · netadmin Hephaestus passthrough · local agent network
53
- version · help
54
-
55
- IN-REPL (agentlas → interactive, Orca multi-session)
56
- /sessions · /tree · /s <n> | /switch <n> · /kill <n> · /rm <n>
57
- /runtime <kind> · /model <id> · /effort <level> · /permission <level> (applies to new sessions)
58
- every command above also works as a slash command (/graph, /search, /automation, …) — /help lists them all
59
- typing during a running turn queues steering; ctrl-c interrupts the turn
60
-
61
- Options: -p|--print · --runtime claude-code|codex|gemini · --model <exact-id> ·
62
- --effort none|minimal|low|medium|high|xhigh|max ·
63
- --tier economy|balanced|frontier (requires --model) ·
64
- --permission read|write|full
65
- `;
66
-
67
2
  /*
68
- * 한국어판 — 2026-08-05 감사 결함 F: ko 세션에서 /help 본문이 전부 영어였다.
69
- * 명령 이름·플래그는 원문(입력 어휘) 유지, 설명만 국문. HELP(영문)와 줄 구조를
70
- * 맞춰 둔다 runForCommand는 모두에서 같은 규칙으로 행을 찾는다.
3
+ * commands/help정본은 ui/commands-catalog.cjs 하나다(2026-08-11 재작성).
4
+ *
5
+ * 파일에는 EN 61줄 + KO 133줄의 손유지 블록 벌이 있었다. 팔레트와 어긋나
6
+ * 같은 명령이 표면마다 다른 인자·설명을 광고했고(`search "<what you need>"` vs
7
+ * `"<필요한 것>"`), `agentlas help doctor` 는 인자를 무시하고 전체를 쏟아냈다.
8
+ * 이제 CLI·기본 REPL·새 셸이 같은 renderHelp 를 부른다 — 다시 갈라질 수 없다.
71
9
  */
72
- const HELP_KO = `agentlas — 터미널 속 에이전트 운영체제
73
-
74
- agentlas 터미널(REPL) 열기
75
- agentlas "<작업>" 이 프로젝트의 컨트롤러로 1회 실행
76
- graph new "<대신 시킬 일>" 대화로 설명하면 자동화를 만들어 줍니다
77
-
78
- PROJECT WORK
79
- run [agent] [prompt] 프로젝트 우선 1회 실행; 특정 에이전트 지정은 명시적 고급 경로
80
- firm <firm> [task] 회사 CEO에게 위임 (--runtime · --model · --effort)
81
-
82
- AGENTS & HUB
83
- search "<필요한 것>" Hub에서 에이전트 찾기
84
- install <slug> Hub 에이전트 설치
85
- plugin add <slug> · plugin list Hub 플러그인 (MCP 서버)
86
- build "<요청>" 에이전트를 로컬에서 만들고 바로 설치
87
- upload <path> Agent Cloud에 소유자 비공개 저장 (--visibility marketplace 로 발행)
88
- import <path> · cd · native prepare 로컬 폴더 에이전트
89
- list 설치 에이전트/회사 + 오케스트레이터·워커 런타임
90
- roles [set <role> <rt>] 오케스트레이터·워커 모델 역할 조회/설정
91
- experience <sub> 이동식 Experience: list|inspect|validate|save|publish|status|export|unpublish
92
- variant resolve --base-release <id> 로컬 변형 선택 (variant help)
93
-
94
- EXECUTE
95
- storm <goal> Goal+UltraCode 하니스: 계획 → 배정 → 실행 → 검증 [--research]
96
- swarm <goal> 창발형 에이전트 스웜 [--parallel N]
97
- workforce | network <request> Agent Workforce Ontology 편성 (공개 Hub 메뉴)
98
- hep-network "<request>" 로컬+오너 클라우드+공개 Hub 연합 편성 (로컬 Core 연합)
99
- hep-local | hep-cloud | hep-hub "<request>" 같은 편성, 한 소스 스코프로 제한
100
- call "a,b" "<ctx>" · browser <url|status|sites|login> · route "<req>" [--json] · research <sub>
101
-
102
- KNOWLEDGE
103
- memory import · evolve 메모리·프롬프트 진화 제안
104
- ontology · career-graph 프로젝트 지식·소스 라우팅
105
- journal <sub> Stormbreaker 런 저널
106
- project <sub> 이 폴더를 프로젝트로 연결 + 순서 팀 편성 (독립)
107
- (status · init · use <에이전트> · team <에이전트>…)
108
- context <sub> 의존성 지도: refresh|locate|refs|slice|impact|verify
109
-
110
- ACCOUNT & OPS
111
- login | logout | whoami Agentlas Cloud 로그인 (브라우저 플로)
112
- cloud <sub> 클라우드 자산: save|publish|package|list|restore|field-test
113
- automation <sub> list|add|on|off|remove|run <id>|runs|daemon
114
- graph <sub> new "<시킬 일>"|list|show|run <이름>|export|inspect|install
115
- creds <sub> · env 자격증명·공유 env 키 (creds list 로 확인)
116
- usage · telegram · mcp 로컬 사용량 · 텔레그램 연결 · MCP 서버 (mcp probe <id>)
117
- multimodal 이미지/영상/음성 제공자 설정
118
- doctor · setup · update 건강 점검 · 첫 실행 마법사 · npm 업데이트 확인
119
- oberon | film <sub> AI 필름 렌더 (scaffold|render|list|open)
120
- hep <sub…> · netadmin Hephaestus 패스스루 · 로컬 에이전트 네트워크
121
- version · help
122
-
123
- IN-REPL (agentlas → 대화형, Orca 다중 세션)
124
- /sessions · /tree · /s <n> | /switch <n> · /kill <n> · /rm <n>
125
- /runtime <kind> · /model <id> · /effort <level> · /permission <level> (새 세션부터 적용)
126
- 위의 모든 명령은 슬래시 명령(/graph, /search, /automation, …)으로도 됩니다 — 전체 목록은 /help
127
- 실행 중 입력하면 조종 큐에 쌓이고, ctrl-c 로 턴을 중단합니다
128
-
129
- Options: -p|--print · --runtime claude-code|codex|gemini · --model <exact-id> ·
130
- --effort none|minimal|low|medium|high|xhigh|max ·
131
- --tier economy|balanced|frontier (--model 필요) ·
132
- --permission read|write|full
133
- `;
134
-
135
- function helpText(ctx) {
136
- return ctx && ctx.lang === "ko" ? HELP_KO : HELP;
137
- }
138
-
139
- function run(ctx) {
140
- ctx.out(helpText(ctx).trimEnd());
10
+ const catalog = require("../ui/commands-catalog.cjs");
11
+
12
+ function run(ctx, args = []) {
13
+ const first = String((args && args[0]) || "").trim();
14
+ const all = first.toLowerCase() === "all";
15
+ const name = !all && first && !first.startsWith("-") ? first : null;
16
+ if (name) return runForCommand(ctx, name);
17
+ ctx.out(catalog.renderHelp({ lang: ctx.lang, surface: "cli", all }));
141
18
  return 0;
142
19
  }
143
20
 
144
21
  function runForCommand(ctx, command) {
145
- const name = String(command || "").trim();
146
- const rows = helpText(ctx).split("\n")
147
- .map((line) => line.trim())
148
- .filter((line) => line && !line.endsWith(":"))
149
- .filter((line) => {
150
- const commandColumn = line.split(/\s{2,}/, 1)[0];
151
- return commandColumn
152
- .split(/\s*·\s*|\s*\|\s*/)
153
- .some((entry) => entry === name || entry.startsWith(`${name} `));
154
- });
155
- ctx.out(`Usage: agentlas ${name} [options]`);
156
- if (rows.length > 0) {
157
- for (const row of rows) ctx.out(` ${row}`);
158
- } else {
159
- ctx.out(` See "agentlas help" for the full command list.`);
22
+ const name = String(command || "").trim().replace(/^\//, "");
23
+ const entry = catalog.byName(name);
24
+ const ko = ctx && ctx.lang === "ko";
25
+ if (!entry) {
26
+ // 없는 이름을 지어내지 않는다 — 전체 목록으로 보낸다.
27
+ ctx.out(`Usage: agentlas ${name}`);
28
+ ctx.out(ko ? ' 전체 목록: agentlas help all' : ' Full list: agentlas help all');
29
+ return 0;
30
+ }
31
+ const replOnly = !entry.surfaces.includes("cli");
32
+ ctx.out(`Usage: agentlas ${catalog.usageFor(entry, ctx.lang)}`);
33
+ ctx.out(` ${catalog.descFor(entry, ctx.lang)}`);
34
+ if (entry.aliases && entry.aliases.length) {
35
+ ctx.out(ko ? ` 다른 이름: ${entry.aliases.join(", ")}` : ` Also: ${entry.aliases.join(", ")}`);
36
+ }
37
+ if (replOnly) {
38
+ ctx.out(ko
39
+ ? ` 이 명령은 터미널 안에서 씁니다: agentlas 를 실행한 뒤 /${entry.name}`
40
+ : ` This one runs inside the terminal: start agentlas, then /${entry.name}`);
160
41
  }
161
42
  return 0;
162
43
  }
163
44
 
164
- module.exports = { run, runForCommand, HELP, HELP_KO };
45
+ module.exports = { run, runForCommand };
@@ -10,8 +10,9 @@ const path = require("node:path");
10
10
 
11
11
  // 각 명령 파일은 { run(ctx, args) } 를 export한다. ctx는 엔진이 만든 얕은 DI 객체.
12
12
  const COMMANDS = {
13
+ // 정본 이름은 agents. list 는 별칭으로 영구 호출 가능(스크립트·게이트 사용).
14
+ agents: () => require("./list.cjs"),
13
15
  version: () => require("./version.cjs"),
14
- list: () => require("./list.cjs"),
15
16
  graph: () => require("./graph.cjs"),
16
17
  doctor: () => require("./doctor.cjs"),
17
18
  mcp: () => require("./mcp.cjs"),
@@ -31,14 +32,11 @@ const COMMANDS = {
31
32
  cd: () => require("./cd.cjs"),
32
33
  install: () => require("./install.cjs"),
33
34
  plugin: () => require("./plugin.cjs"),
34
- plugins: () => require("./plugin.cjs"),
35
35
  automation: () => require("./automation.cjs"),
36
36
  native: () => require("./native.cjs"),
37
37
  multimodal: () => require("./multimodal.cjs"),
38
38
  document: () => require("./document.cjs"),
39
39
  workforce: () => require("./workforce.cjs"),
40
- network: () => require("./workforce.cjs"),
41
- taskforce: () => require("./workforce.cjs"),
42
40
  // 소스 스코프 편성 4종 — 2026-08-05 네이티브 배선(경위는 workforce.cjs 참조).
43
41
  // 별칭이 아니라 1급인 이유는 여전하다: 별칭은 스코프를 전달하지 못한다.
44
42
  "hep-network": () => require("./hep-network.cjs"),
@@ -46,7 +44,6 @@ const COMMANDS = {
46
44
  "hep-cloud": () => require("./hep-cloud.cjs"),
47
45
  "hep-hub": () => require("./hep-hub.cjs"),
48
46
  oberon: () => require("./oberon.cjs"),
49
- film: () => require("./film.cjs"),
50
47
  experience: () => require("./experience.cjs"),
51
48
  memory: () => require("./memory.cjs"),
52
49
  evolve: () => require("./evolve.cjs"),
@@ -60,7 +57,6 @@ const COMMANDS = {
60
57
  route: () => require("./route.cjs"),
61
58
  research: () => require("./research.cjs"),
62
59
  netadmin: () => require("./netadmin.cjs"),
63
- journal: () => require("./journal.cjs"),
64
60
  cloud: () => require("./cloud.cjs"),
65
61
  upload: () => require("./upload.cjs"),
66
62
  storm: () => require("./storm.cjs"),
@@ -68,7 +64,6 @@ const COMMANDS = {
68
64
  project: () => require("./project.cjs"),
69
65
  context: () => require("./context.cjs"),
70
66
  ontology: () => require("./ontology.cjs"),
71
- "career-graph": () => require("./career-graph.cjs"),
72
67
  creds: () => require("./creds.cjs"),
73
68
  billing: () => require("./billing.cjs"),
74
69
  uninstall: () => require("./uninstall.cjs"),
@@ -139,6 +134,30 @@ const COMMAND_ALIASES = {
139
134
  "hep-storm": "storm",
140
135
  "hep-browser": "browser",
141
136
  "hep-connect": "connect",
137
+ // 2026-08-11: 같은 기능을 두 이름으로 광고하던 것을 별칭으로 접었다.
138
+ list: "agents",
139
+ network: "workforce",
140
+ taskforce: "workforce",
141
+ film: "oberon",
142
+ };
143
+
144
+ /*
145
+ * 제거된 명령 — 이름을 그냥 없애면 인자와 함께 프롬프트로 새어 유료 턴이 된다
146
+ * (아래 marketplace 사고 주석과 같은 계열). 착지 안내를 두고 arity 무관하게 잡는다.
147
+ */
148
+ const REMOVED_COMMANDS = {
149
+ journal: {
150
+ en: "`journal` was removed — it reported \"ok\" for runs that do not exist and read the wrong folder.\nExperts: agentlas hep stormbreaker journal --run-id <id> --journal <path>",
151
+ ko: "`journal` 은 제거됐습니다 — 없는 실행에도 \"ok\" 를 답했고 다른 폴더를 봤습니다.\n전문가용: agentlas hep stormbreaker journal --run-id <id> --journal <path>",
152
+ },
153
+ "career-graph": {
154
+ en: "`career-graph` was removed — its read commands silently created project state.\nSources: agentlas ontology · Index: hephaestus career-graph ingest --project .",
155
+ ko: "`career-graph` 는 제거됐습니다 — 조회 명령이 말없이 프로젝트 상태를 만들었습니다.\n소스: agentlas ontology · 색인: hephaestus career-graph ingest --project .",
156
+ },
157
+ plugins: {
158
+ en: "Use: agentlas plugin list",
159
+ ko: "이렇게 쓰세요: agentlas plugin list",
160
+ },
142
161
  };
143
162
 
144
163
  function resolveCommandName(cmd) {
@@ -189,6 +208,10 @@ function dispatch(ctx, argv) {
189
208
  // 낙하 가드부터 만들 것: 상위 오타 가드는 한 단어 전용이라 인자가 붙은
190
209
  // 삭제 이름은 프롬프트로 흘러 에이전트를 기동한다(실측·토큰 소모).
191
210
 
211
+ if (REMOVED_COMMANDS[cmd]) {
212
+ ctx.err(REMOVED_COMMANDS[cmd][ctx.lang === "ko" ? "ko" : "en"]);
213
+ return 1;
214
+ }
192
215
  if (DESKTOP_ONLY_SURFACES[cmd]) {
193
216
  const asTask = [rawCmd, ...rest].join(" ");
194
217
  ctx.err(`${DESKTOP_ONLY_SURFACES[cmd]}\nIt was not run as a prompt — rerun with quotes if you meant a task: agentlas "${asTask}"`);
@@ -207,4 +230,4 @@ function dispatch(ctx, argv) {
207
230
  return undefined; // 알 수 없는 토큰 — 엔진이 에이전트 이름/프롬프트로 해석 시도
208
231
  }
209
232
 
210
- module.exports = { dispatch, COMMANDS, COMMAND_ALIASES, resolveCommandName, SELF_HELP_COMMANDS, NOT_YET_PORTED, GUARDED_NO_ARG, DESKTOP_ONLY_SURFACES };
233
+ module.exports = { dispatch, COMMANDS, COMMAND_ALIASES, resolveCommandName, SELF_HELP_COMMANDS, NOT_YET_PORTED, GUARDED_NO_ARG, DESKTOP_ONLY_SURFACES, REMOVED_COMMANDS };
@@ -48,7 +48,12 @@ async function run(ctx, args) {
48
48
  ctx.out(`${ctx.ui.accent(String(slug).padEnd(34).slice(0, 34))} ${String(name).slice(0, 26).padEnd(27)} ${ctx.ui.dim(String(kind).padEnd(14))} ${String(tagline).slice(0, 60)}`);
49
49
  }
50
50
  ctx.out("");
51
- ctx.out(ctx.ui.dim("Install: agentlas install <slug>"));
51
+ /*
52
+ * 안내는 사용자가 서 있는 표면에서 실제로 칠 수 있는 형태여야 한다. 셸 안에서
53
+ * "agentlas install"을 안내하면 그대로 따라 쳤을 때 "여기서는 안 됩니다"가 나온다
54
+ * (오너 실측: /agentlas install → not available here yet).
55
+ */
56
+ ctx.out(ctx.ui.dim(ctx.surface === "shell" ? "Install: /install <slug>" : "Install: agentlas install <slug>"));
52
57
  return 0;
53
58
  }
54
59
 
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /*
3
+ * ui/commands-catalog — 명령 정본 한 벌 (2026-08-11 전면 재작성).
4
+ *
5
+ * 배경: 같은 목록이 네 곳에 손으로 유지되고 있었다 — palette.SLASH_COMMANDS(71행),
6
+ * help.HELP(EN 61줄), help.HELP_KO(133줄), shell.toSlashCommands(파생). 넷이 서로
7
+ * 어긋난 것이 결함 대부분의 기계적 원인이었다:
8
+ * · 영문 화면에 한글 인자 힌트(`/graph [run <이름>]`)
9
+ * · 같은 기능이 서로 다른 설명으로 두 줄(`search "<what you need>"` vs `"<필요한 것>"`)
10
+ * · renderPalette 가 설명 텍스트로 중복 제거 → `/switch` `/list` `/exit` 가 조용히 사라짐
11
+ * · /help 가 표면마다 61 / 133 / 67줄
12
+ *
13
+ * 그래서 정본을 하나로 합치고, 별칭은 **행이 아니라 필드**로 둔다. 별칭이 행이 아니면
14
+ * 중복 설명 자체가 생기지 않으므로 위 dedup 이 필요 없어지고, 숨김 사고가 구조적으로 막힌다.
15
+ *
16
+ * 불변식(게이트 test/command-surface-contract.cjs 가 잠근다):
17
+ * - COMMANDS 키 ↔ 카탈로그 행 1:1. 예외는 surfaces:["repl"] (셸 switch 가 직접 처리).
18
+ * - args 는 ASCII 정본. 한국어는 argsKo 로만 — 언어 혼재 금지.
19
+ * - tier "core" 만 기본 /help 에 나온다. 나머지는 /help all. 단 Tab 완성은 전부 된다
20
+ * (완성에서 숨기는 것이 바로 `/switch` 결함이었다).
21
+ */
22
+
23
+ const { visWidth } = require("./width.cjs");
24
+
25
+ const GROUPS = [
26
+ { key: "start", ko: "시작", en: "Start here" },
27
+ { key: "work", ko: "일 시키기", en: "Do work" },
28
+ { key: "agents", ko: "에이전트", en: "Agents" },
29
+ { key: "automate", ko: "자동화", en: "Automate" },
30
+ { key: "session", ko: "세션 (셸 안에서만)", en: "Sessions (in-shell only)" },
31
+ { key: "settings", ko: "설정 (셸 안에서만)", en: "Settings (in-shell only)" },
32
+ { key: "account", ko: "계정·자산", en: "Account & assets" },
33
+ { key: "knowledge", ko: "프로젝트 지식", en: "Project knowledge" },
34
+ { key: "advanced", ko: "고급", en: "Advanced" },
35
+ ];
36
+
37
+ const CLI = ["cli"];
38
+ const BOTH = ["cli", "repl"];
39
+ const REPL = ["repl"];
40
+
41
+ /* eslint-disable max-len */
42
+ const CATALOG = [
43
+ // ── 1 start ───────────────────────────────────────────────────────────────
44
+ { name: "setup", group: "start", tier: "core", surfaces: CLI, args: "", ko: "첫 실행 마법사 — 언어·런타임·권한", en: "First-run wizard — language, runtime, permission" },
45
+ { name: "doctor", group: "start", tier: "core", surfaces: BOTH, args: "[--json]", ko: "설치 상태 점검", en: "Check this installation" },
46
+ { name: "login", group: "start", tier: "core", surfaces: BOTH, args: "[--force]", ko: "Agentlas 로그인 (--force 로 계정 전환)", en: "Sign in to Agentlas (--force switches account)" },
47
+ { name: "help", group: "start", tier: "core", surfaces: BOTH, args: "[all|<command>]", argsKo: "[all|<명령>]", ko: "명령 보기 (all = 전체 목록)", en: "Show commands (all = the full list)" },
48
+
49
+ // ── 2 work ────────────────────────────────────────────────────────────────
50
+ { name: "run", group: "work", tier: "core", surfaces: CLI, args: '[agent] "<task>"', argsKo: '[에이전트] "<작업>"', ko: "이 프로젝트 컨트롤러로 1회 실행", en: "Run once with this project's controller" },
51
+ { name: "project", group: "work", tier: "core", surfaces: BOTH, args: "[status|use <agent>]", argsKo: "[status|use <에이전트>]", ko: "이 폴더를 프로젝트로 연결", en: "Connect this folder to a project" },
52
+ { name: "storm", group: "work", tier: "core", surfaces: BOTH, args: '"<goal>"', argsKo: '"<목표>"', ko: "목표 하나를 계획→실행→검증까지", en: "Drive one goal: plan, execute, verify" },
53
+ { name: "workforce", group: "work", tier: "core", surfaces: BOTH, aliases: ["network", "taskforce"], args: '"<request>"', argsKo: '"<요청>"', ko: "여러 에이전트를 편성해 실행 (공개 Hub)", en: "Staff several agents and run (public Hub)" },
54
+ { name: "call", group: "work", tier: "core", surfaces: BOTH, aliases: ["hep-call"], args: '"a,b" "<context>"', argsKo: '"a,b" "<맥락>"', ko: "이름을 아는 에이전트를 직접 호출", en: "Call agents you name" },
55
+
56
+ // ── 3 agents ──────────────────────────────────────────────────────────────
57
+ { name: "agents", group: "agents", tier: "core", surfaces: BOTH, aliases: ["list"], args: "[--json]", ko: "설치된 에이전트·팀", en: "Installed agents and teams" },
58
+ { name: "search", group: "agents", tier: "core", surfaces: BOTH, aliases: ["hep-search"], args: '"<what you need>"', argsKo: '"<필요한 것>"', ko: "Hub에서 에이전트 찾기", en: "Find agents in the Hub" },
59
+ { name: "install", group: "agents", tier: "core", surfaces: BOTH, args: "<slug>", ko: "Hub 에이전트 설치", en: "Install an agent from the Hub" },
60
+ { name: "build", group: "agents", tier: "core", surfaces: BOTH, aliases: ["hep-build"], args: '"<the agent you want>"', argsKo: '"<원하는 에이전트>"', ko: "에이전트를 여기서 만들어 바로 설치 (쓰기 권한)", en: "Build an agent here and install it (runs with write permission)" },
61
+ { name: "roles", group: "agents", tier: "core", surfaces: BOTH, args: "[set <role> <runtime>]", argsKo: "[set <역할> <런타임>]", ko: "오케스트레이터·워커 모델 지정", en: "Set the orchestrator and worker models" },
62
+
63
+ // ── 4 automate ────────────────────────────────────────────────────────────
64
+ { name: "automation", group: "automate", tier: "core", surfaces: BOTH, args: "[list|add|on|off|run]", ko: "예약 실행", en: "Scheduled runs" },
65
+ { name: "graph", group: "automate", tier: "core", surfaces: BOTH, args: "[list|show|run <name>]", argsKo: "[list|show|run <이름>]", ko: "저장된 자동화 그래프", en: "Saved automation graphs" },
66
+
67
+ // ── 5 session (셸 전용) ───────────────────────────────────────────────────
68
+ { name: "sessions", group: "session", tier: "core", surfaces: REPL, aliases: ["tree"], args: "", ko: "지금 도는 세션 목록", en: "Sessions running now" },
69
+ { name: "s", group: "session", tier: "core", surfaces: REPL, aliases: ["switch"], args: "<n>", ko: "그 세션으로 전환", en: "Switch to that session" },
70
+ { name: "kill", group: "session", tier: "core", surfaces: REPL, args: "<n>", ko: "그 세션의 턴 중단", en: "Interrupt that session's turn" },
71
+ { name: "rm", group: "session", tier: "core", surfaces: REPL, args: "<n>", ko: "그 세션 닫기", en: "Close that session" },
72
+ { name: "quit", group: "session", tier: "core", surfaces: REPL, aliases: ["exit"], args: "", ko: "종료", en: "Quit" },
73
+
74
+ // ── 6 settings (셸 전용 · 전부 세션 한정, 영구 경로를 설명에 못박는다) ────
75
+ { name: "permission", group: "settings", tier: "core", surfaces: REPL, args: "read|write|full", ko: "이 셸의 새 세션 권한 (영구: agentlas setup)", en: "Permission for new sessions here (persist: agentlas setup)" },
76
+ { name: "model", group: "settings", tier: "core", surfaces: REPL, args: "<id|default>", ko: "이 셸의 새 세션 모델 (영구: agentlas roles set)", en: "Model for new sessions here (persist: agentlas roles set)" },
77
+ { name: "runtime", group: "settings", tier: "core", surfaces: REPL, args: "<kind>", ko: "이 셸의 새 세션 런타임 (영구: agentlas roles set)", en: "Runtime for new sessions here (persist: agentlas roles set)" },
78
+ { name: "effort", group: "settings", tier: "core", surfaces: REPL, args: "<level>", ko: "이 셸의 새 세션 추론 강도 (영구: agentlas roles set --effort)", en: "Reasoning effort for new sessions here (persist: agentlas roles set --effort)" },
79
+ { name: "shell", group: "settings", tier: "core", surfaces: REPL, args: "on|off", ko: "새 대화형 셸 켜기/끄기", en: "Turn the new interactive shell on or off" },
80
+
81
+ // ── 7 account ─────────────────────────────────────────────────────────────
82
+ { name: "whoami", group: "account", tier: "more", surfaces: BOTH, args: "", ko: "로그인 계정 확인", en: "Show the signed-in account" },
83
+ { name: "logout", group: "account", tier: "more", surfaces: BOTH, args: "", ko: "로그아웃", en: "Sign out" },
84
+ { name: "billing", group: "account", tier: "more", surfaces: BOTH, args: "", ko: "크레딧 잔액 (구독·대여 수익)", en: "Credit balances (subscription and rental earnings)" },
85
+ { name: "usage", group: "account", tier: "more", surfaces: BOTH, args: "", ko: "이 설치의 사용 현황", en: "Local usage on this install" },
86
+ { name: "cloud", group: "account", tier: "more", surfaces: BOTH, args: "<save|publish|list|...>", ko: "Agent Cloud 자산", en: "Agent Cloud assets" },
87
+ { name: "upload", group: "account", tier: "more", surfaces: BOTH, aliases: ["hep-upload"], args: "<path> [--visibility ...]", argsKo: "<경로> [--visibility ...]", ko: "기본은 비공개 저장, --visibility marketplace 로 공개 발행", en: "Owner-private by default; --visibility marketplace publishes" },
88
+ { name: "uninstall", group: "account", tier: "more", surfaces: BOTH, args: "<slug> [--yes]", ko: "에이전트 삭제 (대화 기록도 함께 지워짐)", en: "Delete an agent (its chats are deleted too)" },
89
+ { name: "update", group: "account", tier: "more", surfaces: BOTH, args: "[--json]", ko: "npm 업데이트 확인", en: "Check for an npm update" },
90
+ { name: "version", group: "account", tier: "more", surfaces: BOTH, args: "", ko: "버전", en: "Version" },
91
+
92
+ // ── 8 knowledge ───────────────────────────────────────────────────────────
93
+ { name: "ontology", group: "knowledge", tier: "more", surfaces: BOTH, args: "[status|list|add <path>]", argsKo: "[status|list|add <경로>]", ko: "이 프로젝트가 읽을 지식 소스 등록", en: "Register the knowledge sources this project may read" },
94
+ { name: "context", group: "knowledge", tier: "more", surfaces: BOTH, args: "<locate|slice|impact|...>", ko: "코드 의존성 맵 (Agentlas OS Core 필요)", en: "Code dependency map (requires Agentlas OS Core)" },
95
+ { name: "memory", group: "knowledge", tier: "more", surfaces: BOTH, args: "<sub>", ko: "메모리", en: "Memory" },
96
+ { name: "experience", group: "knowledge", tier: "more", surfaces: BOTH, args: "<list|inspect|save|...>", ko: "이식 가능한 Experience", en: "Portable Experience" },
97
+ { name: "evolve", group: "knowledge", tier: "more", surfaces: BOTH, args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
98
+
99
+ // ── 9 advanced ────────────────────────────────────────────────────────────
100
+ { name: "route", group: "advanced", tier: "more", surfaces: BOTH, args: '"<request>"', argsKo: '"<요청>"', ko: "이 요청에 맞는 에이전트로 라우팅", en: "Route this request to the right agent" },
101
+ { name: "swarm", group: "advanced", tier: "more", surfaces: BOTH, args: '"<goal>" [--parallel N]', argsKo: '"<목표>" [--parallel N]', ko: "창발형 에이전트 스웜", en: "Emergent agent swarm" },
102
+ { name: "hep-network", group: "advanced", tier: "more", surfaces: BOTH, aliases: ["legacy-network"], args: '"<request>"', argsKo: '"<요청>"', ko: "로컬+오너 클라우드+공개 Hub 연합 편성 (로컬 Core 필요)", en: "Staff across Local + owner Cloud + public Hub (needs local Core)" },
103
+ { name: "hep-local", group: "advanced", tier: "more", surfaces: BOTH, args: '"<request>"', argsKo: '"<요청>"', ko: "등록된 로컬 에이전트만으로 편성 (로컬 Core 필요)", en: "Staff from registered Local agents only (needs local Core)" },
104
+ { name: "hep-cloud", group: "advanced", tier: "more", surfaces: BOTH, args: '"<request>"', argsKo: '"<요청>"', ko: "오너 Agent Cloud만으로 편성 (로그인·로컬 Core 필요)", en: "Staff from owner Agent Cloud only (needs sign-in + local Core)" },
105
+ { name: "hep-hub", group: "advanced", tier: "more", surfaces: BOTH, args: '"<request>"', argsKo: '"<요청>"', ko: "공개 Hub 에이전트만으로 편성 (로컬 Core 필요)", en: "Staff from public Hub agents only (needs local Core)" },
106
+ { name: "firm", group: "advanced", tier: "more", surfaces: CLI, args: "<firm> [task]", argsKo: "<회사> [작업]", ko: "회사 CEO에게 위임", en: "Delegate to a company CEO" },
107
+ { name: "import", group: "advanced", tier: "more", surfaces: BOTH, args: "<path>", argsKo: "<경로>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
108
+ { name: "cd", group: "advanced", tier: "more", surfaces: BOTH, args: "<agent>", argsKo: "<에이전트>", ko: "그 에이전트의 폴더 경로를 출력", en: "Print that agent's folder path" },
109
+ { name: "native", group: "advanced", tier: "more", surfaces: BOTH, args: "prepare <agent>", argsKo: "prepare <에이전트>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
110
+ { name: "mcp", group: "advanced", tier: "more", surfaces: BOTH, args: "[list|probe <id>]", ko: "MCP 서버", en: "MCP servers" },
111
+ { name: "plugin", group: "advanced", tier: "more", surfaces: BOTH, args: "<add <slug>|list|remove>", ko: "Hub 플러그인 (MCP 서버)", en: "Hub plugins (MCP servers)" },
112
+ { name: "creds", group: "advanced", tier: "more", surfaces: BOTH, args: "<list|save|file>", ko: "API 키 보관 (값은 절대 표시 안 함)", en: "API keys (values are never printed)" },
113
+ { name: "env", group: "advanced", tier: "more", surfaces: BOTH, args: "", ko: "공유 환경 변수 이름", en: "Shared env key names" },
114
+ { name: "multimodal", group: "advanced", tier: "more", surfaces: BOTH, args: "[set <kind> <provider>]", ko: "이미지·영상·음성 제공자", en: "Image, video and audio providers" },
115
+ { name: "telegram", group: "advanced", tier: "more", surfaces: BOTH, args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
116
+ { name: "connect", group: "advanced", tier: "more", surfaces: BOTH, aliases: ["hep-connect"], args: "<target>", argsKo: "<대상>", ko: "에이전트·팀 연결", en: "Connect an agent or team" },
117
+ { name: "browser", group: "advanced", tier: "more", surfaces: BOTH, aliases: ["hep-browser"], args: "<url|status|sites|login>", ko: "브라우저 하드포인트", en: "Browser hardpoint" },
118
+ { name: "research", group: "advanced", tier: "more", surfaces: BOTH, args: "<sub>", ko: "리서치", en: "Research" },
119
+ { name: "document", group: "advanced", tier: "more", surfaces: BOTH, args: "pdf <html|url>", ko: "문서 PDF 내보내기", en: "Export a document to PDF" },
120
+ { name: "oberon", group: "advanced", tier: "more", surfaces: BOTH, aliases: ["film"], args: "<scaffold|render|list|open>", ko: "AI 필름 렌더", en: "AI film render" },
121
+ { name: "variant", group: "advanced", tier: "more", surfaces: BOTH, args: "resolve --base-release", ko: "로컬 변형 선택", en: "Local variant selection" },
122
+ { name: "netadmin", group: "advanced", tier: "more", surfaces: BOTH, args: "<init|status|reindex|...>", ko: "로컬 에이전트 네트워크 관리", en: "Local agent network administration" },
123
+ { name: "hep", group: "advanced", tier: "more", surfaces: BOTH, aliases: ["hep-storm"], args: "<sub...>", ko: "Hephaestus 패스스루 (전문가용)", en: "Hephaestus passthrough (expert)" },
124
+ ];
125
+ /* eslint-enable max-len */
126
+
127
+ const BY_NAME = new Map();
128
+ const ALIAS_TO_NAME = new Map();
129
+ for (const entry of CATALOG) {
130
+ BY_NAME.set(entry.name, entry);
131
+ for (const alias of entry.aliases || []) ALIAS_TO_NAME.set(alias, entry.name);
132
+ }
133
+
134
+ function byName(name) {
135
+ const key = String(name || "").replace(/^\//, "");
136
+ return BY_NAME.get(key) || BY_NAME.get(ALIAS_TO_NAME.get(key)) || null;
137
+ }
138
+ function aliasMap() { return Object.fromEntries(ALIAS_TO_NAME); }
139
+ function argsFor(entry, lang) { return (lang === "ko" && entry.argsKo) || entry.args || ""; }
140
+ function descFor(entry, lang) { return lang === "ko" ? entry.ko : entry.en; }
141
+ function usageFor(entry, lang) {
142
+ const args = argsFor(entry, lang);
143
+ return entry.name + (args ? " " + args : "");
144
+ }
145
+ function forSurface(surface) { return CATALOG.filter((e) => e.surfaces.includes(surface)); }
146
+
147
+ const padVis = (text, width) => text + " ".repeat(Math.max(0, width - visWidth(text)));
148
+
149
+ /*
150
+ * 정렬은 visWidth 로 잰다 — String.length 로 재면 한글 한 글자를 1칸으로 세어
151
+ * 설명 열이 어긋난다(현행 palette.cjs 의 결함).
152
+ */
153
+ function renderHelp(options = {}) {
154
+ const lang = options.lang === "ko" ? "ko" : "en";
155
+ const surface = options.surface === "repl" ? "repl" : "cli";
156
+ const all = options.all === true;
157
+ const ko = lang === "ko";
158
+ const rows = forSurface(surface).filter((e) => all || e.tier === "core");
159
+ if (!rows.length) return "";
160
+ /*
161
+ * 머리말은 명령표가 아니라 "이게 무엇이고 어떻게 시작하나"다. 예전 HELP_KO 가
162
+ * 들고 있던 제품 한 줄(터미널 속 에이전트 운영체제)을 여기로 옮겼다 —
163
+ * 그 문장이 사라지면 첫 화면이 명령 나열로만 시작한다.
164
+ */
165
+ const head = [];
166
+ {
167
+ let version = "";
168
+ try { version = require("../agentlas-banner.cjs").readVersion(); } catch { version = ""; }
169
+ head.push(`agentlas${version ? " " + version : ""} — ${ko ? "터미널 속 에이전트 운영체제" : "the agent operating system in your terminal"}`);
170
+ head.push("");
171
+ if (surface === "cli") {
172
+ head.push(ko ? ' agentlas 셸 열기' : ' agentlas open the shell');
173
+ head.push(ko ? ' agentlas "<하고 싶은 일>" 한 번 실행' : ' agentlas "<what you want>" run once');
174
+ }
175
+ head.push(ko
176
+ ? " 셸 안에서는 그냥 문장을 치면 실행됩니다 · / 를 누르면 명령이 뜹니다"
177
+ : " In the shell, plain words run a task · press / for commands");
178
+ }
179
+ // 셸에서 셸 전용 명령은 슬래시를 붙여 보여준다 — 그게 실제로 치는 문자열이다.
180
+ const label = (e) => (surface === "repl" && e.surfaces.length === 1 ? "/" : "") + usageFor(e, lang);
181
+ /*
182
+ * 라벨 열에 상한을 둔다. 상한이 없으면 가장 긴 한 줄(automation 의 서브명령 나열)이
183
+ * 전체 열을 밀어 80칸 터미널에서 설명이 통째로 줄바꿈되고, 이어지는 줄은 들여쓰기를
184
+ * 잃는다. 상한을 넘는 소수 행만 자기 열을 넘어가고 나머지는 정렬을 지킨다.
185
+ */
186
+ const LABEL_CAP = 34;
187
+ const width = Math.min(LABEL_CAP, Math.max(...rows.map((e) => visWidth(label(e)))));
188
+ const out = head.slice();
189
+ for (const group of GROUPS) {
190
+ const inGroup = rows.filter((e) => e.group === group.key);
191
+ if (!inGroup.length) continue;
192
+ out.push("", ko ? group.ko : group.en);
193
+ for (const e of inGroup) {
194
+ const text = label(e);
195
+ // 상한을 넘는 라벨은 정렬을 포기하되 설명과 붙지는 않게 최소 두 칸을 보장한다.
196
+ const gap = visWidth(text) >= width ? " " : " ".repeat(width + 2 - visWidth(text));
197
+ out.push(` ${text}${gap}${descFor(e, lang)}`);
198
+ }
199
+ }
200
+ if (!all) {
201
+ const hidden = forSurface(surface).length - rows.length;
202
+ if (hidden > 0) {
203
+ out.push("", ko
204
+ ? ` 나머지 ${hidden}개 명령: ${surface === "repl" ? "/help all" : "agentlas help all"}`
205
+ : ` ${hidden} more: ${surface === "repl" ? "/help all" : "agentlas help all"}`);
206
+ }
207
+ } else {
208
+ const withAliases = CATALOG.filter((e) => (e.aliases || []).length);
209
+ if (withAliases.length) {
210
+ out.push("", ko ? " 같은 명령의 다른 이름" : " Other names for the same command");
211
+ out.push(" " + withAliases.map((e) => `${e.name} (= ${e.aliases.join(", ")})`).join(" · "));
212
+ }
213
+ }
214
+ return out.join("\n").replace(/^\n/, "");
215
+ }
216
+
217
+ module.exports = { GROUPS, CATALOG, byName, aliasMap, argsFor, descFor, usageFor, forSurface, renderHelp };
@@ -11,90 +11,21 @@
11
11
  const { completePath, isAbsolutePathTask } = require("../agentlas-input.cjs");
12
12
 
13
13
  // command, 인자 힌트, 한 줄 설명 — /help 팔레트와 Tab 완성이 같은 정본을 쓴다.
14
- const SLASH_COMMANDS = [
15
- { command: "/help", args: "", ko: "명령·단축키 보기", en: "Show commands and shortcuts" },
16
- { command: "/sessions", args: "", ko: "세션 표", en: "Session table" },
17
- { command: "/tree", args: "", ko: "세션 트리", en: "Session tree" },
18
- { command: "/s", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
19
- { command: "/switch", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
20
- { command: "/kill", args: "<n>", ko: "실행 중 턴 중단", en: "Interrupt a running turn" },
21
- { command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
22
- { command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
23
- { command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
24
- { command: "/graph", args: "[run <이름>]", ko: "저장된 자동화 그래프", en: "Saved automation graphs" },
25
- { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
26
- { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
27
- { command: "/shell", args: "on|off", ko: "대화형 셸 켜기/끄기", en: "Turn the interactive shell on or off" },
28
- { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
29
- { command: "/model", args: "<id|default>", ko: "새 세션 모델 지정", en: "Set model for new sessions" },
30
- { command: "/effort", args: "<level|none>", ko: "새 세션 추론 강도 지정", en: "Set effort for new sessions" },
31
- { command: "/permission", args: "<level>", ko: "새 세션 권한 지정", en: "Set permission for new sessions" },
32
- { command: "/login", args: "", ko: "Agentlas Cloud 로그인", en: "Sign in to Agentlas Cloud" },
33
- { command: "/whoami", args: "", ko: "로그인 상태", en: "Signed-in account" },
34
- { command: "/search", args: "\"<what you need>\"", ko: "Hub 에이전트 검색", en: "Search Hub agents" },
35
- { command: "/install", args: "<slug>", ko: "Hub 에이전트 설치", en: "Install a Hub agent" },
36
- { command: "/usage", args: "", ko: "로컬 사용 현황", en: "Local usage" },
37
- { command: "/billing", args: "", ko: "크레딧 잔액", en: "Credit balance" },
38
- { command: "/automation", args: "[sub]", ko: "자동화", en: "Automations" },
39
- { command: "/storm", args: "<goal>", ko: "Goal+UltraCode 하니스", en: "Goal+UltraCode harness" },
40
- { command: "/swarm", args: "<goal>", ko: "에이전트 스웜", en: "Agent swarm" },
41
- { command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
42
- { command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
43
- { command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
44
- // 소스 스코프 편성 4종 — 2026-08-05 같은 날 삭제 후 네이티브 배선으로 복원.
45
- // 이전에는 외부 CLI 스텁(exit 3)이라 죽은 메뉴였다. 지금은 이 터미널의 편성
46
- // 루프가 직접 돌고 로컬 Agentlas-OS Core가 선언된 스코프의 메뉴를 연합한다.
47
- // 팔레트 문구가 곧 사용자에게 하는 약속이다 — 스코프를 문장에 적는다.
48
- { command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
49
- { command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
50
- { command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
51
- { command: "/hep-hub", args: "\"<request>\"", ko: "공개 Hub 에이전트만으로 편성", en: "Staff from public Hub agents only" },
52
- { command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
53
- { command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
54
- { command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
55
- { command: "/browser", args: "[sub]", ko: "브라우저 하드포인트", en: "Browser hardpoint" },
56
- { command: "/connect", args: "<target>", ko: "에이전트·팀 연결", en: "Connect an agent or team" },
57
- { command: "/research", args: "<sub>", ko: "리서치", en: "Research" },
58
- { command: "/upload", args: "<path>", ko: "Agent Cloud에 저장·발행", en: "Save to Agent Cloud or publish" },
59
- { command: "/cloud", args: "<sub>", ko: "클라우드 자산 관리", en: "Cloud assets" },
60
- { command: "/import", args: "<path>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
61
- { command: "/cd", args: "[path]", ko: "작업 폴더 이동", en: "Change working folder" },
62
- { command: "/native", args: "prepare <agent>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
63
- { command: "/plugin", args: "<sub>", ko: "Hub 플러그인(MCP)", en: "Hub plugins (MCP servers)" },
64
- { command: "/plugins", args: "", ko: "설치된 플러그인", en: "Installed plugins" },
65
- { command: "/experience", args: "<sub>", ko: "이식 가능한 Experience", en: "Portable Experience" },
66
- { command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
67
- { command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
68
- { command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
69
- // 데스크탑의 `ontology` 는 Core 의 지식·메모리 **런타임**(임베딩 포함)이고, 터미널의
70
- // 이것은 **이 프로젝트의 지식 소스 등록부**다. 서로 다른 것이 같은 이름을 쓰고 있어
71
- // (감사 D6) 라벨이라도 정확해야 한다 — 명령 이름은 사용자 습관과 스크립트가 걸려
72
- // 있어 바꾸지 않는다. Core 의 지식 런타임은 터미널에 아직 미노출이다(결함 아님).
73
- { command: "/ontology", args: "", ko: "프로젝트 지식 소스 등록", en: "Project knowledge sources" },
74
- { command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
75
- { command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
76
- { command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
77
- { command: "/context", args: "<sub>", ko: "의존성 맵", en: "Dependency map" },
78
- { command: "/creds", args: "<sub>", ko: "자격증명", en: "Credentials" },
79
- { command: "/env", args: "", ko: "공유 환경 키", en: "Shared env keys" },
80
- { command: "/multimodal", args: "", ko: "이미지·영상·음성 설정", en: "Image/video/audio providers" },
81
- { command: "/document", args: "pdf <html|url>", ko: "문서 PDF 내보내기", en: "Export a document to PDF" },
82
- { command: "/roles", args: "[set <role> <runtime>]", ko: "오케스트레이터·워커 모델 역할 조회/설정", en: "Show or set orchestrator/worker model roles" },
83
- { command: "/telegram", args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
84
- { command: "/oberon", args: "[sub]", ko: "AI 필름", en: "AI film" },
85
- { command: "/film", args: "<sub>", ko: "필름 렌더", en: "Film render" },
86
- { command: "/hep", args: "<sub…>", ko: "Hephaestus 패스스루", en: "Hephaestus passthrough" },
87
- // "네트워크"만 쓰면 WiFi·LAN 관리로 읽힌다. 이 명령이 다루는 것은 로컬
88
- // 에이전트 네트워크(init|status|reindex|bench|add-source)다.
89
- { command: "/netadmin", args: "[sub]", ko: "로컬 에이전트 네트워크 관리", en: "Local agent network" },
90
- { command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
91
- { command: "/version", args: "", ko: "버전", en: "Version" },
92
- { command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
93
- // 대화가 있으면 --yes 없이는 거절한다(챗/메시지 CASCADE 삭제) — 팔레트에도 노출.
94
- { command: "/uninstall", args: "<slug> [--yes]", ko: "에이전트 제거", en: "Uninstall an agent" },
95
- { command: "/quit", args: "", ko: "종료", en: "Quit" },
96
- { command: "/exit", args: "", ko: "종료", en: "Quit" },
97
- ];
14
+ const catalog = require("./commands-catalog.cjs");
15
+
16
+ /*
17
+ * 정본은 ui/commands-catalog.cjs 하나다(2026-08-11). 여기 있던 71행 리터럴은
18
+ * help.cjs EN/KO 블록과 어긋나 언어 혼재·중복 숨김을 만들었다.
19
+ */
20
+ const SLASH_COMMANDS = catalog.forSurface("repl").map((entry) => ({
21
+ command: "/" + entry.name,
22
+ args: entry.args || "",
23
+ argsKo: entry.argsKo,
24
+ ko: entry.ko,
25
+ en: entry.en,
26
+ group: entry.group,
27
+ tier: entry.tier,
28
+ }));
98
29
 
99
30
  const SLASH_NAMES = SLASH_COMMANDS.map((c) => c.command);
100
31
  const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
@@ -157,9 +88,9 @@ function suggestions(line, limit = 12, lang = "en") {
157
88
  const rows = SLASH_COMMANDS.map((entry) => ({
158
89
  command: entry.command,
159
90
  description: ko ? entry.ko : entry.en,
160
- usage: entry.command + (entry.args ? " " + entry.args : ""),
91
+ usage: "/" + catalog.usageFor(catalog.byName(entry.command) || entry, lang),
161
92
  detail: "",
162
- category: "",
93
+ category: entry.group || "",
163
94
  examples: [],
164
95
  }));
165
96
  const q = value.toLowerCase();
@@ -173,14 +104,13 @@ function suggestions(line, limit = 12, lang = "en") {
173
104
  return starts.concat(contains).slice(0, limit);
174
105
  }
175
106
 
176
- /** /help 팔레트 렌더 — Tab 완성과 같은 정본에서 나온다. */
177
- function renderPalette(lang) {
178
- const ko = lang === "ko";
179
- const width = Math.max(...SLASH_COMMANDS.map((c) => (c.command + " " + c.args).length));
180
- return SLASH_COMMANDS
181
- .filter((c, i, all) => all.findIndex((x) => (ko ? x.ko : x.en) === (ko ? c.ko : c.en)) === i)
182
- .map((c) => ` ${(c.command + (c.args ? " " + c.args : "")).padEnd(width + 2)}${ko ? c.ko : c.en}`)
183
- .join("\n");
107
+ /*
108
+ * /help 팔레트 — CLI 와 완전히 같은 렌더러를 쓴다. 예전엔 여기서 설명 텍스트로
109
+ * 중복 제거를 해서 `/switch` `/list` `/exit` 가 조용히 사라졌다. 별칭이 필드가 된
110
+ * 지금은 중복 자체가 없으므로 필터도 없다.
111
+ */
112
+ function renderPalette(lang, opts = {}) {
113
+ return catalog.renderHelp({ lang, surface: "repl", all: opts.all === true });
184
114
  }
185
115
 
186
116
  module.exports = { SLASH_COMMANDS, SLASH_NAMES, makeCompleter, renderPalette, suggestions };
@@ -683,22 +683,17 @@ function handleSlash(ctx, cmdline, api) {
683
683
  switch (cmd) {
684
684
  case "quit": case "exit": return "quit";
685
685
  case "help": {
686
- require("../commands/help.cjs").run(ctx, rest);
687
- ctx.out("");
688
- ctx.out(ui.c.bold(en ? "Project Work runs" : "프로젝트 Work 실행"));
689
- // 팔레트는 Tab 완성과 같은 정본(ui/palette)에서 렌더한다 — 목록 드리프트 금지.
690
- ctx.out(require("./palette.cjs").renderPalette(ctx.lang));
686
+ // CLI 와 같은 렌더러 한 번. 예전엔 CLI 61줄 + 팔레트 67줄 + 트레일러 = 133줄이었다.
687
+ ctx.out(require("./palette.cjs").renderPalette(ctx.lang, { all: String(rest[0] || "") === "all" }));
691
688
  ctx.out("");
692
689
  ctx.out(ctx.uiInstance.c.dim(en
693
- ? "Tab completes commands, agent names and session keys · ↑/↓ history · typing during a run queues steering · ctrl-c interrupts"
694
- : "Tab: 명령·에이전트·세션키 완성 · ↑/↓ 히스토리 · 실행 입력은 스티어링 큐 · ctrl-c 턴 중단"));
695
- // 배너가 광고하는 Shift-Tab 은 여기에도 적힌다 — 문구와 구현은 한 곳에서 움직인다.
696
- ctx.out(ctx.uiInstance.c.dim(`Shift-Tab: ${i18n.t(ctx.lang, "help.shiftTab")}`));
690
+ ? "Tab completes commands · ↑/↓ history · Shift-Tab cycles permission · ctrl-c interrupts a turn"
691
+ : "Tab: 명령 완성 · ↑/↓ 히스토리 · Shift-Tab 권한 순환 · ctrl-c 턴 중단"));
697
692
  return;
698
693
  }
699
694
  case "shell": {
700
695
  /*
701
- * 새 대화형 셸 켜기/끄기. 저장하고 즉시 안내한다 — 프로세스 중간에
696
+ * 새 대화형 셸 켜기/끄기. 저장하고 재실행을 안내한다 — 프로세스 중간에
702
697
  * stdin 소유권을 바꾸면 지금 도는 readline 과 경합한다(실사고 계열).
703
698
  */
704
699
  const want = String(rest[0] || "").toLowerCase();
@@ -711,13 +706,11 @@ function handleSlash(ctx, cmdline, api) {
711
706
  const { userDataDir } = require("../core/paths.cjs");
712
707
  config.updatePrefs(userDataDir(), { shell: want === "on" ? "interactive" : "classic" });
713
708
  if (ctx.prefs) ctx.prefs.shell = want === "on" ? "interactive" : "classic";
714
- ui.line(want === "on"
715
- ? ui.c.dim(en
716
- ? "Interactive shell enabledrestart agentlas to enter it (/shell off to revert)."
717
- : "대화형 셸을 켰습니다 — agentlas 다시 실행하면 그 화면으로 들어갑니다 (/shell off 로 되돌림).")
718
- : ui.c.dim(en
719
- ? "Interactive shell disabled — restart agentlas for the classic REPL."
720
- : "대화형 셸을 껐습니다 — agentlas 를 다시 실행하면 기본 REPL 입니다."));
709
+ ui.line(ui.c.dim(want === "on"
710
+ ? (en ? "Interactive shell enabled — restart agentlas to enter it (/shell off to revert)."
711
+ : "대화형 셸을 켰습니다 — agentlas 다시 실행하면 그 화면으로 들어갑니다 (/shell off 되돌림).")
712
+ : (en ? "Interactive shell disabledrestart agentlas for the classic REPL."
713
+ : "대화형 셸을 껐습니다 — agentlas 를 다시 실행하면 기본 REPL 입니다.")));
721
714
  return;
722
715
  }
723
716
  case "agents": case "list": require("../commands/list.cjs").run(ctx, rest); return;
@@ -47,9 +47,21 @@ function loadRenderer() {
47
47
 
48
48
  /* Ui 를 상속해 write 초크포인트만 렌더러로 돌린다. */
49
49
  class ShellUi extends Ui {
50
+ /*
51
+ * 실터미널 대신 dummy 스트림 — 놓친 직접 out.write 가 프레임을 찢는 대신 소멸한다.
52
+ * 단 columns 는 살려야 한다: Ui 의 줄바꿈·표·구분선이 전부 this.out.columns 를 읽는데
53
+ * PassThrough 에는 그 속성이 없어 전부 80/100 에 고정돼 있었다(폭 넓은 터미널에서
54
+ * 화면 절반만 쓰던 원인). 렌더러의 실제 폭을 게터로 물린다.
55
+ */
56
+ static _sinkFor(tui) {
57
+ const sink = new PassThrough();
58
+ Object.defineProperty(sink, "columns", {
59
+ get() { return (tui.terminal && tui.terminal.columns) || 80; },
60
+ });
61
+ return sink;
62
+ }
50
63
  constructor(opts, pi, tui, transcript) {
51
- // 실터미널 대신 dummy 스트림 — 놓친 직접 out.write 프레임을 찢는 대신 소멸한다.
52
- super({ ...opts, stream: new PassThrough(), color: true });
64
+ super({ ...opts, stream: ShellUi._sinkFor(tui), color: true });
53
65
  this._pi = pi;
54
66
  this._tui = tui;
55
67
  this._transcript = transcript;
@@ -59,7 +71,12 @@ class ShellUi extends Ui {
59
71
  this._loader = null;
60
72
  }
61
73
  _appendText(text) {
62
- this._transcript.addChild(new this._pi.Text(String(text), 1, 0));
74
+ /*
75
+ * 벤더 Text.render 는 공백만 있는 줄에 [] 를 돌려준다 — screens.cjs 의 ui.line("")
76
+ * 22곳이 전부 조용히 사라져 화면이 한 덩어리로 붙어 있었다. 빈 줄은 Spacer 로.
77
+ */
78
+ const value = String(text);
79
+ this._transcript.addChild(value.trim() === "" ? new this._pi.Spacer(1) : new this._pi.Text(value, 1, 0));
63
80
  this._tui.requestRender();
64
81
  }
65
82
  write(s) {
@@ -101,7 +118,7 @@ class ShellUi extends Ui {
101
118
  this.stopSpinner();
102
119
  this.ensureNl();
103
120
  this._mdText = "";
104
- this._md = new this._pi.Markdown("", 3, 0, this._mdTheme());
121
+ this._md = new this._pi.Markdown("", 1, 0, this._mdTheme());
105
122
  this._transcript.addChild(this._md);
106
123
  this._streaming = true;
107
124
  }
@@ -171,11 +188,22 @@ async function startShell(ctx, opts = {}) {
171
188
 
172
189
  const terminal = new pi.ProcessTerminal();
173
190
  const tui = new pi.TuiMainScreen(terminal);
174
- const ui = new ShellUi({ lang: ctx.lang }, pi, tui, tui);
191
+ /*
192
+ * Container.render 는 삽입 순서로 그린다. 예전엔 헤더 3줄 뒤에 Editor 를 넣어서
193
+ * 입력면이 4번째 줄에 영구 고정됐고, 이후 모든 출력이 그 아래로 흘러 입력 상자가
194
+ * 화면 한가운데 박혔다. 위=트랜스크립트 / 아래=입력면 두 칸으로 나눈다.
195
+ */
196
+ const transcript = new pi.Container();
197
+ const bottom = new pi.Container();
198
+ tui.addChild(transcript);
199
+ tui.addChild(bottom);
200
+ const ui = new ShellUi({ lang: ctx.lang }, pi, tui, transcript);
175
201
 
176
202
  // ctx 초크포인트 재지정 — 55파일의 ctx.out 직출력이 전부 프레임 안으로 들어온다.
177
203
  const shellCtx = {
178
204
  ...ctx,
205
+ // 명령이 "지금 사용자가 어디에 서 있는지"를 알아야 안내를 옳게 쓴다.
206
+ surface: "shell",
179
207
  uiInstance: ui,
180
208
  out: (s = "") => ui.line(String(s)),
181
209
  err: (s = "") => ui.line(ui.c.amber(String(s))),
@@ -227,7 +255,26 @@ async function startShell(ctx, opts = {}) {
227
255
  description: ui.c.dim, scrollInfo: ui.c.faint, noMatch: ui.c.dim,
228
256
  },
229
257
  };
230
- const editor = new pi.Editor(tui, editorTheme, { autocompleteMaxVisible: 8 });
258
+ /*
259
+ * 빈 입력 상자가 "구분선 사이의 빈 칸"으로 보이던 문제(오너 지적). 렌더러의 Editor 는
260
+ * placeholder 를 지원하지 않으므로, 비어 있을 때만 첫 내용 줄 뒤에 힌트를 덧붙인다.
261
+ * 커서는 그 줄에 이미 그려져 있으므로 교체가 아니라 append 여야 안전하다.
262
+ */
263
+ class ShellEditor extends pi.Editor {
264
+ render(width) {
265
+ const lines = super.render(width);
266
+ if (this.getText() === "" && lines.length >= 3) {
267
+ const hint = ui.c.faint(en
268
+ ? "type a task · / for commands"
269
+ : "할 일을 문장으로 · / 명령");
270
+ // 에디터가 줄을 폭까지 공백으로 채운다 — 그대로 덧붙이면 힌트가 오른쪽 끝으로 밀린다.
271
+ // 꼬리 공백만 걷어내고 커서 바로 뒤에 붙인다(ANSI 리셋은 보존).
272
+ lines[1] = lines[1].replace(/[ \t]+(\u001b\[0m)?$/, "$1") + " " + hint;
273
+ }
274
+ return lines;
275
+ }
276
+ }
277
+ const editor = new ShellEditor(tui, editorTheme, { autocompleteMaxVisible: 8, paddingX: 1 });
231
278
  editor.setAutocompleteProvider(new pi.CombinedAutocompleteProvider(toSlashCommands(ctx.lang), process.cwd()));
232
279
 
233
280
  // ── 히스토리 디스크 영속 (증분 2) — cli-history.json v2 계약을 그대로 재사용 ──
@@ -250,14 +297,51 @@ async function startShell(ctx, opts = {}) {
250
297
  onMessage: (msg) => { ui.ensureNl(); ui.line(ui.c.dim(msg.text)); },
251
298
  });
252
299
 
300
+ /*
301
+ * 목록 피커 — 슬러그를 손으로 받아치게 하지 않는다(오너 지적).
302
+ * SelectList 는 Focusable 이 아니라 포커스로는 키가 안 온다(Loader 와 같은 함정) —
303
+ * 전역 리스너에서 직접 forward 하고, 뜨는 동안 에디터 입력을 막는다.
304
+ */
305
+ let activePicker = null;
306
+ function pick(items, opts = {}) {
307
+ return new Promise((resolve) => {
308
+ if (!items.length) { resolve(null); return; }
309
+ ui.ensureNl();
310
+ if (opts.title) ui.line(ui.c.bold(opts.title));
311
+ ui.line(ui.c.dim(en
312
+ ? "↑/↓ choose · Enter confirm · Esc cancel"
313
+ : "↑/↓ 이동 · Enter 선택 · Esc 취소"));
314
+ const list = new pi.SelectList(items, Math.min(10, items.length), editorTheme.selectList, {});
315
+ const finish = (value) => {
316
+ if (activePicker !== list) return;
317
+ activePicker = null;
318
+ bottom.removeChild(list);
319
+ tui.setFocus(editor);
320
+ tui.requestRender();
321
+ resolve(value);
322
+ };
323
+ list.onSelect = (item) => finish(item);
324
+ list.onCancel = () => finish(null);
325
+ activePicker = list;
326
+ bottom.addChild(list);
327
+ tui.setFocus(null);
328
+ tui.requestRender();
329
+ });
330
+ }
331
+
253
332
  const commands = require("../commands/index.cjs");
254
333
  const handleSlash = async (cmdline) => {
255
334
  const raw = cmdline.split(/\s+/)[0] || "";
256
- const rest = cmdline.slice(raw.length).trim().split(/\s+/).filter(Boolean);
335
+ // 팔레트가 따옴표 인자를 가르치므로 REPL 과 같은 토크나이저를 쓴다.
336
+ const rest = require("../agentlas-input.cjs").tokenizeCommandLine(cmdline).slice(1);
257
337
  const cmd = commands.resolveCommandName(raw);
258
338
  if (cmd === "quit" || cmd === "exit") return "quit";
259
339
  if (cmd === "help") {
260
- ui.line(palette.renderPalette(ctx.lang));
340
+ ui.line(palette.renderPalette(ctx.lang, { all: String(rest[0] || "") === "all" }));
341
+ ui.line("");
342
+ ui.line(ui.c.dim(en
343
+ ? "Tab completes commands · ↑/↓ history · Shift-Tab cycles permission · Esc interrupts a turn"
344
+ : "Tab: 명령 완성 · ↑/↓ 히스토리 · Shift-Tab 권한 순환 · Esc 턴 중단"));
261
345
  return;
262
346
  }
263
347
  // 그래프 보기 (Phase 4) — 캔버스를 흉내내지 않는다: mermaid → 유니코드 박스 아트.
@@ -275,7 +359,8 @@ async function startShell(ctx, opts = {}) {
275
359
  lines.push(n.type === "condition" ? ` ${n.id}{${label}}` : ` ${n.id}[${label}]`);
276
360
  }
277
361
  for (const e of g.edges || []) {
278
- const lbl = e.sourceHandle === "true" ? "|참|" : e.sourceHandle === "false" ? "|거짓|" : "";
362
+ const lbl = e.sourceHandle === "true" ? (en ? "|yes|" : "|참|")
363
+ : e.sourceHandle === "false" ? (en ? "|no|" : "|거짓|") : "";
279
364
  lines.push(` ${e.source} -->${lbl} ${e.target}`);
280
365
  }
281
366
  const { render, toAnsi } = require("../vendor/mermaid/index.js");
@@ -288,10 +373,100 @@ async function startShell(ctx, opts = {}) {
288
373
  } catch { /* 렌더 실패 → 아래 클래식 폴스루가 텍스트로 보여준다 */ }
289
374
  }
290
375
  }
376
+ /*
377
+ * /search — 결과를 목록으로 띄우고 방향키로 고른다. 슬러그를 손으로 받아치게
378
+ * 하지 않는다(오너 지적). 고르면 바로 설치까지 간다.
379
+ *
380
+ * kind 는 서버 열거값을 그대로 보여주지 않는다. "cloud-callable" 은 사용자에게
381
+ * "설치 안 해도 바로 부를 수 있음"이라는 뜻이지, 설치가 안 된다는 뜻이 아니다.
382
+ */
383
+ if (cmd === "search" && rest.length) {
384
+ const query = rest.join(" ");
385
+ const { callHubTool, HubError } = require("../cloud/hub-client.cjs");
386
+ let result;
387
+ ui.updateSpinner(en ? "Searching the Hub…" : "Hub 검색 중…");
388
+ try {
389
+ result = await callHubTool("marketplace.search_agents", { q: query, query, limit: 12 });
390
+ } catch (e) {
391
+ ui.stopSpinner();
392
+ ui.error(Object.assign(new Error(e instanceof HubError ? e.message : String((e && e.message) || e)),
393
+ { code: "hub_search_failed", honestStop: true }));
394
+ return;
395
+ }
396
+ ui.stopSpinner();
397
+ const raw = (result && (result.results || result.agents || result.items)) || (Array.isArray(result) ? result : []);
398
+ const hidden = (slug) => /^researcher-\d+/.test(String(slug || "").toLowerCase())
399
+ || String(slug || "").toLowerCase().startsWith("hephaestus-");
400
+ const rows = (Array.isArray(raw) ? raw : []).filter((it) => !hidden(it && (it.slug || it.id)));
401
+ if (!rows.length) { ui.line(ui.c.dim(en ? `No results for "${query}"` : `"${query}" 결과 없음`)); return; }
402
+ const callable = (k) => (String(k || "").includes("cloud")
403
+ ? (en ? "callable without installing" : "설치 없이 호출 가능")
404
+ : (en ? "install to use" : "설치해야 사용"));
405
+ const chosen = await pick(rows.map((it) => ({
406
+ value: it.slug || it.id || "?",
407
+ label: `${it.slug || it.id}`,
408
+ description: `${it.name || it.title || ""} — ${callable(it.kind || it.entity_kind)}`,
409
+ })), { title: en ? `Hub results for "${query}"` : `"${query}" Hub 결과` });
410
+ if (!chosen) { ui.line(ui.c.dim(en ? "cancelled" : "취소됨")); return; }
411
+ ui.line(ui.c.dim(en ? `installing ${chosen.value}…` : `${chosen.value} 설치 중…`));
412
+ await commands.COMMANDS.install().run(shellCtx, [chosen.value]);
413
+ return;
414
+ }
415
+
416
+ /*
417
+ * /graph — 저장된 그래프를 목록으로 띄우고 고른 것을 실행한다.
418
+ * (저장 테이블 이름은 automations 이지만 이 화면이 다루는 건 그래프다.)
419
+ */
420
+ if (cmd === "graph" && (!rest.length || rest[0] === "list")) {
421
+ const rowsOf = db.prepare("SELECT name, enabled, schedule, graph_json FROM automations ORDER BY name").all();
422
+ const graphs = rowsOf.filter((r) => r.graph_json);
423
+ if (!graphs.length) { ui.line(ui.c.dim(en ? "No saved graphs yet." : "저장된 그래프가 없습니다.")); return; }
424
+ const chosen = await pick(graphs.map((g) => {
425
+ let steps = 0;
426
+ try { steps = (JSON.parse(g.graph_json).nodes || []).length; } catch { steps = 0; }
427
+ return {
428
+ value: g.name,
429
+ label: g.name,
430
+ description: `${steps} ${en ? "steps" : "단계"} · ${g.enabled ? (en ? "on" : "켜짐") : (en ? "off" : "꺼짐")}`
431
+ + (g.schedule ? ` · ${g.schedule}` : ""),
432
+ };
433
+ }), { title: en ? "Saved graphs" : "저장된 그래프" });
434
+ if (!chosen) { ui.line(ui.c.dim(en ? "cancelled" : "취소됨")); return; }
435
+ await commands.COMMANDS.graph().run(shellCtx, ["run", chosen.value]);
436
+ return;
437
+ }
438
+
439
+ /*
440
+ * 세션 설정 4종. 자동완성은 되는데 처리 case 가 없어 "여기서는 아직 안 됩니다"만
441
+ * 답하던 죽은 광고였다(신설 게이트가 잡았다). 기본 REPL 과 같은 의미로 배선하고,
442
+ * 영구 저장 경로를 같은 줄에서 알려준다 — 이 값들은 이 셸 한정이다.
443
+ */
444
+ if (cmd === "permission" || cmd === "model" || cmd === "runtime" || cmd === "effort") {
445
+ const value = String(rest[0] || "").trim();
446
+ const entry = require("./commands-catalog.cjs").byName(cmd);
447
+ if (!value) {
448
+ ui.line(ui.c.dim(`Usage: /${cmd} ${entry ? entry.args : ""}`));
449
+ return;
450
+ }
451
+ if (cmd === "permission") {
452
+ const next = permissions.normalize(value);
453
+ if (!next) { ui.line(ui.c.dim("Usage: /permission read|write|full")); return; }
454
+ permission = next;
455
+ ui.line(ui.c.dim(`permission: ${next} · ${en ? "persist: agentlas setup" : "영구 저장: agentlas setup"}`));
456
+ return;
457
+ }
458
+ if (cmd === "model") opts.model = value === "default" ? null : value;
459
+ else if (cmd === "runtime") opts.runtime = value;
460
+ else opts.effort = value;
461
+ ui.line(ui.c.dim(`${cmd}: ${value} · ${en
462
+ ? "applies to new sessions here (persist: agentlas roles set)"
463
+ : "이 셸의 새 세션부터 (영구 저장: agentlas roles set)"}`));
464
+ return;
465
+ }
291
466
  // 셸 끄기 — 여기서도 되돌아갈 수 있어야 한다(들어와서 못 나가면 갇힌다)
292
467
  if (cmd === "shell") {
293
468
  const want = String(rest[0] || "").toLowerCase();
294
- if (!["on", "off"].includes(want)) { ui.line(ui.c.dim("Usage: /shell on|off (현재: on)")); return; }
469
+ if (!["on", "off"].includes(want)) { ui.line(ui.c.dim(en ? "Usage: /shell on|off" : "사용법: /shell on|off")); return; }
295
470
  const config = require("../agentlas-config.cjs");
296
471
  const { userDataDir } = require("../core/paths.cjs");
297
472
  config.updatePrefs(userDataDir(), { shell: want === "on" ? "interactive" : "classic" });
@@ -386,7 +561,7 @@ async function startShell(ctx, opts = {}) {
386
561
  }
387
562
  })();
388
563
  };
389
- tui.addChild(editor);
564
+ bottom.addChild(editor);
390
565
  tui.setFocus(editor);
391
566
 
392
567
  const shutdown = (code) => {
@@ -396,6 +571,13 @@ async function startShell(ctx, opts = {}) {
396
571
  };
397
572
 
398
573
  tui.addInputListener((data) => {
574
+ // 피커가 떠 있으면 그 키는 피커 것이다 — 에디터로 새면 목록 위에서 글이 써진다.
575
+ if (activePicker) {
576
+ if (pi.matchesKey(data, "escape")) { activePicker.onCancel && activePicker.onCancel(); return { handled: true }; }
577
+ activePicker.handleInput(data);
578
+ tui.requestRender();
579
+ return { handled: true };
580
+ }
399
581
  // Shift-Tab 권한 순환 — 렌더러가 raw mode 를 단독 소유하므로 readline 의
400
582
  // swallowCompletion 우회 없이 여기서 직접 소비한다 (D2 위험 2의 해소 형태).
401
583
  if (pi.matchesKey(data, "shift+tab")) {
@@ -296,7 +296,13 @@ function coreHarness() {
296
296
 
297
297
  async function workforceAccountContext() {
298
298
  const cookie = await hubClient.cloudSessionCookie();
299
- if (!cookie) throw new Error("Agentlas sign-in is required for cross-session Workforce continuity.");
299
+ // 가장 흔한 로그아웃 경로 마커가 없으면 표시 경계가 삼켜 "복구 중" 한 줄이 된다.
300
+ if (!cookie) {
301
+ throw Object.assign(
302
+ new Error("Agentlas sign-in required — run `agentlas login`, then retry."),
303
+ { code: "auth_required", honestStop: true },
304
+ );
305
+ }
300
306
  const webBase = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
301
307
  const mcpBase = (process.env.AGENTLAS_MCP_BASE_URL || `${webBase}/api/mcp/v1`).replace(/\/$/, "");
302
308
  const response = await hubClient.fetchHub(mcpBase, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"