@vibecook/ghosttea-electron 0.2.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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/automation.d.ts +2 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +2 -0
- package/dist/automation.js.map +1 -0
- package/dist/backend.d.ts +28 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +63 -0
- package/dist/backend.js.map +1 -0
- package/dist/bridge-entry.d.ts +2 -0
- package/dist/bridge-entry.d.ts.map +1 -0
- package/dist/bridge-entry.js +154 -0
- package/dist/bridge-entry.js.map +1 -0
- package/dist/bridge-socket.d.ts +4 -0
- package/dist/bridge-socket.d.ts.map +1 -0
- package/dist/bridge-socket.js +91 -0
- package/dist/bridge-socket.js.map +1 -0
- package/dist/bridge.d.ts +18 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +59 -0
- package/dist/bridge.js.map +1 -0
- package/dist/clipboard.d.ts +18 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +65 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/deadline.d.ts +2 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +16 -0
- package/dist/deadline.js.map +1 -0
- package/dist/edit-commands.d.ts +45 -0
- package/dist/edit-commands.d.ts.map +1 -0
- package/dist/edit-commands.js +42 -0
- package/dist/edit-commands.js.map +1 -0
- package/dist/frame-flow.d.ts +18 -0
- package/dist/frame-flow.d.ts.map +1 -0
- package/dist/frame-flow.js +52 -0
- package/dist/frame-flow.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +8 -0
- package/dist/main.js.map +1 -0
- package/dist/preload.d.ts +4 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +9 -0
- package/dist/preload.js.map +1 -0
- package/dist/supervisor.d.ts +30 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +160 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Yong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# `@vibecook/ghosttea-electron`
|
|
2
|
+
|
|
3
|
+
Electron lifecycle and direct renderer transport for Ghosttea.
|
|
4
|
+
|
|
5
|
+
Use managed mode when this package should start a configured Rust service. Use
|
|
6
|
+
external mode when the Electron application already owns a Rust composition
|
|
7
|
+
service and only wants Ghosttea to attach its authenticated control and frame
|
|
8
|
+
channels.
|
|
9
|
+
|
|
10
|
+
Frame payloads travel directly from the utility process to renderer
|
|
11
|
+
MessagePorts. They are not routed through Electron main.
|
|
12
|
+
|
|
13
|
+
The backend also owns a control-only `GhostteaAutomationClient` for
|
|
14
|
+
application-driven sessions and input. It never opens the frame socket,
|
|
15
|
+
attaches a renderer view, or claims focus/resize authority:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
await backend.start();
|
|
19
|
+
const session = await backend.automation.createSession({
|
|
20
|
+
executable: "claude",
|
|
21
|
+
args: [],
|
|
22
|
+
environment: { mode: "clean", variables: agentEnvironment },
|
|
23
|
+
cols: 120,
|
|
24
|
+
rows: 40,
|
|
25
|
+
persistence: "terminate-with-app",
|
|
26
|
+
});
|
|
27
|
+
const result = await backend.automation.pasteAndSubmit(session.id, "Review this repository");
|
|
28
|
+
if (!result.accepted) {
|
|
29
|
+
// Human input was accepted after the automation epoch was observed.
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Automation paste and submission are one ordered PTY operation. Human input
|
|
34
|
+
invalidates a stale automation epoch rather than being delayed.
|
|
35
|
+
|
|
36
|
+
Hosts that already own a `TerminalDaemonConnection` can import
|
|
37
|
+
`GhostteaAutomationClient` directly from
|
|
38
|
+
`@vibecook/ghosttea-electron/automation`. That subpath is Node-only and does
|
|
39
|
+
not load Electron main-process modules. New Electron-free Node hosts should
|
|
40
|
+
depend directly on `@vibecook/ghosttea-client`; the Electron export is retained
|
|
41
|
+
for compatibility.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../src/automation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,+BAA+B,EACpC,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"automation.js","sourceRoot":"","sources":["../src/automation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,GAMzB,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { WebContents } from "electron";
|
|
3
|
+
import { GhostteaAutomationClient, type GhostteaAutomationClientOptions } from "./automation.js";
|
|
4
|
+
import { type GhostteaElectronBridgeOptions } from "./bridge.js";
|
|
5
|
+
import { type TerminalSupervisorOptions } from "./supervisor.js";
|
|
6
|
+
import type { TerminalDaemonConnection } from "./types.js";
|
|
7
|
+
export type GhostteaElectronBackendOptions = {
|
|
8
|
+
mode: "managed";
|
|
9
|
+
daemon: TerminalSupervisorOptions;
|
|
10
|
+
bridge?: Omit<GhostteaElectronBridgeOptions, "connection">;
|
|
11
|
+
automation?: GhostteaAutomationClientOptions;
|
|
12
|
+
} | {
|
|
13
|
+
mode: "external";
|
|
14
|
+
connection: TerminalDaemonConnection;
|
|
15
|
+
bridge?: Omit<GhostteaElectronBridgeOptions, "connection">;
|
|
16
|
+
automation?: GhostteaAutomationClientOptions;
|
|
17
|
+
};
|
|
18
|
+
export declare class GhostteaElectronBackend extends EventEmitter {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(options: GhostteaElectronBackendOptions);
|
|
21
|
+
get connection(): TerminalDaemonConnection;
|
|
22
|
+
get running(): boolean;
|
|
23
|
+
get automation(): GhostteaAutomationClient;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
attachRenderer(webContents: WebContents): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,iBAAiB,CAAC;AACjG,OAAO,EAA0B,KAAK,6BAA6B,EAAE,MAAM,aAAa,CAAC;AACzF,OAAO,EAAsB,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,MAAM,8BAA8B,GACtC;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,yBAAyB,CAAC;IAClC,MAAM,CAAC,EAAE,IAAI,CAAC,6BAA6B,EAAE,YAAY,CAAC,CAAC;IAC3D,UAAU,CAAC,EAAE,+BAA+B,CAAC;CAC9C,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,CAAC,EAAE,IAAI,CAAC,6BAA6B,EAAE,YAAY,CAAC,CAAC;IAC3D,UAAU,CAAC,EAAE,+BAA+B,CAAC;CAC9C,CAAC;AAEN,qBAAa,uBAAwB,SAAQ,YAAY;;IAMvD,YAAY,OAAO,EAAE,8BAA8B,EAQlD;IASD,IAAI,UAAU,IAAI,wBAAwB,CAEzC;IAED,IAAI,OAAO,IAAI,OAAO,CAGrB;IAED,IAAI,UAAU,IAAI,wBAAwB,CAGzC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB3B;IAED,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAE7C;IAED,IAAI,IAAI,IAAI,CAGX;CACF"}
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { GhostteaAutomationClient } from "./automation.js";
|
|
3
|
+
import { GhostteaElectronBridge } from "./bridge.js";
|
|
4
|
+
import { TerminalSupervisor } from "./supervisor.js";
|
|
5
|
+
export class GhostteaElectronBackend extends EventEmitter {
|
|
6
|
+
#options;
|
|
7
|
+
#supervisor;
|
|
8
|
+
#bridge;
|
|
9
|
+
#automation;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
this.#options = options;
|
|
13
|
+
this.#supervisor = options.mode === "managed" ? new TerminalSupervisor(options.daemon) : undefined;
|
|
14
|
+
this.#supervisor?.on("unexpected-exit", (detail) => {
|
|
15
|
+
this.#resetClients();
|
|
16
|
+
this.emit("unexpected-exit", { source: "daemon", ...detail });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
#resetClients() {
|
|
20
|
+
this.#automation?.dispose();
|
|
21
|
+
this.#automation = undefined;
|
|
22
|
+
this.#bridge?.stop();
|
|
23
|
+
this.#bridge = undefined;
|
|
24
|
+
}
|
|
25
|
+
get connection() {
|
|
26
|
+
return this.#options.mode === "managed" ? this.#supervisor.connection : this.#options.connection;
|
|
27
|
+
}
|
|
28
|
+
get running() {
|
|
29
|
+
const daemonReady = this.#options.mode === "external" || this.#supervisor?.running === true;
|
|
30
|
+
return daemonReady && this.#bridge?.running === true;
|
|
31
|
+
}
|
|
32
|
+
get automation() {
|
|
33
|
+
this.#automation ??= new GhostteaAutomationClient(this.connection, this.#options.automation);
|
|
34
|
+
return this.#automation;
|
|
35
|
+
}
|
|
36
|
+
async start() {
|
|
37
|
+
await this.#supervisor?.start();
|
|
38
|
+
await this.automation.connect();
|
|
39
|
+
if (!this.#bridge) {
|
|
40
|
+
this.#bridge = new GhostteaElectronBridge({
|
|
41
|
+
...this.#options.bridge,
|
|
42
|
+
connection: this.connection,
|
|
43
|
+
});
|
|
44
|
+
this.#bridge.on("unexpected-exit", (detail) => {
|
|
45
|
+
this.#bridge = undefined;
|
|
46
|
+
this.emit("unexpected-exit", { source: "bridge", ...detail });
|
|
47
|
+
});
|
|
48
|
+
this.#bridge.on("connection-lost", (detail) => {
|
|
49
|
+
this.#resetClients();
|
|
50
|
+
this.emit("unexpected-exit", { source: "connection", code: null, signal: null, ...detail });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
this.#bridge.start();
|
|
54
|
+
}
|
|
55
|
+
attachRenderer(webContents) {
|
|
56
|
+
this.#bridge?.attachRenderer(webContents);
|
|
57
|
+
}
|
|
58
|
+
stop() {
|
|
59
|
+
this.#resetClients();
|
|
60
|
+
this.#supervisor?.stop();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-entry.d.ts","sourceRoot":"","sources":["../src/bridge-entry.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { connectSocket, packet } from "./bridge-socket.js";
|
|
2
|
+
import { FrameFlowControl, requestedFrameBridgeCapabilities } from "./frame-flow.js";
|
|
3
|
+
import { isMainToBridgeMessage } from "./types.js";
|
|
4
|
+
const parentPort = process.parentPort;
|
|
5
|
+
const MAX_CONTROL_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
const MAX_FRAME_SUBSCRIPTION_BYTES = 1024 * 1024;
|
|
8
|
+
const FRAME_MAGIC = 0x31465254;
|
|
9
|
+
async function attachRenderer(rawData, ports) {
|
|
10
|
+
const [controlPort, framePort] = ports;
|
|
11
|
+
if (!isMainToBridgeMessage(rawData) || !controlPort || !framePort)
|
|
12
|
+
throw new Error("invalid bridge bootstrap");
|
|
13
|
+
const data = rawData;
|
|
14
|
+
let controlSocket;
|
|
15
|
+
let frameSocket;
|
|
16
|
+
let rendererClosed = false;
|
|
17
|
+
let connectionLost = false;
|
|
18
|
+
let frameFlowEnabled = false;
|
|
19
|
+
const frameFlow = new FrameFlowControl();
|
|
20
|
+
const reportConnectionLost = (cause) => {
|
|
21
|
+
if (rendererClosed || connectionLost)
|
|
22
|
+
return;
|
|
23
|
+
connectionLost = true;
|
|
24
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
25
|
+
controlPort.postMessage({ requestId: 0, type: "bridge-error", message: error.message });
|
|
26
|
+
parentPort.postMessage({ type: "connection-lost", message: error.message });
|
|
27
|
+
controlSocket?.destroy();
|
|
28
|
+
frameSocket?.destroy();
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
controlSocket = await connectSocket(data.connection.controlSocket, data.connection.authToken, MAX_CONTROL_BYTES, (bytes) => {
|
|
32
|
+
try {
|
|
33
|
+
controlPort.postMessage(JSON.parse(bytes.toString("utf8")));
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
37
|
+
controlPort.postMessage({
|
|
38
|
+
requestId: 0,
|
|
39
|
+
type: "bridge-error",
|
|
40
|
+
message: `Invalid terminald response: ${error.message}`,
|
|
41
|
+
});
|
|
42
|
+
controlSocket?.destroy();
|
|
43
|
+
}
|
|
44
|
+
}, reportConnectionLost);
|
|
45
|
+
frameSocket = await connectSocket(data.connection.frameSocket, data.connection.authToken, MAX_FRAME_BYTES, (bytes) => {
|
|
46
|
+
try {
|
|
47
|
+
if (bytes.byteLength >= 4 && bytes.readUInt32LE(0) === FRAME_MAGIC) {
|
|
48
|
+
const transferable = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
49
|
+
const accounted = frameFlowEnabled;
|
|
50
|
+
const action = accounted ? frameFlow.accept(transferable.byteLength) : undefined;
|
|
51
|
+
try {
|
|
52
|
+
framePort.postMessage(transferable);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (accounted)
|
|
56
|
+
frameFlow.release(transferable.byteLength);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (action === "pause")
|
|
60
|
+
frameSocket?.pause();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const message = JSON.parse(bytes.toString("utf8"));
|
|
64
|
+
if (!message ||
|
|
65
|
+
typeof message !== "object" ||
|
|
66
|
+
!["subscription-ack", "frame-gap"].includes(String(message.type))) {
|
|
67
|
+
throw new Error("terminald sent an invalid frame-channel message");
|
|
68
|
+
}
|
|
69
|
+
framePort.postMessage(message);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
reportConnectionLost(cause);
|
|
73
|
+
}
|
|
74
|
+
}, reportConnectionLost);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
controlSocket?.destroy();
|
|
78
|
+
frameSocket?.destroy();
|
|
79
|
+
connectionLost = true;
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
parentPort.postMessage({ type: "connection-lost", message });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
controlPort.on("message", ({ data: command }) => {
|
|
85
|
+
try {
|
|
86
|
+
const encoded = Buffer.from(JSON.stringify(command));
|
|
87
|
+
if (encoded.byteLength > MAX_CONTROL_BYTES)
|
|
88
|
+
throw new Error("control packet exceeds quota");
|
|
89
|
+
controlSocket.write(packet(encoded));
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
93
|
+
controlPort.postMessage({ requestId: 0, type: "bridge-error", message: error.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
framePort.on("message", ({ data: subscription }) => {
|
|
97
|
+
try {
|
|
98
|
+
if (subscription &&
|
|
99
|
+
typeof subscription === "object" &&
|
|
100
|
+
subscription.type === "frame-credit") {
|
|
101
|
+
if (!frameFlowEnabled)
|
|
102
|
+
return;
|
|
103
|
+
const bytes = subscription.bytes;
|
|
104
|
+
if (!Number.isSafeInteger(bytes) || Number(bytes) < 0)
|
|
105
|
+
throw new Error("invalid renderer frame credit");
|
|
106
|
+
if (frameFlow.release(Number(bytes)) === "resume")
|
|
107
|
+
frameSocket.resume();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (subscription &&
|
|
111
|
+
typeof subscription === "object" &&
|
|
112
|
+
subscription.type === "subscribe") {
|
|
113
|
+
const capabilities = requestedFrameBridgeCapabilities(subscription);
|
|
114
|
+
if (capabilities) {
|
|
115
|
+
// Announce support before forwarding the subscription. MessagePort
|
|
116
|
+
// ordering then guarantees that the renderer knows credits are active
|
|
117
|
+
// before terminald can deliver the subscription's first frame.
|
|
118
|
+
framePort.postMessage(capabilities);
|
|
119
|
+
frameFlowEnabled = true;
|
|
120
|
+
}
|
|
121
|
+
else if (subscription.frameCredits === true) {
|
|
122
|
+
// Preserve compatibility with the first credit-enabled renderer,
|
|
123
|
+
// which predated the explicit bridge capability handshake.
|
|
124
|
+
frameFlowEnabled = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const encoded = Buffer.from(JSON.stringify(subscription));
|
|
128
|
+
if (encoded.byteLength > MAX_FRAME_SUBSCRIPTION_BYTES)
|
|
129
|
+
throw new Error("frame subscription exceeds quota");
|
|
130
|
+
frameSocket.write(packet(encoded));
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
134
|
+
controlPort.postMessage({ requestId: 0, type: "bridge-error", message: error.message });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const closeForRenderer = () => {
|
|
138
|
+
rendererClosed = true;
|
|
139
|
+
controlSocket.destroy();
|
|
140
|
+
frameSocket.destroy();
|
|
141
|
+
};
|
|
142
|
+
controlPort.once("close", closeForRenderer);
|
|
143
|
+
framePort.once("close", closeForRenderer);
|
|
144
|
+
controlPort.start();
|
|
145
|
+
framePort.start();
|
|
146
|
+
}
|
|
147
|
+
parentPort.on("message", (event) => {
|
|
148
|
+
void attachRenderer(event.data, event.ports).catch((cause) => {
|
|
149
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
150
|
+
console.error(`[terminal-bridge] ${error.stack ?? error.message}`);
|
|
151
|
+
event.ports[0]?.postMessage({ requestId: 0, type: "bridge-error", message: error.message });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=bridge-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-entry.js","sourceRoot":"","sources":["../src/bridge-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
export declare function packet(bytes: Uint8Array): Buffer;
|
|
3
|
+
export declare function connectSocket(path: string, token: string, limit: number, onPacket: (bytes: Buffer) => void, onDisconnect: (error: Error) => void, timeoutMs?: number): Promise<Socket>;
|
|
4
|
+
//# sourceMappingURL=bridge-socket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-socket.d.ts","sourceRoot":"","sources":["../src/bridge-socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAEzD,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKhD;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EACpC,SAAS,SAAS,GACjB,OAAO,CAAC,MAAM,CAAC,CA0EjB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
export function packet(bytes) {
|
|
3
|
+
const output = Buffer.allocUnsafe(4 + bytes.byteLength);
|
|
4
|
+
output.writeUInt32LE(bytes.byteLength, 0);
|
|
5
|
+
output.set(bytes, 4);
|
|
6
|
+
return output;
|
|
7
|
+
}
|
|
8
|
+
export function connectSocket(path, token, limit, onPacket, onDisconnect, timeoutMs = 10_000) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const socket = createConnection(path);
|
|
11
|
+
let buffered = Buffer.alloc(0);
|
|
12
|
+
let draining = false;
|
|
13
|
+
let authenticated = false;
|
|
14
|
+
let settled = false;
|
|
15
|
+
const timeout = setTimeout(() => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
settled = true;
|
|
19
|
+
socket.destroy();
|
|
20
|
+
reject(new Error(`terminald connection timed out during authentication at ${path}`));
|
|
21
|
+
}, timeoutMs);
|
|
22
|
+
socket.on("error", (error) => {
|
|
23
|
+
if (!settled) {
|
|
24
|
+
settled = true;
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
reject(error);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.error(`[terminal-bridge] socket error at ${path}: ${error.message}`);
|
|
30
|
+
onDisconnect(error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
socket.on("close", () => {
|
|
34
|
+
if (!settled) {
|
|
35
|
+
settled = true;
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
reject(new Error(`terminald connection closed during authentication at ${path}`));
|
|
38
|
+
}
|
|
39
|
+
else if (authenticated) {
|
|
40
|
+
onDisconnect(new Error(`terminald connection closed at ${path}`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
socket.on("connect", () => socket.write(packet(Buffer.from(token))));
|
|
44
|
+
const drainPackets = () => {
|
|
45
|
+
if (draining || socket.isPaused())
|
|
46
|
+
return;
|
|
47
|
+
draining = true;
|
|
48
|
+
try {
|
|
49
|
+
while (buffered.length >= 4) {
|
|
50
|
+
const length = buffered.readUInt32LE(0);
|
|
51
|
+
if (length > limit) {
|
|
52
|
+
socket.destroy(new Error("terminald packet exceeds bridge quota"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (buffered.length < 4 + length)
|
|
56
|
+
return;
|
|
57
|
+
const body = buffered.subarray(4, 4 + length);
|
|
58
|
+
buffered = buffered.subarray(4 + length);
|
|
59
|
+
if (!authenticated) {
|
|
60
|
+
if (body.toString() !== "ok") {
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
socket.destroy();
|
|
64
|
+
reject(new Error("terminald authentication failed"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
authenticated = true;
|
|
68
|
+
settled = true;
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
resolve(socket);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
onPacket(body);
|
|
74
|
+
}
|
|
75
|
+
if (socket.isPaused())
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
draining = false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
socket.on("resume", drainPackets);
|
|
84
|
+
socket.on("data", (chunk) => {
|
|
85
|
+
const incoming = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
86
|
+
buffered = Buffer.concat([buffered, incoming]);
|
|
87
|
+
drainPackets();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=bridge-socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-socket.js","sourceRoot":"","sources":["../src/bridge-socket.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { type WebContents } from "electron";
|
|
3
|
+
import type { TerminalDaemonConnection } from "./types.js";
|
|
4
|
+
export interface GhostteaElectronBridgeOptions {
|
|
5
|
+
connection: TerminalDaemonConnection;
|
|
6
|
+
entryPoint?: string;
|
|
7
|
+
serviceName?: string;
|
|
8
|
+
rendererChannel?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class GhostteaElectronBridge extends EventEmitter {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(options: GhostteaElectronBridgeOptions);
|
|
13
|
+
get running(): boolean;
|
|
14
|
+
start(): void;
|
|
15
|
+
attachRenderer(webContents: WebContents): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAA2D,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACrG,OAAO,KAAK,EAAuB,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,wBAAwB,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,sBAAuB,SAAQ,YAAY;;IAItD,YAAY,OAAO,EAAE,6BAA6B,EAGjD;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,KAAK,IAAI,IAAI,CA2BZ;IAED,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAU7C;IAED,IAAI,IAAI,IAAI,CAIX;CACF"}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { MessageChannelMain, utilityProcess } from "electron";
|
|
4
|
+
export class GhostteaElectronBridge extends EventEmitter {
|
|
5
|
+
#options;
|
|
6
|
+
#process;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
super();
|
|
9
|
+
this.#options = options;
|
|
10
|
+
}
|
|
11
|
+
get running() {
|
|
12
|
+
return this.#process !== undefined;
|
|
13
|
+
}
|
|
14
|
+
start() {
|
|
15
|
+
if (this.#process)
|
|
16
|
+
return;
|
|
17
|
+
const entryPoint = this.#options.entryPoint ?? fileURLToPath(new URL("./bridge-entry.js", import.meta.url));
|
|
18
|
+
const child = utilityProcess.fork(entryPoint, [], {
|
|
19
|
+
serviceName: this.#options.serviceName ?? "ghosttea-terminal-bridge",
|
|
20
|
+
});
|
|
21
|
+
this.#process = child;
|
|
22
|
+
child.once("exit", (code) => {
|
|
23
|
+
if (this.#process !== child)
|
|
24
|
+
return;
|
|
25
|
+
this.#process = undefined;
|
|
26
|
+
this.emit("unexpected-exit", { code });
|
|
27
|
+
});
|
|
28
|
+
child.on("message", (message) => {
|
|
29
|
+
if (this.#process === child &&
|
|
30
|
+
message !== null &&
|
|
31
|
+
typeof message === "object" &&
|
|
32
|
+
message.type === "connection-lost") {
|
|
33
|
+
this.emit("connection-lost", {
|
|
34
|
+
message: typeof message.message === "string"
|
|
35
|
+
? message.message
|
|
36
|
+
: "terminald connection lost",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
attachRenderer(webContents) {
|
|
42
|
+
const child = this.#process;
|
|
43
|
+
if (!child)
|
|
44
|
+
throw new Error("Ghosttea Electron bridge is not running");
|
|
45
|
+
const control = new MessageChannelMain();
|
|
46
|
+
const frames = new MessageChannelMain();
|
|
47
|
+
child.postMessage({ type: "connect", connection: this.#options.connection }, [
|
|
48
|
+
control.port1,
|
|
49
|
+
frames.port1,
|
|
50
|
+
]);
|
|
51
|
+
webContents.postMessage(this.#options.rendererChannel ?? "terminal-ports", null, [control.port2, frames.port2]);
|
|
52
|
+
}
|
|
53
|
+
stop() {
|
|
54
|
+
const child = this.#process;
|
|
55
|
+
this.#process = undefined;
|
|
56
|
+
child?.kill();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAyC,MAAM,UAAU,CAAC;AAUrG,MAAM,OAAO,sBAAuB,SAAQ,YAAY;IAC7C,QAAQ,CAAgC;IACjD,QAAQ,CAA6B;IAErC,YAAY,OAAsC;QAChD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC;IACrC,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,IAAI,aAAa,CAAC,IAAI,GAAG,CAAC,mBAAmB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5G,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,EAAE;YAChD,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,0BAA0B;SACrE,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1B,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK;gBAAE,OAAO;YACpC,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;YACvC,IACE,IAAI,CAAC,QAAQ,KAAK,KAAK;gBACvB,OAAO,KAAK,IAAI;gBAChB,OAAO,OAAO,KAAK,QAAQ;gBAC1B,OAA8B,CAAC,IAAI,KAAK,iBAAiB,EAC1D,CAAC;gBACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE;oBAC3B,OAAO,EACL,OAAQ,OAAiC,CAAC,OAAO,KAAK,QAAQ;wBAC5D,CAAC,CAAE,OAA+B,CAAC,OAAO;wBAC1C,CAAC,CAAC,2BAA2B;iBAClC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,cAAc,CAAC,WAAwB;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC5B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACvE,MAAM,OAAO,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACxC,KAAK,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAgC,EAAE;YACzG,OAAO,CAAC,KAAK;YACb,MAAM,CAAC,KAAK;SACb,CAAC,CAAC;QACH,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,IAAI,gBAAgB,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAClH,CAAC;IAED,IAAI;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC1B,KAAK,EAAE,IAAI,EAAE,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Clipboard, IpcMain, IpcRenderer, WebContents } from "electron";
|
|
2
|
+
export interface GhostteaClipboardHost {
|
|
3
|
+
canCopy(webContents: WebContents): boolean;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Own clipboard access in Electron's main process. Sandboxed preload scripts
|
|
8
|
+
* cannot use Electron's clipboard module directly.
|
|
9
|
+
*/
|
|
10
|
+
export declare function installGhostteaClipboardHost(ipcMain: IpcMain, clipboard: Clipboard): GhostteaClipboardHost;
|
|
11
|
+
export interface GhostteaClipboardBridge {
|
|
12
|
+
writeText(text: string): void;
|
|
13
|
+
readText(): Promise<string>;
|
|
14
|
+
setCanCopy(canCopy: boolean): void;
|
|
15
|
+
}
|
|
16
|
+
/** Build the narrow, context-isolated clipboard API exposed by a preload. */
|
|
17
|
+
export declare function createGhostteaClipboardBridge(ipcRenderer: IpcRenderer): GhostteaClipboardBridge;
|
|
18
|
+
//# sourceMappingURL=clipboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAgB,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM3F,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC;IAC3C,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,qBAAqB,CAuC1G;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACpC;AAED,6EAA6E;AAC7E,wBAAgB,6BAA6B,CAAC,WAAW,EAAE,WAAW,GAAG,uBAAuB,CAe/F"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const READ_CLIPBOARD_CHANNEL = "ghosttea:clipboard:read";
|
|
2
|
+
const WRITE_CLIPBOARD_CHANNEL = "ghosttea:clipboard:write";
|
|
3
|
+
const COPY_AVAILABILITY_CHANNEL = "ghosttea:clipboard:can-copy";
|
|
4
|
+
/**
|
|
5
|
+
* Own clipboard access in Electron's main process. Sandboxed preload scripts
|
|
6
|
+
* cannot use Electron's clipboard module directly.
|
|
7
|
+
*/
|
|
8
|
+
export function installGhostteaClipboardHost(ipcMain, clipboard) {
|
|
9
|
+
const copyAvailability = new Map();
|
|
10
|
+
const trackedWebContents = new Map();
|
|
11
|
+
const readClipboard = () => clipboard.readText();
|
|
12
|
+
const writeClipboard = (event, text) => {
|
|
13
|
+
if (typeof text !== "string")
|
|
14
|
+
return;
|
|
15
|
+
clipboard.writeText(text);
|
|
16
|
+
};
|
|
17
|
+
const setCopyAvailability = (event, value) => {
|
|
18
|
+
const { sender } = event;
|
|
19
|
+
copyAvailability.set(sender.id, value === true);
|
|
20
|
+
if (!trackedWebContents.has(sender.id)) {
|
|
21
|
+
const onDestroyed = () => {
|
|
22
|
+
copyAvailability.delete(sender.id);
|
|
23
|
+
trackedWebContents.delete(sender.id);
|
|
24
|
+
};
|
|
25
|
+
trackedWebContents.set(sender.id, { sender, onDestroyed });
|
|
26
|
+
sender.once("destroyed", onDestroyed);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
ipcMain.handle(READ_CLIPBOARD_CHANNEL, readClipboard);
|
|
30
|
+
ipcMain.on(WRITE_CLIPBOARD_CHANNEL, writeClipboard);
|
|
31
|
+
ipcMain.on(COPY_AVAILABILITY_CHANNEL, setCopyAvailability);
|
|
32
|
+
return {
|
|
33
|
+
canCopy: (webContents) => copyAvailability.get(webContents.id) === true,
|
|
34
|
+
dispose: () => {
|
|
35
|
+
ipcMain.removeHandler(READ_CLIPBOARD_CHANNEL);
|
|
36
|
+
ipcMain.removeListener(WRITE_CLIPBOARD_CHANNEL, writeClipboard);
|
|
37
|
+
ipcMain.removeListener(COPY_AVAILABILITY_CHANNEL, setCopyAvailability);
|
|
38
|
+
copyAvailability.clear();
|
|
39
|
+
for (const { sender, onDestroyed } of trackedWebContents.values()) {
|
|
40
|
+
sender.removeListener("destroyed", onDestroyed);
|
|
41
|
+
}
|
|
42
|
+
trackedWebContents.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Build the narrow, context-isolated clipboard API exposed by a preload. */
|
|
47
|
+
export function createGhostteaClipboardBridge(ipcRenderer) {
|
|
48
|
+
return {
|
|
49
|
+
writeText: (text) => {
|
|
50
|
+
if (typeof text !== "string")
|
|
51
|
+
throw new TypeError("Clipboard text must be a string");
|
|
52
|
+
ipcRenderer.send(WRITE_CLIPBOARD_CHANNEL, text);
|
|
53
|
+
},
|
|
54
|
+
readText: async () => {
|
|
55
|
+
const text = await ipcRenderer.invoke(READ_CLIPBOARD_CHANNEL);
|
|
56
|
+
if (typeof text !== "string")
|
|
57
|
+
throw new TypeError("Main process returned invalid clipboard text");
|
|
58
|
+
return text;
|
|
59
|
+
},
|
|
60
|
+
setCanCopy: (canCopy) => {
|
|
61
|
+
ipcRenderer.send(COPY_AVAILABILITY_CHANNEL, canCopy === true);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=clipboard.js.map
|