dsh-plugin-panel 0.0.0-stage → 0.1.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,3 @@
1
+ - insert:
2
+ - id: dsh-plugin-panel
3
+ name: dsh-plugin-panel
package/docs/DETAIL.md ADDED
@@ -0,0 +1,7 @@
1
+ Open Plugin manager in the right sidebar.
2
+
3
+ Filter by Installed, Built-in, or All. Plugins with a right-sidebar page come first, then Settings, then the left menu. Plugins with no UI entry are last.
4
+
5
+ Pin moves a plugin to the front. Open switches to that plugin's right-sidebar page when it has one. Select a card to read its introduction, package, version, source, and these notes.
6
+
7
+ DSH does not record plugin call history.
@@ -0,0 +1,7 @@
1
+ 在右侧栏打开「插件管理」。
2
+
3
+ 可以按「用户安装」「系统内置」「全部」筛选。有右侧栏页面的插件排在前面,然后是设置页、左侧菜单,没有界面入口的放在最后。
4
+
5
+ 点「置顶」把插件放到最前,再点「取消置顶」恢复。有右侧栏页面的插件可以点「打开」,会切到那个页面。点卡片查看介绍、包名、版本、来源和这篇说明。
6
+
7
+ 系统没有插件调用记录。纯界面插件不会出现在工具列表里。
Binary file
Binary file
package/icon.svg ADDED
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#1b2430"/>
3
+ <ellipse cx="24" cy="30" rx="12" ry="3.2" fill="#0b1016" opacity="0.45"/>
4
+ <g transform="rotate(-11 22 18)">
5
+ <rect x="8" y="9" width="28" height="18" rx="4" fill="#f6f1e7"/>
6
+ <rect x="12" y="14" width="12" height="2.4" rx="1.2" fill="#1b2430" opacity="0.72"/>
7
+ <rect x="12" y="18.4" width="18" height="1.6" rx="0.8" fill="#1b2430" opacity="0.28"/>
8
+ </g>
9
+ <rect x="26" y="30" width="30" height="20" rx="4.5" fill="#f7f3ea"/>
10
+ <rect x="31" y="36" width="13" height="2.6" rx="1.3" fill="#1b2430" opacity="0.75"/>
11
+ <rect x="31" y="41" width="19" height="1.7" rx="0.8" fill="#1b2430" opacity="0.28"/>
12
+ <circle cx="49" cy="40" r="2.3" fill="#2fbe86"/>
13
+ </svg>
package/index.js ADDED
@@ -0,0 +1,371 @@
1
+ import { createRequire } from 'node:module'
2
+ import { spawn } from 'node:child_process'
3
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const CHANNEL = '/dsh-plugins-ui-rpc'
8
+ const MAX_SOURCE_BYTES = 1_200_000
9
+ const MAX_FILES = 40
10
+ const MAX_DOC_CHARS = 2400
11
+ const MAX_COUNT_BATCH = 40
12
+ const moduleTools = new Map()
13
+
14
+ export function apply(ctx) {
15
+ ctx.inject(['connection', 'webServer', 'tools', 'pluginManager'], (scoped) => {
16
+ const tools = scoped.tools
17
+ const pluginManager = scoped.pluginManager
18
+ scoped.effect(() => registerDetailRpc(scoped, { tools, pluginManager }), 'plugin-canvas: detail rpc')
19
+ })
20
+ }
21
+
22
+ function registerDetailRpc(ctx, services) {
23
+ return ctx.webServer.register({
24
+ kind: 'prefix',
25
+ path: CHANNEL,
26
+ handler: async (req, res) => {
27
+ const rejection = ctx.connection.requestRejection(req)
28
+ if (rejection !== undefined) {
29
+ res.statusCode = rejection
30
+ res.end()
31
+ return
32
+ }
33
+ if (req.method !== 'POST') {
34
+ res.statusCode = 404
35
+ res.end('not found')
36
+ return
37
+ }
38
+ const mediaType = String(req.headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase()
39
+ if (mediaType !== 'application/json') {
40
+ res.statusCode = 415
41
+ res.end('content type must be application/json')
42
+ return
43
+ }
44
+ let body
45
+ try {
46
+ body = await readJson(req)
47
+ } catch (error) {
48
+ res.statusCode = 400
49
+ res.end(error instanceof Error ? error.message : 'invalid body')
50
+ return
51
+ }
52
+ const record = body && typeof body === 'object' ? body : {}
53
+ const rpcId = typeof record.rpcId === 'string' ? record.rpcId : ''
54
+ const method = typeof record.method === 'string' ? record.method : ''
55
+ if (!rpcId || !method) {
56
+ res.statusCode = 400
57
+ res.end('invalid client-request envelope')
58
+ return
59
+ }
60
+ let result
61
+ try {
62
+ if (method === 'detail') result = { ok: true, value: await readDetail(services, record.payload) }
63
+ else if (method === 'counts') result = { ok: true, value: countTools(services, record.payload) }
64
+ else if (method === 'facts') result = { ok: true, value: readFacts(record.payload) }
65
+ else if (method === 'reveal') result = { ok: true, value: revealPackage(record.payload) }
66
+ else throw new Error('Unknown detail method.')
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error)
69
+ result = { ok: false, error: { code: 'plugin-canvas/detail', message: message.slice(0, 500), details: {} } }
70
+ }
71
+ res.statusCode = 200
72
+ res.setHeader('content-type', 'application/json; charset=utf-8')
73
+ res.setHeader('cache-control', 'no-store')
74
+ res.end(JSON.stringify({ type: 'server-response', rpcId, result }))
75
+ },
76
+ })
77
+ }
78
+
79
+ async function readDetail(services, payload) {
80
+ const input = payload && typeof payload === 'object' ? payload : {}
81
+ const moduleName = typeof input.moduleName === 'string' ? input.moduleName : ''
82
+ const entryId = typeof input.entryId === 'string' ? input.entryId : ''
83
+ const language = typeof input.language === 'string' ? input.language : 'zh'
84
+ const tools = toolsForModule(listTools(services.tools), moduleName)
85
+ const located = locatePackage(moduleName)
86
+ const manifest = located ? readManifest(located.root) : null
87
+ const bundle = await findBundle(services.pluginManager, moduleName, entryId, manifest && manifest.name)
88
+ const document = await readDocument(services.pluginManager, bundle && bundle.name, language)
89
+ return {
90
+ summary: manifest && manifest.summary ? manifest.summary : '',
91
+ packageDescription: manifest && manifest.description ? manifest.description : '',
92
+ bundleName: bundle && bundle.name ? bundle.name : '',
93
+ version: bundle && bundle.version ? bundle.version : (manifest && manifest.version) || '',
94
+ source: bundle && bundle.source ? bundle.source : '',
95
+ official: bundle ? bundle.official === true : false,
96
+ document,
97
+ tools,
98
+ toolNote: tools.length ? '' : '没有在这个插件的代码里对上已注册、会交给 AI 的工具。',
99
+ }
100
+ }
101
+
102
+ function revealPackage(payload) {
103
+ const input = payload && typeof payload === 'object' ? payload : {}
104
+ const moduleName = typeof input.moduleName === 'string' ? input.moduleName : ''
105
+ const source = typeof input.source === 'string' ? input.source : ''
106
+ const located = locatePackage(moduleName)
107
+ const dir = (located && located.root) || directoryFromSource(source)
108
+ if (!dir) throw new Error('找不到这个插件的目录。')
109
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open'
110
+ const child = spawn(command, [dir], { detached: true, stdio: 'ignore' })
111
+ child.on('error', () => {})
112
+ child.unref()
113
+ return { path: dir }
114
+ }
115
+
116
+ function directoryFromSource(source) {
117
+ const raw = String(source || '')
118
+ const at = raw.lastIndexOf('@')
119
+ const text = (at >= 0 ? raw.slice(at + 1) : raw).replace(/^link:/, '')
120
+ if (!text || text.startsWith('http')) return ''
121
+ try {
122
+ const file = text.startsWith('file:') ? fileURLToPath(text) : text
123
+ if (!file.startsWith('/')) return ''
124
+ const stat = statSync(file)
125
+ const dir = stat.isDirectory() ? file : path.dirname(file)
126
+ return packageRoot(dir)
127
+ } catch {
128
+ return ''
129
+ }
130
+ }
131
+
132
+ function readFacts(payload) {
133
+ const input = payload && typeof payload === 'object' ? payload : {}
134
+ const moduleName = typeof input.moduleName === 'string' ? input.moduleName : ''
135
+ const source = typeof input.source === 'string' ? input.source : ''
136
+ const located = locatePackage(moduleName)
137
+ const root = (located && located.root) || directoryFromSource(source)
138
+ const manifest = root ? readManifest(root) : null
139
+ const document = root ? readLocalDocument(root) : ''
140
+ if (!manifest && !document) return { manifest: null, document: '' }
141
+ return {
142
+ document,
143
+ manifest: manifest && {
144
+ name: manifest.name,
145
+ version: manifest.version,
146
+ description: manifest.description,
147
+ summary: manifest.summary,
148
+ license: manifest.license,
149
+ homepage: manifest.homepage,
150
+ repository: manifest.repository,
151
+ keywords: manifest.keywords,
152
+ engines: manifest.engines,
153
+ clientPlatform: manifest.clientPlatform,
154
+ clientInject: manifest.clientInject,
155
+ },
156
+ }
157
+ }
158
+
159
+ function countTools(services, payload) {
160
+ const input = payload && typeof payload === 'object' ? payload : {}
161
+ const plugins = Array.isArray(input.plugins) ? input.plugins.slice(0, MAX_COUNT_BATCH) : []
162
+ const all = listTools(services.tools)
163
+ const counts = {}
164
+ for (const plugin of plugins) {
165
+ if (!plugin || typeof plugin.entryId !== 'string' || !plugin.entryId) continue
166
+ const moduleName = typeof plugin.moduleName === 'string' ? plugin.moduleName : ''
167
+ counts[plugin.entryId] = toolsForModule(all, moduleName).length
168
+ }
169
+ return { counts }
170
+ }
171
+
172
+ function toolsForModule(allTools, moduleName) {
173
+ const key = moduleName || ''
174
+ if (moduleTools.has(key)) return moduleTools.get(key)
175
+ const located = locatePackage(key)
176
+ const source = located ? readPackageSource(located.root, located.entryFile) : ''
177
+ const manifest = located ? readManifest(located.root) : null
178
+ const tools = ownedTools(allTools, source, manifest && manifest.name)
179
+ moduleTools.set(key, tools)
180
+ return tools
181
+ }
182
+
183
+ function listTools(tools) {
184
+ if (!tools || typeof tools.definitions !== 'function') return []
185
+ const definitions = tools.definitions()
186
+ if (!Array.isArray(definitions)) return []
187
+ return definitions.map((tool) => ({
188
+ name: typeof tool.name === 'string' ? tool.name : '',
189
+ description: typeof tool.description === 'string' ? tool.description : '',
190
+ parameters: summarizeParams(tool.parameters),
191
+ })).filter((tool) => tool.name)
192
+ }
193
+
194
+ function summarizeParams(parameters) {
195
+ if (!parameters || typeof parameters !== 'object') return []
196
+ const props = parameters.properties && typeof parameters.properties === 'object'
197
+ ? parameters.properties
198
+ : null
199
+ if (!props) return []
200
+ return Object.entries(props).slice(0, 16).map(([name, schema]) => ({
201
+ name,
202
+ type: schema && typeof schema.type === 'string' ? schema.type : '',
203
+ description: schema && typeof schema.description === 'string' ? schema.description : '',
204
+ }))
205
+ }
206
+
207
+ function ownedTools(tools, source, packageName) {
208
+ if (!source) return []
209
+ const prefix = packagePrefix(packageName)
210
+ return tools.filter((tool) => {
211
+ if (source.includes(`'${tool.name}'`) || source.includes(`"${tool.name}"`) || source.includes(`\`${tool.name}\``)) return true
212
+ return prefix && (tool.name === prefix || tool.name.startsWith(`${prefix}_`) || tool.name.startsWith(`${prefix}-`))
213
+ })
214
+ }
215
+
216
+ function packagePrefix(name) {
217
+ if (!name) return ''
218
+ const bare = name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name
219
+ return bare.replace(/^dsh-/, '').replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '').toLowerCase()
220
+ }
221
+
222
+ async function findBundle(pluginManager, moduleName, entryId, packageName) {
223
+ if (!pluginManager || typeof pluginManager.listBundles !== 'function') return null
224
+ const bundles = await pluginManager.listBundles()
225
+ if (!Array.isArray(bundles)) return null
226
+ return bundles.find((bundle) => {
227
+ if (packageName && bundle.name === packageName) return true
228
+ if (moduleName && bundle.name === moduleName) return true
229
+ const rows = Array.isArray(bundle.rows) ? bundle.rows : []
230
+ return rows.some((row) => row.moduleName === moduleName || (entryId && row.entryId === entryId))
231
+ }) || null
232
+ }
233
+
234
+ async function readDocument(pluginManager, name, language) {
235
+ if (!name || !pluginManager || typeof pluginManager.readBundleDetail !== 'function') return ''
236
+ try {
237
+ const detail = await pluginManager.readBundleDetail(name, language)
238
+ if (!detail || detail.status !== 'found' || typeof detail.markdown !== 'string') return ''
239
+ return detail.markdown.replace(/^---[\s\S]*?---\s*/, '').trim().slice(0, MAX_DOC_CHARS)
240
+ } catch {
241
+ return ''
242
+ }
243
+ }
244
+
245
+ function locatePackage(moduleName) {
246
+ if (!moduleName) return null
247
+ if (moduleName.startsWith('file:')) {
248
+ const entryFile = fileURLToPath(moduleName)
249
+ return { entryFile, root: packageRoot(path.dirname(entryFile)) }
250
+ }
251
+ const profile = process.env.DSH_PROFILE_DIR || path.join(process.env.HOME || '', '.dsh/profiles/desktop')
252
+ try {
253
+ const req = createRequire(path.join(profile, 'package.json'))
254
+ const resolved = req.resolve(moduleName)
255
+ return { entryFile: resolved, root: packageRoot(path.dirname(resolved)) }
256
+ } catch {
257
+ try {
258
+ const direct = path.join(profile, 'node_modules', moduleName)
259
+ if (statSync(path.join(direct, 'package.json')).isFile()) return { entryFile: '', root: direct }
260
+ } catch { /* not installed under that folder name */ }
261
+ return null
262
+ }
263
+ }
264
+
265
+ function packageRoot(start) {
266
+ let dir = start
267
+ for (let i = 0; i < 8; i += 1) {
268
+ try {
269
+ if (statSync(path.join(dir, 'package.json')).isFile()) return dir
270
+ } catch { /* keep walking */ }
271
+ const parent = path.dirname(dir)
272
+ if (parent === dir) break
273
+ dir = parent
274
+ }
275
+ return start
276
+ }
277
+
278
+ function readManifest(root) {
279
+ try {
280
+ const manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
281
+ const hub = manifest.dsh && manifest.dsh.hub
282
+ const client = manifest.dsh && manifest.dsh.client
283
+ return {
284
+ name: typeof manifest.name === 'string' ? manifest.name : '',
285
+ version: typeof manifest.version === 'string' ? manifest.version : '',
286
+ description: typeof manifest.description === 'string' ? manifest.description : '',
287
+ summary: hub && typeof hub.summary === 'string' ? hub.summary : '',
288
+ license: typeof manifest.license === 'string' ? manifest.license : '',
289
+ homepage: typeof manifest.homepage === 'string' ? manifest.homepage : '',
290
+ repository: repositoryOf(manifest.repository),
291
+ keywords: Array.isArray(manifest.keywords) ? manifest.keywords.filter((item) => typeof item === 'string').slice(0, 12) : [],
292
+ engines: manifest.engines && typeof manifest.engines.dsh === 'string' ? manifest.engines.dsh : '',
293
+ clientPlatform: client && typeof client.platform === 'string' ? client.platform : '',
294
+ clientInject: client && Array.isArray(client.inject) ? client.inject.filter((item) => typeof item === 'string').slice(0, 12) : [],
295
+ }
296
+ } catch {
297
+ return null
298
+ }
299
+ }
300
+
301
+ function readLocalDocument(root) {
302
+ const names = ['docs/DETAIL.zh.md', 'docs/DETAIL.md', 'README.md', 'README.zh.md']
303
+ for (const name of names) {
304
+ try {
305
+ const text = readFileSync(path.join(root, name), 'utf8').replace(/^---[\s\S]*?---\s*/, '').trim()
306
+ if (text) return text.slice(0, 8000)
307
+ } catch { /* try the next name */ }
308
+ }
309
+ return ''
310
+ }
311
+
312
+ function repositoryOf(value) {
313
+ if (typeof value === 'string') return value
314
+ if (value && typeof value.url === 'string') return value.url
315
+ return ''
316
+ }
317
+
318
+ function readPackageSource(root, entryFile) {
319
+ const files = []
320
+ if (entryFile) files.push(entryFile)
321
+ collectJs(root, files, 0)
322
+ let text = ''
323
+ const seen = new Set()
324
+ for (const file of files) {
325
+ if (seen.has(file) || text.length >= MAX_SOURCE_BYTES) continue
326
+ seen.add(file)
327
+ try {
328
+ const chunk = readFileSync(file, 'utf8')
329
+ text += `\n${chunk.slice(0, 180_000)}`
330
+ } catch { /* unreadable file is skipped */ }
331
+ }
332
+ return text
333
+ }
334
+
335
+ function collectJs(dir, files, depth) {
336
+ if (depth > 3 || files.length >= MAX_FILES) return
337
+ let entries
338
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
339
+ for (const entry of entries) {
340
+ if (files.length >= MAX_FILES) return
341
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
342
+ const full = path.join(dir, entry.name)
343
+ if (entry.isDirectory()) collectJs(full, files, depth + 1)
344
+ else if (/\.(js|mjs|cjs|ts)$/.test(entry.name)) files.push(full)
345
+ }
346
+ }
347
+
348
+ function readJson(req) {
349
+ return new Promise((resolve, reject) => {
350
+ const chunks = []
351
+ let size = 0
352
+ req.on('data', (chunk) => {
353
+ size += chunk.length
354
+ if (size > 64_000) {
355
+ reject(new Error('Request body is too large.'))
356
+ req.destroy()
357
+ return
358
+ }
359
+ chunks.push(chunk)
360
+ })
361
+ req.on('end', () => {
362
+ const text = Buffer.concat(chunks).toString('utf8').trim()
363
+ if (!text) {
364
+ resolve({})
365
+ return
366
+ }
367
+ try { resolve(JSON.parse(text)) } catch { reject(new Error('Request body is not valid JSON.')) }
368
+ })
369
+ req.on('error', reject)
370
+ })
371
+ }
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "Plugin manager",
4
+ "description": "Browse and open plugins in the right sidebar.",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "插件管理",
4
+ "description": "在右侧栏查看并打开插件。",
5
+ "detail": "docs/DETAIL.zh.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "dsh-plugin-panel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Plugin manager for the DeepSeek Harness right sidebar: list installed plugins, see where each one lives, pin favourites, and open a plugin folder in Finder.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./client": "./client.js",
9
+ "./locale/*.json": "./locale/*.json",
10
+ "./icon": "./icon.svg"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "client.js",
15
+ "catalog.js",
16
+ "catalog.test.mjs",
17
+ "cordis.patch.yml",
18
+ "locale/*.json",
19
+ "docs",
20
+ "icon.svg",
21
+ "README.md"
22
+ ],
23
+ "dsh": {
24
+ "bundle": {
25
+ "patch": "./cordis.patch.yml"
26
+ },
27
+ "client": {
28
+ "platform": "web",
29
+ "inject": [
30
+ "@deepseek-ai/dsh-api-remotes",
31
+ "@deepseek-ai/dsh-client-ui-layout",
32
+ "@deepseek-ai/dsh-client-ui-sidebar",
33
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
34
+ "@deepseek-ai/dsh-client-locale"
35
+ ]
36
+ }
37
+ },
38
+ "engines": {
39
+ "dsh": ">=0.2.2-rc.1"
40
+ },
41
+ "keywords": [
42
+ "dsh",
43
+ "deepseek-harness",
44
+ "dsh-plugin",
45
+ "plugin-manager",
46
+ "sidebar",
47
+ "插件管理"
48
+ ],
49
+ "author": {
50
+ "name": "jqlong17",
51
+ "url": "https://github.com/jqlong17"
52
+ },
53
+ "license": "MIT",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/jqlong17/dsh-plugins-ui.git"
57
+ },
58
+ "homepage": "https://github.com/jqlong17/dsh-plugins-ui#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/jqlong17/dsh-plugins-ui/issues"
61
+ }
62
+ }