@papi-ai/skills 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/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @papi-ai/skills
2
+
3
+ The canonical, versioned bundle of PAPI's shareable Claude Code skills. Projects
4
+ **install** this package instead of copying skill files, so they pick up fixes by
5
+ bumping a pinned version rather than silently drifting from forked copies.
6
+
7
+ ## What's in the bundle
8
+
9
+ Nine ship-ready skills (the PAPI-internal skills — `bootstrap-project`,
10
+ `friction-promote`, `patch-notes`, `support-debug` — are intentionally **not** shipped):
11
+
12
+ **Generic engineering (eager):** `check-mcp`, `pr-reviewer`,
13
+ `deployment-completeness-audit`, `playwright-skill`
14
+
15
+ **Cycle methodology (lazy / trigger-loaded):** `papi-plan`, `papi-build`,
16
+ `papi-idea`, `papi-strategy`, `papi-advanced`
17
+
18
+ The authoritative inventory + checksums live in [`manifest.json`](./manifest.json),
19
+ regenerated from the skill content on `prepack`.
20
+
21
+ ## Install into a project
22
+
23
+ ```bash
24
+ npx @papi-ai/skills install /path/to/project # symlink (copy fallback)
25
+ npx @papi-ai/skills install /path/to/project --copy # force copy, no symlinks
26
+ npx @papi-ai/skills install /path/to/project --force # overwrite a diverged skill
27
+ ```
28
+
29
+ Each packaged skill is symlinked into `<project>/.claude/skills/<name>`. Where
30
+ symlinks are unsupported (some Windows / restricted filesystems) it copies instead.
31
+
32
+ ## Pinned versioning
33
+
34
+ Published as `@papi-ai/skills@0.1.0`. A project pinned to `0.1.0` keeps that exact
35
+ content when `0.2.0` ships — upgrades are explicit (`npm i @papi-ai/skills@latest`
36
+ then re-run the installer), never silent.
37
+
38
+ ## Local overrides
39
+
40
+ A skill placed in `<project>/.claude/skills.local/<name>` is an intentional
41
+ override: the installer never touches it and PAPI's stale-fork detection never
42
+ flags it. Use this to customise a skill without fighting the package.
43
+
44
+ ## Stale-fork detection
45
+
46
+ `orient` (with `deep_housekeeping: true`) compares the project's `.claude/skills/`
47
+ against this manifest and surfaces any skill whose content has diverged from the
48
+ pinned registry, offering replacement. It prompts — it never auto-overwrites.
49
+
50
+ ```js
51
+ import { detectStaleForks } from '@papi-ai/skills/manifest';
52
+ const forks = detectStaleForks('/path/to/project'); // [{ name, packagedChecksum, localChecksum }]
53
+ ```
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ // @papi-ai/skills installer.
3
+ //
4
+ // Installs the pinned skill bundle into a project's .claude/skills/ by
5
+ // symlinking each packaged skill (with a copy fallback where symlinks are
6
+ // unsupported, e.g. some Windows / restricted filesystems).
7
+ //
8
+ // Precedence: a skill present in <project>/.claude/skills.local/ is an
9
+ // intentional override and is never touched. An already-installed skill whose
10
+ // content has diverged is NOT clobbered unless --force is passed.
11
+ //
12
+ // Usage:
13
+ // papi-skills install <project-dir> [--copy] [--force] [--quiet]
14
+ // node bin/install.mjs <project-dir> [--copy] [--force] [--quiet]
15
+
16
+ import {
17
+ existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync, realpathSync,
18
+ } from 'node:fs';
19
+ import { join, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { loadManifest, listPackagedSkills, checksumSkillDir, PACKAGE_ROOT } from '../lib/manifest.mjs';
22
+
23
+ function main(argv) {
24
+ const args = argv.slice(2);
25
+ if (args[0] === 'install') args.shift(); // tolerate the verb form
26
+ const flags = new Set(args.filter((a) => a.startsWith('--')));
27
+ const positional = args.filter((a) => !a.startsWith('--'));
28
+ const projectDir = resolve(positional[0] ?? process.cwd());
29
+ const forceCopy = flags.has('--copy');
30
+ const force = flags.has('--force');
31
+ const quiet = flags.has('--quiet');
32
+
33
+ const manifest = loadManifest();
34
+ const skillsDir = join(projectDir, '.claude', 'skills');
35
+ const overrideDir = join(projectDir, '.claude', 'skills.local');
36
+ mkdirSync(skillsDir, { recursive: true });
37
+
38
+ const log = quiet ? () => {} : (m) => console.log(m);
39
+ const summary = { linked: 0, copied: 0, upToDate: 0, overridden: 0, diverged: 0 };
40
+
41
+ for (const name of listPackagedSkills()) {
42
+ const src = join(PACKAGE_ROOT, 'skills', name);
43
+ const dest = join(skillsDir, name);
44
+
45
+ if (existsSync(join(overrideDir, name))) {
46
+ summary.overridden++;
47
+ log(` ↪ ${name} — skipped (local override in .claude/skills.local/)`);
48
+ continue;
49
+ }
50
+
51
+ if (existsSync(dest)) {
52
+ const st = lstatSync(dest);
53
+ if (st.isSymbolicLink()) {
54
+ // Already a managed symlink — refresh if it points elsewhere.
55
+ try {
56
+ if (realpathSync(dest) === realpathSync(src)) { summary.upToDate++; log(` = ${name} — up to date (linked)`); continue; }
57
+ } catch { /* dangling link — fall through to re-link */ }
58
+ } else if (st.isDirectory()) {
59
+ const matches = checksumSkillDir(dest) === skillEntry(manifest, name)?.checksum;
60
+ if (matches) { summary.upToDate++; log(` = ${name} — up to date`); continue; }
61
+ if (!force) {
62
+ summary.diverged++;
63
+ log(` ! ${name} — diverged from registry; not overwritten. Move to .claude/skills.local/ to keep, or re-run with --force.`);
64
+ continue;
65
+ }
66
+ }
67
+ rmSync(dest, { recursive: true, force: true });
68
+ }
69
+
70
+ if (forceCopy) {
71
+ cpSync(src, dest, { recursive: true });
72
+ summary.copied++;
73
+ log(` + ${name} — copied`);
74
+ continue;
75
+ }
76
+
77
+ try {
78
+ symlinkSync(src, dest, 'dir');
79
+ summary.linked++;
80
+ log(` → ${name} — linked`);
81
+ } catch {
82
+ // Symlinks unsupported on this platform/filesystem — copy instead.
83
+ cpSync(src, dest, { recursive: true });
84
+ summary.copied++;
85
+ log(` + ${name} — copied (symlink unsupported)`);
86
+ }
87
+ }
88
+
89
+ log(
90
+ `\n@papi-ai/skills@${manifest.packageVersion} → ${skillsDir}\n` +
91
+ ` linked ${summary.linked} · copied ${summary.copied} · up-to-date ${summary.upToDate} · ` +
92
+ `overridden ${summary.overridden} · diverged ${summary.diverged}`,
93
+ );
94
+ if (summary.diverged > 0 && !force) process.exitCode = 0; // informational, not a failure
95
+ }
96
+
97
+ function skillEntry(manifest, name) {
98
+ return manifest.skills.find((s) => s.name === name);
99
+ }
100
+
101
+ // Run only when invoked directly, not when imported.
102
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
103
+ main(process.argv);
104
+ }
@@ -0,0 +1,33 @@
1
+ // Hand-written types for the ESM helpers in manifest.mjs.
2
+ // Lets TypeScript consumers (the MCP server's orient path) import with types.
3
+
4
+ export const PACKAGE_ROOT: string;
5
+ export const MANIFEST_SCHEMA_VERSION: number;
6
+
7
+ export interface SkillManifestEntry {
8
+ /** Skill directory name, e.g. "pr-reviewer". */
9
+ name: string;
10
+ /** Where the skill is loaded from in a project. */
11
+ kind: 'eager' | 'lazy';
12
+ /** Short, stable content checksum of the packaged skill directory. */
13
+ checksum: string;
14
+ }
15
+
16
+ export interface SkillsManifest {
17
+ schemaVersion: number;
18
+ /** Pinned package version, e.g. "0.1.0". */
19
+ packageVersion: string;
20
+ generatedAt: string;
21
+ skills: SkillManifestEntry[];
22
+ }
23
+
24
+ export interface StaleFork {
25
+ name: string;
26
+ packagedChecksum: string;
27
+ localChecksum: string;
28
+ }
29
+
30
+ export function checksumSkillDir(skillDir: string): string;
31
+ export function loadManifest(packageRoot?: string): SkillsManifest;
32
+ export function listPackagedSkills(packageRoot?: string): string[];
33
+ export function detectStaleForks(projectDir: string, packageRoot?: string): StaleFork[];
@@ -0,0 +1,102 @@
1
+ // @papi-ai/skills — shared manifest helpers.
2
+ //
3
+ // Single source of truth for: the manifest schema, version comparison, and
4
+ // stale-fork detection. Both consumers import from here so the schema is
5
+ // defined exactly once (bin/install.mjs and the server's orient path).
6
+
7
+ import { createHash } from 'node:crypto';
8
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ /** Package root = parent of this lib/ directory. */
14
+ export const PACKAGE_ROOT = dirname(HERE);
15
+
16
+ export const MANIFEST_SCHEMA_VERSION = 1;
17
+
18
+ /**
19
+ * Compute a stable content checksum for a single skill directory.
20
+ * Hashes every file's relative path + bytes in sorted order so the result is
21
+ * deterministic across machines and independent of filesystem ordering.
22
+ */
23
+ export function checksumSkillDir(skillDir) {
24
+ const hash = createHash('sha256');
25
+ const files = [];
26
+ walk(skillDir, skillDir, files);
27
+ files.sort();
28
+ for (const rel of files) {
29
+ hash.update(rel);
30
+ hash.update('\0');
31
+ hash.update(readFileSync(join(skillDir, rel)));
32
+ hash.update('\0');
33
+ }
34
+ return hash.digest('hex').slice(0, 16);
35
+ }
36
+
37
+ function walk(root, dir, out) {
38
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39
+ // Never let machine-local cruft influence the checksum.
40
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
41
+ if (entry.name.startsWith('.temp-execution-')) continue;
42
+ if (entry.name === 'package-lock.json') continue;
43
+ const abs = join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ walk(root, abs, out);
46
+ } else if (entry.isFile()) {
47
+ out.push(abs.slice(root.length + 1));
48
+ }
49
+ }
50
+ }
51
+
52
+ /** Load and parse the package manifest. Throws if missing/malformed. */
53
+ export function loadManifest(packageRoot = PACKAGE_ROOT) {
54
+ const raw = readFileSync(join(packageRoot, 'manifest.json'), 'utf8');
55
+ const manifest = JSON.parse(raw);
56
+ if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
57
+ throw new Error(
58
+ `@papi-ai/skills manifest schemaVersion ${manifest.schemaVersion} != expected ${MANIFEST_SCHEMA_VERSION}`,
59
+ );
60
+ }
61
+ return manifest;
62
+ }
63
+
64
+ /** List the skill names that ship in the package. */
65
+ export function listPackagedSkills(packageRoot = PACKAGE_ROOT) {
66
+ return loadManifest(packageRoot).skills.map((s) => s.name);
67
+ }
68
+
69
+ /**
70
+ * Detect skill forks in a project that have drifted from the packaged registry.
71
+ *
72
+ * For each packaged skill, if the project has a same-named skill under
73
+ * .claude/skills/ whose content checksum differs from the manifest, it is
74
+ * reported as a stale fork. Skills under .claude/skills.local/ are intentional
75
+ * overrides and are never reported.
76
+ *
77
+ * Fail-soft: returns [] on any filesystem error so callers (e.g. orient) never
78
+ * break on a missing or partial project layout.
79
+ *
80
+ * @returns {Array<{ name: string, packagedChecksum: string, localChecksum: string }>}
81
+ */
82
+ export function detectStaleForks(projectDir, packageRoot = PACKAGE_ROOT) {
83
+ try {
84
+ const manifest = loadManifest(packageRoot);
85
+ const skillsDir = join(projectDir, '.claude', 'skills');
86
+ const localOverrideDir = join(projectDir, '.claude', 'skills.local');
87
+ const stale = [];
88
+ for (const skill of manifest.skills) {
89
+ const localPath = join(skillsDir, skill.name);
90
+ if (!existsSync(localPath) || !statSync(localPath).isDirectory()) continue;
91
+ // Intentional override — never flag.
92
+ if (existsSync(join(localOverrideDir, skill.name))) continue;
93
+ const localChecksum = checksumSkillDir(localPath);
94
+ if (localChecksum !== skill.checksum) {
95
+ stale.push({ name: skill.name, packagedChecksum: skill.checksum, localChecksum });
96
+ }
97
+ }
98
+ return stale;
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
package/manifest.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "packageVersion": "0.1.0",
4
+ "generatedAt": "2026-05-25T12:07:27.744Z",
5
+ "skills": [
6
+ {
7
+ "name": "check-mcp",
8
+ "kind": "eager",
9
+ "checksum": "e5fe53aca9b35e1e"
10
+ },
11
+ {
12
+ "name": "deployment-completeness-audit",
13
+ "kind": "eager",
14
+ "checksum": "3740d59f520cbe24"
15
+ },
16
+ {
17
+ "name": "papi-advanced",
18
+ "kind": "lazy",
19
+ "checksum": "81cc81ee3f702979"
20
+ },
21
+ {
22
+ "name": "papi-build",
23
+ "kind": "lazy",
24
+ "checksum": "bb0b7107ab8c6b08"
25
+ },
26
+ {
27
+ "name": "papi-idea",
28
+ "kind": "lazy",
29
+ "checksum": "ae5da6393e14fcd5"
30
+ },
31
+ {
32
+ "name": "papi-plan",
33
+ "kind": "lazy",
34
+ "checksum": "6b9a42f263fa409e"
35
+ },
36
+ {
37
+ "name": "papi-strategy",
38
+ "kind": "lazy",
39
+ "checksum": "5b2c55da2b0f4545"
40
+ },
41
+ {
42
+ "name": "playwright-skill",
43
+ "kind": "eager",
44
+ "checksum": "1e8dcd223b968a85"
45
+ },
46
+ {
47
+ "name": "pr-reviewer",
48
+ "kind": "eager",
49
+ "checksum": "836d46ee9fc4b994"
50
+ }
51
+ ]
52
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@papi-ai/skills",
3
+ "version": "0.1.0",
4
+ "description": "PAPI's shareable Claude Code skill bundle — single source of truth, installed (not copied) into projects with pinned versioning",
5
+ "license": "Elastic-2.0",
6
+ "type": "module",
7
+ "exports": {
8
+ "./manifest": {
9
+ "types": "./lib/manifest.d.ts",
10
+ "default": "./lib/manifest.mjs"
11
+ },
12
+ "./manifest.json": "./manifest.json"
13
+ },
14
+ "bin": {
15
+ "papi-skills": "./bin/install.mjs"
16
+ },
17
+ "files": [
18
+ "skills",
19
+ "lib/manifest.mjs",
20
+ "lib/manifest.d.ts",
21
+ "bin/install.mjs",
22
+ "manifest.json",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build:manifest": "node scripts/build-manifest.mjs",
27
+ "prepack": "node scripts/build-manifest.mjs",
28
+ "test": "vitest run"
29
+ },
30
+ "keywords": [
31
+ "papi",
32
+ "claude-code",
33
+ "skills",
34
+ "ai-agents"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/cathalos92/papi-ui.git",
39
+ "directory": "packages/skills"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "vitest": "^4.0.18"
46
+ }
47
+ }
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: check-mcp
3
+ description: Verify the PAPI MCP server is running and healthy. Use when the user says "check mcp", "is papi running", "mcp status", or at the start of any session that needs PAPI tools. Also use proactively if PAPI MCP tools are expected but not appearing in the available tools list.
4
+ ---
5
+
6
+ # Check MCP Server Health
7
+
8
+ Run these steps in order and report the result clearly.
9
+
10
+ ## Step 1: Read config
11
+ Read `.mcp.json` from the project root. Extract the server command, args, and env vars.
12
+
13
+ If `.mcp.json` doesn't exist, stop and report: "No MCP server configured. Create .mcp.json to connect PAPI."
14
+
15
+ ## Step 2: Check build artifact
16
+ Verify the file referenced in `args` exists on disk (e.g., `ls <path>`).
17
+
18
+ If missing, report: "Server build artifact not found at <path>. Rebuild with: `npm run build --workspace=packages/adapter-md && npm run build --workspace=packages/server`"
19
+
20
+ ## Step 3: Test-spawn the server
21
+ Run the server briefly with the configured env vars to check for startup crashes:
22
+
23
+ ```bash
24
+ cd <parent-dir-of-server> && <ENV_VARS> node <path-to-index.js> 2>&1 &; sleep 2; kill %1 2>/dev/null; wait 2>/dev/null
25
+ ```
26
+
27
+ - If it produces an error (like "Project root required"), report the exact error and suggest the fix.
28
+ - If no output (clean start), the server binary is healthy.
29
+
30
+ ## Step 4: Check Claude Code connection
31
+ Look at the available tools list. If PAPI MCP tools (like `mcp__papi__*`) are present, the server is connected. If not, tell the user:
32
+
33
+ "PAPI MCP server binary is healthy but not connected to Claude Code. Reload the VS Code window (Cmd+Shift+P -> Developer: Reload Window) to reconnect."
34
+
35
+ ## Step 5: Report
36
+ Summarize with one of:
37
+ - **Healthy**: Server configured, binary starts clean, tools available
38
+ - **Healthy but disconnected**: Binary works, but Claude Code needs a window reload
39
+ - **Misconfigured**: Explain what's wrong (wrong env var name, missing artifact, crash on start)
40
+ - **Not configured**: No .mcp.json found