@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.100

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