From 3d7254aaf011a1901ca1ef8299e5a4dcbc050d6d Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 19 Aug 2026 19:46:19 +0400 Subject: [PATCH] Publish Agent Skills discovery metadata --- README.md | 6 +++ deploy/nginx/crypstie-agent-skills.conf | 22 +++++++++ package-lock.json | 4 +- package.json | 9 +++- scripts/build-skill-discovery.js | 62 ++++++++++++++++++++++++ scripts/deploy | 63 +++++++++++++++++++++++++ test/discovery.test.js | 31 ++++++++++++ 7 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 deploy/nginx/crypstie-agent-skills.conf create mode 100755 scripts/build-skill-discovery.js create mode 100755 scripts/deploy create mode 100644 test/discovery.test.js diff --git a/README.md b/README.md index 529e885..2604f6d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,12 @@ After a local install, the skill is available at: 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 ```shell diff --git a/deploy/nginx/crypstie-agent-skills.conf b/deploy/nginx/crypstie-agent-skills.conf new file mode 100644 index 0000000..8f6725b --- /dev/null +++ b/deploy/nginx/crypstie-agent-skills.conf @@ -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; +} diff --git a/package-lock.json b/package-lock.json index ee1708e..4edc135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "crypstie", - "version": "0.1.1", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crypstie", - "version": "0.1.1", + "version": "0.1.3", "license": "MIT", "dependencies": { "unicrypto": "^1.14.1" diff --git a/package.json b/package.json index 61a28c2..594ccf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "author": "sergeych", "repository": { @@ -20,12 +20,17 @@ "files": [ "src/", "skills/", + "scripts/build-skill-discovery.js", + "scripts/deploy", + "deploy/nginx/", "docs/", "README.md", "LICENSE" ], "scripts": { - "test": "node --test" + "test": "node --test", + "build:discovery": "node scripts/build-skill-discovery.js", + "deploy": "scripts/deploy" }, "engines": { "node": ">=18" diff --git a/scripts/build-skill-discovery.js b/scripts/build-skill-discovery.js new file mode 100755 index 0000000..dc64367 --- /dev/null +++ b/scripts/build-skill-discovery.js @@ -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); diff --git a/scripts/deploy b/scripts/deploy new file mode 100755 index 0000000..0854598 --- /dev/null +++ b/scripts/deploy @@ -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 diff --git a/test/discovery.test.js b/test/discovery.test.js new file mode 100644 index 0000000..89a7d5f --- /dev/null +++ b/test/discovery.test.js @@ -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 }); + } +});