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.
- package/README.md +111 -140
- package/dist/commands/categories.js +16 -76
- package/dist/commands/compose.js +11 -2
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.js +302 -0
- package/dist/commands/docs.js +149 -159
- package/dist/commands/idea.d.ts +7 -0
- package/dist/commands/idea.js +83 -0
- package/dist/commands/run-local.d.ts +8 -0
- package/dist/commands/run-local.js +64 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +26 -8
- package/dist/commands/start.js +44 -31
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +201 -103
- package/dist/executor.d.ts +2 -4
- package/dist/executor.js +313 -18
- package/dist/index.js +47 -8
- package/package.json +3 -3
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (!
|
|
54
|
+
if (!jsonMode && !quiet) {
|
|
37
55
|
console.log(`\n⚡ Running /${toolName} locally...`);
|
|
38
56
|
}
|
|
39
|
-
const result = await executeTool(toolName, input, { strictJson:
|
|
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 (
|
|
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 (!
|
|
89
|
+
if (!quiet) {
|
|
72
90
|
console.log(`\n✅ Success (${result.durationMs}ms)\n`);
|
|
73
91
|
}
|
|
74
92
|
// Pretty print output
|
package/dist/commands/start.js
CHANGED
|
@@ -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
|
|
48
|
-
✗ JSON.stringify wrapper ✓
|
|
49
|
-
✗ Math.round helper ✓
|
|
50
|
-
✗ Base64 one-liner ✓
|
|
47
|
+
✗ String reverser ✓ API health check (retry + integrity hash)
|
|
48
|
+
✗ JSON.stringify wrapper ✓ Web page word report (fetch → clean → count)
|
|
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
|
|
81
|
-
const
|
|
82
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
│
|
|
94
|
+
│ CORE vs GRAVITY │
|
|
95
95
|
└───────────────────────────────────────────────────────────────────────────────┘
|
|
96
96
|
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
ETL-lite: flatten, pick fields, format in one call
|
|
113
|
+
These tools ACTUALLY call other tools at runtime via devtopiaRun():
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
web-page-word-report web-fetch-text → text-clean → text-word-count
|
|
116
|
+
Fetch a page, clean it, and return word counts
|
|
107
117
|
|
|
108
|
-
api-
|
|
109
|
-
|
|
118
|
+
api-request-retry api-request → retry (with backoff)
|
|
119
|
+
Reliable HTTP calls with retry and timing
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
api-health-check api-request-retry → hash-sha256
|
|
122
|
+
Check endpoint health with integrity signature
|
|
113
123
|
|
|
114
|
-
Read their source with: devtopia cat
|
|
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
|
|
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
|
|
135
|
-
$ devtopia cat api-request
|
|
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
|
|
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
|
|
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 {
|
package/dist/commands/submit.js
CHANGED
|
@@ -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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
166
|
-
return '
|
|
167
|
-
if (
|
|
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 (
|
|
238
|
+
if (/(url|http|https|fetch|request|scrape|crawl|html|dom|web)/.test(text))
|
|
206
239
|
return 'web';
|
|
207
|
-
if (
|
|
208
|
-
return '
|
|
209
|
-
if (
|
|
210
|
-
return '
|
|
211
|
-
if (
|
|
212
|
-
return '
|
|
213
|
-
if (
|
|
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 '
|
|
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
|
-
|
|
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
|
|
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 = ['
|
|
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,
|
package/dist/executor.d.ts
CHANGED
|
@@ -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 {};
|