@vsuryav/agent-sim 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsuryav/agent-sim",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local TUI and sync client for Agent Sim",
5
5
  "type": "module",
6
6
  "files": [
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
  import {
6
- authLoginStartResponseSchema,
6
+ authSessionCompleteResponseSchema,
7
7
  authSessionResponseSchema,
8
8
  pullResponseSchema,
9
9
  type CollectedAgent,
@@ -21,6 +21,24 @@ interface Config {
21
21
  token?: string;
22
22
  }
23
23
 
24
+ type DeviceLoginStartResponse = {
25
+ mode: 'device';
26
+ client_id: string;
27
+ device_code: string;
28
+ user_code: string;
29
+ verification_uri: string;
30
+ verification_uri_complete?: string;
31
+ poll_interval_ms: number;
32
+ expires_in_ms: number;
33
+ };
34
+
35
+ type WebLoginStartResponse = {
36
+ mode: 'web';
37
+ url: string;
38
+ session_id: string;
39
+ poll_interval_ms: number;
40
+ };
41
+
24
42
  export function loadConfig(): Config {
25
43
  try {
26
44
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Config;
@@ -88,6 +106,81 @@ async function pollForLoginSession(
88
106
  throw new Error(`Login timed out. Re-run the ${APP_NAME} login command and finish GitHub auth within 5 minutes.`);
89
107
  }
90
108
 
109
+ async function pollForGitHubDeviceAccessToken(
110
+ loginStart: DeviceLoginStartResponse,
111
+ ): Promise<string> {
112
+ const deadline = Date.now() + loginStart.expires_in_ms;
113
+ let intervalMs = loginStart.poll_interval_ms;
114
+
115
+ while (Date.now() < deadline) {
116
+ const res = await fetch('https://github.com/login/oauth/access_token', {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/x-www-form-urlencoded',
120
+ Accept: 'application/json',
121
+ },
122
+ body: new URLSearchParams({
123
+ client_id: loginStart.client_id,
124
+ device_code: loginStart.device_code,
125
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
126
+ }),
127
+ });
128
+
129
+ const body = await res.json() as {
130
+ access_token?: string;
131
+ interval?: number;
132
+ error?: string;
133
+ };
134
+
135
+ if (body.access_token) {
136
+ return body.access_token;
137
+ }
138
+
139
+ if (body.error === 'authorization_pending') {
140
+ await sleep(intervalMs);
141
+ continue;
142
+ }
143
+
144
+ if (body.error === 'slow_down') {
145
+ intervalMs = body.interval ? body.interval * 1000 : intervalMs + 5000;
146
+ await sleep(intervalMs);
147
+ continue;
148
+ }
149
+
150
+ if (body.error === 'access_denied') {
151
+ throw new Error('GitHub login was denied. Re-run the login command and try again.');
152
+ }
153
+
154
+ if (body.error === 'expired_token') {
155
+ throw new Error('GitHub device login expired. Re-run the login command and try again.');
156
+ }
157
+
158
+ throw new Error(`GitHub device login failed: ${body.error ?? res.statusText}`);
159
+ }
160
+
161
+ throw new Error(`Login timed out. Re-run the ${APP_NAME} login command and finish GitHub auth within 5 minutes.`);
162
+ }
163
+
164
+ function isWebLoginStartResponse(value: unknown): value is WebLoginStartResponse {
165
+ if (!value || typeof value !== 'object') return false;
166
+ const record = value as Record<string, unknown>;
167
+ return typeof record.url === 'string'
168
+ && typeof record.session_id === 'string'
169
+ && typeof record.poll_interval_ms === 'number';
170
+ }
171
+
172
+ function isDeviceLoginStartResponse(value: unknown): value is DeviceLoginStartResponse {
173
+ if (!value || typeof value !== 'object') return false;
174
+ const record = value as Record<string, unknown>;
175
+ return record.mode === 'device'
176
+ && typeof record.client_id === 'string'
177
+ && typeof record.device_code === 'string'
178
+ && typeof record.user_code === 'string'
179
+ && typeof record.verification_uri === 'string'
180
+ && typeof record.poll_interval_ms === 'number'
181
+ && typeof record.expires_in_ms === 'number';
182
+ }
183
+
91
184
  export async function login(serverUrl: string): Promise<void> {
92
185
  const config = loadConfig();
93
186
  config.serverUrl = serverUrl;
@@ -99,13 +192,39 @@ export async function login(serverUrl: string): Promise<void> {
99
192
  const error = body as { error?: string };
100
193
  throw new Error(`Login failed: ${error.error ?? res.statusText}`);
101
194
  }
102
- const { url, session_id, poll_interval_ms } = authLoginStartResponseSchema.parse(body);
103
195
 
104
- console.log(`\n Open this URL to login with GitHub:\n`);
105
- console.log(` ${url}\n`);
106
- console.log(' Waiting for GitHub authorization to complete...\n');
196
+ let session;
197
+ if (isDeviceLoginStartResponse(body)) {
198
+ console.log('\n Open GitHub device login:\n');
199
+ console.log(` ${body.verification_uri_complete ?? body.verification_uri}\n`);
200
+ if (!body.verification_uri_complete) {
201
+ console.log(` Enter this code: ${body.user_code}\n`);
202
+ }
203
+ console.log(' Waiting for GitHub authorization to complete...\n');
204
+
205
+ const githubAccessToken = await pollForGitHubDeviceAccessToken(body);
206
+ const exchangeRes = await fetch(`${serverUrl}/auth/exchange/github-token`, {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ },
211
+ body: JSON.stringify({ access_token: githubAccessToken }),
212
+ });
213
+ const exchangeBody = await exchangeRes.json() as unknown;
214
+ if (!exchangeRes.ok) {
215
+ const error = exchangeBody as { error?: string };
216
+ throw new Error(`Login failed: ${error.error ?? exchangeRes.statusText}`);
217
+ }
218
+ session = authSessionCompleteResponseSchema.parse(exchangeBody);
219
+ } else if (isWebLoginStartResponse(body)) {
220
+ console.log(`\n Open this URL to login with GitHub:\n`);
221
+ console.log(` ${body.url}\n`);
222
+ console.log(' Waiting for GitHub authorization to complete...\n');
223
+ session = await pollForLoginSession(serverUrl, body.session_id, body.poll_interval_ms);
224
+ } else {
225
+ throw new Error('Login failed: server returned an unrecognized auth response');
226
+ }
107
227
 
108
- const session = await pollForLoginSession(serverUrl, session_id, poll_interval_ms);
109
228
  config.token = session.token;
110
229
  saveConfig(config);
111
230
  console.log(` Logged in as ${session.githubLogin}. Token saved.`);