skein-js 0.9.1 → 0.10.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 (3) hide show
  1. package/README.md +26 -6
  2. package/dist/index.js +95 -39
  3. package/package.json +13 -7
package/README.md CHANGED
@@ -17,6 +17,7 @@ one-word change (`langgraph dev` → `skein dev`).
17
17
  - [Commands](#commands)
18
18
  - [`skein dev` flags](#skein-dev-flags)
19
19
  - [Self-hosted, no lock-in](#self-hosted-no-lock-in)
20
+ - [Deploy anywhere](#deploy-anywhere)
20
21
  - [When the managed platform may fit you better](#when-the-managed-platform-may-fit-you-better)
21
22
  - [Learn more](#learn-more)
22
23
  - [License](#license)
@@ -52,12 +53,13 @@ LangGraph Studio — any Agent Protocol client works with only a URL change. See
52
53
 
53
54
  ## Commands
54
55
 
55
- | Command | What it does | LangGraph CLI equivalent | Key flags |
56
- | ------------------ | -------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------- |
57
- | `skein dev` | In-process dev server, hot reload, `.skein/` state, no Docker. | `langgraph dev` | see [`skein dev` flags](#skein-dev-flags) |
58
- | `skein up` | Self-hosted stack via Docker Compose (app + Postgres + Redis). | `langgraph up` | `-p, --port` (8123) · `--host` (0.0.0.0) · `-n, --npmrc <path>` |
59
- | `skein build` | Build a deployable Docker image from the config. | `langgraph build` | `-t, --tag` (defaults to the project dir name) · `-n, --npmrc <path>` |
60
- | `skein dockerfile` | Emit a standalone Dockerfile (stdout by default). | `langgraph dockerfile` | `-o, --output <path>` |
56
+ | Command | What it does | LangGraph CLI equivalent | Key flags |
57
+ | ------------------ | ------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------ |
58
+ | `skein dev` | In-process dev server, hot reload, `.skein/` state, no Docker. | `langgraph dev` | see [`skein dev` flags](#skein-dev-flags) |
59
+ | `skein up` | Self-hosted stack via Docker Compose (app + Postgres + Redis). | `langgraph up` | `-p, --port` (8123) · `--host` (0.0.0.0) · `-n, --npmrc <path>` |
60
+ | `skein build` | Build a deployable Docker image from the config. | `langgraph build` | `-t, --tag` (defaults to the project dir name) · `-n, --npmrc <path>` |
61
+ | `skein dockerfile` | Emit a standalone Dockerfile (stdout by default). | `langgraph dockerfile` | `-o, --output <path>` |
62
+ | `skein start` | Serve a pre-built `.skein/build` artifact (the image's entrypoint). | — | `-p, --port` (8123) · `--host` · `--store` · `--queue` · `--concurrency` |
61
63
 
62
64
  All commands take `-c, --config <path>` (default `langgraph.json`).
63
65
 
@@ -96,6 +98,23 @@ run your LangGraph.js graphs behind the standard Agent Protocol on **your own in
96
98
  license key, no per-deployment fee, and no vendor lock-in. `skein up` brings up a Docker Compose
97
99
  stack (app + your Postgres + your Redis) that you own end to end.
98
100
 
101
+ ## Deploy anywhere
102
+
103
+ `skein build` produces an ordinary Docker image, so your LangGraph.js graphs run **anywhere you can
104
+ run a container** — Google Cloud Run, Railway, Fly.io, Render, AWS App Runner or ECS Fargate,
105
+ Kubernetes, or your own VPS.
106
+
107
+ ```bash
108
+ skein build -t my-agent # → a deployable Docker image
109
+ docker run -p 8123:8123 \
110
+ -e POSTGRES_URI="postgresql://…" \
111
+ -e REDIS_URI="redis://…" my-agent
112
+ ```
113
+
114
+ The image binds `$PORT` when a platform injects one (8123 when nothing does), runs as a non-root
115
+ user, serves a `/ok` health probe, and drains in-flight runs on `SIGTERM`. Step-by-step guides per
116
+ platform, and the knobs that are the same everywhere: [deploy anywhere](../../docs/deploy.md).
117
+
99
118
  ## When the managed platform may fit you better
100
119
 
101
120
  skein-js gives you the code and full ownership, not a support contract. If you're an established
@@ -107,6 +126,7 @@ on top of the open LangGraph runtime.
107
126
 
108
127
  ## Learn more
109
128
 
129
+ - [Deploy anywhere](../../docs/deploy.md) — Cloud Run, Railway, Fly.io, Render, AWS, Kubernetes, VPS
110
130
  - [LangGraph CLI compatibility](../../docs/langgraph-cli-compat.md) — commands + `langgraph.json` fields
111
131
  - [skein-js overview](../../docs/index.md) · [Reuse-first architecture](../../docs/reuse.md) · [Roadmap](../../docs/roadmap.md)
112
132
  - [skein-js root README](../../README.md)
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  readLanggraphDevState
15
15
  } from "@skein-js/express";
16
16
  import { buildRuntime } from "@skein-js/runtime";
17
- import { resolveRunConcurrency } from "@skein-js/server-kit";
17
+ import { resolveRunConcurrency, resolveShutdownGraceMs } from "@skein-js/server-kit";
18
18
 
19
19
  // src/colors.ts
20
20
  var colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
@@ -139,6 +139,8 @@ async function applyProjectEnv(config, configDir) {
139
139
  }
140
140
 
141
141
  // src/serve-env.ts
142
+ var DEFAULT_DEV_PORT = 2024;
143
+ var DEFAULT_CONTAINER_PORT = 8123;
142
144
  function envPort(fallback) {
143
145
  const raw = process.env.PORT;
144
146
  if (raw === void 0 || raw.trim() === "") return fallback;
@@ -157,6 +159,32 @@ function describeBindError(error, port) {
157
159
  return `failed to start server: ${error instanceof Error ? error.message : String(error)}`;
158
160
  }
159
161
 
162
+ // src/shutdown.ts
163
+ import { DEFAULT_SHUTDOWN_GRACE_MS } from "@skein-js/server-kit";
164
+ var FORCE_EXIT_BUFFER_MS = 3e3;
165
+ function forceExitDelayMs(graceMs = DEFAULT_SHUTDOWN_GRACE_MS) {
166
+ return graceMs + FORCE_EXIT_BUFFER_MS;
167
+ }
168
+ var describe = (error) => error instanceof Error ? error.message : String(error);
169
+ function createShutdownHandler(options) {
170
+ const { forceExitMs, close, onShutdownStart, exit = (code) => process.exit(code) } = options;
171
+ let shuttingDown = false;
172
+ return () => {
173
+ if (shuttingDown) return;
174
+ shuttingDown = true;
175
+ try {
176
+ onShutdownStart?.();
177
+ } catch (error) {
178
+ console.error(`skein: pre-shutdown step failed: ${describe(error)}`);
179
+ }
180
+ const forceExit = setTimeout(() => exit(0), forceExitMs);
181
+ forceExit.unref();
182
+ void close().catch((error) => {
183
+ console.error(`skein: shutdown did not complete cleanly: ${describe(error)}`);
184
+ }).finally(() => exit(0));
185
+ };
186
+ }
187
+
160
188
  // src/vite-graph-loader.ts
161
189
  import { createServer as createNetServer } from "net";
162
190
  function findFreePort() {
@@ -211,12 +239,12 @@ async function createViteGraphLoader(root, ignored = []) {
211
239
  // src/dev-command.ts
212
240
  var RELOAD_DEBOUNCE_MS = 120;
213
241
  var AUTOSAVE_MS = 2e3;
214
- var FORCE_EXIT_MS = 5e3;
215
242
  var devLogger = createDevLogger();
216
243
  async function runDev(options) {
217
244
  const configPath = path3.resolve(process.cwd(), options.config);
218
245
  const { config, configDir } = await loadConfig({ configPath });
219
246
  await applyProjectEnv(config, configDir);
247
+ const shutdownGraceMs = resolveShutdownGraceMs();
220
248
  const stateDir = path3.join(configDir, STATE_DIR);
221
249
  const stateFile = devStateFile(configDir);
222
250
  const loader = await createViteGraphLoader(configDir, [`${STATE_DIR}/**`, `**/${STATE_DIR}/**`]);
@@ -268,7 +296,7 @@ async function runDev(options) {
268
296
  cors: runtime.cors,
269
297
  warm: true,
270
298
  logger: devLogger,
271
- worker: { maxConcurrency: runConcurrency }
299
+ worker: { maxConcurrency: runConcurrency, shutdownGraceMs }
272
300
  });
273
301
  await server.listen(port, host);
274
302
  } catch (error) {
@@ -346,18 +374,25 @@ async function runDev(options) {
346
374
  pending = setTimeout(() => void reload(), RELOAD_DEBOUNCE_MS);
347
375
  });
348
376
  }
349
- let shuttingDown = false;
350
- const shutdown = () => {
351
- if (shuttingDown) return;
352
- shuttingDown = true;
353
- if (autosave) clearInterval(autosave);
354
- const forceExit = setTimeout(() => process.exit(0), FORCE_EXIT_MS);
355
- forceExit.unref();
356
- saveState();
357
- void Promise.allSettled([server.close(), loader.close(), runtime.dispose()]).then(
358
- () => process.exit(0)
359
- );
360
- };
377
+ const shutdown = createShutdownHandler({
378
+ forceExitMs: forceExitDelayMs(shutdownGraceMs),
379
+ // Stop autosaving and flush one last snapshot synchronously, before anything can race it.
380
+ onShutdownStart: () => {
381
+ if (autosave) clearInterval(autosave);
382
+ saveState();
383
+ },
384
+ // Strictly ordered, never concurrent. `server.close()` stops the worker, which needs the store
385
+ // and queue still alive to settle in-flight runs terminally — and needs the vite loader alive
386
+ // too, since a draining graph may still `await import(...)` a module it hasn't loaded yet.
387
+ // Tearing either down alongside the drain fails the runs it was meant to save.
388
+ close: async () => {
389
+ try {
390
+ await server.close();
391
+ } finally {
392
+ await Promise.allSettled([loader.close(), runtime.dispose()]);
393
+ }
394
+ }
395
+ });
361
396
  process.on("SIGINT", shutdown);
362
397
  process.on("SIGTERM", shutdown);
363
398
  }
@@ -592,8 +627,9 @@ services:
592
627
  ports:
593
628
  - ${ports}
594
629
  environment:
595
- # The container CMD passes no --port; it binds $PORT (as a hosting platform would inject).
596
- # Set it here so the app listens on the port the mapping above publishes.
630
+ # The container CMD passes no --port; it binds $PORT (as a hosting platform would inject), and
631
+ # falls back to this same port anyway. Set explicitly so the mapping above stays correct even
632
+ # if a host port is published that differs from the port the container listens on.
597
633
  PORT: "${options.containerPort}"
598
634
  POSTGRES_URI: postgresql://postgres:postgres@postgres:5432/skein
599
635
  REDIS_URI: redis://redis:6379
@@ -603,6 +639,11 @@ services:
603
639
  redis:
604
640
  condition: service_healthy
605
641
  restart: unless-stopped
642
+ # Longer than Docker's 10s default so the app can finish its shutdown: it drains in-flight runs
643
+ # for SKEIN_SHUTDOWN_GRACE_MS, then aborts the stragglers so they settle to a terminal status.
644
+ # 10s leaves no headroom the moment that window is raised, and a SIGKILL mid-abort is exactly
645
+ # what leaves a run stuck in a non-terminal state.
646
+ stop_grace_period: 30s
606
647
 
607
648
  postgres:
608
649
  image: pgvector/pgvector:pg16
@@ -688,17 +729,28 @@ function generateDockerfile(options) {
688
729
  // The bundle ships sourcemaps; enable them so stack traces map back to the original TypeScript.
689
730
  `ENV NODE_OPTIONS=--enable-source-maps`,
690
731
  `EXPOSE ${port}`,
691
- // Liveness probe against skein's `/ok` endpoint, targeting the same port the server binds.
732
+ // Liveness probe against skein's `/ok` endpoint, targeting the same port the server binds. PORT
733
+ // is resolved exactly as `envPort` resolves it — integer, in range, blank counts as unset —
734
+ // rather than by truthiness, so a malformed value (`PORT=8080a`, `PORT=0`) has the probe and the
735
+ // server agree on the fallback instead of probing a URL the server never bound.
692
736
  `HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \\`,
693
- ` CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||${port})+'/ok').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`,
737
+ ` CMD node -e "const r=Number(process.env.PORT);const p=Number.isInteger(r)&&r>=0&&r<=65535?r:${port};fetch('http://127.0.0.1:'+p+'/ok').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`,
694
738
  // Run unprivileged. The bundle writes nothing to disk, and /app is world-readable, so no chown.
695
739
  `USER node`,
696
740
  ...dockerfileLines,
697
741
  ``,
698
742
  `# Serve the compiled artifact against Postgres + Redis (POSTGRES_URI / REDIS_URI from the env).`,
699
- `# No --port: the server binds $PORT when the platform injects one (Railway/Fly/Render), else its`,
700
- `# default. Exec form keeps node as PID 1 so SIGTERM reaches skein's graceful-shutdown handler.`,
701
- `CMD ["npx", "skein", "start", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
743
+ `# No --port: the server binds $PORT when the platform injects one (Railway/Fly/Render/Cloud Run),`,
744
+ `# else \`skein start\`'s default \u2014 the port EXPOSEd above, so a bare \`docker run -p ${port}:${port}\``,
745
+ `# works too.`,
746
+ `#`,
747
+ `# Invoked as \`node <entry>\` rather than \`npx skein\` so that **node itself is PID 1**. Under npx,`,
748
+ `# PID 1 is npm, which forwards SIGTERM to a \`sh -c\` child and exits immediately \u2014 the server is`,
749
+ `# killed mid-shutdown and in-flight runs are left stranded in a non-terminal status. Exec form`,
750
+ `# (no shell) keeps the signal path direct, so SIGTERM reaches skein's graceful-shutdown handler.`,
751
+ // Absolute, so a `dockerfile_lines` entry that changes WORKDIR (spliced in just above) can't
752
+ // leave the CMD resolving against the wrong directory.
753
+ `CMD ["node", "/app/node_modules/skein-js/dist/index.js", "start", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
702
754
  ];
703
755
  return `${lines.join("\n")}
704
756
  `;
@@ -709,7 +761,7 @@ function skeinCliVersion() {
709
761
  return createRequire2(import.meta.url)("../package.json").version;
710
762
  }
711
763
  var ARTIFACT_SUBDIR = path5.join(".skein", "build");
712
- var CONTAINER_PORT = 8123;
764
+ var CONTAINER_PORT = DEFAULT_CONTAINER_PORT;
713
765
  var DOCKERFILE_NAME = "Dockerfile";
714
766
  var COMPOSE_NAME = "compose.yaml";
715
767
  var DOCKERIGNORE_NAME = ".dockerignore";
@@ -930,8 +982,7 @@ import { createExpressServer as createExpressServer2 } from "@skein-js/express";
930
982
  import {
931
983
  buildRuntime as buildRuntime3
932
984
  } from "@skein-js/runtime";
933
- import { resolveRunConcurrency as resolveRunConcurrency2 } from "@skein-js/server-kit";
934
- var FORCE_EXIT_MS2 = 5e3;
985
+ import { resolveRunConcurrency as resolveRunConcurrency2, resolveShutdownGraceMs as resolveShutdownGraceMs2 } from "@skein-js/server-kit";
935
986
  var logger = createDevLogger();
936
987
  function readBakedSchemas(configDir) {
937
988
  const schemasFile = path7.join(configDir, "schemas.json");
@@ -944,16 +995,21 @@ function readBakedSchemas(configDir) {
944
995
  }
945
996
  async function runStart(options) {
946
997
  const configPath = path7.resolve(process.cwd(), options.config);
998
+ let handleSignal = () => process.exit(0);
999
+ process.on("SIGINT", () => handleSignal());
1000
+ process.on("SIGTERM", () => handleSignal());
947
1001
  let schemas;
948
1002
  let configDir;
949
1003
  let authPath;
950
1004
  let runConcurrency;
1005
+ let shutdownGraceMs;
951
1006
  try {
952
1007
  const loaded = await loadConfig5({ configPath });
953
1008
  configDir = loaded.configDir;
954
1009
  authPath = loaded.config.auth?.path;
955
1010
  await applyProjectEnv(loaded.config, configDir);
956
1011
  runConcurrency = resolveRunConcurrency2(options.concurrency ?? options.nJobsPerWorker);
1012
+ shutdownGraceMs = resolveShutdownGraceMs2();
957
1013
  schemas = readBakedSchemas(configDir);
958
1014
  } catch (error) {
959
1015
  console.error(`skein: ${error instanceof Error ? error.message : String(error)}`);
@@ -983,7 +1039,7 @@ async function runStart(options) {
983
1039
  cors: runtime.cors,
984
1040
  warm: true,
985
1041
  logger,
986
- worker: { maxConcurrency: runConcurrency }
1042
+ worker: { maxConcurrency: runConcurrency, shutdownGraceMs }
987
1043
  });
988
1044
  await server.listen(port, host);
989
1045
  } catch (error) {
@@ -993,16 +1049,16 @@ async function runStart(options) {
993
1049
  return;
994
1050
  }
995
1051
  printBanner({ host, port, graphIds: runtime.deps.graphs.ids, authPath, runConcurrency }, logger);
996
- let shuttingDown = false;
997
- const shutdown = () => {
998
- if (shuttingDown) return;
999
- shuttingDown = true;
1000
- const forceExit = setTimeout(() => process.exit(0), FORCE_EXIT_MS2);
1001
- forceExit.unref();
1002
- void Promise.allSettled([server.close(), runtime.dispose()]).then(() => process.exit(0));
1003
- };
1004
- process.on("SIGINT", shutdown);
1005
- process.on("SIGTERM", shutdown);
1052
+ handleSignal = createShutdownHandler({
1053
+ forceExitMs: forceExitDelayMs(shutdownGraceMs),
1054
+ close: async () => {
1055
+ try {
1056
+ await server.close();
1057
+ } finally {
1058
+ await runtime.dispose();
1059
+ }
1060
+ }
1061
+ });
1006
1062
  }
1007
1063
 
1008
1064
  // src/index.ts
@@ -1035,7 +1091,7 @@ var parseQueue = parseChoice(["memory", "redis"]);
1035
1091
  var program = new Command().name("skein").description(
1036
1092
  "Agent Protocol server for LangGraph.js \u2014 a drop-in replacement for the LangGraph CLI."
1037
1093
  ).version(version, "-v, --version");
1038
- program.command("dev").description("Run the in-process dev server with hot reload (no Docker).").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to bind", parsePort, 2024).option("--host <host>", "Host to bind", "127.0.0.1").option("--no-reload", "Disable hot reload").option("--no-persist", "Don't persist dev state to .skein/ between restarts").option("--store <driver>", "Store driver: memory | postgres", parseStore, "memory").option("--queue <driver>", "Queue driver: memory | redis", parseQueue, "memory").option(
1094
+ program.command("dev").description("Run the in-process dev server with hot reload (no Docker).").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to bind", parsePort, DEFAULT_DEV_PORT).option("--host <host>", "Host to bind", "127.0.0.1").option("--no-reload", "Disable hot reload").option("--no-persist", "Don't persist dev state to .skein/ between restarts").option("--store <driver>", "Store driver: memory | postgres", parseStore, "memory").option("--queue <driver>", "Queue driver: memory | redis", parseQueue, "memory").option(
1039
1095
  "--concurrency <count>",
1040
1096
  "Queued runs the background worker executes at once (default 10; env SKEIN_RUN_CONCURRENCY)",
1041
1097
  parseConcurrency
@@ -1050,7 +1106,7 @@ program.command("dev").description("Run the in-process dev server with hot reloa
1050
1106
  hostExplicit: command.getOptionValueSource("host") === "cli"
1051
1107
  })
1052
1108
  );
1053
- program.command("start").description("Serve a pre-built .skein/build artifact (the production image entrypoint).").option("-c, --config <path>", "Path to the artifact's langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to bind", parsePort, 2024).option("--host <host>", "Host to bind", "127.0.0.1").option("--store <driver>", "Store driver: memory | postgres", parseStore, "memory").option("--queue <driver>", "Queue driver: memory | redis", parseQueue, "memory").option(
1109
+ program.command("start").description("Serve a pre-built .skein/build artifact (the production image entrypoint).").option("-c, --config <path>", "Path to the artifact's langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to bind", parsePort, DEFAULT_CONTAINER_PORT).option("--host <host>", "Host to bind", "127.0.0.1").option("--store <driver>", "Store driver: memory | postgres", parseStore, "memory").option("--queue <driver>", "Queue driver: memory | redis", parseQueue, "memory").option(
1054
1110
  "--concurrency <count>",
1055
1111
  "Queued runs the background worker executes at once (default 10; env SKEIN_RUN_CONCURRENCY)",
1056
1112
  parseConcurrency
@@ -1065,7 +1121,7 @@ program.command("start").description("Serve a pre-built .skein/build artifact (t
1065
1121
  hostExplicit: command.getOptionValueSource("host") === "cli"
1066
1122
  })
1067
1123
  );
1068
- program.command("up").description("Bring up the production stack (Docker Compose: app + Postgres + Redis).").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to expose", parsePort, 8123).option("--host <host>", "Host to bind", "0.0.0.0").option(
1124
+ program.command("up").description("Bring up the production stack (Docker Compose: app + Postgres + Redis).").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to expose", parsePort, DEFAULT_CONTAINER_PORT).option("--host <host>", "Host to bind", "0.0.0.0").option(
1069
1125
  "-n, --npmrc <path>",
1070
1126
  "Path to an .npmrc for authenticating private-registry installs (wired in as a build secret)"
1071
1127
  ).action((options) => runUp(options));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skein-js",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "The skein-js CLI — a drop-in replacement for the LangGraph CLI (dev/up/build/dockerfile).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Maina Wycliffe <wmmaina7@gmail.com>",
@@ -16,10 +16,16 @@
16
16
  "keywords": [
17
17
  "typescript",
18
18
  "langgraph",
19
+ "langgraph-js",
19
20
  "cli",
20
21
  "agent-protocol",
21
22
  "langgraph-cli",
22
- "self-hosted"
23
+ "self-hosted",
24
+ "ai-agents",
25
+ "deploy",
26
+ "docker",
27
+ "cloud-run",
28
+ "kubernetes"
23
29
  ],
24
30
  "type": "module",
25
31
  "main": "./dist/index.js",
@@ -47,11 +53,11 @@
47
53
  "@commander-js/extra-typings": "~13.1.0",
48
54
  "@langchain/langgraph-checkpoint-postgres": "^1.0.4",
49
55
  "commander": "~13.1.0",
50
- "@skein-js/core": "0.9.1",
51
- "@skein-js/express": "0.9.1",
52
- "@skein-js/runtime": "0.9.1",
53
- "@skein-js/server-kit": "0.9.1",
54
- "@skein-js/config": "0.9.1"
56
+ "@skein-js/core": "0.10.0",
57
+ "@skein-js/config": "0.10.0",
58
+ "@skein-js/express": "0.10.0",
59
+ "@skein-js/server-kit": "0.10.0",
60
+ "@skein-js/runtime": "0.10.0"
55
61
  },
56
62
  "optionalDependencies": {
57
63
  "vite": "^8.1.0"