dsh-reference-anything 0.2.3 → 0.2.5

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
@@ -22,7 +22,13 @@ One `@` to reference them all.
22
22
 
23
23
  </div>
24
24
 
25
- Within the unified `@` menu of DeepSeek Harness (DSH), reference commands, Skills, workspace files/folders, DSH sessions, and historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi.
25
+ <div align="center">
26
+
27
+ <img src="./images/demo.gif" alt="dsh-reference-anything demo" width="800" />
28
+
29
+ </div>
30
+
31
+ Within a unified `@` menu of DeepSeek Harness (DSH), reference commands, Skills, workspace files/folders, DSH sessions, and historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi.
26
32
 
27
33
  This plugin uses OpenCLI to access historical conversations through AI chat sessions that are already logged in. By default, only conversation titles are stored locally, and the agent fetches remote content on demand. An optional offline-mirror mode stores the latest complete conversation bodies locally.
28
34
 
@@ -31,7 +37,7 @@ This plugin uses OpenCLI to access historical conversations through AI chat sess
31
37
 
32
38
  ## News
33
39
 
34
- - **2026-08-19 · v0.2.3** — Added automatic version checks and in-settings updates, Pill/Raw text input rendering modes, and reusable background browser sessions for more reliable OpenCLI synchronization and input interactions.
40
+ - **2026-08-19 · v0.2.4** — Added automatic version checks and in-settings updates, Pill/Raw text input rendering modes, and reusable background browser sessions for more reliable OpenCLI synchronization and input interactions.
35
41
  - **2026-08-18 · v0.2.0** — A redesigned Reference Anything settings page with local session statistics, paginated management, Provider/Profile selection, and sync status checks.
36
42
  - **2026-08-18** — Introduced on-demand read protocol: references default to safe pointers, and the agent reads the body and attachments only after authorization.
37
43
  - **2026-08-17** — Unified ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi under the DSH `@` menu.
@@ -98,14 +104,20 @@ The `@` menu contains five groups: `Commands`, `Skills`, `Files and folders`, `D
98
104
 
99
105
  Available only at the start of the draft. To browse all commands, use `@commands` or the native DSH `/` panel.
100
106
 
107
+ <p align="center"><img src="./images/at-commands.png" alt="Browse DSH commands from the @ menu" width="800" /></p>
108
+
101
109
  #### @Skills — DSH skill library
102
110
 
103
111
  Available only at the start of the draft. To browse all skills, use `@skills:` or the native DSH `/` panel.
104
112
 
113
+ <p align="center"><img src="./images/at-skills.png" alt="Browse DSH skills from the @ menu" width="800" /></p>
114
+
105
115
  #### @Files and folders — workspace files and directories
106
116
 
107
117
  Type `@files:` in the input box to browse all files and folders in the workspace. Search supports fuzzy matching on titles, so both `@cachedes` and `@cache-design` can match “Cache design notes.”
108
118
 
119
+ <p align="center"><img src="./images/at-files.png" alt="Browse workspace files and folders from the @ menu" width="800" /></p>
120
+
109
121
  Features:
110
122
  - Quick reference to workspace files with automatic workspace-boundary validation
111
123
  - File references only write a validated path and type marker into the model context; file content is not preloaded
@@ -115,6 +127,8 @@ Features:
115
127
 
116
128
  Type `@sessions:` to browse locally synced DSH sessions. Sessions are ranked by match quality, with recency as fallback ordering.
117
129
 
130
+ <p align="center"><img src="./images/at-sessions.png" alt="Browse DSH sessions from the @ menu" width="800" /></p>
131
+
118
132
  Search capabilities:
119
133
  - **Title match:** fuzzy search on session titles
120
134
  - **Content search:** when title matches are insufficient, the synced session body is searched; matching excerpts are shown in the candidate row for UI display only and are not injected into model context
@@ -126,6 +140,8 @@ Full browsing is available on the settings page’s paginated list. Session refe
126
140
 
127
141
  Supports historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi.
128
142
 
143
+ <p align="center"><img src="./images/at-external-conversations.png" alt="Browse external conversations from the @ menu" width="800" /></p>
144
+
129
145
  **Platform filtering:**
130
146
  - Use `@chatgpt:cache` or `@claude:refactor` to filter a specific platform
131
147
  - Short aliases are also accepted, such as `@gpt:` and `@ds:`
package/lib/client.js CHANGED
@@ -15477,6 +15477,7 @@ var zh = {
15477
15477
  "settings.adapterInstalled": "\u5DF2\u5B89\u88C5\u4E14\u53EF\u53D1\u73B0",
15478
15478
  "settings.adapterInstalledVersion": "\u5DF2\u5B89\u88C5\u4E14\u53EF\u53D1\u73B0\uFF08v{version}\uFF09",
15479
15479
  "settings.adapterMissing": "\u672A\u5B89\u88C5\u9002\u914D\u5668",
15480
+ "settings.adapterLoadFailed": "\u9002\u914D\u5668\u5DF2\u767B\u8BB0\u4F46\u52A0\u8F7D\u5931\u8D25\uFF1A{error}",
15480
15481
  "settings.adapterIncomplete": "\u9002\u914D\u5668\u5305\u4E0D\u5B8C\u6574\uFF0C\u672A\u6CE8\u518C\u5168\u90E8\u516D\u4E2A\u5BF9\u8BDD\u6765\u6E90",
15481
15482
  "settings.adapterVersionUnsupported": "\u9002\u914D\u5668\u7248\u672C {version} \u8FC7\u65E7\u6216\u65E0\u6CD5\u8BC6\u522B",
15482
15483
  "settings.adapterStillUnavailable": "\u5B89\u88C5\u540E\u5BF9\u8BDD\u9002\u914D\u5668\u4ECD\u4E0D\u53EF\u7528\u3002",
@@ -15513,11 +15514,12 @@ var zh = {
15513
15514
  "settings.setupStep.complete": "\u6240\u6709\u68C0\u67E5\u5747\u5DF2\u901A\u8FC7\u3002",
15514
15515
  "settings.install": "\u5B89\u88C5\u9002\u914D\u5668",
15515
15516
  "settings.reinstall": "\u91CD\u65B0\u5B89\u88C5\u9002\u914D\u5668",
15517
+ "settings.repairAdapter": "\u4FEE\u590D\u9002\u914D\u5668",
15516
15518
  "settings.oneClickSetup": "\u4E00\u952E\u5B89\u88C5",
15517
15519
  "settings.restartDaemon": "\u91CD\u542F\u6D4F\u89C8\u5668\u6865\u63A5\u670D\u52A1",
15518
15520
  "settings.restarting": "\u6B63\u5728\u91CD\u542F\u2026",
15519
15521
  "settings.serviceActions": "\u670D\u52A1\u64CD\u4F5C",
15520
- "settings.serviceActionsDetail": "\u4E00\u952E\u5B89\u88C5\u4F1A\u81EA\u52A8\u5B89\u88C5\u9002\u914D\u5668\u3001\u542F\u52A8\u5B88\u62A4\u8FDB\u7A0B\u5E76\u6253\u5F00\u6D4F\u89C8\u5668\u6269\u5C55\u5B89\u88C5\u9875\uFF1B\u4E5F\u53EF\u5355\u72EC\u5B89\u88C5\u9002\u914D\u5668\u6216\u91CD\u542F\u6D4F\u89C8\u5668\u6865\u63A5\u670D\u52A1\u3002",
15522
+ "settings.serviceActionsDetail": "\u4E00\u952E\u5B89\u88C5\u4F1A\u6309\u9700\u5B89\u88C5\u6216\u4FEE\u590D\u9002\u914D\u5668\u3001\u542F\u52A8\u5B88\u62A4\u8FDB\u7A0B\uFF0C\u5E76\u5F15\u5BFC\u5B89\u88C5\u6D4F\u89C8\u5668\u6269\u5C55\u3002",
15521
15523
  "settings.general": "\u901A\u7528\u8BBE\u7F6E",
15522
15524
  "settings.generalDetail": "\u63A7\u5236 @ \u9762\u677F\u4E2D\u53EF\u7528\u7684\u7C7B\u522B\u3001\u663E\u793A\u987A\u5E8F\u53CA\u6BCF\u7C7B\u6700\u591A\u663E\u793A\u7684\u6761\u76EE\u6570\u3002\u5173\u95ED\u67D0\u7C7B\u540E\uFF0C\u5B83\u4E0D\u4F1A\u51FA\u73B0\u5728 @ \u9762\u677F\uFF0C\u4E5F\u4E0D\u4F1A\u5411 Agent \u63D0\u4F9B\u5BF9\u5E94\u6307\u4EE4\u3002",
15523
15525
  "settings.inputRenderMode": "\u8F93\u5165\u6846\u6E32\u67D3",
@@ -15650,6 +15652,7 @@ var en = {
15650
15652
  "settings.updateConfirm": "Update Reference Anything to v{version}? Restart DSH after the update completes.",
15651
15653
  "settings.github": "GitHub",
15652
15654
  "settings.adapterIncomplete": "The adapter package is incomplete and did not register all six conversation sources.",
15655
+ "settings.adapterLoadFailed": "The adapter is registered but failed to load: {error}",
15653
15656
  "settings.setupStep.checking": "Rechecking every dependency\u2026",
15654
15657
  "settings.setupStep.opencli": "Finding, installing, or upgrading OpenCLI\u2026",
15655
15658
  "settings.setupStep.adapter": "Installing the conversation adapter\u2026",
@@ -15717,11 +15720,12 @@ var en = {
15717
15720
  "settings.moveUp": "Move {item} up",
15718
15721
  "settings.moveDown": "Move {item} down",
15719
15722
  "settings.reinstall": "Reinstall adapter",
15723
+ "settings.repairAdapter": "Repair adapter",
15720
15724
  "settings.oneClickSetup": "One-click setup",
15721
15725
  "settings.settingUp": "Setting up\u2026",
15722
15726
  "settings.restartDaemon": "Restart Browser Bridge",
15723
15727
  "settings.serviceActions": "Service actions",
15724
- "settings.serviceActionsDetail": "One-click setup installs the adapter, starts the daemon, and opens the extension install page; you can also install the adapter or restart Browser Bridge individually.",
15728
+ "settings.serviceActionsDetail": "One-click setup installs or repairs the adapter as needed, starts the daemon, and guides browser-extension installation.",
15725
15729
  "notice.daemonRestarted": "Browser Bridge restarted.",
15726
15730
  "notice.oneClickSetup": 'Extension install page opened. Click "Add to Chrome", then Recheck here.',
15727
15731
  "notice.updateInstalled": "Reference Anything updated to v{version}. Restart DSH to load the new version.",
@@ -15994,6 +15998,7 @@ function ConversationSettings({ useScope, save, sync, cancel, refresh, refreshOn
15994
15998
  const daemonReady = Boolean(state.health?.daemonRunning && !state.health.daemonStale);
15995
15999
  const extensionReady = Boolean(state.health?.extensionConnected && state.health.connectivityOk);
15996
16000
  const adapterReady = Boolean(state.health?.pluginInstalled && state.health.adapterCompatible);
16001
+ const adapterNeedsRepair = Boolean(state.health?.pluginInstalled && !adapterReady);
15997
16002
  const runAction = (name, action) => {
15998
16003
  if (busyActionRef.current || state.loading) return;
15999
16004
  busyActionRef.current = true;
@@ -16216,7 +16221,7 @@ function ConversationSettings({ useScope, save, sync, cancel, refresh, refreshOn
16216
16221
  label: t("check.conversationAdapter"),
16217
16222
  detail: adapterStateDetail(state.health, t),
16218
16223
  ready: adapterReady,
16219
- actionLabel: !opencliReady ? t("settings.installOpenCli") : state.health?.pluginInstalled ? t("settings.reinstall") : t("settings.install"),
16224
+ actionLabel: !opencliReady ? t("settings.installOpenCli") : adapterNeedsRepair ? t("settings.repairAdapter") : state.health?.pluginInstalled ? t("settings.reinstall") : t("settings.install"),
16220
16225
  actionBusy: busyAction === "adapter",
16221
16226
  actionDisabled: Boolean(busyAction),
16222
16227
  onAction: () => {
@@ -16236,18 +16241,10 @@ function ConversationSettings({ useScope, save, sync, cancel, refresh, refreshOn
16236
16241
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: t("settings.serviceActions") }),
16237
16242
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("settings.serviceActionsDetail") })
16238
16243
  ] }),
16239
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh_ref_service_actions", children: [
16240
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "is_primary", type: "button", disabled: state.loading || Boolean(busyAction), onClick: () => {
16241
- const opened = state.health?.extensionConnected ? true : openStore();
16242
- runAction("setup", () => setupAll(opened));
16243
- }, children: busyAction === "setup" ? t("settings.settingUp") : t("settings.oneClickSetup") }),
16244
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: state.loading || Boolean(busyAction) || !opencliReady, onClick: () => {
16245
- runAction("adapter", install);
16246
- }, children: busyAction === "adapter" ? t("settings.installing") : state.health?.pluginInstalled ? t("settings.reinstall") : t("settings.install") }),
16247
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: state.loading || Boolean(busyAction) || !opencliReady, onClick: () => {
16248
- runAction("daemon", restartDaemon);
16249
- }, children: busyAction === "daemon" ? t("settings.restarting") : t("settings.restartDaemon") })
16250
- ] })
16244
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh_ref_service_actions", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "is_primary", type: "button", disabled: state.loading || Boolean(busyAction), onClick: () => {
16245
+ const opened = state.health?.extensionConnected ? true : openStore();
16246
+ runAction("setup", () => setupAll(opened));
16247
+ }, children: busyAction === "setup" ? t("settings.settingUp") : t("settings.oneClickSetup") }) })
16251
16248
  ] })
16252
16249
  ] }),
16253
16250
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh_ref_chat_divider" }),
@@ -16535,6 +16532,7 @@ function daemonStateDetail(health, t) {
16535
16532
  }
16536
16533
  function adapterStateDetail(health, t) {
16537
16534
  if (!health?.pluginInstalled) return health?.pluginError || t("settings.adapterMissing");
16535
+ if (health.pluginError) return t("settings.adapterLoadFailed", { error: health.pluginError });
16538
16536
  if (!health.adapterCommandsReady) return t("settings.adapterIncomplete");
16539
16537
  if (!health.adapterCompatible) return t("settings.adapterVersionUnsupported", { version: health.pluginVersion || t("settings.unknownVersion") });
16540
16538
  return health.pluginVersion ? t("settings.adapterInstalledVersion", { version: health.pluginVersion }) : t("settings.adapterInstalled");
package/lib/opencli.d.ts CHANGED
@@ -107,6 +107,7 @@ export declare class OpenCliRunner {
107
107
  private historyRows;
108
108
  private json;
109
109
  private raw;
110
+ private command;
110
111
  }
111
112
  /** Locate a working OpenCLI without asking a command-line newcomer for PATH details. */
112
113
  export declare function discoverOpenCli(configured?: string, signal?: AbortSignal): Promise<OpenCliDiscovery>;
package/lib/opencli.js CHANGED
@@ -12,11 +12,12 @@ const SITE = {
12
12
  deepseek: 'dsh-deepseek', grok: 'dsh-grok', kimi: 'dsh-kimi',
13
13
  };
14
14
  const REQUIRED_ADAPTER_COMMANDS = ['chatgpt', 'claude', 'gemini', 'deepseek', 'grok', 'kimi'];
15
+ const ADAPTER_PLUGIN_NAME = 'dsh-chat-history';
15
16
  /**
16
- * A persistent OpenCLI site session owns one mutable browser tab. Serialize
17
+ * A persistent OpenCLI site session owns one mutable browser lease. Serialize
17
18
  * commands that target the same executable/profile/site across runner
18
- * instances so concurrent detail workers cannot navigate that tab over one
19
- * another. Different provider sites still run in parallel.
19
+ * instances so concurrent detail workers cannot navigate or close each
20
+ * other's active tab. Different provider sites still run in parallel.
20
21
  */
21
22
  const SITE_COMMAND_TAILS = new Map();
22
23
  export class OpenCliError extends Error {
@@ -127,7 +128,9 @@ export class OpenCliRunner {
127
128
  const daemonVersion = daemon.value.match(/^Version:\s+v?([^\s(]+)/m)?.[1];
128
129
  const pluginVersion = plugins.value.match(/dsh-chat-history\s+@([^\s—]+)/i)?.[1];
129
130
  const pluginInstalled = /dsh-chat-history/i.test(plugins.value);
130
- const adapterCommandsReady = REQUIRED_ADAPTER_COMMANDS.every(command => new RegExp(`\\b${command}\\b`, 'i').test(plugins.value));
131
+ const adapterLoadError = adapterPluginLoadError(plugins.diagnostic);
132
+ const adapterCommandsReady = !adapterLoadError
133
+ && REQUIRED_ADAPTER_COMMANDS.every(command => new RegExp(`\\b${command}\\b`, 'i').test(plugins.value));
131
134
  const opencliCompatible = versionAtLeast(cliVersion, MIN_OPENCLI_VERSION);
132
135
  const daemonStale = Boolean(status.daemonRunning && (!daemonVersion || normalizeVersion(daemonVersion) !== cliVersion));
133
136
  const connectivityOk = /\[OK\]\s+Connectivity:/i.test(doctor.value);
@@ -143,7 +146,7 @@ export class OpenCliRunner {
143
146
  adapterCompatible: pluginInstalled && adapterCommandsReady && versionAtLeast(pluginVersion ?? '', MIN_ADAPTER_VERSION),
144
147
  ...(version.error ? { versionError: version.error } : {}),
145
148
  ...(daemon.error ? { daemonError: daemon.error } : {}),
146
- ...(plugins.error ? { pluginError: plugins.error } : {}),
149
+ ...(plugins.error || adapterLoadError ? { pluginError: plugins.error || adapterLoadError } : {}),
147
150
  ...(doctor.error ? { doctorError: doctor.error } : {}),
148
151
  };
149
152
  }
@@ -164,12 +167,25 @@ export class OpenCliRunner {
164
167
  });
165
168
  }
166
169
  async installPlugin(pluginUrl, signal) {
167
- await this.raw(['plugin', 'install', pluginUrl], signal);
170
+ const before = await this.command(['plugin', 'list'], signal);
171
+ if (new RegExp(`\\b${ADAPTER_PLUGIN_NAME}\\b`, 'i').test(before.stdout)) {
172
+ await this.raw(['plugin', 'update', ADAPTER_PLUGIN_NAME], signal);
173
+ }
174
+ else {
175
+ await this.raw(['plugin', 'install', pluginUrl], signal);
176
+ }
177
+ const after = await this.command(['plugin', 'list'], signal);
178
+ const loadError = adapterPluginLoadError(after.stderr);
179
+ const commandsReady = REQUIRED_ADAPTER_COMMANDS.every(command => new RegExp(`\\b${command}\\b`, 'i').test(after.stdout));
180
+ if (loadError || !commandsReady) {
181
+ throw new OpenCliError(loadError || 'OpenCLI did not register all six conversation adapters', 'OPENCLI_CONFIGURATION');
182
+ }
168
183
  }
169
184
  async restartDaemon(signal) { await this.raw(['daemon', 'restart'], signal); }
170
185
  async probe(args, signal) {
171
186
  try {
172
- return { value: await this.raw(args, signal) };
187
+ const result = await this.command(args, signal);
188
+ return { value: result.stdout, ...(result.stderr.trim() ? { diagnostic: result.stderr.trim() } : {}) };
173
189
  }
174
190
  catch (error) {
175
191
  return { value: '', error: error instanceof Error ? error.message : String(error) };
@@ -194,9 +210,9 @@ export class OpenCliRunner {
194
210
  async json(site, operation, args, signal) {
195
211
  const queueKey = [this.executable, ...this.prefixArgs, this.profile, site].join('\0');
196
212
  return serializeSiteCommand(queueKey, async () => {
197
- // Pin adapter traffic to one persistent background session. OpenCLI
198
- // otherwise lets caller flags override the adapter lifecycle metadata,
199
- // which could release its tab or make an unattended sync steal focus.
213
+ // Pin adapter traffic to one persistent background session. Persistence
214
+ // suppresses OpenCLI's close-window action; the adapter closes only its
215
+ // completed task tab and leaves the background container reusable.
200
216
  const stdout = await this.raw([
201
217
  site, operation, ...args,
202
218
  '--site-session', 'persistent', '--window', 'background', '-f', 'json',
@@ -213,6 +229,9 @@ export class OpenCliRunner {
213
229
  });
214
230
  }
215
231
  async raw(args, signal) {
232
+ return (await this.command(args, signal)).stdout;
233
+ }
234
+ async command(args, signal) {
216
235
  const invocation = resolveInvocation(this.executable);
217
236
  const fullArgs = [...invocation.prefix, ...this.prefixArgs, ...(this.profile ? ['--profile', this.profile] : []), ...args];
218
237
  try {
@@ -220,7 +239,7 @@ export class OpenCliRunner {
220
239
  encoding: 'utf8', timeout: this.timeoutMs, maxBuffer: this.maxStdoutBytes,
221
240
  windowsHide: true, shell: false, signal, env: { ...process.env, OPENCLI_WINDOW: 'background' },
222
241
  });
223
- return result.stdout;
242
+ return { stdout: result.stdout, stderr: result.stderr };
224
243
  }
225
244
  catch (error) {
226
245
  const detail = error;
@@ -237,6 +256,13 @@ export class OpenCliRunner {
237
256
  }
238
257
  }
239
258
  }
259
+ /** Keep successful-process diagnostics: OpenCLI reports plugin import failures on stderr while exiting zero. */
260
+ function adapterPluginLoadError(diagnostic = '') {
261
+ const lines = diagnostic.split(/\r?\n/)
262
+ .map(line => line.trim())
263
+ .filter(line => /Plugin dsh-chat-history\//i.test(line));
264
+ return [...new Set(lines)].join('\n').slice(0, 2_000);
265
+ }
240
266
  async function serializeSiteCommand(key, task) {
241
267
  const previous = SITE_COMMAND_TAILS.get(key) ?? Promise.resolve();
242
268
  const current = previous.then(task, task);
@@ -97,6 +97,7 @@ export default class WebChatHistoryService extends Service implements ReferenceS
97
97
  profiles(signal?: AbortSignal): Promise<import("../../opencli.ts").BrowserProfile[]>;
98
98
  private runner;
99
99
  installAdapter(signal?: AbortSignal): Promise<boolean>;
100
+ private installAdapterFrom;
100
101
  /**
101
102
  * (Re)arm the background sync timer from the current settings.
102
103
  *
@@ -4,7 +4,9 @@ import { providerSchema, referenceAnythingDomainSpec, settingsRecordSchema } fro
4
4
  import { ConversationStore } from "../../store/store.js";
5
5
  import { ConversationSyncManager } from "../../sync/index.js";
6
6
  import { OpenCliError, OpenCliRunner, discoverOpenCli, installOpenCli as installOpenCliPackage } from "../../opencli.js";
7
- import { PackageUpdateManager } from "../../update.js";
7
+ import { PACKAGE_NAME, PackageUpdateManager } from "../../update.js";
8
+ import { join } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
8
10
  import { ReferenceAnythingError } from "../../errors.js";
9
11
  import { parseProviderQuery } from "../../search.js";
10
12
  export { parseProviderQuery };
@@ -27,7 +29,11 @@ export default class WebChatHistoryService extends Service {
27
29
  syncValue;
28
30
  autoSyncInterval;
29
31
  autoSyncCatchUp;
30
- packageUpdates = new PackageUpdateManager();
32
+ packageUpdates = new PackageUpdateManager({
33
+ afterInstall: async (profileDir, _version, signal) => {
34
+ await this.installAdapterFrom(pathToFileURL(join(profileDir, 'node_modules', PACKAGE_NAME, 'opencli-plugin')).href, signal);
35
+ },
36
+ });
31
37
  liveAttachments = new Map();
32
38
  constructor(ctx, config = {}) {
33
39
  super(ctx, 'referenceChatHistory');
@@ -234,11 +240,14 @@ export default class WebChatHistoryService extends Service {
234
240
  timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes });
235
241
  }
236
242
  async installAdapter(signal) {
243
+ await this.installAdapterFrom(new URL('../../../opencli-plugin/', import.meta.url).href, signal);
244
+ return true;
245
+ }
246
+ async installAdapterFrom(pluginUrl, signal) {
237
247
  const settings = this.store.settings;
238
248
  const runner = new OpenCliRunner({ executable: settings.opencliPath, profile: settings.profile,
239
249
  timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes });
240
- await runner.installPlugin(new URL('../../../opencli-plugin/', import.meta.url).href, signal);
241
- return true;
250
+ await runner.installPlugin(pluginUrl, signal);
242
251
  }
243
252
  /**
244
253
  * (Re)arm the background sync timer from the current settings.
package/lib/update.d.ts CHANGED
@@ -16,6 +16,7 @@ interface UpdateManagerOptions {
16
16
  dshHome?: string;
17
17
  fetchLatest?: (signal?: AbortSignal) => Promise<string>;
18
18
  install?: (profileDir: string, version: string, signal?: AbortSignal) => Promise<void>;
19
+ afterInstall?: (profileDir: string, version: string, signal?: AbortSignal) => Promise<void>;
19
20
  }
20
21
  /** One startup/manual-check cache and the confirmed profile-local updater. */
21
22
  export declare class PackageUpdateManager {
@@ -23,6 +24,7 @@ export declare class PackageUpdateManager {
23
24
  private readonly dshHome?;
24
25
  private readonly fetchLatest;
25
26
  private readonly install;
27
+ private readonly afterInstall?;
26
28
  private cached?;
27
29
  private checking?;
28
30
  constructor(options?: UpdateManagerOptions);
package/lib/update.js CHANGED
@@ -16,6 +16,7 @@ export class PackageUpdateManager {
16
16
  dshHome;
17
17
  fetchLatest;
18
18
  install;
19
+ afterInstall;
19
20
  cached;
20
21
  checking;
21
22
  constructor(options = {}) {
@@ -23,6 +24,7 @@ export class PackageUpdateManager {
23
24
  this.dshHome = options.dshHome;
24
25
  this.fetchLatest = options.fetchLatest ?? fetchLatestVersion;
25
26
  this.install = options.install ?? installPackageVersion;
27
+ this.afterInstall = options.afterInstall;
26
28
  }
27
29
  /** Return the startup result, waiting for it when it is still in flight. */
28
30
  status(signal) {
@@ -52,6 +54,7 @@ export class PackageUpdateManager {
52
54
  return { version: status.currentVersion, restartRequired: false };
53
55
  const profileDir = await findOwningProfileDir(this.packageRoot, this.dshHome);
54
56
  await this.install(profileDir, status.latestVersion, signal);
57
+ await this.afterInstall?.(profileDir, status.latestVersion, signal);
55
58
  this.cached = {
56
59
  currentVersion: status.latestVersion,
57
60
  latestVersion: status.latestVersion,
@@ -38,7 +38,7 @@ async function evaluate(page, script, args, operation) {
38
38
  return parseEvaluate(await page.evaluate(source), operation)
39
39
  }
40
40
 
41
- function assertResult(result, domain, operation) {
41
+ function assertResult(result, domain, operation) {
42
42
  if (result?.code === 'AUTH') throw new AuthRequiredError(domain, result.message || `${operation} requires login`)
43
43
  if (result?.code === 'RATE_LIMIT') {
44
44
  throw new CommandExecutionError(`${operation}: ${result.message || 'provider rate limit reached'}`)
@@ -46,9 +46,26 @@ function assertResult(result, domain, operation) {
46
46
  if (result?.ok !== true || !Array.isArray(result.rows)) {
47
47
  throw new CommandExecutionError(`${operation}: ${result?.message || 'provider adapter did not return rows'}`)
48
48
  }
49
- return result.rows
50
- }
51
-
49
+ return result.rows
50
+ }
51
+
52
+ /**
53
+ * Close only the tab used by this adapter command. The persistent site session
54
+ * prevents OpenCLI from sending its `close-window` lease-release action, while
55
+ * Browser Bridge can keep and reuse the background container window.
56
+ */
57
+ async function inTemporaryTab(page, task) {
58
+ try {
59
+ return await task()
60
+ } finally {
61
+ try {
62
+ await page.closeTab?.()
63
+ } catch {
64
+ // Preserve the provider result/error if the tab or browser closed first.
65
+ }
66
+ }
67
+ }
68
+
52
69
  export function registerProvider(config) {
53
70
  const browserSession = SYNC_BROWSER_SESSION
54
71
 
@@ -64,7 +81,7 @@ export function registerProvider(config) {
64
81
  navigateBefore: false,
65
82
  args: [],
66
83
  columns: ['identity'],
67
- func: async (page) => {
84
+ func: async (page) => inTemporaryTab(page, async () => {
68
85
  await page.goto(config.home, { settleMs: 500 })
69
86
  const result = await evaluate(page, config.whoamiScript, {}, `${config.site} whoami`)
70
87
  if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
@@ -72,7 +89,7 @@ export function registerProvider(config) {
72
89
  throw new CommandExecutionError(`${config.site} whoami: ${result?.message || 'stable account identity unavailable'}`)
73
90
  }
74
91
  return [{ identity: result.identity }]
75
- },
92
+ }),
76
93
  })
77
94
 
78
95
  cli({
@@ -90,7 +107,7 @@ export function registerProvider(config) {
90
107
  help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
91
108
  }, { name: 'accountScope', help: 'Previously observed hashed account scope; mismatch forces a full listing' }],
92
109
  columns: SYNC_INDEX_COLUMNS,
93
- func: async (page, kwargs) => {
110
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
94
111
  const since = String(kwargs.since || '').trim()
95
112
  if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
96
113
  await page.goto(config.home, { settleMs: 1200 })
@@ -110,7 +127,7 @@ export function registerProvider(config) {
110
127
  { kind: 'identity', identity: identity.identity, sinceApplied: effectiveSince },
111
128
  ...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
112
129
  ]
113
- },
130
+ }),
114
131
  })
115
132
 
116
133
  cli({
@@ -128,7 +145,7 @@ export function registerProvider(config) {
128
145
  help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
129
146
  }],
130
147
  columns: HISTORY_COLUMNS,
131
- func: async (page, kwargs) => {
148
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
132
149
  const since = String(kwargs.since || '').trim()
133
150
  if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
134
151
  await page.goto(config.home, { settleMs: 1200 })
@@ -138,7 +155,7 @@ export function registerProvider(config) {
138
155
  `${config.site} history-all`,
139
156
  )
140
157
  return rows
141
- },
158
+ }),
142
159
  })
143
160
 
144
161
  cli({
@@ -156,7 +173,7 @@ export function registerProvider(config) {
156
173
  { name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
157
174
  ],
158
175
  columns: DETAIL_COLUMNS,
159
- func: async (page, kwargs) => {
176
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
160
177
  const id = String(kwargs.id || '').trim()
161
178
  if (!id) throw new ArgumentError('id must be a non-empty conversation id')
162
179
  await page.goto(config.home, { settleMs: 600 })
@@ -180,7 +197,7 @@ export function registerProvider(config) {
180
197
  const rows = assertResult(result, config.domain, `${config.site} detail`)
181
198
  if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
182
199
  return rows
183
- },
200
+ }),
184
201
  })
185
202
 
186
203
  cli({
@@ -199,7 +216,7 @@ export function registerProvider(config) {
199
216
  { name: 'maxBytes', type: 'int', default: 26214400, help: 'Maximum decoded bytes' },
200
217
  ],
201
218
  columns: ['attachmentId', 'name', 'mimeType', 'size', 'status', 'localPath'],
202
- func: async (page, kwargs) => {
219
+ func: async (page, kwargs) => inTemporaryTab(page, async () => {
203
220
  const locator = String(kwargs.locator || '').trim()
204
221
  const output = resolve(String(kwargs.output || ''))
205
222
  const maxBytes = Number(kwargs.maxBytes ?? 26_214_400)
@@ -229,15 +246,14 @@ export function registerProvider(config) {
229
246
  status: 'available',
230
247
  localPath: output,
231
248
  }]
232
- },
249
+ }),
233
250
  })
234
251
  }
235
252
 
236
253
  /**
237
254
  * Keep one stable, background Browser Bridge session per provider site.
238
- * OpenCLI maps persistent sessions to `site:<config.site>` and keeps their tab
239
- * lease after both successful and failed commands, so later sync work reuses
240
- * the same tab. If the user closes it manually, OpenCLI recreates it on demand.
255
+ * Persistence suppresses OpenCLI's `close-window` action; inTemporaryTab closes
256
+ * only the finished command's tab so the reusable container window stays alive.
241
257
  */
242
258
  export const SYNC_BROWSER_SESSION = Object.freeze({
243
259
  siteSession: 'persistent',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-reference-anything",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Reference locally mirrored ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi conversations from DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {