@memberjunction/server 5.27.1 → 5.29.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 (50) hide show
  1. package/dist/config.d.ts +151 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +15 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/generated/generated.d.ts +959 -5
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +4639 -280
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/generic/ResolverBase.d.ts +14 -0
  10. package/dist/generic/ResolverBase.d.ts.map +1 -1
  11. package/dist/generic/ResolverBase.js +37 -3
  12. package/dist/generic/ResolverBase.js.map +1 -1
  13. package/dist/generic/RestoreContextInput.d.ts +27 -0
  14. package/dist/generic/RestoreContextInput.d.ts.map +1 -0
  15. package/dist/generic/RestoreContextInput.js +39 -0
  16. package/dist/generic/RestoreContextInput.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +21 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/resolvers/FeedbackResolver.d.ts +150 -0
  22. package/dist/resolvers/FeedbackResolver.d.ts.map +1 -0
  23. package/dist/resolvers/FeedbackResolver.js +876 -0
  24. package/dist/resolvers/FeedbackResolver.js.map +1 -0
  25. package/dist/resolvers/FileResolver.d.ts +27 -0
  26. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  27. package/dist/resolvers/FileResolver.js +32 -3
  28. package/dist/resolvers/FileResolver.js.map +1 -1
  29. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +18 -1
  30. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  31. package/dist/resolvers/IntegrationDiscoveryResolver.js +247 -22
  32. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  33. package/dist/resolvers/MCPResolver.d.ts +77 -0
  34. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  35. package/dist/resolvers/MCPResolver.js +300 -1
  36. package/dist/resolvers/MCPResolver.js.map +1 -1
  37. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  38. package/dist/resolvers/RunAIAgentResolver.js +87 -32
  39. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  40. package/package.json +68 -66
  41. package/src/config.ts +19 -0
  42. package/src/generated/generated.ts +3430 -281
  43. package/src/generic/ResolverBase.ts +41 -4
  44. package/src/generic/RestoreContextInput.ts +32 -0
  45. package/src/index.ts +22 -5
  46. package/src/resolvers/FeedbackResolver.ts +940 -0
  47. package/src/resolvers/FileResolver.ts +33 -4
  48. package/src/resolvers/IntegrationDiscoveryResolver.ts +224 -20
  49. package/src/resolvers/MCPResolver.ts +297 -1
  50. package/src/resolvers/RunAIAgentResolver.ts +89 -32
@@ -5,7 +5,7 @@
5
5
  * including tool synchronization with progress streaming and OAuth management.
6
6
  */
7
7
 
8
- import { Resolver, Mutation, Query, Subscription, Arg, Ctx, Root, Field, ObjectType, InputType, PubSub, registerEnumType } from 'type-graphql';
8
+ import { Resolver, Mutation, Query, Subscription, Arg, Ctx, Root, Field, Int, ObjectType, InputType, PubSub, registerEnumType } from 'type-graphql';
9
9
  import { PubSubEngine } from 'type-graphql';
10
10
  import { LogError, LogStatus, UserInfo, Metadata, RunView } from '@memberjunction/core';
11
11
  import {
@@ -455,6 +455,96 @@ export class MCPOAuthEventNotification {
455
455
  RequiresReauthorization?: boolean;
456
456
  }
457
457
 
458
+ /**
459
+ * Summary of an MCP Server Tool record — only the fields needed for
460
+ * paginated card/list display. Heavy fields (InputSchema, OutputSchema,
461
+ * Annotations) are intentionally excluded to keep responses small.
462
+ */
463
+ @ObjectType()
464
+ export class MCPToolSummary {
465
+ @Field()
466
+ ID: string;
467
+
468
+ @Field()
469
+ MCPServerID: string;
470
+
471
+ @Field()
472
+ ToolName: string;
473
+
474
+ @Field({ nullable: true })
475
+ ToolTitle?: string;
476
+
477
+ @Field({ nullable: true })
478
+ ToolDescription?: string;
479
+
480
+ @Field()
481
+ Status: string;
482
+
483
+ /**
484
+ * MCP Server display name (from the vwMCPServerTools view — column name is "MCPServer").
485
+ */
486
+ @Field({ nullable: true })
487
+ ServerName?: string;
488
+ }
489
+
490
+ /**
491
+ * Paginated result for MCPToolSummary listings.
492
+ */
493
+ @ObjectType()
494
+ export class MCPToolPageResult {
495
+ @Field(() => [MCPToolSummary])
496
+ items: MCPToolSummary[];
497
+
498
+ @Field(() => Int)
499
+ totalCount: number;
500
+
501
+ @Field()
502
+ hasMore: boolean;
503
+ }
504
+
505
+ /**
506
+ * Count-per-server aggregate.
507
+ */
508
+ @ObjectType()
509
+ export class MCPToolServerCount {
510
+ @Field()
511
+ serverID: string;
512
+
513
+ @Field()
514
+ serverName: string;
515
+
516
+ @Field(() => Int)
517
+ count: number;
518
+ }
519
+
520
+ /**
521
+ * Count-per-category aggregate. Category is derived from the ToolName
522
+ * prefix (substring before first underscore).
523
+ */
524
+ @ObjectType()
525
+ export class MCPToolCategoryCount {
526
+ @Field()
527
+ category: string;
528
+
529
+ @Field(() => Int)
530
+ count: number;
531
+ }
532
+
533
+ /**
534
+ * Top-level counts payload for MCP tool group/badge displays.
535
+ */
536
+ @ObjectType()
537
+ export class MCPToolCountsResult {
538
+ @Field(() => Int)
539
+ totalCount: number;
540
+
541
+ @Field(() => [MCPToolServerCount])
542
+ countByServer: MCPToolServerCount[];
543
+
544
+ @Field(() => [MCPToolCategoryCount])
545
+ countByCategory: MCPToolCategoryCount[];
546
+ }
547
+
458
548
  /**
459
549
  * MCP Resolver for GraphQL operations
460
550
  */
@@ -1325,6 +1415,212 @@ export class MCPResolver extends ResolverBase {
1325
1415
  };
1326
1416
  }
1327
1417
 
1418
+ /**
1419
+ * Returns a paginated list of MCP tool summaries filtered by optional
1420
+ * server, category, and free-text search. Uses narrow Fields + simple
1421
+ * ResultType to keep payload sizes manageable at thousands of tools.
1422
+ *
1423
+ * @param skip Number of rows to skip (for pagination)
1424
+ * @param take Page size
1425
+ * @param searchText Free-text filter against ToolName, ToolTitle, ToolDescription
1426
+ * @param serverID Optional MCPServerID filter
1427
+ * @param category Optional category filter (ToolName prefix before first underscore)
1428
+ * @param ctx GraphQL context
1429
+ */
1430
+ @Query(() => MCPToolPageResult)
1431
+ async GetMCPToolsPage(
1432
+ @Arg('skip', () => Int) skip: number,
1433
+ @Arg('take', () => Int) take: number,
1434
+ @Arg('searchText', { nullable: true }) searchText: string | null,
1435
+ @Arg('serverID', { nullable: true }) serverID: string | null,
1436
+ @Arg('category', { nullable: true }) category: string | null,
1437
+ @Ctx() ctx: AppContext
1438
+ ): Promise<MCPToolPageResult> {
1439
+ const user = ctx.userPayload.userRecord;
1440
+ if (!user) {
1441
+ return { items: [], totalCount: 0, hasMore: false };
1442
+ }
1443
+
1444
+ try {
1445
+ const extraFilter = this.buildMCPToolsFilter(searchText, serverID, category);
1446
+ const rv = new RunView();
1447
+
1448
+ const result = await rv.RunView<{
1449
+ ID: string;
1450
+ MCPServerID: string;
1451
+ ToolName: string;
1452
+ ToolTitle: string | null;
1453
+ ToolDescription: string | null;
1454
+ Status: string;
1455
+ MCPServer: string | null;
1456
+ }>({
1457
+ EntityName: 'MJ: MCP Server Tools',
1458
+ ExtraFilter: extraFilter,
1459
+ OrderBy: 'ToolName ASC',
1460
+ Fields: ['ID', 'MCPServerID', 'ToolName', 'ToolTitle', 'ToolDescription', 'Status', 'MCPServer'],
1461
+ StartRow: skip,
1462
+ MaxRows: take,
1463
+ IgnoreMaxRows: false,
1464
+ ResultType: 'simple'
1465
+ }, user);
1466
+
1467
+ if (!result.Success) {
1468
+ LogError(`MCPResolver: GetMCPToolsPage failed: ${result.ErrorMessage}`);
1469
+ return { items: [], totalCount: 0, hasMore: false };
1470
+ }
1471
+
1472
+ const rows = result.Results || [];
1473
+ // RunView's TotalRowCount can be capped when MaxRows is applied. Do an explicit
1474
+ // count query (ignoring MaxRows) so the client sees the true filtered match total.
1475
+ let totalCount = result.TotalRowCount ?? rows.length;
1476
+ try {
1477
+ const countResult = await rv.RunView<{ ID: string }>({
1478
+ EntityName: 'MJ: MCP Server Tools',
1479
+ ExtraFilter: extraFilter,
1480
+ Fields: ['ID'],
1481
+ IgnoreMaxRows: true,
1482
+ ResultType: 'simple'
1483
+ }, user);
1484
+ if (countResult.Success) {
1485
+ totalCount = countResult.Results.length;
1486
+ }
1487
+ } catch {
1488
+ // fall back to the initial totalCount on any error
1489
+ }
1490
+ const items: MCPToolSummary[] = rows.map(r => ({
1491
+ ID: r.ID,
1492
+ MCPServerID: r.MCPServerID,
1493
+ ToolName: r.ToolName,
1494
+ ToolTitle: r.ToolTitle ?? undefined,
1495
+ ToolDescription: r.ToolDescription ?? undefined,
1496
+ Status: r.Status,
1497
+ ServerName: r.MCPServer ?? undefined
1498
+ }));
1499
+
1500
+ return {
1501
+ items,
1502
+ totalCount,
1503
+ hasMore: skip + items.length < totalCount
1504
+ };
1505
+ } catch (error) {
1506
+ const errorMsg = error instanceof Error ? error.message : String(error);
1507
+ LogError(`MCPResolver: GetMCPToolsPage exception: ${errorMsg}`);
1508
+ return { items: [], totalCount: 0, hasMore: false };
1509
+ }
1510
+ }
1511
+
1512
+ /**
1513
+ * Returns aggregate counts for MCP tools — total, per-server, and per-category
1514
+ * (category = ToolName prefix before the first underscore). Used to render
1515
+ * group headers with badges like "Zapier (1,247 tools)".
1516
+ *
1517
+ * @param serverID Optional MCPServerID filter (counts scoped to that server)
1518
+ * @param searchText Optional free-text filter (same semantics as GetMCPToolsPage)
1519
+ * @param ctx GraphQL context
1520
+ */
1521
+ @Query(() => MCPToolCountsResult)
1522
+ async GetMCPToolCounts(
1523
+ @Arg('serverID', { nullable: true }) serverID: string | null,
1524
+ @Arg('searchText', { nullable: true }) searchText: string | null,
1525
+ @Ctx() ctx: AppContext
1526
+ ): Promise<MCPToolCountsResult> {
1527
+ const user = ctx.userPayload.userRecord;
1528
+ if (!user) {
1529
+ return { totalCount: 0, countByServer: [], countByCategory: [] };
1530
+ }
1531
+
1532
+ try {
1533
+ // Build filter WITHOUT the category constraint — counts need the full
1534
+ // matching set so we can bucket by server and by category ourselves.
1535
+ const extraFilter = this.buildMCPToolsFilter(searchText, serverID, null);
1536
+ const rv = new RunView();
1537
+
1538
+ // Fetch minimal fields for ALL matching rows. IgnoreMaxRows so we
1539
+ // don't silently truncate at entity caps when counting.
1540
+ const result = await rv.RunView<{
1541
+ MCPServerID: string;
1542
+ ToolName: string;
1543
+ MCPServer: string | null;
1544
+ }>({
1545
+ EntityName: 'MJ: MCP Server Tools',
1546
+ ExtraFilter: extraFilter,
1547
+ Fields: ['MCPServerID', 'ToolName', 'MCPServer'],
1548
+ IgnoreMaxRows: true,
1549
+ ResultType: 'simple'
1550
+ }, user);
1551
+
1552
+ if (!result.Success) {
1553
+ LogError(`MCPResolver: GetMCPToolCounts failed: ${result.ErrorMessage}`);
1554
+ return { totalCount: 0, countByServer: [], countByCategory: [] };
1555
+ }
1556
+
1557
+ const rows = result.Results || [];
1558
+ const totalCount = rows.length;
1559
+
1560
+ // Bucket by server
1561
+ const serverMap = new Map<string, { serverID: string; serverName: string; count: number }>();
1562
+ // Bucket by category (ToolName prefix before first underscore)
1563
+ const categoryMap = new Map<string, number>();
1564
+
1565
+ for (const r of rows) {
1566
+ const sid = r.MCPServerID;
1567
+ const sname = r.MCPServer ?? '';
1568
+ const existing = serverMap.get(sid);
1569
+ if (existing) {
1570
+ existing.count++;
1571
+ } else {
1572
+ serverMap.set(sid, { serverID: sid, serverName: sname, count: 1 });
1573
+ }
1574
+
1575
+ const underscore = r.ToolName.indexOf('_');
1576
+ const cat = underscore > 0 ? r.ToolName.substring(0, underscore) : r.ToolName;
1577
+ categoryMap.set(cat, (categoryMap.get(cat) ?? 0) + 1);
1578
+ }
1579
+
1580
+ const countByServer = Array.from(serverMap.values()).sort((a, b) => b.count - a.count);
1581
+ const countByCategory = Array.from(categoryMap.entries())
1582
+ .map(([category, count]) => ({ category, count }))
1583
+ .sort((a, b) => b.count - a.count);
1584
+
1585
+ return { totalCount, countByServer, countByCategory };
1586
+ } catch (error) {
1587
+ const errorMsg = error instanceof Error ? error.message : String(error);
1588
+ LogError(`MCPResolver: GetMCPToolCounts exception: ${errorMsg}`);
1589
+ return { totalCount: 0, countByServer: [], countByCategory: [] };
1590
+ }
1591
+ }
1592
+
1593
+ /**
1594
+ * Builds the ExtraFilter string for MCP Server Tools queries. All user
1595
+ * inputs are single-quote-escaped to prevent SQL injection.
1596
+ */
1597
+ private buildMCPToolsFilter(
1598
+ searchText: string | null | undefined,
1599
+ serverID: string | null | undefined,
1600
+ category: string | null | undefined
1601
+ ): string {
1602
+ const clauses: string[] = [];
1603
+
1604
+ if (serverID && serverID.trim().length > 0) {
1605
+ const safe = serverID.replace(/'/g, "''");
1606
+ clauses.push(`MCPServerID='${safe}'`);
1607
+ }
1608
+
1609
+ if (category && category.trim().length > 0) {
1610
+ const safe = category.replace(/'/g, "''");
1611
+ clauses.push(`ToolName LIKE '${safe}\\_%' ESCAPE '\\'`);
1612
+ }
1613
+
1614
+ if (searchText && searchText.trim().length > 0) {
1615
+ const safe = searchText.replace(/'/g, "''");
1616
+ clauses.push(
1617
+ `(ToolName LIKE '%${safe}%' OR ToolTitle LIKE '%${safe}%' OR ToolDescription LIKE '%${safe}%')`
1618
+ );
1619
+ }
1620
+
1621
+ return clauses.join(' AND ');
1622
+ }
1623
+
1328
1624
  /**
1329
1625
  * Gets the public URL for OAuth callbacks
1330
1626
  */
@@ -2,7 +2,7 @@ import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, PubSub, PubSubE
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo, IMetadataProvider } from '@memberjunction/core';
4
4
  import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJConversationDetailArtifactEntity, MJArtifactVersionEntity, MJAIAgentRequestEntity } from '@memberjunction/core-entities';
5
- import { AgentRunner } from '@memberjunction/ai-agents';
5
+ import { AgentRunner, ArtifactToolManager } from '@memberjunction/ai-agents';
6
6
  import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
8
8
  import { ChatMessage, ChatMessageContent } from '@memberjunction/ai';
@@ -1310,43 +1310,100 @@ export class RunAIAgentResolver extends ResolverBase {
1310
1310
  );
1311
1311
  const attachmentDataResults = await Promise.all(attachmentDataPromises);
1312
1312
 
1313
- // Filter out nulls and convert to AttachmentData format
1314
- const validAttachments: AttachmentData[] = attachmentDataResults
1315
- .filter((result): result is NonNullable<typeof result> => result !== null)
1316
- .map(result => ({
1317
- type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
1318
- mimeType: result.attachment.MimeType,
1319
- fileName: result.attachment.FileName ?? undefined,
1320
- sizeBytes: result.attachment.FileSizeBytes ?? undefined,
1321
- width: result.attachment.Width ?? undefined,
1322
- height: result.attachment.Height ?? undefined,
1323
- durationSeconds: result.attachment.DurationSeconds ?? undefined,
1324
- content: result.contentUrl
1325
- }));
1326
-
1327
- // Get input artifacts for this message and convert to AttachmentData
1313
+ // Filter out nulls and convert to AttachmentData format.
1314
+ // IMPORTANT: Skip large document attachments that are handled by artifact tools.
1315
+ // These file types (PDF, Excel, Word) are accessible via ArtifactToolManager
1316
+ // and embedding their multi-MB base64 content in the conversation causes
1317
+ // context overflow on follow-up messages when conversation history is replayed.
1318
+ // Images are kept inline since LLMs handle them natively via multimodal.
1319
+ const ARTIFACT_TOOL_MIME_PREFIXES = [
1320
+ 'application/pdf',
1321
+ 'application/vnd.openxmlformats-officedocument', // .docx, .xlsx, .pptx
1322
+ 'application/vnd.ms-excel', // .xls
1323
+ 'application/msword', // .doc
1324
+ ];
1325
+
1326
+ const validAttachments: AttachmentData[] = [];
1327
+
1328
+ for (const result of attachmentDataResults) {
1329
+ if (!result) continue;
1330
+ const mime = result.attachment.MimeType || '';
1331
+ const isArtifactToolType = ARTIFACT_TOOL_MIME_PREFIXES.some(prefix => mime.startsWith(prefix));
1332
+ if (isArtifactToolType) {
1333
+ // Skip raw file embedding — the agent accesses the file via
1334
+ // artifact tools (manifest injected into prompt) and/or native
1335
+ // file input (resolved per-driver in AIPromptRunner).
1336
+ // Do NOT add a placeholder attachment: 'Document' maps to 'file_url'
1337
+ // content blocks, and drivers attempt base64 decoding of the text,
1338
+ // causing API errors.
1339
+ } else {
1340
+ validAttachments.push({
1341
+ type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
1342
+ mimeType: result.attachment.MimeType,
1343
+ fileName: result.attachment.FileName ?? undefined,
1344
+ sizeBytes: result.attachment.FileSizeBytes ?? undefined,
1345
+ width: result.attachment.Width ?? undefined,
1346
+ height: result.attachment.Height ?? undefined,
1347
+ durationSeconds: result.attachment.DurationSeconds ?? undefined,
1348
+ content: result.contentUrl
1349
+ });
1350
+ }
1351
+ }
1352
+
1353
+ // Get input artifacts for this message and convert to AttachmentData.
1354
+ // Like regular attachments above, skip file-backed artifacts whose MIME
1355
+ // types are handled by ArtifactToolManager — embedding their multi-MB
1356
+ // base64 content in every conversation replay causes context overflow.
1328
1357
  const inputArtifacts = inputArtifactsByDetailId.get(detail.ID) || [];
1329
1358
  for (const artifactVersion of inputArtifacts) {
1359
+ const artifactMime = artifactVersion.MimeType || '';
1360
+ const isArtifactToolHandled = ARTIFACT_TOOL_MIME_PREFIXES.some(
1361
+ prefix => artifactMime.startsWith(prefix)
1362
+ );
1363
+
1330
1364
  if (artifactVersion.ContentMode === 'File' && artifactVersion.FileID) {
1331
- // File-backed artifact — download content and treat like a document attachment
1332
- const fileContent = await this.downloadArtifactFileContent(artifactVersion, contextUser, provider);
1333
- if (fileContent) {
1365
+ if (isArtifactToolHandled) {
1366
+ // Skip raw file embedding the agent accesses the file via
1367
+ // artifact tools (manifest injected into prompt) and/or native
1368
+ // file input (resolved per-driver in AIPromptRunner).
1369
+ // Do NOT add a placeholder attachment here: 'Document' maps to
1370
+ // 'file_url' content blocks, and drivers attempt base64 decoding
1371
+ // of the placeholder text, causing API errors.
1372
+ } else {
1373
+ // Non-artifact-tool file types: embed normally
1374
+ const fileContent = await this.downloadArtifactFileContent(artifactVersion, contextUser, provider);
1375
+ if (fileContent) {
1376
+ validAttachments.push({
1377
+ type: ConversationUtility.GetAttachmentTypeFromMime(artifactMime),
1378
+ mimeType: artifactMime || 'application/octet-stream',
1379
+ fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
1380
+ sizeBytes: artifactVersion.ContentSizeBytes || undefined,
1381
+ content: fileContent
1382
+ });
1383
+ }
1384
+ }
1385
+ } else if (artifactVersion.Content) {
1386
+ // Text artifact — use BuildInlinePreview for large DataSnapshots
1387
+ // to preserve table structure instead of raw substring truncation.
1388
+ const textContent = artifactVersion.Content;
1389
+ const MAX_INLINE_ARTIFACT_CHARS = 10_000;
1390
+
1391
+ if (textContent.length > MAX_INLINE_ARTIFACT_CHARS) {
1392
+ const preview = ArtifactToolManager.BuildInlinePreview(textContent, 5);
1393
+ validAttachments.push({
1394
+ type: 'Document' as AttachmentData['type'],
1395
+ mimeType: 'text/plain',
1396
+ fileName: undefined,
1397
+ content: `[Artifact: ${artifactVersion.Name || 'Untitled'} — ${textContent.length.toLocaleString()} chars, accessible via artifact tools]\n\nPreview (first 5 rows per table):\n${preview}`
1398
+ });
1399
+ } else {
1334
1400
  validAttachments.push({
1335
- type: ConversationUtility.GetAttachmentTypeFromMime(artifactVersion.MimeType || ''),
1336
- mimeType: artifactVersion.MimeType || 'application/octet-stream',
1337
- fileName: artifactVersion.FileName || artifactVersion.Name || undefined,
1338
- sizeBytes: artifactVersion.ContentSizeBytes || undefined,
1339
- content: fileContent
1401
+ type: 'Document' as AttachmentData['type'],
1402
+ mimeType: 'text/plain',
1403
+ fileName: artifactVersion.Name || 'artifact.txt',
1404
+ content: `[Artifact: ${artifactVersion.Name || 'Untitled'}]\n\n${textContent}`
1340
1405
  });
1341
1406
  }
1342
- } else if (artifactVersion.Content) {
1343
- // Text artifact — include content directly as a text attachment
1344
- validAttachments.push({
1345
- type: 'Document' as AttachmentData['type'],
1346
- mimeType: 'text/plain',
1347
- fileName: artifactVersion.Name || 'artifact.txt',
1348
- content: `[Artifact: ${artifactVersion.Name || 'Untitled'}]\n\n${artifactVersion.Content}`
1349
- });
1350
1407
  }
1351
1408
  }
1352
1409