// src/commands/help.js // // Help renderer. // // /help → short, scannable list of every command (one line each) // with grouped headings. // /help → detailed usage + examples for one command. // // The previous version was a single 5KB blob of markdown with hard-to-read // inline URLs. This split keeps the top-level message short for both 1:1 and // group spaces, and pushes verbose detail behind `/help `. const SHORT_HELP = { // Work orders wohistory: 'Recent work order history for a store', wosummary: 'AI summary of a specific work order', woattachments: 'Download all attachments for a work order', // AV / phones avstatus: 'AV / device status for a store (alias: /wostatus)', phonestatus: 'DECT + IP phone status for a store', // Jira jirahistory: 'Recent Jira tickets for a store (optionally filtered by component)', jiraticket: 'Detailed AI summary of one Jira ticket', jirapoll: 'Run the hourly Jira poller on demand (enrich unassigned AV/phone tickets)', // Provisioning / mutating 'provision-dect': 'Interactive DECT provisioning card (add/remove bases & handsets)', 'provision-vc': 'Fully provision a video conferencing device', vcmonitor: 'On-demand VC packet capture (start/stop/status)', offboarduser: 'Offboard user: revoke Webex OAuth tokens + wipe MDM CORP devices', webexhost: 'Check / assign Webex Meetings host license on aeo2go.webex.com', // Bulk / utility bulkavstatuscsv: 'Generate full AV devices report (CSV)', bulkavswitchcsv: 'Generate rear-switch port report (CSV)', devicesbymodel: 'Meraki devices grouped by model (CSV)', }; const LONG_HELP = { avstatus: { title: '/avstatus', usage: ['/avstatus ', '/avstatus detailed'], examples: ['/avstatus 782', '/avstatus 782 detailed'], notes: [ 'Aliases: `/wostatus`.', 'HTTP equivalent: `?storeNum=&detailed=true`.', 'Includes Meraki deep links per device. For interactive topology, open `/av-store-dashboard.html`.', ], }, phonestatus: { title: '/phonestatus', usage: ['/phonestatus ', '/phonestatus detailed'], examples: ['/phonestatus 782', '/phonestatus 782 detailed'], notes: [ 'Shows DECT basestations + IP phones with Meraki links.', 'Detailed mode adds firmware, serial, SIP details and errors.', 'Web dashboard: `/phone-store-dashboard.html`.', ], }, 'provision-dect': { title: '/provision-dect', usage: ['/provision-dect '], examples: ['/provision-dect 782'], notes: [ 'Aliases: `/provisiondect`.', 'Posts an interactive card to add/remove bases and handsets.', 'Looks up "Store XXXX" network using 5-digit person email; access codes are auto-generated.', ], }, 'provision-vc': { title: '/provision-vc', usage: ['/provision-vc ', '/provision-vc '], examples: ['/provision-vc FOC2419NTN2', '/provision-vc FOC2419NTN2 todd'], notes: [ 'Aliases: `/vcprovision` (legacy).', '`` may be partial name (e.g. "todd", "american") or an org ID.', ], }, vcmonitor: { title: '/vcmonitor', usage: ['/vcmonitor [start|stop|status] [Full|Limited|FullRotate]'], examples: [ '/vcmonitor FOC2419NTN2', '/vcmonitor FOC2419NTN2 Limited', '/vcmonitor FOC2419NTN2 stop', '/vcmonitor FOC2419NTN2 status', ], notes: [ 'Default action: `start Full` (~3 min capture including RTP).', 'PCAPs land in the System Log bundle downloadable from Control Hub diagnostics.', ], }, wohistory: { title: '/wohistory', usage: ['/wohistory '], examples: ['/wohistory 782'], notes: ['In a store-linked space, the store can be omitted.'], }, wosummary: { title: '/wosummary', usage: ['/wosummary '], examples: ['/wosummary 12345678'], }, woattachments: { title: '/woattachments', usage: ['/woattachments '], examples: ['/woattachments 12345678'], }, jirahistory: { title: '/jirahistory', usage: [ '/jirahistory ', '/jirahistory ', '/jirahistory all', ], examples: [ '/jirahistory 782', '/jirahistory 782 phone', '/jirahistory 782 av,voice,mobility', ], notes: [ 'Component shortcuts: `av` → Audio Visual, `phone`/`voice` → Communication Services, `mobility` → Mobility.', ], }, jiraticket: { title: '/jiraticket', usage: ['/jiraticket '], examples: ['/jiraticket SUPPORT-817694'], }, jirapoll: { title: '/jirapoll', usage: ['/jirapoll', '/jirapoll prime'], examples: ['/jirapoll', '/jirapoll prime'], notes: [ 'Triggers the same Jira poller that normally runs at the top of every hour. Enriches any unlabeled matching tickets with a phone/av snapshot comment and labels them `bot-enriched`. Tickets the AI classifier decides are out-of-scope get labeled `bot-skipped` so they aren\'t re-classified every hour.', 'Idempotent — labels + JQL prevent double-processing, so running multiple times in a row is safe.', '`/jirapoll prime` labels every matching ticket without enriching or notifying. Use once after adopting the poller to skip enriching the existing backlog. Same as `JIRA_POLLER_PRIME_ON_START=true` at startup.', 'A summary of enriched tickets goes to the configured `JIRA_POLLER_ROOM_ID`. The invoking chat also gets a compact result line.', ], }, webexhost: { title: '/webexhost', usage: [ '/webexhost ', '/webexhost list', '/webexhost debug ', ], examples: [ '/webexhost jdoe@company.com', '/webexhost list', '/webexhost debug jdoe@company.com', ], notes: [ '`/webexhost ` checks whether the user holds any Webex Meetings host license on the configured site (default `aeo2go.webex.com`, override via `WEBEX_HOST_SITE_URL`).', 'If they already have one, the command reports which license. If not — and `WEBEX_HOST_LICENSE_ID` is set in `.env` — it posts a confirmation card to assign that license.', '`/webexhost list` is a discovery helper: lists every meeting license on the site with id + remaining seats, marking the one currently configured for auto-assign.', '`/webexhost debug ` dumps the raw Webex payload for the user (search-endpoint vs `GET /people/{id}` license counts, intersection with site licenses) — use this when Control Hub disagrees with what the bot says.', 'Requires service-app scopes: `spark-admin:licenses_read` + `spark-admin:people_write`. Scope/role failures surface inline.', 'There is no Webex API for the "host vs attendee" account flag itself — host status is determined entirely by holding a meeting license on the site (confirmed via Cisco docs + wxc_sdk source).', 'Audit log: requests + outcomes are emitted under the `webexhost:audit` scope with the requester identity (chat email or `via HTTP API`).', ], }, offboarduser: { title: '/offboarduser', usage: ['/offboarduser '], examples: ['/offboarduser jdoe@company.com'], notes: [ 'Posts a confirmation card showing the Webex user + MDM CORP devices that will be acted on.', 'Confirming revokes every Webex OAuth authorization for the user (signs them out of all Webex clients) and enterprise-wipes each listed MDM CORP device — both run in parallel.', 'Requires the Webex service app to hold `identity:tokens_read` + `identity:tokens_write` scopes and an admin with Full / User / Device Admin role. Scope/role failures surface inline in the success message.', '`Hide from search` is **not** part of this command — Cisco only exposes that toggle in Control Hub, not via any Webex API. Set it manually if your offboarding policy requires it.', 'Every request and outcome is logged under the `offboard:audit` scope with the requesting user (chat email or `via HTTP API`).', ], }, bulkavstatuscsv: { title: '/bulkavstatuscsv', usage: ['/bulkavstatuscsv'], notes: ['Long-running. Posts CSV attachment back to the calling room.'], }, bulkavswitchcsv: { title: '/bulkavswitchcsv', usage: ['/bulkavswitchcsv'], notes: [ 'Long-running. Walks every rear switch in Meraki for VLAN 340/145 ports.', 'Posts CSV attachment back to the calling room.', ], }, devicesbymodel: { title: '/devicesbymodel', usage: ['/devicesbymodel'], notes: ['Posts a Meraki by-model count + CSV to the calling room.'], }, }; const GROUPS = [ { title: 'Work orders', keys: ['wohistory', 'wosummary', 'woattachments'] }, { title: 'AV & phones', keys: ['avstatus', 'phonestatus'] }, { title: 'Jira', keys: ['jirahistory', 'jiraticket', 'jirapoll'] }, { title: 'Provisioning', keys: ['provision-dect', 'provision-vc', 'vcmonitor'] }, { title: 'Admin & bulk', keys: ['offboarduser', 'webexhost', 'bulkavstatuscsv', 'bulkavswitchcsv', 'devicesbymodel'] }, ]; function renderTopLevelHelp(isGroup) { const lines = ['### CollabSupport Bot Help', '']; for (const g of GROUPS) { lines.push(`**${g.title}**`); for (const key of g.keys) { const short = SHORT_HELP[key]; if (!short) continue; lines.push(`- \`/${key}\` — ${short}`); } lines.push(''); } lines.push('Type `/help ` for usage and examples (e.g. `/help avstatus`).'); if (isGroup) { lines.push('In a space linked to a store, most commands accept no arguments and pick up the store automatically.'); } lines.push('Web dashboards: `/av-store-dashboard.html`, `/phone-store-dashboard.html`.'); return lines.join('\n'); } function renderCommandHelp(name) { const detail = LONG_HELP[name]; if (!detail) return null; const lines = [`### ${detail.title}`]; if (SHORT_HELP[name]) { lines.push('', SHORT_HELP[name]); } if (detail.usage?.length) { lines.push('', '**Usage:**'); for (const u of detail.usage) lines.push(`- \`${u}\``); } if (detail.examples?.length) { lines.push('', '**Examples:**'); for (const ex of detail.examples) lines.push(`- \`${ex}\``); } if (detail.notes?.length) { lines.push('', '**Notes:**'); for (const n of detail.notes) lines.push(`- ${n}`); } return lines.join('\n'); } export async function handleHelp(bot, trigger) { const isGroup = trigger.message?.roomType === 'group'; // First arg (after `/help`) selects a specific command's detail page. // Strip a leading slash so both `/help avstatus` and `/help /avstatus` work. const args = trigger.args || []; const requested = (args[0] || trigger.query?.command || '') .toString() .trim() .toLowerCase() .replace(/^\//, ''); if (requested) { const detail = renderCommandHelp(requested); if (detail) { await bot.say('markdown', detail); return; } await bot.say( 'markdown', `Unknown command: \`${requested}\`.\n\n${renderTopLevelHelp(isGroup)}`, ); return; } await bot.say('markdown', renderTopLevelHelp(isGroup)); }