@sschepis/robodev 1.0.0

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.
@@ -0,0 +1,523 @@
1
+ // Project Bootstrapper
2
+ // Discovers design documents in a target directory and pre-populates
3
+ // the SYSTEM_MAP.md manifest with extracted features and invariants.
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { consoleStyler } from '../ui/console-styler.mjs';
8
+
9
+ // Design file names to search for, in priority order
10
+ const DESIGN_FILE_CANDIDATES = ['DESIGN.md', 'ARCHITECTURE.md', 'README.md'];
11
+
12
+ // Keywords that indicate a heading describes a feature/component
13
+ const FEATURE_KEYWORDS = [
14
+ 'feature', 'module', 'component', 'system', 'service', 'api',
15
+ 'engine', 'manager', 'handler', 'layer', 'capability', 'subsystem',
16
+ 'pipeline', 'processor', 'controller', 'provider', 'adapter',
17
+ 'plugin', 'extension', 'tool', 'interface', 'endpoint'
18
+ ];
19
+
20
+ // Section titles that typically list features
21
+ const FEATURE_SECTION_TITLES = [
22
+ 'features', 'components', 'architecture', 'modules', 'capabilities',
23
+ 'services', 'systems', 'subsystems', 'core modules', 'design',
24
+ 'system architecture', 'project structure', 'technical design'
25
+ ];
26
+
27
+ // Keywords that indicate constraints/invariants
28
+ const INVARIANT_KEYWORDS = [
29
+ 'constraint', 'invariant', 'rule', 'requirement', 'limitation',
30
+ 'restriction', 'principle', 'guideline', 'non-functional',
31
+ 'security', 'performance', 'budget'
32
+ ];
33
+
34
+ // Bullet prefixes that indicate constraints
35
+ const CONSTRAINT_PREFIXES = [
36
+ 'must ', 'never ', 'always ', 'no ', 'all ', 'shall ',
37
+ 'cannot ', 'should not ', 'must not ', 'do not '
38
+ ];
39
+
40
+ // Keywords that push phase toward "Interface"
41
+ const INTERFACE_KEYWORDS = [
42
+ 'interface', 'type ', 'schema', 'endpoint', 'signature',
43
+ 'contract', '.d.ts', 'typedef', 'api spec', 'openapi', 'graphql'
44
+ ];
45
+
46
+ // Keywords that push phase toward "Design Review"
47
+ const DESIGN_REVIEW_KEYWORDS = [
48
+ 'edge case', 'complexity', 'o(n', 'o(1', 'o(log', 'trade-off',
49
+ 'tradeoff', 'constraint analysis', 'big-o', 'performance budget',
50
+ 'risk', 'mitigation', 'alternative', 'pros and cons'
51
+ ];
52
+
53
+ export class ProjectBootstrapper {
54
+ constructor(manifestManager) {
55
+ this.manifestManager = manifestManager;
56
+ }
57
+
58
+ /**
59
+ * Main entry point. Bootstraps a project at targetDir.
60
+ * Looks for a design file, parses it, and populates the manifest.
61
+ * @param {string} targetDir - Directory to bootstrap
62
+ * @returns {{ bootstrapped: boolean, message: string }}
63
+ */
64
+ async bootstrap(targetDir) {
65
+ if (!fs.existsSync(targetDir)) {
66
+ return {
67
+ bootstrapped: false,
68
+ message: `Error: Target directory '${targetDir}' does not exist.`
69
+ };
70
+ }
71
+
72
+ // Check if manifest already exists
73
+ const manifestPath = path.join(targetDir, 'SYSTEM_MAP.md');
74
+ if (fs.existsSync(manifestPath)) {
75
+ return {
76
+ bootstrapped: false,
77
+ message: 'Manifest already exists. Skipping bootstrap.'
78
+ };
79
+ }
80
+
81
+ // Discover design file
82
+ const designFilePath = this.discoverDesignFile(targetDir);
83
+ if (!designFilePath) {
84
+ return {
85
+ bootstrapped: false,
86
+ message: 'No design document found. Will use default manifest template.'
87
+ };
88
+ }
89
+
90
+ consoleStyler.log('system', `Found design file: ${path.basename(designFilePath)}`);
91
+
92
+ // Read and parse the design document
93
+ let content;
94
+ try {
95
+ content = await fs.promises.readFile(designFilePath, 'utf8');
96
+ } catch (error) {
97
+ return {
98
+ bootstrapped: false,
99
+ message: `Error reading design file: ${error.message}`
100
+ };
101
+ }
102
+
103
+ if (!content || content.trim().length === 0) {
104
+ return {
105
+ bootstrapped: false,
106
+ message: 'Design document is empty. Will use default manifest template.'
107
+ };
108
+ }
109
+
110
+ const parsed = this.parseDesignDoc(content);
111
+ const features = this.extractFeatures(parsed);
112
+ const invariants = this.extractInvariants(parsed);
113
+
114
+ consoleStyler.log('system', `Extracted ${features.length} feature(s) and ${invariants.length} invariant(s) from design document.`);
115
+
116
+ // Override manifestManager's working dir for the target
117
+ const originalWorkingDir = this.manifestManager.workingDir;
118
+ const originalManifestPath = this.manifestManager.manifestPath;
119
+ const originalSnapshotsDir = this.manifestManager.snapshotsDir;
120
+
121
+ try {
122
+ this.manifestManager.workingDir = targetDir;
123
+ this.manifestManager.manifestPath = manifestPath;
124
+ this.manifestManager.snapshotsDir = path.join(targetDir, '.snapshots');
125
+
126
+ await this.manifestManager.initManifestWithData(features, invariants);
127
+
128
+ const featureNames = features.map(f => `${f.id}: ${f.name}`).join(', ');
129
+ return {
130
+ bootstrapped: true,
131
+ message: `Project bootstrapped from ${path.basename(designFilePath)}.\n` +
132
+ ` Features registered: ${features.length} (${featureNames || 'none'})\n` +
133
+ ` Invariants registered: ${invariants.length}\n` +
134
+ ` Design source: ${designFilePath}`
135
+ };
136
+ } finally {
137
+ // Restore original paths
138
+ this.manifestManager.workingDir = originalWorkingDir;
139
+ this.manifestManager.manifestPath = originalManifestPath;
140
+ this.manifestManager.snapshotsDir = originalSnapshotsDir;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Look for DESIGN.md, ARCHITECTURE.md, README.md in priority order.
146
+ * @param {string} targetDir
147
+ * @returns {string|null} Path to the found design file, or null
148
+ */
149
+ discoverDesignFile(targetDir) {
150
+ for (const candidate of DESIGN_FILE_CANDIDATES) {
151
+ const filePath = path.join(targetDir, candidate);
152
+ if (fs.existsSync(filePath)) {
153
+ // For README.md, only use it if it has substantial content
154
+ if (candidate === 'README.md') {
155
+ try {
156
+ const stat = fs.statSync(filePath);
157
+ // Skip very small READMEs (likely just a title)
158
+ if (stat.size < 200) continue;
159
+ } catch {
160
+ continue;
161
+ }
162
+ }
163
+ return filePath;
164
+ }
165
+ }
166
+ return null;
167
+ }
168
+
169
+ /**
170
+ * Parse a markdown document into structured sections.
171
+ * @param {string} content - Raw markdown content
172
+ * @returns {{ title: string, sections: Array<{heading: string, level: number, content: string}>, rawContent: string }}
173
+ */
174
+ parseDesignDoc(content) {
175
+ const lines = content.split('\n');
176
+ const sections = [];
177
+ let title = '';
178
+ let currentSection = null;
179
+
180
+ for (const line of lines) {
181
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
182
+ if (headingMatch) {
183
+ // Save previous section
184
+ if (currentSection) {
185
+ currentSection.content = currentSection.content.trim();
186
+ sections.push(currentSection);
187
+ }
188
+
189
+ const level = headingMatch[1].length;
190
+ const heading = headingMatch[2].replace(/\*\*/g, '').trim();
191
+
192
+ // First H1 or H2 is the title
193
+ if (!title && level <= 2) {
194
+ title = heading;
195
+ }
196
+
197
+ currentSection = { heading, level, content: '' };
198
+ } else if (currentSection) {
199
+ currentSection.content += line + '\n';
200
+ }
201
+ }
202
+
203
+ // Push final section
204
+ if (currentSection) {
205
+ currentSection.content = currentSection.content.trim();
206
+ sections.push(currentSection);
207
+ }
208
+
209
+ return { title, sections, rawContent: content };
210
+ }
211
+
212
+ /**
213
+ * Extract features from parsed design document.
214
+ * @param {{ title: string, sections: Array, rawContent: string }} parsed
215
+ * @returns {Array<{id: string, name: string, phase: string, priority: string, dependencies: string}>}
216
+ */
217
+ extractFeatures(parsed) {
218
+ const features = [];
219
+ let featureCounter = 1;
220
+
221
+ for (const section of parsed.sections) {
222
+ const headingLower = section.heading.toLowerCase();
223
+
224
+ // Strategy 1: Check if this is a feature-listing section
225
+ const isFeatureListSection = FEATURE_SECTION_TITLES.some(t => headingLower.includes(t));
226
+
227
+ if (isFeatureListSection) {
228
+ // Extract features from bullets and sub-headings within this section
229
+ const bulletFeatures = this.extractFeaturesFromBullets(section.content);
230
+ for (const bf of bulletFeatures) {
231
+ const phase = this.determinePhase(bf.detail || section.content);
232
+ features.push({
233
+ id: `FEAT-${String(featureCounter++).padStart(3, '0')}`,
234
+ name: bf.name,
235
+ phase,
236
+ priority: 'Medium',
237
+ dependencies: '-'
238
+ });
239
+ }
240
+ continue;
241
+ }
242
+
243
+ // Strategy 2: Check if the heading itself describes a feature/component
244
+ const isFeatureHeading = FEATURE_KEYWORDS.some(kw => headingLower.includes(kw));
245
+
246
+ // Also check for Phase-named sections from the project's own design doc pattern
247
+ // e.g., "Phase I: The Discovery Anchor"
248
+ const isPhaseSection = /phase\s+[ivx\d]+/i.test(headingLower);
249
+
250
+ if (isFeatureHeading || isPhaseSection) {
251
+ const phase = this.determinePhase(section.content);
252
+ const name = this.cleanFeatureName(section.heading);
253
+ features.push({
254
+ id: `FEAT-${String(featureCounter++).padStart(3, '0')}`,
255
+ name,
256
+ phase,
257
+ priority: this.inferPriority(section),
258
+ dependencies: '-'
259
+ });
260
+ }
261
+ }
262
+
263
+ // If we found nothing, try extracting from table rows
264
+ if (features.length === 0) {
265
+ const tableFeatures = this.extractFeaturesFromTables(parsed.rawContent);
266
+ for (const tf of tableFeatures) {
267
+ features.push({
268
+ id: `FEAT-${String(featureCounter++).padStart(3, '0')}`,
269
+ name: tf,
270
+ phase: 'Discovery',
271
+ priority: 'Medium',
272
+ dependencies: '-'
273
+ });
274
+ }
275
+ }
276
+
277
+ return features;
278
+ }
279
+
280
+ /**
281
+ * Extract feature names from bullet/numbered lists.
282
+ * @param {string} content - Section content
283
+ * @returns {Array<{name: string, detail: string}>}
284
+ */
285
+ extractFeaturesFromBullets(content) {
286
+ const features = [];
287
+ const lines = content.split('\n');
288
+
289
+ for (const line of lines) {
290
+ // Match bullets: "- Feature Name" or "* Feature Name" or "1. Feature Name"
291
+ const bulletMatch = line.match(/^\s*[-*]\s+\*?\*?(.+?)\*?\*?\s*$/);
292
+ const numberedMatch = line.match(/^\s*\d+\.\s+\*?\*?(.+?)\*?\*?\s*$/);
293
+
294
+ const match = bulletMatch || numberedMatch;
295
+ if (match) {
296
+ let name = match[1].trim();
297
+ // Remove trailing punctuation like colons
298
+ name = name.replace(/[:\.]$/, '').trim();
299
+ // Remove markdown bold/italic
300
+ name = name.replace(/\*\*/g, '').replace(/\*/g, '').trim();
301
+ // Skip very short or generic items
302
+ if (name.length > 2 && name.length < 100) {
303
+ // Split on colon or dash to get name vs detail
304
+ const parts = name.split(/:\s*|—\s*|–\s*/);
305
+ features.push({
306
+ name: parts[0].trim(),
307
+ detail: parts.length > 1 ? parts.slice(1).join(' ').trim() : ''
308
+ });
309
+ }
310
+ }
311
+ }
312
+
313
+ return features;
314
+ }
315
+
316
+ /**
317
+ * Extract feature-like items from markdown tables.
318
+ * @param {string} content - Raw document content
319
+ * @returns {string[]} Feature names
320
+ */
321
+ extractFeaturesFromTables(content) {
322
+ const features = [];
323
+ const lines = content.split('\n');
324
+
325
+ for (let i = 0; i < lines.length; i++) {
326
+ const line = lines[i].trim();
327
+ if (!line.startsWith('|')) continue;
328
+
329
+ const cols = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
330
+
331
+ // Skip header and separator rows
332
+ if (cols.some(c => c.match(/^-+$/))) continue;
333
+ if (i > 0 && lines[i - 1] && lines[i - 1].includes('---')) continue;
334
+
335
+ // If first column looks like a section/feature name
336
+ if (cols.length >= 2 && cols[0].length > 2) {
337
+ const name = cols[0].replace(/\*\*/g, '').trim();
338
+ // Skip generic table headers
339
+ if (!['section', 'name', 'id', 'feature', 'column', '#'].includes(name.toLowerCase())) {
340
+ features.push(name);
341
+ }
342
+ }
343
+ }
344
+
345
+ return features;
346
+ }
347
+
348
+ /**
349
+ * Extract invariants/constraints from parsed design document.
350
+ * @param {{ title: string, sections: Array, rawContent: string }} parsed
351
+ * @returns {Array<{id: string, name: string, description: string}>}
352
+ */
353
+ extractInvariants(parsed) {
354
+ const invariants = [];
355
+ let invCounter = 1;
356
+
357
+ for (const section of parsed.sections) {
358
+ const headingLower = section.heading.toLowerCase();
359
+ const isConstraintSection = INVARIANT_KEYWORDS.some(kw => headingLower.includes(kw));
360
+
361
+ if (isConstraintSection) {
362
+ // Extract constraints from bullets in this section
363
+ const constraints = this.extractConstraintsFromBullets(section.content);
364
+ for (const c of constraints) {
365
+ invariants.push({
366
+ id: `INV-${String(invCounter++).padStart(3, '0')}`,
367
+ name: c.name,
368
+ description: c.description
369
+ });
370
+ }
371
+ continue;
372
+ }
373
+
374
+ // Also scan all sections for constraint-like bullets
375
+ const inlineConstraints = this.extractInlineConstraints(section.content);
376
+ for (const c of inlineConstraints) {
377
+ invariants.push({
378
+ id: `INV-${String(invCounter++).padStart(3, '0')}`,
379
+ name: c.name,
380
+ description: c.description
381
+ });
382
+ }
383
+ }
384
+
385
+ // Deduplicate by name
386
+ const seen = new Set();
387
+ return invariants.filter(inv => {
388
+ const key = inv.name.toLowerCase();
389
+ if (seen.has(key)) return false;
390
+ seen.add(key);
391
+ return true;
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Extract constraints from bullet points in a constraint-focused section.
397
+ * @param {string} content
398
+ * @returns {Array<{name: string, description: string}>}
399
+ */
400
+ extractConstraintsFromBullets(content) {
401
+ const constraints = [];
402
+ const lines = content.split('\n');
403
+
404
+ for (const line of lines) {
405
+ const bulletMatch = line.match(/^\s*[-*]\s+(.+)$/);
406
+ if (!bulletMatch) continue;
407
+
408
+ let text = bulletMatch[1].replace(/\*\*/g, '').trim();
409
+ if (text.length < 5) continue;
410
+
411
+ // Split on colon for name:description pattern
412
+ const parts = text.split(/:\s*/);
413
+ if (parts.length >= 2) {
414
+ constraints.push({
415
+ name: parts[0].trim().substring(0, 40),
416
+ description: parts.slice(1).join(': ').trim()
417
+ });
418
+ } else {
419
+ // Use first few words as name, full text as description
420
+ const words = text.split(/\s+/);
421
+ constraints.push({
422
+ name: words.slice(0, 4).join(' '),
423
+ description: text
424
+ });
425
+ }
426
+ }
427
+
428
+ return constraints;
429
+ }
430
+
431
+ /**
432
+ * Scan content for inline constraint-like statements.
433
+ * These are bullets starting with "Must", "Never", "Always", etc.
434
+ * @param {string} content
435
+ * @returns {Array<{name: string, description: string}>}
436
+ */
437
+ extractInlineConstraints(content) {
438
+ const constraints = [];
439
+ const lines = content.split('\n');
440
+
441
+ for (const line of lines) {
442
+ const bulletMatch = line.match(/^\s*[-*]\s+(.+)$/);
443
+ if (!bulletMatch) continue;
444
+
445
+ const text = bulletMatch[1].replace(/\*\*/g, '').trim();
446
+ const textLower = text.toLowerCase();
447
+
448
+ const isConstraint = CONSTRAINT_PREFIXES.some(prefix => textLower.startsWith(prefix));
449
+ if (isConstraint) {
450
+ const words = text.split(/\s+/);
451
+ constraints.push({
452
+ name: words.slice(0, 4).join(' '),
453
+ description: text
454
+ });
455
+ }
456
+ }
457
+
458
+ return constraints;
459
+ }
460
+
461
+ /**
462
+ * Determine the initial phase for a feature based on content detail level.
463
+ * @param {string} content - The feature's descriptive content
464
+ * @returns {string} Phase name
465
+ */
466
+ determinePhase(content) {
467
+ if (!content || content.length === 0) return 'Discovery';
468
+
469
+ const contentLower = content.toLowerCase();
470
+
471
+ // Check for interface-level detail
472
+ const interfaceScore = INTERFACE_KEYWORDS.reduce((score, kw) => {
473
+ return score + (contentLower.includes(kw) ? 1 : 0);
474
+ }, 0);
475
+
476
+ if (interfaceScore >= 2) return 'Interface';
477
+
478
+ // Check for design-review-level detail
479
+ const designScore = DESIGN_REVIEW_KEYWORDS.reduce((score, kw) => {
480
+ return score + (contentLower.includes(kw) ? 1 : 0);
481
+ }, 0);
482
+
483
+ if (designScore >= 2) return 'Design Review';
484
+
485
+ // Default to Discovery
486
+ return 'Discovery';
487
+ }
488
+
489
+ /**
490
+ * Clean up a heading to use as a feature name.
491
+ * @param {string} heading
492
+ * @returns {string}
493
+ */
494
+ cleanFeatureName(heading) {
495
+ return heading
496
+ .replace(/^(phase\s+[ivx\d]+\s*[:\-–—]\s*)/i, '') // Remove "Phase I: " prefix
497
+ .replace(/^(the\s+)/i, '') // Remove leading "The "
498
+ .replace(/\*\*/g, '') // Remove bold
499
+ .replace(/`/g, '') // Remove code ticks
500
+ .trim();
501
+ }
502
+
503
+ /**
504
+ * Infer priority from section content and heading level.
505
+ * @param {{ heading: string, level: number, content: string }} section
506
+ * @returns {string}
507
+ */
508
+ inferPriority(section) {
509
+ const contentLower = (section.content || '').toLowerCase();
510
+
511
+ if (contentLower.includes('critical') || contentLower.includes('essential') ||
512
+ contentLower.includes('core') || section.level <= 2) {
513
+ return 'High';
514
+ }
515
+
516
+ if (contentLower.includes('optional') || contentLower.includes('nice to have') ||
517
+ contentLower.includes('future') || contentLower.includes('stretch')) {
518
+ return 'Low';
519
+ }
520
+
521
+ return 'Medium';
522
+ }
523
+ }
@@ -0,0 +1,172 @@
1
+ // Desktop Automation Tools using @nut-tree-fork/nut-js
2
+ // Provides keyboard and mouse control, screen analysis, and window management
3
+
4
+ import { mouse, keyboard, screen, Point, Button, Key, imageResource, straightTo, centerOf, sleep } from '@nut-tree-fork/nut-js';
5
+ import { consoleStyler } from '../ui/console-styler.mjs';
6
+
7
+ // Configure nut.js defaults
8
+ mouse.config.mouseSpeed = 1000; // Pixels per second
9
+
10
+ export class DesktopAutomationTools {
11
+ constructor() {
12
+ this.isAutomationEnabled = true; // Can be toggled via config in future
13
+ }
14
+
15
+ // Move mouse to coordinates
16
+ async moveMouse(args) {
17
+ const { x, y, speed } = args;
18
+
19
+ consoleStyler.log('working', `Moving mouse to (${x}, ${y})`);
20
+
21
+ try {
22
+ if (speed) {
23
+ mouse.config.mouseSpeed = speed;
24
+ }
25
+
26
+ await mouse.move(straightTo(new Point(x, y)));
27
+
28
+ // Reset speed to default
29
+ mouse.config.mouseSpeed = 1000;
30
+
31
+ return `Mouse moved to (${x}, ${y})`;
32
+ } catch (error) {
33
+ consoleStyler.log('error', `Mouse move failed: ${error.message}`);
34
+ return `Error moving mouse: ${error.message}`;
35
+ }
36
+ }
37
+
38
+ // Click mouse button
39
+ async clickMouse(args) {
40
+ const { button = 'left', double_click = false } = args;
41
+
42
+ consoleStyler.log('working', `${double_click ? 'Double-' : ''}Clicking ${button} mouse button`);
43
+
44
+ try {
45
+ const btn = button === 'right' ? Button.RIGHT : (button === 'middle' ? Button.MIDDLE : Button.LEFT);
46
+
47
+ if (double_click) {
48
+ await mouse.doubleClick(btn);
49
+ } else {
50
+ await mouse.click(btn);
51
+ }
52
+
53
+ return `Clicked ${button} button${double_click ? ' (double)' : ''}`;
54
+ } catch (error) {
55
+ consoleStyler.log('error', `Mouse click failed: ${error.message}`);
56
+ return `Error clicking mouse: ${error.message}`;
57
+ }
58
+ }
59
+
60
+ // Type text
61
+ async typeText(args) {
62
+ const { text, delay = 0 } = args;
63
+
64
+ consoleStyler.log('working', `Typing text: "${text}"`);
65
+
66
+ try {
67
+ if (delay > 0) {
68
+ keyboard.config.autoDelayMs = delay;
69
+ }
70
+
71
+ await keyboard.type(text);
72
+
73
+ // Reset delay
74
+ keyboard.config.autoDelayMs = 500;
75
+
76
+ return `Typed "${text}"`;
77
+ } catch (error) {
78
+ consoleStyler.log('error', `Typing failed: ${error.message}`);
79
+ return `Error typing text: ${error.message}`;
80
+ }
81
+ }
82
+
83
+ // Press specific key(s)
84
+ async pressKey(args) {
85
+ const { keys } = args; // Array of key names like ['Control', 'c']
86
+
87
+ consoleStyler.log('working', `Pressing keys: ${keys.join(' + ')}`);
88
+
89
+ try {
90
+ const mappedKeys = keys.map(k => this.mapKey(k)).filter(k => k !== null);
91
+
92
+ if (mappedKeys.length === 0) {
93
+ return "Error: No valid keys provided";
94
+ }
95
+
96
+ await keyboard.pressKey(...mappedKeys);
97
+ await keyboard.releaseKey(...mappedKeys);
98
+
99
+ return `Pressed ${keys.join(' + ')}`;
100
+ } catch (error) {
101
+ consoleStyler.log('error', `Key press failed: ${error.message}`);
102
+ return `Error pressing keys: ${error.message}`;
103
+ }
104
+ }
105
+
106
+ // Capture screen
107
+ async captureScreen(args) {
108
+ const { filename = 'screenshot.png' } = args;
109
+
110
+ consoleStyler.log('working', `Capturing screen to ${filename}`);
111
+
112
+ try {
113
+ const img = await screen.grab();
114
+ await screen.save(filename); // nut.js handles format based on extension
115
+ return `Screenshot saved to ${filename} (${img.width}x${img.height})`;
116
+ } catch (error) {
117
+ consoleStyler.log('error', `Screen capture failed: ${error.message}`);
118
+ return `Error capturing screen: ${error.message}`;
119
+ }
120
+ }
121
+
122
+ // Get screen dimensions
123
+ async getScreenSize() {
124
+ try {
125
+ const width = await screen.width();
126
+ const height = await screen.height();
127
+ return `Screen size: ${width}x${height}`;
128
+ } catch (error) {
129
+ return `Error getting screen size: ${error.message}`;
130
+ }
131
+ }
132
+
133
+ // Helper to map string key names to nut.js Key constants
134
+ mapKey(keyName) {
135
+ const keyMap = {
136
+ 'enter': Key.Enter,
137
+ 'escape': Key.Escape,
138
+ 'tab': Key.Tab,
139
+ 'space': Key.Space,
140
+ 'backspace': Key.Backspace,
141
+ 'delete': Key.Delete,
142
+ 'control': Key.LeftControl, // Default to left
143
+ 'alt': Key.LeftAlt,
144
+ 'shift': Key.LeftShift,
145
+ 'command': Key.LeftSuper, // Mac Command / Windows Key
146
+ 'super': Key.LeftSuper,
147
+ 'up': Key.Up,
148
+ 'down': Key.Down,
149
+ 'left': Key.Left,
150
+ 'right': Key.Right,
151
+ 'f1': Key.F1, 'f2': Key.F2, 'f3': Key.F3, 'f4': Key.F4, 'f5': Key.F5,
152
+ 'f6': Key.F6, 'f7': Key.F7, 'f8': Key.F8, 'f9': Key.F9, 'f10': Key.F10,
153
+ 'f11': Key.F11, 'f12': Key.F12,
154
+ 'printscreen': Key.Print,
155
+ 'home': Key.Home,
156
+ 'end': Key.End,
157
+ 'pageup': Key.PageUp,
158
+ 'pagedown': Key.PageDown
159
+ };
160
+
161
+ // Handle single characters
162
+ if (keyName.length === 1) {
163
+ const upper = keyName.toUpperCase();
164
+ if (Key[upper] !== undefined) {
165
+ return Key[upper];
166
+ }
167
+ }
168
+
169
+ const lowerName = keyName.toLowerCase();
170
+ return keyMap[lowerName] !== undefined ? keyMap[lowerName] : null;
171
+ }
172
+ }