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 +18 -2
- package/lib/client.js +13 -15
- package/lib/opencli.d.ts +1 -0
- package/lib/opencli.js +37 -11
- package/lib/sources/web-chat/index.d.ts +1 -0
- package/lib/sources/web-chat/index.js +13 -4
- package/lib/update.d.ts +2 -0
- package/lib/update.js +3 -0
- package/opencli-plugin/common.js +33 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,13 @@ One `@` to reference them all.
|
|
|
22
22
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
-
|
|
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.
|
|
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\
|
|
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
|
|
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.
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
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
|
|
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
|
|
19
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
198
|
-
//
|
|
199
|
-
//
|
|
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(
|
|
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,
|
package/opencli-plugin/common.js
CHANGED
|
@@ -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
|
-
*
|
|
239
|
-
*
|
|
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