@agirails/sdk 2.2.3 → 2.3.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 (93) hide show
  1. package/README.md +20 -23
  2. package/dist/ACTPClient.d.ts +7 -0
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +56 -1
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/abi/AgentRegistry.json +133 -0
  7. package/dist/adapters/X402Adapter.d.ts +34 -7
  8. package/dist/adapters/X402Adapter.d.ts.map +1 -1
  9. package/dist/adapters/X402Adapter.js +36 -8
  10. package/dist/adapters/X402Adapter.js.map +1 -1
  11. package/dist/adapters/index.d.ts +1 -1
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/cli/commands/diff.d.ts +11 -0
  15. package/dist/cli/commands/diff.d.ts.map +1 -0
  16. package/dist/cli/commands/diff.js +115 -0
  17. package/dist/cli/commands/diff.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +51 -2
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/commands/publish.d.ts +11 -0
  22. package/dist/cli/commands/publish.d.ts.map +1 -0
  23. package/dist/cli/commands/publish.js +170 -0
  24. package/dist/cli/commands/publish.js.map +1 -0
  25. package/dist/cli/commands/pull.d.ts +12 -0
  26. package/dist/cli/commands/pull.d.ts.map +1 -0
  27. package/dist/cli/commands/pull.js +99 -0
  28. package/dist/cli/commands/pull.js.map +1 -0
  29. package/dist/cli/index.js +7 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/config/agirailsmd.d.ts +94 -0
  32. package/dist/config/agirailsmd.d.ts.map +1 -0
  33. package/dist/config/agirailsmd.js +209 -0
  34. package/dist/config/agirailsmd.js.map +1 -0
  35. package/dist/config/networks.d.ts +2 -0
  36. package/dist/config/networks.d.ts.map +1 -1
  37. package/dist/config/networks.js +10 -4
  38. package/dist/config/networks.js.map +1 -1
  39. package/dist/config/publishPipeline.d.ts +61 -0
  40. package/dist/config/publishPipeline.d.ts.map +1 -0
  41. package/dist/config/publishPipeline.js +192 -0
  42. package/dist/config/publishPipeline.js.map +1 -0
  43. package/dist/config/syncOperations.d.ts +67 -0
  44. package/dist/config/syncOperations.d.ts.map +1 -0
  45. package/dist/config/syncOperations.js +208 -0
  46. package/dist/config/syncOperations.js.map +1 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +7 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/level0/request.d.ts.map +1 -1
  52. package/dist/level0/request.js +23 -86
  53. package/dist/level0/request.js.map +1 -1
  54. package/dist/level1/Agent.d.ts +0 -11
  55. package/dist/level1/Agent.d.ts.map +1 -1
  56. package/dist/level1/Agent.js +15 -32
  57. package/dist/level1/Agent.js.map +1 -1
  58. package/dist/registry/AgentRegistryClient.d.ts +75 -0
  59. package/dist/registry/AgentRegistryClient.d.ts.map +1 -0
  60. package/dist/registry/AgentRegistryClient.js +160 -0
  61. package/dist/registry/AgentRegistryClient.js.map +1 -0
  62. package/dist/types/adapter.d.ts +39 -0
  63. package/dist/types/adapter.d.ts.map +1 -1
  64. package/dist/types/adapter.js +7 -0
  65. package/dist/types/adapter.js.map +1 -1
  66. package/dist/types/x402.d.ts +23 -0
  67. package/dist/types/x402.d.ts.map +1 -1
  68. package/dist/types/x402.js.map +1 -1
  69. package/dist/wallet/keystore.d.ts +16 -0
  70. package/dist/wallet/keystore.d.ts.map +1 -0
  71. package/dist/wallet/keystore.js +132 -0
  72. package/dist/wallet/keystore.js.map +1 -0
  73. package/package.json +2 -1
  74. package/src/ACTPClient.ts +63 -1
  75. package/src/abi/AgentRegistry.json +133 -0
  76. package/src/adapters/X402Adapter.ts +94 -16
  77. package/src/adapters/index.ts +9 -1
  78. package/src/cli/commands/diff.ts +141 -0
  79. package/src/cli/commands/init.ts +65 -4
  80. package/src/cli/commands/publish.ts +209 -0
  81. package/src/cli/commands/pull.ts +124 -0
  82. package/src/cli/index.ts +8 -0
  83. package/src/config/agirailsmd.ts +262 -0
  84. package/src/config/networks.ts +12 -4
  85. package/src/config/publishPipeline.ts +276 -0
  86. package/src/config/syncOperations.ts +279 -0
  87. package/src/index.ts +3 -0
  88. package/src/level0/request.ts +27 -88
  89. package/src/level1/Agent.ts +16 -32
  90. package/src/registry/AgentRegistryClient.ts +202 -0
  91. package/src/types/adapter.ts +14 -0
  92. package/src/types/x402.ts +32 -0
  93. package/src/wallet/keystore.ts +119 -0
package/src/ACTPClient.ts CHANGED
@@ -655,7 +655,16 @@ export class ACTPClient {
655
655
 
656
656
  // SECURITY FIX (C-4): Pass EASHelper to adapters for attestation verification
657
657
  // ERC-8004: Pass bridge for agent ID resolution, reporter for settlement outcomes
658
- return new ACTPClient(runtime, normalizedAddress, info, easHelper, erc8004Bridge, reputationReporter);
658
+ const client = new ACTPClient(runtime, normalizedAddress, info, easHelper, erc8004Bridge, reputationReporter);
659
+
660
+ // Drift detection: non-blocking check for AGIRAILS.md sync status
661
+ if (config.mode !== 'mock') {
662
+ client.checkConfigDrift(config).catch(() => {
663
+ // Silently ignore drift check errors — non-critical
664
+ });
665
+ }
666
+
667
+ return client;
659
668
  }
660
669
 
661
670
  // ==========================================================================
@@ -1103,4 +1112,57 @@ export class ACTPClient {
1103
1112
  getReputationReporter(): ReputationReporter | undefined {
1104
1113
  return this.reputationReporter;
1105
1114
  }
1115
+
1116
+ /**
1117
+ * Non-blocking drift detection for AGIRAILS.md config.
1118
+ * Checks if local AGIRAILS.md matches on-chain config hash.
1119
+ * Logs warnings but never blocks agent operation.
1120
+ * @internal
1121
+ */
1122
+ private async checkConfigDrift(config: ACTPClientConfig): Promise<void> {
1123
+ try {
1124
+ const { existsSync, readFileSync } = await import('fs');
1125
+ const { join } = await import('path');
1126
+
1127
+ // Look for AGIRAILS.md in cwd
1128
+ const agirailsMdPath = join(process.cwd(), 'AGIRAILS.md');
1129
+ if (!existsSync(agirailsMdPath)) {
1130
+ return; // No local file — nothing to check
1131
+ }
1132
+
1133
+ const network = config.mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
1134
+ const networkConfig = getNetwork(network);
1135
+ if (!networkConfig.contracts.agentRegistry) {
1136
+ return; // No registry on this network
1137
+ }
1138
+
1139
+ const content = readFileSync(agirailsMdPath, 'utf-8');
1140
+ const { computeConfigHash } = await import('./config/agirailsmd');
1141
+ const { configHash: localHash } = computeConfigHash(content);
1142
+
1143
+ const { AgentRegistryClient } = await import('./registry/AgentRegistryClient');
1144
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
1145
+ const registryClient = AgentRegistryClient.readOnly(networkConfig.contracts.agentRegistry, provider);
1146
+
1147
+ // Detect template vs published state from frontmatter
1148
+ const { parseAgirailsMd: parseMd } = await import('./config/agirailsmd');
1149
+ const { frontmatter } = parseMd(content);
1150
+ const isTemplate = !frontmatter.config_hash;
1151
+
1152
+ const onChainState = await registryClient.getConfig(config.requesterAddress);
1153
+ const ZERO_HASH = '0x' + '0'.repeat(64);
1154
+
1155
+ if (onChainState.configHash === ZERO_HASH) {
1156
+ if (isTemplate) {
1157
+ console.info('[AGIRAILS] AGIRAILS.md loaded (template mode). Run "actp publish" to register and sync on-chain.');
1158
+ } else {
1159
+ console.warn('[AGIRAILS] Config not published on-chain. Run: actp publish');
1160
+ }
1161
+ } else if (onChainState.configHash !== localHash) {
1162
+ console.warn('[AGIRAILS] Local AGIRAILS.md differs from on-chain. Run: actp diff');
1163
+ }
1164
+ } catch {
1165
+ // Silently ignore — drift detection is best-effort
1166
+ }
1167
+ }
1106
1168
  }
@@ -10,6 +10,19 @@
10
10
  ],
11
11
  "stateMutability": "nonpayable"
12
12
  },
13
+ {
14
+ "type": "function",
15
+ "name": "MAX_CID_LENGTH",
16
+ "inputs": [],
17
+ "outputs": [
18
+ {
19
+ "name": "",
20
+ "type": "uint256",
21
+ "internalType": "uint256"
22
+ }
23
+ ],
24
+ "stateMutability": "view"
25
+ },
13
26
  {
14
27
  "type": "function",
15
28
  "name": "MAX_ENDPOINT_LENGTH",
@@ -166,6 +179,21 @@
166
179
  "name": "isActive",
167
180
  "type": "bool",
168
181
  "internalType": "bool"
182
+ },
183
+ {
184
+ "name": "configHash",
185
+ "type": "bytes32",
186
+ "internalType": "bytes32"
187
+ },
188
+ {
189
+ "name": "configCID",
190
+ "type": "string",
191
+ "internalType": "string"
192
+ },
193
+ {
194
+ "name": "listed",
195
+ "type": "bool",
196
+ "internalType": "bool"
169
197
  }
170
198
  ],
171
199
  "stateMutability": "view"
@@ -277,6 +305,21 @@
277
305
  "name": "isActive",
278
306
  "type": "bool",
279
307
  "internalType": "bool"
308
+ },
309
+ {
310
+ "name": "configHash",
311
+ "type": "bytes32",
312
+ "internalType": "bytes32"
313
+ },
314
+ {
315
+ "name": "configCID",
316
+ "type": "string",
317
+ "internalType": "string"
318
+ },
319
+ {
320
+ "name": "listed",
321
+ "type": "bool",
322
+ "internalType": "bool"
280
323
  }
281
324
  ]
282
325
  }
@@ -358,6 +401,21 @@
358
401
  "name": "isActive",
359
402
  "type": "bool",
360
403
  "internalType": "bool"
404
+ },
405
+ {
406
+ "name": "configHash",
407
+ "type": "bytes32",
408
+ "internalType": "bytes32"
409
+ },
410
+ {
411
+ "name": "configCID",
412
+ "type": "string",
413
+ "internalType": "string"
414
+ },
415
+ {
416
+ "name": "listed",
417
+ "type": "bool",
418
+ "internalType": "bool"
361
419
  }
362
420
  ]
363
421
  }
@@ -420,6 +478,24 @@
420
478
  ],
421
479
  "stateMutability": "view"
422
480
  },
481
+ {
482
+ "type": "function",
483
+ "name": "publishConfig",
484
+ "inputs": [
485
+ {
486
+ "name": "cid",
487
+ "type": "string",
488
+ "internalType": "string"
489
+ },
490
+ {
491
+ "name": "hash",
492
+ "type": "bytes32",
493
+ "internalType": "bytes32"
494
+ }
495
+ ],
496
+ "outputs": [],
497
+ "stateMutability": "nonpayable"
498
+ },
423
499
  {
424
500
  "type": "function",
425
501
  "name": "queryAgentsByService",
@@ -535,6 +611,19 @@
535
611
  "outputs": [],
536
612
  "stateMutability": "nonpayable"
537
613
  },
614
+ {
615
+ "type": "function",
616
+ "name": "setListed",
617
+ "inputs": [
618
+ {
619
+ "name": "_listed",
620
+ "type": "bool",
621
+ "internalType": "bool"
622
+ }
623
+ ],
624
+ "outputs": [],
625
+ "stateMutability": "nonpayable"
626
+ },
538
627
  {
539
628
  "type": "function",
540
629
  "name": "supportsService",
@@ -656,6 +745,31 @@
656
745
  ],
657
746
  "anonymous": false
658
747
  },
748
+ {
749
+ "type": "event",
750
+ "name": "ConfigPublished",
751
+ "inputs": [
752
+ {
753
+ "name": "agent",
754
+ "type": "address",
755
+ "indexed": true,
756
+ "internalType": "address"
757
+ },
758
+ {
759
+ "name": "configCID",
760
+ "type": "string",
761
+ "indexed": false,
762
+ "internalType": "string"
763
+ },
764
+ {
765
+ "name": "configHash",
766
+ "type": "bytes32",
767
+ "indexed": false,
768
+ "internalType": "bytes32"
769
+ }
770
+ ],
771
+ "anonymous": false
772
+ },
659
773
  {
660
774
  "type": "event",
661
775
  "name": "EndpointUpdated",
@@ -687,6 +801,25 @@
687
801
  ],
688
802
  "anonymous": false
689
803
  },
804
+ {
805
+ "type": "event",
806
+ "name": "ListingChanged",
807
+ "inputs": [
808
+ {
809
+ "name": "agent",
810
+ "type": "address",
811
+ "indexed": true,
812
+ "internalType": "address"
813
+ },
814
+ {
815
+ "name": "listed",
816
+ "type": "bool",
817
+ "indexed": false,
818
+ "internalType": "bool"
819
+ }
820
+ ],
821
+ "anonymous": false
822
+ },
690
823
  {
691
824
  "type": "event",
692
825
  "name": "ReputationUpdated",
@@ -35,6 +35,7 @@ import {
35
35
  X402Error,
36
36
  X402ErrorCode,
37
37
  X402Network,
38
+ X402FeeBreakdown,
38
39
  isValidX402Network,
39
40
  } from '../types/x402';
40
41
 
@@ -46,14 +47,33 @@ import {
46
47
  export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
47
48
 
48
49
  /**
49
- * Transfer function for atomic payments.
50
- *
50
+ * Transfer function for atomic payments (legacy direct transfer).
51
+ *
51
52
  * @param to - Recipient address
52
53
  * @param amount - Amount in USDC wei (string)
53
54
  * @returns Transaction hash as proof
54
55
  */
55
56
  export type TransferFunction = (to: string, amount: string) => Promise<string>;
56
57
 
58
+ /**
59
+ * Approve function for USDC allowance (used with X402Relay).
60
+ *
61
+ * @param spender - Spender address (relay contract)
62
+ * @param amount - Amount in USDC wei (string)
63
+ * @returns Transaction hash
64
+ */
65
+ export type ApproveFunction = (spender: string, amount: string) => Promise<string>;
66
+
67
+ /**
68
+ * Relay pay function — calls X402Relay.payWithFee().
69
+ *
70
+ * @param provider - Provider address
71
+ * @param grossAmount - Gross USDC amount (string)
72
+ * @param serviceId - Service identifier (bytes32 hex)
73
+ * @returns Transaction hash
74
+ */
75
+ export type RelayPayFunction = (provider: string, grossAmount: string, serviceId: string) => Promise<string>;
76
+
57
77
  /** Supported HTTP methods for x402 requests */
58
78
  export type X402HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
59
79
 
@@ -76,12 +96,15 @@ export interface X402PayParams extends UnifiedPayParams {
76
96
 
77
97
  /**
78
98
  * Configuration options for X402Adapter.
99
+ *
100
+ * For fee-enabled payments via X402Relay, provide relayAddress + approveFn + relayPayFn.
101
+ * Without relay config, falls back to legacy direct transfer (no platform fee).
79
102
  */
80
103
  export interface X402AdapterConfig {
81
104
  /** Expected network for validation (must match server's X-Payment-Network) */
82
105
  expectedNetwork: X402Network;
83
106
 
84
- /** Transfer function for atomic payments (required) */
107
+ /** Transfer function for direct atomic payments (legacy fallback) */
85
108
  transferFn: TransferFunction;
86
109
 
87
110
  /** Request timeout in milliseconds (default: 30000) */
@@ -92,6 +115,20 @@ export interface X402AdapterConfig {
92
115
 
93
116
  /** Default headers to include in all requests */
94
117
  defaultHeaders?: Record<string, string>;
118
+
119
+ // --- X402Relay fee-splitting (optional, recommended) ---
120
+
121
+ /** X402Relay contract address for on-chain fee splitting */
122
+ relayAddress?: string;
123
+
124
+ /** USDC approve function — required when relayAddress is set */
125
+ approveFn?: ApproveFunction;
126
+
127
+ /** Relay payWithFee function — required when relayAddress is set */
128
+ relayPayFn?: RelayPayFunction;
129
+
130
+ /** Platform fee in basis points (default: 100 = 1%). Read-only display hint. */
131
+ platformFeeBps?: number;
95
132
  }
96
133
 
97
134
  /**
@@ -104,6 +141,7 @@ interface AtomicPaymentRecord {
104
141
  amount: string;
105
142
  timestamp: number;
106
143
  endpoint: string;
144
+ feeBreakdown?: X402FeeBreakdown;
107
145
  }
108
146
 
109
147
  // ============================================================================
@@ -287,15 +325,15 @@ export class X402Adapter extends BaseAdapter implements IAdapter {
287
325
  );
288
326
  }
289
327
 
290
- // Step 6: ATOMIC PAYMENT - direct transfer, no escrow
291
- const txHash = await this.executeAtomicPayment(paymentHeaders);
328
+ // Step 6: ATOMIC PAYMENT - via relay (with fee) or direct transfer (legacy)
329
+ const { txHash, feeBreakdown } = await this.executeAtomicPayment(paymentHeaders);
292
330
 
293
331
  // Step 7: Retry with proof (same method/headers/body + payment proof)
294
332
  const serviceResponse = await this.retryWithProof(
295
- endpoint,
296
- txHash,
297
- method,
298
- requestHeaders,
333
+ endpoint,
334
+ txHash,
335
+ method,
336
+ requestHeaders,
299
337
  requestBody,
300
338
  contentType
301
339
  );
@@ -308,6 +346,7 @@ export class X402Adapter extends BaseAdapter implements IAdapter {
308
346
  amount: paymentHeaders.amount,
309
347
  timestamp: now,
310
348
  endpoint,
349
+ feeBreakdown,
311
350
  });
312
351
 
313
352
  // Step 9: Return result - DONE! No release needed.
@@ -323,6 +362,7 @@ export class X402Adapter extends BaseAdapter implements IAdapter {
323
362
  provider: paymentHeaders.paymentAddress.toLowerCase(),
324
363
  requester: this.requesterAddress.toLowerCase(),
325
364
  deadline: new Date(paymentHeaders.deadline * 1000).toISOString(),
365
+ feeBreakdown,
326
366
  };
327
367
  }
328
368
 
@@ -562,20 +602,58 @@ export class X402Adapter extends BaseAdapter implements IAdapter {
562
602
  }
563
603
 
564
604
  /**
565
- * Execute atomic payment - direct transfer to provider.
605
+ * Execute atomic payment with fee splitting via X402Relay (if configured),
606
+ * or direct transfer as legacy fallback.
566
607
  *
567
- * This is the key difference from ACTP:
568
- * - No escrow
569
- * - No state machine
570
- * - Just transfer and done
608
+ * Relay flow: approve relay relay.payWithFee(provider, gross, serviceId)
609
+ * Legacy flow: transferFn(provider, amount) — no fee extraction
571
610
  */
572
- private async executeAtomicPayment(headers: X402PaymentHeaders): Promise<string> {
611
+ private async executeAtomicPayment(headers: X402PaymentHeaders): Promise<{
612
+ txHash: string;
613
+ feeBreakdown?: X402FeeBreakdown;
614
+ }> {
573
615
  try {
616
+ // Relay path: on-chain fee splitting
617
+ if (this.config.relayAddress && this.config.approveFn && this.config.relayPayFn) {
618
+ const grossAmount = headers.amount;
619
+ const feeBps = this.config.platformFeeBps ?? 100;
620
+ const MIN_FEE = 50_000n; // $0.05 USDC
621
+
622
+ // Calculate fee: max(gross * bps / 10000, MIN_FEE)
623
+ const grossBig = BigInt(grossAmount);
624
+ const bpsFee = (grossBig * BigInt(feeBps)) / 10_000n;
625
+ const fee = bpsFee > MIN_FEE ? bpsFee : MIN_FEE;
626
+ const providerNet = grossBig - fee;
627
+
628
+ // 1. Approve relay for gross amount
629
+ await this.config.approveFn(this.config.relayAddress, grossAmount);
630
+
631
+ // 2. Call relay.payWithFee
632
+ const serviceId = headers.serviceId ?? '0x' + '0'.repeat(64);
633
+ const txHash = await this.config.relayPayFn(
634
+ headers.paymentAddress,
635
+ grossAmount,
636
+ serviceId
637
+ );
638
+
639
+ return {
640
+ txHash,
641
+ feeBreakdown: {
642
+ grossAmount,
643
+ providerNet: providerNet.toString(),
644
+ platformFee: fee.toString(),
645
+ feeBps,
646
+ estimated: true,
647
+ },
648
+ };
649
+ }
650
+
651
+ // Legacy path: direct transfer, no fee
574
652
  const txHash = await this.transferFn(
575
653
  headers.paymentAddress,
576
654
  headers.amount
577
655
  );
578
- return txHash;
656
+ return { txHash };
579
657
  } catch (error) {
580
658
  throw new X402Error(
581
659
  `Atomic payment failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -23,7 +23,15 @@ export {
23
23
  } from './BaseAdapter';
24
24
  export { BasicAdapter, BasicPayParams, BasicPayResult } from './BasicAdapter';
25
25
  export { StandardAdapter, StandardTransactionParams } from './StandardAdapter';
26
- export { X402Adapter, X402AdapterConfig, FetchFunction } from './X402Adapter';
26
+ export {
27
+ X402Adapter,
28
+ X402AdapterConfig,
29
+ X402PayParams,
30
+ X402HttpMethod,
31
+ FetchFunction,
32
+ ApproveFunction,
33
+ RelayPayFunction,
34
+ } from './X402Adapter';
27
35
  export { AdapterRegistry } from './AdapterRegistry';
28
36
  export { AdapterRouter, AdapterSelectionResult } from './AdapterRouter';
29
37
  export {
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Diff Command - Compare local AGIRAILS.md with on-chain config
3
+ *
4
+ * Shows sync status between local file and AgentRegistry state.
5
+ * Terraform-style: never auto-overwrites, just shows differences.
6
+ *
7
+ * @module cli/commands/diff
8
+ */
9
+
10
+ import { Command } from 'commander';
11
+ import { Output, ExitCode } from '../utils/output';
12
+ import { mapError } from '../utils/client';
13
+ import { resolve } from 'path';
14
+ import { ethers } from 'ethers';
15
+ import { diff } from '../../config/syncOperations';
16
+ import { getNetwork } from '../../config/networks';
17
+
18
+ // ============================================================================
19
+ // Command Definition
20
+ // ============================================================================
21
+
22
+ export function createDiffCommand(): Command {
23
+ const cmd = new Command('diff')
24
+ .description('Compare local AGIRAILS.md with on-chain config')
25
+ .argument('[path]', 'Path to AGIRAILS.md', './AGIRAILS.md')
26
+ .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
27
+ .option('-a, --address <address>', 'Agent address to compare with')
28
+ .option('--json', 'Output as JSON')
29
+ .option('-q, --quiet', 'Output only sync status')
30
+ .action(async (path, options) => {
31
+ const output = new Output(
32
+ options.json ? 'json' : options.quiet ? 'quiet' : 'human'
33
+ );
34
+
35
+ try {
36
+ await runDiff(path, options, output);
37
+ } catch (error) {
38
+ const structuredError = mapError(error);
39
+ output.errorResult({
40
+ code: structuredError.code,
41
+ message: structuredError.message,
42
+ details: structuredError.details,
43
+ });
44
+ process.exit(ExitCode.ERROR);
45
+ }
46
+ });
47
+
48
+ return cmd;
49
+ }
50
+
51
+ // ============================================================================
52
+ // Implementation
53
+ // ============================================================================
54
+
55
+ interface DiffCommandOptions {
56
+ network: string;
57
+ address?: string;
58
+ }
59
+
60
+ async function runDiff(
61
+ filePath: string,
62
+ options: DiffCommandOptions,
63
+ output: Output
64
+ ): Promise<void> {
65
+ const resolvedPath = resolve(filePath);
66
+
67
+ // Determine agent address
68
+ let agentAddress = options.address;
69
+ if (!agentAddress) {
70
+ const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
71
+ if (!privateKey) {
72
+ output.error('Agent address required. Use --address or set ACTP_PRIVATE_KEY env var.');
73
+ process.exit(ExitCode.INVALID_INPUT);
74
+ }
75
+ agentAddress = new ethers.Wallet(privateKey).address;
76
+ }
77
+
78
+ const networkConfig = getNetwork(options.network);
79
+ if (!networkConfig.contracts.agentRegistry) {
80
+ output.error(`AgentRegistry not deployed on ${options.network}`);
81
+ process.exit(ExitCode.ERROR);
82
+ }
83
+
84
+ const spinner = output.spinner('Comparing local and on-chain config...');
85
+
86
+ try {
87
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
88
+
89
+ const result = await diff({
90
+ path: resolvedPath,
91
+ agentAddress,
92
+ registryAddress: networkConfig.contracts.agentRegistry,
93
+ provider,
94
+ });
95
+
96
+ spinner.stop(true);
97
+
98
+ output.result(
99
+ {
100
+ status: result.status,
101
+ inSync: result.inSync,
102
+ localHash: result.localHash,
103
+ onChainHash: result.onChainHash,
104
+ onChainCID: result.onChainCID || null,
105
+ hasLocalFile: result.hasLocalFile,
106
+ hasOnChainConfig: result.hasOnChainConfig,
107
+ network: options.network,
108
+ agent: agentAddress,
109
+ },
110
+ { quietKey: 'status' }
111
+ );
112
+
113
+ output.blank();
114
+
115
+ // Human-friendly status messages
116
+ switch (result.status) {
117
+ case 'in-sync':
118
+ output.success('Local and on-chain configs are in sync.');
119
+ break;
120
+ case 'local-ahead':
121
+ output.warning('Local changes not yet published. Run: actp publish');
122
+ break;
123
+ case 'remote-ahead':
124
+ output.warning('On-chain config is newer. Run: actp pull');
125
+ break;
126
+ case 'diverged':
127
+ output.warning('Local and on-chain configs have diverged.');
128
+ output.print(' Resolve by running: actp publish (to push local) or actp pull --force (to accept remote)');
129
+ break;
130
+ case 'no-local':
131
+ output.info('No local AGIRAILS.md found. Run: actp pull');
132
+ break;
133
+ case 'no-remote':
134
+ output.info('No config published on-chain. Run: actp publish');
135
+ break;
136
+ }
137
+ } catch (error) {
138
+ spinner.stop(false);
139
+ throw error;
140
+ }
141
+ }
@@ -10,6 +10,7 @@
10
10
  import * as crypto from 'crypto';
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
+ import * as readline from 'readline';
13
14
  import { Command } from 'commander';
14
15
  import {
15
16
  saveConfig,
@@ -102,10 +103,10 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
102
103
  address = '0x' + crypto.randomBytes(20).toString('hex');
103
104
  output.info(`Generated mock address: ${address}`);
104
105
  } else {
105
- throw new Error(
106
- `Address required for ${mode} mode.\n` +
107
- 'Use --address <your-address> to specify.'
108
- );
106
+ // Generate a real wallet with encrypted keystore
107
+ const actpDir = getActpDir(projectRoot);
108
+ fs.mkdirSync(actpDir, { recursive: true });
109
+ address = await generateWallet(actpDir, output);
109
110
  }
110
111
  }
111
112
 
@@ -177,6 +178,66 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
177
178
  }
178
179
  }
179
180
 
181
+ // ============================================================================
182
+ // Wallet Generation
183
+ // ============================================================================
184
+
185
+ async function generateWallet(actpDir: string, output: Output): Promise<string> {
186
+ const { Wallet } = await import('ethers');
187
+
188
+ const wallet = Wallet.createRandom();
189
+
190
+ // Get password from env var or interactive prompt
191
+ let password = process.env.ACTP_KEY_PASSWORD;
192
+ if (!password) {
193
+ password = await promptPassword();
194
+ }
195
+
196
+ if (!password || password.length < 8) {
197
+ throw new Error(
198
+ 'Wallet password required (minimum 8 characters).\n' +
199
+ 'Set ACTP_KEY_PASSWORD env var or enter when prompted.'
200
+ );
201
+ }
202
+
203
+ // Encrypt with Keystore V3 (scrypt + AES-128-CTR)
204
+ output.info('Encrypting wallet (this takes a few seconds)...');
205
+ const keystore = await wallet.encrypt(password);
206
+
207
+ // Save with restrictive permissions
208
+ const keystorePath = path.join(actpDir, 'keystore.json');
209
+ fs.writeFileSync(keystorePath, keystore, { mode: 0o600 });
210
+
211
+ output.success('Key securely saved and encrypted');
212
+ output.info(`Address: ${wallet.address}`);
213
+ output.warning('Back up your password — it cannot be recovered.');
214
+ output.info('');
215
+ output.info('To start your agent:');
216
+ output.info(' export ACTP_KEY_PASSWORD="your-password"');
217
+ output.info(' npx ts-node agent.ts');
218
+
219
+ return wallet.address;
220
+ }
221
+
222
+ async function promptPassword(): Promise<string> {
223
+ // If not a TTY (e.g. piped or run by agent), skip prompt
224
+ if (!process.stdin.isTTY) {
225
+ return '';
226
+ }
227
+
228
+ const rl = readline.createInterface({
229
+ input: process.stdin,
230
+ output: process.stdout,
231
+ });
232
+
233
+ return new Promise((resolve) => {
234
+ rl.question('Enter password for wallet encryption (min 8 chars): ', (answer) => {
235
+ rl.close();
236
+ resolve(answer.trim());
237
+ });
238
+ });
239
+ }
240
+
180
241
  // ============================================================================
181
242
  // Scaffold
182
243
  // ============================================================================