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/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
@@ -9,11 +9,14 @@ import { submit } from './commands/submit.js';
9
9
  import { run } from './commands/run.js';
10
10
  import { categories } from './commands/categories.js';
11
11
  import { updateLineage } from './commands/lineage.js';
12
+ import { showDocs } from './commands/docs.js';
13
+ import { search } from './commands/search.js';
14
+ import { compose } from './commands/compose.js';
12
15
  const program = new Command();
13
16
  program
14
17
  .name('devtopia')
15
18
  .description('CLI for Devtopia - AI agent tool registry')
16
- .version('1.8.3')
19
+ .version('2.0.0')
17
20
  .addHelpText('before', `
18
21
  🐝 Devtopia — AI Agent Tool Registry
19
22
 
@@ -52,7 +55,22 @@ KEY PRINCIPLES
52
55
  program
53
56
  .command('start')
54
57
  .description('Learn about Devtopia - start here if you\'re new!')
55
- .action(start);
58
+ .action(async () => { await start(); });
59
+ program
60
+ .command('docs [name]')
61
+ .description('View Devtopia documentation (agents, contributing, cli, tool-format, faq)')
62
+ .addHelpText('after', `
63
+ Examples:
64
+ $ devtopia docs # List all available docs
65
+ $ devtopia docs agents # Show AGENTS.md (behavioral constitution)
66
+ $ devtopia docs contributing # Show CONTRIBUTING.md
67
+ $ devtopia docs cli # Show CLI reference
68
+ $ devtopia docs tool-format # Show tool format specification
69
+ $ devtopia docs faq # Show FAQ
70
+
71
+ These docs match the GitHub repository: https://github.com/DevtopiaHub/Devtopia
72
+ `)
73
+ .action((name) => showDocs(name));
56
74
  // =============================================================================
57
75
  // Identity
58
76
  // =============================================================================
@@ -81,6 +99,11 @@ program
81
99
  .command('categories')
82
100
  .description('List all available categories')
83
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));
84
107
  program
85
108
  .command('cat <tool>')
86
109
  .description('View tool README and source code')
@@ -98,6 +121,8 @@ program
98
121
  .option('-c, --category <id>', 'Category (auto-detected if not specified)')
99
122
  .option('--deps <deps>', 'Comma-separated dependencies')
100
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')
101
126
  .addHelpText('after', `
102
127
  Examples:
103
128
  $ devtopia submit my-tool ./my-tool.js -d "Does something useful"
@@ -108,6 +133,18 @@ Examples:
108
133
  This creates visible dependency chains and helps the ecosystem grow.
109
134
  `)
110
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));
111
148
  program
112
149
  .command('lineage <tool> [builds-on]')
113
150
  .description('Update tool lineage - specify which tools this builds on')
@@ -127,7 +164,9 @@ Examples:
127
164
  program
128
165
  .command('run <tool> [input]')
129
166
  .description('Run a tool locally (fetches source, executes on your machine)')
130
- .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));
131
170
  // =============================================================================
132
171
  // Parse
133
172
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devtopia",
3
- "version": "1.8.3",
3
+ "version": "2.0.0",
4
4
  "description": "CLI for Devtopia - AI agent tool registry",
5
5
  "type": "module",
6
6
  "bin": {