@wingman-ai/gateway 0.2.5 → 0.3.0

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 (90) hide show
  1. package/.wingman/agents/coding/agent.md +5 -0
  2. package/.wingman/agents/coding-v2/agent.md +58 -0
  3. package/.wingman/agents/game-dev/agent.md +94 -0
  4. package/.wingman/agents/game-dev/art-generation.md +37 -0
  5. package/.wingman/agents/game-dev/asset-refinement.md +17 -0
  6. package/.wingman/agents/game-dev/planning-idea.md +17 -0
  7. package/.wingman/agents/game-dev/ui-specialist.md +17 -0
  8. package/.wingman/agents/main/agent.md +2 -0
  9. package/README.md +1 -0
  10. package/dist/agent/config/agentConfig.d.ts +4 -0
  11. package/dist/agent/config/mcpClientManager.cjs +44 -10
  12. package/dist/agent/config/mcpClientManager.d.ts +6 -2
  13. package/dist/agent/config/mcpClientManager.js +44 -10
  14. package/dist/agent/config/toolRegistry.cjs +3 -1
  15. package/dist/agent/config/toolRegistry.js +3 -1
  16. package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
  17. package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
  18. package/dist/agent/tests/mcpClientManager.test.js +118 -0
  19. package/dist/agent/tools/command_execute.cjs +1 -1
  20. package/dist/agent/tools/command_execute.js +1 -1
  21. package/dist/cli/config/schema.d.ts +2 -0
  22. package/dist/cli/core/agentInvoker.cjs +55 -66
  23. package/dist/cli/core/agentInvoker.d.ts +10 -13
  24. package/dist/cli/core/agentInvoker.js +42 -62
  25. package/dist/cli/core/imagePersistence.cjs +125 -0
  26. package/dist/cli/core/imagePersistence.d.ts +24 -0
  27. package/dist/cli/core/imagePersistence.js +85 -0
  28. package/dist/cli/core/sessionManager.cjs +297 -40
  29. package/dist/cli/core/sessionManager.d.ts +9 -0
  30. package/dist/cli/core/sessionManager.js +297 -40
  31. package/dist/debug/terminalProbe.cjs +57 -0
  32. package/dist/debug/terminalProbe.d.ts +10 -0
  33. package/dist/debug/terminalProbe.js +20 -0
  34. package/dist/debug/terminalProbeAuth.cjs +140 -0
  35. package/dist/debug/terminalProbeAuth.d.ts +20 -0
  36. package/dist/debug/terminalProbeAuth.js +97 -0
  37. package/dist/gateway/http/fs.cjs +19 -0
  38. package/dist/gateway/http/fs.js +19 -0
  39. package/dist/gateway/http/sessions.cjs +25 -5
  40. package/dist/gateway/http/sessions.js +25 -5
  41. package/dist/gateway/server.cjs +112 -11
  42. package/dist/gateway/server.d.ts +2 -0
  43. package/dist/gateway/server.js +112 -11
  44. package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
  45. package/dist/tests/agentInvokerSummarization.test.js +58 -39
  46. package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
  47. package/dist/tests/agentInvokerWorkdir.test.js +52 -2
  48. package/dist/tests/cli-init.test.cjs +36 -0
  49. package/dist/tests/cli-init.test.js +36 -0
  50. package/dist/tests/falRuntime.test.cjs +78 -0
  51. package/dist/tests/falRuntime.test.d.ts +1 -0
  52. package/dist/tests/falRuntime.test.js +72 -0
  53. package/dist/tests/falSummary.test.cjs +51 -0
  54. package/dist/tests/falSummary.test.d.ts +1 -0
  55. package/dist/tests/falSummary.test.js +45 -0
  56. package/dist/tests/gateway.test.cjs +109 -1
  57. package/dist/tests/gateway.test.js +109 -1
  58. package/dist/tests/imagePersistence.test.cjs +143 -0
  59. package/dist/tests/imagePersistence.test.d.ts +1 -0
  60. package/dist/tests/imagePersistence.test.js +137 -0
  61. package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
  62. package/dist/tests/sessionMessageAttachments.test.js +30 -0
  63. package/dist/tests/sessionStateMessages.test.cjs +126 -0
  64. package/dist/tests/sessionStateMessages.test.js +126 -0
  65. package/dist/tests/sessions-api.test.cjs +117 -3
  66. package/dist/tests/sessions-api.test.js +118 -4
  67. package/dist/tests/terminalProbe.test.cjs +45 -0
  68. package/dist/tests/terminalProbe.test.d.ts +1 -0
  69. package/dist/tests/terminalProbe.test.js +39 -0
  70. package/dist/tests/terminalProbeAuth.test.cjs +85 -0
  71. package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
  72. package/dist/tests/terminalProbeAuth.test.js +79 -0
  73. package/dist/tools/fal/runtime.cjs +103 -0
  74. package/dist/tools/fal/runtime.d.ts +10 -0
  75. package/dist/tools/fal/runtime.js +60 -0
  76. package/dist/tools/fal/summary.cjs +78 -0
  77. package/dist/tools/fal/summary.d.ts +22 -0
  78. package/dist/tools/fal/summary.js +41 -0
  79. package/dist/tools/mcp-fal-ai.cjs +1041 -0
  80. package/dist/tools/mcp-fal-ai.d.ts +1 -0
  81. package/dist/tools/mcp-fal-ai.js +1025 -0
  82. package/dist/types/mcp.cjs +2 -0
  83. package/dist/types/mcp.d.ts +8 -0
  84. package/dist/types/mcp.js +3 -1
  85. package/dist/webui/assets/index-0nUBsUUq.js +278 -0
  86. package/dist/webui/assets/index-kk7OrD-G.css +11 -0
  87. package/dist/webui/index.html +2 -2
  88. package/package.json +11 -8
  89. package/dist/webui/assets/index-C7EuTbnE.js +0 -270
  90. package/dist/webui/assets/index-DVWQluit.css +0 -11
@@ -24,6 +24,9 @@ function _define_property(obj, key, value) {
24
24
  }
25
25
  const WORKDIR_VIRTUAL_PATH = "/workdir/";
26
26
  const OUTPUT_VIRTUAL_PATH = "/output/";
27
+ const AGENTS_MEMORY_VIRTUAL_PATHS = [
28
+ "/AGENTS.md"
29
+ ];
27
30
  const DEFAULT_DEEPAGENT_MODEL = "claude-sonnet-4-5-20250929";
28
31
  const isPathWithinRoot = (targetPath, rootPath)=>{
29
32
  const normalizedTarget = normalize(targetPath);
@@ -35,6 +38,11 @@ const resolveExecutionWorkspace = (workspace, workdir)=>{
35
38
  if (isAbsolute(workdir)) return normalize(workdir);
36
39
  return normalize(join(workspace, workdir));
37
40
  };
41
+ const resolveAgentExecutionWorkspace = (workspace, workdir, defaultOutputDir)=>{
42
+ const preferredWorkdir = workdir || defaultOutputDir || null;
43
+ return resolveExecutionWorkspace(workspace, preferredWorkdir);
44
+ };
45
+ const resolveAgentMemorySources = (executionWorkspace)=>AGENTS_MEMORY_VIRTUAL_PATHS.filter((memoryPath)=>existsSync(join(executionWorkspace, memoryPath.replace(/^\/+/, ""))));
38
46
  const toWorkspaceAliasVirtualPath = (absolutePath)=>{
39
47
  const normalized = normalize(absolutePath);
40
48
  if (!isAbsolute(normalized)) return null;
@@ -238,24 +246,19 @@ const trackRootLangGraphRunId = (currentRootLangGraphRunId, chunk)=>{
238
246
  return extractEventRunId(eventRecord) || currentRootLangGraphRunId;
239
247
  };
240
248
  const isRootLangGraphTerminalEvent = (chunk, rootLangGraphRunId)=>{
241
- if (!rootLangGraphRunId) return false;
242
249
  const eventRecord = extractStreamEventRecord(chunk);
243
250
  if (!eventRecord || !isRootLangGraphChainEvent(eventRecord, "on_chain_end")) return false;
251
+ if (!rootLangGraphRunId) return true;
244
252
  const chunkRunId = extractEventRunId(eventRecord);
253
+ if (!chunkRunId) return true;
245
254
  return Boolean(chunkRunId && chunkRunId === rootLangGraphRunId);
246
255
  };
247
- const evaluateStreamingCompletion = (input)=>{
248
- if (!input.sawAssistantText && !input.fallbackText) {
249
- const message = input.streamErrorMessage ? `Model call failed: ${input.streamErrorMessage}` : "Model completed without a response. Check provider logs for request errors.";
250
- return {
251
- status: "blocked",
252
- reason: input.streamErrorMessage ? "stream_error" : "empty_stream_response",
253
- message
254
- };
255
- }
256
- return {
257
- status: "ok"
258
- };
256
+ const emitCompletionAndContinuePostProcessing = (input)=>{
257
+ input.outputManager.emitAgentComplete(input.result);
258
+ if (!input.postProcess) return;
259
+ input.postProcess().catch((error)=>{
260
+ input.logger?.debug("Failed post-completion processing for streamed agent response", error);
261
+ });
259
262
  };
260
263
  class AgentInvoker {
261
264
  findAllAgents() {
@@ -269,14 +272,11 @@ class AgentInvoker {
269
272
  let cancellationHandled = false;
270
273
  let activeToolName = null;
271
274
  let lastToolName = null;
272
- let sawAssistantText = false;
273
- let streamErrorMessage;
274
275
  let rootLangGraphRunId;
275
- let preInvocationMessages = null;
276
276
  const isCancelled = ()=>options?.signal?.aborted === true;
277
277
  try {
278
278
  const hookSessionId = sessionId || v4();
279
- const executionWorkspace = resolveExecutionWorkspace(this.workspace, this.workdir);
279
+ const executionWorkspace = resolveAgentExecutionWorkspace(this.workspace, this.workdir, this.defaultOutputDir);
280
280
  const effectiveWorkdir = this.workdir ? executionWorkspace : null;
281
281
  const loader = new AgentLoader(this.configDir, this.workspace, this.wingmanConfig, executionWorkspace, {
282
282
  terminalOwnerId: `${agentName}:${hookSessionId}`,
@@ -295,7 +295,9 @@ class AgentInvoker {
295
295
  if (targetAgent.mcpUseGlobal && this.wingmanConfig.mcp) mcpConfigs.push(this.wingmanConfig.mcp);
296
296
  if (mcpConfigs.length > 0) {
297
297
  this.logger.debug("Initializing MCP client for agent invocation");
298
- this.mcpManager = new MCPClientManager(mcpConfigs, this.logger);
298
+ this.mcpManager = new MCPClientManager(mcpConfigs, this.logger, {
299
+ executionWorkspace
300
+ });
299
301
  await this.mcpManager.initialize();
300
302
  const mcpTools = await this.mcpManager.getTools();
301
303
  if (mcpTools.length > 0) {
@@ -312,6 +314,7 @@ class AgentInvoker {
312
314
  const normalizedSkillsDirectory = skillsDirectory.replace(/^\/+|\/+$/g, "");
313
315
  const skillsVirtualPath = `/${normalizedSkillsDirectory}/`;
314
316
  const outputMount = resolveExternalOutputMount(executionWorkspace, effectiveWorkdir, this.defaultOutputDir);
317
+ const memorySources = resolveAgentMemorySources(executionWorkspace);
315
318
  const middleware = [
316
319
  mediaCompatibilityMiddleware({
317
320
  model: targetAgent.model
@@ -396,6 +399,7 @@ class AgentInvoker {
396
399
  middleware: middleware,
397
400
  interruptOn: hitlSettings?.interruptOn,
398
401
  skills: skillsSources,
402
+ memory: memorySources,
399
403
  subagents: targetAgent.subagents || [],
400
404
  checkpointer: checkpointer
401
405
  });
@@ -404,15 +408,6 @@ class AgentInvoker {
404
408
  const userContent = buildUserContent(prompt, attachments, targetAgent.model);
405
409
  if (this.sessionManager && sessionId) {
406
410
  this.logger.debug(`Using streaming with session: ${sessionId}`);
407
- try {
408
- const messages = await this.sessionManager.listMessages(sessionId);
409
- preInvocationMessages = messages.map((message)=>({
410
- role: message.role,
411
- content: message.content
412
- }));
413
- } catch (stateError) {
414
- this.logger.debug("Failed to capture pre-invocation session state", stateError);
415
- }
416
411
  const stream = await standaloneAgent.streamEvents({
417
412
  messages: [
418
413
  {
@@ -430,8 +425,6 @@ class AgentInvoker {
430
425
  });
431
426
  for await (const chunk of stream){
432
427
  rootLangGraphRunId = trackRootLangGraphRunId(rootLangGraphRunId, chunk);
433
- if (!sawAssistantText && chunkHasAssistantText(chunk)) sawAssistantText = true;
434
- if (!streamErrorMessage) streamErrorMessage = detectStreamErrorMessage(chunk);
435
428
  const toolEvent = detectToolEventContext(chunk);
436
429
  if (toolEvent) {
437
430
  lastToolName = toolEvent.toolName;
@@ -461,37 +454,15 @@ class AgentInvoker {
461
454
  cancelled: true
462
455
  };
463
456
  }
464
- let fallbackText;
465
- if (!sawAssistantText && this.sessionManager && sessionId && preInvocationMessages) try {
466
- const sessionMessages = await this.sessionManager.listMessages(sessionId);
467
- fallbackText = selectStreamingFallbackText(preInvocationMessages, sessionMessages.map((message)=>({
468
- role: message.role,
469
- content: message.content
470
- })));
471
- } catch (stateError) {
472
- this.logger.debug("Failed to derive streaming fallback text from session state", stateError);
473
- }
474
- const completionOutcome = evaluateStreamingCompletion({
475
- sawAssistantText,
476
- fallbackText,
477
- streamErrorMessage
478
- });
479
- if ("blocked" === completionOutcome.status) {
480
- this.logger.warn(completionOutcome.message);
481
- this.outputManager.emitAgentError(completionOutcome.message);
482
- return {
483
- blocked: true,
484
- reason: completionOutcome.reason
485
- };
486
- }
487
- this.logger.info("Agent streaming completed successfully", {
488
- usedFallbackText: Boolean(fallbackText)
489
- });
490
- this.outputManager.emitAgentComplete({
491
- streaming: true,
492
- ...fallbackText ? {
493
- fallbackText
494
- } : {}
457
+ this.logger.info("Agent streaming completed successfully");
458
+ const completionPayload = {
459
+ streaming: true
460
+ };
461
+ emitCompletionAndContinuePostProcessing({
462
+ outputManager: this.outputManager,
463
+ result: completionPayload,
464
+ postProcess: ()=>this.materializeSessionImages(sessionId),
465
+ logger: this.logger
495
466
  });
496
467
  return {
497
468
  streaming: true
@@ -527,7 +498,12 @@ class AgentInvoker {
527
498
  };
528
499
  }
529
500
  this.logger.info("Agent completed successfully");
530
- this.outputManager.emitAgentComplete(result);
501
+ emitCompletionAndContinuePostProcessing({
502
+ outputManager: this.outputManager,
503
+ result,
504
+ postProcess: ()=>this.materializeSessionImages(sessionId),
505
+ logger: this.logger
506
+ });
531
507
  return result;
532
508
  }
533
509
  } catch (error) {
@@ -559,6 +535,10 @@ class AgentInvoker {
559
535
  description: a.description
560
536
  }));
561
537
  }
538
+ async materializeSessionImages(sessionId) {
539
+ if (!this.sessionManager || !sessionId) return;
540
+ await this.sessionManager.listMessages(sessionId);
541
+ }
562
542
  constructor(options){
563
543
  _define_property(this, "loader", void 0);
564
544
  _define_property(this, "outputManager", void 0);
@@ -769,4 +749,4 @@ function buildAttachmentPreview(attachments) {
769
749
  if (hasImage) return "[image]";
770
750
  return "";
771
751
  }
772
- export { AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext, evaluateStreamingCompletion, isRootLangGraphTerminalEvent, resolveExecutionWorkspace, resolveExternalOutputMount, resolveHumanInTheLoopSettings, resolveModelRetryMiddlewareSettings, resolveSummarizationMiddlewareSettings, resolveToolRetryMiddlewareSettings, selectStreamingFallbackText, toWorkspaceAliasVirtualPath, trackRootLangGraphRunId };
752
+ export { AGENTS_MEMORY_VIRTUAL_PATHS, AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, chunkHasAssistantText, configureDeepAgentSummarizationMiddleware, detectStreamErrorMessage, detectToolEventContext, emitCompletionAndContinuePostProcessing, isRootLangGraphTerminalEvent, resolveAgentExecutionWorkspace, resolveAgentMemorySources, resolveExecutionWorkspace, resolveExternalOutputMount, resolveHumanInTheLoopSettings, resolveModelRetryMiddlewareSettings, resolveSummarizationMiddlewareSettings, resolveToolRetryMiddlewareSettings, selectStreamingFallbackText, toWorkspaceAliasVirtualPath, trackRootLangGraphRunId };
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ persistAssistantImagesToDisk: ()=>persistAssistantImagesToDisk,
28
+ parseBase64DataUrl: ()=>parseBase64DataUrl,
29
+ resolveImageExtension: ()=>resolveImageExtension
30
+ });
31
+ const external_node_crypto_namespaceObject = require("node:crypto");
32
+ const external_node_fs_namespaceObject = require("node:fs");
33
+ const external_node_path_namespaceObject = require("node:path");
34
+ const DATA_URL_BASE64_PATTERN = /^data:([^;,]+);base64,(.+)$/i;
35
+ function persistAssistantImagesToDisk(input) {
36
+ if (!input.messages.length) return;
37
+ const mediaRoot = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(input.dbPath), "media", sanitizePathSegment(input.sessionId));
38
+ for (const message of input.messages)if ("assistant" === message.role) {
39
+ if (Array.isArray(message.attachments) && 0 !== message.attachments.length) for (const attachment of message.attachments){
40
+ if (!attachment || "image" !== attachment.kind) continue;
41
+ if (attachment.path) continue;
42
+ const parsed = parseBase64DataUrl(attachment.dataUrl);
43
+ if (!parsed) continue;
44
+ if (!parsed.mimeType.toLowerCase().startsWith("image/")) continue;
45
+ let bytes;
46
+ try {
47
+ bytes = Buffer.from(parsed.data, "base64");
48
+ } catch {
49
+ continue;
50
+ }
51
+ if (0 === bytes.length) continue;
52
+ const extension = resolveImageExtension(parsed.mimeType);
53
+ const hash = (0, external_node_crypto_namespaceObject.createHash)("sha256").update(bytes).digest("hex").slice(0, 20);
54
+ const filename = `${hash}.${extension}`;
55
+ const outputPath = (0, external_node_path_namespaceObject.join)(mediaRoot, filename);
56
+ if (!(0, external_node_fs_namespaceObject.existsSync)(outputPath)) {
57
+ (0, external_node_fs_namespaceObject.mkdirSync)(mediaRoot, {
58
+ recursive: true
59
+ });
60
+ (0, external_node_fs_namespaceObject.writeFileSync)(outputPath, bytes);
61
+ }
62
+ attachment.path = outputPath;
63
+ if (!attachment.mimeType) attachment.mimeType = parsed.mimeType;
64
+ if ("number" != typeof attachment.size || attachment.size <= 0) attachment.size = bytes.length;
65
+ if (!attachment.name) attachment.name = `image-${hash.slice(0, 8)}.${extension}`;
66
+ }
67
+ }
68
+ }
69
+ function parseBase64DataUrl(dataUrl) {
70
+ if ("string" != typeof dataUrl) return null;
71
+ const match = dataUrl.match(DATA_URL_BASE64_PATTERN);
72
+ if (!match) return null;
73
+ return {
74
+ mimeType: match[1].trim().toLowerCase(),
75
+ data: match[2].trim()
76
+ };
77
+ }
78
+ function resolveImageExtension(mimeType) {
79
+ const normalized = (mimeType || "").trim().toLowerCase();
80
+ switch(normalized){
81
+ case "image/jpeg":
82
+ return "jpg";
83
+ case "image/png":
84
+ return "png";
85
+ case "image/webp":
86
+ return "webp";
87
+ case "image/gif":
88
+ return "gif";
89
+ case "image/svg+xml":
90
+ return "svg";
91
+ case "image/bmp":
92
+ return "bmp";
93
+ case "image/tiff":
94
+ return "tiff";
95
+ case "image/heic":
96
+ return "heic";
97
+ case "image/heif":
98
+ return "heif";
99
+ case "image/avif":
100
+ return "avif";
101
+ default:
102
+ {
103
+ const subtype = normalized.split("/")[1] || "";
104
+ const sanitized = subtype.replace(/[^a-z0-9]/g, "");
105
+ return sanitized || "img";
106
+ }
107
+ }
108
+ }
109
+ function sanitizePathSegment(value) {
110
+ const normalized = (value || "").trim();
111
+ if (!normalized) return "default-session";
112
+ const sanitized = normalized.replace(/[^a-zA-Z0-9._-]/g, "_");
113
+ return sanitized.slice(0, 120) || "default-session";
114
+ }
115
+ exports.parseBase64DataUrl = __webpack_exports__.parseBase64DataUrl;
116
+ exports.persistAssistantImagesToDisk = __webpack_exports__.persistAssistantImagesToDisk;
117
+ exports.resolveImageExtension = __webpack_exports__.resolveImageExtension;
118
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
119
+ "parseBase64DataUrl",
120
+ "persistAssistantImagesToDisk",
121
+ "resolveImageExtension"
122
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
123
+ Object.defineProperty(exports, '__esModule', {
124
+ value: true
125
+ });
@@ -0,0 +1,24 @@
1
+ export interface PersistableAttachment {
2
+ kind: "image" | "audio" | "file";
3
+ dataUrl: string;
4
+ mimeType?: string;
5
+ name?: string;
6
+ size?: number;
7
+ path?: string;
8
+ }
9
+ export interface PersistableMessage {
10
+ role: "user" | "assistant";
11
+ attachments?: PersistableAttachment[];
12
+ }
13
+ type ParsedDataUrl = {
14
+ mimeType: string;
15
+ data: string;
16
+ };
17
+ export declare function persistAssistantImagesToDisk(input: {
18
+ dbPath: string;
19
+ sessionId: string;
20
+ messages: PersistableMessage[];
21
+ }): void;
22
+ export declare function parseBase64DataUrl(dataUrl: string): ParsedDataUrl | null;
23
+ export declare function resolveImageExtension(mimeType: string): string;
24
+ export {};
@@ -0,0 +1,85 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ const DATA_URL_BASE64_PATTERN = /^data:([^;,]+);base64,(.+)$/i;
5
+ function persistAssistantImagesToDisk(input) {
6
+ if (!input.messages.length) return;
7
+ const mediaRoot = join(dirname(input.dbPath), "media", sanitizePathSegment(input.sessionId));
8
+ for (const message of input.messages)if ("assistant" === message.role) {
9
+ if (Array.isArray(message.attachments) && 0 !== message.attachments.length) for (const attachment of message.attachments){
10
+ if (!attachment || "image" !== attachment.kind) continue;
11
+ if (attachment.path) continue;
12
+ const parsed = parseBase64DataUrl(attachment.dataUrl);
13
+ if (!parsed) continue;
14
+ if (!parsed.mimeType.toLowerCase().startsWith("image/")) continue;
15
+ let bytes;
16
+ try {
17
+ bytes = Buffer.from(parsed.data, "base64");
18
+ } catch {
19
+ continue;
20
+ }
21
+ if (0 === bytes.length) continue;
22
+ const extension = resolveImageExtension(parsed.mimeType);
23
+ const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 20);
24
+ const filename = `${hash}.${extension}`;
25
+ const outputPath = join(mediaRoot, filename);
26
+ if (!existsSync(outputPath)) {
27
+ mkdirSync(mediaRoot, {
28
+ recursive: true
29
+ });
30
+ writeFileSync(outputPath, bytes);
31
+ }
32
+ attachment.path = outputPath;
33
+ if (!attachment.mimeType) attachment.mimeType = parsed.mimeType;
34
+ if ("number" != typeof attachment.size || attachment.size <= 0) attachment.size = bytes.length;
35
+ if (!attachment.name) attachment.name = `image-${hash.slice(0, 8)}.${extension}`;
36
+ }
37
+ }
38
+ }
39
+ function parseBase64DataUrl(dataUrl) {
40
+ if ("string" != typeof dataUrl) return null;
41
+ const match = dataUrl.match(DATA_URL_BASE64_PATTERN);
42
+ if (!match) return null;
43
+ return {
44
+ mimeType: match[1].trim().toLowerCase(),
45
+ data: match[2].trim()
46
+ };
47
+ }
48
+ function resolveImageExtension(mimeType) {
49
+ const normalized = (mimeType || "").trim().toLowerCase();
50
+ switch(normalized){
51
+ case "image/jpeg":
52
+ return "jpg";
53
+ case "image/png":
54
+ return "png";
55
+ case "image/webp":
56
+ return "webp";
57
+ case "image/gif":
58
+ return "gif";
59
+ case "image/svg+xml":
60
+ return "svg";
61
+ case "image/bmp":
62
+ return "bmp";
63
+ case "image/tiff":
64
+ return "tiff";
65
+ case "image/heic":
66
+ return "heic";
67
+ case "image/heif":
68
+ return "heif";
69
+ case "image/avif":
70
+ return "avif";
71
+ default:
72
+ {
73
+ const subtype = normalized.split("/")[1] || "";
74
+ const sanitized = subtype.replace(/[^a-z0-9]/g, "");
75
+ return sanitized || "img";
76
+ }
77
+ }
78
+ }
79
+ function sanitizePathSegment(value) {
80
+ const normalized = (value || "").trim();
81
+ if (!normalized) return "default-session";
82
+ const sanitized = normalized.replace(/[^a-zA-Z0-9._-]/g, "_");
83
+ return sanitized.slice(0, 120) || "default-session";
84
+ }
85
+ export { parseBase64DataUrl, persistAssistantImagesToDisk, resolveImageExtension };