agentlas 0.4.0 → 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 (41) hide show
  1. package/README.md +112 -23
  2. package/bin/agentlas.cjs +55 -8
  3. package/engine/agentlas-api-agent.cjs +1 -1
  4. package/engine/agentlas-banner.cjs +66 -51
  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 +65 -14
  9. package/engine/agentlas-i18n.cjs +132 -12
  10. package/engine/agentlas-input.cjs +123 -19
  11. package/engine/agentlas-native-host.cjs +381 -83
  12. package/engine/agentlas-parity.cjs +373 -53
  13. package/engine/agentlas-permissions.cjs +90 -0
  14. package/engine/agentlas-repl.cjs +149 -47
  15. package/engine/agentlas-tasks.cjs +111 -0
  16. package/engine/agentlas-tools.cjs +174 -12
  17. package/engine/agentlas-ui.cjs +349 -24
  18. package/engine/agentlas.cjs +3074 -379
  19. package/engine/architecture.data.json +5 -1
  20. package/engine/semver.cjs +64 -0
  21. package/package.json +1 -1
  22. package/test/bootstrap-race.cjs +47 -0
  23. package/test/capture-runtime-guard.cjs +122 -0
  24. package/test/cloud-asset-restore.cjs +423 -0
  25. package/test/cloud-cas-client.cjs +333 -0
  26. package/test/cloud-owner-restore.cjs +183 -0
  27. package/test/cloud-runtime-paths.cjs +40 -0
  28. package/test/cloud-save-publish.cjs +453 -0
  29. package/test/credential-env-regression.cjs +52 -0
  30. package/test/login-loopback-security.cjs +115 -0
  31. package/test/mcp-config-isolation.cjs +36 -0
  32. package/test/permission-mapping.cjs +180 -0
  33. package/test/run-api-regression.cjs +322 -0
  34. package/test/runtime-env-protection.cjs +45 -0
  35. package/test/semver-precedence.cjs +39 -0
  36. package/test/smoke.sh +33 -0
  37. package/test/sqlite-driver-probe.cjs +22 -0
  38. package/test/terminal-ui-regression.cjs +454 -0
  39. package/test/timeout-regression.cjs +218 -0
  40. package/test/tool-workspace-boundary.cjs +165 -0
  41. package/test/update-safety.cjs +376 -0
@@ -2,10 +2,10 @@
2
2
  /*
3
3
  * agentlas-composer: a raw-mode bottom input box (Claude Code / Hermes style).
4
4
  *
5
- * ╭────────────────────────────────────────────╮
6
- * › your message
7
- * ╰────────────────────────────────────────────╯
8
- * claude-code · full · 12.3k tok · / for commands
5
+ * ───────────────────────────────────────────────
6
+ * › your message
7
+ * ───────────────────────────────────────────────
8
+ * read + write · codex · Agent · / for commands
9
9
  * (slash suggestions render here while typing /…)
10
10
  *
11
11
  * Single-line field with horizontal scroll (fixed 3-line box → flicker-free clear/redraw).
@@ -13,6 +13,8 @@
13
13
  * Zero external deps. Caller falls back to readline when stdin/stdout is not a TTY.
14
14
  */
15
15
  const readline = require("node:readline");
16
+ const i18n = require("./agentlas-i18n.cjs");
17
+ const permissions = require("./agentlas-permissions.cjs");
16
18
 
17
19
  // East-Asian width: CJK / Hangul / Kana / fullwidth glyphs occupy 2 terminal cells.
18
20
  function isWide(cp) {
@@ -43,6 +45,91 @@ function visWidth(s) {
43
45
  return n;
44
46
  }
45
47
 
48
+ function truncateWidth(value, max) {
49
+ const text = String(value || "");
50
+ if (visWidth(text) <= max) return text;
51
+ let out = "";
52
+ let width = 0;
53
+ const room = Math.max(0, max - 1);
54
+ for (const ch of text) {
55
+ const cells = charWidth(ch);
56
+ if (width + cells > room) break;
57
+ out += ch;
58
+ width += cells;
59
+ }
60
+ return out + "…";
61
+ }
62
+
63
+ function identityPalette() {
64
+ const id = (value) => String(value);
65
+ return { faint: id, emerald: id, text: id, paw: id, amber: id, blue: id, dim: id, inverse: id };
66
+ }
67
+
68
+ function permissionPresentation(ctx, c) {
69
+ const permission = permissions.normalize(ctx.permission, "write");
70
+ const fallback = permissions.copy(permission, ctx.lang || "en").label;
71
+ const label = ctx.permissionLabel || fallback;
72
+ if (permission === "full") return c.paw("▶▶ " + label);
73
+ if (permission === "read") return c.blue("◇ " + label);
74
+ return c.amber("◆ " + label);
75
+ }
76
+
77
+ // Pure frame builder so layout can be regression-tested without taking over a real TTY.
78
+ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
79
+ const c = palette || identityPalette();
80
+ const w = Math.max(29, width);
81
+ const fieldW = Math.max(8, w - 2);
82
+
83
+ // horizontal scroll by visual width — keep the cursor visible (CJK-safe)
84
+ let start = Math.min(state.scroll, state.cur);
85
+ while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 2) start++;
86
+ state.scroll = start;
87
+
88
+ let shown = "";
89
+ let shownWidth = 0;
90
+ for (let i = start; i < state.buf.length; ) {
91
+ const ch = state.buf.codePointAt(i) > 0xffff ? state.buf.slice(i, i + 2) : state.buf[i];
92
+ const cw = charWidth(ch);
93
+ if (shownWidth + cw > fieldW - 2) break;
94
+ shown += ch;
95
+ shownWidth += cw;
96
+ i += ch.length;
97
+ }
98
+
99
+ const prefix = (ctx.glyph || "›") + " ";
100
+ const top = c.faint("─".repeat(w));
101
+ const mid = c.text(prefix) + c.text(shown);
102
+ const bot = c.faint("─".repeat(w));
103
+ const lines = [top, mid, bot];
104
+ if (ctx.status || ctx.permission) {
105
+ const permission = permissions.normalize(ctx.permission, "write");
106
+ const fallback = permissions.copy(permission, ctx.lang || "en").label;
107
+ const permissionLabel = ctx.permissionLabel || fallback;
108
+ const permissionText = (permission === "full" ? "▶▶ " : permission === "read" ? "◇ " : "◆ ") + permissionLabel;
109
+ const available = Math.max(0, w - visWidth(permissionText) - 5);
110
+ const rest = ctx.status && available > 0 ? c.faint(" · " + truncateWidth(ctx.status, available)) : "";
111
+ lines.push(permissionPresentation(ctx, c) + rest);
112
+ }
113
+ if (ctx.confirmation) {
114
+ const prefix = ctx.confirmationTone === "danger" ? "! " : "✓ ";
115
+ const paint = ctx.confirmationTone === "danger" && c.paw ? c.paw : c.green || c.emerald || ((value) => value);
116
+ lines.push(paint(truncateWidth(prefix + ctx.confirmation, w)));
117
+ }
118
+
119
+ const rows = state.suggest || [];
120
+ rows.slice(0, 8).forEach((row, index) => {
121
+ const cmd = String(row.command || "").padEnd(16);
122
+ const desc = String(row.description || "");
123
+ const descRoom = Math.max(0, w - visWidth(" " + cmd + " "));
124
+ const clippedDesc = truncateWidth(desc, descRoom);
125
+ const label = " " + cmd + " " + clippedDesc;
126
+ lines.push(index === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + " " + c.dim(clippedDesc));
127
+ });
128
+ if (rows.length) lines.push(c.faint(" " + i18n.t(ctx.lang || "en", "palette.controls")));
129
+
130
+ return { lines, curCol: visWidth(prefix) + visWidth(state.buf.slice(start, state.cur)) };
131
+ }
132
+
46
133
  function createComposer(opts) {
47
134
  const out = opts.stream || process.stdout;
48
135
  const inp = opts.input || process.stdin;
@@ -61,45 +148,7 @@ function createComposer(opts) {
61
148
 
62
149
  // Build the rendered block (array of lines) + the cursor target column on the input line.
63
150
  function frame(state, ctx) {
64
- const w = boxWidth();
65
- const inner = w - 2; // chars between │ … │
66
- const glyph = " " + (ctx.glyph || "›") + " "; // " › "
67
- const glyphW = visWidth(glyph);
68
- const fieldW = Math.max(8, inner - glyphW);
69
-
70
- // horizontal scroll by visual width — keep the cursor visible (CJK-safe)
71
- let start = Math.min(state.scroll, state.cur);
72
- while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 1) start++;
73
- state.scroll = start;
74
-
75
- let shown = "";
76
- let ww = 0;
77
- for (let i = start; i < state.buf.length; ) {
78
- const ch = state.buf.codePointAt(i) > 0xffff ? state.buf.slice(i, i + 2) : state.buf[i];
79
- const cw = charWidth(ch);
80
- if (ww + cw > fieldW) break;
81
- shown += ch;
82
- ww += cw;
83
- i += ch.length;
84
- }
85
- const pad = " ".repeat(Math.max(0, fieldW - ww));
86
- const top = c.faint("╭" + "─".repeat(inner) + "╮");
87
- const mid = c.faint("│") + c.emerald(glyph) + c.text(shown) + pad + c.faint("│");
88
- const bot = c.faint("╰" + "─".repeat(inner) + "╯");
89
- const lines = [top, mid, bot];
90
- if (ctx.status) lines.push(" " + c.faint(ctx.status));
91
-
92
- const rows = state.suggest || [];
93
- rows.slice(0, 8).forEach((r, i) => {
94
- const cmd = String(r.command || "").padEnd(16);
95
- const desc = String(r.description || "");
96
- const label = (" " + cmd + " " + desc).slice(0, w);
97
- lines.push(i === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + c.dim(desc.slice(0, w - 20)));
98
- });
99
- if (rows.length) lines.push(c.faint(" ↑↓ move · Tab complete · Enter run · Esc close"));
100
-
101
- const curCol = 1 + glyphW + visWidth(state.buf.slice(start, state.cur)); // 0-based visual column
102
- return { lines, curCol };
151
+ return buildComposerFrame(state, ctx, c, boxWidth());
103
152
  }
104
153
 
105
154
  function render(state, ctx) {
@@ -172,6 +221,14 @@ function createComposer(opts) {
172
221
  function onKey(str, key) {
173
222
  key = key || {};
174
223
  const name = key.name;
224
+ const shiftTab = name === "tab" && key.shift;
225
+
226
+ if (!shiftTab && ctx.onPermissionCycleCancel) {
227
+ const hadConfirmation = Boolean(ctx.confirmation);
228
+ const next = ctx.onPermissionCycleCancel();
229
+ if (next && typeof next === "object") Object.assign(ctx, next);
230
+ if (hadConfirmation && !ctx.confirmation) draw();
231
+ }
175
232
 
176
233
  if (key.ctrl && name === "c") {
177
234
  if (state.buf.length) { ctrlc = 0; return setBuf("", 0); }
@@ -212,6 +269,14 @@ function createComposer(opts) {
212
269
  if (state.suggest.length) { state.suggestSel = (state.suggestSel + 1) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
213
270
  return histNav(-1);
214
271
  }
272
+ if (shiftTab) {
273
+ if (ctx.onCyclePermission) {
274
+ const next = ctx.onCyclePermission(ctx.permission);
275
+ if (next && typeof next === "object") Object.assign(ctx, next);
276
+ draw();
277
+ }
278
+ return;
279
+ }
215
280
  if (name === "tab") {
216
281
  if (state.suggest.length) { const cmd = state.suggest[state.suggestSel].command; state.dismissed = cmd; return setBuf(cmd, cmd.length); }
217
282
  if (ctx.complete) {
@@ -253,4 +318,4 @@ function createComposer(opts) {
253
318
  return { read, setHistory: (h) => { history = (h || []).filter((x) => typeof x === "string"); } };
254
319
  }
255
320
 
256
- module.exports = { createComposer, visWidth };
321
+ module.exports = { createComposer, visWidth, buildComposerFrame, truncateWidth };
@@ -20,23 +20,50 @@ 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
 
32
- /** kind: mcp-oauth-unauthenticated | timeout | cli-exit | unknown (데스크탑 TS와 동일 규칙) */
48
+ /** 에러 텍스트에서 config.toml 스키마 위반이 지목한 mcp_servers 이름을 뽑는다. */
49
+ function extractBadMcpServer(error) {
50
+ // 예: Error loading config.toml: url is not supported for stdio in "mcp_servers.agentlas"
51
+ const m = /mcp_servers\.([a-z0-9_.-]+)/i.exec(error || "");
52
+ return m ? m[1] : null;
53
+ }
54
+
55
+ /** kind: mcp-oauth-unauthenticated | codex-config-invalid | timeout | cli-exit | unknown (데스크탑 TS와 동일 규칙) */
33
56
  function classifyFailure(error) {
34
57
  const text = error || "";
35
58
  if (/authrequired|invalid_token|oauth-protected-resource|www_authenticate/i.test(text)) {
36
59
  return { kind: "mcp-oauth-unauthenticated", hosts: extractHosts(text) };
37
60
  }
61
+ // codex config.toml 파싱 실패(예: stdio 서버에 url 키) → CLI가 아예 기동 못 함.
62
+ if (/error loading config\.toml|url is not supported for stdio|invalid config/i.test(text)) {
63
+ return { kind: "codex-config-invalid", hosts: [], badServer: extractBadMcpServer(text) };
64
+ }
38
65
  if (/no response for \d+s|auto-aborted/i.test(text)) return { kind: "timeout", hosts: [] };
39
- 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) };
40
67
  return { kind: "unknown", hosts: [] };
41
68
  }
42
69
 
@@ -78,7 +105,9 @@ function findOauthPluginsByHost(hosts) {
78
105
  } catch {
79
106
  continue;
80
107
  }
81
- if (hosts.some((h) => h === host || h.endsWith("." + host) || host.endsWith("." + h))) {
108
+ // 호스트가 정확히 같을 때만 자동 수리한다. 부모/자식 도메인 관계만으로는
109
+ // 어느 플러그인이 실패했는지 증명할 수 없다.
110
+ if (hosts.includes(host)) {
82
111
  // cache 디렉토리 "openai-curated-remote"는 config 키에선 "openai-curated".
83
112
  hits.push({ pluginKey: `${plugin}@${marketplace.replace(/-remote$/, "")}`, host });
84
113
  }
@@ -100,12 +129,22 @@ function disableCodexPlugin(pluginKey) {
100
129
  const original = fs.readFileSync(configPath, "utf8");
101
130
  const header = `[plugins."${pluginKey}"]`;
102
131
  let next;
103
- if (original.includes(header)) {
104
- const idx = original.indexOf(header);
105
- const after = original.slice(idx);
106
- const replacedAfter = after.replace(/(\[plugins\."[^"]+"\]\s*\n)enabled\s*=\s*true/, "$1enabled = false");
107
- if (replacedAfter === after) return false; // 이미 false거나 형태가 다름
108
- 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);
109
148
  } else {
110
149
  next = `${original.trimEnd()}\n\n${header}\nenabled = false\n`;
111
150
  }
@@ -120,9 +159,21 @@ function disableCodexPlugin(pluginKey) {
120
159
  * 데스크탑 runtime-doctor.ts 의 runRuntimeDoctor 와 동일 계약.
121
160
  */
122
161
  function runRuntimeDoctor(errorMessage) {
123
- const { kind, hosts } = classifyFailure(errorMessage);
162
+ const { kind, hosts, badServer } = classifyFailure(errorMessage);
124
163
  const actions = [];
125
164
 
165
+ if (kind === "codex-config-invalid") {
166
+ // 진단만 한다(자동 수리 안 함): 원격 MCP 항목이라 안전한 무손실 수리가 없고,
167
+ // 사용자가 어느 항목을 어떻게 고칠지 알아야 한다. 명확한 조치를 안내한다.
168
+ const where = badServer ? `[mcp_servers.${badServer}]` : "일부 [mcp_servers.*] 항목";
169
+ return {
170
+ kind,
171
+ summary: `codex의 ~/.codex/config.toml ${where} 이(가) 잘못돼 codex가 기동하지 못합니다(예: stdio 서버에 url 키). 다른 런타임(--runtime claude-code)으로 우회하거나 그 항목을 고치세요.`,
172
+ repaired: false,
173
+ actions,
174
+ };
175
+ }
176
+
126
177
  if (kind === "mcp-oauth-unauthenticated") {
127
178
  const hitList = findOauthPluginsByHost(hosts);
128
179
  let repairedAny = false;
@@ -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)",
@@ -107,18 +137,23 @@ const STRINGS = {
107
137
  "help.setup": "run language, runtime, and permission setup again",
108
138
  "help.cwd": "show or change the working folder",
109
139
  "help.memory": "show the memory being injected",
140
+ "help.careerGraph": "show/add source refs for the Career Graph routing index",
110
141
  "help.ontology": "turn on/list/add project ontology sources with short natural commands",
111
142
  "help.side": "ask a side question without saving it to chat context",
112
143
  "help.status": "show model/runtime, agent, permission, and directory",
113
144
  "help.multimodal": "show/set image, video, and audio fallback providers",
114
145
  "help.import": "import a local folder (agent or team)",
115
146
  "help.market": "browse/install marketplace agents",
116
- "help.install": "install a cloud agent by slug",
117
147
  "help.clear": "clear the chat and redraw",
118
148
  "help.storm": "run a force-robust Stormbreaker pipeline on a goal",
119
149
  "help.build": "build/repair/package an agent or team (Hephaestus)",
120
150
  "help.route": "preview which agent/pipeline would take a request",
121
151
  "help.research": "run the Research Engine (search/gather/read)",
152
+ "help.search": "discover agents in the Hub",
153
+ "help.install": "install an agent from the Hub by slug",
154
+ "help.network": "decompose a request into an A2A task force",
155
+ "help.browser": "real browser execution hardpoint",
156
+ "help.connect": "wire Telegram / platforms to an agent team",
122
157
  "help.swarm": "fan out an emergent agent swarm on a goal",
123
158
  "help.doctor": "check runtimes and data",
124
159
  "help.compact": "drop older transcript turns and keep the latest context",
@@ -143,6 +178,31 @@ const STRINGS = {
143
178
  "help.tab": "autocomplete commands, agents, runtimes, paths",
144
179
  "help.arrows": "browse persisted input history",
145
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",
146
206
  "compact.noop": "context is already compact (%s message(s))",
147
207
  "compact.done": "compacted context: %s → %s message(s)",
148
208
  // onboarding wizard
@@ -153,9 +213,9 @@ const STRINGS = {
153
213
  "wiz.runtimeInstalled": "installed",
154
214
  "wiz.runtimeMissing": "not installed",
155
215
  "wiz.permQ": "How much should agents be allowed to do by default?",
156
- "wiz.permRead": "read — look only (no changes)",
157
- "wiz.permWrite": "write — read + create/edit files (recommended)",
158
- "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",
159
219
  "wiz.pick": "Enter a number › ",
160
220
  "wiz.saved": "All set. You can change any of this later with /runtime, /permission.",
161
221
  "wiz.changeLang": "Tip: re-run setup anytime with agentlas setup",
@@ -164,6 +224,14 @@ const STRINGS = {
164
224
  "banner.help": "명령 보기",
165
225
  "banner.quit": "종료",
166
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": "권한",
167
235
  "picker.agents": "에이전트",
168
236
  "picker.companies": "회사",
169
237
  "picker.none": "(아직 없음 — Agentlas 앱에서 에이전트를 설치하거나 /import <경로>)",
@@ -180,6 +248,15 @@ const STRINGS = {
180
248
  "stalled": "응답이 지연되어 중단했습니다 (idle timeout)",
181
249
  "thinkingWith": "%s로 생각 중",
182
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": "자동 수리 완료 — 같은 요청을 한 번 다시 시도합니다.",
183
260
  "noKey": "%s API 키가 없습니다. 앱 설정 → BYOK에서 등록하거나 /runtime으로 전환하세요.",
184
261
  "runtimeSet": "런타임 → %s",
185
262
  "runtimeNotInstalled": "%s CLI가 설치돼 있지 않습니다.",
@@ -193,6 +270,10 @@ const STRINGS = {
193
270
  "permSet": "권한 → %s",
194
271
  "permUsage": "사용법: /permission read|write|full",
195
272
  "permCurrent": "현재 권한: %s",
273
+ "permCycleHint": "Shift-Tab으로 변경",
274
+ "permCycleConfirm": "권한 → %s",
275
+ "permFullArm": "무제한 권한은 승인과 샌드박스를 우회합니다 — 5초 안에 Shift-Tab을 다시 눌러 확인",
276
+ "permFullConfirm": "이 세션에서만 무제한 권한 켜짐 — 승인과 샌드박스를 우회함",
196
277
  "sideUsage": "사용법: /side <질문>",
197
278
  "sideNeedsSubject": "먼저 에이전트를 고르거나, 그냥 할 일을 입력해서 Agentlas가 자동 라우팅하게 하세요.",
198
279
  "sideStart": "사이드 질문입니다. 이 답변은 메인 대화 맥락에 저장하지 않습니다.",
@@ -212,8 +293,10 @@ const STRINGS = {
212
293
  "market.loggedIn": "클라우드 세션: 로그인됨",
213
294
  "market.loggedOut": "클라우드 세션: 로그아웃 — 클라우드 설치는 앱에서 로그인(또는 AGENTLAS_SESSION) 필요",
214
295
  "mcp.none": "(설정된 MCP 서버 없음 — 앱에서 추가)",
215
- "mcp.wired": "write/full 턴에 %s개 서버 연결됨 (playwright 포함)",
216
- "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 도구를 주입하지 않음",
217
300
  "help.mcp": "설정된 MCP 서버와 연결 상태 보기",
218
301
  "resume.title": "이어하기 — 최근 세션",
219
302
  "resume.none": "(저장된 세션 없음 — 각 턴 후 저장됩니다)",
@@ -239,6 +322,13 @@ const STRINGS = {
239
322
  "multimodal.set": "멀티모달 %s → %s",
240
323
  "multimodal.usage": "/multimodal set <image|video|audio> <provider-id> 로 변경",
241
324
  "help.talk": "현재 에이전트/회사와 대화 — 스트리밍 + 툴",
325
+ "help.help": "명령·단축키·주요 사용법 보기",
326
+ "help.title": "도움말",
327
+ "help.intro": "런타임·권한·파일·셸·기록을 직접 통제하며 로컬 에이전트를 실행합니다.",
328
+ "help.commands": "명령",
329
+ "skills.title": "스킬",
330
+ "permissions.title": "권한",
331
+ "permissions.current": "현재",
242
332
  "help.skills": "slash 명령 스킬 목록",
243
333
  "help.agents": "설치된 에이전트 목록",
244
334
  "help.team": "에이전트별 LLM 보기/지정 (능력 기반 자동)",
@@ -252,18 +342,23 @@ const STRINGS = {
252
342
  "help.setup": "언어·런타임·권한 설정 다시 실행",
253
343
  "help.cwd": "작업 폴더 보기/변경",
254
344
  "help.memory": "주입되는 메모리 보기",
345
+ "help.careerGraph": "Career Graph 라우팅 색인의 원본 경로 보기/등록",
255
346
  "help.ontology": "짧은 자연어로 프로젝트 온톨로지 켜기/목록/자료 등록",
256
347
  "help.side": "메인 대화 맥락에 저장하지 않는 사이드 질문",
257
348
  "help.status": "모델/런타임, 에이전트, 권한, 작업 폴더 보기",
258
349
  "help.multimodal": "이미지·영상·음성 fallback provider 보기/변경",
259
350
  "help.import": "로컬 폴더(에이전트/팀) 임포트",
260
351
  "help.market": "마켓플레이스 에이전트 보기/설치",
261
- "help.install": "slug로 클라우드 에이전트 설치",
262
352
  "help.clear": "대화 비우고 다시 그리기",
263
353
  "help.storm": "목표를 Stormbreaker 견고 파이프라인으로 실행",
264
354
  "help.build": "에이전트/팀 빌드·수리·패키징 (Hephaestus)",
265
355
  "help.route": "요청이 어떤 에이전트로 라우팅되는지 미리보기",
266
356
  "help.research": "Research Engine 실행 (search/gather/read)",
357
+ "help.search": "Hub에서 에이전트 발견",
358
+ "help.install": "slug로 Hub 에이전트 설치",
359
+ "help.network": "요청을 A2A 태스크포스로 분해",
360
+ "help.browser": "실제 브라우저 실행 하드포인트",
361
+ "help.connect": "Telegram/플랫폼을 에이전트 팀에 연결",
267
362
  "help.swarm": "목표에 emergent 에이전트 스웜 전개",
268
363
  "help.doctor": "런타임/데이터 점검",
269
364
  "help.compact": "오래된 대화 맥락을 줄이고 최근 맥락 유지",
@@ -287,6 +382,31 @@ const STRINGS = {
287
382
  "help.tab": "명령·에이전트·런타임·경로 자동완성",
288
383
  "help.arrows": "세션 간 저장된 입력 기록 탐색",
289
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": "점검",
290
410
  "compact.noop": "이미 충분히 compact 상태입니다 (%s개 메시지)",
291
411
  "compact.done": "맥락 compact 완료: %s → %s개 메시지",
292
412
  "wiz.welcome": "Agentlas에 오신 걸 환영합니다 — 먼저 설정할게요.",
@@ -296,9 +416,9 @@ const STRINGS = {
296
416
  "wiz.runtimeInstalled": "설치됨",
297
417
  "wiz.runtimeMissing": "미설치",
298
418
  "wiz.permQ": "에이전트가 기본적으로 어디까지 할 수 있게 할까요?",
299
- "wiz.permRead": "read — 보기만 (변경 없음)",
300
- "wiz.permWrite": "write — 읽기 + 파일 생성/편집 (권장)",
301
- "wiz.permFull": "full — 명령 포함 전부",
419
+ "wiz.permRead": "read — 조회만; 런타임 read-only/plan",
420
+ "wiz.permWrite": "write — 런타임 샌드박스 안에서 작업 공간 편집; 외부 MCP 없음 (권장)",
421
+ "wiz.permFull": "full — 무제한; 런타임 승인과 샌드박스 우회",
302
422
  "wiz.pick": "번호 입력 › ",
303
423
  "wiz.saved": "완료. 나중에 /runtime, /permission으로 언제든 바꿀 수 있어요.",
304
424
  "wiz.changeLang": "팁: 언제든 agentlas setup 으로 다시 설정",