@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 +1 -1
- package/package.json +4 -4
- package/src/cli.ts +44 -28
- package/src/collector/remote-sync.ts +125 -6
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsuryav/agent-sim",
|
|
3
|
-
"version": "0.1.
|
|
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-
|
|
17
|
+
"url": "git+https://github.com/vsuryav/agent-sim.git"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/vsuryav/agent-
|
|
19
|
+
"homepage": "https://github.com/vsuryav/agent-sim",
|
|
20
20
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/vsuryav/agent-
|
|
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('
|
|
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
|
-
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (command === 'login') {
|
|
78
81
|
const serverUrl = process.argv[3] ?? DEFAULT_SERVER_URL;
|
|
79
82
|
await login(serverUrl);
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.`);
|