@hahnfeld/teams-adapter 1.0.9

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/dist/activity.d.ts +23 -0
  4. package/dist/activity.d.ts.map +1 -0
  5. package/dist/activity.js +3 -0
  6. package/dist/activity.js.map +1 -0
  7. package/dist/adapter.d.ts +174 -0
  8. package/dist/adapter.d.ts.map +1 -0
  9. package/dist/adapter.js +1583 -0
  10. package/dist/adapter.js.map +1 -0
  11. package/dist/app-package.d.ts +7 -0
  12. package/dist/app-package.d.ts.map +1 -0
  13. package/dist/app-package.js +158 -0
  14. package/dist/app-package.js.map +1 -0
  15. package/dist/assistant.d.ts +7 -0
  16. package/dist/assistant.d.ts.map +1 -0
  17. package/dist/assistant.js +32 -0
  18. package/dist/assistant.js.map +1 -0
  19. package/dist/commands/admin.d.ts +27 -0
  20. package/dist/commands/admin.d.ts.map +1 -0
  21. package/dist/commands/admin.js +146 -0
  22. package/dist/commands/admin.js.map +1 -0
  23. package/dist/commands/agents.d.ts +13 -0
  24. package/dist/commands/agents.d.ts.map +1 -0
  25. package/dist/commands/agents.js +98 -0
  26. package/dist/commands/agents.js.map +1 -0
  27. package/dist/commands/doctor.d.ts +8 -0
  28. package/dist/commands/doctor.d.ts.map +1 -0
  29. package/dist/commands/doctor.js +49 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/index.d.ts +16 -0
  32. package/dist/commands/index.d.ts.map +1 -0
  33. package/dist/commands/index.js +253 -0
  34. package/dist/commands/index.js.map +1 -0
  35. package/dist/commands/integrate.d.ts +8 -0
  36. package/dist/commands/integrate.d.ts.map +1 -0
  37. package/dist/commands/integrate.js +45 -0
  38. package/dist/commands/integrate.js.map +1 -0
  39. package/dist/commands/menu.d.ts +16 -0
  40. package/dist/commands/menu.d.ts.map +1 -0
  41. package/dist/commands/menu.js +92 -0
  42. package/dist/commands/menu.js.map +1 -0
  43. package/dist/commands/new-session.d.ts +13 -0
  44. package/dist/commands/new-session.d.ts.map +1 -0
  45. package/dist/commands/new-session.js +105 -0
  46. package/dist/commands/new-session.js.map +1 -0
  47. package/dist/commands/session.d.ts +22 -0
  48. package/dist/commands/session.d.ts.map +1 -0
  49. package/dist/commands/session.js +110 -0
  50. package/dist/commands/session.js.map +1 -0
  51. package/dist/commands/settings.d.ts +8 -0
  52. package/dist/commands/settings.d.ts.map +1 -0
  53. package/dist/commands/settings.js +54 -0
  54. package/dist/commands/settings.js.map +1 -0
  55. package/dist/conversation-store.d.ts +38 -0
  56. package/dist/conversation-store.d.ts.map +1 -0
  57. package/dist/conversation-store.js +101 -0
  58. package/dist/conversation-store.js.map +1 -0
  59. package/dist/draft-manager.d.ts +47 -0
  60. package/dist/draft-manager.d.ts.map +1 -0
  61. package/dist/draft-manager.js +136 -0
  62. package/dist/draft-manager.js.map +1 -0
  63. package/dist/formatting.d.ts +121 -0
  64. package/dist/formatting.d.ts.map +1 -0
  65. package/dist/formatting.js +392 -0
  66. package/dist/formatting.js.map +1 -0
  67. package/dist/graph.d.ts +59 -0
  68. package/dist/graph.d.ts.map +1 -0
  69. package/dist/graph.js +261 -0
  70. package/dist/graph.js.map +1 -0
  71. package/dist/index.d.ts +16 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +10 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/media.d.ts +29 -0
  76. package/dist/media.d.ts.map +1 -0
  77. package/dist/media.js +120 -0
  78. package/dist/media.js.map +1 -0
  79. package/dist/permissions.d.ts +15 -0
  80. package/dist/permissions.d.ts.map +1 -0
  81. package/dist/permissions.js +221 -0
  82. package/dist/permissions.js.map +1 -0
  83. package/dist/plugin.d.ts +13 -0
  84. package/dist/plugin.d.ts.map +1 -0
  85. package/dist/plugin.js +689 -0
  86. package/dist/plugin.js.map +1 -0
  87. package/dist/renderer.d.ts +49 -0
  88. package/dist/renderer.d.ts.map +1 -0
  89. package/dist/renderer.js +55 -0
  90. package/dist/renderer.js.map +1 -0
  91. package/dist/send-utils.d.ts +15 -0
  92. package/dist/send-utils.d.ts.map +1 -0
  93. package/dist/send-utils.js +64 -0
  94. package/dist/send-utils.js.map +1 -0
  95. package/dist/task-modules.d.ts +34 -0
  96. package/dist/task-modules.d.ts.map +1 -0
  97. package/dist/task-modules.js +136 -0
  98. package/dist/task-modules.js.map +1 -0
  99. package/dist/types.d.ts +26 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +3 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/validators.d.ts +54 -0
  104. package/dist/validators.d.ts.map +1 -0
  105. package/dist/validators.js +142 -0
  106. package/dist/validators.js.map +1 -0
  107. package/package.json +56 -0
package/dist/plugin.js ADDED
@@ -0,0 +1,689 @@
1
+ import { TeamsAdapter } from "./adapter.js";
2
+ import { DEFAULT_BOT_PORT } from "./types.js";
3
+ /**
4
+ * Factory for the Teams adapter plugin.
5
+ *
6
+ * Includes a full interactive `install()` wizard that guides users through:
7
+ * 1. Azure Bot registration (with Portal URLs and step-by-step instructions)
8
+ * 2. Credential validation (real-time token acquisition test)
9
+ * 3. Tenant configuration (single vs multi-tenant)
10
+ * 4. Team/channel selection (auto-discovery via Graph or manual)
11
+ * 5. Optional notification channel and Graph API file sharing
12
+ */
13
+ export default function createTeamsPlugin() {
14
+ let adapter = null;
15
+ return {
16
+ name: "@hahnfeld/teams-adapter",
17
+ version: "1.0.0",
18
+ description: "Microsoft Teams adapter with Adaptive Cards, commands, and streaming",
19
+ essential: false,
20
+ permissions: ["services:register", "kernel:access", "events:read", "commands:register"],
21
+ // TODO: Add Zod settingsSchema when @openacp/plugin-sdk exports a schema builder.
22
+ // Required fields: enabled, botAppId, botAppPassword, tenantId, teamId, channelId
23
+ // Optional: notificationChannelId, assistantThreadId, graphClientSecret
24
+ // ─── Interactive Install Wizard ──────────────────────────────────────
25
+ async install(ctx) {
26
+ const { terminal, settings } = ctx;
27
+ const { validateBotCredentials, validateTenant, discoverTeamsAndChannels, parseTeamsLink } = await import("./validators.js");
28
+ // ── Step 1: Azure Bot Registration Guidance ──
29
+ terminal.note("This wizard will help you connect OpenACP to Microsoft Teams.\n" +
30
+ "You'll need an Azure Bot registration. If you don't have one yet,\n" +
31
+ "follow these steps first:\n" +
32
+ "\n" +
33
+ " 1. Go to: https://portal.azure.com/#create/Microsoft.AzureBot\n" +
34
+ " 2. Fill in:\n" +
35
+ " - Bot handle: any unique name (e.g. 'openacp-bot')\n" +
36
+ " - Pricing: Free (F0) for testing\n" +
37
+ " - App type: 'Single Tenant' for enterprise, 'Multi Tenant' for public\n" +
38
+ " - Creation type: 'Create new Microsoft App ID'\n" +
39
+ " 3. Click 'Create' and wait for deployment\n" +
40
+ " 4. Go to the Bot resource → Settings → Configuration\n" +
41
+ " - Copy the 'Microsoft App ID'\n" +
42
+ " 5. Go to 'Manage Password' → 'New client secret'\n" +
43
+ " - Copy the secret value immediately (it's shown only once)\n" +
44
+ " 6. Under 'Channels', add the 'Microsoft Teams' channel\n" +
45
+ "\n" +
46
+ "Docs: https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration", "Azure Bot Setup");
47
+ const ready = await terminal.confirm({
48
+ message: "Do you have your Bot App ID and Password ready?",
49
+ initialValue: true,
50
+ });
51
+ if (!ready) {
52
+ terminal.log.info("No worries! Set up your Azure Bot first, then run this again.");
53
+ terminal.cancel("Setup cancelled — re-run when ready.");
54
+ return;
55
+ }
56
+ // ── Step 2: Bot App ID ──
57
+ let botAppId = await terminal.text({
58
+ message: "Bot App ID (Microsoft App ID from Azure Portal):",
59
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
60
+ validate: (val) => {
61
+ const trimmed = val.trim();
62
+ if (!trimmed)
63
+ return "App ID cannot be empty";
64
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {
65
+ return "App ID should be a GUID (e.g. 12345678-1234-1234-1234-123456789abc)";
66
+ }
67
+ return undefined;
68
+ },
69
+ });
70
+ botAppId = botAppId.trim();
71
+ // ── Step 3: Bot App Password ──
72
+ terminal.log.info("Find this in Azure Portal → Bot resource → Manage Password → Client secrets");
73
+ let botAppPassword = await terminal.password({
74
+ message: "Bot App Password (client secret):",
75
+ validate: (val) => {
76
+ if (!val.trim())
77
+ return "Password cannot be empty";
78
+ return undefined;
79
+ },
80
+ });
81
+ botAppPassword = botAppPassword.trim();
82
+ // ── Step 4: Tenant Configuration ──
83
+ // Collected before credential validation so we can use the correct tenant endpoint.
84
+ terminal.log.info("");
85
+ terminal.note("Azure bots can be single-tenant (one organization) or multi-tenant (any org).\n" +
86
+ "\n" +
87
+ " Single-tenant: For enterprise use within your organization.\n" +
88
+ " Find your Tenant ID at:\n" +
89
+ " Azure Portal → Microsoft Entra ID → Overview → Tenant ID\n" +
90
+ "\n" +
91
+ " Multi-tenant: For bots available to any Microsoft 365 organization.\n" +
92
+ " Uses the default 'botframework.com' tenant.", "Tenant Type");
93
+ const tenantType = await terminal.select({
94
+ message: "What type of bot registration?",
95
+ options: [
96
+ { value: "single", label: "Single-tenant (enterprise)", hint: "Most common for internal bots" },
97
+ { value: "multi", label: "Multi-tenant (public)" },
98
+ ],
99
+ });
100
+ let tenantId = "botframework.com";
101
+ if (tenantType === "single") {
102
+ while (true) {
103
+ const tenantInput = await terminal.text({
104
+ message: "Tenant ID (GUID from Azure Portal → Entra ID → Overview):",
105
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
106
+ validate: (val) => {
107
+ const trimmed = val.trim();
108
+ if (!trimmed)
109
+ return "Tenant ID cannot be empty";
110
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {
111
+ return "Tenant ID should be a GUID";
112
+ }
113
+ return undefined;
114
+ },
115
+ });
116
+ tenantId = tenantInput.trim();
117
+ const spin = terminal.spinner();
118
+ spin.start("Validating tenant...");
119
+ const result = await validateTenant(botAppId, botAppPassword, tenantId);
120
+ if (result.ok) {
121
+ spin.stop(`Tenant validated: ${result.tenantName ?? tenantId}`);
122
+ break;
123
+ }
124
+ spin.fail(result.error);
125
+ const action = await terminal.select({
126
+ message: "What to do?",
127
+ options: [
128
+ { value: "retry", label: "Re-enter tenant ID" },
129
+ { value: "skip", label: "Use as-is (skip validation)" },
130
+ ],
131
+ });
132
+ if (action === "skip")
133
+ break;
134
+ }
135
+ }
136
+ // ── Step 4b: Validate Credentials (now that we know the tenant) ──
137
+ let credentialsValidated = false;
138
+ while (!credentialsValidated) {
139
+ const spin = terminal.spinner();
140
+ spin.start("Validating bot credentials...");
141
+ const result = await validateBotCredentials(botAppId, botAppPassword, tenantId !== "botframework.com" ? tenantId : undefined);
142
+ if (result.ok) {
143
+ spin.stop("Bot credentials validated successfully");
144
+ credentialsValidated = true;
145
+ break;
146
+ }
147
+ spin.fail(result.error);
148
+ const action = await terminal.select({
149
+ message: "What would you like to do?",
150
+ options: [
151
+ { value: "retry", label: "Re-enter App ID and password" },
152
+ { value: "skip", label: "Skip validation (use as-is)" },
153
+ ],
154
+ });
155
+ if (action === "skip")
156
+ break;
157
+ botAppId = await terminal.text({
158
+ message: "Bot App ID:",
159
+ defaultValue: botAppId,
160
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
161
+ });
162
+ botAppId = botAppId.trim();
163
+ botAppPassword = await terminal.password({
164
+ message: "Bot App Password:",
165
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
166
+ });
167
+ botAppPassword = botAppPassword.trim();
168
+ }
169
+ // ── Step 5: Team & Channel Selection ──
170
+ terminal.log.info("");
171
+ let teamId = "";
172
+ let channelId = "";
173
+ // Try auto-discovery (only for single-tenant bots — multi-tenant can't use Graph with botframework.com)
174
+ let discovery = { ok: false, error: "skipped" };
175
+ if (tenantType === "single") {
176
+ const spin2 = terminal.spinner();
177
+ spin2.start("Discovering Teams and channels...");
178
+ discovery = await discoverTeamsAndChannels(botAppId, botAppPassword, tenantId);
179
+ if (discovery.ok && discovery.teams.length > 0) {
180
+ spin2.stop(`Found ${discovery.teams.length} team(s)`);
181
+ const teamOptions = discovery.teams.map((t) => ({
182
+ value: t.id,
183
+ label: t.name,
184
+ hint: `${t.channels.length} channel(s)`,
185
+ }));
186
+ teamOptions.push({ value: "__manual__", label: "Enter manually instead", hint: "" });
187
+ const selectedTeam = await terminal.select({
188
+ message: "Which team should the bot operate in?",
189
+ options: teamOptions,
190
+ });
191
+ if (selectedTeam !== "__manual__") {
192
+ teamId = selectedTeam;
193
+ const team = discovery.teams.find((t) => t.id === selectedTeam);
194
+ if (team && team.channels.length > 0) {
195
+ const channelOptions = team.channels.map((c) => ({
196
+ value: c.id,
197
+ label: c.name,
198
+ }));
199
+ channelId = await terminal.select({
200
+ message: "Which channel should be the default?",
201
+ options: channelOptions,
202
+ });
203
+ }
204
+ else {
205
+ channelId = await terminal.text({
206
+ message: "Channel ID (no channels found — enter manually):",
207
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
208
+ });
209
+ channelId = channelId.trim();
210
+ }
211
+ }
212
+ }
213
+ else {
214
+ if (discovery.ok) {
215
+ spin2.stop("No teams found (bot may not be added to any team yet)");
216
+ }
217
+ else {
218
+ spin2.stop("Auto-discovery not available — will use manual entry");
219
+ terminal.log.info(`(${discovery.error})`);
220
+ }
221
+ }
222
+ } // end single-tenant discovery
223
+ // Manual entry fallback
224
+ if (!teamId || !channelId) {
225
+ terminal.log.info("");
226
+ terminal.note("To find your Team and Channel IDs:\n" +
227
+ "\n" +
228
+ " Option A — From a channel link:\n" +
229
+ " 1. Open Microsoft Teams\n" +
230
+ " 2. Right-click the channel name → 'Get link to channel'\n" +
231
+ " 3. Paste the link below — we'll extract the IDs automatically\n" +
232
+ "\n" +
233
+ " Option B — Manual entry:\n" +
234
+ " Team ID = the 'groupId' parameter from the link\n" +
235
+ " Channel ID = the encoded string in the path (e.g. 19:xxx@thread.tacv2)", "Finding Team & Channel IDs");
236
+ const method = await terminal.select({
237
+ message: "How to provide Team and Channel IDs?",
238
+ options: [
239
+ { value: "link", label: "Paste a channel link (easiest)", hint: "Right-click channel → Get link" },
240
+ { value: "manual", label: "Enter IDs manually" },
241
+ ],
242
+ });
243
+ if (method === "link") {
244
+ const link = await terminal.text({
245
+ message: "Paste the Teams channel link:",
246
+ validate: (v) => {
247
+ if (!v.trim())
248
+ return "Link cannot be empty";
249
+ if (!v.includes("teams.microsoft.com") && !v.includes("teams.cloud.microsoft"))
250
+ return "This doesn't look like a Teams link";
251
+ return undefined;
252
+ },
253
+ });
254
+ const parsed = parseTeamsLink(link.trim());
255
+ if (parsed.teamId && parsed.channelId) {
256
+ teamId = parsed.teamId;
257
+ channelId = parsed.channelId;
258
+ terminal.log.success(`Extracted Team ID: ${teamId}`);
259
+ terminal.log.success(`Extracted Channel ID: ${channelId}`);
260
+ if (parsed.tenantId && tenantType === "single" && parsed.tenantId !== tenantId) {
261
+ terminal.log.warning(`Link tenant (${parsed.tenantId}) differs from configured tenant (${tenantId})`);
262
+ }
263
+ }
264
+ else {
265
+ terminal.log.warning("Could not extract IDs from link. Please enter manually.");
266
+ }
267
+ }
268
+ if (!teamId) {
269
+ teamId = await terminal.text({
270
+ message: "Team ID (groupId GUID):",
271
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
272
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
273
+ });
274
+ teamId = teamId.trim();
275
+ }
276
+ if (!channelId) {
277
+ channelId = await terminal.text({
278
+ message: "Channel ID (e.g. 19:abc123@thread.tacv2):",
279
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
280
+ });
281
+ channelId = channelId.trim();
282
+ }
283
+ }
284
+ // ── Step 6: Notification Channel (Optional) ──
285
+ terminal.log.info("");
286
+ const wantNotifications = await terminal.confirm({
287
+ message: "Set up a dedicated notification channel? (Session completions, errors, permission alerts)",
288
+ initialValue: false,
289
+ });
290
+ let notificationChannelId = null;
291
+ if (wantNotifications) {
292
+ if (discovery.ok) {
293
+ const team = discovery.teams.find((t) => t.id === teamId);
294
+ if (team && team.channels.length > 1) {
295
+ const otherChannels = team.channels
296
+ .filter((c) => c.id !== channelId)
297
+ .map((c) => ({ value: c.id, label: c.name }));
298
+ otherChannels.push({ value: "__manual__", label: "Enter manually" });
299
+ const selected = await terminal.select({
300
+ message: "Which channel for notifications?",
301
+ options: otherChannels,
302
+ });
303
+ if (selected !== "__manual__") {
304
+ notificationChannelId = selected;
305
+ }
306
+ }
307
+ }
308
+ if (!notificationChannelId) {
309
+ const nid = await terminal.text({
310
+ message: "Notification channel ID (or leave empty to skip):",
311
+ defaultValue: "",
312
+ });
313
+ notificationChannelId = nid.trim() || null;
314
+ }
315
+ }
316
+ // ── Step 7: Graph API for File Sharing (Optional) ──
317
+ terminal.log.info("");
318
+ const wantGraph = await terminal.confirm({
319
+ message: "Enable file sharing via OneDrive? (Allows sharing agent-generated files in Teams)",
320
+ initialValue: false,
321
+ });
322
+ let graphClientSecret;
323
+ if (wantGraph) {
324
+ terminal.note("File sharing requires a Graph API client secret with Files.ReadWrite.All permission.\n" +
325
+ "\n" +
326
+ "To set this up:\n" +
327
+ " 1. Azure Portal → App Registrations → find your bot's app\n" +
328
+ " 2. API Permissions → Add → Microsoft Graph → Application permissions\n" +
329
+ " → Files.ReadWrite.All → Grant admin consent\n" +
330
+ " 3. Certificates & secrets → New client secret → copy the value\n" +
331
+ "\n" +
332
+ "Note: You can use the same app registration as your bot.\n" +
333
+ "The client secret can be different from the bot password.", "Graph API Setup");
334
+ const useExisting = await terminal.confirm({
335
+ message: "Use the same client secret as the bot password?",
336
+ initialValue: true,
337
+ });
338
+ if (useExisting) {
339
+ graphClientSecret = botAppPassword;
340
+ terminal.log.success("Using bot password for Graph API");
341
+ }
342
+ else {
343
+ graphClientSecret = await terminal.password({
344
+ message: "Graph API client secret:",
345
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
346
+ });
347
+ graphClientSecret = graphClientSecret.trim();
348
+ }
349
+ }
350
+ // ── Step 8: Save & Summary ──
351
+ // ── Step 8b: Bot Port ──
352
+ terminal.log.info("");
353
+ terminal.note("The Teams adapter runs its own HTTP server for Bot Framework messages.\n" +
354
+ "This is separate from the OpenACP API server.\n" +
355
+ "\n" +
356
+ ` Default port: ${DEFAULT_BOT_PORT} (Bot Framework standard)\n` +
357
+ " Your tunnel must point to this port, not the OpenACP API port.", "Bot Port");
358
+ const useDefaultPort = await terminal.confirm({
359
+ message: `Use the default bot port (${DEFAULT_BOT_PORT})?`,
360
+ initialValue: true,
361
+ });
362
+ let botPort = DEFAULT_BOT_PORT;
363
+ if (!useDefaultPort) {
364
+ const portInput = await terminal.text({
365
+ message: "Bot port:",
366
+ defaultValue: String(DEFAULT_BOT_PORT),
367
+ validate: (v) => {
368
+ const n = Number(v.trim());
369
+ if (isNaN(n) || !Number.isInteger(n) || n < 1 || n > 65535)
370
+ return "Port must be 1–65535";
371
+ return undefined;
372
+ },
373
+ });
374
+ botPort = Number(portInput.trim());
375
+ }
376
+ // ── Step 8c: Tunnel Method ──
377
+ terminal.log.info("");
378
+ terminal.note("Azure Bot Service needs a public URL to reach your bot.\n" +
379
+ "A tunnel exposes your local bot port to the internet.\n" +
380
+ "\n" +
381
+ ` Your bot port: ${botPort}`, "Tunnel Setup");
382
+ const tunnelMethod = await terminal.select({
383
+ message: "How should the bot port be tunneled?",
384
+ options: [
385
+ { value: "devtunnel", label: "@hahnfeld/devtunnel-provider (Recommended)", hint: "Microsoft Dev Tunnels — persistent URLs" },
386
+ { value: "builtin", label: "Built-in tunnel service", hint: "Uses the system tunnel service if available" },
387
+ { value: "manual", label: "Manual", hint: "I'll set up my own tunnel (ngrok, cloudflared, etc.)" },
388
+ ],
389
+ });
390
+ // ── Step 8d: Save Settings ──
391
+ await settings.setAll({
392
+ enabled: true,
393
+ botAppId,
394
+ botAppPassword,
395
+ tenantId,
396
+ teamId,
397
+ channelId,
398
+ notificationChannelId,
399
+ assistantThreadId: null,
400
+ botPort,
401
+ tunnelMethod,
402
+ ...(graphClientSecret ? { graphClientSecret } : {}),
403
+ });
404
+ terminal.log.success("Teams adapter configured!");
405
+ terminal.log.info("");
406
+ terminal.note(`Bot App ID: ${botAppId}\n` +
407
+ `Tenant: ${tenantId === "botframework.com" ? "Multi-tenant" : tenantId}\n` +
408
+ `Team ID: ${teamId}\n` +
409
+ `Channel ID: ${channelId}\n` +
410
+ `Bot port: ${botPort}\n` +
411
+ `Notifications: ${notificationChannelId ?? "Not configured"}\n` +
412
+ `File sharing: ${graphClientSecret ? "Enabled (Graph API)" : "Disabled"}`, "Configuration Summary");
413
+ // ── Step 9: Generate Teams App Package ──
414
+ let appPackagePath = null;
415
+ try {
416
+ const { generateTeamsAppPackage } = await import("./app-package.js");
417
+ appPackagePath = await generateTeamsAppPackage(botAppId, ctx);
418
+ if (appPackagePath) {
419
+ terminal.log.success(`Teams app package created: ${appPackagePath}`);
420
+ }
421
+ }
422
+ catch {
423
+ // Non-fatal — user can create manually
424
+ }
425
+ // Tunnel-specific guidance for next steps
426
+ let tunnelStep;
427
+ if (tunnelMethod === "devtunnel") {
428
+ tunnelStep =
429
+ ` 2. Install the Dev Tunnels plugin:\n` +
430
+ " openacp plugin install @hahnfeld/devtunnel-provider\n" +
431
+ ` It will automatically tunnel port ${botPort} with a persistent URL.`;
432
+ }
433
+ else if (tunnelMethod === "builtin") {
434
+ tunnelStep =
435
+ ` 2. Tunnel: Will be created automatically on startup via the built-in tunnel service.\n` +
436
+ ` Ensure a tunnel service plugin is installed and running.`;
437
+ }
438
+ else {
439
+ tunnelStep =
440
+ ` 2. Set up a tunnel to expose port ${botPort} (the Bot Framework port):\n` +
441
+ " Use ngrok, cloudflared, or any tunnel pointing to localhost:" + botPort;
442
+ }
443
+ terminal.log.info("");
444
+ terminal.note("Next steps:\n" +
445
+ " 1. Upload the Teams app package to your team:\n" +
446
+ (appPackagePath
447
+ ? ` File: ${appPackagePath}\n`
448
+ : " Generate it with: openacp plugin configure @hahnfeld/teams-adapter\n") +
449
+ " Teams → Apps → Manage your apps → Upload a custom app\n" +
450
+ tunnelStep + "\n" +
451
+ " 3. Set the bot's messaging endpoint in Azure:\n" +
452
+ " Azure Portal → Bot resource → Configuration → Messaging endpoint\n" +
453
+ " Example: https://<your-tunnel-url>/api/messages\n" +
454
+ ` 4. Note: The bot port (${botPort}) is NOT the same as the OpenACP API port.\n` +
455
+ " Your tunnel must point to the bot port.\n" +
456
+ " 5. Start OpenACP: openacp start", "Next Steps");
457
+ },
458
+ // ─── Configure (post-install changes) ────────────────────────────────
459
+ async configure(ctx) {
460
+ const { terminal, settings } = ctx;
461
+ const current = await settings.getAll();
462
+ const { validateBotCredentials } = await import("./validators.js");
463
+ const choice = await terminal.select({
464
+ message: "What to configure?",
465
+ options: [
466
+ { value: "credentials", label: "Change bot credentials (App ID / Password)" },
467
+ { value: "tenant", label: "Change tenant ID" },
468
+ { value: "team", label: "Change team / channel" },
469
+ { value: "botPort", label: `Change bot port (current: ${current.botPort ?? DEFAULT_BOT_PORT})` },
470
+ { value: "notifications", label: "Change notification channel" },
471
+ { value: "graph", label: "Configure file sharing (Graph API)" },
472
+ { value: "tunnel", label: `Change tunnel method (current: ${current.tunnelMethod || "devtunnel"})` },
473
+ { value: "appPackage", label: "Regenerate Teams app package (openacp-bot.zip)" },
474
+ { value: "done", label: "Done" },
475
+ ],
476
+ });
477
+ switch (choice) {
478
+ case "credentials": {
479
+ const appId = await terminal.text({
480
+ message: "Bot App ID:",
481
+ defaultValue: current.botAppId ?? "",
482
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
483
+ });
484
+ const password = await terminal.password({
485
+ message: "Bot App Password:",
486
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
487
+ });
488
+ const spin = terminal.spinner();
489
+ spin.start("Validating...");
490
+ const existingTenant = current.tenantId ?? undefined;
491
+ const result = await validateBotCredentials(appId.trim(), password.trim(), existingTenant !== "botframework.com" ? existingTenant : undefined);
492
+ if (result.ok) {
493
+ spin.stop("Credentials validated");
494
+ await settings.set("botAppId", appId.trim());
495
+ await settings.set("botAppPassword", password.trim());
496
+ terminal.log.success("Credentials updated");
497
+ }
498
+ else {
499
+ spin.fail(result.error);
500
+ const save = await terminal.confirm({ message: "Save anyway?", initialValue: false });
501
+ if (save) {
502
+ await settings.set("botAppId", appId.trim());
503
+ await settings.set("botAppPassword", password.trim());
504
+ }
505
+ }
506
+ break;
507
+ }
508
+ case "tenant": {
509
+ const tid = await terminal.text({
510
+ message: "Tenant ID (GUID, or 'botframework.com' for multi-tenant):",
511
+ defaultValue: current.tenantId ?? "",
512
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
513
+ });
514
+ await settings.set("tenantId", tid.trim());
515
+ terminal.log.success("Tenant ID updated");
516
+ break;
517
+ }
518
+ case "team": {
519
+ const tid = await terminal.text({
520
+ message: "Team ID:",
521
+ defaultValue: current.teamId ?? "",
522
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
523
+ });
524
+ const cid = await terminal.text({
525
+ message: "Channel ID:",
526
+ defaultValue: current.channelId ?? "",
527
+ validate: (v) => (!v.trim() ? "Cannot be empty" : undefined),
528
+ });
529
+ await settings.set("teamId", tid.trim());
530
+ await settings.set("channelId", cid.trim());
531
+ terminal.log.success("Team and channel updated");
532
+ break;
533
+ }
534
+ case "botPort": {
535
+ const portInput = await terminal.text({
536
+ message: `Bot port (Bot Framework HTTP server, default ${DEFAULT_BOT_PORT}):`,
537
+ defaultValue: String(current.botPort ?? DEFAULT_BOT_PORT),
538
+ validate: (v) => {
539
+ const n = Number(v.trim());
540
+ if (isNaN(n) || !Number.isInteger(n) || n < 1 || n > 65535)
541
+ return "Port must be 1–65535";
542
+ return undefined;
543
+ },
544
+ });
545
+ await settings.set("botPort", Number(portInput.trim()));
546
+ terminal.log.success(`Bot port updated to ${portInput.trim()}`);
547
+ terminal.log.info("Remember to update your tunnel to point to this port.");
548
+ break;
549
+ }
550
+ case "notifications": {
551
+ const nid = await terminal.text({
552
+ message: "Notification channel ID (empty to disable):",
553
+ defaultValue: current.notificationChannelId ?? "",
554
+ });
555
+ await settings.set("notificationChannelId", nid.trim() || null);
556
+ terminal.log.success(nid.trim() ? "Notification channel updated" : "Notifications disabled");
557
+ break;
558
+ }
559
+ case "graph": {
560
+ const secret = await terminal.password({
561
+ message: "Graph API client secret (empty to disable file sharing):",
562
+ });
563
+ if (secret.trim()) {
564
+ await settings.set("graphClientSecret", secret.trim());
565
+ terminal.log.success("Graph API configured for file sharing");
566
+ }
567
+ else {
568
+ await settings.delete("graphClientSecret");
569
+ terminal.log.success("File sharing disabled");
570
+ }
571
+ break;
572
+ }
573
+ case "tunnel": {
574
+ const method = await terminal.select({
575
+ message: "How should the bot port be tunneled?",
576
+ options: [
577
+ { value: "devtunnel", label: "@hahnfeld/devtunnel-provider (Recommended)", hint: "Microsoft Dev Tunnels — persistent URLs" },
578
+ { value: "builtin", label: "Built-in tunnel service", hint: "Uses the system tunnel service if available" },
579
+ { value: "manual", label: "Manual", hint: "I'll set up my own tunnel (ngrok, cloudflared, etc.)" },
580
+ ],
581
+ });
582
+ await settings.set("tunnelMethod", method);
583
+ terminal.log.success(`Tunnel method: ${method === "builtin" ? "built-in (auto-create)" : method === "devtunnel" ? "devtunnel plugin" : "manual"}`);
584
+ if (method === "devtunnel") {
585
+ terminal.log.info("Install with: openacp plugin install @hahnfeld/devtunnel-provider");
586
+ }
587
+ break;
588
+ }
589
+ case "appPackage": {
590
+ const appId = current.botAppId ?? "";
591
+ if (!appId) {
592
+ terminal.log.error("Bot App ID is not configured. Set credentials first.");
593
+ break;
594
+ }
595
+ try {
596
+ const { generateTeamsAppPackage } = await import("./app-package.js");
597
+ const path = await generateTeamsAppPackage(appId, ctx);
598
+ if (path) {
599
+ terminal.log.success(`Teams app package created: ${path}`);
600
+ terminal.log.info("Upload it in Teams → Apps → Manage your apps → Upload a custom app");
601
+ }
602
+ else {
603
+ terminal.log.error("Failed to generate app package");
604
+ }
605
+ }
606
+ catch (err) {
607
+ terminal.log.error(`App package generation failed: ${err instanceof Error ? err.message : String(err)}`);
608
+ }
609
+ break;
610
+ }
611
+ case "done":
612
+ break;
613
+ }
614
+ },
615
+ // ─── Uninstall ───────────────────────────────────────────────────────
616
+ async uninstall(ctx, opts) {
617
+ if (opts.purge) {
618
+ await ctx.settings.clear();
619
+ ctx.terminal.log.success("Teams adapter settings cleared");
620
+ }
621
+ ctx.terminal.note("Don't forget to:\n" +
622
+ " 1. Remove the bot from your Teams team (if no longer needed)\n" +
623
+ " 2. Delete the Azure Bot resource (if no longer needed)\n" +
624
+ " Azure Portal → Bot resource → Delete", "Cleanup Reminder");
625
+ },
626
+ // ─── Runtime Setup ───────────────────────────────────────────────────
627
+ async setup(ctx) {
628
+ const rawPort = ctx.pluginConfig.botPort;
629
+ const botPort = typeof rawPort === "number" ? rawPort : (typeof rawPort === "string" ? Number(rawPort) : DEFAULT_BOT_PORT) || DEFAULT_BOT_PORT;
630
+ ctx.registerEditableFields([
631
+ { key: "enabled", displayName: "Enabled", type: "toggle", scope: "safe", hotReload: false },
632
+ { key: "botAppId", displayName: "Bot App ID", type: "string", scope: "sensitive", hotReload: false },
633
+ { key: "tenantId", displayName: "Tenant ID", type: "string", scope: "safe", hotReload: false },
634
+ { key: "teamId", displayName: "Team ID", type: "string", scope: "safe", hotReload: false },
635
+ { key: "channelId", displayName: "Channel ID", type: "string", scope: "safe", hotReload: false },
636
+ { key: "botPort", displayName: "Bot Port", type: "number", scope: "safe", hotReload: false },
637
+ { key: "tunnelMethod", displayName: "Tunnel method", type: "string", scope: "safe", hotReload: false },
638
+ ]);
639
+ const config = ctx.pluginConfig;
640
+ if (!config.enabled || !config.botAppId) {
641
+ ctx.log.info("Teams adapter disabled (missing enabled or botAppId)");
642
+ return;
643
+ }
644
+ if (adapter) {
645
+ ctx.log.warn("Teams adapter setup() called again — skipping (already running)");
646
+ return;
647
+ }
648
+ adapter = new TeamsAdapter(ctx.core, config);
649
+ ctx.registerService("adapter:teams", adapter);
650
+ ctx.log.info("Teams adapter registered");
651
+ // Only create a tunnel if the user opted into the built-in tunnel service during install.
652
+ // Most users should use @hahnfeld/devtunnel-provider instead (persistent URLs, auto-managed).
653
+ const tunnelMethod = ctx.pluginConfig.tunnelMethod || "devtunnel";
654
+ if (tunnelMethod === "builtin") {
655
+ // Note: addTunnel() exists on the concrete TunnelService class but is not on
656
+ // the exported TunnelServiceInterface type — use runtime typeof check.
657
+ const tunnelSvc = ctx.getService("tunnel");
658
+ if (tunnelSvc && typeof tunnelSvc.addTunnel === "function") {
659
+ try {
660
+ const entry = await tunnelSvc.addTunnel(botPort, { label: "teams-bot" });
661
+ if (entry?.publicUrl) {
662
+ ctx.log.info(`Teams bot tunnel active — messaging endpoint: ${entry.publicUrl}/api/messages`);
663
+ ctx.log.info("Set this URL as the messaging endpoint in Azure Portal → Bot resource → Configuration");
664
+ }
665
+ }
666
+ catch (err) {
667
+ ctx.log.warn(`Could not create tunnel for bot port ${botPort}: ${err.message}`);
668
+ ctx.log.info(`Tunnel your bot manually: <tunnel-url> → localhost:${botPort}`);
669
+ ctx.log.info("Tip: Install @hahnfeld/devtunnel-provider for Microsoft Dev Tunnels integration");
670
+ }
671
+ }
672
+ else {
673
+ ctx.log.warn(`Auto-tunnel enabled but no tunnel service available — install a tunnel plugin`);
674
+ ctx.log.info("Recommended: openacp plugin install @hahnfeld/devtunnel-provider");
675
+ }
676
+ }
677
+ else {
678
+ ctx.log.info(`Bot listening on port ${botPort} — tunnel method: ${tunnelMethod}`);
679
+ }
680
+ },
681
+ async teardown() {
682
+ if (adapter) {
683
+ await adapter.stop();
684
+ adapter = null;
685
+ }
686
+ },
687
+ };
688
+ }
689
+ //# sourceMappingURL=plugin.js.map