@yemi33/minions 0.1.1936 → 0.1.1938
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.
- package/README.md +0 -2
- package/dashboard/js/command-center.js +12 -15
- package/dashboard/js/render-utils.js +20 -0
- package/dashboard/js/settings.js +1 -36
- package/dashboard.js +34 -153
- package/docs/README.md +1 -3
- package/docs/deprecated.json +24 -0
- package/docs/rfc-completion-json.md +4 -4
- package/engine/ado.js +0 -17
- package/engine/cc-worker-pool.js +54 -25
- package/engine/cli.js +0 -14
- package/engine/github.js +0 -17
- package/engine/lifecycle.js +0 -29
- package/engine/preflight.js +0 -19
- package/engine/shared.js +0 -13
- package/package.json +1 -4
- package/docs/teams-production.md +0 -370
- package/docs/teams-setup.md +0 -352
- package/engine/teams-cards.js +0 -137
- package/engine/teams.js +0 -647
package/engine/cc-worker-pool.js
CHANGED
|
@@ -252,17 +252,19 @@ class Worker {
|
|
|
252
252
|
try { this.inflight.onChunk(text); } catch { /* swallow */ }
|
|
253
253
|
}
|
|
254
254
|
} else if (update.sessionUpdate === 'tool_call' && this.inflight.onToolUse) {
|
|
255
|
-
// ACP `tool_call` (
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// formatToolSummary (Bash → "$ <cmd>", Read → "Reading <path>", etc.)
|
|
259
|
-
// works unchanged. Status updates (`tool_call_update`, status:
|
|
260
|
-
// completed) carry the result and are ignored here — surfacing
|
|
261
|
-
// results too would double the chip count without adding info the
|
|
262
|
-
// user can act on.
|
|
255
|
+
// ACP `tool_call` (pending) → Claude-style {name, input} via
|
|
256
|
+
// _mapAcpToolCallToToolUse so the dashboard's formatToolSummary
|
|
257
|
+
// formatters (Bash → "$ <cmd>", etc.) work unchanged.
|
|
263
258
|
const mapped = _mapAcpToolCallToToolUse(update);
|
|
264
259
|
if (mapped) {
|
|
265
|
-
try { this.inflight.onToolUse(mapped.name, mapped.input); }
|
|
260
|
+
try { this.inflight.onToolUse(mapped.name, mapped.input, update.toolCallId); }
|
|
261
|
+
catch { /* swallow */ }
|
|
262
|
+
}
|
|
263
|
+
} else if (update.sessionUpdate === 'tool_call_update' && this.inflight.onToolUpdate) {
|
|
264
|
+
// Terminal-state updates only (completed/failed). In-progress updates
|
|
265
|
+
// exist in the ACP spec but add chip churn without actionable info.
|
|
266
|
+
if (update.status === 'completed' || update.status === 'failed') {
|
|
267
|
+
try { this.inflight.onToolUpdate(update.toolCallId, update.status); }
|
|
266
268
|
catch { /* swallow */ }
|
|
267
269
|
}
|
|
268
270
|
}
|
|
@@ -290,7 +292,7 @@ class Worker {
|
|
|
290
292
|
|
|
291
293
|
// ── Stream a single turn ───────────────────────────────────────────────
|
|
292
294
|
stream(promptText, opts = {}) {
|
|
293
|
-
const { onChunk, onToolUse, onDone, onError, signal, systemPromptText } = opts;
|
|
295
|
+
const { onChunk, onToolUse, onToolUpdate, onDone, onError, signal, systemPromptText } = opts;
|
|
294
296
|
if (this.killed) {
|
|
295
297
|
const err = new Error('cc-worker-pool: tab is closed');
|
|
296
298
|
if (onError) try { onError(err); } catch { /* swallow */ }
|
|
@@ -319,6 +321,7 @@ class Worker {
|
|
|
319
321
|
sessionId: this.sessionId,
|
|
320
322
|
onChunk,
|
|
321
323
|
onToolUse,
|
|
324
|
+
onToolUpdate,
|
|
322
325
|
onDone,
|
|
323
326
|
onError,
|
|
324
327
|
signal,
|
|
@@ -449,31 +452,44 @@ function _mapAcpToolCallToToolUse(update) {
|
|
|
449
452
|
const rawInput = (update.rawInput && typeof update.rawInput === 'object') ? update.rawInput : {};
|
|
450
453
|
const kind = String(update.kind || '').toLowerCase();
|
|
451
454
|
const title = update.title || '';
|
|
452
|
-
|
|
455
|
+
|
|
456
|
+
// Field-name normalization: Copilot ACP and Claude tool_use use different
|
|
457
|
+
// keys for the same concept (Copilot `path`, Claude `file_path`). Normalize
|
|
458
|
+
// here so the dashboard's formatToolSummary — written against Claude's
|
|
459
|
+
// names — produces the same chip text on both runtimes.
|
|
460
|
+
const filePath = rawInput.file_path || rawInput.path || rawInput.filePath || '';
|
|
461
|
+
|
|
462
|
+
// Pattern (Grep) — Copilot uses `paths` (plural) for the search scope where
|
|
463
|
+
// Claude's Grep takes `path`.
|
|
464
|
+
if (typeof rawInput.pattern === 'string') {
|
|
465
|
+
return {
|
|
466
|
+
name: 'Grep',
|
|
467
|
+
input: { pattern: rawInput.pattern, path: rawInput.paths || rawInput.path || '.' },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
453
471
|
switch (kind) {
|
|
454
472
|
case 'execute':
|
|
455
473
|
return { name: 'Bash', input: rawInput };
|
|
456
474
|
case 'read':
|
|
457
|
-
|
|
475
|
+
// ACP overloads `read` for both file-view and grep — the pattern check
|
|
476
|
+
// above already handled grep, so this branch is the file-view case.
|
|
477
|
+
return { name: 'Read', input: { file_path: filePath } };
|
|
458
478
|
case 'edit':
|
|
459
|
-
return { name: 'Edit', input:
|
|
460
|
-
case 'search':
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
// (matches the dashboard's Grep formatter "Searching <pat> in <path>").
|
|
464
|
-
const isGrep = typeof rawInput.path === 'string' || typeof rawInput.regex === 'string';
|
|
465
|
-
return { name: isGrep ? 'Grep' : 'Glob', input: rawInput };
|
|
466
|
-
}
|
|
479
|
+
return { name: 'Edit', input: { file_path: filePath } };
|
|
480
|
+
case 'search':
|
|
481
|
+
// Pattern check above handled the grep case; arriving here means glob.
|
|
482
|
+
return { name: 'Glob', input: rawInput };
|
|
467
483
|
case 'fetch':
|
|
468
484
|
return { name: 'WebFetch', input: rawInput };
|
|
469
485
|
case 'think':
|
|
470
|
-
// No equivalent Claude tool; show the title so the user sees Copilot's
|
|
471
|
-
// own description of what it's thinking about.
|
|
472
486
|
return { name: title || 'Think', input: rawInput };
|
|
473
487
|
default:
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
488
|
+
// Unknown kind — use ACP's human-readable title as the chip label and
|
|
489
|
+
// drop rawInput so formatToolSummary's default branch shows just the
|
|
490
|
+
// title (avoids `<title>(<key>: <val>)` clutter when the input shape
|
|
491
|
+
// is unfamiliar).
|
|
492
|
+
return { name: title || kind || 'Tool', input: {} };
|
|
477
493
|
}
|
|
478
494
|
}
|
|
479
495
|
|
|
@@ -552,6 +568,18 @@ function closeTab(tabId) {
|
|
|
552
568
|
worker.close();
|
|
553
569
|
}
|
|
554
570
|
|
|
571
|
+
// Cancel the currently in-flight prompt on this tab without killing the
|
|
572
|
+
// worker. Sends ACP `session/cancel` so the remote daemon stops generating;
|
|
573
|
+
// the warm process + initialized MCP servers + session state are preserved
|
|
574
|
+
// so the next prompt skips the ~7-8 s cold-spawn cost. No-op if the tab has
|
|
575
|
+
// no worker or no inflight prompt.
|
|
576
|
+
function cancelInflight(tabId) {
|
|
577
|
+
const worker = _tabs.get(tabId);
|
|
578
|
+
if (!worker || worker.killed || !worker.inflight) return false;
|
|
579
|
+
try { worker.cancel(); } catch { /* swallow */ }
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
555
583
|
function shutdown() {
|
|
556
584
|
for (const worker of _tabs.values()) {
|
|
557
585
|
try { worker.close(); } catch { /* swallow */ }
|
|
@@ -583,6 +611,7 @@ function _reapIdleTabs() {
|
|
|
583
611
|
module.exports = {
|
|
584
612
|
getSession,
|
|
585
613
|
closeTab,
|
|
614
|
+
cancelInflight,
|
|
586
615
|
shutdown,
|
|
587
616
|
// Exposed for unit tests; engine code MUST go through the public API.
|
|
588
617
|
_internals,
|
package/engine/cli.js
CHANGED
|
@@ -731,19 +731,6 @@ const commands = {
|
|
|
731
731
|
}
|
|
732
732
|
}, 1000);
|
|
733
733
|
|
|
734
|
-
// Teams inbox poll timer — process incoming Teams messages through CC
|
|
735
|
-
const teams = require('./teams');
|
|
736
|
-
const teamsInboxInterval = config.teams?.inboxPollInterval ?? shared.ENGINE_DEFAULTS.teams.inboxPollInterval;
|
|
737
|
-
const teamsInboxTimer = teams.isTeamsEnabled() ? setInterval(() => {
|
|
738
|
-
try {
|
|
739
|
-
const ctrl = getControl();
|
|
740
|
-
if (ctrl.state !== 'running') return;
|
|
741
|
-
teams.processTeamsInbox().catch(err => {
|
|
742
|
-
shared.log('warn', `Teams inbox poll error: ${err.message}`);
|
|
743
|
-
});
|
|
744
|
-
} catch {}
|
|
745
|
-
}, teamsInboxInterval) : null;
|
|
746
|
-
|
|
747
734
|
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
|
|
748
735
|
console.log('Press Ctrl+C to stop');
|
|
749
736
|
|
|
@@ -804,7 +791,6 @@ const commands = {
|
|
|
804
791
|
console.log(`\n${signal} received — initiating graceful shutdown...`);
|
|
805
792
|
clearInterval(tickTimer);
|
|
806
793
|
clearInterval(fastPollTimer);
|
|
807
|
-
if (teamsInboxTimer) clearInterval(teamsInboxTimer);
|
|
808
794
|
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
|
|
809
795
|
const stoppingAt = e.ts();
|
|
810
796
|
const stoppingWrite = markControlStoppingForOwner(controlOwner, stoppingAt);
|
package/engine/github.js
CHANGED
|
@@ -646,14 +646,6 @@ async function pollPrStatus(config) {
|
|
|
646
646
|
pr.reviewStatus = newReviewStatus;
|
|
647
647
|
updated = true;
|
|
648
648
|
shared.trackReviewMetric(pr, newReviewStatus, config);
|
|
649
|
-
if (newReviewStatus === 'approved') {
|
|
650
|
-
// Teams notification for PR approval — non-blocking, edge-triggered (only on transition)
|
|
651
|
-
try {
|
|
652
|
-
const teams = require('./teams');
|
|
653
|
-
const prFilePath = shared.projectPrPath(project);
|
|
654
|
-
teams.teamsNotifyPrEvent(pr, 'pr-approved', project, prFilePath).catch(() => {});
|
|
655
|
-
} catch {}
|
|
656
|
-
}
|
|
657
649
|
}
|
|
658
650
|
}
|
|
659
651
|
|
|
@@ -717,15 +709,6 @@ async function pollPrStatus(config) {
|
|
|
717
709
|
}
|
|
718
710
|
}
|
|
719
711
|
updated = true;
|
|
720
|
-
|
|
721
|
-
if (buildStatus === 'failing') {
|
|
722
|
-
// Teams notification for build failure — non-blocking
|
|
723
|
-
try {
|
|
724
|
-
const teams = require('./teams');
|
|
725
|
-
const prFilePath = shared.projectPrPath(project);
|
|
726
|
-
teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
|
|
727
|
-
} catch {}
|
|
728
|
-
}
|
|
729
712
|
}
|
|
730
713
|
if (buildStatus === 'failing') {
|
|
731
714
|
if (buildFailReason && pr.buildFailReason !== buildFailReason) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -377,26 +377,11 @@ function checkPlanCompletion(meta, config) {
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
if (createdAny) {
|
|
381
|
-
try {
|
|
382
|
-
const teams = require('./teams');
|
|
383
|
-
teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
|
|
384
|
-
} catch {}
|
|
385
|
-
}
|
|
386
380
|
if (reopenedAny) log('info', `Plan ${planFile}: re-opened verify WI(s) for modified plan`);
|
|
387
381
|
}
|
|
388
382
|
|
|
389
383
|
// Archive deferred until verify completes
|
|
390
384
|
|
|
391
|
-
// Teams notification for plan completion — non-blocking
|
|
392
|
-
try {
|
|
393
|
-
const teams = require('./teams');
|
|
394
|
-
teams.teamsNotifyPlanEvent({
|
|
395
|
-
name: plan.plan_summary || planFile, file: planFile, project: plan.project,
|
|
396
|
-
doneCount: doneItems.length, totalCount: planFeatureIds.size,
|
|
397
|
-
}, 'plan-completed').catch(() => {});
|
|
398
|
-
} catch {}
|
|
399
|
-
|
|
400
385
|
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
401
386
|
return true;
|
|
402
387
|
}
|
|
@@ -2271,14 +2256,6 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
2271
2256
|
});
|
|
2272
2257
|
}
|
|
2273
2258
|
|
|
2274
|
-
// Teams PR lifecycle notification — non-blocking
|
|
2275
|
-
try {
|
|
2276
|
-
const teams = require('./teams');
|
|
2277
|
-
const prEvent = newStatus === PR_STATUS.MERGED ? 'pr-merged' : 'pr-abandoned';
|
|
2278
|
-
const prFilePath = project ? projectPrPath(project) : null;
|
|
2279
|
-
teams.teamsNotifyPrEvent(pr, prEvent, project, prFilePath).catch(() => {});
|
|
2280
|
-
} catch {}
|
|
2281
|
-
|
|
2282
2259
|
log('info', `Post-merge hooks completed for ${pr.id}`);
|
|
2283
2260
|
}
|
|
2284
2261
|
|
|
@@ -3719,12 +3696,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
3719
3696
|
const metricsResult = isAutoRetry ? 'retry' : finalResult;
|
|
3720
3697
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
3721
3698
|
|
|
3722
|
-
// Teams notification — non-blocking
|
|
3723
|
-
try {
|
|
3724
|
-
const teams = require('./teams');
|
|
3725
|
-
teams.teamsNotifyCompletion(dispatchItem, finalResult, agentId).catch(() => {});
|
|
3726
|
-
} catch {}
|
|
3727
|
-
|
|
3728
3699
|
return { resultSummary, taskUsage, autoRecovered, structuredCompletion, completionContractFailure, agentReportedFailure, agentRetryable };
|
|
3729
3700
|
}
|
|
3730
3701
|
|
package/engine/preflight.js
CHANGED
|
@@ -580,25 +580,6 @@ function doctor(minionsHome) {
|
|
|
580
580
|
} else {
|
|
581
581
|
runtimeResults.push({ name: 'Agents configured', ok: false, message: 'no agents in config.json — re-run: minions init (see docs/auto-discovery.md)' });
|
|
582
582
|
}
|
|
583
|
-
|
|
584
|
-
// Check Teams integration — supports client secret OR certificate auth
|
|
585
|
-
const teams = config.teams;
|
|
586
|
-
if (teams && teams.enabled === true) {
|
|
587
|
-
const hasSecret = !!teams.appId && !!teams.appPassword;
|
|
588
|
-
const hasCert = !!teams.appId && !!teams.certPath && !!teams.privateKeyPath && !!teams.tenantId;
|
|
589
|
-
if (!hasSecret && !hasCert) {
|
|
590
|
-
const missing = [
|
|
591
|
-
!teams.appId && 'appId',
|
|
592
|
-
!teams.appPassword && !teams.certPath && 'appPassword or certPath+privateKeyPath+tenantId',
|
|
593
|
-
].filter(Boolean).join(', ');
|
|
594
|
-
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: `enabled but missing: ${missing} — see docs/teams-setup.md` });
|
|
595
|
-
} else {
|
|
596
|
-
const authMode = hasCert ? 'certificate' : 'client secret';
|
|
597
|
-
runtimeResults.push({ name: 'Teams integration', ok: true, message: `configured (${authMode})` });
|
|
598
|
-
}
|
|
599
|
-
} else {
|
|
600
|
-
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: 'disabled — see docs/teams-setup.md' });
|
|
601
|
-
}
|
|
602
583
|
} catch {
|
|
603
584
|
runtimeResults.push({ name: 'Config', ok: false, message: `missing or invalid — run: minions init (see docs/distribution.md)` });
|
|
604
585
|
}
|
package/engine/shared.js
CHANGED
|
@@ -1147,19 +1147,6 @@ const ENGINE_DEFAULTS = {
|
|
|
1147
1147
|
// knows which subkeys to flag as deprecated. Do not consume `claude.*` in new code — use the runtime
|
|
1148
1148
|
// adapter system (engine/runtimes/) and the resolveAgent*/resolveCc* helpers instead.
|
|
1149
1149
|
_deprecatedConfigClaudeFields: ['binary', 'outputFormat', 'allowedTools', 'maxTurns', 'effort', 'budgetCap'],
|
|
1150
|
-
// Teams integration — config.teams shape: { enabled, appId, appPassword, certPath, privateKeyPath, tenantId, notifyEvents, ccMirror, inboxPollInterval }
|
|
1151
|
-
// Auth modes: (1) appId + appPassword (client secret), or (2) appId + certPath + privateKeyPath + tenantId (certificate)
|
|
1152
|
-
teams: {
|
|
1153
|
-
enabled: false,
|
|
1154
|
-
appId: '',
|
|
1155
|
-
appPassword: '',
|
|
1156
|
-
certPath: '', // PEM certificate file path (certificate auth)
|
|
1157
|
-
privateKeyPath: '', // PEM private key file path (certificate auth)
|
|
1158
|
-
tenantId: '', // Azure AD tenant ID (required for certificate auth)
|
|
1159
|
-
notifyEvents: ['pr-merged', 'agent-completed', 'plan-completed', 'agent-failed'],
|
|
1160
|
-
ccMirror: true,
|
|
1161
|
-
inboxPollInterval: 15000,
|
|
1162
|
-
},
|
|
1163
1150
|
};
|
|
1164
1151
|
|
|
1165
1152
|
// ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1938",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
|
@@ -63,9 +63,6 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@playwright/test": "^1.58.2"
|
|
65
65
|
},
|
|
66
|
-
"dependencies": {
|
|
67
|
-
"botbuilder": "4.23.3"
|
|
68
|
-
},
|
|
69
66
|
"publishConfig": {
|
|
70
67
|
"access": "public"
|
|
71
68
|
}
|
package/docs/teams-production.md
DELETED
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
# Teams Production Endpoint Migration
|
|
2
|
-
|
|
3
|
-
> Last verified: 2026-05-12. `botbuilder` dependency confirmed in `package.json` (4.23.3); `/api/bot` route confirmed in `dashboard.js` (`handleTeamsBot`).
|
|
4
|
-
|
|
5
|
-
Guide for migrating the Minions Teams bot from a Dev Tunnel to a stable public HTTPS endpoint for production use. Choose one of the three deployment options below based on your infrastructure.
|
|
6
|
-
|
|
7
|
-
**Key fact:** The Azure Bot messaging endpoint URL can be changed at any time in the Azure Portal — it takes effect immediately. No bot reinstallation is needed in Teams. This means you can switch between Dev Tunnel and production endpoints freely.
|
|
8
|
-
|
|
9
|
-
**Prerequisites:**
|
|
10
|
-
|
|
11
|
-
- A working Teams integration via Dev Tunnel (see [docs/teams-setup.md](teams-setup.md))
|
|
12
|
-
- Azure CLI installed (`az --version`) for Options 1 and 2
|
|
13
|
-
- A public-facing server or VM for Option 3
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Option 1: Azure App Service
|
|
18
|
-
|
|
19
|
-
Deploy the Minions dashboard as an Azure App Service with a stable FQDN.
|
|
20
|
-
|
|
21
|
-
### Steps
|
|
22
|
-
|
|
23
|
-
1. **Create an App Service Plan** (skip if you have one):
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
az appservice plan create \
|
|
27
|
-
--name minions-plan \
|
|
28
|
-
--resource-group rg-minions \
|
|
29
|
-
--sku B1 \
|
|
30
|
-
--is-linux
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
2. **Create the Web App:**
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
az webapp create \
|
|
37
|
-
--name minions-dashboard \
|
|
38
|
-
--resource-group rg-minions \
|
|
39
|
-
--plan minions-plan \
|
|
40
|
-
--runtime "NODE:20-lts"
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
This creates a publicly accessible URL: `https://minions-dashboard.azurewebsites.net`
|
|
44
|
-
|
|
45
|
-
3. **Configure environment variables:**
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
az webapp config appsettings set \
|
|
49
|
-
--name minions-dashboard \
|
|
50
|
-
--resource-group rg-minions \
|
|
51
|
-
--settings \
|
|
52
|
-
PORT=8080 \
|
|
53
|
-
NODE_ENV=production
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
> Azure App Service routes external port 443 (HTTPS) to your app's internal port (default 8080). Set `PORT=8080` so the dashboard listens on the expected port.
|
|
57
|
-
|
|
58
|
-
4. **Deploy the code:**
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
# From the minions repository root
|
|
62
|
-
az webapp deploy \
|
|
63
|
-
--name minions-dashboard \
|
|
64
|
-
--resource-group rg-minions \
|
|
65
|
-
--src-path . \
|
|
66
|
-
--type zip
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Alternatively, configure continuous deployment from your Git repository:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
az webapp deployment source config \
|
|
73
|
-
--name minions-dashboard \
|
|
74
|
-
--resource-group rg-minions \
|
|
75
|
-
--repo-url https://github.com/your-org/minions \
|
|
76
|
-
--branch master \
|
|
77
|
-
--manual-integration
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
5. **Copy `config.json` to the App Service.** The simplest approach is to use the Kudu console or App Service Editor to upload your `config.json` to the application root. Alternatively, mount an Azure File Share containing your config.
|
|
81
|
-
|
|
82
|
-
6. **Update the Azure Bot messaging endpoint:**
|
|
83
|
-
|
|
84
|
-
- Open the [Azure Portal](https://portal.azure.com) > your Azure Bot resource > **Configuration**.
|
|
85
|
-
- Change the **Messaging endpoint** to:
|
|
86
|
-
```
|
|
87
|
-
https://minions-dashboard.azurewebsites.net/api/bot
|
|
88
|
-
```
|
|
89
|
-
- Click **Apply**. The change takes effect immediately.
|
|
90
|
-
|
|
91
|
-
### Verify
|
|
92
|
-
|
|
93
|
-
1. Open `https://minions-dashboard.azurewebsites.net/api/routes` in a browser — you should see the API route list.
|
|
94
|
-
2. In the Azure Bot resource, click **Test in Web Chat** and send a message.
|
|
95
|
-
3. Send a message in Teams to the bot — confirm it receives and responds.
|
|
96
|
-
|
|
97
|
-
### Rollback
|
|
98
|
-
|
|
99
|
-
To revert to Dev Tunnel:
|
|
100
|
-
|
|
101
|
-
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
102
|
-
2. Update the Azure Bot messaging endpoint back to your tunnel URL: `https://<tunnel>.devtunnels.ms/api/bot`
|
|
103
|
-
3. Click **Apply**. Traffic returns to your local machine immediately.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## Option 2: Azure Container App
|
|
108
|
-
|
|
109
|
-
Containerize the dashboard and deploy to Azure Container Apps with a stable FQDN.
|
|
110
|
-
|
|
111
|
-
### Steps
|
|
112
|
-
|
|
113
|
-
1. **Create a Dockerfile** in the repository root:
|
|
114
|
-
|
|
115
|
-
```dockerfile
|
|
116
|
-
FROM node:20-slim
|
|
117
|
-
WORKDIR /app
|
|
118
|
-
COPY package*.json ./
|
|
119
|
-
RUN npm ci --omit=dev 2>/dev/null || true
|
|
120
|
-
COPY . .
|
|
121
|
-
EXPOSE 7331
|
|
122
|
-
CMD ["node", "dashboard.js"]
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
> Minions ships with `botbuilder` as its only runtime dependency (declared in `package.json`); other operations rely on Node.js built-ins, so `npm ci` is a fast install.
|
|
126
|
-
|
|
127
|
-
2. **Build and push to Azure Container Registry:**
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
# Create a container registry (skip if you have one)
|
|
131
|
-
az acr create \
|
|
132
|
-
--name minionsacr \
|
|
133
|
-
--resource-group rg-minions \
|
|
134
|
-
--sku Basic
|
|
135
|
-
|
|
136
|
-
# Build and push
|
|
137
|
-
az acr build \
|
|
138
|
-
--registry minionsacr \
|
|
139
|
-
--image minions-dashboard:latest .
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
3. **Create a Container Apps environment** (skip if you have one):
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
az containerapp env create \
|
|
146
|
-
--name minions-env \
|
|
147
|
-
--resource-group rg-minions \
|
|
148
|
-
--location eastus
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
4. **Deploy the container:**
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
az containerapp create \
|
|
155
|
-
--name minions-dashboard \
|
|
156
|
-
--resource-group rg-minions \
|
|
157
|
-
--environment minions-env \
|
|
158
|
-
--image minionsacr.azurecr.io/minions-dashboard:latest \
|
|
159
|
-
--registry-server minionsacr.azurecr.io \
|
|
160
|
-
--target-port 7331 \
|
|
161
|
-
--ingress external \
|
|
162
|
-
--min-replicas 1 \
|
|
163
|
-
--max-replicas 1 \
|
|
164
|
-
--env-vars NODE_ENV=production
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
> Use `--min-replicas 1 --max-replicas 1` because the Minions engine uses file-based state that doesn't support multiple replicas.
|
|
168
|
-
|
|
169
|
-
5. **Get the FQDN:**
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
az containerapp show \
|
|
173
|
-
--name minions-dashboard \
|
|
174
|
-
--resource-group rg-minions \
|
|
175
|
-
--query "properties.configuration.ingress.fqdn" \
|
|
176
|
-
--output tsv
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
This returns something like: `minions-dashboard.happyfield-abc123.eastus.azurecontainerapps.io`
|
|
180
|
-
|
|
181
|
-
6. **Update the Azure Bot messaging endpoint:**
|
|
182
|
-
|
|
183
|
-
- Open the [Azure Portal](https://portal.azure.com) > your Azure Bot resource > **Configuration**.
|
|
184
|
-
- Change the **Messaging endpoint** to:
|
|
185
|
-
```
|
|
186
|
-
https://minions-dashboard.happyfield-abc123.eastus.azurecontainerapps.io/api/bot
|
|
187
|
-
```
|
|
188
|
-
- Click **Apply**. The change takes effect immediately.
|
|
189
|
-
|
|
190
|
-
### Verify
|
|
191
|
-
|
|
192
|
-
1. Open `https://<your-fqdn>/api/routes` in a browser.
|
|
193
|
-
2. Test via Azure Bot **Test in Web Chat**.
|
|
194
|
-
3. Send a message in Teams — confirm end-to-end flow works.
|
|
195
|
-
|
|
196
|
-
### Rollback
|
|
197
|
-
|
|
198
|
-
To revert to Dev Tunnel:
|
|
199
|
-
|
|
200
|
-
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
201
|
-
2. Update the Azure Bot messaging endpoint back to your tunnel URL.
|
|
202
|
-
3. Click **Apply**. Immediate switchover.
|
|
203
|
-
|
|
204
|
-
Optionally stop the container to save costs:
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
az containerapp update \
|
|
208
|
-
--name minions-dashboard \
|
|
209
|
-
--resource-group rg-minions \
|
|
210
|
-
--min-replicas 0 --max-replicas 0
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
## Option 3: Reverse Proxy (nginx / Caddy)
|
|
216
|
-
|
|
217
|
-
For servers with a public IP address or an existing reverse proxy setup.
|
|
218
|
-
|
|
219
|
-
### Steps (Caddy — recommended for simplicity)
|
|
220
|
-
|
|
221
|
-
Caddy automatically provisions and renews TLS certificates via Let's Encrypt.
|
|
222
|
-
|
|
223
|
-
1. **Install Caddy:**
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
# Debian/Ubuntu
|
|
227
|
-
sudo apt install -y caddy
|
|
228
|
-
|
|
229
|
-
# macOS
|
|
230
|
-
brew install caddy
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
2. **Configure Caddy.** Create or edit `/etc/caddy/Caddyfile`:
|
|
234
|
-
|
|
235
|
-
```
|
|
236
|
-
minions.yourdomain.com {
|
|
237
|
-
reverse_proxy localhost:7331
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
> Replace `minions.yourdomain.com` with your actual domain. Ensure a DNS A record points this domain to your server's public IP.
|
|
242
|
-
|
|
243
|
-
3. **Start Caddy:**
|
|
244
|
-
|
|
245
|
-
```bash
|
|
246
|
-
sudo systemctl enable --now caddy
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Caddy automatically obtains a Let's Encrypt TLS certificate for your domain.
|
|
250
|
-
|
|
251
|
-
4. **Start the Minions dashboard:**
|
|
252
|
-
|
|
253
|
-
```bash
|
|
254
|
-
minions dash
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Or run it as a systemd service for persistence:
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
# /etc/systemd/system/minions-dashboard.service
|
|
261
|
-
[Unit]
|
|
262
|
-
Description=Minions Dashboard
|
|
263
|
-
After=network.target
|
|
264
|
-
|
|
265
|
-
[Service]
|
|
266
|
-
Type=simple
|
|
267
|
-
User=your-user
|
|
268
|
-
WorkingDirectory=/path/to/minions
|
|
269
|
-
ExecStart=/usr/bin/node dashboard.js
|
|
270
|
-
Restart=on-failure
|
|
271
|
-
|
|
272
|
-
[Install]
|
|
273
|
-
WantedBy=multi-user.target
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
```bash
|
|
277
|
-
sudo systemctl enable --now minions-dashboard
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
5. **Update the Azure Bot messaging endpoint:**
|
|
281
|
-
|
|
282
|
-
- Open the Azure Portal > your Azure Bot resource > **Configuration**.
|
|
283
|
-
- Change the **Messaging endpoint** to:
|
|
284
|
-
```
|
|
285
|
-
https://minions.yourdomain.com/api/bot
|
|
286
|
-
```
|
|
287
|
-
- Click **Apply**. The change takes effect immediately.
|
|
288
|
-
|
|
289
|
-
### Steps (nginx)
|
|
290
|
-
|
|
291
|
-
1. **Install nginx and certbot:**
|
|
292
|
-
|
|
293
|
-
```bash
|
|
294
|
-
sudo apt install -y nginx certbot python3-certbot-nginx
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
2. **Configure nginx.** Create `/etc/nginx/sites-available/minions`:
|
|
298
|
-
|
|
299
|
-
```nginx
|
|
300
|
-
server {
|
|
301
|
-
listen 80;
|
|
302
|
-
server_name minions.yourdomain.com;
|
|
303
|
-
|
|
304
|
-
location / {
|
|
305
|
-
proxy_pass http://localhost:7331;
|
|
306
|
-
proxy_http_version 1.1;
|
|
307
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
308
|
-
proxy_set_header Connection 'upgrade';
|
|
309
|
-
proxy_set_header Host $host;
|
|
310
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
311
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
312
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
sudo ln -s /etc/nginx/sites-available/minions /etc/nginx/sites-enabled/
|
|
319
|
-
sudo nginx -t && sudo systemctl reload nginx
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
3. **Obtain a TLS certificate:**
|
|
323
|
-
|
|
324
|
-
```bash
|
|
325
|
-
sudo certbot --nginx -d minions.yourdomain.com
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Certbot modifies the nginx config to add TLS and sets up auto-renewal.
|
|
329
|
-
|
|
330
|
-
4. **Start the Minions dashboard** (same as Caddy option above).
|
|
331
|
-
|
|
332
|
-
5. **Update the Azure Bot messaging endpoint** (same as Caddy option above).
|
|
333
|
-
|
|
334
|
-
### Verify
|
|
335
|
-
|
|
336
|
-
1. Open `https://minions.yourdomain.com/api/routes` in a browser — confirm the route list loads over HTTPS.
|
|
337
|
-
2. Check the TLS certificate: `curl -vI https://minions.yourdomain.com 2>&1 | grep "SSL certificate"`.
|
|
338
|
-
3. Test via Azure Bot **Test in Web Chat**.
|
|
339
|
-
4. Send a message in Teams — confirm the bot responds.
|
|
340
|
-
|
|
341
|
-
### Rollback
|
|
342
|
-
|
|
343
|
-
To revert to Dev Tunnel:
|
|
344
|
-
|
|
345
|
-
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
346
|
-
2. Update the Azure Bot messaging endpoint back to your tunnel URL.
|
|
347
|
-
3. Click **Apply**. Immediate switchover.
|
|
348
|
-
|
|
349
|
-
The reverse proxy can remain running — it just won't receive Bot Framework traffic until the endpoint is pointed back.
|
|
350
|
-
|
|
351
|
-
---
|
|
352
|
-
|
|
353
|
-
## Choosing an Option
|
|
354
|
-
|
|
355
|
-
| Criteria | App Service | Container App | Reverse Proxy |
|
|
356
|
-
|----------|-------------|---------------|---------------|
|
|
357
|
-
| Setup complexity | Medium | Medium | Low (Caddy) / Medium (nginx) |
|
|
358
|
-
| TLS management | Automatic | Automatic | Automatic (Caddy/certbot) |
|
|
359
|
-
| Cost | ~$13/mo (B1) | Pay-per-use | Free (your server + Let's Encrypt) |
|
|
360
|
-
| Custom domain | Supported | Supported | Required |
|
|
361
|
-
| Scaling | Supported but not needed | Supported but not needed | Manual |
|
|
362
|
-
| Best for | Azure-native teams | Container workflows | Existing servers |
|
|
363
|
-
|
|
364
|
-
> **Note on replicas:** Minions uses file-based state (`engine/*.json`). Do not run multiple replicas — use exactly 1 instance. All three options above default to single-instance deployment.
|
|
365
|
-
|
|
366
|
-
## Common Notes
|
|
367
|
-
|
|
368
|
-
- **Endpoint changes are immediate.** When you update the messaging endpoint in the Azure Bot Configuration, it takes effect right away. No bot reinstallation, no downtime, no user-visible change in Teams.
|
|
369
|
-
- **No deprecated webhooks.** This guide uses Azure Bot Framework exclusively. Do not use deprecated O365 Connector webhooks or Power Automate flows — they are being removed by Microsoft.
|
|
370
|
-
- **Config portability.** The same `config.json` works across all environments. Just ensure the `teams.appId` and `teams.appPassword` are correct for the bot registration that points to your production URL.
|