@ouro.bot/cli 0.1.0-alpha.6 → 0.1.0-alpha.60

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 (117) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +325 -0
  7. package/dist/heart/active-work.js +178 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/config.js +57 -23
  12. package/dist/heart/core.js +236 -90
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/auth-flow.js +351 -0
  16. package/dist/heart/daemon/daemon-cli.js +1173 -227
  17. package/dist/heart/daemon/daemon-entry.js +55 -6
  18. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  19. package/dist/heart/daemon/daemon.js +189 -10
  20. package/dist/heart/daemon/hatch-animation.js +10 -3
  21. package/dist/heart/daemon/hatch-flow.js +4 -82
  22. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  23. package/dist/heart/daemon/launchd.js +159 -0
  24. package/dist/heart/daemon/log-tailer.js +4 -3
  25. package/dist/heart/daemon/message-router.js +17 -8
  26. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  27. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  28. package/dist/heart/daemon/ouro-entry.js +0 -0
  29. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  30. package/dist/heart/daemon/ouro-uti.js +11 -2
  31. package/dist/heart/daemon/process-manager.js +14 -1
  32. package/dist/heart/daemon/run-hooks.js +37 -0
  33. package/dist/heart/daemon/runtime-logging.js +58 -15
  34. package/dist/heart/daemon/runtime-metadata.js +219 -0
  35. package/dist/heart/daemon/runtime-mode.js +67 -0
  36. package/dist/heart/daemon/sense-manager.js +307 -0
  37. package/dist/heart/daemon/socket-client.js +202 -0
  38. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  39. package/dist/heart/daemon/specialist-prompt.js +64 -5
  40. package/dist/heart/daemon/specialist-tools.js +213 -58
  41. package/dist/heart/daemon/staged-restart.js +114 -0
  42. package/dist/heart/daemon/subagent-installer.js +48 -7
  43. package/dist/heart/daemon/thoughts.js +379 -0
  44. package/dist/heart/daemon/update-checker.js +111 -0
  45. package/dist/heart/daemon/update-hooks.js +138 -0
  46. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  47. package/dist/heart/delegation.js +62 -0
  48. package/dist/heart/identity.js +122 -19
  49. package/dist/heart/kicks.js +1 -19
  50. package/dist/heart/model-capabilities.js +40 -0
  51. package/dist/heart/progress-story.js +42 -0
  52. package/dist/heart/providers/anthropic.js +74 -9
  53. package/dist/heart/providers/azure.js +86 -7
  54. package/dist/heart/providers/minimax.js +4 -0
  55. package/dist/heart/providers/openai-codex.js +12 -3
  56. package/dist/heart/safe-workspace.js +228 -0
  57. package/dist/heart/sense-truth.js +61 -0
  58. package/dist/heart/session-activity.js +169 -0
  59. package/dist/heart/session-recall.js +116 -0
  60. package/dist/heart/streaming.js +100 -22
  61. package/dist/heart/target-resolution.js +123 -0
  62. package/dist/heart/turn-coordinator.js +28 -0
  63. package/dist/mind/associative-recall.js +14 -2
  64. package/dist/mind/bundle-manifest.js +70 -0
  65. package/dist/mind/context.js +27 -11
  66. package/dist/mind/first-impressions.js +16 -2
  67. package/dist/mind/friends/channel.js +35 -0
  68. package/dist/mind/friends/group-context.js +144 -0
  69. package/dist/mind/friends/store-file.js +19 -0
  70. package/dist/mind/friends/trust-explanation.js +74 -0
  71. package/dist/mind/friends/types.js +8 -0
  72. package/dist/mind/memory.js +27 -26
  73. package/dist/mind/pending.js +72 -9
  74. package/dist/mind/phrases.js +1 -0
  75. package/dist/mind/prompt.js +299 -77
  76. package/dist/mind/token-estimate.js +8 -12
  77. package/dist/nerves/cli-logging.js +15 -2
  78. package/dist/nerves/coverage/run-artifacts.js +1 -1
  79. package/dist/repertoire/ado-client.js +4 -2
  80. package/dist/repertoire/coding/feedback.js +134 -0
  81. package/dist/repertoire/coding/index.js +4 -1
  82. package/dist/repertoire/coding/manager.js +62 -4
  83. package/dist/repertoire/coding/spawner.js +3 -3
  84. package/dist/repertoire/coding/tools.js +41 -2
  85. package/dist/repertoire/data/ado-endpoints.json +188 -0
  86. package/dist/repertoire/tasks/board.js +12 -0
  87. package/dist/repertoire/tasks/index.js +23 -9
  88. package/dist/repertoire/tasks/transitions.js +1 -2
  89. package/dist/repertoire/tools-base.js +629 -251
  90. package/dist/repertoire/tools-bluebubbles.js +93 -0
  91. package/dist/repertoire/tools-teams.js +58 -25
  92. package/dist/repertoire/tools.js +92 -48
  93. package/dist/senses/bluebubbles-client.js +210 -5
  94. package/dist/senses/bluebubbles-entry.js +2 -0
  95. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  96. package/dist/senses/bluebubbles-media.js +339 -0
  97. package/dist/senses/bluebubbles-model.js +12 -4
  98. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  99. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  100. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  101. package/dist/senses/bluebubbles.js +890 -45
  102. package/dist/senses/cli-layout.js +87 -0
  103. package/dist/senses/cli.js +345 -144
  104. package/dist/senses/continuity.js +94 -0
  105. package/dist/senses/debug-activity.js +148 -0
  106. package/dist/senses/inner-dialog-worker.js +47 -18
  107. package/dist/senses/inner-dialog.js +330 -84
  108. package/dist/senses/pipeline.js +278 -0
  109. package/dist/senses/teams.js +570 -129
  110. package/dist/senses/trust-gate.js +112 -2
  111. package/package.json +14 -3
  112. package/subagents/README.md +46 -33
  113. package/subagents/work-doer.md +28 -24
  114. package/subagents/work-merger.md +24 -30
  115. package/subagents/work-planner.md +44 -27
  116. package/dist/heart/daemon/specialist-session.js +0 -142
  117. package/dist/inner-worker-entry.js +0 -4
@@ -40,10 +40,15 @@ exports.createTeamsCallbacks = createTeamsCallbacks;
40
40
  exports.resolvePendingConfirmation = resolvePendingConfirmation;
41
41
  exports.withConversationLock = withConversationLock;
42
42
  exports.handleTeamsMessage = handleTeamsMessage;
43
+ exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
44
+ exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
43
45
  exports.startTeamsApp = startTeamsApp;
46
+ const fs = __importStar(require("fs"));
44
47
  const teams_apps_1 = require("@microsoft/teams.apps");
45
48
  const teams_dev_1 = require("@microsoft/teams.dev");
46
49
  const core_1 = require("../heart/core");
50
+ const tools_1 = require("../repertoire/tools");
51
+ const channel_1 = require("../mind/friends/channel");
47
52
  const config_1 = require("../heart/config");
48
53
  const prompt_1 = require("../mind/prompt");
49
54
  const phrases_1 = require("../mind/phrases");
@@ -54,12 +59,18 @@ const commands_1 = require("./commands");
54
59
  const nerves_1 = require("../nerves");
55
60
  const runtime_1 = require("../nerves/runtime");
56
61
  const store_file_1 = require("../mind/friends/store-file");
62
+ const types_1 = require("../mind/friends/types");
57
63
  const resolver_1 = require("../mind/friends/resolver");
58
64
  const tokens_1 = require("../mind/friends/tokens");
59
65
  const turn_coordinator_1 = require("../heart/turn-coordinator");
60
66
  const identity_1 = require("../heart/identity");
67
+ const progress_story_1 = require("../heart/progress-story");
68
+ const http = __importStar(require("http"));
61
69
  const path = __importStar(require("path"));
62
70
  const trust_gate_1 = require("./trust-gate");
71
+ const pipeline_1 = require("./pipeline");
72
+ const pending_1 = require("../mind/pending");
73
+ const continuity_1 = require("./continuity");
63
74
  // Strip @mention markup from incoming messages.
64
75
  // Removes <at>...</at> tags and trims extra whitespace.
65
76
  // Fallback safety net -- the SDK's activity.mentions.stripText should handle
@@ -317,14 +328,29 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
317
328
  onToolStart: (name, args) => {
318
329
  stopPhraseRotation();
319
330
  flushTextBuffer();
320
- const argSummary = Object.values(args).join(", ");
321
- safeUpdate(`running ${name} (${argSummary})...`);
331
+ // Emit a placeholder to satisfy the 15s Copilot timeout for initial
332
+ // stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
333
+ // never emit before the timeout and the user sees "this response was
334
+ // stopped". The placeholder is replaced by actual content on next emit.
335
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
336
+ if (!streamHasContent)
337
+ safeEmit("⏳");
338
+ const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
339
+ safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
340
+ scope: "shared-work",
341
+ phase: "processing",
342
+ objective: `running ${name} (${argSummary})...`,
343
+ })));
322
344
  hadToolRun = true;
323
345
  },
324
346
  onToolEnd: (name, summary, success) => {
325
347
  stopPhraseRotation();
326
348
  const msg = (0, format_1.formatToolResult)(name, summary, success);
327
- safeUpdate(msg);
349
+ safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
350
+ scope: "shared-work",
351
+ phase: "processing",
352
+ objective: msg,
353
+ })));
328
354
  },
329
355
  onKick: () => {
330
356
  stopPhraseRotation();
@@ -335,7 +361,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
335
361
  stopPhraseRotation();
336
362
  if (stopped)
337
363
  return;
338
- const msg = (0, format_1.formatError)(error);
364
+ const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
365
+ scope: "shared-work",
366
+ phase: "errored",
367
+ outcomeText: (0, format_1.formatError)(error),
368
+ }));
339
369
  if (severity === "transient") {
340
370
  safeUpdate(msg);
341
371
  }
@@ -424,6 +454,34 @@ function getFriendStore() {
424
454
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
425
455
  return new store_file_1.FileFriendStore(friendsPath);
426
456
  }
457
+ function createTeamsCommandRegistry() {
458
+ const registry = (0, commands_1.createCommandRegistry)();
459
+ (0, commands_1.registerDefaultCommands)(registry);
460
+ return registry;
461
+ }
462
+ function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
463
+ const parsed = (0, commands_1.parseSlashCommand)(text);
464
+ if (!parsed)
465
+ return null;
466
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
467
+ if (!dispatchResult.handled || !dispatchResult.result) {
468
+ return null;
469
+ }
470
+ if (dispatchResult.result.action === "new") {
471
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
472
+ if (emitResponse) {
473
+ stream.emit("session cleared");
474
+ }
475
+ return "new";
476
+ }
477
+ if (dispatchResult.result.action === "response") {
478
+ if (emitResponse) {
479
+ stream.emit(dispatchResult.result.message || "");
480
+ }
481
+ return "response";
482
+ }
483
+ return null;
484
+ }
427
485
  // Handle an incoming Teams message
428
486
  async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
429
487
  const turnKey = teamsTurnKey(conversationId);
@@ -434,148 +492,227 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
434
492
  // before sync I/O (session load, trim) blocks the event loop.
435
493
  stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
436
494
  await new Promise(r => setImmediate(r));
437
- // Resolve context kernel (identity + channel) early so we can use the friend UUID for session path
495
+ // Resolve identity provider early for friend resolution + slash command session path
438
496
  const store = getFriendStore();
439
497
  const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
440
498
  const externalId = teamsContext?.aadObjectId || conversationId;
441
- const toolContext = teamsContext ? {
499
+ // Build FriendResolver for the pipeline
500
+ const resolver = new resolver_1.FriendResolver(store, {
501
+ provider,
502
+ externalId,
503
+ tenantId: teamsContext?.tenantId,
504
+ displayName: teamsContext?.displayName || "Unknown",
505
+ channel: "teams",
506
+ });
507
+ // Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
508
+ const resolvedContext = await resolver.resolve();
509
+ const friendId = resolvedContext.friend.id;
510
+ const registry = createTeamsCommandRegistry();
511
+ // Check for slash commands (before pipeline -- these are transport-level concerns)
512
+ if (handleTeamsSlashCommand(text, registry, friendId, conversationId, stream)) {
513
+ return;
514
+ }
515
+ // ── Teams adapter concerns: controller, callbacks, session path ──────────
516
+ const controller = new AbortController();
517
+ const channelConfig = (0, config_2.getTeamsChannelConfig)();
518
+ const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
519
+ const traceId = (0, nerves_1.createTraceId)();
520
+ const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
521
+ const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
522
+ const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
523
+ // Build Teams-specific toolContext fields for injection into the pipeline
524
+ const teamsToolContext = teamsContext ? {
442
525
  graphToken: teamsContext.graphToken,
443
526
  adoToken: teamsContext.adoToken,
444
527
  githubToken: teamsContext.githubToken,
445
528
  signin: teamsContext.signin,
446
- friendStore: store,
447
529
  summarize: (0, core_1.createSummarize)(),
448
- } : undefined;
449
- if (toolContext) {
450
- const resolver = new resolver_1.FriendResolver(store, {
451
- provider,
452
- externalId,
453
- tenantId: teamsContext?.tenantId,
454
- displayName: teamsContext?.displayName || "Unknown",
530
+ tenantId: teamsContext.tenantId,
531
+ botApi: teamsContext.botApi,
532
+ } : {};
533
+ let currentText = text;
534
+ while (true) {
535
+ let drainedSteeringFollowUps = [];
536
+ // Build runAgentOptions with Teams-specific fields
537
+ const agentOptions = {
538
+ traceId,
539
+ toolContext: teamsToolContext,
540
+ drainSteeringFollowUps: () => {
541
+ drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
542
+ .map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
543
+ return drainedSteeringFollowUps;
544
+ },
545
+ };
546
+ if (channelConfig.skipConfirmation)
547
+ agentOptions.skipConfirmation = true;
548
+ // ── Call shared pipeline ──────────────────────────────────────────
549
+ const result = await (0, pipeline_1.handleInboundTurn)({
455
550
  channel: "teams",
456
- });
457
- toolContext.context = await resolver.resolve();
458
- }
459
- const friendId = toolContext?.context?.friend?.id || "default";
460
- if (toolContext?.context?.friend) {
461
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
462
- friend: toolContext.context.friend,
551
+ sessionKey: conversationId,
552
+ capabilities: teamsCapabilities,
553
+ messages: [{ role: "user", content: currentText }],
554
+ continuityIngressTexts: [currentText],
555
+ callbacks,
556
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
557
+ sessionLoader: {
558
+ loadOrCreate: async () => {
559
+ const existing = (0, context_1.loadSession)(sessPath);
560
+ const messages = existing?.messages && existing.messages.length > 0
561
+ ? existing.messages
562
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
563
+ (0, core_1.repairOrphanedToolCalls)(messages);
564
+ return { messages, sessionPath: sessPath, state: existing?.state };
565
+ },
566
+ },
567
+ pendingDir,
568
+ friendStore: store,
463
569
  provider,
464
570
  externalId,
465
571
  tenantId: teamsContext?.tenantId,
466
- channel: "teams",
572
+ isGroupChat: false,
573
+ groupHasFamilyMember: false,
574
+ hasExistingGroupWithFamily: false,
575
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
576
+ drainPending: pending_1.drainPending,
577
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
578
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
579
+ ...opts,
580
+ toolContext: {
581
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
582
+ signin: async () => undefined,
583
+ ...opts?.toolContext,
584
+ summarize: teamsToolContext.summarize,
585
+ },
586
+ }),
587
+ postTurn: context_1.postTurn,
588
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
589
+ signal: controller.signal,
590
+ runAgentOptions: agentOptions,
467
591
  });
468
- if (!trustGate.allowed) {
469
- if (trustGate.reason === "stranger_first_reply") {
470
- stream.emit(trustGate.autoReply);
592
+ // ── Handle gate result ────────────────────────────────────────
593
+ if (!result.gateResult.allowed) {
594
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
595
+ stream.emit(result.gateResult.autoReply);
471
596
  }
472
597
  return;
473
598
  }
599
+ // Flush any remaining accumulated text at end of turn
600
+ await callbacks.flush();
601
+ // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
602
+ // This must happen after the stream is done so the OAuth card renders properly.
603
+ if (teamsContext && result.messages) {
604
+ const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
605
+ if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
606
+ await teamsContext.signin(teamsContext.graphConnectionName);
607
+ if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
608
+ await teamsContext.signin(teamsContext.adoConnectionName);
609
+ if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
610
+ await teamsContext.signin(teamsContext.githubConnectionName);
611
+ }
612
+ if (result.turnOutcome !== "superseded") {
613
+ return;
614
+ }
615
+ const supersedingIndex = drainedSteeringFollowUps
616
+ .map((followUp) => followUp.effect)
617
+ .lastIndexOf("clear_and_supersede");
618
+ if (supersedingIndex < 0) {
619
+ return;
620
+ }
621
+ const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
622
+ const replayTail = drainedSteeringFollowUps
623
+ .slice(supersedingIndex + 1)
624
+ .map((followUp) => followUp.text.trim())
625
+ .filter((followUpText) => followUpText.length > 0)
626
+ .join("\n");
627
+ if (replayTail) {
628
+ currentText = replayTail;
629
+ continue;
630
+ }
631
+ if (handleTeamsSlashCommand(supersedingFollowUp.text, registry, friendId, conversationId, stream, false)) {
632
+ return;
633
+ }
634
+ currentText = supersedingFollowUp.text;
474
635
  }
475
- const registry = (0, commands_1.createCommandRegistry)();
476
- (0, commands_1.registerDefaultCommands)(registry);
477
- // Check for slash commands
478
- const parsed = (0, commands_1.parseSlashCommand)(text);
479
- if (parsed) {
480
- const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
481
- if (dispatchResult.handled && dispatchResult.result) {
482
- if (dispatchResult.result.action === "new") {
483
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
484
- (0, context_1.deleteSession)(sessPath);
485
- stream.emit("session cleared");
486
- return;
487
- }
488
- else if (dispatchResult.result.action === "response") {
489
- stream.emit(dispatchResult.result.message || "");
490
- return;
491
- }
636
+ }
637
+ // Internal port for the secondary bot App (not exposed externally).
638
+ // The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
639
+ const SECONDARY_INTERNAL_PORT = 3979;
640
+ // Collect all unique OAuth connection names across top-level config and tenant overrides.
641
+ /* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
642
+ function allOAuthConnectionNames() {
643
+ const oauthConfig = (0, config_1.getOAuthConfig)();
644
+ const names = new Set();
645
+ if (oauthConfig.graphConnectionName)
646
+ names.add(oauthConfig.graphConnectionName);
647
+ if (oauthConfig.adoConnectionName)
648
+ names.add(oauthConfig.adoConnectionName);
649
+ if (oauthConfig.githubConnectionName)
650
+ names.add(oauthConfig.githubConnectionName);
651
+ if (oauthConfig.tenantOverrides) {
652
+ for (const ov of Object.values(oauthConfig.tenantOverrides)) {
653
+ if (ov.graphConnectionName)
654
+ names.add(ov.graphConnectionName);
655
+ if (ov.adoConnectionName)
656
+ names.add(ov.adoConnectionName);
657
+ if (ov.githubConnectionName)
658
+ names.add(ov.githubConnectionName);
492
659
  }
493
660
  }
494
- // Load or create session
495
- const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
496
- const existing = (0, context_1.loadSession)(sessPath);
497
- const messages = existing?.messages && existing.messages.length > 0
498
- ? existing.messages
499
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
500
- // Push user message
501
- messages.push({ role: "user", content: text });
502
- // Run agent
503
- const controller = new AbortController();
504
- const channelConfig = (0, config_2.getTeamsChannelConfig)();
505
- const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
506
- const traceId = (0, nerves_1.createTraceId)();
507
- const agentOptions = {};
508
- agentOptions.traceId = traceId;
509
- if (toolContext)
510
- agentOptions.toolContext = toolContext;
511
- if (channelConfig.skipConfirmation)
512
- agentOptions.skipConfirmation = true;
513
- agentOptions.drainSteeringFollowUps = () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text }));
514
- const result = await (0, core_1.runAgent)(messages, callbacks, "teams", controller.signal, agentOptions);
515
- // Flush any remaining accumulated text at end of turn
516
- await callbacks.flush();
517
- // After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
518
- // This must happen after the stream is done so the OAuth card renders properly.
519
- if (teamsContext) {
520
- const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
521
- if (allContent.includes("AUTH_REQUIRED:graph"))
522
- await teamsContext.signin("graph");
523
- if (allContent.includes("AUTH_REQUIRED:ado"))
524
- await teamsContext.signin("ado");
525
- if (allContent.includes("AUTH_REQUIRED:github"))
526
- await teamsContext.signin("github");
527
- }
528
- // Trim context and save session
529
- (0, context_1.postTurn)(messages, sessPath, result.usage);
530
- // Accumulate token usage on friend record
531
- if (toolContext?.context?.friend?.id) {
532
- await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
533
- }
534
- // SDK auto-closes the stream after our handler returns (app.process.js)
661
+ return [...names];
535
662
  }
536
- // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
537
- // Mode is determined by getTeamsConfig().clientId.
538
- // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
539
- function startTeamsApp() {
663
+ // Create an App instance from a TeamsConfig. Returns { app, mode }.
664
+ function createBotApp(teamsConfig) {
540
665
  const mentionStripping = { activity: { mentions: { stripText: true } } };
541
- const teamsConfig = (0, config_2.getTeamsConfig)();
542
- let app;
543
- let mode;
544
666
  const oauthConfig = (0, config_1.getOAuthConfig)();
545
- if (teamsConfig.clientId) {
546
- // Bot Service mode -- real Teams connection with SingleTenant credentials
547
- app = new teams_apps_1.App({
548
- clientId: teamsConfig.clientId,
549
- clientSecret: teamsConfig.clientSecret,
550
- tenantId: teamsConfig.tenantId,
551
- oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
552
- ...mentionStripping,
553
- });
554
- mode = "Bot Service";
667
+ if (teamsConfig.clientId && teamsConfig.clientSecret) {
668
+ return {
669
+ app: new teams_apps_1.App({
670
+ clientId: teamsConfig.clientId,
671
+ clientSecret: teamsConfig.clientSecret,
672
+ tenantId: teamsConfig.tenantId,
673
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
674
+ ...mentionStripping,
675
+ }),
676
+ mode: "Bot Service (client secret)",
677
+ };
678
+ }
679
+ else if (teamsConfig.clientId) {
680
+ return {
681
+ app: new teams_apps_1.App({
682
+ clientId: teamsConfig.clientId,
683
+ tenantId: teamsConfig.tenantId,
684
+ ...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
685
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
686
+ ...mentionStripping,
687
+ }),
688
+ mode: "Bot Service (managed identity)",
689
+ };
555
690
  }
556
691
  else {
557
- // DevtoolsPlugin mode -- local development with Teams DevtoolsPlugin UI
558
- app = new teams_apps_1.App({
559
- plugins: [new teams_dev_1.DevtoolsPlugin()],
560
- ...mentionStripping,
561
- });
562
- mode = "DevtoolsPlugin";
692
+ return {
693
+ app: new teams_apps_1.App({
694
+ plugins: [new teams_dev_1.DevtoolsPlugin()],
695
+ ...mentionStripping,
696
+ }),
697
+ mode: "DevtoolsPlugin",
698
+ };
563
699
  }
700
+ }
701
+ /* v8 ignore stop */
702
+ // Register message, verify-state, and error handlers on an App instance.
703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
704
+ function registerBotHandlers(app, label) {
705
+ const connectionNames = allOAuthConnectionNames();
564
706
  // Override default OAuth verify-state handler. The SDK's built-in handler
565
707
  // uses a single defaultConnectionName, which breaks multi-connection setups
566
708
  // (graph + ado + github). The verifyState activity only carries a `state`
567
709
  // code with no connectionName, so we try each configured connection until
568
710
  // one succeeds.
569
- const allConnectionNames = [
570
- oauthConfig.graphConnectionName,
571
- oauthConfig.adoConnectionName,
572
- oauthConfig.githubConnectionName,
573
- ].filter(Boolean);
574
711
  app.on("signin.verify-state", async (ctx) => {
575
712
  const { api, activity } = ctx;
576
713
  if (!activity.value?.state)
577
714
  return { status: 404 };
578
- for (const cn of allConnectionNames) {
715
+ for (const cn of connectionNames) {
579
716
  try {
580
717
  await api.users.token.get({
581
718
  channelId: activity.channelId,
@@ -583,12 +720,12 @@ function startTeamsApp() {
583
720
  connectionName: cn,
584
721
  code: activity.value.state,
585
722
  });
586
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
723
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
587
724
  return { status: 200 };
588
725
  }
589
726
  catch { /* try next */ }
590
727
  }
591
- (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: "verify-state failed for all connections", meta: {} });
728
+ (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
592
729
  return { status: 412 };
593
730
  });
594
731
  app.on("message", async (ctx) => {
@@ -598,18 +735,49 @@ function startTeamsApp() {
598
735
  const turnKey = teamsTurnKey(convId);
599
736
  const userId = activity.from?.id || "";
600
737
  const channelId = activity.channelId || "msteams";
601
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: "incoming teams message", meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
738
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
602
739
  // Resolve pending confirmations IMMEDIATELY — before token fetches or
603
740
  // the conversation lock. The original message holds the lock while
604
741
  // awaiting confirmation, so acquiring it here would deadlock. Token
605
742
  // fetches are also unnecessary (and slow) for a simple yes/no reply.
606
743
  if (resolvePendingConfirmation(convId, text)) {
607
- // Don't emit on this stream — the original message's stream is still
608
- // active. Opening a second streaming response in the same conversation
609
- // can corrupt the first. The original stream will show tool progress
610
- // once the confirmation Promise resolves.
611
744
  return;
612
745
  }
746
+ const commandRegistry = createTeamsCommandRegistry();
747
+ const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
748
+ if (parsedSlashCommand) {
749
+ const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
750
+ if (dispatchResult.handled && dispatchResult.result) {
751
+ if (dispatchResult.result.action === "response") {
752
+ stream.emit(dispatchResult.result.message || "");
753
+ return;
754
+ }
755
+ if (dispatchResult.result.action === "new") {
756
+ const commandStore = getFriendStore();
757
+ const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
758
+ const commandExternalId = activity.from?.aadObjectId || convId;
759
+ const commandResolver = new resolver_1.FriendResolver(commandStore, {
760
+ provider: commandProvider,
761
+ externalId: commandExternalId,
762
+ tenantId: activity.conversation?.tenantId,
763
+ displayName: activity.from?.name || "Unknown",
764
+ channel: "teams",
765
+ });
766
+ const commandContext = await commandResolver.resolve();
767
+ (0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
768
+ stream.emit("session cleared");
769
+ if (_turnCoordinator.isTurnActive(turnKey)) {
770
+ _turnCoordinator.enqueueFollowUp(turnKey, {
771
+ conversationId: convId,
772
+ text,
773
+ receivedAt: Date.now(),
774
+ effect: "clear_and_supersede",
775
+ });
776
+ }
777
+ return;
778
+ }
779
+ }
780
+ }
613
781
  // If this conversation already has an active turn, steer follow-up input
614
782
  // into that turn and avoid starting a second concurrent turn.
615
783
  if (!_turnCoordinator.tryBeginTurn(turnKey)) {
@@ -617,31 +785,35 @@ function startTeamsApp() {
617
785
  conversationId: convId,
618
786
  text,
619
787
  receivedAt: Date.now(),
788
+ effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
620
789
  });
621
790
  return;
622
791
  }
623
792
  try {
793
+ // Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
794
+ const tenantId = activity.conversation?.tenantId;
795
+ const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
624
796
  // Fetch tokens for both OAuth connections independently.
625
797
  // Failures are silently caught -- the tool handler will request signin if needed.
626
798
  let graphToken;
627
799
  let adoToken;
628
800
  let githubToken;
629
801
  try {
630
- const graphRes = await api.users.token.get({ userId, connectionName: oauthConfig.graphConnectionName, channelId });
802
+ const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
631
803
  graphToken = graphRes?.token;
632
804
  }
633
805
  catch { /* no token yet — tool handler will trigger signin */ }
634
806
  try {
635
- const adoRes = await api.users.token.get({ userId, connectionName: oauthConfig.adoConnectionName, channelId });
807
+ const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
636
808
  adoToken = adoRes?.token;
637
809
  }
638
810
  catch { /* no token yet — tool handler will trigger signin */ }
639
811
  try {
640
- const githubRes = await api.users.token.get({ userId, connectionName: oauthConfig.githubConnectionName, channelId });
812
+ const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
641
813
  githubToken = githubRes?.token;
642
814
  }
643
815
  catch { /* no token yet — tool handler will trigger signin */ }
644
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
816
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
645
817
  const teamsContext = {
646
818
  graphToken,
647
819
  adoToken,
@@ -661,6 +833,11 @@ function startTeamsApp() {
661
833
  aadObjectId: activity.from?.aadObjectId,
662
834
  tenantId: activity.conversation?.tenantId,
663
835
  displayName: activity.from?.name,
836
+ graphConnectionName: tenantOAuth.graphConnectionName,
837
+ adoConnectionName: tenantOAuth.adoConnectionName,
838
+ githubConnectionName: tenantOAuth.githubConnectionName,
839
+ /* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
840
+ botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
664
841
  };
665
842
  /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
666
843
  const ctxSend = async (t) => {
@@ -678,6 +855,227 @@ function startTeamsApp() {
678
855
  _turnCoordinator.endTurn(turnKey);
679
856
  }
680
857
  });
858
+ app.event("error", ({ error }) => {
859
+ const msg = error instanceof Error ? error.message : String(error);
860
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
861
+ });
862
+ }
863
+ function findAadObjectId(friend) {
864
+ for (const ext of friend.externalIds) {
865
+ if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
866
+ return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
867
+ }
868
+ }
869
+ return undefined;
870
+ }
871
+ function resolveTeamsFriendStore(deps) {
872
+ return deps.store
873
+ ?? deps.createFriendStore?.()
874
+ ?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
875
+ }
876
+ function getTeamsConversations(botApi) {
877
+ return botApi.conversations;
878
+ }
879
+ function hasExplicitCrossChatAuthorization(params) {
880
+ return params.intent === "explicit_cross_chat"
881
+ && types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
882
+ }
883
+ async function sendProactiveTeamsMessageToSession(params, deps) {
884
+ const store = resolveTeamsFriendStore(deps);
885
+ const conversations = getTeamsConversations(deps.botApi);
886
+ let friend;
887
+ try {
888
+ friend = await store.get(params.friendId);
889
+ }
890
+ catch {
891
+ friend = null;
892
+ }
893
+ if (!friend) {
894
+ (0, runtime_1.emitNervesEvent)({
895
+ level: "warn",
896
+ component: "senses",
897
+ event: "senses.teams_proactive_no_friend",
898
+ message: "proactive send skipped: friend not found",
899
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
900
+ });
901
+ return { delivered: false, reason: "friend_not_found" };
902
+ }
903
+ if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
904
+ (0, runtime_1.emitNervesEvent)({
905
+ component: "senses",
906
+ event: "senses.teams_proactive_trust_skip",
907
+ message: "proactive send skipped: trust level not allowed",
908
+ meta: {
909
+ friendId: params.friendId,
910
+ trustLevel: friend.trustLevel ?? "unknown",
911
+ intent: params.intent ?? "generic_outreach",
912
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
913
+ },
914
+ });
915
+ return { delivered: false, reason: "trust_skip" };
916
+ }
917
+ const aadInfo = findAadObjectId(friend);
918
+ if (!aadInfo) {
919
+ (0, runtime_1.emitNervesEvent)({
920
+ level: "warn",
921
+ component: "senses",
922
+ event: "senses.teams_proactive_no_aad_id",
923
+ message: "proactive send skipped: no AAD object ID found",
924
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
925
+ });
926
+ return { delivered: false, reason: "missing_target" };
927
+ }
928
+ try {
929
+ const conversation = await conversations.create({
930
+ bot: { id: deps.botApi.id },
931
+ members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
932
+ tenantId: aadInfo.tenantId,
933
+ isGroup: false,
934
+ });
935
+ await conversations.activities(conversation.id).create({
936
+ type: "message",
937
+ text: params.text,
938
+ });
939
+ (0, runtime_1.emitNervesEvent)({
940
+ component: "senses",
941
+ event: "senses.teams_proactive_sent",
942
+ message: "proactive teams message sent",
943
+ meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
944
+ });
945
+ return { delivered: true };
946
+ }
947
+ catch (error) {
948
+ (0, runtime_1.emitNervesEvent)({
949
+ level: "error",
950
+ component: "senses",
951
+ event: "senses.teams_proactive_send_error",
952
+ message: "proactive teams send failed",
953
+ meta: {
954
+ friendId: params.friendId,
955
+ aadObjectId: aadInfo.aadObjectId,
956
+ sessionKey: params.sessionKey,
957
+ reason: error instanceof Error ? error.message : String(error),
958
+ },
959
+ });
960
+ return { delivered: false, reason: "send_error" };
961
+ }
962
+ }
963
+ function scanPendingTeamsFiles(pendingRoot) {
964
+ const results = [];
965
+ let friendIds;
966
+ try {
967
+ friendIds = fs.readdirSync(pendingRoot);
968
+ }
969
+ catch {
970
+ return results;
971
+ }
972
+ for (const friendId of friendIds) {
973
+ const teamsDir = path.join(pendingRoot, friendId, "teams");
974
+ let keys;
975
+ try {
976
+ keys = fs.readdirSync(teamsDir);
977
+ }
978
+ catch {
979
+ continue;
980
+ }
981
+ for (const key of keys) {
982
+ const keyDir = path.join(teamsDir, key);
983
+ let files;
984
+ try {
985
+ files = fs.readdirSync(keyDir);
986
+ }
987
+ catch {
988
+ continue;
989
+ }
990
+ for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
991
+ const filePath = path.join(keyDir, file);
992
+ try {
993
+ const content = fs.readFileSync(filePath, "utf-8");
994
+ results.push({ friendId, key, filePath, content });
995
+ }
996
+ catch {
997
+ // skip unreadable files
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ return results;
1003
+ }
1004
+ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
1005
+ const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
1006
+ const pendingFiles = scanPendingTeamsFiles(root);
1007
+ const result = { sent: 0, skipped: 0, failed: 0 };
1008
+ for (const { friendId, key, filePath, content } of pendingFiles) {
1009
+ let parsed;
1010
+ try {
1011
+ parsed = JSON.parse(content);
1012
+ }
1013
+ catch {
1014
+ result.failed++;
1015
+ try {
1016
+ fs.unlinkSync(filePath);
1017
+ }
1018
+ catch { /* ignore */ }
1019
+ continue;
1020
+ }
1021
+ const messageText = typeof parsed.content === "string" ? parsed.content : "";
1022
+ if (!messageText.trim()) {
1023
+ result.skipped++;
1024
+ try {
1025
+ fs.unlinkSync(filePath);
1026
+ }
1027
+ catch { /* ignore */ }
1028
+ continue;
1029
+ }
1030
+ const sendResult = await sendProactiveTeamsMessageToSession({
1031
+ friendId,
1032
+ sessionKey: key,
1033
+ text: messageText,
1034
+ intent: "generic_outreach",
1035
+ }, {
1036
+ botApi,
1037
+ store,
1038
+ });
1039
+ if (sendResult.delivered) {
1040
+ result.sent++;
1041
+ try {
1042
+ fs.unlinkSync(filePath);
1043
+ }
1044
+ catch { /* ignore */ }
1045
+ continue;
1046
+ }
1047
+ if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
1048
+ result.skipped++;
1049
+ try {
1050
+ fs.unlinkSync(filePath);
1051
+ }
1052
+ catch { /* ignore */ }
1053
+ continue;
1054
+ }
1055
+ result.failed++;
1056
+ }
1057
+ if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
1058
+ (0, runtime_1.emitNervesEvent)({
1059
+ component: "senses",
1060
+ event: "senses.teams_proactive_drain_complete",
1061
+ message: "teams proactive drain complete",
1062
+ meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
1063
+ });
1064
+ }
1065
+ return result;
1066
+ }
1067
+ // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
1068
+ // Mode is determined by getTeamsConfig().clientId.
1069
+ // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
1070
+ //
1071
+ // Dual-bot support: if teamsSecondary is configured with a clientId, a second App
1072
+ // instance starts on an internal port and the primary app proxies requests from
1073
+ // /api/messages-secondary to it. This lets a single App Service serve two bot
1074
+ // registrations (e.g. one per tenant) without SDK modifications.
1075
+ function startTeamsApp() {
1076
+ const teamsConfig = (0, config_2.getTeamsConfig)();
1077
+ const { app, mode } = createBotApp(teamsConfig);
1078
+ registerBotHandlers(app, "primary");
681
1079
  if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
682
1080
  const handler = (err) => {
683
1081
  const msg = err instanceof Error ? err.message : String(err);
@@ -686,11 +1084,54 @@ function startTeamsApp() {
686
1084
  handler.__agentHandler = true;
687
1085
  process.on("unhandledRejection", handler);
688
1086
  }
689
- app.event("error", ({ error }) => {
690
- const msg = error instanceof Error ? error.message : String(error);
691
- (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
692
- });
693
- const port = (0, config_2.getTeamsChannelConfig)().port;
1087
+ /* v8 ignore next -- PORT env branch; runtime-only @preserve */
1088
+ const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
694
1089
  app.start(port);
695
- (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode } });
1090
+ // Diagnostic: log tool count at startup to verify deploy
1091
+ const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
1092
+ const toolNames = startupTools.map((t) => t.function.name);
1093
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
1094
+ // --- Secondary bot (dual-bot support) ---
1095
+ // If teamsSecondary has a clientId, start a second App on an internal port
1096
+ // and proxy /api/messages-secondary on the primary app to it.
1097
+ /* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
1098
+ const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
1099
+ if (secondaryConfig.clientId) {
1100
+ const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
1101
+ registerBotHandlers(secondaryApp, "secondary");
1102
+ secondaryApp.start(SECONDARY_INTERNAL_PORT);
1103
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
1104
+ // Proxy: forward /api/messages-secondary on the primary app's Express
1105
+ // to localhost:SECONDARY_INTERNAL_PORT/api/messages.
1106
+ // The SDK's HttpPlugin exposes .post() bound to its Express instance.
1107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1108
+ const httpPlugin = app.http;
1109
+ httpPlugin.post("/api/messages-secondary", (req, res) => {
1110
+ const body = JSON.stringify(req.body);
1111
+ const proxyReq = http.request({
1112
+ hostname: "127.0.0.1",
1113
+ port: SECONDARY_INTERNAL_PORT,
1114
+ path: "/api/messages",
1115
+ method: "POST",
1116
+ headers: {
1117
+ ...req.headers,
1118
+ "content-length": Buffer.byteLength(body).toString(),
1119
+ },
1120
+ }, (proxyRes) => {
1121
+ res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
1122
+ proxyRes.pipe(res);
1123
+ });
1124
+ proxyReq.on("error", (err) => {
1125
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
1126
+ if (!res.headersSent) {
1127
+ res.writeHead(502);
1128
+ res.end("Bad Gateway");
1129
+ }
1130
+ });
1131
+ proxyReq.write(body);
1132
+ proxyReq.end();
1133
+ });
1134
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
1135
+ }
1136
+ /* v8 ignore stop */
696
1137
  }