@puzzmo/cli 1.0.32 → 1.0.34
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.
- package/lib/agents/claude.d.ts +5 -0
- package/lib/agents/claude.js +69 -0
- package/lib/agents/claude.js.map +1 -0
- package/lib/agents/codex.d.ts +4 -0
- package/lib/agents/codex.js +90 -0
- package/lib/agents/codex.js.map +1 -0
- package/lib/agents/copilot.d.ts +4 -0
- package/lib/agents/copilot.js +123 -0
- package/lib/agents/copilot.js.map +1 -0
- package/lib/agents/gemini.d.ts +5 -0
- package/lib/agents/gemini.js +69 -0
- package/lib/agents/gemini.js.map +1 -0
- package/lib/agents/index.d.ts +5 -0
- package/lib/agents/index.js +15 -0
- package/lib/agents/index.js.map +1 -0
- package/lib/agents/opencode.d.ts +5 -0
- package/lib/agents/opencode.js +159 -0
- package/lib/agents/opencode.js.map +1 -0
- package/lib/agents/stream-json-cli.d.ts +12 -0
- package/lib/agents/stream-json-cli.js +75 -0
- package/lib/agents/stream-json-cli.js.map +1 -0
- package/lib/agents/types.d.ts +37 -0
- package/lib/agents/types.js +2 -0
- package/lib/agents/types.js.map +1 -0
- package/lib/commands/agent-test.d.ts +3 -0
- package/lib/commands/agent-test.js +79 -0
- package/lib/commands/agent-test.js.map +1 -0
- package/lib/commands/game/create.js +6 -4
- package/lib/commands/game/create.js.map +1 -1
- package/lib/commands/upload.d.ts +5 -1
- package/lib/commands/upload.js +41 -40
- package/lib/commands/upload.js.map +1 -1
- package/lib/index.js +10 -4
- package/lib/index.js.map +1 -1
- package/lib/tui/pipeline.d.ts +1 -2
- package/lib/tui/pipeline.js +160 -267
- package/lib/tui/pipeline.js.map +1 -1
- package/lib/util/api.d.ts +5 -1
- package/lib/util/api.js +29 -16
- package/lib/util/api.js.map +1 -1
- package/lib/util/validatePuzzmoFile.js +31 -6
- package/lib/util/validatePuzzmoFile.js.map +1 -1
- package/package.json +4 -1
- package/src/agents/claude.ts +72 -0
- package/src/agents/codex.ts +82 -0
- package/src/agents/copilot.ts +118 -0
- package/src/agents/gemini.ts +74 -0
- package/src/agents/index.ts +20 -0
- package/src/agents/opencode.ts +154 -0
- package/src/agents/stream-json-cli.ts +86 -0
- package/src/agents/types.ts +22 -0
- package/src/commands/agent-test.ts +81 -0
- package/src/commands/game/create.ts +6 -4
- package/src/commands/upload.ts +53 -39
- package/src/index.ts +10 -4
- package/src/tui/pipeline.ts +151 -268
- package/src/util/api.ts +29 -13
- package/src/util/validatePuzzmoFile.ts +30 -7
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { runStreamJsonCli } from "./stream-json-cli.js"
|
|
2
|
+
import type { Agent, AgentEvent } from "./types.js"
|
|
3
|
+
|
|
4
|
+
/** Gemini adapter — wraps the local `gemini` CLI in subprocess + stream-json
|
|
5
|
+
* mode. Auth delegates to the user's `gemini` CLI login (Google account /
|
|
6
|
+
* Code Assist) or falls back to GEMINI_API_KEY. */
|
|
7
|
+
export const geminiAgent: Agent = {
|
|
8
|
+
name: "gemini",
|
|
9
|
+
run: (input) =>
|
|
10
|
+
runStreamJsonCli(
|
|
11
|
+
{
|
|
12
|
+
cmd: "gemini",
|
|
13
|
+
buildArgs: (prompt) => ["-p", prompt, "--output-format", "stream-json"],
|
|
14
|
+
parseLine: parseGeminiLine,
|
|
15
|
+
},
|
|
16
|
+
input,
|
|
17
|
+
),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Map one stream-json line from gemini to an AgentEvent (or null to skip).
|
|
21
|
+
* Event union per `@google/gemini-cli-core`'s `JsonStreamEvent`:
|
|
22
|
+
* init / message / tool_use / tool_result / error / result. */
|
|
23
|
+
const parseGeminiLine = (line: string): AgentEvent | null => {
|
|
24
|
+
let obj: any
|
|
25
|
+
try {
|
|
26
|
+
obj = JSON.parse(line)
|
|
27
|
+
} catch {
|
|
28
|
+
return { type: "system", text: line }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (obj.type === "init") return { type: "system", text: "Session started" }
|
|
32
|
+
|
|
33
|
+
if (obj.type === "message") {
|
|
34
|
+
// Streaming deltas flow inline; complete `content` chunks get a trailing newline
|
|
35
|
+
// so consecutive messages don't glue together in the rendered output.
|
|
36
|
+
if (typeof obj.delta === "string") {
|
|
37
|
+
return obj.role === "thinking" ? { type: "thinking", text: obj.delta } : { type: "text", text: obj.delta }
|
|
38
|
+
}
|
|
39
|
+
const text = obj.content ?? ""
|
|
40
|
+
if (!text) return null
|
|
41
|
+
const withNL = text.endsWith("\n") ? text : text + "\n"
|
|
42
|
+
return obj.role === "thinking" ? { type: "thinking", text: withNL } : { type: "text", text: withNL }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (obj.type === "tool_use") {
|
|
46
|
+
const name = obj.tool_name ?? "tool"
|
|
47
|
+
return { type: "tool_use", name, summary: summarizeToolInput(name, obj.parameters ?? {}) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (obj.type === "tool_result") {
|
|
51
|
+
const ok = obj.status === "success" || obj.status === "ok"
|
|
52
|
+
const summary = String(obj.output ?? obj.error ?? "").slice(0, 200)
|
|
53
|
+
return { type: "tool_result", ok, summary }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (obj.type === "error") return { type: "error", message: obj.message ?? "" }
|
|
57
|
+
|
|
58
|
+
if (obj.type === "result") {
|
|
59
|
+
const ok = obj.status === "success" || obj.status === "ok"
|
|
60
|
+
const cost = typeof obj.stats?.cost_usd === "number" ? obj.stats.cost_usd : undefined
|
|
61
|
+
return { type: "result", ok, costUSD: cost }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const summarizeToolInput = (name: string, input: any): string => {
|
|
68
|
+
if (!input || typeof input !== "object") return ""
|
|
69
|
+
if (typeof input.command === "string") return input.command.slice(0, 200)
|
|
70
|
+
if (typeof input.file_path === "string") return input.file_path
|
|
71
|
+
if (typeof input.path === "string") return input.path
|
|
72
|
+
if (typeof input.pattern === "string") return input.pattern
|
|
73
|
+
return name
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { claudeAgent } from "./claude.js"
|
|
2
|
+
import { codexAgent } from "./codex.js"
|
|
3
|
+
import { copilotAgent } from "./copilot.js"
|
|
4
|
+
import { geminiAgent } from "./gemini.js"
|
|
5
|
+
import { opencodeAgent } from "./opencode.js"
|
|
6
|
+
import type { Agent } from "./types.js"
|
|
7
|
+
|
|
8
|
+
export type { Agent, AgentEvent, RunInput } from "./types.js"
|
|
9
|
+
|
|
10
|
+
export const AGENTS: Record<string, Agent> = {
|
|
11
|
+
claude: claudeAgent,
|
|
12
|
+
codex: codexAgent,
|
|
13
|
+
copilot: copilotAgent,
|
|
14
|
+
gemini: geminiAgent,
|
|
15
|
+
opencode: opencodeAgent,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getAgent = (name: string): Agent | undefined => AGENTS[name]
|
|
19
|
+
|
|
20
|
+
export const agentNames = (): string[] => Object.keys(AGENTS)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk"
|
|
2
|
+
|
|
3
|
+
import type { Agent, AgentEvent, RunInput } from "./types.js"
|
|
4
|
+
|
|
5
|
+
/** OpenCode adapter — spawns a local opencode server, opens an SSE stream
|
|
6
|
+
* against it, creates a session, and sends a prompt. Auth lives in the
|
|
7
|
+
* user's `opencode` CLI config / `client.auth.set()`. */
|
|
8
|
+
export const opencodeAgent: Agent = {
|
|
9
|
+
name: "opencode",
|
|
10
|
+
run(input) {
|
|
11
|
+
return runOpencode(input)
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const runOpencode = async function* (input: RunInput): AsyncIterable<AgentEvent> {
|
|
16
|
+
const cwd = input.cwd ?? process.cwd()
|
|
17
|
+
|
|
18
|
+
let server
|
|
19
|
+
try {
|
|
20
|
+
server = await createOpencodeServer({ signal: input.signal, config: { project: cwd } as any })
|
|
21
|
+
} catch (e: any) {
|
|
22
|
+
yield { type: "error", message: `Opencode server failed to start: ${e.message}` }
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = createOpencodeClient({ baseUrl: server.url as `${string}://${string}` })
|
|
27
|
+
|
|
28
|
+
// Buffered event queue + waiter, so the SSE consumer pushes and the generator yields.
|
|
29
|
+
const events: AgentEvent[] = []
|
|
30
|
+
let waiting: (() => void) | null = null
|
|
31
|
+
const wake = () => {
|
|
32
|
+
waiting?.()
|
|
33
|
+
waiting = null
|
|
34
|
+
}
|
|
35
|
+
const push = (e: AgentEvent) => {
|
|
36
|
+
events.push(e)
|
|
37
|
+
wake()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let done = false
|
|
41
|
+
let sessionID: string | null = null
|
|
42
|
+
|
|
43
|
+
// Start SSE before sending a prompt so we don't lose early events.
|
|
44
|
+
const sseTask = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
const sub = await client.event.subscribe()
|
|
47
|
+
for await (const event of sub.stream as AsyncIterable<any>) {
|
|
48
|
+
if (event.type === "session.idle" && event.properties?.sessionID && event.properties.sessionID === sessionID) {
|
|
49
|
+
push({ type: "result", ok: true })
|
|
50
|
+
done = true
|
|
51
|
+
wake()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const mapped = mapOpencodeEvent(event, sessionID)
|
|
55
|
+
for (const m of mapped) push(m)
|
|
56
|
+
}
|
|
57
|
+
} catch (e: any) {
|
|
58
|
+
push({ type: "error", message: `SSE stream error: ${e.message}` })
|
|
59
|
+
done = true
|
|
60
|
+
wake()
|
|
61
|
+
}
|
|
62
|
+
})()
|
|
63
|
+
|
|
64
|
+
// Create the session, then fire the prompt. We use promptAsync so we don't block
|
|
65
|
+
// on completion — the SSE stream's session.idle event is our signal to stop.
|
|
66
|
+
let session
|
|
67
|
+
try {
|
|
68
|
+
const created = await client.session.create({ body: { title: "puzzmo-cli" }, query: { directory: cwd } })
|
|
69
|
+
session = created.data
|
|
70
|
+
if (!session?.id) throw new Error("session.create returned no id")
|
|
71
|
+
sessionID = session.id
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
push({ type: "error", message: `Session creation failed: ${e.message}` })
|
|
74
|
+
done = true
|
|
75
|
+
wake()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sessionID) {
|
|
79
|
+
client.session
|
|
80
|
+
.promptAsync({
|
|
81
|
+
path: { id: sessionID },
|
|
82
|
+
body: { parts: [{ type: "text", text: input.prompt }] as any },
|
|
83
|
+
query: { directory: cwd },
|
|
84
|
+
})
|
|
85
|
+
.catch((e: Error) => {
|
|
86
|
+
push({ type: "error", message: `Prompt failed: ${e.message}` })
|
|
87
|
+
done = true
|
|
88
|
+
wake()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (input.signal)
|
|
93
|
+
input.signal.addEventListener("abort", () => {
|
|
94
|
+
done = true
|
|
95
|
+
wake()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
while (true) {
|
|
100
|
+
if (events.length > 0) {
|
|
101
|
+
yield events.shift()!
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (done) return
|
|
105
|
+
await new Promise<void>((r) => (waiting = r))
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
server.close()
|
|
109
|
+
await sseTask.catch(() => {})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const mapOpencodeEvent = (event: any, sessionID: string | null): AgentEvent[] => {
|
|
114
|
+
if (event.type === "message.part.updated") {
|
|
115
|
+
const part = event.properties?.part
|
|
116
|
+
const delta = event.properties?.delta as string | undefined
|
|
117
|
+
if (!part) return []
|
|
118
|
+
// Deltas flow inline; complete-snapshot text gets a trailing newline so consecutive messages don't run together.
|
|
119
|
+
if (part.type === "text") {
|
|
120
|
+
if (delta != null) return [{ type: "text", text: delta }]
|
|
121
|
+
const text = part.text ?? ""
|
|
122
|
+
if (!text) return []
|
|
123
|
+
return [{ type: "text", text: text.endsWith("\n") ? text : text + "\n" }]
|
|
124
|
+
}
|
|
125
|
+
if (part.type === "reasoning") {
|
|
126
|
+
if (delta != null) return [{ type: "thinking", text: delta }]
|
|
127
|
+
const text = part.text ?? ""
|
|
128
|
+
if (!text) return []
|
|
129
|
+
return [{ type: "thinking", text: text.endsWith("\n") ? text : text + "\n" }]
|
|
130
|
+
}
|
|
131
|
+
if (part.type === "tool") {
|
|
132
|
+
const state = part.state?.status ?? part.state
|
|
133
|
+
if (state === "completed" || state === "error")
|
|
134
|
+
return [{ type: "tool_result", ok: state === "completed", summary: summarizeToolPart(part) }]
|
|
135
|
+
return [{ type: "tool_use", name: part.tool ?? "tool", summary: summarizeToolPart(part) }]
|
|
136
|
+
}
|
|
137
|
+
return []
|
|
138
|
+
}
|
|
139
|
+
if (event.type === "file.edited") return [{ type: "tool_result", ok: true, summary: `Edited ${event.properties?.file ?? ""}` }]
|
|
140
|
+
if (event.type === "command.executed") return [{ type: "tool_use", name: "Bash", summary: event.properties?.arguments ?? "" }]
|
|
141
|
+
if (event.type === "session.error" && (!sessionID || event.properties?.sessionID === sessionID))
|
|
142
|
+
return [{ type: "error", message: event.properties?.error?.message ?? "session error" }]
|
|
143
|
+
return []
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const summarizeToolPart = (part: any): string => {
|
|
147
|
+
const input = part.state?.input ?? part.input
|
|
148
|
+
if (!input || typeof input !== "object") return part.tool ?? ""
|
|
149
|
+
if (typeof input.command === "string") return input.command.slice(0, 200)
|
|
150
|
+
if (typeof input.file_path === "string") return input.file_path
|
|
151
|
+
if (typeof input.path === "string") return input.path
|
|
152
|
+
if (typeof input.pattern === "string") return input.pattern
|
|
153
|
+
return part.tool ?? ""
|
|
154
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
import type { AgentEvent, RunInput } from "./types.js"
|
|
4
|
+
|
|
5
|
+
/** Shared driver for any CLI that emits NDJSON over stdout when given a prompt
|
|
6
|
+
* in print/headless mode. Used by both the claude and gemini adapters. */
|
|
7
|
+
export type StreamJsonCliConfig = {
|
|
8
|
+
cmd: string
|
|
9
|
+
buildArgs: (prompt: string) => string[]
|
|
10
|
+
/** Map one parsed JSON line to an AgentEvent (or null to drop). */
|
|
11
|
+
parseLine: (line: string) => AgentEvent | null
|
|
12
|
+
/** Optional env mutator. Receives a copy of process.env. */
|
|
13
|
+
env?: (base: NodeJS.ProcessEnv) => NodeJS.ProcessEnv
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const runStreamJsonCli = async function* (config: StreamJsonCliConfig, input: RunInput): AsyncIterable<AgentEvent> {
|
|
17
|
+
const args = config.buildArgs(input.prompt)
|
|
18
|
+
const env = config.env ? config.env({ ...process.env }) : { ...process.env }
|
|
19
|
+
|
|
20
|
+
let proc: ChildProcess
|
|
21
|
+
try {
|
|
22
|
+
proc = spawn(config.cmd, args, {
|
|
23
|
+
cwd: input.cwd ?? process.cwd(),
|
|
24
|
+
env,
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
})
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
yield { type: "error", message: `Failed to spawn ${config.cmd}: ${e.message}` }
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (input.signal) input.signal.addEventListener("abort", () => proc.kill())
|
|
33
|
+
|
|
34
|
+
// Bridge node streams + child events into an async generator.
|
|
35
|
+
const queue: AgentEvent[] = []
|
|
36
|
+
let done = false
|
|
37
|
+
let resolveWaiting: (() => void) | null = null
|
|
38
|
+
let buf = ""
|
|
39
|
+
let sawResult = false
|
|
40
|
+
|
|
41
|
+
const push = (e: AgentEvent) => {
|
|
42
|
+
if (e.type === "result") sawResult = true
|
|
43
|
+
queue.push(e)
|
|
44
|
+
resolveWaiting?.()
|
|
45
|
+
resolveWaiting = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const flushLine = (line: string) => {
|
|
49
|
+
if (!line.trim()) return
|
|
50
|
+
const event = config.parseLine(line)
|
|
51
|
+
if (event) push(event)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
55
|
+
buf += chunk.toString("utf8")
|
|
56
|
+
const lines = buf.split("\n")
|
|
57
|
+
buf = lines.pop() ?? ""
|
|
58
|
+
for (const line of lines) flushLine(line)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
62
|
+
const text = chunk.toString("utf8").trim()
|
|
63
|
+
if (text) push({ type: "error", message: text })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
proc.on("close", (code) => {
|
|
67
|
+
if (buf.trim()) flushLine(buf)
|
|
68
|
+
if (code !== 0 && !sawResult) push({ type: "result", ok: false })
|
|
69
|
+
done = true
|
|
70
|
+
resolveWaiting?.()
|
|
71
|
+
resolveWaiting = null
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
proc.on("error", (err) => {
|
|
75
|
+
push({ type: "error", message: err.message })
|
|
76
|
+
done = true
|
|
77
|
+
resolveWaiting?.()
|
|
78
|
+
resolveWaiting = null
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
while (true) {
|
|
82
|
+
if (queue.length > 0) yield queue.shift()!
|
|
83
|
+
else if (done) return
|
|
84
|
+
else await new Promise<void>((r) => (resolveWaiting = r))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Normalized event union emitted by every agent adapter. Keep this small —
|
|
2
|
+
* adapters fold their richer native event shapes into one of these. */
|
|
3
|
+
export type AgentEvent =
|
|
4
|
+
| { type: "text"; text: string } // assistant text (delta or full chunk)
|
|
5
|
+
| { type: "tool_use"; name: string; summary: string } // tool starting
|
|
6
|
+
| { type: "tool_result"; ok: boolean; summary: string } // tool finished
|
|
7
|
+
| { type: "thinking"; text: string }
|
|
8
|
+
| { type: "system"; text: string }
|
|
9
|
+
| { type: "result"; ok: boolean; costUSD?: number }
|
|
10
|
+
| { type: "error"; message: string }
|
|
11
|
+
|
|
12
|
+
export type RunInput = {
|
|
13
|
+
prompt: string
|
|
14
|
+
cwd?: string
|
|
15
|
+
signal?: AbortSignal
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Agent {
|
|
19
|
+
readonly name: string
|
|
20
|
+
/** Yield events as the agent runs. Should complete (or throw) when the run finishes. */
|
|
21
|
+
run(input: RunInput): AsyncIterable<AgentEvent>
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
|
|
3
|
+
import * as p from "@clack/prompts"
|
|
4
|
+
|
|
5
|
+
import { agentNames, getAgent } from "../agents/index.js"
|
|
6
|
+
|
|
7
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
|
|
8
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`
|
|
9
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`
|
|
10
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`
|
|
11
|
+
|
|
12
|
+
/** Undocumented: send "say hello world" to a chosen agent through the meta-library
|
|
13
|
+
* and stream events to stdout. Use this to verify each adapter end-to-end. */
|
|
14
|
+
export const agentTest = async () => {
|
|
15
|
+
p.intro("Agent SDK Test")
|
|
16
|
+
|
|
17
|
+
const choice = await p.select({
|
|
18
|
+
message: "Which agent?",
|
|
19
|
+
options: agentNames().map((name) => ({ value: name, label: name })),
|
|
20
|
+
initialValue: "claude",
|
|
21
|
+
})
|
|
22
|
+
if (p.isCancel(choice)) process.exit(0)
|
|
23
|
+
const agent = getAgent(String(choice))
|
|
24
|
+
if (!agent) {
|
|
25
|
+
p.log.error(`Unknown agent: ${choice}`)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const prompt = "say hello world"
|
|
30
|
+
const cwd = process.cwd()
|
|
31
|
+
const debugPath = "/tmp/puzzmo-agent-test.log"
|
|
32
|
+
fs.writeFileSync(debugPath, "")
|
|
33
|
+
const log = (msg: string) => fs.appendFileSync(debugPath, `[${new Date().toISOString()}] ${msg}\n`)
|
|
34
|
+
|
|
35
|
+
log(`agent=${agent.name} cwd=${cwd}`)
|
|
36
|
+
log(`prompt=${JSON.stringify(prompt)}`)
|
|
37
|
+
|
|
38
|
+
process.stderr.write(`\n${dim("[agent]")} ${agent.name}\n`)
|
|
39
|
+
process.stderr.write(`${dim("[cwd]")} ${cwd}\n`)
|
|
40
|
+
process.stderr.write(`${dim("[log]")} ${debugPath}\n`)
|
|
41
|
+
process.stderr.write(`${dim("---- events below (Ctrl+C to abort) ----")}\n\n`)
|
|
42
|
+
|
|
43
|
+
const controller = new AbortController()
|
|
44
|
+
const onSig = () => {
|
|
45
|
+
process.stderr.write(`\n${dim("[interrupt]")}\n`)
|
|
46
|
+
controller.abort()
|
|
47
|
+
}
|
|
48
|
+
process.on("SIGINT", onSig)
|
|
49
|
+
|
|
50
|
+
const start = Date.now()
|
|
51
|
+
let count = 0
|
|
52
|
+
try {
|
|
53
|
+
for await (const event of agent.run({ prompt, cwd, signal: controller.signal })) {
|
|
54
|
+
count++
|
|
55
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(3)
|
|
56
|
+
log(`+${elapsed}s ${event.type} ${JSON.stringify(event).slice(0, 400)}`)
|
|
57
|
+
printEvent(event)
|
|
58
|
+
}
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
process.stderr.write(`\n${red("[run threw]")} ${e.message ?? e}\n`)
|
|
61
|
+
log(`THROW ${e.message ?? e}`)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
} finally {
|
|
64
|
+
process.off("SIGINT", onSig)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(3)
|
|
68
|
+
process.stderr.write(`\n${dim(`[+${elapsed}s done]`)} events=${count}\n`)
|
|
69
|
+
log(`done events=${count} elapsed=${elapsed}s`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const printEvent = (e: { type: string } & Record<string, any>) => {
|
|
73
|
+
if (e.type === "text") process.stdout.write(e.text)
|
|
74
|
+
else if (e.type === "thinking") process.stdout.write(dim(e.text))
|
|
75
|
+
else if (e.type === "tool_use") process.stdout.write(`\n${yellow(`[${e.name}]`)} ${dim(e.summary ?? "")}\n`)
|
|
76
|
+
else if (e.type === "tool_result") process.stdout.write(`${e.ok ? green(" ✓") : red(" ✗")} ${dim(e.summary ?? "")}\n`)
|
|
77
|
+
else if (e.type === "system") process.stdout.write(dim(`\n[${e.text}]\n`))
|
|
78
|
+
else if (e.type === "result")
|
|
79
|
+
process.stdout.write(`\n${e.ok ? green("== Done") : red("== Failed")}${e.costUSD ? ` ($${e.costUSD.toFixed(4)})` : ""}\n`)
|
|
80
|
+
else if (e.type === "error") process.stdout.write(red(`\n[error] ${e.message}\n`))
|
|
81
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path"
|
|
|
3
3
|
import { createRequire } from "node:module"
|
|
4
4
|
import * as p from "@clack/prompts"
|
|
5
5
|
|
|
6
|
+
import { agentNames } from "../../agents/index.js"
|
|
6
7
|
import { detectAgent } from "../../wizard/agent-detect.js"
|
|
7
8
|
import { downloadPage } from "../../download/page-downloader.js"
|
|
8
9
|
import { runCommand, gitCommit } from "../../util/exec.js"
|
|
@@ -260,10 +261,11 @@ export const gameCreate = async (args: string[]) => {
|
|
|
260
261
|
gameDir = setupRepoGame(tmpDir, slug, repo.repoRoot, parentFolder)
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
// Step 7: Detect agent and run skills pipeline
|
|
264
|
-
const
|
|
264
|
+
// Step 7: Detect agent and run skills pipeline. Only offer CLIs we have an adapter for.
|
|
265
|
+
const supported = new Set(agentNames())
|
|
266
|
+
const agents = detectAgent().filter((a) => supported.has(a.id))
|
|
265
267
|
const agentChoices = [
|
|
266
|
-
...agents.map((a) => ({ value: a.
|
|
268
|
+
...agents.map((a) => ({ value: a.id, label: `${a.displayName} (${a.path})` })),
|
|
267
269
|
{ value: "none", label: "None - I'll run the steps manually" },
|
|
268
270
|
]
|
|
269
271
|
|
|
@@ -274,7 +276,7 @@ export const gameCreate = async (args: string[]) => {
|
|
|
274
276
|
selectedAgent = (await p.select({ message: "Which LLM agent do you use?", options: agentChoices })) as string
|
|
275
277
|
if (p.isCancel(selectedAgent)) process.exit(0)
|
|
276
278
|
} else {
|
|
277
|
-
p.log.info(
|
|
279
|
+
p.log.info(`No supported LLM agents detected (checked: ${agentNames().join(", ")})`)
|
|
278
280
|
selectedAgent = "none"
|
|
279
281
|
}
|
|
280
282
|
|
package/src/commands/upload.ts
CHANGED
|
@@ -7,45 +7,13 @@ import { uploadFiles } from "../util/api.js"
|
|
|
7
7
|
import { getAPIURL, getToken } from "../util/config.js"
|
|
8
8
|
import { validatePuzzmoJson } from "../util/validatePuzzmoFile.js"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
13
|
-
const files: string[] = []
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
const full = path.join(dir, entry.name)
|
|
16
|
-
if (entry.isDirectory()) files.push(...collectFiles(full))
|
|
17
|
-
else files.push(full)
|
|
18
|
-
}
|
|
19
|
-
return files
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
23
|
-
const getGitSHA = (): string | null => {
|
|
24
|
-
try {
|
|
25
|
-
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
|
|
26
|
-
} catch {
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Hashes all file contents to produce a deterministic SHA */
|
|
32
|
-
const hashFiles = (filePaths: string[]): string => {
|
|
33
|
-
const hash = crypto.createHash("sha256")
|
|
34
|
-
for (const fp of filePaths.sort()) {
|
|
35
|
-
hash.update(fs.readFileSync(fp))
|
|
36
|
-
}
|
|
37
|
-
return hash.digest("hex").slice(0, 12)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Formats a byte count as a human-readable string */
|
|
41
|
-
const formatBytes = (bytes: number): string => {
|
|
42
|
-
if (bytes < 1024) return `${bytes} B`
|
|
43
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
44
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
10
|
+
type UploadOptions = {
|
|
11
|
+
verbose?: boolean
|
|
45
12
|
}
|
|
46
13
|
|
|
47
14
|
/** Uploads game build artifacts to Puzzmo */
|
|
48
|
-
export const upload = async (dir: string) => {
|
|
15
|
+
export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
16
|
+
const { verbose = false } = options
|
|
49
17
|
const token = getToken()
|
|
50
18
|
if (!token) {
|
|
51
19
|
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
|
|
@@ -111,10 +79,56 @@ export const upload = async (dir: string) => {
|
|
|
111
79
|
if (apiURL !== defaultURL) console.log(`Uploading to ${apiURL}...`)
|
|
112
80
|
else console.log("Uploading...")
|
|
113
81
|
|
|
114
|
-
const result = await uploadFiles(
|
|
115
|
-
|
|
116
|
-
|
|
82
|
+
const result = await uploadFiles(
|
|
83
|
+
token,
|
|
84
|
+
gameSlug,
|
|
85
|
+
sha,
|
|
86
|
+
files,
|
|
87
|
+
distDir,
|
|
88
|
+
puzzmoFile,
|
|
89
|
+
(batch, totalBatches, uploaded) => {
|
|
90
|
+
console.log(` Batch ${batch}/${totalBatches} done (${uploaded} file(s) uploaded)`)
|
|
91
|
+
},
|
|
92
|
+
{ verbose },
|
|
93
|
+
)
|
|
117
94
|
|
|
118
95
|
console.log(`\nDone - ${result.versionID}`)
|
|
119
96
|
console.log(`Assets: ${result.assetsBase}`)
|
|
120
97
|
}
|
|
98
|
+
|
|
99
|
+
/** Collects all files in a directory recursively */
|
|
100
|
+
const collectFiles = (dir: string): string[] => {
|
|
101
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
102
|
+
const files: string[] = []
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const full = path.join(dir, entry.name)
|
|
105
|
+
if (entry.isDirectory()) files.push(...collectFiles(full))
|
|
106
|
+
else files.push(full)
|
|
107
|
+
}
|
|
108
|
+
return files
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Tries to get the shortest unique git SHA, returns null if not in a git repo */
|
|
112
|
+
const getGitSHA = (): string | null => {
|
|
113
|
+
try {
|
|
114
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim()
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Hashes all file contents to produce a deterministic SHA */
|
|
121
|
+
const hashFiles = (filePaths: string[]): string => {
|
|
122
|
+
const hash = crypto.createHash("sha256")
|
|
123
|
+
for (const fp of filePaths.sort()) {
|
|
124
|
+
hash.update(fs.readFileSync(fp))
|
|
125
|
+
}
|
|
126
|
+
return hash.digest("hex").slice(0, 12)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Formats a byte count as a human-readable string */
|
|
130
|
+
const formatBytes = (bytes: number): string => {
|
|
131
|
+
if (bytes < 1024) return `${bytes} B`
|
|
132
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
133
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
134
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { agentTest } from "./commands/agent-test.js"
|
|
3
4
|
import { login } from "./commands/login.js"
|
|
4
5
|
import { upload } from "./commands/upload.js"
|
|
5
6
|
import { validate } from "./commands/validate.js"
|
|
@@ -13,7 +14,7 @@ const printUsage = () => {
|
|
|
13
14
|
puzzmo login <token> Save a CLI auth token
|
|
14
15
|
puzzmo game create [token] Create a new Puzzmo game project
|
|
15
16
|
|
|
16
|
-
puzzmo upload <dir>
|
|
17
|
+
puzzmo upload <dir> [-v] Upload game build from <dir> (slug from puzzmo.json). -v/--verbose prints request URLs and full error bodies.
|
|
17
18
|
puzzmo validate [dir] Validate puzzmo.json in a directory (default: .)
|
|
18
19
|
puzzmo migrate List and select migration skills from dev.puzzmo.com`)
|
|
19
20
|
}
|
|
@@ -30,12 +31,13 @@ const run = async () => {
|
|
|
30
31
|
break
|
|
31
32
|
}
|
|
32
33
|
case "upload": {
|
|
33
|
-
const
|
|
34
|
+
const verbose = args.includes("--verbose") || args.includes("-v")
|
|
35
|
+
const dir = args.find((a) => !a.startsWith("-"))
|
|
34
36
|
if (!dir) {
|
|
35
|
-
console.error("Usage: puzzmo upload <dir>")
|
|
37
|
+
console.error("Usage: puzzmo upload <dir> [-v|--verbose]")
|
|
36
38
|
process.exit(1)
|
|
37
39
|
}
|
|
38
|
-
await upload(dir)
|
|
40
|
+
await upload(dir, { verbose })
|
|
39
41
|
break
|
|
40
42
|
}
|
|
41
43
|
case "game": {
|
|
@@ -56,6 +58,10 @@ const run = async () => {
|
|
|
56
58
|
await migrate()
|
|
57
59
|
break
|
|
58
60
|
}
|
|
61
|
+
case "agent-test": {
|
|
62
|
+
await agentTest()
|
|
63
|
+
break
|
|
64
|
+
}
|
|
59
65
|
default:
|
|
60
66
|
printUsage()
|
|
61
67
|
break
|