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.
@@ -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
- // Validate project path exists
139
- if (!existsSync(projectPath)) {
140
- return reply.code(400).send({
141
- error: 'Project path does not exist',
142
- path: projectPath
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
- const result = await addInstance(name, projectPath, {
147
- displayName,
148
- tags,
149
- configPath
150
- });
150
+ if (!result.success) {
151
+ return reply.code(400).send({
152
+ error: result.error,
153
+ validation: result.validation
154
+ });
155
+ }
151
156
 
152
- if (!result.success) {
153
- return reply.code(400).send({
154
- error: result.error,
155
- validation: result.validation
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
- reply.code(201).send(result.instance);
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
  }