devtopia 1.8.3 → 2.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.
@@ -3,6 +3,7 @@ import { extname, resolve, dirname, join } from 'path';
3
3
  import { createInterface } from 'readline';
4
4
  import { API_BASE } from '../config.js';
5
5
  import { loadIdentity, hasIdentity } from '../identity.js';
6
+ import { validateExecution } from '../executor.js';
6
7
  const LANG_MAP = {
7
8
  '.ts': 'typescript',
8
9
  '.js': 'javascript',
@@ -10,74 +11,26 @@ const LANG_MAP = {
10
11
  '.mjs': 'javascript',
11
12
  '.cjs': 'javascript',
12
13
  };
13
- // Valid categories - grouped by type
14
- const CATEGORIES = [
15
- // Data & Parsing
16
- { id: 'data', name: 'Data Processing' },
17
- { id: 'json', name: 'JSON & Objects' },
18
- { id: 'csv', name: 'CSV & Tables' },
19
- { id: 'xml', name: 'XML & Markup' },
20
- { id: 'yaml', name: 'YAML & Config' },
21
- // Text & Strings
22
- { id: 'text', name: 'Text & NLP' },
23
- { id: 'string', name: 'String Utils' },
24
- { id: 'regex', name: 'Regex & Patterns' },
25
- { id: 'format', name: 'Formatting' },
26
- // Web & Network
27
- { id: 'web', name: 'Web & HTTP' },
28
- { id: 'api', name: 'API Tools' },
29
- { id: 'url', name: 'URL & Links' },
30
- { id: 'html', name: 'HTML & DOM' },
31
- // Security & Crypto
32
- { id: 'crypto', name: 'Crypto & Security' },
33
- { id: 'hash', name: 'Hashing' },
34
- { id: 'encode', name: 'Encoding' },
35
- { id: 'auth', name: 'Authentication' },
36
- // Math & Numbers
37
- { id: 'math', name: 'Math & Numbers' },
38
- { id: 'stats', name: 'Statistics' },
39
- { id: 'convert', name: 'Conversion' },
40
- { id: 'random', name: 'Random & Generate' },
41
- // Date & Time
42
- { id: 'time', name: 'Date & Time' },
43
- { id: 'timezone', name: 'Timezones' },
44
- // Files & System
45
- { id: 'file', name: 'File & I/O' },
46
- { id: 'path', name: 'Paths' },
47
- { id: 'compress', name: 'Compression' },
48
- // Arrays & Collections
49
- { id: 'array', name: 'Arrays & Lists' },
50
- { id: 'sort', name: 'Sort & Search' },
51
- { id: 'set', name: 'Sets & Maps' },
52
- // Validation
53
- { id: 'validate', name: 'Validation' },
54
- { id: 'sanitize', name: 'Sanitization' },
55
- // Generation
56
- { id: 'generate', name: 'Generation' },
57
- { id: 'template', name: 'Templates' },
58
- // AI & ML
59
- { id: 'ai', name: 'AI & ML' },
60
- { id: 'nlp', name: 'NLP' },
61
- // Media
62
- { id: 'image', name: 'Images' },
63
- { id: 'color', name: 'Colors' },
64
- { id: 'qr', name: 'QR & Barcodes' },
65
- // Dev Tools
66
- { id: 'cli', name: 'CLI & Terminal' },
67
- { id: 'debug', name: 'Debug & Test' },
68
- { id: 'diff', name: 'Diff & Compare' },
69
- // General
70
- { id: 'util', name: 'Utilities' },
71
- { id: 'other', name: 'Other' },
72
- ];
14
+ /**
15
+ * Fetch categories from the API (single source of truth)
16
+ */
17
+ async function fetchCategories() {
18
+ try {
19
+ const res = await fetch(`${API_BASE}/api/categories`);
20
+ const data = await res.json();
21
+ return (data.categories || []).map((c) => ({
22
+ id: c.id,
23
+ name: c.name,
24
+ description: c.description,
25
+ }));
26
+ }
27
+ catch {
28
+ // Fallback: return empty array submit will still work, server validates
29
+ return [];
30
+ }
31
+ }
73
32
  /**
74
33
  * Extract description from source code comments
75
- * Supports multiple formats:
76
- * - // tool-name - description
77
- * - # tool-name - description
78
- * - JSDoc: * tool-name - description
79
- * - JSDoc: First sentence after title in /** block
80
- * - JSDoc @description tag
81
34
  */
82
35
  function extractDescription(source) {
83
36
  const lines = source.split('\n');
@@ -85,7 +38,6 @@ function extractDescription(source) {
85
38
  let jsdocTitle = false;
86
39
  for (const line of lines) {
87
40
  const trimmed = line.trim();
88
- // Track JSDoc block
89
41
  if (trimmed === '/**') {
90
42
  inJSDoc = true;
91
43
  jsdocTitle = false;
@@ -95,7 +47,7 @@ function extractDescription(source) {
95
47
  inJSDoc = false;
96
48
  continue;
97
49
  }
98
- // TypeScript/JavaScript single line: // tool-name - description
50
+ // TypeScript/JavaScript: // tool-name - description
99
51
  const tsMatch = line.match(/^\/\/\s*[\w-]+\s*[-–:]\s*(.+)$/);
100
52
  if (tsMatch)
101
53
  return tsMatch[1].trim();
@@ -103,55 +55,35 @@ function extractDescription(source) {
103
55
  const pyMatch = line.match(/^#\s*[\w-]+\s*[-–:]\s*(.+)$/);
104
56
  if (pyMatch)
105
57
  return pyMatch[1].trim();
106
- // JSDoc block comment: * tool-name - description
107
- const jsdocToolMatch = line.match(/^\s*\*\s*[\w-]+\s*[-–:]\s*(.+)$/);
108
- if (jsdocToolMatch)
109
- return jsdocToolMatch[1].trim();
110
- // JSDoc @description tag
58
+ // JSDoc: * @description
111
59
  const jsdocDescMatch = line.match(/^\s*\*\s*@description\s+(.+)$/);
112
60
  if (jsdocDescMatch)
113
61
  return jsdocDescMatch[1].trim();
114
- // Inside JSDoc: look for description after title
62
+ // JSDoc: first content line after /** that describes the tool
115
63
  if (inJSDoc) {
116
- // Skip @ tags
117
64
  if (trimmed.match(/^\*\s*@/))
118
65
  continue;
119
- // Extract content after *
120
66
  const jsdocContent = trimmed.match(/^\*\s*(.+)$/);
121
67
  if (jsdocContent) {
122
68
  const content = jsdocContent[1].trim();
123
- // Skip if it looks like a title (short, capitalized, no punctuation)
124
69
  if (!jsdocTitle && content.length < 50 && !content.includes('.') && /^[A-Z]/.test(content)) {
125
70
  jsdocTitle = true;
126
71
  continue;
127
72
  }
128
- // After title, next non-empty content line is likely the description
129
73
  if (jsdocTitle && content.length > 10) {
130
- // Return first sentence
131
74
  const firstSentence = content.match(/^[^.!?]+[.!?]?/);
132
75
  if (firstSentence)
133
76
  return firstSentence[0].trim();
134
77
  return content;
135
78
  }
136
79
  }
137
- // Skip empty JSDoc lines
138
80
  if (trimmed === '*')
139
81
  continue;
140
82
  }
141
- // Skip empty lines, shebang, opening markers, and usage lines
142
- if (trimmed === '' ||
143
- line.startsWith('#!') ||
144
- trimmed === '/**' ||
145
- trimmed === '*/' ||
146
- trimmed === '*' ||
147
- trimmed.toLowerCase().includes('usage:')) {
83
+ if (trimmed === '' || line.startsWith('#!') || trimmed === '/**' || trimmed === '*/' || trimmed === '*' || trimmed.toLowerCase().includes('usage:')) {
148
84
  continue;
149
85
  }
150
- // Stop if we hit non-comment code
151
- if (!trimmed.startsWith('//') &&
152
- !trimmed.startsWith('#') &&
153
- !trimmed.startsWith('*') &&
154
- !trimmed.startsWith('/*')) {
86
+ if (!trimmed.startsWith('//') && !trimmed.startsWith('#') && !trimmed.startsWith('*') && !trimmed.startsWith('/*')) {
155
87
  break;
156
88
  }
157
89
  }
@@ -164,17 +96,13 @@ function extractDescriptionFromReadme(readme) {
164
96
  const lines = readme.split('\n');
165
97
  let foundTitle = false;
166
98
  for (const line of lines) {
167
- // Skip title lines (# Title)
168
99
  if (line.startsWith('#')) {
169
100
  foundTitle = true;
170
101
  continue;
171
102
  }
172
- // Skip empty lines after title
173
103
  if (foundTitle && line.trim() === '')
174
104
  continue;
175
- // First non-empty line after title is the description
176
105
  if (foundTitle && line.trim()) {
177
- // Take first sentence or up to 100 chars
178
106
  const desc = line.trim();
179
107
  const firstSentence = desc.match(/^[^.!?]+[.!?]?/);
180
108
  if (firstSentence && firstSentence[0].length <= 100) {
@@ -190,7 +118,20 @@ function extractDescriptionFromReadme(readme) {
190
118
  */
191
119
  function detectCategory(description, source) {
192
120
  const text = `${description || ''} ${source}`.toLowerCase();
193
- // Specific categories first (more specific matches)
121
+ // Use-case categories first (higher-level, more meaningful)
122
+ if ((text.includes('scrape') || text.includes('crawl') || text.includes('extract from page') || text.includes('web scraping')) && text.includes('html'))
123
+ return 'scraping';
124
+ if (text.includes('health check') || text.includes('uptime') || text.includes('monitor') || text.includes('observ'))
125
+ return 'monitoring';
126
+ if ((text.includes('pipeline') || text.includes('batch') || text.includes('schedule') || text.includes('automat')) && !text.includes('json'))
127
+ return 'automation';
128
+ if ((text.includes('api') && (text.includes('retry') || text.includes('integration') || text.includes('client'))) || text.includes('api integration'))
129
+ return 'integration';
130
+ if ((text.includes('analys') || text.includes('aggregate') || text.includes('report') || text.includes('insight')) && (text.includes('data') || text.includes('text')))
131
+ return 'analysis';
132
+ if (text.includes('content') && (text.includes('process') || text.includes('generat') || text.includes('transform')))
133
+ return 'content';
134
+ // Technical categories
194
135
  if (text.includes('jwt') || text.includes('oauth') || text.includes('token') || text.includes('auth'))
195
136
  return 'auth';
196
137
  if (text.includes('base64') || text.includes('hex') || text.includes('encode') || text.includes('decode'))
@@ -215,9 +156,21 @@ function detectCategory(description, source) {
215
156
  return 'validate';
216
157
  if (text.includes('sanitize') || text.includes('clean') || text.includes('escape'))
217
158
  return 'sanitize';
159
+ if (text.includes('polymarket'))
160
+ return 'polymarket';
161
+ if (text.includes('database') || text.includes('sql') || text.includes('query builder'))
162
+ return 'database';
163
+ if (text.includes('email'))
164
+ return 'email';
165
+ if (text.includes('graph') || text.includes('dfs') || text.includes('bfs') || text.includes('traversal'))
166
+ return 'graph';
167
+ if (text.includes('log') && (text.includes('structured') || text.includes('logging')))
168
+ return 'logging';
169
+ if (text.includes('geo') || text.includes('haversine') || text.includes('latitude') || text.includes('longitude'))
170
+ return 'geo';
218
171
  if (text.includes('qr') || text.includes('barcode'))
219
172
  return 'qr';
220
- if (text.includes('color') || text.includes('rgb') || text.includes('hex') || text.includes('hsl'))
173
+ if (text.includes('color') || text.includes('rgb') || text.includes('hsl'))
221
174
  return 'color';
222
175
  if (text.includes('image') || text.includes('resize') || text.includes('thumbnail'))
223
176
  return 'image';
@@ -241,7 +194,7 @@ function detectCategory(description, source) {
241
194
  return 'nlp';
242
195
  if (text.includes('cli') || text.includes('terminal') || text.includes('command'))
243
196
  return 'cli';
244
- if (text.includes('debug') || text.includes('test') || text.includes('log'))
197
+ if (text.includes('debug') || text.includes('test'))
245
198
  return 'debug';
246
199
  if (text.includes('html') || text.includes('dom') || text.includes('scrape'))
247
200
  return 'html';
@@ -249,7 +202,6 @@ function detectCategory(description, source) {
249
202
  return 'url';
250
203
  if (text.includes('api') || text.includes('rest') || text.includes('graphql'))
251
204
  return 'api';
252
- // Broader categories
253
205
  if (text.includes('fetch') || text.includes('http') || text.includes('request'))
254
206
  return 'web';
255
207
  if (text.includes('encrypt') || text.includes('crypto') || text.includes('secure'))
@@ -286,6 +238,46 @@ function findReadme(filePath, toolName) {
286
238
  }
287
239
  return null;
288
240
  }
241
+ /**
242
+ * Validate tool source: check it defines proper I/O
243
+ */
244
+ function validateToolSource(source, language) {
245
+ const warnings = [];
246
+ if (language === 'javascript' || language === 'typescript') {
247
+ const hasArgvParse = /process\.argv\[2\]/.test(source);
248
+ const definesMain = /\bfunction\s+main\s*\(/.test(source);
249
+ const hasConsoleLog = /console\.log\s*\(/.test(source);
250
+ const hasJSONStringify = /JSON\.stringify/.test(source);
251
+ if (!hasArgvParse && !definesMain) {
252
+ warnings.push('Tool does not read input (process.argv[2]) or define main(). It may not work when run.');
253
+ }
254
+ if (!hasConsoleLog && !definesMain) {
255
+ warnings.push('Tool does not output results (console.log). It may produce no output when run.');
256
+ }
257
+ if (!hasJSONStringify && !definesMain) {
258
+ warnings.push('Tool does not use JSON.stringify. Output may not be valid JSON.');
259
+ }
260
+ if (definesMain && !hasArgvParse) {
261
+ // This will be auto-wrapped by executor — that's fine
262
+ }
263
+ }
264
+ if (language === 'python') {
265
+ const hasSysArgv = /sys\.argv/.test(source);
266
+ const definesMain = /\bdef\s+main\s*\(/.test(source);
267
+ const hasPrint = /\bprint\s*\(/.test(source);
268
+ const hasJsonDumps = /json\.dumps/.test(source);
269
+ if (!hasSysArgv && !definesMain) {
270
+ warnings.push('Tool does not read input (sys.argv) or define main(). It may not work when run.');
271
+ }
272
+ if (!hasPrint && !definesMain) {
273
+ warnings.push('Tool does not print output. It may produce nothing when run.');
274
+ }
275
+ if (!hasJsonDumps && !definesMain) {
276
+ warnings.push('Tool does not use json.dumps. Output may not be valid JSON.');
277
+ }
278
+ }
279
+ return { valid: true, warnings };
280
+ }
289
281
  export async function submit(name, file, options) {
290
282
  // Check identity
291
283
  if (!hasIdentity()) {
@@ -320,6 +312,15 @@ export async function submit(name, file, options) {
320
312
  }
321
313
  // Read source
322
314
  const source = readFileSync(filePath, 'utf-8');
315
+ // ── Validate tool source format ──
316
+ const { warnings } = validateToolSource(source, language);
317
+ if (warnings.length > 0) {
318
+ console.log(`\n⚠️ Source validation warnings:`);
319
+ for (const w of warnings) {
320
+ console.log(` - ${w}`);
321
+ }
322
+ console.log(` (tool may still work if executor can auto-wrap it)\n`);
323
+ }
323
324
  // Find and read README
324
325
  let readmePath = options.readme ? resolve(options.readme) : findReadme(filePath, name);
325
326
  let readme = null;
@@ -339,11 +340,9 @@ export async function submit(name, file, options) {
339
340
  }
340
341
  // Extract or use provided description
341
342
  let description = options.description || extractDescription(source);
342
- // Try to get description from README if not found in source
343
343
  if (!description && readme) {
344
344
  description = extractDescriptionFromReadme(readme);
345
345
  }
346
- // Description is required
347
346
  if (!description) {
348
347
  console.log(`\n❌ Description required. Please provide a description for your tool.`);
349
348
  console.log(`\n Option 1: Use -d "Your description here"`);
@@ -352,13 +351,15 @@ export async function submit(name, file, options) {
352
351
  console.log(` Option 3: Add a description after the title in your README\n`);
353
352
  process.exit(1);
354
353
  }
354
+ // ── Fetch categories from API (single source of truth) ──
355
+ const CATEGORIES = await fetchCategories();
355
356
  // Determine category
356
357
  let category = options.category;
357
- if (category && !CATEGORIES.find(c => c.id === category)) {
358
+ if (category && CATEGORIES.length > 0 && !CATEGORIES.find(c => c.id === category)) {
358
359
  console.log(`\n❌ Invalid category: ${category}`);
359
360
  console.log(`\n Valid categories:`);
360
361
  for (const cat of CATEGORIES) {
361
- console.log(` ${cat.id.padEnd(10)} ${cat.name}`);
362
+ console.log(` ${cat.id.padEnd(12)} ${cat.name}`);
362
363
  }
363
364
  console.log();
364
365
  process.exit(1);
@@ -367,35 +368,25 @@ export async function submit(name, file, options) {
367
368
  if (!category) {
368
369
  category = detectCategory(description, source);
369
370
  }
370
- // Prompt for category confirmation/selection if not provided via CLI
371
- if (!options.category && process.stdin.isTTY) {
372
- const rl = createInterface({
373
- input: process.stdin,
374
- output: process.stdout
375
- });
371
+ // Prompt for category confirmation if TTY
372
+ if (!options.category && process.stdin.isTTY && CATEGORIES.length > 0) {
373
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
376
374
  const detectedCat = CATEGORIES.find(c => c.id === category);
377
375
  console.log(`\n📁 Category Selection`);
378
376
  console.log(` Auto-detected: ${detectedCat?.name || category} (${category})`);
379
377
  console.log(`\n Common categories:`);
380
- // Show most common/relevant categories
381
- const commonCategories = [
382
- 'api', 'json', 'data', 'text', 'web', 'crypto', 'file',
383
- 'array', 'validate', 'util', 'other'
384
- ];
378
+ const commonCategories = ['api', 'json', 'data', 'text', 'web', 'crypto', 'file', 'array', 'validate', 'util', 'other'];
385
379
  for (const catId of commonCategories) {
386
380
  const cat = CATEGORIES.find(c => c.id === catId);
387
381
  if (cat) {
388
- const marker = cat.id === category ? ' detected' : '';
382
+ const marker = cat.id === category ? ' <- detected' : '';
389
383
  console.log(` ${cat.id.padEnd(12)} ${cat.name}${marker}`);
390
384
  }
391
385
  }
392
386
  console.log(`\n (Use 'devtopia categories' to see all categories)`);
393
387
  console.log(`\n Press Enter to use detected category, or type a category ID:`);
394
388
  const answer = await new Promise((resolve) => {
395
- rl.question(` Category [${category}]: `, (ans) => {
396
- rl.close();
397
- resolve(ans.trim());
398
- });
389
+ rl.question(` Category [${category}]: `, (ans) => { rl.close(); resolve(ans.trim()); });
399
390
  });
400
391
  if (answer) {
401
392
  const selectedCat = CATEGORIES.find(c => c.id === answer.toLowerCase());
@@ -415,16 +406,58 @@ export async function submit(name, file, options) {
415
406
  const dependencies = options.deps
416
407
  ? options.deps.split(',').map(d => d.trim()).filter(Boolean)
417
408
  : [];
418
- // Parse builds_on (parent tools this tool extends/composes)
409
+ // Parse builds_on
419
410
  const buildsOn = options.buildsOn
420
411
  ? options.buildsOn.split(',').map(d => d.trim()).filter(Boolean)
421
412
  : [];
422
- // Encourage lineage tracking
423
413
  if (!options.buildsOn && buildsOn.length === 0) {
424
414
  console.log(`\n💡 Tip: Use --builds-on to show lineage!`);
425
415
  console.log(` Example: --builds-on api-request,json-validate`);
426
416
  console.log(` This helps others see how tools build on each other.\n`);
427
417
  }
418
+ // ── Extract schema from JSDoc @param tags ──
419
+ let inputSchema = null;
420
+ let outputSchema = null;
421
+ if (options.schema) {
422
+ try {
423
+ const schemaContent = JSON.parse(readFileSync(resolve(options.schema), 'utf-8'));
424
+ inputSchema = schemaContent.inputs || null;
425
+ outputSchema = schemaContent.outputs || null;
426
+ }
427
+ catch {
428
+ console.log(`\n⚠️ Could not parse schema file: ${options.schema}`);
429
+ }
430
+ }
431
+ if (!inputSchema) {
432
+ // Auto-extract from JSDoc @param tags
433
+ const paramMatches = source.matchAll(/@param\s+\{(\w+)\}\s+([\w.]+)\s*[-–]?\s*(.*)/g);
434
+ const extracted = {};
435
+ for (const match of paramMatches) {
436
+ const type = match[1].toLowerCase();
437
+ const paramName = match[2].replace(/^params\./, '');
438
+ if (paramName !== 'params') {
439
+ extracted[paramName] = type;
440
+ }
441
+ }
442
+ if (Object.keys(extracted).length > 0) {
443
+ inputSchema = extracted;
444
+ }
445
+ }
446
+ // ── Pre-submit execution validation ──
447
+ if (!options.skipValidation) {
448
+ console.log(`\n Validating tool execution...`);
449
+ const validation = await validateExecution(source, language);
450
+ if (!validation.valid) {
451
+ const reason = validation.stderr
452
+ ? validation.stderr.split('\n')[0]
453
+ : (validation.stdout ? 'Tool produced non-JSON output.' : 'Tool produced no output.');
454
+ console.log(`\n❌ Tool failed validation: ${reason}`);
455
+ console.log(`\n Your tool must produce JSON output (even error JSON like {"error":"..."}).`);
456
+ console.log(` Use --skip-validation to bypass this check.\n`);
457
+ process.exit(1);
458
+ }
459
+ console.log(` Validation passed (exit ${validation.exitCode}, ${validation.stdout.length} bytes output)`);
460
+ }
428
461
  const catInfo = CATEGORIES.find(c => c.id === category);
429
462
  console.log(`\n📦 Submitting ${name}...`);
430
463
  console.log(` File: ${file}`);
@@ -433,6 +466,8 @@ export async function submit(name, file, options) {
433
466
  console.log(` README: ${readmePath}`);
434
467
  if (description)
435
468
  console.log(` Desc: ${description}`);
469
+ if (inputSchema)
470
+ console.log(` Schema: ${Object.keys(inputSchema).join(', ')}`);
436
471
  if (dependencies.length)
437
472
  console.log(` Deps: ${dependencies.join(', ')}`);
438
473
  if (buildsOn.length)
@@ -451,6 +486,8 @@ export async function submit(name, file, options) {
451
486
  source,
452
487
  dependencies,
453
488
  builds_on: buildsOn.length > 0 ? buildsOn : undefined,
489
+ input_schema: inputSchema ? JSON.stringify(inputSchema) : undefined,
490
+ output_schema: outputSchema ? JSON.stringify(outputSchema) : undefined,
454
491
  }),
455
492
  });
456
493
  const data = await res.json();
@@ -465,6 +502,14 @@ export async function submit(name, file, options) {
465
502
  if (buildsOn.length > 0) {
466
503
  console.log(` Builds on: ${buildsOn.join(', ')}`);
467
504
  }
505
+ // Show similar tools warning (duplicate detection)
506
+ if (data.similar_tools && data.similar_tools.length > 0) {
507
+ console.log(`\n⚠️ Similar tools already exist:`);
508
+ for (const sim of data.similar_tools) {
509
+ console.log(` - ${sim.name}: "${sim.description || 'No description'}"`);
510
+ }
511
+ console.log(` Consider composing with existing tools instead of creating new ones.`);
512
+ }
468
513
  console.log(`\n Others can now:`);
469
514
  console.log(` $ devtopia cat ${name}`);
470
515
  console.log(` $ devtopia run ${name} '{...}'\n`);
@@ -4,8 +4,34 @@ interface ExecutionResult {
4
4
  error?: string;
5
5
  durationMs: number;
6
6
  }
7
+ interface ExecuteOptions {
8
+ strictJson?: boolean;
9
+ }
10
+ interface ValidationResult {
11
+ valid: boolean;
12
+ stdout: string;
13
+ stderr: string;
14
+ exitCode: number | null;
15
+ }
16
+ /**
17
+ * Get the interpreter command for a language
18
+ */
19
+ export declare function getInterpreter(language: string): {
20
+ cmd: string;
21
+ args: string[];
22
+ ext: string;
23
+ };
24
+ /**
25
+ * Detect if source defines a `function main(...)` without calling it,
26
+ * and wrap it with the standard argv boilerplate so it actually executes.
27
+ */
28
+ export declare function wrapSourceIfNeeded(source: string, language: string): string;
7
29
  /**
8
- * Execute a tool locally
30
+ * Validate that a tool source actually runs and produces valid JSON output.
31
+ * Runs with {} as input, 10s timeout.
32
+ * Pass: process produces valid JSON stdout (even error JSON like {"error":"..."}).
33
+ * Fail: no stdout, non-JSON stdout with exit 0, or process hangs.
9
34
  */
10
- export declare function executeTool(toolName: string, input: any): Promise<ExecutionResult>;
35
+ export declare function validateExecution(source: string, language: string): Promise<ValidationResult>;
36
+ export declare function executeTool(toolName: string, input: any, options?: ExecuteOptions): Promise<ExecutionResult>;
11
37
  export {};