@runuai/host 0.8.34 → 0.8.36
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/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/meta/_journal.json +7 -0
- package/db/schema.ts +24 -0
- package/images/standard/Dockerfile +9 -0
- package/lib/agents/claude.ts +7 -1
- package/lib/agents/codex.ts +8 -2
- package/lib/agents/factory.ts +1 -0
- package/lib/agents/opencode.ts +461 -0
- package/lib/agents/rate-limit.ts +34 -0
- package/lib/agents/types.ts +4 -2
- package/lib/codex-auth.ts +4 -0
- package/lib/engine-accounts.ts +538 -0
- package/lib/engines.ts +69 -4
- package/lib/mcp-gateway.ts +30 -5
- package/lib/orchestrator.ts +168 -7
- package/lib/standard-image.ts +5 -1
- package/package.json +1 -1
- package/scripts/agent/task-up.sh +17 -0
- package/src/ui/server.ts +65 -0
- package/src/ui/types.ts +22 -2
- package/ui/app.js +190 -1
- package/ui/style.css +19 -0
package/ui/app.js
CHANGED
|
@@ -7,7 +7,7 @@ const $ = (id) => document.getElementById(id);
|
|
|
7
7
|
const expanded = new Set(); // task ids whose events are expanded (persist across polls)
|
|
8
8
|
let latestTasks = [];
|
|
9
9
|
let latestEvents = [];
|
|
10
|
-
let latestEngines = null; // { catalog, statuses } from /api/engines
|
|
10
|
+
let latestEngines = null; // { catalog, statuses, cli, accounts } from /api/engines
|
|
11
11
|
|
|
12
12
|
const KIND_CLASS = {
|
|
13
13
|
"task.created": "created",
|
|
@@ -300,6 +300,16 @@ function engineCard(e) {
|
|
|
300
300
|
head.append(meta);
|
|
301
301
|
card.append(head);
|
|
302
302
|
|
|
303
|
+
// ADR-076: multiple accounts per engine. Rendered for account-model kinds
|
|
304
|
+
// (claude/codex/opencode). Extra accounts can be removed; claude/codex can
|
|
305
|
+
// add one by pasting a token / API key.
|
|
306
|
+
const accounts = (latestEngines?.accounts || {})[e.kind] || [];
|
|
307
|
+
const extras = accounts.filter((a) => !a.isDefault);
|
|
308
|
+
const canAdd = e.kind === "claude" || e.kind === "codex";
|
|
309
|
+
if (accounts.length > 0 && (extras.length > 0 || canAdd)) {
|
|
310
|
+
card.append(engineAccounts(e, accounts, canAdd));
|
|
311
|
+
}
|
|
312
|
+
|
|
303
313
|
const btn = document.createElement("button");
|
|
304
314
|
btn.className = "link-btn danger";
|
|
305
315
|
btn.type = "button";
|
|
@@ -318,6 +328,75 @@ function engineCard(e) {
|
|
|
318
328
|
return card;
|
|
319
329
|
}
|
|
320
330
|
|
|
331
|
+
/** Accounts sub-panel for an engine card (ADR-076). */
|
|
332
|
+
function engineAccounts(e, accounts, canAdd) {
|
|
333
|
+
const box = document.createElement("div");
|
|
334
|
+
box.className = "engine-accounts";
|
|
335
|
+
|
|
336
|
+
const title = document.createElement("div");
|
|
337
|
+
title.className = "engine-sub";
|
|
338
|
+
title.textContent = `Accounts (${accounts.length}) — rotated automatically`;
|
|
339
|
+
box.append(title);
|
|
340
|
+
|
|
341
|
+
for (const a of accounts) {
|
|
342
|
+
const row = document.createElement("div");
|
|
343
|
+
row.className = "engine-account-row";
|
|
344
|
+
const name = document.createElement("span");
|
|
345
|
+
name.textContent = a.isDefault ? `${a.label} (default)` : a.label;
|
|
346
|
+
row.append(name);
|
|
347
|
+
if (!a.isDefault) {
|
|
348
|
+
const rm = document.createElement("button");
|
|
349
|
+
rm.className = "link-btn danger";
|
|
350
|
+
rm.type = "button";
|
|
351
|
+
rm.textContent = "Remove";
|
|
352
|
+
rm.addEventListener("click", async () => {
|
|
353
|
+
rm.disabled = true;
|
|
354
|
+
try {
|
|
355
|
+
await postJSON("/api/engines/accounts/remove", { id: a.id });
|
|
356
|
+
} catch {
|
|
357
|
+
/* poll re-syncs truth */
|
|
358
|
+
}
|
|
359
|
+
await poll();
|
|
360
|
+
});
|
|
361
|
+
row.append(rm);
|
|
362
|
+
}
|
|
363
|
+
box.append(row);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (canAdd) {
|
|
367
|
+
const add = document.createElement("button");
|
|
368
|
+
add.className = "link-btn";
|
|
369
|
+
add.type = "button";
|
|
370
|
+
add.textContent = "+ Add account";
|
|
371
|
+
add.addEventListener("click", () => void addAccountFlow(e));
|
|
372
|
+
box.append(add);
|
|
373
|
+
}
|
|
374
|
+
return box;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Minimal add-account flow: prompt for a label + a token/API key, then POST. */
|
|
378
|
+
async function addAccountFlow(e) {
|
|
379
|
+
const label = window.prompt(`Label for the new ${e.label} account (e.g. "work"):`);
|
|
380
|
+
if (!label) return;
|
|
381
|
+
const secretPrompt =
|
|
382
|
+
e.kind === "claude"
|
|
383
|
+
? "Paste a Claude token (run `claude setup-token`) or an API key:"
|
|
384
|
+
: "Paste an OpenAI API key:";
|
|
385
|
+
const secret = window.prompt(secretPrompt);
|
|
386
|
+
if (!secret) return;
|
|
387
|
+
const body =
|
|
388
|
+
e.kind === "claude"
|
|
389
|
+
? { kind: e.kind, label, token: secret }
|
|
390
|
+
: { kind: e.kind, label, apiKey: secret };
|
|
391
|
+
try {
|
|
392
|
+
const res = await postJSON("/api/engines/accounts/add", body);
|
|
393
|
+
if (res && res.ok === false && res.message) window.alert(res.message);
|
|
394
|
+
} catch {
|
|
395
|
+
/* poll re-syncs truth */
|
|
396
|
+
}
|
|
397
|
+
await poll();
|
|
398
|
+
}
|
|
399
|
+
|
|
321
400
|
// --- add-engine modal -------------------------------------------------------
|
|
322
401
|
|
|
323
402
|
function openModal() {
|
|
@@ -386,6 +465,8 @@ function setupEngine(e) {
|
|
|
386
465
|
|
|
387
466
|
if (e.authMode === "api-key") {
|
|
388
467
|
body.append(apiKeyForm(e));
|
|
468
|
+
} else if (e.authMode === "external-login") {
|
|
469
|
+
body.append(externalLoginForm(e));
|
|
389
470
|
} else {
|
|
390
471
|
body.append(commandForm(e));
|
|
391
472
|
}
|
|
@@ -465,6 +546,114 @@ function apiKeyForm(e) {
|
|
|
465
546
|
return form;
|
|
466
547
|
}
|
|
467
548
|
|
|
549
|
+
/**
|
|
550
|
+
* external-login mode (OpenCode): the engine's own CLI is an interactive TUI we
|
|
551
|
+
* can't drive here, so the owner runs it in their terminal and we just detect
|
|
552
|
+
* the credential it wrote. Install (if the CLI is missing) → run the login
|
|
553
|
+
* command → "Check connection".
|
|
554
|
+
*/
|
|
555
|
+
function externalLoginForm(e) {
|
|
556
|
+
const form = document.createElement("div");
|
|
557
|
+
form.className = "engine-setup";
|
|
558
|
+
|
|
559
|
+
if (e.notes) {
|
|
560
|
+
const note = document.createElement("p");
|
|
561
|
+
note.className = "setup-note";
|
|
562
|
+
note.textContent = e.notes;
|
|
563
|
+
form.append(note);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const status = document.createElement("div");
|
|
567
|
+
status.className = "setup-status";
|
|
568
|
+
const log = document.createElement("pre");
|
|
569
|
+
log.className = "log";
|
|
570
|
+
log.hidden = true;
|
|
571
|
+
const pushLine = (line) => {
|
|
572
|
+
log.textContent += (log.textContent ? "\n" : "") + line;
|
|
573
|
+
log.scrollTop = log.scrollHeight;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const cliMissing =
|
|
577
|
+
!!latestEngines?.cli && latestEngines.cli[e.kind] === false && !!e.installHint;
|
|
578
|
+
|
|
579
|
+
// Step 1 (only when the CLI is absent): install it.
|
|
580
|
+
const installWrap = document.createElement("div");
|
|
581
|
+
if (cliMissing) {
|
|
582
|
+
const installNote = document.createElement("p");
|
|
583
|
+
installNote.className = "setup-note";
|
|
584
|
+
installNote.textContent = `The ${e.label} CLI isn't installed on this Mac. Install it with:`;
|
|
585
|
+
const installCmd = document.createElement("pre");
|
|
586
|
+
installCmd.className = "log";
|
|
587
|
+
installCmd.textContent = e.installHint;
|
|
588
|
+
const install = document.createElement("button");
|
|
589
|
+
install.className = "btn";
|
|
590
|
+
install.type = "button";
|
|
591
|
+
install.textContent = "Install";
|
|
592
|
+
install.addEventListener("click", async () => {
|
|
593
|
+
install.disabled = true;
|
|
594
|
+
install.textContent = "Installing…";
|
|
595
|
+
status.className = "setup-status";
|
|
596
|
+
status.textContent = "Running the installer…";
|
|
597
|
+
log.hidden = false;
|
|
598
|
+
log.textContent = "";
|
|
599
|
+
const result = await runInstall({ kind: e.kind }, pushLine);
|
|
600
|
+
if (result.ok) {
|
|
601
|
+
await poll();
|
|
602
|
+
installWrap.hidden = true;
|
|
603
|
+
status.className = "setup-status";
|
|
604
|
+
status.textContent = "Installed. Now sign in from your terminal below.";
|
|
605
|
+
} else {
|
|
606
|
+
install.disabled = false;
|
|
607
|
+
install.textContent = "Try again";
|
|
608
|
+
status.className = "setup-status err";
|
|
609
|
+
status.textContent = result.message || "Install failed.";
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
installWrap.append(installNote, installCmd, install);
|
|
613
|
+
} else {
|
|
614
|
+
installWrap.hidden = true;
|
|
615
|
+
}
|
|
616
|
+
form.append(installWrap);
|
|
617
|
+
|
|
618
|
+
// Step 2: the command the owner runs in their own terminal.
|
|
619
|
+
if (e.loginCmd) {
|
|
620
|
+
const runNote = document.createElement("p");
|
|
621
|
+
runNote.className = "setup-note";
|
|
622
|
+
runNote.textContent = "Run this in your terminal to sign in:";
|
|
623
|
+
const cmd = document.createElement("pre");
|
|
624
|
+
cmd.className = "log";
|
|
625
|
+
cmd.textContent = e.loginCmd;
|
|
626
|
+
form.append(runNote, cmd);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Step 3: re-detect what login wrote.
|
|
630
|
+
const actions = document.createElement("div");
|
|
631
|
+
actions.className = "setup-actions";
|
|
632
|
+
const check = document.createElement("button");
|
|
633
|
+
check.className = "btn";
|
|
634
|
+
check.type = "button";
|
|
635
|
+
check.textContent = "Check connection";
|
|
636
|
+
check.addEventListener("click", async () => {
|
|
637
|
+
check.disabled = true;
|
|
638
|
+
check.textContent = "Checking…";
|
|
639
|
+
status.className = "setup-status";
|
|
640
|
+
status.textContent = "Looking for your credentials…";
|
|
641
|
+
const result = await runConnect({ kind: e.kind });
|
|
642
|
+
if (result.ok) {
|
|
643
|
+
await poll();
|
|
644
|
+
closeModal();
|
|
645
|
+
} else {
|
|
646
|
+
check.disabled = false;
|
|
647
|
+
check.textContent = "Check again";
|
|
648
|
+
status.className = "setup-status err";
|
|
649
|
+
status.textContent = result.message || "Not signed in yet.";
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
actions.append(check);
|
|
653
|
+
form.append(actions, status, log);
|
|
654
|
+
return form;
|
|
655
|
+
}
|
|
656
|
+
|
|
468
657
|
/** token/login mode: a Connect button + a live log; Claude adds a paste link. */
|
|
469
658
|
function commandForm(e) {
|
|
470
659
|
const form = document.createElement("div");
|
package/ui/style.css
CHANGED
|
@@ -377,12 +377,31 @@ code,
|
|
|
377
377
|
.engine-card {
|
|
378
378
|
display: flex;
|
|
379
379
|
align-items: center;
|
|
380
|
+
flex-wrap: wrap;
|
|
380
381
|
gap: 0.75rem;
|
|
381
382
|
border: 1px solid var(--line);
|
|
382
383
|
border-radius: var(--radius);
|
|
383
384
|
padding: 0.6rem 0.9rem;
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
/* ADR-076: accounts sub-panel spans the full card width below the head/actions. */
|
|
388
|
+
.engine-accounts {
|
|
389
|
+
flex-basis: 100%;
|
|
390
|
+
border-top: 1px solid var(--line);
|
|
391
|
+
padding-top: 0.5rem;
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-direction: column;
|
|
394
|
+
gap: 0.35rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.engine-account-row {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
justify-content: space-between;
|
|
401
|
+
gap: 0.75rem;
|
|
402
|
+
font-size: 0.88rem;
|
|
403
|
+
}
|
|
404
|
+
|
|
386
405
|
.engine-head {
|
|
387
406
|
display: flex;
|
|
388
407
|
align-items: center;
|