@kodycodes/cli 1.18.0 → 1.19.1

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 (56) hide show
  1. package/README.md +34 -1
  2. package/dist/api-token.d.ts +23 -4
  3. package/dist/api-token.js +48 -10
  4. package/dist/api-token.js.map +1 -1
  5. package/dist/capability-proxy.js +5 -2
  6. package/dist/capability-proxy.js.map +1 -1
  7. package/dist/cli.d.ts +16 -1
  8. package/dist/cli.js +407 -91
  9. package/dist/cli.js.map +1 -1
  10. package/dist/dev-command.d.ts +96 -0
  11. package/dist/dev-command.js +385 -0
  12. package/dist/dev-command.js.map +1 -0
  13. package/dist/git-credential.d.ts +5 -4
  14. package/dist/git-credential.js +5 -4
  15. package/dist/git-credential.js.map +1 -1
  16. package/dist/help.js +49 -7
  17. package/dist/help.js.map +1 -1
  18. package/dist/local-package-graph.js +5 -2
  19. package/dist/local-package-graph.js.map +1 -1
  20. package/dist/local-package-run.d.ts +30 -0
  21. package/dist/local-package-run.js +304 -0
  22. package/dist/local-package-run.js.map +1 -0
  23. package/dist/mock/mock-server.d.ts +42 -0
  24. package/dist/mock/mock-server.js +373 -0
  25. package/dist/mock/mock-server.js.map +1 -0
  26. package/dist/mock/mock-storage.d.ts +46 -0
  27. package/dist/mock/mock-storage.js +96 -0
  28. package/dist/mock/mock-storage.js.map +1 -0
  29. package/dist/mock/mock-supported.d.ts +18 -0
  30. package/dist/mock/mock-supported.js +43 -0
  31. package/dist/mock/mock-supported.js.map +1 -0
  32. package/dist/open-api-client.js +5 -2
  33. package/dist/open-api-client.js.map +1 -1
  34. package/dist/package-app-dev.d.ts +45 -0
  35. package/dist/package-app-dev.js +357 -0
  36. package/dist/package-app-dev.js.map +1 -0
  37. package/dist/package-info.d.ts +5 -0
  38. package/dist/package-info.js +14 -2
  39. package/dist/package-info.js.map +1 -1
  40. package/dist/package-local-runtime-source.d.ts +35 -0
  41. package/dist/package-local-runtime-source.js +317 -0
  42. package/dist/package-local-runtime-source.js.map +1 -0
  43. package/dist/package-manifest.d.ts +43 -0
  44. package/dist/package-manifest.js +264 -0
  45. package/dist/package-manifest.js.map +1 -0
  46. package/dist/setup-git.d.ts +17 -4
  47. package/dist/setup-git.js +24 -5
  48. package/dist/setup-git.js.map +1 -1
  49. package/dist/structured-error.d.ts +28 -0
  50. package/dist/structured-error.js +57 -0
  51. package/dist/structured-error.js.map +1 -0
  52. package/dist/target.d.ts +27 -0
  53. package/dist/target.js +48 -0
  54. package/dist/target.js.map +1 -0
  55. package/package.json +1 -1
  56. package/skills/kody/SKILL.md +7 -3
package/dist/cli.js CHANGED
@@ -23,7 +23,9 @@ const orgScopedCommands = new Set([
23
23
  'whoami',
24
24
  'search',
25
25
  'api',
26
+ 'package',
26
27
  'execute',
28
+ 'dev',
27
29
  'auth',
28
30
  ]);
29
31
  function mcpUrlFrom(values) {
@@ -62,6 +64,12 @@ function parseKnown(args) {
62
64
  project: { type: 'boolean' },
63
65
  'no-browser': { type: 'boolean' },
64
66
  org: { type: 'string' },
67
+ target: { type: 'string' },
68
+ 'reset-storage': { type: 'boolean' },
69
+ port: { type: 'string' },
70
+ 'app-base-path': { type: 'string' },
71
+ method: { type: 'string' },
72
+ body: { type: 'string' },
65
73
  clients: { type: 'string' },
66
74
  yes: { type: 'boolean', short: 'y' },
67
75
  },
@@ -88,7 +96,9 @@ export function resolveCommand(argv) {
88
96
  case 'whoami':
89
97
  case 'search':
90
98
  case 'api':
99
+ case 'package':
91
100
  case 'execute':
101
+ case 'dev':
92
102
  case 'install':
93
103
  case 'skill':
94
104
  case 'help':
@@ -106,12 +116,23 @@ export async function runCli(argv = process.argv.slice(2), io = {}) {
106
116
  const write = io.stdout ?? ((text) => process.stdout.write(text));
107
117
  const writeErr = io.stderr ?? ((text) => process.stderr.write(text));
108
118
  // Best-effort: upgrade stale git helper pins (major-only / ephemeral /
109
- // outdated exact) before any command, including git-credential. Must not
110
- // write to stdout — git reads the helper protocol from stdout only.
119
+ // outdated exact) before most commands. Must not write to stdout — git
120
+ // reads the helper protocol from stdout only.
111
121
  // Skip under node:test so unit tests never rewrite the developer's
112
- // global git config; production / E2E / npx always refresh.
113
- if (process.env.NODE_TEST !== '1' &&
114
- process.env.KODY_SKIP_GIT_HELPER_REFRESH !== '1') {
122
+ // global git config; skip git-credential (helper must stay fast / offline
123
+ // while the stored token is valid); skip version/help (no git touch).
124
+ const head = argv[0];
125
+ const skipHelperRefresh = process.env.NODE_TEST === '1' ||
126
+ process.env.KODY_SKIP_GIT_HELPER_REFRESH === '1' ||
127
+ head === 'git-credential' ||
128
+ head === 'version' ||
129
+ head === '--version' ||
130
+ head === '-v' ||
131
+ head === 'help' ||
132
+ head === '--help' ||
133
+ head === '-h' ||
134
+ head === undefined;
135
+ if (!skipHelperRefresh) {
115
136
  try {
116
137
  refreshGitCredentialHelperPinIfNeeded({
117
138
  mcpUrl: process.env.KODY_MCP_URL || undefined,
@@ -144,13 +165,28 @@ async function dispatch(parsed, write, writeErr) {
144
165
  mcpUrl: typeof parsed.values['mcp-url'] === 'string' ? parsed.values['mcp-url'] : undefined,
145
166
  });
146
167
  const json = parsed.values.json === true;
147
- if (parsed.values['allow-private-network'] === true &&
148
- (parsed.command !== 'execute' || parsed.values.local !== true)) {
149
- throw new Error('--allow-private-network can only be used with execute --local.');
168
+ if (parsed.values['allow-private-network'] === true) {
169
+ const executeAllowsPrivateNetwork = parsed.command === 'execute' &&
170
+ (parsed.values.local === true ||
171
+ (await import('./target.js')).resolveTarget({
172
+ command: 'execute',
173
+ flag: typeof parsed.values.target === 'string'
174
+ ? parsed.values.target
175
+ : undefined,
176
+ }) === 'mock');
177
+ if (parsed.command !== 'dev' && !executeAllowsPrivateNetwork) {
178
+ throw new Error('--allow-private-network can only be used with execute --local, execute --target mock, or kody dev.');
179
+ }
180
+ }
181
+ if (parsed.values['reset-storage'] === true &&
182
+ parsed.command !== 'dev' &&
183
+ parsed.command !== 'execute' &&
184
+ parsed.command !== 'search') {
185
+ throw new Error('`--reset-storage` is only used with --target mock on execute, search, or dev.');
150
186
  }
151
187
  const orgFlag = typeof parsed.values.org === 'string' ? parsed.values.org : undefined;
152
188
  if (orgFlag !== undefined && !orgScopedCommands.has(parsed.command)) {
153
- throw new Error(`\`--org\` is not used with \`kody ${parsed.command}\`. Use it with login, logout, whoami, search, api, execute, or auth.`);
189
+ throw new Error(`\`--org\` is not used with \`kody ${parsed.command}\`. Use it with login, logout, whoami, search, api, package, execute, or auth.`);
154
190
  }
155
191
  // Login/logout honor `--org` only — not KODY_ORG (that env is for one-shot
156
192
  // org-scoped commands and must not bind browser login or change logout).
@@ -441,63 +477,110 @@ async function dispatch(parsed, write, writeErr) {
441
477
  return 0;
442
478
  }
443
479
  case 'search': {
480
+ const { resolveTarget } = await import('./target.js');
481
+ const target = resolveTarget({
482
+ command: 'search',
483
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
484
+ });
444
485
  const query = parsed.positionals.join(' ').trim();
445
486
  const tokenValues = tokenFlagValues(parsed.values);
446
- const apiUrl = apiUrlFrom({
487
+ let apiUrl = apiUrlFrom({
447
488
  apiUrl: typeof parsed.values['api-url'] === 'string'
448
489
  ? parsed.values['api-url']
449
490
  : undefined,
450
491
  });
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 }),
492
+ let closeMock;
493
+ try {
494
+ if (target === 'mock') {
495
+ const { ensureMockTarget } = await import('./mock/mock-server.js');
496
+ const mock = await ensureMockTarget({
497
+ resetStorage: parsed.values['reset-storage'] === true,
498
+ });
499
+ apiUrl = mock.apiUrl;
500
+ closeMock = mock.started ? () => mock.server.close() : undefined;
501
+ const { searchWithApiToken } = await import('./open-api-client.js');
502
+ const result = await searchWithApiToken({
503
+ token: mock.token,
504
+ apiUrl,
505
+ query: query || undefined,
506
+ entity: typeof parsed.values.entity === 'string'
507
+ ? parsed.values.entity
508
+ : undefined,
509
+ domain: typeof parsed.values.domain === 'string'
510
+ ? parsed.values.domain
511
+ : undefined,
512
+ limit: typeof parsed.values.limit === 'string'
513
+ ? Number(parsed.values.limit)
514
+ : undefined,
515
+ });
516
+ write(formatToolResult(result, json));
517
+ return result.isError ? 1 : 0;
518
+ }
519
+ if (shouldUseApiToken({
520
+ tokenValues,
521
+ mcpUrl,
461
522
  apiUrl,
523
+ allowEnvWithoutLogin: true,
524
+ ...(org ? { org } : {}),
525
+ })) {
526
+ const { searchWithApiToken } = await import('./open-api-client.js');
527
+ const result = await searchWithApiToken({
528
+ token: requireApiToken(tokenValues, process.env, 'search', { apiUrl }),
529
+ apiUrl,
530
+ ...(org ? { org } : {}),
531
+ query: query || undefined,
532
+ entity: typeof parsed.values.entity === 'string'
533
+ ? parsed.values.entity
534
+ : undefined,
535
+ domain: typeof parsed.values.domain === 'string'
536
+ ? parsed.values.domain
537
+ : undefined,
538
+ limit: typeof parsed.values.limit === 'string'
539
+ ? Number(parsed.values.limit)
540
+ : undefined,
541
+ });
542
+ write(formatToolResult(result, json));
543
+ return result.isError ? 1 : 0;
544
+ }
545
+ const args = {};
546
+ if (query)
547
+ args.query = query;
548
+ if (typeof parsed.values.entity === 'string')
549
+ args.entity = parsed.values.entity;
550
+ if (typeof parsed.values.domain === 'string')
551
+ args.domain = parsed.values.domain;
552
+ if (typeof parsed.values.limit === 'string')
553
+ args.limit = Number(parsed.values.limit);
554
+ const { callKodyTool } = await import('./mcp.js');
555
+ const result = await callKodyTool({
556
+ name: 'search',
557
+ args,
558
+ mcpUrl,
462
559
  ...(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
560
  });
470
561
  write(formatToolResult(result, json));
471
562
  return result.isError ? 1 : 0;
472
563
  }
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;
564
+ finally {
565
+ await closeMock?.();
566
+ }
491
567
  }
492
568
  case 'execute': {
569
+ const { resolveTarget } = await import('./target.js');
570
+ const target = resolveTarget({
571
+ command: 'execute',
572
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
573
+ });
493
574
  const invoke = typeof parsed.values.invoke === 'string' ? parsed.values.invoke : undefined;
494
575
  const hasCodeFlag = typeof parsed.values.code === 'string';
495
576
  const hasFileFlag = typeof parsed.values.file === 'string';
496
577
  const positionalModule = parsed.positionals.join('\n').trim();
497
- const local = parsed.values.local === true;
578
+ // --target mock always runs locally against the CLI mock (no cloud sandbox).
579
+ const local = parsed.values.local === true || target === 'mock';
498
580
  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).');
581
+ const localPackageInvoke = invoke !== undefined ? await tryResolveLocalPackageInvoke(invoke) : null;
582
+ if (local && invoke !== undefined && !localPackageInvoke) {
583
+ 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
584
  }
502
585
  if (executeSourcesConflict({ invoke, hasCodeFlag, hasFileFlag, positionalModule })) {
503
586
  throw new Error('--invoke cannot be combined with --code, --file, or a module string.');
@@ -506,23 +589,26 @@ async function dispatch(parsed, write, writeErr) {
506
589
  assertLocalExecuteNodeEngine();
507
590
  const fileArg = hasFileFlag ? parsed.values.file : undefined;
508
591
  // `--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
592
+ const modulePath = localPackageInvoke?.modulePath ??
593
+ (!hasCodeFlag && fileArg !== undefined && fileArg !== '-' ? fileArg : undefined);
594
+ const code = invoke !== undefined && !localPackageInvoke
511
595
  ? undefined
512
596
  : hasCodeFlag
513
597
  ? parsed.values.code
514
- : modulePath !== undefined
598
+ : modulePath !== undefined && !localPackageInvoke
515
599
  ? await readFile(modulePath, 'utf8')
516
- : fileArg === '-'
517
- ? await readStdin()
518
- : positionalModule || undefined;
600
+ : localPackageInvoke
601
+ ? await readFile(localPackageInvoke.modulePath, 'utf8')
602
+ : fileArg === '-'
603
+ ? await readStdin()
604
+ : positionalModule || undefined;
519
605
  const credentials = parseCredentialFlags(Array.isArray(parsed.values.credential)
520
606
  ? parsed.values.credential.filter((value) => typeof value === 'string')
521
607
  : typeof parsed.values.credential === 'string'
522
608
  ? [parsed.values.credential]
523
609
  : undefined);
524
610
  const args = buildExecuteToolArgs({
525
- invoke,
611
+ invoke: localPackageInvoke ? undefined : invoke,
526
612
  code: typeof code === 'string' ? code : undefined,
527
613
  paramsJson: typeof parsed.values.params === 'string' ? parsed.values.params : undefined,
528
614
  conversationId: typeof parsed.values['conversation-id'] === 'string'
@@ -530,57 +616,225 @@ async function dispatch(parsed, write, writeErr) {
530
616
  : undefined,
531
617
  credentials,
532
618
  });
533
- const apiUrl = apiUrlFrom({
619
+ let apiUrl = apiUrlFrom({
534
620
  apiUrl: typeof parsed.values['api-url'] === 'string'
535
621
  ? parsed.values['api-url']
536
622
  : undefined,
537
623
  });
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({
624
+ let closeMock;
625
+ let mockToken;
626
+ try {
627
+ if (target === 'mock') {
628
+ const { ensureMockTarget } = await import('./mock/mock-server.js');
629
+ const mock = await ensureMockTarget({
630
+ resetStorage: parsed.values['reset-storage'] === true,
631
+ packageId: localPackageInvoke
632
+ ? `local:${localPackageInvoke.kodyId}`
633
+ : undefined,
634
+ });
635
+ apiUrl = mock.apiUrl;
636
+ mockToken = mock.token;
637
+ closeMock = mock.started ? () => mock.server.close() : undefined;
638
+ }
639
+ if (localPackageInvoke) {
640
+ const { buildLocalPackageContext, runLocalPackageModule } = await import('./local-package-run.js');
641
+ const result = await runLocalPackageModule({
642
+ modulePath: localPackageInvoke.modulePath,
643
+ params: args.params,
644
+ token: mockToken ??
645
+ (await resolveLocalExecuteBearer({
646
+ tokenValues,
647
+ mcpUrl,
648
+ apiUrl,
649
+ purpose: 'execute local package export',
650
+ ...(org ? { org } : {}),
651
+ })),
652
+ apiUrl,
653
+ target,
654
+ packageContext: buildLocalPackageContext({
655
+ kodyId: localPackageInvoke.kodyId,
656
+ hostedUrl: 'http://127.0.0.1:0',
657
+ }),
658
+ ...(org ? { org } : {}),
659
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
660
+ onStatus: (message) => writeErr(`${message}\n`),
661
+ });
662
+ write(formatToolResult(result, json));
663
+ return result.isError ? 1 : 0;
664
+ }
665
+ const useToken = local ||
666
+ shouldUseApiToken({
554
667
  tokenValues,
555
668
  mcpUrl,
556
669
  apiUrl,
557
- purpose: 'execute --local',
670
+ allowEnvWithoutLogin: true,
558
671
  ...(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,
672
+ });
673
+ const result = local
674
+ ? await (await import('./local-execute.js')).runLocalExecute({
675
+ code: args.code,
676
+ ...(modulePath !== undefined ? { modulePath } : {}),
569
677
  params: args.params,
570
678
  conversationId: args.conversationId,
571
679
  ...(credentials !== undefined ? { credentials } : {}),
572
- token: requireApiToken(tokenValues, process.env, 'execute with an API token', { apiUrl }),
680
+ token: mockToken ??
681
+ (await resolveLocalExecuteBearer({
682
+ tokenValues,
683
+ mcpUrl,
684
+ apiUrl,
685
+ purpose: 'execute --local',
686
+ ...(org ? { org } : {}),
687
+ })),
573
688
  apiUrl,
574
689
  ...(org ? { org } : {}),
690
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
691
+ onStatus: (message) => writeErr(`${message}\n`),
575
692
  })
576
- : await (await import('./mcp.js')).callKodyTool({
577
- name: 'execute',
578
- args,
579
- mcpUrl,
580
- ...(org ? { org } : {}),
581
- });
582
- write(formatToolResult(result, json));
583
- return result.isError ? 1 : 0;
693
+ : useToken
694
+ ? await (await import('./remote-execute.js')).runRemoteExecuteWithToken({
695
+ code: typeof args.code === 'string' ? args.code : undefined,
696
+ invoke,
697
+ params: args.params,
698
+ conversationId: args.conversationId,
699
+ ...(credentials !== undefined ? { credentials } : {}),
700
+ token: requireApiToken(tokenValues, process.env, 'execute with an API token', { apiUrl }),
701
+ apiUrl,
702
+ ...(org ? { org } : {}),
703
+ })
704
+ : await (await import('./mcp.js')).callKodyTool({
705
+ name: 'execute',
706
+ args,
707
+ mcpUrl,
708
+ ...(org ? { org } : {}),
709
+ });
710
+ write(formatToolResult(result, json));
711
+ return result.isError ? 1 : 0;
712
+ }
713
+ finally {
714
+ await closeMock?.();
715
+ }
716
+ }
717
+ case 'dev': {
718
+ const { resolveTarget } = await import('./target.js');
719
+ const { parseDevSubcommand, resolveDevAuth, runDevCheck, runDevEmit, runDevFetch, runDevJob, runDevServe, runDevWebhook, } = await import('./dev-command.js');
720
+ const target = resolveTarget({
721
+ command: 'dev',
722
+ flag: typeof parsed.values.target === 'string' ? parsed.values.target : undefined,
723
+ });
724
+ const { subcommand, rest } = parseDevSubcommand(parsed.positionals);
725
+ const tokenValues = tokenFlagValues(parsed.values);
726
+ const prodApiUrl = apiUrlFrom({
727
+ apiUrl: typeof parsed.values['api-url'] === 'string'
728
+ ? parsed.values['api-url']
729
+ : undefined,
730
+ });
731
+ const io = { stdout: write, stderr: writeErr };
732
+ const bodyPath = typeof parsed.values.body === 'string' ? parsed.values.body : undefined;
733
+ // `check` only bundles + validates the package tree; it never talks to
734
+ // mock/prod, so skip auth (and the mock server) to avoid leaking a listen
735
+ // handle that would keep the process alive after printing results.
736
+ if (subcommand === 'check') {
737
+ return await runDevCheck({ target, json, io });
738
+ }
739
+ const token = target === 'prod'
740
+ ? await resolveLocalExecuteBearer({
741
+ tokenValues,
742
+ mcpUrl,
743
+ apiUrl: prodApiUrl,
744
+ purpose: 'kody dev',
745
+ ...(org ? { org } : {}),
746
+ })
747
+ : 'mock';
748
+ const auth = await resolveDevAuth({
749
+ target,
750
+ token,
751
+ apiUrl: prodApiUrl,
752
+ ...(org ? { org } : {}),
753
+ resetStorage: parsed.values['reset-storage'] === true,
754
+ });
755
+ const port = typeof parsed.values.port === 'string' ? Number(parsed.values.port) : undefined;
756
+ const appBasePath = typeof parsed.values['app-base-path'] === 'string'
757
+ ? parsed.values['app-base-path']
758
+ : undefined;
759
+ try {
760
+ switch (subcommand) {
761
+ case 'serve':
762
+ return await runDevServe({
763
+ target,
764
+ auth,
765
+ port,
766
+ appBasePath,
767
+ allowPrivateNetwork: parsed.values['allow-private-network'] === true,
768
+ io,
769
+ });
770
+ case 'fetch': {
771
+ const path = rest[0];
772
+ if (!path) {
773
+ throw new Error('Usage: kody dev fetch <path> [--method GET] [--body file.json]');
774
+ }
775
+ let body;
776
+ if (bodyPath)
777
+ body = await readFile(bodyPath, 'utf8');
778
+ return await runDevFetch({
779
+ target,
780
+ auth,
781
+ path,
782
+ method: typeof parsed.values.method === 'string'
783
+ ? parsed.values.method
784
+ : undefined,
785
+ body,
786
+ port,
787
+ appBasePath,
788
+ json,
789
+ io,
790
+ });
791
+ }
792
+ case 'job': {
793
+ const jobName = rest[0];
794
+ if (!jobName)
795
+ throw new Error('Usage: kody dev job <name>');
796
+ return await runDevJob({ target, auth, jobName, json, io });
797
+ }
798
+ case 'webhook': {
799
+ const webhookName = rest[0];
800
+ if (!webhookName || !bodyPath) {
801
+ throw new Error('Usage: kody dev webhook <name> --body file.json');
802
+ }
803
+ return await runDevWebhook({
804
+ target,
805
+ auth,
806
+ webhookName,
807
+ bodyPath,
808
+ json,
809
+ io,
810
+ });
811
+ }
812
+ case 'emit': {
813
+ const event = rest[0];
814
+ if (!event || !bodyPath) {
815
+ throw new Error('Usage: kody dev emit <event> --body event.json');
816
+ }
817
+ return await runDevEmit({
818
+ target,
819
+ auth,
820
+ event,
821
+ bodyPath,
822
+ json,
823
+ io,
824
+ });
825
+ }
826
+ default: {
827
+ const exhaustive = subcommand;
828
+ throw new Error(`Unhandled kody dev subcommand: ${String(exhaustive)}`);
829
+ }
830
+ }
831
+ }
832
+ catch (error) {
833
+ // Usage / surface errors after the mock started must still close it,
834
+ // or the listen handle keeps the process alive (same hang as check).
835
+ await auth.closeMock?.();
836
+ throw error;
837
+ }
584
838
  }
585
839
  case 'api': {
586
840
  const operationId = parsed.positionals[0]?.trim() ?? '';
@@ -606,6 +860,34 @@ async function dispatch(parsed, write, writeErr) {
606
860
  write(`${JSON.stringify(result, null, 2)}\n`);
607
861
  return 0;
608
862
  }
863
+ case 'package': {
864
+ const action = parsed.positionals[0];
865
+ if (action !== 'publish') {
866
+ throw new Error('Usage: kody package publish [<@owner/leaf|package_id>] [--params <json>] [--token <token>] [--api-url <url>] [--org <slug>]');
867
+ }
868
+ const apiUrl = apiUrlFrom({
869
+ apiUrl: typeof parsed.values['api-url'] === 'string'
870
+ ? parsed.values['api-url']
871
+ : undefined,
872
+ });
873
+ const tokenValues = tokenFlagValues(parsed.values);
874
+ const params = packagePublishParams({
875
+ positional: parsed.positionals[1],
876
+ paramsJson: typeof parsed.values.params === 'string' ? parsed.values.params : undefined,
877
+ });
878
+ const { callOpenApiOperation } = await import('./open-api-client.js');
879
+ const result = await callOpenApiOperation({
880
+ operationId: 'packagePublishExternalPush',
881
+ params,
882
+ token: requireApiToken(tokenValues, process.env, 'package publish', {
883
+ apiUrl,
884
+ }),
885
+ apiUrl,
886
+ ...(org ? { org } : {}),
887
+ });
888
+ write(`${JSON.stringify(result, null, 2)}\n`);
889
+ return 0;
890
+ }
609
891
  case 'install': {
610
892
  const { runInstall } = await import('./install.js');
611
893
  const result = await runInstall({
@@ -640,6 +922,20 @@ async function readStdin() {
640
922
  }
641
923
  return Buffer.concat(chunks).toString('utf8');
642
924
  }
925
+ async function tryResolveLocalPackageInvoke(invoke) {
926
+ const trimmed = invoke.trim();
927
+ if (!trimmed || trimmed.startsWith('kody:@'))
928
+ return null;
929
+ try {
930
+ const { loadPackageManifest, resolveExportModulePath } = await import('./package-manifest.js');
931
+ const manifest = await loadPackageManifest(process.cwd());
932
+ const modulePath = resolveExportModulePath(manifest, trimmed);
933
+ return { modulePath, kodyId: manifest.kodyId };
934
+ }
935
+ catch {
936
+ return null;
937
+ }
938
+ }
643
939
  /** True when --invoke is set alongside any code source (--code, --file, or positional). */
644
940
  export function executeSourcesConflict(input) {
645
941
  return (input.invoke !== undefined &&
@@ -822,6 +1118,26 @@ export function parseApiParamsJson(paramsJson) {
822
1118
  }
823
1119
  return parsed;
824
1120
  }
1121
+ const packageIdUuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1122
+ /**
1123
+ * Build `packagePublishExternalPush` params for `kody package publish`.
1124
+ * Optional positional is `package_id` when UUID-shaped, otherwise `kody_id`.
1125
+ * Explicit `--params` keys win over the positional.
1126
+ */
1127
+ export function packagePublishParams(input) {
1128
+ const params = parseApiParamsJson(input.paramsJson);
1129
+ const ref = input.positional?.trim();
1130
+ if (!ref)
1131
+ return params;
1132
+ if (packageIdUuidPattern.test(ref)) {
1133
+ if (params.package_id === undefined)
1134
+ params.package_id = ref;
1135
+ }
1136
+ else if (params.kody_id === undefined) {
1137
+ params.kody_id = ref;
1138
+ }
1139
+ return params;
1140
+ }
825
1141
  /**
826
1142
  * Prefer a scoped API token when the user passed `--token`, or when
827
1143
  * `KODY_API_TOKEN` / a stored bootstrap token is available and there is no