@agirails/sdk 2.3.3 → 2.4.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 (63) hide show
  1. package/README.md +10 -12
  2. package/dist/ACTPClient.d.ts +80 -3
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +213 -57
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/adapters/BasicAdapter.d.ts +13 -1
  7. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  8. package/dist/adapters/BasicAdapter.js +24 -3
  9. package/dist/adapters/BasicAdapter.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +9 -292
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/publish.d.ts +11 -3
  14. package/dist/cli/commands/publish.d.ts.map +1 -1
  15. package/dist/cli/commands/publish.js +319 -80
  16. package/dist/cli/commands/publish.js.map +1 -1
  17. package/dist/cli/commands/register.d.ts.map +1 -1
  18. package/dist/cli/commands/register.js +10 -0
  19. package/dist/cli/commands/register.js.map +1 -1
  20. package/dist/cli/utils/config.d.ts +3 -2
  21. package/dist/cli/utils/config.d.ts.map +1 -1
  22. package/dist/cli/utils/config.js +9 -1
  23. package/dist/cli/utils/config.js.map +1 -1
  24. package/dist/cli/utils/wallet.d.ts +31 -0
  25. package/dist/cli/utils/wallet.d.ts.map +1 -0
  26. package/dist/cli/utils/wallet.js +114 -0
  27. package/dist/cli/utils/wallet.js.map +1 -0
  28. package/dist/config/pendingPublish.d.ts +79 -0
  29. package/dist/config/pendingPublish.d.ts.map +1 -0
  30. package/dist/config/pendingPublish.js +167 -0
  31. package/dist/config/pendingPublish.js.map +1 -0
  32. package/dist/config/publishPipeline.d.ts +33 -0
  33. package/dist/config/publishPipeline.d.ts.map +1 -1
  34. package/dist/config/publishPipeline.js +33 -2
  35. package/dist/config/publishPipeline.js.map +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/wallet/AutoWalletProvider.d.ts +2 -1
  41. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  42. package/dist/wallet/AutoWalletProvider.js +6 -2
  43. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  44. package/dist/wallet/IWalletProvider.d.ts +4 -2
  45. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  46. package/dist/wallet/aa/TransactionBatcher.d.ts +54 -0
  47. package/dist/wallet/aa/TransactionBatcher.d.ts.map +1 -1
  48. package/dist/wallet/aa/TransactionBatcher.js +67 -1
  49. package/dist/wallet/aa/TransactionBatcher.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/ACTPClient.ts +265 -49
  52. package/src/adapters/BasicAdapter.ts +48 -12
  53. package/src/cli/commands/init.ts +7 -348
  54. package/src/cli/commands/publish.ts +354 -87
  55. package/src/cli/commands/register.ts +14 -0
  56. package/src/cli/utils/config.ts +11 -2
  57. package/src/cli/utils/wallet.ts +109 -0
  58. package/src/config/pendingPublish.ts +226 -0
  59. package/src/config/publishPipeline.ts +82 -1
  60. package/src/index.ts +8 -0
  61. package/src/wallet/AutoWalletProvider.ts +7 -2
  62. package/src/wallet/IWalletProvider.ts +4 -2
  63. package/src/wallet/aa/TransactionBatcher.ts +113 -0
@@ -1,8 +1,16 @@
1
1
  /**
2
- * Publish Command - Publish AGIRAILS.md config on-chain
2
+ * Publish Command Lazy Publish flow.
3
3
  *
4
- * Reads AGIRAILS.md, computes canonical hash, uploads to IPFS,
5
- * optionally to Arweave, and records on AgentRegistry.
4
+ * New flow (no on-chain calls):
5
+ * 1. Parse AGIRAILS.md compute configHash
6
+ * 2. Generate wallet if .actp/keystore.json missing
7
+ * 3. Upload to IPFS via Filebase
8
+ * 4. Optionally upload to Arweave
9
+ * 5. Save pending-publish.json (activation deferred to first payment)
10
+ * 6. Update AGIRAILS.md frontmatter
11
+ *
12
+ * On-chain activation happens automatically during the first payment
13
+ * via ACTPClient's lazy publish mechanism.
6
14
  *
7
15
  * @module cli/commands/publish
8
16
  */
@@ -10,14 +18,68 @@
10
18
  import { Command } from 'commander';
11
19
  import { Output, ExitCode } from '../utils/output';
12
20
  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 } from '../../config/agirailsmd';
21
+ import { resolve, join } from 'path';
22
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
23
+ import { computeConfigHash, serializeAgirailsMd, parseAgirailsMd } from '../../config/agirailsmd';
24
+ import { preparePublish, extractRegistrationParams, PENDING_ENDPOINT } from '../../config/publishPipeline';
25
+ import { savePendingPublish, getActpDir } from '../../config/pendingPublish';
26
+ import { addToGitignore, loadConfig, saveConfig, isInitialized, CLIConfig, CONFIG_DEFAULTS } from '../utils/config';
17
27
  import { FilebaseClient } from '../../storage/FilebaseClient';
18
28
  import { ArweaveClient } from '../../storage/ArweaveClient';
19
- import { getNetwork } from '../../config/networks';
20
- import { publishAgirailsMd, PENDING_ENDPOINT } from '../../config/publishPipeline';
29
+ import { generateWallet } from '../utils/wallet';
30
+
31
+ // ============================================================================
32
+ // Publish Proxy (fallback when no Filebase credentials)
33
+ // ============================================================================
34
+
35
+ const PUBLISH_PROXY_URL = process.env.AGIRAILS_PUBLISH_URL || 'https://api.agirails.io/v1/publish';
36
+
37
+ /**
38
+ * Public client key for the AGIRAILS publish proxy.
39
+ * This is NOT a secret — it's a rate-limited, revocable identifier
40
+ * (same model as Firebase API keys). Override via AGIRAILS_PUBLISH_KEY.
41
+ */
42
+ const PUBLISH_CLIENT_KEY = process.env.AGIRAILS_PUBLISH_KEY || 'ag_pub_v1_2026';
43
+
44
+ /**
45
+ * Upload AGIRAILS.md content via the AGIRAILS publish proxy API.
46
+ * Used when FILEBASE_ACCESS_KEY / FILEBASE_SECRET_KEY are not set.
47
+ *
48
+ * @param content - Raw AGIRAILS.md file content
49
+ * @param localConfigHash - Locally computed configHash (for validation)
50
+ */
51
+ async function publishViaProxy(content: string, localConfigHash: string): Promise<{ cid: string; configHash: string }> {
52
+ const response = await fetch(PUBLISH_PROXY_URL, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'X-API-Key': PUBLISH_CLIENT_KEY,
57
+ },
58
+ body: JSON.stringify({ content }),
59
+ });
60
+
61
+ if (!response.ok) {
62
+ const body = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
63
+ throw new Error(`Publish proxy error (${response.status}): ${body.error || response.statusText}`);
64
+ }
65
+
66
+ const result = await response.json() as { cid: string; configHash: string };
67
+ if (!result.cid || !result.configHash) {
68
+ throw new Error('Publish proxy returned invalid response (missing cid or configHash)');
69
+ }
70
+
71
+ // Validate: proxy's configHash must match locally computed hash
72
+ // (guards against hash-drift between SDK and API canonicalization logic)
73
+ if (result.configHash !== localConfigHash) {
74
+ throw new Error(
75
+ `Config hash mismatch: proxy returned ${result.configHash.slice(0, 10)}... ` +
76
+ `but local hash is ${localConfigHash.slice(0, 10)}... — ` +
77
+ 'This may indicate an SDK/API version mismatch. Update your SDK.'
78
+ );
79
+ }
80
+
81
+ return result;
82
+ }
21
83
 
22
84
  // ============================================================================
23
85
  // Command Definition
@@ -25,9 +87,9 @@ import { publishAgirailsMd, PENDING_ENDPOINT } from '../../config/publishPipelin
25
87
 
26
88
  export function createPublishCommand(): Command {
27
89
  const cmd = new Command('publish')
28
- .description('Publish AGIRAILS.md config on-chain')
90
+ .description('Publish AGIRAILS.md config (offline — activates on first payment)')
29
91
  .argument('[path]', 'Path to AGIRAILS.md', './AGIRAILS.md')
30
- .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
92
+ .option('-n, --network <network>', 'DEPRECATED: network is auto-detected (accepted but ignored)')
31
93
  .option('--skip-arweave', 'Skip permanent Arweave storage (dev mode)')
32
94
  .option('--dry-run', 'Show what would happen without executing')
33
95
  .option('--json', 'Output as JSON')
@@ -58,7 +120,7 @@ export function createPublishCommand(): Command {
58
120
  // ============================================================================
59
121
 
60
122
  interface PublishCommandOptions {
61
- network: string;
123
+ network?: string;
62
124
  skipArweave?: boolean;
63
125
  dryRun?: boolean;
64
126
  }
@@ -68,6 +130,11 @@ async function runPublish(
68
130
  options: PublishCommandOptions,
69
131
  output: Output
70
132
  ): Promise<void> {
133
+ // Deprecation warning for --network
134
+ if (options.network) {
135
+ output.warning('--network flag is deprecated and ignored. Network is auto-detected at payment time.');
136
+ }
137
+
71
138
  const resolvedPath = resolve(filePath);
72
139
 
73
140
  if (!existsSync(resolvedPath)) {
@@ -91,7 +158,6 @@ async function runPublish(
91
158
  structuredHash,
92
159
  bodyHash,
93
160
  path: resolvedPath,
94
- network: options.network,
95
161
  dryRun: true,
96
162
  },
97
163
  { quietKey: 'configHash' }
@@ -102,107 +168,308 @@ async function runPublish(
102
168
  return;
103
169
  }
104
170
 
105
- // Validate environment
106
- const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
107
- if (!privateKey) {
108
- spinner.stop(false);
109
- output.error('Private key required. Set ACTP_PRIVATE_KEY or PRIVATE_KEY env var.');
110
- process.exit(ExitCode.INVALID_INPUT);
171
+ // Ensure .actp directory exists
172
+ const actpDir = getActpDir();
173
+ if (!existsSync(actpDir)) {
174
+ mkdirSync(actpDir, { recursive: true });
111
175
  }
112
176
 
113
- const networkConfig = getNetwork(options.network);
114
- if (!networkConfig.contracts.agentRegistry) {
115
- spinner.stop(false);
116
- output.error(`AgentRegistry not deployed on ${options.network}`);
117
- process.exit(ExitCode.ERROR);
177
+ // Generate wallet if keystore.json doesn't exist
178
+ const keystorePath = join(actpDir, 'keystore.json');
179
+ let walletAddress = '';
180
+ if (!existsSync(keystorePath)) {
181
+ spinner.stop(true);
182
+ output.info('No wallet found — generating one...');
183
+ walletAddress = await generateWallet(actpDir, output);
184
+ output.blank();
185
+ } else {
186
+ // Read address from existing keystore (plaintext field, no decryption)
187
+ try {
188
+ const ks = JSON.parse(readFileSync(keystorePath, 'utf-8'));
189
+ walletAddress = ks.address ? `0x${ks.address.replace(/^0x/, '')}` : '';
190
+ } catch {
191
+ // Best-effort address extraction
192
+ }
118
193
  }
119
194
 
120
- // Create provider and signer
121
- const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
122
- const signer = new ethers.Wallet(privateKey, provider);
123
-
124
- // Create Filebase client
195
+ // Upload to IPFS: direct (Filebase) or via publish proxy
125
196
  const filebaseAccessKey = process.env.FILEBASE_ACCESS_KEY;
126
197
  const filebaseSecretKey = process.env.FILEBASE_SECRET_KEY;
127
- if (!filebaseAccessKey || !filebaseSecretKey) {
128
- spinner.stop(false);
129
- output.error('Filebase credentials required. Set FILEBASE_ACCESS_KEY and FILEBASE_SECRET_KEY.');
130
- process.exit(ExitCode.INVALID_INPUT);
198
+ const useProxy = !filebaseAccessKey || !filebaseSecretKey;
199
+
200
+ spinner.stop(true);
201
+
202
+ if (useProxy) {
203
+ output.info('No Filebase credentials — using AGIRAILS publish proxy.');
131
204
  }
132
205
 
133
- const filebaseClient = new FilebaseClient({
134
- accessKey: filebaseAccessKey,
135
- secretKey: filebaseSecretKey,
136
- });
206
+ const publishSpinner = output.spinner('Publishing to IPFS...');
137
207
 
138
- // Create Arweave client (optional)
139
- let arweaveClient: ArweaveClient | undefined;
140
- if (!options.skipArweave) {
141
- const arweaveKey = process.env.ARCHIVE_UPLOADER_KEY;
142
- if (arweaveKey) {
143
- arweaveClient = await ArweaveClient.create({
144
- privateKey: arweaveKey,
145
- rpcUrl: networkConfig.rpcUrl,
146
- });
208
+ let cid: string;
209
+ let arweaveTxId: string | undefined;
210
+
211
+ if (useProxy) {
212
+ // Fallback: upload via AGIRAILS publish proxy API
213
+ const proxyResult = await publishViaProxy(content, configHash);
214
+ cid = proxyResult.cid;
215
+ // Proxy doesn't do Arweave — skip
216
+ } else {
217
+ // Direct upload via Filebase S3 + optional Arweave
218
+ const filebaseClient = new FilebaseClient({
219
+ accessKey: filebaseAccessKey,
220
+ secretKey: filebaseSecretKey,
221
+ });
222
+
223
+ let arweaveClient: ArweaveClient | undefined;
224
+ if (!options.skipArweave) {
225
+ const arweaveKey = process.env.ARCHIVE_UPLOADER_KEY;
226
+ if (arweaveKey) {
227
+ arweaveClient = await ArweaveClient.create({
228
+ privateKey: arweaveKey,
229
+ rpcUrl: 'https://mainnet.base.org',
230
+ });
231
+ }
147
232
  }
148
- }
149
233
 
150
- spinner.stop(true);
151
- const publishSpinner = output.spinner('Publishing to IPFS + on-chain...');
152
-
153
- const result = await publishAgirailsMd({
154
- path: resolvedPath,
155
- network: options.network,
156
- registryAddress: networkConfig.contracts.agentRegistry,
157
- signer,
158
- filebaseClient,
159
- arweaveClient,
160
- skipArweave: options.skipArweave || !arweaveClient,
161
- gasSettings: {
162
- maxFeePerGas: networkConfig.gasSettings.maxFeePerGas,
163
- maxPriorityFeePerGas: networkConfig.gasSettings.maxPriorityFeePerGas,
164
- },
165
- });
234
+ const prepResult = await preparePublish({
235
+ path: resolvedPath,
236
+ filebaseClient,
237
+ arweaveClient,
238
+ skipArweave: options.skipArweave || !arweaveClient,
239
+ });
240
+
241
+ cid = prepResult.cid;
242
+ arweaveTxId = prepResult.arweaveTxId;
243
+ }
166
244
 
167
245
  publishSpinner.stop(true);
168
246
 
247
+ // Parse frontmatter for registration params
248
+ const { frontmatter, body } = parseAgirailsMd(content);
249
+ const regParams = extractRegistrationParams(frontmatter as Record<string, unknown>);
250
+
251
+ const pendingData = {
252
+ version: 1 as const,
253
+ configHash,
254
+ cid,
255
+ endpoint: regParams.endpoint,
256
+ serviceDescriptors: regParams.serviceDescriptors,
257
+ createdAt: new Date().toISOString(),
258
+ };
259
+
260
+ const projectRoot = resolve(filePath, '..');
261
+
262
+ // ================================================================
263
+ // Local setup: bootstrap or migrate config, ensure .gitignore
264
+ // "publish covers setup" — no separate `actp init` required.
265
+ // ================================================================
266
+ try {
267
+ if (isInitialized(projectRoot)) {
268
+ // Existing project: migrate config (strip deprecated `registered`)
269
+ const config = loadConfig(projectRoot);
270
+ saveConfig(config, projectRoot);
271
+ } else if (walletAddress) {
272
+ // Fresh project: bootstrap minimal config using address from keystore
273
+ const bootstrapConfig: CLIConfig = {
274
+ ...CONFIG_DEFAULTS,
275
+ mode: 'testnet', // safe default for new projects
276
+ address: walletAddress.toLowerCase(),
277
+ wallet: 'auto',
278
+ version: '1.0',
279
+ };
280
+ saveConfig(bootstrapConfig, projectRoot);
281
+ output.info('Created .actp/config.json (testnet, auto wallet).');
282
+ }
283
+ } catch {
284
+ // Config setup is best-effort — publish works without it
285
+ }
286
+ addToGitignore(projectRoot);
287
+
288
+ // ================================================================
289
+ // ALWAYS activate on testnet (one command, both networks per SPEC)
290
+ // ================================================================
291
+ let testnetTxHash: string | undefined;
292
+ const activationSpinner = output.spinner('Activating on testnet...');
293
+ try {
294
+ testnetTxHash = await activateOnTestnet(
295
+ projectRoot, configHash, cid,
296
+ regParams.endpoint, regParams.serviceDescriptors, output,
297
+ );
298
+ activationSpinner.stop(true);
299
+ if (testnetTxHash) {
300
+ output.success(`Testnet activation: ${testnetTxHash}`);
301
+ } else {
302
+ output.info('Testnet: already up-to-date.');
303
+ }
304
+ } catch (activationError) {
305
+ activationSpinner.stop(false);
306
+ // Save testnet pending as fallback — will activate on first testnet payment
307
+ savePendingPublish({ ...pendingData, network: 'base-sepolia' });
308
+ output.warning(
309
+ `Testnet activation failed: ${(activationError as Error).message}\n` +
310
+ ' Saved as pending — will activate on first testnet payment.'
311
+ );
312
+ }
313
+
314
+ // Always save mainnet pending (lazy — activates on first mainnet payment)
315
+ savePendingPublish({ ...pendingData, network: 'base-mainnet' });
316
+
317
+ // Update AGIRAILS.md frontmatter
318
+ const updatedFrontmatter = {
319
+ ...(frontmatter as Record<string, unknown>),
320
+ config_hash: configHash,
321
+ config_cid: cid,
322
+ published_at: new Date().toISOString(),
323
+ ...(arweaveTxId ? { arweave_tx: arweaveTxId } : {}),
324
+ };
325
+ const updatedContent = serializeAgirailsMd(updatedFrontmatter, body);
326
+ writeFileSync(resolvedPath, updatedContent, 'utf-8');
327
+
328
+ // Output results
169
329
  output.result(
170
330
  {
171
- configHash: result.configHash,
172
- cid: result.cid,
173
- txHash: result.txHash,
174
- arweaveTxId: result.arweaveTxId || null,
175
- registered: result.registered || false,
176
- network: options.network,
331
+ configHash,
332
+ cid,
333
+ arweaveTxId: arweaveTxId || null,
334
+ pendingPublish: true,
335
+ testnetActivated: !!testnetTxHash,
336
+ ...(testnetTxHash ? { testnetTxHash } : {}),
177
337
  },
178
338
  { quietKey: 'configHash' }
179
339
  );
180
340
 
181
341
  output.blank();
182
- if (result.registered) {
183
- output.success('Agent registered and config published!');
184
- } else {
185
- output.success('Config published successfully!');
342
+ output.success('Config published to IPFS and saved locally.');
343
+
344
+ if (testnetTxHash) {
345
+ output.print('');
346
+ output.success('Testnet: activated on-chain.');
186
347
  }
348
+
349
+ output.print('');
350
+ output.print('Mainnet: on-chain activation will happen on your first payment.');
187
351
  output.print('');
188
352
  output.print('Next steps:');
189
- output.print(' - Verify sync: actp diff');
190
- output.print(' - View on-chain: ' + networkConfig.blockExplorer + '/tx/' + result.txHash);
191
-
192
- // Warn if placeholder endpoint was used during auto-register
193
- if (result.registered) {
194
- const content = readFileSync(resolvedPath, 'utf-8');
195
- const { frontmatter } = parseAgirailsMd(content);
196
- if (!frontmatter.endpoint || frontmatter.endpoint === PENDING_ENDPOINT) {
197
- output.print('');
198
- output.warning('No endpoint in AGIRAILS.md registered with placeholder URL.');
199
- output.print(' Update when your agent is deployed:');
200
- output.print(' 1. Add "endpoint: https://your-agent.com/webhook" to AGIRAILS.md');
201
- output.print(' 2. Run: actp publish');
202
- }
353
+ output.print(' - Verify config: actp diff');
354
+ output.print(' - Make a payment: your first mainnet payment activates the agent on-chain');
355
+
356
+ // Warn if placeholder endpoint
357
+ if (!frontmatter.endpoint || frontmatter.endpoint === PENDING_ENDPOINT) {
358
+ output.print('');
359
+ output.warning('No endpoint in AGIRAILS.md using placeholder URL.');
360
+ output.print(' Update when your agent is deployed:');
361
+ output.print(' 1. Add "endpoint: https://your-agent.com/webhook" to AGIRAILS.md');
362
+ output.print(' 2. Run: actp publish');
203
363
  }
204
364
  } catch (error) {
205
365
  spinner.stop(false);
206
366
  throw error;
207
367
  }
208
368
  }
369
+
370
+ // ============================================================================
371
+ // Testnet Activation
372
+ // ============================================================================
373
+
374
+ /**
375
+ * Activate agent on testnet during `actp publish`.
376
+ *
377
+ * SPEC v4 Step 3: activation + mint test USDC in a single gasless UserOp.
378
+ * Always mints test USDC regardless of scenario (SPEC: "always on testnet").
379
+ *
380
+ * @returns Transaction hash of the UserOp, or undefined if already up-to-date
381
+ */
382
+ async function activateOnTestnet(
383
+ projectRoot: string,
384
+ configHash: string,
385
+ cid: string,
386
+ endpoint: string,
387
+ serviceDescriptors: import('../../types/agent').ServiceDescriptor[],
388
+ output: Output,
389
+ ): Promise<string | undefined> {
390
+ const { resolvePrivateKey } = await import('../../wallet/keystore');
391
+ const { ethers } = await import('ethers');
392
+ const { getNetwork } = await import('../../config/networks');
393
+ const { AutoWalletProvider } = await import('../../wallet/AutoWalletProvider');
394
+ const { buildActivationBatch, buildTestnetMintBatch } = await import('../../wallet/aa/TransactionBatcher');
395
+ const { getOnChainAgentState, detectLazyPublishScenario } = await import('../../ACTPClient');
396
+
397
+ const privateKey = await resolvePrivateKey(projectRoot);
398
+ if (!privateKey) {
399
+ throw new Error('No wallet found. Cannot activate on testnet.');
400
+ }
401
+
402
+ const networkConfig = getNetwork('base-sepolia');
403
+ if (!networkConfig.aa || !networkConfig.contracts.agentRegistry) {
404
+ throw new Error('Testnet AA or AgentRegistry not configured.');
405
+ }
406
+
407
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
408
+ const signer = new ethers.Wallet(privateKey, provider);
409
+
410
+ const autoWallet = await AutoWalletProvider.create({
411
+ signer,
412
+ provider,
413
+ chainId: networkConfig.chainId,
414
+ actpKernelAddress: networkConfig.contracts.actpKernel,
415
+ bundler: {
416
+ primaryUrl: networkConfig.aa.bundlerUrls.coinbase,
417
+ backupUrl: networkConfig.aa.bundlerUrls.pimlico,
418
+ },
419
+ paymaster: {
420
+ primaryUrl: networkConfig.aa.paymasterUrls.coinbase,
421
+ backupUrl: networkConfig.aa.paymasterUrls.pimlico,
422
+ },
423
+ });
424
+
425
+ const smartWalletAddress = autoWallet.getAddress();
426
+ output.info(`Smart Wallet: ${smartWalletAddress}`);
427
+
428
+ // Check on-chain state to determine activation scenario
429
+ const onChainState = await getOnChainAgentState(
430
+ provider, networkConfig.contracts.agentRegistry, smartWalletAddress
431
+ );
432
+ const scenario = detectLazyPublishScenario(onChainState, {
433
+ version: 1, configHash, cid, endpoint, serviceDescriptors, createdAt: new Date().toISOString(),
434
+ });
435
+
436
+ if (scenario === 'C' || scenario === 'none') {
437
+ // Already up-to-date on-chain — no activation needed
438
+ return undefined;
439
+ }
440
+
441
+ // Build activation calls
442
+ const activationCalls = buildActivationBatch({
443
+ scenario,
444
+ agentRegistryAddress: networkConfig.contracts.agentRegistry,
445
+ cid,
446
+ configHash,
447
+ listed: true,
448
+ ...(scenario === 'A' ? { endpoint, serviceDescriptors } : {}),
449
+ });
450
+
451
+ // Always mint test USDC on testnet (SPEC: "always")
452
+ const mintCalls = buildTestnetMintBatch(
453
+ networkConfig.contracts.usdc,
454
+ smartWalletAddress,
455
+ '1000000000', // 1000 USDC
456
+ );
457
+
458
+ const allCalls = [...activationCalls, ...mintCalls];
459
+ const txRequests = allCalls.map((c) => ({
460
+ to: c.target,
461
+ data: c.data,
462
+ value: c.value.toString(),
463
+ }));
464
+
465
+ output.info(`Submitting ${allCalls.length}-call UserOp...`);
466
+ const receipt = await autoWallet.sendBatchTransaction(txRequests);
467
+
468
+ if (!receipt.success) {
469
+ throw new Error(`Testnet activation UserOp failed: ${receipt.hash}`);
470
+ }
471
+
472
+ output.success('Minted 1,000 test USDC to Smart Wallet');
473
+ return receipt.hash;
474
+ }
475
+
@@ -27,6 +27,7 @@ export function createRegisterCommand(): Command {
27
27
  const cmd = new Command('register')
28
28
  .description('Register agent on AgentRegistry for gas-free transactions')
29
29
  .option('--endpoint <url>', 'Service endpoint URL (overrides AGIRAILS.md)')
30
+ .option('--force-legacy', 'Bypass deprecation and run legacy registration')
30
31
  .option('--json', 'Output as JSON')
31
32
  .option('-q, --quiet', 'Minimal output')
32
33
  .action(async (options) => {
@@ -34,6 +35,19 @@ export function createRegisterCommand(): Command {
34
35
  options.json ? 'json' : options.quiet ? 'quiet' : 'human'
35
36
  );
36
37
 
38
+ // Deprecation warning
39
+ output.warning(
40
+ 'DEPRECATED: "actp register" is deprecated. Use "actp publish" instead.\n' +
41
+ ' "actp publish" saves config locally. On-chain activation happens\n' +
42
+ ' automatically on your first payment (lazy publish).'
43
+ );
44
+ output.blank();
45
+
46
+ if (!options.forceLegacy) {
47
+ output.print('To proceed anyway, use: actp register --force-legacy');
48
+ return;
49
+ }
50
+
37
51
  try {
38
52
  await runRegister(options, output);
39
53
  } catch (error) {
@@ -38,7 +38,7 @@ export interface CLIConfig {
38
38
  /** AIP-12: Wallet type — 'auto' (Smart Wallet, gasless) or 'eoa' (traditional) */
39
39
  wallet?: 'auto' | 'eoa';
40
40
 
41
- /** AIP-12: Smart Wallet address (set when wallet=auto, used by `actp register`) */
41
+ /** AIP-12: Smart Wallet address (set when wallet=auto, used by `actp publish`) */
42
42
  smartWallet?: string;
43
43
 
44
44
  /** AIP-12: Whether agent is registered on AgentRegistry */
@@ -83,9 +83,13 @@ export const CONFIG_DEFAULTS: CLIConfig = {
83
83
  // ============================================================================
84
84
 
85
85
  /**
86
- * Get the .actp directory path
86
+ * Get the .actp directory path.
87
+ * Respects ACTP_DIR env var override for custom locations.
87
88
  */
88
89
  export function getActpDir(projectRoot: string = process.cwd()): string {
90
+ if (process.env.ACTP_DIR) {
91
+ return process.env.ACTP_DIR;
92
+ }
89
93
  return path.join(projectRoot, '.actp');
90
94
  }
91
95
 
@@ -143,6 +147,11 @@ export function loadConfig(projectRoot: string = process.cwd()): CLIConfig {
143
147
  console.warn('\x1b[33m%s\x1b[0m', ' Run: export ACTP_PRIVATE_KEY=<your-key>');
144
148
  }
145
149
 
150
+ // Config migration: strip deprecated `registered` field (lazy publish)
151
+ if ('registered' in config) {
152
+ delete (config as unknown as Record<string, unknown>).registered;
153
+ }
154
+
146
155
  return config;
147
156
  } catch (error) {
148
157
  if (error instanceof SyntaxError) {