@kadi.build/core 0.0.1-alpha.12 → 0.0.1-alpha.14

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 (45) hide show
  1. package/dist/abilities/AbilityLoader.d.ts +136 -12
  2. package/dist/abilities/AbilityLoader.d.ts.map +1 -1
  3. package/dist/abilities/AbilityLoader.js +432 -64
  4. package/dist/abilities/AbilityLoader.js.map +1 -1
  5. package/dist/abilities/types.d.ts +119 -4
  6. package/dist/abilities/types.d.ts.map +1 -1
  7. package/dist/api/index.d.ts +1 -40
  8. package/dist/api/index.d.ts.map +1 -1
  9. package/dist/api/index.js +0 -57
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/client/KadiClient.d.ts.map +1 -1
  12. package/dist/client/KadiClient.js +65 -4
  13. package/dist/client/KadiClient.js.map +1 -1
  14. package/dist/config/ConfigLoader.d.ts.map +1 -1
  15. package/dist/config/ConfigLoader.js +1 -0
  16. package/dist/config/ConfigLoader.js.map +1 -1
  17. package/dist/config/ConfigResolver.d.ts.map +1 -1
  18. package/dist/config/ConfigResolver.js +1 -0
  19. package/dist/config/ConfigResolver.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/transports/NativeTransport.d.ts.map +1 -1
  25. package/dist/transports/NativeTransport.js +22 -0
  26. package/dist/transports/NativeTransport.js.map +1 -1
  27. package/dist/transports/StdioTransport.d.ts +17 -2
  28. package/dist/transports/StdioTransport.d.ts.map +1 -1
  29. package/dist/transports/StdioTransport.js +45 -10
  30. package/dist/transports/StdioTransport.js.map +1 -1
  31. package/dist/types/config.d.ts +48 -33
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/config.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js.map +1 -1
  37. package/dist/utils/LockfileResolver.d.ts +179 -28
  38. package/dist/utils/LockfileResolver.d.ts.map +1 -1
  39. package/dist/utils/LockfileResolver.js +380 -39
  40. package/dist/utils/LockfileResolver.js.map +1 -1
  41. package/dist/utils/legacyHelpers.d.ts +82 -0
  42. package/dist/utils/legacyHelpers.d.ts.map +1 -0
  43. package/dist/utils/legacyHelpers.js +226 -0
  44. package/dist/utils/legacyHelpers.js.map +1 -0
  45. package/package.json +1 -1
@@ -17,7 +17,8 @@ import { AbilityProxy } from './AbilityProxy.js';
17
17
  import { AbilityCache } from './AbilityCache.js';
18
18
  import { AbilityValidator } from './AbilityValidator.js';
19
19
  import { resolveAbilityConfig } from '../utils/LockfileResolver.js';
20
- import path from 'path';
20
+ import { join } from 'path';
21
+ import { readFileSync } from 'fs';
21
22
  /**
22
23
  * Ability Loader
23
24
  *
@@ -59,6 +60,45 @@ export class AbilityLoader {
59
60
  * - Same name, different brokers
60
61
  */
61
62
  cache = new AbilityCache();
63
+ /**
64
+ * Explicit project root (provided by KadiClient)
65
+ *
66
+ * This is the directory where KADI will look for abilities.
67
+ * - If provided: use this directory
68
+ * - If not provided: default to process.cwd()
69
+ *
70
+ * This makes context explicit instead of inferred.
71
+ */
72
+ explicitProjectRoot;
73
+ /**
74
+ * Create an AbilityLoader
75
+ *
76
+ * **AbilityLoader** is responsible for finding, loading, and caching abilities.
77
+ *
78
+ * @param options - Loader options
79
+ * @param options.projectRoot - Explicit project root (overrides detection)
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // With explicit project root (CLI plugin)
84
+ * const loader = new AbilityLoader({
85
+ * projectRoot: '/opt/homebrew/lib/node_modules/@kadi.build/cli'
86
+ * });
87
+ *
88
+ * // Without explicit project root (user project)
89
+ * const loader = new AbilityLoader({});
90
+ * ```
91
+ */
92
+ constructor(options) {
93
+ // Store the explicit project root for later use
94
+ // This will be used in detectProjectRoot() method
95
+ this.explicitProjectRoot = options?.projectRoot;
96
+ if (process.env.KADI_DEBUG) {
97
+ console.log('[DEBUG] AbilityLoader created:', {
98
+ explicitProjectRoot: this.explicitProjectRoot || 'none (will use default)'
99
+ });
100
+ }
101
+ }
62
102
  /**
63
103
  * Load an ability with explicit transport
64
104
  *
@@ -95,7 +135,19 @@ export class AbilityLoader {
95
135
  * ```
96
136
  */
97
137
  async load(name, transport, options = {}, brokerClient) {
98
- // Step 1: Check cache first (avoid duplicate loading)
138
+ // Step 1: Detect project root
139
+ // This tells us where to look for abilities
140
+ // - CLI plugins: Uses explicitProjectRoot from constructor
141
+ // - User projects: Uses process.cwd()
142
+ const projectRoot = this.detectProjectRoot();
143
+ if (process.env.KADI_DEBUG) {
144
+ console.log('[DEBUG] Loading ability with project root:', {
145
+ abilityName: name,
146
+ transport,
147
+ projectRoot
148
+ });
149
+ }
150
+ // Step 2: Check cache first (avoid duplicate loading)
99
151
  const cacheKey = {
100
152
  name,
101
153
  transport,
@@ -107,17 +159,19 @@ export class AbilityLoader {
107
159
  // Cache hit! Return existing ability
108
160
  return cached;
109
161
  }
110
- // Step 2: Create appropriate transport
111
- const transportInstance = this.createTransport(name, transport, options, brokerClient);
112
- // Step 3: Connect to ability
162
+ // Step 3: Create appropriate transport with project root context
163
+ // The transport will use projectRoot to find the ability
164
+ const transportInstance = this.createTransport(name, transport, options, projectRoot, // Pass projectRoot (was callerFilePath before)
165
+ brokerClient);
166
+ // Step 4: Connect to ability
113
167
  await transportInstance.connect();
114
- // Step 4: Create proxy for natural method calling
168
+ // Step 5: Create proxy for natural method calling
115
169
  const ability = AbilityProxy.create(name, transportInstance, transport);
116
- // Step 5: Validate if requested
170
+ // Step 6: Validate if requested
117
171
  if (options.validate) {
118
172
  AbilityValidator.validate(ability);
119
173
  }
120
- // Step 6: Cache for future use
174
+ // Step 7: Cache for future use
121
175
  this.cache.set(cacheKey, ability);
122
176
  return ability;
123
177
  }
@@ -188,29 +242,122 @@ export class AbilityLoader {
188
242
  cleanup() {
189
243
  return this.cache.cleanup();
190
244
  }
245
+ /**
246
+ * Detect project root directory
247
+ *
248
+ * **Strategy** (simple and explicit):
249
+ * 1. Use explicit override (constructor option or env var)
250
+ * 2. Fall back to process.cwd() (default behavior, like professor's approach)
251
+ *
252
+ * **Why This Approach?**
253
+ * - **Explicit over implicit**: Plugins tell us where to load from
254
+ * - **Simple**: No stack parsing, no complex inference
255
+ * - **Reliable**: Works in all environments (dev, prod, bundlers, etc.)
256
+ * - **Escape hatch**: Env var for edge cases
257
+ *
258
+ * **How It's Used**:
259
+ * - CLI plugins: Pass `ctx.core.getKadiInstallPath()` via constructor
260
+ * - User projects: Don't pass anything, uses process.cwd()
261
+ * - Edge cases: Set KADI_PROJECT_ROOT env var
262
+ *
263
+ * @returns Detected project root directory
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * // Example 1: CLI plugin (explicit)
268
+ * const loader = new AbilityLoader({
269
+ * projectRoot: '/opt/homebrew/lib/node_modules/@kadi.build/cli'
270
+ * });
271
+ * const root = loader.detectProjectRoot();
272
+ * // Returns: '/opt/homebrew/lib/node_modules/@kadi.build/cli'
273
+ *
274
+ * // Example 2: User project (default)
275
+ * const loader = new AbilityLoader({});
276
+ * const root = loader.detectProjectRoot();
277
+ * // Returns: process.cwd() (e.g., '/Users/kassi/Repositories/KADI/gateway-agent')
278
+ *
279
+ * // Example 3: Env var override (escape hatch)
280
+ * process.env.KADI_PROJECT_ROOT = '/app';
281
+ * const loader = new AbilityLoader({});
282
+ * const root = loader.detectProjectRoot();
283
+ * // Returns: '/app'
284
+ * ```
285
+ */
286
+ detectProjectRoot() {
287
+ // ====================================================================
288
+ // Layer 1: Explicit Override
289
+ // ====================================================================
290
+ // Priority: Constructor option > Environment variable
291
+ //
292
+ // This gives us full control over where abilities are loaded from.
293
+ // - CLI plugins use constructor option (explicit)
294
+ // - Edge cases use env var (escape hatch)
295
+ if (this.explicitProjectRoot) {
296
+ // Constructor option provided
297
+ // This happens when CLI plugins pass ctx.core.getKadiInstallPath()
298
+ if (process.env.KADI_DEBUG) {
299
+ console.log('[DEBUG] Using explicit project root from constructor:', this.explicitProjectRoot);
300
+ }
301
+ return this.explicitProjectRoot;
302
+ }
303
+ const envRoot = process.env.KADI_PROJECT_ROOT;
304
+ if (envRoot) {
305
+ // Environment variable set
306
+ // This is the escape hatch for edge cases
307
+ if (process.env.KADI_DEBUG) {
308
+ console.log('[DEBUG] Using explicit project root from env:', envRoot);
309
+ }
310
+ return envRoot;
311
+ }
312
+ // ====================================================================
313
+ // Layer 2: Default Behavior
314
+ // ====================================================================
315
+ // Use process.cwd() (current working directory)
316
+ //
317
+ // This is the default behavior for user projects.
318
+ // It works the same way as professor's original approach.
319
+ //
320
+ // When you run: node src/server.js
321
+ // - process.cwd() returns the directory you ran the command from
322
+ // - Usually this is the project root
323
+ // - Abilities are in {cwd}/abilities/
324
+ const cwd = process.cwd();
325
+ if (process.env.KADI_DEBUG) {
326
+ console.log('[DEBUG] Using default project root (process.cwd):', cwd);
327
+ }
328
+ return cwd;
329
+ }
191
330
  /**
192
331
  * Create appropriate transport instance
193
332
  *
194
- * Clean factory pattern - delegates to specialized creation methods.
195
- * Each transport type has its own dedicated method with proper validation.
333
+ * Factory pattern that creates the right transport based on type.
334
+ * Each transport type (native, stdio, broker) has its own creation method.
196
335
  *
197
336
  * @param name - Ability name
198
- * @param transport - Transport type (from inference)
199
- * @param options - Load options
337
+ * @param transport - Transport type (native, stdio, or broker)
338
+ * @param options - Load options (transport-specific)
200
339
  * @param brokerClient - Broker client (for broker transport)
340
+ * @param projectRoot - Project root directory (where to find abilities)
201
341
  * @returns Transport instance ready to connect
202
342
  *
203
343
  * @throws {KadiError} If transport creation fails
204
344
  */
205
- createTransport(name, transport, options, brokerClient) {
345
+ createTransport(name, transport, options, projectRoot, // ← Changed from callerFilePath?: string (moved before optional params)
346
+ brokerClient) {
206
347
  // Delegate to appropriate transport creation method
348
+ // Pass projectRoot to enable resolution from correct project location
207
349
  switch (transport) {
208
350
  case 'broker':
351
+ // Broker transport doesn't need projectRoot (abilities are remote)
209
352
  return this.createBrokerTransport(name, options, brokerClient);
210
353
  case 'native':
211
- return this.createNativeTransport(name, options);
354
+ // Native transport loads abilities from local filesystem
355
+ // Needs projectRoot to find abilities/ folder
356
+ return this.createNativeTransport(name, options, projectRoot);
212
357
  case 'stdio':
213
- return this.createStdioTransport(name, options);
358
+ // Stdio transport runs abilities as child processes
359
+ // Needs projectRoot to find ability executables
360
+ return this.createStdioTransport(name, options, projectRoot);
214
361
  default:
215
362
  // TypeScript exhaustiveness check ensures this is unreachable
216
363
  const never = transport;
@@ -262,35 +409,52 @@ export class AbilityLoader {
262
409
  }, brokerClient);
263
410
  }
264
411
  /**
265
- * Create native transport with path auto-resolution
412
+ * Create native transport with project root context
266
413
  *
267
- * If path is provided, uses it directly (backwards compatible).
268
- * If path is NOT provided, auto-resolves from agent-lock.json.
414
+ * Native transport loads abilities directly from the filesystem.
415
+ *
416
+ * Resolution strategy:
417
+ * 1. If explicit path provided → use it directly
418
+ * 2. Otherwise → auto-resolve from projectRoot using lockfile
269
419
  *
270
420
  * @param name - Ability name
271
421
  * @param options - Load options (native-specific)
422
+ * @param projectRoot - Project root directory (from detectProjectRoot)
272
423
  * @returns Native transport instance
273
424
  *
274
- * @throws {KadiError} If path is missing and auto-resolution fails
425
+ * @throws {KadiError} If path cannot be resolved
275
426
  */
276
- createNativeTransport(name, options) {
427
+ createNativeTransport(name, options, projectRoot // ← Changed from callerFilePath?: string
428
+ ) {
277
429
  // Step 1: Extract native-specific options (type-safe!)
278
430
  const nativeOptions = options;
279
- // Step 2: Determine ability path
431
+ // Step 2: Determine ability path and version
280
432
  let abilityPath;
281
433
  let abilityVersion = options.version;
282
434
  if (nativeOptions.path) {
283
- // Explicit path provided - use it (backwards compatible)
435
+ // Explicit path provided - use it directly
436
+ // This allows users to override auto-resolution if needed
284
437
  abilityPath = nativeOptions.path;
438
+ if (process.env.KADI_DEBUG) {
439
+ console.log('[DEBUG] Using explicit ability path:', abilityPath);
440
+ }
285
441
  }
286
442
  else {
287
- // No path provided - auto-resolve from lockfile
443
+ // No path provided - auto-resolve using project root
288
444
  try {
289
- const config = resolveAbilityConfig(name);
445
+ // Pass projectRoot directly to resolver
446
+ // Resolver will look in {projectRoot}/agent-lock.json
447
+ const config = resolveAbilityConfig(name, projectRoot);
290
448
  abilityPath = config.path;
291
449
  abilityVersion = config.version;
292
- // Log for debugging (if needed)
293
- // console.debug(`Auto-resolved ${name} to ${abilityPath}`);
450
+ if (process.env.KADI_DEBUG) {
451
+ console.log('[DEBUG] Auto-resolved ability path:', {
452
+ name,
453
+ projectRoot,
454
+ resolvedPath: abilityPath,
455
+ version: abilityVersion
456
+ });
457
+ }
294
458
  }
295
459
  catch (error) {
296
460
  // Re-throw lockfile resolution errors with context
@@ -300,100 +464,304 @@ export class AbilityLoader {
300
464
  throw new KadiError(`Failed to auto-resolve ability '${name}'`, ErrorCode.ABILITY_NOT_FOUND, 404, {
301
465
  abilityName: name,
302
466
  transport: 'native',
303
- reason: 'Could not auto-resolve path from lockfile',
467
+ projectRoot,
468
+ reason: 'Could not auto-resolve path from project root',
304
469
  originalError: error instanceof Error ? error.message : String(error),
305
470
  suggestion: 'Provide explicit path or run `kadi install`',
306
471
  example: 'await client.load("calculator", "native", { path: "./abilities/calculator" })',
307
472
  alternatives: [
308
473
  'Install ability: kadi install ' + name,
309
474
  'Provide explicit path in options',
310
- 'Check that agent-lock.json exists'
475
+ 'Check that agent-lock.json exists in project root'
311
476
  ]
312
477
  });
313
478
  }
314
479
  }
315
- // Step 3: Create NativeTransport with resolved/provided path
480
+ // Step 3: Read agent.json to get entrypoint
481
+ // Abilities can specify their entry file in agent.json (e.g., dist/kadi-ability.js)
482
+ let entryPoint = 'index.js'; // Default fallback
483
+ try {
484
+ const agentJsonPath = join(abilityPath, 'agent.json');
485
+ const agentJson = JSON.parse(readFileSync(agentJsonPath, 'utf8'));
486
+ if (agentJson.entrypoint) {
487
+ entryPoint = agentJson.entrypoint;
488
+ }
489
+ }
490
+ catch (error) {
491
+ // If agent.json doesn't exist or can't be read, use default
492
+ // This maintains backwards compatibility with abilities that don't have agent.json
493
+ }
494
+ // Step 4: Create NativeTransport with resolved path and entrypoint
316
495
  return new NativeTransport({
317
496
  abilityName: name,
318
497
  abilityPath,
319
498
  abilityVersion,
499
+ entryPoint,
320
500
  timeout: options.timeout
321
- // entryPoint defaults to 'index.js' in NativeTransport constructor
322
501
  });
323
502
  }
324
503
  /**
325
- * Create stdio transport with command auto-resolution
504
+ * Create stdio transport with context-aware command resolution
505
+ *
506
+ * Resolution strategy:
507
+ * 1. If explicit command provided → use it directly
508
+ * 2. Otherwise → auto-resolve from caller's project context
509
+ *
510
+ * **Context-Aware Resolution:**
511
+ * - Uses callerFilePath to find the correct project root
512
+ * - CLI plugins resolve from CLI's abilities/
513
+ * - User projects resolve from project's abilities/
514
+ *
515
+ * @param name - Ability name
516
+ * @param options - Load options (stdio-specific)
517
+ * @param callerFilePath - File path where load() was called from
518
+ * @returns Stdio transport instance
519
+ *
520
+ * @throws {KadiError} If command cannot be resolved
521
+ */
522
+ /**
523
+ * Create stdio transport with auto-resolution support
524
+ *
525
+ * **Two modes of operation**:
326
526
  *
327
- * If command is provided, uses it directly (backwards compatible).
328
- * If command is NOT provided, auto-resolves from agent-lock.json.
527
+ * 1. **Auto-resolution** (no command provided):
528
+ * - Resolves ability path using resolveAbilityConfig()
529
+ * - Reads ability's agent.json
530
+ * - Extracts scripts.start command
531
+ * - Spawns child process with shell: true
532
+ * - Sets cwd to ability directory
533
+ * - Injects KADI_* environment variables
534
+ *
535
+ * 2. **Explicit command** (command provided):
536
+ * - Uses provided command/args directly
537
+ * - Developer has full control
538
+ * - Still injects KADI_* environment variables
329
539
  *
330
540
  * @param name - Ability name
331
541
  * @param options - Load options (stdio-specific)
542
+ * @param projectRoot - Project root directory (from detectProjectRoot)
332
543
  * @returns Stdio transport instance
333
544
  *
334
- * @throws {KadiError} If command is missing and auto-resolution fails
545
+ * @throws {KadiError} If command cannot be resolved or scripts.start missing
546
+ *
547
+ * @example
548
+ * ```typescript
549
+ * // Auto-resolution mode
550
+ * const transport = this.createStdioTransport('calculator', {}, '/project/root');
551
+ * // Resolves path, reads agent.json, uses scripts.start
552
+ *
553
+ * // Explicit mode
554
+ * const transport = this.createStdioTransport('calculator', {
555
+ * command: 'node',
556
+ * args: ['dist/index.js']
557
+ * }, '/project/root');
558
+ * ```
335
559
  */
336
- createStdioTransport(name, options) {
337
- // Step 1: Extract stdio-specific options (type-safe!)
560
+ createStdioTransport(name, options, projectRoot) {
561
+ // ====================================================================
562
+ // STEP 1: Extract stdio-specific options (type-safe!)
563
+ // ====================================================================
338
564
  const stdioOptions = options;
339
- // Step 2: Determine command and args
565
+ // ====================================================================
566
+ // STEP 2: Declare variables for transport configuration
567
+ // ====================================================================
568
+ // Variables that may be reassigned based on resolution mode
340
569
  let command;
341
570
  let args = stdioOptions.args;
342
571
  let cwd = stdioOptions.cwd;
343
572
  let abilityVersion = options.version;
573
+ let shellMode = false; // Whether to use shell: true
574
+ // Variables that are only read (never reassigned)
575
+ const env = stdioOptions.env;
576
+ // ====================================================================
577
+ // STEP 3: Determine if we're using explicit command or auto-resolution
578
+ // ====================================================================
344
579
  if (stdioOptions.command) {
345
- // Explicit command provided - use it (backwards compatible)
580
+ // ==================================================================
581
+ // EXPLICIT COMMAND MODE
582
+ // ==================================================================
583
+ // Developer provided command explicitly - use it directly
584
+ // This gives developers full control when they need it
346
585
  command = stdioOptions.command;
586
+ if (process.env.KADI_DEBUG) {
587
+ console.log('[DEBUG] Using explicit stdio command:', {
588
+ abilityName: name,
589
+ command,
590
+ args,
591
+ cwd: cwd || 'default (process.cwd())'
592
+ });
593
+ }
347
594
  }
348
595
  else {
349
- // No command provided - auto-resolve from lockfile
596
+ // ==================================================================
597
+ // AUTO-RESOLUTION MODE
598
+ // ==================================================================
599
+ // No command provided - auto-resolve from ability's agent.json
600
+ if (process.env.KADI_DEBUG) {
601
+ console.log('[DEBUG] Auto-resolving stdio command for ability:', name);
602
+ }
350
603
  try {
351
- const config = resolveAbilityConfig(name);
352
- // TODO: Runtime detection - currently assumes Node.js
353
- // In the future, detect runtime from ability manifest:
354
- // - runtime: "node" | "python" | "go" | "binary"
355
- // - Or auto-detect from entry file extension (.js, .py, .go)
356
- // For now, we assume Node.js for all abilities
357
- command = 'node';
358
- // Construct entry point path
359
- const entryPath = path.join(config.path, config.entry);
360
- args = [entryPath];
361
- // Set working directory to ability directory
604
+ // ================================================================
605
+ // STEP 3a: Resolve ability path and metadata
606
+ // ================================================================
607
+ // This function already handles:
608
+ // - Checking agent-lock.json (if exists)
609
+ // - Scanning abilities/ folder
610
+ // - Picking highest version
611
+ // - Reading agent.json manifest
612
+ const config = resolveAbilityConfig(name, projectRoot);
613
+ if (process.env.KADI_DEBUG) {
614
+ console.log('[DEBUG] Resolved ability config:', {
615
+ name,
616
+ path: config.path,
617
+ version: config.version,
618
+ entry: config.entry
619
+ });
620
+ }
621
+ // ================================================================
622
+ // STEP 3b: Extract scripts.start from manifest
623
+ // ================================================================
624
+ const startScript = config.manifest.scripts?.start;
625
+ // ================================================================
626
+ // STEP 3c: Validate that scripts.start exists
627
+ // ================================================================
628
+ if (!startScript || startScript.trim() === '') {
629
+ // scripts.start is missing or empty - cannot auto-resolve
630
+ // Throw helpful error with actionable suggestions
631
+ throw new KadiError(`Cannot auto-load ability '${name}' via stdio - missing scripts.start in agent.json`, ErrorCode.INVALID_CONFIG, 400, {
632
+ abilityName: name,
633
+ version: config.version,
634
+ abilityPath: config.path,
635
+ agentJsonPath: join(config.path, 'agent.json'),
636
+ reason: 'agent.json does not have a "scripts.start" field',
637
+ suggestion: 'Add scripts.start to agent.json OR provide explicit command options',
638
+ // Detailed fix instructions
639
+ fix: {
640
+ option1: {
641
+ title: 'Add scripts.start to agent.json',
642
+ description: `Edit: ${join(config.path, 'agent.json')}`,
643
+ example: {
644
+ name: name,
645
+ version: config.version,
646
+ scripts: {
647
+ start: 'node dist/index.js'
648
+ }
649
+ },
650
+ instructions: 'Add a "scripts" section with a "start" command that runs your ability'
651
+ },
652
+ option2: {
653
+ title: 'Use explicit command options',
654
+ description: 'Provide command and args when loading',
655
+ example: `await client.load('${name}', 'stdio', {\n command: 'node',\n args: ['${config.entry}']\n})`
656
+ }
657
+ },
658
+ // Examples of valid scripts.start values
659
+ examples: [
660
+ '"node dist/index.js" - Run Node.js file',
661
+ '"npm start" - Run npm script',
662
+ '"python3 main.py" - Run Python script',
663
+ '"npm run build && npm start" - Complex shell command'
664
+ ],
665
+ // Alternative approaches
666
+ alternatives: [
667
+ `Add "scripts": { "start": "node ${config.entry}" } to agent.json`,
668
+ `Use explicit options: client.load('${name}', 'stdio', { command: 'node', args: ['${config.entry}'] })`
669
+ ],
670
+ hint: 'scripts.start is the command used to run your ability when loaded via stdio transport'
671
+ });
672
+ }
673
+ // ================================================================
674
+ // STEP 3d: Use scripts.start as the command
675
+ // ================================================================
676
+ // We'll run this with shell: true so it supports:
677
+ // - Complex commands: "npm run build && npm start"
678
+ // - Pipes: "cat input.txt | process"
679
+ // - Environment variables: "PORT=3000 node server.js"
680
+ command = startScript;
681
+ shellMode = true; // Enable shell mode for complex commands
682
+ // ================================================================
683
+ // STEP 3e: Set working directory to ability's directory
684
+ // ================================================================
685
+ // This ensures relative paths in scripts.start work correctly
686
+ // Example: If scripts.start is "node dist/index.js"
687
+ // and cwd is "/abilities/calculator/1.0.0/"
688
+ // then it will find "/abilities/calculator/1.0.0/dist/index.js"
362
689
  cwd = config.path;
363
- // Use version from lockfile
690
+ // ================================================================
691
+ // STEP 3f: Store version from resolved config
692
+ // ================================================================
364
693
  abilityVersion = config.version;
365
- // Log for debugging (if needed)
366
- // console.debug(`Auto-resolved ${name} stdio command: node ${entryPath}`);
694
+ // ================================================================
695
+ // STEP 3g: Clear args (not used in shell mode - command contains everything)
696
+ // ================================================================
697
+ args = undefined;
698
+ if (process.env.KADI_DEBUG) {
699
+ console.log('[DEBUG] Auto-resolved stdio command:', {
700
+ abilityName: name,
701
+ command: startScript,
702
+ cwd,
703
+ version: abilityVersion,
704
+ shellMode: true
705
+ });
706
+ }
367
707
  }
368
708
  catch (error) {
369
- // Re-throw lockfile resolution errors with context
709
+ // Re-throw KadiError as-is (already has helpful context)
370
710
  if (error instanceof KadiError) {
371
711
  throw error;
372
712
  }
373
- throw new KadiError(`Failed to auto-resolve ability '${name}' for stdio`, ErrorCode.ABILITY_NOT_FOUND, 404, {
713
+ // Wrap unexpected errors with context
714
+ throw new KadiError(`Failed to auto-resolve stdio command for ability '${name}'`, ErrorCode.ABILITY_NOT_FOUND, 404, {
374
715
  abilityName: name,
375
716
  transport: 'stdio',
376
- reason: 'Could not auto-resolve command from lockfile',
717
+ projectRoot,
718
+ reason: 'Could not auto-resolve command from ability manifest',
377
719
  originalError: error instanceof Error ? error.message : String(error),
378
- suggestion: 'Provide explicit command or run `kadi install`',
379
- example: 'await client.load("calculator", "stdio", { command: "node", args: ["./service.js"] })',
720
+ suggestion: 'Provide explicit command or check ability installation',
380
721
  alternatives: [
381
- 'Install ability: kadi install ' + name,
382
- 'Provide explicit command in options',
383
- 'Check that agent-lock.json exists'
722
+ `Install ability: kadi install ${name}`,
723
+ `Use explicit command: client.load('${name}', 'stdio', { command: '...', args: [...] })`,
724
+ 'Check that agent.json exists in ability directory'
384
725
  ]
385
726
  });
386
727
  }
387
728
  }
388
- // Step 3: Create StdioTransport with resolved/provided config
729
+ // ====================================================================
730
+ // STEP 4: Prepare environment variables
731
+ // ====================================================================
732
+ // Merge developer's env vars with auto-injected KADI_* variables
733
+ const mergedEnv = {
734
+ // Inherit parent process environment (important for PATH, etc.)
735
+ ...process.env,
736
+ // Auto-inject KADI-specific variables
737
+ // These help the child process understand its context
738
+ KADI_MODE: 'stdio',
739
+ KADI_ABILITY_NAME: name,
740
+ KADI_ABILITY_VERSION: abilityVersion || 'unknown',
741
+ // Developer's custom environment variables (if provided)
742
+ // These override defaults if there are conflicts
743
+ ...(env || {})
744
+ };
745
+ if (process.env.KADI_DEBUG) {
746
+ console.log('[DEBUG] Stdio transport environment variables:', {
747
+ KADI_MODE: mergedEnv.KADI_MODE,
748
+ KADI_ABILITY_NAME: mergedEnv.KADI_ABILITY_NAME,
749
+ KADI_ABILITY_VERSION: mergedEnv.KADI_ABILITY_VERSION,
750
+ customEnvVars: Object.keys(env || {})
751
+ });
752
+ }
753
+ // ====================================================================
754
+ // STEP 5: Create StdioTransport with resolved/provided configuration
755
+ // ====================================================================
389
756
  return new StdioTransport({
390
757
  abilityName: name,
391
758
  command,
392
759
  args,
393
760
  cwd,
394
- env: stdioOptions.env,
761
+ env: mergedEnv,
395
762
  timeout: options.timeout,
396
- abilityVersion
763
+ abilityVersion,
764
+ shellMode // Pass shell mode flag to transport
397
765
  });
398
766
  }
399
767
  }