mindcraft 0.1.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 (116) hide show
  1. package/FAQ.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +255 -0
  4. package/andy.json +6 -0
  5. package/bin/mindcraft.js +80 -0
  6. package/keys.example.json +19 -0
  7. package/main.js +80 -0
  8. package/package.json +78 -0
  9. package/patches/minecraft-data+3.97.0.patch +13 -0
  10. package/patches/mineflayer+4.33.0.patch +54 -0
  11. package/patches/mineflayer-pathfinder+2.4.5.patch +265 -0
  12. package/patches/mineflayer-pvp+1.3.2.patch +13 -0
  13. package/patches/prismarine-viewer+1.33.0.patch +13 -0
  14. package/patches/protodef+1.19.0.patch +15 -0
  15. package/profiles/andy-4-reasoning.json +14 -0
  16. package/profiles/andy-4.json +7 -0
  17. package/profiles/azure.json +19 -0
  18. package/profiles/claude.json +7 -0
  19. package/profiles/claude_thinker.json +15 -0
  20. package/profiles/deepseek.json +7 -0
  21. package/profiles/defaults/_default.json +256 -0
  22. package/profiles/defaults/assistant.json +14 -0
  23. package/profiles/defaults/creative.json +14 -0
  24. package/profiles/defaults/god_mode.json +14 -0
  25. package/profiles/defaults/survival.json +14 -0
  26. package/profiles/freeguy.json +7 -0
  27. package/profiles/gemini.json +9 -0
  28. package/profiles/gpt.json +12 -0
  29. package/profiles/grok.json +7 -0
  30. package/profiles/llama.json +10 -0
  31. package/profiles/mercury.json +9 -0
  32. package/profiles/mistral.json +5 -0
  33. package/profiles/qwen.json +17 -0
  34. package/profiles/tasks/construction_profile.json +42 -0
  35. package/profiles/tasks/cooking_profile.json +11 -0
  36. package/profiles/tasks/crafting_profile.json +71 -0
  37. package/profiles/vllm.json +10 -0
  38. package/settings.js +64 -0
  39. package/src/agent/action_manager.js +177 -0
  40. package/src/agent/agent.js +561 -0
  41. package/src/agent/coder.js +229 -0
  42. package/src/agent/commands/actions.js +504 -0
  43. package/src/agent/commands/index.js +259 -0
  44. package/src/agent/commands/queries.js +347 -0
  45. package/src/agent/connection_handler.js +96 -0
  46. package/src/agent/conversation.js +353 -0
  47. package/src/agent/history.js +122 -0
  48. package/src/agent/library/full_state.js +89 -0
  49. package/src/agent/library/index.js +23 -0
  50. package/src/agent/library/lockdown.js +32 -0
  51. package/src/agent/library/skill_library.js +93 -0
  52. package/src/agent/library/skills.js +2093 -0
  53. package/src/agent/library/world.js +431 -0
  54. package/src/agent/memory_bank.js +25 -0
  55. package/src/agent/mindserver_proxy.js +136 -0
  56. package/src/agent/modes.js +446 -0
  57. package/src/agent/npc/build_goal.js +80 -0
  58. package/src/agent/npc/construction/dirt_shelter.json +38 -0
  59. package/src/agent/npc/construction/large_house.json +230 -0
  60. package/src/agent/npc/construction/small_stone_house.json +42 -0
  61. package/src/agent/npc/construction/small_wood_house.json +42 -0
  62. package/src/agent/npc/controller.js +261 -0
  63. package/src/agent/npc/data.js +50 -0
  64. package/src/agent/npc/item_goal.js +355 -0
  65. package/src/agent/npc/utils.js +126 -0
  66. package/src/agent/self_prompter.js +146 -0
  67. package/src/agent/settings.js +7 -0
  68. package/src/agent/speak.js +150 -0
  69. package/src/agent/tasks/construction_tasks.js +1104 -0
  70. package/src/agent/tasks/cooking_tasks.js +358 -0
  71. package/src/agent/tasks/tasks.js +594 -0
  72. package/src/agent/templates/execTemplate.js +6 -0
  73. package/src/agent/templates/lintTemplate.js +10 -0
  74. package/src/agent/vision/browser_viewer.js +8 -0
  75. package/src/agent/vision/camera.js +78 -0
  76. package/src/agent/vision/vision_interpreter.js +82 -0
  77. package/src/mindcraft/index.js +28 -0
  78. package/src/mindcraft/mcserver.js +154 -0
  79. package/src/mindcraft/mindcraft.js +111 -0
  80. package/src/mindcraft/mindserver.js +328 -0
  81. package/src/mindcraft/public/index.html +1253 -0
  82. package/src/mindcraft/public/settings_spec.json +145 -0
  83. package/src/mindcraft/userconfig.js +72 -0
  84. package/src/mindcraft-py/example.py +27 -0
  85. package/src/mindcraft-py/init-mindcraft.js +24 -0
  86. package/src/mindcraft-py/mindcraft.py +99 -0
  87. package/src/models/_model_map.js +89 -0
  88. package/src/models/azure.js +32 -0
  89. package/src/models/cerebras.js +61 -0
  90. package/src/models/claude.js +87 -0
  91. package/src/models/deepseek.js +59 -0
  92. package/src/models/gemini.js +176 -0
  93. package/src/models/glhf.js +71 -0
  94. package/src/models/gpt.js +147 -0
  95. package/src/models/grok.js +82 -0
  96. package/src/models/groq.js +95 -0
  97. package/src/models/huggingface.js +86 -0
  98. package/src/models/hyperbolic.js +114 -0
  99. package/src/models/lmstudio.js +74 -0
  100. package/src/models/mercury.js +95 -0
  101. package/src/models/mistral.js +94 -0
  102. package/src/models/novita.js +71 -0
  103. package/src/models/ollama.js +115 -0
  104. package/src/models/openrouter.js +77 -0
  105. package/src/models/prompter.js +366 -0
  106. package/src/models/qwen.js +80 -0
  107. package/src/models/replicate.js +60 -0
  108. package/src/models/vllm.js +81 -0
  109. package/src/process/agent_process.js +84 -0
  110. package/src/process/init_agent.js +54 -0
  111. package/src/utils/examples.js +83 -0
  112. package/src/utils/keys.js +34 -0
  113. package/src/utils/math.js +13 -0
  114. package/src/utils/mcdata.js +572 -0
  115. package/src/utils/text.js +78 -0
  116. package/src/utils/translator.js +30 -0
@@ -0,0 +1,1253 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Mindcraft</title>
5
+ <script src="/socket.io/socket.io.js"></script>
6
+ <style>
7
+ body {
8
+ font-family: Arial, sans-serif;
9
+ margin: 20px;
10
+ background: #1a1a1a;
11
+ color: #e0e0e0;
12
+ }
13
+ #agents {
14
+ background: #2d2d2d;
15
+ padding: 20px;
16
+ border-radius: 8px;
17
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
18
+ }
19
+ h1 {
20
+ color: #ffffff;
21
+ }
22
+ .agent {
23
+ margin: 10px 0;
24
+ padding: 10px;
25
+ background: #363636;
26
+ border-radius: 4px;
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: stretch;
30
+ }
31
+ .restart-btn, .start-btn, .stop-btn {
32
+ color: white;
33
+ border: none;
34
+ padding: 5px 10px;
35
+ border-radius: 4px;
36
+ cursor: pointer;
37
+ margin-left: 5px;
38
+ }
39
+ .restart-btn {
40
+ background: #4CAF50;
41
+ }
42
+ .start-btn {
43
+ background: #2196F3;
44
+ }
45
+ .stop-btn {
46
+ background: #f44336;
47
+ }
48
+ .restart-btn:hover { background: #45a049; }
49
+ .start-btn:hover { background: #1976D2; }
50
+ .stop-btn:hover { background: #d32f2f; }
51
+ .gear-btn {
52
+ background: #505050;
53
+ color: #fff;
54
+ border: none;
55
+ border-radius: 4px;
56
+ padding: 2px 6px;
57
+ cursor: pointer;
58
+ margin-left: 6px;
59
+ font-size: 0.9em;
60
+ }
61
+ .gear-btn:hover { background: #5a5a5a; }
62
+ .status-icon {
63
+ font-size: 12px;
64
+ margin-right: 8px;
65
+ }
66
+ .status-icon.online {
67
+ color: #4CAF50;
68
+ }
69
+ .status-icon.offline {
70
+ color: #f44336;
71
+ }
72
+ #settingsForm {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
75
+ gap: 8px;
76
+ margin-top: 10px;
77
+ }
78
+ .setting-wrapper {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ background: #3a3a3a;
83
+ padding: 6px 8px;
84
+ border-radius: 4px;
85
+ width: 100%;
86
+ box-sizing: border-box;
87
+ min-width: 0;
88
+ }
89
+ .setting-wrapper label {
90
+ flex: 0 0 50%;
91
+ font-size: 0.9em;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+ .setting-wrapper input[type="text"],
97
+ .setting-wrapper input[type="number"] {
98
+ flex: 1 1 0;
99
+ background: #262626;
100
+ border: 1px solid #555;
101
+ color: #e0e0e0;
102
+ border-radius: 4px;
103
+ padding: 4px 6px;
104
+ max-width: 100%;
105
+ min-width: 0;
106
+ }
107
+ .setting-wrapper input[type="checkbox"] {
108
+ transform: scale(1.2);
109
+ }
110
+ .agent-view-container {
111
+ width: 100%;
112
+ height: 100%;
113
+ aspect-ratio: 4/3;
114
+ overflow: hidden;
115
+ }
116
+ .agent-viewer {
117
+ width: 100%;
118
+ height: 100%;
119
+ border: none;
120
+ display: block;
121
+ }
122
+ .last-message {
123
+ font-style: italic;
124
+ color: #aaa;
125
+ margin-top: 5px;
126
+ white-space: nowrap;
127
+ overflow: hidden;
128
+ text-overflow: ellipsis;
129
+ width: 100%;
130
+ }
131
+ .start-btn:disabled {
132
+ opacity: 0.4;
133
+ cursor: not-allowed;
134
+ }
135
+ .agent-view-container {
136
+ margin-top: 6px;
137
+ display: flex;
138
+ justify-content: flex-start;
139
+ }
140
+ .agent-grid {
141
+ display: grid;
142
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
143
+ gap: 8px;
144
+ width: 100%;
145
+ align-items: start;
146
+ }
147
+ .agent-grid .cell {
148
+ background: #3a3a3a;
149
+ padding: 6px 8px;
150
+ border-radius: 4px;
151
+ }
152
+ .agent-grid .cell.title {
153
+ background: transparent;
154
+ padding: 0;
155
+ }
156
+ .agent-inventory {
157
+ margin-top: 8px;
158
+ background: #2f2f2f;
159
+ border-radius: 6px;
160
+ padding: 8px;
161
+ }
162
+ .agent-inventory h3 { margin: 0 0 6px 0; font-size: 1em; }
163
+ .inventory-grid {
164
+ display: grid;
165
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
166
+ gap: 6px;
167
+ }
168
+ .agent-details-grid {
169
+ display: grid;
170
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
171
+ gap: 6px;
172
+ width: 100%;
173
+ }
174
+ .controls-row {
175
+ margin-top: 8px;
176
+ display: grid;
177
+ grid-template-columns: auto minmax(100px, 1fr) repeat(5, auto);
178
+ gap: 8px;
179
+ align-items: center;
180
+ }
181
+ .msg-input {
182
+ width: calc(100% - 8px);
183
+ background: #262626;
184
+ border: 1px solid #555;
185
+ color: #e0e0e0;
186
+ border-radius: 4px;
187
+ padding: 4px 6px;
188
+ }
189
+ .neutral-btn {
190
+ color: white;
191
+ border: none;
192
+ padding: 5px 10px;
193
+ border-radius: 4px;
194
+ cursor: pointer;
195
+ margin-left: 5px;
196
+ background: #505050;
197
+ }
198
+ .neutral-btn:hover { background: #5a5a5a; }
199
+ .neutral-btn:disabled {
200
+ background: #383838;
201
+ color: #666;
202
+ cursor: not-allowed;
203
+ }
204
+ .msg-input:disabled {
205
+ background: #1a1a1a;
206
+ color: #666;
207
+ cursor: not-allowed;
208
+ }
209
+ .page-footer {
210
+ position: fixed;
211
+ bottom: 0;
212
+ left: 0;
213
+ right: 0;
214
+ background: #2d2d2d;
215
+ padding: 16px;
216
+ display: flex;
217
+ gap: 12px;
218
+ justify-content: space-between;
219
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
220
+ }
221
+ body {
222
+ padding-bottom: 80px; /* Make room for footer */
223
+ }
224
+ .agent-stats-row {
225
+ display: grid;
226
+ grid-template-columns: repeat(5, 1fr);
227
+ gap: 8px;
228
+ margin: 6px 0;
229
+ font-size: 0.9em;
230
+ color: #cccccc;
231
+ width: 100%;
232
+ box-sizing: border-box;
233
+ }
234
+ .agent-stats-row .stat {
235
+ background: #3a3a3a;
236
+ padding: 6px 8px;
237
+ border-radius: 4px;
238
+ }
239
+ .status-badge {
240
+ font-size: 0.75em;
241
+ margin-left: 8px;
242
+ padding: 2px 6px;
243
+ border-radius: 4px;
244
+ background: #3a3a3a;
245
+ color: #cccccc;
246
+ text-transform: lowercase;
247
+ }
248
+ .status-badge.online { color: #4CAF50; }
249
+ .status-badge.offline { color: #f44336; }
250
+ .title-row {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 10px;
254
+ }
255
+ .title-left {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 8px;
259
+ }
260
+ .title-spacer { flex: 1; }
261
+ /* Modal styles */
262
+ .modal-backdrop {
263
+ position: fixed;
264
+ top: 0; left: 0; right: 0; bottom: 0;
265
+ background: rgba(0,0,0,0.5);
266
+ display: none;
267
+ align-items: center;
268
+ justify-content: center;
269
+ z-index: 1000;
270
+ }
271
+ .modal {
272
+ background: #2d2d2d;
273
+ border-radius: 8px;
274
+ width: 80vw;
275
+ height: 80vh;
276
+ display: flex;
277
+ flex-direction: column;
278
+ overflow: hidden;
279
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
280
+ }
281
+ .modal-header {
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: space-between;
285
+ padding: 12px 16px;
286
+ border-bottom: 1px solid #3a3a3a;
287
+ }
288
+ .modal-close-btn {
289
+ background: #f44336;
290
+ color: #fff;
291
+ border: none;
292
+ border-radius: 4px;
293
+ padding: 4px 8px;
294
+ cursor: pointer;
295
+ }
296
+ .modal-body {
297
+ flex: 1 1 auto;
298
+ overflow: auto;
299
+ padding: 12px 16px;
300
+ }
301
+ .modal-footer {
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: space-between;
305
+ padding: 12px 16px;
306
+ border-top: 1px solid #3a3a3a;
307
+ }
308
+ .footer-left { color: #cccccc; font-style: italic; }
309
+ /* Setup wizard */
310
+ .wizard-steps {
311
+ display: flex;
312
+ gap: 8px;
313
+ margin-bottom: 16px;
314
+ font-size: 0.85em;
315
+ }
316
+ .wizard-steps .step {
317
+ flex: 1;
318
+ padding: 6px 8px;
319
+ background: #3a3a3a;
320
+ border-radius: 4px;
321
+ text-align: center;
322
+ color: #888;
323
+ }
324
+ .wizard-steps .step.active { background: #2196F3; color: #fff; }
325
+ .wizard-steps .step.done { background: #4CAF50; color: #fff; }
326
+ .wizard-screen { display: none; }
327
+ .wizard-screen.active { display: block; }
328
+ .wizard-field {
329
+ display: flex;
330
+ flex-direction: column;
331
+ gap: 4px;
332
+ margin-bottom: 14px;
333
+ }
334
+ .wizard-field label { font-size: 0.9em; color: #ccc; }
335
+ .wizard-field input, .wizard-field select, .wizard-field textarea {
336
+ background: #262626;
337
+ border: 1px solid #555;
338
+ color: #e0e0e0;
339
+ border-radius: 4px;
340
+ padding: 8px 10px;
341
+ font-size: 1em;
342
+ font-family: inherit;
343
+ }
344
+ .wizard-field .hint { font-size: 0.8em; color: #888; }
345
+ .wizard-row { display: flex; gap: 12px; }
346
+ .wizard-row .wizard-field { flex: 1; }
347
+ .wizard-intro { color: #ccc; line-height: 1.5; margin-bottom: 20px; }
348
+ </style>
349
+ </head>
350
+ <body>
351
+ <div class="title-row">
352
+ <div class="title-left">
353
+ <h1 style="margin: 0;">Mindcraft</h1>
354
+ <span id="msStatus" class="status-badge offline">mindserver offline</span>
355
+ </div>
356
+ <div class="title-spacer"></div>
357
+ <button id="openSetupBtn" class="neutral-btn">⚙ Setup</button>
358
+ </div>
359
+ <div id="agents"></div>
360
+
361
+ <div class="page-footer">
362
+ <div>
363
+ <button id="openCreateAgentBtn" class="start-btn">New Agent</button>
364
+ </div>
365
+ <div style="display:flex; gap:12px;">
366
+ <button class="stop-btn" onclick="disconnectAllAgents()">Disconnect All Agents</button>
367
+ <button class="stop-btn" onclick="confirmShutdown()">Full Shutdown</button>
368
+ </div>
369
+ </div>
370
+
371
+ <!-- Modal -->
372
+ <div id="createAgentModal" class="modal-backdrop">
373
+ <div class="modal">
374
+ <div class="modal-header">
375
+ <h2 style="margin:0;">Create Agent</h2>
376
+ <button id="closeCreateAgentBtn" class="modal-close-btn">Close</button>
377
+ </div>
378
+ <div class="modal-body">
379
+ <div id="createAgentSection">
380
+ <div id="profileStatus" style="margin:6px 0;">Profile: Not uploaded</div>
381
+ <div id="settingsForm"></div>
382
+ <div id="createError" style="color:#f44336;margin-top:10px;"></div>
383
+ <input type="file" id="profileFileInput" accept=".json,application/json" style="display:none">
384
+ </div>
385
+ </div>
386
+ <div class="modal-footer">
387
+ <div class="footer-left" id="footerStatus">Configure settings, then upload a profile and create the agent.</div>
388
+ <div class="footer-actions">
389
+ <button id="uploadProfileBtn" class="start-btn">Upload Profile</button>
390
+ <button id="submitCreateAgentBtn" class="start-btn" disabled>Create Agent</button>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <!-- Agent Settings Modal -->
397
+ <div id="agentSettingsModal" class="modal-backdrop">
398
+ <div class="modal">
399
+ <div class="modal-header">
400
+ <h2 id="agentSettingsTitle" style="margin:0;">Agent Settings</h2>
401
+ <button id="closeAgentSettingsBtn" class="modal-close-btn">Close</button>
402
+ </div>
403
+ <div class="modal-body">
404
+ <div id="agentSettingsSection">
405
+ <div id="agentSettingsForm"></div>
406
+ <div id="agentSettingsError" style="color:#f44336;margin-top:10px;"></div>
407
+ </div>
408
+ </div>
409
+ <div class="modal-footer">
410
+ <div class="footer-left" id="agentSettingsFooter">Modify settings then apply to restart the agent.</div>
411
+ <div class="footer-actions">
412
+ <button id="discardAgentSettingsBtn" class="stop-btn" style="background:#777;">Discard Changes</button>
413
+ <button id="applyAgentSettingsBtn" class="start-btn" disabled>Apply & Restart</button>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+
419
+ <!-- Setup wizard -->
420
+ <div id="setupWizardModal" class="modal-backdrop">
421
+ <div class="modal" style="max-width: 720px; height: auto; max-height: 80vh;">
422
+ <div class="modal-header">
423
+ <h2 style="margin:0;">Setup</h2>
424
+ <button id="closeSetupBtn" class="modal-close-btn">Close</button>
425
+ </div>
426
+ <div class="modal-body">
427
+ <div class="wizard-steps">
428
+ <div class="step" data-step="0">Welcome</div>
429
+ <div class="step" data-step="1">API key</div>
430
+ <div class="step" data-step="2">Server</div>
431
+ <div class="step" data-step="3">Bot</div>
432
+ <div class="step" data-step="4">Launch</div>
433
+ </div>
434
+
435
+ <div class="wizard-screen" data-screen="0">
436
+ <p class="wizard-intro">
437
+ Mindcraft connects an LLM-powered agent to your Minecraft server.
438
+ This wizard will collect your API key, server address, and bot
439
+ settings, save them to <code>~/.config/mindcraft/</code>, and launch the bot.
440
+ </p>
441
+ <div class="wizard-field">
442
+ <label>LLM provider</label>
443
+ <select id="wizProvider">
444
+ <option value="ANTHROPIC_API_KEY" selected>Anthropic (Claude)</option>
445
+ <option value="OPENAI_API_KEY">OpenAI</option>
446
+ <option value="GEMINI_API_KEY">Google Gemini</option>
447
+ <option value="GROQCLOUD_API_KEY">Groq</option>
448
+ <option value="REPLICATE_API_KEY">Replicate</option>
449
+ <option value="OPENROUTER_API_KEY">OpenRouter</option>
450
+ </select>
451
+ <span class="hint">Determines which API key you'll enter next, and the model list.</span>
452
+ </div>
453
+ </div>
454
+
455
+ <div class="wizard-screen" data-screen="1">
456
+ <div class="wizard-field">
457
+ <label id="wizKeyLabel">ANTHROPIC_API_KEY</label>
458
+ <input id="wizApiKey" type="password" placeholder="sk-ant-..." autocomplete="off">
459
+ <span class="hint">Stored to <code>~/.config/mindcraft/keys.json</code> (mode 0600). Never sent anywhere except your chosen provider.</span>
460
+ </div>
461
+ </div>
462
+
463
+ <div class="wizard-screen" data-screen="2">
464
+ <div class="wizard-row">
465
+ <div class="wizard-field">
466
+ <label>Host</label>
467
+ <input id="wizHost" type="text" value="localhost">
468
+ </div>
469
+ <div class="wizard-field" style="max-width:140px;">
470
+ <label>Port</label>
471
+ <input id="wizPort" type="number" value="25565">
472
+ </div>
473
+ </div>
474
+ <div class="wizard-row">
475
+ <div class="wizard-field">
476
+ <label>Auth</label>
477
+ <select id="wizAuth">
478
+ <option value="offline" selected>offline</option>
479
+ <option value="microsoft">microsoft</option>
480
+ </select>
481
+ </div>
482
+ <div class="wizard-field">
483
+ <label>Minecraft version</label>
484
+ <input id="wizVersion" type="text" value="auto">
485
+ <span class="hint">"auto" detects from the server.</span>
486
+ </div>
487
+ </div>
488
+ </div>
489
+
490
+ <div class="wizard-screen" data-screen="3">
491
+ <div class="wizard-row">
492
+ <div class="wizard-field">
493
+ <label>Bot name</label>
494
+ <input id="wizBotName" type="text" value="Claude">
495
+ <span class="hint">Also the bot's Minecraft username.</span>
496
+ </div>
497
+ <div class="wizard-field">
498
+ <label>Model</label>
499
+ <select id="wizModel"></select>
500
+ </div>
501
+ </div>
502
+ <div class="wizard-field">
503
+ <label>Base profile</label>
504
+ <select id="wizBaseProfile">
505
+ <option value="assistant" selected>assistant</option>
506
+ <option value="survival">survival</option>
507
+ <option value="creative">creative</option>
508
+ <option value="god_mode">god_mode</option>
509
+ </select>
510
+ </div>
511
+ <div class="wizard-field">
512
+ <label>Personality (optional)</label>
513
+ <textarea id="wizPersonality" rows="3" placeholder="e.g. You are a cheerful builder who loves redstone."></textarea>
514
+ <span class="hint">Prepended to the bot's system prompt.</span>
515
+ </div>
516
+ </div>
517
+
518
+ <div class="wizard-screen" data-screen="4">
519
+ <p class="wizard-intro">Ready to launch.</p>
520
+ <pre id="wizSummary" style="background:#262626;padding:12px;border-radius:4px;overflow:auto;"></pre>
521
+ </div>
522
+
523
+ <div id="wizError" style="color:#f44336;margin-top:10px;"></div>
524
+ </div>
525
+ <div class="modal-footer">
526
+ <button id="wizBackBtn" class="neutral-btn">Back</button>
527
+ <button id="wizNextBtn" class="start-btn">Next</button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+
532
+ <script>
533
+ const socket = io({ path: window.location.pathname + 'socket.io' })
534
+ const agentsDiv = document.getElementById('agents');
535
+ let settingsSpec = {};
536
+ let profileData = null;
537
+ const agentSettings = {};
538
+ const agentLastMessage = {};
539
+ const inventoryOpen = {};
540
+ let currentAgents = [];
541
+
542
+ const statusEl = document.getElementById('msStatus');
543
+ function updateStatus(connected) {
544
+ if (!statusEl) return;
545
+ if (connected) {
546
+ statusEl.textContent = 'MindServer online';
547
+ statusEl.classList.remove('offline');
548
+ statusEl.classList.add('online');
549
+ } else {
550
+ statusEl.textContent = 'MindServer offline';
551
+ statusEl.classList.remove('online');
552
+ statusEl.classList.add('offline');
553
+ }
554
+ }
555
+ function subscribeToState() {
556
+ socket.emit('listen-to-agents');
557
+ }
558
+ // Initial status
559
+ updateStatus(false);
560
+ socket.on('connect', () => {
561
+ updateStatus(true);
562
+ subscribeToState();
563
+ // Clear all cached settings on reconnect
564
+ Object.keys(agentSettings).forEach(name => delete agentSettings[name]);
565
+ });
566
+ socket.on('disconnect', () => {
567
+ updateStatus(false);
568
+ });
569
+ socket.on('connect_error', () => {
570
+ updateStatus(false);
571
+ });
572
+
573
+ // ---- Setup wizard ----
574
+ const wizard = (() => {
575
+ const MODELS = {
576
+ ANTHROPIC_API_KEY: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-3-5-haiku-latest'],
577
+ OPENAI_API_KEY: ['gpt-4o', 'gpt-4o-mini', 'o1-mini'],
578
+ GEMINI_API_KEY: ['gemini-1.5-pro', 'gemini-1.5-flash'],
579
+ GROQCLOUD_API_KEY: ['groq/llama-3.1-70b-versatile', 'groq/mixtral-8x7b-32768'],
580
+ REPLICATE_API_KEY: ['meta/meta-llama-3-70b-instruct'],
581
+ OPENROUTER_API_KEY:['openrouter/anthropic/claude-sonnet-4-6'],
582
+ };
583
+ const modal = document.getElementById('setupWizardModal');
584
+ const screens = [...modal.querySelectorAll('.wizard-screen')];
585
+ const stepEls = [...modal.querySelectorAll('.wizard-steps .step')];
586
+ const nextBtn = document.getElementById('wizNextBtn');
587
+ const backBtn = document.getElementById('wizBackBtn');
588
+ const errEl = document.getElementById('wizError');
589
+ let step = 0;
590
+ let serverConfig = null;
591
+
592
+ function setStep(n) {
593
+ step = n;
594
+ screens.forEach((s, i) => s.classList.toggle('active', i === n));
595
+ stepEls.forEach((s, i) => {
596
+ s.classList.toggle('active', i === n);
597
+ s.classList.toggle('done', i < n);
598
+ });
599
+ backBtn.style.visibility = n === 0 ? 'hidden' : 'visible';
600
+ nextBtn.textContent = n === screens.length - 1 ? 'Launch' : 'Next';
601
+ errEl.textContent = '';
602
+ if (n === 1) document.getElementById('wizKeyLabel').textContent = providerKey();
603
+ if (n === 3) populateModels();
604
+ if (n === 4) renderSummary();
605
+ }
606
+ function providerKey() { return document.getElementById('wizProvider').value; }
607
+ function populateModels() {
608
+ const sel = document.getElementById('wizModel');
609
+ sel.replaceChildren();
610
+ for (const m of MODELS[providerKey()] || []) {
611
+ const opt = document.createElement('option');
612
+ opt.value = m; opt.textContent = m;
613
+ sel.appendChild(opt);
614
+ }
615
+ }
616
+ function readServer() {
617
+ return {
618
+ host: document.getElementById('wizHost').value.trim(),
619
+ port: Number(document.getElementById('wizPort').value),
620
+ auth: document.getElementById('wizAuth').value,
621
+ minecraft_version: document.getElementById('wizVersion').value.trim() || 'auto',
622
+ };
623
+ }
624
+ function readProfile() {
625
+ const p = {
626
+ name: document.getElementById('wizBotName').value.trim(),
627
+ model: document.getElementById('wizModel').value,
628
+ };
629
+ const personality = document.getElementById('wizPersonality').value.trim();
630
+ if (personality) p.conversing = personality + '\n\n$ALL';
631
+ return p;
632
+ }
633
+ function renderSummary() {
634
+ const s = readServer(), p = readProfile();
635
+ document.getElementById('wizSummary').textContent =
636
+ `Provider: ${providerKey()}\n` +
637
+ `Server: ${s.host}:${s.port} (auth=${s.auth}, mc=${s.minecraft_version})\n` +
638
+ `Bot: ${p.name}\n` +
639
+ `Model: ${p.model}\n` +
640
+ `Base: ${document.getElementById('wizBaseProfile').value}`;
641
+ }
642
+ function emit(ev, data) {
643
+ return new Promise((res, rej) => socket.emit(ev, data, r => r?.success === false ? rej(new Error(r.error)) : res(r)));
644
+ }
645
+ async function next() {
646
+ try {
647
+ if (step === 1) {
648
+ const key = document.getElementById('wizApiKey').value.trim();
649
+ if (!key && !serverConfig?.hasKeys) throw new Error('API key is required');
650
+ if (key) await emit('save-keys', { [providerKey()]: key });
651
+ document.getElementById('wizApiKey').value = '';
652
+ }
653
+ if (step === 3) {
654
+ const p = readProfile();
655
+ if (!p.name) throw new Error('Bot name is required');
656
+ await emit('save-profile', p);
657
+ }
658
+ if (step === screens.length - 1) return launch();
659
+ setStep(step + 1);
660
+ } catch (e) { errEl.textContent = e.message; }
661
+ }
662
+ async function launch() {
663
+ nextBtn.disabled = true;
664
+ errEl.textContent = '';
665
+ try {
666
+ const server = readServer();
667
+ const profile = readProfile();
668
+ const base_profile = document.getElementById('wizBaseProfile').value;
669
+ await emit('save-config', { server, bots: [{ profile: profile.name, base_profile }] });
670
+ await emit('create-agent', { ...server, base_profile, profile });
671
+ close();
672
+ } catch (e) { errEl.textContent = e.message; }
673
+ finally { nextBtn.disabled = false; }
674
+ }
675
+ function open() {
676
+ socket.emit('get-config', (res) => {
677
+ serverConfig = res;
678
+ if (res.config?.server) {
679
+ document.getElementById('wizHost').value = res.config.server.host || 'localhost';
680
+ document.getElementById('wizPort').value = res.config.server.port || 25565;
681
+ document.getElementById('wizAuth').value = res.config.server.auth || 'offline';
682
+ document.getElementById('wizVersion').value = res.config.server.minecraft_version || 'auto';
683
+ }
684
+ if (res.hasKeys) {
685
+ document.getElementById('wizApiKey').placeholder = '(already saved — leave blank to keep)';
686
+ }
687
+ setStep(0);
688
+ modal.style.display = 'flex';
689
+ });
690
+ }
691
+ function close() { modal.style.display = 'none'; }
692
+ function maybeAutoOpen() {
693
+ socket.emit('get-config', (res) => {
694
+ serverConfig = res;
695
+ if (!res.config && !res.hasKeys) open();
696
+ });
697
+ }
698
+
699
+ nextBtn.addEventListener('click', next);
700
+ backBtn.addEventListener('click', () => setStep(Math.max(0, step - 1)));
701
+ document.getElementById('openSetupBtn').addEventListener('click', open);
702
+ document.getElementById('closeSetupBtn').addEventListener('click', close);
703
+ socket.on('connect', maybeAutoOpen);
704
+
705
+ return { open, close };
706
+ })();
707
+
708
+ fetch('/settings_spec.json')
709
+ .then(r => r.json())
710
+ .then(spec => {
711
+ settingsSpec = spec;
712
+ buildSettingsForm();
713
+ });
714
+
715
+ function buildSettingsForm() {
716
+ const form = document.getElementById('settingsForm');
717
+ form.innerHTML = '';
718
+ // ensure grid for multi-column layout
719
+ form.style.display = 'grid';
720
+ form.style.gridTemplateColumns = 'repeat(auto-fit, minmax(320px, 1fr))';
721
+ form.style.gap = '8px';
722
+ Object.keys(settingsSpec).forEach(key => {
723
+ if (key === 'profile') return; // profile handled via upload
724
+ const cfg = settingsSpec[key];
725
+ const wrapper = document.createElement('div');
726
+ wrapper.className = 'setting-wrapper';
727
+ const label = document.createElement('label');
728
+ label.textContent = key;
729
+ label.title = cfg.description || '';
730
+ let input;
731
+ switch (cfg.type) {
732
+ case 'boolean':
733
+ input = document.createElement('input');
734
+ input.type = 'checkbox';
735
+ input.checked = cfg.default === true;
736
+ break;
737
+ case 'number':
738
+ input = document.createElement('input');
739
+ input.type = 'number';
740
+ input.value = cfg.default;
741
+ break;
742
+ default:
743
+ input = document.createElement('input');
744
+ input.type = 'text';
745
+ input.value = typeof cfg.default === 'object' ? JSON.stringify(cfg.default) : cfg.default;
746
+ }
747
+ input.title = cfg.description || '';
748
+ input.id = `setting-${key}`;
749
+ wrapper.appendChild(label);
750
+ wrapper.appendChild(input);
751
+ form.appendChild(wrapper);
752
+ });
753
+ }
754
+
755
+ document.getElementById('uploadProfileBtn').addEventListener('click', () => {
756
+ document.getElementById('profileFileInput').click();
757
+ });
758
+
759
+ document.getElementById('profileFileInput').addEventListener('change', e => {
760
+ const file = e.target.files[0];
761
+ if (!file) return;
762
+ const reader = new FileReader();
763
+ reader.onload = ev => {
764
+ try {
765
+ profileData = JSON.parse(ev.target.result);
766
+ document.getElementById('submitCreateAgentBtn').disabled = false;
767
+ document.getElementById('profileStatus').textContent = `Profile: ${profileData.name || 'Uploaded'}`;
768
+ document.getElementById('createError').textContent = '';
769
+ } catch (err) {
770
+ document.getElementById('createError').textContent = 'Invalid profile JSON: ' + err.message;
771
+ profileData = null;
772
+ document.getElementById('submitCreateAgentBtn').disabled = true;
773
+ document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
774
+ }
775
+ };
776
+ reader.readAsText(file);
777
+ e.target.value = '';
778
+ });
779
+
780
+ document.getElementById('submitCreateAgentBtn').addEventListener('click', () => {
781
+ if (!profileData) return;
782
+ const settings = { profile: profileData };
783
+ Object.keys(settingsSpec).forEach(key => {
784
+ if (key === 'profile') return;
785
+ const input = document.getElementById(`setting-${key}`);
786
+ if (!input) return;
787
+ const type = settingsSpec[key].type;
788
+ let val;
789
+ if (type === 'boolean') val = input.checked;
790
+ else if (type === 'number') val = Number(input.value);
791
+ else if (type === 'array' || type === 'object') {
792
+ try { val = JSON.parse(input.value); }
793
+ catch { val = input.value; }
794
+ } else val = input.value;
795
+ settings[key] = val;
796
+ });
797
+ socket.emit('create-agent', settings, res => {
798
+ if (!res.success) {
799
+ document.getElementById('createError').textContent = res.error || 'Unknown error';
800
+ } else {
801
+ // reset on success
802
+ profileData = null;
803
+ document.getElementById('submitCreateAgentBtn').disabled = true;
804
+ document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
805
+ document.getElementById('createError').textContent = '';
806
+ hideCreateAgentModal();
807
+ }
808
+ });
809
+ });
810
+
811
+ // Modal open/close logic
812
+ const modalBackdrop = document.getElementById('createAgentModal');
813
+ document.getElementById('openCreateAgentBtn').addEventListener('click', () => {
814
+ buildSettingsForm();
815
+ modalBackdrop.style.display = 'flex';
816
+ });
817
+ function hideCreateAgentModal() {
818
+ modalBackdrop.style.display = 'none';
819
+ }
820
+ document.getElementById('closeCreateAgentBtn').addEventListener('click', hideCreateAgentModal);
821
+
822
+ socket.on('bot-output', (agentName, message) => {
823
+ agentLastMessage[agentName] = message;
824
+ const messageDiv = document.getElementById(`lastMessage-${agentName}`);
825
+ if (messageDiv) {
826
+ messageDiv.textContent = message;
827
+ }
828
+ });
829
+
830
+ // Subscribe to aggregated state updates (re-sent on each connect)
831
+ socket.on('state-update', (states) => {
832
+ window.lastStates = states;
833
+ Object.keys(states || {}).forEach(name => {
834
+ const st = states[name];
835
+ const healthEl = document.getElementById(`health-${name}`);
836
+ if (st && !st.error) {
837
+ const gp = st.gameplay || {};
838
+ if (healthEl && typeof gp.health === 'number') {
839
+ const hMax = typeof gp.healthMax === 'number' ? gp.healthMax : 20;
840
+ healthEl.textContent = `health: ${gp.health}/${hMax}`;
841
+ }
842
+ const posEl = document.getElementById(`pos-${name}`);
843
+ const hunEl = document.getElementById(`hunger-${name}`);
844
+ const bioEl = document.getElementById(`biome-${name}`);
845
+ const modeEl = document.getElementById(`mode-${name}`);
846
+ const itemsEl = document.getElementById(`items-${name}`);
847
+ const equippedEl = document.getElementById(`equipped-${name}`);
848
+ const invGrid = document.getElementById(`inventory-${name}`);
849
+ const actionEl = document.getElementById(`action-${name}`);
850
+ if (posEl && gp.position) {
851
+ const p = gp.position;
852
+ posEl.textContent = `x ${p.x}, y ${p.y}, z ${p.z}`;
853
+ }
854
+ if (hunEl && typeof gp.hunger === 'number') {
855
+ const fMax = typeof gp.hungerMax === 'number' ? gp.hungerMax : 20;
856
+ hunEl.textContent = `hunger: ${gp.hunger}/${fMax}`;
857
+ }
858
+ if (bioEl && gp.biome) bioEl.textContent = `biome: ${gp.biome}`;
859
+ if (modeEl && gp.gamemode) modeEl.textContent = `gamemode: ${gp.gamemode}`;
860
+ if (itemsEl && st.inventory) {
861
+ const used = st.inventory.stacksUsed ?? 0;
862
+ const total = st.inventory.totalSlots ?? 0;
863
+ itemsEl.textContent = `inventory slots: ${used}/${total}`;
864
+ }
865
+ if (equippedEl && st.inventory?.equipment) {
866
+ const e = st.inventory.equipment;
867
+ equippedEl.textContent = `equipped: ${e.mainHand || 'none'}`;
868
+ }
869
+ const armorEl = document.getElementById(`armor-${name}`);
870
+ if (armorEl && st.inventory?.equipment) {
871
+ const e = st.inventory.equipment;
872
+ const armor = [];
873
+ if (e.helmet) armor.push(`head: ${e.helmet}`);
874
+ if (e.chestplate) armor.push(`chest: ${e.chestplate}`);
875
+ if (e.leggings) armor.push(`legs: ${e.leggings}`);
876
+ if (e.boots) armor.push(`feet: ${e.boots}`);
877
+ armorEl.textContent = `armor: ${armor.length ? armor.join(', ') : 'none'}`;
878
+ }
879
+ if (actionEl && st.action) {
880
+ actionEl.textContent = `${st.action.current || 'Idle'}`;
881
+ }
882
+ if (invGrid && st.inventory?.counts) {
883
+ const counts = st.inventory.counts;
884
+ invGrid.innerHTML = Object.keys(counts).length ?
885
+ Object.entries(counts).map(([k, v]) => `<div class="cell">${k}: ${v}</div>`).join('') :
886
+ '<div class="cell">(empty)</div>';
887
+ }
888
+ }
889
+ });
890
+ });
891
+
892
+ function fetchAgentSettings(name) {
893
+ return new Promise((resolve) => {
894
+ if (agentSettings[name]) { resolve(agentSettings[name]); return; }
895
+ socket.emit('get-settings', name, res => {
896
+ if (res.settings) {
897
+ agentSettings[name] = res.settings;
898
+ resolve(res.settings);
899
+ } else resolve(null);
900
+ });
901
+ });
902
+ }
903
+
904
+ // Agent settings modal logic
905
+ const agentSettingsModal = document.getElementById('agentSettingsModal');
906
+ const agentSettingsForm = document.getElementById('agentSettingsForm');
907
+ const applyBtn = document.getElementById('applyAgentSettingsBtn');
908
+ const discardBtn = document.getElementById('discardAgentSettingsBtn');
909
+ const closeAgentSettingsBtn = document.getElementById('closeAgentSettingsBtn');
910
+ const agentSettingsTitle = document.getElementById('agentSettingsTitle');
911
+ let currentAgentName = null;
912
+ let originalAgentSettings = null;
913
+
914
+ function buildAgentSettingsForm(settings) {
915
+ agentSettingsForm.innerHTML = '';
916
+ agentSettingsForm.style.display = 'grid';
917
+ agentSettingsForm.style.gridTemplateColumns = 'repeat(auto-fit, minmax(320px, 1fr))';
918
+ agentSettingsForm.style.gap = '8px';
919
+ Object.keys(settingsSpec).forEach(key => {
920
+ if (key === 'profile') return; // profile not edited here
921
+ const cfg = settingsSpec[key];
922
+ const wrapper = document.createElement('div');
923
+ wrapper.className = 'setting-wrapper';
924
+ const label = document.createElement('label');
925
+ label.textContent = key;
926
+ label.title = cfg.description || '';
927
+ let input;
928
+ switch (cfg.type) {
929
+ case 'boolean':
930
+ input = document.createElement('input');
931
+ input.type = 'checkbox';
932
+ input.checked = Boolean(settings[key]);
933
+ break;
934
+ case 'number':
935
+ input = document.createElement('input');
936
+ input.type = 'number';
937
+ input.value = settings[key] ?? cfg.default ?? 0;
938
+ break;
939
+ default:
940
+ input = document.createElement('input');
941
+ input.type = 'text';
942
+ const defVal = settings[key] ?? cfg.default ?? '';
943
+ input.value = typeof defVal === 'object' ? JSON.stringify(defVal) : defVal;
944
+ }
945
+ input.id = `agent-setting-${key}`;
946
+ input.addEventListener('input', onAgentSettingsChanged);
947
+ if (input.type === 'checkbox') input.addEventListener('change', onAgentSettingsChanged);
948
+ wrapper.appendChild(label);
949
+ wrapper.appendChild(input);
950
+ agentSettingsForm.appendChild(wrapper);
951
+ });
952
+ onAgentSettingsChanged();
953
+ }
954
+
955
+ function openAgentSettings(name) {
956
+ currentAgentName = name;
957
+ agentSettingsTitle.textContent = `${name} Settings`;
958
+ fetchAgentSettings(name).then(settings => {
959
+ originalAgentSettings = JSON.parse(JSON.stringify(settings || {}));
960
+ buildAgentSettingsForm(settings || {});
961
+ agentSettingsModal.style.display = 'flex';
962
+ });
963
+ }
964
+ window.openAgentSettings = openAgentSettings;
965
+
966
+ function getEditedAgentSettings() {
967
+ const newSettings = { profile: (originalAgentSettings && originalAgentSettings.profile) || {} };
968
+ Object.keys(settingsSpec).forEach(key => {
969
+ if (key === 'profile') return;
970
+ const cfg = settingsSpec[key];
971
+ const input = document.getElementById(`agent-setting-${key}`);
972
+ if (!input) return;
973
+ let val;
974
+ if (cfg.type === 'boolean') val = input.checked;
975
+ else if (cfg.type === 'number') val = Number(input.value);
976
+ else if (cfg.type === 'array' || cfg.type === 'object') {
977
+ try { val = JSON.parse(input.value); }
978
+ catch { val = input.value; }
979
+ } else val = input.value;
980
+ newSettings[key] = val;
981
+ });
982
+ return newSettings;
983
+ }
984
+
985
+ function shallowEqual(a, b) {
986
+ if (!a || !b) return false;
987
+ const keys = Object.keys(settingsSpec).filter(k => k !== 'profile');
988
+ for (const k of keys) {
989
+ const va = a[k];
990
+ const vb = b[k];
991
+ if (typeof va === 'object' || typeof vb === 'object') {
992
+ if (JSON.stringify(va) !== JSON.stringify(vb)) return false;
993
+ } else if (va !== vb) return false;
994
+ }
995
+ return true;
996
+ }
997
+
998
+ function onAgentSettingsChanged() {
999
+ if (!originalAgentSettings) { applyBtn.disabled = true; return; }
1000
+ const edited = getEditedAgentSettings();
1001
+ applyBtn.disabled = shallowEqual(edited, originalAgentSettings);
1002
+ }
1003
+
1004
+ function closeAgentSettings() {
1005
+ agentSettingsModal.style.display = 'none';
1006
+ currentAgentName = null;
1007
+ originalAgentSettings = null;
1008
+ }
1009
+
1010
+ function updateAgentViewer(name) {
1011
+ const agentEl = document.getElementById(`agent-${name}`);
1012
+ if (!agentEl) return;
1013
+
1014
+ const settings = agentSettings[name];
1015
+ const viewerContainer = agentEl.querySelector('.agent-view-container');
1016
+ if (!viewerContainer) return;
1017
+
1018
+ const agentState = currentAgents.find(a => a.name === name);
1019
+ const shouldShow = agentState?.in_game && settings?.render_bot_view === true;
1020
+ viewerContainer.parentElement.style.display = shouldShow ? '' : 'none';
1021
+ }
1022
+
1023
+ discardBtn.addEventListener('click', () => {
1024
+ if (!currentAgentName || !originalAgentSettings) return;
1025
+ buildAgentSettingsForm(originalAgentSettings);
1026
+ });
1027
+
1028
+ applyBtn.addEventListener('click', () => {
1029
+ if (!currentAgentName) return;
1030
+ const edited = getEditedAgentSettings();
1031
+ socket.emit('set-agent-settings', currentAgentName, edited);
1032
+ // Update local settings immediately
1033
+ agentSettings[currentAgentName] = { ...edited, fetched: true };
1034
+ updateAgentViewer(currentAgentName);
1035
+ closeAgentSettings();
1036
+ });
1037
+
1038
+ closeAgentSettingsBtn.addEventListener('click', closeAgentSettings);
1039
+
1040
+
1041
+ function renderAgentCard(agent) {
1042
+ const cfg = agentSettings[agent.name] || {};
1043
+ const showViewer = agent.in_game && cfg.render_bot_view === true;
1044
+ const viewerPort = agent.viewerPort;
1045
+ const viewerHTML = showViewer ? `<div class="agent-view-container"><iframe class="agent-viewer" id="viewer-${agent.name}" src="http://localhost:${viewerPort}"></iframe></div>` : '';
1046
+ const lastMessage = agentLastMessage[agent.name] || '';
1047
+ const invOpen = inventoryOpen[agent.name] === true;
1048
+ const invStyle = invOpen ? '' : 'display:none;';
1049
+ return `
1050
+ <div class="agent" id="agent-${agent.name}">
1051
+ <div class="agent-grid">
1052
+ <div class="cell title" style="grid-column: 1 / -1; display:flex; align-items:center; justify-content:space-between;">
1053
+ <span><span class="status-icon ${agent.in_game ? 'online' : 'offline'}">●</span>${agent.name}${agent.socket_connected && !agent.in_game ? '<span style="margin-left:6px;color:#f0ad4e;">joining...</span>' : ''}
1054
+ <button class="gear-btn" title="Settings" onclick="openAgentSettings('${agent.name}')">⚙</button>
1055
+ <button class="gear-btn" title="Inventory" onclick="toggleDetails('${agent.name}')">Inventory</button>
1056
+ </span>
1057
+ </div>
1058
+ ${showViewer ? `<div class="cell" style="grid-row: span 3; padding:0;">${viewerHTML}</div>` : ''}
1059
+ <div class="cell" id="action-${agent.name}">action: -</div>
1060
+ <div class="cell" id="mode-${agent.name}">gamemode: -</div>
1061
+ <div class="cell" id="health-${agent.name}">health: -</div>
1062
+ <div class="cell" id="hunger-${agent.name}">hunger: -</div>
1063
+ <div class="cell" id="pos-${agent.name}">pos: -</div>
1064
+ <div class="cell" id="biome-${agent.name}">biome: -</div>
1065
+ <div class="cell" id="items-${agent.name}">inventory slots: -</div>
1066
+ <div class="cell" id="equipped-${agent.name}">equipped: -</div>
1067
+ <div class="agent-inventory" id="inventorySection-${agent.name}" style="${invStyle} grid-column: 1 / -1;">
1068
+ <h3>Inventory</h3>
1069
+ <div class="cell" id="armor-${agent.name}" style="margin-bottom: 8px;">armor: -</div>
1070
+ <div class="inventory-grid" id="inventory-${agent.name}"></div>
1071
+ </div>
1072
+ <div id="lastMessage-${agent.name}" class="last-message" style="grid-column: 1 / -1;"><strong>Last Message:</strong> ${lastMessage}</div>
1073
+ </div>
1074
+ <div class="controls-row">
1075
+ <button class="start-btn" id="sendBtn-${agent.name}" disabled onclick="sendMessage('${agent.name}', document.getElementById('messageInput-${agent.name}').value)">Send</button>
1076
+ <input class="msg-input" type="text" id="messageInput-${agent.name}" placeholder="Enter message..."
1077
+ oninput="onMsgInputChange('${agent.name}')"
1078
+ onkeydown="if(event.key === 'Enter') document.getElementById('sendBtn-${agent.name}').click()"
1079
+ ${!agent.in_game ? 'disabled' : ''}>
1080
+ <button class="neutral-btn" onclick="sendMessage('${agent.name}', '!stop')" ${!agent.in_game ? 'disabled' : ''}>Stop Action</button>
1081
+ <button class="neutral-btn" onclick="sendMessage('${agent.name}', '!stay(-1)')" ${!agent.in_game ? 'disabled' : ''}>Stay Still</button>
1082
+ <button class="neutral-btn" onclick="restartAgent('${agent.name}')" ${!agent.in_game ? 'disabled' : ''}>Restart</button>
1083
+ <button class="neutral-btn" ${agent.in_game ? `onclick=\"disconnectAgent('${agent.name}')\"` : (agent.socket_connected ? 'disabled' : `onclick=\"startAgent('${agent.name}')\"`)}>${agent.in_game ? 'Disconnect' : (agent.socket_connected ? 'Connecting...' : 'Connect')}</button>
1084
+ <button class="stop-btn" onclick="destroyAgent('${agent.name}')">Remove</button>
1085
+ </div>
1086
+ </div>`;
1087
+ }
1088
+
1089
+ async function renderAgents(agents) {
1090
+ if (!agents.length) {
1091
+ agentsDiv.innerHTML = '<div class="agent">No agents connected</div>';
1092
+ return;
1093
+ }
1094
+
1095
+ // If agentsDiv is empty, do a full render
1096
+ if (!agentsDiv.children.length) {
1097
+ agentsDiv.innerHTML = agents.map(agent => renderAgentCard(agent)).join('');
1098
+ // Update all viewers after initial render
1099
+ setTimeout(() => {
1100
+ agents.forEach(a => {
1101
+ if (a.in_game) updateAgentViewer(a.name);
1102
+ });
1103
+ }, 0);
1104
+ return;
1105
+ }
1106
+
1107
+ // Compare with current agents to find changes
1108
+ const prevAgents = currentAgents.reduce((acc, a) => ({ ...acc, [a.name]: a }), {});
1109
+ const changedAgents = agents.filter(a => {
1110
+ const prev = prevAgents[a.name];
1111
+ return !prev || prev.in_game !== a.in_game || prev.viewerPort !== a.viewerPort || prev.socket_connected !== a.socket_connected;
1112
+ });
1113
+
1114
+ // Update only changed agents
1115
+ changedAgents.forEach(agent => {
1116
+ const el = document.getElementById(`agent-${agent.name}`);
1117
+ if (el) {
1118
+ // Update existing card
1119
+ el.outerHTML = renderAgentCard(agent);
1120
+ if (agent.in_game) updateAgentViewer(agent.name);
1121
+ } else {
1122
+ // Add new card
1123
+ agentsDiv.insertAdjacentHTML('beforeend', renderAgentCard(agent));
1124
+ if (agent.in_game) updateAgentViewer(agent.name);
1125
+ }
1126
+ });
1127
+
1128
+ // Remove cards for agents that no longer exist
1129
+ Array.from(agentsDiv.children).forEach(el => {
1130
+ const name = el.id.replace('agent-', '');
1131
+ if (!agents.find(a => a.name === name)) {
1132
+ el.remove();
1133
+ delete inventoryOpen[name];
1134
+ }
1135
+ });
1136
+ }
1137
+
1138
+ socket.on('agents-status', async (agents) => {
1139
+ // Fetch settings for all agents that don't have current settings
1140
+ const needSettings = agents.filter(a => !agentSettings[a.name]);
1141
+ if (needSettings.length > 0) {
1142
+ await Promise.all(needSettings.map(async (a) => {
1143
+ const settings = await fetchAgentSettings(a.name);
1144
+ if (settings) {
1145
+ agentSettings[a.name] = settings;
1146
+ }
1147
+ }));
1148
+ }
1149
+
1150
+ // Compare with current agents to find changes
1151
+ const prevAgents = currentAgents.reduce((acc, a) => ({ ...acc, [a.name]: a }), {});
1152
+ const changedAgents = agents.filter(a => {
1153
+ const prev = prevAgents[a.name];
1154
+ return !prev || prev.in_game !== a.in_game || prev.viewerPort !== a.viewerPort || prev.socket_connected !== a.socket_connected;
1155
+ });
1156
+
1157
+ // Update current agents list
1158
+ currentAgents = agents;
1159
+
1160
+ // If agentsDiv is empty, do a full render
1161
+ if (!agentsDiv.children.length) {
1162
+ agentsDiv.innerHTML = agents.map(agent => renderAgentCard(agent)).join('');
1163
+ // Update all viewers after initial render
1164
+ setTimeout(() => {
1165
+ agents.forEach(a => {
1166
+ if (a.in_game) updateAgentViewer(a.name);
1167
+ });
1168
+ }, 0);
1169
+ return;
1170
+ }
1171
+
1172
+ // Update only changed agents
1173
+ changedAgents.forEach(agent => {
1174
+ const el = document.getElementById(`agent-${agent.name}`);
1175
+ if (el) {
1176
+ // Update existing card
1177
+ el.outerHTML = renderAgentCard(agent);
1178
+ if (agent.in_game) updateAgentViewer(agent.name);
1179
+ } else {
1180
+ // Add new card
1181
+ agentsDiv.insertAdjacentHTML('beforeend', renderAgentCard(agent));
1182
+ if (agent.in_game) updateAgentViewer(agent.name);
1183
+ }
1184
+ });
1185
+
1186
+ // Remove cards for agents that no longer exist
1187
+ Array.from(agentsDiv.children).forEach(el => {
1188
+ const name = el.id.replace('agent-', '');
1189
+ if (!agents.find(a => a.name === name)) {
1190
+ el.remove();
1191
+ delete inventoryOpen[name];
1192
+ }
1193
+ });
1194
+ });
1195
+
1196
+ function restartAgent(n) { socket.emit('restart-agent', n); }
1197
+ function disconnectAgent(n) { socket.emit('stop-agent', n); }
1198
+ function startAgent(n) {
1199
+ const btn = document.querySelector(`button[onclick="startAgent('${n}')"]`);
1200
+ if (btn) {
1201
+ btn.textContent = 'Connecting...';
1202
+ btn.disabled = true;
1203
+ // Re-enable after 10s if still disabled (agent failed to connect)
1204
+ setTimeout(() => {
1205
+ const retryBtn = document.querySelector(`button[onclick=\\"startAgent('${n}')\\"]`);
1206
+ const agentState = (window.currentAgents || []).find(a => a.name === n);
1207
+ const stillWaiting = agentState ? (!agentState.in_game && !agentState.socket_connected) : true;
1208
+ if (retryBtn && stillWaiting) {
1209
+ retryBtn.disabled = false;
1210
+ retryBtn.textContent = 'Connect';
1211
+ }
1212
+ }, 10000);
1213
+ }
1214
+ socket.emit('start-agent', n);
1215
+ }
1216
+ function stopAgent(n) { socket.emit('stop-agent', n); }
1217
+ function destroyAgent(n) { socket.emit('destroy-agent', n); }
1218
+ function disconnectAllAgents() {
1219
+ socket.emit('stop-all-agents');
1220
+ }
1221
+ function confirmShutdown() {
1222
+ if (confirm('Are you sure you want to perform a full shutdown?\nThis will stop all agents and close the server.')) {
1223
+ socket.emit('shutdown');
1224
+ }
1225
+ }
1226
+ function sendMessage(n, m) {
1227
+ if (!m || !m.trim()) return;
1228
+ socket.emit('send-message', n, { from: 'ADMIN', message: m });
1229
+ const input = document.getElementById(`messageInput-${n}`);
1230
+ const btn = document.getElementById(`sendBtn-${n}`);
1231
+ if (input) input.value = '';
1232
+ if (btn) btn.disabled = true;
1233
+ }
1234
+ function onMsgInputChange(name) {
1235
+ const input = document.getElementById(`messageInput-${name}`);
1236
+ const btn = document.getElementById(`sendBtn-${name}`);
1237
+ if (btn && input) {
1238
+ btn.disabled = !(input.value && input.value.trim().length > 0);
1239
+ }
1240
+ }
1241
+
1242
+ function toggleDetails(name) {
1243
+ const invSection = document.getElementById(`inventorySection-${name}`);
1244
+ if (!invSection) return;
1245
+ const visible = invSection.style.display !== 'none';
1246
+ const newVisible = !visible;
1247
+ invSection.style.display = newVisible ? '' : 'none';
1248
+ inventoryOpen[name] = newVisible;
1249
+ }
1250
+ window.toggleDetails = toggleDetails;
1251
+ </script>
1252
+ </body>
1253
+ </html>