Skip to content

Commit ff7193f

Browse files
committed
fix(logging): suppress cursor-sdk shell-parser tree-sitter warn from TUI
@cursor/sdk's bundled shell-parser emits a one-shot console.warn ("shell-parser: tree-sitter natives are unavailable...") when its vendored tree-sitter natives fail to load (e.g. under Bun). opencode renders plugin stderr into the prompt, so the diagnostic appeared visually even though it is benign (shell analysis degrades to parsingFailed). Extend the existing console.log interceptor pattern to console.warn on both transports: - in-process: installCursorLogInterceptor now wraps console.warn; known SDK warning prefixes route through pluginLog("warn") to opencode's app.log instead of stderr - sidecar: agent-host.mjs wraps console.warn and forwards matched lines as {ev:"log", level:"warn"} over the JSONL protocol Adds tests for both paths; unrelated console.warn calls still pass through unchanged.
1 parent d93ac75 commit ff7193f

5 files changed

Lines changed: 194 additions & 46 deletions

File tree

‎src/provider/cursor-log-intercept.ts‎

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ function stripAnsi(input: string): string {
2323
const RULE_LOAD_PATTERN =
2424
/^\d{2}:\d{2}:\d{2}\.\d{3}\s+INFO\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\s+ctx=\S+)?\s+meta=\{([^}]*)\}\s*$/;
2525

26+
/**
27+
* One-shot `console.warn` diagnostics emitted at `@cursor/sdk` module load.
28+
* Currently exactly one known line (vendored tree-sitter natives missing,
29+
* shell parsing degrades to `parsingFailed`) — matched by prefix so future
30+
* SDK builds appending detail still get captured.
31+
*/
32+
const SDK_WARNING_PREFIXES = [
33+
"shell-parser: tree-sitter natives are unavailable in this artifact",
34+
];
35+
36+
function matchesKnownSdkWarning(line: string): boolean {
37+
return SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix));
38+
}
39+
2640
/** Parses the `meta={key: value, ...}` tail into a plain numeric object. */
2741
export function parseCursorLogMeta(raw: string): Record<string, number> {
2842
const out: Record<string, number> = {};
@@ -41,7 +55,9 @@ export interface ParsedCursorRuleLog {
4155
}
4256

4357
/** Matches one line against the known Cursor rules/skills load-completion shape. */
44-
export function parseCursorRuleLoadLine(line: string): ParsedCursorRuleLog | undefined {
58+
export function parseCursorRuleLoadLine(
59+
line: string,
60+
): ParsedCursorRuleLog | undefined {
4561
const match = RULE_LOAD_PATTERN.exec(stripAnsi(line));
4662
if (!match) return undefined;
4763
const [, service, meta] = match;
@@ -50,15 +66,18 @@ export function parseCursorRuleLoadLine(line: string): ParsedCursorRuleLog | und
5066
}
5167

5268
let installed = false;
53-
let original: typeof console.log | undefined;
69+
let originalLog: typeof console.log | undefined;
70+
let originalWarn: typeof console.warn | undefined;
5471

5572
/**
56-
* Installs a narrowly-scoped `console.log` interceptor that recognizes only
57-
* the known Cursor rules/skills "load completed" messages (see
58-
* {@link parseCursorRuleLoadLine}) and re-emits them as structured opencode
59-
* logs via {@link pluginLog}. Every other `console.log` call — including
60-
* anything else the SDK or the host process writes — passes through
61-
* unchanged.
73+
* Installs narrowly-scoped `console.log`/`console.warn` interceptors. On
74+
* `console.log`, recognizes only the known Cursor rules/skills "load
75+
* completed" messages (see {@link parseCursorRuleLoadLine}) and re-emits
76+
* them as structured opencode logs via {@link pluginLog}. On `console.warn`,
77+
* recognizes known one-shot SDK load diagnostics (see
78+
* {@link SDK_WARNING_PREFIXES}) and routes them the same way. Every other
79+
* `console.log`/`console.warn` call — including anything else the SDK or the
80+
* host process writes — passes through unchanged.
6281
*
6382
* Only relevant to the in-process transport, where the SDK runs inside this
6483
* process and writes directly to the shared global `console`. The sidecar
@@ -69,8 +88,8 @@ let original: typeof console.log | undefined;
6988
*/
7089
export function installCursorLogInterceptor(): void {
7190
if (installed) return;
72-
original = console.log.bind(console);
73-
const passthrough = original;
91+
originalLog = console.log.bind(console);
92+
const logPassthrough = originalLog;
7493
console.log = (...args: unknown[]) => {
7594
if (args.length === 1 && typeof args[0] === "string") {
7695
const parsed = parseCursorRuleLoadLine(args[0]);
@@ -79,14 +98,28 @@ export function installCursorLogInterceptor(): void {
7998
return;
8099
}
81100
}
82-
passthrough(...(args as Parameters<typeof console.log>));
101+
logPassthrough(...(args as Parameters<typeof console.log>));
102+
};
103+
originalWarn = console.warn.bind(console);
104+
const warnPassthrough = originalWarn;
105+
console.warn = (...args: unknown[]) => {
106+
if (args.length === 1 && typeof args[0] === "string") {
107+
const line = stripAnsi(args[0]);
108+
if (matchesKnownSdkWarning(line)) {
109+
pluginLog("warn", line);
110+
return;
111+
}
112+
}
113+
warnPassthrough(...(args as Parameters<typeof console.warn>));
83114
};
84115
installed = true;
85116
}
86117

87118
/** Test hook. */
88119
export function resetCursorLogInterceptor(): void {
89-
if (original) console.log = original;
90-
original = undefined;
120+
if (originalLog) console.log = originalLog;
121+
if (originalWarn) console.warn = originalWarn;
122+
originalLog = undefined;
123+
originalWarn = undefined;
91124
installed = false;
92125
}

‎src/sidecar/agent-host.mjs‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ function serializeError(err) {
2727
const out = { name: err.name, message: err.message };
2828
for (const k of ["status", "code", "isRetryable", "helpUrl"]) {
2929
const v = err[k];
30-
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") out[k] = v;
30+
if (
31+
typeof v === "number" ||
32+
typeof v === "string" ||
33+
typeof v === "boolean"
34+
)
35+
out[k] = v;
3136
}
3237
return out;
3338
}
@@ -42,16 +47,23 @@ function write(payload) {
4247
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
4348

4449
// `@cursor/sdk`'s bundled local-exec runtime writes its rules/skills
45-
// load-completion diagnostics straight to `console.log` (no public logger
46-
// hook exists to redirect it — see src/provider/cursor-log-intercept.ts,
47-
// which applies the identical pattern for the in-process transport). This
48-
// process's own JSONL protocol never uses console.log (only
49-
// process.stdout.write via write() above), so console.log here is entirely
50-
// free for the SDK's use: recognized lines are forwarded to the parent as a
51-
// structured "log" event instead of being written as raw, unparseable text.
50+
// load-completion diagnostics straight to `console.log`, and its shell-parser
51+
// emits a one-shot "tree-sitter natives unavailable" diagnostic via
52+
// `console.warn` (no public logger hook exists to redirect either — see
53+
// src/provider/cursor-log-intercept.ts, which applies the identical pattern
54+
// for the in-process transport). This process's own JSONL protocol never uses
55+
// console.log/console.warn (only process.stdout.write via write() above), so
56+
// they are entirely free for the SDK's use: recognized lines are forwarded
57+
// to the parent as a structured "log" event instead of being written as raw,
58+
// unparseable text.
5259
const RULE_LOAD_PATTERN =
5360
/^\d{2}:\d{2}:\d{2}\.\d{3}\s+INFO\s+(LocalCursorRulesService|AgentSkillsCursorRulesService|CursorPluginsAgentSkillsService) load completed(?:\s+ctx=\S+)?\s+meta=\{([^}]*)\}\s*$/;
5461

62+
// One-shot SDK load diagnostics recognized on console.warn (prefix-matched).
63+
const SDK_WARNING_PREFIXES = [
64+
"shell-parser: tree-sitter natives are unavailable in this artifact",
65+
];
66+
5567
function parseLogMeta(raw) {
5668
const out = {};
5769
for (const part of raw.split(",")) {
@@ -81,6 +93,18 @@ console.log = (...args) => {
8193
originalConsoleLog(...args);
8294
};
8395

96+
const originalConsoleWarn = console.warn.bind(console);
97+
console.warn = (...args) => {
98+
if (args.length === 1 && typeof args[0] === "string") {
99+
const line = args[0].replace(ANSI_PATTERN, "");
100+
if (SDK_WARNING_PREFIXES.some((prefix) => line.startsWith(prefix))) {
101+
write({ ev: "log", level: "warn", message: line });
102+
return;
103+
}
104+
}
105+
originalConsoleWarn(...args);
106+
};
107+
84108
let sdkPromise;
85109
function loadSdk() {
86110
// OPENCODE_CURSOR_SDK_PATH lets tests substitute a fake SDK module.

‎test/cursor-log-intercept.test.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ describe("parseCursorRuleLoadLine", () => {
5050
});
5151

5252
it("returns undefined for unrelated log lines", () => {
53-
expect(parseCursorRuleLoadLine("some unrelated cursor sdk output")).toBeUndefined();
54-
expect(parseCursorRuleLoadLine("Plugins reload completed: 3 plugins loaded")).toBeUndefined();
53+
expect(
54+
parseCursorRuleLoadLine("some unrelated cursor sdk output"),
55+
).toBeUndefined();
56+
expect(
57+
parseCursorRuleLoadLine("Plugins reload completed: 3 plugins loaded"),
58+
).toBeUndefined();
5559
});
5660
});
5761

@@ -86,6 +90,34 @@ describe("installCursorLogInterceptor", () => {
8690
passthrough.mockRestore();
8791
});
8892

93+
it("routes known SDK console.warn diagnostics through pluginLog instead of stderr", () => {
94+
const log = vi.fn().mockResolvedValue(undefined);
95+
setLogBridge({ client: { app: { log } } } as never);
96+
97+
const passthrough = vi.spyOn(console, "warn").mockImplementation(() => {});
98+
installCursorLogInterceptor();
99+
100+
console.warn(
101+
"shell-parser: tree-sitter natives are unavailable in this artifact; shell command analysis degrades to parsingFailed",
102+
);
103+
console.warn("unrelated warning");
104+
105+
expect(log).toHaveBeenCalledTimes(1);
106+
expect(log).toHaveBeenCalledWith({
107+
body: {
108+
service: "opencode-cursor",
109+
level: "warn",
110+
message:
111+
"shell-parser: tree-sitter natives are unavailable in this artifact; shell command analysis degrades to parsingFailed",
112+
},
113+
});
114+
115+
resetCursorLogInterceptor();
116+
expect(passthrough).toHaveBeenCalledTimes(1);
117+
expect(passthrough).toHaveBeenCalledWith("unrelated warning");
118+
passthrough.mockRestore();
119+
});
120+
89121
it("is idempotent across repeated installs", () => {
90122
installCursorLogInterceptor();
91123
const first = console.log;

‎test/fixtures/fake-cursor-sdk.mjs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* src/sidecar/agent-host.mjs / src/provider/cursor-log-intercept.ts), plus
1515
* one unrelated console.log line, to verify the sidecar's log interception
1616
* forwards only the recognized lines and passes everything else through.
17+
*
18+
* `options.emitShellParserWarn` -> Agent.create/resume writes the shell-parser
19+
* "tree-sitter natives unavailable" diagnostic to console.warn, as the real
20+
* @cursor/sdk does on first shell parse, plus one unrelated console.warn.
1721
*/
1822

1923
function makeAgent(agentId, options) {
@@ -26,6 +30,12 @@ function makeAgent(agentId, options) {
2630
);
2731
console.log("some unrelated cursor sdk output");
2832
}
33+
if (options?.emitShellParserWarn) {
34+
console.warn(
35+
"shell-parser: tree-sitter natives are unavailable in this artifact; shell command analysis degrades to parsingFailed",
36+
);
37+
console.warn("some unrelated cursor sdk warning");
38+
}
2939
return {
3040
agentId,
3141
model: options?.model,
@@ -45,7 +55,9 @@ function makeAgent(agentId, options) {
4555
err.helpUrl = "https://example.com/rate-limits";
4656
throw err;
4757
}
48-
sendOptions?.onDelta?.({ update: { type: "text-delta", text: `echo:${text}` } });
58+
sendOptions?.onDelta?.({
59+
update: { type: "text-delta", text: `echo:${text}` },
60+
});
4961
if (text === "hang") {
5062
let resolveWait;
5163
const waited = new Promise((resolve) => {

0 commit comments

Comments
 (0)