@remits/remits-cli 0.1.106 → 0.1.108

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 +378 -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,204 @@ 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
+ const groupedSessions = Array.isArray(options.groupedSessions)
1333
+ ? options.groupedSessions.filter((entry) => entry && entry.token)
1334
+ : [];
1335
+ const sessionDataModes = Array.from(new Set(
1336
+ (groupedSessions.length ? groupedSessions : [session])
1337
+ .map((entry) => normalizeDataMode(entry && entry.dataMode))
1338
+ .filter(Boolean)
1339
+ )).sort();
1340
+ const sessionAccountIds = Array.from(new Set(
1341
+ (groupedSessions.length ? groupedSessions : [session])
1342
+ .map((entry) => Number(entry && entry.accountId))
1343
+ .filter((accountId) => Number.isFinite(accountId) && accountId > 0)
1344
+ )).sort((a, b) => a - b);
1345
+ return {
1346
+ client: 'remits-cli',
1347
+ reason: options.reason || 'heartbeat',
1348
+ baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
1349
+ accountId: Number(session.accountId),
1350
+ dataMode: normalizeDataMode(session.dataMode),
1351
+ dataModes: sessionDataModes,
1352
+ sessionAccountIds,
1353
+ registrationSessionCount: groupedSessions.length || 1,
1354
+ websocketTopic: session.websocketTopic || (session.user && session.user.uuid ? String(session.user.uuid).replace(/-/g, '') : null),
1355
+ dashboardUrl: service.dashboardUrl || null,
1356
+ agent: getPreferredAgent(),
1357
+ service,
1358
+ repoAccounts: listenerRegistrationRepoAccounts()
1359
+ };
1360
+ }
1361
+
1362
+ function listenerRegistrationSignature(payload) {
1363
+ return sha256(stableStringify({
1364
+ baseUrl: payload.baseUrl,
1365
+ accountId: payload.accountId,
1366
+ dataMode: payload.dataMode,
1367
+ dataModes: payload.dataModes || [],
1368
+ sessionAccountIds: payload.sessionAccountIds || [],
1369
+ websocketTopic: payload.websocketTopic,
1370
+ dashboardUrl: payload.dashboardUrl,
1371
+ agent: payload.agent,
1372
+ serviceRunning: payload.service && payload.service.running,
1373
+ repoAccounts: (payload.repoAccounts || []).map((entry) => ({
1374
+ accountId: entry.accountId,
1375
+ type: entry.type,
1376
+ routingScope: entry.routingScope,
1377
+ updatedAt: entry.updatedAt
1378
+ }))
1379
+ }));
1380
+ }
1381
+
1382
+ function shouldRegisterCliListener(session, payload, options = {}) {
1383
+ if (options.force === true) {
1384
+ return true;
1385
+ }
1386
+ const state = readListenerRegistrationState();
1387
+ const key = listenerRegistrationStateKey(session);
1388
+ const existing = state[key];
1389
+ if (!existing) {
1390
+ return true;
1391
+ }
1392
+ const lastRegisteredAtMs = Number(existing.lastRegisteredAtMs || 0);
1393
+ if (!lastRegisteredAtMs || Date.now() - lastRegisteredAtMs >= LISTENER_REGISTRATION_MIN_INTERVAL_MS) {
1394
+ return true;
1395
+ }
1396
+ return existing.signature !== listenerRegistrationSignature(payload);
1397
+ }
1398
+
1399
+ function rememberCliListenerRegistration(session, payload) {
1400
+ const state = readListenerRegistrationState();
1401
+ const key = listenerRegistrationStateKey(session);
1402
+ state[key] = {
1403
+ baseUrl: payload.baseUrl,
1404
+ userId: session.user && session.user.id ? session.user.id : null,
1405
+ signature: listenerRegistrationSignature(payload),
1406
+ accountCount: Array.isArray(payload.repoAccounts) ? payload.repoAccounts.length : 0,
1407
+ sessionAccountIds: payload.sessionAccountIds || [],
1408
+ dataModes: payload.dataModes || [],
1409
+ registrationSessionCount: payload.registrationSessionCount || 1,
1410
+ lastRegisteredAt: new Date().toISOString(),
1411
+ lastRegisteredAtMs: Date.now()
1412
+ };
1413
+ writeListenerRegistrationState(state);
1414
+ }
1415
+
1416
+ async function registerCliListenerSession(session, options = {}) {
1417
+ if (!session || !session.token) {
1418
+ return null;
1419
+ }
1420
+ const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
1421
+ const payload = listenerRegistrationPayload(session, options);
1422
+ if (!shouldRegisterCliListener(session, payload, options)) {
1423
+ return { success: true, skipped: true, reason: 'recently_registered' };
1424
+ }
1425
+ const api = buildAxios(baseUrl, session.token, parsePositiveInt(options.timeoutMs, 5000));
1426
+ const response = await api.post('/cli/register', payload);
1427
+ rememberCliListenerRegistration(session, payload);
1428
+ return response.data;
1429
+ }
1430
+
1431
+ async function bestEffortRegisterCliListeners(options = {}) {
1432
+ const sessions = listSessionsFromStore(readAllSessions()).filter((session) => session && session.token);
1433
+ if (!sessions.length) {
1434
+ return [];
1435
+ }
1436
+ const groupedSessions = [];
1437
+ for (const session of sessions) {
1438
+ const key = listenerRegistrationStateKey(session);
1439
+ let group = groupedSessions.find((entry) => entry.key === key);
1440
+ if (!group) {
1441
+ group = { key, session, sessions: [] };
1442
+ groupedSessions.push(group);
1443
+ }
1444
+ group.sessions.push(session);
1445
+ }
1446
+
1447
+ const results = await Promise.all(groupedSessions.map(async (group) => {
1448
+ try {
1449
+ const data = await registerCliListenerSession(group.session, {
1450
+ ...options,
1451
+ groupedSessions: group.sessions
1452
+ });
1453
+ return {
1454
+ success: true,
1455
+ accountId: group.session.accountId,
1456
+ baseUrl: group.session.baseUrl,
1457
+ groupedAccountIds: group.sessions.map((session) => session.accountId),
1458
+ data
1459
+ };
1460
+ } catch (err) {
1461
+ appendGlobalActivityLog('listener', 'registration.failed', {
1462
+ accountId: group.session.accountId,
1463
+ baseUrl: normalizeBaseUrl(group.session.baseUrl || DEFAULT_BASE_URL),
1464
+ groupedAccountIds: group.sessions.map((session) => session.accountId),
1465
+ error: describeError(err)
1466
+ }, 'error');
1467
+ return {
1468
+ success: false,
1469
+ accountId: group.session.accountId,
1470
+ baseUrl: group.session.baseUrl,
1471
+ groupedAccountIds: group.sessions.map((session) => session.accountId),
1472
+ error: describeError(err)
1473
+ };
1474
+ }
1475
+ }));
1476
+ return results;
1477
+ }
1478
+
1276
1479
  function resolvePreferredAccountId(cwd, flags, session) {
1277
1480
  // Explicit --account-id wins over the repo's account-info.json so users can
1278
1481
  // target a child account (e.g. a customer) from a parent-account repo.
@@ -1987,6 +2190,9 @@ async function authCommand(flags) {
1987
2190
  });
1988
2191
  console.log('Authenticated as user', session.user ? session.user.id : 'unknown', 'for account', session.accountId);
1989
2192
  console.log('Data mode:', session.dataMode);
2193
+ try {
2194
+ await bestEffortRegisterCliListeners({ cwd, reason: 'auth' });
2195
+ } catch (_) { /* best-effort */ }
1990
2196
 
1991
2197
  try {
1992
2198
  const branchName = currentBranch(cwd);
@@ -4027,6 +4233,10 @@ let runtimeState = {
4027
4233
  websocket: {},
4028
4234
  lastAction: null
4029
4235
  };
4236
+ let supportTicketSnapshotCache = {
4237
+ fetchedAtMs: 0,
4238
+ value: null
4239
+ };
4030
4240
 
4031
4241
  function listenerSessionEntriesFromStore(store) {
4032
4242
  return listSessionsFromStore(store).filter((session) => session && session.token && session.websocketTopic);
@@ -4414,6 +4624,7 @@ function exportIssueBundle(snapshot) {
4414
4624
  copyFileIfExists(SESSIONS_FILE, path.join(globalDir, 'sessions.json'));
4415
4625
  copyFileIfExists(CONFIG_FILE, path.join(globalDir, 'config.json'));
4416
4626
  copyFileIfExists(SERVICE_STATE_FILE, path.join(globalDir, 'service-state.json'));
4627
+ copyFileIfExists(LISTENER_REGISTRATION_STATE_FILE, path.join(globalDir, 'listener-registration-state.json'));
4417
4628
  copyFileIfExists(ACCOUNT_REPO_INDEX_FILE, path.join(globalDir, 'account-repos.json'));
4418
4629
  copyFileIfExists(DISPATCH_STATE_FILE, path.join(globalDir, 'dispatch-panes.json'));
4419
4630
  copyFileIfExists(TMUX_ACTIVITY_LOG_FILE, path.join(globalDir, 'tmux-activity.log'));
@@ -4455,6 +4666,7 @@ function buildGlobalFileSnapshot() {
4455
4666
  { label: 'config.json', path: CONFIG_FILE, mode: 'json' },
4456
4667
  { label: 'service-state.json', path: SERVICE_STATE_FILE, mode: 'json' },
4457
4668
  { label: 'websocket-state.json', path: WEBSOCKET_STATE_FILE, mode: 'json' },
4669
+ { label: 'listener-registration-state.json', path: LISTENER_REGISTRATION_STATE_FILE, mode: 'json' },
4458
4670
  { label: 'dispatch-panes.json', path: DISPATCH_STATE_FILE, mode: 'json' },
4459
4671
  { label: 'account-repos.json', path: ACCOUNT_REPO_INDEX_FILE, mode: 'json' },
4460
4672
  { label: 'tmux-activity.log (tail)', path: TMUX_ACTIVITY_LOG_FILE, mode: 'tail' }
@@ -4511,7 +4723,140 @@ function buildRepoSnapshot(entry) {
4511
4723
  };
4512
4724
  }
4513
4725
 
4514
- function collectDashboardSnapshot() {
4726
+ function compactSupportTicketRow(row) {
4727
+ if (!row || typeof row !== 'object') {
4728
+ return null;
4729
+ }
4730
+ return {
4731
+ ticketId: row.ticketId || row.id || null,
4732
+ accountId: row.accountId || row.account_id || null,
4733
+ accountName: row.accountName || null,
4734
+ subject: row.subject || null,
4735
+ priority: row.priority || null,
4736
+ status: row.status || null,
4737
+ type: row.type || null,
4738
+ source: row.source || null,
4739
+ assignedTo: row.assignedTo || null,
4740
+ implementationAccountId: row.implementationAccountId || null,
4741
+ implementationAccountName: row.implementationAccountName || null,
4742
+ affectedComponent: row.affectedComponent || null,
4743
+ updatedAt: row.updatedAt || row.updated || row.lastActivityAt || null,
4744
+ activityAt: row.activityAt || row.lastActivityAt || null,
4745
+ activityLabel: row.activityLabel || null
4746
+ };
4747
+ }
4748
+
4749
+ function supportTicketQueueInput(session) {
4750
+ return {
4751
+ action: 'list',
4752
+ accountId: Number(session.accountId),
4753
+ includeChildren: true,
4754
+ includeRoot: true,
4755
+ sortBy: 'triage',
4756
+ limit: Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)
4757
+ };
4758
+ }
4759
+
4760
+ async function fetchSupportTicketQueueForSession(session) {
4761
+ const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
4762
+ const api = buildAxios(baseUrl, session.token, 12000);
4763
+ const response = await api.post('/cli/tool', {
4764
+ token: session.token,
4765
+ accountId: Number(session.accountId),
4766
+ branchName: '',
4767
+ dataMode: 'prod',
4768
+ name: 'mcp_support_ticket_queue',
4769
+ callId: 'dashboard-support-ticket-queue-' + crypto.randomUUID(),
4770
+ input: supportTicketQueueInput(session)
4771
+ }).then((r) => r.data);
4772
+
4773
+ const result = response && response.result ? response.result : {};
4774
+ const tickets = Array.isArray(result.tickets)
4775
+ ? result.tickets.map(compactSupportTicketRow).filter(Boolean)
4776
+ : [];
4777
+ return {
4778
+ success: response && response.success !== false && response.toolSuccess !== false && result.success !== false,
4779
+ baseUrl,
4780
+ accountId: Number(session.accountId),
4781
+ userId: session.user && session.user.id ? session.user.id : null,
4782
+ dataMode: result.dataMode || response.dataMode || 'prod',
4783
+ totalCount: result.totalCount || tickets.length,
4784
+ count: result.count || tickets.length,
4785
+ counts: result.counts || {},
4786
+ recommendedTicket: compactSupportTicketRow(result.recommendedTicket),
4787
+ tickets,
4788
+ searchedAccounts: result.searchedAccounts || [],
4789
+ message: response.toolMessage || result.message || null
4790
+ };
4791
+ }
4792
+
4793
+ async function collectSupportTicketSnapshot() {
4794
+ const nowMs = Date.now();
4795
+ if (supportTicketSnapshotCache.value &&
4796
+ nowMs - supportTicketSnapshotCache.fetchedAtMs < SUPPORT_TICKET_DASHBOARD_REFRESH_MS) {
4797
+ return supportTicketSnapshotCache.value;
4798
+ }
4799
+
4800
+ const sessions = listSessionsFromStore(readAllSessions())
4801
+ .filter((session) => session && session.token && session.accountId);
4802
+ const seen = new Set();
4803
+ const queueSessions = sessions.filter((session) => {
4804
+ const key = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL) + '|' + String(session.accountId) + '|' +
4805
+ String(session.user && session.user.id ? session.user.id : '');
4806
+ if (seen.has(key)) {
4807
+ return false;
4808
+ }
4809
+ seen.add(key);
4810
+ return true;
4811
+ }).slice(0, 12);
4812
+
4813
+ const queues = [];
4814
+ const errors = [];
4815
+ await Promise.all(queueSessions.map(async (session) => {
4816
+ try {
4817
+ queues.push(await fetchSupportTicketQueueForSession(session));
4818
+ } catch (err) {
4819
+ errors.push({
4820
+ baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
4821
+ accountId: Number(session.accountId),
4822
+ userId: session.user && session.user.id ? session.user.id : null,
4823
+ message: describeError(err)
4824
+ });
4825
+ }
4826
+ }));
4827
+
4828
+ const ticketsById = {};
4829
+ for (const queue of queues) {
4830
+ for (const ticket of queue.tickets || []) {
4831
+ const key = String(ticket.ticketId || '');
4832
+ if (!key || ticketsById[key]) {
4833
+ continue;
4834
+ }
4835
+ ticketsById[key] = {
4836
+ ...ticket,
4837
+ queueAccountId: queue.accountId,
4838
+ baseUrl: queue.baseUrl
4839
+ };
4840
+ }
4841
+ }
4842
+
4843
+ const value = {
4844
+ fetchedAt: new Date(nowMs).toISOString(),
4845
+ refreshMs: SUPPORT_TICKET_DASHBOARD_REFRESH_MS,
4846
+ queueCount: queues.length,
4847
+ totalCount: queues.reduce((sum, queue) => sum + Number(queue.totalCount || 0), 0),
4848
+ tickets: Object.values(ticketsById).slice(0, Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)),
4849
+ queues,
4850
+ errors
4851
+ };
4852
+ supportTicketSnapshotCache = {
4853
+ fetchedAtMs: nowMs,
4854
+ value
4855
+ };
4856
+ return value;
4857
+ }
4858
+
4859
+ async function collectDashboardSnapshot() {
4515
4860
  const accountRepoIndex = readAccountRepoIndex();
4516
4861
  const repoEntries = listAccountRepoEntries(accountRepoIndex)
4517
4862
  .sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
@@ -4566,6 +4911,7 @@ function collectDashboardSnapshot() {
4566
4911
  globalFiles,
4567
4912
  repos: repoEntries,
4568
4913
  platformRepo,
4914
+ supportTickets: await collectSupportTicketSnapshot(),
4569
4915
  issues,
4570
4916
  lastAction: runtimeState.lastAction
4571
4917
  };
@@ -4621,6 +4967,9 @@ function renderDashboardHtml(snapshot) {
4621
4967
  '<div id="last-action" class="empty">No dashboard actions have been triggered yet.</div>' +
4622
4968
  '</section>' +
4623
4969
  '</div>' +
4970
+ '<div class="section-grid">' +
4971
+ '<section class="panel"><h2>Support Tickets</h2><div id="support-ticket-section" class="accordion"></div></section>' +
4972
+ '</div>' +
4624
4973
  '<div class="section-grid">' +
4625
4974
  '<section class="panel"><h2>Issues & Escalation</h2><div id="issues-section" class="accordion"></div></section>' +
4626
4975
  '</div>' +
@@ -4651,9 +5000,10 @@ function renderDashboardHtml(snapshot) {
4651
5000
  'function summaryItem(key,value){return \'<div class="summary-item"><span class="k">\'+escapeHtml(key)+\'</span><span class="v">\'+escapeHtml(value)+\'</span></div>\';}' +
4652
5001
  'function codeBlock(key,content,extraClass){return \'<pre class="code-block \'+(extraClass||"")+\'" data-scroll-key="\'+escapeHtml(key)+\'">\'+escapeHtml(content||"[missing]")+\'</pre>\';}' +
4653
5002
  '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=[' +
5003
+ '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
5004
  '{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
5005
  '{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")},' +
5006
+ '{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
5007
  '{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
5008
  '{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
5009
  '{title:"Repos",value:String(repoCount),label:"Indexed account repos",tone:repoCount?"ok":"warn",sub:"Last scan: "+formatTime(state.discovery&&state.discovery.scannedAt)}' +
@@ -4664,15 +5014,18 @@ function renderDashboardHtml(snapshot) {
4664
5014
  'summaryItem("Repo Matches",String(state.discovery&&state.discovery.matches||0)),' +
4665
5015
  'summaryItem("Connections",String(ws.length)),' +
4666
5016
  'summaryItem("Topics",String(topics)),' +
5017
+ 'summaryItem("Open Tickets",String(state.supportTickets&&state.supportTickets.totalCount||0)),' +
4667
5018
  'summaryItem("Tracked Panes",String(tracked))' +
4668
5019
  '].join("");}' +
4669
5020
  '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
5021
  '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("");}' +
5022
+ '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;}' +
5023
+ '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
5024
  '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
5025
  '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
5026
  '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
5027
  '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);}' +
5028
+ '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
5029
  '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
5030
  '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
5031
  '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 +5084,10 @@ function startDashboardServer(preferredPort) {
4731
5084
  const requestUrl = new URL(req.url, 'http://' + DASHBOARD_HOST + ':' + (runtimeState.service.dashboardPort || preferredPort));
4732
5085
  if (req.method === 'GET' && requestUrl.pathname === '/api/state') {
4733
5086
  try {
5087
+ const snapshot = await collectDashboardSnapshot();
4734
5088
  res.statusCode = 200;
4735
5089
  res.setHeader('Content-Type', 'application/json');
4736
- res.end(JSON.stringify(collectDashboardSnapshot(), null, 2));
5090
+ res.end(JSON.stringify(snapshot, null, 2));
4737
5091
  } catch (err) {
4738
5092
  sendDashboardError(res, err, true);
4739
5093
  }
@@ -4756,11 +5110,13 @@ function startDashboardServer(preferredPort) {
4756
5110
  at: new Date().toISOString()
4757
5111
  };
4758
5112
  if (action === 'export-issues') {
4759
- const bundlePath = exportIssueBundle(collectDashboardSnapshot());
5113
+ const bundlePath = exportIssueBundle(await collectDashboardSnapshot());
4760
5114
  actionResult = { bundlePath };
4761
5115
  runtimeState.lastAction.bundlePath = bundlePath;
4762
5116
  } else if (action === 'rescan-repos') {
4763
5117
  recordDiscoverySummary(discoverAccountRepos());
5118
+ supportTicketSnapshotCache = { fetchedAtMs: 0, value: null };
5119
+ await bestEffortRegisterCliListeners({ reason: 'dashboard.rescan' });
4764
5120
  } else if (action === 'open-tmux') {
4765
5121
  openTmuxSessionForUser();
4766
5122
  } else if (action === 'start-tmux') {
@@ -4777,7 +5133,7 @@ function startDashboardServer(preferredPort) {
4777
5133
  success: true,
4778
5134
  action,
4779
5135
  result: actionResult,
4780
- state: collectDashboardSnapshot()
5136
+ state: await collectDashboardSnapshot()
4781
5137
  }));
4782
5138
  return;
4783
5139
  }
@@ -4793,9 +5149,10 @@ function startDashboardServer(preferredPort) {
4793
5149
 
4794
5150
  if (req.method === 'GET' && requestUrl.pathname === '/') {
4795
5151
  try {
5152
+ const snapshot = await collectDashboardSnapshot();
4796
5153
  res.statusCode = 200;
4797
5154
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
4798
- res.end(renderDashboardHtml(collectDashboardSnapshot()));
5155
+ res.end(renderDashboardHtml(snapshot));
4799
5156
  } catch (err) {
4800
5157
  sendDashboardError(res, err, false);
4801
5158
  }
@@ -5324,14 +5681,15 @@ function processInboundWebsocketMessage(client, baseUrl, topic, accountIds, user
5324
5681
  const acctId = payload.accountId || accountIds[0];
5325
5682
  console.log('[ws][TestSuite][account:' + acctId + ']', JSON.stringify(payload));
5326
5683
  }
5327
- } else if (data.type === 'remits-cli') {
5684
+ } else if (data.type === 'remits-cli' || data.type === 'cli_listener' || data.type === 'listener') {
5328
5685
  const payload = data.payload || data;
5329
5686
  const dispatchAccountId = resolveDispatchAccountId(payload, accountIds[0]);
5330
- console.log('[ws][remits-cli][account:' + dispatchAccountId + ']', JSON.stringify(payload));
5331
- appendGlobalActivityLog('tmux', 'dispatch.received', {
5687
+ console.log('[ws][listener][account:' + dispatchAccountId + ']', JSON.stringify(payload));
5688
+ appendGlobalActivityLog('tmux', 'listener_delivery.received', {
5332
5689
  baseUrl,
5333
5690
  topic,
5334
5691
  accountId: dispatchAccountId,
5692
+ messageType: data.type,
5335
5693
  source,
5336
5694
  messageId,
5337
5695
  message: summarizeDispatchMessage(payload)
@@ -5733,6 +6091,8 @@ async function listenCommand(flags) {
5733
6091
  recordDiscoverySummary(discoverAccountRepos());
5734
6092
  const dashboardPort = Number(flags.port || flags.dashboardPort || DEFAULT_DASHBOARD_PORT);
5735
6093
  await startDashboardServer(dashboardPort);
6094
+ await bestEffortRegisterCliListeners({ reason: 'listener.start' });
6095
+ let lastListenerRegistrationAt = Date.now();
5736
6096
 
5737
6097
  // Create the tmux session immediately so the user can attach right away
5738
6098
  try {
@@ -5820,6 +6180,11 @@ async function listenCommand(flags) {
5820
6180
  activeSessionSignature = currentSignature;
5821
6181
  }
5822
6182
 
6183
+ if (Date.now() - lastListenerRegistrationAt >= LISTENER_REGISTRATION_INTERVAL_MS) {
6184
+ await bestEffortRegisterCliListeners({ reason: 'listener.heartbeat' });
6185
+ lastListenerRegistrationAt = Date.now();
6186
+ }
6187
+
5823
6188
  await runManagedWebsocketWatchdog();
5824
6189
  updateRuntimeServiceState();
5825
6190
  } catch (err) {
@@ -6213,6 +6578,9 @@ async function main() {
6213
6578
 
6214
6579
  // Best-effort: update account-repo index whenever running from inside a remits repo
6215
6580
  try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
6581
+ if (!wantsJson) {
6582
+ try { await bestEffortRegisterCliListeners({ cwd: process.cwd(), reason: 'command' }); } catch (_) { /* best-effort */ }
6583
+ }
6216
6584
 
6217
6585
  // Auto-start the background service if not already running.
6218
6586
  // 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.108",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,