Open Changelog
Reusable changelog collection, generation, and publishing for Hasna-coded apps.
Open Changelog stores entries locally as JSONL, exposes the same model through an SDK, HTTP API, CLI, and MCP tools, and generates Keep a Changelog style Markdown. Publishing to a target CHANGELOG.md is dry-run by default and writes only when explicitly requested.
Install
bun add @hasna/changelog
CLI binaries:
changelog --help
changelog-mcp --help
changelog-serve --help
Local data defaults to ~/.hasna/changelog. Override it with CHANGELOG_DATA_DIR.
CLI
changelog init
changelog add "Initial changelog scaffold" --kind added --task 731aace9
changelog list --app open-changelog
changelog show <entry-id>
changelog update <entry-id> --kind fixed --message "Tighten publish safety"
changelog release --version 0.1.0
changelog generate --app open-changelog --version 0.1.0 --kind added --title "Open Changelog Notes"
changelog export --app open-changelog --format jsonl
changelog publish --app open-changelog --dry-run --diff
changelog publish --app open-changelog --write --target CHANGELOG.md --title "Open Changelog Notes"
publish prints a Markdown preview by default. It writes only with --write.
When writing over an existing file, the previous file is backed up under ~/.hasna/changelog/backups unless --no-backup is supplied.
Remote CLI commands use CHANGELOG_API_TOKEN from the environment when --api-url is supplied.
When --app is omitted for add, generate, release, or publish, the CLI infers an app id from the local package.json name.
SDK
import { LocalChangelogStore, generateChangelogMarkdown, publishChangelog } from "@hasna/changelog";
const store = new LocalChangelogStore();
await store.createEntry({
appId: "my-app",
kind: "fixed",
title: "Fix duplicate release notes",
tasks: ["APP-123"],
});
await store.releaseEntries({ appId: "my-app", version: "1.2.0" });
const entries = await store.listEntries({ appId: "my-app", version: "1.2.0" });
const markdown = generateChangelogMarkdown(entries, { appId: "my-app" });
await publishChangelog({ store, appId: "my-app", diff: true }); // dry run
await publishChangelog({ store, appId: "my-app", write: true });
Remote API client:
import { createChangelogClient } from "@hasna/changelog";
const changelog = createChangelogClient({
baseUrl: "http://127.0.0.1:8788",
token: process.env.CHANGELOG_API_TOKEN,
});
await changelog.add({
appId: "my-app",
version: "1.2.0",
kind: "added",
title: "Add export command",
});
HTTP API
Start the server:
CHANGELOG_HOST=127.0.0.1 CHANGELOG_PORT=8788 changelog-serve
If CHANGELOG_API_TOKEN is set, API requests must include Authorization: Bearer <token> or x-changelog-token: <token>.
API publish write mode requires CHANGELOG_API_TOKEN to be configured and supplied. Dry-run publish remains available without a token for local development.
Endpoints:
GET /healthPOST /v1/entriesGET /v1/entries?appId=my-app&version=1.2.0&kind=addedGET /v1/entries/:idPATCH /v1/entries/:idPOST /v1/releaseGET /v1/generate?appId=my-appPOST /v1/generatePOST /v1/publishGET /v1/statsGET /v1/export.jsonl
Example:
curl -X POST http://127.0.0.1:8788/v1/entries \
-H 'content-type: application/json' \
-d '{"appId":"my-app","version":"1.2.0","kind":"fixed","title":"Fix changelog publishing"}'
Publish dry-run:
curl -X POST http://127.0.0.1:8788/v1/publish \
-H 'content-type: application/json' \
-d '{"appId":"my-app","targetPath":"CHANGELOG.md"}'
Write mode:
curl -X POST http://127.0.0.1:8788/v1/publish \
-H "authorization: Bearer $CHANGELOG_API_TOKEN" \
-H 'content-type: application/json' \
-d '{"appId":"my-app","targetPath":"CHANGELOG.md","write":true}'
MCP
Run the MCP server over stdio:
changelog-mcp
Tools:
add_changelog_entrylist_changelog_entriesget_changelog_entryupdate_changelog_entryrelease_changeloggenerate_changelogpublish_changelogchangelog_statsexport_changelog_jsonl
publish_changelog is dry-run unless its write argument is true.
Entry Model
Each stored JSONL entry includes:
appIdversionkindandcategorytitle,message,details, andbodydateauthorandsourcetags,links,commits, andtasksmetadataid,createdAt, andupdatedAt
Text fields and metadata are redacted for obvious secret-shaped values before storage. Duplicate entries are rejected by default using a stable fingerprint derived from app, version, kind, title, tasks, and commits. Pass the explicit duplicate override when a repeated entry is intentional.
Environment
CHANGELOG_DATA_DIR: local JSONL data directoryCHANGELOG_API_TOKEN: optional HTTP API bearer tokenCHANGELOG_HOST: server bind hostCHANGELOG_PORT: server bind port