dsh-skill-importer 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, useSkills, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
16
+ export declare function SkillsPicker({ t, sessionId, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
@@ -0,0 +1,19 @@
1
+ /** One safe, collapsed-caret Backspace rewrite over a leading skill token. */
2
+ export interface AtomicSkillDeleteEdit {
3
+ readonly draft: string;
4
+ readonly caret: number;
5
+ }
6
+ /**
7
+ * Delete one exact, leading, installed `/name` token as a single edit.
8
+ *
9
+ * This deliberately handles only the common boundary gesture. Selections,
10
+ * token-internal edits, unknown names, leading whitespace, and non-boundary
11
+ * carets fall through to the textarea's normal behavior.
12
+ */
13
+ export declare function atomicSkillBackspace(draft: string, caret: number, names: readonly string[]): AtomicSkillDeleteEdit | undefined;
14
+ /**
15
+ * Resolve one whole-token horizontal caret jump.
16
+ * Left from anywhere inside/just after the leading skill goes before it;
17
+ * right from before/inside it goes after its single separator.
18
+ */
19
+ export declare function atomicSkillArrow(draft: string, caret: number, direction: 'left' | 'right', names: readonly string[]): number | undefined;
@@ -0,0 +1,9 @@
1
+ /** Session-routed request emitted after a non-React command popup edits the draft. */
2
+ export interface ComposerFocusRequest {
3
+ readonly sessionId: string;
4
+ readonly expectedDraft: string;
5
+ readonly caret: number;
6
+ }
7
+ export declare const COMPOSER_FOCUS_EVENT = "dsh-skill-importer/composer-focus";
8
+ /** Ask the mounted picker in one session to restore its composer focus. */
9
+ export declare function requestComposerFocus(request: ComposerFocusRequest): void;
@@ -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,6 +39,8 @@ 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
46
  /** Required services (cordis fiber inject). */
@@ -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;
@@ -21,10 +26,15 @@ export declare const zh: {
21
26
  refresh: string;
22
27
  installedEmpty: string;
23
28
  delete: string;
29
+ deleting: string;
30
+ deleteTitle: string;
24
31
  deleteConfirm: string;
32
+ cancel: string;
33
+ close: string;
25
34
  deleteFailed: string;
26
35
  deleted: string;
27
36
  installedFailed: string;
37
+ installedPartial: string;
28
38
  userOnly: string;
29
39
  modelOnly: string;
30
40
  importTitle: string;
@@ -67,6 +77,7 @@ export declare const zh: {
67
77
  batchConflict: string;
68
78
  batchReplace: string;
69
79
  batchConfirm: string;
80
+ batchConfirmTitle: string;
70
81
  batchResultTitle: string;
71
82
  batchResultSummary: string;
72
83
  batchStatusImported: string;
@@ -74,8 +85,29 @@ export declare const zh: {
74
85
  batchStatusSkipped: string;
75
86
  batchStatusError: string;
76
87
  targetLabel: string;
77
- targetUser: string;
88
+ targetProjectDsh: string;
78
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;
79
111
  targetProjectNoWorkspace: string;
80
112
  noWorkspace: string;
81
113
  import: string;
@@ -83,6 +115,18 @@ export declare const zh: {
83
115
  imported: string;
84
116
  importFailed: string;
85
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;
86
130
  };
87
131
  /** The skill importer namespace key union. */
88
132
  export type SkillImporterKey = keyof typeof zh;
@@ -92,7 +136,12 @@ export declare const en: {
92
136
  pickerLabel: string;
93
137
  pickerPlaceholder: string;
94
138
  pickerSearchPlaceholder: string;
139
+ pickerLoading: string;
95
140
  pickerEmpty: string;
141
+ pickerSourceProjectDsh: string;
142
+ pickerSourceProjectAgents: string;
143
+ pickerSourceUserDsh: string;
144
+ pickerSourceUserAgents: string;
96
145
  commandDescription: string;
97
146
  entryTitle: string;
98
147
  entryDescription: string;
@@ -100,10 +149,15 @@ export declare const en: {
100
149
  refresh: string;
101
150
  installedEmpty: string;
102
151
  delete: string;
152
+ deleting: string;
153
+ deleteTitle: string;
103
154
  deleteConfirm: string;
155
+ cancel: string;
156
+ close: string;
104
157
  deleteFailed: string;
105
158
  deleted: string;
106
159
  installedFailed: string;
160
+ installedPartial: string;
107
161
  userOnly: string;
108
162
  modelOnly: string;
109
163
  importTitle: string;
@@ -146,6 +200,7 @@ export declare const en: {
146
200
  batchConflict: string;
147
201
  batchReplace: string;
148
202
  batchConfirm: string;
203
+ batchConfirmTitle: string;
149
204
  batchResultTitle: string;
150
205
  batchResultSummary: string;
151
206
  batchStatusImported: string;
@@ -153,8 +208,29 @@ export declare const en: {
153
208
  batchStatusSkipped: string;
154
209
  batchStatusError: string;
155
210
  targetLabel: string;
156
- targetUser: string;
211
+ targetProjectDsh: string;
157
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;
158
234
  targetProjectNoWorkspace: string;
159
235
  noWorkspace: string;
160
236
  import: string;
@@ -162,4 +238,16 @@ export declare const en: {
162
238
  imported: string;
163
239
  importFailed: string;
164
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;
165
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.