skein-js 0.2.1 → 0.3.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 (2) hide show
  1. package/dist/index.js +286 -64
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -5,14 +5,136 @@ import { createRequire } from "module";
5
5
  import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
6
6
 
7
7
  // src/dev-command.ts
8
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
9
- import path from "path";
10
- import { loadConfig, parseEnvFile, resolveEnv } from "@skein-js/config";
8
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9
+ import path3 from "path";
10
+ import { loadConfig } from "@skein-js/config";
11
11
  import {
12
- createExpressServer
12
+ createExpressServer,
13
+ describeSnapshot,
14
+ readLanggraphDevState
13
15
  } from "@skein-js/express";
14
16
  import { buildRuntime } from "@skein-js/runtime";
15
17
 
18
+ // src/colors.ts
19
+ var colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
20
+ var paint = (code) => (text) => colorEnabled ? `\x1B[${code}m${text}\x1B[0m` : text;
21
+ var green = paint(32);
22
+ var yellow = paint(33);
23
+ var red = paint(31);
24
+ var cyan = paint(36);
25
+ var bold = paint(1);
26
+ var dim = paint(2);
27
+
28
+ // src/banner.ts
29
+ function printBanner(info, logger) {
30
+ const { host, port, graphIds, authPath, workerCount } = info;
31
+ const base = `http://${host}:${port}`;
32
+ console.log();
33
+ console.log(`${bold(green("skein"))} ${dim("\xB7 Agent Protocol dev server")}`);
34
+ console.log();
35
+ console.log(`${dim("API ")} ${cyan(base)}`);
36
+ console.log(`${dim("Docs")} ${cyan(`${base}/docs`)}`);
37
+ console.log();
38
+ for (const id of graphIds) logger.info(`Registering graph with id '${id}'`);
39
+ if (authPath) logger.info(`Loading auth from ${authPath}`);
40
+ logger.info(`Starting ${workerCount} worker${workerCount === 1 ? "" : "s"}`);
41
+ logger.info(`Server running at ${base}`);
42
+ }
43
+
44
+ // src/dev-logger.ts
45
+ var INDENT = " ";
46
+ function describeError(error) {
47
+ const head = error.stack ?? `${error.name}: ${error.message}`;
48
+ if (error.cause === void 0) return head;
49
+ const cause = error.cause instanceof Error ? describeError(error.cause) : String(error.cause);
50
+ return `${head}
51
+ caused by: ${cause}`;
52
+ }
53
+ function metaBlock(meta) {
54
+ if (meta === void 0 || meta === null) return "";
55
+ if (meta instanceof Error) {
56
+ const trace = describeError(meta);
57
+ return `
58
+ ${INDENT}${dim(trace.replace(/\n/g, `
59
+ ${INDENT}`))}`;
60
+ }
61
+ if (typeof meta === "object") {
62
+ const pairs = Object.entries(meta).map(([key, value]) => {
63
+ const rendered = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
64
+ return `${dim(`${key}=`)}${rendered}`;
65
+ });
66
+ return pairs.length ? `
67
+ ${INDENT}${pairs.join(" ")}` : "";
68
+ }
69
+ return `
70
+ ${INDENT}${dim(String(meta))}`;
71
+ }
72
+ function paintHttp(message) {
73
+ if (message.startsWith("<-- ")) return dim(message);
74
+ if (message.startsWith("--> ")) {
75
+ const status = Number(message.match(/ (\d{3}) \d+ms$/)?.[1]);
76
+ if (status >= 500) return red(message);
77
+ if (status >= 400) return yellow(message);
78
+ if (status >= 300) return cyan(message);
79
+ return green(message);
80
+ }
81
+ return message;
82
+ }
83
+ function line(prefix, message, meta) {
84
+ return `${prefix} ${paintHttp(message)}${metaBlock(meta)}`;
85
+ }
86
+ function createDevLogger() {
87
+ return {
88
+ debug: (message, meta) => console.debug(line(dim("debug:"), message, meta)),
89
+ info: (message, meta) => console.log(line(green("info:"), message, meta)),
90
+ warn: (message, meta) => console.warn(line(yellow("warn:"), message, meta)),
91
+ error: (message, meta) => console.error(line(red("error:"), message, meta))
92
+ };
93
+ }
94
+
95
+ // src/dev-state.ts
96
+ import { mkdirSync, renameSync, writeFileSync } from "fs";
97
+ import path from "path";
98
+ var STATE_DIR = ".skein";
99
+ var STATE_FILE = "dev-state.json";
100
+ var LANGGRAPH_DIR = ".langgraph_api";
101
+ function devStateFile(configDir) {
102
+ return path.join(configDir, STATE_DIR, STATE_FILE);
103
+ }
104
+ function writeDevStateFile(stateFile, serialized) {
105
+ mkdirSync(path.dirname(stateFile), { recursive: true });
106
+ const tmp = `${stateFile}.tmp`;
107
+ writeFileSync(tmp, serialized);
108
+ renameSync(tmp, stateFile);
109
+ }
110
+
111
+ // src/project-env.ts
112
+ import { existsSync, readFileSync } from "fs";
113
+ import path2 from "path";
114
+ import { parseEnvFile, resolveEnv } from "@skein-js/config";
115
+ function applyEnv(resolved) {
116
+ for (const [key, value] of Object.entries(resolved)) {
117
+ if (process.env[key] === void 0) process.env[key] = value;
118
+ }
119
+ }
120
+ function readConventionalDotEnv(dir) {
121
+ const envPath = path2.join(dir, ".env");
122
+ if (!existsSync(envPath)) return {};
123
+ try {
124
+ return parseEnvFile(readFileSync(envPath, "utf8"));
125
+ } catch {
126
+ return {};
127
+ }
128
+ }
129
+ async function applyProjectEnv(config, configDir) {
130
+ const declaredEnvPath = typeof config.env === "string" ? path2.resolve(configDir, config.env) : void 0;
131
+ const conventional = declaredEnvPath === path2.join(configDir, ".env") ? {} : readConventionalDotEnv(configDir);
132
+ applyEnv({ ...conventional, ...await resolveEnv(config, configDir) });
133
+ if (declaredEnvPath !== void 0 && !existsSync(declaredEnvPath)) {
134
+ console.warn(`skein: env file "${config.env}" not found; continuing without it.`);
135
+ }
136
+ }
137
+
16
138
  // src/vite-graph-loader.ts
17
139
  import { createServer as createNetServer } from "net";
18
140
  import { createServer, createServerModuleRunner } from "vite";
@@ -60,39 +182,23 @@ async function createViteGraphLoader(root, ignored = []) {
60
182
  var RELOAD_DEBOUNCE_MS = 120;
61
183
  var AUTOSAVE_MS = 2e3;
62
184
  var FORCE_EXIT_MS = 5e3;
63
- var STATE_DIR = ".skein";
64
- var STATE_FILE = "dev-state.json";
65
- var devLogger = {
66
- debug: (message) => console.debug(message),
67
- info: (message) => console.log(message),
68
- warn: (message, meta) => meta === void 0 ? console.warn(message) : console.warn(message, meta),
69
- error: (message, meta) => meta === void 0 ? console.error(message) : console.error(message, meta)
70
- };
71
- function applyEnv(resolved) {
72
- for (const [key, value] of Object.entries(resolved)) {
73
- if (process.env[key] === void 0) process.env[key] = value;
74
- }
185
+ var devLogger = createDevLogger();
186
+ function envPort(fallback) {
187
+ const raw = process.env.PORT;
188
+ if (raw === void 0 || raw.trim() === "") return fallback;
189
+ const port = Number(raw);
190
+ return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : fallback;
75
191
  }
76
- function readConventionalDotEnv(dir) {
77
- const envPath = path.join(dir, ".env");
78
- if (!existsSync(envPath)) return {};
79
- try {
80
- return parseEnvFile(readFileSync(envPath, "utf8"));
81
- } catch {
82
- return {};
83
- }
192
+ function envHost(fallback) {
193
+ const host = process.env.HOST;
194
+ return host !== void 0 && host.trim() !== "" ? host : fallback;
84
195
  }
85
196
  async function runDev(options) {
86
- const configPath = path.resolve(process.cwd(), options.config);
197
+ const configPath = path3.resolve(process.cwd(), options.config);
87
198
  const { config, configDir } = await loadConfig({ configPath });
88
- const declaredEnvPath = typeof config.env === "string" ? path.resolve(configDir, config.env) : void 0;
89
- const conventional = declaredEnvPath === path.join(configDir, ".env") ? {} : readConventionalDotEnv(configDir);
90
- applyEnv({ ...conventional, ...await resolveEnv(config, configDir) });
91
- if (declaredEnvPath !== void 0 && !existsSync(declaredEnvPath)) {
92
- console.warn(`skein: env file "${config.env}" not found; continuing without it.`);
93
- }
94
- const stateDir = path.join(configDir, STATE_DIR);
95
- const stateFile = path.join(stateDir, STATE_FILE);
199
+ await applyProjectEnv(config, configDir);
200
+ const stateDir = path3.join(configDir, STATE_DIR);
201
+ const stateFile = devStateFile(configDir);
96
202
  const loader = await createViteGraphLoader(configDir, [`${STATE_DIR}/**`, `**/${STATE_DIR}/**`]);
97
203
  const runtime = await buildRuntime({
98
204
  configPath,
@@ -100,22 +206,38 @@ async function runDev(options) {
100
206
  store: options.store,
101
207
  queue: options.queue
102
208
  });
103
- const { port, host } = options;
209
+ const port = options.portExplicit ? options.port : envPort(options.port);
210
+ const host = options.hostExplicit ? options.host : envHost(options.host);
104
211
  const canPersist = options.persist && runtime.snapshotState !== void 0;
105
212
  if (options.persist && runtime.snapshotState === void 0) {
106
213
  console.log(
107
214
  `skein: state persists in ${options.store}/${options.queue}; skipping .skein snapshot.`
108
215
  );
109
216
  }
110
- if (canPersist && existsSync(stateFile)) {
217
+ if (canPersist && existsSync2(stateFile)) {
111
218
  try {
112
- runtime.hydrateState?.(JSON.parse(readFileSync(stateFile, "utf8")));
219
+ runtime.hydrateState?.(JSON.parse(readFileSync2(stateFile, "utf8")));
113
220
  console.log("skein: restored dev state.");
114
221
  } catch (error) {
115
222
  console.warn(
116
223
  `skein: could not restore dev state: ${error instanceof Error ? error.message : String(error)}`
117
224
  );
118
225
  }
226
+ } else if (canPersist && existsSync2(path3.join(configDir, LANGGRAPH_DIR))) {
227
+ try {
228
+ const imported = await readLanggraphDevState(path3.join(configDir, LANGGRAPH_DIR));
229
+ if (imported) {
230
+ runtime.hydrateState?.(imported);
231
+ const counts = describeSnapshot(imported);
232
+ console.log(
233
+ `skein: imported dev state from ${LANGGRAPH_DIR}/ (${counts.threads} thread(s), ${counts.checkpointedThreads} with history).`
234
+ );
235
+ }
236
+ } catch (error) {
237
+ console.warn(
238
+ `skein: could not import ${LANGGRAPH_DIR}/: ${error instanceof Error ? error.message : String(error)}`
239
+ );
240
+ }
119
241
  }
120
242
  if (options.verbose) runtime.deps.logRunActivity = true;
121
243
  let server;
@@ -136,17 +258,23 @@ async function runDev(options) {
136
258
  process.exitCode = 1;
137
259
  return;
138
260
  }
139
- console.log(`skein-js listening on http://${host}:${port}`);
261
+ printBanner(
262
+ {
263
+ host,
264
+ port,
265
+ graphIds: runtime.deps.graphs.ids,
266
+ authPath: config.auth?.path,
267
+ workerCount: 1
268
+ },
269
+ devLogger
270
+ );
140
271
  let lastSaved;
141
272
  const saveState = () => {
142
273
  if (!canPersist || runtime.snapshotState === void 0) return;
143
274
  try {
144
275
  const serialized = JSON.stringify(runtime.snapshotState());
145
276
  if (serialized === lastSaved) return;
146
- mkdirSync(path.dirname(stateFile), { recursive: true });
147
- const tmp = `${stateFile}.tmp`;
148
- writeFileSync(tmp, serialized);
149
- renameSync(tmp, stateFile);
277
+ writeDevStateFile(stateFile, serialized);
150
278
  lastSaved = serialized;
151
279
  } catch (error) {
152
280
  console.warn(
@@ -194,7 +322,7 @@ async function runDev(options) {
194
322
  }
195
323
  };
196
324
  loader.watcher.on("change", (file) => {
197
- if (`${path.resolve(file)}${path.sep}`.startsWith(`${stateDir}${path.sep}`)) return;
325
+ if (`${path3.resolve(file)}${path3.sep}`.startsWith(`${stateDir}${path3.sep}`)) return;
198
326
  if (pending) clearTimeout(pending);
199
327
  pending = setTimeout(() => void reload(), RELOAD_DEBOUNCE_MS);
200
328
  });
@@ -217,8 +345,8 @@ async function runDev(options) {
217
345
 
218
346
  // src/docker/commands.ts
219
347
  import { spawn, spawnSync } from "child_process";
220
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
221
- import path3 from "path";
348
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
349
+ import path5 from "path";
222
350
  import { loadConfig as loadConfig2 } from "@skein-js/config";
223
351
 
224
352
  // src/docker/compose.ts
@@ -233,9 +361,15 @@ function generateCompose(options) {
233
361
  services:
234
362
  app:
235
363
  build: .
364
+ # Reap zombies + forward signals with an init process (PID 1), so graph-spawned children don't
365
+ # accumulate; the app itself still handles SIGTERM for graceful shutdown.
366
+ init: true
236
367
  ports:
237
368
  - ${ports}
238
369
  environment:
370
+ # The container CMD passes no --port; it binds $PORT (as a hosting platform would inject).
371
+ # Set it here so the app listens on the port the mapping above publishes.
372
+ PORT: "${options.containerPort}"
239
373
  DATABASE_URL: postgresql://postgres:postgres@postgres:5432/skein
240
374
  REDIS_URL: redis://redis:6379
241
375
  depends_on:
@@ -275,15 +409,15 @@ volumes:
275
409
  }
276
410
 
277
411
  // src/docker/dockerfile.ts
278
- import { existsSync as existsSync2 } from "fs";
279
- import path2 from "path";
412
+ import { existsSync as existsSync3 } from "fs";
413
+ import path4 from "path";
280
414
  function baseImage(nodeVersion) {
281
415
  const major = nodeVersion?.trim().match(/^\d+/)?.[0] ?? "20";
282
416
  return `node:${major}-slim`;
283
417
  }
284
418
  function detectPackageManager(projectDir) {
285
- if (existsSync2(path2.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
286
- if (existsSync2(path2.join(projectDir, "yarn.lock"))) return "yarn";
419
+ if (existsSync3(path4.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
420
+ if (existsSync3(path4.join(projectDir, "yarn.lock"))) return "yarn";
287
421
  return "npm";
288
422
  }
289
423
  function installSteps(manager) {
@@ -291,18 +425,18 @@ function installSteps(manager) {
291
425
  case "pnpm":
292
426
  return {
293
427
  copyLock: "COPY package.json pnpm-lock.yaml* ./",
294
- install: "RUN corepack enable && pnpm install --frozen-lockfile"
428
+ install: "RUN --mount=type=cache,target=/root/.local/share/pnpm/store corepack enable && pnpm install --frozen-lockfile"
295
429
  };
296
430
  case "yarn":
297
431
  return {
298
432
  copyLock: "COPY package.json yarn.lock* ./",
299
- install: "RUN corepack enable && yarn install --frozen-lockfile"
433
+ install: "RUN --mount=type=cache,target=/usr/local/share/.cache/yarn corepack enable && yarn install --frozen-lockfile"
300
434
  };
301
435
  case "npm":
302
436
  return {
303
437
  copyLock: "COPY package.json package-lock.json* ./",
304
438
  // `npm ci` needs a lockfile; fall back to `npm install` when there isn't one.
305
- install: "RUN npm ci || npm install"
439
+ install: "RUN --mount=type=cache,target=/root/.npm npm ci || npm install"
306
440
  };
307
441
  }
308
442
  }
@@ -325,23 +459,37 @@ function generateDockerfile(options) {
325
459
  const { nodeVersion, dockerfileLines = [], port, packageManager } = options;
326
460
  const { copyLock, install } = installSteps(packageManager);
327
461
  const lines = [
462
+ // The syntax directive must be the first line to be honored — it enables the RUN cache mounts.
463
+ `# syntax=docker/dockerfile:1`,
328
464
  `# Generated by skein \u2014 do not edit by hand (regenerated by \`skein build\` / \`skein up\`).`,
329
465
  `FROM ${baseImage(nodeVersion)}`,
466
+ // Hand /app to the unprivileged 'node' user (uid 1000, shipped by the base image) so the server
467
+ // never runs as root. corepack/npm need root to install, so install as root, then chown the
468
+ // whole tree — including the root-created node_modules, where vite writes its cache at runtime.
330
469
  `WORKDIR /app`,
331
470
  ``,
332
- `# Install dependencies first for better layer caching.`,
471
+ `# Install dependencies first for better layer caching (BuildKit cache mount speeds rebuilds).`,
333
472
  copyLock,
334
473
  install,
335
474
  ``,
336
475
  `# Copy the rest of the project (graphs, langgraph.json, source).`,
337
476
  `COPY . .`,
338
477
  ``,
478
+ `# Give the runtime user ownership of the app + installed node_modules so vite can write its`,
479
+ `# on-disk cache under node_modules/.vite without EACCES.`,
480
+ `RUN chown -R node:node /app`,
481
+ ``,
482
+ // Set NODE_ENV only after install so devDependencies (the vite/tsx toolchain `skein dev` loads
483
+ // graphs with) are installed; setting it before install would prune them and break graph loading.
339
484
  `ENV NODE_ENV=production`,
340
485
  `EXPOSE ${port}`,
486
+ `USER node`,
341
487
  ...dockerfileLines,
342
488
  ``,
343
489
  `# Serve the Agent Protocol against Postgres + Redis (DATABASE_URL / REDIS_URL from the environment).`,
344
- `CMD ["npx", "skein", "dev", "--no-reload", "--no-persist", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0", "--port", "${port}"]`
490
+ `# No --port: the server binds $PORT when the platform injects one (Railway/Fly/Render), else its`,
491
+ `# default. Exec form keeps node as PID 1 so SIGTERM reaches skein's graceful-shutdown handler.`,
492
+ `CMD ["npx", "skein", "dev", "--no-reload", "--no-persist", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
345
493
  ];
346
494
  return `${lines.join("\n")}
347
495
  `;
@@ -353,8 +501,11 @@ var DOCKERFILE_NAME = "Dockerfile";
353
501
  var COMPOSE_NAME = "compose.yaml";
354
502
  var DOCKERIGNORE_NAME = ".dockerignore";
355
503
  var GENERATED_MARKER = "# Generated by skein";
504
+ function isSkeinGenerated(contents) {
505
+ return contents.split("\n", 3).some((line2) => line2.startsWith(GENERATED_MARKER));
506
+ }
356
507
  async function loadConfigContext(configOption) {
357
- const configPath = path3.resolve(process.cwd(), configOption);
508
+ const configPath = path5.resolve(process.cwd(), configOption);
358
509
  const { config, configDir } = await loadConfig2({ configPath });
359
510
  return { config, configDir };
360
511
  }
@@ -367,7 +518,7 @@ function renderDockerfile({ config, configDir }, port) {
367
518
  });
368
519
  }
369
520
  function defaultImageTag(configDir) {
370
- const base = path3.basename(configDir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
521
+ const base = path5.basename(configDir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
371
522
  const trimmed = base.replace(/^[-.]+|[-.]+$/g, "");
372
523
  return trimmed.length > 0 ? trimmed : "skein-app";
373
524
  }
@@ -401,8 +552,8 @@ function describeExit(result) {
401
552
  return result.signal !== null ? `killed by ${result.signal}` : `exit ${result.code}`;
402
553
  }
403
554
  function writeGeneratedFile(file, contents) {
404
- const name = path3.basename(file);
405
- if (existsSync3(file) && !readFileSync2(file, "utf8").startsWith(GENERATED_MARKER)) {
555
+ const name = path5.basename(file);
556
+ if (existsSync4(file) && !isSkeinGenerated(readFileSync3(file, "utf8"))) {
406
557
  console.log(`skein: ${name} exists and wasn't generated by skein \u2014 using it as-is.`);
407
558
  return "kept-user";
408
559
  }
@@ -414,7 +565,7 @@ async function runDockerfile(options) {
414
565
  const context = await loadConfigContext(options.config);
415
566
  const contents = renderDockerfile(context, CONTAINER_PORT);
416
567
  if (options.output !== void 0) {
417
- const target = path3.resolve(process.cwd(), options.output);
568
+ const target = path5.resolve(process.cwd(), options.output);
418
569
  writeFileSync2(target, contents);
419
570
  console.log(`skein: wrote ${target}.`);
420
571
  return;
@@ -424,9 +575,9 @@ async function runDockerfile(options) {
424
575
  async function runBuild(options) {
425
576
  const context = await loadConfigContext(options.config);
426
577
  if (!requireDocker()) return;
427
- const dockerfilePath = path3.join(context.configDir, DOCKERFILE_NAME);
578
+ const dockerfilePath = path5.join(context.configDir, DOCKERFILE_NAME);
428
579
  writeGeneratedFile(dockerfilePath, renderDockerfile(context, CONTAINER_PORT));
429
- writeGeneratedFile(path3.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
580
+ writeGeneratedFile(path5.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
430
581
  const tag = options.tag ?? defaultImageTag(context.configDir);
431
582
  console.log(`skein: building image "${tag}"\u2026`);
432
583
  const result = await runToCompletion(
@@ -445,12 +596,12 @@ async function runUp(options) {
445
596
  const context = await loadConfigContext(options.config);
446
597
  if (!requireDocker()) return;
447
598
  writeGeneratedFile(
448
- path3.join(context.configDir, DOCKERFILE_NAME),
599
+ path5.join(context.configDir, DOCKERFILE_NAME),
449
600
  renderDockerfile(context, CONTAINER_PORT)
450
601
  );
451
- writeGeneratedFile(path3.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
602
+ writeGeneratedFile(path5.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
452
603
  writeGeneratedFile(
453
- path3.join(context.configDir, COMPOSE_NAME),
604
+ path5.join(context.configDir, COMPOSE_NAME),
454
605
  generateCompose({ hostPort: options.port, host: options.host, containerPort: CONTAINER_PORT })
455
606
  );
456
607
  console.log(
@@ -467,6 +618,65 @@ async function runUp(options) {
467
618
  }
468
619
  }
469
620
 
621
+ // src/import-command.ts
622
+ import { existsSync as existsSync5 } from "fs";
623
+ import path6 from "path";
624
+ import { loadConfig as loadConfig3 } from "@skein-js/config";
625
+ import {
626
+ describeSnapshot as describeSnapshot2,
627
+ loadSnapshotIntoStore,
628
+ readLanggraphDevState as readLanggraphDevState2
629
+ } from "@skein-js/express";
630
+ import { buildRuntime as buildRuntime2 } from "@skein-js/runtime";
631
+ function summarize(counts) {
632
+ return `${counts.assistants} assistant(s), ${counts.threads} thread(s), ${counts.runs} run(s), ${counts.items} store item(s), checkpoint history for ${counts.checkpointedThreads} thread(s)`;
633
+ }
634
+ async function runImportLanggraph(options) {
635
+ const configPath = path6.resolve(process.cwd(), options.config);
636
+ const { config, configDir } = await loadConfig3({ configPath });
637
+ const sourceDir = options.from ? path6.resolve(process.cwd(), options.from) : path6.join(configDir, LANGGRAPH_DIR);
638
+ const snapshot = await readLanggraphDevState2(sourceDir);
639
+ if (snapshot === null) {
640
+ console.log(`skein: no LangGraph dev state found at ${sourceDir}; nothing to import.`);
641
+ return;
642
+ }
643
+ const counts = describeSnapshot2(snapshot);
644
+ if (options.store === "memory") {
645
+ const stateFile = devStateFile(configDir);
646
+ if (existsSync5(stateFile) && !options.force) {
647
+ console.error(
648
+ `skein: ${path6.relative(process.cwd(), stateFile)} already exists. Re-run with --force to overwrite it (or delete it first).`
649
+ );
650
+ process.exitCode = 1;
651
+ return;
652
+ }
653
+ writeDevStateFile(stateFile, JSON.stringify(snapshot));
654
+ console.log(
655
+ `skein: imported ${summarize(counts)} \u2192 ${path6.relative(process.cwd(), stateFile)}.`
656
+ );
657
+ console.log("skein: run `skein dev` to use it.");
658
+ return;
659
+ }
660
+ await applyProjectEnv(config, configDir);
661
+ const loader = await createViteGraphLoader(configDir);
662
+ try {
663
+ const runtime = await buildRuntime2({
664
+ configPath,
665
+ importModule: loader.importModule,
666
+ store: "postgres",
667
+ queue: "memory"
668
+ });
669
+ try {
670
+ await loadSnapshotIntoStore(snapshot, runtime.deps.store, runtime.deps.checkpointer);
671
+ } finally {
672
+ await runtime.dispose();
673
+ }
674
+ } finally {
675
+ await loader.close();
676
+ }
677
+ console.log(`skein: imported ${summarize(counts)} into Postgres.`);
678
+ }
679
+
470
680
  // src/index.ts
471
681
  var require2 = createRequire(import.meta.url);
472
682
  var { version } = require2("../package.json");
@@ -490,10 +700,22 @@ var parseQueue = parseChoice(["memory", "redis"]);
490
700
  var program = new Command().name("skein").description(
491
701
  "Agent Protocol server for LangGraph.js \u2014 a drop-in replacement for the LangGraph CLI."
492
702
  ).version(version, "-v, --version");
493
- 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("-v, --verbose", "Log per-run activity: start/finish, tool calls, and interrupts").action((options) => runDev(options));
703
+ 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("-v, --verbose", "Log per-run activity: start/finish, tool calls, and interrupts").action(
704
+ (options, command) => runDev({
705
+ ...options,
706
+ portExplicit: command.getOptionValueSource("port") === "cli",
707
+ hostExplicit: command.getOptionValueSource("host") === "cli"
708
+ })
709
+ );
494
710
  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").action((options) => runUp(options));
495
711
  program.command("build").description("Build a deployable Docker image from the config.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-t, --tag <tag>", "Image tag (defaults to the project directory name)").action((options) => runBuild(options));
496
712
  program.command("dockerfile").description("Emit a standalone Dockerfile from the config.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-o, --output <path>", "Write the Dockerfile here instead of stdout").action((options) => runDockerfile(options));
713
+ program.command("import-langgraph").description("Import an existing LangGraph in-memory dev state (.langgraph_api/) into skein.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option(
714
+ "--store <driver>",
715
+ "Import target: memory (.skein/dev-state.json) | postgres (DATABASE_URL)",
716
+ parseStore,
717
+ "memory"
718
+ ).option("--from <dir>", "Source .langgraph_api directory (defaults to alongside langgraph.json)").option("--force", "Overwrite an existing .skein/dev-state.json (memory target)", false).action((options) => runImportLanggraph(options));
497
719
  try {
498
720
  await program.parseAsync(process.argv);
499
721
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skein-js",
3
- "version": "0.2.1",
3
+ "version": "0.3.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>",
@@ -48,10 +48,10 @@
48
48
  "@langchain/langgraph-checkpoint-postgres": "^1.0.4",
49
49
  "commander": "~13.1.0",
50
50
  "vite": "^8.1.0",
51
- "@skein-js/config": "0.2.1",
52
- "@skein-js/core": "0.2.1",
53
- "@skein-js/express": "0.2.1",
54
- "@skein-js/runtime": "0.2.1"
51
+ "@skein-js/core": "0.3.0",
52
+ "@skein-js/config": "0.3.0",
53
+ "@skein-js/express": "0.3.0",
54
+ "@skein-js/runtime": "0.3.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@langchain/langgraph": "^1.4.0"