@flow-engineer/flow 0.2.1 → 0.2.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 +167 -10
- package/coding-agents/auth-skill.md +137 -0
- package/coding-agents/context-skill.md +94 -0
- package/coding-agents/skill.md +219 -17
- package/coding-agents/test-skill.md +3 -1
- package/coding-agents/tools-skill.md +78 -1
- package/dist/cli/src/auth.d.ts +11 -0
- package/dist/cli/src/auth.js +147 -0
- package/dist/cli/src/commands.d.ts +119 -0
- package/dist/cli/src/commands.js +212 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +5 -0
- package/dist/cli/src/run.d.ts +28 -0
- package/dist/cli/src/run.js +523 -0
- package/dist/cli/src/terminal.d.ts +3 -0
- package/dist/cli/src/terminal.js +135 -0
- package/dist/flow-messaging/src/client.d.ts +2 -2
- package/dist/flow-messaging/src/client.js +3 -3
- package/dist/flow-messaging/src/index.js +17 -9
- package/dist/mcp/src/client.d.ts +54 -0
- package/dist/mcp/src/client.js +175 -0
- package/dist/mcp/src/describe.d.ts +41 -0
- package/dist/mcp/src/describe.js +110 -0
- package/dist/mcp/src/index.d.ts +4 -0
- package/dist/mcp/src/index.js +7 -0
- package/dist/mcp/src/server.d.ts +70 -0
- package/dist/mcp/src/server.js +252 -0
- package/dist/mcp/src/terminal.d.ts +24 -0
- package/dist/mcp/src/terminal.js +54 -0
- package/dist/simulators/src/index.d.ts +1 -1
- package/dist/simulators/src/page.html +267 -49
- package/dist/simulators/src/server.d.ts +27 -0
- package/dist/simulators/src/server.js +264 -40
- package/dist/src/agent.d.ts +69 -3
- package/dist/src/agent.js +162 -8
- package/dist/src/channels.d.ts +6 -4
- package/dist/src/channels.js +4 -3
- package/dist/src/conversation.d.ts +17 -1
- package/dist/src/conversation.js +170 -16
- package/dist/src/events.d.ts +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/model.d.ts +27 -0
- package/dist/src/model.js +85 -13
- package/dist/src/skills.d.ts +4 -0
- package/dist/src/skills.js +23 -0
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +18 -0
- package/dist/src/tools.d.ts +69 -0
- package/dist/src/tools.js +17 -1
- package/dist/src/traces.js +32 -4
- package/dist/src/types.d.ts +5 -0
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +368 -57
- package/dist/testing/src/chat.js +24 -16
- package/dist/testing/src/check.d.ts +4 -0
- package/dist/testing/src/check.js +90 -0
- package/dist/testing/src/cli.js +77 -19
- package/dist/testing/src/flow-login.d.ts +33 -0
- package/dist/testing/src/flow-login.js +30 -5
- package/dist/testing/src/new.d.ts +14 -5
- package/dist/testing/src/new.js +54 -20
- package/dist/testing/src/plugin.d.ts +4 -0
- package/dist/testing/src/plugin.js +116 -0
- package/docs/agents.md +87 -12
- package/examples/cafe/src/cli.ts +24 -0
- package/examples/cafe/src/mcp.ts +22 -0
- package/examples/cafe/src/tools.ts +13 -0
- package/flow-messaging/README.md +3 -1
- package/package.json +28 -1
|
@@ -13,26 +13,50 @@
|
|
|
13
13
|
:root { --bg: #161615; --panel: #1f1f1d; --ink: #ecebe6; --quiet: #9a9a94; --line: #33332f; }
|
|
14
14
|
}
|
|
15
15
|
* { box-sizing: border-box; }
|
|
16
|
-
|
|
16
|
+
[hidden] { display: none !important; }
|
|
17
|
+
body { margin: 0; font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--ink); height: 100dvh; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
17
18
|
button { font: inherit; }
|
|
18
|
-
.top { display:
|
|
19
|
-
.top h1 { font-size:
|
|
20
|
-
.top h1
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
.tab
|
|
24
|
-
.tab
|
|
25
|
-
.tab
|
|
26
|
-
.tab {
|
|
27
|
-
.tab .
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
|
|
19
|
+
.top { display: grid; grid-template-columns: minmax(160px, 1fr) auto minmax(0, 1fr); align-items: center; gap: 20px; padding: 12px 20px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
20
|
+
.top h1 { min-width: 0; font-size: 14px; margin: 0; font-weight: 650; line-height: 1.35; }
|
|
21
|
+
.top h1 .agent-title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
22
|
+
.top h1 small { display: block; margin-top: 2px; color: var(--quiet); font-size: 11px; font-weight: 400; }
|
|
23
|
+
.tabs { display: flex; align-items: center; justify-self: center; gap: 4px; max-width: 100%; padding: 6px; border: 1px solid var(--line); border-radius: 14px; background: var(--bg); overflow-x: auto; scrollbar-width: thin; }
|
|
24
|
+
.tab { position: relative; display: flex; flex: none; align-items: center; justify-content: center; min-width: 36px; height: 36px; padding: 8px; border: 0; border-radius: 9px; background: transparent; color: var(--quiet); cursor: pointer; transition: background .16s, color .16s; }
|
|
25
|
+
.tab img, .tab svg { display: block; width: 20px; height: 20px; flex: none; }
|
|
26
|
+
.tab-icon.web { color: #4679d8; } .tab-icon.terminal { color: var(--ink); } .tab-icon.imessage { color: #32bc4c; } .tab-icon.sms { color: #3578e5; }
|
|
27
|
+
.tab-label { display: flex; align-items: center; gap: 6px; max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap; padding-left: 0; font-size: 12px; font-weight: 550; transition: max-width .2s ease, opacity .16s ease, padding-left .2s ease; }
|
|
28
|
+
.tab:hover .tab-label, .tab:focus-visible .tab-label { max-width: 160px; opacity: 1; padding-left: 8px; }
|
|
29
|
+
.tab:hover { background: var(--panel); color: var(--ink); }
|
|
30
|
+
.tab[aria-pressed="true"] { background: var(--panel); color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset, 0 1px 3px rgba(0,0,0,.04); }
|
|
31
|
+
.tab:focus-visible, .ghost:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
32
|
+
.tab .unread { position: absolute; top: -3px; right: -3px; min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px; background: #e5484d; color: #fff; font-size: 9px; font-weight: 700; line-height: 15px; text-align: center; }
|
|
33
|
+
.header-controls { display: flex; justify-self: end; align-items: flex-end; gap: 8px; white-space: nowrap; }
|
|
34
|
+
.person-picker { display: grid; gap: 3px; min-width: 0; }
|
|
35
|
+
.person-picker > span { color: var(--quiet); font-size: 10px; font-weight: 550; }
|
|
36
|
+
.ghost { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: transparent; color: var(--ink); cursor: pointer; }
|
|
37
|
+
.ghost:hover { background: var(--bg); }
|
|
38
|
+
.header-actions { display: flex; align-items: center; gap: 6px; padding-left: 10px; border-left: 1px solid var(--line); }
|
|
39
|
+
.header-actions .ghost { font-size: 12px; }
|
|
40
|
+
.header-actions svg { width: 14px; height: 14px; flex: none; }
|
|
41
|
+
select.ghost { display: block; width: 190px; max-width: 100%; font-size: 12px; text-overflow: ellipsis; }
|
|
42
|
+
@media (max-width: 1240px) {
|
|
43
|
+
.top { grid-template-columns: minmax(0, 1fr) auto; gap: 12px 20px; }
|
|
44
|
+
.tabs { grid-row: 2; grid-column: 1 / -1; justify-self: start; }
|
|
45
|
+
}
|
|
46
|
+
@media (max-width: 580px) {
|
|
47
|
+
.top { grid-template-columns: minmax(0, 1fr); padding: 12px 14px; gap: 12px; }
|
|
48
|
+
.tabs { grid-row: auto; grid-column: auto; justify-self: stretch; width: fit-content; }
|
|
49
|
+
.header-controls { justify-self: stretch; justify-content: space-between; gap: 10px; }
|
|
50
|
+
.person-picker { flex: 1; } select.ghost { width: 100%; }
|
|
51
|
+
.header-actions { padding-left: 8px; }
|
|
52
|
+
}
|
|
53
|
+
@media (hover: none) { .tab:hover:not(:focus-visible) .tab-label { max-width: 0; opacity: 0; padding-left: 0; } }
|
|
54
|
+
@media (prefers-reduced-motion: reduce) { .tab, .tab-label { transition: none; } }
|
|
31
55
|
.empty { grid-column: 1 / -1; padding: 32px; text-align: center; color: var(--quiet); }
|
|
32
56
|
.empty code { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; }
|
|
33
57
|
body:not(.play) .play-only, body.play .cloud-only { display: none; }
|
|
34
|
-
main { display: grid; grid-template-columns: minmax(0, 400px) minmax(0, 1fr); gap: 16px; padding: 16px; height:
|
|
35
|
-
@media (max-width: 820px) { main { grid-template-columns: 1fr; height: auto; } .side { height: 520px; } .phone { height: 680px; } }
|
|
58
|
+
main { display: grid; grid-template-columns: minmax(0, 400px) minmax(0, 1fr); gap: 16px; padding: 16px; min-height: 0; }
|
|
59
|
+
@media (max-width: 820px) { body { height: auto; min-height: 100dvh; } main { grid-template-columns: 1fr; height: auto; } .side { height: 520px; } .phone { height: 680px; } }
|
|
36
60
|
|
|
37
61
|
/* The phone */
|
|
38
62
|
.phone { border-radius: 28px; overflow: hidden; display: flex; flex-direction: column; min-height: 0; border: 1px solid var(--line); box-shadow: 0 2px 10px rgba(0,0,0,.06); position: relative; }
|
|
@@ -233,24 +257,60 @@
|
|
|
233
257
|
.log div { display: grid; grid-template-columns: 70px 1fr; gap: 8px; border-bottom: 1px dashed var(--line); padding: 2px 0; }
|
|
234
258
|
.log .t { color: var(--quiet); }
|
|
235
259
|
.tag { font-weight: 600; } .tag.inline { color: var(--good); } .tag.bg { color: var(--warn); } .tag.fold, .tag.ack { color: var(--fold); } .tag.bad { color: var(--bad); }
|
|
260
|
+
.auth-state { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--line, #ddd); font-size: 13px; }
|
|
261
|
+
.auth-state[hidden] { display: none; }
|
|
262
|
+
.auth-state a { color: var(--accent); text-underline-offset: 3px; }
|
|
263
|
+
.auth-state button { margin-left: auto; font-size: 12px; }
|
|
264
|
+
.local-cli-mode main { grid-template-columns: minmax(0, 1fr); }
|
|
265
|
+
.local-cli-mode .side { display: none; }
|
|
266
|
+
.local-cli-mode .person-picker, .local-cli-mode #debug { display: none; }
|
|
267
|
+
.cli-console { min-height: 0; display: flex; flex-direction: column; border: 1px solid #303844; border-radius: 12px; overflow: hidden; background: #11161e; color: #e2e8f0; font: 13px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
268
|
+
.cli-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 12px 18px; background: #1a212c; border-bottom: 1px solid #303844; }
|
|
269
|
+
.cli-bar b { display: block; font-size: 12px; }
|
|
270
|
+
.cli-bar small { color: #9caabd; font-size: 11px; }
|
|
271
|
+
.cli-console button { border: 1px solid #43516a; border-radius: 5px; background: #273243; color: #d0daea; padding: 6px 12px; cursor: pointer; font: inherit; font-size: 12px; }
|
|
272
|
+
.cli-console button:disabled { opacity: .5; cursor: default; }
|
|
273
|
+
.cli-console pre { flex: 1; min-height: 120px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; padding: 18px 20px; font: inherit; }
|
|
274
|
+
.cli-console .stderr { color: #c9bb9b; }
|
|
275
|
+
.cli-console .input { color: #b8dfc1; }
|
|
276
|
+
.cli-console .exit { display: block; color: #96a5ba; font-size: 11px; margin: 4px 0 16px; }
|
|
277
|
+
.cli-login { margin: 0 18px 12px; color: #9fc5ff; }
|
|
278
|
+
.cli-form { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-top: 1px solid #303844; background: #161d27; }
|
|
279
|
+
.cli-form > span { color: #b8dfc1; }
|
|
280
|
+
.cli-form input { flex: 1; min-width: 0; border: 0; outline: none; padding: 8px 0; background: transparent; color: #e2e8f0; font: inherit; }
|
|
281
|
+
.cli-form input::placeholder { color: #94a3b8; }
|
|
282
|
+
.cli-form:focus-within { box-shadow: inset 0 2px #719c7b; }
|
|
283
|
+
.cli-state { color: #94a3b8; padding: 0 18px 8px; font-size: 11px; }
|
|
284
|
+
@media (max-width: 820px) { .cli-console { height: 680px; } }
|
|
236
285
|
</style>
|
|
237
286
|
</head>
|
|
238
287
|
<body>
|
|
239
|
-
<
|
|
240
|
-
<h1>{{title}}
|
|
288
|
+
<header class="top">
|
|
289
|
+
<h1><span class="agent-title">{{title}}</span><small>Flow Framework playground</small></h1>
|
|
241
290
|
<div class="tabs" id="tabs" role="group" aria-label="Platform"></div>
|
|
242
|
-
<div class="
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
</
|
|
291
|
+
<div class="header-controls">
|
|
292
|
+
<label class="person-picker play-only" for="who"><span>Person</span><select class="ghost" id="who" aria-label="Person" title="Choose a saved test person, or start as a new signed-out user"></select></label>
|
|
293
|
+
<div class="header-actions">
|
|
294
|
+
<button class="ghost" id="debug" type="button" aria-pressed="false" title="Show the agent's tool calls in the conversation"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m8 8-4 4 4 4m8-8 4 4-4 4m-3-11-2 14"/></svg>Debug</button>
|
|
295
|
+
<button class="ghost cloud-only" id="reset" type="button">New conversation</button>
|
|
296
|
+
<button class="ghost play-only" id="clear" type="button" title="Forget this person: their history, schedules and everything the agent kept about them"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 10v7m4-7v7"/></svg>Clear history</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</header>
|
|
248
300
|
<main>
|
|
301
|
+
<section class="cli-console" id="cliPanel" aria-label="Local CLI" hidden>
|
|
302
|
+
<div class="cli-bar"><div><b id="cliTitle">Local CLI</b><small>Runs on this computer · independent sign-in · no shell</small></div><button type="button" id="cliReset">New CLI session</button></div>
|
|
303
|
+
<pre id="cliOutput" role="log" aria-label="CLI output" aria-live="polite" tabindex="0"></pre>
|
|
304
|
+
<a class="cli-login" id="cliLogin" target="_blank" rel="noopener noreferrer" hidden>Open sign-in, then return here</a>
|
|
305
|
+
<form class="cli-form" id="cliForm"><span aria-hidden="true">$</span><input id="cliInput" autocomplete="off" spellcheck="false" aria-label="CLI input"><button type="submit" id="cliSend" disabled>Run ↵</button></form>
|
|
306
|
+
<div class="cli-state" id="cliState">Ready</div>
|
|
307
|
+
</section>
|
|
249
308
|
<section class="phone" id="phone" aria-label="Simulator">
|
|
250
309
|
<div class="bar" id="bar">
|
|
251
310
|
<div class="avatar" id="avatar"></div>
|
|
252
311
|
<div class="who"><b id="name">{{title}}</b><span id="sub"></span></div>
|
|
253
312
|
</div>
|
|
313
|
+
<div class="auth-state" id="authState" hidden><span id="authLabel"></span><a id="signIn" hidden>Sign in</a><button class="ghost" id="startSignedOut" type="button">New signed-out user</button></div>
|
|
254
314
|
<div class="thread" id="thread" aria-live="polite"></div>
|
|
255
315
|
<div class="replying" id="replying"><b>Replying to</b><div id="replyingText"></div><button type="button" id="cancelReply" aria-label="Cancel reply">✕</button></div>
|
|
256
316
|
<form class="compose" id="form">
|
|
@@ -358,8 +418,13 @@
|
|
|
358
418
|
let person = params.get("person") || (PLAY ? recall("chinwag.person") : null) || "demo";
|
|
359
419
|
let people = []; // the playground: the agent's test people
|
|
360
420
|
let channels = []; // the playground: the agent's channels, each with the platform that plays it
|
|
421
|
+
let authentication = false;
|
|
422
|
+
let localCli = null;
|
|
423
|
+
let cliEpoch = 0, cliSeq = 0, cliRunning = false, cliWaiting = false;
|
|
424
|
+
let signedIn = true;
|
|
361
425
|
let title = document.getElementById("name").textContent;
|
|
362
426
|
const $ = (id) => document.getElementById(id);
|
|
427
|
+
const clearLabel = $("clear").lastChild;
|
|
363
428
|
const TAPBACKS = [["❤️", "Loved"], ["👍", "Liked"], ["👎", "Disliked"], ["😂", "Laughed at"], ["‼️", "Emphasized"], ["❓", "Questioned"]];
|
|
364
429
|
let platforms = [];
|
|
365
430
|
let cards = [];
|
|
@@ -372,6 +437,9 @@
|
|
|
372
437
|
const rows = new Map(); // message id → row element
|
|
373
438
|
|
|
374
439
|
const where = () => PLAY ? { channel: current.channel, person } : { platform: current.id, person };
|
|
440
|
+
const isVisitor = () => authentication && person === "visitor";
|
|
441
|
+
const isLocalCli = () => PLAY && localCli && current?.channel === localCli.channel;
|
|
442
|
+
const draftKey = () => `chinwag.draft.${person}.${current.channel}`;
|
|
375
443
|
const query = () => "?" + new URLSearchParams(where()).toString();
|
|
376
444
|
const fmt = (ms) => ms == null ? "…" : ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;
|
|
377
445
|
const clock = (at) => new Date(at).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
@@ -398,6 +466,29 @@
|
|
|
398
466
|
}
|
|
399
467
|
const tab = PLAY ? current.channel : current.id;
|
|
400
468
|
for (const b of document.querySelectorAll(".tab")) b.setAttribute("aria-pressed", String(b.dataset.id === tab));
|
|
469
|
+
cliEpoch++;
|
|
470
|
+
const cli = isLocalCli();
|
|
471
|
+
document.body.classList.toggle("local-cli-mode", !!cli);
|
|
472
|
+
$("phone").hidden = !!cli;
|
|
473
|
+
$("cliPanel").hidden = !cli;
|
|
474
|
+
$("who").disabled = !!cli;
|
|
475
|
+
$("debug").disabled = !!cli;
|
|
476
|
+
clearLabel.textContent = cli ? "Clear output" : "Clear history";
|
|
477
|
+
$("clear").title = cli ? "Clear the visible CLI output; keep this session's sign-in" : "Forget this person: their history, schedules and everything the agent kept about them";
|
|
478
|
+
if (cli) {
|
|
479
|
+
++opened;
|
|
480
|
+
source?.close(); source = null;
|
|
481
|
+
$("cliTitle").textContent = `${localCli.name} · local CLI`;
|
|
482
|
+
$("cliInput").placeholder = `${localCli.name} --help`;
|
|
483
|
+
$("cliSend").disabled = true;
|
|
484
|
+
cliSeq = 0;
|
|
485
|
+
$("cliOutput").textContent = "";
|
|
486
|
+
$("cliLogin").hidden = true;
|
|
487
|
+
void pollCli(cliEpoch);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
signedIn = !isVisitor();
|
|
491
|
+
$("authState").hidden = !isVisitor();
|
|
401
492
|
void refreshPerson();
|
|
402
493
|
$("phone").className = "phone p-" + current.id;
|
|
403
494
|
$("avatar").textContent = title.trim().charAt(0).toUpperCase();
|
|
@@ -414,7 +505,7 @@
|
|
|
414
505
|
$("text").placeholder = current.id === "sms" ? "Text message" : "Message";
|
|
415
506
|
updateCounter();
|
|
416
507
|
if (!current.complete) sysnote("This simulator doesn't model every feature of the real platform yet.");
|
|
417
|
-
connect();
|
|
508
|
+
connect().catch(() => sysnote("Could not load this conversation. Try again.", true));
|
|
418
509
|
}
|
|
419
510
|
|
|
420
511
|
function sysnote(text, bad) {
|
|
@@ -424,6 +515,61 @@
|
|
|
424
515
|
insert(el);
|
|
425
516
|
}
|
|
426
517
|
|
|
518
|
+
function cliOutput(stream, text) {
|
|
519
|
+
const output = $("cliOutput");
|
|
520
|
+
const follow = output.scrollHeight - output.scrollTop - output.clientHeight < 60;
|
|
521
|
+
const chunk = document.createElement("span");
|
|
522
|
+
chunk.className = stream;
|
|
523
|
+
// The CLI writes plain text/ANSI, never HTML. This viewport does not emulate a full TTY.
|
|
524
|
+
chunk.textContent = text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
525
|
+
output.appendChild(chunk);
|
|
526
|
+
while (output.textContent.length > 128 * 1024 && output.children.length > 1) output.firstChild.remove();
|
|
527
|
+
if (follow) output.scrollTop = output.scrollHeight;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function pollCli(epoch) {
|
|
531
|
+
if (epoch !== cliEpoch || !isLocalCli()) return;
|
|
532
|
+
try {
|
|
533
|
+
const res = await fetch(BASE + `/api/cli?after=${cliSeq}`);
|
|
534
|
+
if (!res.ok) throw new Error("Could not read CLI output.");
|
|
535
|
+
const state = await res.json();
|
|
536
|
+
if (epoch !== cliEpoch) return;
|
|
537
|
+
for (const event of state.events) {
|
|
538
|
+
if (event.stream === "login") {
|
|
539
|
+
const url = new URL(event.text);
|
|
540
|
+
if (["http:", "https:"].includes(url.protocol)) { $("cliLogin").href = url.href; $("cliLogin").hidden = false; }
|
|
541
|
+
} else {
|
|
542
|
+
cliOutput(event.stream, event.stream === "exit" ? `exit ${event.text}\n` : event.text);
|
|
543
|
+
if (event.stream === "exit") $("cliLogin").hidden = true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
cliSeq = state.seq; cliRunning = state.running; cliWaiting = state.waiting;
|
|
547
|
+
$("cliSend").disabled = cliRunning && !cliWaiting;
|
|
548
|
+
$("cliSend").textContent = cliRunning ? "Send ↵" : "Run ↵";
|
|
549
|
+
$("cliInput").placeholder = cliWaiting ? "Type a reply…" : cliRunning ? "Command running…" : `${localCli.name} --help`;
|
|
550
|
+
$("cliState").textContent = cliWaiting ? "Waiting for input" : cliRunning ? "Running" : "Ready · use auth login to sign in, chat to talk, or --help for commands";
|
|
551
|
+
} catch { if (epoch === cliEpoch) $("cliState").textContent = "Connection interrupted · reconnecting…"; }
|
|
552
|
+
if (epoch === cliEpoch && isLocalCli()) setTimeout(() => pollCli(epoch), 400);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
$("cliForm").addEventListener("submit", async (event) => {
|
|
556
|
+
event.preventDefault();
|
|
557
|
+
const text = $("cliInput").value;
|
|
558
|
+
if (!text.trim()) return;
|
|
559
|
+
$("cliSend").disabled = true;
|
|
560
|
+
try {
|
|
561
|
+
const res = await fetch(BASE + "/api/cli/input", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }) });
|
|
562
|
+
if (res.ok) $("cliInput").value = "";
|
|
563
|
+
else cliOutput("stderr", `${(await res.json()).error || "Could not send input."}\n`);
|
|
564
|
+
} catch { cliOutput("stderr", "Could not send input. Check the connection before trying again.\n"); }
|
|
565
|
+
});
|
|
566
|
+
$("cliReset").addEventListener("click", async () => {
|
|
567
|
+
if (!confirm("End this CLI session and remove its saved sign-in? Account chat history stays with the agent.")) return;
|
|
568
|
+
const res = await fetch(BASE + "/api/cli/reset", { method: "POST" });
|
|
569
|
+
if (res.ok) { $("cliInput").value = ""; setPlatform(current.channel); }
|
|
570
|
+
else cliOutput("stderr", "Could not reset the CLI session.\n");
|
|
571
|
+
});
|
|
572
|
+
|
|
427
573
|
function insert(el) {
|
|
428
574
|
const thread = $("thread");
|
|
429
575
|
thread.insertBefore(el, typingEl && typingEl.isConnected ? typingEl : null);
|
|
@@ -486,8 +632,12 @@
|
|
|
486
632
|
}
|
|
487
633
|
}
|
|
488
634
|
const body = document.createElement("span");
|
|
489
|
-
|
|
490
|
-
|
|
635
|
+
// Channels without link buttons carry the URL in text. A real auth link must still open login.
|
|
636
|
+
const authURL = line.authentication && !line.link ? /\n(https?:\/\/\S+)$/.exec(line.text) : null;
|
|
637
|
+
const link = line.link || (authURL ? { label: "Sign in", url: authURL[1] } : null);
|
|
638
|
+
const text = authURL ? line.text.slice(0, authURL.index) : line.text;
|
|
639
|
+
if (current.formatting === "whatsapp") body.innerHTML = whatsappHtml(text);
|
|
640
|
+
else body.textContent = text;
|
|
491
641
|
b.appendChild(body);
|
|
492
642
|
if (tpl && tpl.footer) { const f = document.createElement("span"); f.className = "tpl-foot"; f.textContent = tpl.footer; b.appendChild(f); }
|
|
493
643
|
if (current.id === "whatsapp") {
|
|
@@ -540,16 +690,21 @@
|
|
|
540
690
|
btns.appendChild(btn);
|
|
541
691
|
row.appendChild(btns);
|
|
542
692
|
}
|
|
543
|
-
if (
|
|
544
|
-
// A button that opens a page (WhatsApp's call-to-action URL button).
|
|
693
|
+
if (link) {
|
|
694
|
+
// A button that opens a page (WhatsApp's call-to-action URL button). Authentication opens the real login. Other links use a simulated screen.
|
|
545
695
|
const btns = document.createElement("div");
|
|
546
696
|
btns.className = "btns";
|
|
547
697
|
const btn = document.createElement("button");
|
|
548
698
|
btn.type = "button";
|
|
549
|
-
btn.textContent = "↗ " +
|
|
550
|
-
btn.title =
|
|
551
|
-
const url =
|
|
552
|
-
btn.addEventListener("click", () =>
|
|
699
|
+
btn.textContent = "↗ " + link.label;
|
|
700
|
+
btn.title = link.url;
|
|
701
|
+
const url = link.url;
|
|
702
|
+
btn.addEventListener("click", () => {
|
|
703
|
+
if (line.authentication) {
|
|
704
|
+
const target = new URL(url, location.href);
|
|
705
|
+
if (["https:", "http:"].includes(target.protocol)) location.assign(target.href);
|
|
706
|
+
} else openScreen(cards.find((c) => url.includes(c.match)) || { label: link.label, kind: "page" }, url);
|
|
707
|
+
});
|
|
553
708
|
btns.appendChild(btn);
|
|
554
709
|
row.appendChild(btns);
|
|
555
710
|
}
|
|
@@ -899,7 +1054,19 @@
|
|
|
899
1054
|
const mine = ++opened;
|
|
900
1055
|
if (source) { source.close(); source = null; }
|
|
901
1056
|
if (replayData) { showReplay(replayData); return; }
|
|
902
|
-
|
|
1057
|
+
if (isVisitor()) {
|
|
1058
|
+
const response = await fetch(BASE + "/api/auth" + query());
|
|
1059
|
+
if (!response.ok) throw new Error("Could not check sign-in");
|
|
1060
|
+
const state = await response.json();
|
|
1061
|
+
if (mine !== opened) return;
|
|
1062
|
+
showAuth(state);
|
|
1063
|
+
try { $("text").value = sessionStorage.getItem(draftKey()) || $("text").value; } catch {}
|
|
1064
|
+
|
|
1065
|
+
}
|
|
1066
|
+
const response = await fetch(BASE + "/api/transcript" + query());
|
|
1067
|
+
if (isVisitor() && response.status === 401) { showAuth(await response.json()); return; }
|
|
1068
|
+
if (!response.ok) throw new Error("Could not load history");
|
|
1069
|
+
const { lines, seq } = await response.json();
|
|
903
1070
|
if (mine !== opened) return;
|
|
904
1071
|
for (const l of lines) showLine(l, false);
|
|
905
1072
|
if (!lines.length) {
|
|
@@ -975,11 +1142,37 @@
|
|
|
975
1142
|
}
|
|
976
1143
|
|
|
977
1144
|
async function send(text, extra = {}) {
|
|
1145
|
+
if (isVisitor() && !signedIn) { try { sessionStorage.setItem(draftKey(), text); } catch {} }
|
|
978
1146
|
const body = { ...where(), text, ...extra };
|
|
979
1147
|
if (!body.replyTo && replyTo) body.replyTo = replyTo.id;
|
|
980
1148
|
setReply(null);
|
|
981
|
-
await fetch(BASE + "/api/messages", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
1149
|
+
const response = await fetch(BASE + "/api/messages", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
1150
|
+
if (!response.ok) {
|
|
1151
|
+
$("text").value = text;
|
|
1152
|
+
if (isVisitor() && response.status === 401) showAuth(await response.json());
|
|
1153
|
+
sysnote("Your message was not sent. Check sign-in and try again.", true);
|
|
1154
|
+
} else if (signedIn) { try { sessionStorage.removeItem(draftKey()); } catch {} }
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function showAuth(state) {
|
|
1158
|
+
signedIn = state.signedIn === true;
|
|
1159
|
+
$("authState").hidden = false;
|
|
1160
|
+
$("authLabel").textContent = state.label || (signedIn ? "Signed in" : "Signed out");
|
|
1161
|
+
$("signIn").hidden = signedIn || !state.signInUrl;
|
|
1162
|
+
$("signIn").removeAttribute("href");
|
|
1163
|
+
if (state.signInUrl) {
|
|
1164
|
+
const target = new URL(state.signInUrl, location.href);
|
|
1165
|
+
if (["https:", "http:"].includes(target.protocol)) $("signIn").href = target.href;
|
|
1166
|
+
}
|
|
1167
|
+
const option = $("who").querySelector('option[value="visitor"]');
|
|
1168
|
+
if (option) option.textContent = signedIn ? "Connected user" : "New user · signed out";
|
|
982
1169
|
}
|
|
1170
|
+
$("signIn").addEventListener("click", () => { try { sessionStorage.setItem(draftKey(), $("text").value); } catch {} });
|
|
1171
|
+
$("startSignedOut").addEventListener("click", async () => {
|
|
1172
|
+
const response = await fetch(BASE + "/api/auth/reset" + query(), { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
|
|
1173
|
+
if (!response.ok) { sysnote("Could not reset this browser's sign-in.", true); return; }
|
|
1174
|
+
setPlatform(current.channel);
|
|
1175
|
+
});
|
|
983
1176
|
|
|
984
1177
|
// The person's message is shown at once; the agent's id for it (for ticks, replies and
|
|
985
1178
|
// reactions) arrives a moment later.
|
|
@@ -1037,26 +1230,46 @@
|
|
|
1037
1230
|
});
|
|
1038
1231
|
$("cancelReply").addEventListener("click", () => setReply(null));
|
|
1039
1232
|
$("reset").addEventListener("click", () => fetch(BASE + "/api/reset" + query(), { method: "POST" }));
|
|
1040
|
-
$("clear").addEventListener("click", () => {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1233
|
+
$("clear").addEventListener("click", async () => {
|
|
1234
|
+
if (isLocalCli()) { $("cliOutput").textContent = ""; return; }
|
|
1235
|
+
const name = isVisitor() ? (signedIn ? "this account" : "this visitor") : people.find((p) => p.phone === person)?.name;
|
|
1236
|
+
if (!name || !confirm(`Forget ${name}? Their history, schedules and everything the agent kept about them go, on every channel.`)) return;
|
|
1237
|
+
try {
|
|
1238
|
+
const response = await fetch(BASE + "/api/clear" + query(), { method: "POST" });
|
|
1239
|
+
if (!response.ok) throw new Error("clear failed");
|
|
1240
|
+
} catch { sysnote("Could not clear this conversation. Try again.", true); }
|
|
1045
1241
|
});
|
|
1046
1242
|
document.addEventListener("click", (e) => {
|
|
1047
1243
|
if (!e.target.closest(".picker") && !e.target.closest(".actions")) document.querySelectorAll(".picker").forEach((p) => p.remove());
|
|
1048
1244
|
});
|
|
1049
1245
|
|
|
1050
1246
|
function tabFor(id, label, p) {
|
|
1247
|
+
const known = platforms.find(platform => platform.id === id.toLowerCase());
|
|
1248
|
+
const kind = id.toLowerCase() === "terminal" ? "terminal" : (known?.id ?? p.id);
|
|
1249
|
+
const name = known?.name ?? (kind === "terminal" ? "Terminal" : label);
|
|
1051
1250
|
const b = document.createElement("button");
|
|
1052
1251
|
b.className = "tab";
|
|
1053
1252
|
b.type = "button";
|
|
1054
1253
|
b.dataset.id = id;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
b.
|
|
1059
|
-
|
|
1254
|
+
b.dataset.label = name;
|
|
1255
|
+
b.setAttribute("aria-label", name);
|
|
1256
|
+
b.title = name + (p.complete ? "" : " · partial simulation");
|
|
1257
|
+
if (!p.complete) b.setAttribute("aria-description", "Some platform features are not simulated yet.");
|
|
1258
|
+
const drawings = {
|
|
1259
|
+
web: '<circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18M5 6h14M5 18h14"/>',
|
|
1260
|
+
terminal: '<rect x="2" y="3" width="20" height="18" rx="4"/><path d="m6 8 4 4-4 4m7 0h5"/>',
|
|
1261
|
+
imessage: '<rect x="1" y="1" width="22" height="22" rx="6" fill="currentColor" stroke="none"/><path d="M19 11.5c0 3.3-3.1 6-7 6-.9 0-1.7-.1-2.5-.4L6 19l.8-3.2C5.7 14.7 5 13.2 5 11.5c0-3.3 3.1-6 7-6s7 2.7 7 6Z" fill="white" stroke="none"/>',
|
|
1262
|
+
sms: '<rect x="1" y="1" width="22" height="22" rx="6" fill="currentColor" stroke="none"/><path d="M6 6h12v9H9l-3 3V6Zm3 3h6m-6 3h4" stroke="white" stroke-width="1.5"/>',
|
|
1263
|
+
};
|
|
1264
|
+
if (drawings[kind]) {
|
|
1265
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1266
|
+
icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("fill", "none"); icon.setAttribute("stroke", "currentColor"); icon.setAttribute("stroke-width", "1.6"); icon.setAttribute("aria-hidden", "true");
|
|
1267
|
+
icon.classList.add("tab-icon", kind); icon.innerHTML = drawings[kind]; b.appendChild(icon);
|
|
1268
|
+
} else {
|
|
1269
|
+
const img = document.createElement("img"); img.src = `https://cdn.simpleicons.org/${known?.icon ?? p.icon}`; img.alt = ""; b.appendChild(img);
|
|
1270
|
+
}
|
|
1271
|
+
const text = document.createElement("span"); text.className = "tab-label"; text.appendChild(document.createTextNode(name));
|
|
1272
|
+
b.appendChild(text);
|
|
1060
1273
|
const badge = document.createElement("span"); badge.className = "unread"; badge.hidden = true; badge.setAttribute("aria-label", "new message"); b.appendChild(badge);
|
|
1061
1274
|
b.addEventListener("click", () => setPlatform(id));
|
|
1062
1275
|
$("tabs").appendChild(b);
|
|
@@ -1097,7 +1310,7 @@
|
|
|
1097
1310
|
return li;
|
|
1098
1311
|
}
|
|
1099
1312
|
async function refreshPerson() {
|
|
1100
|
-
if (!PLAY || !panel.person || !current) return;
|
|
1313
|
+
if (!PLAY || !panel.person || !current || isLocalCli()) return;
|
|
1101
1314
|
const res = await fetch(BASE + "/api/person" + query());
|
|
1102
1315
|
if (!res.ok) return;
|
|
1103
1316
|
const { now, contact, outbox, lastOut = {} } = await res.json();
|
|
@@ -1145,10 +1358,12 @@
|
|
|
1145
1358
|
const badge = b.querySelector(".unread");
|
|
1146
1359
|
if (!badge) continue;
|
|
1147
1360
|
const stored = recall(key(ch));
|
|
1361
|
+
b.setAttribute("aria-label", b.dataset.label);
|
|
1148
1362
|
if (ch === current.channel || stored === null) { remember(key(ch), String(last)); badge.hidden = true; continue; }
|
|
1149
1363
|
const unread = times.filter((t) => t > Number(stored)).length;
|
|
1150
1364
|
badge.hidden = unread === 0;
|
|
1151
1365
|
badge.textContent = unread > 9 ? "9+" : String(unread);
|
|
1366
|
+
if (unread) b.setAttribute("aria-label", `${b.dataset.label}, ${unread} new ${unread === 1 ? "message" : "messages"}`);
|
|
1152
1367
|
}
|
|
1153
1368
|
}
|
|
1154
1369
|
|
|
@@ -1201,12 +1416,15 @@
|
|
|
1201
1416
|
}
|
|
1202
1417
|
|
|
1203
1418
|
(async () => {
|
|
1204
|
-
({ platforms, cards, people = [], channels = [], templates = [], panel = panel } = await (await fetch(BASE + "/api/platforms")).json());
|
|
1419
|
+
({ platforms, cards, people = [], channels = [], templates = [], panel = panel, authentication = false, localCli = null } = await (await fetch(BASE + "/api/platforms")).json());
|
|
1205
1420
|
if (PLAY) {
|
|
1206
1421
|
if (!channels.length) return empty("This agent has no channels. Declare them in <code>createAgent({ channels: [...] })</code> (whatsapp(), sms(), customUI()) to test it here.");
|
|
1207
|
-
if (!people.length) return empty("This agent has no test people. Declare who may be played here in <code>createAgent({ testPeople: [{ name, phone }] })</code>.");
|
|
1208
|
-
if (!people.some((p) => p.phone === person)) person = people[0].phone;
|
|
1422
|
+
if (!people.length && !authentication) return empty("This agent has no test people. Declare who may be played here in <code>createAgent({ testPeople: [{ name, phone }] })</code>.");
|
|
1423
|
+
if (!people.some((p) => p.phone === person) && !(authentication && person === "visitor")) person = authentication ? "visitor" : people[0].phone;
|
|
1209
1424
|
remember("chinwag.person", person);
|
|
1425
|
+
if (authentication) {
|
|
1426
|
+
const option = document.createElement("option"); option.value = "visitor"; option.textContent = "New user · signed out"; $("who").appendChild(option);
|
|
1427
|
+
}
|
|
1210
1428
|
for (const p of people) {
|
|
1211
1429
|
const o = document.createElement("option");
|
|
1212
1430
|
o.value = p.phone;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import { type Attachment, type ChannelInfo, type ChannelSummary, type Deliver, type EventSink, type FilledTemplate, type Incoming, type ListRow, type PlaygroundSink, type TemplateDefinition, type TestPerson, type TranscriptEntry } from "../../src/index.ts";
|
|
3
3
|
import type { Agent } from "../../src/agent.ts";
|
|
4
|
+
import type { CliConfig } from "../../cli/src/commands.ts";
|
|
4
5
|
/** What the test cloud's simulators need from an agent. */
|
|
5
6
|
export interface AgentConnection {
|
|
6
7
|
/** A person's message; returns its id, or null when the agent answered it without a turn. */
|
|
@@ -35,6 +36,8 @@ export interface PlaygroundAgent {
|
|
|
35
36
|
messageId: string;
|
|
36
37
|
} | null>;
|
|
37
38
|
testConversation(phone: string): Promise<string | null>;
|
|
39
|
+
simulateVisitor?: Agent["simulateVisitor"];
|
|
40
|
+
visitorConversation?: Agent["visitorConversation"];
|
|
38
41
|
transcript(conversationId: string): Promise<TranscriptEntry[]>;
|
|
39
42
|
reacted(conversationId: string, emoji: string, toText: string): void;
|
|
40
43
|
forgetPerson(conversationId: string): Promise<void>;
|
|
@@ -81,6 +84,21 @@ export interface SimulatorsOptions {
|
|
|
81
84
|
};
|
|
82
85
|
/** The agent's clock (playgroundClock(), passed to createAgent as `now`): the Person panel can move it. */
|
|
83
86
|
clock?: PlaygroundClock;
|
|
87
|
+
/**
|
|
88
|
+
* Adds "New user · signed out" to the person picker. `visitor` is an opaque browser identity,
|
|
89
|
+
* scoped to the channel. Return the account `key` only after the application's normal auth
|
|
90
|
+
* verifies it, plus its channel `address` if different from visitor. Keep credentials server-side.
|
|
91
|
+
* Existing declared test people keep their current identity shortcut.
|
|
92
|
+
*/
|
|
93
|
+
authentication?: (input: {
|
|
94
|
+
channel: ChannelSummary;
|
|
95
|
+
visitor: string;
|
|
96
|
+
}) => Promise<{
|
|
97
|
+
key?: string;
|
|
98
|
+
address?: string;
|
|
99
|
+
label?: string;
|
|
100
|
+
signInUrl?: string;
|
|
101
|
+
}>;
|
|
84
102
|
}
|
|
85
103
|
export interface LinkCard {
|
|
86
104
|
/** Text the link's URL contains, such as "/video/". */
|
|
@@ -95,17 +113,25 @@ type Listening = {
|
|
|
95
113
|
close(): Promise<void>;
|
|
96
114
|
url: string;
|
|
97
115
|
};
|
|
116
|
+
export interface LocalCliOptions {
|
|
117
|
+
/** The team's actual defineCli configuration. The browser cannot override its endpoints. */
|
|
118
|
+
config: CliConfig;
|
|
119
|
+
/** The channel tab that opens this runner, e.g. "terminal". */
|
|
120
|
+
channel: string;
|
|
121
|
+
}
|
|
98
122
|
export interface Playground {
|
|
99
123
|
/** Pass to the agent's `events`: the trace and tool calls of test people's conversations. */
|
|
100
124
|
events: EventSink;
|
|
101
125
|
/** Serves the playground on 127.0.0.1 only; a token is optional there. */
|
|
102
126
|
listen(agent: PlaygroundAgent, port: number, options?: {
|
|
103
127
|
token?: string;
|
|
128
|
+
localCli?: LocalCliOptions;
|
|
104
129
|
}): Promise<Listening>;
|
|
105
130
|
/** The playground as a request handler under `base`, for a deployed agent's own server: a token is required. */
|
|
106
131
|
handler(agent: PlaygroundAgent, options: {
|
|
107
132
|
token: string;
|
|
108
133
|
base?: string;
|
|
134
|
+
localCli?: LocalCliOptions;
|
|
109
135
|
}): Handler;
|
|
110
136
|
}
|
|
111
137
|
export interface TestCloudSimulators {
|
|
@@ -140,6 +166,7 @@ export type Line = {
|
|
|
140
166
|
label: string;
|
|
141
167
|
url: string;
|
|
142
168
|
};
|
|
169
|
+
authentication?: boolean;
|
|
143
170
|
/** A photo above the text, which is its caption. */
|
|
144
171
|
image?: {
|
|
145
172
|
url: string;
|