@valerypopoff/rivet2-cli 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ function isInputRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ export function parseJsonInputRecord(text, source) {
5
+ const trimmedText = text.trim();
6
+ if (!trimmedText) {
7
+ return {};
8
+ }
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(trimmedText);
12
+ }
13
+ catch (error) {
14
+ throw new Error(`${source} must be valid JSON.`, { cause: error });
15
+ }
16
+ if (!isInputRecord(parsed)) {
17
+ throw new Error(`${source} must be a JSON object.`);
18
+ }
19
+ return parsed;
20
+ }
21
+ export function parseKeyValueInputRecord(values, source) {
22
+ return Object.fromEntries(values.map((item) => {
23
+ const separatorIndex = item.indexOf('=');
24
+ if (separatorIndex <= 0) {
25
+ throw new Error(`Invalid ${source} value "${item}". Expected key=value.`);
26
+ }
27
+ const key = item.slice(0, separatorIndex).trim();
28
+ if (!key) {
29
+ throw new Error(`Invalid ${source} value "${item}". Expected a non-empty key.`);
30
+ }
31
+ return [key, item.slice(separatorIndex + 1)];
32
+ }));
33
+ }
@@ -1,5 +1,6 @@
1
1
  import { createProcessor, loadProjectFromFile } from '@valerypopoff/rivet2-node';
2
2
  import { resolve } from 'node:path';
3
+ import { parseJsonInputRecord, parseKeyValueInputRecord } from '../commandInputs.js';
3
4
  export function makeCommand(y) {
4
5
  return y
5
6
  .positional('projectFile', {
@@ -40,46 +41,24 @@ export async function run(args) {
40
41
  const project = await loadProjectFromFile(projectPath);
41
42
  if (!args.graphName && !project.metadata.mainGraphId) {
42
43
  const validGraphs = Object.values(project.graphs).map((graph) => [graph.metadata.id, graph.metadata.name]);
43
- const validGraphNames = validGraphs.map(([id, name]) => `• "${name}" (${id})`);
44
- console.error(`No graph name provided, and project does not specify a main graph. Valid graphs are: \n${validGraphNames.join('\n')}\n\n Use either the graph's name or its ID. For example, \`rivet run my-project.rivet-project my-graph\` or \`rivet run my-project.rivet-project 1234abcd\``);
44
+ const validGraphNames = validGraphs.map(([id, name]) => `- "${name}" (${id})`);
45
+ console.error(`No graph name provided, and project does not specify a main graph. Valid graphs are: \n${validGraphNames.join('\n')}\n\nUse either the graph's name or its ID. For example, \`rivet run my-project.rivet-project my-graph\` or \`rivet run my-project.rivet-project 1234abcd\``);
45
46
  process.exit(1);
46
47
  }
47
- let inputs = {};
48
+ let inputs;
48
49
  if (args.inputsStdin) {
49
- // Read json from stdin
50
50
  const stdin = process.stdin;
51
51
  stdin.setEncoding('utf8');
52
- let input = '';
52
+ let inputText = '';
53
53
  for await (const chunk of stdin) {
54
- input += chunk;
55
- }
56
- try {
57
- inputs = JSON.parse(input);
58
- }
59
- catch (err) {
60
- console.error('Failed to parse input JSON');
61
- console.error(err);
62
- process.exit(1);
54
+ inputText += chunk;
63
55
  }
56
+ inputs = parseJsonInputRecord(inputText, 'Input stdin');
64
57
  }
65
58
  else {
66
- inputs = Object.fromEntries(args.input.map((input) => {
67
- const [key, value] = input.split('=');
68
- if (!key || !value) {
69
- console.error(`Invalid input value: ${input}`);
70
- process.exit(1);
71
- }
72
- return [key, value];
73
- }));
59
+ inputs = parseKeyValueInputRecord(args.input, 'input');
74
60
  }
75
- const contextValues = Object.fromEntries(args.context.map((context) => {
76
- const [key, value] = context.split('=');
77
- if (!key || !value) {
78
- console.error(`Invalid context value: ${context}`);
79
- process.exit(1);
80
- }
81
- return [key, value];
82
- }));
61
+ const contextValues = parseKeyValueInputRecord(args.context, 'context');
83
62
  const { run } = createProcessor(project, {
84
63
  graph: args.graphName,
85
64
  inputs,
@@ -1,11 +1,12 @@
1
- import { Hono } from 'hono';
2
1
  import { serve as serveHono } from '@hono/node-server';
3
- import { readdir, stat } from 'node:fs/promises';
4
- import { extname, isAbsolute, join } from 'node:path';
5
- import didYouMean from 'didyoumean2';
6
- import { createProcessor, loadProjectFromFile, getSingleNodeStream, } from '@valerypopoff/rivet2-node';
2
+ import { createProcessor, getSingleNodeStream, loadProjectFromFile } from '@valerypopoff/rivet2-node';
7
3
  import chalk from 'chalk';
4
+ import didYouMean from 'didyoumean2';
8
5
  import { configDotenv } from 'dotenv';
6
+ import { Hono } from 'hono';
7
+ import { readdir, stat } from 'node:fs/promises';
8
+ import { basename, dirname, extname, join, resolve } from 'node:path';
9
+ import { parseJsonInputRecord } from '../commandInputs.js';
9
10
  export function makeCommand(y) {
10
11
  return y
11
12
  .option('port', {
@@ -76,20 +77,21 @@ export async function serve(args) {
76
77
  console.log('Streaming is enabled');
77
78
  }
78
79
  if (args.streamNode != null) {
80
+ if (args.stream == null) {
81
+ throw new Error('--stream-node requires --stream.');
82
+ }
79
83
  console.log(`Streaming node ${chalk.bold(args.streamNode)}`);
80
84
  }
81
85
  app.post('/', async (c) => {
82
86
  const project = args.dev ? await loadProjectFromFile(projectFilePath) : initialProject;
83
- const text = await c.req.text();
84
- let inputs = {};
85
- if (text.trim()) {
86
- inputs = JSON.parse(text);
87
- if (typeof inputs !== 'object') {
88
- throw new Error('Inputs must be an object');
89
- }
90
- }
87
+ const inputs = parseJsonInputRecord(await c.req.text(), 'Request body');
88
+ const graphRunArgs = buildGraphRunArgs(args, project, inputs, args.graph);
91
89
  if (args.stream != null) {
92
- const stream = await streamGraph({ project, inputs, graphId: args.graph, ...args });
90
+ const stream = await streamGraph({
91
+ ...graphRunArgs,
92
+ stream: args.stream,
93
+ streamNode: args.streamNode,
94
+ });
93
95
  return new Response(stream, {
94
96
  headers: {
95
97
  'Content-Type': 'text/event-stream',
@@ -98,34 +100,23 @@ export async function serve(args) {
98
100
  },
99
101
  });
100
102
  }
101
- else {
102
- const outputs = await runGraph({ project, inputs, graphId: args.graph, ...args });
103
- return c.json(outputs);
104
- }
103
+ return c.json(await runGraph(graphRunArgs));
105
104
  });
106
105
  if (args.allowSpecifyingGraphId) {
107
106
  app.post('/:graphId', async (c) => {
108
107
  const project = args.dev ? await loadProjectFromFile(projectFilePath) : initialProject;
109
- const inputs = await c.req.json();
110
- const graphId = c.req.param('graphId');
111
- const outputs = await runGraph({ project, inputs, graphId, ...args });
112
- return c.json(outputs);
108
+ const graph = c.req.param('graphId');
109
+ throwIfInvalidGraph(project, graph);
110
+ const inputs = parseJsonInputRecord(await c.req.text(), 'Request body');
111
+ return c.json(await runGraph(buildGraphRunArgs(args, project, inputs, graph)));
113
112
  });
114
113
  }
115
114
  const server = serveHono({
116
115
  port: args.port,
117
116
  fetch: app.fetch,
118
117
  });
119
- let servedGraphName;
120
- if (args.graph) {
121
- const graph = Object.values(initialProject.graphs).find((g) => g.metadata.id === args.graph || g.metadata.name === args.graph);
122
- servedGraphName = graph.metadata.name;
123
- }
124
- else {
125
- const graph = Object.values(initialProject.graphs).find((g) => g.metadata.id === initialProject.metadata.mainGraphId);
126
- servedGraphName = graph.metadata.name;
127
- }
128
- console.log(`${chalk.green(`Serving project file ${chalk.bold.white(projectFilePath)} on port ${chalk.bold.white(args.port)}.\nServing graph "${chalk.bold.white(servedGraphName)}".`)}`);
118
+ const servedGraphName = resolveServedGraphName(initialProject, args.graph);
119
+ console.log(chalk.green(`Serving project file ${chalk.bold.white(projectFilePath)} on port ${chalk.bold.white(args.port)}.\nServing graph "${chalk.bold.white(servedGraphName)}".`));
129
120
  function shutdown() {
130
121
  console.log('Shutting down...');
131
122
  server.close((err) => {
@@ -144,37 +135,52 @@ export async function serve(args) {
144
135
  process.exit(1);
145
136
  }
146
137
  }
147
- async function streamGraph({ project, inputs, graphId, openaiApiKey, openaiEndpoint, openaiOrganization, streamNode, }) {
138
+ function buildGraphRunArgs(args, project, inputs, graph) {
139
+ return {
140
+ exposeCost: args.exposeCost,
141
+ graph,
142
+ inputs,
143
+ openaiApiKey: args.openaiApiKey,
144
+ openaiEndpoint: args.openaiEndpoint,
145
+ openaiOrganization: args.openaiOrganization,
146
+ project,
147
+ };
148
+ }
149
+ async function streamGraph({ project, inputs, graph, openaiApiKey, openaiEndpoint, openaiOrganization, stream, streamNode, }) {
148
150
  const { run, processor, getSSEStream } = createProcessor(project, {
149
151
  inputs,
150
- graph: graphId,
152
+ graph,
151
153
  openAiKey: openaiApiKey,
152
154
  openAiEndpoint: openaiEndpoint,
153
155
  openAiOrganization: openaiOrganization,
154
156
  });
155
- if (streamNode) {
156
- const stream = getSingleNodeStream(processor, streamNode);
157
- run().catch((err) => {
158
- console.error(err);
159
- });
160
- return stream;
161
- }
162
- else {
163
- const stream = getSSEStream({
164
- nodeStart: streamNode ? [streamNode] : true,
165
- nodeFinish: streamNode ? [streamNode] : true,
166
- partialOutputs: streamNode ? [streamNode] : true,
167
- });
168
- run().catch((err) => {
169
- console.error(err);
170
- });
171
- return stream;
157
+ const responseStream = streamNode
158
+ ? getSingleNodeStream(processor, streamNode)
159
+ : getSSEStream(buildStreamEventFilter(stream));
160
+ run().catch((err) => {
161
+ console.error(err);
162
+ });
163
+ return responseStream;
164
+ }
165
+ export function buildStreamEventFilter(stream) {
166
+ const streamTarget = stream?.trim();
167
+ if (!streamTarget) {
168
+ return {
169
+ nodeStart: true,
170
+ nodeFinish: true,
171
+ partialOutputs: true,
172
+ };
172
173
  }
174
+ return {
175
+ nodeStart: [streamTarget],
176
+ nodeFinish: [streamTarget],
177
+ partialOutputs: [streamTarget],
178
+ };
173
179
  }
174
- async function runGraph({ project, inputs, graphId, openaiApiKey, openaiEndpoint, openaiOrganization, exposeCost, }) {
180
+ async function runGraph({ project, inputs, graph, openaiApiKey, openaiEndpoint, openaiOrganization, exposeCost, }) {
175
181
  const { run } = createProcessor(project, {
176
182
  inputs,
177
- graph: graphId,
183
+ graph,
178
184
  openAiKey: openaiApiKey,
179
185
  openAiEndpoint: openaiEndpoint,
180
186
  openAiOrganization: openaiOrganization,
@@ -185,42 +191,62 @@ async function runGraph({ project, inputs, graphId, openaiApiKey, openaiEndpoint
185
191
  }
186
192
  return outputs;
187
193
  }
194
+ function getGraphSummaries(project) {
195
+ return Object.values(project.graphs).map((graph) => ({
196
+ id: graph.metadata.id,
197
+ name: graph.metadata.name,
198
+ }));
199
+ }
200
+ function findGraph(project, graphIdOrName) {
201
+ if (!graphIdOrName) {
202
+ return undefined;
203
+ }
204
+ return getGraphSummaries(project).find((graph) => graph.id === graphIdOrName || graph.name === graphIdOrName);
205
+ }
206
+ function formatGraphList(project) {
207
+ return getGraphSummaries(project)
208
+ .map((graph) => `- "${graph.name}" (${graph.id})`)
209
+ .join('\n');
210
+ }
211
+ function resolveServedGraphName(project, graph) {
212
+ const servedGraph = findGraph(project, graph ?? project.metadata.mainGraphId);
213
+ if (!servedGraph) {
214
+ throw new Error(`Project main graph "${project.metadata.mainGraphId}" was not found in the project file.`);
215
+ }
216
+ return servedGraph.name;
217
+ }
188
218
  function throwIfNoMainGraph(project, graph, projectFilePath) {
189
- if (!project.metadata.mainGraphId && !graph) {
190
- const validGraphs = Object.values(project.graphs).map((graph) => [graph.metadata.id, graph.metadata.name]);
191
- if (validGraphs.length === 0) {
192
- throw new Error('No graphs found in the project file. Please edit the project file in Rivet and add a graph.');
193
- }
194
- const validGraphNames = validGraphs.map(([id, name]) => `• "${name}" (${id})`);
195
- const firstExample = `rivet serve ${projectFilePath} --graph ${validGraphs[0][0]}`;
196
- const secondExample = `rivet serve ${projectFilePath} --graph "${validGraphs[0][1]}"`;
197
- throw new Error(`No graph name provided, and project does not specify a main graph. Valid graphs are: \n\n${validGraphNames.join('\n')}\n\n Use either the graph's name or its ID. For example, \n• \`${chalk.bold(firstExample)}\` or\n• \`${chalk.bold(secondExample)}\``);
219
+ if (project.metadata.mainGraphId || graph) {
220
+ return;
198
221
  }
222
+ const validGraphs = getGraphSummaries(project);
223
+ if (validGraphs.length === 0) {
224
+ throw new Error('No graphs found in the project file. Please edit the project file in Rivet and add a graph.');
225
+ }
226
+ const firstExample = `rivet serve ${projectFilePath} --graph ${validGraphs[0].id}`;
227
+ const secondExample = `rivet serve ${projectFilePath} --graph "${validGraphs[0].name}"`;
228
+ throw new Error(`No graph name provided, and project does not specify a main graph. Valid graphs are: \n\n${formatGraphList(project)}\n\nUse either the graph's name or its ID. For example, \n- \`${chalk.bold(firstExample)}\` or\n- \`${chalk.bold(secondExample)}\``);
199
229
  }
200
230
  function throwIfInvalidGraph(project, graph) {
201
- if (project.metadata.mainGraphId && !graph) {
231
+ if (!graph) {
232
+ const mainGraphId = project.metadata.mainGraphId;
233
+ if (!mainGraphId || findGraph(project, mainGraphId)) {
234
+ return;
235
+ }
236
+ throw new Error(`Project main graph "${mainGraphId}" was not found in the project file.`);
237
+ }
238
+ if (findGraph(project, graph)) {
202
239
  return;
203
240
  }
204
- const matchingGraph = Object.values(project.graphs).find((g) => g.metadata.id === graph || g.metadata.name === graph);
205
- if (!matchingGraph) {
206
- const validGraphsAndIds = Object.values(project.graphs).flatMap((graph) => [
207
- graph.metadata.id,
208
- graph.metadata.name,
209
- ]);
210
- const suggestion = didYouMean(graph, validGraphsAndIds);
211
- if (suggestion) {
212
- throw new Error(`Graph "${graph}" not found in project file. Did you mean \`${chalk.bold(`--graph "${suggestion}"`)}\`?`);
213
- }
214
- else {
215
- const validGraphsAndIds = Object.values(project.graphs)
216
- .map((graph) => `• "${graph.metadata.name}" (${graph.metadata.id})`)
217
- .join('\n');
218
- throw new Error(`Graph "${graph}" not found in project file. Valid graphs are: \n${validGraphsAndIds}`);
219
- }
241
+ const validGraphsAndIds = getGraphSummaries(project).flatMap((graph) => [graph.id, graph.name]);
242
+ const suggestion = didYouMean(graph, validGraphsAndIds);
243
+ if (suggestion) {
244
+ throw new Error(`Graph "${graph}" not found in project file. Did you mean \`${chalk.bold(`--graph "${suggestion}"`)}\`?`);
220
245
  }
246
+ throw new Error(`Graph "${graph}" not found in project file. Valid graphs are: \n${formatGraphList(project)}`);
221
247
  }
222
248
  async function getProjectFile(initialProjectFilePath) {
223
- let projectFilePath = initialProjectFilePath ?? (await getProjectFilePathFromDirectory(process.cwd()));
249
+ let projectFilePath = resolve(process.cwd(), initialProjectFilePath ?? (await getProjectFilePathFromDirectory(process.cwd())));
224
250
  await throwIfMissingFile(projectFilePath);
225
251
  if ((await stat(projectFilePath)).isDirectory()) {
226
252
  projectFilePath = await getProjectFilePathFromDirectory(projectFilePath);
@@ -229,7 +255,7 @@ async function getProjectFile(initialProjectFilePath) {
229
255
  }
230
256
  async function getProjectFilePathFromDirectory(directory) {
231
257
  const files = await readdir(directory);
232
- const projectFiles = files.filter((file) => extname(file) === '.rivet-project');
258
+ const projectFiles = files.filter((file) => extname(file).toLowerCase() === '.rivet-project');
233
259
  if (projectFiles.length === 0) {
234
260
  throw new Error('No project file found in the current directory. Project files should end with .rivet-project.');
235
261
  }
@@ -246,26 +272,17 @@ async function throwIfMissingFile(filePath) {
246
272
  if (err.code !== 'ENOENT') {
247
273
  throw err;
248
274
  }
249
- if (isAbsolute(filePath)) {
250
- const parentDir = filePath.split('/').slice(0, -1).join('/');
251
- const possibleFiles = await readdir(parentDir);
252
- const suggestion = didYouMean(filePath, possibleFiles);
253
- if (suggestion) {
254
- throw new Error(`Could not find project file "${filePath}". Did you mean "${suggestion}"?`);
255
- }
256
- else {
257
- throw new Error(`Could not find project file "${filePath}".`);
258
- }
275
+ let possibleFiles = [];
276
+ try {
277
+ possibleFiles = await readdir(dirname(filePath));
259
278
  }
260
- else {
261
- const possibleFiles = await readdir(process.cwd());
262
- const suggestion = didYouMean(filePath, possibleFiles);
263
- if (suggestion) {
264
- throw new Error(`Could not find project file "${filePath}". Did you mean "${suggestion}"?`);
265
- }
266
- else {
267
- throw new Error(`Could not find project file "${filePath}".`);
268
- }
279
+ catch {
280
+ throw new Error(`Could not find project file "${filePath}".`);
281
+ }
282
+ const suggestion = didYouMean(basename(filePath), possibleFiles);
283
+ if (suggestion) {
284
+ throw new Error(`Could not find project file "${filePath}". Did you mean "${join(dirname(filePath), suggestion)}"?`);
269
285
  }
286
+ throw new Error(`Could not find project file "${filePath}".`);
270
287
  }
271
288
  }
@@ -0,0 +1,3 @@
1
+ import type { LooseDataValue } from '@valerypopoff/rivet2-node';
2
+ export declare function parseJsonInputRecord(text: string, source: string): Record<string, LooseDataValue>;
3
+ export declare function parseKeyValueInputRecord(values: string[], source: string): Record<string, LooseDataValue>;
@@ -8,9 +8,9 @@ export declare function makeCommand<T>(y: yargs.Argv<T>): yargs.Argv<T & {
8
8
  } & {
9
9
  "include-cost": boolean;
10
10
  } & {
11
- context: never[] | string[];
11
+ context: string[] | never[];
12
12
  } & {
13
- input: never[] | string[];
13
+ input: string[] | never[];
14
14
  }>;
15
15
  export declare function run(args: {
16
16
  projectFile: string;
@@ -1,4 +1,18 @@
1
+ import type { RivetEventStreamFilterSpec } from '@valerypopoff/rivet2-node';
1
2
  import type * as yargs from 'yargs';
3
+ type ServeArgs = {
4
+ port: number;
5
+ projectFile: string | undefined;
6
+ dev: boolean;
7
+ graph: string | undefined;
8
+ allowSpecifyingGraphId: boolean;
9
+ openaiApiKey: string | undefined;
10
+ openaiEndpoint: string | undefined;
11
+ openaiOrganization: string | undefined;
12
+ exposeCost: boolean;
13
+ stream: string | undefined;
14
+ streamNode: string | undefined;
15
+ };
2
16
  export declare function makeCommand<T>(y: yargs.Argv<T>): yargs.Argv<T & {
3
17
  port: number;
4
18
  } & {
@@ -22,16 +36,6 @@ export declare function makeCommand<T>(y: yargs.Argv<T>): yargs.Argv<T & {
22
36
  } & {
23
37
  projectFile: string | undefined;
24
38
  }>;
25
- export declare function serve(args: {
26
- port: number;
27
- projectFile: string | undefined;
28
- dev: boolean;
29
- graph: string | undefined;
30
- allowSpecifyingGraphId: boolean;
31
- openaiApiKey: string | undefined;
32
- openaiEndpoint: string | undefined;
33
- openaiOrganization: string | undefined;
34
- exposeCost: boolean;
35
- stream: string | undefined;
36
- streamNode: string | undefined;
37
- }): Promise<void>;
39
+ export declare function serve(args: ServeArgs): Promise<void>;
40
+ export declare function buildStreamEventFilter(stream: string | undefined): RivetEventStreamFilterSpec;
41
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valerypopoff/rivet2-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "license": "MIT",
5
5
  "types": "dist/types/cli.d.ts",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@hono/node-server": "^1.13.8",
26
26
  "@types/dotenv": "^8.2.3",
27
- "@valerypopoff/rivet2-node": "^2.0.0",
27
+ "@valerypopoff/rivet2-node": "^2.0.2",
28
28
  "chalk": "^5.4.1",
29
29
  "didyoumean2": "^7.0.4",
30
30
  "dotenv": "^16.4.7",