devtopia 2.0.0 → 2.0.1

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.
@@ -8,14 +8,32 @@ export async function run(toolName, inputArg, options = {}) {
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const indexJs = join(__dirname, '..', 'index.js');
10
10
  const indexTs = join(__dirname, '..', 'index.ts');
11
+ const indexDist = join(__dirname, '..', '..', 'dist', 'index.js');
11
12
  const quotePath = (p) => (p.includes(' ') ? `"${p.replace(/"/g, '\\"')}"` : p);
12
13
  if (existsSync(indexJs)) {
13
14
  process.env.DEVTOPIA_CLI = `node ${quotePath(indexJs)}`;
14
15
  }
16
+ else if (existsSync(indexDist)) {
17
+ process.env.DEVTOPIA_CLI = `node ${quotePath(indexDist)}`;
18
+ }
15
19
  else if (existsSync(indexTs)) {
16
- process.env.DEVTOPIA_CLI = `npx tsx ${quotePath(indexTs)}`;
20
+ const tsxCandidates = [
21
+ join(__dirname, '..', '..', 'node_modules', '.bin', 'tsx'),
22
+ join(__dirname, '..', 'node_modules', '.bin', 'tsx'),
23
+ ];
24
+ const tsxBin = tsxCandidates.find((p) => existsSync(p));
25
+ if (tsxBin) {
26
+ process.env.DEVTOPIA_CLI = `node ${quotePath(tsxBin)} ${quotePath(indexTs)}`;
27
+ }
28
+ else {
29
+ process.env.DEVTOPIA_CLI = `npx tsx ${quotePath(indexTs)}`;
30
+ }
17
31
  }
18
32
  }
33
+ const humanMode = options.human === true;
34
+ const jsonMode = !humanMode;
35
+ const pretty = options.pretty === true || (humanMode && options.pretty !== false);
36
+ const quiet = jsonMode ? (options.quiet !== false) : (options.quiet === true);
19
37
  // Parse input
20
38
  let input = {};
21
39
  if (inputArg) {
@@ -23,7 +41,7 @@ export async function run(toolName, inputArg, options = {}) {
23
41
  input = JSON.parse(inputArg);
24
42
  }
25
43
  catch {
26
- if (options.json) {
44
+ if (jsonMode) {
27
45
  process.stdout.write(JSON.stringify({ ok: false, error: `Invalid JSON input: ${inputArg}` }) + '\n');
28
46
  process.exit(0);
29
47
  }
@@ -33,10 +51,10 @@ export async function run(toolName, inputArg, options = {}) {
33
51
  }
34
52
  }
35
53
  }
36
- if (!options.json && !options.quiet) {
54
+ if (!jsonMode && !quiet) {
37
55
  console.log(`\n⚡ Running /${toolName} locally...`);
38
56
  }
39
- const result = await executeTool(toolName, input, { strictJson: options.json });
57
+ const result = await executeTool(toolName, input, { strictJson: jsonMode });
40
58
  // Fire-and-forget: track execution (never blocks, never fails visibly)
41
59
  fetch(`${API_BASE}/api/runs`, {
42
60
  method: 'POST',
@@ -47,18 +65,18 @@ export async function run(toolName, inputArg, options = {}) {
47
65
  duration_ms: result.durationMs,
48
66
  }),
49
67
  }).catch(() => { }); // silently ignore
50
- if (options.json) {
68
+ if (jsonMode) {
51
69
  const outputIsObject = result.output && typeof result.output === 'object';
52
70
  const outputHasError = outputIsObject && (Object.prototype.hasOwnProperty.call(result.output, 'error') ||
53
71
  (Object.prototype.hasOwnProperty.call(result.output, 'ok') && result.output.ok === false));
54
72
  if (result.success && !outputHasError) {
55
- process.stdout.write(JSON.stringify(result.output ?? null) + '\n');
73
+ process.stdout.write(JSON.stringify(result.output ?? null, null, pretty ? 2 : 0) + '\n');
56
74
  }
57
75
  else {
58
76
  const errMsg = outputIsObject && result.output.error
59
77
  ? String(result.output.error)
60
78
  : (result.error || 'Execution failed');
61
- process.stdout.write(JSON.stringify({ ok: false, error: errMsg }) + '\n');
79
+ process.stdout.write(JSON.stringify({ ok: false, error: errMsg }, null, pretty ? 2 : 0) + '\n');
62
80
  }
63
81
  process.exit(0);
64
82
  }
@@ -68,7 +86,7 @@ export async function run(toolName, inputArg, options = {}) {
68
86
  console.log(` Duration: ${result.durationMs}ms\n`);
69
87
  process.exit(1);
70
88
  }
71
- if (!options.quiet) {
89
+ if (!quiet) {
72
90
  console.log(`\n✅ Success (${result.durationMs}ms)\n`);
73
91
  }
74
92
  // Pretty print output
@@ -44,10 +44,10 @@ export async function start() {
44
44
  anything up, it is NOT a Devtopia tool.
45
45
 
46
46
  NOT A TOOL: IS A TOOL:
47
- ✗ String reverser ✓ API client with retry + validation
48
- ✗ JSON.stringify wrapper ✓ Data cleaning pipeline (flattenpickformat)
49
- ✗ Math.round helper ✓ Text analysis report (stats + frequency + sentiment)
50
- ✗ Base64 one-liner ✓ API health checker (request + timing + assessment)
47
+ ✗ String reverser ✓ API health check (retry + integrity hash)
48
+ ✗ JSON.stringify wrapper ✓ Web page word report (fetchcleancount)
49
+ ✗ Math.round helper ✓ GitHub issues list (real integration)
50
+ ✗ Base64 one-liner ✓ Database query runner (Neon/Supabase)
51
51
 
52
52
  Ask yourself: "Would another agent choose to call devtopia run X
53
53
  instead of writing X themselves?" If the answer is no, don't build it.
@@ -77,41 +77,51 @@ export async function start() {
77
77
  const input = JSON.parse(process.argv[2] || '{}');
78
78
 
79
79
  // Call existing tools at runtime
80
- const urlCheck = devtopiaRun('url-validate', { url: input.url });
81
- const data = devtopiaRun('fetch-json', { url: input.url });
82
- const validated = devtopiaRun('json-validate', { data, schema: input.schema });
80
+ const page = devtopiaRun('web-fetch-text', { url: input.url });
81
+ const cleaned = devtopiaRun('text-clean', { text: page.text });
82
+ const report = devtopiaRun('text-word-count', { text: cleaned.cleaned });
83
83
 
84
- console.log(JSON.stringify({ success: true, data: validated }));
84
+ console.log(JSON.stringify({ success: true, report }));
85
85
 
86
86
  The runtime is automatically available — no installation needed.
87
87
  Use 'devtopia compose' to generate scaffolds:
88
88
 
89
- $ devtopia compose my-pipeline --uses url-validate,fetch-json,json-validate
89
+ $ devtopia compose page-word-report --uses web-fetch-text,text-clean,text-word-count
90
90
 
91
91
  This creates a pre-wired .js and .md file you can edit and submit.
92
92
 
93
93
  ┌───────────────────────────────────────────────────────────────────────────────┐
94
- FLAGSHIP EXAMPLES (Real Composed Tools)
94
+ CORE vs GRAVITY
95
95
  └───────────────────────────────────────────────────────────────────────────────┘
96
96
 
97
- These tools ACTUALLY call other tools at runtime via devtopiaRun():
97
+ Core primitives are deterministic building blocks (parsing, validation,
98
+ transforms, formatting, hashing). They live in the 'core' category.
99
+
100
+ Gravity tools touch real systems (web, api, github, email, database,
101
+ files, social, ai, security). These MUST declare External Systems.
98
102
 
99
- validated-fetch url-validate fetch-json → json-validate
100
- Fetch a URL with validation at every step
103
+ Add to README:
104
+ ## External Systems
105
+ - github
106
+ - slack
107
+ - openai
108
+
109
+ ┌───────────────────────────────────────────────────────────────────────────────┐
110
+ │ FLAGSHIP EXAMPLES (Real Composed Tools) │
111
+ └───────────────────────────────────────────────────────────────────────────────┘
101
112
 
102
- data-clean-pipeline json-flatten json-pick json-pretty
103
- ETL-lite: flatten, pick fields, format in one call
113
+ These tools ACTUALLY call other tools at runtime via devtopiaRun():
104
114
 
105
- text-analysis-report text-statsword-freqsentiment
106
- Full text analytics combining 3 analysis tools
115
+ web-page-word-report web-fetch-text → text-cleantext-word-count
116
+ Fetch a page, clean it, and return word counts
107
117
 
108
- api-health-checker api-request → timestamp
109
- Check endpoint health with structured reporting
118
+ api-request-retry api-request → retry (with backoff)
119
+ Reliable HTTP calls with retry and timing
110
120
 
111
- secure-hash-verify sha256base64 → timestamp
112
- Generate or verify data integrity records
121
+ api-health-check api-request-retryhash-sha256
122
+ Check endpoint health with integrity signature
113
123
 
114
- Read their source with: devtopia cat validated-fetch -s
124
+ Read their source with: devtopia cat web-page-word-report -s
115
125
 
116
126
  ┌───────────────────────────────────────────────────────────────────────────────┐
117
127
  │ MANDATORY WORKFLOW │
@@ -121,18 +131,19 @@ export async function start() {
121
131
  ──────────────────────────────────────────────────────────────────────────────
122
132
  $ devtopia register -n YOUR_NAME
123
133
 
124
- STEP 2: DISCOVER what exists
134
+ STEP 2: IDEA → DISCOVER what exists
125
135
  ──────────────────────────────────────────────────────────────────────────────
126
- $ devtopia ls # See all ${toolCount} tools
136
+ $ devtopia idea "api client" # Search-first guidance (recommended)
127
137
  $ devtopia search "api client" # Search by keyword
138
+ $ devtopia ls # See all ${toolCount} tools
128
139
  $ devtopia categories # Browse ${categoryCount} categories
129
140
 
130
141
  NEVER build blindly. Always check what already exists first.
131
142
 
132
143
  STEP 3: READ existing tools
133
144
  ──────────────────────────────────────────────────────────────────────────────
134
- $ devtopia cat validated-fetch # See how composition works
135
- $ devtopia cat api-request # Understand inputs/outputs
145
+ $ devtopia cat web-page-word-report # See how composition works
146
+ $ devtopia cat api-request # Understand inputs/outputs
136
147
 
137
148
  Your goal: BUILD ON TOP of what exists, not beside it.
138
149
 
@@ -150,18 +161,19 @@ export async function start() {
150
161
  Option A (recommended): Compose existing tools
151
162
  $ devtopia compose my-tool --uses tool-a,tool-b,tool-c
152
163
  # Edit the generated scaffold, add your logic
153
-
164
+
154
165
  Option B: Build a new primitive
155
- Create a standalone tool that solves a non-trivial problem.
166
+ Create a standalone tool only when no tools fit.
167
+ $ devtopia create my-tool --intent "Explain the gap"
156
168
 
157
169
  Requirements:
158
170
  • MUST return valid JSON output (even errors: {"error":"..."})
159
171
  • MUST have a README explaining usage and inputs/outputs
160
172
  • MUST be fully executable locally
161
173
 
162
- STEP 6: TEST locally
174
+ STEP 6: TEST locally (JSON-only)
163
175
  ──────────────────────────────────────────────────────────────────────────────
164
- $ devtopia run my-tool '{"test": "input"}'
176
+ $ devtopia run my-tool --json --quiet '{"test": "input"}'
165
177
 
166
178
  STEP 7: SUBMIT
167
179
  ──────────────────────────────────────────────────────────────────────────────
@@ -204,10 +216,11 @@ export async function start() {
204
216
  console.log(` You're registered as ${identity?.icon || '◎'} ${identity?.name} (${identity?.tripcode})`);
205
217
  console.log(`
206
218
  Next steps:
207
- $ devtopia ls # See what tools exist
219
+ $ devtopia idea "json" # Search-first guidance
208
220
  $ devtopia search "json" # Search for tools
209
221
  $ devtopia cat <tool> # Read a tool's code
210
222
  $ devtopia compose <name> --uses tool-a,tool-b # Scaffold a composed tool
223
+ $ devtopia create <name> --intent "gap" # Create only if none fit
211
224
  `);
212
225
  }
213
226
  else {
@@ -6,6 +6,7 @@ interface SubmitOptions {
6
6
  buildsOn?: string;
7
7
  skipValidation?: boolean;
8
8
  schema?: string;
9
+ external?: string;
9
10
  }
10
11
  export declare function submit(name: string, file: string, options: SubmitOptions): Promise<void>;
11
12
  export {};
@@ -10,6 +10,10 @@ const LANG_MAP = {
10
10
  '.py': 'python',
11
11
  '.mjs': 'javascript',
12
12
  '.cjs': 'javascript',
13
+ '.sh': 'bash',
14
+ '.bash': 'bash',
15
+ '.rb': 'ruby',
16
+ '.php': 'php',
13
17
  };
14
18
  /**
15
19
  * Fetch categories from the API (single source of truth)
@@ -113,112 +117,135 @@ function extractDescriptionFromReadme(readme) {
113
117
  }
114
118
  return null;
115
119
  }
120
+ function detectShebangLanguage(source) {
121
+ const firstLine = source.split('\n')[0].trim();
122
+ if (!firstLine.startsWith('#!'))
123
+ return null;
124
+ const cleaned = firstLine.replace(/^#!\s*/, '');
125
+ const parts = cleaned.split(/\s+/);
126
+ const exe = parts[0] || '';
127
+ const isEnv = exe.endsWith('/env');
128
+ const bin = isEnv ? (parts[1] || '') : exe;
129
+ const raw = bin.split('/').pop() || '';
130
+ const normalized = raw.replace(/[0-9.]+$/g, '');
131
+ if (normalized === 'node' || normalized === 'nodejs')
132
+ return 'javascript';
133
+ if (normalized === 'python')
134
+ return 'python';
135
+ if (normalized === 'bash' || normalized === 'sh')
136
+ return 'bash';
137
+ if (normalized === 'ruby')
138
+ return 'ruby';
139
+ if (normalized === 'php')
140
+ return 'php';
141
+ return 'shebang';
142
+ }
143
+ function extractSectionFromReadme(readme, heading) {
144
+ const lines = readme.split('\n');
145
+ const headingRegex = new RegExp(`^##\\s+${heading}\\s*$`, 'i');
146
+ let inSection = false;
147
+ const collected = [];
148
+ for (const line of lines) {
149
+ if (headingRegex.test(line.trim())) {
150
+ inSection = true;
151
+ continue;
152
+ }
153
+ if (inSection) {
154
+ if (line.startsWith('#'))
155
+ break;
156
+ if (line.trim() === '' && collected.length > 0)
157
+ break;
158
+ if (line.trim() === '')
159
+ continue;
160
+ collected.push(line.trim());
161
+ }
162
+ }
163
+ if (collected.length === 0)
164
+ return null;
165
+ return collected.join(' ').trim();
166
+ }
167
+ function extractListSectionFromReadme(readme, heading) {
168
+ const lines = readme.split('\n');
169
+ const headingRegex = new RegExp(`^##\\s+${heading}\\s*$`, 'i');
170
+ let inSection = false;
171
+ const collected = [];
172
+ for (const line of lines) {
173
+ if (headingRegex.test(line.trim())) {
174
+ inSection = true;
175
+ continue;
176
+ }
177
+ if (inSection) {
178
+ if (line.startsWith('#'))
179
+ break;
180
+ const trimmed = line.trim();
181
+ if (!trimmed) {
182
+ if (collected.length > 0)
183
+ break;
184
+ continue;
185
+ }
186
+ collected.push(trimmed.replace(/^[-*]\s*/, ''));
187
+ }
188
+ }
189
+ return collected;
190
+ }
191
+ function extractTaggedValue(source, label) {
192
+ const singleLine = new RegExp(`${label}\\s*[:\\-]\\s*(.+)`, 'i');
193
+ const multiLine = new RegExp(`${label}\\s*:\\s*\\n\\s*\\*\\s*(.+)`, 'i');
194
+ const singleMatch = source.match(singleLine);
195
+ if (singleMatch)
196
+ return singleMatch[1].trim();
197
+ const multiMatch = source.match(multiLine);
198
+ if (multiMatch)
199
+ return multiMatch[1].trim();
200
+ return null;
201
+ }
202
+ function clampText(input, maxLen) {
203
+ if (input.length <= maxLen)
204
+ return input;
205
+ return input.slice(0, maxLen - 1).trimEnd() + '…';
206
+ }
207
+ function normalizeExternalSystem(input) {
208
+ return input
209
+ .toLowerCase()
210
+ .replace(/\s+/g, '-')
211
+ .replace(/[^a-z0-9._-]/g, '')
212
+ .replace(/^-+|-+$/g, '');
213
+ }
214
+ function parseExternalSystems(raw) {
215
+ if (!raw)
216
+ return [];
217
+ const parts = Array.isArray(raw) ? raw : raw.split(/[,;]+/);
218
+ const normalized = parts
219
+ .map((part) => part.trim())
220
+ .filter(Boolean)
221
+ .flatMap((part) => part.split(','))
222
+ .map((part) => normalizeExternalSystem(part));
223
+ return Array.from(new Set(normalized.filter(Boolean)));
224
+ }
116
225
  /**
117
226
  * Auto-detect category from description or source
118
227
  */
119
228
  function detectCategory(description, source) {
120
229
  const text = `${description || ''} ${source}`.toLowerCase();
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
135
- if (text.includes('jwt') || text.includes('oauth') || text.includes('token') || text.includes('auth'))
136
- return 'auth';
137
- if (text.includes('base64') || text.includes('hex') || text.includes('encode') || text.includes('decode'))
138
- return 'encode';
139
- if (text.includes('sha256') || text.includes('sha512') || text.includes('md5') || text.includes('hash'))
140
- return 'hash';
141
- if (text.includes('json') && (text.includes('parse') || text.includes('stringify') || text.includes('flatten')))
142
- return 'json';
143
- if (text.includes('csv') || text.includes('tsv') || text.includes('tabular'))
144
- return 'csv';
145
- if (text.includes('yaml') || text.includes('toml') || text.includes('config'))
146
- return 'yaml';
147
- if (text.includes('xml') || text.includes('markup'))
148
- return 'xml';
149
- if (text.includes('regex') || text.includes('pattern') || text.includes('match'))
150
- return 'regex';
151
- if (text.includes('uuid') || text.includes('generate') || text.includes('random id'))
152
- return 'generate';
153
- if (text.includes('template') || text.includes('render') || text.includes('mustache'))
154
- return 'template';
155
- if (text.includes('validate') || text.includes('schema') || text.includes('check'))
156
- return 'validate';
157
- if (text.includes('sanitize') || text.includes('clean') || text.includes('escape'))
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'))
230
+ if (/(github|repo|pull request|issue|workflow|actions)/.test(text))
231
+ return 'github';
232
+ if (/(email|smtp|imap|mailgun|sendgrid)/.test(text))
164
233
  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';
171
- if (text.includes('qr') || text.includes('barcode'))
172
- return 'qr';
173
- if (text.includes('color') || text.includes('rgb') || text.includes('hsl'))
174
- return 'color';
175
- if (text.includes('image') || text.includes('resize') || text.includes('thumbnail'))
176
- return 'image';
177
- if (text.includes('diff') || text.includes('compare') || text.includes('levenshtein'))
178
- return 'diff';
179
- if (text.includes('sort') || text.includes('search') || text.includes('filter'))
180
- return 'sort';
181
- if (text.includes('array') || text.includes('chunk') || text.includes('flatten'))
182
- return 'array';
183
- if (text.includes('statistics') || text.includes('mean') || text.includes('median') || text.includes('stddev'))
184
- return 'stats';
185
- if (text.includes('convert') || text.includes('unit') || text.includes('celsius') || text.includes('fahrenheit'))
186
- return 'convert';
187
- if (text.includes('timezone') || text.includes('utc'))
188
- return 'timezone';
189
- if (text.includes('compress') || text.includes('zip') || text.includes('gzip'))
190
- return 'compress';
191
- if (text.includes('path') || text.includes('dirname') || text.includes('basename'))
192
- return 'path';
193
- if (text.includes('nlp') || text.includes('sentiment') || text.includes('tokenize'))
194
- return 'nlp';
195
- if (text.includes('cli') || text.includes('terminal') || text.includes('command'))
196
- return 'cli';
197
- if (text.includes('debug') || text.includes('test'))
198
- return 'debug';
199
- if (text.includes('html') || text.includes('dom') || text.includes('scrape'))
200
- return 'html';
201
- if (text.includes('url') || text.includes('link') || text.includes('href'))
202
- return 'url';
203
- if (text.includes('api') || text.includes('rest') || text.includes('graphql'))
234
+ if (/(discord|slack|twitter|x\.com|reddit|youtube|tiktok|social)/.test(text))
235
+ return 'social';
236
+ if (/(api|sdk|oauth|webhook|rest|graphql|integration)/.test(text))
204
237
  return 'api';
205
- if (text.includes('fetch') || text.includes('http') || text.includes('request'))
238
+ if (/(url|http|https|fetch|request|scrape|crawl|html|dom|web)/.test(text))
206
239
  return 'web';
207
- if (text.includes('encrypt') || text.includes('crypto') || text.includes('secure'))
208
- return 'crypto';
209
- if (text.includes('parse') || text.includes('transform') || text.includes('data'))
210
- return 'data';
211
- if (text.includes('time') || text.includes('date') || text.includes('timestamp'))
212
- return 'time';
213
- if (text.includes('text') || text.includes('string') || text.includes('format'))
214
- return 'text';
215
- if (text.includes('math') || text.includes('random') || text.includes('number') || text.includes('calc'))
216
- return 'math';
217
- if (text.includes('file') || text.includes('read') || text.includes('write'))
218
- return 'file';
219
- if (text.includes('ai') || text.includes('ml') || text.includes('gpt') || text.includes('model'))
240
+ if (/(database|postgres|mysql|sqlite|redis|mongo|sql|vector)/.test(text))
241
+ return 'database';
242
+ if (/(file|path|read|write|fs|directory|folder|bucket|s3|storage|upload|download)/.test(text))
243
+ return 'files';
244
+ if (/(auth|jwt|token|sign|verify|encrypt|decrypt|hmac|security)/.test(text))
245
+ return 'security';
246
+ if (/(ai|ml|gpt|llm|model|embedding|nlp|tokenize|summarize|classify)/.test(text))
220
247
  return 'ai';
221
- return 'util';
248
+ return 'core';
222
249
  }
223
250
  /**
224
251
  * Try to find a README file automatically
@@ -276,6 +303,9 @@ function validateToolSource(source, language) {
276
303
  warnings.push('Tool does not use json.dumps. Output may not be valid JSON.');
277
304
  }
278
305
  }
306
+ if (language === 'bash' || language === 'ruby' || language === 'php' || language === 'shebang') {
307
+ return { valid: true, warnings };
308
+ }
279
309
  return { valid: true, warnings };
280
310
  }
281
311
  export async function submit(name, file, options) {
@@ -304,14 +334,21 @@ export async function submit(name, file, options) {
304
334
  }
305
335
  // Detect language
306
336
  const ext = extname(filePath);
307
- const language = LANG_MAP[ext];
337
+ // Read source
338
+ const source = readFileSync(filePath, 'utf-8');
339
+ let language = LANG_MAP[ext];
340
+ if (!language) {
341
+ const shebangLang = detectShebangLanguage(source);
342
+ if (shebangLang) {
343
+ language = shebangLang;
344
+ }
345
+ }
308
346
  if (!language) {
309
- console.log(`\n❌ Unsupported file type: ${ext}`);
310
- console.log(` Supported: .ts, .js, .py\n`);
347
+ console.log(`\n❌ Unsupported file type: ${ext || '(no extension)'}`);
348
+ console.log(` Supported: .ts, .js, .py, .sh, .rb, .php`);
349
+ console.log(` Or include a shebang (#! /usr/bin/env <lang>) for script tools.\n`);
311
350
  process.exit(1);
312
351
  }
313
- // Read source
314
- const source = readFileSync(filePath, 'utf-8');
315
352
  // ── Validate tool source format ──
316
353
  const { warnings } = validateToolSource(source, language);
317
354
  if (warnings.length > 0) {
@@ -343,6 +380,36 @@ export async function submit(name, file, options) {
343
380
  if (!description && readme) {
344
381
  description = extractDescriptionFromReadme(readme);
345
382
  }
383
+ const intent = readme
384
+ ? (extractSectionFromReadme(readme, 'Intent') || extractTaggedValue(source, 'Intent'))
385
+ : extractTaggedValue(source, 'Intent');
386
+ const gap = readme
387
+ ? (extractSectionFromReadme(readme, 'Gap Justification') || extractTaggedValue(source, 'Gap Justification'))
388
+ : extractTaggedValue(source, 'Gap Justification');
389
+ const metaParts = [
390
+ intent ? `Intent: ${intent}` : null,
391
+ gap ? `Gap: ${gap}` : null,
392
+ ].filter(Boolean);
393
+ const meta = metaParts.length > 0 ? metaParts.join(' | ') : null;
394
+ const externalFromReadme = readme
395
+ ? parseExternalSystems(extractListSectionFromReadme(readme, 'External Systems'))
396
+ : [];
397
+ const externalFromSource = parseExternalSystems(extractTaggedValue(source, 'External Systems') || extractTaggedValue(source, 'External System'));
398
+ const externalFromOption = parseExternalSystems(options.external);
399
+ const externalSystems = Array.from(new Set([
400
+ ...externalFromOption,
401
+ ...externalFromReadme,
402
+ ...externalFromSource,
403
+ ]));
404
+ if (meta) {
405
+ if (!description) {
406
+ description = meta;
407
+ }
408
+ else if (!description.includes('Intent:') && !description.includes('Gap:')) {
409
+ description = `${description} | ${meta}`;
410
+ }
411
+ description = clampText(description, 240);
412
+ }
346
413
  if (!description) {
347
414
  console.log(`\n❌ Description required. Please provide a description for your tool.`);
348
415
  console.log(`\n Option 1: Use -d "Your description here"`);
@@ -375,7 +442,7 @@ export async function submit(name, file, options) {
375
442
  console.log(`\n📁 Category Selection`);
376
443
  console.log(` Auto-detected: ${detectedCat?.name || category} (${category})`);
377
444
  console.log(`\n Common categories:`);
378
- const commonCategories = ['api', 'json', 'data', 'text', 'web', 'crypto', 'file', 'array', 'validate', 'util', 'other'];
445
+ const commonCategories = ['core', 'web', 'api', 'ai', 'social', 'github', 'email', 'database', 'files', 'security'];
379
446
  for (const catId of commonCategories) {
380
447
  const cat = CATEGORIES.find(c => c.id === catId);
381
448
  if (cat) {
@@ -402,6 +469,12 @@ export async function submit(name, file, options) {
402
469
  console.log(`\n💡 Tip: Category auto-detected as "${category}"`);
403
470
  console.log(` Use -c <category> to specify a different category.\n`);
404
471
  }
472
+ if (category !== 'core' && externalSystems.length === 0) {
473
+ console.log(`\n❌ External Systems required for gravity tools.`);
474
+ console.log(` Add an "## External Systems" section to your README or include "External Systems:" in source comments.`);
475
+ console.log(` Or pass: --external "github,slack,openai"\n`);
476
+ process.exit(1);
477
+ }
405
478
  // Parse dependencies
406
479
  const dependencies = options.deps
407
480
  ? options.deps.split(',').map(d => d.trim()).filter(Boolean)
@@ -410,6 +483,20 @@ export async function submit(name, file, options) {
410
483
  const buildsOn = options.buildsOn
411
484
  ? options.buildsOn.split(',').map(d => d.trim()).filter(Boolean)
412
485
  : [];
486
+ if (buildsOn.length === 0) {
487
+ if (!intent) {
488
+ console.log(`\n❌ Intent required for primitive tools.`);
489
+ console.log(` Add an "## Intent" section to your README or include "Intent:" in source comments.`);
490
+ console.log(` Example:\n ## Intent\n Normalize a URL before hashing\n`);
491
+ process.exit(1);
492
+ }
493
+ if (!gap) {
494
+ console.log(`\n❌ Gap justification required for primitive tools.`);
495
+ console.log(` Add an "## Gap Justification" section to your README or include "Gap Justification:" in source comments.`);
496
+ console.log(` Example:\n ## Gap Justification\n Existing tools do not trim tracking params before hashing.\n`);
497
+ process.exit(1);
498
+ }
499
+ }
413
500
  if (!options.buildsOn && buildsOn.length === 0) {
414
501
  console.log(`\n💡 Tip: Use --builds-on to show lineage!`);
415
502
  console.log(` Example: --builds-on api-request,json-validate`);
@@ -444,6 +531,14 @@ export async function submit(name, file, options) {
444
531
  }
445
532
  }
446
533
  // ── Pre-submit execution validation ──
534
+ if (options.skipValidation) {
535
+ const allowSkip = ['1', 'true', 'yes'].includes((process.env.DEVTOPIA_ALLOW_SKIP_VALIDATION || '').toLowerCase());
536
+ if (!allowSkip) {
537
+ console.log(`\n❌ --skip-validation is restricted.`);
538
+ console.log(` Set DEVTOPIA_ALLOW_SKIP_VALIDATION=1 to use it (admin only).\n`);
539
+ process.exit(1);
540
+ }
541
+ }
447
542
  if (!options.skipValidation) {
448
543
  console.log(`\n Validating tool execution...`);
449
544
  const validation = await validateExecution(source, language);
@@ -472,6 +567,8 @@ export async function submit(name, file, options) {
472
567
  console.log(` Deps: ${dependencies.join(', ')}`);
473
568
  if (buildsOn.length)
474
569
  console.log(` Builds on: ${buildsOn.join(', ')}`);
570
+ if (externalSystems.length)
571
+ console.log(` External: ${externalSystems.join(', ')}`);
475
572
  try {
476
573
  const res = await fetch(`${API_BASE}/api/submit`, {
477
574
  method: 'POST',
@@ -485,6 +582,7 @@ export async function submit(name, file, options) {
485
582
  category,
486
583
  source,
487
584
  dependencies,
585
+ external_systems: externalSystems.length > 0 ? externalSystems : undefined,
488
586
  builds_on: buildsOn.length > 0 ? buildsOn : undefined,
489
587
  input_schema: inputSchema ? JSON.stringify(inputSchema) : undefined,
490
588
  output_schema: outputSchema ? JSON.stringify(outputSchema) : undefined,
@@ -13,10 +13,7 @@ interface ValidationResult {
13
13
  stderr: string;
14
14
  exitCode: number | null;
15
15
  }
16
- /**
17
- * Get the interpreter command for a language
18
- */
19
- export declare function getInterpreter(language: string): {
16
+ export declare function getInterpreter(language: string, toolFile?: string): {
20
17
  cmd: string;
21
18
  args: string[];
22
19
  ext: string;
@@ -34,4 +31,5 @@ export declare function wrapSourceIfNeeded(source: string, language: string): st
34
31
  */
35
32
  export declare function validateExecution(source: string, language: string): Promise<ValidationResult>;
36
33
  export declare function executeTool(toolName: string, input: any, options?: ExecuteOptions): Promise<ExecutionResult>;
34
+ export declare function executeLocalFile(filePath: string, input: any, options?: ExecuteOptions): Promise<ExecutionResult>;
37
35
  export {};