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
package/README.md CHANGED
@@ -1,7 +1,29 @@
1
1
  # agentlas
2
2
 
3
- **The Agentlas agent terminal.** Chat with your installed AI agents and teams
4
- from the terminal — Claude Code style. Standalone: no desktop app required.
3
+ ```
4
+ █████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ █████╗ ███████╗
5
+ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██║ ██╔══██╗██╔════╝
6
+ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██║ ███████║███████╗
7
+ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██╔══██║╚════██║
8
+ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████╗██║ ██║███████║
9
+ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝
10
+ ```
11
+
12
+ **The operating system for agents — in your terminal.** Chat with your AI
13
+ agents and teams, build new ones, and run the full Agentlas OS surface
14
+ (`build` · `search` · `install` · `storm` · `network` · …) from one command.
15
+ Claude Code style, standalone: **no desktop app required.**
16
+
17
+ Agentlas Terminal is the already-shipped independent terminal product. It is
18
+ not a Desktop `cli/` mirror and does not require the Desktop app to run.
19
+
20
+ > **We are Agent Trust. Your agent is not a program. It is an asset. — Agentlas —**
21
+
22
+ Agent Trust means owner-scoped, portable, inspectable, and restorable agent
23
+ packages. It is a product principle, not a claim of regulated financial or
24
+ legal trust services. Private Agent Cloud stores owned packages; this existing
25
+ Terminal verifies and runs their local execution copies through supported
26
+ runtimes.
5
27
 
6
28
  ```sh
7
29
  npm install -g agentlas
@@ -40,31 +62,97 @@ Windows(미검증): `powershell -ExecutionPolicy Bypass -File install.ps1`
40
62
  ## 사용
41
63
 
42
64
  ```sh
43
- agentlas # 대화형 TUI (온보딩에이전트 피커 → 스트리밍 REPL)
44
- agentlas list # 설치된 에이전트/회사 + 활성 런타임
45
- agentlas <agent> # 해당 에이전트와 대화형 세션
46
- agentlas run <agent> "프롬프트" # 1회 실행 (prompt 없으면 stdin)
47
- agentlas firm <firm> "프롬프트" # 회사(팀) 실행
48
- agentlas cd <agent> # 에이전트 폴더 경로 → cd "$(agentlas cd seo)" && claude
65
+ agentlas # 대화형 TUI (워드마크 입력 → 스트리밍 REPL)
66
+ agentlas "할 일" # 자동 라우팅 1회 실행
67
+ ```
68
+
69
+ **대화 & 실행**
70
+ ```sh
71
+ agentlas <agent> # 해당 에이전트와 대화 (예: agentlas seo)
72
+ agentlas run [agent] "프롬프트" # 1회 실행 (agent 생략 시 자동 라우팅, prompt 없으면 stdin)
73
+ agentlas firm <firm> "프롬프트" # 회사(팀) CEO에 위임
74
+ agentlas chats [n] # 최근 대화 목록
75
+ ```
76
+
77
+ **에이전트 & 허브** (Agentlas OS 표면)
78
+ ```sh
79
+ agentlas search "할 일" # Hub에서 에이전트 발견 (hep-search)
80
+ agentlas install <slug> # 공개 Hub 에이전트 설치 (hep-cloud)
81
+ agentlas build "요청" # 에이전트/팀 빌드·수리·패키징 (hep-build)
82
+ agentlas upload <경로> # 내 Agent Cloud에 비공개 저장 (hep-upload)
83
+ agentlas upload <경로> --visibility marketplace
84
+ # 호환 flag: Agentlas Hub 공개 발행
85
+ agentlas connect # Telegram/플랫폼 연결 (hep-connect)
49
86
  agentlas import <폴더> # 로컬 에이전트/팀 임포트
50
- agentlas storm "목표" # Stormbreaker 파이프라인 (라우팅→검증→실행) [--research]
51
- agentlas swarm "목표" # emergent 에이전트 스웜 (병렬+블랙보드+종합) [--parallel N]
87
+ agentlas list # 설치된 에이전트/회사 + 활성 런타임
88
+ ```
89
+
90
+ **내 Agent Cloud 자산**
91
+ ```sh
92
+ agentlas cloud save <경로> # 소유자 전용 비공개 저장(공개 심사/라우팅 카드 없음)
93
+ agentlas cloud publish <경로> # Agentlas Hub에 명시적으로 공개 발행
94
+ agentlas cloud list # 로그인한 소유자의 비공개 패키지 조회
95
+ agentlas cloud restore <slug> # 전체 hash 검증 후 이 컴퓨터에 exact snapshot 복원
96
+ ```
97
+
98
+ 비공개 저장도 업로드 전 로컬에서 비밀값, 안전하지 않은 경로, 파일별 hash와
99
+ 전체 package hash를 검사한다. 공개 Hub 발행에만 라우팅 카드와 공개 검토가 붙는다.
100
+
101
+ `agentlas cloud install <slug>`은 기존 호환 명령이며 공개 Hub 설치다. 비공개
102
+ Agent Cloud 소유자 복원은 반드시 `agentlas cloud restore <slug>`를 사용한다.
103
+
104
+ **실행 엔진**
105
+ ```sh
106
+ agentlas storm "목표" # 견고 파이프라인 라우팅→검증→실행 (Stormbreaker) [--research]
107
+ agentlas swarm "목표" # emergent 에이전트 스웜 [--parallel N]
108
+ agentlas network "요청" # A2A 태스크포스로 분해 (hep-network)
109
+ agentlas call "a,b" "컨텍스트" # 지정 에이전트 호출 (hep-call)
110
+ agentlas browser # 실제 브라우저 하드포인트 (hep-browser)
111
+ agentlas route "요청" # 라우팅 미리보기 (실행 없음)
112
+ ```
113
+
114
+ **지식 & 리서치**
115
+ ```sh
116
+ agentlas research <sub> # Research Engine (status|gather|search|read|plan)
117
+ agentlas ontology <sub> # 프로젝트 지식 (status|list|add)
118
+ agentlas journal <sub> # Stormbreaker 저널 (status|verify|repair|gate)
119
+ ```
120
+
121
+ **계정 & 운영**
122
+ ```sh
52
123
  agentlas login | logout | whoami # Agentlas Cloud 로그인 (브라우저 플로우)
53
- agentlas automation list|add|on|off|remove|runs # 자동화 등록/관리
54
- agentlas automation run <id> # 자동화 지금 1회 실행
55
- agentlas automation daemon # 상주 실행기 — 앱 없이 예약 자동화 실행 (앱과 리스 공유)
56
- agentlas cloud search "찾는 일" # 마켓플레이스 검색 (로그인 불필요)
57
- agentlas cloud install <slug> # 마켓플레이스에서 에이전트 설치
58
- agentlas usage # 로컬 사용 현황 (실행/메시지/자동화)
59
- agentlas telegram # 텔레그램 바인딩 현황 (읽기 전용)
60
- agentlas creds / env # 자격증명 · env
124
+ agentlas automation <sub> # list|add|on|off|remove|run <id>|runs|daemon (로컬 스케줄러)
125
+ agentlas creds <sub> · env # 자격증명 볼트 · env
61
126
  agentlas multimodal # 이미지/영상/음성 provider
127
+ agentlas usage · telegram · mcp # 사용 현황 · 텔레그램 · MCP 서버
62
128
  agentlas doctor # 런타임/데이터 점검
63
- agentlas setup # 온보딩(언어→런타임→권한) 다시 실행
64
- agentlas update # Desktop 최신 릴리스 확인/설치
129
+ agentlas update # npm 최신판 확인
130
+ agentlas setup # 온보딩 다시 실행
131
+ ```
132
+
133
+ **고급**
134
+ ```sh
135
+ agentlas hep <sub…> # 전체 Hephaestus 패스스루 (wizard·security·cards·ao·plugins…)
136
+ agentlas netadmin <sub> # 로컬 네트워크 관리 (init|status|reindex|bench)
137
+ agentlas cloud <sub> # 자산 저장·공개·복원 (save|publish|package|list|restore|…)
65
138
  ```
66
139
 
67
140
  공통 옵션: `--runtime claude-code|codex|gemini` · `--permission read|write|full`
141
+ REPL 안에서는 `/`로 명령 팔레트 (`/build` `/search` `/storm` `/network` …).
142
+
143
+ 권한은 이름과 실제 런타임 실행 범위를 일치시킨다.
144
+
145
+ | Agentlas 권한 | Claude Code | Codex | Gemini CLI |
146
+ | --- | --- | --- | --- |
147
+ | `read` | `plan` | `read-only` sandbox | `plan` |
148
+ | `write` | `acceptEdits` | `workspace-write` sandbox | `auto_edit` |
149
+ | `full` | permission 검사 우회 | approval + sandbox 우회 | `yolo` |
150
+
151
+ `write`는 무제한 권한의 다른 이름이 아니다. 외부 상태를 바꿀 수 있는 MCP/Playwright
152
+ 도구는 `full` 턴에서만 주입한다. 입력창에서 `Shift-Tab`으로 권한을 순환할 수 있지만,
153
+ `write → full`은 5초 안에 두 번 연속 눌러야 하며 이 변경은 현재 세션에만 적용된다.
154
+ 실행 중 `Ctrl-T`는 Claude Todo/Task, Codex `todo_list`, Gemini `write_todos`가 실제로
155
+ 보낸 체크리스트만 열고 접는다. 일반 Bash/Read 실행을 계획 항목처럼 꾸며내지 않는다.
68
156
 
69
157
  ## 동작 방식
70
158
 
@@ -102,6 +190,7 @@ rm ~/.local/bin/agentlas # install.sh 설치 시
102
190
 
103
191
  ## License
104
192
 
105
- Apache-2.0 — engine sources are vendored from the public
106
- [agentlas-ai/agentlas-desktop](https://github.com/agentlas-ai/agentlas-desktop) repo
107
- (see `engine/ENGINE_META.json` for the exact version/commit).
193
+ Apache-2.0 — Agentlas Terminal is the independent terminal runtime for the
194
+ [Agentlas OS](https://github.com/agentlas-ai/Agentlas-OS) package contract. Its
195
+ `engine/` directory is maintained and released from this repository; it is not
196
+ a generated mirror of Agentlas Desktop.
package/bin/agentlas.cjs CHANGED
@@ -51,14 +51,41 @@ function dbPath() {
51
51
  // require.resolve가 아니라 실제 로드로 판별한다 — ABI가 깨진 better-sqlite3나
52
52
  // node:sqlite가 아직 없는/플래그가 필요한 Node(≤22.4 등)를 정확히 걸러낸다.
53
53
  function probeSqliteDriver() {
54
- try { require("better-sqlite3"); return "better-sqlite3"; } catch { /* absent or ABI-broken */ }
55
54
  try {
56
- const { DatabaseSync } = require("node:sqlite");
57
- if (DatabaseSync) return "node:sqlite";
55
+ const Database = require("better-sqlite3");
56
+ // Loading the JS wrapper alone does not prove that its native binding matches
57
+ // this Node ABI. Exercise the constructor before reporting it to --where.
58
+ const db = new Database(":memory:");
59
+ db.close();
60
+ return "better-sqlite3";
61
+ } catch { /* absent or ABI-broken */ }
62
+ try {
63
+ const { DatabaseSync } = loadNodeSqliteQuietly();
64
+ if (DatabaseSync) {
65
+ const db = new DatabaseSync(":memory:");
66
+ db.close();
67
+ return "node:sqlite";
68
+ }
58
69
  } catch { /* not available without a flag on this Node */ }
59
70
  return null;
60
71
  }
61
72
 
73
+ function loadNodeSqliteQuietly() {
74
+ const originalEmitWarning = process.emitWarning;
75
+ process.emitWarning = function agentlasSqliteWarningFilter(warning, ...args) {
76
+ const message = typeof warning === "string" ? warning : String((warning && warning.message) || warning || "");
77
+ const type = typeof args[0] === "string" ? args[0] : "";
78
+ if (type === "ExperimentalWarning" && /SQLite/i.test(message)) return;
79
+ if (/SQLite is an experimental feature/i.test(message)) return;
80
+ return originalEmitWarning.call(process, warning, ...args);
81
+ };
82
+ try {
83
+ return require("node:sqlite");
84
+ } finally {
85
+ process.emitWarning = originalEmitWarning;
86
+ }
87
+ }
88
+
62
89
  function openSqlite(p) {
63
90
  try {
64
91
  const Database = require("better-sqlite3");
@@ -69,7 +96,7 @@ function openSqlite(p) {
69
96
  if (major < 22) {
70
97
  throw new Error(`Node ${process.version} — better-sqlite3가 없으면 Node 22+ (node:sqlite)가 필요합니다.`);
71
98
  }
72
- const { DatabaseSync } = require("node:sqlite");
99
+ const { DatabaseSync } = loadNodeSqliteQuietly();
73
100
  const db = new DatabaseSync(p);
74
101
  return { exec: (sql) => db.exec(sql), close: () => db.close(), driver: "node:sqlite" };
75
102
  }
@@ -86,16 +113,34 @@ function bootstrapDbIfMissing() {
86
113
  }
87
114
  fs.mkdirSync(path.dirname(p), { recursive: true });
88
115
  const sql = fs.readFileSync(schemaFile, "utf8");
89
- const db = openSqlite(p);
116
+ // DB를 정식 경로에서 직접 만들면 두 첫 실행이 모두 exists=false를 본 뒤 한 프로세스의
117
+ // 실패 cleanup이 다른 프로세스의 정상 DB까지 지울 수 있다. 각자 같은 볼륨의 임시 DB를
118
+ // 완성하고 hard-link(EEXIST=다른 프로세스 승리)로만 정식 이름을 원자 획득한다.
119
+ const temp = `${p}.bootstrap-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
120
+ const db = openSqlite(temp);
90
121
  try {
91
122
  db.exec(sql);
92
123
  } catch (e) {
93
124
  db.close();
94
- try { fs.rmSync(p); } catch { /* leave partial for inspection */ }
125
+ try { fs.rmSync(temp, { force: true }); } catch { /* leave temp for inspection */ }
95
126
  throw new Error(`DB 부트스트랩 실패: ${e.message}`);
96
127
  }
97
128
  db.close();
98
- return { created: true, path: p };
129
+ let created = false;
130
+ try {
131
+ fs.linkSync(temp, p);
132
+ created = true;
133
+ } catch (e) {
134
+ if (!e || e.code !== "EEXIST") {
135
+ throw new Error(`DB 원자 부트스트랩 전환 실패: ${e && e.message ? e.message : e}`);
136
+ }
137
+ } finally {
138
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
139
+ try { fs.rmSync(temp + "-journal", { force: true }); } catch { /* noop */ }
140
+ try { fs.rmSync(temp + "-wal", { force: true }); } catch { /* noop */ }
141
+ try { fs.rmSync(temp + "-shm", { force: true }); } catch { /* noop */ }
142
+ }
143
+ return { created, path: p };
99
144
  }
100
145
 
101
146
  function main() {
@@ -173,4 +218,6 @@ function main() {
173
218
  });
174
219
  }
175
220
 
176
- main();
221
+ if (require.main === module) main();
222
+
223
+ module.exports = { bootstrapDbIfMissing, dbPath, userDataDir, probeSqliteDriver };
@@ -300,7 +300,7 @@ async function runOpenAILoop(req) {
300
300
  // ── Ollama (openai 스타일 tools, /api/chat) ──────────────
301
301
  async function runOllamaLoop(req) {
302
302
  const { ctx, ui } = req;
303
- const host = process.env.OLLAMA_HOST || "http://127.0.0.1:11434";
303
+ const host = (req.ctx.env && req.ctx.env.OLLAMA_HOST) || process.env.OLLAMA_HOST || "http://127.0.0.1:11434";
304
304
  const messages = req.messages.slice();
305
305
  if (!messages.length || messages[0].role !== "system") messages.unshift({ role: "system", content: req.system });
306
306
  const toolDefs = tools.openaiTools(ctx.permission);
@@ -1,17 +1,33 @@
1
1
  "use strict";
2
2
  /*
3
- * Agentlas terminal splash — small dinosaur mascot (Chrome-dino style) + wordmark + status.
3
+ * Agentlas terminal splash — AGENTLAS wordmark banner + status card.
4
4
  */
5
5
  const path = require("node:path");
6
6
  const os = require("node:os");
7
+ const permissions = require("./agentlas-permissions.cjs");
8
+ const { truncateWidth, visWidth } = require("./agentlas-composer.cjs");
7
9
 
8
- // Small T-Rex (side view, facing right) eye is ●.
9
- const DINO_ART = [
10
- " ▟████▙",
11
- " █● ▜█▙",
12
- " ▖ ████████",
13
- " ▜█▄▄▄▄▄███",
14
- " ▀▀▀█▌ █▌",
10
+ // AGENTLAS wordmark (block letters). Rendered with a brand gradient across columns.
11
+ const WORDMARK = [
12
+ " █████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ █████╗ ███████╗",
13
+ " ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██║ ██╔══██╗██╔════╝",
14
+ " ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██║ ███████║███████╗",
15
+ " ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██╔══██║╚════██║",
16
+ " ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████╗██║ ██║███████║",
17
+ " ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝",
18
+ ];
19
+
20
+ // Compact single-line wordmark for narrow terminals.
21
+ const WORDMARK_COMPACT = "▞▖ AGENTLAS";
22
+
23
+ // Brand gradient (emerald → green → lime → blue) applied left-to-right across the wordmark.
24
+ const GRADIENT = [
25
+ [110, 231, 183],
26
+ [52, 211, 153],
27
+ [16, 185, 129],
28
+ [45, 212, 191],
29
+ [56, 189, 248],
30
+ [147, 197, 253],
15
31
  ];
16
32
 
17
33
  function readVersion() {
@@ -28,17 +44,12 @@ function shorten(p) {
28
44
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
29
45
  }
30
46
 
31
- // Just the mascot lines (used by the onboarding wizard header).
47
+ // AGENTLAS wordmark, per-column brand gradient. Falls back to plain / compact.
32
48
  function renderMascot(ui) {
33
49
  const c = ui.c;
34
- if (!ui.enabled) {
35
- ui.line(" Agentlas");
36
- return;
37
- }
38
- for (let i = 0; i < DINO_ART.length; i++) {
39
- const row = DINO_ART[i];
40
- ui.line(" " + (i === 1 ? c.text(row).split("●").join(c.emerald("●")) : c.text(row)));
41
- }
50
+ // A one-line mark keeps first launch and setup compact. The old six-row wordmark
51
+ // remains exported for recordings/assets, but is no longer startup chrome.
52
+ ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)));
42
53
  }
43
54
 
44
55
  function stripAnsi(s) {
@@ -59,50 +70,54 @@ function row(ui, width, text) {
59
70
  ui.line(ui.c.faint("│ ") + ui.c.text(fit(text, inner)) + ui.c.faint(" │"));
60
71
  }
61
72
 
73
+ function sessionValues(ctx) {
74
+ const ui = ctx.ui;
75
+ return {
76
+ version: ctx.version || readVersion(),
77
+ subject: ctx.subjectLabel || ui.t("composer.autoroute"),
78
+ permission: permissions.copy(ctx.permission || "write", ui.lang).label,
79
+ runtime: ctx.runtimeLabel || "—",
80
+ cwd: ctx.cwd ? shorten(ctx.cwd) : process.cwd(),
81
+ };
82
+ }
83
+
62
84
  function renderStatusCard(ctx, opts = {}) {
63
85
  const ui = ctx.ui;
64
86
  const c = ui.c;
65
- const cols = ui.out.columns || 80;
66
- const width = Math.max(54, Math.min(cols - 2, 78));
67
- const version = ctx.version || readVersion();
68
- const subject = ctx.subjectLabel || "Pick an agent, choose a company, or type a task";
69
- const permission = ctx.permission || "write";
70
- const runtime = ctx.runtimeLabel || "(not configured)";
71
- const cwd = ctx.cwd ? shorten(ctx.cwd) : process.cwd();
72
-
87
+ const value = sessionValues(ctx);
88
+ const columns = ui.out.columns || 80;
89
+ const labels = [ui.t("status.runtime"), ui.t("status.agent"), ui.t("status.permission"), ui.t("status.directory")].map((label) => `${label}:`);
90
+ const labelCells = Math.min(Math.max(...labels.map((label) => visWidth(label))) + 2, Math.max(12, Math.floor(columns * 0.42)));
91
+ const line = (label, text) => {
92
+ const labelText = `${label}:`;
93
+ const padded = labelText + " ".repeat(Math.max(1, labelCells - visWidth(labelText)));
94
+ const room = Math.max(8, columns - 2 - visWidth(padded));
95
+ ui.line(" " + c.faint(padded) + c.text(truncateWidth(text, room)));
96
+ };
73
97
  ui.line("");
74
- ui.line(c.faint("╭" + "─".repeat(width - 2) + "╮"));
75
- row(ui, width, `>_ Agentlas${version ? " (v" + version + ")" : ""}`);
76
- row(ui, width, "");
77
- row(ui, width, `model: ${runtime}`);
78
- row(ui, width, `agent: ${subject}`);
79
- row(ui, width, `directory: ${cwd}`);
80
- row(ui, width, `permissions: ${permission}`);
81
- ui.line(c.faint("╰" + "─".repeat(width - 2) + "╯"));
82
- if (!opts.noTip) {
83
- ui.line(
84
- " " +
85
- c.bold(c.text("Tip:")) +
86
- c.dim(" Type ") +
87
- c.faint("/help") +
88
- c.dim(" for commands, ") +
89
- c.faint("/status") +
90
- c.dim(" for session state, ") +
91
- c.faint("/exit") +
92
- c.dim(" to quit."),
93
- );
94
- }
98
+ ui.rule(ui.t("status.title"));
99
+ line(ui.t("status.runtime"), value.runtime);
100
+ line(ui.t("status.agent"), value.subject);
101
+ line(ui.t("status.permission"), value.permission);
102
+ line(ui.t("status.directory"), value.cwd);
95
103
  }
96
104
 
97
105
  // Main splash. ctx = { ui, version, runtimeLabel, subjectLabel, permission, cwd }
98
106
  function renderBanner(ctx) {
99
- renderStatusCard(ctx);
100
- ctx.ui.line("");
107
+ const ui = ctx.ui;
108
+ const c = ui.c;
109
+ const value = sessionValues(ctx);
110
+ const room = Math.max(20, (ui.out.columns || 80) - 4);
111
+ ui.line("");
112
+ ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)) + (value.version ? c.faint(" v" + value.version) : "") + c.dim(" · " + ui.t("banner.product")));
113
+ ui.line(" " + c.text(truncateWidth(ui.t("banner.session", value.runtime, value.subject, value.permission), room)));
114
+ ui.line(" " + c.faint(truncateWidth(ui.t("banner.location", value.cwd), room)));
115
+ ui.line("");
101
116
  }
102
117
 
103
- // runtime · subject · permission · working folder
118
+ // runtime · subject · permission · working folder (no wordmark — used for /status).
104
119
  function renderStatus(ctx) {
105
- renderStatusCard(ctx, { noTip: true });
120
+ renderStatusCard(ctx, { noTip: true, noWordmark: true });
106
121
  }
107
122
 
108
- module.exports = { renderBanner, renderStatus, renderMascot, readVersion, shorten, DINO_ART, fit };
123
+ module.exports = { renderBanner, renderStatus, renderMascot, readVersion, shorten, WORDMARK, fit };
@@ -15,6 +15,7 @@ const RUNTIME_CAPS = {
15
15
  "claude-code": { code: true, image: false, label: "claude" },
16
16
  codex: { code: true, image: true, label: "codex" }, // Imagen
17
17
  gemini: { code: true, image: true, label: "gemini" }, // nano-banana
18
+ grok: { code: true, image: true, label: "grok" }, // Grok Imagine (generate_image/generate_video, 구독 키리스)
18
19
  anthropic: { code: true, image: false, label: "anthropic" },
19
20
  openai: { code: true, image: true, label: "openai" }, // gpt-image
20
21
  google: { code: true, image: true, label: "google" }, // imagen
@@ -22,6 +23,8 @@ const RUNTIME_CAPS = {
22
23
  upstage: { code: true, image: false, label: "solar" }, // Upstage Solar — Korean sovereign LLM (OpenAI-compatible)
23
24
  };
24
25
 
26
+ // NOTE: grok은 CAPS(멀티모달 능력 인지)만 등록 — 터미널 스폰 러너(RUNTIME_BIN)가 아직 없어
27
+ // CLI_KINDS에 넣으면 repl의 which(RUNTIME_BIN[k]) 탐지가 깨진다. 러너 추가 시 함께 확장할 것.
25
28
  const CLI_KINDS = ["claude-code", "codex", "gemini"];
26
29
 
27
30
  function capsFor(spec) {
@@ -78,7 +78,7 @@ function buildManifest(root, options = {}) {
78
78
  publicExportPolicy: "clean-copy",
79
79
  requiredRuntime: ["mcp-client"],
80
80
  license: "call-only-default",
81
- createdBy: "agentlas-desktop-setup-wizard",
81
+ createdBy: "agentlas-terminal-setup-wizard",
82
82
  };
83
83
  }
84
84
 
@@ -132,9 +132,46 @@ function loadManifest(root) {
132
132
  return JSON.parse(fs.readFileSync(path.join(path.resolve(root), "agentlas.json"), "utf8"));
133
133
  }
134
134
 
135
+ function globRegex(pattern) {
136
+ const source = String(pattern || "").replace(/\\/g, "/");
137
+ let out = "";
138
+ for (let i = 0; i < source.length; i += 1) {
139
+ const ch = source[i];
140
+ if (ch === "*") {
141
+ if (source[i + 1] === "*") {
142
+ i += 1;
143
+ // `**/` includes zero directory levels, so **/secrets/** also
144
+ // protects a root-level secrets directory.
145
+ if (source[i + 1] === "/") {
146
+ i += 1;
147
+ out += "(?:.*/)?";
148
+ } else {
149
+ out += ".*";
150
+ }
151
+ } else {
152
+ out += "[^/]*";
153
+ }
154
+ continue;
155
+ }
156
+ out += /[.+^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
157
+ }
158
+ return new RegExp(`^${out}$`, "i");
159
+ }
160
+
135
161
  function matches(filePath, pattern) {
136
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
137
- return new RegExp(`^${escaped}$`, "i").test(filePath);
162
+ return globRegex(pattern).test(String(filePath || "").replace(/\\/g, "/"));
163
+ }
164
+
165
+ function normalizeRequestedPath(requestedPath) {
166
+ const raw = typeof requestedPath === "string" ? requestedPath.replace(/\\/g, "/") : "";
167
+ if (!raw || raw.includes("\0") || raw.startsWith("/") || /^[A-Za-z]:\//.test(raw)) return null;
168
+ const parts = [];
169
+ for (const segment of raw.split("/")) {
170
+ if (!segment || segment === ".") continue;
171
+ if (segment === "..") return null;
172
+ parts.push(segment);
173
+ }
174
+ return parts.length > 0 ? parts.join("/") : null;
138
175
  }
139
176
 
140
177
  function compileBundle(root) {
@@ -160,17 +197,25 @@ function compileBundle(root) {
160
197
 
161
198
  function readAgentFile(root, requestedPath) {
162
199
  const manifest = loadManifest(root);
163
- if ((manifest.denyRead || []).some((pattern) => matches(requestedPath, pattern))) {
164
- return { status: "denied", path: requestedPath, reason: "Denied by agentlas.json denyRead.", redacted: true };
200
+ const safePath = normalizeRequestedPath(requestedPath);
201
+ if (!safePath) {
202
+ return { status: "denied", path: String(requestedPath || ""), reason: "Invalid or escaping path.", redacted: true };
203
+ }
204
+ if ((manifest.denyRead || []).some((pattern) => matches(safePath, pattern))) {
205
+ return { status: "denied", path: safePath, reason: "Denied by agentlas.json denyRead.", redacted: true };
165
206
  }
166
- if (!(manifest.allowRead || []).some((pattern) => matches(requestedPath, pattern))) {
167
- return { status: "denied", path: requestedPath, reason: "Path is not in agentlas.json allowRead.", redacted: false };
207
+ if (!(manifest.allowRead || []).some((pattern) => matches(safePath, pattern))) {
208
+ return { status: "denied", path: safePath, reason: "Path is not in agentlas.json allowRead.", redacted: false };
209
+ }
210
+ const base = path.resolve(root);
211
+ const abs = path.resolve(base, safePath);
212
+ if (abs !== base && !abs.startsWith(base + path.sep)) {
213
+ return { status: "denied", path: safePath, reason: "Path escapes the agent package.", redacted: true };
168
214
  }
169
- const abs = path.join(path.resolve(root), requestedPath);
170
- if (!fs.existsSync(abs)) return { status: "missing", path: requestedPath, reason: "File not found." };
215
+ if (!fs.existsSync(abs)) return { status: "missing", path: safePath, reason: "File not found." };
171
216
  const raw = fs.readFileSync(abs, "utf8");
172
217
  const content = redact(raw);
173
- return { status: "allowed", path: requestedPath, content, redacted: content !== raw };
218
+ return { status: "allowed", path: safePath, content, redacted: content !== raw };
174
219
  }
175
220
 
176
221
  function runFieldTest() {
@@ -196,4 +241,13 @@ function runFieldTest() {
196
241
  return report;
197
242
  }
198
243
 
199
- module.exports = { buildManifest, scanFolder, runWizard, compileBundle, readAgentFile, runFieldTest };
244
+ module.exports = {
245
+ buildManifest,
246
+ scanFolder,
247
+ runWizard,
248
+ compileBundle,
249
+ readAgentFile,
250
+ runFieldTest,
251
+ matches,
252
+ normalizeRequestedPath,
253
+ };