dsh-reference-anything 0.4.2 → 0.4.4

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/contract.d.ts CHANGED
@@ -44,6 +44,20 @@ export declare const driveCandidateSchema: z.ZodReadonly<z.ZodObject<{
44
44
  searchIncomplete: z.ZodOptional<z.ZodBoolean>;
45
45
  isDirectory: z.ZodOptional<z.ZodBoolean>;
46
46
  }, z.core.$strict>>;
47
+ export declare const localPluginCandidateSchema: z.ZodReadonly<z.ZodObject<{
48
+ id: z.ZodString;
49
+ name: z.ZodString;
50
+ label: z.ZodString;
51
+ version: z.ZodOptional<z.ZodString>;
52
+ status: z.ZodEnum<{
53
+ live: "live";
54
+ disabled: "disabled";
55
+ inert: "inert";
56
+ }>;
57
+ type: z.ZodOptional<z.ZodString>;
58
+ directory: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strict>>;
60
+ export type LocalPluginCandidate = z.infer<typeof localPluginCandidateSchema>;
47
61
  export declare const searchInputSchema: z.ZodReadonly<z.ZodObject<{
48
62
  query: z.ZodString;
49
63
  provider: z.ZodOptional<z.ZodEnum<{
@@ -114,6 +128,8 @@ export declare const healthSchema: z.ZodReadonly<z.ZodObject<{
114
128
  pluginVersion: z.ZodOptional<z.ZodString>;
115
129
  adapterCommandsReady: z.ZodBoolean;
116
130
  adapterCompatible: z.ZodBoolean;
131
+ latestVersion: z.ZodOptional<z.ZodString>;
132
+ updateAvailable: z.ZodBoolean;
117
133
  versionError: z.ZodOptional<z.ZodString>;
118
134
  daemonError: z.ZodOptional<z.ZodString>;
119
135
  pluginError: z.ZodOptional<z.ZodString>;
@@ -305,6 +321,7 @@ export declare const deleteInputSchema: z.ZodReadonly<z.ZodObject<{
305
321
  uriId: z.ZodString;
306
322
  }, z.core.$strip>>;
307
323
  export declare const storageStatsSchema: z.ZodReadonly<z.ZodObject<{
324
+ systemTempDirectory: z.ZodOptional<z.ZodString>;
308
325
  bytes: z.ZodNumber;
309
326
  conversations: z.ZodNumber;
310
327
  remoteMissing: z.ZodDefault<z.ZodNumber>;
@@ -409,8 +426,8 @@ export declare const openListMountSchema: z.ZodReadonly<z.ZodObject<{
409
426
  enabled: z.ZodBoolean;
410
427
  status: z.ZodOptional<z.ZodEnum<{
411
428
  error: "error";
412
- ready: "ready";
413
429
  disabled: "disabled";
430
+ ready: "ready";
414
431
  }>>;
415
432
  error: z.ZodOptional<z.ZodString>;
416
433
  capacityUsed: z.ZodOptional<z.ZodNumber>;
package/lib/contract.js CHANGED
@@ -19,6 +19,15 @@ export const driveCandidateSchema = z.object({
19
19
  origin: z.string().optional(), updatedAt: z.number().optional(),
20
20
  searchIncomplete: z.boolean().optional(), isDirectory: z.boolean().optional(),
21
21
  }).strict().readonly();
22
+ export const localPluginCandidateSchema = z.object({
23
+ id: z.string(),
24
+ name: z.string(),
25
+ label: z.string(),
26
+ version: z.string().optional(),
27
+ status: z.enum(['live', 'disabled', 'inert']),
28
+ type: z.string().optional(),
29
+ directory: z.string().optional(),
30
+ }).strict().readonly();
22
31
  /**
23
32
  * Binds the calling session to the invocation, so the Host can scope a search
24
33
  * to the session's cwd. Reintroduced here after 0.3.0 handed file and session
@@ -41,7 +50,7 @@ export const healthSchema = z.object({
41
50
  daemonRunning: z.boolean(), extensionConnected: z.boolean(), extensionState: extensionStateSchema,
42
51
  extensionVersion: z.string().optional(), profileCount: z.number().int().nonnegative().optional(),
43
52
  opencliCompatible: z.boolean(), daemonVersion: z.string().optional(), daemonStale: z.boolean(),
44
- connectivityOk: z.boolean(), connectivityChecked: z.boolean(), pluginVersion: z.string().optional(), adapterCommandsReady: z.boolean(), adapterCompatible: z.boolean(),
53
+ connectivityOk: z.boolean(), connectivityChecked: z.boolean(), pluginVersion: z.string().optional(), adapterCommandsReady: z.boolean(), adapterCompatible: z.boolean(), latestVersion: z.string().optional(), updateAvailable: z.boolean(),
45
54
  versionError: z.string().optional(), daemonError: z.string().optional(), pluginError: z.string().optional(), doctorError: z.string().optional(),
46
55
  }).readonly();
47
56
  export const browserProfileSchema = z.object({ id: z.string(), alias: z.string().optional(), connected: z.boolean(), isDefault: z.boolean() }).readonly();
@@ -84,6 +93,7 @@ export const browsePageSchema = z.object({
84
93
  }).readonly();
85
94
  export const deleteInputSchema = z.object({ uriId: z.string().min(1) }).readonly();
86
95
  export const storageStatsSchema = z.object({
96
+ systemTempDirectory: z.string().optional(),
87
97
  bytes: z.number().int().nonnegative(), conversations: z.number().int().nonnegative(),
88
98
  remoteMissing: z.number().int().nonnegative().default(0), oldAccountConversations: z.number().int().nonnegative().default(0),
89
99
  }).readonly();
@@ -157,6 +167,7 @@ export const REFERENCE_ANYTHING_INVOCATIONS = [
157
167
  descriptor('openListDisableMount', [{ name: 'input', wire: 'input', source: 'json', codec: strict('OpenListDisableMountInput', openListDisableMountSchema) }], strict('Boolean', z.boolean()), true),
158
168
  descriptor('openListRemoveMount', [{ name: 'input', wire: 'input', source: 'json', codec: strict('OpenListMountInput', openListMountInputSchema) }], strict('Boolean', z.boolean()), true),
159
169
  descriptor('openListReindex', [{ name: 'input', wire: 'input', source: 'json', codec: strict('OpenListMountInput', openListMountInputSchema) }], strict('OpenListReindex', z.object({ supported: z.boolean(), reason: z.string().optional() }).readonly()), true),
170
+ descriptor('localPlugins', [], strict('LocalPluginCandidate[]', z.array(localPluginCandidateSchema)), true),
160
171
  ];
161
172
  function strict(type, schema) {
162
173
  return { mode: 'strict', typeSymbol: `dsh-reference-anything#${type}`, schema, create: () => schema };
package/lib/host.d.ts CHANGED
@@ -67,7 +67,7 @@ export declare class ReferenceAnythingRemote extends TypertRemoteService {
67
67
  syncStart(input: {
68
68
  providers: ChatProvider[];
69
69
  mode: SyncMode;
70
- }): string;
70
+ }): Promise<string>;
71
71
  syncStatus(input: {
72
72
  jobId: string;
73
73
  }): import("./sync/index.ts").SyncJobStatus | undefined;
@@ -98,42 +98,82 @@ export declare class ReferenceAnythingRemote extends TypertRemoteService {
98
98
  order: number;
99
99
  limit: number;
100
100
  maxCandidates: number;
101
+ sort: "name" | "updated";
102
+ shortcut: string;
103
+ aliases?: string[] | undefined;
104
+ sourceAliases?: Record<string, string[]> | undefined;
101
105
  };
102
106
  skills: {
103
107
  enabled: boolean;
104
108
  order: number;
105
109
  limit: number;
106
110
  maxCandidates: number;
111
+ sort: "name" | "updated";
112
+ shortcut: string;
113
+ aliases?: string[] | undefined;
114
+ sourceAliases?: Record<string, string[]> | undefined;
107
115
  };
108
116
  files: {
109
117
  enabled: boolean;
110
118
  order: number;
111
119
  limit: number;
112
120
  maxCandidates: number;
121
+ sort: "name" | "updated";
122
+ shortcut: string;
123
+ aliases?: string[] | undefined;
124
+ sourceAliases?: Record<string, string[]> | undefined;
113
125
  };
114
126
  sessions: {
115
127
  enabled: boolean;
116
128
  order: number;
117
129
  limit: number;
118
130
  maxCandidates: number;
131
+ sort: "name" | "updated";
132
+ shortcut: string;
133
+ aliases?: string[] | undefined;
134
+ sourceAliases?: Record<string, string[]> | undefined;
119
135
  };
120
136
  agents: {
121
137
  enabled: boolean;
122
138
  order: number;
123
139
  limit: number;
124
140
  maxCandidates: number;
141
+ sort: "name" | "updated";
142
+ shortcut: string;
143
+ aliases?: string[] | undefined;
144
+ sourceAliases?: Record<string, string[]> | undefined;
125
145
  };
126
146
  conversations: {
127
147
  enabled: boolean;
128
148
  order: number;
129
149
  limit: number;
130
150
  maxCandidates: number;
151
+ sort: "name" | "updated";
152
+ shortcut: string;
153
+ aliases?: string[] | undefined;
154
+ sourceAliases?: Record<string, string[]> | undefined;
131
155
  };
132
156
  drives: {
133
157
  enabled: boolean;
134
158
  order: number;
135
159
  limit: number;
136
160
  maxCandidates: number;
161
+ sort: "name" | "updated";
162
+ shortcut: string;
163
+ aliases?: string[] | undefined;
164
+ sourceAliases?: Record<string, string[]> | undefined;
165
+ };
166
+ plugins: {
167
+ enabled: boolean;
168
+ order: number;
169
+ limit: number;
170
+ maxCandidates: number;
171
+ sort: "name" | "updated";
172
+ shortcut: string;
173
+ onlyLive: boolean;
174
+ showDeepSeek: boolean;
175
+ aliases?: string[] | undefined;
176
+ sourceAliases?: Record<string, string[]> | undefined;
137
177
  };
138
178
  } | undefined;
139
179
  }>;
@@ -150,7 +190,13 @@ export declare class ReferenceAnythingRemote extends TypertRemoteService {
150
190
  deleteConversation(input: {
151
191
  uriId: string;
152
192
  }, signal: AbortSignal): Promise<boolean>;
153
- storageStats(): import("./store/store.ts").StorageStats;
193
+ storageStats(): {
194
+ systemTempDirectory: string;
195
+ bytes: number;
196
+ conversations: number;
197
+ remoteMissing: number;
198
+ oldAccountConversations: number;
199
+ };
154
200
  clearProvider(input: {
155
201
  provider: ChatProvider;
156
202
  }, signal: AbortSignal): Promise<number>;
@@ -191,4 +237,13 @@ export declare class ReferenceAnythingRemote extends TypertRemoteService {
191
237
  }, signal: AbortSignal): Promise<{
192
238
  supported: true;
193
239
  }>;
240
+ localPlugins(signal: AbortSignal): Promise<readonly Readonly<{
241
+ id: string;
242
+ name: string;
243
+ label: string;
244
+ status: "live" | "disabled" | "inert";
245
+ version?: string | undefined;
246
+ type?: string | undefined;
247
+ directory?: string | undefined;
248
+ }>[]>;
194
249
  }
package/lib/host.js CHANGED
@@ -2,6 +2,7 @@ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { validateDownloadDirectory } from "./download-directory.js";
5
+ import { resolveLocalPlugins } from "./local-plugins.js";
5
6
  export class ReferenceAnythingRemote extends TypertRemoteService {
6
7
  constructor(ctx) { super(ctx, 'referenceAnything'); }
7
8
  /**
@@ -75,7 +76,8 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
75
76
  checkUpdate(signal) { return this.ctx.referenceChatHistory.checkUpdate(signal); }
76
77
  installUpdate(signal) { return this.ctx.referenceChatHistory.installUpdate(signal); }
77
78
  stats() { return this.ctx.referenceChatHistory.stats(); }
78
- syncStart(input) {
79
+ async syncStart(input) {
80
+ await this.ctx.referenceChatHistory.ensureBrowser();
79
81
  return this.ctx.referenceChatHistory.sync.start(input.providers, input.mode);
80
82
  }
81
83
  syncStatus(input) { return this.ctx.referenceChatHistory.sync.status(input.jobId); }
@@ -107,7 +109,7 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
107
109
  signal.throwIfAborted();
108
110
  return this.ctx.referenceChatHistory.remove(input.uriId);
109
111
  }
110
- storageStats() { return this.ctx.referenceChatHistory.storageStats(); }
112
+ storageStats() { return { ...this.ctx.referenceChatHistory.storageStats(), systemTempDirectory: tmpdir() }; }
111
113
  clearProvider(input, signal) {
112
114
  signal.throwIfAborted();
113
115
  return this.ctx.referenceChatHistory.removeProvider(input.provider);
@@ -132,4 +134,8 @@ export class ReferenceAnythingRemote extends TypertRemoteService {
132
134
  async openListDisableMount(input, signal) { await this.ctx.openListManager.disableMount(input.id, input.disabled ?? true, signal); return true; }
133
135
  async openListRemoveMount(input, signal) { await this.ctx.openListManager.removeMount(input.id, signal); return true; }
134
136
  openListReindex(input, signal) { return this.ctx.openListManager.reindexMount(input.id, signal); }
137
+ localPlugins(signal) {
138
+ signal.throwIfAborted();
139
+ return resolveLocalPlugins(this.ctx);
140
+ }
135
141
  }
package/lib/index.js CHANGED
@@ -79,8 +79,6 @@ export default class ReferenceRuntime extends Service {
79
79
  static grantKey(ref) { return `${ref.source}\u0000${ref.id}`; }
80
80
  static contentMentions(blocks, key) {
81
81
  return blocks.some(block => {
82
- if (block.type === 'tool-result')
83
- return ReferenceRuntime.contentMentions(block.content, key);
84
82
  if (block.type !== 'text' || !mayContainReference(block.text))
85
83
  return false;
86
84
  try {
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { LocalPluginCandidate } from './contract.ts';
3
+ import type { ReferenceSource } from './types.ts';
4
+ export declare function createPluginReferenceSource(ctx: Context): ReferenceSource;
5
+ /** Read the active loader's composed entries, including bundle patches and effective disables. */
6
+ export declare function resolveLocalPlugins(ctx: Context): Promise<readonly LocalPluginCandidate[]>;
@@ -0,0 +1,119 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, isAbsolute, join } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
+ import { sliceTurns } from "./window.js";
7
+ export function createPluginReferenceSource(ctx) {
8
+ return {
9
+ id: 'local-plugin', requiresGrant: true,
10
+ available: async () => Boolean(ctx.get('loader')),
11
+ async list(query, limit, signal) {
12
+ signal?.throwIfAborted();
13
+ return (await resolveLocalPlugins(ctx)).filter(row => row.name.toLowerCase().includes(query.toLowerCase())).slice(0, limit)
14
+ .map(row => ({ ref: { source: 'local-plugin', id: row.name }, label: row.name }));
15
+ },
16
+ async read(ref, window, signal) {
17
+ signal?.throwIfAborted();
18
+ const plugin = (await resolveLocalPlugins(ctx)).find(row => row.name === ref.id);
19
+ if (!plugin?.directory)
20
+ throw new Error(`Plugin directory unavailable: ${ref.id}`);
21
+ return {
22
+ ref, label: plugin.name, partial: false, capturedAt: Date.now(),
23
+ body: sliceTurns([{ role: 'document', text: JSON.stringify({ name: plugin.name, version: plugin.version, directory: plugin.directory, note: 'Use file tools to inspect this plugin directory. Source contents have not been read.' }) }], window),
24
+ };
25
+ },
26
+ };
27
+ }
28
+ const manifestSchema = z.object({
29
+ name: z.string().optional(), version: z.string().optional(),
30
+ dependencies: z.record(z.string(), z.string()).optional(),
31
+ dsh: z.object({
32
+ bundle: z.object({ patch: z.string() }).optional(),
33
+ client: z.unknown().optional(),
34
+ profile: z.object({ bundles: z.array(z.string()).optional() }).optional(),
35
+ }).optional(),
36
+ });
37
+ async function readManifest(directory) {
38
+ try {
39
+ return manifestSchema.parse(JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')));
40
+ }
41
+ catch {
42
+ return undefined; /* Missing or invalid optional metadata must not hide a configured entry. */
43
+ }
44
+ }
45
+ async function pluginPackage(name, baseUrl) {
46
+ if (!baseUrl?.startsWith('file:'))
47
+ return undefined;
48
+ const require = createRequire(new URL('./package.json', baseUrl));
49
+ if (!name.startsWith('.') && !name.startsWith('/') && !name.includes(':')) {
50
+ const packageName = name.split('/').slice(0, name.startsWith('@') ? 2 : 1).join('/');
51
+ for (const searchPath of require.resolve.paths(packageName) ?? []) {
52
+ const directory = join(searchPath, packageName);
53
+ const manifest = await readManifest(directory);
54
+ if (manifest)
55
+ return { directory: await realpath(directory), manifest };
56
+ }
57
+ return undefined;
58
+ }
59
+ try {
60
+ const url = isAbsolute(name) ? pathToFileURL(name) : new URL(name.replace(/\\/gu, '/'), baseUrl);
61
+ const path = await realpath(fileURLToPath(url));
62
+ const directory = (await stat(path)).isDirectory() ? path : dirname(path);
63
+ let parent = directory;
64
+ while (true) {
65
+ const manifest = await readManifest(parent);
66
+ if (manifest)
67
+ return manifest.dsh?.profile ? { directory, manifest: {} } : { directory: parent, manifest };
68
+ if (dirname(parent) === parent)
69
+ return { directory, manifest: {} };
70
+ parent = dirname(parent);
71
+ }
72
+ }
73
+ catch {
74
+ return undefined; /* A disabled or failed local entry may no longer exist on disk. */
75
+ }
76
+ }
77
+ const STATUS_ORDER = { live: 0, inert: 1, disabled: 2 };
78
+ // Cordis publishes FiberState as an ambient const enum, unavailable with verbatimModuleSyntax.
79
+ const ACTIVE = 2; // FiberState.ACTIVE
80
+ /** Read the active loader's composed entries, including bundle patches and effective disables. */
81
+ export async function resolveLocalPlugins(ctx) {
82
+ const loader = ctx.get('loader');
83
+ if (!loader)
84
+ return [];
85
+ const plugins = new Map();
86
+ for (const entry of loader.entries()) {
87
+ if (entry.options.name.startsWith('cordis:') || entry.options.group)
88
+ continue;
89
+ const info = await pluginPackage(entry.options.name, entry.parent.tree.ctx.baseUrl);
90
+ const name = info?.manifest.name ?? entry.options.name;
91
+ const status = entry.disabled ? 'disabled' : entry.fiber?.state === ACTIVE ? 'live' : 'inert';
92
+ const previous = plugins.get(name);
93
+ if (previous && STATUS_ORDER[previous.status] <= STATUS_ORDER[status])
94
+ continue;
95
+ plugins.set(name, {
96
+ id: entry.id, name, label: name, status,
97
+ ...(info ? { directory: info.directory, version: info.manifest.version } : {}),
98
+ });
99
+ }
100
+ // Installed bundles/client plugins can exist without a host entry. Ordinary dependencies are omitted.
101
+ const baseUrl = loader.root.ctx.baseUrl;
102
+ if (baseUrl?.startsWith('file:')) {
103
+ const profile = await readManifest(fileURLToPath(new URL('.', baseUrl)));
104
+ const bundles = new Set(profile?.dsh?.profile?.bundles ?? []);
105
+ for (const packageName of new Set([...Object.keys(profile?.dependencies ?? {}), ...bundles])) {
106
+ const info = await pluginPackage(packageName, baseUrl);
107
+ if (!bundles.has(packageName) && !info?.manifest.dsh?.bundle && !info?.manifest.dsh?.client)
108
+ continue;
109
+ const name = info?.manifest.name ?? packageName;
110
+ if (plugins.has(name))
111
+ continue;
112
+ plugins.set(name, {
113
+ id: packageName, name, label: name, status: 'inert',
114
+ ...(info ? { directory: info.directory, version: info.manifest.version } : {}),
115
+ });
116
+ }
117
+ }
118
+ return [...plugins.values()].sort((a, b) => STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || a.name.localeCompare(b.name));
119
+ }
package/lib/opencli.d.ts CHANGED
@@ -38,6 +38,8 @@ export interface OpenCliHealth {
38
38
  pluginVersion?: string;
39
39
  adapterCommandsReady: boolean;
40
40
  adapterCompatible: boolean;
41
+ latestVersion?: string;
42
+ updateAvailable: boolean;
41
43
  versionError?: string;
42
44
  daemonError?: string;
43
45
  pluginError?: string;
@@ -107,6 +109,7 @@ export declare class OpenCliRunner {
107
109
  /** Confirm the executable also exposes OpenCLI's plugin command, not merely a semver-looking --version. */
108
110
  verifyInstallation(signal?: AbortSignal): Promise<void>;
109
111
  profiles(signal?: AbortSignal): Promise<BrowserProfile[]>;
112
+ ensureBrowser(signal?: AbortSignal): Promise<void>;
110
113
  installPlugin(pluginUrl: string, signal?: AbortSignal): Promise<void>;
111
114
  restartDaemon(signal?: AbortSignal): Promise<void>;
112
115
  private probe;
package/lib/opencli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execFile as nodeExecFile } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
- import { dirname, join, resolve } from 'node:path';
3
+ import { delimiter, dirname, join, resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { spawn } from 'node:child_process';
5
6
  import { createHash } from 'node:crypto';
6
7
  const execFile = promisify(nodeExecFile);
7
8
  export const MIN_OPENCLI_VERSION = '1.8.6';
@@ -124,9 +125,10 @@ export class OpenCliRunner {
124
125
  return this.inspectHealth(false, signal);
125
126
  }
126
127
  async inspectHealth(checkConnectivity, signal) {
127
- const [version, daemon, plugins, doctor] = await Promise.all([
128
+ const [version, daemon, plugins, doctor, latest] = await Promise.all([
128
129
  this.probe(['--version'], signal), this.probe(['daemon', 'status'], signal), this.probe(['plugin', 'list'], signal),
129
130
  checkConnectivity ? this.probe(['doctor'], signal) : Promise.resolve({ value: '', diagnostic: '' }),
131
+ latestOpenCliVersion(signal),
130
132
  ]);
131
133
  const status = parseDaemonStatus(daemon.value);
132
134
  const cliVersion = normalizeVersion(version.value);
@@ -149,6 +151,8 @@ export class OpenCliRunner {
149
151
  ...(pluginVersion ? { pluginVersion: normalizeVersion(pluginVersion) } : {}),
150
152
  adapterCommandsReady,
151
153
  adapterCompatible: pluginInstalled && adapterCommandsReady && versionAtLeast(pluginVersion ?? '', MIN_ADAPTER_VERSION),
154
+ ...(latest.value.trim() ? { latestVersion: normalizeVersion(latest.value) } : {}),
155
+ updateAvailable: Boolean(latest.value.trim() && versionAtLeast(normalizeVersion(latest.value), cliVersion) && normalizeVersion(latest.value) !== cliVersion),
152
156
  ...(version.error ? { versionError: version.error } : {}),
153
157
  ...(daemon.error ? { daemonError: daemon.error } : {}),
154
158
  ...(plugins.error || adapterLoadError ? { pluginError: plugins.error || adapterLoadError } : {}),
@@ -171,6 +175,22 @@ export class OpenCliRunner {
171
175
  connected: match[3] === 'connected' && (disconnected < 0 || position < disconnected), isDefault: /\sdefault\s+—/.test(line) }];
172
176
  });
173
177
  }
178
+ async ensureBrowser(signal) {
179
+ if ((await this.profiles(signal)).some(profile => profile.connected))
180
+ return;
181
+ const child = process.platform === 'win32'
182
+ ? spawn('cmd.exe', ['/c', 'start', '', 'msedge.exe', 'about:blank'], { detached: true, stdio: 'ignore', windowsHide: true })
183
+ : process.platform === 'darwin'
184
+ ? spawn('open', ['-a', 'Google Chrome', 'about:blank'], { detached: true, stdio: 'ignore' })
185
+ : spawn('xdg-open', ['about:blank'], { detached: true, stdio: 'ignore' });
186
+ child.unref();
187
+ for (let i = 0; i < 20; i++) {
188
+ await new Promise(resolve => setTimeout(resolve, 500));
189
+ if ((await this.profiles(signal)).some(profile => profile.connected))
190
+ return;
191
+ }
192
+ throw new OpenCliError('Browser started, but the OpenCLI extension did not connect', 'EXTENSION_NOT_CONNECTED');
193
+ }
174
194
  async installPlugin(pluginUrl, signal) {
175
195
  const before = await this.command(['plugin', 'list'], signal);
176
196
  if (new RegExp(`\\b${ADAPTER_PLUGIN_NAME}\\b`, 'i').test(before.stdout)) {
@@ -259,6 +279,18 @@ export class OpenCliRunner {
259
279
  }
260
280
  }
261
281
  }
282
+ async function latestOpenCliVersion(signal) {
283
+ const npm = resolveNpmInvocation();
284
+ if (!npm)
285
+ return { value: '', diagnostic: '' };
286
+ try {
287
+ const result = await execFile(npm.file, [...npm.prefix, 'view', OPENCLI_NPM_PACKAGE, 'version'], { encoding: 'utf8', timeout: 15_000, signal });
288
+ return { value: String(result.stdout).trim(), diagnostic: '' };
289
+ }
290
+ catch {
291
+ return { value: '', diagnostic: '' };
292
+ }
293
+ }
262
294
  /** Keep successful-process diagnostics: OpenCLI reports plugin import failures on stderr while exiting zero. */
263
295
  function adapterPluginLoadError(diagnostic = '') {
264
296
  const lines = diagnostic.split(/\r?\n/)
@@ -323,19 +355,40 @@ function openCliCandidates(configured) {
323
355
  return [...new Set(candidates.filter(Boolean))];
324
356
  }
325
357
  function resolveNpmInvocation() {
358
+ const node = resolveNodeExecutable();
326
359
  const npmExecPath = process.env.npm_execpath;
327
360
  if (npmExecPath && existsSync(npmExecPath)) {
328
361
  return /\.(?:c?js|mjs)$/i.test(npmExecPath)
329
- ? { file: process.execPath, prefix: [npmExecPath] }
362
+ ? { file: node, prefix: [npmExecPath] }
330
363
  : { file: npmExecPath, prefix: [] };
331
364
  }
332
- const besideNode = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
333
- if (existsSync(besideNode))
334
- return { file: process.execPath, prefix: [besideNode] };
365
+ const npmEntries = [
366
+ join(dirname(node), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
367
+ join(dirname(node), '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
368
+ ];
369
+ for (const entry of npmEntries)
370
+ if (existsSync(entry))
371
+ return { file: node, prefix: [entry] };
335
372
  if (process.platform !== 'win32')
336
373
  return { file: 'npm', prefix: [] };
337
374
  return undefined;
338
375
  }
376
+ /** Electron Node mode still advertises Electron, so CLI parsers misread argv. */
377
+ function resolveNodeExecutable(shimDirectory) {
378
+ if (!process.versions.electron)
379
+ return process.execPath;
380
+ const binary = process.platform === 'win32' ? 'node.exe' : 'node';
381
+ const resources = process.platform === 'darwin'
382
+ ? resolve(dirname(process.execPath), '..', 'Resources')
383
+ : join(dirname(process.execPath), 'resources');
384
+ const candidates = [
385
+ ...(shimDirectory ? [join(shimDirectory, binary)] : []),
386
+ // Official Desktop ships a standalone dependency runtime as well as Electron.
387
+ join(resources, 'runtime', 'primary-runtime', 'dependencies', 'node', 'bin', binary),
388
+ ...(process.env.PATH ?? '').split(delimiter).filter(Boolean).map(path => join(path.replace(/^"|"$/g, ''), binary)),
389
+ ];
390
+ return candidates.find(candidate => existsSync(candidate) && resolve(candidate) !== resolve(process.execPath)) ?? 'node';
391
+ }
339
392
  function normalizeVersion(value) { return value.trim().replace(/^v/i, '').split(/\s+/)[0] ?? ''; }
340
393
  /** Numeric semver comparison for the stable x.y.z versions OpenCLI publishes. */
341
394
  export function versionAtLeast(value, minimum) {
@@ -357,12 +410,12 @@ function resolveInvocation(configured) {
357
410
  if (/\.cmd$/i.test(configured) && existsSync(explicit)) {
358
411
  const entry = join(explicit, '..', 'node_modules', '@jackwener', 'opencli', 'dist', 'src', 'main.js');
359
412
  if (existsSync(entry))
360
- return { file: process.execPath, prefix: [entry] };
413
+ return { file: resolveNodeExecutable(dirname(explicit)), prefix: [entry] };
361
414
  }
362
415
  if (process.platform === 'win32' && configured === 'opencli') {
363
416
  const entry = process.env.APPDATA && join(process.env.APPDATA, 'npm', 'node_modules', '@jackwener', 'opencli', 'dist', 'src', 'main.js');
364
417
  if (entry && existsSync(entry))
365
- return { file: process.execPath, prefix: [entry] };
418
+ return { file: resolveNodeExecutable(join(process.env.APPDATA, 'npm')), prefix: [entry] };
366
419
  }
367
420
  return { file: configured, prefix: [] };
368
421
  }
package/lib/reference.js CHANGED
@@ -158,8 +158,7 @@ function notice(summary) {
158
158
  text: `A conversation reference in the previous message could not be resolved: ${summary}`,
159
159
  }],
160
160
  source: {
161
- kind: 'plugin',
162
- plugin: name,
161
+ kind: 'reference-anything',
163
162
  form: 'notice',
164
163
  summary: boundContextSummary(summary),
165
164
  },
@@ -100,6 +100,7 @@ export default class WebChatHistoryService extends Service implements ReferenceS
100
100
  health(signal?: AbortSignal): Promise<import("../../opencli.ts").OpenCliHealth>;
101
101
  quickHealth(signal?: AbortSignal): Promise<import("../../opencli.ts").OpenCliHealth>;
102
102
  profiles(signal?: AbortSignal): Promise<import("../../opencli.ts").BrowserProfile[]>;
103
+ ensureBrowser(signal?: AbortSignal): Promise<void>;
103
104
  private runner;
104
105
  installAdapter(signal?: AbortSignal): Promise<boolean>;
105
106
  private installAdapterFrom;
@@ -123,6 +123,7 @@ export default class WebChatHistoryService extends Service {
123
123
  end = decodeLiveCursor(window.cursor, conversationKey);
124
124
  let rows;
125
125
  try {
126
+ await this.ensureBrowser(signal);
126
127
  rows = await this.runner().detail(conversation.provider, conversation.externalId, signal, conversation.accountScope);
127
128
  }
128
129
  catch (error) {
@@ -208,7 +209,12 @@ export default class WebChatHistoryService extends Service {
208
209
  throw new ReferenceAnythingError('wait for the current conversation sync to finish before updating', 'REFERENCE_SYNC_IN_PROGRESS');
209
210
  return this.packageUpdates.update(signal);
210
211
  }
211
- async restartDaemon(signal) { await this.runner().restartDaemon(signal); return true; }
212
+ async restartDaemon(signal) {
213
+ const runner = this.runner();
214
+ await runner.restartDaemon(signal);
215
+ await this.ensureBrowser(signal);
216
+ return true;
217
+ }
212
218
  async discoverOpenCli(signal) {
213
219
  return discoverOpenCli(this.store.settings.opencliPath, signal);
214
220
  }
@@ -267,6 +273,20 @@ export default class WebChatHistoryService extends Service {
267
273
  return new OpenCliRunner({ executable: settings.opencliPath, profile: '',
268
274
  timeoutMs: this.config.timeoutMs, maxStdoutBytes: this.config.maxStdoutBytes }).profiles(signal);
269
275
  }
276
+ async ensureBrowser(signal) {
277
+ const settings = this.store.settings;
278
+ const runner = this.runner();
279
+ const connected = await runner.profiles(signal);
280
+ const selected = connected.find(profile => profile.connected);
281
+ if (selected && !settings.profile) {
282
+ await this.store.setSettings({ ...settings, profile: selected.alias || selected.id });
283
+ return;
284
+ }
285
+ await runner.ensureBrowser(signal);
286
+ const after = (await runner.profiles(signal)).find(profile => profile.connected);
287
+ if (after)
288
+ await this.store.setSettings({ ...settings, profile: after.alias || after.id });
289
+ }
270
290
  runner() {
271
291
  const settings = this.store.settings;
272
292
  return new OpenCliRunner({ executable: settings.opencliPath, profile: settings.profile,