Skip to content

Commit 70bf207

Browse files
committed
chore: add detailed bm command debug logging
1 parent 5ab06c5 commit 70bf207

File tree

2 files changed

+87
-6
lines changed

2 files changed

+87
-6
lines changed

bm-client.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, it, jest } from "bun:test"
22
import {
33
BmClient,
4+
formatCommandForLog,
45
isMissingEditNoteCommandError,
56
isNoteNotFoundError,
67
isProjectAlreadyExistsError,
@@ -40,6 +41,26 @@ Warning: something happened
4041
})
4142
})
4243

44+
describe("formatCommandForLog", () => {
45+
it("quotes arguments with spaces", () => {
46+
expect(
47+
formatCommandForLog("/usr/local/bin/bm", [
48+
"tool",
49+
"search-notes",
50+
"marketing strategy",
51+
"--hybrid",
52+
"--page-size",
53+
"3",
54+
"--project",
55+
"claw",
56+
"--local",
57+
]),
58+
).toBe(
59+
'/usr/local/bin/bm tool search-notes "marketing strategy" --hybrid --page-size 3 --project claw --local',
60+
)
61+
})
62+
})
63+
4364
describe("error classifiers", () => {
4465
it("detects unsupported --strip-frontmatter flag errors", () => {
4566
expect(
@@ -261,6 +282,22 @@ describe("BmClient behavior", () => {
261282
])
262283
})
263284

285+
it("search passes multi-word query as a single argument", async () => {
286+
const execToolNativeJson = jest.fn().mockResolvedValue('{"results":[]}')
287+
;(client as any).execToolNativeJson = execToolNativeJson
288+
289+
await client.search("marketing strategy", 3)
290+
291+
expect(execToolNativeJson).toHaveBeenCalledWith([
292+
"tool",
293+
"search-notes",
294+
"marketing strategy",
295+
"--hybrid",
296+
"--page-size",
297+
"3",
298+
])
299+
})
300+
264301
it("indexConversation does not create fallback note on non-not-found edit errors", async () => {
265302
;(client as any).editNote = jest
266303
.fn()

bm-client.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { promisify } from "node:util"
33
import { log } from "./logger.ts"
44

55
const execFileAsync = promisify(execFile)
6+
let bmCallCounter = 0
67

78
/**
89
* Strip YAML frontmatter from note content.
@@ -106,6 +107,17 @@ export function parseJsonOutput(raw: string): unknown {
106107
throw new Error(`Could not parse JSON from bm output: ${raw.slice(0, 200)}`)
107108
}
108109

110+
function quoteArgForLog(arg: string): string {
111+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg
112+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
113+
}
114+
115+
export function formatCommandForLog(cmd: string, args: string[]): string {
116+
return [quoteArgForLog(cmd), ...args.map((arg) => quoteArgForLog(arg))].join(
117+
" ",
118+
)
119+
}
120+
109121
function getErrorMessage(err: unknown): string {
110122
return err instanceof Error ? err.message : String(err)
111123
}
@@ -161,20 +173,52 @@ export class BmClient {
161173
* Run a raw bm command with no automatic flags.
162174
*/
163175
private async execRaw(args: string[]): Promise<string> {
164-
log.debug(`exec: ${this.bmPath} ${args.join(" ")}`)
176+
const callId = ++bmCallCounter
177+
const startedAt = Date.now()
178+
const renderedCommand = formatCommandForLog(this.bmPath, args)
179+
log.debug(`[bm:${callId}] exec start: ${renderedCommand}`)
180+
log.debug(`[bm:${callId}] exec args`, args)
165181

166182
try {
167-
const { stdout } = await execFileAsync(this.bmPath, args, {
183+
const { stdout, stderr } = await execFileAsync(this.bmPath, args, {
168184
timeout: 30_000,
169185
maxBuffer: 10 * 1024 * 1024,
170186
})
171-
return stdout.trim()
187+
const durationMs = Date.now() - startedAt
188+
const trimmedStdout = stdout.trim()
189+
190+
log.debug(
191+
`[bm:${callId}] exec success (${durationMs}ms): ${renderedCommand}`,
192+
)
193+
log.debug(`[bm:${callId}] stdout`, stdout)
194+
log.debug(`[bm:${callId}] stdout (trimmed)`, trimmedStdout)
195+
if (stderr.trim().length > 0) {
196+
log.debug(`[bm:${callId}] stderr`, stderr)
197+
}
198+
199+
return trimmedStdout
172200
} catch (err) {
201+
const durationMs = Date.now() - startedAt
173202
const msg = err instanceof Error ? err.message : String(err)
174-
log.debug(`bm command failed: ${this.bmPath} ${args.join(" ")}${msg}`)
175-
throw new Error(
176-
`bm command failed: ${this.bmPath} ${args.join(" ")}${msg}`,
203+
const failedStdout =
204+
typeof (err as { stdout?: unknown }).stdout === "string"
205+
? (err as { stdout: string }).stdout
206+
: ""
207+
const failedStderr =
208+
typeof (err as { stderr?: unknown }).stderr === "string"
209+
? (err as { stderr: string }).stderr
210+
: ""
211+
212+
log.debug(
213+
`[bm:${callId}] exec failure (${durationMs}ms): ${renderedCommand}${msg}`,
177214
)
215+
if (failedStdout.trim().length > 0) {
216+
log.debug(`[bm:${callId}] stdout (error path)`, failedStdout)
217+
}
218+
if (failedStderr.trim().length > 0) {
219+
log.debug(`[bm:${callId}] stderr (error path)`, failedStderr)
220+
}
221+
throw new Error(`bm command failed: ${renderedCommand}${msg}`)
178222
}
179223
}
180224

0 commit comments

Comments
 (0)