dsh-gh-pages-artifacts 0.1.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/LICENSE +21 -0
- package/README.md +227 -0
- package/assets/artifact-pages/SKILL.md +84 -0
- package/bin/setup.mjs +270 -0
- package/client/client.js +564 -0
- package/cordis.patch.yml +15 -0
- package/icon.svg +6 -0
- package/lib/approval.d.ts +26 -0
- package/lib/approval.js +59 -0
- package/lib/config.d.ts +204 -0
- package/lib/config.js +241 -0
- package/lib/content.d.ts +43 -0
- package/lib/content.js +142 -0
- package/lib/github.d.ts +78 -0
- package/lib/github.js +190 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +48 -0
- package/lib/manifest.d.ts +88 -0
- package/lib/manifest.js +172 -0
- package/lib/names.d.ts +17 -0
- package/lib/names.js +17 -0
- package/lib/prompt.d.ts +15 -0
- package/lib/prompt.js +33 -0
- package/lib/registry.d.ts +70 -0
- package/lib/registry.js +151 -0
- package/lib/render.d.ts +43 -0
- package/lib/render.js +128 -0
- package/lib/service.d.ts +292 -0
- package/lib/service.js +1216 -0
- package/lib/skill.d.ts +16 -0
- package/lib/skill.js +59 -0
- package/lib/store.d.ts +167 -0
- package/lib/store.js +379 -0
- package/lib/tools.d.ts +10 -0
- package/lib/tools.js +320 -0
- package/lib/visibility.d.ts +34 -0
- package/lib/visibility.js +81 -0
- package/lib/web-routes.d.ts +29 -0
- package/lib/web-routes.js +84 -0
- package/locale/en.json +6 -0
- package/package.json +130 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/** Agent-facing tool definitions. */
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
import { DELETE_TOOL, LIST_TOOL, PUBLISH_TOOL, READ_TOOL, REPOSITORY_TOOL, STATUS_TOOL } from './names.js';
|
|
4
|
+
export { ALL_TOOLS, DELETE_TOOL, LIST_TOOL, MUTATING_TOOLS, PUBLISH_TOOL, READ_TOOL, REPOSITORY_TOOL, STATUS_TOOL } from './names.js';
|
|
5
|
+
const KIND = { type: 'string', enum: ['html', 'markdown'] };
|
|
6
|
+
const STRATEGY = { type: 'string', enum: ['shared', 'per-artifact'] };
|
|
7
|
+
/** Markdown link text with brackets removed so the link stays well formed. */
|
|
8
|
+
function linkText(text) {
|
|
9
|
+
return text.replace(/[[\]]/g, '');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the artifact tools around one runtime.
|
|
13
|
+
* @param runtime - shared operations.
|
|
14
|
+
* @returns tool definitions ready for `ctx.tools.register`.
|
|
15
|
+
*/
|
|
16
|
+
export function createTools(runtime) {
|
|
17
|
+
const publish = defineTool({
|
|
18
|
+
name: PUBLISH_TOOL,
|
|
19
|
+
description: 'Publish an HTML page or a Markdown document as a public artifact on GitHub Pages and get a shareable URL, '
|
|
20
|
+
+ 'or update an existing artifact in place (pass its id; the URL stays the same). '
|
|
21
|
+
+ 'Depending on the repository strategy (see artifact_repository), a new artifact goes into the shared repository or into a new repository of its own. '
|
|
22
|
+
+ 'Prefer writing the page to a workspace file first and passing path; use content only for short inline sources. '
|
|
23
|
+
+ 'HTML is served as-is (one self-contained file works best; extra files go in assets and are referenced by relative URLs). '
|
|
24
|
+
+ 'Markdown is rendered to a styled page. Anyone with the link can see the result and the repository may be public, '
|
|
25
|
+
+ 'so never publish secrets or private data the user did not ask to share. The user may be asked to approve. '
|
|
26
|
+
+ 'Changes go live in one to three minutes. Provide description first, then title, then the source.',
|
|
27
|
+
parameters: {
|
|
28
|
+
description: { type: 'string', required: true, description: 'One line saying what this artifact is, shown to the user and stored with it.' },
|
|
29
|
+
title: { type: 'string', description: 'Page title. Required for a new artifact; on update it renames without changing the URL.' },
|
|
30
|
+
id: { type: 'string', description: 'Id of an existing artifact to update in place. Omit to create a new artifact.' },
|
|
31
|
+
path: { type: 'string', description: 'Workspace file to publish: .html/.htm for a page, .md/.markdown for a document. Relative paths use the Session working directory.' },
|
|
32
|
+
kind: { ...KIND, description: 'html or markdown; inferred from the file extension or the content when omitted.' },
|
|
33
|
+
slug: { type: 'string', description: 'URL id for a new artifact (lowercase letters, digits, hyphens). Default: derived from the title plus a random suffix.' },
|
|
34
|
+
assets: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
description: 'Extra workspace files (images, CSS, JS, data) published next to the page. On update, listed assets are added or replaced and the others are kept.',
|
|
37
|
+
items: {
|
|
38
|
+
type: 'object', additionalProperties: false,
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: 'string', required: true, description: 'Workspace file to upload.' },
|
|
41
|
+
name: { type: 'string', description: 'Published name relative to the page, e.g. img/chart.png. Defaults to the file name.' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
removeAssets: { type: 'array', items: { type: 'string' }, description: 'On update: asset names to delete.' },
|
|
46
|
+
baseRev: { type: 'integer', description: 'On update: the rev you last saw. The update is refused if the artifact changed since.' },
|
|
47
|
+
content: { type: 'string', description: 'Inline source when there is no workspace file: Markdown, or a whole HTML document starting with <!doctype html>. Prefer path for anything long.' },
|
|
48
|
+
},
|
|
49
|
+
output: {
|
|
50
|
+
schema: {
|
|
51
|
+
type: 'object', additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
id: { type: 'string', required: true },
|
|
54
|
+
title: { type: 'string', required: true },
|
|
55
|
+
kind: { ...KIND, required: true },
|
|
56
|
+
url: { type: 'string', required: true },
|
|
57
|
+
repository: { type: 'string', required: true },
|
|
58
|
+
newRepository: { type: 'boolean', required: true },
|
|
59
|
+
rev: { type: 'integer', required: true },
|
|
60
|
+
created: { type: 'boolean', required: true },
|
|
61
|
+
commit: { type: 'string', required: true },
|
|
62
|
+
files: { type: 'array', items: { type: 'string' }, required: true },
|
|
63
|
+
warnings: { type: 'array', items: { type: 'string' }, required: true },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
render: (_args, value) => [{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: [
|
|
69
|
+
`${value.created ? 'Published' : `Updated (rev ${value.rev})`} "${value.title}" [id: ${value.id}]`,
|
|
70
|
+
`URL: ${value.url}`,
|
|
71
|
+
`Repository: ${value.repository}${value.newRepository ? ' (created for this artifact)' : ''}`,
|
|
72
|
+
`Files: ${value.files.join(', ')}`,
|
|
73
|
+
...value.warnings.map(warning => `Warning: ${warning}`),
|
|
74
|
+
'GitHub Pages usually serves the change within one to three minutes (browsers may cache the old version for up to 10 minutes).',
|
|
75
|
+
`Give the user the link as Markdown: [${linkText(value.title)}](${value.url})`,
|
|
76
|
+
].join('\n'),
|
|
77
|
+
}],
|
|
78
|
+
presentationMeta: (_args, value) => ({ url: value.url, id: value.id, kind: value.kind, rev: value.rev, repository: value.repository }),
|
|
79
|
+
},
|
|
80
|
+
async execute(args, exec) {
|
|
81
|
+
return await runtime.publish(args, exec);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const list = defineTool({
|
|
85
|
+
name: LIST_TOOL,
|
|
86
|
+
description: 'List every artifact this plugin has published (newest first), across all repositories, with clickable links. '
|
|
87
|
+
+ 'Show them to the user as a Markdown list of links. The result also names a local index.html with the same links; '
|
|
88
|
+
+ 'pass it to the present tool when the user wants a page of all artifacts.',
|
|
89
|
+
parameters: {
|
|
90
|
+
query: { type: 'string', description: 'Only artifacts whose id, title, description, or repository contains this text.' },
|
|
91
|
+
includeDeleted: { type: 'boolean', description: 'Also list deleted artifacts; default false.' },
|
|
92
|
+
offset: { type: 'integer', description: 'Zero-based offset; default 0.' },
|
|
93
|
+
limit: { type: 'integer', description: 'Page size from 1 to 200; default 50.' },
|
|
94
|
+
},
|
|
95
|
+
output: {
|
|
96
|
+
schema: {
|
|
97
|
+
type: 'object', additionalProperties: false,
|
|
98
|
+
properties: {
|
|
99
|
+
total: { type: 'integer', required: true },
|
|
100
|
+
artifacts: {
|
|
101
|
+
type: 'array', required: true,
|
|
102
|
+
items: {
|
|
103
|
+
type: 'object', additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
id: { type: 'string', required: true },
|
|
106
|
+
title: { type: 'string', required: true },
|
|
107
|
+
kind: { ...KIND, required: true },
|
|
108
|
+
url: { type: 'string', required: true },
|
|
109
|
+
repository: { type: 'string', required: true },
|
|
110
|
+
rev: { type: 'integer', required: true },
|
|
111
|
+
updatedAt: { type: 'string', required: true },
|
|
112
|
+
status: { type: 'string', enum: ['published', 'deleted'], required: true },
|
|
113
|
+
description: { type: 'string' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
indexFile: { type: 'string', required: true },
|
|
118
|
+
notes: { type: 'array', items: { type: 'string' }, required: true },
|
|
119
|
+
nextOffset: { type: 'integer' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
render: (_args, value) => [{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: [
|
|
125
|
+
value.total === 0 ? 'No artifacts published yet.' : `${value.total} artifact${value.total === 1 ? '' : 's'}:`,
|
|
126
|
+
...value.artifacts.map(item => `- [${linkText(item.title)}](${item.url}) · id ${item.id} · ${item.kind} · rev ${item.rev} · ${item.repository} · updated ${item.updatedAt.slice(0, 16).replace('T', ' ')}`
|
|
127
|
+
+ (item.status === 'deleted' ? ' · deleted' : '') + (item.description === undefined ? '' : ` · ${item.description}`)),
|
|
128
|
+
...value.nextOffset === undefined ? [] : [`More: call again with offset ${value.nextOffset}.`],
|
|
129
|
+
`Index page with all links: ${value.indexFile}`,
|
|
130
|
+
...value.notes.map(note => `Note: ${note}`),
|
|
131
|
+
].join('\n'),
|
|
132
|
+
}],
|
|
133
|
+
},
|
|
134
|
+
isConcurrencySafe: () => true,
|
|
135
|
+
async execute(args, exec) {
|
|
136
|
+
return await runtime.list(args, exec.signal);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const read = defineTool({
|
|
140
|
+
name: READ_TOOL,
|
|
141
|
+
description: 'Read the source of a published artifact (the Markdown source of a document, or the HTML of a page), '
|
|
142
|
+
+ 'or another text file of it, in windows of up to 20000 characters. Use it before updating an artifact you did not just write.',
|
|
143
|
+
parameters: {
|
|
144
|
+
id: { type: 'string', required: true, description: 'Artifact id.' },
|
|
145
|
+
file: { type: 'string', description: 'File to read, relative to the artifact; defaults to its source.' },
|
|
146
|
+
offset: { type: 'integer', description: 'Zero-based character offset; default 0.' },
|
|
147
|
+
limit: { type: 'integer', description: 'Characters to return, from 1 to 20000; default 8000.' },
|
|
148
|
+
},
|
|
149
|
+
output: {
|
|
150
|
+
schema: {
|
|
151
|
+
type: 'object', additionalProperties: false,
|
|
152
|
+
properties: {
|
|
153
|
+
id: { type: 'string', required: true },
|
|
154
|
+
title: { type: 'string', required: true },
|
|
155
|
+
kind: { ...KIND, required: true },
|
|
156
|
+
url: { type: 'string', required: true },
|
|
157
|
+
repository: { type: 'string', required: true },
|
|
158
|
+
rev: { type: 'integer', required: true },
|
|
159
|
+
file: { type: 'string', required: true },
|
|
160
|
+
files: { type: 'array', items: { type: 'string' }, required: true },
|
|
161
|
+
totalChars: { type: 'integer', required: true },
|
|
162
|
+
offset: { type: 'integer', required: true },
|
|
163
|
+
content: { type: 'string', required: true },
|
|
164
|
+
nextOffset: { type: 'integer' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
render: (_args, value) => [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: [
|
|
170
|
+
`${value.title} [id: ${value.id}, ${value.kind}, rev ${value.rev}] ${value.url} (${value.repository})`,
|
|
171
|
+
`File ${value.file}: characters ${value.offset}-${value.offset + value.content.length} of ${value.totalChars}. Files: ${value.files.join(', ')}`,
|
|
172
|
+
...value.nextOffset === undefined ? [] : [`More: call again with offset ${value.nextOffset}.`],
|
|
173
|
+
'----- BEGIN CONTENT -----',
|
|
174
|
+
value.content,
|
|
175
|
+
'----- END CONTENT -----',
|
|
176
|
+
].join('\n'),
|
|
177
|
+
}],
|
|
178
|
+
},
|
|
179
|
+
isConcurrencySafe: () => true,
|
|
180
|
+
async execute(args, exec) {
|
|
181
|
+
return await runtime.read(args, exec.signal);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const remove = defineTool({
|
|
185
|
+
name: DELETE_TOOL,
|
|
186
|
+
description: 'Delete a published artifact so its URL stops working. The id is never reused. '
|
|
187
|
+
+ 'Copies that others saved and the repository history remain; a repository created for the artifact is kept. The user may be asked to approve.',
|
|
188
|
+
parameters: {
|
|
189
|
+
id: { type: 'string', required: true, description: 'Artifact id to delete.' },
|
|
190
|
+
},
|
|
191
|
+
output: {
|
|
192
|
+
schema: {
|
|
193
|
+
type: 'object', additionalProperties: false,
|
|
194
|
+
properties: {
|
|
195
|
+
id: { type: 'string', required: true },
|
|
196
|
+
url: { type: 'string', required: true },
|
|
197
|
+
repository: { type: 'string', required: true },
|
|
198
|
+
deleted: { type: 'boolean', required: true },
|
|
199
|
+
commit: { type: 'string', required: true },
|
|
200
|
+
notes: { type: 'array', items: { type: 'string' }, required: true },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
render: (_args, value) => [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: [
|
|
206
|
+
`Deleted artifact ${value.id} from ${value.repository}; ${value.url} stops working once GitHub Pages redeploys (one to three minutes).`,
|
|
207
|
+
...value.notes.map(note => `Note: ${note}`),
|
|
208
|
+
].join('\n'),
|
|
209
|
+
}],
|
|
210
|
+
presentationMeta: (_args, value) => ({ url: value.url, id: value.id, deleted: true, repository: value.repository }),
|
|
211
|
+
},
|
|
212
|
+
async execute(args, exec) {
|
|
213
|
+
return await runtime.remove(args, exec);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const status = defineTool({
|
|
217
|
+
name: STATUS_TOOL,
|
|
218
|
+
description: 'Check the GitHub Pages artifact setup (token, repository, Pages settings) and the latest deployment. '
|
|
219
|
+
+ 'Pass id to check that artifact\'s repository and whether it is live; pass wait: true to wait up to five minutes until the latest change is deployed.',
|
|
220
|
+
parameters: {
|
|
221
|
+
id: { type: 'string', description: 'Artifact id whose repository and URL to check.' },
|
|
222
|
+
wait: { type: 'boolean', description: 'Wait until the latest change is deployed (and the artifact URL responds); default false.' },
|
|
223
|
+
},
|
|
224
|
+
output: {
|
|
225
|
+
schema: {
|
|
226
|
+
type: 'object', additionalProperties: false,
|
|
227
|
+
properties: {
|
|
228
|
+
ready: { type: 'boolean', required: true },
|
|
229
|
+
strategy: { ...STRATEGY, required: true },
|
|
230
|
+
repository: { type: 'string', required: true },
|
|
231
|
+
branch: { type: 'string', required: true },
|
|
232
|
+
siteUrl: { type: 'string' },
|
|
233
|
+
token: {
|
|
234
|
+
type: 'object', additionalProperties: false, required: true,
|
|
235
|
+
properties: { configured: { type: 'boolean', required: true }, source: { type: 'string' } },
|
|
236
|
+
},
|
|
237
|
+
pages: { type: 'string', required: true },
|
|
238
|
+
headCommit: { type: 'string' },
|
|
239
|
+
deployed: { type: 'boolean' },
|
|
240
|
+
latestBuild: {
|
|
241
|
+
type: 'object', additionalProperties: false,
|
|
242
|
+
properties: { status: { type: 'string', required: true }, commit: { type: 'string' }, error: { type: 'string' } },
|
|
243
|
+
},
|
|
244
|
+
artifact: {
|
|
245
|
+
type: 'object', additionalProperties: false,
|
|
246
|
+
properties: {
|
|
247
|
+
id: { type: 'string', required: true },
|
|
248
|
+
url: { type: 'string', required: true },
|
|
249
|
+
live: { type: 'boolean', required: true },
|
|
250
|
+
httpStatus: { type: 'integer' },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
registryFile: { type: 'string', required: true },
|
|
254
|
+
problems: { type: 'array', items: { type: 'string' }, required: true },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
render: (_args, value) => [{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: [
|
|
260
|
+
`${value.ready ? 'Ready' : 'Not ready'}: ${value.repository} (branch ${value.branch})${value.siteUrl === undefined ? '' : `, site ${value.siteUrl}`}; strategy ${value.strategy}`,
|
|
261
|
+
`Token: ${value.token.configured ? `configured (${value.token.source ?? 'unknown source'})` : 'missing'}; Pages: ${value.pages}`,
|
|
262
|
+
...value.latestBuild === undefined ? [] : [`Latest Pages build: ${value.latestBuild.status}${value.latestBuild.commit === undefined ? '' : ` at ${value.latestBuild.commit.slice(0, 7)}`}`],
|
|
263
|
+
...value.deployed === undefined ? [] : [value.deployed ? 'Latest change: deployed' : 'Latest change: not deployed yet'],
|
|
264
|
+
...value.artifact === undefined ? [] : [`Artifact ${value.artifact.id}: ${value.artifact.live ? 'live' : 'not live yet'} at ${value.artifact.url}`],
|
|
265
|
+
...value.problems.map(problem => `Problem: ${problem}`),
|
|
266
|
+
].join('\n'),
|
|
267
|
+
}],
|
|
268
|
+
},
|
|
269
|
+
isConcurrencySafe: () => true,
|
|
270
|
+
async execute(args, exec) {
|
|
271
|
+
return await runtime.status(args, exec.signal);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
const repository = defineTool({
|
|
275
|
+
name: REPOSITORY_TOOL,
|
|
276
|
+
description: 'Show or change where new artifacts are published. '
|
|
277
|
+
+ 'Strategies: "shared" reuses one linked repository (each artifact gets a folder in it); "per-artifact" creates a new repository and Pages site for every new artifact. '
|
|
278
|
+
+ 'Actions: show (current settings); link (reuse an existing repository the user names, as owner/name or a remote URL such as https://github.com/owner/name.git; also selects the shared strategy); '
|
|
279
|
+
+ 'unlink (go back to the configured repository); set_strategy (shared or per-artifact). '
|
|
280
|
+
+ 'Only change settings when the user asks. Existing artifacts stay where they are.',
|
|
281
|
+
parameters: {
|
|
282
|
+
action: { type: 'string', required: true, enum: ['show', 'link', 'unlink', 'set_strategy'], description: 'What to do.' },
|
|
283
|
+
repository: { type: 'string', description: 'For link: the repository as owner/name or a remote URL.' },
|
|
284
|
+
strategy: { ...STRATEGY, description: 'For set_strategy: shared or per-artifact.' },
|
|
285
|
+
},
|
|
286
|
+
output: {
|
|
287
|
+
schema: {
|
|
288
|
+
type: 'object', additionalProperties: false,
|
|
289
|
+
properties: {
|
|
290
|
+
strategy: { ...STRATEGY, required: true },
|
|
291
|
+
strategySource: { type: 'string', enum: ['config', 'user'], required: true },
|
|
292
|
+
repository: { type: 'string', required: true },
|
|
293
|
+
repositorySource: { type: 'string', enum: ['linked', 'config', 'token user'], required: true },
|
|
294
|
+
branch: { type: 'string', required: true },
|
|
295
|
+
newRepositories: { type: 'string', required: true },
|
|
296
|
+
siteUrl: { type: 'string' },
|
|
297
|
+
pages: { type: 'string' },
|
|
298
|
+
registryFile: { type: 'string', required: true },
|
|
299
|
+
indexFile: { type: 'string', required: true },
|
|
300
|
+
warnings: { type: 'array', items: { type: 'string' }, required: true },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
render: (args, value) => [{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: [
|
|
306
|
+
...args.action === 'show' ? [] : [`Done: ${args.action}.`],
|
|
307
|
+
value.strategy === 'shared'
|
|
308
|
+
? `Strategy: shared (${value.strategySource === 'user' ? 'chosen by the user' : 'from the configuration'}). New artifacts go into ${value.repository} (${value.repositorySource}), branch ${value.branch}${value.siteUrl === undefined ? '' : `, site ${value.siteUrl}`}${value.pages === undefined ? '' : `, Pages ${value.pages}`}.`
|
|
309
|
+
: `Strategy: per-artifact (${value.strategySource === 'user' ? 'chosen by the user' : 'from the configuration'}). Each new artifact gets a new repository: ${value.newRepositories}. The shared repository is ${value.repository} (${value.repositorySource}).`,
|
|
310
|
+
`Published artifacts are tracked in ${value.registryFile}; index page: ${value.indexFile}`,
|
|
311
|
+
...value.warnings.map(warning => `Warning: ${warning}`),
|
|
312
|
+
].join('\n'),
|
|
313
|
+
}],
|
|
314
|
+
},
|
|
315
|
+
async execute(args, exec) {
|
|
316
|
+
return await runtime.repository(args, exec);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
return [publish, list, read, remove, status, repository];
|
|
320
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide artifact tools from configured presets and from delegated subagents. The tools register
|
|
3
|
+
* globally, so every Agent sees them unless its own scope restricts them. The restriction is
|
|
4
|
+
* reconciled for Agents that exist at load time, when an Agent is created, and when a preset is
|
|
5
|
+
* selected, because the Web UI picks the mode after the Session's Agent already exists. The tools
|
|
6
|
+
* also check the same policy when they run, so a missed restriction cannot open them.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { SubagentAccess } from './config.js';
|
|
10
|
+
/** Inputs for {@link installVisibility}. */
|
|
11
|
+
export interface VisibilityOptions {
|
|
12
|
+
/** Every tool this plugin registers. */
|
|
13
|
+
readonly allTools: readonly string[];
|
|
14
|
+
/** Tools that change published content. */
|
|
15
|
+
readonly mutatingTools: readonly string[];
|
|
16
|
+
/** Presets that see no artifact tools. */
|
|
17
|
+
readonly hideFromPresets: readonly string[];
|
|
18
|
+
/** Subagent policy. */
|
|
19
|
+
readonly subagentAccess: SubagentAccess;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compute which tools one Agent must not see.
|
|
23
|
+
* @param options - visibility settings.
|
|
24
|
+
* @param preset - the Agent's preset id, when known.
|
|
25
|
+
* @param delegated - whether the Agent is a delegated subagent.
|
|
26
|
+
* @returns tool names to deny, sorted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hiddenTools(options: VisibilityOptions, preset: string | undefined, delegated: boolean): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Keep per-Agent tool restrictions in sync with presets and delegation.
|
|
31
|
+
* @param ctx - plugin context (host root).
|
|
32
|
+
* @param options - visibility settings.
|
|
33
|
+
*/
|
|
34
|
+
export declare function installVisibility(ctx: Context, options: VisibilityOptions): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute which tools one Agent must not see.
|
|
3
|
+
* @param options - visibility settings.
|
|
4
|
+
* @param preset - the Agent's preset id, when known.
|
|
5
|
+
* @param delegated - whether the Agent is a delegated subagent.
|
|
6
|
+
* @returns tool names to deny, sorted.
|
|
7
|
+
*/
|
|
8
|
+
export function hiddenTools(options, preset, delegated) {
|
|
9
|
+
if (preset !== undefined && options.hideFromPresets.includes(preset))
|
|
10
|
+
return [...options.allTools].sort();
|
|
11
|
+
if (delegated) {
|
|
12
|
+
if (options.subagentAccess === 'none')
|
|
13
|
+
return [...options.allTools].sort();
|
|
14
|
+
if (options.subagentAccess === 'read-only')
|
|
15
|
+
return [...options.mutatingTools].sort();
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Keep per-Agent tool restrictions in sync with presets and delegation.
|
|
21
|
+
* @param ctx - plugin context (host root).
|
|
22
|
+
* @param options - visibility settings.
|
|
23
|
+
*/
|
|
24
|
+
export function installVisibility(ctx, options) {
|
|
25
|
+
if (options.hideFromPresets.length === 0 && options.subagentAccess === 'full')
|
|
26
|
+
return;
|
|
27
|
+
const applied = new Map();
|
|
28
|
+
ctx.effect(() => () => {
|
|
29
|
+
for (const entry of applied.values())
|
|
30
|
+
safeDispose(entry.dispose);
|
|
31
|
+
applied.clear();
|
|
32
|
+
}, 'gh-pages-artifacts.visibility');
|
|
33
|
+
const reconcile = (agent, selectedPreset) => {
|
|
34
|
+
try {
|
|
35
|
+
const preset = selectedPreset ?? ctx.get('agentPresets')?.composedPreset(agent.ctx);
|
|
36
|
+
const delegated = (agent.session.header.delegationDepth ?? 0) > 0 || agent.session.header.origin === 'subagent';
|
|
37
|
+
const deny = hiddenTools(options, preset, delegated);
|
|
38
|
+
const key = deny.join(',');
|
|
39
|
+
const current = applied.get(agent);
|
|
40
|
+
if (current?.key === key)
|
|
41
|
+
return;
|
|
42
|
+
if (current !== undefined) {
|
|
43
|
+
safeDispose(current.dispose);
|
|
44
|
+
applied.delete(agent);
|
|
45
|
+
}
|
|
46
|
+
if (deny.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const dispose = agent.ctx.effect(() => agent.ctx.tools.restrict({ deny }), 'gh-pages-artifacts.restrict');
|
|
49
|
+
applied.set(agent, { key, dispose });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
ctx.logger.warn(`gh-pages-artifacts: could not update artifact tool visibility: ${String(error)}`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
56
|
+
reconcile(agent);
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
ctx.on('agent-preset/selected', (sessionId, agentPreset) => {
|
|
60
|
+
const agent = ctx.get('agents')?.get(sessionId);
|
|
61
|
+
if (agent !== undefined)
|
|
62
|
+
reconcile(agent, agentPreset);
|
|
63
|
+
});
|
|
64
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
65
|
+
const entry = applied.get(agent);
|
|
66
|
+
applied.delete(agent);
|
|
67
|
+
if (entry !== undefined)
|
|
68
|
+
safeDispose(entry.dispose);
|
|
69
|
+
});
|
|
70
|
+
// Agents that already run when the plugin loads or reloads (for example after a config edit).
|
|
71
|
+
for (const agent of ctx.get('agents')?.list() ?? [])
|
|
72
|
+
reconcile(agent);
|
|
73
|
+
}
|
|
74
|
+
function safeDispose(dispose) {
|
|
75
|
+
try {
|
|
76
|
+
dispose();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// The Agent scope may already be gone; its restrictions went with it.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web routes the browser half reads: the artifact list for the sidebar's Artifacts panel. They are
|
|
3
|
+
* exact Fetch routes on dsh's shared /api channel, so the Connection's cookie auth and Host/Origin
|
|
4
|
+
* fence apply before these handlers run. Compositions without a web Connection (CLI, headless)
|
|
5
|
+
* simply do not get them.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import { type RegistryData } from './registry.js';
|
|
9
|
+
import type { ArtifactsRuntime, ListedArtifact } from './service.js';
|
|
10
|
+
/** Route keys on the Host; the browser uses the document-relative form without the leading slash. */
|
|
11
|
+
export declare const ARTIFACTS_LIST_PATH = "/api/gh-pages-artifacts/list";
|
|
12
|
+
export declare const ARTIFACTS_REFRESH_PATH = "/api/gh-pages-artifacts/refresh";
|
|
13
|
+
export declare const ARTIFACTS_DELETE_PATH = "/api/gh-pages-artifacts/delete";
|
|
14
|
+
/** Body of both routes. */
|
|
15
|
+
export interface ArtifactsPayload {
|
|
16
|
+
readonly artifacts: ListedArtifact[];
|
|
17
|
+
readonly notes: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @param data - registry contents.
|
|
21
|
+
* @returns published artifacts, newest first.
|
|
22
|
+
*/
|
|
23
|
+
export declare function publishedArtifacts(data: RegistryData): ListedArtifact[];
|
|
24
|
+
/**
|
|
25
|
+
* Serve the artifact list while a web Connection exists.
|
|
26
|
+
* @param ctx - plugin context.
|
|
27
|
+
* @param runtime - artifact operations and registry.
|
|
28
|
+
*/
|
|
29
|
+
export declare function installWebRoutes(ctx: Context, runtime: ArtifactsRuntime): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { repositoryName } from './registry.js';
|
|
2
|
+
/** Route keys on the Host; the browser uses the document-relative form without the leading slash. */
|
|
3
|
+
export const ARTIFACTS_LIST_PATH = '/api/gh-pages-artifacts/list';
|
|
4
|
+
export const ARTIFACTS_REFRESH_PATH = '/api/gh-pages-artifacts/refresh';
|
|
5
|
+
export const ARTIFACTS_DELETE_PATH = '/api/gh-pages-artifacts/delete';
|
|
6
|
+
/** An error the route answers with a 4xx status instead of a 500. */
|
|
7
|
+
class RequestError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function json(body, status = 200) {
|
|
15
|
+
return Response.json(body, { status, headers: { 'cache-control': 'no-store' } });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @param data - registry contents.
|
|
19
|
+
* @returns published artifacts, newest first.
|
|
20
|
+
*/
|
|
21
|
+
export function publishedArtifacts(data) {
|
|
22
|
+
return Object.values(data.artifacts)
|
|
23
|
+
.filter(entry => entry.status === 'published')
|
|
24
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id))
|
|
25
|
+
.map(entry => ({
|
|
26
|
+
id: entry.id, title: entry.title, kind: entry.kind, url: entry.url,
|
|
27
|
+
repository: repositoryName(entry.location), rev: entry.rev, updatedAt: entry.updatedAt, status: entry.status,
|
|
28
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
/** The web server answers a thrown handler with a bare 400, so every failure becomes JSON here. */
|
|
32
|
+
function guarded(handler) {
|
|
33
|
+
return async (request) => {
|
|
34
|
+
try {
|
|
35
|
+
return await handler(request);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, error instanceof RequestError ? error.status : 500);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serve the artifact list while a web Connection exists.
|
|
44
|
+
* @param ctx - plugin context.
|
|
45
|
+
* @param runtime - artifact operations and registry.
|
|
46
|
+
*/
|
|
47
|
+
export function installWebRoutes(ctx, runtime) {
|
|
48
|
+
ctx.inject(['connection'], (webCtx) => {
|
|
49
|
+
// Read structurally so the plugin needs no dependency on the connection package; the
|
|
50
|
+
// registration is owned by this child context and goes away with it.
|
|
51
|
+
const connection = Reflect.get(webCtx, 'connection');
|
|
52
|
+
try {
|
|
53
|
+
connection.fetch.register({
|
|
54
|
+
path: ARTIFACTS_LIST_PATH, methods: ['GET'], requestBody: 'buffered',
|
|
55
|
+
// The local registry only: fast and needs no GitHub call.
|
|
56
|
+
fetch: guarded(async () => json({ artifacts: publishedArtifacts(await runtime.registry.read()), notes: [] })),
|
|
57
|
+
});
|
|
58
|
+
connection.fetch.register({
|
|
59
|
+
path: ARTIFACTS_REFRESH_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
60
|
+
// Reconcile with GitHub first (adds artifacts published from elsewhere).
|
|
61
|
+
fetch: guarded(async (request) => {
|
|
62
|
+
const result = await runtime.list({ limit: 200 }, request.signal);
|
|
63
|
+
return json({ artifacts: result.artifacts, notes: result.notes });
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
connection.fetch.register({
|
|
67
|
+
path: ARTIFACTS_DELETE_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
68
|
+
// A delete the user confirmed in the Artifacts panel. Connection only admits same-origin
|
|
69
|
+
// requests from a signed-in browser, so other sites cannot trigger it.
|
|
70
|
+
fetch: guarded(async (request) => {
|
|
71
|
+
const body = await request.json().catch(() => undefined);
|
|
72
|
+
if (typeof body?.id !== 'string' || body.id === '')
|
|
73
|
+
throw new RequestError('Missing artifact id', 400);
|
|
74
|
+
const result = await runtime.removeByUser(body.id, request.signal);
|
|
75
|
+
return json({ deleted: result, artifacts: publishedArtifacts(await runtime.registry.read()) });
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// A second running row of this plugin would register the same paths.
|
|
81
|
+
webCtx.logger.warn(`gh-pages-artifacts: artifact list routes unavailable: ${String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|