arc402-cli 0.3.0 → 0.3.2

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 (47) hide show
  1. package/dist/commands/accept.d.ts.map +1 -1
  2. package/dist/commands/accept.js +26 -1
  3. package/dist/commands/accept.js.map +1 -1
  4. package/dist/commands/agent.d.ts.map +1 -1
  5. package/dist/commands/agent.js +166 -0
  6. package/dist/commands/agent.js.map +1 -1
  7. package/dist/commands/arena-handshake.d.ts.map +1 -1
  8. package/dist/commands/arena-handshake.js +22 -0
  9. package/dist/commands/arena-handshake.js.map +1 -1
  10. package/dist/commands/arena.d.ts +3 -0
  11. package/dist/commands/arena.d.ts.map +1 -0
  12. package/dist/commands/arena.js +122 -0
  13. package/dist/commands/arena.js.map +1 -0
  14. package/dist/commands/deliver.d.ts.map +1 -1
  15. package/dist/commands/deliver.js +19 -0
  16. package/dist/commands/deliver.js.map +1 -1
  17. package/dist/commands/feed.d.ts +10 -0
  18. package/dist/commands/feed.d.ts.map +1 -0
  19. package/dist/commands/feed.js +203 -0
  20. package/dist/commands/feed.js.map +1 -0
  21. package/dist/commands/hire.d.ts.map +1 -1
  22. package/dist/commands/hire.js +29 -0
  23. package/dist/commands/hire.js.map +1 -1
  24. package/dist/commands/workroom.d.ts.map +1 -1
  25. package/dist/commands/workroom.js +37 -0
  26. package/dist/commands/workroom.js.map +1 -1
  27. package/dist/daemon/index.d.ts.map +1 -1
  28. package/dist/daemon/index.js +336 -1
  29. package/dist/daemon/index.js.map +1 -1
  30. package/dist/endpoint-notify.d.ts +18 -0
  31. package/dist/endpoint-notify.d.ts.map +1 -0
  32. package/dist/endpoint-notify.js +43 -0
  33. package/dist/endpoint-notify.js.map +1 -0
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/commands/accept.ts +31 -1
  38. package/src/commands/agent.ts +187 -0
  39. package/src/commands/arena-handshake.ts +33 -0
  40. package/src/commands/arena.ts +121 -0
  41. package/src/commands/deliver.ts +19 -0
  42. package/src/commands/feed.ts +228 -0
  43. package/src/commands/hire.ts +31 -1
  44. package/src/commands/workroom.ts +44 -0
  45. package/src/daemon/index.ts +347 -1
  46. package/src/endpoint-notify.ts +46 -0
  47. package/src/index.ts +4 -0
@@ -6,7 +6,9 @@ import { requireSigner } from "../client";
6
6
  import { hashFile, hashString } from "../utils/hash";
7
7
  import { parseDuration } from "../utils/time";
8
8
  import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
9
- import { SERVICE_AGREEMENT_ABI } from "../abis";
9
+ import { AGENT_REGISTRY_ABI, SERVICE_AGREEMENT_ABI } from "../abis";
10
+
11
+ const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
10
12
 
11
13
  const sessionManager = new SessionManager();
12
14
 
@@ -183,6 +185,34 @@ export function registerHireCommand(program: Command): void {
183
185
  agreementId = result.agreementId;
184
186
  }
185
187
 
188
+ // Notify provider's HTTP endpoint (non-blocking)
189
+ const hireRegistryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
190
+ try {
191
+ const hireProvider = new ethers.JsonRpcProvider(config.rpcUrl);
192
+ const hireRegistry = new ethers.Contract(hireRegistryAddress, AGENT_REGISTRY_ABI, hireProvider);
193
+ const agentData = await hireRegistry.getAgent(opts.agent);
194
+ const endpoint = agentData.endpoint as string;
195
+ if (endpoint) {
196
+ await fetch(`${endpoint}/hire`, {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({
200
+ agreementId: agreementId!.toString(),
201
+ from: address,
202
+ provider: opts.agent,
203
+ serviceType: opts.serviceType,
204
+ task: opts.task,
205
+ price: price.toString(),
206
+ token,
207
+ deadline: deadlineSeconds,
208
+ deliverablesHash,
209
+ }),
210
+ });
211
+ }
212
+ } catch (err) {
213
+ console.warn(`Warning: could not notify provider endpoint: ${err instanceof Error ? err.message : String(err)}`);
214
+ }
215
+
186
216
  if (opts.session) {
187
217
  sessionManager.setOnChainId(opts.session, agreementId!.toString());
188
218
  }
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import * as os from "os";
5
+ import * as http from "http";
5
6
  import { spawnSync, execSync } from "child_process";
6
7
  import {
7
8
  ARC402_DIR,
@@ -9,6 +10,45 @@ import {
9
10
  } from "../openshell-runtime";
10
11
  import { DAEMON_LOG, DAEMON_TOML } from "../daemon/config";
11
12
 
13
+ // ─── Daemon lifecycle notify ──────────────────────────────────────────────────
14
+
15
+ function notifyDaemonWorkroomStatus(
16
+ event: "entered" | "exited" | "job_started" | "job_completed",
17
+ agentAddress?: string,
18
+ jobId?: string,
19
+ port = 4402
20
+ ): void {
21
+ try {
22
+ // Try to read port from daemon config
23
+ let daemonPort = port;
24
+ if (fs.existsSync(DAEMON_TOML)) {
25
+ try {
26
+ const { loadDaemonConfig } = require("../daemon/config") as typeof import("../daemon/config");
27
+ const cfg = loadDaemonConfig();
28
+ daemonPort = cfg.relay?.listen_port ?? port;
29
+ } catch { /* use default */ }
30
+ }
31
+
32
+ const payload = JSON.stringify({
33
+ event,
34
+ agentAddress: agentAddress ?? "",
35
+ jobId,
36
+ timestamp: Date.now(),
37
+ });
38
+
39
+ const req = http.request({
40
+ hostname: "127.0.0.1",
41
+ port: daemonPort,
42
+ path: "/workroom/status",
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
45
+ });
46
+ req.on("error", () => { /* non-fatal */ });
47
+ req.write(payload);
48
+ req.end();
49
+ } catch { /* non-fatal */ }
50
+ }
51
+
12
52
  // ─── Constants ────────────────────────────────────────────────────────────────
13
53
 
14
54
  const WORKROOM_IMAGE = "arc402-workroom";
@@ -232,6 +272,8 @@ export function registerWorkroomCommands(program: Command): void {
232
272
  console.log(` Policy hash: ${getPolicyHash()}`);
233
273
  console.log(` Relay port: 4402`);
234
274
  console.log(` Logs: arc402 workroom logs`);
275
+ // Notify local daemon of workroom entry
276
+ notifyDaemonWorkroomStatus("entered");
235
277
  } else {
236
278
  console.error("Workroom started but exited immediately. Check logs:");
237
279
  console.error(" docker logs arc402-workroom");
@@ -249,6 +291,8 @@ export function registerWorkroomCommands(program: Command): void {
249
291
  return;
250
292
  }
251
293
  console.log("Stopping ARC-402 Workroom...");
294
+ // Notify daemon before stopping (daemon may be inside container)
295
+ notifyDaemonWorkroomStatus("exited");
252
296
  runCmd("docker", ["stop", WORKROOM_CONTAINER]);
253
297
  console.log("✓ Workroom stopped");
254
298
  });
@@ -10,6 +10,7 @@
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
12
  import * as net from "net";
13
+ import * as http from "http";
13
14
  import { ethers } from "ethers";
14
15
  import Database from "better-sqlite3";
15
16
 
@@ -549,6 +550,350 @@ export async function runDaemon(foreground = false): Promise<void> {
549
550
  // ── Start IPC socket ─────────────────────────────────────────────────────
550
551
  const ipcServer = startIpcServer(ipcCtx, log);
551
552
 
553
+ // ── Start HTTP relay server (public endpoint) ────────────────────────────
554
+ const httpPort = config.relay.listen_port ?? 4402;
555
+
556
+ const httpServer = http.createServer(async (req, res) => {
557
+ // CORS headers
558
+ res.setHeader("Access-Control-Allow-Origin", "*");
559
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
560
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
561
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
562
+
563
+ const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
564
+ const pathname = url.pathname;
565
+
566
+ // Health / info
567
+ if (pathname === "/" || pathname === "/health") {
568
+ const info = {
569
+ protocol: "arc-402",
570
+ version: "0.3.0",
571
+ agent: config.wallet.contract_address,
572
+ status: "online",
573
+ capabilities: config.policy.allowed_capabilities,
574
+ uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
575
+ };
576
+ res.writeHead(200, { "Content-Type": "application/json" });
577
+ res.end(JSON.stringify(info));
578
+ return;
579
+ }
580
+
581
+ // Agent info
582
+ if (pathname === "/agent") {
583
+ res.writeHead(200, { "Content-Type": "application/json" });
584
+ res.end(JSON.stringify({
585
+ wallet: config.wallet.contract_address,
586
+ owner: config.wallet.owner_address,
587
+ machineKey: machineKeyAddress,
588
+ chainId: config.network.chain_id,
589
+ bundlerMode: config.bundler.mode,
590
+ relay: config.relay.enabled,
591
+ }));
592
+ return;
593
+ }
594
+
595
+ // Receive hire proposal
596
+ if (pathname === "/hire" && req.method === "POST") {
597
+ let body = "";
598
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
599
+ req.on("end", async () => {
600
+ try {
601
+ const msg = JSON.parse(body) as Record<string, unknown>;
602
+
603
+ // Feed into the hire listener's message handler
604
+ const proposal = {
605
+ messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
606
+ hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
607
+ capability: String(msg.capability ?? ""),
608
+ priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
609
+ deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
610
+ specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
611
+ agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
612
+ signature: msg.signature ? String(msg.signature) : undefined,
613
+ };
614
+
615
+ // Dedup
616
+ const existing = db.getHireRequest(proposal.messageId);
617
+ if (existing) {
618
+ res.writeHead(200, { "Content-Type": "application/json" });
619
+ res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
620
+ return;
621
+ }
622
+
623
+ // Policy check
624
+ const { evaluatePolicy } = await import("./hire-listener");
625
+ const activeCount = db.countActiveHireRequests();
626
+ const policyResult = evaluatePolicy(proposal, config, activeCount);
627
+
628
+ if (!policyResult.allowed) {
629
+ db.insertHireRequest({
630
+ id: proposal.messageId,
631
+ agreement_id: proposal.agreementId ?? null,
632
+ hirer_address: proposal.hirerAddress,
633
+ capability: proposal.capability,
634
+ price_eth: proposal.priceEth,
635
+ deadline_unix: proposal.deadlineUnix,
636
+ spec_hash: proposal.specHash,
637
+ status: "rejected",
638
+ reject_reason: policyResult.reason ?? "policy_violation",
639
+ });
640
+ log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
641
+ res.writeHead(200, { "Content-Type": "application/json" });
642
+ res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
643
+ return;
644
+ }
645
+
646
+ const status = config.policy.auto_accept ? "accepted" : "pending_approval";
647
+ db.insertHireRequest({
648
+ id: proposal.messageId,
649
+ agreement_id: proposal.agreementId ?? null,
650
+ hirer_address: proposal.hirerAddress,
651
+ capability: proposal.capability,
652
+ price_eth: proposal.priceEth,
653
+ deadline_unix: proposal.deadlineUnix,
654
+ spec_hash: proposal.specHash,
655
+ status,
656
+ reject_reason: null,
657
+ });
658
+
659
+ log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
660
+
661
+ if (config.notifications.notify_on_hire_request) {
662
+ await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
663
+ }
664
+
665
+ res.writeHead(200, { "Content-Type": "application/json" });
666
+ res.end(JSON.stringify({ status, id: proposal.messageId }));
667
+ } catch (err) {
668
+ log({ event: "http_hire_error", error: String(err) });
669
+ res.writeHead(400, { "Content-Type": "application/json" });
670
+ res.end(JSON.stringify({ error: "invalid_request" }));
671
+ }
672
+ });
673
+ return;
674
+ }
675
+
676
+ // Handshake acknowledgment endpoint
677
+ if (pathname === "/handshake" && req.method === "POST") {
678
+ let body = "";
679
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
680
+ req.on("end", () => {
681
+ try {
682
+ const msg = JSON.parse(body);
683
+ log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
684
+ res.writeHead(200, { "Content-Type": "application/json" });
685
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
686
+ } catch {
687
+ res.writeHead(400, { "Content-Type": "application/json" });
688
+ res.end(JSON.stringify({ error: "invalid_request" }));
689
+ }
690
+ });
691
+ return;
692
+ }
693
+
694
+ // POST /hire/accepted — provider accepted, client notified
695
+ if (pathname === "/hire/accepted" && req.method === "POST") {
696
+ let body = "";
697
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
698
+ req.on("end", async () => {
699
+ try {
700
+ const msg = JSON.parse(body) as Record<string, unknown>;
701
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
702
+ const from = String(msg.from ?? "");
703
+ log({ event: "hire_accepted_inbound", agreementId, from });
704
+ if (config.notifications.notify_on_hire_accepted) {
705
+ await notifier.notifyHireAccepted(agreementId, agreementId);
706
+ }
707
+ res.writeHead(200, { "Content-Type": "application/json" });
708
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
709
+ } catch {
710
+ res.writeHead(400, { "Content-Type": "application/json" });
711
+ res.end(JSON.stringify({ error: "invalid_request" }));
712
+ }
713
+ });
714
+ return;
715
+ }
716
+
717
+ // POST /message — off-chain negotiation message
718
+ if (pathname === "/message" && req.method === "POST") {
719
+ let body = "";
720
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
721
+ req.on("end", () => {
722
+ try {
723
+ const msg = JSON.parse(body) as Record<string, unknown>;
724
+ const from = String(msg.from ?? "");
725
+ const to = String(msg.to ?? "");
726
+ const content = String(msg.content ?? "");
727
+ const timestamp = Number(msg.timestamp ?? Date.now());
728
+ log({ event: "message_received", from, to, timestamp, content_len: content.length });
729
+ res.writeHead(200, { "Content-Type": "application/json" });
730
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
731
+ } catch {
732
+ res.writeHead(400, { "Content-Type": "application/json" });
733
+ res.end(JSON.stringify({ error: "invalid_request" }));
734
+ }
735
+ });
736
+ return;
737
+ }
738
+
739
+ // POST /delivery — provider committed a deliverable
740
+ if (pathname === "/delivery" && req.method === "POST") {
741
+ let body = "";
742
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
743
+ req.on("end", async () => {
744
+ try {
745
+ const msg = JSON.parse(body) as Record<string, unknown>;
746
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
747
+ const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
748
+ const from = String(msg.from ?? "");
749
+ log({ event: "delivery_received", agreementId, deliverableHash, from });
750
+ // Update DB: mark delivered
751
+ const active = db.listActiveHireRequests();
752
+ const found = active.find(r => r.agreement_id === agreementId);
753
+ if (found) db.updateHireRequestStatus(found.id, "delivered");
754
+ if (config.notifications.notify_on_delivery) {
755
+ await notifier.notifyDelivery(agreementId, deliverableHash, "");
756
+ }
757
+ res.writeHead(200, { "Content-Type": "application/json" });
758
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
759
+ } catch {
760
+ res.writeHead(400, { "Content-Type": "application/json" });
761
+ res.end(JSON.stringify({ error: "invalid_request" }));
762
+ }
763
+ });
764
+ return;
765
+ }
766
+
767
+ // POST /delivery/accepted — client accepted delivery, payment releasing
768
+ if (pathname === "/delivery/accepted" && req.method === "POST") {
769
+ let body = "";
770
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
771
+ req.on("end", () => {
772
+ try {
773
+ const msg = JSON.parse(body) as Record<string, unknown>;
774
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
775
+ const from = String(msg.from ?? "");
776
+ log({ event: "delivery_accepted_inbound", agreementId, from });
777
+ // Update DB: mark complete
778
+ const all = db.listActiveHireRequests();
779
+ const found = all.find(r => r.agreement_id === agreementId);
780
+ if (found) db.updateHireRequestStatus(found.id, "complete");
781
+ res.writeHead(200, { "Content-Type": "application/json" });
782
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
783
+ } catch {
784
+ res.writeHead(400, { "Content-Type": "application/json" });
785
+ res.end(JSON.stringify({ error: "invalid_request" }));
786
+ }
787
+ });
788
+ return;
789
+ }
790
+
791
+ // POST /dispute — dispute raised against this agent
792
+ if (pathname === "/dispute" && req.method === "POST") {
793
+ let body = "";
794
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
795
+ req.on("end", async () => {
796
+ try {
797
+ const msg = JSON.parse(body) as Record<string, unknown>;
798
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
799
+ const reason = String(msg.reason ?? "");
800
+ const from = String(msg.from ?? "");
801
+ log({ event: "dispute_received", agreementId, reason, from });
802
+ if (config.notifications.notify_on_dispute) {
803
+ await notifier.notifyDispute(agreementId, from);
804
+ }
805
+ res.writeHead(200, { "Content-Type": "application/json" });
806
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
807
+ } catch {
808
+ res.writeHead(400, { "Content-Type": "application/json" });
809
+ res.end(JSON.stringify({ error: "invalid_request" }));
810
+ }
811
+ });
812
+ return;
813
+ }
814
+
815
+ // POST /dispute/resolved — dispute resolved by arbitrator
816
+ if (pathname === "/dispute/resolved" && req.method === "POST") {
817
+ let body = "";
818
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
819
+ req.on("end", () => {
820
+ try {
821
+ const msg = JSON.parse(body) as Record<string, unknown>;
822
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
823
+ const outcome = String(msg.outcome ?? "");
824
+ const from = String(msg.from ?? "");
825
+ log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
826
+ res.writeHead(200, { "Content-Type": "application/json" });
827
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
828
+ } catch {
829
+ res.writeHead(400, { "Content-Type": "application/json" });
830
+ res.end(JSON.stringify({ error: "invalid_request" }));
831
+ }
832
+ });
833
+ return;
834
+ }
835
+
836
+ // POST /workroom/status — workroom lifecycle events
837
+ if (pathname === "/workroom/status" && req.method === "POST") {
838
+ let body = "";
839
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
840
+ req.on("end", () => {
841
+ try {
842
+ const msg = JSON.parse(body) as Record<string, unknown>;
843
+ const event = String(msg.event ?? "");
844
+ const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
845
+ const jobId = msg.jobId ? String(msg.jobId) : undefined;
846
+ const timestamp = Number(msg.timestamp ?? Date.now());
847
+ log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
848
+ res.writeHead(200, { "Content-Type": "application/json" });
849
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
850
+ } catch {
851
+ res.writeHead(400, { "Content-Type": "application/json" });
852
+ res.end(JSON.stringify({ error: "invalid_request" }));
853
+ }
854
+ });
855
+ return;
856
+ }
857
+
858
+ // GET /capabilities — agent capabilities from config
859
+ if (pathname === "/capabilities" && req.method === "GET") {
860
+ res.writeHead(200, { "Content-Type": "application/json" });
861
+ res.end(JSON.stringify({
862
+ capabilities: config.policy.allowed_capabilities,
863
+ max_price_eth: config.policy.max_price_eth,
864
+ auto_accept: config.policy.auto_accept,
865
+ max_concurrent_agreements: config.relay.max_concurrent_agreements,
866
+ }));
867
+ return;
868
+ }
869
+
870
+ // GET /status — health with active agreement count
871
+ if (pathname === "/status" && req.method === "GET") {
872
+ const activeList = db.listActiveHireRequests();
873
+ const pendingList = db.listPendingHireRequests();
874
+ res.writeHead(200, { "Content-Type": "application/json" });
875
+ res.end(JSON.stringify({
876
+ protocol: "arc-402",
877
+ version: "0.3.0",
878
+ agent: config.wallet.contract_address,
879
+ status: "online",
880
+ uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
881
+ active_agreements: activeList.length,
882
+ pending_approval: pendingList.length,
883
+ capabilities: config.policy.allowed_capabilities,
884
+ }));
885
+ return;
886
+ }
887
+
888
+ // 404
889
+ res.writeHead(404, { "Content-Type": "application/json" });
890
+ res.end(JSON.stringify({ error: "not_found" }));
891
+ });
892
+
893
+ httpServer.listen(httpPort, "0.0.0.0", () => {
894
+ log({ event: "http_server_started", port: httpPort });
895
+ });
896
+
552
897
  // ── Step 12: Startup complete ────────────────────────────────────────────
553
898
  const subsystems = [];
554
899
  if (config.relay.enabled) subsystems.push("relay");
@@ -573,7 +918,8 @@ export async function runDaemon(foreground = false): Promise<void> {
573
918
  clearInterval(timeoutInterval);
574
919
  clearInterval(balanceInterval);
575
920
 
576
- // Close IPC
921
+ // Close HTTP + IPC
922
+ httpServer.close();
577
923
  ipcServer.close();
578
924
  if (fs.existsSync(DAEMON_SOCK)) fs.unlinkSync(DAEMON_SOCK);
579
925
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared endpoint-notify helper for CLI commands.
3
+ * Resolves an agent's registered HTTP endpoint from AgentRegistry
4
+ * and POSTs lifecycle events after onchain transactions.
5
+ */
6
+ import { ethers } from "ethers";
7
+ import { AGENT_REGISTRY_ABI } from "./abis";
8
+
9
+ export const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
10
+
11
+ /**
12
+ * Reads an agent's public HTTP endpoint from AgentRegistry.
13
+ * Returns empty string if not registered or no endpoint.
14
+ */
15
+ export async function resolveAgentEndpoint(
16
+ address: string,
17
+ provider: ethers.Provider,
18
+ registryAddress = DEFAULT_REGISTRY_ADDRESS
19
+ ): Promise<string> {
20
+ const registry = new ethers.Contract(registryAddress, AGENT_REGISTRY_ABI, provider);
21
+ const agentData = await registry.getAgent(address);
22
+ return (agentData.endpoint as string) ?? "";
23
+ }
24
+
25
+ /**
26
+ * POSTs JSON payload to {endpoint}{path}. Returns true on success.
27
+ * Never throws — logs a warning on failure.
28
+ */
29
+ export async function notifyAgent(
30
+ endpoint: string,
31
+ path: string,
32
+ payload: Record<string, unknown>
33
+ ): Promise<boolean> {
34
+ if (!endpoint) return false;
35
+ try {
36
+ const res = await fetch(`${endpoint}${path}`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify(payload),
40
+ });
41
+ return res.ok;
42
+ } catch (err) {
43
+ console.warn(`Warning: endpoint notify failed (${endpoint}${path}): ${err instanceof Error ? err.message : String(err)}`);
44
+ return false;
45
+ }
46
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,8 @@ import { registerContractInteractionCommands } from "./commands/contract-interac
30
30
  import { registerWatchtowerCommands } from "./commands/watchtower";
31
31
  import { registerColdStartCommands } from "./commands/coldstart";
32
32
  import { registerMigrateCommands } from "./commands/migrate";
33
+ import { registerFeedCommand } from "./commands/feed";
34
+ import { registerArenaCommands } from "./commands/arena";
33
35
  import reputation from "./commands/reputation.js";
34
36
  import policy from "./commands/policy.js";
35
37
 
@@ -70,6 +72,8 @@ registerContractInteractionCommands(program);
70
72
  registerWatchtowerCommands(program);
71
73
  registerColdStartCommands(program);
72
74
  registerMigrateCommands(program);
75
+ registerFeedCommand(program);
76
+ registerArenaCommands(program);
73
77
  program.addCommand(reputation);
74
78
  program.addCommand(policy);
75
79
  program.parse(process.argv);