@remits/remits-cli 0.1.106 → 0.1.107

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 (2) hide show
  1. package/index.js +330 -10
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -29,6 +29,7 @@ const ACCOUNT_REPO_INDEX_FILE = path.join(SESSION_DIR, 'account-repos.json');
29
29
  const SERVICE_STATE_FILE = path.join(SESSION_DIR, 'service-state.json');
30
30
  const ISSUES_DIR = path.join(SESSION_DIR, 'issues');
31
31
  const WEBSOCKET_STATE_FILE = path.join(SESSION_DIR, 'websocket-state.json');
32
+ const LISTENER_REGISTRATION_STATE_FILE = path.join(SESSION_DIR, 'listener-registration-state.json');
32
33
  const AUTO_UPDATE_LOCK_DIR = path.join(SESSION_DIR, 'auto-update.lock');
33
34
  const AUTO_UPDATE_DISABLED_VALUES = new Set(['0', 'false', 'no', 'off']);
34
35
  const REMITS_CLI_PACKAGE_NAME = '@remits/remits-cli';
@@ -45,6 +46,10 @@ const WEBSOCKET_RECONNECT_DELAY_MS = Number(process.env.REMITS_CLI_WEBSOCKET_REC
45
46
  const WEBSOCKET_HEARTBEAT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_HEARTBEAT_MS || 10000);
46
47
  const WEBSOCKET_CONNECTION_TIMEOUT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_CONNECTION_TIMEOUT_MS || 15000);
47
48
  const WEBSOCKET_WATCHDOG_GRACE_MS = Number(process.env.REMITS_CLI_WEBSOCKET_WATCHDOG_GRACE_MS || 30000);
49
+ const SUPPORT_TICKET_DASHBOARD_REFRESH_MS = Number(process.env.REMITS_CLI_SUPPORT_TICKET_REFRESH_MS || 30000);
50
+ const SUPPORT_TICKET_DASHBOARD_LIMIT = Number(process.env.REMITS_CLI_SUPPORT_TICKET_LIMIT || 20);
51
+ const LISTENER_REGISTRATION_INTERVAL_MS = Number(process.env.REMITS_CLI_REGISTRATION_INTERVAL_MS || 5 * 60 * 1000);
52
+ const LISTENER_REGISTRATION_MIN_INTERVAL_MS = Number(process.env.REMITS_CLI_REGISTRATION_MIN_INTERVAL_MS || 5 * 60 * 1000);
48
53
  const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
49
54
  '.git',
50
55
  '.gradle',
@@ -1273,6 +1278,156 @@ function discoverAccountRepos() {
1273
1278
  };
1274
1279
  }
1275
1280
 
1281
+ function listenerRegistrationRepoAccounts() {
1282
+ return listAccountRepoEntries(readAccountRepoIndex())
1283
+ .sort((a, b) => Number(a.accountId) - Number(b.accountId))
1284
+ .map((entry) => ({
1285
+ accountId: Number(entry.accountId),
1286
+ name: entry.name || null,
1287
+ type: entry.type || null,
1288
+ platformAccountId: entry.platformAccountId || null,
1289
+ routingScope: ['PLATFORM', 'PRODUCT'].includes(String(entry.type || '').toUpperCase())
1290
+ ? 'self_and_descendants'
1291
+ : 'self',
1292
+ source: entry.source || null,
1293
+ updatedAt: entry.updatedAt || null
1294
+ }));
1295
+ }
1296
+
1297
+ function readListenerRegistrationState() {
1298
+ if (!fs.existsSync(LISTENER_REGISTRATION_STATE_FILE)) {
1299
+ return {};
1300
+ }
1301
+ try {
1302
+ const data = JSON.parse(fs.readFileSync(LISTENER_REGISTRATION_STATE_FILE, 'utf8'));
1303
+ return data && typeof data === 'object' ? data : {};
1304
+ } catch (_) {
1305
+ return {};
1306
+ }
1307
+ }
1308
+
1309
+ function writeListenerRegistrationState(data) {
1310
+ ensureSessionDir();
1311
+ atomicWriteFile(LISTENER_REGISTRATION_STATE_FILE, JSON.stringify(data || {}, null, 2));
1312
+ }
1313
+
1314
+ function listenerRegistrationStateKey(session) {
1315
+ const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
1316
+ const userId = session.user && session.user.id ? session.user.id : 'unknown';
1317
+ return baseUrl + '|' + String(userId);
1318
+ }
1319
+
1320
+ function serviceRegistrationState() {
1321
+ const state = readServiceState();
1322
+ return {
1323
+ running: isListenerRunning(),
1324
+ pid: state && state.pid ? state.pid : (fs.existsSync(LISTENER_PID_FILE) ? fs.readFileSync(LISTENER_PID_FILE, 'utf8').trim() : null),
1325
+ startedAt: state && state.startedAt ? state.startedAt : runtimeState.service.startedAt,
1326
+ dashboardUrl: state && state.dashboardUrl ? state.dashboardUrl : runtimeState.service.dashboardUrl
1327
+ };
1328
+ }
1329
+
1330
+ function listenerRegistrationPayload(session, options = {}) {
1331
+ const service = serviceRegistrationState();
1332
+ return {
1333
+ client: 'remits-cli',
1334
+ reason: options.reason || 'heartbeat',
1335
+ baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
1336
+ accountId: Number(session.accountId),
1337
+ dataMode: normalizeDataMode(session.dataMode),
1338
+ websocketTopic: session.websocketTopic || (session.user && session.user.uuid ? String(session.user.uuid).replace(/-/g, '') : null),
1339
+ dashboardUrl: service.dashboardUrl || null,
1340
+ agent: getPreferredAgent(),
1341
+ service,
1342
+ repoAccounts: listenerRegistrationRepoAccounts()
1343
+ };
1344
+ }
1345
+
1346
+ function listenerRegistrationSignature(payload) {
1347
+ return sha256(stableStringify({
1348
+ baseUrl: payload.baseUrl,
1349
+ accountId: payload.accountId,
1350
+ dataMode: payload.dataMode,
1351
+ websocketTopic: payload.websocketTopic,
1352
+ dashboardUrl: payload.dashboardUrl,
1353
+ agent: payload.agent,
1354
+ serviceRunning: payload.service && payload.service.running,
1355
+ repoAccounts: (payload.repoAccounts || []).map((entry) => ({
1356
+ accountId: entry.accountId,
1357
+ type: entry.type,
1358
+ routingScope: entry.routingScope,
1359
+ updatedAt: entry.updatedAt
1360
+ }))
1361
+ }));
1362
+ }
1363
+
1364
+ function shouldRegisterCliListener(session, payload, options = {}) {
1365
+ if (options.force === true) {
1366
+ return true;
1367
+ }
1368
+ const state = readListenerRegistrationState();
1369
+ const key = listenerRegistrationStateKey(session);
1370
+ const existing = state[key];
1371
+ if (!existing) {
1372
+ return true;
1373
+ }
1374
+ const lastRegisteredAtMs = Number(existing.lastRegisteredAtMs || 0);
1375
+ if (!lastRegisteredAtMs || Date.now() - lastRegisteredAtMs >= LISTENER_REGISTRATION_MIN_INTERVAL_MS) {
1376
+ return true;
1377
+ }
1378
+ return existing.signature !== listenerRegistrationSignature(payload);
1379
+ }
1380
+
1381
+ function rememberCliListenerRegistration(session, payload) {
1382
+ const state = readListenerRegistrationState();
1383
+ const key = listenerRegistrationStateKey(session);
1384
+ state[key] = {
1385
+ baseUrl: payload.baseUrl,
1386
+ userId: session.user && session.user.id ? session.user.id : null,
1387
+ signature: listenerRegistrationSignature(payload),
1388
+ accountCount: Array.isArray(payload.repoAccounts) ? payload.repoAccounts.length : 0,
1389
+ lastRegisteredAt: new Date().toISOString(),
1390
+ lastRegisteredAtMs: Date.now()
1391
+ };
1392
+ writeListenerRegistrationState(state);
1393
+ }
1394
+
1395
+ async function registerCliListenerSession(session, options = {}) {
1396
+ if (!session || !session.token) {
1397
+ return null;
1398
+ }
1399
+ const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
1400
+ const payload = listenerRegistrationPayload(session, options);
1401
+ if (!shouldRegisterCliListener(session, payload, options)) {
1402
+ return { success: true, skipped: true, reason: 'recently_registered' };
1403
+ }
1404
+ const api = buildAxios(baseUrl, session.token, parsePositiveInt(options.timeoutMs, 5000));
1405
+ const response = await api.post('/cli/register', payload);
1406
+ rememberCliListenerRegistration(session, payload);
1407
+ return response.data;
1408
+ }
1409
+
1410
+ async function bestEffortRegisterCliListeners(options = {}) {
1411
+ const sessions = listSessionsFromStore(readAllSessions()).filter((session) => session && session.token);
1412
+ if (!sessions.length) {
1413
+ return [];
1414
+ }
1415
+ const results = await Promise.all(sessions.map(async (session) => {
1416
+ try {
1417
+ const data = await registerCliListenerSession(session, options);
1418
+ return { success: true, accountId: session.accountId, baseUrl: session.baseUrl, data };
1419
+ } catch (err) {
1420
+ appendGlobalActivityLog('listener', 'registration.failed', {
1421
+ accountId: session.accountId,
1422
+ baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
1423
+ error: describeError(err)
1424
+ }, 'error');
1425
+ return { success: false, accountId: session.accountId, baseUrl: session.baseUrl, error: describeError(err) };
1426
+ }
1427
+ }));
1428
+ return results;
1429
+ }
1430
+
1276
1431
  function resolvePreferredAccountId(cwd, flags, session) {
1277
1432
  // Explicit --account-id wins over the repo's account-info.json so users can
1278
1433
  // target a child account (e.g. a customer) from a parent-account repo.
@@ -1987,6 +2142,9 @@ async function authCommand(flags) {
1987
2142
  });
1988
2143
  console.log('Authenticated as user', session.user ? session.user.id : 'unknown', 'for account', session.accountId);
1989
2144
  console.log('Data mode:', session.dataMode);
2145
+ try {
2146
+ await bestEffortRegisterCliListeners({ cwd, reason: 'auth' });
2147
+ } catch (_) { /* best-effort */ }
1990
2148
 
1991
2149
  try {
1992
2150
  const branchName = currentBranch(cwd);
@@ -4027,6 +4185,10 @@ let runtimeState = {
4027
4185
  websocket: {},
4028
4186
  lastAction: null
4029
4187
  };
4188
+ let supportTicketSnapshotCache = {
4189
+ fetchedAtMs: 0,
4190
+ value: null
4191
+ };
4030
4192
 
4031
4193
  function listenerSessionEntriesFromStore(store) {
4032
4194
  return listSessionsFromStore(store).filter((session) => session && session.token && session.websocketTopic);
@@ -4414,6 +4576,7 @@ function exportIssueBundle(snapshot) {
4414
4576
  copyFileIfExists(SESSIONS_FILE, path.join(globalDir, 'sessions.json'));
4415
4577
  copyFileIfExists(CONFIG_FILE, path.join(globalDir, 'config.json'));
4416
4578
  copyFileIfExists(SERVICE_STATE_FILE, path.join(globalDir, 'service-state.json'));
4579
+ copyFileIfExists(LISTENER_REGISTRATION_STATE_FILE, path.join(globalDir, 'listener-registration-state.json'));
4417
4580
  copyFileIfExists(ACCOUNT_REPO_INDEX_FILE, path.join(globalDir, 'account-repos.json'));
4418
4581
  copyFileIfExists(DISPATCH_STATE_FILE, path.join(globalDir, 'dispatch-panes.json'));
4419
4582
  copyFileIfExists(TMUX_ACTIVITY_LOG_FILE, path.join(globalDir, 'tmux-activity.log'));
@@ -4455,6 +4618,7 @@ function buildGlobalFileSnapshot() {
4455
4618
  { label: 'config.json', path: CONFIG_FILE, mode: 'json' },
4456
4619
  { label: 'service-state.json', path: SERVICE_STATE_FILE, mode: 'json' },
4457
4620
  { label: 'websocket-state.json', path: WEBSOCKET_STATE_FILE, mode: 'json' },
4621
+ { label: 'listener-registration-state.json', path: LISTENER_REGISTRATION_STATE_FILE, mode: 'json' },
4458
4622
  { label: 'dispatch-panes.json', path: DISPATCH_STATE_FILE, mode: 'json' },
4459
4623
  { label: 'account-repos.json', path: ACCOUNT_REPO_INDEX_FILE, mode: 'json' },
4460
4624
  { label: 'tmux-activity.log (tail)', path: TMUX_ACTIVITY_LOG_FILE, mode: 'tail' }
@@ -4511,7 +4675,140 @@ function buildRepoSnapshot(entry) {
4511
4675
  };
4512
4676
  }
4513
4677
 
4514
- function collectDashboardSnapshot() {
4678
+ function compactSupportTicketRow(row) {
4679
+ if (!row || typeof row !== 'object') {
4680
+ return null;
4681
+ }
4682
+ return {
4683
+ ticketId: row.ticketId || row.id || null,
4684
+ accountId: row.accountId || row.account_id || null,
4685
+ accountName: row.accountName || null,
4686
+ subject: row.subject || null,
4687
+ priority: row.priority || null,
4688
+ status: row.status || null,
4689
+ type: row.type || null,
4690
+ source: row.source || null,
4691
+ assignedTo: row.assignedTo || null,
4692
+ implementationAccountId: row.implementationAccountId || null,
4693
+ implementationAccountName: row.implementationAccountName || null,
4694
+ affectedComponent: row.affectedComponent || null,
4695
+ updatedAt: row.updatedAt || row.updated || row.lastActivityAt || null,
4696
+ activityAt: row.activityAt || row.lastActivityAt || null,
4697
+ activityLabel: row.activityLabel || null
4698
+ };
4699
+ }
4700
+
4701
+ function supportTicketQueueInput(session) {
4702
+ return {
4703
+ action: 'list',
4704
+ accountId: Number(session.accountId),
4705
+ includeChildren: true,
4706
+ includeRoot: true,
4707
+ sortBy: 'triage',
4708
+ limit: Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)
4709
+ };
4710
+ }
4711
+
4712
+ async function fetchSupportTicketQueueForSession(session) {
4713
+ const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
4714
+ const api = buildAxios(baseUrl, session.token, 12000);
4715
+ const response = await api.post('/cli/tool', {
4716
+ token: session.token,
4717
+ accountId: Number(session.accountId),
4718
+ branchName: '',
4719
+ dataMode: 'prod',
4720
+ name: 'mcp_support_ticket_queue',
4721
+ callId: 'dashboard-support-ticket-queue-' + crypto.randomUUID(),
4722
+ input: supportTicketQueueInput(session)
4723
+ }).then((r) => r.data);
4724
+
4725
+ const result = response && response.result ? response.result : {};
4726
+ const tickets = Array.isArray(result.tickets)
4727
+ ? result.tickets.map(compactSupportTicketRow).filter(Boolean)
4728
+ : [];
4729
+ return {
4730
+ success: response && response.success !== false && response.toolSuccess !== false && result.success !== false,
4731
+ baseUrl,
4732
+ accountId: Number(session.accountId),
4733
+ userId: session.user && session.user.id ? session.user.id : null,
4734
+ dataMode: result.dataMode || response.dataMode || 'prod',
4735
+ totalCount: result.totalCount || tickets.length,
4736
+ count: result.count || tickets.length,
4737
+ counts: result.counts || {},
4738
+ recommendedTicket: compactSupportTicketRow(result.recommendedTicket),
4739
+ tickets,
4740
+ searchedAccounts: result.searchedAccounts || [],
4741
+ message: response.toolMessage || result.message || null
4742
+ };
4743
+ }
4744
+
4745
+ async function collectSupportTicketSnapshot() {
4746
+ const nowMs = Date.now();
4747
+ if (supportTicketSnapshotCache.value &&
4748
+ nowMs - supportTicketSnapshotCache.fetchedAtMs < SUPPORT_TICKET_DASHBOARD_REFRESH_MS) {
4749
+ return supportTicketSnapshotCache.value;
4750
+ }
4751
+
4752
+ const sessions = listSessionsFromStore(readAllSessions())
4753
+ .filter((session) => session && session.token && session.accountId);
4754
+ const seen = new Set();
4755
+ const queueSessions = sessions.filter((session) => {
4756
+ const key = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL) + '|' + String(session.accountId) + '|' +
4757
+ String(session.user && session.user.id ? session.user.id : '');
4758
+ if (seen.has(key)) {
4759
+ return false;
4760
+ }
4761
+ seen.add(key);
4762
+ return true;
4763
+ }).slice(0, 12);
4764
+
4765
+ const queues = [];
4766
+ const errors = [];
4767
+ await Promise.all(queueSessions.map(async (session) => {
4768
+ try {
4769
+ queues.push(await fetchSupportTicketQueueForSession(session));
4770
+ } catch (err) {
4771
+ errors.push({
4772
+ baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
4773
+ accountId: Number(session.accountId),
4774
+ userId: session.user && session.user.id ? session.user.id : null,
4775
+ message: describeError(err)
4776
+ });
4777
+ }
4778
+ }));
4779
+
4780
+ const ticketsById = {};
4781
+ for (const queue of queues) {
4782
+ for (const ticket of queue.tickets || []) {
4783
+ const key = String(ticket.ticketId || '');
4784
+ if (!key || ticketsById[key]) {
4785
+ continue;
4786
+ }
4787
+ ticketsById[key] = {
4788
+ ...ticket,
4789
+ queueAccountId: queue.accountId,
4790
+ baseUrl: queue.baseUrl
4791
+ };
4792
+ }
4793
+ }
4794
+
4795
+ const value = {
4796
+ fetchedAt: new Date(nowMs).toISOString(),
4797
+ refreshMs: SUPPORT_TICKET_DASHBOARD_REFRESH_MS,
4798
+ queueCount: queues.length,
4799
+ totalCount: queues.reduce((sum, queue) => sum + Number(queue.totalCount || 0), 0),
4800
+ tickets: Object.values(ticketsById).slice(0, Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)),
4801
+ queues,
4802
+ errors
4803
+ };
4804
+ supportTicketSnapshotCache = {
4805
+ fetchedAtMs: nowMs,
4806
+ value
4807
+ };
4808
+ return value;
4809
+ }
4810
+
4811
+ async function collectDashboardSnapshot() {
4515
4812
  const accountRepoIndex = readAccountRepoIndex();
4516
4813
  const repoEntries = listAccountRepoEntries(accountRepoIndex)
4517
4814
  .sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
@@ -4566,6 +4863,7 @@ function collectDashboardSnapshot() {
4566
4863
  globalFiles,
4567
4864
  repos: repoEntries,
4568
4865
  platformRepo,
4866
+ supportTickets: await collectSupportTicketSnapshot(),
4569
4867
  issues,
4570
4868
  lastAction: runtimeState.lastAction
4571
4869
  };
@@ -4621,6 +4919,9 @@ function renderDashboardHtml(snapshot) {
4621
4919
  '<div id="last-action" class="empty">No dashboard actions have been triggered yet.</div>' +
4622
4920
  '</section>' +
4623
4921
  '</div>' +
4922
+ '<div class="section-grid">' +
4923
+ '<section class="panel"><h2>Support Tickets</h2><div id="support-ticket-section" class="accordion"></div></section>' +
4924
+ '</div>' +
4624
4925
  '<div class="section-grid">' +
4625
4926
  '<section class="panel"><h2>Issues & Escalation</h2><div id="issues-section" class="accordion"></div></section>' +
4626
4927
  '</div>' +
@@ -4651,9 +4952,10 @@ function renderDashboardHtml(snapshot) {
4651
4952
  'function summaryItem(key,value){return \'<div class="summary-item"><span class="k">\'+escapeHtml(key)+\'</span><span class="v">\'+escapeHtml(value)+\'</span></div>\';}' +
4652
4953
  'function codeBlock(key,content,extraClass){return \'<pre class="code-block \'+(extraClass||"")+\'" data-scroll-key="\'+escapeHtml(key)+\'">\'+escapeHtml(content||"[missing]")+\'</pre>\';}' +
4653
4954
  'function detailsBlock(key,title,meta,bodyHtml,openByDefault){return \'<details class="item" data-key="\'+escapeHtml(key)+\'"\'+(openByDefault?" open":"")+\'><summary><div class="summary-main"><div class="summary-text"><span class="summary-title">\'+escapeHtml(title)+\'</span><span class="summary-meta">\'+meta+\'</span></div></div><span class="summary-caret">›</span></summary><div class="item-body">\'+bodyHtml+\'</div></details>\';}' +
4654
- 'function renderStatusCards(state){const wsValues=Object.values((state.websocket&&state.websocket.connections)||{});const connectedCount=wsValues.filter(function(entry){return entry.status==="connected";}).length;const warnCount=wsValues.filter(function(entry){return entry.status==="connecting";}).length;const errorCount=wsValues.filter(function(entry){return entry.status==="error"||entry.status==="disconnected";}).length;const repoCount=(state.repos||[]).length;const issueCount=(state.issues||[]).length;const criticalIssueCount=(state.issues||[]).filter(function(issue){return issue.severity==="bad";}).length;const tmuxHealthy=state.tmux&&state.tmux.available&&state.tmux.sessionExists;const cards=[' +
4955
+ 'function renderStatusCards(state){const wsValues=Object.values((state.websocket&&state.websocket.connections)||{});const connectedCount=wsValues.filter(function(entry){return entry.status==="connected";}).length;const warnCount=wsValues.filter(function(entry){return entry.status==="connecting";}).length;const errorCount=wsValues.filter(function(entry){return entry.status==="error"||entry.status==="disconnected";}).length;const repoCount=(state.repos||[]).length;const issueCount=(state.issues||[]).length;const criticalIssueCount=(state.issues||[]).filter(function(issue){return issue.severity==="bad";}).length;const ticketState=state.supportTickets||{};const ticketErrors=(ticketState.errors||[]).length;const ticketTotal=ticketState.totalCount||0;const tmuxHealthy=state.tmux&&state.tmux.available&&state.tmux.sessionExists;const cards=[' +
4655
4956
  '{title:"Service",value:state.service&&state.service.running?"Running":"Stopped",label:"Daemon status",tone:statusTone(state.service&&state.service.running),sub:(state.service&&state.service.dashboardUrl?state.service.dashboardUrl:"No dashboard URL yet")},' +
4656
4957
  '{title:"Websockets",value:String(connectedCount)+"/"+String(wsValues.length||0),label:"Connected topics groups",tone:errorCount? "bad" : (warnCount? "warn" : "ok"),sub: errorCount? String(errorCount)+" unhealthy connection(s)" : (warnCount? String(warnCount)+" connecting" : "All connections healthy")},' +
4958
+ '{title:"Tickets",value:String(ticketTotal),label:"Open support queue",tone:ticketErrors?"warn":"ok",sub:ticketErrors?String(ticketErrors)+" queue fetch error(s)":"Fetched "+String(ticketState.queueCount||0)+" queue(s)"},' +
4657
4959
  '{title:"Tmux",value:tmuxHealthy?"Ready":"Attention",label:"Dispatch workspace",tone:tmuxHealthy?"ok":(state.tmux&&state.tmux.available?"warn":"bad"),sub:state.tmux&&state.tmux.available?(state.tmux.sessionExists?String((state.tmux.livePanes||[]).length)+" live pane(s)":"tmux installed, session missing"):"tmux not installed"},' +
4658
4960
  '{title:"Issues",value:String(issueCount),label:"Detected escalation signals",tone:criticalIssueCount?"bad":(issueCount?"warn":"ok"),sub:issueCount?(criticalIssueCount?String(criticalIssueCount)+" critical signal(s)":"Review before continuing"):"No issue signals detected"},' +
4659
4961
  '{title:"Repos",value:String(repoCount),label:"Indexed account repos",tone:repoCount?"ok":"warn",sub:"Last scan: "+formatTime(state.discovery&&state.discovery.scannedAt)}' +
@@ -4664,15 +4966,18 @@ function renderDashboardHtml(snapshot) {
4664
4966
  'summaryItem("Repo Matches",String(state.discovery&&state.discovery.matches||0)),' +
4665
4967
  'summaryItem("Connections",String(ws.length)),' +
4666
4968
  'summaryItem("Topics",String(topics)),' +
4969
+ 'summaryItem("Open Tickets",String(state.supportTickets&&state.supportTickets.totalCount||0)),' +
4667
4970
  'summaryItem("Tracked Panes",String(tracked))' +
4668
4971
  '].join("");}' +
4669
4972
  'function renderLastAction(state){const target=document.getElementById("last-action");if(!state.lastAction){target.className="empty";target.textContent="No dashboard actions have been triggered yet.";return;}target.className="summary-item";target.innerHTML=\'<span class="k">Last Action</span><span class="v">\'+escapeHtml(state.lastAction.action||"unknown")+\'</span><div class="muted" style="margin-top:8px;">\'+escapeHtml(formatTime(state.lastAction.at))+\'</div>\'+(state.lastAction.bundlePath?\'<div class="muted" style="margin-top:8px;word-break:break-word;"><strong>Bundle:</strong> \'+escapeHtml(state.lastAction.bundlePath)+\'</div>\':"");}' +
4670
4973
  'function renderIssues(state){const target=document.getElementById("issues-section");const issues=state.issues||[];if(!issues.length){target.innerHTML=\'<div class="empty">No automatic issue signals are currently detected. If something still feels wrong, you can still click "Export Issue Bundle" to prepare a package for a Remits system admin.</div>\';return;}target.innerHTML=issues.map(function(issue,index){const tone=issue.severity==="bad"?"bad":"warn";const meta=badge(issue.severity==="bad"?"critical":"warning",tone)+\' <span class="muted">\'+escapeHtml(issue.scope+(issue.repoAccountId?(" account "+issue.repoAccountId):""))+\'</span>\';const evidence=(issue.evidence||[]).map(function(line){return "<li>"+escapeHtml(line)+"</li>";}).join("");const body=\'<div class="stack"><div class="muted">\'+escapeHtml(issue.summary||"")+\'</div><div class="mini-card"><strong>Recommendation</strong>\'+escapeHtml(issue.recommendation||"") + \'</div><div><strong style="display:block;margin-bottom:8px;">Evidence</strong><ul style="margin:0;padding-left:18px;display:grid;gap:8px;">\'+(evidence||"<li>No evidence lines captured.</li>")+\'</ul></div></div>\';return detailsBlock("issue:"+issue.id,(index+1)+". "+issue.title,meta,body,index===0);}).join("");}' +
4974
+ 'function renderSupportTickets(state){const target=document.getElementById("support-ticket-section");const support=state.supportTickets||{};const tickets=support.tickets||[];const queues=support.queues||[];const errors=support.errors||[];let html=\'<div class="mini-grid"><div class="mini-card"><strong>Total Open</strong>\'+escapeHtml(String(support.totalCount||0))+\'</div><div class="mini-card"><strong>Queues</strong>\'+escapeHtml(String(support.queueCount||0))+\'</div><div class="mini-card"><strong>Fetched</strong>\'+escapeHtml(formatTime(support.fetchedAt))+\'</div></div>\';if(errors.length){html+=detailsBlock("support:errors","Queue fetch errors",badge(String(errors.length)+" error(s)","warn"),codeBlock("support-errors",JSON.stringify(errors,null,2),"log"),false);}if(!queues.length&&!errors.length){target.innerHTML=html+\'<div class="empty">No authenticated support-ticket queue is available yet.</div>\';return;}const recs=queues.map(function(queue){return queue.recommendedTicket?Object.assign({queueAccountId:queue.accountId,baseUrl:queue.baseUrl},queue.recommendedTicket):null;}).filter(Boolean);if(recs.length){const recBody=recs.map(function(ticket,index){return supportTicketDetails(ticket,"support-rec:"+index);}).join("");html+=detailsBlock("support:recommended","Recommended next tickets",badge(String(recs.length)+" queue(s)","ok"),recBody,true);}if(tickets.length){html+=tickets.map(function(ticket,index){const meta=badge(ticket.priority||"priority","warn")+\' \'+badge(ticket.status||"status",ticket.status==="open"?"ok":"warn")+\' <span class="muted">\'+escapeHtml(ticket.accountName||ticket.accountId||"unknown account")+\'</span>\';return detailsBlock("support-ticket:"+ticket.ticketId,String(ticket.ticketId||"ticket")+" "+(ticket.subject||""),meta,supportTicketDetails(ticket,"support-ticket:"+index),index===0&&!recs.length);}).join("");}else{html+=\'<div class="empty">No open support tickets were returned by the current queues.</div>\';}target.innerHTML=html;}' +
4975
+ 'function supportTicketDetails(ticket,key){const command="remits-cli tool --name mcp_support_ticket --input "+JSON.stringify(JSON.stringify({action:"read",ticketId:String(ticket.ticketId||"")}))+" --data-mode prod";return \'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Ticket</strong>\'+escapeHtml(ticket.ticketId||"n/a")+\'</div><div class="mini-card"><strong>Account</strong>\'+escapeHtml((ticket.accountName||"n/a")+" ("+(ticket.accountId||"n/a")+")")+\'</div><div class="mini-card"><strong>Type</strong>\'+escapeHtml(ticket.type||"n/a")+\'</div><div class="mini-card"><strong>Assigned</strong>\'+escapeHtml(ticket.assignedTo||"unassigned")+\'</div><div class="mini-card"><strong>Implementation</strong>\'+escapeHtml((ticket.implementationAccountName||"n/a")+" ("+(ticket.implementationAccountId||"n/a")+")")+\'</div><div class="mini-card"><strong>Activity</strong>\'+escapeHtml(ticket.activityLabel||formatTime(ticket.activityAt||ticket.updatedAt))+\'</div></div><div class="muted">\'+escapeHtml(ticket.affectedComponent||"No affected component recorded.")+\'</div>\'+codeBlock(key+":cmd",command)+\'</div>\';}' +
4671
4976
  'function renderWebsockets(state){const entries=Object.entries((state.websocket&&state.websocket.connections)||{});const target=document.getElementById("websocket-section");if(!entries.length){target.innerHTML=\'<div class="empty">No websocket connections are registered yet.</div>\';return;}target.innerHTML=entries.map(function(pair){const baseUrl=pair[0];const info=pair[1]||{};const topicEntries=Object.entries(info.topics||{});const recentMessages=Array.isArray(info.recentMessages)?info.recentMessages:[];const tone=info.status==="connected"?"ok":(info.status==="connecting"?"warn":"bad");const summaryMeta=badge(info.status||"unknown",tone)+\' <span class="muted">\' + escapeHtml(String(topicEntries.length)) + \' topic(s)</span>\' + \' <span class="muted">\' + escapeHtml(String(recentMessages.length)) + \' recent message(s)</span>\';const topicBody=topicEntries.length?topicEntries.map(function(topicPair){const topic=topicPair[0];const topicInfo=topicPair[1]||{};const mini=\'<div class="mini-grid"><div class="mini-card"><strong>Accounts</strong>\'+escapeHtml((topicInfo.accountIds||[]).join(", ")||"n/a")+\'</div><div class="mini-card"><strong>Subscribed</strong>\'+escapeHtml(formatTime(topicInfo.subscribedAt))+\'</div><div class="mini-card"><strong>Last Message</strong>\'+escapeHtml(formatTime(topicInfo.lastMessageAt))+\'</div></div>\';return detailsBlock("ws-topic:"+baseUrl+":"+topic,topic,"topic subscription",mini,false);}).join(""):\'<div class="empty">No topic subscriptions recorded.</div>\';const messageBody=recentMessages.length?recentMessages.map(function(message,index){const messageMeta=badge(message.duplicate?"duplicate":"handled",message.duplicate?"warn":"ok")+\' <span class="muted">\'+escapeHtml(message.source||"live")+\'</span>\' + \' <span class="muted">\'+escapeHtml(message.type||"unknown-type")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Message ID</strong>\'+escapeHtml(message.messageId||"n/a")+\'</div><div class="mini-card"><strong>Timestamp</strong>\'+escapeHtml(message.timestamp?String(message.timestamp):"n/a")+\'</div><div class="mini-card"><strong>Received</strong>\'+escapeHtml(formatTime(message.receivedAt))+\'</div><div class="mini-card"><strong>Accounts</strong>\'+escapeHtml((message.accountIds||[]).join(", ")||"n/a")+\'</div></div>\'+codeBlock("ws-msg:"+baseUrl+":"+(message.messageId||index),JSON.stringify(message.payload||{},null,2)) + \'</div>\';return detailsBlock("ws-message:"+baseUrl+":"+(message.messageId||index),(message.type||"message")+" "+(message.messageId||""),messageMeta,body,false);}).join(""):\'<div class="empty">No recent websocket messages have been recorded yet.</div>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Status</strong>\'+escapeHtml(info.status||"unknown")+\'</div><div class="mini-card"><strong>Connected</strong>\'+escapeHtml(formatTime(info.connectedAt))+\'</div><div class="mini-card"><strong>Last Message</strong>\'+escapeHtml(formatTime(info.lastMessageAt))+\'</div><div class="mini-card"><strong>Last Error</strong>\'+escapeHtml(info.lastError||"none")+\'</div></div><div class="stack">\'+detailsBlock("ws-recent:"+baseUrl,"Recent messages","latest live and missed-message traffic",messageBody,false)+topicBody+\'</div></div>\';return detailsBlock("ws:"+baseUrl,baseUrl,summaryMeta,body,entries.length===1);}).join("");}' +
4672
4977
  'function renderTmux(state){const target=document.getElementById("tmux-section");const panes=state.tmux&&state.tmux.livePanes||[];const trackedText=state.tmux&&state.tmux.trackedPanes?state.tmux.trackedPanes:"{}";const launchSupported=state.tmux&&state.tmux.terminalLaunchSupported;const attachCommand="tmux attach -t "+(state.tmux&&state.tmux.sessionName||"remits-listener");const health=\'<div class="mini-grid"><div class="mini-card"><strong>tmux Installed</strong>\'+escapeHtml(String(state.tmux&&state.tmux.available))+\'</div><div class="mini-card"><strong>Session Exists</strong>\'+escapeHtml(String(state.tmux&&state.tmux.sessionExists))+\'</div><div class="mini-card"><strong>Session Name</strong>\'+escapeHtml(state.tmux&&state.tmux.sessionName||"n/a")+\'</div><div class="mini-card"><strong>Live Panes</strong>\'+escapeHtml(String(panes.length))+\'</div><div class="mini-card"><strong>Open from Dashboard</strong>\'+escapeHtml(launchSupported?"supported":"not supported")+\'</div></div><div class="muted" style="margin-bottom:14px;">\'+escapeHtml(launchSupported?"Use the Open Tmux Terminal button to bring Terminal forward and attach automatically.":"Dashboard-to-terminal launch is not supported on this platform. Run the attach command manually.")+\'</div>\';const quickAttach=detailsBlock("tmux:attach","Quick attach command","manual fallback",codeBlock("tmux-attach-cmd",attachCommand),false);const paneSection=detailsBlock("tmux:panes","Live panes",escapeHtml(String(panes.length))+" pane(s)",codeBlock("tmux-live-panes",JSON.stringify(panes,null,2),"log"),false);const trackedSection=detailsBlock("tmux:tracked","Tracked pane routing","ticket/task to pane mapping",codeBlock("tmux-tracked",trackedText),false);const logFile=(state.globalFiles||[]).find(function(file){return file.label==="tmux-activity.log (tail)";});const logSection=detailsBlock("tmux:activity","Activity tail","stream of tmux and websocket events",codeBlock("tmux-log",logFile&&logFile.content||"[missing]","log"),true);target.innerHTML=health+quickAttach+paneSection+trackedSection+logSection;const openButton=document.querySelector(\'[data-action="open-tmux"]\');if(openButton){openButton.disabled=!launchSupported||!(state.tmux&&state.tmux.available);}}' +
4673
4978
  'function renderGlobalFiles(state){const target=document.getElementById("global-files");const files=(state.globalFiles||[]).filter(function(file){return file.label!=="tmux-activity.log (tail)";});if(!files.length){target.innerHTML=\'<div class="empty">No global files found.</div>\';return;}target.innerHTML=files.map(function(file){const tone=file.exists?"ok":"warn";const meta=badge(file.exists?"present":"missing",tone)+\' <span class="muted">\'+escapeHtml(file.updatedAt?formatTime(file.updatedAt):"not found")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Path</strong>\'+escapeHtml(file.path||"")+\'</div><div class="mini-card"><strong>Size</strong>\'+escapeHtml(String(file.size||0))+\' bytes</div><div class="mini-card"><strong>Updated</strong>\'+escapeHtml(formatTime(file.updatedAt))+\'</div></div>\'+codeBlock("global:"+file.label,file.content)+(file.label==="service-state.json"?\'<div class="muted">Use this file to discover the dashboard URL and runtime websocket summary.</div>\':"")+\'</div>\';return detailsBlock("global:"+file.label,file.label,meta,body,false);}).join("");}' +
4674
4979
  'function renderRepos(state){const target=document.getElementById("repo-section");const repos=state.repos||[];if(!repos.length){target.innerHTML=\'<div class="empty">No indexed account repos were found. Use "Rescan Account Repos" to rebuild the index.</div>\';return;}target.innerHTML=repos.map(function(repo){const tone=repo.exists?"ok":"warn";const meta=badge(repo.exists?"available":"missing",tone)+\' <span class="muted">type: \'+escapeHtml(repo.type||"n/a")+\'</span>\';const files=(repo.files||[]).map(function(file){const fileMeta=badge(file.exists?"present":"missing",file.exists?"ok":"warn")+\' <span class="muted">\'+escapeHtml(file.updatedAt?formatTime(file.updatedAt):"not found")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Path</strong>\'+escapeHtml(file.path||"")+\'</div><div class="mini-card"><strong>Size</strong>\'+escapeHtml(String(file.size||0))+\' bytes</div><div class="mini-card"><strong>Updated</strong>\'+escapeHtml(formatTime(file.updatedAt))+\'</div></div>\'+codeBlock("repo:"+repo.accountId+":"+file.label,file.content,file.label.indexOf("session log")>=0?"log":"")+\'</div>\';return detailsBlock("repo-file:"+repo.accountId+":"+file.label,file.label,fileMeta,body,false);}).join("");const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Directory</strong>\'+escapeHtml(repo.directory||"")+\'</div><div class="mini-card"><strong>Source</strong>\'+escapeHtml(repo.source||"n/a")+\'</div><div class="mini-card"><strong>Platform Account</strong>\'+escapeHtml(String(repo.platformAccountId||"n/a"))+\'</div></div><div class="repo-files">\'+files+\'</div></div>\';return detailsBlock("repo:"+repo.accountId,(repo.name||"Unknown")+" ("+repo.accountId+")",meta,body,false);}).join("");}' +
4675
- 'function renderAll(state){const detailState=saveDetailsState();const scrollState=saveScrollState();dashboardState=state;document.getElementById("generated-at").textContent=formatTime(state.generatedAt);renderStatusCards(state);renderSummary(state);renderLastAction(state);renderIssues(state);renderWebsockets(state);renderTmux(state);renderGlobalFiles(state);renderRepos(state);restoreDetailsState(detailState);restoreScrollState(scrollState);}' +
4980
+ 'function renderAll(state){const detailState=saveDetailsState();const scrollState=saveScrollState();dashboardState=state;document.getElementById("generated-at").textContent=formatTime(state.generatedAt);renderStatusCards(state);renderSummary(state);renderLastAction(state);renderSupportTickets(state);renderIssues(state);renderWebsockets(state);renderTmux(state);renderGlobalFiles(state);renderRepos(state);restoreDetailsState(detailState);restoreScrollState(scrollState);}' +
4676
4981
  'async function fetchState(){const res=await fetch("/api/state",{cache:"no-store"});if(!res.ok)throw new Error("State refresh failed with "+res.status);return res.json();}' +
4677
4982
  'function showToast(message){const toast=document.getElementById("toast");toast.textContent=message;toast.classList.add("show");clearTimeout(showToast._timer);showToast._timer=setTimeout(function(){toast.classList.remove("show");},2400);}' +
4678
4983
  'async function triggerAction(action,button){if(actionInFlight)return;actionInFlight=true;button.disabled=true;try{const res=await fetch("/action",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","Accept":"application/json"},body:"action="+encodeURIComponent(action)});if(!res.ok)throw new Error("Action failed with "+res.status);const payload=await res.json();renderAll(payload.state);if(payload.result&&payload.result.bundlePath){showToast("Issue bundle prepared at "+payload.result.bundlePath);}else{showToast(action.replace(/-/g," ")+" complete");}}catch(err){showToast(err.message||String(err));}finally{actionInFlight=false;button.disabled=false;}}' +
@@ -4731,9 +5036,10 @@ function startDashboardServer(preferredPort) {
4731
5036
  const requestUrl = new URL(req.url, 'http://' + DASHBOARD_HOST + ':' + (runtimeState.service.dashboardPort || preferredPort));
4732
5037
  if (req.method === 'GET' && requestUrl.pathname === '/api/state') {
4733
5038
  try {
5039
+ const snapshot = await collectDashboardSnapshot();
4734
5040
  res.statusCode = 200;
4735
5041
  res.setHeader('Content-Type', 'application/json');
4736
- res.end(JSON.stringify(collectDashboardSnapshot(), null, 2));
5042
+ res.end(JSON.stringify(snapshot, null, 2));
4737
5043
  } catch (err) {
4738
5044
  sendDashboardError(res, err, true);
4739
5045
  }
@@ -4756,11 +5062,13 @@ function startDashboardServer(preferredPort) {
4756
5062
  at: new Date().toISOString()
4757
5063
  };
4758
5064
  if (action === 'export-issues') {
4759
- const bundlePath = exportIssueBundle(collectDashboardSnapshot());
5065
+ const bundlePath = exportIssueBundle(await collectDashboardSnapshot());
4760
5066
  actionResult = { bundlePath };
4761
5067
  runtimeState.lastAction.bundlePath = bundlePath;
4762
5068
  } else if (action === 'rescan-repos') {
4763
5069
  recordDiscoverySummary(discoverAccountRepos());
5070
+ supportTicketSnapshotCache = { fetchedAtMs: 0, value: null };
5071
+ await bestEffortRegisterCliListeners({ reason: 'dashboard.rescan' });
4764
5072
  } else if (action === 'open-tmux') {
4765
5073
  openTmuxSessionForUser();
4766
5074
  } else if (action === 'start-tmux') {
@@ -4777,7 +5085,7 @@ function startDashboardServer(preferredPort) {
4777
5085
  success: true,
4778
5086
  action,
4779
5087
  result: actionResult,
4780
- state: collectDashboardSnapshot()
5088
+ state: await collectDashboardSnapshot()
4781
5089
  }));
4782
5090
  return;
4783
5091
  }
@@ -4793,9 +5101,10 @@ function startDashboardServer(preferredPort) {
4793
5101
 
4794
5102
  if (req.method === 'GET' && requestUrl.pathname === '/') {
4795
5103
  try {
5104
+ const snapshot = await collectDashboardSnapshot();
4796
5105
  res.statusCode = 200;
4797
5106
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
4798
- res.end(renderDashboardHtml(collectDashboardSnapshot()));
5107
+ res.end(renderDashboardHtml(snapshot));
4799
5108
  } catch (err) {
4800
5109
  sendDashboardError(res, err, false);
4801
5110
  }
@@ -5324,14 +5633,15 @@ function processInboundWebsocketMessage(client, baseUrl, topic, accountIds, user
5324
5633
  const acctId = payload.accountId || accountIds[0];
5325
5634
  console.log('[ws][TestSuite][account:' + acctId + ']', JSON.stringify(payload));
5326
5635
  }
5327
- } else if (data.type === 'remits-cli') {
5636
+ } else if (data.type === 'remits-cli' || data.type === 'cli_listener' || data.type === 'listener') {
5328
5637
  const payload = data.payload || data;
5329
5638
  const dispatchAccountId = resolveDispatchAccountId(payload, accountIds[0]);
5330
- console.log('[ws][remits-cli][account:' + dispatchAccountId + ']', JSON.stringify(payload));
5331
- appendGlobalActivityLog('tmux', 'dispatch.received', {
5639
+ console.log('[ws][listener][account:' + dispatchAccountId + ']', JSON.stringify(payload));
5640
+ appendGlobalActivityLog('tmux', 'listener_delivery.received', {
5332
5641
  baseUrl,
5333
5642
  topic,
5334
5643
  accountId: dispatchAccountId,
5644
+ messageType: data.type,
5335
5645
  source,
5336
5646
  messageId,
5337
5647
  message: summarizeDispatchMessage(payload)
@@ -5733,6 +6043,8 @@ async function listenCommand(flags) {
5733
6043
  recordDiscoverySummary(discoverAccountRepos());
5734
6044
  const dashboardPort = Number(flags.port || flags.dashboardPort || DEFAULT_DASHBOARD_PORT);
5735
6045
  await startDashboardServer(dashboardPort);
6046
+ await bestEffortRegisterCliListeners({ reason: 'listener.start' });
6047
+ let lastListenerRegistrationAt = Date.now();
5736
6048
 
5737
6049
  // Create the tmux session immediately so the user can attach right away
5738
6050
  try {
@@ -5820,6 +6132,11 @@ async function listenCommand(flags) {
5820
6132
  activeSessionSignature = currentSignature;
5821
6133
  }
5822
6134
 
6135
+ if (Date.now() - lastListenerRegistrationAt >= LISTENER_REGISTRATION_INTERVAL_MS) {
6136
+ await bestEffortRegisterCliListeners({ reason: 'listener.heartbeat' });
6137
+ lastListenerRegistrationAt = Date.now();
6138
+ }
6139
+
5823
6140
  await runManagedWebsocketWatchdog();
5824
6141
  updateRuntimeServiceState();
5825
6142
  } catch (err) {
@@ -6213,6 +6530,9 @@ async function main() {
6213
6530
 
6214
6531
  // Best-effort: update account-repo index whenever running from inside a remits repo
6215
6532
  try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
6533
+ if (!wantsJson) {
6534
+ try { await bestEffortRegisterCliListeners({ cwd: process.cwd(), reason: 'command' }); } catch (_) { /* best-effort */ }
6535
+ }
6216
6536
 
6217
6537
  // Auto-start the background service if not already running.
6218
6538
  // Skip for lifecycle subcommands and help.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.106",
3
+ "version": "0.1.107",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,