dsh-agent-plugins-market 0.6.2 → 0.7.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 +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +28533 -1511
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +188 -0
- package/lib/application/catalog.js +229 -1109
- package/lib/application/details.js +19 -8
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +263 -0
- package/lib/application/panel-resources.js +43 -14
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +8 -2
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +35 -30
- package/lib/catalog/component-files.js +14 -6
- package/lib/catalog/fs-probes.js +43 -0
- package/lib/catalog/manifests.js +53 -92
- package/lib/catalog/native-project.js +2 -2
- package/lib/catalog/paths.js +74 -55
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +23 -2
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +61 -10
- package/lib/catalog/source-catalog.js +10 -4
- package/lib/catalog/suite-components.js +7 -7
- package/lib/catalog/suite-scanner.js +3 -1
- package/lib/catalog/surfaces.js +5 -26
- package/lib/catalog/validate.js +7 -11
- package/lib/contracts/market.js +2 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +115 -217
- package/lib/model/layouts.js +10 -1
- package/lib/model/types.js +26 -3
- package/lib/routes.js +57 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +142 -106
- package/lib/runtime/commands-mounts.js +55 -11
- package/lib/runtime/deadline.js +54 -0
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +101 -46
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +49 -24
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +182 -70
- package/lib/runtime/lsp-server-state.js +4 -4
- package/lib/runtime/lsp-status.js +1 -1
- package/lib/runtime/mcp-auth-record.js +42 -0
- package/lib/runtime/mcp-backend.js +14 -14
- package/lib/runtime/mcp-client/config.js +8 -1
- package/lib/runtime/mcp-client/connection.js +38 -43
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-client/oauth.js +5 -7
- package/lib/runtime/mcp-client/plugin-identity.js +28 -0
- package/lib/runtime/mcp-client/projection.js +202 -0
- package/lib/runtime/mcp-client/tools.js +4 -189
- package/lib/runtime/mcp-config.js +36 -14
- package/lib/runtime/mcp-direct-config.js +33 -16
- package/lib/runtime/mcp-mounts.js +35 -75
- package/lib/runtime/mcp-overrides.js +5 -5
- package/lib/runtime/mcp-redaction.js +3 -2
- package/lib/runtime/mcp-status.js +7 -8
- package/lib/runtime/mount-lifecycle.js +73 -0
- package/lib/runtime/profile-seam.js +321 -0
- package/lib/runtime/project-runtime.js +34 -17
- package/lib/runtime/reconcile-scheduler.js +97 -0
- package/lib/runtime/reconciler.js +6 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +144 -0
- package/lib/runtime/skills-provider.js +38 -14
- package/lib/runtime/source-auto-update.js +66 -0
- package/lib/{model/state.js → runtime/state-store.js} +9 -11
- package/lib/runtime/storage-migration.js +35 -18
- package/lib/runtime/subagent-catalog.js +5 -4
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +20 -20
- package/lib/runtime/user-commands.js +7 -3
- package/lib/runtime/user-panels.js +125 -46
- package/lib/runtime/user-store.js +124 -69
- package/lib/types/application/catalog-context.d.ts +124 -0
- package/lib/types/application/catalog.d.ts +108 -304
- package/lib/types/application/install-store.d.ts +21 -0
- package/lib/types/application/lsp-service.d.ts +36 -0
- package/lib/types/application/mcp-service.d.ts +77 -0
- package/lib/types/application/panel-resources.d.ts +8 -0
- package/lib/types/application/ports.d.ts +72 -0
- package/lib/types/application/queries.d.ts +15 -26
- package/lib/types/application/snapshot-cache.d.ts +51 -0
- package/lib/types/application/source-store.d.ts +100 -0
- package/lib/types/catalog/archive.d.ts +3 -11
- package/lib/types/catalog/component-files.d.ts +4 -0
- package/lib/types/catalog/fs-probes.d.ts +10 -0
- package/lib/types/catalog/manifests.d.ts +17 -16
- package/lib/types/catalog/native-project.d.ts +2 -2
- package/lib/types/catalog/paths.d.ts +53 -15
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/scan-pipeline.d.ts +3 -3
- package/lib/types/catalog/scan-resolvers.d.ts +3 -9
- package/lib/types/catalog/skills-parse.d.ts +13 -7
- package/lib/types/catalog/source-catalog.d.ts +10 -5
- package/lib/types/catalog/suite-scanner.d.ts +8 -6
- package/lib/types/catalog/validate.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +35 -0
- package/lib/types/contracts/market.d.ts +13 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +10 -0
- package/lib/types/model/types.d.ts +29 -5
- package/lib/types/runtime/agent-role-names.d.ts +6 -0
- package/lib/types/runtime/agent-role-router.d.ts +59 -52
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +17 -0
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/feedback-tool.d.ts +33 -11
- package/lib/types/runtime/hooks-mounts.d.ts +1 -1
- package/lib/types/runtime/host-locale.d.ts +20 -2
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +69 -19
- package/lib/types/runtime/lsp-status.d.ts +2 -1
- package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
- package/lib/types/runtime/mcp-backend.d.ts +10 -14
- package/lib/types/runtime/mcp-client/config.d.ts +8 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
- package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
- package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
- package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
- package/lib/types/runtime/mcp-config.d.ts +25 -4
- package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
- package/lib/types/runtime/mcp-mounts.d.ts +10 -15
- package/lib/types/runtime/mcp-overrides.d.ts +1 -1
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
- package/lib/types/runtime/profile-seam.d.ts +64 -0
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +32 -0
- package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
- package/lib/types/runtime/storage-migration.d.ts +5 -3
- package/lib/types/runtime/subagent-catalog.d.ts +2 -0
- package/lib/types/runtime/timer-seat.d.ts +51 -0
- package/lib/types/runtime/tool-registry-observer.d.ts +12 -6
- package/lib/types/runtime/user-panels.d.ts +69 -19
- package/lib/types/runtime/user-store.d.ts +67 -20
- package/package.json +68 -24
- package/schemas/README.md +1 -1
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Reading the model-facing MCP tools the host registry currently exposes. */
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The MCP tools the host currently publishes to the model.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Read through the tools service's own listing API rather than its internal
|
|
6
|
+
* layer structure. `schemas()` deep-clones each tool's parameter schema, which
|
|
7
|
+
* this snapshot has no use for, so a failure inside it degrades to an empty
|
|
8
|
+
* observation instead of taking the status surface down.
|
|
9
|
+
* @param tools - the host tools service, when it is mounted.
|
|
10
|
+
* @returns one entry per `mcp__`-namespaced tool.
|
|
8
11
|
*/
|
|
9
|
-
export function inspectToolRegistry(
|
|
10
|
-
|
|
12
|
+
export function inspectToolRegistry(tools) {
|
|
13
|
+
const service = tools;
|
|
14
|
+
if (service === undefined || typeof service !== 'object' || service === null)
|
|
11
15
|
return [];
|
|
12
|
-
|
|
13
|
-
if (typeof layers !== 'object' || layers === null)
|
|
16
|
+
if (typeof service.schemas !== 'function')
|
|
14
17
|
return [];
|
|
15
|
-
const merge = layers.merge;
|
|
16
|
-
if (typeof merge !== 'function')
|
|
17
|
-
return [];
|
|
18
|
-
const empty = { entries: () => [] };
|
|
19
18
|
try {
|
|
20
|
-
const visible = merge.call(layers, undefined, layer => layer.tools ?? empty);
|
|
21
19
|
const output = [];
|
|
22
|
-
for (const
|
|
23
|
-
if (!name.startsWith('mcp__'))
|
|
20
|
+
for (const schema of service.schemas()) {
|
|
21
|
+
if (!schema.name.startsWith('mcp__'))
|
|
24
22
|
continue;
|
|
25
|
-
const description = typeof
|
|
26
|
-
|
|
27
|
-
: undefined;
|
|
28
|
-
output.push({ name, ...(description === undefined ? {} : { description }) });
|
|
23
|
+
const description = typeof schema.description === 'string' ? schema.description : undefined;
|
|
24
|
+
output.push({ name: schema.name, ...(description === undefined ? {} : { description }) });
|
|
29
25
|
}
|
|
30
26
|
return output;
|
|
31
27
|
}
|
|
@@ -33,3 +29,7 @@ export function inspectToolRegistry(runtime) {
|
|
|
33
29
|
return [];
|
|
34
30
|
}
|
|
35
31
|
}
|
|
32
|
+
/** Read the tools service off the plugin context, when the host mounts one. */
|
|
33
|
+
export function toolsServiceOf(ctx) {
|
|
34
|
+
return ctx.get?.('tools');
|
|
35
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* change, keyed `user/<name>`.
|
|
7
7
|
* @module runtime/user-commands
|
|
8
8
|
*/
|
|
9
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
9
10
|
import { USER_ENTRY_NAME } from './user-store.js';
|
|
10
11
|
/** Register/unregister user-panel commands to match the panel's enabled entries. */
|
|
11
12
|
export class UserCommandMountRegistry {
|
|
@@ -62,9 +63,12 @@ export class UserCommandMountRegistry {
|
|
|
62
63
|
...(spec.hint === undefined ? {} : { input: { hint: spec.hint } }),
|
|
63
64
|
handler: invocation => {
|
|
64
65
|
const agent = invocation.agent;
|
|
65
|
-
const text =
|
|
66
|
-
agent.followup({
|
|
67
|
-
|
|
66
|
+
const text = spec.body.replaceAll('$ARGUMENTS', invocation.rawInput.trim());
|
|
67
|
+
agent.followup(createUserMessage({
|
|
68
|
+
content: [{ type: 'text', text }],
|
|
69
|
+
source: { kind: 'plugin', plugin: 'dsh-agent-plugins-market' }
|
|
70
|
+
}));
|
|
71
|
+
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name }) };
|
|
68
72
|
}
|
|
69
73
|
});
|
|
70
74
|
this.live.set(key, disposer);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User panel storage: the persistence layer behind the skills / commands /
|
|
3
|
-
* agent-personas panels. Each surface owns one directory of Markdown
|
|
4
|
-
* under
|
|
5
|
-
* are trivially hand-editable, and
|
|
3
|
+
* agent-personas panels. Each surface owns one directory of Markdown
|
|
4
|
+
* documents under the shared Agent layout root (`~/.agents/<kind>/`), so
|
|
5
|
+
* user-authored entries survive restarts, are trivially hand-editable, and
|
|
6
|
+
* stay outside suite checkouts.
|
|
6
7
|
*
|
|
7
8
|
* Skills entries follow the SKILL.md frontmatter grammar (`name`,
|
|
8
9
|
* `description`, optional `whenToUse`, invocation controls) plus a
|
|
@@ -10,75 +11,133 @@
|
|
|
10
11
|
* frontmatter (`description`, optional `argument-hint`, `disabled`); agent
|
|
11
12
|
* personas follow Claude Code agents frontmatter (`name`, `description`,
|
|
12
13
|
* optional `whenToUse`, `tools`, `model`, `disabled`).
|
|
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.
|
|
17
|
+
*
|
|
18
|
+
* 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.
|
|
13
24
|
* @module runtime/user-panels
|
|
14
25
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
26
|
+
import { dirname } from 'node:path';
|
|
27
|
+
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';
|
|
17
29
|
/** The skill-source label user-panel entries carry into the skill registry. */
|
|
18
30
|
export const USER_PANEL_SKILL_SOURCE = 'user-panel';
|
|
19
|
-
/**
|
|
20
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Panel skills register ahead of the harness reader that maps this same
|
|
33
|
+
* directory (`dsh-skill-filesystem` roots `~/.agents/skills` as `user-agents`
|
|
34
|
+
* at rank 500), so the panel's localized description and its name precedence
|
|
35
|
+
* apply to the entries it serves. Project roots (100-300) and the user's
|
|
36
|
+
* `~/.dsh` skills (400) still outrank it, and it beats the suite user rank
|
|
37
|
+
* (450), so a skill the user wrote by hand wins over one an installed suite
|
|
38
|
+
* ships under the same name.
|
|
39
|
+
*
|
|
40
|
+
* Switching a skill off is not this provider's job: the panel writes the
|
|
41
|
+
* harness's own invocation pair into the document, which every reader of that
|
|
42
|
+
* file honors.
|
|
43
|
+
*/
|
|
44
|
+
const USER_PANEL_RANK = 440;
|
|
21
45
|
/** Throwing CRUD over one panel directory, shared by the three panels. */
|
|
22
46
|
export class UserPanelStore {
|
|
23
|
-
|
|
47
|
+
agentsRoot;
|
|
24
48
|
kind;
|
|
49
|
+
shapes;
|
|
25
50
|
extraNameCheck;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
51
|
+
validate;
|
|
52
|
+
honorInvocationControls;
|
|
53
|
+
constructor(agentsRoot, kind, options = {}) {
|
|
54
|
+
this.agentsRoot = agentsRoot;
|
|
30
55
|
this.kind = kind;
|
|
31
|
-
this.
|
|
56
|
+
this.shapes = options.shapes ?? ['file'];
|
|
57
|
+
this.extraNameCheck = options.extraNameCheck ?? (() => true);
|
|
58
|
+
this.validate = options.validate;
|
|
59
|
+
this.honorInvocationControls = options.honorInvocationControls === true;
|
|
32
60
|
}
|
|
33
|
-
/** The panel's directory under the
|
|
61
|
+
/** The panel's directory under the Agent layout root. */
|
|
34
62
|
dirPath() {
|
|
35
|
-
return userEntryDir(this.
|
|
36
|
-
}
|
|
37
|
-
/** The plugin data root the panel lives under. */
|
|
38
|
-
root() {
|
|
39
|
-
return this.dataRoot;
|
|
63
|
+
return userEntryDir(this.agentsRoot, this.kind);
|
|
40
64
|
}
|
|
41
65
|
get dir() {
|
|
42
66
|
return this.dirPath();
|
|
43
67
|
}
|
|
68
|
+
validName(name) {
|
|
69
|
+
return USER_ENTRY_NAME.test(name) && this.extraNameCheck(name);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 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
|
|
75
|
+
* declared names of the served documents.
|
|
76
|
+
*/
|
|
77
|
+
async document(name) {
|
|
78
|
+
const direct = await resolveEntryDocument(this.dir, name, this.shapes);
|
|
79
|
+
if (direct !== undefined)
|
|
80
|
+
return direct;
|
|
81
|
+
for (const candidate of await listEntryDocuments(this.dir, false, this.shapes)) {
|
|
82
|
+
const file = await readEntryDocument(candidate);
|
|
83
|
+
if (file?.name === name)
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
44
88
|
/** Every entry, sorted by name; strict snapshots propagate temporary I/O failures. */
|
|
45
89
|
async list(strict = false) {
|
|
46
|
-
const files = await listEntryFiles(this.dir, strict);
|
|
90
|
+
const files = await listEntryFiles(this.dir, strict, this.shapes);
|
|
47
91
|
return files.map(file => this.serialize(file));
|
|
48
92
|
}
|
|
49
93
|
/** One entry's full record, including the raw file text. */
|
|
50
94
|
async get(name) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const file = await readEntryFile(join(this.dir, `${name}.md`), name);
|
|
54
|
-
if (file === undefined)
|
|
95
|
+
const document = await this.document(name);
|
|
96
|
+
if (document === undefined)
|
|
55
97
|
return undefined;
|
|
56
|
-
|
|
98
|
+
const file = await readEntryDocument(document);
|
|
99
|
+
return file === undefined ? undefined : this.serialize(file);
|
|
57
100
|
}
|
|
58
101
|
/**
|
|
59
102
|
* Project one parsed file onto the wire shape. The description prefers the
|
|
60
103
|
* strict frontmatter parse (multi-line YAML like `description: |` survives
|
|
61
104
|
* intact there) and falls back to the shallow line record.
|
|
105
|
+
*
|
|
106
|
+
* A document the panel must not register stays listed and editable but reads
|
|
107
|
+
* as disabled with the reason in its metadata, the same shape an unparseable
|
|
108
|
+
* frontmatter already produced — hiding it would leave the user no way to
|
|
109
|
+
* find or fix it.
|
|
62
110
|
*/
|
|
63
111
|
serialize(file) {
|
|
64
112
|
const strictDescription = typeof file.description === 'string' && file.description !== '' ? file.description : undefined;
|
|
65
113
|
const shallowDescription = typeof file.meta['description'] === 'string' && file.meta['description'] !== '' ? file.meta['description'] : '';
|
|
114
|
+
const parseError = typeof file.meta['validationError'] === 'string' ? file.meta['validationError'] : undefined;
|
|
115
|
+
const rejection = parseError ?? this.validate?.(file);
|
|
116
|
+
// Both invocation controls off is the panel's off state as well: the switch
|
|
117
|
+
// writes that pair, and either key alone is an authoring choice (a
|
|
118
|
+
// user-invocable-only skill, for instance) that stays switched on.
|
|
119
|
+
const invocationOff = this.honorInvocationControls && file.invocation !== undefined && !file.invocation.modelInvocable && !file.invocation.userInvocable;
|
|
66
120
|
return {
|
|
67
|
-
name
|
|
121
|
+
// The declared name is the entry's name; a document that declares none
|
|
122
|
+
// the registry would accept is listed under its own file name, which is
|
|
123
|
+
// the name that keeps it addressable and fixable. `path` carries the
|
|
124
|
+
// document either way.
|
|
125
|
+
name: rejection === undefined ? file.name : file.documentName,
|
|
68
126
|
description: strictDescription ?? shallowDescription,
|
|
69
|
-
disabled: file.meta['disabled'] === true,
|
|
70
|
-
metadata: file.meta,
|
|
127
|
+
disabled: file.meta['disabled'] === true || invocationOff || rejection !== undefined,
|
|
128
|
+
metadata: rejection === undefined ? file.meta : { ...file.meta, disabled: true, validationError: rejection },
|
|
71
129
|
path: file.file,
|
|
130
|
+
shape: file.shape,
|
|
72
131
|
content: file.body,
|
|
73
132
|
rawText: file.text,
|
|
74
133
|
origin: 'user'
|
|
75
134
|
};
|
|
76
135
|
}
|
|
77
|
-
/** Create an entry; refuses an occupied name. */
|
|
136
|
+
/** Create an entry; refuses an occupied name. New entries use the flat spelling. */
|
|
78
137
|
async create(name, text) {
|
|
79
|
-
if (!
|
|
138
|
+
if (!this.validName(name))
|
|
80
139
|
throw new Error(`invalid name "${name}" — use lowercase letters, digits, and dashes, starting with a letter or digit`);
|
|
81
|
-
if (await entryExists(this.dir, name))
|
|
140
|
+
if (await entryExists(this.dir, name, this.shapes))
|
|
82
141
|
throw new Error(`an entry named "${name}" already exists`);
|
|
83
142
|
await writeEntryFile(this.dir, name, text);
|
|
84
143
|
const created = await this.get(name);
|
|
@@ -86,27 +145,36 @@ export class UserPanelStore {
|
|
|
86
145
|
throw new Error(`entry "${name}" vanished after write`);
|
|
87
146
|
return created;
|
|
88
147
|
}
|
|
89
|
-
/** Replace one entry's
|
|
148
|
+
/** Replace one entry's content wholesale, keeping the spelling it already has. */
|
|
90
149
|
async update(name, text) {
|
|
91
|
-
if (!
|
|
150
|
+
if (!this.validName(name))
|
|
92
151
|
throw new Error(`invalid entry name "${name}"`);
|
|
93
|
-
|
|
152
|
+
const document = await this.document(name);
|
|
153
|
+
if (document === undefined)
|
|
94
154
|
throw new Error(`no entry named "${name}"`);
|
|
95
|
-
await
|
|
155
|
+
await writeEntryDocument(document, text);
|
|
96
156
|
}
|
|
97
157
|
/** Delete one entry; a missing file is a no-op (idempotent delete). */
|
|
98
158
|
async remove(name) {
|
|
99
159
|
if (!USER_ENTRY_NAME.test(name))
|
|
100
160
|
throw new Error(`invalid entry name "${name}"`);
|
|
101
|
-
await
|
|
161
|
+
const document = await this.document(name);
|
|
162
|
+
if (document === undefined)
|
|
163
|
+
return;
|
|
164
|
+
await deleteEntryDocument(document);
|
|
102
165
|
}
|
|
103
166
|
}
|
|
104
167
|
/** Locate the three stores; constructed once per plugin activation. */
|
|
105
|
-
export function createUserPanelStores(
|
|
168
|
+
export function createUserPanelStores(agentsRoot) {
|
|
106
169
|
return {
|
|
107
|
-
skills: new UserPanelStore(
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
skills: new UserPanelStore(agentsRoot, 'skills', {
|
|
171
|
+
extraNameCheck: isUserSkillEntryName,
|
|
172
|
+
shapes: SKILL_ENTRY_SHAPES,
|
|
173
|
+
validate: file => skillEntryRejection(file.text),
|
|
174
|
+
honorInvocationControls: true
|
|
175
|
+
}),
|
|
176
|
+
commands: new UserPanelStore(agentsRoot, 'commands'),
|
|
177
|
+
agents: new UserPanelStore(agentsRoot, 'agents', { extraNameCheck: isUserSkillEntryName })
|
|
110
178
|
};
|
|
111
179
|
}
|
|
112
180
|
/**
|
|
@@ -125,11 +193,9 @@ export function isUserSkillEntryName(name) {
|
|
|
125
193
|
*/
|
|
126
194
|
export class UserPanelSkillProvider {
|
|
127
195
|
skills;
|
|
128
|
-
t;
|
|
129
196
|
name = 'user-panel';
|
|
130
|
-
constructor(skills
|
|
197
|
+
constructor(skills) {
|
|
131
198
|
this.skills = skills;
|
|
132
|
-
this.t = t;
|
|
133
199
|
}
|
|
134
200
|
async list(_options) {
|
|
135
201
|
const candidates = [];
|
|
@@ -138,8 +204,13 @@ export class UserPanelSkillProvider {
|
|
|
138
204
|
if (entry.disabled || !isUserSkillEntryName(entry.name))
|
|
139
205
|
continue;
|
|
140
206
|
candidates.push({
|
|
207
|
+
// The entry's name is the one its document declares, which is also the
|
|
208
|
+
// name the harness's own reader derives from the same file.
|
|
141
209
|
name: entry.name,
|
|
142
|
-
|
|
210
|
+
// The entry's own description verbatim; a user-entry label is our
|
|
211
|
+
// packaging, and this description reaches the same model-facing
|
|
212
|
+
// catalog a suite skill does.
|
|
213
|
+
description: entry.description === '' ? entry.name : entry.description,
|
|
143
214
|
...(typeof entry.metadata['whenToUse'] === 'string' && entry.metadata['whenToUse'] !== '' ? { whenToUse: entry.metadata['whenToUse'] } : {}),
|
|
144
215
|
invocation: {
|
|
145
216
|
modelInvocable: entry.metadata['disable-model-invocation'] !== true,
|
|
@@ -150,7 +221,7 @@ export class UserPanelSkillProvider {
|
|
|
150
221
|
rank: USER_PANEL_RANK,
|
|
151
222
|
locator: { name: entry.name },
|
|
152
223
|
path: entry.path,
|
|
153
|
-
resourceBase: { kind: 'directory', path: this.
|
|
224
|
+
resourceBase: { kind: 'directory', path: this.resourceDirectory(entry) }
|
|
154
225
|
});
|
|
155
226
|
}
|
|
156
227
|
return candidates;
|
|
@@ -168,9 +239,17 @@ export class UserPanelSkillProvider {
|
|
|
168
239
|
invocation: candidate.invocation,
|
|
169
240
|
source: candidate.source,
|
|
170
241
|
provider: this.name,
|
|
171
|
-
resourceBase: { kind: 'directory', path:
|
|
242
|
+
resourceBase: { kind: 'directory', path: this.resourceDirectory(entry) },
|
|
172
243
|
path: entry.path,
|
|
173
244
|
content: entry.content
|
|
174
245
|
};
|
|
175
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Relative resources resolve beside the document that references them: a
|
|
249
|
+
* directory-shaped skill owns `references/`, `scripts/` and the like next to
|
|
250
|
+
* its `SKILL.md`, while a flat entry resolves against the panel directory.
|
|
251
|
+
*/
|
|
252
|
+
resourceDirectory(entry) {
|
|
253
|
+
return entry.shape === 'skill-directory' ? dirname(entry.path) : this.skills.dirPath();
|
|
254
|
+
}
|
|
176
255
|
}
|
|
@@ -1,34 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared persistence primitives for the market's user-authored panels:
|
|
3
3
|
* skills, commands, and agent personas. Each panel owns one directory of
|
|
4
|
-
* Markdown
|
|
5
|
-
* rules the catalog uses, so user entries look exactly like suite
|
|
6
|
-
* the runtime mounts.
|
|
4
|
+
* Markdown documents under the Agent layout root, parsed with the same
|
|
5
|
+
* frontmatter rules the catalog uses, so user entries look exactly like suite
|
|
6
|
+
* entries to the runtime mounts.
|
|
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.
|
|
7
11
|
* @module runtime/user-store
|
|
8
12
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import { join } from 'node:path';
|
|
13
|
+
import { readdir, readFile, rmdir, rm, stat } from 'node:fs/promises';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
11
16
|
import { parseDocument, stringify } from 'yaml';
|
|
12
17
|
import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse.js';
|
|
13
|
-
/** Where user panel entries persist: `<
|
|
14
|
-
export function userEntryDir(
|
|
15
|
-
return join(
|
|
18
|
+
/** Where user panel entries persist: `<agentsRoot>/<kind>/`. */
|
|
19
|
+
export function userEntryDir(agentsRoot, kind) {
|
|
20
|
+
return join(agentsRoot, kind);
|
|
16
21
|
}
|
|
17
22
|
/** `[a-z][a-z0-9_-]*` — the grammar every panel entry name must satisfy. */
|
|
18
23
|
export const USER_ENTRY_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
19
|
-
/** The
|
|
20
|
-
export const
|
|
24
|
+
/** The file name of a directory-shaped skill's document. */
|
|
25
|
+
export const SKILL_ENTRY_FILE = 'SKILL.md';
|
|
26
|
+
/**
|
|
27
|
+
* The spellings the skills panel serves, most preferred first: a name present
|
|
28
|
+
* as both a directory and a file resolves to the directory, which is the
|
|
29
|
+
* canonical cross-tool spelling and the one the harness's own reader of
|
|
30
|
+
* `~/.agents/skills` reaches first.
|
|
31
|
+
*/
|
|
32
|
+
export const SKILL_ENTRY_SHAPES = ['skill-directory', 'file'];
|
|
33
|
+
/** The document path one entry name maps to under its kind directory. */
|
|
34
|
+
export function entryDocumentPath(dir, name, shape) {
|
|
35
|
+
return shape === 'skill-directory' ? join(dir, name, SKILL_ENTRY_FILE) : join(dir, `${name}.md`);
|
|
36
|
+
}
|
|
37
|
+
/** Whether a path is a regular file; strict runtime snapshots propagate real I/O failures. */
|
|
38
|
+
async function isFile(file, strict) {
|
|
39
|
+
try {
|
|
40
|
+
return (await stat(file)).isFile();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (strict && error.code !== 'ENOENT')
|
|
44
|
+
throw error;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 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.
|
|
51
|
+
*/
|
|
52
|
+
export async function resolveEntryDocument(dir, name, shapes, strict = false) {
|
|
53
|
+
if (!USER_ENTRY_NAME.test(name))
|
|
54
|
+
return undefined;
|
|
55
|
+
for (const shape of shapes) {
|
|
56
|
+
const file = entryDocumentPath(dir, name, shape);
|
|
57
|
+
if (await isFile(file, strict))
|
|
58
|
+
return { name, file, directory: dirname(file), shape };
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
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']) {
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(dir);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (strict && error.code !== 'ENOENT')
|
|
70
|
+
throw error;
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const found = new Map();
|
|
74
|
+
for (const entry of entries.sort()) {
|
|
75
|
+
const name = entry.endsWith('.md') ? entry.slice(0, -3) : shapes.includes('skill-directory') ? entry : undefined;
|
|
76
|
+
if (name === undefined || !USER_ENTRY_NAME.test(name))
|
|
77
|
+
continue;
|
|
78
|
+
for (const shape of shapes) {
|
|
79
|
+
const file = entryDocumentPath(dir, name, shape);
|
|
80
|
+
if (!(await isFile(file, strict)))
|
|
81
|
+
continue;
|
|
82
|
+
found.set(name, { name, file, directory: dirname(file), shape });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...found.values()];
|
|
87
|
+
}
|
|
21
88
|
/** Parse full YAML metadata without flattening arrays, mappings, or multiline strings. */
|
|
22
89
|
export function parseFrontmatterRecord(text) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
90
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
91
|
+
if (frontmatter === undefined) {
|
|
25
92
|
if (/^---(?:\r?\n|$)/.test(text))
|
|
26
93
|
throw new Error('Invalid frontmatter: missing closing delimiter');
|
|
27
94
|
return {};
|
|
28
95
|
}
|
|
29
|
-
const doc = parseDocument(
|
|
30
|
-
|
|
31
|
-
|
|
96
|
+
const doc = parseDocument(frontmatter);
|
|
97
|
+
const error = doc.errors[0];
|
|
98
|
+
if (error !== undefined)
|
|
99
|
+
throw new Error(`Invalid frontmatter: ${error.message}`);
|
|
32
100
|
const value = doc.toJS({ maxAliasCount: 100 });
|
|
33
101
|
if (value === null)
|
|
34
102
|
return {};
|
|
@@ -36,13 +104,11 @@ export function parseFrontmatterRecord(text) {
|
|
|
36
104
|
throw new Error('Frontmatter must be a mapping');
|
|
37
105
|
return value;
|
|
38
106
|
}
|
|
39
|
-
/** Read one entry; strict runtime snapshots throw on I/O errors other than a vanished file. */
|
|
40
|
-
export async function
|
|
41
|
-
if (fallbackName !== '' && !USER_ENTRY_NAME.test(fallbackName))
|
|
42
|
-
return undefined;
|
|
107
|
+
/** Read one resolved entry; strict runtime snapshots throw on I/O errors other than a vanished file. */
|
|
108
|
+
export async function readEntryDocument(document, strict = false) {
|
|
43
109
|
let text;
|
|
44
110
|
try {
|
|
45
|
-
text = await readFile(file, 'utf8');
|
|
111
|
+
text = await readFile(document.file, 'utf8');
|
|
46
112
|
}
|
|
47
113
|
catch (error) {
|
|
48
114
|
if (strict && error.code !== 'ENOENT')
|
|
@@ -56,56 +122,45 @@ export async function readEntryFile(file, fallbackName, strict = false) {
|
|
|
56
122
|
catch (error) {
|
|
57
123
|
meta = { disabled: true, validationError: String(error) };
|
|
58
124
|
}
|
|
59
|
-
|
|
60
|
-
|
|
125
|
+
// The declared name is read without an expected-value check: the harness's
|
|
126
|
+
// own reader of this directory takes the frontmatter name as the skill's
|
|
127
|
+
// name and never compares it to the file or directory name.
|
|
128
|
+
const parsed = parseSkillFrontmatter(text, undefined);
|
|
129
|
+
const name = typeof parsed === 'string' ? document.name : typeof parsed.name === 'string' && parsed.name !== '' ? parsed.name : document.name;
|
|
61
130
|
return {
|
|
62
|
-
|
|
131
|
+
documentName: document.name,
|
|
63
132
|
name,
|
|
64
|
-
file,
|
|
133
|
+
file: document.file,
|
|
134
|
+
directory: document.directory,
|
|
135
|
+
shape: document.shape,
|
|
65
136
|
text,
|
|
66
137
|
body: stripFrontmatter(text),
|
|
67
138
|
meta,
|
|
68
139
|
...(typeof parsed === 'string' ? {} : parsed)
|
|
69
140
|
};
|
|
70
141
|
}
|
|
71
|
-
/** List
|
|
72
|
-
export async function listEntryFiles(dir, strict = false) {
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
entries = await readdir(dir);
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
if (strict && error.code !== 'ENOENT')
|
|
79
|
-
throw error;
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
142
|
+
/** 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);
|
|
82
145
|
const found = [];
|
|
83
|
-
for (const
|
|
84
|
-
|
|
85
|
-
continue;
|
|
86
|
-
const file = join(dir, entry);
|
|
87
|
-
try {
|
|
88
|
-
const info = await stat(file);
|
|
89
|
-
if (!info.isFile())
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
if (strict && error.code !== 'ENOENT')
|
|
94
|
-
throw error;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const parsed = await readEntryFile(file, entry.slice(0, -3), strict);
|
|
146
|
+
for (const document of documents) {
|
|
147
|
+
const parsed = await readEntryDocument(document, strict);
|
|
98
148
|
if (parsed !== undefined)
|
|
99
149
|
found.push(parsed);
|
|
100
150
|
}
|
|
101
151
|
return found;
|
|
102
152
|
}
|
|
103
|
-
/**
|
|
104
|
-
export async function writeEntryFile(dir, name, text) {
|
|
153
|
+
/** Create one panel entry's document from its name: `<name>.md`, or `<name>/SKILL.md` in the directory shape. */
|
|
154
|
+
export async function writeEntryFile(dir, name, text, shape = 'file') {
|
|
105
155
|
assertEntryName(name);
|
|
156
|
+
const file = entryDocumentPath(dir, name, shape);
|
|
157
|
+
await writeEntryDocument({ name, file, directory: dirname(file), shape }, text);
|
|
158
|
+
}
|
|
159
|
+
/** Replace one resolved entry's document in place, wherever it lives. */
|
|
160
|
+
export async function writeEntryDocument(document, text) {
|
|
161
|
+
assertEntryName(document.name);
|
|
106
162
|
parseFrontmatterRecord(text);
|
|
107
|
-
await
|
|
108
|
-
await writeFile(join(dir, `${name}.md`), text, 'utf8');
|
|
163
|
+
await writeFileAtomic(document.file, text, { mode: 0o644, dirMode: 0o700 });
|
|
109
164
|
}
|
|
110
165
|
/**
|
|
111
166
|
* Enforce the entry-name grammar at every filesystem choke point. The name
|
|
@@ -117,22 +172,22 @@ function assertEntryName(name) {
|
|
|
117
172
|
if (!USER_ENTRY_NAME.test(name))
|
|
118
173
|
throw new Error(`invalid entry name "${name}" — use lowercase letters, digits, dashes, or underscores, starting with a letter`);
|
|
119
174
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Delete one resolved entry's document; a missing file resolves silently. A
|
|
177
|
+
* skill directory belongs to whichever tool authored it, so removal deletes
|
|
178
|
+
* the document it served and the directory only once nothing else is left in
|
|
179
|
+
* it — never the `references/`, `scripts/`, or persona files beside its
|
|
180
|
+
* `SKILL.md`.
|
|
181
|
+
*/
|
|
182
|
+
export async function deleteEntryDocument(document) {
|
|
183
|
+
assertEntryName(document.name);
|
|
184
|
+
await rm(document.file, { force: true });
|
|
185
|
+
if (document.shape === 'skill-directory')
|
|
186
|
+
await rmdir(dirname(document.file)).catch(() => undefined);
|
|
124
187
|
}
|
|
125
|
-
/** Whether an entry
|
|
126
|
-
export async function entryExists(dir, name) {
|
|
127
|
-
|
|
128
|
-
return false;
|
|
129
|
-
try {
|
|
130
|
-
await stat(join(dir, `${name}.md`));
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
188
|
+
/** 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;
|
|
136
191
|
}
|
|
137
192
|
/** Serialize a frontmatter block from a shallow record (deterministic key order). */
|
|
138
193
|
export function serializeFrontmatter(meta) {
|