dsh-workspace-kit 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.
package/lib/index.js ADDED
@@ -0,0 +1,259 @@
1
+ // src/shared/i18n.ts
2
+ function localize(locale, zh, en, vars) {
3
+ const template = locale === "zh" ? zh : en;
4
+ if (!vars) return template;
5
+ return template.replace(
6
+ /\{(\w+)\}/g,
7
+ (raw, name2) => vars[name2] !== void 0 ? String(vars[name2]) : raw
8
+ );
9
+ }
10
+ function normalizeLocale(raw) {
11
+ const tag = (raw ?? "").toLowerCase();
12
+ if (tag.startsWith("zh")) return "zh";
13
+ return "en";
14
+ }
15
+
16
+ // src/host/locale.ts
17
+ var cached;
18
+ function detectLocale() {
19
+ if (cached) return cached;
20
+ const override = (process.env.WSKIT_LOCALE ?? "").toLowerCase();
21
+ if (override === "zh" || override === "en") {
22
+ cached = override;
23
+ return cached;
24
+ }
25
+ const envLang = process.env.LC_ALL || process.env.LANG || "";
26
+ cached = normalizeLocale(envLang);
27
+ return cached;
28
+ }
29
+ function L(zh, en, vars) {
30
+ return localize(detectLocale(), zh, en, vars);
31
+ }
32
+
33
+ // src/host/util.ts
34
+ function norm(text) {
35
+ return text.trim().toLowerCase().replace(/\s+/g, " ");
36
+ }
37
+ function scoreQuery(candidate, query) {
38
+ const c = norm(candidate);
39
+ const terms = norm(query).split(" ").filter((term) => term.length > 0);
40
+ if (terms.length === 0) return 0;
41
+ let total = 0;
42
+ for (const term of terms) {
43
+ const at = c.indexOf(term);
44
+ if (at < 0) return Number.POSITIVE_INFINITY;
45
+ total += c === term ? 0 : at === 0 ? 1 : c.includes(term + " ") || c.includes(" " + term) ? 2 : at + 2;
46
+ }
47
+ return total;
48
+ }
49
+ async function toEntry(ws) {
50
+ return {
51
+ id: ws.id,
52
+ title: ws.title,
53
+ path: ws.path,
54
+ createdAt: ws.createdAt,
55
+ updatedAt: ws.updatedAt,
56
+ sessionIds: [...ws.sessionIds],
57
+ sessionCount: ws.sessionIds.length,
58
+ missingDir: await ws.status() === "missing-dir"
59
+ };
60
+ }
61
+ async function listWorkspaces(registry) {
62
+ const entries = await Promise.all(registry.list().map(toEntry));
63
+ entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0);
64
+ return entries;
65
+ }
66
+ async function findWorkspaces(registry, query) {
67
+ const q = norm(query);
68
+ const entries = await listWorkspaces(registry);
69
+ if (q.length === 0) return entries.map((entry) => ({ ...entry, score: 0, why: "recent" }));
70
+ const matches = [];
71
+ for (const entry of entries) {
72
+ const segments = entry.path.replace(/[/\\]+$/, "").split(/[/\\]/).filter(Boolean);
73
+ const haystacks = [
74
+ { text: entry.title, why: "title" },
75
+ { text: entry.path, why: "path" },
76
+ { text: segments.join(" "), why: "segments" }
77
+ ];
78
+ let best = Number.POSITIVE_INFINITY;
79
+ let why = "";
80
+ for (const { text, why: label } of haystacks) {
81
+ const score = scoreQuery(text, q);
82
+ if (score < best) {
83
+ best = score;
84
+ why = label;
85
+ }
86
+ }
87
+ if (Number.isFinite(best)) matches.push({ ...entry, score: best, why });
88
+ }
89
+ matches.sort((a, b) => a.score - b.score || (a.updatedAt < b.updatedAt ? 1 : -1));
90
+ return matches;
91
+ }
92
+ function matchFieldLabel(why) {
93
+ if (detectLocale() !== "zh") return why;
94
+ const zhLabels = { title: "\u6807\u9898", path: "\u8DEF\u5F84", segments: "\u5206\u6BB5", recent: "\u6700\u8FD1\u6D3B\u52A8" };
95
+ return zhLabels[why] ?? why;
96
+ }
97
+ function entryLine(entry) {
98
+ const when = entry.updatedAt.slice(0, 10);
99
+ const count = entry.sessionCount;
100
+ const missing = entry.missingDir ? L("\uFF0C\u76EE\u5F55\u5F53\u524D\u4E0D\u5B58\u5728", ", directory currently missing") : "";
101
+ const meta = L("\uFF08{count} \u4E2A\u4F1A\u8BDD\uFF0C\u6700\u8FD1 {when}{missing}\uFF09", " ({count} sessions, latest {when}{missing})", { count, when, missing });
102
+ return `\xB7 **${entry.title}** \u2014 ${entry.path}${meta}`;
103
+ }
104
+
105
+ // src/host/commands.ts
106
+ var MAX_CANDIDATES = 12;
107
+ function registerWorkspaceCommands(ctx, commands) {
108
+ commands.register({
109
+ name: "workspace-find",
110
+ description: L("\u6309\u6807\u9898/\u8DEF\u5F84\u6A21\u7CCA\u641C\u7D22\u5E76\u5B9A\u4F4D\u5DE5\u4F5C\u533A", "Fuzzy-search workspaces by title/path and locate one"),
111
+ input: { hint: L("\u5173\u952E\u8BCD\uFF0C\u5982 pms / demo", "Keywords, e.g. pms / demo") },
112
+ async handler({ rawInput }) {
113
+ const query = rawInput.trim();
114
+ if (!query) {
115
+ return { kind: "error", text: L("\u7528\u6CD5\uFF1A/workspace-find <\u5173\u952E\u8BCD>", "Usage: /workspace-find <keywords>") };
116
+ }
117
+ const matches = await findWorkspaces(ctx.workspaceRegistry, query);
118
+ if (matches.length === 0) {
119
+ return { kind: "error", text: L("\u6CA1\u6709\u5339\u914D\u300C{query}\u300D\u7684\u5DE5\u4F5C\u533A\u3002", 'No workspace matches "{query}".', { query }) };
120
+ }
121
+ const body = matches.slice(0, MAX_CANDIDATES).map((m) => entryLine(m)).join("\n");
122
+ const note = matches.length > MAX_CANDIDATES ? L("\n\u2026\u8FD8\u6709 {rest} \u4E2A\u5339\u914D", "\n\u2026plus {rest} more matches", { rest: matches.length - MAX_CANDIDATES }) : "";
123
+ return {
124
+ kind: "success",
125
+ text: L(
126
+ "\u5339\u914D\u300C{query}\u300D\u7684\u5DE5\u4F5C\u533A\uFF1A\n{body}{note}\n\n\u5B8C\u6574\u5217\u8868\uFF1A/workspace-list",
127
+ 'Workspaces matching "{query}":\n{body}{note}\n\nFull list: /workspace-list',
128
+ { query, body, note }
129
+ )
130
+ };
131
+ }
132
+ });
133
+ commands.register({
134
+ name: "workspace-list",
135
+ description: L("\u5217\u51FA\u5168\u90E8\u5DE5\u4F5C\u533A\uFF08\u6309\u6700\u8FD1\u6D3B\u52A8\u6392\u5E8F\uFF09", "List all workspaces (newest activity first)"),
136
+ async handler() {
137
+ const entries = await listWorkspaces(ctx.workspaceRegistry);
138
+ if (entries.length === 0) {
139
+ return { kind: "success", text: L("\u5F53\u524D\u6CA1\u6709\u4EFB\u4F55\u5DE5\u4F5C\u533A\u3002", "No workspaces yet.") };
140
+ }
141
+ const body = entries.slice(0, MAX_CANDIDATES).map(entryLine).join("\n");
142
+ const note = entries.length > MAX_CANDIDATES ? L("\n\u2026\u8FD8\u6709 {rest} \u4E2A\u5DE5\u4F5C\u533A", "\n\u2026plus {rest} more workspaces", { rest: entries.length - MAX_CANDIDATES }) : "";
143
+ return {
144
+ kind: "success",
145
+ text: L("\u5171 {count} \u4E2A\u5DE5\u4F5C\u533A\uFF1A\n{body}{note}", "{count} workspaces in total:\n{body}{note}", {
146
+ count: entries.length,
147
+ body,
148
+ note
149
+ })
150
+ };
151
+ }
152
+ });
153
+ }
154
+
155
+ // src/host/tools.ts
156
+ import { defineTool } from "@deepseek-ai/dsh-tools";
157
+ function registerWorkspaceTools(ctx) {
158
+ ctx.tools.register(defineTool({
159
+ name: "workspace_find",
160
+ description: 'Fuzzy-search workspaces (project directories that own dsh sessions) by title or path. Use it to locate which workspace/absolute path holds a project, e.g. when the user asks "which workspace is my pms project in". Returns ranked matches with absolute paths, session counts, and last activity.',
161
+ parameters: {
162
+ query: {
163
+ type: "string",
164
+ required: true,
165
+ description: 'Title or path keywords to search for, e.g. "pms" or a directory name.'
166
+ },
167
+ limit: {
168
+ type: "integer",
169
+ description: "Maximum matches to return (default 8)."
170
+ }
171
+ },
172
+ output: {
173
+ schema: { type: "string" },
174
+ render: (_args, value) => [{ type: "text", text: value }]
175
+ },
176
+ async execute(args) {
177
+ const limit = Math.max(1, Math.min(args.limit ?? 8, 20));
178
+ const matches = await findWorkspaces(ctx.workspaceRegistry, args.query);
179
+ if (matches.length === 0) {
180
+ return L(
181
+ "\u672A\u627E\u5230\u5339\u914D\u300C{query}\u300D\u7684\u5DE5\u4F5C\u533A\u3002\u53EF\u5C1D\u8BD5 workspace_list \u67E5\u770B\u5168\u90E8\u3002",
182
+ 'No workspace matches "{query}". Try workspace_list to see all workspaces.',
183
+ { query: args.query }
184
+ );
185
+ }
186
+ const head = matches.slice(0, limit);
187
+ const body = head.map((m) => {
188
+ const why = matchFieldLabel(m.why);
189
+ const missing = m.missingDir ? L("\uFF0C\u76EE\u5F55\u5F53\u524D\u4E0D\u5B58\u5728", ", directory currently missing") : "";
190
+ const meta = L(
191
+ "\uFF08{count} \u4E2A\u4F1A\u8BDD\uFF0C\u6700\u8FD1 {date}\uFF0C\u547D\u4E2D\uFF1A{why}{missing}\uFF09",
192
+ " ({count} sessions, latest {date}; matched: {why}{missing})",
193
+ { count: m.sessionCount, date: m.updatedAt.slice(0, 10), why, missing }
194
+ );
195
+ return `\xB7 **${m.title}** \u2014 ${m.path}${meta}`;
196
+ }).join("\n");
197
+ const note = matches.length > limit ? L("\n\uFF08\u5171 {total} \u4E2A\u5339\u914D\uFF0C\u4EC5\u663E\u793A\u524D {limit} \u4E2A\uFF09", "\n ({total} matches in total; showing first {limit})", {
198
+ total: matches.length,
199
+ limit
200
+ }) : "";
201
+ return L(
202
+ "\u5339\u914D\u300C{query}\u300D\u7684\u5DE5\u4F5C\u533A\uFF1A\n{body}{note}\n\n\u63D0\u793A\uFF1A\u53EF\u5728 dsh Web \u7684 Spotlight\uFF08\u2318K / Ctrl+K\uFF09\u4E2D\u76F4\u63A5\u641C\u7D22\u5E76\u6253\u5F00\u3002",
203
+ 'Workspaces matching "{query}":\n{body}{note}\n\nTip: you can also search and open it from the Spotlight in dsh Web (\u2318K / Ctrl+K).',
204
+ { query: args.query, body, note }
205
+ );
206
+ }
207
+ }));
208
+ ctx.tools.register(defineTool({
209
+ name: "workspace_list",
210
+ description: "List every workspace (project directories that own dsh sessions), newest activity first, with absolute paths and session counts. Use it for the full inventory of projects.",
211
+ parameters: {
212
+ limit: {
213
+ type: "integer",
214
+ description: "Maximum rows to return (default 20)."
215
+ }
216
+ },
217
+ output: {
218
+ schema: { type: "string" },
219
+ render: (_args, value) => [{ type: "text", text: value }]
220
+ },
221
+ async execute(args) {
222
+ const limit = Math.max(1, Math.min(args.limit ?? 20, 50));
223
+ const entries = await listWorkspaces(ctx.workspaceRegistry);
224
+ if (entries.length === 0) {
225
+ return L(
226
+ "\u5F53\u524D\u6CA1\u6709\u4EFB\u4F55\u5DE5\u4F5C\u533A\u3002\u5728 dsh Web \u4E2D\u4ECE\u67D0\u4E2A\u9879\u76EE\u76EE\u5F55\u5F00\u59CB\u4F1A\u8BDD\u540E\u4F1A\u81EA\u52A8\u521B\u5EFA\u3002",
227
+ "No workspaces yet. Start a session from a project directory in dsh Web and one will be created automatically."
228
+ );
229
+ }
230
+ const body = entries.slice(0, limit).map(entryLine).join("\n");
231
+ const note = entries.length > limit ? L("\n\uFF08\u5171 {total} \u4E2A\u5DE5\u4F5C\u533A\uFF0C\u4EC5\u663E\u793A\u524D {limit} \u4E2A\uFF09", "\n ({total} workspaces in total; showing first {limit})", {
232
+ total: entries.length,
233
+ limit
234
+ }) : "";
235
+ return L("\u5171 {count} \u4E2A\u5DE5\u4F5C\u533A\uFF1A\n{body}{note}", "{count} workspaces in total:\n{body}{note}", {
236
+ count: entries.length,
237
+ body,
238
+ note
239
+ });
240
+ }
241
+ }));
242
+ }
243
+
244
+ // src/host/index.ts
245
+ var name = "workspace-kit";
246
+ var inject = ["workspaceRegistry", "tools", "commands"];
247
+ function apply(ctx) {
248
+ const log = ctx.logger("workspace-kit");
249
+ log.info("workspace-kit loaded");
250
+ registerWorkspaceTools(ctx);
251
+ log.info("workspace tools registered (workspace_find, workspace_list)");
252
+ registerWorkspaceCommands(ctx, ctx.commands);
253
+ log.info("workspace commands registered (/workspace-find, /workspace-list)");
254
+ }
255
+ export {
256
+ apply,
257
+ inject,
258
+ name
259
+ };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "dsh-workspace-kit",
3
+ "version": "0.1.0",
4
+ "description": "dsh (DeepSeek Harness) workspace kit: soft workspace archive/restore, ⌘K Spotlight search, per-workspace icons/colors, and an enhanced sidebar browser with drag reorder — a standard Cordis bundle plugin (host tools/commands + bilingual browser client).",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "files": [
8
+ "lib",
9
+ "cordis.patch.yml",
10
+ "README.md",
11
+ "README.zh-CN.md",
12
+ "docs",
13
+ "LICENSE"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./client": {
20
+ "default": "./lib/client.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "dsh",
29
+ "deepseek-harness",
30
+ "cordis",
31
+ "plugin",
32
+ "workspace",
33
+ "spotlight"
34
+ ],
35
+ "author": "ice5kysl",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/ice5kysl/dsh-workspace-kit.git"
39
+ },
40
+ "homepage": "https://github.com/ice5kysl/dsh-workspace-kit",
41
+ "bugs": {
42
+ "url": "https://github.com/ice5kysl/dsh-workspace-kit/issues"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ },
48
+ "client": {
49
+ "platform": "web",
50
+ "inject": [
51
+ "@deepseek-ai/dsh-client-runtime",
52
+ "@deepseek-ai/dsh-client-ui-layout",
53
+ "@deepseek-ai/dsh-client-ui-workspace"
54
+ ]
55
+ }
56
+ },
57
+ "scripts": {
58
+ "build": "node scripts/build.mjs",
59
+ "typecheck": "tsc -p tsconfig.json --noEmit",
60
+ "prepare": "npm run build",
61
+ "prepack": "npm run build"
62
+ },
63
+ "devDependencies": {
64
+ "@deepseek-ai/cordis": "^4.0.1",
65
+ "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
66
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
67
+ "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
68
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-workspace": "^0.1.1-rc.2",
70
+ "@types/node": "^20.19.43",
71
+ "@types/react": "^18.3.31",
72
+ "@types/react-dom": "^18.3.7",
73
+ "esbuild": "^0.25.12",
74
+ "lucide-react": "^1.41.0",
75
+ "typescript": "^5.9.3"
76
+ },
77
+ "license": "MIT"
78
+ }