Project guidelines for AI agents working in this codebase.
A proxy that bridges OpenCode (Anthropic API format) to Claude Max (Agent SDK). See ARCHITECTURE.md for the full module map and dependency rules.
npm test # Run all tests (bun test)
npm run build # Build with tsup
npm start # Start the proxy server- Do not add code to
server.tsthat belongs in a leaf module. If it's pure logic (no HTTP, no Hono), extract it. session/lineage.tsmust stay pure. No side effects, no I/O, no imports from cache or server.- Leaf modules (
errors.ts,models.ts,tools.ts,messages.ts) must not import fromserver.tsorsession/. Dependencies flow downward only. - No circular dependencies.
OpenCode-specific behavior is documented in ARCHITECTURE.md under "Agent-Specific Logic". When modifying these areas:
- Add a
NOTE:comment marking the code as agent-specific - Do not spread agent-specific logic into new modules
- Future work will use an adapter pattern — see
DEFERRED.md
- Every extracted module must have unit tests
- Pure functions get direct unit tests (no mocks)
- Integration tests go through the HTTP layer with mocked SDK
- All tests must pass before any change is considered complete
- New test files go in
src/__tests__/ - E2E tests are documented in
E2E.md— run manually before releases or after major refactors (requires Claude Max subscription)
- No
as any,@ts-ignore, or@ts-expect-error - No empty catch blocks
- Match existing patterns — check neighboring code before writing
- Keep
server.tsas thin as possible — it should orchestrate, not compute
server.ts → HTTP routes, SSE streaming, concurrency (orchestration only)
adapter.ts → AgentAdapter interface (extensibility point)
adapters/
opencode.ts → OpenCode-specific: headers, CWD, tool config
forgecode.ts → ForgeCode-specific: XML CWD, patch/shell tools, passthrough
query.ts → buildQueryOptions (shared stream/non-stream SDK call builder)
errors.ts → classifyError (pure)
models.ts → mapModelToClaudeModel, resolveClaudeExecutableAsync
tools.ts → BLOCKED_BUILTIN_TOOLS, CLAUDE_CODE_ONLY_TOOLS, MCP_SERVER_NAME
messages.ts → normalizeContent, getLastUserMessage (pure)
fileChanges.ts → PostToolUse hook: file write/edit tracking + summary formatting (pure)
session/
lineage.ts → Hashing, lineage verification (PURE — no I/O)
fingerprint.ts → extractClientCwd, getConversationFingerprint
cache.ts → LRU caches, lookupSession, storeSession (stateful)
External plugins depend on these interfaces. Do not change without project owner approval.
| Interface | Location | Used by |
|---|---|---|
startProxyServer(config) → ProxyInstance |
server.ts |
Plugins that spawn proxy instances |
ProxyInstance.close() |
types.ts |
Plugins for graceful shutdown |
ProxyConfig type |
types.ts |
Plugin configuration |
x-opencode-session header |
adapters/opencode.ts |
Session tracking from agent plugins |
x-meridian-profile header |
server.ts, profiles.ts |
Per-request profile selection |
GET /health response shape |
server.ts |
Plugin health checks |
POST /v1/messages request/response format |
server.ts |
All agents (Anthropic API contract) |
GET /profiles/list response shape |
server.ts |
Profile management UI and CLI |
POST /profiles/active request/response |
server.ts |
Profile switching from CLI and UI |
If you need to modify any of these, open an issue first — breaking changes affect downstream plugin authors.
- Format:
type: brief description - Types: feat, fix, refactor, perf, test, docs, chore
- No AI attribution lines
All changes go through this process, no exceptions:
-
Create a feature branch from
main:git checkout -b feat/my-feature main
-
Make changes, commit, push the branch:
git add -A && git commit -m "feat: my feature" git push origin feat/my-feature
-
Create a PR targeting
main:gh pr create --title "feat: my feature" --base main -
Wait for CI — the
testjob must pass before merging:gh pr checks <PR_NUMBER>
-
Merge the PR (squash merge preferred):
gh pr merge <PR_NUMBER> --squash --delete-branch
-
Never run
git push origin maindirectly — all code reachesmainthrough merged PRs only.
Do NOT run npm version, git push --tags, or npm publish manually.
Releases are handled automatically by Release Please:
- Merge PRs to
mainusing Conventional Commits (feat:,fix:, etc.) - Release Please auto-creates/updates a release PR that batches all unreleased changes
- Review the release PR — the changelog should only show changes since the last release
- When ready to ship, merge the release PR:
gh pr merge <RELEASE_PR_NUMBER> --merge
- Merging the release PR automatically:
- Bumps
package.jsonandCHANGELOG.md - Creates a git tag (
meridian-v*) and GitHub Release - Runs tests, builds, and publishes to npm with provenance
- Bumps
Multiple PRs get batched into a single release. Never publish manually.
- Changelog shows entire history? — Release Please can't find the previous release tag. Check that
meridian-v<version>tags exist for recent releases:git tag -l 'meridian-v*' | tail -5 - Release PR not updating? — It only updates on
pushtomain. If you closed it, push any commit to main to regenerate. - Publish failed with E403? — The version was already published. This is safe to ignore; the release is already on npm.
publish_onlyworkflow dispatch — Emergency escape hatch to publish the current version without Release Please. Only use when the normal flow is broken.
.release-please-manifest.json— tracks the current released version. Release Please updates this automatically when a release PR is merged. Do not edit manually unless resetting the version anchor.release-please-config.json— defines the release type (node), component name, and changelog section mapping..github/workflows/release-please.yml— the workflow that runs on every push tomain. It creates/updates the release PR and publishes to npm when merged.