@yr-kits/dev-copilot 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/app/index.d.mts +2 -0
  2. package/dist/app/index.mjs +4 -0
  3. package/dist/bin/bridge.mjs +4 -916
  4. package/dist/bin/bridge.mjs.map +1 -1
  5. package/dist/bin/dev-copilot.mjs +13 -3
  6. package/dist/bin/dev-copilot.mjs.map +1 -1
  7. package/dist/bridge/app/index.d.mts +24 -0
  8. package/dist/bridge/app/index.d.mts.map +1 -0
  9. package/dist/bridge/app/index.mjs +3 -0
  10. package/dist/{types/index.d.mts → copilot-D8--qKgC.d.mts} +3 -3
  11. package/dist/copilot-D8--qKgC.d.mts.map +1 -0
  12. package/dist/copilot-overlay-ClRoIHew.mjs +1129 -0
  13. package/dist/copilot-overlay-ClRoIHew.mjs.map +1 -0
  14. package/dist/copilot-overlay-NlUgrMjy.d.mts +7 -0
  15. package/dist/copilot-overlay-NlUgrMjy.d.mts.map +1 -0
  16. package/dist/dev-copilot-context-CA9bqV_U.mjs +35 -0
  17. package/dist/dev-copilot-context-CA9bqV_U.mjs.map +1 -0
  18. package/dist/dev-copilot-provider-CP_gJvWG.d.mts +22 -0
  19. package/dist/dev-copilot-provider-CP_gJvWG.d.mts.map +1 -0
  20. package/dist/dev-copilot-provider-D0_wEEem.mjs +14 -0
  21. package/dist/dev-copilot-provider-D0_wEEem.mjs.map +1 -0
  22. package/dist/index.d.mts +5 -33
  23. package/dist/index.mjs +6 -863
  24. package/dist/run-bridge-cli-DVLGcVgq.mjs +1510 -0
  25. package/dist/run-bridge-cli-DVLGcVgq.mjs.map +1 -0
  26. package/dist/types.d.mts +2 -0
  27. package/dist/widgets/copilot-overlay/index.d.mts +2 -0
  28. package/dist/widgets/copilot-overlay/index.mjs +4 -0
  29. package/package.json +25 -7
  30. package/dist/index.d.mts.map +0 -1
  31. package/dist/index.mjs.map +0 -1
  32. package/dist/types/index.d.mts.map +0 -1
  33. /package/dist/{types/index.mjs → types.mjs} +0 -0
@@ -0,0 +1,1510 @@
1
+ import { createServer } from "node:http";
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { promises } from "node:fs";
4
+ import path, { delimiter, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import os, { homedir, tmpdir } from "node:os";
7
+ import { randomUUID } from "node:crypto";
8
+ import { access, copyFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
9
+
10
+ //#region src/bridge/shared/config/bridge-config.ts
11
+ const DEFAULT_ALLOWED_PATHS = [
12
+ "app",
13
+ "src",
14
+ "widgets",
15
+ "features",
16
+ "entities",
17
+ "shared",
18
+ "components"
19
+ ];
20
+ const createDevCopilotBridgeConfig = (config) => {
21
+ return {
22
+ rootDir: config.rootDir,
23
+ host: config.host ?? "127.0.0.1",
24
+ port: config.port ?? 3339,
25
+ corsOrigin: config.corsOrigin ?? "*",
26
+ agent: config.agent ?? "codex",
27
+ allowedPaths: config.allowedPaths ?? DEFAULT_ALLOWED_PATHS
28
+ };
29
+ };
30
+
31
+ //#endregion
32
+ //#region src/bridge/shared/lib/temp-path.ts
33
+ const createTempPath = (prefix, extension = "") => {
34
+ const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
35
+ return path.join(os.tmpdir(), `${prefix}-${suffix}${extension}`);
36
+ };
37
+
38
+ //#endregion
39
+ //#region src/bridge/lib/patch.ts
40
+ const execFileAsync$1 = promisify(execFile);
41
+ const getChangedFiles = (patchPreview) => {
42
+ const regex = /^\+\+\+ b\/(.+)$/gm;
43
+ const changedFiles = /* @__PURE__ */ new Set();
44
+ let match = regex.exec(patchPreview);
45
+ while (match) {
46
+ changedFiles.add(match[1]);
47
+ match = regex.exec(patchPreview);
48
+ }
49
+ return [...changedFiles];
50
+ };
51
+ const getGitApplyDirectoryArgs = async (cwd) => {
52
+ try {
53
+ const { stdout } = await execFileAsync$1("git", ["rev-parse", "--show-prefix"], { cwd });
54
+ const prefix = stdout.trim().replace(/\/$/, "");
55
+ return prefix ? ["--directory", prefix] : [];
56
+ } catch {
57
+ return [];
58
+ }
59
+ };
60
+ const withOriginalLineIndent = (content, oldText, newText) => {
61
+ if (!newText.includes("\n")) return newText;
62
+ const matchIndex = content.indexOf(oldText);
63
+ if (matchIndex < 0) return newText;
64
+ const lineStartIndex = content.lastIndexOf("\n", matchIndex) + 1;
65
+ const indent = content.slice(lineStartIndex, matchIndex).match(/^[ \t]*/)?.[0] ?? "";
66
+ return indent ? newText.replaceAll("\n", `\n${indent}`) : newText;
67
+ };
68
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ const createWhitespaceTolerantPattern = (value) => {
70
+ const tokens = value.trim().split(/\s+/).filter(Boolean);
71
+ if (!tokens.length) return null;
72
+ return new RegExp(tokens.map(escapeRegExp).join("\\s+"));
73
+ };
74
+ const replaceText = (content, oldText, newText) => {
75
+ if (content.includes(oldText)) return {
76
+ replaced: true,
77
+ content: content.replace(oldText, withOriginalLineIndent(content, oldText, newText))
78
+ };
79
+ const match = createWhitespaceTolerantPattern(oldText)?.exec(content);
80
+ if (!match) return {
81
+ replaced: false,
82
+ content
83
+ };
84
+ const matchedText = match[0];
85
+ return {
86
+ replaced: true,
87
+ content: content.replace(matchedText, withOriginalLineIndent(content, matchedText, newText))
88
+ };
89
+ };
90
+ const validatePatchPaths = (patchPreview, validatePath) => {
91
+ const changedFiles = getChangedFiles(patchPreview);
92
+ if (!changedFiles.length) throw new Error("패치에서 변경 파일을 찾을 수 없습니다.");
93
+ changedFiles.forEach(validatePath);
94
+ return changedFiles;
95
+ };
96
+ const checkUnifiedPatch = async (patchPreview, cwd) => {
97
+ const tempFilePath = createTempPath("dev-copilot-check", ".patch");
98
+ await promises.writeFile(tempFilePath, patchPreview, "utf-8");
99
+ try {
100
+ await execFileAsync$1("git", [
101
+ "apply",
102
+ "--check",
103
+ ...await getGitApplyDirectoryArgs(cwd),
104
+ tempFilePath
105
+ ], { cwd });
106
+ } catch (error) {
107
+ const detail = error instanceof Error && "stderr" in error ? String(error.stderr) : error instanceof Error ? error.message : "알 수 없는 patch 검증 오류";
108
+ throw new Error(`적용 가능한 diff를 생성하지 못했습니다: ${detail.trim()}`);
109
+ } finally {
110
+ await promises.rm(tempFilePath, { force: true });
111
+ }
112
+ };
113
+ const createGitPatch = async (filePath, oldContent, newContent) => {
114
+ const tempDir = createTempPath("dev-copilot-diff");
115
+ const oldFilePath = path.join(tempDir, "old", filePath);
116
+ const newFilePath = path.join(tempDir, "new", filePath);
117
+ await promises.mkdir(path.dirname(oldFilePath), { recursive: true });
118
+ await promises.mkdir(path.dirname(newFilePath), { recursive: true });
119
+ await promises.writeFile(oldFilePath, oldContent, "utf-8");
120
+ await promises.writeFile(newFilePath, newContent, "utf-8");
121
+ try {
122
+ const { stdout } = await execFileAsync$1("git", [
123
+ "diff",
124
+ "--no-index",
125
+ "--no-ext-diff",
126
+ "--src-prefix=a/",
127
+ "--dst-prefix=b/",
128
+ "--",
129
+ path.join("old", filePath),
130
+ path.join("new", filePath)
131
+ ], { cwd: tempDir }).catch((error) => {
132
+ const typedError = error;
133
+ if (typedError.code === 1 && typedError.stdout) return { stdout: typedError.stdout };
134
+ throw error;
135
+ });
136
+ return stdout.replaceAll(`a/old/${filePath}`, `a/${filePath}`).replaceAll(`b/new/${filePath}`, `b/${filePath}`);
137
+ } finally {
138
+ await promises.rm(tempDir, {
139
+ force: true,
140
+ recursive: true
141
+ });
142
+ }
143
+ };
144
+ const createPatchFromTextReplacements = async (replacements, cwd, validatePath) => {
145
+ const byPath = /* @__PURE__ */ new Map();
146
+ for (const replacement of replacements) {
147
+ const normalizedPath = validatePath(replacement.path);
148
+ const current = byPath.get(normalizedPath) ?? [];
149
+ current.push(replacement);
150
+ byPath.set(normalizedPath, current);
151
+ }
152
+ const patches = [];
153
+ for (const [filePath, fileReplacements] of byPath.entries()) {
154
+ const absolutePath = path.join(cwd, filePath);
155
+ const oldContent = await promises.readFile(absolutePath, "utf-8");
156
+ let newContent = oldContent;
157
+ for (const replacement of fileReplacements) {
158
+ const result = replaceText(newContent, replacement.oldText, replacement.newText);
159
+ if (!result.replaced) throw new Error(`원문을 파일에서 찾을 수 없습니다: ${filePath}`);
160
+ newContent = result.content;
161
+ }
162
+ if (newContent === oldContent) continue;
163
+ patches.push(await createGitPatch(filePath, oldContent, newContent));
164
+ }
165
+ const patchPreview = patches.join("\n");
166
+ if (!patchPreview.trim()) throw new Error("변경할 내용이 없습니다.");
167
+ return patchPreview;
168
+ };
169
+ const applyUnifiedPatch = async (patchPreview, cwd, actor) => {
170
+ const tempFilePath = createTempPath("dev-copilot", ".patch");
171
+ await promises.writeFile(tempFilePath, patchPreview, "utf-8");
172
+ try {
173
+ const directoryArgs = await getGitApplyDirectoryArgs(cwd);
174
+ const { stdout: statOutput } = await execFileAsync$1("git", [
175
+ "apply",
176
+ "--numstat",
177
+ ...directoryArgs,
178
+ tempFilePath
179
+ ], { cwd });
180
+ if (!statOutput.trim()) throw new Error("patch가 실제 파일 변경을 만들지 않았습니다.");
181
+ await execFileAsync$1("git", [
182
+ "apply",
183
+ "--check",
184
+ ...directoryArgs,
185
+ tempFilePath
186
+ ], { cwd });
187
+ await execFileAsync$1("git", [
188
+ "apply",
189
+ ...directoryArgs,
190
+ tempFilePath
191
+ ], { cwd });
192
+ return {
193
+ actor,
194
+ changedFiles: getChangedFiles(patchPreview)
195
+ };
196
+ } catch (error) {
197
+ const detail = error instanceof Error && "stderr" in error ? String(error.stderr) : error instanceof Error ? error.message : "알 수 없는 patch 적용 오류";
198
+ throw new Error(`patch 적용에 실패했습니다: ${detail.trim()}`);
199
+ } finally {
200
+ await promises.rm(tempFilePath, { force: true });
201
+ }
202
+ };
203
+
204
+ //#endregion
205
+ //#region src/bridge/lib/guards.ts
206
+ const resolveAndValidatePath = (filePath, allowedPaths, rootDir) => {
207
+ if (!filePath) throw new Error("허용되지 않은 파일 경로입니다.");
208
+ let normalizedInput = filePath.replaceAll("\\", "/");
209
+ if (rootDir) {
210
+ const normalizedRootDir = rootDir.replaceAll("\\", "/").replace(/\/+$/, "");
211
+ if (normalizedInput.startsWith(`${normalizedRootDir}/`)) normalizedInput = normalizedInput.slice(normalizedRootDir.length + 1);
212
+ }
213
+ if (path.isAbsolute(normalizedInput) || normalizedInput.includes("..")) throw new Error("허용되지 않은 파일 경로입니다.");
214
+ const normalized = normalizedInput;
215
+ const firstSegment = normalized.split("/")[0];
216
+ if (!(allowedPaths.includes(".") || allowedPaths.includes("*")) && !allowedPaths.includes(firstSegment)) throw new Error(`허용 경로 외 파일은 수정할 수 없습니다: ${firstSegment}`);
217
+ return normalized;
218
+ };
219
+
220
+ //#endregion
221
+ //#region src/bridge/lib/store.ts
222
+ const patchStore = /* @__PURE__ */ new Map();
223
+ const TTL_MS = 1e3 * 60 * 15;
224
+ const pruneExpired = () => {
225
+ const now = Date.now();
226
+ for (const [key, value] of patchStore.entries()) if (now - value.createdAt > TTL_MS) patchStore.delete(key);
227
+ };
228
+ const createProposedPatch = (patchPreview, allowedPaths) => {
229
+ pruneExpired();
230
+ const patchId = randomUUID();
231
+ const approvalToken = `approve:${patchId}`;
232
+ patchStore.set(patchId, {
233
+ patchId,
234
+ approvalToken,
235
+ patchPreview,
236
+ allowedPaths,
237
+ createdAt: Date.now()
238
+ });
239
+ return {
240
+ patchId,
241
+ approvalToken
242
+ };
243
+ };
244
+ const getProposedPatch = (patchId, approvalToken) => {
245
+ pruneExpired();
246
+ const item = patchStore.get(patchId);
247
+ if (!item || item.approvalToken !== approvalToken) return null;
248
+ return item;
249
+ };
250
+ const deleteProposedPatch = (patchId) => {
251
+ patchStore.delete(patchId);
252
+ };
253
+
254
+ //#endregion
255
+ //#region src/bridge/features/patch-flow/patch-flow-service.ts
256
+ const sanitizeAllowedPaths = (fileHints) => {
257
+ if (!fileHints?.length) return null;
258
+ const sanitized = fileHints.map((value) => value.trim()).filter(Boolean).filter((value) => value === "." || value === "*" || /^[A-Za-z0-9._-]+$/.test(value));
259
+ return sanitized.length ? Array.from(new Set(sanitized)) : null;
260
+ };
261
+ const resolveAllowedPathsWithSrcFallback = (config, fileHints) => {
262
+ const resolved = sanitizeAllowedPaths(fileHints) ?? config.allowedPaths;
263
+ if (!resolved.includes(".") && !resolved.includes("*") && !resolved.includes("src")) return [...resolved, "src"];
264
+ return resolved;
265
+ };
266
+ const buildPatchPreview = async (changes, config, allowedPaths) => {
267
+ if (!changes?.length) return "";
268
+ return createPatchFromTextReplacements(changes, config.rootDir, (filePath) => resolveAndValidatePath(filePath, allowedPaths, config.rootDir));
269
+ };
270
+ const validatePatchPreview = async (patchPreview, config, allowedPaths) => {
271
+ validatePatchPaths(patchPreview, (filePath) => resolveAndValidatePath(filePath, allowedPaths, config.rootDir));
272
+ await checkUnifiedPatch(patchPreview, config.rootDir);
273
+ };
274
+ const saveProposedPatch = (patchPreview, allowedPaths) => {
275
+ return createProposedPatch(patchPreview, allowedPaths);
276
+ };
277
+ const applySavedPatch = async (config, patchId, approvalToken) => {
278
+ const proposedPatch = getProposedPatch(patchId, approvalToken);
279
+ if (!proposedPatch) throw new Error("유효하지 않거나 만료된 patchId입니다.");
280
+ validatePatchPaths(proposedPatch.patchPreview, (filePath) => resolveAndValidatePath(filePath, proposedPatch.allowedPaths?.length ? proposedPatch.allowedPaths : config.allowedPaths, config.rootDir));
281
+ const result = await applyUnifiedPatch(proposedPatch.patchPreview, config.rootDir, "local-codex-bridge");
282
+ deleteProposedPatch(patchId);
283
+ return result;
284
+ };
285
+
286
+ //#endregion
287
+ //#region src/bridge/shared/http/read-json-body.ts
288
+ const readJsonBody = async (request) => {
289
+ const chunks = [];
290
+ for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
291
+ const raw = Buffer.concat(chunks).toString("utf-8");
292
+ return JSON.parse(raw);
293
+ };
294
+
295
+ //#endregion
296
+ //#region src/bridge/shared/http/respond.ts
297
+ const createCorsHeaders = (config) => ({
298
+ "Access-Control-Allow-Origin": config.corsOrigin,
299
+ "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
300
+ "Access-Control-Allow-Headers": "Content-Type",
301
+ "Content-Type": "application/json; charset=utf-8"
302
+ });
303
+ const sendJson = (response, config, statusCode, payload) => {
304
+ response.writeHead(statusCode, createCorsHeaders(config));
305
+ response.end(JSON.stringify(payload));
306
+ };
307
+
308
+ //#endregion
309
+ //#region src/bridge/features/apply/apply-route.ts
310
+ const toPatchSummary = (changedFiles) => {
311
+ return changedFiles.length === 1 ? "1개 파일에 수정이 반영되었습니다." : `${changedFiles.length}개 파일에 수정이 반영되었습니다.`;
312
+ };
313
+ const handleApplyRoute = async (request, response, config) => {
314
+ const payload = await readJsonBody(request);
315
+ if (!payload.patchId || !payload.approvalToken) {
316
+ sendJson(response, config, 400, { error: "patchId와 approvalToken이 필요합니다." });
317
+ return;
318
+ }
319
+ const result = await applySavedPatch(config, payload.patchId, payload.approvalToken);
320
+ sendJson(response, config, 200, {
321
+ applied: true,
322
+ changedFiles: result.changedFiles,
323
+ summary: toPatchSummary(result.changedFiles)
324
+ });
325
+ };
326
+
327
+ //#endregion
328
+ //#region src/bridge/internal/agents/edit-response-schema.ts
329
+ const agentEditResponseSchema = {
330
+ type: "object",
331
+ properties: {
332
+ message: { type: "string" },
333
+ warnings: {
334
+ type: "array",
335
+ items: { type: "string" }
336
+ },
337
+ changes: {
338
+ type: "array",
339
+ minItems: 1,
340
+ items: {
341
+ type: "object",
342
+ properties: {
343
+ path: { type: "string" },
344
+ oldText: { type: "string" },
345
+ newText: { type: "string" }
346
+ },
347
+ required: [
348
+ "path",
349
+ "oldText",
350
+ "newText"
351
+ ],
352
+ additionalProperties: false
353
+ }
354
+ }
355
+ },
356
+ required: [
357
+ "message",
358
+ "warnings",
359
+ "changes"
360
+ ],
361
+ additionalProperties: false
362
+ };
363
+ const agentEditResponseSchemaJson = JSON.stringify(agentEditResponseSchema);
364
+
365
+ //#endregion
366
+ //#region src/bridge/internal/agents/constants.ts
367
+ const DEFAULT_AGENT_TIMEOUT_MS = 12e4;
368
+ const DEFAULT_AGENT_MAX_BUFFER_BYTES = 1024 * 1024 * 5;
369
+ const STATUS_CHECK_TIMEOUT_MS = 1e4;
370
+ const QUICK_STATUS_CHECK_TIMEOUT_MS = 5e3;
371
+ const MODEL_STATUS_CHECK_TIMEOUT_MS = 3e4;
372
+
373
+ //#endregion
374
+ //#region src/bridge/internal/agents/agent-response.ts
375
+ const findFirstString = (value) => {
376
+ if (typeof value === "string") {
377
+ const text = value.trim();
378
+ return text ? text : null;
379
+ }
380
+ if (Array.isArray(value)) {
381
+ for (const item of value) {
382
+ const found = findFirstString(item);
383
+ if (found) return found;
384
+ }
385
+ return null;
386
+ }
387
+ if (value && typeof value === "object") {
388
+ const record = value;
389
+ for (const key of [
390
+ "result",
391
+ "message",
392
+ "output",
393
+ "text",
394
+ "content"
395
+ ]) if (key in record) {
396
+ const found = findFirstString(record[key]);
397
+ if (found) return found;
398
+ }
399
+ for (const nestedValue of Object.values(record)) {
400
+ const found = findFirstString(nestedValue);
401
+ if (found) return found;
402
+ }
403
+ }
404
+ return null;
405
+ };
406
+ const normalizeAgentBridgeResponse = (value) => {
407
+ return {
408
+ message: typeof value.message === "string" ? value.message : "",
409
+ patchPreview: value.patchPreview,
410
+ changes: value.changes,
411
+ warnings: Array.isArray(value.warnings) ? value.warnings : []
412
+ };
413
+ };
414
+ const parseJsonLikeText = (text) => {
415
+ const trimmed = text.trim();
416
+ const jsonText = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? trimmed;
417
+ return JSON.parse(jsonText);
418
+ };
419
+ const parseClaudeJsonOutput = (raw) => {
420
+ const parsed = JSON.parse(raw);
421
+ return {
422
+ parsed,
423
+ text: findFirstString(parsed) ?? ""
424
+ };
425
+ };
426
+ const parseClaudeEditResponse = (raw) => {
427
+ const { parsed, text } = parseClaudeJsonOutput(raw);
428
+ return normalizeAgentBridgeResponse(parsed.structured_output && typeof parsed.structured_output === "object" ? parsed.structured_output : parseJsonLikeText(text));
429
+ };
430
+ const parseCodexEditResponse = (raw) => {
431
+ return normalizeAgentBridgeResponse(parseJsonLikeText(raw));
432
+ };
433
+ const parseAnswerResponse = (rawOutput) => {
434
+ return {
435
+ message: rawOutput.trim(),
436
+ warnings: []
437
+ };
438
+ };
439
+
440
+ //#endregion
441
+ //#region src/bridge/internal/agents/cli-error.ts
442
+ const extractCliErrorDetails = (error) => {
443
+ const record = error && typeof error === "object" ? error : null;
444
+ const message = error instanceof Error ? error.message : String(error);
445
+ const stderr = typeof record?.stderr === "string" ? record.stderr : "";
446
+ const stdout = typeof record?.stdout === "string" ? record.stdout : "";
447
+ const signal = typeof record?.signal === "string" ? record.signal : "";
448
+ const killed = record?.killed === true;
449
+ return {
450
+ message,
451
+ stderr,
452
+ stdout,
453
+ signal,
454
+ killed,
455
+ merged: [
456
+ message,
457
+ stderr,
458
+ stdout,
459
+ signal,
460
+ killed ? "killed" : ""
461
+ ].map((part) => part.trim()).filter(Boolean).join("\n")
462
+ };
463
+ };
464
+ const isCliTimeout = (details) => {
465
+ return /SIGTERM|timed out|timeout/i.test(details.merged);
466
+ };
467
+ const isCliCommandMissing = (details) => {
468
+ return /ENOENT/.test(details.merged);
469
+ };
470
+
471
+ //#endregion
472
+ //#region src/bridge/internal/agents/run-cli.ts
473
+ const runCli = (command, args, options) => {
474
+ return new Promise((resolve, reject) => {
475
+ const maxBuffer = options.maxBuffer ?? DEFAULT_AGENT_MAX_BUFFER_BYTES;
476
+ const child = spawn(command, args, {
477
+ cwd: options.cwd,
478
+ stdio: [
479
+ "ignore",
480
+ "pipe",
481
+ "pipe"
482
+ ],
483
+ env: {
484
+ ...process.env,
485
+ NO_COLOR: "1",
486
+ ...options.env
487
+ }
488
+ });
489
+ const stdoutChunks = [];
490
+ const stderrChunks = [];
491
+ let stdoutLength = 0;
492
+ let stderrLength = 0;
493
+ let settled = false;
494
+ const timeout = setTimeout(() => {
495
+ if (settled) return;
496
+ settled = true;
497
+ child.kill("SIGTERM");
498
+ const error = /* @__PURE__ */ new Error(`${command} 실행 시간이 초과되었습니다.`);
499
+ Object.assign(error, {
500
+ signal: "SIGTERM",
501
+ killed: true
502
+ });
503
+ reject(error);
504
+ }, options.timeoutMs);
505
+ const fail = (error) => {
506
+ if (settled) return;
507
+ settled = true;
508
+ clearTimeout(timeout);
509
+ child.kill("SIGTERM");
510
+ reject(error);
511
+ };
512
+ child.stdout.on("data", (chunk) => {
513
+ stdoutLength += chunk.length;
514
+ if (stdoutLength > maxBuffer) {
515
+ fail(/* @__PURE__ */ new Error(`${command} stdout이 허용된 크기를 초과했습니다.`));
516
+ return;
517
+ }
518
+ stdoutChunks.push(chunk);
519
+ });
520
+ child.stderr.on("data", (chunk) => {
521
+ stderrLength += chunk.length;
522
+ if (stderrLength > maxBuffer) {
523
+ fail(/* @__PURE__ */ new Error(`${command} stderr가 허용된 크기를 초과했습니다.`));
524
+ return;
525
+ }
526
+ stderrChunks.push(chunk);
527
+ });
528
+ child.on("error", (error) => {
529
+ fail(error);
530
+ });
531
+ child.on("close", (code, signal) => {
532
+ if (settled) return;
533
+ settled = true;
534
+ clearTimeout(timeout);
535
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
536
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8");
537
+ if (code === 0) {
538
+ resolve({
539
+ stdout,
540
+ stderr
541
+ });
542
+ return;
543
+ }
544
+ const error = new Error(stderr || `${command} 실행에 실패했습니다. code=${code ?? "unknown"}`);
545
+ Object.assign(error, {
546
+ stdout,
547
+ stderr,
548
+ signal,
549
+ killed: signal === "SIGTERM",
550
+ code
551
+ });
552
+ reject(error);
553
+ });
554
+ });
555
+ };
556
+
557
+ //#endregion
558
+ //#region src/bridge/internal/agents/claude-command.ts
559
+ let claudeCommandPromise = null;
560
+ const CLAUDE_SMOKE_MODEL = process.env.DEV_COPILOT_CLAUDE_MODEL ?? "haiku";
561
+ const getPathExecutableNames$1 = () => {
562
+ if (process.platform !== "win32") return ["claude"];
563
+ return ["claude", ...(process.env.PATHEXT?.split(";").filter(Boolean) ?? [
564
+ ".EXE",
565
+ ".CMD",
566
+ ".BAT"
567
+ ]).map((ext) => `claude${ext.toLowerCase()}`)];
568
+ };
569
+ const collectClaudeCandidates = () => {
570
+ const candidates = [];
571
+ const envCommand = process.env.DEV_COPILOT_CLAUDE_BIN?.trim();
572
+ if (envCommand) candidates.push({
573
+ command: envCommand,
574
+ source: "env"
575
+ });
576
+ const executableNames = getPathExecutableNames$1();
577
+ const pathCandidates = (process.env.PATH ?? "").split(delimiter).filter(Boolean).flatMap((pathDir) => executableNames.map((name) => join(pathDir, name)));
578
+ for (const command of pathCandidates) candidates.push({
579
+ command,
580
+ source: "path"
581
+ });
582
+ return Array.from(new Map(candidates.map((candidate) => [candidate.command, candidate])).values());
583
+ };
584
+ const canRunClaude = async (command) => {
585
+ try {
586
+ await runCli(command, [
587
+ "-p",
588
+ "--output-format",
589
+ "json",
590
+ "--model",
591
+ CLAUDE_SMOKE_MODEL,
592
+ "--tools",
593
+ "",
594
+ "--strict-mcp-config",
595
+ "--mcp-config",
596
+ "{\"mcpServers\":{}}",
597
+ "--",
598
+ "OK만 출력해줘."
599
+ ], {
600
+ cwd: process.cwd(),
601
+ timeoutMs: QUICK_STATUS_CHECK_TIMEOUT_MS,
602
+ maxBuffer: 1024 * 128
603
+ });
604
+ return true;
605
+ } catch {
606
+ return false;
607
+ }
608
+ };
609
+ const resolveClaudeCommand = async () => {
610
+ if (claudeCommandPromise) return claudeCommandPromise;
611
+ claudeCommandPromise = (async () => {
612
+ const candidates = collectClaudeCandidates();
613
+ const envCandidate = candidates.find((candidate) => candidate.source === "env");
614
+ if (envCandidate) return envCandidate.command;
615
+ for (const candidate of candidates) if (await canRunClaude(candidate.command)) return candidate.command;
616
+ return process.env.DEV_COPILOT_CLAUDE_BIN?.trim() || "claude";
617
+ })();
618
+ return claudeCommandPromise;
619
+ };
620
+
621
+ //#endregion
622
+ //#region src/bridge/internal/prompts.ts
623
+ const buildAgentPrompt = (request) => {
624
+ if (request.mode === "answer") return [
625
+ "Answer the user's web overlay request.",
626
+ "Do not modify files.",
627
+ "Use the provided project context when it is relevant.",
628
+ "Respond in Korean.",
629
+ "Keep the answer concise and actionable.",
630
+ "",
631
+ `Current route: ${request.route ?? "unknown"}`,
632
+ `Selected text:\n${request.selectedText || "(none)"}`,
633
+ `User prompt:\n${request.prompt}`,
634
+ `Previous AI response:\n${request.previousResponse ?? "(none)"}`,
635
+ `Project context:\n${request.projectContext ?? "(none)"}`
636
+ ].join("\n");
637
+ return [
638
+ "You are a local code-edit proposal generator called from a web overlay.",
639
+ "Do not modify files directly.",
640
+ "Return JSON only.",
641
+ "Do not generate unified diff directly.",
642
+ "Put repository-relative file path(path), exact old text(oldText), and replacement text(newText) into the changes array.",
643
+ "Never use absolute paths. The path must be relative to the repository root, for example src/features/article/model/data.ts.",
644
+ "oldText must exactly match the file content and must not use ellipsis.",
645
+ "If the selected text spans multiple rendered lines, use the actual file text from project context for oldText.",
646
+ "newText must be meaningfully different from oldText.",
647
+ "Ground the proposal in the selected text, user prompt, and provided project context.",
648
+ "Write the message field in Korean.",
649
+ "",
650
+ `Current route: ${request.route ?? "unknown"}`,
651
+ `Allowed path hints: ${(request.fileHints ?? []).join(", ") || "(none)"}`,
652
+ `Selected text:\n${request.selectedText || "(none)"}`,
653
+ `User prompt:\n${request.prompt}`,
654
+ `Previous AI response:\n${request.previousResponse ?? "(none)"}`,
655
+ `Project context:\n${request.projectContext ?? "(none)"}`
656
+ ].join("\n");
657
+ };
658
+
659
+ //#endregion
660
+ //#region src/bridge/entities/agent/status.ts
661
+ const createAuthenticatedStatus = (params) => ({
662
+ available: true,
663
+ authenticated: true,
664
+ agent: params.agent,
665
+ message: params.message,
666
+ model: params.model
667
+ });
668
+ const createLoginRequiredStatus = (params) => ({
669
+ available: params.available ?? true,
670
+ authenticated: false,
671
+ agent: params.agent,
672
+ message: params.message,
673
+ loginCommand: params.loginCommand
674
+ });
675
+ const createUnavailableStatus = (params) => ({
676
+ available: false,
677
+ authenticated: false,
678
+ agent: params.agent,
679
+ message: params.message
680
+ });
681
+ const createUnauthenticatedStatus = (params) => ({
682
+ available: params.available ?? true,
683
+ authenticated: false,
684
+ agent: params.agent,
685
+ message: params.message,
686
+ loginCommand: params.loginCommand
687
+ });
688
+
689
+ //#endregion
690
+ //#region src/bridge/internal/agents/claude-adapter.ts
691
+ const CLAUDE_MODEL = process.env.DEV_COPILOT_CLAUDE_MODEL ?? "haiku";
692
+ const CLAUDE_TIMEOUT_MS = Number(process.env.DEV_COPILOT_CLAUDE_TIMEOUT_MS ?? DEFAULT_AGENT_TIMEOUT_MS);
693
+ const AUTH_ERROR_PATTERN = /401|authentication|invalid authentication credentials|please run \/login|claude\s*\/login|로그인/i;
694
+ const toClaudeErrorMessage = (error) => {
695
+ const details = extractCliErrorDetails(error);
696
+ if (isCliTimeout(details)) return "Claude Code 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.";
697
+ if (AUTH_ERROR_PATTERN.test(details.merged)) return "Claude Code 로그인이 필요합니다. 터미널에서 `claude /login`을 실행해 주세요.";
698
+ if (isCliCommandMissing(details)) return "Claude CLI를 찾을 수 없습니다. Claude Code CLI 설치 상태를 확인해 주세요.";
699
+ return details.message;
700
+ };
701
+ const claudeAdapter = {
702
+ agent: "claude",
703
+ async run(request) {
704
+ const claudeCommand = await resolveClaudeCommand();
705
+ const prompt = buildAgentPrompt(request);
706
+ const args = [
707
+ "-p",
708
+ "--output-format",
709
+ "json",
710
+ "--model",
711
+ CLAUDE_MODEL,
712
+ "--tools",
713
+ "",
714
+ "--strict-mcp-config",
715
+ "--mcp-config",
716
+ "{\"mcpServers\":{}}",
717
+ ...request.mode === "edit" ? ["--json-schema", agentEditResponseSchemaJson] : [],
718
+ "--",
719
+ prompt
720
+ ];
721
+ try {
722
+ const { stdout } = await runCli(claudeCommand, args, {
723
+ cwd: request.cwd,
724
+ timeoutMs: Number(process.env.DEV_COPILOT_AGENT_TIMEOUT_MS ?? CLAUDE_TIMEOUT_MS)
725
+ });
726
+ if (request.mode === "answer") {
727
+ const { text } = parseClaudeJsonOutput(stdout);
728
+ return parseAnswerResponse(text);
729
+ }
730
+ return parseClaudeEditResponse(stdout);
731
+ } catch (error) {
732
+ throw new Error(toClaudeErrorMessage(error));
733
+ }
734
+ },
735
+ async getStatus(cwd) {
736
+ try {
737
+ await runCli(await resolveClaudeCommand(), ["--version"], {
738
+ cwd,
739
+ timeoutMs: QUICK_STATUS_CHECK_TIMEOUT_MS,
740
+ maxBuffer: 1024 * 32
741
+ });
742
+ return createAuthenticatedStatus({
743
+ agent: "claude",
744
+ message: "Claude Code CLI에 로그인되어 있습니다.",
745
+ model: CLAUDE_MODEL
746
+ });
747
+ } catch (error) {
748
+ const details = extractCliErrorDetails(error);
749
+ if (isCliCommandMissing(details)) return createUnavailableStatus({
750
+ agent: "claude",
751
+ message: "Claude CLI를 찾을 수 없습니다."
752
+ });
753
+ if (AUTH_ERROR_PATTERN.test(details.merged)) return createLoginRequiredStatus({
754
+ agent: "claude",
755
+ message: "Claude Code 로그인이 필요합니다.",
756
+ loginCommand: "claude /login"
757
+ });
758
+ return createUnauthenticatedStatus({
759
+ agent: "claude",
760
+ message: isCliTimeout(details) ? "Claude Code 상태 확인 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요." : `Claude Code 상태 확인에 실패했습니다: ${details.message}`,
761
+ loginCommand: isCliTimeout(details) ? void 0 : "claude /login"
762
+ });
763
+ }
764
+ }
765
+ };
766
+
767
+ //#endregion
768
+ //#region src/bridge/internal/agents/codex-command.ts
769
+ let codexCommandPromise = null;
770
+ const canAccess = async (path) => {
771
+ try {
772
+ await access(path);
773
+ return true;
774
+ } catch {
775
+ return false;
776
+ }
777
+ };
778
+ const getPathExecutableNames = () => {
779
+ if (process.platform !== "win32") return ["codex"];
780
+ return ["codex", ...(process.env.PATHEXT?.split(";").filter(Boolean) ?? [
781
+ ".EXE",
782
+ ".CMD",
783
+ ".BAT"
784
+ ]).map((ext) => `codex${ext.toLowerCase()}`)];
785
+ };
786
+ const collectCodexCandidates = async () => {
787
+ const executableNames = getPathExecutableNames();
788
+ const candidates = [];
789
+ const pathCandidates = (process.env.PATH ?? "").split(delimiter).filter(Boolean).flatMap((pathDir) => executableNames.map((name) => join(pathDir, name)));
790
+ for (const command of pathCandidates) if (await canAccess(command)) candidates.push({ command });
791
+ return Array.from(new Map(candidates.map((candidate) => [candidate.command, candidate])).values());
792
+ };
793
+ const parseCodexVersion = (output) => {
794
+ const version = output.match(/\d+\.\d+\.\d+(?:[-+][^\s]+)?/)?.[0];
795
+ if (!version) return null;
796
+ return { version };
797
+ };
798
+ const resolveCandidate = async (candidate) => {
799
+ const { stdout, stderr } = await runCli(candidate.command, ["--version"], {
800
+ cwd: process.cwd(),
801
+ timeoutMs: STATUS_CHECK_TIMEOUT_MS,
802
+ maxBuffer: 1024 * 128
803
+ });
804
+ const parsedVersion = parseCodexVersion(`${stdout}\n${stderr}`);
805
+ if (!parsedVersion) return null;
806
+ return {
807
+ ...candidate,
808
+ ...parsedVersion
809
+ };
810
+ };
811
+ const resolveCodexCommand = async () => {
812
+ if (codexCommandPromise) return codexCommandPromise;
813
+ codexCommandPromise = (async () => {
814
+ const candidates = await collectCodexCandidates();
815
+ const available = (await Promise.all(candidates.map(async (candidate) => {
816
+ try {
817
+ return await resolveCandidate(candidate);
818
+ } catch {
819
+ return null;
820
+ }
821
+ }))).filter((candidate) => Boolean(candidate));
822
+ if (!available.length) throw new Error("Codex CLI를 찾을 수 없습니다.");
823
+ return available[0].command;
824
+ })();
825
+ return codexCommandPromise;
826
+ };
827
+
828
+ //#endregion
829
+ //#region src/bridge/internal/agents/codex-home.ts
830
+ let codexExecutionEnvPromise = null;
831
+ const copyIfExists = async (sourcePath, targetPath) => {
832
+ try {
833
+ await access(sourcePath);
834
+ await copyFile(sourcePath, targetPath);
835
+ } catch {
836
+ return;
837
+ }
838
+ };
839
+ const writeIsolatedConfig = async (targetPath) => {
840
+ try {
841
+ if (await readFile(targetPath, "utf-8") === "") return;
842
+ } catch {}
843
+ await writeFile(targetPath, "", "utf-8");
844
+ };
845
+ const resolveSourceCodexHome = (env = process.env) => {
846
+ return env.CODEX_HOME?.trim() || join(homedir(), ".codex");
847
+ };
848
+ const createIsolatedCodexHome = async (sourceCodexHome) => {
849
+ const isolatedCodexHome = await mkdtemp(join(tmpdir(), "dev-copilot-codex-home-"));
850
+ await copyIfExists(join(sourceCodexHome, "auth.json"), join(isolatedCodexHome, "auth.json"));
851
+ await copyIfExists(join(sourceCodexHome, ".credentials.json"), join(isolatedCodexHome, ".credentials.json"));
852
+ await writeIsolatedConfig(join(isolatedCodexHome, "config.toml"));
853
+ return isolatedCodexHome;
854
+ };
855
+ const getCodexExecutionEnv = async (options) => {
856
+ if (codexExecutionEnvPromise) return codexExecutionEnvPromise;
857
+ codexExecutionEnvPromise = (async () => {
858
+ return { CODEX_HOME: await createIsolatedCodexHome(options?.sourceCodexHome ?? resolveSourceCodexHome()) };
859
+ })();
860
+ return codexExecutionEnvPromise;
861
+ };
862
+
863
+ //#endregion
864
+ //#region src/bridge/internal/agents/codex-health.ts
865
+ const CODEX_MODEL$1 = process.env.DEV_COPILOT_CODEX_MODEL ?? "gpt-5.3-codex";
866
+ let codexHealthPromise = null;
867
+ const isAuthenticatedFromStatus = (statusOutput) => {
868
+ const normalized = statusOutput.toLowerCase();
869
+ if (/not logged in|login required|로그인 필요/.test(normalized)) return false;
870
+ return /logged in|로그인됨|로그인되어/.test(normalized);
871
+ };
872
+ const createSchemaSmokePayload = () => JSON.stringify({
873
+ message: "ok",
874
+ warnings: [],
875
+ changes: [{
876
+ path: "src/App.tsx",
877
+ oldText: "before",
878
+ newText: "after"
879
+ }]
880
+ });
881
+ const createPromptJsonSmokePayload = () => JSON.stringify({
882
+ message: "ok",
883
+ warnings: [],
884
+ changes: [{
885
+ path: "src/App.tsx",
886
+ oldText: "before",
887
+ newText: "after"
888
+ }]
889
+ });
890
+ const runExecSmokeTest = async (command, cwd, env) => {
891
+ const outputPath = createTempPath("dev-copilot-codex-answer-smoke", ".txt");
892
+ try {
893
+ await runCli(command, [
894
+ "exec",
895
+ "--ephemeral",
896
+ "--model",
897
+ CODEX_MODEL$1,
898
+ "--cd",
899
+ cwd,
900
+ "--sandbox",
901
+ "read-only",
902
+ "--skip-git-repo-check",
903
+ "--output-last-message",
904
+ outputPath,
905
+ "OK만 출력해줘."
906
+ ], {
907
+ cwd,
908
+ timeoutMs: MODEL_STATUS_CHECK_TIMEOUT_MS,
909
+ maxBuffer: 1024 * 512,
910
+ env
911
+ });
912
+ if (!(await readFile(outputPath, "utf-8")).trim()) throw new Error("Codex CLI smoke test output이 비어 있습니다.");
913
+ } finally {
914
+ await rm(outputPath, { force: true });
915
+ }
916
+ };
917
+ const runEditSchemaSmokeTest = async (command, cwd, env) => {
918
+ const outputPath = createTempPath("dev-copilot-codex-edit-smoke", ".json");
919
+ const schemaPath = createTempPath("dev-copilot-codex-edit-schema", ".json");
920
+ try {
921
+ await writeFile(schemaPath, JSON.stringify(agentEditResponseSchema), "utf-8");
922
+ await runCli(command, [
923
+ "exec",
924
+ "--ephemeral",
925
+ "--model",
926
+ CODEX_MODEL$1,
927
+ "--cd",
928
+ cwd,
929
+ "--sandbox",
930
+ "read-only",
931
+ "--skip-git-repo-check",
932
+ "--output-last-message",
933
+ outputPath,
934
+ "--output-schema",
935
+ schemaPath,
936
+ "message에 ok, warnings에 빈 배열, changes에 한 개의 수정 항목만 담아 JSON으로 응답해줘."
937
+ ], {
938
+ cwd,
939
+ timeoutMs: MODEL_STATUS_CHECK_TIMEOUT_MS,
940
+ maxBuffer: 1024 * 512,
941
+ env
942
+ });
943
+ const output = await readFile(outputPath, "utf-8");
944
+ if (output.trim() !== createSchemaSmokePayload()) JSON.parse(output);
945
+ } finally {
946
+ await rm(outputPath, { force: true });
947
+ await rm(schemaPath, { force: true });
948
+ }
949
+ };
950
+ const runEditPromptJsonSmokeTest = async (command, cwd, env) => {
951
+ const outputPath = createTempPath("dev-copilot-codex-edit-prompt-json-smoke", ".json");
952
+ try {
953
+ await runCli(command, [
954
+ "exec",
955
+ "--ephemeral",
956
+ "--model",
957
+ CODEX_MODEL$1,
958
+ "--cd",
959
+ cwd,
960
+ "--sandbox",
961
+ "read-only",
962
+ "--skip-git-repo-check",
963
+ "--output-last-message",
964
+ outputPath,
965
+ [
966
+ "Return JSON only.",
967
+ "Respond with an object that has message, warnings, and changes.",
968
+ "warnings must be an empty array.",
969
+ "changes must contain exactly one object with path, oldText, and newText.",
970
+ "Use this exact payload:",
971
+ createPromptJsonSmokePayload()
972
+ ].join("\n")
973
+ ], {
974
+ cwd,
975
+ timeoutMs: MODEL_STATUS_CHECK_TIMEOUT_MS,
976
+ maxBuffer: 1024 * 512,
977
+ env
978
+ });
979
+ const normalized = (await readFile(outputPath, "utf-8")).trim().replace(/^```json\s*|\s*```$/g, "");
980
+ const parsed = JSON.parse(normalized);
981
+ if (JSON.stringify(parsed) !== createPromptJsonSmokePayload()) throw new Error("Codex CLI prompt-json smoke test payload mismatch");
982
+ } finally {
983
+ await rm(outputPath, { force: true });
984
+ }
985
+ };
986
+ const getCodexHealthCheck = async (cwd) => {
987
+ if (codexHealthPromise) return codexHealthPromise;
988
+ codexHealthPromise = (async () => {
989
+ let command;
990
+ try {
991
+ command = await resolveCodexCommand();
992
+ } catch (error) {
993
+ return {
994
+ status: "unavailable",
995
+ message: error instanceof Error ? error.message : "Codex CLI를 찾을 수 없습니다."
996
+ };
997
+ }
998
+ const env = await getCodexExecutionEnv();
999
+ try {
1000
+ const { stdout, stderr } = await runCli(command, ["login", "status"], {
1001
+ cwd,
1002
+ timeoutMs: STATUS_CHECK_TIMEOUT_MS,
1003
+ maxBuffer: 1024 * 128,
1004
+ env
1005
+ });
1006
+ const statusText = stdout.trim() || stderr.trim();
1007
+ if (statusText && !isAuthenticatedFromStatus(statusText)) return {
1008
+ status: "login_required",
1009
+ message: statusText || "Codex CLI 로그인이 필요합니다.",
1010
+ loginCommand: "codex login",
1011
+ command
1012
+ };
1013
+ } catch (error) {
1014
+ const details = extractCliErrorDetails(error);
1015
+ if (isCliTimeout(details)) return {
1016
+ status: "timeout",
1017
+ message: "Codex CLI 상태 확인 중 시간이 초과되었습니다.",
1018
+ command
1019
+ };
1020
+ if (isCliCommandMissing(details)) return {
1021
+ status: "unavailable",
1022
+ message: "Codex CLI를 실행할 수 없습니다."
1023
+ };
1024
+ if (isCliCommandMissing(details)) return {
1025
+ status: "unavailable",
1026
+ message: "Codex CLI를 실행할 수 없습니다."
1027
+ };
1028
+ }
1029
+ try {
1030
+ await runExecSmokeTest(command, cwd, env);
1031
+ } catch (error) {
1032
+ const details = extractCliErrorDetails(error);
1033
+ return {
1034
+ status: isCliTimeout(details) ? "timeout" : "exec_failed",
1035
+ message: isCliTimeout(details) ? "Codex CLI 답변 smoke test 중 시간이 초과되었습니다." : "Codex CLI 답변 smoke test에 실패했습니다.",
1036
+ command
1037
+ };
1038
+ }
1039
+ try {
1040
+ await runEditSchemaSmokeTest(command, cwd, env);
1041
+ } catch (error) {
1042
+ if (isCliTimeout(extractCliErrorDetails(error))) return {
1043
+ status: "timeout",
1044
+ message: "Codex CLI edit schema smoke test 중 시간이 초과되었습니다.",
1045
+ command
1046
+ };
1047
+ try {
1048
+ await runEditPromptJsonSmokeTest(command, cwd, env);
1049
+ return {
1050
+ status: "ready",
1051
+ message: "Codex CLI가 prompt-json fallback으로 응답 준비를 마쳤습니다.",
1052
+ command,
1053
+ model: CODEX_MODEL$1,
1054
+ editStrategy: "prompt-json"
1055
+ };
1056
+ } catch {
1057
+ return {
1058
+ status: "schema_failed",
1059
+ message: "Codex CLI edit 응답 스키마 smoke test에 실패했습니다.",
1060
+ command
1061
+ };
1062
+ }
1063
+ }
1064
+ return {
1065
+ status: "ready",
1066
+ message: "Codex CLI가 응답 준비를 마쳤습니다.",
1067
+ command,
1068
+ model: CODEX_MODEL$1,
1069
+ editStrategy: "output-schema"
1070
+ };
1071
+ })();
1072
+ return codexHealthPromise;
1073
+ };
1074
+
1075
+ //#endregion
1076
+ //#region src/bridge/internal/agents/codex-adapter.ts
1077
+ const CODEX_MODEL = process.env.DEV_COPILOT_CODEX_MODEL ?? "gpt-5.3-codex";
1078
+ const CODEX_TIMEOUT_MS = Number(process.env.DEV_COPILOT_CODEX_TIMEOUT_MS ?? DEFAULT_AGENT_TIMEOUT_MS);
1079
+ const isCodexLoginRequiredError = (error) => {
1080
+ const details = extractCliErrorDetails(error);
1081
+ return /not logged in|login required|authentication|unauthorized/i.test(details.merged);
1082
+ };
1083
+ const toCodexErrorMessage = (error) => {
1084
+ const details = extractCliErrorDetails(error);
1085
+ if (/requires a newer version of Codex/i.test(details.merged)) return ["현재 설치된 Codex CLI 버전과 모델 조합이 호환되지 않습니다.", "Codex CLI를 업데이트하거나 DEV_COPILOT_CODEX_MODEL 값을 호환 가능한 모델로 지정해 주세요."].join(" ");
1086
+ if (/migration .* is missing in the resolved migrations/i.test(details.merged)) return ["Codex 로컬 상태 DB 마이그레이션 오류가 발생했습니다.", "Codex CLI를 최신 버전으로 업데이트한 뒤 다시 시도해 주세요."].join(" ");
1087
+ if (/Error loading config\.toml: invalid transport/i.test(details.merged)) return [
1088
+ "현재 Codex CLI가 Codex 설정 파일의 MCP 형식을 해석하지 못했습니다.",
1089
+ "Dev Copilot이 호환되는 Codex 실행 파일을 자동으로 찾지 못했습니다.",
1090
+ "Codex CLI를 업데이트한 뒤 다시 시도해 주세요."
1091
+ ].join(" ");
1092
+ if (isCodexLoginRequiredError(error)) return "Codex CLI 로그인이 필요합니다. 터미널에서 `codex login`을 실행해 주세요.";
1093
+ if (isCliCommandMissing(details)) return "Codex CLI를 찾을 수 없습니다. Codex CLI 설치 상태를 확인해 주세요.";
1094
+ return details.message;
1095
+ };
1096
+ const codexAdapter = {
1097
+ agent: "codex",
1098
+ async warmup(cwd) {
1099
+ await getCodexHealthCheck(cwd);
1100
+ },
1101
+ async run(request) {
1102
+ const health = await getCodexHealthCheck(request.cwd);
1103
+ if (health.status !== "ready" || !health.command) throw new Error(health.message);
1104
+ const codexCommand = health.command;
1105
+ const outputPath = createTempPath("dev-copilot-codex", ".json");
1106
+ const schemaPath = createTempPath("dev-copilot-codex-schema", ".json");
1107
+ const useOutputSchema = health.editStrategy !== "prompt-json";
1108
+ if (request.mode === "edit" && useOutputSchema) await promises.writeFile(schemaPath, JSON.stringify(agentEditResponseSchema), "utf-8");
1109
+ const prompt = request.mode === "edit" && !useOutputSchema ? [
1110
+ buildAgentPrompt(request),
1111
+ "",
1112
+ "Return a JSON object only.",
1113
+ "Do not include markdown fences unless strictly necessary.",
1114
+ "The object must contain message, warnings, and changes.",
1115
+ "warnings must be an array of strings.",
1116
+ "changes must be an array of { path, oldText, newText } objects."
1117
+ ].join("\n") : buildAgentPrompt(request);
1118
+ const env = await getCodexExecutionEnv();
1119
+ const args = [
1120
+ "exec",
1121
+ "--ephemeral",
1122
+ "--model",
1123
+ CODEX_MODEL,
1124
+ "--cd",
1125
+ request.cwd,
1126
+ "--sandbox",
1127
+ "read-only",
1128
+ "--skip-git-repo-check",
1129
+ "--output-last-message",
1130
+ outputPath,
1131
+ ...request.mode === "edit" && useOutputSchema ? ["--output-schema", schemaPath] : [],
1132
+ prompt
1133
+ ];
1134
+ try {
1135
+ await runCli(codexCommand, args, {
1136
+ cwd: request.cwd,
1137
+ maxBuffer: DEFAULT_AGENT_MAX_BUFFER_BYTES,
1138
+ timeoutMs: Number(process.env.DEV_COPILOT_AGENT_TIMEOUT_MS ?? CODEX_TIMEOUT_MS),
1139
+ env
1140
+ });
1141
+ if (request.mode === "answer") return parseAnswerResponse(await promises.readFile(outputPath, "utf-8"));
1142
+ return parseCodexEditResponse(await promises.readFile(outputPath, "utf-8"));
1143
+ } catch (error) {
1144
+ throw new Error(toCodexErrorMessage(error));
1145
+ } finally {
1146
+ await promises.rm(outputPath, { force: true });
1147
+ await promises.rm(schemaPath, { force: true });
1148
+ }
1149
+ },
1150
+ async getStatus(cwd) {
1151
+ const health = await getCodexHealthCheck(cwd);
1152
+ if (health.status === "ready") return createAuthenticatedStatus({
1153
+ agent: "codex",
1154
+ message: "Codex CLI에 로그인되어 있습니다.",
1155
+ model: health.model ?? CODEX_MODEL
1156
+ });
1157
+ if (health.status === "login_required") return createLoginRequiredStatus({
1158
+ agent: "codex",
1159
+ message: health.message,
1160
+ loginCommand: health.loginCommand ?? "codex login"
1161
+ });
1162
+ if (health.status === "unavailable") return createUnavailableStatus({
1163
+ agent: "codex",
1164
+ message: health.message
1165
+ });
1166
+ return createUnauthenticatedStatus({
1167
+ agent: "codex",
1168
+ message: health.message
1169
+ });
1170
+ }
1171
+ };
1172
+
1173
+ //#endregion
1174
+ //#region src/bridge/features/agent-runner/resolve-agent-adapter.ts
1175
+ const adapters = {
1176
+ codex: codexAdapter,
1177
+ claude: claudeAdapter
1178
+ };
1179
+ const testAdapters = {};
1180
+ const resolveAgentAdapter = (agent) => {
1181
+ return testAdapters[agent] ?? adapters[agent];
1182
+ };
1183
+
1184
+ //#endregion
1185
+ //#region src/bridge/features/project-context/search-strategies.ts
1186
+ const execFileAsync = promisify(execFile);
1187
+ const normalize = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "");
1188
+ const walk = async (rootDir, relativeDir, files, limit) => {
1189
+ if (files.length >= limit) return;
1190
+ const absoluteDir = path.join(rootDir, relativeDir);
1191
+ const entries = await promises.readdir(absoluteDir, { withFileTypes: true });
1192
+ for (const entry of entries) {
1193
+ if (files.length >= limit) return;
1194
+ const relativePath = normalize(path.join(relativeDir, entry.name));
1195
+ if ([
1196
+ "node_modules",
1197
+ ".next",
1198
+ ".git",
1199
+ "dist",
1200
+ "build",
1201
+ "coverage"
1202
+ ].some((name) => relativePath.includes(`/${name}/`) || relativePath.startsWith(`${name}/`))) continue;
1203
+ if (entry.isDirectory()) {
1204
+ await walk(rootDir, relativePath, files, limit);
1205
+ continue;
1206
+ }
1207
+ if (entry.isFile()) files.push(relativePath);
1208
+ }
1209
+ };
1210
+ const searchWithRg = async (config, query, limit) => {
1211
+ const { stdout } = await execFileAsync("rg", [
1212
+ "--line-number",
1213
+ "--fixed-strings",
1214
+ "--color",
1215
+ "never",
1216
+ "--glob",
1217
+ "!node_modules/**",
1218
+ "--glob",
1219
+ "!.next/**",
1220
+ "--glob",
1221
+ "!.git/**",
1222
+ query,
1223
+ ...config.allowedDirs
1224
+ ], {
1225
+ cwd: config.rootDir,
1226
+ maxBuffer: 1024 * 1024
1227
+ });
1228
+ return stdout.split("\n").filter(Boolean).slice(0, limit).map((line) => {
1229
+ const [filePath, lineNumber, ...rest] = line.split(":");
1230
+ return {
1231
+ path: filePath,
1232
+ line: Number(lineNumber),
1233
+ text: rest.join(":").trim()
1234
+ };
1235
+ });
1236
+ };
1237
+ const searchWithNode = async (config, query, limit) => {
1238
+ const files = [];
1239
+ for (const dir of config.allowedDirs) try {
1240
+ await walk(config.rootDir, dir, files, 500);
1241
+ } catch {
1242
+ continue;
1243
+ }
1244
+ const results = [];
1245
+ for (const filePath of files) {
1246
+ if (results.length >= limit) break;
1247
+ try {
1248
+ (await promises.readFile(path.join(config.rootDir, filePath), "utf-8")).slice(0, 1e5).split("\n").forEach((line, index) => {
1249
+ if (results.length < limit && line.includes(query)) results.push({
1250
+ path: filePath,
1251
+ line: index + 1,
1252
+ text: line.trim()
1253
+ });
1254
+ });
1255
+ } catch {
1256
+ continue;
1257
+ }
1258
+ }
1259
+ return results;
1260
+ };
1261
+ const searchProjectText = async (config, query, limit) => {
1262
+ if (!query.trim()) return [];
1263
+ try {
1264
+ return await searchWithRg(config, query, limit);
1265
+ } catch {
1266
+ return searchWithNode(config, query, limit);
1267
+ }
1268
+ };
1269
+ const compact = (value) => value.replace(/\s+/g, "");
1270
+ const searchProjectTextIgnoringWhitespace = async (config, query, limit) => {
1271
+ const compactQuery = compact(query);
1272
+ if (!compactQuery) return [];
1273
+ const files = [];
1274
+ for (const dir of config.allowedDirs) try {
1275
+ await walk(config.rootDir, dir, files, 500);
1276
+ } catch {
1277
+ continue;
1278
+ }
1279
+ const results = [];
1280
+ for (const filePath of files) {
1281
+ if (results.length >= limit) break;
1282
+ try {
1283
+ (await promises.readFile(path.join(config.rootDir, filePath), "utf-8")).slice(0, 1e5).split("\n").forEach((line, index) => {
1284
+ if (results.length < limit && compact(line).includes(compactQuery)) results.push({
1285
+ path: filePath,
1286
+ line: index + 1,
1287
+ text: line.trim()
1288
+ });
1289
+ });
1290
+ } catch {
1291
+ continue;
1292
+ }
1293
+ }
1294
+ return results;
1295
+ };
1296
+
1297
+ //#endregion
1298
+ //#region src/bridge/features/project-context/build-project-context.ts
1299
+ const findBySelectedText = async (rootDir, allowedDirs, text) => {
1300
+ const normalized = text.replace(/\s+/g, " ").trim();
1301
+ const candidates = [
1302
+ normalized,
1303
+ normalized.slice(0, 120),
1304
+ ...normalized.split(/[.!?]\s+/).filter((item) => item.length > 12)
1305
+ ];
1306
+ for (const candidate of candidates) {
1307
+ const results = await searchProjectText({
1308
+ rootDir,
1309
+ allowedDirs
1310
+ }, candidate, 8);
1311
+ if (results.length) return {
1312
+ query: candidate,
1313
+ results
1314
+ };
1315
+ }
1316
+ for (const candidate of candidates) {
1317
+ const results = await searchProjectTextIgnoringWhitespace({
1318
+ rootDir,
1319
+ allowedDirs
1320
+ }, candidate, 8);
1321
+ if (results.length) return {
1322
+ query: `${candidate} (ignore-whitespace)`,
1323
+ results
1324
+ };
1325
+ }
1326
+ return {
1327
+ query: normalized,
1328
+ results: []
1329
+ };
1330
+ };
1331
+ const buildCopilotProjectContext = async (rootDir, allowedDirs, params) => {
1332
+ const routeQuery = params.route?.startsWith("/") ? `app${params.route === "/" ? "" : params.route}/page.tsx` : params.route;
1333
+ const selectedText = params.selectedText?.trim();
1334
+ const textMatches = selectedText ? await findBySelectedText(rootDir, allowedDirs, selectedText) : {
1335
+ query: "",
1336
+ results: []
1337
+ };
1338
+ const routeMatches = routeQuery ? await searchProjectText({
1339
+ rootDir,
1340
+ allowedDirs
1341
+ }, routeQuery, 5) : [];
1342
+ return JSON.stringify({
1343
+ project: { allowedDirs },
1344
+ requestContext: {
1345
+ route: params.route,
1346
+ fileHints: params.fileHints
1347
+ },
1348
+ selectedTextLookup: textMatches,
1349
+ routeLookup: routeMatches,
1350
+ guidance: "selectedTextLookup.results의 path와 text를 우선 사용해 path/oldText/newText 수정안을 작성하세요."
1351
+ }, null, 2);
1352
+ };
1353
+
1354
+ //#endregion
1355
+ //#region src/bridge/features/chat/chat-route.ts
1356
+ const isCopilotAgent$1 = (value) => {
1357
+ return value === "codex" || value === "claude";
1358
+ };
1359
+ const handleChatRoute = async (request, response, config) => {
1360
+ const payload = await readJsonBody(request);
1361
+ const effectiveAllowedPaths = resolveAllowedPathsWithSrcFallback(config, payload.context?.fileHints);
1362
+ const adapter = resolveAgentAdapter(isCopilotAgent$1(payload.context?.agent) ? payload.context.agent : config.agent);
1363
+ if (!payload.prompt?.trim()) {
1364
+ sendJson(response, config, 400, { error: "프롬프트를 입력해 주세요." });
1365
+ return;
1366
+ }
1367
+ const projectContext = await buildCopilotProjectContext(config.rootDir, effectiveAllowedPaths, {
1368
+ selectedText: payload.selectedText,
1369
+ route: payload.context?.route,
1370
+ fileHints: payload.context?.fileHints
1371
+ });
1372
+ const agentResponse = await adapter.run({
1373
+ selectedText: payload.selectedText ?? "",
1374
+ prompt: payload.prompt,
1375
+ mode: payload.mode,
1376
+ route: payload.context?.route,
1377
+ fileHints: payload.context?.fileHints,
1378
+ previousResponse: payload.context?.previousResponse,
1379
+ projectContext,
1380
+ cwd: config.rootDir
1381
+ });
1382
+ if (payload.mode === "answer") {
1383
+ sendJson(response, config, 200, {
1384
+ message: agentResponse.message,
1385
+ warnings: agentResponse.warnings
1386
+ });
1387
+ return;
1388
+ }
1389
+ let patchPreview = "";
1390
+ try {
1391
+ patchPreview = await buildPatchPreview(agentResponse.changes, config, effectiveAllowedPaths);
1392
+ } catch (error) {
1393
+ sendJson(response, config, 200, {
1394
+ message: agentResponse.message ?? "에이전트가 수정안을 만들었지만 실제 파일 내용과 매칭하지 못했습니다.",
1395
+ warnings: [...agentResponse.warnings ?? [], error instanceof Error ? error.message : "수정안 생성에 실패했습니다."]
1396
+ });
1397
+ return;
1398
+ }
1399
+ if (!patchPreview) {
1400
+ sendJson(response, config, 200, {
1401
+ message: agentResponse.message ?? "패치 제안을 생성하지 못했습니다.",
1402
+ warnings: [...agentResponse.warnings ?? [], "적용 가능한 변경 목록이 없어 패치 미리보기와 적용 버튼을 만들 수 없습니다."]
1403
+ });
1404
+ return;
1405
+ }
1406
+ try {
1407
+ await validatePatchPreview(patchPreview, config, effectiveAllowedPaths);
1408
+ } catch (error) {
1409
+ sendJson(response, config, 200, {
1410
+ message: agentResponse.message ?? "에이전트가 수정안을 만들었지만 적용 가능한 diff 형식이 아닙니다.",
1411
+ patchPreview,
1412
+ warnings: [...agentResponse.warnings ?? [], error instanceof Error ? error.message : "patch 검증에 실패했습니다."]
1413
+ });
1414
+ return;
1415
+ }
1416
+ const patch = saveProposedPatch(patchPreview, effectiveAllowedPaths);
1417
+ sendJson(response, config, 200, {
1418
+ message: agentResponse.message ?? "에이전트가 패치 미리보기를 생성했습니다.",
1419
+ patchPreview,
1420
+ patchId: patch.patchId,
1421
+ warnings: agentResponse.warnings ?? []
1422
+ });
1423
+ };
1424
+
1425
+ //#endregion
1426
+ //#region src/bridge/features/status/status-route.ts
1427
+ const isCopilotAgent = (value) => {
1428
+ return value === "codex" || value === "claude";
1429
+ };
1430
+ const handleStatusRoute = async (_request, response, config, url) => {
1431
+ const queryAgent = url.searchParams.get("agent");
1432
+ sendJson(response, config, 200, await resolveAgentAdapter(isCopilotAgent(queryAgent) ? queryAgent : config.agent).getStatus(config.rootDir));
1433
+ };
1434
+
1435
+ //#endregion
1436
+ //#region src/bridge/shared/http/error-mapper.ts
1437
+ const toBridgeErrorMessage = (error) => {
1438
+ if (error instanceof Error) return error.message;
1439
+ return "브리지 서버 처리 중 오류가 발생했습니다.";
1440
+ };
1441
+
1442
+ //#endregion
1443
+ //#region src/bridge/app/create-bridge-server.ts
1444
+ const createDevCopilotBridgeServer = (config) => {
1445
+ return createServer(async (request, response) => {
1446
+ const method = request.method ?? "GET";
1447
+ const url = new URL(request.url ?? "/", `http://${config.host}:${config.port}`);
1448
+ if (method === "OPTIONS") {
1449
+ response.writeHead(204, createCorsHeaders(config));
1450
+ response.end();
1451
+ return;
1452
+ }
1453
+ try {
1454
+ if (method === "GET" && url.pathname === "/status") {
1455
+ await handleStatusRoute(request, response, config, url);
1456
+ return;
1457
+ }
1458
+ if (method === "POST" && url.pathname === "/chat") {
1459
+ await handleChatRoute(request, response, config);
1460
+ return;
1461
+ }
1462
+ if (method === "POST" && url.pathname === "/apply") {
1463
+ await handleApplyRoute(request, response, config);
1464
+ return;
1465
+ }
1466
+ sendJson(response, config, 404, { error: "지원하지 않는 경로입니다." });
1467
+ } catch (error) {
1468
+ sendJson(response, config, 500, { error: toBridgeErrorMessage(error) });
1469
+ }
1470
+ });
1471
+ };
1472
+
1473
+ //#endregion
1474
+ //#region src/bridge/app/run-bridge-cli.ts
1475
+ const resolveAgent = (value) => {
1476
+ if (value === "claude") return "claude";
1477
+ return "codex";
1478
+ };
1479
+ const runDevCopilotBridgeCli = async (argv) => {
1480
+ const portFlagIndex = argv.findIndex((value) => value === "-p");
1481
+ const portFlagValue = portFlagIndex >= 0 ? argv[portFlagIndex + 1] : void 0;
1482
+ const positionalPort = argv.find((value) => /^\d+$/.test(value));
1483
+ const resolvedPort = portFlagValue ? Number(portFlagValue) : positionalPort ? Number(positionalPort) : Number(process.env.DEV_COPILOT_BRIDGE_PORT ?? 3339);
1484
+ const positionalAgent = argv.find((value) => value === "codex" || value === "claude");
1485
+ const config = createDevCopilotBridgeConfig({
1486
+ rootDir: process.cwd(),
1487
+ host: process.env.DEV_COPILOT_BRIDGE_HOST,
1488
+ port: Number.isFinite(resolvedPort) ? resolvedPort : 3339,
1489
+ corsOrigin: process.env.DEV_COPILOT_BRIDGE_CORS_ORIGIN ?? "*",
1490
+ agent: resolveAgent(positionalAgent),
1491
+ allowedPaths: (process.env.DEV_COPILOT_ALLOWED_PATHS ?? "app,src,widgets,features,entities,shared,components").split(",").map((value) => value.trim()).filter(Boolean)
1492
+ });
1493
+ const server = createDevCopilotBridgeServer(config);
1494
+ await new Promise((resolve, reject) => {
1495
+ server.once("error", (error) => {
1496
+ reject(error);
1497
+ });
1498
+ server.listen(config.port, config.host, () => {
1499
+ process.stdout.write(`[dev-copilot-bridge] listening on http://${config.host}:${config.port}\n`);
1500
+ resolve();
1501
+ });
1502
+ });
1503
+ resolveAgentAdapter(config.agent).warmup?.(config.rootDir).catch((error) => {
1504
+ process.stderr.write(`[dev-copilot-bridge] startup health check failed: ${error instanceof Error ? error.message : String(error)}\n`);
1505
+ });
1506
+ };
1507
+
1508
+ //#endregion
1509
+ export { createDevCopilotBridgeServer as n, createDevCopilotBridgeConfig as r, runDevCopilotBridgeCli as t };
1510
+ //# sourceMappingURL=run-bridge-cli-DVLGcVgq.mjs.map