55 ComposioSessionRepository ,
66 getSessionInfo ,
77 getSessionInfoByUserApiKey ,
8+ listOrganizations ,
89 type SessionInfoResponse ,
910} from 'src/services/composio-clients' ;
1011import { ComposioUserContext } from 'src/services/user-context' ;
@@ -32,6 +33,16 @@ const keyOpt = Options.text('key').pipe(
3233 Options . optional
3334) ;
3435
36+ const userApiKeyOpt = Options . text ( 'user-api-key' ) . pipe (
37+ Options . withDescription ( 'Log in directly with a Composio user API key' ) ,
38+ Options . optional
39+ ) ;
40+
41+ const orgOpt = Options . text ( 'org' ) . pipe (
42+ Options . withDescription ( 'Default organization ID or name to store for CLI commands' ) ,
43+ Options . optional
44+ ) ;
45+
3546const yesOpt = Options . boolean ( 'yes' ) . pipe (
3647 Options . withAlias ( 'y' ) ,
3748 Options . withDefault ( false ) ,
@@ -57,10 +68,107 @@ const formatLoginSuccessMessage = (params: { email?: string; orgName?: string })
5768 return 'Logged in successfully' ;
5869} ;
5970
71+ const emitLoginComplete = ( params : {
72+ email ?: string ;
73+ orgId : string ;
74+ orgName ?: string ;
75+ skipHints ?: boolean ;
76+ } ) =>
77+ Effect . gen ( function * ( ) {
78+ const ui = yield * TerminalUI ;
79+ const { email, orgId, orgName, skipHints = false } = params ;
80+
81+ yield * ui . log . success ( formatLoginSuccessMessage ( { email, orgName } ) ) ;
82+ if ( ! skipHints ) {
83+ yield * ui . log . info ( commandHintStep ( 'Execute a tool directly' , 'root.execute' ) ) ;
84+ yield * ui . log . info ( commandHintStep ( 'Switch your default org' , 'root.orgs.switch' ) ) ;
85+ }
86+
87+ yield * ui . output (
88+ JSON . stringify ( {
89+ email,
90+ org_id : orgId ,
91+ org_name : orgName ?? '' ,
92+ } )
93+ ) ;
94+
95+ if ( ! skipHints ) {
96+ yield * ui . outro ( "You're all set!" ) ;
97+ }
98+ } ) ;
99+
100+ const resolveDirectLoginOrganization = ( params : {
101+ apiKey : string ;
102+ baseURL : string ;
103+ requestedOrg ?: string ;
104+ fallbackOrgId : string ;
105+ fallbackOrgName ?: string ;
106+ } ) =>
107+ Effect . gen ( function * ( ) {
108+ const ui = yield * TerminalUI ;
109+ const { apiKey, baseURL, requestedOrg, fallbackOrgId, fallbackOrgName } = params ;
110+
111+ if ( ! requestedOrg ) {
112+ return {
113+ id : fallbackOrgId ,
114+ name : fallbackOrgName ?? fallbackOrgId ,
115+ } ;
116+ }
117+
118+ const organizations = yield * listOrganizations ( {
119+ baseURL,
120+ apiKey,
121+ } ) ;
122+ const match = organizations . data . find (
123+ org => org . id === requestedOrg || org . name === requestedOrg
124+ ) ;
125+
126+ if ( ! match ) {
127+ yield * ui . log . error ( `Organization "${ requestedOrg } " was not found for this API key.` ) ;
128+ return yield * Effect . fail (
129+ new Error ( 'Invalid organization. Run `composio orgs list` to inspect available orgs.' )
130+ ) ;
131+ }
132+
133+ return match ;
134+ } ) ;
135+
136+ const directLogin = ( params : { userApiKey : string ; org ?: string } ) =>
137+ Effect . gen ( function * ( ) {
138+ const ctx = yield * ComposioUserContext ;
139+ const sessionInfo = yield * getSessionInfoByUserApiKey ( {
140+ baseURL : ctx . data . baseURL ,
141+ userApiKey : params . userApiKey ,
142+ } ) ;
143+
144+ const selectedOrg = yield * resolveDirectLoginOrganization ( {
145+ apiKey : params . userApiKey ,
146+ baseURL : ctx . data . baseURL ,
147+ requestedOrg : params . org ,
148+ fallbackOrgId : sessionInfo . project . org . id ,
149+ fallbackOrgName : sessionInfo . project . org . name ,
150+ } ) ;
151+
152+ const sessionUserId = sessionInfo . org_member . user_id ?? sessionInfo . org_member . id ;
153+ const testUserId = sessionUserId
154+ ? `pg-test-${ sessionUserId } `
155+ : Option . getOrUndefined ( ctx . data . testUserId ) ;
156+
157+ yield * ctx . login ( params . userApiKey , selectedOrg . id , testUserId ) ;
158+ yield * primeConsumerConnectedToolkitsCacheInBackground ( {
159+ orgId : selectedOrg . id ,
160+ } ) ;
161+ yield * emitLoginComplete ( {
162+ email : sessionInfo . org_member . email || undefined ,
163+ orgId : selectedOrg . id ,
164+ orgName : selectedOrg . name ,
165+ } ) ;
166+ } ) ;
167+
60168/**
61169 * Verifies credentials via session/info and stores them.
62170 *
63- * Resolves TerminalUI and ComposioUserContext from the Effect context rather
171+ * Resolves ComposioUserContext from the Effect context rather
64172 * than accepting them as parameters -- this keeps the signature focused on
65173 * data and avoids hand-rolled structural types.
66174 */
@@ -76,7 +184,6 @@ const storeCredentials = (params: {
76184 skipOutput ?: boolean ;
77185} ) =>
78186 Effect . gen ( function * ( ) {
79- const ui = yield * TerminalUI ;
80187 const ctx = yield * ComposioUserContext ;
81188
82189 const {
@@ -131,27 +238,13 @@ const storeCredentials = (params: {
131238 orgId,
132239 } ) ;
133240
134- const email = sessionInfo ?. org_member . email || fallbackEmail || undefined ;
135- const orgName = sessionInfo ?. project . org . name || undefined ;
136- yield * ui . log . success ( formatLoginSuccessMessage ( { email, orgName } ) ) ;
137- if ( ! skipHints ) {
138- yield * ui . log . info ( commandHintStep ( 'Execute a tool directly' , 'root.execute' ) ) ;
139- yield * ui . log . info ( commandHintStep ( 'Switch your default org' , 'root.orgs.switch' ) ) ;
140- }
141-
142- // Emit structured JSON for piped/scripted consumption (agent-native)
143241 if ( ! skipOutput ) {
144- yield * ui . output (
145- JSON . stringify ( {
146- email,
147- org_id : orgId ,
148- org_name : sessionInfo ?. project . org . name ?? '' ,
149- } )
150- ) ;
151- }
152-
153- if ( ! skipHints ) {
154- yield * ui . outro ( "You're all set!" ) ;
242+ yield * emitLoginComplete ( {
243+ email : sessionInfo ?. org_member . email || fallbackEmail || undefined ,
244+ orgId,
245+ orgName : sessionInfo ?. project . org . name || undefined ,
246+ skipHints,
247+ } ) ;
155248 }
156249 } ) ;
157250
@@ -256,25 +349,14 @@ const loginWithKey = (params: { key: string; noWait: boolean; skipOrgProjectPick
256349 yield * primeConsumerConnectedToolkitsCacheInBackground ( {
257350 orgId : result . id ,
258351 } ) ;
259- yield * ui . log . success (
260- formatLoginSuccessMessage ( {
261- email : uakSessionInfo . org_member . email || linkedSession . account . email || undefined ,
262- orgName : result . name ,
263- } )
264- ) ;
265352 }
266353 const finalOrgId = result ?. id ?? xOrgId ;
267354 const finalOrgName = result ?. name ?? uakSessionInfo . project . org . name ?? '' ;
268- yield * ui . output (
269- JSON . stringify ( {
270- email : linkedSession . account . email ?? undefined ,
271- org_id : finalOrgId ,
272- org_name : finalOrgName ,
273- } )
274- ) ;
275- yield * ui . log . info ( commandHintStep ( 'Execute a tool directly' , 'root.execute' ) ) ;
276- yield * ui . log . info ( commandHintStep ( 'Switch your default org' , 'root.orgs.switch' ) ) ;
277- yield * ui . outro ( "You're all set!" ) ;
355+ yield * emitLoginComplete ( {
356+ email : linkedSession . account . email ?? undefined ,
357+ orgId : finalOrgId ,
358+ orgName : finalOrgName ,
359+ } ) ;
278360 }
279361 } ) ;
280362
@@ -421,25 +503,14 @@ export const browserLogin = (params: {
421503 yield * primeConsumerConnectedToolkitsCacheInBackground ( {
422504 orgId : result . id ,
423505 } ) ;
424- yield * ui . log . success (
425- formatLoginSuccessMessage ( {
426- email : uakSessionInfo . org_member . email || linkedSession . account . email || undefined ,
427- orgName : result . name ,
428- } )
429- ) ;
430506 }
431507 const finalOrgId = result ?. id ?? xOrgId ;
432508 const finalOrgName = result ?. name ?? uakSessionInfo . project . org . name ?? '' ;
433- yield * ui . output (
434- JSON . stringify ( {
435- email : linkedSession . account . email ?? undefined ,
436- org_id : finalOrgId ,
437- org_name : finalOrgName ,
438- } )
439- ) ;
440- yield * ui . log . info ( commandHintStep ( 'Execute a tool directly' , 'root.execute' ) ) ;
441- yield * ui . log . info ( commandHintStep ( 'Switch your default org' , 'root.orgs.switch' ) ) ;
442- yield * ui . outro ( "You're all set!" ) ;
509+ yield * emitLoginComplete ( {
510+ email : linkedSession . account . email ?? undefined ,
511+ orgId : finalOrgId ,
512+ orgName : finalOrgName ,
513+ } ) ;
443514 }
444515 } ) ;
445516
@@ -451,6 +522,7 @@ export const browserLogin = (params: {
451522 * Use --no-wait to print login URL and session info (JSON) then exit without opening browser or waiting.
452523 * Use --key to complete login with a session key from --no-wait. Without --no-wait, polls until linked;
453524 * with --no-wait, checks once and fails if not linked.
525+ * Use --user-api-key to log in directly without a browser flow, and --org to override the default org.
454526 * Use -y to skip org picker and use session default org.
455527 *
456528 * @example
@@ -460,19 +532,45 @@ export const browserLogin = (params: {
460532 * composio login --no-wait
461533 * composio login --key <key>
462534 * composio login --key <key> --no-wait
535+ * composio login --user-api-key <uak>
536+ * composio login --user-api-key <uak> --org <org>
463537 * composio login -y
464538 * ```
465539 */
466540export const loginCmd = Command . make (
467541 'login' ,
468- { noBrowser, noWait, key : keyOpt , yes : yesOpt , noSkillInstall } ,
469- ( { noBrowser, noWait, key, yes, noSkillInstall } ) =>
542+ {
543+ noBrowser,
544+ noWait,
545+ key : keyOpt ,
546+ userApiKey : userApiKeyOpt ,
547+ org : orgOpt ,
548+ yes : yesOpt ,
549+ noSkillInstall,
550+ } ,
551+ ( { noBrowser, noWait, key, userApiKey, org, yes, noSkillInstall } ) =>
470552 Effect . gen ( function * ( ) {
471553 const ui = yield * TerminalUI ;
472554 const ctx = yield * ComposioUserContext ;
473555
474556 yield * ui . intro ( 'composio login' ) ;
475557
558+ if ( Option . isSome ( key ) && Option . isSome ( userApiKey ) ) {
559+ return yield * Effect . fail ( new Error ( 'Use either `--key` or `--user-api-key`, not both.' ) ) ;
560+ }
561+
562+ if ( Option . isSome ( org ) && Option . isNone ( userApiKey ) ) {
563+ return yield * Effect . fail ( new Error ( '`--org` requires `--user-api-key`.' ) ) ;
564+ }
565+
566+ if ( Option . isSome ( userApiKey ) && ( noBrowser || noWait || Option . isSome ( key ) ) ) {
567+ return yield * Effect . fail (
568+ new Error (
569+ '`--user-api-key` is a direct login path and cannot be combined with browser or session flags.'
570+ )
571+ ) ;
572+ }
573+
476574 if ( Option . isSome ( key ) ) {
477575 yield * loginWithKey ( {
478576 key : key . value ,
@@ -485,6 +583,17 @@ export const loginCmd = Command.make(
485583 return ;
486584 }
487585
586+ if ( Option . isSome ( userApiKey ) ) {
587+ yield * directLogin ( {
588+ userApiKey : userApiKey . value ,
589+ org : Option . getOrUndefined ( org ) ,
590+ } ) ;
591+ if ( ! noSkillInstall ) {
592+ yield * installSkillSafe ( { channel : inferSkillReleaseChannel ( APP_VERSION ) } ) ;
593+ }
594+ return ;
595+ }
596+
488597 if ( ctx . isLoggedIn ( ) ) {
489598 if ( Option . isSome ( ctx . data . orgId ) ) {
490599 yield * ui . log . warn ( `You're already logged in!` ) ;
0 commit comments