@dbx-tools/appkit-graphiti 0.6.113 → 0.6.115

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/src/config.ts CHANGED
@@ -1,26 +1,24 @@
1
1
  /**
2
- * AppKit Graphiti sidecar ports, route prefix, and Python command.
2
+ * AppKit Graphiti sidecar ports and Python command.
3
3
  *
4
4
  * @module
5
5
  */
6
6
  import { ConfigurationError, type BasePluginConfig } from "@databricks/appkit";
7
- import { config as coreConfig } from "@dbx-tools/core";
7
+ import { config as coreConfig, project as coreProject } from "@dbx-tools/core";
8
8
  import type { JSONSchema7 } from "json-schema";
9
9
 
10
10
  export interface GraphitiPluginConfig extends BasePluginConfig {
11
- publicPort?: number;
12
- appPort?: number;
13
11
  graphitiPort?: number;
14
- routePrefix?: string;
12
+ litellmPort?: number;
13
+ proxyPort?: number;
15
14
  python?: string;
16
15
  journalNamespace?: string;
17
16
  }
18
17
 
19
18
  export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
20
- publicPort: number;
21
- appPort: number;
22
19
  graphitiPort: number;
23
- routePrefix: string;
20
+ litellmPort: number;
21
+ proxyPort: number;
24
22
  python: string;
25
23
  journalNamespace: string;
26
24
  }
@@ -28,10 +26,9 @@ export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
28
26
  export const GRAPHITI_CONFIG_SCHEMA = {
29
27
  type: "object",
30
28
  properties: {
31
- publicPort: { type: "integer", minimum: 1, maximum: 65535 },
32
- appPort: { type: "integer", minimum: 1, maximum: 65535 },
33
29
  graphitiPort: { type: "integer", minimum: 1, maximum: 65535 },
34
- routePrefix: { type: "string" },
30
+ litellmPort: { type: "integer", minimum: 1, maximum: 65535 },
31
+ proxyPort: { type: "integer", minimum: 1, maximum: 65535 },
35
32
  python: { type: "string" },
36
33
  journalNamespace: { type: "string" },
37
34
  },
@@ -42,58 +39,31 @@ export const GRAPHITI_CONFIG_SCHEMA = {
42
39
  export function resolveGraphitiConfig(
43
40
  config: GraphitiPluginConfig = {},
44
41
  ): ResolvedGraphitiPluginConfig {
45
- const publicPort = coreConfig.port(
46
- config.publicPort,
47
- "DATABRICKS_APP_PORT",
48
- 8000,
49
- coreConfig.ENV_ONLY,
50
- );
51
- const appPort = coreConfig.port(
52
- config.appPort,
53
- "GRAPHITI_APP_PORT",
54
- publicPort + 1,
55
- coreConfig.ENV_ONLY,
56
- );
57
42
  const graphitiPort = coreConfig.port(
58
43
  config.graphitiPort,
59
44
  "GRAPHITI_PORT",
60
45
  0,
61
46
  coreConfig.ENV_ONLY,
62
47
  );
63
- if (new Set([publicPort, appPort, graphitiPort]).size !== 3) {
64
- throw new ConfigurationError("Graphiti public, AppKit, and sidecar ports must be distinct");
48
+ const litellmPort = coreConfig.port(config.litellmPort, "LITELLM_PORT", 0, coreConfig.ENV_ONLY);
49
+ const proxyPort = coreConfig.port(config.proxyPort, "PROXY_PORT", 0, coreConfig.ENV_ONLY);
50
+ const configuredPorts = [graphitiPort, litellmPort, proxyPort].filter(Boolean);
51
+ if (new Set(configuredPorts).size !== configuredPorts.length) {
52
+ throw new ConfigurationError("Graphiti sidecar ports must be distinct");
65
53
  }
66
- const routePrefix = normalizeRoutePrefix(
67
- config.routePrefix ??
68
- coreConfig.text("GRAPHITI_ROUTE_PREFIX", coreConfig.ENV_ONLY) ??
69
- "/graphiti",
70
- );
71
54
  const python =
72
55
  config.python ?? coreConfig.text("PYTHON", coreConfig.ENV_ONLY)?.trim() ?? "python3";
73
56
  const journalNamespace =
74
57
  config.journalNamespace ??
75
58
  coreConfig.text("JOURNAL_NAMESPACE", coreConfig.ENV_ONLY)?.trim() ??
76
59
  process.env.DATABRICKS_APP_NAME?.trim() ??
60
+ coreProject.name() ??
77
61
  "default";
78
62
  return {
79
- publicPort,
80
- appPort,
81
63
  graphitiPort,
82
- routePrefix,
64
+ litellmPort,
65
+ proxyPort,
83
66
  python,
84
67
  journalNamespace,
85
68
  };
86
69
  }
87
-
88
- /** Return the internal port an AppKit `server()` plugin should bind. */
89
- export function appPort(config: GraphitiPluginConfig = {}): number {
90
- return resolveGraphitiConfig(config).appPort;
91
- }
92
-
93
- function normalizeRoutePrefix(value: string): string {
94
- const normalized = `/${value.trim().replace(/^\/+|\/+$/g, "")}`;
95
- if (normalized === "/") {
96
- throw new ConfigurationError("Graphiti routePrefix must contain a path segment");
97
- }
98
- return normalized;
99
- }
package/src/plugin.ts CHANGED
@@ -1,21 +1,30 @@
1
1
  /**
2
- * AppKit plugin that supervises Graphiti and a Caddy single-port proxy.
2
+ * AppKit plugin that supervises Graphiti and republishes user-scoped MCP tools.
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { createHash } from "node:crypto";
6
7
  import { createServer } from "node:net";
7
8
  import {
9
+ ConfigurationError,
8
10
  Plugin,
9
11
  lakebase,
10
12
  toPlugin,
13
+ type IAppRouter,
11
14
  type PluginManifest,
12
15
  type ResourceRequirement,
13
16
  } from "@databricks/appkit";
14
- import { interceptor as appkitInterceptor, plugin as appkitPlugin } from "@dbx-tools/appkit";
15
- import { exec } from "@dbx-tools/core";
16
- import { async as asyncModule, log } from "@dbx-tools/shared-core";
17
- import type { Tool } from "@mastra/core/tools";
18
- import { MCPClient } from "@mastra/mcp";
17
+ import {
18
+ appkit as dbxAppkit,
19
+ identity as appkitIdentity,
20
+ plugin as appkitPlugin,
21
+ } from "@dbx-tools/appkit";
22
+ import { config as coreConfig } from "@dbx-tools/core";
23
+ import { async as asyncModule, log, object } from "@dbx-tools/shared-core";
24
+ import { createTool, type Tool } from "@mastra/core/tools";
25
+ import { MCPClient, MCPServer } from "@mastra/mcp";
26
+ import concurrently, { type Command, type ConcurrentlyResult } from "concurrently";
27
+ import type express from "express";
19
28
  import {
20
29
  GRAPHITI_CONFIG_SCHEMA,
21
30
  resolveGraphitiConfig,
@@ -24,8 +33,31 @@ import {
24
33
  } from "./config.ts";
25
34
 
26
35
  const LAKEBASE_MANIFEST = appkitPlugin.data(lakebase).plugin.manifest;
36
+ const MCP_PATH = "/api/graphiti/mcp";
37
+ const MCP_SERVER_IDLE_MS = 30 * 60 * 1000;
38
+ const MCP_SERVER_SWEEP_MS = 5 * 60 * 1000;
39
+ const SCOPED_TOOL_FIELDS = {
40
+ add_memory: "group_id",
41
+ add_triplet: "group_id",
42
+ build_communities: "group_ids",
43
+ get_episodes: "group_ids",
44
+ get_status: undefined,
45
+ search_memory_facts: "group_ids",
46
+ search_nodes: "group_ids",
47
+ summarize_saga: "group_id",
48
+ } as const;
49
+ const WRITE_TOOLS = new Set(["add_memory", "add_triplet", "build_communities", "summarize_saga"]);
50
+ const UNSCOPED_ARGUMENTS = [
51
+ "center_node_uuid",
52
+ "previous_episode_uuids",
53
+ "saga_previous_episode_uuid",
54
+ "source_node_uuid",
55
+ "target_node_uuid",
56
+ "uuid",
57
+ ] as const;
27
58
 
28
59
  interface ToolkitEntry {
60
+ readonly __toolkitRef: true;
29
61
  pluginName: string;
30
62
  localName: string;
31
63
  def: {
@@ -33,6 +65,15 @@ interface ToolkitEntry {
33
65
  description: string;
34
66
  parameters: unknown;
35
67
  };
68
+ annotations: {
69
+ effect: "read" | "write";
70
+ requiresUserContext: true;
71
+ };
72
+ }
73
+
74
+ interface UserMcpServer {
75
+ lastUsed: number;
76
+ server: MCPServer;
36
77
  }
37
78
 
38
79
  export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
@@ -58,108 +99,144 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
58
99
  }
59
100
 
60
101
  private readonly logger = log.logger(this);
61
- private children: ReturnType<typeof exec.spawn>[] = [];
102
+ private commands: Command[] = [];
62
103
  private mcp?: MCPClient;
104
+ private mcpServers = new Map<string, UserMcpServer>();
105
+ private mcpServerSweep?: NodeJS.Timeout;
63
106
  private mcpTools: Record<string, Tool> = {};
64
107
  private resolved?: ResolvedGraphitiPluginConfig;
108
+ private setupComplete = false;
109
+ private supervision?: ConcurrentlyResult;
65
110
  private stopping = false;
66
111
 
67
112
  override async setup(): Promise<void> {
68
113
  const configured = resolveGraphitiConfig(this.config);
114
+ const [graphitiPort, litellmPort, proxyPort] = await distinctPorts(
115
+ coreConfig.port(undefined, "DATABRICKS_APP_PORT", 8000, coreConfig.ENV_ONLY),
116
+ configured.graphitiPort,
117
+ configured.litellmPort,
118
+ configured.proxyPort,
119
+ );
69
120
  this.resolved = {
70
121
  ...configured,
71
- graphitiPort: configured.graphitiPort || (await availablePort()),
122
+ graphitiPort,
123
+ litellmPort,
124
+ proxyPort,
72
125
  };
73
- if (!process.env.LAKEBASE_ENDPOINT && !process.env.PGHOST) {
74
- throw new Error(
75
- "Graphiti requires Lakebase; register lakebase() before graphiti() and bind its Postgres resource",
76
- );
77
- }
78
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
79
- process.once(signal, () => this.stopGraphiti());
80
- }
81
- const supervisor = appkitInterceptor.createInterceptorContext({});
82
- supervisor.context.onTeardown(() => this.stopGraphiti());
83
- const graphiti = exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "start"], {
84
- env: {
85
- ...process.env,
86
- GRAPHITI_HOST: "127.0.0.1",
87
- GRAPHITI_PORT: String(this.resolved.graphitiPort),
88
- JOURNAL_NAMESPACE: this.resolved.journalNamespace,
89
- },
90
- stdin: "ignore",
91
- stdout: "inherit",
92
- stderr: "inherit",
93
- });
94
- const caddy = exec.spawn(
95
- this.resolved.python,
126
+ this.supervision = concurrently(
96
127
  [
97
- "-m",
98
- "dbx_tools.graphiti.proxy",
99
- "--public-port",
100
- String(this.resolved.publicPort),
101
- "--app-port",
102
- String(this.resolved.appPort),
103
- "--graphiti-port",
104
- String(this.resolved.graphitiPort),
105
- "--route-prefix",
106
- this.resolved.routePrefix,
128
+ {
129
+ name: "graphiti",
130
+ command: commandLine([this.resolved.python, "-m", "dbx_tools.graphiti", "start"]),
131
+ env: {
132
+ ...process.env,
133
+ GRAPHITI_HOST: "127.0.0.1",
134
+ GRAPHITI_PORT: String(this.resolved.graphitiPort),
135
+ JOURNAL_NAMESPACE: this.resolved.journalNamespace,
136
+ LITELLM_HOST: "127.0.0.1",
137
+ LITELLM_PORT: String(this.resolved.litellmPort),
138
+ MANAGE_LITELLM: "true",
139
+ },
140
+ },
141
+ {
142
+ name: "caddy",
143
+ command: commandLine([
144
+ this.resolved.python,
145
+ "-m",
146
+ "dbx_tools.graphiti.proxy",
147
+ "--proxy-port",
148
+ String(this.resolved.proxyPort),
149
+ "--graphiti-port",
150
+ String(this.resolved.graphitiPort),
151
+ ]),
152
+ env: process.env,
153
+ },
107
154
  ],
108
155
  {
109
- env: process.env,
110
- stdin: "ignore",
111
- stdout: "inherit",
112
- stderr: "inherit",
156
+ killOthersOn: ["failure", "success"],
157
+ killSignal: "SIGTERM",
158
+ killTimeout: 12_000,
159
+ prefix: "name",
160
+ prefixColors: false,
113
161
  },
114
162
  );
115
- this.children = [graphiti, caddy];
116
- for (const child of this.children) supervisor.context.bindProcess(child);
117
- await waitForGraphiti(this.resolved.graphitiPort);
118
- this.mcp = new MCPClient({
119
- id: `appkit-graphiti-${this.resolved.graphitiPort}`,
120
- servers: {
121
- graphiti: {
122
- url: new URL(`http://127.0.0.1:${this.resolved.graphitiPort}/mcp`),
123
- },
163
+ this.commands = this.supervision.commands;
164
+ void this.supervision.result.then(
165
+ () => this.onSupervisorExit(),
166
+ (error) => this.onSupervisorExit(error),
167
+ );
168
+ const supervisionFailure = this.supervision.result.then(
169
+ () => {
170
+ throw new Error("Graphiti sidecar supervisor exited during startup");
171
+ },
172
+ (error) => {
173
+ throw error;
124
174
  },
125
- });
126
- const discovered = await this.mcp.listTools();
127
- this.mcpTools = Object.fromEntries(
128
- Object.entries(discovered).map(([name, tool]) => [
129
- name.replace(/^graphiti_/, ""),
130
- tool as Tool,
131
- ]),
132
175
  );
176
+ try {
177
+ await Promise.race([
178
+ (async () => {
179
+ await waitForGraphiti(graphitiPort);
180
+ await waitForGraphiti(proxyPort);
181
+ })(),
182
+ supervisionFailure,
183
+ ]);
184
+ this.mcp = new MCPClient({
185
+ id: `appkit-graphiti-${this.resolved.graphitiPort}`,
186
+ servers: {
187
+ graphiti: {
188
+ url: new URL(`http://127.0.0.1:${this.resolved.proxyPort}/mcp`),
189
+ },
190
+ },
191
+ });
192
+ const discovered = await this.mcp.listTools();
193
+ this.mcpTools = Object.fromEntries(
194
+ Object.entries(discovered)
195
+ .map(([name, tool]) => [name.replace(/^graphiti_/, ""), tool as Tool] as const)
196
+ .filter(([name]) => name in SCOPED_TOOL_FIELDS),
197
+ );
198
+ const missing = Object.keys(SCOPED_TOOL_FIELDS).filter((name) => !this.mcpTools[name]);
199
+ if (missing.length > 0) {
200
+ throw new Error(`Graphiti did not publish required scoped tools: ${missing.join(", ")}`);
201
+ }
202
+ } catch (error) {
203
+ await this.stopSidecars();
204
+ throw error;
205
+ }
206
+ this.setupComplete = true;
207
+ this.mcpServerSweep = setInterval(() => this.closeIdleMcpServers(), MCP_SERVER_SWEEP_MS);
208
+ this.mcpServerSweep.unref();
133
209
  this.logger.info("ready", {
134
- appPort: this.resolved.appPort,
135
210
  graphitiPort: this.resolved.graphitiPort,
136
- publicPort: this.resolved.publicPort,
137
- mcpPath: `${this.resolved.routePrefix}/mcp/`,
211
+ litellmPort: this.resolved.litellmPort,
212
+ proxyPort: this.resolved.proxyPort,
213
+ mcpPath: MCP_PATH,
138
214
  tools: Object.keys(this.mcpTools),
139
215
  });
140
216
  }
141
217
 
218
+ override injectRoutes(router: IAppRouter): void {
219
+ for (const method of ["get", "post", "delete"] as const) {
220
+ this.route(router, {
221
+ name: `${method}Mcp`,
222
+ method,
223
+ path: "/mcp",
224
+ handler: (request, response) => this.forwardMcp(request, response),
225
+ });
226
+ }
227
+ }
228
+
142
229
  override abortActiveOperations(): void {
143
230
  super.abortActiveOperations();
144
- this.stopGraphiti();
145
231
  void this.mcp?.disconnect();
146
- this.mcp = undefined;
147
- this.mcpTools = {};
148
- for (const child of this.children) {
149
- if (!child.killed) child.kill("SIGTERM");
150
- }
151
- this.children = [];
232
+ }
233
+
234
+ async shutdown(): Promise<void> {
235
+ await this.stopSidecars();
152
236
  }
153
237
 
154
238
  override exports() {
155
- const resolved = this.resolved ?? resolveGraphitiConfig(this.config);
156
- return {
157
- appPort: resolved.appPort,
158
- graphitiPort: resolved.graphitiPort,
159
- mcpPath: `${resolved.routePrefix}/mcp/`,
160
- publicPort: resolved.publicPort,
161
- routePrefix: resolved.routePrefix,
162
- };
239
+ return { mcpPath: MCP_PATH };
163
240
  }
164
241
 
165
242
  toolkit(): Record<string, ToolkitEntry> {
@@ -167,6 +244,7 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
167
244
  Object.entries(this.mcpTools).map(([name, tool]) => [
168
245
  name,
169
246
  {
247
+ __toolkitRef: true as const,
170
248
  pluginName: "graphiti",
171
249
  localName: name,
172
250
  def: {
@@ -174,31 +252,130 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
174
252
  description: tool.description,
175
253
  parameters: tool.inputSchema ?? { type: "object", properties: {} },
176
254
  },
255
+ annotations: {
256
+ effect: WRITE_TOOLS.has(name) ? ("write" as const) : ("read" as const),
257
+ requiresUserContext: true as const,
258
+ },
177
259
  },
178
260
  ]),
179
261
  );
180
262
  }
181
263
 
182
- async executeAgentTool(name: string, args: unknown, signal?: AbortSignal): Promise<unknown> {
264
+ async executeAgentTool(
265
+ name: string,
266
+ args: unknown,
267
+ signal?: AbortSignal,
268
+ context?: { resourceId?: string },
269
+ ): Promise<unknown> {
183
270
  const tool = this.mcpTools[name];
184
271
  if (!tool?.execute) throw new Error(`Unknown Graphiti tool: ${name}`);
185
- return tool.execute(args, { abortSignal: signal } as never);
272
+ const userId = context?.resourceId ?? executionContextUserId();
273
+ return tool.execute(scopedArguments(name, args, userScope(userId)), {
274
+ abortSignal: signal,
275
+ } as never);
186
276
  }
187
277
 
188
- private stopGraphiti(): void {
189
- if (this.stopping || !this.resolved) return;
190
- this.stopping = true;
191
- void exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "down"], {
192
- env: process.env,
193
- stdin: "ignore",
194
- stdout: "inherit",
195
- stderr: "inherit",
278
+ private async forwardMcp(request: express.Request, response: express.Response): Promise<void> {
279
+ if (!this.resolved) {
280
+ response.status(503).json({ error: "Graphiti is not ready" });
281
+ return;
282
+ }
283
+ const userId = appkitIdentity.requestUserId(request) ?? executionContextUserId();
284
+ const server = this.mcpServer(userId);
285
+ await server.startHTTP({
286
+ url: new URL(request.originalUrl, "http://app.local"),
287
+ httpPath: MCP_PATH,
288
+ req: request,
289
+ res: response,
196
290
  });
197
291
  }
292
+
293
+ private mcpServer(userId: string): MCPServer {
294
+ const existing = this.mcpServers.get(userId);
295
+ if (existing) {
296
+ existing.lastUsed = Date.now();
297
+ return existing.server;
298
+ }
299
+ const scope = userScope(userId);
300
+ const tools = Object.fromEntries(
301
+ Object.entries(this.mcpTools).map(([name, tool]) => [
302
+ name,
303
+ createTool({
304
+ id: name,
305
+ description: tool.description ?? name,
306
+ ...(tool.inputSchema ? { inputSchema: tool.inputSchema as never } : {}),
307
+ execute: (args: unknown, context: unknown) => {
308
+ if (!tool.execute) throw new Error(`Graphiti tool cannot execute: ${name}`);
309
+ return tool.execute(scopedArguments(name, args, scope), context as never);
310
+ },
311
+ }),
312
+ ]),
313
+ );
314
+ const server = new MCPServer({
315
+ name: "Graphiti",
316
+ version: "1.0.0",
317
+ tools,
318
+ });
319
+ this.mcpServers.set(userId, { lastUsed: Date.now(), server });
320
+ return server;
321
+ }
322
+
323
+ private onSupervisorExit(error?: unknown): void {
324
+ if (this.stopping || !this.setupComplete) return;
325
+ this.logger.error("sidecar supervisor exited", { error });
326
+ process.kill(process.pid, "SIGTERM");
327
+ }
328
+
329
+ private async stopSidecars(): Promise<void> {
330
+ if (this.stopping) return;
331
+ this.stopping = true;
332
+ if (this.mcpServerSweep) clearInterval(this.mcpServerSweep);
333
+ this.mcpServerSweep = undefined;
334
+ await Promise.allSettled([...this.mcpServers.values()].map(({ server }) => server.close()));
335
+ this.mcpServers.clear();
336
+ await this.mcp?.disconnect();
337
+ this.mcp = undefined;
338
+ this.mcpTools = {};
339
+ for (const command of this.commands) command.kill("SIGTERM");
340
+ if (this.supervision) await this.supervision.result.catch(() => undefined);
341
+ this.commands = [];
342
+ this.supervision = undefined;
343
+ }
344
+
345
+ private closeIdleMcpServers(): void {
346
+ const cutoff = Date.now() - MCP_SERVER_IDLE_MS;
347
+ for (const [userId, entry] of this.mcpServers) {
348
+ if (entry.lastUsed >= cutoff) continue;
349
+ this.mcpServers.delete(userId);
350
+ void entry.server.close().catch((error) => {
351
+ this.logger.warn("idle MCP server close failed", { error });
352
+ });
353
+ }
354
+ }
198
355
  }
199
356
 
200
357
  export const graphiti = toPlugin(GraphitiPlugin);
201
358
 
359
+ function executionContextUserId(): string {
360
+ const context = dbxAppkit.tryGetExecutionContext();
361
+ if (!context) throw new Error("Graphiti memory requires an AppKit execution context");
362
+ return "userId" in context ? context.userId : context.serviceUserId;
363
+ }
364
+
365
+ function userScope(userId: string): string {
366
+ return `user_${createHash("sha256").update(userId).digest("base64url")}`;
367
+ }
368
+
369
+ function scopedArguments(name: string, args: unknown, scope: string): Record<string, unknown> {
370
+ if (!(name in SCOPED_TOOL_FIELDS)) throw new Error(`Graphiti tool is not user-scoped: ${name}`);
371
+ const scoped = object.isRecord(args) ? { ...args } : {};
372
+ for (const argument of UNSCOPED_ARGUMENTS) delete scoped[argument];
373
+ const field = SCOPED_TOOL_FIELDS[name as keyof typeof SCOPED_TOOL_FIELDS];
374
+ if (field === "group_id") scoped.group_id = scope;
375
+ if (field === "group_ids") scoped.group_ids = [scope];
376
+ return scoped;
377
+ }
378
+
202
379
  async function waitForGraphiti(port: number): Promise<void> {
203
380
  for await (const ready of asyncModule.poll(
204
381
  async ({ signal }) => {
@@ -234,3 +411,25 @@ async function availablePort(): Promise<number> {
234
411
  });
235
412
  });
236
413
  }
414
+
415
+ async function distinctPorts(
416
+ appPort: number,
417
+ graphitiPort: number,
418
+ litellmPort: number,
419
+ proxyPort: number,
420
+ ): Promise<[number, number, number]> {
421
+ const ports = [appPort];
422
+ for (const configuredPort of [graphitiPort, litellmPort, proxyPort]) {
423
+ if (configuredPort && ports.includes(configuredPort)) {
424
+ throw new ConfigurationError("Graphiti sidecar ports must differ from DATABRICKS_APP_PORT");
425
+ }
426
+ let port = configuredPort || (await availablePort());
427
+ while (ports.includes(port)) port = await availablePort();
428
+ ports.push(port);
429
+ }
430
+ return ports.slice(1) as [number, number, number];
431
+ }
432
+
433
+ function commandLine(arguments_: string[]): string {
434
+ return arguments_.map((value) => `'${value.replaceAll("'", "'\\''")}'`).join(" ");
435
+ }