dsh-plugin-manager-companion 0.1.0

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +144 -0
  3. package/README.md +142 -0
  4. package/cordis.patch.yml +9 -0
  5. package/dist/about.d.ts +77 -0
  6. package/dist/about.js +179 -0
  7. package/dist/cli.d.ts +226 -0
  8. package/dist/cli.js +856 -0
  9. package/dist/client/AboutPage.d.ts +75 -0
  10. package/dist/client/ConsolePage.d.ts +79 -0
  11. package/dist/client/KindsPage.d.ts +21 -0
  12. package/dist/client/MarketplacePage.d.ts +36 -0
  13. package/dist/client/OfficialSlots.d.ts +35 -0
  14. package/dist/client/UpgradeRow.d.ts +108 -0
  15. package/dist/client/index.d.ts +26 -0
  16. package/dist/client/locales.d.ts +475 -0
  17. package/dist/client/pmSelect.d.ts +38 -0
  18. package/dist/client/shared.d.ts +928 -0
  19. package/dist/client/upgradeView.d.ts +278 -0
  20. package/dist/client/wire.d.ts +401 -0
  21. package/dist/client.js +9194 -0
  22. package/dist/diagnostics.d.ts +332 -0
  23. package/dist/diagnostics.js +2631 -0
  24. package/dist/envManager.d.ts +1047 -0
  25. package/dist/envManager.js +3214 -0
  26. package/dist/fix.d.ts +60 -0
  27. package/dist/fix.js +168 -0
  28. package/dist/guard.d.ts +133 -0
  29. package/dist/guard.js +232 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +1150 -0
  32. package/dist/installSession.d.ts +111 -0
  33. package/dist/installSession.js +150 -0
  34. package/dist/kinds.d.ts +464 -0
  35. package/dist/kinds.js +1029 -0
  36. package/dist/marketView.d.ts +261 -0
  37. package/dist/marketView.js +406 -0
  38. package/dist/marketplace.d.ts +248 -0
  39. package/dist/marketplace.js +500 -0
  40. package/dist/match.d.ts +67 -0
  41. package/dist/match.js +203 -0
  42. package/dist/net.d.ts +108 -0
  43. package/dist/net.js +163 -0
  44. package/dist/official.d.ts +145 -0
  45. package/dist/official.js +205 -0
  46. package/dist/paths.d.ts +108 -0
  47. package/dist/paths.js +236 -0
  48. package/dist/presets.d.ts +299 -0
  49. package/dist/presets.js +578 -0
  50. package/dist/qualityGate.d.ts +66 -0
  51. package/dist/qualityGate.js +247 -0
  52. package/dist/rank.d.ts +88 -0
  53. package/dist/rank.js +164 -0
  54. package/dist/registry.d.ts +295 -0
  55. package/dist/registry.js +686 -0
  56. package/dist/rest.d.ts +122 -0
  57. package/dist/rest.js +219 -0
  58. package/dist/scan.d.ts +134 -0
  59. package/dist/scan.js +396 -0
  60. package/dist/settings.d.ts +447 -0
  61. package/dist/settings.js +263 -0
  62. package/dist/tags.d.ts +119 -0
  63. package/dist/tags.js +166 -0
  64. package/dist/tools.d.ts +131 -0
  65. package/dist/tools.js +377 -0
  66. package/dist/types.d.ts +651 -0
  67. package/dist/types.js +13 -0
  68. package/dist/upgrade.d.ts +428 -0
  69. package/dist/upgrade.js +1100 -0
  70. package/dist/upgradeView.d.ts +313 -0
  71. package/dist/upgradeView.js +273 -0
  72. package/docs/images/readme/01-console-health.png +0 -0
  73. package/docs/images/readme/02-console-envs.png +0 -0
  74. package/docs/images/readme/03-marketplace.png +0 -0
  75. package/docs/images/readme/04-official-plugin-page.png +0 -0
  76. package/package.json +104 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * agent 工具:plugin_search(市场检索)+ plugin_health(环境体检)。
3
+ *
4
+ * 归属:A 类·重写(注册官方工具面;旧 src/tools.ts 仅作意图参考,未复制代码)。
5
+ * 旧实现参考:dsh-web-plugin-manager/src/tools.ts(365 行:defineTool 的用法、
6
+ * 文本渲染形态、plugin_search 的"安装前先浏览仓库"提示、空查询按星数兜底)。
7
+ * 官方复用:@deepseek-ai/dsh-tools 的 defineTool + ctx.tools.register(与官方
8
+ * plugin_manager 工具同一套注册面)。
9
+ * 前提检查:**用户决定删掉了旧实现的四个工具**——"agent 工具只留 plugin_search +
10
+ * 健康检查,其余交还官方"。旧 plugin_status / plugin_install / plugin_uninstall /
11
+ * plugin_toggle 的前提(官方没有当前环境管理能力)在 0.1.6 之后消失:官方
12
+ * plugin_manager 工具已覆盖列出/启停/安装/卸载四个动作,再注册一份同名能力
13
+ * 只会造成两个入口两套语义。因此本模块只提供官方**没有**的两件事:
14
+ * 市场语义检索(官方没有市场概念)与环境体检(官方不做深度诊断)。
15
+ *
16
+ * 依赖注入而不是静态 import:本模块要用的 match(打分)与 diagnostics(诊断引擎)
17
+ * 由同一仓库的其他模块提供,它们的导出面定稿之前静态 import 会让**整个 host 构建**
18
+ * 编译不过(tsc 一次编译 src 下所有 .ts)。所以这里把两者声明成可注入的能力
19
+ * (CompanionToolsHost),由装配点传进来——旧实现用同样的手法避免与 index.ts 循环
20
+ * import,这里顺带把"构建顺序"也解耦了。
21
+ */
22
+ import { defineTool } from '@deepseek-ai/dsh-tools';
23
+ import type { Context } from '@deepseek-ai/cordis';
24
+ import { type CompanionConfig } from './settings.ts';
25
+ import type { DiagnosticReport, EnvironmentInfo, MarketItem } from './types.ts';
26
+ /** 市场检索的返回:索引条目、索引生成时间、命中数量。 */
27
+ export interface SearchLookup {
28
+ readonly items: readonly MarketItem[];
29
+ readonly generatedAt: string;
30
+ /** 索引里的条目总数(含未命中项),用于告诉用户"扫了多少条"。 */
31
+ readonly total?: number;
32
+ }
33
+ /**
34
+ * 注入给工具的宿主能力。
35
+ *
36
+ * 装配点负责把真实实现接上:
37
+ * - market:市场索引读取(marketplace.ts);
38
+ * - rank:纯函数打分(match.ts)——工具本身不做打分决策,只负责把它排出来的
39
+ * 顺序与上限交给模型;
40
+ * - analyze:诊断引擎(diagnostics.ts);
41
+ * - environment:当前环境事实(envManager.ts)。
42
+ * 任何一项缺失时,对应工具仍会注册,但执行时抛出**可读**的缺失原因(绝不返回
43
+ * 一个"看起来健康"的空结果)。
44
+ */
45
+ export interface CompanionToolsHost {
46
+ /** 读市场索引;refresh 为 true 时强制走网络。 */
47
+ readonly market: (options: {
48
+ readonly refresh: boolean;
49
+ }) => Promise<SearchLookup>;
50
+ /** 自然语言检索打分:返回按相关度排好序的条目(调用方再截断上限)。 */
51
+ readonly rank: (items: readonly MarketItem[], query: string) => readonly MarketItem[];
52
+ /** 跑一次环境体检。ctx 为 undefined 时引擎应记 skipped 而不是崩溃。 */
53
+ readonly analyze: (ctx: Context | undefined, environment: EnvironmentInfo | undefined, config: CompanionConfig) => Promise<DiagnosticReport>;
54
+ /** 当前环境事实;不可用时 undefined。 */
55
+ readonly environment?: () => EnvironmentInfo | undefined;
56
+ /** 本插件配置;不可用时用默认配置。 */
57
+ readonly config?: () => CompanionConfig;
58
+ }
59
+ /**
60
+ * 渲染检索结果所需的最小字段集。
61
+ *
62
+ * 刻意不用 wire 类型 MarketItem:工具输出是它自己的投影(少几个字段、stars 用
63
+ * 可选表达未知),让渲染函数按它真正读到的字段声明入参,比硬套一个大类型更能
64
+ * 保证两者一起演进。
65
+ */
66
+ export interface SearchRow {
67
+ readonly repo: string;
68
+ readonly name: string;
69
+ readonly description: string;
70
+ /** 星数;未知时 null(与 src/types.ts 的 MarketItem 契约一致:null 表达未知,而不是缺键)。 */
71
+ readonly stars: number | null;
72
+ readonly topics: readonly string[];
73
+ readonly installed: boolean;
74
+ readonly installedVersion?: string;
75
+ readonly latestVersion?: string;
76
+ }
77
+ /** 结果条数上限(模型侧一次性看到的候选)。 */
78
+ export declare const SEARCH_LIMIT_MAX = 10;
79
+ /** 结果条数下限。 */
80
+ export declare const SEARCH_LIMIT_MIN = 1;
81
+ /** 默认结果条数。 */
82
+ export declare const SEARCH_LIMIT_DEFAULT = 5;
83
+ /** 健康检查里最多带上的问题条数(细节在 UI 里看;工具输出要控制 token)。 */
84
+ export declare const HEALTH_ISSUE_LIMIT = 20;
85
+ /**
86
+ * 把检索结果渲染成模型可读文本。
87
+ *
88
+ * 最后一句"安装前先浏览仓库"是刻意的固定文案:索引元数据(星数/topic/描述)
89
+ * 判断不了质量,而安装会执行第三方代码。工具描述里也说了一遍,因为模型可能
90
+ * 只看渲染结果。
91
+ *
92
+ * @param matches - 已排序、已截断的候选。
93
+ * @param query - 原始查询(空查询时提示这是"按热度兜底")。
94
+ * @returns 文本行。
95
+ */
96
+ export declare function renderSearchText(matches: readonly SearchRow[], query: string): string;
97
+ /**
98
+ * 把体检报告渲染成模型可读文本。
99
+ * @param report - 诊断报告。
100
+ * @returns 文本行。
101
+ */
102
+ export declare function renderHealthText(report: DiagnosticReport): string;
103
+ /** 各诊断层的问题数。用具名字段而不是 Record,因为工具输出 schema 是显式枚举。 */
104
+ export interface LayerCounts {
105
+ readonly dependency: number;
106
+ readonly composition: number;
107
+ readonly runtime: number;
108
+ readonly consistency: number;
109
+ readonly ecosystem: number;
110
+ }
111
+ /** 工具定义的可注册数组类型(defineTool 的返回结构对调用方不透明)。 */
112
+ type RegisteredTool = ReturnType<typeof defineTool>;
113
+ /**
114
+ * 构造两个工具定义(不注册)。
115
+ *
116
+ * 导出它而不是只导出注册函数的原因:测试可以直接对工具定义跑 execute,验证
117
+ * "上限、排序、缺失能力时的报错"这些决策,而不必造一个假的 Cordis 上下文。
118
+ *
119
+ * @param host - 注入的宿主能力。
120
+ * @returns 工具定义数组。
121
+ */
122
+ export declare function createCompanionTools(host: CompanionToolsHost): RegisteredTool[];
123
+ /**
124
+ * 注册两个工具(tools 服务缺失时返回空数组,插件仍能加载)。
125
+ *
126
+ * @param ctx - host 上下文。
127
+ * @param host - 注入的宿主能力。
128
+ * @returns 注销函数数组。
129
+ */
130
+ export declare function registerCompanionTools(ctx: Context, host: CompanionToolsHost): (() => void)[];
131
+ export {};
package/dist/tools.js ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * agent 工具:plugin_search(市场检索)+ plugin_health(环境体检)。
3
+ *
4
+ * 归属:A 类·重写(注册官方工具面;旧 src/tools.ts 仅作意图参考,未复制代码)。
5
+ * 旧实现参考:dsh-web-plugin-manager/src/tools.ts(365 行:defineTool 的用法、
6
+ * 文本渲染形态、plugin_search 的"安装前先浏览仓库"提示、空查询按星数兜底)。
7
+ * 官方复用:@deepseek-ai/dsh-tools 的 defineTool + ctx.tools.register(与官方
8
+ * plugin_manager 工具同一套注册面)。
9
+ * 前提检查:**用户决定删掉了旧实现的四个工具**——"agent 工具只留 plugin_search +
10
+ * 健康检查,其余交还官方"。旧 plugin_status / plugin_install / plugin_uninstall /
11
+ * plugin_toggle 的前提(官方没有当前环境管理能力)在 0.1.6 之后消失:官方
12
+ * plugin_manager 工具已覆盖列出/启停/安装/卸载四个动作,再注册一份同名能力
13
+ * 只会造成两个入口两套语义。因此本模块只提供官方**没有**的两件事:
14
+ * 市场语义检索(官方没有市场概念)与环境体检(官方不做深度诊断)。
15
+ *
16
+ * 依赖注入而不是静态 import:本模块要用的 match(打分)与 diagnostics(诊断引擎)
17
+ * 由同一仓库的其他模块提供,它们的导出面定稿之前静态 import 会让**整个 host 构建**
18
+ * 编译不过(tsc 一次编译 src 下所有 .ts)。所以这里把两者声明成可注入的能力
19
+ * (CompanionToolsHost),由装配点传进来——旧实现用同样的手法避免与 index.ts 循环
20
+ * import,这里顺带把"构建顺序"也解耦了。
21
+ */
22
+ import { defineTool } from '@deepseek-ai/dsh-tools';
23
+ import { DEFAULT_CONFIG } from "./settings.js";
24
+ /** 检索结果里每条最多展示的 topic 数(多了会把一行撑爆)。 */
25
+ const MAX_TOPICS = 3;
26
+ /** 结果条数上限(模型侧一次性看到的候选)。 */
27
+ export const SEARCH_LIMIT_MAX = 10;
28
+ /** 结果条数下限。 */
29
+ export const SEARCH_LIMIT_MIN = 1;
30
+ /** 默认结果条数。 */
31
+ export const SEARCH_LIMIT_DEFAULT = 5;
32
+ /** 健康检查里最多带上的问题条数(细节在 UI 里看;工具输出要控制 token)。 */
33
+ export const HEALTH_ISSUE_LIMIT = 20;
34
+ /**
35
+ * 把检索结果渲染成模型可读文本。
36
+ *
37
+ * 最后一句"安装前先浏览仓库"是刻意的固定文案:索引元数据(星数/topic/描述)
38
+ * 判断不了质量,而安装会执行第三方代码。工具描述里也说了一遍,因为模型可能
39
+ * 只看渲染结果。
40
+ *
41
+ * @param matches - 已排序、已截断的候选。
42
+ * @param query - 原始查询(空查询时提示这是"按热度兜底")。
43
+ * @returns 文本行。
44
+ */
45
+ export function renderSearchText(matches, query) {
46
+ if (matches.length === 0) {
47
+ return query === ''
48
+ ? 'The marketplace index returned no entries. Check that the index is enabled in the companion settings, or run plugin_search again later.'
49
+ : 'No marketplace entry matched "' + query + '". Try broader terms (for example image, terminal, memory, rag).';
50
+ }
51
+ const lines = matches.map((item, index) => {
52
+ const state = item.installed
53
+ ? ' [installed' + (item.installedVersion === undefined ? '' : ' ' + item.installedVersion) + ']'
54
+ : '';
55
+ const latest = item.latestVersion === undefined ? '' : ' latest ' + item.latestVersion;
56
+ const topics = item.topics.length > 0 ? ' [' + item.topics.slice(0, MAX_TOPICS).join(', ') + ']' : '';
57
+ const stars = item.stars === null ? '' : ' ' + String(item.stars) + ' stars';
58
+ return String(index + 1) + '. ' + item.name + ' (' + item.repo + ')'
59
+ + '\n ' + (item.description === '' ? '(no description)' : item.description)
60
+ + '\n https://github.com/' + item.repo + stars + topics + latest + state;
61
+ });
62
+ return lines.join('\n\n')
63
+ + '\n\nReview the repository before installing: marketplace metadata cannot judge quality, and '
64
+ + 'installation runs third-party code. To install, call plugin_manager with action "install_bundle" '
65
+ + 'and the package spec (it requires danger-full-access permission or approval).';
66
+ }
67
+ /**
68
+ * 把体检报告渲染成模型可读文本。
69
+ * @param report - 诊断报告。
70
+ * @returns 文本行。
71
+ */
72
+ export function renderHealthText(report) {
73
+ const layers = Object.entries(countsOf(report))
74
+ .filter(([, count]) => count > 0)
75
+ .map(([layer, count]) => layer + '=' + String(count));
76
+ const header = 'Environment "' + report.environment + '" checked at ' + report.generatedAt
77
+ + '; issues: ' + String(report.issues.length)
78
+ + (layers.length === 0 ? '' : ' (' + layers.join(', ') + ')');
79
+ const lines = report.issues.slice(0, HEALTH_ISSUE_LIMIT).map(issue => {
80
+ const fix = issue.fix === undefined ? '' : '\n fix: ' + issue.fix.action + ' — ' + issue.fix.summary;
81
+ const evidence = issue.evidence.slice(0, 2).map(item => item.at + ' (' + item.note + ')').join('; ');
82
+ return '[' + issue.severity + '] ' + issue.code + ': ' + issue.title
83
+ + '\n ' + issue.detail
84
+ + (evidence === '' ? '' : '\n evidence: ' + evidence)
85
+ + fix;
86
+ });
87
+ const more = report.issues.length > HEALTH_ISSUE_LIMIT
88
+ ? '\n\n(' + String(report.issues.length - HEALTH_ISSUE_LIMIT) + ' more issues omitted; open the companion environment console for the full report.)'
89
+ : '';
90
+ const skipped = report.skipped.length > 0
91
+ ? '\n\nSkipped checks (capability missing, reported honestly): '
92
+ + report.skipped.map(item => item.check + ' — ' + item.reason).join('; ')
93
+ : '';
94
+ const body = lines.length > 0 ? '\n\n' + lines.join('\n\n') : '\n\nNo issues found.';
95
+ return header + body + more + skipped;
96
+ }
97
+ /** 从报告里取各层计数。 */
98
+ function countsOf(report) {
99
+ return {
100
+ dependency: report.counts.dependency,
101
+ composition: report.counts.composition,
102
+ runtime: report.counts.runtime,
103
+ consistency: report.counts.consistency,
104
+ ecosystem: report.counts.ecosystem,
105
+ };
106
+ }
107
+ /** 体检报告的投影:保留模型决策需要的字段,丢掉 UI 专用的量。 */
108
+ function healthValueOf(report) {
109
+ return {
110
+ environment: report.environment,
111
+ generatedAt: report.generatedAt,
112
+ summary: report.issues.length === 0
113
+ ? 'no issues found'
114
+ : String(report.issues.length) + ' issue(s): ' + Object.entries(countsOf(report))
115
+ .filter(([, count]) => count > 0)
116
+ .map(([layer, count]) => layer + '=' + String(count))
117
+ .join(', '),
118
+ counts: countsOf(report),
119
+ issues: report.issues.slice(0, HEALTH_ISSUE_LIMIT).map(issue => ({
120
+ id: issue.id,
121
+ layer: issue.layer,
122
+ severity: issue.severity,
123
+ code: issue.code,
124
+ title: issue.title,
125
+ detail: issue.detail,
126
+ subjects: [...issue.subjects],
127
+ evidence: issue.evidence.map(item => ({ at: item.at, note: item.note })),
128
+ ...issue.fix === undefined ? {} : { fix: { ...issue.fix } },
129
+ })),
130
+ skipped: report.skipped.map(item => ({ check: item.check, reason: item.reason })),
131
+ };
132
+ }
133
+ /**
134
+ * 构造两个工具定义(不注册)。
135
+ *
136
+ * 导出它而不是只导出注册函数的原因:测试可以直接对工具定义跑 execute,验证
137
+ * "上限、排序、缺失能力时的报错"这些决策,而不必造一个假的 Cordis 上下文。
138
+ *
139
+ * @param host - 注入的宿主能力。
140
+ * @returns 工具定义数组。
141
+ */
142
+ export function createCompanionTools(host) {
143
+ const configOf = () => host.config?.() ?? DEFAULT_CONFIG;
144
+ return [
145
+ defineTool({
146
+ name: 'plugin_search',
147
+ description: 'Search the DSH plugin marketplace for plugins that match a need, in natural language '
148
+ + '("OCR screenshots", "memory rag", "terminal UI"; Chinese and English both work). Returns candidate '
149
+ + 'repositories with stars, topics, description and install state. Marketplace metadata cannot judge '
150
+ + 'quality: advise the user to review the repository before installing. To install a result, use the '
151
+ + 'plugin_manager tool with action "install_bundle". Use this when the user wants to find or compare '
152
+ + 'DSH plugins.',
153
+ parameters: {
154
+ query: { type: 'string', required: true, description: 'What the user wants, e.g. "OCR screenshots", "memory rag".' },
155
+ limit: { type: 'number', description: 'Maximum number of results (1-10, default 5).' },
156
+ refresh: { type: 'boolean', description: 'Force a marketplace index refresh instead of using the cache. Defaults to false.' },
157
+ },
158
+ output: {
159
+ schema: {
160
+ type: 'object',
161
+ additionalProperties: false,
162
+ properties: {
163
+ query: { type: 'string', required: true },
164
+ generatedAt: { type: 'string', required: true },
165
+ indexed: { type: 'number', required: true },
166
+ matches: {
167
+ type: 'array',
168
+ required: true,
169
+ items: {
170
+ type: 'object',
171
+ additionalProperties: false,
172
+ properties: {
173
+ repo: { type: 'string', required: true },
174
+ name: { type: 'string', required: true },
175
+ description: { type: 'string', required: true },
176
+ stars: { oneOf: [{ type: 'number' }, { type: 'null' }], required: true },
177
+ topics: { type: 'array', items: { type: 'string' }, required: true },
178
+ installed: { type: 'boolean', required: true },
179
+ installedVersion: { type: 'string' },
180
+ latestVersion: { type: 'string' },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ render: (_args, value) => [{
187
+ type: 'text',
188
+ text: renderSearchText(value.matches.map(match => ({
189
+ repo: match.repo,
190
+ name: match.name,
191
+ description: match.description,
192
+ stars: match.stars ?? null,
193
+ updatedAt: null,
194
+ topics: match.topics,
195
+ installed: match.installed,
196
+ ...match.installedVersion === undefined ? {} : { installedVersion: match.installedVersion },
197
+ ...match.latestVersion === undefined ? {} : { latestVersion: match.latestVersion },
198
+ })), value.query),
199
+ }],
200
+ },
201
+ async execute(args) {
202
+ const raw = typeof args.limit === 'number' && Number.isFinite(args.limit) ? Math.trunc(args.limit) : SEARCH_LIMIT_DEFAULT;
203
+ const limit = Math.min(Math.max(SEARCH_LIMIT_MIN, raw), SEARCH_LIMIT_MAX);
204
+ const query = String(args.query ?? '').trim();
205
+ const lookup = await host.market({ refresh: args.refresh === true });
206
+ // 空查询不报错:match.ts 的回退语义是"按热度给前 N 条",这对
207
+ // "有什么插件"这类探索性提问正是想要的(旧实现在这里是抛错)。
208
+ const ranked = host.rank(lookup.items, query);
209
+ const matches = ranked.slice(0, limit).map(item => ({
210
+ repo: item.repo,
211
+ name: item.name,
212
+ description: item.description,
213
+ stars: item.stars,
214
+ topics: item.topics.slice(0, MAX_TOPICS),
215
+ installed: item.installed === true,
216
+ ...item.installedVersion === undefined ? {} : { installedVersion: item.installedVersion },
217
+ ...item.latestVersion === undefined ? {} : { latestVersion: item.latestVersion },
218
+ }));
219
+ return {
220
+ query,
221
+ generatedAt: lookup.generatedAt,
222
+ indexed: lookup.total ?? lookup.items.length,
223
+ matches,
224
+ };
225
+ },
226
+ presentCall: args => ({
227
+ card: 'generic',
228
+ title: 'Search the plugin marketplace',
229
+ kind: 'read',
230
+ rawInput: args,
231
+ }),
232
+ }),
233
+ defineTool({
234
+ name: 'plugin_health',
235
+ description: 'Run a deep health check of the local DSH environment and return a structured summary: '
236
+ + 'dependency problems, composition problems (duplicate row ids, disabled dependencies), runtime '
237
+ + 'problems (failed or long-pending plugin fibers, service/tool name conflicts), and consistency '
238
+ + 'problems between the installed files and what is actually loaded. Every finding carries evidence '
239
+ + '(a file and line, or a loader entry) and, when a safe fix exists, the action to take. Use this '
240
+ + 'when the user reports a plugin that will not load, a profile that will not start, or asks for a '
241
+ + 'health check. Read-only: it never changes the environment.',
242
+ parameters: {
243
+ layer: {
244
+ type: 'string',
245
+ enum: ['dependency', 'composition', 'runtime', 'consistency', 'ecosystem'],
246
+ description: 'Report only one diagnostic layer. Omit for every enabled layer.',
247
+ },
248
+ },
249
+ output: {
250
+ schema: {
251
+ type: 'object',
252
+ additionalProperties: false,
253
+ properties: {
254
+ environment: { type: 'string', required: true },
255
+ generatedAt: { type: 'string', required: true },
256
+ summary: { type: 'string', required: true },
257
+ counts: {
258
+ type: 'object',
259
+ required: true,
260
+ additionalProperties: false,
261
+ properties: {
262
+ dependency: { type: 'number', required: true },
263
+ composition: { type: 'number', required: true },
264
+ runtime: { type: 'number', required: true },
265
+ consistency: { type: 'number', required: true },
266
+ ecosystem: { type: 'number', required: true },
267
+ },
268
+ },
269
+ issues: {
270
+ type: 'array',
271
+ required: true,
272
+ items: {
273
+ type: 'object',
274
+ additionalProperties: false,
275
+ properties: {
276
+ id: { type: 'string', required: true },
277
+ layer: { type: 'string', required: true },
278
+ severity: { type: 'string', required: true },
279
+ code: { type: 'string', required: true },
280
+ title: { type: 'string', required: true },
281
+ detail: { type: 'string', required: true },
282
+ subjects: { type: 'array', items: { type: 'string' }, required: true },
283
+ evidence: {
284
+ type: 'array',
285
+ required: true,
286
+ items: {
287
+ type: 'object',
288
+ additionalProperties: false,
289
+ properties: {
290
+ at: { type: 'string', required: true },
291
+ note: { type: 'string', required: true },
292
+ },
293
+ },
294
+ },
295
+ fix: {
296
+ type: 'object',
297
+ additionalProperties: false,
298
+ properties: {
299
+ action: { type: 'string', required: true },
300
+ target: { type: 'string' },
301
+ summary: { type: 'string', required: true },
302
+ },
303
+ },
304
+ },
305
+ },
306
+ },
307
+ skipped: {
308
+ type: 'array',
309
+ required: true,
310
+ items: {
311
+ type: 'object',
312
+ additionalProperties: false,
313
+ properties: {
314
+ check: { type: 'string', required: true },
315
+ reason: { type: 'string', required: true },
316
+ },
317
+ },
318
+ },
319
+ },
320
+ },
321
+ render: (_args, value) => [{
322
+ type: 'text',
323
+ text: renderHealthText({
324
+ environment: value.environment,
325
+ generatedAt: value.generatedAt,
326
+ counts: value.counts,
327
+ issues: value.issues.map(issue => ({
328
+ id: issue.id,
329
+ layer: issue.layer,
330
+ severity: issue.severity,
331
+ code: issue.code,
332
+ title: issue.title,
333
+ detail: issue.detail,
334
+ subjects: [...issue.subjects],
335
+ evidence: issue.evidence.map(item => ({ kind: 'file', at: item.at, note: item.note })),
336
+ ...issue.fix === undefined ? {} : { fix: issue.fix },
337
+ })),
338
+ skipped: value.skipped,
339
+ }),
340
+ }],
341
+ },
342
+ async execute(args) {
343
+ const config = configOf();
344
+ const report = await host.analyze(undefined, host.environment?.(), config);
345
+ const filtered = args.layer === undefined
346
+ ? report
347
+ : {
348
+ ...report,
349
+ issues: report.issues.filter(issue => issue.layer === args.layer),
350
+ counts: countsOf(report),
351
+ };
352
+ return healthValueOf(filtered);
353
+ },
354
+ presentCall: args => ({
355
+ card: 'generic',
356
+ title: 'Run an environment health check',
357
+ kind: 'read',
358
+ rawInput: args,
359
+ }),
360
+ }),
361
+ ];
362
+ }
363
+ /**
364
+ * 注册两个工具(tools 服务缺失时返回空数组,插件仍能加载)。
365
+ *
366
+ * @param ctx - host 上下文。
367
+ * @param host - 注入的宿主能力。
368
+ * @returns 注销函数数组。
369
+ */
370
+ export function registerCompanionTools(ctx, host) {
371
+ const tools = ctx.get('tools');
372
+ if (tools === undefined || typeof tools.register !== 'function') {
373
+ ctx.logger?.info?.('plugin-manager-companion: tools service unavailable, agent tools not registered');
374
+ return [];
375
+ }
376
+ return createCompanionTools(host).map(definition => tools.register(definition));
377
+ }