opencode-pollinations-plugin 6.2.7 â 6.4.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/README.de.md +9 -1
- package/README.es.md +9 -1
- package/README.fr.md +9 -1
- package/README.it.md +9 -1
- package/README.md +35 -16
- package/README.zh.md +9 -1
- package/dist/index.js +11 -7
- package/dist/locales/de.json +97 -7
- package/dist/locales/en.json +97 -7
- package/dist/locales/es.json +97 -7
- package/dist/locales/fr.json +97 -7
- package/dist/locales/index.js +3 -1
- package/dist/locales/it.json +97 -7
- package/dist/locales/zh.json +463 -0
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +272 -14
- package/dist/server/connect-response.js +1 -1
- package/dist/server/generate-config.js +4 -6
- package/dist/server/models/cache.js +1 -1
- package/dist/server/models/fetcher.js +24 -2
- package/dist/server/quota.js +29 -8
- package/dist/server/tier-info.d.ts +9 -4
- package/dist/server/tier-info.js +29 -18
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +14 -1
- package/dist/tools/pollinations/beta_discovery.d.ts +11 -4
- package/dist/tools/pollinations/beta_discovery.js +288 -136
- package/dist/tools/pollinations/gen_edit_image_free.d.ts +14 -0
- package/dist/tools/pollinations/gen_edit_image_free.js +146 -0
- package/dist/tools/pollinations/gen_video.js +2 -2
- package/dist/tools/pollinations/gen_video_free.d.ts +19 -0
- package/dist/tools/pollinations/gen_video_free.js +246 -0
- package/dist/tools/pollinations/polli_config.js +1 -1
- package/dist/tools/pollinations/polli_login.d.ts +13 -0
- package/dist/tools/pollinations/polli_login.js +30 -0
- package/dist/tools/pollinations/polli_quests.d.ts +3 -0
- package/dist/tools/pollinations/polli_quests.js +135 -0
- package/package.json +2 -2
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +0 -158
- package/dist/server/scripts/test_cost_endpoints.d.ts +0 -1
- package/dist/server/scripts/test_cost_endpoints.js +0 -61
- package/dist/server/scripts/test_dynamic_pricing.d.ts +0 -1
- package/dist/server/scripts/test_dynamic_pricing.js +0 -39
- package/dist/server/scripts/test_freetier_audit.d.ts +0 -11
- package/dist/server/scripts/test_freetier_audit.js +0 -215
- package/dist/server/scripts/test_parallel_cost.d.ts +0 -1
- package/dist/server/scripts/test_parallel_cost.js +0 -104
- package/dist/tools/pollinations/deepsearch.d.ts +0 -7
- package/dist/tools/pollinations/deepsearch.js +0 -80
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +0 -7
- package/dist/tools/pollinations/search_crawl_scrape.js +0 -85
- package/dist/tools/pollinations/test_estimators.d.ts +0 -1
- package/dist/tools/pollinations/test_estimators.js +0 -22
package/dist/tools/index.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
|
16
16
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
17
17
|
import { polliStatusTool } from './pollinations/polli_status.js';
|
|
18
18
|
import { polliConfigTool } from './pollinations/polli_config.js';
|
|
19
|
+
import { polliQuestsTool } from './pollinations/polli_quests.js';
|
|
19
20
|
/**
|
|
20
21
|
* Build the tool registry based on user's access level
|
|
21
22
|
*
|
|
22
23
|
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
23
24
|
*/
|
|
24
25
|
export declare function createToolRegistry(): Record<string, any>;
|
|
25
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
|
|
26
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
package/dist/tools/index.js
CHANGED
|
@@ -16,6 +16,10 @@ import { removeBackgroundTool } from './power/remove_background.js';
|
|
|
16
16
|
import { extractFramesTool } from './power/extract_frames.js';
|
|
17
17
|
import { extractAudioTool } from './power/extract_audio.js';
|
|
18
18
|
import { rmbgKeysTool } from './power/rmbg_keys.js';
|
|
19
|
+
// === FREE BONUS: standalone always-free image gen/edit + video (no key, no Pollen) ===
|
|
20
|
+
import { genEditImageFreeTool } from './pollinations/gen_edit_image_free.js';
|
|
21
|
+
import { genVideoFreeTool } from './pollinations/gen_video_free.js';
|
|
22
|
+
import { polliLoginTool } from './pollinations/polli_login.js';
|
|
19
23
|
// === ENTER TOOLS (Require API key) ===
|
|
20
24
|
import { polliGenImageTool } from './pollinations/gen_image.js';
|
|
21
25
|
import { polliGenVideoTool } from './pollinations/gen_video.js';
|
|
@@ -27,6 +31,7 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
|
27
31
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
28
32
|
import { polliStatusTool } from './pollinations/polli_status.js';
|
|
29
33
|
import { polliConfigTool } from './pollinations/polli_config.js';
|
|
34
|
+
import { polliQuestsTool } from './pollinations/polli_quests.js';
|
|
30
35
|
import { log } from '../server/logger.js';
|
|
31
36
|
/**
|
|
32
37
|
* Detect if a valid API key is present
|
|
@@ -54,6 +59,12 @@ export function createToolRegistry() {
|
|
|
54
59
|
tools['extract_frames'] = extractFramesTool;
|
|
55
60
|
tools['extract_audio'] = extractAudioTool;
|
|
56
61
|
tools['rmbg_keys'] = rmbgKeysTool;
|
|
62
|
+
// Bonus tool: always-free image gen/edit (works without a Pollinations key)
|
|
63
|
+
tools['gen_edit_image_free'] = genEditImageFreeTool;
|
|
64
|
+
// Bonus tool: always-free video generation (works without a Pollinations key)
|
|
65
|
+
tools['gen_video_free'] = genVideoFreeTool;
|
|
66
|
+
// Login tool: device-flow login, callable by any model (no key needed to run)
|
|
67
|
+
tools['polli_login'] = polliLoginTool;
|
|
57
68
|
log(`Free tools injected: ${Object.keys(tools).length}`);
|
|
58
69
|
// === ENTER UNIVERSE: Only with valid API key (+6 tools) ===
|
|
59
70
|
if (keyPresent) {
|
|
@@ -73,6 +84,8 @@ export function createToolRegistry() {
|
|
|
73
84
|
tools['polli_config'] = polliConfigTool;
|
|
74
85
|
// Plugin Status / Info / Pricing helper map
|
|
75
86
|
tools['polli_status'] = polliStatusTool;
|
|
87
|
+
// Quests â read-only quest status & claimable Pollen nudge
|
|
88
|
+
tools['polli_quests'] = polliQuestsTool;
|
|
76
89
|
log(`Enter tools injected (key detected). Total: ${Object.keys(tools).length}`);
|
|
77
90
|
}
|
|
78
91
|
else {
|
|
@@ -83,4 +96,4 @@ export function createToolRegistry() {
|
|
|
83
96
|
return tools;
|
|
84
97
|
}
|
|
85
98
|
// Re-export for convenience
|
|
86
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
|
|
99
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* beta_discovery Tool (API Explorer
|
|
2
|
+
* beta_discovery Tool (API Explorer V4 â Defense-in-Depth)
|
|
3
3
|
*
|
|
4
|
-
* Combines reading the official OpenAPI Specification with
|
|
5
|
-
* blackbox
|
|
6
|
-
*
|
|
4
|
+
* Combines reading the official OpenAPI Specification with hardened
|
|
5
|
+
* blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
|
|
6
|
+
* injecting invalid values. The AI agent NEVER controls the actual
|
|
7
|
+
* values sent to the API.
|
|
8
|
+
*
|
|
9
|
+
* Security Layers:
|
|
10
|
+
* 1. Command Whitelist â only 4 commands exist
|
|
11
|
+
* 2. Endpoint Whitelist â only 4 API routes are probeable
|
|
12
|
+
* 3. Value Injection â fuzz values are hardcoded, AI cannot override
|
|
13
|
+
* 4. Payload Sabotage â fuzz corrupts, probe_missing removes keys
|
|
7
14
|
*/
|
|
8
15
|
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
9
16
|
export declare const polliBetaDiscoveryTool: ToolDefinition;
|
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* beta_discovery Tool (API Explorer
|
|
2
|
+
* beta_discovery Tool (API Explorer V4 â Defense-in-Depth)
|
|
3
3
|
*
|
|
4
|
-
* Combines reading the official OpenAPI Specification with
|
|
5
|
-
* blackbox
|
|
6
|
-
*
|
|
4
|
+
* Combines reading the official OpenAPI Specification with hardened
|
|
5
|
+
* blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
|
|
6
|
+
* injecting invalid values. The AI agent NEVER controls the actual
|
|
7
|
+
* values sent to the API.
|
|
8
|
+
*
|
|
9
|
+
* Security Layers:
|
|
10
|
+
* 1. Command Whitelist â only 4 commands exist
|
|
11
|
+
* 2. Endpoint Whitelist â only 4 API routes are probeable
|
|
12
|
+
* 3. Value Injection â fuzz values are hardcoded, AI cannot override
|
|
13
|
+
* 4. Payload Sabotage â fuzz corrupts, probe_missing removes keys
|
|
7
14
|
*/
|
|
8
15
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
9
16
|
import { emitStatusToast } from '../../server/toast.js';
|
|
10
17
|
import { getApiKey } from './shared.js';
|
|
11
|
-
import
|
|
18
|
+
import { ModelRegistry } from '../../server/models/index.js';
|
|
12
19
|
import * as https from 'https';
|
|
13
|
-
//
|
|
20
|
+
// âââ Constants âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
14
21
|
const OPENAPI_URL = 'https://enter.pollinations.ai/api/docs/open-api/generate-schema';
|
|
15
|
-
//
|
|
16
|
-
const
|
|
22
|
+
// Hardcoded fuzz sentinel values â AI cannot change these
|
|
23
|
+
const FUZZ_STRING = '@@_FUZZ_INTENTIONAL_INVALID_VALUE_@@';
|
|
24
|
+
const FUZZ_NUMBER = -9999999;
|
|
25
|
+
const FUZZ_BOOLEAN = 'NOT_A_BOOLEAN';
|
|
26
|
+
// Strict endpoint whitelist â only these paths can be probed
|
|
27
|
+
const ALLOWED_PROBE_ENDPOINTS = new Set([
|
|
28
|
+
'/v1/chat/completions',
|
|
29
|
+
'/video/{prompt}',
|
|
30
|
+
'/image/{prompt}',
|
|
31
|
+
'/audio/{text}',
|
|
32
|
+
]);
|
|
33
|
+
// âââ OpenAPI Schema Cache ââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
17
34
|
let cachedSchema = null;
|
|
18
35
|
async function fetchOpenApiSchema() {
|
|
19
36
|
if (cachedSchema)
|
|
20
37
|
return cachedSchema;
|
|
21
|
-
try {
|
|
22
|
-
if (fs.existsSync(LOCAL_FALLBACK_PATH)) {
|
|
23
|
-
const data = fs.readFileSync(LOCAL_FALLBACK_PATH, 'utf-8');
|
|
24
|
-
cachedSchema = JSON.parse(data);
|
|
25
|
-
return cachedSchema;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
// Fallthrough
|
|
30
|
-
}
|
|
31
38
|
try {
|
|
32
39
|
const response = await fetch(OPENAPI_URL);
|
|
33
40
|
if (!response.ok)
|
|
@@ -36,165 +43,310 @@ async function fetchOpenApiSchema() {
|
|
|
36
43
|
return cachedSchema;
|
|
37
44
|
}
|
|
38
45
|
catch (e) {
|
|
39
|
-
throw new Error(`Failed to load OpenAPI Schema: ${e.message}`);
|
|
46
|
+
throw new Error(`Failed to load OpenAPI Schema from ${OPENAPI_URL}: ${e.message}`);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// âââ HTTPS Helper (POST only for safety) âââââââââââââââââââââââââââââââââ
|
|
50
|
+
function sendProbeRequest(endpointPath, payload) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
44
52
|
const apiKey = getApiKey();
|
|
45
|
-
const
|
|
53
|
+
const fullUrl = `https://gen.pollinations.ai${endpointPath.startsWith('/') ? '' : '/'}${endpointPath}`;
|
|
54
|
+
let urlObj;
|
|
55
|
+
try {
|
|
56
|
+
urlObj = new URL(fullUrl);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return resolve(`â Invalid URL constructed: ${fullUrl}`);
|
|
60
|
+
}
|
|
61
|
+
const postData = JSON.stringify(payload);
|
|
46
62
|
const options = {
|
|
47
|
-
method:
|
|
63
|
+
method: 'POST', // ALWAYS POST â never GET (GET on /image/ or /video/ triggers generation)
|
|
48
64
|
headers: {
|
|
49
|
-
'User-Agent': 'OpenCode-Probe-
|
|
50
|
-
'Accept': 'application/json'
|
|
65
|
+
'User-Agent': 'OpenCode-Probe-V4/1.0',
|
|
66
|
+
'Accept': 'application/json',
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
51
69
|
}
|
|
52
70
|
};
|
|
53
71
|
if (apiKey) {
|
|
54
72
|
options.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
55
73
|
}
|
|
56
|
-
let postData;
|
|
57
|
-
if (method === 'POST') {
|
|
58
|
-
options.headers['Content-Type'] = 'application/json';
|
|
59
|
-
if (payloadStr) {
|
|
60
|
-
try {
|
|
61
|
-
// Try to parse just to validate it's json, but send the string
|
|
62
|
-
JSON.parse(payloadStr);
|
|
63
|
-
postData = payloadStr;
|
|
64
|
-
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
65
|
-
}
|
|
66
|
-
catch (e) {
|
|
67
|
-
return resolve(`â Error: payload_json must be a valid JSON string. Parse error: ${e.message}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
postData = '{}';
|
|
72
|
-
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (method === 'GET' && payloadStr) {
|
|
76
|
-
try {
|
|
77
|
-
const queryParams = JSON.parse(payloadStr);
|
|
78
|
-
for (const [key, value] of Object.entries(queryParams)) {
|
|
79
|
-
urlObj.searchParams.append(key, String(value));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
return resolve(`â Error: payload_json must be a valid JSON string representing query params. Parse error: ${e.message}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
74
|
const req = https.request(urlObj, options, (res) => {
|
|
87
75
|
let data = '';
|
|
88
76
|
res.on('data', chunk => data += chunk);
|
|
89
77
|
res.on('end', () => {
|
|
90
|
-
let
|
|
91
|
-
|
|
78
|
+
let result = `**HTTP Status:** \`${res.statusCode} ${res.statusMessage}\`\n`;
|
|
79
|
+
result += `**Method:** \`POST\` (forced)\n`;
|
|
80
|
+
result += `**URL:** \`${fullUrl}\`\n`;
|
|
81
|
+
result += `**Content-Type:** \`${res.headers['content-type']}\`\n\n`;
|
|
92
82
|
try {
|
|
93
83
|
const parsed = JSON.parse(data);
|
|
94
|
-
// Highlight Validation Errors (The main goal of the probe)
|
|
95
84
|
if (res.statusCode === 400 || res.statusCode === 422 || parsed.fieldErrors || parsed.error) {
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
result += `### đ¨ Validation Error â Constraints Revealed!\n`;
|
|
86
|
+
result += `\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
|
|
87
|
+
}
|
|
88
|
+
else if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
89
|
+
result += `### â ī¸ Unexpected 2xx Response (probe may have triggered real processing)\n`;
|
|
90
|
+
result += `\`\`\`json\n${JSON.stringify(parsed, null, 2).substring(0, 1500)}${data.length > 1500 ? '\n... (truncated)' : ''}\n\`\`\``;
|
|
98
91
|
}
|
|
99
92
|
else {
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
result += `### Response Body\n`;
|
|
94
|
+
result += `\`\`\`json\n${JSON.stringify(parsed, null, 2).substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
|
|
102
95
|
}
|
|
103
96
|
}
|
|
104
97
|
catch (e) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
formattedResult += `\`\`\`text\n${data.substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
|
|
98
|
+
result += `### Raw Response\n`;
|
|
99
|
+
result += `\`\`\`text\n${data.substring(0, 2000)}${data.length > 2000 ? '\n... (truncated)' : ''}\n\`\`\``;
|
|
108
100
|
}
|
|
109
|
-
resolve(
|
|
101
|
+
resolve(result);
|
|
110
102
|
});
|
|
111
103
|
});
|
|
112
104
|
req.on('error', (e) => resolve(`â Request Error: ${e.message}`));
|
|
113
|
-
|
|
114
|
-
req.
|
|
105
|
+
req.setTimeout(10000, () => {
|
|
106
|
+
req.destroy();
|
|
107
|
+
resolve('â Request Timeout (10s)');
|
|
108
|
+
});
|
|
109
|
+
req.write(postData);
|
|
115
110
|
req.end();
|
|
116
111
|
});
|
|
117
112
|
}
|
|
113
|
+
// âââ Security: Endpoint Validation âââââââââââââââââââââââââââââââââââââââ
|
|
114
|
+
function validateEndpoint(path) {
|
|
115
|
+
if (!ALLOWED_PROBE_ENDPOINTS.has(path)) {
|
|
116
|
+
const allowed = Array.from(ALLOWED_PROBE_ENDPOINTS).join(', ');
|
|
117
|
+
return `â SECURITY: Endpoint \`${path}\` is NOT in the whitelist.\nAllowed endpoints: ${allowed}\n\nUse \`search_schema\` to explore the OpenAPI spec instead.`;
|
|
118
|
+
}
|
|
119
|
+
return null; // OK
|
|
120
|
+
}
|
|
121
|
+
// âââ Command: search_schema ââââââââââââââââââââââââââââââââââââââââââââââ
|
|
122
|
+
async function cmdSearchSchema(query) {
|
|
123
|
+
const schema = await fetchOpenApiSchema();
|
|
124
|
+
const results = [];
|
|
125
|
+
const queryLower = query.toLowerCase();
|
|
126
|
+
const search = (obj, currentPath) => {
|
|
127
|
+
if (typeof obj !== 'object' || obj === null)
|
|
128
|
+
return;
|
|
129
|
+
// Check current node
|
|
130
|
+
if (typeof obj === 'object') {
|
|
131
|
+
// Check enum values
|
|
132
|
+
if (obj.enum && Array.isArray(obj.enum)) {
|
|
133
|
+
const enumStr = obj.enum.join(', ');
|
|
134
|
+
if (currentPath.toLowerCase().includes(queryLower) || enumStr.toLowerCase().includes(queryLower)) {
|
|
135
|
+
results.push({
|
|
136
|
+
path: currentPath,
|
|
137
|
+
type: 'enum',
|
|
138
|
+
detail: `Values: [${enumStr}]${obj.description ? ` â ${obj.description}` : ''}`
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check description
|
|
143
|
+
if (typeof obj.description === 'string' && obj.description.toLowerCase().includes(queryLower)) {
|
|
144
|
+
if (!results.find(r => r.path === currentPath)) {
|
|
145
|
+
results.push({
|
|
146
|
+
path: currentPath,
|
|
147
|
+
type: 'description',
|
|
148
|
+
detail: obj.description.substring(0, 300)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Check parameter names
|
|
153
|
+
if (typeof obj.name === 'string' && obj.name.toLowerCase().includes(queryLower)) {
|
|
154
|
+
results.push({
|
|
155
|
+
path: currentPath,
|
|
156
|
+
type: 'parameter',
|
|
157
|
+
detail: `Name: ${obj.name}, Type: ${obj.schema?.type || obj.type || '?'}${obj.required ? ' (REQUIRED)' : ''}${obj.description ? ` â ${obj.description.substring(0, 200)}` : ''}`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Recurse
|
|
162
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
163
|
+
search(value, currentPath ? `${currentPath}.${key}` : key);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
search(schema, '');
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
return `âšī¸ No matches found for "${query}" in the OpenAPI specification.\nTry broader terms like "model", "voice", "duration", "aspect", "format".`;
|
|
169
|
+
}
|
|
170
|
+
// Deduplicate and cap at 30 results
|
|
171
|
+
const unique = results.slice(0, 30);
|
|
172
|
+
let output = `### đ OpenAPI Search Results for "${query}" (${unique.length} matches)\n\n`;
|
|
173
|
+
for (const r of unique) {
|
|
174
|
+
output += `- **\`${r.path}\`** [${r.type}]\n ${r.detail}\n\n`;
|
|
175
|
+
}
|
|
176
|
+
if (results.length > 30) {
|
|
177
|
+
output += `\n_... and ${results.length - 30} more matches. Refine your query._`;
|
|
178
|
+
}
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
181
|
+
// âââ Command: fuzz_parameter âââââââââââââââââââââââââââââââââââââââââââââ
|
|
182
|
+
async function cmdFuzzParameter(endpointPath, basePayloadStr, fuzzTarget) {
|
|
183
|
+
// Security Layer 2: Endpoint whitelist
|
|
184
|
+
const endpointError = validateEndpoint(endpointPath);
|
|
185
|
+
if (endpointError)
|
|
186
|
+
return endpointError;
|
|
187
|
+
// Parse the base payload
|
|
188
|
+
let payload;
|
|
189
|
+
try {
|
|
190
|
+
payload = JSON.parse(basePayloadStr);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
return `â Error: base_payload_json is not valid JSON: ${e.message}`;
|
|
194
|
+
}
|
|
195
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
196
|
+
return 'â Error: base_payload_json must be a JSON object (not array or primitive)';
|
|
197
|
+
}
|
|
198
|
+
// Security Layer 3: Forced value injection
|
|
199
|
+
// Detect the current type to inject the right sabotage value
|
|
200
|
+
const currentValue = payload[fuzzTarget];
|
|
201
|
+
let injectedValue;
|
|
202
|
+
if (typeof currentValue === 'number') {
|
|
203
|
+
injectedValue = FUZZ_NUMBER;
|
|
204
|
+
}
|
|
205
|
+
else if (typeof currentValue === 'boolean') {
|
|
206
|
+
injectedValue = FUZZ_BOOLEAN;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// Default to string fuzz for unknown/string/undefined types
|
|
210
|
+
injectedValue = FUZZ_STRING;
|
|
211
|
+
}
|
|
212
|
+
// Force-inject the fuzz value â AI CANNOT override this
|
|
213
|
+
payload[fuzzTarget] = injectedValue;
|
|
214
|
+
const header = `### đ§Ē Fuzzing \`${fuzzTarget}\` on \`${endpointPath}\`\n`;
|
|
215
|
+
const info = `**Injected value:** \`${JSON.stringify(injectedValue)}\` (forced by security layer)\n`;
|
|
216
|
+
const payloadPreview = `**Payload sent:**\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n\n`;
|
|
217
|
+
const response = await sendProbeRequest(endpointPath, payload);
|
|
218
|
+
return header + info + payloadPreview + '---\n\n' + response;
|
|
219
|
+
}
|
|
220
|
+
// âââ Command: probe_missing ââââââââââââââââââââââââââââââââââââââââââââââ
|
|
221
|
+
async function cmdProbeMissing(endpointPath, basePayloadStr, removeKey) {
|
|
222
|
+
// Security Layer 2: Endpoint whitelist
|
|
223
|
+
const endpointError = validateEndpoint(endpointPath);
|
|
224
|
+
if (endpointError)
|
|
225
|
+
return endpointError;
|
|
226
|
+
// Parse the base payload
|
|
227
|
+
let payload;
|
|
228
|
+
try {
|
|
229
|
+
payload = JSON.parse(basePayloadStr);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return `â Error: base_payload_json is not valid JSON: ${e.message}`;
|
|
233
|
+
}
|
|
234
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
235
|
+
return 'â Error: base_payload_json must be a JSON object (not array or primitive)';
|
|
236
|
+
}
|
|
237
|
+
// Security Layer 4: Payload sabotage â REMOVE the key entirely
|
|
238
|
+
const hadKey = removeKey in payload;
|
|
239
|
+
delete payload[removeKey];
|
|
240
|
+
const header = `### đŦ Testing if \`${removeKey}\` is required on \`${endpointPath}\`\n`;
|
|
241
|
+
const info = `**Key "${removeKey}" ${hadKey ? 'existed and was REMOVED' : 'was NOT present'}**\n`;
|
|
242
|
+
const payloadPreview = `**Payload sent (without "${removeKey}"):**\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\`\n\n`;
|
|
243
|
+
const response = await sendProbeRequest(endpointPath, payload);
|
|
244
|
+
return header + info + payloadPreview + '---\n\n' + response;
|
|
245
|
+
}
|
|
246
|
+
// âââ Command: list_models_registry âââââââââââââââââââââââââââââââââââââââ
|
|
247
|
+
function cmdListModelsRegistry(category) {
|
|
248
|
+
if (!ModelRegistry.isReady()) {
|
|
249
|
+
return 'â ī¸ ModelRegistry is not yet initialized. The plugin may still be loading. Try again in a few seconds.';
|
|
250
|
+
}
|
|
251
|
+
const validCategories = ['image', 'video', 'audio', 'text'];
|
|
252
|
+
if (category && !validCategories.includes(category)) {
|
|
253
|
+
return `â Invalid category "${category}". Valid: ${validCategories.join(', ')}`;
|
|
254
|
+
}
|
|
255
|
+
const models = category
|
|
256
|
+
? ModelRegistry.list(category)
|
|
257
|
+
: ModelRegistry.all();
|
|
258
|
+
if (models.length === 0) {
|
|
259
|
+
return `âšī¸ No models found${category ? ` in category "${category}"` : ''}.`;
|
|
260
|
+
}
|
|
261
|
+
let output = `### đĻ Local Model Registry${category ? ` â ${category}` : ' â All Categories'} (${models.length} models)\n\n`;
|
|
262
|
+
// Group by category
|
|
263
|
+
const grouped = new Map();
|
|
264
|
+
for (const m of models) {
|
|
265
|
+
const cat = m.category;
|
|
266
|
+
if (!grouped.has(cat))
|
|
267
|
+
grouped.set(cat, []);
|
|
268
|
+
grouped.get(cat).push(m);
|
|
269
|
+
}
|
|
270
|
+
for (const [cat, catModels] of grouped) {
|
|
271
|
+
output += `#### ${cat.toUpperCase()} (${catModels.length})\n\n`;
|
|
272
|
+
output += `| Model | Description | Paid | I2X | Tools | Modalities |\n`;
|
|
273
|
+
output += `|-------|-------------|------|-----|-------|------------|\n`;
|
|
274
|
+
for (const m of catModels) {
|
|
275
|
+
const desc = (m.description || '').substring(0, 40);
|
|
276
|
+
const paid = m.paid_only ? 'đ' : 'đ';
|
|
277
|
+
const i2x = m.supportsI2X ? 'â
' : 'â';
|
|
278
|
+
const tools = m.tools ? 'â
' : 'â';
|
|
279
|
+
const mods = `${m.input_modalities.join(',')}â${m.output_modalities.join(',')}`;
|
|
280
|
+
output += `| \`${m.name}\` | ${desc} | ${paid} | ${i2x} | ${tools} | ${mods} |\n`;
|
|
281
|
+
}
|
|
282
|
+
output += '\n';
|
|
283
|
+
}
|
|
284
|
+
return output;
|
|
285
|
+
}
|
|
286
|
+
// âââ Tool Export âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
118
287
|
export const polliBetaDiscoveryTool = tool({
|
|
119
|
-
description: `
|
|
120
|
-
Use this tool ONLY to reverse
|
|
288
|
+
description: `API Explorer V4 (Defense-in-Depth) â Safely explore the Pollinations API without risking billing.
|
|
289
|
+
Use this tool ONLY to reverse-engineer API parameters and fill the manual registry.
|
|
290
|
+
|
|
291
|
+
đĄī¸ SECURITY: This tool uses 4 layers of protection to prevent accidental billing:
|
|
292
|
+
- All probe values are force-injected (you cannot send valid values)
|
|
293
|
+
- Only whitelisted endpoints can be probed
|
|
294
|
+
- All probes use POST method (prevents GET-based generation)
|
|
295
|
+
- The payload is always sabotaged (invalid value OR missing required key)
|
|
121
296
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
297
|
+
Commands:
|
|
298
|
+
- 'search_schema': Search the OpenAPI spec for keywords (e.g. "aspectRatio", "voice", "duration"). Pure read, zero network requests.
|
|
299
|
+
- 'fuzz_parameter': Send a request with an intentionally INVALID value for a specific parameter. The tool force-injects garbage to trigger Zod validation errors that reveal real constraints. YOU DO NOT CONTROL THE VALUE SENT.
|
|
300
|
+
- 'probe_missing': Remove a key from the payload to test if it's required. Sends an incomplete request to trigger "Required" errors.
|
|
301
|
+
- 'list_models_registry': Show all models from the local ModelRegistry cache. Zero network requests. Use this first to understand what the plugin already knows.
|
|
125
302
|
|
|
126
|
-
|
|
127
|
-
- 'list_endpoints': (Whitebox) Returns all routes from OpenAPI.
|
|
128
|
-
- 'get_endpoint': (Whitebox) Returns param schema for a route from OpenAPI.
|
|
129
|
-
- 'get_enums': (Whitebox) Recursively searches the OpenAPI spec for a parameter enum (e.g. 'voice', 'model').
|
|
130
|
-
- 'probe_endpoint': (Blackbox) Sends a real HTTP request (GET or POST) to trigger API validation errors (HTTP 400). Use this to reverse-engineer undocumented enums by sending invalid values. The tool auto-injects your API key.`,
|
|
303
|
+
Allowed probe endpoints: /v1/chat/completions, /video/{prompt}, /image/{prompt}, /audio/{text}`,
|
|
131
304
|
args: {
|
|
132
|
-
command: tool.schema.enum(['
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
305
|
+
command: tool.schema.enum(['search_schema', 'fuzz_parameter', 'probe_missing', 'list_models_registry']).describe('Action to perform'),
|
|
306
|
+
query: tool.schema.string().optional().describe('For search_schema: keyword to search (e.g. "aspectRatio", "voice", "model")'),
|
|
307
|
+
endpoint_path: tool.schema.string().optional().describe('For fuzz_parameter/probe_missing: API path (e.g. "/v1/chat/completions")'),
|
|
308
|
+
base_payload_json: tool.schema.string().optional().describe('For fuzz_parameter/probe_missing: base JSON payload (will be sabotaged by security layer)'),
|
|
309
|
+
fuzz_target: tool.schema.string().optional().describe('For fuzz_parameter: parameter name to fuzz (e.g. "model", "aspectRatio")'),
|
|
310
|
+
remove_key: tool.schema.string().optional().describe('For probe_missing: key name to remove from payload to test if required'),
|
|
311
|
+
category: tool.schema.string().optional().describe('For list_models_registry: filter by category (image/video/audio/text)'),
|
|
137
312
|
},
|
|
138
313
|
async execute(args, context) {
|
|
139
|
-
emitStatusToast('info', `
|
|
140
|
-
context.metadata({ title: `
|
|
314
|
+
emitStatusToast('info', `Explorer V4: ${args.command}...`, 'đ API Explorer');
|
|
315
|
+
context.metadata({ title: `Explorer: ${args.command}` });
|
|
141
316
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
317
|
+
switch (args.command) {
|
|
318
|
+
case 'search_schema': {
|
|
319
|
+
if (!args.query)
|
|
320
|
+
return 'â Error: `query` is required for search_schema';
|
|
321
|
+
return await cmdSearchSchema(args.query);
|
|
145
322
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (args.command === 'get_endpoint') {
|
|
155
|
-
if (!args.endpoint_path)
|
|
156
|
-
return 'â Error: `endpoint_path` is required';
|
|
157
|
-
const details = schema.paths[args.endpoint_path];
|
|
158
|
-
if (!details)
|
|
159
|
-
return `â Endpoint '${args.endpoint_path}' not found in OpenAPI schema.`;
|
|
160
|
-
return `**Endpoint Details for \`${args.endpoint_path}\`:**\n\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\``;
|
|
161
|
-
}
|
|
162
|
-
if (args.command === 'get_enums') {
|
|
163
|
-
if (!args.parameter_name)
|
|
164
|
-
return 'â Error: `parameter_name` is required';
|
|
165
|
-
const results = [];
|
|
166
|
-
const findEnums = (obj, path = '') => {
|
|
167
|
-
if (typeof obj !== 'object' || obj === null)
|
|
168
|
-
return;
|
|
169
|
-
if (obj.enum && Array.isArray(obj.enum) && path.includes(args.parameter_name)) {
|
|
170
|
-
results.push({
|
|
171
|
-
path: path,
|
|
172
|
-
enum: obj.enum,
|
|
173
|
-
description: obj.description
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
177
|
-
findEnums(value, path ? `${path}.${key}` : key);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
findEnums(schema.components?.schemas, 'components.schemas');
|
|
181
|
-
findEnums(schema.paths, 'paths');
|
|
182
|
-
if (results.length === 0) {
|
|
183
|
-
return `âšī¸ No enums found matching parameter '${args.parameter_name}' in OpenAPI. You should use 'probe_endpoint' to test it manually!`;
|
|
323
|
+
case 'fuzz_parameter': {
|
|
324
|
+
if (!args.endpoint_path)
|
|
325
|
+
return 'â Error: `endpoint_path` is required for fuzz_parameter';
|
|
326
|
+
if (!args.base_payload_json)
|
|
327
|
+
return 'â Error: `base_payload_json` is required for fuzz_parameter';
|
|
328
|
+
if (!args.fuzz_target)
|
|
329
|
+
return 'â Error: `fuzz_target` is required for fuzz_parameter';
|
|
330
|
+
return await cmdFuzzParameter(args.endpoint_path, args.base_payload_json, args.fuzz_target);
|
|
184
331
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
332
|
+
case 'probe_missing': {
|
|
333
|
+
if (!args.endpoint_path)
|
|
334
|
+
return 'â Error: `endpoint_path` is required for probe_missing';
|
|
335
|
+
if (!args.base_payload_json)
|
|
336
|
+
return 'â Error: `base_payload_json` is required for probe_missing';
|
|
337
|
+
if (!args.remove_key)
|
|
338
|
+
return 'â Error: `remove_key` is required for probe_missing';
|
|
339
|
+
return await cmdProbeMissing(args.endpoint_path, args.base_payload_json, args.remove_key);
|
|
340
|
+
}
|
|
341
|
+
case 'list_models_registry': {
|
|
342
|
+
return cmdListModelsRegistry(args.category);
|
|
343
|
+
}
|
|
344
|
+
default:
|
|
345
|
+
return 'â Unknown command';
|
|
193
346
|
}
|
|
194
|
-
return 'â Unknown command';
|
|
195
347
|
}
|
|
196
348
|
catch (err) {
|
|
197
|
-
emitStatusToast('error', `
|
|
349
|
+
emitStatusToast('error', `Explorer failed: ${err.message}`, 'đ API Explorer');
|
|
198
350
|
return `â Critical Error: ${err.message}`;
|
|
199
351
|
}
|
|
200
352
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_edit_image_free â Always-Free image generation & editing (BONUS tool)
|
|
3
|
+
*
|
|
4
|
+
* Independent FREE-bucket tool: works for ANY OpenCode model, with or WITHOUT a
|
|
5
|
+
* Pollinations API key. Acts as an "always free" fallback for image gen/edit,
|
|
6
|
+
* outside the Pollinations economy (no Pollen, no cost guard).
|
|
7
|
+
*
|
|
8
|
+
* Backed by a public image playground (reverse-engineered open endpoint).
|
|
9
|
+
* Direct-only: the request goes from the END USER's IP, respecting the
|
|
10
|
+
* playground's own 20-generations/IP/day free limit (gen + edit SHARE the same
|
|
11
|
+
* counter). Past the daily quota, prefer Pollinations models.
|
|
12
|
+
*/
|
|
13
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
14
|
+
export declare const genEditImageFreeTool: ToolDefinition;
|