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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "polar",
"displayName": "Polar",
"version": "0.1.0",
"version": "0.2.0",
"description": "Add Polar payments, subscriptions and usage billing to your app. Connects Claude to your Polar organization through the Polar MCP server and adds skills for setup, integration, testing and migrating from other payment platforms.",
"author": {
"name": "Polar",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "polar",
"displayName": "Polar",
"version": "0.1.0",
"version": "0.2.0",
"description": "Add Polar payments, subscriptions and usage billing to your app. Connects Cursor to your Polar organization through the Polar MCP server and adds skills for setup, integration, testing and migrating from other payment platforms.",
"author": {
"name": "Polar"
Expand Down
37 changes: 37 additions & 0 deletions .github/drift-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Weekly skill drift review

You maintain the Polar agent skills in `skills/`. Coding agents follow them to integrate Polar, so a wrong command, field or claim breaks real integrations. Polar's monorepo is checked out at `../polar`.

Find and fix significant drift between the skills and what Polar ships. Below this prompt are the results of this repository's checks and the monorepo commits merged since the last review.

## What to fix

- Every failing check below.
- Code, commands, flags, fields, event names, environment variables, URLs or dashboard paths that no longer match what Polar ships.
- Claims that contradict Polar's code or docs, such as limits, retry behavior or which events fire.
- A path Polar now recommends instead of the one a skill teaches.

## What to leave alone

- Wording, style, formatting and ordering.
- Polar features the skills don't cover yet.
- Anything not released yet. The skills target the latest published `@polar-sh/sdk` and `@polar-sh/cli` (`npm view <package> version`), so ignore `[Unreleased]` changelog entries. The API, MCP server and docs deploy from `main`.
- Anything you can't confirm in `../polar`. A missed fix is cheaper than a wrong one.

## How to work

1. Read every `skills/*/SKILL.md`.
2. Triage the commits by subject. For each one that could affect a skill, run `git -C ../polar show --stat <sha>`, then `git -C ../polar show <sha> -- <path>` for the files that matter. For SDK and CLI releases, read `../polar/sdk/CHANGELOG.md` and `../polar/clients/packages/cli/CHANGELOG.md`. For SDK usage, the SDK team's reference is `../polar/sdk/typescript/.agents/skills/polar-typescript-sdk/`.
3. Confirm each suspected drift in the current files under `../polar` before you change anything.
4. Edit only `skills/`, `README.md` and `CHANGELOG.md`. Keep each change minimal and match the surrounding style.
5. Run these until they pass, exactly as written, without pipes or `cd`:
- `node .github/scripts/check-plugin.mjs`
- `node .github/scripts/check-samples.mjs`
- `node .github/scripts/check-plugin.mjs --cli <Polar CLI path from "This run">`
- `node .github/scripts/check-plugin.mjs --links`
6. Record the changes as "This run" says: either run `node .github/scripts/bump-version.mjs patch '<change>' ...`, or add the lines to the top section of `CHANGELOG.md`. Write one line per change, for people who use the plugin.
7. Write `DRIFT_SUMMARY.md` in the repository root. It becomes the pull request description:
- One bullet per change: the skill, what was wrong, what it says now, and the evidence. Link evidence as `https://github.com/polarsource/polar/blob/<monorepo commit>/<path>#L<line>`, or name the failing check.
- A short "Checked, no change needed" list of the commits that looked relevant but weren't, one line each.

If nothing needs fixing, change no files and don't write `DRIFT_SUMMARY.md`.
36 changes: 33 additions & 3 deletions .github/scripts/check-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const MAX_TEXT_FILE_BYTES = 256 * 1024;
const PIPE_TO_SHELL = /(curl|wget)\b[^\n]*\|\s*(ba|z)?sh\b|\|\s*iex\b/;
const JUNK_FILES = /(^|\/)(\.DS_Store|Thumbs\.db|desktop\.ini|__MACOSX)(\/|$)/;
const SKILL_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
const MARKDOWN_LINK = /\]\((https?:\/\/[^)\s]+)\)|<(https?:\/\/[^>\s]+)>/g;

function checkNames() {
for (const path of NAMED_FILES) {
Expand Down Expand Up @@ -142,10 +143,13 @@ function codeSnippets(text) {
return snippets;
}

function documents() {
return ["README.md", ...readdirSync("skills").map((directory) => join("skills", directory, "SKILL.md"))];
}

function cliCommands() {
const commands = new Map();
const documents = ["README.md", ...readdirSync("skills").map((directory) => join("skills", directory, "SKILL.md"))];
for (const document of documents) {
for (const document of documents()) {
for (const snippet of codeSnippets(readFileSync(document, "utf8"))) {
for (const [, rest] of snippet.matchAll(/(?:^|[\s($])polar ([^#|;&>)]*)/g)) {
const tokens = rest.trim().split(/\s+/);
Expand Down Expand Up @@ -179,8 +183,34 @@ function checkCli(binary) {
}
}

function linkStatus(url) {
return fetch(url, { signal: AbortSignal.timeout(20_000) }).then(
(response) => (response.ok ? null : `HTTP ${response.status}`),
(error) => error.cause?.code ?? error.message,
);
}

async function checkLinks() {
const links = new Map();
for (const document of documents()) {
for (const [, markdownUrl, autolinkUrl] of readFileSync(document, "utf8").matchAll(MARKDOWN_LINK)) {
const url = (markdownUrl ?? autolinkUrl).split("#")[0];
links.set(url, [...new Set([...(links.get(url) ?? []), document])]);
}
}
await Promise.all(
[...links].map(async ([url, linkedFrom]) => {
const status = (await linkStatus(url)) && (await linkStatus(url));
if (status) fail(`${url} (${linkedFrom.join(", ")}): ${status}`);
else console.log(url);
}),
);
}

const cliFlag = process.argv.indexOf("--cli");
if (cliFlag === -1) {
if (process.argv.includes("--links")) {
await checkLinks();
} else if (cliFlag === -1) {
checkNames();
checkVersions();
checkChangelog();
Expand Down
119 changes: 119 additions & 0 deletions .github/scripts/check-samples.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const SDK_VERSION = process.env.POLAR_SDK_VERSION || "latest";
const DEPENDENCIES = [`@polar-sh/sdk@${SDK_VERSION}`, "typescript@5.9.3", "vitest@3.2.4", "@types/node@24"];
const FENCE_START = /^(\s*)```(ts|typescript)\s*$/;
const FENCE_END = /^\s*```\s*$/;
const USES_SDK = /@polar-sh\/sdk|\bpolar\.|\bevent\./;
const ELIDED = /^\s*\/\/ \.\.\./m;
const CLIENT = /^export const polar\b/m;
const WEBHOOK_CASE = /^case /;
const TSC_ERROR = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
const MISSING_RELATIVE_MODULE = /^Cannot find module '\.\.?\//;
const CLIENT_DECLARATION = `declare const polar: typeof import("./polar").polar;\n`;
const PRELUDE = `declare function getUserId(request: Request): Promise<string | undefined>;
declare function recordMigratedSubscription(ids: { stripeSubscriptionId: string; polarSubscriptionId: string }): Promise<void>;
declare const polarCustomerId: string;
declare const appCustomerId: string;
declare const productId: string;
declare const email: string;
declare const name: string;
`;
const TSCONFIG = {
compilerOptions: {
target: "es2023",
lib: ["es2023"],
module: "preserve",
moduleResolution: "bundler",
moduleDetection: "force",
resolveJsonModule: true,
strict: true,
noEmit: true,
skipLibCheck: true,
types: ["node"],
},
include: ["*.ts"],
};

function codeBlocks(path) {
const blocks = [];
let block = null;
readFileSync(path, "utf8")
.split("\n")
.forEach((line, index) => {
if (!block) {
const start = line.match(FENCE_START);
if (start) block = { path, line: index + 1, indent: start[1].length, lines: [] };
} else if (FENCE_END.test(line)) {
blocks.push({ ...block, code: block.lines.join("\n") });
block = null;
} else {
block.lines.push(line.slice(block.indent));
}
});
return blocks;
}

const documents = readdirSync("skills").map((skill) => join("skills", skill, "SKILL.md"));
const blocks = documents.flatMap(codeBlocks);
const checked = blocks.filter((block) => USES_SDK.test(block.code) && !ELIDED.test(block.code));
const client = checked.find((block) => CLIENT.test(block.code));
const sdkModule = client?.code.match(/"(@polar-sh\/sdk[^"]*)"/)?.[1];
const compiled = checked.filter((block) => sdkModule || !WEBHOOK_CASE.test(block.code));

const directory = mkdtempSync(join(tmpdir(), "polar-samples-"));
const files = new Map();
for (const block of compiled) {
const file = block === client ? "polar.ts" : `${block.path.split("/")[1]}-${block.line}.ts`;
const prefix = WEBHOOK_CASE.test(block.code)
? [`import { webhooks } from "${sdkModule}";`, "export async function handle(event: webhooks.WebhookPayload) {", "switch (event.type) {"]
: [];
const suffix = prefix.length > 0 ? ["}", "}"] : [];
files.set(file, { ...block, offset: prefix.length });
writeFileSync(join(directory, file), [...prefix, block.code, ...suffix, ""].join("\n"));
}
writeFileSync(join(directory, "prelude.d.ts"), (client ? CLIENT_DECLARATION : "") + PRELUDE);
writeFileSync(join(directory, "tsconfig.json"), JSON.stringify(TSCONFIG));

execFileSync("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error", ...DEPENDENCIES], {
cwd: directory,
stdio: "inherit",
});
const sdk = JSON.parse(readFileSync(join(directory, "node_modules/@polar-sh/sdk/package.json"), "utf8"));
console.log(`Type-checking ${compiled.length} of ${blocks.length} TypeScript samples against @polar-sh/sdk ${sdk.version}`);

const tsc = spawnSync(process.execPath, ["node_modules/typescript/bin/tsc", "--pretty", "false"], {
cwd: directory,
encoding: "utf8",
});
const errors = tsc.stderr ? [{ location: "tsc", message: tsc.stderr.trim() }] : [];
if (!client) errors.push({ location: "skills", message: "No TypeScript sample exports the `polar` client that the other samples import" });
for (const output of tsc.stdout.split("\n")) {
if (!output.includes("error TS")) continue;
const match = output.match(TSC_ERROR);
const block = match && files.get(match[1]);
if (!block) {
errors.push({ location: "tsc", message: output });
continue;
}
const [, , line, column, code, message] = match;
if (code === "TS2307" && MISSING_RELATIVE_MODULE.test(message)) continue;
const documentLine = block.line + Number(line) - block.offset;
errors.push({
location: `${block.path}:${documentLine}:${Number(column) + block.indent}`,
message: `${code} ${message}`,
annotation: `file=${block.path},line=${documentLine}`,
});
}

if (errors.length > 0) {
for (const { location, message, annotation } of errors) {
console.error(`✘ ${location} ${message}`);
if (process.env.GITHUB_ACTIONS && annotation) console.log(`::error ${annotation}::${message}`);
}
process.exit(1);
}
console.log("✔ TypeScript samples compile");
Loading
Loading