skimpyclaw 0.3.14 → 0.4.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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600;700;800&family=Noto+Sans:wght@400;500;600&display=swap";.sidebar{background:var(--bg-sidebar);display:flex;flex-direction:column;position:sticky;top:0;height:100vh;z-index:10;transition:background .3s ease;overflow-y:auto}.sidebar-header{padding:28px 24px 20px;display:flex;align-items:center;gap:12px}.sidebar-logo{width:40px;height:40px;background:transparent;border-radius:0;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:none;flex-shrink:0}.sidebar-brand{display:flex;flex-direction:column}.sidebar-close-btn{display:none;margin-left:auto;width:30px;height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text-dim);align-items:center;justify-content:center;cursor:pointer}.sidebar-close-btn:hover{background:var(--surface-hover);color:var(--text)}.sidebar-brand-name{font-family:var(--serif);font-size:17px;font-weight:800;color:var(--text);letter-spacing:-.01em}.sidebar-brand-status{font-size:11px;color:var(--success);display:flex;align-items:center;gap:5px;font-weight:500}.sidebar-brand-status:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--success);display:inline-block}.sidebar-section{padding:0 12px;margin-bottom:8px}.sidebar-section-label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;padding:16px 12px 8px}.sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text-dim);font-size:14px;font-weight:500;transition:all .15s ease;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans);position:relative}.sidebar-item:hover{background:var(--sidebar-hover);color:var(--text)}.sidebar-item.active{background:var(--sidebar-active);color:var(--text-dim);font-weight:600;box-shadow:var(--shadow-sm)}.sidebar-item.active:before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:0 3px 3px 0;background:var(--accent)}.sidebar-item svg{width:20px;height:20px;flex-shrink:0;opacity:.6}.sidebar-item.active svg{opacity:1}.sidebar-item:hover svg{opacity:.85}.sidebar-item-icon{display:inline-flex;align-items:center;justify-content:center;line-height:0}.sidebar-item.active .sidebar-item-icon{color:var(--accent)}.sidebar-item-label{display:inline-flex;align-items:center;line-height:1.2}.sidebar-item .item-badge{margin-left:auto;font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--warning-soft);color:var(--warning)}.sidebar-spacer{flex:1}.sidebar-footer{padding:16px 12px;border-top:1px solid var(--border-light)}.sidebar-footer-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:500;transition:all .15s ease;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}.sidebar-footer-item:hover{background:var(--sidebar-hover);color:var(--text-dim)}.sidebar-footer-item.active{background:var(--sidebar-active);color:var(--text-dim);font-weight:600}.sidebar-footer-item svg{width:18px;height:18px;flex-shrink:0;opacity:.5}.sidebar-footer-item.active .sidebar-item-icon{color:var(--accent)}@media (max-width: 980px){.sidebar-close-btn{display:inline-flex}}:root,:root[data-theme=light],[data-theme=light]{--text: #0c0303;--background: #fffaf5;--text-dim: #5a4a40;--text-muted: #9a8a80;--bg: var(--background);--bg-sidebar: var(--background);--surface: #ffffff;--surface-alt: #f5ebe5;--surface-hover: #ebe0d8;--primary: #d33b36;--primary-hover: #c65a24;--primary-soft: rgba(219, 111, 57, .16);--primary-soft-2: rgba(219, 111, 57, .08);--secondary: #f2ded2;--secondary-soft: rgba(242, 222, 210, .22);--accent: #dfaa6d;--accent-hover: #e07030;--accent-soft: rgba(243, 134, 70, .16);--accent-soft-2: rgba(243, 134, 70, .08);--success: #5a9b5a;--success-soft: rgba(90, 155, 90, .16);--warning: #c9a43c;--warning-soft: rgba(201, 164, 60, .12);--error: #d94a4a;--error-soft: rgba(217, 74, 74, .12);--border: rgba(23, 16, 11, .08);--border-light: rgba(23, 16, 11, .05);--shadow-sm: 0 1px 2px rgba(23, 16, 11, .04);--shadow: 0 2px 8px rgba(23, 16, 11, .06);--shadow-md: 0 4px 16px rgba(23, 16, 11, .08);--sidebar-active: #ffffff;--sidebar-hover: rgba(23, 16, 11, .04);--welcome-from: #db6f39;--welcome-to: #f38646;--welcome-shadow: rgba(219, 111, 57, .3);--welcome-text: #ffffff;--log-info: #5a4a40;--log-warn: #b08a1a;--log-error: #d94a4a;--highlight: #f38646;--info: #7bb8f9;--info-soft: rgba(123, 184, 249, .12);--chip-telegram: #4a7a99;--chip-telegram-bg: rgba(109, 166, 214, .13);--chip-discord: #5a5a99;--chip-discord-bg: rgba(130, 146, 235, .14);--chip-cron: #9a7f3e;--chip-cron-bg: rgba(203, 169, 93, .14);--pending: #7a8a91;--pending-soft: rgba(142, 154, 166, .1);--mono: "JetBrains Mono", "SF Mono", "Fira Code", monospace;--sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--serif: "Playfair Display", Georgia, "Times New Roman", serif;--radius: 16px;--radius-md: 12px;--radius-sm: 8px;--sidebar-width: 260px}:root[data-theme=dark],[data-theme=dark]{--text: #fcf3f3;--background: #121212;--text-dim: #d4ccc6;--text-muted: #948c86;--bg: var(--background);--bg-sidebar: var(--background);--surface: #1a120c;--surface-alt: #251c14;--surface-hover: #30261c;--primary: #c9312c;--primary-hover: #db6f39;--primary-soft: rgba(198, 90, 36, .2);--primary-soft-2: rgba(198, 90, 36, .1);--secondary: #f2ded2;--secondary-soft: rgba(242, 222, 210, .22);--accent: #dfaa6d;--accent-hover: #d65a1a;--accent-soft: rgba(187, 76, 12, .2);--accent-soft-2: rgba(187, 76, 12, .1);--success: #5ab85a;--success-soft: rgba(90, 184, 90, .18);--warning: #d4b45a;--warning-soft: rgba(212, 180, 90, .14);--error: #e07a6a;--error-soft: rgba(224, 122, 106, .14);--border: rgba(243, 236, 231, .08);--border-light: rgba(243, 236, 231, .05);--shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);--shadow: 0 2px 8px rgba(0, 0, 0, .35);--shadow-md: 0 4px 16px rgba(0, 0, 0, .4);--sidebar-active: rgba(198, 90, 36, .2);--sidebar-hover: rgba(243, 236, 231, .04);--welcome-from: #c65a24;--welcome-to: #e07a40;--welcome-shadow: rgba(198, 90, 36, .4);--welcome-text: #f3ece7;--log-info: #d4ccc6;--log-warn: #d4b45a;--log-error: #e07a6a;--highlight: #bb4c0c;--info: #7ab8f9;--info-soft: rgba(122, 184, 249, .14);--chip-telegram: #8eb8e0;--chip-telegram-bg: rgba(126, 184, 224, .16);--chip-discord: #9e9eed;--chip-discord-bg: rgba(158, 158, 237, .16);--chip-cron: #d4b45a;--chip-cron-bg: rgba(212, 180, 90, .16);--pending: #9ba5b0;--pending-soft: rgba(155, 165, 176, .14)}*{margin:0;padding:0;box-sizing:border-box}html{font-size:100%}body{font-family:"Noto Sans",var(--sans);font-weight:400;background:var(--bg);color:var(--text);min-height:100vh;font-size:14px;line-height:1.55;transition:background .3s ease,color .3s ease;-webkit-font-smoothing:antialiased}h1,h2,h3,h4,h5{font-family:var(--serif);font-weight:700}h1{font-size:4.21rem}h2{font-size:3.158rem}h3{font-size:2.369rem}h4{font-size:1.777rem}h5{font-size:1.333rem}small{font-size:.75rem}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}.shell{display:grid;grid-template-columns:var(--sidebar-width) 1fr;min-height:100vh}.main{position:relative;overflow-y:auto;padding:32px 36px}.btn{padding:8px 16px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font-family:var(--sans);font-weight:500;font-size:13px;cursor:pointer;transition:all .12s ease}.btn:hover{background:var(--surface-hover)}.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent-hover)}.btn-success{background:var(--success);border-color:var(--success);color:#fff}.btn-success:hover{opacity:.9}.btn-danger{background:var(--error-soft);border-color:#ef444433;color:var(--error)}.btn-danger:hover{background:#ef44441f}.btn-sm,.btn-small{padding:5px 12px;font-size:12px}.btn-refresh{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border:1px solid var(--border-light);border-radius:999px;background:var(--surface-alt);color:var(--text-dim);font-family:var(--sans);font-weight:500;font-size:13px;cursor:pointer;transition:all .15s ease;box-shadow:var(--shadow-sm)}.btn-refresh:hover{background:var(--surface-hover);border-color:var(--border);color:var(--text);box-shadow:var(--shadow)}.btn-refresh:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.btn-refresh:active{transform:scale(.98)}input:not([type=checkbox]):not([type=radio]),select,textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-alt);color:var(--text);font-family:var(--sans);font-size:13px;line-height:1.5;outline:none;transition:border-color .12s ease,box-shadow .12s ease,background .12s ease}textarea{resize:vertical;min-height:90px}input:not([type=checkbox]):not([type=radio]):focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--surface)}input::placeholder,textarea::placeholder{color:var(--text-muted)}.form-grid{display:grid;gap:10px}.form-grid.two{grid-template-columns:1fr 1fr}.form-grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.form-field{display:flex;flex-direction:column;gap:6px}.form-label{font-size:12px;color:var(--text-dim);font-weight:600;letter-spacing:.01em}.form-help{font-size:11px;color:var(--text-muted)}.form-actions{display:flex;gap:8px;align-items:center;margin-top:12px}.form-checkbox{display:flex;align-items:center;gap:10px;font-size:13px;line-height:1.35;color:var(--text-dim);cursor:pointer;-webkit-user-select:none;user-select:none}.form-checkbox input[type=checkbox]{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:18px;height:18px;flex:0 0 18px;border:1px solid var(--border);border-radius:5px;background:var(--surface);display:inline-grid;place-content:center;margin:0;cursor:pointer;transition:border-color .12s ease,box-shadow .12s ease,background .12s ease}.form-checkbox input[type=checkbox]:before{content:"";width:10px;height:10px;transform:scale(0);transition:transform .1s ease-in-out;background:#fff;clip-path:polygon(14% 44%,0 63%,44% 100%,100% 18%,82% 0,40% 64%)}.form-checkbox input[type=checkbox]:checked{border-color:var(--accent);background:var(--accent)}.form-checkbox input[type=checkbox]:checked:before{transform:scale(1)}.form-checkbox input[type=checkbox]:focus-visible{outline:none;box-shadow:0 0 0 3px var(--accent-soft)}.form-check-row{display:flex;gap:20px;align-items:center;flex-wrap:wrap}.form-check-row.tight{gap:12px}.form-check-col{display:flex;flex-direction:column;gap:6px}.form-divider{height:1px;background:var(--border-light);margin:12px 0}.toast-container{position:fixed;bottom:24px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}.toast{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 18px;font-size:13px;box-shadow:var(--shadow-md);animation:toast-in .2s ease}.toast.success{border-left:3px solid var(--success)}.toast.error{border-left:3px solid var(--error)}.toast.warning{border-left:3px solid var(--warning)}@keyframes toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.section{margin-bottom:28px}.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.section-title{font-family:var(--serif);font-size:18px;font-weight:800;color:var(--text);letter-spacing:-.01em}.section-link{font-size:13px;color:var(--accent);cursor:pointer;text-decoration:none;font-weight:500}.section-link:hover{text-decoration:underline}.section-link-content{display:inline-flex;align-items:center;gap:6px}.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:28px}.page-title{font-family:var(--serif);font-size:26px;font-weight:800;letter-spacing:-.02em;color:var(--text)}.header-actions{display:flex;gap:10px;align-items:center;flex-wrap:nowrap}.page-header .header-actions{margin-right:72px}.header-meta{font-size:12px;color:var(--text-muted);font-family:var(--mono)}.theme-toggle{width:36px;height:36px;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius-sm);cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;transition:all .15s ease;color:var(--text)}.theme-toggle:hover{background:var(--surface-hover)}.global-theme-toggle{position:absolute;top:32px;right:36px;width:37px;height:37px;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease;box-shadow:var(--shadow-sm);color:var(--text);z-index:100}.global-theme-toggle:hover{background:var(--surface-hover);box-shadow:var(--shadow)}.global-sidebar-toggle,.sidebar-backdrop{display:none}.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:10px}.cards-link-row{display:flex;justify-content:flex-end;margin-bottom:22px}.stat-card{background:var(--surface);border-radius:var(--radius);padding:22px 24px;box-shadow:var(--shadow-sm);transition:all .2s ease;border:1px solid var(--border-light);display:flex;align-items:center;gap:16px;min-height:138px}.stat-card:hover{box-shadow:var(--shadow);transform:translateY(-1px)}.stat-icon{width:48px;height:48px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:18px;flex:0 0 auto}.stat-icon svg{width:20px;height:20px}.stat-icon.sage{background:var(--accent-soft);color:var(--accent)}.stat-icon.green{background:var(--success-soft);color:var(--success)}.stat-icon.amber{background:var(--warning-soft);color:var(--warning)}.stat-icon.blue{background:var(--info-soft);color:var(--info)}.stat-icon.rose{background:#f43f5e14;color:#f43f5e}.stat-icon.muted{background:var(--surface-alt);color:var(--text-muted)}.stats-grid-5{grid-template-columns:repeat(5,1fr)}.stats-grid-5 .stat-card{min-height:120px;padding:18px 20px;gap:12px}.stats-grid-5 .stat-icon{width:42px;height:42px}.stats-grid-5 .stat-icon svg{width:18px;height:18px}.stats-grid-5 .stat-value{font-size:26px}.stats-grid-5 .stat-value-md{font-size:18px}.stats-grid-6{grid-template-columns:repeat(6,1fr)}.stats-grid-6 .stat-card{min-height:120px;padding:16px 18px;gap:10px}.stats-grid-6 .stat-icon{width:38px;height:38px}.stats-grid-6 .stat-icon svg{width:16px;height:16px}.stats-grid-6 .stat-value{font-size:24px}.stats-grid-6 .stat-value-md{font-size:16px}.stat-body{display:flex;flex-direction:column;justify-content:center;min-width:0}.stat-value{font-family:var(--serif);font-size:32px;font-weight:700;line-height:1.02;letter-spacing:-.02em}.stat-value-md{font-size:22px}.stat-subtitle{font-size:11px;color:var(--text-dim);font-weight:500;line-height:1.2}.feed-card{background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-sm);border:1px solid var(--border-light);overflow:hidden}.feed{display:flex;flex-direction:column}.feed-item{display:grid;grid-template-columns:40px 1fr auto;gap:14px;align-items:start;padding:16px 20px;transition:background .12s ease;border-bottom:1px solid var(--border-light)}.feed-item:last-child{border-bottom:none}.feed-item:hover{background:var(--surface-alt)}.feed-icon{width:40px;height:40px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}.feed-icon.telegram{background:var(--accent-soft);color:var(--accent)}.feed-icon.cron{background:var(--warning-soft);color:var(--warning)}.feed-icon.system{background:var(--surface-alt);color:var(--text-muted)}.feed-icon.code{background:var(--accent-soft);color:var(--accent)}.feed-title{font-weight:600;font-size:14px;margin-bottom:3px}.feed-detail{font-size:13px;color:var(--text-dim);line-height:1.45}.feed-time{font-size:11px;color:var(--text-muted);font-family:var(--mono);white-space:nowrap;padding-top:2px}.cron-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.cron-card{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:20px 22px;display:flex;justify-content:space-between;align-items:center;box-shadow:var(--shadow-sm);transition:all .15s ease}.cron-card:hover{box-shadow:var(--shadow);transform:translateY(-1px)}.cron-info{flex:1}.cron-name{font-weight:600;font-size:14px;margin-bottom:4px;color:var(--text)}.cron-schedule{font-size:12px;color:var(--text-dim);font-family:var(--mono)}.cron-next{font-size:12px;color:var(--success);font-family:var(--mono);text-align:right;font-weight:500}.welcome-banner{background:linear-gradient(135deg,var(--welcome-from),var(--welcome-to));border-radius:var(--radius);padding:28px 32px;margin-bottom:28px;color:var(--welcome-text);position:relative;overflow:hidden}.welcome-banner:after{content:"";position:absolute;right:-20px;top:-20px;width:200px;height:200px;border-radius:50%;background:#ffffff14}.welcome-banner:before{content:"";position:absolute;right:80px;bottom:-40px;width:120px;height:120px;border-radius:50%;background:#ffffff0d}.welcome-greeting{font-family:var(--serif);font-size:24px;font-weight:800;margin-bottom:6px;letter-spacing:-.01em;position:relative;z-index:1}.welcome-sub{font-size:14px;opacity:.85;font-weight:400;position:relative;z-index:1}.approval-banner{background:var(--warning-soft);border:1px solid rgba(245,158,11,.15);border-radius:var(--radius);padding:18px 22px;margin-bottom:24px;display:flex;align-items:center;gap:16px}.approval-banner-icon{font-size:22px;flex-shrink:0}.approval-banner-content{flex:1}.approval-banner-title{font-weight:600;font-size:14px;margin-bottom:3px}.approval-banner-detail{font-size:13px;color:var(--text-dim);font-family:var(--mono)}.approval-banner-actions{display:flex;gap:8px}.approval-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 24px;margin-bottom:10px;display:flex;align-items:center;gap:16px;box-shadow:var(--shadow-sm)}.approval-card-icon{width:44px;height:44px;border-radius:var(--radius-sm);background:var(--warning-soft);color:var(--warning);display:flex;align-items:center;justify-content:center;flex-shrink:0}.approval-card-content{flex:1;min-width:0}.approval-card-title{font-weight:700;font-size:15px;color:var(--text);margin-bottom:4px}.approval-card-detail{font-size:13px;color:var(--text-dim);display:flex;align-items:center;gap:6px;flex-wrap:wrap}.approval-cmd{font-family:var(--mono);font-size:12px;background:var(--surface-alt);border:1px solid var(--border);border-radius:4px;padding:2px 8px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:480px}.approval-card-cwd{font-size:11px;color:var(--text-muted);font-family:var(--mono);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.approval-card-time{font-family:var(--mono);font-size:14px;color:var(--warning);font-weight:600;flex-shrink:0;min-width:40px;text-align:right}.approval-card-actions{display:flex;gap:8px;flex-shrink:0}.approval-btn-deny{padding:8px 18px;font-size:13px;font-weight:600;border-radius:var(--radius-sm);border:1.5px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer;transition:all .12s ease}.approval-btn-deny:hover{background:var(--surface-alt);border-color:var(--text-muted)}.approval-btn-deny:disabled{opacity:.5;cursor:not-allowed}.approval-btn-approve{padding:8px 20px;font-size:13px;font-weight:600;border-radius:var(--radius-sm);border:none;background:var(--accent);color:#fff;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .12s ease}.approval-btn-approve:hover{background:var(--accent-hover)}.approval-btn-approve:disabled{opacity:.5;cursor:not-allowed}.approvals-page{display:flex;flex-direction:column;gap:0}.appr-auto-refresh{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:12px;color:var(--text-muted);border:1px solid var(--border);border-radius:999px;padding:5px 14px}.appr-auto-dot{width:7px;height:7px;border-radius:50%;background:var(--success);display:inline-block}.appr-section-label{font-size:11px;font-weight:700;letter-spacing:.06em;color:var(--text-muted);text-transform:uppercase;margin-bottom:12px;margin-top:8px}.appr-list{display:flex;flex-direction:column;gap:14px}.appr-item{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}.appr-item-border{width:4px;flex-shrink:0}.appr-item-border.pending{background:var(--warning)}.appr-item-border.approved{background:var(--success)}.appr-item-border.denied{background:var(--error)}.appr-item-border.expired{background:var(--text-muted)}.appr-item-body{flex:1;padding:16px 20px;display:flex;flex-direction:column;gap:10px;min-width:0}.appr-item-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.appr-item-icon{width:36px;height:36px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0}.appr-item-icon.pending{background:var(--warning-soft);color:var(--warning)}.appr-item-icon.approved{background:var(--success-soft);color:var(--success)}.appr-item-icon.denied{background:var(--error-soft);color:var(--error)}.appr-item-icon.expired{background:var(--surface-alt);color:var(--text-muted)}.appr-item-id{font-family:var(--mono);font-weight:600;font-size:14px;color:var(--text)}.appr-chip{display:inline-block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.04em;padding:2px 8px;border-radius:4px;text-transform:uppercase}.appr-chip.tier{background:var(--surface-alt);color:var(--text-dim)}.appr-chip.tier.tier-1{background:var(--info-soft);color:var(--info)}.appr-chip.tier.tier-2{background:var(--warning-soft);color:var(--warning)}.appr-chip.tier.tier-3{background:var(--error-soft);color:var(--error)}.appr-chip.pending{background:var(--warning-soft);color:var(--warning)}.appr-chip.approved{background:var(--success-soft);color:var(--success)}.appr-chip.denied{background:var(--error-soft);color:var(--error)}.appr-chip.expired{background:var(--surface-alt);color:var(--text-muted)}.appr-tier-legend{margin-top:10px;display:grid;grid-template-columns:auto 1fr;gap:6px 10px;align-items:center}.appr-tier-label{font-size:12px;color:var(--text-dim)}.appr-elapsed{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:13px;color:var(--warning);font-weight:600}.appr-actions{display:flex;gap:8px}.appr-reason{font-size:14px;color:var(--text-dim)}.appr-cmd-block{font-family:var(--mono);font-size:13px;background:var(--surface-alt);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 14px;color:var(--text);overflow-x:auto;white-space:pre}.appr-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--text-muted)}.appr-footer-item{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono)}.appr-footer-sep{color:var(--border)}.appr-footer-resolution{font-style:italic;color:var(--text-muted)}.spinner{width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:48px 24px;color:var(--text-muted)}.empty-state-icon{font-size:36px;margin-bottom:12px}.empty-state-text{font-size:14px}.markdown-view{font-size:14px;line-height:1.55;color:var(--text-dim)}.markdown-view h1,.markdown-view h2,.markdown-view h3,.markdown-view h4,.markdown-view h5,.markdown-view h6{font-family:var(--serif);color:var(--text);font-weight:800;margin:0 0 8px}.markdown-view p{margin:0 0 7px}.markdown-view ul,.markdown-view ol{padding-left:20px;margin:0 0 7px}.markdown-view code{font-family:var(--mono);background:var(--surface-alt);padding:1px 4px;border-radius:4px;font-size:12px}.markdown-view pre{margin:0 0 9px;background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:12px;overflow:auto}.markdown-view pre code{background:transparent;padding:0}.markdown-view blockquote{border-left:3px solid var(--accent);padding-left:10px;margin:0 0 9px;color:var(--text-muted)}.chat-list{display:flex;flex-direction:column;gap:10px}.chat-row{display:flex;flex-direction:column}.chat-row.user{align-items:flex-end}.chat-row.assistant{align-items:flex-start}.chat-meta{font-size:11px;color:var(--text-muted);font-family:var(--mono);margin-bottom:4px}.chat-bubble{max-width:82%;padding:10px 12px;border-radius:12px;font-size:14px;line-height:1.5;border:1px solid var(--border-light)}.chat-bubble.user{background:var(--accent-soft);color:var(--text)}.chat-bubble.assistant{background:var(--surface-alt);color:var(--text-dim)}.chat-bubble .markdown-view p:last-child,.chat-bubble .markdown-view ul:last-child,.chat-bubble .markdown-view ol:last-child,.chat-bubble .markdown-view pre:last-child,.chat-bubble .markdown-view blockquote:last-child{margin-bottom:0}.messages-page{height:calc(100vh - 118px);display:flex;flex-direction:column}.messages-chat-card{flex:1;height:100%;min-height:0;display:flex;flex-direction:column;margin-bottom:0}.messages-chat-list{flex:1;min-height:0;overflow-y:auto;padding-right:4px;padding-bottom:8px;-ms-overflow-style:none;scrollbar-width:none}.messages-chat-list::-webkit-scrollbar{width:0;height:0;display:none}.messages-composer-inline{margin-top:auto;padding-top:12px;border-top:1px solid var(--border-light)}.messages-filler{flex:1;display:flex;align-items:center;justify-content:center}.card{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:20px 22px;margin-bottom:14px;box-shadow:var(--shadow-sm)}.card-title{font-size:14px;color:var(--text);margin-bottom:8px;font-weight:500}.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}.agents-layout{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:16px;align-items:start}.agents-list{display:grid;gap:8px}.agent-profile-item{width:100%;display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:10px;padding:10px;border:1px solid var(--border-light);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);cursor:pointer;text-align:left}.agent-profile-item:hover,.agent-profile-item.active{border-color:var(--accent);background:var(--surface-hover)}.agent-profile-icon{width:34px;height:34px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;background:var(--accent-soft);color:var(--accent);font-size:14px;font-weight:700}.agent-profile-main{min-width:0;display:grid;gap:2px}.agent-profile-alias{font-weight:700;color:var(--text)}.agent-profile-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-family:var(--mono);font-size:11px}.agent-profile-count{min-width:22px;height:22px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:var(--surface-alt);color:var(--text-muted);font-family:var(--mono);font-size:11px}.agents-empty{border:1px solid var(--border-light);border-radius:var(--radius);background:var(--surface)}.agents-editor{margin-bottom:0}.agents-editor-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}.agent-editor-subtitle{color:var(--text-muted);font-size:12px}.agent-inherit-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:10px 0 14px;color:var(--text-dim);font-size:12px}.agent-inherit-row code{padding:3px 7px;border-radius:999px;background:var(--surface-alt);color:var(--text-muted);font-family:var(--mono);font-size:11px}.agent-model-aliases{display:flex;gap:6px;flex-wrap:wrap;margin:10px 0 14px}.agents-prompt{min-height:220px;font-family:var(--mono)}.split{display:grid;grid-template-columns:320px 1fr;gap:18px;height:calc(100vh - 180px)}.split-list{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);overflow-y:auto;box-shadow:var(--shadow-sm)}.split-detail{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);overflow-y:auto;padding:24px;box-shadow:var(--shadow-sm)}.list-item{padding:16px 20px;border-bottom:1px solid var(--border-light);cursor:pointer;transition:background .12s ease}.list-item:last-child{border-bottom:none}.list-item:hover{background:var(--surface-alt)}.list-item.active{background:var(--accent-soft-2);border-left:3px solid var(--accent)}.list-item-title{font-weight:600;font-size:14px;margin-bottom:3px;color:var(--text)}.list-item-sub,.list-item-meta{font-size:12px;color:var(--text-dim);font-family:var(--mono)}.audit-entry{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:16px 18px;margin-bottom:10px}.audit-header{display:flex;justify-content:space-between;align-items:start;gap:10px;margin-bottom:8px}.audit-id{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600}.audit-meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-muted);font-family:var(--mono)}.audit-summary{font-size:13px;color:var(--text-dim);line-height:1.5}.audit-page{display:flex;flex-direction:column;gap:12px}.audit-header-meta{font-family:var(--mono);font-size:11px;color:var(--text-muted)}.audit-top-stats{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:10px 14px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.audit-stat-left{display:flex;align-items:baseline;gap:8px}.audit-big{font-family:var(--serif);font-size:20px;line-height:1}.audit-big.ok{color:var(--success)}.audit-big.error{color:var(--error)}.audit-big.running{color:var(--warning)}.audit-stat-label{font-size:11px;color:var(--text-muted);font-weight:600}.audit-stat-triggers{display:flex;gap:8px;flex-wrap:wrap}.audit-filters-row{display:flex;flex-direction:column;gap:8px;align-items:stretch}.audit-search{width:100%;min-width:0;max-width:none;display:inline-flex;align-items:center;gap:8px;border:1px solid var(--border-light);background:var(--surface);color:var(--text-muted);border-radius:14px;padding:10px 14px;min-height:56px}.audit-search input{border:none;background:transparent;color:var(--text);width:100%;min-width:120px;font-size:11px;font-weight:500}.audit-search input:focus{outline:none}.audit-search svg{width:20px;height:20px}.audit-filter-controls{display:flex;gap:8px;align-items:center}.audit-filter-controls>.btn.btn-sm{min-height:32px;height:32px;min-width:160px;width:auto;border-radius:10px;font-size:12px;font-weight:500;padding:5px 12px}.audit-timeline{display:flex;flex-direction:column;gap:8px}.audit-row{display:grid;grid-template-columns:56px 1fr;gap:12px}.audit-time-col{display:flex;flex-direction:column;align-items:center;color:var(--text-muted)}.audit-time{font-family:var(--mono);font-size:10px;margin-top:12px}.audit-time-line{width:1px;flex:1;min-height:40px;background:var(--border);margin-top:8px}.audit-card{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:12px 14px}.audit-card-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:6px}.audit-chip-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}.audit-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border-light);border-radius:999px;padding:2px 8px;font-size:9px;line-height:1.4;font-family:var(--mono);color:var(--text-muted);text-transform:uppercase;letter-spacing:.01em}.audit-chip strong{color:var(--text-dim);font-weight:600}.audit-chip.muted{background:var(--surface-alt)}.audit-chip.id{text-transform:none;letter-spacing:0}.audit-chip.trigger.telegram{background:var(--chip-telegram-bg);color:var(--chip-telegram);border-color:transparent}.audit-chip.trigger.discord{background:var(--chip-discord-bg);color:var(--chip-discord);border-color:transparent}.audit-chip.trigger.cron{background:var(--chip-cron-bg);color:var(--chip-cron);border-color:transparent}.audit-chip.trigger.system,.audit-chip.trigger.gateway,.audit-chip.trigger.dashboard{background:var(--surface-alt);border-color:transparent;color:var(--text-dim)}.audit-chip.status.success{background:var(--success-soft);color:var(--success);border-color:transparent}.audit-chip.status.error{background:var(--error-soft);color:var(--error);border-color:transparent}.audit-chip.status.running{background:var(--warning-soft);color:var(--warning);border-color:transparent}.audit-chip.soft{background:var(--surface-alt);text-transform:none}.audit-duration{font-family:var(--mono);font-size:10px;color:var(--text-muted);white-space:nowrap}.audit-title-line{font-size:14px;line-height:1.35;color:var(--text-dim);margin-bottom:8px}.audit-event-pills{display:flex;gap:6px;flex-wrap:wrap}.audit-load-more-wrap{text-align:center;margin-top:8px}.audit-card-expandable:hover{border-color:var(--border)}.audit-card-head-right{display:flex;align-items:center;gap:6px}.audit-expand-icon{color:var(--text-muted);display:flex;align-items:center}.audit-detail-section{margin-top:10px;border-top:1px solid var(--border-light);padding-top:8px;display:flex;flex-direction:column;gap:0}.audit-event-row{display:grid;grid-template-columns:90px 1fr auto;gap:8px;align-items:baseline;padding:5px 2px;border-bottom:1px solid var(--border-light)}.audit-event-row:last-child{border-bottom:none}.audit-event-type-label{font-family:var(--mono);font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.audit-event-type-label.tool{color:var(--accent, #7c6af0)}.audit-event-content{font-size:12px;color:var(--text-dim);line-height:1.4;word-break:break-word;overflow:hidden}.audit-event-content.mono{font-family:var(--mono);font-size:11px}.audit-event-dur{font-family:var(--mono);font-size:10px;color:var(--text-muted);white-space:nowrap;text-align:right}.audit-event-detail{display:flex;flex-direction:column;gap:4px}.audit-event-detail-label{font-family:var(--mono);font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}.audit-json-block{background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:10px 12px;font-family:var(--mono);font-size:11px;line-height:1.6;color:var(--text-dim);white-space:pre;overflow-x:auto;-moz-tab-size:2;tab-size:2}.audit-kv-list{display:flex;flex-direction:column;gap:2px;margin:0;padding:8px 10px;background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm)}.audit-kv-row{display:grid;grid-template-columns:minmax(80px,28%) 1fr;gap:8px;align-items:baseline;min-height:20px}.audit-kv-row dt,.audit-kv-key{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.audit-kv-row dd,.audit-kv-val{font-family:var(--mono);font-size:11px;color:var(--text-dim);margin:0;word-break:break-word}.audit-kv-inline{font-family:var(--mono);font-size:11px;color:var(--text-muted);background:transparent;padding:0;white-space:pre-wrap;word-break:break-all}.audit-detail-text{font-family:var(--mono);font-size:11px;line-height:1.6;color:var(--text-dim);padding:8px 12px;background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm);white-space:pre-wrap;word-break:break-word;display:block}@media (max-width: 900px){.audit-row{grid-template-columns:1fr}.audit-time-col{align-items:flex-start;flex-direction:row;gap:8px}.audit-time-line{display:none}.audit-filter-controls{flex-direction:column;align-items:stretch}.audit-filter-controls>.btn.btn-sm{width:100%}}.coding-page{--coding-accent: #ad5038;--coding-accent-soft: rgba(173, 80, 56, .08)}.coding-page .page-header{margin-bottom:18px}.coding-refresh-pill{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border:1px solid var(--border-light);border-radius:999px;background:var(--surface-alt);color:var(--text-dim);font-size:13px;font-family:var(--sans);font-weight:500;box-shadow:var(--shadow-sm);line-height:1}.coding-refresh-pill .dot{width:6px;height:6px;border-radius:50%;background:var(--success)}.coding-page .btn{display:inline-flex;align-items:center;gap:6px}.coding-body{display:flex;flex-direction:column;gap:14px}.coding-stats-grid{display:flex;gap:12px;margin-bottom:6px;flex-wrap:wrap}.coding-stat-card{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:14px 20px;display:flex;align-items:center;gap:10px;min-width:140px;flex:1}.coding-stat-icon{width:38px;height:38px;border-radius:var(--radius-sm);display:inline-flex;align-items:center;justify-content:center}.coding-stat-icon svg{width:18px;height:18px}.coding-stat-content{display:flex;flex-direction:column;justify-content:center;gap:0;min-width:0}.coding-stat-icon.running{background:var(--coding-accent-soft);color:var(--coding-accent)}.coding-stat-icon.completed{background:var(--success-soft);color:var(--success)}.coding-stat-icon.failed{background:var(--error-soft);color:var(--error)}.coding-stat-icon.cost{background:var(--warning-soft);color:var(--warning)}.coding-stat-value{font-family:var(--serif);font-size:18px;font-weight:500;line-height:1;color:var(--text)}.coding-stat-label{font-size:10px;color:var(--text-muted);font-weight:400;margin-top:2px;line-height:1.1}.coding-task-card{background:var(--surface);border:1px solid var(--border-light);border-top-width:3px;border-top-color:var(--card-accent, var(--coding-accent));border-radius:var(--radius);padding:0;overflow:hidden;box-shadow:var(--shadow-sm)}.coding-task-main{padding:18px 22px 0;display:grid;grid-template-columns:36px 1fr auto;gap:14px;align-items:start}.coding-task-icon{width:36px;height:36px;border-radius:var(--radius-sm);display:inline-flex;align-items:center;justify-content:center;margin-top:2px}.coding-task-icon.running{background:var(--coding-accent-soft);color:var(--coding-accent)}.coding-task-card.running .coding-task-icon.running{background:var(--card-accent-soft, var(--coding-accent-soft));color:var(--card-accent, var(--coding-accent))}.coding-task-icon.completed{background:var(--success-soft);color:var(--success)}.coding-task-icon.failed{background:var(--error-soft);color:var(--error)}.coding-task-icon.pending{background:var(--pending-soft);color:var(--pending)}.coding-task-content{min-width:0}.coding-task-pills{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px}.coding-pill{display:inline-flex;align-items:center;border:1px solid var(--border-light);background:var(--surface-alt);color:var(--text-muted);border-radius:999px;padding:2px 8px;font-size:9px;text-transform:uppercase;letter-spacing:.04em;font-family:var(--sans);font-weight:500}.coding-pill.id{border:none;background:transparent;color:var(--card-accent, var(--coding-accent));padding:0;font-size:12px;letter-spacing:0;text-transform:none;font-family:var(--mono);font-weight:500}.coding-pill.model,.coding-pill.effort{text-transform:none;letter-spacing:0;font-family:var(--mono);max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coding-pill.effort,.coding-pill.status.running{color:var(--card-accent, var(--coding-accent));background:var(--card-accent-soft, var(--coding-accent-soft));border-color:transparent}.coding-pill.status.completed{color:var(--success);background:var(--success-soft);border-color:transparent}.coding-pill.status.failed{color:var(--error);background:var(--error-soft);border-color:transparent}.coding-pill.status.pending{color:var(--pending)}.coding-task-title{margin-top:0;font-size:13px;color:var(--text);font-weight:400;line-height:1.5}.coding-task-submeta{padding:10px 0 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;font-size:11px;color:var(--text-muted);font-family:var(--mono)}.coding-task-submeta span{display:inline-flex;align-items:center;gap:6px}.coding-task-action{align-self:flex-start;border:1px solid transparent;background:transparent;color:var(--text-dim);border-radius:999px;font-size:11px;font-weight:500;padding:6px 14px;display:inline-flex;align-items:center;gap:6px}.coding-task-action.running{color:var(--card-accent, var(--coding-accent));border-color:color-mix(in srgb,var(--card-accent, var(--coding-accent)) 25%,transparent);background:color-mix(in srgb,var(--card-accent, var(--coding-accent)) 8%,transparent)}.coding-task-action.failed{color:var(--error);border-color:#c254504d;background:#c254500a}.coding-task-progress{height:1px;background:var(--border-light)}.coding-task-progress.running{background:linear-gradient(to right,var(--card-accent, var(--coding-accent)) 56%,var(--border-light) 56%)}.coding-task-progress.failed{background:linear-gradient(to right,var(--error) 32%,var(--border-light) 32%)}.coding-task-progress.completed{background:linear-gradient(to right,var(--success) 100%,var(--success) 100%)}.coding-task-progress.pending{background:linear-gradient(to right,var(--pending) 20%,var(--border-light) 20%)}.coding-subagents,.coding-output{border-top:1px solid var(--border-light);padding:8px 22px}.coding-subagents>summary,.coding-output>summary{list-style:none;cursor:pointer;font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;font-weight:500;display:inline-flex;align-items:center;gap:6px}.coding-subagents>summary::-webkit-details-marker,.coding-output>summary::-webkit-details-marker{display:none}.coding-subagents-list{margin-top:10px;border-left:2px solid var(--border);padding-left:16px;margin-left:8px}.coding-subagent-row{margin-top:8px;border:1px solid var(--border-light);background:var(--surface-alt);border-radius:var(--radius-sm);padding:12px 14px;display:grid;grid-template-columns:40px auto auto auto 1fr auto;gap:8px;align-items:center}.coding-subagent-icon{width:24px;height:24px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center}.coding-subagent-icon.running{background:var(--card-accent-soft, var(--coding-accent-soft));color:var(--card-accent, var(--coding-accent))}.coding-subagent-icon.completed{background:var(--success-soft);color:var(--success)}.coding-subagent-icon.failed{background:var(--error-soft);color:var(--error)}.coding-subagent-icon.pending{background:var(--pending-soft);color:var(--pending)}.coding-subagent-task{font-size:12px;color:var(--text-dim);line-height:1.45;font-weight:400}.coding-subagent-time{font-size:11px;color:var(--text-muted);font-family:var(--mono);display:inline-flex;align-items:center;gap:3px}.coding-subagent-card{margin-top:8px;border:1px solid var(--border-light);background:var(--surface-alt);border-radius:var(--radius-sm);overflow:hidden;transition:border-color .15s}.coding-subagent-card.running{border-left:2px solid var(--card-accent, var(--coding-accent))}.coding-subagent-card.completed{border-left:2px solid var(--success)}.coding-subagent-card.failed{border-left:2px solid var(--error)}.coding-subagent-card.pending{border-left:2px solid var(--border);opacity:.7}.coding-subagent-header{padding:10px 14px;display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;font-size:12px}.coding-subagent-header::-webkit-details-marker{display:none}.coding-subagent-chevron{flex-shrink:0;transition:transform .15s;color:var(--text-muted)}.coding-subagent-card[open]>.coding-subagent-header .coding-subagent-chevron{transform:rotate(0)}.coding-subagent-card:not([open])>.coding-subagent-header .coding-subagent-chevron{transform:rotate(-90deg)}.coding-subagent-body{padding:0 14px 12px;border-top:1px solid var(--border-light)}.coding-subagent-body .coding-subagent-task{padding:8px 0 4px}.ca-subagent-output{max-height:300px;overflow-y:auto;font-size:12px;padding:8px;background:#0000001a;border-radius:var(--radius-sm);margin-top:6px}.ca-status-badge{display:inline-flex;align-items:center;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}.ca-status-badge.running{background:var(--accent-soft);color:var(--accent)}.ca-status-badge.validating{background:var(--warning-soft);color:var(--warning)}.ca-status-badge.completed{background:var(--success-soft);color:var(--success)}.ca-status-badge.failed,.ca-status-badge.timeout,.ca-status-badge.cancelled{background:var(--error-soft);color:var(--error)}.tree-container{display:flex;flex-direction:column;gap:8px}.ca-tree{margin-bottom:12px}.ca-tree-children{margin:8px 0 0 18px;padding-left:14px;border-left:2px solid var(--border-light)}.ca-tree-child{background:var(--surface-alt);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;margin-bottom:8px}.ca-output{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:10px 12px;font-size:12px;line-height:1.6;max-height:260px;overflow-y:auto;margin-top:8px}.ca-output.markdown-content{font-family:var(--sans)}.ca-output.markdown-content code,.ca-output.markdown-content pre{font-family:var(--mono)}.ca-error{background:var(--error-soft);border-color:#c2545033;color:var(--error)}.usage-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:28px}.usage-table{width:100%;border-collapse:collapse;font-size:13px}.usage-table th{background:var(--surface-alt);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);padding:10px 14px;text-align:left;border-bottom:1px solid var(--border-light)}.usage-table td{padding:10px 14px;border-bottom:1px solid var(--border-light);color:var(--text-dim);font-family:var(--mono);font-size:12px}.usage-table tr:last-child td{border-bottom:none}.usage-table tr:hover td{background:var(--surface-alt)}.usage-cost-cell{font-weight:600;color:var(--text)}.usage-model-pill{font-family:var(--mono);font-size:11px;background:var(--surface-alt);border:1px solid var(--border-light);border-radius:4px;padding:2px 6px;color:var(--text)}.digest-header{margin-bottom:14px}.digest-title{font-family:var(--serif);font-size:24px;font-weight:700;line-height:1.2}.digest-meta{font-size:12px;color:var(--text-muted);font-family:var(--mono);margin-top:4px}.digest-reader{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:16px 18px;word-break:break-word;line-height:1.65;font-size:14px;color:var(--text-dim)}.digest-articles{padding:6px 0}.digest-article-card{padding:12px 0;border-bottom:1px solid var(--border-light)}.digest-article-card:last-child{border-bottom:none}.digest-article-card .source-badge{font-size:11px;color:var(--accent);font-weight:600;text-transform:uppercase;letter-spacing:.04em}.digest-article-card .article-title{display:inline-block;font-size:15px;font-weight:600;color:var(--text);margin-top:4px;text-decoration:none}.digest-article-card .article-title:hover{color:var(--accent);text-decoration:underline}.digest-article-card .article-stats{font-size:12px;color:var(--text-muted);margin-top:5px}.skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px}.skill-card{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-sm)}.skill-header{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:8px}.skill-name{font-weight:600;font-size:15px}.skill-desc{font-size:13px;color:var(--text-dim);line-height:1.5;margin-bottom:10px}.skill-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}.skill-tag{font-size:11px;font-family:var(--mono);padding:2px 8px;border-radius:999px;background:var(--surface-alt);color:var(--text-muted);border:1px solid var(--border-light)}.markdown-content{line-height:1.65;color:var(--text-dim)}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{font-family:var(--serif);color:var(--text);font-weight:800;margin:16px 0 10px;line-height:1.3}.markdown-content h1:first-child,.markdown-content h2:first-child,.markdown-content h3:first-child,.markdown-content h4:first-child,.markdown-content h5:first-child,.markdown-content h6:first-child{margin-top:0}.markdown-content h1{font-size:22px}.markdown-content h2{font-size:19px}.markdown-content h3{font-size:17px}.markdown-content h4{font-size:15px}.markdown-content h5{font-size:14px}.markdown-content h6{font-size:13px}.markdown-content p{margin:0 0 10px}.markdown-content p:last-child{margin-bottom:0}.markdown-content ul,.markdown-content ol{padding-left:20px;margin:0 0 10px}.markdown-content li{margin-bottom:4px}.markdown-content a{color:var(--accent);text-decoration:none}.markdown-content a:hover{text-decoration:underline}.markdown-content code{font-family:var(--mono);background:var(--surface-alt);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--text)}.markdown-content pre{margin:0 0 12px;background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:12px;overflow:auto}.markdown-content pre code{background:transparent;padding:0;font-size:12px}.markdown-content blockquote{border-left:3px solid var(--accent);padding-left:12px;margin:0 0 12px;color:var(--text-muted);font-style:italic}.markdown-content table{width:100%;border-collapse:collapse;margin:0 0 12px;font-size:13px}.markdown-content th,.markdown-content td{border:1px solid var(--border-light);padding:8px 10px;text-align:left}.markdown-content th{background:var(--surface-alt);font-weight:600;color:var(--text)}.markdown-content td{color:var(--text-dim)}.markdown-content hr{border:none;border-top:1px solid var(--border-light);margin:16px 0}.markdown-content strong,.markdown-content b{font-weight:600;color:var(--text)}.markdown-content em,.markdown-content i{font-style:italic}.templates-page{display:flex;flex-direction:column;gap:0}.templates-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:28px;position:relative;z-index:1;padding-right:72px}.templates-loading{display:flex;justify-content:center;align-items:center;padding:64px}.templates-layout{display:grid;grid-template-columns:280px 1fr;gap:24px;align-items:stretch;min-height:600px}.templates-sidebar{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);box-shadow:var(--shadow-sm);display:flex;flex-direction:column;height:100%}.templates-sidebar-header{padding:20px 20px 16px;border-bottom:1px solid var(--border-light)}.templates-sidebar-label{font-size:11px;font-weight:700;letter-spacing:.06em;color:var(--text-muted);text-transform:uppercase;margin-bottom:10px}.templates-agent-select{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-alt);color:var(--text-dim);font-family:var(--sans);font-size:13px;font-weight:500;cursor:pointer;outline:none}.templates-agent-select:disabled{opacity:.6;cursor:not-allowed}.templates-list{display:flex;flex-direction:column;flex:1;overflow-y:auto}.templates-list-item{display:flex;align-items:center;gap:12px;padding:14px 20px;border:none;border-bottom:1px solid var(--border-light);background:transparent;cursor:pointer;transition:background .15s ease;text-align:left;width:100%;position:relative}.templates-list-item:last-child{border-bottom:none}.templates-list-item:hover{background:var(--surface-alt)}.templates-list-item.active{background:var(--accent-soft-2)}.templates-list-item.active:before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent)}.templates-list-icon{width:32px;height:32px;border-radius:var(--radius-sm);background:var(--surface-alt);display:flex;align-items:center;justify-content:center;color:var(--text-dim);flex-shrink:0;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em}.templates-list-item.active .templates-list-icon{background:var(--accent-soft);color:var(--accent)}.templates-list-content{flex:1;min-width:0}.templates-list-title{font-size:13px;font-weight:500;color:var(--text);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.templates-list-item.active .templates-list-title{color:var(--accent);font-weight:600}.templates-list-meta{font-size:11px;font-family:var(--mono);color:var(--text-muted)}.templates-main{background:var(--surface);border:1px solid var(--border-light);border-radius:var(--radius);box-shadow:var(--shadow-sm);display:flex;flex-direction:column;height:100%}.templates-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 32px;color:var(--text-muted);gap:12px}.templates-empty p{font-size:14px;margin:0}.templates-editor-header{display:flex;justify-content:space-between;align-items:center;padding:20px 28px;border-bottom:1px solid var(--border-light);gap:16px}.templates-editor-title-row{display:flex;align-items:center;gap:12px;flex:1;min-width:0}.templates-editor-title{font-family:var(--serif);font-size:20px;font-weight:700;color:var(--text);margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.templates-unsaved-pill{display:inline-flex;align-items:center;padding:4px 12px;border-radius:999px;background:var(--warning-soft);color:var(--warning);font-size:11px;font-weight:600;letter-spacing:.02em;white-space:nowrap}.templates-editor-actions{display:flex;gap:10px;align-items:center}.templates-discard-btn{padding:8px 16px;border:none;border-radius:var(--radius-sm);background:transparent;color:var(--text-dim);font-family:var(--sans);font-weight:500;font-size:13px;cursor:pointer;transition:all .15s ease}.templates-discard-btn:hover{color:var(--text);background:var(--surface-alt)}.templates-save-btn{padding:8px 20px;border:none;border-radius:var(--radius-sm);background:var(--accent);color:#fff;font-family:var(--sans);font-weight:600;font-size:13px;cursor:pointer;transition:all .15s ease}.templates-save-btn:hover{background:var(--accent-hover)}.templates-save-btn:disabled{opacity:.5;cursor:not-allowed}.templates-tabs{display:flex;gap:0;padding:0 28px;border-bottom:1px solid var(--border-light)}.templates-tab{padding:12px 20px;border:none;background:transparent;color:var(--text-muted);font-family:var(--sans);font-weight:500;font-size:13px;cursor:pointer;position:relative;transition:color .15s ease}.templates-tab:hover{color:var(--text-dim)}.templates-tab.active{color:var(--accent)}.templates-tab.active:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--accent)}.templates-metadata{display:grid;grid-template-columns:auto auto auto 1fr;gap:24px;padding:16px 28px;border-bottom:1px solid var(--border-light);background:var(--surface-alt)}.templates-metadata-item{display:flex;flex-direction:column;gap:4px}.templates-metadata-label{font-size:10px;font-weight:700;letter-spacing:.06em;color:var(--text-muted);text-transform:uppercase}.templates-metadata-value{font-size:12px;font-family:var(--mono);color:var(--text-dim);font-weight:500}.templates-metadata-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.templates-content{flex:1;display:flex;flex-direction:column;overflow:hidden}.templates-textarea{flex:1;width:100%;min-height:500px;padding:24px 28px;border:none;background:var(--surface);color:var(--text);font-family:var(--mono);font-size:13px;line-height:1.65;resize:none;outline:none}.templates-markdown-preview{flex:1;padding:28px 32px;overflow-y:auto}.templates-markdown-preview .markdown-view{max-width:800px;margin:0 auto}.templates-markdown-preview .markdown-view h1,.templates-markdown-preview .markdown-view h2,.templates-markdown-preview .markdown-view h3,.templates-markdown-preview .markdown-view h4,.templates-markdown-preview .markdown-view h5,.templates-markdown-preview .markdown-view h6{font-family:var(--serif);color:var(--text);font-weight:800;margin:16px 0 8px;line-height:1.3}.templates-markdown-preview .markdown-view h1:first-child,.templates-markdown-preview .markdown-view h2:first-child,.templates-markdown-preview .markdown-view h3:first-child{margin-top:0}.templates-markdown-preview .markdown-view h1{font-size:28px;padding-bottom:8px;border-bottom:1px solid var(--border-light)}.templates-markdown-preview .markdown-view h2{font-size:22px}.templates-markdown-preview .markdown-view h3{font-size:18px}.templates-markdown-preview .markdown-view p{margin:0 0 8px;line-height:1.58}.templates-markdown-preview .markdown-view blockquote{border-left:4px solid var(--accent);padding-left:16px;margin:10px 0;color:var(--text-dim);font-style:italic}.templates-markdown-preview .markdown-view hr{border:none;border-top:1px solid var(--border-light);margin:18px 0}.templates-markdown-preview .markdown-view code{font-family:var(--mono);background:var(--surface-alt);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--text)}.templates-markdown-preview .markdown-view pre{background:var(--surface-alt);border:1px solid var(--border-light);border-radius:var(--radius-sm);padding:12px;margin:10px 0;overflow-x:auto}.templates-markdown-preview .markdown-view pre code{background:transparent;padding:0}[data-theme=dark] .split-list,[data-theme=dark] .split-detail,[data-theme=dark] .templates-list,[data-theme=dark] .templates-markdown-preview,[data-theme=dark] .ca-output{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.12) transparent}[data-theme=dark] .split-list::-webkit-scrollbar,[data-theme=dark] .split-detail::-webkit-scrollbar,[data-theme=dark] .templates-list::-webkit-scrollbar,[data-theme=dark] .templates-markdown-preview::-webkit-scrollbar,[data-theme=dark] .ca-output::-webkit-scrollbar{width:6px}[data-theme=dark] .split-list::-webkit-scrollbar-thumb,[data-theme=dark] .split-detail::-webkit-scrollbar-thumb,[data-theme=dark] .templates-list::-webkit-scrollbar-thumb,[data-theme=dark] .templates-markdown-preview::-webkit-scrollbar-thumb,[data-theme=dark] .ca-output::-webkit-scrollbar-thumb{background:#ffffff1f;border-radius:3px}[data-theme=dark] .split-list::-webkit-scrollbar-track,[data-theme=dark] .split-detail::-webkit-scrollbar-track,[data-theme=dark] .templates-list::-webkit-scrollbar-track,[data-theme=dark] .templates-markdown-preview::-webkit-scrollbar-track,[data-theme=dark] .ca-output::-webkit-scrollbar-track{background:transparent}@media (max-width: 980px){.shell{grid-template-columns:1fr}.shell.shell-narrow .sidebar{position:fixed;top:0;left:0;height:100vh;width:min(86vw,320px);border-right:1px solid var(--border-light);box-shadow:var(--shadow-md);transform:translate(-104%);transition:transform .2s ease;z-index:210}.shell.shell-narrow.sidebar-open .sidebar{transform:translate(0)}.global-sidebar-toggle{position:fixed;top:20px;left:16px;width:37px;height:37px;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius-sm);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:all .15s ease;box-shadow:var(--shadow-sm);color:var(--text);z-index:220}.global-sidebar-toggle:hover{background:var(--surface-hover);box-shadow:var(--shadow)}.sidebar-backdrop{display:block;position:fixed;top:0;right:0;bottom:0;left:0;border:none;padding:0;background:#00000042;z-index:205}.main{padding:20px 16px}.page-header{padding-left:56px;min-height:40px}.global-theme-toggle{top:20px;right:16px;z-index:220}.page-header .header-actions{margin-right:62px;flex-wrap:wrap}.split{grid-template-columns:1fr;height:auto}.split-list{max-height:280px}.stats-grid,.stats-grid-5,.stats-grid-6,.usage-stats-grid{grid-template-columns:1fr 1fr}.stat-card{min-height:116px;padding:18px;gap:12px}.stat-icon{width:44px;height:44px}.stat-icon svg{width:18px;height:18px}.stat-value{font-size:24px}.stat-value-md{font-size:18px}.stat-subtitle{font-size:10px}.coding-stats-grid{grid-template-columns:1fr 1fr}.agents-layout{grid-template-columns:1fr}.coding-tl-row{grid-template-columns:56px 1fr 46px}.coding-subagent-row,.cron-grid{grid-template-columns:1fr}.templates-layout{grid-template-columns:1fr;gap:16px}.templates-sidebar{max-height:280px;overflow-y:auto}.templates-metadata{grid-template-columns:1fr 1fr;gap:16px}.templates-editor-header{flex-direction:column;align-items:stretch}.templates-editor-title-row{flex-direction:column;align-items:flex-start;gap:8px}.templates-editor-actions{width:100%;justify-content:flex-end}}
@@ -6,8 +6,8 @@
6
6
  <title>SkimpyClaw Dashboard</title>
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700;800&display=swap" rel="stylesheet">
9
- <script type="module" crossorigin src="/assets/index-BoTHPby4.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-D4mufvBg.css">
9
+ <script type="module" crossorigin src="/assets/index-B345aOO-.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-ZWK4dalJ.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="app"></div>
package/dist/digests.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface DigestArticle {
9
9
  summary?: string;
10
10
  content?: string;
11
11
  publishedAt?: string;
12
+ sourceUrl?: string;
12
13
  read?: boolean;
13
14
  }
14
15
  export interface Digest {
package/dist/digests.js CHANGED
@@ -1,9 +1,12 @@
1
1
  // Digest storage and management for cron job outputs
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, } from 'fs';
3
- import { join } from 'path';
3
+ import { join, resolve, sep } from 'path';
4
4
  import { createHash } from 'crypto';
5
5
  import { getLogsDir } from './config.js';
6
6
  const DIGESTS_DIR_NAME = 'digests';
7
+ const DIGEST_INDEX_FILE = 'index.json';
8
+ const DIGEST_ID_PATTERN = /^[A-Za-z0-9_-]+-[a-f0-9]{8}$/;
9
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
7
10
  export function getDigestsDir() {
8
11
  const dir = join(getLogsDir(), DIGESTS_DIR_NAME);
9
12
  if (!existsSync(dir)) {
@@ -25,15 +28,85 @@ function generateDigestId(jobId, timestamp) {
25
28
  function generateArticleId(url) {
26
29
  return createHash('md5').update(url).digest('hex').slice(0, 12);
27
30
  }
31
+ function getDigestIndexPath() {
32
+ return join(getDigestsDir(), DIGEST_INDEX_FILE);
33
+ }
34
+ function isValidPathToken(value) {
35
+ return value.length > 0 && !value.includes('/') && !value.includes('\\') && !value.includes('\0') && !value.includes('..');
36
+ }
37
+ function isValidDigestId(id) {
38
+ return DIGEST_ID_PATTERN.test(id) && isValidPathToken(id);
39
+ }
40
+ function isValidDate(value) {
41
+ return DATE_PATTERN.test(value);
42
+ }
43
+ function getDigestDate(digest) {
44
+ return new Date(digest.createdAt).toISOString().split('T')[0];
45
+ }
46
+ function loadDigestIndex() {
47
+ const indexPath = getDigestIndexPath();
48
+ if (!existsSync(indexPath))
49
+ return {};
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(indexPath, 'utf-8'));
52
+ const valid = {};
53
+ for (const [id, entry] of Object.entries(parsed)) {
54
+ if (!isValidDigestId(id) || !entry || typeof entry !== 'object')
55
+ continue;
56
+ const jobId = entry.jobId;
57
+ const date = entry.date;
58
+ if (typeof jobId !== 'string' || typeof date !== 'string')
59
+ continue;
60
+ if (!isValidPathToken(jobId) || !isValidDate(date))
61
+ continue;
62
+ valid[id] = { jobId, date };
63
+ }
64
+ return valid;
65
+ }
66
+ catch {
67
+ return {};
68
+ }
69
+ }
70
+ function saveDigestIndex(index) {
71
+ writeFileSync(getDigestIndexPath(), JSON.stringify(index, null, 2), 'utf-8');
72
+ }
73
+ function setDigestIndexEntry(index, digest) {
74
+ const date = getDigestDate(digest);
75
+ if (!isValidDigestId(digest.id) || !isValidPathToken(digest.jobId) || !isValidDate(date))
76
+ return false;
77
+ const current = index[digest.id];
78
+ if (current && current.jobId === digest.jobId && current.date === date)
79
+ return false;
80
+ index[digest.id] = { jobId: digest.jobId, date };
81
+ return true;
82
+ }
83
+ function resolveDigestFilePath(id, entry) {
84
+ if (!isValidDigestId(id) || !isValidPathToken(entry.jobId) || !isValidDate(entry.date)) {
85
+ return null;
86
+ }
87
+ const baseDir = getDigestsDir();
88
+ const resolvedBase = resolve(baseDir);
89
+ const resolvedPath = resolve(baseDir, entry.jobId, `${entry.date}-${id}.json`);
90
+ if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(`${resolvedBase}${sep}`)) {
91
+ return null;
92
+ }
93
+ return resolvedPath;
94
+ }
28
95
  export function saveDigest(digest) {
29
96
  const jobDir = getJobDir(digest.jobId);
30
- const date = new Date(digest.createdAt).toISOString().split('T')[0];
97
+ const date = getDigestDate(digest);
31
98
  const filePath = join(jobDir, `${date}-${digest.id}.json`);
32
99
  writeFileSync(filePath, JSON.stringify(digest, null, 2), 'utf-8');
100
+ const index = loadDigestIndex();
101
+ if (setDigestIndexEntry(index, digest)) {
102
+ saveDigestIndex(index);
103
+ }
33
104
  }
34
105
  export function getDigests(jobId, limit) {
35
106
  const digestsDir = getDigestsDir();
36
107
  const items = [];
108
+ const index = loadDigestIndex();
109
+ let indexChanged = false;
37
110
  const jobDirs = jobId
38
111
  ? [join(digestsDir, jobId)].filter(existsSync)
39
112
  : readdirSync(digestsDir, { withFileTypes: true })
@@ -55,6 +128,9 @@ export function getDigests(jobId, limit) {
55
128
  articleCount: digest.articles.length,
56
129
  preview: digest.articles.slice(0, 2).map(a => a.title),
57
130
  });
131
+ if (setDigestIndexEntry(index, digest)) {
132
+ indexChanged = true;
133
+ }
58
134
  }
59
135
  catch {
60
136
  // Skip invalid files
@@ -64,54 +140,55 @@ export function getDigests(jobId, limit) {
64
140
  // Sort by createdAt desc
65
141
  items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
66
142
  if (limit && limit > 0) {
143
+ if (indexChanged) {
144
+ saveDigestIndex(index);
145
+ }
67
146
  return items.slice(0, limit);
68
147
  }
148
+ if (indexChanged) {
149
+ saveDigestIndex(index);
150
+ }
69
151
  return items;
70
152
  }
71
153
  export function getDigest(id) {
72
- const digestsDir = getDigestsDir();
73
- const jobDirs = readdirSync(digestsDir, { withFileTypes: true })
74
- .filter(d => d.isDirectory())
75
- .map(d => join(digestsDir, d.name));
76
- for (const dir of jobDirs) {
77
- const files = readdirSync(dir).filter(f => f.endsWith('.json'));
78
- for (const file of files) {
79
- try {
80
- const content = readFileSync(join(dir, file), 'utf-8');
81
- const digest = JSON.parse(content);
82
- if (digest.id === id) {
83
- return digest;
84
- }
85
- }
86
- catch {
87
- // Skip invalid files
88
- }
89
- }
154
+ const index = loadDigestIndex();
155
+ const entry = index[id];
156
+ if (!entry)
157
+ return null;
158
+ const filePath = resolveDigestFilePath(id, entry);
159
+ if (!filePath || !existsSync(filePath)) {
160
+ delete index[id];
161
+ saveDigestIndex(index);
162
+ return null;
163
+ }
164
+ try {
165
+ const content = readFileSync(filePath, 'utf-8');
166
+ return JSON.parse(content);
167
+ }
168
+ catch {
169
+ return null;
90
170
  }
91
- return null;
92
171
  }
93
172
  export function deleteDigest(id) {
94
- const digestsDir = getDigestsDir();
95
- const jobDirs = readdirSync(digestsDir, { withFileTypes: true })
96
- .filter(d => d.isDirectory())
97
- .map(d => join(digestsDir, d.name));
98
- for (const dir of jobDirs) {
99
- const files = readdirSync(dir).filter(f => f.endsWith('.json'));
100
- for (const file of files) {
101
- try {
102
- const content = readFileSync(join(dir, file), 'utf-8');
103
- const digest = JSON.parse(content);
104
- if (digest.id === id) {
105
- unlinkSync(join(dir, file));
106
- return true;
107
- }
108
- }
109
- catch {
110
- // Skip invalid files
111
- }
112
- }
173
+ const index = loadDigestIndex();
174
+ const entry = index[id];
175
+ if (!entry)
176
+ return false;
177
+ const filePath = resolveDigestFilePath(id, entry);
178
+ if (!filePath || !existsSync(filePath)) {
179
+ delete index[id];
180
+ saveDigestIndex(index);
181
+ return false;
182
+ }
183
+ try {
184
+ unlinkSync(filePath);
185
+ delete index[id];
186
+ saveDigestIndex(index);
187
+ return true;
188
+ }
189
+ catch {
190
+ return false;
113
191
  }
114
- return false;
115
192
  }
116
193
  export function updateArticleReadStatus(digestId, articleId, read) {
117
194
  const digest = getDigest(digestId);
@@ -157,9 +234,19 @@ function extractUrls(text) {
157
234
  }
158
235
  function extractTitleForUrl(text, url) {
159
236
  const lines = text.split('\n');
237
+ const isMarkdownHeading = (value) => /^#{1,6}\s+\S/.test(value);
160
238
  for (let i = 0; i < lines.length; i++) {
161
239
  const line = lines[i];
162
240
  if (line.includes(url)) {
241
+ const markdownLinkMatches = [...line.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g)];
242
+ for (const match of markdownLinkMatches) {
243
+ const markdownUrl = match[2]?.replace(/[.,;:!?)\]>'"`]+$/, '');
244
+ if (markdownUrl === url) {
245
+ const title = match[1]?.trim();
246
+ if (title)
247
+ return title.slice(0, 150);
248
+ }
249
+ }
163
250
  // If the URL is on its own line (🔗 link line), look backwards for the title
164
251
  const isLinkOnlyLine = /^\s*🔗?\s*https?:\/\//i.test(line.trim());
165
252
  if (isLinkOnlyLine) {
@@ -173,6 +260,9 @@ function extractTitleForUrl(text, url) {
173
260
  // eslint-disable-next-line no-misleading-character-class
174
261
  if (/^[\u2B06\uFE0F\u{1F4AC}\u{1F525}\u{1F504}\u2B50\u{1F517}]+/u.test(prev))
175
262
  continue;
263
+ // Skip markdown section headers (e.g. "## World Headlines")
264
+ if (isMarkdownHeading(prev))
265
+ continue;
176
266
  // Skip lines that are just URLs
177
267
  if (/^https?:\/\//.test(prev))
178
268
  continue;
@@ -196,7 +286,7 @@ function extractTitleForUrl(text, url) {
196
286
  if (i > 0) {
197
287
  const prevLine = lines[i - 1].trim();
198
288
  // eslint-disable-next-line no-misleading-character-class
199
- if (prevLine && !prevLine.startsWith('http') && !/^[\u2B06\uFE0F\u{1F4AC}\u{1F525}\u{1F504}\u2B50\u{1F517}]+/u.test(prevLine)) {
289
+ if (prevLine && !prevLine.startsWith('http') && !isMarkdownHeading(prevLine) && !/^[\u2B06\uFE0F\u{1F4AC}\u{1F525}\u{1F504}\u2B50\u{1F517}]+/u.test(prevLine)) {
200
290
  const cleaned = prevLine.replace(/^\d+\.\s*/, '').replace(/^[-*•]\s*/, '').trim();
201
291
  if (cleaned.length > 0)
202
292
  return cleaned.slice(0, 150);
@@ -10,11 +10,8 @@ export declare function checkAllowedPathsWritable(config: Config): Promise<Docto
10
10
  export declare function checkProviderAuth(providerName: string, providerConfig: NonNullable<Config['models']['providers'][string]>): Promise<DoctorCheckResult>;
11
11
  export declare function checkTelegramToken(token: string): Promise<DoctorCheckResult>;
12
12
  export declare function checkDiscordToken(token: string): Promise<DoctorCheckResult>;
13
- export declare function checkBrowserBinaryIfEnabled(config: Config): Promise<DoctorCheckResult>;
14
13
  export declare function checkVoiceDependencies(config: Config): Promise<DoctorCheckResult>;
15
- export declare function checkPlaywrightIfBrowserEnabled(config: Config): Promise<DoctorCheckResult>;
16
14
  export declare function checkMcpConfig(config: Config): Promise<DoctorCheckResult>;
17
15
  export declare function checkGatewayHostBindable(host: string): Promise<DoctorCheckResult>;
18
16
  export declare function checkSkimpyclawDirWritable(): Promise<DoctorCheckResult>;
19
- export declare function checkSandboxAvailable(config: Config): Promise<DoctorCheckResult>;
20
17
  export declare function checkPortAvailability(port: number): Promise<DoctorCheckResult>;
@@ -101,7 +101,7 @@ export async function checkEnvVarPatterns(config) {
101
101
  const key = providerCfg?.apiKey || providerCfg?.authToken || '';
102
102
  if (!key)
103
103
  continue;
104
- if (/openai|anthropic|minimax/i.test(providerName) && !key.startsWith('sk-')) {
104
+ if (/anthropic/i.test(providerName) && !key.startsWith('sk-')) {
105
105
  issues.push(`${providerName} key should usually start with "sk-"`);
106
106
  }
107
107
  }
@@ -147,16 +147,6 @@ export async function checkProviderAuth(providerName, providerConfig) {
147
147
  }
148
148
  const normalized = providerName.toLowerCase();
149
149
  try {
150
- if (normalized === 'openai') {
151
- const res = await fetch(`${providerConfig.baseURL || 'https://api.openai.com/v1'}/models`, {
152
- headers: { Authorization: `Bearer ${token}` },
153
- signal: AbortSignal.timeout(7000),
154
- });
155
- if (!res.ok) {
156
- return fail(name, category, `${res.status} ${res.statusText}`, `Check ${providerName.toUpperCase()}_API_KEY and provider base URL.`);
157
- }
158
- return ok(name, category, `${res.status} ${res.statusText}`);
159
- }
160
150
  if (normalized === 'anthropic') {
161
151
  // OAuth tokens can't be validated via API (they use Claude Code's internal refresh flow)
162
152
  if (providerConfig.authToken) {
@@ -225,44 +215,6 @@ export async function checkDiscordToken(token) {
225
215
  return fail(name, category, detail, 'Check network and DISCORD_BOT_TOKEN.');
226
216
  }
227
217
  }
228
- function isAnyBrowserEnabled(config) {
229
- return Boolean(config.channels.telegram?.tools?.browser?.enabled
230
- || config.channels.discord?.tools?.browser?.enabled
231
- || config.heartbeat?.tools?.browser?.enabled);
232
- }
233
- export async function checkBrowserBinaryIfEnabled(config) {
234
- const name = 'browser_binary_available';
235
- const category = 'runtime';
236
- if (!isAnyBrowserEnabled(config)) {
237
- return ok(name, category, 'Browser tools disabled');
238
- }
239
- const explicitPath = config.channels.telegram?.tools?.browser?.executablePath
240
- || config.channels.discord?.tools?.browser?.executablePath
241
- || config.heartbeat?.tools?.browser?.executablePath;
242
- if (explicitPath) {
243
- if (existsSync(explicitPath)) {
244
- return ok(name, category, explicitPath);
245
- }
246
- return fail(name, category, `Browser executable not found at ${explicitPath}`, 'Fix tools.browser.executablePath or install a supported browser.');
247
- }
248
- const probes = [
249
- ['which', ['google-chrome']],
250
- ['which', ['chromium']],
251
- ['which', ['chromium-browser']],
252
- ['which', ['firefox']],
253
- ];
254
- for (const [cmd, args] of probes) {
255
- const probe = spawnSync(cmd, args, { encoding: 'utf-8' });
256
- if (probe.status === 0 && probe.stdout.trim()) {
257
- return ok(name, category, probe.stdout.trim());
258
- }
259
- }
260
- const macChrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
261
- if (existsSync(macChrome)) {
262
- return ok(name, category, macChrome);
263
- }
264
- return fail(name, category, 'No supported browser binary found', 'Install Chrome/Chromium/Firefox or set tools.browser.executablePath.');
265
- }
266
218
  export async function checkVoiceDependencies(config) {
267
219
  const name = 'voice_dependencies';
268
220
  const category = 'runtime';
@@ -289,18 +241,6 @@ export async function checkVoiceDependencies(config) {
289
241
  const sttMethod = hasLocalWhisper ? (whisperCli.status === 0 ? 'whisper-cli' : 'whisper') : 'API STT';
290
242
  return ok(name, category, `ffmpeg and ${sttMethod} available`);
291
243
  }
292
- export async function checkPlaywrightIfBrowserEnabled(config) {
293
- const name = 'playwright_installed';
294
- const category = 'runtime';
295
- if (!isAnyBrowserEnabled(config)) {
296
- return ok(name, category, 'Browser tools disabled');
297
- }
298
- const pw = spawnSync('npx', ['playwright', '--version'], { encoding: 'utf-8', timeout: 10000 });
299
- if (pw.status === 0) {
300
- return ok(name, category, `Playwright ${(pw.stdout || '').trim()}`);
301
- }
302
- return fail(name, category, 'Playwright not installed', 'Run: npx playwright install chromium');
303
- }
304
244
  export async function checkMcpConfig(config) {
305
245
  const name = 'mcp_config';
306
246
  const category = 'runtime';
@@ -357,53 +297,6 @@ export async function checkSkimpyclawDirWritable() {
357
297
  return fail(name, category, `Directory not writable: ${base}`, 'Fix permissions for ~/.skimpyclaw.');
358
298
  }
359
299
  }
360
- export async function checkSandboxAvailable(config) {
361
- const name = 'sandbox_available';
362
- const category = 'runtime';
363
- if (!config.sandbox?.enabled) {
364
- return ok(name, category, 'Sandbox disabled');
365
- }
366
- // Determine which runtime to check
367
- const explicit = config.sandbox.runtime;
368
- let rt = null;
369
- if (explicit) {
370
- const check = spawnSync(explicit, ['--version'], { encoding: 'utf-8' });
371
- if (check.status !== 0) {
372
- return fail(name, category, `${explicit} CLI not found`, `Install ${explicit} or change sandbox.runtime in config.`);
373
- }
374
- rt = explicit;
375
- }
376
- else {
377
- // Auto-detect: prefer container, fall back to docker
378
- const containerCheck = spawnSync('container', ['--version'], { encoding: 'utf-8' });
379
- if (containerCheck.status === 0) {
380
- rt = 'container';
381
- }
382
- else {
383
- const dockerCheck = spawnSync('docker', ['--version'], { encoding: 'utf-8' });
384
- if (dockerCheck.status === 0) {
385
- rt = 'docker';
386
- }
387
- }
388
- if (!rt) {
389
- return fail(name, category, 'No container runtime found', 'Install Apple Containers or Docker, or disable sandbox in config.');
390
- }
391
- }
392
- // For Apple Containers, check system is running
393
- if (rt === 'container') {
394
- const systemCheck = spawnSync('container', ['system', 'status'], { encoding: 'utf-8' });
395
- if (systemCheck.status !== 0) {
396
- return fail(name, category, 'Container system not running', 'Run "container system start" to start the container runtime.');
397
- }
398
- }
399
- // Check if sandbox image exists
400
- const image = config.sandbox.image || 'skimpyclaw-sandbox';
401
- const imageCheck = spawnSync(rt, ['image', 'inspect', image], { encoding: 'utf-8' });
402
- if (imageCheck.status !== 0) {
403
- return fail(name, category, `Sandbox image "${image}" not found`, `Build it: ${rt} build -t ${image} sandbox/`);
404
- }
405
- return ok(name, category, `Runtime: ${rt}, image "${image}" available`);
406
- }
407
300
  export async function checkPortAvailability(port) {
408
301
  const name = 'gateway_port_available';
409
302
  const category = 'runtime';
@@ -1,5 +1,5 @@
1
1
  import { loadConfig } from '../config.js';
2
- import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkBrowserBinaryIfEnabled, checkPlaywrightIfBrowserEnabled, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, checkSandboxAvailable, } from './checks.js';
2
+ import { checkNodeVersion, checkPackageManagerAvailable, checkTypeScriptCompile, checkConfigExistsAndValidJson, checkRequiredEnvVars, checkEnvVarPatterns, checkAllowedPathsWritable, checkProviderAuth, checkTelegramToken, checkDiscordToken, checkVoiceDependencies, checkMcpConfig, checkGatewayHostBindable, checkSkimpyclawDirWritable, checkPortAvailability, } from './checks.js';
3
3
  export function computeExitCode(report) {
4
4
  if (report.checks.some((check) => !check.ok && check.fatal)) {
5
5
  return 2;
@@ -98,14 +98,11 @@ export async function runDoctor() {
98
98
  checks.push(await runSafe('discord_token_valid', 'channels', () => checkDiscordToken(discord.token)));
99
99
  }
100
100
  }
101
- checks.push(await runSafe('browser_binary_available', 'runtime', () => checkBrowserBinaryIfEnabled(config)));
102
- checks.push(await runSafe('playwright_installed', 'runtime', () => checkPlaywrightIfBrowserEnabled(config)));
103
101
  checks.push(await runSafe('voice_dependencies', 'runtime', () => checkVoiceDependencies(config)));
104
102
  checks.push(await runSafe('mcp_config', 'runtime', () => checkMcpConfig(config)));
105
103
  checks.push(await runSafe('gateway_host_bindable', 'runtime', () => checkGatewayHostBindable(config.gateway.host ?? '127.0.0.1')));
106
104
  checks.push(await runSafe('skimpyclaw_dirs_writable', 'runtime', () => checkSkimpyclawDirWritable()));
107
105
  checks.push(await runSafe('gateway_port_available', 'runtime', () => checkPortAvailability(config.gateway.port)));
108
- checks.push(await runSafe('sandbox_available', 'runtime', () => checkSandboxAvailable(config)));
109
106
  const report = buildReport(startedAt, checks);
110
107
  return { report, exitCode: report.exitCode };
111
108
  }
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeExecEnv(): Record<string, string | undefined>;
2
+ export declare function sanitizeCronEnv(): Record<string, string | undefined>;