@bubblebrain-ai/bubble 0.0.13 → 0.0.15

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.
Files changed (80) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/tool-intent.js +1 -0
  3. package/dist/agent.d.ts +2 -0
  4. package/dist/agent.js +589 -316
  5. package/dist/approval/controller.d.ts +1 -0
  6. package/dist/approval/controller.js +20 -3
  7. package/dist/approval/tool-helper.js +2 -0
  8. package/dist/approval/types.d.ts +14 -1
  9. package/dist/cli.d.ts +3 -1
  10. package/dist/cli.js +12 -0
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +41 -0
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +12 -0
  33. package/dist/slash-commands/types.d.ts +2 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/display-history.d.ts +1 -0
  55. package/dist/tui/display-history.js +5 -4
  56. package/dist/tui/edit-diff.js +6 -1
  57. package/dist/tui/model-picker-data.d.ts +10 -0
  58. package/dist/tui/model-picker-data.js +32 -0
  59. package/dist/tui/run.d.ts +2 -0
  60. package/dist/tui/run.js +717 -122
  61. package/dist/tui/tool-renderers/fallback.js +1 -1
  62. package/dist/tui/tool-renderers/write-preview.js +2 -0
  63. package/dist/tui/trace-groups.js +10 -3
  64. package/dist/tui-ink/app.js +1 -4
  65. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  66. package/dist/tui-ink/display-history.d.ts +1 -0
  67. package/dist/tui-ink/display-history.js +5 -4
  68. package/dist/tui-ink/message-list.js +14 -8
  69. package/dist/tui-ink/trace-groups.js +1 -1
  70. package/dist/tui-opentui/app.js +2 -0
  71. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  72. package/dist/tui-opentui/display-history.d.ts +1 -0
  73. package/dist/tui-opentui/display-history.js +5 -4
  74. package/dist/tui-opentui/edit-diff.js +6 -1
  75. package/dist/tui-opentui/message-list.js +6 -3
  76. package/dist/tui-opentui/trace-groups.js +10 -3
  77. package/dist/types.d.ts +12 -2
  78. package/dist/update/index.d.ts +46 -0
  79. package/dist/update/index.js +240 -0
  80. package/package.json +1 -1
@@ -0,0 +1,312 @@
1
+ export class PatchApplyError extends Error {
2
+ status;
3
+ constructor(message, status = "no_match") {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = "PatchApplyError";
7
+ }
8
+ }
9
+ const CHANGE_MARKERS = [
10
+ "*** Add File: ",
11
+ "*** Delete File: ",
12
+ "*** Update File: ",
13
+ "*** End Patch",
14
+ ];
15
+ export function parseApplyPatch(patchText) {
16
+ const lines = normalizeToLF(patchText).split("\n");
17
+ if (lines[lines.length - 1] === "")
18
+ lines.pop();
19
+ if (lines[0] !== "*** Begin Patch") {
20
+ throw new PatchApplyError("Error: apply_patch must start with *** Begin Patch", "blocked");
21
+ }
22
+ const operations = [];
23
+ let index = 1;
24
+ while (index < lines.length) {
25
+ const line = lines[index];
26
+ if (line === "*** End Patch") {
27
+ if (index !== lines.length - 1) {
28
+ throw new PatchApplyError("Error: Unexpected content after *** End Patch", "blocked");
29
+ }
30
+ if (operations.length === 0) {
31
+ throw new PatchApplyError("Error: apply_patch rejected an empty patch", "blocked");
32
+ }
33
+ return { operations };
34
+ }
35
+ if (line.startsWith("*** Add File: ")) {
36
+ const path = parseMarkerPath(line, "*** Add File: ");
37
+ index++;
38
+ const addLines = [];
39
+ while (index < lines.length && !isFileMarker(lines[index])) {
40
+ const current = lines[index];
41
+ if (!current.startsWith("+")) {
42
+ throw new PatchApplyError(`Error: Add File ${path} contains a non-added line: ${current}`, "blocked");
43
+ }
44
+ addLines.push(current.slice(1));
45
+ index++;
46
+ }
47
+ operations.push({ type: "add", path, lines: addLines });
48
+ continue;
49
+ }
50
+ if (line.startsWith("*** Delete File: ")) {
51
+ const path = parseMarkerPath(line, "*** Delete File: ");
52
+ operations.push({ type: "delete", path });
53
+ index++;
54
+ continue;
55
+ }
56
+ if (line.startsWith("*** Update File: ")) {
57
+ const path = parseMarkerPath(line, "*** Update File: ");
58
+ index++;
59
+ let movePath;
60
+ const chunks = [];
61
+ while (index < lines.length && !isFileMarker(lines[index])) {
62
+ const current = lines[index];
63
+ if (current.startsWith("*** Move to: ")) {
64
+ if (movePath || chunks.length > 0) {
65
+ throw new PatchApplyError(`Error: Move marker for ${path} must appear before update chunks`, "blocked");
66
+ }
67
+ movePath = parseMarkerPath(current, "*** Move to: ");
68
+ index++;
69
+ continue;
70
+ }
71
+ if (!current.startsWith("@@")) {
72
+ throw new PatchApplyError(`Error: Update File ${path} expected @@ hunk header, got: ${current}`, "blocked");
73
+ }
74
+ const header = current;
75
+ index++;
76
+ const chunkLines = [];
77
+ while (index < lines.length && !isFileMarker(lines[index]) && !lines[index].startsWith("@@")) {
78
+ const patchLine = lines[index];
79
+ if (patchLine.startsWith("\")) {
80
+ index++;
81
+ continue;
82
+ }
83
+ const prefix = patchLine[0];
84
+ const text = patchLine.slice(1);
85
+ if (prefix === " ") {
86
+ chunkLines.push({ kind: "context", text });
87
+ }
88
+ else if (prefix === "-") {
89
+ chunkLines.push({ kind: "remove", text });
90
+ }
91
+ else if (prefix === "+") {
92
+ chunkLines.push({ kind: "add", text });
93
+ }
94
+ else {
95
+ throw new PatchApplyError(`Error: Hunk for ${path} contains invalid line: ${patchLine}`, "blocked");
96
+ }
97
+ index++;
98
+ }
99
+ if (chunkLines.length === 0) {
100
+ throw new PatchApplyError(`Error: Empty hunk in ${path}`, "blocked");
101
+ }
102
+ chunks.push({ header, lines: chunkLines });
103
+ }
104
+ if (!movePath && chunks.length === 0) {
105
+ throw new PatchApplyError(`Error: Update File ${path} has no hunks`, "blocked");
106
+ }
107
+ operations.push({ type: "update", path, ...(movePath ? { movePath } : {}), chunks });
108
+ continue;
109
+ }
110
+ throw new PatchApplyError(`Error: Unexpected patch marker: ${line}`, "blocked");
111
+ }
112
+ throw new PatchApplyError("Error: apply_patch must end with *** End Patch", "blocked");
113
+ }
114
+ export function buildAddedFileContent(lines) {
115
+ if (lines.length === 0)
116
+ return "";
117
+ return `${lines.join("\n")}\n`;
118
+ }
119
+ export function applyPatchChunks(rawContent, chunks, path) {
120
+ const { bom, text } = stripBom(rawContent);
121
+ const lineEnding = detectLineEnding(text);
122
+ let normalized = normalizeToLF(text);
123
+ let usedFallback = false;
124
+ for (let index = 0; index < chunks.length; index++) {
125
+ const result = applyChunk(normalized, chunks[index], path, index);
126
+ normalized = result.content;
127
+ usedFallback ||= result.usedFallback;
128
+ }
129
+ return {
130
+ content: bom + restoreLineEndings(normalized, lineEnding),
131
+ usedFallback,
132
+ };
133
+ }
134
+ function applyChunk(content, chunk, path, chunkIndex) {
135
+ const oldLines = chunk.lines
136
+ .filter((line) => line.kind === "context" || line.kind === "remove")
137
+ .map((line) => line.text);
138
+ const newLines = chunk.lines
139
+ .filter((line) => line.kind === "context" || line.kind === "add")
140
+ .map((line) => line.text);
141
+ if (oldLines.length === 0) {
142
+ throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} has no context to locate an insertion.`, "blocked");
143
+ }
144
+ const exactMatches = findExactLineBlockMatches(content, oldLines);
145
+ if (exactMatches.length === 1) {
146
+ return {
147
+ content: replaceSpan(content, exactMatches[0], newLines),
148
+ usedFallback: false,
149
+ };
150
+ }
151
+ if (exactMatches.length > 1) {
152
+ throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} matched ${exactMatches.length} exact locations. Add more context.`, "blocked");
153
+ }
154
+ const fallbackMatches = findNormalizedLineBlockMatches(content, oldLines, path);
155
+ if (fallbackMatches.length === 1) {
156
+ return {
157
+ content: replaceSpan(content, fallbackMatches[0], newLines),
158
+ usedFallback: true,
159
+ };
160
+ }
161
+ if (fallbackMatches.length > 1) {
162
+ throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} matched ${fallbackMatches.length} normalized locations. Add more context.`, "blocked");
163
+ }
164
+ throw new PatchApplyError(`Error: Hunk ${chunkIndex + 1} in ${path} did not match the file. Re-read the file and regenerate the patch.`);
165
+ }
166
+ function parseMarkerPath(line, marker) {
167
+ const path = line.slice(marker.length).trim();
168
+ if (!path)
169
+ throw new PatchApplyError(`Error: Patch marker is missing a path: ${line}`, "blocked");
170
+ return path;
171
+ }
172
+ function isFileMarker(line) {
173
+ return CHANGE_MARKERS.some((marker) => line === marker || line.startsWith(marker));
174
+ }
175
+ function detectLineEnding(content) {
176
+ const crlf = content.indexOf("\r\n");
177
+ const lf = content.indexOf("\n");
178
+ return crlf !== -1 && crlf === lf - 1 ? "\r\n" : "\n";
179
+ }
180
+ function stripBom(content) {
181
+ return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
182
+ }
183
+ function normalizeToLF(text) {
184
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
185
+ }
186
+ function restoreLineEndings(text, lineEnding) {
187
+ return lineEnding === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
188
+ }
189
+ function splitLines(content) {
190
+ const lines = [];
191
+ let start = 0;
192
+ for (let index = 0; index < content.length; index++) {
193
+ if (content[index] === "\n") {
194
+ lines.push({ text: content.slice(start, index), start, endNoNewline: index });
195
+ start = index + 1;
196
+ }
197
+ }
198
+ lines.push({ text: content.slice(start), start, endNoNewline: content.length });
199
+ return lines;
200
+ }
201
+ function findExactLineBlockMatches(content, oldLines) {
202
+ const lines = splitLines(content);
203
+ const matches = [];
204
+ for (let index = 0; index <= lines.length - oldLines.length; index++) {
205
+ let matched = true;
206
+ for (let offset = 0; offset < oldLines.length; offset++) {
207
+ if (lines[index + offset].text !== oldLines[offset]) {
208
+ matched = false;
209
+ break;
210
+ }
211
+ }
212
+ if (matched) {
213
+ matches.push({
214
+ start: lines[index].start,
215
+ end: lines[index + oldLines.length - 1].endNoNewline,
216
+ });
217
+ }
218
+ }
219
+ return matches;
220
+ }
221
+ function findNormalizedLineBlockMatches(content, oldLines, path) {
222
+ const expected = oldLines
223
+ .map((line) => normalizeLineForMatch(line))
224
+ .filter((line) => line.trim().length > 0);
225
+ if (expected.length === 0)
226
+ return [];
227
+ const contentLines = splitLines(content)
228
+ .map((line) => ({ line, normalized: normalizeLineForMatch(line.text) }))
229
+ .filter((item) => item.normalized.trim().length > 0);
230
+ const matches = [];
231
+ for (let index = 0; index <= contentLines.length - expected.length; index++) {
232
+ let matched = true;
233
+ for (let offset = 0; offset < expected.length; offset++) {
234
+ if (!lineEquivalent(contentLines[index + offset].line.text, expected[offset], path, expected.length)) {
235
+ matched = false;
236
+ break;
237
+ }
238
+ }
239
+ if (matched) {
240
+ matches.push({
241
+ start: contentLines[index].line.start,
242
+ end: contentLines[index + expected.length - 1].line.endNoNewline,
243
+ });
244
+ }
245
+ }
246
+ return matches;
247
+ }
248
+ function normalizeLineForMatch(line) {
249
+ return line
250
+ .normalize("NFKC")
251
+ .trimEnd()
252
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
253
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
254
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
255
+ .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
256
+ }
257
+ function lineEquivalent(actual, expectedNormalized, path, expectedLineCount) {
258
+ const actualNormalized = normalizeLineForMatch(actual);
259
+ if (actualNormalized === expectedNormalized)
260
+ return true;
261
+ const actualCells = splitMarkdownTableCells(actualNormalized);
262
+ const expectedCells = splitMarkdownTableCells(expectedNormalized);
263
+ if (actualCells && expectedCells && sameCells(actualCells, expectedCells))
264
+ return true;
265
+ if (expectedLineCount === 1 && isDocumentLikePath(path)) {
266
+ return collapseInlineWhitespace(actualNormalized) === collapseInlineWhitespace(expectedNormalized);
267
+ }
268
+ return false;
269
+ }
270
+ function splitMarkdownTableCells(line) {
271
+ const normalized = line.trim();
272
+ if (!normalized.startsWith("|") || !normalized.endsWith("|"))
273
+ return undefined;
274
+ const parts = [];
275
+ let current = "";
276
+ let escaped = false;
277
+ for (const char of normalized) {
278
+ if (escaped) {
279
+ current += char;
280
+ escaped = false;
281
+ continue;
282
+ }
283
+ if (char === "\\") {
284
+ current += char;
285
+ escaped = true;
286
+ continue;
287
+ }
288
+ if (char === "|") {
289
+ parts.push(current);
290
+ current = "";
291
+ continue;
292
+ }
293
+ current += char;
294
+ }
295
+ parts.push(current);
296
+ if (parts.length < 4 || parts[0] !== "" || parts[parts.length - 1] !== "")
297
+ return undefined;
298
+ const cells = parts.slice(1, -1).map((cell) => cell.trim());
299
+ return cells.length >= 2 ? cells : undefined;
300
+ }
301
+ function sameCells(a, b) {
302
+ return a.length === b.length && a.every((cell, index) => cell === b[index]);
303
+ }
304
+ function collapseInlineWhitespace(text) {
305
+ return text.trim().replace(/[ \t]+/g, " ");
306
+ }
307
+ function isDocumentLikePath(path) {
308
+ return /\.(?:md|mdx|markdown|txt|rst|adoc)$/i.test(path);
309
+ }
310
+ function replaceSpan(content, span, newLines) {
311
+ return content.slice(0, span.start) + newLines.join("\n") + content.slice(span.end);
312
+ }
@@ -0,0 +1,36 @@
1
+ export type ManagedServerPurpose = "preview" | "verification";
2
+ export type ManagedServerLifecycle = "auto" | "keep_alive";
3
+ export type ManagedServerStatus = "starting" | "ready" | "running" | "exited" | "failed" | "stopped";
4
+ export interface ManagedServerInfo {
5
+ id: string;
6
+ command: string;
7
+ cwd: string;
8
+ port?: number;
9
+ url?: string;
10
+ ownerSessionId?: string;
11
+ ownerRunId?: string;
12
+ purpose: ManagedServerPurpose;
13
+ lifecycle: ManagedServerLifecycle;
14
+ startedAt: number;
15
+ lastUsedAt: number;
16
+ status: ManagedServerStatus;
17
+ pid?: number;
18
+ exitCode?: number | null;
19
+ }
20
+ export interface StartManagedServerInput {
21
+ command: string;
22
+ cwd: string;
23
+ port?: number;
24
+ readinessUrl?: string;
25
+ timeoutSec?: number;
26
+ ownerSessionId?: string;
27
+ ownerRunId?: string;
28
+ purpose?: ManagedServerPurpose;
29
+ lifecycle?: ManagedServerLifecycle;
30
+ }
31
+ export declare function startManagedServer(input: StartManagedServerInput): Promise<ManagedServerInfo>;
32
+ export declare function listManagedServers(): ManagedServerInfo[];
33
+ export declare function getManagedServer(id: string): ManagedServerInfo | undefined;
34
+ export declare function getManagedServerLogs(id: string, maxChars?: number): string | undefined;
35
+ export declare function stopManagedServer(id: string): Promise<ManagedServerInfo | undefined>;
36
+ export declare function stopAutoServersForSession(sessionID?: string): Promise<void>;
@@ -0,0 +1,234 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import net from "node:net";
4
+ import { platform } from "node:os";
5
+ const MAX_LOG_BYTES = 96 * 1024;
6
+ const STOP_FORCE_AFTER_MS = 1000;
7
+ const servers = new Map();
8
+ let nextServerId = 1;
9
+ export async function startManagedServer(input) {
10
+ if (!existsSync(input.cwd)) {
11
+ throw new Error(`Working directory does not exist: ${input.cwd}`);
12
+ }
13
+ const command = input.command.trim();
14
+ if (!command) {
15
+ throw new Error("command is required");
16
+ }
17
+ if (input.port !== undefined) {
18
+ const managed = Array.from(servers.values()).find((server) => server.port === input.port && isActiveStatus(server.status));
19
+ if (managed) {
20
+ throw new Error(`Port ${input.port} is already managed by ${managed.id}. Stop it before starting another server.`);
21
+ }
22
+ if (await isPortOpen(input.port)) {
23
+ throw new Error(`Port ${input.port} is already in use by an unmanaged process.`);
24
+ }
25
+ }
26
+ const shell = platform() === "win32" ? "cmd.exe" : "bash";
27
+ const shellArgs = platform() === "win32" ? ["/c", command] : ["-c", command];
28
+ const child = spawn(shell, shellArgs, {
29
+ cwd: input.cwd,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ env: process.env,
32
+ detached: platform() !== "win32",
33
+ windowsHide: true,
34
+ });
35
+ child.unref();
36
+ unrefStream(child.stdout);
37
+ unrefStream(child.stderr);
38
+ const now = Date.now();
39
+ const id = `server_${String(nextServerId++).padStart(4, "0")}`;
40
+ const readinessUrl = input.readinessUrl ?? (input.port ? `http://localhost:${input.port}` : undefined);
41
+ const record = {
42
+ id,
43
+ command,
44
+ cwd: input.cwd,
45
+ ...(input.port !== undefined ? { port: input.port } : {}),
46
+ ...(readinessUrl ? { url: readinessUrl } : {}),
47
+ ownerSessionId: input.ownerSessionId,
48
+ ownerRunId: input.ownerRunId,
49
+ purpose: input.purpose ?? "preview",
50
+ lifecycle: input.lifecycle ?? "keep_alive",
51
+ startedAt: now,
52
+ lastUsedAt: now,
53
+ status: "starting",
54
+ pid: child.pid,
55
+ child,
56
+ logs: "",
57
+ };
58
+ servers.set(id, record);
59
+ child.stdout.on("data", (data) => appendLog(record, data.toString()));
60
+ child.stderr.on("data", (data) => appendLog(record, data.toString()));
61
+ child.once("error", (error) => {
62
+ record.status = "failed";
63
+ appendLog(record, `\n[server failed: ${error.message}]\n`);
64
+ });
65
+ child.once("exit", (code) => {
66
+ record.exitCode = code;
67
+ if (record.status !== "stopped") {
68
+ record.status = code === 0 ? "exited" : "failed";
69
+ }
70
+ record.child = undefined;
71
+ });
72
+ const timeoutSec = input.timeoutSec ?? 30;
73
+ const ready = readinessUrl
74
+ ? await waitForReadiness(record, readinessUrl, timeoutSec)
75
+ : await waitForProcessToStayAlive(record, Math.min(timeoutSec, 2));
76
+ if (!ready) {
77
+ const logs = record.logs.trim();
78
+ await stopManagedServer(id);
79
+ throw new Error(`Server ${id} did not become ready within ${timeoutSec}s.${logs ? `\n\nLogs:\n${tail(logs, 4000)}` : ""}`);
80
+ }
81
+ record.status = readinessUrl ? "ready" : "running";
82
+ record.lastUsedAt = Date.now();
83
+ return publicInfo(record);
84
+ }
85
+ export function listManagedServers() {
86
+ return Array.from(servers.values()).map(publicInfo);
87
+ }
88
+ export function getManagedServer(id) {
89
+ const server = servers.get(id);
90
+ if (!server)
91
+ return undefined;
92
+ server.lastUsedAt = Date.now();
93
+ return publicInfo(server);
94
+ }
95
+ export function getManagedServerLogs(id, maxChars = 12000) {
96
+ const server = servers.get(id);
97
+ if (!server)
98
+ return undefined;
99
+ server.lastUsedAt = Date.now();
100
+ return tail(server.logs, maxChars);
101
+ }
102
+ export async function stopManagedServer(id) {
103
+ const server = servers.get(id);
104
+ if (!server)
105
+ return undefined;
106
+ server.status = "stopped";
107
+ server.lastUsedAt = Date.now();
108
+ const child = server.child;
109
+ if (child?.pid) {
110
+ killProcessTree(child.pid, "SIGTERM");
111
+ await new Promise((resolve) => setTimeout(resolve, STOP_FORCE_AFTER_MS));
112
+ killProcessTree(child.pid, "SIGKILL");
113
+ }
114
+ server.child = undefined;
115
+ return publicInfo(server);
116
+ }
117
+ export async function stopAutoServersForSession(sessionID) {
118
+ if (!sessionID)
119
+ return;
120
+ const owned = Array.from(servers.values()).filter((server) => server.ownerSessionId === sessionID && server.lifecycle === "auto" && isActiveStatus(server.status));
121
+ await Promise.all(owned.map((server) => stopManagedServer(server.id)));
122
+ }
123
+ function publicInfo(server) {
124
+ return {
125
+ id: server.id,
126
+ command: server.command,
127
+ cwd: server.cwd,
128
+ port: server.port,
129
+ url: server.url,
130
+ ownerSessionId: server.ownerSessionId,
131
+ ownerRunId: server.ownerRunId,
132
+ purpose: server.purpose,
133
+ lifecycle: server.lifecycle,
134
+ startedAt: server.startedAt,
135
+ lastUsedAt: server.lastUsedAt,
136
+ status: server.status,
137
+ pid: server.pid,
138
+ exitCode: server.exitCode,
139
+ };
140
+ }
141
+ function appendLog(server, chunk) {
142
+ server.logs += chunk;
143
+ if (Buffer.byteLength(server.logs, "utf-8") > MAX_LOG_BYTES) {
144
+ server.logs = Buffer.from(server.logs, "utf-8").subarray(-MAX_LOG_BYTES).toString("utf-8");
145
+ }
146
+ }
147
+ function unrefStream(stream) {
148
+ stream.unref?.();
149
+ }
150
+ async function waitForReadiness(server, url, timeoutSec) {
151
+ const deadline = Date.now() + timeoutSec * 1000;
152
+ while (Date.now() < deadline) {
153
+ if (!isActiveStatus(server.status))
154
+ return false;
155
+ if (await canFetch(url))
156
+ return true;
157
+ await new Promise((resolve) => setTimeout(resolve, 250));
158
+ }
159
+ return false;
160
+ }
161
+ async function waitForProcessToStayAlive(server, timeoutSec) {
162
+ const deadline = Date.now() + timeoutSec * 1000;
163
+ while (Date.now() < deadline) {
164
+ if (!isActiveStatus(server.status))
165
+ return false;
166
+ await new Promise((resolve) => setTimeout(resolve, 100));
167
+ }
168
+ return isActiveStatus(server.status);
169
+ }
170
+ async function canFetch(url) {
171
+ const controller = new AbortController();
172
+ const timeout = setTimeout(() => controller.abort(), 1000);
173
+ try {
174
+ const response = await fetch(url, { signal: controller.signal });
175
+ return response.status < 500;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ finally {
181
+ clearTimeout(timeout);
182
+ }
183
+ }
184
+ function isActiveStatus(status) {
185
+ return status === "starting" || status === "ready" || status === "running";
186
+ }
187
+ async function isPortOpen(port) {
188
+ return await new Promise((resolve) => {
189
+ const socket = net.createConnection({ host: "127.0.0.1", port });
190
+ const done = (open) => {
191
+ socket.removeAllListeners();
192
+ socket.destroy();
193
+ resolve(open);
194
+ };
195
+ socket.once("connect", () => done(true));
196
+ socket.once("error", () => done(false));
197
+ socket.setTimeout(250, () => done(false));
198
+ });
199
+ }
200
+ function killProcessTree(pid, signal) {
201
+ if (platform() === "win32") {
202
+ try {
203
+ spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
204
+ stdio: "ignore",
205
+ windowsHide: true,
206
+ });
207
+ }
208
+ catch {
209
+ // Process may already be gone.
210
+ }
211
+ return;
212
+ }
213
+ try {
214
+ process.kill(-pid, signal);
215
+ }
216
+ catch {
217
+ try {
218
+ process.kill(pid, signal);
219
+ }
220
+ catch {
221
+ // Process already exited.
222
+ }
223
+ }
224
+ }
225
+ function tail(value, maxChars) {
226
+ return value.length <= maxChars ? value : value.slice(-maxChars);
227
+ }
228
+ process.once("exit", () => {
229
+ for (const server of servers.values()) {
230
+ if (server.child?.pid && server.lifecycle !== "keep_alive") {
231
+ killProcessTree(server.child.pid, "SIGKILL");
232
+ }
233
+ }
234
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Managed development server tools.
3
+ */
4
+ import type { ApprovalController } from "../approval/types.js";
5
+ import type { ToolRegistryEntry } from "../types.js";
6
+ export declare function createManagedServerTools(cwd: string, approval?: ApprovalController): ToolRegistryEntry[];