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/content.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read one regular file that lies inside the Session working directory. Publishing makes data
|
|
3
|
+
* public, so this is stricter than the built-in read tool: symlinks, directories, and paths that
|
|
4
|
+
* resolve outside the workspace are refused.
|
|
5
|
+
* @param ctx - plugin context providing `fs`.
|
|
6
|
+
* @param exec - tool execution (Session working directory, cancellation).
|
|
7
|
+
* @param path - workspace-relative or absolute path.
|
|
8
|
+
* @param maxBytes - size ceiling.
|
|
9
|
+
* @returns the file bytes.
|
|
10
|
+
*/
|
|
11
|
+
export async function readWorkspaceFile(ctx, exec, path, maxBytes) {
|
|
12
|
+
if (path.trim() === '')
|
|
13
|
+
throw new Error('path must not be empty');
|
|
14
|
+
const fs = ctx.get('fs');
|
|
15
|
+
if (fs === undefined)
|
|
16
|
+
throw new Error('Publishing from a path needs the filesystem service, which this harness does not provide; pass the content inline instead');
|
|
17
|
+
const cwd = exec.agent?.session.header.cwd;
|
|
18
|
+
if (cwd === undefined)
|
|
19
|
+
throw new Error('Publishing from a path needs a Session workspace; pass the content inline instead');
|
|
20
|
+
const signal = exec.signal;
|
|
21
|
+
const entry = await fs.lstat(path, { cwd }, signal);
|
|
22
|
+
if (entry === undefined)
|
|
23
|
+
throw new Error(`${path} does not exist; write the file first or check the path`);
|
|
24
|
+
if (entry.type !== 'file')
|
|
25
|
+
throw new Error(`${path} is not a regular file (symlinks and directories are not published)`);
|
|
26
|
+
const root = await fs.resolve(cwd, { signal });
|
|
27
|
+
const target = await fs.resolve(path, { cwd, signal });
|
|
28
|
+
if (!fs.contains(root, target))
|
|
29
|
+
throw new Error(`${path} is outside the Session workspace; only files inside the workspace can be published`);
|
|
30
|
+
const info = await fs.stat(target, signal);
|
|
31
|
+
if (info === undefined || info.type !== 'file')
|
|
32
|
+
throw new Error(`${path} is not a regular file`);
|
|
33
|
+
if (info.size !== undefined && info.size > maxBytes) {
|
|
34
|
+
throw new Error(`${path} is ${formatBytes(info.size)}, above the ${formatBytes(maxBytes)} publish limit`);
|
|
35
|
+
}
|
|
36
|
+
return await fs.readBytes(target, signal, maxBytes);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The last path segment, used as the default asset name.
|
|
40
|
+
* @param path - workspace path.
|
|
41
|
+
*/
|
|
42
|
+
export function baseName(path) {
|
|
43
|
+
const parts = path.split(/[\\/]/).filter(part => part !== '');
|
|
44
|
+
return parts.at(-1) ?? path;
|
|
45
|
+
}
|
|
46
|
+
/** Extensions published as text and therefore screened for secrets. */
|
|
47
|
+
const TEXT_EXTENSIONS = new Set([
|
|
48
|
+
'html', 'htm', 'md', 'markdown', 'txt', 'css', 'js', 'mjs', 'json', 'svg', 'xml', 'csv', 'tsv', 'yaml', 'yml', 'map',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* @param name - file name.
|
|
52
|
+
* @returns whether the name has a text extension.
|
|
53
|
+
*/
|
|
54
|
+
export function isTextName(name) {
|
|
55
|
+
const dot = name.lastIndexOf('.');
|
|
56
|
+
return dot >= 0 && TEXT_EXTENSIONS.has(name.slice(dot + 1).toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
const SECRET_PATTERNS = [
|
|
59
|
+
{ label: 'GitHub token', pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/ },
|
|
60
|
+
{ label: 'private key', pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
|
|
61
|
+
{ label: 'AWS access key id', pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
62
|
+
{ label: 'AWS secret access key', pattern: /aws_secret_access_key\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}\b/i },
|
|
63
|
+
{ label: 'Slack token', pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/ },
|
|
64
|
+
{ label: 'API secret key (sk-...)', pattern: /\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{32,}\b/ },
|
|
65
|
+
{ label: 'Google API key', pattern: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
66
|
+
{ label: 'Google OAuth token', pattern: /\b(?:ya29\.[0-9A-Za-z_-]{20,}|1\/\/0[0-9A-Za-z_-]{30,})/ },
|
|
67
|
+
{ label: 'Stripe secret key', pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}\b/ },
|
|
68
|
+
{ label: 'npm token', pattern: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
69
|
+
{ label: 'GitLab token', pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/ },
|
|
70
|
+
{ label: 'Hugging Face token', pattern: /\bhf_[A-Za-z0-9]{30,}\b/ },
|
|
71
|
+
{ label: 'PyPI token', pattern: /\bpypi-[A-Za-z0-9_-]{50,}\b/ },
|
|
72
|
+
{ label: 'JSON Web Token', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
73
|
+
{ label: 'password in a URL', pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/?#@'"<>]+:[^\s/?#@'"<>]{3,}@[^\s/?#'"<>]+/i },
|
|
74
|
+
{
|
|
75
|
+
label: 'password or secret assignment',
|
|
76
|
+
pattern: /\b(?:password|passwd|pwd|secret|client_secret|api[_-]?key|access[_-]?token|auth[_-]?token)\b["']?\s*[:=]\s*["']?(?=[^\s"'<>{}$]*\d)(?=[^\s"'<>{}$]*[A-Za-z])[^\s"'<>{}$]{12,}/i,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Find credential-like strings in text that is about to be published.
|
|
81
|
+
* @param text - content to screen.
|
|
82
|
+
* @param knownSecrets - exact secret values that must never appear (for example the token in use);
|
|
83
|
+
* their base64, hex, and URL-encoded forms are checked too.
|
|
84
|
+
* @returns labels of the kinds found; empty when clean.
|
|
85
|
+
*/
|
|
86
|
+
export function findSecrets(text, knownSecrets = []) {
|
|
87
|
+
const found = new Set();
|
|
88
|
+
for (const secret of knownSecrets) {
|
|
89
|
+
if (secret.length < 8)
|
|
90
|
+
continue;
|
|
91
|
+
const forms = [
|
|
92
|
+
secret,
|
|
93
|
+
Buffer.from(secret).toString('base64').replace(/=+$/, ''),
|
|
94
|
+
Buffer.from(secret).toString('base64url'),
|
|
95
|
+
Buffer.from(secret).toString('hex'),
|
|
96
|
+
encodeURIComponent(secret),
|
|
97
|
+
];
|
|
98
|
+
if (forms.some(form => text.includes(form)))
|
|
99
|
+
found.add('the GitHub token this plugin uses');
|
|
100
|
+
}
|
|
101
|
+
for (const { label, pattern } of SECRET_PATTERNS) {
|
|
102
|
+
if (pattern.test(text))
|
|
103
|
+
found.add(label);
|
|
104
|
+
}
|
|
105
|
+
return [...found];
|
|
106
|
+
}
|
|
107
|
+
const CREDENTIAL_NAMES = [
|
|
108
|
+
/^\.env(?:\..*)?$/i, /^\.envrc$/i, /^\.netrc$/i, /^_netrc$/i, /^\.npmrc$/i, /^\.pypirc$/i, /^\.git-credentials$/i,
|
|
109
|
+
/^\.pgpass$/i, /^\.htpasswd$/i, /^id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?$/i, /\.(?:pem|key|p12|pfx|jks|keystore|kdbx|gpg|asc|ovpn)$/i,
|
|
110
|
+
/^credentials(?:\.(?:json|ya?ml|toml|ini|env|csv))?$/i, /^secrets?\.(?:json|ya?ml|toml|ini|env)$/i, /^\.credentials\.ya?ml$/i, /^service[-_]?account.*\.json$/i,
|
|
111
|
+
/^application_default_credentials\.json$/i, /^kubeconfig$/i,
|
|
112
|
+
];
|
|
113
|
+
const CREDENTIAL_DIRS = new Set(['.ssh', '.aws', '.gnupg', '.dsh', '.git', '.kube', '.docker', '.config', '.azure', '.gcloud']);
|
|
114
|
+
/**
|
|
115
|
+
* Check whether a workspace path looks like a credential file or a hidden file.
|
|
116
|
+
* @param path - workspace path as given.
|
|
117
|
+
* @returns a reason to refuse, or undefined when it looks like ordinary content.
|
|
118
|
+
*/
|
|
119
|
+
export function credentialPathProblem(path) {
|
|
120
|
+
const segments = path.split(/[\\/]/).filter(segment => segment !== '' && segment !== '.');
|
|
121
|
+
for (const segment of segments.slice(0, -1)) {
|
|
122
|
+
if (CREDENTIAL_DIRS.has(segment.toLowerCase()))
|
|
123
|
+
return `${path} is inside ${segment}/, which holds credentials or configuration`;
|
|
124
|
+
}
|
|
125
|
+
const name = segments.at(-1) ?? path;
|
|
126
|
+
if (CREDENTIAL_NAMES.some(pattern => pattern.test(name)))
|
|
127
|
+
return `${path} looks like a credential or key file`;
|
|
128
|
+
if (name.startsWith('.'))
|
|
129
|
+
return `${path} is a hidden file`;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @param bytes - byte count.
|
|
134
|
+
* @returns a short human size.
|
|
135
|
+
*/
|
|
136
|
+
export function formatBytes(bytes) {
|
|
137
|
+
if (bytes < 1024)
|
|
138
|
+
return `${bytes} B`;
|
|
139
|
+
if (bytes < 1024 * 1024)
|
|
140
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
141
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
142
|
+
}
|
package/lib/github.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Minimal GitHub REST client: JSON requests, bounded retries, rate-limit handling, token scrubbing. */
|
|
2
|
+
/** Options for one {@link GitHubClient}. */
|
|
3
|
+
export interface GitHubClientOptions {
|
|
4
|
+
/** API base URL without trailing slash, e.g. https://api.github.com. */
|
|
5
|
+
readonly apiBaseUrl: string;
|
|
6
|
+
/** Bearer token. Never logged, never placed in errors. */
|
|
7
|
+
readonly token: string;
|
|
8
|
+
/** User-Agent header; GitHub rejects requests without one. */
|
|
9
|
+
readonly userAgent: string;
|
|
10
|
+
/** Fetch implementation; defaults to the global fetch (which honours the harness proxy). */
|
|
11
|
+
readonly fetch?: typeof fetch;
|
|
12
|
+
/** Per-request timeout in milliseconds. */
|
|
13
|
+
readonly requestTimeoutMs?: number;
|
|
14
|
+
/** Retries for transient failures (network errors, 5xx, rate limits). */
|
|
15
|
+
readonly maxRetries?: number;
|
|
16
|
+
/** Longest wait the client accepts for a rate-limit reset before failing. */
|
|
17
|
+
readonly maxRateLimitWaitMs?: number;
|
|
18
|
+
/** Sleep implementation, replaceable in tests. */
|
|
19
|
+
readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Per-request options. */
|
|
22
|
+
export interface RequestOptions {
|
|
23
|
+
/** JSON body. */
|
|
24
|
+
readonly body?: unknown;
|
|
25
|
+
/** Statuses returned to the caller instead of thrown. */
|
|
26
|
+
readonly allowStatus?: readonly number[];
|
|
27
|
+
/** Whether transient failures may be retried; false for non-idempotent writes. */
|
|
28
|
+
readonly retry?: boolean;
|
|
29
|
+
/** Caller cancellation. */
|
|
30
|
+
readonly signal?: AbortSignal | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** A completed response. */
|
|
33
|
+
export interface GitHubResponse<T> {
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly data: T;
|
|
36
|
+
readonly headers: Headers;
|
|
37
|
+
}
|
|
38
|
+
/** A GitHub API error with the HTTP status and GitHub's message. */
|
|
39
|
+
export declare class GitHubApiError extends Error {
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly method: string;
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly apiMessage: string;
|
|
44
|
+
readonly acceptedPermissions?: string | undefined;
|
|
45
|
+
readonly name = "GitHubApiError";
|
|
46
|
+
constructor(status: number, method: string, path: string, apiMessage: string, acceptedPermissions?: string | undefined);
|
|
47
|
+
}
|
|
48
|
+
/** Thin JSON client for the GitHub REST API. */
|
|
49
|
+
export declare class GitHubClient {
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly fetchImpl;
|
|
52
|
+
private readonly timeoutMs;
|
|
53
|
+
private readonly maxRetries;
|
|
54
|
+
private readonly maxRateLimitWaitMs;
|
|
55
|
+
private readonly sleep;
|
|
56
|
+
constructor(options: GitHubClientOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Send one request.
|
|
59
|
+
* @param method - HTTP method.
|
|
60
|
+
* @param path - path beginning with '/', relative to the API base URL.
|
|
61
|
+
* @param options - body, allowed statuses, retry policy, cancellation.
|
|
62
|
+
* @returns the parsed response.
|
|
63
|
+
* @throws GitHubApiError for statuses outside 2xx and `allowStatus`.
|
|
64
|
+
*/
|
|
65
|
+
request<T = unknown>(method: string, path: string, options?: RequestOptions): Promise<GitHubResponse<T>>;
|
|
66
|
+
private send;
|
|
67
|
+
/**
|
|
68
|
+
* Remove the token from text that may reach the model or logs.
|
|
69
|
+
* @param text - text to clean.
|
|
70
|
+
* @returns the text with every token occurrence replaced.
|
|
71
|
+
*/
|
|
72
|
+
scrub(text: string): string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Encode each segment of a repository path for use in a URL.
|
|
76
|
+
* @param path - slash-separated path.
|
|
77
|
+
*/
|
|
78
|
+
export declare function encodePath(path: string): string;
|
package/lib/github.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** Minimal GitHub REST client: JSON requests, bounded retries, rate-limit handling, token scrubbing. */
|
|
2
|
+
/** A GitHub API error with the HTTP status and GitHub's message. */
|
|
3
|
+
export class GitHubApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
method;
|
|
6
|
+
path;
|
|
7
|
+
apiMessage;
|
|
8
|
+
acceptedPermissions;
|
|
9
|
+
name = 'GitHubApiError';
|
|
10
|
+
constructor(status, method, path, apiMessage, acceptedPermissions) {
|
|
11
|
+
super(`GitHub API ${method} ${path} failed with ${status}: ${apiMessage}`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.method = method;
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.apiMessage = apiMessage;
|
|
16
|
+
this.acceptedPermissions = acceptedPermissions;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const API_VERSION = '2022-11-28';
|
|
20
|
+
/** Thin JSON client for the GitHub REST API. */
|
|
21
|
+
export class GitHubClient {
|
|
22
|
+
options;
|
|
23
|
+
fetchImpl;
|
|
24
|
+
timeoutMs;
|
|
25
|
+
maxRetries;
|
|
26
|
+
maxRateLimitWaitMs;
|
|
27
|
+
sleep;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
31
|
+
this.timeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
32
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
33
|
+
this.maxRateLimitWaitMs = options.maxRateLimitWaitMs ?? 60_000;
|
|
34
|
+
this.sleep = options.sleep ?? abortableSleep;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Send one request.
|
|
38
|
+
* @param method - HTTP method.
|
|
39
|
+
* @param path - path beginning with '/', relative to the API base URL.
|
|
40
|
+
* @param options - body, allowed statuses, retry policy, cancellation.
|
|
41
|
+
* @returns the parsed response.
|
|
42
|
+
* @throws GitHubApiError for statuses outside 2xx and `allowStatus`.
|
|
43
|
+
*/
|
|
44
|
+
async request(method, path, options = {}) {
|
|
45
|
+
const retry = options.retry ?? true;
|
|
46
|
+
let attempt = 0;
|
|
47
|
+
for (;;) {
|
|
48
|
+
options.signal?.throwIfAborted();
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await this.send(method, path, options);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (options.signal?.aborted === true)
|
|
55
|
+
throw options.signal.reason;
|
|
56
|
+
if (retry && attempt < this.maxRetries) {
|
|
57
|
+
await this.sleep(backoff(attempt++), options.signal);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(this.scrub(`GitHub API ${method} ${path} could not be reached: ${describeError(error)}`));
|
|
61
|
+
}
|
|
62
|
+
const status = response.status;
|
|
63
|
+
if (status >= 200 && status < 300 || options.allowStatus?.includes(status) === true) {
|
|
64
|
+
return { status, data: await readBody(response), headers: response.headers };
|
|
65
|
+
}
|
|
66
|
+
const body = await readBody(response).catch(() => undefined);
|
|
67
|
+
const apiMessage = this.scrub(messageOf(body) ?? response.statusText ?? 'request failed');
|
|
68
|
+
const rateWait = rateLimitWait(response, apiMessage);
|
|
69
|
+
if (rateWait !== undefined) {
|
|
70
|
+
if (rateWait <= this.maxRateLimitWaitMs && attempt < this.maxRetries) {
|
|
71
|
+
attempt++;
|
|
72
|
+
await this.sleep(rateWait, options.signal);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw new GitHubApiError(status, method, path, `${apiMessage} (GitHub rate limit; retry in about ${Math.ceil(rateWait / 1000)} s)`);
|
|
76
|
+
}
|
|
77
|
+
if (status >= 500 && retry && attempt < this.maxRetries) {
|
|
78
|
+
await this.sleep(backoff(attempt++), options.signal);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw new GitHubApiError(status, method, path, apiMessage, response.headers.get('x-accepted-github-permissions') ?? undefined);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async send(method, path, options) {
|
|
85
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
86
|
+
const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout]);
|
|
87
|
+
const headers = {
|
|
88
|
+
'Accept': 'application/vnd.github+json',
|
|
89
|
+
'Authorization': `Bearer ${this.options.token}`,
|
|
90
|
+
'User-Agent': this.options.userAgent,
|
|
91
|
+
'X-GitHub-Api-Version': API_VERSION,
|
|
92
|
+
};
|
|
93
|
+
const init = { method, headers, signal, redirect: 'follow' };
|
|
94
|
+
if (options.body !== undefined) {
|
|
95
|
+
headers['Content-Type'] = 'application/json';
|
|
96
|
+
init.body = JSON.stringify(options.body);
|
|
97
|
+
}
|
|
98
|
+
return await this.fetchImpl(`${this.options.apiBaseUrl}${path}`, init);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove the token from text that may reach the model or logs.
|
|
102
|
+
* @param text - text to clean.
|
|
103
|
+
* @returns the text with every token occurrence replaced.
|
|
104
|
+
*/
|
|
105
|
+
scrub(text) {
|
|
106
|
+
const token = this.options.token;
|
|
107
|
+
return token.length >= 8 ? text.split(token).join('***') : text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function readBody(response) {
|
|
111
|
+
if (response.status === 204 || response.status === 205)
|
|
112
|
+
return undefined;
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
if (text === '')
|
|
115
|
+
return undefined;
|
|
116
|
+
const type = response.headers.get('content-type') ?? '';
|
|
117
|
+
if (type.includes('json'))
|
|
118
|
+
return JSON.parse(text);
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(text);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function messageOf(body) {
|
|
127
|
+
if (typeof body === 'string')
|
|
128
|
+
return body.slice(0, 300);
|
|
129
|
+
if (typeof body !== 'object' || body === null)
|
|
130
|
+
return undefined;
|
|
131
|
+
const record = body;
|
|
132
|
+
const message = typeof record.message === 'string' ? record.message : undefined;
|
|
133
|
+
const details = Array.isArray(record.errors)
|
|
134
|
+
? record.errors.map(item => typeof item === 'string' ? item : item.message)
|
|
135
|
+
.filter((item) => typeof item === 'string')
|
|
136
|
+
: [];
|
|
137
|
+
return details.length > 0 ? `${message ?? 'error'} (${details.join('; ')})` : message;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Milliseconds to wait before retrying a rate-limited response, or undefined when it is not one.
|
|
141
|
+
* @param response - the failed response.
|
|
142
|
+
* @param message - GitHub's message.
|
|
143
|
+
*/
|
|
144
|
+
function rateLimitWait(response, message) {
|
|
145
|
+
const status = response.status;
|
|
146
|
+
if (status !== 403 && status !== 429)
|
|
147
|
+
return undefined;
|
|
148
|
+
const retryAfter = response.headers.get('retry-after');
|
|
149
|
+
if (retryAfter !== null && /^\d+$/.test(retryAfter))
|
|
150
|
+
return Number(retryAfter) * 1000;
|
|
151
|
+
const remaining = response.headers.get('x-ratelimit-remaining');
|
|
152
|
+
const reset = response.headers.get('x-ratelimit-reset');
|
|
153
|
+
if (remaining === '0' && reset !== null && /^\d+$/.test(reset)) {
|
|
154
|
+
return Math.max(1000, Number(reset) * 1000 - Date.now());
|
|
155
|
+
}
|
|
156
|
+
if (status === 429 || /secondary rate limit|abuse/i.test(message))
|
|
157
|
+
return 60_000;
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
function backoff(attempt) {
|
|
161
|
+
return Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
|
|
162
|
+
}
|
|
163
|
+
function describeError(error) {
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
const cause = error.cause;
|
|
166
|
+
return cause instanceof Error ? `${error.message} (${cause.message})` : error.message;
|
|
167
|
+
}
|
|
168
|
+
return String(error);
|
|
169
|
+
}
|
|
170
|
+
async function abortableSleep(ms, signal) {
|
|
171
|
+
signal?.throwIfAborted();
|
|
172
|
+
await new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
signal?.removeEventListener('abort', onAbort);
|
|
175
|
+
resolve();
|
|
176
|
+
}, ms);
|
|
177
|
+
const onAbort = () => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
reject(signal?.reason);
|
|
180
|
+
};
|
|
181
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Encode each segment of a repository path for use in a URL.
|
|
186
|
+
* @param path - slash-separated path.
|
|
187
|
+
*/
|
|
188
|
+
export function encodePath(path) {
|
|
189
|
+
return path.split('/').map(encodeURIComponent).join('/');
|
|
190
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness plugin that lets agents publish HTML pages and Markdown documents as
|
|
3
|
+
* shareable artifacts on GitHub Pages.
|
|
4
|
+
* @module dsh-gh-pages-artifacts
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import { Config } from './config.js';
|
|
8
|
+
export { Config };
|
|
9
|
+
export type { ApprovalMode, RepoStrategy, SubagentAccess, Settings } from './config.js';
|
|
10
|
+
export { ArtifactsRuntime, VERSION } from './service.js';
|
|
11
|
+
export type { RuntimeHooks } from './service.js';
|
|
12
|
+
export { ALL_TOOLS, MUTATING_TOOLS, PUBLISH_TOOL, LIST_TOOL, READ_TOOL, DELETE_TOOL, STATUS_TOOL, REPOSITORY_TOOL } from './tools.js';
|
|
13
|
+
/** Loader identity. */
|
|
14
|
+
export declare const name = "gh-pages-artifacts";
|
|
15
|
+
/** Required services; the skill registry, system prompt, credentials, and approval are optional. */
|
|
16
|
+
export declare const inject: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Register the artifact tools, their visibility rules, prompt guidance, and the bundled skill.
|
|
19
|
+
* @param ctx - host plugin context.
|
|
20
|
+
* @param config - validated configuration.
|
|
21
|
+
*/
|
|
22
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Config, normalizeConfig } from './config.js';
|
|
2
|
+
import { registerPrompt } from './prompt.js';
|
|
3
|
+
import { ArtifactsRuntime } from './service.js';
|
|
4
|
+
import { registerSkill } from './skill.js';
|
|
5
|
+
import { ALL_TOOLS, createTools, MUTATING_TOOLS } from './tools.js';
|
|
6
|
+
import { installVisibility } from './visibility.js';
|
|
7
|
+
import { installWebRoutes } from './web-routes.js';
|
|
8
|
+
export { Config };
|
|
9
|
+
export { ArtifactsRuntime, VERSION } from './service.js';
|
|
10
|
+
export { ALL_TOOLS, MUTATING_TOOLS, PUBLISH_TOOL, LIST_TOOL, READ_TOOL, DELETE_TOOL, STATUS_TOOL, REPOSITORY_TOOL } from './tools.js';
|
|
11
|
+
/** Loader identity. */
|
|
12
|
+
export const name = 'gh-pages-artifacts';
|
|
13
|
+
/** Required services; the skill registry, system prompt, credentials, and approval are optional. */
|
|
14
|
+
export const inject = ['tools'];
|
|
15
|
+
/**
|
|
16
|
+
* Register the artifact tools, their visibility rules, prompt guidance, and the bundled skill.
|
|
17
|
+
* @param ctx - host plugin context.
|
|
18
|
+
* @param config - validated configuration.
|
|
19
|
+
*/
|
|
20
|
+
export function apply(ctx, config) {
|
|
21
|
+
// Validate once up front; volatile options (edited on the settings page) are re-read per operation.
|
|
22
|
+
const settings = normalizeConfig(config);
|
|
23
|
+
// The entry id names this row's settings namespace, which artifact_repository writes to.
|
|
24
|
+
const entryId = ctx.fiber.entry?.options?.id;
|
|
25
|
+
const runtime = new ArtifactsRuntime(ctx, () => normalizeConfig(config), {}, typeof entryId === 'string' ? entryId : undefined);
|
|
26
|
+
for (const tool of createTools(runtime))
|
|
27
|
+
ctx.tools.register(tool);
|
|
28
|
+
// The sidebar's Artifacts panel reads the list through these routes (web and desktop only).
|
|
29
|
+
installWebRoutes(ctx, runtime);
|
|
30
|
+
installVisibility(ctx, {
|
|
31
|
+
allTools: ALL_TOOLS,
|
|
32
|
+
mutatingTools: MUTATING_TOOLS,
|
|
33
|
+
hideFromPresets: settings.hideFromPresets,
|
|
34
|
+
subagentAccess: settings.subagentAccess,
|
|
35
|
+
});
|
|
36
|
+
if (settings.promptGuidance)
|
|
37
|
+
ctx.inject(['systemPrompt'], promptCtx => registerPrompt(promptCtx, settings.bundledSkill));
|
|
38
|
+
if (settings.bundledSkill) {
|
|
39
|
+
ctx.inject(['skills'], async (skillsCtx) => {
|
|
40
|
+
try {
|
|
41
|
+
await registerSkill(skillsCtx);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
skillsCtx.logger.warn(`gh-pages-artifacts: bundled skill unavailable: ${String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Artifact manifest stored on the Pages branch, plus id and asset-name rules. */
|
|
2
|
+
/** Repository-root path of the manifest. It sits outside the artifact folders. */
|
|
3
|
+
export declare const MANIFEST_PATH = ".dsh-artifacts.json";
|
|
4
|
+
/** Kinds of artifact the plugin publishes. */
|
|
5
|
+
export type ArtifactKind = 'html' | 'markdown';
|
|
6
|
+
/** One published artifact. */
|
|
7
|
+
export interface ArtifactRecord {
|
|
8
|
+
/** Stable id; the URL path segment. Never changes. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Human title; may change without moving the URL. */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Page kind. */
|
|
13
|
+
kind: ArtifactKind;
|
|
14
|
+
/** Optional one-line summary. */
|
|
15
|
+
description?: string;
|
|
16
|
+
/** ISO timestamp of the first publish. */
|
|
17
|
+
createdAt: string;
|
|
18
|
+
/** ISO timestamp of the latest publish. */
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
/** Revision, starting at 1 and incremented by every update. */
|
|
21
|
+
rev: number;
|
|
22
|
+
/** Files published in the artifact folder, relative to it, sorted. */
|
|
23
|
+
files: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Manifest file shape. */
|
|
26
|
+
export interface Manifest {
|
|
27
|
+
version: 1;
|
|
28
|
+
artifacts: Record<string, ArtifactRecord>;
|
|
29
|
+
/** Deleted ids, never reused, so an old link cannot show different content. */
|
|
30
|
+
tombstones: string[];
|
|
31
|
+
}
|
|
32
|
+
/** File holding the served page. */
|
|
33
|
+
export declare const PAGE_FILE = "index.html";
|
|
34
|
+
/** File holding the Markdown source of a markdown artifact. */
|
|
35
|
+
export declare const SOURCE_FILE = "source.md";
|
|
36
|
+
/** Valid artifact id: lowercase letters, digits, and inner hyphens, 1-64 characters. */
|
|
37
|
+
export declare const ID_PATTERN: RegExp;
|
|
38
|
+
/** Ids that would collide with site-level files or common folders. */
|
|
39
|
+
export declare const RESERVED_IDS: ReadonlySet<string>;
|
|
40
|
+
/** Error for a manifest that cannot be read safely. */
|
|
41
|
+
export declare class ManifestError extends Error {
|
|
42
|
+
readonly name = "ManifestError";
|
|
43
|
+
}
|
|
44
|
+
/** @returns an empty manifest. */
|
|
45
|
+
export declare function emptyManifest(): Manifest;
|
|
46
|
+
/**
|
|
47
|
+
* Parse and validate manifest text. A malformed manifest is never overwritten silently.
|
|
48
|
+
* @param text - file content.
|
|
49
|
+
* @returns the manifest.
|
|
50
|
+
* @throws ManifestError when the content is not a version-1 manifest.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseManifest(text: string): Manifest;
|
|
53
|
+
/**
|
|
54
|
+
* Serialize a manifest deterministically so diffs stay readable.
|
|
55
|
+
* @param manifest - manifest to write.
|
|
56
|
+
* @returns JSON text with a trailing newline.
|
|
57
|
+
*/
|
|
58
|
+
export declare function serializeManifest(manifest: Manifest): string;
|
|
59
|
+
/**
|
|
60
|
+
* Turn a title into a URL slug: lowercase ASCII letters and digits joined by hyphens.
|
|
61
|
+
* @param title - free text.
|
|
62
|
+
* @param maxLength - longest result.
|
|
63
|
+
* @returns the slug, or 'artifact' when nothing usable remains.
|
|
64
|
+
*/
|
|
65
|
+
export declare function slugify(title: string, maxLength?: number): string;
|
|
66
|
+
/**
|
|
67
|
+
* Build a fresh id from a title plus a random suffix, so URLs are readable but not guessable.
|
|
68
|
+
* @param title - artifact title.
|
|
69
|
+
* @param suffix - random lowercase base32 text.
|
|
70
|
+
*/
|
|
71
|
+
export declare function idFromTitle(title: string, suffix: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Check a caller-chosen id.
|
|
74
|
+
* @param id - candidate id.
|
|
75
|
+
* @returns an error message, or undefined when valid.
|
|
76
|
+
*/
|
|
77
|
+
export declare function idProblem(id: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Check an asset name, the path of an extra file relative to the artifact folder.
|
|
80
|
+
* @param name - candidate name such as `chart.png` or `img/logo.svg`.
|
|
81
|
+
* @returns an error message, or undefined when valid.
|
|
82
|
+
*/
|
|
83
|
+
export declare function assetNameProblem(name: string): string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* @param path - candidate relative path.
|
|
86
|
+
* @returns whether it is 1-4 safe segments with no dot-segments.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isSafeRelativePath(path: string): boolean;
|