botmux 2.86.0 → 2.87.0

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 (141) hide show
  1. package/README.en.md +26 -0
  2. package/README.md +26 -0
  3. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/herdr-backend.js +12 -1
  5. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  6. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  7. package/dist/adapters/backend/pty-backend.js +4 -1
  8. package/dist/adapters/backend/pty-backend.js.map +1 -1
  9. package/dist/adapters/backend/tmux-backend.d.ts +1 -1
  10. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  11. package/dist/adapters/backend/tmux-backend.js +22 -9
  12. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  13. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  14. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  15. package/dist/adapters/backend/types.d.ts +9 -0
  16. package/dist/adapters/backend/types.d.ts.map +1 -1
  17. package/dist/adapters/backend/types.js.map +1 -1
  18. package/dist/adapters/backend/zellij-backend.js +1 -1
  19. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  20. package/dist/adapters/cli/codex.d.ts.map +1 -1
  21. package/dist/adapters/cli/codex.js +8 -5
  22. package/dist/adapters/cli/codex.js.map +1 -1
  23. package/dist/adapters/cli/shared-hints.d.ts +2 -1
  24. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  25. package/dist/adapters/cli/shared-hints.js +25 -3
  26. package/dist/adapters/cli/shared-hints.js.map +1 -1
  27. package/dist/bot-registry.d.ts +10 -0
  28. package/dist/bot-registry.d.ts.map +1 -1
  29. package/dist/bot-registry.js +6 -0
  30. package/dist/bot-registry.js.map +1 -1
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +362 -16
  33. package/dist/cli.js.map +1 -1
  34. package/dist/core/command-handler.d.ts.map +1 -1
  35. package/dist/core/command-handler.js +6 -4
  36. package/dist/core/command-handler.js.map +1 -1
  37. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  38. package/dist/core/dashboard-ipc-server.js +45 -0
  39. package/dist/core/dashboard-ipc-server.js.map +1 -1
  40. package/dist/core/maintenance.d.ts +18 -0
  41. package/dist/core/maintenance.d.ts.map +1 -1
  42. package/dist/core/maintenance.js +58 -30
  43. package/dist/core/maintenance.js.map +1 -1
  44. package/dist/core/per-bot-env.d.ts +38 -0
  45. package/dist/core/per-bot-env.d.ts.map +1 -0
  46. package/dist/core/per-bot-env.js +79 -0
  47. package/dist/core/per-bot-env.js.map +1 -0
  48. package/dist/core/session-manager.d.ts +3 -0
  49. package/dist/core/session-manager.d.ts.map +1 -1
  50. package/dist/core/session-manager.js +70 -6
  51. package/dist/core/session-manager.js.map +1 -1
  52. package/dist/core/skills/discovery.d.ts +7 -0
  53. package/dist/core/skills/discovery.d.ts.map +1 -1
  54. package/dist/core/skills/discovery.js +51 -0
  55. package/dist/core/skills/discovery.js.map +1 -1
  56. package/dist/core/trigger-session.d.ts.map +1 -1
  57. package/dist/core/trigger-session.js +5 -2
  58. package/dist/core/trigger-session.js.map +1 -1
  59. package/dist/core/update-check.d.ts +67 -0
  60. package/dist/core/update-check.d.ts.map +1 -0
  61. package/dist/core/update-check.js +159 -0
  62. package/dist/core/update-check.js.map +1 -0
  63. package/dist/core/worker-pool.d.ts.map +1 -1
  64. package/dist/core/worker-pool.js +4 -0
  65. package/dist/core/worker-pool.js.map +1 -1
  66. package/dist/daemon.d.ts.map +1 -1
  67. package/dist/daemon.js +29 -6
  68. package/dist/daemon.js.map +1 -1
  69. package/dist/dashboard/bot-payload.d.ts +1 -0
  70. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  71. package/dist/dashboard/bot-payload.js +1 -0
  72. package/dist/dashboard/bot-payload.js.map +1 -1
  73. package/dist/dashboard/skill-install-request.d.ts +10 -0
  74. package/dist/dashboard/skill-install-request.d.ts.map +1 -1
  75. package/dist/dashboard/skill-install-request.js +17 -0
  76. package/dist/dashboard/skill-install-request.js.map +1 -1
  77. package/dist/dashboard/web/app.d.ts.map +1 -1
  78. package/dist/dashboard/web/app.js +29 -1
  79. package/dist/dashboard/web/app.js.map +1 -1
  80. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  81. package/dist/dashboard/web/bot-defaults.js +62 -1
  82. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  83. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  84. package/dist/dashboard/web/i18n.js +128 -0
  85. package/dist/dashboard/web/i18n.js.map +1 -1
  86. package/dist/dashboard/web/settings.d.ts.map +1 -1
  87. package/dist/dashboard/web/settings.js +289 -0
  88. package/dist/dashboard/web/settings.js.map +1 -1
  89. package/dist/dashboard/web/skills.d.ts.map +1 -1
  90. package/dist/dashboard/web/skills.js +183 -10
  91. package/dist/dashboard/web/skills.js.map +1 -1
  92. package/dist/dashboard/web/whiteboards.d.ts +2 -0
  93. package/dist/dashboard/web/whiteboards.d.ts.map +1 -0
  94. package/dist/dashboard/web/whiteboards.js +267 -0
  95. package/dist/dashboard/web/whiteboards.js.map +1 -0
  96. package/dist/dashboard-web/app.js +774 -613
  97. package/dist/dashboard-web/index.html +1 -0
  98. package/dist/dashboard-web/style.css +271 -1
  99. package/dist/dashboard.js +259 -3
  100. package/dist/dashboard.js.map +1 -1
  101. package/dist/global-config.d.ts +6 -0
  102. package/dist/global-config.d.ts.map +1 -1
  103. package/dist/global-config.js +12 -0
  104. package/dist/global-config.js.map +1 -1
  105. package/dist/im/lark/card-handler.d.ts.map +1 -1
  106. package/dist/im/lark/card-handler.js +7 -3
  107. package/dist/im/lark/card-handler.js.map +1 -1
  108. package/dist/services/bot-config-store.d.ts.map +1 -1
  109. package/dist/services/bot-config-store.js +19 -0
  110. package/dist/services/bot-config-store.js.map +1 -1
  111. package/dist/services/resumable-session-discovery.d.ts.map +1 -1
  112. package/dist/services/resumable-session-discovery.js +21 -4
  113. package/dist/services/resumable-session-discovery.js.map +1 -1
  114. package/dist/services/skill-registry-store.d.ts +1 -0
  115. package/dist/services/skill-registry-store.d.ts.map +1 -1
  116. package/dist/services/skill-registry-store.js +32 -0
  117. package/dist/services/skill-registry-store.js.map +1 -1
  118. package/dist/services/whiteboard-store.d.ts +67 -0
  119. package/dist/services/whiteboard-store.d.ts.map +1 -0
  120. package/dist/services/whiteboard-store.js +450 -0
  121. package/dist/services/whiteboard-store.js.map +1 -0
  122. package/dist/setup/bot-config-editor.d.ts +8 -0
  123. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  124. package/dist/setup/bot-config-editor.js +9 -0
  125. package/dist/setup/bot-config-editor.js.map +1 -1
  126. package/dist/skills/definitions.d.ts.map +1 -1
  127. package/dist/skills/definitions.js +118 -0
  128. package/dist/skills/definitions.js.map +1 -1
  129. package/dist/types.d.ts +3 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/utils/install-diagnostics.d.ts +52 -0
  132. package/dist/utils/install-diagnostics.d.ts.map +1 -0
  133. package/dist/utils/install-diagnostics.js +155 -0
  134. package/dist/utils/install-diagnostics.js.map +1 -0
  135. package/dist/utils/install-info.d.ts +4 -0
  136. package/dist/utils/install-info.d.ts.map +1 -1
  137. package/dist/utils/install-info.js +6 -0
  138. package/dist/utils/install-info.js.map +1 -1
  139. package/dist/worker.js +12 -0
  140. package/dist/worker.js.map +1 -1
  141. package/package.json +1 -1
@@ -25,6 +25,7 @@
25
25
  <a href="#/workflows" data-route="workflows"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="3.4" cy="8" r="1.9"/><circle cx="12.6" cy="3.6" r="1.9"/><circle cx="12.6" cy="12.4" r="1.9"/><path d="M5.2 7.2l5.6-2.8M5.2 8.8l5.6 2.8"/></svg><span data-i18n="nav.workflows">工作流(beta)</span></a>
26
26
  <a href="#/groups" data-route="groups"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="5.6" cy="5.8" r="2.4"/><circle cx="11" cy="6.8" r="1.9"/><path d="M1.8 13.2c.5-2.4 2-3.6 3.8-3.6s3.3 1.2 3.8 3.6M9.8 12.6c.4-1.7 1.5-2.6 2.8-2.6 1 0 1.9.5 2.4 1.6"/></svg><span data-i18n="nav.groups">群组</span></a>
27
27
  <a href="#/schedules" data-route="schedules"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.2"/><path d="M8 4.5V8l2.4 1.6"/></svg><span data-i18n="nav.schedules">定时</span></a>
28
+ <a href="#/whiteboards" data-route="whiteboards"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.2" y="2.2" width="11.6" height="11.6" rx="2"/><path d="M4.8 5.2h6.4M4.8 8h6.4M4.8 10.8h4"/></svg><span>白板</span></a>
28
29
  <a href="#/roles" data-route="roles"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.8l5.2 2v3.4c0 3.4-2.2 5.9-5.2 7-3-1.1-5.2-3.6-5.2-7V3.8z"/><path d="M5.8 8l1.6 1.6 2.8-3"/></svg><span data-i18n="nav.roles">角色管理</span></a>
29
30
  <a href="#/bot-defaults" data-route="bot-defaults"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.5" y="5" width="11" height="8" rx="2"/><circle cx="5.8" cy="9" r="1"/><circle cx="10.2" cy="9" r="1"/><path d="M8 5V2.5M5.5 13v1.2M10.5 13v1.2"/></svg><span data-i18n="nav.botDefaults">Bot 配置</span></a>
30
31
  <a href="#/skills" data-route="skills"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2.5h10v3H3zM3 7h10v6.5H3z"/><path d="M5.4 9.2h5.2M5.4 11.2h3.8"/></svg><span data-i18n="nav.skills">Skills</span></a>
@@ -157,6 +157,20 @@ a { color: inherit; }
157
157
  color: var(--accent-strong);
158
158
  }
159
159
 
160
+ /* Red dot on the Settings nav when a newer botmux version is available. */
161
+ .sidebar-nav a.nav-has-update { position: relative; }
162
+ .sidebar-nav a.nav-has-update::after {
163
+ content: "";
164
+ position: absolute;
165
+ top: 7px;
166
+ right: 8px;
167
+ width: 7px;
168
+ height: 7px;
169
+ border-radius: 50%;
170
+ background: var(--danger);
171
+ box-shadow: 0 0 0 2px var(--surface);
172
+ }
173
+
160
174
  .sidebar-foot {
161
175
  margin-top: auto;
162
176
  padding: 10px 8px 4px;
@@ -2228,6 +2242,21 @@ input.row-select,
2228
2242
  }
2229
2243
  .bd-section-note { margin: 6px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
2230
2244
 
2245
+ /* ── Version & update card ── */
2246
+ .update-version { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
2247
+ .update-badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 999px; }
2248
+ .update-badge-new { color: var(--accent-strong); background: var(--accent-soft); }
2249
+ .update-badge-ok { color: var(--success); background: var(--success-soft); }
2250
+ .update-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
2251
+ .update-install-list { margin: 6px 0 0; padding-left: 18px; font-size: 12px; line-height: 1.6; }
2252
+ .update-install-list code { font-size: 11px; }
2253
+ .update-install-list small { color: var(--muted); }
2254
+ .update-changelog { margin-top: 14px; display: flex; flex-direction: column; gap: 8px; }
2255
+ .update-release { border: 1px solid var(--border-soft); border-radius: 8px; padding: 8px 12px; background: var(--surface-muted); }
2256
+ .update-release > summary { cursor: pointer; font-size: 13px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
2257
+ .update-release > summary small { color: var(--muted); font-size: 11px; }
2258
+ .update-release-body { margin: 8px 0 0; white-space: pre-wrap; word-break: break-word; font-size: 12px; line-height: 1.5; max-height: 320px; overflow: auto; color: var(--fg); }
2259
+
2231
2260
  /* A lighter sub-group inside a bd-section (e.g. 主动开工 lives under 新群 Oncall).
2232
2261
  Separated by a soft top rule + indent so it reads as "part of this block". */
2233
2262
  .bd-subsection {
@@ -4179,6 +4208,19 @@ button.contrast:hover { background: var(--danger-soft); border-color: var(--dang
4179
4208
  min-width: 0;
4180
4209
  }
4181
4210
 
4211
+ .skills-source-control {
4212
+ display: grid;
4213
+ grid-template-columns: minmax(0, 1fr) auto;
4214
+ gap: 8px;
4215
+ align-items: center;
4216
+ }
4217
+
4218
+ .skills-source-control button {
4219
+ min-height: 36px;
4220
+ padding: 0 13px;
4221
+ white-space: nowrap;
4222
+ }
4223
+
4182
4224
  .skills-main-panel,
4183
4225
  .skills-bot-grid {
4184
4226
  display: grid;
@@ -4323,7 +4365,27 @@ button.contrast:hover { background: var(--danger-soft); border-color: var(--dang
4323
4365
  line-height: 1.45;
4324
4366
  overflow-wrap: anywhere;
4325
4367
  }
4326
- .skills-row small { color: var(--muted); font-family: var(--mono); font-size: 11px; overflow-wrap: anywhere; }
4368
+ .skills-row small {
4369
+ color: var(--muted);
4370
+ font-family: var(--mono);
4371
+ font-size: 11px;
4372
+ overflow-wrap: anywhere;
4373
+ }
4374
+
4375
+ .skills-source-badge {
4376
+ display: inline-flex;
4377
+ align-items: center;
4378
+ width: fit-content;
4379
+ max-width: 100%;
4380
+ min-height: 22px;
4381
+ margin-top: 4px;
4382
+ padding: 2px 7px;
4383
+ border: 1px solid var(--border-soft);
4384
+ border-radius: 999px;
4385
+ background: var(--surface-muted);
4386
+ overflow: hidden;
4387
+ text-overflow: ellipsis;
4388
+ }
4327
4389
 
4328
4390
  .skills-card-actions {
4329
4391
  display: grid;
@@ -4758,6 +4820,213 @@ button.contrast:hover { background: var(--danger-soft); border-color: var(--dang
4758
4820
  align-self: start;
4759
4821
  }
4760
4822
 
4823
+ .skills-discovery-dialog {
4824
+ width: min(860px, calc(100vw - 44px));
4825
+ max-height: min(760px, calc(100vh - 64px));
4826
+ padding: 0;
4827
+ border: 1px solid var(--border-soft);
4828
+ border-radius: 14px;
4829
+ background: var(--surface);
4830
+ box-shadow: var(--modal-shadow);
4831
+ color: var(--fg);
4832
+ }
4833
+
4834
+ .skills-discovery-dialog::backdrop {
4835
+ background: color-mix(in srgb, #0b1524 42%, transparent);
4836
+ backdrop-filter: blur(4px);
4837
+ }
4838
+
4839
+ .skills-discovery-dialog article {
4840
+ display: grid;
4841
+ grid-template-rows: auto minmax(0, 1fr) auto;
4842
+ max-height: inherit;
4843
+ }
4844
+
4845
+ .skills-discovery-dialog header {
4846
+ display: grid;
4847
+ gap: 5px;
4848
+ padding: 18px 20px 12px;
4849
+ border-bottom: 1px solid var(--border-soft);
4850
+ }
4851
+
4852
+ .skills-discovery-dialog h3,
4853
+ .skills-discovery-dialog p {
4854
+ margin: 0;
4855
+ }
4856
+
4857
+ .skills-discovery-dialog h3 {
4858
+ font-size: 17px;
4859
+ }
4860
+
4861
+ .skills-discovery-dialog p {
4862
+ color: var(--muted);
4863
+ font-size: 12.5px;
4864
+ line-height: 1.5;
4865
+ }
4866
+
4867
+ .skills-discovery-body {
4868
+ display: grid;
4869
+ gap: 12px;
4870
+ min-height: 0;
4871
+ max-height: 520px;
4872
+ overflow: auto;
4873
+ padding: 16px 20px;
4874
+ }
4875
+
4876
+ .skills-discovery-tabs {
4877
+ display: flex;
4878
+ gap: 8px;
4879
+ align-items: center;
4880
+ min-height: 40px;
4881
+ overflow-x: auto;
4882
+ overflow-y: hidden;
4883
+ padding-bottom: 2px;
4884
+ scrollbar-width: thin;
4885
+ }
4886
+
4887
+ .skills-discovery-tabs button {
4888
+ display: grid;
4889
+ grid-template-columns: auto auto;
4890
+ gap: 8px;
4891
+ align-items: center;
4892
+ flex: 0 0 auto;
4893
+ min-height: 38px;
4894
+ padding: 0 12px;
4895
+ border-radius: 999px;
4896
+ text-transform: uppercase;
4897
+ }
4898
+
4899
+ .skills-discovery-tabs button.selected {
4900
+ border-color: color-mix(in srgb, var(--accent) 72%, var(--border-soft));
4901
+ background: color-mix(in srgb, var(--accent) 10%, var(--surface));
4902
+ color: var(--fg);
4903
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);
4904
+ }
4905
+
4906
+ .skills-discovery-tabs strong {
4907
+ font-size: 12px;
4908
+ }
4909
+
4910
+ .skills-discovery-tabs small {
4911
+ color: var(--muted);
4912
+ font-size: 11px;
4913
+ font-weight: 700;
4914
+ text-transform: none;
4915
+ }
4916
+
4917
+ .skills-discovery-path {
4918
+ min-width: 0;
4919
+ padding: 7px 10px;
4920
+ border-radius: 8px;
4921
+ background: color-mix(in srgb, var(--surface-muted) 70%, transparent);
4922
+ }
4923
+
4924
+ .skills-discovery-path code {
4925
+ display: block;
4926
+ overflow: hidden;
4927
+ color: var(--muted);
4928
+ font-size: 12px;
4929
+ text-overflow: ellipsis;
4930
+ white-space: nowrap;
4931
+ }
4932
+
4933
+ .skills-discovery-group {
4934
+ display: grid;
4935
+ gap: 8px;
4936
+ }
4937
+
4938
+ .skills-discovery-group[hidden] {
4939
+ display: none;
4940
+ }
4941
+
4942
+ .skills-discovery-group header {
4943
+ display: grid;
4944
+ grid-template-columns: auto minmax(0, 1fr);
4945
+ gap: 8px;
4946
+ align-items: center;
4947
+ padding: 0;
4948
+ border: 0;
4949
+ }
4950
+
4951
+ .skills-discovery-group strong {
4952
+ text-transform: uppercase;
4953
+ color: var(--fg);
4954
+ font-size: 12px;
4955
+ }
4956
+
4957
+ .skills-discovery-group code {
4958
+ overflow: hidden;
4959
+ color: var(--muted);
4960
+ font-size: 11px;
4961
+ text-overflow: ellipsis;
4962
+ white-space: nowrap;
4963
+ }
4964
+
4965
+ .skills-discovery-list {
4966
+ display: grid;
4967
+ gap: 7px;
4968
+ }
4969
+
4970
+ .skills-discovery-row {
4971
+ display: grid;
4972
+ grid-template-columns: auto minmax(0, 1fr) auto;
4973
+ gap: 10px;
4974
+ align-items: start;
4975
+ min-height: 54px;
4976
+ padding: 10px 12px;
4977
+ border: 1px solid var(--border-soft);
4978
+ border-radius: 8px;
4979
+ background: var(--surface-muted);
4980
+ cursor: pointer;
4981
+ }
4982
+
4983
+ .skills-discovery-row input {
4984
+ margin-top: 3px;
4985
+ }
4986
+
4987
+ .skills-discovery-row span {
4988
+ display: grid;
4989
+ gap: 3px;
4990
+ min-width: 0;
4991
+ }
4992
+
4993
+ .skills-discovery-row strong {
4994
+ overflow: hidden;
4995
+ color: var(--fg);
4996
+ font-size: 13px;
4997
+ text-overflow: ellipsis;
4998
+ white-space: nowrap;
4999
+ }
5000
+
5001
+ .skills-discovery-row small {
5002
+ display: -webkit-box;
5003
+ -webkit-line-clamp: 2;
5004
+ -webkit-box-orient: vertical;
5005
+ overflow: hidden;
5006
+ color: var(--muted);
5007
+ font-size: 12px;
5008
+ line-height: 1.4;
5009
+ }
5010
+
5011
+ .skills-discovery-row em {
5012
+ align-self: center;
5013
+ color: var(--muted);
5014
+ font-size: 11px;
5015
+ font-style: normal;
5016
+ font-weight: 700;
5017
+ }
5018
+
5019
+ .skills-discovery-row.installed {
5020
+ opacity: 0.68;
5021
+ cursor: default;
5022
+ }
5023
+
5024
+ .skills-discovery-dialog footer {
5025
+ justify-content: end;
5026
+ padding: 12px 20px 16px;
5027
+ border-top: 1px solid var(--border-soft);
5028
+ }
5029
+
4761
5030
  .skills-policy-controls [data-bot-status] {
4762
5031
  grid-column: 1 / -1;
4763
5032
  }
@@ -4803,6 +5072,7 @@ button.contrast:hover { background: var(--danger-soft); border-color: var(--dang
4803
5072
  }
4804
5073
  .skills-side-rail,
4805
5074
  .skills-install-grid,
5075
+ .skills-source-control,
4806
5076
  .skills-section-head-row,
4807
5077
  .skills-overview,
4808
5078
  .skills-metric-strip,
package/dist/dashboard.js CHANGED
@@ -27,15 +27,24 @@ import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
27
27
  import { CLI_SELECT_OPTIONS, resolveCliSelection, isTtadkWrapper, ttadkAcceptsModel, TTADK_DEFAULT_MODEL, TTADK_MODEL_SUGGESTIONS, } from './setup/cli-selection.js';
28
28
  import { invalidWorkingDirs } from './utils/working-dir.js';
29
29
  import { mergeDashboardConfig, mergeGlobalConfig, mergeMaintenanceConfig, parseMaintenancePatch, readGlobalConfig, setGlobalLocale } from './global-config.js';
30
+ import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
30
31
  import { isLocale } from './i18n/types.js';
31
- import { isLocalDevInstall } from './utils/install-info.js';
32
+ import { isLocalDevInstall, botmuxVersion } from './utils/install-info.js';
33
+ import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
34
+ import { fetchLatestVersion, fetchReleasesSince, isNewerVersion } from './core/update-check.js';
35
+ import { GITHUB_REPO } from './core/restart-report.js';
36
+ import { spawnDetachedRestart, npmGlobalUpdateLockTarget } from './core/maintenance.js';
37
+ import { writeRestartIntent } from './services/restart-intent-store.js';
38
+ import { withFileLock } from './utils/file-lock.js';
39
+ import { spawn } from 'node:child_process';
32
40
  import { listTeamReports, readTeamBoard, setTeamBoardEntry } from './services/team-board-store.js';
33
41
  import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
34
- import { readSkillRegistry, removeInstalledSkill, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
42
+ import { installLocalSkillLinks, readSkillRegistry, removeInstalledSkill, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
35
43
  import { redactGitUrlCredentials } from './core/skills/sources.js';
36
44
  import { loadBotConfigs } from './bot-registry.js';
45
+ import { discoverNativeCliSkillGroups } from './core/skills/discovery.js';
37
46
  import { analyzeSkillReferences } from './core/skills/references.js';
38
- import { installDashboardSkill, parseDashboardSkillInstallRequest } from './dashboard/skill-install-request.js';
47
+ import { installDashboardSkill, parseDashboardSkillInstallRequest, parseInstallLocalLinksSources, MAX_LOCAL_LINK_SOURCES } from './dashboard/skill-install-request.js';
39
48
  import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
40
49
  import { isValidRoleProfileId } from './services/role-profile-store.js';
41
50
  import { mergeSafeInsightOverviews } from './services/insight/report.js';
@@ -116,6 +125,7 @@ function resolveDashboardSettings() {
116
125
  repoPickerMode: global.repoPickerMode ?? 'all',
117
126
  maintenance: global.maintenance ?? {},
118
127
  localDevInstall: isLocalDevInstall(),
128
+ whiteboard: { enabled: global.whiteboard?.enabled === true },
119
129
  };
120
130
  }
121
131
  async function readJsonBody(req) {
@@ -125,6 +135,67 @@ async function readJsonBody(req) {
125
135
  const raw = Buffer.concat(chunks).toString('utf8').trim();
126
136
  return raw ? JSON.parse(raw) : {};
127
137
  }
138
+ /** Fast in-process guard against double-clicks within this dashboard process.
139
+ * Cross-process serialization against the maintenance auto-update (a different
140
+ * process) is handled separately by the shared file lock in the run route. */
141
+ let updateInFlight = false;
142
+ // Cache the upstream version/changelog lookups so the nav-badge check + the
143
+ // Settings card don't hammer the npm registry / GitHub on every page load.
144
+ // GitHub's unauthenticated API is only 60 req/h per IP, so caching the changelog
145
+ // also keeps us from exhausting it. Failures cache briefly so they self-heal.
146
+ const LATEST_TTL_MS = 30 * 60_000;
147
+ const CHANGELOG_TTL_MS = 15 * 60_000;
148
+ const FAILURE_TTL_MS = 60_000;
149
+ let latestVersionCache = null;
150
+ let changelogCache = null;
151
+ async function cachedLatestVersion(now = Date.now()) {
152
+ const ttl = latestVersionCache?.value ? LATEST_TTL_MS : FAILURE_TTL_MS;
153
+ if (latestVersionCache && now - latestVersionCache.at < ttl)
154
+ return latestVersionCache.value;
155
+ const value = await fetchLatestVersion();
156
+ latestVersionCache = { value, at: now };
157
+ return value;
158
+ }
159
+ async function cachedChangelog(current, now = Date.now()) {
160
+ const ttl = changelogCache?.value.ok ? CHANGELOG_TTL_MS : FAILURE_TTL_MS;
161
+ if (changelogCache && changelogCache.key === current && now - changelogCache.at < ttl)
162
+ return changelogCache.value;
163
+ const value = await fetchReleasesSince(current);
164
+ changelogCache = { key: current, value, at: now };
165
+ return value;
166
+ }
167
+ /**
168
+ * Run `npm install -g botmux@latest` for the manual-update flow WITHOUT blocking
169
+ * the event loop (async spawn, not execSync — the dashboard must keep serving
170
+ * during the ~10-30s install). Resolves on exit 0; rejects with the tail of
171
+ * stdout/stderr on a non-zero exit, spawn error, or 3-minute timeout. Args are
172
+ * a fixed literal — no shell interpolation of untrusted input.
173
+ */
174
+ function runNpmInstallLatest() {
175
+ return new Promise((resolve, reject) => {
176
+ const child = spawn('npm', ['install', '-g', 'botmux@latest'], {
177
+ env: process.env,
178
+ stdio: ['ignore', 'pipe', 'pipe'],
179
+ shell: process.platform === 'win32', // resolve npm.cmd on Windows
180
+ });
181
+ let tail = '';
182
+ const capture = (d) => { tail = (tail + d.toString()).slice(-2000); };
183
+ child.stdout?.on('data', capture);
184
+ child.stderr?.on('data', capture);
185
+ const timer = setTimeout(() => {
186
+ child.kill('SIGKILL');
187
+ reject(new Error('npm install timed out after 180s'));
188
+ }, 180_000);
189
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
190
+ child.on('exit', (code) => {
191
+ clearTimeout(timer);
192
+ if (code === 0)
193
+ resolve();
194
+ else
195
+ reject(new Error(`npm exited ${code}: ${tail.trim().slice(-500)}`));
196
+ });
197
+ });
198
+ }
128
199
  /**
129
200
  * Attach to one daemon: hydrate its sessions/schedules into the aggregator,
130
201
  * THEN open the SSE subscription. Order matters — hydrating after subscribe
@@ -507,12 +578,33 @@ function sanitizeSkillForDashboard(skill) {
507
578
  source: { ...skill.source, url: redactGitUrlCredentials(skill.source.url) },
508
579
  };
509
580
  }
581
+ function dashboardSkillCliIds() {
582
+ const ids = new Set();
583
+ try {
584
+ for (const cliId of configuredCliIds().values())
585
+ ids.add(cliId);
586
+ }
587
+ catch {
588
+ // Fall back to daemon descriptors below when persistent config is unavailable.
589
+ }
590
+ for (const bot of registry.list()) {
591
+ if (bot.cliId)
592
+ ids.add(bot.cliId);
593
+ }
594
+ return [...ids];
595
+ }
510
596
  function dashboardSkillsPayload() {
511
597
  const globalSkills = readGlobalConfig().skills ?? {};
598
+ const nativeSkillGroups = discoverNativeCliSkillGroups(dashboardSkillCliIds())
599
+ .map(group => ({
600
+ ...group,
601
+ skills: group.skills.map(sanitizeSkillForDashboard),
602
+ }));
512
603
  return {
513
604
  skills: Object.values(readSkillRegistry().skills)
514
605
  .sort((a, b) => a.name.localeCompare(b.name))
515
606
  .map(sanitizeSkillForDashboard),
607
+ nativeSkillGroups,
516
608
  trustProjectSkills: globalSkills.trustProjectSkills ?? 'off',
517
609
  delivery: globalSkills.delivery ?? 'auto',
518
610
  };
@@ -774,6 +866,16 @@ const server = createServer(async (req, res) => {
774
866
  mergeGlobalConfig({ repoPickerMode: v });
775
867
  touched = true;
776
868
  }
869
+ if ('whiteboard' in body) {
870
+ const raw = body.whiteboard;
871
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
872
+ return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard' });
873
+ const wb = raw;
874
+ if (typeof wb.enabled !== 'boolean')
875
+ return jsonRes(res, 400, { ok: false, error: 'invalid_whiteboard_enabled' });
876
+ mergeGlobalConfig({ whiteboard: { enabled: wb.enabled } });
877
+ touched = true;
878
+ }
777
879
  if ('maintenance' in body) {
778
880
  const r = parseMaintenancePatch(body.maintenance);
779
881
  if (!r.ok)
@@ -813,6 +915,96 @@ const server = createServer(async (req, res) => {
813
915
  return jsonRes(res, 400, { ok: false, error: 'empty_patch' });
814
916
  return jsonRes(res, 200, { ok: true, settings: resolveDashboardSettings() });
815
917
  }
918
+ // ─── Version & manual update ─────────────────────────────────────────────
919
+ // `npm install -g` and a host restart are privileged: none of these paths
920
+ // are on PUBLIC_READ_PATHS, so decideDashboardAuth already 401s an
921
+ // unauthenticated caller (in both normal and public-read mode). The explicit
922
+ // `authed` guards on the two mutations are defense-in-depth for host actions.
923
+ if (req.method === 'GET' && url.pathname === '/api/update/status') {
924
+ const current = resolveCurrentVersion();
925
+ // Compare against the npm `latest` dist-tag (always stable; the update
926
+ // button installs `@latest`). isNewerVersion uses semver precedence, so a
927
+ // canary running AHEAD of the latest stable (e.g. 2.87.0-canary.0 vs
928
+ // 2.86.0) is NOT flagged behind — exactly the canary case we want.
929
+ const latest = await cachedLatestVersion();
930
+ return jsonRes(res, 200, {
931
+ current,
932
+ latest,
933
+ behind: !!latest && isNewerVersion(latest, current),
934
+ localDevInstall: isLocalDevInstall(),
935
+ node: checkNode(),
936
+ installs: detectBotmuxInstalls(),
937
+ });
938
+ }
939
+ if (req.method === 'GET' && url.pathname === '/api/update/changelog') {
940
+ const current = resolveCurrentVersion();
941
+ const result = await cachedChangelog(current);
942
+ return jsonRes(res, 200, {
943
+ current,
944
+ ok: result.ok,
945
+ rateLimited: result.rateLimited === true,
946
+ releases: result.releases,
947
+ releasesUrl: `https://github.com/${GITHUB_REPO}/releases`,
948
+ });
949
+ }
950
+ if (req.method === 'POST' && url.pathname === '/api/update/run') {
951
+ if (!authed)
952
+ return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
953
+ if (isLocalDevInstall())
954
+ return jsonRes(res, 400, { ok: false, error: 'local_dev_no_update' });
955
+ const node = checkNode();
956
+ if (!node.ok)
957
+ return jsonRes(res, 400, { ok: false, error: 'node_too_old', node });
958
+ if (updateInFlight)
959
+ return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
960
+ updateInFlight = true;
961
+ const oldVersion = botmuxVersion();
962
+ // Acquire the shared cross-process lock so a scheduled maintenance
963
+ // auto-update (running in the bot-0 daemon) can't `npm install -g` at the
964
+ // same time. `acquired` distinguishes "lock held by maintenance" (409)
965
+ // from "npm itself failed" (500). Short wait: don't block the request on a
966
+ // full in-progress install — report busy fast.
967
+ let acquired = false;
968
+ try {
969
+ await withFileLock(npmGlobalUpdateLockTarget(), async () => {
970
+ acquired = true;
971
+ await runNpmInstallLatest();
972
+ }, { maxWaitMs: 2_000 });
973
+ }
974
+ catch (e) {
975
+ if (!acquired)
976
+ return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
977
+ return jsonRes(res, 500, { ok: false, error: 'npm_failed', detail: e instanceof Error ? e.message : String(e) });
978
+ }
979
+ finally {
980
+ updateInFlight = false;
981
+ }
982
+ const newVersion = botmuxVersion();
983
+ return jsonRes(res, 200, { ok: true, oldVersion, newVersion, changed: newVersion !== oldVersion });
984
+ }
985
+ if (req.method === 'POST' && url.pathname === '/api/update/restart') {
986
+ if (!authed)
987
+ return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
988
+ let body = {};
989
+ try {
990
+ const parsed = await readJsonBody(req);
991
+ if (parsed && typeof parsed === 'object')
992
+ body = parsed;
993
+ }
994
+ catch { /* empty / bad body → plain restart */ }
995
+ const upd = body.update && typeof body.update === 'object' ? body.update : null;
996
+ // After a manual update, leave an `update` breadcrumb so the fresh daemon
997
+ // DMs the owner the changelog (reuses the restart-report pipeline). A plain
998
+ // restart leaves none here; cmdRestart writes a `manual` breadcrumb itself.
999
+ if (upd && typeof upd.oldVersion === 'string' && typeof upd.newVersion === 'string' && upd.oldVersion !== upd.newVersion) {
1000
+ try {
1001
+ writeRestartIntent({ kind: 'update', oldVersion: upd.oldVersion, newVersion: upd.newVersion, at: new Date().toISOString() });
1002
+ }
1003
+ catch { /* breadcrumb is best-effort */ }
1004
+ }
1005
+ spawnDetachedRestart('dashboard');
1006
+ return jsonRes(res, 200, { ok: true });
1007
+ }
816
1008
  if (req.method === 'GET' && url.pathname === '/api/skills') {
817
1009
  return jsonRes(res, 200, dashboardSkillsPayload());
818
1010
  }
@@ -865,6 +1057,30 @@ const server = createServer(async (req, res) => {
865
1057
  return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
866
1058
  }
867
1059
  }
1060
+ if (req.method === 'POST' && url.pathname === '/api/skills/install-local-links') {
1061
+ let parsed;
1062
+ try {
1063
+ parsed = await readJsonBody(req);
1064
+ }
1065
+ catch {
1066
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
1067
+ }
1068
+ const sources = parseInstallLocalLinksSources(parsed);
1069
+ if (sources.length === 0)
1070
+ return jsonRes(res, 400, { ok: false, error: 'sources_required' });
1071
+ if (sources.length > MAX_LOCAL_LINK_SOURCES)
1072
+ return jsonRes(res, 400, { ok: false, error: 'too_many_sources' });
1073
+ try {
1074
+ const skills = installLocalSkillLinks(sources);
1075
+ // Frontend re-fetches /api/skills (refresh()) after success, so we keep
1076
+ // the response lean — no need to spread a full dashboardSkillsPayload()
1077
+ // (which would re-run the native-skill discovery scan a second time).
1078
+ return jsonRes(res, 200, { ok: true, installed: skills.map(sanitizeSkillForDashboard) });
1079
+ }
1080
+ catch (err) {
1081
+ return jsonRes(res, 400, { ok: false, error: redactGitUrlCredentials(err?.message ?? String(err)) });
1082
+ }
1083
+ }
868
1084
  let mSkillJob;
869
1085
  if (req.method === 'GET' && (mSkillJob = url.pathname.match(/^\/api\/skills\/jobs\/([^/]+)$/))) {
870
1086
  const job = skillJobs.get(decodeURIComponent(mSkillJob[1]));
@@ -908,6 +1124,28 @@ const server = createServer(async (req, res) => {
908
1124
  ...dashboardSkillsPayload(),
909
1125
  });
910
1126
  }
1127
+ if (req.method === 'GET' && url.pathname === '/api/whiteboards') {
1128
+ return jsonRes(res, 200, { enabled: whiteboardEnabled(), whiteboards: listWhiteboards() });
1129
+ }
1130
+ const mWhiteboard = url.pathname.match(/^\/api\/whiteboards\/([^/]+)$/);
1131
+ if (req.method === 'GET' && mWhiteboard) {
1132
+ try {
1133
+ const id = decodeURIComponent(mWhiteboard[1]);
1134
+ return jsonRes(res, 200, { enabled: whiteboardEnabled(), id, content: readWhiteboard(id, { allowDisabled: true }) });
1135
+ }
1136
+ catch (err) {
1137
+ return jsonRes(res, 404, { ok: false, error: err?.message ?? 'whiteboard_not_found' });
1138
+ }
1139
+ }
1140
+ if (req.method === 'DELETE' && mWhiteboard) {
1141
+ try {
1142
+ const id = decodeURIComponent(mWhiteboard[1]);
1143
+ return jsonRes(res, 200, deleteWhiteboard(id));
1144
+ }
1145
+ catch (err) {
1146
+ return jsonRes(res, 400, { ok: false, error: err?.message ?? 'whiteboard_delete_failed' });
1147
+ }
1148
+ }
911
1149
  if (await handleConnectorApi(req, res, url)) {
912
1150
  return;
913
1151
  }
@@ -1658,6 +1896,24 @@ const server = createServer(async (req, res) => {
1658
1896
  res.end(await upstream.text());
1659
1897
  return;
1660
1898
  }
1899
+ // PUT /api/bots/:appId/env — proxy to that bot's daemon. Body
1900
+ // `{ env: string }` (raw JSON text; '' = clear).
1901
+ let mBotEnv;
1902
+ if (req.method === 'PUT' && (mBotEnv = url.pathname.match(/^\/api\/bots\/([^/]+)\/env$/))) {
1903
+ const appId = decodeURIComponent(mBotEnv[1]);
1904
+ const chunks = [];
1905
+ for await (const c of req)
1906
+ chunks.push(c);
1907
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
1908
+ const upstream = await proxyToDaemon(appId, `/api/bot-env`, {
1909
+ method: 'PUT',
1910
+ headers: { 'content-type': 'application/json' },
1911
+ body: raw,
1912
+ });
1913
+ res.writeHead(upstream.status, { 'content-type': 'application/json' });
1914
+ res.end(await upstream.text());
1915
+ return;
1916
+ }
1661
1917
  // PUT /api/bots/:appId/sandbox — proxy to that bot's daemon. Body `{ enabled: boolean }`.
1662
1918
  let mBotSandbox;
1663
1919
  if (req.method === 'PUT' && (mBotSandbox = url.pathname.match(/^\/api\/bots\/([^/]+)\/sandbox$/))) {