Store associates share accounts and iPads, so the existing findMyTickets (keyed by reporter email) misses tickets a coworker opened for the same store earlier in the shift. This adds a store-keyed lookup so a store caller can see everything open at their location. - New service function searchOpenTicketsByStoreNumber(): normalizes to 5 digits, validates against the stores cache (warn-only on miss), runs the same Grok-enriched search as the reporter path. - New route GET /wxccai/open-tickets-by-store?storeNumber=782 — accepts padded or unpadded input, degrade-gracefully 200 on runtime failures to match the sibling reporter route. - JQL note: the CMDB "Store Number" field only matches on the object *label* (the 5-digit padded string). Neither the objectId, ASSET-<id> objectKey, workspace-qualified id, nor raw digits match. Probed all variants before landing. - WxCC AI Agent docs updated: findMyStoreTickets promoted to the primary lookup for store callers; findMyTickets reframed as the corporate-caller path. System prompt Step 1 rewritten to route store vs corporate callers to the right tool. Verified end-to-end against store 782 — returns 5 open tickets including two from other reporters (SS-20380 wireless phone, SS-11943 Zipline training) that email-based lookup misses. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| bin | ||
| docs | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| discover-ss-fields.js | ||
| discover-ss-request-types.js | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| ss-fields-266.json | ||
| ss-fields-267.json | ||
| ss-fields-268.json | ||
| ss-fields-269.json | ||
| ss-fields-270.json | ||
| ss-fields-271.json | ||
| ss-fields-272.json | ||
| ss-fields-273.json | ||
| ss-fields-274.json | ||
| ss-fields-275.json | ||
| ss-fields-426.json | ||
| ss-fields-493.json | ||
| ss-request-types-clean.json | ||
| ss-request-types.json | ||
wxcc-ai
Node/Express service that wires Webex Contact Center summaries, Jira tickets (core + JSM Store Support), and xAI Grok summarization into a single set of internal HTTP endpoints.
Setup
npm install
cp .env.example .env
# fill in JIRA_*, XAI_*, and optionally the Assets values
npm run dev
The server listens on PORT (default 1866).
Environment variables
See .env.example for the full list. The important ones:
| Var | Purpose |
|---|---|
JIRA_CLOUD_ID |
If set, requests use https://api.atlassian.com/ex/jira/{cloudId}. |
JIRA_BASE_URL |
Fallback for legacy site URLs (https://your-site.atlassian.net). |
JIRA_EMAIL, JIRA_API_TOKEN |
Basic auth to Jira. JIRA_AUTH_TYPE=bearer switches to bearer. |
JIRA_SERVICE_DESK_ID |
Numeric JSM service desk id for Store Support. |
JIRA_ASSETS_WORKSPACE_ID |
Assets workspace id (auto-discovered if omitted). |
JIRA_ASSETS_STORE_SCHEMA_ID, _OBJECT_TYPE_ID |
Locate the Store schema/type. |
JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE[_ID] |
Attribute name (or id) holding the store number in Assets. |
JIRA_STORE_CUSTOM_FIELD_ID |
Custom field on the JSM request that holds the Store Assets reference. |
XAI_API_KEY, XAI_BASE_URL |
Grok credentials for summary generation. |
Endpoints
Base path: /api/wxccai.
Read
GET /getticket?jiraKey=CS-1234— Grok-summarized single ticket.GET /open-tickets-by-reporter?email=user@example.com— Grok-summarized list of open tickets a person reported. Best for corporate callers (unique per-user emails).GET /open-tickets-by-store?storeNumber=00782— Grok-summarized list of open SS tickets filed for a given store, regardless of reporter. Best for store callers (shared accounts). Store number can be unpadded; service pads to 5 digits.GET /ticket/:key/status— raw status fields (no Grok).GET /ticket/:key/transitions— available workflow transitions.
Write
PATCH /ticket/:key— body:{ summary?, description?, priority?, labels?, assigneeAccountId?, additional? }POST /ticket/:key/comment— body:{ text, internal? }POST /ticket/:key/close— body:{ transitionName?, resolution?, comment?, internal?, component?, businessService?, system?, cause?, subType?, preserveExistingClassification?, skipValidatorFields? }. Auto-picks the first "done" transition whentransitionNameis omitted. When the chosen transition is done-category, the four SS workflow-validator fields (components,Business Service,System,Cause) are populated first (see "Closing SS tickets" below). Non-done transitions skip the resolution field (fixes issue #9).POST /ticket/:key/confirmFixed— CC-agent convenience. Body:{ subType?, comment?, component?, businessService?, system?, cause?, internal? }. Closes withresolution=Done.POST /ticket/:key/customerCancelled— CC-agent convenience. Body:{ subType?, reason?, component?, businessService?, system?, cause?, internal? }. Closes withresolution=Won't Do.POST /ticket/:key/duplicate— CC-agent convenience. Body:{ primaryKey (required), subType?, comment?, component?, businessService?, system?, cause?, internal? }. Creates a formalDuplicateissue link toprimaryKey, then closes withresolution=Duplicate.
Store Support (JSM requests)
GET /ssRequestTypes— supportedsubTypevalues + Assets config summary.POST /createSSRequest— body includessubType,summary,storeNumber(auto-resolved via Assets),onBehalfOf,description,additional.
Webex webhook
POST /issueTranscript/:jiraKey— attaches audio + JSON transcript + human-readable transcript, then posts a restricted-visibility summary comment.
Admin
GET /admin/caches— snapshot of all four Assets object caches (stores, businessServices, systems, causes):{ caches: [{ name, count, lastSyncAt, ageSeconds, syncing, assetsSyncConfigured, ... }] }. Safe for health checks.GET /admin/caches/:name/status— same shape, one cache.POST /admin/caches/:name/refresh— force an immediate resync of one cache via the personal PAT (see below). Takes a few seconds.POST /admin/caches/refreshAll— refresh every cache in parallel.GET /admin/storesCache/statusandPOST /admin/storesCache/refresh— backward-compat aliases for the stores cache endpoints above.
Debug (non-production only)
GET /debug/assetsProbe?storeNumber=305— runs several AQL variants against Jira Assets and returns visible schemas + object-type detail + a computed diagnosis. Returns 404 whenNODE_ENV=production.
Assets object caches (Assets workaround)
Several parts of the SS lifecycle need to translate a human-readable name (a store number, a Business Service name, a System name, a Cause code) into a Jira Assets object id before the value can be written to a CMDB custom field. The shared service account is silently filtered out of the underlying object schema (68), so the app maintains four local caches that are populated from a personal Atlassian PAT (a real human account with the right Assets role):
| Cache | Object type | Used for |
|---|---|---|
stores |
109 | customfield_10261 Store Number on new SS tickets |
businessServices |
100 | customfield_10224 Business Service workflow validator on close |
systems |
103 | customfield_10225 System workflow validator on close |
causes |
107 | customfield_10233 Cause workflow validator on close |
All four are backed by the same shared factory (services/jira/assetsObjectCache.js) and use the same PAT credentials. The service account is still used for everything else (creating tickets, comments, attachments, transitions).
Both storage patterns end up in the same place — process.env.ASSETS_SYNC_TOKEN — so the runtime code path is identical. Pick whichever fits the host.
Setup A — production / Linux host (.env)
Put the values directly in .env (which is gitignored) and lock the file down:
cat >> .env <<'EOF'
ASSETS_SYNC_EMAIL=you@ae.com
ASSETS_SYNC_TOKEN=<paste-your-atlassian-api-token>
EOF
chmod 600 .env # only the bot user can read it
Then start normally:
npm start
Rotate the token in Atlassian → Account → Security → API tokens whenever the trust boundary on the host changes (new operator, offboarding, suspected leak). The app reloads it on the next process start.
Setup B — local dev on macOS (Keychain)
If you're running the app on a Mac and would rather not keep the PAT in .env, use the wrapper script — it pulls the token from Keychain into ASSETS_SYNC_TOKEN before exec'ing the process:
security add-generic-password \
-s jira-assets-sync \
-a you@ae.com \
-w '<paste-your-atlassian-api-token>' \
-U
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env # email in .env, token stays in Keychain
./bin/load-assets-sync-secret.sh npm start
If ASSETS_SYNC_TOKEN is already in the process env (Setup A above), the wrapper is a no-op and skips the Keychain lookup.
Runtime behavior
On boot the app loads each on-disk cache from $CACHES_DIR/{name}.json (default ./data/), kicks off a background refresh for any snapshot missing or older than CACHES_STALE_AFTER_HOURS, and schedules a periodic full resync every CACHES_REFRESH_HOURS. resolveStoreAssetReference serves lookups from memory (sub-ms) with a live PAT lookup as fallback for brand-new stores. The three close-time caches (business services, systems, causes) are much smaller (dozens to a few hundred entries) and rarely change.
Force a refresh at any time:
curl -X POST http://localhost:1866/api/wxccai/admin/caches/stores/refresh
curl -X POST http://localhost:1866/api/wxccai/admin/caches/refreshAll
Closing SS tickets
The Resolved transition on SS tickets fires a workflow validator that requires four fields to be populated:
components— Jira native (63 options in the SS project)customfield_10224Business Service — Assets CMDBcustomfield_10225System — Assets CMDBcustomfield_10233Cause — Assets CMDB (the value "Unknown" exists as a designed catch-all)
closeTicket fills these in before calling the Resolved transition. Resolution order per field:
- Explicit value in the request body (
component,businessService,system,cause) - Whatever is already on the ticket (if
preserveExistingClassificationistrue, the default — respects human triage) - The per-subType default from
src/config/ssCloseDefaults.js - The
__default__entry (Help Desk / Store Technology / I can't find my option - Misc / Unknown)
For CC-agent-driven closes, the shortest path is to send just subType and comment; everything else is defaulted. Use the convenience routes:
# Caller confirms the issue is resolved
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20948/confirmFixed \
-H 'Content-Type: application/json' \
-d '{"subType":"Report a Technology issue"}'
# Caller wants to cancel
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20949/customerCancelled \
-H 'Content-Type: application/json' \
-d '{"subType":"Broken device / hardware","reason":"changed their mind"}'
# Duplicate of an earlier ticket (creates a formal Duplicate issueLink)
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20950/duplicate \
-H 'Content-Type: application/json' \
-d '{"primaryKey":"SS-20948"}'
To override the auto-detected classification (for a subType not in the defaults map, or when the caller volunteers specific context), pass any of the four fields explicitly. CMDB names are matched case-insensitively; you can also pass a raw Assets objectId as a shortcut for businessService / system / cause:
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-XXXXX/confirmFixed \
-H 'Content-Type: application/json' \
-d '{"subType":"Broken device / hardware","system":"Printer","cause":"Broken Equipment"}'
Jira Assets gotcha
createSSRequest resolves a store number to an Assets object reference (customfield_10261). Two independent permission layers must both grant access, or every AQL query silently returns total: 0:
- OAuth scopes on the API token:
read:cmdb-schema:jira,read:cmdb-type:jira,read:cmdb-object:jira,read:cmdb-attribute:jira(and thewrite:equivalents for updates). - Object Schema role membership in Jira Assets itself — the underlying user needs to be added to a role on the Store schema in Jira → Assets → Object schemas → Configure → Roles.
If AQL keeps returning total: 0 with HTTP 200, run the probe endpoint above; the diagnosis field will tell you exactly which layer is missing.
Docker
npm run docker:build
npm run docker:run
The image runs as a non-root user and exposes 1866. HEALTHCHECK pings GET /health.
Repo hygiene
- Secrets live only in
.env, which is.gitignored. - The two
discover-ss-*.jshelper scripts read from.env— never hardcode credentials in them. - The debug logger no longer echoes the outbound
Authorizationheader. Rotate any token that appears in olderlogs/*.logfiles.