@dp-pcs/ogp 0.7.0-rc.1 → 0.7.1
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/docs/RC1-FEDERATION-TEST-CHECKLIST.md +477 -0
- package/package.json +1 -1
- package/scripts/completion.zsh +216 -100
- package/scripts/render-ogp-overview-video.mjs +58 -21
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# OGP RC.1 Federation Test Checklist
|
|
2
|
+
|
|
3
|
+
This is the real-world validation plan for `@dp-pcs/ogp@0.7.0-rc.1` across three gateways:
|
|
4
|
+
|
|
5
|
+
- `OpenClaw` on `https://ogp.sarcastek.com`
|
|
6
|
+
- `Hermes` on `https://hermes.sarcastek.com`
|
|
7
|
+
- `Cosmo` on the AWS machine as the hub
|
|
8
|
+
- `Aleph` on `https://ogp-aleph.aicoe.fit` operated by Stephen Barr
|
|
9
|
+
|
|
10
|
+
The goal is not just "can they federate once?" The goal is to prove the full `rc.1` surface area behaves correctly under normal operation, restart/recovery, and obvious failure modes.
|
|
11
|
+
|
|
12
|
+
## Success Criteria
|
|
13
|
+
|
|
14
|
+
`rc.1` is in decent shape if all of these are true:
|
|
15
|
+
|
|
16
|
+
- all three gateways answer `/.well-known/ogp` and `/federation/ping`
|
|
17
|
+
- all three pairings can request, approve, exchange traffic, and remove cleanly
|
|
18
|
+
- peer health transitions make sense when a daemon goes down and comes back
|
|
19
|
+
- scoped access is enforced
|
|
20
|
+
- agent-comms works, including reply-wait paths
|
|
21
|
+
- project-intent flows work across real gateways, not just the local harness
|
|
22
|
+
- multi-agent/persona routing works where advertised
|
|
23
|
+
- rendezvous/invite flow works if you intend to rely on it
|
|
24
|
+
- restart behavior is acceptable, including daemon and tunnel recovery
|
|
25
|
+
|
|
26
|
+
## Participants
|
|
27
|
+
|
|
28
|
+
Set these once in your shell while testing:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export OC_URL="https://ogp.sarcastek.com"
|
|
32
|
+
export HERMES_URL="https://hermes.sarcastek.com"
|
|
33
|
+
export COSMO_URL="https://david-proctor.gw.clawporate.elelem.expert"
|
|
34
|
+
export ALEPH_URL="https://ogp-aleph.aicoe.fit"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Useful topology:
|
|
38
|
+
|
|
39
|
+
| Pair | Why it matters |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| OpenClaw <-> Hermes | Cross-framework, same operator, easiest first smoke |
|
|
42
|
+
| OpenClaw <-> Cosmo | David local gateway to AWS hub |
|
|
43
|
+
| Hermes <-> Cosmo | David second local gateway to AWS hub |
|
|
44
|
+
| Aleph <-> Cosmo | Stephen gateway to AWS hub |
|
|
45
|
+
|
|
46
|
+
Primary `rc.1` goal for this round:
|
|
47
|
+
|
|
48
|
+
- prove both David and Stephen can federate with Cosmo cleanly
|
|
49
|
+
- prove Cosmo stays healthy as the shared external hub
|
|
50
|
+
- prove project-intent and agent-comms traffic survives the hub topology
|
|
51
|
+
|
|
52
|
+
Direct `OpenClaw/Hermes <-> Aleph` federation is optional for this round unless you specifically want a full mesh test.
|
|
53
|
+
|
|
54
|
+
## 0. Preflight On Every Machine
|
|
55
|
+
|
|
56
|
+
Run this before any pairwise testing:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ogp --version
|
|
60
|
+
ogp --for all status
|
|
61
|
+
ogp whoami
|
|
62
|
+
ogp config health-check show
|
|
63
|
+
curl -sS "$OC_URL/.well-known/ogp" | jq .
|
|
64
|
+
curl -sS "$HERMES_URL/.well-known/ogp" | jq .
|
|
65
|
+
curl -sS "$COSMO_URL/.well-known/ogp" | jq .
|
|
66
|
+
curl -sS "$ALEPH_URL/.well-known/ogp" | jq .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Check:
|
|
70
|
+
|
|
71
|
+
- version is `0.7.0-rc.1` everywhere
|
|
72
|
+
- each daemon is actually listening on its configured port
|
|
73
|
+
- each public URL matches the gateway's configured `gatewayUrl`
|
|
74
|
+
- each gateway advertises `multi-agent-personas` in `capabilities.features`
|
|
75
|
+
- each gateway advertises the expected `agents[]`
|
|
76
|
+
|
|
77
|
+
### Optional: speed up health-check testing
|
|
78
|
+
|
|
79
|
+
For testing, lower the heartbeat interval so you do not wait forever for state changes:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ogp config health-check interval 30000
|
|
83
|
+
ogp config health-check timeout 5000
|
|
84
|
+
ogp config health-check max-failures 2
|
|
85
|
+
ogp stop --for all
|
|
86
|
+
ogp start --for all --background
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Expected result:
|
|
90
|
+
|
|
91
|
+
- health config shows `30s / 5s / 2 failures`
|
|
92
|
+
- after restart, `ogp --for all status` shows both daemons up
|
|
93
|
+
|
|
94
|
+
## 1. Local Harness First
|
|
95
|
+
|
|
96
|
+
Before you spend time on the three-machine test, run the existing local project-intent harness in this repo:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run build
|
|
100
|
+
npm test -- --run
|
|
101
|
+
npm run test:project-intents
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Optional stateful run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run test:project-intents -- --keep-state
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This catches obvious regressions in:
|
|
111
|
+
|
|
112
|
+
- federation request / approve
|
|
113
|
+
- project create / join / contribute / query / status-peer
|
|
114
|
+
- local state persistence
|
|
115
|
+
|
|
116
|
+
## 2. Discovery And Liveness
|
|
117
|
+
|
|
118
|
+
Run these against every public gateway:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
ogp federation ping "$OC_URL"
|
|
122
|
+
ogp federation ping "$HERMES_URL"
|
|
123
|
+
ogp federation ping "$COSMO_URL"
|
|
124
|
+
ogp federation ping "$ALEPH_URL"
|
|
125
|
+
|
|
126
|
+
curl -sS "$OC_URL/.well-known/ogp" | jq '{version,displayName,gatewayUrl,features:.capabilities.features,agents}'
|
|
127
|
+
curl -sS "$HERMES_URL/.well-known/ogp" | jq '{version,displayName,gatewayUrl,features:.capabilities.features,agents}'
|
|
128
|
+
curl -sS "$COSMO_URL/.well-known/ogp" | jq '{version,displayName,gatewayUrl,features:.capabilities.features,agents}'
|
|
129
|
+
curl -sS "$ALEPH_URL/.well-known/ogp" | jq '{version,displayName,gatewayUrl,features:.capabilities.features,agents}'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Expected result:
|
|
133
|
+
|
|
134
|
+
- ping succeeds for all three URLs
|
|
135
|
+
- each card shows `version: 0.7.0-rc.1`
|
|
136
|
+
- `gatewayUrl` in the card matches the public URL you hit
|
|
137
|
+
- `agents[]` looks correct for that gateway
|
|
138
|
+
|
|
139
|
+
## 3. Federation Lifecycle
|
|
140
|
+
|
|
141
|
+
Run this for each required hub/spoke pair.
|
|
142
|
+
|
|
143
|
+
Example commands from OpenClaw -> Hermes:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
ogp --for openclaw federation request "$HERMES_URL" --alias hermes-local
|
|
147
|
+
ogp --for hermes federation list --status pending
|
|
148
|
+
ogp --for hermes federation approve hermes-local --intents message,agent-comms,project.join,project.contribute,project.query,project.status
|
|
149
|
+
ogp --for openclaw federation list
|
|
150
|
+
ogp --for hermes federation list
|
|
151
|
+
ogp --for openclaw federation status
|
|
152
|
+
ogp --for hermes federation status
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Repeat the same flow for:
|
|
156
|
+
|
|
157
|
+
- OpenClaw -> Cosmo
|
|
158
|
+
- Hermes -> Cosmo
|
|
159
|
+
- Aleph -> Cosmo
|
|
160
|
+
|
|
161
|
+
Expected result:
|
|
162
|
+
|
|
163
|
+
- requester sees the peer move to `approved`
|
|
164
|
+
- approver sees the requester move to `approved`
|
|
165
|
+
- status output shows the correct alias and gateway
|
|
166
|
+
- no peer lands in a weird half-approved state
|
|
167
|
+
|
|
168
|
+
## 4. Health Checks And Heartbeat State
|
|
169
|
+
|
|
170
|
+
After all pairs are approved, inspect the health view:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ogp --for openclaw federation status
|
|
174
|
+
ogp --for hermes federation status
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Look for:
|
|
178
|
+
|
|
179
|
+
- `healthy` peers
|
|
180
|
+
- a sensible `healthState` such as `established`
|
|
181
|
+
- recent inbound and outbound timestamps
|
|
182
|
+
|
|
183
|
+
### Failure and recovery test
|
|
184
|
+
|
|
185
|
+
For one pair at a time, stop one side and watch the other side detect it:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
ogp --for hermes stop
|
|
189
|
+
ogp --for openclaw federation status
|
|
190
|
+
sleep 70
|
|
191
|
+
ogp --for openclaw federation status
|
|
192
|
+
ogp --for hermes start --background
|
|
193
|
+
sleep 70
|
|
194
|
+
ogp --for openclaw federation status
|
|
195
|
+
ogp --for hermes federation status
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Expected result:
|
|
199
|
+
|
|
200
|
+
- after enough failed heartbeats, the surviving peer marks the stopped peer degraded or down
|
|
201
|
+
- after restart, health recovers back to `established`
|
|
202
|
+
- `/.well-known/ogp` becomes reachable again after restart
|
|
203
|
+
|
|
204
|
+
Run the same stop/restart test with Cosmo, because that is the critical shared dependency.
|
|
205
|
+
|
|
206
|
+
## 5. Identity And Persona Advertisement
|
|
207
|
+
|
|
208
|
+
Check that each peer advertises the expected identity and agents:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
curl -sS "$OC_URL/.well-known/ogp" | jq '.agents'
|
|
212
|
+
curl -sS "$HERMES_URL/.well-known/ogp" | jq '.agents'
|
|
213
|
+
curl -sS "$COSMO_URL/.well-known/ogp" | jq '.agents'
|
|
214
|
+
curl -sS "$ALEPH_URL/.well-known/ogp" | jq '.agents'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Then change local identity on one gateway and push it to an approved peer:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
ogp config show-identity
|
|
221
|
+
ogp federation update-identity <peer-id>
|
|
222
|
+
ogp federation status
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Expected result:
|
|
226
|
+
|
|
227
|
+
- approved peers see the updated identity snapshot
|
|
228
|
+
- no federation relationship breaks just because identity fields changed
|
|
229
|
+
|
|
230
|
+
## 6. Basic Message Traffic
|
|
231
|
+
|
|
232
|
+
Test raw message intent first:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
ogp federation send <peer-id> message '{"text":"hello from rc1"}'
|
|
236
|
+
ogp federation send <peer-id> task-request '{"task":"echo test","priority":"normal"}'
|
|
237
|
+
ogp federation send <peer-id> status-update '{"status":"completed","source":"rc1-manual-test"}'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Expected result:
|
|
241
|
+
|
|
242
|
+
- receiver gets the message
|
|
243
|
+
- sender gets a successful transport result
|
|
244
|
+
- daemon log records the request cleanly
|
|
245
|
+
|
|
246
|
+
## 7. Agent-Comms
|
|
247
|
+
|
|
248
|
+
First inspect policies:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
ogp agent-comms policies
|
|
252
|
+
ogp agent-comms activity --last 20
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Then test real agent-comms:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
ogp federation agent <peer-id> memory-management "Store this test marker"
|
|
259
|
+
ogp federation agent <peer-id> queries "What gateway are you on?" --wait --timeout 60000
|
|
260
|
+
ogp federation agent <peer-id> task-delegation "Return a short ack" --priority high --wait --timeout 60000
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Expected result:
|
|
264
|
+
|
|
265
|
+
- fire-and-forget delivery succeeds
|
|
266
|
+
- `--wait` returns a real reply path, not just transport success
|
|
267
|
+
- activity log records the interaction
|
|
268
|
+
|
|
269
|
+
### Persona routing
|
|
270
|
+
|
|
271
|
+
If the peer advertises more than one agent:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
ogp federation agent <peer-id> queries "hello primary" --to-agent junior --wait
|
|
275
|
+
ogp federation agent <peer-id> queries "hello specialist" --to-agent apollo --wait
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Negative test:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
ogp federation agent <peer-id> queries "bad persona test" --to-agent definitely-not-real --wait
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Expected result:
|
|
285
|
+
|
|
286
|
+
- valid persona IDs route successfully
|
|
287
|
+
- invalid persona ID fails with a clear error
|
|
288
|
+
|
|
289
|
+
## 8. Scope Enforcement
|
|
290
|
+
|
|
291
|
+
Approve or grant a peer with restricted scopes, then verify allowed vs denied behavior.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
ogp federation grant <peer-id> --intents message --rate 20/3600
|
|
297
|
+
ogp federation scopes <peer-id>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Now try:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
ogp federation send <peer-id> message '{"text":"allowed"}'
|
|
304
|
+
ogp federation agent <peer-id> memory-management "should be denied"
|
|
305
|
+
ogp project query-peer <peer-id> some-project
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Expected result:
|
|
309
|
+
|
|
310
|
+
- `message` works
|
|
311
|
+
- `agent-comms` fails if not granted
|
|
312
|
+
- project intents fail if project scopes are not granted
|
|
313
|
+
|
|
314
|
+
Then restore the broader scope set and verify those flows work again.
|
|
315
|
+
|
|
316
|
+
## 9. Project Intents
|
|
317
|
+
|
|
318
|
+
This is the highest-value cross-machine test after basic federation.
|
|
319
|
+
|
|
320
|
+
Recommended topology for this round:
|
|
321
|
+
|
|
322
|
+
- create the canonical shared project on `Cosmo`
|
|
323
|
+
- have `OpenClaw`, `Hermes`, and `Aleph` join it
|
|
324
|
+
- send contributions from each spoke into Cosmo
|
|
325
|
+
- verify that queries against Cosmo reflect the combined state
|
|
326
|
+
|
|
327
|
+
On the owner gateway:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
ogp project create rc1-shared "RC1 Shared Test" --description "Real multi-gateway test"
|
|
331
|
+
ogp project contribute rc1-shared note "owner bootstrap note" --local-only
|
|
332
|
+
ogp project status rc1-shared
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
From a remote approved peer:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
ogp project request-join <owner-peer-id> rc1-shared "RC1 Shared Test" --description "Join from remote gateway"
|
|
339
|
+
ogp project query-peer <owner-peer-id> rc1-shared
|
|
340
|
+
ogp project send-contribution <owner-peer-id> rc1-shared task "Remote task from rc1 test"
|
|
341
|
+
ogp project send-contribution <owner-peer-id> rc1-shared decision "Remote decision from rc1 test" --metadata '{"confidence":"high"}'
|
|
342
|
+
ogp project status-peer <owner-peer-id> rc1-shared
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Run this with both remote peers joining the same project.
|
|
346
|
+
|
|
347
|
+
Expected result:
|
|
348
|
+
|
|
349
|
+
- pre-join query should fail or be denied
|
|
350
|
+
- join succeeds and the project membership becomes visible
|
|
351
|
+
- remote contributions show up on the owner gateway
|
|
352
|
+
- query after join returns the new entries
|
|
353
|
+
- status-peer succeeds at least at the transport level
|
|
354
|
+
|
|
355
|
+
Evidence:
|
|
356
|
+
|
|
357
|
+
- `ogp project status rc1-shared`
|
|
358
|
+
- `~/.ogp/projects.json`
|
|
359
|
+
- `~/.ogp-hermes/projects.json`
|
|
360
|
+
- Cosmo's `projects.json`
|
|
361
|
+
- Aleph's `projects.json`
|
|
362
|
+
|
|
363
|
+
## 10. Rendezvous / Invite Flow
|
|
364
|
+
|
|
365
|
+
Only run this if you intend to use rendezvous in production.
|
|
366
|
+
|
|
367
|
+
From one gateway:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
ogp federation invite
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
From another:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
ogp federation accept <token>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Also test connect-by-pubkey:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
ogp federation connect <pubkey>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Expected result:
|
|
386
|
+
|
|
387
|
+
- invite token resolves and creates the same approved peer state you get from direct request/approve
|
|
388
|
+
- connect-by-pubkey finds the peer and completes federation
|
|
389
|
+
- rendezvous registration stays alive while the daemon is up
|
|
390
|
+
|
|
391
|
+
## 11. Removal And Re-Federation
|
|
392
|
+
|
|
393
|
+
For each pair:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
ogp federation remove <peer-id>
|
|
397
|
+
ogp federation list
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Expected result:
|
|
401
|
+
|
|
402
|
+
- removed peer is marked removed locally
|
|
403
|
+
- remote side receives the removal notification on a best-effort basis
|
|
404
|
+
- sending further traffic to the removed peer fails
|
|
405
|
+
|
|
406
|
+
Then immediately re-request federation and make sure the pair can recover cleanly.
|
|
407
|
+
|
|
408
|
+
## 12. Restart, Reboot, And Tunnel Recovery
|
|
409
|
+
|
|
410
|
+
These are operational tests, not protocol tests, but they matter.
|
|
411
|
+
|
|
412
|
+
### Daemon restart
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
ogp --for all stop
|
|
416
|
+
ogp --for all start --background
|
|
417
|
+
ogp --for all status
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Public endpoint recovery
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
curl -sS "$OC_URL/.well-known/ogp" | jq .version
|
|
424
|
+
curl -sS "$HERMES_URL/.well-known/ogp" | jq .version
|
|
425
|
+
curl -sS "$COSMO_URL/.well-known/ogp" | jq .version
|
|
426
|
+
curl -sS "$ALEPH_URL/.well-known/ogp" | jq .version
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Tunnel validation
|
|
430
|
+
|
|
431
|
+
Verify each public URL continues to map to the correct local daemon after restart.
|
|
432
|
+
|
|
433
|
+
## 13. Known Local Risk To Explicitly Test
|
|
434
|
+
|
|
435
|
+
On David's macOS machine, the current LaunchAgent can fail if launchd cannot find `node` for the `ogp` wrapper. That is worth an explicit reboot/login test because it will absolutely look like "tunnel is up but OGP is dead."
|
|
436
|
+
|
|
437
|
+
Check:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
launchctl print gui/$(id -u)/com.dp-pcs.ogp
|
|
441
|
+
sed -n '1,80p' ~/.ogp/launchagent.log
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
If this still shows `env: node: No such file or directory`, autostart is not trustworthy yet.
|
|
445
|
+
|
|
446
|
+
## 14. Evidence To Save Per Test Run
|
|
447
|
+
|
|
448
|
+
For each major run, save:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
ogp --for all status
|
|
452
|
+
ogp --for openclaw federation list
|
|
453
|
+
ogp --for hermes federation list
|
|
454
|
+
ogp agent-comms activity --last 50
|
|
455
|
+
curl -sS "$OC_URL/.well-known/ogp" | jq .
|
|
456
|
+
curl -sS "$HERMES_URL/.well-known/ogp" | jq .
|
|
457
|
+
curl -sS "$COSMO_URL/.well-known/ogp" | jq .
|
|
458
|
+
curl -sS "$ALEPH_URL/.well-known/ogp" | jq .
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Also keep:
|
|
462
|
+
|
|
463
|
+
- `~/.ogp/daemon.log`
|
|
464
|
+
- `~/.ogp-hermes/daemon.log`
|
|
465
|
+
- each gateway's `peers.json`
|
|
466
|
+
- each gateway's `projects.json`
|
|
467
|
+
|
|
468
|
+
## Minimal Ship Gate
|
|
469
|
+
|
|
470
|
+
If you do not have time to run everything, the minimum bar before treating `rc.1` as real is:
|
|
471
|
+
|
|
472
|
+
1. all four public cards return `200`
|
|
473
|
+
2. `OpenClaw <-> Cosmo`, `Hermes <-> Cosmo`, and `Aleph <-> Cosmo` can federate
|
|
474
|
+
3. health changes on Cosmo stop/start are visible and recover from the spoke gateways
|
|
475
|
+
4. one successful `agent-comms --wait` round-trip from each spoke to Cosmo
|
|
476
|
+
5. one successful project join + contribution + query flow with Cosmo as the shared project owner
|
|
477
|
+
6. one successful removal + re-federation cycle against Cosmo
|
package/package.json
CHANGED
package/scripts/completion.zsh
CHANGED
|
@@ -3,12 +3,78 @@
|
|
|
3
3
|
# OGP zsh completion script
|
|
4
4
|
# Auto-generated by ogp completion install
|
|
5
5
|
|
|
6
|
+
_ogp_command_word() {
|
|
7
|
+
local i=2
|
|
8
|
+
|
|
9
|
+
while (( i <= $#words )); do
|
|
10
|
+
case "${words[i]}" in
|
|
11
|
+
--for)
|
|
12
|
+
(( i += 2 ))
|
|
13
|
+
continue
|
|
14
|
+
;;
|
|
15
|
+
--for=*)
|
|
16
|
+
(( i += 1 ))
|
|
17
|
+
continue
|
|
18
|
+
;;
|
|
19
|
+
-h|--help|-v|--version)
|
|
20
|
+
(( i += 1 ))
|
|
21
|
+
continue
|
|
22
|
+
;;
|
|
23
|
+
-*)
|
|
24
|
+
(( i += 1 ))
|
|
25
|
+
continue
|
|
26
|
+
;;
|
|
27
|
+
*)
|
|
28
|
+
print -r -- "${words[i]}"
|
|
29
|
+
return 0
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_ogp_subcommand_word() {
|
|
38
|
+
local parent="$1"
|
|
39
|
+
local seen_parent=0
|
|
40
|
+
local i=2
|
|
41
|
+
|
|
42
|
+
while (( i <= $#words )); do
|
|
43
|
+
case "${words[i]}" in
|
|
44
|
+
--for)
|
|
45
|
+
(( i += 2 ))
|
|
46
|
+
continue
|
|
47
|
+
;;
|
|
48
|
+
--for=*)
|
|
49
|
+
(( i += 1 ))
|
|
50
|
+
continue
|
|
51
|
+
;;
|
|
52
|
+
-*)
|
|
53
|
+
(( i += 1 ))
|
|
54
|
+
continue
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
if (( seen_parent == 0 )); then
|
|
59
|
+
if [[ "${words[i]}" == "$parent" ]]; then
|
|
60
|
+
seen_parent=1
|
|
61
|
+
fi
|
|
62
|
+
(( i += 1 ))
|
|
63
|
+
continue
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
print -r -- "${words[i]}"
|
|
67
|
+
return 0
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
6
73
|
_ogp() {
|
|
7
|
-
local -a frameworks
|
|
74
|
+
local -a frameworks
|
|
8
75
|
local curcontext="$curcontext" state line
|
|
9
76
|
typeset -A opt_args
|
|
10
77
|
|
|
11
|
-
# Get frameworks for --for flag
|
|
12
78
|
frameworks=(${(f)"$(ogp config list --quiet 2>/dev/null)"} all)
|
|
13
79
|
|
|
14
80
|
_arguments -C \
|
|
@@ -40,7 +106,7 @@ _ogp() {
|
|
|
40
106
|
))'
|
|
41
107
|
;;
|
|
42
108
|
args)
|
|
43
|
-
case $
|
|
109
|
+
case "$(_ogp_command_word)" in
|
|
44
110
|
federation)
|
|
45
111
|
_ogp_federation
|
|
46
112
|
;;
|
|
@@ -69,41 +135,44 @@ _ogp() {
|
|
|
69
135
|
|
|
70
136
|
_ogp_federation() {
|
|
71
137
|
local curcontext="$curcontext" state line
|
|
138
|
+
local subcmd="$(_ogp_subcommand_word federation)"
|
|
72
139
|
typeset -A opt_args
|
|
73
140
|
|
|
74
|
-
_arguments
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
141
|
+
_arguments \
|
|
142
|
+
'1:subcommand:((
|
|
143
|
+
list\:"List all peers"
|
|
144
|
+
status\:"Show federation status and alias mappings"
|
|
145
|
+
request\:"Send federation request to a peer"
|
|
146
|
+
connect\:"Connect to peer by public key via rendezvous"
|
|
147
|
+
invite\:"Generate invite token to share with peer"
|
|
148
|
+
accept\:"Accept peer invite token and connect"
|
|
149
|
+
approve\:"Approve pending federation request"
|
|
150
|
+
reject\:"Reject pending federation request"
|
|
151
|
+
remove\:"Remove peer from federation list"
|
|
152
|
+
alias\:"Set user-friendly alias for peer"
|
|
153
|
+
tag\:"Add tags to a peer (local categorization)"
|
|
154
|
+
untag\:"Remove tags from a peer"
|
|
155
|
+
update-identity\:"Send updated identity information to approved peer"
|
|
156
|
+
ping\:"Ping peer gateway to test connectivity"
|
|
157
|
+
send\:"Send message to federated peer"
|
|
158
|
+
scopes\:"Show scope grants for peer"
|
|
159
|
+
grant\:"Update scope grants for approved peer"
|
|
160
|
+
agent\:"Send agent-comms message to peer"
|
|
161
|
+
))' \
|
|
162
|
+
'*::arg:->args'
|
|
95
163
|
|
|
96
164
|
case "$state" in
|
|
97
165
|
args)
|
|
98
|
-
case $
|
|
166
|
+
case "$subcmd" in
|
|
99
167
|
list)
|
|
100
168
|
_arguments \
|
|
101
|
-
'(-s --status)'{-s,--status}'[Filter by status]:status:(pending approved rejected removed)'
|
|
169
|
+
'(-s --status)'{-s,--status}'[Filter by status]:status:(pending approved rejected removed)' \
|
|
170
|
+
'(-t --tag)'{-t,--tag}'[Filter by tag]:tag:'
|
|
102
171
|
;;
|
|
103
172
|
request)
|
|
104
173
|
_arguments \
|
|
105
174
|
'1:peer-url:' \
|
|
106
|
-
'2:
|
|
175
|
+
'2:alias::' \
|
|
107
176
|
'(-a --alias)'{-a,--alias}'[User-friendly alias]:alias:' \
|
|
108
177
|
'--petname[Deprecated - use --alias]:petname:'
|
|
109
178
|
;;
|
|
@@ -113,16 +182,29 @@ _ogp_federation() {
|
|
|
113
182
|
'(-a --alias)'{-a,--alias}'[User-friendly alias]:alias:' \
|
|
114
183
|
'--petname[Deprecated - use --alias]:petname:'
|
|
115
184
|
;;
|
|
116
|
-
approve)
|
|
185
|
+
approve|grant)
|
|
117
186
|
_arguments \
|
|
118
187
|
'1:peer-id:' \
|
|
119
188
|
'--intents[Comma-separated intents to grant]:intents:' \
|
|
120
|
-
'--rate[Rate limit as requests/
|
|
189
|
+
'--rate[Rate limit as requests/windowSeconds]:rate:' \
|
|
121
190
|
'--topics[Comma-separated topics for agent-comms]:topics:'
|
|
122
191
|
;;
|
|
123
|
-
reject|remove|scopes|
|
|
192
|
+
reject|remove|scopes|update-identity)
|
|
124
193
|
_arguments '1:peer-id:'
|
|
125
194
|
;;
|
|
195
|
+
alias)
|
|
196
|
+
_arguments \
|
|
197
|
+
'1:peer-id:' \
|
|
198
|
+
'2:alias:'
|
|
199
|
+
;;
|
|
200
|
+
tag|untag)
|
|
201
|
+
_arguments \
|
|
202
|
+
'1:peer-id:' \
|
|
203
|
+
'*:tag:'
|
|
204
|
+
;;
|
|
205
|
+
ping)
|
|
206
|
+
_arguments '1:peer-url:'
|
|
207
|
+
;;
|
|
126
208
|
send)
|
|
127
209
|
_arguments \
|
|
128
210
|
'1:peer-id:' \
|
|
@@ -130,13 +212,6 @@ _ogp_federation() {
|
|
|
130
212
|
'3:payload:' \
|
|
131
213
|
'--to-agent[Target a specific persona on the peer]:persona:'
|
|
132
214
|
;;
|
|
133
|
-
grant)
|
|
134
|
-
_arguments \
|
|
135
|
-
'1:peer-id:' \
|
|
136
|
-
'--intents[Comma-separated intents to grant]:intents:' \
|
|
137
|
-
'--rate[Rate limit as requests/seconds]:rate:' \
|
|
138
|
-
'--topics[Comma-separated topics for agent-comms]:topics:'
|
|
139
|
-
;;
|
|
140
215
|
agent)
|
|
141
216
|
_arguments \
|
|
142
217
|
'1:peer-id:' \
|
|
@@ -155,25 +230,27 @@ _ogp_federation() {
|
|
|
155
230
|
|
|
156
231
|
_ogp_agent_comms() {
|
|
157
232
|
local curcontext="$curcontext" state line
|
|
233
|
+
local subcmd="$(_ogp_subcommand_word agent-comms)"
|
|
158
234
|
typeset -A opt_args
|
|
159
235
|
|
|
160
|
-
_arguments
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
_arguments \
|
|
237
|
+
'1:subcommand:((
|
|
238
|
+
policies\:"Show response policies"
|
|
239
|
+
configure\:"Configure response policies"
|
|
240
|
+
add-topic\:"Add topic to peer response policy"
|
|
241
|
+
set-topic\:"Set topic policy for peer"
|
|
242
|
+
set-default\:"Set per-peer default level"
|
|
243
|
+
remove-topic\:"Remove topic from peer policy"
|
|
244
|
+
reset\:"Reset peer policy to global defaults"
|
|
245
|
+
activity\:"Show agent-comms activity log"
|
|
246
|
+
default\:"Set default response level for unknown topics"
|
|
247
|
+
logging\:"Enable or disable activity logging"
|
|
248
|
+
))' \
|
|
249
|
+
'*::arg:->args'
|
|
173
250
|
|
|
174
251
|
case "$state" in
|
|
175
252
|
args)
|
|
176
|
-
case $
|
|
253
|
+
case "$subcmd" in
|
|
177
254
|
policies|activity)
|
|
178
255
|
_arguments '1:peer-id::'
|
|
179
256
|
;;
|
|
@@ -204,8 +281,13 @@ _ogp_agent_comms() {
|
|
|
204
281
|
'1:peer-id:' \
|
|
205
282
|
'2:level:(full summary escalate deny off)'
|
|
206
283
|
;;
|
|
207
|
-
remove-topic
|
|
208
|
-
_arguments
|
|
284
|
+
remove-topic)
|
|
285
|
+
_arguments \
|
|
286
|
+
'1:peer-id:' \
|
|
287
|
+
'2:topic:'
|
|
288
|
+
;;
|
|
289
|
+
reset)
|
|
290
|
+
_arguments '1:peer-id:'
|
|
209
291
|
;;
|
|
210
292
|
default)
|
|
211
293
|
_arguments '1:level:(full summary escalate deny off)'
|
|
@@ -220,29 +302,32 @@ _ogp_agent_comms() {
|
|
|
220
302
|
|
|
221
303
|
_ogp_config() {
|
|
222
304
|
local curcontext="$curcontext" state line
|
|
223
|
-
typeset -A opt_args
|
|
224
305
|
local -a frameworks
|
|
306
|
+
local subcmd="$(_ogp_subcommand_word config)"
|
|
307
|
+
typeset -A opt_args
|
|
308
|
+
|
|
225
309
|
frameworks=(${(f)"$(ogp config list --quiet 2>/dev/null)"})
|
|
226
310
|
|
|
227
|
-
_arguments
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
311
|
+
_arguments \
|
|
312
|
+
'1:subcommand:((
|
|
313
|
+
show\:"Show all configured frameworks and default"
|
|
314
|
+
set-default\:"Set default framework"
|
|
315
|
+
list\:"List all frameworks"
|
|
316
|
+
enable\:"Enable a framework"
|
|
317
|
+
disable\:"Disable a framework"
|
|
318
|
+
frameworks\:"Show all detected frameworks"
|
|
319
|
+
health-check\:"Manage health check configuration"
|
|
320
|
+
show-identity\:"Show current identity configuration"
|
|
321
|
+
set-identity\:"Update identity information"
|
|
322
|
+
set-tags\:"Set tags (replaces existing)"
|
|
323
|
+
add-tag\:"Add a tag"
|
|
324
|
+
remove-tag\:"Remove a tag"
|
|
325
|
+
))' \
|
|
326
|
+
'*::arg:->args'
|
|
242
327
|
|
|
243
328
|
case "$state" in
|
|
244
329
|
args)
|
|
245
|
-
case $
|
|
330
|
+
case "$subcmd" in
|
|
246
331
|
set-default|enable|disable)
|
|
247
332
|
_arguments "1:framework:($frameworks)"
|
|
248
333
|
;;
|
|
@@ -258,32 +343,58 @@ _ogp_config() {
|
|
|
258
343
|
'--agent-name[Agent name]:name:' \
|
|
259
344
|
'--organization[Organization name]:org:'
|
|
260
345
|
;;
|
|
346
|
+
set-tags)
|
|
347
|
+
_arguments '*:tag:'
|
|
348
|
+
;;
|
|
349
|
+
add-tag|remove-tag)
|
|
350
|
+
_arguments '1:tag:'
|
|
351
|
+
;;
|
|
261
352
|
esac
|
|
262
353
|
;;
|
|
263
354
|
esac
|
|
264
355
|
}
|
|
265
356
|
|
|
266
357
|
_ogp_config_health_check() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
358
|
+
local curcontext="$curcontext" state line
|
|
359
|
+
local subcmd="$(_ogp_subcommand_word health-check)"
|
|
360
|
+
typeset -A opt_args
|
|
361
|
+
|
|
362
|
+
_arguments \
|
|
363
|
+
'1:subcommand:((
|
|
364
|
+
show\:"Show current health check configuration"
|
|
365
|
+
interval\:"Set health check interval in milliseconds"
|
|
366
|
+
timeout\:"Set health check timeout in milliseconds"
|
|
367
|
+
max-failures\:"Set max consecutive failures before unhealthy"
|
|
368
|
+
))' \
|
|
369
|
+
'*::arg:->args'
|
|
370
|
+
|
|
371
|
+
case "$state" in
|
|
372
|
+
args)
|
|
373
|
+
case "$subcmd" in
|
|
374
|
+
interval|timeout|max-failures)
|
|
375
|
+
_arguments '1:value:'
|
|
376
|
+
;;
|
|
377
|
+
esac
|
|
378
|
+
;;
|
|
379
|
+
esac
|
|
274
380
|
}
|
|
275
381
|
|
|
276
382
|
_ogp_intent() {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
383
|
+
local curcontext="$curcontext" state line
|
|
384
|
+
local subcmd="$(_ogp_subcommand_word intent)"
|
|
385
|
+
typeset -A opt_args
|
|
386
|
+
|
|
387
|
+
_arguments \
|
|
388
|
+
'1:subcommand:((
|
|
389
|
+
register\:"Register new intent handler"
|
|
390
|
+
list\:"List all registered intents"
|
|
391
|
+
remove\:"Remove registered intent"
|
|
392
|
+
))' \
|
|
393
|
+
'*::arg:->args'
|
|
283
394
|
|
|
284
395
|
case "$state" in
|
|
285
396
|
args)
|
|
286
|
-
case $
|
|
397
|
+
case "$subcmd" in
|
|
287
398
|
register)
|
|
288
399
|
_arguments \
|
|
289
400
|
'1:name:' \
|
|
@@ -299,25 +410,30 @@ _ogp_intent() {
|
|
|
299
410
|
}
|
|
300
411
|
|
|
301
412
|
_ogp_project() {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
413
|
+
local curcontext="$curcontext" state line
|
|
414
|
+
local subcmd="$(_ogp_subcommand_word project)"
|
|
415
|
+
typeset -A opt_args
|
|
416
|
+
|
|
417
|
+
_arguments \
|
|
418
|
+
'1:subcommand:((
|
|
419
|
+
create\:"Create new project locally"
|
|
420
|
+
join\:"Join existing project"
|
|
421
|
+
list\:"List all local projects"
|
|
422
|
+
remove\:"Remove local project"
|
|
423
|
+
contribute\:"Add contribution to project"
|
|
424
|
+
query\:"Query project contributions"
|
|
425
|
+
status\:"Show project status overview"
|
|
426
|
+
request-join\:"Request to join project from peer"
|
|
427
|
+
send-contribution\:"Send contribution to peer project"
|
|
428
|
+
query-peer\:"Query peer project contributions"
|
|
429
|
+
status-peer\:"Request project status from peer"
|
|
430
|
+
delete\:"Delete local project and all contributions"
|
|
431
|
+
))' \
|
|
432
|
+
'*::arg:->args'
|
|
317
433
|
|
|
318
434
|
case "$state" in
|
|
319
435
|
args)
|
|
320
|
-
case $
|
|
436
|
+
case "$subcmd" in
|
|
321
437
|
create)
|
|
322
438
|
_arguments \
|
|
323
439
|
'1:project-id:' \
|
|
@@ -11,7 +11,9 @@ const renderSlidesOnly = process.argv.includes('--slides-only');
|
|
|
11
11
|
|
|
12
12
|
const width = 1920;
|
|
13
13
|
const height = 1080;
|
|
14
|
-
const
|
|
14
|
+
const fps = 30;
|
|
15
|
+
const clipDuration = 6.8;
|
|
16
|
+
const transitionDuration = 0.6;
|
|
15
17
|
|
|
16
18
|
const palette = {
|
|
17
19
|
ink: '#10131c',
|
|
@@ -386,32 +388,67 @@ for (const png of pngs) {
|
|
|
386
388
|
}
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
+
const motionProfiles = [
|
|
392
|
+
{ dx: 26, dy: 14, phaseX: 0.0, phaseY: 0.5 },
|
|
393
|
+
{ dx: -24, dy: 18, phaseX: 0.8, phaseY: 1.2 },
|
|
394
|
+
{ dx: 20, dy: -16, phaseX: 1.4, phaseY: 0.3 },
|
|
395
|
+
{ dx: -22, dy: -14, phaseX: 0.2, phaseY: 1.8 }
|
|
396
|
+
];
|
|
397
|
+
const transitions = [
|
|
398
|
+
'smoothleft',
|
|
399
|
+
'fadeblack',
|
|
400
|
+
'smoothup',
|
|
401
|
+
'circleopen',
|
|
402
|
+
'smoothright',
|
|
403
|
+
'wipeleft',
|
|
404
|
+
'fadegrays',
|
|
405
|
+
'diagtl',
|
|
406
|
+
'smoothdown'
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
const ffmpegArgs = ['-y'];
|
|
391
410
|
for (const png of pngs) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
411
|
+
ffmpegArgs.push('-loop', '1', '-t', String(clipDuration), '-i', resolve(png));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const filterParts = [];
|
|
415
|
+
for (let i = 0; i < pngs.length; i++) {
|
|
416
|
+
const profile = motionProfiles[i % motionProfiles.length];
|
|
417
|
+
filterParts.push(
|
|
418
|
+
`[${i}:v]scale=2160:1215,` +
|
|
419
|
+
`crop=${width}:${height}:` +
|
|
420
|
+
`x='(in_w-out_w)/2+(${profile.dx})*sin(t*0.42+${profile.phaseX})':` +
|
|
421
|
+
`y='(in_h-out_h)/2+(${profile.dy})*cos(t*0.34+${profile.phaseY})',` +
|
|
422
|
+
`fps=${fps},trim=duration=${clipDuration},setpts=PTS-STARTPTS[v${i}]`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let currentLabel = 'v0';
|
|
427
|
+
for (let i = 1; i < pngs.length; i++) {
|
|
428
|
+
const offset = ((clipDuration - transitionDuration) * i).toFixed(3);
|
|
429
|
+
const nextLabel = i === pngs.length - 1 ? 'outv' : `x${i}`;
|
|
430
|
+
const transition = transitions[(i - 1) % transitions.length];
|
|
431
|
+
filterParts.push(
|
|
432
|
+
`[${currentLabel}][v${i}]xfade=transition=${transition}:duration=${transitionDuration}:offset=${offset}[${nextLabel}]`
|
|
433
|
+
);
|
|
434
|
+
currentLabel = nextLabel;
|
|
395
435
|
}
|
|
396
|
-
concatLines.push(`file '${resolve(pngs.at(-1)).replaceAll("'", "'\\''")}'`);
|
|
397
|
-
writeFileSync(concatPath, concatLines.join('\n') + '\n');
|
|
398
436
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
'-
|
|
402
|
-
'-
|
|
403
|
-
'-
|
|
404
|
-
'-i', concatPath,
|
|
405
|
-
'-vf', 'format=yuv420p,fps=30',
|
|
406
|
-
'-r', '30',
|
|
437
|
+
const animatedVideo = join(outDir, 'ogp-overview-demo-animated.mp4');
|
|
438
|
+
ffmpegArgs.push(
|
|
439
|
+
'-filter_complex', filterParts.join(';'),
|
|
440
|
+
'-map', `[${currentLabel}]`,
|
|
441
|
+
'-r', String(fps),
|
|
407
442
|
'-c:v', 'libx264',
|
|
408
443
|
'-movflags', '+faststart',
|
|
409
444
|
'-pix_fmt', 'yuv420p',
|
|
410
|
-
|
|
411
|
-
|
|
445
|
+
animatedVideo
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
execFileSync('ffmpeg', ffmpegArgs, { stdio: 'inherit' });
|
|
412
449
|
|
|
413
|
-
if (!existsSync(
|
|
414
|
-
throw new Error(`Render failed: ${
|
|
450
|
+
if (!existsSync(animatedVideo)) {
|
|
451
|
+
throw new Error(`Render failed: ${animatedVideo} was not created`);
|
|
415
452
|
}
|
|
416
453
|
|
|
417
|
-
console.log(`Rendered ${
|
|
454
|
+
console.log(`Rendered ${animatedVideo}`);
|