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
@@ -1,16 +1,32 @@
1
1
  import { Command } from "commander";
2
2
  import { ServiceAgreementClient } from "@arc402/sdk";
3
+ import { ethers } from "ethers";
3
4
  import { loadConfig } from "../config";
4
5
  import { requireSigner } from "../client";
5
6
  import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
6
7
  import { SERVICE_AGREEMENT_ABI } from "../abis";
8
+ import { resolveAgentEndpoint, notifyAgent, DEFAULT_REGISTRY_ADDRESS } from "../endpoint-notify";
7
9
 
8
10
  export function registerAcceptCommand(program: Command): void {
9
11
  program.command("accept <id>").description("Provider accepts a proposed agreement").action(async (id) => {
10
12
  const config = loadConfig();
11
13
  if (!config.serviceAgreementAddress) throw new Error("serviceAgreementAddress missing in config");
12
- const { signer } = await requireSigner(config);
14
+ const { signer, address: signerAddress } = await requireSigner(config);
13
15
  printSenderInfo(config);
16
+
17
+ // Read agreement to get client address for endpoint notification
18
+ let clientAddress = "";
19
+ try {
20
+ const prefProvider = new ethers.JsonRpcProvider(config.rpcUrl);
21
+ const saContract = new ethers.Contract(
22
+ config.serviceAgreementAddress,
23
+ ["function getAgreement(uint256 id) external view returns (tuple(uint256 id, address client, address provider, string serviceType, string description, uint256 price, address token, uint256 deadline, bytes32 deliverablesHash, uint8 status, uint256 createdAt, uint256 resolvedAt, uint256 verifyWindowEnd, bytes32 committedHash))"],
24
+ prefProvider
25
+ );
26
+ const ag = await saContract.getAgreement(BigInt(id));
27
+ clientAddress = String(ag.client ?? "");
28
+ } catch { /* non-fatal */ }
29
+
14
30
  if (config.walletContractAddress) {
15
31
  await executeContractWriteViaWallet(
16
32
  config.walletContractAddress, signer, config.serviceAgreementAddress,
@@ -21,5 +37,19 @@ export function registerAcceptCommand(program: Command): void {
21
37
  await client.accept(BigInt(id));
22
38
  }
23
39
  console.log(`accepted ${id}`);
40
+
41
+ // Notify client's HTTP endpoint (non-blocking)
42
+ if (clientAddress) {
43
+ try {
44
+ const notifyProvider = new ethers.JsonRpcProvider(config.rpcUrl);
45
+ const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
46
+ const endpoint = await resolveAgentEndpoint(clientAddress, notifyProvider, registryAddress);
47
+ const payload = { agreementId: id, from: signerAddress };
48
+ await notifyAgent(endpoint, "/hire/accepted", payload);
49
+ await notifyAgent(endpoint, "/delivery/accepted", payload);
50
+ } catch (err) {
51
+ console.warn(`Warning: could not notify client endpoint: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ }
24
54
  });
25
55
  }
@@ -413,6 +413,193 @@ export function registerAgentCommands(program: Command): void {
413
413
  if (!address) throw new Error("No wallet configured");
414
414
  await program.parseAsync([process.argv[0], process.argv[1], "agent", "info", address], { from: "user" });
415
415
  });
416
+
417
+ // ─── profile (subgraph view) ─────────────────────────────────────────────
418
+
419
+ agent
420
+ .command("profile <address>")
421
+ .description("Show detailed agent profile from the Arena subgraph")
422
+ .option("--json", "Output as JSON")
423
+ .action(async (address: string, opts: { json?: boolean }) => {
424
+ const normalizedAddr = address.toLowerCase();
425
+ try {
426
+ const data = await agentSubgraphQuery(`{
427
+ agent(id: "${normalizedAddr}") {
428
+ id name serviceType endpoint active
429
+ trustScore { globalScore }
430
+ capabilities(where: { active: true }) { capability active }
431
+ handshakesSent(first: 100, orderBy: timestamp, orderDirection: desc) {
432
+ id to { id name } hsType note timestamp
433
+ }
434
+ handshakesReceived(first: 100, orderBy: timestamp, orderDirection: desc) {
435
+ id from { id name } hsType note timestamp
436
+ }
437
+ }
438
+ clientAgreements: agreements(where: { client: "${normalizedAddr}", state: 1 }, first: 10) {
439
+ id serviceType price state
440
+ }
441
+ providerAgreements: agreements(where: { provider: "${normalizedAddr}", state: 1 }, first: 10) {
442
+ id serviceType price state
443
+ }
444
+ vouchedFor: vouches(where: { voucher: "${normalizedAddr}", active: true }, first: 100) {
445
+ id newAgent { id name } stakeAmount
446
+ }
447
+ vouchedBy: vouches(where: { newAgent: "${normalizedAddr}", active: true }, first: 100) {
448
+ id voucher { id name } stakeAmount
449
+ }
450
+ }`);
451
+
452
+ const agentData = data["agent"] as Record<string, unknown> | null;
453
+
454
+ if (!agentData) {
455
+ if (opts.json) {
456
+ console.log(JSON.stringify({ error: "Agent not registered", address }));
457
+ } else {
458
+ console.log(chalk.red(`Agent not registered: ${address}`));
459
+ }
460
+ process.exit(1);
461
+ }
462
+
463
+ if (opts.json) {
464
+ console.log(JSON.stringify(data, null, 2));
465
+ return;
466
+ }
467
+
468
+ const sent = (agentData["handshakesSent"] as Record<string, unknown>[]) ?? [];
469
+ const received = (agentData["handshakesReceived"] as Record<string, unknown>[]) ?? [];
470
+ const sentToIds = new Set(sent.map((h) => (h["to"] as Record<string, string>)["id"]));
471
+ const receivedFromIds = new Set(received.map((h) => (h["from"] as Record<string, string>)["id"]));
472
+ const mutual = [...sentToIds].filter((id) => receivedFromIds.has(id)).length;
473
+
474
+ const trustScore =
475
+ ((agentData["trustScore"] as Record<string, unknown> | null)?.["globalScore"] as number | undefined) ?? 0;
476
+ const caps = (agentData["capabilities"] as Record<string, unknown>[]) ?? [];
477
+
478
+ const allAgreements = [
479
+ ...((data["clientAgreements"] as unknown[]) ?? []),
480
+ ...((data["providerAgreements"] as unknown[]) ?? []),
481
+ ] as Record<string, unknown>[];
482
+
483
+ const hsTypeLabels: Record<number, string> = {
484
+ 0: "Respected",
485
+ 1: "Curious",
486
+ 2: "Endorsed",
487
+ 3: "Thanked",
488
+ 4: "Collaborated",
489
+ 5: "Challenged",
490
+ 6: "Referred",
491
+ 7: "Hello",
492
+ };
493
+
494
+ const recentActivity = [
495
+ ...sent.slice(0, 3).map((h) => {
496
+ const to = h["to"] as Record<string, string>;
497
+ const label = hsTypeLabels[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
498
+ const note = h["note"] ? ` — "${h["note"]}"` : "";
499
+ return {
500
+ ts: Number(h["timestamp"]),
501
+ line: ` [${profileDate(Number(h["timestamp"]))}] ${label} ${to["name"] || shortAddress(to["id"])}${note}`,
502
+ };
503
+ }),
504
+ ...received.slice(0, 3).map((h) => {
505
+ const from = h["from"] as Record<string, string>;
506
+ const label = hsTypeLabels[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
507
+ const note = h["note"] ? ` — "${h["note"]}"` : "";
508
+ return {
509
+ ts: Number(h["timestamp"]),
510
+ line: ` [${profileDate(Number(h["timestamp"]))}] Received ${label} from ${from["name"] || shortAddress(from["id"])}${note}`,
511
+ };
512
+ }),
513
+ ]
514
+ .sort((a, b) => b.ts - a.ts)
515
+ .slice(0, 5);
516
+
517
+ const vouchedFor = (data["vouchedFor"] as unknown[]) ?? [];
518
+ const vouchedBy = (data["vouchedBy"] as unknown[]) ?? [];
519
+
520
+ const line = "═".repeat(43);
521
+ console.log(chalk.bold(line));
522
+ console.log(` ${chalk.bold((agentData["name"] as string) || "(unnamed)")}`);
523
+ console.log(` ${agentData["id"]}`);
524
+ console.log(chalk.bold(line));
525
+ console.log();
526
+ console.log(` Service Type: ${agentData["serviceType"]}`);
527
+ console.log(` Endpoint: ${agentData["endpoint"] || chalk.dim("(none)")}`);
528
+ console.log(
529
+ ` Status: ${agentData["active"] ? chalk.green("✅ Active") : chalk.red("❌ Inactive")}`,
530
+ );
531
+ console.log(` Trust Score: ${trustScore}`);
532
+ console.log();
533
+
534
+ if (caps.length > 0) {
535
+ console.log(" Capabilities:");
536
+ for (const cap of caps) {
537
+ console.log(` ✓ ${(cap as Record<string, unknown>)["capability"]}`);
538
+ }
539
+ console.log();
540
+ }
541
+
542
+ console.log(
543
+ ` Handshakes: ${sent.length} sent • ${received.length} received • ${mutual} mutual connections`,
544
+ );
545
+ console.log();
546
+
547
+ if (recentActivity.length > 0) {
548
+ console.log(" Recent Activity:");
549
+ for (const a of recentActivity) {
550
+ console.log(a.line);
551
+ }
552
+ console.log();
553
+ }
554
+
555
+ console.log(" Active Agreements:");
556
+ if (allAgreements.length === 0) {
557
+ console.log(" None");
558
+ } else {
559
+ for (const a of allAgreements.slice(0, 5)) {
560
+ console.log(` #${(a["id"] as string).slice(0, 8)} ${a["serviceType"]}`);
561
+ }
562
+ }
563
+ console.log();
564
+
565
+ console.log(" Vouches:");
566
+ console.log(` Vouched for: ${vouchedFor.length} agents`);
567
+ console.log(` Vouched by: ${vouchedBy.length} agents`);
568
+ } catch (err) {
569
+ const msg = err instanceof Error ? err.message : String(err);
570
+ if (opts.json) {
571
+ console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
572
+ } else {
573
+ console.error(chalk.red(`Subgraph unavailable: ${msg}`));
574
+ }
575
+ process.exit(1);
576
+ }
577
+ });
578
+ }
579
+
580
+ // ─── subgraph helpers (used by agent profile) ────────────────────────────────
581
+
582
+ const AGENT_SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
583
+
584
+ async function agentSubgraphQuery(query: string): Promise<Record<string, unknown>> {
585
+ const res = await fetch(AGENT_SUBGRAPH_URL, {
586
+ method: "POST",
587
+ headers: { "Content-Type": "application/json" },
588
+ body: JSON.stringify({ query }),
589
+ });
590
+ if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
591
+ const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
592
+ if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
593
+ return json.data ?? {};
594
+ }
595
+
596
+ function shortAddress(addr: string): string {
597
+ return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
598
+ }
599
+
600
+ function profileDate(ts: number): string {
601
+ const d = new Date(ts * 1000);
602
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
416
603
  }
417
604
 
418
605
  // ─── subdomain claim ──────────────────────────────────────────────────────────
@@ -2,6 +2,26 @@ import { Command } from "commander";
2
2
  import { ethers } from "ethers";
3
3
  import { loadConfig, getUsdcAddress } from "../config";
4
4
  import { requireSigner } from "../client";
5
+ import { AGENT_REGISTRY_ABI } from "../abis";
6
+
7
+ const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
8
+
9
+ async function pingHandshakeEndpoint(
10
+ agentAddress: string,
11
+ payload: Record<string, unknown>,
12
+ registryAddress: string,
13
+ provider: ethers.Provider
14
+ ): Promise<void> {
15
+ const registry = new ethers.Contract(registryAddress, AGENT_REGISTRY_ABI, provider);
16
+ const agentData = await registry.getAgent(agentAddress);
17
+ const endpoint = agentData.endpoint as string;
18
+ if (!endpoint) return;
19
+ await fetch(`${endpoint}/handshake`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(payload),
23
+ });
24
+ }
5
25
 
6
26
  // ─── Handshake Contract ABI (from Handshake.sol) ─────────────────────────────
7
27
 
@@ -114,6 +134,19 @@ export function registerArenaHandshakeCommands(program: Command): void {
114
134
  tx = await handshake.sendHandshake(agentAddress, hsType, opts.note, { value });
115
135
  }
116
136
 
137
+ // Notify recipient's HTTP endpoint (non-blocking)
138
+ const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
139
+ try {
140
+ await pingHandshakeEndpoint(
141
+ agentAddress,
142
+ { from: myAddress, type: opts.type, note: opts.note, txHash: tx.hash },
143
+ registryAddress,
144
+ provider
145
+ );
146
+ } catch (err) {
147
+ console.warn(`Warning: could not notify recipient endpoint: ${err instanceof Error ? err.message : String(err)}`);
148
+ }
149
+
117
150
  if (opts.json) {
118
151
  console.log(JSON.stringify({ tx: tx.hash, from: myAddress, to: agentAddress, type: opts.type, note: opts.note }));
119
152
  } else {
@@ -0,0 +1,121 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { runFeed, FeedOptions } from "./feed";
4
+
5
+ const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
6
+
7
+ async function gql(query: string): Promise<Record<string, unknown>> {
8
+ const res = await fetch(SUBGRAPH_URL, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify({ query }),
12
+ });
13
+ if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
14
+ const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
15
+ if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
16
+ return json.data ?? {};
17
+ }
18
+
19
+ export function registerArenaCommands(program: Command): void {
20
+ const arena = program.command("arena").description("Arena network commands");
21
+
22
+ // ─── arena stats ───────────────────────────────────────────────────────────
23
+
24
+ arena
25
+ .command("stats")
26
+ .description("Show Arena network statistics")
27
+ .option("--json", "Output as JSON")
28
+ .action(async (opts) => {
29
+ try {
30
+ const data = await gql(`{
31
+ protocolStats(id: "global") {
32
+ totalAgents
33
+ totalWallets
34
+ totalAgreements
35
+ totalHandshakes
36
+ totalConnections
37
+ totalVouches
38
+ totalCapabilityClaims
39
+ }
40
+ }`);
41
+
42
+ const stats = data["protocolStats"] as Record<string, unknown> | null;
43
+ if (!stats) {
44
+ const msg = "No stats available — subgraph may still be syncing.";
45
+ if (opts.json) {
46
+ console.log(JSON.stringify({ error: msg }));
47
+ } else {
48
+ console.error(chalk.red(msg));
49
+ }
50
+ process.exit(1);
51
+ }
52
+
53
+ // Try to count active agreements separately — non-fatal if it fails
54
+ let activeAgreements = 0;
55
+ try {
56
+ const agData = await gql(`{
57
+ proposals: agreements(where: { state: 0 }, first: 1000) { id }
58
+ accepted: agreements(where: { state: 1 }, first: 1000) { id }
59
+ }`);
60
+ activeAgreements =
61
+ ((agData["proposals"] as unknown[]) ?? []).length +
62
+ ((agData["accepted"] as unknown[]) ?? []).length;
63
+ } catch {
64
+ // ignore — active count just stays 0
65
+ }
66
+
67
+ if (opts.json) {
68
+ console.log(JSON.stringify({ ...stats, activeAgreements }, null, 2));
69
+ return;
70
+ }
71
+
72
+ const pad = (v: unknown) => String(v ?? 0).padStart(6);
73
+ console.log(chalk.bold("╔══════════════════════════════════════╗"));
74
+ console.log(chalk.bold("║ ARC Arena — Network Stats ║"));
75
+ console.log(chalk.bold("╚══════════════════════════════════════╝"));
76
+ console.log();
77
+ console.log(` Agents ${pad(stats["totalAgents"])} registered`);
78
+ console.log(` Wallets ${pad(stats["totalWallets"])} deployed`);
79
+ console.log(
80
+ ` Agreements ${pad(stats["totalAgreements"])} total (${activeAgreements} active)`,
81
+ );
82
+ console.log(` Handshakes ${pad(stats["totalHandshakes"])} sent`);
83
+ console.log(` Connections ${pad(stats["totalConnections"])} unique pairs`);
84
+ console.log(` Vouches ${pad(stats["totalVouches"])} active`);
85
+ console.log(` Capabilities ${pad(stats["totalCapabilityClaims"])} claimed`);
86
+ console.log();
87
+ console.log(chalk.dim(" Subgraph: v0.2.0 · synced"));
88
+ } catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ if (opts.json) {
91
+ console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
92
+ } else {
93
+ console.error(chalk.red(`Subgraph unavailable: ${msg}`));
94
+ }
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ // ─── arena feed (alias) ─────────────────────────────────────────────────────
100
+
101
+ arena
102
+ .command("feed")
103
+ .description("Live terminal feed of recent Arena events (alias for arc402 feed)")
104
+ .option("--limit <n>", "Number of events to show", "20")
105
+ .option("--live", "Poll every 30s for new events")
106
+ .option("--type <type>", "Filter by event type: handshake|hire|fulfill|vouch")
107
+ .option("--json", "Output as JSON")
108
+ .action(async (opts) => {
109
+ try {
110
+ await runFeed(opts as FeedOptions);
111
+ } catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ if ((opts as FeedOptions).json) {
114
+ console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
115
+ } else {
116
+ console.error(chalk.red(`Subgraph unavailable: ${msg}`));
117
+ }
118
+ process.exit(1);
119
+ }
120
+ });
121
+ }
@@ -8,6 +8,7 @@ import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router
8
8
  import { SERVICE_AGREEMENT_ABI } from "../abis";
9
9
  import { readFile } from "fs/promises";
10
10
  import prompts from "prompts";
11
+ import { resolveAgentEndpoint, notifyAgent, DEFAULT_REGISTRY_ADDRESS } from "../endpoint-notify";
11
12
 
12
13
  export function registerDeliverCommand(program: Command): void {
13
14
  program
@@ -24,6 +25,7 @@ export function registerDeliverCommand(program: Command): void {
24
25
  printSenderInfo(config);
25
26
 
26
27
  // Pre-flight: check deadline and legacyFulfillEnabled (J3-01, J3-02)
28
+ let clientAddress = "";
27
29
  {
28
30
  const { provider: prefProvider } = await getClient(config);
29
31
  const saAbi = [
@@ -42,6 +44,7 @@ export function registerDeliverCommand(program: Command): void {
42
44
  console.error(`Contact the client to open a new agreement.`);
43
45
  process.exit(1);
44
46
  }
47
+ clientAddress = String(ag.client ?? "");
45
48
  } catch (e) {
46
49
  // If it's a contract read failure, skip the check (let the tx reveal the error)
47
50
  if (e instanceof Error && !e.message.includes("CALL_EXCEPTION") && !e.message.includes("could not decode")) throw e;
@@ -112,5 +115,21 @@ export function registerDeliverCommand(program: Command): void {
112
115
  if (opts.fulfill) await client.fulfill(BigInt(id), hash); else await client.commitDeliverable(BigInt(id), hash);
113
116
  }
114
117
  console.log(`${opts.fulfill ? 'fulfilled' : 'committed'} ${id} hash=${hash}`);
118
+
119
+ // Notify client's HTTP endpoint (non-blocking)
120
+ if (clientAddress) {
121
+ try {
122
+ const notifyProvider = new ethers.JsonRpcProvider(config.rpcUrl);
123
+ const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
124
+ const endpoint = await resolveAgentEndpoint(clientAddress, notifyProvider, registryAddress);
125
+ await notifyAgent(endpoint, "/delivery", {
126
+ agreementId: id,
127
+ deliverableHash: hash,
128
+ from: signerAddress,
129
+ });
130
+ } catch (err) {
131
+ console.warn(`Warning: could not notify client endpoint: ${err instanceof Error ? err.message : String(err)}`);
132
+ }
133
+ }
115
134
  });
116
135
  }
@@ -0,0 +1,228 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+
4
+ const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744310/arc-402/v0.2.0";
5
+
6
+ const HS_TYPE_LABELS: Record<number, string> = {
7
+ 0: "Respected",
8
+ 1: "Curious",
9
+ 2: "Endorsed",
10
+ 3: "Thanked",
11
+ 4: "Collaborated",
12
+ 5: "Challenged",
13
+ 6: "Referred",
14
+ 7: "Hello",
15
+ };
16
+
17
+ async function gql(query: string): Promise<Record<string, unknown>> {
18
+ const res = await fetch(SUBGRAPH_URL, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({ query }),
22
+ });
23
+ if (!res.ok) throw new Error(`Subgraph HTTP ${res.status}`);
24
+ const json = (await res.json()) as { data?: Record<string, unknown>; errors?: unknown[] };
25
+ if (json.errors?.length) throw new Error(`Subgraph error: ${JSON.stringify(json.errors[0])}`);
26
+ return json.data ?? {};
27
+ }
28
+
29
+ function shortAddr(addr: string): string {
30
+ return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
31
+ }
32
+
33
+ function utcTime(ts: number): string {
34
+ const d = new Date(ts * 1000);
35
+ const h = d.getUTCHours().toString().padStart(2, "0");
36
+ const m = d.getUTCMinutes().toString().padStart(2, "0");
37
+ return `${h}:${m} UTC`;
38
+ }
39
+
40
+ function weiToEth(wei: string): string {
41
+ return (Number(BigInt(wei)) / 1e18).toFixed(4);
42
+ }
43
+
44
+ function weiToUsdc(wei: string): string {
45
+ return (Number(BigInt(wei)) / 1e6).toFixed(2);
46
+ }
47
+
48
+ type FeedEvent = {
49
+ type: "handshake" | "hire" | "fulfill" | "vouch";
50
+ timestamp: number;
51
+ raw: Record<string, unknown>;
52
+ };
53
+
54
+ async function fetchFeedEvents(limit: number, typeFilter?: string, sinceTs?: number): Promise<FeedEvent[]> {
55
+ const includeHandshakes = !typeFilter || typeFilter === "handshake";
56
+ const includeHire = !typeFilter || typeFilter === "hire";
57
+ const includeFulfill = !typeFilter || typeFilter === "fulfill";
58
+ const includeVouch = !typeFilter || typeFilter === "vouch";
59
+
60
+ const tsFilt = sinceTs ? `, where: { timestamp_gt: "${sinceTs}" }` : "";
61
+ const agTsFilt = sinceTs ? `, where: { proposedAt_gt: "${sinceTs}" }` : "";
62
+
63
+ const parts: string[] = [];
64
+
65
+ if (includeHandshakes) {
66
+ parts.push(`
67
+ handshakes(orderBy: timestamp, orderDirection: desc, first: ${limit}${tsFilt}) {
68
+ id from { id name } to { id name } hsType note timestamp isNewConnection
69
+ }`);
70
+ }
71
+
72
+ if (includeHire || includeFulfill) {
73
+ parts.push(`
74
+ agreements(orderBy: proposedAt, orderDirection: desc, first: ${limit}${agTsFilt}) {
75
+ id client provider serviceType price state proposedAt updatedAt
76
+ }`);
77
+ }
78
+
79
+ if (includeVouch) {
80
+ parts.push(`
81
+ vouches(orderBy: id, orderDirection: desc, first: ${limit}) {
82
+ id voucher { id name } newAgent { id name } stakeAmount active
83
+ }`);
84
+ }
85
+
86
+ const data = await gql(`{ ${parts.join("\n")} }`);
87
+ const events: FeedEvent[] = [];
88
+
89
+ if (includeHandshakes) {
90
+ for (const h of (data.handshakes as Record<string, unknown>[]) ?? []) {
91
+ events.push({ type: "handshake", timestamp: Number(h["timestamp"]), raw: h });
92
+ }
93
+ }
94
+
95
+ if (data["agreements"]) {
96
+ for (const a of data["agreements"] as Record<string, unknown>[]) {
97
+ const state = Number(a["state"]);
98
+ if (includeHire && state === 0) {
99
+ events.push({ type: "hire", timestamp: Number(a["proposedAt"]), raw: a });
100
+ }
101
+ if (includeFulfill && state === 2) {
102
+ events.push({ type: "fulfill", timestamp: Number(a["updatedAt"]), raw: a });
103
+ }
104
+ }
105
+ }
106
+
107
+ if (includeVouch) {
108
+ for (const v of (data["vouches"] as Record<string, unknown>[]) ?? []) {
109
+ events.push({ type: "vouch", timestamp: 0, raw: v });
110
+ }
111
+ }
112
+
113
+ events.sort((a, b) => b.timestamp - a.timestamp);
114
+ return events.slice(0, limit);
115
+ }
116
+
117
+ function renderFeedEvent(ev: FeedEvent): string {
118
+ const ts = ev.timestamp > 0 ? `[${utcTime(ev.timestamp)}]` : "[ ]";
119
+
120
+ switch (ev.type) {
121
+ case "handshake": {
122
+ const h = ev.raw;
123
+ const from = h["from"] as Record<string, string>;
124
+ const to = h["to"] as Record<string, string>;
125
+ const fromName = from["name"] || shortAddr(from["id"]);
126
+ const toName = to["name"] || shortAddr(to["id"]);
127
+ const label = HS_TYPE_LABELS[Number(h["hsType"])] ?? `Type${h["hsType"]}`;
128
+ const note = h["note"] ? ` "${h["note"]}"` : "";
129
+ return chalk.cyan(`${ts} 🤝 ${fromName} → ${toName} ${label}${note}`);
130
+ }
131
+ case "hire": {
132
+ const a = ev.raw;
133
+ const price = a["price"] ? ` ${weiToEth(a["price"] as string)} ETH` : "";
134
+ return chalk.yellow(
135
+ `${ts} 📋 ${shortAddr(a["client"] as string)} hired ${shortAddr(a["provider"] as string)} ${a["serviceType"]}${price}`,
136
+ );
137
+ }
138
+ case "fulfill": {
139
+ const a = ev.raw;
140
+ const price = a["price"] ? ` ${weiToEth(a["price"] as string)} ETH released` : "";
141
+ return chalk.green(`${ts} ✅ Agreement #${(a["id"] as string).slice(0, 8)} fulfilled${price}`);
142
+ }
143
+ case "vouch": {
144
+ const v = ev.raw;
145
+ const voucher = v["voucher"] as Record<string, string>;
146
+ const newAgent = v["newAgent"] as Record<string, string>;
147
+ const vName = voucher["name"] || shortAddr(voucher["id"]);
148
+ const nName = newAgent["name"] || shortAddr(newAgent["id"]);
149
+ const stake = v["stakeAmount"] ? ` ${weiToUsdc(v["stakeAmount"] as string)} USDC staked` : "";
150
+ return chalk.magenta(`${ts} 🔗 ${vName} vouched for ${nName}${stake}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ export interface FeedOptions {
156
+ limit?: string;
157
+ live?: boolean;
158
+ type?: string;
159
+ json?: boolean;
160
+ }
161
+
162
+ export async function runFeed(opts: FeedOptions): Promise<void> {
163
+ const limit = parseInt(opts.limit ?? "20", 10);
164
+ const typeFilter = opts.type;
165
+
166
+ const events = await fetchFeedEvents(limit, typeFilter);
167
+
168
+ if (opts.json) {
169
+ console.log(JSON.stringify(events, null, 2));
170
+ if (!opts.live) return;
171
+ } else {
172
+ for (const ev of [...events].reverse()) {
173
+ console.log(renderFeedEvent(ev));
174
+ }
175
+ if (!opts.live) return;
176
+ }
177
+
178
+ // Live mode — keep process alive and poll every 30s
179
+ process.stdin.resume();
180
+ let lastTs = events.length > 0 ? events[0].timestamp : Math.floor(Date.now() / 1000);
181
+
182
+ const interval = setInterval(async () => {
183
+ try {
184
+ const newEvents = await fetchFeedEvents(limit, typeFilter, lastTs);
185
+ if (newEvents.length > 0) {
186
+ if (opts.json) {
187
+ console.log(JSON.stringify(newEvents, null, 2));
188
+ } else {
189
+ console.log(chalk.dim("─".repeat(50)));
190
+ for (const ev of [...newEvents].reverse()) {
191
+ console.log(renderFeedEvent(ev));
192
+ }
193
+ }
194
+ lastTs = newEvents[0].timestamp;
195
+ }
196
+ } catch {
197
+ if (!opts.json) console.log(chalk.dim(" (subgraph unavailable)"));
198
+ }
199
+ }, 30_000);
200
+
201
+ process.on("SIGINT", () => {
202
+ clearInterval(interval);
203
+ process.exit(0);
204
+ });
205
+ }
206
+
207
+ export function registerFeedCommand(program: Command): void {
208
+ program
209
+ .command("feed")
210
+ .description("Live terminal feed of recent Arena events")
211
+ .option("--limit <n>", "Number of events to show", "20")
212
+ .option("--live", "Poll every 30s for new events")
213
+ .option("--type <type>", "Filter by event type: handshake|hire|fulfill|vouch")
214
+ .option("--json", "Output as JSON")
215
+ .action(async (opts) => {
216
+ try {
217
+ await runFeed(opts as FeedOptions);
218
+ } catch (err) {
219
+ const msg = err instanceof Error ? err.message : String(err);
220
+ if ((opts as FeedOptions).json) {
221
+ console.log(JSON.stringify({ error: "Subgraph unavailable", details: msg }));
222
+ } else {
223
+ console.error(chalk.red(`Subgraph unavailable: ${msg}`));
224
+ }
225
+ process.exit(1);
226
+ }
227
+ });
228
+ }