dsh-skill-importer 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/lib/index.js CHANGED
@@ -107,17 +107,29 @@ function dshHomeDir() {
107
107
  /** Absolute skill root for one target under one workspace. */
108
108
  function skillRoot(target, workspacePath) {
109
109
  switch (target) {
110
- case "user": return join(dshHomeDir(), "skills");
110
+ case "project-dsh": return join(workspacePath, ".dsh", "skills");
111
111
  case "project-agents": return join(workspacePath, ".agents", "skills");
112
+ case "user-dsh": return join(dshHomeDir(), "skills");
113
+ case "user-agents": return join(process.env.DSH_AGENTS_HOME ?? join(homedir(), ".agents"), "skills");
112
114
  }
113
115
  }
114
116
  /** Discovery rank per root (lower wins), mirroring dsh-skill-filesystem. */
115
117
  const ROOT_RANK = {
118
+ "project-dsh": 100,
116
119
  "project-agents": 200,
117
- user: 400
120
+ "user-dsh": 400,
121
+ "user-agents": 500
118
122
  };
119
- /** Scan order: project roots first so project skills win duplicate names. */
120
- const SCAN_ORDER = ["project-agents", "user"];
123
+ /** Official default-root order (custom rank 300 is not a stable import destination). */
124
+ const SCAN_ORDER = [
125
+ "project-dsh",
126
+ "project-agents",
127
+ "user-dsh",
128
+ "user-agents"
129
+ ];
130
+ function isProjectTarget(target) {
131
+ return target === "project-dsh" || target === "project-agents";
132
+ }
121
133
  /**
122
134
  * Quote one frontmatter string value when plain YAML would mis-parse it:
123
135
  * a value containing `: ` (or any colon), leading/trailing whitespace, a
@@ -163,7 +175,7 @@ function normalizeSkillText(text) {
163
175
  function writeSkillFile(name, target, content, workspacePath) {
164
176
  if (!KEBAB_CASE.test(name)) throw new Error("技能名必须是 kebab-case(小写字母、数字、短横线)");
165
177
  if (Buffer.byteLength(content, "utf8") > 262144) throw new Error(`内容超过 ${MAX_CONTENT_BYTES / 1024} KB`);
166
- if (target !== "user" && workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
178
+ if (isProjectTarget(target) && workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
167
179
  const normalized = normalizeSkillText(content);
168
180
  const directory = join(skillRoot(target, workspacePath ?? ""), name);
169
181
  mkdirSync(directory, { recursive: true });
@@ -174,28 +186,47 @@ function writeSkillFile(name, target, content, workspacePath) {
174
186
  return file;
175
187
  }
176
188
  /** Scan one skill root and fold its skills into the name-keyed map (rank-aware). */
177
- function scanRoot(root, source, out) {
178
- if (!existsSync(root)) return;
179
- for (const entry of readdirSync(root, { withFileTypes: true })) {
180
- const entryPath = join(root, entry.name);
181
- let file;
182
- if (entry.isDirectory()) {
183
- const candidate = join(entryPath, "SKILL.md");
184
- if (existsSync(candidate)) file = candidate;
185
- } else if (entry.isFile() && entry.name.endsWith(".md")) file = entryPath;
186
- if (file === void 0) continue;
187
- const { frontmatter } = parseSkillFile(readFileSync(file, "utf8"));
188
- if (frontmatter.name === void 0 || frontmatter.description === void 0) continue;
189
- if (!isValidSkillName(frontmatter.name)) continue;
190
- const existing = out.get(frontmatter.name);
191
- if (existing !== void 0 && ROOT_RANK[existing.source] <= ROOT_RANK[source]) continue;
192
- out.set(frontmatter.name, {
193
- name: frontmatter.name,
194
- description: frontmatter.description,
195
- ...frontmatter.whenToUse !== void 0 ? { whenToUse: frontmatter.whenToUse } : {},
196
- modelInvocable: frontmatter.disableModelInvocation !== true,
197
- userInvocable: frontmatter.userInvocable !== false,
198
- source
189
+ function scanRoot(root, source, workspacePath, out, issues) {
190
+ try {
191
+ if (!existsSync(root)) return;
192
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
193
+ const entryPath = join(root, entry.name);
194
+ let file;
195
+ if (entry.isDirectory()) {
196
+ const candidate = join(entryPath, "SKILL.md");
197
+ if (existsSync(candidate)) file = candidate;
198
+ } else if (entry.isFile() && entry.name.endsWith(".md")) file = entryPath;
199
+ if (file === void 0) continue;
200
+ try {
201
+ const { frontmatter } = parseSkillFile(readFileSync(file, "utf8"));
202
+ if (frontmatter.name === void 0 || frontmatter.description === void 0) continue;
203
+ if (!isValidSkillName(frontmatter.name)) continue;
204
+ const existing = out.get(frontmatter.name);
205
+ if (existing !== void 0 && ROOT_RANK[existing.source] <= ROOT_RANK[source]) continue;
206
+ out.set(frontmatter.name, {
207
+ name: frontmatter.name,
208
+ description: frontmatter.description,
209
+ ...frontmatter.whenToUse !== void 0 ? { whenToUse: frontmatter.whenToUse } : {},
210
+ modelInvocable: frontmatter.disableModelInvocation !== true,
211
+ userInvocable: frontmatter.userInvocable !== false,
212
+ source,
213
+ ...workspacePath === void 0 ? {} : { workspacePath }
214
+ });
215
+ } catch (error) {
216
+ issues.push({
217
+ source,
218
+ path: file,
219
+ ...workspacePath === void 0 ? {} : { workspacePath },
220
+ message: error instanceof Error ? error.message : String(error)
221
+ });
222
+ }
223
+ }
224
+ } catch (error) {
225
+ issues.push({
226
+ source,
227
+ path: root,
228
+ ...workspacePath === void 0 ? {} : { workspacePath },
229
+ message: error instanceof Error ? error.message : String(error)
199
230
  });
200
231
  }
201
232
  }
@@ -208,12 +239,24 @@ function scanRoot(root, source, out) {
208
239
  */
209
240
  function listSkills(workspacePaths) {
210
241
  const rows = [];
242
+ const issues = [];
211
243
  for (const target of SCAN_ORDER) {
212
- const out = /* @__PURE__ */ new Map();
213
- for (const path of target === "user" ? [dshHomeDir()] : workspacePaths) scanRoot(skillRoot(target, path), target, out);
214
- rows.push(...out.values());
244
+ if (!isProjectTarget(target)) {
245
+ const out = /* @__PURE__ */ new Map();
246
+ scanRoot(skillRoot(target, ""), target, void 0, out, issues);
247
+ rows.push(...out.values());
248
+ continue;
249
+ }
250
+ for (const workspacePath of workspacePaths) {
251
+ const out = /* @__PURE__ */ new Map();
252
+ scanRoot(skillRoot(target, workspacePath), target, workspacePath, out, issues);
253
+ rows.push(...out.values());
254
+ }
215
255
  }
216
- return rows.sort((a, b) => a.name.localeCompare(b.name));
256
+ return {
257
+ skills: rows.sort((a, b) => a.name.localeCompare(b.name)),
258
+ issues
259
+ };
217
260
  }
218
261
  /**
219
262
  * Delete one installed skill (its whole bundle directory `<root>/<name>/`,
@@ -225,7 +268,7 @@ function listSkills(workspacePaths) {
225
268
  */
226
269
  function deleteSkillFile(name, source, workspacePath) {
227
270
  if (!KEBAB_CASE.test(name)) throw new Error("技能名必须是 kebab-case(小写字母、数字、短横线)");
228
- if (source !== "user" && workspacePath === void 0) throw new Error("项目来源需要 workspacePath(当前工作区路径)");
271
+ if (isProjectTarget(source) && workspacePath === void 0) throw new Error("项目来源需要 workspacePath(当前工作区路径)");
229
272
  const root = skillRoot(source, workspacePath ?? "");
230
273
  const directory = join(root, name);
231
274
  if (existsSync(directory)) {
@@ -308,7 +351,7 @@ function scanBatch(request) {
308
351
  const leaf = basename(sourcePath).toLowerCase();
309
352
  const parent = basename(dirname(sourcePath)).toLowerCase();
310
353
  if (!(leaf === "skills") && !(parent === "skills")) throw new Error("批量导入仅支持名为 skills 的目录或其中的单个技能目录");
311
- if (request.target !== "user" && request.workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
354
+ if (isProjectTarget(request.target) && request.workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
312
355
  const sources = batchSources(sourcePath);
313
356
  if (sources.length === 0) throw new Error("所选目录中没有找到可导入的技能");
314
357
  if (sources.length > 200) throw new Error(`一次最多扫描 200 个技能`);
@@ -621,6 +664,80 @@ function sendError(res, status, error) {
621
664
  });
622
665
  }
623
666
  //#endregion
667
+ //#region src/update.ts
668
+ const REGISTRY_URL = "https://registry.npmjs.org/dsh-skill-importer/latest";
669
+ const CACHE_TTL_MS = 216e5;
670
+ const REQUEST_TIMEOUT_MS = 5e3;
671
+ let cachedLatest;
672
+ function parseVersion(value) {
673
+ const match = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
674
+ if (match === null) return void 0;
675
+ const prerelease = match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part);
676
+ return {
677
+ core: [
678
+ Number(match[1]),
679
+ Number(match[2]),
680
+ Number(match[3])
681
+ ],
682
+ prerelease
683
+ };
684
+ }
685
+ /** SemVer ordering used to avoid offering a downgrade or an older prerelease. */
686
+ function compareVersions(left, right) {
687
+ const a = parseVersion(left);
688
+ const b = parseVersion(right);
689
+ if (a === void 0 || b === void 0) throw new Error("npm 返回了无效版本号");
690
+ for (const index of [
691
+ 0,
692
+ 1,
693
+ 2
694
+ ]) if (a.core[index] !== b.core[index]) return a.core[index] < b.core[index] ? -1 : 1;
695
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
696
+ if (a.prerelease.length === b.prerelease.length) return 0;
697
+ return a.prerelease.length === 0 ? 1 : -1;
698
+ }
699
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
700
+ for (let index = 0; index < length; index += 1) {
701
+ const av = a.prerelease[index];
702
+ const bv = b.prerelease[index];
703
+ if (av === void 0) return -1;
704
+ if (bv === void 0) return 1;
705
+ if (av === bv) continue;
706
+ if (typeof av === "number" && typeof bv === "string") return -1;
707
+ if (typeof av === "string" && typeof bv === "number") return 1;
708
+ return av < bv ? -1 : 1;
709
+ }
710
+ return 0;
711
+ }
712
+ /** Read from package metadata in both src and built lib layouts. */
713
+ function currentVersion() {
714
+ const metadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
715
+ if (typeof metadata.version !== "string" || parseVersion(metadata.version) === void 0) throw new Error("package.json 缺少有效版本号");
716
+ return metadata.version;
717
+ }
718
+ async function latestVersion(now = Date.now()) {
719
+ if (cachedLatest !== void 0 && cachedLatest.expiresAt > now) return cachedLatest.version;
720
+ const response = await fetch(REGISTRY_URL, {
721
+ headers: {
722
+ accept: "application/json",
723
+ "user-agent": "dsh-skill-importer-update-check"
724
+ },
725
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
726
+ });
727
+ if (!response.ok) throw new Error(`npm registry 返回 ${response.status}`);
728
+ const metadata = await response.json();
729
+ if (typeof metadata.version !== "string" || parseVersion(metadata.version) === void 0) throw new Error("npm registry 返回了无效版本信息");
730
+ cachedLatest = {
731
+ version: metadata.version,
732
+ expiresAt: now + CACHE_TTL_MS
733
+ };
734
+ return metadata.version;
735
+ }
736
+ function updateCommand(version) {
737
+ if (parseVersion(version) === void 0) throw new Error("无法为无效版本生成更新命令");
738
+ return `npx @deepseek-ai/dsh plugin --profile web add dsh-skill-importer@${version}`;
739
+ }
740
+ //#endregion
624
741
  //#region src/index.ts
625
742
  /** Route paths this plugin owns (exact matches; the client fetches the same literals). */
626
743
  const ROUTES = {
@@ -630,10 +747,20 @@ const ROUTES = {
630
747
  importUrl: "/skill-importer/import-url",
631
748
  delete: "/skill-importer/delete",
632
749
  batchScan: "/skill-importer/batch/scan",
633
- batchCommit: "/skill-importer/batch/commit"
750
+ batchCommit: "/skill-importer/batch/commit",
751
+ updateStatus: "/skill-importer/update-status"
634
752
  };
635
753
  /** Required services: the harness web server's route registry and the workspace registry. */
636
754
  const inject = ["webServer", "workspaceRegistry"];
755
+ const IMPORT_TARGETS = /* @__PURE__ */ new Set([
756
+ "project-dsh",
757
+ "project-agents",
758
+ "user-dsh",
759
+ "user-agents"
760
+ ]);
761
+ function isImportTarget(value) {
762
+ return typeof value === "string" && IMPORT_TARGETS.has(value);
763
+ }
637
764
  const handle = (fn) => (req, res) => {
638
765
  fn(req, res).catch((error) => {
639
766
  if (res.writableEnded) return;
@@ -660,13 +787,40 @@ function apply(ctx) {
660
787
  {
661
788
  kind: "exact",
662
789
  path: ROUTES.list,
663
- handler: handle(async (_req, res) => {
790
+ handler: handle(async (req, res) => {
791
+ const url = new URL(req.url ?? ROUTES.list, "http://localhost");
792
+ const requested = url.searchParams.get("workspacePath");
664
793
  sendJson(res, 200, {
665
794
  ok: true,
666
- skills: listSkills(projectPaths())
795
+ ...listSkills(url.searchParams.get("scope") === "global" ? [] : requested === null ? projectPaths() : [requireRegisteredWorkspace(requested)])
667
796
  });
668
797
  })
669
798
  },
799
+ {
800
+ kind: "exact",
801
+ path: ROUTES.updateStatus,
802
+ handler: handle(async (_req, res) => {
803
+ const installed = currentVersion();
804
+ try {
805
+ const latest = await latestVersion();
806
+ const updateAvailable = compareVersions(installed, latest) < 0;
807
+ sendJson(res, 200, {
808
+ ok: true,
809
+ currentVersion: installed,
810
+ latestVersion: latest,
811
+ updateAvailable,
812
+ command: updateAvailable ? updateCommand(latest) : void 0
813
+ });
814
+ } catch (error) {
815
+ sendJson(res, 200, {
816
+ ok: true,
817
+ currentVersion: installed,
818
+ updateAvailable: false,
819
+ error: error instanceof Error ? error.message : String(error)
820
+ });
821
+ }
822
+ })
823
+ },
670
824
  {
671
825
  kind: "exact",
672
826
  path: ROUTES.import,
@@ -676,7 +830,7 @@ function apply(ctx) {
676
830
  return;
677
831
  }
678
832
  const body = await readJsonBody(req);
679
- if (typeof body.name !== "string" || typeof body.content !== "string" || body.target !== "user" && body.target !== "project-agents") {
833
+ if (typeof body.name !== "string" || typeof body.content !== "string" || !isImportTarget(body.target)) {
680
834
  sendError(res, 400, "请求缺少 name / content / target 字段");
681
835
  return;
682
836
  }
@@ -701,7 +855,7 @@ function apply(ctx) {
701
855
  return;
702
856
  }
703
857
  const body = await readJsonBody(req);
704
- if (typeof body.name !== "string" || typeof body.url !== "string" || body.target !== "user" && body.target !== "project-agents") {
858
+ if (typeof body.name !== "string" || typeof body.url !== "string" || !isImportTarget(body.target)) {
705
859
  sendError(res, 400, "请求缺少 name / url / target 字段");
706
860
  return;
707
861
  }
@@ -726,7 +880,7 @@ function apply(ctx) {
726
880
  return;
727
881
  }
728
882
  const body = await readJsonBody(req);
729
- if (typeof body.name !== "string" || body.source !== "user" && body.source !== "project-agents") {
883
+ if (typeof body.name !== "string" || !isImportTarget(body.source)) {
730
884
  sendError(res, 400, "请求缺少 name / source 字段");
731
885
  return;
732
886
  }
@@ -746,7 +900,7 @@ function apply(ctx) {
746
900
  return;
747
901
  }
748
902
  const body = await readJsonBody(req);
749
- if (typeof body.sourcePath !== "string" || body.target !== "user" && body.target !== "project-agents") {
903
+ if (typeof body.sourcePath !== "string" || !isImportTarget(body.target)) {
750
904
  sendError(res, 400, "请求缺少 sourcePath / target 字段");
751
905
  return;
752
906
  }
@@ -806,11 +960,15 @@ function apply(ctx) {
806
960
  * from the host process cwd, which may differ from the user's workspace).
807
961
  */
808
962
  function requireWorkspace(target, candidate) {
809
- if (target === "user") return void 0;
963
+ if (!isProjectTarget(target)) return void 0;
810
964
  if (typeof candidate !== "string") throw new Error("项目目标需要 workspacePath(当前工作区路径)");
811
965
  if (!projectPaths().includes(candidate)) throw new Error("workspacePath 不是已注册的工作区");
812
966
  return candidate;
813
967
  }
968
+ function requireRegisteredWorkspace(candidate) {
969
+ if (!projectPaths().includes(candidate)) throw new Error("workspacePath 不是已注册的工作区");
970
+ return candidate;
971
+ }
814
972
  ctx.effect(() => {
815
973
  const disposers = routes.map((route) => ctx.webServer.register(route));
816
974
  return () => {
@@ -13,4 +13,4 @@ import type { SkillImporterInjected } from './index.ts';
13
13
  /** Props the renderer binds for the section. */
14
14
  export type SkillImporterProps = PropsRuntime<'settings.section'> & PropsLocale<'skills.importer'> & InjectFace<SkillImporterInjected>;
15
15
  /** The settings section's page body. */
16
- export declare function SkillImporterSection({ t, useSkills, useWorkspaces, actions }: SkillImporterProps): ReactNode;
16
+ export declare function SkillImporterSection({ t, useSkills, useSkillIssues, useWorkspaces, actions }: SkillImporterProps): ReactNode;
@@ -13,4 +13,4 @@ import type { SkillImporterInjected } from './index.ts';
13
13
  /** Props the renderer binds for the picker. */
14
14
  export type SkillsPickerProps = PropsRuntime<'conversation.input.left'> & PropsLocale<'skills.importer'> & InjectFace<SkillImporterInjected>;
15
15
  /** One row of the composer skill picker. */
16
- export declare function SkillsPicker({ t, sessionId, useSkills, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
16
+ export declare function SkillsPicker({ t, sessionId, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
@@ -0,0 +1,3 @@
1
+ import type { SkillListEntry } from '../types.ts';
2
+ /** Mirror DSH's name-based winner selection for one session-scoped catalog. */
3
+ export declare function effectiveSkills(rows: readonly SkillListEntry[]): readonly SkillListEntry[];
@@ -1,26 +1,32 @@
1
1
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
2
+ import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
2
3
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
3
4
  import type { SkillImporterProps } from './SkillImporterSection.tsx';
4
5
  import type { SkillsPickerProps } from './SkillsPicker.tsx';
5
6
  import { type SkillImporterKey } from './locales.ts';
6
- import type { BatchCommitRequest, BatchCommitResponse, BatchScanRequest, BatchScanResponse, DeleteRequest, ImportRequest, ImportUrlRequest, SkillListResponse } from '../types.ts';
7
+ import type { BatchCommitRequest, BatchCommitResponse, BatchScanRequest, BatchScanResponse, DeleteRequest, ImportRequest, ImportUrlRequest, SkillListResponse, UpdateStatusResponse } from '../types.ts';
7
8
  declare module '@deepseek-ai/dsh-client-ui-slots' {
8
9
  interface LocaleNamespaceMap {
9
10
  /** The skill importer section's copy. */
10
11
  'skills.importer': SkillImporterKey;
11
12
  }
12
13
  }
13
- /** Installed skill catalog snapshot (global — not per-session anymore). */
14
+ /** Installed skill catalog snapshot used by the all-workspace settings view. */
14
15
  export type SkillCatalogSnapshot = SkillListResponse["skills"];
16
+ export type SkillIssueSnapshot = SkillListResponse["issues"];
15
17
  /** Registration-side business face for the section. */
16
18
  export interface SkillImporterInjected {
17
19
  hooks: {
18
20
  /** Last successful `/skill-importer/list` result. */
19
21
  skills: HostObservable<SkillCatalogSnapshot>;
22
+ /** Non-fatal filesystem failures from the last catalog refresh. */
23
+ skillIssues: HostObservable<SkillIssueSnapshot>;
20
24
  };
21
25
  actions: {
22
26
  /** Re-fetch the installed catalog; resolves to the fresh rows. */
23
- refreshSkills: () => Promise<SkillCatalogSnapshot>;
27
+ refreshSkills: (workspacePath?: string) => Promise<SkillCatalogSnapshot>;
28
+ /** Load the effective catalog for one conversation without mutating the settings catalog. */
29
+ refreshSkillsForSession: (sessionId: SessionId) => Promise<SkillCatalogSnapshot>;
24
30
  /** Write one skill file via the host route; resolves to the written path. */
25
31
  importSkill: (request: ImportRequest) => Promise<string>;
26
32
  /** Fetch a URL and write the skill via the host route; resolves to the written path. */
@@ -33,9 +39,11 @@ export interface SkillImporterInjected {
33
39
  scanBatch: (request: BatchScanRequest) => Promise<BatchScanResponse>;
34
40
  /** Commit one preflight once, replacing only explicitly confirmed names. */
35
41
  commitBatch: (request: BatchCommitRequest) => Promise<BatchCommitResponse>;
42
+ /** Return the installed version and the latest npm version when reachable. */
43
+ checkForUpdates: () => Promise<UpdateStatusResponse>;
36
44
  };
37
45
  }
38
- /** Required services (cordis fiber inject). */
46
+ /** Every client service used during apply is explicit; alpha DSH no longer tolerates load-order access. */
39
47
  export declare const inject: string[];
40
48
  /**
41
49
  * Client plugin body: register the section and its dictionaries.
@@ -13,7 +13,12 @@ export declare const zh: {
13
13
  pickerLabel: string;
14
14
  pickerPlaceholder: string;
15
15
  pickerSearchPlaceholder: string;
16
+ pickerLoading: string;
16
17
  pickerEmpty: string;
18
+ pickerSourceProjectDsh: string;
19
+ pickerSourceProjectAgents: string;
20
+ pickerSourceUserDsh: string;
21
+ pickerSourceUserAgents: string;
17
22
  commandDescription: string;
18
23
  entryTitle: string;
19
24
  entryDescription: string;
@@ -29,6 +34,7 @@ export declare const zh: {
29
34
  deleteFailed: string;
30
35
  deleted: string;
31
36
  installedFailed: string;
37
+ installedPartial: string;
32
38
  userOnly: string;
33
39
  modelOnly: string;
34
40
  importTitle: string;
@@ -79,8 +85,29 @@ export declare const zh: {
79
85
  batchStatusSkipped: string;
80
86
  batchStatusError: string;
81
87
  targetLabel: string;
82
- targetUser: string;
88
+ targetProjectDsh: string;
83
89
  targetProjectAgents: string;
90
+ targetUserDsh: string;
91
+ targetUserAgents: string;
92
+ locationChange: string;
93
+ locationTitle: string;
94
+ locationProject: string;
95
+ locationGlobal: string;
96
+ workspaceSearch: string;
97
+ workspaceNoMatch: string;
98
+ chooseRootHint: string;
99
+ rootProjectDsh: string;
100
+ rootProjectAgents: string;
101
+ rootUserDsh: string;
102
+ rootUserAgents: string;
103
+ priorityHighest: string;
104
+ priorityHigher: string;
105
+ priorityLower: string;
106
+ priorityLowest: string;
107
+ locationSummaryProjectDsh: string;
108
+ locationSummaryProjectAgents: string;
109
+ locationSummaryUserDsh: string;
110
+ locationSummaryUserAgents: string;
84
111
  targetProjectNoWorkspace: string;
85
112
  noWorkspace: string;
86
113
  import: string;
@@ -88,6 +115,18 @@ export declare const zh: {
88
115
  imported: string;
89
116
  importFailed: string;
90
117
  hostUnreachable: string;
118
+ versionChecking: string;
119
+ versionCurrent: string;
120
+ versionLatest: string;
121
+ versionAvailable: string;
122
+ versionCheckFailed: string;
123
+ versionRetry: string;
124
+ versionUpdate: string;
125
+ updateTitle: string;
126
+ updateDescription: string;
127
+ copyCommand: string;
128
+ commandCopied: string;
129
+ restartHint: string;
91
130
  };
92
131
  /** The skill importer namespace key union. */
93
132
  export type SkillImporterKey = keyof typeof zh;
@@ -97,7 +136,12 @@ export declare const en: {
97
136
  pickerLabel: string;
98
137
  pickerPlaceholder: string;
99
138
  pickerSearchPlaceholder: string;
139
+ pickerLoading: string;
100
140
  pickerEmpty: string;
141
+ pickerSourceProjectDsh: string;
142
+ pickerSourceProjectAgents: string;
143
+ pickerSourceUserDsh: string;
144
+ pickerSourceUserAgents: string;
101
145
  commandDescription: string;
102
146
  entryTitle: string;
103
147
  entryDescription: string;
@@ -113,6 +157,7 @@ export declare const en: {
113
157
  deleteFailed: string;
114
158
  deleted: string;
115
159
  installedFailed: string;
160
+ installedPartial: string;
116
161
  userOnly: string;
117
162
  modelOnly: string;
118
163
  importTitle: string;
@@ -163,8 +208,29 @@ export declare const en: {
163
208
  batchStatusSkipped: string;
164
209
  batchStatusError: string;
165
210
  targetLabel: string;
166
- targetUser: string;
211
+ targetProjectDsh: string;
167
212
  targetProjectAgents: string;
213
+ targetUserDsh: string;
214
+ targetUserAgents: string;
215
+ locationChange: string;
216
+ locationTitle: string;
217
+ locationProject: string;
218
+ locationGlobal: string;
219
+ workspaceSearch: string;
220
+ workspaceNoMatch: string;
221
+ chooseRootHint: string;
222
+ rootProjectDsh: string;
223
+ rootProjectAgents: string;
224
+ rootUserDsh: string;
225
+ rootUserAgents: string;
226
+ priorityHighest: string;
227
+ priorityHigher: string;
228
+ priorityLower: string;
229
+ priorityLowest: string;
230
+ locationSummaryProjectDsh: string;
231
+ locationSummaryProjectAgents: string;
232
+ locationSummaryUserDsh: string;
233
+ locationSummaryUserAgents: string;
168
234
  targetProjectNoWorkspace: string;
169
235
  noWorkspace: string;
170
236
  import: string;
@@ -172,4 +238,16 @@ export declare const en: {
172
238
  imported: string;
173
239
  importFailed: string;
174
240
  hostUnreachable: string;
241
+ versionChecking: string;
242
+ versionCurrent: string;
243
+ versionLatest: string;
244
+ versionAvailable: string;
245
+ versionCheckFailed: string;
246
+ versionRetry: string;
247
+ versionUpdate: string;
248
+ updateTitle: string;
249
+ updateDescription: string;
250
+ copyCommand: string;
251
+ commandCopied: string;
252
+ restartHint: string;
175
253
  };
@@ -28,6 +28,7 @@ export declare const ROUTES: {
28
28
  readonly delete: "/skill-importer/delete";
29
29
  readonly batchScan: "/skill-importer/batch/scan";
30
30
  readonly batchCommit: "/skill-importer/batch/commit";
31
+ readonly updateStatus: "/skill-importer/update-status";
31
32
  };
32
33
  /** Required services: the harness web server's route registry and the workspace registry. */
33
34
  export declare const inject: string[];
@@ -8,7 +8,7 @@
8
8
  * and the skill-filesystem watcher discovers it in place.
9
9
  */
10
10
  import type { IncomingMessage, ServerResponse } from 'node:http';
11
- import type { BatchCommitEntry, BatchScanEntry, BatchScanRequest, ImportRequest, ImportTarget, ImportUrlRequest, SkillListEntry } from './types.ts';
11
+ import type { BatchCommitEntry, BatchScanEntry, BatchScanRequest, ImportRequest, ImportTarget, ImportUrlRequest, SkillListEntry, SkillListIssue } from './types.ts';
12
12
  /** Hard cap for one imported skill body (matches the client preview limit). */
13
13
  export declare const MAX_CONTENT_BYTES: number;
14
14
  /** Cap for one request body read (JSON overhead above the content cap). */
@@ -22,6 +22,7 @@ export declare const BATCH_SCAN_TTL_MS: number;
22
22
  export declare function dshHomeDir(): string;
23
23
  /** Absolute skill root for one target under one workspace. */
24
24
  export declare function skillRoot(target: ImportTarget, workspacePath: string): string;
25
+ export declare function isProjectTarget(target: ImportTarget): boolean;
25
26
  /**
26
27
  * Rebuild a skill file's frontmatter in the canonical, strictly-YAML-valid
27
28
  * form the harness's provider parses. Unknown keys are dropped (the harness
@@ -48,7 +49,10 @@ export declare function writeSkillFile(name: string, target: ImportTarget, conte
48
49
  * rank at discovery time). Display order groups by source, then name.
49
50
  * @param workspacePaths - canonical paths of the registered workspaces.
50
51
  */
51
- export declare function listSkills(workspacePaths: readonly string[]): SkillListEntry[];
52
+ export declare function listSkills(workspacePaths: readonly string[]): {
53
+ skills: SkillListEntry[];
54
+ issues: SkillListIssue[];
55
+ };
52
56
  /**
53
57
  * Delete one installed skill (its whole bundle directory `<root>/<name>/`,
54
58
  * or the flat `<root>/<name>.md`), scoped to the skill roots only.
@@ -4,7 +4,7 @@
4
4
  * browser bundle can reference them safely.
5
5
  */
6
6
  /** Where the imported skill file should land. */
7
- export type ImportTarget = 'user' | 'project-agents';
7
+ export type ImportTarget = 'project-dsh' | 'project-agents' | 'user-dsh' | 'user-agents';
8
8
  /** One installed skill row served by `/skill-importer/list`. */
9
9
  export interface SkillListEntry {
10
10
  /** Kebab-case skill name (frontmatter `name`). */
@@ -17,8 +17,17 @@ export interface SkillListEntry {
17
17
  readonly modelInvocable: boolean;
18
18
  /** False marks a model-only skill (`user-invocable: false`). */
19
19
  readonly userInvocable: boolean;
20
- /** Skill root (`'project-agents'` or `'user'`). */
20
+ /** Writable DSH skill root that owns this copy. */
21
21
  readonly source: ImportTarget;
22
+ /** Exact registered workspace that owns a project-scoped copy. */
23
+ readonly workspacePath?: string;
24
+ }
25
+ /** One skill root or entry that could not be inspected without failing the whole list. */
26
+ export interface SkillListIssue {
27
+ readonly source: ImportTarget;
28
+ readonly path: string;
29
+ readonly workspacePath?: string;
30
+ readonly message: string;
22
31
  }
23
32
  /** File-import request body. */
24
33
  export interface ImportRequest {
@@ -32,7 +41,7 @@ export interface ImportRequest {
32
41
  /** Delete-request body. */
33
42
  export interface DeleteRequest {
34
43
  readonly name: string;
35
- /** Which root the copy lives in (`'project-agents'` or `'user'`). */
44
+ /** Which writable DSH root owns this copy. */
36
45
  readonly source: ImportTarget;
37
46
  /** Canonical workspace path for project sources (host-validated). */
38
47
  readonly workspacePath?: string;
@@ -95,6 +104,17 @@ export interface BatchCommitResponse {
95
104
  export interface SkillListResponse {
96
105
  readonly ok: true;
97
106
  readonly skills: readonly SkillListEntry[];
107
+ readonly issues: readonly SkillListIssue[];
108
+ }
109
+ /** `/skill-importer/update-status` response. */
110
+ export interface UpdateStatusResponse {
111
+ readonly ok: true;
112
+ readonly currentVersion: string;
113
+ readonly latestVersion?: string;
114
+ readonly updateAvailable: boolean;
115
+ readonly command?: string;
116
+ /** A failed registry check is non-fatal: the installed version is still useful. */
117
+ readonly error?: string;
98
118
  }
99
119
  /** Success response of an import. */
100
120
  export interface ImportResponse {
@@ -0,0 +1,6 @@
1
+ /** SemVer ordering used to avoid offering a downgrade or an older prerelease. */
2
+ export declare function compareVersions(left: string, right: string): number;
3
+ /** Read from package metadata in both src and built lib layouts. */
4
+ export declare function currentVersion(): string;
5
+ export declare function latestVersion(now?: number): Promise<string>;
6
+ export declare function updateCommand(version: string): string;