@playcraft/cli 0.0.57 → 0.0.58

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 (225) hide show
  1. package/README.md +34 -16
  2. package/dist/cli-root-help.js +1 -0
  3. package/dist/commands/build-all.js +10 -9
  4. package/dist/commands/build.js +13 -12
  5. package/dist/commands/create.js +26 -33
  6. package/dist/commands/platform-skills.generated.js +19 -0
  7. package/dist/commands/skills.js +2 -0
  8. package/dist/commands/tools-generation.js +1 -1
  9. package/dist/commands/workspace-runtime.js +54 -0
  10. package/dist/index.js +3 -1
  11. package/dist/project-skills/commands.js +72 -0
  12. package/dist/project-skills/lifecycle.js +28 -0
  13. package/dist/project-skills/local-run.js +112 -0
  14. package/dist/project-skills/local-store.js +149 -0
  15. package/dist/project-skills/messages.js +72 -0
  16. package/dist/project-skills/reconcile.js +62 -0
  17. package/dist/project-skills/remote-cache.js +51 -0
  18. package/dist/project-skills/validation.js +19 -0
  19. package/dist/remix/clone.js +2 -0
  20. package/dist/remix/init-template.js +2 -0
  21. package/dist/remix/pull.js +2 -0
  22. package/dist/remix/push.js +21 -1
  23. package/dist/utils/agent-api-client.js +54 -17
  24. package/dist/utils/tool-operation-journal.js +54 -0
  25. package/dist/workspace-runtime/codex/app-server.js +395 -0
  26. package/dist/workspace-runtime/codex/config-toml.js +25 -0
  27. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +106 -0
  28. package/dist/workspace-runtime/codex/loopback.js +59 -0
  29. package/dist/workspace-runtime/codex/native-adapter.js +3 -0
  30. package/dist/workspace-runtime/main.js +10 -0
  31. package/dist/workspace-runtime/persistence/journal.js +350 -0
  32. package/dist/workspace-runtime/processes/managed-writes.js +69 -0
  33. package/dist/workspace-runtime/serve.js +111 -0
  34. package/dist/workspace-runtime/server/auth.js +21 -0
  35. package/dist/workspace-runtime/server/dispatch.js +861 -0
  36. package/dist/workspace-runtime/server/execution-group.js +88 -0
  37. package/dist/workspace-runtime/server/http.js +561 -0
  38. package/dist/workspace-runtime/server/lock.js +53 -0
  39. package/dist/workspace-runtime/server/types.js +1 -0
  40. package/dist/workspace-runtime/workspaces/context-error.js +2 -0
  41. package/dist/workspace-runtime/workspaces/file-snapshot.js +213 -0
  42. package/dist/workspace-runtime/workspaces/files.js +143 -0
  43. package/dist/workspace-runtime/workspaces/git.js +242 -0
  44. package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
  45. package/dist/workspace-runtime/workspaces/parameters.js +272 -0
  46. package/dist/workspace-runtime/workspaces/prepare.js +364 -0
  47. package/dist/workspace-runtime/workspaces/registry.js +107 -0
  48. package/dist/workspace-runtime/workspaces/revisions.js +31 -0
  49. package/package.json +4 -1
  50. package/project-template-v2/.claude/agents/artist.md +82 -0
  51. package/project-template-v2/.claude/agents/developer.md +153 -0
  52. package/project-template-v2/.claude/agents/game-designer.md +264 -0
  53. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
  54. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
  55. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
  56. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
  57. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
  58. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
  59. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
  60. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
  61. package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
  62. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
  63. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
  64. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  65. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  66. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
  67. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  68. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
  69. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
  70. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  71. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
  72. package/project-template-v2/.claude/agents/reviewer.md +75 -0
  73. package/project-template-v2/.claude/agents/technical-artist.md +86 -0
  74. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
  75. package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
  76. package/project-template-v2/.claude/settings.json +33 -0
  77. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
  78. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
  79. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
  80. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
  81. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
  82. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
  83. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
  84. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  85. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
  86. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  87. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  88. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
  89. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  90. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  91. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  92. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
  93. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  94. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  95. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
  96. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  97. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  98. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  99. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  100. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  101. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  102. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
  103. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
  104. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
  105. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
  106. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
  107. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  108. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
  109. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
  110. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  111. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  112. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  113. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  114. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
  115. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  116. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  117. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  118. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
  119. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
  120. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
  121. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  122. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
  123. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
  124. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
  125. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
  126. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
  127. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
  128. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
  129. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
  130. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
  131. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
  132. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
  133. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
  134. package/project-template-v2/.claude/snapshot.mjs +263 -0
  135. package/project-template-v2/.playcraft/skills.lock.json +152 -0
  136. package/project-template-v2/CLAUDE.md +146 -0
  137. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  138. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  139. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  140. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  141. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  142. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  143. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  144. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  145. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  146. package/project-template-v2/assets/models/.gitkeep +0 -0
  147. package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
  148. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
  149. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
  150. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
  151. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
  152. package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
  153. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
  154. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
  155. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
  156. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
  157. package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
  158. package/project-template-v2/docs/project-state.json +60 -0
  159. package/project-template-v2/docs/project-state.md +72 -0
  160. package/project-template-v2/docs/standards/README.md +225 -0
  161. package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
  162. package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
  163. package/project-template-v2/docs/standards/artifacts/design.md +22 -0
  164. package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
  165. package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
  166. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
  167. package/project-template-v2/game/index.ts +18 -0
  168. package/project-template-v2/globals.d.ts +51 -0
  169. package/project-template-v2/index.css +34 -0
  170. package/project-template-v2/index.html +18 -0
  171. package/project-template-v2/main.ts +9 -0
  172. package/project-template-v2/package.json +46 -0
  173. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
  174. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
  175. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
  176. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
  177. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
  178. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
  179. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
  180. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
  181. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
  182. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
  183. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
  184. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
  185. package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
  186. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
  187. package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
  188. package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
  189. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
  190. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
  191. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
  192. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
  193. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
  194. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
  195. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
  196. package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
  197. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
  198. package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
  199. package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
  200. package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
  201. package/project-template-v2/skills/playable-record/SKILL.md +140 -0
  202. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
  203. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
  204. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
  205. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
  206. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
  207. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
  208. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
  209. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
  210. package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
  211. package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
  212. package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
  213. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
  214. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
  215. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
  216. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
  217. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
  218. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
  219. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
  220. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
  221. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
  222. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
  223. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  224. package/project-template-v2/tsconfig.json +20 -0
  225. package/project-template-v2/vite.config.ts +27 -0
@@ -3,6 +3,8 @@ import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { loadGlobalConfig } from '../config.js';
5
5
  import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
6
+ import { AGENT_TOOL_OPERATION_PROTOCOL, AGENT_TOOL_OPERATION_HEADERS, AGENT_TOOL_OPERATION_CAPABILITIES_PATH, } from '@playcraft/common/agent-tools';
7
+ import { beginToolOperation } from './tool-operation-journal.js';
6
8
  /**
7
9
  * 判断 token 是否为用户 PAT(Personal Access Token)。
8
10
  * PAT 以 "playcraft_pat_" 开头;沙箱 JWT 为三段式 base64。
@@ -48,9 +50,7 @@ export class AgentApiClient {
48
50
  this.apiUrl = apiUrl;
49
51
  this.token = token;
50
52
  this.isPat = isPatToken(token);
51
- this.playcraftEnv = (process.env.PLAYCRAFT_ENV ||
52
- localConfig.env ||
53
- '').trim();
53
+ this.playcraftEnv = (process.env.PLAYCRAFT_ENV || localConfig.env || '').trim();
54
54
  }
55
55
  /** 根据 token 类型构造鉴权 headers */
56
56
  authHeaders() {
@@ -67,21 +67,58 @@ export class AgentApiClient {
67
67
  const p = path.startsWith('/') ? path : `/${path}`;
68
68
  return new URL(`/api/agent/tools${p}`, this.apiUrl);
69
69
  }
70
- async post(path, body) {
71
- const url = this.toolsRequestUrl(path).href;
72
- const res = await fetch(url, {
73
- method: 'POST',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- ...this.authHeaders(),
77
- },
78
- body: JSON.stringify(body),
79
- });
80
- if (!res.ok) {
81
- const text = await res.text().catch(() => res.statusText);
82
- throw new Error(`API ${res.status}: ${text}`);
70
+ async post(path, body, options) {
71
+ const url = this.toolsRequestUrl(path);
72
+ const serializedBody = JSON.stringify(body);
73
+ if (serializedBody === undefined)
74
+ throw new Error('Tool request body is required');
75
+ const operation = beginToolOperation(url.pathname, serializedBody, options?.requestId ?? process.env.PLAYCRAFT_TOOL_REQUEST_ID);
76
+ try {
77
+ const capabilityResponse = await fetch(this.toolsRequestUrl(AGENT_TOOL_OPERATION_CAPABILITIES_PATH), {
78
+ headers: this.authHeaders(),
79
+ redirect: 'error',
80
+ signal: AbortSignal.timeout(30_000),
81
+ });
82
+ if (!capabilityResponse.ok) {
83
+ await capabilityResponse.body?.cancel();
84
+ throw new Error(`Business tool protocol unavailable (${capabilityResponse.status}); no operation was sent`);
85
+ }
86
+ const capabilities = (await capabilityResponse.json());
87
+ if (capabilities.protocol !== AGENT_TOOL_OPERATION_PROTOCOL ||
88
+ capabilities.settledOnly !== true)
89
+ throw new Error('Business tool protocol is incompatible; no operation was sent');
90
+ const res = await fetch(url, {
91
+ method: 'POST',
92
+ redirect: 'error',
93
+ signal: AbortSignal.timeout(30 * 60_000),
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ ...this.authHeaders(),
97
+ [AGENT_TOOL_OPERATION_HEADERS.protocol]: AGENT_TOOL_OPERATION_PROTOCOL,
98
+ [AGENT_TOOL_OPERATION_HEADERS.requestId]: operation.requestId,
99
+ ...(operation.settledOnly ? { [AGENT_TOOL_OPERATION_HEADERS.settledOnly]: 'true' } : {}),
100
+ },
101
+ body: serializedBody,
102
+ });
103
+ if (res.headers.get(AGENT_TOOL_OPERATION_HEADERS.protocol) !== AGENT_TOOL_OPERATION_PROTOCOL ||
104
+ res.headers.get(AGENT_TOOL_OPERATION_HEADERS.requestId) !== operation.requestId) {
105
+ await res.body?.cancel();
106
+ throw new Error('Business did not acknowledge a durable receipt; outcome remains unconfirmed');
107
+ }
108
+ if (!res.ok) {
109
+ const text = await res.text().catch(() => res.statusText);
110
+ throw new Error(`API ${res.status}: ${text}`);
111
+ }
112
+ const result = (await res.json());
113
+ operation.complete();
114
+ return result;
115
+ }
116
+ catch (error) {
117
+ throw new Error(`${error instanceof Error ? error.message : String(error)}\n` +
118
+ `Tool request ${operation.requestId}; record: ${operation.file}. ` +
119
+ `To query its settled result, rerun the same command with PLAYCRAFT_TOOL_REQUEST_ID=${operation.requestId}. ` +
120
+ 'This recovery does not start another operation.');
83
121
  }
84
- return res.json();
85
122
  }
86
123
  async get(path, params) {
87
124
  const url = this.toolsRequestUrl(path);
@@ -0,0 +1,54 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { isAbsolute, join } from 'node:path';
5
+ import { isAgentToolRequestId } from '@playcraft/common/agent-tools';
6
+ /** Persist an identity before sending. Store neither credentials nor request/response bodies. */
7
+ export function beginToolOperation(path, serializedBody, recoverId) {
8
+ const identifier = recoverId ?? randomUUID();
9
+ if (!isAgentToolRequestId(identifier))
10
+ throw new Error('Invalid PLAYCRAFT_TOOL_REQUEST_ID');
11
+ const requestId = identifier.toLowerCase();
12
+ const directory = process.env.PLAYCRAFT_TOOL_REQUEST_DIR ?? join(homedir(), '.playcraft', 'tool-requests');
13
+ if (!isAbsolute(directory))
14
+ throw new Error('PLAYCRAFT_TOOL_REQUEST_DIR must be an absolute path');
15
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
16
+ const file = join(directory, `${requestId}.json`);
17
+ const inputHash = createHash('sha256').update(serializedBody).digest('hex');
18
+ const now = new Date().toISOString();
19
+ const entry = {
20
+ version: 1,
21
+ requestId,
22
+ path,
23
+ inputHash,
24
+ state: 'unconfirmed',
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ };
28
+ let settledOnly = recoverId !== undefined;
29
+ try {
30
+ writeFileSync(file, JSON.stringify(entry), { flag: 'wx', mode: 0o600 });
31
+ }
32
+ catch (error) {
33
+ if (error.code !== 'EEXIST')
34
+ throw error;
35
+ const previous = JSON.parse(readFileSync(file, 'utf8'));
36
+ if (previous.version !== 1 ||
37
+ previous.requestId !== requestId ||
38
+ previous.path !== path ||
39
+ previous.inputHash !== inputHash)
40
+ throw new Error(`Tool request ${requestId} conflicts with its durable local record`);
41
+ settledOnly = true;
42
+ }
43
+ return {
44
+ requestId,
45
+ settledOnly,
46
+ file,
47
+ complete() {
48
+ const current = JSON.parse(readFileSync(file, 'utf8'));
49
+ const temporary = `${file}.${randomUUID()}.tmp`;
50
+ writeFileSync(temporary, JSON.stringify({ ...current, state: 'completed', updatedAt: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
51
+ renameSync(temporary, file);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,395 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { writePlaycraftCodexConfig } from './config-toml.js';
5
+ import { createJsonRpcStdioClient } from './jsonrpc-stdio.js';
6
+ import { nativeEvent } from './native-adapter.js';
7
+ function asRecord(value) {
8
+ return value && typeof value === 'object' && !Array.isArray(value)
9
+ ? value
10
+ : {};
11
+ }
12
+ function nestedId(value, key) {
13
+ const record = asRecord(value);
14
+ const nested = asRecord(record[key]);
15
+ return typeof nested.id === 'string' ? nested.id : undefined;
16
+ }
17
+ function waitForChildExit(child) {
18
+ if (child.exitCode !== null || child.signalCode !== null)
19
+ return Promise.resolve();
20
+ return new Promise((resolve) => {
21
+ const done = () => {
22
+ clearTimeout(timer);
23
+ resolve();
24
+ };
25
+ const timer = setTimeout(() => {
26
+ if (child.exitCode === null && child.signalCode === null)
27
+ child.kill('SIGKILL');
28
+ }, 3_000);
29
+ child.once('exit', done);
30
+ if (child.exitCode !== null || child.signalCode !== null)
31
+ done();
32
+ });
33
+ }
34
+ function spawnCommand(bin) {
35
+ if (bin.endsWith('.mjs') || bin.endsWith('.js') || bin.endsWith('.ts')) {
36
+ return { command: process.execPath, prefix: [bin] };
37
+ }
38
+ return { command: bin, prefix: [] };
39
+ }
40
+ async function readToken(options) {
41
+ if (options.token?.trim())
42
+ return options.token.trim();
43
+ const fromEnv = process.env.PLAYCRAFT_MODEL_PROXY_TOKEN?.trim() || process.env.OPENAI_API_KEY?.trim() || '';
44
+ if (fromEnv)
45
+ return fromEnv;
46
+ const tokenFile = options.tokenFile || process.env.PLAYCRAFT_MODEL_PROXY_TOKEN_FILE;
47
+ if (tokenFile) {
48
+ return (await readFile(tokenFile, 'utf8')).trim();
49
+ }
50
+ throw new Error('model_proxy_token_missing');
51
+ }
52
+ export function createCodexAppServerAdapter(options) {
53
+ const homeDir = options.homeDir || process.env.PLAYCRAFT_CODEX_HOME || join(options.config.stateDir, 'codex-home');
54
+ const model = options.model || process.env.PLAYCRAFT_CODEX_MODEL || 'gpt-4o';
55
+ const turnTimeoutMs = options.turnTimeoutMs ?? Number(process.env.PLAYCRAFT_CODEX_TURN_TIMEOUT_MS || 120_000);
56
+ const threads = new Map();
57
+ const approvals = new Map();
58
+ let child;
59
+ let rpc;
60
+ let readyFlag = false;
61
+ let closed = false;
62
+ let startPromise;
63
+ let live;
64
+ const exitListeners = new Set();
65
+ const unmatchedTurns = new Map();
66
+ const turnWaiters = new Map();
67
+ function finishTurn(turnId, params, waiter) {
68
+ turnWaiters.delete(turnId);
69
+ unmatchedTurns.delete(turnId);
70
+ const status = String(asRecord(asRecord(params).turn).status ?? 'completed');
71
+ const outcome = status === 'interrupted' ? 'interrupted' : status === 'failed' ? 'failed' : 'completed';
72
+ waiter.resolve({ outcome, turnId, events: waiter.events });
73
+ }
74
+ async function ensureStarted() {
75
+ if (readyFlag && rpc)
76
+ return;
77
+ startPromise ??= start();
78
+ await startPromise;
79
+ }
80
+ async function start() {
81
+ const token = await readToken(options);
82
+ await writePlaycraftCodexConfig({
83
+ homeDir,
84
+ modelProxyUrl: options.config.modelProxyUrl,
85
+ model,
86
+ });
87
+ const bin = options.command || process.env.PLAYCRAFT_CODEX_BIN || 'codex';
88
+ const { command, prefix } = spawnCommand(bin);
89
+ const args = [...prefix, ...(options.args ?? ['app-server'])];
90
+ child = spawn(command, args, {
91
+ cwd: options.config.workspaceRoot,
92
+ env: {
93
+ ...process.env,
94
+ ...options.env,
95
+ CODEX_HOME: homeDir,
96
+ OPENAI_API_KEY: token,
97
+ PLAYCRAFT_SKIP_UPDATE_CHECK: '1',
98
+ },
99
+ stdio: ['pipe', 'pipe', 'pipe'],
100
+ });
101
+ child.stderr?.on('data', (chunk) => {
102
+ process.stderr.write(chunk);
103
+ });
104
+ rpc = createJsonRpcStdioClient(child);
105
+ rpc.onNotification((method, params) => {
106
+ const turnId = nestedId(params, 'turn') ?? String(asRecord(params).turnId ?? '');
107
+ const event = nativeEvent(method, params);
108
+ if (!turnId)
109
+ return;
110
+ const waiter = turnWaiters.get(turnId);
111
+ if (waiter) {
112
+ waiter.events.push(event);
113
+ if (method === 'turn/completed')
114
+ finishTurn(turnId, params, waiter);
115
+ return;
116
+ }
117
+ const queued = unmatchedTurns.get(turnId) ?? [];
118
+ queued.push(event);
119
+ unmatchedTurns.set(turnId, queued);
120
+ });
121
+ rpc.onServerRequest((id, method, params) => {
122
+ const payload = asRecord(params);
123
+ const approvalId = String(payload.itemId ?? id);
124
+ const turnId = typeof payload.turnId === 'string' ? payload.turnId : '';
125
+ approvals.set(JSON.stringify([String(payload.threadId ?? ''), approvalId]), {
126
+ rpcId: id,
127
+ threadId: String(payload.threadId ?? ''),
128
+ turnId: turnId || undefined,
129
+ });
130
+ const event = nativeEvent(method, params);
131
+ const waiter = turnId ? turnWaiters.get(turnId) : undefined;
132
+ if (waiter) {
133
+ waiter.events.push(event);
134
+ turnWaiters.delete(turnId);
135
+ waiter.resolve({ outcome: 'completed', turnId, approvalId, events: waiter.events });
136
+ return;
137
+ }
138
+ if (turnId) {
139
+ const queued = unmatchedTurns.get(turnId) ?? [];
140
+ queued.push(event);
141
+ unmatchedTurns.set(turnId, queued);
142
+ }
143
+ });
144
+ const pid = child.pid;
145
+ if (!pid)
146
+ throw new Error('codex_pid_missing');
147
+ const current = {
148
+ child,
149
+ rpc,
150
+ pid,
151
+ startedAt: new Date().toISOString(),
152
+ superseded: false,
153
+ };
154
+ live = current;
155
+ child.once('exit', () => {
156
+ if (live === current) {
157
+ readyFlag = false;
158
+ live = undefined;
159
+ rpc = undefined;
160
+ child = undefined;
161
+ for (const [turnId, waiter] of turnWaiters) {
162
+ turnWaiters.delete(turnId);
163
+ waiter.resolve({ outcome: 'uncertain', turnId, events: waiter.events });
164
+ }
165
+ }
166
+ if (!current.superseded && !closed) {
167
+ for (const listener of exitListeners)
168
+ listener({ pid: current.pid });
169
+ }
170
+ });
171
+ await rpc.request('initialize', {
172
+ clientInfo: {
173
+ name: 'playcraft_workspace_runtime',
174
+ title: 'PlayCraft Workspace Runtime',
175
+ version: options.config.cliVersion,
176
+ },
177
+ });
178
+ rpc.notify('initialized', {});
179
+ readyFlag = true;
180
+ }
181
+ return {
182
+ name: 'codex-app-server',
183
+ ready: () => readyFlag && !closed,
184
+ executionProcess() {
185
+ if (!live)
186
+ return undefined;
187
+ return { pid: live.pid, startedAt: live.startedAt };
188
+ },
189
+ onUnexpectedExit(listener) {
190
+ exitListeners.add(listener);
191
+ },
192
+ async replaceProcess() {
193
+ const previous = live;
194
+ if (previous)
195
+ previous.superseded = true;
196
+ readyFlag = false;
197
+ startPromise = undefined;
198
+ threads.clear();
199
+ approvals.clear();
200
+ turnWaiters.clear();
201
+ unmatchedTurns.clear();
202
+ if (previous && previous.child.exitCode === null && previous.child.signalCode === null) {
203
+ previous.child.kill('SIGTERM');
204
+ }
205
+ await previous?.rpc.close().catch(() => undefined);
206
+ if (previous)
207
+ await waitForChildExit(previous.child);
208
+ if (closed)
209
+ throw new Error('codex_not_started');
210
+ try {
211
+ startPromise = start();
212
+ await startPromise;
213
+ }
214
+ catch (error) {
215
+ startPromise = undefined;
216
+ throw error;
217
+ }
218
+ const next = live;
219
+ if (!next)
220
+ throw new Error('codex_not_started');
221
+ return { pid: next.pid, startedAt: next.startedAt };
222
+ },
223
+ async canContinueThread(input) {
224
+ await ensureStarted();
225
+ if (!rpc)
226
+ return false;
227
+ try {
228
+ const result = await rpc.request('thread/resume', { threadId: input.threadId, cwd: input.cwd }, 30_000);
229
+ const resumedId = result.thread?.id ?? result.threadId;
230
+ if (resumedId !== input.threadId)
231
+ return false;
232
+ threads.set(input.threadId, { sessionId: input.sessionId });
233
+ return true;
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ },
239
+ async captureNativeCheckpoint(input) {
240
+ const thread = threads.get(input.threadId);
241
+ if (!thread || thread.sessionId !== input.sessionId)
242
+ return undefined;
243
+ if (!thread.lastTurnId)
244
+ return { threadId: input.threadId };
245
+ return { threadId: input.threadId, nativeDurableTurnId: thread.lastTurnId };
246
+ },
247
+ async start() {
248
+ await ensureStarted();
249
+ },
250
+ async startThread(input) {
251
+ await ensureStarted();
252
+ if (!rpc)
253
+ throw new Error('codex_not_started');
254
+ const result = await rpc.request('thread/start', {
255
+ cwd: input.cwd,
256
+ model,
257
+ });
258
+ const threadId = result.thread?.id;
259
+ if (!threadId)
260
+ throw new Error('native_receipt_lost');
261
+ threads.set(threadId, { sessionId: input.sessionId });
262
+ return { threadId };
263
+ },
264
+ async startTurn(input) {
265
+ await ensureStarted();
266
+ if (!rpc)
267
+ throw new Error('codex_not_started');
268
+ if (input.simulateLostReceipt) {
269
+ return { outcome: 'uncertain', events: [] };
270
+ }
271
+ if (threads.get(input.threadId)?.sessionId !== input.sessionId) {
272
+ return {
273
+ outcome: 'failed',
274
+ events: [nativeEvent('turn.failed', { reason: 'unknown_thread' })],
275
+ };
276
+ }
277
+ const started = await rpc.request('turn/start', {
278
+ threadId: input.threadId,
279
+ input: [{ type: 'text', text: input.text }],
280
+ });
281
+ const turnId = started.turn?.id;
282
+ if (!turnId)
283
+ return { outcome: 'uncertain', events: [] };
284
+ const thread = threads.get(input.threadId);
285
+ if (thread)
286
+ thread.lastTurnId = turnId;
287
+ const queued = unmatchedTurns.get(turnId) ?? [];
288
+ unmatchedTurns.delete(turnId);
289
+ const events = [nativeEvent('turn/started', started), ...queued];
290
+ const alreadyDone = queued.find((event) => event.type === 'turn/completed');
291
+ if (alreadyDone) {
292
+ const status = String(asRecord(asRecord(alreadyDone.payload).turn).status ?? 'completed');
293
+ const outcome = status === 'interrupted' ? 'interrupted' : status === 'failed' ? 'failed' : 'completed';
294
+ return { outcome, turnId, events };
295
+ }
296
+ const approvalEvent = queued.find((event) => event.type.includes('requestApproval') || event.type.includes('requestUserInput'));
297
+ if (approvalEvent) {
298
+ const approvalId = String(asRecord(approvalEvent.payload).itemId ?? '');
299
+ return { outcome: 'completed', turnId, approvalId, events };
300
+ }
301
+ return await new Promise((resolve) => {
302
+ let timer;
303
+ const waiter = {
304
+ events,
305
+ resolve: (result) => {
306
+ clearTimeout(timer);
307
+ resolve(result);
308
+ },
309
+ };
310
+ timer = setTimeout(() => {
311
+ turnWaiters.delete(turnId);
312
+ resolve({ outcome: 'uncertain', turnId, events: waiter.events });
313
+ }, turnTimeoutMs);
314
+ turnWaiters.set(turnId, waiter);
315
+ });
316
+ },
317
+ async interrupt(input) {
318
+ await ensureStarted();
319
+ if (!rpc)
320
+ return { outcome: 'uncertain', events: [] };
321
+ if (threads.get(input.threadId)?.sessionId !== input.sessionId)
322
+ return { outcome: 'uncertain', events: [] };
323
+ const turnId = threads.get(input.threadId)?.lastTurnId;
324
+ if (!turnId)
325
+ return { outcome: 'uncertain', events: [] };
326
+ try {
327
+ await rpc.request('turn/interrupt', { threadId: input.threadId, turnId });
328
+ }
329
+ catch {
330
+ return { outcome: 'uncertain', events: [nativeEvent('turn.interrupted', { threadId: input.threadId })] };
331
+ }
332
+ return {
333
+ outcome: 'interrupted',
334
+ turnId,
335
+ events: [nativeEvent('turn.interrupted', { threadId: input.threadId, turnId })],
336
+ };
337
+ },
338
+ async respondApproval(input) {
339
+ if (!rpc) {
340
+ return {
341
+ outcome: 'failed',
342
+ events: [nativeEvent('approval.unknown', { approvalId: input.approvalId })],
343
+ };
344
+ }
345
+ const threadId = [...threads].find(([, thread]) => thread.sessionId === input.sessionId)?.[0];
346
+ const key = JSON.stringify([threadId, input.approvalId]);
347
+ const pending = approvals.get(key);
348
+ if (!pending) {
349
+ return {
350
+ outcome: 'failed',
351
+ events: [nativeEvent('approval.unknown', { approvalId: input.approvalId })],
352
+ };
353
+ }
354
+ approvals.delete(key);
355
+ rpc.respond(pending.rpcId, { decision: input.accepted ? 'accept' : 'decline' });
356
+ return {
357
+ outcome: 'completed',
358
+ approvalId: input.approvalId,
359
+ events: [
360
+ nativeEvent('approval.resolved', {
361
+ approvalId: input.approvalId,
362
+ accepted: input.accepted,
363
+ }),
364
+ ],
365
+ };
366
+ },
367
+ async close() {
368
+ if (closed)
369
+ return;
370
+ closed = true;
371
+ readyFlag = false;
372
+ if (live)
373
+ live.superseded = true;
374
+ const current = child;
375
+ await rpc?.close().catch(() => undefined);
376
+ rpc = undefined;
377
+ if (current && current.exitCode === null && !current.killed) {
378
+ current.kill('SIGTERM');
379
+ await new Promise((resolve) => {
380
+ const timer = setTimeout(() => {
381
+ if (current.exitCode === null)
382
+ current.kill('SIGKILL');
383
+ resolve();
384
+ }, 3000);
385
+ current.once('exit', () => {
386
+ clearTimeout(timer);
387
+ resolve();
388
+ });
389
+ });
390
+ }
391
+ child = undefined;
392
+ live = undefined;
393
+ },
394
+ };
395
+ }
@@ -0,0 +1,25 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ function tomlString(value) {
4
+ return JSON.stringify(value);
5
+ }
6
+ export async function writePlaycraftCodexConfig(input) {
7
+ await mkdir(input.homeDir, { recursive: true, mode: 0o700 });
8
+ const path = join(input.homeDir, 'config.toml');
9
+ const body = [
10
+ `model = ${tomlString(input.model)}`,
11
+ 'model_provider = "playcraft"',
12
+ 'approval_policy = "never"',
13
+ 'sandbox_mode = "danger-full-access"',
14
+ '',
15
+ '[model_providers.playcraft]',
16
+ 'name = "PlayCraft Model Proxy"',
17
+ `base_url = ${tomlString(input.modelProxyUrl.replace(/\/+$/, ''))}`,
18
+ 'env_key = "OPENAI_API_KEY"',
19
+ 'wire_api = "responses"',
20
+ 'requires_openai_auth = false',
21
+ '',
22
+ ].join('\n');
23
+ await writeFile(path, body, { encoding: 'utf8', mode: 0o600 });
24
+ return path;
25
+ }
@@ -0,0 +1,106 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export function createJsonRpcStdioClient(child) {
3
+ const stdin = child.stdin;
4
+ const stdout = child.stdout;
5
+ if (!stdin || !stdout)
6
+ throw new Error('codex_stdio_missing');
7
+ const emitter = new EventEmitter();
8
+ const pending = new Map();
9
+ let nextId = 1;
10
+ let buffer = '';
11
+ let closed = false;
12
+ const onData = (chunk) => {
13
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
14
+ let newline = buffer.indexOf('\n');
15
+ while (newline >= 0) {
16
+ const line = buffer.slice(0, newline).trim();
17
+ buffer = buffer.slice(newline + 1);
18
+ if (line)
19
+ handleLine(line);
20
+ newline = buffer.indexOf('\n');
21
+ }
22
+ };
23
+ stdout.setEncoding('utf8');
24
+ stdout.on('data', onData);
25
+ function handleLine(line) {
26
+ let message;
27
+ try {
28
+ message = JSON.parse(line);
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ if (message.id !== undefined && pending.has(Number(message.id))) {
34
+ const waiter = pending.get(Number(message.id));
35
+ if (!waiter)
36
+ return;
37
+ pending.delete(Number(message.id));
38
+ clearTimeout(waiter.timer);
39
+ if (message.error) {
40
+ waiter.reject(new Error(message.error.message || 'jsonrpc_error'));
41
+ return;
42
+ }
43
+ waiter.resolve(message.result);
44
+ return;
45
+ }
46
+ if (message.method && message.id !== undefined) {
47
+ emitter.emit('serverRequest', message.id, message.method, message.params ?? {});
48
+ return;
49
+ }
50
+ if (message.method) {
51
+ emitter.emit('notification', message.method, message.params ?? {});
52
+ }
53
+ }
54
+ const send = (payload) => {
55
+ if (closed)
56
+ throw new Error('jsonrpc_closed');
57
+ stdin.write(`${JSON.stringify(payload)}\n`);
58
+ };
59
+ return {
60
+ notify(method, params) {
61
+ send({ method, params: params ?? {} });
62
+ },
63
+ request(method, params, timeoutMs = 20_000) {
64
+ const id = nextId++;
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => {
67
+ pending.delete(id);
68
+ reject(new Error(`jsonrpc_timeout:${method}`));
69
+ }, timeoutMs);
70
+ pending.set(id, { resolve: (value) => resolve(value), reject, timer });
71
+ try {
72
+ send({ method, id, params: params ?? {} });
73
+ }
74
+ catch (error) {
75
+ pending.delete(id);
76
+ clearTimeout(timer);
77
+ reject(error instanceof Error ? error : new Error(String(error)));
78
+ }
79
+ });
80
+ },
81
+ onNotification(listener) {
82
+ emitter.on('notification', listener);
83
+ return () => emitter.off('notification', listener);
84
+ },
85
+ onServerRequest(listener) {
86
+ emitter.on('serverRequest', listener);
87
+ return () => emitter.off('serverRequest', listener);
88
+ },
89
+ respond(id, result) {
90
+ send({ id, result });
91
+ },
92
+ async close() {
93
+ if (closed)
94
+ return;
95
+ closed = true;
96
+ stdout.off('data', onData);
97
+ for (const [id, waiter] of pending) {
98
+ clearTimeout(waiter.timer);
99
+ waiter.reject(new Error('jsonrpc_closed'));
100
+ pending.delete(id);
101
+ }
102
+ if (!stdin.destroyed)
103
+ stdin.end();
104
+ },
105
+ };
106
+ }