real-link-ai 1.6.0

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 (106) hide show
  1. package/README.md +79 -0
  2. package/dist/commands/agent.d.ts +3 -0
  3. package/dist/commands/agent.d.ts.map +1 -0
  4. package/dist/commands/agent.js +120 -0
  5. package/dist/commands/agent.js.map +1 -0
  6. package/dist/commands/asset.d.ts +3 -0
  7. package/dist/commands/asset.d.ts.map +1 -0
  8. package/dist/commands/asset.js +53 -0
  9. package/dist/commands/asset.js.map +1 -0
  10. package/dist/commands/bug.d.ts +3 -0
  11. package/dist/commands/bug.d.ts.map +1 -0
  12. package/dist/commands/bug.js +179 -0
  13. package/dist/commands/bug.js.map +1 -0
  14. package/dist/commands/call.d.ts +3 -0
  15. package/dist/commands/call.d.ts.map +1 -0
  16. package/dist/commands/call.js +116 -0
  17. package/dist/commands/call.js.map +1 -0
  18. package/dist/commands/game.d.ts +3 -0
  19. package/dist/commands/game.d.ts.map +1 -0
  20. package/dist/commands/game.js +291 -0
  21. package/dist/commands/game.js.map +1 -0
  22. package/dist/commands/learning.d.ts +3 -0
  23. package/dist/commands/learning.d.ts.map +1 -0
  24. package/dist/commands/learning.js +83 -0
  25. package/dist/commands/learning.js.map +1 -0
  26. package/dist/commands/login.d.ts +5 -0
  27. package/dist/commands/login.d.ts.map +1 -0
  28. package/dist/commands/login.js +102 -0
  29. package/dist/commands/login.js.map +1 -0
  30. package/dist/commands/loop.d.ts +3 -0
  31. package/dist/commands/loop.d.ts.map +1 -0
  32. package/dist/commands/loop.js +382 -0
  33. package/dist/commands/loop.js.map +1 -0
  34. package/dist/commands/memory.d.ts +3 -0
  35. package/dist/commands/memory.d.ts.map +1 -0
  36. package/dist/commands/memory.js +80 -0
  37. package/dist/commands/memory.js.map +1 -0
  38. package/dist/commands/message.d.ts +3 -0
  39. package/dist/commands/message.d.ts.map +1 -0
  40. package/dist/commands/message.js +80 -0
  41. package/dist/commands/message.js.map +1 -0
  42. package/dist/commands/onboarding.d.ts +3 -0
  43. package/dist/commands/onboarding.d.ts.map +1 -0
  44. package/dist/commands/onboarding.js +100 -0
  45. package/dist/commands/onboarding.js.map +1 -0
  46. package/dist/commands/ops.d.ts +3 -0
  47. package/dist/commands/ops.d.ts.map +1 -0
  48. package/dist/commands/ops.js +80 -0
  49. package/dist/commands/ops.js.map +1 -0
  50. package/dist/commands/orchestrate.d.ts +3 -0
  51. package/dist/commands/orchestrate.d.ts.map +1 -0
  52. package/dist/commands/orchestrate.js +91 -0
  53. package/dist/commands/orchestrate.js.map +1 -0
  54. package/dist/commands/plan.d.ts +3 -0
  55. package/dist/commands/plan.d.ts.map +1 -0
  56. package/dist/commands/plan.js +157 -0
  57. package/dist/commands/plan.js.map +1 -0
  58. package/dist/commands/signal.d.ts +3 -0
  59. package/dist/commands/signal.d.ts.map +1 -0
  60. package/dist/commands/signal.js +50 -0
  61. package/dist/commands/signal.js.map +1 -0
  62. package/dist/commands/system.d.ts +3 -0
  63. package/dist/commands/system.d.ts.map +1 -0
  64. package/dist/commands/system.js +110 -0
  65. package/dist/commands/system.js.map +1 -0
  66. package/dist/commands/task.d.ts +3 -0
  67. package/dist/commands/task.d.ts.map +1 -0
  68. package/dist/commands/task.js +222 -0
  69. package/dist/commands/task.js.map +1 -0
  70. package/dist/commands/wish.d.ts +3 -0
  71. package/dist/commands/wish.d.ts.map +1 -0
  72. package/dist/commands/wish.js +133 -0
  73. package/dist/commands/wish.js.map +1 -0
  74. package/dist/commands/work.d.ts +3 -0
  75. package/dist/commands/work.d.ts.map +1 -0
  76. package/dist/commands/work.js +533 -0
  77. package/dist/commands/work.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +55 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/lib/api.d.ts +11 -0
  83. package/dist/lib/api.d.ts.map +1 -0
  84. package/dist/lib/api.js +74 -0
  85. package/dist/lib/api.js.map +1 -0
  86. package/dist/lib/config.d.ts +23 -0
  87. package/dist/lib/config.d.ts.map +1 -0
  88. package/dist/lib/config.js +50 -0
  89. package/dist/lib/config.js.map +1 -0
  90. package/dist/lib/doc-sync.d.ts +31 -0
  91. package/dist/lib/doc-sync.d.ts.map +1 -0
  92. package/dist/lib/doc-sync.js +145 -0
  93. package/dist/lib/doc-sync.js.map +1 -0
  94. package/dist/lib/production-pack.d.ts +73 -0
  95. package/dist/lib/production-pack.d.ts.map +1 -0
  96. package/dist/lib/production-pack.js +282 -0
  97. package/dist/lib/production-pack.js.map +1 -0
  98. package/dist/lib/storyworld-sync.d.ts +30 -0
  99. package/dist/lib/storyworld-sync.d.ts.map +1 -0
  100. package/dist/lib/storyworld-sync.js +437 -0
  101. package/dist/lib/storyworld-sync.js.map +1 -0
  102. package/dist/lib/thriller-vn.d.ts +38 -0
  103. package/dist/lib/thriller-vn.d.ts.map +1 -0
  104. package/dist/lib/thriller-vn.js +1106 -0
  105. package/dist/lib/thriller-vn.js.map +1 -0
  106. package/package.json +48 -0
@@ -0,0 +1,1106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bootstrapThrillerVN = bootstrapThrillerVN;
4
+ const node_fs_1 = require("node:fs");
5
+ const config_js_1 = require("./config.js");
6
+ const DEFAULT_TEMPLATE_PATH = "G:\\Real Link AI\\story\\thriller\\interactive-fiction\\templates\\vn-project\\build\\story.json";
7
+ const DEFAULT_API_BASE = "https://api.real-link.ai";
8
+ const DEFAULT_STYLE_ID = "style.thriller-manor";
9
+ const DEFAULT_STYLE_NAME = "Thriller Manor";
10
+ function normalizeApiBase(api) {
11
+ return (api || process.env.REAL_LINK_API_URL || DEFAULT_API_BASE).replace(/\/+$/, "");
12
+ }
13
+ async function requestJson(options) {
14
+ const headers = {};
15
+ if (options.apiKey)
16
+ headers["x-api-key"] = options.apiKey;
17
+ if (options.workspaceId)
18
+ headers["x-workspace-id"] = options.workspaceId;
19
+ if (options.bearerToken)
20
+ headers.authorization = `Bearer ${options.bearerToken}`;
21
+ let body;
22
+ if (options.body !== undefined) {
23
+ headers["content-type"] = "application/json";
24
+ body = JSON.stringify(options.body);
25
+ }
26
+ const response = await fetch(`${options.apiBase}/api/v1${options.path}`, {
27
+ method: options.method || (options.body !== undefined ? "POST" : "GET"),
28
+ headers,
29
+ body,
30
+ });
31
+ const json = await response.json();
32
+ if (!response.ok || !json.ok) {
33
+ throw new Error(json.error || `HTTP ${response.status}`);
34
+ }
35
+ return json.data;
36
+ }
37
+ function readStarter(templatePath) {
38
+ const resolved = templatePath || process.env.REAL_LINK_STORY_TEMPLATE || DEFAULT_TEMPLATE_PATH;
39
+ if (!(0, node_fs_1.existsSync)(resolved)) {
40
+ throw new Error(`Template not found: ${resolved}`);
41
+ }
42
+ return JSON.parse((0, node_fs_1.readFileSync)(resolved, "utf8"));
43
+ }
44
+ function inferRole(character) {
45
+ return character.id.includes("protagonist") ? "player" : "npc";
46
+ }
47
+ function inferVariableCategory(variable) {
48
+ if (variable.type === "number")
49
+ return "counter";
50
+ if (variable.id.startsWith("trust."))
51
+ return "stat";
52
+ if (variable.id.startsWith("flag."))
53
+ return "flag";
54
+ return "quest";
55
+ }
56
+ function stringifyDefaultValue(value) {
57
+ if (value === undefined)
58
+ return "";
59
+ if (typeof value === "boolean")
60
+ return value ? "true" : "false";
61
+ return String(value);
62
+ }
63
+ function pickSpeakerId(node) {
64
+ const speakers = new Set((node.body || [])
65
+ .filter((entry) => entry.kind === "dialogue" && entry.speaker)
66
+ .map((entry) => entry.speaker));
67
+ return speakers.size === 1 ? Array.from(speakers)[0] : null;
68
+ }
69
+ function flattenNodeText(node, characterNames) {
70
+ if (node.text)
71
+ return node.text;
72
+ return (node.body || [])
73
+ .map((entry) => {
74
+ if (!entry.text)
75
+ return null;
76
+ if (entry.kind === "dialogue" && entry.speaker) {
77
+ const speaker = characterNames.get(entry.speaker) || entry.speaker;
78
+ return `${speaker}: ${entry.text}`;
79
+ }
80
+ return entry.text;
81
+ })
82
+ .filter((line) => !!line)
83
+ .join("\n\n");
84
+ }
85
+ function mapNodeType(kind) {
86
+ if (kind === "ending")
87
+ return "end";
88
+ return kind;
89
+ }
90
+ function buildCharacterBio(character) {
91
+ const profile = character.thrillerProfile || {};
92
+ const personalityBits = [
93
+ typeof profile.pressureStyle === "string" ? `Pressure style: ${profile.pressureStyle}.` : null,
94
+ typeof profile.suspectWeight === "string" ? `Suspicion weight: ${profile.suspectWeight}.` : null,
95
+ ].filter((value) => !!value);
96
+ const backgroundBits = [
97
+ character.description ? `Surface read: ${character.description}` : null,
98
+ typeof profile.innocentRead === "string" ? `Innocent read: ${profile.innocentRead}` : null,
99
+ typeof profile.dangerousRead === "string" ? `Dangerous read: ${profile.dangerousRead}` : null,
100
+ ].filter((value) => !!value);
101
+ return {
102
+ appearance: character.description || "",
103
+ personality: personalityBits.join(" "),
104
+ background: backgroundBits.join(" "),
105
+ voice: character.defaultExpression || "neutral",
106
+ };
107
+ }
108
+ function buildCharacterStats(character) {
109
+ const profile = character.thrillerProfile || {};
110
+ return {
111
+ suspectWeight: typeof profile.suspectWeight === "string" ? profile.suspectWeight : "supporting",
112
+ pressureStyle: typeof profile.pressureStyle === "string" ? profile.pressureStyle : character.defaultExpression || "neutral",
113
+ };
114
+ }
115
+ function buildLocationMetadata(location, starter) {
116
+ const nodeIds = starter.nodes
117
+ .filter((node) => node.location === location.id)
118
+ .map((node) => node.id);
119
+ return {
120
+ source: "thriller-writing",
121
+ backgroundHint: location.background || null,
122
+ narrativeNodeIds: nodeIds,
123
+ nodeCount: nodeIds.length,
124
+ };
125
+ }
126
+ function countNodesForCharacter(starter, characterId) {
127
+ return starter.nodes.filter((node) => Array.isArray(node.cast) && node.cast.some((entry) => entry?.character === characterId)).length;
128
+ }
129
+ function buildClueMetadata(clue) {
130
+ return {
131
+ source: "thriller-writing",
132
+ entityKind: "clue",
133
+ clueType: clue.kind || "evidence",
134
+ question: clue.question || "",
135
+ supports: clue.supports || [],
136
+ critical: !!clue.critical,
137
+ };
138
+ }
139
+ function buildThrillerSourceSpec(starter) {
140
+ return {
141
+ id: starter.meta.id,
142
+ title: starter.meta.title,
143
+ skillId: "thriller-writing",
144
+ summary: "Authored thriller story package adapted into real-link.ai.",
145
+ counts: {
146
+ characters: starter.entities.characters?.length || 0,
147
+ locations: starter.entities.locations?.length || 0,
148
+ clues: starter.entities.clues?.length || 0,
149
+ variables: starter.entities.variables?.length || 0,
150
+ nodes: starter.nodes.length,
151
+ },
152
+ };
153
+ }
154
+ function buildThrillerProjectionSpec() {
155
+ return {
156
+ id: "visual-novel",
157
+ label: "visual novel",
158
+ capability: "storyworld-vn",
159
+ publishLabel: "lightweight VN shell",
160
+ };
161
+ }
162
+ function buildThrillerAssetTargets(starter) {
163
+ const targets = [];
164
+ for (const character of starter.entities.characters || []) {
165
+ targets.push({
166
+ key: `portrait:${character.id}`,
167
+ entityId: character.id,
168
+ entityName: character.name,
169
+ entityType: "character",
170
+ role: "portrait",
171
+ });
172
+ }
173
+ for (const location of starter.entities.locations || []) {
174
+ targets.push({
175
+ key: `background:${location.id}`,
176
+ entityId: location.id,
177
+ entityName: location.name,
178
+ entityType: "location",
179
+ role: "background",
180
+ });
181
+ }
182
+ return targets;
183
+ }
184
+ function buildNarrativeGoal(gameName, planSpec) {
185
+ return [
186
+ `Adapt the ${planSpec.source.skillId} source package "${planSpec.source.title}" into the real-link.ai story-world workflow for ${gameName}.`,
187
+ `Create a reusable workspace IP package, preserve authored IDs, project it into a ${planSpec.projection.label} realization, generate core reusable assets, and only publish after the plan DAG is materialized.`,
188
+ ].join(" ");
189
+ }
190
+ function buildPlanStrategy(planSpec) {
191
+ const { source, projection } = planSpec;
192
+ return [
193
+ `Use ${source.skillId} as the authored source only.`,
194
+ "First create a studio plan and task DAG.",
195
+ `Then project the source into reusable workspace IP and a ${projection.label} runtime realization.`,
196
+ `Source scale: ${source.counts.characters} characters, ${source.counts.locations} locations, ${source.counts.clues} clues, ${source.counts.variables} variables, ${source.counts.nodes} nodes.`,
197
+ "Generate reusable assets after the story-world entities and style bible exist.",
198
+ ].join(" ");
199
+ }
200
+ function buildPlanTasks(planSpec) {
201
+ const { source, projection } = planSpec;
202
+ const tasks = [
203
+ {
204
+ key: "source-audit",
205
+ title: "Audit source package",
206
+ description: `Read the authored ${source.skillId} package, extract the reusable story-world contract, and record the adaptation constraints for real-link.ai.`,
207
+ type: "design",
208
+ priority: 10,
209
+ dependsOn: [],
210
+ requiredCapabilities: [source.skillId, "story-world"],
211
+ },
212
+ {
213
+ key: "style-bible",
214
+ title: "Seed projection style bible",
215
+ description: `Create the reusable visual style package and projection rules for the ${projection.label} realization.`,
216
+ type: "design",
217
+ priority: 8,
218
+ dependsOn: [0],
219
+ requiredCapabilities: ["story-world", projection.capability],
220
+ },
221
+ {
222
+ key: "characters",
223
+ title: "Seed cast characters",
224
+ description: `Import ${source.counts.characters} authored characters with reusable identity metadata.`,
225
+ type: "design",
226
+ priority: 9,
227
+ dependsOn: [0],
228
+ requiredCapabilities: [source.skillId, "story-world"],
229
+ },
230
+ {
231
+ key: "locations",
232
+ title: "Seed locations",
233
+ description: `Import ${source.counts.locations} authored locations with atmosphere and narrative coverage metadata.`,
234
+ type: "design",
235
+ priority: 8,
236
+ dependsOn: [0],
237
+ requiredCapabilities: ["story-world"],
238
+ },
239
+ {
240
+ key: "variables",
241
+ title: "Seed story variables",
242
+ description: `Import ${source.counts.variables} authored state variables with design-role metadata and payoff coverage.`,
243
+ type: "design",
244
+ priority: 8,
245
+ dependsOn: [0],
246
+ requiredCapabilities: ["story-world"],
247
+ },
248
+ {
249
+ key: "dialogue",
250
+ title: "Seed branching dialogue graph",
251
+ description: `Import ${source.counts.nodes} authored narrative nodes and preserve branch logic, choice metadata, and evidence references.`,
252
+ type: "design",
253
+ priority: 9,
254
+ dependsOn: [2, 3, 4],
255
+ requiredCapabilities: [source.skillId, "story-world", projection.capability],
256
+ },
257
+ {
258
+ key: "coverage-review",
259
+ title: "Review narrative coverage",
260
+ description: "Check that the story-world projection preserves speakers, locations, clue support links, and branch topology before runtime publish.",
261
+ type: "qa",
262
+ priority: 7,
263
+ dependsOn: [5],
264
+ requiredCapabilities: ["story-world", projection.capability],
265
+ },
266
+ ];
267
+ if (source.counts.clues > 0) {
268
+ tasks.splice(4, 0, {
269
+ key: "clues",
270
+ title: "Seed narrative clues",
271
+ description: `Import ${source.counts.clues} authored clues into the evidence registry, not gameplay inventory items.`,
272
+ type: "design",
273
+ priority: 8,
274
+ dependsOn: [0],
275
+ requiredCapabilities: [source.skillId, "story-world"],
276
+ });
277
+ const dialogueTask = tasks.find((task) => task.key === "dialogue");
278
+ if (dialogueTask) {
279
+ dialogueTask.dependsOn = [2, 3, 4, 5];
280
+ }
281
+ const coverageTask = tasks.find((task) => task.key === "coverage-review");
282
+ if (coverageTask) {
283
+ coverageTask.dependsOn = [6];
284
+ }
285
+ }
286
+ if (planSpec.generateAssets) {
287
+ const styleDependencyIndex = 1;
288
+ const characterDependencyIndex = 2;
289
+ const locationDependencyIndex = 3;
290
+ const assetTaskIndices = [];
291
+ for (const target of planSpec.assetTargets) {
292
+ const nextIndex = tasks.length;
293
+ tasks.push({
294
+ key: target.key,
295
+ title: `Generate ${target.role} for ${target.entityName}`,
296
+ description: `Create a reusable ${target.role} for ${target.entityName} after the entity spec and style bible are in place.`,
297
+ type: "art",
298
+ priority: 6,
299
+ dependsOn: [styleDependencyIndex, target.entityType === "character" ? characterDependencyIndex : locationDependencyIndex],
300
+ requiredCapabilities: ["story-world", projection.capability],
301
+ });
302
+ assetTaskIndices.push(nextIndex);
303
+ }
304
+ tasks.push({
305
+ key: "asset-review",
306
+ title: "Review asset coverage",
307
+ description: "Confirm that every planned asset task produced reusable studio assets before publish.",
308
+ type: "qa",
309
+ priority: 6,
310
+ dependsOn: assetTaskIndices,
311
+ requiredCapabilities: ["story-world", projection.capability],
312
+ });
313
+ }
314
+ if (planSpec.publish) {
315
+ const finalDependencies = tasks.map((_, index) => index);
316
+ tasks.push({
317
+ key: "publish",
318
+ title: `Publish ${projection.publishLabel}`,
319
+ description: `Advance the game to release, validate the publish contract, upload the ${projection.label} shell bundle, and finalize the live play URL.`,
320
+ type: "deploy",
321
+ priority: 7,
322
+ dependsOn: finalDependencies,
323
+ requiredCapabilities: ["story-world", projection.capability],
324
+ });
325
+ }
326
+ return tasks;
327
+ }
328
+ async function registerBootstrapAgent(apiBase, apiKey, workspaceId, gameName, capabilities) {
329
+ const id = `agent_story_${crypto.randomUUID().replace(/-/g, "").slice(0, 10)}`;
330
+ const name = `${gameName} Bootstrap`;
331
+ await requestJson({
332
+ apiBase,
333
+ apiKey,
334
+ workspaceId,
335
+ path: "/agents/register",
336
+ body: {
337
+ id,
338
+ name,
339
+ engine: "codex",
340
+ role: "orchestrator",
341
+ capabilities,
342
+ },
343
+ });
344
+ return { id, name };
345
+ }
346
+ async function createBootstrapPlan(apiBase, apiKey, workspaceId, gameId, gameName, planSpec, agentId) {
347
+ const plan = await requestJson({
348
+ apiBase,
349
+ apiKey,
350
+ workspaceId,
351
+ path: "/plans",
352
+ body: {
353
+ gameId,
354
+ title: `${gameName} story-world adaptation`,
355
+ goal: buildNarrativeGoal(gameName, planSpec),
356
+ strategy: buildPlanStrategy(planSpec),
357
+ constraints: planSpec.constraints,
358
+ createdBy: agentId,
359
+ },
360
+ });
361
+ const taskBlueprint = buildPlanTasks(planSpec);
362
+ const decomposition = await requestJson({
363
+ apiBase,
364
+ apiKey,
365
+ workspaceId,
366
+ path: `/plans/${plan.id}/decompose`,
367
+ body: {
368
+ strategy: buildPlanStrategy(planSpec),
369
+ agentId,
370
+ tasks: taskBlueprint.map((task) => ({
371
+ title: task.title,
372
+ description: task.description,
373
+ type: task.type,
374
+ priority: task.priority,
375
+ dependsOn: task.dependsOn,
376
+ requiredCapabilities: task.requiredCapabilities,
377
+ })),
378
+ },
379
+ });
380
+ await requestJson({
381
+ apiBase,
382
+ apiKey,
383
+ workspaceId,
384
+ path: `/plans/${plan.id}/activate`,
385
+ body: {},
386
+ });
387
+ return {
388
+ id: plan.id,
389
+ tasks: decomposition.tasks,
390
+ };
391
+ }
392
+ async function completeTaskStep(apiBase, apiKey, workspaceId, agentId, taskId, summary) {
393
+ await requestJson({
394
+ apiBase,
395
+ apiKey,
396
+ workspaceId,
397
+ path: "/tasks/claim",
398
+ body: { taskId, agentId },
399
+ });
400
+ await requestJson({
401
+ apiBase,
402
+ apiKey,
403
+ workspaceId,
404
+ path: "/tasks/complete",
405
+ body: {
406
+ taskId,
407
+ agentId,
408
+ output: { summary },
409
+ },
410
+ });
411
+ }
412
+ function escapeHtml(value) {
413
+ return value
414
+ .replaceAll("&", "&")
415
+ .replaceAll("<", "&lt;")
416
+ .replaceAll(">", "&gt;")
417
+ .replaceAll('"', "&quot;")
418
+ .replaceAll("'", "&#39;");
419
+ }
420
+ function buildPublishedFiles(starter) {
421
+ const storyJson = JSON.stringify(starter, null, 2);
422
+ const title = starter.meta.title;
423
+ const indexHtml = `<!doctype html>
424
+ <html lang="en">
425
+ <head>
426
+ <meta charset="utf-8" />
427
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
428
+ <title>${escapeHtml(title)}</title>
429
+ <link rel="stylesheet" href="./styles.css" />
430
+ </head>
431
+ <body>
432
+ <div id="app"></div>
433
+ <script type="module" src="./app.js"></script>
434
+ </body>
435
+ </html>`;
436
+ const stylesCss = `:root {
437
+ color-scheme: dark;
438
+ --bg-0: #090b10;
439
+ --bg-1: #121722;
440
+ --bg-2: rgba(22, 28, 41, 0.92);
441
+ --line: rgba(214, 202, 170, 0.14);
442
+ --text-0: #f3efe6;
443
+ --text-1: #c2bbad;
444
+ --accent: #c38f5a;
445
+ --accent-soft: rgba(195, 143, 90, 0.18);
446
+ }
447
+ * { box-sizing: border-box; }
448
+ body {
449
+ margin: 0;
450
+ min-height: 100vh;
451
+ font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif;
452
+ color: var(--text-0);
453
+ background:
454
+ radial-gradient(circle at top, rgba(195, 143, 90, 0.14), transparent 32%),
455
+ linear-gradient(180deg, #0f1219 0%, #090b10 100%);
456
+ }
457
+ .shell {
458
+ min-height: 100vh;
459
+ display: grid;
460
+ grid-template-columns: minmax(0, 1.4fr) minmax(320px, 420px);
461
+ }
462
+ .stage {
463
+ position: relative;
464
+ padding: 56px 40px 36px;
465
+ }
466
+ .backdrop {
467
+ position: absolute;
468
+ inset: 0;
469
+ opacity: 0.22;
470
+ background:
471
+ radial-gradient(circle at 18% 18%, rgba(255,255,255,0.1), transparent 25%),
472
+ linear-gradient(135deg, rgba(195, 143, 90, 0.18), transparent 48%);
473
+ pointer-events: none;
474
+ }
475
+ .story { position: relative; max-width: 860px; }
476
+ .eyebrow {
477
+ display: inline-flex;
478
+ gap: 8px;
479
+ padding: 6px 10px;
480
+ border: 1px solid var(--line);
481
+ background: rgba(9, 11, 16, 0.42);
482
+ border-radius: 999px;
483
+ color: var(--text-1);
484
+ font-size: 12px;
485
+ letter-spacing: 0.08em;
486
+ text-transform: uppercase;
487
+ }
488
+ h1 {
489
+ margin: 18px 0 10px;
490
+ font-size: clamp(40px, 7vw, 72px);
491
+ line-height: 0.98;
492
+ letter-spacing: -0.04em;
493
+ }
494
+ .subtitle { max-width: 560px; color: var(--text-1); font-size: 16px; line-height: 1.6; }
495
+ .panel {
496
+ margin-top: 28px;
497
+ padding: 24px;
498
+ background: linear-gradient(180deg, rgba(17, 22, 31, 0.94), rgba(12, 16, 24, 0.94));
499
+ border: 1px solid var(--line);
500
+ border-radius: 24px;
501
+ backdrop-filter: blur(12px);
502
+ }
503
+ .meta-grid {
504
+ display: grid;
505
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
506
+ gap: 14px;
507
+ margin-top: 18px;
508
+ }
509
+ .meta-card {
510
+ padding: 14px;
511
+ border: 1px solid var(--line);
512
+ border-radius: 18px;
513
+ background: rgba(255,255,255,0.02);
514
+ }
515
+ .meta-card span {
516
+ display: block;
517
+ font-size: 11px;
518
+ color: var(--text-1);
519
+ text-transform: uppercase;
520
+ letter-spacing: 0.08em;
521
+ }
522
+ .meta-card strong {
523
+ display: block;
524
+ margin-top: 8px;
525
+ font-size: 15px;
526
+ font-weight: 600;
527
+ }
528
+ .sidebar {
529
+ padding: 32px 24px;
530
+ border-left: 1px solid var(--line);
531
+ background: rgba(8, 10, 15, 0.74);
532
+ }
533
+ .sidebar h2 {
534
+ margin: 0 0 14px;
535
+ font-size: 14px;
536
+ letter-spacing: 0.08em;
537
+ text-transform: uppercase;
538
+ color: var(--text-1);
539
+ }
540
+ .node-title { margin: 0 0 8px; font-size: 28px; }
541
+ .node-copy { white-space: pre-wrap; line-height: 1.7; color: var(--text-0); }
542
+ .choice-list { display: grid; gap: 10px; margin-top: 18px; }
543
+ .choice {
544
+ width: 100%;
545
+ text-align: left;
546
+ border: 1px solid var(--line);
547
+ background: linear-gradient(180deg, rgba(195, 143, 90, 0.14), rgba(17, 22, 31, 0.92));
548
+ color: var(--text-0);
549
+ padding: 14px 16px;
550
+ border-radius: 16px;
551
+ cursor: pointer;
552
+ font: inherit;
553
+ }
554
+ .choice:hover {
555
+ border-color: rgba(195, 143, 90, 0.4);
556
+ transform: translateY(-1px);
557
+ }
558
+ .pill-row {
559
+ display: flex;
560
+ flex-wrap: wrap;
561
+ gap: 8px;
562
+ margin: 12px 0 0;
563
+ }
564
+ .pill {
565
+ padding: 6px 10px;
566
+ border-radius: 999px;
567
+ background: var(--accent-soft);
568
+ color: #f0d8bd;
569
+ font-size: 12px;
570
+ }
571
+ .small-list { display: grid; gap: 10px; margin-top: 18px; }
572
+ .small-card {
573
+ padding: 12px 14px;
574
+ border-radius: 16px;
575
+ border: 1px solid var(--line);
576
+ background: rgba(255,255,255,0.02);
577
+ }
578
+ .small-card strong {
579
+ display: block;
580
+ font-size: 14px;
581
+ margin-bottom: 6px;
582
+ }
583
+ .restart {
584
+ margin-top: 16px;
585
+ background: transparent;
586
+ color: var(--text-1);
587
+ border: 1px solid var(--line);
588
+ }
589
+ @media (max-width: 980px) {
590
+ .shell { grid-template-columns: 1fr; }
591
+ .sidebar { border-left: 0; border-top: 1px solid var(--line); }
592
+ .stage { padding: 36px 20px 24px; }
593
+ }`;
594
+ const appJs = `const app = document.getElementById("app");
595
+ const state = { story: null, currentId: null, nodeById: new Map(), locationById: new Map(), characterById: new Map() };
596
+
597
+ boot().catch((error) => {
598
+ app.innerHTML = '<main class="stage"><div class="panel"><h1>Bootstrap failed</h1><p class="subtitle">' + error.message + '</p></div></main>';
599
+ });
600
+
601
+ async function boot() {
602
+ const response = await fetch("./story.json");
603
+ state.story = await response.json();
604
+ for (const node of state.story.nodes) state.nodeById.set(node.id, node);
605
+ for (const location of state.story.entities.locations || []) state.locationById.set(location.id, location);
606
+ for (const character of state.story.entities.characters || []) state.characterById.set(character.id, character);
607
+ state.currentId = state.story.meta.entry;
608
+ render();
609
+ }
610
+
611
+ function render() {
612
+ const node = state.nodeById.get(state.currentId);
613
+ if (!node) throw new Error("Missing node: " + state.currentId);
614
+ const location = node.location ? state.locationById.get(node.location) : null;
615
+ const lines = (node.body || []).map(renderLine).join("");
616
+ const clues = ((node.thriller && node.thriller.introducesClues) || []).map((id) => '<span class="pill">' + escapeHtml(id.replace(/^clue\\./, "").replace(/[-_]/g, " ")) + '</span>').join("");
617
+ const cast = (node.cast || []).map((entry) => {
618
+ const character = state.characterById.get(entry.character);
619
+ return '<div class="small-card"><strong>' + escapeHtml(character?.name || entry.character) + '</strong><div>' + escapeHtml((character && character.description) || "") + '</div></div>';
620
+ }).join("");
621
+ const choices = (node.choices || []).map((choice) =>
622
+ '<button class="choice" data-next="' + escapeHtml(choice.to || "") + '">' +
623
+ '<strong>' + escapeHtml(choice.text) + '</strong>' +
624
+ (choice.thriller && choice.thriller.immediateOutcome ? '<div class="subtitle" style="font-size:13px;margin-top:6px;">' + escapeHtml(choice.thriller.immediateOutcome) + '</div>' : '') +
625
+ '</button>'
626
+ ).join("");
627
+
628
+ app.innerHTML = '<main class="shell">' +
629
+ '<section class="stage">' +
630
+ '<div class="backdrop"></div>' +
631
+ '<div class="story">' +
632
+ '<div class="eyebrow"><span>Real Link Storyworld</span><span>' + escapeHtml(node.kind) + '</span></div>' +
633
+ '<h1>' + escapeHtml(state.story.meta.title) + '</h1>' +
634
+ '<p class="subtitle">A lightweight dogfood VN shell generated from the same story world package used by real-link.ai for characters, locations, variables, and branching narrative.</p>' +
635
+ '<div class="panel">' +
636
+ '<h2 class="node-title">' + escapeHtml(node.title || node.id) + '</h2>' +
637
+ (location ? '<p class="subtitle">' + escapeHtml(location.name + ' - ' + (location.description || "")) + '</p>' : '') +
638
+ '<div class="node-copy">' + lines + '</div>' +
639
+ (clues ? '<div class="pill-row">' + clues + '</div>' : '') +
640
+ ((node.choices || []).length ? '<div class="choice-list">' + choices + '</div>' : '<button class="choice restart" data-restart="1">Restart from the beginning</button>') +
641
+ '</div>' +
642
+ '</div>' +
643
+ '</section>' +
644
+ '<aside class="sidebar">' +
645
+ '<h2>Story World</h2>' +
646
+ '<div class="meta-grid">' +
647
+ '<div class="meta-card"><span>Characters</span><strong>' + (state.story.entities.characters || []).length + '</strong></div>' +
648
+ '<div class="meta-card"><span>Locations</span><strong>' + (state.story.entities.locations || []).length + '</strong></div>' +
649
+ '<div class="meta-card"><span>Variables</span><strong>' + (state.story.entities.variables || []).length + '</strong></div>' +
650
+ '<div class="meta-card"><span>Nodes</span><strong>' + state.story.nodes.length + '</strong></div>' +
651
+ '</div>' +
652
+ '<h2 style="margin-top:22px;">Cast In Scene</h2>' +
653
+ '<div class="small-list">' + (cast || '<div class="small-card">No staged cast on this node.</div>') + '</div>' +
654
+ '<h2 style="margin-top:22px;">North Star</h2>' +
655
+ '<div class="small-card"><strong>One workspace owns the IP.</strong><div>Characters, lore, branching story, and style live once. Multiple games can project that same package into different runtime forms.</div></div>' +
656
+ '</aside>' +
657
+ '</main>';
658
+
659
+ app.querySelectorAll("[data-next]").forEach((button) => {
660
+ button.addEventListener("click", () => {
661
+ state.currentId = button.getAttribute("data-next");
662
+ render();
663
+ });
664
+ });
665
+ const restart = app.querySelector("[data-restart]");
666
+ if (restart) {
667
+ restart.addEventListener("click", () => {
668
+ state.currentId = state.story.meta.entry;
669
+ render();
670
+ });
671
+ }
672
+ }
673
+
674
+ function renderLine(entry) {
675
+ if (!entry || !entry.text) return "";
676
+ if (entry.kind === "dialogue" && entry.speaker) {
677
+ const character = state.characterById.get(entry.speaker);
678
+ return '<p><strong>' + escapeHtml((character && character.name) || entry.speaker) + ':</strong> ' + escapeHtml(entry.text) + '</p>';
679
+ }
680
+ return '<p>' + escapeHtml(entry.text) + '</p>';
681
+ }
682
+
683
+ function escapeHtml(value) {
684
+ return String(value)
685
+ .replaceAll("&", "&amp;")
686
+ .replaceAll("<", "&lt;")
687
+ .replaceAll(">", "&gt;")
688
+ .replaceAll('"', "&quot;")
689
+ .replaceAll("'", "&#39;");
690
+ }`;
691
+ return [
692
+ { path: "index.html", contentType: "text/html", content: indexHtml },
693
+ { path: "styles.css", contentType: "text/css", content: stylesCss },
694
+ { path: "app.js", contentType: "application/javascript", content: appJs },
695
+ { path: "story.json", contentType: "application/json", content: storyJson },
696
+ ];
697
+ }
698
+ async function bootstrapThrillerVN(options) {
699
+ const config = (0, config_js_1.loadConfig)();
700
+ const apiBase = normalizeApiBase(options.api);
701
+ const starter = readStarter(options.template);
702
+ const publishFiles = buildPublishedFiles(starter);
703
+ const generateAssets = typeof options.generateAssets === "boolean" ? options.generateAssets : !!options.publish;
704
+ const planOnly = !!options.planOnly;
705
+ const sourceSpec = buildThrillerSourceSpec(starter);
706
+ const projectionSpec = buildThrillerProjectionSpec();
707
+ const planSpec = {
708
+ source: sourceSpec,
709
+ projection: projectionSpec,
710
+ publish: !!options.publish,
711
+ generateAssets,
712
+ constraints: [
713
+ "workspace owns reusable IP",
714
+ `${sourceSpec.skillId} is source package only`,
715
+ "items remain physical gameplay objects",
716
+ "narrative clues live in the clue registry",
717
+ `${projectionSpec.label} is one projection of the same workspace IP package`,
718
+ ],
719
+ assetTargets: buildThrillerAssetTargets(starter),
720
+ };
721
+ const taskBlueprint = buildPlanTasks(planSpec);
722
+ const taskIndexByKey = new Map(taskBlueprint.map((task, index) => [task.key, index]));
723
+ const characterNames = new Map((starter.entities.characters || []).map((character) => [character.id, character.name]));
724
+ if (options.dryRun) {
725
+ return {
726
+ workspaceId: options.workspace || config.workspaceId || null,
727
+ gameId: null,
728
+ planId: "plan_dry_run",
729
+ agentId: "agent_dry_run",
730
+ tasksCreated: taskBlueprint.length,
731
+ createdWorkspace: !!options.workspaceName,
732
+ published: false,
733
+ starterTitle: starter.meta.title,
734
+ counts: {
735
+ characters: starter.entities.characters?.length || 0,
736
+ locations: starter.entities.locations?.length || 0,
737
+ variables: starter.entities.variables?.length || 0,
738
+ clues: starter.entities.clues?.length || 0,
739
+ nodes: starter.nodes.length,
740
+ files: publishFiles.length,
741
+ },
742
+ };
743
+ }
744
+ const explicitWorkspaceId = options.workspace || process.env.REAL_LINK_WORKSPACE_ID || null;
745
+ let workspaceId = explicitWorkspaceId || (process.env.REAL_LINK_API_KEY ? null : config.workspaceId || null);
746
+ let apiKey = process.env.REAL_LINK_API_KEY || config.apiKey;
747
+ const bearerToken = options.bearerToken || process.env.REAL_LINK_BEARER_TOKEN;
748
+ let createdWorkspace = false;
749
+ if (options.workspaceName) {
750
+ if (!bearerToken) {
751
+ throw new Error("workspace creation requires --bearer-token or REAL_LINK_BEARER_TOKEN");
752
+ }
753
+ const workspace = await requestJson({
754
+ apiBase,
755
+ path: "/workspaces/create",
756
+ bearerToken,
757
+ body: {
758
+ name: options.workspaceName,
759
+ description: options.workspaceDescription || `Story world workspace for ${options.name || starter.meta.title}`,
760
+ },
761
+ });
762
+ workspaceId = workspace.id;
763
+ const keyResult = await requestJson({
764
+ apiBase,
765
+ path: `/workspaces/${workspaceId}/api-keys`,
766
+ bearerToken,
767
+ body: {
768
+ name: `${options.name || starter.meta.title} bootstrap`,
769
+ scopes: ["agent"],
770
+ },
771
+ });
772
+ apiKey = keyResult.key;
773
+ createdWorkspace = true;
774
+ if (options.saveConfig) {
775
+ (0, config_js_1.saveConfig)({
776
+ ...config,
777
+ apiUrl: apiBase,
778
+ apiKey,
779
+ workspaceId,
780
+ });
781
+ }
782
+ }
783
+ if (!apiKey) {
784
+ throw new Error("Missing studio credentials. Provide a valid real-link.ai workspace API key, or create a workspace with a valid real-link.ai bearer token.");
785
+ }
786
+ const gameName = options.name || starter.meta.title;
787
+ const gameDescription = options.description ||
788
+ "A branching thriller visual novel generated from a reusable story world package inside real-link.ai.";
789
+ const engine = options.engine || "storyworld-vn";
790
+ const game = await requestJson({
791
+ apiBase,
792
+ apiKey,
793
+ workspaceId,
794
+ path: "/games/create",
795
+ body: {
796
+ name: gameName,
797
+ description: gameDescription,
798
+ engine,
799
+ },
800
+ });
801
+ const gameId = game.id;
802
+ if (!workspaceId || options.saveConfig) {
803
+ const games = await requestJson({
804
+ apiBase,
805
+ apiKey,
806
+ workspaceId,
807
+ path: "/games",
808
+ });
809
+ workspaceId = workspaceId || games.find((entry) => entry.id === gameId)?.workspaceId || games[0]?.workspaceId || null;
810
+ if (options.saveConfig && workspaceId) {
811
+ (0, config_js_1.saveConfig)({
812
+ ...config,
813
+ apiUrl: apiBase,
814
+ apiKey,
815
+ workspaceId,
816
+ });
817
+ }
818
+ }
819
+ const bootstrapAgent = await registerBootstrapAgent(apiBase, apiKey, workspaceId, gameName, ["story-world", sourceSpec.skillId, projectionSpec.capability, "solo-build"]);
820
+ const plan = await createBootstrapPlan(apiBase, apiKey, workspaceId, gameId, gameName, planSpec, bootstrapAgent.id);
821
+ if (planOnly) {
822
+ return {
823
+ workspaceId,
824
+ gameId,
825
+ planId: plan.id,
826
+ agentId: bootstrapAgent.id,
827
+ tasksCreated: plan.tasks.length,
828
+ createdWorkspace,
829
+ published: false,
830
+ starterTitle: starter.meta.title,
831
+ counts: {
832
+ characters: starter.entities.characters?.length || 0,
833
+ locations: starter.entities.locations?.length || 0,
834
+ variables: starter.entities.variables?.length || 0,
835
+ clues: starter.entities.clues?.length || 0,
836
+ nodes: starter.nodes.length,
837
+ files: publishFiles.length,
838
+ },
839
+ };
840
+ }
841
+ const taskIdByIndex = new Map(plan.tasks.map((task) => [task.index, task.id]));
842
+ const completeByKey = async (key, summary) => {
843
+ const taskIndex = taskIndexByKey.get(key);
844
+ if (taskIndex === undefined)
845
+ return;
846
+ const taskId = taskIdByIndex.get(taskIndex);
847
+ if (!taskId) {
848
+ throw new Error(`Missing plan task for ${key}`);
849
+ }
850
+ await completeTaskStep(apiBase, apiKey, workspaceId, bootstrapAgent.id, taskId, summary);
851
+ };
852
+ await completeByKey("source-audit", `Audited ${sourceSpec.skillId} source package ${sourceSpec.id}. Preserved authored IDs for ${sourceSpec.counts.characters} characters, ${sourceSpec.counts.locations} locations, ${sourceSpec.counts.clues} clues, ${sourceSpec.counts.variables} variables, and ${sourceSpec.counts.nodes} nodes.`);
853
+ await requestJson({
854
+ apiBase,
855
+ apiKey,
856
+ workspaceId,
857
+ path: "/content/styles",
858
+ body: {
859
+ id: DEFAULT_STYLE_ID,
860
+ gameId,
861
+ name: DEFAULT_STYLE_NAME,
862
+ description: "Grounded thriller interiors, warm practicals, cold rain light, cinematic VN framing.",
863
+ prompt: "cinematic thriller visual novel, grounded interiors, tension-driven blocking, warm tungsten practicals, cold rain light, subtle film grain, realistic anime-noir illustration",
864
+ negativePrompt: "muddy composition, duplicate characters, comedic tone, bright fantasy daylight",
865
+ rules: {
866
+ source: "thriller-writing",
867
+ northStar: "same workspace IP, multiple game projections",
868
+ },
869
+ isDefault: true,
870
+ },
871
+ });
872
+ await completeByKey("style-bible", `Seeded the default ${projectionSpec.label} style bible (${DEFAULT_STYLE_NAME}) with reusable story-world projection rules.`);
873
+ for (const character of starter.entities.characters || []) {
874
+ await requestJson({
875
+ apiBase,
876
+ apiKey,
877
+ workspaceId,
878
+ path: "/content/characters",
879
+ body: {
880
+ id: character.id,
881
+ gameId,
882
+ name: character.name,
883
+ role: inferRole(character),
884
+ bio: buildCharacterBio(character),
885
+ stats: buildCharacterStats(character),
886
+ metadata: {
887
+ source: "thriller-writing",
888
+ thrillerProfile: character.thrillerProfile || {},
889
+ defaultExpression: character.defaultExpression || "neutral",
890
+ sceneCount: countNodesForCharacter(starter, character.id),
891
+ },
892
+ },
893
+ });
894
+ }
895
+ await completeByKey("characters", `Seeded ${sourceSpec.counts.characters} characters with richer source metadata and scene coverage fields.`);
896
+ for (const location of starter.entities.locations || []) {
897
+ await requestJson({
898
+ apiBase,
899
+ apiKey,
900
+ workspaceId,
901
+ path: "/content/locations",
902
+ body: {
903
+ id: location.id,
904
+ gameId,
905
+ name: location.name,
906
+ description: location.description || "",
907
+ locationType: "area",
908
+ metadata: buildLocationMetadata(location, starter),
909
+ },
910
+ });
911
+ }
912
+ await completeByKey("locations", `Seeded ${sourceSpec.counts.locations} locations with background hints and narrative node coverage.`);
913
+ for (const clue of starter.entities.clues || []) {
914
+ await requestJson({
915
+ apiBase,
916
+ apiKey,
917
+ workspaceId,
918
+ path: "/content/clues",
919
+ body: {
920
+ id: clue.id,
921
+ gameId,
922
+ label: clue.label,
923
+ summary: clue.summary || clue.question || clue.label,
924
+ clueType: clue.kind || "evidence",
925
+ question: clue.question || "",
926
+ supports: clue.supports || [],
927
+ critical: !!clue.critical,
928
+ metadata: buildClueMetadata(clue),
929
+ },
930
+ });
931
+ }
932
+ await completeByKey("clues", `Seeded ${sourceSpec.counts.clues} narrative clues into the evidence registry instead of gameplay inventory items.`);
933
+ for (const variable of starter.entities.variables || []) {
934
+ await requestJson({
935
+ apiBase,
936
+ apiKey,
937
+ workspaceId,
938
+ path: "/content/variables",
939
+ body: {
940
+ id: variable.id,
941
+ gameId,
942
+ name: variable.id,
943
+ variableType: variable.type || "boolean",
944
+ defaultValue: stringifyDefaultValue(variable.default),
945
+ description: variable.rationale || variable.id,
946
+ category: inferVariableCategory(variable),
947
+ },
948
+ });
949
+ }
950
+ await completeByKey("variables", `Seeded ${sourceSpec.counts.variables} story variables with rationale, design role, and payoff coverage.`);
951
+ for (const node of starter.nodes) {
952
+ await requestJson({
953
+ apiBase,
954
+ apiKey,
955
+ workspaceId,
956
+ path: "/content/dialogue",
957
+ body: {
958
+ id: node.id,
959
+ gameId,
960
+ speakerId: pickSpeakerId(node),
961
+ nodeType: mapNodeType(node.kind),
962
+ text: flattenNodeText(node, characterNames),
963
+ nextNodeId: node.to || node.nextNodeId || null,
964
+ choices: (node.choices || []).map((choice) => ({
965
+ text: choice.text,
966
+ nextNodeId: choice.to || null,
967
+ condition: choice.condition || null,
968
+ })),
969
+ metadata: {
970
+ title: node.title || node.id,
971
+ source: "thriller-writing",
972
+ originalKind: node.kind,
973
+ locationId: node.location || null,
974
+ cast: node.cast || [],
975
+ thriller: node.thriller || {},
976
+ presentation: node.presentation || {},
977
+ sourceFile: node.source || {},
978
+ notes: node.notes || "",
979
+ body: node.body || [],
980
+ choiceDetails: (node.choices || []).map((choice) => ({
981
+ id: choice.id,
982
+ text: choice.text,
983
+ effects: choice.effects || [],
984
+ thriller: choice.thriller || {},
985
+ })),
986
+ },
987
+ },
988
+ });
989
+ }
990
+ await completeByKey("dialogue", `Seeded ${sourceSpec.counts.nodes} branching nodes with preserved authored IDs, choice details, and clue-introduction metadata.`);
991
+ const narrativeOverview = await requestJson({
992
+ apiBase,
993
+ apiKey,
994
+ workspaceId,
995
+ path: `/content/narrative?gameId=${encodeURIComponent(gameId)}`,
996
+ });
997
+ await completeByKey("coverage-review", `Narrative review: ${narrativeOverview.summary.nodes} nodes, ${narrativeOverview.summary.branchPoints} branch points, ${narrativeOverview.summary.clues} clues, ${narrativeOverview.summary.danglingEdges} dangling edges, ${narrativeOverview.summary.unreachableNodes} unreachable nodes.`);
998
+ if (generateAssets) {
999
+ for (const character of starter.entities.characters || []) {
1000
+ await requestJson({
1001
+ apiBase,
1002
+ apiKey,
1003
+ workspaceId,
1004
+ path: "/content/generate-for",
1005
+ body: {
1006
+ entityType: "character",
1007
+ entityId: character.id,
1008
+ assetRole: "portrait",
1009
+ prompt: `${character.name}, ${character.description || "thriller character portrait"}`,
1010
+ },
1011
+ });
1012
+ await completeByKey(`portrait:${character.id}`, `Generated reusable portrait for ${character.name} in the workspace IP package.`);
1013
+ }
1014
+ for (const location of starter.entities.locations || []) {
1015
+ await requestJson({
1016
+ apiBase,
1017
+ apiKey,
1018
+ workspaceId,
1019
+ path: "/content/generate-for",
1020
+ body: {
1021
+ entityType: "location",
1022
+ entityId: location.id,
1023
+ assetRole: "background",
1024
+ prompt: `${location.name}, ${location.description || "thriller location background"}`,
1025
+ },
1026
+ });
1027
+ await completeByKey(`background:${location.id}`, `Generated reusable background for ${location.name} in the workspace IP package.`);
1028
+ }
1029
+ await completeByKey("asset-review", `Generated core asset coverage for ${sourceSpec.counts.characters} characters and ${sourceSpec.counts.locations} locations.`);
1030
+ }
1031
+ let publishUrl;
1032
+ if (options.publish) {
1033
+ await requestJson({
1034
+ apiBase,
1035
+ apiKey,
1036
+ workspaceId,
1037
+ path: "/phases/set",
1038
+ body: {
1039
+ gameId,
1040
+ phaseId: "release",
1041
+ reason: "Thriller VN bootstrap publish",
1042
+ },
1043
+ });
1044
+ await requestJson({
1045
+ apiBase,
1046
+ apiKey,
1047
+ workspaceId,
1048
+ path: "/publish/validate",
1049
+ body: { gameId },
1050
+ });
1051
+ await requestJson({
1052
+ apiBase,
1053
+ apiKey,
1054
+ workspaceId,
1055
+ path: "/publish/upload-batch",
1056
+ body: {
1057
+ gameId,
1058
+ files: publishFiles.map((file) => ({
1059
+ path: file.path,
1060
+ contentType: file.contentType,
1061
+ content: Buffer.from(file.content, "utf8").toString("base64"),
1062
+ })),
1063
+ },
1064
+ });
1065
+ const publish = await requestJson({
1066
+ apiBase,
1067
+ apiKey,
1068
+ workspaceId,
1069
+ path: "/publish/finalize",
1070
+ body: {
1071
+ gameId,
1072
+ version: options.version || "0.1.0",
1073
+ fileCount: publishFiles.length,
1074
+ },
1075
+ });
1076
+ publishUrl = publish.url;
1077
+ await completeByKey("publish", `Published ${projectionSpec.publishLabel} to ${publishUrl}.`);
1078
+ }
1079
+ await requestJson({
1080
+ apiBase,
1081
+ apiKey,
1082
+ workspaceId,
1083
+ path: `/plans/${plan.id}/complete`,
1084
+ body: {},
1085
+ });
1086
+ return {
1087
+ workspaceId,
1088
+ gameId,
1089
+ planId: plan.id,
1090
+ agentId: bootstrapAgent.id,
1091
+ tasksCreated: plan.tasks.length,
1092
+ createdWorkspace,
1093
+ published: !!publishUrl,
1094
+ publishUrl,
1095
+ starterTitle: starter.meta.title,
1096
+ counts: {
1097
+ characters: starter.entities.characters?.length || 0,
1098
+ locations: starter.entities.locations?.length || 0,
1099
+ variables: starter.entities.variables?.length || 0,
1100
+ clues: starter.entities.clues?.length || 0,
1101
+ nodes: starter.nodes.length,
1102
+ files: publishFiles.length,
1103
+ },
1104
+ };
1105
+ }
1106
+ //# sourceMappingURL=thriller-vn.js.map