@vsuryav/agent-sim 0.1.0 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # agent sim
2
2
 
3
- a cute pastel idle world that grows from your agentic coding sessions.
3
+ an idle world that grows from your agentic coding sessions.
4
4
 
5
5
  ## install
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsuryav/agent-sim",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local TUI and sync client for Agent Sim",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/vsuryav/agent-wars.git"
17
+ "url": "git+https://github.com/vsuryav/agent-sim.git"
18
18
  },
19
- "homepage": "https://github.com/vsuryav/agent-wars",
19
+ "homepage": "https://github.com/vsuryav/agent-sim",
20
20
  "bugs": {
21
- "url": "https://github.com/vsuryav/agent-wars/issues"
21
+ "url": "https://github.com/vsuryav/agent-sim/issues"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
package/src/cli.ts CHANGED
@@ -53,7 +53,7 @@ function printHelp() {
53
53
 
54
54
  console.log(`\n ${APP_NAME}`);
55
55
  console.log(' ──────────');
56
- console.log(' whimsical local TUI for your agentic coding life\n');
56
+ console.log(' an idle world that grows from your agentic coding sessions\n');
57
57
  console.log(' Commands:');
58
58
  console.log(` ${APP_NAME} Start the game`);
59
59
  console.log(` ${APP_NAME} login [url] Login for cross-machine sync (default: ${DEFAULT_SERVER_URL})`);
@@ -74,53 +74,69 @@ async function main() {
74
74
  try {
75
75
  if (command === 'help' || command === '--help' || command === '-h') {
76
76
  printHelp();
77
- } else if (command === 'login') {
77
+ return;
78
+ }
79
+
80
+ if (command === 'login') {
78
81
  const serverUrl = process.argv[3] ?? DEFAULT_SERVER_URL;
79
82
  await login(serverUrl);
80
- } else if (command === 'set-token') {
83
+ return;
84
+ }
85
+
86
+ if (command === 'set-token') {
81
87
  const token = process.argv[3];
82
88
  if (!token) { console.error(`Usage: ${APP_NAME} set-token <token>`); process.exit(1); }
83
89
  setToken(token);
84
- } else if (command === 'pull') {
90
+ return;
91
+ }
92
+
93
+ if (command === 'pull') {
85
94
  ensureDb();
86
95
  const remote = await pullFromServer();
87
96
  const merge = mergeRemoteState(remote);
88
97
  console.log(` Pulled ${remote.agents.length} agent(s) for ${remote.world.github_login}.`);
89
98
  console.log(` Added ${merge.inserted} new local agent(s), updated ${merge.updated}.`);
90
- } else if (command === 'push') {
99
+ return;
100
+ }
101
+
102
+ if (command === 'push') {
91
103
  ensureDb();
92
104
  const pushResult = await runRemotePush();
93
105
  if (!pushResult) {
94
106
  throw new Error(`Not logged in. Run: ${APP_NAME} login <server-url>`);
95
107
  }
96
108
  console.log(` Pushed ${pushResult.synced} agents to server.`);
97
- } else {
98
- if (command === 'sync') {
99
- ensureDb();
100
- const { runStartupSync } = await import('./app-sync.js');
101
- const result = await runStartupSync();
102
- printStartupSyncSummary(result);
103
- } else if (command === 'status') {
104
- ensureDb();
105
- const result = syncAll();
106
- if (result.newAgents > 0) {
107
- console.log(` Synced ${result.newAgents} new agent(s). Total: ${result.totalAgents}`);
108
- }
109
- }
109
+ return;
110
+ }
110
111
 
111
- if (command === 'status' || command === 'sync') {
112
- printStatus();
113
- }
112
+ if (command === 'sync') {
113
+ ensureDb();
114
+ const { runStartupSync } = await import('./app-sync.js');
115
+ const result = await runStartupSync();
116
+ printStartupSyncSummary(result);
117
+ printStatus();
118
+ return;
119
+ }
114
120
 
115
- if (!command) {
116
- ensureDb();
117
- await runInteractiveSession();
118
- } else if (command) {
119
- console.error(`Unknown command: ${command}`);
120
- printHelp();
121
- process.exitCode = 1;
121
+ if (command === 'status') {
122
+ ensureDb();
123
+ const result = syncAll();
124
+ if (result.newAgents > 0) {
125
+ console.log(` Synced ${result.newAgents} new agent(s). Total: ${result.totalAgents}`);
122
126
  }
127
+ printStatus();
128
+ return;
123
129
  }
130
+
131
+ if (!command) {
132
+ ensureDb();
133
+ await runInteractiveSession();
134
+ return;
135
+ }
136
+
137
+ console.error(`Unknown command: ${command}`);
138
+ printHelp();
139
+ process.exitCode = 1;
124
140
  } finally {
125
141
  closeDb();
126
142
  }
@@ -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.`);