jutell 0.3.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/assets/default-config.json +26 -0
  4. package/assets/local-admin/assets/index-CVml-p-C.css +1 -0
  5. package/assets/local-admin/assets/index-Gxd8X8ii.js +60 -0
  6. package/assets/local-admin/index.html +14 -0
  7. package/assets/local-admin-server.js +1391 -0
  8. package/assets/mcp-server/config/bridge-config.js +72 -0
  9. package/assets/mcp-server/index.js +42 -0
  10. package/assets/mcp-server/tools/bridge-tools.js +64 -0
  11. package/assets/mcp-server/tools/catalog.js +25 -0
  12. package/assets/mcp-server/tools/usage-counters.js +97 -0
  13. package/assets/skill/SKILL.md +135 -0
  14. package/assets/skill/references/explained-diff-format.md +117 -0
  15. package/assets/skill/references/feature-registry.md +29 -0
  16. package/assets/skill/references/glossary-ko.md +302 -0
  17. package/assets/skill/references/report-format.md +176 -0
  18. package/assets/skill/references/risk-level-guide.md +85 -0
  19. package/assets/templates/request-builder/BUG_REPORT_REQUEST.md +100 -0
  20. package/assets/templates/request-builder/CODE_REVIEW_REQUEST.md +93 -0
  21. package/assets/templates/request-builder/DESIGN_REQUEST.md +111 -0
  22. package/assets/templates/request-builder/FEATURE_REQUEST.md +93 -0
  23. package/assets/templates/request-builder/MANUAL_EDIT_GUIDE.md +95 -0
  24. package/assets/templates/request-builder/NEXT_AGENT_HANDOFF.md +106 -0
  25. package/assets/templates/request-builder/PROJECT_PLANNING_REQUEST.md +106 -0
  26. package/assets/templates/request-builder/README.md +48 -0
  27. package/assets/version.json +6 -0
  28. package/dist/cli.js +82 -0
  29. package/dist/commands/dashboard.js +81 -0
  30. package/dist/commands/default.js +103 -0
  31. package/dist/commands/lifecycle.js +166 -0
  32. package/dist/commands/migrate.js +159 -0
  33. package/dist/commands/provider.js +135 -0
  34. package/dist/commands/session/add-work.js +43 -0
  35. package/dist/commands/session/create-page.js +53 -0
  36. package/dist/commands/session/finish-session.js +28 -0
  37. package/dist/commands/session/index.js +76 -0
  38. package/dist/commands/session/move-page.js +37 -0
  39. package/dist/commands/session/new-session.js +24 -0
  40. package/dist/commands/session/operator-storage.js +126 -0
  41. package/dist/commands/session/prompt.js +77 -0
  42. package/dist/commands/session/storage-command.js +74 -0
  43. package/dist/commands/session/storage.js +212 -0
  44. package/dist/commands/session/types.js +1 -0
  45. package/dist/commands/status.js +208 -0
  46. package/dist/commands/upgrade.js +113 -0
  47. package/dist/commands/use.js +180 -0
  48. package/dist/compat.js +5 -0
  49. package/dist/config/managed.js +257 -0
  50. package/dist/config/paths.js +100 -0
  51. package/dist/index.js +4 -0
  52. package/dist/installer/agents.js +42 -0
  53. package/dist/installer/claude.js +160 -0
  54. package/dist/installer/config.js +45 -0
  55. package/dist/installer/opencode.js +237 -0
  56. package/dist/installer/providers.js +15 -0
  57. package/dist/installer/skill.js +94 -0
  58. package/dist/output/format.js +187 -0
  59. package/dist/process/mcpProbe.js +122 -0
  60. package/dist/process/system.js +34 -0
  61. package/dist/types.js +1 -0
  62. package/package.json +55 -0
@@ -0,0 +1,1391 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // ../../apps/local-admin/server/config/schema.ts
12
+ var schema_exports = {};
13
+ __export(schema_exports, {
14
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
15
+ FEATURE_IDS: () => FEATURE_IDS,
16
+ LIMIT_RANGES: () => LIMIT_RANGES,
17
+ PROFILES: () => PROFILES,
18
+ SUPPORTED_VERSION: () => SUPPORTED_VERSION,
19
+ changedFields: () => changedFields,
20
+ validateConfig: () => validateConfig
21
+ });
22
+ function validateConfig(input) {
23
+ if (!isRecord(input)) return { ok: false, error: "\uC124\uC815\uC740 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
24
+ if (!hasOnlyKeys(input, ["version", "profile", "features", "limits", "mcp", "usageMeasurement", "voice"])) {
25
+ return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC124\uC815 \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4." };
26
+ }
27
+ if (input.version !== SUPPORTED_VERSION) return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC124\uC815 \uBC84\uC804\uC785\uB2C8\uB2E4." };
28
+ if (!["minimal", "balanced", "learning", "detailed"].includes(String(input.profile))) {
29
+ return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 Profile\uC785\uB2C8\uB2E4." };
30
+ }
31
+ if (!isRecord(input.features) || !hasOnlyKeys(input.features, FEATURE_IDS)) {
32
+ return { ok: false, error: "\uACF5\uC2DD Feature\uB9CC \uC124\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." };
33
+ }
34
+ for (const featureId of FEATURE_IDS) {
35
+ if (input.features[featureId] !== void 0 && typeof input.features[featureId] !== "boolean") {
36
+ return { ok: false, error: "Feature \uAC12\uC740 ON \uB610\uB294 OFF\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
37
+ }
38
+ }
39
+ if (!isRecord(input.limits) || !hasOnlyKeys(input.limits, Object.keys(LIMIT_RANGES))) {
40
+ return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 limits \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4." };
41
+ }
42
+ for (const [key, range] of Object.entries(LIMIT_RANGES)) {
43
+ const value = input.limits[key];
44
+ if (typeof value !== "number" || !Number.isInteger(value) || value < range.min || value > range.max) {
45
+ return { ok: false, error: `${key}\uB294 ${range.min}~${range.max} \uC0AC\uC774\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.` };
46
+ }
47
+ }
48
+ if ("mcp" in input && (!isRecord(input.mcp) || !hasOnlyKeys(input.mcp, ["enabled", "autoStart"]) || typeof input.mcp.enabled !== "boolean" || input.mcp.autoStart !== void 0 && typeof input.mcp.autoStart !== "boolean")) {
49
+ return { ok: false, error: "MCP \uC124\uC815\uC740 enabled(ON/OFF)\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." };
50
+ }
51
+ if ("usageMeasurement" in input && (!isRecord(input.usageMeasurement) || !hasOnlyKeys(input.usageMeasurement, ["localCountersEnabled"]) || typeof input.usageMeasurement.localCountersEnabled !== "boolean")) {
52
+ return { ok: false, error: "usageMeasurement \uC124\uC815\uC740 localCountersEnabled(ON/OFF)\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." };
53
+ }
54
+ if ("voice" in input && (!isRecord(input.voice) || !hasOnlyKeys(input.voice, ["preset"]) || input.voice.preset !== void 0 && !["default", "plain", "learning", "jutell"].includes(String(input.voice.preset)))) {
55
+ return { ok: false, error: "voice \uC124\uC815\uC758 preset\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." };
56
+ }
57
+ const profile = input.profile;
58
+ const rawFeatures = input.features;
59
+ const features = Object.fromEntries(
60
+ FEATURE_IDS.map((id) => [id, typeof rawFeatures[id] === "boolean" ? rawFeatures[id] : PROFILES[profile].features[id]])
61
+ );
62
+ return {
63
+ ok: true,
64
+ value: {
65
+ version: SUPPORTED_VERSION,
66
+ profile,
67
+ features,
68
+ limits: { ...input.limits },
69
+ mcp: "mcp" in input ? { enabled: isRecord(input.mcp) ? input.mcp.enabled : false } : { ...DEFAULT_CONFIG.mcp },
70
+ usageMeasurement: "usageMeasurement" in input ? { ...input.usageMeasurement } : { ...DEFAULT_CONFIG.usageMeasurement },
71
+ ..."voice" in input ? { voice: { preset: input.voice && isRecord(input.voice) && typeof input.voice.preset === "string" ? input.voice.preset : "default" } } : {}
72
+ }
73
+ };
74
+ }
75
+ function changedFields(before, after) {
76
+ const changes = [];
77
+ if (before.profile !== after.profile) changes.push({ field: "profile", before: before.profile, after: after.profile });
78
+ for (const id of FEATURE_IDS) {
79
+ if (before.features[id] !== after.features[id]) {
80
+ changes.push({ field: `features.${id}`, before: before.features[id], after: after.features[id] });
81
+ }
82
+ }
83
+ for (const key of Object.keys(LIMIT_RANGES)) {
84
+ if (before.limits[key] !== after.limits[key]) {
85
+ changes.push({ field: `limits.${key}`, before: before.limits[key], after: after.limits[key] });
86
+ }
87
+ }
88
+ for (const key of ["enabled"]) {
89
+ if (before.mcp[key] !== after.mcp[key]) changes.push({ field: `mcp.${key}`, before: before.mcp[key], after: after.mcp[key] });
90
+ }
91
+ if (before.usageMeasurement.localCountersEnabled !== after.usageMeasurement.localCountersEnabled) {
92
+ changes.push({ field: "usageMeasurement.localCountersEnabled", before: before.usageMeasurement.localCountersEnabled, after: after.usageMeasurement.localCountersEnabled });
93
+ }
94
+ return changes;
95
+ }
96
+ var SUPPORTED_VERSION, FEATURE_IDS, PROFILES, DEFAULT_CONFIG, LIMIT_RANGES, isRecord, hasOnlyKeys;
97
+ var init_schema = __esm({
98
+ "../../apps/local-admin/server/config/schema.ts"() {
99
+ SUPPORTED_VERSION = 1;
100
+ FEATURE_IDS = [
101
+ "changeSummary",
102
+ "userVisibleChanges",
103
+ "internalChanges",
104
+ "mainFiles",
105
+ "explainedDiff",
106
+ "glossary",
107
+ "validationResults",
108
+ "riskAssessment",
109
+ "userActions",
110
+ "nextActionSuggestions",
111
+ "requestClarificationGuide",
112
+ "manualEditGuidance",
113
+ "requestBuilder"
114
+ ];
115
+ PROFILES = {
116
+ minimal: {
117
+ features: {
118
+ changeSummary: true,
119
+ userVisibleChanges: true,
120
+ internalChanges: false,
121
+ mainFiles: false,
122
+ explainedDiff: false,
123
+ glossary: false,
124
+ validationResults: true,
125
+ riskAssessment: false,
126
+ userActions: true,
127
+ nextActionSuggestions: false,
128
+ requestClarificationGuide: false,
129
+ manualEditGuidance: false,
130
+ requestBuilder: true
131
+ },
132
+ limits: { maxMainFiles: 3, maxGlossaryTerms: 1, compactReportMaxSentences: 8 }
133
+ },
134
+ balanced: {
135
+ features: {
136
+ changeSummary: true,
137
+ userVisibleChanges: true,
138
+ internalChanges: true,
139
+ mainFiles: true,
140
+ explainedDiff: true,
141
+ glossary: true,
142
+ validationResults: true,
143
+ riskAssessment: true,
144
+ userActions: true,
145
+ nextActionSuggestions: true,
146
+ requestClarificationGuide: true,
147
+ manualEditGuidance: true,
148
+ requestBuilder: true
149
+ },
150
+ limits: { maxMainFiles: 5, maxGlossaryTerms: 3, compactReportMaxSentences: 12 }
151
+ },
152
+ learning: {
153
+ features: {
154
+ changeSummary: true,
155
+ userVisibleChanges: true,
156
+ internalChanges: true,
157
+ mainFiles: true,
158
+ explainedDiff: true,
159
+ glossary: true,
160
+ validationResults: true,
161
+ riskAssessment: true,
162
+ userActions: true,
163
+ nextActionSuggestions: true,
164
+ requestClarificationGuide: true,
165
+ manualEditGuidance: true,
166
+ requestBuilder: true
167
+ },
168
+ limits: { maxMainFiles: 5, maxGlossaryTerms: 6, compactReportMaxSentences: 12 }
169
+ },
170
+ detailed: {
171
+ features: {
172
+ changeSummary: true,
173
+ userVisibleChanges: true,
174
+ internalChanges: true,
175
+ mainFiles: true,
176
+ explainedDiff: true,
177
+ glossary: true,
178
+ validationResults: true,
179
+ riskAssessment: true,
180
+ userActions: true,
181
+ nextActionSuggestions: true,
182
+ requestClarificationGuide: true,
183
+ manualEditGuidance: true,
184
+ requestBuilder: true
185
+ },
186
+ limits: { maxMainFiles: 5, maxGlossaryTerms: 6, compactReportMaxSentences: 18 }
187
+ }
188
+ };
189
+ DEFAULT_CONFIG = {
190
+ version: SUPPORTED_VERSION,
191
+ profile: "balanced",
192
+ features: { ...PROFILES.balanced.features },
193
+ limits: { ...PROFILES.balanced.limits },
194
+ mcp: { enabled: false },
195
+ usageMeasurement: { localCountersEnabled: false }
196
+ };
197
+ LIMIT_RANGES = {
198
+ maxMainFiles: { min: 1, max: 10 },
199
+ maxGlossaryTerms: { min: 0, max: 10 },
200
+ compactReportMaxSentences: { min: 4, max: 30 }
201
+ };
202
+ isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
203
+ hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
204
+ }
205
+ });
206
+
207
+ // ../../apps/local-admin/server/app.ts
208
+ init_schema();
209
+ import { randomUUID as randomUUID2 } from "node:crypto";
210
+ import { createServer } from "node:http";
211
+ import { promises as fs7 } from "node:fs";
212
+ import path5 from "node:path";
213
+
214
+ // ../../apps/local-admin/server/storage/files.ts
215
+ init_schema();
216
+ import { promises as fs, existsSync } from "node:fs";
217
+ import { randomUUID } from "node:crypto";
218
+ import path from "node:path";
219
+ function getStoragePaths(projectRoot) {
220
+ const preferredLocalDir = path.join(projectRoot, ".jutell-local");
221
+ const legacyLocalDir = path.join(projectRoot, ".beginner-bridge-local");
222
+ const localDir = existsSync(preferredLocalDir) || !existsSync(legacyLocalDir) ? preferredLocalDir : legacyLocalDir;
223
+ return {
224
+ projectRoot,
225
+ configFile: path.join(projectRoot, ".jutell.json"),
226
+ legacyConfigFile: path.join(projectRoot, ".beginner-bridge.json"),
227
+ localDir,
228
+ legacyLocalDir,
229
+ feedbackFile: path.join(localDir, "beta-feedback.json"),
230
+ historyFile: path.join(localDir, "settings-history.json"),
231
+ metadataFile: path.join(localDir, "metadata.json"),
232
+ usageCountersFile: path.join(localDir, "usage-counters.json"),
233
+ usageExperimentsFile: path.join(localDir, "usage-experiments.json")
234
+ };
235
+ }
236
+ var storageInitialization = /* @__PURE__ */ new Map();
237
+ var fileWrites = /* @__PURE__ */ new Map();
238
+ async function initializeStorage(paths) {
239
+ await fs.mkdir(paths.localDir, { recursive: true });
240
+ for (const [file, fallback] of [
241
+ [paths.feedbackFile, []],
242
+ [paths.historyFile, []],
243
+ [paths.metadataFile, { configVersion: 1, skillVersion: "not-recorded" }]
244
+ ]) {
245
+ try {
246
+ await fs.access(file);
247
+ } catch (error2) {
248
+ if (error2.code !== "ENOENT") throw error2;
249
+ await writeJsonAtomically(file, fallback);
250
+ }
251
+ }
252
+ }
253
+ function ensureStorage(paths) {
254
+ const existing = storageInitialization.get(paths.localDir);
255
+ if (existing) return existing;
256
+ const initialization = initializeStorage(paths).catch((error2) => {
257
+ storageInitialization.delete(paths.localDir);
258
+ throw error2;
259
+ });
260
+ storageInitialization.set(paths.localDir, initialization);
261
+ return initialization;
262
+ }
263
+ async function readJson(file) {
264
+ const text = await fs.readFile(file, "utf8");
265
+ return JSON.parse(text);
266
+ }
267
+ async function readConfig(paths) {
268
+ try {
269
+ const configFile = await fs.access(paths.configFile).then(() => paths.configFile).catch(() => paths.legacyConfigFile);
270
+ const result = validateConfig(await readJson(configFile));
271
+ if (result.ok) return { config: result.value, fallback: false };
272
+ return { config: await readDefaultConfig(), fallback: true, warning: result.error };
273
+ } catch {
274
+ return { config: await readDefaultConfig(), fallback: true, warning: "\uC124\uC815 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC5B4 \uAE30\uBCF8 \uC124\uC815\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4." };
275
+ }
276
+ }
277
+ async function readDefaultConfig() {
278
+ const { DEFAULT_CONFIG: DEFAULT_CONFIG2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
279
+ return structuredClone(DEFAULT_CONFIG2);
280
+ }
281
+ async function writeJsonAtomicallyUnlocked(file, value) {
282
+ const directory = path.dirname(file);
283
+ await fs.mkdir(directory, { recursive: true });
284
+ const tempFile = path.join(directory, `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
285
+ await fs.writeFile(tempFile, JSON.stringify(value, null, 2) + "\n", "utf8");
286
+ try {
287
+ JSON.parse(await fs.readFile(tempFile, "utf8"));
288
+ const backupFile = `${file}.previous`;
289
+ try {
290
+ await fs.rm(backupFile, { force: true });
291
+ } catch {
292
+ }
293
+ try {
294
+ await fs.rename(file, backupFile);
295
+ } catch {
296
+ }
297
+ try {
298
+ await fs.rename(tempFile, file);
299
+ await fs.rm(backupFile, { force: true });
300
+ } catch (error2) {
301
+ try {
302
+ await fs.rm(file, { force: true });
303
+ } catch {
304
+ }
305
+ try {
306
+ await fs.rename(backupFile, file);
307
+ } catch {
308
+ }
309
+ throw error2;
310
+ }
311
+ } finally {
312
+ await fs.rm(tempFile, { force: true });
313
+ }
314
+ }
315
+ function writeJsonAtomically(file, value) {
316
+ const previous = fileWrites.get(file) ?? Promise.resolve();
317
+ const write = previous.then(() => writeJsonAtomicallyUnlocked(file, value), () => writeJsonAtomicallyUnlocked(file, value));
318
+ const tracked = write.finally(() => {
319
+ if (fileWrites.get(file) === tracked) fileWrites.delete(file);
320
+ });
321
+ fileWrites.set(file, tracked);
322
+ return tracked;
323
+ }
324
+ async function readArray(file) {
325
+ const value = await readJson(file);
326
+ if (!Array.isArray(value)) throw new Error("\uC800\uC7A5\uB41C \uBAA9\uB85D \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
327
+ return value;
328
+ }
329
+ async function saveFeedback(paths, items) {
330
+ await writeJsonAtomically(paths.feedbackFile, items);
331
+ }
332
+
333
+ // ../../apps/local-admin/server/validation/feedback.ts
334
+ init_schema();
335
+ var TASK_TYPES = ["screen", "feature", "docs", "settings", "ui-ux-beta", "other"];
336
+ var LENGTHES = ["short", "appropriate", "long"];
337
+ var UNDERSTANDABLE = ["yes", "partial", "no"];
338
+ var REUSE = ["yes", "no", "unknown"];
339
+ var SEVERITIES = ["low", "medium", "high"];
340
+ var STATUSES = ["noted", "needs_reproduction", "planned", "improving", "confirmed"];
341
+ var requiredKeys = [
342
+ "date",
343
+ "projectAlias",
344
+ "taskType",
345
+ "profile",
346
+ "activeFeatures",
347
+ "perceivedLength",
348
+ "understandable",
349
+ "mostUsefulFeature",
350
+ "unnecessaryFeature",
351
+ "missingInfo",
352
+ "inaccurateContent",
353
+ "reuseConfig",
354
+ "improvementIdea",
355
+ "severity",
356
+ "status"
357
+ ];
358
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
359
+ function validateFeedback(input) {
360
+ if (!isRecord2(input)) return { ok: false, error: "\uD53C\uB4DC\uBC31\uC740 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
361
+ if (Object.keys(input).some((key) => !requiredKeys.includes(key))) return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD53C\uB4DC\uBC31 \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4." };
362
+ if (requiredKeys.some((key) => !(key in input))) return { ok: false, error: "\uD544\uC218 \uD53C\uB4DC\uBC31 \uD56D\uBAA9\uC774 \uBE60\uC84C\uC2B5\uB2C8\uB2E4." };
363
+ const strings = ["projectAlias", "mostUsefulFeature", "unnecessaryFeature", "missingInfo", "inaccurateContent", "improvementIdea"];
364
+ if (typeof input.date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(input.date)) return { ok: false, error: "\uB0A0\uC9DC \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
365
+ if (strings.some((key) => typeof input[key] !== "string" || String(input[key]).length > 2e3)) return { ok: false, error: "\uD53C\uB4DC\uBC31 \uAE00\uC790 \uC218\uB098 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
366
+ if (!String(input.projectAlias).trim()) return { ok: false, error: "\uC775\uBA85 \uD504\uB85C\uC81D\uD2B8 \uBCC4\uCE6D\uC744 \uC785\uB825\uD558\uC138\uC694." };
367
+ if (!TASK_TYPES.includes(String(input.taskType))) return { ok: false, error: "\uC791\uC5C5 \uC885\uB958\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
368
+ if (!Object.keys(PROFILES).includes(String(input.profile))) return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 Profile\uC785\uB2C8\uB2E4." };
369
+ if (!Array.isArray(input.activeFeatures) || input.activeFeatures.some((id) => typeof id !== "string" || !FEATURE_IDS.includes(id))) {
370
+ return { ok: false, error: "\uD65C\uC131 Feature \uBAA9\uB85D\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
371
+ }
372
+ if (!LENGTHES.includes(String(input.perceivedLength))) return { ok: false, error: "\uBCF4\uACE0\uC11C \uAE38\uC774 \uC120\uD0DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
373
+ if (!UNDERSTANDABLE.includes(String(input.understandable))) return { ok: false, error: "\uC774\uD574\uD558\uAE30 \uC26C\uC6E0\uB294\uC9C0 \uC120\uD0DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
374
+ if (!REUSE.includes(String(input.reuseConfig))) return { ok: false, error: "\uC7AC\uC0AC\uC6A9 \uC758\uD5A5 \uC120\uD0DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
375
+ if (!SEVERITIES.includes(String(input.severity))) return { ok: false, error: "\uC2EC\uAC01\uB3C4 \uC120\uD0DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
376
+ if (!STATUSES.includes(String(input.status))) return { ok: false, error: "\uC870\uCE58 \uC0C1\uD0DC \uC120\uD0DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
377
+ return { ok: true, value: input };
378
+ }
379
+
380
+ // ../../apps/local-admin/server/mcp/config.ts
381
+ import { promises as fs2 } from "node:fs";
382
+ import os from "node:os";
383
+ import path2 from "node:path";
384
+ import { spawnSync } from "node:child_process";
385
+ var BEGIN_MARKER = "# JUTELL_MCP_BEGIN";
386
+ var END_MARKER = "# JUTELL_MCP_END";
387
+ var LEGACY_BEGIN_MARKER = "# BEGINNER_BRIDGE_MCP_BEGIN";
388
+ var LEGACY_END_MARKER = "# BEGINNER_BRIDGE_MCP_END";
389
+ function codexHome() {
390
+ const override = process.env.CODEX_HOME;
391
+ return path2.resolve(override && override.trim() ? override : path2.join(os.homedir(), ".codex"));
392
+ }
393
+ var OPENCODE_BEGIN_MARKER = "BEGIN JUTELL MANAGED BLOCK";
394
+ var OPENCODE_END_MARKER = "END JUTELL MANAGED BLOCK";
395
+ var OPENCODE_MCP_KEY = "jutell";
396
+ var LEGACY_OPENCODE_MCP_KEY = "beginner_bridge";
397
+ function providerDetected(id) {
398
+ if (id !== "codex" && id !== "opencode" && id !== "claude-code" && id !== "claude") return false;
399
+ const map = { codex: "codex.cmd", opencode: "opencode.cmd", "claude-code": "claude.cmd", claude: "claude.cmd" };
400
+ const executable = map[id] ?? `${id}.cmd`;
401
+ const result = spawnSync(process.platform === "win32" ? executable : executable.replace(".cmd", ""), ["--version"], { stdio: "ignore", windowsHide: true });
402
+ return result.status === 0;
403
+ }
404
+ function claudeHome() {
405
+ const override = process.env.CLAUDE_CONFIG_DIR;
406
+ return path2.resolve(override && override.trim() ? override : path2.join(os.homedir()));
407
+ }
408
+ function hasJuTellMcpEvidence(content, key) {
409
+ const keyPattern = new RegExp(`^\\s*\\[mcp_servers\\.${key}\\]\\s*$`, "m");
410
+ const m = content.match(keyPattern);
411
+ if (!m || m.index === void 0) return false;
412
+ return /(?:assets|apps)[\\/]mcp-server/i.test(content.slice(m.index, m.index + 1200));
413
+ }
414
+ function filePath(_projectRoot) {
415
+ return path2.join(codexHome(), "config.toml");
416
+ }
417
+ function configuredServerEntry() {
418
+ const configured = process.env.BEGINNER_BRIDGE_MCP_SERVER;
419
+ return configured && path2.isAbsolute(configured) ? configured : void 0;
420
+ }
421
+ function buildMcpConfigBlock(enabled) {
422
+ const serverEntry2 = configuredServerEntry();
423
+ return [
424
+ BEGIN_MARKER,
425
+ "[mcp_servers.jutell]",
426
+ 'command = "node"',
427
+ `args = [${JSON.stringify(serverEntry2 ?? "apps/mcp-server/dist/index.js")}]`,
428
+ // No `cwd` here: this block is written to Codex's global config, shared
429
+ // across every project, so it must not be pinned to one project's dir.
430
+ `enabled = ${enabled ? "true" : "false"}`,
431
+ "required = false",
432
+ 'default_tools_approval_mode = "prompt"',
433
+ END_MARKER
434
+ ].join("\n");
435
+ }
436
+ async function readConfigFile(projectRoot) {
437
+ try {
438
+ return await fs2.readFile(filePath(projectRoot), "utf8");
439
+ } catch {
440
+ return "";
441
+ }
442
+ }
443
+ async function readCodexMcpRegistration(projectRoot, enabled = false) {
444
+ const text = await readConfigFile(projectRoot);
445
+ const canonicalBlock = text.match(new RegExp(`${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`, "m"))?.[0] ?? "";
446
+ const legacyBlock = text.match(new RegExp(`${LEGACY_BEGIN_MARKER}[\\s\\S]*?${LEGACY_END_MARKER}`, "m"))?.[0] ?? "";
447
+ const canonicalRegistered = /^\s*\[mcp_servers\.jutell\]\s*$/m.test(text);
448
+ const legacyRegistered = /^\s*\[mcp_servers\.beginner_bridge\]\s*$/m.test(text);
449
+ const canonicalHeuristic = !canonicalBlock && canonicalRegistered && hasJuTellMcpEvidence(text, "jutell");
450
+ const legacyHeuristic = !legacyBlock && legacyRegistered && hasJuTellMcpEvidence(text, "beginner_bridge");
451
+ const registered = Boolean(canonicalBlock || legacyBlock || canonicalHeuristic || legacyHeuristic);
452
+ const conflict = !registered && (canonicalRegistered || legacyRegistered);
453
+ const enabledFlag = (() => {
454
+ if (canonicalBlock) return /^\s*enabled\s*=\s*true\s*$/m.test(canonicalBlock);
455
+ if (canonicalHeuristic) {
456
+ const m = text.match(/^\s*\[mcp_servers\.jutell\]\s*$/m);
457
+ if (m && m.index !== void 0) return /^\s*enabled\s*=\s*true\s*$/m.test(text.slice(m.index, m.index + 1200));
458
+ }
459
+ if (legacyBlock) return /^\s*enabled\s*=\s*true\s*$/m.test(legacyBlock);
460
+ if (legacyHeuristic) {
461
+ const m = text.match(/^\s*\[mcp_servers\.beginner_bridge\]\s*$/m);
462
+ if (m && m.index !== void 0) return /^\s*enabled\s*=\s*true\s*$/m.test(text.slice(m.index, m.index + 1200));
463
+ }
464
+ return false;
465
+ })();
466
+ return { path: filePath(projectRoot), exists: Boolean(text), registered, conflict, enabled: enabledFlag, canonicalRegistered, legacyRegistered, bothRegistered: canonicalRegistered && legacyRegistered, preview: buildMcpConfigBlock(enabled) };
467
+ }
468
+ async function backup(file) {
469
+ try {
470
+ await fs2.copyFile(file, `${file}.previous`);
471
+ } catch {
472
+ }
473
+ }
474
+ function replaceManagedBlock(text, block) {
475
+ const pattern = new RegExp(`${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`, "m");
476
+ return text.replace(pattern, block).replace(/[ \t]*\n{3,}/g, "\n\n").trimEnd() + "\n";
477
+ }
478
+ async function registerCodexMcp(projectRoot, enabled) {
479
+ const file = filePath(projectRoot);
480
+ const current = await readConfigFile(projectRoot);
481
+ const existing = await readCodexMcpRegistration(projectRoot, enabled);
482
+ if (existing.conflict) throw new Error("Codex \uC124\uC815\uC5D0 \uAC19\uC740 \uC774\uB984\uC758 \uAD00\uB9AC\uB418\uC9C0 \uC54A\uB294 MCP \uD56D\uBAA9\uC774 \uC788\uC5B4 \uC790\uB3D9 \uBCC0\uACBD\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
483
+ if (existing.legacyRegistered && !existing.canonicalRegistered) return existing;
484
+ if (existing.bothRegistered) return existing;
485
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
486
+ if (current) await backup(file);
487
+ const next = existing.registered ? replaceManagedBlock(current, buildMcpConfigBlock(enabled)) : `${current.trimEnd()}${current.trimEnd() ? "\n\n" : ""}${buildMcpConfigBlock(enabled)}
488
+ `;
489
+ await fs2.writeFile(file, next, "utf8");
490
+ return readCodexMcpRegistration(projectRoot, enabled);
491
+ }
492
+ async function removeCodexMcp(projectRoot) {
493
+ const file = filePath(projectRoot);
494
+ const current = await readConfigFile(projectRoot);
495
+ const existing = await readCodexMcpRegistration(projectRoot);
496
+ if (existing.conflict) throw new Error("\uAD00\uB9AC\uB418\uC9C0 \uC54A\uB294 \uAC19\uC740 \uC774\uB984\uC758 MCP \uD56D\uBAA9\uC740 \uC790\uB3D9\uC73C\uB85C \uC81C\uAC70\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
497
+ if (!existing.registered) return existing;
498
+ await backup(file);
499
+ const patterns = [
500
+ new RegExp(`\\n?${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}\\n?`, "m"),
501
+ new RegExp(`\\n?${LEGACY_BEGIN_MARKER}[\\s\\S]*?${LEGACY_END_MARKER}\\n?`, "m")
502
+ ];
503
+ const next = patterns.reduce((value, pattern) => value.replace(pattern, ""), current).replace(/\n{3,}/g, "\n\n").trim();
504
+ await fs2.writeFile(file, next ? `${next}
505
+ ` : "", "utf8");
506
+ return readCodexMcpRegistration(projectRoot);
507
+ }
508
+ function openCodeFilePath(projectRoot) {
509
+ const jsonc = path2.join(projectRoot, "opencode.jsonc");
510
+ const json2 = path2.join(projectRoot, "opencode.json");
511
+ return jsonc;
512
+ }
513
+ async function readOpenCodeFile(projectRoot) {
514
+ for (const name of ["opencode.jsonc", "opencode.json"]) {
515
+ const file = path2.join(projectRoot, name);
516
+ try {
517
+ return { file, text: await fs2.readFile(file, "utf8") };
518
+ } catch {
519
+ }
520
+ }
521
+ return { file: openCodeFilePath(projectRoot), text: "" };
522
+ }
523
+ function stripJsoncComments(text) {
524
+ let out = "";
525
+ let inString = false;
526
+ let escaped = false;
527
+ let inLine = false;
528
+ let inBlock = false;
529
+ for (let i = 0; i < text.length; i += 1) {
530
+ const c = text[i];
531
+ const next = text[i + 1];
532
+ if (inBlock) {
533
+ if (c === "*" && next === "/") {
534
+ inBlock = false;
535
+ out += " ";
536
+ i += 1;
537
+ } else out += c === "\n" ? "\n" : " ";
538
+ continue;
539
+ }
540
+ if (inLine) {
541
+ if (c === "\n") {
542
+ inLine = false;
543
+ out += "\n";
544
+ } else out += " ";
545
+ continue;
546
+ }
547
+ if (inString) {
548
+ out += c;
549
+ if (escaped) escaped = false;
550
+ else if (c === "\\") escaped = true;
551
+ else if (c === '"') inString = false;
552
+ continue;
553
+ }
554
+ if (c === '"') {
555
+ inString = true;
556
+ out += c;
557
+ continue;
558
+ }
559
+ if (c === "/" && next === "/") {
560
+ inLine = true;
561
+ out += " ";
562
+ i += 1;
563
+ continue;
564
+ }
565
+ if (c === "/" && next === "*") {
566
+ inBlock = true;
567
+ out += " ";
568
+ i += 1;
569
+ continue;
570
+ }
571
+ out += c;
572
+ }
573
+ return out;
574
+ }
575
+ function tryParseJson(text) {
576
+ const trimmed = (text ?? "").trim();
577
+ if (!trimmed) return {};
578
+ try {
579
+ const cleaned = stripJsoncComments(trimmed).replace(/,\s*([}\]])/g, "$1");
580
+ const value = JSON.parse(cleaned);
581
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
582
+ } catch {
583
+ return void 0;
584
+ }
585
+ }
586
+ function buildOpenCodeBlock(enabled) {
587
+ const serverEntry2 = configuredServerEntry() ?? "apps/mcp-server/dist/index.js";
588
+ return JSON.stringify({ type: "local", command: [process.execPath, serverEntry2], enabled, cwd: "." }, null, 2);
589
+ }
590
+ function indentLines(value, prefix) {
591
+ return value.replace(/\n/g, `
592
+ ${prefix}`);
593
+ }
594
+ function serializeWithManaged(config, enabled) {
595
+ const copy = { ...config };
596
+ const rawMcp = copy.mcp && typeof copy.mcp === "object" && !Array.isArray(copy.mcp) ? copy.mcp : {};
597
+ const others = Object.entries(rawMcp).filter(([key]) => key !== OPENCODE_MCP_KEY);
598
+ const lines = ["{"];
599
+ for (const [key, value] of Object.entries(copy)) {
600
+ if (key === "mcp") continue;
601
+ lines.push(` ${JSON.stringify(key)}: ${indentLines(JSON.stringify(value, null, 2), " ")},`);
602
+ }
603
+ lines.push(' "mcp": {');
604
+ for (const [key, value] of others) {
605
+ lines.push(` ${JSON.stringify(key)}: ${indentLines(JSON.stringify(value, null, 2), " ")},`);
606
+ }
607
+ lines.push(` // ${OPENCODE_BEGIN_MARKER}`);
608
+ lines.push(` ${JSON.stringify(OPENCODE_MCP_KEY)}: ${indentLines(buildOpenCodeBlock(enabled), " ")},`);
609
+ lines.push(` // ${OPENCODE_END_MARKER}`);
610
+ lines.push(" }");
611
+ lines.push("}");
612
+ return lines.join("\n");
613
+ }
614
+ async function readOpenCodeRegistration(projectRoot, enabled = false) {
615
+ const { file, text } = await readOpenCodeFile(projectRoot);
616
+ const parsed = tryParseJson(text);
617
+ const rawMcp = parsed?.mcp && typeof parsed.mcp === "object" && !Array.isArray(parsed.mcp) ? parsed.mcp : {};
618
+ const canonicalEntry = rawMcp[OPENCODE_MCP_KEY];
619
+ const legacyEntry = rawMcp[LEGACY_OPENCODE_MCP_KEY];
620
+ const begin = text.indexOf(`// ${OPENCODE_BEGIN_MARKER}`);
621
+ const end = text.indexOf(`// ${OPENCODE_END_MARKER}`);
622
+ const managedText = begin >= 0 && end > begin ? text.slice(begin, end) : "";
623
+ const canonicalManaged = managedText.includes(JSON.stringify(OPENCODE_MCP_KEY));
624
+ const legacyManaged = managedText.includes(JSON.stringify(LEGACY_OPENCODE_MCP_KEY));
625
+ const canonicalRegistered = canonicalEntry !== void 0;
626
+ const legacyRegistered = legacyEntry !== void 0;
627
+ function isJuTellEntry(entry) {
628
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
629
+ const cmd = entry.command;
630
+ if (!Array.isArray(cmd)) return false;
631
+ return cmd.some((c) => typeof c === "string" && /(?:assets|apps)[\\/]mcp-server/i.test(c));
632
+ }
633
+ const canonicalHeuristic = !canonicalManaged && canonicalRegistered && isJuTellEntry(canonicalEntry);
634
+ const legacyHeuristic = !legacyManaged && legacyRegistered && isJuTellEntry(legacyEntry);
635
+ const registered = canonicalManaged || legacyManaged || canonicalHeuristic || legacyHeuristic;
636
+ const conflict = !registered && (canonicalRegistered || legacyRegistered);
637
+ const enabledFlag = (() => {
638
+ if (canonicalManaged && canonicalEntry && typeof canonicalEntry === "object") return canonicalEntry.enabled === true;
639
+ if (canonicalHeuristic && canonicalEntry && typeof canonicalEntry === "object") return canonicalEntry.enabled === true;
640
+ if (legacyManaged && legacyEntry && typeof legacyEntry === "object") return legacyEntry.enabled === true;
641
+ if (legacyHeuristic && legacyEntry && typeof legacyEntry === "object") return legacyEntry.enabled === true;
642
+ return false;
643
+ })();
644
+ return { file, exists: text.trim().length > 0, registered, conflict, enabled: enabledFlag, canonicalRegistered, legacyRegistered, bothRegistered: canonicalRegistered && legacyRegistered, preview: serializeWithManaged(parsed ?? {}, enabled) };
645
+ }
646
+ async function writeOpenCodeFile(file, text) {
647
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
648
+ await fs2.writeFile(file, text, "utf8");
649
+ }
650
+ async function registerOpenCodeMcp(projectRoot, enabled) {
651
+ const current = await readOpenCodeRegistration(projectRoot, enabled);
652
+ if (current.conflict) throw new Error("OpenCode \uC124\uC815\uC5D0 \uAC19\uC740 \uC774\uB984\uC758 \uAD00\uB9AC\uB418\uC9C0 \uC54A\uB294 MCP \uD56D\uBAA9\uC774 \uC788\uC5B4 \uC790\uB3D9 \uBCC0\uACBD\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
653
+ if (current.legacyRegistered && !current.canonicalRegistered) return current;
654
+ if (current.bothRegistered) return current;
655
+ const { file, text } = await readOpenCodeFile(projectRoot);
656
+ const parsed = tryParseJson(text);
657
+ if (text.trim() && !parsed) throw new Error("OpenCode \uC124\uC815 \uD30C\uC77C\uC744 \uC77D\uC9C0 \uBABB\uD574 \uC790\uB3D9 \uBCC0\uACBD\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
658
+ await backup(file);
659
+ await writeOpenCodeFile(file, `${serializeWithManaged(parsed ?? { $schema: "https://opencode.ai/config.json" }, enabled)}
660
+ `);
661
+ return readOpenCodeRegistration(projectRoot, enabled);
662
+ }
663
+ async function setOpenCodeMcpEnabled(projectRoot, enabled) {
664
+ const current = await readOpenCodeRegistration(projectRoot, enabled);
665
+ if (current.conflict || !current.registered) return current;
666
+ if (current.legacyRegistered && !current.canonicalRegistered) return current;
667
+ if (current.bothRegistered) return current;
668
+ const { text } = await readOpenCodeFile(projectRoot);
669
+ const parsed = tryParseJson(text);
670
+ if (!parsed) throw new Error("OpenCode \uC124\uC815 \uD30C\uC77C\uC744 \uC77D\uC9C0 \uBABB\uD574 \uC790\uB3D9 \uBCC0\uACBD\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
671
+ await backup(current.file);
672
+ await writeOpenCodeFile(current.file, `${serializeWithManaged(parsed, enabled)}
673
+ `);
674
+ return readOpenCodeRegistration(projectRoot, enabled);
675
+ }
676
+ function claudeConfigFile() {
677
+ return path2.join(claudeHome(), ".claude.json");
678
+ }
679
+ async function readClaudeMcpRegistration(projectRoot, _enabled = false) {
680
+ const file = claudeConfigFile();
681
+ let text = "";
682
+ try {
683
+ text = await fs2.readFile(file, "utf8");
684
+ } catch {
685
+ text = "";
686
+ }
687
+ let parsed;
688
+ try {
689
+ parsed = text.trim() ? JSON.parse(text) : {};
690
+ } catch {
691
+ parsed = void 0;
692
+ }
693
+ if (!parsed) return { file, claudeScope: "local", exists: Boolean(text), registered: false, conflict: false, enabled: false, canonicalRegistered: false, legacyRegistered: false, bothRegistered: false, preview: JSON.stringify({ scope: "local", type: "stdio", command: "node", args: ["assets/mcp-server/index.js"] }, null, 2) };
694
+ const projects = parsed.projects;
695
+ const normalizedRoot = projectRoot.replace(/\\/g, "/");
696
+ let mcp;
697
+ for (const [k, v] of Object.entries(projects ?? {})) {
698
+ if (k.replace(/\\/g, "/").toLowerCase() === normalizedRoot.toLowerCase()) {
699
+ const entry2 = v;
700
+ const servers = entry2.mcpServers;
701
+ mcp = servers;
702
+ break;
703
+ }
704
+ }
705
+ const topMcp = parsed.mcpServers;
706
+ const entry = mcp?.jutell ?? topMcp?.jutell;
707
+ const registered = entry !== void 0;
708
+ return { file, claudeScope: "local", exists: Boolean(text), registered, conflict: false, enabled: registered, canonicalRegistered: registered, legacyRegistered: false, bothRegistered: false, preview: JSON.stringify({ scope: "local", type: "stdio", command: "node", args: ["assets/mcp-server/index.js"] }, null, 2) };
709
+ }
710
+
711
+ // ../../apps/local-admin/server/mcp/runtime.ts
712
+ import { spawn } from "node:child_process";
713
+ import { promises as fs3 } from "node:fs";
714
+ import path3 from "node:path";
715
+ var child;
716
+ var state = { state: "stopped" };
717
+ function serverEntry(projectRoot) {
718
+ const configured = process.env.BEGINNER_BRIDGE_MCP_SERVER;
719
+ return configured && path3.isAbsolute(configured) ? configured : path3.join(projectRoot, "apps", "mcp-server", "dist", "index.js");
720
+ }
721
+ function getMcpRuntimeState() {
722
+ return { ...state };
723
+ }
724
+ async function startMcpRuntime(projectRoot) {
725
+ if (child && state.state === "running") return getMcpRuntimeState();
726
+ try {
727
+ await fs3.access(serverEntry(projectRoot));
728
+ } catch {
729
+ throw new Error("MCP \uC11C\uBC84 \uBE4C\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 apps/mcp-server\uC5D0\uC11C npm run build\uB97C \uC2E4\uD589\uD558\uC138\uC694.");
730
+ }
731
+ state = { state: "starting" };
732
+ const processHandle = spawn(process.execPath, [serverEntry(projectRoot)], {
733
+ cwd: projectRoot,
734
+ stdio: ["pipe", "ignore", "ignore"],
735
+ windowsHide: true
736
+ });
737
+ child = processHandle;
738
+ processHandle.once("spawn", () => {
739
+ state = { state: "running" };
740
+ });
741
+ processHandle.once("error", () => {
742
+ child = void 0;
743
+ state = { state: "error", error: "MCP \uC11C\uBC84\uB97C \uC2DC\uC791\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." };
744
+ });
745
+ processHandle.once("exit", (code) => {
746
+ child = void 0;
747
+ if (state.state !== "error") state = code === 0 ? { state: "stopped" } : { state: "error", error: "MCP \uC11C\uBC84\uAC00 \uC608\uAE30\uCE58 \uC54A\uAC8C \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4." };
748
+ });
749
+ await new Promise((resolve) => setTimeout(resolve, 25));
750
+ return getMcpRuntimeState();
751
+ }
752
+ async function stopMcpRuntime() {
753
+ if (!child) {
754
+ state = { state: "stopped" };
755
+ return getMcpRuntimeState();
756
+ }
757
+ child.stdin?.end();
758
+ child.kill();
759
+ child = void 0;
760
+ state = { state: "stopped" };
761
+ return getMcpRuntimeState();
762
+ }
763
+
764
+ // ../../apps/local-admin/server/templates/request-templates.ts
765
+ import { promises as fs4 } from "node:fs";
766
+ import path4 from "node:path";
767
+ var REQUEST_TEMPLATE_DIR = "templates/request-builder";
768
+ var REQUEST_TEMPLATE_LIST = [
769
+ { name: "README.md", description: "\uC0AC\uC6A9\uBC95\uACFC \uACF5\uD1B5 \uC120\uD0DD\uC9C0 \uC124\uBA85" },
770
+ { name: "DESIGN_REQUEST.md", description: "\uD654\uBA74 \uBAA8\uC591\uC774\uB098 \uBD84\uC704\uAE30 \uBCC0\uACBD" },
771
+ { name: "FEATURE_REQUEST.md", description: "\uC0C8 \uAE30\uB2A5\xB7\uD654\uBA74 \uCD94\uAC00" },
772
+ { name: "BUG_REPORT_REQUEST.md", description: "\uBB38\uC81C(\uBC84\uADF8) \uC2E0\uACE0" },
773
+ { name: "PROJECT_PLANNING_REQUEST.md", description: "\uD504\uB85C\uC81D\uD2B8 \uACC4\uD68D" },
774
+ { name: "CODE_REVIEW_REQUEST.md", description: "\uC791\uC5C5 \uAC80\uD1A0" },
775
+ { name: "NEXT_AGENT_HANDOFF.md", description: "\uB2E4\uC74C Agent\uC5D0\uAC8C \uC791\uC5C5 \uC804\uB2EC" },
776
+ { name: "MANUAL_EDIT_GUIDE.md", description: "\uC0AC\uC6A9\uC790 \uC9C1\uC811 \uC218\uC815 \uC548\uB0B4" }
777
+ ];
778
+ async function exists(file) {
779
+ try {
780
+ await fs4.access(file);
781
+ return true;
782
+ } catch {
783
+ return false;
784
+ }
785
+ }
786
+ async function findRequestTemplatesRoot(projectRoot) {
787
+ const candidates = [
788
+ { root: path4.join(projectRoot, REQUEST_TEMPLATE_DIR), source: "project" },
789
+ ...process.env.JUTELL_TEMPLATES_ROOT ? [{ root: process.env.JUTELL_TEMPLATES_ROOT, source: "assets" }] : []
790
+ ];
791
+ for (const candidate of candidates) {
792
+ if (await exists(path4.join(candidate.root, "README.md"))) return candidate;
793
+ }
794
+ return null;
795
+ }
796
+ async function readRequestTemplates(projectRoot) {
797
+ const found = await findRequestTemplatesRoot(projectRoot);
798
+ if (!found) return { templates: [], source: "unavailable" };
799
+ const templates = [];
800
+ for (const item of REQUEST_TEMPLATE_LIST) {
801
+ try {
802
+ templates.push({ ...item, content: await fs4.readFile(path4.join(found.root, item.name), "utf8") });
803
+ } catch {
804
+ }
805
+ }
806
+ return { templates, source: found.source };
807
+ }
808
+
809
+ // ../../apps/local-admin/server/usage/counters.ts
810
+ import { promises as fs5 } from "node:fs";
811
+ async function readUsageCounters(paths) {
812
+ try {
813
+ const value = await readJson(paths.usageCountersFile);
814
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { exists: true, corrupt: true, counters: null };
815
+ const record = value;
816
+ if (record.schemaVersion !== 1 || typeof record.enabled !== "boolean" || typeof record.totalToolCalls !== "number" || typeof record.tools !== "object" || record.tools === null || Array.isArray(record.tools)) {
817
+ return { exists: true, corrupt: true, counters: null };
818
+ }
819
+ return { exists: true, corrupt: false, counters: value };
820
+ } catch {
821
+ return { exists: false, corrupt: false, counters: null };
822
+ }
823
+ }
824
+ async function deleteUsageCounters(paths) {
825
+ await fs5.rm(paths.usageCountersFile, { force: true });
826
+ }
827
+ async function writeCounters(paths, value) {
828
+ await writeJsonAtomically(paths.usageCountersFile, value);
829
+ }
830
+ async function recordTemplateCopy(paths, template, taskType, profile) {
831
+ const current = await readUsageCounters(paths);
832
+ if (current.corrupt) return { ok: false, reason: "\uCE74\uC6B4\uD130 \uD30C\uC77C \uD615\uC2DD\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC5B4 \uBCF4\uC874\uD558\uACE0 \uAE30\uB85D\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." };
833
+ const base = current.counters ?? { schemaVersion: 1, enabled: true, updatedAt: null, totalToolCalls: 0, tools: {}, templateCopies: {} };
834
+ const copies = base.templateCopies ?? (base.templateCopies = {});
835
+ const item = copies[template] ?? { count: 0, byTaskType: {}, byProfile: {}, lastCopiedAt: null };
836
+ item.count += 1;
837
+ item.byTaskType[taskType] = (item.byTaskType[taskType] ?? 0) + 1;
838
+ item.byProfile[profile] = (item.byProfile[profile] ?? 0) + 1;
839
+ item.lastCopiedAt = (/* @__PURE__ */ new Date()).toISOString();
840
+ copies[template] = item;
841
+ base.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
842
+ await writeCounters(paths, base);
843
+ return { ok: true };
844
+ }
845
+
846
+ // ../../apps/local-admin/server/usage/experiments.ts
847
+ init_schema();
848
+ import { promises as fs6 } from "node:fs";
849
+ function emptyFile() {
850
+ const now = (/* @__PURE__ */ new Date()).toISOString();
851
+ return { schemaVersion: 1, createdAt: now, updatedAt: now, experiments: [] };
852
+ }
853
+ async function readFile(paths) {
854
+ try {
855
+ const value = await readJson(paths.usageExperimentsFile);
856
+ if (!value || typeof value !== "object" || Array.isArray(value)) return emptyFile();
857
+ const record = value;
858
+ if (record.schemaVersion !== 1 || !Array.isArray(record.experiments)) return emptyFile();
859
+ return value;
860
+ } catch {
861
+ return emptyFile();
862
+ }
863
+ }
864
+ async function writeFile(paths, file) {
865
+ await writeJsonAtomically(paths.usageExperimentsFile, file);
866
+ }
867
+ function isRecord3(value) {
868
+ return typeof value === "object" && value !== null && !Array.isArray(value);
869
+ }
870
+ function validateField(key, value) {
871
+ if (key === "title") {
872
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 100) return { ok: false, error: "\uC2E4\uD5D8 \uC774\uB984\uC740 1~100\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
873
+ return { ok: true };
874
+ }
875
+ if (key === "status") {
876
+ if (value !== "in_progress" && value !== "completed") return { ok: false, error: "status\uB294 in_progress \uB610\uB294 completed\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
877
+ return { ok: true };
878
+ }
879
+ if (key === "profile") {
880
+ if (value !== "minimal" && value !== "balanced" && value !== "learning" && value !== "detailed") return { ok: false, error: "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 Profile\uC785\uB2C8\uB2E4." };
881
+ return { ok: true };
882
+ }
883
+ if (key === "features") {
884
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !FEATURE_IDS.includes(item))) return { ok: false, error: "\uD65C\uC131 Feature \uBAA9\uB85D\uC5D0 \uACF5\uC2DD Feature ID\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." };
885
+ if (new Set(value).size !== value.length) return { ok: false, error: "\uD65C\uC131 Feature \uBAA9\uB85D\uC5D0 \uC911\uBCF5\uC774 \uC788\uC2B5\uB2C8\uB2E4." };
886
+ return { ok: true };
887
+ }
888
+ if (key === "environment") {
889
+ if (!isRecord3(value) || value.provider !== "codex" && value.provider !== "opencode" && value.provider !== "other" || typeof value.mcpEnabled !== "boolean" || typeof value.skillEnabled !== "boolean") {
890
+ return { ok: false, error: "\uD658\uACBD \uC815\uBCF4\uB294 provider(\uCF54\uB4DC\uB2C9\uC2A4/OpenCode/\uAE30\uD0C0)\uC640 MCP\xB7Skill \uC0AC\uC6A9 \uC5EC\uBD80\uB9CC \uC800\uC7A5\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." };
891
+ }
892
+ return { ok: true };
893
+ }
894
+ if (key === "measurement") {
895
+ if (!isRecord3(value)) return { ok: false, error: "\uCE21\uC815 \uC815\uBCF4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
896
+ for (const field of ["toolCalls", "responseCharacters", "estimatedTokens", "durationMs"]) {
897
+ const number = value[field];
898
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 0) return { ok: false, error: `${field}\uB294 0 \uC774\uC0C1\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.` };
899
+ }
900
+ return { ok: true };
901
+ }
902
+ if (key === "evaluation") {
903
+ if (!isRecord3(value)) return { ok: false, error: "\uD3C9\uAC00 \uC815\uBCF4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." };
904
+ for (const field of ["understanding", "readability", "accuracy", "overall"]) {
905
+ const number = value[field];
906
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > 5) return { ok: false, error: `${field}\uB294 1~5 \uC0AC\uC774\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.` };
907
+ }
908
+ return { ok: true };
909
+ }
910
+ if (key === "issues") {
911
+ if (!Array.isArray(value) || value.length > 20 || value.some((item) => typeof item !== "string" || item.length === 0 || item.length > 200)) return { ok: false, error: "\uBB38\uC81C \uBAA9\uB85D\uC740 20\uAC1C \uC774\uD558, \uAC01 1~200\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
912
+ return { ok: true };
913
+ }
914
+ if (key === "decision") {
915
+ if (typeof value !== "string" || value.length > 500) return { ok: false, error: "\uCD5C\uC885 \uACB0\uC815\uC740 500\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
916
+ return { ok: true };
917
+ }
918
+ if (key === "notes") {
919
+ if (typeof value !== "string" || value.length > 500) return { ok: false, error: "\uC6B4\uC601\uC790 \uBA54\uBAA8\uB294 500\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4." };
920
+ return { ok: true };
921
+ }
922
+ return { ok: false, error: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC2E4\uD5D8 \uD56D\uBAA9\uC785\uB2C8\uB2E4: ${key}` };
923
+ }
924
+ function validatePartial(input) {
925
+ for (const [key, value] of Object.entries(input)) {
926
+ const result = validateField(key, value);
927
+ if (!result.ok) return result;
928
+ }
929
+ return { ok: true, value: input };
930
+ }
931
+ async function readUsageExperiments(paths) {
932
+ const file = await readFile(paths);
933
+ return { summary: summarize(file), experiments: file.experiments };
934
+ }
935
+ async function createUsageExperiment(paths, input) {
936
+ const validated = validatePartial(input);
937
+ if (!validated.ok) return validated;
938
+ const file = await readFile(paths);
939
+ const maxId = file.experiments.reduce((max, item) => {
940
+ const number = Number(item.id.replace(/^EXP-/, ""));
941
+ return Number.isInteger(number) && number > max ? number : max;
942
+ }, 0);
943
+ const now = (/* @__PURE__ */ new Date()).toISOString();
944
+ const experiment = {
945
+ id: `EXP-${String(maxId + 1).padStart(3, "0")}`,
946
+ title: typeof validated.value.title === "string" ? validated.value.title : "\uC81C\uBAA9 \uC5C6\uB294 \uC2E4\uD5D8",
947
+ status: validated.value.status ?? "in_progress",
948
+ profile: validated.value.profile ?? "balanced",
949
+ features: validated.value.features ?? [],
950
+ environment: validated.value.environment ?? { provider: "codex", mcpEnabled: false, skillEnabled: true },
951
+ issues: validated.value.issues ?? [],
952
+ decision: validated.value.decision ?? "",
953
+ notes: validated.value.notes ?? "",
954
+ createdAt: now,
955
+ updatedAt: now,
956
+ ...validated.value.measurement ? { measurement: validated.value.measurement } : {},
957
+ ...validated.value.evaluation ? { evaluation: validated.value.evaluation } : {}
958
+ };
959
+ file.experiments.push(experiment);
960
+ file.updatedAt = now;
961
+ await writeFile(paths, file);
962
+ return { ok: true, experiment };
963
+ }
964
+ async function updateUsageExperiment(paths, id, input) {
965
+ const validated = validatePartial(input);
966
+ if (!validated.ok) return validated;
967
+ const file = await readFile(paths);
968
+ const index = file.experiments.findIndex((item) => item.id === id);
969
+ if (index < 0) return { ok: false, error: "\uC2E4\uD5D8 \uAE30\uB85D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." };
970
+ const current = file.experiments[index];
971
+ const now = (/* @__PURE__ */ new Date()).toISOString();
972
+ const next = {
973
+ ...current,
974
+ title: validated.value.title !== void 0 ? validated.value.title : current.title,
975
+ status: validated.value.status !== void 0 ? validated.value.status : current.status,
976
+ profile: validated.value.profile !== void 0 ? validated.value.profile : current.profile,
977
+ features: validated.value.features !== void 0 ? validated.value.features : current.features,
978
+ environment: validated.value.environment !== void 0 ? validated.value.environment : current.environment,
979
+ issues: validated.value.issues !== void 0 ? validated.value.issues : current.issues,
980
+ decision: validated.value.decision !== void 0 ? validated.value.decision : current.decision,
981
+ notes: validated.value.notes !== void 0 ? validated.value.notes : current.notes,
982
+ ...validated.value.measurement !== void 0 ? { measurement: validated.value.measurement } : current.measurement ? { measurement: current.measurement } : {},
983
+ ...validated.value.evaluation !== void 0 ? { evaluation: validated.value.evaluation } : current.evaluation ? { evaluation: current.evaluation } : {},
984
+ updatedAt: now
985
+ };
986
+ file.experiments[index] = next;
987
+ file.updatedAt = now;
988
+ await writeFile(paths, file);
989
+ return { ok: true, experiment: next };
990
+ }
991
+ async function deleteAllUsageExperiments(paths) {
992
+ await fs6.rm(paths.usageExperimentsFile, { force: true });
993
+ }
994
+ function average(values) {
995
+ if (values.length === 0) return null;
996
+ return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length * 10) / 10;
997
+ }
998
+ function summarize(file) {
999
+ const experiments = file.experiments;
1000
+ const evaluated = experiments.filter((item) => item.evaluation);
1001
+ const measured = experiments.filter((item) => item.measurement);
1002
+ const profileCounts = /* @__PURE__ */ new Map();
1003
+ const disabledCounts = /* @__PURE__ */ new Map();
1004
+ const issueCounts = /* @__PURE__ */ new Map();
1005
+ for (const item of experiments) {
1006
+ profileCounts.set(item.profile, (profileCounts.get(item.profile) ?? 0) + 1);
1007
+ for (const id of FEATURE_IDS) {
1008
+ if (!item.features.includes(id)) disabledCounts.set(id, (disabledCounts.get(id) ?? 0) + 1);
1009
+ }
1010
+ for (const issue of item.issues) issueCounts.set(issue, (issueCounts.get(issue) ?? 0) + 1);
1011
+ }
1012
+ const mostUsedProfile = profileCounts.size === 0 ? null : [...profileCounts.entries()].sort((a, b) => b[1] - a[1])[0][0];
1013
+ const mostDisabledFeatures = [...disabledCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id);
1014
+ const topIssues = [...issueCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([text, count]) => ({ text, count }));
1015
+ return {
1016
+ total: experiments.length,
1017
+ completed: experiments.filter((item) => item.status === "completed").length,
1018
+ inProgress: experiments.filter((item) => item.status === "in_progress").length,
1019
+ averageUnderstanding: average(evaluated.map((item) => item.evaluation.understanding)),
1020
+ averageReadability: average(evaluated.map((item) => item.evaluation.readability)),
1021
+ averageAccuracy: average(evaluated.map((item) => item.evaluation.accuracy)),
1022
+ averageResponseCharacters: average(measured.map((item) => item.measurement.responseCharacters)),
1023
+ averageToolCalls: average(measured.map((item) => item.measurement.toolCalls)),
1024
+ mostUsedProfile,
1025
+ mostDisabledFeatures,
1026
+ topIssues
1027
+ };
1028
+ }
1029
+
1030
+ // ../../apps/local-admin/server/app.ts
1031
+ var MAX_BODY_BYTES = 512 * 1024;
1032
+ var writeQueue = Promise.resolve();
1033
+ var lastMcpCheckAt = null;
1034
+ function withWriteLock(task) {
1035
+ const run = writeQueue.then(task, task);
1036
+ writeQueue = run.then(() => void 0, () => void 0);
1037
+ return run;
1038
+ }
1039
+ function json(res, status, body2) {
1040
+ const payload = JSON.stringify(body2);
1041
+ res.statusCode = status;
1042
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1043
+ res.setHeader("Cache-Control", "no-store");
1044
+ res.end(payload);
1045
+ }
1046
+ function error(res, status, message) {
1047
+ json(res, status, { error: message });
1048
+ }
1049
+ async function body(req) {
1050
+ let size = 0;
1051
+ const chunks = [];
1052
+ for await (const chunk of req) {
1053
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1054
+ size += buffer.length;
1055
+ if (size > MAX_BODY_BYTES) throw new Error("\uC694\uCCAD\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.");
1056
+ chunks.push(buffer);
1057
+ }
1058
+ if (size === 0) return {};
1059
+ try {
1060
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1061
+ } catch {
1062
+ throw new Error("JSON \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
1063
+ }
1064
+ }
1065
+ async function getHistory(paths) {
1066
+ return readArray(paths.historyFile);
1067
+ }
1068
+ async function getFeedback(paths) {
1069
+ return readArray(paths.feedbackFile);
1070
+ }
1071
+ function safeId(value) {
1072
+ return Boolean(value && /^[0-9a-f-]{36}$/i.test(value));
1073
+ }
1074
+ async function updateHistory(paths, entry) {
1075
+ const history = await getHistory(paths);
1076
+ const next = history.map((item) => item.id === entry.id ? entry : item);
1077
+ await writeJsonAtomically(paths.historyFile, next.slice(-100));
1078
+ }
1079
+ async function saveConfig(paths, next) {
1080
+ return withWriteLock(async () => {
1081
+ const current = (await readConfig(paths)).config;
1082
+ const changes = changedFields(current, next);
1083
+ if (changes.length === 0) return { config: next, changed: false, historyRecorded: true };
1084
+ const entry = { id: randomUUID2(), changedAt: (/* @__PURE__ */ new Date()).toISOString(), changedFields: changes, status: "pending" };
1085
+ const history = await getHistory(paths);
1086
+ await writeJsonAtomically(paths.historyFile, [...history, entry].slice(-100));
1087
+ try {
1088
+ await writeJsonAtomically(paths.configFile, next);
1089
+ } catch {
1090
+ await updateHistory(paths, { ...entry, status: "failed" });
1091
+ throw new Error("\uC124\uC815\uC744 \uC800\uC7A5\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uAE30\uC874 \uC124\uC815\uC744 \uC720\uC9C0\uD588\uC2B5\uB2C8\uB2E4.");
1092
+ }
1093
+ await updateHistory(paths, { ...entry, status: "applied" });
1094
+ return { config: next, changed: true, historyRecorded: true };
1095
+ });
1096
+ }
1097
+ async function readOverview(paths) {
1098
+ const result = await readConfig(paths);
1099
+ const history = await getHistory(paths);
1100
+ let metadata = {};
1101
+ try {
1102
+ metadata = await readJson(paths.metadataFile);
1103
+ } catch {
1104
+ }
1105
+ return {
1106
+ ...result,
1107
+ metadata,
1108
+ lastChangedAt: history.at(-1)?.changedAt ?? null
1109
+ };
1110
+ }
1111
+ async function exists2(file) {
1112
+ try {
1113
+ await fs7.access(file);
1114
+ return true;
1115
+ } catch {
1116
+ return false;
1117
+ }
1118
+ }
1119
+ async function readReadiness(projectRoot, paths) {
1120
+ const configExists = await exists2(paths.configFile) || await exists2(paths.legacyConfigFile);
1121
+ const agentsPath = path5.join(projectRoot, "AGENTS.md");
1122
+ const skillPath = path5.join(projectRoot, ".agents", "skills", "beginner-bridge", "SKILL.md");
1123
+ const [agentsExists, skillExists] = await Promise.all([exists2(agentsPath), exists2(skillPath)]);
1124
+ const agentsText = agentsExists ? await fs7.readFile(agentsPath, "utf8") : "";
1125
+ let safetyExists = false;
1126
+ if (skillExists) {
1127
+ try {
1128
+ const skill = await fs7.readFile(skillPath, "utf8");
1129
+ safetyExists = skill.includes("\uBE44\uBC00\uC815\uBCF4") && skill.includes("\uC911\uC694\uD55C \uBBF8\uD655\uC778 \uC0AC\uD56D");
1130
+ } catch {
1131
+ safetyExists = false;
1132
+ }
1133
+ }
1134
+ const config = await readConfig(paths);
1135
+ return {
1136
+ config: { exists: configExists, valid: configExists && !config.fallback, profile: config.config.profile, activeFeatures: Object.values(config.config.features).filter(Boolean).length },
1137
+ skill: { exists: skillExists },
1138
+ agents: { exists: agentsExists, jutellBlock: agentsText.includes("<!-- BEGIN JUTELL MANAGED BLOCK -->") && agentsText.includes("<!-- END JUTELL MANAGED BLOCK -->") },
1139
+ safetyRules: { exists: safetyExists },
1140
+ sessionApplied: "manual_check_required"
1141
+ };
1142
+ }
1143
+ async function readProviderStatuses(projectRoot, config) {
1144
+ const [codex, opencode, claude] = await Promise.all([
1145
+ readCodexMcpRegistration(projectRoot, config.mcp.enabled),
1146
+ readOpenCodeRegistration(projectRoot, config.mcp.enabled),
1147
+ readClaudeMcpRegistration(projectRoot, config.mcp.enabled)
1148
+ ]);
1149
+ const base = { lastCheckedAt: lastMcpCheckAt };
1150
+ return [
1151
+ { id: "codex", label: "Codex", status: "supported", detected: providerDetected("codex"), registered: codex.registered, conflict: codex.conflict, enabled: codex.enabled, ...base },
1152
+ { id: "opencode", label: "OpenCode", status: "beta", detected: providerDetected("opencode"), registered: opencode.registered, conflict: opencode.conflict, enabled: opencode.enabled, ...base },
1153
+ { id: "claude-code", label: "Claude Code", status: "beta", detected: providerDetected("claude-code"), registered: claude.registered, conflict: claude.conflict, enabled: claude.enabled, ...base },
1154
+ { id: "cline", label: "Cline", status: "planned", detected: false, registered: false, conflict: false, enabled: false, ...base }
1155
+ ];
1156
+ }
1157
+ async function readMcpStatus(projectRoot, paths) {
1158
+ const config = (await readConfig(paths)).config;
1159
+ const codex = await readCodexMcpRegistration(projectRoot, config.mcp.enabled);
1160
+ const preparation = codex.conflict ? "error" : !codex.registered ? "not_registered" : config.mcp.enabled ? "enabled" : "registered";
1161
+ return {
1162
+ settings: config.mcp,
1163
+ server: getMcpRuntimeState(),
1164
+ codex: { registered: codex.registered, path: codex.path, conflict: codex.conflict, enabled: codex.enabled },
1165
+ providers: await readProviderStatuses(projectRoot, config),
1166
+ preparation,
1167
+ connection: { state: "not_checked", lastCheckedAt: lastMcpCheckAt },
1168
+ skillFallback: { available: true, message: "MCP\uB97C \uB044\uAC70\uB098 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4\uB3C4 AGENTS.md, Skill, \uC124\uC815 \uD30C\uC77C \uBC29\uC2DD\uC740 \uACC4\uC18D \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." }
1169
+ };
1170
+ }
1171
+ async function providerRequest(input) {
1172
+ const value = input && typeof input === "object" ? input : {};
1173
+ const provider = value.provider;
1174
+ if (provider !== "codex" && provider !== "opencode" && provider !== "claude-code" && provider !== "claude") return { error: "\uC5F0\uACB0\uD560 Agent\uB97C codex, opencode \uB610\uB294 claude \uC911\uC5D0\uC11C \uC120\uD0DD\uD558\uC138\uC694." };
1175
+ if (value.confirm !== true) return { error: "Provider \uC5F0\uACB0 \uBCC0\uACBD \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4." };
1176
+ const normalized = provider === "claude" ? "claude-code" : provider;
1177
+ return { provider: normalized };
1178
+ }
1179
+ async function applyProviderAction(projectRoot, provider, action, config) {
1180
+ const opencode = await readOpenCodeRegistration(projectRoot, config.mcp.enabled);
1181
+ const codex = await readCodexMcpRegistration(projectRoot, config.mcp.enabled);
1182
+ if (action === "disconnect") {
1183
+ if (provider === "codex") await registerCodexMcp(projectRoot, false);
1184
+ else if (provider === "opencode") await setOpenCodeMcpEnabled(projectRoot, false);
1185
+ else return config;
1186
+ return config;
1187
+ }
1188
+ if (action === "default" && provider !== "codex" && codex.registered && codex.enabled) await registerCodexMcp(projectRoot, false);
1189
+ if (action === "default" && provider !== "opencode" && opencode.registered && opencode.enabled) await setOpenCodeMcpEnabled(projectRoot, false);
1190
+ if (provider === "codex") await registerCodexMcp(projectRoot, true);
1191
+ else if (provider === "opencode") await registerOpenCodeMcp(projectRoot, true);
1192
+ else return config;
1193
+ if (action === "connect" || action === "default") return { ...config, mcp: { ...config.mcp, enabled: true } };
1194
+ return config;
1195
+ }
1196
+ function confirmBody(input, message) {
1197
+ return Boolean(input && typeof input === "object" && input.confirm === true) ? null : message;
1198
+ }
1199
+ async function handleApiRequest(req, res, projectRoot) {
1200
+ const paths = getStoragePaths(projectRoot);
1201
+ try {
1202
+ await ensureStorage(paths);
1203
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1204
+ const method = req.method ?? "GET";
1205
+ if (!url.pathname.startsWith("/api/")) return error(res, 404, "\uC694\uCCAD\uD55C \uC8FC\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
1206
+ if (method === "GET" && url.pathname === "/api/config") return json(res, 200, await readOverview(paths));
1207
+ if (method === "GET" && url.pathname === "/api/request-templates") return json(res, 200, await readRequestTemplates(projectRoot));
1208
+ if (method === "GET" && url.pathname === "/api/usage-counters") return json(res, 200, await readUsageCounters(paths));
1209
+ if (method === "DELETE" && url.pathname === "/api/usage-counters") {
1210
+ const errorMessage = confirmBody(await body(req), "\uB85C\uCEEC \uC0AC\uC6A9\uB7C9 \uCE74\uC6B4\uD130 \uC0AD\uC81C \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1211
+ if (errorMessage) return error(res, 400, errorMessage);
1212
+ await deleteUsageCounters(paths);
1213
+ return json(res, 200, { counters: null });
1214
+ }
1215
+ if (method === "PATCH" && url.pathname === "/api/usage-settings") {
1216
+ const input = await body(req);
1217
+ if (!input || typeof input !== "object" || typeof input.localCountersEnabled !== "boolean") {
1218
+ return error(res, 400, "localCountersEnabled\uB294 ON \uB610\uB294 OFF\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
1219
+ }
1220
+ const config = (await readConfig(paths)).config;
1221
+ const saved = await saveConfig(paths, { ...config, usageMeasurement: { localCountersEnabled: input.localCountersEnabled } });
1222
+ return json(res, 200, { ...saved, ...await readOverview(paths) });
1223
+ }
1224
+ if (method === "GET" && url.pathname === "/api/usage-experiments") return json(res, 200, await readUsageExperiments(paths));
1225
+ if (method === "POST" && url.pathname === "/api/usage-experiments") {
1226
+ const result = await createUsageExperiment(paths, await body(req));
1227
+ if (!result.ok) return error(res, 400, result.error);
1228
+ return json(res, 201, { experiment: result.experiment, ...await readUsageExperiments(paths) });
1229
+ }
1230
+ if (method === "POST" && url.pathname === "/api/usage-experiments/template-copy") {
1231
+ const config = (await readConfig(paths)).config;
1232
+ if (!config.usageMeasurement.localCountersEnabled) return error(res, 409, "\uB85C\uCEEC \uC0AC\uC6A9\uB7C9 \uCE21\uC815\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uCF1C\uC57C \uD15C\uD50C\uB9BF \uBCF5\uC0AC \uD69F\uC218\uB97C \uAE30\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
1233
+ const input = await body(req);
1234
+ const record = input && typeof input === "object" ? input : {};
1235
+ const template = typeof record.template === "string" ? record.template : "";
1236
+ const taskType = typeof record.taskType === "string" && record.taskType.length > 0 && record.taskType.length <= 40 ? record.taskType : "\uAE30\uD0C0";
1237
+ if (!REQUEST_TEMPLATE_LIST.some((item) => item.name === template)) return error(res, 400, "\uACF5\uC2DD \uD15C\uD50C\uB9BF \uC774\uB984\uB9CC \uAE30\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
1238
+ const result = await recordTemplateCopy(paths, template, taskType, config.profile);
1239
+ if (!result.ok) return error(res, 409, result.reason);
1240
+ return json(res, 200, { recorded: true, counters: (await readUsageCounters(paths)).counters });
1241
+ }
1242
+ const experimentMatch = url.pathname.match(/^\/api\/usage-experiments\/(EXP-\d{3})$/);
1243
+ if (experimentMatch && method === "PATCH") {
1244
+ const result = await updateUsageExperiment(paths, experimentMatch[1], await body(req));
1245
+ if (!result.ok) return error(res, result.error === "\uC2E4\uD5D8 \uAE30\uB85D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." ? 404 : 400, result.error);
1246
+ return json(res, 200, { experiment: result.experiment, ...await readUsageExperiments(paths) });
1247
+ }
1248
+ if (method === "DELETE" && url.pathname === "/api/usage-experiments") {
1249
+ const errorMessage = confirmBody(await body(req), "\uC2E4\uD5D8 \uAE30\uB85D \uC804\uCCB4 \uC0AD\uC81C \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1250
+ if (errorMessage) return error(res, 400, errorMessage);
1251
+ await deleteAllUsageExperiments(paths);
1252
+ return json(res, 200, { experiments: [], summary: null });
1253
+ }
1254
+ if (method === "GET" && url.pathname === "/api/readiness") return json(res, 200, await readReadiness(projectRoot, paths));
1255
+ if (method === "GET" && url.pathname === "/api/mcp/status") return json(res, 200, await readMcpStatus(projectRoot, paths));
1256
+ if (method === "POST" && url.pathname === "/api/mcp/preview") {
1257
+ const config = (await readConfig(paths)).config;
1258
+ const codex = await readCodexMcpRegistration(projectRoot, config.mcp.enabled);
1259
+ return json(res, 200, { path: codex.path, preview: codex.preview, registered: codex.registered, conflict: codex.conflict, note: "Codex\uB294 MCP \uC11C\uBC84 \uBAA9\uB85D\uC744 \uC0AC\uC6A9\uC790 \uC804\uC5ED \uC124\uC815\uC5D0\uC11C\uB9CC \uC77D\uC2B5\uB2C8\uB2E4. \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uC124\uC815\uC740 \uADF8\uB300\uB85C \uB450\uACE0, \uC804\uC5ED Codex \uC124\uC815\uC5D0 JuTell \uAD00\uB9AC \uBE14\uB85D\uB9CC \uCD94\uAC00\uD569\uB2C8\uB2E4." });
1260
+ }
1261
+ if (method === "POST" && url.pathname === "/api/mcp/register") {
1262
+ const errorMessage = confirmBody(await body(req), "Codex \uC5F0\uACB0 \uC124\uC815 \uC0DD\uC131 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1263
+ if (errorMessage) return error(res, 400, errorMessage);
1264
+ const config = (await readConfig(paths)).config;
1265
+ return json(res, 200, { registration: await registerCodexMcp(projectRoot, config.mcp.enabled) });
1266
+ }
1267
+ if (method === "POST" && url.pathname === "/api/mcp/remove") {
1268
+ const errorMessage = confirmBody(await body(req), "Codex \uC5F0\uACB0 \uC124\uC815 \uC81C\uAC70 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1269
+ if (errorMessage) return error(res, 400, errorMessage);
1270
+ return json(res, 200, { registration: await removeCodexMcp(projectRoot) });
1271
+ }
1272
+ if (method === "POST" && (url.pathname === "/api/mcp/connect" || url.pathname === "/api/mcp/disconnect" || url.pathname === "/api/mcp/set-default")) {
1273
+ const requested = await providerRequest(await body(req));
1274
+ if ("error" in requested) return error(res, 400, requested.error);
1275
+ const { provider } = requested;
1276
+ const action = url.pathname === "/api/mcp/connect" ? "connect" : url.pathname === "/api/mcp/disconnect" ? "disconnect" : "default";
1277
+ const config = (await readConfig(paths)).config;
1278
+ const next = await applyProviderAction(projectRoot, provider, action, config);
1279
+ if (action === "connect" || action === "default") await saveConfig(paths, next);
1280
+ return json(res, 200, { providers: await readProviderStatuses(projectRoot, next) });
1281
+ }
1282
+ if (method === "POST" && url.pathname === "/api/mcp/start") {
1283
+ const config = (await readConfig(paths)).config;
1284
+ if (!config.mcp.enabled) return error(res, 409, "MCP \uC5F0\uACB0\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 MCP \uC0AC\uC6A9\uC744 \uCF1C\uC138\uC694.");
1285
+ return json(res, 200, { server: await startMcpRuntime(projectRoot) });
1286
+ }
1287
+ if (method === "POST" && url.pathname === "/api/mcp/stop") return json(res, 200, { server: await stopMcpRuntime() });
1288
+ if (method === "POST" && url.pathname === "/api/mcp/check") {
1289
+ lastMcpCheckAt = (/* @__PURE__ */ new Date()).toISOString();
1290
+ return json(res, 200, { connection: { state: "not_checked", lastCheckedAt: lastMcpCheckAt, message: "\uC2E4\uC81C \uB3C4\uAD6C \uD638\uCD9C \uC5EC\uBD80\uB294 \uC0C8 Codex \uC138\uC158\uC5D0\uC11C \uC9C1\uC811 \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." } });
1291
+ }
1292
+ if (method === "GET" && url.pathname === "/api/config/history") return json(res, 200, { history: await getHistory(paths) });
1293
+ if (method === "PUT" && url.pathname === "/api/config") {
1294
+ const result = validateConfig(await body(req));
1295
+ if (!result.ok) return error(res, 400, result.error);
1296
+ const saved = await saveConfig(paths, result.value);
1297
+ return json(res, 200, { ...saved, ...await readOverview(paths) });
1298
+ }
1299
+ if (method === "POST" && url.pathname === "/api/config/reset") {
1300
+ const input = await body(req);
1301
+ if (!input || typeof input !== "object" || input.confirm !== true) {
1302
+ return error(res, 400, "\uAE30\uBCF8\uAC12 \uBCF5\uC6D0 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1303
+ }
1304
+ const saved = await saveConfig(paths, structuredClone(DEFAULT_CONFIG));
1305
+ return json(res, 200, { ...saved, ...await readOverview(paths) });
1306
+ }
1307
+ if (method === "DELETE" && url.pathname === "/api/config/history") {
1308
+ const input = await body(req);
1309
+ if (!input || typeof input !== "object" || input.confirm !== true) {
1310
+ return error(res, 400, "\uC124\uC815 \uAE30\uB85D \uC0AD\uC81C \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1311
+ }
1312
+ await withWriteLock(() => writeJsonAtomically(paths.historyFile, []));
1313
+ return json(res, 200, { history: [] });
1314
+ }
1315
+ if (method === "GET" && url.pathname === "/api/feedback") return json(res, 200, { feedback: await getFeedback(paths) });
1316
+ if (method === "POST" && url.pathname === "/api/feedback") {
1317
+ const result = validateFeedback(await body(req));
1318
+ if (!result.ok) return error(res, 400, result.error);
1319
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1320
+ const item = { ...result.value, id: randomUUID2(), createdAt: now, updatedAt: now };
1321
+ await withWriteLock(async () => {
1322
+ const items = await getFeedback(paths);
1323
+ await saveFeedback(paths, [...items, item]);
1324
+ });
1325
+ return json(res, 201, { feedback: item });
1326
+ }
1327
+ const feedbackMatch = url.pathname.match(/^\/api\/feedback\/([^/]+)$/);
1328
+ if (feedbackMatch && !safeId(feedbackMatch[1])) return error(res, 400, "\uD53C\uB4DC\uBC31 \uC2DD\uBCC4\uC790\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
1329
+ if (feedbackMatch && method === "PUT") {
1330
+ const result = validateFeedback(await body(req));
1331
+ if (!result.ok) return error(res, 400, result.error);
1332
+ const item = await withWriteLock(async () => {
1333
+ const items = await getFeedback(paths);
1334
+ const index = items.findIndex((item2) => item2.id === feedbackMatch[1]);
1335
+ if (index < 0) return null;
1336
+ const updated = { ...result.value, id: items[index].id, createdAt: items[index].createdAt, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1337
+ items[index] = updated;
1338
+ await saveFeedback(paths, items);
1339
+ return updated;
1340
+ });
1341
+ if (!item) return error(res, 404, "\uD53C\uB4DC\uBC31\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
1342
+ return json(res, 200, { feedback: item });
1343
+ }
1344
+ if (feedbackMatch && method === "DELETE") {
1345
+ const next = await withWriteLock(async () => {
1346
+ const items = await getFeedback(paths);
1347
+ const updated = items.filter((item) => item.id !== feedbackMatch[1]);
1348
+ if (updated.length === items.length) return null;
1349
+ await saveFeedback(paths, updated);
1350
+ return updated;
1351
+ });
1352
+ if (!next) return error(res, 404, "\uD53C\uB4DC\uBC31\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
1353
+ return json(res, 200, { feedback: next });
1354
+ }
1355
+ if (method === "DELETE" && url.pathname === "/api/feedback") {
1356
+ const input = await body(req);
1357
+ if (!input || typeof input !== "object" || input.confirm !== true) {
1358
+ return error(res, 400, "\uD53C\uB4DC\uBC31 \uC804\uCCB4 \uC0AD\uC81C \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
1359
+ }
1360
+ await withWriteLock(() => saveFeedback(paths, []));
1361
+ return json(res, 200, { feedback: [] });
1362
+ }
1363
+ return error(res, 404, "\uC694\uCCAD\uD55C \uC8FC\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
1364
+ } catch (caught) {
1365
+ const message = caught instanceof Error ? caught.message : "\uB85C\uCEEC \uC694\uCCAD\uC744 \uCC98\uB9AC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.";
1366
+ return error(res, 500, message.includes("\uC694\uCCAD\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4") || message.includes("JSON \uD615\uC2DD") ? message : "\uB85C\uCEEC \uC800\uC7A5\uC744 \uCC98\uB9AC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
1367
+ }
1368
+ }
1369
+ async function startLocalAdminServer(options) {
1370
+ const host = options.host ?? "127.0.0.1";
1371
+ const port = options.port ?? 8787;
1372
+ const server = createServer((req, res) => {
1373
+ void handleApiRequest(req, res, options.projectRoot);
1374
+ });
1375
+ server.once("close", () => {
1376
+ void stopMcpRuntime();
1377
+ });
1378
+ await new Promise((resolve, reject) => {
1379
+ server.once("error", reject);
1380
+ server.listen(port, host, () => {
1381
+ server.off("error", reject);
1382
+ resolve();
1383
+ });
1384
+ });
1385
+ const address = server.address();
1386
+ return { server, port: typeof address === "object" && address ? address.port : port };
1387
+ }
1388
+ export {
1389
+ handleApiRequest,
1390
+ startLocalAdminServer
1391
+ };