dsh-agent-plugins-market 0.7.2 → 0.8.1
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/README.md +11 -3
- package/README.zh.md +11 -3
- package/client/client.js +61 -44
- package/client/client.js.map +1 -1
- package/lib/application/catalog.js +9 -2
- package/lib/catalog/component-files.js +3 -1
- package/lib/catalog/native-project.js +1 -1
- package/lib/catalog/project-config.js +0 -2
- package/lib/catalog/project-hooks.js +12 -7
- package/lib/catalog/surfaces.js +17 -10
- package/lib/index.js +23 -2
- package/lib/model/command-names.js +14 -0
- package/lib/model/layouts.js +1 -0
- package/lib/runtime/commands-mounts.js +3 -2
- package/lib/runtime/hooks-mounts.js +7 -4
- package/lib/runtime/mcp-backend.js +3 -2
- package/lib/runtime/mcp-direct-config.js +18 -9
- package/lib/runtime/plugin-message-source.js +4 -0
- package/lib/runtime/project-runtime.js +10 -8
- package/lib/runtime/settings-namespace.js +44 -66
- package/lib/runtime/user-commands.js +23 -8
- package/lib/runtime/user-hooks.js +102 -0
- package/lib/runtime/user-panels.js +34 -22
- package/lib/runtime/user-store.js +81 -19
- package/lib/types/catalog/component-files.d.ts +1 -0
- package/lib/types/catalog/project-hooks.d.ts +15 -2
- package/lib/types/catalog/surfaces.d.ts +6 -4
- package/lib/types/contracts/market.d.ts +5 -1
- package/lib/types/index.d.ts +87 -1
- package/lib/types/model/command-names.d.ts +12 -0
- package/lib/types/model/layouts.d.ts +1 -0
- package/lib/types/model/types.d.ts +2 -1
- package/lib/types/runtime/mcp-backend.d.ts +20 -13
- package/lib/types/runtime/mcp-direct-config.d.ts +1 -5
- package/lib/types/runtime/plugin-message-source.d.ts +24 -0
- package/lib/types/runtime/settings-namespace.d.ts +41 -31
- package/lib/types/runtime/user-commands.d.ts +5 -2
- package/lib/types/runtime/user-hooks.d.ts +22 -0
- package/lib/types/runtime/user-panels.d.ts +35 -15
- package/lib/types/runtime/user-store.d.ts +27 -10
- package/package.json +38 -31
- package/schemas/com.deepseek.harness/spec.md +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct user configuration for command hooks: the Agent layout root's own
|
|
3
|
+
* hook files, mounted through the same bridge lifecycle a suite's hooks use.
|
|
4
|
+
*
|
|
5
|
+
* The files live at `~/.agents/hooks/hooks.json` and `~/.agents/hooks.json`
|
|
6
|
+
* and are read in that order, merged additively exactly like the project
|
|
7
|
+
* `.agents` layout reads them. A malformed file fails the suite closed with a
|
|
8
|
+
* diagnostic, never a thrown discovery, and the hooks carry no
|
|
9
|
+
* `projectRoot`: the bridge defaults `${CLAUDE_PROJECT_DIR}` to the calling
|
|
10
|
+
* session's workspace.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { normalizeHookDocuments, standaloneHookDocument } from '../catalog/project-hooks.js';
|
|
15
|
+
import { effectiveSurfaces } from '../model/types.js';
|
|
16
|
+
export const USER_HOOKS_SOURCE = '@user-hooks';
|
|
17
|
+
export const USER_HOOKS_SUITE = 'user-hooks';
|
|
18
|
+
/**
|
|
19
|
+
* The Agent layout root's hook documents, relative to that root and read in
|
|
20
|
+
* order. Later files merge additively over earlier ones, the same order and
|
|
21
|
+
* semantics the project `.agents` layout applies. The first entry is also the
|
|
22
|
+
* suite's manifest path and the file a suite with no documents is addressed by.
|
|
23
|
+
*/
|
|
24
|
+
export const USER_HOOK_FILES = [join('hooks', 'hooks.json'), 'hooks.json'];
|
|
25
|
+
/** Resolve one Agent-layout-relative hook document against the layout root. */
|
|
26
|
+
export function userHookPath(agentsRoot, file) {
|
|
27
|
+
return join(agentsRoot, file);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load the user's Agent layout hooks as one synthetic suite.
|
|
31
|
+
*
|
|
32
|
+
* The suite is always built, so one call answers with both the events and the
|
|
33
|
+
* reasons a document was rejected: an absent, malformed, or event-free set of
|
|
34
|
+
* hook files leaves `surfaces.hooks` at 0 and carries the diagnostics.
|
|
35
|
+
* {@link ../application/catalog.ts} merges the suite into the enabled set only
|
|
36
|
+
* when it declares events.
|
|
37
|
+
*/
|
|
38
|
+
export async function loadUserHooksSuite(agentsRoot) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const documents = [];
|
|
41
|
+
for (const file of USER_HOOK_FILES) {
|
|
42
|
+
const settings = await readHookDocument(agentsRoot, file, errors);
|
|
43
|
+
// A malformed file fails the whole set closed, the same way one malformed
|
|
44
|
+
// project hook layer drops every hook the layout declared.
|
|
45
|
+
if (settings === null)
|
|
46
|
+
return userHooksSuite(agentsRoot, undefined, errors);
|
|
47
|
+
// A standalone hook file may carry the bare event table; the shared rule
|
|
48
|
+
// that recognizes it is the same one the project `.agents` layout applies.
|
|
49
|
+
if (settings !== undefined)
|
|
50
|
+
documents.push({ file, settings: standaloneHookDocument(settings) });
|
|
51
|
+
}
|
|
52
|
+
return userHooksSuite(agentsRoot, normalizeHookDocuments(undefined, documents, errors), errors);
|
|
53
|
+
}
|
|
54
|
+
/** One synthetic suite for the Agent layout root's hook files. */
|
|
55
|
+
function userHooksSuite(agentsRoot, hooks, errors) {
|
|
56
|
+
return {
|
|
57
|
+
sourceId: USER_HOOKS_SOURCE,
|
|
58
|
+
id: USER_HOOKS_SUITE,
|
|
59
|
+
root: agentsRoot,
|
|
60
|
+
manifest: { layout: 'agent-plugin-v1', path: userHookPath(agentsRoot, USER_HOOK_FILES[0]), id: USER_HOOKS_SUITE, name: USER_HOOKS_SUITE },
|
|
61
|
+
skills: [],
|
|
62
|
+
...(hooks === undefined ? {} : { hooks }),
|
|
63
|
+
surfaces: { skills: 0, mcp: 0, hooks: hookCount(hooks), commands: 0, agents: 0, lsp: 0 },
|
|
64
|
+
dimension: 'user',
|
|
65
|
+
enabled: true,
|
|
66
|
+
// The suite declares hooks only; the other surfaces would otherwise make
|
|
67
|
+
// the command and LSP registries read this root a second time.
|
|
68
|
+
activeSurfaces: effectiveSurfaces({ skills: false, mcp: false, commands: false, agents: false, lsp: false }),
|
|
69
|
+
installedAt: 'user',
|
|
70
|
+
errors
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** One hook file's parsed document: absent is undefined, malformed is null plus a diagnostic. */
|
|
74
|
+
async function readHookDocument(agentsRoot, file, errors) {
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = await readFile(userHookPath(agentsRoot, file), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code === 'ENOENT')
|
|
81
|
+
return undefined;
|
|
82
|
+
errors.push(`${file}: hook file is unreadable`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
errors.push(`${file}: hook file is invalid JSON`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
94
|
+
errors.push(`${file}: hook file must be an object`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
/** Declared hook commands, the same count a native suite reports for `surfaces.hooks`. */
|
|
100
|
+
function hookCount(hooks) {
|
|
101
|
+
return Object.values(hooks?.events ?? {}).reduce((total, groups) => total + groups.reduce((count, group) => count + group.hooks.length, 0), 0);
|
|
102
|
+
}
|
|
@@ -12,20 +12,26 @@
|
|
|
12
12
|
* personas follow Claude Code agents frontmatter (`name`, `description`,
|
|
13
13
|
* optional `whenToUse`, `tools`, `model`, `disabled`).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Every panel reads the flat `<name>.md` children of its directory. The
|
|
16
|
+
* commands and agent-persona panels also read every `.md` document at any
|
|
17
|
+
* depth, addressing a nested entry by its path relative to the panel
|
|
18
|
+
* directory (`git/commit`, `review/code`), so subdirectories another Agent
|
|
19
|
+
* tool writes are picked up. The skills panel keeps the cross-tool
|
|
20
|
+
* `<name>/SKILL.md` directory spelling and reads it from the top level only,
|
|
21
|
+
* matching the harness's own reader of `~/.agents/skills`.
|
|
17
22
|
*
|
|
18
23
|
* An entry's name is the one its document declares — the same name the
|
|
19
|
-
* harness reader derives from that file —
|
|
20
|
-
* deletes the entry by that name. A document that
|
|
21
|
-
* listed disabled with the reason instead of joining
|
|
22
|
-
* is switched off by writing that reader's own
|
|
23
|
-
* state lives in the file rather than in this
|
|
24
|
+
* harness reader derives from that file — else its document name, and the
|
|
25
|
+
* panel addresses, edits, and deletes the entry by that name. A document that
|
|
26
|
+
* reader would reject is listed disabled with the reason instead of joining
|
|
27
|
+
* the catalog, and a skill is switched off by writing that reader's own
|
|
28
|
+
* invocation controls, so the off state lives in the file rather than in this
|
|
29
|
+
* provider.
|
|
24
30
|
* @module runtime/user-panels
|
|
25
31
|
*/
|
|
26
32
|
import { dirname } from 'node:path';
|
|
27
33
|
import { skillEntryRejection } from '../catalog/skills-parse.js';
|
|
28
|
-
import { deleteEntryDocument, entryExists, listEntryDocuments, listEntryFiles, readEntryDocument, resolveEntryDocument, SKILL_ENTRY_SHAPES, USER_ENTRY_NAME, userEntryDir, writeEntryDocument, writeEntryFile } from './user-store.js';
|
|
34
|
+
import { deleteEntryDocument, entryExists, listEntryDocuments, listEntryFiles, readEntryDocument, resolveEntryDocument, SKILL_ENTRY_SHAPES, USER_ENTRY_NAME, USER_ENTRY_PATH, userEntryDir, writeEntryDocument, writeEntryFile } from './user-store.js';
|
|
29
35
|
/** The skill-source label user-panel entries carry into the skill registry. */
|
|
30
36
|
export const USER_PANEL_SKILL_SOURCE = 'user-panel';
|
|
31
37
|
/**
|
|
@@ -48,6 +54,8 @@ export class UserPanelStore {
|
|
|
48
54
|
kind;
|
|
49
55
|
shapes;
|
|
50
56
|
extraNameCheck;
|
|
57
|
+
namePattern;
|
|
58
|
+
nested;
|
|
51
59
|
validate;
|
|
52
60
|
honorInvocationControls;
|
|
53
61
|
constructor(agentsRoot, kind, options = {}) {
|
|
@@ -55,6 +63,8 @@ export class UserPanelStore {
|
|
|
55
63
|
this.kind = kind;
|
|
56
64
|
this.shapes = options.shapes ?? ['file'];
|
|
57
65
|
this.extraNameCheck = options.extraNameCheck ?? (() => true);
|
|
66
|
+
this.nested = options.nested === true;
|
|
67
|
+
this.namePattern = this.nested ? USER_ENTRY_PATH : USER_ENTRY_NAME;
|
|
58
68
|
this.validate = options.validate;
|
|
59
69
|
this.honorInvocationControls = options.honorInvocationControls === true;
|
|
60
70
|
}
|
|
@@ -65,20 +75,21 @@ export class UserPanelStore {
|
|
|
65
75
|
get dir() {
|
|
66
76
|
return this.dirPath();
|
|
67
77
|
}
|
|
78
|
+
/** The path grammar plus the panel's per-segment extra grammar. */
|
|
68
79
|
validName(name) {
|
|
69
|
-
return
|
|
80
|
+
return this.namePattern.test(name) && name.split('/').every(segment => this.extraNameCheck(segment));
|
|
70
81
|
}
|
|
71
82
|
/**
|
|
72
83
|
* Resolve the document one entry name addresses. The name a panel entry
|
|
73
|
-
* carries is the one its document declares, which is only the
|
|
74
|
-
* convention, so a name that matches no path is matched against the
|
|
84
|
+
* carries is the one its document declares, which is only the document path
|
|
85
|
+
* by convention, so a name that matches no path is matched against the
|
|
75
86
|
* declared names of the served documents.
|
|
76
87
|
*/
|
|
77
88
|
async document(name) {
|
|
78
|
-
const direct = await resolveEntryDocument(this.dir, name, this.shapes);
|
|
89
|
+
const direct = await resolveEntryDocument(this.dir, name, this.shapes, false, this.nested);
|
|
79
90
|
if (direct !== undefined)
|
|
80
91
|
return direct;
|
|
81
|
-
for (const candidate of await listEntryDocuments(this.dir, false, this.shapes)) {
|
|
92
|
+
for (const candidate of await listEntryDocuments(this.dir, false, this.shapes, this.nested)) {
|
|
82
93
|
const file = await readEntryDocument(candidate);
|
|
83
94
|
if (file?.name === name)
|
|
84
95
|
return candidate;
|
|
@@ -87,7 +98,7 @@ export class UserPanelStore {
|
|
|
87
98
|
}
|
|
88
99
|
/** Every entry, sorted by name; strict snapshots propagate temporary I/O failures. */
|
|
89
100
|
async list(strict = false) {
|
|
90
|
-
const files = await listEntryFiles(this.dir, strict, this.shapes);
|
|
101
|
+
const files = await listEntryFiles(this.dir, strict, this.shapes, this.nested);
|
|
91
102
|
return files.map(file => this.serialize(file));
|
|
92
103
|
}
|
|
93
104
|
/** One entry's full record, including the raw file text. */
|
|
@@ -133,11 +144,11 @@ export class UserPanelStore {
|
|
|
133
144
|
origin: 'user'
|
|
134
145
|
};
|
|
135
146
|
}
|
|
136
|
-
/** Create an entry; refuses an occupied name. New entries use the flat spelling. */
|
|
147
|
+
/** Create an entry; refuses an occupied name. New entries use the flat `<name>.md` spelling, creating any parent directories. */
|
|
137
148
|
async create(name, text) {
|
|
138
149
|
if (!this.validName(name))
|
|
139
150
|
throw new Error(`invalid name "${name}" — use lowercase letters, digits, and dashes, starting with a letter or digit`);
|
|
140
|
-
if (await entryExists(this.dir, name, this.shapes))
|
|
151
|
+
if (await entryExists(this.dir, name, this.shapes, this.nested))
|
|
141
152
|
throw new Error(`an entry named "${name}" already exists`);
|
|
142
153
|
await writeEntryFile(this.dir, name, text);
|
|
143
154
|
const created = await this.get(name);
|
|
@@ -156,7 +167,7 @@ export class UserPanelStore {
|
|
|
156
167
|
}
|
|
157
168
|
/** Delete one entry; a missing file is a no-op (idempotent delete). */
|
|
158
169
|
async remove(name) {
|
|
159
|
-
if (!
|
|
170
|
+
if (!this.namePattern.test(name))
|
|
160
171
|
throw new Error(`invalid entry name "${name}"`);
|
|
161
172
|
const document = await this.document(name);
|
|
162
173
|
if (document === undefined)
|
|
@@ -173,16 +184,17 @@ export function createUserPanelStores(agentsRoot) {
|
|
|
173
184
|
validate: file => skillEntryRejection(file.text),
|
|
174
185
|
honorInvocationControls: true
|
|
175
186
|
}),
|
|
176
|
-
commands: new UserPanelStore(agentsRoot, 'commands'),
|
|
177
|
-
agents: new UserPanelStore(agentsRoot, 'agents', { extraNameCheck: isUserSkillEntryName })
|
|
187
|
+
commands: new UserPanelStore(agentsRoot, 'commands', { nested: true }),
|
|
188
|
+
agents: new UserPanelStore(agentsRoot, 'agents', { extraNameCheck: isUserSkillEntryName, nested: true })
|
|
178
189
|
};
|
|
179
190
|
}
|
|
180
191
|
/**
|
|
181
|
-
* The skills/agents panels accept only harness skill-grammar
|
|
192
|
+
* The skills/agents panels accept only harness skill-grammar path segments
|
|
182
193
|
* (`[a-z0-9-]`, no underscores): a candidate name reaching the skill
|
|
183
194
|
* registry's `get()` with an invalid name is dropped there, and several
|
|
184
|
-
* consumers validate strictly, so `_`-named entries would be dead weight.
|
|
185
|
-
* Commands keep the looser
|
|
195
|
+
* consumers validate strictly, so `_`-named entries would be dead weight. A
|
|
196
|
+
* nested persona applies this to each segment. Commands keep the looser
|
|
197
|
+
* command grammar (`_` allowed).
|
|
186
198
|
*/
|
|
187
199
|
export function isUserSkillEntryName(name) {
|
|
188
200
|
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* frontmatter rules the catalog uses, so user entries look exactly like suite
|
|
6
6
|
* entries to the runtime mounts.
|
|
7
7
|
*
|
|
8
|
-
* An entry is one document.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* An entry is one document. A flat panel reads the immediate `<name>.md`
|
|
9
|
+
* children of its directory; a nested panel (commands and agent personas)
|
|
10
|
+
* reads every `.md` document at any depth and names each by its path relative
|
|
11
|
+
* to that directory. Skills also accept the cross-tool directory spelling
|
|
12
|
+
* (`<name>/SKILL.md`) that other Agent tools and the harness's own reader of
|
|
13
|
+
* `~/.agents/skills` author, and read it from the top level only.
|
|
11
14
|
* @module runtime/user-store
|
|
12
15
|
*/
|
|
13
16
|
import { readdir, readFile, rmdir, rm, stat } from 'node:fs/promises';
|
|
@@ -19,8 +22,15 @@ import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse
|
|
|
19
22
|
export function userEntryDir(agentsRoot, kind) {
|
|
20
23
|
return join(agentsRoot, kind);
|
|
21
24
|
}
|
|
22
|
-
/** `[a-z][a-z0-9_-]*` —
|
|
25
|
+
/** `[a-z][a-z0-9_-]*` — one segment of a panel entry name. */
|
|
23
26
|
export const USER_ENTRY_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
27
|
+
/**
|
|
28
|
+
* The path grammar a nested panel entry name satisfies: one or more
|
|
29
|
+
* {@link USER_ENTRY_NAME} segments joined by `/`. It rejects `..`, leading or
|
|
30
|
+
* trailing separators, empty segments, backslashes, and uppercase, so the name
|
|
31
|
+
* stays a path relative to the panel directory.
|
|
32
|
+
*/
|
|
33
|
+
export const USER_ENTRY_PATH = /^[a-z][a-z0-9_-]*(?:\/[a-z][a-z0-9_-]*)*$/;
|
|
24
34
|
/** The file name of a directory-shaped skill's document. */
|
|
25
35
|
export const SKILL_ENTRY_FILE = 'SKILL.md';
|
|
26
36
|
/**
|
|
@@ -47,10 +57,12 @@ async function isFile(file, strict) {
|
|
|
47
57
|
}
|
|
48
58
|
/**
|
|
49
59
|
* Resolve one entry name to its document, taking the first shape the path
|
|
50
|
-
* exists in.
|
|
60
|
+
* exists in. A nested panel accepts a path name (`git/commit`); a flat panel
|
|
61
|
+
* accepts a single segment. Names outside the panel's grammar never touch the
|
|
62
|
+
* filesystem.
|
|
51
63
|
*/
|
|
52
|
-
export async function resolveEntryDocument(dir, name, shapes, strict = false) {
|
|
53
|
-
if (!USER_ENTRY_NAME.test(name))
|
|
64
|
+
export async function resolveEntryDocument(dir, name, shapes, strict = false, nested = false) {
|
|
65
|
+
if (!(nested ? USER_ENTRY_PATH : USER_ENTRY_NAME).test(name))
|
|
54
66
|
return undefined;
|
|
55
67
|
for (const shape of shapes) {
|
|
56
68
|
const file = entryDocumentPath(dir, name, shape);
|
|
@@ -59,8 +71,18 @@ export async function resolveEntryDocument(dir, name, shapes, strict = false) {
|
|
|
59
71
|
}
|
|
60
72
|
return undefined;
|
|
61
73
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Every entry document the served shapes expose. A flat panel reads immediate
|
|
76
|
+
* children only; a nested panel walks every subdirectory to any depth and
|
|
77
|
+
* names each entry by its path relative to `dir`. Entries are deduplicated by
|
|
78
|
+
* name at the first shape that has it.
|
|
79
|
+
*/
|
|
80
|
+
export async function listEntryDocuments(dir, strict = false, shapes = ['file'], nested = false) {
|
|
81
|
+
const found = new Map();
|
|
82
|
+
if (nested) {
|
|
83
|
+
await collectNestedEntryDocuments(dir, '', shapes, strict, found);
|
|
84
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
+
}
|
|
64
86
|
let entries;
|
|
65
87
|
try {
|
|
66
88
|
entries = await readdir(dir);
|
|
@@ -70,7 +92,6 @@ export async function listEntryDocuments(dir, strict = false, shapes = ['file'])
|
|
|
70
92
|
throw error;
|
|
71
93
|
return [];
|
|
72
94
|
}
|
|
73
|
-
const found = new Map();
|
|
74
95
|
for (const entry of entries.sort()) {
|
|
75
96
|
const name = entry.endsWith('.md') ? entry.slice(0, -3) : shapes.includes('skill-directory') ? entry : undefined;
|
|
76
97
|
if (name === undefined || !USER_ENTRY_NAME.test(name))
|
|
@@ -85,6 +106,45 @@ export async function listEntryDocuments(dir, strict = false, shapes = ['file'])
|
|
|
85
106
|
}
|
|
86
107
|
return [...found.values()];
|
|
87
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Recursively collect the entry documents under one directory. Dot-directories,
|
|
111
|
+
* `node_modules`, and symlinks are skipped, so the walk cannot follow a link
|
|
112
|
+
* out of the panel directory; every segment must satisfy the entry grammar, so
|
|
113
|
+
* a discovered name stays a relative path. `prefix` is that path so far, with
|
|
114
|
+
* `/` separators and no trailing slash.
|
|
115
|
+
*/
|
|
116
|
+
async function collectNestedEntryDocuments(dir, prefix, shapes, strict, found) {
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (strict && error.code !== 'ENOENT')
|
|
123
|
+
throw error;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
127
|
+
if (entry.isSymbolicLink() || entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
128
|
+
continue;
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
if (!USER_ENTRY_NAME.test(entry.name))
|
|
131
|
+
continue;
|
|
132
|
+
const name = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
133
|
+
if (shapes.includes('skill-directory') && (await isFile(join(dir, entry.name, SKILL_ENTRY_FILE), strict))) {
|
|
134
|
+
found.set(name, { name, file: join(dir, entry.name, SKILL_ENTRY_FILE), directory: join(dir, entry.name), shape: 'skill-directory' });
|
|
135
|
+
}
|
|
136
|
+
await collectNestedEntryDocuments(join(dir, entry.name), name, shapes, strict, found);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
140
|
+
continue;
|
|
141
|
+
const segment = entry.name.slice(0, -3);
|
|
142
|
+
if (!USER_ENTRY_NAME.test(segment))
|
|
143
|
+
continue;
|
|
144
|
+
const name = prefix === '' ? segment : `${prefix}/${segment}`;
|
|
145
|
+
found.set(name, { name, file: join(dir, entry.name), directory: dir, shape: 'file' });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
88
148
|
/** Parse full YAML metadata without flattening arrays, mappings, or multiline strings. */
|
|
89
149
|
export function parseFrontmatterRecord(text) {
|
|
90
150
|
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
@@ -140,8 +200,8 @@ export async function readEntryDocument(document, strict = false) {
|
|
|
140
200
|
};
|
|
141
201
|
}
|
|
142
202
|
/** List entries across the served shapes; strict runtime snapshots propagate I/O errors instead of publishing partial inventories. */
|
|
143
|
-
export async function listEntryFiles(dir, strict = false, shapes = ['file']) {
|
|
144
|
-
const documents = await listEntryDocuments(dir, strict, shapes);
|
|
203
|
+
export async function listEntryFiles(dir, strict = false, shapes = ['file'], nested = false) {
|
|
204
|
+
const documents = await listEntryDocuments(dir, strict, shapes, nested);
|
|
145
205
|
const found = [];
|
|
146
206
|
for (const document of documents) {
|
|
147
207
|
const parsed = await readEntryDocument(document, strict);
|
|
@@ -163,13 +223,15 @@ export async function writeEntryDocument(document, text) {
|
|
|
163
223
|
await writeFileAtomic(document.file, text, { mode: 0o644, dirMode: 0o700 });
|
|
164
224
|
}
|
|
165
225
|
/**
|
|
166
|
-
* Enforce the entry-name grammar at every filesystem choke point. The
|
|
167
|
-
* is interpolated into `join(dir, name + '.md')`, so an unchecked name
|
|
168
|
-
* separators or `..` could address any `.md` file (or directory) on disk
|
|
169
|
-
* read, write, and delete all go through this guard.
|
|
226
|
+
* Enforce the entry-name path grammar at every filesystem choke point. The
|
|
227
|
+
* name is interpolated into `join(dir, name + '.md')`, so an unchecked name
|
|
228
|
+
* with separators or `..` could address any `.md` file (or directory) on disk
|
|
229
|
+
* — read, write, and delete all go through this guard. Whether the panel
|
|
230
|
+
* accepts a nested path or a single segment is decided where a caller-supplied
|
|
231
|
+
* name enters the store (`resolveEntryDocument` and the store's own checks).
|
|
170
232
|
*/
|
|
171
233
|
function assertEntryName(name) {
|
|
172
|
-
if (!
|
|
234
|
+
if (!USER_ENTRY_PATH.test(name))
|
|
173
235
|
throw new Error(`invalid entry name "${name}" — use lowercase letters, digits, dashes, or underscores, starting with a letter`);
|
|
174
236
|
}
|
|
175
237
|
/**
|
|
@@ -186,8 +248,8 @@ export async function deleteEntryDocument(document) {
|
|
|
186
248
|
await rmdir(dirname(document.file)).catch(() => undefined);
|
|
187
249
|
}
|
|
188
250
|
/** Whether an entry document with that name exists in any served shape. */
|
|
189
|
-
export async function entryExists(dir, name, shapes = ['file']) {
|
|
190
|
-
return (await resolveEntryDocument(dir, name, shapes)) !== undefined;
|
|
251
|
+
export async function entryExists(dir, name, shapes = ['file'], nested = false) {
|
|
252
|
+
return (await resolveEntryDocument(dir, name, shapes, false, nested)) !== undefined;
|
|
191
253
|
}
|
|
192
254
|
/** Serialize a frontmatter block from a shallow record (deterministic key order). */
|
|
193
255
|
export function serializeFrontmatter(meta) {
|
|
@@ -18,4 +18,5 @@ export declare function firstComponentFile(root: string, candidates: readonly st
|
|
|
18
18
|
/** Legacy/native suites without catalog resource lists keep their conventional directory behavior. */
|
|
19
19
|
export declare function defaultMarkdownResources(root: string, component: 'agents' | 'commands'): Promise<SuiteMarkdownResource[]>;
|
|
20
20
|
export declare function resourceText(resource: SuiteMarkdownResource): Promise<string>;
|
|
21
|
+
/** Registration name of a command resource; one rule governs suite, project-native, and user-panel commands. */
|
|
21
22
|
export declare function resourceCommandName(name: string): string;
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/** Normalize native Claude-shaped command hooks for the existing host execution bridge. */
|
|
2
2
|
import type { ProjectHooks } from '../model/types.js';
|
|
3
3
|
import type { ProjectHookFormat } from '../model/layouts.js';
|
|
4
|
+
/**
|
|
5
|
+
* Present one read document as the settings shape the shared validator reads:
|
|
6
|
+
* a standalone hook file may carry the bare event table instead of a `hooks`
|
|
7
|
+
* key. A recognized top-level event name is the marker — a document with no
|
|
8
|
+
* event name at the top stays verbatim, so an ordinary settings file missing
|
|
9
|
+
* `hooks` is still not accepted as a bare event table. Exported because the
|
|
10
|
+
* user Agent layout root reads the same standalone file shapes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function standaloneHookDocument(settings: Record<string, unknown>, format?: ProjectHookFormat): Record<string, unknown>;
|
|
4
13
|
/** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
|
|
5
14
|
export declare function discoverProjectHooks(projectRoot: string, files: readonly string[], errors: string[], format?: ProjectHookFormat): Promise<ProjectHooks | undefined>;
|
|
6
|
-
/**
|
|
7
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Pure validation shared by native settings and plugin file/inline
|
|
17
|
+
* declarations. `projectRoot` is absent for hooks declared in the user Agent
|
|
18
|
+
* layout root, which belong to no project.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeHookDocuments(projectRoot: string | undefined, documents: Array<{
|
|
8
21
|
file: string;
|
|
9
22
|
settings: Record<string, unknown>;
|
|
10
23
|
}>, errors: string[], format?: ProjectHookFormat): ProjectHooks | undefined;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { LspSuiteConfig, McpSuiteConfig, SuiteManifest, SuiteSkill, SuiteSurfaceCounts } from '../model/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Discover SKILL.md files under the suite's skills directory, up to 3 levels
|
|
4
|
-
* deep.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* deep. A path that carries `SKILL.md` is one skill directory — a manifest may
|
|
5
|
+
* list those individually — and only that document is read from it. Portable
|
|
6
|
+
* v1 suites (§7.1) instead discover exactly one level of `skills/`
|
|
7
|
+
* subdirectories each carrying a `SKILL.md` — the spec forbids recursive
|
|
8
|
+
* deeper search and root-level or flat skill files are outside the portable
|
|
9
|
+
* discovery shape.
|
|
8
10
|
*/
|
|
9
11
|
export declare function discoverSkills(root: string, errors: string[], declared?: unknown, portable?: boolean): Promise<SuiteSkill[]>;
|
|
10
12
|
/**
|
|
@@ -343,7 +343,11 @@ export interface SkillContent {
|
|
|
343
343
|
}
|
|
344
344
|
/** A user panel entry (skills / commands / agent personas) over HTTP. */
|
|
345
345
|
export interface UserPanelEntryWire {
|
|
346
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* Entry name: the name its document declares, else its document name — the
|
|
348
|
+
* file's base name, or, on a panel that reads subdirectories, the document's
|
|
349
|
+
* path relative to the panel directory (`git/commit`).
|
|
350
|
+
*/
|
|
347
351
|
name: string;
|
|
348
352
|
description: string;
|
|
349
353
|
disabled: boolean;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* self-contained: suites' MCP servers mount through the market's own bridge
|
|
14
14
|
* plugin on the host `tools` registry.
|
|
15
15
|
*/
|
|
16
|
-
import type
|
|
16
|
+
import { type Context, type Volatile } from '@deepseek-ai/cordis';
|
|
17
|
+
import z from '@deepseek-ai/schemastery';
|
|
18
|
+
import type { DownloadRegionSetting } from './contracts/settings.js';
|
|
17
19
|
import type { SourceRef } from './model/types.js';
|
|
18
20
|
export declare const name = "dsh-agent-plugins-market";
|
|
19
21
|
export declare const inject: string[];
|
|
20
22
|
/** Host configuration. */
|
|
23
|
+
/** Host configuration; the five market settings arrive as volatile references the host updates in place. */
|
|
21
24
|
export interface Config {
|
|
22
25
|
/** User-dimension suite root; defaults to `~/.dsh/agent-plugins` (`$DSH_HOME/agent-plugins`). */
|
|
23
26
|
userRoot?: string;
|
|
@@ -40,5 +43,88 @@ export interface Config {
|
|
|
40
43
|
fallbackTarball?: boolean;
|
|
41
44
|
allowHttpArchives?: boolean;
|
|
42
45
|
};
|
|
46
|
+
/** MCP mount backend switch; updated live through the host settings service. */
|
|
47
|
+
mcpEnhanced: Volatile<boolean | undefined>;
|
|
48
|
+
/** Project-layout discovery switch; updated live through the host settings service. */
|
|
49
|
+
scanProjectLayouts: Volatile<boolean | undefined>;
|
|
50
|
+
/** Download region choice; updated live through the host settings service. */
|
|
51
|
+
downloadRegion: Volatile<DownloadRegionSetting | undefined>;
|
|
52
|
+
/** Experience-feedback tool switch; updated live through the host settings service. */
|
|
53
|
+
feedbackEnabled: Volatile<boolean | undefined>;
|
|
54
|
+
/** Background source-update switch; updated live through the host settings service. */
|
|
55
|
+
autoUpdateSources: Volatile<boolean | undefined>;
|
|
43
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Schemastery projection the host loader reads: the five volatile fields become
|
|
59
|
+
* the `dsh-agent-plugins-market` settings namespace (the Plugins panel's
|
|
60
|
+
* configuration page reads it), while the startup fields stay plain.
|
|
61
|
+
*/
|
|
62
|
+
/** The schema's input face: every field optional, exactly what a profile patch may carry. */
|
|
63
|
+
export interface ConfigInput {
|
|
64
|
+
userRoot?: string | null;
|
|
65
|
+
dataRoot?: string | null;
|
|
66
|
+
sources?: unknown;
|
|
67
|
+
git?: {
|
|
68
|
+
proxy?: string | null;
|
|
69
|
+
insteadOf?: Record<string, unknown>;
|
|
70
|
+
timeoutMs?: number | null;
|
|
71
|
+
cloneRetry?: boolean | null;
|
|
72
|
+
fallbackTarball?: boolean | null;
|
|
73
|
+
allowHttpArchives?: boolean | null;
|
|
74
|
+
};
|
|
75
|
+
mcpEnhanced?: boolean | null;
|
|
76
|
+
scanProjectLayouts?: boolean | null;
|
|
77
|
+
downloadRegion?: 'auto' | 'global' | 'china' | null;
|
|
78
|
+
feedbackEnabled?: boolean | null;
|
|
79
|
+
autoUpdateSources?: boolean | null;
|
|
80
|
+
}
|
|
81
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
82
|
+
userRoot: z<string | undefined, string | undefined, "plain">;
|
|
83
|
+
dataRoot: z<string | undefined, string | undefined, "plain">;
|
|
84
|
+
sources: z<any, any, "plain">;
|
|
85
|
+
git: z<Schemastery.ObjectS<NoInfer<{
|
|
86
|
+
proxy: z<string, string, "plain">;
|
|
87
|
+
insteadOf: z<import("@deepseek-ai/cosmokit").Dict<string, any>, import("@deepseek-ai/cosmokit").Dict<string, any>, "plain">;
|
|
88
|
+
timeoutMs: z<number, number, "plain">;
|
|
89
|
+
cloneRetry: z<boolean, boolean, "plain">;
|
|
90
|
+
fallbackTarball: z<boolean, boolean, "plain">;
|
|
91
|
+
allowHttpArchives: z<boolean, boolean, "plain">;
|
|
92
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
93
|
+
proxy: z<string, string, "plain">;
|
|
94
|
+
insteadOf: z<import("@deepseek-ai/cosmokit").Dict<string, any>, import("@deepseek-ai/cosmokit").Dict<string, any>, "plain">;
|
|
95
|
+
timeoutMs: z<number, number, "plain">;
|
|
96
|
+
cloneRetry: z<boolean, boolean, "plain">;
|
|
97
|
+
fallbackTarball: z<boolean, boolean, "plain">;
|
|
98
|
+
allowHttpArchives: z<boolean, boolean, "plain">;
|
|
99
|
+
}>>, "plain">;
|
|
100
|
+
mcpEnhanced: z<boolean, boolean, "volatile-defined">;
|
|
101
|
+
scanProjectLayouts: z<boolean, boolean, "volatile-defined">;
|
|
102
|
+
downloadRegion: z<"auto" | "global" | "china", "auto" | "global" | "china", "volatile-defined">;
|
|
103
|
+
feedbackEnabled: z<boolean, boolean, "volatile-defined">;
|
|
104
|
+
autoUpdateSources: z<boolean, boolean, "volatile-defined">;
|
|
105
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
106
|
+
userRoot: z<string | undefined, string | undefined, "plain">;
|
|
107
|
+
dataRoot: z<string | undefined, string | undefined, "plain">;
|
|
108
|
+
sources: z<any, any, "plain">;
|
|
109
|
+
git: z<Schemastery.ObjectS<NoInfer<{
|
|
110
|
+
proxy: z<string, string, "plain">;
|
|
111
|
+
insteadOf: z<import("@deepseek-ai/cosmokit").Dict<string, any>, import("@deepseek-ai/cosmokit").Dict<string, any>, "plain">;
|
|
112
|
+
timeoutMs: z<number, number, "plain">;
|
|
113
|
+
cloneRetry: z<boolean, boolean, "plain">;
|
|
114
|
+
fallbackTarball: z<boolean, boolean, "plain">;
|
|
115
|
+
allowHttpArchives: z<boolean, boolean, "plain">;
|
|
116
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
117
|
+
proxy: z<string, string, "plain">;
|
|
118
|
+
insteadOf: z<import("@deepseek-ai/cosmokit").Dict<string, any>, import("@deepseek-ai/cosmokit").Dict<string, any>, "plain">;
|
|
119
|
+
timeoutMs: z<number, number, "plain">;
|
|
120
|
+
cloneRetry: z<boolean, boolean, "plain">;
|
|
121
|
+
fallbackTarball: z<boolean, boolean, "plain">;
|
|
122
|
+
allowHttpArchives: z<boolean, boolean, "plain">;
|
|
123
|
+
}>>, "plain">;
|
|
124
|
+
mcpEnhanced: z<boolean, boolean, "volatile-defined">;
|
|
125
|
+
scanProjectLayouts: z<boolean, boolean, "volatile-defined">;
|
|
126
|
+
downloadRegion: z<"auto" | "global" | "china", "auto" | "global" | "china", "volatile-defined">;
|
|
127
|
+
feedbackEnabled: z<boolean, boolean, "volatile-defined">;
|
|
128
|
+
autoUpdateSources: z<boolean, boolean, "volatile-defined">;
|
|
129
|
+
}>>, "plain">;
|
|
44
130
|
export declare function apply(ctx: Context, config?: Config): Promise<void>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The call name a command resource registers under.
|
|
3
|
+
*
|
|
4
|
+
* A command resource may live at any depth under its `commands/` directory,
|
|
5
|
+
* but the host's slash-command grammar accepts single-segment names only, so
|
|
6
|
+
* registration flattens the path separators: `git/commit` registers as
|
|
7
|
+
* `git-commit`. The user panel renders the same call name, so a card never
|
|
8
|
+
* advertises a slash name the host cannot accept.
|
|
9
|
+
* @module model/command-names
|
|
10
|
+
*/
|
|
11
|
+
/** Flatten a resource path into the single-segment name a slash command registers under. */
|
|
12
|
+
export declare function commandCallName(name: string): string;
|
|
@@ -86,6 +86,7 @@ export declare const PROJECT_LAYOUTS: readonly [{
|
|
|
86
86
|
readonly dirName: ".agents";
|
|
87
87
|
readonly label: "agents";
|
|
88
88
|
readonly subdirs: readonly ["skills", "agents", "commands"];
|
|
89
|
+
readonly mcpFiles: readonly [".agents/mcp.json"];
|
|
89
90
|
readonly hookFiles: readonly [".agents/hooks/hooks.json", ".agents/hooks.json"];
|
|
90
91
|
}, {
|
|
91
92
|
readonly dirName: ".codex";
|
|
@@ -259,7 +259,8 @@ export interface Suite extends DiscoveredSuite {
|
|
|
259
259
|
activeSurfaces: Record<SuiteSurfaceKey, boolean>;
|
|
260
260
|
}
|
|
261
261
|
export interface ProjectHooks {
|
|
262
|
-
|
|
262
|
+
/** Absent when the hooks belong to the user Agent layout root: the bridge then defaults `${CLAUDE_PROJECT_DIR}` to the calling session's workspace. */
|
|
263
|
+
projectRoot?: string;
|
|
263
264
|
events: Record<string, Array<{
|
|
264
265
|
matcher?: string;
|
|
265
266
|
hooks: Array<{
|