oh-my-agent-web 0.2.0 → 0.3.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.
- package/README.md +9 -1
- package/bin/oh-my-agent-web-ws.mjs +195 -6
- package/package.json +1 -1
- package/web/dist/assets/index-5LJ5fRcO.js +42 -0
- package/web/dist/assets/index-QiDfPKFo.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-B8-VzDf2.css +0 -1
- package/web/dist/assets/index-DeC2eiiq.js +0 -42
package/README.md
CHANGED
|
@@ -171,6 +171,12 @@ oh-my-agent-web ws -t <threadId> "continue" # resume an existing thread
|
|
|
171
171
|
oh-my-agent-web ws -y --model gpt-5.5 "..." # auto-approve approvals, pick a model
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
Interactive mode prints the active model on connect and shows it in the prompt; use
|
|
175
|
+
`/model` to pick from the server's models with ↑/↓ + Enter (or `/model <name>` to switch
|
|
176
|
+
directly; applies from the next turn), `/help` for commands and `/quit` to exit. A turn
|
|
177
|
+
that fails with the upstream "prompt flagged" false-positive is retried automatically
|
|
178
|
+
once (`--retry <n>` / `--no-retry`).
|
|
179
|
+
|
|
174
180
|
Run the same client through npm (pass flags after `--`):
|
|
175
181
|
|
|
176
182
|
```bash
|
|
@@ -185,11 +191,13 @@ npm run ws -- -t <threadId> "continue"
|
|
|
185
191
|
| `--addr <host:port>` / `--host <h>` / `-p, --port <n>` | Address or port (default `127.0.0.1:25258`) |
|
|
186
192
|
| `--cwd <dir>` | Working directory (default: current dir) |
|
|
187
193
|
| `-t, --thread <id>` | Resume an existing thread |
|
|
188
|
-
| `--model <name>` | Model id |
|
|
194
|
+
| `--model <name>` | Model id (`/model` in interactive mode to list/switch) |
|
|
189
195
|
| `--sandbox <mode>` / `--approval <mode>` | Session sandbox / approval policy |
|
|
190
196
|
| `-y, --auto-approve` / `--decline` | Auto-answer approvals instead of prompting |
|
|
191
197
|
| `-v, --verbose` / `--json` | Print reasoning deltas / raw event JSON |
|
|
192
198
|
| `--timeout <sec>` | Max wait per turn (default 1800) |
|
|
199
|
+
| `--retry <n>` / `--no-retry` | Retry a flagged/transient turn up to n times (default 1) |
|
|
200
|
+
| `--retry-delay <ms>` | Base retry delay, default 1000 (grows per attempt) |
|
|
193
201
|
|
|
194
202
|
Env: `CODEX_WS_URL` / `CODEX_WS_HOST` / `CODEX_WS_PORT`.
|
|
195
203
|
|
|
@@ -36,7 +36,7 @@ Connection:
|
|
|
36
36
|
Session:
|
|
37
37
|
--cwd <dir> Working directory (default: current dir)
|
|
38
38
|
-t, --thread <id> Resume an existing thread
|
|
39
|
-
--model <name> Model id
|
|
39
|
+
--model <name> Model id (use /model interactively to list/switch)
|
|
40
40
|
--sandbox <mode> read-only | workspace-write | danger-full-access
|
|
41
41
|
--approval <mode> untrusted | on-request | on-failure | never
|
|
42
42
|
-y, --auto-approve Auto-accept approvals (acceptForSession)
|
|
@@ -44,7 +44,15 @@ Session:
|
|
|
44
44
|
-v, --verbose Print reasoning deltas
|
|
45
45
|
--json Print raw event JSON only
|
|
46
46
|
--timeout <sec> Max wait for a turn, default 1800
|
|
47
|
+
--retry <n> Retry a flagged/transient turn up to n times, default 1 (0 = off)
|
|
48
|
+
--retry-delay <ms> Base delay between retries, default 1000 (grows per attempt)
|
|
47
49
|
-h, --help Show this help
|
|
50
|
+
|
|
51
|
+
Interactive commands (type at the prompt):
|
|
52
|
+
/model Pick a model (↑/↓ + Enter), or list available ones
|
|
53
|
+
/model <name> Switch the model for subsequent turns
|
|
54
|
+
/help Print this help
|
|
55
|
+
/quit Exit
|
|
48
56
|
`;
|
|
49
57
|
|
|
50
58
|
/** Parse `oh-my-agent-web ws [ ... ]` arguments. */
|
|
@@ -65,6 +73,8 @@ export function parseWsArgs(argv) {
|
|
|
65
73
|
verbose: false,
|
|
66
74
|
json: false,
|
|
67
75
|
timeout: 1800,
|
|
76
|
+
retry: 1,
|
|
77
|
+
retryDelayMs: 1000,
|
|
68
78
|
messages: [],
|
|
69
79
|
help: false,
|
|
70
80
|
};
|
|
@@ -131,6 +141,15 @@ export function parseWsArgs(argv) {
|
|
|
131
141
|
case "--timeout":
|
|
132
142
|
opts.timeout = Number(next());
|
|
133
143
|
break;
|
|
144
|
+
case "--retry":
|
|
145
|
+
opts.retry = Number(next());
|
|
146
|
+
break;
|
|
147
|
+
case "--no-retry":
|
|
148
|
+
opts.retry = 0;
|
|
149
|
+
break;
|
|
150
|
+
case "--retry-delay":
|
|
151
|
+
opts.retryDelayMs = Number(next());
|
|
152
|
+
break;
|
|
134
153
|
case "-h":
|
|
135
154
|
case "--help":
|
|
136
155
|
opts.help = true;
|
|
@@ -384,9 +403,24 @@ export async function runWsClient(argv) {
|
|
|
384
403
|
: await client.request("thread/start", threadParams);
|
|
385
404
|
|
|
386
405
|
const threadId = threadResp.thread.id;
|
|
406
|
+
let currentModel = threadResp.model ?? opts.model ?? null;
|
|
387
407
|
log(
|
|
388
|
-
`[thread] ${threadId}
|
|
408
|
+
`[thread] ${threadId} cwd=${threadResp.cwd} sandbox=${JSON.stringify(threadResp.sandbox)}`,
|
|
389
409
|
);
|
|
410
|
+
log(`[model] ${currentModel ?? "(server default)"} \x1b[2m(/model to list or switch)\x1b[0m`);
|
|
411
|
+
|
|
412
|
+
/** Lazily fetch the server's model catalog (id / model / displayName / …). */
|
|
413
|
+
let modelsCache = null;
|
|
414
|
+
async function loadModels() {
|
|
415
|
+
if (modelsCache) return modelsCache;
|
|
416
|
+
try {
|
|
417
|
+
const resp = await client.request("model/list", {});
|
|
418
|
+
modelsCache = Array.isArray(resp?.data) ? resp.data : [];
|
|
419
|
+
} catch {
|
|
420
|
+
modelsCache = [];
|
|
421
|
+
}
|
|
422
|
+
return modelsCache;
|
|
423
|
+
}
|
|
390
424
|
|
|
391
425
|
/* -------- event rendering -------- */
|
|
392
426
|
let inAgentMessage = false;
|
|
@@ -503,20 +537,164 @@ export async function runWsClient(argv) {
|
|
|
503
537
|
decide(id, "decline");
|
|
504
538
|
});
|
|
505
539
|
|
|
506
|
-
|
|
507
|
-
|
|
540
|
+
/** Run one turn to completion and return its final state. */
|
|
541
|
+
async function runTurn(text) {
|
|
542
|
+
const done = new Promise((resolve) => client.on("turnDone", (ev) => resolve(ev.detail), { once: true }));
|
|
508
543
|
const started = await client.request("turn/start", {
|
|
509
544
|
threadId,
|
|
510
545
|
input: [{ type: "text", text, text_elements: [] }],
|
|
546
|
+
...(currentModel ? { model: currentModel } : {}),
|
|
511
547
|
});
|
|
512
548
|
activeTurn = { threadId, turnId: started?.turn?.id ?? null };
|
|
513
549
|
const timer = setTimeout(() => {
|
|
514
550
|
log(`\x1b[31m[timeout] turn did not finish within ${opts.timeout}s\x1b[0m`);
|
|
515
551
|
client.emit("turnDone", { status: "timeout" });
|
|
516
552
|
}, opts.timeout * 1000);
|
|
517
|
-
await done;
|
|
553
|
+
const turn = await done;
|
|
518
554
|
activeTurn = null;
|
|
519
555
|
clearTimeout(timer);
|
|
556
|
+
return turn;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* A failed turn worth retrying. The upstream moderation layer sometimes flags
|
|
561
|
+
* a perfectly innocent prompt ("Invalid prompt … potentially violating our
|
|
562
|
+
* usage policy") and codex reports willRetry:false, so without this the turn is
|
|
563
|
+
* simply lost. Re-sending the same input almost always succeeds.
|
|
564
|
+
*/
|
|
565
|
+
function isRetryableTurn(turn) {
|
|
566
|
+
if (!turn || turn.status !== "failed") return false;
|
|
567
|
+
const msg = String(turn.error?.message ?? "");
|
|
568
|
+
return /flagged as potentially violating|Invalid prompt/i.test(msg);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function ask(text) {
|
|
572
|
+
const maxRetries = Math.max(0, Math.floor(Number(opts.retry)) || 0);
|
|
573
|
+
const baseDelay = Math.max(0, Number(opts.retryDelayMs) || 0);
|
|
574
|
+
for (let attempt = 0; ; attempt++) {
|
|
575
|
+
const turn = await runTurn(text);
|
|
576
|
+
if (attempt >= maxRetries || !isRetryableTurn(turn)) return;
|
|
577
|
+
const wait = baseDelay * (attempt + 1);
|
|
578
|
+
log(
|
|
579
|
+
`\x1b[33m[retry] retryable failure — retrying in ${wait}ms (attempt ${attempt + 2}/${maxRetries + 1})\x1b[0m`,
|
|
580
|
+
);
|
|
581
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Arrow-key model chooser used by `/model`. Returns a chosen id, or null. */
|
|
586
|
+
async function pickModel(models, activeId) {
|
|
587
|
+
const rows = models.map((m) => {
|
|
588
|
+
const id = m.id ?? m.model;
|
|
589
|
+
const label = m.displayName && m.displayName !== id ? ` \x1b[2m${m.displayName}\x1b[0m` : "";
|
|
590
|
+
return { id, text: `${id}${label}` };
|
|
591
|
+
});
|
|
592
|
+
if (!process.stdin.isTTY) {
|
|
593
|
+
log(" available:");
|
|
594
|
+
for (const r of rows) log(` ${r.text}`);
|
|
595
|
+
log(" pick one with `/model <name>`");
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
let index = Math.max(0, rows.findIndex((r) => r.id === activeId));
|
|
599
|
+
const total = rows.length;
|
|
600
|
+
const draw = (initial) => {
|
|
601
|
+
if (!initial) process.stderr.write(`\x1b[${total + 1}A`);
|
|
602
|
+
for (let i = 0; i < total; i++) {
|
|
603
|
+
const pointer = i === index ? "\x1b[36m›\x1b[0m" : " ";
|
|
604
|
+
const star = rows[i].id === activeId ? " \x1b[32m*\x1b[0m" : "";
|
|
605
|
+
process.stderr.write(`\x1b[2K${pointer} ${rows[i].text}${star}\n`);
|
|
606
|
+
}
|
|
607
|
+
process.stderr.write("\x1b[2K\x1b[2m ↑/↓ move · Enter select · Esc cancel\x1b[0m\n");
|
|
608
|
+
};
|
|
609
|
+
log("[model] select a model:");
|
|
610
|
+
draw(true);
|
|
611
|
+
return await new Promise((resolve) => {
|
|
612
|
+
const stdin = process.stdin;
|
|
613
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
614
|
+
rl?.pause();
|
|
615
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
|
|
616
|
+
stdin.resume();
|
|
617
|
+
const finish = (value) => {
|
|
618
|
+
stdin.removeListener("data", onData);
|
|
619
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(wasRaw);
|
|
620
|
+
stdin.pause();
|
|
621
|
+
rl?.resume();
|
|
622
|
+
resolve(value);
|
|
623
|
+
};
|
|
624
|
+
const onData = (buf) => {
|
|
625
|
+
const s = buf.toString("utf8");
|
|
626
|
+
if (s === "\u0003") {
|
|
627
|
+
finish(null);
|
|
628
|
+
process.kill(process.pid, "SIGINT");
|
|
629
|
+
} else if (s === "\u001b[A" || s === "k") {
|
|
630
|
+
index = (index - 1 + total) % total;
|
|
631
|
+
draw();
|
|
632
|
+
} else if (s === "\u001b[B" || s === "j") {
|
|
633
|
+
index = (index + 1) % total;
|
|
634
|
+
draw();
|
|
635
|
+
} else if (s === "\r" || s === "\n") {
|
|
636
|
+
finish(rows[index].id);
|
|
637
|
+
} else if (s === "\u001b" || s === "q") {
|
|
638
|
+
finish(null);
|
|
639
|
+
} else if (/^[1-9]$/.test(s)) {
|
|
640
|
+
const n = Number(s) - 1;
|
|
641
|
+
if (n < total) {
|
|
642
|
+
index = n;
|
|
643
|
+
draw();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
stdin.on("data", onData);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Interactive slash commands (/model, /help, /quit). */
|
|
652
|
+
async function handleCommand(line) {
|
|
653
|
+
const [cmd, ...rest] = line.slice(1).trim().split(/\s+/);
|
|
654
|
+
switch (cmd) {
|
|
655
|
+
case "help":
|
|
656
|
+
process.stdout.write(HELP);
|
|
657
|
+
return true;
|
|
658
|
+
case "quit":
|
|
659
|
+
case "exit":
|
|
660
|
+
await shutdown(0);
|
|
661
|
+
return true;
|
|
662
|
+
case "model":
|
|
663
|
+
case "models": {
|
|
664
|
+
const models = await loadModels();
|
|
665
|
+
if (!rest.length) {
|
|
666
|
+
log(`[model] current: ${currentModel ?? "(server default)"}`);
|
|
667
|
+
if (!models.length) {
|
|
668
|
+
log(" (this server exposes no model list)");
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
const picked = await pickModel(models, currentModel);
|
|
672
|
+
if (picked && picked !== currentModel) {
|
|
673
|
+
currentModel = picked;
|
|
674
|
+
log(`[model] switched to ${currentModel} \x1b[2m(applies from the next turn)\x1b[0m`);
|
|
675
|
+
updatePrompt();
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
const wanted = rest[0];
|
|
680
|
+
const hit = models.find((m) => m.id === wanted || m.model === wanted || m.displayName === wanted);
|
|
681
|
+
if (models.length && !hit) {
|
|
682
|
+
log(`\x1b[31m[model] unknown model "${wanted}" — run /model to list\x1b[0m`);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
currentModel = hit ? (hit.id ?? hit.model) : wanted;
|
|
686
|
+
log(`[model] switched to ${currentModel} \x1b[2m(applies from the next turn)\x1b[0m`);
|
|
687
|
+
updatePrompt();
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
default:
|
|
691
|
+
log(`unknown command: /${cmd} — try /help`);
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function updatePrompt() {
|
|
697
|
+
if (rl && process.stdin.isTTY) rl.setPrompt(`\x1b[2m${currentModel ?? "model?"}\x1b[0m \x1b[36m›\x1b[0m `);
|
|
520
698
|
}
|
|
521
699
|
|
|
522
700
|
function setupRepl() {
|
|
@@ -527,10 +705,20 @@ export async function runWsClient(argv) {
|
|
|
527
705
|
if (process.stdin.isTTY) rl.prompt();
|
|
528
706
|
return;
|
|
529
707
|
}
|
|
708
|
+
if (text.startsWith("/")) {
|
|
709
|
+
try {
|
|
710
|
+
await handleCommand(text);
|
|
711
|
+
} catch (e) {
|
|
712
|
+
log(`\x1b[31m[error] ${e.message}\x1b[0m`);
|
|
713
|
+
}
|
|
714
|
+
if (process.stdin.isTTY) rl.prompt();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
530
717
|
if (pendingApproval) {
|
|
531
718
|
if (/^(y|yes)$/i.test(text)) decide(pendingApproval.id, "accept");
|
|
532
719
|
else if (/^(a|all)$/i.test(text)) decide(pendingApproval.id, "acceptForSession");
|
|
533
720
|
else decide(pendingApproval.id, "decline");
|
|
721
|
+
updatePrompt();
|
|
534
722
|
if (process.stdin.isTTY) rl.prompt();
|
|
535
723
|
return;
|
|
536
724
|
}
|
|
@@ -548,7 +736,8 @@ export async function runWsClient(argv) {
|
|
|
548
736
|
void shutdown(0);
|
|
549
737
|
});
|
|
550
738
|
if (process.stdin.isTTY) {
|
|
551
|
-
|
|
739
|
+
updatePrompt();
|
|
740
|
+
log("\ntype a message and press Enter (Ctrl-C: interrupt & close / Ctrl-D: exit). approvals: y=once / a=session / n=decline\n /model to list or switch models · /help for all commands\n");
|
|
552
741
|
rl.prompt();
|
|
553
742
|
}
|
|
554
743
|
}
|