create-spatius-app 0.1.3-beta.1 → 0.1.3-beta.2

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 (41) hide show
  1. package/README.md +11 -10
  2. package/dist/cli.js +87 -162
  3. package/dist/templates.js +71 -154
  4. package/package.json +2 -2
  5. package/templates/cloudflare-livekit/AGENTS.md +2 -0
  6. package/templates/cloudflare-livekit/agent/README.md +13 -0
  7. package/templates/cloudflare-livekit/agent/pyproject.toml +1 -1
  8. package/templates/cloudflare-livekit/agent/src/agent.py +2 -1
  9. package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +49 -0
  10. package/templates/cloudflare-livekit/agent/uv.lock +14 -14
  11. package/templates/cloudflare-livekit/gitignore +1 -0
  12. package/templates/cloudflare-livekit/package-lock.json +177 -16
  13. package/templates/cloudflare-livekit/package.json +8 -7
  14. package/templates/cloudflare-livekit/pnpm-lock.yaml +104 -24
  15. package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +7 -2
  16. package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +17 -14
  17. package/templates/layers/agora/AGENTS.md +9 -4
  18. package/templates/layers/agora/README.md +14 -8
  19. package/templates/layers/agora/package-lock.json +1344 -424
  20. package/templates/layers/agora/package.json +17 -11
  21. package/templates/layers/agora/pnpm-lock.yaml +1069 -237
  22. package/templates/layers/agora/scripts/check-worker.mjs +98 -0
  23. package/templates/layers/agora/tsconfig.worker.json +23 -0
  24. package/templates/layers/agora/worker/agora.test.ts +2 -0
  25. package/templates/layers/agora/worker/agora.ts +1 -10
  26. package/templates/layers/agora/worker/index.test.ts +66 -0
  27. package/templates/layers/agora/worker/index.ts +4 -0
  28. package/templates/layers/agora/wrangler.jsonc +24 -0
  29. package/templates/cloudflare-livekit/worker-configuration.d.ts +0 -17184
  30. package/templates/layers/agora/tsconfig.json +0 -16
  31. package/templates/layers/node/Dockerfile +0 -12
  32. package/templates/layers/node/dockerignore +0 -10
  33. package/templates/layers/node/package-lock.json +0 -5569
  34. package/templates/layers/node/pnpm-lock.yaml +0 -3470
  35. package/templates/layers/node/railway.json +0 -8
  36. package/templates/layers/node/scripts/dev-service.mjs +0 -14
  37. package/templates/layers/node/scripts/dev-web.mjs +0 -22
  38. package/templates/layers/node/server/index.ts +0 -92
  39. package/templates/layers/node/vite.config.ts +0 -17
  40. package/templates/layers/railway-agent/agent/railway.json +0 -5
  41. /package/templates/layers/agora/{env.example → dev.vars.example} +0 -0
package/dist/templates.js CHANGED
@@ -95,32 +95,14 @@ var init_catalog = __esm({
95
95
  init_errors();
96
96
  stacks = {
97
97
  "cloudflare-livekit": {
98
- label: "Cloudflare + LiveKit Cloud Agents",
98
+ label: "LiveKit",
99
99
  web: "cloudflare",
100
100
  agent: "livekit",
101
101
  provider: "livekit"
102
102
  },
103
- "cloudflare-livekit-railway": {
104
- label: "Cloudflare + LiveKit Agents on Railway",
103
+ "cloudflare-agora": {
104
+ label: "Agora Conversational AI",
105
105
  web: "cloudflare",
106
- agent: "railway",
107
- provider: "livekit"
108
- },
109
- "railway-livekit-cloud": {
110
- label: "Railway + LiveKit Cloud Agents",
111
- web: "railway",
112
- agent: "livekit",
113
- provider: "livekit"
114
- },
115
- "railway-livekit": {
116
- label: "Railway + LiveKit Agents on Railway",
117
- web: "railway",
118
- agent: "railway",
119
- provider: "livekit"
120
- },
121
- "zeabur-agora": {
122
- label: "Zeabur + Agora Conversational AI",
123
- web: "zeabur",
124
106
  agent: "agora",
125
107
  provider: "agora"
126
108
  }
@@ -158,8 +140,8 @@ async function readProjectConfig(directory) {
158
140
  }
159
141
  }
160
142
  async function webEnvironmentPath(directory) {
161
- const config = await readProjectConfig(directory);
162
- return config && !config.stack.startsWith("cloudflare") ? ".env.local" : ".dev.vars";
143
+ await readProjectConfig(directory);
144
+ return ".dev.vars";
163
145
  }
164
146
  var init_project_config = __esm({
165
147
  "src/project-config.ts"() {
@@ -9850,6 +9832,10 @@ var init_wizard = __esm({
9850
9832
  init_project_config();
9851
9833
  init_catalog();
9852
9834
 
9835
+ // src/templates/composed.ts
9836
+ import { writeFile as writeFile6 } from "fs/promises";
9837
+ import { join as join8 } from "path";
9838
+
9853
9839
  // src/templates/agora.ts
9854
9840
  init_package_managers();
9855
9841
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
@@ -10131,6 +10117,7 @@ var cloudflareLivekitTemplate = {
10131
10117
  "- agent/ Python LiveKit agent"
10132
10118
  ],
10133
10119
  includeFile(path, configuration) {
10120
+ if (path === "worker-configuration.d.ts") return false;
10134
10121
  const javascript = configuration?.packageManagers.javascript.name ?? "pnpm";
10135
10122
  const python = configuration?.packageManagers.python?.name ?? "uv";
10136
10123
  if (path === "package-lock.json") return javascript === "npm";
@@ -10210,17 +10197,13 @@ var runAgoraSetup = async ({
10210
10197
  { exitCode: EXIT_CODES.invalidArgument }
10211
10198
  );
10212
10199
  const config = await readProjectConfig(targetDirectory);
10213
- if (config?.stack !== "zeabur-agora")
10200
+ if (config?.stack !== "cloudflare-agora")
10214
10201
  throw new CliError(
10215
10202
  "INVALID_ARGUMENT",
10216
- "Expected a Zeabur + Agora project.",
10203
+ "Expected a Cloudflare + Agora project.",
10217
10204
  { exitCode: EXIT_CODES.invalidArgument }
10218
10205
  );
10219
- for (const name of [
10220
- "package.json",
10221
- ".env.local.example",
10222
- "worker/agora.ts"
10223
- ]) {
10206
+ for (const name of ["package.json", ".dev.vars.example", "worker/agora.ts"]) {
10224
10207
  const stats = await lstat4(join6(targetDirectory, name));
10225
10208
  if (!stats.isFile() || stats.isSymbolicLink())
10226
10209
  throw new CliError(
@@ -10229,7 +10212,7 @@ var runAgoraSetup = async ({
10229
10212
  { exitCode: EXIT_CODES.invalidArgument }
10230
10213
  );
10231
10214
  }
10232
- const path = join6(targetDirectory, ".env.local");
10215
+ const path = join6(targetDirectory, ".dev.vars");
10233
10216
  let existing;
10234
10217
  try {
10235
10218
  const stats = await lstat4(path);
@@ -10238,7 +10221,7 @@ var runAgoraSetup = async ({
10238
10221
  existing = await readFile5(path, "utf8");
10239
10222
  } catch (error) {
10240
10223
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
10241
- throw new CliError("FILESYSTEM_ERROR", "Cannot safely read .env.local.", {
10224
+ throw new CliError("FILESYSTEM_ERROR", "Cannot safely read .dev.vars.", {
10242
10225
  exitCode: EXIT_CODES.filesystem,
10243
10226
  path
10244
10227
  });
@@ -10290,10 +10273,10 @@ var runAgoraSetup = async ({
10290
10273
  }
10291
10274
  }
10292
10275
  const rendered = mergeDotenv(
10293
- existing ?? await readFile5(join6(targetDirectory, ".env.local.example"), "utf8"),
10276
+ existing ?? await readFile5(join6(targetDirectory, ".dev.vars.example"), "utf8"),
10294
10277
  values
10295
10278
  );
10296
- const temporary = join6(targetDirectory, `.env.local.${randomUUID3()}.tmp`);
10279
+ const temporary = join6(targetDirectory, `.dev.vars.${randomUUID3()}.tmp`);
10297
10280
  try {
10298
10281
  await writeFile4(temporary, rendered, { flag: "wx", mode: 384 });
10299
10282
  await rename2(temporary, path);
@@ -10301,7 +10284,7 @@ var runAgoraSetup = async ({
10301
10284
  await rm3(temporary, { force: true });
10302
10285
  }
10303
10286
  onStatus(
10304
- "Saved .env.local. Configure an English assistant in the published Agora pipeline, with its TTS sample rate matching AGORA_AVATAR_SAMPLE_RATE. No cloud services were started."
10287
+ "Saved .dev.vars. Configure an English assistant in the published Agora pipeline, with its TTS sample rate matching AGORA_AVATAR_SAMPLE_RATE. No cloud services were started."
10305
10288
  );
10306
10289
  return "configured";
10307
10290
  };
@@ -10315,8 +10298,6 @@ var files = /* @__PURE__ */ new Set([
10315
10298
  "pnpm-workspace.yaml",
10316
10299
  "npmrc",
10317
10300
  "gitignore",
10318
- "dockerignore",
10319
- "Dockerfile",
10320
10301
  ".editorconfig",
10321
10302
  ".prettierrc.json",
10322
10303
  ".prettierignore",
@@ -10326,12 +10307,17 @@ var files = /* @__PURE__ */ new Set([
10326
10307
  "spatius.config.json",
10327
10308
  "index.html",
10328
10309
  "tsconfig.json",
10310
+ "tsconfig.web.json",
10311
+ "tsconfig.worker.json",
10312
+ "tsconfig.node.json",
10329
10313
  "vite.config.ts",
10330
10314
  "vitest.config.ts",
10331
10315
  "eslint.config.mjs",
10332
- "env.example",
10333
- "server/index.ts",
10316
+ "dev.vars.example",
10317
+ "wrangler.jsonc",
10318
+ "scripts/check-worker.mjs",
10334
10319
  "worker/index.ts",
10320
+ "worker/index.test.ts",
10335
10321
  "worker/agora.ts",
10336
10322
  "worker/agora.test.ts",
10337
10323
  "worker/capability.ts",
@@ -10349,8 +10335,8 @@ var files = /* @__PURE__ */ new Set([
10349
10335
  "web/components/agents-ui/PROVENANCE.md"
10350
10336
  ]);
10351
10337
  var agoraTemplate = {
10352
- id: "zeabur-agora",
10353
- stack: "zeabur-agora",
10338
+ id: "cloudflare-agora",
10339
+ stack: "cloudflare-agora",
10354
10340
  requiresPython: false,
10355
10341
  humanSteps: [
10356
10342
  {
@@ -10361,17 +10347,13 @@ var agoraTemplate = {
10361
10347
  }
10362
10348
  ],
10363
10349
  directory: "templates/cloudflare-livekit",
10364
- description: "Zeabur + Agora Conversational AI",
10350
+ description: "Cloudflare Workers + Agora Conversational AI",
10365
10351
  components: [
10366
10352
  "- web/ React + Spatius AvatarKit + Agora",
10367
- "- server/ Node HTTP API; Agora hosts the conversational agent"
10353
+ "- worker/ Cloudflare API; Agora hosts the conversational agent"
10368
10354
  ],
10369
10355
  layers: [
10370
10356
  { directory: "templates/layers/catalog" },
10371
- {
10372
- directory: "templates/layers/node",
10373
- overrides: ["vite.config.ts", "package-lock.json", "pnpm-lock.yaml"]
10374
- },
10375
10357
  {
10376
10358
  directory: "templates/layers/agora",
10377
10359
  overrides: [
@@ -10380,7 +10362,10 @@ var agoraTemplate = {
10380
10362
  "pnpm-lock.yaml",
10381
10363
  "web/App.tsx",
10382
10364
  "worker/index.ts",
10383
- "tsconfig.json",
10365
+ "worker/index.test.ts",
10366
+ "tsconfig.worker.json",
10367
+ ".dev.vars.example",
10368
+ "wrangler.jsonc",
10384
10369
  "vitest.config.ts",
10385
10370
  "README.md",
10386
10371
  "AGENTS.md"
@@ -10392,7 +10377,7 @@ var agoraTemplate = {
10392
10377
  return files.has(path) || [...files].some((file) => file.startsWith(path + "/")) || path === "web/assets" || path.startsWith("web/assets/");
10393
10378
  },
10394
10379
  mapFile(path) {
10395
- return path === "env.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path);
10380
+ return cloudflareLivekitTemplate.mapFile(path);
10396
10381
  },
10397
10382
  async configure(directory, configuration) {
10398
10383
  const manager = configuration.packageManagers.javascript;
@@ -10404,8 +10389,20 @@ var agoraTemplate = {
10404
10389
  );
10405
10390
  if (manager.version && manager.version !== "unknown")
10406
10391
  pkg.packageManager = `${manager.name}@${manager.version}`;
10407
- pkg.scripts.check = ["format:check", "lint", "typecheck", "test", "build"].map(run).join(" && ");
10408
- if (manager.name === "bun") pkg.trustedDependencies = ["esbuild"];
10392
+ else delete pkg.packageManager;
10393
+ pkg.scripts.check = [
10394
+ "format:check",
10395
+ "lint",
10396
+ "typecheck",
10397
+ "test",
10398
+ "build",
10399
+ "test:worker"
10400
+ ].map(run).join(" && ");
10401
+ pkg.scripts.deploy = `${run("build")} && wrangler deploy`;
10402
+ if (manager.name === "bun")
10403
+ pkg.trustedDependencies = ["core-js", "esbuild", "workerd"];
10404
+ else if (manager.name === "npm" && Number.parseInt(manager.version ?? "", 10) >= 12)
10405
+ pkg.allowScripts = { "core-js": true, esbuild: true, workerd: true };
10409
10406
  await writeFile5(path, JSON.stringify(pkg, null, 2) + "\n");
10410
10407
  if (manager.name === "npm") {
10411
10408
  const lockPath = join7(directory, "package-lock.json");
@@ -10416,24 +10413,7 @@ var agoraTemplate = {
10416
10413
  }
10417
10414
  await writeFile5(
10418
10415
  join7(directory, "spatius.config.json"),
10419
- JSON.stringify({ version: 2, stack: "zeabur-agora" }, null, 2) + "\n"
10420
- );
10421
- const serverPath = join7(directory, "server/index.ts");
10422
- await writeFile5(
10423
- serverPath,
10424
- (await readFile6(serverPath, "utf8")).replace(
10425
- "import { handleRequest }",
10426
- "import type { AgoraEnvironment } from '../worker/agora.js';\nimport { handleRequest }"
10427
- ).replace(
10428
- "const env = process.env;",
10429
- "const env = process.env as unknown as AgoraEnvironment;"
10430
- )
10431
- );
10432
- const dockerPath = join7(directory, "Dockerfile");
10433
- const install = manager.name === "pnpm" ? "npm install --global pnpm@12.3.4 && pnpm install --frozen-lockfile" : manager.name === "bun" ? "npm install --global bun && bun install" : "npm ci";
10434
- await writeFile5(
10435
- dockerPath,
10436
- (await readFile6(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
10416
+ JSON.stringify({ version: 2, stack: "cloudflare-agora" }, null, 2) + "\n"
10437
10417
  );
10438
10418
  for (const name of ["README.md", "AGENTS.md"]) {
10439
10419
  const path2 = join7(directory, name);
@@ -10444,7 +10424,16 @@ var agoraTemplate = {
10444
10424
  }
10445
10425
  await writeFile5(
10446
10426
  join7(directory, "DEPLOYMENT.md"),
10447
- "# Zeabur + Agora Conversational AI\n\nCreate one Zeabur service from this repository with the repository root as build root. Set ZBPACK_DOCKERFILE_PATH=Dockerfile and PORT=8787. Copy the environment variables from .env.local.example into Zeabur service variables, then generate an HTTPS domain. Use /api/health for the health check. Git redeployments rebuild the same service.\n\nThe Node service serves both frontend and API. There is no Python worker or database. Enable Agora Conversational AI and RTM, publish an English assistant pipeline, and set AGORA_PIPELINE_ID. The pipeline owns its models and voice. Match AGORA_AVATAR_SAMPLE_RATE to its TTS output.\n\nSession creation starts a hosted agent. Disconnect and initialization failures stop it through the backend. Page-close cleanup is best effort, with a 60-second idle timeout fallback. Sessions and client tokens last 30 minutes; start a fresh conversation afterward.\n\nLocal setup writes .env.local only. It never provisions Zeabur or uploads secrets.\n"
10427
+ `# Cloudflare Workers + Agora Conversational AI
10428
+
10429
+ Run \`${run("wrangler -- login")}\`. Set your non-secret configuration from .dev.vars in the vars section of wrangler.jsonc. Upload AGORA_APP_CERTIFICATE and SPATIUS_API_KEY with \`${run("wrangler -- secret put AGORA_APP_CERTIFICATE")}\` (repeat for SPATIUS_API_KEY). Run \`${run("deploy")}\` to deploy the React frontend and API as one Cloudflare Worker.
10430
+
10431
+ Agora hosts the conversational agent. There is no Python worker, Node server, container, or database to deploy. Enable Agora Conversational AI and RTM, publish an English assistant pipeline, and set AGORA_PIPELINE_ID. The pipeline owns its models and voice. Match AGORA_AVATAR_SAMPLE_RATE to its TTS output.
10432
+
10433
+ Session creation starts a hosted agent. Disconnect and initialization failures stop it through the Worker API. Page-close cleanup is best effort, with a 60-second idle timeout fallback. Sessions and client tokens last 30 minutes; start a fresh conversation afterward.
10434
+
10435
+ Local setup writes .dev.vars only. It never provisions Cloudflare services or uploads secrets.
10436
+ `
10448
10437
  );
10449
10438
  },
10450
10439
  createInstallPlan(directory, managers, platform) {
@@ -10466,7 +10455,7 @@ var agoraTemplate = {
10466
10455
  ];
10467
10456
  },
10468
10457
  setup: {
10469
- recognizes: async (directory) => (await readProjectConfig(directory))?.stack === "zeabur-agora",
10458
+ recognizes: async (directory) => (await readProjectConfig(directory))?.stack === "cloudflare-agora",
10470
10459
  run: runAgoraSetup
10471
10460
  },
10472
10461
  verification: [
@@ -10476,105 +10465,33 @@ var agoraTemplate = {
10476
10465
  };
10477
10466
 
10478
10467
  // src/templates/composed.ts
10479
- init_catalog();
10480
- import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
10481
- import { join as join8 } from "path";
10482
10468
  init_package_managers();
10483
10469
  function composeTemplate(stack) {
10484
- if (stack === "zeabur-agora") return agoraTemplate;
10485
- const selected = stacks[stack];
10486
- const node = selected.web === "railway";
10470
+ if (stack === "cloudflare-agora") return agoraTemplate;
10487
10471
  return {
10488
10472
  ...cloudflareLivekitTemplate,
10489
- id: stack,
10490
10473
  stack,
10491
10474
  requiresPython: true,
10492
- description: selected.label,
10493
- components: [
10494
- "- web/ React + Spatius AvatarKit",
10495
- `- ${node ? "server/" : "worker/"} ${selected.web} API`,
10496
- "- agent/ Python LiveKit agent"
10497
- ],
10498
- layers: [
10499
- { directory: "templates/layers/catalog" },
10500
- ...selected.agent === "railway" ? [{ directory: "templates/layers/railway-agent" }] : [],
10501
- ...node ? [
10502
- {
10503
- directory: "templates/layers/node",
10504
- overrides: [
10505
- "vite.config.ts",
10506
- "scripts/dev-web.mjs",
10507
- "package-lock.json",
10508
- "pnpm-lock.yaml"
10509
- ]
10510
- }
10511
- ] : []
10512
- ],
10513
- includeFile(path, configuration) {
10514
- if (node && ["wrangler.jsonc"].includes(path)) return false;
10515
- return cloudflareLivekitTemplate.includeFile(path, configuration);
10516
- },
10517
- mapFile(path) {
10518
- return node && path === "dev.vars.example" ? ".env.local.example" : cloudflareLivekitTemplate.mapFile(path);
10519
- },
10475
+ layers: [{ directory: "templates/layers/catalog" }],
10520
10476
  async configure(directory, configuration) {
10521
10477
  await cloudflareLivekitTemplate.configure(directory, configuration);
10522
10478
  await writeFile6(
10523
10479
  join8(directory, "spatius.config.json"),
10524
10480
  JSON.stringify({ version: 2, stack }, null, 2) + "\n"
10525
10481
  );
10526
- const packagePath = join8(directory, "package.json");
10527
- const pkg = JSON.parse(await readFile7(packagePath, "utf8"));
10528
10482
  const manager = configuration.packageManagers.javascript.name;
10529
10483
  const run = (script) => javascriptRunCommand(manager, script);
10530
- let deployment = `# ${selected.label}
10484
+ await writeFile6(
10485
+ join8(directory, "DEPLOYMENT.md"),
10486
+ `# Cloudflare Workers + LiveKit Cloud
10531
10487
 
10532
- `;
10533
- if (node) {
10534
- for (const name of ["README.md", "AGENTS.md", "agent/README.md"]) {
10535
- const path = join8(directory, name);
10536
- await writeFile6(
10537
- path,
10538
- (await readFile7(path, "utf8")).replaceAll("Cloudflare Workers", "Railway").replaceAll("Cloudflare Worker", "Node API").replaceAll(".dev.vars", ".env.local") + "\nSee DEPLOYMENT.md for this stack\u2019s deployment steps.\n"
10539
- );
10540
- }
10541
- const workerConfigPath = join8(directory, "tsconfig.worker.json");
10542
- await writeFile6(
10543
- workerConfigPath,
10544
- (await readFile7(workerConfigPath, "utf8")).replace(
10545
- '"types": ["./worker-configuration.d.ts"]',
10546
- '"types": ["./worker-configuration.d.ts", "node"]'
10547
- ).replace(
10548
- '"include": ["worker", "worker-configuration.d.ts"]',
10549
- '"include": ["worker", "worker-configuration.d.ts", "server"]'
10550
- )
10551
- );
10552
- delete pkg.devDependencies["@cloudflare/vite-plugin"];
10553
- delete pkg.devDependencies.wrangler;
10554
- pkg.dependencies.tsx = "4.23.13";
10555
- delete pkg.scripts["cf-typegen"];
10556
- delete pkg.scripts.wrangler;
10557
- pkg.scripts.start = "node --env-file-if-exists=.env.local node_modules/tsx/dist/cli.mjs server/index.ts";
10558
- pkg.scripts.deploy = "railway up";
10559
- const dockerPath = join8(directory, "Dockerfile");
10560
- const install = manager === "pnpm" ? "npm install --global pnpm@12.3.4 && pnpm install --frozen-lockfile" : manager === "bun" ? "npm install --global bun && bun install" : "npm ci";
10561
- await writeFile6(
10562
- dockerPath,
10563
- (await readFile7(dockerPath, "utf8")).replace("__SPATIUS_CONTAINER_INSTALL__", install).replace("__SPATIUS_CONTAINER_BUILD__", run("build"))
10564
- );
10565
- deployment += "Create a Railway service from this repository with the repository root as its build root and railway.json as its configuration. Add the variables from .env.local.example in Railway. Generate a public domain. The web service serves both the frontend and /api. Deploy with `railway up`; subsequent Git deployments rebuild the service.\n\n";
10566
- } else {
10567
- deployment += `Run \`${run("wrangler -- login")}\`, set the non-secret configuration from .dev.vars.example in wrangler.jsonc, and upload LIVEKIT_API_KEY and LIVEKIT_API_SECRET using \`${run("wrangler -- secret put LIVEKIT_API_SECRET")}\` (repeat for the key). Run \`${run("deploy")}\`.
10488
+ Run \`${run("wrangler -- login")}\`, set the non-secret configuration from .dev.vars.example in wrangler.jsonc, and upload LIVEKIT_API_KEY and LIVEKIT_API_SECRET using \`${run("wrangler -- secret put LIVEKIT_API_SECRET")}\` (repeat for the key). Run \`${run("deploy")}\` to deploy the frontend and API together.
10568
10489
 
10569
- `;
10570
- }
10571
- if (selected.agent === "railway") {
10572
- deployment += "Create a separate Railway agent service from this repository, with root directory /agent and configuration /agent/railway.json. Add the variables from agent/.env.example. Keep it running continuously; do not enable sleeping. No public domain is needed. Set PORT=8081 for the agent health check. Deploy this service independently of the web service.\n";
10573
- } else
10574
- deployment += "From agent/, run `lk cloud auth` and `lk agent create` for the first deployment. Configure the agent secrets from agent/.env.example. Use `lk agent deploy` for updates.\n";
10575
- deployment += "\nDeploy the agent before opening conversations. Use separate LiveKit projects for development and production. Local setup never uploads secrets.\n";
10576
- await writeFile6(join8(directory, "DEPLOYMENT.md"), deployment);
10577
- await writeFile6(packagePath, JSON.stringify(pkg, null, 2) + "\n");
10490
+ From agent/, run \`lk cloud auth\` and \`lk agent create\` for the first deployment. Configure the agent secrets from agent/.env.example. Use \`lk agent deploy\` for updates.
10491
+
10492
+ Deploy the agent before opening conversations. Use separate LiveKit projects for development and production. Local setup never uploads secrets.
10493
+ `
10494
+ );
10578
10495
  }
10579
10496
  };
10580
10497
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-spatius-app",
3
- "version": "0.1.3-beta.1",
4
- "description": "Create a Spatius voice-avatar app with LiveKit or Agora and your choice of hosting.",
3
+ "version": "0.1.3-beta.2",
4
+ "description": "Create a Spatius voice-avatar app with LiveKit or Agora Conversational AI on Cloudflare Workers.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-spatius-app": "./dist/cli.js"
@@ -22,6 +22,8 @@ Run from the project root:
22
22
 
23
23
  All checks can run after dependency installation, before credential setup.
24
24
  Browser tests require Chromium; see README.md for the installation command.
25
+ Wrangler generates the ignored `worker-configuration.d.ts` during installation
26
+ and before lint, type checking, and builds. Update `wrangler.jsonc` to change bindings.
25
27
 
26
28
  ## Working guidelines
27
29
 
@@ -6,6 +6,19 @@ and turn detection. The Spatius plugin sends the generated speech through the
6
6
  Spatius motion service and publishes synchronized avatar data into the same
7
7
  LiveKit room as the browser.
8
8
 
9
+ ## Session startup
10
+
11
+ The worker runs `spatius.prewarm` through `AgentServer`'s `setup_fnc` when each
12
+ job process initializes. It prepares automatic region selection, TLS connections,
13
+ and a session token using the same Spatius environment variables as the avatar
14
+ session, so a warm process can skip that setup when a room is dispatched.
15
+ The avatar ID still comes from the job's dispatch metadata.
16
+
17
+ Warm-up is best effort: missing configuration or a failed warm-up leaves normal
18
+ session startup responsible for resolving the region and fetching a token.
19
+ LiveKit keeps idle processes ready in production; development mode initializes
20
+ processes on demand, so its first session still pays the warm-up cost.
21
+
9
22
  ## Voice configuration
10
23
 
11
24
  - TTS uses `cartesia/sonic-3.6` through LiveKit Inference. Setup offers
@@ -5,7 +5,7 @@ description = "A LiveKit voice agent with a Spatius avatar"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11,<3.15"
7
7
  dependencies = [
8
- "livekit-agents[spatius]~=1.8.0",
8
+ "livekit-agents[spatius]~=1.8.1",
9
9
  "livekit-plugins-noise-cancellation~=0.3.0",
10
10
  "python-dotenv>=1.2.1,<2",
11
11
  ]
@@ -43,7 +43,8 @@ class Assistant(Agent):
43
43
  )
44
44
 
45
45
 
46
- server = AgentServer()
46
+ # Warm region, TLS, and session-token caches in each job process before dispatch.
47
+ server = AgentServer(setup_fnc=spatius.prewarm)
47
48
 
48
49
 
49
50
  @server.on("worker_registered")
@@ -11,6 +11,55 @@ with patch("dotenv.load_dotenv"):
11
11
  from src import agent as agent_module
12
12
 
13
13
 
14
+ class ProcessPrewarmTests(unittest.TestCase):
15
+ def setUp(self) -> None:
16
+ self.enterContext(
17
+ patch.dict(
18
+ "os.environ",
19
+ {"SPATIUS_APP_ID": "app-test", "SPATIUS_API_KEY": "key-test"},
20
+ clear=True,
21
+ )
22
+ )
23
+ # Exercise the registered hook and installed plugin without provider calls.
24
+ self.sdk_prewarm = self.enterContext(
25
+ patch(
26
+ "spatius.prewarm",
27
+ new_callable=AsyncMock,
28
+ return_value=SimpleNamespace(
29
+ region="test-region",
30
+ tls_warmed=True,
31
+ session_token_prefetched=True,
32
+ ),
33
+ )
34
+ )
35
+
36
+ def test_process_setup_warms_connection_state_without_an_avatar(self) -> None:
37
+ with patch.object(agent_module.spatius, "AvatarSession") as avatar:
38
+ agent_module.server.setup_fnc(SimpleNamespace())
39
+
40
+ self.sdk_prewarm.assert_awaited_once()
41
+ options = self.sdk_prewarm.call_args.kwargs
42
+ self.assertEqual(options["app_id"], "app-test")
43
+ self.assertEqual(options["api_key"], "key-test")
44
+ self.assertEqual(options["region"], "auto")
45
+ self.assertTrue(options["prefetch_session_token"])
46
+ avatar.assert_not_called()
47
+
48
+ def test_warmup_failure_does_not_fail_process_setup(self) -> None:
49
+ self.sdk_prewarm.side_effect = ConnectionError("warm-up unavailable")
50
+
51
+ with self.assertLogs("livekit.plugins.spatius", level="WARNING"):
52
+ agent_module.server.setup_fnc(SimpleNamespace())
53
+
54
+ self.sdk_prewarm.assert_awaited_once()
55
+
56
+ def test_process_setup_without_configuration_skips_warmup(self) -> None:
57
+ with patch.dict("os.environ", {}, clear=True):
58
+ agent_module.server.setup_fnc(SimpleNamespace())
59
+
60
+ self.sdk_prewarm.assert_not_awaited()
61
+
62
+
14
63
  class AgentStartupTests(unittest.IsolatedAsyncioTestCase):
15
64
  def setUp(self) -> None:
16
65
  self.events: list[str] = []
@@ -777,7 +777,7 @@ wheels = [
777
777
 
778
778
  [[package]]
779
779
  name = "livekit"
780
- version = "1.1.17"
780
+ version = "1.1.18"
781
781
  source = { registry = "https://pypi.org/simple" }
782
782
  dependencies = [
783
783
  { name = "aiofiles" },
@@ -786,18 +786,18 @@ dependencies = [
786
786
  { name = "protobuf" },
787
787
  { name = "types-protobuf" },
788
788
  ]
789
- sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/e20f97969cf21959061cdc6e1c89e3ad88e23693030398df283c236430fc/livekit-1.1.17.tar.gz", hash = "sha256:0553bfc9041dc8430029597a2278b43e4bb75594a72cb9dcc42bf3ee8e1689a0", size = 378467, upload-time = "2026-09-01T05:03:19.976Z" }
789
+ sdist = { url = "https://files.pythonhosted.org/packages/43/06/ed2cda99ce0c7f10f6599588b296d2bde08ec1235a97416e9986629ce131/livekit-1.1.18.tar.gz", hash = "sha256:1097b8613fa4625e3fb0bd4fe5b09a3b04b39151c08b78498831dfc57235d1d1", size = 386573, upload-time = "2026-09-08T01:50:55.05Z" }
790
790
  wheels = [
791
- { url = "https://files.pythonhosted.org/packages/fd/74/d93c5dd19ff2139c070eacb634e075cd2aff239a9cf692002afaa529a630/livekit-1.1.17-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:39188ac6c805643c8ef952cccb96b2e81e430600e626dea9c6aa1faca7013fdb", size = 10468167, upload-time = "2026-09-01T05:03:06.001Z" },
792
- { url = "https://files.pythonhosted.org/packages/d6/3a/9f4f5e3f0c325d0d5d19c5dc1ae345f4bf1353369db3b42e265a1ee4dbc5/livekit-1.1.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:078d249974aed5e845b60f840802e36dc9d2c9c1dd606341fb95ee018749169e", size = 9289118, upload-time = "2026-09-01T05:03:09.011Z" },
793
- { url = "https://files.pythonhosted.org/packages/1a/d5/89293b55b7a719c7f7b7929d995c009bee7ce9f4d29876c77ee878fd9bf9/livekit-1.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:984c7c0efa3e1f917e973ce412a141ff8244baf98ff24587aa82c206b621054f", size = 10580945, upload-time = "2026-09-01T05:03:11.743Z" },
794
- { url = "https://files.pythonhosted.org/packages/c9/4e/aad025688f8aceeecb4ec97aaae3541d64302e03454c84dc57e02bbaf5b9/livekit-1.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:100f2ddd8e433ea906ecf45f1d37063e43e72c7c2cff0b65ddddc11181bc8dd7", size = 11805780, upload-time = "2026-09-01T05:03:14.614Z" },
795
- { url = "https://files.pythonhosted.org/packages/97/7a/5447238155494f2a2fd55ff0f7705b9769fe67e787b52150a5d269eeaa61/livekit-1.1.17-py3-none-win_amd64.whl", hash = "sha256:7a7b1bdb646775d8042ec97baec6c6b615c5828d6f9b6dd2adaa167841d6f6c2", size = 10968316, upload-time = "2026-09-01T05:03:17.671Z" },
791
+ { url = "https://files.pythonhosted.org/packages/d3/b1/be04d760136480aa2d27f8247adc6d0a1e25395b146f04267fb8e716fe6a/livekit-1.1.18-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:8337e956e9816814572c964e7553d66e4845bc3299cb5aeab8a0d671e293ed32", size = 10471856, upload-time = "2026-09-08T01:50:41.85Z" },
792
+ { url = "https://files.pythonhosted.org/packages/d3/60/a0bf23774eecfc012b60bae84f329537053a0e0952d6f43dc6154d01ac5b/livekit-1.1.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8557d6438da4b8968263b44920e7ab606a9ea34bc49fb8fd89054eb3e5cc162d", size = 9292804, upload-time = "2026-09-08T01:50:44.968Z" },
793
+ { url = "https://files.pythonhosted.org/packages/f1/68/4b5e996660185f2db937a356c9e72981eab42c846f24ba992b997fa9f973/livekit-1.1.18-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e9305aa097fe75f533db3205ab15868730e467130ffdda7d4adc3840f30b282a", size = 10584633, upload-time = "2026-09-08T01:50:47.735Z" },
794
+ { url = "https://files.pythonhosted.org/packages/e0/5e/a0220ddf5e53bb3ee4f4969e9ca5e3a3e9567c584b66cd71355a45c069a8/livekit-1.1.18-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dc2461b668ffa66ca504d05c88af7ea640372f53672ae367145412e374d6aff0", size = 11809468, upload-time = "2026-09-08T01:50:50.332Z" },
795
+ { url = "https://files.pythonhosted.org/packages/f6/47/22455e01c76b271f7d9399546c2becabe4bf0f58a43e79859bb0ca4fc426/livekit-1.1.18-py3-none-win_amd64.whl", hash = "sha256:a61d4fd39392e7d28a7dbbe3b68be7b600115ed32501f5b016579846de945913", size = 10972019, upload-time = "2026-09-08T01:50:52.912Z" },
796
796
  ]
797
797
 
798
798
  [[package]]
799
799
  name = "livekit-agents"
800
- version = "1.8.0"
800
+ version = "1.8.1"
801
801
  source = { registry = "https://pypi.org/simple" }
802
802
  dependencies = [
803
803
  { name = "aiofiles" },
@@ -833,9 +833,9 @@ dependencies = [
833
833
  { name = "typing-extensions" },
834
834
  { name = "watchfiles" },
835
835
  ]
836
- sdist = { url = "https://files.pythonhosted.org/packages/99/88/ace06ebd95b8cc45e25430a2a10ec7ccfb6b386b67907192ad421493858f/livekit_agents-1.8.0.tar.gz", hash = "sha256:132029af152a585770b58623a1a2ac7c8b1b9241b045e8cd220b30415fbcf2ad", size = 2688509, upload-time = "2026-09-05T00:10:03.294Z" }
836
+ sdist = { url = "https://files.pythonhosted.org/packages/5c/f1/cc1fcbc24da6f90cf38809c5ceafff1a1d1c1527a32431ce9b00e64deb95/livekit_agents-1.8.1.tar.gz", hash = "sha256:9319b4a7cf3891db4c1de9edd98fad4918919beadb2ceff5b3fbd34ae67384b5", size = 2712687, upload-time = "2026-09-10T18:59:30.729Z" }
837
837
  wheels = [
838
- { url = "https://files.pythonhosted.org/packages/f4/79/a1dce13ad49ab5b3248727b808ef265b2a535928c53161440d049a0b747d/livekit_agents-1.8.0-py3-none-any.whl", hash = "sha256:0da6c79c71eb7620af14d5c18adba1da796d6743338630813c341e9ce72f45c8", size = 2806075, upload-time = "2026-09-05T00:10:01.23Z" },
838
+ { url = "https://files.pythonhosted.org/packages/0e/2c/29419a913c57b863eba93cbb41df2f3e58b57c3d8f790c6f97be81203ef7/livekit_agents-1.8.1-py3-none-any.whl", hash = "sha256:3e9e4e732ecebcf0512d5eec25621a365c3572289d1f136396d8a1c5ea78e2ac", size = 2832498, upload-time = "2026-09-10T18:59:28.67Z" },
839
839
  ]
840
840
 
841
841
  [package.optional-dependencies]
@@ -935,15 +935,15 @@ wheels = [
935
935
 
936
936
  [[package]]
937
937
  name = "livekit-plugins-spatius"
938
- version = "1.8.0"
938
+ version = "1.8.1"
939
939
  source = { registry = "https://pypi.org/simple" }
940
940
  dependencies = [
941
941
  { name = "livekit-agents" },
942
942
  { name = "spatius", extra = ["opus"] },
943
943
  ]
944
- sdist = { url = "https://files.pythonhosted.org/packages/c0/40/ce00a90cdfd3fd0840ef452e8706e88ae5da9ab2c2cc900a615bfae7abcf/livekit_plugins_spatius-1.8.0.tar.gz", hash = "sha256:76d80c79db4673da4c1ce84f51fbec986a0bd7b888226d8fced68992c7d798e8", size = 8601, upload-time = "2026-09-05T00:12:14.914Z" }
944
+ sdist = { url = "https://files.pythonhosted.org/packages/3f/a7/96d690fa9177fb29ae162fdd3957eee2c87ff3c0b9c4e85956532f94a779/livekit_plugins_spatius-1.8.1.tar.gz", hash = "sha256:d71b74eea79a58861c5893d67d4196d4a3deb2836c23cf4316759aae368b4b9d", size = 10093, upload-time = "2026-09-10T19:01:10.334Z" }
945
945
  wheels = [
946
- { url = "https://files.pythonhosted.org/packages/4d/34/bd44feacb043d16e18396bb7bb9ccb8f100348efce5090d853f8a6bfff1d/livekit_plugins_spatius-1.8.0-py3-none-any.whl", hash = "sha256:c852c7c1ec26b094cc207fa127de1f65e305c6def43d626ee65a250339a1bb69", size = 8712, upload-time = "2026-09-05T00:12:13.838Z" },
946
+ { url = "https://files.pythonhosted.org/packages/f7/93/b562e8e3ff22b19f69575302b41649bcde379497b73b2fc003f76553a8c4/livekit_plugins_spatius-1.8.1-py3-none-any.whl", hash = "sha256:234d2d0817e43a0aa736c2bfc698b364f6ad64c2ba55c469bab35169099f928e", size = 11330, upload-time = "2026-09-10T19:01:09.016Z" },
947
947
  ]
948
948
 
949
949
  [[package]]
@@ -1891,7 +1891,7 @@ dev = [
1891
1891
 
1892
1892
  [package.metadata]
1893
1893
  requires-dist = [
1894
- { name = "livekit-agents", extras = ["spatius"], specifier = "~=1.8.0" },
1894
+ { name = "livekit-agents", extras = ["spatius"], specifier = "~=1.8.1" },
1895
1895
  { name = "livekit-plugins-noise-cancellation", specifier = "~=0.3.0" },
1896
1896
  { name = "python-dotenv", specifier = ">=1.2.1,<2" },
1897
1897
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15,<1" },
@@ -8,6 +8,7 @@ playwright-report/
8
8
 
9
9
  # Cloudflare
10
10
  .wrangler/
11
+ worker-configuration.d.ts
11
12
  .dev.vars
12
13
  .dev.vars.*
13
14
  !.dev.vars.example