@orchid-labs/pluxx 0.1.0 → 0.1.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 (51) hide show
  1. package/README.md +100 -522
  2. package/dist/cli/agent.d.ts +7 -0
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/eval.d.ts +22 -0
  7. package/dist/cli/eval.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +19 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/init-from-mcp.d.ts +17 -2
  11. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  12. package/dist/cli/install.d.ts +2 -0
  13. package/dist/cli/install.d.ts.map +1 -1
  14. package/dist/cli/lint.d.ts +5 -1
  15. package/dist/cli/lint.d.ts.map +1 -1
  16. package/dist/cli/mcp-proxy.d.ts +10 -0
  17. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  18. package/dist/cli/migrate.d.ts.map +1 -1
  19. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  20. package/dist/cli/test.d.ts +2 -0
  21. package/dist/cli/test.d.ts.map +1 -1
  22. package/dist/generators/claude-code/index.d.ts +2 -0
  23. package/dist/generators/claude-code/index.d.ts.map +1 -1
  24. package/dist/generators/codex/index.d.ts +1 -0
  25. package/dist/generators/codex/index.d.ts.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +99 -1
  29. package/dist/mcp/introspect.d.ts +43 -1
  30. package/dist/mcp/introspect.d.ts.map +1 -1
  31. package/dist/permissions.d.ts.map +1 -1
  32. package/dist/validation/platform-rules.d.ts +20 -0
  33. package/dist/validation/platform-rules.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/cli/agent.ts +459 -34
  36. package/src/cli/doctor.ts +400 -1
  37. package/src/cli/eval.ts +470 -0
  38. package/src/cli/index.ts +633 -114
  39. package/src/cli/init-from-mcp.ts +545 -41
  40. package/src/cli/install.ts +166 -4
  41. package/src/cli/lint.ts +56 -26
  42. package/src/cli/mcp-proxy.ts +322 -0
  43. package/src/cli/migrate.ts +256 -3
  44. package/src/cli/sync-from-mcp.ts +23 -0
  45. package/src/cli/test.ts +10 -2
  46. package/src/generators/claude-code/index.ts +143 -0
  47. package/src/generators/codex/index.ts +23 -0
  48. package/src/index.ts +12 -1
  49. package/src/mcp/introspect.ts +297 -24
  50. package/src/permissions.ts +3 -1
  51. package/src/validation/platform-rules.ts +121 -0
package/src/cli/index.ts CHANGED
@@ -13,10 +13,12 @@ import {
13
13
  runAgentPlan,
14
14
  type AgentPromptKind,
15
15
  type AgentRunner,
16
+ type AgentRunnerModelSummary,
16
17
  } from './agent'
17
- import { doctorProject, printDoctorReport } from './doctor'
18
+ import { doctorConsumer, doctorProject, printDoctorReport } from './doctor'
18
19
  import {
19
20
  ensureHookTrust,
21
+ getInstallFollowupNotes,
20
22
  installPlugin,
21
23
  listHookCommands,
22
24
  planInstallPlugin,
@@ -29,6 +31,7 @@ import {
29
31
  analyzeMcpQuality,
30
32
  applyMcpScaffoldPlan,
31
33
  buildToolExampleRequest,
34
+ deriveDisplayName,
32
35
  derivePluginName,
33
36
  MCP_HOOK_MODES,
34
37
  MCP_RUNTIME_AUTH_MODES,
@@ -42,18 +45,26 @@ import {
42
45
  writeMcpScaffold,
43
46
  } from './init-from-mcp'
44
47
  import { migrate } from './migrate'
48
+ import { runMcpProxy } from './mcp-proxy'
45
49
  import { lintProject, printLintResult, runLint } from './lint'
46
- import { introspectMcpServer, McpIntrospectionError } from '../mcp/introspect'
50
+ import {
51
+ discoverMcpAuthFromError,
52
+ introspectMcpServer,
53
+ McpIntrospectionError,
54
+ type IntrospectedMcpServer,
55
+ } from '../mcp/introspect'
47
56
  import { promptText, promptYesNo, PromptCancelledError } from './prompt'
48
57
  import * as clack from '@clack/prompts'
49
58
  import type { McpAuth, McpServer, TargetPlatform } from '../schema'
50
- import { basename } from 'path'
59
+ import { basename, resolve } from 'path'
51
60
  import { mkdir, mkdtemp, rm } from 'fs/promises'
52
61
  import { tmpdir } from 'os'
62
+ import { spawn } from 'child_process'
53
63
  import { formatSyncSummary, planSyncFromMcp, syncFromMcp } from './sync-from-mcp'
54
64
  import { formatPublishPlan, planPublish, runPublish } from './publish'
55
- import { createCliRuntime, createSpinner, printJson, readMultiValueOption, readOption } from './runtime'
65
+ import { createCliRuntime, createSpinner, printJson, readFlag, readMultiValueOption, readOption } from './runtime'
56
66
  import { printTestResult, runTestSuite, type TestRunResult } from './test'
67
+ import { printEvalReport, runEvalSuite } from './eval'
57
68
 
58
69
  const args = process.argv.slice(2)
59
70
  const command = args[0]
@@ -86,6 +97,7 @@ export interface InitFromMcpOptions {
86
97
  authHeader?: string
87
98
  authTemplate?: string
88
99
  runtimeAuth?: string
100
+ oauthWrapper: boolean
89
101
  grouping?: string
90
102
  hooks?: string
91
103
  transport?: string
@@ -122,6 +134,7 @@ interface AutopilotSummary {
122
134
  source: string
123
135
  mode: AutopilotMode
124
136
  runner: AgentRunner
137
+ model: AgentRunnerModelSummary
125
138
  targets: TargetPlatform[]
126
139
  toolCount: number
127
140
  grouping: McpSkillGrouping
@@ -204,6 +217,9 @@ export async function main() {
204
217
  case 'agent':
205
218
  await runAgent()
206
219
  break
220
+ case 'mcp':
221
+ await runMcp()
222
+ break
207
223
  case 'autopilot':
208
224
  await runAutopilot()
209
225
  break
@@ -228,6 +244,9 @@ export async function main() {
228
244
  case 'test':
229
245
  await runTestCommand()
230
246
  break
247
+ case 'eval':
248
+ await runEvalCommand()
249
+ break
231
250
  case undefined:
232
251
  case 'help':
233
252
  case '--help':
@@ -349,6 +368,59 @@ function formatDuration(durationMs?: number): string | undefined {
349
368
  return `${(durationMs / 1000).toFixed(durationMs >= 10_000 ? 0 : 1)}s`
350
369
  }
351
370
 
371
+ interface InstallActionSummary {
372
+ enabled: boolean
373
+ platforms: TargetPlatform[]
374
+ notes: string[]
375
+ installTargets: Array<{
376
+ platform: TargetPlatform
377
+ pluginDir: string
378
+ built: boolean
379
+ existing: boolean
380
+ }>
381
+ }
382
+
383
+ async function maybeInstallBuiltOutputs(
384
+ config: Awaited<ReturnType<typeof loadConfig>>,
385
+ platforms: TargetPlatform[],
386
+ ): Promise<InstallActionSummary | undefined> {
387
+ if (!args.includes('--install')) {
388
+ return undefined
389
+ }
390
+
391
+ const distDir = `${process.cwd()}/${config.outDir}`
392
+ const installPlan = planInstallPlugin(distDir, config.name, platforms)
393
+
394
+ if (!runtime.dryRun) {
395
+ await ensureHookTrust({
396
+ pluginName: config.name,
397
+ hooks: config.hooks,
398
+ trust: args.includes('--trust'),
399
+ isTTY: runtime.isInteractive,
400
+ })
401
+ const resolvedUserConfig = await resolveInstallUserConfig(config, platforms, {
402
+ isTTY: runtime.isInteractive,
403
+ })
404
+ await installPlugin(distDir, config.name, platforms, {
405
+ config,
406
+ quiet: true,
407
+ resolvedUserConfig,
408
+ })
409
+ }
410
+
411
+ return {
412
+ enabled: true,
413
+ platforms,
414
+ notes: getInstallFollowupNotes(platforms),
415
+ installTargets: installPlan.map((target) => ({
416
+ platform: target.platform,
417
+ pluginDir: target.description,
418
+ built: target.built,
419
+ existing: target.existing,
420
+ })),
421
+ }
422
+ }
423
+
352
424
  function logAutopilotRunnerWait(step: number, totalSteps: number, label: string, runner: AgentRunner): void {
353
425
  if (runtime.jsonOutput || runtime.quiet || !runtime.isInteractive) {
354
426
  return
@@ -393,13 +465,17 @@ async function runBuild() {
393
465
  const targets = parseTargetFlagValues(args)
394
466
  const config = await loadConfig()
395
467
  const platforms = targets ?? config.targets
468
+ const cwd = process.cwd()
469
+ const shouldInstall = args.includes('--install')
396
470
 
397
471
  if (runtime.dryRun) {
472
+ const install = await maybeInstallBuiltOutputs(config, platforms)
398
473
  const summary = {
399
474
  dryRun: true,
400
475
  targets: platforms,
401
476
  outDir: config.outDir,
402
477
  outputPaths: platforms.map((platform) => `${config.outDir}/${platform}/`),
478
+ install,
403
479
  }
404
480
  if (runtime.jsonOutput) {
405
481
  printJson(summary)
@@ -414,7 +490,27 @@ async function runBuild() {
414
490
  console.log(`Building for: ${platforms.join(', ')}`)
415
491
  }
416
492
 
417
- await build(config, process.cwd(), { targets })
493
+ const lintResult = await lintProject(cwd, { targets: platforms })
494
+ if (lintResult.errors > 0) {
495
+ if (runtime.jsonOutput) {
496
+ printJson({
497
+ ok: false,
498
+ reason: 'lint-errors',
499
+ lint: lintResult,
500
+ })
501
+ } else {
502
+ printLintResult(lintResult, cwd)
503
+ console.error('Build aborted due to lint errors.')
504
+ }
505
+ process.exit(1)
506
+ }
507
+
508
+ if (!runtime.jsonOutput && !runtime.quiet && lintResult.warnings > 0) {
509
+ printLintResult(lintResult, cwd)
510
+ }
511
+
512
+ await build(config, cwd, { targets })
513
+ const install = await maybeInstallBuiltOutputs(config, platforms)
418
514
 
419
515
  if (runtime.jsonOutput) {
420
516
  printJson({
@@ -422,6 +518,8 @@ async function runBuild() {
422
518
  targets: platforms,
423
519
  outDir: config.outDir,
424
520
  outputPaths: platforms.map((platform) => `${config.outDir}/${platform}/`),
521
+ lint: lintResult,
522
+ install,
425
523
  })
426
524
  return
427
525
  }
@@ -431,6 +529,15 @@ async function runBuild() {
431
529
  for (const platform of platforms) {
432
530
  console.log(` ${config.outDir}/${platform}/`)
433
531
  }
532
+ if (shouldInstall && install) {
533
+ console.log('Installed for local testing:')
534
+ for (const target of install.installTargets) {
535
+ console.log(` ${target.platform} -> ${target.pluginDir}`)
536
+ }
537
+ for (const note of install.notes) {
538
+ console.log(note)
539
+ }
540
+ }
434
541
  }
435
542
  }
436
543
 
@@ -561,9 +668,120 @@ function defaultHookMode(source: { auth?: { type: string; envVar?: string }; tra
561
668
  return 'none'
562
669
  }
563
670
 
564
- export function resolveRemoteAuthType(options: Pick<InitFromMcpOptions, 'authType' | 'authHeader'>): 'bearer' | 'header' {
671
+ interface McpAuthProviderHint {
672
+ id: 'linear' | 'generic-oauth'
673
+ label: string
674
+ envVar: string
675
+ tokenLabel: string
676
+ tokenAuthType: 'bearer' | 'header'
677
+ authHeader?: string
678
+ authTemplate?: string
679
+ wrapperCommand?: string
680
+ guidance?: string
681
+ }
682
+
683
+ function getSourceUrl(source: McpServer): string | undefined {
684
+ return source.transport === 'stdio' ? undefined : source.url
685
+ }
686
+
687
+ function collectAuthCandidateUrls(
688
+ source: McpServer,
689
+ discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
690
+ ): string[] {
691
+ return [
692
+ getSourceUrl(source),
693
+ discoveredAuth?.authorizationUrl,
694
+ discoveredAuth?.resourceMetadataUrl,
695
+ ].filter((value): value is string => Boolean(value))
696
+ }
697
+
698
+ function matchesHost(urlValue: string, suffix: string): boolean {
699
+ try {
700
+ return new URL(urlValue).hostname.endsWith(suffix)
701
+ } catch {
702
+ return false
703
+ }
704
+ }
705
+
706
+ export function buildOauthWrapperCommand(source: McpServer): string | undefined {
707
+ if (source.transport !== 'http') return undefined
708
+ return `npx -y mcp-remote ${source.url}`
709
+ }
710
+
711
+ export function buildOauthWrapperSource(source: McpServer): McpServer | undefined {
712
+ if (source.transport !== 'http') return undefined
713
+ return {
714
+ transport: 'stdio',
715
+ command: 'npx',
716
+ args: ['-y', 'mcp-remote', source.url],
717
+ }
718
+ }
719
+
720
+ export function inferMcpAuthProvider(
721
+ source: McpServer,
722
+ discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
723
+ ): McpAuthProviderHint | null {
724
+ const candidates = collectAuthCandidateUrls(source, discoveredAuth)
725
+ if (candidates.some((value) => matchesHost(value, 'linear.app'))) {
726
+ return {
727
+ id: 'linear',
728
+ label: 'Linear',
729
+ envVar: 'LINEAR_API_KEY',
730
+ tokenLabel: 'API key or OAuth token',
731
+ tokenAuthType: 'bearer',
732
+ authTemplate: 'Bearer ${value}',
733
+ wrapperCommand: buildOauthWrapperCommand(source),
734
+ guidance: 'Linear supports direct Authorization: Bearer tokens/API keys and the official mcp-remote wrapper for clients that do not support remote MCP.',
735
+ }
736
+ }
737
+
738
+ if (discoveredAuth?.kind === 'platform') {
739
+ return {
740
+ id: 'generic-oauth',
741
+ label: 'OAuth-first MCP',
742
+ envVar: 'OAUTH_ACCESS_TOKEN',
743
+ tokenLabel: 'access token or API key',
744
+ tokenAuthType: 'bearer',
745
+ authTemplate: 'Bearer ${value}',
746
+ wrapperCommand: buildOauthWrapperCommand(source),
747
+ }
748
+ }
749
+
750
+ return null
751
+ }
752
+
753
+ function defaultAuthEnvVar(
754
+ provider: McpAuthProviderHint | null,
755
+ discoveredAuth?: ReturnType<typeof discoverMcpAuthFromError> | null,
756
+ ): string {
757
+ if (provider?.envVar) return provider.envVar
758
+ if (discoveredAuth?.kind === 'header') return 'API_KEY'
759
+ if (discoveredAuth?.kind === 'platform') return 'OAUTH_ACCESS_TOKEN'
760
+ return ''
761
+ }
762
+
763
+ function tryOpenBrowser(url: string): boolean {
764
+ const launcher = process.platform === 'darwin'
765
+ ? { command: 'open', args: [url] }
766
+ : process.platform === 'win32'
767
+ ? { command: 'cmd', args: ['/c', 'start', '', url] }
768
+ : { command: 'xdg-open', args: [url] }
769
+
770
+ try {
771
+ const child = spawn(launcher.command, launcher.args, {
772
+ detached: true,
773
+ stdio: 'ignore',
774
+ })
775
+ child.unref()
776
+ return true
777
+ } catch {
778
+ return false
779
+ }
780
+ }
781
+
782
+ export function resolveRemoteAuthType(options: Pick<InitFromMcpOptions, 'authType' | 'authHeader'>): 'bearer' | 'header' | 'platform' {
565
783
  if (options.authType) {
566
- return parseChoiceOption(options.authType, ['bearer', 'header'] as const, 'Auth type')
784
+ return parseChoiceOption(options.authType, ['bearer', 'header', 'platform'] as const, 'Auth type')
567
785
  }
568
786
 
569
787
  if (options.authHeader && options.authHeader.trim() && options.authHeader.trim().toLowerCase() !== 'authorization') {
@@ -574,11 +792,25 @@ export function resolveRemoteAuthType(options: Pick<InitFromMcpOptions, 'authTyp
574
792
  }
575
793
 
576
794
  export function buildRemoteAuthConfig(options: Pick<InitFromMcpOptions, 'authEnv' | 'authType' | 'authHeader' | 'authTemplate'>): McpAuth | undefined {
795
+ if (options.authType?.trim() === 'platform') {
796
+ return {
797
+ type: 'platform',
798
+ mode: 'oauth',
799
+ }
800
+ }
801
+
577
802
  const envVar = options.authEnv?.trim()
578
803
  if (!envVar) return undefined
579
804
 
580
805
  const authType = resolveRemoteAuthType(options)
581
806
 
807
+ if (authType === 'platform') {
808
+ return {
809
+ type: 'platform',
810
+ mode: 'oauth',
811
+ }
812
+ }
813
+
582
814
  if (authType === 'header') {
583
815
  const headerName = options.authHeader?.trim()
584
816
  if (!headerName) {
@@ -657,40 +889,82 @@ function isAuthRequiredError(error: unknown): error is McpIntrospectionError {
657
889
  }
658
890
 
659
891
  function isLikelyOAuthFirstError(error: McpIntrospectionError): boolean {
660
- const message = error.message.toLowerCase()
661
- const wwwAuthenticate = error.context?.responseHeaders?.['www-authenticate']?.toLowerCase() ?? ''
662
- const location = error.context?.responseHeaders?.location?.toLowerCase() ?? ''
663
- const body = error.context?.responseBodySnippet?.toLowerCase() ?? ''
664
- const responseUrl = error.context?.responseUrl?.toLowerCase() ?? ''
665
-
666
- return message.includes('oauth')
667
- || wwwAuthenticate.includes('oauth')
668
- || wwwAuthenticate.includes('authorization_uri=')
669
- || location.includes('oauth')
670
- || location.includes('authorize')
671
- || location.includes('login')
672
- || responseUrl.includes('oauth')
673
- || responseUrl.includes('authorize')
674
- || responseUrl.includes('login')
675
- || body.includes('oauth')
676
- || body.includes('authorize')
677
- }
678
-
679
- function formatAuthRequiredMessage(commandName: 'init' | 'autopilot', error?: McpIntrospectionError): string {
680
- const rerun = `Re-run ${commandName} with --auth-env YOUR_ENV_VAR and either:
892
+ return discoverMcpAuthFromError(error)?.kind === 'platform'
893
+ }
894
+
895
+ function formatAuthRequiredMessage(
896
+ commandName: 'init' | 'autopilot',
897
+ error?: McpIntrospectionError,
898
+ source?: McpServer,
899
+ ): string {
900
+ const discoveredAuth = error ? discoverMcpAuthFromError(error) : null
901
+ const provider = source ? inferMcpAuthProvider(source, discoveredAuth) : null
902
+ const rerun = discoveredAuth?.kind === 'header' && discoveredAuth.headerName
903
+ ? `Re-run ${commandName} with --auth-env YOUR_ENV_VAR --auth-type header --auth-header ${discoveredAuth.headerName} [--auth-template '\${value}']`
904
+ : `Re-run ${commandName} with --auth-env YOUR_ENV_VAR and either:
681
905
  - Bearer auth: --auth-type bearer
682
906
  - Custom header auth: --auth-type header --auth-header HEADER_NAME [--auth-template '\${value}']`
683
- const oauthNote = error && isLikelyOAuthFirstError(error)
907
+ const providerNote = provider?.guidance ? `\n\n${provider.guidance}` : ''
908
+ const wrapperNote = provider?.wrapperCommand
909
+ ? `\nLocal wrapper/proxy helper: ${provider.wrapperCommand}`
910
+ : ''
911
+ const oauthNote = discoveredAuth?.kind === 'platform'
684
912
  ? `
685
913
 
686
- This server appears OAuth-first. Complete the provider's OAuth flow first, export the resulting token/API key to YOUR_ENV_VAR, then rerun.
687
- If it requires browser-interactive OAuth during handshake, run a local stdio MCP wrapper/proxy and import that command instead.`
914
+ This server appears OAuth-first${discoveredAuth.authorizationUrl ? ` (${discoveredAuth.authorizationUrl})` : ''}. Complete the provider's OAuth flow first, export the resulting token/API key to YOUR_ENV_VAR, then rerun.
915
+ If the server supports public discovery and only needs OAuth at runtime, you can scaffold it with --auth-type platform --runtime-auth platform.
916
+ If it requires browser-interactive OAuth during handshake, run a local stdio MCP wrapper/proxy and import that command instead.${wrapperNote}${providerNote}`
688
917
  : ''
689
918
 
690
919
  return `This MCP server requires authentication.
691
920
  ${rerun}${oauthNote}`
692
921
  }
693
922
 
923
+ type OAuthImportStrategy = 'token' | 'wrapper'
924
+
925
+ async function chooseOAuthImportStrategy(
926
+ provider: McpAuthProviderHint | null,
927
+ ): Promise<OAuthImportStrategy> {
928
+ const options: Array<{ value: OAuthImportStrategy; label: string; hint?: string }> = [
929
+ {
930
+ value: 'token',
931
+ label: 'token',
932
+ hint: `Complete auth in the browser, then continue with a ${provider?.tokenLabel ?? 'token or API key'}`,
933
+ },
934
+ ]
935
+
936
+ if (provider?.wrapperCommand) {
937
+ options.push({
938
+ value: 'wrapper',
939
+ label: 'wrapper',
940
+ hint: `Import through ${provider.wrapperCommand}`,
941
+ })
942
+ }
943
+
944
+ return await clackSelect('OAuth import path', options, provider?.wrapperCommand ? 'wrapper' : 'token')
945
+ }
946
+
947
+ async function maybeCaptureOAuthCredential(
948
+ provider: McpAuthProviderHint | null,
949
+ authorizationUrl?: string,
950
+ ): Promise<string | undefined> {
951
+ if (authorizationUrl) {
952
+ const opened = tryOpenBrowser(authorizationUrl)
953
+ const message = opened
954
+ ? `Opened ${authorizationUrl} in your browser. Finish the auth flow, then paste the resulting ${provider?.tokenLabel ?? 'token or API key'} below if you want Pluxx to retry immediately.`
955
+ : `Open ${authorizationUrl} in your browser. Finish the auth flow, then paste the resulting ${provider?.tokenLabel ?? 'token or API key'} below if you want Pluxx to retry immediately.`
956
+ clack.note(message, 'OAuth flow')
957
+ }
958
+
959
+ const credential = (await clackPassword(`Paste ${provider?.tokenLabel ?? 'token or API key'} for this session (optional)`)).trim()
960
+ return credential || undefined
961
+ }
962
+
963
+ function applySessionCredential(envVar: string | undefined, credential: string | undefined) {
964
+ if (!envVar || !credential) return
965
+ process.env[envVar] = credential
966
+ }
967
+
694
968
  function buildInitSummary(input: {
695
969
  pluginName: string
696
970
  displayName: string
@@ -765,6 +1039,21 @@ function formatMcpQualityLines(report: McpQualityReport): string[] {
765
1039
  return lines
766
1040
  }
767
1041
 
1042
+ function formatMcpDiscoverySummary(introspection: IntrospectedMcpServer): string {
1043
+ const parts = [`${introspection.tools.length} tools`]
1044
+ const resourceCount = (introspection.resources?.length ?? 0) + (introspection.resourceTemplates?.length ?? 0)
1045
+ const promptCount = introspection.prompts?.length ?? 0
1046
+
1047
+ if (resourceCount > 0) {
1048
+ parts.push(`${resourceCount} resources`)
1049
+ }
1050
+ if (promptCount > 0) {
1051
+ parts.push(`${promptCount} prompts`)
1052
+ }
1053
+
1054
+ return `${parts.join(', ')} discovered`
1055
+ }
1056
+
768
1057
  export function parseInitFromMcpOptions(rawArgs: string[], initialName?: string, initialSource?: string): InitFromMcpOptions {
769
1058
  return {
770
1059
  source: initialSource ?? readOption(rawArgs, '--from-mcp'),
@@ -778,6 +1067,7 @@ export function parseInitFromMcpOptions(rawArgs: string[], initialName?: string,
778
1067
  authHeader: readOption(rawArgs, '--auth-header'),
779
1068
  authTemplate: readOption(rawArgs, '--auth-template'),
780
1069
  runtimeAuth: readOption(rawArgs, '--runtime-auth'),
1070
+ oauthWrapper: rawArgs.includes('--oauth-wrapper'),
781
1071
  grouping: readOption(rawArgs, '--grouping'),
782
1072
  hooks: readOption(rawArgs, '--hooks'),
783
1073
  transport: readOption(rawArgs, '--transport'),
@@ -978,6 +1268,7 @@ async function runInitFromMcp(initialName?: string, initialSource?: string) {
978
1268
  }
979
1269
 
980
1270
  let source = parseMcpSourceInput(rawSource, options.transport)
1271
+ let introspectionSource = source
981
1272
  const configuredRemoteAuth = source.transport === 'stdio'
982
1273
  ? undefined
983
1274
  : buildRemoteAuthConfig(options)
@@ -993,55 +1284,114 @@ async function runInitFromMcp(initialName?: string, initialSource?: string) {
993
1284
 
994
1285
  let introspection
995
1286
  try {
996
- introspection = await introspectMcpServer(source)
1287
+ introspection = await introspectMcpServer(introspectionSource)
997
1288
  } catch (error) {
998
1289
  if (source.transport !== 'stdio' && isAuthRequiredError(error)) {
1290
+ const discoveredAuth = error instanceof McpIntrospectionError ? discoverMcpAuthFromError(error) : null
1291
+ const provider = inferMcpAuthProvider(source, discoveredAuth)
999
1292
  s?.stop('Server requires authentication')
1000
- const envVar = options.authEnv ?? (interactive
1001
- ? await clackText('Auth env var for this MCP server')
1002
- : '')
1003
- if (!envVar) {
1004
- throw new Error(formatAuthRequiredMessage('init', error))
1005
- }
1006
-
1293
+ let envVar = options.authEnv
1007
1294
  let authType = options.authType
1008
1295
  let authHeader = options.authHeader
1009
1296
  let authTemplate = options.authTemplate
1297
+ let usedOauthWrapper = false
1298
+
1299
+ if (!usedOauthWrapper && discoveredAuth?.kind === 'platform' && (interactive || options.oauthWrapper)) {
1300
+ const strategy = options.oauthWrapper
1301
+ ? 'wrapper'
1302
+ : interactive
1303
+ ? await chooseOAuthImportStrategy(provider)
1304
+ : 'token'
1305
+
1306
+ if (strategy === 'wrapper') {
1307
+ const wrapperSource = buildOauthWrapperSource(source)
1308
+ if (!wrapperSource) {
1309
+ throw new Error(formatAuthRequiredMessage('init', error, source))
1310
+ }
1311
+ introspectionSource = wrapperSource
1312
+ runtimeAuthMode = 'platform'
1313
+ s?.start('Step 1/4 · Reconnecting via local wrapper...')
1314
+ try {
1315
+ introspection = await introspectMcpServer(introspectionSource)
1316
+ usedOauthWrapper = true
1317
+ } catch (wrapperError) {
1318
+ if (isAuthRequiredError(wrapperError)) {
1319
+ throw new Error(`Wrapper-based OAuth import failed.
1320
+ ${formatAuthRequiredMessage('init', wrapperError, source)}`)
1321
+ }
1322
+ throw new Error(`MCP introspection failed via local wrapper: ${wrapperError instanceof Error ? wrapperError.message : String(wrapperError)}`)
1323
+ }
1324
+ }
1325
+ }
1010
1326
 
1011
- if (interactive && !authType) {
1012
- authType = await clackSelect<'bearer' | 'header'>('Auth type', [
1013
- { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
1014
- { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
1015
- ], 'bearer')
1327
+ if (!options.runtimeAuth && (discoveredAuth?.kind === 'platform' || usedOauthWrapper)) {
1328
+ runtimeAuthMode = 'platform'
1016
1329
  }
1017
1330
 
1018
- if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
1019
- if (interactive && !authHeader) {
1020
- authHeader = await clackText('Auth header name', 'X-API-Key')
1331
+ if (usedOauthWrapper) {
1332
+ envVar = envVar ?? (interactive
1333
+ ? await clackText('Auth env var for generated non-platform targets (optional)', defaultAuthEnvVar(provider, discoveredAuth))
1334
+ : undefined)
1335
+ source = {
1336
+ ...source,
1337
+ auth: envVar
1338
+ ? buildRemoteAuthConfig({
1339
+ authEnv: envVar,
1340
+ authType: provider?.tokenAuthType ?? 'bearer',
1341
+ authHeader: provider?.authHeader,
1342
+ authTemplate: provider?.authTemplate,
1343
+ })
1344
+ : { type: 'platform', mode: 'oauth' },
1021
1345
  }
1022
- if (interactive && !authTemplate) {
1023
- authTemplate = await clackText('Auth header template', '${value}')
1346
+ } else {
1347
+ envVar = envVar ?? (interactive
1348
+ ? await clackText('Auth env var for this MCP server', defaultAuthEnvVar(provider, discoveredAuth))
1349
+ : '')
1350
+ if (!envVar) {
1351
+ throw new Error(formatAuthRequiredMessage('init', error, source))
1024
1352
  }
1025
- }
1026
1353
 
1027
- source = {
1028
- ...source,
1029
- auth: buildRemoteAuthConfig({
1030
- authEnv: envVar,
1031
- authType,
1032
- authHeader,
1033
- authTemplate,
1034
- }),
1035
- }
1036
- s?.start('Step 1/4 \u00b7 Reconnecting with auth...')
1037
- try {
1038
- introspection = await introspectMcpServer(source)
1039
- } catch (retryError) {
1040
- if (isAuthRequiredError(retryError)) {
1041
- throw new Error(`Authentication failed after retry.
1042
- ${formatAuthRequiredMessage('init', retryError)}`)
1354
+ if (interactive && discoveredAuth?.kind === 'platform') {
1355
+ const credential = await maybeCaptureOAuthCredential(provider, discoveredAuth.authorizationUrl)
1356
+ applySessionCredential(envVar, credential)
1357
+ }
1358
+
1359
+ if (interactive && !authType) {
1360
+ authType = await clackSelect<'bearer' | 'header'>('Auth type', [
1361
+ { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
1362
+ { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
1363
+ ], discoveredAuth?.kind === 'header' ? 'header' : (provider?.tokenAuthType ?? 'bearer'))
1364
+ }
1365
+
1366
+ if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
1367
+ if (interactive && !authHeader) {
1368
+ authHeader = await clackText('Auth header name', discoveredAuth?.headerName ?? provider?.authHeader ?? 'X-API-Key')
1369
+ }
1370
+ if (interactive && !authTemplate) {
1371
+ authTemplate = await clackText('Auth header template', provider?.authTemplate ?? '${value}')
1372
+ }
1373
+ }
1374
+
1375
+ source = {
1376
+ ...source,
1377
+ auth: buildRemoteAuthConfig({
1378
+ authEnv: envVar,
1379
+ authType,
1380
+ authHeader,
1381
+ authTemplate,
1382
+ }),
1383
+ }
1384
+ introspectionSource = source
1385
+ s?.start('Step 1/4 · Reconnecting with auth...')
1386
+ try {
1387
+ introspection = await introspectMcpServer(introspectionSource)
1388
+ } catch (retryError) {
1389
+ if (isAuthRequiredError(retryError)) {
1390
+ throw new Error(`Authentication failed after retry.
1391
+ ${formatAuthRequiredMessage('init', retryError, source)}`)
1392
+ }
1393
+ throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
1043
1394
  }
1044
- throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
1045
1395
  }
1046
1396
  } else {
1047
1397
  s?.stop('Connection failed')
@@ -1049,8 +1399,12 @@ ${formatAuthRequiredMessage('init', retryError)}`)
1049
1399
  }
1050
1400
  }
1051
1401
 
1402
+ if (!introspection) {
1403
+ throw new Error('MCP introspection did not return server metadata.')
1404
+ }
1405
+
1052
1406
  const serverLabel = introspection.serverInfo.title ?? introspection.serverInfo.name
1053
- s?.stop(`Connected: ${serverLabel} (${introspection.tools.length} tools discovered)`)
1407
+ s?.stop(`Connected: ${serverLabel} (${formatMcpDiscoverySummary(introspection)})`)
1054
1408
  const quality = analyzeMcpQuality(introspection.tools)
1055
1409
 
1056
1410
  if (!options.jsonOutput && !runtime.quiet && quality.issues.length > 0) {
@@ -1081,6 +1435,7 @@ ${formatAuthRequiredMessage('init', retryError)}`)
1081
1435
  && source.auth.type !== 'none'
1082
1436
  && !options.runtimeAuth
1083
1437
  ) {
1438
+ runtimeAuthMode = source.auth.type === 'platform' ? 'platform' : runtimeAuthMode
1084
1439
  runtimeAuthMode = await clackSelect<McpRuntimeAuthMode>('Claude/Cursor runtime auth', [
1085
1440
  { value: 'inline', label: 'inline', hint: 'Generate env/header auth directly into plugin output' },
1086
1441
  { value: 'platform', label: 'platform', hint: 'Use native platform-managed auth (for example OAuth/custom connector flows)' },
@@ -1099,7 +1454,7 @@ ${formatAuthRequiredMessage('init', retryError)}`)
1099
1454
  ? await clackText('Plugin name', defaultPluginName)
1100
1455
  : defaultPluginName),
1101
1456
  )
1102
- const defaultDisplayName = options.displayName ?? introspection.serverInfo.title ?? pluginName
1457
+ const defaultDisplayName = options.displayName ?? deriveDisplayName(introspection, pluginName)
1103
1458
  const displayName = options.displayName ?? (interactive
1104
1459
  ? await clackText('Display name', defaultDisplayName)
1105
1460
  : defaultDisplayName)
@@ -1294,6 +1649,18 @@ async function clackText(message: string, defaultValue?: string): Promise<string
1294
1649
  return result
1295
1650
  }
1296
1651
 
1652
+ /** Wrapper for clack.password that handles cancellation. */
1653
+ async function clackPassword(message: string): Promise<string> {
1654
+ const result = await clack.password({
1655
+ message,
1656
+ mask: '*',
1657
+ })
1658
+ if (clack.isCancel(result)) {
1659
+ throw new PromptCancelledError()
1660
+ }
1661
+ return result
1662
+ }
1663
+
1297
1664
  /** Wrapper for clack.select that handles cancellation. */
1298
1665
  async function clackSelect<T extends string>(
1299
1666
  message: string,
@@ -1347,7 +1714,12 @@ async function runSync() {
1347
1714
  }
1348
1715
 
1349
1716
  async function runDoctor() {
1350
- const report = await doctorProject(process.cwd())
1717
+ const consumerMode = readFlag(args, '--consumer')
1718
+ const doctorPath = args.slice(1).find((value) => !value.startsWith('-'))
1719
+ const rootDir = doctorPath ? resolve(process.cwd(), doctorPath) : process.cwd()
1720
+ const report = consumerMode
1721
+ ? await doctorConsumer(rootDir)
1722
+ : await doctorProject(rootDir)
1351
1723
 
1352
1724
  if (runtime.jsonOutput) {
1353
1725
  printJson(report)
@@ -1483,6 +1855,7 @@ async function runAgent() {
1483
1855
  pluginName: plan.pluginName,
1484
1856
  kind: plan.kind,
1485
1857
  runner: plan.runner,
1858
+ model: plan.model,
1486
1859
  verify: plan.verify,
1487
1860
  command: plan.command,
1488
1861
  commandDisplay: plan.commandDisplay,
@@ -1500,6 +1873,7 @@ async function runAgent() {
1500
1873
  } else if (!runtime.quiet) {
1501
1874
  console.log(`Planned ${plan.kind} run for ${plan.pluginName}`)
1502
1875
  console.log(` Runner: ${plan.runner}`)
1876
+ console.log(` Model: ${plan.model.display}`)
1503
1877
  console.log(` Command: ${plan.commandDisplay}`)
1504
1878
  }
1505
1879
  return
@@ -1519,6 +1893,7 @@ async function runAgent() {
1519
1893
 
1520
1894
  if (!runtime.quiet) {
1521
1895
  console.log(`Completed ${result.kind} run for ${result.pluginName} via ${result.runner}`)
1896
+ console.log(` Model: ${result.model.display}`)
1522
1897
  if (!verboseRunner) {
1523
1898
  console.log(' Runner logs: suppressed (use --verbose-runner to stream)')
1524
1899
  }
@@ -1557,12 +1932,12 @@ async function runAutopilot() {
1557
1932
  let runtimeAuthMode = resolveRuntimeAuthMode(initOptions.runtimeAuth)
1558
1933
 
1559
1934
  if (!initOptions.source && !interactive) {
1560
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1935
+ console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1561
1936
  process.exit(1)
1562
1937
  }
1563
1938
 
1564
1939
  if ((!runnerRaw || !AGENT_RUNNERS.includes(runnerRaw as AgentRunner)) && !interactive) {
1565
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1940
+ console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join('|')}> [--mode <${AUTOPILOT_MODES.join('|')}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`)
1566
1941
  process.exit(1)
1567
1942
  }
1568
1943
 
@@ -1610,6 +1985,7 @@ async function runAutopilot() {
1610
1985
  }
1611
1986
 
1612
1987
  let source = parseMcpSourceInput(rawSource, initOptions.transport)
1988
+ let introspectionSource = source
1613
1989
  const configuredRemoteAuth = source.transport === 'stdio'
1614
1990
  ? undefined
1615
1991
  : buildRemoteAuthConfig({
@@ -1631,51 +2007,110 @@ async function runAutopilot() {
1631
2007
 
1632
2008
  let introspection
1633
2009
  try {
1634
- introspection = await introspectMcpServer(source)
2010
+ introspection = await introspectMcpServer(introspectionSource)
1635
2011
  } catch (error) {
1636
2012
  if (source.transport !== 'stdio' && isAuthRequiredError(error)) {
2013
+ const discoveredAuth = error instanceof McpIntrospectionError ? discoverMcpAuthFromError(error) : null
2014
+ const provider = inferMcpAuthProvider(source, discoveredAuth)
1637
2015
  connectSpinner?.stop('Server requires authentication')
1638
- authEnv = authEnv ?? (interactive
1639
- ? await clackText('Auth env var for this MCP server')
1640
- : '')
1641
- if (!authEnv) {
1642
- throw new Error(formatAuthRequiredMessage('autopilot', error))
2016
+ let usedOauthWrapper = false
2017
+
2018
+ if (!usedOauthWrapper && discoveredAuth?.kind === 'platform' && (interactive || initOptions.oauthWrapper)) {
2019
+ const strategy = initOptions.oauthWrapper
2020
+ ? 'wrapper'
2021
+ : interactive
2022
+ ? await chooseOAuthImportStrategy(provider)
2023
+ : 'token'
2024
+
2025
+ if (strategy === 'wrapper') {
2026
+ const wrapperSource = buildOauthWrapperSource(source)
2027
+ if (!wrapperSource) {
2028
+ throw new Error(formatAuthRequiredMessage('autopilot', error, source))
2029
+ }
2030
+ introspectionSource = wrapperSource
2031
+ runtimeAuthMode = 'platform'
2032
+ connectSpinner?.start('Autopilot · Reconnecting via local wrapper...')
2033
+ try {
2034
+ introspection = await introspectMcpServer(introspectionSource)
2035
+ usedOauthWrapper = true
2036
+ } catch (wrapperError) {
2037
+ if (isAuthRequiredError(wrapperError)) {
2038
+ throw new Error(`Wrapper-based OAuth import failed.
2039
+ ${formatAuthRequiredMessage('autopilot', wrapperError, source)}`)
2040
+ }
2041
+ throw new Error(`MCP introspection failed via local wrapper: ${wrapperError instanceof Error ? wrapperError.message : String(wrapperError)}`)
2042
+ }
2043
+ }
1643
2044
  }
1644
2045
 
1645
- if (interactive && !authType) {
1646
- authType = await clackSelect<'bearer' | 'header'>('Auth type', [
1647
- { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
1648
- { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
1649
- ], 'bearer')
2046
+ if (!initOptions.runtimeAuth && (discoveredAuth?.kind === 'platform' || usedOauthWrapper)) {
2047
+ runtimeAuthMode = 'platform'
1650
2048
  }
1651
2049
 
1652
- if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
1653
- if (interactive && !authHeader) {
1654
- authHeader = await clackText('Auth header name', 'X-API-Key')
2050
+ if (usedOauthWrapper) {
2051
+ authEnv = authEnv ?? (interactive
2052
+ ? await clackText('Auth env var for generated non-platform targets (optional)', defaultAuthEnvVar(provider, discoveredAuth))
2053
+ : undefined)
2054
+ source = {
2055
+ ...source,
2056
+ auth: authEnv
2057
+ ? buildRemoteAuthConfig({
2058
+ authEnv,
2059
+ authType: provider?.tokenAuthType ?? 'bearer',
2060
+ authHeader: provider?.authHeader,
2061
+ authTemplate: provider?.authTemplate,
2062
+ })
2063
+ : { type: 'platform', mode: 'oauth' },
1655
2064
  }
1656
- if (interactive && !authTemplate) {
1657
- authTemplate = await clackText('Auth header template', '${value}')
2065
+ } else {
2066
+ authEnv = authEnv ?? (interactive
2067
+ ? await clackText('Auth env var for this MCP server', defaultAuthEnvVar(provider, discoveredAuth))
2068
+ : '')
2069
+ if (!authEnv) {
2070
+ throw new Error(formatAuthRequiredMessage('autopilot', error, source))
1658
2071
  }
1659
- }
1660
2072
 
1661
- source = {
1662
- ...source,
1663
- auth: buildRemoteAuthConfig({
1664
- authEnv,
1665
- authType,
1666
- authHeader,
1667
- authTemplate,
1668
- }),
1669
- }
1670
- connectSpinner?.start('Autopilot · Reconnecting with auth...')
1671
- try {
1672
- introspection = await introspectMcpServer(source)
1673
- } catch (retryError) {
1674
- if (isAuthRequiredError(retryError)) {
1675
- throw new Error(`Authentication failed after retry.
1676
- ${formatAuthRequiredMessage('autopilot', retryError)}`)
2073
+ if (interactive && discoveredAuth?.kind === 'platform') {
2074
+ const credential = await maybeCaptureOAuthCredential(provider, discoveredAuth.authorizationUrl)
2075
+ applySessionCredential(authEnv, credential)
2076
+ }
2077
+
2078
+ if (interactive && !authType) {
2079
+ authType = await clackSelect<'bearer' | 'header'>('Auth type', [
2080
+ { value: 'bearer', label: 'bearer', hint: 'Authorization: Bearer <token>' },
2081
+ { value: 'header', label: 'header', hint: 'Custom header such as X-API-Key' },
2082
+ ], discoveredAuth?.kind === 'header' ? 'header' : (provider?.tokenAuthType ?? 'bearer'))
2083
+ }
2084
+
2085
+ if (resolveRemoteAuthType({ authType, authHeader }) === 'header') {
2086
+ if (interactive && !authHeader) {
2087
+ authHeader = await clackText('Auth header name', discoveredAuth?.headerName ?? provider?.authHeader ?? 'X-API-Key')
2088
+ }
2089
+ if (interactive && !authTemplate) {
2090
+ authTemplate = await clackText('Auth header template', provider?.authTemplate ?? '${value}')
2091
+ }
2092
+ }
2093
+
2094
+ source = {
2095
+ ...source,
2096
+ auth: buildRemoteAuthConfig({
2097
+ authEnv,
2098
+ authType,
2099
+ authHeader,
2100
+ authTemplate,
2101
+ }),
2102
+ }
2103
+ introspectionSource = source
2104
+ connectSpinner?.start('Autopilot · Reconnecting with auth...')
2105
+ try {
2106
+ introspection = await introspectMcpServer(introspectionSource)
2107
+ } catch (retryError) {
2108
+ if (isAuthRequiredError(retryError)) {
2109
+ throw new Error(`Authentication failed after retry.
2110
+ ${formatAuthRequiredMessage('autopilot', retryError, source)}`)
2111
+ }
2112
+ throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
1677
2113
  }
1678
- throw new Error(`MCP introspection failed after auth retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`)
1679
2114
  }
1680
2115
  } else {
1681
2116
  connectSpinner?.stop('Connection failed')
@@ -1683,6 +2118,10 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
1683
2118
  }
1684
2119
  }
1685
2120
 
2121
+ if (!introspection) {
2122
+ throw new Error('MCP introspection did not return server metadata.')
2123
+ }
2124
+
1686
2125
  const stdioHasEnv = source.transport === 'stdio'
1687
2126
  && source.env
1688
2127
  && Object.keys(source.env).length > 0
@@ -1703,12 +2142,13 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
1703
2142
  && source.auth.type !== 'none'
1704
2143
  && !initOptions.runtimeAuth
1705
2144
  ) {
2145
+ runtimeAuthMode = source.auth.type === 'platform' ? 'platform' : runtimeAuthMode
1706
2146
  runtimeAuthMode = await clackSelect<McpRuntimeAuthMode>('Claude/Cursor runtime auth', [
1707
2147
  { value: 'inline', label: 'inline', hint: 'Generate env/header auth directly into plugin output' },
1708
2148
  { value: 'platform', label: 'platform', hint: 'Use native platform-managed auth (for example OAuth/custom connector flows)' },
1709
2149
  ], runtimeAuthMode)
1710
2150
  }
1711
- connectSpinner?.stop(`Connected: ${introspection.serverInfo.title ?? introspection.serverInfo.name} (${introspection.tools.length} tools discovered)`)
2151
+ connectSpinner?.stop(`Connected: ${introspection.serverInfo.title ?? introspection.serverInfo.name} (${formatMcpDiscoverySummary(introspection)})`)
1712
2152
  const quality = analyzeMcpQuality(introspection.tools)
1713
2153
 
1714
2154
  if (!runtime.jsonOutput && !runtime.quiet && quality.issues.length > 0) {
@@ -1721,7 +2161,7 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
1721
2161
  ? await clackText('Plugin name', defaultPluginName)
1722
2162
  : defaultPluginName),
1723
2163
  )
1724
- const defaultDisplayName = initOptions.displayName ?? introspection.serverInfo.title ?? pluginName
2164
+ const defaultDisplayName = initOptions.displayName ?? deriveDisplayName(introspection, pluginName)
1725
2165
  const displayName = initOptions.displayName ?? (interactive
1726
2166
  ? await clackText('Display name', defaultDisplayName)
1727
2167
  : defaultDisplayName)
@@ -1859,6 +2299,10 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
1859
2299
  source: rawSource,
1860
2300
  mode,
1861
2301
  runner,
2302
+ model: taxonomyPlan?.model ?? instructionsPlan?.model ?? reviewPlan?.model ?? {
2303
+ source: 'unknown',
2304
+ display: 'local default (CLI-managed)',
2305
+ },
1862
2306
  targets,
1863
2307
  toolCount: introspection.tools.length,
1864
2308
  grouping,
@@ -1911,6 +2355,7 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
1911
2355
  console.log(` Mode: ${mode}`)
1912
2356
  console.log(` Import: ${introspection.tools.length} tools -> ${targets.join(', ')}`)
1913
2357
  console.log(` Runner: ${runner}`)
2358
+ console.log(` Model: ${summary.model.display}`)
1914
2359
  console.log(` Workload: ${summarizeAutopilotWorkload({
1915
2360
  taxonomy: passDecisions.taxonomy,
1916
2361
  instructions: passDecisions.instructions,
@@ -2056,6 +2501,10 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
2056
2501
  source: rawSource,
2057
2502
  mode,
2058
2503
  runner,
2504
+ model: taxonomyPlan?.model ?? instructionsPlan?.model ?? reviewPlan?.model ?? {
2505
+ source: 'unknown',
2506
+ display: 'local default (CLI-managed)',
2507
+ },
2059
2508
  targets,
2060
2509
  toolCount: introspection.tools.length,
2061
2510
  grouping,
@@ -2117,6 +2566,7 @@ ${formatAuthRequiredMessage('autopilot', retryError)}`)
2117
2566
  console.log(` Mode: ${mode}`)
2118
2567
  console.log(` Import: ${introspection.tools.length} tools -> ${targets.join(', ')}`)
2119
2568
  console.log(` Runner: ${runner}`)
2569
+ console.log(` Model: ${summary.model.display}`)
2120
2570
  console.log(` Workload: ${summarizeAutopilotWorkload({
2121
2571
  taxonomy: passDecisions.taxonomy,
2122
2572
  instructions: passDecisions.instructions,
@@ -2170,14 +2620,31 @@ async function runTestCommand() {
2170
2620
  rootDir: process.cwd(),
2171
2621
  targets,
2172
2622
  })
2623
+ const config = result.config.ok ? await loadConfig() : null
2624
+ const platforms = targets ?? config?.targets ?? []
2625
+ const install = result.ok && config
2626
+ ? await maybeInstallBuiltOutputs(config, platforms)
2627
+ : undefined
2173
2628
 
2174
2629
  if (runtime.jsonOutput) {
2175
- printJson(result)
2630
+ printJson({
2631
+ ...result,
2632
+ install,
2633
+ })
2176
2634
  return
2177
2635
  }
2178
2636
 
2179
2637
  if (!runtime.quiet) {
2180
2638
  printTestResult(result)
2639
+ if (install) {
2640
+ console.log('Installed for local testing:')
2641
+ for (const target of install.installTargets) {
2642
+ console.log(` ${target.platform} -> ${target.pluginDir}`)
2643
+ }
2644
+ for (const note of install.notes) {
2645
+ console.log(note)
2646
+ }
2647
+ }
2181
2648
  }
2182
2649
 
2183
2650
  if (!result.ok) {
@@ -2185,6 +2652,25 @@ async function runTestCommand() {
2185
2652
  }
2186
2653
  }
2187
2654
 
2655
+ async function runEvalCommand() {
2656
+ const report = await runEvalSuite({
2657
+ rootDir: process.cwd(),
2658
+ })
2659
+
2660
+ if (runtime.jsonOutput) {
2661
+ printJson(report)
2662
+ return
2663
+ }
2664
+
2665
+ if (!runtime.quiet) {
2666
+ printEvalReport(report)
2667
+ }
2668
+
2669
+ if (!report.ok) {
2670
+ process.exit(1)
2671
+ }
2672
+ }
2673
+
2188
2674
  async function runInstall() {
2189
2675
  const trust = args.includes('--trust')
2190
2676
  const targets = parseTargetFlagValues(args)
@@ -2201,6 +2687,7 @@ async function runInstall() {
2201
2687
  dryRun: true,
2202
2688
  pluginName: config.name,
2203
2689
  platforms,
2690
+ notes: getInstallFollowupNotes(platforms),
2204
2691
  trustRequired: hookCommands.length > 0,
2205
2692
  userConfig: plannedUserConfig.map((entry) => ({
2206
2693
  key: entry.field.key,
@@ -2234,6 +2721,9 @@ async function runInstall() {
2234
2721
  if (listHookCommands(config.hooks).length > 0) {
2235
2722
  console.log(' trust reminder: this plugin defines local hook commands; install requires review or --trust')
2236
2723
  }
2724
+ for (const note of getInstallFollowupNotes(platforms)) {
2725
+ console.log(` note: ${note}`)
2726
+ }
2237
2727
  }
2238
2728
  return
2239
2729
  }
@@ -2338,25 +2828,45 @@ async function runMigrate() {
2338
2828
  await migrate(inputPath)
2339
2829
  }
2340
2830
 
2831
+ async function runMcp() {
2832
+ const subcommand = args[1]
2833
+ if (subcommand !== 'proxy') {
2834
+ console.error('Usage: pluxx mcp proxy --from-mcp <source> [--record <tape.json>]')
2835
+ console.error(' pluxx mcp proxy --replay <tape.json>')
2836
+ process.exit(1)
2837
+ }
2838
+
2839
+ try {
2840
+ await runMcpProxy(args.slice(2))
2841
+ } catch (error) {
2842
+ if (error instanceof Error && error.message === 'Invalid MCP proxy arguments.') {
2843
+ process.exit(1)
2844
+ }
2845
+ throw error
2846
+ }
2847
+ }
2848
+
2341
2849
  function printHelp() {
2342
2850
  console.log(`
2343
2851
  pluxx — Cross-platform AI agent plugin SDK
2344
2852
 
2345
2853
  Usage:
2346
- pluxx build [--target <platforms...>] Generate platform-specific plugin files
2854
+ pluxx build [--target <platforms...>] [--install] Generate platform-specific plugin files
2347
2855
  pluxx dev [--target <platforms...>] Watch for changes and auto-rebuild
2348
2856
  pluxx validate Validate your config
2349
2857
  pluxx lint Lint skills and cross-platform metadata
2350
- pluxx doctor Check runtime, config, paths, MCP, and trust advisories
2858
+ pluxx doctor [path] [--consumer] Check source-project or installed-bundle health
2351
2859
  pluxx agent prepare Generate agent context + boundary files for host agents
2352
2860
  pluxx agent prompt <kind> Generate a prompt pack (taxonomy, instructions, review)
2353
2861
  pluxx agent run <kind> --runner <id> Execute a prompt pack via Claude, Cursor, Codex, or OpenCode headlessly
2862
+ pluxx mcp proxy ... Run a local MCP proxy with optional record/replay tapes
2354
2863
  pluxx autopilot --from-mcp ... Run import + agent refinement + verification in one command
2355
2864
  pluxx init [name] [--from-mcp <source>] Create a new pluxx.config.ts
2356
2865
  pluxx sync [--from-mcp <source>] Refresh MCP-derived scaffold files
2357
2866
  pluxx migrate <path> Import an existing plugin into pluxx
2358
- pluxx test [--target <platforms...>] Run config, lint, build, and smoke checks
2359
- pluxx install [--target <platforms>] [--trust] Symlink built plugins for local testing
2867
+ pluxx test [--target <platforms...>] [--install] Run config, lint, eval, build, and smoke checks
2868
+ pluxx eval Evaluate scaffold and prompt-pack quality
2869
+ pluxx install [--target <platforms>] [--trust] Install built plugins for local testing
2360
2870
  pluxx publish [--npm] [--github-release] [--dry-run] [--json] [--tag latest] [--version x.y.z]
2361
2871
  pluxx uninstall [--target <platforms>] Remove symlinked plugins
2362
2872
  pluxx help Show this help
@@ -2374,12 +2884,15 @@ Targets:
2374
2884
 
2375
2885
  Examples:
2376
2886
  pluxx build Build for all configured targets
2887
+ pluxx build --install Build and install all configured targets locally
2377
2888
  pluxx build --target claude-code cursor Build for specific platforms
2378
2889
  pluxx init my-plugin Scaffold a new plugin config
2379
2890
  pluxx init --from-mcp https://example.com/mcp Scaffold from a remote MCP server
2380
2891
  pluxx init --from-mcp "npx -y @acme/mcp" Scaffold from a local MCP command
2381
2892
  pluxx init --from-mcp https://example.com/mcp --yes --name acme --display-name "Acme" --author "Acme" --targets claude-code,codex --grouping workflow --hooks safe --json
2382
2893
  pluxx init --from-mcp https://example.com/mcp --yes --auth-env API_KEY --auth-type header --auth-header X-API-Key --auth-template "\${value}"
2894
+ pluxx init --from-mcp https://example.com/mcp --yes --auth-type platform --runtime-auth platform
2895
+ pluxx init --from-mcp https://mcp.linear.app/mcp --yes --oauth-wrapper --runtime-auth platform
2383
2896
  pluxx init --from-mcp https://example.com/sse --transport sse Scaffold from an SSE-transport MCP server
2384
2897
  pluxx init --from-mcp https://example.com/mcp --yes --dry-run Preview scaffold files without writing
2385
2898
  pluxx sync Refresh a scaffold using .pluxx/mcp.json metadata
@@ -2392,13 +2905,19 @@ Examples:
2392
2905
  pluxx agent run taxonomy --runner codex
2393
2906
  pluxx agent run taxonomy --runner codex --verbose-runner
2394
2907
  pluxx agent run review --runner opencode --attach http://localhost:4096 --no-verify
2908
+ pluxx mcp proxy --from-mcp "bun ./server.js" --record .pluxx/tapes/dev.json
2909
+ pluxx mcp proxy --replay .pluxx/tapes/dev.json
2395
2910
  --attach is only supported for the opencode runner
2396
2911
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode quick --yes
2397
2912
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --yes --name acme --display-name "Acme"
2398
2913
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode thorough --yes --verbose-runner
2914
+ pluxx autopilot --from-mcp https://mcp.linear.app/mcp --runner codex --yes --oauth-wrapper
2399
2915
  pluxx autopilot --from-mcp "npx -y @acme/mcp" --runner claude --targets claude-code,codex --website https://example.com --docs https://docs.example.com
2400
- pluxx doctor --json Inspect project health as JSON
2916
+ pluxx doctor --json Inspect source-project health as JSON
2917
+ pluxx doctor --consumer ./dist/cursor Inspect a built or installed platform bundle
2918
+ pluxx eval --json Inspect scaffold/prompt-pack quality as JSON
2401
2919
  pluxx test --target claude-code codex Verify selected target outputs
2920
+ pluxx test --install Verify and install all configured targets locally
2402
2921
  pluxx install Install to all configured targets
2403
2922
  pluxx install --target claude-code Install to Claude Code only
2404
2923
  pluxx install --dry-run Preview local install paths and trust implications