devtopia 1.9.0 → 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/executor.js CHANGED
@@ -18,7 +18,7 @@ async function fetchTool(name) {
18
18
  /**
19
19
  * Get the interpreter command for a language
20
20
  */
21
- function getInterpreter(language) {
21
+ export function getInterpreter(language) {
22
22
  switch (language) {
23
23
  case 'typescript':
24
24
  return { cmd: 'npx', args: ['tsx'], ext: '.ts' };
@@ -31,102 +31,272 @@ function getInterpreter(language) {
31
31
  }
32
32
  }
33
33
  /**
34
- * Execute a tool locally
34
+ * Detect if source defines a `function main(...)` without calling it,
35
+ * and wrap it with the standard argv boilerplate so it actually executes.
35
36
  */
36
- export async function executeTool(toolName, input) {
37
+ export function wrapSourceIfNeeded(source, language) {
38
+ if (language === 'javascript' || language === 'typescript') {
39
+ const definesMain = /\bfunction\s+main\s*\(/.test(source);
40
+ const callsMain = /\bmain\s*\(/.test(source.replace(/function\s+main\s*\(/, ''));
41
+ const hasArgvParse = /process\.argv\[2\]/.test(source);
42
+ if (definesMain && !callsMain && !hasArgvParse) {
43
+ return `${source}
44
+
45
+ // --- Devtopia auto-wrapper: call main() with argv input ---
46
+ const __input = JSON.parse(process.argv[2] || '{}');
47
+ const __result = main(__input);
48
+ if (__result !== undefined) {
49
+ if (__result instanceof Promise) {
50
+ __result.then(r => console.log(JSON.stringify(r))).catch(e => {
51
+ console.error(e.message || e);
52
+ process.exit(1);
53
+ });
54
+ } else {
55
+ console.log(JSON.stringify(__result));
56
+ }
57
+ }
58
+ `;
59
+ }
60
+ }
61
+ if (language === 'python') {
62
+ const definesMain = /\bdef\s+main\s*\(/.test(source);
63
+ const callsMain = /\bmain\s*\(/.test(source.replace(/def\s+main\s*\(/, ''));
64
+ const hasArgvParse = /sys\.argv/.test(source);
65
+ if (definesMain && !callsMain && !hasArgvParse) {
66
+ return `${source}
67
+
68
+ # --- Devtopia auto-wrapper: call main() with argv input ---
69
+ import json, sys
70
+ __input = json.loads(sys.argv[1] if len(sys.argv) > 1 else '{}')
71
+ __result = main(__input)
72
+ if __result is not None:
73
+ print(json.dumps(__result))
74
+ `;
75
+ }
76
+ }
77
+ return source;
78
+ }
79
+ // ─── Composition Runtime Templates ───────────────────────────────────────────
80
+ const JS_RUNTIME = `// devtopia-runtime.js — Composition runtime for Devtopia tools
81
+ // Usage: const { devtopiaRun } = require('./devtopia-runtime');
82
+ // const result = devtopiaRun('tool-name', { input: 'data' });
83
+
84
+ const { execSync } = require('child_process');
85
+
86
+ function devtopiaRun(toolName, input) {
87
+ const inputStr = JSON.stringify(input || {});
88
+ const cliCmd = process.env.DEVTOPIA_CLI || 'npx devtopia';
89
+ try {
90
+ const stdout = execSync(
91
+ \`\${cliCmd} run \${toolName} '\${inputStr.replace(/'/g, "'\\\\''")}' --json --quiet\`,
92
+ { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
93
+ );
94
+ const text = (stdout || '').trim();
95
+ const parsed = text ? JSON.parse(text) : null;
96
+ if (parsed && typeof parsed === 'object' && (parsed.error || parsed.ok === false)) {
97
+ throw new Error(parsed.error || 'Unknown error');
98
+ }
99
+ return parsed;
100
+ } catch (err) {
101
+ const stderr = err && err.stderr ? String(err.stderr).trim() : '';
102
+ const message = stderr || (err && err.message ? err.message : 'Unknown error');
103
+ throw new Error(\`devtopiaRun(\${toolName}) failed: \${message}\`);
104
+ }
105
+ }
106
+
107
+ module.exports = { devtopiaRun };
108
+ `;
109
+ const PY_RUNTIME = `# devtopia_runtime.py — Composition runtime for Devtopia tools
110
+ # Usage: from devtopia_runtime import devtopia_run
111
+ # result = devtopia_run('tool-name', {'input': 'data'})
112
+
113
+ import subprocess, json, os
114
+
115
+ def devtopia_run(tool_name, input_data=None):
116
+ input_str = json.dumps(input_data or {})
117
+ cli_cmd = os.environ.get('DEVTOPIA_CLI', 'npx devtopia')
118
+ try:
119
+ result = subprocess.run(
120
+ (cli_cmd.split() + ['run', tool_name, input_str, '--json', '--quiet']),
121
+ capture_output=True, text=True, timeout=30
122
+ )
123
+ if result.returncode != 0:
124
+ raise RuntimeError(f"devtopia_run({tool_name}) failed: {result.stderr}")
125
+ output = (result.stdout or '').strip()
126
+ parsed = json.loads(output) if output else None
127
+ if isinstance(parsed, dict) and (parsed.get('error') is not None or parsed.get('ok') is False):
128
+ raise RuntimeError(parsed.get('error') or 'Unknown error')
129
+ return parsed
130
+ except subprocess.TimeoutExpired:
131
+ raise RuntimeError(f"devtopia_run({tool_name}) timed out")
132
+ `;
133
+ /**
134
+ * Write composition runtime helper into the working directory
135
+ */
136
+ function writeRuntimeHelper(workDir, language) {
137
+ if (language === 'javascript' || language === 'typescript') {
138
+ writeFileSync(join(workDir, 'devtopia-runtime.js'), JS_RUNTIME);
139
+ }
140
+ if (language === 'python') {
141
+ writeFileSync(join(workDir, 'devtopia_runtime.py'), PY_RUNTIME);
142
+ }
143
+ }
144
+ // ─── Validate tool execution (pre-submit) ────────────────────────────────────
145
+ /**
146
+ * Validate that a tool source actually runs and produces valid JSON output.
147
+ * Runs with {} as input, 10s timeout.
148
+ * Pass: process produces valid JSON stdout (even error JSON like {"error":"..."}).
149
+ * Fail: no stdout, non-JSON stdout with exit 0, or process hangs.
150
+ */
151
+ export async function validateExecution(source, language) {
152
+ const workDir = join(tmpdir(), `devtopia-validate-${randomUUID()}`);
153
+ mkdirSync(workDir, { recursive: true });
154
+ const { cmd, args, ext } = getInterpreter(language);
155
+ const toolFile = join(workDir, `tool${ext}`);
156
+ const wrappedSource = wrapSourceIfNeeded(source, language);
157
+ writeFileSync(toolFile, wrappedSource);
158
+ // Also write runtime helper in case tool uses it
159
+ writeRuntimeHelper(workDir, language);
160
+ return new Promise((resolve) => {
161
+ const proc = spawn(cmd, [...args, toolFile, '{}'], {
162
+ cwd: workDir,
163
+ env: { ...process.env },
164
+ timeout: 10000,
165
+ });
166
+ let stdout = '';
167
+ let stderr = '';
168
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
169
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
170
+ proc.on('close', (code) => {
171
+ try {
172
+ if (existsSync(workDir))
173
+ rmSync(workDir, { recursive: true, force: true });
174
+ }
175
+ catch { }
176
+ const trimmedOut = stdout.trim();
177
+ // No output at all = broken
178
+ if (trimmedOut.length === 0) {
179
+ resolve({ valid: false, stdout: '', stderr: stderr.trim(), exitCode: code });
180
+ return;
181
+ }
182
+ // Check if output is valid JSON
183
+ let isJson = false;
184
+ try {
185
+ JSON.parse(trimmedOut);
186
+ isJson = true;
187
+ }
188
+ catch { }
189
+ // Non-zero exit with JSON stderr/stdout = valid (error handling works)
190
+ if (code !== 0 && isJson) {
191
+ resolve({ valid: true, stdout: trimmedOut, stderr: stderr.trim(), exitCode: code });
192
+ return;
193
+ }
194
+ // Exit 0 with non-JSON output = invalid (must produce JSON)
195
+ if (code === 0 && !isJson) {
196
+ resolve({
197
+ valid: false,
198
+ stdout: trimmedOut,
199
+ stderr: 'Tool produced non-JSON output. All Devtopia tools must output valid JSON.',
200
+ exitCode: code,
201
+ });
202
+ return;
203
+ }
204
+ // Exit 0 with JSON = valid, non-zero with non-JSON stderr = broken
205
+ resolve({ valid: isJson, stdout: trimmedOut, stderr: stderr.trim(), exitCode: code });
206
+ });
207
+ proc.on('error', (err) => {
208
+ try {
209
+ if (existsSync(workDir))
210
+ rmSync(workDir, { recursive: true, force: true });
211
+ }
212
+ catch { }
213
+ resolve({ valid: false, stdout: '', stderr: err.message, exitCode: null });
214
+ });
215
+ });
216
+ }
217
+ // ─── Execute a tool locally ──────────────────────────────────────────────────
218
+ export async function executeTool(toolName, input, options = {}) {
37
219
  const startTime = Date.now();
220
+ const strictJson = options.strictJson === true;
38
221
  try {
39
- // Fetch the tool
40
222
  const tool = await fetchTool(toolName);
41
- // Fetch dependencies
42
- const deps = new Map();
43
- for (const depName of tool.dependencies) {
44
- const dep = await fetchTool(depName);
45
- deps.set(depName, dep);
46
- }
47
- // Create temp directory
48
223
  const workDir = join(tmpdir(), `devtopia-${randomUUID()}`);
49
224
  mkdirSync(workDir, { recursive: true });
50
- // Write tool source
51
225
  const { cmd, args, ext } = getInterpreter(tool.language);
52
226
  const toolFile = join(workDir, `tool${ext}`);
53
- // Inject a simple devtopia runtime for dependencies
54
- let source = tool.source;
55
- if (deps.size > 0) {
56
- const depComment = `// Dependencies: ${[...deps.keys()].join(', ')}\n`;
57
- source = depComment + source;
58
- }
227
+ const source = wrapSourceIfNeeded(tool.source, tool.language);
59
228
  writeFileSync(toolFile, source);
60
- // Execute and wait for completion
229
+ // Always write runtime helper so tools can compose
230
+ writeRuntimeHelper(workDir, tool.language);
61
231
  const result = await new Promise((resolve) => {
62
232
  const inputStr = JSON.stringify(input);
63
233
  const proc = spawn(cmd, [...args, toolFile, inputStr], {
64
234
  cwd: workDir,
65
- env: {
66
- ...process.env,
67
- INPUT: inputStr,
68
- },
69
- timeout: 30000, // 30 second timeout
235
+ env: { ...process.env, INPUT: inputStr },
236
+ timeout: 30000,
70
237
  });
71
238
  let stdout = '';
72
239
  let stderr = '';
73
- proc.stdout.on('data', (data) => {
74
- stdout += data.toString();
75
- });
76
- proc.stderr.on('data', (data) => {
77
- stderr += data.toString();
78
- });
240
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
241
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
79
242
  proc.on('close', (code) => {
80
243
  const durationMs = Date.now() - startTime;
244
+ const trimmedOut = stdout.trim();
245
+ const trimmedErr = stderr.trim();
246
+ const tryParseJson = (text) => {
247
+ if (!text)
248
+ return null;
249
+ try {
250
+ return JSON.parse(text);
251
+ }
252
+ catch {
253
+ return null;
254
+ }
255
+ };
81
256
  if (code !== 0) {
82
- resolve({
83
- success: false,
84
- output: null,
85
- error: stderr || `Process exited with code ${code}`,
86
- durationMs,
87
- });
257
+ if (strictJson) {
258
+ const parsed = tryParseJson(trimmedOut) ?? tryParseJson(trimmedErr);
259
+ resolve({
260
+ success: false,
261
+ output: parsed,
262
+ error: (parsed && typeof parsed === 'object' && parsed.error) ? String(parsed.error) : (trimmedErr || `Process exited with code ${code}`),
263
+ durationMs,
264
+ });
265
+ return;
266
+ }
267
+ resolve({ success: false, output: null, error: trimmedErr || `Process exited with code ${code}`, durationMs });
88
268
  return;
89
269
  }
90
- try {
91
- // Parse JSON output
92
- const output = JSON.parse(stdout.trim());
93
- resolve({
94
- success: true,
95
- output,
96
- durationMs,
97
- });
270
+ if (trimmedOut.length === 0) {
271
+ if (strictJson) {
272
+ resolve({ success: false, output: null, error: 'Tool produced no output', durationMs });
273
+ }
274
+ else {
275
+ resolve({ success: true, output: '', durationMs });
276
+ }
277
+ return;
98
278
  }
99
- catch {
100
- // Return raw output if not JSON
101
- resolve({
102
- success: true,
103
- output: stdout.trim(),
104
- durationMs,
105
- });
279
+ const parsed = tryParseJson(trimmedOut);
280
+ if (parsed === null) {
281
+ if (strictJson) {
282
+ resolve({ success: false, output: null, error: 'Tool produced non-JSON output', durationMs });
283
+ }
284
+ else {
285
+ resolve({ success: true, output: trimmedOut, durationMs });
286
+ }
287
+ return;
106
288
  }
289
+ resolve({ success: true, output: parsed, durationMs });
107
290
  });
108
291
  proc.on('error', (err) => {
109
- resolve({
110
- success: false,
111
- output: null,
112
- error: err.message,
113
- durationMs: Date.now() - startTime,
114
- });
292
+ resolve({ success: false, output: null, error: err.message, durationMs: Date.now() - startTime });
115
293
  });
116
294
  });
117
- // Cleanup temp directory after execution completes
118
- if (existsSync(workDir)) {
295
+ if (existsSync(workDir))
119
296
  rmSync(workDir, { recursive: true, force: true });
120
- }
121
297
  return result;
122
298
  }
123
299
  catch (err) {
124
- const durationMs = Date.now() - startTime;
125
- return {
126
- success: false,
127
- output: null,
128
- error: err.message,
129
- durationMs,
130
- };
300
+ return { success: false, output: null, error: err.message, durationMs: Date.now() - startTime };
131
301
  }
132
302
  }
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ import { run } from './commands/run.js';
10
10
  import { categories } from './commands/categories.js';
11
11
  import { updateLineage } from './commands/lineage.js';
12
12
  import { showDocs } from './commands/docs.js';
13
+ import { search } from './commands/search.js';
14
+ import { compose } from './commands/compose.js';
13
15
  const program = new Command();
14
16
  program
15
17
  .name('devtopia')
16
18
  .description('CLI for Devtopia - AI agent tool registry')
17
- .version('1.9.0')
19
+ .version('2.0.0')
18
20
  .addHelpText('before', `
19
21
  🐝 Devtopia — AI Agent Tool Registry
20
22
 
@@ -53,7 +55,7 @@ KEY PRINCIPLES
53
55
  program
54
56
  .command('start')
55
57
  .description('Learn about Devtopia - start here if you\'re new!')
56
- .action(start);
58
+ .action(async () => { await start(); });
57
59
  program
58
60
  .command('docs [name]')
59
61
  .description('View Devtopia documentation (agents, contributing, cli, tool-format, faq)')
@@ -66,7 +68,7 @@ Examples:
66
68
  $ devtopia docs tool-format # Show tool format specification
67
69
  $ devtopia docs faq # Show FAQ
68
70
 
69
- These docs match the GitHub repository: https://github.com/Devtopia/Devtopia
71
+ These docs match the GitHub repository: https://github.com/DevtopiaHub/Devtopia
70
72
  `)
71
73
  .action((name) => showDocs(name));
72
74
  // =============================================================================
@@ -97,6 +99,11 @@ program
97
99
  .command('categories')
98
100
  .description('List all available categories')
99
101
  .action(categories);
102
+ program
103
+ .command('search <query>')
104
+ .description('Search tools by name or description')
105
+ .option('-l, --limit <n>', 'Max results (default: 20)', '20')
106
+ .action((query, options) => search(query, options));
100
107
  program
101
108
  .command('cat <tool>')
102
109
  .description('View tool README and source code')
@@ -114,6 +121,8 @@ program
114
121
  .option('-c, --category <id>', 'Category (auto-detected if not specified)')
115
122
  .option('--deps <deps>', 'Comma-separated dependencies')
116
123
  .option('--builds-on <tools>', 'Comma-separated parent tools this extends/composes (RECOMMENDED: shows lineage)')
124
+ .option('--skip-validation', 'Skip pre-submit execution validation')
125
+ .option('--schema <path>', 'Path to JSON file with input/output schemas')
117
126
  .addHelpText('after', `
118
127
  Examples:
119
128
  $ devtopia submit my-tool ./my-tool.js -d "Does something useful"
@@ -124,6 +133,18 @@ Examples:
124
133
  This creates visible dependency chains and helps the ecosystem grow.
125
134
  `)
126
135
  .action(submit);
136
+ program
137
+ .command('compose <name>')
138
+ .description('Generate a composed tool scaffold pre-wired to call parent tools')
139
+ .requiredOption('--uses <tools>', 'Comma-separated parent tools to compose')
140
+ .addHelpText('after', `
141
+ Example:
142
+ $ devtopia compose my-pipeline --uses json-validate,json-flatten,csv-writer
143
+
144
+ Generates my-pipeline.js and my-pipeline.md with devtopiaRun() calls
145
+ pre-wired for each parent tool. Edit the TODOs, test, then submit.
146
+ `)
147
+ .action((name, options) => compose(name, options));
127
148
  program
128
149
  .command('lineage <tool> [builds-on]')
129
150
  .description('Update tool lineage - specify which tools this builds on')
@@ -143,7 +164,9 @@ Examples:
143
164
  program
144
165
  .command('run <tool> [input]')
145
166
  .description('Run a tool locally (fetches source, executes on your machine)')
146
- .action(run);
167
+ .option('--json', 'Output JSON only (no logs). Errors return {"error":"..."}')
168
+ .option('--quiet', 'Suppress status logs (prints only tool output)')
169
+ .action((tool, input, options) => run(tool, input, options));
147
170
  // =============================================================================
148
171
  // Parse
149
172
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devtopia",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI for Devtopia - AI agent tool registry",
5
5
  "type": "module",
6
6
  "bin": {