@minimalcorp/tsunagi 0.4.2 → 0.4.3
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/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_03lm.~7._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_09br4yx._.js +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{11u7x4ic4~mp3.js → 0t3d~cifmhdv9.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{0ae7zmpe8063z.js → 1662w8cu9n67h.js} +1 -1
- package/dist/server/pty-manager.d.ts +28 -2
- package/dist/server/pty-manager.js +71 -4
- package/dist/server/routes/terminal.js +24 -48
- package/npm-shrinkwrap.json +202 -214
- package/package.json +1 -1
- /package/.next/standalone/apps/web/.next/static/{vdAs-hr87-L7y2bUSdX3i → h5xwYydO3EbBSaceTFcLG}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{vdAs-hr87-L7y2bUSdX3i → h5xwYydO3EbBSaceTFcLG}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{vdAs-hr87-L7y2bUSdX3i → h5xwYydO3EbBSaceTFcLG}/_ssgManifest.js +0 -0
|
@@ -25,9 +25,17 @@ export interface PtySession {
|
|
|
25
25
|
respawning: boolean;
|
|
26
26
|
/** 最後にユーザー入力があった時刻(入力中の作り直しを避けるため) */
|
|
27
27
|
lastInputAt: number;
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* PTY出力のリングバッファ(再接続時にまとめて送信)。
|
|
30
|
+
* 文字列は PTY 出力、TerminalSize は「ここで PTY がこのサイズに変わった」印。
|
|
31
|
+
* replay 時にサイズの区切りごとに xterm を resize してから書くことで、録画時と同じ幅で再生する
|
|
32
|
+
* (幅の違う出力を一つの幅で再生すると、列指定が切り詰められて崩れる)。
|
|
33
|
+
*/
|
|
34
|
+
scrollback: ScrollbackEntry[];
|
|
35
|
+
/** scrollback 中の出力のバイト数(サイズ印は含まない) */
|
|
30
36
|
scrollbackSize: number;
|
|
37
|
+
/** scrollback の先頭時点の PTY サイズ(最初のサイズ印より前の出力がこの幅で描かれた) */
|
|
38
|
+
scrollbackStartSize: TerminalSize;
|
|
31
39
|
/** 最後にPTY出力があった時刻(GC判定用) */
|
|
32
40
|
lastOutputAt: number;
|
|
33
41
|
/** 最後にWebSocket接続があった時刻 */
|
|
@@ -39,6 +47,12 @@ export interface TerminalSize {
|
|
|
39
47
|
cols: number;
|
|
40
48
|
rows: number;
|
|
41
49
|
}
|
|
50
|
+
/** scrollback の要素。文字列は PTY 出力、TerminalSize は PTY の resize 位置を示す印 */
|
|
51
|
+
export type ScrollbackEntry = string | TerminalSize;
|
|
52
|
+
/** replay 用に scrollback をサイズごとに区切ったもの。data はそのサイズで描かれた出力 */
|
|
53
|
+
export interface ReplaySegment extends TerminalSize {
|
|
54
|
+
data: string;
|
|
55
|
+
}
|
|
42
56
|
declare class PtyManager {
|
|
43
57
|
private sessions;
|
|
44
58
|
/** sessionId → 最後に input を送信した socket.id */
|
|
@@ -60,6 +74,18 @@ declare class PtyManager {
|
|
|
60
74
|
*/
|
|
61
75
|
createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[], options?: PtySessionOptions): PtySession;
|
|
62
76
|
getSession(sessionId: string): PtySession | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* PTY を resize する。サイズが変わったときだけ SIGWINCH が飛び、scrollback にサイズ印を残す。
|
|
79
|
+
* PTY の resize は必ずここを通し、replay が「どの出力がどの幅で描かれたか」を再現できるようにする。
|
|
80
|
+
* @returns 実際にサイズが変わったか
|
|
81
|
+
*/
|
|
82
|
+
resizePty(session: PtySession, size: TerminalSize): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* 再接続時の画面復元用に、scrollback を PTY サイズごとの区間に分ける。
|
|
85
|
+
* 末尾の区間は必ず現在の PTY サイズになる(出力が無くても返す)。
|
|
86
|
+
* @param transform 各区間の出力に適用する加工(端末クエリの除去など。区間ごとに join 済みの文字列を渡す)
|
|
87
|
+
*/
|
|
88
|
+
getReplaySegments(session: PtySession, transform: (data: string) => string): ReplaySegment[];
|
|
63
89
|
deleteSession(sessionId: string): void;
|
|
64
90
|
/**
|
|
65
91
|
* WebSocket接続が切れた際に呼ぶ。
|
|
@@ -45,6 +45,7 @@ function addTsunagiPluginDir(env) {
|
|
|
45
45
|
dirs.push(pluginDir);
|
|
46
46
|
env.CLAUDE_CODE_PLUGIN_DIRS = dirs.join(':');
|
|
47
47
|
}
|
|
48
|
+
const INITIAL_PTY_SIZE = { cols: 80, rows: 24 };
|
|
48
49
|
class PtyManager {
|
|
49
50
|
sessions = new Map();
|
|
50
51
|
/** sessionId → 最後に input を送信した socket.id */
|
|
@@ -118,8 +119,8 @@ class PtyManager {
|
|
|
118
119
|
addTsunagiPluginDir(spawnEnv);
|
|
119
120
|
const ptyProcess = pty.spawn(shell, [], {
|
|
120
121
|
name: 'xterm-256color',
|
|
121
|
-
cols:
|
|
122
|
-
rows:
|
|
122
|
+
cols: INITIAL_PTY_SIZE.cols,
|
|
123
|
+
rows: INITIAL_PTY_SIZE.rows,
|
|
123
124
|
cwd,
|
|
124
125
|
env: spawnEnv,
|
|
125
126
|
});
|
|
@@ -136,6 +137,7 @@ class PtyManager {
|
|
|
136
137
|
lastInputAt: 0,
|
|
137
138
|
scrollback: [],
|
|
138
139
|
scrollbackSize: 0,
|
|
140
|
+
scrollbackStartSize: INITIAL_PTY_SIZE,
|
|
139
141
|
lastOutputAt: now,
|
|
140
142
|
lastConnectedAt: now,
|
|
141
143
|
gcTimer: null,
|
|
@@ -147,8 +149,14 @@ class PtyManager {
|
|
|
147
149
|
session.scrollbackSize += data.length;
|
|
148
150
|
// バッファ上限を超えたら古いものから削除
|
|
149
151
|
while (session.scrollbackSize > SCROLLBACK_MAX_BYTES && session.scrollback.length > 0) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
const head = session.scrollback.shift();
|
|
153
|
+
if (typeof head === 'string') {
|
|
154
|
+
session.scrollbackSize -= head.length;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// サイズ印を捨てる → 残りの先頭はこのサイズで描かれている
|
|
158
|
+
session.scrollbackStartSize = head;
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
});
|
|
154
162
|
this.sessions.set(sessionId, session);
|
|
@@ -157,6 +165,65 @@ class PtyManager {
|
|
|
157
165
|
getSession(sessionId) {
|
|
158
166
|
return this.sessions.get(sessionId);
|
|
159
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* PTY を resize する。サイズが変わったときだけ SIGWINCH が飛び、scrollback にサイズ印を残す。
|
|
170
|
+
* PTY の resize は必ずここを通し、replay が「どの出力がどの幅で描かれたか」を再現できるようにする。
|
|
171
|
+
* @returns 実際にサイズが変わったか
|
|
172
|
+
*/
|
|
173
|
+
resizePty(session, size) {
|
|
174
|
+
const { pty: ptyProcess } = session;
|
|
175
|
+
if (ptyProcess.cols === size.cols && ptyProcess.rows === size.rows)
|
|
176
|
+
return false;
|
|
177
|
+
try {
|
|
178
|
+
ptyProcess.resize(size.cols, size.rows);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// 終了済みの PTY は無視する
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
// 直前の印以降に出力が無ければ、その印は不要なので置き換える
|
|
185
|
+
// (ウィンドウのドラッグ中など、出力を挟まず resize が連続しても印が溜まらないようにする)
|
|
186
|
+
const last = session.scrollback.at(-1);
|
|
187
|
+
if (last !== undefined && typeof last !== 'string') {
|
|
188
|
+
session.scrollback[session.scrollback.length - 1] = { cols: size.cols, rows: size.rows };
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
session.scrollback.push({ cols: size.cols, rows: size.rows });
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 再接続時の画面復元用に、scrollback を PTY サイズごとの区間に分ける。
|
|
197
|
+
* 末尾の区間は必ず現在の PTY サイズになる(出力が無くても返す)。
|
|
198
|
+
* @param transform 各区間の出力に適用する加工(端末クエリの除去など。区間ごとに join 済みの文字列を渡す)
|
|
199
|
+
*/
|
|
200
|
+
getReplaySegments(session, transform) {
|
|
201
|
+
const segments = [];
|
|
202
|
+
let size = session.scrollbackStartSize;
|
|
203
|
+
let chunks = [];
|
|
204
|
+
const flush = () => {
|
|
205
|
+
if (chunks.length > 0) {
|
|
206
|
+
segments.push({ ...size, data: transform(chunks.join('')) });
|
|
207
|
+
chunks = [];
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
for (const entry of session.scrollback) {
|
|
211
|
+
if (typeof entry === 'string') {
|
|
212
|
+
chunks.push(entry);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
flush();
|
|
216
|
+
size = entry;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
flush();
|
|
220
|
+
const current = { cols: session.pty.cols, rows: session.pty.rows };
|
|
221
|
+
const last = segments.at(-1);
|
|
222
|
+
if (!last || last.cols !== current.cols || last.rows !== current.rows) {
|
|
223
|
+
segments.push({ ...current, data: '' });
|
|
224
|
+
}
|
|
225
|
+
return segments;
|
|
226
|
+
}
|
|
160
227
|
deleteSession(sessionId) {
|
|
161
228
|
const session = this.sessions.get(sessionId);
|
|
162
229
|
if (!session)
|
|
@@ -27,27 +27,6 @@ function stripTerminalQueries(s) {
|
|
|
27
27
|
.replace(/\x1b\[[?>]?[0-9;]*n/g, '') // DSR: ESC[?6n(主犯) / ESC[6n / ESC[5n など
|
|
28
28
|
.replace(/\x1b\[[?>=]?[0-9;]*c/g, ''); // DA: ESC[c / ESC[0c / ESC[>c / ESC[=c
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* 表示中のクライアントがいなくなった PTY の cols をこの幅に縮める。
|
|
32
|
-
* 一定の幅を下回ると Claude(Ink) が full-frame redraw を行い <Static> を再 emit するという
|
|
33
|
-
* 観測に基づくしきい値。元 cols が既にこの値の場合のみ -1 にして必ず cols を変化させる。
|
|
34
|
-
*
|
|
35
|
-
* 再表示時は replay 後にクライアントが実サイズへ resize するため、必ず cols が変わり
|
|
36
|
-
* SIGWINCH が飛んで Claude が現在の画面を描き直す(同一サイズの resize は no-op で
|
|
37
|
-
* SIGWINCH が飛ばない)。縮める処理を切断時に済ませておくことで、縮めた時の再描画が
|
|
38
|
-
* 再表示より前に scrollback に入り、replay と描き直しの順序がタイミングに依存しない。
|
|
39
|
-
*/
|
|
40
|
-
const REDRAW_BUMP_COLS = 64;
|
|
41
|
-
function bumpColsForRedraw(session) {
|
|
42
|
-
const { pty: ptyProcess } = session;
|
|
43
|
-
const cols = ptyProcess.cols === REDRAW_BUMP_COLS ? REDRAW_BUMP_COLS - 1 : REDRAW_BUMP_COLS;
|
|
44
|
-
try {
|
|
45
|
-
ptyProcess.resize(cols, ptyProcess.rows);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// 終了済みの PTY は無視する
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
30
|
export async function terminalRoutes(fastify) {
|
|
52
31
|
const io = fastify.io;
|
|
53
32
|
// 作成中のセッション(sessionId → 作成処理)。存在確認から PTY 作成までの間に await を挟むため、
|
|
@@ -112,20 +91,22 @@ export async function terminalRoutes(fastify) {
|
|
|
112
91
|
}
|
|
113
92
|
ptyManager.setActiveSocket(sessionId, socket.id);
|
|
114
93
|
const { pty: ptyProcess } = session;
|
|
115
|
-
const isReused = session.
|
|
94
|
+
const isReused = session.scrollbackSize > 0;
|
|
116
95
|
// リングバッファの内容を一括送信(画面復元)。初回接続・再接続を問わず join のたびに送る。
|
|
117
|
-
//
|
|
118
|
-
//
|
|
96
|
+
// 出力は PTY サイズごとの区間に分けて送り、クライアントは画面をクリアした後、区間ごとに
|
|
97
|
+
// xterm をそのサイズにしてから書き込む(録画時と同じ幅で再生する)。書き終えたら実サイズへ
|
|
98
|
+
// resize し、サイズが変わっていれば SIGWINCH で Claude が現在の画面を描き直す。
|
|
99
|
+
// 切断中に PTY のサイズは変えない(以前は再描画を誘発するため cols を縮めていたが、
|
|
100
|
+
// Claude は resize のたびに履歴全体を再出力するため、狭い幅の履歴コピーが scrollback に
|
|
101
|
+
// 積まれて再表示時に横幅の狭い行や同じ内容の繰り返しが残る原因になっていた)。
|
|
119
102
|
// 端末クエリ列(ESC[?6n / ESC[c 等)を除去してから replay する。
|
|
120
103
|
// これを怠ると xterm.js が応答を生成し PTY へ注入され、入力欄に "1;2c64;3R..." が
|
|
121
104
|
// 勝手に入力される(stripTerminalQueries のコメント参照)。
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
rows: ptyProcess.rows,
|
|
128
|
-
});
|
|
105
|
+
const segments = ptyManager.getReplaySegments(session, stripTerminalQueries);
|
|
106
|
+
// 末尾の \r\n / \n / \r をトリムする。
|
|
107
|
+
const lastSegment = segments[segments.length - 1];
|
|
108
|
+
lastSegment.data = lastSegment.data.replace(/[\r\n]+$/, '');
|
|
109
|
+
socket.emit('replay', { segments });
|
|
129
110
|
// reused の場合、最初の resize まで onData 出力をバッファリング
|
|
130
111
|
let initialResizeHandled = !isReused;
|
|
131
112
|
const pendingOutput = [];
|
|
@@ -167,7 +148,7 @@ export async function terminalRoutes(fastify) {
|
|
|
167
148
|
return;
|
|
168
149
|
ptyManager.setTerminalSize(sessionId, socket.id, { cols, rows });
|
|
169
150
|
if (ptyManager.getActiveSocket(sessionId) === socket.id) {
|
|
170
|
-
|
|
151
|
+
ptyManager.resizePty(session, { cols, rows });
|
|
171
152
|
}
|
|
172
153
|
// reused時: 初回 resize 後にバッファリングしていた出力をフラッシュ
|
|
173
154
|
if (!initialResizeHandled) {
|
|
@@ -186,9 +167,7 @@ export async function terminalRoutes(fastify) {
|
|
|
186
167
|
return;
|
|
187
168
|
ptyManager.setTerminalSize(sessionId, socket.id, { cols, rows });
|
|
188
169
|
ptyManager.setActiveSocket(sessionId, socket.id);
|
|
189
|
-
|
|
190
|
-
ptyProcess.resize(cols, rows);
|
|
191
|
-
}
|
|
170
|
+
ptyManager.resizePty(session, { cols, rows });
|
|
192
171
|
});
|
|
193
172
|
// input イベント: 所有者 socket からの入力のみ PTY へ書き込む。
|
|
194
173
|
// ゾンビ/非所有 socket(再接続前の旧 socket・誤バインド socket 等)からの重複 input は
|
|
@@ -223,23 +202,16 @@ export async function terminalRoutes(fastify) {
|
|
|
223
202
|
const next = ptyManager.removeTerminalSocket(boundSessionId, socket.id);
|
|
224
203
|
if (wasOwner && next) {
|
|
225
204
|
// 同じ PTY を表示している socket が残っている → 最後に使われた socket に所有権と
|
|
226
|
-
// サイズを引き継ぐ(表示中なので GC
|
|
205
|
+
// サイズを引き継ぐ(表示中なので GC は行わない)
|
|
227
206
|
ptyManager.setActiveSocket(boundSessionId, next.socketId);
|
|
228
207
|
const session = ptyManager.getSession(boundSessionId);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
// 終了済みの PTY は無視する
|
|
234
|
-
}
|
|
208
|
+
if (session)
|
|
209
|
+
ptyManager.resizePty(session, next.size);
|
|
235
210
|
}
|
|
236
211
|
else if (wasOwner) {
|
|
212
|
+
// 表示中のクライアントがいなくなっても PTY のサイズはそのまま保つ。
|
|
213
|
+
// 再表示時にサイズが同じなら replay だけで復元され、違えば resize で Claude が描き直す
|
|
237
214
|
ptyManager.scheduleGc(boundSessionId);
|
|
238
|
-
// 次に表示されたとき Claude に描き直させるため、表示中のクライアントがいない間に
|
|
239
|
-
// cols を縮めておく(REDRAW_BUMP_COLS 参照)
|
|
240
|
-
const session = ptyManager.getSession(boundSessionId);
|
|
241
|
-
if (session)
|
|
242
|
-
bumpColsForRedraw(session);
|
|
243
215
|
}
|
|
244
216
|
}
|
|
245
217
|
});
|
|
@@ -350,11 +322,15 @@ export async function terminalRoutes(fastify) {
|
|
|
350
322
|
if (!session) {
|
|
351
323
|
return reply.status(404).send({ error: 'Session not found' });
|
|
352
324
|
}
|
|
353
|
-
|
|
325
|
+
// サイズ印は <RESIZE cols x rows> として埋め込む
|
|
326
|
+
const raw = session.scrollback
|
|
327
|
+
.map((e) => (typeof e === 'string' ? e : `<RESIZE ${e.cols}x${e.rows}>`))
|
|
328
|
+
.join('');
|
|
354
329
|
// 制御文字を可視化して返す
|
|
355
330
|
const visible = raw.replace(/\x1b/g, '<ESC>').replace(/\r/g, '<CR>').replace(/\n/g, '<LF>\n');
|
|
356
331
|
return reply.send({
|
|
357
332
|
sessionId,
|
|
333
|
+
startSize: session.scrollbackStartSize,
|
|
358
334
|
chunks: session.scrollback.length,
|
|
359
335
|
sizeBytes: session.scrollbackSize,
|
|
360
336
|
raw: Buffer.from(raw).toString('base64'),
|