dsh-agent-plugins-market 0.6.2 → 0.7.0
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 +547 -540
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +181 -0
- package/lib/application/catalog.js +217 -1110
- package/lib/application/details.js +6 -6
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +246 -0
- package/lib/application/panel-resources.js +29 -12
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +1 -1
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +34 -29
- 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 +66 -55
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +9 -1
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +14 -10
- package/lib/catalog/source-catalog.js +3 -2
- package/lib/catalog/suite-components.js +5 -5
- 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 +1 -0
- package/lib/index.js +91 -208
- package/lib/model/types.js +26 -3
- package/lib/routes.js +45 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +116 -102
- package/lib/runtime/commands-mounts.js +9 -6
- package/lib/runtime/deadline.js +28 -0
- package/lib/runtime/feedback-tool.js +94 -43
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +10 -21
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +159 -68
- 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 +6 -5
- package/lib/runtime/mcp-client/config.js +0 -1
- package/lib/runtime/mcp-client/connection.js +32 -37
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- 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 +29 -10
- 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 +11 -8
- package/lib/runtime/reconcile-scheduler.js +99 -0
- package/lib/runtime/reconciler.js +4 -0
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +136 -0
- package/lib/runtime/skills-provider.js +5 -3
- package/lib/runtime/source-auto-update.js +70 -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/user-commands.js +2 -2
- package/lib/runtime/user-panels.js +31 -21
- package/lib/runtime/user-store.js +12 -13
- package/lib/types/application/catalog-context.d.ts +118 -0
- package/lib/types/application/catalog.d.ts +99 -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 +71 -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 +13 -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 +47 -15
- 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 +0 -7
- package/lib/types/catalog/source-catalog.d.ts +3 -3
- 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 +3 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -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 +54 -51
- package/lib/types/runtime/deadline.d.ts +18 -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 +1 -1
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +62 -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 +7 -4
- package/lib/types/runtime/mcp-client/config.d.ts +0 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- 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/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/source-auto-update.d.ts +33 -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/user-panels.d.ts +8 -11
- package/lib/types/runtime/user-store.d.ts +2 -4
- package/package.json +48 -21
- package/schemas/README.md +1 -1
|
@@ -79,7 +79,9 @@ export class SuiteSkillProvider {
|
|
|
79
79
|
candidateFor(entry) {
|
|
80
80
|
return {
|
|
81
81
|
name: entry.skill.name,
|
|
82
|
-
description
|
|
82
|
+
// The author's description verbatim — the harness renders it into the
|
|
83
|
+
// model's skill catalog, where a suite-name prefix is our packaging.
|
|
84
|
+
description: entry.skill.description,
|
|
83
85
|
...(entry.skill.whenToUse === undefined ? {} : { whenToUse: entry.skill.whenToUse }),
|
|
84
86
|
invocation: entry.skill.invocation,
|
|
85
87
|
source: entry.source,
|
|
@@ -100,7 +102,7 @@ export class SuiteSkillProvider {
|
|
|
100
102
|
const located = [];
|
|
101
103
|
const userSuites = await this.manager.enabledUserSuites();
|
|
102
104
|
for (const suite of userSuites) {
|
|
103
|
-
for (const skill of suite.activeSurfaces
|
|
105
|
+
for (const skill of suite.activeSurfaces.skills === false ? [] : suite.skills) {
|
|
104
106
|
try {
|
|
105
107
|
if (parseFrontmatterRecord(await readFile(skill.file, 'utf8')).disabled === true)
|
|
106
108
|
continue;
|
|
@@ -120,7 +122,7 @@ export class SuiteSkillProvider {
|
|
|
120
122
|
const snapshot = await this.manager.readProjectCatalog(cwd);
|
|
121
123
|
const located = [];
|
|
122
124
|
for (const suite of snapshot.enabledSuites) {
|
|
123
|
-
for (const skill of suite.activeSurfaces
|
|
125
|
+
for (const skill of suite.activeSurfaces.skills === false ? [] : suite.skills) {
|
|
124
126
|
try {
|
|
125
127
|
if (parseFrontmatterRecord(await readFile(skill.file, 'utf8')).disabled === true)
|
|
126
128
|
continue;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional background refresh of every configured market source.
|
|
3
|
+
*
|
|
4
|
+
* The `autoUpdateSources` setting (default off) turns this on from the
|
|
5
|
+
* plugin-config card. While enabled the updater runs one refresh pass every
|
|
6
|
+
* {@link AUTO_UPDATE_INTERVAL_MS} and then waits again; the first pass runs a
|
|
7
|
+
* full interval after the switch flips, so enabling the feature never starts
|
|
8
|
+
* network traffic as a surprise. A pass that is still running when the next
|
|
9
|
+
* tick arrives is skipped rather than queued, and the timer is unref'd so it
|
|
10
|
+
* never keeps the DSH process alive on its own.
|
|
11
|
+
* @module runtime/source-auto-update
|
|
12
|
+
*/
|
|
13
|
+
/** How often the background updater refreshes every source. */
|
|
14
|
+
export const AUTO_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
15
|
+
export class SourceAutoUpdater {
|
|
16
|
+
ctx;
|
|
17
|
+
refresh;
|
|
18
|
+
intervalMs;
|
|
19
|
+
timer;
|
|
20
|
+
inFlight = false;
|
|
21
|
+
constructor(ctx, refresh, intervalMs = AUTO_UPDATE_INTERVAL_MS) {
|
|
22
|
+
this.ctx = ctx;
|
|
23
|
+
this.refresh = refresh;
|
|
24
|
+
this.intervalMs = intervalMs;
|
|
25
|
+
}
|
|
26
|
+
/** Whether a refresh timer is currently armed. */
|
|
27
|
+
get enabled() {
|
|
28
|
+
return this.timer !== undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Arm or disarm the interval; repeated calls with the same state do nothing. */
|
|
31
|
+
setEnabled(enabled) {
|
|
32
|
+
if (enabled === this.enabled)
|
|
33
|
+
return;
|
|
34
|
+
if (!enabled) {
|
|
35
|
+
this.clearTimer();
|
|
36
|
+
this.ctx.logger?.info?.('[dsh-agent-plugins-market] background source updates off');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.timer = setInterval(() => void this.run(), this.intervalMs);
|
|
40
|
+
this.timer.unref?.();
|
|
41
|
+
this.ctx.logger?.info?.(`[dsh-agent-plugins-market] background source updates on: every ${Math.round(this.intervalMs / 60_000)} minutes`);
|
|
42
|
+
}
|
|
43
|
+
/** Disarm the interval; an in-flight pass finishes on its own. */
|
|
44
|
+
dispose() {
|
|
45
|
+
this.clearTimer();
|
|
46
|
+
}
|
|
47
|
+
clearTimer() {
|
|
48
|
+
if (this.timer === undefined)
|
|
49
|
+
return;
|
|
50
|
+
clearInterval(this.timer);
|
|
51
|
+
this.timer = undefined;
|
|
52
|
+
}
|
|
53
|
+
async run() {
|
|
54
|
+
if (this.inFlight) {
|
|
55
|
+
this.ctx.logger?.warn?.('[dsh-agent-plugins-market] background source update skipped: the previous pass is still running');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.inFlight = true;
|
|
59
|
+
try {
|
|
60
|
+
await this.refresh();
|
|
61
|
+
this.ctx.logger?.info?.('[dsh-agent-plugins-market] background source update finished');
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.ctx.logger?.warn?.(`[dsh-agent-plugins-market] background source update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
this.inFlight = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Persisted suite state: configured sources and per-suite install entries.
|
|
3
3
|
*
|
|
4
4
|
* State is a plain JSON file at `<dimensionRoot>/state.json`. The host is the
|
|
5
|
-
* only writer (through
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* only writer (through catalog actions and the HTTP routes), and it re-reads
|
|
6
|
+
* the file whenever a mutation races, keeping the on-disk copy authoritative
|
|
7
|
+
* like the market profile state this pattern follows. The record shapes live
|
|
8
|
+
* in `src/model/types.ts`; only the reading and writing live here.
|
|
8
9
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { SUITE_SURFACE_KEYS } from '
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
12
|
+
import { SUITE_SURFACE_KEYS } from '../model/types.js';
|
|
12
13
|
export const EMPTY_STATE = { version: 1, sources: [], installed: {} };
|
|
13
14
|
/** Parse persisted state; unreadable or wrong-version files yield a contained empty state. */
|
|
14
15
|
export async function loadState(statePath) {
|
|
@@ -86,10 +87,7 @@ function parseSurfaceOverrides(raw) {
|
|
|
86
87
|
}
|
|
87
88
|
return hasAny ? { surfaces: overrides } : {};
|
|
88
89
|
}
|
|
89
|
-
/** Persist state atomically through
|
|
90
|
+
/** Persist state atomically through the harness's atomic-write helper. */
|
|
90
91
|
export async function saveState(statePath, state) {
|
|
91
|
-
await
|
|
92
|
-
const temp = `${statePath}.${process.pid}.tmp`;
|
|
93
|
-
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
94
|
-
await rename(temp, statePath);
|
|
92
|
+
await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
95
93
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Consolidate plugin-owned storage before stores, routes, or providers are exposed. */
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { copyFile, lstat, mkdir, readdir, readFile,
|
|
4
|
-
import { dirname,
|
|
5
|
-
import {
|
|
3
|
+
import { copyFile, lstat, mkdir, readdir, readFile, rmdir, unlink } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
6
|
+
import { expandHome, isWithin, resolveAgentsRoot, resolveDataRoot, resolveDshHome, resolveUserRoot } from '../catalog/paths.js';
|
|
6
7
|
async function info(path) {
|
|
7
8
|
try {
|
|
8
9
|
return await lstat(path);
|
|
@@ -54,20 +55,24 @@ export async function mergeStorageTree(from, to, result) {
|
|
|
54
55
|
}
|
|
55
56
|
await unlink(from);
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return path === '' || (!path.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) && path !== '..' && !isAbsolute(path));
|
|
60
|
-
}
|
|
58
|
+
/** The three hand-authored resource kinds the market panel owns. */
|
|
59
|
+
const PANEL_KINDS = ['skills', 'commands', 'agents'];
|
|
61
60
|
/**
|
|
62
|
-
* Migrate former root overrides
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* Migrate former root overrides, data-root panels, and hand-authored
|
|
62
|
+
* resources into their canonical homes: plugin state under the user/data
|
|
63
|
+
* roots, and user content under the shared Agent layout root. Unknown files in
|
|
64
|
+
* override directories remain untouched. Conflicts remain at the old path and
|
|
65
|
+
* are returned to block activation; reruns are safe.
|
|
65
66
|
*/
|
|
66
67
|
export async function migratePluginStorage(config = {}) {
|
|
67
68
|
const userRoot = resolveUserRoot();
|
|
68
69
|
const dataRoot = resolveDataRoot();
|
|
70
|
+
const agentsRoot = resolveAgentsRoot();
|
|
69
71
|
const result = { conflicts: [] };
|
|
70
|
-
|
|
72
|
+
if (isWithin(userRoot, agentsRoot) || isWithin(agentsRoot, userRoot) || isWithin(dataRoot, agentsRoot) || isWithin(agentsRoot, dataRoot)) {
|
|
73
|
+
throw new Error('The Agent layout root overlaps canonical plugin storage');
|
|
74
|
+
}
|
|
75
|
+
for (const path of [resolveDshHome(), userRoot, dataRoot, agentsRoot, join(userRoot, 'user'), join(userRoot, '.sources')]) {
|
|
71
76
|
if ((await info(path))?.isSymbolicLink() === true)
|
|
72
77
|
throw new Error(`Plugin storage cannot use a symbolic-link directory: ${path}`);
|
|
73
78
|
}
|
|
@@ -78,7 +83,7 @@ export async function migratePluginStorage(config = {}) {
|
|
|
78
83
|
throw new Error(`Plugin storage migration cannot traverse a symbolic-link root: ${path}`);
|
|
79
84
|
}
|
|
80
85
|
if (legacyUserRoot !== userRoot) {
|
|
81
|
-
if (
|
|
86
|
+
if (isWithin(legacyUserRoot, userRoot) || isWithin(userRoot, legacyUserRoot))
|
|
82
87
|
throw new Error('Legacy userRoot overlaps canonical plugin storage');
|
|
83
88
|
const legacySources = join(legacyUserRoot, '.sources');
|
|
84
89
|
const sourceInfo = await info(legacySources);
|
|
@@ -123,13 +128,27 @@ export async function migratePluginStorage(config = {}) {
|
|
|
123
128
|
for (const legacy of new Set([legacyDataRoot, join(resolveDshHome(), 'agent-plugins-data')])) {
|
|
124
129
|
if (legacy === dataRoot)
|
|
125
130
|
continue;
|
|
126
|
-
if (
|
|
131
|
+
if (isWithin(legacy, dataRoot) || isWithin(dataRoot, legacy))
|
|
127
132
|
throw new Error('Legacy dataRoot overlaps canonical plugin data storage');
|
|
128
133
|
for (const entry of ['data', 'overrides', 'user', 'settings.json', 'lsp-servers.json', 'feedback']) {
|
|
129
134
|
await mergeStorageTree(join(legacy, entry), join(dataRoot, entry), result);
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
|
-
|
|
137
|
+
// Hand-authored resources moved out of plugin storage into the shared Agent
|
|
138
|
+
// layout root; every former `<root>/user/<kind>` layout maps onto `<kind>`.
|
|
139
|
+
// Each source is moved straight to its final location so a conflict is
|
|
140
|
+
// reported at the file the user actually has, not at a hop along the way.
|
|
141
|
+
for (const kind of PANEL_KINDS) {
|
|
142
|
+
await mergeStorageTree(join(dataRoot, 'user', kind), join(agentsRoot, kind), result);
|
|
143
|
+
await mergeStorageTree(join(userRoot, 'user', kind), join(agentsRoot, kind), result);
|
|
144
|
+
}
|
|
145
|
+
// Drop the emptied former panel directories; rmdir refuses a non-empty one,
|
|
146
|
+
// so files retained by a conflict (or an unrecognized kind) keep their home.
|
|
147
|
+
await rmdir(join(dataRoot, 'user')).catch(() => { });
|
|
148
|
+
await rmdir(join(userRoot, 'user')).catch(() => { });
|
|
149
|
+
// Hand-written service declarations moved from data files to Agent-layout files.
|
|
150
|
+
await mergeStorageTree(join(dataRoot, 'mcp-servers.json'), join(agentsRoot, 'mcp.json'), result);
|
|
151
|
+
await mergeStorageTree(join(dataRoot, 'lsp-servers.json'), join(agentsRoot, 'lsp.json'), result);
|
|
133
152
|
return result;
|
|
134
153
|
}
|
|
135
154
|
/** Retain all state fields while rebasing adopted checkouts moved with a root. */
|
|
@@ -145,15 +164,13 @@ async function relocateManagedSourceUrls(userRoot, legacyUserRoot) {
|
|
|
145
164
|
if (typeof source !== 'object' || source === null || !('local' in source) || source.local !== true || !('url' in source) || typeof source.url !== 'string')
|
|
146
165
|
continue;
|
|
147
166
|
const oldCheckout = join(legacyUserRoot, '.sources');
|
|
148
|
-
if (!
|
|
167
|
+
if (!isWithin(oldCheckout, resolve(expandHome(source.url))))
|
|
149
168
|
continue;
|
|
150
169
|
source.url = join(userRoot, '.sources', relative(oldCheckout, resolve(expandHome(source.url))));
|
|
151
170
|
changed = true;
|
|
152
171
|
}
|
|
153
172
|
if (changed) {
|
|
154
|
-
|
|
155
|
-
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { flag: 'wx' });
|
|
156
|
-
await rename(temp, path);
|
|
173
|
+
await writeFileAtomic(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
async function collectUnsupportedEntries(path, conflicts) {
|
|
@@ -51,8 +51,8 @@ function escapeText(value) {
|
|
|
51
51
|
}
|
|
52
52
|
function renderCatalog(entries, update, t) {
|
|
53
53
|
const lines = entries.map(entry => {
|
|
54
|
-
const
|
|
55
|
-
return
|
|
54
|
+
const config = [entry.provider, entry.model, entry.reasoningEffort].filter(value => value !== undefined).map(escapeText);
|
|
55
|
+
return `- ${escapeText(entry.name)}: ${escapeText(entry.description)}${config.length === 0 ? '' : ` (${config.join(' / ')})`}`;
|
|
56
56
|
});
|
|
57
57
|
return createUserMessage({
|
|
58
58
|
content: [
|
|
@@ -74,7 +74,7 @@ function renderCatalog(entries, update, t) {
|
|
|
74
74
|
}
|
|
75
75
|
function digestEntries(entries) {
|
|
76
76
|
return createHash('sha256')
|
|
77
|
-
.update(entries.map(entry => JSON.stringify([entry.name, entry.title, entry.description, entry.provider, entry.model, entry.reasoningEffort])).join('\n'))
|
|
77
|
+
.update(entries.map(entry => JSON.stringify([entry.name, entry.roleId, entry.title, entry.description, entry.provider, entry.model, entry.reasoningEffort])).join('\n'))
|
|
78
78
|
.digest('hex');
|
|
79
79
|
}
|
|
80
80
|
/** Resumed and forked seeds may carry unvalidated source fields; ignore unreadable catalogs. */
|
|
@@ -88,10 +88,11 @@ function readEntries(source) {
|
|
|
88
88
|
const entry = raw;
|
|
89
89
|
if (typeof entry.name !== 'string' || entry.name === '' || typeof entry.title !== 'string' || typeof entry.description !== 'string')
|
|
90
90
|
return undefined;
|
|
91
|
-
if (['provider', 'model', 'reasoningEffort'].some(key => entry[key] !== undefined && (typeof entry[key] !== 'string' || entry[key] === '')))
|
|
91
|
+
if (['roleId', 'provider', 'model', 'reasoningEffort'].some(key => entry[key] !== undefined && (typeof entry[key] !== 'string' || entry[key] === '')))
|
|
92
92
|
return undefined;
|
|
93
93
|
entries.push({
|
|
94
94
|
name: entry.name,
|
|
95
|
+
...(entry.roleId === undefined ? {} : { roleId: entry.roleId }),
|
|
95
96
|
title: entry.title,
|
|
96
97
|
description: entry.description,
|
|
97
98
|
...(entry.provider === undefined ? {} : { provider: entry.provider }),
|
|
@@ -62,9 +62,9 @@ export class UserCommandMountRegistry {
|
|
|
62
62
|
...(spec.hint === undefined ? {} : { input: { hint: spec.hint } }),
|
|
63
63
|
handler: invocation => {
|
|
64
64
|
const agent = invocation.agent;
|
|
65
|
-
const text =
|
|
65
|
+
const text = spec.body.replaceAll('$ARGUMENTS', invocation.rawInput.trim());
|
|
66
66
|
agent.followup({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'dsh-agent-plugins-market' } });
|
|
67
|
-
return { kind: 'success', text: this.t('
|
|
67
|
+
return { kind: 'success', text: this.t('commandAcknowledged', { command: spec.name }) };
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
this.live.set(key, disposer);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User panel storage: the persistence layer behind the skills / commands /
|
|
3
3
|
* agent-personas panels. Each surface owns one directory of Markdown files
|
|
4
|
-
* under
|
|
5
|
-
* are trivially hand-editable, and stay outside
|
|
4
|
+
* under the shared Agent layout root (`~/.agents/<kind>/`), so user-authored
|
|
5
|
+
* entries survive restarts, are trivially hand-editable, and stay outside
|
|
6
|
+
* suite checkouts.
|
|
6
7
|
*
|
|
7
8
|
* Skills entries follow the SKILL.md frontmatter grammar (`name`,
|
|
8
9
|
* `description`, optional `whenToUse`, invocation controls) plus a
|
|
@@ -16,27 +17,35 @@ import { join } from 'node:path';
|
|
|
16
17
|
import { deleteEntryFile, entryExists, listEntryFiles, readEntryFile, USER_ENTRY_NAME, userEntryDir, writeEntryFile } from './user-store.js';
|
|
17
18
|
/** The skill-source label user-panel entries carry into the skill registry. */
|
|
18
19
|
export const USER_PANEL_SKILL_SOURCE = 'user-panel';
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Panel skills outrank both the harness's reader of this directory and the
|
|
22
|
+
* suite skills an install adds.
|
|
23
|
+
*
|
|
24
|
+
* `dsh-skill-filesystem` maps `~/.agents/skills` as its `user-agents` root at
|
|
25
|
+
* rank 500, and a lower rank wins a duplicated skill name. Sitting behind it
|
|
26
|
+
* handed every entry to that reader, which knows neither this panel's
|
|
27
|
+
* `disabled` frontmatter nor its localized description, so a skill the user
|
|
28
|
+
* disabled stayed loaded. At 440 the panel keeps its own controls; project
|
|
29
|
+
* roots (100-300) and the user's `~/.dsh` skills (400) still outrank it, while
|
|
30
|
+
* a skill the user wrote by hand now beats one an installed suite ships under
|
|
31
|
+
* the same name (the suite user rank is 450).
|
|
32
|
+
*/
|
|
33
|
+
const USER_PANEL_RANK = 440;
|
|
21
34
|
/** Throwing CRUD over one panel directory, shared by the three panels. */
|
|
22
35
|
export class UserPanelStore {
|
|
23
|
-
|
|
36
|
+
agentsRoot;
|
|
24
37
|
kind;
|
|
25
38
|
extraNameCheck;
|
|
26
|
-
constructor(
|
|
39
|
+
constructor(agentsRoot, kind,
|
|
27
40
|
/** Extra name grammar for this panel (runs after USER_ENTRY_NAME). */
|
|
28
41
|
extraNameCheck = () => true) {
|
|
29
|
-
this.
|
|
42
|
+
this.agentsRoot = agentsRoot;
|
|
30
43
|
this.kind = kind;
|
|
31
44
|
this.extraNameCheck = extraNameCheck;
|
|
32
45
|
}
|
|
33
|
-
/** The panel's directory under the
|
|
46
|
+
/** The panel's directory under the Agent layout root. */
|
|
34
47
|
dirPath() {
|
|
35
|
-
return userEntryDir(this.
|
|
36
|
-
}
|
|
37
|
-
/** The plugin data root the panel lives under. */
|
|
38
|
-
root() {
|
|
39
|
-
return this.dataRoot;
|
|
48
|
+
return userEntryDir(this.agentsRoot, this.kind);
|
|
40
49
|
}
|
|
41
50
|
get dir() {
|
|
42
51
|
return this.dirPath();
|
|
@@ -102,11 +111,11 @@ export class UserPanelStore {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
/** Locate the three stores; constructed once per plugin activation. */
|
|
105
|
-
export function createUserPanelStores(
|
|
114
|
+
export function createUserPanelStores(agentsRoot) {
|
|
106
115
|
return {
|
|
107
|
-
skills: new UserPanelStore(
|
|
108
|
-
commands: new UserPanelStore(
|
|
109
|
-
agents: new UserPanelStore(
|
|
116
|
+
skills: new UserPanelStore(agentsRoot, 'skills', isUserSkillEntryName),
|
|
117
|
+
commands: new UserPanelStore(agentsRoot, 'commands'),
|
|
118
|
+
agents: new UserPanelStore(agentsRoot, 'agents', isUserSkillEntryName)
|
|
110
119
|
};
|
|
111
120
|
}
|
|
112
121
|
/**
|
|
@@ -125,11 +134,9 @@ export function isUserSkillEntryName(name) {
|
|
|
125
134
|
*/
|
|
126
135
|
export class UserPanelSkillProvider {
|
|
127
136
|
skills;
|
|
128
|
-
t;
|
|
129
137
|
name = 'user-panel';
|
|
130
|
-
constructor(skills
|
|
138
|
+
constructor(skills) {
|
|
131
139
|
this.skills = skills;
|
|
132
|
-
this.t = t;
|
|
133
140
|
}
|
|
134
141
|
async list(_options) {
|
|
135
142
|
const candidates = [];
|
|
@@ -139,7 +146,10 @@ export class UserPanelSkillProvider {
|
|
|
139
146
|
continue;
|
|
140
147
|
candidates.push({
|
|
141
148
|
name: entry.name,
|
|
142
|
-
|
|
149
|
+
// The entry's own description verbatim; a user-entry label is our
|
|
150
|
+
// packaging, and this description reaches the same model-facing
|
|
151
|
+
// catalog a suite skill does.
|
|
152
|
+
description: entry.description === '' ? entry.name : entry.description,
|
|
143
153
|
...(typeof entry.metadata['whenToUse'] === 'string' && entry.metadata['whenToUse'] !== '' ? { whenToUse: entry.metadata['whenToUse'] } : {}),
|
|
144
154
|
invocation: {
|
|
145
155
|
modelInvocable: entry.metadata['disable-model-invocation'] !== true,
|
|
@@ -6,29 +6,29 @@
|
|
|
6
6
|
* the runtime mounts.
|
|
7
7
|
* @module runtime/user-store
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
11
12
|
import { parseDocument, stringify } from 'yaml';
|
|
12
13
|
import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse.js';
|
|
13
|
-
/** Where user panel entries persist: `<
|
|
14
|
-
export function userEntryDir(
|
|
15
|
-
return join(
|
|
14
|
+
/** Where user panel entries persist: `<agentsRoot>/<kind>/`. */
|
|
15
|
+
export function userEntryDir(agentsRoot, kind) {
|
|
16
|
+
return join(agentsRoot, kind);
|
|
16
17
|
}
|
|
17
18
|
/** `[a-z][a-z0-9_-]*` — the grammar every panel entry name must satisfy. */
|
|
18
19
|
export const USER_ENTRY_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
19
|
-
/** The enablement frontmatter key shared by skills and commands. */
|
|
20
|
-
export const DISABLED_KEY = 'disabled';
|
|
21
20
|
/** Parse full YAML metadata without flattening arrays, mappings, or multiline strings. */
|
|
22
21
|
export function parseFrontmatterRecord(text) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
22
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1];
|
|
23
|
+
if (frontmatter === undefined) {
|
|
25
24
|
if (/^---(?:\r?\n|$)/.test(text))
|
|
26
25
|
throw new Error('Invalid frontmatter: missing closing delimiter');
|
|
27
26
|
return {};
|
|
28
27
|
}
|
|
29
|
-
const doc = parseDocument(
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const doc = parseDocument(frontmatter);
|
|
29
|
+
const error = doc.errors[0];
|
|
30
|
+
if (error !== undefined)
|
|
31
|
+
throw new Error(`Invalid frontmatter: ${error.message}`);
|
|
32
32
|
const value = doc.toJS({ maxAliasCount: 100 });
|
|
33
33
|
if (value === null)
|
|
34
34
|
return {};
|
|
@@ -104,8 +104,7 @@ export async function listEntryFiles(dir, strict = false) {
|
|
|
104
104
|
export async function writeEntryFile(dir, name, text) {
|
|
105
105
|
assertEntryName(name);
|
|
106
106
|
parseFrontmatterRecord(text);
|
|
107
|
-
await
|
|
108
|
-
await writeFile(join(dir, `${name}.md`), text, 'utf8');
|
|
107
|
+
await writeFileAtomic(join(dir, `${name}.md`), text, { mode: 0o644, dirMode: 0o700 });
|
|
109
108
|
}
|
|
110
109
|
/**
|
|
111
110
|
* Enforce the entry-name grammar at every filesystem choke point. The name
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { GitOptions } from '../catalog/git.js';
|
|
2
|
+
import { type DiscoveredSuite, type InstalledEntry, type Suite, type SuiteDimension, type SuiteState } from '../model/types.js';
|
|
3
|
+
import type { CatalogPortsOverride } from './ports.js';
|
|
4
|
+
import { SnapshotCache, type SnapshotHost } from './snapshot-cache.js';
|
|
5
|
+
/**
|
|
6
|
+
* Git network tuning, plumbed from the host config to every remote-touching
|
|
7
|
+
* invocation (proxy, `insteadOf` mirrors, timeout, retry). `fallbackTarball`
|
|
8
|
+
* additionally lets a failed GitHub clone fall back to a codeload tarball
|
|
9
|
+
* download through the archive pipeline.
|
|
10
|
+
*/
|
|
11
|
+
export interface CatalogGitOptions extends GitOptions {
|
|
12
|
+
/** Retry a failed GitHub clone as a codeload tarball download; default false. */
|
|
13
|
+
fallbackTarball?: boolean;
|
|
14
|
+
/** Allow plain-http archive downloads (intranet mirrors); default false. */
|
|
15
|
+
allowHttpArchives?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Dependencies and host callback used by the catalog application module. */
|
|
18
|
+
export interface CatalogOptions {
|
|
19
|
+
userRoot: string;
|
|
20
|
+
dataRoot: string;
|
|
21
|
+
/** Shared Agent layout root holding the user's hand-authored resources and service declarations. */
|
|
22
|
+
agentsRoot: string;
|
|
23
|
+
onChanged: () => void | Promise<void>;
|
|
24
|
+
/** Git/archive acquisition tuning. */
|
|
25
|
+
git?: CatalogGitOptions;
|
|
26
|
+
/** Host seams; unwired members fall back to the defaults in `ports.ts`. */
|
|
27
|
+
ports?: CatalogPortsOverride;
|
|
28
|
+
/**
|
|
29
|
+
* Freshness window for cached project-dimension snapshots. The project
|
|
30
|
+
* catalog sits on the skill-list hot path (every provider `list()` call),
|
|
31
|
+
* and its inputs — the project's own files — change only through editor
|
|
32
|
+
* saves; a short window keeps listing cheap without going stale. Defaults
|
|
33
|
+
* to 5 seconds; 0 disables caching.
|
|
34
|
+
*/
|
|
35
|
+
projectSnapshotTtlMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Freshness window for the cached user-dimension snapshot (defaults to the
|
|
38
|
+
* scan-cache TTL). It bounds how long out-of-band edits to local sources
|
|
39
|
+
* stay invisible; 0 disables snapshot caching. Tests pass a small value so
|
|
40
|
+
* staleness regressions need no real sleeps.
|
|
41
|
+
*/
|
|
42
|
+
userSnapshotTtlMs?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Key of one install entry in the persisted `installed` record. */
|
|
45
|
+
export declare function installKey(sourceId: string, suiteId: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* How long an explicit caller waits for the derived refresh before answering
|
|
48
|
+
* anyway. The refresh keeps running; only the wait ends.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DERIVED_REFRESH_WAIT_MS = 10000;
|
|
51
|
+
export declare class CatalogContext implements SnapshotHost {
|
|
52
|
+
private currentState;
|
|
53
|
+
private mutationQueue;
|
|
54
|
+
private currentRevision;
|
|
55
|
+
private currentScanGeneration;
|
|
56
|
+
private scanProjectLayoutsEnabled;
|
|
57
|
+
/** The derived refresh in flight, shared by every change that lands while it runs. */
|
|
58
|
+
private refreshPass;
|
|
59
|
+
/** Whether a change landed after the running pass started; it earns one more pass. */
|
|
60
|
+
private refreshQueued;
|
|
61
|
+
readonly statePath: string;
|
|
62
|
+
readonly userRoot: string;
|
|
63
|
+
readonly dataRoot: string;
|
|
64
|
+
readonly agentsRoot: string;
|
|
65
|
+
readonly git: CatalogGitOptions;
|
|
66
|
+
/** Discovery scans and dimension snapshots derived from {@link state}. */
|
|
67
|
+
readonly snapshots: SnapshotCache;
|
|
68
|
+
private readonly onChanged;
|
|
69
|
+
constructor(options: CatalogOptions);
|
|
70
|
+
get state(): SuiteState;
|
|
71
|
+
/** Bumped by every mutation: a snapshot built at an older revision is never published. */
|
|
72
|
+
get revision(): number;
|
|
73
|
+
/** Bumped by every content-changing mutation: a scan started earlier never repopulates its cache. */
|
|
74
|
+
get scanGeneration(): number;
|
|
75
|
+
/** Whether native project layouts (`.claude/`, `.agents/`) take part in discovery. */
|
|
76
|
+
get scanProjectLayouts(): boolean;
|
|
77
|
+
/** One install entry, or undefined when the suite is not installed. */
|
|
78
|
+
installed(sourceId: string, suiteId: string): InstalledEntry | undefined;
|
|
79
|
+
/** Load persisted user state once at plugin activation. */
|
|
80
|
+
load(): Promise<void>;
|
|
81
|
+
/** Replace the persisted state and write it out. */
|
|
82
|
+
commit(next: SuiteState): Promise<void>;
|
|
83
|
+
/** Apply the host's project-layout switch and invalidate every project snapshot. */
|
|
84
|
+
setScanProjectLayouts(enabled: boolean): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Apply the install state to freshly discovered suites: enablement, the
|
|
87
|
+
* effective surface toggles, and the lock/install timestamps the market
|
|
88
|
+
* cards report.
|
|
89
|
+
*/
|
|
90
|
+
project(discovered: readonly DiscoveredSuite[], state: SuiteState, dimension: SuiteDimension): Suite[];
|
|
91
|
+
/**
|
|
92
|
+
* Invalidate snapshots and schedule the change pipeline. `keepScanCache`
|
|
93
|
+
* marks state-only mutations (install, enable, surface toggles, MCP
|
|
94
|
+
* overrides) whose inputs leave every checkout untouched, so the next
|
|
95
|
+
* snapshot re-derives from cached discovery instead of rescanning the
|
|
96
|
+
* filesystem.
|
|
97
|
+
*
|
|
98
|
+
* The invalidation is synchronous and the refresh is coalesced in the
|
|
99
|
+
* background: resolving here means the state is durable, never that every
|
|
100
|
+
* mount has finished. Callers that must observe the refreshed surfaces await
|
|
101
|
+
* {@link refreshSettled}, whose wait is bounded so one stuck mount cannot
|
|
102
|
+
* hold a request — or the next mutation behind it — open.
|
|
103
|
+
*/
|
|
104
|
+
notifyChanged(keepScanCache?: boolean): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Resolve once the queued derived refresh finishes, `false` when the deadline
|
|
107
|
+
* elapses first. The refresh itself is never cancelled.
|
|
108
|
+
*/
|
|
109
|
+
refreshSettled(deadlineMs?: number): Promise<boolean>;
|
|
110
|
+
/** Queue one derived refresh; a pass already in flight absorbs it. */
|
|
111
|
+
private scheduleRefresh;
|
|
112
|
+
/** Drop every cached dimension snapshot; `increment` advances the revision. */
|
|
113
|
+
invalidateSnapshot(increment: boolean): void;
|
|
114
|
+
/** Drop cached discovery and advance the generation a scan is validated against. */
|
|
115
|
+
invalidateScans(): void;
|
|
116
|
+
/** Serialize one mutation behind every mutation queued before it. */
|
|
117
|
+
enqueue<T>(operation: () => Promise<T>): Promise<T>;
|
|
118
|
+
}
|