dsh-bce 0.1.0 → 0.1.1

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Context-aware prompt enhancement for the DeepSeek Harness composer, powered by your Better Context Engine MCP.**
8
8
 
9
- [![Version](https://img.shields.io/badge/version-0.1.0-4D6BFE)](package.json) [![License](https://img.shields.io/badge/license-MIT-3DA639)](#license) [![Node.js](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=node.js&logoColor=white)](#requirements) [![Platform](https://img.shields.io/badge/DeepSeek%20Harness-web%20%7C%20Desktop-1F2937)](https://github.com/deepseek-ai/deepseek-harness)
9
+ [![npm](https://img.shields.io/npm/v/dsh-bce?color=4D6BFE)](https://www.npmjs.com/package/dsh-bce) [![License](https://img.shields.io/badge/license-MIT-3DA639)](#license) [![Node.js](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=node.js&logoColor=white)](#requirements) [![Platform](https://img.shields.io/badge/DeepSeek%20Harness-web%20%7C%20Desktop-1F2937)](https://github.com/deepseek-ai/deepseek-harness)
10
10
 
11
11
  English · [简体中文](README.zh.md)
12
12
 
@@ -19,7 +19,7 @@ English · [简体中文](README.zh.md)
19
19
  `dsh-bce` adds an enhance icon next to the composer's attach button. One click incrementally indexes the current workspace through the Better Context Engine (BCE) MCP server already configured in your profile, then rewrites the draft into a context-rich prompt.
20
20
 
21
21
  > [!NOTE]
22
- > The plugin stores no BCE base URL or token. Credentials stay on the MCP entry of the active profile and never reach the browser.
22
+ > The plugin stores no BCE base URL or token. Credentials live only in the active profile's MCP entry and are used by bce-tool on the host; they are never sent to the UI (the web page or the Desktop window).
23
23
 
24
24
  ## Demo
25
25
 
@@ -175,7 +175,7 @@ dsh plugin --profile web add "$(pwd)"
175
175
  | Cmd/Ctrl+Z does not revert | `setDraft` is outside the composer undo stack | Use **Restore original**. |
176
176
  | Markdown is not rendered in the composer | The composer is a plain-text draft with file-reference chips | Keep enhancer output as plain text. |
177
177
 
178
- **Why not call BCE from the browser?** The BCE service has no CORS layer, and the token must stay on the host. The enhance icon calls the host RPC, which executes the configured MCP tools.
178
+ **Why doesn't the UI call BCE directly?** The BCE service has no CORS layer, and the token must stay on the host. The enhance icon calls the host RPC, which executes the configured MCP tools.
179
179
 
180
180
  ## License
181
181
 
package/README.zh.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **为 DeepSeek Harness Composer 提供基于工作区上下文的提示增强,复用你已配置的 Better Context Engine MCP。**
8
8
 
9
- [![Version](https://img.shields.io/badge/version-0.1.0-4D6BFE)](package.json) [![License](https://img.shields.io/badge/license-MIT-3DA639)](#许可) [![Node.js](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=node.js&logoColor=white)](#环境要求) [![Platform](https://img.shields.io/badge/DeepSeek%20Harness-web%20%7C%20Desktop-1F2937)](https://github.com/deepseek-ai/deepseek-harness)
9
+ [![npm](https://img.shields.io/npm/v/dsh-bce?color=4D6BFE)](https://www.npmjs.com/package/dsh-bce) [![License](https://img.shields.io/badge/license-MIT-3DA639)](#许可) [![Node.js](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933?logo=node.js&logoColor=white)](#环境要求) [![Platform](https://img.shields.io/badge/DeepSeek%20Harness-web%20%7C%20Desktop-1F2937)](https://github.com/deepseek-ai/deepseek-harness)
10
10
 
11
11
  [English](README.md) · 简体中文
12
12
 
@@ -19,7 +19,7 @@
19
19
  `dsh-bce` 在 Composer 附件按钮旁增加一个增强图标。点击后,插件通过当前 profile 已配置的 Better Context Engine(BCE)MCP 对工作区做增量索引,再把草稿重写为带有上下文的提示词。
20
20
 
21
21
  > [!NOTE]
22
- > 插件不保存 BCE 服务地址或 Token。凭证仅存在于当前 profile 的 MCP 配置中,不会进入浏览器。
22
+ > 插件不保存 BCE 服务地址或 Token。凭证只保存在当前 profile 的 MCP 配置中,仅由 Host 端的 bce-tool 使用,不会下发到前端界面(web 页面或 Desktop 窗口)。
23
23
 
24
24
  ## 演示
25
25
 
@@ -175,7 +175,7 @@ dsh plugin --profile web add "$(pwd)"
175
175
  | Cmd/Ctrl+Z 无法撤销 | `setDraft` 不进入 Composer 撤销栈 | 使用 **还原**。 |
176
176
  | 输入框不渲染 Markdown | Composer 是纯文本草稿加文件引用 chip | 增强结果使用纯文本。 |
177
177
 
178
- **为什么不从浏览器直连 BCE?** BCE 服务端没有 CORS,且 Token 不能进入浏览器。增强图标通过 Host RPC 调用已配置的 MCP 工具。
178
+ **为什么前端不直连 BCE?** BCE 服务端没有 CORS,而且 Token 只能留在 Host 端。增强图标通过 Host RPC 调用已配置的 MCP 工具。
179
179
 
180
180
  ## 许可
181
181
 
package/lib/client.js CHANGED
@@ -40,8 +40,10 @@ window.__ModuleLoader__.load({
40
40
  "wand.mcpMissing": "BCE MCP was not found. Add bce-tool to this profile with --no-webbrowser-enhance-prompt.",
41
41
  "wand.notGit": "This folder is not a Git repository. bce-tool only indexes Git projects.",
42
42
  "wand.tooLarge": "This project is too large to index. Trim it with .gitignore or .bceignore.",
43
- "wand.timeout": "Enhancement timed out (90s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args.",
43
+ "wand.timeout": "Enhancement timed out (90s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args, or switch to a faster enhancement model in BCE.",
44
44
  "wand.indexFailed": "Indexing failed. Check the bce-tool MCP logs.",
45
+ "wand.indexTimeout": "Indexing timed out. Raise toolCallTimeoutMs on the bce-tool MCP entry.",
46
+ "wand.draftChanged": "The draft changed while enhancing, so it was not replaced.",
45
47
  "wand.upstream": "Enhancement failed. Check MCP tool permissions and bce-tool logs.",
46
48
  "wand.noIndex": "Enhanced without a fresh index (search_context was not found).",
47
49
  "wand.done": "Prompt enhanced.",
@@ -56,8 +58,10 @@ window.__ModuleLoader__.load({
56
58
  "wand.mcpMissing": "未检测到 BCE MCP。请在当前 profile 配置 bce-tool,并加上 --no-webbrowser-enhance-prompt。",
57
59
  "wand.notGit": "当前目录不是 Git 仓库。bce-tool 默认只索引 Git 项目。",
58
60
  "wand.tooLarge": "项目过大,无法索引。请用 .gitignore 或 .bceignore 排除大目录后再试。",
59
- "wand.timeout": "增强超时(90 秒)。请确认 MCP 启动参数包含 --no-webbrowser-enhance-prompt。",
61
+ "wand.timeout": "增强超时(90 秒)。请确认 MCP 启动参数包含 --no-webbrowser-enhance-prompt,或在 BCE 中换用更快的增强模型。",
60
62
  "wand.indexFailed": "索引失败。请查看 bce-tool MCP 日志。",
63
+ "wand.indexTimeout": "索引超时。请在 bce-tool MCP 配置中调大 toolCallTimeoutMs。",
64
+ "wand.draftChanged": "增强期间草稿已修改,未覆盖。",
61
65
  "wand.upstream": "增强失败。请检查 MCP 工具权限和 bce-tool 日志。",
62
66
  "wand.noIndex": "已增强,但未做索引(未找到 search_context)。",
63
67
  "wand.done": "提示已增强。",
@@ -263,50 +267,27 @@ window.__ModuleLoader__.load({
263
267
  return;
264
268
  }
265
269
  }
266
- function hookValue(props, name) {
267
- const hook = props[name];
268
- if (typeof hook !== "function") return void 0;
269
- try {
270
- return hook((state) => state);
271
- } catch {
272
- return;
273
- }
270
+ function sessionsPath(sessions, sessionId) {
271
+ const id = sessionId ?? nonEmpty(sessions?.current);
272
+ return id === void 0 ? void 0 : cwdOf(sessions?.byId?.[id]);
274
273
  }
275
- function resolveWorkspace(props, extras) {
276
- const session = hookValue(props, "useSession") ?? props.session;
277
- const sessions = hookValue(props, "useSessions") ?? snapshotOf(extras.sessions);
278
- const workspaces = hookValue(props, "useWorkspaces") ?? snapshotOf(extras.workspaces);
279
- const sessionId = nonEmpty(props.sessionId) ?? nonEmpty(sessions?.current);
280
- const fromSession = cwdOf(session);
281
- if (fromSession !== void 0) return {
282
- path: fromSession,
283
- sessionId
284
- };
285
- if (sessionId !== void 0) {
286
- const rowCwd = cwdOf(sessions?.byId?.[sessionId]);
287
- if (rowCwd !== void 0) return {
288
- path: rowCwd,
289
- sessionId
290
- };
291
- }
274
+ function workspacesPath(workspaces, sessionId) {
292
275
  const items = workspaces?.items ?? [];
293
276
  if (sessionId !== void 0) {
294
- const ownedPath = nonEmpty(items.find((item) => item.sessionIds?.includes(sessionId))?.path);
295
- if (ownedPath !== void 0) return {
296
- path: ownedPath,
297
- sessionId
298
- };
277
+ const owned = nonEmpty(items.find((item) => item.sessionIds?.includes(sessionId))?.path);
278
+ if (owned !== void 0) return owned;
299
279
  }
300
280
  const recentId = nonEmpty(workspaces?.recentWorkspaceId);
301
- if (recentId !== void 0) {
302
- const recentPath = nonEmpty(items.find((item) => item.workspaceId === recentId || item.id === recentId)?.path);
303
- if (recentPath !== void 0) return {
304
- path: recentPath,
305
- sessionId
306
- };
307
- }
281
+ if (recentId === void 0) return void 0;
282
+ return nonEmpty(items.find((item) => item.workspaceId === recentId || item.id === recentId)?.path);
283
+ }
284
+ /** Reads store snapshots only; hook values must be captured during render. */
285
+ function resolveWorkspace(props, hooked, extras) {
286
+ const sessions = snapshotOf(extras.sessions);
287
+ const workspaces = snapshotOf(extras.workspaces);
288
+ const sessionId = nonEmpty(props.sessionId) ?? nonEmpty(sessions?.current);
308
289
  return {
309
- path: void 0,
290
+ path: hooked.session ?? cwdOf(props.session) ?? hooked.sessions ?? sessionsPath(sessions, sessionId) ?? hooked.workspaces ?? workspacesPath(workspaces, sessionId),
310
291
  sessionId
311
292
  };
312
293
  }
@@ -319,6 +300,7 @@ window.__ModuleLoader__.load({
319
300
  case "too_large": return t("wand.tooLarge");
320
301
  case "timeout": return t("wand.timeout");
321
302
  case "index_failed": return t("wand.indexFailed");
303
+ case "index_timeout": return t("wand.indexTimeout");
322
304
  case "no_rpc": return t("wand.noRpc");
323
305
  default: return t("wand.upstream");
324
306
  }
@@ -383,11 +365,18 @@ window.__ModuleLoader__.load({
383
365
  const lock = react.default.useRef(false);
384
366
  const draftFromHook = props.useInput?.((state) => state.draft);
385
367
  const draft = typeof draftFromHook === "string" ? draftFromHook : props.input?.draft ?? "";
368
+ const draftRef = react.default.useRef(draft);
369
+ draftRef.current = draft;
386
370
  const extras = {
387
371
  sessions: sessionsStore,
388
372
  workspaces: workspacesStore
389
373
  };
390
374
  const sessionId = nonEmpty(props.sessionId);
375
+ const hooked = {
376
+ session: props.useSession?.((state) => cwdOf(state)),
377
+ sessions: props.useSessions?.((state) => sessionsPath(state, sessionId)),
378
+ workspaces: props.useWorkspaces?.((state) => workspacesPath(state, sessionId))
379
+ };
391
380
  const empty = draft.trim().length === 0;
392
381
  const disabled = busy || empty;
393
382
  const title = empty ? t("wand.empty") : status.length > 0 ? status : t("wand.label");
@@ -411,7 +400,7 @@ window.__ModuleLoader__.load({
411
400
  setBusy(true);
412
401
  announce("busy", t("wand.enhancing"));
413
402
  const prompt = draft.trim();
414
- const workspace = resolveWorkspace(props, extras);
403
+ const workspace = resolveWorkspace(props, hooked, extras);
415
404
  try {
416
405
  if (workspace.path === void 0 && workspace.sessionId === void 0) {
417
406
  announce("error", t("wand.noWorkspace"));
@@ -428,6 +417,10 @@ window.__ModuleLoader__.load({
428
417
  sessionId: workspace.sessionId ?? sessionId
429
418
  }));
430
419
  if (parsed?.ok === true) {
420
+ if (draftRef.current.trim() !== prompt) {
421
+ announce("error", t("wand.draftChanged"));
422
+ return;
423
+ }
431
424
  inputActions?.setDraft(parsed.value.text);
432
425
  const previous = getRestore();
433
426
  const original = (previous === void 0 || previous.sessionId === void 0 || previous.sessionId === sessionId) && previous !== void 0 && prompt === previous.enhanced ? previous.original : prompt;
package/lib/index.js CHANGED
@@ -95,13 +95,19 @@ function resolveWorkspacePath(ctx, record) {
95
95
  if (sessionId === void 0) return void 0;
96
96
  return workspacePathFromRegistry(ctx, sessionId);
97
97
  }
98
- function classifyToolText(text, isError) {
98
+ /** `ctx.tools.execute` reports tool failures and aborts as results, never by throwing. */
99
+ function isFailure(result, text) {
100
+ return result.isError || text.startsWith("Error:");
101
+ }
102
+ /** Only meaningful for failed results: successful text may quote these phrases. */
103
+ function classifyFailure(text) {
99
104
  const lower = text.toLowerCase();
100
105
  if (lower.includes("not managed by git") || lower.includes("not a git repository")) return "not_git";
101
106
  if (lower.includes("too large to index") || lower.includes("exceeds the") && lower.includes("mb")) return "too_large";
102
- if (text.startsWith("Error:") || isError) return "index_failed";
103
- if (lower.includes("tool unavailable")) return "upstream";
107
+ if (lower.includes("timed out") || lower.includes("timeout") || lower.includes("tool call aborted")) return "timeout";
104
108
  }
109
+ const INDEX_TIMEOUT_MESSAGE = "Indexing timed out. Raise toolCallTimeoutMs on the bce-tool MCP entry.";
110
+ const ENHANCE_TIMEOUT_MESSAGE = `Enhancement timed out (${ENHANCE_TIMEOUT_MS / 1e3}s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args if the review page is blocking.`;
105
111
  function isAbortError(error) {
106
112
  if (error instanceof Error && error.name === "AbortError") return true;
107
113
  if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") return true;
@@ -134,19 +140,26 @@ async function enhancePrompt(ctx, payload, signal) {
134
140
  query: prompt
135
141
  }, signal);
136
142
  } catch (error) {
137
- if (isAbortError(error)) return fail("timeout", "Indexing timed out. Raise toolCallTimeoutMs on the bce-tool MCP entry.");
143
+ if (isAbortError(error)) return fail("index_timeout", INDEX_TIMEOUT_MESSAGE);
138
144
  return fail("index_failed", error instanceof Error ? error.message : String(error));
139
145
  }
140
146
  const searchText = contentText(searchResult);
141
- const searchCode = classifyToolText(searchText, searchResult.isError);
142
- if (searchCode === "not_git" || searchCode === "too_large") return fail(searchCode, searchText || searchCode);
143
- if (searchCode !== void 0) return fail("index_failed", searchText || "search_context failed");
147
+ if (isFailure(searchResult, searchText)) {
148
+ const searchCode = classifyFailure(searchText);
149
+ if (searchCode === "timeout") return fail("index_timeout", INDEX_TIMEOUT_MESSAGE);
150
+ if (searchCode !== void 0) return fail(searchCode, searchText);
151
+ return fail("index_failed", searchText || "search_context failed");
152
+ }
144
153
  } else warning = "no_index";
145
154
  const enhanceAbort = new AbortController();
146
155
  const onParentAbort = () => enhanceAbort.abort();
147
156
  if (signal.aborted) enhanceAbort.abort();
148
157
  else signal.addEventListener("abort", onParentAbort, { once: true });
149
- const timer = setTimeout(() => enhanceAbort.abort(), ENHANCE_TIMEOUT_MS);
158
+ let timedOut = false;
159
+ const timer = setTimeout(() => {
160
+ timedOut = true;
161
+ enhanceAbort.abort();
162
+ }, ENHANCE_TIMEOUT_MS);
150
163
  let enhanceResult;
151
164
  try {
152
165
  enhanceResult = await callTool(ctx, tools.enhance, {
@@ -155,16 +168,20 @@ async function enhancePrompt(ctx, payload, signal) {
155
168
  conversation_history: ""
156
169
  }, enhanceAbort.signal);
157
170
  } catch (error) {
158
- if (isAbortError(error) || enhanceAbort.signal.aborted) return fail("timeout", `Enhancement timed out (${ENHANCE_TIMEOUT_MS / 1e3}s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args if the review page is blocking.`);
171
+ if (timedOut || isAbortError(error)) return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
159
172
  return fail("upstream", error instanceof Error ? error.message : String(error));
160
173
  } finally {
161
174
  clearTimeout(timer);
162
175
  signal.removeEventListener("abort", onParentAbort);
163
176
  }
177
+ if (timedOut) return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
164
178
  const text = contentText(enhanceResult);
165
- const enhanceCode = classifyToolText(text, enhanceResult.isError);
166
- if (enhanceCode === "not_git" || enhanceCode === "too_large" || enhanceCode === "timeout") return fail(enhanceCode, text || enhanceCode);
167
- if (enhanceCode !== void 0) return fail("upstream", text || "enhance_prompt failed");
179
+ if (isFailure(enhanceResult, text)) {
180
+ const enhanceCode = classifyFailure(text);
181
+ if (enhanceCode === "timeout") return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
182
+ if (enhanceCode !== void 0) return fail(enhanceCode, text);
183
+ return fail("upstream", text || "enhance_prompt failed");
184
+ }
168
185
  if (text.length === 0) return fail("upstream", "enhance_prompt returned empty text");
169
186
  return ok(warning === void 0 ? { text } : {
170
187
  text,
@@ -235,7 +252,20 @@ function fetchRouteFor(handler) {
235
252
  function registerEnhanceRpc(ctx) {
236
253
  ctx.inject(["connection"], (wired) => {
237
254
  const connection = wired.get("connection");
238
- const handler = (payload, signal) => enhancePrompt(ctx, payload, signal);
255
+ const handler = async (payload, signal) => {
256
+ try {
257
+ return await enhancePrompt(ctx, payload, signal);
258
+ } catch (error) {
259
+ return {
260
+ ok: false,
261
+ error: {
262
+ code: "upstream",
263
+ message: error instanceof Error ? error.message : String(error),
264
+ details: {}
265
+ }
266
+ };
267
+ }
268
+ };
239
269
  const register = connection.fetch?.register;
240
270
  if (typeof register === "function") {
241
271
  wired.effect(() => register(fetchRouteFor(handler)), "dsh-bce: /api/dsh-bce.enhance fetch route");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bce",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DeepSeek Harness plugin that incrementally indexes the current workspace via the configured BCE MCP and enhances composer prompts.",
5
5
  "keywords": [
6
6
  "deepseek-harness",