@vibecodetown/mcp-server 2.2.0 → 2.2.1

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 (70) hide show
  1. package/README.md +10 -10
  2. package/build/auth/index.js +0 -2
  3. package/build/auth/public_key.js +6 -4
  4. package/build/bootstrap/doctor.js +113 -5
  5. package/build/bootstrap/installer.js +85 -15
  6. package/build/bootstrap/registry.js +11 -6
  7. package/build/bootstrap/skills-installer.js +365 -0
  8. package/build/dx/activity.js +26 -3
  9. package/build/engine.js +151 -0
  10. package/build/errors.js +107 -0
  11. package/build/generated/bridge_build_seed_input.js +2 -0
  12. package/build/generated/bridge_build_seed_output.js +2 -0
  13. package/build/generated/bridge_confirm_reference_input.js +2 -0
  14. package/build/generated/bridge_confirm_reference_output.js +2 -0
  15. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  16. package/build/generated/bridge_generate_references_input.js +2 -0
  17. package/build/generated/bridge_generate_references_output.js +2 -0
  18. package/build/generated/bridge_references_file.js +2 -0
  19. package/build/generated/bridge_work_order_seed_file.js +2 -0
  20. package/build/generated/contracts_bundle_info.js +3 -3
  21. package/build/generated/index.js +14 -0
  22. package/build/generated/ingress_input.js +2 -0
  23. package/build/generated/ingress_output.js +2 -0
  24. package/build/generated/ingress_resolution_file.js +2 -0
  25. package/build/generated/ingress_summary_file.js +2 -0
  26. package/build/generated/message_template_id_mapping_file.js +2 -0
  27. package/build/generated/run_app_input.js +1 -1
  28. package/build/index.js +4 -3
  29. package/build/local-mode/paths.js +1 -0
  30. package/build/local-mode/setup.js +21 -1
  31. package/build/path-utils.js +68 -0
  32. package/build/runtime/cli_invoker.js +1 -1
  33. package/build/tools/vibe_pm/advisory_review.js +5 -3
  34. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  35. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  36. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  37. package/build/tools/vibe_pm/briefing.js +27 -3
  38. package/build/tools/vibe_pm/context.js +79 -0
  39. package/build/tools/vibe_pm/create_work_order.js +200 -3
  40. package/build/tools/vibe_pm/doctor.js +95 -0
  41. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  42. package/build/tools/vibe_pm/export_output.js +14 -13
  43. package/build/tools/vibe_pm/finalize_work.js +78 -40
  44. package/build/tools/vibe_pm/get_decision.js +2 -2
  45. package/build/tools/vibe_pm/index.js +128 -42
  46. package/build/tools/vibe_pm/ingress.js +645 -0
  47. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  48. package/build/tools/vibe_pm/inspect_code.js +90 -20
  49. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  50. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  51. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  52. package/build/tools/vibe_pm/memory_status.js +11 -8
  53. package/build/tools/vibe_pm/memory_sync.js +11 -8
  54. package/build/tools/vibe_pm/pm_language.js +17 -16
  55. package/build/tools/vibe_pm/python_error.js +115 -0
  56. package/build/tools/vibe_pm/run_app.js +169 -43
  57. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  58. package/build/tools/vibe_pm/search_oss.js +5 -3
  59. package/build/tools/vibe_pm/spec_rag.js +185 -0
  60. package/build/tools/vibe_pm/status.js +50 -3
  61. package/build/tools/vibe_pm/submit_decision.js +2 -2
  62. package/build/tools/vibe_pm/types.js +28 -0
  63. package/build/tools/vibe_pm/undo_last_task.js +9 -2
  64. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  65. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  66. package/build/tools.js +13 -5
  67. package/build/vibe-cli.js +245 -7
  68. package/package.json +5 -4
  69. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  70. package/skills/index.json +14 -0
@@ -0,0 +1,365 @@
1
+ // adapters/mcp-ts/src/bootstrap/skills-installer.ts
2
+ // Skill bundle installation, activation, and health check
3
+ // Skills are prebundled in the npm package (no network required)
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { SKILL_SPEC } from "./registry.js";
8
+ // ============================================================
9
+ // Path Utilities
10
+ // ============================================================
11
+ /**
12
+ * Find package root by looking for package.json
13
+ */
14
+ function findPackageRoot() {
15
+ // Start from current module's directory
16
+ let dir = path.dirname(new URL(import.meta.url).pathname);
17
+ // On Windows, remove leading slash from /C:/...
18
+ if (process.platform === "win32" && dir.startsWith("/")) {
19
+ dir = dir.slice(1);
20
+ }
21
+ for (let i = 0; i < 10; i++) {
22
+ const pkgPath = path.join(dir, "package.json");
23
+ try {
24
+ if (fs.existsSync(pkgPath)) {
25
+ return dir;
26
+ }
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ const parent = path.dirname(dir);
32
+ if (parent === dir)
33
+ break;
34
+ dir = parent;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Get the path to the prebundled skills directory in the npm package
40
+ */
41
+ export function getSkillsBundlePath() {
42
+ const pkgRoot = findPackageRoot();
43
+ if (!pkgRoot)
44
+ return null;
45
+ const skillsDir = path.join(pkgRoot, SKILL_SPEC.bundlePath);
46
+ try {
47
+ if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
48
+ return skillsDir;
49
+ }
50
+ }
51
+ catch {
52
+ // ignore
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Get the path to the project's .vibe/skills/ directory
58
+ */
59
+ export function getProjectSkillsPath(projectRoot) {
60
+ const root = projectRoot || process.cwd();
61
+ return path.join(root, ".vibe", "skills");
62
+ }
63
+ // ============================================================
64
+ // Index Loading
65
+ // ============================================================
66
+ /**
67
+ * Load the skills index from the prebundled directory
68
+ */
69
+ export function loadSkillsIndex() {
70
+ const bundlePath = getSkillsBundlePath();
71
+ if (!bundlePath)
72
+ return null;
73
+ const indexPath = path.join(bundlePath, "index.json");
74
+ try {
75
+ const content = fs.readFileSync(indexPath, "utf-8");
76
+ return JSON.parse(content);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Load the activated skills index from the project
84
+ */
85
+ export function loadActivatedSkillsIndex(projectRoot) {
86
+ const skillsPath = getProjectSkillsPath(projectRoot);
87
+ const indexPath = path.join(skillsPath, "index.json");
88
+ try {
89
+ const content = fs.readFileSync(indexPath, "utf-8");
90
+ return JSON.parse(content);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ // ============================================================
97
+ // Health Check
98
+ // ============================================================
99
+ /**
100
+ * Calculate SHA256 hash of a file
101
+ */
102
+ function calculateSha256(filePath) {
103
+ try {
104
+ const content = fs.readFileSync(filePath);
105
+ return crypto.createHash("sha256").update(content).digest("hex");
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /**
112
+ * Check health of a single skill in the bundle
113
+ */
114
+ function checkSkillHealth(bundlePath, skill) {
115
+ const filePath = path.join(bundlePath, skill.file);
116
+ if (!fs.existsSync(filePath)) {
117
+ return {
118
+ id: skill.id,
119
+ status: "missing",
120
+ version: skill.version,
121
+ path: null,
122
+ error: `Skill file not found: ${skill.file}`
123
+ };
124
+ }
125
+ const actualSha = calculateSha256(filePath);
126
+ if (!actualSha) {
127
+ return {
128
+ id: skill.id,
129
+ status: "sha_mismatch",
130
+ version: skill.version,
131
+ path: filePath,
132
+ error: "Failed to calculate SHA256"
133
+ };
134
+ }
135
+ if (actualSha !== skill.sha256) {
136
+ return {
137
+ id: skill.id,
138
+ status: "sha_mismatch",
139
+ version: skill.version,
140
+ path: filePath,
141
+ error: `SHA256 mismatch: expected ${skill.sha256}, got ${actualSha}`
142
+ };
143
+ }
144
+ return {
145
+ id: skill.id,
146
+ status: "ok",
147
+ version: skill.version,
148
+ path: filePath
149
+ };
150
+ }
151
+ /**
152
+ * Check health of the entire skills bundle
153
+ */
154
+ export function getSkillsHealth() {
155
+ const bundlePath = getSkillsBundlePath();
156
+ if (!bundlePath) {
157
+ return {
158
+ installed: false,
159
+ bundlePath: null,
160
+ version: null,
161
+ skills: [],
162
+ summary: { total: 0, ok: 0, missing: 0, corrupted: 0 }
163
+ };
164
+ }
165
+ const index = loadSkillsIndex();
166
+ if (!index) {
167
+ return {
168
+ installed: true,
169
+ bundlePath,
170
+ version: null,
171
+ skills: [],
172
+ summary: { total: 0, ok: 0, missing: 0, corrupted: 0 }
173
+ };
174
+ }
175
+ const skills = index.skills.map(skill => checkSkillHealth(bundlePath, skill));
176
+ const summary = {
177
+ total: skills.length,
178
+ ok: skills.filter(s => s.status === "ok").length,
179
+ missing: skills.filter(s => s.status === "missing").length,
180
+ corrupted: skills.filter(s => s.status === "sha_mismatch").length
181
+ };
182
+ return {
183
+ installed: true,
184
+ bundlePath,
185
+ version: index.version,
186
+ skills,
187
+ summary
188
+ };
189
+ }
190
+ // ============================================================
191
+ // Skill Listing
192
+ // ============================================================
193
+ /**
194
+ * List all available skills from the prebundled directory
195
+ */
196
+ export function listAvailableSkills() {
197
+ const index = loadSkillsIndex();
198
+ return index?.skills ?? [];
199
+ }
200
+ /**
201
+ * List activated skills in the project
202
+ */
203
+ export function listActivatedSkills(projectRoot) {
204
+ const index = loadActivatedSkillsIndex(projectRoot);
205
+ return index?.activated ?? [];
206
+ }
207
+ /**
208
+ * Get skill metadata by ID
209
+ */
210
+ export function getSkillById(skillId) {
211
+ const skills = listAvailableSkills();
212
+ return skills.find(s => s.id === skillId) ?? null;
213
+ }
214
+ // ============================================================
215
+ // Skill Activation
216
+ // ============================================================
217
+ /**
218
+ * Activate skills for a project by copying them to .vibe/skills/
219
+ */
220
+ export async function activateSkills(projectRoot, skillIds) {
221
+ const bundlePath = getSkillsBundlePath();
222
+ if (!bundlePath) {
223
+ return { activated: [], errors: ["Skill bundle not found in package"] };
224
+ }
225
+ const index = loadSkillsIndex();
226
+ if (!index) {
227
+ return { activated: [], errors: ["Skills index not found or invalid"] };
228
+ }
229
+ // If no skillIds specified, activate all
230
+ const toActivate = skillIds ?? index.skills.map(s => s.id);
231
+ const skillsToActivate = toActivate
232
+ .map(id => index.skills.find(s => s.id === id))
233
+ .filter((s) => s !== undefined);
234
+ if (skillsToActivate.length === 0) {
235
+ return { activated: [], errors: ["No valid skills to activate"] };
236
+ }
237
+ const projectSkillsPath = getProjectSkillsPath(projectRoot);
238
+ // Ensure .vibe/skills/ directory exists
239
+ try {
240
+ await fs.promises.mkdir(projectSkillsPath, { recursive: true });
241
+ }
242
+ catch (e) {
243
+ return {
244
+ activated: [],
245
+ errors: [`Failed to create skills directory: ${e instanceof Error ? e.message : String(e)}`]
246
+ };
247
+ }
248
+ const activated = [];
249
+ const errors = [];
250
+ // Copy each skill file
251
+ for (const skill of skillsToActivate) {
252
+ const srcPath = path.join(bundlePath, skill.file);
253
+ const destPath = path.join(projectSkillsPath, skill.file);
254
+ try {
255
+ await fs.promises.copyFile(srcPath, destPath);
256
+ activated.push(skill.id);
257
+ }
258
+ catch (e) {
259
+ errors.push(`Failed to copy ${skill.id}: ${e instanceof Error ? e.message : String(e)}`);
260
+ }
261
+ }
262
+ // Write the activated skills index
263
+ if (activated.length > 0) {
264
+ const activatedIndex = {
265
+ version: index.version,
266
+ activated,
267
+ activated_at: new Date().toISOString(),
268
+ source: "prebundled",
269
+ skills: skillsToActivate.filter(s => activated.includes(s.id))
270
+ };
271
+ const indexPath = path.join(projectSkillsPath, "index.json");
272
+ try {
273
+ await fs.promises.writeFile(indexPath, JSON.stringify(activatedIndex, null, 2), "utf-8");
274
+ }
275
+ catch (e) {
276
+ errors.push(`Failed to write index.json: ${e instanceof Error ? e.message : String(e)}`);
277
+ }
278
+ }
279
+ return { activated, errors };
280
+ }
281
+ /**
282
+ * Deactivate a skill from the project
283
+ */
284
+ export async function deactivateSkill(projectRoot, skillId) {
285
+ const projectSkillsPath = getProjectSkillsPath(projectRoot);
286
+ const activatedIndex = loadActivatedSkillsIndex(projectRoot);
287
+ if (!activatedIndex) {
288
+ return { success: false, error: "No activated skills found" };
289
+ }
290
+ const skillMeta = activatedIndex.skills.find(s => s.id === skillId);
291
+ if (!skillMeta) {
292
+ return { success: false, error: `Skill ${skillId} is not activated` };
293
+ }
294
+ // Remove the skill file
295
+ const skillPath = path.join(projectSkillsPath, skillMeta.file);
296
+ try {
297
+ if (fs.existsSync(skillPath)) {
298
+ await fs.promises.unlink(skillPath);
299
+ }
300
+ }
301
+ catch (e) {
302
+ return {
303
+ success: false,
304
+ error: `Failed to remove skill file: ${e instanceof Error ? e.message : String(e)}`
305
+ };
306
+ }
307
+ // Update the index
308
+ const newActivated = activatedIndex.activated.filter(id => id !== skillId);
309
+ const newSkills = activatedIndex.skills.filter(s => s.id !== skillId);
310
+ if (newActivated.length === 0) {
311
+ // Remove the index file if no skills remain
312
+ const indexPath = path.join(projectSkillsPath, "index.json");
313
+ try {
314
+ await fs.promises.unlink(indexPath);
315
+ }
316
+ catch {
317
+ // ignore
318
+ }
319
+ }
320
+ else {
321
+ // Update the index
322
+ const updatedIndex = {
323
+ ...activatedIndex,
324
+ activated: newActivated,
325
+ skills: newSkills
326
+ };
327
+ const indexPath = path.join(projectSkillsPath, "index.json");
328
+ await fs.promises.writeFile(indexPath, JSON.stringify(updatedIndex, null, 2), "utf-8");
329
+ }
330
+ return { success: true };
331
+ }
332
+ // ============================================================
333
+ // Ensure Skills (for setup/update)
334
+ // ============================================================
335
+ /**
336
+ * Ensure skills are installed and healthy
337
+ * Returns the bundle path if successful
338
+ */
339
+ export function ensureSkills() {
340
+ const health = getSkillsHealth();
341
+ // For prebundled skills, we can't "repair" - they're part of the package
342
+ // If they're corrupted, the user needs to reinstall the package
343
+ return {
344
+ path: health.bundlePath,
345
+ health
346
+ };
347
+ }
348
+ /**
349
+ * Get skill content by ID
350
+ */
351
+ export function getSkillContent(skillId) {
352
+ const bundlePath = getSkillsBundlePath();
353
+ if (!bundlePath)
354
+ return null;
355
+ const skill = getSkillById(skillId);
356
+ if (!skill)
357
+ return null;
358
+ const filePath = path.join(bundlePath, skill.file);
359
+ try {
360
+ return fs.readFileSync(filePath, "utf-8");
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
@@ -1,5 +1,6 @@
1
1
  // adapters/mcp-ts/src/dx/activity.ts
2
2
  // Activity/DX helpers: branded header, status icons, optional desktop notifications
3
+ import { getWaiterMapping, renderVerdictFromTemplateId, resolveLocaleFromData } from "../tools/vibe_pm/waiter_mapping.js";
3
4
  function boolEnv(name, defaultValue) {
4
5
  const raw = (process.env[name] ?? "").trim().toLowerCase();
5
6
  if (!raw)
@@ -91,14 +92,35 @@ function verdictIcon(data, outcome) {
91
92
  }
92
93
  return "✅";
93
94
  }
95
+ function verdictLabel(data, outcome) {
96
+ if (outcome === "error")
97
+ return "";
98
+ if (!data || typeof data !== "object")
99
+ return "";
100
+ const obj = data;
101
+ const templateId = typeof obj.message_template_id === "string" ? String(obj.message_template_id) : "";
102
+ if (!templateId)
103
+ return "";
104
+ try {
105
+ const mapping = getWaiterMapping();
106
+ const locale = resolveLocaleFromData(data, mapping);
107
+ const label = renderVerdictFromTemplateId(templateId, locale, mapping);
108
+ return label ? label : "";
109
+ }
110
+ catch {
111
+ return "";
112
+ }
113
+ }
94
114
  export function renderActivityHeader(toolName, data, outcome = "success", durationMs) {
95
115
  const badge = toolBadge(toolName, data);
96
116
  const status = verdictIcon(data, outcome);
117
+ const label = verdictLabel(data, outcome);
97
118
  const duration = typeof durationMs === "number" && durationMs >= 0 ? ` · ${Math.round(durationMs)}ms` : "";
119
+ const verdict = label ? ` · ${label}` : "";
98
120
  return [
99
121
  "╭──────────────────────────────╮",
100
122
  "│ VibePM ⚡️ Active │",
101
- `│ ${status} ${badge.icon} ${badge.label}${duration}`,
123
+ `│ ${status} ${badge.icon} ${badge.label}${verdict}${duration}`,
102
124
  "╰──────────────────────────────╯"
103
125
  ].join("\n");
104
126
  }
@@ -107,9 +129,10 @@ export function decorateToolOutputText(toolName, jsonText, data, outcome = "succ
107
129
  return [{ type: "text", text: jsonText }];
108
130
  }
109
131
  const header = renderActivityHeader(toolName, data, outcome, durationMs);
132
+ // Combine header and JSON into single block to ensure both are displayed
133
+ // (some MCP clients only show the first content block on error)
110
134
  return [
111
- { type: "text", text: header },
112
- { type: "text", text: jsonText }
135
+ { type: "text", text: header + "\n" + jsonText }
113
136
  ];
114
137
  }
115
138
  export async function notifyDesktopBestEffort(args) {
package/build/engine.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // adapters/mcp-ts/src/engine.ts
2
2
  // Engine execution using cached binaries (no PATH dependency)
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { ensureEngines } from "./bootstrap/installer.js";
4
7
  import { runCmd } from "./cli.js";
5
8
  import { TieredCache, createCacheKey } from "./cache/index.js";
@@ -104,3 +107,151 @@ export function invalidateEngineCache(pattern) {
104
107
  export function getEngineCacheStats() {
105
108
  return engineResultCache.stats();
106
109
  }
110
+ // ============================================================
111
+ // Python CLI Support (vibecoding_helper package)
112
+ // ============================================================
113
+ /**
114
+ * Python CLI commands that are safe to cache (read-only operations)
115
+ */
116
+ const PYTHON_CACHEABLE_COMMANDS = new Set([
117
+ "memory-status",
118
+ "kce-status",
119
+ "entity-gate",
120
+ "show-ask-queue",
121
+ ]);
122
+ function isDebugMode() {
123
+ const v = (process.env.VIBECODE_DEBUG ?? "").trim().toLowerCase();
124
+ return v === "1" || v === "true" || v === "yes" || v === "on";
125
+ }
126
+ /**
127
+ * Find repo root for Python import.
128
+ *
129
+ * Priority order:
130
+ * 1. VIBECODE_PYTHONPATH environment variable (explicit override)
131
+ * 2. MCP server directory (fallback for when MCP runs from user project)
132
+ * 3. startDir upwards search (heuristic: nearest ancestor with pyproject.toml or vibecoding_helper/)
133
+ *
134
+ * @param startDir - Directory to start searching from
135
+ * @returns Repo root path or null if not found
136
+ */
137
+ export function findPythonRepoRoot(startDir) {
138
+ // Priority 1: VIBECODE_PYTHONPATH explicit override
139
+ const override = process.env.VIBECODE_PYTHONPATH?.trim();
140
+ if (override && existsSync(override)) {
141
+ return override;
142
+ }
143
+ // Priority 2: MCP server directory fallback
144
+ // When MCP runs from user project, we need to find the package from MCP installation
145
+ try {
146
+ const mcpDir = dirname(fileURLToPath(import.meta.url));
147
+ // mcpDir is typically: .../adapters/mcp-ts/build/
148
+ // repo root is: .../adapters/mcp-ts/build/../../../.. = repo root
149
+ const repoFromMcp = resolve(mcpDir, "..", "..", "..", "..");
150
+ const pkgFromMcp = join(repoFromMcp, "vibecoding_helper");
151
+ if (existsSync(pkgFromMcp)) {
152
+ return repoFromMcp;
153
+ }
154
+ }
155
+ catch {
156
+ // import.meta.url failed, continue to next priority
157
+ }
158
+ // Priority 3: startDir upwards search (original heuristic)
159
+ let current = resolve(startDir);
160
+ // Walk up until filesystem root
161
+ while (true) {
162
+ const pyproject = join(current, "pyproject.toml");
163
+ const pkgDir = join(current, "vibecoding_helper");
164
+ // Check for pyproject.toml or vibecoding_helper/ directory
165
+ if (existsSync(pyproject) || existsSync(pkgDir)) {
166
+ return current;
167
+ }
168
+ const parent = dirname(current);
169
+ if (parent === current)
170
+ break; // Reached filesystem root
171
+ current = parent;
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * Build env for Python CLI so that 'python -m vibecoding_helper ...' can import
177
+ * even when MCP client ignores .mcp.json env (e.g., Claude Code).
178
+ *
179
+ * Priority:
180
+ * 1. VIBECODE_PYTHONPATH (explicit override)
181
+ * 2. Auto-detected repo root
182
+ * 3. Existing PYTHONPATH
183
+ *
184
+ * @param baseEnv - Base environment variables
185
+ * @param cwd - Current working directory for repo root detection
186
+ * @returns Environment with PYTHONPATH properly set
187
+ */
188
+ export function buildPythonCliEnv(baseEnv, cwd) {
189
+ const env = { ...baseEnv };
190
+ const delim = process.platform === "win32" ? ";" : ":";
191
+ // Priority 1: Explicit override via VIBECODE_PYTHONPATH
192
+ const override = env.VIBECODE_PYTHONPATH?.trim();
193
+ // Priority 2: Auto-detect repo root
194
+ const repoRoot = override || findPythonRepoRoot(cwd);
195
+ if (!repoRoot)
196
+ return env;
197
+ // Parse existing PYTHONPATH
198
+ const existing = env.PYTHONPATH ?? "";
199
+ const parts = existing.split(delim).filter(Boolean);
200
+ // Avoid duplicate insertion
201
+ if (!parts.includes(repoRoot)) {
202
+ env.PYTHONPATH = [repoRoot, ...parts].join(delim);
203
+ }
204
+ else {
205
+ env.PYTHONPATH = parts.join(delim);
206
+ }
207
+ return env;
208
+ }
209
+ /**
210
+ * Run Python vibecoding_helper CLI command
211
+ * Uses `python -m vibecoding_helper` for cross-platform compatibility
212
+ *
213
+ * Automatically injects PYTHONPATH by detecting the package root,
214
+ * making it work without .mcp.json env configuration.
215
+ */
216
+ export async function runPythonCli(args, opts) {
217
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
218
+ const fullArgs = ["-m", "vibecoding_helper", ...args];
219
+ const cwd = process.cwd();
220
+ // Auto-inject PYTHONPATH using buildPythonCliEnv
221
+ const env = buildPythonCliEnv(process.env, cwd);
222
+ if (isDebugMode()) {
223
+ const repoRoot = findPythonRepoRoot(cwd);
224
+ console.error(`[DEBUG runPythonCli] cmd=${pythonCmd} args=${JSON.stringify(fullArgs)}`);
225
+ console.error(`[DEBUG runPythonCli] PYTHONPATH=${env.PYTHONPATH ?? "(not set)"}`);
226
+ console.error(`[DEBUG runPythonCli] detected_root=${repoRoot ?? "(not found)"}`);
227
+ console.error(`[DEBUG runPythonCli] cwd=${cwd}`);
228
+ }
229
+ const result = await runCmd(pythonCmd, fullArgs, {
230
+ timeoutMs: opts?.timeoutMs ?? 120_000,
231
+ env,
232
+ });
233
+ if (isDebugMode()) {
234
+ console.error(`[DEBUG runPythonCli] code=${result.code} stdout=${result.stdout.slice(0, 200)} stderr=${result.stderr.slice(0, 200)}`);
235
+ }
236
+ return result;
237
+ }
238
+ /**
239
+ * Run Python CLI command with optional caching support
240
+ */
241
+ export async function runPythonCliWithCache(args, opts) {
242
+ const subCommand = extractSubcommand(args);
243
+ const isCacheable = PYTHON_CACHEABLE_COMMANDS.has(subCommand) && !opts?.skipCache;
244
+ if (isCacheable) {
245
+ const cacheKey = createCacheKey("python-cli", ...args);
246
+ const cachedResult = engineResultCache.get(cacheKey);
247
+ if (cachedResult) {
248
+ return cachedResult;
249
+ }
250
+ const result = await runPythonCli(args, opts);
251
+ if (result.code === 0) {
252
+ engineResultCache.set(cacheKey, result);
253
+ }
254
+ return result;
255
+ }
256
+ return await runPythonCli(args, opts);
257
+ }
package/build/errors.js CHANGED
@@ -169,3 +169,110 @@ export function doNotTouchError(path, pattern) {
169
169
  recovery: "This path is protected and cannot be modified. Choose a different target.",
170
170
  });
171
171
  }
172
+ // ============================================================
173
+ // Error Detection Patterns (P1-4: Normalized string-based error detection)
174
+ // ============================================================
175
+ /**
176
+ * Centralized error string patterns for consistent detection across the codebase.
177
+ * Use these constants instead of hardcoded strings for maintainability.
178
+ */
179
+ export const ErrorPatterns = {
180
+ // Bootstrap/download errors
181
+ DOWNLOAD_TIMEOUT: ["download_timeout", "AbortError"],
182
+ DOWNLOAD_404: ["404", "no_matching_asset", "download_failed:404"],
183
+ DOWNLOAD_FAILED: ["download_failed", "fetch"],
184
+ SHA_MISMATCH: ["sha_mismatch"],
185
+ SHA_MISSING: ["sha_missing_for_asset"],
186
+ BIN_NOT_FOUND: ["bin_not_found_after_extract"],
187
+ // Git errors
188
+ GIT_CONFLICT: ["conflict", "CONFLICT"],
189
+ // OPA/Policy errors
190
+ OPA_EVAL_FAILED: ["eval_failed", "opa_eval_failed", "opa exception", "opa_exception", "eval failed"],
191
+ OPA_MISSING: ["missing"],
192
+ };
193
+ /**
194
+ * Check if a message matches any of the given patterns.
195
+ * Case-insensitive matching for OPA patterns, case-sensitive for others.
196
+ */
197
+ export function matchesErrorPattern(message, patterns, options = {}) {
198
+ const normalizedMessage = options.caseInsensitive ? message.toLowerCase() : message;
199
+ return patterns.some((pattern) => {
200
+ const normalizedPattern = options.caseInsensitive ? pattern.toLowerCase() : pattern;
201
+ return normalizedMessage.includes(normalizedPattern);
202
+ });
203
+ }
204
+ /**
205
+ * Check if message indicates a download timeout error.
206
+ */
207
+ export function isDownloadTimeoutError(message) {
208
+ return matchesErrorPattern(message, ErrorPatterns.DOWNLOAD_TIMEOUT);
209
+ }
210
+ /**
211
+ * Check if message indicates a 404/not found error.
212
+ */
213
+ export function isDownload404Error(message) {
214
+ return matchesErrorPattern(message, ErrorPatterns.DOWNLOAD_404);
215
+ }
216
+ /**
217
+ * Check if message indicates a general download failure.
218
+ */
219
+ export function isDownloadFailedError(message) {
220
+ return matchesErrorPattern(message, ErrorPatterns.DOWNLOAD_FAILED);
221
+ }
222
+ /**
223
+ * Check if message indicates a git conflict.
224
+ */
225
+ export function isGitConflictError(message) {
226
+ return matchesErrorPattern(message, ErrorPatterns.GIT_CONFLICT);
227
+ }
228
+ /**
229
+ * Check if message indicates an OPA evaluation failure.
230
+ * Always case-insensitive for OPA patterns.
231
+ */
232
+ export function isOpaEvalFailedError(message) {
233
+ return matchesErrorPattern(message, ErrorPatterns.OPA_EVAL_FAILED, { caseInsensitive: true });
234
+ }
235
+ /**
236
+ * Check if message indicates OPA is missing.
237
+ */
238
+ export function isOpaMissingError(message) {
239
+ return matchesErrorPattern(message, ErrorPatterns.OPA_MISSING, { caseInsensitive: true });
240
+ }
241
+ // ============================================================
242
+ // P2-23: Error Message Formatting Utilities
243
+ // ============================================================
244
+ /**
245
+ * Format CLI command failure error message.
246
+ * @param command - The command that failed (e.g., "kce-status", "zoekt-evidence")
247
+ * @param exitCode - Exit code from the process
248
+ * @param stderr - Standard error output
249
+ * @param stdout - Standard output (used as fallback)
250
+ */
251
+ export function formatCliError(command, exitCode, stderr, stdout) {
252
+ const detail = stderr || stdout || `exit_code=${exitCode ?? "unknown"}`;
253
+ return `${command} failed: ${detail}`;
254
+ }
255
+ /**
256
+ * Format JSON parsing error message.
257
+ * @param context - Context where parsing failed (e.g., "kce-status", "config")
258
+ * @param error - The parsing error
259
+ */
260
+ export function formatJsonParseError(context, error) {
261
+ const msg = error instanceof Error ? error.message : String(error);
262
+ return `${context} invalid_json: ${msg}`;
263
+ }
264
+ /**
265
+ * Format schema validation error message.
266
+ * @param context - Context where validation failed
267
+ */
268
+ export function formatSchemaError(context) {
269
+ return `${context} schema mismatch`;
270
+ }
271
+ /**
272
+ * Format user-facing Korean error message.
273
+ * @param action - What action failed (e.g., "프로젝트 초기화", "작업 지시서 생성")
274
+ * @param detail - Additional detail (optional)
275
+ */
276
+ export function formatKoreanError(action, detail) {
277
+ return `${action} 실패${detail ? `: ${detail}` : ""}`;
278
+ }
@@ -0,0 +1,2 @@
1
+ import { z } from "zod";
2
+ export const BridgeBuildSeedInputSchema = z.object({ "project_id": z.string().min(1), "run_id": z.string().min(1).optional(), "raw_user_intent": z.string().min(1).max(4000).optional(), "goal": z.string().min(1).max(2000).optional(), "user_story": z.string().min(1).max(2000).optional(), "acceptance": z.array(z.string().min(1).max(300)).min(3).max(12).optional(), "constraints": z.array(z.string().min(1).max(200)).max(12).optional(), "non_goals": z.array(z.string().min(1).max(200)).max(8).optional(), "scope_seed": z.object({ "include": z.array(z.string().min(1).max(300)).min(1).max(20), "exclude": z.array(z.string().min(1).max(300)).max(20), "do_not_touch": z.array(z.string().min(1).max(300)).max(20).optional() }).strict().optional(), "preferred_stack_hint": z.string().max(200).optional(), "risk_notes": z.array(z.string().min(1).max(200)).max(8).optional(), "overwrite": z.boolean().default(true), "write_files": z.boolean().default(true) }).strict().describe("SSOT schema for vibe_pm.bridge_build_seed MCP tool input");