feat(claude-code): add MCP server and hooks#24
Conversation
📝 WalkthroughWalkthroughThis PR introduces Claude Code plugin support, adding an MCP server ( Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Two high-density modules (509 + 211 lines) introduce substantial logic: MCP tool schemas with 7 groups and multiple operations per group; deduplication logic with hashing; event routing. Heterogeneous changes span build config, package exports, new modules, refactoring, and 300+ test lines. No control-flow surprises, but need to verify tool schema completeness, dedup edge cases, and error handling under SQLite constraint races. Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
✨ Simplify code
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/claude-code-hooks.ts (1)
188-196: Silent swallow of JSON parse errors.If stdin contains malformed JSON,
inputbecomes{}with no warning. For a hooks script called by Claude Code, this is probably fine (bad input = caller bug). But a debug log would help troubleshooting.♻️ Optional: log parse failures
try { const raw = await readStdin(); if (raw.trim().length > 0) { input = JSON.parse(raw); } - } catch { - // stdin may be empty or non-JSON + } catch (err) { + console.error("[obsxa] Warning: stdin parse failed, using empty input"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/claude-code-hooks.ts` around lines 188 - 196, The try/catch around readStdin()/JSON.parse silently swallows parse errors so malformed stdin becomes an empty HookInput with no diagnostics; update the catch in the block that sets input (where HookInput, readStdin and variable input are used) to log the parse failure (include the caught error message and the raw stdin if available) at debug or warn level (e.g., console.debug/console.warn or the module logger) while keeping the existing behavior of leaving input as {}.src/claude-code.ts (2)
21-29: Consider importingRELATION_TYPESfromsrc/types.tsinstead of duplicating.Same constant defined at
src/types.ts:278-286. If the canonical list changes, this copy drifts.♻️ Suggested change
-const RELATION_TYPES = [ - "similar_to", - "contradicts", - "supports", - "derived_from", - "duplicate_of", - "refines", - "same_signal_as", -] as const; +import { RELATION_TYPES } from "./types.ts";You'd need to update the
z.enum()calls to useRELATION_TYPESdirectly (Zod 4 accepts arrays).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/claude-code.ts` around lines 21 - 29, Replace the duplicated RELATION_TYPES constant with an import of the canonical RELATION_TYPES from your central types module and remove the local definition; then update any z.enum(...) usages in this file to pass RELATION_TYPES directly (Zod 4 accepts arrays) instead of the inline literal so the file uses the single source of truth and won't drift from the canonical list.
198-210: JSON parsed, cast to expected type without shape validation.If
recordscontains objects missing required fields (likeprojectId,title), this blows up insideaddManywith a less helpful error. Same pattern at line 222.Not blocking since the store layer will reject bad data anyway, but a schema check here would give clearer errors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/claude-code.ts` around lines 198 - 210, The JSON parse and direct cast in the "import" case can pass malformed objects into obsxa.observation.addMany and produce unclear errors; after parsing and confirming Array.isArray(parsed), validate each item has the required shape (e.g., required fields like projectId and title and any other required properties expected by obsxa.observation.addMany) and return errorResult with a descriptive message listing missing/invalid fields for the first bad item instead of casting blindly; apply the same shape-check logic to the other similar branch that calls obsxa.observation.addMany (the pattern noted around the second occurrence) so callers get clear validation errors before calling addMany.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@claude-code/package.json`:
- Around line 15-30: The package's published type entrypoint index.d.mts
re-exports ../src/claude-code.ts and ../src/claude-code-hooks.ts which aren't
included in the published files, breaking consumers' tsc; either publish the
corresponding .d.mts files for those modules or rewrite index.d.mts to re-export
only the shipped declarations (index.d.mts) or relative declarations that exist
(e.g., create and include ./claude-code.d.mts and ./claude-code-hooks.d.mts),
and update package.json "files" and "exports" entries (index.d.mts, hooks.mjs,
any new .d.mts) so the declared types resolve after publish.
- Around line 21-30: The package currently maps the package root (".") to
index.mjs which runs the MCP server on import while index.d.mts declares named
exports (e.g., startMcpServer), causing side-effects and breaking named imports;
change the package.json "exports" entry for "." to point to a pure re-export
module (not the bootstrap) that exports the named APIs declared in index.d.mts,
move the side-effectful CLI bootstrap into the bin entry only (leave the
server-starting code in a separate bootstrap file, e.g., bin/bootstrap.mjs), and
ensure the new root module (e.g., ./index.mjs or ./api.mjs) only re-exports
startMcpServer and other named symbols to match index.d.mts so imports like
import { startMcpServer } from "obsxa-claude-code" work without side effects.
In `@src/claude-code.ts`:
- Line 4: The current import uses a named import for Zod which is incorrect;
replace the named import statement that references z with a namespace import
(e.g., change the import line that currently references z to use "import * as z
from 'zod/v4'" or "import * as z from 'zod'") so downstream uses of z (the zod
schema functions/types in this file) resolve correctly.
---
Nitpick comments:
In `@src/claude-code-hooks.ts`:
- Around line 188-196: The try/catch around readStdin()/JSON.parse silently
swallows parse errors so malformed stdin becomes an empty HookInput with no
diagnostics; update the catch in the block that sets input (where HookInput,
readStdin and variable input are used) to log the parse failure (include the
caught error message and the raw stdin if available) at debug or warn level
(e.g., console.debug/console.warn or the module logger) while keeping the
existing behavior of leaving input as {}.
In `@src/claude-code.ts`:
- Around line 21-29: Replace the duplicated RELATION_TYPES constant with an
import of the canonical RELATION_TYPES from your central types module and remove
the local definition; then update any z.enum(...) usages in this file to pass
RELATION_TYPES directly (Zod 4 accepts arrays) instead of the inline literal so
the file uses the single source of truth and won't drift from the canonical
list.
- Around line 198-210: The JSON parse and direct cast in the "import" case can
pass malformed objects into obsxa.observation.addMany and produce unclear
errors; after parsing and confirming Array.isArray(parsed), validate each item
has the required shape (e.g., required fields like projectId and title and any
other required properties expected by obsxa.observation.addMany) and return
errorResult with a descriptive message listing missing/invalid fields for the
first bad item instead of casting blindly; apply the same shape-check logic to
the other similar branch that calls obsxa.observation.addMany (the pattern noted
around the second occurrence) so callers get clear validation errors before
calling addMany.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 188094f2-4ef5-473c-8a3b-9c7dad056c8b
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
build.config.tsclaude-code/hooks.mjsclaude-code/index.d.mtsclaude-code/index.mjsclaude-code/package.jsonpackage.jsonsrc/claude-code-hooks.tssrc/claude-code.tssrc/opencode.tssrc/shared.tstest/claude-code.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,mjs}
📄 CodeRabbit inference engine (AGENTS.md)
Use ESM-only TypeScript with
"type": "module"in package.json; do not introduce CommonJS
Files:
src/opencode.tsclaude-code/hooks.mjssrc/claude-code.tstest/claude-code.test.tssrc/shared.tsbuild.config.tsclaude-code/index.mjssrc/claude-code-hooks.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ts: Keep imports explicit and extension-aware using./file.tsstyle; useimport typefor type-only imports
Never useas any,@ts-ignore, or@ts-expect-error
Files:
src/opencode.tssrc/claude-code.tstest/claude-code.test.tssrc/shared.tsbuild.config.tssrc/claude-code-hooks.ts
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Use temporary database directories created withmkdtempSyncand ensure cleanup in test suite lifecycle hooks
Prefer integration tests over mocks when testing store behavior
Do not share sqlite files across tests; maintain isolation between test cases
Do not weaken coverage of migration, backup, and lifecycle transitions in tests
Do not replace meaningful integration assertions with snapshot-only checks
Keep tests deterministic even when using large fixtures
Files:
test/claude-code.test.ts
🧠 Learnings (13)
📚 Learning: 2026-03-10T19:06:48.549Z
Learnt from: oritwoen
Repo: oritwoen/obsxa PR: 2
File: src/core/observation.ts:375-393
Timestamp: 2026-03-10T19:06:48.549Z
Learning: In the obsxa project (`src/core/observation.ts` and related files), the database driver is `better-sqlite3` — a synchronous, single-connection SQLite driver. There is no concurrency possible, so read-modify-write patterns (e.g., read frequency, increment, write back) are safe and do not lose updates. Concurrency-based race condition comments are not applicable here.
Applied to files:
src/opencode.ts
📚 Learning: 2026-03-12T07:42:55.367Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: src/commands/AGENTS.md:0-0
Timestamp: 2026-03-12T07:42:55.367Z
Learning: Applies to src/commands/*.ts : Do not inline database bootstrap logic per command; keep shared open/close flow in `_db.ts`
Applied to files:
src/opencode.ts
📚 Learning: 2026-03-10T19:06:35.925Z
Learnt from: oritwoen
Repo: oritwoen/obsxa PR: 2
File: src/core/cluster.ts:84-97
Timestamp: 2026-03-10T19:06:35.925Z
Learning: In the obsxa project (`src/core/cluster.ts` and similar files), the database driver is `better-sqlite3`, which is fully synchronous and single-connection. Concurrent insert races (TOCTOU) cannot occur within the same process, so check-then-insert patterns are safe and do not need duplicate-key error handling.
Applied to files:
src/opencode.tssrc/claude-code-hooks.ts
📚 Learning: 2026-03-11T19:03:11.241Z
Learnt from: oritwoen
Repo: oritwoen/obsxa PR: 9
File: package.json:58-58
Timestamp: 2026-03-11T19:03:11.241Z
Learning: In `package.json` of `oritwoen/obsxa`, `opencode-ai/plugin` is intentionally pinned to `"latest"` in devDependencies to track plugin API movement. Do not flag this as a non-reproducible build issue.
Applied to files:
src/opencode.tsclaude-code/package.jsonpackage.json
📚 Learning: 2026-03-10T19:06:28.832Z
Learnt from: oritwoen
Repo: oritwoen/obsxa PR: 2
File: src/core/db.ts:81-93
Timestamp: 2026-03-10T19:06:28.832Z
Learning: In the `obsxa` repository (TypeScript, Node.js, better-sqlite3), read-then-insert patterns (e.g., in `src/core/dedup.ts` scan() and `src/core/cluster.ts`) are intentionally safe without DB-level unique constraints because `better-sqlite3` is fully synchronous — it blocks the event loop on every call, making concurrent interleaving within the same process impossible. Do not flag these as race conditions.
Applied to files:
src/opencode.ts
📚 Learning: 2026-03-12T07:43:10.730Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-12T07:43:10.730Z
Learning: Applies to test/**/*.test.ts : Do not share sqlite files across tests; maintain isolation between test cases
Applied to files:
src/opencode.tssrc/shared.ts
📚 Learning: 2026-03-12T07:43:10.730Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-12T07:43:10.730Z
Learning: Applies to test/**/*.test.ts : Do not weaken coverage of migration, backup, and lifecycle transitions in tests
Applied to files:
test/claude-code.test.ts
📚 Learning: 2026-03-12T07:43:10.730Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-12T07:43:10.730Z
Learning: Applies to test/**/*.test.ts : Do not replace meaningful integration assertions with snapshot-only checks
Applied to files:
test/claude-code.test.ts
📚 Learning: 2026-03-12T07:43:10.730Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-03-12T07:43:10.730Z
Learning: Applies to test/**/*.test.ts : Use temporary database directories created with `mkdtempSync` and ensure cleanup in test suite lifecycle hooks
Applied to files:
test/claude-code.test.ts
📚 Learning: 2026-03-12T13:19:11.251Z
Learnt from: oritwoen
Repo: oritwoen/obsxa PR: 19
File: scripts/release-with-opencode.mjs:10-14
Timestamp: 2026-03-12T13:19:11.251Z
Learning: In `oritwoen/obsxa`, releases are only ever run on Linux by a single author (oritwoen). Do not flag Windows compatibility issues (e.g., `execFileSync` without `shell: true`, `.cmd` wrapper handling) in release scripts such as `scripts/release-with-opencode.mjs`.
Applied to files:
claude-code/package.jsonpackage.json
📚 Learning: 2026-03-12T11:16:06.009Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-12T11:16:06.009Z
Learning: Applies to **/*.{ts,tsx,js,mjs} : Use ESM-only TypeScript with `"type": "module"` in package.json; do not introduce CommonJS
Applied to files:
package.jsonbuild.config.ts
📚 Learning: 2026-03-12T11:16:06.009Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-12T11:16:06.009Z
Learning: Applies to {drizzle/**/*.ts,src/**/schema.ts} : Schema changes must go through Drizzle migrations using `pnpm generate`, not ad-hoc SQL edits in runtime code
Applied to files:
package.json
📚 Learning: 2026-03-12T07:43:02.506Z
Learnt from: CR
Repo: oritwoen/obsxa PR: 0
File: src/core/AGENTS.md:0-0
Timestamp: 2026-03-12T07:43:02.506Z
Learning: Applies to src/core/{dedup,observation}.ts : Keep transaction-heavy flows contained, notably in `dedup.ts` and lifecycle updates
Applied to files:
src/claude-code-hooks.ts
🧬 Code graph analysis (5)
claude-code/hooks.mjs (1)
src/claude-code-hooks.ts (1)
runHookCli(168-199)
src/claude-code.ts (5)
src/types.ts (1)
RELATION_TYPES(279-287)src/index.ts (2)
ObsxaInstance(193-202)createObsxa(217-298)src/core/mappers.ts (1)
parseTags(12-20)src/core/db-path.ts (1)
getDefaultDbPath(8-42)src/shared.ts (1)
isSqliteConstraintError(9-36)
test/claude-code.test.ts (3)
src/index.ts (2)
ObsxaInstance(193-202)createObsxa(217-298)src/claude-code.ts (1)
registerTools(42-449)src/claude-code-hooks.ts (4)
handlePostToolUse(25-62)handleSessionStart(64-91)handleStop(93-122)handleHookEvent(124-154)
claude-code/index.mjs (1)
src/claude-code.ts (1)
startMcpServer(451-497)
src/claude-code-hooks.ts (3)
src/index.ts (2)
ObsxaInstance(193-202)createObsxa(217-298)src/shared.ts (2)
computeInputHash(3-7)isSqliteConstraintError(9-36)src/core/db-path.ts (1)
getDefaultDbPath(8-42)
🔇 Additional comments (10)
test/claude-code.test.ts (2)
21-31: The DB isolation setup is correct.Each suite gets its own sqlite file under a fresh temp dir and tears it down in
afterEach, so these integration tests won't bleed state across cases. As per coding guidelines "Use temporary database directories created withmkdtempSyncand ensure cleanup in test suite lifecycle hooks" and "Do not share sqlite files across tests; maintain isolation between test cases".
275-328: Good call testinghandleHookEventagainst a real database.This path exercises project creation, persistence, and collector wiring together, which is the part mocks usually miss. As per coding guidelines "Prefer integration tests over mocks when testing store behavior".
claude-code/index.d.mts (1)
1-9: LGTM!Clean barrel file. Uses
export typefor the type-only export, explicit.tsextensions throughout.src/claude-code.ts (2)
499-502:isMaindetection works but is fragile with symlinks or relative paths.This fails if
process.argv[1]is relative and not resolved. Node usually provides absolute paths, so it's fine in practice. Just noting it.
42-449: Tool registration looks solid.Clean dispatcher pattern with proper validation and error handling for each operation. Required fields are checked before calling store methods.
src/claude-code-hooks.ts (5)
1-8: Imports and constants look good.Uses
import typecorrectly forObsxaInstance. SKIP_TOOLS covers the expected read-only tools per PR description.
25-62: Dedup logic is sound for single-process use.If two hook processes run concurrently with the same input, both could miss the hash lookup and insert duplicates. For typical hook usage (sequential invocations), this won't happen. Just noting it.
156-166: LGTM.Standard stdin reader. Will block until EOF, which is expected for piped input.
124-154: Solid dispatcher.Creates obsxa, ensures project exists (ignoring already-exists errors), routes to handler, closes in finally. Good resource hygiene.
64-122: Session handlers follow the same clean pattern.
handleSessionStartandhandleStopuse consistent dedup logic. Using "pattern" type for session events vs "measurement" for tool use makes sense.
| "files": [ | ||
| "index.mjs", | ||
| "hooks.mjs", | ||
| "index.d.mts" | ||
| ], | ||
| "type": "module", | ||
| "main": "./index.mjs", | ||
| "types": "./index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./index.d.mts", | ||
| "default": "./index.mjs" | ||
| }, | ||
| "./hooks": { | ||
| "default": "./hooks.mjs" | ||
| } |
There was a problem hiding this comment.
Published typings point to files that are not in the tarball.
Per the claude-code/index.d.mts change in this PR, the type barrel re-exports ../src/claude-code.ts and ../src/claude-code-hooks.ts. This package only publishes index.mjs, hooks.mjs, and index.d.mts, so those declaration imports go dead after publish and tsc will fail on first import. Ship local .d.mts files for those modules or rewrite the barrel to only reference files included here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude-code/package.json` around lines 15 - 30, The package's published type
entrypoint index.d.mts re-exports ../src/claude-code.ts and
../src/claude-code-hooks.ts which aren't included in the published files,
breaking consumers' tsc; either publish the corresponding .d.mts files for those
modules or rewrite index.d.mts to re-export only the shipped declarations
(index.d.mts) or relative declarations that exist (e.g., create and include
./claude-code.d.mts and ./claude-code-hooks.d.mts), and update package.json
"files" and "exports" entries (index.d.mts, hooks.mjs, any new .d.mts) so the
declared types resolve after publish.
| "main": "./index.mjs", | ||
| "types": "./index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./index.d.mts", | ||
| "default": "./index.mjs" | ||
| }, | ||
| "./hooks": { | ||
| "default": "./hooks.mjs" | ||
| } |
There was a problem hiding this comment.
Don't export the CLI bootstrap as ".".
index.d.mts says this package has named APIs, but index.mjs just starts the MCP server on import and exports nothing. That means import "obsxa-claude-code" has side effects, and import { startMcpServer } from "obsxa-claude-code" cannot work at runtime. Keep the bootstrap in bin only and point "." at a pure re-export module instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@claude-code/package.json` around lines 21 - 30, The package currently maps
the package root (".") to index.mjs which runs the MCP server on import while
index.d.mts declares named exports (e.g., startMcpServer), causing side-effects
and breaking named imports; change the package.json "exports" entry for "." to
point to a pure re-export module (not the bootstrap) that exports the named APIs
declared in index.d.mts, move the side-effectful CLI bootstrap into the bin
entry only (leave the server-starting code in a separate bootstrap file, e.g.,
bin/bootstrap.mjs), and ensure the new root module (e.g., ./index.mjs or
./api.mjs) only re-exports startMcpServer and other named symbols to match
index.d.mts so imports like import { startMcpServer } from "obsxa-claude-code"
work without side effects.
| import { parseArgs } from "node:util"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z } from "zod/v4"; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
How to import Zod 4 in JavaScript/TypeScript?
💡 Result:
- Install Zod v4
npm i zod@^4Zod v4 is exported from the package root ("zod") as of July 8, 2025. [1]
- Import (recommended)
TypeScript / ESM
import * as z from "zod";[2]
CommonJS
const z = require("zod");(same idea: import the module namespace)
- If you want to pin specifically to v4 (useful in libraries/monorepos)
import * as z4 from "zod/v4";[1]
- Optional subpaths
“Mini” build
import * as z from "zod/mini";(older docs/versions may mention "zod/v4-mini" → "zod/mini"). [1]
Core-only utilities/types
import * as zc from "zod/v4/core";
// or: import * as z from "zod"; then use z.core.*[3]
Sources: [1] [2] [3]
Fix Zod import syntax
Zod exports a namespace, not a named export—import { z } won't work. Use import * as z from "zod/v4" instead (or just import * as z from "zod" to use the standard entry point). The zod/v4 subpath is valid if you want to pin specifically to v4, but you need the namespace syntax.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/claude-code.ts` at line 4, The current import uses a named import for Zod
which is incorrect; replace the named import statement that references z with a
namespace import (e.g., change the import line that currently references z to
use "import * as z from 'zod/v4'" or "import * as z from 'zod'") so downstream
uses of z (the zod schema functions/types in this file) resolve correctly.
obsxa had an OpenCode plugin but nothing for Claude Code. This adds both layers of integration.
MCP server (
src/claude-code.ts) exposes 7 tools matching the existing AI SDK tools - observation, relation, cluster, search, analysis, promote, dedup. Runs over stdio, takes--dband--projectargs.Hook handler (
src/claude-code-hooks.ts) reads JSON from stdin and writes observations for PostToolUse (skipping read-only tools like Read/Grep/Glob), SessionStart, and Stop events. Same SHA-256 dedup as the OpenCode plugin.Extracted
computeInputHashandisSqliteConstraintErrorintosrc/shared.tsso both plugins share the logic instead of duplicating it.Wrapper package in
claude-code/with bin entries (obsxa-mcp,obsxa-hooks) follows the same pattern asopencode/.Closes #23