forge-openclaw-plugin 0.2.60 → 0.2.65

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.
Files changed (59) hide show
  1. package/README.md +121 -51
  2. package/dist/assets/{board-B1V3M__K.js → board-DUwMfZvN.js} +1 -1
  3. package/dist/assets/index-B9VOpR7r.css +1 -0
  4. package/dist/assets/index-DoHjjze2.js +90 -0
  5. package/dist/assets/{motion-CltSTItx.js → motion-Crg3QyXD.js} +1 -1
  6. package/dist/assets/{table-B-VrSFx8.js → table-CTlDeYRs.js} +1 -1
  7. package/dist/assets/{ui-DUqM4jkt.js → ui-CJPaElbj.js} +1 -1
  8. package/dist/assets/{vendor-C0otBhgu.js → vendor-BdrT2htV.js} +217 -207
  9. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  11. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  12. package/dist/companion-iroh-src/Cargo.lock +4559 -0
  13. package/dist/companion-iroh-src/Cargo.toml +37 -0
  14. package/dist/companion-iroh-src/src/lib.rs +279 -0
  15. package/dist/companion-iroh-src/src/main.rs +478 -0
  16. package/dist/companion-iroh-src/src/protocol.rs +129 -0
  17. package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
  18. package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
  19. package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
  20. package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
  21. package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
  22. package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
  23. package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
  24. package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
  25. package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
  26. package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
  27. package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
  28. package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
  29. package/dist/index.html +7 -7
  30. package/dist/openclaw/parity.js +27 -0
  31. package/dist/openclaw/plugin-entry-shared.js +2 -2
  32. package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
  33. package/dist/openclaw/routes.d.ts +4 -0
  34. package/dist/openclaw/routes.js +112 -3
  35. package/dist/openclaw/tools.js +32 -4
  36. package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
  37. package/dist/server/server/src/app.js +288 -61
  38. package/dist/server/server/src/data-management-types.js +2 -0
  39. package/dist/server/server/src/discovery-advertiser.js +13 -0
  40. package/dist/server/server/src/health.js +58 -3
  41. package/dist/server/server/src/movement.js +16 -1
  42. package/dist/server/server/src/openapi.js +410 -9
  43. package/dist/server/server/src/repositories/rewards.js +60 -0
  44. package/dist/server/server/src/services/companion-iroh.js +425 -0
  45. package/dist/server/server/src/services/data-management.js +32 -2
  46. package/dist/server/server/src/services/doctor.js +762 -0
  47. package/dist/server/server/src/services/gamification.js +75 -3
  48. package/dist/server/server/src/services/life-force.js +166 -25
  49. package/dist/server/server/src/web.js +88 -12
  50. package/dist/server/src/lib/api.js +9 -0
  51. package/dist/server/src/lib/gamification-catalog.js +1 -1
  52. package/openclaw.plugin.json +85 -3
  53. package/package.json +10 -6
  54. package/server/migrations/059_data_backup_retention.sql +2 -0
  55. package/skills/forge-openclaw/SKILL.md +80 -19
  56. package/skills/forge-openclaw/entity_conversation_playbooks.md +283 -25
  57. package/skills/forge-openclaw/psyche_entity_playbooks.md +82 -0
  58. package/dist/assets/index-BwKAPo98.css +0 -1
  59. package/dist/assets/index-Dy7c-dRY.js +0 -90
@@ -0,0 +1,478 @@
1
+ use std::path::{Path, PathBuf};
2
+ use std::time::Duration;
3
+
4
+ use anyhow::{Context, anyhow};
5
+ use base64::Engine;
6
+ use clap::{Parser, Subcommand};
7
+ use forge_companion_iroh::protocol::{
8
+ BridgeRequest, BridgeResponse, COMPANION_ALPN, FORGE_AGENT_NAME, ForgeHttpRequest,
9
+ ForgeHttpResponse, HeaderPair, PROTOCOL_VERSION, PairPayload,
10
+ };
11
+ use iroh::endpoint::{IdleTimeout, QuicTransportConfig, RecvStream, SendStream, presets};
12
+ use iroh::{Endpoint, SecretKey};
13
+ use reqwest::header::{HeaderName, HeaderValue};
14
+ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
15
+ use tracing::{info, warn};
16
+ use tracing_subscriber::EnvFilter;
17
+
18
+ const MAX_FRAME_BYTES: usize = 50 * 1024 * 1024;
19
+
20
+ #[derive(Parser, Debug)]
21
+ #[command(version, about = "Forge Companion transport over Iroh QUIC")]
22
+ struct Cli {
23
+ #[command(subcommand)]
24
+ command: Command,
25
+ }
26
+
27
+ #[derive(Subcommand, Debug)]
28
+ enum Command {
29
+ Host(HostArgs),
30
+ Probe(ProbeArgs),
31
+ }
32
+
33
+ #[derive(Parser, Debug)]
34
+ struct HostArgs {
35
+ #[arg(long)]
36
+ state_dir: PathBuf,
37
+ #[arg(long)]
38
+ token: Option<String>,
39
+ #[arg(long)]
40
+ local_base_url: String,
41
+ #[arg(long)]
42
+ relay: Option<String>,
43
+ }
44
+
45
+ #[derive(Parser, Debug)]
46
+ struct ProbeArgs {
47
+ #[arg(long)]
48
+ node_id: String,
49
+ #[arg(long)]
50
+ token: String,
51
+ #[arg(long)]
52
+ relay: Option<String>,
53
+ #[arg(long, default_value = "GET")]
54
+ method: String,
55
+ #[arg(long, default_value = "/api/v1/health")]
56
+ path: String,
57
+ }
58
+
59
+ #[tokio::main]
60
+ async fn main() -> anyhow::Result<()> {
61
+ init_logging();
62
+ match Cli::parse().command {
63
+ Command::Host(args) => run_host(args).await,
64
+ Command::Probe(args) => run_probe(args).await,
65
+ }
66
+ }
67
+
68
+ async fn run_host(args: HostArgs) -> anyhow::Result<()> {
69
+ tokio::fs::create_dir_all(&args.state_dir)
70
+ .await
71
+ .with_context(|| format!("creating {}", args.state_dir.display()))?;
72
+ let secret_key = load_or_create_secret_key(&args.state_dir).await?;
73
+ let token = match args.token {
74
+ Some(token) => token,
75
+ None => load_or_create_token(&args.state_dir).await?,
76
+ };
77
+ let endpoint = bind_endpoint(secret_key.clone()).await?;
78
+ wait_for_endpoint_route(&endpoint).await;
79
+ let relay = endpoint_home_relay(&endpoint).or(args.relay);
80
+ let payload = PairPayload {
81
+ v: PROTOCOL_VERSION,
82
+ node_id: secret_key.public().to_string(),
83
+ token,
84
+ host_name: local_host_name(),
85
+ relay,
86
+ };
87
+ println!(
88
+ "{}",
89
+ serde_json::to_string(&serde_json::json!({
90
+ "event": "ready",
91
+ "pairPayload": payload,
92
+ "alpn": String::from_utf8_lossy(COMPANION_ALPN),
93
+ }))?
94
+ );
95
+
96
+ let client = reqwest::Client::builder()
97
+ .redirect(reqwest::redirect::Policy::none())
98
+ .timeout(Duration::from_secs(60))
99
+ .build()
100
+ .context("building HTTP client")?;
101
+ let shutdown = wait_for_shutdown();
102
+ tokio::pin!(shutdown);
103
+
104
+ loop {
105
+ tokio::select! {
106
+ _ = &mut shutdown => {
107
+ break;
108
+ }
109
+ incoming = endpoint.accept() => {
110
+ let Some(connecting) = incoming else { break };
111
+ let client = client.clone();
112
+ let local_base_url = args.local_base_url.clone();
113
+ let expected_token = payload.token.clone();
114
+ tokio::spawn(async move {
115
+ match connecting.await {
116
+ Ok(conn) => {
117
+ let conn_id = conn.stable_id();
118
+ info!(conn = conn_id, node_id = %conn.remote_id(), "forge iroh connection accepted");
119
+ while let Ok((send, recv)) = conn.accept_bi().await {
120
+ let client = client.clone();
121
+ let local_base_url = local_base_url.clone();
122
+ let expected_token = expected_token.clone();
123
+ tokio::spawn(async move {
124
+ if let Err(error) = handle_stream(send, recv, client, &local_base_url, &expected_token).await {
125
+ warn!("forge iroh stream ended: {error:#}");
126
+ }
127
+ });
128
+ }
129
+ }
130
+ Err(error) => warn!("incoming iroh connection failed: {error:#}"),
131
+ }
132
+ });
133
+ }
134
+ }
135
+ }
136
+ endpoint.close().await;
137
+ Ok(())
138
+ }
139
+
140
+ async fn run_probe(args: ProbeArgs) -> anyhow::Result<()> {
141
+ let endpoint = Endpoint::builder(presets::N0)
142
+ .secret_key(SecretKey::generate())
143
+ .alpns(vec![COMPANION_ALPN.to_vec()])
144
+ .bind()
145
+ .await
146
+ .context("binding probe endpoint")?;
147
+ let result = run_probe_with_endpoint(&endpoint, args).await;
148
+ endpoint.close().await;
149
+ result
150
+ }
151
+
152
+ async fn run_probe_with_endpoint(endpoint: &Endpoint, args: ProbeArgs) -> anyhow::Result<()> {
153
+ let node_id = args
154
+ .node_id
155
+ .parse()
156
+ .with_context(|| format!("parsing node id {}", args.node_id))?;
157
+ let mut addr = iroh::EndpointAddr::new(node_id);
158
+ if let Some(relay) = args.relay {
159
+ addr = addr.with_relay_url(
160
+ relay
161
+ .parse()
162
+ .with_context(|| format!("parsing relay URL {relay}"))?,
163
+ );
164
+ }
165
+ let conn = endpoint
166
+ .connect(addr, COMPANION_ALPN)
167
+ .await
168
+ .context("connecting over Forge Iroh bridge")?;
169
+ let (mut send, mut recv) = conn.open_bi().await.context("opening stream")?;
170
+ write_json_frame(
171
+ &mut send,
172
+ &BridgeRequest::Connect {
173
+ v: PROTOCOL_VERSION,
174
+ token: args.token,
175
+ agent: FORGE_AGENT_NAME.to_string(),
176
+ },
177
+ )
178
+ .await?;
179
+ let ack: BridgeResponse = read_json_frame(&mut recv).await?;
180
+ if !ack.ok {
181
+ anyhow::bail!("connect rejected: {}", ack.error.unwrap_or_default());
182
+ }
183
+ write_json_frame(
184
+ &mut send,
185
+ &ForgeHttpRequest {
186
+ v: PROTOCOL_VERSION,
187
+ method: args.method,
188
+ path: args.path,
189
+ headers: vec![],
190
+ body_base64: None,
191
+ },
192
+ )
193
+ .await?;
194
+ let response: ForgeHttpResponse = read_json_frame(&mut recv).await?;
195
+ println!("{}", serde_json::to_string_pretty(&response)?);
196
+ conn.close(iroh::endpoint::VarInt::from_u32(0), b"probe complete");
197
+ Ok(())
198
+ }
199
+
200
+ async fn handle_stream(
201
+ mut send: SendStream,
202
+ mut recv: RecvStream,
203
+ client: reqwest::Client,
204
+ local_base_url: &str,
205
+ expected_token: &str,
206
+ ) -> anyhow::Result<()> {
207
+ let request: BridgeRequest = read_json_frame(&mut recv).await?;
208
+ validate_bridge_request(&request, expected_token)?;
209
+ match request {
210
+ BridgeRequest::ListAgents { .. } => {
211
+ write_json_frame(&mut send, &BridgeResponse::agents()).await?;
212
+ Ok(())
213
+ }
214
+ BridgeRequest::Connect { agent, .. } if agent == FORGE_AGENT_NAME => {
215
+ write_json_frame(&mut send, &BridgeResponse::ok()).await?;
216
+ let request: ForgeHttpRequest = read_json_frame(&mut recv).await?;
217
+ let response = proxy_http_request(client, local_base_url, request).await?;
218
+ write_json_frame(&mut send, &response).await?;
219
+ let _ = send.finish();
220
+ Ok(())
221
+ }
222
+ BridgeRequest::Connect { agent, .. } => {
223
+ let message = format!("agent `{agent}` is disabled or unknown");
224
+ write_json_frame(&mut send, &BridgeResponse::error(&message)).await?;
225
+ Err(anyhow!(message))
226
+ }
227
+ }
228
+ }
229
+
230
+ fn validate_bridge_request(request: &BridgeRequest, expected_token: &str) -> anyhow::Result<()> {
231
+ if request.version() != PROTOCOL_VERSION {
232
+ anyhow::bail!(
233
+ "protocol mismatch: client={} host={}",
234
+ request.version(),
235
+ PROTOCOL_VERSION
236
+ );
237
+ }
238
+ if request.token() != expected_token {
239
+ anyhow::bail!("invalid token");
240
+ }
241
+ Ok(())
242
+ }
243
+
244
+ async fn proxy_http_request(
245
+ client: reqwest::Client,
246
+ local_base_url: &str,
247
+ request: ForgeHttpRequest,
248
+ ) -> anyhow::Result<ForgeHttpResponse> {
249
+ if request.v != PROTOCOL_VERSION {
250
+ anyhow::bail!(
251
+ "HTTP protocol mismatch: client={} host={}",
252
+ request.v,
253
+ PROTOCOL_VERSION
254
+ );
255
+ }
256
+ let url = build_proxy_url(local_base_url, &request.path)?;
257
+ let method = request.method.parse().context("parsing HTTP method")?;
258
+ let mut builder = client.request(method, url);
259
+ for header in request.headers {
260
+ let lower = header.name.to_ascii_lowercase();
261
+ if matches!(
262
+ lower.as_str(),
263
+ "connection" | "host" | "content-length" | "transfer-encoding"
264
+ ) {
265
+ continue;
266
+ }
267
+ let name = HeaderName::from_bytes(header.name.as_bytes())
268
+ .with_context(|| format!("invalid header name {}", header.name))?;
269
+ let value = HeaderValue::from_str(&header.value)
270
+ .with_context(|| format!("invalid header value for {}", header.name))?;
271
+ builder = builder.header(name, value);
272
+ }
273
+ if let Some(body) = request.body_base64 {
274
+ let bytes = base64::engine::general_purpose::STANDARD
275
+ .decode(body)
276
+ .context("decoding request body")?;
277
+ builder = builder.body(bytes);
278
+ }
279
+ let response = builder.send().await.context("forwarding HTTP request")?;
280
+ let status = response.status().as_u16();
281
+ let headers = response
282
+ .headers()
283
+ .iter()
284
+ .filter_map(|(name, value)| {
285
+ value.to_str().ok().map(|value| HeaderPair {
286
+ name: name.as_str().to_string(),
287
+ value: value.to_string(),
288
+ })
289
+ })
290
+ .collect();
291
+ let body = response
292
+ .bytes()
293
+ .await
294
+ .context("reading HTTP response body")?;
295
+ Ok(ForgeHttpResponse {
296
+ v: PROTOCOL_VERSION,
297
+ status,
298
+ headers,
299
+ body_base64: Some(base64::engine::general_purpose::STANDARD.encode(body)),
300
+ })
301
+ }
302
+
303
+ fn build_proxy_url(local_base_url: &str, path: &str) -> anyhow::Result<String> {
304
+ if path.starts_with("http://") || path.starts_with("https://") {
305
+ anyhow::bail!("absolute proxy paths are not allowed");
306
+ }
307
+ if !path.starts_with('/') {
308
+ anyhow::bail!("proxy path must start with /");
309
+ }
310
+ Ok(format!("{}{}", local_base_url.trim_end_matches('/'), path))
311
+ }
312
+
313
+ async fn bind_endpoint(secret_key: SecretKey) -> anyhow::Result<Endpoint> {
314
+ let idle_timeout = IdleTimeout::try_from(Duration::from_secs(600))
315
+ .context("constructing Iroh idle timeout")?;
316
+ let transport = QuicTransportConfig::builder()
317
+ .max_idle_timeout(Some(idle_timeout))
318
+ .build();
319
+ let endpoint = Endpoint::builder(presets::N0)
320
+ .secret_key(secret_key)
321
+ .alpns(vec![COMPANION_ALPN.to_vec()])
322
+ .transport_config(transport)
323
+ .bind()
324
+ .await
325
+ .context("binding Iroh endpoint")?;
326
+ Ok(endpoint)
327
+ }
328
+
329
+ async fn wait_for_endpoint_route(endpoint: &Endpoint) {
330
+ let deadline = tokio::time::Instant::now() + Duration::from_secs(8);
331
+ while tokio::time::Instant::now() < deadline {
332
+ if endpoint_home_relay(endpoint).is_some() {
333
+ info!(addr = ?endpoint.addr(), "forge iroh endpoint route ready");
334
+ return;
335
+ }
336
+ tokio::time::sleep(Duration::from_millis(250)).await;
337
+ }
338
+ warn!("forge iroh endpoint did not report relay routing within timeout");
339
+ }
340
+
341
+ async fn load_or_create_secret_key(state_dir: &Path) -> anyhow::Result<SecretKey> {
342
+ let path = state_dir.join("host.key");
343
+ match tokio::fs::read_to_string(&path).await {
344
+ Ok(raw) => parse_secret_key(raw.trim())
345
+ .with_context(|| format!("parsing host key {}", path.display())),
346
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
347
+ let key = SecretKey::generate();
348
+ write_private_file(&path, hex::encode(key.to_bytes())).await?;
349
+ Ok(key)
350
+ }
351
+ Err(error) => Err(error).with_context(|| format!("reading {}", path.display())),
352
+ }
353
+ }
354
+
355
+ async fn load_or_create_token(state_dir: &Path) -> anyhow::Result<String> {
356
+ let path = state_dir.join("host.token");
357
+ match tokio::fs::read_to_string(&path).await {
358
+ Ok(raw) => Ok(raw.trim().to_string()),
359
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
360
+ let token = hex::encode(SecretKey::generate().to_bytes());
361
+ write_private_file(&path, token.clone()).await?;
362
+ Ok(token)
363
+ }
364
+ Err(error) => Err(error).with_context(|| format!("reading {}", path.display())),
365
+ }
366
+ }
367
+
368
+ fn parse_secret_key(raw: &str) -> anyhow::Result<SecretKey> {
369
+ let bytes = hex::decode(raw).context("host key must be 32 bytes of hex")?;
370
+ let bytes: [u8; 32] = bytes
371
+ .try_into()
372
+ .map_err(|_| anyhow!("host key must decode to exactly 32 bytes"))?;
373
+ Ok(SecretKey::from_bytes(&bytes))
374
+ }
375
+
376
+ async fn write_private_file(path: &Path, content: String) -> anyhow::Result<()> {
377
+ let tmp = path.with_extension("tmp");
378
+ tokio::fs::write(&tmp, format!("{content}\n"))
379
+ .await
380
+ .with_context(|| format!("writing {}", tmp.display()))?;
381
+ #[cfg(unix)]
382
+ {
383
+ use std::os::unix::fs::PermissionsExt;
384
+ std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))
385
+ .with_context(|| format!("chmod 0600 {}", tmp.display()))?;
386
+ }
387
+ tokio::fs::rename(&tmp, path)
388
+ .await
389
+ .with_context(|| format!("renaming {} -> {}", tmp.display(), path.display()))?;
390
+ Ok(())
391
+ }
392
+
393
+ async fn read_json_frame<T, R>(reader: &mut R) -> anyhow::Result<T>
394
+ where
395
+ T: serde::de::DeserializeOwned,
396
+ R: AsyncRead + Unpin,
397
+ {
398
+ let len = reader.read_u32().await.context("reading frame length")? as usize;
399
+ if len > MAX_FRAME_BYTES {
400
+ anyhow::bail!("frame too large: {len} bytes");
401
+ }
402
+ let mut buf = vec![0u8; len];
403
+ reader
404
+ .read_exact(&mut buf)
405
+ .await
406
+ .context("reading frame body")?;
407
+ serde_json::from_slice(&buf).context("decoding JSON frame")
408
+ }
409
+
410
+ async fn write_json_frame<T, W>(writer: &mut W, value: &T) -> anyhow::Result<()>
411
+ where
412
+ T: serde::Serialize,
413
+ W: AsyncWrite + Unpin,
414
+ {
415
+ let buf = serde_json::to_vec(value).context("encoding JSON frame")?;
416
+ if buf.len() > MAX_FRAME_BYTES {
417
+ anyhow::bail!("frame too large: {} bytes", buf.len());
418
+ }
419
+ writer
420
+ .write_u32(buf.len() as u32)
421
+ .await
422
+ .context("writing frame length")?;
423
+ writer.write_all(&buf).await.context("writing frame body")?;
424
+ writer.flush().await.context("flushing frame")?;
425
+ Ok(())
426
+ }
427
+
428
+ fn endpoint_home_relay(endpoint: &Endpoint) -> Option<String> {
429
+ endpoint
430
+ .addr()
431
+ .relay_urls()
432
+ .next()
433
+ .map(|url| url.to_string())
434
+ }
435
+
436
+ fn local_host_name() -> Option<String> {
437
+ hostname::get()
438
+ .ok()
439
+ .and_then(|name| name.into_string().ok())
440
+ .map(|name| name.trim().trim_end_matches('.').to_string())
441
+ .filter(|name| !name.is_empty())
442
+ }
443
+
444
+ async fn wait_for_shutdown() {
445
+ let _ = tokio::signal::ctrl_c().await;
446
+ }
447
+
448
+ fn init_logging() {
449
+ let _ = tracing_subscriber::fmt()
450
+ .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| {
451
+ EnvFilter::new(
452
+ "warn,forge_companion_iroh=info,iroh=error,noq=error,noq_udp=error,quinn=error",
453
+ )
454
+ }))
455
+ .with_writer(std::io::stderr)
456
+ .try_init();
457
+ }
458
+
459
+ #[cfg(test)]
460
+ mod tests {
461
+ use super::*;
462
+
463
+ #[test]
464
+ fn proxy_url_rejects_absolute_paths() {
465
+ assert!(build_proxy_url("http://127.0.0.1:4317", "https://example.com").is_err());
466
+ }
467
+
468
+ #[test]
469
+ fn proxy_url_requires_absolute_origin_path() {
470
+ assert!(build_proxy_url("http://127.0.0.1:4317", "api/v1/health").is_err());
471
+ }
472
+
473
+ #[test]
474
+ fn proxy_url_joins_local_origin() {
475
+ let url = build_proxy_url("http://127.0.0.1:4317/", "/api/v1/health").unwrap();
476
+ assert_eq!(url, "http://127.0.0.1:4317/api/v1/health");
477
+ }
478
+ }
@@ -0,0 +1,129 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ pub const PROTOCOL_VERSION: u32 = 1;
4
+ pub const COMPANION_ALPN: &[u8] = b"forge-companion/1";
5
+ pub const FORGE_AGENT_NAME: &str = "forge";
6
+
7
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8
+ pub struct PairPayload {
9
+ pub v: u32,
10
+ pub node_id: String,
11
+ pub token: String,
12
+ #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub host_name: Option<String>,
14
+ #[serde(skip_serializing_if = "Option::is_none")]
15
+ pub relay: Option<String>,
16
+ }
17
+
18
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19
+ #[serde(rename_all = "snake_case")]
20
+ pub enum AgentWire {
21
+ HttpJson,
22
+ }
23
+
24
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25
+ pub struct AgentInfo {
26
+ pub name: String,
27
+ pub display_name: String,
28
+ pub wire: AgentWire,
29
+ pub available: bool,
30
+ }
31
+
32
+ #[derive(Debug, Serialize, Deserialize)]
33
+ #[serde(tag = "op", rename_all = "snake_case")]
34
+ pub enum BridgeRequest {
35
+ ListAgents {
36
+ v: u32,
37
+ token: String,
38
+ },
39
+ Connect {
40
+ v: u32,
41
+ token: String,
42
+ agent: String,
43
+ },
44
+ }
45
+
46
+ impl BridgeRequest {
47
+ pub fn version(&self) -> u32 {
48
+ match self {
49
+ Self::ListAgents { v, .. } | Self::Connect { v, .. } => *v,
50
+ }
51
+ }
52
+
53
+ pub fn token(&self) -> &str {
54
+ match self {
55
+ Self::ListAgents { token, .. } | Self::Connect { token, .. } => token,
56
+ }
57
+ }
58
+ }
59
+
60
+ #[derive(Debug, Serialize, Deserialize)]
61
+ pub struct BridgeResponse {
62
+ pub v: u32,
63
+ pub ok: bool,
64
+ #[serde(default, skip_serializing_if = "Option::is_none")]
65
+ pub agents: Option<Vec<AgentInfo>>,
66
+ #[serde(default, skip_serializing_if = "Option::is_none")]
67
+ pub error: Option<String>,
68
+ }
69
+
70
+ impl BridgeResponse {
71
+ pub fn ok() -> Self {
72
+ Self {
73
+ v: PROTOCOL_VERSION,
74
+ ok: true,
75
+ agents: None,
76
+ error: None,
77
+ }
78
+ }
79
+
80
+ pub fn agents() -> Self {
81
+ Self {
82
+ v: PROTOCOL_VERSION,
83
+ ok: true,
84
+ agents: Some(vec![AgentInfo {
85
+ name: FORGE_AGENT_NAME.to_string(),
86
+ display_name: "Forge Companion".to_string(),
87
+ wire: AgentWire::HttpJson,
88
+ available: true,
89
+ }]),
90
+ error: None,
91
+ }
92
+ }
93
+
94
+ pub fn error(error: impl Into<String>) -> Self {
95
+ Self {
96
+ v: PROTOCOL_VERSION,
97
+ ok: false,
98
+ agents: None,
99
+ error: Some(error.into()),
100
+ }
101
+ }
102
+ }
103
+
104
+ #[derive(Debug, Serialize, Deserialize)]
105
+ pub struct ForgeHttpRequest {
106
+ pub v: u32,
107
+ pub method: String,
108
+ pub path: String,
109
+ #[serde(default)]
110
+ pub headers: Vec<HeaderPair>,
111
+ #[serde(default, skip_serializing_if = "Option::is_none")]
112
+ pub body_base64: Option<String>,
113
+ }
114
+
115
+ #[derive(Debug, Serialize, Deserialize)]
116
+ pub struct ForgeHttpResponse {
117
+ pub v: u32,
118
+ pub status: u16,
119
+ #[serde(default)]
120
+ pub headers: Vec<HeaderPair>,
121
+ #[serde(default, skip_serializing_if = "Option::is_none")]
122
+ pub body_base64: Option<String>,
123
+ }
124
+
125
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126
+ pub struct HeaderPair {
127
+ pub name: String,
128
+ pub value: String,
129
+ }
package/dist/index.html CHANGED
@@ -13,14 +13,14 @@
13
13
  />
14
14
  <link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
15
15
  <link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
16
- <script type="module" crossorigin src="/forge/assets/index-Dy7c-dRY.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-C0otBhgu.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/board-B1V3M__K.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-DUqM4jkt.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-CltSTItx.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/table-B-VrSFx8.js">
16
+ <script type="module" crossorigin src="/forge/assets/index-DoHjjze2.js"></script>
17
+ <link rel="modulepreload" crossorigin href="/forge/assets/vendor-BdrT2htV.js">
18
+ <link rel="modulepreload" crossorigin href="/forge/assets/board-DUwMfZvN.js">
19
+ <link rel="modulepreload" crossorigin href="/forge/assets/ui-CJPaElbj.js">
20
+ <link rel="modulepreload" crossorigin href="/forge/assets/motion-Crg3QyXD.js">
21
+ <link rel="modulepreload" crossorigin href="/forge/assets/table-CTlDeYRs.js">
22
22
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
23
- <link rel="stylesheet" crossorigin href="/forge/assets/index-BwKAPo98.css">
23
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-B9VOpR7r.css">
24
24
  </head>
25
25
  <body class="bg-canvas text-ink antialiased">
26
26
  <div id="root"></div>