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.
Files changed (42) hide show
  1. package/README.md +11 -3
  2. package/README.zh.md +11 -3
  3. package/client/client.js +61 -44
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog.js +9 -2
  6. package/lib/catalog/component-files.js +3 -1
  7. package/lib/catalog/native-project.js +1 -1
  8. package/lib/catalog/project-config.js +0 -2
  9. package/lib/catalog/project-hooks.js +12 -7
  10. package/lib/catalog/surfaces.js +17 -10
  11. package/lib/index.js +23 -2
  12. package/lib/model/command-names.js +14 -0
  13. package/lib/model/layouts.js +1 -0
  14. package/lib/runtime/commands-mounts.js +3 -2
  15. package/lib/runtime/hooks-mounts.js +7 -4
  16. package/lib/runtime/mcp-backend.js +3 -2
  17. package/lib/runtime/mcp-direct-config.js +18 -9
  18. package/lib/runtime/plugin-message-source.js +4 -0
  19. package/lib/runtime/project-runtime.js +10 -8
  20. package/lib/runtime/settings-namespace.js +44 -66
  21. package/lib/runtime/user-commands.js +23 -8
  22. package/lib/runtime/user-hooks.js +102 -0
  23. package/lib/runtime/user-panels.js +34 -22
  24. package/lib/runtime/user-store.js +81 -19
  25. package/lib/types/catalog/component-files.d.ts +1 -0
  26. package/lib/types/catalog/project-hooks.d.ts +15 -2
  27. package/lib/types/catalog/surfaces.d.ts +6 -4
  28. package/lib/types/contracts/market.d.ts +5 -1
  29. package/lib/types/index.d.ts +87 -1
  30. package/lib/types/model/command-names.d.ts +12 -0
  31. package/lib/types/model/layouts.d.ts +1 -0
  32. package/lib/types/model/types.d.ts +2 -1
  33. package/lib/types/runtime/mcp-backend.d.ts +20 -13
  34. package/lib/types/runtime/mcp-direct-config.d.ts +1 -5
  35. package/lib/types/runtime/plugin-message-source.d.ts +24 -0
  36. package/lib/types/runtime/settings-namespace.d.ts +41 -31
  37. package/lib/types/runtime/user-commands.d.ts +5 -2
  38. package/lib/types/runtime/user-hooks.d.ts +22 -0
  39. package/lib/types/runtime/user-panels.d.ts +35 -15
  40. package/lib/types/runtime/user-store.d.ts +27 -10
  41. package/package.json +38 -31
  42. 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
- * Only the skills panel serves the cross-tool `<name>/SKILL.md` directory
16
- * spelling beside its own flat entries; commands and personas stay flat.
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 — and the panel addresses, edits, and
20
- * deletes the entry by that name. A document that reader would reject is
21
- * listed disabled with the reason instead of joining the catalog, and a skill
22
- * is switched off by writing that reader's own invocation controls, so the off
23
- * state lives in the file rather than in this provider.
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 USER_ENTRY_NAME.test(name) && this.extraNameCheck(name);
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 file name by
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 (!USER_ENTRY_NAME.test(name))
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 names
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 command grammar (`_` allowed).
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. Skills also accept the cross-tool directory
9
- * spelling (`<name>/SKILL.md`) that other Agent tools and the harness's own
10
- * reader of `~/.agents/skills` author; commands and personas are flat only.
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_-]*` — the grammar every panel entry name must satisfy. */
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. Names outside the entry grammar never touch the filesystem.
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
- /** Every entry document the served shapes expose, deduplicated by name at the first shape that has it. */
63
- export async function listEntryDocuments(dir, strict = false, shapes = ['file']) {
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 name
167
- * is interpolated into `join(dir, name + '.md')`, so an unchecked name with
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 (!USER_ENTRY_NAME.test(name))
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
- /** Pure validation shared by native settings and plugin file/inline declarations. */
7
- export declare function normalizeHookDocuments(projectRoot: string, documents: Array<{
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. Portable v1 suites (§7.1) instead discover exactly one level of
5
- * `skills/` subdirectories each carrying a `SKILL.md` — the spec forbids
6
- * recursive deeper search and root-level or flat skill files are outside the
7
- * portable discovery shape.
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
- /** Entry name: the name its document declares, else the file's base name. */
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;
@@ -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 { Context } from '@deepseek-ai/cordis';
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
- projectRoot: string;
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<{