dsh-agent-plugins-market 0.7.0 → 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 +2 -2
- package/README.zh.md +2 -2
- package/client/client.js +28048 -1033
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +8 -1
- package/lib/application/catalog.js +13 -0
- package/lib/application/details.js +13 -2
- package/lib/application/mcp-service.js +17 -0
- package/lib/application/panel-resources.js +16 -4
- package/lib/application/project-agent-roles.js +7 -1
- package/lib/catalog/archive.js +1 -1
- package/lib/catalog/paths.js +8 -0
- package/lib/catalog/plugin-variables.js +38 -0
- package/lib/catalog/project-hooks.js +14 -1
- package/lib/catalog/skills-parse.js +47 -0
- package/lib/catalog/source-catalog.js +8 -3
- package/lib/catalog/suite-components.js +2 -2
- package/lib/contracts/market.js +1 -0
- package/lib/contracts/settings.js +63 -0
- package/lib/index.js +26 -11
- package/lib/model/layouts.js +10 -1
- package/lib/routes.js +12 -0
- package/lib/runtime/agent-role-router.js +28 -6
- package/lib/runtime/commands-mounts.js +50 -9
- package/lib/runtime/deadline.js +32 -6
- package/lib/runtime/dynamic-context.js +140 -0
- package/lib/runtime/feedback-tool.js +7 -3
- package/lib/runtime/host-locale.js +39 -3
- package/lib/runtime/lsp-mounts.js +23 -2
- package/lib/runtime/mcp-backend.js +14 -15
- package/lib/runtime/mcp-client/config.js +8 -0
- package/lib/runtime/mcp-client/connection.js +6 -6
- package/lib/runtime/mcp-client/host-seams.js +8 -88
- package/lib/runtime/mcp-config.js +7 -4
- package/lib/runtime/project-runtime.js +23 -9
- package/lib/runtime/reconcile-scheduler.js +6 -8
- package/lib/runtime/reconciler.js +2 -1
- package/lib/runtime/regions.js +0 -19
- package/lib/runtime/settings-namespace.js +21 -13
- package/lib/runtime/skills-provider.js +33 -11
- package/lib/runtime/source-auto-update.js +10 -14
- package/lib/runtime/timer-seat.js +80 -0
- package/lib/runtime/tool-registry-observer.js +20 -20
- package/lib/runtime/user-commands.js +5 -1
- package/lib/runtime/user-panels.js +110 -41
- package/lib/runtime/user-store.js +114 -58
- package/lib/types/application/catalog-context.d.ts +6 -0
- package/lib/types/application/catalog.d.ts +9 -0
- package/lib/types/application/mcp-service.d.ts +6 -0
- package/lib/types/application/ports.d.ts +1 -1
- package/lib/types/application/queries.d.ts +2 -0
- package/lib/types/catalog/paths.d.ts +6 -0
- package/lib/types/catalog/plugin-variables.d.ts +16 -0
- package/lib/types/catalog/skills-parse.d.ts +13 -0
- package/lib/types/catalog/source-catalog.d.ts +7 -2
- package/lib/types/contracts/market.d.ts +10 -0
- package/lib/types/contracts/settings.d.ts +59 -0
- package/lib/types/model/layouts.d.ts +10 -0
- package/lib/types/runtime/agent-role-router.d.ts +6 -2
- package/lib/types/runtime/commands-mounts.d.ts +15 -7
- package/lib/types/runtime/deadline.d.ts +10 -11
- package/lib/types/runtime/dynamic-context.d.ts +81 -0
- package/lib/types/runtime/host-locale.d.ts +19 -1
- package/lib/types/runtime/lsp-mounts.d.ts +7 -0
- package/lib/types/runtime/mcp-backend.d.ts +8 -15
- package/lib/types/runtime/mcp-client/config.d.ts +8 -0
- package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
- package/lib/types/runtime/project-runtime.d.ts +10 -3
- package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
- package/lib/types/runtime/regions.d.ts +1 -4
- package/lib/types/runtime/settings-namespace.d.ts +10 -10
- package/lib/types/runtime/skills-provider.d.ts +11 -2
- package/lib/types/runtime/source-auto-update.d.ts +3 -4
- 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 +65 -12
- package/lib/types/runtime/user-store.d.ts +65 -16
- package/package.json +21 -4
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* notification path — have a single owner.
|
|
9
9
|
*/
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { MARKET_SETTINGS_DEFAULTS } from '../contracts/settings.js';
|
|
11
12
|
import { STATE_FILE_NAME } from '../catalog/paths.js';
|
|
12
13
|
import { settlesWithin } from '../runtime/deadline.js';
|
|
13
14
|
import { EMPTY_STATE, loadState, saveState } from '../runtime/state-store.js';
|
|
@@ -27,7 +28,13 @@ export class CatalogContext {
|
|
|
27
28
|
mutationQueue = Promise.resolve();
|
|
28
29
|
currentRevision = 0;
|
|
29
30
|
currentScanGeneration = 0;
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The project-layout switch in effect. The host settings document owns the
|
|
33
|
+
* value and pushes it here on registration and on every change; the initial
|
|
34
|
+
* value is the contract's declared default, which is what the catalog would
|
|
35
|
+
* read from the document before the settings service resolves.
|
|
36
|
+
*/
|
|
37
|
+
scanProjectLayoutsEnabled = MARKET_SETTINGS_DEFAULTS.scanProjectLayouts;
|
|
31
38
|
/** The derived refresh in flight, shared by every change that lands while it runs. */
|
|
32
39
|
refreshPass;
|
|
33
40
|
/** Whether a change landed after the running pass started; it earns one more pass. */
|
|
@@ -48,6 +48,10 @@ export class Catalog {
|
|
|
48
48
|
get userRoot() {
|
|
49
49
|
return this.context.userRoot;
|
|
50
50
|
}
|
|
51
|
+
/** The plugin storage root holding per-suite `${PLUGIN_DATA}` directories and overrides. */
|
|
52
|
+
get dataRoot() {
|
|
53
|
+
return this.context.dataRoot;
|
|
54
|
+
}
|
|
51
55
|
get sources() {
|
|
52
56
|
return this.context.state.sources;
|
|
53
57
|
}
|
|
@@ -238,6 +242,15 @@ export class Catalog {
|
|
|
238
242
|
async setMcpOverride(sourceId, suiteId, serverKey, override) {
|
|
239
243
|
await this.mcp.setOverride(sourceId, suiteId, serverKey, override);
|
|
240
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Enable or disable one declared MCP server, addressed by the same
|
|
247
|
+
* source-qualified suite id its status row carries. Disabling writes an
|
|
248
|
+
* override rather than editing the suite's `mcp.json`, so a refresh cannot
|
|
249
|
+
* clobber the user's choice.
|
|
250
|
+
*/
|
|
251
|
+
async setMcpServerEnabled(suiteKey, serverKey, enabled) {
|
|
252
|
+
await this.mcp.setServerEnabled(suiteKey, serverKey, enabled);
|
|
253
|
+
}
|
|
241
254
|
/** Re-run the MCP reconcile pass without changing any catalog state. */
|
|
242
255
|
async retryMounts() {
|
|
243
256
|
await this.mcp.retryMounts();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Suite detail and preview projections owned by the application layer. */
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { discoverLspEntries } from '../catalog/surfaces.js';
|
|
5
5
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
@@ -7,6 +7,16 @@ import { credentialRefsInServer } from '../runtime/mcp-config.js';
|
|
|
7
7
|
import { redactMcpConfig, redactMcpOverrides } from '../runtime/mcp-redaction.js';
|
|
8
8
|
import { applyOverride } from '../runtime/mcp-overrides.js';
|
|
9
9
|
import { effectiveSurfaces } from '../model/types.js';
|
|
10
|
+
/** The checkout's last modification, or null when the path cannot be read. */
|
|
11
|
+
async function rootModifiedAt(root) {
|
|
12
|
+
try {
|
|
13
|
+
const stats = await stat(root);
|
|
14
|
+
return new Date(stats.mtimeMs).toISOString();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
/** Build the detail response for one normalized suite. */
|
|
11
21
|
export async function buildSuiteDetail(suite, installed, diagnostics, mcpOverrides = {}) {
|
|
12
22
|
const remoteUrl = suite.remote?.url;
|
|
@@ -18,6 +28,7 @@ export async function buildSuiteDetail(suite, installed, diagnostics, mcpOverrid
|
|
|
18
28
|
description: suite.manifest.description ?? null,
|
|
19
29
|
author: suite.manifest.author ?? null,
|
|
20
30
|
keywords: suite.manifest.keywords ?? [],
|
|
31
|
+
updatedAt: await rootModifiedAt(suite.root),
|
|
21
32
|
layout: suite.manifest.layout,
|
|
22
33
|
dimension: suite.dimension,
|
|
23
34
|
root: remoteUrl ?? suite.root,
|
|
@@ -66,7 +77,7 @@ export async function readSkillContent(suite, skillName) {
|
|
|
66
77
|
content = await readFile(skill.file, 'utf8');
|
|
67
78
|
}
|
|
68
79
|
catch (error) {
|
|
69
|
-
throw new Error(`skill file unreadable: ${error instanceof Error ? error.message : String(error)}
|
|
80
|
+
throw new Error(`skill file unreadable: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
70
81
|
}
|
|
71
82
|
return { name: skill.name, description: skill.description, content, path: skill.file };
|
|
72
83
|
}
|
|
@@ -139,8 +139,25 @@ export class McpService {
|
|
|
139
139
|
}
|
|
140
140
|
await saveSuiteOverrides(this.context.dataRoot, suiteKey, overrides);
|
|
141
141
|
await this.context.notifyChanged(true);
|
|
142
|
+
// The status surface reads the live tool registry, so an immediate read
|
|
143
|
+
// races the reconciler's teardown: a just-disabled server still carries
|
|
144
|
+
// its observed tools and reports the orphaned state with its switch on
|
|
145
|
+
// until the pass finishes. Joining the pass here is bounded — one stuck
|
|
146
|
+
// mount costs the request its deadline, never the plugin.
|
|
147
|
+
await this.context.refreshSettled();
|
|
142
148
|
});
|
|
143
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Enable or disable one declared server, addressed by the source-qualified
|
|
152
|
+
* suite id the status rows carry. The qualified id is split here rather than
|
|
153
|
+
* by callers so the separator stays a server-side detail.
|
|
154
|
+
*/
|
|
155
|
+
async setServerEnabled(suiteKey, serverKey, enabled) {
|
|
156
|
+
const separator = suiteKey.indexOf('/');
|
|
157
|
+
if (separator <= 0)
|
|
158
|
+
throw new Error(`invalid suite id "${suiteKey}"`);
|
|
159
|
+
await this.setOverride(suiteKey.slice(0, separator), suiteKey.slice(separator + 1), serverKey, { enabled });
|
|
160
|
+
}
|
|
144
161
|
/**
|
|
145
162
|
* Re-run the MCP reconcile pass: retries failed mounts and clears residual
|
|
146
163
|
* tools, without changing any catalog state.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Installed suite and user resources share one inventory; paths never come from HTTP callers. */
|
|
2
|
-
import { realpath, unlink } from 'node:fs/promises';
|
|
2
|
+
import { realpath, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
4
4
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
5
|
-
import {
|
|
5
|
+
import { pluginRootOf } from '../catalog/plugin-variables.js';
|
|
6
|
+
import { isWithin, suiteDataDir } from '../catalog/paths.js';
|
|
6
7
|
import { stripFrontmatter } from '../catalog/skills-parse.js';
|
|
7
8
|
import { parseFrontmatterRecord } from '../runtime/user-store.js';
|
|
8
9
|
/**
|
|
@@ -68,11 +69,22 @@ class PanelResources {
|
|
|
68
69
|
rawText,
|
|
69
70
|
content: stripFrontmatter(rawText),
|
|
70
71
|
path: file,
|
|
71
|
-
description: typeof metadata.description === 'string' ? metadata.description : ''
|
|
72
|
+
description: typeof metadata.description === 'string' ? metadata.description : '',
|
|
73
|
+
...(pluginRootOf(suite) === undefined ? {} : { suiteRoot: suite.root, suiteData: suiteDataDir(this.catalog.dataRoot, suite.sourceId, suite.id) })
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
// One pass stamps each entry's own last modification: the panel reads every
|
|
78
|
+
// file's text anyway, and both origins get the same treatment here.
|
|
79
|
+
return await Promise.all(entries.map(async (entry) => {
|
|
80
|
+
try {
|
|
81
|
+
const stats = await stat(entry.path);
|
|
82
|
+
return { ...entry, updatedAt: new Date(stats.mtimeMs).toISOString() };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
76
88
|
}
|
|
77
89
|
async get(id) {
|
|
78
90
|
return (await this.list()).find(entry => (entry.id ?? entry.name) === id);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Resolve read-only project roles from the calling session, never from tool-supplied paths. */
|
|
2
2
|
import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
|
|
3
|
+
import { pluginRootOf } from '../catalog/plugin-variables.js';
|
|
4
|
+
import { suiteDataDir } from '../catalog/paths.js';
|
|
3
5
|
import { parseAgentRole } from '../runtime/agent-role-router.js';
|
|
4
6
|
export async function projectAgentRoles(catalog, parent) {
|
|
5
7
|
const cwd = parent?.session?.header?.cwd;
|
|
@@ -10,6 +12,8 @@ export async function projectAgentRoles(catalog, parent) {
|
|
|
10
12
|
for (const suite of snapshot.enabledSuites) {
|
|
11
13
|
if (suite.activeSurfaces.agents === false)
|
|
12
14
|
continue;
|
|
15
|
+
const suiteRoot = pluginRootOf(suite);
|
|
16
|
+
const suiteData = suiteRoot === undefined ? undefined : suiteDataDir(catalog.dataRoot, suite.sourceId, suite.id);
|
|
13
17
|
for (const resource of suite.resources?.agents ?? (await defaultMarkdownResources(suite.root, 'agents'))) {
|
|
14
18
|
const path = resource.file;
|
|
15
19
|
const rawText = await resourceText(resource).catch(error => {
|
|
@@ -27,7 +31,9 @@ export async function projectAgentRoles(catalog, parent) {
|
|
|
27
31
|
rawText,
|
|
28
32
|
title: policy.title ?? resource.name.replace(/\.agent$/, ''),
|
|
29
33
|
description: policy.description ?? `${suite.manifest.name}: ${resource.name}`,
|
|
30
|
-
disabled: policy.disabled
|
|
34
|
+
disabled: policy.disabled,
|
|
35
|
+
...(suiteRoot === undefined ? {} : { suiteRoot }),
|
|
36
|
+
...(suiteData === undefined ? {} : { suiteData })
|
|
31
37
|
});
|
|
32
38
|
}
|
|
33
39
|
catch {
|
package/lib/catalog/archive.js
CHANGED
package/lib/catalog/paths.js
CHANGED
|
@@ -93,6 +93,14 @@ export function sourceCheckoutDir(dimensionRoot, sourceId) {
|
|
|
93
93
|
export function qualifiedSuiteId(sourceId, suiteId) {
|
|
94
94
|
return `${sourceId}/${suiteId}`;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* The data directory `${PLUGIN_DATA}` resolves to for one suite: a
|
|
98
|
+
* source-qualified directory under the plugin storage root, shared by every
|
|
99
|
+
* surface of that suite so MCP servers, skills and commands see one location.
|
|
100
|
+
*/
|
|
101
|
+
export function suiteDataDir(dataRoot, sourceId, suiteId) {
|
|
102
|
+
return `${dataRoot.replace(/[\\/]$/, '')}/${qualifiedSuiteId(sourceId, suiteId)}`;
|
|
103
|
+
}
|
|
96
104
|
/**
|
|
97
105
|
* Whether `candidate` is `root` itself or a path below it, compared on whole
|
|
98
106
|
* path segments by `flavor`'s rules so a sibling whose name merely starts with
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-variable expansion for author-written suite text.
|
|
3
|
+
*
|
|
4
|
+
* A suite authored for Claude Code or its dialects writes `${CLAUDE_PLUGIN_ROOT}`
|
|
5
|
+
* (or the dialect's own spelling) into command bodies, agent personas, hook
|
|
6
|
+
* commands, LSP declarations, startup instructions and skill prose, and uses
|
|
7
|
+
* `${CLAUDE_PLUGIN_DATA}`, `${CLAUDE_PROJECT_DIR}` and `${CLAUDE_SKILL_DIR}` the
|
|
8
|
+
* same way. Its home ecosystem replaces those variables before the text reaches
|
|
9
|
+
* the model or a process; every surface here has to do the same, or the command
|
|
10
|
+
* runs with an empty path. One implementation keeps the surfaces agreeing on
|
|
11
|
+
* what each spelling means.
|
|
12
|
+
*
|
|
13
|
+
* A variable whose value the calling layer does not hold is left exactly as
|
|
14
|
+
* written: an author's `${NAME:-default}`-style intent is never silently
|
|
15
|
+
* emptied, and a surface that cannot supply a session directory does not
|
|
16
|
+
* invent one.
|
|
17
|
+
*/
|
|
18
|
+
import { PLUGIN_DATA_VARIABLES, PLUGIN_ROOT_VARIABLES, PROJECT_DIR_VARIABLES, SKILL_DIR_VARIABLE } from '../model/layouts.js';
|
|
19
|
+
/** The suite root `${PLUGIN_ROOT}` and its dialect aliases resolve to. */
|
|
20
|
+
export function pluginRootOf(suite) {
|
|
21
|
+
// Project-native directories are the repository's own files, read in place;
|
|
22
|
+
// no plugin root exists for them, so the variable stays as the author wrote it.
|
|
23
|
+
return suite.manifest.layout === 'project-native' ? undefined : suite.root;
|
|
24
|
+
}
|
|
25
|
+
/** Replace every path variable the context can resolve; other text stays exactly as written. */
|
|
26
|
+
export function expandPluginPaths(text, context) {
|
|
27
|
+
return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => {
|
|
28
|
+
if (PLUGIN_ROOT_VARIABLES.has(name))
|
|
29
|
+
return context.root ?? match;
|
|
30
|
+
if (PLUGIN_DATA_VARIABLES.has(name))
|
|
31
|
+
return context.data ?? match;
|
|
32
|
+
if (name === SKILL_DIR_VARIABLE)
|
|
33
|
+
return context.skillDir ?? match;
|
|
34
|
+
if (PROJECT_DIR_VARIABLES.has(name))
|
|
35
|
+
return context.projectDir ?? match;
|
|
36
|
+
return match;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -11,6 +11,19 @@ function object(value) {
|
|
|
11
11
|
function declarationText(value) {
|
|
12
12
|
return String(value);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Present one read document as the settings shape the shared validator reads:
|
|
16
|
+
* a standalone hook file may carry the bare event table instead of a `hooks`
|
|
17
|
+
* key. A recognized top-level event name is the marker — a document with no
|
|
18
|
+
* event name at the top stays verbatim, so an ordinary settings file missing
|
|
19
|
+
* `hooks` is still not accepted as a bare event table.
|
|
20
|
+
*/
|
|
21
|
+
function settingsDocument(settings, format) {
|
|
22
|
+
if (format !== 'claude' || settings.hooks !== undefined)
|
|
23
|
+
return settings;
|
|
24
|
+
const isEventTable = Object.keys(settings).some(key => EVENTS.has(key));
|
|
25
|
+
return isEventTable ? { ...settings, hooks: settings } : settings;
|
|
26
|
+
}
|
|
14
27
|
/** Merge settings layers additively, deduplicating identical event/matcher/command triples. */
|
|
15
28
|
export async function discoverProjectHooks(projectRoot, files, errors, format = 'claude') {
|
|
16
29
|
const documents = [];
|
|
@@ -19,7 +32,7 @@ export async function discoverProjectHooks(projectRoot, files, errors, format =
|
|
|
19
32
|
if (settings === null)
|
|
20
33
|
return undefined;
|
|
21
34
|
if (settings !== undefined)
|
|
22
|
-
documents.push({ file, settings });
|
|
35
|
+
documents.push({ file, settings: settingsDocument(settings, format) });
|
|
23
36
|
}
|
|
24
37
|
return normalizeHookDocuments(projectRoot, documents, errors, format);
|
|
25
38
|
}
|
|
@@ -99,6 +99,53 @@ export function parseSkillFrontmatter(text, expectedName) {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/** Frontmatter record for the panel's host-parity check, or the reason the YAML is unusable. */
|
|
103
|
+
function strictSkillRecord(text) {
|
|
104
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
105
|
+
if (frontmatter === undefined)
|
|
106
|
+
return 'missing YAML frontmatter';
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = parseYaml(frontmatter);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return 'invalid YAML frontmatter';
|
|
113
|
+
}
|
|
114
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
115
|
+
return 'frontmatter is not an object';
|
|
116
|
+
return raw;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rejection for a user skill document the harness's own `dsh-skill-filesystem`
|
|
120
|
+
* reader would drop from `~/.agents/skills`, or `undefined` when that reader
|
|
121
|
+
* accepts it.
|
|
122
|
+
*
|
|
123
|
+
* The two readers share the directory, so the panel publishes only documents
|
|
124
|
+
* both accept: a listed entry never becomes a skill that loads in a
|
|
125
|
+
* market-equipped session alone. The checks mirror the shipped reader rather
|
|
126
|
+
* than the lenient suite scan — strict YAML, no display-name normalization,
|
|
127
|
+
* and a `name` the reader compares only against its kebab grammar.
|
|
128
|
+
* @returns the drop reason, phrased as the reader's own diagnostic.
|
|
129
|
+
*/
|
|
130
|
+
export function skillEntryRejection(text) {
|
|
131
|
+
const record = strictSkillRecord(text);
|
|
132
|
+
if (typeof record === 'string')
|
|
133
|
+
return record;
|
|
134
|
+
const name = record['name'];
|
|
135
|
+
const description = record['description'];
|
|
136
|
+
if (typeof name !== 'string' || name === '' || typeof description !== 'string' || description.trim() === '') {
|
|
137
|
+
return 'frontmatter requires name and description';
|
|
138
|
+
}
|
|
139
|
+
if (!isSkillName(name))
|
|
140
|
+
return `invalid skill name "${name}"`;
|
|
141
|
+
const disableModel = parseBoolean(record['disable-model-invocation']);
|
|
142
|
+
if (disableModel === undefined && record['disable-model-invocation'] !== undefined)
|
|
143
|
+
return 'invalid disable-model-invocation value';
|
|
144
|
+
const userInvocable = parseBoolean(record['user-invocable']);
|
|
145
|
+
if (userInvocable === undefined && record['user-invocable'] !== undefined)
|
|
146
|
+
return 'invalid user-invocable value';
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
102
149
|
/** Lenient frontmatter recovery for prose fields with embedded `: `. */
|
|
103
150
|
function lenientFrontmatter(body) {
|
|
104
151
|
const record = {};
|
|
@@ -15,10 +15,15 @@ import { scanSource as scanSourceWithNotes } from './suite-scanner.js';
|
|
|
15
15
|
import { discoverNativeProjectSuites } from './native-project.js';
|
|
16
16
|
/** Discover suites from the selected configured or project checkouts. */
|
|
17
17
|
export async function discoverSourceList(sources, dimension, dimensionRoot) {
|
|
18
|
-
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot)).suites;
|
|
18
|
+
return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot, true)).suites;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Discover suites plus per-source scan diagnostics for one dimension.
|
|
22
|
+
* @param scanProjectLayouts - whether the project's own native Agent directories
|
|
23
|
+
* join the scan. Required on purpose: the switch has one owner (the host
|
|
24
|
+
* settings document), so a default here would be a second copy of it.
|
|
25
|
+
*/
|
|
26
|
+
export async function discoverSourceListWithNotes(sources, dimension, dimensionRoot, scanProjectLayouts) {
|
|
22
27
|
const checkoutRoot = sourcesDir(dimensionRoot);
|
|
23
28
|
const listed = new Set(sources.map(source => source.id));
|
|
24
29
|
const checkouts = sources.map(source => ({
|
|
@@ -4,7 +4,7 @@ import { relative } from 'node:path';
|
|
|
4
4
|
import { componentDocuments, componentPath, firstComponentFile, isRecord, isStringArray, isUnknownArray } from './component-files.js';
|
|
5
5
|
import { parseLspServers } from './lsp-spec.js';
|
|
6
6
|
import { normalizeHookDocuments } from './project-hooks.js';
|
|
7
|
-
import {
|
|
7
|
+
import { expandPluginPaths } from './plugin-variables.js';
|
|
8
8
|
export async function discoverSuiteLsp(root, manifest, errors) {
|
|
9
9
|
const declaration = manifest.components?.lspServers;
|
|
10
10
|
const fallback = await firstComponentFile(root, manifest.layout === 'github-copilot' || manifest.layout === 'universal' ? ['lsp.json', '.github/lsp.json', 'lsp-config/servers.json', '.lsp.json'] : ['.lsp.json', 'lsp.json']);
|
|
@@ -22,7 +22,7 @@ function quote(argument) {
|
|
|
22
22
|
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
23
23
|
}
|
|
24
24
|
function rootVariables(text, root) {
|
|
25
|
-
return text
|
|
25
|
+
return expandPluginPaths(text, { root });
|
|
26
26
|
}
|
|
27
27
|
const EVENT_ALIASES = {
|
|
28
28
|
sessionStart: 'SessionStart',
|
package/lib/contracts/market.js
CHANGED
|
@@ -28,6 +28,7 @@ export const MARKET_ROUTES = {
|
|
|
28
28
|
setSurface: `${MARKET_API_PREFIX}set-surface`,
|
|
29
29
|
mcpOverrides: `${MARKET_API_PREFIX}mcp-overrides`,
|
|
30
30
|
setMcpOverride: `${MARKET_API_PREFIX}set-mcp-override`,
|
|
31
|
+
setMcpServerEnabled: `${MARKET_API_PREFIX}set-mcp-server-enabled`,
|
|
31
32
|
mcpRetry: `${MARKET_API_PREFIX}mcp-retry`,
|
|
32
33
|
mcpReauthorize: `${MARKET_API_PREFIX}mcp-reauthorize`,
|
|
33
34
|
mcpBackend: `${MARKET_API_PREFIX}mcp-backend`,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe settings contract of the market's host settings namespace: the
|
|
3
|
+
* namespace key, the fields with their defaults, and the one reader that turns
|
|
4
|
+
* a stored section into resolved values.
|
|
5
|
+
*
|
|
6
|
+
* The defaults live here and nowhere else. The namespace schema declares them
|
|
7
|
+
* to the host, the node half reads them back through {@link resolveMarketSettings},
|
|
8
|
+
* and the browser half — which cannot import `src/runtime` — reads the same
|
|
9
|
+
* resolved values off the settings snapshot it is handed. Changing a default is
|
|
10
|
+
* therefore a one-line edit in this module.
|
|
11
|
+
*
|
|
12
|
+
* @module contracts/settings
|
|
13
|
+
*/
|
|
14
|
+
/** Settings namespace this plugin registers; the key the plugin-config tab pairs our card by. */
|
|
15
|
+
export const MARKET_SETTINGS_NAMESPACE = 'dsh-agent-plugins-market';
|
|
16
|
+
/**
|
|
17
|
+
* The default of every market setting — the single place one is written.
|
|
18
|
+
*
|
|
19
|
+
* These are user-visible behavior: a flip here is a behavior change that ships
|
|
20
|
+
* with its documentation, not a private implementation detail.
|
|
21
|
+
*/
|
|
22
|
+
export const MARKET_SETTINGS_DEFAULTS = {
|
|
23
|
+
mcpEnhanced: true,
|
|
24
|
+
scanProjectLayouts: false,
|
|
25
|
+
downloadRegion: 'auto',
|
|
26
|
+
feedbackEnabled: true,
|
|
27
|
+
autoUpdateSources: false
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Narrow an untrusted stored region to a setting; anything else reads as the
|
|
31
|
+
* declared default.
|
|
32
|
+
* @param value - the stored `downloadRegion` value.
|
|
33
|
+
* @param fallback - the value an unrecognized input reads as.
|
|
34
|
+
*/
|
|
35
|
+
export function narrowDownloadRegion(value, fallback = MARKET_SETTINGS_DEFAULTS.downloadRegion) {
|
|
36
|
+
return value === 'auto' || value === 'global' || value === 'china' ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
/** One boolean field: a stored boolean, else the field's default. */
|
|
39
|
+
function narrowBoolean(value, fallback) {
|
|
40
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a stored settings section the way the host resolves it: each field
|
|
44
|
+
* takes its stored value when it is one this field accepts, and its declared
|
|
45
|
+
* default otherwise.
|
|
46
|
+
*
|
|
47
|
+
* The host hands the resolved section back on its own, but not every caller has
|
|
48
|
+
* one — the plugin's own halves read a section they may not have received yet,
|
|
49
|
+
* and a test hands in a bare literal. Funnelling all of them through this
|
|
50
|
+
* function is what keeps a second reading of "what does missing mean" from
|
|
51
|
+
* growing somewhere else.
|
|
52
|
+
* @param section - the stored (or absent) namespace section, untrusted.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveMarketSettings(section) {
|
|
55
|
+
const stored = (typeof section === 'object' && section !== null ? section : {});
|
|
56
|
+
return {
|
|
57
|
+
mcpEnhanced: narrowBoolean(stored['mcpEnhanced'], MARKET_SETTINGS_DEFAULTS.mcpEnhanced),
|
|
58
|
+
scanProjectLayouts: narrowBoolean(stored['scanProjectLayouts'], MARKET_SETTINGS_DEFAULTS.scanProjectLayouts),
|
|
59
|
+
downloadRegion: narrowDownloadRegion(stored['downloadRegion']),
|
|
60
|
+
feedbackEnabled: narrowBoolean(stored['feedbackEnabled'], MARKET_SETTINGS_DEFAULTS.feedbackEnabled),
|
|
61
|
+
autoUpdateSources: narrowBoolean(stored['autoUpdateSources'], MARKET_SETTINGS_DEFAULTS.autoUpdateSources)
|
|
62
|
+
};
|
|
63
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { RuntimeReconciler } from './runtime/reconciler.js';
|
|
|
4
4
|
import { ReconcileScheduler } from './runtime/reconcile-scheduler.js';
|
|
5
5
|
import { MarketSettingsNamespace } from './runtime/settings-namespace.js';
|
|
6
6
|
import { deleteMcpAuthGrant } from './runtime/mcp-auth-record.js';
|
|
7
|
-
import { inspectToolRegistry } from './runtime/tool-registry-observer.js';
|
|
7
|
+
import { inspectToolRegistry, toolsServiceOf } from './runtime/tool-registry-observer.js';
|
|
8
8
|
import { migratePluginStorage } from './runtime/storage-migration.js';
|
|
9
9
|
import { mountAgentRoleTool } from './runtime/agent-role-router.js';
|
|
10
10
|
import { projectAgentRoles } from './application/project-agent-roles.js';
|
|
@@ -13,9 +13,10 @@ import { createPanelResources } from './application/panel-resources.js';
|
|
|
13
13
|
import { resolveAgentsRoot, resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
|
|
14
14
|
import { mountSuiteRoutes } from './routes.js';
|
|
15
15
|
import { SuiteSkillProvider } from './runtime/skills-provider.js';
|
|
16
|
+
import { shellSeamOf } from './runtime/dynamic-context.js';
|
|
16
17
|
import { loadLspServers } from './runtime/lsp-direct-config.js';
|
|
17
18
|
import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
|
|
18
|
-
import { bindHostLocale, loadHostLocale } from './runtime/host-locale.js';
|
|
19
|
+
import { bindHostLocale, loadHostLocale, readHostLocalePreference, setHostLocaleSource } from './runtime/host-locale.js';
|
|
19
20
|
import { createUserPanelStores } from './runtime/user-panels.js';
|
|
20
21
|
import { SourceAutoUpdater } from './runtime/source-auto-update.js';
|
|
21
22
|
import { UserPanelSkillProvider } from './runtime/user-panels.js';
|
|
@@ -34,10 +35,20 @@ export async function apply(ctx, config = {}) {
|
|
|
34
35
|
// Host runtime copy resolves from the harness `locale.preference` setting;
|
|
35
36
|
// the async settings read lands before the first session starts in practice.
|
|
36
37
|
const hostLocale = { t: bindHostLocale(undefined) };
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const refreshHostLocale = () => {
|
|
39
|
+
void loadHostLocale().then(locale => {
|
|
40
|
+
hostLocale.t = locale.t;
|
|
41
|
+
providerControl?.invalidate();
|
|
42
|
+
userPanelControl?.invalidate();
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
void refreshHostLocale();
|
|
46
|
+
// The locale preference the GUI writes is owned by the host settings service:
|
|
47
|
+
// read it there rather than re-parsing the document, and re-bind once the
|
|
48
|
+
// service lands (the locale namespace may register after this plugin).
|
|
49
|
+
ctx.inject(['settings'], settingsCtx => {
|
|
50
|
+
setHostLocaleSource(() => readHostLocalePreference(settingsCtx));
|
|
51
|
+
refreshHostLocale();
|
|
41
52
|
});
|
|
42
53
|
const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
|
|
43
54
|
// User panel stores (skills / commands / agent personas) and their runtime
|
|
@@ -110,9 +121,10 @@ export async function apply(ctx, config = {}) {
|
|
|
110
121
|
// port below is read at call time: this plugin's apply may run before the
|
|
111
122
|
// credentials, tools and settings services provision, and a snapshot taken
|
|
112
123
|
// here would be permanently undefined even after the service is live.
|
|
113
|
-
|
|
124
|
+
// Read at call time: the tools service provisions after apply() returns.
|
|
125
|
+
let toolsRegistry = toolsServiceOf(ctx);
|
|
114
126
|
const ports = {
|
|
115
|
-
mcpToolSnapshot: () =>
|
|
127
|
+
mcpToolSnapshot: () => inspectToolRegistry(toolsRegistry),
|
|
116
128
|
credentialsStore: {
|
|
117
129
|
deleteGrantRecord: async (serverName) => {
|
|
118
130
|
const store = ctx.get?.('credentials');
|
|
@@ -155,9 +167,12 @@ export async function apply(ctx, config = {}) {
|
|
|
155
167
|
await scheduler.request();
|
|
156
168
|
})
|
|
157
169
|
.catch(() => { });
|
|
170
|
+
// The shell seam resolves lazily: the service may land after this plugin, and
|
|
171
|
+
// a profile without one keeps dynamic-context placeholders literal.
|
|
172
|
+
const shellSeam = () => shellSeamOf(ctx);
|
|
158
173
|
ctx.skills.registerProvider(control => {
|
|
159
174
|
providerControl = control;
|
|
160
|
-
return new SuiteSkillProvider(catalog);
|
|
175
|
+
return new SuiteSkillProvider(catalog, { dataRoot, shell: shellSeam });
|
|
161
176
|
});
|
|
162
177
|
// User panel skills ride a second provider so a panel
|
|
163
178
|
// edit invalidates only its own catalog contribution.
|
|
@@ -173,10 +188,10 @@ export async function apply(ctx, config = {}) {
|
|
|
173
188
|
});
|
|
174
189
|
ctx.inject(['agents'], hostCtx => {
|
|
175
190
|
hostCtx.effect(() => {
|
|
176
|
-
const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key));
|
|
191
|
+
const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key), dataRoot);
|
|
177
192
|
const mcp = mountProjectMcp(hostCtx, catalog, dataRoot);
|
|
178
193
|
const hooks = mountProjectHooks(hostCtx, catalog);
|
|
179
|
-
const prompts = mountSuiteInstructions(hostCtx, catalog);
|
|
194
|
+
const prompts = mountSuiteInstructions(hostCtx, catalog, dataRoot);
|
|
180
195
|
projectCommands = mounted;
|
|
181
196
|
projectMcp = mcp;
|
|
182
197
|
projectHooks = hooks;
|
package/lib/model/layouts.js
CHANGED
|
@@ -15,6 +15,10 @@ export const MANIFEST_ALIASES = { kimi: ['kimi.plugin.json'] };
|
|
|
15
15
|
/** Equivalent plugin-owned path variables; these never resolve as credentials. */
|
|
16
16
|
export const PLUGIN_ROOT_VARIABLES = new Set(['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'ZCODE_PLUGIN_ROOT', 'QODER_PLUGIN_ROOT']);
|
|
17
17
|
export const PLUGIN_DATA_VARIABLES = new Set(['PLUGIN_DATA', 'CLAUDE_PLUGIN_DATA', 'ZCODE_PLUGIN_DATA', 'QODER_PLUGIN_DATA']);
|
|
18
|
+
/** Equivalent session project-directory variables; only a calling session supplies the directory. */
|
|
19
|
+
export const PROJECT_DIR_VARIABLES = new Set(['CLAUDE_PROJECT_DIR', 'ZCODE_PROJECT_DIR', 'QODER_PROJECT_DIR']);
|
|
20
|
+
/** Claude Code names the directory holding the skill's own file; no dialect alias exists. */
|
|
21
|
+
export const SKILL_DIR_VARIABLE = 'CLAUDE_SKILL_DIR';
|
|
18
22
|
export const PROJECT_LAYOUTS = [
|
|
19
23
|
{
|
|
20
24
|
dirName: '.claude',
|
|
@@ -23,7 +27,12 @@ export const PROJECT_LAYOUTS = [
|
|
|
23
27
|
mcpFiles: ['.mcp.json'],
|
|
24
28
|
hookFiles: ['.claude/settings.json', '.claude/settings.local.json']
|
|
25
29
|
},
|
|
26
|
-
{
|
|
30
|
+
{
|
|
31
|
+
dirName: '.agents',
|
|
32
|
+
label: 'agents',
|
|
33
|
+
subdirs: ['skills', 'agents', 'commands'],
|
|
34
|
+
hookFiles: ['.agents/hooks/hooks.json', '.agents/hooks.json']
|
|
35
|
+
},
|
|
27
36
|
{ dirName: '.codex', label: 'Codex', subdirs: ['skills'], mcpFiles: ['.codex/config.toml'], mcpFormat: 'codex' },
|
|
28
37
|
{ dirName: '.cursor', label: 'Cursor', subdirs: ['skills', 'agents', 'commands'], mcpFiles: ['.cursor/mcp.json'] },
|
|
29
38
|
{ dirName: '.kimi', label: 'Kimi', subdirs: ['skills'] },
|
package/lib/routes.js
CHANGED
|
@@ -257,6 +257,18 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
|
|
|
257
257
|
await manager.setMcpOverride(sourceId, suiteId, serverKey, sanitized ?? null);
|
|
258
258
|
return {};
|
|
259
259
|
});
|
|
260
|
+
// Enable or disable one declared MCP server. Addressed by the same
|
|
261
|
+
// source-qualified suite id the status rows carry, so the card can toggle a
|
|
262
|
+
// server without holding the source and suite ids apart.
|
|
263
|
+
post(MARKET_ROUTES.setMcpServerEnabled, async (body) => {
|
|
264
|
+
const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
|
|
265
|
+
const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
|
|
266
|
+
const enabled = body['enabled'];
|
|
267
|
+
if (typeof enabled !== 'boolean')
|
|
268
|
+
throw new Error('missing boolean enabled');
|
|
269
|
+
await manager.setMcpServerEnabled(suiteKey, serverKey, enabled);
|
|
270
|
+
return {};
|
|
271
|
+
});
|
|
260
272
|
// Manual MCP reconcile: retries failed mounts and clears residual tools
|
|
261
273
|
// without touching any catalog state.
|
|
262
274
|
post(MARKET_ROUTES.mcpRetry, async () => {
|