agentlas 0.5.2 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +48 -6
  2. package/bin/agentlas.cjs +55 -8
  3. package/engine/agentlas-api-agent.cjs +1 -1
  4. package/engine/agentlas-banner.cjs +40 -56
  5. package/engine/agentlas-capabilities.cjs +3 -0
  6. package/engine/agentlas-cloud-runtime.cjs +65 -11
  7. package/engine/agentlas-composer.cjs +109 -44
  8. package/engine/agentlas-doctor.cjs +40 -12
  9. package/engine/agentlas-i18n.cjs +120 -12
  10. package/engine/agentlas-input.cjs +116 -19
  11. package/engine/agentlas-native-host.cjs +381 -83
  12. package/engine/agentlas-parity.cjs +315 -45
  13. package/engine/agentlas-permissions.cjs +90 -0
  14. package/engine/agentlas-repl.cjs +99 -45
  15. package/engine/agentlas-tasks.cjs +111 -0
  16. package/engine/agentlas-tools.cjs +174 -12
  17. package/engine/agentlas-ui.cjs +348 -23
  18. package/engine/agentlas.cjs +2742 -338
  19. package/engine/semver.cjs +64 -0
  20. package/package.json +1 -1
  21. package/test/bootstrap-race.cjs +47 -0
  22. package/test/capture-runtime-guard.cjs +122 -0
  23. package/test/cloud-asset-restore.cjs +423 -0
  24. package/test/cloud-cas-client.cjs +333 -0
  25. package/test/cloud-owner-restore.cjs +183 -0
  26. package/test/cloud-runtime-paths.cjs +40 -0
  27. package/test/cloud-save-publish.cjs +453 -0
  28. package/test/credential-env-regression.cjs +52 -0
  29. package/test/login-loopback-security.cjs +115 -0
  30. package/test/mcp-config-isolation.cjs +36 -0
  31. package/test/permission-mapping.cjs +180 -0
  32. package/test/run-api-regression.cjs +322 -0
  33. package/test/runtime-env-protection.cjs +45 -0
  34. package/test/semver-precedence.cjs +39 -0
  35. package/test/smoke.sh +19 -0
  36. package/test/sqlite-driver-probe.cjs +22 -0
  37. package/test/terminal-ui-regression.cjs +454 -0
  38. package/test/timeout-regression.cjs +218 -0
  39. package/test/tool-workspace-boundary.cjs +165 -0
  40. package/test/update-safety.cjs +376 -0
@@ -20,12 +20,28 @@ function codexHome() {
20
20
  return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
21
21
  }
22
22
 
23
- /** 에러 텍스트에서 실패 원인으로 지목된 원격 호스트들을 추출한다. */
23
+ /**
24
+ * 에러 텍스트에서 OAuth 자원 메타데이터로 명시된 호스트만 추출한다.
25
+ * stderr의 모든 URL을 수집하면 도움말·문서 링크와 관련된 정상 플러그인까지
26
+ * 수리 대상이 될 수 있다. 자동 수리는 구조화된 증거가 있을 때만 실행한다.
27
+ */
24
28
  function extractHosts(error) {
25
29
  const hosts = new Set();
26
- const re = /https?:\/\/([a-z0-9][a-z0-9.-]*[a-z0-9])/gi;
27
- let m;
28
- while ((m = re.exec(error)) !== null) hosts.add(m[1].toLowerCase());
30
+ const addUrlHost = (rawUrl) => {
31
+ try {
32
+ hosts.add(new URL(rawUrl.replace(/[\]}>),.;]+$/g, "")).hostname.toLowerCase());
33
+ } catch {
34
+ /* 잘못된 메타데이터 URL은 자동 수리 증거로 쓰지 않음 */
35
+ }
36
+ };
37
+ const patterns = [
38
+ /resource_metadata(?:_url)?\s*[:=]\s*\\?["']?(https?:\/\/[^\s"'\\)>,]+)/gi,
39
+ /(https?:\/\/[^\s"'\\)>,]+\/\.well-known\/oauth-protected-resource(?:[/?#][^\s"'\\)>,]*)?)/gi,
40
+ ];
41
+ for (const re of patterns) {
42
+ let m;
43
+ while ((m = re.exec(error || "")) !== null) addUrlHost(m[1]);
44
+ }
29
45
  return [...hosts];
30
46
  }
31
47
 
@@ -47,7 +63,7 @@ function classifyFailure(error) {
47
63
  return { kind: "codex-config-invalid", hosts: [], badServer: extractBadMcpServer(text) };
48
64
  }
49
65
  if (/no response for \d+s|auto-aborted/i.test(text)) return { kind: "timeout", hosts: [] };
50
- if (/CLI exit \d+|exited with code [1-9]/i.test(text)) return { kind: "cli-exit", hosts: extractHosts(text) };
66
+ if (/(?:CLI exit|exited with code)\s+[1-9]\d*/i.test(text)) return { kind: "cli-exit", hosts: extractHosts(text) };
51
67
  return { kind: "unknown", hosts: [] };
52
68
  }
53
69
 
@@ -89,7 +105,9 @@ function findOauthPluginsByHost(hosts) {
89
105
  } catch {
90
106
  continue;
91
107
  }
92
- if (hosts.some((h) => h === host || h.endsWith("." + host) || host.endsWith("." + h))) {
108
+ // 호스트가 정확히 같을 때만 자동 수리한다. 부모/자식 도메인 관계만으로는
109
+ // 어느 플러그인이 실패했는지 증명할 수 없다.
110
+ if (hosts.includes(host)) {
93
111
  // cache 디렉토리 "openai-curated-remote"는 config 키에선 "openai-curated".
94
112
  hits.push({ pluginKey: `${plugin}@${marketplace.replace(/-remote$/, "")}`, host });
95
113
  }
@@ -111,12 +129,22 @@ function disableCodexPlugin(pluginKey) {
111
129
  const original = fs.readFileSync(configPath, "utf8");
112
130
  const header = `[plugins."${pluginKey}"]`;
113
131
  let next;
114
- if (original.includes(header)) {
115
- const idx = original.indexOf(header);
116
- const after = original.slice(idx);
117
- const replacedAfter = after.replace(/(\[plugins\."[^"]+"\]\s*\n)enabled\s*=\s*true/, "$1enabled = false");
118
- if (replacedAfter === after) return false; // 이미 false거나 형태가 다름
119
- next = original.slice(0, idx) + replacedAfter;
132
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ const headerMatch = new RegExp(`^[ \\t]*${escapeRegExp(header)}[ \\t]*(?:#.*)?\\r?$`, "m").exec(original);
134
+ if (headerMatch) {
135
+ const headerLineEnd = headerMatch.index + headerMatch[0].length;
136
+ const newlineIndex = original.indexOf("\n", headerLineEnd);
137
+ const bodyStart = newlineIndex === -1 ? original.length : newlineIndex + 1;
138
+ const remaining = original.slice(bodyStart);
139
+ const nextSection = /^[ \t]*\[[^\r\n]+\][ \t]*(?:#.*)?\r?$/m.exec(remaining);
140
+ const sectionEnd = nextSection ? bodyStart + nextSection.index : original.length;
141
+ const section = original.slice(headerMatch.index, sectionEnd);
142
+ const replacedSection = section.replace(
143
+ /^([ \t]*enabled[ \t]*=[ \t]*)true([ \t]*(?:#.*)?)(?=\r?$)/m,
144
+ "$1false$2",
145
+ );
146
+ if (replacedSection === section) return false; // 이미 false거나 해당 섹션에 enabled=true가 없음
147
+ next = original.slice(0, headerMatch.index) + replacedSection + original.slice(sectionEnd);
120
148
  } else {
121
149
  next = `${original.trimEnd()}\n\n${header}\nenabled = false\n`;
122
150
  }
@@ -16,6 +16,14 @@ const STRINGS = {
16
16
  "banner.help": "for commands",
17
17
  "banner.quit": "to quit",
18
18
  "banner.interrupt": "to interrupt",
19
+ "banner.product": "Agent OS terminal",
20
+ "banner.session": "%s · %s · %s",
21
+ "banner.location": "%s · / commands · Shift-Tab permissions",
22
+ "status.title": "Session",
23
+ "status.runtime": "runtime",
24
+ "status.agent": "agent",
25
+ "status.directory": "directory",
26
+ "status.permission": "permission",
19
27
  // picker
20
28
  "picker.agents": "Agents",
21
29
  "picker.companies": "Companies",
@@ -34,6 +42,15 @@ const STRINGS = {
34
42
  "stalled": "stream stalled — interrupted (idle timeout)",
35
43
  "thinkingWith": "thinking with %s",
36
44
  "spinnerStop": "ctrl-c to stop",
45
+ "runtime.thinking": "thinking…",
46
+ "runtime.reasoning": "reasoning…",
47
+ "runtime.starting": "starting %s…",
48
+ "runtime.failed": "failed to run %s: %s",
49
+ "runtime.exited": "%s exited with code %s",
50
+ "runtime.noOutput": "%s returned no output",
51
+ "runtime.rateLimit": "%s rate limit reached",
52
+ "runtime.doctor": "Runtime Doctor: %s",
53
+ "runtime.doctorRetry": "Repair complete — retrying the same request once.",
37
54
  "noKey": "No %s API key. Add one in the app (Settings → BYOK), or switch with /runtime.",
38
55
  "runtimeSet": "runtime → %s",
39
56
  "runtimeNotInstalled": "%s CLI is not installed.",
@@ -47,6 +64,10 @@ const STRINGS = {
47
64
  "permSet": "permission → %s",
48
65
  "permUsage": "usage: /permission read|write|full",
49
66
  "permCurrent": "current permission: %s",
67
+ "permCycleHint": "Shift-Tab to cycle",
68
+ "permCycleConfirm": "permission → %s",
69
+ "permFullArm": "FULL ACCESS IS UNRESTRICTED — press Shift-Tab again within 5 seconds to confirm",
70
+ "permFullConfirm": "FULL ACCESS ON FOR THIS SESSION — approvals and sandboxing are bypassed",
50
71
  "sideUsage": "usage: /side <question>",
51
72
  "sideNeedsSubject": "pick an agent first, or type the task normally so Agentlas can auto-route it",
52
73
  "sideStart": "side question; this answer will not be saved to chat context",
@@ -66,8 +87,10 @@ const STRINGS = {
66
87
  "market.loggedIn": "cloud session: signed in",
67
88
  "market.loggedOut": "cloud session: signed out — sign in via the app (or set AGENTLAS_SESSION) to install cloud agents",
68
89
  "mcp.none": "(no MCP servers configured — add them in the app)",
69
- "mcp.wired": "%s server(s) wired into write/full turns (incl. playwright)",
70
- "mcp.usage": "add/remove MCP servers in the app; the terminal wires enabled stdio servers",
90
+ "mcp.playwright": "Playwright browser",
91
+ "mcp.fullOnly": "full only",
92
+ "mcp.wired": "%s server(s) available only in full-access turns (incl. playwright)",
93
+ "mcp.usage": "add/remove MCP servers in the app; read/write never inject external MCP tools",
71
94
  "help.mcp": "list configured MCP servers and which are wired",
72
95
  "resume.title": "Resume — recent sessions",
73
96
  "resume.none": "(no saved sessions yet — they're saved after each turn)",
@@ -94,6 +117,13 @@ const STRINGS = {
94
117
  "multimodal.usage": "set with /multimodal set <image|video|audio> <provider-id>",
95
118
  // help rows
96
119
  "help.talk": "talk to the current agent/company — streaming + tools",
120
+ "help.help": "show commands, shortcuts, and common flows",
121
+ "help.title": "Help",
122
+ "help.intro": "Run local agents with explicit runtime, permission, files, shell, and history controls.",
123
+ "help.commands": "Commands",
124
+ "skills.title": "Skills",
125
+ "permissions.title": "Permissions",
126
+ "permissions.current": "Current",
97
127
  "help.skills": "list slash-command skills",
98
128
  "help.agents": "list installed agents",
99
129
  "help.team": "view/assign each agent's LLM (auto-routed by capability)",
@@ -114,7 +144,6 @@ const STRINGS = {
114
144
  "help.multimodal": "show/set image, video, and audio fallback providers",
115
145
  "help.import": "import a local folder (agent or team)",
116
146
  "help.market": "browse/install marketplace agents",
117
- "help.install": "install a cloud agent by slug",
118
147
  "help.clear": "clear the chat and redraw",
119
148
  "help.storm": "run a force-robust Stormbreaker pipeline on a goal",
120
149
  "help.build": "build/repair/package an agent or team (Hephaestus)",
@@ -149,6 +178,31 @@ const STRINGS = {
149
178
  "help.tab": "autocomplete commands, agents, runtimes, paths",
150
179
  "help.arrows": "browse persisted input history",
151
180
  "help.ctrlc": "interrupt a running turn, or press twice when idle to quit",
181
+ "help.shiftTab": "cycle read → write → full permission",
182
+ "help.ctrlT": "show or hide the real runtime task/todo list during a turn",
183
+ "palette.title": "Slash commands",
184
+ "palette.search": "type to search",
185
+ "palette.category": "category: %s",
186
+ "palette.examples": "examples: %s",
187
+ "palette.controls": "↑↓ move · Enter run · Tab complete · Esc close",
188
+ "tasks.title": "Tasks",
189
+ "tasks.show": "Ctrl-T show",
190
+ "tasks.hide": "Ctrl-T hide",
191
+ "tasks.pending": "pending",
192
+ "tasks.progress": "in progress",
193
+ "tasks.done": "done",
194
+ "tasks.failed": "failed",
195
+ "category.Help": "Help",
196
+ "category.Session": "Session",
197
+ "category.Discovery": "Discovery",
198
+ "category.Knowledge": "Knowledge",
199
+ "category.Routing": "Routing",
200
+ "category.Settings": "Settings",
201
+ "category.Files": "Files",
202
+ "category.Context": "Context",
203
+ "category.Engine": "Engine",
204
+ "category.Hub": "Hub",
205
+ "category.Health": "Health",
152
206
  "compact.noop": "context is already compact (%s message(s))",
153
207
  "compact.done": "compacted context: %s → %s message(s)",
154
208
  // onboarding wizard
@@ -159,9 +213,9 @@ const STRINGS = {
159
213
  "wiz.runtimeInstalled": "installed",
160
214
  "wiz.runtimeMissing": "not installed",
161
215
  "wiz.permQ": "How much should agents be allowed to do by default?",
162
- "wiz.permRead": "read — look only (no changes)",
163
- "wiz.permWrite": "write — read + create/edit files (recommended)",
164
- "wiz.permFull": "full — everything, including shell commands",
216
+ "wiz.permRead": "read — inspect only; runtime is read-only/plan",
217
+ "wiz.permWrite": "write — edit this workspace in the runtime sandbox; no external MCP (recommended)",
218
+ "wiz.permFull": "full — unrestricted; bypass runtime approvals and sandboxing",
165
219
  "wiz.pick": "Enter a number › ",
166
220
  "wiz.saved": "All set. You can change any of this later with /runtime, /permission.",
167
221
  "wiz.changeLang": "Tip: re-run setup anytime with agentlas setup",
@@ -170,6 +224,14 @@ const STRINGS = {
170
224
  "banner.help": "명령 보기",
171
225
  "banner.quit": "종료",
172
226
  "banner.interrupt": "턴 중단",
227
+ "banner.product": "Agent OS 터미널",
228
+ "banner.session": "%s · %s · %s",
229
+ "banner.location": "%s · / 명령 · Shift-Tab 권한",
230
+ "status.title": "세션",
231
+ "status.runtime": "런타임",
232
+ "status.agent": "에이전트",
233
+ "status.directory": "작업 폴더",
234
+ "status.permission": "권한",
173
235
  "picker.agents": "에이전트",
174
236
  "picker.companies": "회사",
175
237
  "picker.none": "(아직 없음 — Agentlas 앱에서 에이전트를 설치하거나 /import <경로>)",
@@ -186,6 +248,15 @@ const STRINGS = {
186
248
  "stalled": "응답이 지연되어 중단했습니다 (idle timeout)",
187
249
  "thinkingWith": "%s로 생각 중",
188
250
  "spinnerStop": "ctrl-c로 중단",
251
+ "runtime.thinking": "생각 중…",
252
+ "runtime.reasoning": "추론 중…",
253
+ "runtime.starting": "%s 시작 중…",
254
+ "runtime.failed": "%s 실행 실패: %s",
255
+ "runtime.exited": "%s 종료 코드 %s",
256
+ "runtime.noOutput": "%s가 출력 없이 종료됨",
257
+ "runtime.rateLimit": "%s 사용량 한도에 도달함",
258
+ "runtime.doctor": "런타임 진단: %s",
259
+ "runtime.doctorRetry": "자동 수리 완료 — 같은 요청을 한 번 다시 시도합니다.",
189
260
  "noKey": "%s API 키가 없습니다. 앱 설정 → BYOK에서 등록하거나 /runtime으로 전환하세요.",
190
261
  "runtimeSet": "런타임 → %s",
191
262
  "runtimeNotInstalled": "%s CLI가 설치돼 있지 않습니다.",
@@ -199,6 +270,10 @@ const STRINGS = {
199
270
  "permSet": "권한 → %s",
200
271
  "permUsage": "사용법: /permission read|write|full",
201
272
  "permCurrent": "현재 권한: %s",
273
+ "permCycleHint": "Shift-Tab으로 변경",
274
+ "permCycleConfirm": "권한 → %s",
275
+ "permFullArm": "무제한 권한은 승인과 샌드박스를 우회합니다 — 5초 안에 Shift-Tab을 다시 눌러 확인",
276
+ "permFullConfirm": "이 세션에서만 무제한 권한 켜짐 — 승인과 샌드박스를 우회함",
202
277
  "sideUsage": "사용법: /side <질문>",
203
278
  "sideNeedsSubject": "먼저 에이전트를 고르거나, 그냥 할 일을 입력해서 Agentlas가 자동 라우팅하게 하세요.",
204
279
  "sideStart": "사이드 질문입니다. 이 답변은 메인 대화 맥락에 저장하지 않습니다.",
@@ -218,8 +293,10 @@ const STRINGS = {
218
293
  "market.loggedIn": "클라우드 세션: 로그인됨",
219
294
  "market.loggedOut": "클라우드 세션: 로그아웃 — 클라우드 설치는 앱에서 로그인(또는 AGENTLAS_SESSION) 필요",
220
295
  "mcp.none": "(설정된 MCP 서버 없음 — 앱에서 추가)",
221
- "mcp.wired": "write/full 턴에 %s개 서버 연결됨 (playwright 포함)",
222
- "mcp.usage": "MCP 서버 추가/삭제는 앱에서; 터미널은 enabled stdio 서버를 연결합니다",
296
+ "mcp.playwright": "Playwright 브라우저",
297
+ "mcp.fullOnly": "full 전용",
298
+ "mcp.wired": "무제한 권한 턴에서만 %s개 서버 사용 가능 (playwright 포함)",
299
+ "mcp.usage": "MCP 서버 추가/삭제는 앱에서; read/write에는 외부 MCP 도구를 주입하지 않음",
223
300
  "help.mcp": "설정된 MCP 서버와 연결 상태 보기",
224
301
  "resume.title": "이어하기 — 최근 세션",
225
302
  "resume.none": "(저장된 세션 없음 — 각 턴 후 저장됩니다)",
@@ -245,6 +322,13 @@ const STRINGS = {
245
322
  "multimodal.set": "멀티모달 %s → %s",
246
323
  "multimodal.usage": "/multimodal set <image|video|audio> <provider-id> 로 변경",
247
324
  "help.talk": "현재 에이전트/회사와 대화 — 스트리밍 + 툴",
325
+ "help.help": "명령·단축키·주요 사용법 보기",
326
+ "help.title": "도움말",
327
+ "help.intro": "런타임·권한·파일·셸·기록을 직접 통제하며 로컬 에이전트를 실행합니다.",
328
+ "help.commands": "명령",
329
+ "skills.title": "스킬",
330
+ "permissions.title": "권한",
331
+ "permissions.current": "현재",
248
332
  "help.skills": "slash 명령 스킬 목록",
249
333
  "help.agents": "설치된 에이전트 목록",
250
334
  "help.team": "에이전트별 LLM 보기/지정 (능력 기반 자동)",
@@ -265,7 +349,6 @@ const STRINGS = {
265
349
  "help.multimodal": "이미지·영상·음성 fallback provider 보기/변경",
266
350
  "help.import": "로컬 폴더(에이전트/팀) 임포트",
267
351
  "help.market": "마켓플레이스 에이전트 보기/설치",
268
- "help.install": "slug로 클라우드 에이전트 설치",
269
352
  "help.clear": "대화 비우고 다시 그리기",
270
353
  "help.storm": "목표를 Stormbreaker 견고 파이프라인으로 실행",
271
354
  "help.build": "에이전트/팀 빌드·수리·패키징 (Hephaestus)",
@@ -299,6 +382,31 @@ const STRINGS = {
299
382
  "help.tab": "명령·에이전트·런타임·경로 자동완성",
300
383
  "help.arrows": "세션 간 저장된 입력 기록 탐색",
301
384
  "help.ctrlc": "실행 중인 턴 중단, 유휴 시 두 번 누르면 종료",
385
+ "help.shiftTab": "read → write → full 권한 순환",
386
+ "help.ctrlT": "실행 중 실제 런타임 작업 목록 열기/접기",
387
+ "palette.title": "슬래시 명령",
388
+ "palette.search": "입력해서 검색",
389
+ "palette.category": "분류: %s",
390
+ "palette.examples": "예시: %s",
391
+ "palette.controls": "↑↓ 이동 · Enter 실행 · Tab 완성 · Esc 닫기",
392
+ "tasks.title": "작업",
393
+ "tasks.show": "Ctrl-T 열기",
394
+ "tasks.hide": "Ctrl-T 접기",
395
+ "tasks.pending": "대기",
396
+ "tasks.progress": "진행 중",
397
+ "tasks.done": "완료",
398
+ "tasks.failed": "실패",
399
+ "category.Help": "도움말",
400
+ "category.Session": "세션",
401
+ "category.Discovery": "탐색",
402
+ "category.Knowledge": "지식",
403
+ "category.Routing": "라우팅",
404
+ "category.Settings": "설정",
405
+ "category.Files": "파일",
406
+ "category.Context": "맥락",
407
+ "category.Engine": "엔진",
408
+ "category.Hub": "허브",
409
+ "category.Health": "점검",
302
410
  "compact.noop": "이미 충분히 compact 상태입니다 (%s개 메시지)",
303
411
  "compact.done": "맥락 compact 완료: %s → %s개 메시지",
304
412
  "wiz.welcome": "Agentlas에 오신 걸 환영합니다 — 먼저 설정할게요.",
@@ -308,9 +416,9 @@ const STRINGS = {
308
416
  "wiz.runtimeInstalled": "설치됨",
309
417
  "wiz.runtimeMissing": "미설치",
310
418
  "wiz.permQ": "에이전트가 기본적으로 어디까지 할 수 있게 할까요?",
311
- "wiz.permRead": "read — 보기만 (변경 없음)",
312
- "wiz.permWrite": "write — 읽기 + 파일 생성/편집 (권장)",
313
- "wiz.permFull": "full — 명령 포함 전부",
419
+ "wiz.permRead": "read — 조회만; 런타임 read-only/plan",
420
+ "wiz.permWrite": "write — 런타임 샌드박스 안에서 작업 공간 편집; 외부 MCP 없음 (권장)",
421
+ "wiz.permFull": "full — 무제한; 런타임 승인과 샌드박스 우회",
314
422
  "wiz.pick": "번호 입력 › ",
315
423
  "wiz.saved": "완료. 나중에 /runtime, /permission으로 언제든 바꿀 수 있어요.",
316
424
  "wiz.changeLang": "팁: 언제든 agentlas setup 으로 다시 설정",
@@ -11,6 +11,7 @@ const fs = require("node:fs");
11
11
  const os = require("node:os");
12
12
  const path = require("node:path");
13
13
  const readline = require("node:readline");
14
+ const i18n = require("./agentlas-i18n.cjs");
14
15
 
15
16
  function userDataDir() {
16
17
  const override = process.env.AGENTLAS_USER_DATA_DIR;
@@ -92,7 +93,7 @@ const SLASH_COMMAND_META = [
92
93
  { 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." },
93
94
  { 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"] },
94
95
  { command: "/multimodal", description: "Show or set image, video, and audio fallback providers", category: "Settings", usage: "/multimodal", detail: "Inspect or change fallback providers for media work." },
95
- { command: "/mcp", description: "List configured MCP servers", category: "Settings", usage: "/mcp", detail: "Show MCP servers and which enabled stdio servers the terminal wires into write/full turns." },
96
+ { command: "/mcp", description: "List configured MCP servers", category: "Settings", usage: "/mcp", detail: "Show MCP servers available only during explicit full-access turns." },
96
97
  { command: "/diff", description: "Show the current git diff", category: "Files", usage: "/diff", detail: "Print the working-tree diff for the current cwd." },
97
98
  { command: "/history", description: "Show recent inputs", category: "Session", usage: "/history", detail: "Show persisted terminal input history." },
98
99
  { command: "/resume", description: "Resume a recent runtime session", category: "Session", usage: "/resume [n]", detail: "List recent agent/runtime sessions and continue one (restores the native session thread)." },
@@ -102,7 +103,6 @@ const SLASH_COMMAND_META = [
102
103
  { command: "/clear", description: "Clear the chat and redraw", category: "Session", usage: "/clear", detail: "Clear local conversation state and redraw the Agentlas banner." },
103
104
  { command: "/import", description: "Import a local agent or team folder", category: "Files", usage: "/import <path>", detail: "Install a local agent or team into Agentlas." },
104
105
  { command: "/marketplace", description: "Browse/install marketplace agents", category: "Routing", usage: "/marketplace", detail: "Show how to install agents from the Agentlas cloud marketplace or a local folder.", aliases: ["/market"] },
105
- { command: "/install", description: "Install a cloud agent by slug", category: "Routing", usage: "/install <slug>", detail: "Download and install an agent from the Agentlas cloud marketplace by slug." },
106
106
  { command: "/storm", description: "Run a force-robust Stormbreaker pipeline on a goal", category: "Engine", usage: "/storm <goal> [--research]", detail: "Route the goal through Hephaestus Stormbreaker and execute the verified pipeline; --research grounds it with Research Engine evidence." },
107
107
  { command: "/swarm", description: "Fan out an emergent agent swarm on a goal", category: "Engine", usage: "/swarm <goal> [--parallel N]", detail: "Parallel workers share a blackboard and spawn subtasks with ## Spawn; a synthesizer merges results into one answer." },
108
108
  { command: "/build", description: "Build/repair/package an agent or team (Hephaestus)", category: "Engine", usage: "/build <what to build>", detail: "Runs Hephaestus hep-build natively — deep interview, scaffolding, packaging." },
@@ -120,19 +120,79 @@ const SLASH_COMMANDS = SLASH_COMMAND_META.flatMap((entry) => [entry.command].con
120
120
  const RUNTIME_SPECS = ["claude-code", "codex", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
121
121
  const PERM_LEVELS = ["read", "write", "full"];
122
122
 
123
+ const HELP_KEY_BY_COMMAND = {
124
+ "/help": "help.help",
125
+ "/status": "help.status",
126
+ "/skills": "help.skills",
127
+ "/career-graph": "help.careerGraph",
128
+ "/ontology": "help.ontology",
129
+ "/agents": "help.agents",
130
+ "/team": "help.team",
131
+ "/agent": "help.agent",
132
+ "/firms": "help.firms",
133
+ "/firm": "help.firms",
134
+ "/runtime": "help.runtime",
135
+ "/model": "help.model",
136
+ "/effort": "help.effort",
137
+ "/permission": "help.permission",
138
+ "/permissions": "help.permissions",
139
+ "/setup": "help.setup",
140
+ "/cwd": "help.cwd",
141
+ "/memory": "help.memory",
142
+ "/side": "help.side",
143
+ "/multimodal": "help.multimodal",
144
+ "/mcp": "help.mcp",
145
+ "/diff": "help.diff",
146
+ "/history": "help.history",
147
+ "/resume": "help.resume",
148
+ "/compact": "help.compact",
149
+ "/cost": "help.cost",
150
+ "/keybindings": "help.keybindings",
151
+ "/clear": "help.clear",
152
+ "/import": "help.import",
153
+ "/marketplace": "help.market",
154
+ "/install": "help.install",
155
+ "/storm": "help.storm",
156
+ "/swarm": "help.swarm",
157
+ "/build": "help.build",
158
+ "/route": "help.route",
159
+ "/research": "help.research",
160
+ "/search": "help.search",
161
+ "/network": "help.network",
162
+ "/browser": "help.browser",
163
+ "/connect": "help.connect",
164
+ "/doctor": "help.doctor",
165
+ "/exit": "help.exit",
166
+ };
167
+
123
168
  function uniqStartsWith(cands, token) {
124
169
  const hits = cands.filter((c) => c.startsWith(token));
125
170
  return hits.length ? hits : cands;
126
171
  }
127
172
 
128
- function slashCommandEntries() {
173
+ function localizeSlashEntry(entry, lang) {
174
+ const helpKey = HELP_KEY_BY_COMMAND[entry.command];
175
+ const description = helpKey ? i18n.t(lang, helpKey) : entry.description;
176
+ const category = entry.category ? i18n.t(lang, `category.${entry.category}`) : entry.category;
177
+ return {
178
+ ...entry,
179
+ description,
180
+ category,
181
+ // English keeps the longer authored detail. Other languages must not fall back to
182
+ // an English paragraph under an otherwise-localized command palette.
183
+ detail: lang === "en" ? entry.detail : description,
184
+ };
185
+ }
186
+
187
+ function slashCommandEntries(lang = "en") {
129
188
  const rows = [];
130
- for (const entry of SLASH_COMMAND_META) {
189
+ for (const rawEntry of SLASH_COMMAND_META) {
190
+ const entry = localizeSlashEntry(rawEntry, lang);
131
191
  rows.push({ ...entry, aliasOf: null });
132
192
  for (const alias of entry.aliases || []) {
133
193
  rows.push({
134
194
  command: alias,
135
- description: `Alias for ${entry.command}`,
195
+ description: lang === "ko" ? `${entry.command} 별칭` : `Alias for ${entry.command}`,
136
196
  category: entry.category,
137
197
  usage: alias + (entry.usage && entry.usage.includes(" ") ? entry.usage.slice(entry.usage.indexOf(" ")) : ""),
138
198
  detail: entry.detail,
@@ -152,11 +212,11 @@ function slashCommandQuery(line) {
152
212
  return value;
153
213
  }
154
214
 
155
- function slashCommandSuggestions(line, limit = 12) {
215
+ function slashCommandSuggestions(line, limit = 12, lang = "en") {
156
216
  const query = slashCommandQuery(line);
157
217
  if (query == null) return [];
158
218
  const q = query.toLowerCase();
159
- const entries = slashCommandEntries();
219
+ const entries = slashCommandEntries(lang);
160
220
  const starts = entries.filter((entry) => entry.command.toLowerCase().startsWith(q));
161
221
  const contains = entries.filter(
162
222
  (entry) =>
@@ -167,9 +227,9 @@ function slashCommandSuggestions(line, limit = 12) {
167
227
  }
168
228
 
169
229
  function padVisible(value, width) {
170
- const clean = stripAnsiLite(value);
171
- if (clean.length >= width) return value;
172
- return value + " ".repeat(width - clean.length);
230
+ const current = visibleWidthLite(value);
231
+ if (current >= width) return value;
232
+ return value + " ".repeat(width - current);
173
233
  }
174
234
 
175
235
  function stripAnsiLite(value) {
@@ -179,8 +239,40 @@ function stripAnsiLite(value) {
179
239
 
180
240
  function truncateVisible(value, width) {
181
241
  const clean = stripAnsiLite(value);
182
- if (clean.length <= width) return value;
183
- return clean.slice(0, Math.max(0, width - 1)) + "";
242
+ if (visibleWidthLite(clean) <= width) return value;
243
+ let out = "";
244
+ let used = 0;
245
+ const room = Math.max(0, width - 1);
246
+ for (const ch of clean) {
247
+ const cells = cellWidthLite(ch);
248
+ if (used + cells > room) break;
249
+ out += ch;
250
+ used += cells;
251
+ }
252
+ return out + "…";
253
+ }
254
+
255
+ function cellWidthLite(ch) {
256
+ const cp = ch.codePointAt(0);
257
+ if (cp < 0x20) return 0;
258
+ return (
259
+ (cp >= 0x1100 && cp <= 0x115f) ||
260
+ (cp >= 0x2e80 && cp <= 0x303e) ||
261
+ (cp >= 0x3041 && cp <= 0x33ff) ||
262
+ (cp >= 0x3400 && cp <= 0x4dbf) ||
263
+ (cp >= 0x4e00 && cp <= 0x9fff) ||
264
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
265
+ (cp >= 0xf900 && cp <= 0xfaff) ||
266
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
267
+ (cp >= 0xff00 && cp <= 0xff60) ||
268
+ (cp >= 0x1f300 && cp <= 0x1faff)
269
+ ) ? 2 : 1;
270
+ }
271
+
272
+ function visibleWidthLite(value) {
273
+ let width = 0;
274
+ for (const ch of stripAnsiLite(value)) width += cellWidthLite(ch);
275
+ return width;
184
276
  }
185
277
 
186
278
  function renderSlashPalette(rows, selectedIndex, opts = {}) {
@@ -194,32 +286,35 @@ function renderSlashPalette(rows, selectedIndex, opts = {}) {
194
286
  inverse: (s) => String(s),
195
287
  };
196
288
  const c = { ...fallbackColors, ...(opts.colors || {}) };
289
+ const lang = opts.lang || "en";
197
290
  const commandWidth = Math.min(24, Math.max(16, rows.reduce((n, row) => Math.max(n, row.command.length), 0) + 2));
198
291
  const descWidth = Math.max(12, columns - commandWidth - 8);
199
292
  const lineWidth = Math.min(columns - 1, commandWidth + descWidth + 5);
200
293
  const selected = rows[Math.max(0, Math.min(selectedIndex, rows.length - 1))] || rows[0];
201
294
  const out = [
202
- c.faint("Slash commands") + c.dim(" type to search"),
295
+ c.faint(truncateVisible(`${i18n.t(lang, "palette.title")} ${i18n.t(lang, "palette.search")}`, lineWidth)),
203
296
  c.faint("─".repeat(lineWidth)),
204
297
  ];
205
298
  rows.forEach((row, index) => {
206
299
  const command = padVisible(row.command, commandWidth);
207
300
  const desc = truncateVisible(row.description, descWidth);
208
301
  const body = " " + c.blue(command) + c.text(desc);
209
- out.push(index === selectedIndex ? c.inverse(body.padEnd(lineWidth)) : body);
302
+ out.push(index === selectedIndex ? c.inverse(padVisible(body, lineWidth)) : body);
210
303
  });
211
304
  out.push(c.faint("─".repeat(lineWidth)));
212
305
  if (selected) {
213
306
  const usage = truncateVisible(selected.usage || selected.command, lineWidth - 2);
214
307
  const detail = truncateVisible(selected.detail || selected.description || "", lineWidth - 2);
215
- const category = selected.category ? `category: ${selected.category}` : "";
216
- out.push(" " + c.text(usage) + (category ? c.dim(" " + category) : ""));
308
+ const category = selected.category ? i18n.t(lang, "palette.category", selected.category) : "";
309
+ const categoryRoom = Math.max(0, lineWidth - visibleWidthLite(usage) - 3);
310
+ const categoryText = categoryRoom > 0 ? truncateVisible(category, categoryRoom) : "";
311
+ out.push(" " + c.text(usage) + (categoryText ? c.dim(" " + categoryText) : ""));
217
312
  if (detail) out.push(" " + c.dim(detail));
218
313
  if (selected.examples && selected.examples.length) {
219
- out.push(" " + c.dim("examples: " + selected.examples.slice(0, 2).join(" | ")));
314
+ out.push(c.dim(truncateVisible(" " + i18n.t(lang, "palette.examples", selected.examples.slice(0, 2).join(" | ")), lineWidth)));
220
315
  }
221
316
  }
222
- out.push(c.dim(" ↑↓ move Enter run Tab complete Esc close"));
317
+ out.push(c.dim(truncateVisible(" " + i18n.t(lang, "palette.controls"), lineWidth)));
223
318
  return out.join("\n");
224
319
  }
225
320
 
@@ -244,7 +339,7 @@ function attachSlashPalette(rl, opts = {}) {
244
339
 
245
340
  function rows() {
246
341
  if (!state.enabled) return [];
247
- return slashCommandSuggestions(rl.line || "");
342
+ return slashCommandSuggestions(rl.line || "", 12, opts.lang || (opts.ui && opts.ui.lang) || "en");
248
343
  }
249
344
  function active() {
250
345
  return rows().length > 0 && state.dismissedForLine !== (rl.line || "");
@@ -276,6 +371,7 @@ function attachSlashPalette(rl, opts = {}) {
276
371
  const body = renderSlashPalette(list, state.selected, {
277
372
  columns: stream.columns || process.stdout.columns || 88,
278
373
  colors,
374
+ lang: opts.lang || (opts.ui && opts.ui.lang) || "en",
279
375
  });
280
376
  stream.write("\x1b7\x1b[E\x1b[0J" + body + "\x1b8");
281
377
  state.visible = true;
@@ -365,6 +461,7 @@ function isAbsolutePathTask(line) {
365
461
  const value = String(line || "").trim();
366
462
  if (!value.startsWith("/")) return false;
367
463
  const first = value.split(/\s+/)[0] || "";
464
+ if (first === "/") return false;
368
465
  if (!first || SLASH_COMMANDS.includes(first)) return false;
369
466
  if (!path.isAbsolute(first)) return false;
370
467
  if (fs.existsSync(first)) return true;