@langchain/langgraph-cli 0.0.0-preview.4 → 0.0.0-preview.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,93 @@
1
+ Elastic License 2.0
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject to
14
+ the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set of
20
+ the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other notices
27
+ of the licensor in the software. Any use of the licensor’s trademarks is subject
28
+ to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license for
39
+ the software granted under these terms ends immediately. If your company makes
40
+ such a claim, your patent license ends immediately for work on behalf of your
41
+ company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from you
46
+ also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license no
61
+ later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your licenses
64
+ to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ *As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.*
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is the
76
+ software the licensor makes available under these terms, including any portion
77
+ of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that
84
+ organization. **control** means ownership of substantially all the assets of an
85
+ entity, or the power to direct its management and policies by vote, contract, or
86
+ otherwise. Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your licenses.
92
+
93
+ **trademark** means trademarks, service marks, and similar rights.
package/README.md CHANGED
@@ -1,24 +1,69 @@
1
- # LangGraph.js API
1
+ # LangGraph.js CLI
2
2
 
3
- This package implements the LangGraph API for rapid development and testing. Build and iterate on LangGraph.js agents with a tight feedback loop. The server is backed by a predominently in-memory data store that is persisted to local disk.
4
-
5
- For production use, see the various [deployment options](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for the LangGraph API, which are backed by a production-grade database.
3
+ The official command-line interface for LangGraph.js, providing tools to create, develop, and deploy LangGraph.js applications.
6
4
 
7
5
  ## Installation
8
6
 
9
- Install the `@langchain/langgraph-api` package via your package manager of choice.
7
+ The `@langchain/langgraph-cli` is a CLI binary that can be run via `npx` or installed via your package manager of choice:
8
+
9
+ ```bash
10
+ npx @langchain/langgraph-cli
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `langgraph dev`
16
+
17
+ Run LangGraph.js API server in development mode with hot reloading.
18
+
19
+ ```bash
20
+ langgraph dev
21
+ ```
22
+
23
+ ### `langgraph build`
24
+
25
+ Build a Docker image for your LangGraph.js application.
26
+
27
+ ```bash
28
+ langgraph build
29
+ ```
30
+
31
+ ### `langgraph up`
32
+
33
+ Run LangGraph.js API server in Docker.
10
34
 
11
35
  ```bash
12
- npm install @langchain/langgraph-api
36
+ langgraph up
13
37
  ```
14
38
 
15
- ## Usage
39
+ ### `langgraph dockerfile`
16
40
 
17
- Start the development server:
41
+ Generate a Dockerfile for custom deployments
18
42
 
19
43
  ```bash
20
- npm run langgraph dev
44
+ langgraph dockerfile <save path>
21
45
  ```
22
46
 
23
- Your agent's state (threads, runs, assistants, store) persists in memory while the server is running - perfect for development and testing. Each run's state is tracked and can be inspected, making it easy to debug and improve your agent's behavior.
47
+ ## Configuration
48
+
49
+ The CLI uses a `langgraph.json` configuration file with these key settings:
50
+
51
+ ```json5
52
+ {
53
+ // Required: Graph definitions
54
+ "graphs": {
55
+ "graph": "./src/graph.ts:graph"
56
+ },
57
+
58
+ // Optional: Node version (20 only at the moment)
59
+ "node_version": "20",
60
+
61
+ // Optional: Environment variables
62
+ "env": ".env",
63
+
64
+ // Optional: Additional Dockerfile commands
65
+ "dockerfile_lines": []
66
+ }
67
+ ```
24
68
 
69
+ See the [full documentation](https://langchain-ai.github.io/langgraph/cloud/reference/cli) for detailed configuration options.
@@ -1,7 +1,7 @@
1
1
  import { getDockerCapabilities } from "../docker/compose.mjs";
2
- import { assembleLocalDeps, configToDocker, getBaseImage, } from "../docker/dockerfile.mjs";
2
+ import { assembleLocalDeps, configToDocker, getBaseImage, } from "../docker/docker.mjs";
3
3
  import { getExecaOptions } from "../docker/shell.mjs";
4
- import { ConfigSchema } from "../utils/config.mjs";
4
+ import { getConfig } from "../utils/config.mjs";
5
5
  import { builder } from "./utils/builder.mjs";
6
6
  import { getProjectPath } from "./utils/project.mjs";
7
7
  import { $ } from "execa";
@@ -24,7 +24,7 @@ builder
24
24
  const configPath = await getProjectPath(params.config);
25
25
  await getDockerCapabilities();
26
26
  const projectDir = path.dirname(configPath);
27
- const config = ConfigSchema.parse(JSON.parse(await fs.readFile(configPath, "utf-8")));
27
+ const config = getConfig(await fs.readFile(configPath, "utf-8"));
28
28
  const opts = await getExecaOptions({
29
29
  cwd: projectDir,
30
30
  stderr: "inherit",
package/dist/cli/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { builder } from "./utils/builder.mjs";
3
3
  import "./dev.mjs";
4
- import "./dockerfile.mjs";
4
+ import "./docker.mjs";
5
5
  import "./build.mjs";
6
- // import "./up.mjs";
6
+ import "./up.mjs";
7
7
  builder.parse();
package/dist/cli/dev.mjs CHANGED
@@ -1,18 +1,14 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs/promises";
3
- import { fileURLToPath } from "node:url";
4
- import { spawn } from "node:child_process";
3
+ import { parse, populate } from "dotenv";
4
+ import { watch } from "chokidar";
5
+ import { z } from "zod";
5
6
  import open from "open";
6
- import * as dotenv from "dotenv";
7
- import { logger } from "../logging.mjs";
8
- import { ConfigSchema } from "../utils/config.mjs";
9
7
  import { createIpcServer } from "./utils/ipc/server.mjs";
10
- import { z } from "zod";
11
- import { watch } from "chokidar";
12
- import { builder } from "./utils/builder.mjs";
13
8
  import { getProjectPath } from "./utils/project.mjs";
14
- const tsxTarget = new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api"));
15
- const entrypointTarget = new URL(import.meta.resolve("./dev.entrypoint.mjs"));
9
+ import { getConfig } from "../utils/config.mjs";
10
+ import { builder } from "./utils/builder.mjs";
11
+ import { logger } from "../logging.mjs";
16
12
  builder
17
13
  .command("dev")
18
14
  .description("Run LangGraph API server in development mode with hot reloading.")
@@ -21,7 +17,9 @@ builder
21
17
  .option("--no-browser", "disable auto-opening the browser")
22
18
  .option("-n, --n-jobs-per-worker <number>", "number of workers to run", "10")
23
19
  .option("-c, --config <path>", "path to configuration file", process.cwd())
24
- .action(async (options) => {
20
+ .allowExcessArguments()
21
+ .allowUnknownOption()
22
+ .action(async (options, { args }) => {
25
23
  try {
26
24
  const configPath = await getProjectPath(options.config);
27
25
  const projectCwd = path.dirname(configPath);
@@ -32,32 +30,11 @@ builder
32
30
  });
33
31
  let hasOpenedFlag = false;
34
32
  let child = undefined;
35
- const localUrl = `http://${options.host}:${options.port}`;
36
- const studioUrl = `https://smith.langchain.com/studio?baseUrl=${localUrl}`;
37
- console.log(`
38
- Welcome to
39
-
40
- ╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
41
- ║ ├─┤││││ ┬║ ╦├┬┘├─┤├─┘├─┤
42
- ╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴.js
43
-
44
- - 🚀 API: \x1b[36m${localUrl}\x1b[0m
45
- - 🎨 Studio UI: \x1b[36m${studioUrl}\x1b[0m
46
-
47
- This in-memory server is designed for development and testing.
48
- For production use, please use LangGraph Cloud.
49
-
50
- `);
51
33
  server.on("data", (data) => {
52
- const { host, organizationId } = z
53
- .object({ host: z.string(), organizationId: z.string().nullish() })
54
- .parse(data);
55
- logger.info(`Server running at ${host}`);
34
+ const response = z.object({ queryParams: z.string() }).parse(data);
56
35
  if (options.browser && !hasOpenedFlag) {
57
36
  hasOpenedFlag = true;
58
- open(organizationId
59
- ? `${studioUrl}&organizationId=${organizationId}`
60
- : studioUrl);
37
+ open(`https://smith.langchain.com/studio${response.queryParams}`);
61
38
  }
62
39
  });
63
40
  // check if .gitignore already contains .langgraph-api
@@ -70,7 +47,7 @@ For production use, please use LangGraph Cloud.
70
47
  await fs.appendFile(gitignorePath, "\n# LangGraph API\n.langgraph_api\n");
71
48
  }
72
49
  const prepareContext = async () => {
73
- const config = ConfigSchema.parse(JSON.parse(await fs.readFile(configPath, "utf-8")));
50
+ const config = getConfig(await fs.readFile(configPath, "utf-8"));
74
51
  const newWatch = [configPath];
75
52
  const env = { ...process.env };
76
53
  const configEnv = config?.env;
@@ -79,7 +56,7 @@ For production use, please use LangGraph Cloud.
79
56
  const envPath = path.resolve(projectCwd, configEnv);
80
57
  newWatch.push(envPath);
81
58
  const envData = await fs.readFile(envPath, "utf-8");
82
- dotenv.populate(env, dotenv.parse(envData));
59
+ populate(env, parse(envData));
83
60
  }
84
61
  else if (Array.isArray(configEnv)) {
85
62
  throw new Error("Env storage is not supported by CLI.");
@@ -87,7 +64,7 @@ For production use, please use LangGraph Cloud.
87
64
  else if (typeof configEnv === "object") {
88
65
  if (!process.env)
89
66
  throw new Error("process.env is not defined");
90
- dotenv.populate(env, configEnv);
67
+ populate(env, configEnv);
91
68
  }
92
69
  }
93
70
  const oldWatch = Object.entries(watcher.getWatched()).flatMap(([dir, files]) => files.map((file) => path.resolve(projectCwd, dir, file)));
@@ -96,31 +73,27 @@ For production use, please use LangGraph Cloud.
96
73
  watcher.unwatch(removedTarget).add(addedTarget);
97
74
  return { config, env };
98
75
  };
99
- const launchTsx = async () => {
76
+ const launchServer = async () => {
100
77
  const { config, env } = await prepareContext();
101
78
  if (child != null)
102
79
  child.kill();
103
- child = spawn(process.execPath, [
104
- fileURLToPath(tsxTarget),
105
- "watch",
106
- "--clear-screen=false",
107
- fileURLToPath(entrypointTarget),
108
- pid.toString(),
109
- JSON.stringify({
110
- port: Number.parseInt(options.port, 10),
111
- nWorkers: Number.parseInt(options.nJobsPerWorker, 10),
112
- host: options.host,
113
- graphs: config.graphs,
114
- cwd: projectCwd,
115
- }),
116
- ], { stdio: ["inherit", "inherit", "inherit", "ipc"], env });
80
+ if ("python_version" in config) {
81
+ logger.warn("Launching Python server from @langchain/langgraph-cli is experimental. Please use the `langgraph-cli` package from PyPi instead.");
82
+ const { spawnPythonServer } = await import("./dev.python.mjs");
83
+ child = await spawnPythonServer({ ...options, rest: args }, { configPath, config, env }, { pid, projectCwd });
84
+ }
85
+ else {
86
+ const { spawnNodeServer } = await import("./dev.node.mjs");
87
+ child = await spawnNodeServer({ ...options, rest: args }, { configPath, config, env }, { pid, projectCwd });
88
+ }
117
89
  };
118
90
  watcher.on("all", async (_name, path) => {
119
91
  logger.warn(`Detected changes in ${path}, restarting server`);
120
- launchTsx();
92
+ launchServer();
121
93
  });
122
- // TODO: handle errors
123
- launchTsx();
94
+ // TODO: sometimes the server keeps sending stuff
95
+ // while gracefully exiting
96
+ launchServer();
124
97
  process.on("exit", () => {
125
98
  watcher.close();
126
99
  server.close();
@@ -16,8 +16,9 @@ const isTracingEnabled = () => {
16
16
  process.env?.LANGCHAIN_TRACING;
17
17
  return value === "true";
18
18
  };
19
+ const options = StartServerSchema.parse(JSON.parse(payload));
19
20
  const [{ host, cleanup }, organizationId] = await Promise.all([
20
- startServer(StartServerSchema.parse(JSON.parse(payload))),
21
+ startServer(options),
21
22
  (async () => {
22
23
  if (isTracingEnabled()) {
23
24
  try {
@@ -31,5 +32,9 @@ const [{ host, cleanup }, organizationId] = await Promise.all([
31
32
  return null;
32
33
  })(),
33
34
  ]);
35
+ logger.info(`Server running at ${host}`);
36
+ let queryParams = `?baseUrl=http://${options.host}:${options.port}`;
37
+ if (organizationId)
38
+ queryParams += `&organizationId=${organizationId}`;
34
39
  asyncExitHook(cleanup, { wait: 1000 });
35
- sendToParent?.({ host, organizationId });
40
+ sendToParent?.({ queryParams });
@@ -0,0 +1,35 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { spawn } from "node:child_process";
3
+ import {} from "../utils/config.mjs";
4
+ export async function spawnNodeServer(args, context, options) {
5
+ const localUrl = `http://${args.host}:${args.port}`;
6
+ const studioUrl = `https://smith.langchain.com/studio?baseUrl=${localUrl}`;
7
+ console.log(`
8
+ Welcome to
9
+
10
+ ╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
11
+ ║ ├─┤││││ ┬║ ╦├┬┘├─┤├─┘├─┤
12
+ ╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴.js
13
+
14
+ - 🚀 API: \x1b[36m${localUrl}\x1b[0m
15
+ - 🎨 Studio UI: \x1b[36m${studioUrl}\x1b[0m
16
+
17
+ This in-memory server is designed for development and testing.
18
+ For production use, please use LangGraph Cloud.
19
+
20
+ `);
21
+ return spawn(process.execPath, [
22
+ fileURLToPath(new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api"))),
23
+ "watch",
24
+ "--clear-screen=false",
25
+ fileURLToPath(new URL(import.meta.resolve("./dev.node.entrypoint.mjs"))),
26
+ options.pid.toString(),
27
+ JSON.stringify({
28
+ port: Number.parseInt(args.port, 10),
29
+ nWorkers: Number.parseInt(args.nJobsPerWorker, 10),
30
+ host: args.host,
31
+ graphs: context.config.graphs,
32
+ cwd: options.projectCwd,
33
+ }),
34
+ ], { stdio: ["inherit", "inherit", "inherit", "ipc"], env: context.env });
35
+ }
@@ -0,0 +1,128 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Readable } from "node:stream";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import { extract as tarExtract } from "tar";
8
+ import zipExtract from "extract-zip";
9
+ import { logger } from "../logging.mjs";
10
+ import { assembleLocalDeps } from "../docker/docker.mjs";
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const UV_VERSION = "0.5.20";
13
+ const UV_BINARY_CACHE = path.join(__dirname, ".uv", UV_VERSION);
14
+ function getPlatformInfo() {
15
+ const platform = os.platform();
16
+ const arch = os.arch();
17
+ let binaryName = "uv";
18
+ let extension = "";
19
+ if (platform === "win32") {
20
+ extension = ".exe";
21
+ }
22
+ return {
23
+ platform,
24
+ arch,
25
+ extension,
26
+ binaryName: binaryName + extension,
27
+ };
28
+ }
29
+ function getDownloadUrl(info) {
30
+ let platformStr;
31
+ switch (info.platform) {
32
+ case "darwin":
33
+ platformStr = "apple-darwin";
34
+ break;
35
+ case "win32":
36
+ platformStr = "pc-windows-msvc";
37
+ break;
38
+ case "linux":
39
+ platformStr = "unknown-linux-gnu";
40
+ break;
41
+ default:
42
+ throw new Error(`Unsupported platform: ${info.platform}`);
43
+ }
44
+ let archStr;
45
+ switch (info.arch) {
46
+ case "x64":
47
+ archStr = "x86_64";
48
+ break;
49
+ case "arm64":
50
+ archStr = "aarch64";
51
+ break;
52
+ default:
53
+ throw new Error(`Unsupported architecture: ${info.arch}`);
54
+ }
55
+ const fileName = `uv-${archStr}-${platformStr}${info.platform === "win32" ? ".zip" : ".tar.gz"}`;
56
+ return `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${fileName}`;
57
+ }
58
+ async function downloadAndExtract(url, destPath, info) {
59
+ const response = await fetch(url);
60
+ if (!response.ok)
61
+ throw new Error(`Failed to download uv: ${response.statusText}`);
62
+ if (!response.body)
63
+ throw new Error("No response body");
64
+ const tempDirPath = await fs.mkdtemp(path.join(os.tmpdir(), "uv-"));
65
+ const tempFilePath = path.join(tempDirPath, path.basename(url));
66
+ try {
67
+ // @ts-expect-error invalid types for response.body
68
+ await fs.writeFile(tempFilePath, Readable.fromWeb(response.body));
69
+ let sourceBinaryPath = tempDirPath;
70
+ if (url.endsWith(".zip")) {
71
+ await zipExtract(tempFilePath, { dir: tempDirPath });
72
+ }
73
+ else {
74
+ await tarExtract({ file: tempFilePath, cwd: tempDirPath });
75
+ sourceBinaryPath = path.resolve(sourceBinaryPath, path.basename(tempFilePath).slice(0, ".tar.gz".length * -1));
76
+ }
77
+ sourceBinaryPath = path.resolve(sourceBinaryPath, info.binaryName);
78
+ // Move binary to cache directory
79
+ const targetBinaryPath = path.join(destPath, info.binaryName);
80
+ await fs.rename(sourceBinaryPath, targetBinaryPath);
81
+ await fs.chmod(targetBinaryPath, 0o755);
82
+ return targetBinaryPath;
83
+ }
84
+ finally {
85
+ await fs.rm(tempDirPath, { recursive: true, force: true });
86
+ }
87
+ }
88
+ export async function getUvBinary() {
89
+ await fs.mkdir(UV_BINARY_CACHE, { recursive: true });
90
+ const info = getPlatformInfo();
91
+ const cachedBinaryPath = path.join(UV_BINARY_CACHE, info.binaryName);
92
+ try {
93
+ await fs.access(cachedBinaryPath);
94
+ return cachedBinaryPath;
95
+ }
96
+ catch {
97
+ // Binary not found in cache, download it
98
+ logger.info(`Downloading uv ${UV_VERSION} for ${info.platform}...`);
99
+ const url = getDownloadUrl(info);
100
+ return await downloadAndExtract(url, UV_BINARY_CACHE, info);
101
+ }
102
+ }
103
+ export async function spawnPythonServer(args, context, options) {
104
+ const deps = await assembleLocalDeps(context.configPath, context.config);
105
+ const requirements = deps.rebuildFiles.filter((i) => i.endsWith(".txt"));
106
+ return spawn(await getUvBinary(), [
107
+ "run",
108
+ "--with",
109
+ "langgraph-cli[inmem]",
110
+ ...requirements?.flatMap((i) => ["--with-requirements", i]),
111
+ "langgraph",
112
+ "dev",
113
+ "--port",
114
+ args.port,
115
+ "--host",
116
+ args.host,
117
+ "--n-jobs-per-worker",
118
+ args.nJobsPerWorker,
119
+ "--config",
120
+ context.configPath,
121
+ ...(args.browser ? [] : ["--no-browser"]),
122
+ ...args.rest,
123
+ ], {
124
+ stdio: ["inherit", "inherit", "inherit"],
125
+ env: context.env,
126
+ cwd: options.projectCwd,
127
+ });
128
+ }
@@ -0,0 +1,107 @@
1
+ import { assembleLocalDeps, configToCompose, configToDocker, } from "../docker/docker.mjs";
2
+ import { createCompose, getDockerCapabilities } from "../docker/compose.mjs";
3
+ import { getConfig } from "../utils/config.mjs";
4
+ import { getProjectPath } from "./utils/project.mjs";
5
+ import { builder } from "./utils/builder.mjs";
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import dedent from "dedent";
9
+ import { logger } from "../logging.mjs";
10
+ const fileExists = async (path) => {
11
+ try {
12
+ await fs.access(path);
13
+ return true;
14
+ }
15
+ catch (e) {
16
+ return false;
17
+ }
18
+ };
19
+ builder
20
+ .command("dockerfile")
21
+ .description("Generate a Dockerfile for the LangGraph API server, with Docker Compose options.")
22
+ .argument("<save-path>", "Path to save the Dockerfile")
23
+ .option("--add-docker-compose", "Add additional files for running the LangGraph API server with docker-compose. These files include a docker-compose.yml, .env file, and a .dockerignore file.")
24
+ .option("-c, --config <path>", "Path to configuration file", process.cwd())
25
+ .action(async (savePath, options) => {
26
+ const configPath = await getProjectPath(options.config);
27
+ const config = getConfig(await fs.readFile(configPath, "utf-8"));
28
+ const localDeps = await assembleLocalDeps(configPath, config);
29
+ const dockerfile = await configToDocker(configPath, config, localDeps);
30
+ if (savePath === "-") {
31
+ process.stdout.write(dockerfile);
32
+ process.stdout.write("\n");
33
+ return;
34
+ }
35
+ const targetPath = path.resolve(process.cwd(), savePath);
36
+ await fs.writeFile(targetPath, dockerfile);
37
+ logger.info(`✅ Created: ${path.basename(targetPath)}`);
38
+ if (options.addDockerCompose) {
39
+ const { apiDef } = await configToCompose(configPath, config, {
40
+ watch: false,
41
+ });
42
+ const capabilities = await getDockerCapabilities();
43
+ const compose = createCompose(capabilities, { apiDef });
44
+ const composePath = path.resolve(path.dirname(targetPath), "docker-compose.yml");
45
+ await fs.writeFile(composePath, compose);
46
+ logger.info("✅ Created: .docker-compose.yml");
47
+ const dockerignorePath = path.resolve(path.dirname(targetPath), ".dockerignore");
48
+ if (!fileExists(dockerignorePath)) {
49
+ await fs.writeFile(dockerignorePath, dedent `
50
+ # Ignore node_modules and other dependency directories
51
+ node_modules
52
+ bower_components
53
+ vendor
54
+
55
+ # Ignore logs and temporary files
56
+ *.log
57
+ *.tmp
58
+ *.swp
59
+
60
+ # Ignore .env files and other environment files
61
+ .env
62
+ .env.*
63
+ *.local
64
+
65
+ # Ignore git-related files
66
+ .git
67
+ .gitignore
68
+
69
+ # Ignore Docker-related files and configs
70
+ .dockerignore
71
+ docker-compose.yml
72
+
73
+ # Ignore build and cache directories
74
+ dist
75
+ build
76
+ .cache
77
+ __pycache__
78
+
79
+ # Ignore IDE and editor configurations
80
+ .vscode
81
+ .idea
82
+ *.sublime-project
83
+ *.sublime-workspace
84
+ .DS_Store # macOS-specific
85
+
86
+ # Ignore test and coverage files
87
+ coverage
88
+ *.coverage
89
+ *.test.js
90
+ *.spec.js
91
+ tests
92
+ `);
93
+ logger.info(`✅ Created: ${path.basename(dockerignorePath)}`);
94
+ }
95
+ const envPath = path.resolve(path.dirname(targetPath), ".env");
96
+ if (!fileExists(envPath)) {
97
+ await fs.writeFile(envPath, dedent `
98
+ # Uncomment the following line to add your LangSmith API key
99
+ # LANGSMITH_API_KEY=your-api-key
100
+ # Or if you have a LangGraph Cloud license key, then uncomment the following line:
101
+ # LANGGRAPH_CLOUD_LICENSE_KEY=your-license-key
102
+ # Add any other environment variables go below...
103
+ `);
104
+ logger.info(`✅ Created: ${path.basename(envPath)}`);
105
+ }
106
+ }
107
+ });
@@ -0,0 +1,126 @@
1
+ import { builder } from "./utils/builder.mjs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { getConfig } from "../utils/config.mjs";
5
+ import { getProjectPath } from "./utils/project.mjs";
6
+ import { logger } from "../logging.mjs";
7
+ import { createCompose, getDockerCapabilities } from "../docker/compose.mjs";
8
+ import { configToCompose, getBaseImage } from "../docker/docker.mjs";
9
+ import { getExecaOptions } from "../docker/shell.mjs";
10
+ import { $ } from "execa";
11
+ import { createHash } from "node:crypto";
12
+ import dedent from "dedent";
13
+ const sha256 = (input) => createHash("sha256").update(input).digest("hex");
14
+ const getProjectName = (configPath) => {
15
+ const cwd = path.dirname(configPath).toLocaleLowerCase();
16
+ return `${path.basename(cwd)}-${sha256(cwd)}`;
17
+ };
18
+ const stream = (proc) => {
19
+ logger.debug(`Running "${proc.spawnargs.join(" ")}"`);
20
+ return proc;
21
+ };
22
+ const waitForHealthcheck = async (port) => {
23
+ const now = Date.now();
24
+ while (Date.now() - now < 10_000) {
25
+ const ok = await fetch(`http://localhost:${port}/ok`).then((res) => res.ok, () => false);
26
+ await new Promise((resolve) => setTimeout(resolve, 100));
27
+ if (ok)
28
+ return true;
29
+ }
30
+ throw new Error("Healthcheck timed out");
31
+ };
32
+ builder
33
+ .command("up")
34
+ .description("Launch LangGraph API server.")
35
+ .option("-c, --config <path>", "Path to configuration file", process.cwd())
36
+ .option("-d, --docker-compose <path>", "Advanced: Path to docker-compose.yml file with additional services to launch")
37
+ .option("-p, --port <port>", "Port to run the server on", "8123")
38
+ .option("--recreate", "Force recreate containers and volumes", false)
39
+ .option("--no-pull", "Running the server with locally-built images. By default LangGraph will pull the latest images from the registry")
40
+ .option("--watch", "Restart on file changes", false)
41
+ .option("--wait", "Wait for services to start before returning. Implies --detach", false)
42
+ .option("--postgres-uri <uri>", "Postgres URI to use for the database. Defaults to launching a local database")
43
+ .action(async (params) => {
44
+ logger.info("Starting LangGraph API server...");
45
+ logger.warn(dedent `
46
+ For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Cloud closed beta.
47
+ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.
48
+ `);
49
+ const configPath = await getProjectPath(params.config);
50
+ const config = getConfig(await fs.readFile(configPath, "utf-8"));
51
+ const cwd = path.dirname(configPath);
52
+ const capabilities = await getDockerCapabilities();
53
+ const fullRestartFiles = [configPath];
54
+ if (typeof config.env === "string") {
55
+ fullRestartFiles.push(path.resolve(cwd, config.env));
56
+ }
57
+ const { apiDef } = await configToCompose(configPath, config, {
58
+ watch: capabilities.watchAvailable,
59
+ });
60
+ const name = getProjectName(configPath);
61
+ const execOpts = await getExecaOptions({
62
+ cwd,
63
+ stdout: "inherit",
64
+ stderr: "inherit",
65
+ });
66
+ const exec = $(execOpts);
67
+ if (!config._INTERNAL_docker_tag && params.pull) {
68
+ // pull the image
69
+ logger.info(`Pulling image ${getBaseImage(config)}...`);
70
+ await stream(exec `docker pull ${getBaseImage(config)}`);
71
+ }
72
+ // remove dangling images
73
+ logger.info(`Pruning dangling images...`);
74
+ await stream(exec `docker image prune -f --filter ${`label=com.docker.compose.project=${name}`}`);
75
+ // remove stale containers
76
+ logger.info(`Pruning stale containers...`);
77
+ await stream(exec `docker container prune -f --filter ${`label=com.docker.compose.project=${name}`}`);
78
+ const input = createCompose(capabilities, {
79
+ port: +params.port,
80
+ postgresUri: params.postgresUri,
81
+ apiDef,
82
+ });
83
+ const args = ["--remove-orphans"];
84
+ if (params.recreate) {
85
+ args.push("--force-recreate", "--renew-anon-volumes");
86
+ try {
87
+ await stream(exec `docker volume rm langgraph-data`);
88
+ }
89
+ catch (e) {
90
+ // ignore
91
+ }
92
+ }
93
+ if (params.watch) {
94
+ if (capabilities.watchAvailable) {
95
+ args.push("--watch");
96
+ }
97
+ else {
98
+ logger.warn("Watch mode is not available. Please upgrade your Docker Engine.");
99
+ }
100
+ }
101
+ else if (params.wait) {
102
+ args.push("--wait");
103
+ }
104
+ else {
105
+ args.push("--abort-on-container-exit");
106
+ }
107
+ logger.info(`Launching docker-compose...`);
108
+ const cmd = capabilities.composeType === "plugin"
109
+ ? ["docker", "compose"]
110
+ : ["docker-compose"];
111
+ cmd.push("--project-directory", cwd, "--project-name", name);
112
+ const userCompose = params.dockerCompose || config.docker_compose_file;
113
+ if (userCompose)
114
+ cmd.push("-f", userCompose);
115
+ cmd.push("-f", "-");
116
+ const up = stream($({ ...execOpts, input }) `${cmd} up ${args}`);
117
+ waitForHealthcheck(+params.port).then(() => {
118
+ logger.info(`
119
+ Ready!
120
+ - API: http://localhost:${params.port}
121
+ - Docs: http://localhost:${params.port}/docs
122
+ - LangGraph Studio: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:${params.port}
123
+ `);
124
+ }, () => void 0);
125
+ await up.catch(() => void 0);
126
+ });
@@ -14,7 +14,7 @@ const StoreConfigSchema = z.object({
14
14
  const BaseConfigSchema = z.object({
15
15
  docker_compose_file: z.string().optional(),
16
16
  dockerfile_lines: z.array(z.string()).default([]),
17
- graphs: z.record(z.string()).default({}),
17
+ graphs: z.record(z.string()),
18
18
  env: z
19
19
  .union([z.array(z.string()), z.record(z.string()), z.string()])
20
20
  .default({}),
@@ -22,7 +22,7 @@ const BaseConfigSchema = z.object({
22
22
  _INTERNAL_docker_tag: z.string().optional(),
23
23
  auth: AuthConfigSchema.optional(),
24
24
  });
25
- export const PythonConfigSchema = BaseConfigSchema.merge(z.object({
25
+ const PythonConfigSchema = BaseConfigSchema.merge(z.object({
26
26
  python_version: z
27
27
  .union([z.literal("3.11"), z.literal("3.12")])
28
28
  .default("3.11"),
@@ -31,5 +31,17 @@ export const PythonConfigSchema = BaseConfigSchema.merge(z.object({
31
31
  .array(z.string())
32
32
  .nonempty("You need to specify at least one dependency"),
33
33
  }));
34
- export const NodeConfigSchema = BaseConfigSchema.merge(z.object({ node_version: z.literal("20") }));
35
- export const ConfigSchema = z.union([NodeConfigSchema, PythonConfigSchema]);
34
+ const NodeConfigSchema = BaseConfigSchema.merge(z.object({ node_version: z.literal("20").default("20") }));
35
+ const ConfigSchema = z.union([NodeConfigSchema, PythonConfigSchema]);
36
+ import * as path from "node:path";
37
+ const PYTHON_EXTENSIONS = [".py", ".pyx", ".pyd", ".pyi"];
38
+ // TODO: implement this in Python CLI
39
+ export const getConfig = (config) => {
40
+ const rawConfig = typeof config === "string" ? JSON.parse(config) : config;
41
+ const { graphs } = BaseConfigSchema.parse(rawConfig);
42
+ const preferPython = Object.values(graphs).every((i) => PYTHON_EXTENSIONS.includes(path.extname(i.split(":")[0])));
43
+ if (preferPython) {
44
+ return z.union([PythonConfigSchema, NodeConfigSchema]).parse(rawConfig);
45
+ }
46
+ return z.union([NodeConfigSchema, PythonConfigSchema]).parse(rawConfig);
47
+ };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-cli",
3
- "version": "0.0.0-preview.4",
3
+ "version": "0.0.0-preview.6",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=18"
7
7
  },
8
+ "license": "Elastic-2.0",
8
9
  "main": "./dist/server.mjs",
9
10
  "bin": {
10
11
  "langgraph": "dist/cli/cli.mjs"
@@ -34,10 +35,12 @@
34
35
  "dotenv": "^16.4.7",
35
36
  "execa": "^9.5.2",
36
37
  "exit-hook": "^4.0.0",
38
+ "extract-zip": "^2.0.1",
37
39
  "hono": "^4.5.4",
38
40
  "langsmith": "^0.2.15",
39
41
  "open": "^10.1.0",
40
42
  "superjson": "^2.2.2",
43
+ "tar": "^7.4.3",
41
44
  "tsx": "^4.19.2",
42
45
  "uuid": "^10.0.0",
43
46
  "winston": "^3.17.0",
@@ -1,35 +0,0 @@
1
- import { assembleLocalDeps, configToCompose, configToDocker, } from "../docker/dockerfile.mjs";
2
- import { createCompose, getDockerCapabilities } from "../docker/compose.mjs";
3
- import { ConfigSchema } from "../utils/config.mjs";
4
- import { getProjectPath } from "./utils/project.mjs";
5
- import { builder } from "./utils/builder.mjs";
6
- import * as fs from "node:fs/promises";
7
- import * as path from "node:path";
8
- builder
9
- .command("dockerfile")
10
- .description("Generate a Dockerfile for the LangGraph API server, with Docker Compose options.")
11
- .argument("<save-path>", "Path to save the Dockerfile")
12
- .option("--add-docker-compose", "Add additional files for running the LangGraph API server with docker-compose. These files include a docker-compose.yml, .env file, and a .dockerignore file.")
13
- .option("-c, --config <path>", "Path to configuration file", process.cwd())
14
- .action(async (savePath, options) => {
15
- const configPath = await getProjectPath(options.config);
16
- const config = ConfigSchema.parse(JSON.parse(await fs.readFile(configPath, "utf-8")));
17
- const localDeps = await assembleLocalDeps(configPath, config);
18
- const dockerfile = await configToDocker(configPath, config, localDeps);
19
- if (savePath === "-") {
20
- console.log(dockerfile);
21
- return;
22
- }
23
- const targetPath = path.resolve(process.cwd(), savePath, "Dockerfile");
24
- await fs.writeFile(targetPath, dockerfile);
25
- if (options.addDockerCompose) {
26
- const { apiDef } = await configToCompose(configPath, config, {
27
- watch: false,
28
- });
29
- const capabilities = await getDockerCapabilities();
30
- const compose = createCompose(capabilities, { apiDef });
31
- const composePath = path.resolve(process.cwd(), savePath, "docker-compose.yml");
32
- await fs.writeFile(composePath, compose);
33
- // TODO: add .dockerignore and .env files
34
- }
35
- });
File without changes