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/README.md +21 -11
- package/images/logo-icon.png +0 -0
- package/images/logo-mono.png +0 -0
- package/lib/client.js +4185 -1244
- package/lib/contract.d.ts +18 -1
- package/lib/contract.js +12 -1
- package/lib/host.d.ts +57 -2
- package/lib/host.js +8 -2
- package/lib/index.js +0 -2
- package/lib/local-plugins.d.ts +6 -0
- package/lib/local-plugins.js +119 -0
- package/lib/opencli.d.ts +3 -0
- package/lib/opencli.js +61 -8
- package/lib/reference.js +1 -2
- package/lib/sources/web-chat/index.d.ts +1 -0
- package/lib/sources/web-chat/index.js +21 -1
- package/lib/store/spec.d.ts +64 -0
- package/lib/typert.js +1 -0
- package/lib/types.d.ts +5 -1
- package/lib/web.js +6 -5
- package/lib/wire.d.ts +141 -0
- package/lib/wire.js +27 -11
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/opencli-plugin/common.js +22 -6
- package/package.json +71 -51
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():
|
|
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:
|
|
362
|
+
? { file: node, prefix: [npmExecPath] }
|
|
330
363
|
: { file: npmExecPath, prefix: [] };
|
|
331
364
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
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:
|
|
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.
|
|
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: '
|
|
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) {
|
|
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,
|