@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +286 -0
- package/README.zh.md +138 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +10100 -0
- package/lib/index.js +4022 -0
- package/lib/invariant.js +25 -0
- package/lib/mcpConnector-C2TbkPBY.js +69 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1417 -0
- package/lib/typert.remote-client.d.ts +68 -0
- package/lib/typert.remote-client.js +1136 -0
- package/lib/types/capabilities.d.ts +103 -0
- package/lib/types/capabilities.js +116 -0
- package/lib/types/channels.d.ts +41 -0
- package/lib/types/channels.js +75 -0
- package/lib/types/client/AddMcpDialog.d.ts +13 -0
- package/lib/types/client/AddMcpDialog.js +79 -0
- package/lib/types/client/AddSkillModal.d.ts +17 -0
- package/lib/types/client/AddSkillModal.js +138 -0
- package/lib/types/client/BundleFileTree.d.ts +7 -0
- package/lib/types/client/BundleFileTree.js +74 -0
- package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
- package/lib/types/client/CapabilityCatalogCard.js +479 -0
- package/lib/types/client/CredentialField.d.ts +14 -0
- package/lib/types/client/CredentialField.js +9 -0
- package/lib/types/client/McpCards.d.ts +13 -0
- package/lib/types/client/McpCards.js +12 -0
- package/lib/types/client/McpServerManageModal.d.ts +18 -0
- package/lib/types/client/McpServerManageModal.js +63 -0
- package/lib/types/client/MetadataRow.d.ts +10 -0
- package/lib/types/client/MetadataRow.js +6 -0
- package/lib/types/client/ModalShell.d.ts +16 -0
- package/lib/types/client/ModalShell.js +27 -0
- package/lib/types/client/ModeChips.d.ts +38 -0
- package/lib/types/client/ModeChips.js +42 -0
- package/lib/types/client/SchemaView.d.ts +11 -0
- package/lib/types/client/SchemaView.js +143 -0
- package/lib/types/client/SkillCards.d.ts +28 -0
- package/lib/types/client/SkillCards.js +43 -0
- package/lib/types/client/SkillDetailModal.d.ts +69 -0
- package/lib/types/client/SkillDetailModal.js +167 -0
- package/lib/types/client/ToolCards.d.ts +28 -0
- package/lib/types/client/ToolCards.js +52 -0
- package/lib/types/client/ToolDetailModal.d.ts +12 -0
- package/lib/types/client/ToolDetailModal.js +29 -0
- package/lib/types/client/icons.d.ts +26 -0
- package/lib/types/client/icons.js +10 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +196 -0
- package/lib/types/client/locales.d.ts +392 -0
- package/lib/types/client/locales.js +395 -0
- package/lib/types/client/mcp-model.d.ts +24 -0
- package/lib/types/client/mcp-model.js +87 -0
- package/lib/types/client/mode-model.d.ts +108 -0
- package/lib/types/client/mode-model.js +140 -0
- package/lib/types/client/preset-display.d.ts +21 -0
- package/lib/types/client/preset-display.js +20 -0
- package/lib/types/client/settle.d.ts +40 -0
- package/lib/types/client/settle.js +57 -0
- package/lib/types/client/slots.d.ts +86 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/envHint.d.ts +22 -0
- package/lib/types/envHint.js +76 -0
- package/lib/types/import.d.ts +106 -0
- package/lib/types/import.js +635 -0
- package/lib/types/index.d.ts +216 -0
- package/lib/types/index.js +739 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/mcpConnector.d.ts +24 -0
- package/lib/types/mcpConnector.js +73 -0
- package/lib/types/mcpStore.d.ts +68 -0
- package/lib/types/mcpStore.js +235 -0
- package/lib/types/mcpTools.d.ts +45 -0
- package/lib/types/mcpTools.js +125 -0
- package/lib/types/mcps.d.ts +64 -0
- package/lib/types/mcps.js +157 -0
- package/lib/types/modes.d.ts +65 -0
- package/lib/types/modes.js +97 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/namespace.js +8 -0
- package/lib/types/official-tools.d.ts +3 -0
- package/lib/types/official-tools.js +44 -0
- package/lib/types/preset-scope.d.ts +105 -0
- package/lib/types/preset-scope.js +117 -0
- package/lib/types/remote.d.ts +48 -0
- package/lib/types/remote.js +120 -0
- package/lib/types/scoped-delivery.d.ts +243 -0
- package/lib/types/scoped-delivery.js +686 -0
- package/lib/types/scoped-edits.d.ts +90 -0
- package/lib/types/scoped-edits.js +234 -0
- package/lib/types/settings.d.ts +28 -0
- package/lib/types/settings.js +28 -0
- package/lib/types/shellEnv.d.ts +65 -0
- package/lib/types/shellEnv.js +232 -0
- package/lib/types/skills.d.ts +103 -0
- package/lib/types/skills.js +303 -0
- package/lib/types/tool-origin.d.ts +29 -0
- package/lib/types/tool-origin.js +30 -0
- package/lib/types/types.d.ts +315 -0
- package/lib/types/types.js +8 -0
- package/lib/types/zip.d.ts +24 -0
- package/lib/types/zip.js +80 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +119 -0
- package/scripts/gen-official-tools.mts +52 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing side of preset-scoped delivery: rewrite one managed skill's
|
|
3
|
+
* `metadata.presetScope`, adopt an installed skill into the managed root, and
|
|
4
|
+
* release one back out.
|
|
5
|
+
*
|
|
6
|
+
* Every write here is deliberately narrow. The preset scope is edited in place
|
|
7
|
+
* in the skill's OWN frontmatter (the same store the delivery reads), adoption
|
|
8
|
+
* copies before it removes anything, and release refuses to overwrite an
|
|
9
|
+
* existing default-root skill. Nothing deletes a managed skill: releasing is the
|
|
10
|
+
* documented recovery direction, and a broken plugin must not take the user's
|
|
11
|
+
* files with it.
|
|
12
|
+
* @module @khorsheed/dsh-capability-catalog/scoped-edits
|
|
13
|
+
*/
|
|
14
|
+
/** Outcome of one write: a reason is present exactly when it failed. */
|
|
15
|
+
export interface ScopedEditResult {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly error?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Where a managed skill came from, for the release path. */
|
|
20
|
+
export interface ManagedSkillLocation {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** The absolute `SKILL.md` path inside the managed root. */
|
|
23
|
+
readonly path: string;
|
|
24
|
+
/** Its directory (the bundle the release moves). */
|
|
25
|
+
readonly dir: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Rewrite a SKILL.md's `metadata.presetScope` list.
|
|
29
|
+
*
|
|
30
|
+
* The frontmatter is edited line by line rather than re-serialized, so every
|
|
31
|
+
* other key, comment, and quote in the file survives untouched. The canonical
|
|
32
|
+
* inline form is written: `presetScope: []` for "every preset", otherwise
|
|
33
|
+
* `presetScope: [a, b]`.
|
|
34
|
+
* @param content - the current SKILL.md text.
|
|
35
|
+
* @param presets - the preset ids to declare (deduplicated, order preserved).
|
|
36
|
+
* @returns the rewritten file text.
|
|
37
|
+
* @throws when the file has no frontmatter block to edit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withPresetScope(content: string, presets: readonly string[]): string;
|
|
40
|
+
/** Find the managed entry for one skill name. */
|
|
41
|
+
export declare function managedLocation(dshHome: string, name: string): Promise<ManagedSkillLocation | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* Set one managed skill's preset scope.
|
|
44
|
+
* @param dshHome - the harness home owning the managed root.
|
|
45
|
+
* @param name - the skill name (its directory under the managed root).
|
|
46
|
+
* @param presets - the preset ids to declare.
|
|
47
|
+
* @returns the outcome, with a reason on failure.
|
|
48
|
+
*/
|
|
49
|
+
export declare function setManagedPresetScope(dshHome: string, name: string, presets: readonly string[]): Promise<ScopedEditResult>;
|
|
50
|
+
/** The default root currently supplying one skill, with its shape. */
|
|
51
|
+
export interface SkillSource {
|
|
52
|
+
readonly root: string;
|
|
53
|
+
readonly dir?: string;
|
|
54
|
+
readonly file?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Locate one skill in the default roots.
|
|
58
|
+
* @param dshHome - the harness home.
|
|
59
|
+
* @param workdir - the session workspace, when known.
|
|
60
|
+
* @param name - the skill name.
|
|
61
|
+
* @returns the source found, or undefined.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findSkillSource(dshHome: string, workdir: string | undefined, name: string): SkillSource | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Move an installed skill into the managed root and declare its preset scope.
|
|
66
|
+
*
|
|
67
|
+
* Order matters: the source is only removed after the copy has been committed
|
|
68
|
+
* and the scope written, so a failure at any step leaves one complete copy
|
|
69
|
+
* behind. The copy lands as a temp sibling and is committed with one rename, so
|
|
70
|
+
* the managed root never holds a half-written skill.
|
|
71
|
+
* @param dshHome - the harness home.
|
|
72
|
+
* @param name - the skill name.
|
|
73
|
+
* @param presets - the preset ids to declare.
|
|
74
|
+
* @param workdir - the session workspace, when known.
|
|
75
|
+
* @returns the outcome, with a reason on failure.
|
|
76
|
+
*/
|
|
77
|
+
export declare function adoptManagedSkill(dshHome: string, name: string, presets: readonly string[], workdir?: string | undefined): Promise<ScopedEditResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Move a managed skill back to the user skill root — the recovery direction.
|
|
80
|
+
*
|
|
81
|
+
* A managed skill is delivered by this plugin, so releasing it restores plain
|
|
82
|
+
* filesystem behavior that survives the plugin being removed.
|
|
83
|
+
* @param dshHome - the harness home.
|
|
84
|
+
* @param name - the skill name.
|
|
85
|
+
* @returns the outcome, with a reason on failure.
|
|
86
|
+
*/
|
|
87
|
+
export declare function releaseManagedSkill(dshHome: string, name: string): Promise<ScopedEditResult>;
|
|
88
|
+
/** Whether a path exists and is a directory. */
|
|
89
|
+
export declare function isDirectory(path: string): Promise<boolean>;
|
|
90
|
+
//# sourceMappingURL=scoped-edits.d.ts.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing side of preset-scoped delivery: rewrite one managed skill's
|
|
3
|
+
* `metadata.presetScope`, adopt an installed skill into the managed root, and
|
|
4
|
+
* release one back out.
|
|
5
|
+
*
|
|
6
|
+
* Every write here is deliberately narrow. The preset scope is edited in place
|
|
7
|
+
* in the skill's OWN frontmatter (the same store the delivery reads), adoption
|
|
8
|
+
* copies before it removes anything, and release refuses to overwrite an
|
|
9
|
+
* existing default-root skill. Nothing deletes a managed skill: releasing is the
|
|
10
|
+
* documented recovery direction, and a broken plugin must not take the user's
|
|
11
|
+
* files with it.
|
|
12
|
+
* @module @khorsheed/dsh-capability-catalog/scoped-edits
|
|
13
|
+
*/
|
|
14
|
+
import { cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { defaultSkillRoots, isKebabId, scanManagedSkills, scopedSkillsRoot, splitFrontmatter, } from './scoped-delivery.js';
|
|
18
|
+
/** Shared guard: every write path addresses one skill directory by name. */
|
|
19
|
+
function invalidName(name) {
|
|
20
|
+
return isKebabId(name) ? undefined : { ok: false, error: `"${name}" is not a valid skill name` };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite a SKILL.md's `metadata.presetScope` list.
|
|
24
|
+
*
|
|
25
|
+
* The frontmatter is edited line by line rather than re-serialized, so every
|
|
26
|
+
* other key, comment, and quote in the file survives untouched. The canonical
|
|
27
|
+
* inline form is written: `presetScope: []` for "every preset", otherwise
|
|
28
|
+
* `presetScope: [a, b]`.
|
|
29
|
+
* @param content - the current SKILL.md text.
|
|
30
|
+
* @param presets - the preset ids to declare (deduplicated, order preserved).
|
|
31
|
+
* @returns the rewritten file text.
|
|
32
|
+
* @throws when the file has no frontmatter block to edit.
|
|
33
|
+
*/
|
|
34
|
+
export function withPresetScope(content, presets) {
|
|
35
|
+
const split = splitFrontmatter(content);
|
|
36
|
+
if (split === undefined) {
|
|
37
|
+
throw new Error('the skill has no frontmatter block to edit');
|
|
38
|
+
}
|
|
39
|
+
const ids = [...new Set(presets)];
|
|
40
|
+
const lines = split.front.split('\n');
|
|
41
|
+
const kept = [];
|
|
42
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
43
|
+
const line = lines[index] ?? '';
|
|
44
|
+
const match = /^(\s*)presetScope\s*:/.exec(line);
|
|
45
|
+
if (match === null) {
|
|
46
|
+
kept.push(line);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Drop the key and any block-list items it owned.
|
|
50
|
+
const indent = (match[1] ?? '').length;
|
|
51
|
+
while (index + 1 < lines.length) {
|
|
52
|
+
const next = lines[index + 1] ?? '';
|
|
53
|
+
const entry = /^(\s*)-\s*/.exec(next);
|
|
54
|
+
if (entry === null || (entry[1] ?? '').length <= indent)
|
|
55
|
+
break;
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const rendered = `presetScope: [${ids.join(', ')}]`;
|
|
60
|
+
const metadataAt = kept.findIndex(line => /^\s*metadata\s*:\s*$/.test(line));
|
|
61
|
+
if (metadataAt >= 0) {
|
|
62
|
+
const indent = (/^(\s*)/.exec(kept[metadataAt] ?? '')?.[1] ?? '').length + 2;
|
|
63
|
+
kept.splice(metadataAt + 1, 0, `${' '.repeat(indent)}${rendered}`);
|
|
64
|
+
}
|
|
65
|
+
else if (kept.some(line => line.trim() !== '')) {
|
|
66
|
+
kept.push('metadata:', ` ${rendered}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
kept.push('metadata:', ` ${rendered}`);
|
|
70
|
+
}
|
|
71
|
+
return `---\n${kept.join('\n')}\n---\n${split.body}`;
|
|
72
|
+
}
|
|
73
|
+
/** Find the managed entry for one skill name. */
|
|
74
|
+
export async function managedLocation(dshHome, name) {
|
|
75
|
+
const root = scopedSkillsRoot(dshHome);
|
|
76
|
+
const skill = (await scanManagedSkills(root)).find(entry => entry.name === name);
|
|
77
|
+
if (skill === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { name: skill.name, path: skill.path, dir: skill.dir };
|
|
80
|
+
}
|
|
81
|
+
/** Write a file through a sibling temp file and one atomic rename. */
|
|
82
|
+
async function writeAtomic(path, content) {
|
|
83
|
+
const temp = join(dirname(path), `.${Date.now()}-${process.pid}.tmp`);
|
|
84
|
+
await writeFile(temp, content, 'utf8');
|
|
85
|
+
await rename(temp, path);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Set one managed skill's preset scope.
|
|
89
|
+
* @param dshHome - the harness home owning the managed root.
|
|
90
|
+
* @param name - the skill name (its directory under the managed root).
|
|
91
|
+
* @param presets - the preset ids to declare.
|
|
92
|
+
* @returns the outcome, with a reason on failure.
|
|
93
|
+
*/
|
|
94
|
+
export async function setManagedPresetScope(dshHome, name, presets) {
|
|
95
|
+
const badName = invalidName(name);
|
|
96
|
+
if (badName !== undefined)
|
|
97
|
+
return badName;
|
|
98
|
+
const location = await managedLocation(dshHome, name);
|
|
99
|
+
if (location === undefined) {
|
|
100
|
+
return { ok: false, error: `"${name}" is not a managed skill` };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const content = await readFile(location.path, 'utf8');
|
|
104
|
+
const updated = withPresetScope(content, presets);
|
|
105
|
+
// Nothing to write is not a failure: the declared scope already matches.
|
|
106
|
+
if (updated !== content)
|
|
107
|
+
await writeAtomic(location.path, updated);
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return { ok: false, error: messageOf(error) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Locate one skill in the default roots.
|
|
116
|
+
* @param dshHome - the harness home.
|
|
117
|
+
* @param workdir - the session workspace, when known.
|
|
118
|
+
* @param name - the skill name.
|
|
119
|
+
* @returns the source found, or undefined.
|
|
120
|
+
*/
|
|
121
|
+
export function findSkillSource(dshHome, workdir, name) {
|
|
122
|
+
for (const root of defaultSkillRoots(dshHome, workdir)) {
|
|
123
|
+
const dir = join(root, name);
|
|
124
|
+
if (existsSync(join(dir, 'SKILL.md')))
|
|
125
|
+
return { root, dir };
|
|
126
|
+
const file = join(root, `${name}.md`);
|
|
127
|
+
if (existsSync(file))
|
|
128
|
+
return { root, file };
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Move an installed skill into the managed root and declare its preset scope.
|
|
134
|
+
*
|
|
135
|
+
* Order matters: the source is only removed after the copy has been committed
|
|
136
|
+
* and the scope written, so a failure at any step leaves one complete copy
|
|
137
|
+
* behind. The copy lands as a temp sibling and is committed with one rename, so
|
|
138
|
+
* the managed root never holds a half-written skill.
|
|
139
|
+
* @param dshHome - the harness home.
|
|
140
|
+
* @param name - the skill name.
|
|
141
|
+
* @param presets - the preset ids to declare.
|
|
142
|
+
* @param workdir - the session workspace, when known.
|
|
143
|
+
* @returns the outcome, with a reason on failure.
|
|
144
|
+
*/
|
|
145
|
+
export async function adoptManagedSkill(dshHome, name, presets, workdir) {
|
|
146
|
+
const badName = invalidName(name);
|
|
147
|
+
if (badName !== undefined)
|
|
148
|
+
return badName;
|
|
149
|
+
const root = scopedSkillsRoot(dshHome);
|
|
150
|
+
const target = join(root, name);
|
|
151
|
+
if (existsSync(target)) {
|
|
152
|
+
return { ok: false, error: `the managed root already holds "${name}"` };
|
|
153
|
+
}
|
|
154
|
+
const source = findSkillSource(dshHome, workdir, name);
|
|
155
|
+
if (source === undefined) {
|
|
156
|
+
return { ok: false, error: `"${name}" was not found in a default skill root` };
|
|
157
|
+
}
|
|
158
|
+
const staging = join(root, `.${name}-${process.pid}`);
|
|
159
|
+
try {
|
|
160
|
+
await mkdir(root, { recursive: true });
|
|
161
|
+
await rm(staging, { recursive: true, force: true });
|
|
162
|
+
if (source.dir !== undefined) {
|
|
163
|
+
await cp(source.dir, staging, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await mkdir(staging, { recursive: true });
|
|
167
|
+
await writeFile(join(staging, 'SKILL.md'), await readFile(source.file ?? '', 'utf8'), 'utf8');
|
|
168
|
+
}
|
|
169
|
+
const staged = join(staging, 'SKILL.md');
|
|
170
|
+
const content = await readFile(staged, 'utf8');
|
|
171
|
+
await writeFile(staged, withPresetScope(content, presets), 'utf8');
|
|
172
|
+
await rename(staging, target);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
await rm(staging, { recursive: true, force: true }).catch(() => { });
|
|
176
|
+
return { ok: false, error: messageOf(error) };
|
|
177
|
+
}
|
|
178
|
+
// Committed: only now drop the default-root copy, and never fail the whole
|
|
179
|
+
// operation over it — the managed copy is already authoritative.
|
|
180
|
+
try {
|
|
181
|
+
if (source.dir !== undefined)
|
|
182
|
+
await rm(source.dir, { recursive: true, force: true });
|
|
183
|
+
else if (source.file !== undefined)
|
|
184
|
+
await rm(source.file, { force: true });
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return { ok: false, error: `adopted "${name}", but the default-root copy could not be removed: ${messageOf(error)}` };
|
|
188
|
+
}
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Move a managed skill back to the user skill root — the recovery direction.
|
|
193
|
+
*
|
|
194
|
+
* A managed skill is delivered by this plugin, so releasing it restores plain
|
|
195
|
+
* filesystem behavior that survives the plugin being removed.
|
|
196
|
+
* @param dshHome - the harness home.
|
|
197
|
+
* @param name - the skill name.
|
|
198
|
+
* @returns the outcome, with a reason on failure.
|
|
199
|
+
*/
|
|
200
|
+
export async function releaseManagedSkill(dshHome, name) {
|
|
201
|
+
const badName = invalidName(name);
|
|
202
|
+
if (badName !== undefined)
|
|
203
|
+
return badName;
|
|
204
|
+
const location = await managedLocation(dshHome, name);
|
|
205
|
+
if (location === undefined) {
|
|
206
|
+
return { ok: false, error: `"${name}" is not a managed skill` };
|
|
207
|
+
}
|
|
208
|
+
const target = join(dshHome, 'skills', name);
|
|
209
|
+
if (existsSync(target)) {
|
|
210
|
+
return { ok: false, error: `"${name}" already exists in ${join(dshHome, 'skills')}` };
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await mkdir(dirname(target), { recursive: true });
|
|
214
|
+
await cp(location.dir, target, { recursive: true });
|
|
215
|
+
await rm(location.dir, { recursive: true, force: true });
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return { ok: false, error: messageOf(error) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Whether a path exists and is a directory. */
|
|
223
|
+
export async function isDirectory(path) {
|
|
224
|
+
try {
|
|
225
|
+
return (await stat(path)).isDirectory();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Render an unknown failure. */
|
|
232
|
+
function messageOf(error) {
|
|
233
|
+
return error instanceof Error ? error.message : String(error);
|
|
234
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability-catalog row's plugin Config: the `mcp` block carries the
|
|
3
|
+
* persisted MCP management state (server configs, namespaced credential refs,
|
|
4
|
+
* and discovered tools) so MCP servers survive a restart. Credential values are
|
|
5
|
+
* stored here (the user's own config document) but are never exposed over the
|
|
6
|
+
* Remote — `mcpSnapshot` reports only `configured` state.
|
|
7
|
+
*
|
|
8
|
+
* Marked `.volatile()` so store mutations write through the SettingsForms
|
|
9
|
+
* service (host 0.1.7+) without a row reload, and external document updates
|
|
10
|
+
* reach the running fiber's Volatile reference. The block stays permissive
|
|
11
|
+
* (`z.any()`): the catalog card reads/writes it through the Remote, and a
|
|
12
|
+
* hand-typed schemastery shape for the nested config/tuple/secret structure is
|
|
13
|
+
* fragile.
|
|
14
|
+
*
|
|
15
|
+
* Dual-line: `.volatile()` exists only on schemastery 3.18.4+ (the 0.1.7 host
|
|
16
|
+
* line). On 0.1.5's schemastery the call is absent — the plain field then
|
|
17
|
+
* rides the legacy `settings.register` namespace path instead (see index.ts).
|
|
18
|
+
*/
|
|
19
|
+
import z from '@deepseek-ai/schemastery';
|
|
20
|
+
/** The persisted MCP state block lives under this key of the plugin config. */
|
|
21
|
+
export declare const MCP_SETTINGS_KEY = "mcp";
|
|
22
|
+
/** Section shape: the MCP management state block (permissive) + nothing else. */
|
|
23
|
+
export declare const CapabilityCatalogSettingsSchema: z<Schemastery.ObjectS<{
|
|
24
|
+
mcp: z<any, any>;
|
|
25
|
+
}>, Schemastery.ObjectT<{
|
|
26
|
+
mcp: z<any, any>;
|
|
27
|
+
}>>;
|
|
28
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability-catalog row's plugin Config: the `mcp` block carries the
|
|
3
|
+
* persisted MCP management state (server configs, namespaced credential refs,
|
|
4
|
+
* and discovered tools) so MCP servers survive a restart. Credential values are
|
|
5
|
+
* stored here (the user's own config document) but are never exposed over the
|
|
6
|
+
* Remote — `mcpSnapshot` reports only `configured` state.
|
|
7
|
+
*
|
|
8
|
+
* Marked `.volatile()` so store mutations write through the SettingsForms
|
|
9
|
+
* service (host 0.1.7+) without a row reload, and external document updates
|
|
10
|
+
* reach the running fiber's Volatile reference. The block stays permissive
|
|
11
|
+
* (`z.any()`): the catalog card reads/writes it through the Remote, and a
|
|
12
|
+
* hand-typed schemastery shape for the nested config/tuple/secret structure is
|
|
13
|
+
* fragile.
|
|
14
|
+
*
|
|
15
|
+
* Dual-line: `.volatile()` exists only on schemastery 3.18.4+ (the 0.1.7 host
|
|
16
|
+
* line). On 0.1.5's schemastery the call is absent — the plain field then
|
|
17
|
+
* rides the legacy `settings.register` namespace path instead (see index.ts).
|
|
18
|
+
*/
|
|
19
|
+
import z from '@deepseek-ai/schemastery';
|
|
20
|
+
/** The persisted MCP state block lives under this key of the plugin config. */
|
|
21
|
+
export const MCP_SETTINGS_KEY = 'mcp';
|
|
22
|
+
const mcpField = z.any().default({});
|
|
23
|
+
/** Section shape: the MCP management state block (permissive) + nothing else. */
|
|
24
|
+
export const CapabilityCatalogSettingsSchema = z.object({
|
|
25
|
+
[MCP_SETTINGS_KEY]: typeof mcpField.volatile === 'function'
|
|
26
|
+
? mcpField.volatile()
|
|
27
|
+
: mcpField,
|
|
28
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill env injection via the official `ctx.shellEnv` seam (service-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* The catalog lets a user configure a credential for a skill's declared env var
|
|
5
|
+
* (e.g. `$WEREED_API_KEY`). That value is NOT in `process.env` — the launch env
|
|
6
|
+
* is a read-only snapshot and never injects credentials. This module registers
|
|
7
|
+
* a `ctx.shellEnv` contributor that exposes each configured credential as a
|
|
8
|
+
* trusted, per-execution `DSH_<KEY>` variable. The model references it by shell
|
|
9
|
+
* expansion (`--key="$DSH_WEREED_API_KEY"`) so the raw value never enters the
|
|
10
|
+
* model's context; it only becomes visible if the model (or a command) actively
|
|
11
|
+
* prints it — a default-hide, not a hard secret boundary.
|
|
12
|
+
*
|
|
13
|
+
* Constraint handling (shell-env/src/index.ts):
|
|
14
|
+
* - keys must start `DSH_` and match `/^[A-Z][A-Z0-9_]*$/`; one owner per key;
|
|
15
|
+
* reserved built-ins (`DSH_HOME`/`DSH_SHELL`/`DSH_SESSION_ID`) are not ownable.
|
|
16
|
+
* - `resolve()` is synchronous; credential reads are async, so values live in a
|
|
17
|
+
* cache (`envCache`) refreshed on apply + on `credentials/*-updated`.
|
|
18
|
+
*
|
|
19
|
+
* Robustness (addressed for the review):
|
|
20
|
+
* - read the ref namespace: `credentials.resolve(credentialRef(key))` (value from
|
|
21
|
+
* the .credentials store that `setCredential` writes) — never `readRecord`.
|
|
22
|
+
* - filter env keys that map onto reserved (`HOME`/`SHELL`/`SESSION_ID`) or are
|
|
23
|
+
* already owned by another contributor (e.g. `DSH_SESSION_JSONL`) so a single
|
|
24
|
+
* bad/conflicting key cannot break the whole registration; skip + warn.
|
|
25
|
+
* - use a *declared superset*: register the full valid key set once and keep it;
|
|
26
|
+
* deleting a credential just makes `resolve` return empty (no delete-then-
|
|
27
|
+
* re-register window). Re-register only when the key SET changes.
|
|
28
|
+
* - serialise refresh (single in-flight + queued) and gate commits on a
|
|
29
|
+
* generation counter + `disposed` so a stale run cannot overwrite or
|
|
30
|
+
* re-register after dispose.
|
|
31
|
+
* @module @khorsheed/dsh-capability-catalog/shellEnv
|
|
32
|
+
*/
|
|
33
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
34
|
+
declare const DSH_PREFIX = "DSH_";
|
|
35
|
+
/** The `ctx.shellEnv` registry slice this module consumes (optional). */
|
|
36
|
+
export interface ShellEnvRegistryLike {
|
|
37
|
+
register: (contributor: {
|
|
38
|
+
name: string;
|
|
39
|
+
variables: Record<string, {
|
|
40
|
+
description: string;
|
|
41
|
+
}>;
|
|
42
|
+
resolve: (execution: unknown) => Record<string, string>;
|
|
43
|
+
}) => () => void;
|
|
44
|
+
list: () => readonly {
|
|
45
|
+
readonly key: string;
|
|
46
|
+
readonly contributor: string;
|
|
47
|
+
}[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Install the skill env injection contributor. Returns a disposer; the `ctx.on`
|
|
51
|
+
* and `shellEnv.register` disposers are also effect-scoped to the calling fiber.
|
|
52
|
+
* Degrades silently (a no-op) when `shellEnv`, `credentials`, or `skills` is
|
|
53
|
+
* absent in the composition.
|
|
54
|
+
*
|
|
55
|
+
* Boot-timing robustness: the contributor's `register()` runs `ctx.effect(...)`
|
|
56
|
+
* internally, so it must be called while its backing fiber is still `active`
|
|
57
|
+
* (alpha's reordered app-boot can leave it in a transient inactive window where
|
|
58
|
+
* a captured reference would throw `INACTIVE_EFFECT`). This install never caches
|
|
59
|
+
* a service reference across an `await`; it re-reads `ctx.get` (strict — returns
|
|
60
|
+
* the impl only while its fiber is active) immediately before calling `register`,
|
|
61
|
+
* and self-retries on `INACTIVE_EFFECT` so it self-heals once boot stabilizes.
|
|
62
|
+
*/
|
|
63
|
+
export declare function installSkillEnvInjection(ctx: Context, getScope: () => Promise<unknown | undefined>): () => void;
|
|
64
|
+
export { DSH_PREFIX };
|
|
65
|
+
//# sourceMappingURL=shellEnv.d.ts.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill env injection via the official `ctx.shellEnv` seam (service-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* The catalog lets a user configure a credential for a skill's declared env var
|
|
5
|
+
* (e.g. `$WEREED_API_KEY`). That value is NOT in `process.env` — the launch env
|
|
6
|
+
* is a read-only snapshot and never injects credentials. This module registers
|
|
7
|
+
* a `ctx.shellEnv` contributor that exposes each configured credential as a
|
|
8
|
+
* trusted, per-execution `DSH_<KEY>` variable. The model references it by shell
|
|
9
|
+
* expansion (`--key="$DSH_WEREED_API_KEY"`) so the raw value never enters the
|
|
10
|
+
* model's context; it only becomes visible if the model (or a command) actively
|
|
11
|
+
* prints it — a default-hide, not a hard secret boundary.
|
|
12
|
+
*
|
|
13
|
+
* Constraint handling (shell-env/src/index.ts):
|
|
14
|
+
* - keys must start `DSH_` and match `/^[A-Z][A-Z0-9_]*$/`; one owner per key;
|
|
15
|
+
* reserved built-ins (`DSH_HOME`/`DSH_SHELL`/`DSH_SESSION_ID`) are not ownable.
|
|
16
|
+
* - `resolve()` is synchronous; credential reads are async, so values live in a
|
|
17
|
+
* cache (`envCache`) refreshed on apply + on `credentials/*-updated`.
|
|
18
|
+
*
|
|
19
|
+
* Robustness (addressed for the review):
|
|
20
|
+
* - read the ref namespace: `credentials.resolve(credentialRef(key))` (value from
|
|
21
|
+
* the .credentials store that `setCredential` writes) — never `readRecord`.
|
|
22
|
+
* - filter env keys that map onto reserved (`HOME`/`SHELL`/`SESSION_ID`) or are
|
|
23
|
+
* already owned by another contributor (e.g. `DSH_SESSION_JSONL`) so a single
|
|
24
|
+
* bad/conflicting key cannot break the whole registration; skip + warn.
|
|
25
|
+
* - use a *declared superset*: register the full valid key set once and keep it;
|
|
26
|
+
* deleting a credential just makes `resolve` return empty (no delete-then-
|
|
27
|
+
* re-register window). Re-register only when the key SET changes.
|
|
28
|
+
* - serialise refresh (single in-flight + queued) and gate commits on a
|
|
29
|
+
* generation counter + `disposed` so a stale run cannot overwrite or
|
|
30
|
+
* re-register after dispose.
|
|
31
|
+
* @module @khorsheed/dsh-capability-catalog/shellEnv
|
|
32
|
+
*/
|
|
33
|
+
import { decodeCredentialDecls, decodeEnvDecls, mergeCredentialDecls } from './skills.js';
|
|
34
|
+
const DSH_PREFIX = 'DSH_';
|
|
35
|
+
const ENV_KEY_SUFFIX = /^[A-Z][A-Z0-9_]*$/;
|
|
36
|
+
/** Reserved `DSH_*` names the host's shell-env registry owns as built-in facts
|
|
37
|
+
* (the instance's real HOME / SHELL / SESSION_ID). A credential must never be
|
|
38
|
+
* allowed to take one of these over — a mapped value shadowing the built-in
|
|
39
|
+
* `DSH_HOME` would make the agent read the configured credential as the
|
|
40
|
+
* instance home. Keyed on the MAPPED name (not the raw declaration), so it
|
|
41
|
+
* matches the upstream registry's own reserved set (dsh-home-paths/dsh-shell)
|
|
42
|
+
* and survives prefix/schema drift. The catalog's own `DSH_`-prefixed keys are
|
|
43
|
+
* unaffected: declaring e.g. `DSH_HOME` maps to `DSH_DSH_HOME`, which is not a
|
|
44
|
+
* reserved name and never collides. */
|
|
45
|
+
const RESERVED_ENV_KEYS = new Set(['DSH_HOME', 'DSH_SHELL', 'DSH_SESSION_ID']);
|
|
46
|
+
/**
|
|
47
|
+
* Install the skill env injection contributor. Returns a disposer; the `ctx.on`
|
|
48
|
+
* and `shellEnv.register` disposers are also effect-scoped to the calling fiber.
|
|
49
|
+
* Degrades silently (a no-op) when `shellEnv`, `credentials`, or `skills` is
|
|
50
|
+
* absent in the composition.
|
|
51
|
+
*
|
|
52
|
+
* Boot-timing robustness: the contributor's `register()` runs `ctx.effect(...)`
|
|
53
|
+
* internally, so it must be called while its backing fiber is still `active`
|
|
54
|
+
* (alpha's reordered app-boot can leave it in a transient inactive window where
|
|
55
|
+
* a captured reference would throw `INACTIVE_EFFECT`). This install never caches
|
|
56
|
+
* a service reference across an `await`; it re-reads `ctx.get` (strict — returns
|
|
57
|
+
* the impl only while its fiber is active) immediately before calling `register`,
|
|
58
|
+
* and self-retries on `INACTIVE_EFFECT` so it self-heals once boot stabilizes.
|
|
59
|
+
*/
|
|
60
|
+
export function installSkillEnvInjection(ctx, getScope) {
|
|
61
|
+
let disposed = false;
|
|
62
|
+
let envCache = new Map();
|
|
63
|
+
let disposeEnv;
|
|
64
|
+
let refreshKey;
|
|
65
|
+
let refreshGen = 0;
|
|
66
|
+
let inFlight;
|
|
67
|
+
let queued = false;
|
|
68
|
+
let retryTimer;
|
|
69
|
+
let retries = 0;
|
|
70
|
+
const MAX_RETRIES = 30;
|
|
71
|
+
const RETRY_DELAY = 300;
|
|
72
|
+
const cancelRetry = () => {
|
|
73
|
+
if (retryTimer !== undefined) {
|
|
74
|
+
clearTimeout(retryTimer);
|
|
75
|
+
retryTimer = undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const scheduleRetry = () => {
|
|
79
|
+
if (disposed || retries >= MAX_RETRIES)
|
|
80
|
+
return;
|
|
81
|
+
cancelRetry();
|
|
82
|
+
retries++;
|
|
83
|
+
retryTimer = setTimeout(() => {
|
|
84
|
+
retryTimer = undefined;
|
|
85
|
+
if (!disposed)
|
|
86
|
+
void refresh();
|
|
87
|
+
}, RETRY_DELAY);
|
|
88
|
+
};
|
|
89
|
+
/** True for the `INACTIVE_EFFECT` Cordis error thrown by `ctx.effect` when the
|
|
90
|
+
* fiber is inactive — a transient boot-time race, not a real owner collision. */
|
|
91
|
+
const isInactiveEffect = (err) => {
|
|
92
|
+
return !!err && typeof err === 'object' && 'code' in err
|
|
93
|
+
&& err.code === 'INACTIVE_EFFECT';
|
|
94
|
+
};
|
|
95
|
+
/** One refresh pass; commits only if still the latest generation and not disposed. */
|
|
96
|
+
const refresh = async () => {
|
|
97
|
+
if (disposed)
|
|
98
|
+
return;
|
|
99
|
+
if (inFlight !== undefined) {
|
|
100
|
+
// Bump the generation so the in-flight pass is now stale and will not
|
|
101
|
+
// commit the value it read before this update arrived (strict
|
|
102
|
+
// latest-wins); the queued pass re-reads + commits fresh.
|
|
103
|
+
refreshGen++;
|
|
104
|
+
queued = true;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Re-resolve the services FRESH on every pass (never across an `await`).
|
|
108
|
+
// `ctx.get` (strict) returns the implementation only while its providing
|
|
109
|
+
// fiber is active, so a reload/reorder that makes a previously-captured
|
|
110
|
+
// reference inactive is never called into here. If any service is not
|
|
111
|
+
// currently active, this is a transient boot window — retry.
|
|
112
|
+
const shellEnv = ctx.get?.('shellEnv');
|
|
113
|
+
const credentials = ctx.get?.('credentials');
|
|
114
|
+
const skills = ctx.get?.('skills');
|
|
115
|
+
if (shellEnv?.register === undefined || credentials === undefined || skills === undefined) {
|
|
116
|
+
scheduleRetry();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const gen = ++refreshGen;
|
|
120
|
+
inFlight = (async () => {
|
|
121
|
+
const scope = await getScope().catch(() => undefined);
|
|
122
|
+
const snapshotOptions = scope === undefined ? {} : { scope };
|
|
123
|
+
try {
|
|
124
|
+
const snapshot = await skills.snapshot(snapshotOptions);
|
|
125
|
+
// Keys already owned by another contributor (must not redeclare).
|
|
126
|
+
const occupied = new Set((shellEnv.list?.() ?? []).filter(e => e.contributor !== 'capability-catalog').map(e => e.key));
|
|
127
|
+
const keys = new Set();
|
|
128
|
+
const values = new Map();
|
|
129
|
+
for (const row of snapshot.skills) {
|
|
130
|
+
// Only skills the model may invoke get their credentials injected —
|
|
131
|
+
// a model-disabled (user-only) skill cannot be used by the model, so
|
|
132
|
+
// putting its secret in the model shell would be pure exposure.
|
|
133
|
+
if (row.invocation?.modelInvocable === false)
|
|
134
|
+
continue;
|
|
135
|
+
let def;
|
|
136
|
+
try {
|
|
137
|
+
def = await skills.get(row.name, snapshotOptions);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (def === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
const decls = mergeCredentialDecls(decodeCredentialDecls(def.metadata), decodeEnvDecls(def.content ?? ''));
|
|
145
|
+
for (const decl of decls) {
|
|
146
|
+
if (!ENV_KEY_SUFFIX.test(decl.key))
|
|
147
|
+
continue;
|
|
148
|
+
const dshKey = `${DSH_PREFIX}${decl.key}`;
|
|
149
|
+
// Never let a credential take over a reserved built-in DSH_* name
|
|
150
|
+
// (DSH_HOME etc.). Declaring `HOME` maps to `DSH_HOME` and is
|
|
151
|
+
// dropped here; declaring `DSH_HOME` maps to `DSH_DSH_HOME`, which
|
|
152
|
+
// is not reserved and stays.
|
|
153
|
+
if (RESERVED_ENV_KEYS.has(dshKey) || occupied.has(dshKey))
|
|
154
|
+
continue;
|
|
155
|
+
keys.add(decl.key);
|
|
156
|
+
const resolved = await credentials.resolve(decl.key).catch(() => undefined);
|
|
157
|
+
if (resolved !== undefined && resolved.value.length > 0)
|
|
158
|
+
values.set(dshKey, resolved.value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (disposed || gen !== refreshGen)
|
|
162
|
+
return; // stale or disposed — drop the commit
|
|
163
|
+
envCache = values;
|
|
164
|
+
const keySet = [...keys].sort().join('\n');
|
|
165
|
+
if (keySet !== refreshKey) {
|
|
166
|
+
disposeEnv?.();
|
|
167
|
+
disposeEnv = undefined;
|
|
168
|
+
refreshKey = undefined;
|
|
169
|
+
const variables = {};
|
|
170
|
+
for (const k of keys) {
|
|
171
|
+
variables[`${DSH_PREFIX}${k}`] = { description: `Configured via the capability catalog for skill env \`${k}\`` };
|
|
172
|
+
}
|
|
173
|
+
// Re-resolve the registry immediately before `register` — never call
|
|
174
|
+
// `ctx.effect` on a reference captured across an `await`. If the
|
|
175
|
+
// service is inactive right now (boot reorder window), drop this pass
|
|
176
|
+
// and retry; leave refreshKey unset so the next pass re-registers.
|
|
177
|
+
const shellEnvNow = ctx.get?.('shellEnv');
|
|
178
|
+
if (shellEnvNow?.register === undefined) {
|
|
179
|
+
scheduleRetry();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
disposeEnv = shellEnvNow.register({
|
|
184
|
+
name: 'capability-catalog',
|
|
185
|
+
variables,
|
|
186
|
+
resolve: () => {
|
|
187
|
+
const out = {};
|
|
188
|
+
for (const [k, v] of envCache) {
|
|
189
|
+
if (Object.hasOwn(variables, k))
|
|
190
|
+
out[k] = v;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
refreshKey = keySet;
|
|
196
|
+
retries = 0;
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
// Register failed (e.g. a TOCTOU owner collision). Leave refreshKey
|
|
200
|
+
// unset so the NEXT refresh retries regardless of the key set even
|
|
201
|
+
// if the key set later returns to a previously-registered value.
|
|
202
|
+
process.stderr.write(`capability-catalog: skill env contributor register failed (${String(err)})`);
|
|
203
|
+
if (isInactiveEffect(err))
|
|
204
|
+
scheduleRetry();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
process.stderr.write(`capability-catalog: skill env injection refresh failed (${String(err)})`);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
inFlight = undefined;
|
|
213
|
+
if (queued && !disposed) {
|
|
214
|
+
queued = false;
|
|
215
|
+
void refresh();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
await inFlight;
|
|
220
|
+
};
|
|
221
|
+
void refresh();
|
|
222
|
+
const offRecord = ctx.on('credentials/record-updated', () => { void refresh(); });
|
|
223
|
+
const offReference = ctx.on('credentials/reference-updated', () => { void refresh(); });
|
|
224
|
+
return () => {
|
|
225
|
+
disposed = true;
|
|
226
|
+
cancelRetry();
|
|
227
|
+
disposeEnv?.();
|
|
228
|
+
offRecord();
|
|
229
|
+
offReference();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export { DSH_PREFIX };
|