@kodycodes/cli 1.18.0 → 1.19.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.
package/README.md CHANGED
@@ -85,7 +85,8 @@ keychain credentials migrate silently into the multi-org store as the active org
85
85
  | `kody whoami` | Confirms the CLI MCP connection and lists tools. With a scoped API token (and no login), shows token identity via the Open API. |
86
86
  | `kody search [query]` | Calls Kody `search` from the CLI (prefer the host MCP tool). Token-only auth uses Open API `GET /v1/search`. |
87
87
  | `kody api <operationId>` | Thin Open API wrapper matching the MCP `api` tool: `operationId` + flat `--params` JSON. Auth: `--token` / `KODY_API_TOKEN` / stored `auth bootstrap` token. The parsed `/openapi.json` route map is cached under the Kody cache dir keyed by the document's `ETag` and revalidated with `If-None-Match` on each run. |
88
- | `kody execute` | Calls Kody `execute` from the CLI (`--invoke`, `--code`, `--file`, or stdin via `--file -`). With a scoped API token and no login (or with `--token`), cloud execute goes through CapabilityProxy → `kody.execute` — no `kody login`. Add `--local` to run the module (and static `kody:@…` package modules) on this machine instead. `--credential key=name` (repeatable; `org:name` / `personal:name` / empty `=` for none) picks ADR 0068 credentials for `--local` or a `kody login` MCP session. `--allow-private-network` opts local execution into private and local network access. |
88
+ | `kody execute` | Calls Kody `execute` from the CLI (`--invoke`, `--code`, `--file`, or stdin via `--file -`). With a scoped API token and no login (or with `--token`), cloud execute goes through CapabilityProxy → `kody.execute` — no `kody login`. Add `--local` to run the module (and static `kody:@…` package modules) on this machine instead. `--credential key=name` (repeatable; `org:name` / `personal:name` / empty `=` for none) picks ADR 0068 credentials for `--local` or a `kody login` MCP session. `--allow-private-network` opts local execution into private and local network access. `--target prod\|mock` (default `prod`) sends CapabilityProxy / search hops to prod or the built-in CLI mock; `--target mock` implies local workerd. |
89
+ | `kody dev` | Local testing for unpublished packages. Serves `kody.app.entry` on localhost via workerd with hot reload. Subcommands: `fetch`, `job`, `webhook`, `emit`, `check`. Default `--target mock` (scratch storage, fake credentials). Covers exports, webhooks, schedules (run-once), and subscriptions. See [Local package testing](#local-package-testing-kody-dev). |
89
90
 
90
91
  `--json` prints structured MCP results.
91
92
 
@@ -204,6 +205,37 @@ config helpers, revokes and deletes the stored API token; leaves login alone).
204
205
  Push moves live HEAD only (packages do not auto-publish). Existing `kody login`
205
206
  / `auth bootstrap` flows are unchanged.
206
207
 
208
+ ## Local package testing (`kody dev`)
209
+
210
+ Test an unpublished package (app, exports, jobs, webhooks, subscriptions)
211
+ before publish. Bundling reuses the same esbuild path as `execute --file` /
212
+ publish (`local-bundle.ts`). One shared flag:
213
+
214
+ | Command | `--target prod` (default for execute/search) | `--target mock` (default for `kody dev`) |
215
+ | --- | --- | --- |
216
+ | `kody dev` | Local app against real Kody | Local app against the CLI mock |
217
+ | `execute` / `search` | Real capabilities, storage, secrets | Mock catalog / scratch storage / fake credentials |
218
+
219
+ The mock is built into the CLI (no kody-celld). It refuses unimplemented routes
220
+ with `not_implemented_in_mock` rather than faking success. `--reset-storage`
221
+ wipes the mock session's scratch store. Share previews are out of scope;
222
+ `execute --local` behavior is unchanged when `--target` is omitted (prod).
223
+
224
+ ```bash
225
+ cd ./my-package
226
+ kody dev # serve kody.app.entry; hot reload; --target mock
227
+ kody dev fetch /health # agent-friendly HTTP probe
228
+ kody dev job heartbeat # run a declared job once (never schedules locally)
229
+ kody dev webhook ping --body ./fixture.json
230
+ kody dev emit demo.event --body ./event.json # subscription delivery shape
231
+ kody dev check # bundle + light publish validations
232
+
233
+ # Four surfaces against mock then prod:
234
+ kody execute --target mock --invoke ./greet --params '{"name":"kent"}'
235
+ kody execute --target prod --invoke ./greet --params '{"name":"kent"}'
236
+ kody search --target mock local
237
+ ```
238
+
207
239
  ## Local execute
208
240
 
209
241
  `kody execute --local` runs the same execute module (default export called
package/dist/cli.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type CredentialCallMapInput } from './credential-call-map.js';
3
3
  export { readApiToken, requireApiToken as resolveApiToken, resolveScopedApiToken, } from './api-token.js';
4
4
  export { resolveLocalExecuteBearer } from './local-execute-auth.js';
5
5
  export { authBootstrap, authBootstrapWithScopes, bootstrapRedeemRequestBody, cliTokenLifetimeAliases, cliTokenLifetimeMissingError, cliTokenLifetimePolicy, parseCliLifetimeSecondsFlag, parseCliScopesFlag, redeemBootstrapCode, resolveCliTokenLifetime, } from './auth-bootstrap.js';
6
- export type CommandName = 'login' | 'logout' | 'org' | 'status' | 'auth' | 'git-credential' | 'whoami' | 'search' | 'api' | 'execute' | 'install' | 'skill' | 'help' | 'version';
6
+ export type CommandName = 'login' | 'logout' | 'org' | 'status' | 'auth' | 'git-credential' | 'whoami' | 'search' | 'api' | 'execute' | 'dev' | 'install' | 'skill' | 'help' | 'version';
7
7
  export declare function apiUrlFrom(values: {
8
8
  apiUrl?: string;
9
9
  }, env?: NodeJS.ProcessEnv): string;
@@ -35,6 +35,12 @@ declare function parseKnown(args: Array<string>): {
35
35
  project?: string | boolean | undefined;
36
36
  'no-browser'?: string | boolean | undefined;
37
37
  org?: string | boolean | undefined;
38
+ target?: string | boolean | undefined;
39
+ 'reset-storage'?: string | boolean | undefined;
40
+ port?: string | boolean | undefined;
41
+ 'app-base-path'?: string | boolean | undefined;
42
+ method?: string | boolean | undefined;
43
+ body?: string | boolean | undefined;
38
44
  clients?: string | boolean | undefined;
39
45
  yes?: string | boolean | undefined;
40
46
  };
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ const orgScopedCommands = new Set([
24
24
  'search',
25
25
  'api',
26
26
  'execute',
27
+ 'dev',
27
28
  'auth',
28
29
  ]);
29
30
  function mcpUrlFrom(values) {
@@ -62,6 +63,12 @@ function parseKnown(args) {
62
63
  project: { type: 'boolean' },
63
64
  'no-browser': { type: 'boolean' },
64
65
  org: { type: 'string' },
66
+ target: { type: 'string' },
67
+ 'reset-storage': { type: 'boolean' },
68
+ port: { type: 'string' },
69
+ 'app-base-path': { type: 'string' },
70
+ method: { type: 'string' },
71
+ body: { type: 'string' },
65
72
  clients: { type: 'string' },
66
73
  yes: { type: 'boolean', short: 'y' },
67
74
  },
@@ -89,6 +96,7 @@ export function resolveCommand(argv) {
89
96
  case 'search':
90
97
  case 'api':
91
98
  case 'execute':
99
+ case 'dev':
92
100
  case 'install':
93
101
  case 'skill':
94
102
  case 'help':
@@ -145,8 +153,15 @@ async function dispatch(parsed, write, writeErr) {
145
153
  });
146
154
  const json = parsed.values.json === true;
147
155
  if (parsed.values['allow-private-network'] === true &&
156
+ parsed.command !== 'dev' &&
148
157
  (parsed.command !== 'execute' || parsed.values.local !== true)) {
149
- throw new Error('--allow-private-network can only be used with execute --local.');
158
+ throw new Error('--allow-private-network can only be used with execute --local or kody dev.');
159
+ }
160
+ if (parsed.values['reset-storage'] === true &&
161
+ parsed.command !== 'dev' &&
162
+ parsed.command !== 'execute' &&
163
+ parsed.command !== 'search') {
164
+ throw new Error('`--reset-storage` is only used with --target mock on execute, search, or dev.');
150
165
  }
151
166
  const orgFlag = typeof parsed.values.org === 'string' ? parsed.values.org : undefined;
152
167
  if (orgFlag !== undefined && !orgScopedCommands.has(parsed.command)) {
@@ -441,63 +456,110 @@ async function dispatch(parsed, write, writeErr) {
441
456
  return 0;
442
457
  }
443
458
  case 'search': {
459
+ const { resolveTarget } = await import('./target.js');
460
+ const target = resolveTarget({
461
+ command: 'search',
462
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
463
+ });
444
464
  const query = parsed.positionals.join(' ').trim();
445
465
  const tokenValues = tokenFlagValues(parsed.values);
446
- const apiUrl = apiUrlFrom({
466
+ let apiUrl = apiUrlFrom({
447
467
  apiUrl: typeof parsed.values['api-url'] === 'string'
448
468
  ? parsed.values['api-url']
449
469
  : undefined,
450
470
  });
451
- if (shouldUseApiToken({
452
- tokenValues,
453
- mcpUrl,
454
- apiUrl,
455
- allowEnvWithoutLogin: true,
456
- ...(org ? { org } : {}),
457
- })) {
458
- const { searchWithApiToken } = await import('./open-api-client.js');
459
- const result = await searchWithApiToken({
460
- token: requireApiToken(tokenValues, process.env, 'search', { apiUrl }),
471
+ let closeMock;
472
+ try {
473
+ if (target === 'mock') {
474
+ const { ensureMockTarget } = await import('./mock/mock-server.js');
475
+ const mock = await ensureMockTarget({
476
+ resetStorage: parsed.values['reset-storage'] === true,
477
+ });
478
+ apiUrl = mock.apiUrl;
479
+ closeMock = mock.started ? () => mock.server.close() : undefined;
480
+ const { searchWithApiToken } = await import('./open-api-client.js');
481
+ const result = await searchWithApiToken({
482
+ token: mock.token,
483
+ apiUrl,
484
+ query: query || undefined,
485
+ entity: typeof parsed.values.entity === 'string'
486
+ ? parsed.values.entity
487
+ : undefined,
488
+ domain: typeof parsed.values.domain === 'string'
489
+ ? parsed.values.domain
490
+ : undefined,
491
+ limit: typeof parsed.values.limit === 'string'
492
+ ? Number(parsed.values.limit)
493
+ : undefined,
494
+ });
495
+ write(formatToolResult(result, json));
496
+ return result.isError ? 1 : 0;
497
+ }
498
+ if (shouldUseApiToken({
499
+ tokenValues,
500
+ mcpUrl,
461
501
  apiUrl,
502
+ allowEnvWithoutLogin: true,
503
+ ...(org ? { org } : {}),
504
+ })) {
505
+ const { searchWithApiToken } = await import('./open-api-client.js');
506
+ const result = await searchWithApiToken({
507
+ token: requireApiToken(tokenValues, process.env, 'search', { apiUrl }),
508
+ apiUrl,
509
+ ...(org ? { org } : {}),
510
+ query: query || undefined,
511
+ entity: typeof parsed.values.entity === 'string'
512
+ ? parsed.values.entity
513
+ : undefined,
514
+ domain: typeof parsed.values.domain === 'string'
515
+ ? parsed.values.domain
516
+ : undefined,
517
+ limit: typeof parsed.values.limit === 'string'
518
+ ? Number(parsed.values.limit)
519
+ : undefined,
520
+ });
521
+ write(formatToolResult(result, json));
522
+ return result.isError ? 1 : 0;
523
+ }
524
+ const args = {};
525
+ if (query)
526
+ args.query = query;
527
+ if (typeof parsed.values.entity === 'string')
528
+ args.entity = parsed.values.entity;
529
+ if (typeof parsed.values.domain === 'string')
530
+ args.domain = parsed.values.domain;
531
+ if (typeof parsed.values.limit === 'string')
532
+ args.limit = Number(parsed.values.limit);
533
+ const { callKodyTool } = await import('./mcp.js');
534
+ const result = await callKodyTool({
535
+ name: 'search',
536
+ args,
537
+ mcpUrl,
462
538
  ...(org ? { org } : {}),
463
- query: query || undefined,
464
- entity: typeof parsed.values.entity === 'string' ? parsed.values.entity : undefined,
465
- domain: typeof parsed.values.domain === 'string' ? parsed.values.domain : undefined,
466
- limit: typeof parsed.values.limit === 'string'
467
- ? Number(parsed.values.limit)
468
- : undefined,
469
539
  });
470
540
  write(formatToolResult(result, json));
471
541
  return result.isError ? 1 : 0;
472
542
  }
473
- const args = {};
474
- if (query)
475
- args.query = query;
476
- if (typeof parsed.values.entity === 'string')
477
- args.entity = parsed.values.entity;
478
- if (typeof parsed.values.domain === 'string')
479
- args.domain = parsed.values.domain;
480
- if (typeof parsed.values.limit === 'string')
481
- args.limit = Number(parsed.values.limit);
482
- const { callKodyTool } = await import('./mcp.js');
483
- const result = await callKodyTool({
484
- name: 'search',
485
- args,
486
- mcpUrl,
487
- ...(org ? { org } : {}),
488
- });
489
- write(formatToolResult(result, json));
490
- return result.isError ? 1 : 0;
543
+ finally {
544
+ await closeMock?.();
545
+ }
491
546
  }
492
547
  case 'execute': {
548
+ const { resolveTarget } = await import('./target.js');
549
+ const target = resolveTarget({
550
+ command: 'execute',
551
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
552
+ });
493
553
  const invoke = typeof parsed.values.invoke === 'string' ? parsed.values.invoke : undefined;
494
554
  const hasCodeFlag = typeof parsed.values.code === 'string';
495
555
  const hasFileFlag = typeof parsed.values.file === 'string';
496
556
  const positionalModule = parsed.positionals.join('\n').trim();
497
- const local = parsed.values.local === true;
557
+ // --target mock always runs locally against the CLI mock (no cloud sandbox).
558
+ const local = parsed.values.local === true || target === 'mock';
498
559
  const tokenValues = tokenFlagValues(parsed.values);
499
- if (local && invoke !== undefined) {
500
- throw new Error('--invoke runs a saved package export in Kody; --local runs a module you provide (--code, --file, or a module string).');
560
+ const localPackageInvoke = invoke !== undefined ? await tryResolveLocalPackageInvoke(invoke) : null;
561
+ if (local && invoke !== undefined && !localPackageInvoke) {
562
+ throw new Error('--invoke runs a saved package export in Kody; --local runs a module you provide (--code, --file, or a module string). To test an unpublished package export, pass an export key from package.json#exports (e.g. `--invoke ./on-user-deleted`) from the package directory, or use --file.');
501
563
  }
502
564
  if (executeSourcesConflict({ invoke, hasCodeFlag, hasFileFlag, positionalModule })) {
503
565
  throw new Error('--invoke cannot be combined with --code, --file, or a module string.');
@@ -506,23 +568,26 @@ async function dispatch(parsed, write, writeErr) {
506
568
  assertLocalExecuteNodeEngine();
507
569
  const fileArg = hasFileFlag ? parsed.values.file : undefined;
508
570
  // `--code` wins over `--file`. Only attribute the path when the file is the source.
509
- const modulePath = !hasCodeFlag && fileArg !== undefined && fileArg !== '-' ? fileArg : undefined;
510
- const code = invoke !== undefined
571
+ const modulePath = localPackageInvoke?.modulePath ??
572
+ (!hasCodeFlag && fileArg !== undefined && fileArg !== '-' ? fileArg : undefined);
573
+ const code = invoke !== undefined && !localPackageInvoke
511
574
  ? undefined
512
575
  : hasCodeFlag
513
576
  ? parsed.values.code
514
- : modulePath !== undefined
577
+ : modulePath !== undefined && !localPackageInvoke
515
578
  ? await readFile(modulePath, 'utf8')
516
- : fileArg === '-'
517
- ? await readStdin()
518
- : positionalModule || undefined;
579
+ : localPackageInvoke
580
+ ? await readFile(localPackageInvoke.modulePath, 'utf8')
581
+ : fileArg === '-'
582
+ ? await readStdin()
583
+ : positionalModule || undefined;
519
584
  const credentials = parseCredentialFlags(Array.isArray(parsed.values.credential)
520
585
  ? parsed.values.credential.filter((value) => typeof value === 'string')
521
586
  : typeof parsed.values.credential === 'string'
522
587
  ? [parsed.values.credential]
523
588
  : undefined);
524
589
  const args = buildExecuteToolArgs({
525
- invoke,
590
+ invoke: localPackageInvoke ? undefined : invoke,
526
591
  code: typeof code === 'string' ? code : undefined,
527
592
  paramsJson: typeof parsed.values.params === 'string' ? parsed.values.params : undefined,
528
593
  conversationId: typeof parsed.values['conversation-id'] === 'string'
@@ -530,57 +595,215 @@ async function dispatch(parsed, write, writeErr) {
530
595
  : undefined,
531
596
  credentials,
532
597
  });
533
- const apiUrl = apiUrlFrom({
598
+ let apiUrl = apiUrlFrom({
534
599
  apiUrl: typeof parsed.values['api-url'] === 'string'
535
600
  ? parsed.values['api-url']
536
601
  : undefined,
537
602
  });
538
- const useToken = local ||
539
- shouldUseApiToken({
540
- tokenValues,
541
- mcpUrl,
542
- apiUrl,
543
- allowEnvWithoutLogin: true,
544
- ...(org ? { org } : {}),
545
- });
546
- const result = local
547
- ? await (await import('./local-execute.js')).runLocalExecute({
548
- code: args.code,
549
- ...(modulePath !== undefined ? { modulePath } : {}),
550
- params: args.params,
551
- conversationId: args.conversationId,
552
- ...(credentials !== undefined ? { credentials } : {}),
553
- token: await resolveLocalExecuteBearer({
603
+ let closeMock;
604
+ let mockToken;
605
+ try {
606
+ if (target === 'mock') {
607
+ const { ensureMockTarget } = await import('./mock/mock-server.js');
608
+ const mock = await ensureMockTarget({
609
+ resetStorage: parsed.values['reset-storage'] === true,
610
+ packageId: localPackageInvoke
611
+ ? `local:${localPackageInvoke.kodyId}`
612
+ : undefined,
613
+ });
614
+ apiUrl = mock.apiUrl;
615
+ mockToken = mock.token;
616
+ closeMock = mock.started ? () => mock.server.close() : undefined;
617
+ }
618
+ if (localPackageInvoke) {
619
+ const { buildLocalPackageContext, runLocalPackageModule } = await import('./local-package-run.js');
620
+ const result = await runLocalPackageModule({
621
+ modulePath: localPackageInvoke.modulePath,
622
+ params: args.params,
623
+ token: mockToken ??
624
+ (await resolveLocalExecuteBearer({
625
+ tokenValues,
626
+ mcpUrl,
627
+ apiUrl,
628
+ purpose: 'execute local package export',
629
+ ...(org ? { org } : {}),
630
+ })),
631
+ apiUrl,
632
+ target,
633
+ packageContext: buildLocalPackageContext({
634
+ kodyId: localPackageInvoke.kodyId,
635
+ hostedUrl: 'http://127.0.0.1:0',
636
+ }),
637
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
638
+ onStatus: (message) => writeErr(`${message}\n`),
639
+ });
640
+ write(formatToolResult(result, json));
641
+ return result.isError ? 1 : 0;
642
+ }
643
+ const useToken = local ||
644
+ shouldUseApiToken({
554
645
  tokenValues,
555
646
  mcpUrl,
556
647
  apiUrl,
557
- purpose: 'execute --local',
648
+ allowEnvWithoutLogin: true,
558
649
  ...(org ? { org } : {}),
559
- }),
560
- apiUrl,
561
- ...(org ? { org } : {}),
562
- allowPrivateNetwork: parsed.values['allow-private-network'] === true,
563
- onStatus: (message) => writeErr(`${message}\n`),
564
- })
565
- : useToken
566
- ? await (await import('./remote-execute.js')).runRemoteExecuteWithToken({
567
- code: typeof args.code === 'string' ? args.code : undefined,
568
- invoke,
650
+ });
651
+ const result = local
652
+ ? await (await import('./local-execute.js')).runLocalExecute({
653
+ code: args.code,
654
+ ...(modulePath !== undefined ? { modulePath } : {}),
569
655
  params: args.params,
570
656
  conversationId: args.conversationId,
571
657
  ...(credentials !== undefined ? { credentials } : {}),
572
- token: requireApiToken(tokenValues, process.env, 'execute with an API token', { apiUrl }),
658
+ token: mockToken ??
659
+ (await resolveLocalExecuteBearer({
660
+ tokenValues,
661
+ mcpUrl,
662
+ apiUrl,
663
+ purpose: 'execute --local',
664
+ ...(org ? { org } : {}),
665
+ })),
573
666
  apiUrl,
574
667
  ...(org ? { org } : {}),
668
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
669
+ onStatus: (message) => writeErr(`${message}\n`),
575
670
  })
576
- : await (await import('./mcp.js')).callKodyTool({
577
- name: 'execute',
578
- args,
579
- mcpUrl,
580
- ...(org ? { org } : {}),
671
+ : useToken
672
+ ? await (await import('./remote-execute.js')).runRemoteExecuteWithToken({
673
+ code: typeof args.code === 'string' ? args.code : undefined,
674
+ invoke,
675
+ params: args.params,
676
+ conversationId: args.conversationId,
677
+ ...(credentials !== undefined ? { credentials } : {}),
678
+ token: requireApiToken(tokenValues, process.env, 'execute with an API token', { apiUrl }),
679
+ apiUrl,
680
+ ...(org ? { org } : {}),
681
+ })
682
+ : await (await import('./mcp.js')).callKodyTool({
683
+ name: 'execute',
684
+ args,
685
+ mcpUrl,
686
+ ...(org ? { org } : {}),
687
+ });
688
+ write(formatToolResult(result, json));
689
+ return result.isError ? 1 : 0;
690
+ }
691
+ finally {
692
+ await closeMock?.();
693
+ }
694
+ }
695
+ case 'dev': {
696
+ const { resolveTarget } = await import('./target.js');
697
+ const { parseDevSubcommand, resolveDevAuth, runDevCheck, runDevEmit, runDevFetch, runDevJob, runDevServe, runDevWebhook, } = await import('./dev-command.js');
698
+ const target = resolveTarget({
699
+ command: 'dev',
700
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
701
+ });
702
+ const { subcommand, rest } = parseDevSubcommand(parsed.positionals);
703
+ const tokenValues = tokenFlagValues(parsed.values);
704
+ const prodApiUrl = apiUrlFrom({
705
+ apiUrl: typeof parsed.values['api-url'] === 'string'
706
+ ? parsed.values['api-url']
707
+ : undefined,
708
+ });
709
+ const io = { stdout: write, stderr: writeErr };
710
+ const bodyPath = typeof parsed.values.body === 'string' ? parsed.values.body : undefined;
711
+ // `check` only bundles + validates the package tree; it never talks to
712
+ // mock/prod, so skip auth (and the mock server) to avoid leaking a listen
713
+ // handle that would keep the process alive after printing results.
714
+ if (subcommand === 'check') {
715
+ return await runDevCheck({ target, json, io });
716
+ }
717
+ const token = target === 'prod'
718
+ ? await resolveLocalExecuteBearer({
719
+ tokenValues,
720
+ mcpUrl,
721
+ apiUrl: prodApiUrl,
722
+ purpose: 'kody dev',
723
+ ...(org ? { org } : {}),
724
+ })
725
+ : 'mock';
726
+ const auth = await resolveDevAuth({
727
+ target,
728
+ token,
729
+ apiUrl: prodApiUrl,
730
+ resetStorage: parsed.values['reset-storage'] === true,
731
+ });
732
+ const port = typeof parsed.values.port === 'string' ? Number(parsed.values.port) : undefined;
733
+ const appBasePath = typeof parsed.values['app-base-path'] === 'string'
734
+ ? parsed.values['app-base-path']
735
+ : undefined;
736
+ switch (subcommand) {
737
+ case 'serve':
738
+ return await runDevServe({
739
+ target,
740
+ auth,
741
+ port,
742
+ appBasePath,
743
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
744
+ io,
745
+ });
746
+ case 'fetch': {
747
+ const path = rest[0];
748
+ if (!path) {
749
+ throw new Error('Usage: kody dev fetch <path> [--method GET] [--body file.json]');
750
+ }
751
+ let body;
752
+ if (bodyPath)
753
+ body = await readFile(bodyPath, 'utf8');
754
+ return await runDevFetch({
755
+ target,
756
+ auth,
757
+ path,
758
+ method: typeof parsed.values.method === 'string'
759
+ ? parsed.values.method
760
+ : undefined,
761
+ body,
762
+ port,
763
+ appBasePath,
764
+ json,
765
+ io,
766
+ });
767
+ }
768
+ case 'job': {
769
+ const jobName = rest[0];
770
+ if (!jobName)
771
+ throw new Error('Usage: kody dev job <name>');
772
+ return await runDevJob({ target, auth, jobName, json, io });
773
+ }
774
+ case 'webhook': {
775
+ const webhookName = rest[0];
776
+ if (!webhookName || !bodyPath) {
777
+ throw new Error('Usage: kody dev webhook <name> --body file.json');
778
+ }
779
+ return await runDevWebhook({
780
+ target,
781
+ auth,
782
+ webhookName,
783
+ bodyPath,
784
+ json,
785
+ io,
581
786
  });
582
- write(formatToolResult(result, json));
583
- return result.isError ? 1 : 0;
787
+ }
788
+ case 'emit': {
789
+ const event = rest[0];
790
+ if (!event || !bodyPath) {
791
+ throw new Error('Usage: kody dev emit <event> --body event.json');
792
+ }
793
+ return await runDevEmit({
794
+ target,
795
+ auth,
796
+ event,
797
+ bodyPath,
798
+ json,
799
+ io,
800
+ });
801
+ }
802
+ default: {
803
+ const exhaustive = subcommand;
804
+ throw new Error(`Unhandled kody dev subcommand: ${String(exhaustive)}`);
805
+ }
806
+ }
584
807
  }
585
808
  case 'api': {
586
809
  const operationId = parsed.positionals[0]?.trim() ?? '';
@@ -640,6 +863,20 @@ async function readStdin() {
640
863
  }
641
864
  return Buffer.concat(chunks).toString('utf8');
642
865
  }
866
+ async function tryResolveLocalPackageInvoke(invoke) {
867
+ const trimmed = invoke.trim();
868
+ if (!trimmed || trimmed.startsWith('kody:@'))
869
+ return null;
870
+ try {
871
+ const { loadPackageManifest, resolveExportModulePath } = await import('./package-manifest.js');
872
+ const manifest = await loadPackageManifest(process.cwd());
873
+ const modulePath = resolveExportModulePath(manifest, trimmed);
874
+ return { modulePath, kodyId: manifest.kodyId };
875
+ }
876
+ catch {
877
+ return null;
878
+ }
879
+ }
643
880
  /** True when --invoke is set alongside any code source (--code, --file, or positional). */
644
881
  export function executeSourcesConflict(input) {
645
882
  return (input.invoke !== undefined &&