agentlas 1.0.47 → 1.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - `agentlas acp` — Agentlas as an Agent Client Protocol agent (Phase B-3). Zed,
6
+ JetBrains IDEs and other ACP clients can run the project controller (or an
7
+ installed agent via `_meta.agentlas.agent`) on the runtime you subscribe to,
8
+ with tool calls and streamed text projected onto ACP. `--info` prints the
9
+ registry-style descriptor. CLI-only (stdout is the wire).
10
+ - kimi, grok, and cursor run through the desktop core's generic ACP runner
11
+ (`engine/runtimes/acp-driver.cjs`) instead of being refused as "no v2
12
+ streaming driver". Same file, same tool-call vocabulary as Desktop; an old
13
+ core without `electron/runtime/acp.js` still refuses honestly with a repair
14
+ hint. `agentlas doctor` lists the newly executable runtimes.
15
+ - `npm test` is the smoke gate; the stale `.internal` copy of the architecture
16
+ sync script is gone (one canonical script under `scripts/`).
17
+
18
+
3
19
  ## 1.0.47 — 2026-08-14
4
20
 
5
21
  Antigravity and the shared runtime contracts now reach the independent
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /*
3
+ * acp/server — Agentlas as an ACP *agent* (PRD 2026-08-15 Phase B-3, plan-acp §2 (B)).
4
+ *
5
+ * `agentlas acp` speaks the Agent Client Protocol v1 on stdio (JSON-RPC 2.0, ndjson)
6
+ * as the AGENT side. Any ACP client — Zed, JetBrains IDEs, VS Code extensions,
7
+ * Emacs/neovim, other Agentlas surfaces — can now run Agentlas' project controller
8
+ * (the agent Desktop/Terminal already run for `agentlas run`) on the runtime the
9
+ * user subscribes to (Claude Code, Codex, Antigravity, …), inside their editor.
10
+ *
11
+ * BYOM stays true: Agentlas has no model of its own. A prompt turn is exactly an
12
+ * `agentlas run` turn (engine/sessions/session.cjs → native-host / ACP driver),
13
+ * projected onto ACP notifications:
14
+ * stream-delta → agent_message_chunk · tool → tool_call(completed) ·
15
+ * tool-result → tool_call_update · status → (dropped) · error → stopReason/refusal
16
+ * Nothing here executes anything a plain `agentlas run` would not; the same
17
+ * permission model (prefs.permission, default read for a remote client) applies.
18
+ *
19
+ * Wire choices, all measured against the registry matrix behaviour:
20
+ * protocolVersion 1 only (v2 is a draft) · authMethods [] (Agentlas login is a
21
+ * separate `agentlas login`; nothing to authenticate over ACP) ·
22
+ * session/cancel kills the child · unknown methods → -32601.
23
+ */
24
+ const readline = require("node:readline");
25
+ const path = require("node:path");
26
+
27
+ const PROTOCOL_VERSION = 1;
28
+
29
+ function pkgVersion() {
30
+ try { return require(path.join(__dirname, "..", "..", "package.json")).version || "0.0.0"; } catch { return "0.0.0"; }
31
+ }
32
+
33
+ function textOfPrompt(blocks) {
34
+ if (!Array.isArray(blocks)) return String(blocks || "");
35
+ return blocks.map((b) => {
36
+ if (!b || typeof b !== "object") return "";
37
+ if (b.type === "text") return String(b.text || "");
38
+ if (b.type === "resource" && b.resource && typeof b.resource.text === "string") return `\n[resource ${b.resource.uri || ""}]\n${b.resource.text}\n`;
39
+ if (b.type === "resource_link") return `[${b.name || "resource"}](${b.uri || ""})`;
40
+ return "";
41
+ }).join("");
42
+ }
43
+
44
+ /**
45
+ * Turn-execution boundary. Production uses engine/sessions (Orchestrator + project
46
+ * controller); contract tests inject a fake `runTurn` so no runtime is spawned.
47
+ *
48
+ * runTurn(ctx, {cwd, prompt, permission, runtimeKind?, sessionKey?, events}) →
49
+ * Promise<{ text, error, errorKind, session }>
50
+ * events: { onDelta(text), onTool(name, summary, id), onToolResult(text, ok, id), onStatus(text) }
51
+ * cancel(sessionKey) → void
52
+ */
53
+ function productionTurnRunner() {
54
+ const sessions = new Map(); // acp sessionId → { session, agent, runtime }
55
+ return {
56
+ async newSession(ctx, { cwd, runtimeKind, agentSlug }) {
57
+ const { projectCwd } = require("../project/paths.cjs");
58
+ const { resolveProjectController, withProjectControllerContext } = require("../project/controller.cjs");
59
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
60
+ const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
61
+ const { findAgent } = require("../agents/registry.cjs");
62
+ const permissions = require("../agentlas-permissions.cjs");
63
+ const db = ctx.db();
64
+ const workdir = cwd || projectCwd();
65
+ // Same ladder as `agentlas run [agent]`: an explicit installed agent
66
+ // (session/new _meta.agentlas.agent) is the advanced direct call; otherwise
67
+ // the folder's project controller — and no silent substitution when neither.
68
+ let agent = null;
69
+ let resolved = null;
70
+ if (agentSlug) {
71
+ agent = findAgent(db, String(agentSlug));
72
+ if (!agent) throw new Error(`unknown agent: ${agentSlug} (agentlas agents lists installed agents)`);
73
+ } else {
74
+ resolved = resolveProjectController(db, workdir);
75
+ agent = withProjectControllerContext(resolved.controller, resolved.project);
76
+ }
77
+ const runtime = resolveRuntimeForAgent({ db, prefs: ctx.prefs, explicit: runtimeKind || null, role: "orchestrator", agentId: agent.id });
78
+ // A remote editor client is unattended: never widen beyond the saved preference, default read.
79
+ const permission = permissions.normalize((ctx.prefs && ctx.prefs.permission) || "read");
80
+ ensureTerminalProjectForExecutionCli(db, workdir, permission, "terminal-acp");
81
+ return { agent, runtime, permission, cwd: workdir, project: resolved ? resolved.project : null };
82
+ },
83
+ async runTurn(ctx, spec, prompt, events) {
84
+ const { Orchestrator } = require("../sessions/orchestrator.cjs");
85
+ const orch = new Orchestrator({ db: ctx.db(), lang: ctx.lang });
86
+ const session = orch.spawn({ agent: spec.agent, runtime: spec.runtime, permission: spec.permission, cwd: spec.cwd, title: prompt.slice(0, 60) });
87
+ sessions.set(spec.acpSessionId, session);
88
+ const listener = (ev) => {
89
+ try {
90
+ if (ev.type === "stream-delta") events.onDelta(ev.text);
91
+ else if (ev.type === "tool") events.onTool(ev.name, ev.summary, ev.id);
92
+ else if (ev.type === "tool-result") events.onToolResult(ev.text, ev.ok, ev.id);
93
+ else if (ev.type === "status") events.onStatus(ev.text);
94
+ } catch { /* a projection failure must not break the turn */ }
95
+ };
96
+ session.on("event", listener);
97
+ try {
98
+ const res = await session.send(prompt);
99
+ return {
100
+ text: (res && (res.finalText || res.text)) || "",
101
+ error: session.status === "failed" ? (session.lastError || (res && res.error) || "failed") : (res && res.error) || null,
102
+ errorKind: res && res.errorKind,
103
+ cancelled: session.status === "killed",
104
+ };
105
+ } finally {
106
+ session.removeListener("event", listener);
107
+ sessions.delete(spec.acpSessionId);
108
+ }
109
+ },
110
+ cancel(acpSessionId) {
111
+ const s = sessions.get(acpSessionId);
112
+ if (s) { try { s.kill(); } catch { /* best effort */ } }
113
+ },
114
+ };
115
+ }
116
+
117
+ class AcpAgentServer {
118
+ /**
119
+ * @param {object} opts { ctx, input?, output?, runner? }
120
+ * ctx: agentlas command ctx (db(), prefs, lang) — may be null in tests with a fake runner
121
+ */
122
+ constructor(opts) {
123
+ this.ctx = opts.ctx || null;
124
+ this.input = opts.input || process.stdin;
125
+ this.output = opts.output || process.stdout;
126
+ this.runner = opts.runner || productionTurnRunner();
127
+ this.sessions = new Map(); // sessionId → spec
128
+ this.initialized = false;
129
+ this.nextSessionSeq = 1;
130
+ this.closed = false;
131
+ }
132
+
133
+ send(obj) {
134
+ if (this.closed) return;
135
+ this.output.write(JSON.stringify(obj) + "\n");
136
+ }
137
+ notify(method, params) { this.send({ jsonrpc: "2.0", method, params }); }
138
+ reply(id, result) { this.send({ jsonrpc: "2.0", id, result }); }
139
+ error(id, code, message, data) { this.send({ jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } }); }
140
+
141
+ start() {
142
+ const rl = readline.createInterface({ input: this.input, crlfDelay: Infinity });
143
+ rl.on("line", (line) => {
144
+ const trimmed = line.trim();
145
+ if (!trimmed) return;
146
+ let msg;
147
+ try { msg = JSON.parse(trimmed); } catch { return; }
148
+ if (!msg || typeof msg !== "object" || !msg.method) return;
149
+ void this.handle(msg);
150
+ });
151
+ rl.on("close", () => { this.closed = true; });
152
+ return new Promise((resolve) => rl.on("close", resolve));
153
+ }
154
+
155
+ async handle(msg) {
156
+ const { id, method } = msg;
157
+ const params = msg.params || {};
158
+ try {
159
+ switch (method) {
160
+ case "initialize": {
161
+ const requested = Number(params.protocolVersion);
162
+ if (requested !== PROTOCOL_VERSION) {
163
+ return this.error(id, -32602, `unsupported protocolVersion ${params.protocolVersion}; this agent speaks v${PROTOCOL_VERSION}`);
164
+ }
165
+ this.initialized = true;
166
+ return this.reply(id, {
167
+ protocolVersion: PROTOCOL_VERSION,
168
+ agentCapabilities: {
169
+ loadSession: false,
170
+ promptCapabilities: { image: false, audio: false, embeddedContext: true },
171
+ mcpCapabilities: { http: false, sse: false },
172
+ },
173
+ authMethods: [],
174
+ agentInfo: { name: "agentlas", title: "Agentlas", version: pkgVersion() },
175
+ });
176
+ }
177
+ case "authenticate":
178
+ return this.reply(id, {});
179
+ case "session/new": {
180
+ if (!this.initialized) return this.error(id, -32002, "initialize first");
181
+ const sessionId = `agentlas-${process.pid}-${this.nextSessionSeq++}`;
182
+ const meta = (params._meta && params._meta.agentlas) || params._meta || {};
183
+ const spec = await this.runner.newSession(this.ctx, { cwd: params.cwd, runtimeKind: meta.runtime, agentSlug: meta.agent });
184
+ spec.acpSessionId = sessionId;
185
+ this.sessions.set(sessionId, spec);
186
+ const rt = spec.runtime || {};
187
+ return this.reply(id, {
188
+ sessionId,
189
+ configOptions: [
190
+ {
191
+ id: "runtime", category: "mode", type: "select", name: "Runtime",
192
+ currentValue: rt.kind || "",
193
+ options: [{ value: rt.kind || "", name: `${rt.kind || "runtime"}${rt.model ? ` · ${rt.model}` : ""}` }],
194
+ },
195
+ ...(rt.model ? [{ id: "model", category: "model", type: "select", name: "Model", currentValue: rt.model, options: [{ value: rt.model, name: rt.model }] }] : []),
196
+ ],
197
+ _meta: {
198
+ agentlas: {
199
+ controller: spec.agent && (spec.agent.slug || spec.agent.name),
200
+ project: spec.project && spec.project.name,
201
+ permission: spec.permission,
202
+ runtime: rt.kind,
203
+ },
204
+ },
205
+ });
206
+ }
207
+ case "session/prompt": {
208
+ const spec = this.sessions.get(String(params.sessionId));
209
+ if (!spec) return this.error(id, -32602, "unknown sessionId");
210
+ const prompt = textOfPrompt(params.prompt).trim();
211
+ if (!prompt) return this.reply(id, { stopReason: "end_turn" });
212
+ const sid = spec.acpSessionId;
213
+ let streamed = false;
214
+ const events = {
215
+ onDelta: (text) => {
216
+ if (!text) return;
217
+ streamed = true;
218
+ this.notify("session/update", { sessionId: sid, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } } });
219
+ },
220
+ onTool: (name, summary, toolId) => {
221
+ this.notify("session/update", { sessionId: sid, update: {
222
+ sessionUpdate: "tool_call", toolCallId: String(toolId || `${name}-${Date.now()}`), title: [name, summary].filter(Boolean).join(" "),
223
+ kind: kindOfTool(name), status: "completed",
224
+ } });
225
+ },
226
+ onToolResult: (text, ok, toolId) => {
227
+ if (!toolId) return;
228
+ this.notify("session/update", { sessionId: sid, update: {
229
+ sessionUpdate: "tool_call_update", toolCallId: String(toolId), status: ok === false ? "failed" : "completed",
230
+ content: text ? [{ type: "content", content: { type: "text", text: String(text).slice(0, 4000) } }] : undefined,
231
+ } });
232
+ },
233
+ onStatus: () => {},
234
+ };
235
+ const res = await this.runner.runTurn(this.ctx, spec, prompt, events);
236
+ if (res && res.cancelled) return this.reply(id, { stopReason: "cancelled" });
237
+ if (res && res.error) {
238
+ const kind = String(res.errorKind || "");
239
+ if (kind === "refused") return this.reply(id, { stopReason: "refusal" });
240
+ if (kind === "auth") return this.error(id, -32000, `auth_required: ${res.error}`);
241
+ // Non-marker failures: surface the runtime's own words as the answer, then end the turn.
242
+ if (!streamed) events.onDelta(String(res.error));
243
+ return this.reply(id, { stopReason: "end_turn", _meta: { agentlas: { error: String(res.error), errorKind: kind || null } } });
244
+ }
245
+ if (!streamed && res && res.text) events.onDelta(res.text);
246
+ return this.reply(id, { stopReason: "end_turn" });
247
+ }
248
+ case "session/cancel": {
249
+ const spec = this.sessions.get(String(params.sessionId));
250
+ if (spec) this.runner.cancel(spec.acpSessionId);
251
+ return; // notification: no reply
252
+ }
253
+ case "session/set_mode":
254
+ case "session/set_model":
255
+ return this.error(id, -32601, `Method not found: ${method}`);
256
+ default:
257
+ if (id !== undefined) return this.error(id, -32601, `Method not found: ${method}`);
258
+ return;
259
+ }
260
+ } catch (e) {
261
+ if (id !== undefined) this.error(id, -32603, (e && e.message) || String(e));
262
+ }
263
+ }
264
+ }
265
+
266
+ function kindOfTool(name) {
267
+ const n = String(name || "").toLowerCase();
268
+ if (/^(bash|shell|exec|command|run)/.test(n)) return "execute";
269
+ if (/read|cat|view|open/.test(n)) return "read";
270
+ if (/edit|write|patch|apply|create/.test(n)) return "edit";
271
+ if (/delete|remove|rm/.test(n)) return "delete";
272
+ if (/move|rename/.test(n)) return "move";
273
+ if (/grep|search|find|glob/.test(n)) return "search";
274
+ if (/fetch|http|web|browse/.test(n)) return "fetch";
275
+ if (/think|plan/.test(n)) return "think";
276
+ return "other";
277
+ }
278
+
279
+ module.exports = { AcpAgentServer, PROTOCOL_VERSION, textOfPrompt, kindOfTool, productionTurnRunner };
@@ -26,7 +26,8 @@ const RUNTIME_CAPS = {
26
26
 
27
27
  // NOTE: grok은 CAPS(멀티모달 능력 인지)만 등록 — 터미널 스폰 러너(RUNTIME_BIN)가 아직 없어
28
28
  // CLI_KINDS에 넣으면 repl의 which(RUNTIME_BIN[k]) 탐지가 깨진다. 러너 추가 시 함께 확장할 것.
29
- const CLI_KINDS = ["claude-code", "codex", "agy", "gemini"];
29
+ // 목록의 정본은 runtimes/kinds.cjs 여기서는 네이티브 스폰 러너 4종만 가져다 쓴다.
30
+ const CLI_KINDS = require("./runtimes/kinds.cjs").NATIVE_CLI_KINDS;
30
31
 
31
32
  function capsFor(spec) {
32
33
  return RUNTIME_CAPS[spec] || { code: true, image: false, label: spec || "?" };
@@ -306,7 +306,8 @@ const SLASH_COMMAND_META = [
306
306
  { command: "/exit", description: "Quit Agentlas", category: "Session", usage: "/exit", detail: "Close the terminal session.", aliases: ["/quit"] },
307
307
  ];
308
308
  const SLASH_COMMANDS = SLASH_COMMAND_META.flatMap((entry) => [entry.command].concat(entry.aliases || []));
309
- const RUNTIME_SPECS = ["claude-code", "codex", "agy", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
309
+ // /runtime 받는 spec 전체(네이티브 CLI + API 백엔드) 정본은 runtimes/kinds.cjs.
310
+ const RUNTIME_SPECS = require("./runtimes/kinds.cjs").RUNTIME_SPECS;
310
311
  const PERM_LEVELS = ["read", "write", "full"];
311
312
 
312
313
  const HELP_KEY_BY_COMMAND = {
@@ -19,6 +19,7 @@ const fs = require("node:fs");
19
19
  const os = require("node:os");
20
20
  const path = require("node:path");
21
21
  const permissions = require("./agentlas-permissions.cjs");
22
+ const acpDriver = require("./runtimes/acp-driver.cjs");
22
23
  const i18n = require("./agentlas-i18n.cjs");
23
24
  const { wrapStdioServer } = require("./agentlas-mcp-env.cjs");
24
25
 
@@ -915,6 +916,11 @@ function handleGeminiLine(line, st, ui) {
915
916
  function runNativeTurn(req) {
916
917
  const { kind, bin, ui } = req;
917
918
  const cwd = req.cwd;
919
+ // kimi/grok/cursor: 손코딩 3번째 대신 벤더 코어의 공용 ACP 러너로 (PRD 2026-08-15 T-2).
920
+ // 같은 결과 계약({text, session, usage, error, errorKind, errorSource})으로 돌아온다.
921
+ if (acpDriver.ACP_KINDS.has(kind)) {
922
+ return acpDriver.runAcpTurn(req);
923
+ }
918
924
  let launchReq = req;
919
925
  if (
920
926
  kind === "gemini" && permissions.normalize(req.permission) === "full" &&
@@ -133,7 +133,8 @@ async function runOnboard({ ui, rl, helpers, persist }) {
133
133
  // Step 2 — default runtime
134
134
  ui.line("");
135
135
  printIndented(ui.t("wiz.runtimeQ"), c.bold);
136
- const cliKinds = ["claude-code", "codex", "agy", "gemini"];
136
+ // 위저드 선택지 = 네이티브 스폰 러너 4종 (정본 runtimes/kinds.cjs, 표시 순서 포함).
137
+ const cliKinds = require("./runtimes/kinds.cjs").NATIVE_CLI_KINDS;
137
138
  const rtOpts = [{ value: "auto", label: ui.t("wiz.runtimeAuto") }];
138
139
  for (const k of cliKinds) {
139
140
  const has = !!H.which(H.RUNTIME_BIN[k]);
@@ -24,13 +24,9 @@ const store = require("./store.cjs");
24
24
  // getAutomationExecutionContractState 동형) ──────────────────────────────
25
25
  // 손상된/미래 계약 값은 절대 조용히 넓혀 실행하지 않는다 — raw-row 게이트로
26
26
  // 무인 실행 직전에 검사한다(데스크탑 automation-scheduler.ts:538-549).
27
- const RUNTIME_KINDS = new Set([
28
- "claude-code", "codex", "agy", "gemini", "kimi", "grok", "cursor", "byok", "ollama", "lmstudio", "mlx",
29
- ]);
30
- const RUNTIME_BACKENDS = new Set([
31
- "anthropic", "openai", "google", "ollama", "lmstudio", "mlx", "upstage", "custom", "glm",
32
- "kimi", "deepseek", "minimax", "xai", "openrouter", "cursor",
33
- ]);
27
+ const { CONTRACT_RUNTIME_KINDS, CONTRACT_RUNTIME_BACKENDS } = require("../runtimes/kinds.cjs");
28
+ const RUNTIME_KINDS = new Set(CONTRACT_RUNTIME_KINDS);
29
+ const RUNTIME_BACKENDS = new Set(CONTRACT_RUNTIME_BACKENDS);
34
30
  const RUNTIME_SELECTION_KEYS = new Set(["kind", "backend", "source", "model", "longContext", "effort"]);
35
31
 
36
32
  function decodeRuntimeSelection(raw) {
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /*
3
+ * acp — run Agentlas as an Agent Client Protocol agent on stdio (PRD 2026-08-15 B-3).
4
+ *
5
+ * agentlas acp start the ACP v1 agent server (stdin/stdout are the wire)
6
+ * agentlas acp --info print the registry-style descriptor and exit
7
+ *
8
+ * Register in an ACP client (Zed settings.json example):
9
+ * "agent_servers": { "Agentlas": { "command": "agentlas", "args": ["acp"] } }
10
+ * JetBrains / other clients: same command + args. The client then runs Agentlas'
11
+ * project controller on the runtime you subscribe to — no keys leave your machine.
12
+ */
13
+ const { AcpAgentServer, PROTOCOL_VERSION } = require("../acp/server.cjs");
14
+
15
+ function descriptor() {
16
+ let version = "0.0.0";
17
+ try { version = require("../../package.json").version || version; } catch { /* keep */ }
18
+ return {
19
+ id: "agentlas",
20
+ name: "Agentlas",
21
+ version,
22
+ description: "Agentlas project controller over ACP — runs on the coding runtime you already subscribe to (Claude Code, Codex, Antigravity, ACP agents).",
23
+ protocolVersion: PROTOCOL_VERSION,
24
+ distribution: { npm: { package: `agentlas@${version}`, args: ["acp"] } },
25
+ authMethods: [],
26
+ capabilities: { loadSession: false, promptCapabilities: { image: false, audio: false, embeddedContext: true } },
27
+ };
28
+ }
29
+
30
+ async function run(ctx, args) {
31
+ if (args.includes("--info") || args.includes("--json")) {
32
+ ctx.out(JSON.stringify(descriptor(), null, 2));
33
+ return 0;
34
+ }
35
+ if (args.includes("--help") || args.includes("help")) {
36
+ ctx.out("Usage: agentlas acp [--info]\n Speak the Agent Client Protocol (v1) on stdio so an editor can run Agentlas as its agent.");
37
+ return 0;
38
+ }
39
+ // stdout is the protocol wire from here on: route everything human to stderr.
40
+ const server = new AcpAgentServer({ ctx, input: process.stdin, output: process.stdout });
41
+ await server.start();
42
+ return 0;
43
+ }
44
+
45
+ module.exports = { run, descriptor };
@@ -26,8 +26,8 @@ function usage(ko) {
26
26
  ? " 구독 계좌(A)와 렌트수익 계좌(B) 잔액을 표시합니다."
27
27
  : " Shows the subscription account (A) and rental-earnings account (B) balances.",
28
28
  ko
29
- ? " 크레딧은 Hub 에이전트 호출(공개 에이전트 3·팀 10, 활성 리스는 0)에 쓰입니다."
30
- : " Credits pay for Hub agent calls (public agent 3 · team 10; active leases cost 0).",
29
+ ? " 크레딧은 Hub 에이전트 호출에 작업당 쓰입니다(기본 공개 에이전트 3·팀 10, 크리에이터 가격이 있으면 그 가격). 활성 장기대여 중에는 0."
30
+ : " Credits pay per work order for Hub agent calls (base: public agent 3 · team 10; creator-priced agents charge their price). 0 while a day-lease is active.",
31
31
  ko
32
32
  ? " 참고: 렌트수익(B) → 구독(A) 전송은 Agentlas Desktop 에서만 가능합니다 (터미널 전송 명령 없음)."
33
33
  : " Note: earnings (B) → subscription (A) transfer is Desktop-only (no transfer command in the terminal).",
@@ -18,6 +18,10 @@ async function run(ctx, args) {
18
18
  ctx.err("✖ " + usageFor("call", ctx.lang));
19
19
  return 1;
20
20
  }
21
+ // 과금 사전 고지 — 가격은 서버가 청구 시 확정하므로 숫자를 지어내지 않는다.
22
+ ctx.out(ctx.lang !== "en"
23
+ ? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
24
+ : "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
21
25
  return create(ctx).cmdHep(["hep-call", ...args]);
22
26
  }
23
27
 
@@ -9,6 +9,7 @@ const fs = require("node:fs");
9
9
  const path = require("node:path");
10
10
  const { dbPath, userDataDir } = require("../core/paths.cjs");
11
11
  const { listAvailableCliRuntimes, activeRuntimeRow } = require("../runtimes/detect.cjs");
12
+ const { RUNTIME_BIN } = require("../runtimes/kinds.cjs");
12
13
  const { runtimeAuthEvidence } = require("../runtimes/auth-evidence.cjs");
13
14
  const { sharedRuntimeKind } = require("../runtimes/resolve.cjs");
14
15
  const { resolvedModelRole } = require("../runtimes/roles.cjs");
@@ -106,7 +107,7 @@ async function run(ctx, args = []) {
106
107
  // 가 아니라 경고다. 흔적 없음 = 미로그인 "가능성"이므로 단정하지 않는다.
107
108
  const evidence = runtimeAuthEvidence(activeKind);
108
109
  if (evidence.status === "none") {
109
- const bin = { "claude-code": "claude", codex: "codex", gemini: "gemini", agy: "agy" }[activeKind] || activeKind;
110
+ const bin = RUNTIME_BIN[activeKind] || activeKind;
110
111
  warn(
111
112
  en ? "active runtime" : "활성 런타임",
112
113
  en
@@ -34,6 +34,8 @@ const COMMANDS = {
34
34
  plugin: () => require("./plugin.cjs"),
35
35
  automation: () => require("./automation.cjs"),
36
36
  native: () => require("./native.cjs"),
37
+ // Agentlas as an ACP agent for editors (Zed, JetBrains, …) — PRD 2026-08-15 B-3.
38
+ acp: () => require("./acp.cjs"),
37
39
  multimodal: () => require("./multimodal.cjs"),
38
40
  document: () => require("./document.cjs"),
39
41
  workforce: () => require("./workforce.cjs"),
@@ -75,6 +75,11 @@ async function dispatch(ctx, command, args) {
75
75
  permission,
76
76
  `terminal-${command}`,
77
77
  ) || cwd;
78
+ // 과금 사전 고지 — 이 표면은 서버가 청구 시 확정하는 가격을 미리 모르므로
79
+ // 숫자를 지어내지 않고 사실만 말한다(공개 Hub 호출=크레딧 소모, 장기대여=0).
80
+ ctx.out(ctx.lang !== "en"
81
+ ? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
82
+ : "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
78
83
  const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out, uiInstance: ctx.uiInstance });
79
84
  const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
80
85
  cwd,
@@ -124,6 +129,12 @@ async function dispatch(ctx, command, args) {
124
129
  const cwd = projectCwd();
125
130
  const permission = resolvePermission(ctx);
126
131
  const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, permission, `terminal-${command}`) || cwd;
132
+ // 과금 사전 고지 — local 스코프는 원격 과금이 없다.
133
+ if (sourceScope !== "local") {
134
+ ctx.out(ko
135
+ ? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
136
+ : "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
137
+ }
127
138
  const { createLocalCoreHubTool } = require("../workforce/local-core-transport.cjs");
128
139
  const { createLocalCoreWorkforceRuntime } = require("../workforce/deps.cjs");
129
140
  const transport = createLocalCoreHubTool({ sourceScope, projectDir: projectPath, cwd });
@@ -178,6 +178,58 @@ function loadDesktopCore() {
178
178
  return _cache;
179
179
  }
180
180
 
181
+ /**
182
+ * 코어의 공용 ACP 러너(electron/runtime/acp.js)만 가볍게 로드한다 (PRD 2026-08-15 T-2).
183
+ * initStore·그래프 커널을 끌지 않고, electron 셰임과 네이티브 모듈 훅만 건 뒤 require 한다 —
184
+ * kimi/grok/cursor 실행에 DB가 필요 없기 때문. 코어가 없거나 acp.js가 없는 옛 코어면
185
+ * { error } 를 준다(정직한 부재; 조용한 폴백 금지).
186
+ */
187
+ let _acpCache = undefined;
188
+ function loadCoreAcpRuntime() {
189
+ if (_acpCache !== undefined) return _acpCache;
190
+ const root = findCoreRoot();
191
+ if (!root) { _acpCache = null; return null; }
192
+ const file = path.join(root, "electron", "runtime", "acp.js");
193
+ if (!fs.existsSync(file)) { _acpCache = { root, error: new Error("desktop core predates the ACP runner (no electron/runtime/acp.js)") }; return _acpCache; }
194
+ try {
195
+ installRetiredProjectProvisioningHook();
196
+ installNativeModuleHook();
197
+ const mod = require(file);
198
+ _acpCache = { root, module: mod };
199
+ } catch (e) {
200
+ _acpCache = { root, error: e };
201
+ }
202
+ return _acpCache;
203
+ }
204
+
205
+ /**
206
+ * 코어의 **공유 순수 모듈**(dist/shared/*)만 가볍게 로드한다 (예: "agent-control-blocks").
207
+ * shared/* 는 electron·DB 의존이 없는 순수 함수 모듈이라 initStore·그래프 커널·셰임 없이
208
+ * require 만 한다. 코어가 없으면 null, 그 모듈이 없는 옛 벤더 번들이면 { root, error } —
209
+ * 정직한 부재(조용한 폴백 금지는 호출부 계약; 표시 경로는 fail-open 해도 된다).
210
+ */
211
+ const _sharedCache = new Map();
212
+ function loadCoreShared(rel) {
213
+ const key = String(rel || "").replace(/\.js$/, "");
214
+ if (_sharedCache.has(key)) return _sharedCache.get(key);
215
+ let result = null;
216
+ const root = findCoreRoot();
217
+ if (root) {
218
+ const file = path.join(root, "shared", key + ".js");
219
+ if (!fs.existsSync(file)) {
220
+ result = { root, error: new Error(`desktop core has no shared/${key}.js (older vendor bundle)`) };
221
+ } else {
222
+ try {
223
+ result = { root, module: require(file) };
224
+ } catch (e) {
225
+ result = { root, error: e };
226
+ }
227
+ }
228
+ }
229
+ _sharedCache.set(key, result);
230
+ return result;
231
+ }
232
+
181
233
  /** 재사용 코어가 이 머신에서 가용한가(정직한 가부). */
182
234
  function desktopCoreAvailable() {
183
235
  const c = loadDesktopCore();
@@ -202,6 +254,8 @@ async function loadDesktopCoreAsync({ onNotice } = {}) {
202
254
  module.exports = {
203
255
  findCoreRoot,
204
256
  loadDesktopCore,
257
+ loadCoreAcpRuntime,
258
+ loadCoreShared,
205
259
  loadDesktopCoreAsync,
206
260
  desktopCoreAvailable,
207
261
  _test: { stripRetiredProjectProvisioningSource },
@@ -68,6 +68,26 @@ function loadDelegateParser() {
68
68
  return parseDelegationsLocal;
69
69
  }
70
70
 
71
+ /*
72
+ * 제어 블록 스트리퍼 정본 — 벤더 코어의 shared/agent-control-blocks
73
+ * (Desktop·Mobile 과 같은 규칙). 옛 벤더 번들이라 없으면 null — cleanFenceText 는
74
+ * 종전 규칙만으로 fail-open 한다(원문 파괴보다 마커 잔존이 낫다).
75
+ */
76
+ let _stripCanonical; // undefined=미시도 · null=정본 없음 · function=정본
77
+ function loadCanonicalStripper() {
78
+ if (_stripCanonical === undefined) {
79
+ try {
80
+ const loaded = require("../core/desktop-core.cjs").loadCoreShared("agent-control-blocks");
81
+ _stripCanonical = loaded && loaded.module && typeof loaded.module.stripAgentControlBlocks === "function"
82
+ ? loaded.module.stripAgentControlBlocks
83
+ : null;
84
+ } catch {
85
+ _stripCanonical = null;
86
+ }
87
+ }
88
+ return _stripCanonical;
89
+ }
90
+
71
91
  /** 표시/전달용 텍스트에서 제어 펜스를 제거한다(파싱만 — 부작용 없음). 실패 시 원문. */
72
92
  function cleanFenceText(text) {
73
93
  const raw = String(text || "");
@@ -79,8 +99,18 @@ function cleanFenceText(text) {
79
99
  }
80
100
  } catch { /* fences 미존재/파서 실패 — 원문 보존 */ }
81
101
  if (cleaned == null) cleaned = parseDelegationsLocal(raw).cleanedText;
102
+ // HTML 주석 봉투는 정본보다 먼저 — 정본이 헤딩만 도려내면 주석 껍데기가 남는다.
103
+ cleaned = cleaned.replace(/<!--\s*[\s\S]*?## Memory Events[\s\S]*?-->/gi, "");
104
+ // 정본 스트리퍼(settled 모드): 손코딩이 몰랐던 <<agentlas-ask>>·surface·followups·
105
+ // goal-complete 마커와 잔여 헤딩까지 Desktop 과 같은 규칙으로 지운다.
106
+ const strip = loadCanonicalStripper();
107
+ if (strip) {
108
+ try {
109
+ cleaned = strip(cleaned, { streaming: false });
110
+ } catch { /* 정본 실패 — 종전 규칙만으로 fail-open */ }
111
+ }
112
+ // 터미널 고유 정제(정본 범위 밖): 스킬 나레이션·오케스트레이터 헤더·판정 태그.
82
113
  return cleaned
83
- .replace(/<!--\s*[\s\S]*?## Memory Events[\s\S]*?-->/gi, "")
84
114
  .replace(/^\s*(?:사용 스킬|Skills used)\s*:[^\n.!?]*[.!?]?\s*(?:(?:이유|Reason)\s*:[^.!?]*[.!?]\s*)?/i, "")
85
115
  .replace(/^\s*I(?:'|’)m using (?:the )?`?[^`.\n]+`? skill because [^.]*\.\s*/i, "")
86
116
  .replace(/^\s*Execution mode:\s*`?appbridge-ceo-orchestrator`?[^\n]*\n?/gim, "")
@@ -485,6 +515,7 @@ async function runFirmTurn(p) {
485
515
  module.exports = {
486
516
  runFirmTurn,
487
517
  parseDelegationsLocal,
518
+ cleanFenceText,
488
519
  buildDelegateProtocol,
489
520
  matchTargets,
490
521
  resolveDivisions,
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/acp-driver — kimi · grok · cursor 를 위한 ACP 드라이버 (PRD 2026-08-15 T-2).
4
+ *
5
+ * 세 번째 손코딩이 아니다. Desktop 이 만든 공용 ACP 러너(electron/runtime/acp.js)를 벤더 코어에서
6
+ * 그대로 로드해 쓴다 — 데스크탑·터미널이 같은 파일 하나로 같은 런타임을 같은 품질로 돈다.
7
+ * (터미널이 손으로 미러링해 온 native-host 4종과 달리 드리프트가 구조적으로 없다.)
8
+ *
9
+ * 벤더 코어에 acp.js 가 없으면(옛 코어) — 종전 그대로 "드라이버 없음"으로 정직하게 거부한다.
10
+ * 조용히 다른 런타임으로 넘어가지 않는다.
11
+ */
12
+ const { loadCoreAcpRuntime } = require("../core/desktop-core.cjs");
13
+
14
+ // 정본(runtimes/kinds.cjs)의 ACP 3종에서 파생 — resolve.cjs의 ACP_CLI_KINDS와 같은 원소.
15
+ const ACP_KINDS = new Set(require("./kinds.cjs").ACP_CLI_KINDS);
16
+
17
+ /** 이 머신에서 ACP 드라이버를 쓸 수 있는가. { ok, reason?, module? } */
18
+ function acpDriverAvailability() {
19
+ const loaded = loadCoreAcpRuntime();
20
+ if (!loaded) return { ok: false, reason: "desktop core not available (run `agentlas doctor`)" };
21
+ if (loaded.error) return { ok: false, reason: loaded.error.message };
22
+ if (!loaded.module || typeof loaded.module.createAcpRunner !== "function") {
23
+ return { ok: false, reason: "desktop core exposes no createAcpRunner" };
24
+ }
25
+ return { ok: true, module: loaded.module };
26
+ }
27
+
28
+ function acpSpecFor(kind, mod) {
29
+ const spec = mod.ACP_AGENTS && mod.ACP_AGENTS[kind];
30
+ return spec || null;
31
+ }
32
+
33
+ /**
34
+ * native-host 계약으로 ACP 턴을 돈다.
35
+ * req = { kind, bin, prompt, systemPrompt, cwd, permission, ui, env, signal, model, locale }
36
+ * 반환: { text, session, usage, error, errorKind, errorSource }
37
+ */
38
+ async function runAcpTurn(req) {
39
+ const { kind, bin, ui } = req;
40
+ const avail = acpDriverAvailability();
41
+ if (!avail.ok) {
42
+ return { text: "", session: req.session || {}, error: `runtime '${kind}' has no ACP driver here: ${avail.reason}`, errorKind: "unsupported", errorSource: "marker" };
43
+ }
44
+ const mod = avail.module;
45
+ const spec = acpSpecFor(kind, mod);
46
+ if (!spec) {
47
+ return { text: "", session: req.session || {}, error: `runtime '${kind}' is not an ACP agent in this core`, errorKind: "unsupported", errorSource: "marker" };
48
+ }
49
+ const runner = mod.createAcpRunner(spec);
50
+ const locale = req.locale === "ko" ? "ko" : "en";
51
+ let streaming = false;
52
+ let lastText = "";
53
+ const events = {
54
+ onPartial: (full) => {
55
+ const text = String(full || "");
56
+ if (!streaming) { ui.streamStart(); streaming = true; }
57
+ const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
58
+ lastText = text;
59
+ if (delta) ui.streamDelta(delta);
60
+ },
61
+ onStatus: (status) => ui.status(status),
62
+ onTool: (name, args, result, id, isError) => {
63
+ ui.tool(name, args || "");
64
+ if (result) ui.toolResult(result, !isError);
65
+ },
66
+ onThinking: (phase) => { if (phase === "start") ui.status(locale === "ko" ? "생각 중..." : "thinking..."); },
67
+ onNotice: (notice) => { if (notice && notice.message) ui.status(notice.message); },
68
+ };
69
+ try {
70
+ const result = await runner({
71
+ systemPrompt: req.systemPrompt || "",
72
+ history: [],
73
+ userPrompt: req.prompt || "",
74
+ backendLabel: spec.label,
75
+ locale,
76
+ permission: req.permission,
77
+ runtimeSource: bin,
78
+ cwd: req.cwd,
79
+ env: req.env || process.env,
80
+ signal: req.signal,
81
+ ...(req.model ? { model: req.model } : {}),
82
+ }, events);
83
+ if (streaming) ui.streamEnd();
84
+ const session = { ...(req.session || {}), ...(result.sessionId ? { acpSessionId: result.sessionId } : {}) };
85
+ if (result.failure) {
86
+ return { text: result.text || "", session, usage: null, error: result.failure.message, errorKind: result.failure.kind, errorSource: result.failure.source };
87
+ }
88
+ return { text: result.text || "", session, usage: null, error: null };
89
+ } catch (e) {
90
+ if (streaming) ui.streamEnd();
91
+ const message = e && e.message ? e.message : String(e);
92
+ return { text: "", session: req.session || {}, usage: null, error: message, errorKind: /abort/i.test(message) ? "cancelled" : "exit", errorSource: "marker" };
93
+ }
94
+ }
95
+
96
+ module.exports = { ACP_KINDS, acpDriverAvailability, acpSpecFor, runAcpTurn };
@@ -7,20 +7,10 @@
7
7
  * (없으면 no_runtime 정직 정지 — 폴백 금지는 상위 계층의 계약).
8
8
  */
9
9
  const { spawnSync } = require("node:child_process");
10
+ // kind 목록/실행 파일 이름의 정본은 runtimes/kinds.cjs 하나다 — 여기서 다시 적지 않는다.
11
+ const { RUNTIME_BIN, CLI_KINDS } = require("./kinds.cjs");
10
12
 
11
- const RUNTIME_BIN = {
12
- "claude-code": "claude",
13
- codex: "codex",
14
- // Antigravity CLI — gemini 후속. 공식 gemini CLI가 계정 티어로 죽어도(IneligibleTierError,
15
- // 실측 2026-08-06) 이쪽은 산다. 데스크탑 gemini 러너의 agy 경로와 같은 실물.
16
- agy: "agy",
17
- gemini: "gemini",
18
- kimi: "kimi",
19
- grok: "grok",
20
- cursor: "cursor-agent",
21
- };
22
-
23
- const CLI_RUNTIMES = Object.keys(RUNTIME_BIN);
13
+ const CLI_RUNTIMES = CLI_KINDS;
24
14
 
25
15
  function whichSync(bin) {
26
16
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/kinds — 런타임 종류(RuntimeKind)의 단일 정본 (2026-08-18).
4
+ *
5
+ * 배경: 같은 어휘가 8곳(detect·capture·resolve·acp-driver·capabilities·onboard·
6
+ * palette·input)에 손 목록으로 흩어져 있었다 — 새 kind 를 추가하면 한두 곳이
7
+ * 반드시 빠진다. 여기 한 벌만 두고, 다른 표면은 이 상수를 **가져다 쓴다**.
8
+ * 좁은 목록(캡처 4종 등)도 정본에서 파생해야 새 kind 가 모든 표면에 보인다.
9
+ *
10
+ * 순서가 곧 계약이다:
11
+ * - detect.listAvailableCliRuntimes 는 이 순서로 PATH 를 훑고, resolve 의
12
+ * "detected" 폴백은 그 첫 항목을 쓴다.
13
+ * - 온보딩 위저드 선택지·팔레트/입력 완성 후보도 이 순서로 표시된다.
14
+ */
15
+
16
+ const RUNTIME_KIND_SPECS = [
17
+ { kind: "claude-code", bin: "claude", driver: "native", capture: true },
18
+ { kind: "codex", bin: "codex", driver: "native", capture: true },
19
+ // Antigravity CLI — gemini 후속. 공식 gemini CLI가 계정 티어로 죽어도(IneligibleTierError,
20
+ // 실측 2026-08-06) 이쪽은 산다. 데스크탑 gemini 러너의 agy 경로와 같은 실물.
21
+ { kind: "agy", bin: "agy", driver: "native", capture: true },
22
+ { kind: "gemini", bin: "gemini", driver: "native", capture: true },
23
+ // kimi/grok/cursor 는 ACP 드라이버(runtimes/acp-driver.cjs → 벤더 코어의 공용
24
+ // ACP 러너)로 돈다 (PRD 2026-08-15 T-2). 캡처(buildArgs/텍스트 추출) 계약은 없다.
25
+ { kind: "kimi", bin: "kimi", driver: "acp" },
26
+ { kind: "grok", bin: "grok", driver: "acp" },
27
+ { kind: "cursor", bin: "cursor-agent", driver: "acp" },
28
+ ];
29
+
30
+ /** kind → 실행 파일 이름. CLI 런타임 전체(네이티브 + ACP). */
31
+ const RUNTIME_BIN = Object.fromEntries(RUNTIME_KIND_SPECS.map((s) => [s.kind, s.bin]));
32
+
33
+ /** CLI 런타임 kind 전체(탐지 순서). */
34
+ const CLI_KINDS = RUNTIME_KIND_SPECS.map((s) => s.kind);
35
+
36
+ /** native-host 드라이버(스폰 러너)를 갖춘 CLI 4종. */
37
+ const NATIVE_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.driver === "native").map((s) => s.kind);
38
+
39
+ /** 벤더 코어의 공용 ACP 러너로 도는 3종. */
40
+ const ACP_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.driver === "acp").map((s) => s.kind);
41
+
42
+ /** 캡처(no-authority headless) 드라이버가 검증된 kind — buildArgs/텍스트 추출 계약 보유. */
43
+ const CAPTURE_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.capture).map((s) => s.kind);
44
+
45
+ /** kind → bin, 캡처 검증본만. workforce/capture 가 쓴다. */
46
+ const CAPTURE_RUNTIME_BIN = Object.fromEntries(
47
+ RUNTIME_KIND_SPECS.filter((s) => s.capture).map((s) => [s.kind, s.bin]),
48
+ );
49
+
50
+ /** CLI 가 아니라 로컬 API loop 로 실행되는 kind. */
51
+ const API_EXECUTABLE_KINDS = ["ollama"];
52
+
53
+ /** BYOK/API 백엔드 spec 문자열(/runtime 완성 후보의 API 절반). */
54
+ const API_BACKEND_SPECS = ["anthropic", "openai", "google", "ollama", "upstage"];
55
+
56
+ /** /runtime 이 받는 spec 전체: 네이티브 CLI kind + API 백엔드. */
57
+ const RUNTIME_SPECS = [...NATIVE_CLI_KINDS, ...API_BACKEND_SPECS];
58
+
59
+ /**
60
+ * 저장 계약(automation 등)이 허용하는 kind 전체 — CLI + 로컬/BYOK 실행 kind.
61
+ * 데스크탑 shared/runtime-kinds.ts 와 동형(터미널 표기: antigravity→agy, acp 미지원).
62
+ */
63
+ const CONTRACT_RUNTIME_KINDS = [...CLI_KINDS, "byok", "ollama", "lmstudio", "mlx"];
64
+
65
+ /** 저장 계약이 허용하는 LLM 백엔드 — 데스크탑 shared/runtime-backends.ts 와 동일 15종. */
66
+ const CONTRACT_RUNTIME_BACKENDS = [
67
+ "anthropic", "openai", "google", "ollama", "lmstudio", "mlx", "upstage", "custom", "glm",
68
+ "kimi", "deepseek", "minimax", "xai", "openrouter", "cursor",
69
+ ];
70
+
71
+ module.exports = {
72
+ RUNTIME_KIND_SPECS,
73
+ RUNTIME_BIN,
74
+ CLI_KINDS,
75
+ NATIVE_CLI_KINDS,
76
+ ACP_CLI_KINDS,
77
+ CAPTURE_CLI_KINDS,
78
+ CAPTURE_RUNTIME_BIN,
79
+ API_EXECUTABLE_KINDS,
80
+ API_BACKEND_SPECS,
81
+ RUNTIME_SPECS,
82
+ CONTRACT_RUNTIME_KINDS,
83
+ CONTRACT_RUNTIME_BACKENDS,
84
+ };
@@ -6,17 +6,45 @@
6
6
  * 아무것도 없으면 no_runtime "정직 정지" — 키워드/저품질 폴백 금지(오너 결정).
7
7
  */
8
8
  const { RUNTIME_BIN, whichSync, listAvailableCliRuntimes, activeRuntimeRow } = require("./detect.cjs");
9
+ const KINDS = require("./kinds.cjs");
9
10
  const path = require("node:path");
10
11
 
11
12
  // Session이 실제 드라이버를 갖춘 런타임만 실행 대상으로 삼는다.
12
13
  // CLI는 native-host, Ollama는 로컬 API loop를 쓴다. 다른 드라이버가 포팅되면
13
14
  // 해당 집합에 추가한다(조용한 오폭 방지).
14
- const CLI_EXECUTABLE_KINDS = new Set(["claude-code", "codex", "gemini", "agy"]);
15
- const API_EXECUTABLE_KINDS = new Set(["ollama"]);
16
- const EXECUTABLE_KINDS = new Set([
17
- ...CLI_EXECUTABLE_KINDS,
18
- ...API_EXECUTABLE_KINDS,
19
- ]);
15
+ //
16
+ // kimi/grok/cursor ACP 드라이버(runtimes/acp-driver.cjs → 벤더 코어의 공용 ACP 러너)로 돈다
17
+ // (PRD 2026-08-15 T-2). 벤더 코어가 그 러너를 갖고 있을 때만 실행 대상에 든다 — 옛 코어면
18
+ // 종전과 같은 "드라이버 없음" 정직 거부.
19
+ // 집합의 원소는 정본(runtimes/kinds.cjs)에서 파생한다 — 여기서 다시 적지 않는다.
20
+ const NATIVE_CLI_KINDS = new Set(KINDS.NATIVE_CLI_KINDS);
21
+ const ACP_CLI_KINDS = new Set(KINDS.ACP_CLI_KINDS);
22
+ const API_EXECUTABLE_KINDS = new Set(KINDS.API_EXECUTABLE_KINDS);
23
+
24
+ function acpKindsAvailable() {
25
+ try {
26
+ const { acpDriverAvailability } = require("./acp-driver.cjs");
27
+ return acpDriverAvailability().ok ? ACP_CLI_KINDS : new Set();
28
+ } catch {
29
+ return new Set();
30
+ }
31
+ }
32
+
33
+ // 라이브 집합: 네이티브 4종 + (코어가 ACP 러너를 갖고 있으면) ACP 3종.
34
+ const CLI_EXECUTABLE_KINDS = new Proxy(NATIVE_CLI_KINDS, {
35
+ get(target, prop) {
36
+ const live = new Set([...target, ...acpKindsAvailable()]);
37
+ const value = live[prop];
38
+ return typeof value === "function" ? value.bind(live) : value;
39
+ },
40
+ });
41
+ const EXECUTABLE_KINDS = new Proxy(NATIVE_CLI_KINDS, {
42
+ get(target, prop) {
43
+ const live = new Set([...target, ...acpKindsAvailable(), ...API_EXECUTABLE_KINDS]);
44
+ const value = live[prop];
45
+ return typeof value === "function" ? value.bind(live) : value;
46
+ },
47
+ });
20
48
 
21
49
  function apiRuntime(kind, model, source) {
22
50
  if (!API_EXECUTABLE_KINDS.has(kind)) return null;
@@ -56,7 +84,10 @@ function resolveRuntime({ db, prefs, explicit }) {
56
84
  const bin = RUNTIME_BIN[explicit];
57
85
  if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
58
86
  if (!CLI_EXECUTABLE_KINDS.has(explicit)) {
59
- throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})`);
87
+ const acpHint = ACP_CLI_KINDS.has(explicit)
88
+ ? ` — its ACP driver needs the desktop core with electron/runtime/acp.js (npm run vendor:core / agentlas doctor)`
89
+ : "";
90
+ throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})${acpHint}`);
60
91
  }
61
92
  const p = whichSync(bin);
62
93
  if (!p) throw new NoRuntimeError(`runtime '${explicit}' requested but '${bin}' is not on PATH`);
@@ -109,6 +140,8 @@ module.exports = {
109
140
  NoRuntimeError,
110
141
  EXECUTABLE_KINDS,
111
142
  CLI_EXECUTABLE_KINDS,
143
+ NATIVE_CLI_KINDS,
144
+ ACP_CLI_KINDS,
112
145
  API_EXECUTABLE_KINDS,
113
146
  sharedRuntimeKind,
114
147
  };
@@ -107,6 +107,8 @@ const CATALOG = [
107
107
  { name: "import", group: "advanced", tier: "more", surfaces: BOTH, args: "<path>", argsKo: "<경로>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
108
108
  { name: "cd", group: "advanced", tier: "more", surfaces: BOTH, args: "<agent>", argsKo: "<에이전트>", ko: "그 에이전트의 폴더 경로를 출력", en: "Print that agent's folder path" },
109
109
  { name: "native", group: "advanced", tier: "more", surfaces: BOTH, args: "prepare <agent>", argsKo: "prepare <에이전트>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
110
+ // CLI only: stdout is the protocol wire, so it cannot run inside the REPL.
111
+ { name: "acp", group: "advanced", tier: "more", surfaces: CLI, args: "[--info]", ko: "에디터(Zed·JetBrains)용 ACP 에이전트로 실행", en: "Serve Agentlas as an ACP agent for editors (Zed, JetBrains)" },
110
112
  { name: "mcp", group: "advanced", tier: "more", surfaces: BOTH, args: "[list|probe <id>]", ko: "MCP 서버", en: "MCP servers" },
111
113
  { name: "plugin", group: "advanced", tier: "more", surfaces: BOTH, args: "<add <slug>|list|remove>", ko: "Hub 플러그인 (MCP 서버)", en: "Hub plugins (MCP servers)" },
112
114
  { name: "creds", group: "advanced", tier: "more", surfaces: BOTH, args: "<list|save|file>", ko: "API 키 보관 (값은 절대 표시 안 함)", en: "API keys (values are never printed)" },
@@ -28,7 +28,8 @@ const SLASH_COMMANDS = catalog.forSurface("repl").map((entry) => ({
28
28
  }));
29
29
 
30
30
  const SLASH_NAMES = SLASH_COMMANDS.map((c) => c.command);
31
- const RUNTIME_KINDS = ["claude-code", "codex", "agy", "gemini"];
31
+ // /runtime 완성 후보 정본(runtimes/kinds.cjs)의 네이티브 스폰 러너 4종.
32
+ const RUNTIME_KINDS = require("../runtimes/kinds.cjs").NATIVE_CLI_KINDS;
32
33
  const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
33
34
  const PERM_LEVELS = ["read", "write", "full"];
34
35
  // 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
@@ -791,7 +791,8 @@ function handleSlash(ctx, cmdline, api) {
791
791
  * (REPL의 평문 입력이 곧 run이다).
792
792
  */
793
793
  // help/agents/list/mcp/doctor 등은 위 케이스에서 이미 처리된다.
794
- const REPL_EXCLUDED = new Set(["firm", "setup", "run"]);
794
+ // acp: stdout becomes the protocol wire — meaningless (and destructive) inside the REPL.
795
+ const REPL_EXCLUDED = new Set(["firm", "setup", "run", "acp"]);
795
796
  if (!REPL_EXCLUDED.has(cmd) && commands.COMMANDS[cmd]) {
796
797
  const result = commands.COMMANDS[cmd]().run(ctx, rest);
797
798
  if (result && typeof result.then === "function") {
@@ -45,6 +45,35 @@ function loadRenderer() {
45
45
  }
46
46
  }
47
47
 
48
+ /*
49
+ * 제어 블록 스트리퍼 — 정본은 벤더 코어의 shared/agent-control-blocks
50
+ * (Desktop·Mobile 과 같은 규칙: Memory Events/Delegate/Automation 헤딩,
51
+ * <<agentlas-ask>>·<<agentlas-surface>>·<<agentlas-one-followups>>·goal-complete
52
+ * 마커, 스트리밍 미완성 꼬리까지). 손 regex 는 Memory Events 만 알아 나머지
53
+ * 마커가 화면에 원문으로 샜다. 옛 벤더 번들이라 정본이 없으면 종전 regex 로
54
+ * fail-open — 스트리퍼 부재가 TUI 를 죽여선 안 된다.
55
+ */
56
+ let _stripCanonical; // undefined=미시도 · null=정본 없음 · function=정본
57
+ function stripControlBlocksForDisplay(text, streaming) {
58
+ if (_stripCanonical === undefined) {
59
+ try {
60
+ const loaded = require("../core/desktop-core.cjs").loadCoreShared("agent-control-blocks");
61
+ _stripCanonical = loaded && loaded.module && typeof loaded.module.stripAgentControlBlocks === "function"
62
+ ? loaded.module.stripAgentControlBlocks
63
+ : null;
64
+ } catch {
65
+ _stripCanonical = null;
66
+ }
67
+ }
68
+ const value = String(text);
69
+ if (_stripCanonical) {
70
+ try {
71
+ return _stripCanonical(value, { streaming: !!streaming });
72
+ } catch { /* 정본 실패 → 아래 종전 regex 로 fail-open */ }
73
+ }
74
+ return value.replace(/\n#{1,3} Memory Events\b[\s\S]*$/, "\n");
75
+ }
76
+
48
77
  /* Ui 를 상속해 write 초크포인트만 렌더러로 돌린다. */
49
78
  class ShellUi extends Ui {
50
79
  /*
@@ -127,16 +156,22 @@ class ShellUi extends Ui {
127
156
  if (!this._md) this.streamStart();
128
157
  this._mdText += String(text);
129
158
  /*
130
- * Memory Events 봉투는 런타임 계약(펜스 파이프라인이 수확)이지 사용자용이 아니다.
159
+ * 제어 블록은 런타임 계약(펜스 파이프라인이 수확)이지 사용자용이 아니다.
131
160
  * append-only 기본 REPL은 이미 찍힌 봉투를 지울 수 없지만, 누적 재렌더는
132
161
  * 표시만 잘라낼 수 있다 — 수확 경로(st.text/fences)는 건드리지 않는다.
162
+ * 정본 스트리퍼의 streaming 모드가 미완성 마커 꼬리도 한 프레임 감춘다.
133
163
  */
134
- const visible = this._mdText.replace(/\n#{1,3} Memory Events\b[\s\S]*$/, "\n");
164
+ const visible = stripControlBlocksForDisplay(this._mdText, true);
135
165
  this._md.setText(visible);
136
166
  this._tui.requestRender();
137
167
  this._streaming = true;
138
168
  }
139
169
  streamEnd() {
170
+ // 확정 렌더 — streaming 모드가 감춰 두던 꼬리를 settled 규칙으로 최종 판정한다.
171
+ if (this._md && this._mdText) {
172
+ this._md.setText(stripControlBlocksForDisplay(this._mdText, false));
173
+ this._tui.requestRender();
174
+ }
140
175
  this._md = null;
141
176
  this._mdText = "";
142
177
  this._streaming = false;
@@ -615,4 +650,4 @@ async function startShell(ctx, opts = {}) {
615
650
  return new Promise(() => {});
616
651
  }
617
652
 
618
- module.exports = { startShell };
653
+ module.exports = { startShell, stripControlBlocksForDisplay };
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "2",
3
- "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v2/desktop-core.tar.gz",
4
- "sha256": "782760c34af1d06b9efd212d48ba032fe46907bdc8bc836025c44fc658968d6f",
5
- "sizeBytes": 12295321,
6
- "writtenAt": "2026-08-09T21:43:34.707Z"
2
+ "version": "3",
3
+ "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v3/desktop-core.tar.gz",
4
+ "sha256": "a72cce0ccfe718e2a9255b1b4722d330ef9887e22be92c654a0435f17f5ee178",
5
+ "sizeBytes": 12437404,
6
+ "writtenAt": "2026-08-18T04:09:04.540Z"
7
7
  }
@@ -24,14 +24,10 @@ const path = require("node:path");
24
24
  const { spawn } = require("node:child_process");
25
25
  const { dbPath, userDataDir } = require("../core/paths.cjs");
26
26
 
27
- // 캡처 드라이버가 검증된 런타임만. v2 detect.cjs의 RUNTIME_BIN에는 kimi/grok/cursor도
28
- // 있지만 buildArgs/텍스트 추출 계약이 없으므로 여기 목록에 절대 조용히 추가하지 않는다.
29
- const RUNTIME_BIN = {
30
- "claude-code": "claude",
31
- codex: "codex",
32
- agy: "agy",
33
- gemini: "gemini",
34
- };
27
+ // 캡처 드라이버가 검증된 런타임만. 정본(runtimes/kinds.cjs)의 RUNTIME_BIN에는 kimi/grok/cursor도
28
+ // 있지만 buildArgs/텍스트 추출 계약이 없으므로 캡처 검증 파생본만 쓴다 새 kind 를
29
+ // 정본에 추가해도 capture:true 를 명시하기 전엔 여기 조용히 들어오지 않는다.
30
+ const { CAPTURE_RUNTIME_BIN: RUNTIME_BIN } = require("../runtimes/kinds.cjs");
35
31
 
36
32
  const SERVICE = "com.agentlas.desktop";
37
33
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"
7
7
  },
8
8
  "scripts": {
9
+ "test": "sh test/smoke.sh",
9
10
  "smoke": "sh test/smoke.sh",
10
11
  "test:release-contracts": "npm run smoke",
11
12
  "sync:architecture": "node scripts/sync-architecture-from-desktop.cjs",