@playcraft/cli 0.1.66 → 0.1.68
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/dist/assets/client.js +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-program.js +82 -0
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +13 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +257 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
- package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/git.js +7 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +9 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +2 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -42,7 +42,8 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
|
|
|
42
42
|
const archive = createCanonicalBundleArchive([{ path: 'SKILL.md', data: oldContent }]);
|
|
43
43
|
if (createHash('sha256').update(oldContent).digest('hex') !== ORIGINAL_BUNDLED_GUIDE
|
|
44
44
|
|| current.sourceTreeHash !== locked.sourceTreeHash
|
|
45
|
-
|| createHash('sha256').update(archive).digest('hex') !== locked.revision.artifactHash)
|
|
45
|
+
|| (previous.lock.lockVersion === 3 && createHash('sha256').update(archive).digest('hex') !== locked.revision.artifactHash)
|
|
46
|
+
|| (previous.lock.lockVersion === 4 && current.sourceTreeHash !== locked.revision.contentHash))
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
else {
|
|
@@ -66,7 +67,7 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
|
|
|
66
67
|
const addition = buildLocalProjectSkillsSnapshot({
|
|
67
68
|
declaration: { schemaVersion: 1, entries: [existing ?? { entryKey, source: { kind: 'repository', root: source }, enabled: true, surface: 'sandbox-agent' }] },
|
|
68
69
|
discovered: [], sources: [{ entryKey, root: source, files, sourceTreeHash: sourceTreeDigest(files) }], issues: [],
|
|
69
|
-
}, existing ? previous.lock : undefined);
|
|
70
|
+
}, existing ? previous.lock : undefined, { lockVersion: previous.lock.lockVersion });
|
|
70
71
|
const next = structuredClone(previous);
|
|
71
72
|
if (existing) {
|
|
72
73
|
next.declaration.entries[next.declaration.entries.findIndex(entry => entry.entryKey === entryKey)] = addition.declaration.entries[0];
|
|
@@ -74,7 +75,7 @@ export async function ensureWorkspaceRemixReferenceSkill(root, cache) {
|
|
|
74
75
|
}
|
|
75
76
|
else {
|
|
76
77
|
next.declaration.entries.push(...addition.declaration.entries);
|
|
77
|
-
next.lock.entries.
|
|
78
|
+
next.lock = { ...next.lock, entries: [...next.lock.entries, ...addition.lock.entries] };
|
|
78
79
|
}
|
|
79
80
|
next.lock.declarationHash = declarationDigest(next.declaration);
|
|
80
81
|
validateProjectSkillsSnapshot(next.declaration, next.lock);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { extractBundleArchive, sourceTreeDigest } from '@playcraft/common/project-skills';
|
|
2
3
|
import { workspaceRoot } from './registry.js';
|
|
3
4
|
import { PLATFORM_ROUTES, platformEndpoint } from '@playcraft/common/cloud-workspace/runtime';
|
|
4
5
|
const MAX_ARTIFACT_BYTES = 80 * 1024 * 1024;
|
|
@@ -40,12 +41,18 @@ async function requestWorkspaceSkillArtifact(state, workspaceId, entry) {
|
|
|
40
41
|
await reader.cancel().catch(() => undefined);
|
|
41
42
|
}
|
|
42
43
|
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
43
|
-
if (typeof result.content !== 'string' || result.artifactHash !==
|
|
44
|
+
if (typeof result.content !== 'string' || typeof result.artifactHash !== 'string' || !/^[a-f0-9]{64}$/.test(result.artifactHash)
|
|
45
|
+
|| (!entry.revision.contentHash && result.artifactHash !== entry.revision.artifactHash)
|
|
44
46
|
|| result.content.length > Math.ceil(MAX_ARTIFACT_BYTES * 4 / 3) || result.content.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(result.content))
|
|
45
47
|
throw new Error('workspace_skills_digest_mismatch');
|
|
46
48
|
const archive = Buffer.from(result.content, 'base64');
|
|
47
|
-
if (archive.byteLength > MAX_ARTIFACT_BYTES || archive.toString('base64') !== result.content || createHash('sha256').update(archive).digest('hex') !==
|
|
49
|
+
if (archive.byteLength > MAX_ARTIFACT_BYTES || archive.toString('base64') !== result.content || createHash('sha256').update(archive).digest('hex') !== result.artifactHash)
|
|
48
50
|
throw new Error('workspace_skills_digest_mismatch');
|
|
51
|
+
if (entry.revision.contentHash) {
|
|
52
|
+
const extracted = extractBundleArchive(archive);
|
|
53
|
+
if (extracted.issues.length || sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.revision.contentHash)
|
|
54
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
55
|
+
}
|
|
49
56
|
if (state.journal.destroyed() || epoch !== state.runtimeEpoch)
|
|
50
57
|
throw new Error('workspace_skills_unavailable');
|
|
51
58
|
return archive;
|
|
@@ -53,16 +60,23 @@ async function requestWorkspaceSkillArtifact(state, workspaceId, entry) {
|
|
|
53
60
|
/** The platform independently authorizes the trusted binding, including an unpublished checkout. */
|
|
54
61
|
export function workspacePlatformSkillAccess(state, workspaceId) {
|
|
55
62
|
return {
|
|
56
|
-
resolve: async (skillRefs) => {
|
|
63
|
+
resolve: async (skillRefs, lockVersion = 3, existingLockVersion) => {
|
|
57
64
|
const epoch = state.runtimeEpoch;
|
|
58
65
|
if (state.journal.destroyed())
|
|
59
66
|
throw new Error('workspace_skills_unavailable');
|
|
60
|
-
const
|
|
67
|
+
const request = (includeVersion) => fetch(`${platformEndpoint(state.config.publicBaseUrl, PLATFORM_ROUTES.skillArtifacts)}/resolve`, {
|
|
61
68
|
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30_000),
|
|
62
69
|
headers: { authorization: `Bearer ${state.config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': state.config.environment },
|
|
63
70
|
body: JSON.stringify({ runtimeId: state.config.runtimeId, generation: state.config.generation,
|
|
64
|
-
runtimeEpoch: epoch, workspaceId, skillRefs }),
|
|
71
|
+
runtimeEpoch: epoch, workspaceId, skillRefs, ...(includeVersion ? { lockVersion, ...(existingLockVersion ? { existingLockVersion } : {}) } : {}) }),
|
|
65
72
|
});
|
|
73
|
+
let response = await request(lockVersion === 4);
|
|
74
|
+
// Older strict-schema servers report an unknown field as 401. Retry only to
|
|
75
|
+
// prove legacy capability with the same credentials; a second 401 remains fatal.
|
|
76
|
+
if (lockVersion === 4 && (response.status === 400 || response.status === 401)) {
|
|
77
|
+
await response.body?.cancel().catch(() => undefined);
|
|
78
|
+
response = await request(false);
|
|
79
|
+
}
|
|
66
80
|
if (!response.ok) {
|
|
67
81
|
await response.body?.cancel().catch(() => undefined);
|
|
68
82
|
throw new Error(response.status === 401 || response.status === 403
|
|
@@ -82,7 +82,11 @@ async function lockedFiles(root, cache, entry, view, loadArtifact) {
|
|
|
82
82
|
const persistent = await safeDirectory(root, '.playcraft/skill-artifacts');
|
|
83
83
|
const saved = join(persistent, `${entry.revision.artifactHash}.tar.gz`);
|
|
84
84
|
const path = join(cache, `${entry.revision.artifactHash}.tar.gz`);
|
|
85
|
+
const contentHash = view.lock.lockVersion === 4 ? entry.revision.contentHash : undefined;
|
|
86
|
+
const contentSaved = contentHash ? join(persistent, `content-${contentHash}.tar.gz`) : undefined;
|
|
87
|
+
const contentPath = contentHash ? join(cache, `content-${contentHash}.tar.gz`) : undefined;
|
|
85
88
|
let archive;
|
|
89
|
+
let contentAddressed = false;
|
|
86
90
|
try {
|
|
87
91
|
const stat = await lstat(saved);
|
|
88
92
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 80 * 1024 * 1024)
|
|
@@ -93,33 +97,64 @@ async function lockedFiles(root, cache, entry, view, loadArtifact) {
|
|
|
93
97
|
if (error.code !== 'ENOENT')
|
|
94
98
|
throw error;
|
|
95
99
|
try {
|
|
96
|
-
|
|
100
|
+
if (!contentSaved)
|
|
101
|
+
throw error;
|
|
102
|
+
const stat = await lstat(contentSaved);
|
|
103
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 80 * 1024 * 1024)
|
|
104
|
+
throw new Error('workspace_skills_invalid_bundle');
|
|
105
|
+
archive = await readFile(contentSaved);
|
|
106
|
+
contentAddressed = true;
|
|
97
107
|
}
|
|
98
108
|
catch (cacheError) {
|
|
99
109
|
if (cacheError.code !== 'ENOENT')
|
|
100
110
|
throw cacheError;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const source = await readProjectSkillSource(root, declaration);
|
|
104
|
-
if (source.sourceTreeHash !== entry.sourceTreeHash)
|
|
105
|
-
throw new Error('workspace_skills_source_changed');
|
|
106
|
-
archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
|
|
111
|
+
try {
|
|
112
|
+
archive = await readFile(path);
|
|
107
113
|
}
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
throw
|
|
111
|
-
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code !== 'ENOENT')
|
|
116
|
+
throw error;
|
|
117
|
+
try {
|
|
118
|
+
if (!contentPath)
|
|
119
|
+
throw error;
|
|
120
|
+
archive = await readFile(contentPath);
|
|
121
|
+
contentAddressed = true;
|
|
122
|
+
}
|
|
123
|
+
catch (contentError) {
|
|
124
|
+
if (contentError.code !== 'ENOENT')
|
|
125
|
+
throw contentError;
|
|
126
|
+
const declaration = view.declaration.entries.find(item => item.entryKey === entry.entryKey);
|
|
127
|
+
if (entry.revision.kind === 'source' && declaration.source.kind === 'repository') {
|
|
128
|
+
const source = await readProjectSkillSource(root, declaration);
|
|
129
|
+
if (source.sourceTreeHash !== entry.sourceTreeHash)
|
|
130
|
+
throw new Error('workspace_skills_source_changed');
|
|
131
|
+
if (contentHash) {
|
|
132
|
+
if (source.sourceTreeHash !== contentHash)
|
|
133
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
134
|
+
return source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) }));
|
|
135
|
+
}
|
|
136
|
+
archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (!loadArtifact)
|
|
140
|
+
throw new Error('workspace_skills_artifact_unavailable');
|
|
141
|
+
archive = await loadArtifact(entry);
|
|
142
|
+
contentAddressed = Boolean(contentHash);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
112
145
|
}
|
|
113
146
|
}
|
|
114
147
|
}
|
|
115
|
-
if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
|
|
148
|
+
if (!contentAddressed && createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
|
|
116
149
|
throw new Error('workspace_skills_digest_mismatch');
|
|
117
150
|
const extracted = extractBundleArchive(archive);
|
|
118
151
|
if (extracted.issues.length)
|
|
119
152
|
throw new Error('workspace_skills_invalid_bundle');
|
|
153
|
+
if (contentHash && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== contentHash)
|
|
154
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
120
155
|
if (entry.revision.kind === 'source' && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.sourceTreeHash)
|
|
121
156
|
throw new Error('workspace_skills_digest_mismatch');
|
|
122
|
-
await writeSharedFile(root, saved, archive);
|
|
157
|
+
await writeSharedFile(root, contentAddressed ? contentSaved : saved, archive);
|
|
123
158
|
return extracted.files;
|
|
124
159
|
}
|
|
125
160
|
function metadata(entryKey, content) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"dev": "tsc -w",
|
|
26
|
-
"build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
|
|
26
|
+
"build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-level-editor-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
|
|
27
27
|
"start": "node dist/index.js",
|
|
28
28
|
"test": "pnpm build && vitest run",
|
|
29
29
|
"test:agent": "node scripts/run-agent-tests.mjs --list",
|
|
@@ -42,7 +42,10 @@
|
|
|
42
42
|
"generate:video-skills": "node scripts/generate-video-skills.mjs",
|
|
43
43
|
"verify:video-skills": "node scripts/generate-video-skills.mjs --check && node --test scripts/generate-video-skills.test.mjs",
|
|
44
44
|
"verify:parameter-skill": "node scripts/generate-parameter-skill.mjs --check && node --test scripts/generate-parameter-skill.test.mjs",
|
|
45
|
+
"verify:level-editor-skill": "node scripts/generate-level-editor-skill.mjs --check",
|
|
45
46
|
"verify:asset-skills": "node scripts/generate-asset-skills.mjs --check && node --test scripts/generate-asset-skills.test.mjs",
|
|
47
|
+
"generate:audio-program-companion": "node scripts/generate-audio-program-companion.mjs",
|
|
48
|
+
"verify:audio-program-companion": "node scripts/generate-audio-program-companion.mjs --check",
|
|
46
49
|
"verify:agent-stop": "node scripts/verify-agent-stop.mjs",
|
|
47
50
|
"verify-sprite-overlay": "node scripts/verify-sprite-overlay.mjs",
|
|
48
51
|
"link": "node scripts/bump-local.js && pnpm build && npm link",
|
|
@@ -53,8 +56,8 @@
|
|
|
53
56
|
"@gltf-transform/core": "^4.3.0",
|
|
54
57
|
"@gltf-transform/extensions": "^4.3.0",
|
|
55
58
|
"@gltf-transform/functions": "^4.3.0",
|
|
56
|
-
"@playcraft/build": "0.1.
|
|
57
|
-
"@playcraft/common": "0.1.
|
|
59
|
+
"@playcraft/build": "0.1.68",
|
|
60
|
+
"@playcraft/common": "0.1.68",
|
|
58
61
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
59
62
|
"chokidar": "^4.0.3",
|
|
60
63
|
"commander": "^13.1.0",
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
"dotenv": "^17.2.3",
|
|
64
67
|
"drizzle-orm": "^0.45.1",
|
|
65
68
|
"earcut": "3.2.4",
|
|
69
|
+
"es-module-lexer": "1.7.0",
|
|
66
70
|
"execa": "^9.6.1",
|
|
67
71
|
"express": "^5.2.1",
|
|
68
72
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -80,6 +84,7 @@
|
|
|
80
84
|
"three": "0.183.2",
|
|
81
85
|
"update-notifier": "^7.3.1",
|
|
82
86
|
"ws": "^8.19.0",
|
|
87
|
+
"yaml": "2.8.3",
|
|
83
88
|
"zod": "^3.25.76"
|
|
84
89
|
},
|
|
85
90
|
"devDependencies": {
|
|
@@ -3,7 +3,7 @@ name: playcraft-audio-generation
|
|
|
3
3
|
description: Use when generating or replacing playable audio, reusing library audio, managing durable audio tasks, or locally compressing, trimming, looping, normalizing, and mixing sound.
|
|
4
4
|
triggers: 生成音效,生成bgm,生成背景音乐,generate sfx,generate bgm,generate audio,音频,音效,背景音乐,sfx,bgm,loop音频,音频压缩,音频处理,audio,compress audio,fade,normalize,音频后处理,音频管线
|
|
5
5
|
compatibility: agent,opencode
|
|
6
|
-
version: 1.
|
|
6
|
+
version: 1.4.0
|
|
7
7
|
surfaces:
|
|
8
8
|
- sandbox-agent
|
|
9
9
|
---
|
|
@@ -12,7 +12,95 @@ surfaces:
|
|
|
12
12
|
|
|
13
13
|
## 0. 开始前必读
|
|
14
14
|
|
|
15
|
-
AI (Artificial Intelligence, 人工智能) 音频包含 SFX (Sound Effects, 音效) 与 BGM (Background Music, 背景音乐)
|
|
15
|
+
AI (Artificial Intelligence, 人工智能) 音频包含 SFX (Sound Effects, 音效) 与 BGM (Background Music, 背景音乐)。文件音频工作素材写入当前工程的 `assets/audio/` 或当前主题素材目录;最终交付导出到工程根目录 `dist/`。可编辑短音效源见下节。请求文件和工作记录保存在 `dist/` 外。
|
|
16
|
+
|
|
17
|
+
### 云工作区的可编辑短音效
|
|
18
|
+
|
|
19
|
+
仅在当前锁定技能版本与工作区能力支持时使用本段流程。先运行 `playcraft audio source capabilities`,检查 `enabled`、`writable`、对应 `operations`;生成与渲染还须 `renderer.ready`。能力缺失或关闭时保留源及已有试听,报告不可用,不替换工程技能锁,不移除声音上下文后继续执行。`synth-sfx` 保持短音效流程;仅在 `audioScoreVersion:1` 与 `audioScoreContextVersion:1` 均声明时使用下述乐谱/多轨流程。动态混合使用独立高级程序入口,不写入既有音效或乐谱源。
|
|
20
|
+
|
|
21
|
+
默认从描述、试听、采用开始,只展示适用的时长、音量、音高、明亮度简单控件;用户要求细节后再展开精确参数。工程源在 `source/audio/` 等 `dist/` 外目录,最终表示包位于 `dist/audio/`。未采用候选保持私有,不因续改而强制写入工程。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# short-sound-request.json: {"kind":"audio","representation":"synth-sfx","category":"sfx","description":"短促明亮的金币音","duration":0.2,"candidateCount":1}
|
|
25
|
+
playcraft audio source create --request short-sound-request.json --request-id <stable-request-id>
|
|
26
|
+
playcraft assets get <taskId>
|
|
27
|
+
playcraft assets wait <taskId>
|
|
28
|
+
playcraft audio source inspect --task <taskId> --candidate <candidateId>
|
|
29
|
+
|
|
30
|
+
# duration 是秒;volume 是主增益;pitch 是半音;brightness 是滤波截止频率倍数。
|
|
31
|
+
playcraft audio source edit --task <taskId> --candidate <candidateId> \
|
|
32
|
+
--revision <candidateRevision> --hash <sourceContentHash> --request-id <stable-edit-id> \
|
|
33
|
+
--macro duration --value 0.15
|
|
34
|
+
playcraft audio source render --task <taskId> --candidate <candidateId> \
|
|
35
|
+
--revision <newRevision> --hash <newSourceContentHash> --request-id <stable-render-id>
|
|
36
|
+
|
|
37
|
+
# 用户明确采用后才保存到工程;替换已有目标还须其真实修订和摘要。
|
|
38
|
+
playcraft audio source adopt --task <taskId> --candidate <candidateId> \
|
|
39
|
+
--revision <candidateRevision> --hash <sourceContentHash> --request-id <stable-adopt-id> \
|
|
40
|
+
--output source/audio/coin.pcaudio.json --destination-revision 0
|
|
41
|
+
playcraft audio source inspect --path source/audio/coin.pcaudio.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
这些命令使用同一工作区素材任务。候选身份是任务、候选、修订与源内容摘要;工程源身份是工作区、目录、路径、修订与摘要。保存完整源用 `edit/save --document <file>`,只改简单目标用 `--macro`。每次保存后重新渲染才获得对应试听。所有写命令都要求稳定 `--request-id`;响应不确定时使用原身份恢复,不重复生成。`assets download` 不会把参数候选伪装成音频文件,请走专用采用。
|
|
45
|
+
|
|
46
|
+
接收 `composer.context.audio` 时先核对目录与精确版本,并保留 `selection`、`lockedVoiceIds`、`preserveTargets`。将保护项传给 `edit/save --locked-voices <逗号分隔声部> --preserve-targets <逗号分隔参数目标>`。锁定和曲线不可静默重置。`intent: propose` 只授权提出可比较方向;明确修改并保存才写入原目标;用于游戏需要接入授权。修订冲突表示用户已有新修改,保留结果供比较,不能刷新基准后覆盖。
|
|
47
|
+
|
|
48
|
+
保存、试听就绪、实际引用、构建通过分别记录。`pending`、`failed`、`cancelled` 和 `render_peak_exceeded` 不能宣称完成;峰值超限时保留原源、说明需调整增益,不静默归一化。候选修改不会自动采用,收起精细视图也不能丢失源字段。
|
|
49
|
+
|
|
50
|
+
用于游戏时读取采用回执的 `representation.modulePath`,其中 `sound.mjs` 与运行时、源和试听均是不可变包内相对引用。定位用户指定事件的实际调用点,或沿已有绑定替换;保存旧调用和绑定后,导入 `createSound`,在该交互中调用 `play()`,场景销毁调用 `dispose()`。共享播放器最多 8 个实例、32 个声部;播放需浏览器交互解锁,按工程既有方式处理失败。随后执行工程原有构建验证;没有明确调用点时保持待接入,不猜测事件映射。
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { createSound } from '<representation.modulePath 相对当前模块的路径>';
|
|
54
|
+
const coinSound = createSound();
|
|
55
|
+
// 放入用户指定的金币事件处理器;按项目方式处理播放失败。
|
|
56
|
+
await coinSound.play();
|
|
57
|
+
// 场景销毁:coinSound.dispose();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`playcraft audio source bind --path <source> --revision <revision> --hash <hash> --event <event> --request-id <id>` 只创建固定版本的交接记录,返回 `integration_required`,不能证明代码已引用或构建成功。`rollback --path <source> --revision <current> --hash <hash> --version <historical> --request-id <id>` 追加历史源恢复修订,之后重新渲染。游戏回退还必须恢复旧模块调用及绑定,再次构建;不能只恢复源就宣称游戏回退。
|
|
61
|
+
|
|
62
|
+
### 可编辑音乐与多轨协作
|
|
63
|
+
|
|
64
|
+
音乐使用 `format: "playcraft.score"`、`content.kind: "score"` 的 `.pcscore.json`,引擎 `playcraft-webaudio-score@1.0.0`。先核对音乐能力、写权限与渲染就绪。默认让用户描述用途、试听候选,再调速度、整体强弱、乐器和循环;选轨、拍段或音符后才展开细节。符号音高采用 MIDI (Musical Instrument Digital Interface, 乐器数字接口) 音符号;时间是四分音符拍点,速度为 BPM (Beats Per Minute, 每分钟节拍数),不能混用秒和拍。
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# music-request.json: {"kind":"audio","representation":"score","category":"bgm","description":"轻快的两轨游戏循环音乐","duration":8,"bpm":120,"loop":true,"candidateCount":1}
|
|
68
|
+
playcraft audio source create --request music-request.json --request-id <stable-create-id>
|
|
69
|
+
playcraft assets wait <taskId>
|
|
70
|
+
playcraft audio source inspect --task <taskId> --candidate <candidateId>
|
|
71
|
+
playcraft audio source edit --task <taskId> --candidate <candidateId> \
|
|
72
|
+
--revision <revision> --hash <sourceContentHash> --request-id <stable-edit-id> --macro tempo --value 132
|
|
73
|
+
playcraft audio source render --task <taskId> --candidate <candidateId> \
|
|
74
|
+
--revision <newRevision> --hash <newHash> --request-id <stable-render-id>
|
|
75
|
+
playcraft audio source adopt --task <taskId> --candidate <candidateId> \
|
|
76
|
+
--revision <newRevision> --hash <newHash> --request-id <stable-adopt-id> --output source/audio/loop.pcscore.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
精细操作使用 `edit --adjustment <file>`:例如 `{"kind":"instrument","instrument":"bell","trackIds":["melody"]}`;或者在读取精确源后 `edit --document <file>` 保存完整源。内置音色限 `piano / bell / bass / pad / sine / drums`,不是外部采样乐器库。最多 8 轨、512 音符、32 并发音符、60 秒和 64 KiB 源;超限、无法表达的特性必须报告,不截断或改成另一种音频。
|
|
80
|
+
|
|
81
|
+
`composer.context.audio.version:2` 明确 `timeUnit:"beats"`,冻结 `selection.trackIds / noteIds / rangeBeats`、`protection.lockedTrackIds / preserveMelody`。把该选区原样写入工程外临时文档,保存时传 `--selection <file> --locked-tracks <ids> --preserve-melody` 中实际适用的项。只调整鼓轨第 5—8 小节时,按拍号计算精确拍段,保护其余轨和拍段;改变整轨音色、增益、自动化或全局速度会影响范围之外,不能把它当作局部修改。服务器会与可信上下文再次核对,命令自行删掉保护条件不会解除限制。
|
|
82
|
+
|
|
83
|
+
修订不匹配时保留双方,不自行刷新后覆盖。网页可由用户显式合并没有依赖冲突的轨道或音符;速度、循环、全局混音、同轨音色/自动化及同音符冲突需要比较后重做。候选仍须独立采用,回退仍追加新源修订,游戏引用仍单独恢复。
|
|
84
|
+
|
|
85
|
+
音乐完整包成员为 `sound.pcscore.json / audition.wav / runtime.mjs / sound.mjs / manifest.json`,只使用回执中的固定模块。`createSound().prepare()` 不默认展开整段音乐采样;`play()` 按乐谱和循环段调度,返回的 `stop()` 或播放器 `stop()/dispose()` 会结束循环并释放资源。场景退出、页面隐藏或切换试听时应调用停止,禁止遗留循环。单次播放可传 `play({loop:false})`;指定段落或轨道试听通过受支持的 `selection` 参数。试听渲染文件本身是完整单次表示,循环起止需随乐谱一起使用。
|
|
86
|
+
|
|
87
|
+
### 高级:游戏状态与短采样混合
|
|
88
|
+
|
|
89
|
+
用户明确要求按游戏状态改变声音,或组合短采样与合成音时,先检查 `playcraft audio program --help` 是否存在。旧工作区不支持该命令时保留现有源和技能锁并报告。不要为普通生成和简单调整主动创建音频程序。
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# --asset 必须是采用回执中的完整不可变声音包成员;可选 --sample 是 dist/audio 中有摘要目录的短采样。
|
|
93
|
+
playcraft audio program init --asset <sound.pcscore.json或sound.pcaudio.json固定路径> --output source/audio/states.pcaudioprogram.json
|
|
94
|
+
playcraft audio program validate --path source/audio/states.pcaudioprogram.json
|
|
95
|
+
playcraft audio program compile --path source/audio/states.pcaudioprogram.json
|
|
96
|
+
playcraft audio program inspect --manifest <编译回执manifestPath>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
程序是独立的 `playcraft.audio-program@1`,最多 8 项资产、16 个离散状态、每状态 4 层及 60 秒。状态明确指定增益、半音、低通与乐谱拍段;切换采用停止并重启语义,不承诺无缝交叉淡化。短采样只接受单声道 44100 Hz、PCM(Pulse Code Modulation,脉冲编码调制)16 位标准波形文件,每个最多 2 秒、最多 4 个;不接受外部地址或任意脚本。依赖必须带精确路径、内容摘要和大小。编译会校验完整声音包及平台引擎,渲染所有状态并拒绝超峰值;不静默截断或归一化。全部状态波形总量限 16 MiB,输入依赖限 2 MiB。
|
|
100
|
+
|
|
101
|
+
从编译回执的 `runtimeModulePath` 导入 `createSound`,在明确交互中 `await player.play('idle')`;状态变化时 `await player.setState('action')`。切换场景或页面隐藏时停止,销毁时 `dispose()`。`prerenderModulePath` 使用同一接口,已把音高、滤波和混合烘焙到各状态波形,播放不重复处理;两种方案仍要求浏览器支持音频上下文。模块与共享引擎均不可变,修改程序后重新编译,保留旧模块用于回退。只修改 `dist/audio` 包内文件会触发校验失败。
|
|
102
|
+
|
|
103
|
+
渠道选型必须按完整实际构建产物字节数比较,包括运行时、嵌入依赖和波形;源 JSON(JavaScript Object Notation,JavaScript 对象表示法)大小不能代表最终压缩收益。未知渠道能力采用保守回退,渠道不支持音频上下文时不能宣称预渲染模块可用。完整渠道包通过平台交付页生成与下载,不保存到工作区 `dist/`。
|
|
16
104
|
|
|
17
105
|
### 复用素材与持久化音频任务
|
|
18
106
|
|
|
@@ -199,7 +287,7 @@ playcraft audio info --input assets/audio/bgm/main_theme_loop.mp3
|
|
|
199
287
|
|
|
200
288
|
## 3. 本地音频后处理
|
|
201
289
|
|
|
202
|
-
|
|
290
|
+
本节文件后处理子命令**完全本地运行**,无需网络;前述 `playcraft audio source` 使用云工作区服务。
|
|
203
291
|
|
|
204
292
|
### 3.1 常用处理命令
|
|
205
293
|
|
|
@@ -253,7 +341,7 @@ playcraft audio mix \
|
|
|
253
341
|
|
|
254
342
|
### 3.2 标准音频处理管线
|
|
255
343
|
|
|
256
|
-
|
|
344
|
+
文件音频生成后的处理步骤:
|
|
257
345
|
|
|
258
346
|
```
|
|
259
347
|
SFX 管线:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playcraft-level-editor
|
|
3
|
+
description: Create or evolve a project's declarative level editor by mapping its actual level files to platform editing components and connecting the same saved data to production and development playtests.
|
|
4
|
+
compatibility: agent,opencode
|
|
5
|
+
version: 1.1.0
|
|
6
|
+
surfaces:
|
|
7
|
+
- sandbox-agent
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Project level editor
|
|
11
|
+
|
|
12
|
+
Generate a project-owned editing declaration, data schema and real game integration. The platform composes its editor from the declaration; do not build a game-specific platform page or infer a writable scene from arbitrary engine objects.
|
|
13
|
+
|
|
14
|
+
The runtime supplies an exact task identity, frozen component catalog and admission-report path. Read [the protocol](references/protocol-v1.md) and the installed component catalog before changing files. Use [preview integration](references/preview.md) when connecting the game.
|
|
15
|
+
|
|
16
|
+
Inspect the current production entry, level loading, authoring data, resource registration and lifecycle. Prefer existing native files and identifiers. Choose controls that address the user's actual request; preserve unrelated data, comments, layouts and existing editor customization. Repeated initialization evolves the current declaration, rather than resetting the game to an example.
|
|
17
|
+
|
|
18
|
+
Declare explicit collections, stable identities, selection, bindings, commands and view composition. Field names and panel layouts belong to the project. Expose only supported component and command versions; do not generate executable expressions inside declarations or game-specific platform frontend code. Use the installed extension contracts for project-owned dedicated views, validators and data providers. Ordinary edits use the declared commands without another Agent call.
|
|
19
|
+
|
|
20
|
+
Give every declared data-schema node an explicit single `type`; annotations such as `readOnly` cannot replace it. Preserve unknown author fields with their actual types and readonly boundaries rather than changing their values to fit a schema. Follow the exact node scope and subset rules in [the protocol](references/protocol-v1.md).
|
|
21
|
+
|
|
22
|
+
For paths, three-dimensional data, custom shapes, command compositions, dedicated views or nonstandard files, read [project extensions](references/extensions.md). Use only features reported by the current runtime; preserve existing frozen Skill locks.
|
|
23
|
+
|
|
24
|
+
Production and development playtests must consume the same neutral data resolver. Connect actual consumers: a declaration, getter or hand-written success report is insufficient. Editing changes authored initial data; playtest scores, deaths, motion and generated objects must not be written back to it. Restart the current level when the change cannot honestly apply in place.
|
|
25
|
+
|
|
26
|
+
Before changing a data contract, preserve the original files and account for existing user values and object identities. A layout-only change must not migrate level data. If a required migration cannot preserve meaning, leave the current data intact and report the missing work.
|
|
27
|
+
|
|
28
|
+
Add executable `test:level-editor` checks for defaults and at least two meaningful edited outcomes in real game consumers. Preserve an existing `typecheck` and successful production `build`. Production output belongs in `dist/` and excludes development editor code. Keep reviewable partial changes and report concrete unsupported work on failure.
|
|
29
|
+
|
|
30
|
+
These permanent checks must remain valid after ordinary legal author edits. Exercise controlled copies of current author data and actual consumers; do not pin the original level file hash, budget, coordinates, or object count as an immutable test expectation. Original-byte preservation belongs to the platform admission baseline and report, not a permanent restriction on the user's future levels.
|
|
31
|
+
|
|
32
|
+
Use the admission report's `missing` only for actual incomplete or failed engineering capabilities, such as schema, real consumers, adapter wiring, required scripts or production isolation. Post-authoring platform verification, managed preview context, browser handshake and Portal operations belong to their later authoritative phases; not having run them does not itself describe a code gap. `missing: []` claims no identified engineering gap, not checked, verified or connected status. Preserve unverified facts in the final reply or project README; follow [the protocol's phase boundaries](references/protocol-v1.md) without inventing pending report fields or platform results.
|
|
33
|
+
|
|
34
|
+
After editing, calculate the actual source-file hashes and write the exact admission report supplied by the runtime. End with the implemented scope and remaining work. The platform independently verifies sources and checks, then prepares the first development instance; only its real handshake establishes connected capability.
|