relay-dsh-plugin-manager 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/CHANGELOG.md +22 -0
- package/README.md +30 -0
- package/README.zh.md +23 -0
- package/SPEC.md +6 -1
- package/docs/acceptance.md +2 -0
- package/lib/index.d.ts +17 -1
- package/lib/index.js +221 -7
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.5] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Default-on anonymous operational analytics for plugin-manager use and
|
|
10
|
+
manager-executed install start, success, and failure outcomes, with explicit
|
|
11
|
+
opt-out through `RELAY_PLUGIN_MANAGER_TELEMETRY=0`.
|
|
12
|
+
- First-party delivery to the Registry's versioned telemetry endpoint; the npm
|
|
13
|
+
package contains no PostHog host or project key.
|
|
14
|
+
- A locally persisted random anonymous identifier; search text, installed
|
|
15
|
+
inventory, local paths, command output, error messages, IP geolocation, and
|
|
16
|
+
account identity are excluded.
|
|
17
|
+
|
|
18
|
+
## [0.2.4] - 2026-09-05
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Expose one-based search ranks, default search pages to twenty candidates, and
|
|
23
|
+
instruct the DSH Agent to exclude clearly unrelated purposes while presenting
|
|
24
|
+
every remaining possibly relevant result instead of silently truncating the
|
|
25
|
+
answer to a fixed top-N.
|
|
26
|
+
|
|
5
27
|
## [0.2.3] - 2026-09-04
|
|
6
28
|
|
|
7
29
|
### Added
|
package/README.md
CHANGED
|
@@ -100,6 +100,32 @@ missing companion peers, waits for one confirmation, and runs the installs in
|
|
|
100
100
|
sequence. Completed changes that still need restart are reported explicitly as
|
|
101
101
|
restart-required or waiting for a manual restart.
|
|
102
102
|
|
|
103
|
+
### Anonymous usage analytics
|
|
104
|
+
|
|
105
|
+
Anonymous operational analytics are enabled by default to measure plugin-manager
|
|
106
|
+
search and installation outcomes. To opt out without disabling plugin management,
|
|
107
|
+
set this when starting DSH Web:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The manager sends events only to the first-party
|
|
114
|
+
`https://dsh-plugins.tech/v1/telemetry/events` endpoint. The Registry validates
|
|
115
|
+
the exact event schema and forwards accepted events to its PostHog analytics
|
|
116
|
+
project. The plugin records only the manager action type and plugin
|
|
117
|
+
install start, success, or failure events produced by the manager. Install
|
|
118
|
+
events contain the public package name, activation/restart booleans, and a
|
|
119
|
+
bounded error code. Search terms, conversation content, local paths, command
|
|
120
|
+
output, error text, IP geolocation, and account identity are not sent. A random
|
|
121
|
+
anonymous ID is stored in `.relay-plugin-manager/telemetry.json` inside the
|
|
122
|
+
current Profile to avoid counting one installation as many users; delete the
|
|
123
|
+
file to reset it.
|
|
124
|
+
|
|
125
|
+
These events measure manager use and manager-executed installs. They do not
|
|
126
|
+
claim that another plugin's feature was actually used merely because it loaded
|
|
127
|
+
or activated.
|
|
128
|
+
|
|
103
129
|
### DSH Registry discovery provider
|
|
104
130
|
|
|
105
131
|
The source-only Registry search provider uses `https://dsh-plugins.tech` by
|
|
@@ -112,6 +138,10 @@ This provider sends only the task query, inferred Chinese/English locale, and
|
|
|
112
138
|
result limit. Returned candidates are
|
|
113
139
|
always inspected locally through the same npm/GitHub flow before any plan can be
|
|
114
140
|
created; Registry descriptions are untrusted text and cannot authorize changes.
|
|
141
|
+
Search returns one relevance-ranked page (twenty candidates by default). The
|
|
142
|
+
Agent is instructed to preserve that order and show every possibly relevant
|
|
143
|
+
candidate instead of silently shortening the page to a fixed top five; results
|
|
144
|
+
whose plugin purpose is clearly unrelated to the request are excluded.
|
|
115
145
|
|
|
116
146
|
## Part of Relay
|
|
117
147
|
|
package/README.zh.md
CHANGED
|
@@ -95,6 +95,26 @@ dsh web
|
|
|
95
95
|
一次确认后依次安装。修改完成但仍需重启时,状态会明确区分“需要另行重启”和
|
|
96
96
|
“等待人工重启”。
|
|
97
97
|
|
|
98
|
+
### 匿名使用统计
|
|
99
|
+
|
|
100
|
+
匿名运行统计默认开启,用于衡量插件管理器的搜索和安装结果。如需关闭,可在启动
|
|
101
|
+
DSH Web 时设置以下变量;关闭统计不会影响插件管理功能:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
管理器只向第一方接口 `https://dsh-plugins.tech/v1/telemetry/events` 发送事件;Registry
|
|
108
|
+
会校验严格的事件结构,再把通过校验的事件转发到自己的 PostHog 项目。统计只记录
|
|
109
|
+
管理器操作类型,以及由管理器执行的插件安装开始、成功或失败;安装
|
|
110
|
+
事件包含公开的插件包名、是否激活、是否需要重启和有限错误代码。不会发送搜索词、
|
|
111
|
+
对话内容、本地路径、命令输出、错误文本、IP 地理位置或登录身份。一个随机匿名 ID
|
|
112
|
+
保存在当前 Profile 的 `.relay-plugin-manager/telemetry.json`,用于避免把同一安装重复
|
|
113
|
+
计算为多个用户;删除该文件即可重置。
|
|
114
|
+
|
|
115
|
+
这些数据只能说明“插件管理器被使用”和“管理器执行了安装”,不会把插件加载或
|
|
116
|
+
激活推断成其他插件的真实功能使用。
|
|
117
|
+
|
|
98
118
|
### DSH Registry 发现数据源
|
|
99
119
|
|
|
100
120
|
来源级 Registry 搜索 provider 默认使用 `https://dsh-plugins.tech`。可设置插件
|
|
@@ -105,6 +125,9 @@ dsh web
|
|
|
105
125
|
该 provider 只发送任务查询、推断出的中英文语言和结果数量。返回候选仍必须进入同一套本地 npm/GitHub
|
|
106
126
|
检查流程,完成检查后才能生成计划;Registry 描述属于不可信文本,不能授权任何
|
|
107
127
|
变更。
|
|
128
|
+
搜索返回一页按相关性排序的结果(默认二十个)。Agent 必须保持该顺序并展示所有
|
|
129
|
+
可能相关的候选,不能静默截成固定的前五个;插件用途明确与需求完全无关时必须
|
|
130
|
+
省略。
|
|
108
131
|
|
|
109
132
|
## 与 Relay 的关系
|
|
110
133
|
|
package/SPEC.md
CHANGED
|
@@ -50,6 +50,8 @@ service.
|
|
|
50
50
|
| PM-023 | `plugin_manage confirm` owns its DSH question: it validates the token, expiry, and exact Session before asking; supplies a stable plan-specific id, visible plan detail, exact approve/decline options, and `plan-review` intent; and executes in that same tool call only for the exact single approve answer. Declines, malformed or unrelated answers, provider failure/cancellation, and cross-session attempts do not execute or consume a still-valid plan. Generic model-authored question results are never mutation authority. |
|
|
51
51
|
| PM-024 | GitHub owner discovery recognizes `owner:<name>`, owner-only GitHub identities, `<name> DSH plugins`, and conservative bare identifiers containing digits. It sends a typed owner intent to providers, uses GitHub's exact owner qualifier, case-insensitively verifies returned ownership, and ranks verified owner matches first. Owner-only `inspect` fails with an actionable error directing callers to search. Every emitted repository identity is accepted by `inspect`; every recommended immutable source is accepted by `plan`. |
|
|
52
52
|
| PM-025 | Register a read-only DSH Registry provider against `https://dsh-plugins.tech` by default, with an explicit `registryUrl: false` opt-out and HTTPS-only configuration override. Send only the task query, inferred Chinese/English locale and result limit; accept only source-level untrusted DiscoveryEntry records, convert their npm/GitHub descriptors into core-owned source types, and submit them to the same mandatory local inspection used by all providers. Registry responses cannot supply exact versions, approval, plans, installers, or profile state, and provider failure remains isolated. |
|
|
53
|
+
| PM-026 | Return search candidates as one explicit, one-based relevance-ranked result page with a default and maximum size of 20. The model-facing contract requires the Agent to preserve rank order, surface every possibly relevant returned candidate, exclude candidates whose purpose is clearly unrelated, and MUST NOT silently reduce the remaining page to a fixed top-N. Ranking remains distinct from compatibility, security, or installation approval. |
|
|
54
|
+
| PM-027 | Send default-on anonymous operational telemetry only to the Registry's canonical first-party HTTPS endpoint under schema `1.0.0`, with explicit `RELAY_PLUGIN_MANAGER_TELEMETRY=0` or `telemetry.enabled: false` opt-out. Events are limited to manager discovery/planning type and manager-executed install start/success/failure, including per-plugin batch outcomes. Never send raw search text, conversation content, Profile state, installed inventory, paths, credentials, command output, error text, account identity, PostHog configuration, or client timestamps. Store only a stable random anonymous ID locally, create it lazily, and never let storage, validation, network, Registry, or analytics failure affect plugin management. |
|
|
53
55
|
|
|
54
56
|
## Command Grammar
|
|
55
57
|
|
|
@@ -72,7 +74,10 @@ Read-only actions: `list`, `search`, `inspect`, `status`.
|
|
|
72
74
|
`search` accepts capability text or GitHub owner forms such as
|
|
73
75
|
`owner:yangbobo2021`. `inspect` accepts npm sources and all three GitHub
|
|
74
76
|
repository forms from PM-007. An owner without a repository belongs to
|
|
75
|
-
`search`, not `inspect`.
|
|
77
|
+
`search`, not `inspect`. Search defaults to a twenty-candidate ranked page. Each
|
|
78
|
+
candidate carries its explicit display rank; the Agent presents all possibly
|
|
79
|
+
relevant returned candidates in that order instead of silently selecting an
|
|
80
|
+
arbitrary top five.
|
|
76
81
|
|
|
77
82
|
### `plugin_manage`
|
|
78
83
|
|
package/docs/acceptance.md
CHANGED
|
@@ -38,6 +38,8 @@ This matrix is the release contract. Tests name the relevant `PM-*` ids.
|
|
|
38
38
|
| A-030 owner-only inspect forms return actionable search guidance while malformed, unsafe, and non-GitHub inputs remain rejected without weakening source validation | PM-018, PM-024 | source/manager negative unit |
|
|
39
39
|
| A-031 search results feed one immutable multi-install plan through the real DSH tool and user-question registries; no mutation starts before one exact UI approval, then child installs execute serially and approval replay is rejected | PM-002, PM-005, PM-009, PM-010, PM-015, PM-023, PM-024 | combined host/tool/search/question/manager integration |
|
|
40
40
|
| A-032 The default formal Registry searches at least 3,000 source records for Chinese task text, English task text, exact plugin names and an honest empty result; every returned source is locally inspected to an immutable npm version or GitHub commit, while Registry authority remains source-only, untrusted, non-recommending and non-approving | PM-005, PM-006, PM-025 | provider/unit/host integration tests plus `npm run acceptance:live:registry` |
|
|
41
|
+
| A-033 Search defaults to a complete twenty-candidate result page, assigns stable one-based display ranks after merge and deduplication, and tells the Agent to exclude clearly unrelated purposes while surfacing every remaining possibly relevant candidate without silent fixed-count truncation | PM-005, PM-026 | search orchestration and conversation-tool contract unit tests plus Agent answer acceptance |
|
|
42
|
+
| A-034 Default manager actions send only schema-valid anonymous envelopes to the first-party Registry endpoint, reuse a lazily created random ID, record each attempted batch install outcome, and continue normally through storage or delivery failure; explicit opt-out creates no ID and makes no telemetry request | PM-027 | telemetry unit tests + manager integration event assertions + Registry endpoint contract and end-to-end PostHog verification |
|
|
41
43
|
|
|
42
44
|
## Test Layers
|
|
43
45
|
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ import { a as PluginSearchRuntime, c as PluginSource, i as PluginSearchRequest,
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
4
|
|
|
5
|
+
//#region src/telemetry.d.ts
|
|
6
|
+
type TelemetryProperty = string | number | boolean;
|
|
7
|
+
interface Telemetry {
|
|
8
|
+
capture(event: string, properties?: Readonly<Record<string, TelemetryProperty>>): void;
|
|
9
|
+
}
|
|
10
|
+
interface TelemetryConfig {
|
|
11
|
+
/** Anonymous operational telemetry is enabled unless this is explicitly false. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Registry telemetry endpoint. Only the canonical service or localhost is accepted. */
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
5
17
|
//#region src/profile.d.ts
|
|
6
18
|
interface PackageSurface {
|
|
7
19
|
packageName: string;
|
|
@@ -238,6 +250,7 @@ interface PluginManagerDependencies {
|
|
|
238
250
|
operations?: OperationTracker;
|
|
239
251
|
fetchOptions?: Omit<FetchOptions, 'signal'>;
|
|
240
252
|
hmrTimeoutMs?: number;
|
|
253
|
+
telemetry?: Telemetry;
|
|
241
254
|
}
|
|
242
255
|
interface DiscoverRequest {
|
|
243
256
|
action: 'list' | 'search' | 'inspect' | 'status';
|
|
@@ -301,7 +314,9 @@ declare class PluginManager {
|
|
|
301
314
|
private readonly operations;
|
|
302
315
|
private readonly fetchOptions;
|
|
303
316
|
private readonly hmrTimeoutMs;
|
|
317
|
+
private readonly telemetry;
|
|
304
318
|
constructor(dependencies: PluginManagerDependencies);
|
|
319
|
+
private capture;
|
|
305
320
|
private loaderEntries;
|
|
306
321
|
list(): PluginStatus[];
|
|
307
322
|
discover(request: DiscoverRequest, signal?: AbortSignal): Promise<unknown>;
|
|
@@ -331,8 +346,9 @@ declare const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
|
|
|
331
346
|
interface Config {
|
|
332
347
|
allowRestart?: boolean;
|
|
333
348
|
registryUrl?: string | false;
|
|
349
|
+
telemetry?: TelemetryConfig;
|
|
334
350
|
}
|
|
335
351
|
declare function apply(ctx: Context, config?: Config): void;
|
|
336
352
|
//#endregion
|
|
337
|
-
export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, apply, inject, name };
|
|
353
|
+
export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, type TelemetryConfig, apply, inject, name };
|
|
338
354
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -74,7 +74,7 @@ function registerConversationSurface(ctx, manager) {
|
|
|
74
74
|
const confirmations = /* @__PURE__ */ new Map();
|
|
75
75
|
ctx.tools.register(defineTool({
|
|
76
76
|
name: "plugin_discover",
|
|
77
|
-
description: "Read-only DSH plugin discovery. List installed plugins, search registered sources (including GitHub owner:NAME), inspect one npm/GitHub repository source, or query plugin/operation status. Search result repository and recommendedSource values can be passed directly to inspect and plan. This tool never changes the profile.",
|
|
77
|
+
description: "Read-only DSH plugin discovery. List installed plugins, search registered sources (including GitHub owner:NAME), inspect one npm/GitHub repository source, or query plugin/operation status. Search candidates form one relevance-ranked result page: present every possibly relevant candidate in ascending rank, exclude candidates whose purpose is clearly unrelated, and NEVER silently truncate the remaining page to a fixed top-N. Ranking is not a compatibility, security, or installation approval. Search result repository and recommendedSource values can be passed directly to inspect and plan. This tool never changes the profile.",
|
|
78
78
|
parameters: {
|
|
79
79
|
action: {
|
|
80
80
|
type: "string",
|
|
@@ -101,7 +101,7 @@ function registerConversationSurface(ctx, manager) {
|
|
|
101
101
|
},
|
|
102
102
|
maxResults: {
|
|
103
103
|
type: "integer",
|
|
104
|
-
description: "
|
|
104
|
+
description: "Ranked result-page size from 1 to 20. Use 20 for ordinary need-based searches unless the user explicitly asks for fewer."
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
output: {
|
|
@@ -736,7 +736,7 @@ function candidateSources(provider, rows) {
|
|
|
736
736
|
}
|
|
737
737
|
async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
738
738
|
const parsed = parseSearchQuery(rawQuery);
|
|
739
|
-
const maxResults = Math.max(1, Math.min(20, options.maxResults ??
|
|
739
|
+
const maxResults = Math.max(1, Math.min(20, options.maxResults ?? 20));
|
|
740
740
|
const timeoutMs = Math.max(100, options.providerTimeoutMs ?? 1e4);
|
|
741
741
|
const providers = runtime.entries();
|
|
742
742
|
const settled = await Promise.allSettled(providers.map(async (provider) => ({
|
|
@@ -813,13 +813,22 @@ async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
|
813
813
|
existing.recommendedSource = existing.sources.find((source) => source.inspection.sourceType === "npm")?.inspection.installSpec ?? existing.sources[0].inspection.installSpec;
|
|
814
814
|
projects.set(identity, existing);
|
|
815
815
|
}
|
|
816
|
-
const candidates = [...projects.values()].sort((left, right) => left.matchPriority - right.matchPriority || left.rank - right.rank || left.packageName.localeCompare(right.packageName)).slice(0, maxResults).map(({ rank:
|
|
816
|
+
const candidates = [...projects.values()].sort((left, right) => left.matchPriority - right.matchPriority || left.rank - right.rank || left.packageName.localeCompare(right.packageName)).slice(0, maxResults).map(({ rank: _providerRank, matchPriority: _matchPriority, ...candidate }, index) => ({
|
|
817
817
|
...candidate,
|
|
818
|
+
rank: index + 1,
|
|
818
819
|
providers: candidate.providers.sort()
|
|
819
820
|
}));
|
|
820
821
|
return {
|
|
821
822
|
query: parsed.query,
|
|
822
823
|
candidates,
|
|
824
|
+
presentation: {
|
|
825
|
+
order: "rank_ascending",
|
|
826
|
+
returnedCandidates: candidates.length,
|
|
827
|
+
requestedMaximum: maxResults,
|
|
828
|
+
includeEveryPossiblyRelevant: true,
|
|
829
|
+
excludeClearlyIrrelevant: true,
|
|
830
|
+
silentTopNTruncation: false
|
|
831
|
+
},
|
|
823
832
|
providerErrors,
|
|
824
833
|
rejectedCandidates
|
|
825
834
|
};
|
|
@@ -1214,11 +1223,56 @@ const PROTECTED_ENTRY_IDS = /* @__PURE__ */ new Set([
|
|
|
1214
1223
|
"webserver",
|
|
1215
1224
|
"web-runtime"
|
|
1216
1225
|
]);
|
|
1226
|
+
const TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1227
|
+
"INVALID_SOURCE",
|
|
1228
|
+
"INVALID_NPM_SPEC",
|
|
1229
|
+
"INVALID_NPM_VERSION",
|
|
1230
|
+
"INVALID_GITHUB_SPEC",
|
|
1231
|
+
"INVALID_GITHUB_REF",
|
|
1232
|
+
"GITHUB_OWNER_REQUIRES_SEARCH",
|
|
1233
|
+
"IMMUTABLE_SOURCE_REQUIRED",
|
|
1234
|
+
"NETWORK_ERROR",
|
|
1235
|
+
"SOURCE_HTTP_ERROR",
|
|
1236
|
+
"INVALID_SOURCE_METADATA",
|
|
1237
|
+
"INVALID_PLUGIN_MANIFEST",
|
|
1238
|
+
"NOT_DSH_PLUGIN",
|
|
1239
|
+
"PACKAGE_NAME_MISMATCH",
|
|
1240
|
+
"NPM_INTEGRITY_MISSING",
|
|
1241
|
+
"INVALID_SEARCH_QUERY",
|
|
1242
|
+
"INVALID_ACTION",
|
|
1243
|
+
"INVALID_BATCH",
|
|
1244
|
+
"DUPLICATE_SEARCH_PROVIDER",
|
|
1245
|
+
"PROFILE_READ_FAILED",
|
|
1246
|
+
"PROFILE_WRITE_FAILED",
|
|
1247
|
+
"PLUGIN_NOT_INSTALLED",
|
|
1248
|
+
"PLUGIN_ALREADY_INSTALLED",
|
|
1249
|
+
"ENABLEMENT_UNSUPPORTED",
|
|
1250
|
+
"ENABLEMENT_CONFLICT",
|
|
1251
|
+
"PROTECTED_PLUGIN",
|
|
1252
|
+
"CONFIRMATION_REQUIRED",
|
|
1253
|
+
"CONFIRMATION_INVALID",
|
|
1254
|
+
"CONFIRMATION_EXPIRED",
|
|
1255
|
+
"CONFIRMATION_REPLAYED",
|
|
1256
|
+
"PLAN_STALE",
|
|
1257
|
+
"OPERATION_NOT_FOUND",
|
|
1258
|
+
"DSH_COMMAND_FAILED",
|
|
1259
|
+
"BATCH_INSTALL_FAILED",
|
|
1260
|
+
"POSTCONDITION_FAILED",
|
|
1261
|
+
"RESTART_UNAVAILABLE"
|
|
1262
|
+
]);
|
|
1217
1263
|
function safePackageName(value) {
|
|
1218
1264
|
const name = value?.trim() ?? "";
|
|
1219
1265
|
if (!NPM_NAME.test(name)) fail("INVALID_NPM_SPEC", "A valid installed package name is required.");
|
|
1220
1266
|
return name;
|
|
1221
1267
|
}
|
|
1268
|
+
function queryLengthBucket(value) {
|
|
1269
|
+
const length = value?.trim().length ?? 0;
|
|
1270
|
+
if (length === 0) return "empty";
|
|
1271
|
+
if (length <= 10) return "1-10";
|
|
1272
|
+
if (length <= 30) return "11-30";
|
|
1273
|
+
if (length <= 80) return "31-80";
|
|
1274
|
+
return "81+";
|
|
1275
|
+
}
|
|
1222
1276
|
function commandResult(result) {
|
|
1223
1277
|
return {
|
|
1224
1278
|
exitCode: result.exitCode,
|
|
@@ -1232,6 +1286,10 @@ function operationError(error) {
|
|
|
1232
1286
|
message: error instanceof Error ? error.message : String(error)
|
|
1233
1287
|
};
|
|
1234
1288
|
}
|
|
1289
|
+
function telemetryErrorCode(error) {
|
|
1290
|
+
const code = operationError(error).code;
|
|
1291
|
+
return code !== void 0 && TELEMETRY_ERROR_CODES.has(code) ? code : "UNKNOWN";
|
|
1292
|
+
}
|
|
1235
1293
|
function installedPackageManifest(profileDir, packageName) {
|
|
1236
1294
|
try {
|
|
1237
1295
|
return JSON.parse(readFileSync(join(profileDir, "node_modules", packageName, "package.json"), "utf8"));
|
|
@@ -1256,6 +1314,7 @@ var PluginManager = class {
|
|
|
1256
1314
|
operations;
|
|
1257
1315
|
fetchOptions;
|
|
1258
1316
|
hmrTimeoutMs;
|
|
1317
|
+
telemetry;
|
|
1259
1318
|
constructor(dependencies) {
|
|
1260
1319
|
this.profileDir = dependencies.profileDir;
|
|
1261
1320
|
this.searchRuntime = dependencies.searchRuntime;
|
|
@@ -1268,6 +1327,12 @@ var PluginManager = class {
|
|
|
1268
1327
|
this.operations = dependencies.operations ?? new OperationTracker();
|
|
1269
1328
|
this.fetchOptions = dependencies.fetchOptions ?? {};
|
|
1270
1329
|
this.hmrTimeoutMs = dependencies.hmrTimeoutMs ?? 5e3;
|
|
1330
|
+
this.telemetry = dependencies.telemetry ?? { capture() {} };
|
|
1331
|
+
}
|
|
1332
|
+
capture(event, properties = {}) {
|
|
1333
|
+
try {
|
|
1334
|
+
this.telemetry.capture(event, properties);
|
|
1335
|
+
} catch {}
|
|
1271
1336
|
}
|
|
1272
1337
|
loaderEntries() {
|
|
1273
1338
|
if (this.loader === void 0) return [];
|
|
@@ -1288,6 +1353,14 @@ var PluginManager = class {
|
|
|
1288
1353
|
return listPluginStatuses(this.profileDir, this.loaderEntries());
|
|
1289
1354
|
}
|
|
1290
1355
|
async discover(request, signal) {
|
|
1356
|
+
this.capture("plugin_manager_used", {
|
|
1357
|
+
surface: "discover",
|
|
1358
|
+
action: request.action,
|
|
1359
|
+
...request.action === "search" ? {
|
|
1360
|
+
has_query: (request.query?.trim().length ?? 0) > 0,
|
|
1361
|
+
query_length_bucket: queryLengthBucket(request.query)
|
|
1362
|
+
} : {}
|
|
1363
|
+
});
|
|
1291
1364
|
if (request.action === "list") return {
|
|
1292
1365
|
profile: "web",
|
|
1293
1366
|
plugins: this.list()
|
|
@@ -1351,6 +1424,11 @@ var PluginManager = class {
|
|
|
1351
1424
|
});
|
|
1352
1425
|
}
|
|
1353
1426
|
async plan(request, signal) {
|
|
1427
|
+
this.capture("plugin_manager_used", {
|
|
1428
|
+
surface: "plan",
|
|
1429
|
+
action: request.operation,
|
|
1430
|
+
...request.operation === "install_many" ? { batch_size: request.sources?.length ?? 0 } : {}
|
|
1431
|
+
});
|
|
1354
1432
|
if (request.operation === "install_many") return await this.planInstallMany(request.sources, signal);
|
|
1355
1433
|
if (request.operation === "restart") {
|
|
1356
1434
|
if (!this.restarter.available()) fail("RESTART_UNAVAILABLE", "Automatic restart is unavailable in this deployment.");
|
|
@@ -1477,7 +1555,22 @@ var PluginManager = class {
|
|
|
1477
1555
|
if (plan.packageName === void 0) fail("POSTCONDITION_FAILED", "Mutation plan has no package name.");
|
|
1478
1556
|
if (plan.action === "install" || plan.action === "update") {
|
|
1479
1557
|
if (plan.installSpec === void 0) fail("POSTCONDITION_FAILED", "Install/update plan has no immutable source.");
|
|
1480
|
-
|
|
1558
|
+
if (plan.action === "install") this.capture("plugin_install_started", { plugin_name: plan.packageName });
|
|
1559
|
+
try {
|
|
1560
|
+
const result = this.withRestartGuidance(await this.installOrUpdate(plan.action, plan.packageName, plan.installSpec, context), automaticRestartAvailable);
|
|
1561
|
+
if (plan.action === "install") this.capture("plugin_install_succeeded", {
|
|
1562
|
+
plugin_name: plan.packageName,
|
|
1563
|
+
activated: result.activated === true,
|
|
1564
|
+
restart_required: result.restartRequired
|
|
1565
|
+
});
|
|
1566
|
+
return result;
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
if (plan.action === "install") this.capture("plugin_install_failed", {
|
|
1569
|
+
plugin_name: plan.packageName,
|
|
1570
|
+
error_code: telemetryErrorCode(error)
|
|
1571
|
+
});
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1481
1574
|
}
|
|
1482
1575
|
if (plan.action === "remove") return this.withRestartGuidance(await this.remove(plan.packageName, context), automaticRestartAvailable);
|
|
1483
1576
|
return this.withRestartGuidance(await this.toggle(plan.action, plan.packageName, context), automaticRestartAvailable);
|
|
@@ -1525,6 +1618,10 @@ var PluginManager = class {
|
|
|
1525
1618
|
break;
|
|
1526
1619
|
}
|
|
1527
1620
|
context.progress(`install_many: ${index + 1}/${items.length} installing ${item.packageName}`);
|
|
1621
|
+
this.capture("plugin_install_started", {
|
|
1622
|
+
plugin_name: item.packageName,
|
|
1623
|
+
batch: true
|
|
1624
|
+
});
|
|
1528
1625
|
try {
|
|
1529
1626
|
const result = this.withRestartGuidance(await this.installOrUpdate("install", item.packageName, item.installSpec, context), automaticRestartAvailable);
|
|
1530
1627
|
results.push({
|
|
@@ -1533,6 +1630,12 @@ var PluginManager = class {
|
|
|
1533
1630
|
status: this.mutationCompletion(result, automaticRestartAvailable).status,
|
|
1534
1631
|
result
|
|
1535
1632
|
});
|
|
1633
|
+
this.capture("plugin_install_succeeded", {
|
|
1634
|
+
plugin_name: item.packageName,
|
|
1635
|
+
batch: true,
|
|
1636
|
+
activated: result.activated === true,
|
|
1637
|
+
restart_required: result.restartRequired
|
|
1638
|
+
});
|
|
1536
1639
|
} catch (error) {
|
|
1537
1640
|
results.push({
|
|
1538
1641
|
packageName: item.packageName,
|
|
@@ -1540,6 +1643,11 @@ var PluginManager = class {
|
|
|
1540
1643
|
status: context.signal.aborted ? "cancelled" : "failed",
|
|
1541
1644
|
error: operationError(error)
|
|
1542
1645
|
});
|
|
1646
|
+
this.capture("plugin_install_failed", {
|
|
1647
|
+
plugin_name: item.packageName,
|
|
1648
|
+
batch: true,
|
|
1649
|
+
error_code: telemetryErrorCode(error)
|
|
1650
|
+
});
|
|
1543
1651
|
skipRemaining(index + 1);
|
|
1544
1652
|
break;
|
|
1545
1653
|
}
|
|
@@ -1686,7 +1794,7 @@ var PluginManager = class {
|
|
|
1686
1794
|
};
|
|
1687
1795
|
//#endregion
|
|
1688
1796
|
//#region src/providers.ts
|
|
1689
|
-
const MAX_PROVIDER_RESULTS =
|
|
1797
|
+
const MAX_PROVIDER_RESULTS = 20;
|
|
1690
1798
|
const REGISTRY_SNAPSHOT_ID = /^discovery\.[a-z0-9.-]+$/u;
|
|
1691
1799
|
function query(value) {
|
|
1692
1800
|
const normalized = value.trim();
|
|
@@ -2069,6 +2177,107 @@ var DshCliRunner = class {
|
|
|
2069
2177
|
}
|
|
2070
2178
|
};
|
|
2071
2179
|
//#endregion
|
|
2180
|
+
//#region src/telemetry.ts
|
|
2181
|
+
const DEFAULT_ENDPOINT = "https://dsh-plugins.tech/v1/telemetry/events";
|
|
2182
|
+
const STATE_DIRECTORY = ".relay-plugin-manager";
|
|
2183
|
+
const STATE_FILE = "telemetry.json";
|
|
2184
|
+
const SCHEMA_VERSION = "1.0.0";
|
|
2185
|
+
const EVENTS = /* @__PURE__ */ new Set([
|
|
2186
|
+
"plugin_manager_used",
|
|
2187
|
+
"plugin_install_started",
|
|
2188
|
+
"plugin_install_succeeded",
|
|
2189
|
+
"plugin_install_failed"
|
|
2190
|
+
]);
|
|
2191
|
+
const noopTelemetry = Object.freeze({ capture() {} });
|
|
2192
|
+
function safeEndpoint(value) {
|
|
2193
|
+
try {
|
|
2194
|
+
const parsed = new URL(value ?? DEFAULT_ENDPOINT);
|
|
2195
|
+
const local = [
|
|
2196
|
+
"localhost",
|
|
2197
|
+
"127.0.0.1",
|
|
2198
|
+
"::1"
|
|
2199
|
+
].includes(parsed.hostname);
|
|
2200
|
+
const canonical = parsed.protocol === "https:" && parsed.hostname === "dsh-plugins.tech";
|
|
2201
|
+
if (!local && !canonical || local && !["http:", "https:"].includes(parsed.protocol)) return null;
|
|
2202
|
+
if (parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/v1/telemetry/events" || parsed.search !== "" || parsed.hash !== "") return null;
|
|
2203
|
+
return parsed.href;
|
|
2204
|
+
} catch {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
function anonymousId(profileDir, random) {
|
|
2209
|
+
const directory = join(profileDir, STATE_DIRECTORY);
|
|
2210
|
+
const path = join(directory, STATE_FILE);
|
|
2211
|
+
try {
|
|
2212
|
+
const existing = JSON.parse(readFileSync(path, "utf8"));
|
|
2213
|
+
if (typeof existing.anonymousId === "string" && /^[0-9a-f-]{36}$/iu.test(existing.anonymousId)) return existing.anonymousId;
|
|
2214
|
+
} catch {}
|
|
2215
|
+
const id = random();
|
|
2216
|
+
try {
|
|
2217
|
+
mkdirSync(directory, {
|
|
2218
|
+
recursive: true,
|
|
2219
|
+
mode: 448
|
|
2220
|
+
});
|
|
2221
|
+
writeFileSync(path, `${JSON.stringify({ anonymousId: id })}\n`, { mode: 384 });
|
|
2222
|
+
} catch {}
|
|
2223
|
+
return id;
|
|
2224
|
+
}
|
|
2225
|
+
function allowedProperties(event, properties) {
|
|
2226
|
+
const keys = new Set(Object.keys(properties));
|
|
2227
|
+
const exact = (required, optional = []) => {
|
|
2228
|
+
if (required.some((key) => !keys.has(key))) return false;
|
|
2229
|
+
return [...keys].every((key) => required.includes(key) || optional.includes(key));
|
|
2230
|
+
};
|
|
2231
|
+
if (event === "plugin_manager_used") {
|
|
2232
|
+
if (!exact(["surface", "action"], [
|
|
2233
|
+
"has_query",
|
|
2234
|
+
"query_length_bucket",
|
|
2235
|
+
"batch_size"
|
|
2236
|
+
])) return false;
|
|
2237
|
+
if (!["discover", "plan"].includes(String(properties.surface))) return false;
|
|
2238
|
+
return typeof properties.action === "string";
|
|
2239
|
+
}
|
|
2240
|
+
if (event === "plugin_install_started") return exact(["plugin_name"], ["batch"]);
|
|
2241
|
+
if (event === "plugin_install_succeeded") return exact([
|
|
2242
|
+
"plugin_name",
|
|
2243
|
+
"activated",
|
|
2244
|
+
"restart_required"
|
|
2245
|
+
], ["batch"]);
|
|
2246
|
+
if (event === "plugin_install_failed") return exact(["plugin_name", "error_code"], ["batch"]);
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
function createTelemetry(profileDir, config, runtime = {
|
|
2250
|
+
fetch,
|
|
2251
|
+
random: randomUUID
|
|
2252
|
+
}) {
|
|
2253
|
+
if (config?.enabled === false) return noopTelemetry;
|
|
2254
|
+
const endpoint = safeEndpoint(config?.endpoint);
|
|
2255
|
+
if (endpoint === null) return noopTelemetry;
|
|
2256
|
+
let distinctId;
|
|
2257
|
+
return Object.freeze({ capture(event, properties = {}) {
|
|
2258
|
+
if (!EVENTS.has(event) || !allowedProperties(event, properties)) return;
|
|
2259
|
+
distinctId ??= anonymousId(profileDir, runtime.random);
|
|
2260
|
+
const controller = new AbortController();
|
|
2261
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
2262
|
+
timeout.unref?.();
|
|
2263
|
+
try {
|
|
2264
|
+
runtime.fetch(endpoint, {
|
|
2265
|
+
method: "POST",
|
|
2266
|
+
headers: { "content-type": "application/json" },
|
|
2267
|
+
body: JSON.stringify({
|
|
2268
|
+
schema_version: SCHEMA_VERSION,
|
|
2269
|
+
anonymous_id: distinctId,
|
|
2270
|
+
event,
|
|
2271
|
+
properties
|
|
2272
|
+
}),
|
|
2273
|
+
signal: controller.signal
|
|
2274
|
+
}).catch(() => void 0).finally(() => clearTimeout(timeout));
|
|
2275
|
+
} catch {
|
|
2276
|
+
clearTimeout(timeout);
|
|
2277
|
+
}
|
|
2278
|
+
} });
|
|
2279
|
+
}
|
|
2280
|
+
//#endregion
|
|
2072
2281
|
//#region src/index.ts
|
|
2073
2282
|
const name = "relay-dsh-plugin-manager";
|
|
2074
2283
|
const inject = [
|
|
@@ -2081,6 +2290,10 @@ const inject = [
|
|
|
2081
2290
|
const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
|
|
2082
2291
|
function apply(ctx, config = {}) {
|
|
2083
2292
|
const profileDir = profileDirectory("web");
|
|
2293
|
+
const telemetry = config.telemetry ?? {
|
|
2294
|
+
enabled: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY !== "0",
|
|
2295
|
+
endpoint: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY_ENDPOINT
|
|
2296
|
+
};
|
|
2084
2297
|
ctx.pluginSearch.register(npmSearchProvider());
|
|
2085
2298
|
ctx.pluginSearch.register(githubSearchProvider());
|
|
2086
2299
|
const configuredRegistryUrl = config.registryUrl ?? process.env.DSH_PLUGIN_REGISTRY_URL?.trim();
|
|
@@ -2092,7 +2305,8 @@ function apply(ctx, config = {}) {
|
|
|
2092
2305
|
runner: new DshCliRunner(),
|
|
2093
2306
|
hot: new HotRuntime(ctx, profileDir),
|
|
2094
2307
|
restarter: new DshRestarter({ allowRestart: config.allowRestart }),
|
|
2095
|
-
loader: ctx.loader
|
|
2308
|
+
loader: ctx.loader,
|
|
2309
|
+
telemetry: createTelemetry(profileDir, telemetry)
|
|
2096
2310
|
}));
|
|
2097
2311
|
}
|
|
2098
2312
|
//#endregion
|