Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/cross-platform-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ jobs:
run: bun run build
shell: bash

# The skills package copies a directory tree and resolves it back by
# path, so it is exactly the kind of code that breaks on Windows only.
- name: Run skills package tests
run: bun run --cwd packages/skills test
shell: bash

# Split into two bun-test invocations (two processes). The unit specs
# under src/ use `mock.module(...)` to stub out `StorageManager` and
# other modules; Bun does not scope those mocks to a single file —
Expand Down
112 changes: 112 additions & 0 deletions .github/workflows/skills-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: skills

# The skills package versions on its own line. release.yml bumps sdk, app, and
# server in lockstep and is deliberately not used here: a skill edit should not
# need a server release, which is the whole point of splitting the package out.

on:
push:
branches: [main]
paths:
- "skills/**"
- "packages/skills/**"
- ".github/workflows/skills-npm.yml"
pull_request:
paths:
- "skills/**"
- "packages/skills/**"
- ".github/workflows/skills-npm.yml"
workflow_dispatch:

env:
BUN_VERSION: 1.3.13
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
check_pack:
name: Verify the published tarball
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Install dependencies
run: bun install --frozen-lockfile

# A skills/ dir that failed to copy packs as nothing at all, with exit 0
# and no warning, so this is the only thing standing between a bad refactor
# and publishing an empty package.
- name: Check pack contents
run: bun run --cwd packages/skills check-pack

- name: Test
run: bun run --cwd packages/skills test

publish:
name: Publish to NPM
if: github.event_name == 'workflow_dispatch'
needs: check_pack
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"

- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Read version
id: version_step
run: |
CURRENT_VERSION="$(node -p "require('./packages/skills/package.json').version")"

# Stable versions publish under "latest"; any pre-release under "next".
if [[ "$CURRENT_VERSION" == *-* ]]; then
NPM_TAG="next"
else
NPM_TAG="latest"
fi

echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "npm_tag=$NPM_TAG" >> $GITHUB_OUTPUT
echo "::notice title=Current Version::Publishing skills version $CURRENT_VERSION under npm dist-tag '$NPM_TAG'"

# Trusted publishing (OIDC); there is no NPM_TOKEN fallback in this repo.
#
# npm configures trusted publishing per package, on the package's own
# settings page, so the package has to exist before the entry can be
# created. The first publish of a new name is therefore manual, by an
# @malloy-publisher owner. Order: publish once by hand, add the
# trusted-publisher entry naming this workflow file, then every release
# after that is a dispatch here. Run before that entry exists and npm's
# OIDC helper returns undefined rather than raising, so this falls through
# to the empty token and fails with a bare ENEEDAUTH.
#
# Run that manual publish with scripts ENABLED. prepack is what copies
# skills/ into the package, so `npm publish --ignore-scripts`, or an
# ignore-scripts=true in your own npmrc, ships three files and no skills
# at all, without an error.
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ""
run: |
cd packages/skills
npm publish --access public --tag ${{ steps.version_step.outputs.npm_tag }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ publisher_data/
# These are local Credible workflow files (an installed skill tree + IDE rules) and are
# ignored by the `credible-` prefix so a stray `git add` can't pull them in.
.credible/
skills/credible-*/
.claude/skills/credible-*/
.claude/rules/credible-*.md
.cursor/rules/credible-*.mdc
20 changes: 20 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,32 @@
"build:server": "cd packages/server && bun run build",
"build": "NODE_ENV=production bun run build:sdk && NODE_ENV=production bun run build:server",
"build:cli": "cd packages/cli && bun run build",
"build:skills": "cd packages/skills && bun run build",
"build:server-deploy": "NODE_ENV=production bun run build:sdk && NODE_ENV=production bun run build:app:server && NODE_ENV=production bun run build:server",
"start": "cd packages/server && bun run start",
"start:init": "cd packages/server && bun run start:init",
"start:dev": "cd packages/server && bun run start:dev",
"start:dev:init": "cd packages/server && bun run start:dev:init",
"start:dev:react": "cd packages/app && bun run dev",
"test:server": "cd packages/server && NODE_ENV=test bun run test",
"test": "NODE_ENV=test bun run test:server",
"test:skills": "cd packages/skills && bun run test",
"test": "NODE_ENV=test bun run test:server && bun run test:skills",
"generate-api-types:sdk": "cd packages/sdk && bun run generate-api-types",
"generate-api-types:server": "cd packages/server && bun run generate-api-types",
"generate-api-types": "bun run generate-api-types:sdk && bun run generate-api-types:server",
"generate-api-types:cli": "cd packages/cli && bun run generate-api-types",
"lint:sdk": "cd packages/sdk && bun run lint",
"lint:app": "cd packages/app && bun run lint",
"lint:server": "cd packages/server && bun run lint",
"lint": "bun run lint:sdk && bun run lint:app && bun run lint:server",
"lint:skills": "cd packages/skills && bun run lint",
"lint": "bun run lint:sdk && bun run lint:app && bun run lint:server && bun run lint:skills",
"lint:cli": "cd packages/cli && bun run lint",
"typecheck:sdk": "cd packages/sdk && bun run tsc --noEmit",
"typecheck:app": "cd packages/app && bun run tsc --noEmit",
"typecheck:server": "cd packages/server && bun run tsc --noEmit",
"typecheck:cli": "cd packages/cli && bun run typecheck",
"typecheck": "bun run typecheck:sdk && bun run typecheck:app && bun run typecheck:server",
"typecheck:skills": "cd packages/skills && bun run typecheck",
"typecheck": "bun run typecheck:sdk && bun run typecheck:app && bun run typecheck:server && bun run typecheck:skills",
"format:sdk": "cd packages/sdk && bun run format",
"format:app": "cd packages/app && bun run format",
"format:server": "cd packages/server && bun run format",
Expand Down
38 changes: 29 additions & 9 deletions packages/server/src/mcp/skills/build_skills_bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import * as fs from "fs";
import * as path from "path";

interface SkillEntry {
export interface SkillEntry {
name: string;
description: string;
body: string;
Expand All @@ -36,6 +36,33 @@ export function parseSkill(md: string, dirName: string): SkillEntry {
return { name, description, body };
}

/**
* `credible-*` skills are Credible-platform-specific and never ship (see
* skills/README.md). They can legitimately sit in skills/ uncommitted: the
* directory is a gitignored local install target (`skills/credible-*` in
* .gitignore), so the bundle must skip them or a regeneration on a machine
* with Credible installed would bake them into the committed bundle, and the
* regenerate-and-compare spec would fail on a clean checkout.
*
* The canonical predicate lives in packages/skills/scripts/exclusions.ts;
* duplicated here because the server does not depend on that package.
*/
export function isCredible(dirName: string): boolean {
return dirName.toLowerCase().startsWith("credible-");
}

/** Read every skill under a skills directory, in the bundle's own order. */
export function buildSkills(skillsDir: string): SkillEntry[] {
const skills: SkillEntry[] = [];
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
if (!entry.isDirectory() || isCredible(entry.name)) continue;
const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
if (!fs.existsSync(skillFile)) continue;
skills.push(parseSkill(fs.readFileSync(skillFile, "utf8"), entry.name));
}
return skills.sort((a, b) => a.name.localeCompare(b.name));
}

function main(): void {
const skillsDir = process.argv[2];
const outFile =
Expand All @@ -47,14 +74,7 @@ function main(): void {
process.exit(1);
}

const skills: SkillEntry[] = [];
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
if (!fs.existsSync(skillFile)) continue;
skills.push(parseSkill(fs.readFileSync(skillFile, "utf8"), entry.name));
}
skills.sort((a, b) => a.name.localeCompare(b.name));
const skills = buildSkills(skillsDir);

fs.writeFileSync(outFile, JSON.stringify({ skills }));
console.log(`Wrote ${skills.length} skills to ${outFile}`);
Expand Down
33 changes: 31 additions & 2 deletions packages/server/src/mcp/skills/skills_bundle.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import { buildSkills, type SkillEntry } from "./build_skills_bundle";
import bundle from "./skills_bundle.json";

const skills = (
Expand All @@ -7,9 +9,36 @@ const skills = (
}
).skills;

/** The repo's top-level skills/, which this bundle is generated from. */
const sourceDir = path.join(
import.meta.dir,
"..",
"..",
"..",
"..",
"..",
"skills",
);

/**
* Codepoint order. The committed bundle is sorted with localeCompare, which
* depends on the runtime's locale, and this suite runs on three platforms.
* Membership and content are what matter here; file order is cosmetic.
*/
const byName = (a: SkillEntry, b: SkillEntry) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0;

describe("skills_bundle.json (generated dual-channel asset)", () => {
it("contains the full ported skill set", () => {
expect(skills.length).toBeGreaterThanOrEqual(24);
it("is in sync with skills/", () => {
// Regenerate with:
// bun run src/mcp/skills/build_skills_bundle.ts ../../skills
expect([...skills].sort(byName)).toEqual(
[...buildSkills(sourceDir)].sort(byName),
);
});

it("is not empty", () => {
expect(skills.length).toBeGreaterThan(0);
});

it("every skill has a nonempty name, description, and body", () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/skills/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated by scripts/copy-skills.ts at pack time; the source of truth is the
# repo's top-level skills/ directory. Shipped anyway: package.json "files"
# overrides .gitignore.
skills/
dist/
4 changes: 4 additions & 0 deletions packages/skills/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 80,
"tabWidth": 3
}
21 changes: 21 additions & 0 deletions packages/skills/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) The Linux Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading