@dbx-tools/appkit-graphiti 0.6.114 → 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,28 +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
12
  litellmPort?: number;
15
- routePrefix?: string;
13
+ proxyPort?: number;
16
14
  python?: string;
17
15
  journalNamespace?: string;
18
16
  }
19
17
 
20
18
  export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
21
- publicPort: number;
22
- appPort: number;
23
19
  graphitiPort: number;
24
20
  litellmPort: number;
25
- routePrefix: string;
21
+ proxyPort: number;
26
22
  python: string;
27
23
  journalNamespace: string;
28
24
  }
@@ -30,11 +26,9 @@ export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
30
26
  export const GRAPHITI_CONFIG_SCHEMA = {
31
27
  type: "object",
32
28
  properties: {
33
- publicPort: { type: "integer", minimum: 1, maximum: 65535 },
34
- appPort: { type: "integer", minimum: 1, maximum: 65535 },
35
29
  graphitiPort: { type: "integer", minimum: 1, maximum: 65535 },
36
30
  litellmPort: { type: "integer", minimum: 1, maximum: 65535 },
37
- routePrefix: { type: "string" },
31
+ proxyPort: { type: "integer", minimum: 1, maximum: 65535 },
38
32
  python: { type: "string" },
39
33
  journalNamespace: { type: "string" },
40
34
  },
@@ -45,18 +39,6 @@ export const GRAPHITI_CONFIG_SCHEMA = {
45
39
  export function resolveGraphitiConfig(
46
40
  config: GraphitiPluginConfig = {},
47
41
  ): ResolvedGraphitiPluginConfig {
48
- const publicPort = coreConfig.port(
49
- config.publicPort,
50
- "DATABRICKS_APP_PORT",
51
- 8000,
52
- coreConfig.ENV_ONLY,
53
- );
54
- const appPort = coreConfig.port(
55
- config.appPort,
56
- "GRAPHITI_APP_PORT",
57
- publicPort + 1,
58
- coreConfig.ENV_ONLY,
59
- );
60
42
  const graphitiPort = coreConfig.port(
61
43
  config.graphitiPort,
62
44
  "GRAPHITI_PORT",
@@ -64,42 +46,24 @@ export function resolveGraphitiConfig(
64
46
  coreConfig.ENV_ONLY,
65
47
  );
66
48
  const litellmPort = coreConfig.port(config.litellmPort, "LITELLM_PORT", 0, coreConfig.ENV_ONLY);
67
- const configuredPorts = [publicPort, appPort, graphitiPort, litellmPort].filter(Boolean);
49
+ const proxyPort = coreConfig.port(config.proxyPort, "PROXY_PORT", 0, coreConfig.ENV_ONLY);
50
+ const configuredPorts = [graphitiPort, litellmPort, proxyPort].filter(Boolean);
68
51
  if (new Set(configuredPorts).size !== configuredPorts.length) {
69
- throw new ConfigurationError("Graphiti public, AppKit, and sidecar ports must be distinct");
52
+ throw new ConfigurationError("Graphiti sidecar ports must be distinct");
70
53
  }
71
- const routePrefix = normalizeRoutePrefix(
72
- config.routePrefix ??
73
- coreConfig.text("GRAPHITI_ROUTE_PREFIX", coreConfig.ENV_ONLY) ??
74
- "/graphiti",
75
- );
76
54
  const python =
77
55
  config.python ?? coreConfig.text("PYTHON", coreConfig.ENV_ONLY)?.trim() ?? "python3";
78
56
  const journalNamespace =
79
57
  config.journalNamespace ??
80
58
  coreConfig.text("JOURNAL_NAMESPACE", coreConfig.ENV_ONLY)?.trim() ??
81
59
  process.env.DATABRICKS_APP_NAME?.trim() ??
60
+ coreProject.name() ??
82
61
  "default";
83
62
  return {
84
- publicPort,
85
- appPort,
86
63
  graphitiPort,
87
64
  litellmPort,
88
- routePrefix,
65
+ proxyPort,
89
66
  python,
90
67
  journalNamespace,
91
68
  };
92
69
  }
93
-
94
- /** Return the internal port an AppKit `server()` plugin should bind. */
95
- export function appPort(config: GraphitiPluginConfig = {}): number {
96
- return resolveGraphitiConfig(config).appPort;
97
- }
98
-
99
- function normalizeRoutePrefix(value: string): string {
100
- const normalized = `/${value.trim().replace(/^\/+|\/+$/g, "")}`;
101
- if (normalized === "/") {
102
- throw new ConfigurationError("Graphiti routePrefix must contain a path segment");
103
- }
104
- return normalized;
105
- }
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,117 +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);
69
- const graphitiPort = configured.graphitiPort || (await availablePort());
70
- let litellmPort = configured.litellmPort || (await availablePort());
71
- while (litellmPort === graphitiPort) litellmPort = await availablePort();
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
+ );
72
120
  this.resolved = {
73
121
  ...configured,
74
122
  graphitiPort,
75
123
  litellmPort,
124
+ proxyPort,
76
125
  };
77
- if (!process.env.LAKEBASE_ENDPOINT && !process.env.PGHOST) {
78
- throw new Error(
79
- "Graphiti requires Lakebase; register lakebase() before graphiti() and bind its Postgres resource",
80
- );
81
- }
82
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
83
- process.once(signal, () => this.stopGraphiti());
84
- }
85
- const supervisor = appkitInterceptor.createInterceptorContext({});
86
- supervisor.context.onTeardown(() => this.stopGraphiti());
87
- const graphiti = exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "start"], {
88
- env: {
89
- ...process.env,
90
- GRAPHITI_HOST: "127.0.0.1",
91
- GRAPHITI_PORT: String(this.resolved.graphitiPort),
92
- JOURNAL_NAMESPACE: this.resolved.journalNamespace,
93
- LITELLM_HOST: "127.0.0.1",
94
- LITELLM_PORT: String(this.resolved.litellmPort),
95
- MANAGE_LITELLM: "true",
96
- },
97
- stdin: "ignore",
98
- stdout: "inherit",
99
- stderr: "inherit",
100
- });
101
- const caddy = exec.spawn(
102
- this.resolved.python,
126
+ this.supervision = concurrently(
103
127
  [
104
- "-m",
105
- "dbx_tools.graphiti.proxy",
106
- "--public-port",
107
- String(this.resolved.publicPort),
108
- "--app-port",
109
- String(this.resolved.appPort),
110
- "--graphiti-port",
111
- String(this.resolved.graphitiPort),
112
- "--route-prefix",
113
- 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
+ },
114
154
  ],
115
155
  {
116
- env: process.env,
117
- stdin: "ignore",
118
- stdout: "inherit",
119
- stderr: "inherit",
156
+ killOthersOn: ["failure", "success"],
157
+ killSignal: "SIGTERM",
158
+ killTimeout: 12_000,
159
+ prefix: "name",
160
+ prefixColors: false,
120
161
  },
121
162
  );
122
- this.children = [graphiti, caddy];
123
- for (const child of this.children) supervisor.context.bindProcess(child);
124
- await waitForGraphiti(this.resolved.graphitiPort);
125
- this.mcp = new MCPClient({
126
- id: `appkit-graphiti-${this.resolved.graphitiPort}`,
127
- servers: {
128
- graphiti: {
129
- url: new URL(`http://127.0.0.1:${this.resolved.graphitiPort}/mcp`),
130
- },
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;
131
174
  },
132
- });
133
- const discovered = await this.mcp.listTools();
134
- this.mcpTools = Object.fromEntries(
135
- Object.entries(discovered).map(([name, tool]) => [
136
- name.replace(/^graphiti_/, ""),
137
- tool as Tool,
138
- ]),
139
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();
140
209
  this.logger.info("ready", {
141
- appPort: this.resolved.appPort,
142
210
  graphitiPort: this.resolved.graphitiPort,
143
211
  litellmPort: this.resolved.litellmPort,
144
- publicPort: this.resolved.publicPort,
145
- mcpPath: `${this.resolved.routePrefix}/mcp/`,
212
+ proxyPort: this.resolved.proxyPort,
213
+ mcpPath: MCP_PATH,
146
214
  tools: Object.keys(this.mcpTools),
147
215
  });
148
216
  }
149
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
+
150
229
  override abortActiveOperations(): void {
151
230
  super.abortActiveOperations();
152
- this.stopGraphiti();
153
231
  void this.mcp?.disconnect();
154
- this.mcp = undefined;
155
- this.mcpTools = {};
156
- for (const child of this.children) {
157
- if (!child.killed) child.kill("SIGTERM");
158
- }
159
- this.children = [];
232
+ }
233
+
234
+ async shutdown(): Promise<void> {
235
+ await this.stopSidecars();
160
236
  }
161
237
 
162
238
  override exports() {
163
- const resolved = this.resolved ?? resolveGraphitiConfig(this.config);
164
- return {
165
- appPort: resolved.appPort,
166
- graphitiPort: resolved.graphitiPort,
167
- litellmPort: resolved.litellmPort,
168
- mcpPath: `${resolved.routePrefix}/mcp/`,
169
- publicPort: resolved.publicPort,
170
- routePrefix: resolved.routePrefix,
171
- };
239
+ return { mcpPath: MCP_PATH };
172
240
  }
173
241
 
174
242
  toolkit(): Record<string, ToolkitEntry> {
@@ -176,6 +244,7 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
176
244
  Object.entries(this.mcpTools).map(([name, tool]) => [
177
245
  name,
178
246
  {
247
+ __toolkitRef: true as const,
179
248
  pluginName: "graphiti",
180
249
  localName: name,
181
250
  def: {
@@ -183,31 +252,130 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
183
252
  description: tool.description,
184
253
  parameters: tool.inputSchema ?? { type: "object", properties: {} },
185
254
  },
255
+ annotations: {
256
+ effect: WRITE_TOOLS.has(name) ? ("write" as const) : ("read" as const),
257
+ requiresUserContext: true as const,
258
+ },
186
259
  },
187
260
  ]),
188
261
  );
189
262
  }
190
263
 
191
- 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> {
192
270
  const tool = this.mcpTools[name];
193
271
  if (!tool?.execute) throw new Error(`Unknown Graphiti tool: ${name}`);
194
- 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);
195
276
  }
196
277
 
197
- private stopGraphiti(): void {
198
- if (this.stopping || !this.resolved) return;
199
- this.stopping = true;
200
- void exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "down"], {
201
- env: process.env,
202
- stdin: "ignore",
203
- stdout: "inherit",
204
- 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,
205
290
  });
206
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
+ }
207
355
  }
208
356
 
209
357
  export const graphiti = toPlugin(GraphitiPlugin);
210
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
+
211
379
  async function waitForGraphiti(port: number): Promise<void> {
212
380
  for await (const ready of asyncModule.poll(
213
381
  async ({ signal }) => {
@@ -243,3 +411,25 @@ async function availablePort(): Promise<number> {
243
411
  });
244
412
  });
245
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
+ }