feat(cursor): Phase 1 foundation (IDE abstraction + AgentSdk factory) — held until Phase 2 extension validates#129
Open
George-iam wants to merge 6 commits intomainfrom
Open
Conversation
Phase 1 / 3 of cursor-full-support series — purely additive abstractions
that let later PRs ship Cursor setup writers and a Cursor SDK adapter
without re-touching the hook hot path.
- New src/utils/ide-detect.ts: IdeKind type + parseIdeFlag(argv) +
detectIdeFromEnv + detectIdeFromHookStdin + resolveIde precedence
(argv → env → stdin heuristic → claude-code default).
- New src/hooks/adapters/{types,claude-code,cursor}.ts: NormalizedHookEvent
+ HookInputAdapter / HookOutputAdapter contracts; per-IDE stdin parsers
(Cursor: conversation_id, cursor_version, transcript_path|null);
per-IDE deny emitters (Claude: hookSpecificOutput JSON, exit 0;
Cursor: flat permission/user_message JSON, exit 2).
- Refactor src/hooks/{pre,post,session-end}.ts to consume
NormalizedHookEvent + a HookOutputAdapter. Safety-checking core in
pre-tool-use.ts is byte-identical for Claude Code users.
- src/cli.ts case "hook" now parses --ide and forwards into the right
adapter; defaults to claude-code so existing setups are unaffected.
- src/audit-spawner.ts forwards --ide to the detached worker argv (used
by PR-3 to dispatch the correct transcript parser).
- src/types.ts: add IdeKind type; ClaudeSessionRef gains optional
ide?: IdeKind. ensureAxmeSessionForClaude / attachClaudeSession in
src/storage/sessions.ts thread the optional ide param through.
Tests (+30): test/ide-detect.test.ts covers flag/env/stdin precedence;
test/cursor-hook-adapter.test.ts covers Cursor stdin parsing (incl.
session_id vs conversation_id selection per hook kind, transcript_path
null-preservation) and exit-code-2 deny emission; test/
claude-code-hook-adapter.test.ts asserts byte-for-byte regression of
the existing deny JSON.
Full suite: 572 / 572 (was 542). tsc --noEmit clean. npm run build
clean. No behaviour change for Claude Code users.
Decisions: D-145 (Phase 1 ships before VS Code Extension). Memories
saved: cursor-sdk-system-prompt-via-inline-agent-definition,
cursor-hook-protocol-exit-code-2-deny, cursor-sdk-1-0-12-no-win-arm64,
cursor-agent-transcript-format-jsonl-with-top-level-role.
#!axme pr=none repo=AxmeAI/axme-code
…hook events Surfaced by Cursor agent spec-check on PR #129 (2026-05-10). Previous behaviour was: pre/postToolUse mapped by conversation_id, sessionEnd preferred session_id with conversation_id fallback. Cursor's sessionEnd payload contains BOTH (conversation_id from common-base fields + session_id from event-specific fields), and the two IDs identify different scopes (conversation_id = chat thread; session_id = broader SDK session that may span multiple conversations). The mismatch would orphan AXME sessions: pre/postToolUse creates .axme-code/active-sessions/<conversation_id>.txt, then sessionEnd looks up by session_id and finds nothing — clearClaudeSessionMapping no-ops, the original mapping leaks, and the audit worker doesn't fire on the right session. Fix: always prefer conversation_id (present in every Cursor hook payload via common-base fields), fall back to session_id only if conversation_id is somehow absent. Apply to all three hook kinds uniformly. Tests updated: existing "uses session_id for sessionEnd" assertion inverted to "uses conversation_id for sessionEnd"; new test for session_id fallback when conversation_id absent. Full suite: 573 / 573 (was 572 before fix; +1 new fallback test). tsc clean. build clean. Also supersedes memory cursor-sdk-system-prompt-via-inline-agent-... (deleted) with cursor-sdk-system-prompt-prepend-to-first-send-... after the same spec-check found that Agent.create() has no top-level systemPrompt option and the agents+agentId pattern injects subagents, not the outer agent's system prompt. PR-2 implementation will use agent.send(`<system>\n\${SYSTEM}\n</system>\n\n\${user}`) instead. This fix touches PR-1 code only; the PR-2 SDK adapter design is already updated in the plan file. #!axme pr=129 repo=AxmeAI/axme-code
…2 of #129) Phase 1 / 3 second pass — adds the user-visible Cursor pieces on top of the IDE abstraction landed earlier in this PR: Setup writers (src/setup/cursor-writers.ts, ~155 LOC) - writeCursorMcpJson / writeCursorHooksJson / writeCursorRulesMdc - All idempotent: re-runs preserve user-added entries, dedupe axme entries by command-string match. version:1 hooks.json + frontmatter on .cursor/rules/axme-code.mdc (alwaysApply: true). - Hook commands include "--ide cursor" so the spawned subprocess picks the right adapter without re-detecting. CLI setup branch (src/cli.ts) - Parse --ide=<claude-code|cursor> flag (default claude-code). - Cursor branch: write .cursor/{mcp,hooks}.json + rules/axme-code.mdc; skip CLAUDE.md (D-080) and .claude/settings.json. Root .mcp.json is always written so a repo can be opened in either IDE. - Reject --ide=cursor + --plugin combination (Phase 2 follow-up). - usage() documents --ide. AuthMode = subscription | api_key | cursor_sdk (src/types.ts) - New CursorApiKeyConfig type for ~/.config/axme-code/cursor.yaml. - auth-config.ts: loadCursorApiKey / saveCursorApiKey (chmod 600 on POSIX), heuristicMode prefers cursor_sdk only when nothing else detected, validate-mode whitelist extended. - auth-detect.ts: detectCursorSdk reads CURSOR_API_KEY env or ~/.config/axme-code/cursor.yaml, surfaces as third AuthOption. - auth-prompt.ts: option [3] Cursor SDK in formatDetectionBlock and promptAuthChoice; new promptCursorApiKey() paste-once flow. - cli.ts auth subcommand: cursor_sdk handled in both interactive (paste-on-pick) and "auth use cursor_sdk" non-interactive paths. AgentSdk factory (src/utils/agent-sdk.ts + agent-sdk-{claude,cursor}.ts) - IDE-agnostic interface mirroring the Claude SDK message envelope. - createAgentSdk(role) selects IDE via opts → AXME_IDE env → authImpliedIde → "claude-code" default. Fallback chain: cursor → claude (if findClaudePath() or ANTHROPIC_API_KEY). Throws AgentSdkUnavailableError when neither usable; the detached audit worker (PR-3) will catch and skip. - win-arm64 short-circuit: never attempts @cursor/sdk import there (no native binary in 1.0.12). - Cursor wrapper translates Cursor stream events (assistant / thinking / status / tool_call) into the shared AgentMessage shape and synthesizes a terminal "result" message. - System-prompt injection: prepend <system>\n...\n</system>\n\n to the first agent.send() — Cursor SDK has no top-level systemPrompt option (verified via Cursor agent spec-check on PR-1). agent-options.ts - buildAgentEnv(): mode=cursor_sdk hydrates CURSOR_API_KEY from cursor.yaml and deletes ANTHROPIC_API_KEY (preempt dual-provider surprise if Cursor ever adds one). - mapClaudeToolsToCursor(): Bash → Shell, drops NotebookEdit / Agent / Skill / TodoWrite / WebFetch / WebSearch / ToolSearch (not in Cursor tool taxonomy). @cursor/sdk@1.0.12 added to optionalDependencies (exact pin), so npm install succeeds on win-arm64 (no native binary) without erroring. Tests (+22): - test/cursor-setup-writers.test.ts: idempotent merge for hooks/mcp, user-entry preservation, frontmatter on rules.mdc. - test/cursor-auth-config.test.ts: cursor_sdk YAML round-trip, cursor.yaml chmod 600, missing/empty key handling. - test/agent-sdk-factory.test.ts: IDE selection precedence, fallback chain when CURSOR_API_KEY missing, mapClaudeToolsToCursor. Full suite: 595 / 595 (was 573). tsc clean. build clean. This commit is PR-2 of the cursor-full-support series, all of which land in PR #129 (per user direction: don't merge until full Cursor E2E). PR-3 will wire the session-auditor + scanners through the new AgentSdk factory and add a Cursor-aware branch to transcript-parser. #!axme pr=129 repo=AxmeAI/axme-code
Replaces every direct `await import("@anthropic-ai/claude-agent-sdk")`
in src/agents/* with `await createAgentSdk(role, { cwd })` so the
auditor, scanners, kb-auditor, and memory-extractor pick the right SDK
(Claude or Cursor) at runtime based on auth.yaml mode + AXME_IDE env.
Touched call sites (8 in 7 files):
- src/agents/session-auditor.ts: runAuditChunk + formatAuditResult
- src/agents/kb-auditor.ts: runKbAudit
- src/agents/memory-extractor.ts: extractMemories
- src/agents/scanners/oracle.ts: runOracleScan
- src/agents/scanners/safety.ts: runSafetyScan
- src/agents/scanners/decision.ts: runDecisionScan
- src/agents/scanners/deploy.ts: runDeployScan
Each call site retains its existing buildAgentQueryOptions/manual
queryOpts construction unchanged — only the SDK access path swaps to
the factory. Loop body (`for await msg`) shape is identical because
AgentMessage mirrors Claude SDK's message envelope.
src/utils/agent-sdk.ts:
- Renamed inline `options` shape to exported `AgentQueryOptions`.
- Made every field except `prompt` optional. Reason: existing callers
pass Claude SDK's `Options` type (which has most fields optional),
and the Cursor adapter only needs `cwd` + `model` + `systemPrompt` —
the rest is forwarded as-is to whichever wrapper.
- `systemPrompt` widened to `string | string[] | { preset object }` to
match Claude SDK 0.2.112+ which now accepts string arrays.
- `permissionMode` widened to `string` (Claude SDK has more values
than just "bypassPermissions").
src/utils/agent-sdk-cursor.ts:
- resolveSystemPrompt now handles `undefined` and `string[]` inputs.
- Cursor wrapper falls back to `process.cwd()` and "composer-2" when
options.cwd / options.model are absent (defensive — shouldn't
happen in practice, but type-correctness matters).
Required for the upcoming Cursor extension: when an .vsix-installed
extension running in Cursor invokes an audit, the factory selects
@cursor/sdk via cursor_sdk auth mode and the auditor uses Cursor's
billing instead of falling through to Claude. Without this commit
the auditor would skip-or-fall-back even with a valid Cursor key.
Tests: 595/595 pass (no test changes needed — call shape unchanged).
tsc --noEmit clean. Build clean.
#!axme pr=129 repo=AxmeAI/axme-code
Cursor's stored agent transcripts at
~/.cursor/projects/<slug>/agent-transcripts/<uuid>/<uuid>.jsonl
use a different shape from Claude Code's:
Cursor: {"role":"user","message":{"content":[{"type":"text","text":"..."}]}}
Claude: {"message":{"role":"user","content":[{"type":"text","text":"..."}]}}
Cursor puts `role` at the TOP level; Claude nests it inside `message`.
Cursor's stored transcripts contain ONLY type=text blocks — no
tool_use, thinking, or tool_result (verified empirically against
~/.cursor/projects/home-georgeb-axme-workspace-AxmeAI-code-workspace
on 2026-05-10).
Changes
-------
src/transcript-parser.ts:
- Add optional `ide?: IdeKind` parameter to parseTranscriptFromOffset,
parseTranscript, parseAndRenderTranscript. Default "claude-code"
preserves existing behaviour for every current caller.
- Role extraction now reads `event.role` (Cursor) when ide==="cursor",
falls back to `msg.role` (Claude) otherwise. The fallback chain
`event.role ?? msg.role` is defensive — a Cursor transcript with
ide=undefined (older ClaudeSessionRef without ide field) still
parses correctly.
- parseAndRenderTranscripts threads `ref.ide` into the per-ref call
so multi-agent Cursor sessions get the right shape per ref.
- Imported IdeKind from ../types.js.
test/transcript-parser.test.ts:
- New `Cursor JSONL transcript parsing` describe block with 4 tests:
* Cursor format with ide="cursor" — top-level role honored
* Cursor format without ide param — defensive fallback works
* Text-only Cursor transcript — zero tool_use/thinking turns
* Claude format with ide="claude-code" — regression check
This commit completes PR-3 wiring on the parser side. The session
auditor (after commit 1/3 of this batch routes through AgentSdk)
already reads ClaudeSessionRef.ide from session metadata; it now
hands that value to the parser at audit time.
Tests: 599 / 599 pass (was 595, +4 new in this commit).
tsc --noEmit clean.
#!axme pr=129 repo=AxmeAI/axme-code
… absent User-level Cursor hooks installed at ~/.cursor/hooks.json by the VS Code extension cannot hard-code a workspace path because they apply machine-wide. Cursor's hook stdin payload always includes workspace_roots[] (a Cursor common-base field, present in every event type), so hook handlers now fall back to workspace_roots[0] when the --workspace flag is absent. Precedence order, all three handlers: 1. explicit --workspace flag (existing Claude Code path — unchanged) 2. stdin workspace_roots[0] (new — Cursor user-level hook path) 3. process.cwd() (last-resort fallback — unchanged) Files ----- src/hooks/pre-tool-use.ts: read stdin first, resolve workspace path AFTER, dispatch to handlePreToolUse with the resolved path. src/hooks/post-tool-use.ts: same. src/hooks/session-end.ts: same; preserves the existing tolerance for empty/invalid stdin (sessionEnd is sometimes fired with no payload). build.mjs: add @cursor/sdk to the plugin bundle's external list. The plugin bundle uses packages: "bundle" and was choking on @cursor/sdk's internal @anysphere/* sub-package imports. Marking it external keeps the plugin distribution buildable; the cursor_sdk path is meaningless in plugin context anyway (Claude Code marketplace users use the Claude Agent SDK), and the AgentSdk factory's fallback already handles the missing-module case gracefully. test/hooks-workspace-fallback.test.ts: new — spawns real `tsx` hook subprocess with no --workspace flag, feeds Cursor-shaped stdin via pipe, asserts each of the three handlers exits cleanly with the right resolved path. 5 tests. Test suite: 604 / 604 pass (was 599; +5 new). tsc clean. Build clean. This commit completes PR-3 wiring (the third of three additional foundation commits requested by the rewritten Phase 2 plan). PR #129 foundation is now genuinely complete: AgentSdk factory, Cursor transcript parser, hook workspace fallback. The Phase 2 extension branch can rely on these without further core changes. #!axme pr=129 repo=AxmeAI/axme-code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: Phase 1 foundation — held open until Phase 2 (VS Code Extension) validates the full pipeline end-to-end
This PR contains commits PR-1 + PR-2 of the original three-PR Phase 1 plan. After PR-2 spec-check by a Cursor agent, we discovered that Cursor 0.42+ does not auto-spawn project-level MCP servers from
.cursor/mcp.jsoneven when they are well-formed. Cursor's security model requires manual enable in Settings → MCP for any new project-level server, as a safeguard against malicious.cursor/mcp.jsonin cloned repos. Diagnostic from~/.cursor-server/data/logs/<ts>/exthost*/anysphere.cursor-mcp/MCP project-*.log:This is documented Cursor behaviour, not an axme-code bug. The fix is to distribute axme-code as a VS Code Extension (Open VSX) that calls Cursor's proprietary
cursor.mcp.registerServer()API at activation time, bypassing the per-project Enable gate via the extension-install trust boundary. Tracked as Phase 2 /feat/vscode-extension-20260510.What's in this PR
PR-1 commits (foundation — reused by extension)
feat(cursor): add IDE abstraction layer and Cursor hook adapter—IdeKindtype, hook adapter pattern (NormalizedHookEvent), Cursor stdin parser, deny-output emitter (exit code 2 + flat permission JSON),--ideflag in CLI hook dispatcher and audit-spawner.fix(cursor): unify conversation_id session-key precedence across all hook events— fix surfaced by PR-1 spec-check.PR-2 commits
feat(cursor): setup writers + cursor_sdk auth + AgentSdk factory—.cursor/{mcp,hooks}.json+rules/axme-code.mdcwriters (idempotent merge).--ide=cursorbranch inaxme-code setup.cursor_sdk+~/.config/axme-code/cursor.yaml(chmod 600).src/utils/agent-sdk.ts+ Claude/Cursor wrappers).@cursor/sdk@1.0.12as optional dep.Why this PR is held open
The Phase 2 Extension reuses everything here. Final E2E validation happens once Phase 2 lands and a real Cursor user installs the .vsix. Merging this PR alone, without that validation, would put unverified config-writer logic on
main. We keep the branch alive so the Extension can build directly on top.Tests
tsc --noEmitclean.npm run buildclean.Phase 2 (parallel work)
Branch:
feat/vscode-extension-20260510. Target: one.vsixon Open VSX covering Cursor / VS Code Copilot / Cline / Continue / Roo Code / Windsurf — register MCP via Cursor'scursor.mcp.registerServer(Cursor branch) orvscode.lm.registerMcpServerDefinitionProvider(VS Code 1.101+ branch). User installs from Extensions panel → AXME works without Enable gate.🤖 Generated with Claude Code