swarmbox 0.0.0-stage → 0.12.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/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/checks.d.ts +48 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +43 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -0
- package/dist/control-room/page.d.ts +2 -0
- package/dist/control-room/page.d.ts.map +1 -0
- package/dist/control-room/page.js +212 -0
- package/dist/control-room/page.js.map +1 -0
- package/dist/control-room/server.d.ts +8 -0
- package/dist/control-room/server.d.ts.map +1 -0
- package/dist/control-room/server.js +180 -0
- package/dist/control-room/server.js.map +1 -0
- package/dist/device/device.d.ts +20 -0
- package/dist/device/device.d.ts.map +1 -0
- package/dist/device/device.js +108 -0
- package/dist/device/device.js.map +1 -0
- package/dist/device/props.d.ts +31 -0
- package/dist/device/props.d.ts.map +1 -0
- package/dist/device/props.js +143 -0
- package/dist/device/props.js.map +1 -0
- package/dist/fleet/browser.d.ts +116 -0
- package/dist/fleet/browser.d.ts.map +1 -0
- package/dist/fleet/browser.js +298 -0
- package/dist/fleet/browser.js.map +1 -0
- package/dist/fleet/sight.d.ts +70 -0
- package/dist/fleet/sight.d.ts.map +1 -0
- package/dist/fleet/sight.js +288 -0
- package/dist/fleet/sight.js.map +1 -0
- package/dist/harness.d.ts +258 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +907 -0
- package/dist/harness.js.map +1 -0
- package/dist/hooks.d.ts +69 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +80 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/device.d.ts +6 -0
- package/dist/mcp/device.d.ts.map +1 -0
- package/dist/mcp/device.js +61 -0
- package/dist/mcp/device.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +670 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/standins.d.ts +11 -0
- package/dist/mcp/standins.d.ts.map +1 -0
- package/dist/mcp/standins.js +142 -0
- package/dist/mcp/standins.js.map +1 -0
- package/dist/product.d.ts +226 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +10 -0
- package/dist/product.js.map +1 -0
- package/dist/provenance.d.ts +22 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +55 -0
- package/dist/provenance.js.map +1 -0
- package/dist/runs/budget.d.ts +23 -0
- package/dist/runs/budget.d.ts.map +1 -0
- package/dist/runs/budget.js +56 -0
- package/dist/runs/budget.js.map +1 -0
- package/dist/runs/report.d.ts +3 -0
- package/dist/runs/report.d.ts.map +1 -0
- package/dist/runs/report.js +125 -0
- package/dist/runs/report.js.map +1 -0
- package/dist/runs/run.d.ts +86 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +227 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/scenarios/spec.d.ts +33 -0
- package/dist/scenarios/spec.d.ts.map +1 -0
- package/dist/scenarios/spec.js +89 -0
- package/dist/scenarios/spec.js.map +1 -0
- package/dist/serve.d.ts +31 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +39 -0
- package/dist/serve.js.map +1 -0
- package/dist/standins/builtin/email/index.d.ts +2 -0
- package/dist/standins/builtin/email/index.d.ts.map +1 -0
- package/dist/standins/builtin/email/index.js +213 -0
- package/dist/standins/builtin/email/index.js.map +1 -0
- package/dist/standins/builtin/email/manifest.json +41 -0
- package/dist/standins/builtin/expo-push/index.d.ts +2 -0
- package/dist/standins/builtin/expo-push/index.d.ts.map +1 -0
- package/dist/standins/builtin/expo-push/index.js +203 -0
- package/dist/standins/builtin/expo-push/index.js.map +1 -0
- package/dist/standins/builtin/expo-push/manifest.json +45 -0
- package/dist/standins/builtin/helcim/index.d.ts +7 -0
- package/dist/standins/builtin/helcim/index.d.ts.map +1 -0
- package/dist/standins/builtin/helcim/index.js +438 -0
- package/dist/standins/builtin/helcim/index.js.map +1 -0
- package/dist/standins/builtin/helcim/manifest.json +37 -0
- package/dist/standins/builtin/index.d.ts +8 -0
- package/dist/standins/builtin/index.d.ts.map +1 -0
- package/dist/standins/builtin/index.js +22 -0
- package/dist/standins/builtin/index.js.map +1 -0
- package/dist/standins/builtin/s3/checksums.d.ts +8 -0
- package/dist/standins/builtin/s3/checksums.d.ts.map +1 -0
- package/dist/standins/builtin/s3/checksums.js +67 -0
- package/dist/standins/builtin/s3/checksums.js.map +1 -0
- package/dist/standins/builtin/s3/index.d.ts +6 -0
- package/dist/standins/builtin/s3/index.d.ts.map +1 -0
- package/dist/standins/builtin/s3/index.js +688 -0
- package/dist/standins/builtin/s3/index.js.map +1 -0
- package/dist/standins/builtin/s3/manifest.json +35 -0
- package/dist/standins/builtin/twilio/index.d.ts +10 -0
- package/dist/standins/builtin/twilio/index.d.ts.map +1 -0
- package/dist/standins/builtin/twilio/index.js +477 -0
- package/dist/standins/builtin/twilio/index.js.map +1 -0
- package/dist/standins/builtin/twilio/manifest.json +133 -0
- package/dist/standins/guard.d.ts +16 -0
- package/dist/standins/guard.d.ts.map +1 -0
- package/dist/standins/guard.js +78 -0
- package/dist/standins/guard.js.map +1 -0
- package/dist/standins/manifest.d.ts +94 -0
- package/dist/standins/manifest.d.ts.map +1 -0
- package/dist/standins/manifest.js +121 -0
- package/dist/standins/manifest.js.map +1 -0
- package/dist/standins/protocol.d.ts +114 -0
- package/dist/standins/protocol.d.ts.map +1 -0
- package/dist/standins/protocol.js +11 -0
- package/dist/standins/protocol.js.map +1 -0
- package/dist/standins/runner.d.ts +78 -0
- package/dist/standins/runner.d.ts.map +1 -0
- package/dist/standins/runner.js +746 -0
- package/dist/standins/runner.js.map +1 -0
- package/dist/standins/sdk.d.ts +73 -0
- package/dist/standins/sdk.d.ts.map +1 -0
- package/dist/standins/sdk.js +217 -0
- package/dist/standins/sdk.js.map +1 -0
- package/dist/standins/shape.d.ts +25 -0
- package/dist/standins/shape.d.ts.map +1 -0
- package/dist/standins/shape.js +131 -0
- package/dist/standins/shape.js.map +1 -0
- package/dist/time.d.ts +12 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +64 -0
- package/dist/time.js.map +1 -0
- package/package.json +71 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Swarmify contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Swarmbox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A sandbox where AI agents use your product the way people do: in real browsers, against a throwaway copy of
|
|
4
|
+
it (a _world_), as each of your roles at once. The agent sees what a person sees and what the server did,
|
|
5
|
+
logs what isn't right, and ends with your checks and a report anyone can read.
|
|
6
|
+
|
|
7
|
+
Built for Swarmify's agents, and usable by any team whose product runs locally. MIT licensed.
|
|
8
|
+
|
|
9
|
+
## What it gives an agent
|
|
10
|
+
|
|
11
|
+
- **Sight.** Open dialogs come first and whole when it reads a screen. Every action reports what changed
|
|
12
|
+
(a dialog opened or closed, a status line, the address), what the server did (its own command and what
|
|
13
|
+
that set off, and anything that failed or was refused), and requests refused. A click that can't land
|
|
14
|
+
says what is in the way; of two buttons with one name, the one in the dialog in front is meant.
|
|
15
|
+
- **Worlds.** Each run starts your product the way it is deployed, with its own data, on tenant hosts that
|
|
16
|
+
resolve only inside the browsers Swarmbox launches. Nothing is shared with other runs or with real data.
|
|
17
|
+
- **People.** Everyone in the world, signed in without a password, each on their own screen; a tablet
|
|
18
|
+
app past its first screen; a customer at a link, with no sign-in at all. Any screen can be a phone, a
|
|
19
|
+
tablet either way up or a desktop, light or dark.
|
|
20
|
+
- **The device and props.** Swarmbox makes the photos, documents and signatures a person would use, each tagged
|
|
21
|
+
with its id. It puts them in a screen's file chooser (a tablet's camera button) or in front of its live camera,
|
|
22
|
+
and sets where the device is and whether it shows notifications.
|
|
23
|
+
- **Hands on the screen.** Click, type, choose, press keys, draw (a signature pad) and drag with the pointer
|
|
24
|
+
(a block onto another lane, an edge to resize), each element found by its accessible role and name. A styled
|
|
25
|
+
radio or checkbox is clicked through the label drawn over it, and of two alike in stacked layers, the one in
|
|
26
|
+
front is taken.
|
|
27
|
+
- **Time.** Move a world's clock forward, and every screen follows; people stay signed in across the jump
|
|
28
|
+
(`renewSignIn`). A kept world that comes back with its clock moved on an earlier run starts its screens on its
|
|
29
|
+
own time. A scenario can say when its day starts (`world.startsAt`: "09:00", or a date and time
|
|
30
|
+
in the tenant's zone, to test across a time change). Swarmbox works out times with Node's tzdata; a
|
|
31
|
+
screen shows them with Chromium's own, and the two can differ on a zone whose rules changed recently.
|
|
32
|
+
A time a screen shows an hour out may be that, not your product.
|
|
33
|
+
- **Runs.** A scenario in plain words (what to try, what good looks like, who is on screen), findings with
|
|
34
|
+
screenshots, Playwright traces of every screen, your checks at the end, and one HTML report.
|
|
35
|
+
- **A control room** at http://127.0.0.1:3100: every screen live, and take-over, during which the agent's
|
|
36
|
+
actions on that screen are refused.
|
|
37
|
+
- **The world's own log** (`world_log`), when the product shares it: the stack behind a screen's
|
|
38
|
+
"something went wrong", while the world runs.
|
|
39
|
+
- **A daily budget** in browser-minutes and runs.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm add -D swarmbox playwright-core
|
|
45
|
+
pnpm exec playwright-core install chromium
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`playwright-core` is a peer dependency, so your product and Swarmbox share one copy. Node 24 or later.
|
|
49
|
+
|
|
50
|
+
## Plugging a product in
|
|
51
|
+
|
|
52
|
+
A product is one object (`Product` in `src/product.ts`): how to start a world, who is in it, how each of
|
|
53
|
+
them signs in, and how to check the world afterwards. Then:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { serve } from 'swarmbox';
|
|
57
|
+
import { myProduct } from './product.ts';
|
|
58
|
+
|
|
59
|
+
await serve(myProduct());
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
claude mcp add --transport http swarmbox-myproduct http://127.0.0.1:3101/mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`test/fixtures/sample-product.ts` is a complete product in about 200 lines.
|
|
67
|
+
|
|
68
|
+
A product can take options of its own for a world (`worldOptions`, as zod fields): a tenant still
|
|
69
|
+
migrating from another system, a feature switched on. `world_create` takes them as `options` and a
|
|
70
|
+
scenario as `world.options`.
|
|
71
|
+
|
|
72
|
+
What adapters have learnt so far:
|
|
73
|
+
|
|
74
|
+
- **Mint sessions on the world's time.** A world's clock can be days ahead of the machine's, so a session dated on
|
|
75
|
+
the machine's clock may already be over to the product. Date it from the product's own clock (its hooks).
|
|
76
|
+
- **Renew by extending, not replacing** (`renewSignIn`). An open page often keeps something derived from its
|
|
77
|
+
session (a CSRF token); a fresh session under it ends in the product's "sign in again".
|
|
78
|
+
- **Share the server's output** (`log`): `world_log` is how an agent gets from a screen's "something went wrong" to
|
|
79
|
+
the stack.
|
|
80
|
+
- **In the product's own tests, check for the tools it relies on, not Swarmbox's whole list**: each release adds
|
|
81
|
+
tools.
|
|
82
|
+
- **Give each of the product's system tests its own port**, apart from the one a running sandbox uses.
|
|
83
|
+
|
|
84
|
+
### In production
|
|
85
|
+
|
|
86
|
+
A product that uses stand-ins calls the guard as it starts in production, before it builds any provider client:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { assertNoStandins } from 'swarmbox';
|
|
90
|
+
|
|
91
|
+
if (process.env.NODE_ENV === 'production') assertNoStandins(process.env, { allowLoopback: ['METRICS_URL'] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
It refuses to start with anything a stand-in issued (marked keys, fictional 555-01xx numbers), a provider address on
|
|
95
|
+
this machine however it's written, or Swarmbox's local certificate authority. It names the variable, never its
|
|
96
|
+
value. `allowLoopback` lists variables that really do point at this machine in production.
|
|
97
|
+
|
|
98
|
+
### Hooks
|
|
99
|
+
|
|
100
|
+
A product can serve Swarmbox's hooks (protocol v0, `src/hooks.ts`) in its sandbox worlds: a feed of what the
|
|
101
|
+
server did, the clock, the phone inbox, and fake providers with rules (fail the next text, a customer
|
|
102
|
+
replying). Without them Swarmbox still drives the screens; with them the agent sees the `server:` lines
|
|
103
|
+
after each action and can move time.
|
|
104
|
+
|
|
105
|
+
## Developing
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
pnpm install
|
|
109
|
+
pnpm run check # typecheck, lint, formatting
|
|
110
|
+
pnpm test # unit tests
|
|
111
|
+
pnpm exec playwright-core install chromium
|
|
112
|
+
pnpm run test:browser # everything, the browser tests too
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Node 24. The source runs as TypeScript in tests; the package ships `dist/` (`pnpm run build`).
|
|
116
|
+
|
|
117
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) to send a change, and [SECURITY.md](SECURITY.md) to report a
|
|
118
|
+
security problem.
|
|
119
|
+
|
|
120
|
+
## Licence
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE).
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type Outcome = 'pass' | 'fail' | 'nothing-to-check' | 'error' | 'known-red' | 'known-red-now-passes';
|
|
2
|
+
export interface Violation {
|
|
3
|
+
summary: string;
|
|
4
|
+
/** The rows behind it, for the report (a few are shown). */
|
|
5
|
+
rows?: Record<string, unknown>[];
|
|
6
|
+
}
|
|
7
|
+
export interface CheckResult {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
/** Where it ran: a tenant, or what the product calls its other scopes ("control", "code"). */
|
|
11
|
+
where: string;
|
|
12
|
+
outcome: Outcome;
|
|
13
|
+
/** How much it looked at: a check with nothing to look at says so instead of passing. */
|
|
14
|
+
examined: number;
|
|
15
|
+
unit: string;
|
|
16
|
+
violations: Violation[];
|
|
17
|
+
error?: string;
|
|
18
|
+
/** A known, ticketed defect this check is red for. */
|
|
19
|
+
knownRed?: {
|
|
20
|
+
ref: string;
|
|
21
|
+
why: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Background work still outstanding when settling gave up: an event a handler has not finished. */
|
|
25
|
+
export interface Pending {
|
|
26
|
+
seq: number;
|
|
27
|
+
type: string;
|
|
28
|
+
handler: string;
|
|
29
|
+
/** Null when the handler has not picked the work up at all. */
|
|
30
|
+
status: string | null;
|
|
31
|
+
lastError: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface Settled {
|
|
34
|
+
settled: boolean;
|
|
35
|
+
/** How long it took, or how long it waited before giving up. */
|
|
36
|
+
ms: number;
|
|
37
|
+
/** What was still outstanding when it gave up; empty when settled. */
|
|
38
|
+
pending: Pending[];
|
|
39
|
+
/** Outstanding work that is a known, ticketed defect: reported, never dropped, and never waited for. */
|
|
40
|
+
known: Array<Pending & {
|
|
41
|
+
ref: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/** A plain-text report: failures first, then what could not be checked, then passes. */
|
|
45
|
+
export declare function formatReport(results: CheckResult[]): string;
|
|
46
|
+
/** Failing or broken checks: what a run's end counts. */
|
|
47
|
+
export declare const failing: (results: CheckResult[]) => CheckResult[];
|
|
48
|
+
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,kBAAkB,GAAG,OAAO,GAAG,WAAW,GAAG,sBAAsB,CAAC;AAE5G,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,QAAQ,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAED,oGAAoG;AACpG,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,wGAAwG;IACxG,KAAK,EAAE,KAAK,CAAC,OAAO,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzC;AAWD,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CAqB3D;AAED,yDAAyD;AACzD,eAAO,MAAM,OAAO,GAAI,SAAS,WAAW,EAAE,KAAG,WAAW,EACU,CAAC"}
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// What a product's checks report, and how a run shows them. The checks themselves are the product's: rules
|
|
2
|
+
// its data must always obey, asked of its database or its code, never of a model.
|
|
3
|
+
//
|
|
4
|
+
// Outcomes are kept distinct on purpose. "Nothing to check" is not a pass, an error is not a pass, and a
|
|
5
|
+
// known defect that starts passing is announced, not quietly taken as good news.
|
|
6
|
+
const MARK = {
|
|
7
|
+
pass: 'pass',
|
|
8
|
+
fail: 'FAIL',
|
|
9
|
+
error: 'ERROR',
|
|
10
|
+
'nothing-to-check': 'nothing to check yet',
|
|
11
|
+
'known-red': 'known red',
|
|
12
|
+
'known-red-now-passes': 'KNOWN RED NOW PASSES',
|
|
13
|
+
};
|
|
14
|
+
/** A plain-text report: failures first, then what could not be checked, then passes. */
|
|
15
|
+
export function formatReport(results) {
|
|
16
|
+
const order = ['fail', 'error', 'known-red-now-passes', 'known-red', 'nothing-to-check', 'pass'];
|
|
17
|
+
const sorted = [...results].sort((a, b) => order.indexOf(a.outcome) - order.indexOf(b.outcome));
|
|
18
|
+
const tally = order
|
|
19
|
+
.map((o) => [o, results.filter((r) => r.outcome === o).length])
|
|
20
|
+
.filter(([, n]) => n > 0)
|
|
21
|
+
.map(([o, n]) => `${n} ${MARK[o]}`)
|
|
22
|
+
.join(', ');
|
|
23
|
+
const lines = [`${results.length} checks: ${tally}`];
|
|
24
|
+
for (const r of sorted) {
|
|
25
|
+
const head = `[${MARK[r.outcome]}] ${r.id} @ ${r.where} — ${r.examined} ${r.unit}`;
|
|
26
|
+
lines.push(head);
|
|
27
|
+
if (r.error)
|
|
28
|
+
lines.push(` error: ${r.error}`);
|
|
29
|
+
if (r.knownRed)
|
|
30
|
+
lines.push(` known defect ${r.knownRed.ref}: ${r.knownRed.why}`);
|
|
31
|
+
for (const v of r.violations) {
|
|
32
|
+
lines.push(` ${v.summary}`);
|
|
33
|
+
for (const row of v.rows?.slice(0, 5) ?? [])
|
|
34
|
+
lines.push(` ${JSON.stringify(row)}`);
|
|
35
|
+
if ((v.rows?.length ?? 0) > 5)
|
|
36
|
+
lines.push(` … ${v.rows.length - 5} more`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** Failing or broken checks: what a run's end counts. */
|
|
42
|
+
export const failing = (results) => results.filter((c) => c.outcome === 'fail' || c.outcome === 'error');
|
|
43
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// swarmbox call <tool> ['{"json":"args"}'] [--port 3101]
|
|
3
|
+
//
|
|
4
|
+
// Call one Swarmbox tool from a shell, through the same MCP server an agent uses: for a session that cannot
|
|
5
|
+
// load the MCP server itself (one already running when Swarmbox started), and for scripts. It prints the
|
|
6
|
+
// tool's text; an image result is saved to a file and its path printed. A refusal prints ERROR and exits 1,
|
|
7
|
+
// so a pipe through grep cannot hide it.
|
|
8
|
+
import { writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
12
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
13
|
+
import { DEFAULT_PORTS } from "./serve.js";
|
|
14
|
+
const USAGE = "usage: swarmbox call <tool> ['{json args}'] [--port <mcp port>]";
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const portAt = args.indexOf('--port');
|
|
17
|
+
const port = portAt >= 0
|
|
18
|
+
? Number(args.splice(portAt, 2)[1])
|
|
19
|
+
: Number(process.env.SWARMBOX_MCP_PORT ?? DEFAULT_PORTS.mcp);
|
|
20
|
+
const [command, tool, raw] = args;
|
|
21
|
+
if (command !== 'call' || !tool || !Number.isInteger(port)) {
|
|
22
|
+
console.error(USAGE);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
const client = new Client({ name: 'swarmbox-call', version: '0.1.0' });
|
|
26
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)));
|
|
27
|
+
try {
|
|
28
|
+
// Starting a world or a run can take minutes (a product's seed data): past the client's default timeout.
|
|
29
|
+
const r = (await client.callTool({ name: tool, arguments: raw ? JSON.parse(raw) : {} }, undefined, {
|
|
30
|
+
timeout: 600_000,
|
|
31
|
+
}));
|
|
32
|
+
if (r.isError)
|
|
33
|
+
console.log('ERROR');
|
|
34
|
+
for (const c of r.content) {
|
|
35
|
+
if (c.type === 'text')
|
|
36
|
+
(r.isError ? console.error : console.log)(c.text);
|
|
37
|
+
else if (c.type === 'image' && c.data) {
|
|
38
|
+
const file = join(tmpdir(), `swarmbox-${tool}-${Date.now()}.${c.mimeType?.split('/')[1] ?? 'png'}`);
|
|
39
|
+
writeFileSync(file, Buffer.from(c.data, 'base64'));
|
|
40
|
+
console.log(`image: ${file}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
process.exitCode = r.isError ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await client.close();
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/control-room/page.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,IAAI,GAAI,OAAO,MAAM,KAAG,MA6MpC,CAAC"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The control room page. One file, no build step, no dependencies: it is served by the harness itself.
|
|
2
|
+
//
|
|
3
|
+
// Names and URLs shown here come from the world's data, which an AI tester can write to, so every value
|
|
4
|
+
// is set with textContent and never parsed as HTML. The title is the product's own, escaped here.
|
|
5
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
6
|
+
export const page = (title) => /* html */ `<!doctype html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>${esc(title)} · Swarmbox</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #f6f5f2; --panel: #ffffff; --ink: #1c1b19; --muted: #6b6860; --line: #e2dfd8;
|
|
15
|
+
--agent: #2f6f4f; --agent-bg: #e3f0e8; --person: #9a4a12; --person-bg: #fbeadb;
|
|
16
|
+
color-scheme: light;
|
|
17
|
+
}
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #141413; --panel: #1d1c1a; --ink: #ece9e2; --muted: #9a968c; --line: #2e2d2a;
|
|
21
|
+
--agent: #7fc8a1; --agent-bg: #1d3328; --person: #f0a26a; --person-bg: #3a2516;
|
|
22
|
+
color-scheme: dark;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
body { margin: 0; background: var(--bg); color: var(--ink);
|
|
27
|
+
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
28
|
+
header { display: flex; align-items: baseline; gap: 16px; padding: 18px 24px 8px; flex-wrap: wrap; }
|
|
29
|
+
h1 { font-size: 17px; margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
30
|
+
#worlds { color: var(--muted); font-size: 13px; }
|
|
31
|
+
#worlds a { color: inherit; }
|
|
32
|
+
main { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 520px), 1fr)); gap: 16px; padding: 12px 24px 32px; }
|
|
33
|
+
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; display: flex; flex-direction: column; }
|
|
34
|
+
.tile.person { border-color: var(--person); box-shadow: 0 0 0 1px var(--person); }
|
|
35
|
+
.bar { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
|
|
36
|
+
.who { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
37
|
+
.where { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
|
|
38
|
+
.badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
|
|
39
|
+
.badge.agent { color: var(--agent); background: var(--agent-bg); }
|
|
40
|
+
.badge.person { color: var(--person); background: var(--person-bg); }
|
|
41
|
+
button { font: inherit; font-size: 12px; font-weight: 600; padding: 5px 10px; border-radius: 6px; cursor: pointer;
|
|
42
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--ink); white-space: nowrap; }
|
|
43
|
+
button:hover { border-color: var(--muted); }
|
|
44
|
+
.view { aspect-ratio: 16 / 10; background: var(--bg); display: grid; place-items: center; }
|
|
45
|
+
.view img { width: 100%; height: 100%; object-fit: contain; display: block; }
|
|
46
|
+
.view:focus { outline: none; }
|
|
47
|
+
.tile.person .view { cursor: crosshair; }
|
|
48
|
+
.tile.person .view:focus { box-shadow: inset 0 0 0 2px var(--person); }
|
|
49
|
+
.hint { padding: 6px 12px; font-size: 12px; color: var(--person); border-top: 1px solid var(--line); }
|
|
50
|
+
.url { padding: 6px 12px; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted);
|
|
51
|
+
border-top: 1px solid var(--line); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
52
|
+
.empty { grid-column: 1 / -1; color: var(--muted); padding: 48px 0; text-align: center; }
|
|
53
|
+
.empty code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink); }
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<header><h1>${esc(title)} · Swarmbox</h1><div id="worlds"></div></header>
|
|
58
|
+
<main id="tiles"></main>
|
|
59
|
+
<script>
|
|
60
|
+
const tiles = new Map();
|
|
61
|
+
const main = document.getElementById('tiles');
|
|
62
|
+
const worldsEl = document.getElementById('worlds');
|
|
63
|
+
|
|
64
|
+
function el(tag, cls, text) {
|
|
65
|
+
const n = document.createElement(tag);
|
|
66
|
+
if (cls) n.className = cls;
|
|
67
|
+
if (text !== undefined) n.textContent = text;
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function post(path, body) {
|
|
72
|
+
try {
|
|
73
|
+
await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Where on the screen a point on the picture is, as fractions: the picture is letterboxed in the tile
|
|
78
|
+
// (object-fit: contain), so the margins are not part of the screen.
|
|
79
|
+
function pointOn(img, e) {
|
|
80
|
+
const r = img.getBoundingClientRect();
|
|
81
|
+
const iw = img.naturalWidth || 16, ih = img.naturalHeight || 10;
|
|
82
|
+
const scale = Math.min(r.width / iw, r.height / ih);
|
|
83
|
+
const w = iw * scale, h = ih * scale;
|
|
84
|
+
const left = r.left + (r.width - w) / 2, top = r.top + (r.height - h) / 2;
|
|
85
|
+
const x = (e.clientX - left) / w, y = (e.clientY - top) / h;
|
|
86
|
+
return x >= 0 && x <= 1 && y >= 0 && y <= 1 ? { x, y } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const KEYS = new Set(['Enter', 'Backspace', 'Tab', 'Escape', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown']);
|
|
90
|
+
|
|
91
|
+
function tile(s) {
|
|
92
|
+
const root = el('section', 'tile');
|
|
93
|
+
const bar = el('div', 'bar');
|
|
94
|
+
const who = el('div', 'who');
|
|
95
|
+
const where = el('div', 'where');
|
|
96
|
+
const badge = el('span', 'badge');
|
|
97
|
+
const btn = el('button');
|
|
98
|
+
bar.append(who, where, badge, btn);
|
|
99
|
+
const view = el('div', 'view');
|
|
100
|
+
view.tabIndex = 0;
|
|
101
|
+
const img = el('img');
|
|
102
|
+
img.alt = 'Live view of ' + s.person;
|
|
103
|
+
view.append(img);
|
|
104
|
+
const hint = el('div', 'hint', 'You have this screen: click, scroll and type here. Hand back when done.');
|
|
105
|
+
const url = el('div', 'url');
|
|
106
|
+
root.append(bar, view, hint, url);
|
|
107
|
+
const t = { root, who, where, badge, btn, url, img, view, hint, seq: -1, loading: false, objectUrl: null, id: s.id };
|
|
108
|
+
const mine = () => root.dataset.controller === 'person';
|
|
109
|
+
btn.addEventListener('click', async () => {
|
|
110
|
+
const to = mine() ? 'agent' : 'person';
|
|
111
|
+
await post('/screens/' + s.id + '/control', { controller: to });
|
|
112
|
+
await refresh();
|
|
113
|
+
if (to === 'person') view.focus();
|
|
114
|
+
});
|
|
115
|
+
view.addEventListener('click', (e) => {
|
|
116
|
+
if (!mine()) return;
|
|
117
|
+
view.focus();
|
|
118
|
+
const p = pointOn(img, e);
|
|
119
|
+
if (p) post('/screens/' + s.id + '/input', { type: 'click', x: p.x, y: p.y }).then(() => refresh());
|
|
120
|
+
});
|
|
121
|
+
let wheel = 0, wheelAt = null, wheelTimer = null;
|
|
122
|
+
view.addEventListener('wheel', (e) => {
|
|
123
|
+
if (!mine()) return;
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
wheel += e.deltaY;
|
|
126
|
+
wheelAt = pointOn(img, e) || { x: 0.5, y: 0.5 };
|
|
127
|
+
if (!wheelTimer) wheelTimer = setTimeout(() => {
|
|
128
|
+
const dy = wheel;
|
|
129
|
+
wheel = 0; wheelTimer = null;
|
|
130
|
+
post('/screens/' + s.id + '/input', { type: 'wheel', x: wheelAt.x, y: wheelAt.y, dy });
|
|
131
|
+
}, 80);
|
|
132
|
+
}, { passive: false });
|
|
133
|
+
view.addEventListener('keydown', (e) => {
|
|
134
|
+
if (!mine() || e.metaKey || e.ctrlKey) return;
|
|
135
|
+
if (e.key.length === 1) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
post('/screens/' + s.id + '/input', { type: 'text', text: e.key });
|
|
138
|
+
} else if (KEYS.has(e.key)) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
post('/screens/' + s.id + '/input', { type: 'key', key: e.key });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return t;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The tile's picture, fetched only when the screen has a new frame.
|
|
147
|
+
async function picture(t, seq) {
|
|
148
|
+
if (t.loading || seq === t.seq) return;
|
|
149
|
+
t.loading = true;
|
|
150
|
+
try {
|
|
151
|
+
const r = await fetch('/screens/' + t.id + '/frame', { cache: 'no-store' });
|
|
152
|
+
if (r.status === 200) {
|
|
153
|
+
const next = URL.createObjectURL(await r.blob());
|
|
154
|
+
const prev = t.objectUrl;
|
|
155
|
+
t.img.onload = () => { if (prev) URL.revokeObjectURL(prev); };
|
|
156
|
+
t.img.src = next;
|
|
157
|
+
t.objectUrl = next;
|
|
158
|
+
t.seq = Number(r.headers.get('x-frame-seq') || seq);
|
|
159
|
+
}
|
|
160
|
+
} catch {} finally {
|
|
161
|
+
t.loading = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function update(t, s) {
|
|
166
|
+
t.root.dataset.controller = s.controller;
|
|
167
|
+
t.root.classList.toggle('person', s.controller === 'person');
|
|
168
|
+
t.hint.hidden = s.controller !== 'person';
|
|
169
|
+
t.who.textContent = s.person;
|
|
170
|
+
t.where.textContent = s.tenant + ' · ' + s.world + ' · ' + s.id;
|
|
171
|
+
t.badge.className = 'badge ' + s.controller;
|
|
172
|
+
t.badge.textContent = s.controller === 'person' ? 'You have it' : 'Agent driving';
|
|
173
|
+
t.btn.textContent = s.controller === 'person' ? 'Hand back' : 'Take over';
|
|
174
|
+
t.url.textContent = s.url;
|
|
175
|
+
picture(t, s.frameSeq);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function refresh() {
|
|
179
|
+
let state;
|
|
180
|
+
try { state = await (await fetch('/state', { cache: 'no-store' })).json(); } catch { return; }
|
|
181
|
+
worldsEl.replaceChildren();
|
|
182
|
+
for (const w of state.worlds) {
|
|
183
|
+
for (const c of w.tenants) {
|
|
184
|
+
const a = el('a', null, c.slug);
|
|
185
|
+
a.href = c.origin;
|
|
186
|
+
worldsEl.append(w.id + ' ', a, ' ');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
for (const s of state.screens) {
|
|
191
|
+
seen.add(s.id);
|
|
192
|
+
let t = tiles.get(s.id);
|
|
193
|
+
if (!t) { t = tile(s); tiles.set(s.id, t); main.append(t.root); }
|
|
194
|
+
update(t, s);
|
|
195
|
+
}
|
|
196
|
+
for (const [id, t] of tiles) if (!seen.has(id)) { t.root.remove(); if (t.objectUrl) URL.revokeObjectURL(t.objectUrl); tiles.delete(id); }
|
|
197
|
+
const empty = main.querySelector('.empty');
|
|
198
|
+
if (state.screens.length === 0 && !empty) {
|
|
199
|
+
const e = el('div', 'empty');
|
|
200
|
+
e.append('No screens open. From Claude: ', el('code', null, 'world_create'), ', then ', el('code', null, 'screen_open'), '.');
|
|
201
|
+
main.append(e);
|
|
202
|
+
} else if (state.screens.length && empty) empty.remove();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Short, frequent requests: a tile's picture is fetched only when its screen has a new frame.
|
|
206
|
+
refresh();
|
|
207
|
+
setInterval(refresh, 300);
|
|
208
|
+
</script>
|
|
209
|
+
</body>
|
|
210
|
+
</html>
|
|
211
|
+
`;
|
|
212
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/control-room/page.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,EAAE;AACF,wGAAwG;AACxG,kGAAkG;AAElG,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CACxB,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;AAE9G,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC;;;;;SAKjD,GAAG,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8CL,GAAG,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0JvB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Harness } from '../harness.ts';
|
|
2
|
+
import type { ProductWorld } from '../product.ts';
|
|
3
|
+
export interface ControlRoom {
|
|
4
|
+
url: string;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function serveControlRoom<W extends ProductWorld>(h: Harness<W>, port: number): Promise<ControlRoom>;
|
|
8
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/control-room/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAMlD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,YAAY,EAC3D,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CA6KtB"}
|