edge-book 0.5.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/edge-book.js +333 -6
  2. package/package.json +4 -2
package/dist/edge-book.js CHANGED
@@ -54,6 +54,7 @@ var SIGNALS_FILE = "signals.json";
54
54
  var CAPABILITIES_FILE = "capabilities.json";
55
55
  var EPHEMERAL_FILE = "ephemeral-posts.json";
56
56
  var ANSWERS_FILE = "answers.json";
57
+ var RECEIVED_POSTS_FILE = "received-posts.json";
57
58
  var DEFAULT_SIGNAL_TTL_MS = 6 * 60 * 60 * 1e3;
58
59
  var DEFAULT_EPHEMERAL_TTL_MS = 24 * 60 * 60 * 1e3;
59
60
  function resolveHome(home) {
@@ -484,6 +485,7 @@ var EdgeBookStore = class {
484
485
  const contacts = await this.contacts();
485
486
  const contact = contacts[peerAgentId];
486
487
  if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
488
+ if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked", `Peer ${peerAgentId} is blocked`);
487
489
  if (contact.relationship_state !== "friend") {
488
490
  throw new EdgeBookError("not_friend", `Cannot send friend-gated message to relationship_state=${contact.relationship_state}`);
489
491
  }
@@ -672,6 +674,18 @@ var EdgeBookStore = class {
672
674
  const { signature, lifecycle: _lc, ...signedPayload } = sig;
673
675
  return verifyPayload(signedPayload, signature, pub);
674
676
  }
677
+ // Verify an Endorsement signature. Endorsements have no lifecycle field.
678
+ async verifyEndorsement(e) {
679
+ const identity = await this.identity();
680
+ let pub = identity.agent_id === e.endorser_agent_id ? identity.public_key_pem : void 0;
681
+ if (!pub) {
682
+ const c = (await this.contacts())[e.endorser_agent_id];
683
+ pub = c?.public_keys?.[0]?.public_key_pem;
684
+ }
685
+ if (!pub) return false;
686
+ const { signature, ...rest } = e;
687
+ return verifyPayload(rest, signature, pub);
688
+ }
675
689
  // Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
676
690
  async endorsements() {
677
691
  return readJson(this.file(ENDORSEMENTS_FILE), {});
@@ -1119,6 +1133,123 @@ var EdgeBookStore = class {
1119
1133
  throw new EdgeBookError("invalid_grant_signature", "Grant signature does not verify against the issuer key");
1120
1134
  }
1121
1135
  }
1136
+ // ─── Received posts (peer posts delivered via mailbox) ──────────────────────
1137
+ async receivedPosts() {
1138
+ return readJson(this.file(RECEIVED_POSTS_FILE), {});
1139
+ }
1140
+ async saveReceivedPosts(posts) {
1141
+ await writeJson(this.file(RECEIVED_POSTS_FILE), posts);
1142
+ }
1143
+ /** Grouped view for `/api/received` and the reader. */
1144
+ async receivedByCategory() {
1145
+ const all = await this.receivedPosts();
1146
+ const out = {
1147
+ signals: {},
1148
+ ephemeral: {},
1149
+ answers: {},
1150
+ endorsements: {}
1151
+ };
1152
+ for (const id of Object.keys(all)) {
1153
+ const p = all[id];
1154
+ if (p.post_type === "signal") out.signals[id] = p;
1155
+ else if (p.post_type === "answer") out.answers[id] = p;
1156
+ else if (p.post_type === "endorse") out.endorsements[id] = p;
1157
+ else out.ephemeral[id] = p;
1158
+ }
1159
+ return out;
1160
+ }
1161
+ async verifyReceivedPost(p) {
1162
+ switch (p.post_type) {
1163
+ case "signal":
1164
+ return this.verifySignal(p);
1165
+ case "answer":
1166
+ return this.verifyAnswer(p);
1167
+ case "endorse":
1168
+ return this.verifyEndorsement(p);
1169
+ case "query":
1170
+ case "share":
1171
+ case "coordinate":
1172
+ case "delegation_request":
1173
+ return this.verifyEphemeral(p);
1174
+ default:
1175
+ return false;
1176
+ }
1177
+ }
1178
+ receivedPostId(p) {
1179
+ return p.signal_id || p.post_id || p.answer_id || p.endorse_id || "";
1180
+ }
1181
+ receivedPostAuthor(p) {
1182
+ switch (p.post_type) {
1183
+ case "answer":
1184
+ return p.answerer_agent_id ?? "";
1185
+ case "endorse":
1186
+ return p.endorser_agent_id ?? "";
1187
+ case "signal":
1188
+ case "query":
1189
+ case "share":
1190
+ case "coordinate":
1191
+ case "delegation_request":
1192
+ return p.from_agent ?? "";
1193
+ default:
1194
+ return "";
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Receive a `post_publish` envelope from a friend.
1199
+ * Security order:
1200
+ * 1. verifyEnvelope (recipient/expiry/replay/sender-key + envelope sig)
1201
+ * 2. type guard: must be "post_publish"
1202
+ * 3. sender must be a known contact with relationship_state === "friend"
1203
+ * 4. inner post author must match envelope.from_agent_id
1204
+ * 5. inner post signature must verify
1205
+ * Only then store.
1206
+ */
1207
+ async receivePostPublish(envelope) {
1208
+ await this.verifyEnvelope(envelope);
1209
+ if (envelope.type !== "post_publish") {
1210
+ throw new EdgeBookError("wrong_message_type", "Expected post_publish envelope");
1211
+ }
1212
+ const contact = (await this.contacts())[envelope.from_agent_id];
1213
+ if (!contact || contact.relationship_state !== "friend") {
1214
+ throw new EdgeBookError("not_friend", "post_publish only accepted from friends");
1215
+ }
1216
+ const post = envelope.body.post;
1217
+ if (!post || !post.post_type) {
1218
+ throw new EdgeBookError("malformed_post_publish", "missing or malformed post in envelope body");
1219
+ }
1220
+ if (this.receivedPostAuthor(post) !== envelope.from_agent_id) {
1221
+ throw new EdgeBookError("author_mismatch", "post author does not match envelope sender");
1222
+ }
1223
+ const id = this.receivedPostId(post);
1224
+ if (!id) {
1225
+ throw new EdgeBookError("malformed_post_publish", "post missing id");
1226
+ }
1227
+ if (!await this.verifyReceivedPost(post)) {
1228
+ throw new EdgeBookError("invalid_signature", "inner post signature invalid");
1229
+ }
1230
+ const all = await this.receivedPosts();
1231
+ const key = envelope.from_agent_id + ":" + id;
1232
+ all[key] = post;
1233
+ await this.saveReceivedPosts(all);
1234
+ await this.audit("post.receive", envelope.from_agent_id, {
1235
+ post_type: post.post_type,
1236
+ id
1237
+ });
1238
+ return post;
1239
+ }
1240
+ /** Build a signed `post_publish` envelope wrapping any post type. */
1241
+ async signPostPublishEnvelope(input) {
1242
+ const identity = await this.identity();
1243
+ return this.signEnvelope({
1244
+ type: "post_publish",
1245
+ to_agent_id: input.to_agent_id,
1246
+ relationship_id: relationshipId(identity.agent_id, input.to_agent_id),
1247
+ capability_id: "",
1248
+ ref: "",
1249
+ transport: "direct",
1250
+ body: { post: input.post }
1251
+ });
1252
+ }
1122
1253
  async signEnvelope(input) {
1123
1254
  const identity = await this.identity();
1124
1255
  const unsigned = {
@@ -1168,6 +1299,10 @@ var EdgeBookStore = class {
1168
1299
  await this.receiveObjectRevoke(envelope);
1169
1300
  return;
1170
1301
  }
1302
+ if (envelope.type === "post_publish") {
1303
+ await this.receivePostPublish(envelope);
1304
+ return;
1305
+ }
1171
1306
  throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
1172
1307
  }
1173
1308
  async audit(action, peerAgentId, details) {
@@ -1423,6 +1558,7 @@ var EdgeBookStore = class {
1423
1558
  const contacts = await this.contacts();
1424
1559
  const contact = contacts[peerAgentId];
1425
1560
  if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1561
+ if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked", `Peer ${peerAgentId} is blocked`);
1426
1562
  if (contact.relationship_state !== "friend") throw new EdgeBookError("not_friend", `Feed denied for relationship_state=${contact.relationship_state}`);
1427
1563
  const grants = await this.grants();
1428
1564
  const grant = Object.values(grants).find(
@@ -1774,6 +1910,10 @@ async function handleOwnerApi(req, res, url, adapters) {
1774
1910
  sendJson(res, 200, { ephemeral: await store.ephemeralPosts() });
1775
1911
  return true;
1776
1912
  }
1913
+ if (req.method === "GET" && url.pathname === "/api/received") {
1914
+ sendJson(res, 200, await store.receivedByCategory());
1915
+ return true;
1916
+ }
1777
1917
  if (req.method === "GET" && url.pathname === "/api/answers") {
1778
1918
  sendJson(res, 200, { answers: await store.answers() });
1779
1919
  return true;
@@ -3597,6 +3737,138 @@ async function revokeOneSession(options) {
3597
3737
  }
3598
3738
  }
3599
3739
 
3740
+ // src/resolver.ts
3741
+ function nextAction(result, target) {
3742
+ switch (result.status) {
3743
+ case "resolved":
3744
+ return `friend request ${target} --deliver`;
3745
+ case "approval_required":
3746
+ case "candidates": {
3747
+ const first = result.candidates?.[0];
3748
+ return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
3749
+ }
3750
+ default:
3751
+ return "(no match \u2014 check the target)";
3752
+ }
3753
+ }
3754
+ var localContactProvider = {
3755
+ name: "local",
3756
+ priority: 100,
3757
+ async resolve(store, target) {
3758
+ const contacts = await store.contacts();
3759
+ const match = Object.values(contacts).find(
3760
+ (c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
3761
+ );
3762
+ if (!match) return null;
3763
+ return {
3764
+ kind: "card",
3765
+ agent_id: match.peer_agent_id,
3766
+ provenance: {
3767
+ source: "local",
3768
+ confidence: "high",
3769
+ display_name: match.display_name,
3770
+ reason: `known contact (relationship_state=${match.relationship_state})`
3771
+ }
3772
+ };
3773
+ }
3774
+ };
3775
+ function cardProvider(name, source, match) {
3776
+ return {
3777
+ name,
3778
+ priority: 90,
3779
+ async resolve(_store, target) {
3780
+ if (!match(target)) return null;
3781
+ const card = await loadCard(target);
3782
+ return {
3783
+ kind: "card",
3784
+ card,
3785
+ agent_id: card.agent_id,
3786
+ provenance: { source, confidence: "high", display_name: card.handle, reason: `${source} card verified` }
3787
+ };
3788
+ }
3789
+ };
3790
+ }
3791
+ var inviteProvider = cardProvider("invite", "invite", (t) => t.startsWith("edgebook:invite:"));
3792
+ var cardUrlProvider = cardProvider("card_url", "card_url", (t) => /^https?:\/\//.test(t));
3793
+ var cardFileProvider = cardProvider(
3794
+ "card_file",
3795
+ "card_file",
3796
+ (t) => t.startsWith("file://") || t.startsWith("/") || t.startsWith("./") || t.endsWith(".json")
3797
+ );
3798
+ var CANDIDATES_FILE = "candidates.json";
3799
+ function candidateKey(c) {
3800
+ return `${c.source}:${c.card_url ?? c.agent_id ?? ""}`;
3801
+ }
3802
+ async function readCandidates(store) {
3803
+ return readJson(store.file(CANDIDATES_FILE), {});
3804
+ }
3805
+ async function listCandidates(store) {
3806
+ return Object.values(await readCandidates(store));
3807
+ }
3808
+ async function getCandidate(store, id) {
3809
+ return (await readCandidates(store))[id];
3810
+ }
3811
+ async function writeCandidate(store, input) {
3812
+ const map = await readCandidates(store);
3813
+ const existing = Object.values(map).find((c) => candidateKey(c) === candidateKey(input));
3814
+ if (existing) return existing;
3815
+ const candidate = {
3816
+ candidate_id: randomId("cand"),
3817
+ approved: false,
3818
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
3819
+ ...input
3820
+ };
3821
+ map[candidate.candidate_id] = candidate;
3822
+ await writeJson(store.file(CANDIDATES_FILE), map);
3823
+ await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
3824
+ return candidate;
3825
+ }
3826
+ function defaultProviders(registryLookup = async () => null) {
3827
+ return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
3828
+ }
3829
+ async function resolveTarget(store, target, opts) {
3830
+ const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
3831
+ for (const provider of ordered) {
3832
+ const r = await provider.resolve(store, target);
3833
+ if (!r) continue;
3834
+ if (r.kind === "card") {
3835
+ const result2 = { status: "resolved", card: r.card, agent_id: r.agent_id, provenance: r.provenance, next_action: "" };
3836
+ result2.next_action = nextAction(result2, target);
3837
+ return result2;
3838
+ }
3839
+ const candidate = await writeCandidate(store, r.candidate);
3840
+ const result = { status: "approval_required", candidates: [candidate], provenance: r.provenance, next_action: "" };
3841
+ result.next_action = nextAction(result, target);
3842
+ return result;
3843
+ }
3844
+ return { status: "not_found", next_action: "(no match \u2014 check the target)" };
3845
+ }
3846
+ async function markCandidateApproved(store, candidateId, agentId) {
3847
+ const map = await readJson(store.file(CANDIDATES_FILE), {});
3848
+ if (!map[candidateId]) return;
3849
+ map[candidateId].approved = true;
3850
+ map[candidateId].agent_id = agentId;
3851
+ await writeJson(store.file(CANDIDATES_FILE), map);
3852
+ }
3853
+ function makeRegistryProvider(lookup) {
3854
+ return {
3855
+ name: "registry",
3856
+ priority: 50,
3857
+ async resolve(_store, target) {
3858
+ if (!target.startsWith("registry:")) return null;
3859
+ const cardTarget = await lookup(target);
3860
+ if (!cardTarget) return null;
3861
+ const card = await loadCard(cardTarget);
3862
+ return {
3863
+ kind: "card",
3864
+ card,
3865
+ agent_id: card.agent_id,
3866
+ provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry handle lookup" }
3867
+ };
3868
+ }
3869
+ };
3870
+ }
3871
+
3600
3872
  // src/cli.ts
3601
3873
  function usage() {
3602
3874
  return `Edge Book
@@ -3697,6 +3969,17 @@ async function deliverToPeer(store, envelope, peerAgentId) {
3697
3969
  }
3698
3970
  throw new EdgeBookError("no_route", `No direct or relay endpoint for ${peerAgentId}`);
3699
3971
  }
3972
+ async function broadcastPost(store, host, socketFactory2, post) {
3973
+ const contacts = await store.contacts();
3974
+ const friends = Object.values(contacts).filter((c) => c.relationship_state === "friend");
3975
+ let count = 0;
3976
+ for (const f of friends) {
3977
+ const envelope = await store.signPostPublishEnvelope({ to_agent_id: f.peer_agent_id, post });
3978
+ await deliverEnvelopeViaMailbox({ home: store.home, host, socketFactory: socketFactory2, envelope });
3979
+ count++;
3980
+ }
3981
+ return count;
3982
+ }
3700
3983
  function serverAddress(server) {
3701
3984
  const address = server.address();
3702
3985
  if (!address || typeof address === "string") return String(address);
@@ -3772,13 +4055,33 @@ share_owner_label: ${id.share_owner_label ? "true" : "false"} (${shared})`,
3772
4055
  return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id } };
3773
4056
  }
3774
4057
  }
4058
+ if (command === "resolve") {
4059
+ const target = requireArg(args.shift(), "target");
4060
+ const result = await resolveTarget(store, target, { providers: defaultProviders() });
4061
+ const label = result.agent_id ?? result.candidates?.[0]?.candidate_id ?? "";
4062
+ return { text: `${result.status} ${label}
4063
+ next: ${result.next_action}`, json: result };
4064
+ }
4065
+ if (command === "candidates") {
4066
+ const action = args.shift() || "list";
4067
+ if (action === "list") {
4068
+ const candidates = await listCandidates(store);
4069
+ const text = candidates.length ? candidates.map((c) => `${c.candidate_id} ${c.source} ${c.display_name} ${c.approved ? "[approved]" : ""}`).join("\n") : "No candidates.";
4070
+ return { text, json: { candidates } };
4071
+ }
4072
+ }
3775
4073
  if (command === "friend") {
3776
4074
  const action = args.shift();
3777
4075
  if (action === "request") {
3778
4076
  const deliver = takeBoolFlag(args, "--deliver");
3779
- const target = requireArg(args.shift(), "card-path-or-url");
3780
- const card = await loadCard(target);
4077
+ const target = requireArg(args.shift(), "card-path-url-or-candidate");
4078
+ const candidate = await getCandidate(store, target);
4079
+ if (candidate && !candidate.card_url) {
4080
+ throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot request");
4081
+ }
4082
+ const card = candidate ? await loadCard(candidate.card_url) : await loadCard(target);
3781
4083
  const envelope = await store.createFriendRequest(card);
4084
+ if (candidate) await markCandidateApproved(store, candidate.candidate_id, card.agent_id);
3782
4085
  if (deliver) {
3783
4086
  const direct = card.transports.find((entry) => entry.mode === "direct")?.endpoint;
3784
4087
  if (direct) return { text: await deliverToEndpoint(envelope, direct), json: envelope };
@@ -4007,22 +4310,34 @@ ${JSON.stringify(result, null, 2)}`, json: result };
4007
4310
  return { text: `Attestation ${id.attestation_id}`, json: id };
4008
4311
  }
4009
4312
  if (command === "endorse") {
4313
+ const deliver = takeBoolFlag(args, "--deliver");
4314
+ const hostUrl = parseHost(args, ctx);
4010
4315
  const subject = requireArg(args.shift(), "<subject-agent-id>");
4011
4316
  const evAtt = takeFlag(args, "--evidence-attestation");
4012
4317
  const evTask = takeFlag(args, "--evidence-task");
4013
- const id = await store.createEndorsement({
4318
+ const post = await store.createEndorsement({
4014
4319
  subject_agent_id: subject,
4015
4320
  parent: { uri: requireArg(takeFlag(args, "--parent-uri"), "--parent-uri"), hash: requireArg(takeFlag(args, "--parent-hash"), "--parent-hash") },
4016
4321
  ...evAtt ? { evidence_ref: { uri: `edgebook:attestation:${evAtt}`, hash: evAtt } } : {},
4017
4322
  ...evTask ? { evidence_task_id: evTask } : {},
4018
4323
  statement: requireArg(takeFlag(args, "--statement"), "--statement")
4019
4324
  });
4020
- return { text: `Endorsement ${id.endorse_id}`, json: id };
4325
+ if (deliver) {
4326
+ const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
4327
+ return { text: `Endorsement ${post.endorse_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
4328
+ }
4329
+ return { text: `Endorsement ${post.endorse_id}`, json: post };
4021
4330
  }
4022
4331
  if (command === "signal") {
4332
+ const deliver = takeBoolFlag(args, "--deliver");
4333
+ const hostUrl = parseHost(args, ctx);
4023
4334
  const ttl = takeFlag(args, "--ttl-ms");
4024
- const id = await store.createSignal({ body: requireArg(takeFlag(args, "--body"), "--body"), ttlMs: ttl ? Number(ttl) : void 0 });
4025
- return { text: `Signal ${id.signal_id}`, json: id };
4335
+ const post = await store.createSignal({ body: requireArg(takeFlag(args, "--body"), "--body"), ttlMs: ttl ? Number(ttl) : void 0 });
4336
+ if (deliver) {
4337
+ const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
4338
+ return { text: `Signal ${post.signal_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
4339
+ }
4340
+ return { text: `Signal ${post.signal_id}`, json: post };
4026
4341
  }
4027
4342
  if (command === "capability") {
4028
4343
  const action = args.shift() || "list";
@@ -4045,15 +4360,23 @@ ${JSON.stringify(result, null, 2)}`, json: result };
4045
4360
  throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
4046
4361
  }
4047
4362
  if (command === "query" || command === "share" || command === "coordinate" || command === "delegate") {
4363
+ const deliver = takeBoolFlag(args, "--deliver");
4364
+ const hostUrl = parseHost(args, ctx);
4048
4365
  const type = command === "delegate" ? "delegation_request" : command;
4049
4366
  const body = requireArg(takeFlag(args, "--body"), "--body");
4050
4367
  const to = takeFlag(args, "--to") || takeFlag(args, "--with");
4051
4368
  const ref = takeFlag(args, "--ref");
4052
4369
  const ttl = takeFlag(args, "--ttl-ms");
4053
4370
  const post = await store.createEphemeral(type, { body, subject_agent_id: to, ref, ttlMs: ttl ? Number(ttl) : void 0 });
4371
+ if (deliver) {
4372
+ const n = await broadcastPost(store, hostUrl, ctx.socketFactory, post);
4373
+ return { text: `${post.post_type} ${post.post_id} \u2014 delivered to ${n} friend(s)`, json: { post, delivered: n } };
4374
+ }
4054
4375
  return { text: `${post.post_type} ${post.post_id}`, json: post };
4055
4376
  }
4056
4377
  if (command === "answer") {
4378
+ const deliver = takeBoolFlag(args, "--deliver");
4379
+ const hostUrl = parseHost(args, ctx);
4057
4380
  const queryId = requireArg(args.shift(), "<query-id>");
4058
4381
  const ephemeral = await store.ephemeralPosts();
4059
4382
  const query = ephemeral[queryId];
@@ -4063,6 +4386,10 @@ ${JSON.stringify(result, null, 2)}`, json: result };
4063
4386
  parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
4064
4387
  body: requireArg(takeFlag(args, "--body"), "--body")
4065
4388
  });
4389
+ if (deliver) {
4390
+ const n = await broadcastPost(store, hostUrl, ctx.socketFactory, ans);
4391
+ return { text: `answer ${ans.answer_id} \u2014 delivered to ${n} friend(s)`, json: { post: ans, delivered: n } };
4392
+ }
4066
4393
  return { text: `answer ${ans.answer_id}`, json: ans };
4067
4394
  }
4068
4395
  if (command === "query-delete") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-book",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Run your own Edge Book agent and connect it to the hosted reader.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,9 @@
12
12
  "test": "node --test test/*.test.ts",
13
13
  "harness": "node bin/edge-book.js harness two-agent",
14
14
  "harness:e2e": "node scripts/convergence-e2e.ts",
15
- "prepublishOnly": "npm run build"
15
+ "prepublishOnly": "npm run build",
16
+ "smoke": "node scripts/smoke-2agent.ts",
17
+ "smoke:host": "node scripts/smoke-2agent.ts --host"
16
18
  },
17
19
  "openclaw": {
18
20
  "extensions": [