Skip to content
Draft
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
39 changes: 33 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ name: Publish to npm
# version, so a typo in either place is caught before anything reaches npm.
on:
push:
tags: ['v[0-9]*']
tags: ["v[0-9]*"]

jobs:
publish:
name: Publish on tag
runs-on: ubuntu-latest
permissions:
contents: write # required to create the GitHub Release
id-token: write # required for npm provenance via GitHub OIDC
contents: write # required to create the GitHub Release
id-token: write # required for npm provenance via GitHub OIDC
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v5
with:
# Must match package.json#engines.node (>= 22.19.0); pi 0.75+ requires it.
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: "npm"

- name: Verify tag matches package.json version
run: |
Expand Down Expand Up @@ -94,3 +94,30 @@ jobs:
--generate-notes \
--latest \
--verify-tag

publish-pi-package:
name: Publish pi package
runs-on: ubuntu-latest
needs: publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v5
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: "npm"

- run: npm ci

- name: Build pi package
run: node scripts/build-pi-package.mjs

- name: Publish pi package to npm
run: npm publish --provenance --access public
working-directory: ./dist/pi-package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ __pycache__/
*.log
debug_payload.json
brainstorm_outputs/
dist/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"test": "vitest run",
"test:py": "python3 -m pytest benchmarks/test_rpc_client.py -q",
"typecheck": "tsc --noEmit",
"postinstall": "node scripts/patch-pi.mjs"
"postinstall": "node scripts/patch-pi.mjs",
"build:pi-package": "node scripts/build-pi-package.mjs"
},
"dependencies": {
"@earendil-works/pi-coding-agent": "^0.79.4",
Expand Down
211 changes: 211 additions & 0 deletions scripts/build-pi-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#!/usr/bin/env node
/**
* Build a pi package from the little-coder source tree.
* This creates a package that can be published as `little-coder-pi` (or similar)
* and installed via `npm install little-coder-pi` to get the extensions/skills.
*/
import {
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const root = resolve(here, "..");
let pkg;
try {
pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
} catch (e) {
console.error("Failed to read package.json:", e);
process.exit(1);
}
const version = pkg.version;

// Extensions to include in the pi package (subset of all little-coder extensions)
// These are the ones most useful for vanilla pi users
const PI_PACKAGE_EXTENSIONS = [
"_shared",
"extra-tools",
"output-parser",
"permission-gate",
"prompt-history",
"quality-monitor",
"read-guard",
"read-guard-edit",
"skill-inject",
"thinking-budget",
"write-guard",
"checkpoint",
"evidence",
"evidence-compact",
"knowledge-inject",
"tool-gating",
"browser",
"browser-extract-retention",
"shell-session",
"subagent",
"plan-mode",
"finalize-warn",
];

// Extensions to register as pi extensions (excludes _shared which is a utility module)
const PI_PACKAGE_PI_EXTENSIONS = PI_PACKAGE_EXTENSIONS.filter(
(e) => e !== "_shared",
);

// Skills to include (copied for extensions to load internally)
// These are NOT standalone pi skills - they're loaded by their extensions
// tools/* -> skill-inject extension (type: tool-guidance)
// knowledge/* -> knowledge-inject extension (type: domain-knowledge)
// protocols/* -> protocols extension (type: workflow)
const PI_PACKAGE_SKILL_DIRS = ["tools", "knowledge", "protocols"];

const outDir = join(root, "dist", "pi-package");
const extSrcDir = join(root, ".pi", "extensions");
const skillsSrcDir = join(root, "skills");

function pruneTests(dir) {
if (!existsSync(dir)) return;
for (const name of readdirSync(dir)) {
const full = join(dir, name);
if (statSync(full).isDirectory()) {
pruneTests(full);
continue;
}
if (name.endsWith(".test.ts")) rmSync(full, { force: true });
}
}

// Clean and create output directory
rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });
mkdirSync(join(outDir, "extensions"), { recursive: true });
mkdirSync(join(outDir, "skills"), { recursive: true });

// Copy extensions
for (const ext of PI_PACKAGE_EXTENSIONS) {
const src = join(extSrcDir, ext);
const dest = join(outDir, "extensions", ext);
if (!existsSync(src)) {
console.warn(`Warning: Extension not found: ${ext}`);
continue;
}
cpSync(src, dest, { recursive: true });
console.log(`Copied extension: ${ext}`);
// Prune test files after copy
pruneTests(dest);
}

// Copy skills
for (const skillDir of PI_PACKAGE_SKILL_DIRS) {
const src = join(skillsSrcDir, skillDir);
const dest = join(outDir, "skills", skillDir);
if (!existsSync(src)) {
console.warn(`Warning: Skills directory not found: ${skillDir}`);
continue;
}
cpSync(src, dest, { recursive: true });
console.log(`Copied skills: ${skillDir}`);
}

// Create package.json for the pi package
const piPackageJson = {
name: "little-coder-pi",
version,
description:
"Pi package providing little-coder extensions and skills for vanilla pi",
type: "module",
license: "Apache-2.0",
repository: {
type: "git",
url: "git+https://github.com/itayinbarr/little-coder.git",
},
keywords: [
"pi-package",
"pi",
"little-coder",
"extensions",
"skills",
"coding-agent",
],
files: ["extensions/", "skills/", "UPSTREAM.json", "README.md"],
peerDependencies: {
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*",
},
pi: {
extensions: PI_PACKAGE_PI_EXTENSIONS.map((e) => `./extensions/${e}`),
skills: [],
},
devDependencies: {
"@earendil-works/pi-ai": "^0.80.0",
"@earendil-works/pi-coding-agent": "^0.80.0",
"@earendil-works/pi-tui": "^0.80.0",
},
};

writeFileSync(
join(outDir, "package.json"),
JSON.stringify(piPackageJson, null, 2) + "\n",
);

// Create UPSTREAM.json with provenance
writeFileSync(
join(outDir, "UPSTREAM.json"),
JSON.stringify(
{
source: pkg.repository.url,
version,
builtAt: new Date().toISOString(),
included: {
extensions: PI_PACKAGE_EXTENSIONS,
skills: PI_PACKAGE_SKILL_DIRS,
},
},
null,
2,
) + "\n",
);

// Create README.md
const readme = `# little-coder-pi

Pi package providing [little-coder](https://github.com/itayinbarr/little-coder) extensions and skills for vanilla pi.

## Install

\`\`\`bash
npm install little-coder-pi
\`\`\`

This will auto-load all included extensions and skills via pi's package system.

## What's Included

### Extensions (${PI_PACKAGE_EXTENSIONS.length})
${PI_PACKAGE_EXTENSIONS.map((e) => `- ${e}`).join("\n")}

### Skills (${PI_PACKAGE_SKILL_DIRS.length})
${PI_PACKAGE_SKILL_DIRS.map((s) => `- ${s}`).join("\n")}

## Upstream

Built from little-coder v${version}. See [UPSTREAM.json](./UPSTREAM.json) for details.

## License

Apache-2.0 — see little-coder [LICENSE](https://github.com/itayinbarr/little-coder/blob/main/LICENSE).
`;

writeFileSync(join(outDir, "README.md"), readme);

console.log(`\nPi package built at: ${outDir}`);
console.log(`Run 'npm publish' from that directory to publish.`);