agentlas 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -6
- package/bin/agentlas.cjs +55 -8
- package/engine/agentlas-api-agent.cjs +1 -1
- package/engine/agentlas-banner.cjs +40 -56
- package/engine/agentlas-capabilities.cjs +3 -0
- package/engine/agentlas-cloud-runtime.cjs +65 -11
- package/engine/agentlas-composer.cjs +112 -44
- package/engine/agentlas-doctor.cjs +40 -12
- package/engine/agentlas-i18n.cjs +136 -12
- package/engine/agentlas-input.cjs +118 -19
- package/engine/agentlas-native-host.cjs +381 -83
- package/engine/agentlas-parity.cjs +315 -45
- package/engine/agentlas-permissions.cjs +90 -0
- package/engine/agentlas-repl.cjs +239 -70
- package/engine/agentlas-tasks.cjs +111 -0
- package/engine/agentlas-tools.cjs +174 -12
- package/engine/agentlas-ui.cjs +352 -23
- package/engine/agentlas.cjs +2819 -351
- package/engine/semver.cjs +64 -0
- package/package.json +1 -1
- package/test/bootstrap-race.cjs +47 -0
- package/test/capture-runtime-guard.cjs +122 -0
- package/test/cloud-asset-restore.cjs +423 -0
- package/test/cloud-cas-client.cjs +333 -0
- package/test/cloud-owner-restore.cjs +183 -0
- package/test/cloud-runtime-paths.cjs +40 -0
- package/test/cloud-save-publish.cjs +453 -0
- package/test/credential-env-regression.cjs +52 -0
- package/test/login-loopback-security.cjs +115 -0
- package/test/mcp-config-isolation.cjs +36 -0
- package/test/permission-mapping.cjs +180 -0
- package/test/route-regression.cjs +121 -0
- package/test/run-api-regression.cjs +322 -0
- package/test/runtime-env-protection.cjs +45 -0
- package/test/semver-precedence.cjs +39 -0
- package/test/smoke.sh +20 -0
- package/test/sqlite-driver-probe.cjs +22 -0
- package/test/terminal-ui-regression.cjs +472 -0
- package/test/timeout-regression.cjs +218 -0
- package/test/tool-workspace-boundary.cjs +165 -0
- package/test/update-safety.cjs +376 -0
package/README.md
CHANGED
|
@@ -14,6 +14,17 @@ agents and teams, build new ones, and run the full Agentlas OS surface
|
|
|
14
14
|
(`build` · `search` · `install` · `storm` · `network` · …) from one command.
|
|
15
15
|
Claude Code style, standalone: **no desktop app required.**
|
|
16
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.
|
|
27
|
+
|
|
17
28
|
```sh
|
|
18
29
|
npm install -g agentlas
|
|
19
30
|
agentlas
|
|
@@ -66,14 +77,30 @@ agentlas chats [n] # 최근 대화 목록
|
|
|
66
77
|
**에이전트 & 허브** (Agentlas OS 표면)
|
|
67
78
|
```sh
|
|
68
79
|
agentlas search "할 일" # Hub에서 에이전트 발견 (hep-search)
|
|
69
|
-
agentlas install <slug> # Hub 에이전트 설치
|
|
80
|
+
agentlas install <slug> # 공개 Hub 에이전트 설치 (hep-cloud)
|
|
70
81
|
agentlas build "요청" # 에이전트/팀 빌드·수리·패키징 (hep-build)
|
|
71
|
-
agentlas upload <경로> #
|
|
82
|
+
agentlas upload <경로> # 내 Agent Cloud에 비공개 저장 (hep-upload)
|
|
83
|
+
agentlas upload <경로> --visibility marketplace
|
|
84
|
+
# 호환 flag: Agentlas Hub 공개 발행
|
|
72
85
|
agentlas connect # Telegram/플랫폼 연결 (hep-connect)
|
|
73
86
|
agentlas import <폴더> # 로컬 에이전트/팀 임포트
|
|
74
87
|
agentlas list # 설치된 에이전트/회사 + 활성 런타임
|
|
75
88
|
```
|
|
76
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
|
+
|
|
77
104
|
**실행 엔진**
|
|
78
105
|
```sh
|
|
79
106
|
agentlas storm "목표" # 견고 파이프라인 라우팅→검증→실행 (Stormbreaker) [--research]
|
|
@@ -107,12 +134,26 @@ agentlas setup # 온보딩 다시 실행
|
|
|
107
134
|
```sh
|
|
108
135
|
agentlas hep <sub…> # 전체 Hephaestus 패스스루 (wizard·security·cards·ao·plugins…)
|
|
109
136
|
agentlas netadmin <sub> # 로컬 네트워크 관리 (init|status|reindex|bench)
|
|
110
|
-
agentlas cloud <sub> #
|
|
137
|
+
agentlas cloud <sub> # 자산 저장·공개·복원 (save|publish|package|list|restore|…)
|
|
111
138
|
```
|
|
112
139
|
|
|
113
140
|
공통 옵션: `--runtime claude-code|codex|gemini` · `--permission read|write|full`
|
|
114
141
|
REPL 안에서는 `/`로 명령 팔레트 (`/build` `/search` `/storm` `/network` …).
|
|
115
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 실행을 계획 항목처럼 꾸며내지 않는다.
|
|
156
|
+
|
|
116
157
|
## 동작 방식
|
|
117
158
|
|
|
118
159
|
런처(`bin/agentlas.cjs`)가 이 패키지의 `engine/`(정본)을 시스템 Node로 실행한다 —
|
|
@@ -149,6 +190,7 @@ rm ~/.local/bin/agentlas # install.sh 설치 시
|
|
|
149
190
|
|
|
150
191
|
## License
|
|
151
192
|
|
|
152
|
-
Apache-2.0 —
|
|
153
|
-
[
|
|
154
|
-
|
|
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
|
|
57
|
-
|
|
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 } =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|
|
@@ -4,6 +4,8 @@
|
|
|
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
10
|
// AGENTLAS wordmark (block letters). Rendered with a brand gradient across columns.
|
|
9
11
|
const WORDMARK = [
|
|
@@ -45,28 +47,9 @@ function shorten(p) {
|
|
|
45
47
|
// AGENTLAS wordmark, per-column brand gradient. Falls back to plain / compact.
|
|
46
48
|
function renderMascot(ui) {
|
|
47
49
|
const c = ui.c;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
// Narrow terminal → compact wordmark.
|
|
54
|
-
if (cols < 72) {
|
|
55
|
-
ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const g = (rgb, s) => `\x1b[1;38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${s}\x1b[0m`;
|
|
59
|
-
for (const rowStr of WORDMARK) {
|
|
60
|
-
let outLine = "";
|
|
61
|
-
const n = rowStr.length || 1;
|
|
62
|
-
for (let i = 0; i < rowStr.length; i++) {
|
|
63
|
-
const ch = rowStr[i];
|
|
64
|
-
if (ch === " ") { outLine += " "; continue; }
|
|
65
|
-
const idx = Math.min(GRADIENT.length - 1, Math.floor((i / n) * GRADIENT.length));
|
|
66
|
-
outLine += g(GRADIENT[idx], ch);
|
|
67
|
-
}
|
|
68
|
-
ui.line(outLine);
|
|
69
|
-
}
|
|
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)));
|
|
70
53
|
}
|
|
71
54
|
|
|
72
55
|
function stripAnsi(s) {
|
|
@@ -87,48 +70,49 @@ function row(ui, width, text) {
|
|
|
87
70
|
ui.line(ui.c.faint("│ ") + ui.c.text(fit(text, inner)) + ui.c.faint(" │"));
|
|
88
71
|
}
|
|
89
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
|
+
|
|
90
84
|
function renderStatusCard(ctx, opts = {}) {
|
|
91
85
|
const ui = ctx.ui;
|
|
92
86
|
const c = ui.c;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
};
|
|
101
97
|
ui.line("");
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
ui.
|
|
107
|
-
ui.line(c.faint("╭" + "─".repeat(width - 2) + "╮"));
|
|
108
|
-
row(ui, width, `model: ${runtime}`);
|
|
109
|
-
row(ui, width, `agent: ${subject}`);
|
|
110
|
-
row(ui, width, `directory: ${cwd}`);
|
|
111
|
-
row(ui, width, `permissions: ${permission}`);
|
|
112
|
-
ui.line(c.faint("╰" + "─".repeat(width - 2) + "╯"));
|
|
113
|
-
if (!opts.noTip) {
|
|
114
|
-
ui.line(
|
|
115
|
-
" " +
|
|
116
|
-
c.bold(c.text("Tip:")) +
|
|
117
|
-
c.dim(" Type ") +
|
|
118
|
-
c.faint("/help") +
|
|
119
|
-
c.dim(" for commands, ") +
|
|
120
|
-
c.faint("/status") +
|
|
121
|
-
c.dim(" for session state, ") +
|
|
122
|
-
c.faint("/exit") +
|
|
123
|
-
c.dim(" to quit."),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
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);
|
|
126
103
|
}
|
|
127
104
|
|
|
128
105
|
// Main splash. ctx = { ui, version, runtimeLabel, subjectLabel, permission, cwd }
|
|
129
106
|
function renderBanner(ctx) {
|
|
130
|
-
|
|
131
|
-
|
|
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("");
|
|
132
116
|
}
|
|
133
117
|
|
|
134
118
|
// runtime · subject · permission · working folder (no wordmark — used for /status).
|
|
@@ -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-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
167
|
-
return { status: "denied", path:
|
|
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
|
-
|
|
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:
|
|
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 = {
|
|
244
|
+
module.exports = {
|
|
245
|
+
buildManifest,
|
|
246
|
+
scanFolder,
|
|
247
|
+
runWizard,
|
|
248
|
+
compileBundle,
|
|
249
|
+
readAgentFile,
|
|
250
|
+
runFieldTest,
|
|
251
|
+
matches,
|
|
252
|
+
normalizeRequestedPath,
|
|
253
|
+
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* agentlas-composer: a raw-mode bottom input box (Claude Code / Hermes style).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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,94 @@ 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
|
+
// 연결 LLM 세션 사용량 상시 표시줄 — 입력박스 바로 아래에 항상 유지된다.
|
|
114
|
+
const usageText = typeof ctx.usage === "function" ? ctx.usage() : ctx.usage;
|
|
115
|
+
if (usageText) lines.push(c.faint(truncateWidth(String(usageText), w)));
|
|
116
|
+
if (ctx.confirmation) {
|
|
117
|
+
const prefix = ctx.confirmationTone === "danger" ? "! " : "✓ ";
|
|
118
|
+
const paint = ctx.confirmationTone === "danger" && c.paw ? c.paw : c.green || c.emerald || ((value) => value);
|
|
119
|
+
lines.push(paint(truncateWidth(prefix + ctx.confirmation, w)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rows = state.suggest || [];
|
|
123
|
+
rows.slice(0, 8).forEach((row, index) => {
|
|
124
|
+
const cmd = String(row.command || "").padEnd(16);
|
|
125
|
+
const desc = String(row.description || "");
|
|
126
|
+
const descRoom = Math.max(0, w - visWidth(" " + cmd + " "));
|
|
127
|
+
const clippedDesc = truncateWidth(desc, descRoom);
|
|
128
|
+
const label = " " + cmd + " " + clippedDesc;
|
|
129
|
+
lines.push(index === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + " " + c.dim(clippedDesc));
|
|
130
|
+
});
|
|
131
|
+
if (rows.length) lines.push(c.faint(" " + i18n.t(ctx.lang || "en", "palette.controls")));
|
|
132
|
+
|
|
133
|
+
return { lines, curCol: visWidth(prefix) + visWidth(state.buf.slice(start, state.cur)) };
|
|
134
|
+
}
|
|
135
|
+
|
|
46
136
|
function createComposer(opts) {
|
|
47
137
|
const out = opts.stream || process.stdout;
|
|
48
138
|
const inp = opts.input || process.stdin;
|
|
@@ -61,45 +151,7 @@ function createComposer(opts) {
|
|
|
61
151
|
|
|
62
152
|
// Build the rendered block (array of lines) + the cursor target column on the input line.
|
|
63
153
|
function frame(state, ctx) {
|
|
64
|
-
|
|
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 };
|
|
154
|
+
return buildComposerFrame(state, ctx, c, boxWidth());
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
function render(state, ctx) {
|
|
@@ -172,6 +224,14 @@ function createComposer(opts) {
|
|
|
172
224
|
function onKey(str, key) {
|
|
173
225
|
key = key || {};
|
|
174
226
|
const name = key.name;
|
|
227
|
+
const shiftTab = name === "tab" && key.shift;
|
|
228
|
+
|
|
229
|
+
if (!shiftTab && ctx.onPermissionCycleCancel) {
|
|
230
|
+
const hadConfirmation = Boolean(ctx.confirmation);
|
|
231
|
+
const next = ctx.onPermissionCycleCancel();
|
|
232
|
+
if (next && typeof next === "object") Object.assign(ctx, next);
|
|
233
|
+
if (hadConfirmation && !ctx.confirmation) draw();
|
|
234
|
+
}
|
|
175
235
|
|
|
176
236
|
if (key.ctrl && name === "c") {
|
|
177
237
|
if (state.buf.length) { ctrlc = 0; return setBuf("", 0); }
|
|
@@ -212,6 +272,14 @@ function createComposer(opts) {
|
|
|
212
272
|
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
273
|
return histNav(-1);
|
|
214
274
|
}
|
|
275
|
+
if (shiftTab) {
|
|
276
|
+
if (ctx.onCyclePermission) {
|
|
277
|
+
const next = ctx.onCyclePermission(ctx.permission);
|
|
278
|
+
if (next && typeof next === "object") Object.assign(ctx, next);
|
|
279
|
+
draw();
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
215
283
|
if (name === "tab") {
|
|
216
284
|
if (state.suggest.length) { const cmd = state.suggest[state.suggestSel].command; state.dismissed = cmd; return setBuf(cmd, cmd.length); }
|
|
217
285
|
if (ctx.complete) {
|
|
@@ -253,4 +321,4 @@ function createComposer(opts) {
|
|
|
253
321
|
return { read, setHistory: (h) => { history = (h || []).filter((x) => typeof x === "string"); } };
|
|
254
322
|
}
|
|
255
323
|
|
|
256
|
-
module.exports = { createComposer, visWidth };
|
|
324
|
+
module.exports = { createComposer, visWidth, buildComposerFrame, truncateWidth };
|