xgen-dex-cli 1.2.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.
@@ -0,0 +1,4560 @@
1
+ // ../../packages/protocol/src/client.ts
2
+ var ApiError = class extends Error {
3
+ constructor(status, message, body) {
4
+ super(message);
5
+ this.status = status;
6
+ this.body = body;
7
+ this.name = "ApiError";
8
+ }
9
+ };
10
+ var HttpClient = class {
11
+ baseUrl;
12
+ accessToken = null;
13
+ fetchImpl;
14
+ onAuthFailure;
15
+ timeoutMs;
16
+ constructor(opts) {
17
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
18
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
19
+ this.onAuthFailure = opts.onAuthFailure;
20
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
21
+ if (!this.fetchImpl) {
22
+ throw new Error("HttpClient: no fetch implementation available");
23
+ }
24
+ }
25
+ setBaseUrl(baseUrl) {
26
+ this.baseUrl = normalizeBaseUrl(baseUrl);
27
+ }
28
+ getBaseUrl() {
29
+ return this.baseUrl;
30
+ }
31
+ setToken(token) {
32
+ this.accessToken = token;
33
+ }
34
+ url(path) {
35
+ return `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
36
+ }
37
+ headers(extra) {
38
+ const h = { ...extra };
39
+ if (this.accessToken) h["Authorization"] = `Bearer ${this.accessToken}`;
40
+ return h;
41
+ }
42
+ /** GET/POST/… returning parsed JSON. Throws ApiError on non-2xx. */
43
+ async json(method, path, body, opts) {
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? this.timeoutMs);
46
+ let res;
47
+ try {
48
+ res = await this.fetchImpl(this.url(path), {
49
+ method,
50
+ headers: this.headers({
51
+ "Content-Type": "application/json",
52
+ Accept: "application/json"
53
+ }),
54
+ body: body === void 0 ? void 0 : JSON.stringify(body),
55
+ signal: controller.signal
56
+ });
57
+ } finally {
58
+ clearTimeout(timer);
59
+ }
60
+ const text = await res.text();
61
+ let parsed = void 0;
62
+ if (text) {
63
+ try {
64
+ parsed = JSON.parse(text);
65
+ } catch {
66
+ parsed = text;
67
+ }
68
+ }
69
+ if (!res.ok) {
70
+ if (res.status === 401 && opts?.auth !== false) this.onAuthFailure?.();
71
+ throw new ApiError(res.status, `${method} ${path} \u2192 ${res.status}`, parsed);
72
+ }
73
+ return parsed;
74
+ }
75
+ get(path, opts) {
76
+ return this.json("GET", path, void 0, opts);
77
+ }
78
+ /** Multipart upload (아바타 에셋 등). Content-Type 은 fetch 가 boundary 와
79
+ * 함께 자동 설정하므로 지정하지 않는다. 대용량(모델 zip) 대비 긴 타임아웃. */
80
+ async upload(path, form, opts) {
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 12e4);
83
+ let res;
84
+ try {
85
+ res = await this.fetchImpl(this.url(path), {
86
+ method: "POST",
87
+ headers: this.headers({ Accept: "application/json" }),
88
+ body: form,
89
+ signal: controller.signal
90
+ });
91
+ } finally {
92
+ clearTimeout(timer);
93
+ }
94
+ const text = await res.text();
95
+ let parsed = void 0;
96
+ if (text) {
97
+ try {
98
+ parsed = JSON.parse(text);
99
+ } catch {
100
+ parsed = text;
101
+ }
102
+ }
103
+ if (!res.ok) {
104
+ if (res.status === 401) this.onAuthFailure?.();
105
+ throw new ApiError(res.status, `POST ${path} \u2192 ${res.status}`, parsed);
106
+ }
107
+ return parsed;
108
+ }
109
+ post(path, body, opts) {
110
+ return this.json("POST", path, body, opts);
111
+ }
112
+ /**
113
+ * POST a JSON body and read the raw BINARY response (e.g. TTS audio bytes).
114
+ * Returns the bytes plus the response `Content-Type` so the caller can wrap a
115
+ * correctly-typed Blob (audio/wav|mpeg|ogg). Throws ApiError on non-2xx.
116
+ */
117
+ async postBinary(path, body, opts) {
118
+ const controller = new AbortController();
119
+ const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? this.timeoutMs);
120
+ let res;
121
+ try {
122
+ res = await this.fetchImpl(this.url(path), {
123
+ method: "POST",
124
+ headers: this.headers({ "Content-Type": "application/json", Accept: "audio/*" }),
125
+ body: JSON.stringify(body),
126
+ signal: controller.signal
127
+ });
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ if (!res.ok) {
132
+ if (res.status === 401) this.onAuthFailure?.();
133
+ const text = await res.text().catch(() => "");
134
+ throw new ApiError(res.status, `POST ${path} \u2192 ${res.status}`, text);
135
+ }
136
+ const ab = await res.arrayBuffer();
137
+ return { bytes: new Uint8Array(ab), contentType: res.headers.get("content-type") ?? "" };
138
+ }
139
+ /**
140
+ * GET a raw BINARY response (Teams 첨부 다운로드 등). `postBinary` 의 GET 짝.
141
+ *
142
+ * 파일명은 응답 헤더가 아니라 **호출자가 이미 아는 값**을 쓴다 —
143
+ * Content-Disposition 의 RFC 5987 인코딩을 여기서 되풀이 파싱할 이유가 없고,
144
+ * 서버도 우리가 쿼리로 넘긴 이름을 그대로 되돌려줄 뿐이다.
145
+ */
146
+ async getBinary(path, opts) {
147
+ const controller = new AbortController();
148
+ const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 12e4);
149
+ let res;
150
+ try {
151
+ res = await this.fetchImpl(this.url(path), {
152
+ method: "GET",
153
+ headers: this.headers({ Accept: "*/*" }),
154
+ signal: controller.signal
155
+ });
156
+ } finally {
157
+ clearTimeout(timer);
158
+ }
159
+ if (!res.ok) {
160
+ if (res.status === 401) this.onAuthFailure?.();
161
+ const text = await res.text().catch(() => "");
162
+ throw new ApiError(res.status, `GET ${path} \u2192 ${res.status}`, text);
163
+ }
164
+ const ab = await res.arrayBuffer();
165
+ return { bytes: new Uint8Array(ab), contentType: res.headers.get("content-type") ?? "" };
166
+ }
167
+ put(path, body, opts) {
168
+ return this.json("PUT", path, body, opts);
169
+ }
170
+ patch(path, body, opts) {
171
+ return this.json("PATCH", path, body, opts);
172
+ }
173
+ /** `delete` is a reserved word in some call sites — keep the short alias. */
174
+ del(path, opts) {
175
+ return this.json("DELETE", path, void 0, opts);
176
+ }
177
+ /**
178
+ * Open a raw streaming POST (for SSE). Returns the Response so the caller can
179
+ * read `response.body` as a stream. Does NOT enforce the JSON timeout — SSE
180
+ * connections are long-lived (the gateway allows 1h for `/stream` paths).
181
+ */
182
+ async stream(path, body, signal) {
183
+ const res = await this.fetchImpl(this.url(path), {
184
+ method: "POST",
185
+ headers: this.headers({
186
+ "Content-Type": "application/json",
187
+ Accept: "text/event-stream"
188
+ }),
189
+ body: JSON.stringify(body),
190
+ signal
191
+ });
192
+ if (!res.ok) {
193
+ if (res.status === 401) this.onAuthFailure?.();
194
+ const text = await res.text().catch(() => "");
195
+ throw new ApiError(res.status, `stream ${path} \u2192 ${res.status}`, text);
196
+ }
197
+ return res;
198
+ }
199
+ };
200
+ function normalizeBaseUrl(url) {
201
+ return (url || "").trim().replace(/\/+$/, "");
202
+ }
203
+
204
+ // ../../packages/engine/src/errors.ts
205
+ var DexError = class extends Error {
206
+ constructor(code, message, details) {
207
+ super(message);
208
+ this.code = code;
209
+ this.details = details;
210
+ this.name = "DexError";
211
+ }
212
+ };
213
+ function isUnauthorized(error) {
214
+ return error instanceof ApiError && (error.status === 401 || error.status === 403);
215
+ }
216
+ function publicError(error) {
217
+ if (error instanceof DexError) {
218
+ return { code: error.code, message: error.message, details: error.details };
219
+ }
220
+ if (error instanceof ApiError) {
221
+ return {
222
+ code: `http_${error.status}`,
223
+ message: error.message,
224
+ details: error.body
225
+ };
226
+ }
227
+ if (error instanceof Error) {
228
+ if (error.name === "AbortError") return { code: "cancelled", message: "\uC694\uCCAD\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4." };
229
+ return { code: "internal_error", message: error.message };
230
+ }
231
+ return { code: "internal_error", message: String(error) };
232
+ }
233
+
234
+ // ../../packages/engine/src/config-store.ts
235
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
236
+ import { dirname, join } from "node:path";
237
+ import { homedir, platform } from "node:os";
238
+
239
+ // ../../packages/engine/src/local-tools-config.ts
240
+ function defaultLocalToolsConfig() {
241
+ return {
242
+ enabled: false,
243
+ cwd: "",
244
+ timeoutMs: 12e4,
245
+ allowedRoots: [],
246
+ blockedCommands: [],
247
+ allowDangerous: false
248
+ };
249
+ }
250
+ var MIN_TIMEOUT = 1e3;
251
+ var MAX_TIMEOUT = 36e5;
252
+ function cleanList(value) {
253
+ if (!Array.isArray(value)) return [];
254
+ const out = [];
255
+ for (const item of value) {
256
+ const s = String(item ?? "").trim();
257
+ if (s && !out.includes(s)) out.push(s);
258
+ }
259
+ return out;
260
+ }
261
+ function normalizeLocalToolsConfig(value) {
262
+ const d = defaultLocalToolsConfig();
263
+ const v = value && typeof value === "object" ? value : {};
264
+ const timeout = Number(v.timeoutMs);
265
+ return {
266
+ enabled: v.enabled === true,
267
+ cwd: String(v.cwd ?? "").trim(),
268
+ timeoutMs: Number.isFinite(timeout) ? Math.min(MAX_TIMEOUT, Math.max(MIN_TIMEOUT, Math.round(timeout))) : d.timeoutMs,
269
+ allowedRoots: cleanList(v.allowedRoots),
270
+ blockedCommands: cleanList(v.blockedCommands),
271
+ allowDangerous: v.allowDangerous === true
272
+ };
273
+ }
274
+ function toShellConfig(config) {
275
+ return {
276
+ enabled: config.enabled,
277
+ cwd: config.cwd,
278
+ timeoutMs: config.timeoutMs,
279
+ allowedRoots: config.allowedRoots,
280
+ blocked: config.blockedCommands
281
+ };
282
+ }
283
+ function dangerousApprovalFromConfig(config) {
284
+ if (!config.allowDangerous) return void 0;
285
+ return async () => "session";
286
+ }
287
+
288
+ // ../../packages/engine/src/config-store.ts
289
+ var DEFAULT_PROFILE = "default";
290
+ function defaultConfig() {
291
+ return {
292
+ version: 1,
293
+ currentProfile: DEFAULT_PROFILE,
294
+ profiles: {},
295
+ localTools: defaultLocalToolsConfig()
296
+ };
297
+ }
298
+ function dataDirectory(env = process.env) {
299
+ if (env.DEX_CLI_HOME?.trim()) return env.DEX_CLI_HOME.trim();
300
+ if (platform() === "win32") return join(env.APPDATA || homedir(), "xgen-dex-cli");
301
+ if (platform() === "darwin") return join(homedir(), "Library", "Application Support", "xgen-dex-cli");
302
+ return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "xgen-dex-cli");
303
+ }
304
+ function configPath(env = process.env) {
305
+ return join(dataDirectory(env), "config.json");
306
+ }
307
+ function parseConfig(raw) {
308
+ if (!raw || typeof raw !== "object") throw new DexError("config_invalid", "\uC124\uC815 \uD30C\uC77C\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4.");
309
+ const value = raw;
310
+ const profiles = {};
311
+ if (value.profiles && typeof value.profiles === "object") {
312
+ for (const [name, profile] of Object.entries(value.profiles)) {
313
+ if (!profile || typeof profile !== "object") continue;
314
+ const serverUrl = String(profile.serverUrl ?? "").trim();
315
+ if (serverUrl) profiles[name] = { serverUrl };
316
+ }
317
+ }
318
+ const localTools = normalizeLocalToolsConfig(value.localTools);
319
+ return {
320
+ version: 1,
321
+ currentProfile: String(value.currentProfile || DEFAULT_PROFILE),
322
+ profiles,
323
+ localTools
324
+ };
325
+ }
326
+ var FileConfigStore = class {
327
+ constructor(path = configPath()) {
328
+ this.path = path;
329
+ }
330
+ queue = Promise.resolve();
331
+ async read() {
332
+ try {
333
+ return parseConfig(JSON.parse(await readFile(this.path, "utf8")));
334
+ } catch (error) {
335
+ if (error.code === "ENOENT") return defaultConfig();
336
+ if (error instanceof DexError) throw error;
337
+ throw new DexError("config_invalid", `\uC124\uC815 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${this.path}`, error);
338
+ }
339
+ }
340
+ async write(config) {
341
+ const operation = async () => {
342
+ await mkdir(dirname(this.path), { recursive: true, mode: 448 });
343
+ const temporary = `${this.path}.${process.pid}.tmp`;
344
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}
345
+ `, { mode: 384 });
346
+ await chmod(temporary, 384);
347
+ await rename(temporary, this.path);
348
+ };
349
+ this.queue = this.queue.then(operation, operation);
350
+ await this.queue;
351
+ }
352
+ };
353
+ function validateProfileName(input) {
354
+ const name = input.trim();
355
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
356
+ throw new DexError(
357
+ "config_invalid",
358
+ "\uD504\uB85C\uD544 \uC774\uB984\uC740 \uC601\uBB38\uC790/\uC22B\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uC601\uBB38\uC790, \uC22B\uC790, \uC810, \uBC11\uC904, \uD558\uC774\uD508\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
359
+ );
360
+ }
361
+ return name;
362
+ }
363
+ function validateServerUrl(input) {
364
+ let url;
365
+ try {
366
+ url = new URL(input.trim());
367
+ } catch {
368
+ throw new DexError("config_invalid", "\uC11C\uBC84 URL\uC740 http:// \uB610\uB294 https://\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.");
369
+ }
370
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
371
+ throw new DexError("config_invalid", "\uC11C\uBC84 URL\uC740 http:// \uB610\uB294 https://\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
372
+ }
373
+ if (url.username || url.password || url.search || url.hash) {
374
+ throw new DexError("config_invalid", "\uC11C\uBC84 URL\uC5D0\uB294 \uC790\uACA9 \uC99D\uBA85, query, fragment\uB97C \uB123\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
375
+ }
376
+ return url.toString().replace(/\/$/, "");
377
+ }
378
+
379
+ // ../../packages/engine/src/dex-engine.ts
380
+ import { randomUUID } from "node:crypto";
381
+
382
+ // ../../packages/protocol/src/agent-data.ts
383
+ function workspaceStoragePath(path) {
384
+ const clean = String(path ?? "").replace(/\\/g, "/").replace(/^\/+/, "");
385
+ return clean === "workspace" || clean.startsWith("workspace/") ? clean : `workspace/${clean}`;
386
+ }
387
+ var AgentDataApi = class {
388
+ constructor(http) {
389
+ this.http = http;
390
+ }
391
+ // ── 전체로그 ──────────────────────────────────────────────────
392
+ /** 이 에이전트의 실행 트레이스 목록(최근 50). */
393
+ traceList(workflowId) {
394
+ const params = new URLSearchParams({
395
+ workflow_id: workflowId,
396
+ page: "1",
397
+ page_size: "50"
398
+ });
399
+ return this.http.get(`/api/agentflow/trace/list?${params}`);
400
+ }
401
+ /** 트레이스 하나의 스팬(단계) 전부. */
402
+ traceDetail(traceId) {
403
+ return this.http.get(`/api/agentflow/trace/detail/${encodeURIComponent(traceId)}`);
404
+ }
405
+ // ── 메모리 ────────────────────────────────────────────────────
406
+ memoryList(workflowId) {
407
+ return this.http.get(
408
+ `/api/agentflow/geny-memory/${encodeURIComponent(workflowId)}/files`
409
+ );
410
+ }
411
+ /** filename 은 서버에서 `{filename:path}` — 슬래시는 살리고 세그먼트만 인코딩한다. */
412
+ memoryRead(workflowId, path) {
413
+ const fp = path.split("/").map(encodeURIComponent).join("/");
414
+ return this.http.get(
415
+ `/api/agentflow/geny-memory/${encodeURIComponent(workflowId)}/files/${fp}`
416
+ );
417
+ }
418
+ // ── 작업 ──────────────────────────────────────────────────────
419
+ tasksList(workflowId) {
420
+ return this.http.get(
421
+ `/api/agentflow/geny-tasks/${encodeURIComponent(workflowId)}`
422
+ );
423
+ }
424
+ /** 예약 작업(job=session_id) 1건의 실행 기록. */
425
+ taskRuns(workflowId, sessionId) {
426
+ return this.http.get(
427
+ `/api/agentflow/geny-tasks/${encodeURIComponent(workflowId)}/job/${encodeURIComponent(sessionId ?? "")}/runs`
428
+ );
429
+ }
430
+ /** 백그라운드/서브에이전트 작업(task_id) 1건의 출력. */
431
+ taskOutput(workflowId, runId) {
432
+ return this.http.get(
433
+ `/api/agentflow/geny-tasks/${encodeURIComponent(workflowId)}/task/${encodeURIComponent(runId)}/output`
434
+ );
435
+ }
436
+ // ── 세션 수명 ──────────────────────────────────────────────────
437
+ /** '진행 중 대화' 종료 — 서버가 들고 있는 세션 RAM(executor + 라우팅)을 회수한다.
438
+ * 이력은 지우지 않는다(삭제는 세션 종료이지 대화 기록 삭제가 아니다). */
439
+ endSession(workflowId, interactionId) {
440
+ return this.http.post(
441
+ `/api/agentflow/geny-agent/${encodeURIComponent(workflowId)}/end-session`,
442
+ { interaction_id: interactionId }
443
+ );
444
+ }
445
+ // ── 기본정보 ──────────────────────────────────────────────────
446
+ /** 실행 없이 재구성한 턴 프롬프트 + 도구 표면(web/connector 둘 다). */
447
+ basicInfo(workflowId) {
448
+ return this.http.get(
449
+ `/api/agentflow/${encodeURIComponent(workflowId)}/basic-info`
450
+ );
451
+ }
452
+ // ── 도구 ──────────────────────────────────────────────────────
453
+ toolsList(workflowId) {
454
+ return this.http.get(
455
+ `/api/agentflow/geny-tools/${encodeURIComponent(workflowId)}`
456
+ );
457
+ }
458
+ /** 제작 도구 하나 — 소스 코드까지. */
459
+ toolGet(workflowId, functionId) {
460
+ return this.http.get(
461
+ `/api/agentflow/geny-tools/${encodeURIComponent(workflowId)}/${encodeURIComponent(functionId)}?with_source=true`
462
+ );
463
+ }
464
+ // ── 스토리지 ──────────────────────────────────────────────────
465
+ /** 워크스페이스 전체(평면) 목록 — 파일/폴더 각각 한 항목. `path` 는 예약(미사용). */
466
+ workspaceTree(workflowId, _path) {
467
+ return this.http.get(
468
+ `/api/agentflow/geny-workspace/${encodeURIComponent(workflowId)}/storage/list`
469
+ );
470
+ }
471
+ /** 텍스트 파일 미리보기. 바이너리/과대 파일은 서버가 415/413 으로 거부한다. */
472
+ workspaceFile(workflowId, path) {
473
+ const params = new URLSearchParams({ path: workspaceStoragePath(path) });
474
+ return this.http.get(
475
+ `/api/agentflow/geny-workspace/${encodeURIComponent(workflowId)}/storage/text?${params}`
476
+ );
477
+ }
478
+ /** 원바이트 파일 읽기 — 이미지처럼 텍스트 API로 읽을 수 없는 미리보기용. */
479
+ workspaceBinary(workflowId, path, purpose) {
480
+ const encodedPath = workspaceStoragePath(path).split("/").map(encodeURIComponent).join("/");
481
+ const query = purpose ? `?purpose=${encodeURIComponent(purpose)}` : "";
482
+ return this.http.getBinary(
483
+ `/api/agentflow/geny-workspace/${encodeURIComponent(workflowId)}/storage-raw/${encodedPath}${query}`
484
+ );
485
+ }
486
+ /** Image bytes land in this agent's durable workspace before chat execution. */
487
+ workspaceUpload(workflowId, bytes, filename, mimeType, interactionId, attachmentId) {
488
+ const form = new FormData();
489
+ const owned = new Uint8Array(bytes);
490
+ form.append("file", new Blob([owned.buffer], { type: mimeType }), filename);
491
+ return this.http.upload(
492
+ `/api/agentflow/geny-workspace/${encodeURIComponent(workflowId)}/storage/upload?subdir=uploads&purpose=chat_attachment&interaction_id=${encodeURIComponent(interactionId)}&attachment_id=${encodeURIComponent(attachmentId)}`,
493
+ form,
494
+ { timeoutMs: 3e5 }
495
+ );
496
+ }
497
+ };
498
+
499
+ // ../../packages/protocol/src/agents.ts
500
+ function mapAgent(r) {
501
+ return {
502
+ id: r.id,
503
+ workflowId: r.workflow_id,
504
+ workflowName: r.workflow_name,
505
+ nodeCount: r.node_count ?? 0,
506
+ isShared: !!r.is_shared,
507
+ isDeployed: !!r.is_deployed,
508
+ isCompleted: !!r.is_completed,
509
+ workflowType: r.workflow_type ?? "canvas",
510
+ description: r.description ?? "",
511
+ username: r.username ?? "",
512
+ fullName: r.full_name ?? "",
513
+ createdAt: r.created_at ?? "",
514
+ updatedAt: r.updated_at ?? "",
515
+ hasAgentGeny: !!r.has_agent_geny
516
+ };
517
+ }
518
+ var AgentsApi = class {
519
+ constructor(http) {
520
+ this.http = http;
521
+ }
522
+ /** Paged agent list matching the UI grid (default page_size 24). */
523
+ async list(query = {}) {
524
+ const params = new URLSearchParams();
525
+ params.set("page", String(query.page ?? 1));
526
+ params.set("page_size", String(query.pageSize ?? 24));
527
+ if (query.search) params.set("search", query.search);
528
+ if (query.status) params.set("status", query.status);
529
+ if (query.owner) params.set("owner", query.owner);
530
+ if (query.includeHarness) params.set("include_harness", "true");
531
+ const res = await this.http.get(`/api/agentflow/list/detail?${params}`);
532
+ const raw = res.items ?? res.workflows ?? [];
533
+ return {
534
+ items: raw.map(mapAgent),
535
+ pagination: {
536
+ page: res.pagination?.page ?? query.page ?? 1,
537
+ pageSize: res.pagination?.page_size ?? query.pageSize ?? 24,
538
+ totalCount: res.pagination?.total_count ?? raw.length,
539
+ totalPages: res.pagination?.total_pages ?? 1
540
+ }
541
+ };
542
+ }
543
+ /**
544
+ * Fetch every page and return the full agent list. Convenience for small
545
+ * accounts / pickers; bounded by `maxPages` to avoid runaway loops.
546
+ */
547
+ async listAll(query = {}, maxPages = 50) {
548
+ const first = await this.list({ ...query, page: 1 });
549
+ const all = [...first.items];
550
+ for (let page = 2; page <= Math.min(first.pagination.totalPages, maxPages); page++) {
551
+ const next = await this.list({ ...query, page });
552
+ all.push(...next.items);
553
+ }
554
+ return all;
555
+ }
556
+ };
557
+
558
+ // ../../packages/protocol/src/hash.ts
559
+ async function subtle() {
560
+ const g = globalThis.crypto;
561
+ if (g?.subtle) return g.subtle;
562
+ const { webcrypto } = await import("node:crypto");
563
+ return webcrypto.subtle;
564
+ }
565
+ async function sha256Hex(plaintext) {
566
+ const data = new TextEncoder().encode(plaintext);
567
+ const digest = await (await subtle()).digest("SHA-256", data);
568
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
569
+ }
570
+
571
+ // ../../packages/protocol/src/auth.ts
572
+ var AuthApi = class {
573
+ constructor(http) {
574
+ this.http = http;
575
+ }
576
+ /**
577
+ * Log in with email + plaintext password. The password is SHA-256-hex hashed
578
+ * before sending (the gateway compares the hash verbatim). Returns tokens +
579
+ * identity. Throws ApiError on bad credentials / locked / inactive account.
580
+ */
581
+ async login(email, password) {
582
+ const passwordHash = await sha256Hex(password);
583
+ const res = await this.http.post(
584
+ "/api/auth/login",
585
+ { email, password: passwordHash, token: null }
586
+ // login itself must not trigger the onAuthFailure hook
587
+ );
588
+ if (!res.success || !res.access_token) {
589
+ throw new Error(res.message || "\uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
590
+ }
591
+ return {
592
+ accessToken: res.access_token,
593
+ refreshToken: res.refresh_token ?? void 0,
594
+ tokenType: res.token_type ?? "bearer",
595
+ userId: res.user_id ?? "",
596
+ username: res.username ?? email
597
+ };
598
+ }
599
+ /** SSO login with a pre-obtained token. */
600
+ async loginWithToken(ssoToken) {
601
+ const res = await this.http.post("/api/auth/login", {
602
+ token: ssoToken
603
+ });
604
+ if (!res.success || !res.access_token) {
605
+ throw new Error(res.message || "SSO \uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
606
+ }
607
+ return {
608
+ accessToken: res.access_token,
609
+ refreshToken: res.refresh_token ?? void 0,
610
+ tokenType: res.token_type ?? "bearer",
611
+ userId: res.user_id ?? "",
612
+ username: res.username ?? ""
613
+ };
614
+ }
615
+ /**
616
+ * Validate the access token and return the current user + permissions. If the
617
+ * access token is expired and a refresh token is supplied, the gateway may
618
+ * return a rotated access token in `newAccessToken`.
619
+ */
620
+ async validate(accessToken, refreshToken) {
621
+ const res = await this.http.post(
622
+ "/api/auth/validate-token",
623
+ { token: accessToken, refresh_token: refreshToken },
624
+ { auth: false }
625
+ );
626
+ if (!res.valid) return { user: null, newAccessToken: res.new_access_token ?? void 0 };
627
+ return {
628
+ user: {
629
+ userId: res.user_id ?? "",
630
+ username: res.username ?? "",
631
+ isSuperuser: !!res.is_superuser,
632
+ roles: res.roles ?? [],
633
+ permissions: res.permissions ?? []
634
+ },
635
+ newAccessToken: res.new_access_token ?? void 0
636
+ };
637
+ }
638
+ /** Exchange a refresh token for a fresh access token. */
639
+ async refresh(refreshToken) {
640
+ const res = await this.http.post(
641
+ "/api/auth/refresh",
642
+ { refresh_token: refreshToken },
643
+ { auth: false }
644
+ );
645
+ return res.success ? res.access_token : null;
646
+ }
647
+ async logout(accessToken) {
648
+ try {
649
+ await this.http.post("/api/auth/logout", { token: accessToken }, { timeoutMs: 8e3 });
650
+ } catch {
651
+ }
652
+ }
653
+ /** Server session policy (timeouts) — useful for a refresh scheduler. */
654
+ async sessionConfig() {
655
+ return this.http.get("/api/auth/session-config", { timeoutMs: 8e3 });
656
+ }
657
+ };
658
+
659
+ // ../../packages/protocol/src/avatars.ts
660
+ var AvatarsApi = class {
661
+ constructor(http) {
662
+ this.http = http;
663
+ }
664
+ /** Upload one avatar file (model zip or photo) → parsed descriptor. */
665
+ async uploadAsset(bytes, filename) {
666
+ const form = new FormData();
667
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
668
+ form.append("file", new Blob([buf]), filename);
669
+ const res = await this.http.upload("/api/storage/avatar/upload", form);
670
+ return res.avatar;
671
+ }
672
+ /** Delete an avatar's stored asset tree. */
673
+ async deleteAsset(avatarId) {
674
+ await this.http.json("DELETE", `/api/storage/avatar/${avatarId}`);
675
+ }
676
+ // ── store ──────────────────────────────────────────────────────
677
+ async storeList() {
678
+ const res = await this.http.get("/api/storage/avatar/store/list");
679
+ return res.items || [];
680
+ }
681
+ async storePublish(descriptor, name, description = "") {
682
+ const res = await this.http.post("/api/storage/avatar/store/publish", {
683
+ descriptor,
684
+ name,
685
+ description
686
+ });
687
+ return res.item;
688
+ }
689
+ /** Add a store avatar to my assets → descriptor with a fresh local id. */
690
+ async storeDownload(storeId) {
691
+ const res = await this.http.post(`/api/storage/avatar/store/${storeId}/download`, {});
692
+ return res.avatar;
693
+ }
694
+ async storeRate(storeId, stars) {
695
+ const res = await this.http.post(`/api/storage/avatar/store/${storeId}/rate`, { stars });
696
+ return res.item;
697
+ }
698
+ async storeUnpublish(storeId) {
699
+ await this.http.json("DELETE", `/api/storage/avatar/store/${storeId}`);
700
+ }
701
+ };
702
+
703
+ // ../../packages/protocol/src/sse.ts
704
+ var SseParser = class {
705
+ buffer = "";
706
+ /** Feed a raw chunk; returns any complete frames it produced. */
707
+ push(chunk) {
708
+ this.buffer += chunk;
709
+ const frames = [];
710
+ let sep2;
711
+ while ((sep2 = this.nextSeparator()) !== -1) {
712
+ const rawFrame = this.buffer.slice(0, sep2.valueOf());
713
+ this.buffer = this.buffer.slice(this.advanceAfterSeparator(sep2));
714
+ const frame = this.parseFrame(rawFrame);
715
+ if (frame) frames.push(frame);
716
+ }
717
+ return frames;
718
+ }
719
+ /** Flush any trailing frame not terminated by a blank line (stream end). */
720
+ flush() {
721
+ const rest = this.buffer.trim();
722
+ this.buffer = "";
723
+ if (!rest) return [];
724
+ const frame = this.parseFrame(rest);
725
+ return frame ? [frame] : [];
726
+ }
727
+ nextSeparator() {
728
+ const a = this.buffer.indexOf("\n\n");
729
+ const b = this.buffer.indexOf("\r\n\r\n");
730
+ if (a === -1) return b;
731
+ if (b === -1) return a;
732
+ return Math.min(a, b);
733
+ }
734
+ advanceAfterSeparator(sep2) {
735
+ return this.buffer.startsWith("\r\n\r\n", sep2) ? sep2 + 4 : sep2 + 2;
736
+ }
737
+ parseFrame(raw) {
738
+ let event;
739
+ const dataLines = [];
740
+ for (const line of raw.split(/\r?\n/)) {
741
+ if (!line || line.startsWith(":")) continue;
742
+ if (line.startsWith("event:")) {
743
+ event = line.slice(6).trim();
744
+ } else if (line.startsWith("data:")) {
745
+ dataLines.push(line.slice(5).replace(/^ /, ""));
746
+ }
747
+ }
748
+ if (dataLines.length === 0 && event === void 0) return null;
749
+ return { event, data: dataLines.join("\n") };
750
+ }
751
+ };
752
+
753
+ // ../../packages/protocol/src/chat.ts
754
+ function toRequestBody(req) {
755
+ return {
756
+ workflow_name: req.workflowName,
757
+ workflow_id: req.workflowId,
758
+ input_data: req.input,
759
+ interaction_id: req.interactionId,
760
+ selected_collections: req.selectedCollections ?? [],
761
+ selected_files: req.selectedFiles ?? [],
762
+ include_logs: req.includeLogs ?? true,
763
+ include_node_status: req.includeNodeStatus ?? true,
764
+ include_tool_events: req.includeToolEvents ?? true,
765
+ response_format: "stream",
766
+ // 대화 출처 — 이 턴이 데스크톱 커넥터에서 왔음을 서버에 알린다. 서버는 이
767
+ // 값이 "connector" 인 실행에만 커넥터 호스팅 로컬 도구(이 PC 의 파일/셸/
768
+ // 브라우저/오피스 조작)를 에이전트에 노출·실행한다. 웹 채팅은 이 필드를
769
+ // 보내지 않으므로, 같은 사용자가 커넥터를 켜 둔 상태로 웹에서 대화해도
770
+ // 로컬 도구는 절대 작동하지 않는다.
771
+ client_surface: "connector",
772
+ // 실행 환경 지시 — 로컬 실행 v2 폴백 턴은 'sandbox'(서버 sandbox 강제; 커넥터
773
+ // 로컬 워크스페이스를 원격 조작하는 중간 형태를 쓰지 않는다). 없으면 생략(auto).
774
+ ...req.executionTarget ? { execution_target: req.executionTarget } : {}
775
+ };
776
+ }
777
+ function mapToolEvent(d) {
778
+ return {
779
+ eventType: String(d.event_type ?? d.type ?? "tool"),
780
+ toolName: d.tool_name,
781
+ toolInput: d.tool_input,
782
+ result: d.result,
783
+ resultLength: d.result_length,
784
+ error: d.error,
785
+ citations: d.citations,
786
+ runId: d.run_id,
787
+ indicator: d.indicator,
788
+ durationMs: d.duration_ms,
789
+ timestamp: d.timestamp
790
+ };
791
+ }
792
+ function parseData(raw) {
793
+ try {
794
+ const v = JSON.parse(raw);
795
+ return v && typeof v === "object" ? v : null;
796
+ } catch {
797
+ return null;
798
+ }
799
+ }
800
+ function frameToChatEvent(frameEvent, rawData) {
801
+ const d = parseData(rawData);
802
+ switch (frameEvent) {
803
+ case "tool":
804
+ return d ? { kind: "tool", event: mapToolEvent(d) } : null;
805
+ case "node_status":
806
+ return d ? {
807
+ kind: "node_status",
808
+ event: { nodeId: String(d.node_id ?? ""), status: String(d.status ?? ""), ...d }
809
+ } : null;
810
+ case "log":
811
+ return { kind: "log", data: d ?? rawData };
812
+ case "execution_io":
813
+ return d ? { kind: "execution_io", executionIoId: Number(d.execution_io_id ?? 0) } : null;
814
+ case "download_artifact":
815
+ return d ? { kind: "download", data: d } : null;
816
+ case "a2ui_command":
817
+ return d ? { kind: "ui_command", surface: "a2ui", command: d } : null;
818
+ case "floui_command":
819
+ return d ? { kind: "ui_command", surface: "floui", command: d } : null;
820
+ case "quota_warning":
821
+ return d ? { kind: "quota", level: "warning", data: d } : null;
822
+ case "quota_exceeded":
823
+ return d ? { kind: "quota", level: "exceeded", data: d } : null;
824
+ case "execution_suspended":
825
+ return { kind: "error", detail: "\uC6CC\uD06C\uD50C\uB85C\uC6B0\uAC00 \uAD00\uB9AC\uC790\uC5D0 \uC758\uD574 \uC77C\uC2DC \uC911\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." };
826
+ case void 0:
827
+ case "":
828
+ case "message":
829
+ break;
830
+ // default frame — dispatch on the JSON `type` below
831
+ default:
832
+ return null;
833
+ }
834
+ if (!d) return null;
835
+ switch (d.type) {
836
+ case "data":
837
+ return { kind: "text", content: String(d.content ?? "") };
838
+ case "summary": {
839
+ const data = d.data ?? {};
840
+ const outputs = data.outputs ?? [];
841
+ return { kind: "summary", text: outputs.map(String).join(""), data };
842
+ }
843
+ case "end":
844
+ return { kind: "end" };
845
+ case "error":
846
+ return { kind: "error", detail: String(d.detail ?? d.error ?? "unknown error") };
847
+ // Some tool/agent frames arrive as bare `data:` JSON (no event: line).
848
+ case "tool_call":
849
+ case "tool_start":
850
+ case "tool_result":
851
+ case "tool_error":
852
+ return { kind: "tool", event: mapToolEvent(d) };
853
+ default:
854
+ return null;
855
+ }
856
+ }
857
+ var ChatApi = class {
858
+ constructor(http) {
859
+ this.http = http;
860
+ }
861
+ /**
862
+ * Stream a chat turn. Yields normalized ChatEvents until the terminal `end`
863
+ * (or the stream closes). Pass an AbortSignal to cancel mid-stream.
864
+ */
865
+ async *stream(req, signal) {
866
+ const res = await this.http.stream(
867
+ "/api/agentflow/execute/based-id/stream",
868
+ toRequestBody(req),
869
+ signal
870
+ );
871
+ const body = res.body;
872
+ if (!body) throw new Error("\uC2A4\uD2B8\uB9BC \uC751\uB2F5 \uBCF8\uBB38\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
873
+ const reader = body.getReader();
874
+ const decoder = new TextDecoder();
875
+ const parser = new SseParser();
876
+ try {
877
+ for (; ; ) {
878
+ const { value, done } = await reader.read();
879
+ if (done) break;
880
+ const frames = parser.push(decoder.decode(value, { stream: true }));
881
+ for (const f of frames) {
882
+ const ev = frameToChatEvent(f.event, f.data);
883
+ if (ev) {
884
+ yield ev;
885
+ if (ev.kind === "end") return;
886
+ }
887
+ }
888
+ }
889
+ for (const f of parser.flush()) {
890
+ const ev = frameToChatEvent(f.event, f.data);
891
+ if (ev) yield ev;
892
+ }
893
+ } finally {
894
+ try {
895
+ reader.releaseLock();
896
+ } catch {
897
+ }
898
+ }
899
+ }
900
+ /**
901
+ * Convenience: run a turn to completion and return the accumulated assistant
902
+ * text plus collected tool events. Ignores intermediate UI/log frames.
903
+ */
904
+ async complete(req, onEvent, signal) {
905
+ let text = "";
906
+ let summary = "";
907
+ const tools = [];
908
+ let error;
909
+ let executionIoId;
910
+ for await (const e of this.stream(req, signal)) {
911
+ onEvent?.(e);
912
+ if (e.kind === "text") text += e.content;
913
+ else if (e.kind === "summary") summary = e.text;
914
+ else if (e.kind === "tool") tools.push(e.event);
915
+ else if (e.kind === "execution_io") executionIoId = e.executionIoId;
916
+ else if (e.kind === "error") error = e.detail;
917
+ }
918
+ return { text: text || summary, tools, error, executionIoId };
919
+ }
920
+ };
921
+
922
+ // ../../packages/protocol/src/browser.ts
923
+ var BROWSER_CONTEXT_START = "<xgen_browser_context>";
924
+ var BROWSER_CONTEXT_END = "</xgen_browser_context>";
925
+ function stripBrowserContext(text) {
926
+ if (typeof text !== "string" || !text.startsWith(BROWSER_CONTEXT_START)) return text;
927
+ const end = text.indexOf(BROWSER_CONTEXT_END);
928
+ if (end < 0) return text;
929
+ return text.slice(end + BROWSER_CONTEXT_END.length).replace(/^\r?\n/, "");
930
+ }
931
+
932
+ // ../../packages/protocol/src/history.ts
933
+ function toHistoryAttachments(value) {
934
+ if (!Array.isArray(value)) return [];
935
+ const result = [];
936
+ for (const item of value) {
937
+ if (!item || typeof item !== "object") continue;
938
+ const raw = item;
939
+ const path = String(raw.minioPath ?? raw.filePath ?? raw.object_name ?? raw.path ?? "").trim();
940
+ if (!path) continue;
941
+ const name = String(raw.name ?? raw.original_name ?? path.split("/").pop() ?? "attachment");
942
+ const contentType = String(raw.contentType ?? raw.content_type ?? "application/octet-stream").split(";", 1)[0].trim().toLowerCase();
943
+ const type = raw.type === "picture" || contentType.startsWith("image/") ? "picture" : "file";
944
+ const numericSize = Number(raw.size ?? raw.file_size ?? 0);
945
+ result.push({
946
+ id: typeof raw.id === "string" || typeof raw.id === "number" ? raw.id : void 0,
947
+ name,
948
+ size: Number.isFinite(numericSize) && numericSize > 0 ? numericSize : 0,
949
+ contentType,
950
+ type,
951
+ path,
952
+ bucket: String(raw.bucket ?? "")
953
+ });
954
+ }
955
+ return result;
956
+ }
957
+ function toDisplayText(v) {
958
+ if (v == null) return "";
959
+ if (typeof v === "string") return stripBrowserContext(v);
960
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
961
+ if (Array.isArray(v)) {
962
+ const parts = v.map((b) => {
963
+ if (b == null) return "";
964
+ if (typeof b === "string") return b;
965
+ if (typeof b === "object") {
966
+ const o = b;
967
+ if (typeof o.text === "string") return o.text;
968
+ const t = typeof o.type === "string" ? o.type : "";
969
+ if (t.includes("image")) return "[\uC774\uBBF8\uC9C0]";
970
+ try {
971
+ return JSON.stringify(b);
972
+ } catch {
973
+ return String(b);
974
+ }
975
+ }
976
+ return String(b);
977
+ });
978
+ return stripBrowserContext(parts.filter(Boolean).join("\n"));
979
+ }
980
+ try {
981
+ return JSON.stringify(v, null, 2);
982
+ } catch {
983
+ return String(v);
984
+ }
985
+ }
986
+ var HistoryApi = class {
987
+ constructor(http) {
988
+ this.http = http;
989
+ }
990
+ /** Ordered turns of one conversation. */
991
+ async turns(workflowId, interactionId, workflowName) {
992
+ const params = new URLSearchParams({ workflow_id: workflowId, interaction_id: interactionId });
993
+ if (workflowName) params.set("workflow_name", workflowName);
994
+ const res = await this.http.get(`/api/chat/io-logs?${params}`);
995
+ return (res.in_out_logs ?? []).map((r) => ({
996
+ logId: r.log_id,
997
+ ioId: r.io_id,
998
+ interactionId: r.interaction_id,
999
+ workflowId: r.workflow_id,
1000
+ workflowName: r.workflow_name,
1001
+ input: toDisplayText(r.input_data),
1002
+ output: toDisplayText(r.output_data),
1003
+ attachments: toHistoryAttachments(r.attachments),
1004
+ updatedAt: r.updated_at
1005
+ }));
1006
+ }
1007
+ /** Past conversations (interactions) for the sidebar. */
1008
+ async conversations() {
1009
+ const res = await this.http.get("/api/interaction/list");
1010
+ return (res.execution_meta_list ?? []).map((r) => ({
1011
+ id: r.id,
1012
+ interactionId: r.interaction_id,
1013
+ workflowId: r.workflow_id,
1014
+ workflowName: r.workflow_name,
1015
+ interactionCount: r.interaction_count ?? 0,
1016
+ metadata: r.metadata ?? {},
1017
+ createdAt: r.created_at ?? "",
1018
+ updatedAt: r.updated_at ?? ""
1019
+ }));
1020
+ }
1021
+ };
1022
+
1023
+ // ../../packages/protocol/src/preferences.ts
1024
+ var PreferencesApi = class {
1025
+ constructor(http) {
1026
+ this.http = http;
1027
+ }
1028
+ /** GET /api/admin/user → preferences.avatar.
1029
+ *
1030
+ * THROWS on failure (network / 401 / not-yet-authenticated) rather than
1031
+ * returning an empty config: at startup the overlay's fetch can beat the
1032
+ * main window's session restore, and masking that as `{enabled:false}` made
1033
+ * the avatar look permanently absent. Propagating lets the caller retry until
1034
+ * the client is authed. A genuinely empty config (feature off) still returns
1035
+ * normally. */
1036
+ async getAvatarConfig() {
1037
+ const res = await this.http.get("/api/admin/user");
1038
+ if (!res || !res.user) {
1039
+ throw new Error("avatar config: no authenticated profile");
1040
+ }
1041
+ let prefs = res.user.preferences ?? {};
1042
+ if (typeof prefs === "string") {
1043
+ try {
1044
+ prefs = JSON.parse(prefs);
1045
+ } catch {
1046
+ prefs = {};
1047
+ }
1048
+ }
1049
+ const raw = prefs?.avatar ?? {};
1050
+ return {
1051
+ enabled: !!raw.enabled,
1052
+ defaultAvatarId: typeof raw.defaultAvatarId === "string" ? raw.defaultAvatarId : null,
1053
+ avatars: Array.isArray(raw.avatars) ? raw.avatars : []
1054
+ };
1055
+ }
1056
+ /** Persist the whole avatar config (PUT shallow-merges preferences top-level,
1057
+ * so sending {avatar} replaces just that key). Used when the overlay adjusts
1058
+ * the avatar's scale/position in-place. */
1059
+ async saveAvatarConfig(config) {
1060
+ await this.http.put("/api/admin/user", { preferences: { avatar: config } });
1061
+ }
1062
+ /** Read-modify-write: 서버의 CURRENT config 를 읽어 최소 패치만 적용한다.
1063
+ * 화면에 캐시된 스냅샷 전체를 저장하면 그 사이의 변경(선택 등)을 조용히
1064
+ * 되돌린다 — 모든 부분 수정은 반드시 이 경로를 쓴다. */
1065
+ async mutateAvatarConfig(mutate) {
1066
+ const cfg = await this.getAvatarConfig();
1067
+ const next = mutate(cfg);
1068
+ await this.saveAvatarConfig(next);
1069
+ return next;
1070
+ }
1071
+ /** Persist ONE avatar's scale/position (read-modify-write). */
1072
+ async saveAvatarTransform(avatarId, tf) {
1073
+ await this.mutateAvatarConfig((cfg) => ({
1074
+ ...cfg,
1075
+ avatars: cfg.avatars.map(
1076
+ (a) => a.id === avatarId ? { ...a, scale: tf.scale, position: tf.position } : a
1077
+ )
1078
+ }));
1079
+ }
1080
+ setAvatarEnabled(enabled) {
1081
+ return this.mutateAvatarConfig((c) => ({ ...c, enabled }));
1082
+ }
1083
+ selectAvatar(id) {
1084
+ return this.mutateAvatarConfig((c) => ({ ...c, defaultAvatarId: id }));
1085
+ }
1086
+ renameAvatar(id, name) {
1087
+ return this.mutateAvatarConfig((c) => ({
1088
+ ...c,
1089
+ avatars: c.avatars.map((a) => a.id === id ? { ...a, name } : a)
1090
+ }));
1091
+ }
1092
+ /** Add an uploaded/downloaded descriptor (optionally renamed); first avatar
1093
+ * becomes the selection. */
1094
+ addAvatar(descriptor, name) {
1095
+ return this.mutateAvatarConfig((c) => ({
1096
+ ...c,
1097
+ avatars: [...c.avatars, { ...descriptor, name: (name ?? descriptor.name) || descriptor.name }],
1098
+ defaultAvatarId: c.defaultAvatarId ?? descriptor.id
1099
+ }));
1100
+ }
1101
+ removeAvatar(id) {
1102
+ return this.mutateAvatarConfig((c) => {
1103
+ const remaining = c.avatars.filter((a) => a.id !== id);
1104
+ return {
1105
+ ...c,
1106
+ avatars: remaining,
1107
+ defaultAvatarId: c.defaultAvatarId === id ? remaining[0]?.id ?? null : c.defaultAvatarId
1108
+ };
1109
+ });
1110
+ }
1111
+ };
1112
+
1113
+ // ../../packages/protocol/src/ssh.ts
1114
+ var BASE = "/api/agentflow/user-ssh";
1115
+ var SshApi = class {
1116
+ constructor(http) {
1117
+ this.http = http;
1118
+ }
1119
+ getConfig() {
1120
+ return this.http.get(`${BASE}/config`);
1121
+ }
1122
+ /** Master switch only — the server list survives being turned off. */
1123
+ setEnabled(enabled) {
1124
+ return this.http.put(`${BASE}/config`, { enabled });
1125
+ }
1126
+ createServer(input) {
1127
+ return this.http.post(`${BASE}/servers`, input);
1128
+ }
1129
+ /** Partial update. Renaming also rewrites this server out of others' jump paths. */
1130
+ updateServer(name, input) {
1131
+ return this.http.put(`${BASE}/servers/${encodeURIComponent(name)}`, input);
1132
+ }
1133
+ /** Refused (400) while another server still lists it as a jump host. */
1134
+ deleteServer(name) {
1135
+ return this.http.del(`${BASE}/servers/${encodeURIComponent(name)}`);
1136
+ }
1137
+ /**
1138
+ * Dial it for real, through the jump path.
1139
+ *
1140
+ * Works regardless of the master switch — you must be able to check a server
1141
+ * *before* turning the feature on, otherwise the only order available is
1142
+ * "switch it on and hope".
1143
+ *
1144
+ * The connection is opened by the XGEN server, not this machine: the agent
1145
+ * runs there, so that is the only reachability that matters.
1146
+ */
1147
+ testServer(name) {
1148
+ return this.http.post(
1149
+ `${BASE}/servers/${encodeURIComponent(name)}/test`,
1150
+ {},
1151
+ // A three-hop chain can legitimately take a while; the default JSON
1152
+ // timeout would report a failure the server never saw.
1153
+ { timeoutMs: 7e4 }
1154
+ );
1155
+ }
1156
+ };
1157
+
1158
+ // ../../packages/protocol/src/teams.ts
1159
+ var str = (v, fallback = "") => typeof v === "string" ? v : v === null || v === void 0 ? fallback : String(v);
1160
+ var num = (v, fallback = 0) => {
1161
+ const n = Number(v);
1162
+ return Number.isFinite(n) ? n : fallback;
1163
+ };
1164
+ function normalizeRouterMode(v) {
1165
+ const raw = str(v, "hybrid");
1166
+ if (raw === "chat" || raw === "manual" || raw === "hybrid" || raw === "auto") return raw;
1167
+ return "hybrid";
1168
+ }
1169
+ function normalizeSenderType(v) {
1170
+ const raw = str(v, "user");
1171
+ if (raw === "agent" || raw === "router" || raw === "system") return raw;
1172
+ return "user";
1173
+ }
1174
+ function mapRoom(raw) {
1175
+ const r = raw ?? {};
1176
+ return {
1177
+ id: str(r.id),
1178
+ name: str(r.name, "\uC774\uB984 \uC5C6\uB294 \uB300\uD654"),
1179
+ description: str(r.description) || void 0,
1180
+ routerMode: normalizeRouterMode(r.router_mode),
1181
+ isDirect: Boolean(r.is_direct),
1182
+ createdAt: str(r.created_at),
1183
+ createdBy: num(r.created_by),
1184
+ lastMessageAt: str(r.last_message_at) || void 0
1185
+ };
1186
+ }
1187
+ function mapMember(raw) {
1188
+ const m = raw ?? {};
1189
+ const role = str(m.role, "member");
1190
+ return {
1191
+ userId: num(m.user_id),
1192
+ username: str(m.username) || `User-${num(m.user_id)}`,
1193
+ fullName: str(m.full_name) || str(m.name) || void 0,
1194
+ role: role === "owner" || role === "admin" ? role : "member",
1195
+ isOnline: Boolean(m.is_online),
1196
+ joinedAt: str(m.joined_at)
1197
+ };
1198
+ }
1199
+ function directRoomNameForViewer(room, members, viewerUserId) {
1200
+ if (!room.isDirect || !viewerUserId) return room.name;
1201
+ const other = members.find((member) => String(member.userId) !== viewerUserId);
1202
+ return other ? other.fullName || other.username || room.name : room.name;
1203
+ }
1204
+ function mapReactions(raw) {
1205
+ if (!Array.isArray(raw) || raw.length === 0) return void 0;
1206
+ return raw.map((item) => {
1207
+ const r = item ?? {};
1208
+ return {
1209
+ emoji: str(r.emoji),
1210
+ count: num(r.count),
1211
+ userIds: Array.isArray(r.user_ids) ? r.user_ids.map((x) => num(x)) : []
1212
+ };
1213
+ });
1214
+ }
1215
+ function mapAttachment(raw) {
1216
+ const a = raw ?? {};
1217
+ const extracted = str(a.extracted_text) || str(a.extractedText);
1218
+ return {
1219
+ id: str(a.id),
1220
+ filename: str(a.original_filename) || str(a.display_name) || str(a.name) || str(a.filename, "file"),
1221
+ mime: str(a.mime, "application/octet-stream"),
1222
+ size: num(a.size),
1223
+ storageKey: str(a.storage_key) || str(a.storageKey),
1224
+ extractedText: extracted || void 0,
1225
+ truncated: Boolean(a.truncated) || void 0
1226
+ };
1227
+ }
1228
+ function mapAttachments(raw) {
1229
+ let arr = [];
1230
+ if (Array.isArray(raw)) arr = raw;
1231
+ else if (typeof raw === "string" && raw.trim().startsWith("[")) {
1232
+ try {
1233
+ const parsed = JSON.parse(raw);
1234
+ if (Array.isArray(parsed)) arr = parsed;
1235
+ } catch {
1236
+ arr = [];
1237
+ }
1238
+ }
1239
+ if (arr.length === 0) return void 0;
1240
+ return arr.map((item) => {
1241
+ const { extractedText: _drop, ...meta } = mapAttachment(item);
1242
+ return meta;
1243
+ });
1244
+ }
1245
+ function senderName(raw, type, senderId) {
1246
+ const name = str(raw).trim();
1247
+ if (name) return name;
1248
+ if (type === "system") return "\uC2DC\uC2A4\uD15C";
1249
+ if (type === "agent") return "Agent";
1250
+ return senderId ? `User-${senderId}` : "\uC54C \uC218 \uC5C6\uC74C";
1251
+ }
1252
+ function mapMessage(raw) {
1253
+ const m = raw ?? {};
1254
+ const type = normalizeSenderType(m.sender_type);
1255
+ const senderId = str(m.sender_id);
1256
+ return {
1257
+ id: str(m.id),
1258
+ roomId: str(m.room_id),
1259
+ senderType: type,
1260
+ senderId,
1261
+ senderName: senderName(m.sender_name, type, senderId),
1262
+ content: str(m.content),
1263
+ createdAt: str(m.created_at),
1264
+ reactions: mapReactions(m.reactions),
1265
+ attachments: mapAttachments(m.attachments),
1266
+ replyToId: str(m.reply_to_id) || void 0,
1267
+ replyToSenderName: str(m.reply_to_sender_name) || void 0,
1268
+ replyToContent: str(m.reply_to_content) || void 0,
1269
+ isEdited: Boolean(m.is_edited),
1270
+ editedAt: str(m.edited_at) || void 0
1271
+ };
1272
+ }
1273
+ function safeMapMessage(raw) {
1274
+ if (!raw || typeof raw !== "object") return null;
1275
+ try {
1276
+ const mapped = mapMessage(raw);
1277
+ return mapped.id ? mapped : null;
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
1282
+ var TEAMS_ATTACHMENT_MAX_BYTES = 50 * 1024 * 1024;
1283
+ var TeamsApi = class {
1284
+ constructor(http) {
1285
+ this.http = http;
1286
+ }
1287
+ // ── 방 ───────────────────────────────────────────────────
1288
+ /** 내가 속한 방 전체. 최근 메시지 순 정렬은 호출자(렌더러 store)가 한다. */
1289
+ async listRooms(viewerUserId) {
1290
+ const res = await this.http.get("/api/teams/rooms/list");
1291
+ const rooms = (res.data ?? []).map(mapRoom);
1292
+ if (!viewerUserId) return rooms;
1293
+ return Promise.all(
1294
+ rooms.map(async (room) => {
1295
+ if (!room.isDirect) return room;
1296
+ try {
1297
+ const members = await this.listMembers(room.id);
1298
+ return { ...room, name: directRoomNameForViewer(room, members, viewerUserId) };
1299
+ } catch {
1300
+ return room;
1301
+ }
1302
+ })
1303
+ );
1304
+ }
1305
+ async getRoom(roomId) {
1306
+ const res = await this.http.get(
1307
+ `/api/teams/rooms/${encodeURIComponent(roomId)}`
1308
+ );
1309
+ return res.data ? mapRoom(res.data) : null;
1310
+ }
1311
+ async createRoom(opts) {
1312
+ const res = await this.http.post("/api/teams/rooms/create", {
1313
+ name: opts.name,
1314
+ description: opts.description ?? null,
1315
+ router_mode: opts.routerMode ?? "chat"
1316
+ });
1317
+ return mapRoom(res.data);
1318
+ }
1319
+ /**
1320
+ * 1:1 대화 — 이미 있으면 그 방을, 없으면 새로 만들어 돌려준다.
1321
+ * 서버가 `dm:u{min}:u{max}` 키로 중복을 막으므로 클라이언트가 찾을 필요가 없다.
1322
+ */
1323
+ async openDirectMessage(userId, username) {
1324
+ const res = await this.http.post(
1325
+ "/api/teams/rooms/dm/lookup-or-create",
1326
+ {
1327
+ target_type: "user",
1328
+ target_id: String(userId),
1329
+ target_name: username ?? null,
1330
+ target_description: null,
1331
+ target_color: null
1332
+ }
1333
+ );
1334
+ return mapRoom(res.data?.room);
1335
+ }
1336
+ /**
1337
+ * 방 정보 수정 (이름·설명). 서버는 **멤버 전원**에게 허용한다(방장 전용이 아니다).
1338
+ *
1339
+ * ⚠ 서버가 이 변경을 broadcast 하지 않는다 — 다른 클라이언트는 새로고침 전까지
1340
+ * 옛 이름을 본다. 우리 화면만 즉시 갱신할 수 있다.
1341
+ */
1342
+ async updateRoom(roomId, patch) {
1343
+ const res = await this.http.put(
1344
+ `/api/teams/rooms/${encodeURIComponent(roomId)}`,
1345
+ {
1346
+ // 서버는 null 을 "변경 없음" 으로 읽는다. 보내지 않을 값은 넣지 않는다.
1347
+ ...patch.name !== void 0 ? { name: patch.name } : {},
1348
+ ...patch.description !== void 0 ? { description: patch.description } : {}
1349
+ }
1350
+ );
1351
+ return res.data ? mapRoom(res.data) : null;
1352
+ }
1353
+ /** 마지막 멤버의 나가기를 빈 방 정리로 바꿀 때만 쓰는 내부 경로. */
1354
+ async deleteRoom(roomId) {
1355
+ await this.http.del(`/api/teams/rooms/${encodeURIComponent(roomId)}`);
1356
+ }
1357
+ /**
1358
+ * 사용자가 보는 방 종료 동작은 항상 "나가기" 하나다. 마지막 멤버라면 빈 방을
1359
+ * 남기지 않도록 내부적으로 방을 정리한다. 멤버 조회나 정리 권한이 없는 구버전
1360
+ * 서버에서는 기존 leave API 로 폴백해 사용자가 방에 갇히지 않게 한다.
1361
+ */
1362
+ async leaveRoom(roomId) {
1363
+ let lastMember = false;
1364
+ try {
1365
+ lastMember = (await this.listMembers(roomId)).length <= 1;
1366
+ } catch {
1367
+ }
1368
+ if (lastMember) {
1369
+ try {
1370
+ await this.deleteRoom(roomId);
1371
+ return;
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ await this.http.post(`/api/teams/rooms/${encodeURIComponent(roomId)}/leave`);
1376
+ }
1377
+ // ── 멤버 ─────────────────────────────────────────────────
1378
+ async listMembers(roomId) {
1379
+ const res = await this.http.get(
1380
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/members`
1381
+ );
1382
+ return (res.data ?? []).map(mapMember);
1383
+ }
1384
+ async addMember(roomId, userId) {
1385
+ await this.http.post(`/api/teams/rooms/${encodeURIComponent(roomId)}/members`, {
1386
+ user_id: userId,
1387
+ role: "member",
1388
+ force_override: false
1389
+ });
1390
+ }
1391
+ /** 초대 대상 검색. 빈 질의는 서버를 부르지 않는다. */
1392
+ async searchUsers(query, limit = 20) {
1393
+ const q = query.trim();
1394
+ if (!q) return [];
1395
+ const params = new URLSearchParams({ q, limit: String(limit) });
1396
+ const res = await this.http.get(`/api/teams/users/search?${params}`);
1397
+ return (res.data ?? []).map((item) => {
1398
+ const u = item ?? {};
1399
+ const id = num(u.id);
1400
+ return {
1401
+ id,
1402
+ username: str(u.username) || str(u.user_name) || `user_${id}`,
1403
+ fullName: str(u.full_name) || str(u.name) || void 0,
1404
+ email: str(u.email) || void 0
1405
+ };
1406
+ });
1407
+ }
1408
+ // ── 메시지 ───────────────────────────────────────────────
1409
+ /**
1410
+ * 메시지 조회 (커서 페이지네이션). `before` 는 더 과거를 부르는 커서이고,
1411
+ * 서버는 **최신순**으로 돌려주므로 시간 오름차순 정렬은 호출자가 한다
1412
+ * (렌더러 store 의 mergeMessages 가 담당).
1413
+ */
1414
+ async listMessages(roomId, opts) {
1415
+ const params = new URLSearchParams({ limit: String(opts?.limit ?? 50) });
1416
+ if (opts?.before) params.set("before", opts.before);
1417
+ const res = await this.http.get(
1418
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/messages?${params}`
1419
+ );
1420
+ return (res.data ?? []).flatMap((raw) => {
1421
+ const mapped = safeMapMessage(raw);
1422
+ return mapped ? [mapped] : [];
1423
+ });
1424
+ }
1425
+ /**
1426
+ * 메시지 전송. 응답의 `data.message` 가 서버가 확정한 메시지다 —
1427
+ * 낙관적으로 그려 둔 임시 메시지를 이것으로 교체한다.
1428
+ *
1429
+ * 라우팅 결과(`data.routing`)는 에이전트 실행용이라 1차 범위에서는 버린다.
1430
+ * router_mode='chat' 방에서는 서버가 애초에 에이전트를 부르지 않는다.
1431
+ */
1432
+ async sendMessage(roomId, content, opts) {
1433
+ const res = await this.http.post(
1434
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/messages`,
1435
+ {
1436
+ content,
1437
+ mentioned_agent_ids: null,
1438
+ attachments: opts?.attachments?.length ? opts.attachments.map((a) => ({
1439
+ id: a.id,
1440
+ filename: a.filename,
1441
+ mime: a.mime,
1442
+ size: a.size,
1443
+ storage_key: a.storageKey,
1444
+ // 업로드 응답을 그대로 되돌려준다 — null 로 덮으면 첨부 내용이 사라진다.
1445
+ extracted_text: a.extractedText ?? null,
1446
+ truncated: a.truncated ?? false
1447
+ })) : null,
1448
+ discussion_max_rounds: null,
1449
+ reply_to_id: opts?.replyToId ?? null
1450
+ }
1451
+ );
1452
+ const mapped = safeMapMessage(res.data?.message);
1453
+ if (!mapped) throw new Error("\uBA54\uC2DC\uC9C0\uB97C \uBCF4\uB0C8\uC9C0\uB9CC \uC11C\uBC84 \uC751\uB2F5\uC744 \uD574\uC11D\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
1454
+ return mapped;
1455
+ }
1456
+ /** 본인 메시지 편집. 서버가 `message_updated` 를 broadcast 한다. */
1457
+ async editMessage(roomId, messageId, content) {
1458
+ const res = await this.http.patch(
1459
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`,
1460
+ { content }
1461
+ );
1462
+ return safeMapMessage(res.data);
1463
+ }
1464
+ // ── 첨부 ─────────────────────────────────────────────────
1465
+ /**
1466
+ * 첨부 업로드 → 메타. 이 메타를 `sendMessage` 의 `attachments` 로 넘겨야
1467
+ * 실제로 메시지에 붙는다 (업로드만으로는 방에 나타나지 않는다).
1468
+ *
1469
+ * 서버가 같은 요청 안에서 문서 본문까지 추출해 `extracted_text` 로 돌려주므로
1470
+ * 응답을 통째로 들고 다닌다 — 그래야 나중에 에이전트가 그 파일의 내용을 본다.
1471
+ * 상한은 서버 기준 50MB, 허용 확장자는 `attachment_controller.ALLOWED_EXTENSIONS`.
1472
+ */
1473
+ async uploadAttachment(roomId, bytes, filename, mime) {
1474
+ const form = new FormData();
1475
+ const buf = bytes.buffer.slice(
1476
+ bytes.byteOffset,
1477
+ bytes.byteOffset + bytes.byteLength
1478
+ );
1479
+ form.append("file", new Blob([buf], mime ? { type: mime } : void 0), filename);
1480
+ const res = await this.http.upload(
1481
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/attachments/upload`,
1482
+ form,
1483
+ // 50MB 까지 받는 엔드포인트라 기본 타임아웃으로는 큰 파일이 끊긴다.
1484
+ { timeoutMs: 3e5 }
1485
+ );
1486
+ const mapped = mapAttachment(res.data);
1487
+ if (!mapped.storageKey) throw new Error("\uCCA8\uBD80\uB97C \uC62C\uB838\uC9C0\uB9CC \uC11C\uBC84 \uC751\uB2F5\uC744 \uD574\uC11D\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
1488
+ return { ...mapped, filename: mapped.filename || filename };
1489
+ }
1490
+ /**
1491
+ * 첨부 원본 바이트. 다운로드 주소에 `filename` 을 함께 넘겨야 서버가
1492
+ * Content-Disposition 에 실제 이름을 실어 준다 (안 넘기면 `att-xxx.docx` 로 떨어진다).
1493
+ */
1494
+ async downloadAttachment(roomId, attachment) {
1495
+ const params = attachment.filename ? `?filename=${encodeURIComponent(attachment.filename)}` : "";
1496
+ const { bytes } = await this.http.getBinary(
1497
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/attachments/${encodeURIComponent(
1498
+ attachment.storageKey
1499
+ )}${params}`
1500
+ );
1501
+ return bytes;
1502
+ }
1503
+ /**
1504
+ * 이모지 리액션 토글. 서버가 집계 전체를 돌려주고 동시에 `reaction_update` 를
1505
+ * broadcast 하므로, 반환값은 즉시 반영용 보조다.
1506
+ */
1507
+ async toggleReaction(roomId, messageId, emoji) {
1508
+ const res = await this.http.post(
1509
+ `/api/teams/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/reactions`,
1510
+ { emoji }
1511
+ );
1512
+ return mapReactions(res.data?.reactions) ?? [];
1513
+ }
1514
+ };
1515
+
1516
+ // ../../packages/protocol/src/voice.ts
1517
+ function filenameFor(mime) {
1518
+ const m = (mime || "").toLowerCase();
1519
+ if (m.includes("webm")) return "audio.webm";
1520
+ if (m.includes("ogg")) return "audio.ogg";
1521
+ if (m.includes("wav")) return "audio.wav";
1522
+ if (m.includes("mp4") || m.includes("m4a") || m.includes("aac")) return "audio.m4a";
1523
+ if (m.includes("mpeg") || m.includes("mp3")) return "audio.mp3";
1524
+ return "audio.webm";
1525
+ }
1526
+ var VoiceApi = class {
1527
+ constructor(http) {
1528
+ this.http = http;
1529
+ }
1530
+ /** GET /api/admin/user → { stt: preferences.stt|null, tts: preferences.tts|null }.
1531
+ * UI hints only — no secrets. THROWS when not yet authenticated (mirrors
1532
+ * PreferencesApi.getAvatarConfig) so callers can retry rather than latch a
1533
+ * false "voice off". */
1534
+ async getVoiceConfig() {
1535
+ const res = await this.http.get("/api/admin/user");
1536
+ if (!res || !res.user) {
1537
+ throw new Error("voice config: no authenticated profile");
1538
+ }
1539
+ let prefs = res.user.preferences ?? {};
1540
+ if (typeof prefs === "string") {
1541
+ try {
1542
+ prefs = JSON.parse(prefs);
1543
+ } catch {
1544
+ prefs = {};
1545
+ }
1546
+ }
1547
+ const p = prefs ?? {};
1548
+ const stt = p.stt && typeof p.stt === "object" ? p.stt : null;
1549
+ const tts = p.tts && typeof p.tts === "object" ? p.tts : null;
1550
+ return { stt, tts };
1551
+ }
1552
+ /** POST an audio clip → transcript text. Uses the caller's saved STT
1553
+ * preference server-side unless `language` overrides it. */
1554
+ async transcribe(blob, language) {
1555
+ const form = new FormData();
1556
+ form.append("file", blob, filenameFor(blob.type));
1557
+ if (language) form.append("language", language);
1558
+ const res = await this.http.upload("/api/audio/stt/transcribe", form);
1559
+ return res?.text ?? "";
1560
+ }
1561
+ /** POST text → synthesized audio Blob. Uses the caller's active TTS profile
1562
+ * server-side unless `opts` overrides it. */
1563
+ async speak(text, opts) {
1564
+ const { bytes, contentType } = await this.http.postBinary(
1565
+ "/api/audio/tts/speak",
1566
+ { text, ...opts ?? {} },
1567
+ // Synthesis can take a few seconds for longer replies.
1568
+ { timeoutMs: 6e4 }
1569
+ );
1570
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1571
+ return new Blob([buf], { type: contentType || "audio/wav" });
1572
+ }
1573
+ };
1574
+
1575
+ // ../../packages/protocol/src/index.ts
1576
+ var XgenClient = class {
1577
+ http;
1578
+ auth;
1579
+ agents;
1580
+ chat;
1581
+ history;
1582
+ preferences;
1583
+ ssh;
1584
+ teams;
1585
+ avatars;
1586
+ voice;
1587
+ agentData;
1588
+ refreshToken;
1589
+ onTokensRotated;
1590
+ /** ensureFreshAuth 의 single-flight 가드 — 동시 401 들이 refresh 를 한 번만 태운다. */
1591
+ refreshing = null;
1592
+ user = null;
1593
+ constructor(opts) {
1594
+ this.http = new HttpClient({
1595
+ baseUrl: opts.baseUrl,
1596
+ fetch: opts.fetch,
1597
+ onAuthFailure: opts.onAuthFailure
1598
+ });
1599
+ if (opts.accessToken) this.http.setToken(opts.accessToken);
1600
+ this.refreshToken = opts.refreshToken;
1601
+ this.onTokensRotated = opts.onTokensRotated;
1602
+ this.auth = new AuthApi(this.http);
1603
+ this.agents = new AgentsApi(this.http);
1604
+ this.chat = new ChatApi(this.http);
1605
+ this.history = new HistoryApi(this.http);
1606
+ this.preferences = new PreferencesApi(this.http);
1607
+ this.ssh = new SshApi(this.http);
1608
+ this.teams = new TeamsApi(this.http);
1609
+ this.avatars = new AvatarsApi(this.http);
1610
+ this.voice = new VoiceApi(this.http);
1611
+ this.agentData = new AgentDataApi(this.http);
1612
+ }
1613
+ setBaseUrl(baseUrl) {
1614
+ this.http.setBaseUrl(baseUrl);
1615
+ }
1616
+ setTokens(accessToken, refreshToken) {
1617
+ this.http.setToken(accessToken);
1618
+ if (refreshToken !== void 0) this.refreshToken = refreshToken;
1619
+ }
1620
+ /** Log in and adopt the returned tokens. */
1621
+ async login(email, password) {
1622
+ return this.adoptLogin(await this.auth.login(email, password));
1623
+ }
1624
+ /** Adopt tokens returned by an external SSO bridge and resolve full identity. */
1625
+ async adoptLogin(res) {
1626
+ this.http.setToken(res.accessToken);
1627
+ this.refreshToken = res.refreshToken;
1628
+ this.onTokensRotated?.(res.accessToken, res.refreshToken);
1629
+ this.user = {
1630
+ userId: res.userId,
1631
+ username: res.username,
1632
+ isSuperuser: false,
1633
+ roles: [],
1634
+ permissions: []
1635
+ };
1636
+ try {
1637
+ const { user } = await this.auth.validate(res.accessToken, res.refreshToken);
1638
+ if (user) this.user = user;
1639
+ } catch {
1640
+ }
1641
+ return res;
1642
+ }
1643
+ /**
1644
+ * Validate the current session, rotating the access token if the gateway
1645
+ * returned a fresh one. Returns true if still/again authenticated.
1646
+ */
1647
+ async restore(accessToken, refreshToken) {
1648
+ return await this.restoreDetailed(accessToken, refreshToken) === "valid";
1649
+ }
1650
+ /**
1651
+ * restore() 의 판정 세분화 — 호출자가 토큰 폐기 여부를 올바르게 정할 수
1652
+ * 있게 한다 (geny-connector validateAndRefreshAuth 강건성 이식):
1653
+ * 'valid' — 인증 성공 (토큰 회전 반영됨)
1654
+ * 'invalid' — 서버가 **응답으로** 거부 (토큰 폐기가 맞다)
1655
+ * 'network' — 서버 미응답/네트워크 오류 (토큰을 지우면 안 된다 — 일시
1656
+ * 장애 후 재시작에서 재로그인을 강요하게 된다)
1657
+ */
1658
+ async restoreDetailed(accessToken, refreshToken) {
1659
+ this.http.setToken(accessToken);
1660
+ this.refreshToken = refreshToken;
1661
+ let sawNetworkError = false;
1662
+ try {
1663
+ const { user, newAccessToken } = await this.auth.validate(accessToken, refreshToken);
1664
+ if (newAccessToken) {
1665
+ this.http.setToken(newAccessToken);
1666
+ this.onTokensRotated?.(newAccessToken, refreshToken);
1667
+ }
1668
+ if (user) {
1669
+ this.user = user;
1670
+ return "valid";
1671
+ }
1672
+ } catch {
1673
+ sawNetworkError = true;
1674
+ }
1675
+ if (refreshToken) {
1676
+ try {
1677
+ const fresh = await this.auth.refresh(refreshToken);
1678
+ if (fresh) {
1679
+ this.http.setToken(fresh);
1680
+ this.onTokensRotated?.(fresh, refreshToken);
1681
+ const { user } = await this.auth.validate(fresh, refreshToken);
1682
+ if (user) {
1683
+ this.user = user;
1684
+ return "valid";
1685
+ }
1686
+ }
1687
+ sawNetworkError = false;
1688
+ } catch {
1689
+ sawNetworkError = true;
1690
+ }
1691
+ }
1692
+ return sawNetworkError ? "network" : "invalid";
1693
+ }
1694
+ getAccessTokenAfterRotation() {
1695
+ return this.http.accessToken ?? "";
1696
+ }
1697
+ /**
1698
+ * 인증 실패(401/403)를 맞은 소비자가 부르는 **자가치유** 경로: refresh 토큰으로
1699
+ * 액세스 토큰을 회전시키고 새 토큰을 돌려준다. 실패(refresh 토큰 없음/거부)면
1700
+ * null — 그때는 진짜 재로그인 대상이다.
1701
+ *
1702
+ * single-flight: WS 브릿지·워크스페이스 동기화·HTTP 가 동시에 401 을 맞아도
1703
+ * refresh 는 한 번만 나간다 (게이트웨이는 refresh 마다 이전 세션을 지우므로,
1704
+ * 동시 refresh 는 서로의 새 토큰을 폐기하는 경쟁이 된다).
1705
+ *
1706
+ * ``fallbackRefreshToken`` — 인메모리에 refresh 토큰이 없을 때(재시작 직후 등)
1707
+ * 호스트가 keychain 값을 넘겨줄 수 있다.
1708
+ */
1709
+ async ensureFreshAuth(fallbackRefreshToken) {
1710
+ if (this.refreshing) return this.refreshing;
1711
+ const rt = this.refreshToken ?? fallbackRefreshToken;
1712
+ if (!rt) return null;
1713
+ this.refreshing = (async () => {
1714
+ try {
1715
+ const fresh = await this.auth.refresh(rt);
1716
+ if (!fresh) return null;
1717
+ this.http.setToken(fresh);
1718
+ this.refreshToken = rt;
1719
+ this.onTokensRotated?.(fresh, rt);
1720
+ return fresh;
1721
+ } catch {
1722
+ return null;
1723
+ } finally {
1724
+ this.refreshing = null;
1725
+ }
1726
+ })();
1727
+ return this.refreshing;
1728
+ }
1729
+ /** The current refresh token, so the host can persist it (e.g. keychain). */
1730
+ getRefreshToken() {
1731
+ return this.refreshToken;
1732
+ }
1733
+ async logout() {
1734
+ const token = this.getAccessTokenAfterRotation();
1735
+ if (token) await this.auth.logout(token);
1736
+ this.http.setToken(null);
1737
+ this.refreshToken = void 0;
1738
+ this.user = null;
1739
+ }
1740
+ };
1741
+
1742
+ // ../../packages/engine/src/mcp-bridge.ts
1743
+ import WebSocket from "ws";
1744
+
1745
+ // ../../packages/engine/src/secret-stores.ts
1746
+ var MCP_SECRET_PREFIX = "xgen_mcp_secret_";
1747
+ var MCP_OAUTH_PREFIX = "xgen_mcp_oauth_";
1748
+ function nonEmpty(o) {
1749
+ return !!o && Object.values(o).some((v) => typeof v === "string" && v.length > 0);
1750
+ }
1751
+ function parseObject(raw, fallback) {
1752
+ if (!raw) return fallback;
1753
+ try {
1754
+ const p = JSON.parse(raw);
1755
+ return p && typeof p === "object" ? p : fallback;
1756
+ } catch {
1757
+ return fallback;
1758
+ }
1759
+ }
1760
+ function createMcpSecretStore(secrets) {
1761
+ return {
1762
+ /** 비어 있으면 저장이 아니라 **삭제**다 — 빈 객체를 남기면 다음 로드가
1763
+ * "설정됨"으로 읽고 사용자는 왜 인증이 안 되는지 알 수 없다. */
1764
+ async save(server, value) {
1765
+ if (!nonEmpty(value.env) && !nonEmpty(value.headers)) {
1766
+ await secrets.set(MCP_SECRET_PREFIX + server, null);
1767
+ return true;
1768
+ }
1769
+ return secrets.set(MCP_SECRET_PREFIX + server, JSON.stringify(value));
1770
+ },
1771
+ async get(server) {
1772
+ return parseObject(
1773
+ await secrets.get(MCP_SECRET_PREFIX + server),
1774
+ null
1775
+ );
1776
+ },
1777
+ async clear(server) {
1778
+ await secrets.set(MCP_SECRET_PREFIX + server, null);
1779
+ }
1780
+ };
1781
+ }
1782
+ function createMcpOAuthStore(secrets) {
1783
+ const writeChain = /* @__PURE__ */ new Map();
1784
+ const store = {
1785
+ async load(server) {
1786
+ return parseObject(await secrets.get(MCP_OAUTH_PREFIX + server), {});
1787
+ },
1788
+ async save(server, state) {
1789
+ return secrets.set(MCP_OAUTH_PREFIX + server, JSON.stringify(state));
1790
+ },
1791
+ async patch(server, patch) {
1792
+ const prev = writeChain.get(server) ?? Promise.resolve();
1793
+ const next = prev.then(async () => {
1794
+ const cur = await store.load(server);
1795
+ return store.save(server, { ...cur, ...patch });
1796
+ });
1797
+ writeChain.set(
1798
+ server,
1799
+ next.catch(() => void 0)
1800
+ );
1801
+ return next;
1802
+ },
1803
+ async clear(server) {
1804
+ await secrets.set(MCP_OAUTH_PREFIX + server, null);
1805
+ writeChain.delete(server);
1806
+ }
1807
+ };
1808
+ return store;
1809
+ }
1810
+
1811
+ // ../../packages/engine/src/host.ts
1812
+ var bound = null;
1813
+ function bindHost(ports) {
1814
+ bound = {
1815
+ ports,
1816
+ mcpSecrets: createMcpSecretStore(ports.secrets),
1817
+ mcpOAuth: createMcpOAuthStore(ports.secrets)
1818
+ };
1819
+ }
1820
+ function isHostBound() {
1821
+ return bound !== null;
1822
+ }
1823
+ function need() {
1824
+ if (!bound) {
1825
+ throw new Error(
1826
+ "@dex/engine: \uD638\uC2A4\uD2B8\uAC00 \uBD99\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC571 \uC2DC\uC791 \uC2DC bindHost({ secrets, config, paths }) \uB97C \uD55C \uBC88 \uD638\uCD9C\uD558\uC138\uC694."
1827
+ );
1828
+ }
1829
+ return bound;
1830
+ }
1831
+ function hostPorts() {
1832
+ return need().ports;
1833
+ }
1834
+ function interaction() {
1835
+ return need().ports.interaction ?? {};
1836
+ }
1837
+ var mcpSecretStore = {
1838
+ save: (s, v) => need().mcpSecrets.save(s, v),
1839
+ get: (s) => need().mcpSecrets.get(s),
1840
+ clear: (s) => need().mcpSecrets.clear(s)
1841
+ };
1842
+ var mcpOAuthStore = {
1843
+ load: (s) => need().mcpOAuth.load(s),
1844
+ save: (s, v) => need().mcpOAuth.save(s, v),
1845
+ patch: (s, p) => need().mcpOAuth.patch(s, p),
1846
+ clear: (s) => need().mcpOAuth.clear(s)
1847
+ };
1848
+
1849
+ // ../../packages/engine/src/mcp-secrets.ts
1850
+ function resolveSecretKv(configVals, secretVals) {
1851
+ const keys = /* @__PURE__ */ new Set([...Object.keys(configVals || {}), ...Object.keys(secretVals || {})]);
1852
+ if (!keys.size) return void 0;
1853
+ const out = {};
1854
+ for (const k of keys) {
1855
+ const cfg = configVals?.[k];
1856
+ const v = cfg && cfg.length ? cfg : secretVals?.[k] || "";
1857
+ if (v) out[k] = v;
1858
+ }
1859
+ return Object.keys(out).length ? out : void 0;
1860
+ }
1861
+ function withResolvedSecrets(cfg, secrets) {
1862
+ const env = resolveSecretKv(cfg.env, secrets?.env ?? void 0);
1863
+ const headers = resolveSecretKv(cfg.headers, secrets?.headers ?? void 0);
1864
+ const out = { ...cfg };
1865
+ if (env) out.env = env;
1866
+ else delete out.env;
1867
+ if (headers) out.headers = headers;
1868
+ else delete out.headers;
1869
+ return out;
1870
+ }
1871
+
1872
+ // ../../packages/engine/src/mcp-oauth.ts
1873
+ var CLIENT_NAME = "XGen Dex";
1874
+ var AUTH_TIMEOUT_MS = 5 * 6e4;
1875
+ var ConnectorOAuthProvider = class {
1876
+ constructor(server, port, interactive, onRedirect, stateValue) {
1877
+ this.server = server;
1878
+ this.port = port;
1879
+ this.interactive = interactive;
1880
+ this.onRedirect = onRedirect;
1881
+ this.stateValue = stateValue;
1882
+ }
1883
+ get redirectUrl() {
1884
+ return `http://127.0.0.1:${this.port}/callback`;
1885
+ }
1886
+ /** OAuth2 state (CSRF) — only the interactive flow sets one; verified at the
1887
+ * loopback callback before the code is accepted. */
1888
+ state() {
1889
+ return this.stateValue;
1890
+ }
1891
+ get clientMetadata() {
1892
+ return {
1893
+ client_name: CLIENT_NAME,
1894
+ redirect_uris: [this.redirectUrl],
1895
+ grant_types: ["authorization_code", "refresh_token"],
1896
+ response_types: ["code"],
1897
+ token_endpoint_auth_method: "none"
1898
+ };
1899
+ }
1900
+ async clientInformation() {
1901
+ return (await mcpOAuthStore.load(this.server)).clientInformation;
1902
+ }
1903
+ async saveClientInformation(info) {
1904
+ await mcpOAuthStore.patch(this.server, { clientInformation: info });
1905
+ }
1906
+ async tokens() {
1907
+ return (await mcpOAuthStore.load(this.server)).tokens;
1908
+ }
1909
+ async saveTokens(tokens) {
1910
+ await mcpOAuthStore.patch(this.server, { tokens });
1911
+ }
1912
+ async saveCodeVerifier(v) {
1913
+ await mcpOAuthStore.patch(this.server, { codeVerifier: v });
1914
+ }
1915
+ async codeVerifier() {
1916
+ const s = await mcpOAuthStore.load(this.server);
1917
+ if (!s.codeVerifier) throw new Error("PKCE code_verifier \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC7AC\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4).");
1918
+ return s.codeVerifier;
1919
+ }
1920
+ async redirectToAuthorization(url) {
1921
+ if (!this.interactive) return;
1922
+ if (this.onRedirect) await this.onRedirect(url);
1923
+ }
1924
+ /** SDK self-heal: on InvalidGrant/registration errors the SDK asks us to drop
1925
+ * the stale credential so the next attempt re-registers / re-authorizes. */
1926
+ async invalidateCredentials(scope) {
1927
+ if (scope === "all") {
1928
+ await mcpOAuthStore.clear(this.server);
1929
+ return;
1930
+ }
1931
+ await mcpOAuthStore.patch(this.server, {
1932
+ ...scope === "client" ? { clientInformation: void 0 } : {},
1933
+ ...scope === "tokens" ? { tokens: void 0 } : {},
1934
+ ...scope === "verifier" ? { codeVerifier: void 0 } : {}
1935
+ });
1936
+ }
1937
+ };
1938
+ function makeSilentOAuthProvider(server) {
1939
+ return new ConnectorOAuthProvider(server, 0, false);
1940
+ }
1941
+ function oauthTransportOptions(cfg, base) {
1942
+ if (cfg.auth !== "oauth") return base;
1943
+ return { ...base, authProvider: makeSilentOAuthProvider(cfg.name) };
1944
+ }
1945
+
1946
+ // ../../packages/engine/src/mcp-manager.ts
1947
+ import { homedir as homedir3 } from "os";
1948
+
1949
+ // ../../packages/engine/src/exec-resolve.ts
1950
+ import { execFile } from "child_process";
1951
+ import { accessSync, constants, statSync } from "fs";
1952
+ import { homedir as homedir2 } from "os";
1953
+ import { basename, delimiter, isAbsolute, join as join2, sep } from "path";
1954
+ var IS_WIN = process.platform === "win32";
1955
+ var IS_MAC = process.platform === "darwin";
1956
+ var RUNTIMES = [
1957
+ {
1958
+ label: "uv",
1959
+ commands: ["uvx", "uv"],
1960
+ what: "Python MCP \uC11C\uBC84\uB97C \uC2E4\uD589\uD558\uB294 uv \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800\uC758 \uC2E4\uD589\uAE30\uC785\uB2C8\uB2E4.",
1961
+ url: "https://docs.astral.sh/uv/getting-started/installation/",
1962
+ install: () => IS_WIN ? ['powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', "winget install astral-sh.uv"] : IS_MAC ? ["curl -LsSf https://astral.sh/uv/install.sh | sh", "brew install uv"] : ["curl -LsSf https://astral.sh/uv/install.sh | sh"]
1963
+ },
1964
+ {
1965
+ label: "Node.js",
1966
+ commands: ["npx", "npm", "node"],
1967
+ what: "JavaScript MCP \uC11C\uBC84\uB97C \uC2E4\uD589\uD558\uB294 Node.js \uB7F0\uD0C0\uC784\uC785\uB2C8\uB2E4.",
1968
+ url: "https://nodejs.org/",
1969
+ install: () => IS_WIN ? ["winget install OpenJS.NodeJS.LTS", "https://nodejs.org \uC5D0\uC11C LTS \uC124\uCE58 \uD504\uB85C\uADF8\uB7A8 \uB0B4\uB824\uBC1B\uAE30"] : IS_MAC ? ["brew install node", "https://nodejs.org \uC5D0\uC11C LTS \uC124\uCE58 \uD504\uB85C\uADF8\uB7A8 \uB0B4\uB824\uBC1B\uAE30"] : ["sudo apt install nodejs npm (Debian/Ubuntu)", "https://nodejs.org \uC5D0\uC11C LTS \uB0B4\uB824\uBC1B\uAE30"]
1970
+ },
1971
+ {
1972
+ label: "Python",
1973
+ commands: ["python3", "python", "pip", "pip3", "pipx"],
1974
+ what: "Python \uB7F0\uD0C0\uC784/\uD328\uD0A4\uC9C0 \uB3C4\uAD6C\uC785\uB2C8\uB2E4.",
1975
+ url: "https://www.python.org/downloads/",
1976
+ install: () => IS_WIN ? ["winget install Python.Python.3.12"] : IS_MAC ? ["brew install python"] : ["sudo apt install python3 python3-pip (Debian/Ubuntu)"]
1977
+ },
1978
+ {
1979
+ label: "Docker",
1980
+ commands: ["docker"],
1981
+ what: "\uCEE8\uD14C\uC774\uB108\uB85C \uBC30\uD3EC\uB41C MCP \uC11C\uBC84\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.",
1982
+ url: "https://docs.docker.com/get-started/get-docker/",
1983
+ install: () => ["https://docs.docker.com/get-started/get-docker/ \uC5D0\uC11C Docker Desktop \uC124\uCE58"]
1984
+ },
1985
+ {
1986
+ label: "Bun",
1987
+ commands: ["bun", "bunx"],
1988
+ what: "Bun \uB7F0\uD0C0\uC784\uC785\uB2C8\uB2E4.",
1989
+ url: "https://bun.sh/",
1990
+ install: () => IS_WIN ? ['powershell -c "irm bun.sh/install.ps1 | iex"'] : ["curl -fsSL https://bun.sh/install | bash"]
1991
+ },
1992
+ {
1993
+ label: "Deno",
1994
+ commands: ["deno"],
1995
+ what: "Deno \uB7F0\uD0C0\uC784\uC785\uB2C8\uB2E4.",
1996
+ url: "https://deno.land/",
1997
+ install: () => IS_WIN ? ["irm https://deno.land/install.ps1 | iex"] : ["curl -fsSL https://deno.land/install.sh | sh"]
1998
+ }
1999
+ ];
2000
+ function runtimeFor(command) {
2001
+ const base = basename(command.trim()).toLowerCase().replace(/\.(exe|cmd|bat|com)$/i, "");
2002
+ return RUNTIMES.find((r) => r.commands.includes(base)) ?? null;
2003
+ }
2004
+ function commonBinDirs(home = homedir2()) {
2005
+ if (IS_WIN) {
2006
+ const appData = process.env.APPDATA || join2(home, "AppData", "Roaming");
2007
+ const localAppData = process.env.LOCALAPPDATA || join2(home, "AppData", "Local");
2008
+ return [
2009
+ join2(localAppData, "Microsoft", "WindowsApps"),
2010
+ join2(appData, "npm"),
2011
+ join2(localAppData, "Programs", "nodejs"),
2012
+ join2(home, ".local", "bin"),
2013
+ join2(home, ".cargo", "bin"),
2014
+ join2(home, ".bun", "bin"),
2015
+ join2(localAppData, "Programs", "Python", "Scripts"),
2016
+ "C:\\Program Files\\nodejs"
2017
+ ];
2018
+ }
2019
+ return [
2020
+ "/opt/homebrew/bin",
2021
+ // Apple Silicon homebrew
2022
+ "/usr/local/bin",
2023
+ "/opt/local/bin",
2024
+ // MacPorts
2025
+ join2(home, ".local", "bin"),
2026
+ // uv / pipx
2027
+ join2(home, ".cargo", "bin"),
2028
+ join2(home, ".bun", "bin"),
2029
+ join2(home, ".deno", "bin"),
2030
+ join2(home, "bin"),
2031
+ "/usr/bin",
2032
+ "/bin",
2033
+ "/snap/bin"
2034
+ ];
2035
+ }
2036
+ var loginPathCache = null;
2037
+ var PATH_CACHE_TTL_MS = 2e4;
2038
+ function resetPathCache() {
2039
+ loginPathCache = null;
2040
+ }
2041
+ function parseEnvPath(stdout) {
2042
+ let found = null;
2043
+ for (const line of stdout.split(/\r?\n/)) {
2044
+ const m = /^PATH=(.*)$/.exec(line);
2045
+ if (m) found = m[1];
2046
+ }
2047
+ if (!found) return null;
2048
+ const value = found.trim();
2049
+ if (!value) return null;
2050
+ const usable = value.split(delimiter).some((d) => {
2051
+ if (!d) return false;
2052
+ try {
2053
+ return statSync(d).isDirectory();
2054
+ } catch {
2055
+ return false;
2056
+ }
2057
+ });
2058
+ return usable ? value : null;
2059
+ }
2060
+ async function loginShellPath() {
2061
+ if (IS_WIN) return null;
2062
+ if (loginPathCache && Date.now() - loginPathCache.at < PATH_CACHE_TTL_MS) {
2063
+ return loginPathCache.value;
2064
+ }
2065
+ const shell = process.env.SHELL || "/bin/bash";
2066
+ const value = await new Promise((resolve) => {
2067
+ execFile(shell, ["-ilc", "env"], { timeout: 4e3, windowsHide: true, maxBuffer: 1 << 20 }, (_err, stdout) => {
2068
+ resolve(stdout ? parseEnvPath(stdout) : null);
2069
+ });
2070
+ });
2071
+ loginPathCache = { value, at: Date.now() };
2072
+ return value;
2073
+ }
2074
+ function mergePaths(...sources) {
2075
+ const seen = /* @__PURE__ */ new Set();
2076
+ const out = [];
2077
+ for (const src of sources) {
2078
+ if (!src) continue;
2079
+ for (const p of src.split(delimiter)) {
2080
+ const dir = p.trim().replace(new RegExp(`${sep === "\\" ? "\\\\" : sep}+$`), "");
2081
+ if (!dir || seen.has(dir)) continue;
2082
+ seen.add(dir);
2083
+ out.push(dir);
2084
+ }
2085
+ }
2086
+ return out.join(delimiter);
2087
+ }
2088
+ async function augmentedPath() {
2089
+ const login = await loginShellPath();
2090
+ return mergePaths(login, process.env.PATH, commonBinDirs().join(delimiter));
2091
+ }
2092
+ function buildChildEnv(pathStr, cfgEnv, base = process.env) {
2093
+ const isPathKey = (k) => k.toLowerCase() === "path";
2094
+ const out = {};
2095
+ for (const [k, v] of Object.entries(base)) {
2096
+ if (v === void 0) continue;
2097
+ if (IS_WIN && isPathKey(k)) continue;
2098
+ out[k] = v;
2099
+ }
2100
+ const userPathKey = Object.keys(cfgEnv || {}).find(isPathKey);
2101
+ const effectivePath = userPathKey ? cfgEnv[userPathKey] : pathStr;
2102
+ for (const [k, v] of Object.entries(cfgEnv || {})) {
2103
+ if (isPathKey(k)) continue;
2104
+ out[k] = v;
2105
+ }
2106
+ if (IS_WIN) {
2107
+ out.Path = effectivePath;
2108
+ } else {
2109
+ out.PATH = effectivePath;
2110
+ }
2111
+ return out;
2112
+ }
2113
+ function isExecutableFile(p) {
2114
+ try {
2115
+ if (!statSync(p).isFile()) return false;
2116
+ if (IS_WIN) return true;
2117
+ accessSync(p, constants.X_OK);
2118
+ return true;
2119
+ } catch {
2120
+ return false;
2121
+ }
2122
+ }
2123
+ function winExtensions() {
2124
+ const raw = process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD";
2125
+ return raw.split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
2126
+ }
2127
+ function resolveExecutable(command, pathStr) {
2128
+ const cmd = command.trim();
2129
+ if (!cmd) return null;
2130
+ const hasSep = cmd.includes("/") || IS_WIN && cmd.includes("\\");
2131
+ const candidates = (base) => IS_WIN ? [base, ...winExtensions().map((e) => base + e)] : [base];
2132
+ if (hasSep || isAbsolute(cmd)) {
2133
+ for (const c of candidates(cmd)) if (isExecutableFile(c)) return c;
2134
+ return null;
2135
+ }
2136
+ for (const dir of pathStr.split(delimiter)) {
2137
+ if (!dir) continue;
2138
+ for (const c of candidates(join2(dir, cmd))) {
2139
+ if (isExecutableFile(c)) return c;
2140
+ }
2141
+ }
2142
+ return null;
2143
+ }
2144
+ function diagnoseMissing(command, pathStr) {
2145
+ const rt = runtimeFor(command);
2146
+ const hints = [];
2147
+ const dirCount = pathStr.split(delimiter).filter(Boolean).length;
2148
+ const base = basename(command);
2149
+ if (rt) {
2150
+ const sibling = rt.commands.find((c) => c !== base && resolveExecutable(c, pathStr));
2151
+ if (sibling) {
2152
+ hints.push(
2153
+ `\uCC38\uACE0: \uAC19\uC740 \uB7F0\uD0C0\uC784\uC758 "${sibling}" \uB294 \uCC3E\uC558\uC2B5\uB2C8\uB2E4 \u2014 ${rt.label} \uBC84\uC804\uC774 \uB0AE\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uCD5C\uC2E0\uC73C\uB85C \uC62C\uB9AC\uBA74 "${base}" \uB3C4 \uD568\uAED8 \uC124\uCE58\uB429\uB2C8\uB2E4.`
2154
+ );
2155
+ }
2156
+ hints.push(...rt.install().map((cmd) => `\uC124\uCE58: ${cmd}`));
2157
+ hints.push(`\uC548\uB0B4: ${rt.url}`);
2158
+ }
2159
+ const example = IS_WIN ? join2(homedir2(), ".local", "bin", `${base}.exe`) : join2(homedir2(), ".local", "bin", base);
2160
+ hints.push(
2161
+ IS_WIN ? `\uB610\uB294 \uC2E4\uD589 \uBA85\uB839\uC5D0 \uC808\uB300 \uACBD\uB85C\uB97C \uC801\uC73C\uC138\uC694 (\uC608: ${example}). PowerShell \uC5D0\uC11C \`Get-Command ${base}\` \uB85C \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.` : `\uB610\uB294 \uC2E4\uD589 \uBA85\uB839\uC5D0 \uC808\uB300 \uACBD\uB85C\uB97C \uC801\uC73C\uC138\uC694 (\uC608: ${example}). \uD130\uBBF8\uB110\uC5D0\uC11C \`which ${base}\` \uB85C \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
2162
+ );
2163
+ hints.push("\uC124\uCE58\uD55C \uB4A4 [\uD14C\uC2A4\uD2B8]\uB97C \uB2E4\uC2DC \uB204\uB974\uBA74 \uC7AC\uAC80\uC0C9\uD569\uB2C8\uB2E4 (\uC571 \uC7AC\uC2DC\uC791 \uBD88\uD544\uC694).");
2164
+ const summary = rt ? `"${command}" \u2014 \uC2E4\uD589 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. ${rt.label} \uC124\uCE58\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. ${rt.what}` : `"${command}" \u2014 \uC2E4\uD589 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (PATH ${dirCount}\uAC1C \uACBD\uB85C \uAC80\uC0C9).`;
2165
+ return { command, summary, hints };
2166
+ }
2167
+ var ExecNotFoundError = class extends Error {
2168
+ hints;
2169
+ command;
2170
+ constructor(d) {
2171
+ super(d.summary);
2172
+ this.name = "ExecNotFoundError";
2173
+ this.command = d.command;
2174
+ this.hints = d.hints;
2175
+ }
2176
+ };
2177
+
2178
+ // ../../packages/engine/src/mcp-manager.ts
2179
+ function tokenize(cmd) {
2180
+ const m = cmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
2181
+ return m.map((t) => t.replace(/^["']|["']$/g, ""));
2182
+ }
2183
+ function tailLines(text, maxLines = 12, maxChars = 200) {
2184
+ return text.split(/\r?\n/).map((l) => l.replace(/\s+$/, "")).filter(Boolean).slice(-maxLines).map((l) => l.length > maxChars ? `${l.slice(0, maxChars)}\u2026` : l);
2185
+ }
2186
+ var McpStartError = class extends Error {
2187
+ hints;
2188
+ constructor(message, hints) {
2189
+ super(message);
2190
+ this.name = "McpStartError";
2191
+ this.hints = hints;
2192
+ }
2193
+ };
2194
+ function collectStderr(transport, onData, cap = 64 * 1024) {
2195
+ let buf = "";
2196
+ let last = Date.now();
2197
+ const stream = transport.stderr;
2198
+ stream?.on?.("data", (chunk) => {
2199
+ buf += String(chunk);
2200
+ if (buf.length > cap) buf = buf.slice(-cap);
2201
+ last = Date.now();
2202
+ onData?.(tailLines(buf));
2203
+ });
2204
+ return { text: () => buf, lastAt: () => last };
2205
+ }
2206
+ function throttle(fn, everyMs) {
2207
+ let at = 0;
2208
+ let pending = null;
2209
+ let latest;
2210
+ return (v) => {
2211
+ latest = v;
2212
+ const now = Date.now();
2213
+ if (now - at >= everyMs) {
2214
+ at = now;
2215
+ fn(latest);
2216
+ return;
2217
+ }
2218
+ if (pending) return;
2219
+ pending = setTimeout(() => {
2220
+ pending = null;
2221
+ at = Date.now();
2222
+ fn(latest);
2223
+ }, everyMs - (now - at));
2224
+ };
2225
+ }
2226
+ var _sdk = null;
2227
+ async function loadSdk() {
2228
+ if (_sdk) return _sdk;
2229
+ const [{ Client }, { StdioClientTransport }, { StreamableHTTPClientTransport }, sse, types] = await Promise.all([
2230
+ import("@modelcontextprotocol/sdk/client/index.js"),
2231
+ import("@modelcontextprotocol/sdk/client/stdio.js"),
2232
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
2233
+ // 레거시 HTTP+SSE 전송 — 없는 빌드일 수 있어 안전하게 감싼다.
2234
+ import("@modelcontextprotocol/sdk/client/sse.js").catch(() => null),
2235
+ // tools/list_changed 알림 스키마 — 구버전 SDK 에 없을 수 있어 안전하게 감싼다.
2236
+ import("@modelcontextprotocol/sdk/types.js").catch(() => null)
2237
+ ]);
2238
+ _sdk = {
2239
+ Client,
2240
+ StdioClientTransport,
2241
+ StreamableHTTPClientTransport,
2242
+ SSEClientTransport: sse?.SSEClientTransport ?? null,
2243
+ ToolListChangedNotificationSchema: types?.ToolListChangedNotificationSchema ?? null
2244
+ };
2245
+ return _sdk;
2246
+ }
2247
+ async function withTimeout(p, ms, label) {
2248
+ let t;
2249
+ const timeout = new Promise((_, rej) => {
2250
+ t = setTimeout(() => rej(new Error(`${label} timed out after ${ms}ms`)), ms);
2251
+ });
2252
+ try {
2253
+ return await Promise.race([p, timeout]);
2254
+ } finally {
2255
+ clearTimeout(t);
2256
+ }
2257
+ }
2258
+ var IDLE_TIMEOUT_MS = 9e4;
2259
+ var MAX_START_MS = 15 * 6e4;
2260
+ async function waitWhileProgressing(p, lastAt, label, idleMs = IDLE_TIMEOUT_MS, maxMs = MAX_START_MS) {
2261
+ const started = Date.now();
2262
+ let timer;
2263
+ const guard = new Promise((_, rej) => {
2264
+ const tick = () => {
2265
+ const idle = Date.now() - lastAt();
2266
+ const total = Date.now() - started;
2267
+ if (total >= maxMs) {
2268
+ return rej(new Error(`${label}: ${Math.round(maxMs / 6e4)}\uBD84\uC744 \uB118\uACA8 \uC911\uB2E8\uD588\uC2B5\uB2C8\uB2E4`));
2269
+ }
2270
+ if (idle >= idleMs) {
2271
+ return rej(
2272
+ new Error(`${label}: ${Math.round(idleMs / 1e3)}\uCD08 \uB3D9\uC548 \uC544\uBB34 \uC751\uB2F5\uC774 \uC5C6\uC5B4 \uC911\uB2E8\uD588\uC2B5\uB2C8\uB2E4`)
2273
+ );
2274
+ }
2275
+ timer = setTimeout(tick, Math.max(250, Math.min(idleMs - idle, maxMs - total)));
2276
+ };
2277
+ timer = setTimeout(tick, Math.min(idleMs, maxMs));
2278
+ });
2279
+ try {
2280
+ return await Promise.race([p, guard]);
2281
+ } finally {
2282
+ clearTimeout(timer);
2283
+ }
2284
+ }
2285
+ var MCPManager = class {
2286
+ states = /* @__PURE__ */ new Map();
2287
+ httpFetch;
2288
+ allowPrivateCertificate = false;
2289
+ /** 서버가 도구 목록을 바꾸거나(list_changed) 죽었을 때(onclose) 카탈로그를 다시
2290
+ * 광고하도록 호출된다 (index 가 bridge.refreshCatalog 로 배선). */
2291
+ onCatalogChange;
2292
+ /** 카탈로그 변경(도구 추가/제거/서버 종료) 시 재광고할 리스너를 등록한다. */
2293
+ setCatalogChangeListener(fn) {
2294
+ this.onCatalogChange = fn;
2295
+ }
2296
+ /** Reconcile the configured server list into live state (drops removed,
2297
+ * reconnects changed configs lazily). Does NOT connect yet. */
2298
+ configure(servers, options = {}) {
2299
+ const certificatePolicyChanged = this.allowPrivateCertificate !== (options.allowPrivateCertificate === true);
2300
+ this.httpFetch = options.httpFetch;
2301
+ this.allowPrivateCertificate = options.allowPrivateCertificate === true;
2302
+ const next = /* @__PURE__ */ new Map();
2303
+ for (const s of servers || []) if (s && s.name) next.set(s.name, s);
2304
+ for (const [name, st] of [...this.states]) {
2305
+ const cfg = next.get(name);
2306
+ if (!cfg || JSON.stringify(cfg) !== JSON.stringify(st.config) || certificatePolicyChanged && (st.config.transport === "http" || st.config.transport === "sse")) {
2307
+ void this.disconnect(name);
2308
+ this.states.delete(name);
2309
+ }
2310
+ }
2311
+ for (const [name, cfg] of next) {
2312
+ if (!this.states.has(name)) this.states.set(name, { config: cfg, client: null, tools: [] });
2313
+ }
2314
+ }
2315
+ async connect(name) {
2316
+ const st = this.states.get(name);
2317
+ if (!st) throw new Error(`unknown MCP server: ${name}`);
2318
+ if (st.client) return;
2319
+ const label = name.replace(/^__test__/, "");
2320
+ if (st.connecting) return st.connecting;
2321
+ st.connecting = (async () => {
2322
+ const {
2323
+ Client,
2324
+ StdioClientTransport,
2325
+ StreamableHTTPClientTransport,
2326
+ SSEClientTransport,
2327
+ ToolListChangedNotificationSchema
2328
+ } = await loadSdk();
2329
+ const secrets = await mcpSecretStore.get(name).catch(() => null);
2330
+ const cfg = withResolvedSecrets(st.config, secrets);
2331
+ let transport;
2332
+ let tap = null;
2333
+ if (cfg.transport === "stdio") {
2334
+ if (!cfg.command) throw new Error("stdio server has no command");
2335
+ const [command, ...args] = cfg.args?.length ? [cfg.command.trim(), ...cfg.args] : tokenize(cfg.command);
2336
+ if (!command) throw new Error("empty command");
2337
+ let pathStr = await augmentedPath();
2338
+ let resolved = resolveExecutable(command, pathStr);
2339
+ if (!resolved) {
2340
+ resetPathCache();
2341
+ pathStr = await augmentedPath();
2342
+ resolved = resolveExecutable(command, pathStr);
2343
+ }
2344
+ if (!resolved) throw new ExecNotFoundError(diagnoseMissing(command, pathStr));
2345
+ transport = new StdioClientTransport({
2346
+ command: resolved,
2347
+ args,
2348
+ env: buildChildEnv(pathStr, cfg.env),
2349
+ // 작업 디렉터리를 홈으로 고정한다. 안 정하면 앱을 어떻게 띄웠는지에
2350
+ // 따라(터미널 vs Finder/시작 메뉴) `/` 나 `C:\Windows\System32` 가
2351
+ // 되어 상대 경로 인자와 캐시 위치가 플랫폼마다 달라진다.
2352
+ cwd: homedir3(),
2353
+ // 기동 실패 원인을 읽으려면 파이프여야 한다 (기본 'inherit' 는
2354
+ // Electron 콘솔로 흘려보내 사용자에게 안 보인다).
2355
+ stderr: "pipe"
2356
+ });
2357
+ const notify = st.onProgress ? throttle(st.onProgress, 300) : void 0;
2358
+ tap = collectStderr(transport, notify);
2359
+ } else if (cfg.transport === "sse") {
2360
+ if (!cfg.url) throw new Error("sse server has no url");
2361
+ if (!SSEClientTransport) throw new Error("\uC774 \uBE4C\uB4DC\uC5D0\uC11C SSE \uC804\uC1A1\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2362
+ transport = new SSEClientTransport(
2363
+ new URL(cfg.url),
2364
+ oauthTransportOptions(cfg, {
2365
+ requestInit: cfg.headers ? { headers: cfg.headers } : void 0,
2366
+ fetch: this.httpFetch
2367
+ })
2368
+ );
2369
+ } else {
2370
+ if (!cfg.url) throw new Error("http server has no url");
2371
+ transport = new StreamableHTTPClientTransport(
2372
+ new URL(cfg.url),
2373
+ oauthTransportOptions(cfg, {
2374
+ requestInit: cfg.headers ? { headers: cfg.headers } : void 0,
2375
+ fetch: this.httpFetch
2376
+ })
2377
+ );
2378
+ }
2379
+ const client = new Client({ name: "xgen-dex", version: "1.0.0" }, { capabilities: {} });
2380
+ let listed;
2381
+ try {
2382
+ if (tap) {
2383
+ await waitWhileProgressing(client.connect(transport), tap.lastAt, `${label} \uC5F0\uACB0`);
2384
+ } else {
2385
+ await withTimeout(client.connect(transport), 2e4, `${label} \uC5F0\uACB0`);
2386
+ }
2387
+ listed = await withTimeout(client.listTools(), 3e4, `${label} \uB3C4\uAD6C \uBAA9\uB85D`);
2388
+ } catch (e) {
2389
+ const tail = tap ? tailLines(tap.text()) : [];
2390
+ if (tail.length) {
2391
+ throw new McpStartError(
2392
+ `${e.message} \u2014 \uC11C\uBC84\uAC00 \uAE30\uB3D9\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC544\uB798 \uCD9C\uB825\uC744 \uD655\uC778\uD558\uC138\uC694.`,
2393
+ tail
2394
+ );
2395
+ }
2396
+ throw e;
2397
+ }
2398
+ st.client = client;
2399
+ st.tools = (listed?.tools || []).map((t) => ({
2400
+ name: t.name,
2401
+ description: t.description,
2402
+ inputSchema: t.inputSchema
2403
+ }));
2404
+ st.error = void 0;
2405
+ if (ToolListChangedNotificationSchema) {
2406
+ try {
2407
+ client.setNotificationHandler(
2408
+ ToolListChangedNotificationSchema,
2409
+ async () => {
2410
+ try {
2411
+ const relisted = await withTimeout(client.listTools(), 3e4, `${label} \uB3C4\uAD6C \uC7AC\uC870\uD68C`);
2412
+ if (st.client !== client) return;
2413
+ st.tools = (relisted?.tools || []).map((t) => ({
2414
+ name: t.name,
2415
+ description: t.description,
2416
+ inputSchema: t.inputSchema
2417
+ }));
2418
+ this.onCatalogChange?.();
2419
+ } catch {
2420
+ }
2421
+ }
2422
+ );
2423
+ } catch {
2424
+ }
2425
+ }
2426
+ client.onclose = () => {
2427
+ if (st.client !== client) return;
2428
+ st.client = null;
2429
+ st.tools = [];
2430
+ this.onCatalogChange?.();
2431
+ };
2432
+ })();
2433
+ try {
2434
+ await st.connecting;
2435
+ } catch (e) {
2436
+ st.error = String(e.message);
2437
+ st.client = null;
2438
+ throw e;
2439
+ } finally {
2440
+ st.connecting = void 0;
2441
+ }
2442
+ }
2443
+ async disconnect(name) {
2444
+ const st = this.states.get(name);
2445
+ if (!st) return;
2446
+ const c = st.client;
2447
+ st.client = null;
2448
+ st.tools = [];
2449
+ try {
2450
+ await c?.close?.();
2451
+ } catch {
2452
+ }
2453
+ }
2454
+ /**
2455
+ * Connect every enabled server + return their tool catalogs.
2456
+ *
2457
+ * **병렬로** 붙는다. 순차로 붙이면 첫 실행이라 의존성을 내려받는 서버 하나가
2458
+ * 나머지 전부를 막는다 (기동 대기가 진행 상황 기반이라 몇 분까지 갈 수 있다).
2459
+ * 결과 순서는 설정 순서를 유지한다.
2460
+ */
2461
+ async advertise() {
2462
+ const targets = [...this.states].filter(([, st]) => st.config.enabled !== false);
2463
+ return Promise.all(
2464
+ targets.map(async ([name, st]) => {
2465
+ try {
2466
+ await this.connect(name);
2467
+ return { name, connected: true, tools: st.tools };
2468
+ } catch (e) {
2469
+ return { name, connected: false, error: String(e.message), tools: [] };
2470
+ }
2471
+ })
2472
+ );
2473
+ }
2474
+ /** Flat catalog for the bridge `hello` frame (only connected servers' tools). */
2475
+ async advertisedTools() {
2476
+ const adverts = await this.advertise();
2477
+ const flat = [];
2478
+ for (const a of adverts) {
2479
+ if (!a.connected) continue;
2480
+ for (const t of a.tools) {
2481
+ flat.push({ server: a.name, name: t.name, description: t.description, inputSchema: t.inputSchema });
2482
+ }
2483
+ }
2484
+ return flat;
2485
+ }
2486
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2487
+ async callTool(name, tool, args) {
2488
+ if (!this.states.has(name)) {
2489
+ throw new Error(
2490
+ `\uB85C\uCEEC MCP \uC11C\uBC84 '${name}' \uAC00 \uC9C0\uAE08\uC740 \uCEE4\uB125\uD130\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC124\uC815\uC5D0\uC11C \uC81C\uAC70\uB410\uAC70\uB098 \uB85C\uCEEC MCP \uC2A4\uC704\uCE58\uAC00 \uAEBC\uC84C\uC2B5\uB2C8\uB2E4). \uB3C4\uAD6C \uC774\uB984 \uBB38\uC81C\uAC00 \uC544\uB2C8\uBBC0\uB85C \uC7AC\uC2DC\uB3C4\uD574\uB3C4 \uAC19\uC2B5\uB2C8\uB2E4 \u2014 \uC774 \uC11C\uBC84 \uC5C6\uC774 \uC9C4\uD589\uD558\uAC70\uB098 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD558\uC138\uC694.`
2491
+ );
2492
+ }
2493
+ await this.connect(name);
2494
+ const st = this.states.get(name);
2495
+ if (!st?.client) throw new Error(`MCP server ${name} not connected`);
2496
+ try {
2497
+ return await withTimeout(
2498
+ st.client.callTool({ name: tool, arguments: args || {} }),
2499
+ 12e4,
2500
+ `callTool ${name}.${tool}`
2501
+ );
2502
+ } catch (e) {
2503
+ await this.disconnect(name);
2504
+ throw e;
2505
+ }
2506
+ }
2507
+ /** One-shot connect → list → disconnect, for the settings "테스트" button. */
2508
+ async test(config, onProgress) {
2509
+ const tmp = `__test__${config.name || "srv"}`;
2510
+ this.states.set(tmp, { config: { ...config, name: tmp }, client: null, tools: [], onProgress });
2511
+ try {
2512
+ await this.connect(tmp);
2513
+ const tools = this.states.get(tmp)?.tools || [];
2514
+ return { ok: true, tools };
2515
+ } catch (e) {
2516
+ const err = e;
2517
+ const hints = Array.isArray(err.hints) && err.hints.length ? err.hints : void 0;
2518
+ return { ok: false, error: String(err.message), hints };
2519
+ } finally {
2520
+ await this.disconnect(tmp);
2521
+ this.states.delete(tmp);
2522
+ }
2523
+ }
2524
+ listServers() {
2525
+ return [...this.states.values()].map((s) => s.config);
2526
+ }
2527
+ async closeAll() {
2528
+ for (const name of [...this.states.keys()]) await this.disconnect(name);
2529
+ }
2530
+ };
2531
+ var _manager = null;
2532
+ function getMcpManager() {
2533
+ if (!_manager) _manager = new MCPManager();
2534
+ return _manager;
2535
+ }
2536
+
2537
+ // ../../packages/engine/src/local-tools.ts
2538
+ import { spawn } from "node:child_process";
2539
+ import { homedir as homedir4, platform as platform2 } from "node:os";
2540
+ import {
2541
+ readFile as fsReadFile,
2542
+ writeFile as fsWriteFile,
2543
+ appendFile as fsAppendFile,
2544
+ readdir,
2545
+ stat,
2546
+ mkdir as mkdir2
2547
+ } from "node:fs/promises";
2548
+ import {
2549
+ resolve as pathResolve,
2550
+ relative as pathRelative,
2551
+ isAbsolute as isAbsolute2,
2552
+ join as pathJoin,
2553
+ dirname as dirname2,
2554
+ extname
2555
+ } from "node:path";
2556
+ var LOCAL_SERVER = "local";
2557
+ var SHELL_TOOL = "Shell";
2558
+ var OPEN_TOOL = "Open";
2559
+ var READ_FILE_TOOL = "ReadFile";
2560
+ var WRITE_FILE_TOOL = "WriteFile";
2561
+ var LIST_DIR_TOOL = "ListDir";
2562
+ var SEARCH_TOOL = "Search";
2563
+ var CLIPBOARD_TOOL = "Clipboard";
2564
+ var NOTIFY_TOOL = "Notify";
2565
+ var SHELL_JOB_TOOL = "ShellJob";
2566
+ function localToolCallContext(raw) {
2567
+ const value = raw && typeof raw === "object" ? raw : {};
2568
+ const text = (input) => {
2569
+ const normalized = String(input ?? "").trim();
2570
+ return normalized || void 0;
2571
+ };
2572
+ return {
2573
+ workflowId: text(value.workflow_id ?? value.workflowId),
2574
+ workflowName: text(value.workflow_name ?? value.workflowName),
2575
+ interactionId: text(value.interaction_id ?? value.interactionId)
2576
+ };
2577
+ }
2578
+ var DEFAULT_TIMEOUT_MS = 6e5;
2579
+ var MIN_TIMEOUT_MS = 1e3;
2580
+ var MAX_TIMEOUT_MS = 60 * 6e4;
2581
+ var OUTPUT_CAP = 2e5;
2582
+ var BG_SETTLE_MS = 350;
2583
+ var JOB_STREAM_CAP = 262144;
2584
+ var MAX_JOBS = 50;
2585
+ var MAX_RUNNING_JOBS = 25;
2586
+ var IS_WIN2 = platform2() === "win32";
2587
+ var IS_MAC2 = platform2() === "darwin";
2588
+ function shellConfig(cfg) {
2589
+ const c = cfg || {};
2590
+ const t = typeof c.timeoutMs === "number" && c.timeoutMs > 0 ? c.timeoutMs : DEFAULT_TIMEOUT_MS;
2591
+ const cwd = (c.cwd || "").trim();
2592
+ const listed = Array.isArray(c.allowedRoots) ? c.allowedRoots.map((r) => String(r).trim()).filter(Boolean) : [];
2593
+ const allowedRoots = cwd ? [...listed.length ? listed : ["~"], cwd] : listed;
2594
+ return {
2595
+ enabled: c.enabled === true,
2596
+ // opt-in (default OFF) — 로컬 셸은 명시적으로 켜야 한다
2597
+ cwd,
2598
+ timeoutMs: Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.round(t))),
2599
+ blocked: Array.isArray(c.blocked) ? c.blocked.map((b) => String(b).trim()).filter(Boolean) : [],
2600
+ allowedRoots
2601
+ };
2602
+ }
2603
+ function nativeShellLabel() {
2604
+ if (IS_WIN2) return "PowerShell";
2605
+ if (IS_MAC2) return "zsh/bash";
2606
+ return "bash/sh";
2607
+ }
2608
+ function shellInvocation(command, userShellBin, explicitShell) {
2609
+ const want = (explicitShell || "default").toLowerCase();
2610
+ if (want === "powershell" || want === "default" && IS_WIN2) {
2611
+ return {
2612
+ file: "powershell.exe",
2613
+ args: ["-NoProfile", "-NonInteractive", "-Command", command]
2614
+ };
2615
+ }
2616
+ if (want === "cmd") {
2617
+ return { file: "cmd.exe", args: ["/d", "/s", "/c", command] };
2618
+ }
2619
+ if (want === "bash") return { file: "bash", args: ["-lc", command] };
2620
+ if (want === "sh") return { file: "sh", args: ["-lc", command] };
2621
+ const bin = (userShellBin || "").trim();
2622
+ const file = bin.startsWith("/") ? bin : "bash";
2623
+ return { file, args: ["-lc", command] };
2624
+ }
2625
+ function openerInvocation(target) {
2626
+ const t = String(target || "").trim();
2627
+ if (IS_WIN2) return { file: "cmd.exe", args: ["/d", "/s", "/c", "start", "", t] };
2628
+ if (IS_MAC2) return { file: "open", args: [t] };
2629
+ return { file: "xdg-open", args: [t] };
2630
+ }
2631
+ function firstToken(command) {
2632
+ const m = String(command || "").trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))/);
2633
+ const raw = m && (m[1] || m[2] || m[3]) || "";
2634
+ const base = raw.split(/[\\/]/).pop() || raw;
2635
+ return base.replace(/\.(exe|cmd|bat|com|ps1)$/i, "").toLowerCase();
2636
+ }
2637
+ function isBlocked(command, blocked) {
2638
+ if (!blocked.length) return false;
2639
+ const tok = firstToken(command);
2640
+ return blocked.some((b) => firstToken(b) === tok || b.trim().toLowerCase() === tok);
2641
+ }
2642
+ var DANGEROUS_PATTERNS = [
2643
+ /\brm\s+-[a-z]*[rf]/i,
2644
+ // rm -rf / -r / -f
2645
+ /(^|[;&|`(])\s*rm\s+\//i,
2646
+ // rm on an absolute path
2647
+ /\bRemove-Item\b[^\n]*-Recurse/i,
2648
+ /\brmdir\s+\/s/i,
2649
+ /\bdel\s+\/[a-z]*[sf]/i,
2650
+ /\b(mkfs|fdisk|format)\b/i,
2651
+ /\bdd\b[^\n]*\b(of|if)=/i,
2652
+ /\b(shutdown|reboot|halt|poweroff)\b/i,
2653
+ /\bchmod\s+-R\b/i,
2654
+ /\bchown\s+-R\b/i,
2655
+ />\s*\/dev\/(sd|nvme|disk|hd)/i,
2656
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:/,
2657
+ // fork bomb
2658
+ /\bgit\s+push\b[^\n]*--force/i,
2659
+ /\b(curl|wget)\b[^\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/i,
2660
+ // curl … | sh
2661
+ /\bsudo\s+rm\b/i
2662
+ ];
2663
+ function isDangerousShellCommand(command) {
2664
+ const c = String(command || "");
2665
+ return DANGEROUS_PATTERNS.some((re) => re.test(c));
2666
+ }
2667
+ var sessionApprovedDangerous = false;
2668
+ async function ensureDangerousApproval(command) {
2669
+ if (!isDangerousShellCommand(command)) return true;
2670
+ if (sessionApprovedDangerous) return true;
2671
+ const ask = interaction().confirmDangerous;
2672
+ if (!ask) return false;
2673
+ try {
2674
+ const answer = await ask(command);
2675
+ if (answer === "session") {
2676
+ sessionApprovedDangerous = true;
2677
+ return true;
2678
+ }
2679
+ return answer === "once";
2680
+ } catch {
2681
+ return false;
2682
+ }
2683
+ }
2684
+ var DANGEROUS_COMMAND_PROMPT = {
2685
+ title: "\uC704\uD5D8\uD560 \uC218 \uC788\uB294 \uBA85\uB839 \uC2E4\uD589 \uD655\uC778",
2686
+ message: "XGEN \uC5D0\uC774\uC804\uD2B8\uAC00 \uC774 PC \uC5D0\uC11C \uB418\uB3CC\uB9AC\uAE30 \uC5B4\uB824\uC6B4 \uBA85\uB839\uC744 \uC2E4\uD589\uD558\uB824 \uD569\uB2C8\uB2E4.",
2687
+ detail: (command) => command
2688
+ };
2689
+ function shapeResult(stdout, stderr, code, signal) {
2690
+ const parts = [];
2691
+ const out = stdout.length > OUTPUT_CAP ? stdout.slice(0, OUTPUT_CAP) + "\n\u2026(truncated)" : stdout;
2692
+ const err = stderr.length > OUTPUT_CAP ? stderr.slice(0, OUTPUT_CAP) + "\n\u2026(truncated)" : stderr;
2693
+ if (out.trim()) parts.push(out.replace(/\s+$/, ""));
2694
+ if (err.trim()) parts.push(`STDERR:
2695
+ ${err.replace(/\s+$/, "")}`);
2696
+ const failed = signal != null || code != null && code !== 0;
2697
+ if (signal) parts.push(`(terminated by signal ${signal})`);
2698
+ else if (code != null && code !== 0) parts.push(`(exit code ${code})`);
2699
+ return {
2700
+ content: [{ type: "text", text: parts.join("\n\n") || "(no output)" }],
2701
+ isError: failed
2702
+ };
2703
+ }
2704
+ var SYNCED_WORKSPACE_NOTE = `
2705
+ AGENT WORKSPACE ON THIS COMPUTER: when connected through this desktop connector, your own agent workspace is synced to a LOCAL folder \u2014 under the configured default working folder, one subfolder per connected agent (named after the agent). PREFER working there with these local tools; every change syncs back to your server workspace automatically, so web sessions and the sandbox see the same files.`;
2706
+ function shellToolSchema() {
2707
+ return {
2708
+ name: SHELL_TOOL,
2709
+ description: `Run ONE command on the USER'S OWN COMPUTER (the local desktop where this connector runs), through its native shell (${nativeShellLabel()}), as the logged-in user. This is the physical machine \u2014 NOT the cloud workspace/sandbox. Use it to operate that computer: run scripts, read/write local files, inspect the system, launch apps.` + SYNCED_WORKSPACE_NOTE + `
2710
+ IMPORTANT for reliability:
2711
+ \u2022 Non-interactive only \u2014 stdin is closed, so REPLs/prompts (bash, python with no args, \`read\`, pagers) return immediately instead of hanging. Pass the full command each call.
2712
+ \u2022 To launch a GUI app or a long-running/never-exiting process (editors like notepad/gedit, servers, watchers) \u2014 or ANY job that may run longer than a couple of minutes \u2014 set background:true. It starts detached, returns a job_id at once, and is NOT killed at the timeout; its output is captured. Poll it later with the ShellJob tool (action:'poll', job_id).
2713
+ \u2022 To just open a file/URL/folder with its default app, prefer the Open tool.
2714
+ Returns combined stdout/stderr and the exit code (foreground); a job_id (background). For huge output, pass head/tail (lines) or max_bytes to page it.`,
2715
+ inputSchema: {
2716
+ type: "object",
2717
+ properties: {
2718
+ command: { type: "string", description: "The shell command line to execute." },
2719
+ background: {
2720
+ type: "boolean",
2721
+ description: "Launch detached and return a job_id immediately (output captured, pollable via ShellJob). Use for GUI apps and long-running jobs so they keep running and are not killed at the timeout."
2722
+ },
2723
+ cwd: {
2724
+ type: "string",
2725
+ description: "Working directory (absolute). Defaults to the configured directory or home."
2726
+ },
2727
+ shell: {
2728
+ type: "string",
2729
+ enum: ["default", "powershell", "cmd", "bash", "sh"],
2730
+ description: "Shell to use. 'default' picks the OS native shell."
2731
+ },
2732
+ timeout_ms: {
2733
+ type: "integer",
2734
+ description: "Optional per-command timeout override (ms). Ignored when background=true."
2735
+ },
2736
+ tail: {
2737
+ type: "integer",
2738
+ description: "Return only the last N lines of output (for chatty commands)."
2739
+ },
2740
+ head: { type: "integer", description: "Return only the first N lines of output." },
2741
+ max_bytes: {
2742
+ type: "integer",
2743
+ description: `Cap returned output bytes (default/cap ${OUTPUT_CAP}).`
2744
+ }
2745
+ },
2746
+ required: ["command"]
2747
+ }
2748
+ };
2749
+ }
2750
+ function shellJobToolSchema() {
2751
+ return {
2752
+ name: SHELL_JOB_TOOL,
2753
+ description: `Manage long-running background jobs started with Shell(background:true) on the USER'S OWN COMPUTER. This is how you run work that outlives a single tool call: start it in the background, then poll it here until it finishes.
2754
+ \u2022 action:'list' \u2014 show all recent/running jobs (id, status, pid, duration, command).
2755
+ \u2022 action:'poll' (job_id) \u2014 status + captured stdout/stderr (paginated: tail default, or head/max_bytes).
2756
+ \u2022 action:'kill' (job_id) \u2014 terminate a running job (whole process tree).`,
2757
+ inputSchema: {
2758
+ type: "object",
2759
+ properties: {
2760
+ action: { type: "string", enum: ["list", "poll", "kill"], description: "Default 'list'." },
2761
+ job_id: {
2762
+ type: "string",
2763
+ description: "The job id returned by Shell(background:true). Required for poll/kill."
2764
+ },
2765
+ tail: { type: "integer", description: "poll: last N lines (default 200)." },
2766
+ head: { type: "integer", description: "poll: first N lines." },
2767
+ max_bytes: { type: "integer", description: "poll: cap returned output bytes." }
2768
+ }
2769
+ }
2770
+ };
2771
+ }
2772
+ function openToolSchema() {
2773
+ return {
2774
+ name: OPEN_TOOL,
2775
+ description: `Open a file, folder, or URL on the USER'S OWN COMPUTER with its default application. Non-blocking \u2014 the app launches and this returns immediately. Use this for "open <file>", "show me <folder>", "open <url>". Safe by construction: only http/https/mailto/tel/ftp URLs and filesystem paths within the allowed folders are opened (javascript:/data: and unknown schemes are refused). To launch an app by name or run a command, use Shell(background:true).`,
2776
+ inputSchema: {
2777
+ type: "object",
2778
+ properties: {
2779
+ target: {
2780
+ type: "string",
2781
+ description: "A file/folder path (within allowed folders) or an http/https/mailto/tel/ftp URL."
2782
+ }
2783
+ },
2784
+ required: ["target"]
2785
+ }
2786
+ };
2787
+ }
2788
+ function coerceShellArgs(args) {
2789
+ const a = args && typeof args === "object" ? args : {};
2790
+ const command = typeof a.command === "string" ? a.command : String(a.command ?? "");
2791
+ const cwd = typeof a.cwd === "string" && a.cwd.trim() ? a.cwd.trim() : void 0;
2792
+ const shell = typeof a.shell === "string" ? a.shell : void 0;
2793
+ const t = a.timeout_ms ?? a.timeoutMs;
2794
+ const timeoutMs = typeof t === "number" && t > 0 ? t : void 0;
2795
+ const bg = a.background ?? a.detach ?? a.detached;
2796
+ const background = bg === true || bg === "true" || bg === 1;
2797
+ return { command, cwd, shell, timeoutMs, background };
2798
+ }
2799
+ function coerceOpenArgs(args) {
2800
+ const a = args && typeof args === "object" ? args : {};
2801
+ const raw = a.target ?? a.path ?? a.url ?? a.file;
2802
+ return { target: typeof raw === "string" ? raw : String(raw ?? "") };
2803
+ }
2804
+ function killTree(child, detachedGroup) {
2805
+ try {
2806
+ if (IS_WIN2) {
2807
+ if (child.pid)
2808
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true });
2809
+ else child.kill("SIGKILL");
2810
+ } else if (detachedGroup && child.pid) {
2811
+ process.kill(-child.pid, "SIGKILL");
2812
+ } else {
2813
+ child.kill("SIGKILL");
2814
+ }
2815
+ } catch {
2816
+ try {
2817
+ child.kill("SIGKILL");
2818
+ } catch {
2819
+ }
2820
+ }
2821
+ }
2822
+ var bgJobs = /* @__PURE__ */ new Map();
2823
+ var bgJobSeq = 0;
2824
+ function newJobId() {
2825
+ bgJobSeq += 1;
2826
+ return `job-${Date.now().toString(36)}-${bgJobSeq}`;
2827
+ }
2828
+ function evictFinishedJobs() {
2829
+ if (bgJobs.size <= MAX_JOBS) return;
2830
+ const finished = [...bgJobs.values()].filter((j) => j.status !== "running").sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt));
2831
+ for (const j of finished) {
2832
+ if (bgJobs.size <= MAX_JOBS) break;
2833
+ bgJobs.delete(j.id);
2834
+ }
2835
+ }
2836
+ function appendCapped(prev, chunk) {
2837
+ const next = prev + chunk;
2838
+ return next.length > JOB_STREAM_CAP ? next.slice(-JOB_STREAM_CAP) : next;
2839
+ }
2840
+ function paginate(text, opts) {
2841
+ const totalBytes = Buffer.byteLength(text);
2842
+ let out = text;
2843
+ let truncated = false;
2844
+ const head = Number(opts.head) > 0 ? Math.floor(Number(opts.head)) : 0;
2845
+ const tail = Number(opts.tail) > 0 ? Math.floor(Number(opts.tail)) : 0;
2846
+ if (head || tail) {
2847
+ const lines = text.split("\n");
2848
+ if (head) out = lines.slice(0, head).join("\n");
2849
+ else out = lines.slice(-tail).join("\n");
2850
+ if (out.length < text.length) truncated = true;
2851
+ }
2852
+ const cap = Math.max(
2853
+ 1,
2854
+ Math.min(
2855
+ OUTPUT_CAP,
2856
+ Number(opts.maxBytes) > 0 ? Math.floor(Number(opts.maxBytes)) : OUTPUT_CAP
2857
+ )
2858
+ );
2859
+ if (Buffer.byteLength(out) > cap) {
2860
+ const buf = Buffer.from(out, "utf8");
2861
+ if (head) {
2862
+ let end = cap;
2863
+ while (end > 0 && (buf[end] & 192) === 128) end--;
2864
+ out = buf.subarray(0, end).toString("utf8");
2865
+ } else {
2866
+ let start = buf.length - cap;
2867
+ while (start < buf.length && (buf[start] & 192) === 128) start++;
2868
+ out = buf.subarray(start).toString("utf8");
2869
+ }
2870
+ truncated = true;
2871
+ }
2872
+ return { text: out, truncated, totalBytes };
2873
+ }
2874
+ var SAFE_OPEN_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "ftp", "ftps"]);
2875
+ function classifyOpenTarget(target) {
2876
+ const t = String(target || "").trim();
2877
+ if (!t) return { kind: "blocked", reason: "target \uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4." };
2878
+ const m = t.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
2879
+ if (m) {
2880
+ const scheme = m[1].toLowerCase();
2881
+ if (scheme === "file")
2882
+ return { kind: "path", value: t.replace(/^file:\/\//i, "").replace(/^file:/i, "") };
2883
+ if (SAFE_OPEN_SCHEMES.has(scheme)) return { kind: "url", value: t };
2884
+ if (IS_WIN2 && /^[a-zA-Z]:[\\/]/.test(t)) return { kind: "path", value: t };
2885
+ return {
2886
+ kind: "blocked",
2887
+ reason: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uC2A4\uD0B4 '${scheme}:' (javascript/data \uB4F1\uC740 \uCC28\uB2E8).`
2888
+ };
2889
+ }
2890
+ return { kind: "path", value: t };
2891
+ }
2892
+ var BINARY_EXT = /* @__PURE__ */ new Set([
2893
+ ".png",
2894
+ ".jpg",
2895
+ ".jpeg",
2896
+ ".gif",
2897
+ ".webp",
2898
+ ".ico",
2899
+ ".bmp",
2900
+ ".pdf",
2901
+ ".zip",
2902
+ ".gz",
2903
+ ".tar",
2904
+ ".7z",
2905
+ ".rar",
2906
+ ".mp3",
2907
+ ".mp4",
2908
+ ".mov",
2909
+ ".avi",
2910
+ ".wav",
2911
+ ".ogg",
2912
+ ".woff",
2913
+ ".woff2",
2914
+ ".ttf",
2915
+ ".eot",
2916
+ ".so",
2917
+ ".dll",
2918
+ ".dylib",
2919
+ ".exe",
2920
+ ".bin",
2921
+ ".class",
2922
+ ".o"
2923
+ ]);
2924
+ function resolveOne(p, base) {
2925
+ const home = homedir4();
2926
+ const expanded = p.startsWith("~") ? home + p.slice(1) : p;
2927
+ return isAbsolute2(expanded) ? pathResolve(expanded) : pathResolve(base, expanded);
2928
+ }
2929
+ function resolveWithinRoots(input, roots) {
2930
+ const home = homedir4();
2931
+ const effective = (roots && roots.length ? roots : [home]).map((r) => resolveOne(r, home));
2932
+ const abs = resolveOne(String(input || ""), home);
2933
+ for (const root of effective) {
2934
+ const rel = pathRelative(root, abs);
2935
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) return abs;
2936
+ }
2937
+ return null;
2938
+ }
2939
+ function readFileToolSchema() {
2940
+ return {
2941
+ name: READ_FILE_TOOL,
2942
+ description: "Read a text file on the USER'S OWN COMPUTER (the local desktop), within the allowed folders. Prefer this over `Shell cat` \u2014 it distinguishes \u201Cnot found\u201D from \u201Cno permission\u201D cleanly. Returns UTF-8 text (truncated at maxBytes)." + SYNCED_WORKSPACE_NOTE,
2943
+ inputSchema: {
2944
+ type: "object",
2945
+ properties: {
2946
+ path: {
2947
+ type: "string",
2948
+ description: "File path. Absolute, ~ for home, or relative to home."
2949
+ },
2950
+ maxBytes: {
2951
+ type: "number",
2952
+ description: `Max bytes to return (default/cap ${OUTPUT_CAP}).`
2953
+ }
2954
+ },
2955
+ required: ["path"]
2956
+ }
2957
+ };
2958
+ }
2959
+ function writeFileToolSchema() {
2960
+ return {
2961
+ name: WRITE_FILE_TOOL,
2962
+ description: "Write (or append to) a text file on the USER'S OWN COMPUTER, within the allowed folders. Creates parent directories as needed. Prefer this over shell redirection." + SYNCED_WORKSPACE_NOTE,
2963
+ inputSchema: {
2964
+ type: "object",
2965
+ properties: {
2966
+ path: {
2967
+ type: "string",
2968
+ description: "File path. Absolute, ~ for home, or relative to home."
2969
+ },
2970
+ content: { type: "string", description: "Text to write." },
2971
+ mode: { type: "string", enum: ["overwrite", "append"], description: "Default overwrite." }
2972
+ },
2973
+ required: ["path", "content"]
2974
+ }
2975
+ };
2976
+ }
2977
+ function listDirToolSchema() {
2978
+ return {
2979
+ name: LIST_DIR_TOOL,
2980
+ description: "List a directory on the USER'S OWN COMPUTER (within allowed folders). Shows type/size/name." + SYNCED_WORKSPACE_NOTE,
2981
+ inputSchema: {
2982
+ type: "object",
2983
+ properties: { path: { type: "string", description: "Directory path (default: home)." } }
2984
+ }
2985
+ };
2986
+ }
2987
+ function searchToolSchema() {
2988
+ return {
2989
+ name: SEARCH_TOOL,
2990
+ description: "Recursively search text files under a folder on the USER'S OWN COMPUTER (within allowed folders) for a literal substring. Skips node_modules/.git/binaries. Returns path:line: match.",
2991
+ inputSchema: {
2992
+ type: "object",
2993
+ properties: {
2994
+ query: { type: "string", description: "Literal substring to find." },
2995
+ path: { type: "string", description: "Root folder to search (default: home)." },
2996
+ maxResults: { type: "number", description: "Max matches (default 100, cap 500)." }
2997
+ },
2998
+ required: ["query"]
2999
+ }
3000
+ };
3001
+ }
3002
+ function clipboardToolSchema() {
3003
+ return {
3004
+ name: CLIPBOARD_TOOL,
3005
+ description: "Read or write the USER'S system clipboard (plain text).",
3006
+ inputSchema: {
3007
+ type: "object",
3008
+ properties: {
3009
+ action: {
3010
+ type: "string",
3011
+ enum: ["read", "write"],
3012
+ description: "read (default) or write."
3013
+ },
3014
+ text: { type: "string", description: "Text to put on the clipboard when action=write." }
3015
+ }
3016
+ }
3017
+ };
3018
+ }
3019
+ function notifyToolSchema() {
3020
+ return {
3021
+ name: NOTIFY_TOOL,
3022
+ description: "Show a desktop notification on the USER'S OWN COMPUTER.",
3023
+ inputSchema: {
3024
+ type: "object",
3025
+ properties: {
3026
+ title: { type: "string", description: "Notification title." },
3027
+ body: { type: "string", description: "Notification body." }
3028
+ },
3029
+ required: ["title"]
3030
+ }
3031
+ };
3032
+ }
3033
+ var LocalToolProvider = class {
3034
+ cfg = shellConfig(void 0);
3035
+ delegate = null;
3036
+ /** 서버 런타임이 이 PC 를 실행 환경으로 쓰는 내부 브리지 (workspace-bridge-tools). */
3037
+ workspaceBridge = null;
3038
+ /** main 의 공통 NotificationCenter. 주입해 Node 단위 테스트는 Electron 을 요구하지 않는다. */
3039
+ notificationHandler = null;
3040
+ /** 로컬 MCP 자기관리(McpAddServer/McpRemoveServer/McpListServers). 로컬 MCP 가 켜져
3041
+ * 있을 때만 도구를 광고한다 — 이 delegate 자신이 게이트를 판단한다. */
3042
+ mcpAdmin = null;
3043
+ configure(cfg, delegate) {
3044
+ this.cfg = shellConfig(cfg);
3045
+ this.delegate = delegate ?? null;
3046
+ }
3047
+ /** 워크스페이스 브리지 배선 — 로컬 동기화 매니저가 준비된 뒤 한 번 건다. */
3048
+ configureWorkspaceBridge(bridge) {
3049
+ this.workspaceBridge = bridge;
3050
+ }
3051
+ configureNotificationHandler(handler) {
3052
+ this.notificationHandler = handler;
3053
+ }
3054
+ /** 로컬 MCP 자기관리 delegate 배선(syncMcp 에서). null 이면 미노출. */
3055
+ configureMcpAdmin(admin) {
3056
+ this.mcpAdmin = admin;
3057
+ }
3058
+ /** True iff this call frame belongs to a built-in tool (server === LOCAL_SERVER). */
3059
+ owns(server) {
3060
+ return server === LOCAL_SERVER;
3061
+ }
3062
+ /** Tools advertised into the catalog. Empty when the capability is off. */
3063
+ /**
3064
+ * 셸/파일 도구의 **전체 카탈로그** — 켜져 있는지와 무관하게.
3065
+ *
3066
+ * `advertise()` 는 "지금 에이전트에게 노출되는 것"이라 꺼져 있으면 빈 목록이다.
3067
+ * 그건 서버에 광고할 때는 맞지만, 사용자가 "이 도구로 뭘 할 수 있지"를 물을 때는
3068
+ * 아무 답이 안 된다. 두 질문은 다르므로 답도 둘이다.
3069
+ */
3070
+ catalog() {
3071
+ return [
3072
+ shellToolSchema(),
3073
+ shellJobToolSchema(),
3074
+ openToolSchema(),
3075
+ readFileToolSchema(),
3076
+ writeFileToolSchema(),
3077
+ listDirToolSchema(),
3078
+ searchToolSchema(),
3079
+ clipboardToolSchema(),
3080
+ notifyToolSchema()
3081
+ ];
3082
+ }
3083
+ advertise() {
3084
+ const shell = this.cfg.enabled ? [
3085
+ shellToolSchema(),
3086
+ shellJobToolSchema(),
3087
+ openToolSchema(),
3088
+ readFileToolSchema(),
3089
+ writeFileToolSchema(),
3090
+ listDirToolSchema(),
3091
+ searchToolSchema(),
3092
+ clipboardToolSchema(),
3093
+ notifyToolSchema()
3094
+ ] : [];
3095
+ const bridge = this.cfg.enabled ? this.workspaceBridge?.advertise() ?? [] : [];
3096
+ const mcpAdmin = this.mcpAdmin?.advertise() ?? [];
3097
+ return [...shell, ...bridge, ...mcpAdmin, ...this.delegate?.advertise() ?? []];
3098
+ }
3099
+ async callTool(tool, args, context) {
3100
+ if (this.delegate?.owns(tool)) return this.delegate.callTool(tool, args, context);
3101
+ if (this.mcpAdmin?.owns(tool)) return this.mcpAdmin.callTool(tool, args);
3102
+ if (!this.cfg.enabled) throw new Error("\uB85C\uCEEC \uB3C4\uAD6C \uC811\uADFC\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4 (\uC124\uC815 > \uB85C\uCEEC \uB3C4\uAD6C).");
3103
+ if (this.workspaceBridge?.owns(tool)) return this.workspaceBridge.callTool(tool, args);
3104
+ if (tool === SHELL_TOOL) return this.shell(args);
3105
+ if (tool === SHELL_JOB_TOOL) return this.shellJob(args);
3106
+ if (tool === OPEN_TOOL) return this.open(args);
3107
+ if (tool === READ_FILE_TOOL) return this.readFile(args);
3108
+ if (tool === WRITE_FILE_TOOL) return this.writeFile(args);
3109
+ if (tool === LIST_DIR_TOOL) return this.listDir(args);
3110
+ if (tool === SEARCH_TOOL) return this.search(args);
3111
+ if (tool === CLIPBOARD_TOOL) return this.clipboard(args);
3112
+ if (tool === NOTIFY_TOOL) return this.notify(args, context);
3113
+ throw new Error(`unknown local tool: ${tool}`);
3114
+ }
3115
+ /** Resolve + scope-check a file path against allowedRoots (throws if outside). */
3116
+ guardPath(p) {
3117
+ const abs = resolveWithinRoots(String(p ?? ""), this.cfg.allowedRoots);
3118
+ if (!abs) {
3119
+ throw new Error(
3120
+ `\uACBD\uB85C\uAC00 \uD5C8\uC6A9\uB41C \uBC94\uC704 \uBC16\uC785\uB2C8\uB2E4: ${String(p ?? "")} (\uC124\uC815 > \uB85C\uCEEC \uB3C4\uAD6C > \uD5C8\uC6A9 \uD3F4\uB354\uC5D0\uC11C \uBC94\uC704\uB97C \uB113\uD790 \uC218 \uC788\uC2B5\uB2C8\uB2E4).`
3121
+ );
3122
+ }
3123
+ return abs;
3124
+ }
3125
+ async readFile(args) {
3126
+ const a = args && typeof args === "object" ? args : {};
3127
+ const abs = this.guardPath(a.path);
3128
+ const maxBytes = Math.max(1, Math.min(OUTPUT_CAP, Number(a.maxBytes) || OUTPUT_CAP));
3129
+ try {
3130
+ const buf = await fsReadFile(abs);
3131
+ const text = buf.subarray(0, maxBytes).toString("utf8");
3132
+ const suffix = buf.byteLength > maxBytes ? `
3133
+ \u2026(truncated, ${buf.byteLength} bytes total)` : "";
3134
+ return { content: [{ type: "text", text: (text || "(empty file)") + suffix }] };
3135
+ } catch (e) {
3136
+ return {
3137
+ content: [{ type: "text", text: `\uC77D\uAE30 \uC2E4\uD328: ${e.message}` }],
3138
+ isError: true
3139
+ };
3140
+ }
3141
+ }
3142
+ async writeFile(args) {
3143
+ const a = args && typeof args === "object" ? args : {};
3144
+ const abs = this.guardPath(a.path);
3145
+ const content = typeof a.content === "string" ? a.content : String(a.content ?? "");
3146
+ const append = a.mode === "append" || a.append === true;
3147
+ try {
3148
+ await mkdir2(dirname2(abs), { recursive: true });
3149
+ if (append) await fsAppendFile(abs, content, "utf8");
3150
+ else await fsWriteFile(abs, content, "utf8");
3151
+ return {
3152
+ content: [
3153
+ {
3154
+ type: "text",
3155
+ text: `${append ? "\uC774\uC5B4\uC37C\uC2B5\uB2C8\uB2E4" : "\uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4"}: ${abs} (${Buffer.byteLength(content)} bytes)`
3156
+ }
3157
+ ]
3158
+ };
3159
+ } catch (e) {
3160
+ return {
3161
+ content: [{ type: "text", text: `\uC4F0\uAE30 \uC2E4\uD328: ${e.message}` }],
3162
+ isError: true
3163
+ };
3164
+ }
3165
+ }
3166
+ async listDir(args) {
3167
+ const a = args && typeof args === "object" ? args : {};
3168
+ const abs = this.guardPath(a.path ?? "~");
3169
+ try {
3170
+ const names = await readdir(abs);
3171
+ const rows = [];
3172
+ for (const name of names.slice(0, 1e3)) {
3173
+ try {
3174
+ const s = await stat(pathJoin(abs, name));
3175
+ rows.push(`${s.isDirectory() ? "d" : "-"} ${String(s.size).padStart(10)} ${name}`);
3176
+ } catch {
3177
+ rows.push(`? ? ${name}`);
3178
+ }
3179
+ }
3180
+ const more = names.length > 1e3 ? `
3181
+ \u2026(${names.length} entries, first 1000 shown)` : "";
3182
+ return { content: [{ type: "text", text: rows.join("\n") + more || "(empty directory)" }] };
3183
+ } catch (e) {
3184
+ return {
3185
+ content: [{ type: "text", text: `\uBAA9\uB85D \uC2E4\uD328: ${e.message}` }],
3186
+ isError: true
3187
+ };
3188
+ }
3189
+ }
3190
+ async search(args) {
3191
+ const a = args && typeof args === "object" ? args : {};
3192
+ const query = String(a.query ?? "");
3193
+ if (!query) throw new Error("query must not be empty");
3194
+ const abs = this.guardPath(a.path ?? "~");
3195
+ const maxResults = Math.max(1, Math.min(500, Number(a.maxResults) || 100));
3196
+ const hits = [];
3197
+ const skipDirs = /* @__PURE__ */ new Set([
3198
+ "node_modules",
3199
+ ".git",
3200
+ ".venv",
3201
+ "dist",
3202
+ "out",
3203
+ ".next",
3204
+ "__pycache__"
3205
+ ]);
3206
+ const walk = async (dir, depth) => {
3207
+ if (hits.length >= maxResults || depth > 8) return;
3208
+ let entries2;
3209
+ try {
3210
+ entries2 = await readdir(dir);
3211
+ } catch {
3212
+ return;
3213
+ }
3214
+ for (const name of entries2) {
3215
+ if (hits.length >= maxResults) return;
3216
+ const full = pathJoin(dir, name);
3217
+ let s;
3218
+ try {
3219
+ s = await stat(full);
3220
+ } catch {
3221
+ continue;
3222
+ }
3223
+ if (s.isDirectory()) {
3224
+ if (!skipDirs.has(name) && !name.startsWith(".")) await walk(full, depth + 1);
3225
+ } else if (s.size <= 2e6 && !BINARY_EXT.has(extname(name).toLowerCase())) {
3226
+ try {
3227
+ const text = await fsReadFile(full, "utf8");
3228
+ const lines = text.split(/\r?\n/);
3229
+ for (let i = 0; i < lines.length; i++) {
3230
+ if (lines[i].includes(query)) {
3231
+ hits.push(`${full}:${i + 1}: ${lines[i].trim().slice(0, 200)}`);
3232
+ if (hits.length >= maxResults) return;
3233
+ }
3234
+ }
3235
+ } catch {
3236
+ }
3237
+ }
3238
+ }
3239
+ };
3240
+ await walk(abs, 0);
3241
+ return {
3242
+ content: [
3243
+ {
3244
+ type: "text",
3245
+ text: hits.length ? hits.join("\n") : `'${query}' \uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${abs}).`
3246
+ }
3247
+ ]
3248
+ };
3249
+ }
3250
+ async clipboard(args) {
3251
+ const a = args && typeof args === "object" ? args : {};
3252
+ const action = String(a.action ?? "read");
3253
+ const clip = interaction().clipboard;
3254
+ if (!clip) {
3255
+ return {
3256
+ content: [{ type: "text", text: "\uC774 \uD638\uC2A4\uD2B8\uC5D0\uC11C\uB294 \uD074\uB9BD\uBCF4\uB4DC\uB97C \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." }],
3257
+ isError: true
3258
+ };
3259
+ }
3260
+ try {
3261
+ if (action === "write") {
3262
+ await clip.write(String(a.text ?? ""));
3263
+ return { content: [{ type: "text", text: "\uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC\uD588\uC2B5\uB2C8\uB2E4." }] };
3264
+ }
3265
+ const text = await clip.read();
3266
+ return { content: [{ type: "text", text: text || "(\uD074\uB9BD\uBCF4\uB4DC\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4)" }] };
3267
+ } catch (e) {
3268
+ return {
3269
+ content: [{ type: "text", text: `\uD074\uB9BD\uBCF4\uB4DC \uC811\uADFC \uC2E4\uD328: ${e.message}` }],
3270
+ isError: true
3271
+ };
3272
+ }
3273
+ }
3274
+ async notify(args, context) {
3275
+ const a = args && typeof args === "object" ? args : {};
3276
+ const title = String(a.title ?? "XGEN");
3277
+ const body = String(a.body ?? "");
3278
+ try {
3279
+ if (this.notificationHandler) {
3280
+ const shown2 = await this.notificationHandler(title, body, context);
3281
+ return {
3282
+ content: [
3283
+ {
3284
+ type: "text",
3285
+ text: shown2 ? "\uC54C\uB9BC\uC744 \uD45C\uC2DC\uD588\uC2B5\uB2C8\uB2E4." : "\uC0AC\uC6A9\uC790\uC758 \uC54C\uB9BC \uC124\uC815\uC5D0 \uB530\uB77C \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."
3286
+ }
3287
+ ]
3288
+ };
3289
+ }
3290
+ const notify = interaction().notify;
3291
+ if (!notify) {
3292
+ return {
3293
+ content: [{ type: "text", text: "\uC774 \uD638\uC2A4\uD2B8\uC5D0\uC11C\uB294 \uC54C\uB9BC\uC744 \uD45C\uC2DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." }],
3294
+ isError: true
3295
+ };
3296
+ }
3297
+ const shown = await notify(title, body);
3298
+ return {
3299
+ content: [
3300
+ { type: "text", text: shown ? "\uC54C\uB9BC\uC744 \uD45C\uC2DC\uD588\uC2B5\uB2C8\uB2E4." : "\uC54C\uB9BC\uC774 \uD45C\uC2DC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." }
3301
+ ]
3302
+ };
3303
+ } catch (e) {
3304
+ return {
3305
+ content: [{ type: "text", text: `\uC54C\uB9BC \uC2E4\uD328: ${e.message}` }],
3306
+ isError: true
3307
+ };
3308
+ }
3309
+ }
3310
+ async shell(args) {
3311
+ const { command, cwd, shell, timeoutMs, background } = coerceShellArgs(args);
3312
+ if (!command.trim()) throw new Error("command must not be empty");
3313
+ if (isBlocked(command, this.cfg.blocked)) {
3314
+ throw new Error(`\uBA85\uB839 '${firstToken(command)}' \uC740(\uB294) \uCC28\uB2E8 \uBAA9\uB85D\uC5D0 \uC788\uC5B4 \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
3315
+ }
3316
+ if (!await ensureDangerousApproval(command)) {
3317
+ return {
3318
+ content: [
3319
+ { type: "text", text: "\uC0AC\uC6A9\uC790\uAC00 \uC774 \uBA85\uB839\uC758 \uC2E4\uD589\uC744 \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4 (\uC704\uD5D8\uD560 \uC218 \uC788\uB294 \uBA85\uB839)." }
3320
+ ],
3321
+ isError: true
3322
+ };
3323
+ }
3324
+ const pathStr = await augmentedPath();
3325
+ const userShellBin = IS_WIN2 ? null : process.env.SHELL || null;
3326
+ const { file, args: argv } = shellInvocation(command, userShellBin, shell);
3327
+ const env = buildChildEnv(pathStr);
3328
+ const runCwd = cwd || this.cfg.cwd || homedir4();
3329
+ if (background) return this.spawnBackground(command, file, argv, env, runCwd);
3330
+ const timeout = Math.max(
3331
+ MIN_TIMEOUT_MS,
3332
+ Math.min(MAX_TIMEOUT_MS, Math.round(timeoutMs || this.cfg.timeoutMs))
3333
+ );
3334
+ const r = await this.spawnCapture(file, argv, env, runCwd, timeout);
3335
+ if (r.error)
3336
+ return {
3337
+ content: [{ type: "text", text: `\uC178 \uC2E4\uD589 \uC2E4\uD328: ${r.error.message}` }],
3338
+ isError: true
3339
+ };
3340
+ if (r.timedOut) {
3341
+ return {
3342
+ content: [
3343
+ {
3344
+ type: "text",
3345
+ text: `\uBA85\uB839\uC774 ${Math.round(timeout / 1e3)}\uCD08 \uC548\uC5D0 \uB05D\uB098\uC9C0 \uC54A\uC544 \uC911\uB2E8\uD588\uC2B5\uB2C8\uB2E4. \uB300\uD654\uD615 \uBA85\uB839\uC774\uAC70\uB098 \uC885\uB8CC\uB418\uC9C0 \uC54A\uB294 \uD504\uB85C\uADF8\uB7A8(\uC5D0\uB514\uD130\xB7\uC11C\uBC84 \uB4F1)\uC774\uBA74 background:true \uB85C \uC2E4\uD589\uD558\uC138\uC694.` + (r.stdout || r.stderr ? `
3346
+
3347
+ --- \uC911\uB2E8 \uC804 \uCD9C\uB825 ---
3348
+ ${(r.stdout + "\n" + r.stderr).trim().slice(-2e3)}` : "")
3349
+ }
3350
+ ],
3351
+ isError: true
3352
+ };
3353
+ }
3354
+ const a = args && typeof args === "object" ? args : {};
3355
+ const head = Number(a.head) || 0;
3356
+ const tail = Number(a.tail) || 0;
3357
+ const maxBytes = Number(a.max_bytes ?? a.maxBytes) || 0;
3358
+ if (head || tail || maxBytes) {
3359
+ const outP = paginate(r.stdout, { head, tail, maxBytes });
3360
+ const errP = paginate(r.stderr, { head, tail, maxBytes });
3361
+ return shapeResult(outP.text, errP.text, r.code, r.signal);
3362
+ }
3363
+ return shapeResult(r.stdout, r.stderr, r.code, r.signal);
3364
+ }
3365
+ async open(args) {
3366
+ const { target } = coerceOpenArgs(args);
3367
+ if (!target.trim()) throw new Error("target must not be empty");
3368
+ const cls = classifyOpenTarget(target);
3369
+ if (cls.kind === "blocked") {
3370
+ return { content: [{ type: "text", text: `\uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${cls.reason}` }], isError: true };
3371
+ }
3372
+ const host = interaction();
3373
+ if (!host.openExternal && !host.openPath) {
3374
+ return {
3375
+ content: [{ type: "text", text: "\uC774 \uD638\uC2A4\uD2B8\uC5D0\uC11C\uB294 \uC678\uBD80 \uC5F4\uAE30\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." }],
3376
+ isError: true
3377
+ };
3378
+ }
3379
+ try {
3380
+ if (cls.kind === "url") {
3381
+ if (!host.openExternal) {
3382
+ return {
3383
+ content: [{ type: "text", text: "\uC774 \uD638\uC2A4\uD2B8\uC5D0\uC11C\uB294 \uB9C1\uD06C\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." }],
3384
+ isError: true
3385
+ };
3386
+ }
3387
+ await host.openExternal(cls.value);
3388
+ return { content: [{ type: "text", text: `\uC5F4\uC5C8\uC2B5\uB2C8\uB2E4: ${cls.value}` }] };
3389
+ }
3390
+ const abs = this.guardPath(cls.value);
3391
+ if (!host.openPath) {
3392
+ return {
3393
+ content: [{ type: "text", text: "\uC774 \uD638\uC2A4\uD2B8\uC5D0\uC11C\uB294 \uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." }],
3394
+ isError: true
3395
+ };
3396
+ }
3397
+ const err = await host.openPath(abs);
3398
+ if (err) return { content: [{ type: "text", text: `\uC5F4\uAE30 \uC2E4\uD328: ${err}` }], isError: true };
3399
+ return { content: [{ type: "text", text: `\uC5F4\uC5C8\uC2B5\uB2C8\uB2E4: ${abs}` }] };
3400
+ } catch (e) {
3401
+ return {
3402
+ content: [{ type: "text", text: `\uC5F4\uAE30 \uC2E4\uD328: ${e.message}` }],
3403
+ isError: true
3404
+ };
3405
+ }
3406
+ }
3407
+ /** Foreground: capture output, close stdin (no interactive hang), tree-kill on timeout. */
3408
+ spawnCapture(file, argv, env, cwd, timeoutMs) {
3409
+ const detachedGroup = !IS_WIN2;
3410
+ return new Promise((resolve) => {
3411
+ let child;
3412
+ try {
3413
+ child = spawn(file, argv, {
3414
+ cwd: cwd || homedir4(),
3415
+ env,
3416
+ windowsHide: true,
3417
+ detached: detachedGroup,
3418
+ // stdin IGNORED → interactive programs get EOF immediately instead of
3419
+ // blocking to the timeout (the "대화형 쉘 타임아웃" report).
3420
+ stdio: ["ignore", "pipe", "pipe"]
3421
+ });
3422
+ } catch (e) {
3423
+ resolve({ code: null, signal: null, stdout: "", stderr: "", error: e });
3424
+ return;
3425
+ }
3426
+ let out = "";
3427
+ let err = "";
3428
+ let done = false;
3429
+ const finish = (r) => {
3430
+ if (done) return;
3431
+ done = true;
3432
+ clearTimeout(timer);
3433
+ resolve(r);
3434
+ };
3435
+ const timer = setTimeout(() => {
3436
+ killTree(child, detachedGroup);
3437
+ finish({ code: null, signal: "SIGKILL", stdout: out, stderr: err, timedOut: true });
3438
+ }, timeoutMs);
3439
+ child.stdout?.on("data", (d) => {
3440
+ out += String(d);
3441
+ if (out.length > OUTPUT_CAP * 2) out = out.slice(-OUTPUT_CAP * 2);
3442
+ });
3443
+ child.stderr?.on("data", (d) => {
3444
+ err += String(d);
3445
+ if (err.length > OUTPUT_CAP * 2) err = err.slice(-OUTPUT_CAP * 2);
3446
+ });
3447
+ child.on(
3448
+ "error",
3449
+ (e) => finish({ code: null, signal: null, stdout: out, stderr: err, error: e })
3450
+ );
3451
+ child.on("close", (code, signal) => finish({ code, signal, stdout: out, stderr: err }));
3452
+ });
3453
+ }
3454
+ /** Background: detached, output captured into the job registry, returns a
3455
+ * job_id at once. The process keeps running past any tool-call timeout; poll
3456
+ * or kill it later with the ShellJob tool. */
3457
+ spawnBackground(command, file, argv, env, cwd) {
3458
+ const detachedGroup = !IS_WIN2;
3459
+ return new Promise((resolve) => {
3460
+ const running = [...bgJobs.values()].filter((j) => j.status === "running").length;
3461
+ if (running >= MAX_RUNNING_JOBS) {
3462
+ resolve({
3463
+ content: [
3464
+ {
3465
+ type: "text",
3466
+ text: `\uC2E4\uD589 \uC911\uC778 \uBC31\uADF8\uB77C\uC6B4\uB4DC \uC791\uC5C5\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4 (${running}/${MAX_RUNNING_JOBS}). ShellJob(action:'list')\uB85C \uD655\uC778\uD558\uACE0 kill \uB85C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
3467
+ }
3468
+ ],
3469
+ isError: true
3470
+ });
3471
+ return;
3472
+ }
3473
+ let child;
3474
+ try {
3475
+ child = spawn(file, argv, {
3476
+ cwd: cwd || homedir4(),
3477
+ env,
3478
+ windowsHide: true,
3479
+ detached: detachedGroup,
3480
+ // Capture output (so it can be polled) but close stdin so REPLs don't hang.
3481
+ stdio: ["ignore", "pipe", "pipe"]
3482
+ });
3483
+ } catch (e) {
3484
+ resolve({
3485
+ content: [
3486
+ {
3487
+ type: "text",
3488
+ text: `\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`
3489
+ }
3490
+ ],
3491
+ isError: true
3492
+ });
3493
+ return;
3494
+ }
3495
+ const job = {
3496
+ id: newJobId(),
3497
+ command,
3498
+ pid: child.pid,
3499
+ child,
3500
+ detachedGroup,
3501
+ status: "running",
3502
+ code: null,
3503
+ signal: null,
3504
+ startedAt: Date.now(),
3505
+ stdout: "",
3506
+ stderr: ""
3507
+ };
3508
+ bgJobs.set(job.id, job);
3509
+ evictFinishedJobs();
3510
+ child.stdout?.on("data", (d) => {
3511
+ job.stdout = appendCapped(job.stdout, String(d));
3512
+ });
3513
+ child.stderr?.on("data", (d) => {
3514
+ job.stderr = appendCapped(job.stderr, String(d));
3515
+ });
3516
+ let settled = false;
3517
+ const done = (r) => {
3518
+ if (settled) return;
3519
+ settled = true;
3520
+ resolve(r);
3521
+ };
3522
+ child.on("error", (e) => {
3523
+ if (job.status === "running") {
3524
+ job.status = "error";
3525
+ job.errorMsg = e.message;
3526
+ job.endedAt = Date.now();
3527
+ }
3528
+ done({
3529
+ content: [{ type: "text", text: `\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2E4\uD328: ${e.message}` }],
3530
+ isError: true
3531
+ });
3532
+ });
3533
+ child.on("close", (code, signal) => {
3534
+ job.code = code;
3535
+ job.signal = signal;
3536
+ if (job.status === "running") {
3537
+ job.status = signal ? "killed" : "exited";
3538
+ job.endedAt = Date.now();
3539
+ } else if (!job.endedAt) {
3540
+ job.endedAt = Date.now();
3541
+ }
3542
+ });
3543
+ child.unref();
3544
+ child.stdout?.unref?.();
3545
+ child.stderr?.unref?.();
3546
+ setTimeout(
3547
+ () => done({
3548
+ content: [
3549
+ {
3550
+ type: "text",
3551
+ text: `\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC791\uC5C5\uC744 \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4.
3552
+ job_id: ${job.id} (pid ${job.pid ?? "?"})
3553
+ \uACC4\uC18D \uC2E4\uD589\uB418\uBA70 \uCD9C\uB825\uC774 \uCEA1\uCC98\uB429\uB2C8\uB2E4. \uC0C1\uD0DC\xB7\uCD9C\uB825\uC740 ShellJob(action:'poll', job_id) \uB85C, \uC885\uB8CC\uB294 ShellJob(action:'kill', job_id) \uB85C \uD655\uC778/\uC81C\uC5B4\uD558\uC138\uC694.`
3554
+ }
3555
+ ]
3556
+ }),
3557
+ BG_SETTLE_MS
3558
+ );
3559
+ });
3560
+ }
3561
+ /** ShellJob: manage background jobs — list / poll (status+output) / kill. */
3562
+ async shellJob(args) {
3563
+ const a = args && typeof args === "object" ? args : {};
3564
+ const action = String(a.action ?? "list").toLowerCase();
3565
+ const jobId = String(a.job_id ?? a.jobId ?? "").trim();
3566
+ if (action === "list") {
3567
+ if (!bgJobs.size)
3568
+ return {
3569
+ content: [
3570
+ { type: "text", text: "\uC2E4\uD589 \uC911\uC774\uAC70\uB098 \uCD5C\uADFC \uC885\uB8CC\uB41C \uBC31\uADF8\uB77C\uC6B4\uB4DC \uC791\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." }
3571
+ ]
3572
+ };
3573
+ const rows = [...bgJobs.values()].sort((x, y) => y.startedAt - x.startedAt).map((j) => {
3574
+ const dur = Math.round(((j.endedAt ?? Date.now()) - j.startedAt) / 1e3);
3575
+ const exit = j.status === "running" ? "" : ` exit=${j.signal ? j.signal : j.code}`;
3576
+ return `${j.id} [${j.status}${exit}] pid=${j.pid ?? "?"} ${dur}s ${j.command.slice(0, 80)}`;
3577
+ });
3578
+ return { content: [{ type: "text", text: rows.join("\n") }] };
3579
+ }
3580
+ const job = jobId ? bgJobs.get(jobId) : void 0;
3581
+ if (!job) {
3582
+ return {
3583
+ content: [
3584
+ {
3585
+ type: "text",
3586
+ text: `job_id '${jobId}' \uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. ShellJob(action:'list') \uB85C \uD655\uC778\uD558\uC138\uC694.`
3587
+ }
3588
+ ],
3589
+ isError: true
3590
+ };
3591
+ }
3592
+ if (action === "kill") {
3593
+ if (job.status === "running") {
3594
+ killTree(job.child, job.detachedGroup);
3595
+ job.status = "killed";
3596
+ job.endedAt = Date.now();
3597
+ }
3598
+ return {
3599
+ content: [
3600
+ { type: "text", text: `\uC791\uC5C5 ${job.id} \uC744(\uB97C) \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4 (\uC0C1\uD0DC: ${job.status}).` }
3601
+ ]
3602
+ };
3603
+ }
3604
+ if (action === "poll" || action === "logs") {
3605
+ const head = Number(a.head) || 0;
3606
+ const tail = Number(a.tail) || (head ? 0 : 200);
3607
+ const maxBytes = Number(a.max_bytes ?? a.maxBytes) || 0;
3608
+ const outP = paginate(job.stdout, { head, tail, maxBytes });
3609
+ const errP = paginate(job.stderr, { head, tail, maxBytes });
3610
+ const dur = Math.round(((job.endedAt ?? Date.now()) - job.startedAt) / 1e3);
3611
+ const header = `job ${job.id} \u2014 ${job.status}` + (job.status !== "running" ? ` (exit ${job.signal ? job.signal : job.code})` : "") + ` pid=${job.pid ?? "?"} ${dur}s`;
3612
+ const parts = [header];
3613
+ if (job.errorMsg) parts.push(`ERROR: ${job.errorMsg}`);
3614
+ parts.push(
3615
+ `--- stdout${outP.truncated ? ` (last, ${outP.totalBytes}B total)` : ""} ---
3616
+ ${outP.text || "(none)"}`
3617
+ );
3618
+ if (errP.text.trim() || errP.totalBytes) {
3619
+ parts.push(
3620
+ `--- stderr${errP.truncated ? ` (last, ${errP.totalBytes}B total)` : ""} ---
3621
+ ${errP.text || "(none)"}`
3622
+ );
3623
+ }
3624
+ return {
3625
+ content: [{ type: "text", text: parts.join("\n\n") }],
3626
+ isError: job.status === "error"
3627
+ };
3628
+ }
3629
+ return {
3630
+ content: [
3631
+ {
3632
+ type: "text",
3633
+ text: `\uC54C \uC218 \uC5C6\uB294 action '${action}'. list | poll | kill \uC911 \uD558\uB098\uB97C \uC4F0\uC138\uC694.`
3634
+ }
3635
+ ],
3636
+ isError: true
3637
+ };
3638
+ }
3639
+ };
3640
+ var _provider = null;
3641
+ function getLocalToolProvider() {
3642
+ if (!_provider) _provider = new LocalToolProvider();
3643
+ return _provider;
3644
+ }
3645
+
3646
+ // ../../packages/engine/src/mcp-runtime-log.ts
3647
+ var MAX_ENTRIES = 200;
3648
+ var entries = [];
3649
+ var listeners = /* @__PURE__ */ new Set();
3650
+ var sequence = 0;
3651
+ function appendMcpRuntimeLog(entry) {
3652
+ const next = {
3653
+ ...entry,
3654
+ id: ++sequence,
3655
+ timestamp: Date.now()
3656
+ };
3657
+ entries.push(next);
3658
+ if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES);
3659
+ for (const listener of listeners) {
3660
+ try {
3661
+ listener(next);
3662
+ } catch {
3663
+ }
3664
+ }
3665
+ return next;
3666
+ }
3667
+
3668
+ // ../../packages/engine/src/connection-security.ts
3669
+ function xgenWebSocketTlsOptions(enabled) {
3670
+ return { rejectUnauthorized: !enabled };
3671
+ }
3672
+
3673
+ // ../../packages/engine/src/mcp-bridge.ts
3674
+ var HEARTBEAT_MS = 2e4;
3675
+ var RECONNECT_MIN_MS = 5e3;
3676
+ var RECONNECT_MAX_MS = 6e4;
3677
+ var SETTLE_MS = 1200;
3678
+ var GRACE_MS = 4e3;
3679
+ var McpBridge = class {
3680
+ ws = null;
3681
+ hb = null;
3682
+ retry = null;
3683
+ settle = null;
3684
+ grace = null;
3685
+ backoff = RECONNECT_MIN_MS;
3686
+ stopped = true;
3687
+ /** Debounced UI state — NOT the raw socket state, to avoid flicker. */
3688
+ uiConnected = false;
3689
+ catalogSynced = false;
3690
+ serverToolCount = 0;
3691
+ catalogSeq = 0;
3692
+ pendingCatalogId = "";
3693
+ serverUrl = "";
3694
+ userId = "";
3695
+ allowPrivateCertificate = false;
3696
+ getToken = async () => null;
3697
+ refreshAuth = async () => null;
3698
+ lastServers = [];
3699
+ lastError;
3700
+ lastEmit = "";
3701
+ onStatus = () => {
3702
+ };
3703
+ setStatusListener(cb) {
3704
+ this.onStatus = cb;
3705
+ }
3706
+ status() {
3707
+ const localTools = getLocalToolProvider().advertise();
3708
+ const localServer = localTools.length ? [{ name: LOCAL_SERVER, connected: true, tools: localTools }] : [];
3709
+ return {
3710
+ enabled: !this.stopped,
3711
+ connected: this.uiConnected,
3712
+ catalogSynced: this.catalogSynced,
3713
+ serverToolCount: this.serverToolCount,
3714
+ error: this.lastError,
3715
+ servers: [...localServer, ...this.lastServers]
3716
+ };
3717
+ }
3718
+ /** Emit only when the status actually changed (dedupe). */
3719
+ emit() {
3720
+ const s = this.status();
3721
+ const key = JSON.stringify({
3722
+ e: s.enabled,
3723
+ c: s.connected,
3724
+ sync: s.catalogSynced,
3725
+ tools: s.serverToolCount,
3726
+ err: s.error,
3727
+ n: s.servers.map((x) => [x.name, x.connected, x.tools.length])
3728
+ });
3729
+ if (key === this.lastEmit) return;
3730
+ this.lastEmit = key;
3731
+ this.onStatus(s);
3732
+ }
3733
+ start(opts) {
3734
+ const sameTarget = this.serverUrl === opts.serverUrl && this.userId === opts.userId && this.allowPrivateCertificate === opts.allowPrivateCertificate;
3735
+ this.serverUrl = opts.serverUrl;
3736
+ this.userId = opts.userId;
3737
+ this.allowPrivateCertificate = opts.allowPrivateCertificate;
3738
+ this.getToken = opts.getToken;
3739
+ if (opts.refreshAuth) this.refreshAuth = opts.refreshAuth;
3740
+ if (!this.stopped && sameTarget && (this.ws || this.retry)) {
3741
+ void this.refreshCatalog();
3742
+ return;
3743
+ }
3744
+ this.stopped = false;
3745
+ this.backoff = RECONNECT_MIN_MS;
3746
+ this.reconnect(true);
3747
+ }
3748
+ stop() {
3749
+ this.stopped = true;
3750
+ this.clearTimers();
3751
+ try {
3752
+ const ws = this.ws;
3753
+ if (ws) {
3754
+ ws.removeAllListeners();
3755
+ ws.on("error", () => void 0);
3756
+ ws.close();
3757
+ }
3758
+ } catch {
3759
+ }
3760
+ this.ws = null;
3761
+ this.uiConnected = false;
3762
+ this.catalogSynced = false;
3763
+ this.serverToolCount = 0;
3764
+ this.pendingCatalogId = "";
3765
+ this.lastError = void 0;
3766
+ this.emit();
3767
+ }
3768
+ clearTimers() {
3769
+ for (const t of [this.retry, this.hb, this.settle, this.grace]) if (t) clearTimeout(t);
3770
+ this.retry = this.hb = this.settle = this.grace = null;
3771
+ }
3772
+ /**
3773
+ * 서버들에 다시 붙어 카탈로그·상태를 갱신한다.
3774
+ *
3775
+ * 소켓이 열려 있지 않아도 재광고한다 — 한 번 실패한 서버의 오류 문구가
3776
+ * 소켓 이벤트가 있을 때까지 화면에 그대로 남아 있던 문제(사용자가 uv 를
3777
+ * 나중에 설치한 경우)를 여기서 끊는다. sendHello() 는 열려 있을 때만
3778
+ * 실제로 전송하고, 상태는 항상 emit 한다.
3779
+ */
3780
+ /**
3781
+ * 카탈로그가 서버에 반영될 때까지 기다린다 (헤드리스 실행용).
3782
+ *
3783
+ * CLI 는 한 번 물어보고 끝나는 명령이 많다 — 브릿지가 붙기 전에 채팅을 시작하면
3784
+ * 에이전트에게 로컬 도구가 없는 채로 첫 턴이 돈다. 데스크톱은 창이 떠 있으니
3785
+ * 상태 표시로 충분하지만 CLI 에는 기다릴 자리가 필요하다.
3786
+ *
3787
+ * 타임아웃은 실패가 아니다 — 현재 상태를 그대로 돌려주고, 부를 쪽이 판단한다.
3788
+ */
3789
+ async waitUntilReady(timeoutMs = 3e3) {
3790
+ if (this.status().catalogSynced) return this.status();
3791
+ return new Promise((resolve) => {
3792
+ const previous = this.onStatus;
3793
+ let done = false;
3794
+ const finish = (s) => {
3795
+ if (done) return;
3796
+ done = true;
3797
+ this.setStatusListener(previous);
3798
+ resolve(s);
3799
+ };
3800
+ const timer = setTimeout(() => finish(this.status()), Math.max(0, timeoutMs));
3801
+ this.setStatusListener((s) => {
3802
+ previous(s);
3803
+ if (!s.catalogSynced) return;
3804
+ clearTimeout(timer);
3805
+ finish(s);
3806
+ });
3807
+ });
3808
+ }
3809
+ async refreshCatalog() {
3810
+ await this.sendHello();
3811
+ }
3812
+ wsUrl() {
3813
+ const base = this.serverUrl.replace(/\/+$/, "").replace(/^http/, "ws");
3814
+ return `${base}/api/tools/ws/connector-mcp/${encodeURIComponent(this.userId)}`;
3815
+ }
3816
+ scheduleRetry() {
3817
+ if (this.stopped || this.retry) return;
3818
+ const delay = this.backoff;
3819
+ this.backoff = Math.min(RECONNECT_MAX_MS, Math.round(this.backoff * 1.8));
3820
+ this.retry = setTimeout(() => {
3821
+ this.retry = null;
3822
+ void this.reconnect(false);
3823
+ }, delay);
3824
+ }
3825
+ async reconnect(immediate) {
3826
+ if (this.stopped) return;
3827
+ if (immediate && this.retry) {
3828
+ clearTimeout(this.retry);
3829
+ this.retry = null;
3830
+ }
3831
+ if (!immediate && this.retry) return;
3832
+ if (this.settle) {
3833
+ clearTimeout(this.settle);
3834
+ this.settle = null;
3835
+ }
3836
+ if (this.hb) {
3837
+ clearInterval(this.hb);
3838
+ this.hb = null;
3839
+ }
3840
+ try {
3841
+ const ws2 = this.ws;
3842
+ if (ws2) {
3843
+ ws2.removeAllListeners();
3844
+ ws2.on("error", () => void 0);
3845
+ ws2.close();
3846
+ }
3847
+ } catch {
3848
+ }
3849
+ this.ws = null;
3850
+ const token = await this.getToken();
3851
+ if (this.stopped || !this.serverUrl || !this.userId) {
3852
+ if (!this.stopped) this.scheduleRetry();
3853
+ return;
3854
+ }
3855
+ let ws;
3856
+ try {
3857
+ ws = new WebSocket(this.wsUrl(), {
3858
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0,
3859
+ ...xgenWebSocketTlsOptions(this.allowPrivateCertificate)
3860
+ });
3861
+ } catch (e) {
3862
+ this.lastError = e instanceof Error ? e.message : String(e);
3863
+ this.emit();
3864
+ this.scheduleRetry();
3865
+ return;
3866
+ }
3867
+ this.ws = ws;
3868
+ ws.on("unexpected-response", (_req, res) => {
3869
+ const sc = res?.statusCode ?? 0;
3870
+ try {
3871
+ res?.resume?.();
3872
+ } catch {
3873
+ }
3874
+ this.lastError = `handshake HTTP ${sc}`;
3875
+ const heal = sc === 401 || sc === 403 ? Promise.resolve(this.refreshAuth()).catch(() => null) : Promise.resolve(null);
3876
+ void heal.then((fresh) => {
3877
+ if (this.ws === ws) this.ws = null;
3878
+ try {
3879
+ ws.removeAllListeners();
3880
+ ws.close();
3881
+ } catch {
3882
+ }
3883
+ if (fresh) this.backoff = RECONNECT_MIN_MS;
3884
+ this.emit();
3885
+ this.scheduleRetry();
3886
+ });
3887
+ });
3888
+ ws.on("open", () => {
3889
+ if (this.grace) {
3890
+ clearTimeout(this.grace);
3891
+ this.grace = null;
3892
+ }
3893
+ this.lastError = void 0;
3894
+ void this.sendHello();
3895
+ if (this.hb) clearInterval(this.hb);
3896
+ this.hb = setInterval(() => {
3897
+ try {
3898
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
3899
+ } catch {
3900
+ }
3901
+ }, HEARTBEAT_MS);
3902
+ if (this.settle) clearTimeout(this.settle);
3903
+ this.settle = setTimeout(() => {
3904
+ this.settle = null;
3905
+ if (this.ws === ws && ws.readyState === WebSocket.OPEN) {
3906
+ this.uiConnected = true;
3907
+ this.backoff = RECONNECT_MIN_MS;
3908
+ this.emit();
3909
+ }
3910
+ }, SETTLE_MS);
3911
+ });
3912
+ ws.on("message", (raw) => void this.onMessage(String(raw)));
3913
+ ws.on("close", () => {
3914
+ if (this.settle) {
3915
+ clearTimeout(this.settle);
3916
+ this.settle = null;
3917
+ }
3918
+ if (this.hb) {
3919
+ clearInterval(this.hb);
3920
+ this.hb = null;
3921
+ }
3922
+ if (this.stopped) return;
3923
+ this.catalogSynced = false;
3924
+ this.serverToolCount = 0;
3925
+ this.pendingCatalogId = "";
3926
+ if (this.uiConnected && !this.grace) {
3927
+ this.grace = setTimeout(() => {
3928
+ this.grace = null;
3929
+ this.uiConnected = false;
3930
+ this.emit();
3931
+ }, GRACE_MS);
3932
+ }
3933
+ this.scheduleRetry();
3934
+ });
3935
+ ws.on("error", (e) => {
3936
+ this.lastError = e?.message;
3937
+ try {
3938
+ ws.close();
3939
+ } catch {
3940
+ }
3941
+ });
3942
+ }
3943
+ async sendHello() {
3944
+ try {
3945
+ const adverts = await getMcpManager().advertise();
3946
+ this.lastServers = adverts;
3947
+ const tools = adverts.filter((a) => a.connected).flatMap(
3948
+ (a) => a.tools.map((t) => ({
3949
+ server: a.name,
3950
+ name: t.name,
3951
+ description: t.description,
3952
+ inputSchema: t.inputSchema
3953
+ }))
3954
+ );
3955
+ const builtins = getLocalToolProvider().advertise().map((t) => ({
3956
+ server: LOCAL_SERVER,
3957
+ name: t.name,
3958
+ description: t.description,
3959
+ inputSchema: t.inputSchema
3960
+ }));
3961
+ tools.unshift(...builtins);
3962
+ if (this.ws?.readyState === WebSocket.OPEN) {
3963
+ const catalogId = `${Date.now()}-${++this.catalogSeq}`;
3964
+ this.pendingCatalogId = catalogId;
3965
+ this.catalogSynced = false;
3966
+ this.serverToolCount = 0;
3967
+ this.ws.send(JSON.stringify({ type: "hello", catalog_id: catalogId, tools }));
3968
+ appendMcpRuntimeLog({
3969
+ kind: "catalog",
3970
+ message: `\uB3C4\uAD6C \uCE74\uD0C8\uB85C\uADF8 ${tools.length}\uAC1C \uC7AC\uCD08\uAE30\uD654 \uC694\uCCAD`,
3971
+ requestId: catalogId
3972
+ });
3973
+ }
3974
+ this.emit();
3975
+ } catch (e) {
3976
+ this.lastError = e instanceof Error ? e.message : String(e);
3977
+ this.emit();
3978
+ }
3979
+ }
3980
+ async onMessage(text) {
3981
+ let msg;
3982
+ try {
3983
+ msg = JSON.parse(text);
3984
+ } catch {
3985
+ return;
3986
+ }
3987
+ if (msg.type === "ready") {
3988
+ if (msg.catalog_id !== this.pendingCatalogId) return;
3989
+ this.catalogSynced = true;
3990
+ this.serverToolCount = Number.isFinite(msg.tool_count) ? Math.max(0, Math.trunc(msg.tool_count)) : 0;
3991
+ appendMcpRuntimeLog({
3992
+ kind: "catalog",
3993
+ message: `workflow \uB3C4\uAD6C ${this.serverToolCount}\uAC1C \uC801\uC6A9 \uC644\uB8CC`,
3994
+ requestId: msg.catalog_id,
3995
+ ok: true
3996
+ });
3997
+ this.emit();
3998
+ return;
3999
+ }
4000
+ if (msg.type === "mcp_call") {
4001
+ const { request_id, server, tool, args } = msg;
4002
+ const context = localToolCallContext(msg.context);
4003
+ const startedAt = Date.now();
4004
+ appendMcpRuntimeLog({
4005
+ kind: "call",
4006
+ message: "\uB85C\uCEEC MCP \uB3C4\uAD6C \uD638\uCD9C \uC218\uC2E0",
4007
+ requestId: request_id,
4008
+ server: String(server),
4009
+ tool: String(tool)
4010
+ });
4011
+ let payload;
4012
+ try {
4013
+ const local = getLocalToolProvider();
4014
+ const result = local.owns(String(server)) ? await local.callTool(String(tool), args ?? {}, context) : await getMcpManager().callTool(String(server), String(tool), args ?? {});
4015
+ payload = { request_id, ok: true, result };
4016
+ } catch (e) {
4017
+ payload = { request_id, ok: false, error: e instanceof Error ? e.message : String(e) };
4018
+ }
4019
+ appendMcpRuntimeLog({
4020
+ kind: "result",
4021
+ message: payload.ok ? "\uB85C\uCEEC MCP \uB3C4\uAD6C \uC2E4\uD589 \uC131\uACF5" : String(payload.error || "\uB85C\uCEEC MCP \uB3C4\uAD6C \uC2E4\uD589 \uC2E4\uD328"),
4022
+ requestId: request_id,
4023
+ server: String(server),
4024
+ tool: String(tool),
4025
+ ok: payload.ok === true,
4026
+ durationMs: Date.now() - startedAt
4027
+ });
4028
+ try {
4029
+ this.ws?.send(JSON.stringify({ type: "mcp_result", ...payload }));
4030
+ } catch {
4031
+ }
4032
+ }
4033
+ }
4034
+ };
4035
+ var _bridge = null;
4036
+ function getMcpBridge() {
4037
+ if (!_bridge) _bridge = new McpBridge();
4038
+ return _bridge;
4039
+ }
4040
+
4041
+ // ../../packages/engine/src/dex-engine.ts
4042
+ var DexEngine = class {
4043
+ constructor(configs, credentials, options = {}) {
4044
+ this.configs = configs;
4045
+ this.credentials = credentials;
4046
+ this.localTools = options.localToolProvider ?? getLocalToolProvider();
4047
+ this.localToolBridge = options.localToolBridge ?? getMcpBridge();
4048
+ }
4049
+ clients = /* @__PURE__ */ new Map();
4050
+ localTools;
4051
+ localToolBridge;
4052
+ async listProfiles() {
4053
+ const config = await this.configs.read();
4054
+ return Object.entries(config.profiles).map(([name, profile]) => ({ name, ...profile, current: name === config.currentProfile })).sort((a, b) => a.name.localeCompare(b.name));
4055
+ }
4056
+ onLocalToolsStatus(listener) {
4057
+ this.localToolBridge.setStatusListener(listener);
4058
+ return () => this.localToolBridge.setStatusListener(() => void 0);
4059
+ }
4060
+ /**
4061
+ * 설정을 도구 제공자에 반영한다.
4062
+ *
4063
+ * `allowDangerous` 는 여기서 승인 포트로 바뀐다 — 물을 사람이 없는 실행에서
4064
+ * "설정으로 미리 승인했다"를 표현하는 유일한 자리다. 이미 호스트가 붙어 있으면
4065
+ * 그 포트를 유지한 채 승인 답만 덮는다(데스크톱에서 물을 수 있는 능력을
4066
+ * 설정 하나로 잃지 않게).
4067
+ */
4068
+ applyToolConfig(config) {
4069
+ this.localTools.configure(toShellConfig(config));
4070
+ const preApproved = dangerousApprovalFromConfig(config);
4071
+ if (!preApproved) return;
4072
+ const current = isHostBound() ? hostPorts() : null;
4073
+ if (!current) return;
4074
+ bindHost({
4075
+ ...current,
4076
+ interaction: { ...current.interaction ?? {}, confirmDangerous: preApproved }
4077
+ });
4078
+ }
4079
+ async localToolsStatus() {
4080
+ const config = normalizeLocalToolsConfig((await this.configs.read()).localTools);
4081
+ this.applyToolConfig(config);
4082
+ const tools = this.localTools.advertise();
4083
+ const bridge = this.localToolBridge.status();
4084
+ return { config, tools, catalog: this.localTools.catalog(), bridge };
4085
+ }
4086
+ async configureLocalTools(patch) {
4087
+ const config = await this.configs.read();
4088
+ const current = normalizeLocalToolsConfig(config.localTools);
4089
+ config.localTools = normalizeLocalToolsConfig({
4090
+ ...current,
4091
+ ...patch,
4092
+ allowedRoots: patch.allowedRoots ?? current.allowedRoots,
4093
+ blockedCommands: patch.blockedCommands ?? current.blockedCommands
4094
+ });
4095
+ await this.configs.write(config);
4096
+ this.applyToolConfig(config.localTools);
4097
+ if (!config.localTools.enabled) this.localToolBridge.stop();
4098
+ else this.localToolBridge.refreshCatalog();
4099
+ return this.localToolsStatus();
4100
+ }
4101
+ async runLocalTool(tool, args) {
4102
+ const config = normalizeLocalToolsConfig((await this.configs.read()).localTools);
4103
+ this.applyToolConfig(config);
4104
+ return this.localTools.callTool(tool, args);
4105
+ }
4106
+ async startLocalTools(requestedProfile, waitMs = 0) {
4107
+ const config = normalizeLocalToolsConfig((await this.configs.read()).localTools);
4108
+ this.applyToolConfig(config);
4109
+ if (!config.enabled) {
4110
+ this.localToolBridge.stop();
4111
+ return this.localToolsStatus();
4112
+ }
4113
+ const record = await this.authenticatedRecord(requestedProfile);
4114
+ const userId = record.client.user?.userId?.trim();
4115
+ if (!userId) throw new DexError("auth_invalid", "\uB85C\uCEEC \uB3C4\uAD6C \uC5F0\uACB0\uC5D0 \uD544\uC694\uD55C \uC0AC\uC6A9\uC790 ID\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
4116
+ this.localToolBridge.start({
4117
+ serverUrl: record.serverUrl,
4118
+ userId,
4119
+ // CLI 는 사내 인증서를 아직 설정으로 받지 않는다 — 기본은 검증이다.
4120
+ allowPrivateCertificate: false,
4121
+ getToken: async () => {
4122
+ await this.flush(record);
4123
+ return record.client.getAccessTokenAfterRotation() || (await this.credentials.get(record.profile))?.accessToken || null;
4124
+ },
4125
+ refreshAuth: async () => {
4126
+ const session = await this.credentials.get(record.profile);
4127
+ const token = await record.client.ensureFreshAuth(session?.refreshToken);
4128
+ await this.flush(record);
4129
+ return token;
4130
+ }
4131
+ });
4132
+ if (waitMs > 0) await this.localToolBridge.waitUntilReady(waitMs);
4133
+ return this.localToolsStatus();
4134
+ }
4135
+ stopLocalTools() {
4136
+ this.localToolBridge.stop();
4137
+ }
4138
+ // ── SSH ───────────────────────────────────────────────────────────
4139
+ //
4140
+ // 개인 SSH 서버 목록은 XGEN 계정에 있고 접속은 서버가 연다 — 이 기기에서
4141
+ // 닿는지는 에이전트에게 아무 의미가 없다. 그래서 여기는 얇은 통과 계층이고,
4142
+ // 검증(이름 규칙 · 점프 그래프 · 자격증명 유무)은 전부 서버가 한다.
4143
+ //
4144
+ // 비밀번호와 개인키는 응답에 실리지 않는다. 쓰기는 부분 수정이라 보내지 않은
4145
+ // 자격증명은 유지되고 빈 문자열로만 지워진다 — 설명만 고치려던 저장이 접속을
4146
+ // 끊으면 안 된다.
4147
+ async sshConfig(profile) {
4148
+ const record = await this.authenticatedRecord(profile);
4149
+ return record.client.ssh.getConfig();
4150
+ }
4151
+ async setSshEnabled(enabled, profile) {
4152
+ const record = await this.authenticatedRecord(profile);
4153
+ return record.client.ssh.setEnabled(enabled);
4154
+ }
4155
+ async createSshServer(input, profile) {
4156
+ const record = await this.authenticatedRecord(profile);
4157
+ return record.client.ssh.createServer(input);
4158
+ }
4159
+ async updateSshServer(name, input, profile) {
4160
+ const record = await this.authenticatedRecord(profile);
4161
+ return record.client.ssh.updateServer(name, input);
4162
+ }
4163
+ async deleteSshServer(name, profile) {
4164
+ const record = await this.authenticatedRecord(profile);
4165
+ return record.client.ssh.deleteServer(name);
4166
+ }
4167
+ /** 서버가 점프 경로를 그대로 타고 실제로 접속해 본다. 마스터 스위치와 무관하다 —
4168
+ * 켜기 전에 맞는지 확인할 수 있어야 한다. */
4169
+ async testSshServer(name, profile) {
4170
+ const record = await this.authenticatedRecord(profile);
4171
+ return record.client.ssh.testServer(name);
4172
+ }
4173
+ async setProfile(nameInput, serverUrlInput) {
4174
+ const name = validateProfileName(nameInput);
4175
+ const serverUrl = validateServerUrl(serverUrlInput);
4176
+ const config = await this.configs.read();
4177
+ const previous = config.profiles[name];
4178
+ config.profiles[name] = { serverUrl };
4179
+ if (!config.profiles[config.currentProfile]) config.currentProfile = name;
4180
+ await this.configs.write(config);
4181
+ this.clients.delete(name);
4182
+ if (name === config.currentProfile && previous?.serverUrl !== serverUrl) this.localToolBridge.stop();
4183
+ if (previous && previous.serverUrl !== serverUrl) await this.credentials.delete(name);
4184
+ return { name, serverUrl, current: name === config.currentProfile };
4185
+ }
4186
+ async useProfile(nameInput) {
4187
+ const name = validateProfileName(nameInput);
4188
+ const config = await this.configs.read();
4189
+ const profile = config.profiles[name];
4190
+ if (!profile) throw new DexError("not_found", `\uD504\uB85C\uD544\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${name}`);
4191
+ config.currentProfile = name;
4192
+ await this.configs.write(config);
4193
+ this.localToolBridge.stop();
4194
+ return { name, ...profile, current: true };
4195
+ }
4196
+ async login(email, password, requestedProfile) {
4197
+ if (!email.trim() || !password) throw new DexError("usage_error", "\uC774\uBA54\uC77C\uACFC \uBE44\uBC00\uBC88\uD638\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
4198
+ const { name, profile } = await this.resolveProfile(requestedProfile);
4199
+ this.clients.delete(name);
4200
+ const record = this.createClient(name, profile.serverUrl);
4201
+ const result = await record.client.login(email.trim(), password);
4202
+ const session = {
4203
+ serverUrl: profile.serverUrl,
4204
+ accessToken: result.accessToken,
4205
+ refreshToken: result.refreshToken
4206
+ };
4207
+ await this.credentials.set(name, session);
4208
+ await this.flush(record);
4209
+ record.authenticated = true;
4210
+ return {
4211
+ profile: name,
4212
+ serverUrl: profile.serverUrl,
4213
+ authenticated: true,
4214
+ user: record.client.user ?? void 0
4215
+ };
4216
+ }
4217
+ async authStatus(requestedProfile) {
4218
+ const { name, profile } = await this.resolveProfile(requestedProfile);
4219
+ const session = await this.credentials.get(name);
4220
+ if (!session || session.serverUrl !== profile.serverUrl) {
4221
+ return {
4222
+ profile: name,
4223
+ serverUrl: profile.serverUrl,
4224
+ authenticated: false,
4225
+ reason: "missing_session"
4226
+ };
4227
+ }
4228
+ try {
4229
+ const record = await this.ensureAuthenticated(name, profile.serverUrl, session);
4230
+ return {
4231
+ profile: name,
4232
+ serverUrl: profile.serverUrl,
4233
+ authenticated: true,
4234
+ user: record.client.user ?? void 0
4235
+ };
4236
+ } catch (error) {
4237
+ if (error instanceof DexError && error.code === "network_error") {
4238
+ return {
4239
+ profile: name,
4240
+ serverUrl: profile.serverUrl,
4241
+ authenticated: false,
4242
+ reason: "network"
4243
+ };
4244
+ }
4245
+ if (error instanceof DexError && (error.code === "auth_required" || error.code === "auth_invalid")) {
4246
+ return {
4247
+ profile: name,
4248
+ serverUrl: profile.serverUrl,
4249
+ authenticated: false,
4250
+ reason: "invalid_session"
4251
+ };
4252
+ }
4253
+ throw error;
4254
+ }
4255
+ }
4256
+ async logout(requestedProfile) {
4257
+ const { name, profile } = await this.resolveProfile(requestedProfile);
4258
+ const session = await this.credentials.get(name);
4259
+ if (session?.serverUrl === profile.serverUrl) {
4260
+ const record = this.createClient(name, profile.serverUrl);
4261
+ record.client.setTokens(session.accessToken, session.refreshToken);
4262
+ await record.client.logout().catch(() => {
4263
+ });
4264
+ }
4265
+ await this.credentials.delete(name);
4266
+ this.clients.delete(name);
4267
+ this.localToolBridge.stop();
4268
+ }
4269
+ async listAgents(query = {}, requestedProfile) {
4270
+ return this.withAuthRetry(requestedProfile, (client) => client.agents.list(query));
4271
+ }
4272
+ async listConversations(requestedProfile) {
4273
+ return this.withAuthRetry(requestedProfile, (client) => client.history.conversations());
4274
+ }
4275
+ async historyTurns(workflowId, interactionId, workflowName, requestedProfile) {
4276
+ if (!workflowId || !interactionId) {
4277
+ throw new DexError("usage_error", "workflowId\uC640 interactionId\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
4278
+ }
4279
+ return this.withAuthRetry(
4280
+ requestedProfile,
4281
+ (client) => client.history.turns(workflowId, interactionId, workflowName)
4282
+ );
4283
+ }
4284
+ async resolveChatInput(input) {
4285
+ const workflowId = input.workflowId.trim();
4286
+ if (!workflowId) throw new DexError("usage_error", "Agent workflow ID\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
4287
+ const { name } = await this.resolveProfile(input.profile);
4288
+ const workflowName = input.workflowName?.trim() || (await this.findAgent(workflowId, name)).workflowName;
4289
+ return {
4290
+ profile: name,
4291
+ workflowId,
4292
+ workflowName,
4293
+ input: input.input,
4294
+ interactionId: input.interactionId?.trim() || randomUUID()
4295
+ };
4296
+ }
4297
+ async *chat(input, signal) {
4298
+ const resolved = await this.resolveChatInput(input);
4299
+ let record = await this.authenticatedRecord(resolved.profile);
4300
+ let emitted = false;
4301
+ try {
4302
+ const local = await this.startLocalTools(resolved.profile, 3e3);
4303
+ if (local.config.enabled) {
4304
+ yield local.bridge.catalogSynced ? {
4305
+ kind: "status",
4306
+ surface: "connector_local",
4307
+ detail: `\uB85C\uCEEC \uB3C4\uAD6C ${local.bridge.serverToolCount || local.tools.length}\uAC1C \uC5F0\uACB0\uB428`
4308
+ } : {
4309
+ kind: "status",
4310
+ surface: "connector_local",
4311
+ detail: local.bridge.error || "\uB85C\uCEEC \uB3C4\uAD6C \uCE74\uD0C8\uB85C\uADF8 \uC5F0\uACB0 \uB300\uAE30 \uC911",
4312
+ reason: "bridge_not_ready"
4313
+ };
4314
+ }
4315
+ } catch (error) {
4316
+ yield {
4317
+ kind: "status",
4318
+ surface: "connector_local",
4319
+ detail: `\uB85C\uCEEC \uB3C4\uAD6C \uC5F0\uACB0 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`,
4320
+ reason: "bridge_error"
4321
+ };
4322
+ }
4323
+ for (let attempt = 0; attempt < 2; attempt += 1) {
4324
+ try {
4325
+ for await (const event of record.client.chat.stream(
4326
+ {
4327
+ workflowId: resolved.workflowId,
4328
+ workflowName: resolved.workflowName,
4329
+ input: resolved.input,
4330
+ interactionId: resolved.interactionId
4331
+ },
4332
+ signal
4333
+ )) {
4334
+ emitted = true;
4335
+ yield event;
4336
+ }
4337
+ return resolved;
4338
+ } catch (error) {
4339
+ if (attempt === 0 && !emitted && isUnauthorized(error)) {
4340
+ await this.refresh(record);
4341
+ record = await this.authenticatedRecord(resolved.profile);
4342
+ continue;
4343
+ }
4344
+ throw error;
4345
+ }
4346
+ }
4347
+ return resolved;
4348
+ }
4349
+ async findAgent(selector, profile) {
4350
+ const agents = await this.withAuthRetry(profile, (client) => client.agents.listAll({}, 100));
4351
+ const normalized = selector.trim().toLocaleLowerCase();
4352
+ const found = agents.find((agent) => agent.workflowId === selector.trim()) ?? agents.find((agent) => agent.workflowName.toLocaleLowerCase() === normalized);
4353
+ if (!found) throw new DexError("not_found", `Agent\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${selector}`);
4354
+ return found;
4355
+ }
4356
+ async withAuthRetry(requestedProfile, operation) {
4357
+ const record = await this.authenticatedRecord(requestedProfile);
4358
+ try {
4359
+ return await operation(record.client);
4360
+ } catch (error) {
4361
+ if (!isUnauthorized(error)) throw error;
4362
+ await this.refresh(record);
4363
+ return operation(record.client);
4364
+ }
4365
+ }
4366
+ async authenticatedRecord(requestedProfile) {
4367
+ const { name, profile } = await this.resolveProfile(requestedProfile);
4368
+ const session = await this.credentials.get(name);
4369
+ if (!session || session.serverUrl !== profile.serverUrl) {
4370
+ throw new DexError("auth_required", `\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4: dex login --profile ${name}`);
4371
+ }
4372
+ return this.ensureAuthenticated(name, profile.serverUrl, session);
4373
+ }
4374
+ async ensureAuthenticated(name, serverUrl, session) {
4375
+ const cached = this.clients.get(name);
4376
+ if (cached?.authenticated && cached.serverUrl === serverUrl) return cached;
4377
+ const record = cached?.serverUrl === serverUrl ? cached : this.createClient(name, serverUrl);
4378
+ const state = await record.client.restoreDetailed(session.accessToken, session.refreshToken);
4379
+ await this.flush(record);
4380
+ if (state === "valid") {
4381
+ record.authenticated = true;
4382
+ return record;
4383
+ }
4384
+ if (state === "invalid") {
4385
+ await this.credentials.delete(name);
4386
+ this.clients.delete(name);
4387
+ throw new DexError("auth_invalid", `\uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4: dex login --profile ${name}`);
4388
+ }
4389
+ throw new DexError("network_error", `XGEN \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${serverUrl}`);
4390
+ }
4391
+ createClient(profile, serverUrl) {
4392
+ const existing = this.clients.get(profile);
4393
+ if (existing?.serverUrl === serverUrl) return existing;
4394
+ const record = {
4395
+ profile,
4396
+ serverUrl,
4397
+ authenticated: false,
4398
+ persisting: Promise.resolve()
4399
+ };
4400
+ record.client = new XgenClient({
4401
+ baseUrl: serverUrl,
4402
+ onTokensRotated: (accessToken, refreshToken) => {
4403
+ record.persisting = record.persisting.then(
4404
+ () => this.credentials.set(profile, { serverUrl, accessToken, refreshToken })
4405
+ );
4406
+ }
4407
+ });
4408
+ this.clients.set(profile, record);
4409
+ return record;
4410
+ }
4411
+ async refresh(record) {
4412
+ const session = await this.credentials.get(record.profile);
4413
+ const accessToken = await record.client.ensureFreshAuth(session?.refreshToken);
4414
+ await this.flush(record);
4415
+ if (!accessToken) {
4416
+ await this.credentials.delete(record.profile);
4417
+ record.authenticated = false;
4418
+ throw new DexError("auth_invalid", `\uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4: dex login --profile ${record.profile}`);
4419
+ }
4420
+ record.authenticated = true;
4421
+ }
4422
+ async flush(record) {
4423
+ await record.persisting;
4424
+ }
4425
+ async resolveProfile(requested) {
4426
+ const config = await this.configs.read();
4427
+ const name = requested ? validateProfileName(requested) : config.currentProfile;
4428
+ const profile = config.profiles[name];
4429
+ if (!profile) {
4430
+ throw new DexError(
4431
+ "config_invalid",
4432
+ `XGEN \uC11C\uBC84 \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694: dex profile set ${name} --server <URL>`
4433
+ );
4434
+ }
4435
+ return { name, profile };
4436
+ }
4437
+ };
4438
+
4439
+ // ../../packages/engine/src/credential-store.ts
4440
+ var SERVICE = "xgen-dex-cli";
4441
+ var ACCOUNT_PREFIX = "profile:";
4442
+ async function loadKeytar() {
4443
+ try {
4444
+ const loaded = await import("keytar");
4445
+ const keytar = loaded.default ?? loaded;
4446
+ if (!keytar.getPassword || !keytar.setPassword || !keytar.deletePassword) throw new Error("invalid keytar module");
4447
+ return keytar;
4448
+ } catch (error) {
4449
+ throw new DexError(
4450
+ "credential_store_unavailable",
4451
+ "OS \uD0A4\uCCB4\uC778\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. keytar \uC124\uCE58\uC640 OS \uD0A4\uB9C1 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
4452
+ error
4453
+ );
4454
+ }
4455
+ }
4456
+ function parseSession(raw) {
4457
+ if (!raw) return null;
4458
+ try {
4459
+ const value = JSON.parse(raw);
4460
+ if (!value.serverUrl || !value.accessToken) return null;
4461
+ return value;
4462
+ } catch {
4463
+ return null;
4464
+ }
4465
+ }
4466
+ var KeytarCredentialStore = class {
4467
+ /**
4468
+ * 엔진의 `SecretPort` 로 내보내는 두 함수 — 프로파일 세션 말고 **임의의 비밀**
4469
+ * (MCP 서버 시크릿 · OAuth 상태)을 같은 백엔드에 둔다.
4470
+ *
4471
+ * 저장소를 나누지 않는 이유: 사용자에게는 "이 앱이 내 키체인에 무엇을 넣었나"가
4472
+ * 하나의 질문이고, 두 곳에 나뉘면 로그아웃할 때 한쪽이 남는다.
4473
+ */
4474
+ async getRaw(name) {
4475
+ const keytar = await loadKeytar();
4476
+ return keytar.getPassword(SERVICE, name);
4477
+ }
4478
+ async setRaw(name, value) {
4479
+ const keytar = await loadKeytar();
4480
+ if (value === null) {
4481
+ await keytar.deletePassword(SERVICE, name);
4482
+ return true;
4483
+ }
4484
+ await keytar.setPassword(SERVICE, name, value);
4485
+ return true;
4486
+ }
4487
+ async get(profile) {
4488
+ const keytar = await loadKeytar();
4489
+ return parseSession(await keytar.getPassword(SERVICE, ACCOUNT_PREFIX + profile));
4490
+ }
4491
+ async set(profile, session) {
4492
+ const keytar = await loadKeytar();
4493
+ await keytar.setPassword(SERVICE, ACCOUNT_PREFIX + profile, JSON.stringify(session));
4494
+ }
4495
+ async delete(profile) {
4496
+ const keytar = await loadKeytar();
4497
+ await keytar.deletePassword(SERVICE, ACCOUNT_PREFIX + profile);
4498
+ }
4499
+ };
4500
+
4501
+ // ../../packages/engine/src/deployment-defaults.ts
4502
+ function optionalBoolean(value, name) {
4503
+ if (value === void 0 || value === "") return void 0;
4504
+ if (value === "true" || value === "1") return true;
4505
+ if (value === "false" || value === "0") return false;
4506
+ throw new Error(`${name}\uC740 true, false, 1, 0 \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
4507
+ }
4508
+ function resolveDeploymentDefaults(input) {
4509
+ const defaults = {};
4510
+ if (input.serverUrl) {
4511
+ const serverUrl = input.serverUrl.trim().replace(/\/+$/, "");
4512
+ const parsed = new URL(serverUrl);
4513
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
4514
+ throw new Error("\uAE30\uBCF8 \uC11C\uBC84 URL\uC740 http \uB610\uB294 https URL\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
4515
+ }
4516
+ defaults.serverUrl = serverUrl;
4517
+ }
4518
+ const allowPrivateCertificate = optionalBoolean(
4519
+ input.allowPrivateCertificate,
4520
+ "XGEN_DEFAULT_ALLOW_PRIVATE_CERTIFICATE"
4521
+ );
4522
+ if (allowPrivateCertificate !== void 0)
4523
+ defaults.allowPrivateCertificate = allowPrivateCertificate;
4524
+ const ssoEnabled = optionalBoolean(input.ssoEnabled, "XGEN_DEFAULT_SSO_ENABLED");
4525
+ if (ssoEnabled !== void 0) defaults.ssoEnabled = ssoEnabled;
4526
+ if (input.ssoPath) {
4527
+ const ssoPath = input.ssoPath.trim();
4528
+ if (!ssoPath.startsWith("/") || ssoPath.startsWith("//")) {
4529
+ throw new Error("\uAE30\uBCF8 SSO PATH\uB294 /\uB85C \uC2DC\uC791\uD558\uB294 \uC0C1\uB300 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4530
+ }
4531
+ defaults.ssoPath = ssoPath;
4532
+ }
4533
+ if (input.updateServer) {
4534
+ if (input.updateServer !== "github" && input.updateServer !== "xgen") {
4535
+ throw new Error("\uAE30\uBCF8 \uC5C5\uB370\uC774\uD2B8 \uC11C\uBC84\uB294 github \uB610\uB294 xgen\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
4536
+ }
4537
+ defaults.updateServer = input.updateServer;
4538
+ }
4539
+ return defaults;
4540
+ }
4541
+ var DEPLOYMENT_DEFAULTS = resolveDeploymentDefaults({
4542
+ serverUrl: process.env.XGEN_DEFAULT_SERVER_URL,
4543
+ allowPrivateCertificate: process.env.XGEN_DEFAULT_ALLOW_PRIVATE_CERTIFICATE,
4544
+ ssoEnabled: process.env.XGEN_DEFAULT_SSO_ENABLED,
4545
+ ssoPath: process.env.XGEN_DEFAULT_SSO_PATH,
4546
+ updateServer: process.env.XGEN_DEFAULT_UPDATE_SERVER
4547
+ });
4548
+
4549
+ export {
4550
+ bindHost,
4551
+ openerInvocation,
4552
+ DANGEROUS_COMMAND_PROMPT,
4553
+ DexError,
4554
+ publicError,
4555
+ dataDirectory,
4556
+ FileConfigStore,
4557
+ DexEngine,
4558
+ KeytarCredentialStore
4559
+ };
4560
+ //# sourceMappingURL=chunk-QXSCKZEG.js.map