Skip to content

Commit c4b9f41

Browse files
Sync packages from monorepo (fcba6f6)
1 parent a375fd4 commit c4b9f41

4 files changed

Lines changed: 65 additions & 46 deletions

File tree

packages/cli/src/commands/game/create.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export const gameCreate = async (args: string[]) => {
198198
const agents = detectAgent()
199199
const agentChoices = [
200200
...agents.map((a) => ({ value: a.binary, label: `${a.displayName} (${a.path})` })),
201-
{ value: "none", label: "None - I'll run the skills manually" },
201+
{ value: "none", label: "None - I'll run the steps manually" },
202202
]
203203

204204
let selectedAgent: string
@@ -213,7 +213,7 @@ export const gameCreate = async (args: string[]) => {
213213
}
214214

215215
if (selectedAgent !== "none") {
216-
p.log.step("Running Puzzmo skills pipeline...")
216+
p.log.step("Running Puzzmo migration pipeline...")
217217
await runSkillsPipelineTUI(selectedAgent, gameDir)
218218
}
219219

packages/cli/src/skills/mcp-client.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ type McpConfig = {
55
mcpServers: Record<string, { url: string; headers?: Record<string, string> }>
66
}
77

8-
/** Reads the .mcp.json config from the game directory */
8+
/** Reads the .mcp.json config, searching the game directory and ancestor directories */
99
const readMcpConfig = (gameDir: string): McpConfig | null => {
10-
const mcpPath = path.join(gameDir, ".mcp.json")
11-
if (!fs.existsSync(mcpPath)) return null
12-
try {
13-
return JSON.parse(fs.readFileSync(mcpPath, "utf-8"))
14-
} catch {
15-
return null
10+
let dir = path.resolve(gameDir)
11+
const root = path.parse(dir).root
12+
while (dir !== root) {
13+
const mcpPath = path.join(dir, ".mcp.json")
14+
if (fs.existsSync(mcpPath)) {
15+
try {
16+
return JSON.parse(fs.readFileSync(mcpPath, "utf-8"))
17+
} catch {
18+
return null
19+
}
20+
}
21+
dir = path.dirname(dir)
1622
}
23+
return null
1724
}
1825

1926
/** Sends a JSON-RPC request to the MCP server and parses the SSE response */
@@ -28,16 +35,21 @@ const mcpRequest = async (url: string, headers: Record<string, string>, method:
2835
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
2936
})
3037

38+
if (!response.ok) throw new Error(`MCP server returned ${response.status} ${response.statusText}`)
39+
3140
const text = await response.text()
3241
// Parse SSE format: lines starting with "data: " contain the JSON payload
3342
for (const line of text.split("\n")) {
3443
if (!line.startsWith("data: ")) continue
3544
try {
3645
const parsed = JSON.parse(line.slice(6))
46+
if (parsed.error) throw new Error(`MCP error: ${parsed.error.message ?? JSON.stringify(parsed.error)}`)
3747
if (parsed.result) return parsed.result
38-
} catch {}
48+
} catch (e) {
49+
if (e instanceof Error && e.message.startsWith("MCP error:")) throw e
50+
}
3951
}
40-
return null
52+
throw new Error(`MCP server returned no result. Response body: ${text.slice(0, 200)}`)
4153
}
4254

4355
/** Returns the MCP server URL from .mcp.json, or null if not configured */
@@ -65,26 +77,25 @@ export const listMcpPrompts = async (gameDir: string): Promise<{ name: string; d
6577
}
6678
}
6779

68-
/** Fetches a skill prompt's content from the MCP server configured in .mcp.json */
69-
export const fetchSkillPrompt = async (skillName: string, gameDir: string): Promise<string | null> => {
80+
/** Fetches a step's instructions from the MCP server configured in .mcp.json */
81+
export const fetchSkillPrompt = async (stepName: string, gameDir: string): Promise<string> => {
7082
const config = readMcpConfig(gameDir)
71-
if (!config) return null
83+
if (!config) throw new Error(`No .mcp.json found in ${gameDir}`)
7284

7385
const server = Object.values(config.mcpServers)[0]
74-
if (!server) return null
86+
if (!server) throw new Error("No MCP server configured in .mcp.json")
7587

76-
try {
77-
const result = await mcpRequest(server.url, server.headers ?? {}, "prompts/get", { name: skillName })
78-
if (!result?.messages?.length) return null
88+
const result = await mcpRequest(server.url, server.headers ?? {}, "prompts/get", { name: stepName })
89+
if (!result?.messages?.length) throw new Error(`MCP server returned no instructions for "${stepName}"`)
7990

80-
return result.messages
81-
.map((m: any) => {
82-
if (typeof m.content === "string") return m.content
83-
if (m.content?.text) return m.content.text
84-
return ""
85-
})
86-
.join("\n")
87-
} catch {
88-
return null
89-
}
91+
const text = result.messages
92+
.map((m: any) => {
93+
if (typeof m.content === "string") return m.content
94+
if (m.content?.text) return m.content.text
95+
return ""
96+
})
97+
.join("\n")
98+
99+
if (!text.trim()) throw new Error(`MCP server returned empty instructions for "${stepName}"`)
100+
return text
90101
}

packages/cli/src/skills/runner.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import { verifyBuild, runCommand, gitCommit } from "../util/exec.js"
55
import { fetchSkillPrompt } from "./mcp-client.js"
66
import { runPipelineTUI } from "../tui/pipeline.js"
77

8-
/** Fetches skill instructions from the MCP server, then builds a self-contained prompt for the agent */
9-
const buildPrompt = async (skillName: string, gameDir: string): Promise<string> => {
10-
const instructions = await fetchSkillPrompt(skillName, gameDir)
11-
if (instructions) return `${instructions}\n\nThe game source is in the current directory.`
12-
return `Run the skill "${skillName}". The game source is in the current directory.`
8+
/** Fetches step instructions from the MCP server and wraps them as an agent prompt */
9+
const buildPrompt = async (stepName: string, gameDir: string): Promise<string> => {
10+
const instructions = await fetchSkillPrompt(stepName, gameDir)
11+
return `Follow these instructions. The game source is in the current directory.\n\n${instructions}`
1312
}
1413

1514
/** Invokes an LLM agent with a prompt (plain mode, stdio inherited) */
@@ -38,7 +37,7 @@ export const runSkillsPipeline = async (agent: string, gameDir: string) => {
3837
for (let i = 0; i < total; i++) {
3938
const skill = skillsPipeline[i]
4039
const step = i + 1
41-
console.log(`[${step}/${total}] Running skill: ${skill.name}`)
40+
console.log(`[${step}/${total}] Running step: ${skill.name}`)
4241

4342
const prompt = await buildPrompt(skill.name, gameDir)
4443
let result = invokeAgent(agent, prompt, gameDir)
@@ -47,7 +46,7 @@ export const runSkillsPipeline = async (agent: string, gameDir: string) => {
4746
console.log(` Agent failed, retrying...`)
4847
result = invokeAgent(agent, prompt, gameDir)
4948
if (!result.success) {
50-
console.error(` Skill ${skill.name} failed after retry. Stopping pipeline.`)
49+
console.error(` Step ${skill.name} failed after retry. Stopping pipeline.`)
5150
process.exit(1)
5251
}
5352
}
@@ -56,7 +55,7 @@ export const runSkillsPipeline = async (agent: string, gameDir: string) => {
5655
const buildResult = verifyBuild(gameDir)
5756
if (!buildResult.success) {
5857
console.log(` Build failed after ${skill.name}, asking agent to fix...`)
59-
const fixPrompt = `The vite build failed after running skill ${skill.name}. Fix the build errors:\n\n${buildResult.error}`
58+
const fixPrompt = `The vite build failed after the "${skill.name}" step. Fix the build errors:\n\n${buildResult.error}`
6059
invokeAgent(agent, fixPrompt, gameDir)
6160

6261
const retryBuild = verifyBuild(gameDir)
@@ -69,7 +68,7 @@ export const runSkillsPipeline = async (agent: string, gameDir: string) => {
6968
// Commit
7069
try {
7170
runCommand("git add -A", { cwd: gameDir })
72-
gitCommit(`skill: ${skill.name}`, { cwd: gameDir })
71+
gitCommit(`step: ${skill.name}`, { cwd: gameDir })
7372
console.log(` Committed.`)
7473
} catch {
7574
console.log(` No changes to commit.`)

packages/cli/src/tui/pipeline.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ const buildAgentCmd = (agent: string, prompt: string): { cmd: string; args: stri
5858
return { cmd: agent, args: [prompt] }
5959
}
6060

61-
/** Fetches skill instructions from the MCP server, then builds a self-contained prompt for the agent */
62-
const buildPrompt = async (skillName: string, gameDir: string): Promise<string> => {
63-
const instructions = await fetchSkillPrompt(skillName, gameDir)
64-
if (instructions) return `${instructions}\n\nThe game source is in the current directory.`
65-
return `Run the skill "${skillName}". The game source is in the current directory.`
61+
/** Fetches step instructions from the MCP server and wraps them as an agent prompt */
62+
const buildPrompt = async (stepName: string, gameDir: string): Promise<string> => {
63+
const instructions = await fetchSkillPrompt(stepName, gameDir)
64+
return `Follow these instructions. The game source is in the current directory.\n\n${instructions}`
6665
}
6766

6867
class PipelineTUI {
@@ -115,7 +114,7 @@ class PipelineTUI {
115114

116115
// Sidebar header
117116
buf += moveTo(2, 1)
118-
buf += "│ " + bold("Migration Pipeline") + " ".repeat(Math.max(0, sidebarWidth - 21)) + "│"
117+
buf += "│ " + bold("Migration Steps") + " ".repeat(Math.max(0, sidebarWidth - 18)) + "│"
119118

120119
// Output panel header
121120
const headerLabel = this.done ? "Done" : `${skillsPipeline[this.currentStep]?.name ?? ""} ${dim(`(${this.phase})`)}`
@@ -399,7 +398,17 @@ class PipelineTUI {
399398
this.phase = "agent"
400399
this.render()
401400

402-
const prompt = await buildPrompt(skill.name, this.gameDir)
401+
let prompt: string
402+
try {
403+
prompt = await buildPrompt(skill.name, this.gameDir)
404+
} catch (e: any) {
405+
this.appendOutput(`Failed to fetch instructions: ${e.message}`)
406+
this.writeSkillLog(skill.name)
407+
this.states[stepIndex] = "failed"
408+
this.render()
409+
return false
410+
}
411+
403412
let success = await this.runAgent(prompt)
404413

405414
if (!success) {
@@ -420,7 +429,7 @@ class PipelineTUI {
420429
const buildResult = this.runCmd("npx vite build")
421430
if (!buildResult.success) {
422431
this.appendOutput("Asking agent to fix...")
423-
const fixPrompt = `The vite build failed after running skill ${skill.name}. Fix the build errors:\n\n${buildResult.output}`
432+
const fixPrompt = `The vite build failed after the "${skill.name}" step. Fix the build errors:\n\n${buildResult.output}`
424433
await this.runAgent(fixPrompt)
425434

426435
const retry = this.runCmd("npx vite build")
@@ -436,7 +445,7 @@ class PipelineTUI {
436445
this.phase = "commit"
437446
this.render()
438447
this.runCmd("git add -A")
439-
const commitResult = this.runCmd(`git commit -m "skill: ${skill.name}"`)
448+
const commitResult = this.runCmd(`git commit -m "step: ${skill.name}"`)
440449
if (commitResult.success) this.appendOutput("Committed.")
441450
else this.appendOutput("No changes to commit.")
442451

0 commit comments

Comments
 (0)