-
-
Notifications
You must be signed in to change notification settings - Fork 29
Add a CLI that agents can also use instead of the MCP #997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
241b575
bd92067
7fd359f
1d5861c
db12a52
383272a
29dc7af
e48ed17
9ea35f3
1015034
e0ba603
78925cd
a758f1e
6d2d0d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@spotlightjs/sidecar": minor | ||
"@spotlightjs/spotlight": minor | ||
--- | ||
|
||
Add CLI -- `spotlight logs+errors` etc are now available |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
"buildx", | ||
"codesign", | ||
"contextlines", | ||
"decompressors", | ||
"Endcaps", | ||
"fontsource", | ||
"getsentry", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,2 @@ | ||
export const DEFAULT_SIDECAR_URL = "http://localhost:8969"; | ||
export const DEFAULT_SIDECAR_STREAM_URL = new URL("/stream", DEFAULT_SIDECAR_URL).href; | ||
export const DEFAULT_INITIAL_TAB = "/traces"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,42 @@ | ||
#!/usr/bin/env node | ||
import { EventSource } from "eventsource"; | ||
import { formatEnvelope } from "./src/format"; | ||
import { parseCLIArgs, setupSidecar } from "./src/main.js"; | ||
import { EventContainer } from "./src/utils/eventContainer.js"; | ||
import { SENTRY_CONTENT_TYPE } from "./src/constants.js"; | ||
import { captureException } from "@sentry/core"; | ||
import { exit } from "process"; | ||
|
||
const args = parseCLIArgs(); | ||
const connectUpstream = async (port: number) => | ||
new Promise<EventSource>((resolve, reject) => { | ||
const client = new EventSource(`http://localhost:${port}/stream`); | ||
client.onerror = reject; | ||
client.onopen = () => resolve(client); | ||
}); | ||
|
||
if (args.help) { | ||
const SEPARATOR = Array(10).fill("─").join(""); | ||
|
||
function displayEnvelope(envelope: EventContainer) { | ||
const { event } = envelope.getParsedEnvelope(); | ||
console.log(`${event[0].event_id} | ${event[1][0][0].type} | ${event[0].sdk?.name}\n\n`); | ||
const lines = formatEnvelope(envelope); | ||
if (lines.length > 0) { | ||
console.log(lines.join("")); | ||
} else { | ||
console.log("No parser for the given event type"); | ||
} | ||
console.log("\n"); | ||
console.log(SEPARATOR); | ||
} | ||
|
||
const printHelp = () => { | ||
console.log(` | ||
Spotlight Sidecar - Development proxy server for Spotlight | ||
|
||
Usage: spotlight-sidecar [options] | ||
|
||
Options: | ||
-p, --port <port> Port to listen on (default: 8969) | ||
--stdio-mcp Enable MCP stdio transport | ||
-d, --debug Enable debug logging | ||
-h, --help Show this help message | ||
|
||
|
@@ -21,9 +46,97 @@ Examples: | |
spotlight-sidecar -p 3000 -d # Start on port 3000 with debug logging | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential bug: The code does not validate the result of
|
||
`); | ||
process.exit(0); | ||
}; | ||
|
||
let runServer = true; | ||
const args = parseCLIArgs(); | ||
let stdioMCP = false; | ||
|
||
if (args.help) { | ||
runServer = false; | ||
printHelp(); | ||
} | ||
|
||
await setupSidecar({ | ||
...args, | ||
isStandalone: true, | ||
let onEnvelope: ((envelope: EventContainer) => void) | undefined = undefined; | ||
const NAME_TO_TYPE_MAPPING: Record<string, string[]> = Object.freeze({ | ||
traces: ["transaction", "span"], | ||
profiles: ["profile"], | ||
logs: ["log"], | ||
attachments: ["attachment"], | ||
errors: ["event"], | ||
sessions: ["session"], | ||
replays: ["replay_video"], | ||
// client_report | ||
}); | ||
const EVERYTHING_MAGIC_WORDS = new Set(["everything", "all", "*"]); | ||
export const SUPPORTED_ARGS = new Set([...Object.keys(NAME_TO_TYPE_MAPPING), ...EVERYTHING_MAGIC_WORDS]); | ||
|
||
const cmd = args._positionals[0]; | ||
switch (cmd) { | ||
case "help": | ||
printHelp(); | ||
runServer = false; | ||
break; | ||
case "mcp": | ||
stdioMCP = true; | ||
break; | ||
case "run": | ||
// do crazy stuff | ||
break; | ||
case undefined: | ||
case "": | ||
break; | ||
default: { | ||
if (args._positionals.length > 1) { | ||
console.error("Error: Too many positional arguments."); | ||
printHelp(); | ||
} | ||
const eventTypes = cmd.toLowerCase().split(/\s*[,+]\s*/gi); | ||
betegon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
for (const eventType of eventTypes) { | ||
if (!SUPPORTED_ARGS.has(eventType)) { | ||
console.error(`Error: Unsupported argument "${eventType}".`); | ||
console.error(`Supported arguments are: ${[...SUPPORTED_ARGS].join(", ")}`); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
if (eventTypes.some(type => EVERYTHING_MAGIC_WORDS.has(type))) { | ||
onEnvelope = displayEnvelope; | ||
} else { | ||
const types = new Set([...eventTypes.flatMap(type => NAME_TO_TYPE_MAPPING[type] || [])]); | ||
onEnvelope = envelope => { | ||
const { event } = envelope.getParsedEnvelope(); | ||
for (const [header] of event[1]) { | ||
if (header.type && types.has(header.type)) { | ||
displayEnvelope(envelope); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
// try to connect to an already existing server first | ||
betegon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try { | ||
const client = await connectUpstream(args.port); | ||
runServer = false; | ||
client.addEventListener(SENTRY_CONTENT_TYPE, event => | ||
onEnvelope!(new EventContainer(SENTRY_CONTENT_TYPE, event.data)), | ||
); | ||
} catch (err) { | ||
// if we fail, fine then we'll start our own | ||
if (err instanceof Error && !err.message?.includes(args.port.toString())) { | ||
captureException(err); | ||
console.error("Error when trying to connect to upstream sidecar:", err); | ||
process.exit(1); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Event Parsing Fails; Error Handling InvertedThe |
||
} | ||
} | ||
|
||
if (runServer) { | ||
await setupSidecar({ | ||
...args, | ||
stdioMCP, | ||
onEnvelope, | ||
isStandalone: true, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { isErrorEvent, isLogEvent } from "~/parser/index.js"; | ||
import type { EventContainer } from "~/utils/index.js"; | ||
import { processErrorEvent } from "./errors.js"; | ||
import { formatEventOutput } from "./event.js"; | ||
import { processLogEvent } from "./logs.js"; | ||
|
||
export const eventHanlers = { | ||
error: payload => formatEventOutput(processErrorEvent(payload)), | ||
log: payload => { | ||
const content: string[] = []; | ||
for (const log of payload.items) { | ||
content.push(processLogEvent(log)); | ||
} | ||
return content; | ||
}, | ||
}; | ||
|
||
export function formatEnvelope(container: EventContainer) { | ||
const processedEnvelope = container.getParsedEnvelope(); | ||
|
||
const { | ||
event: [, items], | ||
} = processedEnvelope; | ||
|
||
const formatted: string[] = []; | ||
for (const item of items) { | ||
const [{ type }, payload] = item; | ||
|
||
if (type === "event" && isErrorEvent(payload)) { | ||
formatted.push(eventHanlers.error(payload)); | ||
} else if (type === "log" && isLogEvent(payload)) { | ||
formatted.push(...eventHanlers.log(payload)); | ||
} | ||
} | ||
|
||
return formatted; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Envelope Parsing Error: Missing Existence Checks
Accessing
event[1][0][0].type
lacks checks forevent[1]
orevent[1][0]
being undefined or empty. This can lead to runtime errors if theParsedEnvelope
structure is malformed.