@hybridaione/hybridclaw 0.1.5

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 (196) hide show
  1. package/.env.example +14 -0
  2. package/.github/workflows/pages.yml +41 -0
  3. package/AGENTS.md +24 -0
  4. package/CHANGELOG.md +113 -0
  5. package/LICENSE +21 -0
  6. package/README.md +226 -0
  7. package/SECURITY.md +67 -0
  8. package/config.example.json +62 -0
  9. package/container/Dockerfile +23 -0
  10. package/container/package-lock.json +246 -0
  11. package/container/package.json +17 -0
  12. package/container/src/hybridai-client.ts +38 -0
  13. package/container/src/index.ts +198 -0
  14. package/container/src/ipc.ts +37 -0
  15. package/container/src/tools.ts +1008 -0
  16. package/container/src/types.ts +74 -0
  17. package/container/src/web-fetch.ts +389 -0
  18. package/container/tsconfig.json +16 -0
  19. package/dist/agent.d.ts +3 -0
  20. package/dist/agent.d.ts.map +1 -0
  21. package/dist/agent.js +21 -0
  22. package/dist/agent.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +32 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config.d.ts +38 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +107 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/container-runner.d.ts +11 -0
  32. package/dist/container-runner.d.ts.map +1 -0
  33. package/dist/container-runner.js +246 -0
  34. package/dist/container-runner.js.map +1 -0
  35. package/dist/conversation.d.ts +18 -0
  36. package/dist/conversation.d.ts.map +1 -0
  37. package/dist/conversation.js +29 -0
  38. package/dist/conversation.js.map +1 -0
  39. package/dist/db.d.ts +29 -0
  40. package/dist/db.d.ts.map +1 -0
  41. package/dist/db.js +205 -0
  42. package/dist/db.js.map +1 -0
  43. package/dist/discord.d.ts +17 -0
  44. package/dist/discord.d.ts.map +1 -0
  45. package/dist/discord.js +115 -0
  46. package/dist/discord.js.map +1 -0
  47. package/dist/env.d.ts +6 -0
  48. package/dist/env.d.ts.map +1 -0
  49. package/dist/env.js +36 -0
  50. package/dist/env.js.map +1 -0
  51. package/dist/gateway-client.d.ts +8 -0
  52. package/dist/gateway-client.d.ts.map +1 -0
  53. package/dist/gateway-client.js +57 -0
  54. package/dist/gateway-client.js.map +1 -0
  55. package/dist/gateway-service.d.ts +23 -0
  56. package/dist/gateway-service.d.ts.map +1 -0
  57. package/dist/gateway-service.js +360 -0
  58. package/dist/gateway-service.js.map +1 -0
  59. package/dist/gateway-types.d.ts +40 -0
  60. package/dist/gateway-types.d.ts.map +1 -0
  61. package/dist/gateway-types.js +6 -0
  62. package/dist/gateway-types.js.map +1 -0
  63. package/dist/gateway.d.ts +2 -0
  64. package/dist/gateway.d.ts.map +1 -0
  65. package/dist/gateway.js +138 -0
  66. package/dist/gateway.js.map +1 -0
  67. package/dist/hatch.d.ts +7 -0
  68. package/dist/hatch.d.ts.map +1 -0
  69. package/dist/hatch.js +99 -0
  70. package/dist/hatch.js.map +1 -0
  71. package/dist/health.d.ts +2 -0
  72. package/dist/health.d.ts.map +1 -0
  73. package/dist/health.js +169 -0
  74. package/dist/health.js.map +1 -0
  75. package/dist/heartbeat.d.ts +3 -0
  76. package/dist/heartbeat.d.ts.map +1 -0
  77. package/dist/heartbeat.js +103 -0
  78. package/dist/heartbeat.js.map +1 -0
  79. package/dist/hybridai-bots.d.ts +5 -0
  80. package/dist/hybridai-bots.d.ts.map +1 -0
  81. package/dist/hybridai-bots.js +34 -0
  82. package/dist/hybridai-bots.js.map +1 -0
  83. package/dist/index.d.ts +2 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +60 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/ipc.d.ts +33 -0
  88. package/dist/ipc.d.ts.map +1 -0
  89. package/dist/ipc.js +142 -0
  90. package/dist/ipc.js.map +1 -0
  91. package/dist/logger.d.ts +3 -0
  92. package/dist/logger.d.ts.map +1 -0
  93. package/dist/logger.js +21 -0
  94. package/dist/logger.js.map +1 -0
  95. package/dist/mount-security.d.ts +28 -0
  96. package/dist/mount-security.d.ts.map +1 -0
  97. package/dist/mount-security.js +187 -0
  98. package/dist/mount-security.js.map +1 -0
  99. package/dist/onboarding.d.ts +7 -0
  100. package/dist/onboarding.d.ts.map +1 -0
  101. package/dist/onboarding.js +445 -0
  102. package/dist/onboarding.js.map +1 -0
  103. package/dist/prompt-hooks.d.ts +17 -0
  104. package/dist/prompt-hooks.d.ts.map +1 -0
  105. package/dist/prompt-hooks.js +83 -0
  106. package/dist/prompt-hooks.js.map +1 -0
  107. package/dist/runtime-config.d.ts +78 -0
  108. package/dist/runtime-config.d.ts.map +1 -0
  109. package/dist/runtime-config.js +471 -0
  110. package/dist/runtime-config.js.map +1 -0
  111. package/dist/scheduled-task-runner.d.ts +11 -0
  112. package/dist/scheduled-task-runner.d.ts.map +1 -0
  113. package/dist/scheduled-task-runner.js +16 -0
  114. package/dist/scheduled-task-runner.js.map +1 -0
  115. package/dist/scheduler.d.ts +11 -0
  116. package/dist/scheduler.d.ts.map +1 -0
  117. package/dist/scheduler.js +165 -0
  118. package/dist/scheduler.js.map +1 -0
  119. package/dist/session-maintenance.d.ts +9 -0
  120. package/dist/session-maintenance.d.ts.map +1 -0
  121. package/dist/session-maintenance.js +168 -0
  122. package/dist/session-maintenance.js.map +1 -0
  123. package/dist/session-transcripts.d.ts +11 -0
  124. package/dist/session-transcripts.d.ts.map +1 -0
  125. package/dist/session-transcripts.js +32 -0
  126. package/dist/session-transcripts.js.map +1 -0
  127. package/dist/side-effects.d.ts +3 -0
  128. package/dist/side-effects.d.ts.map +1 -0
  129. package/dist/side-effects.js +30 -0
  130. package/dist/side-effects.js.map +1 -0
  131. package/dist/skills.d.ts +32 -0
  132. package/dist/skills.d.ts.map +1 -0
  133. package/dist/skills.js +376 -0
  134. package/dist/skills.js.map +1 -0
  135. package/dist/tui.d.ts +2 -0
  136. package/dist/tui.d.ts.map +1 -0
  137. package/dist/tui.js +305 -0
  138. package/dist/tui.js.map +1 -0
  139. package/dist/types.d.ts +132 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +3 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/workspace.d.ts +25 -0
  144. package/dist/workspace.d.ts.map +1 -0
  145. package/dist/workspace.js +154 -0
  146. package/dist/workspace.js.map +1 -0
  147. package/docs/chat.html +929 -0
  148. package/docs/hai_logo_free.png +0 -0
  149. package/docs/hero.png +0 -0
  150. package/docs/index.html +1213 -0
  151. package/package.json +34 -0
  152. package/skills/current-time/SKILL.md +26 -0
  153. package/skills/iss-position/SKILL.md +46 -0
  154. package/skills/iss-position/agents/openai.yaml +3 -0
  155. package/skills/iss-position/scripts/get_iss_position.py +107 -0
  156. package/skills/repo-orientation/SKILL.md +74 -0
  157. package/src/agent.ts +35 -0
  158. package/src/cli.ts +35 -0
  159. package/src/config.ts +137 -0
  160. package/src/container-runner.ts +305 -0
  161. package/src/conversation.ts +49 -0
  162. package/src/db.ts +290 -0
  163. package/src/discord.ts +156 -0
  164. package/src/env.ts +36 -0
  165. package/src/gateway-client.ts +73 -0
  166. package/src/gateway-service.ts +456 -0
  167. package/src/gateway-types.ts +47 -0
  168. package/src/gateway.ts +199 -0
  169. package/src/health.ts +189 -0
  170. package/src/heartbeat.ts +121 -0
  171. package/src/hybridai-bots.ts +48 -0
  172. package/src/ipc.ts +163 -0
  173. package/src/logger.ts +26 -0
  174. package/src/mount-security.ts +216 -0
  175. package/src/onboarding.ts +569 -0
  176. package/src/prompt-hooks.ts +113 -0
  177. package/src/runtime-config.ts +588 -0
  178. package/src/scheduled-task-runner.ts +26 -0
  179. package/src/scheduler.ts +196 -0
  180. package/src/session-maintenance.ts +263 -0
  181. package/src/session-transcripts.ts +45 -0
  182. package/src/side-effects.ts +41 -0
  183. package/src/skills.ts +429 -0
  184. package/src/tui.ts +327 -0
  185. package/src/types.ts +135 -0
  186. package/src/workspace.ts +171 -0
  187. package/templates/AGENTS.md +104 -0
  188. package/templates/BOOT.md +3 -0
  189. package/templates/BOOTSTRAP.md +54 -0
  190. package/templates/HEARTBEAT.md +5 -0
  191. package/templates/IDENTITY.md +18 -0
  192. package/templates/MEMORY.md +19 -0
  193. package/templates/SOUL.md +36 -0
  194. package/templates/TOOLS.md +22 -0
  195. package/templates/USER.md +17 -0
  196. package/tsconfig.json +18 -0
package/docs/chat.html ADDED
@@ -0,0 +1,929 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HybridClaw Chat</title>
7
+ <style>
8
+ :root {
9
+ --page-bg: #ffffff;
10
+ --panel-bg: #ffffff;
11
+ --line: #ebeff5;
12
+ --text: #1f2937;
13
+ --muted: #8b98ab;
14
+ --brand-blue: #4a6cf7;
15
+ --brand-blue-2: #3657e9;
16
+ --error: #d94848;
17
+ --nav-width: 260px;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ html, body {
25
+ margin: 0;
26
+ height: 100%;
27
+ font-family: system-ui, ui-sans-serif, -apple-system, BlinkMacSystemFont, Inter, NotoSansHans, sans-serif;
28
+ background: var(--page-bg);
29
+ color: var(--text);
30
+ }
31
+
32
+ .layout {
33
+ position: relative;
34
+ z-index: 1;
35
+ height: 100vh;
36
+ display: grid;
37
+ grid-template-columns: var(--nav-width) 1fr;
38
+ }
39
+
40
+ .sidebar {
41
+ background: rgb(241, 245, 249);
42
+ padding: 14px 14px 10px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 10px;
46
+ border-right: 1px solid #e3e8f0;
47
+ }
48
+
49
+ .sidebar-spacer {
50
+ flex: 1 1 auto;
51
+ min-height: 10px;
52
+ }
53
+
54
+ .app-version {
55
+ font-size: 12px;
56
+ line-height: 1.2;
57
+ color: #8b98ab;
58
+ padding: 2px 2px 0;
59
+ user-select: none;
60
+ }
61
+
62
+ .sidebar-top {
63
+ display: flex;
64
+ align-items: flex-start;
65
+ justify-content: space-between;
66
+ padding: 2px 0 6px;
67
+ gap: 8px;
68
+ }
69
+
70
+ .brand {
71
+ padding: 0;
72
+ border: none;
73
+ border-radius: 0;
74
+ background: transparent;
75
+ }
76
+
77
+ .brand-logo {
78
+ width: 46px;
79
+ max-width: 100%;
80
+ display: block;
81
+ height: auto;
82
+ }
83
+
84
+ .sidebar-toggle {
85
+ width: 34px;
86
+ height: 34px;
87
+ border: none;
88
+ border-radius: 0;
89
+ background: transparent;
90
+ color: #96a0af;
91
+ display: grid;
92
+ place-items: center;
93
+ cursor: pointer;
94
+ padding: 0;
95
+ transition: color 0.2s ease;
96
+ }
97
+
98
+ .sidebar-toggle:hover {
99
+ color: #6f7a8c;
100
+ }
101
+
102
+ .sidebar-toggle svg {
103
+ width: 22px;
104
+ height: 22px;
105
+ display: block;
106
+ }
107
+
108
+ .new-chat-btn {
109
+ width: 100%;
110
+ border: none;
111
+ padding: 10px 2px;
112
+ font: inherit;
113
+ cursor: pointer;
114
+ background: transparent;
115
+ color: #374151;
116
+ text-align: left;
117
+ transition: color 0.2s ease;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 10px;
121
+ font-size: 15px;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .new-chat-btn:hover {
126
+ color: #111827;
127
+ }
128
+
129
+ .new-chat-icon {
130
+ width: auto;
131
+ height: auto;
132
+ display: inline-block;
133
+ font-size: 36px;
134
+ line-height: 1;
135
+ color: #4b5563;
136
+ transform: translateY(-1px);
137
+ }
138
+
139
+ .layout.is-sidebar-collapsed {
140
+ grid-template-columns: 84px 1fr;
141
+ }
142
+
143
+ .layout.is-sidebar-collapsed .new-chat-label {
144
+ display: none;
145
+ }
146
+
147
+ .layout.is-sidebar-collapsed .new-chat-btn {
148
+ justify-content: center;
149
+ padding-left: 0;
150
+ padding-right: 0;
151
+ }
152
+
153
+ .layout.is-sidebar-collapsed .brand-logo {
154
+ width: 44px;
155
+ }
156
+
157
+ .chat {
158
+ min-height: 0;
159
+ display: grid;
160
+ grid-template-rows: 1fr auto;
161
+ background: #ffffff;
162
+ }
163
+
164
+ .chat-body {
165
+ min-height: 0;
166
+ display: grid;
167
+ grid-template-rows: 1fr auto;
168
+ }
169
+
170
+ .messages {
171
+ min-height: 0;
172
+ overflow-y: auto;
173
+ padding: 20px;
174
+ display: grid;
175
+ gap: 10px;
176
+ align-content: start;
177
+ }
178
+
179
+ .msg {
180
+ max-width: 860px;
181
+ width: fit-content;
182
+ border-radius: 12px;
183
+ padding: 10px 12px;
184
+ border: 1px solid var(--line);
185
+ background: #ffffff;
186
+ color: #1f304d;
187
+ white-space: pre-wrap;
188
+ line-height: 1.45;
189
+ box-shadow: 0 2px 8px rgba(50, 80, 140, 0.06);
190
+ }
191
+
192
+ .msg.markdown p {
193
+ margin: 0;
194
+ }
195
+
196
+ .msg.markdown p + p {
197
+ margin-top: 10px;
198
+ }
199
+
200
+ .msg.markdown h1,
201
+ .msg.markdown h2,
202
+ .msg.markdown h3 {
203
+ margin: 0 0 8px;
204
+ color: #172034;
205
+ line-height: 1.2;
206
+ }
207
+
208
+ .msg.markdown h1 { font-size: 21px; }
209
+ .msg.markdown h2 { font-size: 19px; }
210
+ .msg.markdown h3 { font-size: 17px; }
211
+
212
+ .msg.markdown ul,
213
+ .msg.markdown ol {
214
+ margin: 0;
215
+ padding-left: 20px;
216
+ }
217
+
218
+ .msg.markdown ul + p,
219
+ .msg.markdown ol + p,
220
+ .msg.markdown p + ul,
221
+ .msg.markdown p + ol {
222
+ margin-top: 10px;
223
+ }
224
+
225
+ .msg.markdown blockquote {
226
+ margin: 0;
227
+ padding: 0 0 0 10px;
228
+ border-left: 3px solid #d8dfeb;
229
+ color: #4e6285;
230
+ }
231
+
232
+ .msg.markdown hr {
233
+ border: none;
234
+ border-top: 1px solid #d8dfeb;
235
+ margin: 8px 0;
236
+ }
237
+
238
+ .msg.markdown code {
239
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
240
+ font-size: 0.92em;
241
+ background: #f2f5fa;
242
+ color: #203a63;
243
+ padding: 1px 5px;
244
+ border-radius: 6px;
245
+ }
246
+
247
+ .msg.markdown pre {
248
+ margin: 0;
249
+ padding: 10px 12px;
250
+ border-radius: 10px;
251
+ border: 1px solid #dde5f2;
252
+ background: #f7f9fd;
253
+ overflow-x: auto;
254
+ }
255
+
256
+ .msg.markdown pre code {
257
+ background: transparent;
258
+ padding: 0;
259
+ border-radius: 0;
260
+ color: #1f304d;
261
+ display: block;
262
+ line-height: 1.4;
263
+ }
264
+
265
+ .msg-block {
266
+ max-width: 860px;
267
+ width: fit-content;
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: 5px;
271
+ }
272
+
273
+ .msg-actions {
274
+ display: flex;
275
+ justify-content: flex-start;
276
+ }
277
+
278
+ .copy-btn {
279
+ border: none;
280
+ background: transparent;
281
+ color: #8b98ab;
282
+ font-size: 14px;
283
+ line-height: 1;
284
+ padding: 2px 4px;
285
+ cursor: pointer;
286
+ }
287
+
288
+ .copy-btn:hover {
289
+ color: #4b5563;
290
+ }
291
+
292
+ .copy-btn.copied {
293
+ color: #3657e9;
294
+ }
295
+
296
+ .msg.user {
297
+ margin-left: auto;
298
+ background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-2));
299
+ border-color: transparent;
300
+ color: #ffffff;
301
+ }
302
+
303
+ .msg.assistant {
304
+ background: #ffffff;
305
+ }
306
+
307
+ .msg.system {
308
+ background: #f8faff;
309
+ color: #4e6285;
310
+ }
311
+
312
+ .msg.thinking {
313
+ min-width: 56px;
314
+ padding: 14px 16px;
315
+ }
316
+
317
+ .thinking-dots {
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: 6px;
321
+ }
322
+
323
+ .thinking-dots span {
324
+ width: 7px;
325
+ height: 7px;
326
+ border-radius: 999px;
327
+ background: #8b98ab;
328
+ opacity: 0.25;
329
+ animation: thinking-dot 1.1s infinite ease-in-out;
330
+ }
331
+
332
+ .thinking-dots span:nth-child(2) {
333
+ animation-delay: 0.15s;
334
+ }
335
+
336
+ .thinking-dots span:nth-child(3) {
337
+ animation-delay: 0.3s;
338
+ }
339
+
340
+ @keyframes thinking-dot {
341
+ 0%, 80%, 100% {
342
+ opacity: 0.2;
343
+ transform: translateY(0);
344
+ }
345
+ 40% {
346
+ opacity: 0.95;
347
+ transform: translateY(-2px);
348
+ }
349
+ }
350
+
351
+ .composer {
352
+ border-top: 1px solid var(--line);
353
+ padding: 14px 20px 16px;
354
+ display: block;
355
+ background: #ffffff;
356
+ }
357
+
358
+ .chat.is-empty .chat-body {
359
+ display: flex;
360
+ flex-direction: column;
361
+ align-items: center;
362
+ justify-content: center;
363
+ padding: 0 20px;
364
+ gap: 22px;
365
+ }
366
+
367
+ .chat.is-empty .messages {
368
+ display: none;
369
+ }
370
+
371
+ .empty-greeting {
372
+ display: none;
373
+ margin: 0;
374
+ text-align: center;
375
+ font-size: clamp(30px, 3.6vw, 54px);
376
+ line-height: 1.08;
377
+ font-weight: 620;
378
+ letter-spacing: -0.02em;
379
+ color: #172034;
380
+ }
381
+
382
+ .chat.is-empty .empty-greeting {
383
+ display: block;
384
+ }
385
+
386
+ .chat.is-empty .composer {
387
+ width: min(860px, 100%);
388
+ border-top: none;
389
+ padding: 0;
390
+ background: transparent;
391
+ }
392
+
393
+ .prompt-wrap {
394
+ position: relative;
395
+ }
396
+
397
+ .composer textarea {
398
+ width: 100%;
399
+ min-height: 56px;
400
+ max-height: 180px;
401
+ resize: vertical;
402
+ border: 1px solid var(--line);
403
+ border-radius: 12px;
404
+ background: #ffffff;
405
+ color: var(--text);
406
+ padding: 11px 74px 14px 12px;
407
+ font: inherit;
408
+ line-height: 1.45;
409
+ box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.02);
410
+ }
411
+
412
+ #send {
413
+ position: absolute;
414
+ right: 10px;
415
+ bottom: 10px;
416
+ border: none;
417
+ border-radius: 999px;
418
+ width: 44px;
419
+ height: 44px;
420
+ padding: 0;
421
+ background: #8e95a4;
422
+ color: #ffffff;
423
+ display: grid;
424
+ place-items: center;
425
+ transition: background-color 0.2s ease;
426
+ }
427
+
428
+ #send:hover {
429
+ background: #7c8393;
430
+ }
431
+
432
+ #send svg {
433
+ width: 22px;
434
+ height: 22px;
435
+ display: block;
436
+ }
437
+
438
+ .error {
439
+ min-height: 20px;
440
+ color: var(--error);
441
+ font-size: 13px;
442
+ padding: 0 20px 14px;
443
+ }
444
+
445
+ @media (max-width: 900px) {
446
+ .layout {
447
+ grid-template-columns: 1fr;
448
+ grid-template-rows: auto 1fr;
449
+ }
450
+
451
+ .sidebar {
452
+ border-right: none;
453
+ border-bottom: 1px solid #e3e8f0;
454
+ }
455
+
456
+ .empty-greeting {
457
+ font-size: clamp(22px, 6.8vw, 34px);
458
+ }
459
+ }
460
+ </style>
461
+ </head>
462
+ <body>
463
+ <div class="layout">
464
+ <aside class="sidebar">
465
+ <div class="sidebar-top">
466
+ <div class="brand">
467
+ <img
468
+ class="brand-logo"
469
+ src="https://hybridai.one/static/hai_logo_free.png"
470
+ alt="HybridAI"
471
+ >
472
+ </div>
473
+ <button id="sidebarToggle" class="sidebar-toggle" aria-label="Toggle sidebar">
474
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
475
+ <rect x="3" y="4" width="18" height="16" rx="4"></rect>
476
+ <path d="M12 4v16"></path>
477
+ </svg>
478
+ </button>
479
+ </div>
480
+
481
+ <button id="newChat" class="new-chat-btn">
482
+ <span class="new-chat-icon">+</span>
483
+ <span class="new-chat-label">New Conversation</span>
484
+ </button>
485
+ <div class="sidebar-spacer"></div>
486
+ <div id="appVersion" class="app-version">HybridClaw v...</div>
487
+ </aside>
488
+
489
+ <main class="chat">
490
+ <div id="chatBody" class="chat-body">
491
+ <section id="messages" class="messages"></section>
492
+ <h1 class="empty-greeting">Ready to claw through your to-do list? 🦞</h1>
493
+ <div class="composer">
494
+ <div class="prompt-wrap">
495
+ <button id="send" aria-label="Send message">
496
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
497
+ <path d="M12 19V5"></path>
498
+ <path d="m5 12 7-7 7 7"></path>
499
+ </svg>
500
+ </button>
501
+ <textarea id="message" placeholder="Message HybridClaw"></textarea>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ <div id="error" class="error"></div>
506
+ </main>
507
+ </div>
508
+
509
+ <script>
510
+ const layoutEl = document.querySelector('.layout');
511
+ const chatEl = document.querySelector('.chat');
512
+ const greetingEl = document.querySelector('.empty-greeting');
513
+ const appVersionEl = document.getElementById('appVersion');
514
+ const messageEl = document.getElementById('message');
515
+ const messagesEl = document.getElementById('messages');
516
+ const errorEl = document.getElementById('error');
517
+ const urlParams = new URLSearchParams(window.location.search);
518
+ const queryToken = urlParams.get('token') || '';
519
+ let activeSessionId = localStorage.getItem('hybridclaw_session') || 'web:default';
520
+ let activeSessionNumber = parseInt(localStorage.getItem('hybridclaw_session_number') || '1', 10);
521
+ if (Number.isNaN(activeSessionNumber) || activeSessionNumber < 1) {
522
+ activeSessionNumber = 1;
523
+ }
524
+ let lastSessionNumber = parseInt(localStorage.getItem('hybridclaw_last_session_number') || String(activeSessionNumber), 10);
525
+ if (Number.isNaN(lastSessionNumber) || lastSessionNumber < activeSessionNumber) {
526
+ lastSessionNumber = activeSessionNumber;
527
+ }
528
+ const apiToken = queryToken || localStorage.getItem('hybridclaw_token') || '';
529
+ const introLines = [
530
+ 'Ready to claw through your to-do list? 🦞',
531
+ 'What should we build, fix, or break first?',
532
+ 'Drop your wildest idea, I’ll sharpen it.',
533
+ 'Stuck? Let’s turn chaos into a plan.',
534
+ 'Tell me the mission and I’ll bring the claws.',
535
+ 'Big question or tiny bug, I’m in.',
536
+ 'What are we shipping today?',
537
+ 'Let’s make something clever in one prompt.',
538
+ 'Hit me with your hardest problem.',
539
+ 'Brainstorm mode activated. What’s first?',
540
+ ];
541
+ let introIndex = 0;
542
+
543
+ function renderIntro() {
544
+ if (greetingEl) {
545
+ greetingEl.textContent = introLines[introIndex];
546
+ }
547
+ }
548
+
549
+ function savePrefs() {
550
+ localStorage.setItem('hybridclaw_session', activeSessionId);
551
+ localStorage.setItem('hybridclaw_session_number', String(activeSessionNumber));
552
+ localStorage.setItem('hybridclaw_last_session_number', String(lastSessionNumber));
553
+ if (apiToken) {
554
+ localStorage.setItem('hybridclaw_token', apiToken);
555
+ }
556
+ }
557
+
558
+ function updateWindowTitle() {
559
+ document.title = `HybridClaw - (${activeSessionNumber})`;
560
+ }
561
+
562
+ function apiHeaders() {
563
+ const headers = { 'Content-Type': 'application/json' };
564
+ if (apiToken) {
565
+ headers.Authorization = `Bearer ${apiToken}`;
566
+ }
567
+ return headers;
568
+ }
569
+
570
+ function setError(text) {
571
+ errorEl.textContent = text || '';
572
+ }
573
+
574
+ function escapeHtml(raw) {
575
+ return String(raw)
576
+ .replace(/&/g, '&amp;')
577
+ .replace(/</g, '&lt;')
578
+ .replace(/>/g, '&gt;')
579
+ .replace(/"/g, '&quot;')
580
+ .replace(/'/g, '&#39;');
581
+ }
582
+
583
+ function sanitizeUrl(rawUrl) {
584
+ const url = String(rawUrl || '').trim();
585
+ if (!url) return '';
586
+ const lowered = url.toLowerCase();
587
+ if (lowered.startsWith('http://') || lowered.startsWith('https://') || lowered.startsWith('mailto:')) {
588
+ return url;
589
+ }
590
+ return '';
591
+ }
592
+
593
+ function renderInlineMarkdown(raw) {
594
+ let text = String(raw || '');
595
+ const inlineCode = [];
596
+ const links = [];
597
+
598
+ text = text.replace(/`([^`\n]+)`/g, (_match, code) => {
599
+ const idx = inlineCode.push(`<code>${escapeHtml(code)}</code>`) - 1;
600
+ return `@@IC${idx}@@`;
601
+ });
602
+
603
+ text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
604
+ const safeHref = sanitizeUrl(href);
605
+ const safeLabel = escapeHtml(label);
606
+ const html = safeHref
607
+ ? `<a href="${escapeHtml(safeHref)}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
608
+ : safeLabel;
609
+ const idx = links.push(html) - 1;
610
+ return `@@LK${idx}@@`;
611
+ });
612
+
613
+ text = escapeHtml(text);
614
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
615
+ text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
616
+ text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
617
+ text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
618
+
619
+ text = text.replace(/@@LK(\d+)@@/g, (_match, index) => links[Number(index)] || '');
620
+ text = text.replace(/@@IC(\d+)@@/g, (_match, index) => inlineCode[Number(index)] || '');
621
+ return text;
622
+ }
623
+
624
+ function renderMarkdown(raw) {
625
+ let text = String(raw || '').replace(/\r\n/g, '\n');
626
+ const codeBlocks = [];
627
+
628
+ text = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, (_match, _lang, code) => {
629
+ const idx = codeBlocks.push(`<pre><code>${escapeHtml(code.replace(/\n$/, ''))}</code></pre>`) - 1;
630
+ return `@@CB${idx}@@`;
631
+ });
632
+
633
+ const lines = text.split('\n');
634
+ const out = [];
635
+ let paragraphLines = [];
636
+ let openList = '';
637
+
638
+ const closeList = () => {
639
+ if (openList) {
640
+ out.push(openList === 'ul' ? '</ul>' : '</ol>');
641
+ openList = '';
642
+ }
643
+ };
644
+
645
+ const flushParagraph = () => {
646
+ if (paragraphLines.length === 0) return;
647
+ const html = paragraphLines.map((line) => renderInlineMarkdown(line)).join('<br>');
648
+ out.push(`<p>${html}</p>`);
649
+ paragraphLines = [];
650
+ };
651
+
652
+ for (const line of lines) {
653
+ const codeMatch = line.match(/^@@CB(\d+)@@$/);
654
+ if (codeMatch) {
655
+ flushParagraph();
656
+ closeList();
657
+ out.push(codeBlocks[Number(codeMatch[1])] || '');
658
+ continue;
659
+ }
660
+
661
+ if (!line.trim()) {
662
+ flushParagraph();
663
+ closeList();
664
+ continue;
665
+ }
666
+
667
+ const heading = line.match(/^(#{1,3})\s+(.*)$/);
668
+ if (heading) {
669
+ flushParagraph();
670
+ closeList();
671
+ const level = heading[1].length;
672
+ out.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
673
+ continue;
674
+ }
675
+
676
+ const quote = line.match(/^>\s?(.*)$/);
677
+ if (quote) {
678
+ flushParagraph();
679
+ closeList();
680
+ out.push(`<blockquote>${renderInlineMarkdown(quote[1])}</blockquote>`);
681
+ continue;
682
+ }
683
+
684
+ const hline = line.match(/^\s*((\*\s*){3,}|(-\s*){3,}|(_\s*){3,})\s*$/);
685
+ if (hline) {
686
+ flushParagraph();
687
+ closeList();
688
+ out.push('<hr>');
689
+ continue;
690
+ }
691
+
692
+ const unordered = line.match(/^\s*[-*+]\s+(.*)$/);
693
+ if (unordered) {
694
+ flushParagraph();
695
+ if (openList !== 'ul') {
696
+ closeList();
697
+ out.push('<ul>');
698
+ openList = 'ul';
699
+ }
700
+ out.push(`<li>${renderInlineMarkdown(unordered[1])}</li>`);
701
+ continue;
702
+ }
703
+
704
+ const ordered = line.match(/^\s*\d+\.\s+(.*)$/);
705
+ if (ordered) {
706
+ flushParagraph();
707
+ if (openList !== 'ol') {
708
+ closeList();
709
+ out.push('<ol>');
710
+ openList = 'ol';
711
+ }
712
+ out.push(`<li>${renderInlineMarkdown(ordered[1])}</li>`);
713
+ continue;
714
+ }
715
+
716
+ closeList();
717
+ paragraphLines.push(line);
718
+ }
719
+
720
+ flushParagraph();
721
+ closeList();
722
+ return out.join('');
723
+ }
724
+
725
+ function copyTextFallback(text) {
726
+ const helper = document.createElement('textarea');
727
+ helper.value = text;
728
+ helper.setAttribute('readonly', '');
729
+ helper.style.position = 'absolute';
730
+ helper.style.left = '-9999px';
731
+ document.body.appendChild(helper);
732
+ helper.select();
733
+ document.execCommand('copy');
734
+ document.body.removeChild(helper);
735
+ }
736
+
737
+ async function copyMessage(content, button) {
738
+ try {
739
+ if (navigator.clipboard && navigator.clipboard.writeText) {
740
+ await navigator.clipboard.writeText(content);
741
+ } else {
742
+ copyTextFallback(content);
743
+ }
744
+ button.textContent = '✓';
745
+ button.classList.add('copied');
746
+ window.setTimeout(() => {
747
+ button.textContent = '⧉';
748
+ button.classList.remove('copied');
749
+ }, 900);
750
+ } catch {
751
+ copyTextFallback(content);
752
+ }
753
+ }
754
+
755
+ function setAppVersion(version) {
756
+ if (!appVersionEl) return;
757
+ if (!version) {
758
+ appVersionEl.textContent = 'HybridClaw v...';
759
+ return;
760
+ }
761
+ appVersionEl.textContent = `HybridClaw v.${String(version).replace(/^v/i, '')}`;
762
+ }
763
+
764
+ function updateEmptyState() {
765
+ const hasMessages = messagesEl.children.length > 0;
766
+ chatEl.classList.toggle('is-empty', !hasMessages);
767
+ }
768
+
769
+ function addMessage(role, content) {
770
+ const item = document.createElement('div');
771
+ item.className = `msg ${role}`;
772
+ if (role === 'assistant') {
773
+ item.classList.add('markdown');
774
+ item.innerHTML = renderMarkdown(content || '');
775
+ } else {
776
+ item.textContent = content || '';
777
+ }
778
+ if (role === 'assistant') {
779
+ const block = document.createElement('div');
780
+ block.className = 'msg-block';
781
+ block.appendChild(item);
782
+
783
+ const actions = document.createElement('div');
784
+ actions.className = 'msg-actions';
785
+ const copy = document.createElement('button');
786
+ copy.type = 'button';
787
+ copy.className = 'copy-btn';
788
+ copy.setAttribute('aria-label', 'Copy response');
789
+ copy.title = 'Copy response';
790
+ copy.textContent = '⧉';
791
+ copy.addEventListener('click', () => {
792
+ void copyMessage(content || '', copy);
793
+ });
794
+ actions.appendChild(copy);
795
+ block.appendChild(actions);
796
+ messagesEl.appendChild(block);
797
+ } else {
798
+ messagesEl.appendChild(item);
799
+ }
800
+ updateEmptyState();
801
+ messagesEl.scrollTop = messagesEl.scrollHeight;
802
+ }
803
+
804
+ function addThinkingMessage() {
805
+ const item = document.createElement('div');
806
+ item.className = 'msg assistant thinking';
807
+ const dots = document.createElement('span');
808
+ dots.className = 'thinking-dots';
809
+ for (let i = 0; i < 3; i += 1) {
810
+ dots.appendChild(document.createElement('span'));
811
+ }
812
+ item.appendChild(dots);
813
+ messagesEl.appendChild(item);
814
+ updateEmptyState();
815
+ messagesEl.scrollTop = messagesEl.scrollHeight;
816
+ return item;
817
+ }
818
+
819
+ async function requestJson(url, options) {
820
+ const response = await fetch(url, options);
821
+ const payload = await response.json().catch(() => ({}));
822
+ if (!response.ok) {
823
+ throw new Error(payload.error || `${response.status} ${response.statusText}`);
824
+ }
825
+ return payload;
826
+ }
827
+
828
+ async function loadHistory() {
829
+ savePrefs();
830
+ setError('');
831
+ messagesEl.innerHTML = '';
832
+ updateEmptyState();
833
+ try {
834
+ const sessionId = encodeURIComponent(activeSessionId);
835
+ const payload = await requestJson(`/api/history?sessionId=${sessionId}&limit=80`, {
836
+ method: 'GET',
837
+ headers: apiHeaders(),
838
+ });
839
+ for (const msg of payload.history || []) {
840
+ addMessage(msg.role, msg.content);
841
+ }
842
+ } catch (err) {
843
+ setError(err instanceof Error ? err.message : String(err));
844
+ }
845
+ }
846
+
847
+ async function loadStatus() {
848
+ try {
849
+ const payload = await requestJson('/api/status', {
850
+ method: 'GET',
851
+ headers: apiHeaders(),
852
+ });
853
+ setAppVersion(payload.version || '');
854
+ } catch {
855
+ setAppVersion('');
856
+ }
857
+ }
858
+
859
+ async function sendMessage() {
860
+ const content = messageEl.value.trim();
861
+ if (!content) return;
862
+
863
+ savePrefs();
864
+ setError('');
865
+ addMessage('user', content);
866
+ messageEl.value = '';
867
+ const thinkingItem = addThinkingMessage();
868
+
869
+ try {
870
+ const payload = await requestJson('/api/chat', {
871
+ method: 'POST',
872
+ headers: apiHeaders(),
873
+ body: JSON.stringify({
874
+ sessionId: activeSessionId,
875
+ channelId: 'web',
876
+ userId: 'web-user',
877
+ username: 'web',
878
+ content,
879
+ }),
880
+ });
881
+ if (payload.status === 'error') {
882
+ throw new Error(payload.error || 'Unknown error');
883
+ }
884
+ thinkingItem.remove();
885
+ addMessage('assistant', payload.result || '');
886
+ } catch (err) {
887
+ thinkingItem.remove();
888
+ const text = err instanceof Error ? err.message : String(err);
889
+ setError(text);
890
+ addMessage('system', `Error: ${text}`);
891
+ }
892
+ }
893
+
894
+ function newChat() {
895
+ lastSessionNumber += 1;
896
+ activeSessionNumber = lastSessionNumber;
897
+ activeSessionId = `web:${Date.now()}`;
898
+ messageEl.value = '';
899
+ messagesEl.innerHTML = '';
900
+ updateEmptyState();
901
+ updateWindowTitle();
902
+ savePrefs();
903
+ setError('');
904
+ messageEl.focus();
905
+ }
906
+
907
+ document.getElementById('send').addEventListener('click', () => {
908
+ void sendMessage();
909
+ });
910
+ document.getElementById('newChat').addEventListener('click', newChat);
911
+ document.getElementById('sidebarToggle').addEventListener('click', () => {
912
+ layoutEl.classList.toggle('is-sidebar-collapsed');
913
+ });
914
+ messageEl.addEventListener('keydown', (event) => {
915
+ if (event.key === 'Enter' && !event.shiftKey) {
916
+ event.preventDefault();
917
+ void sendMessage();
918
+ }
919
+ });
920
+ introIndex = Math.floor(Math.random() * introLines.length);
921
+ renderIntro();
922
+ updateEmptyState();
923
+ updateWindowTitle();
924
+ savePrefs();
925
+ void loadStatus();
926
+ void loadHistory();
927
+ </script>
928
+ </body>
929
+ </html>