Publish Agent Skills discovery metadata
This commit is contained in:
parent
8799cee0ca
commit
3d7254aaf0
@ -91,6 +91,12 @@ After a local install, the skill is available at:
|
|||||||
node_modules/crypstie/skills/crypstie
|
node_modules/crypstie/skills/crypstie
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The canonical public copy and its SHA-256 discovery metadata are available at
|
||||||
|
`https://crypstie.com/.well-known/agent-skills/index.json`. Maintainers can run
|
||||||
|
`npm run deploy` to test the package, publish an unpublished package version,
|
||||||
|
and atomically update the public skill. Use `-- --npm-only` or
|
||||||
|
`-- --skills-only` to deploy just one part.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
22
deploy/nginx/crypstie-agent-skills.conf
Normal file
22
deploy/nginx/crypstie-agent-skills.conf
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Public Agent Skills metadata is stored outside the frontend release tree so
|
||||||
|
# application deployments cannot overwrite it.
|
||||||
|
location = /.well-known/agent-skills/index.json {
|
||||||
|
alias /home/crypstie/agent-skills/index.json;
|
||||||
|
default_type application/json;
|
||||||
|
add_header Cache-Control "public, max-age=300";
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /.well-known/agent-skills/crypstie/SKILL.md {
|
||||||
|
alias /home/crypstie/agent-skills/crypstie/SKILL.md;
|
||||||
|
default_type text/markdown;
|
||||||
|
add_header Cache-Control "public, max-age=300";
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /llms.txt {
|
||||||
|
alias /home/crypstie/agent-skills/llms.txt;
|
||||||
|
default_type text/plain;
|
||||||
|
add_header Cache-Control "public, max-age=300";
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
}
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "crypstie",
|
"name": "crypstie",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "crypstie",
|
"name": "crypstie",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"unicrypto": "^1.14.1"
|
"unicrypto": "^1.14.1"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "crypstie",
|
"name": "crypstie",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"description": "Create and open end-to-end encrypted Crypstie links from Node.js and the command line",
|
"description": "Create and open end-to-end encrypted Crypstie links from Node.js and the command line",
|
||||||
"author": "sergeych",
|
"author": "sergeych",
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -20,12 +20,17 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"src/",
|
"src/",
|
||||||
"skills/",
|
"skills/",
|
||||||
|
"scripts/build-skill-discovery.js",
|
||||||
|
"scripts/deploy",
|
||||||
|
"deploy/nginx/",
|
||||||
"docs/",
|
"docs/",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test"
|
"test": "node --test",
|
||||||
|
"build:discovery": "node scripts/build-skill-discovery.js",
|
||||||
|
"deploy": "scripts/deploy"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
62
scripts/build-skill-discovery.js
Executable file
62
scripts/build-skill-discovery.js
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
const source = path.join(projectRoot, 'skills', 'crypstie', 'SKILL.md');
|
||||||
|
const outputRoot = path.resolve(process.argv[2] || path.join(projectRoot, 'build', 'agent-skills'));
|
||||||
|
const skillDirectory = path.join(outputRoot, 'crypstie');
|
||||||
|
const skillTarget = path.join(skillDirectory, 'SKILL.md');
|
||||||
|
const indexTarget = path.join(outputRoot, 'index.json');
|
||||||
|
const llmsTarget = path.join(outputRoot, 'llms.txt');
|
||||||
|
|
||||||
|
const skill = fs.readFileSync(source);
|
||||||
|
const text = skill.toString('utf8');
|
||||||
|
const description = text.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
||||||
|
|
||||||
|
if (!description) {
|
||||||
|
throw new Error('skills/crypstie/SKILL.md has no description in its frontmatter');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(skillDirectory, { recursive: true });
|
||||||
|
fs.writeFileSync(skillTarget, skill);
|
||||||
|
|
||||||
|
const digest = crypto.createHash('sha256').update(skill).digest('hex');
|
||||||
|
const index = {
|
||||||
|
$schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
name: 'crypstie',
|
||||||
|
type: 'skill-md',
|
||||||
|
description,
|
||||||
|
url: '/.well-known/agent-skills/crypstie/SKILL.md',
|
||||||
|
digest: `sha256:${digest}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(indexTarget, `${JSON.stringify(index, null, 2)}\n`);
|
||||||
|
fs.writeFileSync(
|
||||||
|
llmsTarget,
|
||||||
|
[
|
||||||
|
'# Crypstie',
|
||||||
|
'',
|
||||||
|
'> End-to-end encrypted links for text and files.',
|
||||||
|
'',
|
||||||
|
'## Agent skill',
|
||||||
|
'',
|
||||||
|
'- [Crypstie skill](https://crypstie.com/.well-known/agent-skills/crypstie/SKILL.md): Create and open Crypstie links with the npm CLI.',
|
||||||
|
'- [Skill discovery index](https://crypstie.com/.well-known/agent-skills/index.json): Machine-readable Agent Skills index with integrity metadata.',
|
||||||
|
'',
|
||||||
|
'## Package',
|
||||||
|
'',
|
||||||
|
'- [crypstie on npm](https://www.npmjs.com/package/crypstie): Node.js library and command-line client.',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(outputRoot);
|
||||||
63
scripts/deploy
Executable file
63
scripts/deploy
Executable file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
SSH_HOST=${CRYPSTIE_DEPLOY_HOST:-crypstie@sergeych.net}
|
||||||
|
REMOTE_ROOT=${CRYPSTIE_SKILLS_ROOT:-/home/crypstie/agent-skills}
|
||||||
|
TOKEN_FILE=${CRYPSTIE_NPM_TOKEN_FILE:-${PROJECT_ROOT}/.node_token}
|
||||||
|
DEPLOY_NPM=1
|
||||||
|
DEPLOY_SKILLS=1
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: scripts/deploy [--npm-only | --skills-only]"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
"") ;;
|
||||||
|
--npm-only) DEPLOY_SKILLS=0 ;;
|
||||||
|
--skills-only) DEPLOY_NPM=0 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
npm test
|
||||||
|
|
||||||
|
STAGING=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$STAGING"' EXIT
|
||||||
|
|
||||||
|
if [[ "$DEPLOY_NPM" == 1 ]]; then
|
||||||
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||||
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
|
||||||
|
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version --json >/dev/null 2>&1; then
|
||||||
|
echo "npm: ${PACKAGE_NAME}@${PACKAGE_VERSION} is already published; skipping"
|
||||||
|
else
|
||||||
|
if [[ ! -r "$TOKEN_FILE" ]]; then
|
||||||
|
echo "npm: token file is not readable: $TOKEN_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TOKEN=$(tr -d '\r\n' < "$TOKEN_FILE")
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "npm: token file is empty: $TOKEN_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NPMRC="$STAGING/npmrc"
|
||||||
|
(umask 077; printf '//registry.npmjs.org/:_authToken=%s\n' "$TOKEN" > "$NPMRC")
|
||||||
|
npm publish --access public --userconfig "$NPMRC"
|
||||||
|
unset TOKEN
|
||||||
|
rm -f -- "$NPMRC"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DEPLOY_SKILLS" == 1 ]]; then
|
||||||
|
DISCOVERY="$STAGING/discovery"
|
||||||
|
node scripts/build-skill-discovery.js "$DISCOVERY" >/dev/null
|
||||||
|
REMOTE_STAGING="${REMOTE_ROOT}.incoming"
|
||||||
|
|
||||||
|
ssh "$SSH_HOST" "mkdir -p '$REMOTE_STAGING'"
|
||||||
|
rsync -az --delete "$DISCOVERY/" "$SSH_HOST:$REMOTE_STAGING/"
|
||||||
|
ssh "$SSH_HOST" "set -e; chmod -R a+rX '$REMOTE_STAGING'; rm -rf '${REMOTE_ROOT}.previous'; if [ -d '$REMOTE_ROOT' ]; then mv '$REMOTE_ROOT' '${REMOTE_ROOT}.previous'; fi; mv '$REMOTE_STAGING' '$REMOTE_ROOT'"
|
||||||
|
echo "skills: deployed to https://crypstie.com/.well-known/agent-skills/index.json"
|
||||||
|
fi
|
||||||
31
test/discovery.test.js
Normal file
31
test/discovery.test.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
test('builds a valid discovery index matching the published skill', () => {
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const output = fs.mkdtempSync(path.join(os.tmpdir(), 'crypstie-discovery-'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
execFileSync(process.execPath, [path.join(root, 'scripts', 'build-skill-discovery.js'), output]);
|
||||||
|
const skill = fs.readFileSync(path.join(output, 'crypstie', 'SKILL.md'));
|
||||||
|
const index = JSON.parse(fs.readFileSync(path.join(output, 'index.json'), 'utf8'));
|
||||||
|
const expectedDigest = crypto.createHash('sha256').update(skill).digest('hex');
|
||||||
|
|
||||||
|
assert.equal(index.$schema, 'https://schemas.agentskills.io/discovery/0.2.0/schema.json');
|
||||||
|
assert.equal(index.skills.length, 1);
|
||||||
|
assert.equal(index.skills[0].name, 'crypstie');
|
||||||
|
assert.equal(index.skills[0].type, 'skill-md');
|
||||||
|
assert.equal(index.skills[0].url, '/.well-known/agent-skills/crypstie/SKILL.md');
|
||||||
|
assert.equal(index.skills[0].digest, `sha256:${expectedDigest}`);
|
||||||
|
assert.match(fs.readFileSync(path.join(output, 'llms.txt'), 'utf8'), /crypstie on npm/);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(output, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user