@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
3
|
+
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
|
+
import type {
|
|
5
|
+
DapCapabilities,
|
|
6
|
+
DapClientState,
|
|
7
|
+
DapEventMessage,
|
|
8
|
+
DapInitializeArguments,
|
|
9
|
+
DapPendingRequest,
|
|
10
|
+
DapRequestMessage,
|
|
11
|
+
DapResolvedAdapter,
|
|
12
|
+
DapResponseMessage,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
interface DapSpawnOptions {
|
|
16
|
+
adapter: DapResolvedAdapter;
|
|
17
|
+
cwd: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Minimal write interface shared by Bun.FileSink and Bun TCP sockets. */
|
|
21
|
+
interface DapWriteSink {
|
|
22
|
+
write(data: string | Uint8Array): number | Promise<number>;
|
|
23
|
+
flush(): number | Promise<number> | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type DapEventHandler = (body: unknown, event: DapEventMessage) => void | Promise<void>;
|
|
27
|
+
type DapReverseRequestHandler = (args: unknown) => unknown | Promise<unknown>;
|
|
28
|
+
|
|
29
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
function findHeaderEnd(buffer: Uint8Array): number {
|
|
32
|
+
for (let index = 0; index < buffer.length - 3; index += 1) {
|
|
33
|
+
if (buffer[index] === 13 && buffer[index + 1] === 10 && buffer[index + 2] === 13 && buffer[index + 3] === 10) {
|
|
34
|
+
return index;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseMessage(
|
|
41
|
+
buffer: Buffer,
|
|
42
|
+
): { message: DapResponseMessage | DapEventMessage | DapRequestMessage; remaining: Buffer } | null {
|
|
43
|
+
const headerEndIndex = findHeaderEnd(buffer);
|
|
44
|
+
if (headerEndIndex === -1) return null;
|
|
45
|
+
const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
|
|
46
|
+
const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
|
|
47
|
+
if (!contentLengthMatch) return null;
|
|
48
|
+
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
|
|
49
|
+
const messageStart = headerEndIndex + 4;
|
|
50
|
+
const messageEnd = messageStart + contentLength;
|
|
51
|
+
if (buffer.length < messageEnd) return null;
|
|
52
|
+
const messageText = new TextDecoder().decode(buffer.subarray(messageStart, messageEnd));
|
|
53
|
+
return {
|
|
54
|
+
message: JSON.parse(messageText) as DapResponseMessage | DapEventMessage | DapRequestMessage,
|
|
55
|
+
remaining: buffer.subarray(messageEnd),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function writeMessage(sink: DapWriteSink, message: DapRequestMessage | DapResponseMessage): Promise<void> {
|
|
60
|
+
const content = JSON.stringify(message);
|
|
61
|
+
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
62
|
+
sink.write(content);
|
|
63
|
+
await sink.flush();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toErrorMessage(value: unknown): string {
|
|
67
|
+
if (value instanceof Error) return value.message;
|
|
68
|
+
return String(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class DapClient {
|
|
72
|
+
readonly adapter: DapResolvedAdapter;
|
|
73
|
+
readonly cwd: string;
|
|
74
|
+
readonly proc: DapClientState["proc"];
|
|
75
|
+
/** ReadableStream of DAP bytes — from proc.stdout (stdio) or a socket (socket mode). */
|
|
76
|
+
readonly #readable: ReadableStream<Uint8Array>;
|
|
77
|
+
/** Write sink — proc.stdin (stdio) or a socket (socket mode). */
|
|
78
|
+
readonly #writeSink: DapWriteSink;
|
|
79
|
+
/** Optional socket to close on dispose (socket mode only). */
|
|
80
|
+
readonly #socket?: { end(): void };
|
|
81
|
+
#requestSeq = 0;
|
|
82
|
+
#pendingRequests = new Map<number, DapPendingRequest>();
|
|
83
|
+
#messageBuffer = Buffer.alloc(0);
|
|
84
|
+
#isReading = false;
|
|
85
|
+
#disposed = false;
|
|
86
|
+
#lastActivity = Date.now();
|
|
87
|
+
#capabilities?: DapCapabilities;
|
|
88
|
+
#eventHandlers = new Map<string, Set<DapEventHandler>>();
|
|
89
|
+
#anyEventHandlers = new Set<DapEventHandler>();
|
|
90
|
+
#reverseRequestHandlers = new Map<string, DapReverseRequestHandler>();
|
|
91
|
+
|
|
92
|
+
constructor(
|
|
93
|
+
adapter: DapResolvedAdapter,
|
|
94
|
+
cwd: string,
|
|
95
|
+
proc: DapClientState["proc"],
|
|
96
|
+
options?: { readable?: ReadableStream<Uint8Array>; writeSink?: DapWriteSink; socket?: { end(): void } },
|
|
97
|
+
) {
|
|
98
|
+
this.adapter = adapter;
|
|
99
|
+
this.cwd = cwd;
|
|
100
|
+
this.proc = proc;
|
|
101
|
+
this.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
|
|
102
|
+
this.#writeSink = options?.writeSink ?? proc.stdin;
|
|
103
|
+
this.#socket = options?.socket;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static async spawn({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
107
|
+
if (adapter.connectMode === "socket") {
|
|
108
|
+
return DapClient.#spawnSocket({ adapter, cwd });
|
|
109
|
+
}
|
|
110
|
+
// Merge non-interactive env and start in a new session (detached → setsid)
|
|
111
|
+
// so the adapter process tree has no controlling terminal. Without this,
|
|
112
|
+
// debuggee children can reach /dev/tty and trigger SIGTTIN, suspending
|
|
113
|
+
// the parent harness under shell job control.
|
|
114
|
+
const env = {
|
|
115
|
+
...Bun.env,
|
|
116
|
+
...NON_INTERACTIVE_ENV,
|
|
117
|
+
};
|
|
118
|
+
const proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args], {
|
|
119
|
+
cwd,
|
|
120
|
+
stdin: "pipe",
|
|
121
|
+
env,
|
|
122
|
+
detached: true,
|
|
123
|
+
});
|
|
124
|
+
const client = new DapClient(adapter, cwd, proc);
|
|
125
|
+
proc.exited.then(() => {
|
|
126
|
+
client.#handleProcessExit();
|
|
127
|
+
});
|
|
128
|
+
void client.#startMessageReader();
|
|
129
|
+
return client;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Spawn a socket-mode adapter (e.g. dlv).
|
|
134
|
+
* Linux: connect to a unix domain socket via --listen=unix:<path>
|
|
135
|
+
* macOS/other: the adapter dials into our TCP listener via --client-addr
|
|
136
|
+
*/
|
|
137
|
+
static async #spawnSocket({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
138
|
+
const env = {
|
|
139
|
+
...Bun.env,
|
|
140
|
+
...NON_INTERACTIVE_ENV,
|
|
141
|
+
};
|
|
142
|
+
const isLinux = process.platform === "linux";
|
|
143
|
+
|
|
144
|
+
if (isLinux) {
|
|
145
|
+
return DapClient.#spawnSocketUnix({ adapter, cwd, env });
|
|
146
|
+
}
|
|
147
|
+
return DapClient.#spawnSocketClientAddr({ adapter, cwd, env });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Linux: spawn adapter with --listen=unix:<path>, then connect to the socket. */
|
|
151
|
+
static async #spawnSocketUnix({
|
|
152
|
+
adapter,
|
|
153
|
+
cwd,
|
|
154
|
+
env,
|
|
155
|
+
}: {
|
|
156
|
+
adapter: DapResolvedAdapter;
|
|
157
|
+
cwd: string;
|
|
158
|
+
env: Record<string, string | undefined>;
|
|
159
|
+
}): Promise<DapClient> {
|
|
160
|
+
const socketPath = `/tmp/dap-${adapter.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`;
|
|
161
|
+
const proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
|
|
162
|
+
cwd,
|
|
163
|
+
stdin: "pipe",
|
|
164
|
+
env,
|
|
165
|
+
detached: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Wait for the socket file to appear (dlv needs to start listening)
|
|
169
|
+
await waitForCondition(
|
|
170
|
+
() => {
|
|
171
|
+
try {
|
|
172
|
+
Bun.file(socketPath).size;
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
10_000,
|
|
179
|
+
proc,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
|
|
183
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
184
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
185
|
+
void client.#startMessageReader();
|
|
186
|
+
return client;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** macOS/other: listen on a random TCP port, spawn adapter with --client-addr, accept connection. */
|
|
190
|
+
static async #spawnSocketClientAddr({
|
|
191
|
+
adapter,
|
|
192
|
+
cwd,
|
|
193
|
+
env,
|
|
194
|
+
}: {
|
|
195
|
+
adapter: DapResolvedAdapter;
|
|
196
|
+
cwd: string;
|
|
197
|
+
env: Record<string, string | undefined>;
|
|
198
|
+
}): Promise<DapClient> {
|
|
199
|
+
const { promise: connPromise, resolve: resolveConn } = Promise.withResolvers<Bun.Socket<undefined>>();
|
|
200
|
+
|
|
201
|
+
// Listen on port 0 (OS picks a free port)
|
|
202
|
+
const server = Bun.listen({
|
|
203
|
+
hostname: "127.0.0.1",
|
|
204
|
+
port: 0,
|
|
205
|
+
socket: {
|
|
206
|
+
open(socket) {
|
|
207
|
+
resolveConn(socket);
|
|
208
|
+
},
|
|
209
|
+
data() {},
|
|
210
|
+
close() {},
|
|
211
|
+
error() {},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const port = server.port;
|
|
216
|
+
const proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--client-addr=127.0.0.1:${port}`], {
|
|
217
|
+
cwd,
|
|
218
|
+
stdin: "pipe",
|
|
219
|
+
env,
|
|
220
|
+
detached: true,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Wait for dlv to connect (with timeout)
|
|
224
|
+
let rawSocket: Bun.Socket<undefined>;
|
|
225
|
+
const { promise: timeoutPromise, reject: rejectTimeout } = Promise.withResolvers<never>();
|
|
226
|
+
const connectTimeout = setTimeout(
|
|
227
|
+
() => rejectTimeout(new Error(`${adapter.name} did not connect within 10s`)),
|
|
228
|
+
10_000,
|
|
229
|
+
);
|
|
230
|
+
try {
|
|
231
|
+
rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
232
|
+
} finally {
|
|
233
|
+
clearTimeout(connectTimeout);
|
|
234
|
+
server.stop();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
238
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
239
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
240
|
+
void client.#startMessageReader();
|
|
241
|
+
return client;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get capabilities(): DapCapabilities | undefined {
|
|
245
|
+
return this.#capabilities;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get lastActivity(): number {
|
|
249
|
+
return this.#lastActivity;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
isAlive(): boolean {
|
|
253
|
+
return !this.#disposed && this.proc.exitCode === null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async initialize(args: DapInitializeArguments, signal?: AbortSignal, timeoutMs?: number): Promise<DapCapabilities> {
|
|
257
|
+
const body = (await this.sendRequest("initialize", args, signal, timeoutMs)) as DapCapabilities | undefined;
|
|
258
|
+
this.#capabilities = body ?? {};
|
|
259
|
+
return this.#capabilities;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onEvent(event: string, handler: DapEventHandler): () => void {
|
|
263
|
+
const handlers = this.#eventHandlers.get(event) ?? new Set<DapEventHandler>();
|
|
264
|
+
handlers.add(handler);
|
|
265
|
+
this.#eventHandlers.set(event, handlers);
|
|
266
|
+
return () => {
|
|
267
|
+
handlers.delete(handler);
|
|
268
|
+
if (handlers.size === 0) {
|
|
269
|
+
this.#eventHandlers.delete(event);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
onAnyEvent(handler: DapEventHandler): () => void {
|
|
275
|
+
this.#anyEventHandlers.add(handler);
|
|
276
|
+
return () => {
|
|
277
|
+
this.#anyEventHandlers.delete(handler);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
onReverseRequest(command: string, handler: DapReverseRequestHandler): () => void {
|
|
282
|
+
this.#reverseRequestHandlers.set(command, handler);
|
|
283
|
+
return () => {
|
|
284
|
+
if (this.#reverseRequestHandlers.get(command) === handler) {
|
|
285
|
+
this.#reverseRequestHandlers.delete(command);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async waitForEvent<TBody>(
|
|
291
|
+
event: string,
|
|
292
|
+
predicate?: (body: TBody) => boolean,
|
|
293
|
+
signal?: AbortSignal,
|
|
294
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
295
|
+
): Promise<TBody> {
|
|
296
|
+
if (signal?.aborted) {
|
|
297
|
+
throw signal.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
298
|
+
}
|
|
299
|
+
const { promise, resolve, reject } = Promise.withResolvers<TBody>();
|
|
300
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
301
|
+
const cleanup = () => {
|
|
302
|
+
unsubscribe();
|
|
303
|
+
if (timeout) clearTimeout(timeout);
|
|
304
|
+
if (signal) {
|
|
305
|
+
signal.removeEventListener("abort", abortHandler);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const abortHandler = () => {
|
|
309
|
+
cleanup();
|
|
310
|
+
reject(signal?.reason instanceof Error ? signal.reason : new ToolAbortError());
|
|
311
|
+
};
|
|
312
|
+
const unsubscribe = this.onEvent(event, body => {
|
|
313
|
+
const typedBody = body as TBody;
|
|
314
|
+
if (predicate && !predicate(typedBody)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
cleanup();
|
|
318
|
+
resolve(typedBody);
|
|
319
|
+
});
|
|
320
|
+
if (signal) {
|
|
321
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
322
|
+
}
|
|
323
|
+
timeout = setTimeout(() => {
|
|
324
|
+
cleanup();
|
|
325
|
+
reject(new Error(`DAP event ${event} timed out after ${timeoutMs}ms`));
|
|
326
|
+
}, timeoutMs);
|
|
327
|
+
return promise;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async sendRequest<TBody = unknown>(
|
|
331
|
+
command: string,
|
|
332
|
+
args?: unknown,
|
|
333
|
+
signal?: AbortSignal,
|
|
334
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
335
|
+
): Promise<TBody> {
|
|
336
|
+
if (signal?.aborted) {
|
|
337
|
+
throw signal.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
338
|
+
}
|
|
339
|
+
if (this.#disposed) {
|
|
340
|
+
throw new Error(`DAP adapter ${this.adapter.name} is not running`);
|
|
341
|
+
}
|
|
342
|
+
const requestSeq = ++this.#requestSeq;
|
|
343
|
+
const request: DapRequestMessage = {
|
|
344
|
+
seq: requestSeq,
|
|
345
|
+
type: "request",
|
|
346
|
+
command,
|
|
347
|
+
arguments: args,
|
|
348
|
+
};
|
|
349
|
+
const { promise, resolve, reject } = Promise.withResolvers<TBody>();
|
|
350
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
351
|
+
const cleanup = () => {
|
|
352
|
+
if (timeout) clearTimeout(timeout);
|
|
353
|
+
if (signal) {
|
|
354
|
+
signal.removeEventListener("abort", abortHandler);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const abortHandler = () => {
|
|
358
|
+
this.#pendingRequests.delete(requestSeq);
|
|
359
|
+
cleanup();
|
|
360
|
+
reject(signal?.reason instanceof Error ? signal.reason : new ToolAbortError());
|
|
361
|
+
};
|
|
362
|
+
timeout = setTimeout(() => {
|
|
363
|
+
if (!this.#pendingRequests.has(requestSeq)) return;
|
|
364
|
+
this.#pendingRequests.delete(requestSeq);
|
|
365
|
+
cleanup();
|
|
366
|
+
reject(new Error(`DAP request ${command} timed out after ${timeoutMs}ms`));
|
|
367
|
+
}, timeoutMs);
|
|
368
|
+
if (signal) {
|
|
369
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
370
|
+
}
|
|
371
|
+
this.#pendingRequests.set(requestSeq, {
|
|
372
|
+
command,
|
|
373
|
+
resolve: body => {
|
|
374
|
+
cleanup();
|
|
375
|
+
resolve(body as TBody);
|
|
376
|
+
},
|
|
377
|
+
reject: error => {
|
|
378
|
+
cleanup();
|
|
379
|
+
reject(error);
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
this.#lastActivity = Date.now();
|
|
383
|
+
try {
|
|
384
|
+
await writeMessage(this.#writeSink, request);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
this.#pendingRequests.delete(requestSeq);
|
|
387
|
+
cleanup();
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
return promise;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async sendResponse(request: DapRequestMessage, success: boolean, body?: unknown, message?: string): Promise<void> {
|
|
394
|
+
const response: DapResponseMessage = {
|
|
395
|
+
seq: ++this.#requestSeq,
|
|
396
|
+
type: "response",
|
|
397
|
+
request_seq: request.seq,
|
|
398
|
+
success,
|
|
399
|
+
command: request.command,
|
|
400
|
+
...(message ? { message } : {}),
|
|
401
|
+
...(body !== undefined ? { body } : {}),
|
|
402
|
+
};
|
|
403
|
+
await writeMessage(this.#writeSink, response);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async dispose(): Promise<void> {
|
|
407
|
+
if (this.#disposed) return;
|
|
408
|
+
this.#disposed = true;
|
|
409
|
+
this.#rejectPendingRequests(new Error(`DAP adapter ${this.adapter.name} disposed`));
|
|
410
|
+
try {
|
|
411
|
+
this.#socket?.end();
|
|
412
|
+
} catch {
|
|
413
|
+
/* socket may already be closed */
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
this.proc.kill();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
logger.debug("Failed to kill DAP adapter", {
|
|
419
|
+
adapter: this.adapter.name,
|
|
420
|
+
error: toErrorMessage(error),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
await this.proc.exited.catch(() => {});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async #startMessageReader(): Promise<void> {
|
|
427
|
+
if (this.#isReading) return;
|
|
428
|
+
this.#isReading = true;
|
|
429
|
+
const reader = this.#readable.getReader();
|
|
430
|
+
try {
|
|
431
|
+
while (true) {
|
|
432
|
+
const { done, value } = await reader.read();
|
|
433
|
+
if (done) break;
|
|
434
|
+
const currentBuffer = Buffer.concat([this.#messageBuffer, value]);
|
|
435
|
+
this.#messageBuffer = currentBuffer;
|
|
436
|
+
let workingBuffer = currentBuffer;
|
|
437
|
+
let parsed = parseMessage(workingBuffer);
|
|
438
|
+
while (parsed) {
|
|
439
|
+
const { message, remaining } = parsed;
|
|
440
|
+
workingBuffer = Buffer.from(remaining);
|
|
441
|
+
this.#lastActivity = Date.now();
|
|
442
|
+
if (message.type === "response") {
|
|
443
|
+
this.#handleResponse(message);
|
|
444
|
+
} else if (message.type === "event") {
|
|
445
|
+
await this.#dispatchEvent(message);
|
|
446
|
+
} else {
|
|
447
|
+
await this.#handleAdapterRequest(message);
|
|
448
|
+
}
|
|
449
|
+
parsed = parseMessage(workingBuffer);
|
|
450
|
+
}
|
|
451
|
+
this.#messageBuffer = workingBuffer;
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.#rejectPendingRequests(new Error(`DAP connection closed: ${toErrorMessage(error)}`));
|
|
455
|
+
} finally {
|
|
456
|
+
reader.releaseLock();
|
|
457
|
+
this.#isReading = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#handleResponse(message: DapResponseMessage): void {
|
|
462
|
+
const pending = this.#pendingRequests.get(message.request_seq);
|
|
463
|
+
if (!pending) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.#pendingRequests.delete(message.request_seq);
|
|
467
|
+
if (message.success) {
|
|
468
|
+
pending.resolve(message.body);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const errorMessage = message.message ?? `DAP request ${pending.command} failed`;
|
|
472
|
+
pending.reject(new Error(errorMessage));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async #dispatchEvent(message: DapEventMessage): Promise<void> {
|
|
476
|
+
const handlers = Array.from(this.#eventHandlers.get(message.event) ?? []);
|
|
477
|
+
const anyHandlers = Array.from(this.#anyEventHandlers);
|
|
478
|
+
for (const handler of [...handlers, ...anyHandlers]) {
|
|
479
|
+
try {
|
|
480
|
+
await handler(message.body, message);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger.warn("DAP event handler failed", {
|
|
483
|
+
adapter: this.adapter.name,
|
|
484
|
+
event: message.event,
|
|
485
|
+
error: toErrorMessage(error),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async #handleAdapterRequest(message: DapRequestMessage): Promise<void> {
|
|
492
|
+
try {
|
|
493
|
+
const handler = this.#reverseRequestHandlers.get(message.command);
|
|
494
|
+
if (handler) {
|
|
495
|
+
try {
|
|
496
|
+
const body = await handler(message.arguments);
|
|
497
|
+
await this.sendResponse(message, true, body);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const errorMessage = toErrorMessage(error);
|
|
500
|
+
await this.sendResponse(
|
|
501
|
+
message,
|
|
502
|
+
false,
|
|
503
|
+
{
|
|
504
|
+
error: {
|
|
505
|
+
id: 1,
|
|
506
|
+
format: errorMessage,
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
errorMessage,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const errorMessage = `Unsupported DAP request: ${message.command}`;
|
|
515
|
+
await this.sendResponse(
|
|
516
|
+
message,
|
|
517
|
+
false,
|
|
518
|
+
{
|
|
519
|
+
error: {
|
|
520
|
+
id: 1,
|
|
521
|
+
format: errorMessage,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
errorMessage,
|
|
525
|
+
);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
logger.warn("Failed to answer DAP adapter request", {
|
|
528
|
+
adapter: this.adapter.name,
|
|
529
|
+
command: message.command,
|
|
530
|
+
error: toErrorMessage(error),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#handleProcessExit(): void {
|
|
536
|
+
if (this.#disposed) return;
|
|
537
|
+
this.#disposed = true;
|
|
538
|
+
const stderr = this.proc.peekStderr().trim();
|
|
539
|
+
const exitCode = this.proc.exitCode;
|
|
540
|
+
const error = new Error(
|
|
541
|
+
stderr
|
|
542
|
+
? `DAP adapter exited (code ${exitCode}): ${stderr}`
|
|
543
|
+
: `DAP adapter exited unexpectedly (code ${exitCode})`,
|
|
544
|
+
);
|
|
545
|
+
this.#rejectPendingRequests(error);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#rejectPendingRequests(error: Error): void {
|
|
549
|
+
for (const pending of this.#pendingRequests.values()) {
|
|
550
|
+
pending.reject(error);
|
|
551
|
+
}
|
|
552
|
+
this.#pendingRequests.clear();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Poll a condition until it returns true, or timeout/process exit. */
|
|
557
|
+
async function waitForCondition(
|
|
558
|
+
check: () => boolean,
|
|
559
|
+
timeoutMs: number,
|
|
560
|
+
proc: { exitCode: number | null },
|
|
561
|
+
): Promise<void> {
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
while (Date.now() < deadline) {
|
|
564
|
+
if (check()) return;
|
|
565
|
+
if (proc.exitCode !== null) {
|
|
566
|
+
throw new Error("Adapter process exited before socket was ready");
|
|
567
|
+
}
|
|
568
|
+
await Bun.sleep(50);
|
|
569
|
+
}
|
|
570
|
+
throw new Error(`Socket not ready after ${timeoutMs}ms`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
interface SocketTransport {
|
|
574
|
+
readable: ReadableStream<Uint8Array>;
|
|
575
|
+
writeSink: DapWriteSink;
|
|
576
|
+
socket: { end(): void };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Adapt a Bun.Socket to DapWriteSink. */
|
|
580
|
+
function socketToSink(socket: Bun.Socket<undefined>): DapWriteSink {
|
|
581
|
+
return {
|
|
582
|
+
write(data: string | Uint8Array) {
|
|
583
|
+
return socket.write(data);
|
|
584
|
+
},
|
|
585
|
+
flush() {
|
|
586
|
+
socket.flush();
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Connect to a unix domain socket and return DAP transport streams. */
|
|
592
|
+
async function connectSocket(options: { unix: string }): Promise<SocketTransport> {
|
|
593
|
+
const { promise, resolve } = Promise.withResolvers<SocketTransport>();
|
|
594
|
+
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
595
|
+
|
|
596
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
597
|
+
start(controller) {
|
|
598
|
+
streamController = controller;
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
Bun.connect({
|
|
603
|
+
unix: options.unix,
|
|
604
|
+
socket: {
|
|
605
|
+
open(socket) {
|
|
606
|
+
resolve({
|
|
607
|
+
readable,
|
|
608
|
+
writeSink: socketToSink(socket),
|
|
609
|
+
socket,
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
data(_socket, data) {
|
|
613
|
+
streamController.enqueue(new Uint8Array(data));
|
|
614
|
+
},
|
|
615
|
+
close() {
|
|
616
|
+
try {
|
|
617
|
+
streamController.close();
|
|
618
|
+
} catch {
|
|
619
|
+
/* already closed */
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
error(_socket, err) {
|
|
623
|
+
try {
|
|
624
|
+
streamController.error(err);
|
|
625
|
+
} catch {
|
|
626
|
+
/* already closed */
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
return promise;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Wrap an already-connected Bun.Socket into DAP transport streams. */
|
|
636
|
+
function wrapBunSocket(rawSocket: Bun.Socket<undefined>): SocketTransport {
|
|
637
|
+
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
638
|
+
|
|
639
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
640
|
+
start(controller) {
|
|
641
|
+
streamController = controller;
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Attach data/close/error handlers to the already-open socket
|
|
646
|
+
rawSocket.reload({
|
|
647
|
+
socket: {
|
|
648
|
+
open() {},
|
|
649
|
+
data(_socket, data) {
|
|
650
|
+
streamController.enqueue(new Uint8Array(data));
|
|
651
|
+
},
|
|
652
|
+
close() {
|
|
653
|
+
try {
|
|
654
|
+
streamController.close();
|
|
655
|
+
} catch {
|
|
656
|
+
/* already closed */
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
error(_socket, err) {
|
|
660
|
+
try {
|
|
661
|
+
streamController.error(err);
|
|
662
|
+
} catch {
|
|
663
|
+
/* already closed */
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
readable,
|
|
671
|
+
writeSink: socketToSink(rawSocket),
|
|
672
|
+
socket: rawSocket,
|
|
673
|
+
};
|
|
674
|
+
}
|