@agirails/sdk 2.3.1 → 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 (77) 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 +1 -1
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +82 -237
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/publish.d.ts +11 -3
  15. package/dist/cli/commands/publish.d.ts.map +1 -1
  16. package/dist/cli/commands/publish.js +319 -80
  17. package/dist/cli/commands/publish.js.map +1 -1
  18. package/dist/cli/commands/register.d.ts.map +1 -1
  19. package/dist/cli/commands/register.js +10 -0
  20. package/dist/cli/commands/register.js.map +1 -1
  21. package/dist/cli/utils/config.d.ts +17 -2
  22. package/dist/cli/utils/config.d.ts.map +1 -1
  23. package/dist/cli/utils/config.js +9 -1
  24. package/dist/cli/utils/config.js.map +1 -1
  25. package/dist/cli/utils/wallet.d.ts +31 -0
  26. package/dist/cli/utils/wallet.d.ts.map +1 -0
  27. package/dist/cli/utils/wallet.js +114 -0
  28. package/dist/cli/utils/wallet.js.map +1 -0
  29. package/dist/config/pendingPublish.d.ts +79 -0
  30. package/dist/config/pendingPublish.d.ts.map +1 -0
  31. package/dist/config/pendingPublish.js +167 -0
  32. package/dist/config/pendingPublish.js.map +1 -0
  33. package/dist/config/publishPipeline.d.ts +33 -0
  34. package/dist/config/publishPipeline.d.ts.map +1 -1
  35. package/dist/config/publishPipeline.js +33 -2
  36. package/dist/config/publishPipeline.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +7 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/wallet/AutoWalletProvider.d.ts +2 -1
  42. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  43. package/dist/wallet/AutoWalletProvider.js +6 -2
  44. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  45. package/dist/wallet/IWalletProvider.d.ts +4 -2
  46. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  47. package/dist/wallet/aa/BundlerClient.d.ts.map +1 -1
  48. package/dist/wallet/aa/BundlerClient.js +2 -1
  49. package/dist/wallet/aa/BundlerClient.js.map +1 -1
  50. package/dist/wallet/aa/DualNonceManager.d.ts +2 -1
  51. package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
  52. package/dist/wallet/aa/DualNonceManager.js +16 -5
  53. package/dist/wallet/aa/DualNonceManager.js.map +1 -1
  54. package/dist/wallet/aa/PaymasterClient.d.ts.map +1 -1
  55. package/dist/wallet/aa/PaymasterClient.js +2 -1
  56. package/dist/wallet/aa/PaymasterClient.js.map +1 -1
  57. package/dist/wallet/aa/TransactionBatcher.d.ts +54 -0
  58. package/dist/wallet/aa/TransactionBatcher.d.ts.map +1 -1
  59. package/dist/wallet/aa/TransactionBatcher.js +67 -1
  60. package/dist/wallet/aa/TransactionBatcher.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/ACTPClient.ts +265 -49
  63. package/src/adapters/BasicAdapter.ts +48 -12
  64. package/src/cli/commands/init.ts +81 -281
  65. package/src/cli/commands/publish.ts +354 -87
  66. package/src/cli/commands/register.ts +14 -0
  67. package/src/cli/utils/config.ts +32 -2
  68. package/src/cli/utils/wallet.ts +109 -0
  69. package/src/config/pendingPublish.ts +226 -0
  70. package/src/config/publishPipeline.ts +82 -1
  71. package/src/index.ts +8 -0
  72. package/src/wallet/AutoWalletProvider.ts +7 -2
  73. package/src/wallet/IWalletProvider.ts +4 -2
  74. package/src/wallet/aa/BundlerClient.ts +2 -1
  75. package/src/wallet/aa/DualNonceManager.ts +19 -9
  76. package/src/wallet/aa/PaymasterClient.ts +2 -1
  77. package/src/wallet/aa/TransactionBatcher.ts +113 -0
@@ -10,7 +10,6 @@
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';
14
13
  import { Command } from 'commander';
15
14
  import {
16
15
  saveConfig,
@@ -21,6 +20,7 @@ import {
21
20
  CLIMode,
22
21
  } from '../utils/config';
23
22
  import { Output, ExitCode } from '../utils/output';
23
+ import { generateWallet, computeSmartWalletInit } from '../utils/wallet';
24
24
  import { MockStateManager } from '../../runtime/MockStateManager';
25
25
 
26
26
  // ============================================================================
@@ -40,13 +40,13 @@ export function createInitCommand(): Command {
40
40
  .option('--price <usdc>', 'Base price in USDC (default: 1)')
41
41
  .option('--json', 'Output as JSON')
42
42
  .option('-q, --quiet', 'Minimal output')
43
- .action(async (options) => {
43
+ .action(async (options, command) => {
44
44
  const output = new Output(
45
45
  options.json ? 'json' : options.quiet ? 'quiet' : 'human'
46
46
  );
47
47
 
48
48
  try {
49
- await runInit(options, output);
49
+ await runInit(options, output, command);
50
50
  } catch (error) {
51
51
  output.errorResult({
52
52
  code: 'INIT_FAILED',
@@ -76,7 +76,7 @@ interface InitOptions {
76
76
  price?: string;
77
77
  }
78
78
 
79
- async function runInit(options: InitOptions, output: Output): Promise<void> {
79
+ async function runInit(options: InitOptions, output: Output, cmd?: Command): Promise<void> {
80
80
  const projectRoot = process.cwd();
81
81
 
82
82
  // Check if already initialized
@@ -87,6 +87,65 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
87
87
  );
88
88
  }
89
89
 
90
+ // ── AGIRAILS.md pre-fill ──────────────────────────────────────────────
91
+ const agirailsMdPath = path.join(projectRoot, 'AGIRAILS.md');
92
+ let mdConfig: Record<string, unknown> | null = null;
93
+
94
+ if (fs.existsSync(agirailsMdPath)) {
95
+ try {
96
+ const { parseAgirailsMd } = await import('../../config/agirailsmd');
97
+ const parsed = parseAgirailsMd(fs.readFileSync(agirailsMdPath, 'utf-8'));
98
+ mdConfig = parsed.frontmatter;
99
+ } catch {
100
+ output.warning('Found AGIRAILS.md but could not parse it — ignoring');
101
+ }
102
+ }
103
+
104
+ // Helper: true when the user explicitly passed a flag on the CLI
105
+ const isExplicit = (flag: string): boolean =>
106
+ cmd?.getOptionValueSource(flag) === 'cli';
107
+
108
+ // Apply AGIRAILS.md values where the user didn't set an explicit flag
109
+ if (mdConfig) {
110
+ // network → mode mapping
111
+ if (!isExplicit('mode') && mdConfig.network) {
112
+ const net = String(mdConfig.network);
113
+ if (net === 'base-sepolia' || net === 'testnet') options.mode = 'testnet';
114
+ else if (net === 'base-mainnet' || net === 'mainnet') options.mode = 'mainnet';
115
+ else if (net === 'mock') options.mode = 'mock';
116
+ }
117
+
118
+ // intent
119
+ if (!isExplicit('intent') && mdConfig.intent) {
120
+ options.intent = String(mdConfig.intent);
121
+ }
122
+
123
+ // capabilities → service (first capability)
124
+ if (!isExplicit('service') && Array.isArray(mdConfig.capabilities) && mdConfig.capabilities.length > 0) {
125
+ options.service = String(mdConfig.capabilities[0]);
126
+ }
127
+
128
+ // price
129
+ if (!isExplicit('price') && mdConfig.price != null) {
130
+ options.price = String(mdConfig.price);
131
+ }
132
+
133
+ // Log what we pre-filled
134
+ const lines: string[] = [];
135
+ if (mdConfig.network) lines.push(` Mode: ${options.mode}`);
136
+ if (mdConfig.name) lines.push(` Agent: ${String(mdConfig.name)}`);
137
+ if (mdConfig.intent) lines.push(` Intent: ${options.intent || mdConfig.intent}`);
138
+ if (Array.isArray(mdConfig.capabilities)) lines.push(` Capabilities: ${mdConfig.capabilities.join(', ')}`);
139
+ if (mdConfig.price != null) lines.push(` Price: $${mdConfig.price} USDC`);
140
+
141
+ output.info('Found AGIRAILS.md \u2014 using config from file');
142
+ for (const line of lines) {
143
+ output.print(line);
144
+ }
145
+ output.blank();
146
+ }
147
+ // ── End AGIRAILS.md pre-fill ──────────────────────────────────────────
148
+
90
149
  // Validate mode
91
150
  const validModes: CLIMode[] = ['mock', 'testnet', 'mainnet'];
92
151
  if (!validModes.includes(options.mode as CLIMode)) {
@@ -108,7 +167,6 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
108
167
  // Get or generate address
109
168
  let address = options.address;
110
169
  let smartWalletAddress: string | undefined;
111
- let didRegister = false;
112
170
  if (!address) {
113
171
  if (mode === 'mock') {
114
172
  // Generate a random address for mock mode
@@ -123,16 +181,11 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
123
181
  if (walletType === 'auto') {
124
182
  // Compute Smart Wallet address from signer
125
183
  smartWalletAddress = await computeSmartWalletInit(eoaAddress, mode, output);
184
+ address = smartWalletAddress;
126
185
 
127
- // Y/N: Register for gas-free transactions?
128
- const shouldRegister = await promptRegister(output);
129
- if (shouldRegister) {
130
- didRegister = await runInlineRegistration(projectRoot, mode, output);
131
- }
132
-
133
- // address = Smart Wallet if registered, EOA if not
134
- // This ensures CLI commands (balance, tx list) show the correct address
135
- address = didRegister ? smartWalletAddress : eoaAddress;
186
+ output.info('');
187
+ output.info('Gas-free transactions enabled (activates on first payment)');
188
+ output.info('Next: run "actp publish" to publish your agent config');
136
189
  } else {
137
190
  address = eoaAddress;
138
191
  }
@@ -154,7 +207,14 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
154
207
  version: '1.0',
155
208
  ...(walletType !== 'mock' && { wallet: walletType as 'auto' | 'eoa' }),
156
209
  ...(smartWalletAddress && { smartWallet: smartWalletAddress.toLowerCase() }),
157
- ...(didRegister && { registered: true }),
210
+ // AGIRAILS.md-derived values (stored for downstream use)
211
+ ...(mdConfig && mdConfig.name ? { agentName: String(mdConfig.name) } : {}),
212
+ ...(mdConfig && mdConfig.intent ? { intent: String(mdConfig.intent) as 'earn' | 'pay' | 'both' } : {}),
213
+ ...(mdConfig && Array.isArray(mdConfig.capabilities) ? { capabilities: mdConfig.capabilities.map(String) } : {}),
214
+ ...(mdConfig && mdConfig.price != null ? { price: Number(mdConfig.price) } : {}),
215
+ ...(mdConfig && mdConfig.concurrency != null ? { concurrency: Number(mdConfig.concurrency) } : {}),
216
+ ...(mdConfig && mdConfig.payment_mode ? { paymentMode: String(mdConfig.payment_mode) as 'actp' | 'x402' | 'both' } : {}),
217
+ ...(mdConfig && mdConfig.budget != null ? { budget: Number(mdConfig.budget) } : {}),
158
218
  };
159
219
 
160
220
  // Save configuration
@@ -199,18 +259,12 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
199
259
 
200
260
  // Generate scaffold if requested
201
261
  if (options.scaffold) {
202
- await runScaffold(options, mode, output);
262
+ await runScaffold(options, mode, output, mdConfig);
203
263
  } else {
204
264
  output.blank();
205
265
  output.print('Next steps:');
206
- if (walletType === 'auto' && didRegister) {
207
- // Already registered ready to go
208
- output.print(' 1. Create a payment: actp pay <provider> <amount>');
209
- output.print(' 2. Check your balance: actp balance');
210
- output.print(' 3. List transactions: actp tx list');
211
- } else if (walletType === 'auto') {
212
- // Skipped registration — remind them
213
- output.print(' 1. Register for gas-free: actp register');
266
+ if (walletType === 'auto') {
267
+ output.print(' 1. Publish config: actp publish');
214
268
  output.print(' 2. Create a payment: actp pay <provider> <amount>');
215
269
  output.print(' 3. Check your balance: actp balance');
216
270
  } else {
@@ -223,261 +277,6 @@ async function runInit(options: InitOptions, output: Output): Promise<void> {
223
277
  }
224
278
  }
225
279
 
226
- // ============================================================================
227
- // Wallet Generation
228
- // ============================================================================
229
-
230
- async function generateWallet(actpDir: string, output: Output): Promise<string> {
231
- const { Wallet } = await import('ethers');
232
-
233
- const wallet = Wallet.createRandom();
234
-
235
- // Get password from env var or interactive prompt
236
- let password = process.env.ACTP_KEY_PASSWORD;
237
- if (!password) {
238
- password = await promptPassword();
239
- }
240
-
241
- if (!password || password.length < 8) {
242
- throw new Error(
243
- 'Wallet password required (minimum 8 characters).\n' +
244
- 'Set ACTP_KEY_PASSWORD env var or enter when prompted.'
245
- );
246
- }
247
-
248
- // Encrypt with Keystore V3 (scrypt + AES-128-CTR)
249
- output.info('Encrypting wallet (this takes a few seconds)...');
250
- const keystore = await wallet.encrypt(password);
251
-
252
- // Save with restrictive permissions
253
- const keystorePath = path.join(actpDir, 'keystore.json');
254
- fs.writeFileSync(keystorePath, keystore, { mode: 0o600 });
255
-
256
- output.success('Key securely saved and encrypted');
257
- output.info(`Address: ${wallet.address}`);
258
- output.warning('Back up your password — it cannot be recovered.');
259
- output.info('');
260
- output.info('To start your agent:');
261
- output.info(' export ACTP_KEY_PASSWORD="your-password"');
262
- output.info(' npx ts-node agent.ts');
263
-
264
- return wallet.address;
265
- }
266
-
267
- /**
268
- * Compute the Smart Wallet address for an EOA signer.
269
- * Uses CREATE2 counterfactual derivation — no deployment needed.
270
- */
271
- async function computeSmartWalletInit(
272
- eoaAddress: string,
273
- mode: string,
274
- output: Output
275
- ): Promise<string> {
276
- const { ethers } = await import('ethers');
277
- const { getNetwork } = await import('../../config/networks');
278
- const { computeSmartWalletAddress } = await import('../../wallet/aa/UserOpBuilder');
279
-
280
- const network = mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
281
- const networkConfig = getNetwork(network);
282
- const rpcUrl = networkConfig.rpcUrl;
283
- const provider = new ethers.JsonRpcProvider(rpcUrl);
284
-
285
- output.info('Computing Smart Wallet address...');
286
- const smartWalletAddress = await computeSmartWalletAddress(eoaAddress, provider);
287
-
288
- output.success(`Smart Wallet: ${smartWalletAddress}`);
289
- output.info('Gas-free transactions enabled (requires registration)');
290
- output.info('Register with: actp register');
291
-
292
- return smartWalletAddress;
293
- }
294
-
295
- /**
296
- * Ask user if they want to register for gas-free transactions.
297
- * Non-TTY (piped/agent) defaults to yes.
298
- */
299
- async function promptRegister(output: Output): Promise<boolean> {
300
- output.blank();
301
- output.print('Register for gas-free transactions? (recommended)');
302
- output.print(' Your agent gets a Smart Wallet with sponsored gas — no ETH needed.');
303
- output.print(' Requires on-chain registration on AgentRegistry.');
304
- output.blank();
305
-
306
- if (!process.stdin.isTTY) {
307
- output.info('Non-interactive mode: auto-registering');
308
- return true;
309
- }
310
-
311
- const rl = readline.createInterface({
312
- input: process.stdin,
313
- output: process.stdout,
314
- });
315
-
316
- return new Promise((resolve) => {
317
- rl.question(' Register now? [Y/n] ', (answer) => {
318
- rl.close();
319
- const trimmed = answer.trim().toLowerCase();
320
- resolve(trimmed === '' || trimmed === 'y' || trimmed === 'yes');
321
- });
322
- });
323
- }
324
-
325
- /**
326
- * Run inline registration during init.
327
- * Reuses the same logic as `actp register` — parses AGIRAILS.md,
328
- * builds gasless UserOp (testnet: register + mint 1000 USDC).
329
- *
330
- * Returns true if registration succeeded, false on failure (non-fatal).
331
- */
332
- async function runInlineRegistration(
333
- projectRoot: string,
334
- mode: string,
335
- output: Output
336
- ): Promise<boolean> {
337
- try {
338
- const { resolvePrivateKey } = await import('../../wallet/keystore');
339
- const privateKey = await resolvePrivateKey(projectRoot);
340
- if (!privateKey) {
341
- output.warning('Could not load wallet key. Run "actp register" later.');
342
- return false;
343
- }
344
-
345
- const { parseAgirailsMd } = await import('../../config/agirailsmd');
346
- const { extractRegistrationParams } = await import('../../config/publishPipeline');
347
- const { ethers } = await import('ethers');
348
- const { getNetwork } = await import('../../config/networks');
349
- const { AutoWalletProvider } = await import('../../wallet/AutoWalletProvider');
350
- const { buildRegisterAgentBatch, buildTestnetInitBatch } = await import('../../wallet/aa/TransactionBatcher');
351
-
352
- // Parse AGIRAILS.md if present
353
- const agirailsMdPath = path.join(projectRoot, 'AGIRAILS.md');
354
- let endpoint = '';
355
- let serviceDescriptors;
356
-
357
- if (fs.existsSync(agirailsMdPath)) {
358
- const content = fs.readFileSync(agirailsMdPath, 'utf-8');
359
- const parsed = parseAgirailsMd(content);
360
- const regParams = extractRegistrationParams(parsed.frontmatter);
361
- endpoint = regParams.endpoint;
362
- serviceDescriptors = regParams.serviceDescriptors;
363
- output.info(`Parsed ${serviceDescriptors.length} service(s) from AGIRAILS.md`);
364
- } else {
365
- const serviceType = 'general';
366
- serviceDescriptors = [{
367
- serviceTypeHash: ethers.keccak256(ethers.toUtf8Bytes(serviceType)),
368
- serviceType,
369
- schemaURI: '',
370
- minPrice: 0n,
371
- maxPrice: 1_000_000_000n,
372
- avgCompletionTime: 3600,
373
- metadataCID: '',
374
- }];
375
- output.info('No AGIRAILS.md found. Using default "general" service.');
376
- }
377
-
378
- const network = mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
379
- const networkConfig = getNetwork(network);
380
-
381
- if (!networkConfig.aa || !networkConfig.contracts.agentRegistry) {
382
- output.warning('AA or AgentRegistry not configured. Run "actp register" later.');
383
- return false;
384
- }
385
-
386
- // Check for valid bundler/paymaster URL (CDP_API_KEY must be set)
387
- const cdpUrl = networkConfig.aa.bundlerUrls.coinbase;
388
- const hasPimlico = !!networkConfig.aa.bundlerUrls.pimlico;
389
- if (cdpUrl.endsWith('/') && !hasPimlico) {
390
- output.warning('CDP_API_KEY not set. Skipping registration.');
391
- output.info('Set CDP_API_KEY and run "actp register" later.');
392
- return false;
393
- }
394
-
395
- const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
396
- const signer = new ethers.Wallet(privateKey, provider);
397
-
398
- const autoWallet = await AutoWalletProvider.create({
399
- signer,
400
- provider,
401
- chainId: networkConfig.chainId,
402
- actpKernelAddress: networkConfig.contracts.actpKernel,
403
- bundler: {
404
- primaryUrl: networkConfig.aa.bundlerUrls.coinbase,
405
- backupUrl: networkConfig.aa.bundlerUrls.pimlico,
406
- },
407
- paymaster: {
408
- primaryUrl: networkConfig.aa.paymasterUrls.coinbase,
409
- backupUrl: networkConfig.aa.paymasterUrls.pimlico,
410
- },
411
- });
412
-
413
- const smartWalletAddress = autoWallet.getAddress();
414
-
415
- // Build batch
416
- let calls;
417
- if (mode === 'testnet') {
418
- output.info('Testnet: registering + minting 1000 test USDC in one gasless tx...');
419
- calls = buildTestnetInitBatch({
420
- agentRegistryAddress: networkConfig.contracts.agentRegistry,
421
- endpoint,
422
- serviceDescriptors,
423
- mockUsdcAddress: networkConfig.contracts.usdc,
424
- recipient: smartWalletAddress,
425
- mintAmount: '1000000000',
426
- });
427
- } else {
428
- output.info('Registering on AgentRegistry (gasless)...');
429
- calls = buildRegisterAgentBatch(
430
- networkConfig.contracts.agentRegistry,
431
- endpoint,
432
- serviceDescriptors
433
- );
434
- }
435
-
436
- const txRequests = calls.map((c) => ({
437
- to: c.target,
438
- data: c.data,
439
- value: c.value.toString(),
440
- }));
441
-
442
- const receipt = await autoWallet.sendBatchTransaction(txRequests);
443
-
444
- if (!receipt.success) {
445
- output.warning(`Registration failed (tx: ${receipt.hash}). Run "actp register" later.`);
446
- return false;
447
- }
448
-
449
- output.success('Agent registered on AgentRegistry');
450
- if (mode === 'testnet') {
451
- output.success('Minted 1,000 test USDC to Smart Wallet');
452
- }
453
- output.print(` Tx: ${receipt.hash}`);
454
- return true;
455
- } catch (error) {
456
- output.warning(`Registration failed: ${(error as Error).message}`);
457
- output.info('You can register later with: actp register');
458
- return false;
459
- }
460
- }
461
-
462
- async function promptPassword(): Promise<string> {
463
- // If not a TTY (e.g. piped or run by agent), skip prompt
464
- if (!process.stdin.isTTY) {
465
- return '';
466
- }
467
-
468
- const rl = readline.createInterface({
469
- input: process.stdin,
470
- output: process.stdout,
471
- });
472
-
473
- return new Promise((resolve) => {
474
- rl.question('Enter password for wallet encryption (min 8 chars): ', (answer) => {
475
- rl.close();
476
- resolve(answer.trim());
477
- });
478
- });
479
- }
480
-
481
280
  // ============================================================================
482
281
  // Scaffold
483
282
  // ============================================================================
@@ -486,6 +285,7 @@ async function runScaffold(
486
285
  options: InitOptions,
487
286
  mode: CLIMode,
488
287
  output: Output,
288
+ mdConfig?: Record<string, unknown> | null,
489
289
  ): Promise<void> {
490
290
  const validIntents: ScaffoldIntent[] = ['earn', 'pay', 'both'];
491
291
  const intent: ScaffoldIntent = (options.intent as ScaffoldIntent) || 'earn';
@@ -506,8 +306,8 @@ async function runScaffold(
506
306
  return;
507
307
  }
508
308
 
509
- // Derive agent name from directory
510
- const agentName = path.basename(process.cwd());
309
+ // Derive agent name: prefer AGIRAILS.md name, fallback to directory name
310
+ const agentName = (mdConfig?.name ? String(mdConfig.name) : null) || path.basename(process.cwd());
511
311
 
512
312
  // Get template and substitute variables
513
313
  const template = getTemplate(intent);