agent-window 1.2.0 → 1.2.3
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/bin/cli.js +45 -45
- package/config/config.json.backup +12 -0
- package/docker-compose.yml +5 -5
- package/ecosystem.config.cjs +4 -4
- package/package.json +1 -1
- package/scripts/final-test.sh +136 -0
- package/scripts/fix-agentbridge-instance.js +70 -212
- package/scripts/import-instances.js +55 -0
- package/scripts/test-api-endpoints.js +39 -0
- package/scripts/test-discovery.js +95 -0
- package/scripts/test-instance-type.js +85 -0
- package/scripts/test-pm2-data.js +54 -0
- package/src/api/routes/instances.js +123 -49
- package/src/api/routes/operations.js +221 -1
- package/src/bot.js +200 -2
- package/src/core/instance/manager.js +117 -16
- package/src/core/instance/pm2-bridge.js +56 -6
- package/src/core/instance/validator.js +7 -7
- package/src/core/platform/pm2-bridge.js +15 -0
- package/web/dist/assets/Dashboard-1TTWybTq.js +1 -0
- package/web/dist/assets/Dashboard-DRg2tFpk.css +1 -0
- package/web/dist/assets/{InstanceDetail-E-9-YoyH.css → InstanceDetail-BRMjUfAV.css} +1 -1
- package/web/dist/assets/InstanceDetail-BWV1wz24.js +3 -0
- package/web/dist/assets/{Instances-DFQOoLYE.js → Instances-D4SbRoep.js} +1 -1
- package/web/dist/assets/{Settings-DUvrHXXe.js → Settings-CdoSWOhM.js} +1 -1
- package/web/dist/assets/{main-CICHNbG_.js → main-BKf0mqau.js} +4 -4
- package/web/dist/index.html +1 -1
- package/web/dist/assets/Dashboard-CJDMEpOk.css +0 -1
- package/web/dist/assets/Dashboard-CK9p-dpz.js +0 -1
- package/web/dist/assets/InstanceDetail-txSTEfck.js +0 -3
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../../core/instance/pm2-bridge.js';
|
|
25
25
|
import { readConfig, writeConfig } from '../../core/instance/config-reader.js';
|
|
26
26
|
import { existsSync } from 'fs';
|
|
27
|
+
import path from 'path';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Register instance routes
|
|
@@ -37,24 +38,8 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
37
38
|
fastify.get('/api/instances', {
|
|
38
39
|
schema: {
|
|
39
40
|
description: 'List all instances',
|
|
40
|
-
tags: ['instances']
|
|
41
|
-
response
|
|
42
|
-
200: {
|
|
43
|
-
type: 'array',
|
|
44
|
-
items: {
|
|
45
|
-
type: 'object',
|
|
46
|
-
properties: {
|
|
47
|
-
name: { type: 'string' },
|
|
48
|
-
displayName: { type: 'string' },
|
|
49
|
-
projectPath: { type: 'string' },
|
|
50
|
-
pluginPath: { type: 'string' },
|
|
51
|
-
enabled: { type: 'boolean' },
|
|
52
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
53
|
-
addedAt: { type: 'string' }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
41
|
+
tags: ['instances']
|
|
42
|
+
// Remove response schema to avoid field filtering
|
|
58
43
|
}
|
|
59
44
|
}, async (request, reply) => {
|
|
60
45
|
try {
|
|
@@ -77,19 +62,8 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
77
62
|
description: 'Get instance details',
|
|
78
63
|
params: {
|
|
79
64
|
name: { type: 'string' }
|
|
80
|
-
},
|
|
81
|
-
response: {
|
|
82
|
-
200: {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
name: { type: 'string' },
|
|
86
|
-
displayName: { type: 'string' },
|
|
87
|
-
projectPath: { type: 'string' },
|
|
88
|
-
configPath: { type: 'string' },
|
|
89
|
-
enabled: { type: 'boolean' }
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
65
|
}
|
|
66
|
+
// Remove response schema to avoid field filtering
|
|
93
67
|
}
|
|
94
68
|
}, async (request, reply) => {
|
|
95
69
|
try {
|
|
@@ -127,36 +101,84 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
127
101
|
projectPath: { type: 'string' },
|
|
128
102
|
displayName: { type: 'string' },
|
|
129
103
|
tags: { type: 'array', items: { type: 'string' } },
|
|
130
|
-
configPath: { type: 'string' }
|
|
104
|
+
configPath: { type: 'string' },
|
|
105
|
+
config: { type: 'object' },
|
|
106
|
+
createConfig: { type: 'boolean' }
|
|
131
107
|
}
|
|
132
108
|
}
|
|
133
109
|
}
|
|
134
110
|
}, async (request, reply) => {
|
|
135
111
|
try {
|
|
136
|
-
const { name, projectPath, displayName, tags, configPath } = request.body;
|
|
112
|
+
const { name, projectPath, displayName, tags, configPath, config, createConfig } = request.body;
|
|
113
|
+
|
|
114
|
+
// If creating config file, ensure project path exists or create it
|
|
115
|
+
if (createConfig) {
|
|
116
|
+
const fs = await import('fs/promises');
|
|
117
|
+
const { existsSync } = await import('fs');
|
|
118
|
+
|
|
119
|
+
// Create project directory if it doesn't exist
|
|
120
|
+
if (!existsSync(projectPath)) {
|
|
121
|
+
try {
|
|
122
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return reply.code(400).send({
|
|
125
|
+
error: 'Failed to create project directory',
|
|
126
|
+
message: err.message
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
// Write config file
|
|
132
|
+
const configFilePath = configPath || path.join(projectPath, 'config.json');
|
|
133
|
+
try {
|
|
134
|
+
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return reply.code(400).send({
|
|
137
|
+
error: 'Failed to write config file',
|
|
138
|
+
message: err.message
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Use the created config path
|
|
143
|
+
const finalConfigPath = configFilePath;
|
|
144
|
+
const result = await addInstance(name, projectPath, {
|
|
145
|
+
displayName,
|
|
146
|
+
tags,
|
|
147
|
+
configPath: finalConfigPath
|
|
143
148
|
});
|
|
144
|
-
}
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
return reply.code(400).send({
|
|
152
|
+
error: result.error,
|
|
153
|
+
validation: result.validation
|
|
154
|
+
});
|
|
155
|
+
}
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
reply.code(201).send(result.instance);
|
|
158
|
+
} else {
|
|
159
|
+
// Validate project path exists for non-config creation
|
|
160
|
+
if (!existsSync(projectPath)) {
|
|
161
|
+
return reply.code(400).send({
|
|
162
|
+
error: 'Project path does not exist',
|
|
163
|
+
path: projectPath
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await addInstance(name, projectPath, {
|
|
168
|
+
displayName,
|
|
169
|
+
tags,
|
|
170
|
+
configPath
|
|
156
171
|
});
|
|
157
|
-
}
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
if (!result.success) {
|
|
174
|
+
return reply.code(400).send({
|
|
175
|
+
error: result.error,
|
|
176
|
+
validation: result.validation
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
reply.code(201).send(result.instance);
|
|
181
|
+
}
|
|
160
182
|
} catch (error) {
|
|
161
183
|
reply.code(500).send({
|
|
162
184
|
error: 'Failed to add instance',
|
|
@@ -438,5 +460,57 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
438
460
|
}
|
|
439
461
|
});
|
|
440
462
|
|
|
463
|
+
/**
|
|
464
|
+
* GET /api/instances/tokens/claude
|
|
465
|
+
* Get Claude OAuth tokens from existing instances
|
|
466
|
+
*/
|
|
467
|
+
fastify.get('/api/instances/tokens/claude', {
|
|
468
|
+
schema: {
|
|
469
|
+
description: 'Get Claude tokens from existing instances for reuse'
|
|
470
|
+
}
|
|
471
|
+
}, async (request, reply) => {
|
|
472
|
+
try {
|
|
473
|
+
const instances = await listInstances();
|
|
474
|
+
const tokens = [];
|
|
475
|
+
|
|
476
|
+
for (const instance of instances) {
|
|
477
|
+
try {
|
|
478
|
+
const result = await readConfig(instance.configPath);
|
|
479
|
+
if (!result.success) continue;
|
|
480
|
+
|
|
481
|
+
const config = JSON.parse(result.config);
|
|
482
|
+
if (config.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
483
|
+
// Mask the token for security (show first 12 and last 4 characters)
|
|
484
|
+
const token = config.CLAUDE_CODE_OAUTH_TOKEN;
|
|
485
|
+
const masked = token.length > 16
|
|
486
|
+
? `${token.substring(0, 12)}...${token.substring(token.length - 4)}`
|
|
487
|
+
: token;
|
|
488
|
+
|
|
489
|
+
tokens.push({
|
|
490
|
+
instanceName: instance.name,
|
|
491
|
+
displayName: instance.displayName || instance.name,
|
|
492
|
+
token: token, // Full token for reuse
|
|
493
|
+
maskedToken: masked, // Display version
|
|
494
|
+
instanceType: instance.instanceType
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
} catch (err) {
|
|
498
|
+
// Skip instances that can't be read
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
tokens
|
|
506
|
+
};
|
|
507
|
+
} catch (error) {
|
|
508
|
+
reply.code(500).send({
|
|
509
|
+
error: 'Failed to get tokens',
|
|
510
|
+
message: error.message
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
441
515
|
fastify.log.info('Instance routes registered');
|
|
442
516
|
}
|
|
@@ -19,7 +19,7 @@ import { getInstance } from '../../core/instance/manager.js';
|
|
|
19
19
|
|
|
20
20
|
// Get package root directory
|
|
21
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
22
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..', '..');
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Register operation routes
|
|
@@ -183,5 +183,225 @@ export async function registerOperationRoutes(fastify) {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* POST /api/validate-config
|
|
188
|
+
* Validate configuration before creating instance
|
|
189
|
+
*/
|
|
190
|
+
fastify.post('/api/validate-config', {
|
|
191
|
+
schema: {
|
|
192
|
+
description: 'Validate instance configuration',
|
|
193
|
+
body: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
required: ['config'],
|
|
196
|
+
properties: {
|
|
197
|
+
config: { type: 'object' },
|
|
198
|
+
projectPath: { type: 'string' }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, async (request, reply) => {
|
|
203
|
+
try {
|
|
204
|
+
const { config, projectPath } = request.body;
|
|
205
|
+
const { existsSync } = await import('fs');
|
|
206
|
+
const { promisify } = await import('util');
|
|
207
|
+
const { exec } = await import('child_process');
|
|
208
|
+
const execAsync = promisify(exec);
|
|
209
|
+
|
|
210
|
+
const checks = [];
|
|
211
|
+
let allPassed = true;
|
|
212
|
+
|
|
213
|
+
// Check 1: Required fields
|
|
214
|
+
const requiredFields = ['BOT_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'PROJECT_DIR', 'ALLOWED_CHANNELS'];
|
|
215
|
+
const missingFields = requiredFields.filter(field => !config[field]);
|
|
216
|
+
|
|
217
|
+
if (missingFields.length > 0) {
|
|
218
|
+
checks.push({
|
|
219
|
+
name: 'Required Fields',
|
|
220
|
+
status: 'failed',
|
|
221
|
+
message: `Missing fields: ${missingFields.join(', ')}`
|
|
222
|
+
});
|
|
223
|
+
allPassed = false;
|
|
224
|
+
} else {
|
|
225
|
+
checks.push({
|
|
226
|
+
name: 'Required Fields',
|
|
227
|
+
status: 'passed',
|
|
228
|
+
message: 'All required fields present'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check 2: Discord token format
|
|
233
|
+
if (config.BOT_TOKEN) {
|
|
234
|
+
if (config.BOT_TOKEN.startsWith('MT') || config.BOT_TOKEN.length >= 50) {
|
|
235
|
+
checks.push({
|
|
236
|
+
name: 'Discord Token Format',
|
|
237
|
+
status: 'passed',
|
|
238
|
+
message: 'Token format looks valid'
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
checks.push({
|
|
242
|
+
name: 'Discord Token Format',
|
|
243
|
+
status: 'warning',
|
|
244
|
+
message: 'Token format may be invalid (should start with MT and be long)'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check 3: Claude OAuth token format
|
|
250
|
+
if (config.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
251
|
+
if (config.CLAUDE_CODE_OAUTH_TOKEN.startsWith('sk-ant-')) {
|
|
252
|
+
checks.push({
|
|
253
|
+
name: 'Claude OAuth Token Format',
|
|
254
|
+
status: 'passed',
|
|
255
|
+
message: 'Token format valid'
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
checks.push({
|
|
259
|
+
name: 'Claude OAuth Token Format',
|
|
260
|
+
status: 'failed',
|
|
261
|
+
message: 'Token must start with sk-ant-'
|
|
262
|
+
});
|
|
263
|
+
allPassed = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check 4: Project directory exists or can be created
|
|
268
|
+
if (config.PROJECT_DIR) {
|
|
269
|
+
if (existsSync(config.PROJECT_DIR)) {
|
|
270
|
+
checks.push({
|
|
271
|
+
name: 'Project Directory',
|
|
272
|
+
status: 'passed',
|
|
273
|
+
message: `Directory exists: ${config.PROJECT_DIR}`
|
|
274
|
+
});
|
|
275
|
+
} else if (projectPath) {
|
|
276
|
+
// Will be created
|
|
277
|
+
checks.push({
|
|
278
|
+
name: 'Project Directory',
|
|
279
|
+
status: 'warning',
|
|
280
|
+
message: `Will be created: ${config.PROJECT_DIR}`
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
checks.push({
|
|
284
|
+
name: 'Project Directory',
|
|
285
|
+
status: 'warning',
|
|
286
|
+
message: `Directory does not exist: ${config.PROJECT_DIR}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check 5: Allowed channels format
|
|
292
|
+
if (config.ALLOWED_CHANNELS) {
|
|
293
|
+
const channels = config.ALLOWED_CHANNELS.split(',').map(c => c.trim()).filter(Boolean);
|
|
294
|
+
const validChannels = channels.every(c => /^\d+$/.test(c));
|
|
295
|
+
|
|
296
|
+
if (validChannels && channels.length > 0) {
|
|
297
|
+
checks.push({
|
|
298
|
+
name: 'Allowed Channels',
|
|
299
|
+
status: 'passed',
|
|
300
|
+
message: `${channels.length} channel(s) specified`
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
checks.push({
|
|
304
|
+
name: 'Allowed Channels',
|
|
305
|
+
status: 'warning',
|
|
306
|
+
message: 'Channel IDs should be numeric, comma-separated'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check 6: PM2 availability
|
|
312
|
+
try {
|
|
313
|
+
await execAsync('which pm2');
|
|
314
|
+
checks.push({
|
|
315
|
+
name: 'PM2 Installation',
|
|
316
|
+
status: 'passed',
|
|
317
|
+
message: 'PM2 is installed'
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
checks.push({
|
|
321
|
+
name: 'PM2 Installation',
|
|
322
|
+
status: 'failed',
|
|
323
|
+
message: 'PM2 not found. Install: npm install -g pm2'
|
|
324
|
+
});
|
|
325
|
+
allPassed = false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check 7: Docker availability AND functionality
|
|
329
|
+
try {
|
|
330
|
+
await execAsync('which docker');
|
|
331
|
+
|
|
332
|
+
// Actually test if Docker daemon is running
|
|
333
|
+
try {
|
|
334
|
+
const { stdout } = await execAsync('docker info');
|
|
335
|
+
checks.push({
|
|
336
|
+
name: 'Docker Installation',
|
|
337
|
+
status: 'passed',
|
|
338
|
+
message: 'Docker is installed and running'
|
|
339
|
+
});
|
|
340
|
+
} catch (dockerErr) {
|
|
341
|
+
checks.push({
|
|
342
|
+
name: 'Docker Installation',
|
|
343
|
+
status: 'warning',
|
|
344
|
+
message: 'Docker installed but daemon not running. Start Docker Desktop.'
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: 'Docker Installation',
|
|
350
|
+
status: 'warning',
|
|
351
|
+
message: 'Docker not found (required for sandbox feature)'
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check 8: Discord Token validation (basic format check)
|
|
356
|
+
if (config.BOT_TOKEN) {
|
|
357
|
+
// Discord tokens start with specific prefixes and are base64-like
|
|
358
|
+
const tokenValid = config.BOT_TOKEN.length >= 50 &&
|
|
359
|
+
(config.BOT_TOKEN.startsWith('MT') ||
|
|
360
|
+
config.BOT_TOKEN.includes('.') ||
|
|
361
|
+
/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(config.BOT_TOKEN));
|
|
362
|
+
|
|
363
|
+
if (tokenValid) {
|
|
364
|
+
checks.push({
|
|
365
|
+
name: 'Discord Token Validation',
|
|
366
|
+
status: 'passed',
|
|
367
|
+
message: 'Token format appears valid'
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
checks.push({
|
|
371
|
+
name: 'Discord Token Validation',
|
|
372
|
+
status: 'warning',
|
|
373
|
+
message: 'Token format may be invalid (should be 50+ chars with dots)'
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check 9: AgentWindow bot.js exists
|
|
379
|
+
if (existsSync(path.join(PACKAGE_ROOT, 'src', 'bot.js'))) {
|
|
380
|
+
checks.push({
|
|
381
|
+
name: 'AgentWindow Installation',
|
|
382
|
+
status: 'passed',
|
|
383
|
+
message: 'AgentWindow bot.js found'
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
checks.push({
|
|
387
|
+
name: 'AgentWindow Installation',
|
|
388
|
+
status: 'failed',
|
|
389
|
+
message: `AgentWindow bot.js not found at ${path.join(PACKAGE_ROOT, 'src', 'bot.js')}`
|
|
390
|
+
});
|
|
391
|
+
allPassed = false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
success: allPassed,
|
|
396
|
+
checks
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
reply.code(500).send({
|
|
400
|
+
error: 'Validation failed',
|
|
401
|
+
message: error.message
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
186
406
|
fastify.log.info('Operation routes registered');
|
|
187
407
|
}
|