opencode-pollinations-plugin 6.0.0-beta.18 → 6.0.0-beta.2
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 +7 -7
- package/dist/index.js +15 -85
- package/dist/server/commands.js +19 -9
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -100
- package/dist/server/proxy.js +109 -65
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +97 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +185 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +60 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +75 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +215 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +115 -0
- package/package.json +6 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/dist/server/proxy.js
CHANGED
|
@@ -78,6 +78,23 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
78
78
|
schema.description = (schema.description || "") + " [Ref Failed]";
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
// VERTEX FIX: 'const' not supported -> convert to 'enum'
|
|
82
|
+
if (schema.const !== undefined) {
|
|
83
|
+
schema.enum = [schema.const];
|
|
84
|
+
delete schema.const;
|
|
85
|
+
}
|
|
86
|
+
// VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
|
|
87
|
+
if (schema.anyOf || schema.oneOf) {
|
|
88
|
+
// Vertex demands strict exclusivity.
|
|
89
|
+
// We keep 'definitions'/'$defs' if present at root (though unlikely here)
|
|
90
|
+
// But for a property node, we must strip EVERYTHING else.
|
|
91
|
+
const keys = Object.keys(schema);
|
|
92
|
+
keys.forEach(k => {
|
|
93
|
+
if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
|
|
94
|
+
delete schema[k];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
81
98
|
if (schema.properties) {
|
|
82
99
|
for (const key in schema.properties) {
|
|
83
100
|
schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
|
|
@@ -86,6 +103,15 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
86
103
|
if (schema.items) {
|
|
87
104
|
schema.items = dereferenceSchema(schema.items, rootDefs);
|
|
88
105
|
}
|
|
106
|
+
if (schema.anyOf) {
|
|
107
|
+
schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
108
|
+
}
|
|
109
|
+
if (schema.oneOf) {
|
|
110
|
+
schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
111
|
+
}
|
|
112
|
+
if (schema.allOf) {
|
|
113
|
+
schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
114
|
+
}
|
|
89
115
|
if (schema.optional !== undefined)
|
|
90
116
|
delete schema.optional;
|
|
91
117
|
if (schema.title)
|
|
@@ -107,6 +133,38 @@ function sanitizeToolsForVertex(tools) {
|
|
|
107
133
|
return tool;
|
|
108
134
|
});
|
|
109
135
|
}
|
|
136
|
+
function sanitizeToolsForBedrock(tools) {
|
|
137
|
+
return tools.map(tool => {
|
|
138
|
+
if (tool.function) {
|
|
139
|
+
if (!tool.function.description || tool.function.description.length === 0) {
|
|
140
|
+
tool.function.description = " "; // Force non-empty string
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return tool;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function sanitizeSchemaForKimi(schema) {
|
|
147
|
+
if (!schema || typeof schema !== 'object')
|
|
148
|
+
return schema;
|
|
149
|
+
// Kimi Fixes
|
|
150
|
+
if (schema.title)
|
|
151
|
+
delete schema.title;
|
|
152
|
+
// Fix empty objects "{}" which Kimi hates.
|
|
153
|
+
// If it's an empty object without type, assume string or object?
|
|
154
|
+
// Often happens with "additionalProperties: {}"
|
|
155
|
+
if (Object.keys(schema).length === 0) {
|
|
156
|
+
schema.type = "string"; // Fallback to safe type
|
|
157
|
+
schema.description = "Any value";
|
|
158
|
+
}
|
|
159
|
+
if (schema.properties) {
|
|
160
|
+
for (const key in schema.properties) {
|
|
161
|
+
schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (schema.items)
|
|
165
|
+
sanitizeSchemaForKimi(schema.items);
|
|
166
|
+
return schema;
|
|
167
|
+
}
|
|
110
168
|
function truncateTools(tools, limit = 120) {
|
|
111
169
|
if (!tools || tools.length <= limit)
|
|
112
170
|
return tools;
|
|
@@ -114,12 +172,16 @@ function truncateTools(tools, limit = 120) {
|
|
|
114
172
|
}
|
|
115
173
|
const MAX_RETRIES = 3;
|
|
116
174
|
const RETRY_DELAY_MS = 1000;
|
|
175
|
+
const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
|
|
117
176
|
function sleep(ms) {
|
|
118
177
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
119
178
|
}
|
|
120
179
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
121
180
|
try {
|
|
122
|
-
const
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
183
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
184
|
+
clearTimeout(timeoutId);
|
|
123
185
|
if (response.ok)
|
|
124
186
|
return response;
|
|
125
187
|
if (response.status === 404 || response.status === 401 || response.status === 400) {
|
|
@@ -153,11 +215,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
153
215
|
const config = loadConfig();
|
|
154
216
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
155
217
|
log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
|
|
156
|
-
// SPY LOGGING
|
|
157
|
-
try {
|
|
158
|
-
fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
|
|
159
|
-
}
|
|
160
|
-
catch (e) { }
|
|
161
218
|
// 0. COMMAND HANDLING
|
|
162
219
|
if (body.messages && body.messages.length > 0) {
|
|
163
220
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -216,16 +273,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
216
273
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
217
274
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
218
275
|
const quota = await getQuotaStatus(false);
|
|
219
|
-
// A. Resolve Base Target
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
276
|
+
// A. Resolve Base Target
|
|
277
|
+
if (actualModel.startsWith('enter/')) {
|
|
278
|
+
isEnterprise = true;
|
|
279
|
+
actualModel = actualModel.replace('enter/', '');
|
|
223
280
|
}
|
|
224
|
-
else if (actualModel.startsWith('free/')
|
|
225
|
-
|
|
281
|
+
else if (actualModel.startsWith('free/')) {
|
|
282
|
+
isEnterprise = false;
|
|
283
|
+
actualModel = actualModel.replace('free/', '');
|
|
226
284
|
}
|
|
227
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
-
isEnterprise = true;
|
|
229
285
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
230
286
|
// Check dynamic list saved by generate-config.ts
|
|
231
287
|
if (isEnterprise) {
|
|
@@ -337,16 +393,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
337
393
|
}
|
|
338
394
|
}
|
|
339
395
|
}
|
|
340
|
-
// C. Construct URL & Headers
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
396
|
+
// C. Construct URL & Headers
|
|
397
|
+
if (isEnterprise) {
|
|
398
|
+
if (!config.apiKey) {
|
|
399
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
400
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
405
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
406
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
410
|
+
authHeader = undefined;
|
|
411
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
412
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
346
413
|
}
|
|
347
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
348
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
349
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
350
414
|
// NOTIFY SWITCH
|
|
351
415
|
if (isFallbackActive) {
|
|
352
416
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -378,17 +442,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
378
442
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
379
443
|
// =========================================================
|
|
380
444
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
381
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
382
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
445
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
383
446
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
384
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
447
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
385
448
|
proxyBody.frequency_penalty = 1.1;
|
|
386
449
|
proxyBody.presence_penalty = 0.4;
|
|
387
450
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
451
|
+
// KIMI FIX: Remove 'title' from schema
|
|
452
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
453
|
+
if (t.function && t.function.parameters) {
|
|
454
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
455
|
+
}
|
|
456
|
+
return t;
|
|
457
|
+
});
|
|
388
458
|
}
|
|
389
|
-
// A. AZURE/OPENAI FIXES
|
|
390
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
391
|
-
|
|
459
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
460
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
461
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
462
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
392
463
|
if (proxyBody.messages) {
|
|
393
464
|
proxyBody.messages.forEach((m) => {
|
|
394
465
|
if (m.tool_calls) {
|
|
@@ -403,6 +474,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
403
474
|
});
|
|
404
475
|
}
|
|
405
476
|
}
|
|
477
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
478
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
479
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
480
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
481
|
+
}
|
|
406
482
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
407
483
|
if (actualModel === "nomnom") {
|
|
408
484
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -410,36 +486,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
410
486
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
411
487
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
412
488
|
}
|
|
413
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
414
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
415
489
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
416
|
-
|
|
417
|
-
// GLOBAL BEDROCK FIX (All Models)
|
|
418
|
-
// Check if history has tools but current request misses tools definition.
|
|
419
|
-
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
420
|
-
// but Bedrock requires toolConfig to validate the history.
|
|
421
|
-
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
422
|
-
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
423
|
-
// Inject Shim Tool to satisfy Bedrock
|
|
424
|
-
proxyBody.tools = [{
|
|
425
|
-
type: 'function',
|
|
426
|
-
function: {
|
|
427
|
-
name: '_bedrock_compatibility_shim',
|
|
428
|
-
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
429
|
-
parameters: { type: 'object', properties: {} }
|
|
430
|
-
}
|
|
431
|
-
}];
|
|
432
|
-
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
433
|
-
}
|
|
434
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
435
|
-
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
436
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
437
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
490
|
+
else if (actualModel.includes("gemini")) {
|
|
438
491
|
let hasFunctions = false;
|
|
439
492
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
440
493
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
441
494
|
}
|
|
442
|
-
// Old Shim logic removed (moved up)
|
|
443
495
|
if (hasFunctions) {
|
|
444
496
|
// 1. Strict cleanup of 'google_search' tool
|
|
445
497
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -470,14 +522,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
470
522
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
471
523
|
}
|
|
472
524
|
}
|
|
473
|
-
// B5. BEDROCK TOKEN LIMIT FIX
|
|
474
|
-
if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
|
|
475
|
-
// Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
|
|
476
|
-
if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
|
|
477
|
-
proxyBody.max_tokens = 4096;
|
|
478
|
-
log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
525
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
482
526
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
483
527
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -586,10 +630,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
586
630
|
// 2. Notify
|
|
587
631
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
588
632
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
589
|
-
// 3. Re-Prepare Request
|
|
590
|
-
targetUrl = 'https://
|
|
633
|
+
// 3. Re-Prepare Request
|
|
634
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
591
635
|
const retryHeaders = { ...headers };
|
|
592
|
-
//
|
|
636
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
593
637
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
594
638
|
// 4. Retry Fetch
|
|
595
639
|
const retryRes = await fetchWithRetry(targetUrl, {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
const SAVE_DIR = path.join(os.homedir(), 'Downloads', 'pollinations', 'diagrams');
|
|
7
|
+
const MERMAID_INK_BASE = 'https://mermaid.ink';
|
|
8
|
+
/**
|
|
9
|
+
* Encode Mermaid code for mermaid.ink API
|
|
10
|
+
* Uses base64 encoding of the diagram definition
|
|
11
|
+
*/
|
|
12
|
+
function encodeMermaid(code) {
|
|
13
|
+
return Buffer.from(code, 'utf-8').toString('base64url');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Fetch binary content from URL
|
|
17
|
+
*/
|
|
18
|
+
function fetchBinary(url) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const req = https.get(url, { headers: { 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0' } }, (res) => {
|
|
21
|
+
// Follow redirects
|
|
22
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
23
|
+
return fetchBinary(res.headers.location).then(resolve).catch(reject);
|
|
24
|
+
}
|
|
25
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
26
|
+
return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
27
|
+
}
|
|
28
|
+
const chunks = [];
|
|
29
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
30
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
31
|
+
});
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
req.setTimeout(15000, () => {
|
|
34
|
+
req.destroy();
|
|
35
|
+
reject(new Error('Timeout fetching diagram'));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export const genDiagramTool = tool({
|
|
40
|
+
description: `Render a Mermaid diagram to SVG or PNG image.
|
|
41
|
+
Uses mermaid.ink (free, no auth required). Supports all Mermaid syntax:
|
|
42
|
+
flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram, gantt, pie, mindmap, timeline, etc.
|
|
43
|
+
The diagram code should be valid Mermaid syntax WITHOUT the \`\`\`mermaid fences.`,
|
|
44
|
+
args: {
|
|
45
|
+
code: tool.schema.string().describe('Mermaid diagram code (e.g. "graph LR; A-->B; B-->C")'),
|
|
46
|
+
format: tool.schema.enum(['svg', 'png']).optional().describe('Output format (default: svg)'),
|
|
47
|
+
theme: tool.schema.enum(['default', 'dark', 'forest', 'neutral']).optional().describe('Diagram theme (default: default)'),
|
|
48
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
49
|
+
},
|
|
50
|
+
async execute(args, context) {
|
|
51
|
+
const format = args.format || 'svg';
|
|
52
|
+
const theme = args.theme || 'default';
|
|
53
|
+
// Ensure save directory
|
|
54
|
+
if (!fs.existsSync(SAVE_DIR)) {
|
|
55
|
+
fs.mkdirSync(SAVE_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
// Build mermaid.ink URL
|
|
58
|
+
// For themed rendering, we wrap with config
|
|
59
|
+
const themedCode = theme !== 'default'
|
|
60
|
+
? `%%{init: {'theme': '${theme}'}}%%\n${args.code}`
|
|
61
|
+
: args.code;
|
|
62
|
+
const encoded = encodeMermaid(themedCode);
|
|
63
|
+
const endpoint = format === 'svg' ? 'svg' : 'img';
|
|
64
|
+
const url = `${MERMAID_INK_BASE}/${endpoint}/${encoded}`;
|
|
65
|
+
// Generate filename
|
|
66
|
+
const safeName = args.filename
|
|
67
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
68
|
+
: `diagram_${Date.now()}`;
|
|
69
|
+
const filePath = path.join(SAVE_DIR, `${safeName}.${format}`);
|
|
70
|
+
try {
|
|
71
|
+
const data = await fetchBinary(url);
|
|
72
|
+
if (data.length < 50) {
|
|
73
|
+
return `❌ Diagram Error: mermaid.ink returned empty/invalid response. Check your Mermaid syntax.`;
|
|
74
|
+
}
|
|
75
|
+
fs.writeFileSync(filePath, data);
|
|
76
|
+
const fileSizeKB = (data.length / 1024).toFixed(1);
|
|
77
|
+
// Extract diagram type from first line
|
|
78
|
+
const firstLine = args.code.trim().split('\n')[0].trim();
|
|
79
|
+
const diagramType = firstLine.replace(/[;\s{].*/g, '');
|
|
80
|
+
context.metadata({ title: `📊 Diagram: ${diagramType}` });
|
|
81
|
+
return [
|
|
82
|
+
`📊 Diagram Rendered`,
|
|
83
|
+
`━━━━━━━━━━━━━━━━━━━`,
|
|
84
|
+
`Type: ${diagramType}`,
|
|
85
|
+
`Theme: ${theme}`,
|
|
86
|
+
`Format: ${format.toUpperCase()}`,
|
|
87
|
+
`File: ${filePath}`,
|
|
88
|
+
`Weight: ${fileSizeKB} KB`,
|
|
89
|
+
`URL: ${url}`,
|
|
90
|
+
`Cost: Free (mermaid.ink)`,
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return `❌ Diagram Error: ${err.message}\n💡 Verify your Mermaid syntax at https://mermaid.live`;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
const SAVE_DIR = path.join(os.homedir(), 'Downloads', 'pollinations', 'palettes');
|
|
6
|
+
function hexToHSL(hex) {
|
|
7
|
+
hex = hex.replace('#', '');
|
|
8
|
+
if (hex.length === 3)
|
|
9
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
10
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
11
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
12
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
13
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
14
|
+
let h = 0, s = 0;
|
|
15
|
+
const l = (max + min) / 2;
|
|
16
|
+
if (max !== min) {
|
|
17
|
+
const d = max - min;
|
|
18
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
19
|
+
switch (max) {
|
|
20
|
+
case r:
|
|
21
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
22
|
+
break;
|
|
23
|
+
case g:
|
|
24
|
+
h = ((b - r) / d + 2) / 6;
|
|
25
|
+
break;
|
|
26
|
+
case b:
|
|
27
|
+
h = ((r - g) / d + 4) / 6;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
32
|
+
}
|
|
33
|
+
function hslToHex(h, s, l) {
|
|
34
|
+
s /= 100;
|
|
35
|
+
l /= 100;
|
|
36
|
+
const a = s * Math.min(l, 1 - l);
|
|
37
|
+
const f = (n) => {
|
|
38
|
+
const k = (n + h / 30) % 12;
|
|
39
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
40
|
+
return Math.round(255 * color).toString(16).padStart(2, '0');
|
|
41
|
+
};
|
|
42
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
43
|
+
}
|
|
44
|
+
function generatePalette(baseHex, scheme, count) {
|
|
45
|
+
const base = hexToHSL(baseHex);
|
|
46
|
+
const colors = [];
|
|
47
|
+
switch (scheme) {
|
|
48
|
+
case 'complementary':
|
|
49
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
50
|
+
colors.push({ hex: hslToHex((base.h + 180) % 360, base.s, base.l), role: 'Complement' });
|
|
51
|
+
// Fill shades
|
|
52
|
+
for (let i = 2; i < count; i++) {
|
|
53
|
+
const lShift = base.l + (i % 2 === 0 ? 15 : -15) * Math.ceil(i / 2);
|
|
54
|
+
colors.push({ hex: hslToHex(base.h, base.s, Math.max(10, Math.min(90, lShift))), role: `Shade ${i - 1}` });
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case 'analogous':
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const offset = (i - Math.floor(count / 2)) * 30;
|
|
60
|
+
colors.push({
|
|
61
|
+
hex: hslToHex((base.h + offset + 360) % 360, base.s, base.l),
|
|
62
|
+
role: offset === 0 ? 'Base' : `${offset > 0 ? '+' : ''}${offset}°`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case 'triadic':
|
|
67
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
68
|
+
colors.push({ hex: hslToHex((base.h + 120) % 360, base.s, base.l), role: 'Triad +120°' });
|
|
69
|
+
colors.push({ hex: hslToHex((base.h + 240) % 360, base.s, base.l), role: 'Triad +240°' });
|
|
70
|
+
for (let i = 3; i < count; i++) {
|
|
71
|
+
const lShift = base.l + (i % 2 === 0 ? 12 : -12) * Math.ceil((i - 2) / 2);
|
|
72
|
+
colors.push({ hex: hslToHex((base.h + (i * 120)) % 360, base.s, Math.max(10, Math.min(90, lShift))), role: `Accent ${i - 2}` });
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'split-complementary':
|
|
76
|
+
colors.push({ hex: baseHex, role: 'Base' });
|
|
77
|
+
colors.push({ hex: hslToHex((base.h + 150) % 360, base.s, base.l), role: 'Split +150°' });
|
|
78
|
+
colors.push({ hex: hslToHex((base.h + 210) % 360, base.s, base.l), role: 'Split +210°' });
|
|
79
|
+
for (let i = 3; i < count; i++) {
|
|
80
|
+
colors.push({ hex: hslToHex(base.h, base.s, Math.max(10, Math.min(90, base.l + (i * 10 - 30)))), role: `Tone ${i - 2}` });
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'monochromatic':
|
|
84
|
+
default:
|
|
85
|
+
for (let i = 0; i < count; i++) {
|
|
86
|
+
const l = Math.round(15 + (i / (count - 1)) * 70); // 15% to 85%
|
|
87
|
+
colors.push({
|
|
88
|
+
hex: hslToHex(base.h, base.s, l),
|
|
89
|
+
role: l < base.l ? `Dark ${Math.abs(i - Math.floor(count / 2))}` : l === base.l ? 'Base' : `Light ${Math.abs(i - Math.floor(count / 2))}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Mark closest to base
|
|
93
|
+
let closestIdx = 0;
|
|
94
|
+
let closestDiff = Infinity;
|
|
95
|
+
colors.forEach((c, i) => {
|
|
96
|
+
const diff = Math.abs(hexToHSL(c.hex).l - base.l);
|
|
97
|
+
if (diff < closestDiff) {
|
|
98
|
+
closestDiff = diff;
|
|
99
|
+
closestIdx = i;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
colors[closestIdx].role = 'Base';
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return colors.slice(0, count);
|
|
106
|
+
}
|
|
107
|
+
function generateSVG(colors) {
|
|
108
|
+
const swatchW = 120;
|
|
109
|
+
const swatchH = 80;
|
|
110
|
+
const gap = 8;
|
|
111
|
+
const totalW = colors.length * (swatchW + gap) - gap + 40;
|
|
112
|
+
const totalH = swatchH + 60;
|
|
113
|
+
const swatches = colors.map((c, i) => {
|
|
114
|
+
const x = 20 + i * (swatchW + gap);
|
|
115
|
+
const textColor = hexToHSL(c.hex).l > 50 ? '#1a1a1a' : '#ffffff';
|
|
116
|
+
return `
|
|
117
|
+
<rect x="${x}" y="20" width="${swatchW}" height="${swatchH}" rx="8" fill="${c.hex}" stroke="#333" stroke-width="1"/>
|
|
118
|
+
<text x="${x + swatchW / 2}" y="${swatchH / 2 + 15}" text-anchor="middle" fill="${textColor}" font-family="monospace" font-size="13" font-weight="bold">${c.hex.toUpperCase()}</text>
|
|
119
|
+
<text x="${x + swatchW / 2}" y="${swatchH + 38}" text-anchor="middle" fill="#666" font-family="sans-serif" font-size="11">${c.role}</text>`;
|
|
120
|
+
}).join('');
|
|
121
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">
|
|
122
|
+
<rect width="100%" height="100%" fill="#0d0d0d" rx="12"/>
|
|
123
|
+
${swatches}
|
|
124
|
+
</svg>`;
|
|
125
|
+
}
|
|
126
|
+
export const genPaletteTool = tool({
|
|
127
|
+
description: `Generate a harmonious color palette from a base hex color.
|
|
128
|
+
Outputs a visual SVG palette + JSON color codes. Works 100% offline.
|
|
129
|
+
Schemes: monochromatic, complementary, analogous, triadic, split-complementary.
|
|
130
|
+
Perfect for frontend design, branding, and UI theming.`,
|
|
131
|
+
args: {
|
|
132
|
+
color: tool.schema.string().describe('Base hex color (e.g. "#3B82F6" or "3B82F6")'),
|
|
133
|
+
scheme: tool.schema.enum(['monochromatic', 'complementary', 'analogous', 'triadic', 'split-complementary']).optional()
|
|
134
|
+
.describe('Color harmony scheme (default: analogous)'),
|
|
135
|
+
count: tool.schema.number().min(3).max(8).optional().describe('Number of colors (default: 5, max: 8)'),
|
|
136
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
137
|
+
},
|
|
138
|
+
async execute(args, context) {
|
|
139
|
+
const scheme = args.scheme || 'analogous';
|
|
140
|
+
const count = args.count || 5;
|
|
141
|
+
// Normalize hex
|
|
142
|
+
let hex = args.color.trim();
|
|
143
|
+
if (!hex.startsWith('#'))
|
|
144
|
+
hex = '#' + hex;
|
|
145
|
+
if (!/^#[0-9a-fA-F]{3,6}$/.test(hex)) {
|
|
146
|
+
return `❌ Invalid hex color: "${args.color}". Use format: #3B82F6 or 3B82F6`;
|
|
147
|
+
}
|
|
148
|
+
if (hex.length === 4)
|
|
149
|
+
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
|
150
|
+
// Generate palette
|
|
151
|
+
const colors = generatePalette(hex, scheme, count);
|
|
152
|
+
// Ensure save directory
|
|
153
|
+
if (!fs.existsSync(SAVE_DIR)) {
|
|
154
|
+
fs.mkdirSync(SAVE_DIR, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
// Save SVG
|
|
157
|
+
const safeName = args.filename
|
|
158
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
159
|
+
: `palette_${hex.replace('#', '')}_${scheme}`;
|
|
160
|
+
const svgPath = path.join(SAVE_DIR, `${safeName}.svg`);
|
|
161
|
+
const svg = generateSVG(colors);
|
|
162
|
+
fs.writeFileSync(svgPath, svg);
|
|
163
|
+
// Build CSS custom properties snippet
|
|
164
|
+
const cssVars = colors.map((c, i) => ` --color-${i + 1}: ${c.hex};`).join('\n');
|
|
165
|
+
context.metadata({ title: `🎨 Palette: ${scheme} from ${hex}` });
|
|
166
|
+
const colorTable = colors.map(c => ` ${c.hex.toUpperCase()} ${c.role}`).join('\n');
|
|
167
|
+
return [
|
|
168
|
+
`🎨 Color Palette Generated`,
|
|
169
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
170
|
+
`Base: ${hex.toUpperCase()}`,
|
|
171
|
+
`Scheme: ${scheme}`,
|
|
172
|
+
`Colors (${count}):`,
|
|
173
|
+
colorTable,
|
|
174
|
+
``,
|
|
175
|
+
`File: ${svgPath}`,
|
|
176
|
+
``,
|
|
177
|
+
`CSS Variables:`,
|
|
178
|
+
`:root {`,
|
|
179
|
+
cssVars,
|
|
180
|
+
`}`,
|
|
181
|
+
``,
|
|
182
|
+
`Cost: Free (local computation)`,
|
|
183
|
+
].join('\n');
|
|
184
|
+
},
|
|
185
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as QRCode from 'qrcode';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
const SAVE_DIR = path.join(os.homedir(), 'Downloads', 'pollinations', 'qrcodes');
|
|
7
|
+
export const genQrcodeTool = tool({
|
|
8
|
+
description: `Generate a QR code image from text, URL, or WiFi credentials.
|
|
9
|
+
Outputs a PNG file saved locally. Works 100% offline, no API key needed.
|
|
10
|
+
Examples: URLs, plain text, WiFi (format: WIFI:T:WPA;S:NetworkName;P:Password;;)`,
|
|
11
|
+
args: {
|
|
12
|
+
content: tool.schema.string().describe('The text, URL, or WiFi string to encode into a QR code'),
|
|
13
|
+
size: tool.schema.number().min(128).max(2048).optional().describe('QR code size in pixels (default: 512)'),
|
|
14
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension). Auto-generated if omitted'),
|
|
15
|
+
},
|
|
16
|
+
async execute(args, context) {
|
|
17
|
+
const size = args.size || 512;
|
|
18
|
+
// Ensure save directory exists
|
|
19
|
+
if (!fs.existsSync(SAVE_DIR)) {
|
|
20
|
+
fs.mkdirSync(SAVE_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Generate filename
|
|
23
|
+
const safeName = args.filename
|
|
24
|
+
? args.filename.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
25
|
+
: `qr_${Date.now()}`;
|
|
26
|
+
const filePath = path.join(SAVE_DIR, `${safeName}.png`);
|
|
27
|
+
try {
|
|
28
|
+
// Generate QR code PNG
|
|
29
|
+
await QRCode.toFile(filePath, args.content, {
|
|
30
|
+
width: size,
|
|
31
|
+
margin: 2,
|
|
32
|
+
color: {
|
|
33
|
+
dark: '#000000',
|
|
34
|
+
light: '#ffffff',
|
|
35
|
+
},
|
|
36
|
+
errorCorrectionLevel: 'M',
|
|
37
|
+
});
|
|
38
|
+
// Get file size
|
|
39
|
+
const stats = fs.statSync(filePath);
|
|
40
|
+
const fileSizeKB = (stats.size / 1024).toFixed(1);
|
|
41
|
+
// Truncate content for display
|
|
42
|
+
const displayContent = args.content.length > 80
|
|
43
|
+
? args.content.substring(0, 77) + '...'
|
|
44
|
+
: args.content;
|
|
45
|
+
context.metadata({ title: `🔲 QR Code: ${displayContent}` });
|
|
46
|
+
return [
|
|
47
|
+
`🔲 QR Code Generated`,
|
|
48
|
+
`━━━━━━━━━━━━━━━━━━━`,
|
|
49
|
+
`Content: ${displayContent}`,
|
|
50
|
+
`Size: ${size}×${size}px`,
|
|
51
|
+
`File: ${filePath}`,
|
|
52
|
+
`Weight: ${fileSizeKB} KB`,
|
|
53
|
+
`Cost: Free (local generation)`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return `❌ QR Code Error: ${err.message}`;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry — Conditional Injection System
|
|
3
|
+
*
|
|
4
|
+
* Free Universe (no key): 7 tools always available
|
|
5
|
+
* Enter Universe (with key): +5 Pollinations tools
|
|
6
|
+
*
|
|
7
|
+
* Tools are injected ONCE at plugin init. Restart needed after /poll connect.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build the tool registry based on user's access level
|
|
11
|
+
*
|
|
12
|
+
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
13
|
+
*/
|
|
14
|
+
export declare function createToolRegistry(): Record<string, any>;
|