@agirails/sdk 2.2.2 → 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 (97) 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/runtime/MockRuntime.d.ts.map +1 -1
  63. package/dist/runtime/MockRuntime.js +3 -1
  64. package/dist/runtime/MockRuntime.js.map +1 -1
  65. package/dist/types/adapter.d.ts +39 -0
  66. package/dist/types/adapter.d.ts.map +1 -1
  67. package/dist/types/adapter.js +7 -0
  68. package/dist/types/adapter.js.map +1 -1
  69. package/dist/types/x402.d.ts +23 -0
  70. package/dist/types/x402.d.ts.map +1 -1
  71. package/dist/types/x402.js.map +1 -1
  72. package/dist/wallet/keystore.d.ts +16 -0
  73. package/dist/wallet/keystore.d.ts.map +1 -0
  74. package/dist/wallet/keystore.js +132 -0
  75. package/dist/wallet/keystore.js.map +1 -0
  76. package/package.json +2 -1
  77. package/src/ACTPClient.ts +63 -1
  78. package/src/abi/AgentRegistry.json +133 -0
  79. package/src/adapters/X402Adapter.ts +94 -16
  80. package/src/adapters/index.ts +9 -1
  81. package/src/cli/commands/diff.ts +141 -0
  82. package/src/cli/commands/init.ts +65 -4
  83. package/src/cli/commands/publish.ts +209 -0
  84. package/src/cli/commands/pull.ts +124 -0
  85. package/src/cli/index.ts +8 -0
  86. package/src/config/agirailsmd.ts +262 -0
  87. package/src/config/networks.ts +12 -4
  88. package/src/config/publishPipeline.ts +276 -0
  89. package/src/config/syncOperations.ts +279 -0
  90. package/src/index.ts +3 -0
  91. package/src/level0/request.ts +27 -88
  92. package/src/level1/Agent.ts +16 -32
  93. package/src/registry/AgentRegistryClient.ts +202 -0
  94. package/src/runtime/MockRuntime.ts +3 -1
  95. package/src/types/adapter.ts +14 -0
  96. package/src/types/x402.ts +32 -0
  97. package/src/wallet/keystore.ts +119 -0
@@ -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
  // ============================================================================
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Publish Command - Publish AGIRAILS.md config on-chain
3
+ *
4
+ * Reads AGIRAILS.md, computes canonical hash, uploads to IPFS,
5
+ * optionally to Arweave, and records on AgentRegistry.
6
+ *
7
+ * @module cli/commands/publish
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 { readFileSync, existsSync } from 'fs';
15
+ import { ethers } from 'ethers';
16
+ import { computeConfigHash, parseAgirailsMd, serializeAgirailsMd } from '../../config/agirailsmd';
17
+ import { AgentRegistryClient } from '../../registry/AgentRegistryClient';
18
+ import { FilebaseClient } from '../../storage/FilebaseClient';
19
+ import { ArweaveClient } from '../../storage/ArweaveClient';
20
+ import { getNetwork } from '../../config/networks';
21
+ import { publishAgirailsMd, PENDING_ENDPOINT } from '../../config/publishPipeline';
22
+
23
+ // ============================================================================
24
+ // Command Definition
25
+ // ============================================================================
26
+
27
+ export function createPublishCommand(): Command {
28
+ const cmd = new Command('publish')
29
+ .description('Publish AGIRAILS.md config on-chain')
30
+ .argument('[path]', 'Path to AGIRAILS.md', './AGIRAILS.md')
31
+ .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
32
+ .option('--skip-arweave', 'Skip permanent Arweave storage (dev mode)')
33
+ .option('--dry-run', 'Show what would happen without executing')
34
+ .option('--json', 'Output as JSON')
35
+ .option('-q, --quiet', 'Output only the config hash')
36
+ .action(async (path, options) => {
37
+ const output = new Output(
38
+ options.json ? 'json' : options.quiet ? 'quiet' : 'human'
39
+ );
40
+
41
+ try {
42
+ await runPublish(path, options, output);
43
+ } catch (error) {
44
+ const structuredError = mapError(error);
45
+ output.errorResult({
46
+ code: structuredError.code,
47
+ message: structuredError.message,
48
+ details: structuredError.details,
49
+ });
50
+ process.exit(ExitCode.ERROR);
51
+ }
52
+ });
53
+
54
+ return cmd;
55
+ }
56
+
57
+ // ============================================================================
58
+ // Implementation
59
+ // ============================================================================
60
+
61
+ interface PublishCommandOptions {
62
+ network: string;
63
+ skipArweave?: boolean;
64
+ dryRun?: boolean;
65
+ }
66
+
67
+ async function runPublish(
68
+ filePath: string,
69
+ options: PublishCommandOptions,
70
+ output: Output
71
+ ): Promise<void> {
72
+ const resolvedPath = resolve(filePath);
73
+
74
+ if (!existsSync(resolvedPath)) {
75
+ output.error(`File not found: ${filePath}`);
76
+ process.exit(ExitCode.INVALID_INPUT);
77
+ }
78
+
79
+ const spinner = output.spinner('Reading AGIRAILS.md...');
80
+
81
+ try {
82
+ // Read and compute hash
83
+ const content = readFileSync(resolvedPath, 'utf-8');
84
+ const { configHash, structuredHash, bodyHash } = computeConfigHash(content);
85
+
86
+ if (options.dryRun) {
87
+ spinner.stop(true);
88
+
89
+ output.result(
90
+ {
91
+ configHash,
92
+ structuredHash,
93
+ bodyHash,
94
+ path: resolvedPath,
95
+ network: options.network,
96
+ dryRun: true,
97
+ },
98
+ { quietKey: 'configHash' }
99
+ );
100
+
101
+ output.blank();
102
+ output.success('Dry run complete. No changes made.');
103
+ return;
104
+ }
105
+
106
+ // Validate environment
107
+ const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
108
+ if (!privateKey) {
109
+ spinner.stop(false);
110
+ output.error('Private key required. Set ACTP_PRIVATE_KEY or PRIVATE_KEY env var.');
111
+ process.exit(ExitCode.INVALID_INPUT);
112
+ }
113
+
114
+ const networkConfig = getNetwork(options.network);
115
+ if (!networkConfig.contracts.agentRegistry) {
116
+ spinner.stop(false);
117
+ output.error(`AgentRegistry not deployed on ${options.network}`);
118
+ process.exit(ExitCode.ERROR);
119
+ }
120
+
121
+ // Create provider and signer
122
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
123
+ const signer = new ethers.Wallet(privateKey, provider);
124
+
125
+ // Create Filebase client
126
+ const filebaseAccessKey = process.env.FILEBASE_ACCESS_KEY;
127
+ const filebaseSecretKey = process.env.FILEBASE_SECRET_KEY;
128
+ if (!filebaseAccessKey || !filebaseSecretKey) {
129
+ spinner.stop(false);
130
+ output.error('Filebase credentials required. Set FILEBASE_ACCESS_KEY and FILEBASE_SECRET_KEY.');
131
+ process.exit(ExitCode.INVALID_INPUT);
132
+ }
133
+
134
+ const filebaseClient = new FilebaseClient({
135
+ accessKey: filebaseAccessKey,
136
+ secretKey: filebaseSecretKey,
137
+ });
138
+
139
+ // Create Arweave client (optional)
140
+ let arweaveClient: ArweaveClient | undefined;
141
+ if (!options.skipArweave) {
142
+ const arweaveKey = process.env.ARCHIVE_UPLOADER_KEY;
143
+ if (arweaveKey) {
144
+ arweaveClient = await ArweaveClient.create({
145
+ privateKey: arweaveKey,
146
+ rpcUrl: networkConfig.rpcUrl,
147
+ });
148
+ }
149
+ }
150
+
151
+ spinner.stop(true);
152
+ const publishSpinner = output.spinner('Publishing to IPFS + on-chain...');
153
+
154
+ const result = await publishAgirailsMd({
155
+ path: resolvedPath,
156
+ network: options.network,
157
+ registryAddress: networkConfig.contracts.agentRegistry,
158
+ signer,
159
+ filebaseClient,
160
+ arweaveClient,
161
+ skipArweave: options.skipArweave || !arweaveClient,
162
+ gasSettings: {
163
+ maxFeePerGas: networkConfig.gasSettings.maxFeePerGas,
164
+ maxPriorityFeePerGas: networkConfig.gasSettings.maxPriorityFeePerGas,
165
+ },
166
+ });
167
+
168
+ publishSpinner.stop(true);
169
+
170
+ output.result(
171
+ {
172
+ configHash: result.configHash,
173
+ cid: result.cid,
174
+ txHash: result.txHash,
175
+ arweaveTxId: result.arweaveTxId || null,
176
+ registered: result.registered || false,
177
+ network: options.network,
178
+ },
179
+ { quietKey: 'configHash' }
180
+ );
181
+
182
+ output.blank();
183
+ if (result.registered) {
184
+ output.success('Agent registered and config published!');
185
+ } else {
186
+ output.success('Config published successfully!');
187
+ }
188
+ output.print('');
189
+ output.print('Next steps:');
190
+ output.print(' - Verify sync: actp diff');
191
+ output.print(' - View on-chain: ' + networkConfig.blockExplorer + '/tx/' + result.txHash);
192
+
193
+ // Warn if placeholder endpoint was used during auto-register
194
+ if (result.registered) {
195
+ const content = readFileSync(resolvedPath, 'utf-8');
196
+ const { frontmatter } = parseAgirailsMd(content);
197
+ if (!frontmatter.endpoint || frontmatter.endpoint === PENDING_ENDPOINT) {
198
+ output.print('');
199
+ output.warning('No endpoint in AGIRAILS.md — registered with placeholder URL.');
200
+ output.print(' Update when your agent is deployed:');
201
+ output.print(' 1. Add "endpoint: https://your-agent.com/webhook" to AGIRAILS.md');
202
+ output.print(' 2. Run: actp publish');
203
+ }
204
+ }
205
+ } catch (error) {
206
+ spinner.stop(false);
207
+ throw error;
208
+ }
209
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pull Command - Pull on-chain config to local AGIRAILS.md
3
+ *
4
+ * Fetches the published AGIRAILS.md from IPFS (via on-chain CID),
5
+ * verifies integrity against on-chain hash, and writes locally.
6
+ * Use --force to overwrite existing file without confirmation.
7
+ *
8
+ * @module cli/commands/pull
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { Output, ExitCode } from '../utils/output';
13
+ import { mapError } from '../utils/client';
14
+ import { resolve } from 'path';
15
+ import { ethers } from 'ethers';
16
+ import { pull } from '../../config/syncOperations';
17
+ import { getNetwork } from '../../config/networks';
18
+
19
+ // ============================================================================
20
+ // Command Definition
21
+ // ============================================================================
22
+
23
+ export function createPullCommand(): Command {
24
+ const cmd = new Command('pull')
25
+ .description('Pull on-chain AGIRAILS.md config to local file')
26
+ .argument('[path]', 'Path to write AGIRAILS.md', './AGIRAILS.md')
27
+ .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
28
+ .option('-a, --address <address>', 'Agent address to pull config for')
29
+ .option('--force', 'Overwrite without confirmation (CI mode)')
30
+ .option('--json', 'Output as JSON')
31
+ .option('-q, --quiet', 'Minimal output')
32
+ .action(async (path, options) => {
33
+ const output = new Output(
34
+ options.json ? 'json' : options.quiet ? 'quiet' : 'human'
35
+ );
36
+
37
+ try {
38
+ await runPull(path, options, output);
39
+ } catch (error) {
40
+ const structuredError = mapError(error);
41
+ output.errorResult({
42
+ code: structuredError.code,
43
+ message: structuredError.message,
44
+ details: structuredError.details,
45
+ });
46
+ process.exit(ExitCode.ERROR);
47
+ }
48
+ });
49
+
50
+ return cmd;
51
+ }
52
+
53
+ // ============================================================================
54
+ // Implementation
55
+ // ============================================================================
56
+
57
+ interface PullCommandOptions {
58
+ network: string;
59
+ address?: string;
60
+ force?: boolean;
61
+ }
62
+
63
+ async function runPull(
64
+ filePath: string,
65
+ options: PullCommandOptions,
66
+ output: Output
67
+ ): Promise<void> {
68
+ const resolvedPath = resolve(filePath);
69
+
70
+ // Determine agent address
71
+ let agentAddress = options.address;
72
+ if (!agentAddress) {
73
+ const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
74
+ if (!privateKey) {
75
+ output.error('Agent address required. Use --address or set ACTP_PRIVATE_KEY env var.');
76
+ process.exit(ExitCode.INVALID_INPUT);
77
+ }
78
+ agentAddress = new ethers.Wallet(privateKey).address;
79
+ }
80
+
81
+ const networkConfig = getNetwork(options.network);
82
+ if (!networkConfig.contracts.agentRegistry) {
83
+ output.error(`AgentRegistry not deployed on ${options.network}`);
84
+ process.exit(ExitCode.ERROR);
85
+ }
86
+
87
+ const spinner = output.spinner('Pulling config from on-chain...');
88
+
89
+ try {
90
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
91
+
92
+ const result = await pull({
93
+ path: resolvedPath,
94
+ agentAddress,
95
+ registryAddress: networkConfig.contracts.agentRegistry,
96
+ provider,
97
+ force: options.force,
98
+ });
99
+
100
+ spinner.stop(result.written);
101
+
102
+ output.result(
103
+ {
104
+ written: result.written,
105
+ cid: result.cid || null,
106
+ status: result.status,
107
+ path: resolvedPath,
108
+ network: options.network,
109
+ },
110
+ { quietKey: 'status' }
111
+ );
112
+
113
+ if (result.written) {
114
+ output.blank();
115
+ output.success(`Config pulled and written to ${filePath}`);
116
+ } else {
117
+ output.blank();
118
+ output.info(result.status);
119
+ }
120
+ } catch (error) {
121
+ spinner.stop(false);
122
+ throw error;
123
+ }
124
+ }
package/src/cli/index.ts CHANGED
@@ -48,6 +48,9 @@ import { createWatchCommand } from './commands/watch';
48
48
  import { createSimulateCommand } from './commands/simulate';
49
49
  import { createBatchCommand } from './commands/batch';
50
50
  import { createTimeCommand } from './commands/time';
51
+ import { createPublishCommand } from './commands/publish';
52
+ import { createPullCommand } from './commands/pull';
53
+ import { createDiffCommand } from './commands/diff';
51
54
 
52
55
  // ============================================================================
53
56
  // Program Setup
@@ -92,6 +95,11 @@ program.addCommand(createBatchCommand());
92
95
  // Mock mode utilities
93
96
  program.addCommand(createTimeCommand());
94
97
 
98
+ // Config sync commands (AGIRAILS.md as source of truth)
99
+ program.addCommand(createPublishCommand());
100
+ program.addCommand(createPullCommand());
101
+ program.addCommand(createDiffCommand());
102
+
95
103
  // ============================================================================
96
104
  // Error Handling
97
105
  // ============================================================================