@sma1lboy/kobe 0.8.19 → 0.8.21

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 (58) hide show
  1. package/dist/cli/index.js +365 -358
  2. package/dist/cli/pty-host-node.mjs +906 -0
  3. package/dist/web-ui/assets/{AppShell-CvbbXJ5F.js → AppShell-Z8dP5tHs.js} +1 -1
  4. package/dist/web-ui/assets/{Board-Bv4goPMT.js → Board-VYFdvD23.js} +1 -1
  5. package/dist/web-ui/assets/{BoardPeek-BkY9UuOO.js → BoardPeek-VjYNGa_e.js} +2 -2
  6. package/dist/web-ui/assets/{ChatTerminal-Ci4mphvC.js → ChatTerminal-Dq5p6wd6.js} +1 -1
  7. package/dist/web-ui/assets/{ChatTranscript-ChbwT5aF.js → ChatTranscript-hdh-Z0CK.js} +1 -1
  8. package/dist/web-ui/assets/{ConfirmDialog-DLIhXq7i.js → ConfirmDialog-CUQLUsl5.js} +1 -1
  9. package/dist/web-ui/assets/{IssueIntakePanel-GVI4EcXz.js → IssueIntakePanel-CLYooVNv.js} +1 -1
  10. package/dist/web-ui/assets/{IssuePeek-GlD8qNa1.js → IssuePeek-DSvnw2pj.js} +1 -1
  11. package/dist/web-ui/assets/{IssuesPage-CCi4SQui.js → IssuesPage--95SRz1C.js} +1 -1
  12. package/dist/web-ui/assets/{RichEditor-zHTUYZU1.js → RichEditor-CTJuSqgK.js} +1 -1
  13. package/dist/web-ui/assets/{SettingsPage-DaEgIDC8.js → SettingsPage-CCAlgkym.js} +1 -1
  14. package/dist/web-ui/assets/{SlideOver-BL608zkP.js → SlideOver-BlmF_2fr.js} +1 -1
  15. package/dist/web-ui/assets/{ViewToggle-CMm5Drjy.js → ViewToggle-Dy8qGxUZ.js} +1 -1
  16. package/dist/web-ui/assets/{WorkspaceTabs-B_KOnpJT.js → WorkspaceTabs-8YMM_mCR.js} +3 -3
  17. package/dist/web-ui/assets/active-task-D3aDSt8x.js +1 -0
  18. package/dist/web-ui/assets/arrow-left-CVsK1Z_1.js +1 -0
  19. package/dist/web-ui/assets/board-BU6SpFlJ.js +1 -0
  20. package/dist/web-ui/assets/{chips-ER3SiHRg.js → chips-fTvSKM6V.js} +1 -1
  21. package/dist/web-ui/assets/{columns-3-B2K2DW2I.js → columns-3-y4l60-at.js} +1 -1
  22. package/dist/web-ui/assets/{createLucideIcon-B3ImGkvG.js → createLucideIcon-BFra8ckc.js} +1 -1
  23. package/dist/web-ui/assets/desktop-DnKVqhdT.js +2 -0
  24. package/dist/web-ui/assets/{engines-BpGfH1Zd.js → engines-VaA1oIxi.js} +1 -1
  25. package/dist/web-ui/assets/{external-link-D0CjtZaF.js → external-link-D72l1S75.js} +1 -1
  26. package/dist/web-ui/assets/{external-store-BMGmoHuF.js → external-store-CcZewmhW.js} +1 -1
  27. package/dist/web-ui/assets/{harness-BLR9sq9b.js → harness-D5256zaS.js} +1 -1
  28. package/dist/web-ui/assets/{index-CxTIVbF1.js → index-kwi5l86q.js} +3 -3
  29. package/dist/web-ui/assets/{issues-D1vtEzDa.js → issues-89uT5swt.js} +1 -1
  30. package/dist/web-ui/assets/issues-CTAEv0gu.js +1 -0
  31. package/dist/web-ui/assets/{lazy-issue-panels-CqdZEwZJ.js → lazy-issue-panels-D77tReDb.js} +3 -3
  32. package/dist/web-ui/assets/plus-BhhrqdEO.js +1 -0
  33. package/dist/web-ui/assets/{react-dom-BzHW8sem.js → react-dom-BQKJ1DoI.js} +1 -1
  34. package/dist/web-ui/assets/{rotate-cw-BxAflhQC.js → rotate-cw-Dg0ivobX.js} +1 -1
  35. package/dist/web-ui/assets/routes-DDtBxDwQ.js +1 -0
  36. package/dist/web-ui/assets/search-DA3nII27.js +1 -0
  37. package/dist/web-ui/assets/{store-DfONeFqN.js → store-LXLUDGEO.js} +2 -2
  38. package/dist/web-ui/assets/{tabs-C6drmk5t.js → tabs-C_UMvAeW.js} +1 -1
  39. package/dist/web-ui/assets/{task._taskId-DnczIHek.js → task._taskId-CTKLplr9.js} +2 -2
  40. package/dist/web-ui/assets/task._taskId-fF9XZnCt.js +1 -0
  41. package/dist/web-ui/assets/{toast-B5B1PUen.js → toast-Da-Q67N5.js} +1 -1
  42. package/dist/web-ui/assets/{triage-B8LSomfv.js → triage-D4d3Ytrq.js} +1 -1
  43. package/dist/web-ui/assets/{use-focus-trap-Bxa2qAjl.js → use-focus-trap-Zurfhrn8.js} +1 -1
  44. package/dist/web-ui/assets/{with-selector-9aQB38X4.js → with-selector-BQWSRu28.js} +1 -1
  45. package/dist/web-ui/assets/{worktrees-CyLbHRDQ.js → worktrees-DBbyo2ew.js} +1 -1
  46. package/dist/web-ui/assets/x-CopxPdj3.js +1 -0
  47. package/dist/web-ui/index.html +18 -18
  48. package/package.json +11 -11
  49. package/dist/web-ui/assets/active-task-bg1gRRr9.js +0 -1
  50. package/dist/web-ui/assets/arrow-left-CTu620DY.js +0 -1
  51. package/dist/web-ui/assets/board-BFy_bR1J.js +0 -1
  52. package/dist/web-ui/assets/desktop-BMy3eltH.js +0 -2
  53. package/dist/web-ui/assets/issues-wKDOEVmV.js +0 -1
  54. package/dist/web-ui/assets/plus-DZE7jB02.js +0 -1
  55. package/dist/web-ui/assets/routes-DoLYSrQS.js +0 -1
  56. package/dist/web-ui/assets/search-cqUzSqUT.js +0 -1
  57. package/dist/web-ui/assets/task._taskId-CCkQTzD1.js +0 -1
  58. package/dist/web-ui/assets/x-BtkkvyxG.js +0 -1
@@ -0,0 +1,906 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // ../kobe-daemon/src/daemon/pty-driver.ts
5
+ var TERMINAL_NAME = "xterm-256color";
6
+ function bunTerminalDriver(spawn) {
7
+ const spawnTerminal = spawn ?? ((argv, options) => Bun.spawn(argv, options));
8
+ return (request) => {
9
+ const proc = spawnTerminal([...request.argv], {
10
+ cwd: request.cwd,
11
+ env: request.env,
12
+ terminal: {
13
+ cols: request.cols,
14
+ rows: request.rows,
15
+ name: TERMINAL_NAME,
16
+ data: (_terminal, data) => request.onData(data)
17
+ }
18
+ });
19
+ return {
20
+ pid: proc.pid,
21
+ exited: proc.exited,
22
+ write: (data) => proc.terminal?.write(data),
23
+ resize: (cols, rows) => proc.terminal?.resize(cols, rows),
24
+ close: () => proc.terminal?.close(),
25
+ kill: (signal) => proc.kill(signal)
26
+ };
27
+ };
28
+ }
29
+ async function nodePtyDriver(spawn) {
30
+ const spawnPty = spawn ?? (await import("node-pty")).spawn;
31
+ return (request) => {
32
+ const [file, ...args] = request.argv;
33
+ const child = spawnPty(file ?? "", args, {
34
+ name: TERMINAL_NAME,
35
+ cols: request.cols,
36
+ rows: request.rows,
37
+ cwd: request.cwd,
38
+ env: Object.fromEntries(Object.entries(request.env).filter((entry) => entry[1] !== undefined))
39
+ });
40
+ let settle = () => {};
41
+ const exited = new Promise((resolve) => {
42
+ settle = resolve;
43
+ });
44
+ child.onData((data) => request.onData(data));
45
+ child.onExit(({ exitCode }) => settle(exitCode));
46
+ return {
47
+ pid: child.pid,
48
+ exited,
49
+ write: (data) => child.write(data),
50
+ resize: (cols, rows) => child.resize(cols, rows),
51
+ close: () => {},
52
+ kill: () => child.kill()
53
+ };
54
+ };
55
+ }
56
+
57
+ // ../kobe-daemon/src/daemon/pty-server.ts
58
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
59
+ import { createServer } from "node:net";
60
+ import { dirname } from "node:path";
61
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
62
+
63
+ // ../kobe-daemon/src/daemon/client-writer.ts
64
+ var DEFAULT_WRITE_HIGH_WATER_MARK = 8 * 1024 * 1024;
65
+
66
+ class ClientWriter {
67
+ socket;
68
+ highWaterMark;
69
+ onOverflow;
70
+ queue = [];
71
+ queuedBytes = 0;
72
+ paused = false;
73
+ draining = false;
74
+ droppedFrames = 0;
75
+ overflowed = false;
76
+ constructor(socket, options = {}) {
77
+ this.socket = socket;
78
+ this.highWaterMark = options.highWaterMark ?? DEFAULT_WRITE_HIGH_WATER_MARK;
79
+ this.onOverflow = options.onOverflow;
80
+ }
81
+ get isPaused() {
82
+ return this.paused;
83
+ }
84
+ get pendingBytes() {
85
+ return this.queuedBytes;
86
+ }
87
+ get pendingCount() {
88
+ return this.queue.length;
89
+ }
90
+ get dropped() {
91
+ return this.droppedFrames;
92
+ }
93
+ write(line, critical) {
94
+ if (this.overflowed)
95
+ return;
96
+ if (!this.paused) {
97
+ if (this.safeWrite(line))
98
+ return;
99
+ this.pause();
100
+ return;
101
+ }
102
+ this.enqueue({ line, bytes: Buffer.byteLength(line), critical });
103
+ }
104
+ enqueue(frame) {
105
+ this.queue.push(frame);
106
+ this.queuedBytes += frame.bytes;
107
+ if (this.queuedBytes <= this.highWaterMark)
108
+ return;
109
+ const survivors = [];
110
+ for (const queued of this.queue) {
111
+ if (this.queuedBytes > this.highWaterMark && !queued.critical) {
112
+ this.queuedBytes -= queued.bytes;
113
+ this.droppedFrames++;
114
+ continue;
115
+ }
116
+ survivors.push(queued);
117
+ }
118
+ this.queue = survivors;
119
+ if (this.queuedBytes <= this.highWaterMark || !this.onOverflow)
120
+ return;
121
+ this.overflowed = true;
122
+ this.queue = [];
123
+ this.queuedBytes = 0;
124
+ this.onOverflow();
125
+ }
126
+ pause() {
127
+ if (this.draining)
128
+ return;
129
+ this.paused = true;
130
+ this.draining = true;
131
+ this.socket.once("drain", () => {
132
+ this.draining = false;
133
+ this.paused = false;
134
+ this.flush();
135
+ });
136
+ }
137
+ flush() {
138
+ while (this.queue.length > 0) {
139
+ const frame = this.queue[0];
140
+ const ok = this.safeWrite(frame.line);
141
+ this.queue.shift();
142
+ this.queuedBytes -= frame.bytes;
143
+ if (!ok) {
144
+ this.pause();
145
+ return;
146
+ }
147
+ }
148
+ }
149
+ safeWrite(line) {
150
+ try {
151
+ return this.socket.write(line);
152
+ } catch {
153
+ return true;
154
+ }
155
+ }
156
+ }
157
+
158
+ // ../kobe-daemon/src/daemon/crash-log.ts
159
+ function toError(err) {
160
+ if (err instanceof Error)
161
+ return err;
162
+ let text;
163
+ try {
164
+ text = typeof err === "string" ? err : JSON.stringify(err);
165
+ } catch {
166
+ text = String(err);
167
+ }
168
+ return new Error(text);
169
+ }
170
+ function formatDaemonError(subsystem, err, now = new Date) {
171
+ const e = toError(err);
172
+ return `[${now.toISOString()}] daemon error [${subsystem}]: ${e.stack ?? `${e.name}: ${e.message}`}
173
+ `;
174
+ }
175
+ function logDaemonError(subsystem, err) {
176
+ process.stderr.write(formatDaemonError(subsystem, err));
177
+ }
178
+
179
+ // ../kobe-daemon/src/daemon/handler-validators.ts
180
+ function objectPayload(payload) {
181
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
182
+ return {};
183
+ return payload;
184
+ }
185
+ function requireString(payload, key) {
186
+ const value = payload[key];
187
+ if (typeof value !== "string" || value.length === 0)
188
+ throw new Error(`${key} is required`);
189
+ return value;
190
+ }
191
+
192
+ // ../kobe-daemon/src/daemon/paths.ts
193
+ import { createHash } from "node:crypto";
194
+ import { homedir, tmpdir } from "node:os";
195
+ import { join } from "node:path";
196
+ var SOCKET_PATH_SAFETY_LIMIT = 100;
197
+ function shortHomeTag(homeDir) {
198
+ return createHash("sha1").update(homeDir).digest("hex").slice(0, 8);
199
+ }
200
+ function fitSocketPath(naturalPath, homeDir, role, pidTag) {
201
+ if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
202
+ return naturalPath;
203
+ const tag = shortHomeTag(homeDir);
204
+ const suffix = pidTag === undefined ? "" : `-${pidTag}`;
205
+ const fallback = join(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
206
+ if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
207
+ return fallback;
208
+ throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
209
+ }
210
+ function isWindowsPipePath(path) {
211
+ return path.startsWith("\\\\.\\pipe\\") || path.startsWith("//./pipe/");
212
+ }
213
+ function windowsPipePath(homeDir, role) {
214
+ return `\\\\.\\pipe\\kobe-${shortHomeTag(homeDir)}-${role}`;
215
+ }
216
+ function defaultPtyHostSocketPath(homeDir, platform = process.platform) {
217
+ const override = process.env.KOBE_PTY_SOCKET_PATH;
218
+ if (override && override.length > 0)
219
+ return override;
220
+ const explicit = homeDir ?? process.env.KOBE_HOME_DIR;
221
+ if (platform === "win32")
222
+ return windowsPipePath(explicit || homedir(), "pty");
223
+ if (explicit && explicit.length > 0) {
224
+ return fitSocketPath(join(explicit, ".kobe", "pty.sock"), explicit, "pty");
225
+ }
226
+ const runtimeDir = process.env.XDG_RUNTIME_DIR;
227
+ if (runtimeDir && runtimeDir.length > 0) {
228
+ return fitSocketPath(join(runtimeDir, "kobe-pty.sock"), runtimeDir, "pty");
229
+ }
230
+ const home = homedir();
231
+ return fitSocketPath(join(home, ".kobe", "pty.sock"), home, "pty");
232
+ }
233
+ function defaultPtyHostPidPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
234
+ const override = process.env.KOBE_PTY_PID_PATH;
235
+ if (override && override.length > 0)
236
+ return override;
237
+ return join(homeDir, ".kobe", "pty.pid");
238
+ }
239
+ // ../kobe-daemon/src/daemon/channels.ts
240
+ var CHANNEL_NAMES = [
241
+ "task.snapshot",
242
+ "issue.snapshot",
243
+ "active-task",
244
+ "update",
245
+ "engine-state",
246
+ "attention.inbox",
247
+ "ui-prefs",
248
+ "keybindings",
249
+ "task.jobs",
250
+ "worktree.changes",
251
+ "transcript.activity",
252
+ "session.deliver",
253
+ "notice.event",
254
+ "usage.snapshot"
255
+ ];
256
+ var CHANNEL_NAME_SET = new Set(CHANNEL_NAMES);
257
+
258
+ // ../kobe-daemon/src/daemon/protocol.ts
259
+ var DAEMON_PROTOCOL_VERSION = 4;
260
+ function frameToLine(frame) {
261
+ return `${JSON.stringify(frame)}
262
+ `;
263
+ }
264
+
265
+ // ../kobe-daemon/src/daemon/pty-host.ts
266
+ import { StringDecoder } from "node:string_decoder";
267
+
268
+ // ../kobe-daemon/src/daemon/platform-shell.js
269
+ import { existsSync } from "node:fs";
270
+ function windowsBashCandidates(env) {
271
+ const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && `${env.LOCALAPPDATA}\\Programs`];
272
+ return roots.filter(Boolean).map((root) => `${root}\\Git\\bin\\bash.exe`);
273
+ }
274
+ var WINDOWS_BASH_FALLBACK = "C:\\Program Files\\Git\\bin\\bash.exe";
275
+ function isWindowsSpawnable(shellPath) {
276
+ return /^[A-Za-z]:[\\/]/.test(shellPath) || shellPath.startsWith("\\\\");
277
+ }
278
+ var cache = new Map;
279
+ function resolveLoginShell(options = {}) {
280
+ const fallback = options.fallback ?? "/bin/bash";
281
+ const platform = options.platform ?? process.platform;
282
+ const env = options.env ?? process.env;
283
+ const exists = options.exists ?? existsSync;
284
+ const cacheable = platform === process.platform && env === process.env && options.exists === undefined;
285
+ if (cacheable) {
286
+ const hit = cache.get(fallback);
287
+ if (hit)
288
+ return hit;
289
+ }
290
+ const resolved = resolveUncached(fallback, platform, env, exists);
291
+ if (cacheable)
292
+ cache.set(fallback, resolved);
293
+ return resolved;
294
+ }
295
+ function resolveUncached(fallback, platform, env, exists) {
296
+ const explicit = env.SHELL?.trim();
297
+ if (platform !== "win32")
298
+ return explicit || fallback;
299
+ if (explicit && isWindowsSpawnable(explicit) && exists(explicit))
300
+ return explicit;
301
+ for (const candidate of windowsBashCandidates(env)) {
302
+ if (exists(candidate))
303
+ return candidate;
304
+ }
305
+ return WINDOWS_BASH_FALLBACK;
306
+ }
307
+
308
+ // ../kobe-daemon/src/daemon/pty-env.js
309
+ var IDENTITY_VARS = new Set([
310
+ "TERM_PROGRAM",
311
+ "TERM_PROGRAM_VERSION",
312
+ "TERM_SESSION_ID",
313
+ "TERM_FEATURES",
314
+ "LC_TERMINAL",
315
+ "LC_TERMINAL_VERSION",
316
+ "__CFBundleIdentifier",
317
+ "VTE_VERSION",
318
+ "WT_SESSION",
319
+ "WT_PROFILE_ID",
320
+ "TMUX",
321
+ "TMUX_PANE",
322
+ "ZELLIJ",
323
+ "STY",
324
+ "WINDOW"
325
+ ]);
326
+ var IDENTITY_PREFIXES = ["ITERM_", "KITTY_", "GHOSTTY_", "WEZTERM_", "ALACRITTY_", "KONSOLE_", "ZELLIJ_"];
327
+ function embeddedTerminalEnv(base, overrides = {}) {
328
+ const env = {};
329
+ for (const [key, value] of Object.entries(base)) {
330
+ if (IDENTITY_VARS.has(key))
331
+ continue;
332
+ if (IDENTITY_PREFIXES.some((prefix) => key.startsWith(prefix)))
333
+ continue;
334
+ env[key] = value;
335
+ }
336
+ return { ...env, ...overrides };
337
+ }
338
+
339
+ // ../kobe-daemon/src/daemon/pty-observability.ts
340
+ var OSC_TITLE_RE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
341
+ var TITLE_CARRY_CAP = 1024;
342
+ function titleCarryFrom(rest) {
343
+ const osc = rest.lastIndexOf("\x1B]");
344
+ if (osc !== -1)
345
+ return rest.slice(osc, osc + TITLE_CARRY_CAP);
346
+ return rest.endsWith("\x1B") ? "\x1B" : "";
347
+ }
348
+ function foldOscTitle(prevCarry, chunkText) {
349
+ const text = prevCarry + chunkText;
350
+ let title = null;
351
+ let end = 0;
352
+ OSC_TITLE_RE.lastIndex = 0;
353
+ for (let match = OSC_TITLE_RE.exec(text);match; match = OSC_TITLE_RE.exec(text)) {
354
+ title = match[1] ?? "";
355
+ end = match.index + match[0].length;
356
+ }
357
+ return { title, carry: titleCarryFrom(text.slice(end)) };
358
+ }
359
+ function scanOscTitle(session, buf) {
360
+ const { title, carry } = foldOscTitle(session.titleCarry, session.titleDecoder.write(buf));
361
+ if (title !== null)
362
+ session.title = title;
363
+ session.titleCarry = carry;
364
+ }
365
+ function peekRing(session, sinceOffset) {
366
+ if (!session)
367
+ return { exists: false, alive: false, pid: null, offset: 0, data: "", sinceValid: false };
368
+ const windowStart = session.totalBytes - session.bytes;
369
+ let buf = Buffer.concat(session.chunks);
370
+ let sinceValid = false;
371
+ if (sinceOffset !== undefined && sinceOffset >= windowStart && sinceOffset <= session.totalBytes) {
372
+ buf = buf.subarray(sinceOffset - windowStart);
373
+ sinceValid = true;
374
+ }
375
+ return {
376
+ exists: true,
377
+ alive: session.alive,
378
+ pid: session.proc?.pid ?? null,
379
+ offset: session.totalBytes,
380
+ data: buf.toString("base64"),
381
+ sinceValid
382
+ };
383
+ }
384
+
385
+ // ../kobe-daemon/src/daemon/pty-termination.ts
386
+ async function settledWithin(exited, ms) {
387
+ let timer;
388
+ const settled = await Promise.race([
389
+ exited.then(() => true, () => true),
390
+ new Promise((resolve) => {
391
+ timer = setTimeout(() => resolve(false), ms);
392
+ })
393
+ ]);
394
+ if (timer)
395
+ clearTimeout(timer);
396
+ return settled;
397
+ }
398
+ function signalProcessGroup(pid, signal, fallback, platform = process.platform) {
399
+ if (platform !== "win32" && pid > 1) {
400
+ try {
401
+ process.kill(-pid, signal);
402
+ return;
403
+ } catch {}
404
+ }
405
+ try {
406
+ fallback();
407
+ } catch {}
408
+ }
409
+
410
+ // ../kobe-daemon/src/daemon/pty-host.ts
411
+ var DEFAULT_SCROLLBACK_CAP = 512 * 1024;
412
+ var TERMINATION_GRACE_MS = 500;
413
+
414
+ class PtyHost {
415
+ sessions = new Map;
416
+ opts;
417
+ parkRestoreDeltas = 0;
418
+ parkRestoreFallbacks = 0;
419
+ spare = null;
420
+ constructor(opts = {}) {
421
+ this.opts = opts;
422
+ }
423
+ open(key, spec, token, sink, sinceOffset, sincePid) {
424
+ let session = this.sessions.get(key);
425
+ let created = false;
426
+ if (!session) {
427
+ created = true;
428
+ session = this.adoptSpare(key, spec) ?? this.spawn(key, spec);
429
+ this.sessions.set(key, session);
430
+ } else if (session.alive && (session.cols !== spec.cols || session.rows !== spec.rows)) {
431
+ this.resize(key, spec.cols, spec.rows);
432
+ }
433
+ session.sinks.set(token, sink);
434
+ session.parked = false;
435
+ session.parkedScreenBytes = 0;
436
+ const windowStart = session.totalBytes - session.bytes;
437
+ let replay = Buffer.concat(session.chunks);
438
+ let sinceValid = false;
439
+ if (!created && sinceOffset !== undefined && sinceOffset >= windowStart && sinceOffset <= session.totalBytes && sincePid !== undefined && sincePid === session.proc?.pid) {
440
+ replay = replay.subarray(sinceOffset - windowStart);
441
+ sinceValid = true;
442
+ }
443
+ if (sinceOffset !== undefined && sincePid !== undefined) {
444
+ if (sinceValid)
445
+ this.parkRestoreDeltas++;
446
+ else
447
+ this.parkRestoreFallbacks++;
448
+ }
449
+ return {
450
+ replay: replay.toString("base64"),
451
+ alive: session.alive,
452
+ pid: session.proc?.pid ?? null,
453
+ created,
454
+ offset: session.totalBytes,
455
+ sinceValid
456
+ };
457
+ }
458
+ warm(cwd, shell, cols = 80, rows = 24) {
459
+ const argv0 = shell ?? resolveLoginShell();
460
+ if (this.spare?.alive && this.spare.cwd === cwd && this.spare.command[0] === argv0)
461
+ return;
462
+ const old = this.spare;
463
+ this.spare = null;
464
+ if (old)
465
+ this.endChild(old);
466
+ const session = this.spawn("::spare", { cwd, command: [argv0], cols, rows }, true);
467
+ this.spare = session.alive ? session : null;
468
+ }
469
+ adoptSpare(key, spec) {
470
+ const spare = this.spare;
471
+ if (!spare?.alive || spare.cwd !== spec.cwd)
472
+ return null;
473
+ const want = spec.command && spec.command.length > 0 ? spec.command : [spec.shell ?? resolveLoginShell()];
474
+ if (want.length !== 1 || want[0] !== spare.command[0])
475
+ return null;
476
+ this.spare = null;
477
+ spare.key = key;
478
+ if (spare.cols !== spec.cols || spare.rows !== spec.rows) {
479
+ spare.cols = spec.cols;
480
+ spare.rows = spec.rows;
481
+ try {
482
+ spare.proc?.resize(spec.cols, spec.rows);
483
+ } catch {
484
+ this.markExited(spare);
485
+ return null;
486
+ }
487
+ }
488
+ this.opts.log?.("pty", `adopted warm shell for ${key} (pid ${spare.proc?.pid})`);
489
+ this.opts.onSessionStart?.();
490
+ this.warm(spec.cwd, spare.command[0], spec.cols, spec.rows);
491
+ return spare;
492
+ }
493
+ write(key, data) {
494
+ const session = this.sessions.get(key);
495
+ if (!session?.alive || data.length === 0)
496
+ return;
497
+ try {
498
+ session.proc?.write(data);
499
+ } catch {}
500
+ }
501
+ resize(key, cols, rows) {
502
+ const session = this.sessions.get(key);
503
+ if (!session?.alive)
504
+ return;
505
+ session.cols = cols;
506
+ session.rows = rows;
507
+ try {
508
+ session.proc?.resize(cols, rows);
509
+ } catch {}
510
+ }
511
+ kill(key) {
512
+ const session = this.sessions.get(key);
513
+ if (!session)
514
+ return Promise.resolve();
515
+ this.sessions.delete(key);
516
+ return this.endChild(session);
517
+ }
518
+ detach(key, token, parked = false, parkedScreenBytes = 0) {
519
+ const session = this.sessions.get(key);
520
+ if (!session)
521
+ return;
522
+ session.sinks.delete(token);
523
+ if (session.sinks.size === 0) {
524
+ session.parked = parked;
525
+ session.parkedScreenBytes = parked ? Math.max(0, parkedScreenBytes) : 0;
526
+ }
527
+ }
528
+ detachClient(token) {
529
+ for (const session of this.sessions.values()) {
530
+ session.sinks.delete(token);
531
+ if (session.sinks.size === 0) {
532
+ session.parked = false;
533
+ session.parkedScreenBytes = 0;
534
+ }
535
+ }
536
+ }
537
+ list() {
538
+ return Array.from(this.sessions.values(), (s) => ({
539
+ key: s.key,
540
+ alive: s.alive,
541
+ pid: s.proc?.pid ?? null,
542
+ command: s.command,
543
+ title: s.title,
544
+ parked: s.parked,
545
+ parkedScreenBytes: s.parkedScreenBytes
546
+ }));
547
+ }
548
+ peek(key, sinceOffset) {
549
+ return peekRing(this.sessions.get(key), sinceOffset);
550
+ }
551
+ stats() {
552
+ let ringBytes = 0;
553
+ let parkedSessions = 0;
554
+ let parkedScreenBytes = 0;
555
+ for (const session of this.sessions.values()) {
556
+ ringBytes += session.bytes;
557
+ if (session.parked) {
558
+ parkedSessions++;
559
+ parkedScreenBytes += session.parkedScreenBytes;
560
+ }
561
+ }
562
+ return {
563
+ ringBytes,
564
+ ringCapacityBytes: this.sessions.size * (this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP),
565
+ parkedSessions,
566
+ parkedScreenBytes,
567
+ parkRestoreDeltas: this.parkRestoreDeltas,
568
+ parkRestoreFallbacks: this.parkRestoreFallbacks
569
+ };
570
+ }
571
+ sweepTasks(liveTaskIds) {
572
+ for (const key of Array.from(this.sessions.keys())) {
573
+ const taskId = key.split("::")[0] ?? key;
574
+ if (!liveTaskIds.has(taskId))
575
+ this.kill(key);
576
+ }
577
+ }
578
+ async killAll() {
579
+ const sessions = Array.from(this.sessions.keys(), (key) => this.kill(key));
580
+ const spare = this.spare;
581
+ this.spare = null;
582
+ if (spare)
583
+ sessions.push(this.endChild(spare));
584
+ await Promise.all(sessions);
585
+ }
586
+ liveCount() {
587
+ let n = 0;
588
+ for (const session of this.sessions.values())
589
+ if (session.alive)
590
+ n++;
591
+ return n;
592
+ }
593
+ spawn(key, spec, spare = false) {
594
+ const argv = spec.command && spec.command.length > 0 ? [...spec.command] : [spec.shell ?? resolveLoginShell()];
595
+ const session = {
596
+ key,
597
+ cwd: spec.cwd,
598
+ proc: null,
599
+ alive: true,
600
+ chunks: [],
601
+ bytes: 0,
602
+ totalBytes: 0,
603
+ cols: spec.cols,
604
+ rows: spec.rows,
605
+ command: argv,
606
+ title: "",
607
+ titleCarry: "",
608
+ titleDecoder: new StringDecoder("utf8"),
609
+ sinks: new Map,
610
+ parked: false,
611
+ parkedScreenBytes: 0
612
+ };
613
+ try {
614
+ session.proc = (this.opts.driver ?? bunTerminalDriver())({
615
+ argv,
616
+ cwd: spec.cwd,
617
+ env: embeddedTerminalEnv(process.env, {
618
+ TERM: "xterm-256color",
619
+ COLUMNS: String(spec.cols),
620
+ LINES: String(spec.rows),
621
+ BASH_SILENCE_DEPRECATION_WARNING: "1",
622
+ KOBE_TERMINAL_PTY: "1"
623
+ }),
624
+ cols: spec.cols,
625
+ rows: spec.rows,
626
+ onData: (data) => this.onData(session, data)
627
+ });
628
+ session.proc.exited.then(() => this.markExited(session), () => this.markExited(session));
629
+ this.opts.log?.("pty", `spawned ${argv[0]} for ${key} (pid ${session.proc.pid})`);
630
+ if (!spare)
631
+ this.opts.onSessionStart?.();
632
+ } catch (err) {
633
+ session.alive = false;
634
+ this.opts.log?.("pty", `spawn failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
635
+ }
636
+ return session;
637
+ }
638
+ onData(session, data) {
639
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
640
+ scanOscTitle(session, buf);
641
+ session.chunks.push(buf);
642
+ session.bytes += buf.byteLength;
643
+ session.totalBytes += buf.byteLength;
644
+ while (session.bytes > (this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP) && session.chunks.length > 1) {
645
+ const dropped = session.chunks.shift();
646
+ if (dropped)
647
+ session.bytes -= dropped.byteLength;
648
+ }
649
+ if (session.sinks.size === 0)
650
+ return;
651
+ const frame = {
652
+ type: "event",
653
+ name: "pty.data",
654
+ payload: { key: session.key, data: buf.toString("base64") }
655
+ };
656
+ for (const sink of session.sinks.values())
657
+ sink(frame);
658
+ }
659
+ markExited(session) {
660
+ if (!session.alive)
661
+ return;
662
+ session.alive = false;
663
+ try {
664
+ session.proc?.close();
665
+ } catch {}
666
+ const frame = {
667
+ type: "event",
668
+ name: "pty.exit",
669
+ payload: { key: session.key, pid: session.proc?.pid ?? null }
670
+ };
671
+ for (const sink of session.sinks.values())
672
+ sink(frame);
673
+ this.opts.log?.("pty", `session ${session.key} exited`);
674
+ this.opts.onSessionEnd?.();
675
+ }
676
+ async endChild(session) {
677
+ if (!session.alive)
678
+ return;
679
+ const proc = session.proc;
680
+ if (!proc) {
681
+ this.markExited(session);
682
+ return;
683
+ }
684
+ signalProcessGroup(proc.pid, "SIGTERM", () => proc.kill("SIGTERM"));
685
+ if (!await settledWithin(proc.exited, TERMINATION_GRACE_MS)) {
686
+ signalProcessGroup(proc.pid, "SIGKILL", () => proc.kill("SIGKILL"));
687
+ await settledWithin(proc.exited, TERMINATION_GRACE_MS);
688
+ }
689
+ this.markExited(session);
690
+ }
691
+ }
692
+
693
+ // ../kobe-daemon/src/daemon/pty-server.ts
694
+ var DEFAULT_IDLE_EXIT_MS = 60000;
695
+ function resolveIdleExitMs() {
696
+ const raw = process.env.KOBE_PTY_IDLE_EXIT_MS;
697
+ if (raw === undefined)
698
+ return DEFAULT_IDLE_EXIT_MS;
699
+ const n = Number(raw);
700
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_IDLE_EXIT_MS;
701
+ }
702
+ async function startPtyHostServer(options = {}) {
703
+ const socketPath = options.socketPath ?? defaultPtyHostSocketPath();
704
+ const pidPath = options.pidPath ?? defaultPtyHostPidPath();
705
+ const idleExitMs = options.idleExitMs || resolveIdleExitMs();
706
+ const log = options.log ?? (() => {});
707
+ const clients = new Set;
708
+ let stopping = false;
709
+ let idleTimer = null;
710
+ const cancelIdle = () => {
711
+ if (idleTimer)
712
+ clearTimeout(idleTimer);
713
+ idleTimer = null;
714
+ };
715
+ const armIdle = () => {
716
+ if (stopping)
717
+ return;
718
+ cancelIdle();
719
+ idleTimer = setTimeout(() => {
720
+ if (stopping || ptys.liveCount() > 0)
721
+ return;
722
+ log("idle", `no live sessions for ${idleExitMs}ms — exiting`);
723
+ stop();
724
+ }, idleExitMs);
725
+ };
726
+ const ptys = new PtyHost({
727
+ onSessionStart: cancelIdle,
728
+ onSessionEnd: () => {
729
+ if (ptys.liveCount() === 0)
730
+ armIdle();
731
+ },
732
+ driver: options.driver,
733
+ log
734
+ });
735
+ const pipeSocket = isWindowsPipePath(socketPath);
736
+ if (!pipeSocket)
737
+ await mkdir(dirname(socketPath), { recursive: true });
738
+ await mkdir(dirname(pidPath), { recursive: true });
739
+ const server = createServer((socket) => {
740
+ const client = {
741
+ socket,
742
+ writer: new ClientWriter(socket, {
743
+ onOverflow: () => {
744
+ log("backpressure", "disconnecting PTY client whose critical queue exceeded 8MiB");
745
+ socket.destroy();
746
+ }
747
+ }),
748
+ buffer: ""
749
+ };
750
+ clients.add(client);
751
+ const decoder = new StringDecoder2("utf8");
752
+ socket.on("data", (chunk) => {
753
+ client.buffer += decoder.write(chunk);
754
+ drain(client);
755
+ });
756
+ socket.on("error", () => {});
757
+ socket.on("close", () => {
758
+ clients.delete(client);
759
+ ptys.detachClient(client);
760
+ });
761
+ });
762
+ const api = {
763
+ socketPath,
764
+ pidPath,
765
+ async close() {
766
+ if (stopping)
767
+ return;
768
+ stopping = true;
769
+ cancelIdle();
770
+ await ptys.killAll();
771
+ for (const client of Array.from(clients))
772
+ client.socket.destroy();
773
+ await new Promise((resolve) => server.close(() => resolve()));
774
+ if (!pipeSocket)
775
+ await unlink(socketPath).catch(() => {});
776
+ await unlink(pidPath).catch(() => {});
777
+ }
778
+ };
779
+ async function stop() {
780
+ await api.close().catch((err) => logDaemonError("pty-host-shutdown", err));
781
+ options.onStop?.();
782
+ }
783
+ function dispatch(req, client) {
784
+ switch (req.name) {
785
+ case "hello":
786
+ return { protocolVersion: DAEMON_PROTOCOL_VERSION, ptyHost: true, pid: process.pid };
787
+ case "pty.open": {
788
+ const payload = objectPayload(req.payload);
789
+ return ptys.open(requireString(payload, "key"), {
790
+ cwd: requireString(payload, "cwd"),
791
+ command: Array.isArray(payload.command) ? payload.command.filter((c) => typeof c === "string") : undefined,
792
+ shell: typeof payload.shell === "string" ? payload.shell : undefined,
793
+ cols: typeof payload.cols === "number" ? payload.cols : 80,
794
+ rows: typeof payload.rows === "number" ? payload.rows : 24
795
+ }, client, (frame) => writeFrame(client, frame), typeof payload.sinceOffset === "number" ? payload.sinceOffset : undefined, typeof payload.sincePid === "number" ? payload.sincePid : undefined);
796
+ }
797
+ case "pty.write": {
798
+ const payload = objectPayload(req.payload);
799
+ ptys.write(requireString(payload, "key"), typeof payload.data === "string" ? payload.data : "");
800
+ return {};
801
+ }
802
+ case "pty.resize": {
803
+ const payload = objectPayload(req.payload);
804
+ ptys.resize(requireString(payload, "key"), typeof payload.cols === "number" ? payload.cols : 80, typeof payload.rows === "number" ? payload.rows : 24);
805
+ return {};
806
+ }
807
+ case "pty.kill":
808
+ ptys.kill(requireString(objectPayload(req.payload), "key"));
809
+ return {};
810
+ case "pty.detach":
811
+ {
812
+ const payload = objectPayload(req.payload);
813
+ ptys.detach(requireString(payload, "key"), client, payload.parked === true, typeof payload.parkedScreenBytes === "number" ? payload.parkedScreenBytes : 0);
814
+ }
815
+ return {};
816
+ case "pty.list":
817
+ return { pid: process.pid, rssBytes: process.memoryUsage().rss, sessions: ptys.list(), stats: ptys.stats() };
818
+ case "pty.peek": {
819
+ const payload = objectPayload(req.payload);
820
+ return ptys.peek(requireString(payload, "key"), typeof payload.sinceOffset === "number" ? payload.sinceOffset : undefined);
821
+ }
822
+ case "pty.warm": {
823
+ const payload = objectPayload(req.payload);
824
+ ptys.warm(requireString(payload, "cwd"), typeof payload.shell === "string" ? payload.shell : undefined, typeof payload.cols === "number" ? payload.cols : undefined, typeof payload.rows === "number" ? payload.rows : undefined);
825
+ return {};
826
+ }
827
+ case "pty.sweep": {
828
+ const payload = objectPayload(req.payload);
829
+ const ids = Array.isArray(payload.liveTaskIds) ? payload.liveTaskIds.filter((id) => typeof id === "string") : [];
830
+ ptys.sweepTasks(new Set(ids));
831
+ return {};
832
+ }
833
+ case "daemon.stop":
834
+ setTimeout(() => void stop(), 0).unref();
835
+ return {};
836
+ default:
837
+ throw new Error(`unknown pty-host request: ${req.name}`);
838
+ }
839
+ }
840
+ function drain(client) {
841
+ let nl = client.buffer.indexOf(`
842
+ `);
843
+ while (nl !== -1) {
844
+ const line = client.buffer.slice(0, nl);
845
+ client.buffer = client.buffer.slice(nl + 1);
846
+ if (line.trim().length > 0) {
847
+ let frame = null;
848
+ try {
849
+ frame = JSON.parse(line);
850
+ } catch {
851
+ writeFrame(client, { type: "response", id: "parse-error", error: { message: "malformed frame" } });
852
+ }
853
+ if (frame) {
854
+ if (frame.type !== "request") {
855
+ writeFrame(client, { type: "response", id: "parse-error", error: { message: "requests only" } });
856
+ } else {
857
+ try {
858
+ writeFrame(client, { type: "response", id: frame.id, name: frame.name, payload: dispatch(frame, client) });
859
+ } catch (err) {
860
+ writeFrame(client, {
861
+ type: "response",
862
+ id: frame.id,
863
+ name: frame.name,
864
+ error: { message: err instanceof Error ? err.message : String(err) }
865
+ });
866
+ }
867
+ }
868
+ }
869
+ }
870
+ nl = client.buffer.indexOf(`
871
+ `);
872
+ }
873
+ }
874
+ await new Promise((resolve, reject) => {
875
+ server.once("error", reject);
876
+ server.listen(socketPath, () => resolve());
877
+ });
878
+ await writeFile(pidPath, `${process.pid}
879
+ `, "utf8");
880
+ armIdle();
881
+ log("boot", `pty host listening on ${socketPath}`);
882
+ return api;
883
+ }
884
+ function writeFrame(client, frame) {
885
+ client.writer.write(frameToLine(frame), true);
886
+ }
887
+
888
+ // ../kobe-daemon/src/daemon/pty-host-node-entry.ts
889
+ async function main() {
890
+ process.on("uncaughtException", (err) => console.error(`[pty-host crash] ${err?.stack ?? String(err)}`));
891
+ process.on("unhandledRejection", (err) => console.error(`[pty-host reject] ${String(err)}`));
892
+ const driver = await nodePtyDriver();
893
+ const server = await startPtyHostServer({
894
+ driver,
895
+ log: (event, message) => console.log(`[pty-host ${event}] ${message}`),
896
+ onStop: () => process.exit(0)
897
+ });
898
+ console.log(`kobe pty-host (node): listening on ${server.socketPath}`);
899
+ const shutdown = async () => {
900
+ await server.close();
901
+ process.exit(0);
902
+ };
903
+ process.once("SIGINT", () => void shutdown());
904
+ process.once("SIGTERM", () => void shutdown());
905
+ }
906
+ await main();