@skyphusion/sidvicious-exe 0.2.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.
@@ -0,0 +1,150 @@
1
+ // lib/helpers.mjs
2
+ // Pure, network-free logic extracted from bot.mjs (#39) so the roadie's
3
+ // critical branches are unit-testable without a live Discord/Cloudflare.
4
+ // bot.mjs imports from here; keep this file dependency-free.
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Gateway URL building + chat model normalization
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export function buildGatewayCompatEndpoint(accountId, gatewayId) {
11
+ if (!accountId || !gatewayId) return '';
12
+ return `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat/chat/completions`;
13
+ }
14
+
15
+ export function anthropicBaseFromGatewayEndpoint(endpoint) {
16
+ const base = endpoint
17
+ .replace(/\/compat\/chat\/completions\/?$/, '')
18
+ .replace(/\/compat\/?$/, '')
19
+ .replace(/\/$/, '');
20
+ return `${base}/anthropic`;
21
+ }
22
+
23
+ export function normalizeChatModel(model, useGatewayAnthropic) {
24
+ if (useGatewayAnthropic) {
25
+ if (model.startsWith('anthropic/')) return model.slice('anthropic/'.length);
26
+ return model;
27
+ }
28
+ if (model.includes('/')) return model;
29
+ if (model.startsWith('claude')) return `anthropic/${model}`;
30
+ return model;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Image model catalog
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export const IMAGE_MODELS = [
38
+ { alias: 'flux-schnell', id: '@cf/black-forest-labs/flux-1-schnell', label: 'FLUX-1 Schnell (fast, default)' },
39
+ { alias: 'flux2-fast', id: '@cf/black-forest-labs/flux-2-klein-4b', label: 'FLUX 2 Klein 4B (faster frontier)' },
40
+ { alias: 'flux2', id: '@cf/black-forest-labs/flux-2-klein-9b', label: 'FLUX 2 Klein 9B (frontier quality)' },
41
+ { alias: 'flux2-dev', id: '@cf/black-forest-labs/flux-2-dev', label: 'FLUX 2 Dev (multi-reference)' },
42
+ { alias: 'phoenix', id: '@cf/leonardo/phoenix-1.0', label: 'Phoenix 1.0 (Leonardo)' },
43
+ { alias: 'lucid', id: '@cf/leonardo/lucid-origin', label: 'Lucid Origin (Leonardo)' },
44
+ { alias: 'dreamshaper', id: '@cf/lykon/dreamshaper-8-lcm', label: 'Dreamshaper 8 LCM (fast SD)' },
45
+ { alias: 'sdxl', id: '@cf/stabilityai/stable-diffusion-xl-base-1.0', label: 'Stable Diffusion XL' },
46
+ { alias: 'gpt-image', id: 'openai/gpt-image-1.5', label: 'GPT Image 1.5 (OpenAI)' },
47
+ { alias: 'recraft', id: 'recraft/recraftv4', label: 'Recraft V4 (art-directed)' },
48
+ { alias: 'nano-banana', id: 'google/nano-banana-pro', label: 'Nano Banana Pro (Google)' },
49
+ ];
50
+
51
+ export const DEFAULT_IMAGE_MODEL = '@cf/black-forest-labs/flux-1-schnell';
52
+
53
+ // FLUX 2 models require multipart form data even for prompt-only requests.
54
+ export const MULTIPART_IMAGE_MODELS = new Set([
55
+ '@cf/black-forest-labs/flux-2-klein-4b',
56
+ '@cf/black-forest-labs/flux-2-klein-9b',
57
+ '@cf/black-forest-labs/flux-2-dev',
58
+ ]);
59
+
60
+ export function resolveImageModel(input) {
61
+ const lower = input.toLowerCase().trim();
62
+ const byAlias = IMAGE_MODELS.find(m => m.alias === lower);
63
+ if (byAlias) return byAlias;
64
+ const byId = IMAGE_MODELS.find(m => m.id === input);
65
+ if (byId) return byId;
66
+ const byPartial = IMAGE_MODELS.find(m => m.id.includes(lower));
67
+ if (byPartial) return byPartial;
68
+ return null;
69
+ }
70
+
71
+ export function formatModelList(currentId) {
72
+ const lines = ['**Image Models** (`!model <name>` or `/model <name>` to switch)\n'];
73
+ for (const m of IMAGE_MODELS) {
74
+ const active = m.id === currentId ? ' **<-- active**' : '';
75
+ lines.push(` \`${m.alias}\` -- ${m.label}${active}`);
76
+ }
77
+ return lines.join('\n');
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // LLM plumbing
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export function stripThink(text) {
85
+ return text.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
86
+ }
87
+
88
+ export function flattenForOllama(messages) {
89
+ return messages.map(m => {
90
+ if (typeof m.content === 'string') return m;
91
+ if (!Array.isArray(m.content)) return m;
92
+ const imgCount = m.content.filter(b => b.type === 'image').length;
93
+ const text = m.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
94
+ const prefix = imgCount > 0 ? `[${imgCount} image(s) attached -- vision not supported in ollama mode]\n` : '';
95
+ return { ...m, content: prefix + text };
96
+ });
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Session shape
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export function freshSession() {
104
+ return { history: [], imageModel: DEFAULT_IMAGE_MODEL };
105
+ }
106
+
107
+ /** Fill missing fields on a (possibly stale/partial) persisted session in place. */
108
+ export function normalizeSession(s) {
109
+ if (!s.imageModel) s.imageModel = DEFAULT_IMAGE_MODEL;
110
+ if (!s.history) s.history = [];
111
+ return s;
112
+ }
113
+
114
+ /** Trim rolling history to historyLen exchange PAIRS (user+assistant), oldest out. */
115
+ export function trimHistory(history, historyLen) {
116
+ while (history.length > historyLen * 2) history.shift();
117
+ return history;
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Discord message chunking (2000 char limit)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ export function splitMessage(text, limit = 1990) {
125
+ if (text.length <= limit) return [text];
126
+ const chunks = [];
127
+ while (text.length > 0) {
128
+ let slice = text.slice(0, limit);
129
+ const lastNl = slice.lastIndexOf('\n');
130
+ if (lastNl > limit * 0.5) slice = text.slice(0, lastNl + 1);
131
+ chunks.push(slice.trimEnd());
132
+ text = text.slice(slice.length).trimStart();
133
+ }
134
+ return chunks.filter(Boolean);
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Error hygiene (#39 audit): errors get echoed into Discord replies, and SDK /
139
+ // fetch errors can embed request URLs that carry the Cloudflare ACCOUNT ID
140
+ // (the gateway base URL). Scrub configured identifiers before anything
141
+ // user-facing; keep the raw message for the local log.
142
+ // ---------------------------------------------------------------------------
143
+
144
+ export function sanitizeErrorMessage(message, secrets = []) {
145
+ let out = String(message ?? '');
146
+ for (const s of secrets) {
147
+ if (s) out = out.split(s).join('[redacted]');
148
+ }
149
+ return out;
150
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@skyphusion/sidvicious-exe",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "SidVicious_exe -- punk rock Discord roadie for search and image generation via Cloudflare",
7
+ "homepage": "https://github.com/skyphusion-labs/SidVicious_exe",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/skyphusion-labs/SidVicious_exe.git"
11
+ },
12
+ "keywords": [
13
+ "discord-bot",
14
+ "cloudflare-workers",
15
+ "llm",
16
+ "web-search",
17
+ "image-generation",
18
+ "workers-ai",
19
+ "self-hosted"
20
+ ],
21
+ "scripts": {
22
+ "roadie": "node --env-file-if-exists=.env bot.mjs",
23
+ "bot": "node --env-file-if-exists=.env bot.mjs",
24
+ "test": "vitest run",
25
+ "test:coverage": "vitest run --coverage"
26
+ },
27
+ "dependencies": {
28
+ "@anthropic-ai/sdk": "^0.111",
29
+ "discord.js": "^14"
30
+ },
31
+ "overrides": {
32
+ "undici": "6.27.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=24"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/coverage-istanbul": "^4.1.10",
39
+ "vitest": "^4.1.9"
40
+ },
41
+ "license": "AGPL-3.0-only",
42
+ "bin": {
43
+ "sidvicious": "./bot.mjs"
44
+ },
45
+ "files": [
46
+ "bot.mjs",
47
+ "lib/",
48
+ ".env.example",
49
+ "NOTICE"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }