44 * agent-analytics CLI
55 *
66 * Usage:
7- * npx @agent-analytics/cli login --token <key> — Save your API key
8- * npx @agent-analytics/cli logout — Clear your saved API key
7+ * npx @agent-analytics/cli login — Start browser-based agent session login
8+ * npx @agent-analytics/cli login --detached — Detached/browser approval flow
9+ * npx @agent-analytics/cli login --token <key> — Advanced fallback: save a raw API key
10+ * npx @agent-analytics/cli logout — Clear saved local auth
911 * npx @agent-analytics/cli create <name> — Create a project and get your snippet
1012 * npx @agent-analytics/cli projects — List your projects
1113 * npx @agent-analytics/cli all-sites — Historical summary across all projects
4244 */
4345
4446import { AgentAnalyticsAPI } from '../lib/api.mjs' ;
47+ import { finishManualExchange , loginDetached , loginInteractive } from '../lib/auth-flow.mjs' ;
4548import {
4649 clearStoredAuth ,
47- getApiKey ,
4850 getBaseUrl ,
49- getConfig ,
5051 getConfigFile ,
51- saveConfig ,
52+ getStoredAuth ,
53+ setAgentSession ,
5254 setApiKey ,
55+ updateStoredAccount ,
5356} from '../lib/config.mjs' ;
5457
5558const BOLD = '\x1b[1m' ;
@@ -68,17 +71,67 @@ function warn(msg) { log(`${YELLOW}⚠${RESET} ${msg}`); }
6871function error ( msg ) { log ( `${ RED } ✗${ RESET } ${ msg } ` ) ; process . exit ( 1 ) ; }
6972function heading ( msg ) { log ( `\n${ BOLD } ${ msg } ${ RESET } ` ) ; }
7073
71- function requireKey ( ) {
72- const key = getApiKey ( ) ;
73- if ( ! key ) {
74+ function startWaitingIndicator ( message , { interruptMessage = 'Stopped waiting for browser approval.' } = { } ) {
75+ if ( ! process . stdout . isTTY ) {
76+ log ( `${ DIM } ${ message } ${ RESET } ` ) ;
77+ return ( ) => { } ;
78+ }
79+
80+ const frames = [ '-' , '\\' , '|' , '/' ] ;
81+ let index = 0 ;
82+ let stopped = false ;
83+ const HIDE_CURSOR = '\x1b[?25l' ;
84+ const SHOW_CURSOR = '\x1b[?25h' ;
85+ const clearLine = ( ) => process . stdout . write ( `\r\x1b[2K${ SHOW_CURSOR } ` ) ;
86+ const render = ( ) => {
87+ if ( stopped ) return ;
88+ process . stdout . write ( `\r${ HIDE_CURSOR } ${ DIM } ${ frames [ index ] } ${ message } ${ RESET } ` ) ;
89+ } ;
90+ const stop = ( finalMessage = '' ) => {
91+ if ( stopped ) return ;
92+ stopped = true ;
93+ clearInterval ( timer ) ;
94+ process . removeListener ( 'SIGINT' , handleSigint ) ;
95+ clearLine ( ) ;
96+ if ( finalMessage ) {
97+ log ( finalMessage ) ;
98+ }
99+ } ;
100+ const handleSigint = ( ) => {
101+ stop ( `${ DIM } ${ interruptMessage } ${ RESET } ` ) ;
102+ process . exit ( 130 ) ;
103+ } ;
104+
105+ render ( ) ;
106+ const timer = setInterval ( ( ) => {
107+ if ( stopped ) return ;
108+ index = ( index + 1 ) % frames . length ;
109+ render ( ) ;
110+ } , 120 ) ;
111+ process . once ( 'SIGINT' , handleSigint ) ;
112+
113+ return stop ;
114+ }
115+
116+ function createApiClient ( auth = getStoredAuth ( ) ) {
117+ return new AgentAnalyticsAPI ( auth , getBaseUrl ( ) , {
118+ onAuthUpdate ( nextAuth ) {
119+ setAgentSession ( nextAuth ) ;
120+ } ,
121+ } ) ;
122+ }
123+
124+ function requireClient ( ) {
125+ const auth = getStoredAuth ( ) ;
126+ if ( ! auth ) {
74127 error ( 'Not logged in. Run: npx @agent-analytics/cli login' ) ;
75128 }
76- return new AgentAnalyticsAPI ( key , getBaseUrl ( ) ) ;
129+ return createApiClient ( auth ) ;
77130}
78131
79132function withApi ( fn ) {
80133 return async ( ...args ) => {
81- const api = requireKey ( ) ;
134+ const api = requireClient ( ) ;
82135 try {
83136 return await fn ( api , ...args ) ;
84137 } catch ( err ) {
@@ -94,30 +147,85 @@ function ifEmpty(arr, label) {
94147
95148// ==================== COMMANDS ====================
96149
97- async function cmdLogin ( token ) {
150+ async function cmdLogin ( { token, detached , exchangeCode , authRequestId } ) {
98151 if ( ! token ) {
152+ const api = createApiClient ( null ) ;
153+
154+ if ( exchangeCode ) {
155+ if ( ! authRequestId ) {
156+ error ( 'Manual exchange requires --auth-request <id> together with --exchange-code <code>' ) ;
157+ }
158+ const result = await finishManualExchange ( api , authRequestId , exchangeCode ) ;
159+ setAgentSession ( result . agent_session ) ;
160+ updateStoredAccount ( result . account ) ;
161+ success ( `Connected as ${ BOLD } ${ result . account . github_login || result . account . google_name || result . account . email } ${ RESET } (${ result . account . tier } )` ) ;
162+ log ( `${ DIM } Agent session saved to ${ getConfigFile ( ) } ${ RESET } ` ) ;
163+ return ;
164+ }
165+
166+ if ( detached ) {
167+ heading ( 'Agent Analytics — Detached Login' ) ;
168+ let stopWaiting = ( ) => { } ;
169+ try {
170+ const { started, exchanged } = await loginDetached ( api , {
171+ onPending ( started ) {
172+ log ( `Approval URL: ${ CYAN } ${ started . authorize_url } ${ RESET } ` ) ;
173+ log ( `Approval code: ${ YELLOW } ${ started . approval_code } ${ RESET } ` ) ;
174+ log ( `${ DIM } Approve in the browser. If polling is blocked, finish manually with:${ RESET } ` ) ;
175+ log ( ` ${ CYAN } npx @agent-analytics/cli login --auth-request ${ started . auth_request_id } --exchange-code <code>${ RESET } ` ) ;
176+ log ( '' ) ;
177+ stopWaiting = startWaitingIndicator ( 'Waiting for browser approval...' ) ;
178+ } ,
179+ } ) ;
180+ stopWaiting ( `${ DIM } Browser approval received.${ RESET } ` ) ;
181+ setAgentSession ( exchanged . agent_session ) ;
182+ updateStoredAccount ( exchanged . account ) ;
183+ success ( `Connected as ${ BOLD } ${ exchanged . account . github_login || exchanged . account . google_name || exchanged . account . email } ${ RESET } (${ exchanged . account . tier } )` ) ;
184+ log ( `${ DIM } Detached request ${ started . auth_request_id } approved and saved to ${ getConfigFile ( ) } ${ RESET } ` ) ;
185+ return ;
186+ } catch ( err ) {
187+ stopWaiting ( `${ DIM } Stopped waiting for browser approval.${ RESET } ` ) ;
188+ throw err ;
189+ }
190+ }
191+
99192 heading ( 'Agent Analytics — Login' ) ;
100193 log ( '' ) ;
101- log ( 'Pass your API key from the dashboard:' ) ;
102- log ( ` ${ CYAN } npx @agent-analytics/cli login --token aak_your_key_here${ RESET } ` ) ;
103- log ( '' ) ;
104- log ( 'Or set it as an environment variable:' ) ;
105- log ( ` ${ CYAN } export AGENT_ANALYTICS_API_KEY=aak_your_key_here${ RESET } ` ) ;
106- log ( '' ) ;
107- log ( `Get your API key at: ${ CYAN } https://app.agentanalytics.sh${ RESET } ` ) ;
108- log ( `${ DIM } Sign in with GitHub or Google — your API key is on the settings page.${ RESET } ` ) ;
109- return ;
194+ let stopWaiting = ( ) => { } ;
195+ try {
196+ const result = await loginInteractive ( api , {
197+ onPending ( started ) {
198+ log ( `Approval URL: ${ CYAN } ${ started . authorize_url } ${ RESET } ` ) ;
199+ log ( `Approval code: ${ YELLOW } ${ started . approval_code } ${ RESET } ` ) ;
200+ log ( `${ DIM } The browser should open automatically. If it does not, open the URL above.${ RESET } ` ) ;
201+ log ( '' ) ;
202+ stopWaiting = startWaitingIndicator ( 'Waiting for browser approval...' ) ;
203+ } ,
204+ } ) ;
205+ stopWaiting ( `${ DIM } Browser approval received.${ RESET } ` ) ;
206+ setAgentSession ( result . agent_session ) ;
207+ updateStoredAccount ( result . account ) ;
208+ success ( `Connected as ${ BOLD } ${ result . account . github_login || result . account . google_name || result . account . email } ${ RESET } (${ result . account . tier } )` ) ;
209+ log ( `${ DIM } Agent session saved to ${ getConfigFile ( ) } ${ RESET } ` ) ;
210+ log ( '' ) ;
211+ log ( `Fallbacks:` ) ;
212+ log ( ` ${ CYAN } npx @agent-analytics/cli login --detached${ RESET } ` ) ;
213+ log ( ` ${ CYAN } npx @agent-analytics/cli login --token aak_your_key_here${ RESET } ${ DIM } advanced/manual fallback${ RESET } ` ) ;
214+ return ;
215+ } catch ( err ) {
216+ stopWaiting ( `${ DIM } Stopped waiting for browser approval.${ RESET } ` ) ;
217+ warn ( `Interactive login failed: ${ err . message } ` ) ;
218+ log ( `Retry with detached approval: ${ CYAN } npx @agent-analytics/cli login --detached${ RESET } ` ) ;
219+ return ;
220+ }
110221 }
111222
112- // Validate the token works
113- const api = new AgentAnalyticsAPI ( token , getBaseUrl ( ) ) ;
223+ // Advanced/manual fallback: API key login
224+ const api = createApiClient ( { api_key : token } ) ;
114225 try {
115226 const account = await api . getAccount ( ) ;
116227 setApiKey ( token ) ;
117- const config = getConfig ( ) ;
118- config . email = account . email ;
119- config . github_login = account . github_login ;
120- saveConfig ( config ) ;
228+ updateStoredAccount ( account ) ;
121229
122230 success ( `Logged in as ${ BOLD } ${ account . github_login || account . email } ${ RESET } (${ account . tier } )` ) ;
123231 log ( `${ DIM } API key saved to ${ getConfigFile ( ) } ${ RESET } ` ) ;
@@ -1065,8 +1173,10 @@ ${BOLD}USAGE${RESET}
10651173 npx @agent-analytics/cli <command> [options]
10661174
10671175${ BOLD } SETUP${ RESET }
1068- ${ CYAN } login${ RESET } --token <key> Save your API key
1069- ${ CYAN } logout${ RESET } Clear your saved API key
1176+ ${ CYAN } login${ RESET } Browser-based agent session login
1177+ ${ CYAN } login${ RESET } --detached Detached approval flow for remote/headless runtimes
1178+ ${ CYAN } login${ RESET } --token <key> Advanced/manual API key fallback
1179+ ${ CYAN } logout${ RESET } Clear saved local auth
10701180 ${ CYAN } create${ RESET } <name> Create a project and get your tracking snippet
10711181 ${ CYAN } projects${ RESET } List all your projects
10721182
@@ -1117,8 +1227,8 @@ ${BOLD}KEY OPTIONS${RESET}
11171227 --window <N> Live view time window in seconds (default: 60)
11181228
11191229${ BOLD } QUICK START${ RESET }
1120- ${ DIM } # 1. Save your API key ${ RESET }
1121- npx @agent-analytics/cli login --token aak_your_key
1230+ ${ DIM } # 1. Start agent login ${ RESET }
1231+ npx @agent-analytics/cli login
11221232
11231233 ${ DIM } # 2. Create a project${ RESET }
11241234 npx @agent-analytics/cli create my-site --domain https://mysite.com
@@ -1151,7 +1261,12 @@ function getArg(flag) {
11511261try {
11521262 switch ( command ) {
11531263 case 'login' :
1154- await cmdLogin ( getArg ( '--token' ) ) ;
1264+ await cmdLogin ( {
1265+ token : getArg ( '--token' ) ,
1266+ detached : args . includes ( '--detached' ) ,
1267+ exchangeCode : getArg ( '--exchange-code' ) ,
1268+ authRequestId : getArg ( '--auth-request' ) ,
1269+ } ) ;
11551270 break ;
11561271 case 'logout' :
11571272 cmdLogout ( ) ;
0 commit comments