Skip to content

feat: structured logging for provider diagnostics + Cursor SDK rules/skills logs - #85

Draft
rfhold wants to merge 1 commit into
mainfrom
cursor-sdk-logging
Draft

feat: structured logging for provider diagnostics + Cursor SDK rules/skills logs#85
rfhold wants to merge 1 commit into
mainfrom
cursor-sdk-logging

Conversation

@rfhold

@rfhold rfhold commented Jul 29, 2026

Copy link
Copy Markdown

Summary

The plugin's own diagnostics, and diagnostics from the bundled @cursor/sdk, were being written straight to console.warn/console.error/console.log instead of using opencode's plugin logging API. This surfaced as raw, unstructured lines in the terminal, e.g.:

AgentSkillsCursorRulesService load completed meta={durationMs: 86, ruleCount: 18, skillCount: 18}
LocalCursorRulesService load completed meta={durationMs: 89, ruleCount: 1}

These two (plus a third, CursorPluginsAgentSkillsService load completed) come from @cursor/sdk's bundled local-exec runtime, which writes them straight to console.log — there's no public logger hook in the SDK to redirect them.

What changed

  • Plugin's own logging: every console.warn/console.error call in agent-backend.ts, agent-events.ts, and language-model.ts now routes through client.app.log({ body: { service: "opencode-cursor", level, message, extra } }), with a console.* fallback when no client bridge is available (e.g. the provider used standalone/in tests).
  • New src/provider/log-bridge.ts: mirrors the existing subagent-bridge.ts globalThis pattern so the provider layer (which has no direct import path to the plugin's opencode client) can log through it. Published by plugin/index.ts on init, cleared on dispose().
  • New src/provider/cursor-log-intercept.ts: a narrowly-scoped console.log interceptor used for the in-process transport. It matches only the three known Cursor rules/skills "load completed" messages (ANSI-safe) and re-emits them via pluginLog; every other console.log call passes through untouched.
  • Sidecar transport: src/sidecar/agent-host.mjs (the Node child that hosts @cursor/sdk when Bun's http2 client can't be used) installs the same interception in the child process and forwards matches to the parent over the existing JSONL protocol as a new { ev: "log" } message. SidecarClient gained an onLog option to receive these.

Testing

  • npm run typecheck — clean
  • npm test — 371 passed (29 pre-existing files + 2 new: log-bridge.test.ts, cursor-log-intercept.test.ts; sidecar.test.ts gained a log-forwarding case backed by a new emitRulesLog flag in the fake SDK fixture)
  • npm run build — verified the interceptor + log bridge are present in both dist/chunk-*.js (shared by provider/plugin) and dist/sidecar/agent-host.js

Notes

A broader interception of every Cursor SDK console.log call would risk swallowing unrelated output; this only recognizes the three confirmed rules/skills messages. If Cursor exposes a proper logger callback upstream, we can drop the interceptor and pass it through directly.

…skills logs

Converts every console.warn/console.error in the provider (agent-backend,
agent-events, language-model) to route through opencode's client.app.log()
API instead, with a console.* fallback when no client bridge is published.

Also captures @cursor/sdk's own internal rules/skills load-completion
diagnostics (e.g. "LocalCursorRulesService load completed
meta={durationMs, ruleCount}"), which the SDK writes straight to
console.log with no public logger hook, and re-emits them as structured
opencode logs instead of raw terminal noise:

- In-process transport: a narrowly-scoped console.log interceptor matches
  only the three known Cursor rules/skills messages; everything else
  passes through unchanged (src/provider/cursor-log-intercept.ts).
- Sidecar transport: the child process installs the same interception and
  forwards matches over the existing JSONL protocol as a new "log" event
  (src/sidecar/agent-host.mjs), which SidecarClient forwards via a new
  onLog option.

New src/provider/log-bridge.ts mirrors the existing subagent-bridge.ts
globalThis pattern to give the provider layer access to the plugin's
opencode client without a circular import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant