Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

github.com/suraciii/debug-skill

Package Overview
Dependencies
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

github.com/suraciii/debug-skill - go Package Compare versions

Comparing version
v0.2.0
to
v0.2.1
+95
-204
backend.go

@@ -6,2 +6,3 @@ package dap

"fmt"
"io"
"os"

@@ -15,2 +16,43 @@ "os/exec"

// waitForReady scans pipe lines for readyString, extracts an address via parseAddr,
// and kills cmd on timeout (10s) or early exit. Caller must have already started cmd.
func waitForReady(cmd *exec.Cmd, pipe io.ReadCloser, readyString string, parseAddr func(line string) string) (string, error) {
scanner := bufio.NewScanner(pipe)
addrCh := make(chan string, 1)
go func() {
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, readyString) {
addrCh <- parseAddr(line)
for scanner.Scan() {
}
return
}
}
close(addrCh)
}()
select {
case addr, ok := <-addrCh:
if !ok || addr == "" {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return "", fmt.Errorf("process exited without reporting listen address")
}
return addr, nil
case <-time.After(10 * time.Second):
_ = cmd.Process.Kill()
_ = cmd.Wait()
return "", fmt.Errorf("process did not start within 10s")
}
}
// normalizePort ensures port has a ":" prefix and returns both forms.
func normalizePort(port string) (withColon, bare string) {
if !strings.HasPrefix(port, ":") {
port = ":" + port
}
return port, strings.TrimPrefix(port, ":")
}
// Backend abstracts the debugger-specific logic for spawning a DAP server

@@ -23,3 +65,2 @@ // and building launch/attach argument maps.

LaunchArgs(program string, stopOnEntry bool, args []string) (launchArgs map[string]any, cleanup func(), err error)
AttachArgs(pid int) (map[string]any, error)
RemoteAttachArgs(host string, port int) (map[string]any, error)

@@ -68,13 +109,7 @@ // StopOnEntryBreakpoint returns a function name to use as a breakpoint

func (b *debugpyBackend) Spawn(port string) (*exec.Cmd, string, error) {
if !strings.HasPrefix(port, ":") {
port = ":" + port
}
actualPort := strings.TrimPrefix(port, ":")
_, actualPort := normalizePort(port)
// Use debugpy.adapter in debugServer mode (standalone DAP server).
// This is how VS Code launches debugpy - it listens for DAP connections on TCP.
cmd := exec.Command("python3", "-m", "debugpy.adapter", "--host", "127.0.0.1", "--port", actualPort, "--log-stderr")
cmd.Stdout = nil
// Capture stderr to detect readiness ("Listening for incoming Client connections")
stderrPipe, err := cmd.StderrPipe()

@@ -84,38 +119,13 @@ if err != nil {

}
if err := cmd.Start(); err != nil {
return nil, "", fmt.Errorf("starting debugpy adapter: %w", err)
return nil, "", fmt.Errorf("starting debugpy: %w", err)
}
// Wait for "Listening" message on stderr
scanner := bufio.NewScanner(stderrPipe)
ready := make(chan struct{})
go func() {
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "Listening") {
close(ready)
// Keep draining to avoid blocking the adapter
for scanner.Scan() {
}
return
}
}
// If scanner ends without finding "Listening", close ready anyway
select {
case <-ready:
default:
close(ready)
}
}()
select {
case <-ready:
case <-time.After(10 * time.Second):
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("debugpy adapter did not start within 10s")
addr, err := waitForReady(cmd, stderrPipe, "Listening", func(string) string {
return "127.0.0.1:" + actualPort
})
if err != nil {
return nil, "", fmt.Errorf("starting debugpy: %w", err)
}
return cmd, "127.0.0.1:" + actualPort, nil
return cmd, addr, nil
}

@@ -147,9 +157,2 @@

func (b *debugpyBackend) AttachArgs(pid int) (map[string]any, error) {
return map[string]any{
"request": "attach",
"processId": pid,
}, nil
}
func (b *debugpyBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) {

@@ -173,5 +176,3 @@ return map[string]any{

if !strings.HasPrefix(port, ":") {
port = ":" + port
}
port, _ = normalizePort(port)

@@ -181,3 +182,2 @@ cmd := exec.Command("dlv", "dap", "--listen", port)

// Capture stdout to detect "DAP server listening at:"
stdoutPipe, err := cmd.StdoutPipe()

@@ -187,3 +187,2 @@ if err != nil {

}
if err := cmd.Start(); err != nil {

@@ -193,38 +192,14 @@ return nil, "", fmt.Errorf("starting dlv: %w", err)

// Parse listen address from stdout
scanner := bufio.NewScanner(stdoutPipe)
addrCh := make(chan string, 1)
go func() {
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "DAP server listening at:") {
// Format: "DAP server listening at: [::]:PORT" or "DAP server listening at: 127.0.0.1:PORT"
idx := strings.Index(line, "DAP server listening at:")
addr := strings.TrimSpace(line[idx+len("DAP server listening at:"):])
// Normalize [::]:PORT to 127.0.0.1:PORT
if strings.HasPrefix(addr, "[::]") {
addr = "127.0.0.1" + addr[4:]
}
addrCh <- addr
for scanner.Scan() {
}
return
}
addr, err := waitForReady(cmd, stdoutPipe, "DAP server listening at:", func(line string) string {
idx := strings.Index(line, "DAP server listening at:")
a := strings.TrimSpace(line[idx+len("DAP server listening at:"):])
if strings.HasPrefix(a, "[::]") {
a = "127.0.0.1" + a[4:]
}
close(addrCh)
}()
select {
case addr, ok := <-addrCh:
if !ok || addr == "" {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("dlv exited without reporting listen address")
}
return cmd, addr, nil
case <-time.After(10 * time.Second):
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("dlv did not start within 10s")
return a
})
if err != nil {
return nil, "", fmt.Errorf("starting dlv: %w", err)
}
return cmd, addr, nil
}

@@ -285,10 +260,2 @@

func (b *delveBackend) AttachArgs(pid int) (map[string]any, error) {
return map[string]any{
"request": "attach",
"mode": "local",
"processId": pid,
}, nil
}
func (b *delveBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) {

@@ -382,10 +349,7 @@ return map[string]any{

if !strings.HasPrefix(port, ":") {
port = ":" + port
}
actualPort := strings.TrimPrefix(port, ":")
_, actualPort := normalizePort(port)
cmd := exec.Command(binary, "--connection", fmt.Sprintf("listen://127.0.0.1:%s", actualPort))
cmd.Stderr = nil
// Capture stdout to detect "Listening for: connection://..."
stdoutPipe, err := cmd.StdoutPipe()

@@ -395,4 +359,2 @@ if err != nil {

}
cmd.Stderr = nil
if err := cmd.Start(); err != nil {

@@ -402,41 +364,15 @@ return nil, "", fmt.Errorf("starting lldb-dap: %w", err)

// Parse listen address from stdout
scanner := bufio.NewScanner(stdoutPipe)
addrCh := make(chan string, 1)
go func() {
for scanner.Scan() {
line := scanner.Text()
// Format: "Listening for: connection://[127.0.0.1]:PORT"
if strings.Contains(line, "Listening") {
// Extract host:port from the connection URL
if idx := strings.Index(line, "connection://"); idx >= 0 {
raw := line[idx+len("connection://"):]
// Handle bracketed addresses like [127.0.0.1]:PORT
raw = strings.ReplaceAll(raw, "[", "")
raw = strings.ReplaceAll(raw, "]", "")
addrCh <- raw
} else {
addrCh <- "127.0.0.1:" + actualPort
}
for scanner.Scan() {
}
return
}
addr, err := waitForReady(cmd, stdoutPipe, "Listening", func(line string) string {
if idx := strings.Index(line, "connection://"); idx >= 0 {
raw := line[idx+len("connection://"):]
raw = strings.ReplaceAll(raw, "[", "")
raw = strings.ReplaceAll(raw, "]", "")
return raw
}
close(addrCh)
}()
select {
case addr, ok := <-addrCh:
if !ok || addr == "" {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("lldb-dap exited without reporting listen address")
}
return cmd, addr, nil
case <-time.After(10 * time.Second):
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("lldb-dap did not start within 10s")
return "127.0.0.1:" + actualPort
})
if err != nil {
return nil, "", fmt.Errorf("starting lldb-dap: %w", err)
}
return cmd, addr, nil
}

@@ -502,8 +438,2 @@

func (b *lldbBackend) AttachArgs(pid int) (map[string]any, error) {
return map[string]any{
"pid": pid,
}, nil
}
func (b *lldbBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) {

@@ -523,10 +453,7 @@ return nil, fmt.Errorf("lldb-dap does not support remote attach")

if !strings.HasPrefix(port, ":") {
port = ":" + port
}
actualPort := strings.TrimPrefix(port, ":")
_, actualPort := normalizePort(port)
cmd := exec.Command("node", serverPath, actualPort)
cmd.Stderr = nil
// Capture stdout to detect "Listening at HOST:PORT"
stdoutPipe, err := cmd.StdoutPipe()

@@ -536,4 +463,2 @@ if err != nil {

}
cmd.Stderr = nil
if err := cmd.Start(); err != nil {

@@ -543,50 +468,24 @@ return nil, "", fmt.Errorf("starting js-debug: %w", err)

scanner := bufio.NewScanner(stdoutPipe)
addrCh := make(chan string, 1)
go func() {
for scanner.Scan() {
line := scanner.Text()
// js-debug prints: "Debug server listening at HOST:PORT"
// e.g. "Debug server listening at ::1:12345"
if strings.Contains(line, "Debug server listening at") {
parts := strings.Fields(line)
addr := "127.0.0.1:" + actualPort
if len(parts) > 0 {
raw := parts[len(parts)-1]
// Extract port from the last colon (handles IPv6 like "::1:PORT")
if idx := strings.LastIndex(raw, ":"); idx >= 0 {
port := raw[idx+1:]
host := raw[:idx]
// Normalize: use the original host but bracket IPv6
if strings.Contains(host, ":") {
addr = "[" + host + "]:" + port
} else if host == "" {
addr = "127.0.0.1:" + port
} else {
addr = host + ":" + port
}
}
}
addrCh <- addr
for scanner.Scan() {
}
return
addr, err := waitForReady(cmd, stdoutPipe, "Debug server listening at", func(line string) string {
parts := strings.Fields(line)
if len(parts) == 0 {
return "127.0.0.1:" + actualPort
}
raw := parts[len(parts)-1]
if idx := strings.LastIndex(raw, ":"); idx >= 0 {
p := raw[idx+1:]
host := raw[:idx]
if strings.Contains(host, ":") {
return "[" + host + "]:" + p
} else if host == "" {
return "127.0.0.1:" + p
}
return host + ":" + p
}
close(addrCh)
}()
select {
case addr, ok := <-addrCh:
if !ok || addr == "" {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("js-debug exited without reporting listen address")
}
return cmd, addr, nil
case <-time.After(10 * time.Second):
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, "", fmt.Errorf("js-debug did not start within 10s")
return "127.0.0.1:" + actualPort
})
if err != nil {
return nil, "", fmt.Errorf("starting js-debug: %w", err)
}
return cmd, addr, nil
}

@@ -617,10 +516,2 @@

func (b *jsDebugBackend) AttachArgs(pid int) (map[string]any, error) {
return map[string]any{
"type": "pwa-node",
"request": "attach",
"processId": pid,
}, nil
}
func (b *jsDebugBackend) RemoteAttachArgs(host string, port int) (map[string]any, error) {

@@ -627,0 +518,0 @@ return map[string]any{

@@ -157,3 +157,3 @@ # CLI API Reference

**Flags:**
- `--break-on-exception <filter>` — Set exception breakpoint filters (repeatable, replaces current filters)
- `--break-on-exception <filter>` — Add exception breakpoint filters (repeatable, merged with existing)

@@ -160,0 +160,0 @@ ```bash

+34
-102

@@ -125,2 +125,19 @@ package dap

// runDaemonCommand marshals args, sends command to daemon, prints response, exits on error.
func runDaemonCommand(command string, args any) error {
var rawArgs json.RawMessage
if args != nil {
rawArgs, _ = json.Marshal(args)
}
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: command, Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
}
// addBreakpointFlags registers --break, --remove-break, --break-on-exception flags on a command.

@@ -131,3 +148,3 @@ func addBreakpointFlags(cmd *cobra.Command, breaks, removeBreaks *breakpointFlag, exceptionFilters *[]string) {

cmd.Flags().StringArrayVar(exceptionFilters, "break-on-exception", nil,
"Set exception breakpoints (repeatable, replaces current).\n"+
"Add exception breakpoint filters (repeatable, merged with existing).\n"+
"Filter IDs are backend-specific (see 'dap debug --help').")

@@ -288,17 +305,6 @@ }

}
stepArgs := StepArgs{
return runDaemonCommand("step", StepArgs{
Mode: mode,
BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters),
}
rawArgs, _ := json.Marshal(stepArgs)
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "step", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
})
},

@@ -327,3 +333,3 @@ }

--remove-break removes specific breakpoints
--break-on-exception replaces exception breakpoint filters`,
--break-on-exception adds exception breakpoint filters (merged with existing)`,
Example: ` dap continue

@@ -337,15 +343,5 @@ dap continue --break app.py:42 # add a breakpoint and continue

RunE: func(cmd *cobra.Command, args []string) error {
contArgs := ContinueArgs{
return runDaemonCommand("continue", ContinueArgs{
BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters),
}
rawArgs, _ := json.Marshal(contArgs)
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "continue", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
})
},

@@ -377,16 +373,6 @@ }

RunE: func(cmd *cobra.Command, args []string) error {
ctxArgs := ContextArgs{
return runDaemonCommand("context", ContextArgs{
Frame: frame,
BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters),
}
rawArgs, _ := json.Marshal(ctxArgs)
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "context", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
})
},

@@ -419,17 +405,7 @@ }

RunE: func(cmd *cobra.Command, args []string) error {
evalArgs := EvalArgs{
return runDaemonCommand("eval", EvalArgs{
Expression: args[0],
Frame: frame,
BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters),
}
rawArgs, _ := json.Marshal(evalArgs)
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "eval", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
})
},

@@ -461,15 +437,5 @@ }

RunE: func(cmd *cobra.Command, args []string) error {
outArgs := OutputArgs{
return runDaemonCommand("output", OutputArgs{
BreakpointUpdates: breakpointUpdatesFromFlags(breaks, removeBreaks, exceptionFilters),
}
rawArgs, _ := json.Marshal(outArgs)
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "output", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
})
},

@@ -496,11 +462,3 @@ }

RunE: func(cmd *cobra.Command, args []string) error {
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_list"})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
return runDaemonCommand("break_list", nil)
},

@@ -537,15 +495,6 @@ }

}
rawArgs, _ := json.Marshal(BreakAddArgs{
return runDaemonCommand("break_add", BreakAddArgs{
Breaks: allBreaks,
ExceptionFilters: addExceptionFilters,
})
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_add", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
},

@@ -555,3 +504,3 @@ }

addCmd.Flags().StringArrayVar(&addExceptionFilters, "break-on-exception", nil,
"Set exception breakpoint filters (repeatable, replaces current filters). Filter IDs are backend-specific (see 'dap debug --help').")
"Add exception breakpoint filters (repeatable, merged with existing). Filter IDs are backend-specific (see 'dap debug --help').")

@@ -586,15 +535,6 @@ // break remove

}
rawArgs, _ := json.Marshal(BreakRemoveArgs{
return runDaemonCommand("break_remove", BreakRemoveArgs{
Breaks: allBreaks,
ExceptionFilters: rmExceptionFilters,
})
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_remove", Args: rawArgs})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
},

@@ -612,11 +552,3 @@ }

RunE: func(cmd *cobra.Command, args []string) error {
resp, err := SendCommand(globalFlags.socketPath, &Request{Command: "break_clear"})
if err != nil {
return noDaemonError(err)
}
fmt.Print(FormatResponse(resp, globalFlags.jsonOutput))
if resp.Status == "error" {
os.Exit(1)
}
return nil
return runDaemonCommand("break_clear", nil)
},

@@ -623,0 +555,0 @@ }

@@ -46,2 +46,10 @@ package dap

}
// Multi-byte: emoji runes should not be split
emoji := "hello🌍🌎🌏world"
got := truncateString(emoji, 8) // "hello🌍🌎🌏" (8 runes)
want := "hello🌍🌎🌏..."
if got != want {
t.Errorf("multi-byte truncated = %q, want %q", got, want)
}
}

@@ -209,8 +209,9 @@ package dap

// truncateString truncates a string to maxLen characters.
// truncateString truncates a string to maxLen runes.
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return s[:maxLen] + "..."
return string(runes[:maxLen]) + "..."
}

@@ -217,0 +218,0 @@

package dap
import (
"encoding/json"
"fmt"

@@ -250,2 +251,47 @@ "strings"

func TestRequireSession(t *testing.T) {
d := &Daemon{}
// No client — should return error response
resp := d.requireSession()
if resp == nil {
t.Fatal("expected error response when client is nil")
}
if resp.Status != "error" {
t.Errorf("expected status error, got %q", resp.Status)
}
if !strings.Contains(resp.Error, "no active debug session") {
t.Errorf("expected 'no active debug session' in error, got %q", resp.Error)
}
}
func TestMalformedJSONArgs(t *testing.T) {
d := &Daemon{}
// Set a fake client so requireSession passes
d.client = &DAPClient{}
malformed := json.RawMessage(`{invalid`)
tests := []struct {
name string
handler func(json.RawMessage) *Response
}{
{"handleStep", d.handleStep},
{"handleContinue", d.handleContinue},
{"handleContext", d.handleContext},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := tt.handler(malformed)
if resp.Status != "error" {
t.Errorf("expected error status, got %q", resp.Status)
}
if !strings.Contains(resp.Error, "invalid args") {
t.Errorf("expected 'invalid args' in error, got %q", resp.Error)
}
})
}
}
func TestMergeBreakpoints(t *testing.T) {

@@ -252,0 +298,0 @@ tests := []struct {

+166
-94

@@ -23,8 +23,11 @@ package dap

// DefaultSocketDir is the directory for the daemon socket.
var DefaultSocketDir = filepath.Join(os.Getenv("HOME"), ".dap-cli")
// defaultSocketDir returns the directory for the daemon socket.
func defaultSocketDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".dap-cli")
}
// SessionSocketPath returns the socket path for a named session.
func SessionSocketPath(name string) string {
return filepath.Join(DefaultSocketDir, name+".sock")
return filepath.Join(defaultSocketDir(), name+".sock")
}

@@ -39,4 +42,6 @@

type Daemon struct {
client *DAPClient
backend Backend
clientMu sync.Mutex // guards d.client pointer; never held during I/O
client *DAPClient
backend Backend
adapterCmd *exec.Cmd

@@ -71,2 +76,4 @@

// (single-threaded dispatch via Serve) — no mutex needed.
// d.client is guarded by clientMu for pointer swaps (readLoop↔handlers); once
// requireSession passes, handlers use d.client directly (sequential dispatch).
sessionBreaks []Breakpoint // stored breakpoints for child session re-init

@@ -86,2 +93,31 @@ sessionExceptionFilters []string // stored exception filter IDs for child session re-init

const errNoSession = "no active debug session (program may have terminated) — run 'dap debug' to start a new session"
func errResponse(msg string) *Response {
return &Response{Status: "error", Error: msg}
}
func errResponsef(format string, args ...any) *Response {
return &Response{Status: "error", Error: fmt.Sprintf(format, args...)}
}
func (d *Daemon) requireSession() *Response {
if d.getClient() == nil {
return errResponse(errNoSession)
}
return nil
}
func (d *Daemon) getClient() *DAPClient {
d.clientMu.Lock()
defer d.clientMu.Unlock()
return d.client
}
func (d *Daemon) setClient(c *DAPClient) {
d.clientMu.Lock()
d.client = c
d.clientMu.Unlock()
}
func idleTimeout() time.Duration {

@@ -417,3 +453,3 @@ if v := os.Getenv("DAP_IDLE_TIMEOUT"); v != "" {

default:
return &Response{Status: "error", Error: fmt.Sprintf("unknown command: %s", req.Command)}
return errResponsef("unknown command: %s", req.Command)
}

@@ -425,3 +461,3 @@ }

if err := json.Unmarshal(rawArgs, &args); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)}
return errResponsef("invalid args: %v", err)
}

@@ -435,3 +471,3 @@

if err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -441,7 +477,8 @@ } else if args.Script != "" {

} else if args.Attach != "" {
return &Response{Status: "error", Error: "backend required for remote attach (e.g. --backend debugpy)"}
return errResponse("backend required for remote attach (e.g. --backend debugpy)")
} else {
return &Response{Status: "error", Error: "script path or --attach required"}
return errResponse("script path or --attach required")
}
d.backend = backend
d.stopSession() // clean up any previous session

@@ -454,5 +491,5 @@ isRemote := args.Attach != ""

if err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("connecting to %s: %v", args.Attach, err)}
return errResponsef("connecting to %s: %v", args.Attach, err)
}
d.client = client
d.setClient(client)
d.expectCh = make(chan godap.Message, 64)

@@ -462,3 +499,3 @@ go d.readLoop()

if err := d.initializeDAP(backend); err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -469,3 +506,3 @@

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("invalid attach address %q: %v", args.Attach, splitErr)}
return errResponsef("invalid attach address %q: %v", args.Attach, splitErr)
}

@@ -476,7 +513,7 @@ remotePort, _ := strconv.Atoi(portStr)

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("preparing attach: %v", err)}
return errResponsef("preparing attach: %v", err)
}
if err := d.client.AttachRequestWithArgs(attachArgs); err != nil {
d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("attach: %v", err)}
return errResponsef("attach: %v", err)
}

@@ -486,6 +523,6 @@ } else {

if err := d.startAdapter(backend); err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}
if err := d.initializeDAP(backend); err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -496,3 +533,3 @@

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("preparing launch: %v", err)}
return errResponsef("preparing launch: %v", err)
}

@@ -502,3 +539,3 @@ d.cleanupFn = cleanupFn

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("launch: %v", err)}
return errResponsef("launch: %v", err)
}

@@ -514,3 +551,3 @@ }

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("waiting for initialized: %v", err)}
return errResponsef("waiting for initialized: %v", err)
}

@@ -524,10 +561,10 @@ switch m := msg.(type) {

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("request failed: %s", errMsg)}
return errResponsef("request failed: %s", errMsg)
case *godap.ExitedEvent, *godap.TerminatedEvent:
d.stopSession()
return &Response{Status: "error", Error: "debug adapter exited during initialization — check that the program path is valid and the debugger is installed"}
return errResponse("debug adapter exited during initialization — check that the program path is valid and the debugger is installed")
case godap.ResponseMessage:
if !m.GetResponse().Success {
d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("request failed: %s", m.GetResponse().Message)}
return errResponsef("request failed: %s", m.GetResponse().Message)
}

@@ -546,3 +583,3 @@ case *godap.InitializedEvent:

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("set entry breakpoint: %v", err)}
return errResponsef("set entry breakpoint: %v", err)
}

@@ -557,3 +594,3 @@ }

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)}
return errResponsef("set breakpoints: %v", err)
}

@@ -567,7 +604,7 @@ }

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)}
return errResponsef("set exception breakpoints: %v", err)
}
if err := d.client.ConfigurationDoneRequest(); err != nil {
d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("finalizing debug setup: %v", err)}
return errResponsef("finalizing debug setup: %v", err)
}

@@ -580,3 +617,3 @@

d.stopSession()
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -594,3 +631,3 @@ if ctx.Location == nil {

d.stopSession()
return &Response{Status: "error", Error: fmt.Sprintf("continue past entry: %v", err)}
return errResponsef("continue past entry: %v", err)
}

@@ -610,13 +647,18 @@ continue

if err := d.updateBreakpoints(bu.Breaks, bu.RemoveBreaks); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)}
return errResponsef("set breakpoints: %v", err)
}
}
// ExceptionFilters on inline commands (--break-on-exception) replaces all current filters,
// unlike handleBreakAdd which merges additively. This matches the CLI flag semantics
// documented in the API: "replaces current filters".
if bu.ExceptionFilters != nil {
d.sessionExceptionFilters = bu.ExceptionFilters
if err := d.client.SetExceptionBreakpointsRequest(bu.ExceptionFilters); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)}
if len(bu.ExceptionFilters) > 0 {
existing := make(map[string]bool)
for _, f := range d.sessionExceptionFilters {
existing[f] = true
}
for _, f := range bu.ExceptionFilters {
if !existing[f] {
d.sessionExceptionFilters = append(d.sessionExceptionFilters, f)
}
}
if err := d.client.SetExceptionBreakpointsRequest(d.sessionExceptionFilters); err != nil {
return errResponsef("set exception breakpoints: %v", err)
}
}

@@ -627,4 +669,4 @@ return nil

func (d *Daemon) handleStep(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -634,3 +676,5 @@

if rawArgs != nil {
_ = json.Unmarshal(rawArgs, &args)
if err := json.Unmarshal(rawArgs, &args); err != nil {
return errResponsef("invalid args: %v", err)
}
}

@@ -650,14 +694,14 @@ if args.Mode == "" {

if err := d.client.NextRequest(threadID); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("step over: %v", err)}
return errResponsef("step over: %v", err)
}
case "in":
if err := d.client.StepInRequest(threadID); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("step in: %v", err)}
return errResponsef("step in: %v", err)
}
case "out":
if err := d.client.StepOutRequest(threadID); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("step out: %v", err)}
return errResponsef("step out: %v", err)
}
default:
return &Response{Status: "error", Error: fmt.Sprintf("invalid step mode %q — use: in, out, over", args.Mode)}
return errResponsef("invalid step mode %q — use: in, out, over", args.Mode)
}

@@ -669,4 +713,4 @@

func (d *Daemon) handleContinue(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -676,3 +720,5 @@

if rawArgs != nil {
_ = json.Unmarshal(rawArgs, &args)
if err := json.Unmarshal(rawArgs, &args); err != nil {
return errResponsef("invalid args: %v", err)
}
}

@@ -687,3 +733,3 @@

if err := d.client.ContinueRequest(threadID); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("continue: %v", err)}
return errResponsef("continue: %v", err)
}

@@ -695,4 +741,4 @@

func (d *Daemon) handleContext(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -702,3 +748,5 @@

if rawArgs != nil {
_ = json.Unmarshal(rawArgs, &args)
if err := json.Unmarshal(rawArgs, &args); err != nil {
return errResponsef("invalid args: %v", err)
}
}

@@ -714,3 +762,3 @@

if err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -721,4 +769,4 @@ return &Response{Status: "stopped", Data: ctx}

func (d *Daemon) handleEval(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -728,3 +776,3 @@

if err := json.Unmarshal(rawArgs, &args); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)}
return errResponsef("invalid args: %v", err)
}

@@ -739,3 +787,3 @@

if args.Frame >= len(d.frameIDs) {
return &Response{Status: "error", Error: fmt.Sprintf("frame %d out of range (stack has %d frames)", args.Frame, len(d.frameIDs))}
return errResponsef("frame %d out of range (stack has %d frames)", args.Frame, len(d.frameIDs))
}

@@ -746,3 +794,3 @@ frameID = d.frameIDs[args.Frame]

if err := d.client.EvaluateRequest(args.Expression, frameID, "repl"); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("evaluate: %v", err)}
return errResponsef("evaluate: %v", err)
}

@@ -753,3 +801,3 @@

if err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("reading eval response: %v", err)}
return errResponsef("reading eval response: %v", err)
}

@@ -759,3 +807,3 @@ switch resp := msg.(type) {

if !resp.Success {
return &Response{Status: "error", Error: fmt.Sprintf("eval failed: %s", resp.Message)}
return errResponsef("eval failed: %s", resp.Message)
}

@@ -772,6 +820,6 @@ return &Response{

case *godap.ExitedEvent, *godap.TerminatedEvent:
return &Response{Status: "error", Error: "program terminated during evaluation"}
return errResponse("program terminated during evaluation")
case godap.ResponseMessage:
if !resp.GetResponse().Success {
return &Response{Status: "error", Error: fmt.Sprintf("eval failed: %s", resp.GetResponse().Message)}
return errResponsef("eval failed: %s", resp.GetResponse().Message)
}

@@ -784,6 +832,8 @@ return &Response{Status: "ok", Data: &ContextResult{EvalResult: &EvalResult{Value: "(no result)"}}}

func (d *Daemon) handleOutput(rawArgs json.RawMessage) *Response {
if d.client != nil {
if d.getClient() != nil {
var args OutputArgs
if rawArgs != nil {
_ = json.Unmarshal(rawArgs, &args)
if err := json.Unmarshal(rawArgs, &args); err != nil {
return errResponsef("invalid args: %v", err)
}
}

@@ -802,4 +852,4 @@ if errResp := d.applyBreakpointUpdates(args.BreakpointUpdates); errResp != nil {

func (d *Daemon) handleBreakList() *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -818,4 +868,4 @@ breaks := make([]Breakpoint, len(d.sessionBreaks))

func (d *Daemon) handleBreakAdd(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -825,3 +875,3 @@

if err := json.Unmarshal(rawArgs, &args); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)}
return errResponsef("invalid args: %v", err)
}

@@ -831,3 +881,3 @@

if err := d.updateBreakpoints(args.Breaks, nil); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("set breakpoints: %v", err)}
return errResponsef("set breakpoints: %v", err)
}

@@ -837,6 +887,14 @@ }

if len(args.ExceptionFilters) > 0 {
d.sessionExceptionFilters = args.ExceptionFilters
if err := d.client.SetExceptionBreakpointsRequest(args.ExceptionFilters); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)}
existing := make(map[string]bool)
for _, f := range d.sessionExceptionFilters {
existing[f] = true
}
for _, f := range args.ExceptionFilters {
if !existing[f] {
d.sessionExceptionFilters = append(d.sessionExceptionFilters, f)
}
}
if err := d.client.SetExceptionBreakpointsRequest(d.sessionExceptionFilters); err != nil {
return errResponsef("set exception breakpoints: %v", err)
}
}

@@ -848,4 +906,4 @@

func (d *Daemon) handleBreakRemove(rawArgs json.RawMessage) *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -855,3 +913,3 @@

if err := json.Unmarshal(rawArgs, &args); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("invalid args: %v", err)}
return errResponsef("invalid args: %v", err)
}

@@ -861,3 +919,3 @@

if err := d.updateBreakpoints(nil, args.Breaks); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("remove breakpoints: %v", err)}
return errResponsef("remove breakpoints: %v", err)
}

@@ -883,3 +941,3 @@ }

if err := d.client.SetExceptionBreakpointsRequest(filters); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("set exception breakpoints: %v", err)}
return errResponsef("set exception breakpoints: %v", err)
}

@@ -892,4 +950,4 @@ }

func (d *Daemon) handleBreakClear() *Response {
if d.client == nil {
return &Response{Status: "error", Error: "no active debug session (program may have terminated) — run 'dap debug' to start a new session"}
if resp := d.requireSession(); resp != nil {
return resp
}

@@ -904,3 +962,3 @@

if err := d.client.SetBreakpointsRequest(file, nil); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("clear breakpoints: %v", err)}
return errResponsef("clear breakpoints: %v", err)
}

@@ -913,3 +971,3 @@ }

if err := d.client.SetExceptionBreakpointsRequest([]string{}); err != nil {
return &Response{Status: "error", Error: fmt.Sprintf("clear exception breakpoints: %v", err)}
return errResponsef("clear exception breakpoints: %v", err)
}

@@ -932,6 +990,9 @@

func (d *Daemon) stopSession() {
if d.client != nil {
_ = d.client.DisconnectRequest(true)
d.client.Close()
d.client = nil
d.clientMu.Lock()
c := d.client
d.client = nil
d.clientMu.Unlock()
if c != nil {
_ = c.DisconnectRequest(true)
c.Close()
}

@@ -974,3 +1035,3 @@ if d.adapterCmd != nil && d.adapterCmd.Process != nil {

}
d.client = client
d.setClient(client)
d.expectCh = make(chan godap.Message, 64)

@@ -1017,3 +1078,6 @@ go d.readLoop()

// Initialize child
_ = childClient.InitializeRequest(d.backend.AdapterID())
if err := childClient.InitializeRequest(d.backend.AdapterID()); err != nil {
childClient.Close()
return fmt.Errorf("child initialize: %w", err)
}
for {

@@ -1031,3 +1095,5 @@ cmsg, cerr := childClient.ReadMessage()

// Launch child with config from startDebugging
_ = childClient.LaunchRequestWithArgs(config)
if err := childClient.LaunchRequestWithArgs(config); err != nil {
log.Printf("child launch: %v", err)
}

@@ -1051,3 +1117,5 @@ // Read until InitializedEvent (child may send it immediately)

d.recordRequestedBreaks(file, bps)
_ = childClient.SetBreakpointsRequest(file, bps)
if err := childClient.SetBreakpointsRequest(file, bps); err != nil {
log.Printf("child set breakpoints: %v", err)
}
}

@@ -1058,7 +1126,11 @@ childExceptionFilters := d.sessionExceptionFilters

}
_ = childClient.SetExceptionBreakpointsRequest(childExceptionFilters)
_ = childClient.ConfigurationDoneRequest()
if err := childClient.SetExceptionBreakpointsRequest(childExceptionFilters); err != nil {
log.Printf("child set exception breakpoints: %v", err)
}
if err := childClient.ConfigurationDoneRequest(); err != nil {
log.Printf("child configuration done: %v", err)
}
// Swap to child session — readLoop continues reading from child
d.client = childClient
d.setClient(childClient)
return nil

@@ -1080,3 +1152,3 @@ }

if err != nil {
return &Response{Status: "error", Error: err.Error()}
return errResponse(err.Error())
}

@@ -1083,0 +1155,0 @@ if ctx.Location == nil {

@@ -105,3 +105,3 @@ package dap

RemoveBreaks []Breakpoint `json:"remove_breaks,omitempty"` // breakpoints to remove
ExceptionFilters []string `json:"exception_filters,omitempty"` // backend-specific filter IDs (replaces current)
ExceptionFilters []string `json:"exception_filters,omitempty"` // backend-specific filter IDs (additive, merged with existing)
}

@@ -108,0 +108,0 @@