dsh-skill-hub 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CONTRIBUTING.md +6 -4
  2. package/README.md +9 -5
  3. package/README.zh.md +9 -5
  4. package/lib/client.js +55 -58
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +529 -315
  7. package/lib/types/client/SkillHubSettingsCard.d.ts +5 -3
  8. package/lib/types/client/icons.d.ts +4 -5
  9. package/lib/types/client/index.d.ts +13 -12
  10. package/lib/types/client/locales/market.d.ts +1 -0
  11. package/lib/types/client/locales.d.ts +1 -0
  12. package/lib/types/client/settings-form.d.ts +9 -3
  13. package/lib/types/client/slash-dots.d.ts +20 -11
  14. package/lib/types/index.d.ts +59 -24
  15. package/lib/types/protocol/api.d.ts +22 -2
  16. package/lib/types/protocol/config.d.ts +30 -4
  17. package/lib/types/protocol/market.d.ts +1 -1
  18. package/lib/types/protocol/repo.d.ts +18 -1
  19. package/lib/types/protocol.d.ts +3 -3
  20. package/lib/types/repo/discovery.d.ts +59 -3
  21. package/lib/types/repo/github-client.d.ts +16 -0
  22. package/lib/types/repo.d.ts +6 -3
  23. package/lib/types/routes/helpers.d.ts +8 -1
  24. package/lib/types/routes/market.d.ts +2 -2
  25. package/lib/types/routes.d.ts +1 -1
  26. package/lib/types/store/store.d.ts +4 -0
  27. package/package.json +31 -41
  28. package/src/client/SkillHubSettingsCard.tsx +5 -3
  29. package/src/client/icons.tsx +4 -5
  30. package/src/client/index.tsx +33 -32
  31. package/src/client/locales/market.ts +2 -0
  32. package/src/client/panel/RepoScanCard.tsx +6 -3
  33. package/src/client/panel/SkillHubPanel.tsx +1 -1
  34. package/src/client/panel/SourcesView.tsx +1 -1
  35. package/src/client/panel/hooks/useGroupFlow.ts +1 -8
  36. package/src/client/settings-form.ts +9 -4
  37. package/src/client/slash-dots.test.ts +19 -36
  38. package/src/client/slash-dots.tsx +30 -60
  39. package/src/index.ts +145 -82
  40. package/src/protocol/api.ts +24 -2
  41. package/src/protocol/config.ts +35 -4
  42. package/src/protocol/market.ts +1 -1
  43. package/src/protocol/repo.ts +22 -1
  44. package/src/protocol.ts +3 -3
  45. package/src/repo/discovery.ts +120 -33
  46. package/src/repo/github-client.ts +18 -1
  47. package/src/repo/install.ts +8 -7
  48. package/src/repo.test.ts +158 -4
  49. package/src/repo.ts +10 -2
  50. package/src/routes/config.ts +19 -2
  51. package/src/routes/helpers.ts +9 -2
  52. package/src/routes/market.ts +49 -37
  53. package/src/routes/repo-import.ts +3 -2
  54. package/src/routes/sources.ts +2 -1
  55. package/src/routes.test.ts +107 -1
  56. package/src/routes.ts +21 -3
  57. package/src/store/migrate.ts +3 -2
  58. package/src/store/store.ts +7 -2
  59. package/src/store.test.ts +50 -0
  60. package/src/update.ts +4 -2
@@ -2,8 +2,10 @@
2
2
  * The dsh-skill-hub plugin settings card: bridges the hub's settings
3
3
  * namespace (bound through the official settings transport) onto the
4
4
  * family-style staged card form (enabled master switch + agent announcement).
5
- * Registered into the official `settings.plugin.item` slot keyed by that
6
- * namespace, so the plugin shows up in Settings → 插件 on dsh rc.7+.
5
+ * Registered into `plugins.bundle.config` under the bundle's package name, so
6
+ * the Plugins manager renders it on dsh-skill-hub's own page. That slot is
7
+ * dispatched with `view: 'page'` only, so the card always renders its full
8
+ * form (no `summary` one-liner variant).
7
9
  */
8
10
  import { type ReactElement } from 'react';
9
11
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
@@ -34,7 +36,7 @@ export interface SkillHubSettingsCardFace {
34
36
  resetField: (field: string) => void;
35
37
  }
36
38
  /** Props the slot renderer binds (locale copy + injected form actions). */
37
- export type SkillHubSettingsCardProps = PropsRuntime<'settings.plugin.item'> & PropsLocale<'dsh-skill-hub'> & InjectFace<SkillHubSettingsCardFace>;
39
+ export type SkillHubSettingsCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<'dsh-skill-hub'> & InjectFace<SkillHubSettingsCardFace>;
38
40
  /** Bridges the hub's config scope onto the card's staged form. */
39
41
  export declare class SkillHubSettingsCardController {
40
42
  private readonly form;
@@ -2,11 +2,10 @@
2
2
  * Vendored UI icons for dsh-skill-hub.
3
3
  *
4
4
  * These were originally imported from `@deepseek-ai/dsh-client-ui-primitives`.
5
- * That package is still published for the rc.7/rc.2 SDK families, but newer dsh
6
- * web builds no longer expose it as a standalone plugin module — they keep a
7
- * static compatibility module instead. Vendoring the few tiny outline icons the
8
- * hub uses makes the browser half self-contained and equally compatible with
9
- * older and newer dsh hosts.
5
+ * That package is not exposed as a standalone plugin module by the dsh web
6
+ * build this plugin targets — dsh keeps a static compatibility module instead.
7
+ * Vendoring the few tiny outline icons the hub uses keeps the browser half
8
+ * self-contained and independent of how that module is packaged.
10
9
  *
11
10
  * SVG paths are copied verbatim from dsh-client-ui-primitives (MIT licensed)
12
11
  * so the visuals remain pixel-identical to the dsh icon family.
@@ -2,16 +2,17 @@
2
2
  * Browser-half entry for the dsh-skill-hub plugin — runs inside the dsh
3
3
  * web GUI.
4
4
  *
5
- * Registers the dsh-skill-hub locale dictionaries and mounts two Settings
6
- * surfaces, both through official slots (no DOM injection):
7
- * - a plugin-management card in the `settings.plugin.item` slot (Settings →
8
- * 插件 → 可配置插件列表), keyed by the hub's settings namespace and bound
9
- * through the official settings transport (dsh rc.7 serves every
10
- * registered namespace to the web client, and the tab dispatches cards by
11
- * namespace) — the family-bucket card pattern (PluginSettingsCard +
12
- * CardForm vendored from dsh-task-board);
5
+ * Registers the dsh-skill-hub locale dictionaries and mounts:
13
6
  * - a top-level Settings section (Settings → 技能) hosting the skill hub
14
- * panel: catalog, search, enable/disable, diagnostics, new-skill form.
7
+ * panel: catalog, search, enable/disable, diagnostics, new-skill form;
8
+ * - the chat "/" menu skill dots, colored from the same config the panel reads.
9
+ *
10
+ * - a configuration card in the `plugins.bundle.config` slot, keyed by the
11
+ * BUNDLE PACKAGE NAME, rendered on the plugin's own page in the Plugins
12
+ * manager (sidebar → 插件 → dsh-skill-hub). dsh 0.1.7 has no auto-generated
13
+ * config page: the manager only renders that section for bundles that
14
+ * register this slot, and the card writes through the shared config form
15
+ * (`ctx.configForms.get(skill-hub)`), the same form the host routes read.
15
16
  *
16
17
  * Failure policy: mounting problems are logged, never thrown — the web
17
18
  * shell fails the whole boot when a plugin apply throws, and an external
@@ -37,9 +38,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
37
38
  /**
38
39
  * Required services (fiber inject waiting — the runtime must be up first).
39
40
  * `connection`/`remote` are the settings transport's own prerequisites
40
- * (`ctx.settingsScope.bind` resolves them on the caller's fiber), and
41
- * `settingsScope` is the namespace-scope binder itself; mirror the official
42
- * settings-plugins inject list.
41
+ * (`ctx.configForms.get` resolves them on the caller's fiber), and
42
+ * `configForms` is the shared settings-form service itself; mirror the
43
+ * official settings-plugins inject list.
43
44
  */
44
45
  export declare const inject: string[];
45
46
  /** Type-only surface (export discipline: no value exports beyond the plugin contract). */
@@ -74,6 +74,7 @@ export declare const zhMarket: {
74
74
  readonly 'repo.cancel': "取消";
75
75
  readonly 'repo.cancelled': "已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理";
76
76
  readonly 'repo.importingCurrent': "正在导入: {name} ({done}/{total})";
77
+ readonly 'repo.root.self': "仓库根";
77
78
  readonly 'repo.root.skills': "skills";
78
79
  readonly 'repo.root.designTemplates': "design-templates";
79
80
  readonly 'repo.root.generic': "{root}";
@@ -157,6 +157,7 @@ export declare const zh: {
157
157
  readonly 'repo.cancel': "取消";
158
158
  readonly 'repo.cancelled': "已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理";
159
159
  readonly 'repo.importingCurrent': "正在导入: {name} ({done}/{total})";
160
+ readonly 'repo.root.self': "仓库根";
160
161
  readonly 'repo.root.skills': "skills";
161
162
  readonly 'repo.root.designTemplates': "design-templates";
162
163
  readonly 'repo.root.generic': "{root}";
@@ -54,7 +54,12 @@ export interface FieldState {
54
54
  overridden: boolean;
55
55
  invalid: boolean;
56
56
  }
57
- /** The settings scope face the form consumes (subset of SettingsScope<T>). */
57
+ /**
58
+ * The settings-form face this card consumes. Declared structurally rather than
59
+ * imported so the card stays independent of the settings package's generics:
60
+ * the browser half passes it `ctx.configForms.get<HubSettingsValue>(HUB_ENTRY_ID)`,
61
+ * which satisfies this shape (ConfigForm<T>).
62
+ */
58
63
  export interface FormScope {
59
64
  subscribe(listener: () => void): () => void;
60
65
  getSnapshot(): {
@@ -64,8 +69,9 @@ export interface FormScope {
64
69
  base?: unknown;
65
70
  user?: unknown;
66
71
  };
67
- set(field: string, value: unknown): Promise<void>;
68
- unset(field: string): Promise<void>;
72
+ /** Both write paths report host acceptance (ConfigForm.set/unset contract). */
73
+ set(field: string, value: unknown): Promise<boolean>;
74
+ unset(field: string): Promise<boolean>;
69
75
  }
70
76
  /** Stages one card's edits and writes them through the settings transport. */
71
77
  export declare class CardForm {
@@ -2,14 +2,15 @@
2
2
  * Slash-menu skill dots: puts the invocation-status dot (model-callable blue /
3
3
  * user-only green) in front of every skill candidate in the chat `/` menu.
4
4
  *
5
- * Mechanism (mirrors how dsh-at-file fills the menu icon slot): the candidate
6
- * menu's rows already render an optional `icon` slot (`MenuView` renders
7
- * `item.icon` in a 16×16 leading span when it's defined), but the core `/skill`
8
- * source (`dsh-client-ui-skill`) returns candidates without `icon`. This module
9
- * wraps that source's `candidates` and stamps each row with a colored dot,
10
- * reusing the same settings (dotModelColor / dotUserColor) and the same
11
- * `modelInvocable` classification the panel legend uses — so the chat menu and
12
- * the Settings → 技能 panel stay in sync, and editing the color updates both.
5
+ * Mechanism: hooks the `/skill` source's `candidates` so the hub learns which
6
+ * rows are skill candidates and how each one classifies for model invocation,
7
+ * then injects the dot straight into the rendered option rows. The menu's
8
+ * `icon` slot is unusable for this — `MenuView` narrowed `icon` to an enum
9
+ * ('file' | 'folder' | 'session') and renders it through `ReferenceIcon`, so a
10
+ * custom element never reaches the DOM. Colors come from the same settings
11
+ * (dotModelColor / dotUserColor) and the same `modelInvocable` classification
12
+ * the panel legend uses — so the chat menu and the Settings → 技能 panel stay
13
+ * in sync, and editing the color updates both.
13
14
  *
14
15
  * The skill source is registered by the core plugin under the `name` "skill"
15
16
  * on the `/` trigger; re-registering the same name would throw, so this wraps
@@ -18,7 +19,7 @@
18
19
  * / `sessionOf` — the lookup below is defensive: it never throws).
19
20
  */
20
21
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
21
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
22
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
22
23
  import type { InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client';
23
24
  import type { HubSettingsValue } from '../protocol.ts';
24
25
  import type { SkillHubApi } from './api.ts';
@@ -35,7 +36,15 @@ export declare function findSkillSource(service: InputTriggerServiceContract): I
35
36
  * catalog wins after reconnect; exported for deterministic unit tests.
36
37
  */
37
38
  export declare function resetModelCache(): void;
38
- export declare function wrapSkillSource(source: InputTriggerSource, api: SkillHubApi, scope: SettingsScope<HubSettingsValue>): () => void;
39
+ /**
40
+ * Wrap the core skill source so every menu row carries the invocation dot.
41
+ * Exported for unit tests; production wiring goes through setupSkillSlashDots.
42
+ * @param source - the registered `/skill` source.
43
+ * @param api - hub browser API for the modelInvocable lookup.
44
+ * @param scope - hub settings scope for the dot colors.
45
+ * @returns a disposer restoring the original candidates.
46
+ */
47
+ export declare function wrapSkillSource(source: InputTriggerSource, api: SkillHubApi, scope: ConfigForm<HubSettingsValue>): () => void;
39
48
  /**
40
49
  * Mount the slash-menu dots on the registered `/skill` source. Idempotent and
41
50
  * defensive: if the core source isn't registered yet (or the registry shape
@@ -47,4 +56,4 @@ export declare function wrapSkillSource(source: InputTriggerSource, api: SkillHu
47
56
  * @param scope - hub settings scope for dot colors.
48
57
  * @returns a cleanup function for `ctx.effect`.
49
58
  */
50
- export declare function setupSkillSlashDots(ctx: ClientContext, api: SkillHubApi, scope: SettingsScope<HubSettingsValue>): () => void;
59
+ export declare function setupSkillSlashDots(ctx: ClientContext, api: SkillHubApi, scope: ConfigForm<HubSettingsValue>): () => void;
@@ -6,47 +6,82 @@
6
6
  * the skill hub panel. Everything rides official NPM SDK packages — no dsh
7
7
  * source changes.
8
8
  */
9
- import type { Context } from '@deepseek-ai/cordis';
9
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
10
10
  import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
11
- import z from 'schemastery';
12
- import { type HubSettingsValue } from './protocol.ts';
11
+ import z from '@deepseek-ai/schemastery';
13
12
  /** Stable cordis plugin name (matches cordis.patch.yml insert id). */
14
13
  export declare const name = "skill-hub";
15
- /** Services required before the skill-hub surfaces can mount. */
14
+ /** Services required before the skill-hub surfaces can mount. `settings` is deliberately NOT here: the plugin reads its own volatile config refs and only reaches for the settings service when it is present (issue #11). */
16
15
  export declare const inject: string[];
17
- /** Plugin config, validated by the same-named schemastery schema. */
16
+ /**
17
+ * Plugin config, validated by the same-named schemastery schema.
18
+ *
19
+ * dsh 0.1.7 replaced the "plugin registers a settings namespace" model with
20
+ * "the Loader entry's Config IS the settings namespace", so this one schema is
21
+ * both the composition config and the settings page. Every field is volatile:
22
+ * the Loader re-resolves volatile values in place and re-enters apply()
23
+ * instead of rebuilding the fiber, and only volatile fields are writable
24
+ * through the settings transport.
25
+ */
18
26
  export interface Config {
19
27
  /** When true (default), a system-prompt section announces the hub to every agent. */
20
- announceToAgent?: boolean;
28
+ announceToAgent: Volatile<boolean>;
21
29
  /** Master switch for the plugin (routes, prompt section). */
22
- enabled?: boolean;
30
+ enabled: Volatile<boolean>;
23
31
  /** Show per-skill invocation count chip. Default true. */
24
- showUseCount?: boolean;
32
+ showUseCount: Volatile<boolean>;
25
33
  /** Show per-skill last-used relative time. Default true. */
26
- showUseTime?: boolean;
34
+ showUseTime: Volatile<boolean>;
27
35
  /** Show group-header usage summaries (count + last used). Default true. */
28
- showGroupSummary?: boolean;
29
- /** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
30
- statsWindowDays?: number;
31
- /** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
32
- statsScanMinutes?: number;
36
+ showGroupSummary: Volatile<boolean>;
37
+ /** 模型可调圆点颜色(#rrggbb);缺省用面板默认色。 */
38
+ dotModelColor: Volatile<string | undefined>;
39
+ /** 用户可调圆点颜色(#rrggbb);缺省用面板默认色。 */
40
+ dotUserColor: Volatile<string | undefined>;
41
+ /** GitHub token;`role('secret')` 让 settings 层统一脱敏,缺省为匿名。 */
42
+ githubToken: Volatile<string | undefined>;
43
+ /** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。 */
44
+ statsWindowDays: Volatile<number>;
45
+ /** 自动统计扫描间隔(分钟,最小 1)。 */
46
+ statsScanMinutes: Volatile<number>;
33
47
  }
34
- export declare const Config: z<Config>;
35
48
  /**
36
- * Settings namespace hosting the hub's runtime config. Since dsh rc.7 the
37
- * host serves every registered settings namespace to the web client (the
38
- * dsh-host-apiproxy allowlist is gone), so the browser card and the settings
39
- * page edit this namespace through the official settings transport, and the
40
- * plugin consumes the same resolved value — one source of truth.
49
+ * The live plugin config the settings page edits. Field order here is the row
50
+ * order the auto-generated page renders.
51
+ */
52
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
53
+ enabled: z<boolean, boolean, "volatile-defined">;
54
+ announceToAgent: z<boolean, boolean, "volatile-defined">;
55
+ dotModelColor: z<string, string, "volatile">;
56
+ dotUserColor: z<string, string, "volatile">;
57
+ showUseCount: z<boolean, boolean, "volatile-defined">;
58
+ showUseTime: z<boolean, boolean, "volatile-defined">;
59
+ showGroupSummary: z<boolean, boolean, "volatile-defined">;
60
+ statsWindowDays: z<number, number, "volatile-defined">;
61
+ statsScanMinutes: z<number, number, "volatile-defined">;
62
+ githubToken: z<string, string, "volatile">;
63
+ }>>, Schemastery.ObjectT<NoInfer<{
64
+ enabled: z<boolean, boolean, "volatile-defined">;
65
+ announceToAgent: z<boolean, boolean, "volatile-defined">;
66
+ dotModelColor: z<string, string, "volatile">;
67
+ dotUserColor: z<string, string, "volatile">;
68
+ showUseCount: z<boolean, boolean, "volatile-defined">;
69
+ showUseTime: z<boolean, boolean, "volatile-defined">;
70
+ showGroupSummary: z<boolean, boolean, "volatile-defined">;
71
+ statsWindowDays: z<number, number, "volatile-defined">;
72
+ statsScanMinutes: z<number, number, "volatile-defined">;
73
+ githubToken: z<string, string, "volatile">;
74
+ }>>, "plain">;
75
+ /**
76
+ * The settings namespace this plugin's config lives under, narrowed to the
77
+ * settings package's branded type. The browser half resolves the same form
78
+ * through `ctx.configForms.get(HUB_ENTRY_ID)`.
41
79
  */
42
- export declare const CONFIG_NAMESPACE: SettingsNamespace;
43
- /** Schema of the hub's settings namespace: the card's fields (booleans + optional dot colors). */
44
- export declare const HubSettingsSchema: z<HubSettingsValue>;
80
+ export declare const ENTRY_ID: SettingsNamespace;
45
81
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
46
82
  export declare const SKILL_HUB_GUIDANCE: string;
47
83
  /**
48
84
  * Mount the skill hub routes and announcement.
49
85
  * @param ctx - host plugin context carrying webServer/skills/systemPrompt/settings.
50
- * @param config - resolved plugin config (schema defaults applied by the loader).
51
86
  */
52
87
  export declare function apply(ctx: Context, config?: Config): void;
@@ -1,4 +1,24 @@
1
- /** Browser-facing base paths of the skill-hub API family. */
1
+ /**
2
+ * Root path of the skill-hub API family. The host also registers this as its
3
+ * 404 catch-all prefix, so a mistyped path answers with a plain 404 naming the
4
+ * path instead of falling through to the SPA fallback (which answers 401 and
5
+ * reads like an auth problem).
6
+ */
7
+ export declare const SKILL_HUB_API_ROOT = "/api/skill-hub";
8
+ /**
9
+ * Deprecated path of the market update check, kept routable for one release
10
+ * after the naming unification below. A browser tab that loaded the previous
11
+ * client bundle keeps calling it until it reloads; delete this (and its route
12
+ * in routes/market.ts) in the next minor.
13
+ */
14
+ export declare const SKILL_HUB_API_DEPRECATED_MARKET_CHECK = "/api/skill-hub/market/check";
15
+ /**
16
+ * Browser-facing base paths of the skill-hub API family.
17
+ *
18
+ * Naming rule: a path's segments mirror its scope. Market sources own the
19
+ * `/market/source/*` subtree — add, delete, ref, versions, check, sync — so
20
+ * the update check and the sync that acts on its result sit side by side.
21
+ */
2
22
  export declare const SKILL_HUB_API: {
3
23
  readonly catalog: "/api/skill-hub/catalog";
4
24
  readonly skill: "/api/skill-hub/skill";
@@ -12,7 +32,7 @@ export declare const SKILL_HUB_API: {
12
32
  readonly marketSource: "/api/skill-hub/market/source";
13
33
  readonly marketSourceDelete: "/api/skill-hub/market/source/delete";
14
34
  readonly marketSourceRef: "/api/skill-hub/market/source/ref";
15
- readonly marketCheck: "/api/skill-hub/market/check";
35
+ readonly marketCheck: "/api/skill-hub/market/source/check";
16
36
  readonly marketSync: "/api/skill-hub/market/source/sync";
17
37
  readonly repo: "/api/skill-hub/repo";
18
38
  readonly repoImport: "/api/skill-hub/repo/import";
@@ -29,6 +29,14 @@ export interface HubConfig {
29
29
  */
30
30
  githubToken?: string;
31
31
  }
32
+ /**
33
+ * HubConfig minus the GitHub token: the shape of every config payload that
34
+ * leaves the host over HTTP. The token is write-only there — a response
35
+ * pasted into an issue or a screenshot would otherwise hand the credential
36
+ * over, so callers read `ConfigResponse.githubTokenSet` when they only need
37
+ * to know whether one is in effect.
38
+ */
39
+ export type RedactedHubConfig = Omit<HubConfig, 'githubToken'>;
32
40
  /**
33
41
  * The resolved shape of the hub's settings namespace (schema defaults, then
34
42
  * the composition base, then the user layer). Kept as a type alias so the
@@ -52,6 +60,14 @@ export type HubSettingsValue = {
52
60
  /** GitHub token(明文存本地设置文档,仅回环可读;缺省为匿名)。 */
53
61
  githubToken?: string;
54
62
  };
63
+ /**
64
+ * This plugin's Loader entry id — the settings namespace dsh serves its config
65
+ * under. It is cordis.patch.yml's insert id, NOT the package name, and it is
66
+ * shared by contract because the browser half addresses the same form through
67
+ * `ctx.configForms.get(HUB_ENTRY_ID)` while the host half writes through
68
+ * `ctx.settings.update(HUB_ENTRY_ID, …)`.
69
+ */
70
+ export declare const HUB_ENTRY_ID = "skill-hub";
55
71
  /**
56
72
  * Hub config defaults — the single source every layer reads: the cordis
57
73
  * schema (index.ts), the host's saved-override merge, and the routes'
@@ -74,6 +90,14 @@ export declare const HUB_CONFIG_DEFAULTS: {
74
90
  * their sane ranges (window ≥ 0, scan interval ≥ 1 minute).
75
91
  */
76
92
  export declare function resolveHubConfig(saved: Partial<HubConfig>, base?: Partial<HubConfig>): HubConfig;
93
+ /**
94
+ * Drop the GitHub token from a config-shaped object. Returns a copy, so the
95
+ * caller's own config layer keeps the token. Shared by the config route's GET
96
+ * and POST responses — neither may echo it back.
97
+ */
98
+ export declare function redactGithubToken<T extends {
99
+ githubToken?: string;
100
+ }>(value: T): Omit<T, 'githubToken'>;
77
101
  /** HEX color validation shared by host routes and the settings card. */
78
102
  export declare const HEX_COLOR_RE: RegExp;
79
103
  /** GET /api/skill-hub/config */
@@ -81,10 +105,12 @@ export interface ConfigResponse {
81
105
  ok: true;
82
106
  /** 已安装插件自身的版本号(package.json version),设置卡标题旁显示。 */
83
107
  pluginVersion: string;
84
- /** Effective configuration (saved overrides merged over the defaults). */
85
- config: HubConfig;
86
- /** Raw user overrides persisted in the sidecar (absent fields inherit defaults). */
87
- saved: Partial<HubConfig>;
108
+ /** Effective configuration (saved overrides merged over the defaults), token stripped. */
109
+ config: RedactedHubConfig;
110
+ /** Raw user overrides persisted in the sidecar (absent fields inherit defaults), token stripped. */
111
+ saved: Partial<RedactedHubConfig>;
112
+ /** True when a GitHub token is in effect (saved override or the base/env layer). */
113
+ githubTokenSet: boolean;
88
114
  }
89
115
  /** POST /api/skill-hub/config — a partial patch; omitted fields keep their values. */
90
116
  export interface ConfigRequest {
@@ -64,7 +64,7 @@ export interface MarketStatsResponse {
64
64
  error?: string;
65
65
  }>;
66
66
  }
67
- /** GET /api/skill-hub/market/check — update check over market sources. */
67
+ /** GET /api/skill-hub/market/source/check — update check over market sources. */
68
68
  export interface MarketCheckResponse {
69
69
  ok: true;
70
70
  results: Array<{
@@ -17,8 +17,25 @@ export interface RepoSkillEntry {
17
17
  /** Whether a same-named skill already exists locally. */
18
18
  existing: boolean;
19
19
  }
20
- /** Skill root in a GitHub repo: the top-level directory that contains skills (e.g. skills, design-templates, templates). Auto-derived from SKILL.md locations, not hard-coded. */
20
+ /**
21
+ * Skill root in a GitHub repo: the top-level directory that contains skills
22
+ * (e.g. skills, design-templates, templates). Auto-derived from SKILL.md
23
+ * locations, not hard-coded.
24
+ *
25
+ * The empty string is the repo root itself — a skill whose SKILL.md sits at
26
+ * the top of the tree (a Claude Code plugin manifest may declare
27
+ * `"skills": ["./"]`). It is a legal value, not a missing one, so state
28
+ * sanitizers must accept it instead of coercing it to a default root.
29
+ */
21
30
  export type RepoRoot = string;
31
+ /** Top-level directory pattern for a skill root: visible, non-dot, safe chars. First char must be alphanum. */
32
+ export declare const REPO_ROOT_RE: RegExp;
33
+ /**
34
+ * True when a value is a usable root: a visible safe top-level directory name,
35
+ * or the empty string for the repo root. Guards the persisted store against
36
+ * corrupt roots without rewriting the repo-root sentinel.
37
+ */
38
+ export declare function isValidRepoRoot(value: unknown): value is RepoRoot;
22
39
  /** GET /api/skill-hub/repo — discover importable skills in a GitHub repo. */
23
40
  export interface RepoDiscoverResponse {
24
41
  ok: true;
@@ -7,11 +7,11 @@
7
7
  * 按域拆分后的重导出入口:老 `from './protocol.ts'` 写法保持可用,
8
8
  * 新代码可按需直引 `from './protocol/<domain>.ts'`。
9
9
  */
10
- export { SKILL_HUB_API } from './protocol/api.ts';
10
+ export { SKILL_HUB_API, SKILL_HUB_API_ROOT, SKILL_HUB_API_DEPRECATED_MARKET_CHECK } from './protocol/api.ts';
11
11
  export type { WritableRoot, HubInvocation, CatalogSkill, DisabledSkill, DiagnosticEntry, DiagnosticFixRequest, DiagnosticFixResponse, CatalogResponse, SkillDetail, SkillDetailResponse, SkillDeleteRequest, SkillDeleteResponse, ToggleRequest, ToggleResponse, ToggleBatchRequest, ToggleBatchResponse, CreateRequest, CreateResponse, } from './protocol/catalog.ts';
12
12
  export type { SkillStat, StatsResponse, SkillStatsCheckpoint } from './protocol/stats.ts';
13
- export type { ErrorResponse, HubConfig, HubSettingsValue, ConfigResponse, ConfigRequest } from './protocol/config.ts';
14
- export { HUB_CONFIG_DEFAULTS, HEX_COLOR_RE, GITHUB_TOKEN_RE, resolveHubConfig } from './protocol/config.ts';
13
+ export type { ErrorResponse, HubConfig, HubSettingsValue, RedactedHubConfig, ConfigResponse, ConfigRequest } from './protocol/config.ts';
14
+ export { HUB_CONFIG_DEFAULTS, HUB_ENTRY_ID, HEX_COLOR_RE, GITHUB_TOKEN_RE, resolveHubConfig, redactGithubToken } from './protocol/config.ts';
15
15
  export type { MarketSourceRecord, MarketSourcesResponse, MarketSourceRequest, MarketSourceResponse, MarketSourceRefRequest, MarketSourceVersionsResponse, MarketStatsSnapshot, MarketStatsResponse, MarketCheckResponse, MarketSyncResponse, } from './protocol/market.ts';
16
16
  export type { RepoSkillEntry, RepoRoot, RepoDiscoverResponse, RepoImportRequest, RepoImportResponse, RepoImportProgressResponse, RepoImportCancelRequest, RepoImportCancelResponse, UpdateCheckResponse, } from './protocol/repo.ts';
17
17
  export type { SkillTag, CollectionGroup, GroupsResponse, TagSaveRequest, TagSaveResponse, TagDeleteRequest, TagDeleteResponse, TagMembersRequest, TagMembersResponse, TagReorderRequest, TagReorderResponse, CollectionReorderRequest, CollectionReorderResponse, SourceGroupReorderRequest, SourceGroupReorderResponse, } from './protocol/groups.ts';
@@ -2,9 +2,48 @@
2
2
  * 仓库技能发现:从 GitHub 仓库树推断技能根目录、列出可导入技能、
3
3
  * 生成目录清单并做上游差异比对。纯函数为主,便于单测。
4
4
  * 从 repo.ts 抽出。
5
+ *
6
+ * 路径语义集中在几个内部函数上:`skillDirPrefix`(技能目录在树里的前缀)、
7
+ * `isRepoTooling`(永不属于技能的仓库基建)、`skillFileAt`(目录内的
8
+ * SKILL.md 路径)。collect / manifest / diff 三处必须共用它们,否则差异
9
+ * 比对会永远报「有更新」。
5
10
  */
6
- import type { RepoSkillEntry } from '../protocol.ts';
11
+ import type { RepoRoot, RepoSkillEntry } from '../protocol.ts';
7
12
  import type { RepoFile, RepoRef, RepoTreeItem } from './types.ts';
13
+ /**
14
+ * Sentinel root for a skill whose SKILL.md sits directly at the repository
15
+ * root instead of under a top-level directory. That layout is legal and used
16
+ * upstream (a Claude Code plugin manifest may declare `"skills": ["./"]`),
17
+ * and the empty string is the representation that composes with plain prefix
18
+ * arithmetic: an empty prefix *is* the repo root. Every path helper below
19
+ * special-cases it, because `'' + '/' + name` would produce a bogus absolute
20
+ * `/name` that matches no tree path.
21
+ */
22
+ export declare const REPO_ROOT: RepoRoot;
23
+ /** True when a root denotes the repo root itself (the repo is the skill dir). */
24
+ export declare function isRepoRoot(root: string): boolean;
25
+ /**
26
+ * The tree prefix delimiting a skill directory: empty for the repo-root skill
27
+ * (it owns the whole tree), otherwise `<dir>/`. Single source for collect,
28
+ * manifest, diff and the store's baseline cleanup, so they can never disagree.
29
+ */
30
+ export declare function skillDirPrefix(dir: string): string;
31
+ /**
32
+ * A path inside a skill directory, as it appears in the repo tree. Both
33
+ * directions of this mapping matter: repo paths are always '/'-joined (never
34
+ * node:path's OS separator, these are GitHub paths shown in the UI), and the
35
+ * repo-root case must not grow a leading slash.
36
+ */
37
+ export declare function skillPathIn(dir: string, relative: string): string;
38
+ /** The SKILL.md path inside a skill directory. */
39
+ export declare function skillFileAt(dir: string): string;
40
+ /**
41
+ * A repo file's path relative to its skill directory — the path it is written
42
+ * to under the skill's own directory. `dir === ''` means the repo file path
43
+ * *is* the relative path; slicing `dir.length + 1` would eat its first
44
+ * character.
45
+ */
46
+ export declare function relativeToSkillDir(dir: string, path: string): string;
8
47
  /** `owner/repo` slug for a parsed reference. */
9
48
  export declare function repoSlug(ref: RepoRef): string;
10
49
  /**
@@ -14,11 +53,20 @@ export declare function repoSlug(ref: RepoRef): string;
14
53
  export declare function normalizeRepoInput(input: string): RepoRef | null;
15
54
  /** Collect every file inside a skill directory, including SKILL.md itself. */
16
55
  export declare function collectRepoSkillFiles(tree: readonly RepoTreeItem[], dir: string): RepoFile[];
17
- /** Compute an origin collection name. Multiple roots split by root, one root keeps the repo slug. */
56
+ /**
57
+ * Compute an origin collection name. Multiple roots split by root, one root
58
+ * keeps the repo slug. The repo-root skill's name *is* the repo slug, so it
59
+ * never gets a trailing-slash suffix (`owner/repo/`).
60
+ */
18
61
  export declare function originForRoot(repo: string, rootsPresent: ReadonlySet<string>, root: string): string;
19
62
  /** Discover importable skills from a repo tree. Invalid names are ignored. Roots are auto-derived from the top-level directory of each SKILL.md. */
20
63
  export declare function discoverRepoEntries(tree: readonly RepoTreeItem[], repo: string, existingNames?: ReadonlySet<string>): RepoSkillEntry[];
21
- /** Build a path→size manifest for one skill directory from a repo tree. */
64
+ /**
65
+ * Build a path→size manifest for one skill directory from a repo tree.
66
+ * Shares `skillTreeFiles` with collectRepoSkillFiles: a manifest that
67
+ * disagreed with the collected file set would make every update diff report
68
+ * "changed" forever.
69
+ */
22
70
  export declare function skillManifest(tree: readonly RepoTreeItem[], dir: string): Record<string, number>;
23
71
  /**
24
72
  * The real upstream directory of one tracked skill. Source records keep only
@@ -29,6 +77,12 @@ export declare function skillManifest(tree: readonly RepoTreeItem[], dir: string
29
77
  * manifest's recorded blob path, then any same-name skill in the tree, then
30
78
  * the flat root/name fallback. Passing the tree paths makes the lookup
31
79
  * resilient to incomplete manifests and upstream moves.
80
+ *
81
+ * A repo-root record (root '') has no directory to resolve: its SKILL.md is a
82
+ * bare top-level path, so every nested lookup below would miss and the final
83
+ * fallback would hand back a bogus '/name'. Answer before them — including
84
+ * when the upstream SKILL.md is gone, so diffRemoteSkills reports a deletion
85
+ * instead of hunting for a skill that was never nested.
32
86
  */
33
87
  export declare function skillDirOf(source: {
34
88
  root: string;
@@ -38,6 +92,8 @@ export declare function skillDirOf(source: {
38
92
  * Diff one source record against an upstream tree at treeSha: which tracked
39
93
  * skills disappeared (no SKILL.md blob) and which changed (manifest baseline
40
94
  * differs, or no baseline exists — treated as changed). Pure over the tree.
95
+ * The remote side is read through skillManifest, so the diff compares like
96
+ * with like against the baseline recorded at import time.
41
97
  */
42
98
  export declare function diffRemoteSkills(tree: readonly RepoTreeItem[], source: {
43
99
  root: string;
@@ -12,6 +12,22 @@ export declare class RepoFetchError extends Error {
12
12
  export declare function setGithubToken(token: string | undefined): void;
13
13
  /** Authorization header for api.github.com / raw.githubusercontent.com calls. */
14
14
  export declare function githubAuthHeaders(): Record<string, string>;
15
+ /**
16
+ * 传输层:显式要求服务端返回未压缩实体。
17
+ *
18
+ * 为什么必须加:dsh 会把**启动环境**里的 `HTTP_PROXY`/`HTTPS_PROXY` 装成
19
+ * undici 的全局 dispatcher(`@deepseek-ai/dsh-http-proxy`),插件里的 `fetch`
20
+ * 因此走该代理。实测本机 Clash:经代理返回的响应会**丢掉 `content-encoding`
21
+ * 与 `content-type` 头,而 body 仍是 gzip**;undici 只依据 `content-encoding`
22
+ * 决定是否解压,于是 `response.json()` 拿到 gzip 二进制并抛错,被上层的
23
+ * catch 映射成 `invalid github response for <url>` —— 看起来像 GitHub 坏了。
24
+ * 实测对照(同一 URL、同一代理):默认 1331 字节解析失败,声明 identity 后
25
+ * 5245 字节解析成功。
26
+ *
27
+ * 代价是不走压缩(我们的响应体都很小),换来的是**无论中间代理是否改写头部
28
+ * 都能正确取到实体**,且用户无需为插件调整启动命令。
29
+ */
30
+ export declare const NO_COMPRESSION: Record<string, string>;
15
31
  /** JSON API 默认请求头(含鉴权)。 */
16
32
  export declare function apiHeaders(): Record<string, string>;
17
33
  /**
@@ -4,13 +4,16 @@
4
4
  * Kept dependency-free and mostly pure so the root/origin rules are easy to
5
5
  * test. Roots are auto-derived: any top-level directory that contains a
6
6
  * `**\/SKILL.md` (e.g. `skills/**`, `design-templates/**`, `templates/**`,
7
- * `workflows/**`) is treated as a skill root. No hard-coded allowlist.
7
+ * `workflows/**`) is treated as a skill root. No hard-coded allowlist. A
8
+ * `SKILL.md` at the repo root is the empty-string root (`REPO_ROOT`): the
9
+ * whole repo is one skill, named after the repo, with top-level dot entries
10
+ * treated as repo tooling rather than skill content.
8
11
  *
9
12
  * The implementation now lives under `./repo/` (types, discovery, api,
10
13
  * install) and this module is a barrel that re-exports it.
11
14
  */
12
- export { RepoFetchError, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, setGithubToken } from './repo/github-client.ts';
15
+ export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, setGithubToken } from './repo/github-client.ts';
13
16
  export type { RepoRef, RepoTreeItem, RepoFile } from './repo/types.ts';
14
- export { repoSlug, normalizeRepoInput, collectRepoSkillFiles, originForRoot, discoverRepoEntries, skillManifest, skillDirOf, diffRemoteSkills, repoSkillEntry, } from './repo/discovery.ts';
17
+ export { REPO_ROOT, isRepoRoot, repoSlug, normalizeRepoInput, collectRepoSkillFiles, originForRoot, discoverRepoEntries, skillManifest, skillDirOf, skillFileAt, skillPathIn, relativeToSkillDir, diffRemoteSkills, repoSkillEntry, } from './repo/discovery.ts';
15
18
  export { loadRepoTree, getLatestReleaseTag, getRepoStats, listRepoReleases, listRepoBranches, getLatestCommit, loadRepoTreeAt, } from './repo/api.ts';
16
19
  export { downloadGitHubFile, downloadRepoSkill, cleanupLeftoverImportDirs } from './repo/install.ts';
@@ -7,7 +7,7 @@
7
7
  * ./catalog-data.ts(目录装配);对外导出面保持不变。
8
8
  */
9
9
  import type { IncomingMessage, ServerResponse } from 'node:http';
10
- import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
10
+ import type { WebRoute, WebRouteKind } from '@deepseek-ai/dsh-host-webserver';
11
11
  import { type SkillHubRouteDeps } from './deps.ts';
12
12
  export { MAX_JSON_BODY_BYTES, STORE_ERROR_STATUS, isLoopbackRequest, pathExists, queryParam, readJsonBody, readString, readStrings, writeError, writeJson, writeRouteError, } from './http.ts';
13
13
  export { configOf, disabledGate, homeOf, isWritableSource, resolveWritableSkill, savedOf, } from './deps.ts';
@@ -28,6 +28,13 @@ export type RouteHandler = (context: RouteContext) => Promise<void>;
28
28
  /** One declarative route: path + accepted methods + the business handler. */
29
29
  export interface RouteSpec {
30
30
  path: string;
31
+ /**
32
+ * Match kind: 'exact' (default) matches the pathname verbatim; 'prefix'
33
+ * matches the path and everything beneath it. The webserver consults the
34
+ * exact table first and then resolves prefixes longest-first, so a prefix
35
+ * route is a catch-all that can never shadow a real route.
36
+ */
37
+ kind?: WebRouteKind;
31
38
  /** Accepted HTTP methods; anything else answers 405. */
32
39
  methods: readonly ('GET' | 'POST')[];
33
40
  /** POST requests must carry a JSON body (400 when missing/unparseable). */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 市场域路由:market 列表 / source 增删 / ref 锁定 / versions 选择器 /
3
- * check 更新检查 / stats 星标统计 / sync 对齐版本。从 routes.ts 原样搬出,
4
- * handler 逻辑不变。
3
+ * source/check 更新检查 / stats 星标统计 / source/sync 对齐版本。
4
+ * 从 routes.ts 原样搬出,handler 逻辑不变。
5
5
  */
6
6
  import { type RouteSpec, type SkillHubRouteDeps } from './helpers.ts';
7
7
  /** 市场域全部路由 spec(由 routes.ts 经 createRoute 包上统一围栏)。 */
@@ -17,6 +17,6 @@ export type { SkillHubRouteDeps, SkillLookupLike } from './routes/helpers.ts';
17
17
  /**
18
18
  * Build every /api/skill-hub route.
19
19
  * @param deps - skill registry view + sidecar store.
20
- * @returns the exact-path routes.
20
+ * @returns the exact-path routes plus the family's 404 catch-all.
21
21
  */
22
22
  export declare function makeRoutes(deps: SkillHubRouteDeps): WebRoute[];