agello 0.4.1 → 0.4.3
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 +35 -1
- package/bin/agello.ts +68 -0
- package/package.json +1 -1
- package/src/player.ts +244 -0
- package/src/screencast.ts +71 -1
- package/src/server.ts +129 -15
- package/web/agent-bridge.js +372 -54
package/src/server.ts
CHANGED
|
@@ -6,17 +6,22 @@
|
|
|
6
6
|
// GET /events SSE: status, chat messages, tool start/end (from transcript JSONL)
|
|
7
7
|
// POST /send {action, text, images?: [{type, data(base64)}]} -> herdr agent prompt
|
|
8
8
|
// (images are saved to $TMPDIR/agello-uploads and sent as [image: <path>])
|
|
9
|
+
// (action "annotate": + target {selector, label, rect, text?, url?} from inspect)
|
|
9
10
|
// GET /screen SSE: terminal-browser screen frames (CDP screencast)
|
|
10
11
|
// POST /upload {images: [{type, data(base64)}]} -> {paths}: terminal image paste
|
|
11
12
|
// WS /terminal interactive herdr terminal frames, keyboard input, resize
|
|
12
13
|
// GET /panes herdr workspace -> tab -> pane tree
|
|
13
14
|
// POST /panes/connect {pane} -> url of that pane's server (started if needed)
|
|
14
15
|
// POST /panes/create {kind: workspace|tab|pane, cwd?, label?, workspace?, pane?, direction?}
|
|
15
|
-
// WS /input user control: CDP Input.* commands forwarded to the page
|
|
16
|
+
// WS /input user control: CDP Input.* commands forwarded to the page;
|
|
17
|
+
// {id, inspect: {x, y} | {selector}, full?} -> {id, result}: element at a point (annotation)
|
|
16
18
|
// GET /present presentation state
|
|
17
19
|
// POST /present {rect?: {x,y,w,h}} start (or move the crop), {stop: true} end
|
|
18
20
|
// ({stop: true, from: "viewer"}: ended from the page, the agent is told
|
|
19
21
|
// with "[browser] action=present-stop")
|
|
22
|
+
// GET /script presentation script + player position
|
|
23
|
+
// POST /script {script: {rect?, steps: [{go?, say: string[], hold?}]}} load (keeps position)
|
|
24
|
+
// POST /player {cmd: "resume" | "pause" | "goto", at?: "3" | "3.2"}
|
|
20
25
|
|
|
21
26
|
import { createTerminal, type SocketData } from "./terminal.ts";
|
|
22
27
|
import { panesRoute } from "./panes.ts";
|
|
@@ -24,7 +29,8 @@ import { mkdir } from "node:fs/promises";
|
|
|
24
29
|
import { homedir, tmpdir } from "node:os";
|
|
25
30
|
import { join } from "node:path";
|
|
26
31
|
import { runJson } from "./run.ts";
|
|
27
|
-
import {
|
|
32
|
+
import { createPlayer, parsePos, parseScript, type PlayerInfo } from "./player.ts";
|
|
33
|
+
import { createScreencast, type Inspected, type Rect } from "./screencast.ts";
|
|
28
34
|
|
|
29
35
|
export type ServerOptions = {
|
|
30
36
|
port: number;
|
|
@@ -123,7 +129,22 @@ export function parseRect(r: any): Rect | null {
|
|
|
123
129
|
return { x, y, w, h };
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
// Annotation prompt: the viewer's request plus the element it points at, in
|
|
133
|
+
// 3 lines (request / selector / element summary).
|
|
134
|
+
export function formatAnnotation(text: string, t: any): string | null {
|
|
135
|
+
if (!t || typeof t.selector !== "string" || !t.selector || t.selector.length > 1000) return null;
|
|
136
|
+
const clean = (v: unknown, n: number) => String(v ?? "").replace(/\s+/g, " ").trim().slice(0, n);
|
|
137
|
+
const request = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).join(" / ");
|
|
138
|
+
const r = t.rect ?? {};
|
|
139
|
+
const [x, y, w, h] = [r.x, r.y, r.w, r.h].map((v) => Math.round(Number(v) || 0));
|
|
140
|
+
const label = clean(t.label, 120);
|
|
141
|
+
const snippet = clean(t.text, 80);
|
|
142
|
+
const url = clean(t.url, 300);
|
|
143
|
+
const info = [`요소: ${label}${snippet ? ` "${snippet}"` : ""}`, `위치 ${x},${y} 크기 ${w}x${h}`, url].filter(Boolean);
|
|
144
|
+
return [request, `대상: ${clean(t.selector, 1000)}`, info.join(" · ")].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type Present = { on: boolean; rect?: Rect; since?: string; player?: PlayerInfo };
|
|
127
148
|
|
|
128
149
|
type Status =
|
|
129
150
|
| { alive: false; reason: string; pane: string }
|
|
@@ -351,10 +372,67 @@ export async function startServer(opts: ServerOptions) {
|
|
|
351
372
|
// short-lived bubbles. rect: crop of the visible viewport (unset = whole screen).
|
|
352
373
|
|
|
353
374
|
let present: Present = { on: false };
|
|
375
|
+
const presentView = (): Present => ({ ...present, player: player.info() });
|
|
354
376
|
function setPresent(next: Present) {
|
|
355
377
|
present = next;
|
|
356
378
|
screen.setClip(next.on ? (next.rect ?? null) : null);
|
|
357
|
-
|
|
379
|
+
if (!next.on) player.stop();
|
|
380
|
+
broadcast("present", presentView());
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Script player: agello owns order and timing, the agent owns the deck
|
|
384
|
+
// (each step's `go`) and decides when to resume after a question.
|
|
385
|
+
const player = createPlayer({
|
|
386
|
+
async go(expr) {
|
|
387
|
+
const js = expr.startsWith("#") ? `location.hash = ${JSON.stringify(expr)}` : expr;
|
|
388
|
+
if (!(await screen.evaluate(js))) console.error(`present: go failed: ${expr}`);
|
|
389
|
+
},
|
|
390
|
+
say(text, at, hold) {
|
|
391
|
+
broadcast("message", { role: "assistant", text, ts: new Date().toISOString(), script: at, hold });
|
|
392
|
+
},
|
|
393
|
+
done() {
|
|
394
|
+
const n = player.info().steps;
|
|
395
|
+
promptAgent("present-done", `대본 끝까지 재생함 (${n}장)`);
|
|
396
|
+
},
|
|
397
|
+
changed() {
|
|
398
|
+
broadcast("present", presentView());
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// "마지막 표시 3.2 · 다음 3.3" for agent notices
|
|
403
|
+
const where = () => {
|
|
404
|
+
const i = player.info();
|
|
405
|
+
if (i.state === "empty") return "";
|
|
406
|
+
return [i.last && `마지막 표시 ${i.last}`, i.next && `다음 ${i.next}`].filter(Boolean).join(" · ");
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
async function scriptRequest(req: Request): Promise<Response> {
|
|
410
|
+
if (req.method !== "POST") return Response.json({ script: player.script(), player: player.info() });
|
|
411
|
+
const data = (await req.json().catch(() => ({}))) as { script?: unknown };
|
|
412
|
+
const script = parseScript(data.script);
|
|
413
|
+
if (typeof script === "string") return Response.json({ ok: false, error: script }, { status: 400 });
|
|
414
|
+
if (script.rect && !parseRect(script.rect))
|
|
415
|
+
return Response.json({ ok: false, error: "invalid_rect" }, { status: 400 });
|
|
416
|
+
player.load(script);
|
|
417
|
+
return Response.json({ ok: true, player: player.info() });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function playerRequest(req: Request): Promise<Response> {
|
|
421
|
+
const data = (await req.json().catch(() => ({}))) as { cmd?: string; at?: string };
|
|
422
|
+
let error: string | null = null;
|
|
423
|
+
if (data.cmd === "resume") {
|
|
424
|
+
if (!present.on) {
|
|
425
|
+
const rect = player.script()?.rect;
|
|
426
|
+
setPresent({ on: true, rect: rect ? parseRect(rect)! : undefined, since: new Date().toISOString() });
|
|
427
|
+
}
|
|
428
|
+
error = player.resume();
|
|
429
|
+
} else if (data.cmd === "pause") player.pause();
|
|
430
|
+
else if (data.cmd === "goto") {
|
|
431
|
+
const pos = parsePos(data.at ?? "");
|
|
432
|
+
error = pos ? await player.goto(pos) : "invalid_position";
|
|
433
|
+
} else error = "unknown_cmd";
|
|
434
|
+
if (error) return Response.json({ ok: false, error, player: player.info() }, { status: 400 });
|
|
435
|
+
return Response.json({ ok: true, player: player.info() });
|
|
358
436
|
}
|
|
359
437
|
|
|
360
438
|
async function presentRequest(req: Request): Promise<Response> {
|
|
@@ -366,10 +444,11 @@ export async function startServer(opts: ServerOptions) {
|
|
|
366
444
|
// (The CLI stop comes from the agent itself and needs no notice.)
|
|
367
445
|
let notified: string | undefined;
|
|
368
446
|
if (was && data.from === "viewer") {
|
|
369
|
-
const
|
|
447
|
+
const at = where();
|
|
448
|
+
const r = await promptAgent("present-stop", `사용자가 페이지에서 발표를 종료함${at ? ` (대본 정지: ${at})` : ""}`);
|
|
370
449
|
notified = r.ok ? "ok" : r.error;
|
|
371
450
|
}
|
|
372
|
-
return Response.json({ ok: true, present, notified });
|
|
451
|
+
return Response.json({ ok: true, present: presentView(), notified });
|
|
373
452
|
}
|
|
374
453
|
let rect: Rect | undefined;
|
|
375
454
|
if (data.rect != null) {
|
|
@@ -377,7 +456,7 @@ export async function startServer(opts: ServerOptions) {
|
|
|
377
456
|
if (!rect) return Response.json({ ok: false, error: "invalid_rect" }, { status: 400 });
|
|
378
457
|
}
|
|
379
458
|
setPresent({ on: true, rect, since: present.on ? present.since : new Date().toISOString() });
|
|
380
|
-
return Response.json({ ok: true, present });
|
|
459
|
+
return Response.json({ ok: true, present: presentView() });
|
|
381
460
|
}
|
|
382
461
|
|
|
383
462
|
// ----- HTTP -----
|
|
@@ -418,7 +497,7 @@ export async function startServer(opts: ServerOptions) {
|
|
|
418
497
|
tools: [...pendingTools.keys()],
|
|
419
498
|
});
|
|
420
499
|
if (lastStatus) send(c, "status", lastStatus);
|
|
421
|
-
send(c, "present",
|
|
500
|
+
send(c, "present", presentView());
|
|
422
501
|
for (const t of pendingTools.values()) send(c, "tool_start", t);
|
|
423
502
|
ping = setInterval(() => send(c, "ping", {}), 15000);
|
|
424
503
|
},
|
|
@@ -443,11 +522,25 @@ export async function startServer(opts: ServerOptions) {
|
|
|
443
522
|
}
|
|
444
523
|
|
|
445
524
|
async function sendPrompt(req: Request): Promise<Response> {
|
|
446
|
-
const data = (await req.json().catch(() => ({}))) as { action?: string; text?: string; images?: unknown };
|
|
525
|
+
const data = (await req.json().catch(() => ({}))) as { action?: string; text?: string; images?: unknown; target?: Inspected };
|
|
447
526
|
const action = /^[\w-]{1,32}$/.test(data.action ?? "") ? data.action! : "message";
|
|
527
|
+
let text = data.text ?? "";
|
|
528
|
+
if (action === "annotate") {
|
|
529
|
+
const t = formatAnnotation(text, data.target);
|
|
530
|
+
if (!t || !text.trim()) return Response.json({ ok: false, error: "invalid_target" }, { status: 400 });
|
|
531
|
+
text = t;
|
|
532
|
+
}
|
|
533
|
+
// Raised hand: stop the script at once (no agent round trip), rewind so
|
|
534
|
+
// resume replays the interrupted line, and tell the agent where it stopped.
|
|
535
|
+
if (action === "hand-raise") {
|
|
536
|
+
player.pause();
|
|
537
|
+
player.rewind();
|
|
538
|
+
const at = where();
|
|
539
|
+
if (at) text += ` (대본 정지: ${at})`;
|
|
540
|
+
}
|
|
448
541
|
const images = await saveImages(data.images);
|
|
449
542
|
if (!images) return Response.json({ ok: false, error: "invalid_images" }, { status: 400 });
|
|
450
|
-
|
|
543
|
+
text = [text, ...images.map((p) => `[image: ${p}]`)].join("\n");
|
|
451
544
|
const r = await promptAgent(action, text);
|
|
452
545
|
return r.ok ? Response.json({ ok: true }) : Response.json({ ok: false, error: r.error }, { status: r.status });
|
|
453
546
|
}
|
|
@@ -463,7 +556,7 @@ export async function startServer(opts: ServerOptions) {
|
|
|
463
556
|
...(await checkStatus()),
|
|
464
557
|
browser: opts.browser,
|
|
465
558
|
screen: await screen.describe(),
|
|
466
|
-
present,
|
|
559
|
+
present: presentView(),
|
|
467
560
|
});
|
|
468
561
|
case "/events":
|
|
469
562
|
return events();
|
|
@@ -488,7 +581,12 @@ export async function startServer(opts: ServerOptions) {
|
|
|
488
581
|
}
|
|
489
582
|
break;
|
|
490
583
|
case "/present":
|
|
491
|
-
return req.method === "POST" ? presentRequest(req) : Response.json(
|
|
584
|
+
return req.method === "POST" ? presentRequest(req) : Response.json(presentView());
|
|
585
|
+
case "/script":
|
|
586
|
+
return scriptRequest(req);
|
|
587
|
+
case "/player":
|
|
588
|
+
if (req.method === "POST") return playerRequest(req);
|
|
589
|
+
break;
|
|
492
590
|
}
|
|
493
591
|
return new Response(HTML, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" } });
|
|
494
592
|
}
|
|
@@ -500,9 +598,25 @@ export async function startServer(opts: ServerOptions) {
|
|
|
500
598
|
websocket: {
|
|
501
599
|
maxPayloadLength: 128 * 1024,
|
|
502
600
|
open(ws) { if (ws.data.kind === "terminal") terminal.open(ws); },
|
|
503
|
-
message(ws, msg) {
|
|
504
|
-
|
|
505
|
-
|
|
601
|
+
async message(ws, msg) {
|
|
602
|
+
const raw = String(msg);
|
|
603
|
+
if (ws.data.kind === "terminal") return terminal.message(ws, raw);
|
|
604
|
+
let m: any;
|
|
605
|
+
try {
|
|
606
|
+
m = JSON.parse(raw);
|
|
607
|
+
} catch {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (m?.inspect) {
|
|
611
|
+
const q = m.inspect;
|
|
612
|
+
const query =
|
|
613
|
+
typeof q.selector === "string"
|
|
614
|
+
? { selector: q.selector.slice(0, 1000), full: !!q.full }
|
|
615
|
+
: { x: Number(q.x) || 0, y: Number(q.y) || 0, full: !!q.full };
|
|
616
|
+
ws.send(JSON.stringify({ id: m.id, result: await screen.inspect(query) }));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
screen.input(raw);
|
|
506
620
|
},
|
|
507
621
|
close(ws) { if (ws.data.kind === "terminal") terminal.close(ws); },
|
|
508
622
|
},
|