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.
- package/dist/commands/compose.d.ts +5 -0
- package/dist/commands/compose.js +120 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +810 -0
- package/dist/commands/run.d.ts +6 -1
- package/dist/commands/run.js +57 -6
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +52 -0
- package/dist/commands/start.d.ts +1 -1
- package/dist/commands/start.js +150 -207
- package/dist/commands/submit.d.ts +2 -0
- package/dist/commands/submit.js +167 -122
- package/dist/executor.d.ts +28 -2
- package/dist/executor.js +237 -67
- package/dist/index.js +42 -3
- package/package.json +1 -1
package/dist/commands/submit.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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('
|
|
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')
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 ? '
|
|
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
|
|
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`);
|
package/dist/executor.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 {};
|