@auggieteo/dsh-mcp-adapter 0.2.0 → 0.2.2
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/CHANGELOG.md +26 -0
- package/README.md +10 -1
- package/docs/adr/0009-dual-generation-settings-api.md +2 -0
- package/lib/client.js +180 -172
- package/lib/client.js.map +2 -2
- package/package.json +1 -1
- package/src/host/index.js +7 -2
- package/src/host/settings.js +10 -1
- package/src/host/skill.js +125 -10
package/src/host/skill.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync as defaultMkdirSync,
|
|
3
|
+
readFileSync as defaultReadFileSync,
|
|
4
|
+
renameSync as defaultRenameSync,
|
|
5
|
+
writeFileSync as defaultWriteFileSync,
|
|
6
|
+
} from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { homedir as defaultHomedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
2
10
|
import { fileURLToPath } from 'node:url'
|
|
3
11
|
|
|
4
12
|
import { errorMessage } from './errors.js'
|
|
@@ -7,8 +15,26 @@ import { errorMessage } from './errors.js'
|
|
|
7
15
|
export const SKILL_DIR = fileURLToPath(new URL('../../skills/mcp-adapter/', import.meta.url))
|
|
8
16
|
|
|
9
17
|
const SKILL_FILE = 'SKILL.md'
|
|
18
|
+
const SKILL_TEMP_FILE = '.SKILL.md.tmp'
|
|
10
19
|
const SKILL_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
11
20
|
|
|
21
|
+
/** The Adapter package version, read lazily so a missing manifest never breaks load. */
|
|
22
|
+
function packageVersion() {
|
|
23
|
+
try {
|
|
24
|
+
return createRequire(import.meta.url)('../../package.json').version
|
|
25
|
+
} catch {
|
|
26
|
+
return 'unknown'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the DSH skills root the same way the harness does: `$DSH_HOME`
|
|
32
|
+
* when set, otherwise `.dsh` under the OS home directory.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSkillsRoot(env = process.env, homedir = defaultHomedir) {
|
|
35
|
+
return join(env.DSH_HOME ?? join(homedir(), '.dsh'), 'skills')
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Split one `SKILL.md` into its frontmatter fields and body. The block must
|
|
14
40
|
* open the file: `---`, flat `key: value` lines, then a closing `---`. Only
|
|
@@ -34,16 +60,96 @@ export function parseSkillFrontmatter(text) {
|
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
63
|
+
* Copy the bundled `SKILL.md` to `$DSH_HOME/skills/<name>/SKILL.md` so the
|
|
64
|
+
* filesystem skill provider (and with it Settings > Skills) discovers it.
|
|
65
|
+
* The copy is atomic — temp file in the target directory, then rename — and
|
|
66
|
+
* never a symlink or hardlink. An installed file is never overwritten: when
|
|
67
|
+
* it exists and matches, nothing happens; when it differs, the human's edit
|
|
68
|
+
* wins and one warning names the path and the package version. Returns true
|
|
69
|
+
* when the file now carries the skill (install done or already present).
|
|
70
|
+
*/
|
|
71
|
+
function installSkillFile(ctx, text, name, {
|
|
72
|
+
readInstalledFileSync,
|
|
73
|
+
writeFileSync,
|
|
74
|
+
mkdirSync,
|
|
75
|
+
renameSync,
|
|
76
|
+
env,
|
|
77
|
+
homedir,
|
|
78
|
+
}) {
|
|
79
|
+
const warn = (reason) => {
|
|
80
|
+
ctx.logger?.warn?.(`dsh-mcp-adapter: ${reason}`)
|
|
81
|
+
}
|
|
82
|
+
const targetDir = join(resolveSkillsRoot(env, homedir), name)
|
|
83
|
+
const target = join(targetDir, SKILL_FILE)
|
|
84
|
+
let installed
|
|
85
|
+
try {
|
|
86
|
+
installed = String(readInstalledFileSync(target, 'utf8'))
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code !== 'ENOENT') {
|
|
89
|
+
warn(
|
|
90
|
+
`cannot read the installed skill file ${target} (${errorMessage(error)}); `
|
|
91
|
+
+ 'registering the bundled skill at runtime instead',
|
|
92
|
+
)
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (installed !== undefined) {
|
|
97
|
+
if (installed === text) return true
|
|
98
|
+
// The human's edit wins: leave the file untouched and treat it as the
|
|
99
|
+
// one definition for this name.
|
|
100
|
+
warn(
|
|
101
|
+
`the installed skill file ${target} differs from the bundled copy `
|
|
102
|
+
+ `(dsh-mcp-adapter ${packageVersion()}); delete ${targetDir} to refresh `
|
|
103
|
+
+ 'it from the package on the next start',
|
|
104
|
+
)
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(targetDir, { recursive: true })
|
|
109
|
+
const temp = join(targetDir, SKILL_TEMP_FILE)
|
|
110
|
+
writeFileSync(temp, text, 'utf8')
|
|
111
|
+
renameSync(temp, target)
|
|
112
|
+
return true
|
|
113
|
+
} catch (error) {
|
|
114
|
+
warn(
|
|
115
|
+
`cannot install the skill file at ${target} (${errorMessage(error)}); `
|
|
116
|
+
+ 'registering the bundled skill at runtime instead',
|
|
117
|
+
)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Install the bundled `mcp-adapter` skill so agents discover how to drive the
|
|
124
|
+
* Proxy Tool. The body lives in `skills/mcp-adapter/SKILL.md` next to the
|
|
125
|
+
* package root, so a human can edit the wording without touching code;
|
|
126
|
+
* frontmatter still drives routing.
|
|
41
127
|
*
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
128
|
+
* The `skillInstall` mode comes from the Adapter Config (default `'file'`).
|
|
129
|
+
* `'file'` copies the skill to `$DSH_HOME/skills/<name>/SKILL.md` and lets
|
|
130
|
+
* the filesystem provider pick it up — one definition per name, so the
|
|
131
|
+
* runtime registration is skipped. `'runtime'` registers directly on the
|
|
132
|
+
* `skills` service as before. `'off'` installs nothing.
|
|
133
|
+
*
|
|
134
|
+
* The skill is a convenience, never a dependency: an unreadable file, bad
|
|
135
|
+
* frontmatter, or a failed install logs a warning and never throws; a failed
|
|
136
|
+
* file install falls back to the runtime registration. Every filesystem
|
|
137
|
+
* access (`readFileSync` for the bundled copy, `readInstalledFileSync` for
|
|
138
|
+
* the installed one, plus `writeFileSync`, `mkdirSync`, and `renameSync`)
|
|
139
|
+
* and `dir`, `env`, and `homedir` are injectable for tests.
|
|
45
140
|
*/
|
|
46
|
-
export function installMcpSkill(ctx, {
|
|
141
|
+
export function installMcpSkill(ctx, {
|
|
142
|
+
readFileSync = defaultReadFileSync,
|
|
143
|
+
readInstalledFileSync = defaultReadFileSync,
|
|
144
|
+
writeFileSync = defaultWriteFileSync,
|
|
145
|
+
mkdirSync = defaultMkdirSync,
|
|
146
|
+
renameSync = defaultRenameSync,
|
|
147
|
+
env = process.env,
|
|
148
|
+
homedir = defaultHomedir,
|
|
149
|
+
dir = SKILL_DIR,
|
|
150
|
+
skillInstall = 'file',
|
|
151
|
+
} = {}) {
|
|
152
|
+
if (skillInstall === 'off') return undefined
|
|
47
153
|
const fail = (reason) => {
|
|
48
154
|
ctx.logger?.warn?.(`dsh-mcp-adapter: bundled skill unavailable (${reason})`)
|
|
49
155
|
return undefined
|
|
@@ -57,7 +163,7 @@ export function installMcpSkill(ctx, { readFileSync = defaultReadFileSync, dir =
|
|
|
57
163
|
if (typeof description !== 'string' || description === '') {
|
|
58
164
|
return fail('frontmatter requires a non-empty description')
|
|
59
165
|
}
|
|
60
|
-
|
|
166
|
+
const register = () => ctx.skills.register({
|
|
61
167
|
name,
|
|
62
168
|
description,
|
|
63
169
|
...(whenToUse === undefined ? {} : { whenToUse }),
|
|
@@ -65,6 +171,15 @@ export function installMcpSkill(ctx, { readFileSync = defaultReadFileSync, dir =
|
|
|
65
171
|
content: body,
|
|
66
172
|
resourceBase: { kind: 'directory', path: dir },
|
|
67
173
|
})
|
|
174
|
+
if (skillInstall !== 'runtime') {
|
|
175
|
+
const installed = installSkillFile(ctx, text, name, {
|
|
176
|
+
readInstalledFileSync, writeFileSync, mkdirSync, renameSync, env, homedir,
|
|
177
|
+
})
|
|
178
|
+
// One definition per name: the filesystem provider owns the skill once
|
|
179
|
+
// the file is in place (or the human's edit already covers it).
|
|
180
|
+
if (installed) return undefined
|
|
181
|
+
}
|
|
182
|
+
return register()
|
|
68
183
|
} catch (error) {
|
|
69
184
|
return fail(errorMessage(error))
|
|
70
185
|
}
|