dsh-plugin-experts 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.
package/dist/host.js ADDED
@@ -0,0 +1,1298 @@
1
+ // ../../shared/host/route.mjs
2
+ function json(body, status) {
3
+ return Response.json(body, { status, headers: { "cache-control": "no-store" } });
4
+ }
5
+ function ok(value) {
6
+ return json({ ok: true, value: value ?? null }, 200);
7
+ }
8
+ function fail(code, message) {
9
+ return json({ ok: false, error: { code, message } }, 200);
10
+ }
11
+ var FeatureError2 = class extends Error {
12
+ /**
13
+ * @param {string} code - stable machine-readable code, for example `connector/not-found`.
14
+ * @param {string} message - human-readable reason shown to the user.
15
+ */
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.name = "FeatureError";
19
+ this.code = code;
20
+ }
21
+ };
22
+ function registerFeatureRoute(ctx, { feature, handlers, methods = ["POST"] }) {
23
+ const path = `/api/dsh-skills/${feature}`;
24
+ return ctx.effect(
25
+ () => ctx.connection.fetch.register({
26
+ path,
27
+ methods: [...methods],
28
+ requestBody: "buffered",
29
+ fetch: async (request) => {
30
+ try {
31
+ if (request.method !== "POST") return fail("method-not-allowed", "\u6B64\u8DEF\u7531\u53EA\u63A5\u53D7 POST");
32
+ const body = await request.json().catch(() => null);
33
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
34
+ return fail("malformed-request", "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
35
+ }
36
+ const { endpoint, payload } = body;
37
+ if (typeof endpoint !== "string") return fail("malformed-request", "endpoint \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
38
+ const handler = Object.hasOwn(handlers, endpoint) ? handlers[endpoint] : void 0;
39
+ if (typeof handler !== "function") return fail("unknown-endpoint", `\u672A\u77E5\u7AEF\u70B9 ${JSON.stringify(endpoint)}`);
40
+ const result = await handler(payload ?? {}, request);
41
+ return result instanceof Response ? result : ok(result);
42
+ } catch (error) {
43
+ const code = typeof error?.code === "string" && error.code.length > 0 ? error.code : "handler-failed";
44
+ return fail(code, String(error?.message ?? error));
45
+ }
46
+ }
47
+ }),
48
+ `dsh-skills: ${feature} route`
49
+ );
50
+ }
51
+
52
+ // ../../shared/dsh-package.mjs
53
+ import { createRequire } from "node:module";
54
+ import { isAbsolute, join } from "node:path";
55
+ import { pathToFileURL } from "node:url";
56
+ function dshResolutionAnchors({ argv = process.argv, cwd = process.cwd() } = {}) {
57
+ const anchors = [];
58
+ if (typeof argv[1] === "string" && isAbsolute(argv[1])) anchors.push(argv[1]);
59
+ if (typeof argv[2] === "string" && isAbsolute(argv[2])) anchors.push(join(argv[2], "package.json"));
60
+ anchors.push(join(cwd, "package.json"));
61
+ return [...new Set(anchors)];
62
+ }
63
+ var stubs = /* @__PURE__ */ new Map();
64
+ function resolveDshPackage(specifier, options = {}) {
65
+ if (stubs.has(specifier)) return `stub:${specifier}`;
66
+ const tried = [];
67
+ for (const anchor of dshResolutionAnchors(options)) {
68
+ try {
69
+ return createRequire(anchor).resolve(specifier);
70
+ } catch (error) {
71
+ tried.push(`${anchor} (${error?.code ?? error?.name ?? "error"})`);
72
+ }
73
+ }
74
+ throw new Error(
75
+ `dsh-skills: cannot resolve ${JSON.stringify(specifier)} from the dsh installation or the profile.
76
+ Tried: ${tried.join(", ")}
77
+ A Host half must not import an official package by bare specifier: Node resolves that from
78
+ this plugin's realpath, which is outside the installation. Use importDshPackage() instead.`
79
+ );
80
+ }
81
+ async function importDshPackage(specifier, options) {
82
+ const stub = stubs.get(specifier);
83
+ if (stub !== void 0) return stub;
84
+ return import(pathToFileURL(resolveDshPackage(specifier, options)).href);
85
+ }
86
+
87
+ // ../../shared/host/storage.mjs
88
+ var cached;
89
+ async function storageKit() {
90
+ if (cached !== void 0) return cached;
91
+ const [domain, zod] = await Promise.all([
92
+ importDshPackage("@deepseek-ai/dsh-storage-domain"),
93
+ importDshPackage("zod")
94
+ ]);
95
+ const z = zod.z ?? zod.default ?? zod;
96
+ if (typeof domain?.defineDomain !== "function" || typeof domain?.domainTable !== "function") {
97
+ throw new Error("dsh-skills: @deepseek-ai/dsh-storage-domain no longer exports defineDomain/domainTable");
98
+ }
99
+ if (typeof z?.object !== "function") {
100
+ throw new Error("dsh-skills: the installation's zod does not expose z.object");
101
+ }
102
+ cached = { defineDomain: domain.defineDomain, domainTable: domain.domainTable, descriptorOf: domain.descriptorOf, z };
103
+ return cached;
104
+ }
105
+
106
+ // src/domains.js
107
+ var LOCAL_PRINCIPAL = "local";
108
+ function buildExpertDomains({ z, defineDomain, domainTable }) {
109
+ const loose = z.record(z.string(), z.unknown());
110
+ const stateSchema = z.object({
111
+ schemaVersion: z.literal(1),
112
+ experts: loose,
113
+ drafts: loose,
114
+ revisions: loose,
115
+ adoptions: loose,
116
+ // Additive collections must be `.default()`ed, never plain: the domain
117
+ // facility validates every stored record **on `open()`**, before the
118
+ // consumer ever sees it (`dsh-storage-domain`'s `parseRecord`), and an
119
+ // unparsable record is a hard `DomainError` unless the spec opts into
120
+ // `backup-and-skip`. A record written before `teams` existed therefore
121
+ // throws out of `ctx.storageDomain.open()`, which used to abort the whole
122
+ // of `apply()` in `index.js` — losing the route, the tools *and* the
123
+ // `/expert-manager` skill, while every unit test still passed
124
+ // (none of them opened a real domain against a real stored record).
125
+ // `.default({})` is what keeps a new collection additive: the record stays
126
+ // `schemaVersion: 1` and old rows still parse.
127
+ teams: loose.default({})
128
+ });
129
+ return {
130
+ stateSchema,
131
+ expertsDomain: defineDomain({
132
+ name: "dsh_skills_experts",
133
+ version: 1,
134
+ layout: "per-record",
135
+ tables: { states: domainTable(stateSchema) }
136
+ })
137
+ };
138
+ }
139
+
140
+ // src/manager.js
141
+ import { createHash, randomUUID } from "node:crypto";
142
+ var ROLE_TOKEN_BUDGET = 8e3;
143
+ var MAX_PREFERRED_SKILLS = 12;
144
+ var SKILL_NAME_MAX = 128;
145
+ var NAME_MAX = 120;
146
+ var TEAM_NAME_MAX = NAME_MAX;
147
+ var TEAM_DESCRIPTION_MAX = 2e3;
148
+ var TEAM_MANDATE_MAX = 6e4;
149
+ var TEAM_MEMBERS_MIN = 2;
150
+ var TEAM_MEMBERS_MAX = 16;
151
+ var MEMBER_KEY = /^[a-z][a-z0-9-]{0,39}$/;
152
+ function estimateRoleTokens(value) {
153
+ let wide = 0;
154
+ let narrow = 0;
155
+ for (const character of value) /[^\u0000-\u00ff]/u.test(character) ? wide++ : narrow++;
156
+ return wide + Math.ceil(narrow / 4);
157
+ }
158
+ function clean(value, code, max) {
159
+ const next = typeof value === "string" ? value.trim() : "";
160
+ if (next.length === 0 || next.length > max) throw new FeatureError2(code, "\u5185\u5BB9\u4E3A\u7A7A\u6216\u8D85\u51FA\u957F\u5EA6\u9650\u5236\u3002");
161
+ return next;
162
+ }
163
+ function revisionFor(fields) {
164
+ return createHash("sha256").update(JSON.stringify({
165
+ name: fields.name,
166
+ description: fields.description,
167
+ role: fields.role,
168
+ preferredSkills: fields.preferredSkills
169
+ }), "utf8").digest("hex");
170
+ }
171
+ function pickFields(row) {
172
+ return {
173
+ name: row.name,
174
+ description: row.description,
175
+ role: row.role,
176
+ preferredSkills: [...row.preferredSkills ?? []]
177
+ };
178
+ }
179
+ function teamRevisionFor(fields) {
180
+ return createHash("sha256").update(JSON.stringify({
181
+ name: fields.name,
182
+ description: fields.description,
183
+ mandate: fields.mandate,
184
+ members: fields.members.map((member) => ({ key: member.key, expertId: member.expertId, lead: member.lead }))
185
+ }), "utf8").digest("hex");
186
+ }
187
+ function readSkillNames(value) {
188
+ if (value === void 0 || value === null) return [];
189
+ if (!Array.isArray(value)) {
190
+ throw new FeatureError2("experts/invalid-request", "\u5B57\u6BB5 preferredSkills \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\u3002");
191
+ }
192
+ if (value.length > MAX_PREFERRED_SKILLS) {
193
+ throw new FeatureError2("experts/too-many-skills", `\u9996\u9009\u6280\u80FD\u6700\u591A ${MAX_PREFERRED_SKILLS} \u4E2A\u3002`);
194
+ }
195
+ const names = [];
196
+ for (const entry of value) {
197
+ const name2 = typeof entry === "string" ? entry.trim() : "";
198
+ if (name2.length === 0 || name2.length > SKILL_NAME_MAX) {
199
+ throw new FeatureError2("experts/invalid-skill-name", "\u9996\u9009\u6280\u80FD\u540D\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u4E14\u4E0D\u8D85\u8FC7 128 \u4E2A\u5B57\u7B26\u3002");
200
+ }
201
+ if (!names.includes(name2)) names.push(name2);
202
+ }
203
+ return names;
204
+ }
205
+ function normalizeExpertFields(input) {
206
+ const name2 = clean(input?.name, "experts/invalid-name", NAME_MAX);
207
+ const description = clean(input?.description, "experts/invalid-description", 2e3);
208
+ const role = clean(input?.role, "experts/invalid-role", 6e4);
209
+ if (estimateRoleTokens(role) > ROLE_TOKEN_BUDGET) {
210
+ throw new FeatureError2("experts/role-budget-exceeded", "\u89D2\u8272\u8BF4\u660E\u8D85\u8FC7\u9884\u7559\u4E0A\u4E0B\u6587\u9884\u7B97\uFF0C\u8BF7\u5220\u51CF\u91CD\u590D\u80CC\u666F\u3002");
211
+ }
212
+ return { name: name2, description, role, preferredSkills: readSkillNames(input?.preferredSkills) };
213
+ }
214
+ function optionalText(value, code, max) {
215
+ const next = typeof value === "string" ? value.trim() : "";
216
+ if (next.length > max) throw new FeatureError2(code, "\u5185\u5BB9\u8D85\u51FA\u957F\u5EA6\u9650\u5236\u3002");
217
+ return next;
218
+ }
219
+ function readTeamMembers(value, state) {
220
+ if (!Array.isArray(value)) {
221
+ throw new FeatureError2("experts/invalid-team-members", "\u5B57\u6BB5 members \u5FC5\u987B\u662F\u6570\u7EC4\u3002");
222
+ }
223
+ if (value.length < TEAM_MEMBERS_MIN || value.length > TEAM_MEMBERS_MAX) {
224
+ throw new FeatureError2(
225
+ "experts/team-members-out-of-range",
226
+ `\u4E13\u5BB6\u56E2\u9700\u8981 ${TEAM_MEMBERS_MIN} \u81F3 ${TEAM_MEMBERS_MAX} \u4F4D\u6210\u5458\u3002`
227
+ );
228
+ }
229
+ const members = [];
230
+ const keys = /* @__PURE__ */ new Set();
231
+ const expertIds = /* @__PURE__ */ new Set();
232
+ let leads = 0;
233
+ for (const entry of value) {
234
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
235
+ throw new FeatureError2("experts/invalid-team-members", "\u6BCF\u4E2A\u6210\u5458\u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
236
+ }
237
+ const key = typeof entry.key === "string" ? entry.key.trim() : "";
238
+ if (!MEMBER_KEY.test(key) || key === "lead") {
239
+ throw new FeatureError2("experts/invalid-member-key", "\u6210\u5458\u6807\u8BC6\u987B\u4E3A\u5C0F\u5199\u82F1\u6587\u548C\u8FDE\u5B57\u7B26\uFF0Clead \u4FDD\u7559\u7ED9\u4E3B\u7406\u4EBA\u3002");
240
+ }
241
+ if (keys.has(key)) throw new FeatureError2("experts/duplicate-member-key", `\u6210\u5458\u6807\u8BC6 ${key} \u91CD\u590D\u3002`);
242
+ keys.add(key);
243
+ const expertId = requireString(entry.expertId, "members[].expertId");
244
+ if (state.experts[expertId] === void 0) {
245
+ throw new FeatureError2("experts/unknown-member", `\u6210\u5458 ${key} \u5F15\u7528\u7684\u4E13\u5BB6\u4E0D\u5B58\u5728\u3002`);
246
+ }
247
+ if (expertIds.has(expertId)) throw new FeatureError2("experts/duplicate-member", "\u540C\u4E00\u4F4D\u4E13\u5BB6\u4E0D\u80FD\u5728\u4E13\u5BB6\u56E2\u91CC\u51FA\u73B0\u4E24\u6B21\u3002");
248
+ expertIds.add(expertId);
249
+ const lead = entry.lead === true;
250
+ if (lead) leads += 1;
251
+ members.push({ key, expertId, lead });
252
+ }
253
+ if (leads > 1) throw new FeatureError2("experts/multiple-leads", "\u4E13\u5BB6\u56E2\u53EA\u80FD\u6709\u4E00\u4F4D\u4E3B\u7406\u4EBA\u6210\u5458\u3002");
254
+ return members;
255
+ }
256
+ function normalizeTeamFields(input, state) {
257
+ const name2 = clean(input?.name, "experts/invalid-team-name", TEAM_NAME_MAX);
258
+ return {
259
+ name: name2,
260
+ description: optionalText(input?.description, "experts/invalid-team-description", TEAM_DESCRIPTION_MAX),
261
+ mandate: optionalText(input?.mandate, "experts/invalid-team-mandate", TEAM_MANDATE_MAX),
262
+ members: readTeamMembers(input?.members, state)
263
+ };
264
+ }
265
+ function requireString(value, field) {
266
+ if (typeof value !== "string" || value.length === 0) {
267
+ throw new FeatureError2("experts/invalid-request", `\u7F3A\u5C11\u5B57\u6BB5 ${field}\u3002`);
268
+ }
269
+ return value;
270
+ }
271
+ function emptyState() {
272
+ return { schemaVersion: 1, experts: {}, drafts: {}, revisions: {}, adoptions: {}, teams: {} };
273
+ }
274
+ function copyName(name2) {
275
+ const suffix = " \u526F\u672C";
276
+ const room = NAME_MAX - suffix.length;
277
+ return `${name2.length > room ? name2.slice(0, room).trimEnd() : name2}${suffix}`;
278
+ }
279
+ var ExpertManager = class {
280
+ /**
281
+ * @param {import('@deepseek-ai/cordis').Context} ctx - Host context.
282
+ * @param {{ expertsDomain: object }} domains - built spec.
283
+ */
284
+ constructor(ctx, domains) {
285
+ this.ctx = ctx;
286
+ this.domains = domains;
287
+ this.states = void 0;
288
+ this.state = void 0;
289
+ this.serial = Promise.resolve();
290
+ }
291
+ /** Open the domain and load the single state record into memory. */
292
+ async init() {
293
+ const domain = await this.ctx.storageDomain.open(this.domains.expertsDomain);
294
+ this.ctx.effect(() => () => domain.close(), "dsh-plugin-experts: domain");
295
+ this.states = domain.table("states");
296
+ const stored = this.states.get(LOCAL_PRINCIPAL);
297
+ const filled = stored === void 0 ? emptyState() : { ...stored, teams: stored.teams ?? {} };
298
+ this.state = filled;
299
+ if (stored === void 0 || stored.teams === void 0) await this.states.put(LOCAL_PRINCIPAL, filled);
300
+ }
301
+ // ---- read surface --------------------------------------------------------
302
+ /** Expert summaries, newest edit first. */
303
+ list(query = "") {
304
+ const state = this.load();
305
+ const needle = typeof query === "string" ? query.trim().toLocaleLowerCase() : "";
306
+ return Object.values(state.experts).map((expert) => this.summary(state, expert)).filter((row) => needle.length === 0 || `${row.name} ${row.description} ${row.preferredSkills.join(" ")}`.toLocaleLowerCase().includes(needle)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
307
+ }
308
+ /** One expert's summary row. */
309
+ summary(state, expert) {
310
+ const draft = state.drafts[expert.id];
311
+ const published = expert.publishedRevisionId === void 0 ? void 0 : state.revisions[expert.publishedRevisionId];
312
+ return {
313
+ id: expert.id,
314
+ name: draft?.name ?? published?.name ?? "",
315
+ description: draft?.description ?? published?.description ?? "",
316
+ preferredSkills: draft?.preferredSkills ?? published?.preferredSkills ?? [],
317
+ published: published !== void 0,
318
+ revisionNumber: published?.number ?? 0,
319
+ draftRevision: expert.draftRevision,
320
+ updatedAt: expert.updatedAt
321
+ };
322
+ }
323
+ /** One expert with its draft, every revision and the published pointer. */
324
+ get(expertId) {
325
+ const state = this.load();
326
+ return this.detail(state, this.requireExpert(state, expertId));
327
+ }
328
+ /** Build the detail shape from a state snapshot. */
329
+ detail(state, expert) {
330
+ const revisions = Object.values(state.revisions).filter((row) => row.expertId === expert.id).sort((a, b) => b.number - a.number);
331
+ return {
332
+ expert,
333
+ draft: state.drafts[expert.id] ?? null,
334
+ revisions,
335
+ publishedRevision: revisions.find((row) => row.id === expert.publishedRevisionId) ?? null
336
+ };
337
+ }
338
+ /** The adoption record for one conversation, or null. */
339
+ adoption(sessionId) {
340
+ return this.load().adoptions[sessionId] ?? null;
341
+ }
342
+ /**
343
+ * Everything the prompt needs for one conversation, or undefined when it has
344
+ * no adoption (or the referenced expert/revision disappeared).
345
+ */
346
+ adoptionContext(sessionId) {
347
+ const state = this.load();
348
+ const adoption = state.adoptions[sessionId];
349
+ if (adoption === void 0) return void 0;
350
+ const expert = state.experts[adoption.expertId];
351
+ const revision = state.revisions[adoption.revisionId];
352
+ if (expert === void 0 || revision === void 0) return void 0;
353
+ return { adoption, expert, revision };
354
+ }
355
+ // ---- team read surface ---------------------------------------------------
356
+ /**
357
+ * Team summaries, newest edit first.
358
+ *
359
+ * A summary resolves each member to the expert it references so a card can be
360
+ * rendered without a second round trip. A member whose expert has since been
361
+ * deleted is reported with `missing: true` and placeholder text rather than
362
+ * dropped, because silently shortening a roster would misrepresent the team.
363
+ */
364
+ listTeams(query = "") {
365
+ const state = this.load();
366
+ const needle = typeof query === "string" ? query.trim().toLocaleLowerCase() : "";
367
+ return Object.values(state.teams).map((team) => this.teamSummary(state, team)).filter((row) => needle.length === 0 || `${row.name} ${row.description} ${row.mandate} ${row.members.map((member) => member.name).join(" ")}`.toLocaleLowerCase().includes(needle)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
368
+ }
369
+ /** One team's summary row, with its roster resolved. */
370
+ teamSummary(state, team) {
371
+ return {
372
+ id: team.id,
373
+ name: team.name,
374
+ description: team.description,
375
+ mandate: team.mandate,
376
+ members: team.members.map((member) => this.memberSummary(state, member)),
377
+ revision: team.revision,
378
+ createdAt: team.createdAt,
379
+ updatedAt: team.updatedAt
380
+ };
381
+ }
382
+ /** Resolve one member reference against the live expert record. */
383
+ memberSummary(state, member) {
384
+ const expert = state.experts[member.expertId];
385
+ if (expert === void 0) {
386
+ return { key: member.key, expertId: member.expertId, lead: member.lead, name: "", description: "", published: false, missing: true };
387
+ }
388
+ const row = this.summary(state, expert);
389
+ return { key: member.key, expertId: member.expertId, lead: member.lead, name: row.name, description: row.description, published: row.published, missing: false };
390
+ }
391
+ /** One team with its roster resolved. */
392
+ getTeam(teamId) {
393
+ const state = this.load();
394
+ return this.teamSummary(state, this.requireTeam(state, teamId));
395
+ }
396
+ // ---- write surface -------------------------------------------------------
397
+ /** Create an expert with its first draft. Nothing is published yet. */
398
+ create(input) {
399
+ return this.mutate((state) => {
400
+ const fields = normalizeExpertFields(input);
401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
402
+ const expertId = randomUUID();
403
+ const draft = { expertId, ...fields, revision: revisionFor(fields), createdAt: now, updatedAt: now };
404
+ const expert = { id: expertId, draftRevision: draft.revision, createdAt: now, updatedAt: now };
405
+ const next = {
406
+ ...state,
407
+ experts: { ...state.experts, [expertId]: expert },
408
+ drafts: { ...state.drafts, [expertId]: draft }
409
+ };
410
+ return { next, result: this.detail(next, expert) };
411
+ });
412
+ }
413
+ /**
414
+ * Edit an expert's draft under an optimistic revision check.
415
+ *
416
+ * The expected revision is the draft's content address, so a stale save is
417
+ * refused rather than silently overwriting another editor's change.
418
+ */
419
+ updateDraft(expertId, patch, expectedRevision) {
420
+ return this.mutate((state) => {
421
+ const expert = this.requireExpert(state, expertId);
422
+ const draft = this.requireDraft(state, expertId);
423
+ if (patch === null || typeof patch !== "object" || Array.isArray(patch)) {
424
+ throw new FeatureError2("experts/invalid-request", "\u5B57\u6BB5 patch \u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
425
+ }
426
+ if (draft.revision !== expectedRevision) {
427
+ throw new FeatureError2("experts/draft-conflict", "\u8349\u7A3F\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
428
+ }
429
+ const fields = normalizeExpertFields({
430
+ name: patch.name ?? draft.name,
431
+ description: patch.description ?? draft.description,
432
+ role: patch.role ?? draft.role,
433
+ preferredSkills: patch.preferredSkills ?? draft.preferredSkills
434
+ });
435
+ const now = (/* @__PURE__ */ new Date()).toISOString();
436
+ const nextDraft = { ...draft, ...fields, revision: revisionFor(fields), updatedAt: now };
437
+ const nextExpert = { ...expert, draftRevision: nextDraft.revision, updatedAt: now };
438
+ const next = {
439
+ ...state,
440
+ experts: { ...state.experts, [expertId]: nextExpert },
441
+ drafts: { ...state.drafts, [expertId]: nextDraft }
442
+ };
443
+ return { next, result: this.detail(next, nextExpert) };
444
+ });
445
+ }
446
+ /**
447
+ * Freeze the draft into a new immutable numbered revision and point the
448
+ * expert at it. The previous revision and every existing adoption survive
449
+ * untouched.
450
+ */
451
+ publish(expertId, expectedRevision) {
452
+ return this.mutate((state) => {
453
+ const expert = this.requireExpert(state, expertId);
454
+ const draft = this.requireDraft(state, expertId);
455
+ if (expectedRevision !== void 0 && draft.revision !== expectedRevision) {
456
+ throw new FeatureError2("experts/draft-conflict", "\u8349\u7A3F\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
457
+ }
458
+ const fields = normalizeExpertFields(draft);
459
+ const now = (/* @__PURE__ */ new Date()).toISOString();
460
+ const revision = {
461
+ id: randomUUID(),
462
+ expertId,
463
+ number: this.nextRevisionNumber(state, expertId),
464
+ ...fields,
465
+ createdAt: now
466
+ };
467
+ const nextExpert = { ...expert, publishedRevisionId: revision.id, updatedAt: now };
468
+ const next = {
469
+ ...state,
470
+ experts: { ...state.experts, [expertId]: nextExpert },
471
+ revisions: { ...state.revisions, [revision.id]: revision }
472
+ };
473
+ return { next, result: { expert: nextExpert, revision } };
474
+ });
475
+ }
476
+ /**
477
+ * Copy an expert's published revision (or its draft when nothing is
478
+ * published) into a new unpublished expert.
479
+ * @param {string} expertId - the source expert.
480
+ * @param {string} [revisionId] - an exact source revision; defaults to the published one.
481
+ */
482
+ copy(expertId, revisionId) {
483
+ return this.mutate((state) => {
484
+ const source = this.requireExpert(state, expertId);
485
+ let fields;
486
+ if (revisionId !== void 0) {
487
+ const revision = state.revisions[revisionId];
488
+ if (revision === void 0 || revision.expertId !== expertId) {
489
+ throw new FeatureError2("experts/revision-not-found", "\u672A\u627E\u5230\u8BE5\u4E13\u5BB6\u4FEE\u8BA2\u3002");
490
+ }
491
+ fields = pickFields(revision);
492
+ } else if (source.publishedRevisionId !== void 0) {
493
+ const revision = state.revisions[source.publishedRevisionId];
494
+ if (revision === void 0) throw new FeatureError2("experts/not-found", "\u4E13\u5BB6\u53D1\u5E03\u4FEE\u8BA2\u7F3A\u5931\u3002");
495
+ fields = pickFields(revision);
496
+ } else {
497
+ fields = pickFields(this.requireDraft(state, expertId));
498
+ }
499
+ const now = (/* @__PURE__ */ new Date()).toISOString();
500
+ const newId = randomUUID();
501
+ const named = { ...fields, name: copyName(fields.name) };
502
+ const draft = { expertId: newId, ...named, revision: revisionFor(named), createdAt: now, updatedAt: now };
503
+ const expert = { id: newId, draftRevision: draft.revision, createdAt: now, updatedAt: now };
504
+ const next = {
505
+ ...state,
506
+ experts: { ...state.experts, [newId]: expert },
507
+ drafts: { ...state.drafts, [newId]: draft }
508
+ };
509
+ return { next, result: this.detail(next, expert) };
510
+ });
511
+ }
512
+ /**
513
+ * Delete an expert, its draft and every revision it owns.
514
+ *
515
+ * Adoptions that reference it are dropped too, and the caller is told which
516
+ * conversations were released so their live prompt context can be removed.
517
+ *
518
+ * Teams are the harder case, because a team is *defined* by its roster and
519
+ * `createTeam`/`updateTeam` refuse a member that does not exist. Deleting an
520
+ * expert therefore has to leave every team consistent. Two designs were
521
+ * possible and this one is deliberate:
522
+ *
523
+ * (a) drop the member from every team that listed it, or
524
+ * (b) leave a dangling reference the reader has to tolerate.
525
+ *
526
+ * (b) is what upstream effectively does — it materializes each member as a
527
+ * child expert row and lets the roster keep pointing at it — but upstream also
528
+ * owns that child row's lifetime, which this repo does not port. Tolerating a
529
+ * dangling reference here would make a team unsaveable the moment it is
530
+ * edited (`updateTeam` revalidates the whole roster), which is a trap rather
531
+ * than a feature. So: **(a)**.
532
+ *
533
+ * A team whose roster would fall below `TEAM_MEMBERS_MIN` is deleted too,
534
+ * because it could no longer be re-saved or represented — a two-member team
535
+ * that loses one member is not a team any more. The caller receives both the
536
+ * teams it updated and the teams it removed, so the Client can say what
537
+ * happened instead of leaving the user to discover it.
538
+ */
539
+ remove(expertId) {
540
+ return this.mutate((state) => {
541
+ this.requireExpert(state, expertId);
542
+ const experts = { ...state.experts };
543
+ const drafts = { ...state.drafts };
544
+ delete experts[expertId];
545
+ delete drafts[expertId];
546
+ const revisions = {};
547
+ for (const [id, row] of Object.entries(state.revisions)) {
548
+ if (row.expertId !== expertId) revisions[id] = row;
549
+ }
550
+ const adoptions = {};
551
+ const releasedSessions = [];
552
+ for (const [sessionId, row] of Object.entries(state.adoptions)) {
553
+ if (row.expertId === expertId) releasedSessions.push(sessionId);
554
+ else adoptions[sessionId] = row;
555
+ }
556
+ const teams = { ...state.teams };
557
+ const updatedTeams = [];
558
+ const removedTeams = [];
559
+ for (const [teamId, team] of Object.entries(state.teams)) {
560
+ if (!team.members.some((member) => member.expertId === expertId)) continue;
561
+ const members = team.members.filter((member) => member.expertId !== expertId);
562
+ if (members.length < TEAM_MEMBERS_MIN) {
563
+ delete teams[teamId];
564
+ removedTeams.push({ id: teamId, name: team.name });
565
+ continue;
566
+ }
567
+ const now = (/* @__PURE__ */ new Date()).toISOString();
568
+ const fields = { name: team.name, description: team.description, mandate: team.mandate, members };
569
+ teams[teamId] = { ...team, ...fields, revision: teamRevisionFor(fields), updatedAt: now };
570
+ updatedTeams.push({ id: teamId, name: team.name });
571
+ }
572
+ return {
573
+ next: { ...state, experts, drafts, revisions, adoptions, teams },
574
+ result: { removed: true, releasedSessions, updatedTeams, removedTeams }
575
+ };
576
+ });
577
+ }
578
+ // ---- team write surface --------------------------------------------------
579
+ /**
580
+ * Create a team over existing experts. Nothing is published; a team has no
581
+ * revision history of its own, only a content address for optimistic edits.
582
+ */
583
+ createTeam(input) {
584
+ return this.mutate((state) => {
585
+ const fields = normalizeTeamFields(input, state);
586
+ const now = (/* @__PURE__ */ new Date()).toISOString();
587
+ const team = { id: randomUUID(), ...fields, revision: teamRevisionFor(fields), createdAt: now, updatedAt: now };
588
+ const next = { ...state, teams: { ...state.teams, [team.id]: team } };
589
+ return { next, result: this.teamSummary(next, team) };
590
+ });
591
+ }
592
+ /**
593
+ * Replace a team's authored fields under an optimistic revision check.
594
+ *
595
+ * Unlike an expert, a team has no draft/published split: the roster is the
596
+ * whole content and there is nothing downstream to freeze. The revision is a
597
+ * content address, so a stale save is refused rather than silently
598
+ * overwriting another editor's roster.
599
+ */
600
+ updateTeam(teamId, patch, expectedRevision) {
601
+ return this.mutate((state) => {
602
+ const team = this.requireTeam(state, teamId);
603
+ if (patch === null || typeof patch !== "object" || Array.isArray(patch)) {
604
+ throw new FeatureError2("experts/invalid-request", "\u5B57\u6BB5 patch \u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
605
+ }
606
+ if (expectedRevision !== void 0 && team.revision !== expectedRevision) {
607
+ throw new FeatureError2("experts/team-conflict", "\u4E13\u5BB6\u56E2\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
608
+ }
609
+ const fields = normalizeTeamFields({
610
+ name: patch.name ?? team.name,
611
+ description: patch.description ?? team.description,
612
+ mandate: patch.mandate ?? team.mandate,
613
+ members: patch.members ?? team.members
614
+ }, state);
615
+ const now = (/* @__PURE__ */ new Date()).toISOString();
616
+ const nextTeam = { ...team, ...fields, revision: teamRevisionFor(fields), updatedAt: now };
617
+ const next = { ...state, teams: { ...state.teams, [teamId]: nextTeam } };
618
+ return { next, result: this.teamSummary(next, nextTeam) };
619
+ });
620
+ }
621
+ /**
622
+ * Delete a team. Teams own nothing else, so this is a plain removal — but the
623
+ * experts it listed are untouched, because a team only ever referenced them.
624
+ */
625
+ removeTeam(teamId) {
626
+ return this.mutate((state) => {
627
+ this.requireTeam(state, teamId);
628
+ const teams = { ...state.teams };
629
+ delete teams[teamId];
630
+ return { next: { ...state, teams }, result: { removed: true } };
631
+ });
632
+ }
633
+ /**
634
+ * Adopt an expert for one conversation, pinning its current published
635
+ * revision. Passing `null` un-adopts.
636
+ */
637
+ setAdoption(sessionId, expertId) {
638
+ return this.mutate((state) => {
639
+ if (expertId === null) {
640
+ if (state.adoptions[sessionId] === void 0) return { next: state, result: null };
641
+ const adoptions = { ...state.adoptions };
642
+ delete adoptions[sessionId];
643
+ return { next: { ...state, adoptions }, result: null };
644
+ }
645
+ const expert = this.requireExpert(state, expertId);
646
+ const revisionId = expert.publishedRevisionId;
647
+ if (revisionId === void 0 || state.revisions[revisionId] === void 0) {
648
+ throw new FeatureError2("experts/not-published", "\u8BE5\u4E13\u5BB6\u8FD8\u6CA1\u6709\u53D1\u5E03\u4FEE\u8BA2\uFF0C\u65E0\u6CD5\u91C7\u7528\u3002");
649
+ }
650
+ const adoption = {
651
+ sessionId,
652
+ expertId,
653
+ revisionId,
654
+ adoptedAt: (/* @__PURE__ */ new Date()).toISOString()
655
+ };
656
+ return {
657
+ next: { ...state, adoptions: { ...state.adoptions, [sessionId]: adoption } },
658
+ result: adoption
659
+ };
660
+ });
661
+ }
662
+ /** The route dispatch table. */
663
+ handlers() {
664
+ return {
665
+ list: (payload) => this.list(typeof payload.query === "string" ? payload.query : ""),
666
+ get: (payload) => this.get(requireString(payload.expertId, "expertId")),
667
+ create: (payload) => this.create(payload),
668
+ "update-draft": (payload) => this.updateDraft(
669
+ requireString(payload.expertId, "expertId"),
670
+ payload.patch ?? {},
671
+ requireString(payload.expectedRevision, "expectedRevision")
672
+ ),
673
+ publish: (payload) => this.publish(
674
+ requireString(payload.expertId, "expertId"),
675
+ typeof payload.expectedRevision === "string" ? payload.expectedRevision : void 0
676
+ ),
677
+ copy: (payload) => this.copy(
678
+ requireString(payload.expertId, "expertId"),
679
+ typeof payload.revisionId === "string" ? payload.revisionId : void 0
680
+ ),
681
+ delete: (payload) => this.remove(requireString(payload.expertId, "expertId")),
682
+ "set-adoption": (payload) => this.setAdoption(
683
+ requireString(payload.sessionId, "sessionId"),
684
+ typeof payload.expertId === "string" && payload.expertId.length > 0 ? payload.expertId : null
685
+ ),
686
+ adoption: (payload) => this.adoption(requireString(payload.sessionId, "sessionId")),
687
+ "list-teams": (payload) => this.listTeams(typeof payload.query === "string" ? payload.query : ""),
688
+ "get-team": (payload) => this.getTeam(requireString(payload.teamId, "teamId")),
689
+ "create-team": (payload) => this.createTeam(payload),
690
+ "update-team": (payload) => this.updateTeam(
691
+ requireString(payload.teamId, "teamId"),
692
+ payload.patch ?? {},
693
+ typeof payload.expectedRevision === "string" ? payload.expectedRevision : void 0
694
+ ),
695
+ "delete-team": (payload) => this.removeTeam(requireString(payload.teamId, "teamId"))
696
+ };
697
+ }
698
+ // ---- internals -----------------------------------------------------------
699
+ /** In-memory state, loaded by `init`. */
700
+ load() {
701
+ if (this.state === void 0) throw new FeatureError2("experts/not-ready", "\u4E13\u5BB6\u5B58\u50A8\u5C1A\u672A\u5C31\u7EEA\u3002");
702
+ return this.state;
703
+ }
704
+ /** Apply one state transition and persist it. */
705
+ async mutate(transition) {
706
+ const run = async () => {
707
+ const { next, result } = transition(this.load());
708
+ if (next !== this.state) {
709
+ this.state = next;
710
+ await this.states.put(LOCAL_PRINCIPAL, next);
711
+ }
712
+ return result;
713
+ };
714
+ const queued = this.serial.then(run, run);
715
+ this.serial = queued.then(() => void 0, () => void 0);
716
+ return queued;
717
+ }
718
+ /** Read a live expert or refuse. */
719
+ requireExpert(state, expertId) {
720
+ const row = state.experts[expertId];
721
+ if (row === void 0) throw new FeatureError2("experts/not-found", "\u672A\u627E\u5230\u8BE5\u4E13\u5BB6\u3002");
722
+ return row;
723
+ }
724
+ /** Read a live team or refuse. */
725
+ requireTeam(state, teamId) {
726
+ const row = state.teams[teamId];
727
+ if (row === void 0) throw new FeatureError2("experts/team-not-found", "\u672A\u627E\u5230\u8BE5\u4E13\u5BB6\u56E2\u3002");
728
+ return row;
729
+ }
730
+ /** Read an expert's draft or refuse. */
731
+ requireDraft(state, expertId) {
732
+ const row = state.drafts[expertId];
733
+ if (row === void 0) throw new FeatureError2("experts/not-found", "\u4E13\u5BB6\u8349\u7A3F\u7F3A\u5931\u3002");
734
+ return row;
735
+ }
736
+ /** The next revision number for one expert. */
737
+ nextRevisionNumber(state, expertId) {
738
+ const numbers = Object.values(state.revisions).filter((row) => row.expertId === expertId).map((row) => row.number);
739
+ return numbers.length === 0 ? 1 : Math.max(...numbers) + 1;
740
+ }
741
+ };
742
+
743
+ // src/runtime.js
744
+ function renderExpertContext({ revision }) {
745
+ const lines = [
746
+ `\u4F60\u73B0\u5728\u7684\u89D2\u8272\u662F\u300C${revision.name}\u300D\uFF08\u4E13\u5BB6\u4FEE\u8BA2 ${revision.number}\uFF09\u3002`,
747
+ revision.description,
748
+ "",
749
+ "\u89D2\u8272\u8BF4\u660E\uFF1A",
750
+ revision.role
751
+ ];
752
+ if (Array.isArray(revision.preferredSkills) && revision.preferredSkills.length > 0) {
753
+ lines.push("", `\u4F18\u5148\u4F7F\u7528\u8FD9\u4E9B\u6280\u80FD\uFF1A${revision.preferredSkills.join("\u3001")}\u3002\u4E0D\u53EF\u7528\u65F6\u8BF7\u8BF4\u660E\uFF0C\u4E0D\u8981\u5047\u88C5\u5DF2\u8C03\u7528\u3002`);
754
+ }
755
+ lines.push("", "\u8FD9\u6BB5\u89D2\u8272\u8BF4\u660E\u7531\u7528\u6237\u914D\u7F6E\u7684\u4E13\u5BB6\u63D0\u4F9B\uFF0C\u4E0D\u662F\u65B0\u7684\u6388\u6743\uFF1B\u8BF7\u59CB\u7EC8\u4EE5\u8BE5\u8EAB\u4EFD\u56DE\u5E94\uFF0C\u8D85\u51FA\u80FD\u529B\u8FB9\u754C\u65F6\u660E\u786E\u8BF4\u660E\u3002");
756
+ return lines.join("\n");
757
+ }
758
+ function registerExpertPromptContext(ctx, manager) {
759
+ if (ctx.systemPrompt === void 0) return { sync: () => {
760
+ } };
761
+ const disposers = /* @__PURE__ */ new Map();
762
+ const release = (agentId) => {
763
+ const dispose = disposers.get(agentId);
764
+ if (dispose === void 0) return;
765
+ disposers.delete(agentId);
766
+ try {
767
+ dispose();
768
+ } catch {
769
+ }
770
+ };
771
+ const sync = (agentId, agent) => {
772
+ if (typeof agentId !== "string" || agentId.length === 0) return;
773
+ try {
774
+ const selected = manager.adoptionContext(agentId);
775
+ if (selected === void 0) {
776
+ release(agentId);
777
+ return;
778
+ }
779
+ const target = agent ?? (ctx.get?.("agents") ?? ctx.agents)?.get?.(agentId);
780
+ const registry = target?.ctx?.systemPrompt;
781
+ if (registry === void 0 || typeof registry.context !== "function") return;
782
+ release(agentId);
783
+ const dispose = registry.context({
784
+ name: "dsh-skills:expert-role",
785
+ order: EXPERT_CONTEXT_ORDER,
786
+ text: renderExpertContext(selected)
787
+ });
788
+ disposers.set(agentId, dispose);
789
+ } catch (error) {
790
+ ctx.logger?.warn?.(`dsh-plugin-experts: \u65E0\u6CD5\u6CE8\u5165\u4E13\u5BB6\u89D2\u8272\uFF08${String(error?.message ?? error)}\uFF09`);
791
+ }
792
+ };
793
+ ctx.on("agent/created", ({ agent }) => {
794
+ const agentId = String(agent?.id ?? "");
795
+ if (agentId.length === 0) return;
796
+ sync(agentId, agent);
797
+ });
798
+ ctx.on("agent/disposed", ({ agent }) => {
799
+ release(String(agent?.id ?? ""));
800
+ });
801
+ ctx.effect(() => () => {
802
+ for (const agentId of [...disposers.keys()]) release(agentId);
803
+ }, "dsh-plugin-experts: prompt contexts");
804
+ return {
805
+ /** Re-read the adoption for one conversation after a state change. */
806
+ sync: (sessionId) => sync(String(sessionId ?? ""), void 0)
807
+ };
808
+ }
809
+ var EXPERT_CONTEXT_ORDER = 126;
810
+
811
+ // src/skill.js
812
+ import { readFileSync } from "node:fs";
813
+ import { dirname, join as join2 } from "node:path";
814
+ import { fileURLToPath } from "node:url";
815
+
816
+ // ../skills/src/frontmatter.js
817
+ var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
818
+ var MAX_DOCUMENT_BYTES = 1024 * 1024;
819
+ function isSkillName(name2) {
820
+ return typeof name2 === "string" && SKILL_NAME.test(name2);
821
+ }
822
+ function scalar(text) {
823
+ if (text.startsWith('"')) {
824
+ try {
825
+ const parsed = JSON.parse(text);
826
+ if (typeof parsed === "string") return parsed;
827
+ } catch {
828
+ }
829
+ const closing = text.lastIndexOf('"');
830
+ return closing > 0 ? text.slice(1, closing) : text.slice(1);
831
+ }
832
+ if (text.startsWith("'")) {
833
+ const match = /^'((?:[^']|'')*)'/.exec(text);
834
+ if (match !== null) return match[1].replace(/''/g, "'");
835
+ return text.slice(1);
836
+ }
837
+ if (text === "true") return true;
838
+ if (text === "false") return false;
839
+ return text.split(" #")[0].trim();
840
+ }
841
+ function parseTopLevel(yaml) {
842
+ const data = {};
843
+ const lines = yaml.split("\n").map((line) => line.replace(/\r$/, ""));
844
+ for (let index = 0; index < lines.length; index += 1) {
845
+ const line = lines[index];
846
+ const trimmed = line.trim();
847
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
848
+ const indent = line.length - line.trimStart().length;
849
+ if (indent !== 0) continue;
850
+ const separator = line.indexOf(":");
851
+ if (separator <= 0) continue;
852
+ const key = line.slice(0, separator).trim();
853
+ if (key.length === 0) continue;
854
+ const rest = line.slice(separator + 1).trim();
855
+ const block = /^([|>])([+-]?)(\d*)$/.exec(rest);
856
+ if (block !== null) {
857
+ const collected = [];
858
+ let contentIndent = -1;
859
+ let cursor = index + 1;
860
+ for (; cursor < lines.length; cursor += 1) {
861
+ const raw = lines[cursor];
862
+ if (raw.trim().length === 0) {
863
+ collected.push("");
864
+ continue;
865
+ }
866
+ const inner = raw.length - raw.trimStart().length;
867
+ if (inner <= indent) break;
868
+ if (contentIndent < 0) contentIndent = inner;
869
+ collected.push(raw.slice(contentIndent));
870
+ }
871
+ const keepTrailing = block[2] === "+";
872
+ while (!keepTrailing && collected.length > 0 && collected[collected.length - 1] === "") collected.pop();
873
+ const text = collected.join("\n");
874
+ data[key] = block[1] === ">" ? text.split(/\n{2,}/).map((paragraph) => paragraph.split("\n").map((part) => part.trim()).filter((part) => part.length > 0).join(" ")).filter((paragraph) => paragraph.length > 0).join("\n\n") : text;
875
+ index = cursor - 1;
876
+ continue;
877
+ }
878
+ if (rest.length === 0) continue;
879
+ data[key] = scalar(rest);
880
+ }
881
+ return data;
882
+ }
883
+ function readFrontmatter(document) {
884
+ const text = document.charCodeAt(0) === 65279 ? document.slice(1) : document;
885
+ const firstBreak = text.indexOf("\n");
886
+ if (firstBreak < 0) return void 0;
887
+ if (text.slice(0, firstBreak).replace(/\r$/, "") !== "---") return void 0;
888
+ let cursor = firstBreak + 1;
889
+ while (cursor <= text.length) {
890
+ const next = text.indexOf("\n", cursor);
891
+ const end = next < 0 ? text.length : next;
892
+ if (text.slice(cursor, end).replace(/\r$/, "") === "---") {
893
+ return {
894
+ data: parseTopLevel(text.slice(firstBreak + 1, cursor)),
895
+ body: text.slice(next < 0 ? text.length : next + 1)
896
+ };
897
+ }
898
+ if (next < 0) break;
899
+ cursor = next + 1;
900
+ }
901
+ return void 0;
902
+ }
903
+
904
+ // src/skill.js
905
+ var SKILL_DIRECTORY = fileURLToPath(new URL("../resources/skills/expert-manager/", import.meta.url));
906
+ var SKILL_FILE = join2(SKILL_DIRECTORY, "SKILL.md");
907
+ var SKILL_PROVIDER = "dsh-plugin-experts";
908
+ function loadExpertManagerSkill() {
909
+ let document;
910
+ try {
911
+ document = readFileSync(SKILL_FILE, "utf8");
912
+ } catch (error) {
913
+ throw new Error(`experts/skill-missing: cannot read ${SKILL_FILE} (${error?.message ?? error})`);
914
+ }
915
+ const parsed = readFrontmatter(document);
916
+ if (parsed === void 0) {
917
+ throw new Error(`experts/skill-invalid-frontmatter: ${SKILL_FILE} must start with a --- delimited header`);
918
+ }
919
+ const { name: name2, description, "when-to-use": whenToUse } = parsed.data;
920
+ if (typeof name2 !== "string" || !isSkillName(name2)) {
921
+ throw new Error(`experts/skill-invalid-name: frontmatter "name" must be a kebab-case skill name in ${SKILL_FILE}`);
922
+ }
923
+ for (const [key, value] of [["description", description], ["when-to-use", whenToUse]]) {
924
+ if (typeof value !== "string" || value.trim().length === 0) {
925
+ throw new Error(`experts/skill-invalid-frontmatter: frontmatter "${key}" must be a non-empty string in ${SKILL_FILE}`);
926
+ }
927
+ }
928
+ const content = parsed.body.trim();
929
+ if (content.length === 0) {
930
+ throw new Error(`experts/skill-empty-body: no instructions after the frontmatter in ${SKILL_FILE}`);
931
+ }
932
+ return { name: name2, description: description.trim(), whenToUse: whenToUse.trim(), content };
933
+ }
934
+ function registerExpertManagerSkill(ctx) {
935
+ const skill = loadExpertManagerSkill();
936
+ const registry = ctx.skills;
937
+ if (registry === void 0 || typeof registry.register !== "function") return { registered: false, name: skill.name };
938
+ ctx.effect(() => registry.register({
939
+ name: skill.name,
940
+ description: skill.description,
941
+ whenToUse: skill.whenToUse,
942
+ source: "bundled",
943
+ provider: SKILL_PROVIDER,
944
+ content: skill.content,
945
+ // The body cites `references/...` and `runtime/...` by relative path; the
946
+ // registry renders this directory into `<skill_resources>` so the model can
947
+ // resolve them against a real absolute path.
948
+ resourceBase: { kind: "directory", path: dirname(SKILL_FILE) }
949
+ }), "dsh-plugin-experts: skill expert-manager");
950
+ return { registered: true, name: skill.name };
951
+ }
952
+
953
+ // ../library/src/tools.js
954
+ function compileValue(node) {
955
+ if (node === null || typeof node !== "object") throw new Error("dsh-plugin-library: schema node must be an object");
956
+ if (node.type === "object") {
957
+ const properties = {};
958
+ const required = [];
959
+ for (const [key, value] of Object.entries(node.properties ?? {})) {
960
+ if (value?.required === true) required.push(key);
961
+ properties[key] = compileValue(value);
962
+ }
963
+ return {
964
+ type: "object",
965
+ additionalProperties: node.additionalProperties === true,
966
+ properties,
967
+ ...required.length > 0 ? { required } : {},
968
+ ...node.description !== void 0 ? { description: node.description } : {}
969
+ };
970
+ }
971
+ if (node.type === "array") {
972
+ return {
973
+ type: "array",
974
+ ...node.items !== void 0 ? { items: compileValue(node.items) } : {},
975
+ ...node.description !== void 0 ? { description: node.description } : {}
976
+ };
977
+ }
978
+ return {
979
+ type: node.type,
980
+ ...node.description !== void 0 ? { description: node.description } : {},
981
+ ...node.enum !== void 0 ? { enum: [...node.enum] } : {}
982
+ };
983
+ }
984
+ function compileParameters(spec) {
985
+ const properties = {};
986
+ const required = [];
987
+ for (const [key, value] of Object.entries(spec ?? {})) {
988
+ if (value?.required === true) required.push(key);
989
+ properties[key] = compileValue(value);
990
+ }
991
+ return { type: "object", properties, ...required.length > 0 ? { required } : {} };
992
+ }
993
+ function fallbackDefineTool(options) {
994
+ return {
995
+ name: options.name,
996
+ description: options.description,
997
+ parameters: compileParameters(options.parameters),
998
+ output: { schema: compileValue(options.output.schema), render: options.output.render },
999
+ execute: options.execute
1000
+ };
1001
+ }
1002
+ var cached2;
1003
+ async function loadDefineTool() {
1004
+ if (cached2 !== void 0) return cached2;
1005
+ try {
1006
+ const mod = await importDshPackage("@deepseek-ai/dsh-tools");
1007
+ if (typeof mod?.defineTool === "function") {
1008
+ cached2 = mod.defineTool;
1009
+ return cached2;
1010
+ }
1011
+ } catch {
1012
+ }
1013
+ cached2 = fallbackDefineTool;
1014
+ return cached2;
1015
+ }
1016
+
1017
+ // src/tools.js
1018
+ async function guard(run) {
1019
+ try {
1020
+ return await run();
1021
+ } catch (error) {
1022
+ if (error instanceof FeatureError2) {
1023
+ throw new FeatureError2(error.code, error.message);
1024
+ }
1025
+ const code = typeof error?.code === "string" && error.code.length > 0 && error.code !== "ERR_ASSERTION" ? error.code : "experts/tool-failed";
1026
+ throw new FeatureError2(code, String(error?.message ?? error));
1027
+ }
1028
+ }
1029
+ function listRow(row) {
1030
+ return {
1031
+ expert_id: row.id,
1032
+ name: row.name,
1033
+ description: row.description,
1034
+ preferred_skills: [...row.preferredSkills],
1035
+ published: row.published,
1036
+ revision_number: row.revisionNumber,
1037
+ draft_revision: row.draftRevision,
1038
+ updated_at: row.updatedAt
1039
+ };
1040
+ }
1041
+ function draftFields(draft) {
1042
+ if (draft === null || draft === void 0) {
1043
+ return { name: "", description: "", role: "", preferred_skills: [] };
1044
+ }
1045
+ return {
1046
+ name: draft.name,
1047
+ description: draft.description,
1048
+ role: draft.role,
1049
+ preferred_skills: [...draft.preferredSkills ?? []]
1050
+ };
1051
+ }
1052
+ function detailForModel(detail) {
1053
+ const fields = draftFields(detail.draft);
1054
+ const published = detail.publishedRevision;
1055
+ return {
1056
+ expert_id: detail.expert.id,
1057
+ name: fields.name,
1058
+ description: fields.description,
1059
+ role: fields.role,
1060
+ preferred_skills: fields.preferred_skills,
1061
+ draft_revision: detail.expert.draftRevision,
1062
+ published: published !== null,
1063
+ revision_number: published?.number ?? 0,
1064
+ ...published !== null ? { published_revision_id: published.id } : {},
1065
+ updated_at: detail.expert.updatedAt
1066
+ };
1067
+ }
1068
+ var authoredParameters = {
1069
+ name: { type: "string", description: "\u4E13\u5BB6\u540D\u79F0\uFF0C1\u2013120 \u5B57\u7B26\uFF1B\u8FD9\u662F\u5217\u8868\u91CC\u663E\u793A\u7684\u540D\u5B57\u3002" },
1070
+ description: { type: "string", description: "\u4E00\u53E5\u8BDD\u63CF\u8FF0\u8FD9\u4F4D\u4E13\u5BB6\u89E3\u51B3\u4EC0\u4E48\u95EE\u9898\uFF0C\u6700\u591A 2000 \u5B57\u7B26\u3002" },
1071
+ role: { type: "string", description: "\u89D2\u8272\u8BF4\u660E\u6B63\u6587\uFF1A\u8FD9\u4F4D\u4E13\u5BB6\u7684\u80FD\u529B\u3001\u5224\u65AD\u6846\u67B6\u3001\u5DE5\u4F5C\u65B9\u5F0F\u4E0E\u8FB9\u754C\u3002\u8FD9\u6BB5\u6587\u5B57\u4F1A\u539F\u6837\u8FDB\u5165\u91C7\u7528\u5B83\u7684\u4F1A\u8BDD\u7684\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u5199\u5B8C\u6574\u7684\u6307\u4EE4\u6587\u672C\u800C\u4E0D\u662F\u6458\u8981\u3002" },
1072
+ preferred_skills: {
1073
+ type: "array",
1074
+ description: "\u53EF\u9009\uFF1A\u9996\u9009\u6280\u80FD\u540D\u5217\u8868\uFF08\u6700\u591A 12 \u4E2A\uFF0C\u6BCF\u4E2A\u4E0D\u8D85\u8FC7 128 \u5B57\u7B26\uFF09\u3002\u53EA\u662F\u7ED9\u6A21\u578B\u7684\u63D0\u793A\uFF0C\u6280\u80FD\u4E0D\u5B58\u5728\u65F6\u4E0D\u62A5\u9519\uFF0C\u4F46\u4E0D\u8981\u5728\u7528\u6237\u6CA1\u63D0\u4F9B\u65F6\u51ED\u7A7A\u7F16\u9020\u540D\u79F0\u3002",
1075
+ items: { type: "string" }
1076
+ }
1077
+ };
1078
+ var expertProperties = {
1079
+ expert_id: { type: "string", required: true },
1080
+ name: { type: "string", required: true },
1081
+ description: { type: "string", required: true },
1082
+ role: { type: "string", required: true },
1083
+ preferred_skills: { type: "array", required: true, items: { type: "string" } },
1084
+ draft_revision: { type: "string", required: true, description: "\u8349\u7A3F\u7684\u5E76\u53D1\u4EE4\u724C\uFF1B\u4E0B\u6B21 expert_update \u4F20\u8FD9\u4E2A\u503C\u4F5C\u4E3A expected_revision\u3002" },
1085
+ published: { type: "boolean", required: true },
1086
+ revision_number: { type: "integer", required: true, description: "\u5DF2\u53D1\u5E03\u4FEE\u8BA2\u7684\u7F16\u53F7\uFF1B0 \u8868\u793A\u8FD8\u6CA1\u6709\u53D1\u5E03\u8FC7\u3002" },
1087
+ published_revision_id: { type: "string", description: "\u5DF2\u53D1\u5E03\u4FEE\u8BA2 id\uFF1B\u4EC5\u5F53 published \u4E3A true \u65F6\u5B58\u5728\u3002" },
1088
+ updated_at: { type: "string", required: true }
1089
+ };
1090
+ function renderExpert(value) {
1091
+ return [
1092
+ `${value.name}\uFF08expert_id=${value.expert_id}\uFF09`,
1093
+ value.description,
1094
+ value.published ? `\u5DF2\u53D1\u5E03\u4FEE\u8BA2 ${value.revision_number}\uFF08${value.published_revision_id}\uFF09` : "\u5C1A\u672A\u53D1\u5E03",
1095
+ `\u8349\u7A3F\u4EE4\u724C ${value.draft_revision}`,
1096
+ value.preferred_skills.length > 0 ? `\u9996\u9009\u6280\u80FD\uFF1A${value.preferred_skills.join("\u3001")}` : "\u672A\u58F0\u660E\u9996\u9009\u6280\u80FD"
1097
+ ].join("\n");
1098
+ }
1099
+ async function registerExpertTools(ctx, manager) {
1100
+ const defineTool = await loadDefineTool();
1101
+ ctx.effect(() => ctx.tools.register(defineTool({
1102
+ name: "expert_list",
1103
+ description: "\u5217\u51FA\u672C\u673A\u5DF2\u6709\u7684\u4E13\u5BB6\uFF08\u89D2\u8272\u8BBE\u5B9A\uFF09\uFF0C\u53EF\u7528\u5173\u952E\u8BCD\u6309\u540D\u79F0\u3001\u63CF\u8FF0\u548C\u9996\u9009\u6280\u80FD\u8FC7\u6EE4\u3002\u8981\u4FEE\u6539\u67D0\u4F4D\u4E13\u5BB6\u524D\u5148\u7528\u5B83\u627E\u5230 expert_id\uFF1B\u53EA\u8BFB\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u5185\u5BB9\u3002",
1104
+ parameters: {
1105
+ query: { type: "string", description: "\u53EF\u9009\u5173\u952E\u8BCD\uFF0C\u6309\u540D\u79F0\u3001\u63CF\u8FF0\u548C\u9996\u9009\u6280\u80FD\u505A\u4E0D\u533A\u5206\u5927\u5C0F\u5199\u7684\u5339\u914D\uFF1B\u7701\u7565\u5219\u8FD4\u56DE\u5168\u90E8\u3002" }
1106
+ },
1107
+ output: {
1108
+ schema: {
1109
+ type: "object",
1110
+ additionalProperties: false,
1111
+ properties: {
1112
+ experts: {
1113
+ type: "array",
1114
+ required: true,
1115
+ items: {
1116
+ type: "object",
1117
+ additionalProperties: false,
1118
+ properties: {
1119
+ expert_id: { type: "string", required: true },
1120
+ name: { type: "string", required: true },
1121
+ description: { type: "string", required: true },
1122
+ preferred_skills: { type: "array", required: true, items: { type: "string" } },
1123
+ published: { type: "boolean", required: true },
1124
+ revision_number: { type: "integer", required: true },
1125
+ draft_revision: { type: "string", required: true },
1126
+ updated_at: { type: "string", required: true }
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ },
1132
+ render: (_args, value) => [{
1133
+ type: "text",
1134
+ text: value.experts.length > 0 ? value.experts.map((row) => `${row.name}\uFF08expert_id=${row.expert_id}${row.published ? ` \xB7 \u5DF2\u53D1\u5E03\u4FEE\u8BA2 ${row.revision_number}` : " \xB7 \u672A\u53D1\u5E03"}\uFF09
1135
+ ${row.description}`).join("\n\n") : "\u672C\u673A\u8FD8\u6CA1\u6709\u4E13\u5BB6\u3002\u7528 expert_create \u521B\u5EFA\u7B2C\u4E00\u4F4D\u3002"
1136
+ }]
1137
+ },
1138
+ async execute(args) {
1139
+ const rows = await guard(() => manager.list(typeof args.query === "string" ? args.query : ""));
1140
+ return { experts: rows.map(listRow) };
1141
+ }
1142
+ })), "dsh-plugin-experts: tool expert_list");
1143
+ ctx.effect(() => ctx.tools.register(defineTool({
1144
+ name: "expert_get",
1145
+ description: "\u8BFB\u53D6\u4E00\u4F4D\u4E13\u5BB6\u7684\u5B8C\u6574\u5185\u5BB9\uFF1A\u540D\u79F0\u3001\u63CF\u8FF0\u3001\u89D2\u8272\u8BF4\u660E\u3001\u9996\u9009\u6280\u80FD\u3001\u8349\u7A3F\u5E76\u53D1\u4EE4\u724C\u548C\u5DF2\u53D1\u5E03\u4FEE\u8BA2\u3002\u4FEE\u6539\u524D\u5148\u8C03\u7528\u5B83\u62FF\u6700\u65B0\u7684 draft_revision\u3002\u53EA\u8BFB\u3002",
1146
+ parameters: {
1147
+ expert_id: { type: "string", required: true, description: "expert_list \u8FD4\u56DE\u7684 expert_id\u3002" }
1148
+ },
1149
+ output: {
1150
+ schema: {
1151
+ type: "object",
1152
+ additionalProperties: false,
1153
+ properties: {
1154
+ expert_id: { type: "string", required: true },
1155
+ name: { type: "string", required: true, description: "\u4EE5\u4E0B\u5B57\u6BB5\u662F**\u5F53\u524D\u8349\u7A3F**\u7684\u5185\u5BB9\uFF0C\u4E5F\u5C31\u662F\u6B63\u5728\u7F16\u8F91\u7684\u7248\u672C\u3002" },
1156
+ description: { type: "string", required: true },
1157
+ role: { type: "string", required: true },
1158
+ preferred_skills: { type: "array", required: true, items: { type: "string" } },
1159
+ draft_revision: { type: "string", required: true },
1160
+ published: { type: "boolean", required: true },
1161
+ revision_number: { type: "integer", required: true },
1162
+ published_revision_id: { type: "string" },
1163
+ updated_at: { type: "string", required: true }
1164
+ }
1165
+ },
1166
+ render: (_args, value) => [{
1167
+ type: "text",
1168
+ text: `${renderExpert(value)}
1169
+
1170
+ \u89D2\u8272\u8BF4\u660E\uFF1A
1171
+ ${value.role}`
1172
+ }]
1173
+ },
1174
+ async execute(args) {
1175
+ const detail = await guard(() => manager.get(args.expert_id));
1176
+ return detailForModel(detail);
1177
+ }
1178
+ })), "dsh-plugin-experts: tool expert_get");
1179
+ ctx.effect(() => ctx.tools.register(defineTool({
1180
+ name: "expert_create",
1181
+ description: "\u521B\u5EFA\u4E00\u4F4D\u65B0\u7684\u4E13\u5BB6\uFF08\u53EF\u590D\u7528\u7684\u89D2\u8272\u8BBE\u5B9A\uFF09\u3002\u8FD9\u662F**\u53EA\u5199\u8349\u7A3F**\uFF1A\u521B\u5EFA\u540E\u4E13\u5BB6\u5904\u4E8E\u672A\u53D1\u5E03\u72B6\u6001\uFF0C\u7528\u6237\u91C7\u7528\u524D\u8FD8\u9700\u8981 expert_publish\u3002\u89D2\u8272\u8BF4\u660E\u8981\u5199\u5B8C\u6574\u7684\u6307\u4EE4\u6B63\u6587\uFF0C\u4E0D\u8981\u53EA\u5199\u6458\u8981\uFF1B\u5B57\u6BB5\u7F3A\u5931\u6216\u8D85\u957F\u4F1A\u88AB\u62D2\u7EDD\u3002",
1182
+ parameters: { ...authoredParameters, name: { ...authoredParameters.name, required: true }, description: { ...authoredParameters.description, required: true }, role: { ...authoredParameters.role, required: true } },
1183
+ output: {
1184
+ schema: { type: "object", additionalProperties: false, properties: expertProperties },
1185
+ render: (_args, value) => [{ type: "text", text: `\u5DF2\u521B\u5EFA\u8349\u7A3F\uFF1A
1186
+ ${renderExpert(value)}
1187
+
1188
+ \u8BE5\u4E13\u5BB6\u5C1A\u672A\u53D1\u5E03\uFF1B\u786E\u8BA4\u5185\u5BB9\u65E0\u8BEF\u540E\u8C03\u7528 expert_publish\u3002` }]
1189
+ },
1190
+ async execute(args) {
1191
+ const detail = await guard(() => manager.create({
1192
+ name: args.name,
1193
+ description: args.description,
1194
+ role: args.role,
1195
+ preferredSkills: args.preferred_skills
1196
+ }));
1197
+ return detailForModel(detail);
1198
+ }
1199
+ })), "dsh-plugin-experts: tool expert_create");
1200
+ ctx.effect(() => ctx.tools.register(defineTool({
1201
+ name: "expert_update",
1202
+ description: "\u4FEE\u6539\u4E00\u4F4D\u4E13\u5BB6\u7684**\u8349\u7A3F**\u3002\u53EA\u4F20\u8981\u6539\u7684\u5B57\u6BB5\uFF0C\u672A\u63D0\u4F9B\u7684\u5B57\u6BB5\u4FDD\u6301\u4E0D\u53D8\u3002\u5FC5\u987B\u4F20 expected_revision\uFF08\u6765\u81EA expert_get / expert_create / \u4E0A\u4E00\u6B21 expert_update \u7684 draft_revision\uFF09\uFF0C\u8349\u7A3F\u5DF2\u88AB\u6539\u52A8\u65F6\u4F1A\u62D2\u7EDD\u5E76\u8981\u6C42\u91CD\u65B0\u8BFB\u53D6\uFF0C\u4E0D\u4F1A\u8986\u76D6\u4ED6\u4EBA\u7684\u4FEE\u6539\u3002\u7EDD\u4E0D\u6539\u52A8\u5DF2\u53D1\u5E03\u7684\u4FEE\u8BA2\u3002",
1203
+ parameters: {
1204
+ expert_id: { type: "string", required: true, description: "expert_list \u8FD4\u56DE\u7684 expert_id\u3002" },
1205
+ expected_revision: { type: "string", required: true, description: "\u5E76\u53D1\u4EE4\u724C\uFF1Aexpert_get \u8FD4\u56DE\u7684 draft_revision\uFF0C\u539F\u6837\u4F20\u5165\u3002" },
1206
+ name: { ...authoredParameters.name, description: "\u53EF\u9009\uFF1A\u65B0\u7684\u540D\u79F0\uFF1B\u7701\u7565\u5219\u4FDD\u6301\u4E0D\u53D8\u3002" },
1207
+ description: { ...authoredParameters.description, description: "\u53EF\u9009\uFF1A\u65B0\u7684\u63CF\u8FF0\uFF1B\u7701\u7565\u5219\u4FDD\u6301\u4E0D\u53D8\u3002" },
1208
+ role: { ...authoredParameters.role, description: "\u53EF\u9009\uFF1A\u65B0\u7684\u89D2\u8272\u8BF4\u660E\u6B63\u6587\uFF1B\u7701\u7565\u5219\u4FDD\u6301\u4E0D\u53D8\u3002" },
1209
+ preferred_skills: { ...authoredParameters.preferred_skills, description: "\u53EF\u9009\uFF1A\u66FF\u6362\u6574\u4E2A\u9996\u9009\u6280\u80FD\u5217\u8868\uFF1B\u7701\u7565\u5219\u4FDD\u6301\u4E0D\u53D8\u3002" }
1210
+ },
1211
+ output: {
1212
+ schema: { type: "object", additionalProperties: false, properties: expertProperties },
1213
+ render: (_args, value) => [{ type: "text", text: `\u8349\u7A3F\u5DF2\u66F4\u65B0\uFF1A
1214
+ ${renderExpert(value)}` }]
1215
+ },
1216
+ async execute(args) {
1217
+ const patch = {};
1218
+ if (args.name !== void 0) patch.name = args.name;
1219
+ if (args.description !== void 0) patch.description = args.description;
1220
+ if (args.role !== void 0) patch.role = args.role;
1221
+ if (args.preferred_skills !== void 0) patch.preferredSkills = args.preferred_skills;
1222
+ const detail = await guard(() => manager.updateDraft(args.expert_id, patch, args.expected_revision));
1223
+ return detailForModel(detail);
1224
+ }
1225
+ })), "dsh-plugin-experts: tool expert_update");
1226
+ ctx.effect(() => ctx.tools.register(defineTool({
1227
+ name: "expert_publish",
1228
+ description: "\u628A\u5F53\u524D\u8349\u7A3F\u51BB\u7ED3\u6210\u4E00\u4E2A**\u65B0\u7684\u3001\u4E0D\u53EF\u53D8\u7684\u3001\u5E26\u7F16\u53F7\u7684**\u5DF2\u53D1\u5E03\u4FEE\u8BA2\uFF0C\u5E76\u8BA9\u4E13\u5BB6\u6307\u5411\u5B83\u3002\u8FD9\u662F\u4E0D\u53EF\u64A4\u9500\u7684\u4E00\u6B65\uFF1A\u65E7\u4FEE\u8BA2\u548C\u5DF2\u7ECF\u91C7\u7528\u8BE5\u4E13\u5BB6\u7684\u4F1A\u8BDD\u90FD\u4E0D\u53D8\uFF0C\u4F46\u7528\u6237\u4ECE\u6B64\u91C7\u7528\u5230\u7684\u5C31\u662F\u8FD9\u4E00\u7248\u3002\u8C03\u7528\u524D\u5E94\u786E\u8BA4\u7528\u6237\u5DF2\u770B\u8FC7\u8349\u7A3F\u5185\u5BB9\u3002",
1229
+ parameters: {
1230
+ expert_id: { type: "string", required: true, description: "expert_list \u8FD4\u56DE\u7684 expert_id\u3002" },
1231
+ expected_revision: { type: "string", description: "\u53EF\u9009\u5E76\u53D1\u4EE4\u724C\uFF1A\u8349\u7A3F\u7684 draft_revision\u3002\u4F20\u5165\u65F6\u8349\u7A3F\u4E0E\u4E4B\u4E0D\u7B26\u5C31\u62D2\u7EDD\uFF1B\u7701\u7565\u5219\u76F4\u63A5\u53D1\u5E03\u5F53\u524D\u8349\u7A3F\u3002" }
1232
+ },
1233
+ output: {
1234
+ schema: {
1235
+ type: "object",
1236
+ additionalProperties: false,
1237
+ properties: {
1238
+ expert_id: { type: "string", required: true },
1239
+ revision_id: { type: "string", required: true, description: "\u65B0\u51BB\u7ED3\u7684\u4FEE\u8BA2 id\u3002" },
1240
+ revision_number: { type: "integer", required: true },
1241
+ name: { type: "string", required: true },
1242
+ published: { type: "boolean", required: true },
1243
+ draft_revision: { type: "string", required: true, description: "\u53D1\u5E03\u4E0D\u6539\u53D8\u8349\u7A3F\u4EE4\u724C\u3002" }
1244
+ }
1245
+ },
1246
+ render: (_args, value) => [{ type: "text", text: `\u5DF2\u53D1\u5E03\u300C${value.name}\u300D\u4FEE\u8BA2 ${value.revision_number}\uFF08revision_id=${value.revision_id}\uFF09\u3002\u7528\u6237\u73B0\u5728\u53EF\u4EE5\u91C7\u7528\u8FD9\u4F4D\u4E13\u5BB6\u3002` }]
1247
+ },
1248
+ async execute(args) {
1249
+ const result = await guard(() => manager.publish(args.expert_id, args.expected_revision));
1250
+ return {
1251
+ expert_id: result.expert.id,
1252
+ revision_id: result.revision.id,
1253
+ revision_number: result.revision.number,
1254
+ name: result.revision.name,
1255
+ published: true,
1256
+ draft_revision: result.expert.draftRevision
1257
+ };
1258
+ }
1259
+ })), "dsh-plugin-experts: tool expert_publish");
1260
+ }
1261
+
1262
+ // index.js
1263
+ var name = "dsh-plugin-experts";
1264
+ var inject = ["connection", "storageDomain", "systemPrompt", "tools"];
1265
+ async function apply(ctx) {
1266
+ registerExpertManagerSkillForLife(ctx);
1267
+ const kit = await storageKit();
1268
+ const manager = new ExpertManager(ctx, buildExpertDomains(kit));
1269
+ await manager.init();
1270
+ const runtime = registerExpertPromptContext(ctx, manager);
1271
+ const handlers = manager.handlers();
1272
+ const setAdoption = handlers["set-adoption"];
1273
+ handlers["set-adoption"] = async (payload) => {
1274
+ const result = await setAdoption(payload);
1275
+ runtime.sync(payload?.sessionId);
1276
+ return result;
1277
+ };
1278
+ const remove = handlers.delete;
1279
+ handlers.delete = async (payload) => {
1280
+ const result = await remove(payload);
1281
+ for (const sessionId of result?.releasedSessions ?? []) runtime.sync(sessionId);
1282
+ return result;
1283
+ };
1284
+ registerFeatureRoute(ctx, { feature: "experts", handlers });
1285
+ await registerExpertTools(ctx, manager);
1286
+ ctx.provide("dshSkillsExperts", { list: (query = "") => manager.list(query) });
1287
+ }
1288
+ function registerExpertManagerSkillForLife(ctx) {
1289
+ if (typeof ctx.inject !== "function") return;
1290
+ ctx.inject(["skills"], (scope) => {
1291
+ registerExpertManagerSkill(scope);
1292
+ });
1293
+ }
1294
+ export {
1295
+ apply,
1296
+ inject,
1297
+ name
1298
+ };