@khorsheed/dsh-capability-catalog 0.1.95

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +286 -0
  4. package/README.zh.md +138 -0
  5. package/cordis.patch.yml +8 -0
  6. package/lib/client.js +10100 -0
  7. package/lib/index.js +4022 -0
  8. package/lib/invariant.js +25 -0
  9. package/lib/mcpConnector-C2TbkPBY.js +69 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +1417 -0
  12. package/lib/typert.remote-client.d.ts +68 -0
  13. package/lib/typert.remote-client.js +1136 -0
  14. package/lib/types/capabilities.d.ts +103 -0
  15. package/lib/types/capabilities.js +116 -0
  16. package/lib/types/channels.d.ts +41 -0
  17. package/lib/types/channels.js +75 -0
  18. package/lib/types/client/AddMcpDialog.d.ts +13 -0
  19. package/lib/types/client/AddMcpDialog.js +79 -0
  20. package/lib/types/client/AddSkillModal.d.ts +17 -0
  21. package/lib/types/client/AddSkillModal.js +138 -0
  22. package/lib/types/client/BundleFileTree.d.ts +7 -0
  23. package/lib/types/client/BundleFileTree.js +74 -0
  24. package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
  25. package/lib/types/client/CapabilityCatalogCard.js +479 -0
  26. package/lib/types/client/CredentialField.d.ts +14 -0
  27. package/lib/types/client/CredentialField.js +9 -0
  28. package/lib/types/client/McpCards.d.ts +13 -0
  29. package/lib/types/client/McpCards.js +12 -0
  30. package/lib/types/client/McpServerManageModal.d.ts +18 -0
  31. package/lib/types/client/McpServerManageModal.js +63 -0
  32. package/lib/types/client/MetadataRow.d.ts +10 -0
  33. package/lib/types/client/MetadataRow.js +6 -0
  34. package/lib/types/client/ModalShell.d.ts +16 -0
  35. package/lib/types/client/ModalShell.js +27 -0
  36. package/lib/types/client/ModeChips.d.ts +38 -0
  37. package/lib/types/client/ModeChips.js +42 -0
  38. package/lib/types/client/SchemaView.d.ts +11 -0
  39. package/lib/types/client/SchemaView.js +143 -0
  40. package/lib/types/client/SkillCards.d.ts +28 -0
  41. package/lib/types/client/SkillCards.js +43 -0
  42. package/lib/types/client/SkillDetailModal.d.ts +69 -0
  43. package/lib/types/client/SkillDetailModal.js +167 -0
  44. package/lib/types/client/ToolCards.d.ts +28 -0
  45. package/lib/types/client/ToolCards.js +52 -0
  46. package/lib/types/client/ToolDetailModal.d.ts +12 -0
  47. package/lib/types/client/ToolDetailModal.js +29 -0
  48. package/lib/types/client/icons.d.ts +26 -0
  49. package/lib/types/client/icons.js +10 -0
  50. package/lib/types/client/index.d.ts +21 -0
  51. package/lib/types/client/index.js +196 -0
  52. package/lib/types/client/locales.d.ts +392 -0
  53. package/lib/types/client/locales.js +395 -0
  54. package/lib/types/client/mcp-model.d.ts +24 -0
  55. package/lib/types/client/mcp-model.js +87 -0
  56. package/lib/types/client/mode-model.d.ts +108 -0
  57. package/lib/types/client/mode-model.js +140 -0
  58. package/lib/types/client/preset-display.d.ts +21 -0
  59. package/lib/types/client/preset-display.js +20 -0
  60. package/lib/types/client/settle.d.ts +40 -0
  61. package/lib/types/client/settle.js +57 -0
  62. package/lib/types/client/slots.d.ts +86 -0
  63. package/lib/types/client/slots.js +1 -0
  64. package/lib/types/envHint.d.ts +22 -0
  65. package/lib/types/envHint.js +76 -0
  66. package/lib/types/import.d.ts +106 -0
  67. package/lib/types/import.js +635 -0
  68. package/lib/types/index.d.ts +216 -0
  69. package/lib/types/index.js +739 -0
  70. package/lib/types/invariant.d.ts +16 -0
  71. package/lib/types/invariant.js +23 -0
  72. package/lib/types/mcpConnector.d.ts +24 -0
  73. package/lib/types/mcpConnector.js +73 -0
  74. package/lib/types/mcpStore.d.ts +68 -0
  75. package/lib/types/mcpStore.js +235 -0
  76. package/lib/types/mcpTools.d.ts +45 -0
  77. package/lib/types/mcpTools.js +125 -0
  78. package/lib/types/mcps.d.ts +64 -0
  79. package/lib/types/mcps.js +157 -0
  80. package/lib/types/modes.d.ts +65 -0
  81. package/lib/types/modes.js +97 -0
  82. package/lib/types/namespace.d.ts +9 -0
  83. package/lib/types/namespace.js +8 -0
  84. package/lib/types/official-tools.d.ts +3 -0
  85. package/lib/types/official-tools.js +44 -0
  86. package/lib/types/preset-scope.d.ts +105 -0
  87. package/lib/types/preset-scope.js +117 -0
  88. package/lib/types/remote.d.ts +48 -0
  89. package/lib/types/remote.js +120 -0
  90. package/lib/types/scoped-delivery.d.ts +243 -0
  91. package/lib/types/scoped-delivery.js +686 -0
  92. package/lib/types/scoped-edits.d.ts +90 -0
  93. package/lib/types/scoped-edits.js +234 -0
  94. package/lib/types/settings.d.ts +28 -0
  95. package/lib/types/settings.js +28 -0
  96. package/lib/types/shellEnv.d.ts +65 -0
  97. package/lib/types/shellEnv.js +232 -0
  98. package/lib/types/skills.d.ts +103 -0
  99. package/lib/types/skills.js +303 -0
  100. package/lib/types/tool-origin.d.ts +29 -0
  101. package/lib/types/tool-origin.js +30 -0
  102. package/lib/types/types.d.ts +315 -0
  103. package/lib/types/types.js +8 -0
  104. package/lib/types/zip.d.ts +24 -0
  105. package/lib/types/zip.js +80 -0
  106. package/locale/en.json +6 -0
  107. package/locale/zh.json +6 -0
  108. package/package.json +119 -0
  109. package/scripts/gen-official-tools.mts +52 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The capability FINGERPRINT: a canonical form of a catalog snapshot, and the
3
+ * sha256 over it.
4
+ *
5
+ * A catalog snapshot is a listing — ordered by registration, worded for a
6
+ * human, stamped with file mtimes. None of that is the capability face. Two
7
+ * instances that register the same tools in a different order have the same
8
+ * capabilities; an instance whose tool description was reworded has the same
9
+ * capabilities; an instance whose tool gained a parameter does NOT. This
10
+ * module decides which of those differences count, so a condition document
11
+ * can claim `preset: X` and something can check the claim instead of
12
+ * believing it.
13
+ *
14
+ * What enters the fingerprint, and why:
15
+ *
16
+ * | Row | Fields | Left out |
17
+ * |---|---|---|
18
+ * | skill | `name`, `source`, `body` (sha256 of SKILL.md) | `description`, `whenToUse`, `provider`, `updatedAt` |
19
+ * | tool | `name`, `channel`, `parameters` | `description`, `confidence`, `owner` |
20
+ * | mcpServer | `name`, `tools` (its tool names) | tool count (derived) |
21
+ * | channel | the names | the counts (derived from the rows) |
22
+ *
23
+ * The exclusions are deliberate and each one is a claim: **prose is not a
24
+ * capability**. Rewording a tool description, or a skill's routing blurb,
25
+ * changes what the model reads but not what it can do — and a factor that
26
+ * moves when someone fixes a typo is a factor nobody can hold still across a
27
+ * run. A skill's BODY is the opposite: it is the procedure the model
28
+ * executes, so it enters as a sha. `updatedAt` is a filesystem fact, not a
29
+ * capability: touching a file must not mint a new subject.
30
+ *
31
+ * Determinism is the whole product. Every list is sorted by name, and the
32
+ * digest is taken over canonical JSON (sorted keys, no whitespace), so the
33
+ * same capability face hashes the same on two machines and in two processes.
34
+ * @module @khorsheed/dsh-capability-catalog/capabilities
35
+ */
36
+ import type { CapabilityCatalogSnapshot, CatalogJsonValue } from './types.ts';
37
+ /** Prefix of the human-facing capability tag (`caps:<sha256>`). */
38
+ export declare const CAPS_TAG_PREFIX = "caps:";
39
+ /** One skill as the fingerprint sees it. */
40
+ export interface CanonicalSkill {
41
+ readonly name: string;
42
+ readonly source: string;
43
+ /** sha256 of the SKILL.md body, or null when the skill has no readable body. */
44
+ readonly body: string | null;
45
+ }
46
+ /** One tool as the fingerprint sees it. */
47
+ export interface CanonicalTool {
48
+ readonly name: string;
49
+ readonly channel: string;
50
+ /** The model-facing parameter schema, or null when the tool declares none. */
51
+ readonly parameters: CatalogJsonValue | null;
52
+ }
53
+ /** One MCP server as the fingerprint sees it. */
54
+ export interface CanonicalMcpServer {
55
+ readonly name: string;
56
+ /** The server's tool names, sorted. */
57
+ readonly tools: readonly string[];
58
+ }
59
+ /** The canonical capability face of one snapshot. */
60
+ export interface CanonicalCapabilities {
61
+ readonly skills: readonly CanonicalSkill[];
62
+ readonly tools: readonly CanonicalTool[];
63
+ readonly mcpServers: readonly CanonicalMcpServer[];
64
+ readonly channels: readonly string[];
65
+ }
66
+ /**
67
+ * Canonical JSON: object keys sorted (code-unit order), no whitespace, arrays
68
+ * in document order. Two documents differing only in key order canonicalize
69
+ * alike.
70
+ *
71
+ * Deliberately duplicated rather than imported from `@khorsheed/dsh-eval`:
72
+ * community plugins never depend on siblings, and the two copies are pinned
73
+ * against each other by the eval contract tests that consume a `caps:` tag.
74
+ * @param value - any JSON value.
75
+ * @returns its canonical serialization.
76
+ */
77
+ export declare function canonicalJson(value: unknown): string;
78
+ /**
79
+ * Project a snapshot onto its canonical capability face.
80
+ *
81
+ * Pure and total: it reads nothing but the snapshot, and a row missing an
82
+ * optional field contributes `null` rather than disappearing — a tool with no
83
+ * parameter schema is a different capability from one whose schema is `{}`,
84
+ * and both are different from the tool being absent.
85
+ * @param snapshot - a catalog snapshot (its own `sha` field, if any, is ignored).
86
+ * @returns the canonical form the digest is taken over.
87
+ */
88
+ export declare function canonicalCapabilities(snapshot: CapabilityCatalogSnapshot): CanonicalCapabilities;
89
+ /**
90
+ * The capability hash: sha256 hex of the canonical form's canonical JSON.
91
+ * @param snapshot - a catalog snapshot.
92
+ * @returns the 64-hex digest.
93
+ */
94
+ export declare function hashOf(snapshot: CapabilityCatalogSnapshot): string;
95
+ /**
96
+ * The human-facing tag form of a capability hash.
97
+ * @param sha - a sha256 hex digest (or the hash of a snapshot).
98
+ * @returns `caps:<sha>`.
99
+ */
100
+ export declare function capsTag(sha: string): string;
101
+ /** sha256 hex of a skill body, the form {@link CanonicalSkill.body} carries. */
102
+ export declare function hashSkillBody(content: string): string;
103
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The capability FINGERPRINT: a canonical form of a catalog snapshot, and the
3
+ * sha256 over it.
4
+ *
5
+ * A catalog snapshot is a listing — ordered by registration, worded for a
6
+ * human, stamped with file mtimes. None of that is the capability face. Two
7
+ * instances that register the same tools in a different order have the same
8
+ * capabilities; an instance whose tool description was reworded has the same
9
+ * capabilities; an instance whose tool gained a parameter does NOT. This
10
+ * module decides which of those differences count, so a condition document
11
+ * can claim `preset: X` and something can check the claim instead of
12
+ * believing it.
13
+ *
14
+ * What enters the fingerprint, and why:
15
+ *
16
+ * | Row | Fields | Left out |
17
+ * |---|---|---|
18
+ * | skill | `name`, `source`, `body` (sha256 of SKILL.md) | `description`, `whenToUse`, `provider`, `updatedAt` |
19
+ * | tool | `name`, `channel`, `parameters` | `description`, `confidence`, `owner` |
20
+ * | mcpServer | `name`, `tools` (its tool names) | tool count (derived) |
21
+ * | channel | the names | the counts (derived from the rows) |
22
+ *
23
+ * The exclusions are deliberate and each one is a claim: **prose is not a
24
+ * capability**. Rewording a tool description, or a skill's routing blurb,
25
+ * changes what the model reads but not what it can do — and a factor that
26
+ * moves when someone fixes a typo is a factor nobody can hold still across a
27
+ * run. A skill's BODY is the opposite: it is the procedure the model
28
+ * executes, so it enters as a sha. `updatedAt` is a filesystem fact, not a
29
+ * capability: touching a file must not mint a new subject.
30
+ *
31
+ * Determinism is the whole product. Every list is sorted by name, and the
32
+ * digest is taken over canonical JSON (sorted keys, no whitespace), so the
33
+ * same capability face hashes the same on two machines and in two processes.
34
+ * @module @khorsheed/dsh-capability-catalog/capabilities
35
+ */
36
+ import { createHash } from 'node:crypto';
37
+ /** Prefix of the human-facing capability tag (`caps:<sha256>`). */
38
+ export const CAPS_TAG_PREFIX = 'caps:';
39
+ /**
40
+ * Canonical JSON: object keys sorted (code-unit order), no whitespace, arrays
41
+ * in document order. Two documents differing only in key order canonicalize
42
+ * alike.
43
+ *
44
+ * Deliberately duplicated rather than imported from `@khorsheed/dsh-eval`:
45
+ * community plugins never depend on siblings, and the two copies are pinned
46
+ * against each other by the eval contract tests that consume a `caps:` tag.
47
+ * @param value - any JSON value.
48
+ * @returns its canonical serialization.
49
+ */
50
+ export function canonicalJson(value) {
51
+ if (value === null || typeof value !== 'object')
52
+ return JSON.stringify(value) ?? 'null';
53
+ if (Array.isArray(value))
54
+ return `[${value.map(canonicalJson).join(',')}]`;
55
+ const obj = value;
56
+ return `{${Object.keys(obj).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(',')}}`;
57
+ }
58
+ /** Sort by `name`, code-unit order (the one ordering rule of this module). */
59
+ function byName(rows) {
60
+ return [...rows].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
61
+ }
62
+ /**
63
+ * Project a snapshot onto its canonical capability face.
64
+ *
65
+ * Pure and total: it reads nothing but the snapshot, and a row missing an
66
+ * optional field contributes `null` rather than disappearing — a tool with no
67
+ * parameter schema is a different capability from one whose schema is `{}`,
68
+ * and both are different from the tool being absent.
69
+ * @param snapshot - a catalog snapshot (its own `sha` field, if any, is ignored).
70
+ * @returns the canonical form the digest is taken over.
71
+ */
72
+ export function canonicalCapabilities(snapshot) {
73
+ const tools = byName(snapshot.tools);
74
+ return {
75
+ skills: byName(snapshot.skills).map(skill => ({
76
+ name: skill.name,
77
+ source: skill.source,
78
+ body: skill.bodySha ?? null,
79
+ })),
80
+ tools: tools.map(tool => ({
81
+ name: tool.name,
82
+ channel: tool.channel,
83
+ parameters: tool.parameters ?? null,
84
+ })),
85
+ // The server's tool NAMES, not its tool count: a server that swapped one
86
+ // tool for another keeps its count and changes its capability face.
87
+ mcpServers: byName(snapshot.mcpServers).map(server => ({
88
+ name: server.name,
89
+ tools: tools.filter(tool => tool.serverName === server.name).map(tool => tool.name),
90
+ })),
91
+ // Names only — the counts are derived from the rows above, and a summary
92
+ // that disagreed with them would make the digest depend on which of the
93
+ // two was right.
94
+ channels: [...snapshot.channels].map(channel => channel.channel).sort(),
95
+ };
96
+ }
97
+ /**
98
+ * The capability hash: sha256 hex of the canonical form's canonical JSON.
99
+ * @param snapshot - a catalog snapshot.
100
+ * @returns the 64-hex digest.
101
+ */
102
+ export function hashOf(snapshot) {
103
+ return createHash('sha256').update(canonicalJson(canonicalCapabilities(snapshot))).digest('hex');
104
+ }
105
+ /**
106
+ * The human-facing tag form of a capability hash.
107
+ * @param sha - a sha256 hex digest (or the hash of a snapshot).
108
+ * @returns `caps:<sha>`.
109
+ */
110
+ export function capsTag(sha) {
111
+ return `${CAPS_TAG_PREFIX}${sha}`;
112
+ }
113
+ /** sha256 hex of a skill body, the form {@link CanonicalSkill.body} carries. */
114
+ export function hashSkillBody(content) {
115
+ return createHash('sha256').update(content, 'utf8').digest('hex');
116
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tool channel attribution and skill source labels.
3
+ *
4
+ * Tools have no source field on `ToolDefinition`, so the catalog infers the
5
+ * channel from three signals (see the proposal "工具归因" section):
6
+ * 1. the `mcp__` prefix (exact),
7
+ * 2. the generated official-tools whitelist (exact),
8
+ * 3. baseline-diff inference (`builtin?` / `plugin`).
9
+ *
10
+ * MCP serverName is resolved by longest-match: the mcp-client server-name
11
+ * charset permits underscores (`[A-Za-z0-9_-]{1,32}`), so `mcp__a__b__tool`
12
+ * could belong to server `a` or `a__b`; the longest configured prefix that
13
+ * leaves a non-empty raw tool name wins.
14
+ * @module @khorsheed/dsh-capability-catalog/channels
15
+ */
16
+ import type { CatalogToolChannel } from './types.ts';
17
+ import type { ToolOrigin } from './tool-origin.ts';
18
+ /** The `mcp__` prefix mcp-client stamps on every tool it registers. */
19
+ export declare const MCP_TOOL_PREFIX = "mcp__";
20
+ /** Tool names this plugin itself registers (self-attribution). */
21
+ export declare const SELF_TOOL_NAMES: readonly ['list_capabilities'];
22
+ /** One tool's channel attribution result. */
23
+ export interface ToolAttribution {
24
+ readonly channel: CatalogToolChannel;
25
+ readonly confidence: 'exact' | 'inferred';
26
+ readonly serverName?: string;
27
+ readonly owner?: string;
28
+ }
29
+ /**
30
+ * Resolve the channel for one tool name.
31
+ * @param name - wire tool name.
32
+ * @param officialTools - official-tools whitelist (Set).
33
+ * @param mcpServerNames - configured MCP serverNames (longest first).
34
+ * @param appearedAfterApply - whether the tool appeared after apply-time baseline.
35
+ */
36
+ export declare function attributeToolChannel(name: string, officialTools: ReadonlySet<string>, mcpServerNames: readonly string[], appearedAfterApply: boolean, toolOrigins?: ReadonlyMap<string, ToolOrigin>): ToolAttribution;
37
+ /** Longest-match MCP serverName resolution. */
38
+ export declare function resolveMcpServerName(name: string, serverNames: readonly string[]): string | undefined;
39
+ /** Human label for a skill channel bucket. */
40
+ export declare function skillChannelLabel(source: string): string;
41
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tool channel attribution and skill source labels.
3
+ *
4
+ * Tools have no source field on `ToolDefinition`, so the catalog infers the
5
+ * channel from three signals (see the proposal "工具归因" section):
6
+ * 1. the `mcp__` prefix (exact),
7
+ * 2. the generated official-tools whitelist (exact),
8
+ * 3. baseline-diff inference (`builtin?` / `plugin`).
9
+ *
10
+ * MCP serverName is resolved by longest-match: the mcp-client server-name
11
+ * charset permits underscores (`[A-Za-z0-9_-]{1,32}`), so `mcp__a__b__tool`
12
+ * could belong to server `a` or `a__b`; the longest configured prefix that
13
+ * leaves a non-empty raw tool name wins.
14
+ * @module @khorsheed/dsh-capability-catalog/channels
15
+ */
16
+ /** The `mcp__` prefix mcp-client stamps on every tool it registers. */
17
+ export const MCP_TOOL_PREFIX = 'mcp__';
18
+ /** Tool names this plugin itself registers (self-attribution). */
19
+ export const SELF_TOOL_NAMES = ['list_capabilities'];
20
+ /**
21
+ * Resolve the channel for one tool name.
22
+ * @param name - wire tool name.
23
+ * @param officialTools - official-tools whitelist (Set).
24
+ * @param mcpServerNames - configured MCP serverNames (longest first).
25
+ * @param appearedAfterApply - whether the tool appeared after apply-time baseline.
26
+ */
27
+ export function attributeToolChannel(name, officialTools, mcpServerNames, appearedAfterApply, toolOrigins = new Map()) {
28
+ if (name.startsWith(MCP_TOOL_PREFIX)) {
29
+ const serverName = resolveMcpServerName(name, mcpServerNames);
30
+ return { channel: 'mcp', confidence: 'exact', ...(serverName !== undefined ? { serverName } : {}) };
31
+ }
32
+ if (SELF_TOOL_NAMES.includes(name)) {
33
+ return { channel: 'plugin', confidence: 'exact', owner: '@khorsheed/dsh-capability-catalog' };
34
+ }
35
+ // A tagged origin is authoritatively declared by the tool author — honor it
36
+ // before the official whitelist / baseline fallback.
37
+ const origin = toolOrigins.get(name);
38
+ if (origin !== undefined) {
39
+ return { channel: origin.channel, confidence: 'exact', ...(origin.owner !== undefined ? { owner: origin.owner } : {}) };
40
+ }
41
+ if (officialTools.has(name)) {
42
+ return { channel: 'builtin', confidence: 'exact' };
43
+ }
44
+ if (!appearedAfterApply) {
45
+ return { channel: 'builtin', confidence: 'inferred' };
46
+ }
47
+ return { channel: 'plugin', confidence: 'inferred' };
48
+ }
49
+ /** Longest-match MCP serverName resolution. */
50
+ export function resolveMcpServerName(name, serverNames) {
51
+ const candidates = [...serverNames].sort((a, b) => b.length - a.length);
52
+ for (const server of candidates) {
53
+ const prefix = `${MCP_TOOL_PREFIX}${server}__`;
54
+ if (name.startsWith(prefix) && name.length > prefix.length)
55
+ return server;
56
+ }
57
+ const rest = name.slice(MCP_TOOL_PREFIX.length);
58
+ const separator = rest.indexOf('__');
59
+ if (separator > 0)
60
+ return rest.slice(0, separator);
61
+ return undefined;
62
+ }
63
+ /** Human label for a skill channel bucket. */
64
+ export function skillChannelLabel(source) {
65
+ switch (source) {
66
+ case 'bundled': return '官方 · bundled';
67
+ case 'runtime': return '插件 · runtime';
68
+ case 'project-dsh': return '项目 · .dsh';
69
+ case 'project-agents': return '项目 · .agents';
70
+ case 'custom': return '自定义';
71
+ case 'user-dsh': return '用户 · dsh';
72
+ case 'user-agents': return '用户 · agents';
73
+ default: return source;
74
+ }
75
+ }
@@ -0,0 +1,13 @@
1
+ import type { CatalogMcpServerConfig, CatalogMcpTool } from '@khorsheed/dsh-capability-catalog/types';
2
+ import type { CapabilityCatalogKey } from './locales.ts';
3
+ /** Add-MCP dialog: paste a server config → parse it into a server card →
4
+ * configure credentials + enable → add. One server per add. */
5
+ export declare function AddMcpDialog({ onClose, mcpAdd, mcpSetCredential, mcpDiscover, refreshMcp, t }: {
6
+ onClose: () => void;
7
+ mcpAdd: (config: CatalogMcpServerConfig) => Promise<boolean>;
8
+ mcpSetCredential: (ref: string, value: string) => Promise<boolean>;
9
+ mcpDiscover: (serverName: string) => Promise<readonly CatalogMcpTool[]>;
10
+ refreshMcp: () => Promise<void>;
11
+ t: (key: CapabilityCatalogKey) => string;
12
+ }): import("react").JSX.Element;
13
+ //# sourceMappingURL=AddMcpDialog.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { credentialStoredRefs, parseServerEntry } from '../mcps.js';
4
+ import { ModalShell } from './ModalShell.js';
5
+ import { configDisplay, transportLabel } from './mcp-model.js';
6
+ import css from './CapabilityCatalogCard.module.css';
7
+ /** Extract a single server entry from a pasted snippet, plus its suggested
8
+ * name. Accepts a bare entry (`command`/`url`), the `mcpServers` wrapper, or a
9
+ * bare `{name: entry}` map; picks the first entry. */
10
+ function extractServerPreamble(raw) {
11
+ let json;
12
+ try {
13
+ json = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ if (typeof json !== 'object' || json === null)
19
+ return null;
20
+ const obj = json;
21
+ if (typeof obj['command'] === 'string' || typeof obj['url'] === 'string') {
22
+ return { entry: obj, suggestedName: '' };
23
+ }
24
+ const container = (obj['mcpServers'] ?? obj);
25
+ const firstKey = Object.keys(container).find((k) => typeof container[k] === 'object' && container[k] !== null);
26
+ if (firstKey === undefined)
27
+ return null;
28
+ return { entry: container[firstKey], suggestedName: firstKey };
29
+ }
30
+ /** Add-MCP dialog: paste a server config → parse it into a server card →
31
+ * configure credentials + enable → add. One server per add. */
32
+ export function AddMcpDialog({ onClose, mcpAdd, mcpSetCredential, mcpDiscover, refreshMcp, t }) {
33
+ const [raw, setRaw] = useState('');
34
+ const [serverName, setServerName] = useState('');
35
+ const [credValues, setCredValues] = useState({});
36
+ const [enabled, setEnabled] = useState(false);
37
+ const [busy, setBusy] = useState(false);
38
+ const [msg, setMsg] = useState(null);
39
+ const preamble = useMemo(() => (raw.trim() === '' ? null : extractServerPreamble(raw)), [raw]);
40
+ const parseError = raw.trim() !== '' && preamble === null;
41
+ const finalName = serverName.trim() !== '' ? serverName.trim() : (preamble?.suggestedName ?? '');
42
+ const parsed = useMemo(() => {
43
+ if (preamble === null || finalName === '')
44
+ return null;
45
+ return parseServerEntry(finalName, preamble.entry);
46
+ }, [preamble, finalName]);
47
+ // Seed the name field once a suggestion appears (does not overwrite edits).
48
+ useEffect(() => {
49
+ if (serverName === '' && preamble !== null && preamble.suggestedName !== '') {
50
+ setServerName(preamble.suggestedName);
51
+ }
52
+ }, [preamble, serverName]);
53
+ const canSubmit = parsed !== null && finalName !== '';
54
+ const submit = async () => {
55
+ if (parsed === null || busy)
56
+ return;
57
+ setBusy(true);
58
+ setMsg(null);
59
+ const config = { ...parsed.config, enabled };
60
+ const ok = await mcpAdd(config);
61
+ if (!ok) {
62
+ setMsg({ ok: false, text: t('addError') });
63
+ setBusy(false);
64
+ return;
65
+ }
66
+ const refs = credentialStoredRefs(config.serverName, config);
67
+ for (const cred of parsed.credentials) {
68
+ const value = credValues[cred.ref] ?? '';
69
+ if (value === '')
70
+ continue;
71
+ await mcpSetCredential(refs[cred.ref] ?? cred.ref, value);
72
+ }
73
+ if (enabled)
74
+ await mcpDiscover(config.serverName);
75
+ await refreshMcp();
76
+ onClose();
77
+ };
78
+ return (_jsxs(ModalShell, { title: t('mcpAddTitled'), onClose: onClose, className: css.addModal, closeOnMask: false, t: t, children: [_jsx("p", { className: css.confHint, children: t('mcpAddHint') }), _jsx("textarea", { className: css.textarea, rows: 6, value: raw, onChange: (e) => setRaw(e.target.value), placeholder: t('mcpPastePlaceholder'), spellCheck: false }), parseError ? _jsx("div", { className: `${css.addMsg} ${css.addMsgErr}`, children: t('mcpParseFailed') }) : null, parsed === null ? null : (_jsxs("div", { className: css.addPanel, children: [_jsx("label", { className: css.fieldLabel, children: t('mcpServerName') }), _jsx("input", { className: css.input, value: serverName, onChange: (e) => setServerName(e.target.value), placeholder: preamble?.suggestedName !== '' && preamble?.suggestedName !== undefined ? preamble.suggestedName : t('mcpServerNamePlaceholder'), spellCheck: false }), _jsxs("div", { className: css.meta, children: [_jsx("span", { className: css.metaKey, children: t('mcpTransport') }), _jsx("span", { className: css.metaVal, children: transportLabel(t, parsed.config.transport) })] }), _jsxs("div", { children: [_jsx("div", { className: css.mcpBlockLabel, children: t('mcpConfig') }), _jsx("pre", { className: css.mcpConfig, children: configDisplay({ ...parsed.config, enabled }) })] }), parsed.credentials.length > 0 ? (_jsxs("div", { children: [_jsx("div", { className: css.confHint, children: t('mcpNeedsCred') }), parsed.credentials.map((cred) => (_jsxs("div", { className: css.credRow, children: [_jsxs("label", { className: css.credLabel, children: [cred.label, _jsx("span", { className: `${css.badge} ${css.badgeWarn}`, children: t('mcpCredBadge') })] }), _jsx("div", { className: css.credInputRow, children: _jsx("div", { className: css.inputWrap, children: _jsx("input", { className: css.input, type: "password", value: credValues[cred.ref] ?? '', placeholder: t('credPlaceholder'), onChange: (e) => setCredValues((s) => ({ ...s, [cred.ref]: e.target.value })) }) }) })] }, cred.ref)))] })) : null, _jsxs("label", { className: css.switchRow, children: [_jsx("span", { className: css.enableLabel, children: t('mcpEnabled') }), _jsxs("span", { className: css.switch, children: [_jsx("input", { type: "checkbox", checked: enabled, onChange: (e) => setEnabled(e.target.checked) }), _jsx("span", { className: css.track }), _jsx("span", { className: css.thumb })] })] }), msg !== null ? _jsx("div", { className: `${css.addMsg} ${msg.ok ? css.addMsgOk : css.addMsgErr}`, children: msg.text }) : null] })), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.btnGhost, onClick: onClose, children: t('cancel') }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.btnPrimary, disabled: !canSubmit || busy, onClick: () => void submit(), children: t('mcpAdd') })] })] }));
79
+ }
@@ -0,0 +1,17 @@
1
+ import type { CapabilityCatalogSnapshot, CatalogAddSkillRequest, CatalogDirSkillInfo } from '@khorsheed/dsh-capability-catalog/types';
2
+ import type { CapabilityCatalogKey } from './locales.ts';
3
+ /** Add-skill modal: file upload (drag-drop), clone-from-source, or local dir. */
4
+ export declare function AddSkillModal({ onClose, addSkill, listDirSkills, pickDirectory, refreshSettled, t }: {
5
+ onClose: () => void;
6
+ addSkill: (request: CatalogAddSkillRequest) => Promise<{
7
+ ok: boolean;
8
+ error?: string;
9
+ name?: string;
10
+ exists?: boolean;
11
+ }>;
12
+ listDirSkills: (dirPath: string) => Promise<readonly CatalogDirSkillInfo[]>;
13
+ pickDirectory: () => Promise<string | null>;
14
+ refreshSettled: (settled: (snapshot: CapabilityCatalogSnapshot) => boolean) => Promise<boolean>;
15
+ t: (key: CapabilityCatalogKey) => string;
16
+ }): import("react").JSX.Element;
17
+ //# sourceMappingURL=AddSkillModal.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { IconFolderOpenOutlineMedium } from './icons.js';
5
+ import { ModalShell } from './ModalShell.js';
6
+ import { installedNames } from './settle.js';
7
+ import css from './CapabilityCatalogCard.module.css';
8
+ /** Add-skill modal: file upload (drag-drop), clone-from-source, or local dir. */
9
+ export function AddSkillModal({ onClose, addSkill, listDirSkills, pickDirectory, refreshSettled, t }) {
10
+ const [tab, setTab] = useState('upload');
11
+ const [zipBase64, setZipBase64] = useState(null);
12
+ const [zipName, setZipName] = useState('');
13
+ const [dragging, setDragging] = useState(false);
14
+ const [command, setCommand] = useState('');
15
+ const [dir, setDir] = useState('');
16
+ const [dirSkills, setDirSkills] = useState([]);
17
+ const [selectedSkills, setSelectedSkills] = useState([]);
18
+ const [modelInvocable, setModelInvocable] = useState(true);
19
+ const [root, setRoot] = useState('user');
20
+ const [busy, setBusy] = useState(false);
21
+ const [msg, setMsg] = useState(null);
22
+ const [confirm, setConfirm] = useState(null);
23
+ const rootPath = root === 'user' ? t('rootUserPath') : t('rootProjectPath');
24
+ const canSubmit = tab === 'upload' ? zipBase64 !== null : tab === 'command' ? command.trim() !== '' : dir !== '' && dirSkills.length > 0 && selectedSkills.length > 0;
25
+ // Tabs are independent: switching clears the previous tab's message so a
26
+ // warning on one tab never bleeds into another.
27
+ const switchTab = (next) => {
28
+ setTab(next);
29
+ setMsg(null);
30
+ };
31
+ // One picker: open the host's native directory chooser, then auto-parse the
32
+ // chosen directory for skills (list, or report none found).
33
+ const browseDir = async () => {
34
+ setMsg(null);
35
+ const path = await pickDirectory();
36
+ if (path === null || path === '')
37
+ return;
38
+ setDir(path);
39
+ const skills = await listDirSkills(path);
40
+ setDirSkills(skills);
41
+ setSelectedSkills(skills.map(s => s.name));
42
+ if (skills.length === 0)
43
+ setMsg({ ok: false, text: t('noSkillsFound') });
44
+ };
45
+ const toggleSkill = (name) => {
46
+ setSelectedSkills(prev => prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]);
47
+ };
48
+ const readFile = (file) => {
49
+ const reader = new FileReader();
50
+ reader.onload = () => setZipBase64(arrayBufferToBase64(reader.result));
51
+ reader.onerror = () => setMsg({ ok: false, text: t('readFailed') });
52
+ reader.readAsArrayBuffer(file);
53
+ setZipName(file.name);
54
+ };
55
+ const onFile = (e) => {
56
+ const file = e.target.files?.[0];
57
+ if (file !== undefined)
58
+ readFile(file);
59
+ };
60
+ const onDrop = (e) => {
61
+ e.preventDefault();
62
+ setDragging(false);
63
+ const file = e.dataTransfer.files?.[0];
64
+ if (file !== undefined)
65
+ readFile(file);
66
+ };
67
+ /**
68
+ * Wait for the installed skill(s) to reach the catalog snapshot. The host
69
+ * writes the files before its watcher invalidates the registry, so a single
70
+ * refresh here returns the pre-install snapshot and the new card stays absent
71
+ * until the panel is reopened.
72
+ */
73
+ const settleInstalled = async (name) => {
74
+ const wanted = installedNames(name);
75
+ await refreshSettled(s => wanted.every(n => s.skills.some(skill => skill.name === n)));
76
+ };
77
+ const submit = async () => {
78
+ if (!canSubmit || busy)
79
+ return;
80
+ setBusy(true);
81
+ setMsg(null);
82
+ const req = (() => {
83
+ if (tab === 'upload')
84
+ return { channel: 'zip', payload: zipBase64, modelInvocable, root };
85
+ if (tab === 'command')
86
+ return { channel: 'command', payload: '', repo: command, modelInvocable, root };
87
+ // From-directory: a `kind:'self'` entry is the picked dir itself (a single
88
+ // skill bundle) and is installed with repo=<dir> and no `skills`. Child
89
+ // entries are sub-dir skills of a container and go in `skills:[name]`.
90
+ const selfNames = dirSkills.filter(s => s.kind === 'self').map(s => s.name);
91
+ const childSkills = selectedSkills.filter(n => !selfNames.includes(n));
92
+ const selfOnly = selfNames.some(n => selectedSkills.includes(n)) && childSkills.length === 0;
93
+ return selfOnly
94
+ ? { channel: 'command', payload: '', repo: dir, modelInvocable, root }
95
+ : { channel: 'command', payload: '', repo: dir, modelInvocable, root, ...(childSkills.length > 0 ? { skills: childSkills } : {}) };
96
+ })();
97
+ const res = await addSkill(req);
98
+ if (res.ok) {
99
+ await settleInstalled(res.name);
100
+ setMsg({ ok: true, text: `${t('addSuccess')}${res.name ?? ''}` });
101
+ }
102
+ else if (res.exists === true) {
103
+ // Soft refusal: a same-name skill already lives in the target root. Offer overwrite.
104
+ setConfirm({ name: res.name ?? '', req });
105
+ }
106
+ else {
107
+ setMsg({ ok: false, text: res.error ?? t('addError') });
108
+ }
109
+ setBusy(false);
110
+ };
111
+ const overwrite = async () => {
112
+ if (confirm === null || busy)
113
+ return;
114
+ const req = confirm.req;
115
+ setConfirm(null);
116
+ setBusy(true);
117
+ setMsg(null);
118
+ const res = await addSkill({ ...req, overwrite: true });
119
+ if (res.ok) {
120
+ await settleInstalled(res.name);
121
+ setMsg({ ok: true, text: `${t('addSuccess')}${res.name ?? ''}` });
122
+ }
123
+ else {
124
+ setMsg({ ok: false, text: res.error ?? t('addError') });
125
+ }
126
+ setBusy(false);
127
+ };
128
+ return (_jsxs(_Fragment, { children: [_jsxs(ModalShell, { title: t('addSkill'), onClose: onClose, className: css.addModal, closeOnMask: false, t: t, children: [_jsxs("div", { className: `${css.tabs} ${css.innerTabs}`, role: "tablist", children: [_jsx("button", { type: "button", className: css.tab, "data-active": tab === 'upload', role: "tab", onClick: () => switchTab('upload'), children: t('addTabUpload') }), _jsx("button", { type: "button", className: css.tab, "data-active": tab === 'command', role: "tab", onClick: () => switchTab('command'), children: t('addTabCommand') }), _jsx("button", { type: "button", className: css.tab, "data-active": tab === 'localdir', role: "tab", onClick: () => switchTab('localdir'), children: t('addTabDir') })] }), _jsxs("div", { className: css.addPanel, children: [tab === 'upload' ? (_jsxs("label", { className: `${css.dropzone} ${dragging ? css.dragging : ''}`, onDragOver: (e) => { e.preventDefault(); setDragging(true); }, onDragLeave: () => setDragging(false), onDrop: onDrop, children: [_jsx("span", { className: css.dropzoneIcon, children: _jsx(IconFolderOpenOutlineMedium, { size: 28 }) }), _jsx("div", { className: css.dropTitle, children: t('dropTitle') }), _jsx("div", { className: css.dropHint, children: t('dropHint') }), _jsx("input", { type: "file", accept: ".zip,.md", className: css.fileInput, onChange: onFile }), zipName !== '' ? _jsx("div", { className: css.fileName, children: zipName }) : null] })) : null, tab === 'command' ? (_jsxs("div", { className: css.command, children: [_jsx("label", { className: css.fieldLabel, children: t('commandLabel') }), _jsx("input", { className: css.input, value: command, onChange: (e) => setCommand(e.target.value), placeholder: t('commandPlaceholder'), spellCheck: false }), _jsx("p", { className: css.confHint, children: t('commandHint') })] })) : null, tab === 'localdir' ? (_jsxs("div", { className: css.command, children: [_jsxs("div", { className: css.dirHintRow, children: [_jsx("button", { type: "button", className: css.btnGhost, onClick: () => void browseDir(), children: t('browseDir') }), _jsx("span", { className: css.dirHintInline, children: t('dirHint') })] }), dir !== '' ? _jsx("p", { className: css.dirPath, children: dir }) : null, dirSkills.length > 0 ? (_jsxs("div", { className: css.skillPick, children: [_jsx("div", { className: css.fieldLabel, children: t('pickSkills') }), dirSkills.map((s) => (_jsxs("label", { className: css.pickRow, children: [_jsxs("span", { className: css.check, children: [_jsx("input", { type: "checkbox", checked: selectedSkills.includes(s.name), onChange: () => toggleSkill(s.name) }), _jsx("span", { className: css.checkMark })] }), _jsxs("div", { className: css.pickText, children: [_jsx("div", { className: css.pickName, children: s.name }), _jsx("div", { className: css.pickDesc, children: s.description })] })] }, s.name)))] })) : null] })) : null, _jsxs("label", { className: css.switchRow, children: [_jsx("span", { className: css.enableLabel, children: t('addModelInvocable') }), _jsxs("span", { className: css.switch, children: [_jsx("input", { type: "checkbox", checked: modelInvocable, onChange: (e) => setModelInvocable(e.target.checked) }), _jsx("span", { className: css.track }), _jsx("span", { className: css.thumb })] })] }), _jsxs("label", { className: css.rootRow, children: [_jsx("span", { className: css.enableLabel, children: t('addRoot') }), _jsxs("select", { className: css.select, value: root, onChange: (e) => setRoot(e.target.value), children: [_jsx("option", { value: "user", children: t('rootUser') }), _jsx("option", { value: "project", children: t('rootProject') })] }), _jsx("span", { className: css.fieldPath, children: rootPath })] }), msg !== null ? _jsx("div", { className: `${css.addMsg} ${msg.ok ? css.addMsgOk : css.addMsgErr}`, children: msg.text }) : null] }), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.btnGhost, onClick: onClose, children: t('cancel') }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.btnPrimary, disabled: !canSubmit || busy, onClick: () => void submit(), children: t('addSubmit') })] })] }), confirm !== null ? (_jsx(Modal, { open: true, onClose: () => setConfirm(null), title: t('addExistsTitle'), closeLabel: t('close'), description: `「${confirm.name}」${t('addExists')}`, footer: (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: () => setConfirm(null), children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: busy, onClick: () => void overwrite(), children: t('replace') })] })) })) : null] }));
129
+ }
130
+ /** Encode an ArrayBuffer to base64 (chunked to avoid stack overflow). */
131
+ function arrayBufferToBase64(buf) {
132
+ const bytes = new Uint8Array(buf);
133
+ let binary = '';
134
+ const chunk = 0x8000;
135
+ for (let i = 0; i < bytes.length; i += chunk)
136
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
137
+ return btoa(binary);
138
+ }
@@ -0,0 +1,7 @@
1
+ /** Skill-bundle file tree (IDE-explorer anatomy: chevron, folder, indent). */
2
+ export declare function BundleFileTree({ files, selectedPath, onSelect }: {
3
+ files: readonly string[];
4
+ selectedPath: string;
5
+ onSelect: (path: string) => void;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=BundleFileTree.d.ts.map