switchroom 0.15.37 → 0.15.39

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 (73) hide show
  1. package/dist/agent-scheduler/index.js +89 -89
  2. package/dist/auth-broker/index.js +89 -89
  3. package/dist/cli/autoaccept-poll.js +13 -7
  4. package/dist/cli/drive-write-pretool.mjs +10 -10
  5. package/dist/cli/notion-write-pretool.mjs +91 -91
  6. package/dist/cli/skill-validate-pretool.mjs +72 -72
  7. package/dist/cli/switchroom.js +857 -572
  8. package/dist/cli/ui/index.html +87 -17
  9. package/dist/host-control/main.js +158 -158
  10. package/dist/vault/approvals/kernel-server.js +91 -91
  11. package/dist/vault/broker/server.js +92 -92
  12. package/package.json +1 -1
  13. package/profiles/_base/cron-session.sh.hbs +1 -1
  14. package/profiles/_base/start.sh.hbs +1 -1
  15. package/profiles/default/CLAUDE.md.hbs +2 -0
  16. package/skills/switchroom-manage/SKILL.md +1 -1
  17. package/skills/switchroom-runtime/SKILL.md +1 -1
  18. package/telegram-plugin/answer-stream.ts +1 -1
  19. package/telegram-plugin/bridge/bridge.ts +18 -1
  20. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  21. package/telegram-plugin/bridge/tool-filter.ts +77 -0
  22. package/telegram-plugin/chat-lock.ts +1 -1
  23. package/telegram-plugin/credits-watch.ts +1 -1
  24. package/telegram-plugin/dist/bridge/bridge.js +141 -115
  25. package/telegram-plugin/dist/gateway/gateway.js +318 -207
  26. package/telegram-plugin/dist/server.js +193 -164
  27. package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
  28. package/telegram-plugin/gateway/boot-card.ts +5 -1
  29. package/telegram-plugin/gateway/boot-probes.ts +62 -0
  30. package/telegram-plugin/gateway/cron-session.ts +1 -1
  31. package/telegram-plugin/gateway/gateway.ts +133 -12
  32. package/telegram-plugin/gateway/grant-restart.ts +1 -1
  33. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
  34. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
  35. package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
  36. package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
  37. package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
  38. package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
  39. package/telegram-plugin/gateway/permission-timeout.ts +70 -0
  40. package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
  41. package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
  42. package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
  43. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
  44. package/telegram-plugin/interrupt-marker.ts +1 -1
  45. package/telegram-plugin/over-ping-safety-net.ts +1 -1
  46. package/telegram-plugin/scoped-approval.ts +1 -1
  47. package/telegram-plugin/secret-detect/vault-error.ts +1 -1
  48. package/telegram-plugin/silence-poke.ts +2 -2
  49. package/telegram-plugin/silent-reply-anchor.ts +1 -1
  50. package/telegram-plugin/slot-banner-driver.ts +1 -1
  51. package/telegram-plugin/startup-reset.ts +1 -1
  52. package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
  53. package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
  54. package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
  55. package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
  56. package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
  57. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
  58. package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
  59. package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
  60. package/telegram-plugin/tests/silence-poke.test.ts +1 -1
  61. package/telegram-plugin/tests/tool-filter.test.ts +87 -0
  62. package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
  63. package/telegram-plugin/turn-flush-safety.ts +1 -1
  64. package/telegram-plugin/uat/assertions.ts +1 -1
  65. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
  66. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
  67. package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
  68. package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
  69. package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
  70. package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
  71. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
  72. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
  73. package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
@@ -474,7 +474,14 @@
474
474
  }
475
475
 
476
476
  @media (min-width: 701px) {
477
- .accounts-grid { display: none; }
477
+ /* The Accounts tab shows a wide-screen TABLE and a mobile card grid,
478
+ hiding the grid on desktop. Scope that hide to #accounts: the
479
+ Connections tab reuses .accounts-grid for its OAuth/Notion cards but
480
+ has NO table fallback, so an unscoped rule blanked every connection
481
+ card on any screen wider than 700px (the cards rendered but were
482
+ display:none, while the empty-state text — not in a grid — still
483
+ showed). */
484
+ #accounts .accounts-grid { display: none; }
478
485
  }
479
486
 
480
487
  @media (max-width: 600px) {
@@ -1029,20 +1036,21 @@
1029
1036
  .then(r => r.ok ? r.json().then(d => ({ ok: true, data: d })) : { ok: false, data: fallback })
1030
1037
  .catch(() => ({ ok: false, data: fallback }));
1031
1038
  try {
1032
- const [g, ms, n, ag] = await Promise.all([
1039
+ const [g, ms, n, li, ag] = await Promise.all([
1033
1040
  safe(fetch(`${API}/api/google-accounts`, { headers: authHeaders() }), []),
1034
1041
  safe(fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }), []),
1035
1042
  safe(fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }), { configured: false, databases: [] }),
1043
+ safe(fetch(`${API}/api/linear-agents`, { headers: authHeaders() }), { configured: false, agents: [] }),
1036
1044
  safe(fetch(`${API}/api/agents`, { headers: authHeaders() }), []),
1037
1045
  ]);
1038
- // Notion legitimately reports unconfigured — not a failure. The OAuth
1039
- // providers + the agent list are the ones whose failure must not read
1040
- // as "empty".
1046
+ // Notion + Linear legitimately report unconfigured — not a failure. The
1047
+ // OAuth providers + the agent list are the ones whose failure must not
1048
+ // read as "empty".
1041
1049
  const fetchFailed = !g.ok || !ms.ok || !ag.ok;
1042
1050
  renderConnections({
1043
- google: g.data, microsoft: ms.data, notion: n.data,
1051
+ google: g.data, microsoft: ms.data, notion: n.data, linear: li.data,
1044
1052
  agentNames: (ag.data || []).map(a => a.name).sort(),
1045
- googleFailed: !g.ok, microsoftFailed: !ms.ok, fetchFailed,
1053
+ googleFailed: !g.ok, microsoftFailed: !ms.ok, linearFailed: !li.ok, fetchFailed,
1046
1054
  });
1047
1055
  clearError();
1048
1056
  // Self-heal a transient blip without a manual re-click. Bounded
@@ -1240,12 +1248,33 @@
1240
1248
  }
1241
1249
  }
1242
1250
 
1243
- function switchTab(tab) {
1244
- const tabs = ['summary', 'agents', 'accounts', 'system', 'memory', 'connections', 'schedule', 'approvals'];
1245
- for (const t of tabs) {
1251
+ // Canonical tab list — kept in sync with the nav buttons above and with
1252
+ // SPA_TAB_ROUTES in src/web/server.ts (the server serves the SPA shell for
1253
+ // these paths so a reload/deep-link doesn't 404).
1254
+ const TAB_NAMES = ['summary', 'agents', 'accounts', 'system', 'memory', 'connections', 'schedule', 'approvals'];
1255
+
1256
+ // Derive the active tab from the URL path (`/connections` → 'connections',
1257
+ // '/' → 'summary'). Unknown paths fall back to summary.
1258
+ function tabFromPath() {
1259
+ const name = location.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
1260
+ return TAB_NAMES.includes(name) ? name : 'summary';
1261
+ }
1262
+
1263
+ // opts.push === false suppresses the history push — used when the switch is
1264
+ // itself driven by navigation (initial load, back/forward) so we don't
1265
+ // double-stack history entries.
1266
+ function switchTab(tab, opts) {
1267
+ if (!TAB_NAMES.includes(tab)) tab = 'summary';
1268
+ for (const t of TAB_NAMES) {
1246
1269
  document.getElementById(`tab-${t}`).classList.toggle('active', tab === t);
1247
1270
  document.getElementById(t).style.display = tab === t ? '' : 'none';
1248
1271
  }
1272
+ // Mirror the tab into the URL path so reload/deep-link/back-forward all
1273
+ // land here. Summary canonicalises to '/'.
1274
+ if (!opts || opts.push !== false) {
1275
+ const path = tab === 'summary' ? '/' : '/' + tab;
1276
+ if (location.pathname !== path) history.pushState({ tab }, '', path);
1277
+ }
1249
1278
  if (tab === 'summary') fetchSummary();
1250
1279
  if (tab === 'accounts') fetchAccounts();
1251
1280
  if (tab === 'system') fetchSystemHealth();
@@ -1255,6 +1284,11 @@
1255
1284
  if (tab === 'approvals') fetchApprovals();
1256
1285
  }
1257
1286
 
1287
+ // Back/forward: restore the tab from the history entry (or the path).
1288
+ window.addEventListener('popstate', (e) => {
1289
+ switchTab((e.state && e.state.tab) || tabFromPath(), { push: false });
1290
+ });
1291
+
1258
1292
  // Fleet overview — ONE round-trip to the server-side aggregate
1259
1293
  // (/api/summary), which pulls each part through the same per-tab
1260
1294
  // cache so a Summary open warms the tabs and they can never disagree.
@@ -2117,6 +2151,7 @@
2117
2151
  const google = data.google || [];
2118
2152
  const microsoft = data.microsoft || [];
2119
2153
  const notion = data.notion || { configured: false, databases: [] };
2154
+ const linear = data.linear || { configured: false, agents: [] };
2120
2155
  const agentNames = data.agentNames || [];
2121
2156
 
2122
2157
  // When a provider's fetch FAILED (not genuinely empty), its empty-state
@@ -2180,6 +2215,37 @@
2180
2215
  ${fullAccessBanner}${notionBody}
2181
2216
  </div>`;
2182
2217
 
2218
+ // Linear — first-class OAuth app actors (config-only; the web tier never
2219
+ // reads the per-agent vault, so no token/expiry is shown — and never the
2220
+ // secret). One card per agent with linear_agent.enabled.
2221
+ const linearCards = (linear.agents || []).map(a => {
2222
+ const meta = [];
2223
+ if (a.workspaceId) meta.push(`<div class="meta-item"><label>Workspace </label><span>${escapeHtml(a.workspaceId)}</span></div>`);
2224
+ if (a.defaultTeamId) meta.push(`<div class="meta-item"><label>Default team </label><span>${escapeHtml(a.defaultTeamId)}</span></div>`);
2225
+ if (a.tokenVaultKey) meta.push(`<div class="meta-item"><label>Token </label><span><code>${escapeHtml(a.tokenVaultKey)}</code></span></div>`);
2226
+ return `
2227
+ <div class="account-card">
2228
+ <div class="account-card-header">
2229
+ <div class="account-label">${escapeHtml(a.agent)}</div>
2230
+ <span style="margin-left:auto" class="usage-pill primary">OAuth agent</span>
2231
+ </div>
2232
+ <div class="card-meta" style="padding:0">${meta.join('') || _dimC('—')}</div>
2233
+ </div>`;
2234
+ }).join('');
2235
+ const linearNote = (linear.agents && linear.agents.length)
2236
+ ? `<div style="margin:0 0 .6rem;font-size:.85rem;color:var(--text-dim)">Connected as Linear <b>OAuth app actors</b> — each appears in the workspace as its own actor (not a personal API token for MCP).</div>`
2237
+ : '';
2238
+ const linearBody = linearCards
2239
+ ? `<div class="accounts-grid">${linearCards}</div>`
2240
+ : `<div class="loading" style="padding:.8rem">${data.linearFailed
2241
+ ? "Couldn't load Linear agents — the data source was unreachable (retrying)."
2242
+ : 'No Linear agents connected. Run <code>switchroom linear-agent setup --agent &lt;name&gt;</code> to connect one as a Linear OAuth app actor.'}</div>`;
2243
+ const linearSection = `
2244
+ <div style="margin-bottom:1.5rem">
2245
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">Linear</h3>
2246
+ ${linearNote}${linearBody}
2247
+ </div>`;
2248
+
2183
2249
  // Tab-level degraded banner: when every OAuth account (Google +
2184
2250
  // Microsoft) is config-only (no live broker slot) and there's at least
2185
2251
  // one, the broker data is unavailable — surface that ABOVE the sections
@@ -2197,7 +2263,7 @@
2197
2263
  ? renderProblem(problemFor('connections-unreachable', {}))
2198
2264
  : '';
2199
2265
 
2200
- container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection;
2266
+ container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection + linearSection;
2201
2267
  }
2202
2268
 
2203
2269
  function renderSchedule(data) {
@@ -2635,12 +2701,16 @@
2635
2701
  }
2636
2702
  }
2637
2703
 
2638
- // Init. Summary is the default visible tab; fetchAgents still runs
2639
- // (populates the agents tab + keeps the 10s fleet poll warm + the
2640
- // log WS depends on it). Summary is fetched on init + on tab-switch
2641
- // only deliberately NOT on the 10s interval (it fans out to
2642
- // system-health etc.; on-demand refresh is enough).
2643
- fetchSummary();
2704
+ // Init. The initial tab comes from the URL path (so a reload/deep-link of
2705
+ // `/connections` opens Connections, not Summary); '/' Summary. We stamp
2706
+ // the history entry so the first back/forward works, then switchTab
2707
+ // (push:false) shows the tab and fetches just its data. fetchAgents still
2708
+ // runs unconditionally (keeps the 10s fleet poll warm + the log WS depends
2709
+ // on it). Per-tab data is fetched on switch only — deliberately NOT on the
2710
+ // 10s interval (it fans out to system-health etc.; on-demand is enough).
2711
+ const initialTab = tabFromPath();
2712
+ history.replaceState({ tab: initialTab }, '', location.pathname);
2713
+ switchTab(initialTab, { push: false });
2644
2714
  fetchAgents();
2645
2715
  connectWebSocket();
2646
2716
  // Fleet poll — gated on tab visibility. A phone with the dashboard