linkshell-cli 0.9.0 → 0.10.0

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 (153) hide show
  1. package/README.md +11 -6
  2. package/dist/cli/src/commands/gateway.d.ts +2 -0
  3. package/dist/cli/src/commands/gateway.js +29 -0
  4. package/dist/cli/src/commands/gateway.js.map +1 -0
  5. package/dist/cli/src/commands/host.d.ts +2 -0
  6. package/dist/cli/src/commands/host.js +12 -2
  7. package/dist/cli/src/commands/host.js.map +1 -1
  8. package/dist/cli/src/commands/setup.js +4 -2
  9. package/dist/cli/src/commands/setup.js.map +1 -1
  10. package/dist/cli/src/index.js +12 -334
  11. package/dist/cli/src/index.js.map +1 -1
  12. package/dist/cli/src/utils/daemon.d.ts +0 -7
  13. package/dist/cli/src/utils/daemon.js +1 -18
  14. package/dist/cli/src/utils/daemon.js.map +1 -1
  15. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  16. package/dist/gateway/src/accounts.d.ts +6 -0
  17. package/dist/gateway/src/accounts.js +32 -0
  18. package/dist/gateway/src/accounts.js.map +1 -0
  19. package/dist/gateway/src/index.d.ts +4 -0
  20. package/dist/gateway/src/index.js +5 -0
  21. package/dist/gateway/src/index.js.map +1 -0
  22. package/dist/gateway/src/main.d.ts +1 -0
  23. package/dist/gateway/src/main.js +57 -0
  24. package/dist/gateway/src/main.js.map +1 -0
  25. package/dist/gateway/src/rate-limit.d.ts +22 -0
  26. package/dist/gateway/src/rate-limit.js +66 -0
  27. package/dist/gateway/src/rate-limit.js.map +1 -0
  28. package/dist/gateway/src/relay.d.ts +84 -0
  29. package/dist/gateway/src/relay.js +434 -0
  30. package/dist/gateway/src/relay.js.map +1 -0
  31. package/dist/gateway/src/serve.d.ts +34 -0
  32. package/dist/gateway/src/serve.js +102 -0
  33. package/dist/gateway/src/serve.js.map +1 -0
  34. package/dist/gateway/src/store.d.ts +35 -0
  35. package/dist/gateway/src/store.js +109 -0
  36. package/dist/gateway/src/store.js.map +1 -0
  37. package/dist/gateway/src/subscription.d.ts +18 -0
  38. package/dist/gateway/src/subscription.js +46 -0
  39. package/dist/gateway/src/subscription.js.map +1 -0
  40. package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
  41. package/dist/host/src/fs.d.ts +3 -3
  42. package/dist/host/src/input.d.ts +4 -4
  43. package/dist/host/src/terminals.d.ts +1 -1
  44. package/dist/wire/src/model.d.ts +6 -6
  45. package/dist/wire/src/relay.d.ts +8 -8
  46. package/dist/wire/src/rpc.d.ts +818 -818
  47. package/dist/wire/src/updates.d.ts +18 -18
  48. package/package.json +4 -13
  49. package/src/commands/gateway.ts +31 -0
  50. package/src/commands/host.ts +13 -2
  51. package/src/commands/setup.ts +4 -2
  52. package/src/index.ts +14 -429
  53. package/src/utils/daemon.ts +2 -27
  54. package/dist/cli/src/config.d.ts +0 -14
  55. package/dist/cli/src/config.js +0 -23
  56. package/dist/cli/src/config.js.map +0 -1
  57. package/dist/cli/src/machine-id.d.ts +0 -6
  58. package/dist/cli/src/machine-id.js +0 -50
  59. package/dist/cli/src/machine-id.js.map +0 -1
  60. package/dist/cli/src/providers.d.ts +0 -13
  61. package/dist/cli/src/providers.js +0 -77
  62. package/dist/cli/src/providers.js.map +0 -1
  63. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
  64. package/dist/cli/src/runtime/acp/acp-client.js +0 -430
  65. package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
  66. package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
  67. package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
  68. package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
  69. package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
  70. package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
  71. package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
  72. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
  73. package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
  74. package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
  75. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
  76. package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
  77. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
  78. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
  79. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
  80. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
  81. package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
  82. package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
  83. package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
  84. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
  85. package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
  86. package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
  87. package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
  88. package/dist/cli/src/runtime/bridge-session.js +0 -1364
  89. package/dist/cli/src/runtime/bridge-session.js.map +0 -1
  90. package/dist/cli/src/runtime/hook-command.d.ts +0 -9
  91. package/dist/cli/src/runtime/hook-command.js +0 -80
  92. package/dist/cli/src/runtime/hook-command.js.map +0 -1
  93. package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
  94. package/dist/cli/src/runtime/screen-fallback.js +0 -202
  95. package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
  96. package/dist/cli/src/runtime/screen-share.d.ts +0 -34
  97. package/dist/cli/src/runtime/screen-share.js +0 -376
  98. package/dist/cli/src/runtime/screen-share.js.map +0 -1
  99. package/dist/cli/src/runtime/scrollback.d.ts +0 -11
  100. package/dist/cli/src/runtime/scrollback.js +0 -33
  101. package/dist/cli/src/runtime/scrollback.js.map +0 -1
  102. package/dist/cli/src/runtime/usage-report.d.ts +0 -34
  103. package/dist/cli/src/runtime/usage-report.js +0 -332
  104. package/dist/cli/src/runtime/usage-report.js.map +0 -1
  105. package/dist/cli/src/utils/keep-awake.d.ts +0 -6
  106. package/dist/cli/src/utils/keep-awake.js +0 -48
  107. package/dist/cli/src/utils/keep-awake.js.map +0 -1
  108. package/dist/cli/src/utils/lan-ip.d.ts +0 -9
  109. package/dist/cli/src/utils/lan-ip.js +0 -61
  110. package/dist/cli/src/utils/lan-ip.js.map +0 -1
  111. package/dist/host/src/input-helper.d.ts +0 -1
  112. package/dist/host/src/input-helper.js +0 -564
  113. package/dist/host/src/input-helper.js.map +0 -1
  114. package/dist/shared-protocol/src/index.d.ts +0 -17029
  115. package/dist/shared-protocol/src/index.js +0 -1064
  116. package/dist/shared-protocol/src/index.js.map +0 -1
  117. package/scripts/copy-web.mjs +0 -34
  118. package/src/config.ts +0 -36
  119. package/src/machine-id.ts +0 -61
  120. package/src/providers.ts +0 -97
  121. package/src/runtime/acp/acp-client.ts +0 -521
  122. package/src/runtime/acp/agent-process-discovery.ts +0 -308
  123. package/src/runtime/acp/agent-session.ts +0 -1186
  124. package/src/runtime/acp/agent-workspace.ts +0 -6590
  125. package/src/runtime/acp/claude-sdk-client.ts +0 -1221
  126. package/src/runtime/acp/claude-stream-json-client.ts +0 -633
  127. package/src/runtime/acp/json-rpc.ts +0 -184
  128. package/src/runtime/acp/provider-resolver.ts +0 -154
  129. package/src/runtime/bridge-session.ts +0 -1549
  130. package/src/runtime/hook-command.ts +0 -73
  131. package/src/runtime/screen-fallback.ts +0 -235
  132. package/src/runtime/screen-share.ts +0 -426
  133. package/src/runtime/scrollback.ts +0 -43
  134. package/src/runtime/usage-report.ts +0 -342
  135. package/src/utils/keep-awake.ts +0 -61
  136. package/src/utils/lan-ip.ts +0 -61
  137. package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
  138. package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
  139. package/web/assets/TerminalPanel-CTSklkct.js +0 -1
  140. package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
  141. package/web/assets/index-BsnDaeCt.css +0 -33
  142. package/web/assets/index-q85psYZ5.js +0 -36
  143. package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
  144. package/web/assets/vendor-react-M7xG3qiw.js +0 -49
  145. package/web/assets/vendor-xterm-D15THcI4.js +0 -9
  146. package/web/index.html +0 -37
  147. package/web/logos/claude.png +0 -0
  148. package/web/logos/codex.png +0 -0
  149. package/web/logos/copilot.png +0 -0
  150. package/web/logos/gemini.png +0 -0
  151. package/web/logos/linkshell-1024.png +0 -0
  152. package/web/logos/linkshell-splash.png +0 -0
  153. package/web/logos/linkshell.png +0 -0
@@ -1,426 +0,0 @@
1
- import { spawn, execSync } from "node:child_process";
2
- import type { ChildProcess } from "node:child_process";
3
- import { createRequire } from "node:module";
4
- import { platform } from "node:os";
5
- import { createEnvelope } from "@linkshell/protocol";
6
- import type { Envelope } from "@linkshell/protocol";
7
-
8
- const RTP_MTU = 1200; // Max payload size before FU-A fragmentation
9
-
10
- /** Detect the avfoundation device index for "Capture screen" on macOS. */
11
- function detectMacScreenIndex(): string {
12
- try {
13
- const out = execSync('ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true', {
14
- timeout: 5000,
15
- stdio: ["pipe", "pipe", "pipe"],
16
- }).toString();
17
- // Look for lines like: [AVFoundation ...] [2] Capture screen 0
18
- const match = out.match(/\[(\d+)]\s+Capture screen/);
19
- if (match) return match[1]!;
20
- } catch {}
21
- return "1"; // legacy fallback
22
- }
23
-
24
- export interface ScreenShareOptions {
25
- sessionId: string;
26
- fps: number;
27
- quality: number;
28
- scale: number;
29
- turnUrl?: string;
30
- turnUser?: string;
31
- turnPass?: string;
32
- onSignal: (envelope: Envelope) => void;
33
- onStatus: (envelope: Envelope) => void;
34
- }
35
-
36
- /**
37
- * WebRTC screen sharing using werift (pure TS WebRTC) + ffmpeg.
38
- *
39
- * Flow:
40
- * 1. ffmpeg captures screen → H.264 → writes to stdout as raw annexb
41
- * 2. We read H.264 NAL units and push them into werift video track
42
- * 3. werift handles ICE/DTLS/SRTP and sends to remote peer
43
- * 4. Signaling (SDP offer/answer, ICE candidates) goes through existing WebSocket
44
- */
45
- export class ScreenShare {
46
- private pc: any; // werift.RTCPeerConnection
47
- private ffmpeg: ChildProcess | undefined;
48
- private active = false;
49
- private readonly options: ScreenShareOptions;
50
-
51
- constructor(options: ScreenShareOptions) {
52
- this.options = options;
53
- }
54
-
55
- static isAvailable(): boolean {
56
- // Check ffmpeg exists
57
- try {
58
- execSync("which ffmpeg", { stdio: "pipe" });
59
- } catch {
60
- return false;
61
- }
62
- // Check werift can be resolved from this package's context
63
- try {
64
- const require = createRequire(import.meta.url);
65
- require.resolve("werift");
66
- const werift = require("werift") as any;
67
- if (!werift.RTCPeerConnection || !werift.MediaStreamTrack) {
68
- return false;
69
- }
70
- return true;
71
- } catch {
72
- return false;
73
- }
74
- }
75
-
76
- async start(): Promise<void> {
77
- if (this.active) return;
78
- this.active = true;
79
-
80
- try {
81
- const werift = await import("werift") as any;
82
-
83
- // ICE servers
84
- const iceServers: any[] = [
85
- { urls: "stun:stun.l.google.com:19302" },
86
- ];
87
- if (this.options.turnUrl) {
88
- iceServers.push({
89
- urls: this.options.turnUrl,
90
- username: this.options.turnUser ?? "",
91
- credential: this.options.turnPass ?? "",
92
- });
93
- }
94
-
95
- this.pc = new werift.RTCPeerConnection({
96
- iceServers,
97
- bundlePolicy: "max-bundle",
98
- codecs: { video: [werift.useH264()] },
99
- });
100
-
101
- // Create video track — werift defaults to H264, no need to set codecs explicitly
102
- // (setting transceiver.codecs crashes createOffer in werift 0.22.x)
103
- const videoTrack = new werift.MediaStreamTrack({ kind: "video" });
104
- this.pc.addTransceiver(videoTrack, {
105
- direction: "sendonly",
106
- });
107
-
108
- // ICE candidate → send to remote
109
- this.pc.onIceCandidate.subscribe((candidate: any) => {
110
- if (candidate) {
111
- this.options.onSignal(
112
- createEnvelope({
113
- type: "screen.ice",
114
- sessionId: this.options.sessionId,
115
- payload: {
116
- candidate: candidate.candidate,
117
- sdpMid: candidate.sdpMid ?? null,
118
- sdpMLineIndex: candidate.sdpMLineIndex ?? null,
119
- },
120
- }),
121
- );
122
- }
123
- });
124
-
125
- // Connection state changes
126
- this.pc.connectionStateChange.subscribe((state: string) => {
127
- if (state === "connected") {
128
- this.options.onStatus(
129
- createEnvelope({
130
- type: "screen.status",
131
- sessionId: this.options.sessionId,
132
- payload: { active: true, mode: "webrtc" as const },
133
- }),
134
- );
135
- } else if (state === "failed" || state === "disconnected" || state === "closed") {
136
- this.options.onStatus(
137
- createEnvelope({
138
- type: "screen.status",
139
- sessionId: this.options.sessionId,
140
- payload: { active: false, mode: "off" as const, error: `WebRTC ${state}` },
141
- }),
142
- );
143
- }
144
- });
145
-
146
- // Create offer
147
- const offer = await this.pc.createOffer();
148
- await this.pc.setLocalDescription(offer);
149
-
150
- // Notify client that WebRTC mode is active BEFORE sending the offer,
151
- // so the client mounts the WebView and is ready to process the offer.
152
- this.options.onStatus(
153
- createEnvelope({
154
- type: "screen.status",
155
- sessionId: this.options.sessionId,
156
- payload: { active: true, mode: "webrtc" as const },
157
- }),
158
- );
159
-
160
- this.options.onSignal(
161
- createEnvelope({
162
- type: "screen.offer",
163
- sessionId: this.options.sessionId,
164
- payload: { sdp: offer.sdp },
165
- }),
166
- );
167
-
168
- // Start ffmpeg capture (will begin sending once ICE connects)
169
- this.startFfmpeg(videoTrack, werift);
170
-
171
- } catch (err) {
172
- this.options.onStatus(
173
- createEnvelope({
174
- type: "screen.status",
175
- sessionId: this.options.sessionId,
176
- payload: {
177
- active: false,
178
- mode: "off" as const,
179
- error: `WebRTC init failed: ${err instanceof Error ? err.message : String(err)}`,
180
- },
181
- }),
182
- );
183
- this.active = false;
184
- }
185
- }
186
-
187
- async handleAnswer(sdp: string): Promise<void> {
188
- if (!this.pc) return;
189
- try {
190
- const werift = await import("werift") as any;
191
- await this.pc.setRemoteDescription(
192
- new werift.RTCSessionDescription(sdp, "answer"),
193
- );
194
- } catch (err) {
195
- process.stderr.write(`[screen-share] failed to set answer: ${err}\n`);
196
- }
197
- }
198
-
199
- async handleIceCandidate(candidate: string, sdpMid?: string | null, sdpMLineIndex?: number | null): Promise<void> {
200
- if (!this.pc) return;
201
- try {
202
- const werift = await import("werift") as any;
203
- await this.pc.addIceCandidate(
204
- new werift.RTCIceCandidate({ candidate, sdpMid: sdpMid ?? undefined, sdpMLineIndex: sdpMLineIndex ?? undefined }),
205
- );
206
- } catch (err) {
207
- process.stderr.write(`[screen-share] failed to add ICE candidate: ${err}\n`);
208
- }
209
- }
210
-
211
- stop(): void {
212
- this.active = false;
213
- if (this.ffmpeg) {
214
- this.ffmpeg.kill("SIGTERM");
215
- this.ffmpeg = undefined;
216
- }
217
- if (this.pc) {
218
- try { this.pc.close(); } catch {}
219
- this.pc = undefined;
220
- }
221
- this.options.onStatus(
222
- createEnvelope({
223
- type: "screen.status",
224
- sessionId: this.options.sessionId,
225
- payload: { active: false, mode: "off" as const },
226
- }),
227
- );
228
- }
229
-
230
- private startFfmpeg(videoTrack: any, werift: any): void {
231
- const os = platform();
232
- const fps = this.options.fps;
233
- const scale = this.options.scale;
234
-
235
- let inputArgs: string[];
236
- if (os === "darwin") {
237
- const screenIdx = detectMacScreenIndex();
238
- inputArgs = ["-f", "avfoundation", "-framerate", String(fps), "-i", `${screenIdx}:none`];
239
- } else if (os === "linux") {
240
- inputArgs = ["-f", "x11grab", "-framerate", String(fps), "-i", ":0"];
241
- } else {
242
- inputArgs = ["-f", "gdigrab", "-framerate", String(fps), "-i", "desktop"];
243
- }
244
-
245
- // libx264 requires even width & height; crop 1px if needed
246
- const evenFilter = "crop=trunc(iw/2)*2:trunc(ih/2)*2";
247
- const vf = scale < 1
248
- ? ["-vf", `scale=iw*${scale}:ih*${scale},${evenFilter}`]
249
- : ["-vf", evenFilter];
250
-
251
- this.ffmpeg = spawn("ffmpeg", [
252
- ...inputArgs,
253
- ...vf,
254
- "-c:v", "libx264",
255
- "-preset", "ultrafast",
256
- "-tune", "zerolatency",
257
- "-profile:v", "baseline",
258
- "-level", "3.1",
259
- "-pix_fmt", "yuv420p",
260
- "-g", String(fps * 2),
261
- "-b:v", "1500k",
262
- "-maxrate", "2000k",
263
- "-bufsize", "4000k",
264
- "-f", "h264",
265
- "-an",
266
- "pipe:1",
267
- ], {
268
- stdio: ["pipe", "pipe", "pipe"],
269
- });
270
-
271
- // RTP state
272
- let seqNum = 0;
273
- let timestamp = -Math.floor(90000 / fps); // so first bump yields 0
274
- const clockRate = 90000;
275
- const frameDuration = Math.floor(clockRate / fps);
276
- const ssrc = (Math.random() * 0xffffffff) >>> 0;
277
-
278
- // Parse NAL units from annexb stream and packetize as RTP
279
- let nalBuffer = Buffer.alloc(0);
280
-
281
- const sendNalUnit = (nal: Buffer) => {
282
- if (nal.length === 0) return;
283
-
284
- // Strip start code
285
- let offset = 0;
286
- if (nal[0] === 0 && nal[1] === 0 && nal[2] === 0 && nal[3] === 1) {
287
- offset = 4;
288
- } else if (nal[0] === 0 && nal[1] === 0 && nal[2] === 1) {
289
- offset = 3;
290
- }
291
- const nalData = nal.subarray(offset);
292
- if (nalData.length === 0) return;
293
-
294
- const nalType = nalData[0]! & 0x1f;
295
- // Update timestamp on each new access unit (SPS/IDR/non-IDR slice)
296
- if (nalType === 7 || nalType === 5 || nalType === 1) {
297
- // Only bump timestamp for actual picture NALs (not SPS/PPS)
298
- if (nalType === 5 || nalType === 1) {
299
- timestamp += frameDuration;
300
- }
301
- }
302
-
303
- if (nalData.length <= RTP_MTU) {
304
- // Single NAL unit packet
305
- const header = new werift.RtpHeader({
306
- sequenceNumber: seqNum++ & 0xffff,
307
- timestamp: timestamp >>> 0,
308
- payloadType: 96,
309
- ssrc,
310
- marker: nalType === 1 || nalType === 5, // marker on last NAL of frame
311
- });
312
- try {
313
- videoTrack.writeRtp(new werift.RtpPacket(header, nalData));
314
- } catch {}
315
- } else {
316
- // FU-A fragmentation (RFC 6184)
317
- const fnri = nalData[0]! & 0xe0; // F + NRI bits
318
- const nalTypeVal = nalData[0]! & 0x1f;
319
- const fuIndicator = fnri | 28; // FU-A type = 28
320
- let pos = 1; // skip NAL header byte
321
- let isFirst = true;
322
-
323
- while (pos < nalData.length) {
324
- const end = Math.min(pos + RTP_MTU - 2, nalData.length); // -2 for FU indicator + FU header
325
- const isLast = end >= nalData.length;
326
-
327
- let fuHeader = nalTypeVal;
328
- if (isFirst) fuHeader |= 0x80; // Start bit
329
- if (isLast) fuHeader |= 0x40; // End bit
330
-
331
- const payload = Buffer.concat([
332
- Buffer.from([fuIndicator, fuHeader]),
333
- nalData.subarray(pos, end),
334
- ]);
335
-
336
- const header = new werift.RtpHeader({
337
- sequenceNumber: seqNum++ & 0xffff,
338
- timestamp: timestamp >>> 0,
339
- payloadType: 96,
340
- ssrc,
341
- marker: isLast && (nalType === 1 || nalType === 5),
342
- });
343
- try {
344
- videoTrack.writeRtp(new werift.RtpPacket(header, payload));
345
- } catch {}
346
-
347
- isFirst = false;
348
- pos = end;
349
- }
350
- }
351
- };
352
-
353
- const extractNalUnits = (flush = false) => {
354
- // Find NAL unit boundaries (0x00000001 or 0x000001)
355
- const units: Buffer[] = [];
356
- let start = -1;
357
-
358
- for (let i = 0; i < nalBuffer.length - 3; i++) {
359
- if (nalBuffer[i] === 0 && nalBuffer[i + 1] === 0) {
360
- let scLen = 0;
361
- if (nalBuffer[i + 2] === 0 && i + 3 < nalBuffer.length && nalBuffer[i + 3] === 1) {
362
- scLen = 4;
363
- } else if (nalBuffer[i + 2] === 1) {
364
- scLen = 3;
365
- }
366
- if (scLen > 0) {
367
- if (start >= 0) {
368
- units.push(nalBuffer.subarray(start, i));
369
- }
370
- start = i;
371
- i += scLen - 1; // skip past start code
372
- }
373
- }
374
- }
375
-
376
- // On flush (ffmpeg exit), emit the trailing NAL unit too
377
- if (flush && start >= 0) {
378
- units.push(nalBuffer.subarray(start));
379
- nalBuffer = Buffer.alloc(0);
380
- return units;
381
- }
382
-
383
- if (start >= 0) {
384
- // Keep remaining data from last start code onward
385
- nalBuffer = nalBuffer.subarray(start);
386
- }
387
-
388
- // Safety cap: if buffer grows beyond 4MB without producing NALs, discard
389
- if (nalBuffer.length > 4 * 1024 * 1024 && units.length === 0) {
390
- process.stderr.write(`[screen-share] NAL buffer exceeded 4MB, resetting\n`);
391
- nalBuffer = Buffer.alloc(0);
392
- }
393
-
394
- return units;
395
- };
396
-
397
- this.ffmpeg.stdout?.on("data", (chunk: Buffer) => {
398
- if (!this.active) return;
399
- nalBuffer = Buffer.concat([nalBuffer, chunk]);
400
-
401
- const units = extractNalUnits();
402
- for (const unit of units) {
403
- sendNalUnit(unit);
404
- }
405
- });
406
-
407
- this.ffmpeg.stderr?.on("data", (data: Buffer) => {
408
- const msg = data.toString();
409
- if (msg.includes("Error") || msg.includes("error")) {
410
- process.stderr.write(`[screen-share:ffmpeg] ${msg}\n`);
411
- }
412
- });
413
-
414
- this.ffmpeg.on("exit", (code) => {
415
- // Flush any remaining NAL unit in the buffer
416
- const remaining = extractNalUnits(true);
417
- for (const unit of remaining) {
418
- sendNalUnit(unit);
419
- }
420
- if (this.active) {
421
- process.stderr.write(`[screen-share] ffmpeg exited with code ${code}\n`);
422
- this.stop();
423
- }
424
- });
425
- }
426
- }
@@ -1,43 +0,0 @@
1
- import type { Envelope } from "@linkshell/protocol";
2
-
3
- export class ScrollbackBuffer {
4
- private buffer: Envelope[] = [];
5
- private readonly capacity: number;
6
-
7
- constructor(capacity = 1000) {
8
- this.capacity = capacity;
9
- }
10
-
11
- push(envelope: Envelope): void {
12
- if (this.buffer.length >= this.capacity) {
13
- this.buffer.shift();
14
- }
15
- this.buffer.push(envelope);
16
- }
17
-
18
- replayFrom(seq: number): Envelope[] {
19
- return this.buffer.filter(
20
- (e) => e.seq !== undefined && e.seq > seq,
21
- );
22
- }
23
-
24
- trimUpTo(seq: number): void {
25
- const idx = this.buffer.findIndex(
26
- (e) => e.seq !== undefined && e.seq > seq,
27
- );
28
- if (idx > 0) {
29
- this.buffer.splice(0, idx);
30
- } else if (idx === -1) {
31
- this.buffer = [];
32
- }
33
- }
34
-
35
- get size(): number {
36
- return this.buffer.length;
37
- }
38
-
39
- get lastSeq(): number {
40
- const last = this.buffer[this.buffer.length - 1];
41
- return last?.seq ?? -1;
42
- }
43
- }