@classytic/arc 2.17.1 → 2.18.1

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.
Files changed (71) hide show
  1. package/dist/{BaseController-CgHomZ4d.mjs → BaseController-DPwe89qs.mjs} +1 -1
  2. package/dist/auth/index.d.mts +1 -1
  3. package/dist/cli/commands/describe.d.mts +1 -1
  4. package/dist/cli/commands/docs.mjs +1 -1
  5. package/dist/cli/commands/init.mjs +1 -1
  6. package/dist/cli/index.mjs +1 -1
  7. package/dist/context/index.d.mts +54 -1
  8. package/dist/context/index.mjs +65 -2
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +3 -3
  11. package/dist/{core-D8tpQ4ZL.mjs → core-B41eZxyM.mjs} +8 -5
  12. package/dist/{createActionRouter-BHUMb1-y.mjs → createActionRouter-CRaIwB5S.mjs} +2 -2
  13. package/dist/{createAggregationRouter-BYVvhfc5.mjs → createAggregationRouter-n3O6WdLO.mjs} +1 -1
  14. package/dist/{createApp-CzCEEsEm.mjs → createApp-DZvrb587.mjs} +29 -7
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +1 -1
  17. package/dist/{eventPlugin-CnJCUyM0.mjs → eventPlugin-Dxp9JlKK.mjs} +1 -1
  18. package/dist/events/index.d.mts +48 -0
  19. package/dist/events/index.mjs +27 -5
  20. package/dist/factory/index.d.mts +1 -1
  21. package/dist/factory/index.mjs +1 -1
  22. package/dist/{fastify-augmentation-7LIR0nDv.d.mts → fastify-augmentation-CL48B6gW.d.mts} +1 -1
  23. package/dist/hooks/index.d.mts +1 -1
  24. package/dist/{index-C1JSBan-.d.mts → index-C000faom.d.mts} +10 -1
  25. package/dist/{index-CruZir1o.d.mts → index-CNiryUMN.d.mts} +1 -1
  26. package/dist/index.d.mts +2 -2
  27. package/dist/index.mjs +4 -4
  28. package/dist/{init-DYzfniuX.mjs → init-DQ_rY1J7.mjs} +13 -13
  29. package/dist/integrations/event-gateway.d.mts +1 -1
  30. package/dist/integrations/index.d.mts +2 -2
  31. package/dist/integrations/mcp/index.d.mts +43 -3
  32. package/dist/integrations/mcp/index.mjs +102 -2
  33. package/dist/integrations/mcp/testing.d.mts +1 -1
  34. package/dist/integrations/mcp/testing.mjs +1 -1
  35. package/dist/integrations/streamline.d.mts +8 -1
  36. package/dist/integrations/streamline.mjs +34 -3
  37. package/dist/integrations/websocket-pushref-redis.d.mts +71 -0
  38. package/dist/integrations/websocket-pushref-redis.mjs +205 -0
  39. package/dist/integrations/websocket-redis.d.mts +1 -1
  40. package/dist/integrations/websocket.d.mts +3 -2
  41. package/dist/integrations/websocket.mjs +1142 -23
  42. package/dist/{mcpPlugin-C6FViZXw.mjs → mcpPlugin-BrepZ0cV.mjs} +3 -3
  43. package/dist/middleware/index.d.mts +1 -1
  44. package/dist/{openapi-DKDs9NfD.mjs → openapi-CLZx9bwS.mjs} +1 -1
  45. package/dist/pipeline/index.d.mts +1 -1
  46. package/dist/plugins/index.d.mts +17 -3
  47. package/dist/plugins/index.mjs +44 -3
  48. package/dist/plugins/tracing-entry.d.mts +1 -1
  49. package/dist/plugins/tracing-entry.mjs +1 -1
  50. package/dist/presets/filesUpload.d.mts +2 -2
  51. package/dist/presets/index.d.mts +1 -1
  52. package/dist/presets/multiTenant.d.mts +1 -1
  53. package/dist/presets/search.d.mts +1 -1
  54. package/dist/pushref-registry-Bq_-EVsC.d.mts +435 -0
  55. package/dist/registry/index.d.mts +1 -1
  56. package/dist/{requestContext-SSaaTgW8.mjs → requestContext-DDLS3NVU.mjs} +26 -1
  57. package/dist/{routerShared-nCyJGOKX.mjs → routerShared-Dl0A77Ba.mjs} +1 -1
  58. package/dist/{schemaIR-DAOvBWgT.mjs → schemaIR-Bk6Tns82.mjs} +17 -5
  59. package/dist/testing/index.d.mts +2 -2
  60. package/dist/testing/index.mjs +1 -1
  61. package/dist/testing/storageContract.d.mts +1 -1
  62. package/dist/types/index.d.mts +1 -1
  63. package/dist/types/storage.d.mts +1 -1
  64. package/dist/{types-BRmS8TNa.d.mts → types-DEEWwQFR.d.mts} +1 -1
  65. package/dist/{types-CoMjNmkF.d.mts → types-DH-7WO64.d.mts} +1 -1
  66. package/dist/utils/index.d.mts +1 -1
  67. package/dist/websocket-BZMUP7Gr.d.mts +456 -0
  68. package/package.json +12 -1
  69. package/dist/websocket-BJ0w-3Dt.d.mts +0 -186
  70. /package/dist/{storage-HcAgP5Ni.d.mts → storage-BFmamN5m.d.mts} +0 -0
  71. /package/dist/{tracing-bRqqUxFO.d.mts → tracing-BYQ3hsbM.d.mts} +0 -0
@@ -917,9 +917,9 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
917
917
  //#region src/cli/commands/init/dependency-plan.ts
918
918
  const SCAFFOLD_DEP_VERSIONS = {
919
919
  core: {
920
- "@classytic/arc": "^2.13.0",
920
+ "@classytic/arc": "^2.17.2",
921
921
  "@classytic/primitives": "^0.6.0",
922
- "@classytic/repo-core": "^0.4.2",
922
+ "@classytic/repo-core": "^0.5.0",
923
923
  "@fastify/cors": "^11.2.0",
924
924
  "@fastify/helmet": "^13.0.2",
925
925
  "@fastify/rate-limit": "^10.3.0",
@@ -929,27 +929,27 @@ const SCAFFOLD_DEP_VERSIONS = {
929
929
  fastify: "^5.8.5"
930
930
  },
931
931
  authJwt: {
932
- "@fastify/jwt": "^10.0.0",
933
- bcryptjs: "^3.0.0"
932
+ "@fastify/jwt": "^10.1.0",
933
+ bcryptjs: "^3.0.3"
934
934
  },
935
935
  authBetterAuth: {
936
- "better-auth": "^1.6.9",
937
- mongodb: "^7.1.0"
936
+ "better-auth": "^1.6.11",
937
+ mongodb: "^7.2.0"
938
938
  },
939
- authBetterAuthApiKey: { "@better-auth/api-key": "^1.6.9" },
939
+ authBetterAuthApiKey: { "@better-auth/api-key": "^1.6.11" },
940
940
  adapterMongokit: {
941
- "@classytic/mongokit": "^3.13.0",
942
- mongoose: "^9.6.1"
941
+ "@classytic/mongokit": "^3.14.0",
942
+ mongoose: "^9.6.2"
943
943
  },
944
944
  devCommon: {
945
945
  "mongodb-memory-server": "^11.1.0",
946
- "pino-pretty": "^13.0.0",
947
- vitest: "^4.1.5"
946
+ "pino-pretty": "^13.1.3",
947
+ vitest: "^4.1.7"
948
948
  },
949
949
  devTypescript: {
950
950
  "@types/node": "^22.10.0",
951
- tsx: "^4.21.0",
952
- typescript: "^5.7.2"
951
+ tsx: "^4.22.3",
952
+ typescript: "^6.0.3"
953
953
  },
954
954
  typesJwt: { "@types/bcryptjs": "^3.0.0" }
955
955
  };
@@ -1,5 +1,5 @@
1
1
  import { n as DomainEvent } from "../EventTransport-B1GFiYUy.mjs";
2
- import { a as WebSocketMessage, i as WebSocketClient } from "../websocket-BJ0w-3Dt.mjs";
2
+ import { c as WebSocketClient, l as WebSocketMessage } from "../websocket-BZMUP7Gr.mjs";
3
3
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
4
 
5
5
  //#region src/integrations/event-gateway.d.ts
@@ -1,7 +1,7 @@
1
- import { a as WebSocketMessage, i as WebSocketClient, o as WebSocketPluginOptions } from "../websocket-BJ0w-3Dt.mjs";
1
+ import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-BZMUP7Gr.mjs";
2
2
  import { EventGatewayOptions } from "./event-gateway.mjs";
3
3
  import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
4
- import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-CoMjNmkF.mjs";
4
+ import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-DH-7WO64.mjs";
5
5
  import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
6
6
  import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
7
7
  export { type BetterAuthHandler, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type EventGatewayOptions, type JobDefinition, type JobDispatchOptions, type JobDispatcher, type JobMeta, type JobsPluginOptions, type McpAuthResult, type McpPluginOptions, type McpResourceConfig, type PromptDefinition, type QueueStats, type StreamlinePluginOptions, type ToolAnnotations, type ToolContext, type ToolDefinition, type WebSocketClient, type WebSocketMessage, type WebSocketPluginOptions, type WebhookDeliveryRecord, type WebhookManager, type WebhookPluginOptions, type WebhookStore, type WebhookSubscription, type WorkflowLike, type WorkflowRunLike };
@@ -1,7 +1,7 @@
1
- import { V as ResourceDefinition, bt as IRequestContext, yt as IControllerResponse } from "../../index-C1JSBan-.mjs";
1
+ import { V as ResourceDefinition, bt as IRequestContext, yt as IControllerResponse } from "../../index-C000faom.mjs";
2
2
  import { i as RequestScope } from "../../types-CfapW2t8.mjs";
3
3
  import { c as PermissionCheck } from "../../fields-DqZLp5d6.mjs";
4
- import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-CoMjNmkF.mjs";
4
+ import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-DH-7WO64.mjs";
5
5
  import { ErrorContract } from "@classytic/repo-core/errors";
6
6
  import { FastifyPluginAsync } from "fastify";
7
7
  import { z } from "zod";
@@ -505,6 +505,46 @@ declare function filterResourcesForMcp<T extends {
505
505
  }>(resources: readonly T[], inputs: ResourceFilterInputs): T[];
506
506
  declare const mcpPlugin: FastifyPluginAsync<McpPluginOptions>;
507
507
  //#endregion
508
+ //#region src/integrations/mcp/realtimeBridge.d.ts
509
+ /**
510
+ * Inbound tool-call shape — accepts both LiveKit Agents (`arguments`)
511
+ * and OpenAI Realtime (`arguments_json` string) variants. Other realtime
512
+ * vendors stay flexible because we only look at the canonical fields.
513
+ */
514
+ interface RealtimeToolCall {
515
+ id: string;
516
+ name: string;
517
+ /** Parsed object form. Preferred over `arguments_json`. */
518
+ arguments?: Record<string, unknown>;
519
+ /** JSON-string form (OpenAI Realtime tools). Parsed if `arguments` is absent. */
520
+ arguments_json?: string;
521
+ }
522
+ /** Outbound shape — mirrors js-genai's sendToolResponse. */
523
+ interface RealtimeToolResponse {
524
+ id: string;
525
+ name: string;
526
+ /** Tool result — `content` from the MCP CallToolResult, normalized to plain JSON. */
527
+ response: Record<string, unknown>;
528
+ /** Surfaces tool-side errors so the model can recover. */
529
+ isError?: boolean;
530
+ }
531
+ interface DispatchOptions<TSession = unknown> {
532
+ session?: TSession | null;
533
+ log?: ToolContext<TSession>["log"];
534
+ /** Forwarded into `ctx.extra` for advanced handlers. */
535
+ extra?: Record<string, unknown>;
536
+ }
537
+ /**
538
+ * Dispatch a batch of tool calls through the host's MCP tool registry.
539
+ *
540
+ * Returns the response array in the same order as the input. Each
541
+ * response carries either the tool's `content` (success path) or a
542
+ * `{isError:true, error:{message,...}}` shape (validation failure or
543
+ * handler throw). The realtime client uses the `id` to correlate
544
+ * responses to the original calls.
545
+ */
546
+ declare function dispatchRealtimeToolCall<TSession = unknown>(tools: readonly ToolDefinition[], calls: readonly RealtimeToolCall[], opts?: DispatchOptions<TSession>): Promise<RealtimeToolResponse[]>;
547
+ //#endregion
508
548
  //#region src/integrations/mcp/resourceToTools.d.ts
509
549
  interface ResourceToToolsConfig extends McpResourceConfig {
510
550
  toolNamePrefix?: string;
@@ -578,4 +618,4 @@ declare function permissionDeniedResult(args: {
578
618
  session: McpAuthResult | null;
579
619
  }): CallToolResult;
580
620
  //#endregion
581
- export { type AuthRef, type BetterAuthHandler, type BuildMcpToolsFromBridgesOptions, type CallToolResult, type CreateMcpAuthFromBetterAuthApiKeyOptions, type CreateMcpServerConfig, type CrudDescriptionMeta, type CrudDescriptionOverride, type CrudOperation, type DefinePromptConfig, type DefineToolConfig, type FieldRuleEntry, type FieldRulesToZodOptions, type InvokeControllerOptions, type McpAuthResolver, type McpAuthResult, type McpBridge, type McpGuard, type McpOperation, type McpPluginOptions, type McpResourceConfig, type McpServerInstance, type PromptDefinition, type PromptResult, type ResourceToToolsConfig, type ToolAnnotations, type ToolContext, type ToolDefinition, bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpAuthFromBetterAuthApiKey, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resolveToolCollisions, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
621
+ export { type AuthRef, type BetterAuthHandler, type BuildMcpToolsFromBridgesOptions, type CallToolResult, type CreateMcpAuthFromBetterAuthApiKeyOptions, type CreateMcpServerConfig, type CrudDescriptionMeta, type CrudDescriptionOverride, type CrudOperation, type DefinePromptConfig, type DefineToolConfig, type DispatchOptions, type FieldRuleEntry, type FieldRulesToZodOptions, type InvokeControllerOptions, type McpAuthResolver, type McpAuthResult, type McpBridge, type McpGuard, type McpOperation, type McpPluginOptions, type McpResourceConfig, type McpServerInstance, type PromptDefinition, type PromptResult, type RealtimeToolCall, type RealtimeToolResponse, type ResourceToToolsConfig, type ToolAnnotations, type ToolContext, type ToolDefinition, bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpAuthFromBetterAuthApiKey, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, dispatchRealtimeToolCall, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resolveToolCollisions, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
@@ -1,4 +1,4 @@
1
- import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-C6FViZXw.mjs";
1
+ import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-BrepZ0cV.mjs";
2
2
  //#region src/integrations/mcp/defineTool.ts
3
3
  /**
4
4
  * Define a type-safe MCP tool.
@@ -275,4 +275,104 @@ function definePrompt(name, config) {
275
275
  };
276
276
  }
277
277
  //#endregion
278
- export { bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpAuthFromBetterAuthApiKey, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resolveToolCollisions, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
278
+ //#region src/integrations/mcp/realtimeBridge.ts
279
+ /**
280
+ * Dispatch a batch of tool calls through the host's MCP tool registry.
281
+ *
282
+ * Returns the response array in the same order as the input. Each
283
+ * response carries either the tool's `content` (success path) or a
284
+ * `{isError:true, error:{message,...}}` shape (validation failure or
285
+ * handler throw). The realtime client uses the `id` to correlate
286
+ * responses to the original calls.
287
+ */
288
+ async function dispatchRealtimeToolCall(tools, calls, opts = {}) {
289
+ const byName = new Map(tools.map((t) => [t.name, t]));
290
+ const noopLog = async () => {};
291
+ const baseCtx = {
292
+ session: opts.session ?? null,
293
+ log: opts.log ?? noopLog,
294
+ extra: opts.extra ?? {}
295
+ };
296
+ return (await Promise.allSettled(calls.map((call) => dispatchOne(byName.get(call.name), call, baseCtx)))).map((s, i) => {
297
+ const call = calls[i];
298
+ if (!call) return {
299
+ id: "unknown",
300
+ name: "unknown",
301
+ isError: true,
302
+ response: { error: "missing call slot" }
303
+ };
304
+ if (s.status === "fulfilled") return s.value;
305
+ return {
306
+ id: call.id,
307
+ name: call.name,
308
+ isError: true,
309
+ response: { error: s.reason instanceof Error ? s.reason.message : String(s.reason) }
310
+ };
311
+ });
312
+ }
313
+ async function dispatchOne(def, call, ctx) {
314
+ if (!def) return {
315
+ id: call.id,
316
+ name: call.name,
317
+ isError: true,
318
+ response: { error: `unknown tool: ${call.name}` }
319
+ };
320
+ let parsedArgs = call.arguments ?? {};
321
+ if (!call.arguments && typeof call.arguments_json === "string") try {
322
+ parsedArgs = JSON.parse(call.arguments_json);
323
+ } catch {
324
+ return {
325
+ id: call.id,
326
+ name: call.name,
327
+ isError: true,
328
+ response: { error: "arguments_json is not valid JSON" }
329
+ };
330
+ }
331
+ if (def.inputSchema) {
332
+ const fieldErrors = [];
333
+ const validated = {};
334
+ for (const [key, schema] of Object.entries(def.inputSchema)) {
335
+ const result = schema.safeParse(parsedArgs[key]);
336
+ if (!result.success) fieldErrors.push(`${key}: ${result.error.message}`);
337
+ else validated[key] = result.data;
338
+ }
339
+ if (fieldErrors.length > 0) return {
340
+ id: call.id,
341
+ name: call.name,
342
+ isError: true,
343
+ response: {
344
+ error: "argument validation failed",
345
+ fields: fieldErrors
346
+ }
347
+ };
348
+ parsedArgs = validated;
349
+ }
350
+ const result = await def.handler(parsedArgs, ctx);
351
+ return {
352
+ id: call.id,
353
+ name: call.name,
354
+ isError: result.isError,
355
+ response: normalizeResult(result)
356
+ };
357
+ }
358
+ /**
359
+ * Normalize an MCP `CallToolResult` into a flat JSON object the realtime
360
+ * wire can carry. Prefers structuredContent; falls back to concatenated
361
+ * text content.
362
+ */
363
+ function normalizeResult(result) {
364
+ if (result.structuredContent) return result.structuredContent;
365
+ const texts = [];
366
+ for (const part of result.content) if (part.type === "text" && typeof part.text === "string") texts.push(part.text);
367
+ if (texts.length > 0) {
368
+ const joined = texts.join("");
369
+ try {
370
+ const parsed = JSON.parse(joined);
371
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
372
+ } catch {}
373
+ return { text: joined };
374
+ }
375
+ return {};
376
+ }
377
+ //#endregion
378
+ export { bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpAuthFromBetterAuthApiKey, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, dispatchRealtimeToolCall, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resolveToolCollisions, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
@@ -1,4 +1,4 @@
1
- import { c as McpAuthResult, l as McpPluginOptions } from "../../types-CoMjNmkF.mjs";
1
+ import { c as McpAuthResult, l as McpPluginOptions } from "../../types-DH-7WO64.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
@@ -1,4 +1,4 @@
1
- import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-C6FViZXw.mjs";
1
+ import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-BrepZ0cV.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -238,6 +238,13 @@ declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed",
238
238
  * the run is still active after them.
239
239
  */
240
240
  declare const STREAMLINE_TERMINAL_EVENTS: readonly ["workflow:completed", "workflow:failed", "workflow:cancelled"];
241
+ /**
242
+ * Terminal run STATUSES (distinct from the terminal EVENT names above).
243
+ * Mirrors streamline's `isTerminalState()` — a run in one of these will
244
+ * emit no further bus events, so the SSE handler sends its snapshot and
245
+ * closes immediately rather than holding a connection open on a dead run.
246
+ */
247
+ declare const TERMINAL_RUN_STATUSES: Set<string>;
241
248
  /**
242
249
  * Pluggable streamline integration for Arc.
243
250
  *
@@ -248,4 +255,4 @@ declare const STREAMLINE_TERMINAL_EVENTS: readonly ["workflow:completed", "workf
248
255
  */
249
256
  declare const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions>;
250
257
  //#endregion
251
- export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, streamlinePlugin };
258
+ export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, TERMINAL_RUN_STATUSES, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, streamlinePlugin };
@@ -58,6 +58,17 @@ const STREAMLINE_TERMINAL_EVENTS = [
58
58
  "workflow:failed",
59
59
  "workflow:cancelled"
60
60
  ];
61
+ /**
62
+ * Terminal run STATUSES (distinct from the terminal EVENT names above).
63
+ * Mirrors streamline's `isTerminalState()` — a run in one of these will
64
+ * emit no further bus events, so the SSE handler sends its snapshot and
65
+ * closes immediately rather than holding a connection open on a dead run.
66
+ */
67
+ const TERMINAL_RUN_STATUSES = new Set([
68
+ "done",
69
+ "failed",
70
+ "cancelled"
71
+ ]);
61
72
  const streamlinePluginImpl = async (fastify, options) => {
62
73
  const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, enableStreaming = false, enableHookEndpoint = false, tenantResolver, bypassTenantResolver, permissions: perms } = options;
63
74
  const routeScope = prefix;
@@ -255,15 +266,19 @@ const streamlinePluginImpl = async (fastify, options) => {
255
266
  if (enableStreaming && wf.container?.eventBus) fastify.get(`${routePrefix}/runs/:runId/stream`, { preHandler: authPreHandler }, async (request, reply) => {
256
267
  if (!await checkPerm("get", request)) throw new ForbiddenError();
257
268
  const { runId } = request.params;
258
- if (!await wf.get(runId)) throw new NotFoundError("Workflow run", runId);
269
+ const run = await wf.get(runId);
270
+ if (!run) throw new NotFoundError("Workflow run", runId);
259
271
  reply.raw.writeHead(200, {
260
272
  "Content-Type": "text/event-stream",
261
273
  "Cache-Control": "no-cache",
262
- Connection: "keep-alive"
274
+ Connection: "keep-alive",
275
+ "X-Accel-Buffering": "no"
263
276
  });
277
+ reply.raw.flushHeaders();
264
278
  const terminalEvents = new Set(STREAMLINE_TERMINAL_EVENTS);
265
279
  const listeners = [];
266
280
  let closed = false;
281
+ let heartbeat;
267
282
  const send = (event, data) => {
268
283
  if (closed) return;
269
284
  try {
@@ -275,12 +290,28 @@ const streamlinePluginImpl = async (fastify, options) => {
275
290
  const cleanup = () => {
276
291
  if (closed) return;
277
292
  closed = true;
293
+ if (heartbeat) clearInterval(heartbeat);
278
294
  for (const { event, fn } of listeners) wf.container?.eventBus.off(event, fn);
279
295
  listeners.length = 0;
280
296
  try {
281
297
  reply.raw.end();
282
298
  } catch {}
283
299
  };
300
+ send("workflow:snapshot", run);
301
+ const runStatus = run.status;
302
+ if (runStatus && TERMINAL_RUN_STATUSES.has(runStatus)) {
303
+ cleanup();
304
+ return;
305
+ }
306
+ heartbeat = setInterval(() => {
307
+ if (closed) return;
308
+ try {
309
+ reply.raw.write(`: ping\n\n`);
310
+ } catch {
311
+ cleanup();
312
+ }
313
+ }, 15e3);
314
+ heartbeat.unref?.();
284
315
  for (const eventName of STREAMLINE_BUS_EVENTS) {
285
316
  const fn = (payload) => {
286
317
  const p = payload;
@@ -336,4 +367,4 @@ const streamlinePlugin = fp(streamlinePluginImpl, {
336
367
  fastify: "5.x"
337
368
  });
338
369
  //#endregion
339
- export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_TERMINAL_EVENTS, streamlinePlugin };
370
+ export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_TERMINAL_EVENTS, TERMINAL_RUN_STATUSES, streamlinePlugin };
@@ -0,0 +1,71 @@
1
+ import { c as SerializedEntry, i as Principal, n as FencedWriteOutcome, s as PushRefStore, t as EnvelopeSnapshot } from "../pushref-registry-Bq_-EVsC.mjs";
2
+
3
+ //#region src/integrations/websocket-pushref-redis.d.ts
4
+ /**
5
+ * Minimal `ioredis` shape — typed structurally so this file doesn't
6
+ * take a typecheck-time dep on `ioredis`. The peer dep is declared
7
+ * `optional: true` in package.json; hosts who don't use this subpath
8
+ * never need to install ioredis.
9
+ */
10
+ interface RedisLike {
11
+ get(key: string): Promise<string | null>;
12
+ set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;
13
+ del(key: string): Promise<number>;
14
+ pexpire(key: string, ttlMs: number): Promise<number>;
15
+ scan(cursor: string | number, matchKw: "MATCH", pattern: string, countKw: "COUNT", count: number): Promise<[string, string[]]>;
16
+ eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
17
+ }
18
+ interface RedisPushRefStoreOptions {
19
+ /** Redis key namespace. Default `arc:pushref:`. */
20
+ keyPrefix?: string;
21
+ /**
22
+ * Grace ms added to the Redis-side TTL beyond the registry's logical
23
+ * TTL — protects against clock skew between application and broker.
24
+ * Default 5_000.
25
+ */
26
+ graceMs?: number;
27
+ }
28
+ declare class RedisPushRefStore implements PushRefStore {
29
+ private redis;
30
+ private prefix;
31
+ private graceMs;
32
+ constructor(redis: RedisLike, opts?: RedisPushRefStoreOptions);
33
+ get(pushRef: string): Promise<SerializedEntry | undefined>;
34
+ set(pushRef: string, entry: SerializedEntry): Promise<void>;
35
+ delete(pushRef: string): Promise<void>;
36
+ claim(args: {
37
+ pushRef: string;
38
+ principal: Principal;
39
+ now: number;
40
+ ttlMs: number;
41
+ mintFresh: () => SerializedEntry;
42
+ maxEntries: number;
43
+ countNonExpired: () => Promise<number>;
44
+ }): Promise<{
45
+ outcome: "new" | "resumed" | "rejected";
46
+ entry?: SerializedEntry;
47
+ supersededActive?: boolean;
48
+ }>;
49
+ releaseIfGen(args: {
50
+ pushRef: string;
51
+ expectedGeneration: number;
52
+ snapshot?: EnvelopeSnapshot;
53
+ expiresAt: number;
54
+ }): Promise<FencedWriteOutcome>;
55
+ persistIfGen(args: {
56
+ pushRef: string;
57
+ expectedGeneration: number;
58
+ snapshot: EnvelopeSnapshot;
59
+ expiresAt: number;
60
+ }): Promise<FencedWriteOutcome>;
61
+ stageIfInactive(args: {
62
+ pushRef: string;
63
+ payload: unknown;
64
+ timestamp: number;
65
+ expiresAt: number;
66
+ }): Promise<"ok" | "missing" | "active" | "raw">;
67
+ gc(_now: number): Promise<number>;
68
+ size(): Promise<number>;
69
+ }
70
+ //#endregion
71
+ export { RedisLike, RedisPushRefStore, RedisPushRefStoreOptions };
@@ -0,0 +1,205 @@
1
+ //#region src/integrations/websocket-pushref-redis.ts
2
+ /**
3
+ * Lua script for atomic claim. Runs server-side, single-roundtrip.
4
+ *
5
+ * KEYS[1] = key
6
+ * ARGV[1] = principal
7
+ * ARGV[2] = now (ms)
8
+ * ARGV[3] = ttlMs
9
+ * ARGV[4] = mintedJson (serialized SerializedEntry for new-claim path)
10
+ * ARGV[5] = graceMs (added to PEXPIRE so the broker GC's stale keys)
11
+ *
12
+ * Returns: { outcome, json, supersededActive } where
13
+ * outcome ∈ {"new","resumed","rejected"}
14
+ * json is the SerializedEntry (or empty string for reject)
15
+ * supersededActive is "1" when the previous entry was still active
16
+ *
17
+ * Generation handling: every resumed claim bumps `entry.generation` so
18
+ * the connection layer can fence the previous owner. The bump happens
19
+ * atomically inside the script — no read-modify-write race across nodes.
20
+ */
21
+ const CLAIM_SCRIPT = `
22
+ local existing = redis.call('GET', KEYS[1])
23
+ local now = tonumber(ARGV[2])
24
+ local ttl = tonumber(ARGV[3])
25
+ local pexpire = ttl + tonumber(ARGV[5])
26
+ if existing then
27
+ local entry = cjson.decode(existing)
28
+ if entry.active == false and entry.expiresAt <= now then
29
+ -- expired — fall through to new-claim path
30
+ else
31
+ if entry.principal ~= ARGV[1] then
32
+ return {'rejected', '', '0'}
33
+ end
34
+ local superseded = '0'
35
+ if entry.active == true then
36
+ superseded = '1'
37
+ end
38
+ entry.active = true
39
+ entry.expiresAt = now + ttl
40
+ entry.generation = (entry.generation or 0) + 1
41
+ local payload = cjson.encode(entry)
42
+ redis.call('SET', KEYS[1], payload, 'PX', pexpire)
43
+ return {'resumed', payload, superseded}
44
+ end
45
+ end
46
+ redis.call('SET', KEYS[1], ARGV[4], 'PX', pexpire)
47
+ return {'new', ARGV[4], '0'}
48
+ `;
49
+ /**
50
+ * Generation-fenced release. Single Lua roundtrip:
51
+ *
52
+ * KEYS[1] = key
53
+ * ARGV[1] = expectedGeneration
54
+ * ARGV[2] = expiresAt (ms epoch)
55
+ * ARGV[3] = pexpire ms (relative)
56
+ * ARGV[4] = snapshotJson or empty string (skip envelope flush when empty)
57
+ *
58
+ * Returns "ok" | "stale" | "missing".
59
+ *
60
+ * Stale outcome means another node has claimed a higher generation
61
+ * since the caller captured theirs — caller's release is dropped at
62
+ * the store level. This closes the 2.17.2 race where a fenced Node A's
63
+ * close handler could mutate Node B's freshly-claimed entry.
64
+ */
65
+ const RELEASE_IF_GEN_SCRIPT = `
66
+ local existing = redis.call('GET', KEYS[1])
67
+ if not existing then return 'missing' end
68
+ local entry = cjson.decode(existing)
69
+ local expected = tonumber(ARGV[1])
70
+ if entry.generation ~= expected then return 'stale' end
71
+ entry.active = false
72
+ entry.expiresAt = tonumber(ARGV[2])
73
+ if ARGV[4] ~= '' then
74
+ local snap = cjson.decode(ARGV[4])
75
+ entry.nextSeq = snap.nextSeq
76
+ entry.deadQueue = snap.deadQueue
77
+ end
78
+ redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[3]))
79
+ return 'ok'
80
+ `;
81
+ /**
82
+ * Atomic offline-stage. Closes the read-modify-write race the 2.17.2
83
+ * review surfaced for `stageForResume` — a get-then-set could clobber
84
+ * a freshly-claimed higher-generation entry with the stale inactive
85
+ * snapshot. The whole sequence (test active + seq mode, assign seq,
86
+ * wrap payload, append, trim, extend TTL) runs in one EVAL.
87
+ *
88
+ * KEYS[1] = key
89
+ * ARGV[1] = payloadJson (raw payload, NOT pre-wrapped)
90
+ * ARGV[2] = timestamp (ms, server clock)
91
+ * ARGV[3] = expiresAt (ms epoch)
92
+ * ARGV[4] = pexpire ms
93
+ *
94
+ * Returns "ok" | "missing" | "active" | "raw".
95
+ *
96
+ * Note on JSON key order: cjson.encode and JSON.stringify may emit
97
+ * different orderings; consumers parse the envelope and read by key,
98
+ * so order is not part of the wire contract.
99
+ */
100
+ const STAGE_IF_INACTIVE_SCRIPT = `
101
+ local existing = redis.call('GET', KEYS[1])
102
+ if not existing then return 'missing' end
103
+ local entry = cjson.decode(existing)
104
+ if entry.active then return 'active' end
105
+ if entry.envelopeMode ~= 'seq' then return 'raw' end
106
+ local seq = entry.nextSeq
107
+ entry.nextSeq = seq + 1
108
+ local payload = cjson.decode(ARGV[1])
109
+ local envelope = { seq = seq, t = tonumber(ARGV[2]), msg = payload }
110
+ local serialized = cjson.encode(envelope)
111
+ if not entry.deadQueue then entry.deadQueue = {} end
112
+ table.insert(entry.deadQueue, { seq = seq, payload = serialized })
113
+ while #entry.deadQueue > entry.deadQueueSize do
114
+ table.remove(entry.deadQueue, 1)
115
+ end
116
+ entry.expiresAt = tonumber(ARGV[3])
117
+ redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[4]))
118
+ return 'ok'
119
+ `;
120
+ /**
121
+ * Generation-fenced persist. Same atomicity contract as release.
122
+ *
123
+ * KEYS[1] = key
124
+ * ARGV[1] = expectedGeneration
125
+ * ARGV[2] = expiresAt
126
+ * ARGV[3] = pexpire ms
127
+ * ARGV[4] = snapshotJson (required for persist — non-empty)
128
+ */
129
+ const PERSIST_IF_GEN_SCRIPT = `
130
+ local existing = redis.call('GET', KEYS[1])
131
+ if not existing then return 'missing' end
132
+ local entry = cjson.decode(existing)
133
+ local expected = tonumber(ARGV[1])
134
+ if entry.generation ~= expected then return 'stale' end
135
+ local snap = cjson.decode(ARGV[4])
136
+ entry.nextSeq = snap.nextSeq
137
+ entry.deadQueue = snap.deadQueue
138
+ entry.expiresAt = tonumber(ARGV[2])
139
+ redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[3]))
140
+ return 'ok'
141
+ `;
142
+ var RedisPushRefStore = class {
143
+ redis;
144
+ prefix;
145
+ graceMs;
146
+ constructor(redis, opts = {}) {
147
+ this.redis = redis;
148
+ this.prefix = opts.keyPrefix ?? "arc:pushref:";
149
+ this.graceMs = opts.graceMs ?? 5e3;
150
+ }
151
+ async get(pushRef) {
152
+ const raw = await this.redis.get(this.prefix + pushRef);
153
+ if (!raw) return void 0;
154
+ return JSON.parse(raw);
155
+ }
156
+ async set(pushRef, entry) {
157
+ const json = JSON.stringify(entry);
158
+ const pexpire = Math.max(1e3, entry.expiresAt - Date.now() + this.graceMs);
159
+ await this.redis.set(this.prefix + pushRef, json, "PX", pexpire);
160
+ }
161
+ async delete(pushRef) {
162
+ await this.redis.del(this.prefix + pushRef);
163
+ }
164
+ async claim(args) {
165
+ if (args.maxEntries > 0) {
166
+ if (await args.countNonExpired() >= args.maxEntries) {}
167
+ }
168
+ const minted = JSON.stringify(args.mintFresh());
169
+ const result = await this.redis.eval(CLAIM_SCRIPT, 1, this.prefix + args.pushRef, args.principal, args.now, args.ttlMs, minted, this.graceMs);
170
+ const outcome = result[0];
171
+ if (outcome === "rejected") return { outcome };
172
+ return {
173
+ outcome,
174
+ entry: JSON.parse(result[1]),
175
+ supersededActive: result[2] === "1"
176
+ };
177
+ }
178
+ async releaseIfGen(args) {
179
+ const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
180
+ return await this.redis.eval(RELEASE_IF_GEN_SCRIPT, 1, this.prefix + args.pushRef, args.expectedGeneration, args.expiresAt, pexpire, args.snapshot ? JSON.stringify(args.snapshot) : "");
181
+ }
182
+ async persistIfGen(args) {
183
+ const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
184
+ return await this.redis.eval(PERSIST_IF_GEN_SCRIPT, 1, this.prefix + args.pushRef, args.expectedGeneration, args.expiresAt, pexpire, JSON.stringify(args.snapshot));
185
+ }
186
+ async stageIfInactive(args) {
187
+ const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
188
+ return await this.redis.eval(STAGE_IF_INACTIVE_SCRIPT, 1, this.prefix + args.pushRef, JSON.stringify(args.payload), args.timestamp, args.expiresAt, pexpire);
189
+ }
190
+ async gc(_now) {
191
+ return 0;
192
+ }
193
+ async size() {
194
+ let cursor = "0";
195
+ let count = 0;
196
+ do {
197
+ const [next, keys] = await this.redis.scan(cursor, "MATCH", `${this.prefix}*`, "COUNT", 500);
198
+ count += keys.length;
199
+ cursor = next;
200
+ } while (cursor !== "0");
201
+ return count;
202
+ }
203
+ };
204
+ //#endregion
205
+ export { RedisPushRefStore };
@@ -1,4 +1,4 @@
1
- import { c as WebSocketAdapter } from "../websocket-BJ0w-3Dt.mjs";
1
+ import { f as WebSocketAdapter } from "../websocket-BZMUP7Gr.mjs";
2
2
 
3
3
  //#region src/integrations/websocket-redis.d.ts
4
4
  interface RedisLike {
@@ -1,2 +1,3 @@
1
- import { a as WebSocketMessage, c as WebSocketAdapter, i as WebSocketClient, n as websocketPlugin, o as WebSocketPluginOptions, r as AuthResult, s as LocalWebSocketAdapter, t as RoomManager } from "../websocket-BJ0w-3Dt.mjs";
2
- export { AuthResult, LocalWebSocketAdapter, RoomManager, WebSocketAdapter, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin };
1
+ import { a as GeoRoomOptions, c as WebSocketClient, d as LocalWebSocketAdapter, f as WebSocketAdapter, i as GeoRoom, l as WebSocketMessage, n as GeoPoint, o as RoomManager, r as GeoPublishOpts, s as AuthResult, t as websocketPlugin, u as WebSocketPluginOptions } from "../websocket-BZMUP7Gr.mjs";
2
+ import { a as PushRefRegistry, c as SerializedEntry, i as Principal, o as PushRefRegistryOptions, r as MemoryPushRefStore, s as PushRefStore } from "../pushref-registry-Bq_-EVsC.mjs";
3
+ export { AuthResult, GeoPoint, GeoPublishOpts, GeoRoom, GeoRoomOptions, LocalWebSocketAdapter, MemoryPushRefStore, Principal, PushRefRegistry, PushRefRegistryOptions, PushRefStore, RoomManager, SerializedEntry, WebSocketAdapter, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin };