dsh-cmdk 0.0.0-stage → 0.3.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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +125 -2
- package/client.js +2263 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +16 -0
- package/index.js +443 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/screenshot-actions.png +0 -0
- package/screenshot-ext.png +0 -0
- package/screenshot.png +0 -0
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#5B6CF9"/>
|
|
5
|
+
<stop offset="1" stop-color="#8E5BF6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="0" y="0" width="64" height="64" rx="15" fill="url(#g)"/>
|
|
9
|
+
<rect x="13" y="15" width="38" height="26" rx="7" fill="#FFFFFF" fill-opacity="0.96"/>
|
|
10
|
+
<rect x="19" y="23" width="14" height="2.6" rx="1.3" fill="#5B6CF9"/>
|
|
11
|
+
<rect x="19" y="29" width="22" height="2.6" rx="1.3" fill="#C9CEF8"/>
|
|
12
|
+
<rect x="19" y="35" width="9" height="2.6" rx="1.3" fill="#C9CEF8"/>
|
|
13
|
+
<circle cx="43" cy="43" r="8.5" fill="#FFFFFF" fill-opacity="0.96"/>
|
|
14
|
+
<circle cx="43" cy="43" r="5" stroke="#5B6CF9" stroke-width="2.4"/>
|
|
15
|
+
<rect x="46.6" y="47.4" width="7" height="2.6" rx="1.3" transform="rotate(45 46.6 47.4)" fill="#5B6CF9"/>
|
|
16
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-cmdk — host half.
|
|
3
|
+
*
|
|
4
|
+
* The palette itself is client-only (navigation, theme and workspace state all
|
|
5
|
+
* live on the Client side). The Host half exists for the things a browser
|
|
6
|
+
* sandbox cannot reach:
|
|
7
|
+
*
|
|
8
|
+
* GET /cmdk/info → where this profile lives (home + profile dir)
|
|
9
|
+
* GET /cmdk/sessions?q= → full session catalogue + message-content hits
|
|
10
|
+
* POST /cmdk/open-profile → reveal the profile directory in Finder/Explorer
|
|
11
|
+
* GET /cmdk/plugins → installed bundles (name, version, enabled, dir)
|
|
12
|
+
* POST /cmdk/plugin → enable/disable one bundle via pluginManager
|
|
13
|
+
*
|
|
14
|
+
* Every route goes through the connection trust fence first, so a random page
|
|
15
|
+
* cannot probe the host filesystem through the loopback port.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
export const name = 'dsh-cmdk';
|
|
23
|
+
export const inject = ['webServer'];
|
|
24
|
+
|
|
25
|
+
const ROUTE = '/cmdk';
|
|
26
|
+
|
|
27
|
+
/** How long the session title catalogue stays warm between keystrokes. */
|
|
28
|
+
const CATALOG_TTL = 20_000;
|
|
29
|
+
const CATALOG_SIZE = 120;
|
|
30
|
+
/** How many of the newest sessions the content scan looks at per query. */
|
|
31
|
+
const CONTENT_SCAN_SIZE = 12;
|
|
32
|
+
|
|
33
|
+
let catalogCache = { at: 0, items: [] };
|
|
34
|
+
/** Query → content hits, so held-down keys do not rescan the logs every 180 ms. */
|
|
35
|
+
const contentCache = new Map();
|
|
36
|
+
const CONTENT_TTL = 60_000;
|
|
37
|
+
|
|
38
|
+
function dshHome() {
|
|
39
|
+
const fromEnv = process.env.DSH_HOME;
|
|
40
|
+
return fromEnv && fromEnv.trim() ? fromEnv : path.join(os.homedir(), '.dsh');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Best-effort running profile directory (same order as the rest of the UI): env dir → env name → desktop. */
|
|
44
|
+
function profileDir() {
|
|
45
|
+
const dir = process.env.DSH_PROFILE_DIR;
|
|
46
|
+
if (dir && dir.trim()) return dir;
|
|
47
|
+
const profile = process.env.DSH_PROFILE;
|
|
48
|
+
if (profile && profile.trim()) return path.join(dshHome(), 'profiles', profile);
|
|
49
|
+
return path.join(dshHome(), 'profiles', 'desktop');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sendJson(res, code, payload) {
|
|
53
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
54
|
+
res.writeHead(code, {
|
|
55
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
56
|
+
'Cache-Control': 'no-store',
|
|
57
|
+
'Content-Length': String(body.length),
|
|
58
|
+
});
|
|
59
|
+
res.end(body);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readBody(req, limit = 16 * 1024) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let size = 0;
|
|
65
|
+
const chunks = [];
|
|
66
|
+
req.on('data', (chunk) => {
|
|
67
|
+
size += chunk.length;
|
|
68
|
+
if (size > limit) {
|
|
69
|
+
req.destroy();
|
|
70
|
+
resolve('');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
chunks.push(chunk);
|
|
74
|
+
});
|
|
75
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
76
|
+
req.on('error', () => resolve(''));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Returns true when the request was already rejected (and answered). */
|
|
81
|
+
function refused(ctx, req, res) {
|
|
82
|
+
const conn = ctx.get('connection') || ctx.connection;
|
|
83
|
+
if (!conn || typeof conn.requestRejection !== 'function') return false;
|
|
84
|
+
const code = conn.requestRejection({ headers: req.headers });
|
|
85
|
+
if (code === undefined || code === null || code === false) return false;
|
|
86
|
+
res.statusCode = typeof code === 'number' ? code : 403;
|
|
87
|
+
res.end();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function openInFileManager(dir) {
|
|
92
|
+
const [command, args] =
|
|
93
|
+
process.platform === 'darwin'
|
|
94
|
+
? ['open', [dir]]
|
|
95
|
+
: process.platform === 'win32'
|
|
96
|
+
? ['explorer', [dir]]
|
|
97
|
+
: ['xdg-open', [dir]];
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
execFile(command, args, (error) => (error ? reject(error) : resolve()));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Session ids are brand types at the type level but plain strings at runtime. */
|
|
104
|
+
function idOf(record) {
|
|
105
|
+
return record && record.header && record.header.id ? String(record.header.id) : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Fold titles for a batch of ids; missing titles come back as ''. */
|
|
109
|
+
async function titlesFor(query, ids) {
|
|
110
|
+
const out = new Map();
|
|
111
|
+
if (!ids.length || typeof query.readTitleSnapshots !== 'function') return out;
|
|
112
|
+
let results = [];
|
|
113
|
+
try {
|
|
114
|
+
results = await query.readTitleSnapshots(ids);
|
|
115
|
+
} catch {
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
for (const result of results || []) {
|
|
119
|
+
if (!result || result.status !== 'fulfilled' || !result.value || !result.value.title) continue;
|
|
120
|
+
const title = result.value.title.title;
|
|
121
|
+
if (typeof title === 'string' && title.trim()) out.set(String(result.sessionId), title.trim());
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Human-facing sessions: no subagent branches, no seeded placeholders, and
|
|
128
|
+
* never more than CATALOG_SIZE entries. Cached briefly so typing in the
|
|
129
|
+
* palette does not re-read every log on each keystroke.
|
|
130
|
+
*/
|
|
131
|
+
async function sessionCatalog(ctx) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (now - catalogCache.at < CATALOG_TTL && catalogCache.items.length) return catalogCache.items;
|
|
134
|
+
|
|
135
|
+
const query = ctx.get('sessionQuery');
|
|
136
|
+
if (!query || typeof query.listSessions !== 'function') return [];
|
|
137
|
+
|
|
138
|
+
let records = [];
|
|
139
|
+
try {
|
|
140
|
+
records = await query.listSessions();
|
|
141
|
+
} catch {
|
|
142
|
+
return catalogCache.items;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const picked = (records || [])
|
|
146
|
+
.filter((record) => {
|
|
147
|
+
const header = record && record.header;
|
|
148
|
+
if (!header || !header.id) return false;
|
|
149
|
+
if (header.origin === 'subagent' || header.parentSession) return false;
|
|
150
|
+
if (header.isSeeded) return false;
|
|
151
|
+
return true;
|
|
152
|
+
})
|
|
153
|
+
.slice(0, CATALOG_SIZE);
|
|
154
|
+
|
|
155
|
+
const titles = await titlesFor(query, picked.map(idOf).filter(Boolean));
|
|
156
|
+
const items = picked
|
|
157
|
+
.map((record) => {
|
|
158
|
+
const sessionId = idOf(record);
|
|
159
|
+
return {
|
|
160
|
+
sessionId,
|
|
161
|
+
title: titles.get(sessionId) || '',
|
|
162
|
+
cwd: record.header && record.header.cwd ? record.header.cwd : '',
|
|
163
|
+
at: record.header && record.header.createdAt ? record.header.createdAt : 0,
|
|
164
|
+
snippet: '',
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
.filter((item) => item.sessionId);
|
|
168
|
+
|
|
169
|
+
catalogCache = { at: now, items };
|
|
170
|
+
return items;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Content hits. The SQLite full-text index can be empty in the wild (the
|
|
175
|
+
* native session search reports "no matches" on such machines), so this walks
|
|
176
|
+
* the newest sessions and uses the backend-independent `filterEvents` text
|
|
177
|
+
* predicate instead: no index required, just the session logs.
|
|
178
|
+
*/
|
|
179
|
+
async function matchContent(ctx, text, limit) {
|
|
180
|
+
const query = ctx.get('sessionQuery');
|
|
181
|
+
if (!query || typeof query.filterEvents !== 'function') return [];
|
|
182
|
+
if (text.length < 2) return [];
|
|
183
|
+
|
|
184
|
+
const cached = contentCache.get(text);
|
|
185
|
+
if (cached && Date.now() - cached.at < CONTENT_TTL) return cached.items.slice(0, limit);
|
|
186
|
+
const catalog = await sessionCatalog(ctx);
|
|
187
|
+
const scan = catalog.slice(0, CONTENT_SCAN_SIZE);
|
|
188
|
+
|
|
189
|
+
const hits = [];
|
|
190
|
+
for (const item of scan) {
|
|
191
|
+
let documents = [];
|
|
192
|
+
try {
|
|
193
|
+
documents = await query.filterEvents(item.sessionId, [{ kind: 'text', text }]);
|
|
194
|
+
} catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(documents) || !documents.length) continue;
|
|
198
|
+
const doc = documents.find((entry) => entry && typeof entry.text === 'string');
|
|
199
|
+
if (!doc) continue;
|
|
200
|
+
hits.push({
|
|
201
|
+
sessionId: item.sessionId,
|
|
202
|
+
title: item.title,
|
|
203
|
+
cwd: item.cwd,
|
|
204
|
+
at: item.at,
|
|
205
|
+
snippet: snippetAround(doc.text, text),
|
|
206
|
+
});
|
|
207
|
+
if (hits.length >= limit) break;
|
|
208
|
+
}
|
|
209
|
+
if (contentCache.size > 40) contentCache.clear();
|
|
210
|
+
contentCache.set(text, { at: Date.now(), items: hits });
|
|
211
|
+
return hits;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 40 chars either side of the match, whitespace collapsed. */
|
|
215
|
+
function snippetAround(text, needle) {
|
|
216
|
+
const flat = String(text || '').replace(/\s+/g, ' ');
|
|
217
|
+
const at = flat.toLowerCase().indexOf(String(needle).toLowerCase());
|
|
218
|
+
if (at < 0) return flat.slice(0, 120);
|
|
219
|
+
const from = Math.max(0, at - 40);
|
|
220
|
+
const to = Math.min(flat.length, at + needle.length + 60);
|
|
221
|
+
return (from > 0 ? '…' : '') + flat.slice(from, to) + (to < flat.length ? '…' : '');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
function workspaceStorePath() {
|
|
226
|
+
return path.join(dshHome(), 'storages', 'workspace.json');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Workspaces live in the Client store; read the same file without touching it. */
|
|
230
|
+
function readWorkspaces() {
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = JSON.parse(fs.readFileSync(workspaceStorePath(), 'utf8'));
|
|
234
|
+
} catch {
|
|
235
|
+
return { items: [], archived: [] };
|
|
236
|
+
}
|
|
237
|
+
if (!raw || typeof raw !== 'object') return { items: [], archived: [] };
|
|
238
|
+
const global = raw.global && typeof raw.global === 'object' ? raw.global : {};
|
|
239
|
+
const table = (raw.tables && raw.tables.workspaces) || {};
|
|
240
|
+
const items = [];
|
|
241
|
+
for (const id of Array.isArray(global.workspaceIds) ? global.workspaceIds : Object.keys(table)) {
|
|
242
|
+
const row = table[id];
|
|
243
|
+
if (!row) continue;
|
|
244
|
+
items.push({
|
|
245
|
+
workspaceId: id,
|
|
246
|
+
title: typeof row.title === 'string' ? row.title : '',
|
|
247
|
+
path: typeof row.path === 'string' ? row.path : '',
|
|
248
|
+
sessionIds: Array.isArray(row.sessionIds) ? row.sessionIds : [],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
items,
|
|
253
|
+
archived: Array.isArray(global.archivedSessionIds) ? global.archivedSessionIds.slice() : [],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function apply(ctx, config) {
|
|
258
|
+
const server = ctx.get('webServer') || ctx.webServer;
|
|
259
|
+
if (!server || typeof server.register !== 'function') {
|
|
260
|
+
ctx.logger?.warn?.('dsh-cmdk: no webServer carrier, palette routes unavailable');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const disposers = [];
|
|
265
|
+
|
|
266
|
+
disposers.push(
|
|
267
|
+
server.register({
|
|
268
|
+
kind: 'exact',
|
|
269
|
+
path: ROUTE + '/info',
|
|
270
|
+
handler: async (req, res) => {
|
|
271
|
+
if (refused(ctx, req, res)) return;
|
|
272
|
+
sendJson(res, 200, {
|
|
273
|
+
ok: true,
|
|
274
|
+
home: dshHome(),
|
|
275
|
+
profile: profileDir(),
|
|
276
|
+
platform: process.platform,
|
|
277
|
+
arch: process.arch,
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
disposers.push(
|
|
284
|
+
server.register({
|
|
285
|
+
kind: 'exact',
|
|
286
|
+
path: ROUTE + '/sessions',
|
|
287
|
+
handler: async (req, res) => {
|
|
288
|
+
if (refused(ctx, req, res)) return;
|
|
289
|
+
let url;
|
|
290
|
+
try {
|
|
291
|
+
url = new URL(req.url || ROUTE, 'http://localhost');
|
|
292
|
+
} catch {
|
|
293
|
+
sendJson(res, 400, { ok: false, error: 'bad url' });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const text = (url.searchParams.get('q') || '').trim();
|
|
297
|
+
const asked = Number(url.searchParams.get('limit'));
|
|
298
|
+
const limit = Number.isFinite(asked) && asked > 0 ? Math.min(asked, 50) : 20;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const catalog = await sessionCatalog(ctx);
|
|
302
|
+
if (!text) {
|
|
303
|
+
sendJson(res, 200, { ok: true, items: catalog, hits: [], total: catalog.length });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// The client filters titles locally (fuzzy + pinyin); the Host only
|
|
307
|
+
// contributes the expensive part: message-content hits.
|
|
308
|
+
const hits = await matchContent(ctx, text, limit);
|
|
309
|
+
sendJson(res, 200, { ok: true, items: catalog, hits, total: catalog.length });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
disposers.push(
|
|
318
|
+
server.register({
|
|
319
|
+
kind: 'exact',
|
|
320
|
+
path: ROUTE + '/workspaces',
|
|
321
|
+
handler: (req, res) => {
|
|
322
|
+
if (refused(ctx, req, res)) return;
|
|
323
|
+
try {
|
|
324
|
+
const data = readWorkspaces();
|
|
325
|
+
sendJson(res, 200, { ok: true, items: data.items, archived: data.archived });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
disposers.push(
|
|
334
|
+
server.register({
|
|
335
|
+
kind: 'exact',
|
|
336
|
+
path: ROUTE + '/open-profile',
|
|
337
|
+
handler: async (req, res) => {
|
|
338
|
+
if (refused(ctx, req, res)) return;
|
|
339
|
+
if (req.method !== 'POST') {
|
|
340
|
+
sendJson(res, 405, { ok: false, error: 'POST only' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const raw = await readBody(req);
|
|
344
|
+
let target = profileDir();
|
|
345
|
+
if (raw) {
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(raw);
|
|
348
|
+
if (parsed && typeof parsed.path === 'string' && parsed.path.trim()) {
|
|
349
|
+
target = parsed.path.trim();
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
/* fall back to the profile directory */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await openInFileManager(target);
|
|
357
|
+
sendJson(res, 200, { ok: true, path: target });
|
|
358
|
+
} catch (error) {
|
|
359
|
+
sendJson(res, 500, { ok: false, error: String(error && error.message ? error.message : error) });
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
disposers.push(
|
|
366
|
+
server.register({
|
|
367
|
+
kind: 'exact',
|
|
368
|
+
path: ROUTE + '/plugins',
|
|
369
|
+
handler: async (req, res) => {
|
|
370
|
+
if (refused(ctx, req, res)) return;
|
|
371
|
+
try {
|
|
372
|
+
const manager = ctx.get('pluginManager') || ctx.pluginManager;
|
|
373
|
+
const bundles = manager && typeof manager.listBundles === 'function' ? await manager.listBundles() : [];
|
|
374
|
+
const items = bundles
|
|
375
|
+
.filter((bundle) => bundle && bundle.installed && bundle.availability !== 'installation')
|
|
376
|
+
.map((bundle) => ({
|
|
377
|
+
name: bundle.name,
|
|
378
|
+
version: bundle.version || '',
|
|
379
|
+
enabled: !!bundle.enabled,
|
|
380
|
+
dir: path.join(profileDir(), 'node_modules', ...bundle.name.split('/')),
|
|
381
|
+
}))
|
|
382
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
383
|
+
sendJson(res, 200, { ok: true, items });
|
|
384
|
+
} catch (error) {
|
|
385
|
+
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
disposers.push(
|
|
392
|
+
server.register({
|
|
393
|
+
kind: 'exact',
|
|
394
|
+
path: ROUTE + '/plugin',
|
|
395
|
+
handler: async (req, res) => {
|
|
396
|
+
if (refused(ctx, req, res)) return;
|
|
397
|
+
if (req.method !== 'POST') {
|
|
398
|
+
sendJson(res, 405, { ok: false, error: 'POST only' });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const raw = await readBody(req);
|
|
402
|
+
let body = null;
|
|
403
|
+
try {
|
|
404
|
+
body = raw ? JSON.parse(raw) : null;
|
|
405
|
+
} catch {
|
|
406
|
+
sendJson(res, 400, { ok: false, error: 'invalid json' });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const name = body && typeof body.name === 'string' ? body.name.trim() : '';
|
|
410
|
+
if (!name) {
|
|
411
|
+
sendJson(res, 400, { ok: false, error: 'missing name' });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const manager = ctx.get('pluginManager') || ctx.pluginManager;
|
|
415
|
+
if (!manager || typeof manager.setBundleEnabled !== 'function') {
|
|
416
|
+
sendJson(res, 503, { ok: false, error: 'plugin manager unavailable' });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const result = await manager.setBundleEnabled(name, !!body.enabled);
|
|
421
|
+
sendJson(res, 200, {
|
|
422
|
+
ok: !result.error,
|
|
423
|
+
changed: result.changed,
|
|
424
|
+
application: result.application,
|
|
425
|
+
error: result.error ? result.error.code || 'failed' : undefined,
|
|
426
|
+
});
|
|
427
|
+
} catch (error) {
|
|
428
|
+
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
ctx.effect(() => () => {
|
|
435
|
+
for (const dispose of disposers) {
|
|
436
|
+
try {
|
|
437
|
+
dispose();
|
|
438
|
+
} catch {
|
|
439
|
+
/* already gone */
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cmdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "⌘K command palette for DeepSeek Harness: jump to sessions, search message content, switch workspaces, toggle plugins.",
|
|
6
|
+
"icon": "icon.svg",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"command-palette",
|
|
10
|
+
"search",
|
|
11
|
+
"productivity"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./client": "./client.js",
|
|
16
|
+
"./locale/*.json": "./locale/*.json",
|
|
17
|
+
"./icon": "./icon.svg",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"screenshot.png",
|
|
25
|
+
"screenshot-actions.png",
|
|
26
|
+
"screenshot-ext.png",
|
|
27
|
+
"index.js",
|
|
28
|
+
"client.js",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"locale/*.json",
|
|
31
|
+
"icon.svg"
|
|
32
|
+
],
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"platform": "web",
|
|
39
|
+
"immediately": true,
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"author": "mumuhmumu",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/hhse/dsh-cmdk.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/hhse/dsh-cmdk#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/hhse/dsh-cmdk/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
Binary file
|
|
Binary file
|
package/screenshot.png
ADDED
|
Binary file
|