@controlflow-ai/daemon 0.1.0 → 0.1.2
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.
- package/README.md +21 -19
- package/package.json +14 -3
- package/src/agent-runtime.ts +15 -0
- package/src/app.ts +314 -3
- package/src/client.ts +11 -3
- package/src/console.ts +397 -18
- package/src/daemon.ts +25 -6
- package/src/db.ts +181 -6
- package/src/lark/app-registration.ts +141 -0
- package/src/lark/cli.ts +4 -134
- package/src/lark/credentials.ts +36 -3
- package/src/lark/event-router.ts +22 -2
- package/src/lark/server-integration.ts +9 -16
- package/src/lark/setup.ts +74 -5
- package/src/local-api.ts +69 -2
- package/src/local-auth.ts +4 -3
- package/src/migrations/022_lark_authorized_users.ts +16 -0
- package/src/migrations/023_projects.ts +65 -0
- package/src/migrations.ts +3 -1
- package/src/network.ts +24 -0
- package/src/server.ts +21 -7
- package/src/types.ts +40 -0
- package/src/web.ts +368 -29
package/src/web.ts
CHANGED
|
@@ -63,8 +63,9 @@ export function dashboardHtml(): string {
|
|
|
63
63
|
grid-template-columns: 300px minmax(360px, 1fr) 330px;
|
|
64
64
|
gap: 14px;
|
|
65
65
|
width: min(1540px, calc(100vw - 28px));
|
|
66
|
-
|
|
66
|
+
height: calc(100vh - 28px);
|
|
67
67
|
margin: 14px auto;
|
|
68
|
+
overflow: hidden;
|
|
68
69
|
}
|
|
69
70
|
.panel {
|
|
70
71
|
min-width: 0;
|
|
@@ -115,7 +116,7 @@ export function dashboardHtml(): string {
|
|
|
115
116
|
}
|
|
116
117
|
.pill.good { color: var(--active); border-color: #a7d7c4; background: var(--active-soft); }
|
|
117
118
|
.pill.blue { color: var(--blue); border-color: #b7c8f6; background: var(--blue-soft); }
|
|
118
|
-
.chat { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
|
|
119
|
+
.chat { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; min-height: 0; }
|
|
119
120
|
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 74px; min-width: 0; background: var(--panel); }
|
|
120
121
|
.panel-head > div { min-width: 0; }
|
|
121
122
|
.panel-head h2 { margin: 0; font-size: 22px; letter-spacing: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -138,6 +139,94 @@ export function dashboardHtml(): string {
|
|
|
138
139
|
.split { display: grid; grid-template-columns: minmax(0, 1fr) 120px; gap: 8px; }
|
|
139
140
|
.toolbox { display: grid; gap: 8px; background: var(--panel-2); }
|
|
140
141
|
.section-title { margin: 0; color: var(--muted); font-size: 12px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
|
|
142
|
+
.field { display: grid; gap: 5px; }
|
|
143
|
+
.field span { color: var(--muted); font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
|
|
144
|
+
.settings-trigger { white-space: nowrap; }
|
|
145
|
+
.settings-backdrop {
|
|
146
|
+
position: fixed;
|
|
147
|
+
inset: 0;
|
|
148
|
+
z-index: 40;
|
|
149
|
+
display: none;
|
|
150
|
+
padding: 18px;
|
|
151
|
+
background: rgba(24, 23, 20, .28);
|
|
152
|
+
}
|
|
153
|
+
.settings-backdrop.open { display: grid; place-items: center; }
|
|
154
|
+
.settings-dialog {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-rows: auto 1fr;
|
|
157
|
+
width: min(1120px, 100%);
|
|
158
|
+
max-height: min(860px, calc(100vh - 36px));
|
|
159
|
+
border: 1px solid var(--ink);
|
|
160
|
+
background: var(--panel);
|
|
161
|
+
box-shadow: var(--shadow);
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
}
|
|
164
|
+
.settings-head {
|
|
165
|
+
display: flex;
|
|
166
|
+
justify-content: space-between;
|
|
167
|
+
gap: 14px;
|
|
168
|
+
padding: 16px;
|
|
169
|
+
border-bottom: 1px solid var(--line);
|
|
170
|
+
background: #fffdf8;
|
|
171
|
+
}
|
|
172
|
+
.settings-head h2 { margin: 0; font-size: 24px; letter-spacing: 0; }
|
|
173
|
+
.settings-body {
|
|
174
|
+
display: grid;
|
|
175
|
+
grid-template-columns: 210px minmax(0, 1fr);
|
|
176
|
+
min-height: 0;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
}
|
|
179
|
+
.settings-nav {
|
|
180
|
+
display: grid;
|
|
181
|
+
align-content: start;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
padding: 14px;
|
|
184
|
+
border-right: 1px solid var(--line);
|
|
185
|
+
background: var(--panel-2);
|
|
186
|
+
overflow: auto;
|
|
187
|
+
}
|
|
188
|
+
.settings-tab {
|
|
189
|
+
width: 100%;
|
|
190
|
+
background: transparent;
|
|
191
|
+
color: var(--ink);
|
|
192
|
+
border-color: var(--line);
|
|
193
|
+
text-align: left;
|
|
194
|
+
}
|
|
195
|
+
.settings-tab.active { background: var(--ink); color: #fffaf1; border-color: var(--ink); }
|
|
196
|
+
.settings-content { overflow: auto; padding: 16px; }
|
|
197
|
+
.settings-pane { display: none; gap: 12px; }
|
|
198
|
+
.settings-pane.active { display: grid; }
|
|
199
|
+
.settings-section-head {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: start;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
gap: 12px;
|
|
204
|
+
padding-bottom: 10px;
|
|
205
|
+
border-bottom: 1px solid var(--line);
|
|
206
|
+
}
|
|
207
|
+
.settings-section-head h3 { margin: 0; font-size: 18px; letter-spacing: 0; }
|
|
208
|
+
.settings-section-head .meta { max-width: 620px; }
|
|
209
|
+
.setup-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
210
|
+
.setup-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: #fffdf8; }
|
|
211
|
+
.setup-panel.collapsed { display: none; }
|
|
212
|
+
.setup-panel .section-title { color: var(--ink); }
|
|
213
|
+
.setup-actions { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
|
|
214
|
+
.summary-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: var(--active-soft); }
|
|
215
|
+
.settings-list { display: grid; gap: 8px; }
|
|
216
|
+
.settings-row {
|
|
217
|
+
display: grid;
|
|
218
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
219
|
+
gap: 10px;
|
|
220
|
+
align-items: start;
|
|
221
|
+
padding: 11px;
|
|
222
|
+
border: 1px solid var(--line);
|
|
223
|
+
background: #fffdf8;
|
|
224
|
+
}
|
|
225
|
+
.settings-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
226
|
+
.settings-row .meta { overflow-wrap: anywhere; word-break: break-word; }
|
|
227
|
+
.command-wrap { display: grid; gap: 7px; }
|
|
228
|
+
.command-wrap textarea { font-family: var(--mono); font-size: 11px; min-height: 104px; }
|
|
229
|
+
.secret-note { color: var(--faint); font-size: 11px; line-height: 1.4; }
|
|
141
230
|
.empty, .error, .readonly-note { border: 1px dashed var(--line); padding: 18px; text-align: center; background: rgba(255, 253, 248, .65); }
|
|
142
231
|
.error { display: none; color: var(--danger); border-color: rgba(163, 58, 43, .35); background: #fff1ee; }
|
|
143
232
|
.readonly-note { color: var(--muted); line-height: 1.5; }
|
|
@@ -188,12 +277,14 @@ export function dashboardHtml(): string {
|
|
|
188
277
|
.app { grid-template-columns: 280px minmax(0, 1fr); }
|
|
189
278
|
.inspector { grid-column: 1 / -1; grid-template-rows: auto auto; }
|
|
190
279
|
.inspector .agents { max-height: 320px; }
|
|
280
|
+
.settings-body, .setup-grid, .settings-row { grid-template-columns: 1fr; }
|
|
281
|
+
.settings-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); border-right: 0; border-bottom: 1px solid var(--line); }
|
|
191
282
|
}
|
|
192
283
|
@media (max-width: 760px) {
|
|
193
|
-
.app { width: 100%;
|
|
284
|
+
.app { width: 100%; height: 100vh; margin: 0; grid-template-columns: 1fr; overflow: auto; }
|
|
194
285
|
.sidebar, .inspector { max-height: none; }
|
|
195
|
-
.chat { min-height:
|
|
196
|
-
.composer-controls, .split, .form-row { grid-template-columns: 1fr; }
|
|
286
|
+
.chat { height: 100vh; min-height: 0; }
|
|
287
|
+
.composer-controls, .split, .form-row, .setup-actions { grid-template-columns: 1fr; }
|
|
197
288
|
.message { max-width: 100%; }
|
|
198
289
|
}
|
|
199
290
|
</style>
|
|
@@ -246,6 +337,7 @@ export function dashboardHtml(): string {
|
|
|
246
337
|
<h2>Agents</h2>
|
|
247
338
|
<div class="meta">Use runtime=codex for executable demo agents.</div>
|
|
248
339
|
</div>
|
|
340
|
+
<button id="open-settings" class="secondary settings-trigger" type="button">Settings</button>
|
|
249
341
|
</header>
|
|
250
342
|
<div class="toolbox">
|
|
251
343
|
<form id="invite-form" class="split">
|
|
@@ -259,29 +351,135 @@ export function dashboardHtml(): string {
|
|
|
259
351
|
</select>
|
|
260
352
|
<button>Invite</button>
|
|
261
353
|
</form>
|
|
262
|
-
<form id="agent-form" class="create-agent">
|
|
263
|
-
<p class="section-title">Create or Update Agent</p>
|
|
264
|
-
<input id="agent-key" placeholder="codex" autocomplete="off" required>
|
|
265
|
-
<input id="agent-name" placeholder="Codex" autocomplete="off" required>
|
|
266
|
-
<input id="agent-runtime" placeholder="codex" autocomplete="off">
|
|
267
|
-
<input id="agent-computer" placeholder="machine id" autocomplete="off">
|
|
268
|
-
<button>Create agent</button>
|
|
269
|
-
</form>
|
|
270
|
-
<form id="computer-form" class="create-agent">
|
|
271
|
-
<p class="section-title">Provision Computer</p>
|
|
272
|
-
<input id="computer-name" placeholder="Bill's Team" autocomplete="off">
|
|
273
|
-
<button>Generate command</button>
|
|
274
|
-
<textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
|
|
275
|
-
</form>
|
|
276
354
|
<div id="members" class="members"></div>
|
|
277
355
|
</div>
|
|
278
356
|
<div id="agents" class="agents"></div>
|
|
279
357
|
</aside>
|
|
280
358
|
</main>
|
|
359
|
+
<section id="settings-backdrop" class="settings-backdrop" aria-hidden="true">
|
|
360
|
+
<div class="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
|
361
|
+
<header class="settings-head">
|
|
362
|
+
<div>
|
|
363
|
+
<h2 id="settings-title">Settings</h2>
|
|
364
|
+
<div class="meta">Configure access, agents, computers, and Feishu/Lark from one place.</div>
|
|
365
|
+
</div>
|
|
366
|
+
<button id="close-settings" class="secondary icon" type="button" aria-label="Close settings">×</button>
|
|
367
|
+
</header>
|
|
368
|
+
<div class="settings-body">
|
|
369
|
+
<nav class="settings-nav" aria-label="Settings sections">
|
|
370
|
+
<button class="settings-tab active" type="button" data-settings-tab="access">Access</button>
|
|
371
|
+
<button class="settings-tab" type="button" data-settings-tab="agents">Agents</button>
|
|
372
|
+
<button class="settings-tab" type="button" data-settings-tab="computers">Computers</button>
|
|
373
|
+
<button class="settings-tab" type="button" data-settings-tab="lark">Lark</button>
|
|
374
|
+
</nav>
|
|
375
|
+
<div class="settings-content">
|
|
376
|
+
<section id="settings-access" class="settings-pane active">
|
|
377
|
+
<div class="summary-panel">
|
|
378
|
+
<p class="section-title">Server Access</p>
|
|
379
|
+
<div id="server-access" class="meta">Listening locally. Tailscale address is detected on load.</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="settings-section-head">
|
|
382
|
+
<div>
|
|
383
|
+
<h3>Lark Users</h3>
|
|
384
|
+
<div class="meta">Only listed Lark user IDs may trigger inbound bot handling.</div>
|
|
385
|
+
</div>
|
|
386
|
+
<button class="secondary" type="button" data-add-panel="lark-user-form">Add User</button>
|
|
387
|
+
</div>
|
|
388
|
+
<div class="setup-grid">
|
|
389
|
+
<div id="settings-lark-user-list" class="settings-list"></div>
|
|
390
|
+
<form id="lark-user-form" class="setup-panel collapsed">
|
|
391
|
+
<p class="section-title">Authorized Lark User</p>
|
|
392
|
+
<label class="field"><span>User ID</span><input id="lark-user-id" placeholder="on_xxx union id" autocomplete="off" required></label>
|
|
393
|
+
<label class="field"><span>Display name</span><input id="lark-user-name" placeholder="Optional" autocomplete="off"></label>
|
|
394
|
+
<div class="setup-actions">
|
|
395
|
+
<button>Save user</button>
|
|
396
|
+
<button class="secondary" type="button" data-cancel-panel="lark-user-form">Cancel</button>
|
|
397
|
+
</div>
|
|
398
|
+
</form>
|
|
399
|
+
</div>
|
|
400
|
+
</section>
|
|
401
|
+
<section id="settings-agents" class="settings-pane">
|
|
402
|
+
<div class="settings-section-head">
|
|
403
|
+
<div>
|
|
404
|
+
<h3>Agents</h3>
|
|
405
|
+
<div class="meta">Manage logical agents and assign them to an available computer.</div>
|
|
406
|
+
</div>
|
|
407
|
+
<button class="secondary" type="button" data-add-panel="agent-form">Add Agent</button>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="setup-grid">
|
|
410
|
+
<div id="settings-agent-list" class="settings-list"></div>
|
|
411
|
+
<form id="agent-form" class="setup-panel collapsed">
|
|
412
|
+
<p class="section-title">Agent Onboard</p>
|
|
413
|
+
<label class="field"><span>Key</span><input id="agent-key" placeholder="codex" autocomplete="off" required></label>
|
|
414
|
+
<label class="field"><span>Name</span><input id="agent-name" placeholder="Codex" autocomplete="off" required></label>
|
|
415
|
+
<label class="field"><span>Runtime</span><select id="agent-runtime"><option value="codex">codex</option><option value="neeko">neeko</option><option value="coco">coco</option><option value="coco-stream-json">coco-stream-json</option></select></label>
|
|
416
|
+
<label class="field"><span>Computer</span><select id="agent-computer"><option value="">No assignment</option></select></label>
|
|
417
|
+
<label class="field"><span>Description</span><input id="agent-desc" placeholder="Optional" autocomplete="off"></label>
|
|
418
|
+
<div class="setup-actions">
|
|
419
|
+
<button>Onboard agent</button>
|
|
420
|
+
<button class="secondary" type="button" data-cancel-panel="agent-form">Cancel</button>
|
|
421
|
+
</div>
|
|
422
|
+
</form>
|
|
423
|
+
</div>
|
|
424
|
+
</section>
|
|
425
|
+
<section id="settings-computers" class="settings-pane">
|
|
426
|
+
<div class="settings-section-head">
|
|
427
|
+
<div>
|
|
428
|
+
<h3>Computers</h3>
|
|
429
|
+
<div class="meta">Provision daemon credentials and see connected machines.</div>
|
|
430
|
+
</div>
|
|
431
|
+
<button class="secondary" type="button" data-add-panel="computer-form">Add Computer</button>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="setup-grid">
|
|
434
|
+
<div id="settings-computer-list" class="settings-list"></div>
|
|
435
|
+
<form id="computer-form" class="setup-panel collapsed">
|
|
436
|
+
<p class="section-title">Computer Onboard</p>
|
|
437
|
+
<label class="field"><span>Name</span><input id="computer-name" placeholder="Local computer" autocomplete="off"></label>
|
|
438
|
+
<label class="field"><span>Server URL</span><input id="computer-server" autocomplete="off"></label>
|
|
439
|
+
<label class="field"><span>Daemon package</span><input id="computer-package" value="@controlflow-ai/daemon@latest" autocomplete="off"></label>
|
|
440
|
+
<div class="setup-actions">
|
|
441
|
+
<button>Generate command</button>
|
|
442
|
+
<button id="copy-command" class="secondary" type="button">Copy</button>
|
|
443
|
+
</div>
|
|
444
|
+
<button class="secondary" type="button" data-cancel-panel="computer-form">Cancel</button>
|
|
445
|
+
<div class="command-wrap">
|
|
446
|
+
<textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
|
|
447
|
+
</div>
|
|
448
|
+
</form>
|
|
449
|
+
</div>
|
|
450
|
+
</section>
|
|
451
|
+
<section id="settings-lark" class="settings-pane">
|
|
452
|
+
<div class="settings-section-head">
|
|
453
|
+
<div>
|
|
454
|
+
<h3>Lark</h3>
|
|
455
|
+
<div class="meta">Bind Feishu/Lark bot credentials to a Pal agent. Secrets stay in the local runtime profile.</div>
|
|
456
|
+
</div>
|
|
457
|
+
<button class="secondary" type="button" data-add-panel="lark-form">Add Lark Bot</button>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="setup-grid">
|
|
460
|
+
<div id="settings-lark-list" class="settings-list"></div>
|
|
461
|
+
<form id="lark-form" class="setup-panel collapsed">
|
|
462
|
+
<p class="section-title">Lark Setup</p>
|
|
463
|
+
<label class="field"><span>Bind agent</span><select id="lark-agent"></select></label>
|
|
464
|
+
<label class="field"><span>Label</span><input id="lark-label" placeholder="Team bot" autocomplete="off"></label>
|
|
465
|
+
<label class="field"><span>App ID</span><input id="lark-app-id" placeholder="cli_xxx" autocomplete="off" required></label>
|
|
466
|
+
<label class="field"><span>App Secret</span><input id="lark-app-secret" type="password" autocomplete="off" required></label>
|
|
467
|
+
<div class="secret-note">Secret is sent only to this local Pal server, validated with Feishu, and stored in the local Lark config file.</div>
|
|
468
|
+
<div class="setup-actions">
|
|
469
|
+
<button>Save Lark bot</button>
|
|
470
|
+
<button class="secondary" type="button" data-cancel-panel="lark-form">Cancel</button>
|
|
471
|
+
</div>
|
|
472
|
+
</form>
|
|
473
|
+
</div>
|
|
474
|
+
</section>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</section>
|
|
281
479
|
<div id="error" class="error" role="alert"></div>
|
|
282
480
|
<div id="toast" class="toast" role="status"></div>
|
|
283
481
|
<script>
|
|
284
|
-
const state = { rooms: [], agents: [], members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
|
|
482
|
+
const state = { rooms: [], agents: [], computers: [], lark: null, larkUsers: [], serverAccess: null, members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
|
|
285
483
|
const root = (id) => document.getElementById(id);
|
|
286
484
|
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&':'&', '<':'<', '>':'>', "'":''', '"':'"' }[char]));
|
|
287
485
|
async function api(path, options) {
|
|
@@ -358,11 +556,58 @@ export function dashboardHtml(): string {
|
|
|
358
556
|
}
|
|
359
557
|
function renderAgents() {
|
|
360
558
|
root('invite-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">No agents</option>';
|
|
559
|
+
root('lark-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">Onboard an agent first</option>';
|
|
361
560
|
root('agents').innerHTML = state.agents.length ? state.agents.map((agent) => (
|
|
362
561
|
'<article class="agent"><div class="agent-title"><span>' + escapeHtml(agent.display_name) + '</span>' + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div>' +
|
|
363
562
|
'<div class="pills">' + pill(agent.agent_key) + pill(agent.id) + '</div>' +
|
|
364
563
|
'<div class="meta">' + escapeHtml(agent.description || 'No description') + '</div></article>'
|
|
365
564
|
)).join('') : empty('No agents yet. Create codex to start.');
|
|
565
|
+
root('settings-agent-list').innerHTML = state.agents.length
|
|
566
|
+
? state.agents.map((agent) => '<article class="settings-row"><div><strong>' + escapeHtml(agent.display_name) + '</strong><div class="meta">' + escapeHtml(agent.description || 'No description') + '</div><div class="pills">' + pill(agent.agent_key, 'blue') + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div></div></article>').join('')
|
|
567
|
+
: empty('No agents yet.');
|
|
568
|
+
}
|
|
569
|
+
function renderComputers() {
|
|
570
|
+
root('agent-computer').innerHTML = '<option value="">No assignment</option>' + state.computers.map((computer) => '<option value="' + escapeHtml(computer.id) + '">' + escapeHtml(computer.name) + ' · ' + escapeHtml(computer.id) + '</option>').join('');
|
|
571
|
+
root('settings-computer-list').innerHTML = state.computers.length
|
|
572
|
+
? state.computers.map((computer) => '<article class="settings-row"><div><strong>' + escapeHtml(computer.name) + '</strong><div class="meta">' + escapeHtml(computer.id) + '</div><div class="pills">' + pill(computer.status, computer.status === 'online' ? 'good' : '') + (computer.last_seen_at ? pill('last seen ' + computer.last_seen_at) : '') + '</div></div></article>').join('')
|
|
573
|
+
: empty('No computers yet.');
|
|
574
|
+
}
|
|
575
|
+
function renderLarkConfig() {
|
|
576
|
+
const el = root('settings-lark-list');
|
|
577
|
+
if (!el) return;
|
|
578
|
+
const bots = state.lark?.bots || [];
|
|
579
|
+
el.innerHTML = bots.length
|
|
580
|
+
? bots.map((bot) => '<article class="settings-row"><div><strong>' + escapeHtml(bot.label || bot.appId) + '</strong><div class="meta">' + escapeHtml(bot.appId) + '</div><div class="pills">' + pill('@' + (bot.agent || '-'), 'blue') + pill(bot.botOpenId ? 'open_id resolved' : 'open_id missing', bot.botOpenId ? 'good' : '') + pill(bot.hasSecret ? 'secret stored' : 'secret missing') + '</div></div></article>').join('') + '<div class="meta">Config: ' + escapeHtml(state.lark.path || '') + '</div>'
|
|
581
|
+
: empty('No Lark bots configured yet.');
|
|
582
|
+
}
|
|
583
|
+
function renderLarkUsers() {
|
|
584
|
+
const el = root('settings-lark-user-list');
|
|
585
|
+
if (!el) return;
|
|
586
|
+
el.innerHTML = state.larkUsers.length
|
|
587
|
+
? state.larkUsers.map((user) => '<article class="settings-row"><div><strong>' + escapeHtml(user.display_name || user.user_id) + '</strong><div class="meta">' + escapeHtml(user.user_id) + '</div><div class="pills">' + pill('authorized', 'good') + '</div></div><button class="secondary" type="button" data-delete-lark-user="' + escapeHtml(user.user_id) + '">Delete</button></article>').join('')
|
|
588
|
+
: empty('No authorized Lark users. Inbound bot messages will be ignored.');
|
|
589
|
+
[...el.querySelectorAll('[data-delete-lark-user]')].forEach((button) => {
|
|
590
|
+
button.addEventListener('click', async () => {
|
|
591
|
+
const userId = button.dataset.deleteLarkUser;
|
|
592
|
+
if (!userId) return;
|
|
593
|
+
const deleted = await api('/api/lark/authorized-users/' + encodeURIComponent(userId), { method: 'DELETE' }).catch((error) => { showError(error); return null; });
|
|
594
|
+
if (!deleted) return;
|
|
595
|
+
await loadLarkUsers();
|
|
596
|
+
showToast('Lark user removed');
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function renderServerAccess() {
|
|
601
|
+
const access = state.serverAccess;
|
|
602
|
+
const el = root('server-access');
|
|
603
|
+
if (!el) return;
|
|
604
|
+
if (!access) {
|
|
605
|
+
el.textContent = 'Listening locally. Tailscale address is detected on load.';
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
el.textContent = access.tailscaleUrl
|
|
609
|
+
? 'Listening on local ' + access.localUrl + ' and Tailscale ' + access.tailscaleUrl
|
|
610
|
+
: 'Listening on local ' + access.localUrl + '. No Tailscale interface detected.';
|
|
366
611
|
}
|
|
367
612
|
function renderMembers() {
|
|
368
613
|
root('members').innerHTML = state.members.length ? state.members.map((member) => (
|
|
@@ -370,6 +615,25 @@ export function dashboardHtml(): string {
|
|
|
370
615
|
'<div class="pills">' + pill(member.source) + pill(member.status) + '</div></article>'
|
|
371
616
|
)).join('') : empty('No known members in this room.');
|
|
372
617
|
}
|
|
618
|
+
function openSettings(tab) {
|
|
619
|
+
root('settings-backdrop').classList.add('open');
|
|
620
|
+
root('settings-backdrop').setAttribute('aria-hidden', 'false');
|
|
621
|
+
selectSettingsTab(tab || 'access');
|
|
622
|
+
}
|
|
623
|
+
function closeSettings() {
|
|
624
|
+
root('settings-backdrop').classList.remove('open');
|
|
625
|
+
root('settings-backdrop').setAttribute('aria-hidden', 'true');
|
|
626
|
+
}
|
|
627
|
+
function selectSettingsTab(tab) {
|
|
628
|
+
document.querySelectorAll('.settings-tab').forEach((button) => button.classList.toggle('active', button.dataset.settingsTab === tab));
|
|
629
|
+
document.querySelectorAll('.settings-pane').forEach((pane) => pane.classList.toggle('active', pane.id === 'settings-' + tab));
|
|
630
|
+
}
|
|
631
|
+
function showPanel(id) {
|
|
632
|
+
root(id)?.classList.remove('collapsed');
|
|
633
|
+
}
|
|
634
|
+
function hidePanel(id) {
|
|
635
|
+
root(id)?.classList.add('collapsed');
|
|
636
|
+
}
|
|
373
637
|
function currentMentionQuery() {
|
|
374
638
|
const input = root('message-content');
|
|
375
639
|
if (!input) return null;
|
|
@@ -461,6 +725,26 @@ export function dashboardHtml(): string {
|
|
|
461
725
|
state.agents = data.agents || [];
|
|
462
726
|
renderAgents();
|
|
463
727
|
}
|
|
728
|
+
async function loadComputers() {
|
|
729
|
+
const data = await api('/api/computers');
|
|
730
|
+
state.computers = data.computers || [];
|
|
731
|
+
renderComputers();
|
|
732
|
+
}
|
|
733
|
+
async function loadLarkConfig() {
|
|
734
|
+
const data = await api('/api/lark/config');
|
|
735
|
+
state.lark = data;
|
|
736
|
+
renderLarkConfig();
|
|
737
|
+
}
|
|
738
|
+
async function loadLarkUsers() {
|
|
739
|
+
const data = await api('/api/lark/authorized-users');
|
|
740
|
+
state.larkUsers = data.users || [];
|
|
741
|
+
renderLarkUsers();
|
|
742
|
+
}
|
|
743
|
+
async function loadServerAccess() {
|
|
744
|
+
const data = await api('/api/server/access');
|
|
745
|
+
state.serverAccess = data;
|
|
746
|
+
renderServerAccess();
|
|
747
|
+
}
|
|
464
748
|
async function loadMessages() {
|
|
465
749
|
const room = activeRoom();
|
|
466
750
|
if (!room) {
|
|
@@ -483,7 +767,7 @@ export function dashboardHtml(): string {
|
|
|
483
767
|
renderMembers();
|
|
484
768
|
}
|
|
485
769
|
async function refresh() {
|
|
486
|
-
await Promise.all([loadRooms(), loadAgents()]);
|
|
770
|
+
await Promise.all([loadRooms(), loadAgents(), loadComputers(), loadLarkConfig(), loadLarkUsers(), loadServerAccess()]);
|
|
487
771
|
await loadMessages();
|
|
488
772
|
}
|
|
489
773
|
window.selectRoom = async (id) => {
|
|
@@ -492,6 +776,23 @@ export function dashboardHtml(): string {
|
|
|
492
776
|
await loadMessages().catch(showError);
|
|
493
777
|
};
|
|
494
778
|
root('refresh').addEventListener('click', () => refresh().catch(showError));
|
|
779
|
+
root('open-settings').addEventListener('click', () => openSettings('access'));
|
|
780
|
+
root('close-settings').addEventListener('click', closeSettings);
|
|
781
|
+
root('settings-backdrop').addEventListener('mousedown', (event) => {
|
|
782
|
+
if (event.target === root('settings-backdrop')) closeSettings();
|
|
783
|
+
});
|
|
784
|
+
document.addEventListener('keydown', (event) => {
|
|
785
|
+
if (event.key === 'Escape' && root('settings-backdrop').classList.contains('open')) closeSettings();
|
|
786
|
+
});
|
|
787
|
+
document.querySelectorAll('.settings-tab').forEach((button) => {
|
|
788
|
+
button.addEventListener('click', () => selectSettingsTab(button.dataset.settingsTab));
|
|
789
|
+
});
|
|
790
|
+
document.querySelectorAll('[data-add-panel]').forEach((button) => {
|
|
791
|
+
button.addEventListener('click', () => showPanel(button.dataset.addPanel));
|
|
792
|
+
});
|
|
793
|
+
document.querySelectorAll('[data-cancel-panel]').forEach((button) => {
|
|
794
|
+
button.addEventListener('click', () => hidePanel(button.dataset.cancelPanel));
|
|
795
|
+
});
|
|
495
796
|
root('room-form').addEventListener('submit', async (event) => {
|
|
496
797
|
event.preventDefault();
|
|
497
798
|
const name = root('room-name').value.trim();
|
|
@@ -507,29 +808,67 @@ export function dashboardHtml(): string {
|
|
|
507
808
|
event.preventDefault();
|
|
508
809
|
const agent_key = root('agent-key').value.trim();
|
|
509
810
|
const display_name = root('agent-name').value.trim();
|
|
510
|
-
const runtime = root('agent-runtime').value.trim() ||
|
|
511
|
-
const computer_id = root('agent-computer').value.trim();
|
|
811
|
+
const runtime = root('agent-runtime').value.trim() || 'codex';
|
|
812
|
+
const computer_id = root('agent-computer').value.trim() || undefined;
|
|
813
|
+
const description = root('agent-desc').value.trim() || null;
|
|
512
814
|
if (!agent_key || !display_name) return;
|
|
513
|
-
const saved = await api('/api/agents', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime }) }).catch((error) => { showError(error); return null; });
|
|
815
|
+
const saved = await api('/api/agents/onboard', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime, description, computer_id }) }).catch((error) => { showError(error); return null; });
|
|
514
816
|
if (!saved) return;
|
|
515
|
-
if (computer_id) {
|
|
516
|
-
await api('/api/agents/' + encodeURIComponent(agent_key) + '/assignment', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ computer_id }) }).catch((error) => { showError(error); return null; });
|
|
517
|
-
}
|
|
518
817
|
root('agent-key').value = '';
|
|
519
818
|
root('agent-name').value = '';
|
|
520
|
-
root('agent-runtime').value = '';
|
|
819
|
+
root('agent-runtime').value = 'codex';
|
|
521
820
|
root('agent-computer').value = '';
|
|
821
|
+
root('agent-desc').value = '';
|
|
522
822
|
await loadAgents();
|
|
823
|
+
hidePanel('agent-form');
|
|
523
824
|
showToast('Agent saved: @' + agent_key);
|
|
524
825
|
});
|
|
525
826
|
root('computer-form').addEventListener('submit', async (event) => {
|
|
526
827
|
event.preventDefault();
|
|
527
828
|
const name = root('computer-name').value.trim();
|
|
528
|
-
const
|
|
829
|
+
const server_url = root('computer-server').value.trim() || location.origin;
|
|
830
|
+
const package_name = root('computer-package').value.trim() || undefined;
|
|
831
|
+
const data = await api('/api/computers/provision', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name || undefined, server_url, package_name }) }).catch((error) => { showError(error); return null; });
|
|
529
832
|
if (!data) return;
|
|
530
833
|
root('computer-command').value = data.command;
|
|
834
|
+
await loadComputers();
|
|
531
835
|
showToast('Computer provisioned: ' + data.computer.id);
|
|
532
836
|
});
|
|
837
|
+
root('copy-command').addEventListener('click', async () => {
|
|
838
|
+
const command = root('computer-command').value;
|
|
839
|
+
if (!command) return;
|
|
840
|
+
await navigator.clipboard?.writeText(command).catch(() => null);
|
|
841
|
+
showToast('Daemon command copied');
|
|
842
|
+
});
|
|
843
|
+
root('computer-server').value = location.origin;
|
|
844
|
+
root('lark-user-form').addEventListener('submit', async (event) => {
|
|
845
|
+
event.preventDefault();
|
|
846
|
+
const user_id = root('lark-user-id').value.trim();
|
|
847
|
+
const display_name = root('lark-user-name').value.trim() || null;
|
|
848
|
+
if (!user_id) return;
|
|
849
|
+
const saved = await api('/api/lark/authorized-users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ user_id, display_name }) }).catch((error) => { showError(error); return null; });
|
|
850
|
+
if (!saved) return;
|
|
851
|
+
root('lark-user-id').value = '';
|
|
852
|
+
root('lark-user-name').value = '';
|
|
853
|
+
await loadLarkUsers();
|
|
854
|
+
hidePanel('lark-user-form');
|
|
855
|
+
showToast('Lark user authorized');
|
|
856
|
+
});
|
|
857
|
+
root('lark-form').addEventListener('submit', async (event) => {
|
|
858
|
+
event.preventDefault();
|
|
859
|
+
const agent = root('lark-agent').value;
|
|
860
|
+
const app_id = root('lark-app-id').value.trim();
|
|
861
|
+
const app_secret = root('lark-app-secret').value.trim();
|
|
862
|
+
const label = root('lark-label').value.trim() || undefined;
|
|
863
|
+
if (!agent || !app_id || !app_secret) return;
|
|
864
|
+
const data = await api('/api/lark/setup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent, app_id, app_secret, label }) }).catch((error) => { showError(error); return null; });
|
|
865
|
+
if (!data) return;
|
|
866
|
+
root('lark-app-secret').value = '';
|
|
867
|
+
await loadLarkConfig();
|
|
868
|
+
const reload = await api('/api/lark/reload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }).catch((error) => ({ reloadError: error.message }));
|
|
869
|
+
if (!reload.reloadError) hidePanel('lark-form');
|
|
870
|
+
showToast(reload.reloadError ? 'Lark saved, reload failed: ' + reload.reloadError : 'Lark bot saved and reloaded: ' + data.appId);
|
|
871
|
+
});
|
|
533
872
|
root('invite-form').addEventListener('submit', async (event) => {
|
|
534
873
|
event.preventDefault();
|
|
535
874
|
const room = activeRoom();
|