@langchain/langgraph-cli 0.0.23 → 0.0.24
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/dist/cli/cloudflare.mjs +172 -0
- package/dist/cli/dev.mjs +17 -2
- package/dist/cli/utils/stream.mjs +90 -0
- package/package.json +2 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import { Readable, Transform, Writable } from "node:stream";
|
|
5
|
+
import { ChildProcess, spawn } from "node:child_process";
|
|
6
|
+
import { extract as tarExtract } from "tar";
|
|
7
|
+
import * as nodeStream from "node:stream/web";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { logger } from "../utils/logging.mjs";
|
|
10
|
+
import { BytesLineDecoder } from "./utils/stream.mjs";
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const CLOUDFLARED_VERSION = "2025.2.1";
|
|
13
|
+
const CLOUDFLARED_CACHE_DIR = path.join(__dirname, ".cloudflare", CLOUDFLARED_VERSION);
|
|
14
|
+
const writeFile = async (path, stream) => {
|
|
15
|
+
if (stream == null)
|
|
16
|
+
throw new Error("Stream is null");
|
|
17
|
+
return await fs.writeFile(path, Readable.fromWeb(stream));
|
|
18
|
+
};
|
|
19
|
+
class CloudflareLoggerStream extends WritableStream {
|
|
20
|
+
constructor() {
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
super({
|
|
23
|
+
write(chunk) {
|
|
24
|
+
const text = decoder.decode(chunk);
|
|
25
|
+
const [_timestamp, level, ...rest] = text.split(" ");
|
|
26
|
+
const message = rest.join(" ");
|
|
27
|
+
if (level === "INF") {
|
|
28
|
+
logger.debug(message);
|
|
29
|
+
}
|
|
30
|
+
else if (level === "ERR") {
|
|
31
|
+
logger.error(message);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
logger.info(message);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
fromWeb() {
|
|
40
|
+
return Writable.fromWeb(this);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class CloudflareUrlStream extends TransformStream {
|
|
44
|
+
constructor() {
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
super({
|
|
47
|
+
transform(chunk, controller) {
|
|
48
|
+
const str = decoder.decode(chunk);
|
|
49
|
+
const urlMatch = str.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
|
|
50
|
+
if (urlMatch)
|
|
51
|
+
controller.enqueue(urlMatch);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
fromWeb() {
|
|
56
|
+
// @ts-expect-error
|
|
57
|
+
return Transform.fromWeb(this, { objectMode: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function startCloudflareTunnel(port) {
|
|
61
|
+
const targetBinaryPath = await ensureCloudflared();
|
|
62
|
+
logger.info("Starting tunnel");
|
|
63
|
+
const child = spawn(targetBinaryPath, ["tunnel", "--url", `http://localhost:${port}`], { stdio: ["inherit", "pipe", "pipe"] });
|
|
64
|
+
child.stdout
|
|
65
|
+
.pipe(new BytesLineDecoder().fromWeb())
|
|
66
|
+
.pipe(new CloudflareLoggerStream().fromWeb());
|
|
67
|
+
child.stderr
|
|
68
|
+
.pipe(new BytesLineDecoder().fromWeb())
|
|
69
|
+
.pipe(new CloudflareLoggerStream().fromWeb());
|
|
70
|
+
const tunnelUrl = new Promise((resolve) => {
|
|
71
|
+
child.stderr
|
|
72
|
+
.pipe(new CloudflareUrlStream().fromWeb())
|
|
73
|
+
.once("data", (data) => {
|
|
74
|
+
logger.info(`Tunnel URL: "${data}"`);
|
|
75
|
+
resolve(data);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return { child, tunnelUrl };
|
|
79
|
+
}
|
|
80
|
+
function getFiles() {
|
|
81
|
+
const platform = getPlatform();
|
|
82
|
+
const arch = getArchitecture();
|
|
83
|
+
if (platform === "windows") {
|
|
84
|
+
if (arch !== "386" && arch !== "amd64") {
|
|
85
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
86
|
+
}
|
|
87
|
+
return { binary: `cloudflared-${platform}-${arch}.exe` };
|
|
88
|
+
}
|
|
89
|
+
if (platform === "darwin") {
|
|
90
|
+
if (arch !== "arm64" && arch !== "amd64") {
|
|
91
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
archive: `cloudflared-${platform}-${arch}.tgz`,
|
|
95
|
+
binary: "cloudflared",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (platform === "linux") {
|
|
99
|
+
if (arch !== "arm64" && arch !== "amd64" && arch !== "386") {
|
|
100
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
101
|
+
}
|
|
102
|
+
return { binary: `cloudflared-${platform}-${arch}` };
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
105
|
+
}
|
|
106
|
+
async function downloadCloudflared() {
|
|
107
|
+
await fs.mkdir(CLOUDFLARED_CACHE_DIR, { recursive: true });
|
|
108
|
+
logger.info("Requesting download of `cloudflared`");
|
|
109
|
+
const { binary, archive } = getFiles();
|
|
110
|
+
const downloadFile = archive ?? binary;
|
|
111
|
+
const tempDirPath = await fs.mkdtemp(path.join(os.tmpdir(), "cloudflared-"));
|
|
112
|
+
const tempFilePath = path.join(tempDirPath, downloadFile);
|
|
113
|
+
const url = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${downloadFile}`;
|
|
114
|
+
logger.debug("Downloading `${archive}`", { url, target: tempDirPath });
|
|
115
|
+
const response = await fetch(url);
|
|
116
|
+
if (!response.ok || !response.body) {
|
|
117
|
+
throw new Error(`Failed to download cloudflared: ${response.statusText}`);
|
|
118
|
+
}
|
|
119
|
+
await writeFile(tempFilePath, response.body);
|
|
120
|
+
if (archive != null) {
|
|
121
|
+
if (path.extname(archive) !== ".tgz") {
|
|
122
|
+
throw new Error(`Invalid archive type: "${path.extname(archive)}"`);
|
|
123
|
+
}
|
|
124
|
+
logger.debug("Extracting `cloudflared`");
|
|
125
|
+
await tarExtract({ file: tempFilePath, cwd: tempDirPath });
|
|
126
|
+
}
|
|
127
|
+
const sourceBinaryPath = path.resolve(tempDirPath, binary);
|
|
128
|
+
const targetBinaryPath = path.resolve(CLOUDFLARED_CACHE_DIR, binary);
|
|
129
|
+
logger.debug("Moving `cloudflared` to target directory", {
|
|
130
|
+
targetBinaryPath,
|
|
131
|
+
});
|
|
132
|
+
await fs.rename(sourceBinaryPath, targetBinaryPath);
|
|
133
|
+
await fs.chmod(targetBinaryPath, 0o755);
|
|
134
|
+
}
|
|
135
|
+
async function ensureCloudflared() {
|
|
136
|
+
const { binary } = getFiles();
|
|
137
|
+
const targetBinaryPath = path.resolve(CLOUDFLARED_CACHE_DIR, binary);
|
|
138
|
+
try {
|
|
139
|
+
await fs.access(targetBinaryPath);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
await downloadCloudflared();
|
|
143
|
+
}
|
|
144
|
+
return targetBinaryPath;
|
|
145
|
+
}
|
|
146
|
+
function getArchitecture() {
|
|
147
|
+
const arch = os.arch();
|
|
148
|
+
switch (arch) {
|
|
149
|
+
case "x64":
|
|
150
|
+
return "amd64";
|
|
151
|
+
case "arm64":
|
|
152
|
+
return "arm64";
|
|
153
|
+
case "ia32":
|
|
154
|
+
case "x86":
|
|
155
|
+
return "386";
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function getPlatform() {
|
|
161
|
+
const platform = os.platform();
|
|
162
|
+
switch (platform) {
|
|
163
|
+
case "darwin":
|
|
164
|
+
return "darwin";
|
|
165
|
+
case "linux":
|
|
166
|
+
return "linux";
|
|
167
|
+
case "win32":
|
|
168
|
+
return "windows";
|
|
169
|
+
default:
|
|
170
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
package/dist/cli/dev.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { parse, populate } from "dotenv";
|
|
|
4
4
|
import { watch } from "chokidar";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import open from "open";
|
|
7
|
+
import { startCloudflareTunnel } from "./cloudflare.mjs";
|
|
7
8
|
import { createIpcServer } from "./utils/ipc/server.mjs";
|
|
8
9
|
import { getProjectPath } from "./utils/project.mjs";
|
|
9
10
|
import { getConfig } from "../utils/config.mjs";
|
|
@@ -19,6 +20,7 @@ builder
|
|
|
19
20
|
.option("--no-browser", "disable auto-opening the browser")
|
|
20
21
|
.option("-n, --n-jobs-per-worker <number>", "number of workers to run", "10")
|
|
21
22
|
.option("-c, --config <path>", "path to configuration file", process.cwd())
|
|
23
|
+
.option("--tunnel", "use Cloudflare Tunnel to expose the server to the internet")
|
|
22
24
|
.allowExcessArguments()
|
|
23
25
|
.allowUnknownOption()
|
|
24
26
|
.exitOverride((error) => gracefulExit(error.exitCode))
|
|
@@ -27,6 +29,7 @@ builder
|
|
|
27
29
|
port: command.opts().port !== "2024",
|
|
28
30
|
host: command.opts().host !== "localhost",
|
|
29
31
|
n_jobs_per_worker: command.opts().nJobsPerWorker !== "10",
|
|
32
|
+
tunnel: Boolean(command.opts().tunnel),
|
|
30
33
|
})))
|
|
31
34
|
.action(async (options, { args }) => {
|
|
32
35
|
try {
|
|
@@ -39,12 +42,20 @@ builder
|
|
|
39
42
|
});
|
|
40
43
|
let hasOpenedFlag = false;
|
|
41
44
|
let child = undefined;
|
|
45
|
+
let tunnel = undefined;
|
|
42
46
|
let hostUrl = "https://smith.langchain.com";
|
|
43
|
-
server.on("data", (data) => {
|
|
47
|
+
server.on("data", async (data) => {
|
|
44
48
|
const response = z.object({ queryParams: z.string() }).parse(data);
|
|
45
49
|
if (options.browser && !hasOpenedFlag) {
|
|
46
50
|
hasOpenedFlag = true;
|
|
47
|
-
|
|
51
|
+
const queryParams = new URLSearchParams(response.queryParams);
|
|
52
|
+
const tunnelUrl = await tunnel?.tunnelUrl;
|
|
53
|
+
if (tunnelUrl)
|
|
54
|
+
queryParams.set("baseUrl", tunnelUrl);
|
|
55
|
+
let queryParamsStr = queryParams.toString();
|
|
56
|
+
if (queryParamsStr)
|
|
57
|
+
queryParamsStr = `?${queryParams.toString()}`;
|
|
58
|
+
open(`${hostUrl}/studio${queryParamsStr}`);
|
|
48
59
|
}
|
|
49
60
|
});
|
|
50
61
|
// check if .gitignore already contains .langgraph-api
|
|
@@ -97,6 +108,10 @@ builder
|
|
|
97
108
|
const { config, env, hostUrl } = await prepareContext();
|
|
98
109
|
if (child != null)
|
|
99
110
|
child.kill();
|
|
111
|
+
if (tunnel != null)
|
|
112
|
+
tunnel.child.kill();
|
|
113
|
+
if (options.tunnel)
|
|
114
|
+
tunnel = await startCloudflareTunnel(options.port);
|
|
100
115
|
if ("python_version" in config) {
|
|
101
116
|
logger.warn("Launching Python server from @langchain/langgraph-cli is experimental. Please use the `langgraph-cli` package from PyPi instead.");
|
|
102
117
|
const { spawnPythonServer } = await import("./dev.python.mjs");
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
const CR = "\r".charCodeAt(0);
|
|
3
|
+
const LF = "\n".charCodeAt(0);
|
|
4
|
+
const TRAILING_NEWLINE = [CR, LF];
|
|
5
|
+
function joinArrays(data) {
|
|
6
|
+
const totalLength = data.reduce((acc, curr) => acc + curr.length, 0);
|
|
7
|
+
let merged = new Uint8Array(totalLength);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const c of data) {
|
|
10
|
+
merged.set(c, offset);
|
|
11
|
+
offset += c.length;
|
|
12
|
+
}
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
export class BytesLineDecoder extends TransformStream {
|
|
16
|
+
constructor() {
|
|
17
|
+
let buffer = [];
|
|
18
|
+
let trailingCr = false;
|
|
19
|
+
super({
|
|
20
|
+
start() {
|
|
21
|
+
buffer = [];
|
|
22
|
+
trailingCr = false;
|
|
23
|
+
},
|
|
24
|
+
transform(chunk, controller) {
|
|
25
|
+
// See https://docs.python.org/3/glossary.html#term-universal-newlines
|
|
26
|
+
let text = chunk;
|
|
27
|
+
// Handle trailing CR from previous chunk
|
|
28
|
+
if (trailingCr) {
|
|
29
|
+
text = joinArrays([[CR], text]);
|
|
30
|
+
trailingCr = false;
|
|
31
|
+
}
|
|
32
|
+
// Check for trailing CR in current chunk
|
|
33
|
+
if (text.length > 0 && text.at(-1) === CR) {
|
|
34
|
+
trailingCr = true;
|
|
35
|
+
text = text.subarray(0, -1);
|
|
36
|
+
}
|
|
37
|
+
if (!text.length)
|
|
38
|
+
return;
|
|
39
|
+
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1));
|
|
40
|
+
const lastIdx = text.length - 1;
|
|
41
|
+
const { lines } = text.reduce((acc, cur, idx) => {
|
|
42
|
+
if (acc.from > idx)
|
|
43
|
+
return acc;
|
|
44
|
+
if (cur === CR || cur === LF) {
|
|
45
|
+
acc.lines.push(text.subarray(acc.from, idx));
|
|
46
|
+
if (cur === CR && text[idx + 1] === LF) {
|
|
47
|
+
acc.from = idx + 2;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
acc.from = idx + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (idx === lastIdx && acc.from <= lastIdx) {
|
|
54
|
+
acc.lines.push(text.subarray(acc.from));
|
|
55
|
+
}
|
|
56
|
+
return acc;
|
|
57
|
+
}, { lines: [], from: 0 });
|
|
58
|
+
if (lines.length === 1 && !trailingNewline) {
|
|
59
|
+
buffer.push(lines[0]);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (buffer.length) {
|
|
63
|
+
// Include existing buffer in first line
|
|
64
|
+
buffer.push(lines[0]);
|
|
65
|
+
lines[0] = joinArrays(buffer);
|
|
66
|
+
buffer = [];
|
|
67
|
+
}
|
|
68
|
+
if (!trailingNewline) {
|
|
69
|
+
// If the last segment is not newline terminated,
|
|
70
|
+
// buffer it for the next chunk
|
|
71
|
+
if (lines.length)
|
|
72
|
+
buffer = [lines.pop()];
|
|
73
|
+
}
|
|
74
|
+
// Enqueue complete lines
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
controller.enqueue(line);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
flush(controller) {
|
|
80
|
+
if (buffer.length) {
|
|
81
|
+
controller.enqueue(joinArrays(buffer));
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
fromWeb() {
|
|
87
|
+
// @ts-expect-error invalid types for response.body
|
|
88
|
+
return Transform.fromWeb(this);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langchain/langgraph-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^18.19.0 || >=20.16.0"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"yaml": "^2.7.0",
|
|
37
37
|
"zod": "^3.23.8",
|
|
38
38
|
"@babel/code-frame": "^7.26.2",
|
|
39
|
-
"@langchain/langgraph-api": "0.0.
|
|
39
|
+
"@langchain/langgraph-api": "0.0.24"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"tsx": "^4.19.3",
|