@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.
- package/ai.mjs +8 -0
- package/package.json +48 -0
- package/src/cli/cli-interface.mjs +271 -0
- package/src/config.mjs +64 -0
- package/src/core/ai-assistant.mjs +540 -0
- package/src/core/ai-provider.mjs +579 -0
- package/src/core/history-manager.mjs +330 -0
- package/src/core/system-prompt.mjs +182 -0
- package/src/custom-tools/custom-tools-manager.mjs +310 -0
- package/src/execution/tool-executor.mjs +892 -0
- package/src/lib/README.md +114 -0
- package/src/lib/adapters/console-status-adapter.mjs +48 -0
- package/src/lib/adapters/network-llm-adapter.mjs +37 -0
- package/src/lib/index.mjs +101 -0
- package/src/lib/interfaces.d.ts +98 -0
- package/src/main.mjs +61 -0
- package/src/package/package-manager.mjs +223 -0
- package/src/quality/code-validator.mjs +126 -0
- package/src/quality/quality-evaluator.mjs +248 -0
- package/src/reasoning/reasoning-system.mjs +258 -0
- package/src/structured-dev/flow-manager.mjs +321 -0
- package/src/structured-dev/implementation-planner.mjs +223 -0
- package/src/structured-dev/manifest-manager.mjs +423 -0
- package/src/structured-dev/plan-executor.mjs +113 -0
- package/src/structured-dev/project-bootstrapper.mjs +523 -0
- package/src/tools/desktop-automation-tools.mjs +172 -0
- package/src/tools/file-tools.mjs +141 -0
- package/src/tools/tool-definitions.mjs +872 -0
- package/src/ui/console-styler.mjs +503 -0
- package/src/workspace/workspace-manager.mjs +215 -0
- package/themes.json +66 -0
|
@@ -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
|
+
}
|