@ebony-vinyl/dsh-our-free-model 2.8.0 → 2.8.3
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 +12 -7
- package/client.js +894 -6
- package/index.js +245 -8
- package/package.json +41 -3
- package/src/adapter.js +54 -6
- package/src/aggregate.js +53 -13
- package/src/bun-fetch.js +33 -10
- package/src/catalog.js +140 -16
- package/src/core.js +122 -33
- package/src/egress.js +204 -3
- package/src/engine-parts/win32-x64.000.part +0 -0
- package/src/engine-parts/win32-x64.001.part +0 -0
- package/src/engine-parts/win32-x64.002.part +0 -0
- package/src/engine-parts/win32-x64.003.part +0 -0
- package/src/engine-parts/win32-x64.004.part +0 -0
- package/src/engine-parts/win32-x64.005.part +0 -0
- package/src/engine-parts/win32-x64.006.part +0 -0
- package/src/extensions.js +1888 -0
- package/src/feed.js +6 -0
- package/src/http.js +14 -3
- package/src/kilo.js +55 -5
- package/src/magpie-engine.js +73 -3
- package/src/magpie-models.js +31 -3
- package/src/model-test.js +3 -1
- package/src/o2a.js +207 -8
- package/src/open-external.js +43 -5
- package/src/plugins.js +1240 -0
- package/src/providers/registry.json +72 -30
- package/src/semver.js +40 -0
- package/src/store.js +17 -1
- package/src/stream.js +39 -4
- package/src/subscription-completion.js +11 -2
- package/src/updater.js +5 -33
- package/src/upstream.js +43 -0
- package/vendor/channel-pack/NOTICE.md +1 -1
- package/vendor/channel-pack/pack.js +7242 -3952
- package/vendor/magpie/NOTICE.md +221 -5
- package/vendor/magpie/engine.json +35 -15
package/src/plugins.js
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin kernel: manifest, scans, built-in descriptors, the page bridge
|
|
3
|
+
* and the bundled marketplace shelf — one module, on purpose.
|
|
4
|
+
*
|
|
5
|
+
* The release manifest must stay inside the legacy upgrader's 80-file budget,
|
|
6
|
+
* so everything the kernel needs ships in this single file. The sections are
|
|
7
|
+
* ordered: manifest, static scans, built-in data and descriptors, the bundled
|
|
8
|
+
* registry snapshot, and the page bridge.
|
|
9
|
+
*
|
|
10
|
+
* @module src/plugins.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import { compareVersions, parseVersion } from './semver.js'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/** The plugin API this host implements. Manifests may ask for a compatible range. */
|
|
19
|
+
export const PLUGIN_API_VERSION = '2.0.0'
|
|
20
|
+
|
|
21
|
+
export const PLUGIN_KINDS = new Set(['channel', 'enhancement', 'page', 'bundle'])
|
|
22
|
+
|
|
23
|
+
/** Icons a page contribution may name. The console maps each to a lucide icon. */
|
|
24
|
+
export const PAGE_ICONS = new Set([
|
|
25
|
+
'puzzle', 'sparkles', 'chart', 'tool', 'image', 'globe',
|
|
26
|
+
'terminal', 'book', 'palette', 'activity', 'boxes', 'route',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** Capabilities a plugin may require. They are host features, not other plugins. */
|
|
30
|
+
export const PLUGIN_CAPABILITIES = new Set([
|
|
31
|
+
'ui', 'channels', 'enhancements', 'providers', 'clients', 'formats',
|
|
32
|
+
'network', 'storage', 'secrets', 'http',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const MANIFEST_ID_RE = /^[a-z][a-z0-9-]{1,47}$/
|
|
36
|
+
const PAGE_ID_RE = /^[a-z][a-z0-9-]{1,31}$/
|
|
37
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/
|
|
38
|
+
const NETWORK_ORIGIN_RE = /^https?:\/\//
|
|
39
|
+
const COVER_NAME_RE = /^[\w.-]+\.(?:png|jpe?g|webp|svg)$/i
|
|
40
|
+
|
|
41
|
+
const FILE_EXTENSIONS = new Set([
|
|
42
|
+
'.mjs', '.js', '.css', '.html', '.svg', '.png', '.jpg', '.jpeg',
|
|
43
|
+
'.webp', '.gif', '.woff2', '.json', '.txt',
|
|
44
|
+
])
|
|
45
|
+
const MAX_FILES = 64
|
|
46
|
+
const MAX_PAGE_FILES = 32
|
|
47
|
+
const MAX_FILE_PATH = 200
|
|
48
|
+
const MAX_PAGES = 8
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A relative package path: no absolute root, no drive letter, no backslash, no
|
|
52
|
+
* `..` segment, no empty segment, and a short allowlisted extension. This is
|
|
53
|
+
* the only path grammar the installer will copy or serve.
|
|
54
|
+
*/
|
|
55
|
+
export function safePluginPath(value) {
|
|
56
|
+
if (typeof value !== 'string') return null
|
|
57
|
+
const text = value.trim()
|
|
58
|
+
if (text === '' || text.length > MAX_FILE_PATH || text.startsWith('/') || text.includes('\\')) return null
|
|
59
|
+
if (text.includes('\0') || text.split('/').some(part => part === '' || part === '.' || part === '..')) return null
|
|
60
|
+
if (!/^[A-Za-z0-9._@+-]+(?:\/[A-Za-z0-9._@+-]+)*$/.test(text)) return null
|
|
61
|
+
const dot = text.lastIndexOf('.')
|
|
62
|
+
if (dot <= 0 || !FILE_EXTENSIONS.has(text.slice(dot).toLowerCase())) return null
|
|
63
|
+
return text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeUrl(value) {
|
|
67
|
+
if (typeof value !== 'string' || value === '') return ''
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(value)
|
|
70
|
+
return parsed.protocol === 'https:' ? parsed.href : ''
|
|
71
|
+
} catch { return '' }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePermissions(raw) {
|
|
75
|
+
const permissions = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
76
|
+
const network = (Array.isArray(permissions.network) ? permissions.network : [])
|
|
77
|
+
.map(origin => String(origin ?? '').trim().replace(/\/+$/, ''))
|
|
78
|
+
.filter(origin => NETWORK_ORIGIN_RE.test(origin) && origin.length <= 128)
|
|
79
|
+
.slice(0, 8)
|
|
80
|
+
const secrets = (Array.isArray(permissions.secrets) ? permissions.secrets : [])
|
|
81
|
+
.map(key => String(key ?? '').trim())
|
|
82
|
+
.filter(key => /^[\w-]{1,64}$/.test(key))
|
|
83
|
+
.slice(0, 8)
|
|
84
|
+
return { network, secrets, storage: permissions.storage === true }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeHooks(raw) {
|
|
88
|
+
const allowed = new Set(['onRequest', 'onModels', 'onUsage'])
|
|
89
|
+
const hooks = (Array.isArray(raw) ? raw : []).filter(hook => allowed.has(hook))
|
|
90
|
+
return hooks.length > 0 ? hooks : ['onRequest']
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeConfig(raw) {
|
|
94
|
+
const rows = Array.isArray(raw) ? raw : []
|
|
95
|
+
const out = []
|
|
96
|
+
for (const row of rows.slice(0, 12)) {
|
|
97
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) continue
|
|
98
|
+
const key = typeof row.key === 'string' && /^[\w-]{1,64}$/.test(row.key) ? row.key : ''
|
|
99
|
+
if (key === '' || ['__proto__', 'prototype', 'constructor'].includes(key)) continue
|
|
100
|
+
const type = ['text', 'password', 'toggle', 'number', 'select'].includes(row.type) ? row.type : 'text'
|
|
101
|
+
out.push({
|
|
102
|
+
key,
|
|
103
|
+
type,
|
|
104
|
+
label: typeof row.label === 'string' ? row.label.slice(0, 80) : key,
|
|
105
|
+
description: typeof row.description === 'string' ? row.description.slice(0, 200) : '',
|
|
106
|
+
default: row.default,
|
|
107
|
+
...(type === 'select' && Array.isArray(row.options) ? { options: row.options.slice(0, 16).map(String) } : {}),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeFiles(raw) {
|
|
114
|
+
const rows = Array.isArray(raw) ? raw : []
|
|
115
|
+
const out = []
|
|
116
|
+
const seen = new Set()
|
|
117
|
+
for (const row of rows.slice(0, MAX_FILES)) {
|
|
118
|
+
const file = safePluginPath(row)
|
|
119
|
+
if (file === null || seen.has(file)) continue
|
|
120
|
+
seen.add(file)
|
|
121
|
+
out.push(file)
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizePages(raw, { files }) {
|
|
127
|
+
const rows = Array.isArray(raw) ? raw : []
|
|
128
|
+
const pages = []
|
|
129
|
+
const seen = new Set()
|
|
130
|
+
for (const row of rows.slice(0, MAX_PAGES)) {
|
|
131
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) continue
|
|
132
|
+
const id = typeof row.id === 'string' && PAGE_ID_RE.test(row.id) ? row.id : ''
|
|
133
|
+
if (id === '' || seen.has(id)) continue
|
|
134
|
+
const entry = safePluginPath(row.entry)
|
|
135
|
+
if (entry === null || !entry.toLowerCase().endsWith('.html')) throw new Error(`page "${id}" must name a relative .html entry`)
|
|
136
|
+
if (files.length > 0 && !files.includes(entry)) throw new Error(`page "${id}" entry "${entry}" is not in the manifest files list`)
|
|
137
|
+
const order = Number.isInteger(row.order) ? Math.max(-1000, Math.min(1000, row.order)) : 100
|
|
138
|
+
const icon = typeof row.icon === 'string' && PAGE_ICONS.has(row.icon) ? row.icon : 'puzzle'
|
|
139
|
+
const group = typeof row.group === 'string' && row.group.trim() !== '' ? row.group.trim().slice(0, 40) : '插件'
|
|
140
|
+
pages.push({
|
|
141
|
+
id,
|
|
142
|
+
title: typeof row.title === 'string' && row.title.trim() !== '' ? row.title.trim().slice(0, 60) : id,
|
|
143
|
+
description: typeof row.description === 'string' ? row.description.slice(0, 200) : '',
|
|
144
|
+
entry,
|
|
145
|
+
slot: 'sidebar',
|
|
146
|
+
group,
|
|
147
|
+
order,
|
|
148
|
+
icon,
|
|
149
|
+
})
|
|
150
|
+
seen.add(id)
|
|
151
|
+
}
|
|
152
|
+
return pages
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeStringList(raw, { pattern, max }) {
|
|
156
|
+
const rows = Array.isArray(raw) ? raw : []
|
|
157
|
+
const seen = new Set()
|
|
158
|
+
const out = []
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
const value = typeof row === 'string' ? row.trim() : ''
|
|
161
|
+
if (value === '' || value.length > 64 || !pattern.test(value) || seen.has(value)) continue
|
|
162
|
+
seen.add(value)
|
|
163
|
+
out.push(value)
|
|
164
|
+
if (out.length >= max) break
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeApiVersion(raw) {
|
|
170
|
+
if (raw === undefined || raw === null || raw === '') return ''
|
|
171
|
+
if (typeof raw !== 'string' || raw.length > 32) throw new Error('manifest apiVersion must be a short string')
|
|
172
|
+
const value = raw.trim()
|
|
173
|
+
if (!/^(?:\^|~)?\d+(?:\.\d+){0,2}(?:\.x)?$|^\*\*$|^\*$|^>=\s*\d+(?:\.\d+){0,2}(?:\s+<\s*\d+(?:\.\d+){0,2})?$/.test(value)) {
|
|
174
|
+
throw new Error(`manifest apiVersion "${value}" is not a supported range`)
|
|
175
|
+
}
|
|
176
|
+
return value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Does `range` accept `host`? Only the compact forms authors actually write:
|
|
181
|
+
* `^2`, `^2.0.0`, `~2.1`, `2.x`, `2`, `>=2 <3`, `*`.
|
|
182
|
+
*/
|
|
183
|
+
export function satisfiesApiVersion(range, host = PLUGIN_API_VERSION) {
|
|
184
|
+
if (typeof range !== 'string' || range.trim() === '' || range.trim() === '*') return true
|
|
185
|
+
const hostVersion = parseVersion(host)
|
|
186
|
+
if (hostVersion === null) return false
|
|
187
|
+
const value = range.trim()
|
|
188
|
+
const major = hostVersion.major
|
|
189
|
+
if (value === '**') return true
|
|
190
|
+
const caret = /^\^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(value)
|
|
191
|
+
if (caret !== null) {
|
|
192
|
+
const atMajor = Number(caret[1])
|
|
193
|
+
if (atMajor !== major) return false
|
|
194
|
+
if (caret[2] === undefined) return true
|
|
195
|
+
const atMinor = Number(caret[2])
|
|
196
|
+
if (hostVersion.minor !== atMinor) return hostVersion.minor > atMinor
|
|
197
|
+
if (caret[3] === undefined) return true
|
|
198
|
+
return compareVersions(host, `${atMajor}.${atMinor}.${Number(caret[3])}`) >= 0
|
|
199
|
+
}
|
|
200
|
+
const tilde = /^~(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(value)
|
|
201
|
+
if (tilde !== null) {
|
|
202
|
+
if (Number(tilde[1]) !== major) return false
|
|
203
|
+
if (tilde[2] === undefined) return true
|
|
204
|
+
if (Number(tilde[2]) !== hostVersion.minor) return false
|
|
205
|
+
if (tilde[3] === undefined) return true
|
|
206
|
+
return compareVersions(host, `${tilde[1]}.${tilde[2]}.${Number(tilde[3])}`) >= 0
|
|
207
|
+
}
|
|
208
|
+
const xRange = /^(\d+)(?:\.(\d+))?\.x$/.exec(value)
|
|
209
|
+
if (xRange !== null) {
|
|
210
|
+
if (Number(xRange[1]) !== major) return false
|
|
211
|
+
return xRange[2] === undefined || Number(xRange[2]) === hostVersion.minor
|
|
212
|
+
}
|
|
213
|
+
const bare = /^(\d+)$/.exec(value)
|
|
214
|
+
if (bare !== null) return Number(bare[1]) === major
|
|
215
|
+
const bounded = /^>=\s*(\d+(?:\.\d+){0,2})(?:\s+<\s*(\d+(?:\.\d+){0,2}))?$/.exec(value)
|
|
216
|
+
if (bounded !== null) {
|
|
217
|
+
if (compareVersions(host, bounded[1]) < 0) return false
|
|
218
|
+
return bounded[2] === undefined || compareVersions(host, bounded[2]) < 0
|
|
219
|
+
}
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One manifest, v1 or v2, normalized. Throws with a human-readable reason. */
|
|
224
|
+
export function parsePluginManifest(raw, { source = 'registry' } = {}) {
|
|
225
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('manifest must be a JSON object')
|
|
226
|
+
if (raw.schemaVersion !== undefined && raw.schemaVersion !== 1 && raw.schemaVersion !== 2) {
|
|
227
|
+
throw new Error('manifest schemaVersion must be 1 or 2')
|
|
228
|
+
}
|
|
229
|
+
const schemaVersion = raw.schemaVersion === 2 ? 2 : 1
|
|
230
|
+
const id = typeof raw.id === 'string' ? raw.id.trim() : ''
|
|
231
|
+
if (!MANIFEST_ID_RE.test(id)) throw new Error(`manifest id "${id}" must match /^[a-z][a-z0-9-]{1,47}$/`)
|
|
232
|
+
const version = typeof raw.version === 'string' ? raw.version.trim() : ''
|
|
233
|
+
if (!SEMVER_RE.test(version)) throw new Error(`manifest version "${version}" is not semver`)
|
|
234
|
+
|
|
235
|
+
const contributes = raw.contributes !== null && typeof raw.contributes === 'object' && !Array.isArray(raw.contributes)
|
|
236
|
+
? raw.contributes : {}
|
|
237
|
+
const rawChannels = contributes.channels !== undefined ? contributes.channels : (Array.isArray(raw.models) ? [{ models: raw.models }] : [])
|
|
238
|
+
const channelRows = Array.isArray(rawChannels) ? rawChannels : []
|
|
239
|
+
const channelModels = channelRows.flatMap(row => Array.isArray(row?.models) ? row.models : []).slice(0, 256)
|
|
240
|
+
const enhancementHooks = contributes.enhancements !== undefined
|
|
241
|
+
? normalizeHooks(contributes.enhancements?.hooks)
|
|
242
|
+
: normalizeHooks(raw.hooks)
|
|
243
|
+
const files = normalizeFiles(raw.files)
|
|
244
|
+
const pages = normalizePages(contributes.pages !== undefined ? contributes.pages : raw.pages, { files })
|
|
245
|
+
|
|
246
|
+
const kind = (() => {
|
|
247
|
+
if (PLUGIN_KINDS.has(raw.kind)) return raw.kind
|
|
248
|
+
if (raw.type === 'channel' || raw.type === 'enhancement') return raw.type
|
|
249
|
+
if (raw.type === 'page') return 'page'
|
|
250
|
+
const hasChannels = channelRows.length > 0 || channelModels.length > 0
|
|
251
|
+
const hasEnhancements = contributes.enhancements !== undefined
|
|
252
|
+
|| (Array.isArray(raw.hooks) && raw.hooks.length > 0)
|
|
253
|
+
const hasPages = pages.length > 0
|
|
254
|
+
if (hasChannels && (hasEnhancements || hasPages)) return 'bundle'
|
|
255
|
+
if (hasChannels) return 'channel'
|
|
256
|
+
if (hasEnhancements) return 'enhancement'
|
|
257
|
+
if (hasPages) return 'page'
|
|
258
|
+
return 'bundle'
|
|
259
|
+
})()
|
|
260
|
+
const hasChannels = channelRows.length > 0 || channelModels.length > 0 || kind === 'channel'
|
|
261
|
+
const hasEnhancements = contributes.enhancements !== undefined || (schemaVersion === 1 && raw.type === 'enhancement') || kind === 'enhancement'
|
|
262
|
+
const kinds = []
|
|
263
|
+
if (hasChannels) kinds.push('channel')
|
|
264
|
+
if (hasEnhancements) kinds.push('enhancement')
|
|
265
|
+
if (pages.length > 0) kinds.push('page')
|
|
266
|
+
if (kinds.length === 0) kinds.push(kind === 'page' ? 'page' : 'bundle')
|
|
267
|
+
|
|
268
|
+
// A page-only package has no code entry; a channel/enhancement package must
|
|
269
|
+
// name one. `main` stays optional in the manifest when the registry supplies
|
|
270
|
+
// its own file list, but a code contribution without `main` can never load.
|
|
271
|
+
// A registry entry inlines the manifest and uses `main` for the signed file
|
|
272
|
+
// object (`{path, url, size, sha256}`); a package's own `ofm-plugin.json`
|
|
273
|
+
// uses a string. Accept both without making the registry carry a second name.
|
|
274
|
+
const rawMain = typeof raw.main === 'string'
|
|
275
|
+
? raw.main.trim()
|
|
276
|
+
: (raw.main !== null && typeof raw.main === 'object' && !Array.isArray(raw.main)
|
|
277
|
+
? String(raw.main.path ?? '').trim()
|
|
278
|
+
: '')
|
|
279
|
+
if (rawMain !== '' && (safePluginPath(rawMain) === null || !rawMain.toLowerCase().endsWith('.mjs'))) {
|
|
280
|
+
throw new Error('manifest main must be a relative .mjs file name')
|
|
281
|
+
}
|
|
282
|
+
const needsMain = hasChannels || hasEnhancements
|
|
283
|
+
const main = rawMain !== '' ? rawMain : (needsMain ? 'main.mjs' : '')
|
|
284
|
+
if (main !== '' && files.length > 0 && !files.includes(main)) throw new Error(`manifest main "${main}" is not in the files list`)
|
|
285
|
+
|
|
286
|
+
const apiVersion = schemaVersion === 2 ? normalizeApiVersion(raw.apiVersion) : ''
|
|
287
|
+
const apiCompatible = apiVersion === '' || satisfiesApiVersion(apiVersion)
|
|
288
|
+
|
|
289
|
+
const name = typeof raw.name === 'string' && raw.name.trim() !== '' ? raw.name.trim().slice(0, 64) : id
|
|
290
|
+
const cover = typeof raw.cover === 'string' && COVER_NAME_RE.test(raw.cover) && !raw.cover.includes('..') ? raw.cover : undefined
|
|
291
|
+
const permissions = normalizePermissions(raw.permissions)
|
|
292
|
+
const minHost = typeof raw.minHost === 'string' && SEMVER_RE.test(raw.minHost.trim()) ? raw.minHost.trim() : '0.0.0'
|
|
293
|
+
// Keep unknown capabilities on purpose: a manifest that requires something
|
|
294
|
+
// this host does not provide must mount as `pending`, not silently pretend
|
|
295
|
+
// the requirement was satisfied.
|
|
296
|
+
const requires = normalizeStringList(raw.requires, { pattern: /^[a-z][a-z0-9-]{0,31}$/, max: 16 })
|
|
297
|
+
const provides = normalizeStringList(raw.provides, { pattern: /^[a-z][a-z0-9-]{0,31}(?::[a-z0-9][a-z0-9-]{0,31})?$/, max: 16 })
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
schemaVersion,
|
|
301
|
+
id,
|
|
302
|
+
version,
|
|
303
|
+
kind,
|
|
304
|
+
kinds,
|
|
305
|
+
// Legacy consumers read `type`; keep it faithful to their two-value world.
|
|
306
|
+
type: hasChannels ? 'channel' : hasEnhancements ? 'enhancement' : 'page',
|
|
307
|
+
name,
|
|
308
|
+
description: typeof raw.description === 'string' ? raw.description.slice(0, 400) : '',
|
|
309
|
+
author: typeof raw.author === 'string' ? raw.author.slice(0, 80) : '',
|
|
310
|
+
license: typeof raw.license === 'string' ? raw.license.slice(0, 40) : '',
|
|
311
|
+
homepage: safeUrl(raw.homepage),
|
|
312
|
+
repository: safeUrl(raw.repository),
|
|
313
|
+
icon: typeof raw.icon === 'string' ? raw.icon.slice(0, 200) : '',
|
|
314
|
+
minHost,
|
|
315
|
+
apiVersion,
|
|
316
|
+
apiCompatible,
|
|
317
|
+
main,
|
|
318
|
+
permissions,
|
|
319
|
+
requires,
|
|
320
|
+
provides,
|
|
321
|
+
replaces: typeof raw.replaces === 'string' ? raw.replaces : undefined,
|
|
322
|
+
...(cover !== undefined ? { cover } : {}),
|
|
323
|
+
...(hasEnhancements ? { hooks: enhancementHooks } : {}),
|
|
324
|
+
...(hasChannels && channelModels.length > 0 ? { models: channelModels } : {}),
|
|
325
|
+
files,
|
|
326
|
+
pages,
|
|
327
|
+
config: normalizeConfig(raw.config),
|
|
328
|
+
// Built-ins set this; a registry manifest cannot make itself uninstallable.
|
|
329
|
+
essential: source === 'builtin' && raw.essential === true,
|
|
330
|
+
...(source === 'builtin' ? { builtin: true } : {}),
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Backwards-compatible name for the v1 entry point. */
|
|
335
|
+
export const parseExtensionManifest = parsePluginManifest
|
|
336
|
+
|
|
337
|
+
export function manifestHasChannel(manifest) {
|
|
338
|
+
return Array.isArray(manifest?.kinds) ? manifest.kinds.includes('channel') : manifest?.type === 'channel'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function manifestHasEnhancement(manifest) {
|
|
342
|
+
return Array.isArray(manifest?.kinds) ? manifest.kinds.includes('enhancement') : manifest?.type === 'enhancement'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function manifestHasPage(manifest) {
|
|
346
|
+
return Array.isArray(manifest?.pages) && manifest.pages.length > 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const MAX_SOURCE_BYTES = 256 * 1024
|
|
350
|
+
const MAX_PAGE_BYTES = 2 * 1024 * 1024
|
|
351
|
+
|
|
352
|
+
/** APIs a code plugin must never name. */
|
|
353
|
+
const BANNED_SOURCE_PATTERNS = [
|
|
354
|
+
/\brequire\s*\(/,
|
|
355
|
+
/\bprocess\b/,
|
|
356
|
+
/\bglobalThis\b/,
|
|
357
|
+
/\bglobal\s*\./,
|
|
358
|
+
/\bFunction\s*\(/,
|
|
359
|
+
/\beval\s*\(/,
|
|
360
|
+
/\bWebAssembly\b/,
|
|
361
|
+
/\bimportScripts\b/,
|
|
362
|
+
/\bchild_process\b/,
|
|
363
|
+
/\bworker_threads\b/,
|
|
364
|
+
/\bnode\s*:\s*(?:fs|path|os|net|tls|dns|http|https|child_process|worker_threads|module|vm|inspector)\b/,
|
|
365
|
+
/\bimport\s*\(/,
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
/** APIs a page plugin must never name: the bridge is its only way out. */
|
|
369
|
+
const BANNED_PAGE_SCRIPT_PATTERNS = [
|
|
370
|
+
/\brequire\s*\(/,
|
|
371
|
+
/\bprocess\b/,
|
|
372
|
+
/\bglobalThis\b/,
|
|
373
|
+
/\bFunction\s*\(/,
|
|
374
|
+
/\beval\s*\(/,
|
|
375
|
+
/\bWebAssembly\b/,
|
|
376
|
+
/\bimportScripts\b/,
|
|
377
|
+
/\bimport\s*\(/,
|
|
378
|
+
/\bfetch\s*\(/,
|
|
379
|
+
/\bXMLHttpRequest\b/,
|
|
380
|
+
/\bWebSocket\b/,
|
|
381
|
+
/\bEventSource\b/,
|
|
382
|
+
/\bsendBeacon\b/,
|
|
383
|
+
/\blocalStorage\b/,
|
|
384
|
+
/\bsessionStorage\b/,
|
|
385
|
+
/\bdocument\s*\.\s*cookie\b/,
|
|
386
|
+
/\bwindow\s*\.\s*open\b/,
|
|
387
|
+
/\bparent\s*\.\s*document\b/,
|
|
388
|
+
/\btop\s*\.\s*(?:location|document|window)\b/,
|
|
389
|
+
/\bjavascript\s*:/i,
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Static scan of one code plugin's source, before it ever executes.
|
|
394
|
+
*/
|
|
395
|
+
export function scanExtensionSource(source, { manifest } = {}) {
|
|
396
|
+
const violations = []
|
|
397
|
+
const text = String(source ?? '')
|
|
398
|
+
if (text.length > MAX_SOURCE_BYTES) violations.push(`source is ${text.length} bytes; the limit is ${MAX_SOURCE_BYTES}`)
|
|
399
|
+
if (text.trim() === '') violations.push('source is empty')
|
|
400
|
+
// Import specifiers live inside strings, so they are read from the raw
|
|
401
|
+
// source; the API tripwire runs over the stripped text so a quoted manual
|
|
402
|
+
// ("never call process") is prose, not a violation.
|
|
403
|
+
const imports = text.matchAll(/\bimport\b[^'";]*['"]([^'"]+)['"]/g)
|
|
404
|
+
for (const match of imports) {
|
|
405
|
+
if (match[1] !== 'ofm:sdk') violations.push(`import of "${match[1]}" — only "ofm:sdk" is available`)
|
|
406
|
+
}
|
|
407
|
+
const withoutStringsAndComments = stripStringsAndComments(text)
|
|
408
|
+
for (const pattern of BANNED_SOURCE_PATTERNS) {
|
|
409
|
+
if (pattern.test(withoutStringsAndComments)) violations.push(`source names a forbidden API: ${pattern}`)
|
|
410
|
+
}
|
|
411
|
+
const lines = text.split('\n')
|
|
412
|
+
const longLines = lines.filter(line => line.length > 500).length
|
|
413
|
+
if (longLines > 2) violations.push(`${longLines} lines exceed 500 characters — bundles and minified output are not reviewable`)
|
|
414
|
+
if (manifest !== undefined && manifest !== null && typeof manifest === 'object') {
|
|
415
|
+
// The manifest is already validated; keeping the parameter documents the
|
|
416
|
+
// contract and leaves room for per-manifest rules without a signature change.
|
|
417
|
+
}
|
|
418
|
+
return { ok: violations.length === 0, violations }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* One page asset. HTML, JS and CSS get their own scans; images, fonts and JSON
|
|
423
|
+
* are display/data only and pass as long as the path was declared.
|
|
424
|
+
*/
|
|
425
|
+
export function scanPageAsset({ path, text } = {}) {
|
|
426
|
+
const source = String(text ?? '')
|
|
427
|
+
const name = String(path ?? '').toLowerCase()
|
|
428
|
+
if (source.length > MAX_PAGE_BYTES) return { ok: false, violations: [`${path} is ${source.length} bytes; the limit is ${MAX_PAGE_BYTES}`] }
|
|
429
|
+
if (name.endsWith('.html')) return scanPageHtml(source)
|
|
430
|
+
if (name.endsWith('.js') || name.endsWith('.mjs')) return scanPageScript(source)
|
|
431
|
+
if (name.endsWith('.css')) return scanPageStyle(source)
|
|
432
|
+
return { ok: true, violations: [] }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function scanPageHtml(source) {
|
|
436
|
+
const violations = []
|
|
437
|
+
const text = String(source ?? '')
|
|
438
|
+
if (text.trim() === '') violations.push('page HTML is empty')
|
|
439
|
+
const lower = text.toLowerCase()
|
|
440
|
+
|
|
441
|
+
// Inline scripts are refused: every script is a declared file with a digest,
|
|
442
|
+
// and the host injects the one bridge script itself.
|
|
443
|
+
const scripts = [...text.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)]
|
|
444
|
+
for (const match of scripts) {
|
|
445
|
+
const attrs = match[1]
|
|
446
|
+
if (!/\bsrc\s*=/i.test(attrs)) violations.push('inline <script> is not allowed; put the code in a declared .js file')
|
|
447
|
+
const src = /\bsrc\s*=\s*["']([^"']*)["']/i.exec(attrs)?.[1] ?? ''
|
|
448
|
+
if (src !== '' && !isRelativeAsset(src)) violations.push(`script src "${src}" must be a relative package path`)
|
|
449
|
+
}
|
|
450
|
+
if (/<script\b(?![^>]*\bsrc\s*=)/i.test(text) && scripts.length === 0) {
|
|
451
|
+
violations.push('unterminated or inline <script> is not allowed')
|
|
452
|
+
}
|
|
453
|
+
for (const tag of ['iframe', 'object', 'embed', 'form', 'base']) {
|
|
454
|
+
if (new RegExp(`<\\s*${tag}\\b`, 'i').test(text)) violations.push(`<${tag}> is not allowed in a page plugin`)
|
|
455
|
+
}
|
|
456
|
+
if (/<meta[^>]+http-equiv\s*=\s*["']?refresh/i.test(text)) violations.push('meta refresh is not allowed')
|
|
457
|
+
if (/\son[a-z]+\s*=/i.test(text)) violations.push('inline event handlers are not allowed')
|
|
458
|
+
const refs = [...text.matchAll(/\b(?:src|href|poster|data)\s*=\s*["']([^"']*)["']/gi)]
|
|
459
|
+
for (const [, value] of refs) {
|
|
460
|
+
if (value === '' || value.startsWith('#') || value.startsWith('data:') || value.startsWith('blob:')) continue
|
|
461
|
+
if (!isRelativeAsset(value)) violations.push(`asset reference "${value}" must be a relative package path`)
|
|
462
|
+
}
|
|
463
|
+
if (/javascript\s*:/i.test(lower)) violations.push('javascript: URLs are not allowed')
|
|
464
|
+
if (/@import\b/i.test(text)) violations.push('@import is not allowed in page HTML')
|
|
465
|
+
return { ok: violations.length === 0, violations }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function scanPageScript(source) {
|
|
469
|
+
const violations = []
|
|
470
|
+
const text = String(source ?? '')
|
|
471
|
+
if (text.trim() === '') violations.push('page script is empty')
|
|
472
|
+
const visible = stripStringsAndComments(text)
|
|
473
|
+
for (const pattern of BANNED_PAGE_SCRIPT_PATTERNS) {
|
|
474
|
+
if (pattern.test(visible)) violations.push(`page script names a forbidden API: ${pattern}`)
|
|
475
|
+
}
|
|
476
|
+
const longLines = text.split('\n').filter(line => line.length > 500).length
|
|
477
|
+
if (longLines > 2) violations.push(`${longLines} lines exceed 500 characters — bundles and minified output are not reviewable`)
|
|
478
|
+
return { ok: violations.length === 0, violations }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function scanPageStyle(source) {
|
|
482
|
+
const violations = []
|
|
483
|
+
const text = String(source ?? '')
|
|
484
|
+
if (text.trim() === '') violations.push('page style is empty')
|
|
485
|
+
if (/@import\b/i.test(text)) violations.push('@import is not allowed in page CSS')
|
|
486
|
+
if (/\bexpression\s*\(/i.test(text)) violations.push('CSS expression() is not allowed')
|
|
487
|
+
for (const match of text.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)) {
|
|
488
|
+
const value = match[1].trim()
|
|
489
|
+
if (value === '' || value.startsWith('data:') || value.startsWith('#')) continue
|
|
490
|
+
if (!isRelativeAsset(value)) violations.push(`CSS url("${value}") must be a relative package path or data: URL`)
|
|
491
|
+
}
|
|
492
|
+
return { ok: violations.length === 0, violations }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isRelativeAsset(value) {
|
|
496
|
+
const text = String(value ?? '').trim()
|
|
497
|
+
if (text === '') return true
|
|
498
|
+
if (text.startsWith('//') || text.startsWith('/') || text.includes('\\')) return false
|
|
499
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(text)) return false
|
|
500
|
+
if (text.split('/').some(part => part === '..')) return false
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Blank comments and string bodies so pattern scans read code, not prose. */
|
|
505
|
+
export function stripStringsAndComments(source) {
|
|
506
|
+
let out = ''
|
|
507
|
+
let index = 0
|
|
508
|
+
while (index < source.length) {
|
|
509
|
+
const char = source[index]
|
|
510
|
+
const next = source[index + 1]
|
|
511
|
+
if (char === '/' && next === '/') {
|
|
512
|
+
while (index < source.length && source[index] !== '\n') index += 1
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
if (char === '/' && next === '*') {
|
|
516
|
+
index += 2
|
|
517
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index += 1
|
|
518
|
+
index += 2
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
522
|
+
const end = stringEnd(source, index, char)
|
|
523
|
+
if (end !== -1) {
|
|
524
|
+
out += char + char
|
|
525
|
+
index = end + 1
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
out += char
|
|
530
|
+
index += 1
|
|
531
|
+
}
|
|
532
|
+
return out
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The index of the quote closing the string opened at `start`, or -1 when this
|
|
537
|
+
* quote does not open one. A single- or double-quoted literal cannot span a raw
|
|
538
|
+
* newline, so a lone quote is a character inside something else — a regular
|
|
539
|
+
* expression such as `/['"]/`, for one.
|
|
540
|
+
*/
|
|
541
|
+
function stringEnd(source, start, quote) {
|
|
542
|
+
let index = start + 1
|
|
543
|
+
while (index < source.length) {
|
|
544
|
+
const char = source[index]
|
|
545
|
+
if (char === '\\') { index += 2; continue }
|
|
546
|
+
if (char === quote) return index
|
|
547
|
+
if (char === '\n' && quote !== '`') return -1
|
|
548
|
+
index += 1
|
|
549
|
+
}
|
|
550
|
+
return -1
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export const CHANNEL_PACK_CHANNELS = [
|
|
554
|
+
{ id: 'lobsterai', providerId: 'llm-lobsterai', name: 'LobsterAI' },
|
|
555
|
+
{ id: 'qoder', providerId: 'llm-qoder', name: 'Qoder' },
|
|
556
|
+
{ id: 'qodercn', providerId: 'llm-qodercn', name: 'Qoder CN' },
|
|
557
|
+
{ id: 'trae', providerId: 'llm-trae', name: 'TRAE' },
|
|
558
|
+
{ id: 'cline', providerId: 'llm-cline', name: 'Cline' },
|
|
559
|
+
{ id: 'loomy', providerId: 'llm-loomy', name: 'Loomy' },
|
|
560
|
+
{ id: 'raccoon', providerId: 'llm-raccoon', name: 'Raccoon' },
|
|
561
|
+
{ id: 'minimax', providerId: 'llm-minimax', name: 'MiniMax' },
|
|
562
|
+
{ id: 'zcode', providerId: 'llm-zcode', name: 'ZCode' },
|
|
563
|
+
{ id: 'gemini', providerId: 'llm-gemini', name: 'Gemini' },
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
export const CHANNEL_PACK_DISABLED = [
|
|
567
|
+
{ id: 'opencode', providerId: 'opencode', name: 'OpenCode', reason: 'disabled in this build' },
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
export const WORKBENCH_CLIENTS = [
|
|
571
|
+
{ id: 'codex', name: 'Codex CLI', kind: 'cli', command: 'codex' },
|
|
572
|
+
{ id: 'claude', name: 'Claude Code', kind: 'cli', command: 'claude' },
|
|
573
|
+
{ id: 'gemini', name: 'Gemini CLI', kind: 'cli', command: 'gemini' },
|
|
574
|
+
{ id: 'codex-desktop', name: 'Codex Desktop', kind: 'desktop', command: 'Codex' },
|
|
575
|
+
{ id: 'cc-switch', name: 'CC Switch', kind: 'desktop', command: 'cc-switch', nativeOnly: true },
|
|
576
|
+
{ id: 'codex-plus-plus', name: 'Codex++', kind: 'desktop', command: 'codex-plus-plus', nativeOnly: true },
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
export const LIBRARY_FORMATS = {
|
|
580
|
+
codex: { context: 'AGENTS.md', skills: 'skills', codex: true },
|
|
581
|
+
'codex-desktop': { context: 'AGENTS.md', skills: 'skills', codex: true },
|
|
582
|
+
claude: { context: 'CLAUDE.md', skills: 'skills', agents: 'agents', config: '.claude.json' },
|
|
583
|
+
gemini: { context: '.gemini/GEMINI.md', skills: '.gemini/skills', agents: '.gemini/agents', config: '.gemini/settings.json' },
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
/** A compact projection of the aggregate provider registry: the plugin center
|
|
589
|
+
* does not need every model row, only the provider identity and size. */
|
|
590
|
+
let cached = null
|
|
591
|
+
|
|
592
|
+
export function providerContributions() {
|
|
593
|
+
if (cached !== null) return cached
|
|
594
|
+
try {
|
|
595
|
+
const rows = JSON.parse(fs.readFileSync(new URL('./providers/registry.json', import.meta.url), 'utf8'))
|
|
596
|
+
cached = (Array.isArray(rows) ? rows : []).map(row => ({
|
|
597
|
+
id: String(row.id ?? ''),
|
|
598
|
+
name: typeof row.name === 'string' ? row.name : String(row.id ?? ''),
|
|
599
|
+
baseUrl: typeof row.baseUrl === 'string' ? row.baseUrl : '',
|
|
600
|
+
format: typeof row.format === 'string' ? row.format : '',
|
|
601
|
+
category: typeof row.category === 'string' ? row.category : '',
|
|
602
|
+
modelCount: Array.isArray(row.models) ? row.models.length : 0,
|
|
603
|
+
})).filter(row => row.id !== '')
|
|
604
|
+
} catch {
|
|
605
|
+
cached = []
|
|
606
|
+
}
|
|
607
|
+
return cached
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
export function builtinDescriptors() {
|
|
613
|
+
const providers = providerContributions()
|
|
614
|
+
return [
|
|
615
|
+
{
|
|
616
|
+
manifest: {
|
|
617
|
+
id: 'core-channel-pack',
|
|
618
|
+
name: '内置账号渠道',
|
|
619
|
+
description: '随应用发布的十个账号渠道:登录、账号池、每日额度与本地网关。',
|
|
620
|
+
icon: 'network',
|
|
621
|
+
provides: ['channels'],
|
|
622
|
+
},
|
|
623
|
+
contributions: {
|
|
624
|
+
channels: CHANNEL_PACK_CHANNELS,
|
|
625
|
+
disabled: CHANNEL_PACK_DISABLED,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
manifest: {
|
|
630
|
+
id: 'core-aggregate-providers',
|
|
631
|
+
name: '聚合渠道目录',
|
|
632
|
+
description: '用户自带 API Key 的提供商目录与连线格式。',
|
|
633
|
+
icon: 'boxes',
|
|
634
|
+
provides: ['providers'],
|
|
635
|
+
},
|
|
636
|
+
contributions: { providers },
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
manifest: {
|
|
640
|
+
id: 'core-workbench-clients',
|
|
641
|
+
name: '客户端启动器',
|
|
642
|
+
description: '识别本机的 CLI 与桌面客户端,并为每次启动写入独立配置。',
|
|
643
|
+
icon: 'terminal',
|
|
644
|
+
provides: ['clients'],
|
|
645
|
+
},
|
|
646
|
+
contributions: { clients: WORKBENCH_CLIENTS },
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
manifest: {
|
|
650
|
+
id: 'core-library-formats',
|
|
651
|
+
name: '资源库格式',
|
|
652
|
+
description: '把资源库条目物化为各客户端的上下文、技能与 MCP 配置。',
|
|
653
|
+
icon: 'book',
|
|
654
|
+
provides: ['formats'],
|
|
655
|
+
},
|
|
656
|
+
contributions: {
|
|
657
|
+
formats: Object.entries(LIBRARY_FORMATS).map(([id, format]) => ({ id, ...format })),
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
manifest: {
|
|
662
|
+
id: 'core-model-workspace',
|
|
663
|
+
name: '模型工作台',
|
|
664
|
+
description: '内置的本地模型工作台页面与模型目录同步。',
|
|
665
|
+
icon: 'panels',
|
|
666
|
+
provides: ['ui'],
|
|
667
|
+
},
|
|
668
|
+
contributions: {},
|
|
669
|
+
},
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
/** The marketplace snapshot shipped with the build; see readBundledRegistry. */
|
|
675
|
+
export const REGISTRY_FALLBACK = {
|
|
676
|
+
"schemaVersion": 1,
|
|
677
|
+
"generatedAt": "2026-11-07T09:25:39.628Z",
|
|
678
|
+
"plugins": [
|
|
679
|
+
{
|
|
680
|
+
"schemaVersion": 1,
|
|
681
|
+
"id": "anthropic-compatible",
|
|
682
|
+
"version": "1.0.0",
|
|
683
|
+
"type": "channel",
|
|
684
|
+
"name": "Anthropic-Compatible",
|
|
685
|
+
"main": {
|
|
686
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/anthropic-compatible/main.mjs",
|
|
687
|
+
"size": 2309,
|
|
688
|
+
"sha256": "138a77b64a4b5b64cfb38b3acbe1004cdab7b855ac66176791d51313ef35c3ad"
|
|
689
|
+
},
|
|
690
|
+
"description": "Point the harness at any Anthropic-compatible /v1/messages server — Anthropic itself, a gateway, or a loopback proxy. Speaks the messages wire.",
|
|
691
|
+
"author": "OFM Extensions maintainers",
|
|
692
|
+
"license": "MIT",
|
|
693
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
694
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
695
|
+
"icon": "",
|
|
696
|
+
"minHost": "2.8.1",
|
|
697
|
+
"permissions": {
|
|
698
|
+
"network": [
|
|
699
|
+
"https://api.anthropic.com",
|
|
700
|
+
"http://127.0.0.1:8080",
|
|
701
|
+
"http://127.0.0.1:8000",
|
|
702
|
+
"http://localhost:8080"
|
|
703
|
+
],
|
|
704
|
+
"secrets": [
|
|
705
|
+
"apiKey"
|
|
706
|
+
],
|
|
707
|
+
"storage": false
|
|
708
|
+
},
|
|
709
|
+
"cover": {
|
|
710
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/anthropic-compatible/cover.svg",
|
|
711
|
+
"size": 838,
|
|
712
|
+
"sha256": "7f3943f8aab12d08798c95db2d5a5b49c81e8d79340dc8e66e6e99167542227a"
|
|
713
|
+
},
|
|
714
|
+
"models": [
|
|
715
|
+
{
|
|
716
|
+
"id": "claude-sonnet-4-5",
|
|
717
|
+
"name": "Claude Sonnet 4.5",
|
|
718
|
+
"contextWindow": 200000,
|
|
719
|
+
"maxOutput": 64000,
|
|
720
|
+
"vision": true,
|
|
721
|
+
"reasoning": true,
|
|
722
|
+
"wire": "messages"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"id": "claude-opus-4-1",
|
|
726
|
+
"name": "Claude Opus 4.1",
|
|
727
|
+
"contextWindow": 200000,
|
|
728
|
+
"maxOutput": 32000,
|
|
729
|
+
"vision": true,
|
|
730
|
+
"reasoning": true,
|
|
731
|
+
"wire": "messages"
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"id": "claude-haiku-4-5",
|
|
735
|
+
"name": "Claude Haiku 4.5",
|
|
736
|
+
"contextWindow": 200000,
|
|
737
|
+
"maxOutput": 64000,
|
|
738
|
+
"vision": true,
|
|
739
|
+
"reasoning": false,
|
|
740
|
+
"wire": "messages"
|
|
741
|
+
}
|
|
742
|
+
],
|
|
743
|
+
"config": [
|
|
744
|
+
{
|
|
745
|
+
"key": "baseUrl",
|
|
746
|
+
"type": "text",
|
|
747
|
+
"label": "Base URL",
|
|
748
|
+
"description": "The server root — /v1/messages is appended, e.g. https://api.anthropic.com. Its origin must be one this package's permissions declare.",
|
|
749
|
+
"default": "https://api.anthropic.com"
|
|
750
|
+
}
|
|
751
|
+
],
|
|
752
|
+
"summary": "Turns any Anthropic-compatible /v1/messages server into an OFM channel: base URL plus API key, speaking the messages wire.",
|
|
753
|
+
"tags": [
|
|
754
|
+
"anthropic",
|
|
755
|
+
"claude",
|
|
756
|
+
"messages",
|
|
757
|
+
"self-hosted"
|
|
758
|
+
],
|
|
759
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
760
|
+
"changelog": [
|
|
761
|
+
"1.0.0 — initial release: Anthropic-compatible /v1/messages channel with a canonical Claude model shelf"
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
"schemaVersion": 1,
|
|
766
|
+
"id": "kilo-free",
|
|
767
|
+
"version": "1.0.0",
|
|
768
|
+
"type": "channel",
|
|
769
|
+
"name": "Kilo Free",
|
|
770
|
+
"main": {
|
|
771
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/kilo-free/main.mjs",
|
|
772
|
+
"size": 4390,
|
|
773
|
+
"sha256": "954d775191d3407092eeb842e0023c4a8da7295e0efca95c232a06447bdbad13"
|
|
774
|
+
},
|
|
775
|
+
"description": "The Kilo gateway's free tier as a keyless channel. Takes the built-in Kilo lane over; no API key required.",
|
|
776
|
+
"author": "OFM Extensions maintainers",
|
|
777
|
+
"license": "MIT",
|
|
778
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
779
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
780
|
+
"icon": "",
|
|
781
|
+
"minHost": "2.8.1",
|
|
782
|
+
"permissions": {
|
|
783
|
+
"network": [
|
|
784
|
+
"https://ai-gateway.kilo.ai",
|
|
785
|
+
"https://api.kilo.ai"
|
|
786
|
+
],
|
|
787
|
+
"secrets": [],
|
|
788
|
+
"storage": false
|
|
789
|
+
},
|
|
790
|
+
"replaces": "kilo",
|
|
791
|
+
"cover": {
|
|
792
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/kilo-free/cover.svg",
|
|
793
|
+
"size": 838,
|
|
794
|
+
"sha256": "917288a7a7edf17fab751fe57bb3aee63178dd498c5506e68fdc97b8f5c62181"
|
|
795
|
+
},
|
|
796
|
+
"models": [
|
|
797
|
+
{
|
|
798
|
+
"id": "kilo-auto/free",
|
|
799
|
+
"name": "Kilo Auto",
|
|
800
|
+
"contextWindow": 256000,
|
|
801
|
+
"maxOutput": 32768,
|
|
802
|
+
"vision": false,
|
|
803
|
+
"reasoning": false,
|
|
804
|
+
"wire": "chat"
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"id": "nvidia/nemotron-3.5-super-vl:free",
|
|
808
|
+
"name": "Nemotron 3.5 Super VL",
|
|
809
|
+
"contextWindow": 262144,
|
|
810
|
+
"maxOutput": 32768,
|
|
811
|
+
"vision": true,
|
|
812
|
+
"reasoning": false,
|
|
813
|
+
"wire": "chat"
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"id": "reflection/beam-free",
|
|
817
|
+
"name": "Beam",
|
|
818
|
+
"contextWindow": 131072,
|
|
819
|
+
"maxOutput": 16384,
|
|
820
|
+
"vision": false,
|
|
821
|
+
"reasoning": true,
|
|
822
|
+
"wire": "chat"
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
"id": "stealth/glyph-cluster",
|
|
826
|
+
"name": "Glyph Cluster",
|
|
827
|
+
"contextWindow": 200000,
|
|
828
|
+
"maxOutput": 32768,
|
|
829
|
+
"vision": false,
|
|
830
|
+
"reasoning": true,
|
|
831
|
+
"wire": "chat"
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"id": "stealth/crouton-alpha",
|
|
835
|
+
"name": "Crouton Alpha",
|
|
836
|
+
"contextWindow": 200000,
|
|
837
|
+
"maxOutput": 32768,
|
|
838
|
+
"vision": false,
|
|
839
|
+
"reasoning": false,
|
|
840
|
+
"wire": "chat"
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
"id": "dots-studio/dots-3-note-preview:free",
|
|
844
|
+
"name": "Dots 3 Note Preview",
|
|
845
|
+
"contextWindow": 131072,
|
|
846
|
+
"maxOutput": 16384,
|
|
847
|
+
"vision": false,
|
|
848
|
+
"reasoning": false,
|
|
849
|
+
"wire": "chat"
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"id": "nvidia/nemotron-3.5-lightning:free",
|
|
853
|
+
"name": "Nemotron 3.5 Lightning",
|
|
854
|
+
"contextWindow": 262144,
|
|
855
|
+
"maxOutput": 16384,
|
|
856
|
+
"vision": false,
|
|
857
|
+
"reasoning": false,
|
|
858
|
+
"wire": "chat"
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
"id": "cohere/north-mini-code:free",
|
|
862
|
+
"name": "North Mini Code",
|
|
863
|
+
"contextWindow": 256000,
|
|
864
|
+
"maxOutput": 32768,
|
|
865
|
+
"vision": false,
|
|
866
|
+
"reasoning": false,
|
|
867
|
+
"wire": "chat"
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
"id": "nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
871
|
+
"name": "Nemotron 3 Ultra 550B",
|
|
872
|
+
"contextWindow": 262144,
|
|
873
|
+
"maxOutput": 32768,
|
|
874
|
+
"vision": false,
|
|
875
|
+
"reasoning": true,
|
|
876
|
+
"wire": "chat"
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
|
880
|
+
"name": "Nemotron 3 Nano Omni 30B",
|
|
881
|
+
"contextWindow": 262144,
|
|
882
|
+
"maxOutput": 32768,
|
|
883
|
+
"vision": true,
|
|
884
|
+
"reasoning": true,
|
|
885
|
+
"wire": "chat"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"id": "openrouter/free",
|
|
889
|
+
"name": "OpenRouter Free",
|
|
890
|
+
"contextWindow": 131072,
|
|
891
|
+
"maxOutput": 16384,
|
|
892
|
+
"vision": false,
|
|
893
|
+
"reasoning": false,
|
|
894
|
+
"wire": "chat"
|
|
895
|
+
}
|
|
896
|
+
],
|
|
897
|
+
"config": [],
|
|
898
|
+
"summary": "The Kilo gateway's free model tier as a keyless channel — takes the built-in Kilo lane over with no API key required.",
|
|
899
|
+
"tags": [
|
|
900
|
+
"free",
|
|
901
|
+
"gateway",
|
|
902
|
+
"kilo",
|
|
903
|
+
"no-key"
|
|
904
|
+
],
|
|
905
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
906
|
+
"changelog": [
|
|
907
|
+
"1.0.0 — initial release: keyless Kilo gateway free tier, takes over the built-in kilo lane"
|
|
908
|
+
]
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"schemaVersion": 1,
|
|
912
|
+
"id": "model-filter",
|
|
913
|
+
"version": "1.0.0",
|
|
914
|
+
"type": "enhancement",
|
|
915
|
+
"name": "Model Filter",
|
|
916
|
+
"main": {
|
|
917
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/model-filter/main.mjs",
|
|
918
|
+
"size": 1577,
|
|
919
|
+
"sha256": "530ae8873b73252599e7e7e76082917aa239f5f1af644d921d7d38820e264479"
|
|
920
|
+
},
|
|
921
|
+
"description": "Hides catalog models whose id or name contains one of your comma-separated substrings. Reserved for the lane's model listing; request bodies pass through.",
|
|
922
|
+
"author": "OFM Extensions maintainers",
|
|
923
|
+
"license": "MIT",
|
|
924
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
925
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
926
|
+
"icon": "",
|
|
927
|
+
"minHost": "2.8.1",
|
|
928
|
+
"permissions": {
|
|
929
|
+
"network": [],
|
|
930
|
+
"secrets": [],
|
|
931
|
+
"storage": false
|
|
932
|
+
},
|
|
933
|
+
"cover": {
|
|
934
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/model-filter/cover.svg",
|
|
935
|
+
"size": 838,
|
|
936
|
+
"sha256": "eda4a836f67fb8ce4e38e01fba4965df14455328c491b3002a7184555ad2b118"
|
|
937
|
+
},
|
|
938
|
+
"hooks": [
|
|
939
|
+
"onRequest",
|
|
940
|
+
"onModels"
|
|
941
|
+
],
|
|
942
|
+
"config": [
|
|
943
|
+
{
|
|
944
|
+
"key": "hide",
|
|
945
|
+
"type": "text",
|
|
946
|
+
"label": "Hide models matching",
|
|
947
|
+
"description": "Comma-separated substrings, matched case-insensitively against each model's id and name. Empty shows everything.",
|
|
948
|
+
"default": ""
|
|
949
|
+
}
|
|
950
|
+
],
|
|
951
|
+
"summary": "Filters catalog rows whose id or name contains one of your comma-separated substrings; outgoing bodies pass through untouched.",
|
|
952
|
+
"tags": [
|
|
953
|
+
"catalog",
|
|
954
|
+
"filter",
|
|
955
|
+
"models"
|
|
956
|
+
],
|
|
957
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
958
|
+
"changelog": [
|
|
959
|
+
"1.0.0 — initial release: catalog filtering by comma-separated substrings"
|
|
960
|
+
]
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
"schemaVersion": 1,
|
|
964
|
+
"id": "openai-compatible",
|
|
965
|
+
"version": "1.0.0",
|
|
966
|
+
"type": "channel",
|
|
967
|
+
"name": "OpenAI-Compatible",
|
|
968
|
+
"main": {
|
|
969
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/openai-compatible/main.mjs",
|
|
970
|
+
"size": 2374,
|
|
971
|
+
"sha256": "b194b9ef5c44903db5d4aa1a5739101673ad882bc3a12cfc4b10aac70da7d233"
|
|
972
|
+
},
|
|
973
|
+
"description": "Point the harness at any OpenAI-compatible /chat/completions server — OpenAI itself, Ollama, LM Studio, llama.cpp, vLLM, or a loopback proxy.",
|
|
974
|
+
"author": "OFM Extensions maintainers",
|
|
975
|
+
"license": "MIT",
|
|
976
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
977
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
978
|
+
"icon": "",
|
|
979
|
+
"minHost": "2.8.1",
|
|
980
|
+
"permissions": {
|
|
981
|
+
"network": [
|
|
982
|
+
"https://api.openai.com",
|
|
983
|
+
"http://127.0.0.1:11434",
|
|
984
|
+
"http://127.0.0.1:1234",
|
|
985
|
+
"http://127.0.0.1:8080",
|
|
986
|
+
"http://127.0.0.1:8000",
|
|
987
|
+
"http://[::1]:11434"
|
|
988
|
+
],
|
|
989
|
+
"secrets": [
|
|
990
|
+
"apiKey"
|
|
991
|
+
],
|
|
992
|
+
"storage": false
|
|
993
|
+
},
|
|
994
|
+
"cover": {
|
|
995
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/openai-compatible/cover.svg",
|
|
996
|
+
"size": 838,
|
|
997
|
+
"sha256": "3aefa41e87f9f946956ac3a0173f3d76f4171bb499f1ff23a4391d2603c9c6d2"
|
|
998
|
+
},
|
|
999
|
+
"models": [
|
|
1000
|
+
{
|
|
1001
|
+
"id": "gpt-4o",
|
|
1002
|
+
"name": "GPT-4o",
|
|
1003
|
+
"contextWindow": 128000,
|
|
1004
|
+
"maxOutput": 16384,
|
|
1005
|
+
"vision": true,
|
|
1006
|
+
"reasoning": false,
|
|
1007
|
+
"wire": "chat"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
"id": "gpt-4o-mini",
|
|
1011
|
+
"name": "GPT-4o mini",
|
|
1012
|
+
"contextWindow": 128000,
|
|
1013
|
+
"maxOutput": 16384,
|
|
1014
|
+
"vision": true,
|
|
1015
|
+
"reasoning": false,
|
|
1016
|
+
"wire": "chat"
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"id": "gpt-4.1",
|
|
1020
|
+
"name": "GPT-4.1",
|
|
1021
|
+
"contextWindow": 128000,
|
|
1022
|
+
"maxOutput": 32768,
|
|
1023
|
+
"vision": true,
|
|
1024
|
+
"reasoning": false,
|
|
1025
|
+
"wire": "chat"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
"id": "deepseek-chat",
|
|
1029
|
+
"name": "DeepSeek Chat",
|
|
1030
|
+
"contextWindow": 131072,
|
|
1031
|
+
"maxOutput": 8192,
|
|
1032
|
+
"vision": false,
|
|
1033
|
+
"reasoning": false,
|
|
1034
|
+
"wire": "chat"
|
|
1035
|
+
}
|
|
1036
|
+
],
|
|
1037
|
+
"config": [
|
|
1038
|
+
{
|
|
1039
|
+
"key": "baseUrl",
|
|
1040
|
+
"type": "text",
|
|
1041
|
+
"label": "Base URL",
|
|
1042
|
+
"description": "The server root, e.g. https://api.openai.com/v1 or http://127.0.0.1:11434/v1. Its origin must be one this package's permissions declare.",
|
|
1043
|
+
"default": "https://api.openai.com/v1"
|
|
1044
|
+
}
|
|
1045
|
+
],
|
|
1046
|
+
"summary": "Turns any OpenAI-compatible server into an OFM channel: set the base URL, optionally store an API key, and chat over the chat wire.",
|
|
1047
|
+
"tags": [
|
|
1048
|
+
"chat",
|
|
1049
|
+
"compatible",
|
|
1050
|
+
"openai",
|
|
1051
|
+
"self-hosted"
|
|
1052
|
+
],
|
|
1053
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
1054
|
+
"changelog": [
|
|
1055
|
+
"1.0.0 — initial release: OpenAI-compatible /chat/completions channel with a canonical model shelf"
|
|
1056
|
+
]
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
"schemaVersion": 1,
|
|
1060
|
+
"id": "param-override",
|
|
1061
|
+
"version": "1.0.0",
|
|
1062
|
+
"type": "enhancement",
|
|
1063
|
+
"name": "Param Override",
|
|
1064
|
+
"main": {
|
|
1065
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/param-override/main.mjs",
|
|
1066
|
+
"size": 1544,
|
|
1067
|
+
"sha256": "3ee28eda4fbc30d517d9b97ebd4c86fc4c8c809f83b2b80170cec07a5c97c86d"
|
|
1068
|
+
},
|
|
1069
|
+
"description": "Caps max_tokens (and max_completion_tokens) on every outgoing body and can drop temperature — for gateways that reject large or hot requests.",
|
|
1070
|
+
"author": "OFM Extensions maintainers",
|
|
1071
|
+
"license": "MIT",
|
|
1072
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
1073
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
1074
|
+
"icon": "",
|
|
1075
|
+
"minHost": "2.8.1",
|
|
1076
|
+
"permissions": {
|
|
1077
|
+
"network": [],
|
|
1078
|
+
"secrets": [],
|
|
1079
|
+
"storage": false
|
|
1080
|
+
},
|
|
1081
|
+
"cover": {
|
|
1082
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/param-override/cover.svg",
|
|
1083
|
+
"size": 838,
|
|
1084
|
+
"sha256": "441c4cc182acfa8fbbc4b5870a62dff6cd7cbd70e2a8ed753a9e8ae88f05f9ec"
|
|
1085
|
+
},
|
|
1086
|
+
"hooks": [
|
|
1087
|
+
"onRequest"
|
|
1088
|
+
],
|
|
1089
|
+
"config": [
|
|
1090
|
+
{
|
|
1091
|
+
"key": "maxTokens",
|
|
1092
|
+
"type": "number",
|
|
1093
|
+
"label": "max_tokens cap",
|
|
1094
|
+
"description": "Outgoing max_tokens / max_completion_tokens above this cap are lowered to it. Values the gateway already accepts are left alone.",
|
|
1095
|
+
"default": 8192
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"key": "dropTemperature",
|
|
1099
|
+
"type": "toggle",
|
|
1100
|
+
"label": "Drop temperature",
|
|
1101
|
+
"description": "Remove the temperature field from every outgoing body. For servers that reject sampling options.",
|
|
1102
|
+
"default": false
|
|
1103
|
+
}
|
|
1104
|
+
],
|
|
1105
|
+
"summary": "Caps outgoing max_tokens and optionally strips temperature from every body — fail-open when disabled.",
|
|
1106
|
+
"tags": [
|
|
1107
|
+
"body",
|
|
1108
|
+
"parameters",
|
|
1109
|
+
"rewrite"
|
|
1110
|
+
],
|
|
1111
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
1112
|
+
"changelog": [
|
|
1113
|
+
"1.0.0 — initial release: max_tokens cap with optional temperature drop"
|
|
1114
|
+
]
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
"schemaVersion": 1,
|
|
1118
|
+
"id": "system-prompt",
|
|
1119
|
+
"version": "1.0.0",
|
|
1120
|
+
"type": "enhancement",
|
|
1121
|
+
"name": "System Prompt",
|
|
1122
|
+
"main": {
|
|
1123
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/system-prompt/main.mjs",
|
|
1124
|
+
"size": 2589,
|
|
1125
|
+
"sha256": "c11337c14c8077fcf23e4bf83af1d94774a88b3f6a95424709404d93d3fb79e8"
|
|
1126
|
+
},
|
|
1127
|
+
"description": "Prepends one system prompt to every outgoing turn — merged into the chat system message, the Claude system string, or the responses instructions field, per wire.",
|
|
1128
|
+
"author": "OFM Extensions maintainers",
|
|
1129
|
+
"license": "MIT",
|
|
1130
|
+
"homepage": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
1131
|
+
"repository": "https://github.com/Ebony-Vinyl/ofm-extensions",
|
|
1132
|
+
"icon": "",
|
|
1133
|
+
"minHost": "2.8.1",
|
|
1134
|
+
"permissions": {
|
|
1135
|
+
"network": [],
|
|
1136
|
+
"secrets": [],
|
|
1137
|
+
"storage": false
|
|
1138
|
+
},
|
|
1139
|
+
"cover": {
|
|
1140
|
+
"url": "https://raw.githubusercontent.com/Ebony-Vinyl/ofm-extensions/main/packages/system-prompt/cover.svg",
|
|
1141
|
+
"size": 838,
|
|
1142
|
+
"sha256": "65ab2c703a0c5ab203464a7e77261af6cbef4412086425fd9bf11f54773b22e1"
|
|
1143
|
+
},
|
|
1144
|
+
"hooks": [
|
|
1145
|
+
"onRequest"
|
|
1146
|
+
],
|
|
1147
|
+
"config": [
|
|
1148
|
+
{
|
|
1149
|
+
"key": "prompt",
|
|
1150
|
+
"type": "text",
|
|
1151
|
+
"label": "System prompt",
|
|
1152
|
+
"description": "Prepended to every outgoing turn. Empty disables the enhancement. Merged with any system prompt the lane itself sets.",
|
|
1153
|
+
"default": ""
|
|
1154
|
+
}
|
|
1155
|
+
],
|
|
1156
|
+
"summary": "Prepends one configurable system prompt to every outgoing turn, across the chat, messages and responses wires.",
|
|
1157
|
+
"tags": [
|
|
1158
|
+
"prompt",
|
|
1159
|
+
"rewrite",
|
|
1160
|
+
"system"
|
|
1161
|
+
],
|
|
1162
|
+
"publishedAt": "2026-11-07T00:00:00.000Z",
|
|
1163
|
+
"changelog": [
|
|
1164
|
+
"1.0.0 — initial release: one system prompt prepended across the chat, messages and responses wires"
|
|
1165
|
+
]
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function pageBridgeSource() {
|
|
1171
|
+
return `(() => {
|
|
1172
|
+
const pending = new Map()
|
|
1173
|
+
let sequence = 0
|
|
1174
|
+
const themeHandlers = new Set()
|
|
1175
|
+
const reply = (event) => {
|
|
1176
|
+
const data = event && event.data
|
|
1177
|
+
if (!data || typeof data !== 'object' || data.__ofm !== 1) return
|
|
1178
|
+
if (data.type === 'rpc-reply') {
|
|
1179
|
+
const entry = pending.get(data.id)
|
|
1180
|
+
if (entry === undefined) return
|
|
1181
|
+
clearTimeout(entry.timer)
|
|
1182
|
+
pending.delete(data.id)
|
|
1183
|
+
if (data.ok) entry.resolve(data.result)
|
|
1184
|
+
else entry.reject(new Error(String(data.error || 'host call failed')))
|
|
1185
|
+
return
|
|
1186
|
+
}
|
|
1187
|
+
if (data.type === 'theme') {
|
|
1188
|
+
const tokens = data.tokens && typeof data.tokens === 'object' ? data.tokens : {}
|
|
1189
|
+
for (const key of Object.keys(tokens)) {
|
|
1190
|
+
if (/^--[\\w-]+$/.test(key) && typeof tokens[key] === 'string') {
|
|
1191
|
+
document.documentElement.style.setProperty(key, tokens[key])
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
document.documentElement.dataset.theme = data.theme === 'dark' ? 'dark' : 'light'
|
|
1195
|
+
for (const handler of themeHandlers) {
|
|
1196
|
+
try { handler(data) } catch {}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
window.addEventListener('message', reply)
|
|
1201
|
+
const call = (method, params) => new Promise((resolve, reject) => {
|
|
1202
|
+
const id = ++sequence
|
|
1203
|
+
const timer = setTimeout(() => {
|
|
1204
|
+
pending.delete(id)
|
|
1205
|
+
reject(new Error('host call timed out'))
|
|
1206
|
+
}, 20000)
|
|
1207
|
+
pending.set(id, { resolve, reject, timer })
|
|
1208
|
+
parent.postMessage({ __ofm: 1, type: 'rpc', id, method, params }, '*')
|
|
1209
|
+
})
|
|
1210
|
+
const storage = {
|
|
1211
|
+
get: key => call('storage.get', { key: String(key) }),
|
|
1212
|
+
set: (key, value) => call('storage.set', { key: String(key), value }),
|
|
1213
|
+
delete: key => call('storage.delete', { key: String(key) }),
|
|
1214
|
+
}
|
|
1215
|
+
window.ofm = {
|
|
1216
|
+
plugin: { info: () => call('plugin.info', {}) },
|
|
1217
|
+
config: { get: () => call('config.get', {}) },
|
|
1218
|
+
storage,
|
|
1219
|
+
http: {
|
|
1220
|
+
fetch: (url, init) => call('http.fetch', {
|
|
1221
|
+
url: String(url),
|
|
1222
|
+
method: init && init.method ? String(init.method) : 'GET',
|
|
1223
|
+
headers: init && init.headers ? init.headers : {},
|
|
1224
|
+
body: init ? init.body : undefined,
|
|
1225
|
+
}),
|
|
1226
|
+
},
|
|
1227
|
+
notify: message => parent.postMessage({ __ofm: 1, type: 'notify', message: String(message).slice(0, 200) }, '*'),
|
|
1228
|
+
resize: height => parent.postMessage({ __ofm: 1, type: 'resize', height: Number(height) || 0 }, '*'),
|
|
1229
|
+
onTheme: handler => {
|
|
1230
|
+
if (typeof handler !== 'function') return () => {}
|
|
1231
|
+
themeHandlers.add(handler)
|
|
1232
|
+
return () => themeHandlers.delete(handler)
|
|
1233
|
+
},
|
|
1234
|
+
ready: Promise.resolve(),
|
|
1235
|
+
}
|
|
1236
|
+
parent.postMessage({ __ofm: 1, type: 'ready' }, '*')
|
|
1237
|
+
})()
|
|
1238
|
+
`
|
|
1239
|
+
}
|
|
1240
|
+
|