rterm-backend 3.0.4 → 3.0.5
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/bin/gybackend.cjs +536 -28
- package/package.json +2 -2
package/bin/gybackend.cjs
CHANGED
|
@@ -284093,6 +284093,37 @@ var WinRMTransport = class {
|
|
|
284093
284093
|
await this.deleteShell(shellId);
|
|
284094
284094
|
}
|
|
284095
284095
|
}
|
|
284096
|
+
/**
|
|
284097
|
+
* Run a command on an EXISTING (persistent) shell, streaming output chunks to
|
|
284098
|
+
* `onChunk` as they arrive instead of buffering everything. The shell is left
|
|
284099
|
+
* open for the next command (persistent runspace). The caller owns the shell's
|
|
284100
|
+
* lifecycle (createShell/deleteShell) and must serialize commands per shell.
|
|
284101
|
+
*/
|
|
284102
|
+
async runCommandOnShell(shellId, command, opts) {
|
|
284103
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
|
|
284104
|
+
const commandId = await this.sendCommand(shellId, command);
|
|
284105
|
+
let stdout = "";
|
|
284106
|
+
let stderr = "";
|
|
284107
|
+
let exitCode = 0;
|
|
284108
|
+
for (; ; ) {
|
|
284109
|
+
if (opts?.signal?.aborted) throw new Error("AbortError");
|
|
284110
|
+
if (Date.now() > deadline) {
|
|
284111
|
+
throw new Error(`WinRM command timed out after ${opts?.timeoutMs ?? 12e4}ms`);
|
|
284112
|
+
}
|
|
284113
|
+
const r = await this.receive(shellId, commandId);
|
|
284114
|
+
stdout += r.stdout;
|
|
284115
|
+
stderr += r.stderr;
|
|
284116
|
+
if (opts?.onChunk) {
|
|
284117
|
+
if (r.stdout) opts.onChunk("stdout", r.stdout);
|
|
284118
|
+
if (r.stderr) opts.onChunk("stderr", r.stderr);
|
|
284119
|
+
}
|
|
284120
|
+
if (r.done) {
|
|
284121
|
+
exitCode = r.exitCode ?? 0;
|
|
284122
|
+
break;
|
|
284123
|
+
}
|
|
284124
|
+
}
|
|
284125
|
+
return { stdout, stderr, exitCode };
|
|
284126
|
+
}
|
|
284096
284127
|
/** Lightweight connectivity probe: create then immediately delete a shell. */
|
|
284097
284128
|
async ping() {
|
|
284098
284129
|
const shellId = await this.createShell();
|
|
@@ -284123,7 +284154,7 @@ var WinRMBackend = class {
|
|
|
284123
284154
|
const cfg = config2;
|
|
284124
284155
|
const ptyId = `winrm-${(0, import_node_crypto2.randomUUID)()}`;
|
|
284125
284156
|
const transport = this.buildTransport(cfg);
|
|
284126
|
-
const instance = { config: cfg, transport, ready: false, failed: false };
|
|
284157
|
+
const instance = { config: cfg, transport, ready: false, failed: false, commandQueue: Promise.resolve() };
|
|
284127
284158
|
this.instances.set(ptyId, instance);
|
|
284128
284159
|
void this.probe(instance).then((ok) => {
|
|
284129
284160
|
if (ok) {
|
|
@@ -284166,7 +284197,9 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
|
|
|
284166
284197
|
timeoutMs: 3e4
|
|
284167
284198
|
});
|
|
284168
284199
|
}
|
|
284169
|
-
/** Direct command execution — the path TerminalService uses for winrm tabs.
|
|
284200
|
+
/** Direct command execution — the path TerminalService uses for winrm tabs.
|
|
284201
|
+
* Uses a persistent runspace (reused across commands) + streams output live,
|
|
284202
|
+
* and tracks the working directory so `cd` persists between commands. */
|
|
284170
284203
|
async executeCommand(ptyId, command, options) {
|
|
284171
284204
|
const instance = this.instances.get(ptyId);
|
|
284172
284205
|
if (!instance) {
|
|
@@ -284179,16 +284212,60 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
|
|
|
284179
284212
|
if (!waited) {
|
|
284180
284213
|
throw new Error("WinRM session is still initializing; try again shortly.");
|
|
284181
284214
|
}
|
|
284215
|
+
const run = instance.commandQueue.then(
|
|
284216
|
+
() => this.executeOnPersistentShell(instance, command, options)
|
|
284217
|
+
);
|
|
284218
|
+
instance.commandQueue = run.catch(() => {
|
|
284219
|
+
});
|
|
284220
|
+
return run;
|
|
284221
|
+
}
|
|
284222
|
+
/** Lazily create (or recreate) the persistent runspace. */
|
|
284223
|
+
async ensurePersistentShell(instance) {
|
|
284224
|
+
if (instance.persistentShellId) return instance.persistentShellId;
|
|
284225
|
+
const shellId = await instance.transport.createShell();
|
|
284226
|
+
instance.persistentShellId = shellId;
|
|
284227
|
+
try {
|
|
284228
|
+
const r = await instance.transport.runCommandOnShell(shellId, "cd", { timeoutMs: 1e4 });
|
|
284229
|
+
const cwd = r.stdout.trim();
|
|
284230
|
+
if (cwd) instance.cwd = cwd;
|
|
284231
|
+
} catch {
|
|
284232
|
+
}
|
|
284233
|
+
return shellId;
|
|
284234
|
+
}
|
|
284235
|
+
async executeOnPersistentShell(instance, command, options) {
|
|
284182
284236
|
instance.dataCallback?.(`\r
|
|
284183
284237
|
\x1B[36m\u276F ${command}\x1B[0m\r
|
|
284184
284238
|
`);
|
|
284185
|
-
|
|
284186
|
-
|
|
284187
|
-
|
|
284188
|
-
|
|
284189
|
-
|
|
284190
|
-
|
|
284191
|
-
|
|
284239
|
+
let result;
|
|
284240
|
+
const cwdPrefix = instance.cwd ? `cd /d ${instance.cwd} & ` : "";
|
|
284241
|
+
const isCd = /^\s*(cd|chdir)\s+/i.test(command);
|
|
284242
|
+
try {
|
|
284243
|
+
const shellId = await this.ensurePersistentShell(instance);
|
|
284244
|
+
result = await instance.transport.runCommandOnShell(shellId, cwdPrefix + command, {
|
|
284245
|
+
timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
|
|
284246
|
+
signal: options?.signal,
|
|
284247
|
+
onChunk: (stream, text) => {
|
|
284248
|
+
if (text) instance.dataCallback?.(stream === "stderr" ? `\x1B[33m${text}\x1B[0m` : text);
|
|
284249
|
+
}
|
|
284250
|
+
});
|
|
284251
|
+
if (isCd && result.exitCode === 0) {
|
|
284252
|
+
const target = command.replace(/^\s*(cd|chdir)\s+\/?d?\s*/i, "").replace(/"/g, "").trim();
|
|
284253
|
+
instance.cwd = this.resolveWinCwd(instance.cwd, target);
|
|
284254
|
+
} else if (result.exitCode === 0) {
|
|
284255
|
+
const probe = await instance.transport.runCommandOnShell(shellId, `${cwdPrefix}cd`, { timeoutMs: 1e4 }).catch(() => null);
|
|
284256
|
+
const probeCwd = probe?.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => /^[A-Za-z]:\\/.test(l)).pop();
|
|
284257
|
+
if (probeCwd) instance.cwd = probeCwd;
|
|
284258
|
+
}
|
|
284259
|
+
} catch (error40) {
|
|
284260
|
+
instance.persistentShellId = void 0;
|
|
284261
|
+
const shellId = await this.ensurePersistentShell(instance);
|
|
284262
|
+
result = await instance.transport.runCommandOnShell(shellId, cwdPrefix + command, {
|
|
284263
|
+
timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
|
|
284264
|
+
signal: options?.signal,
|
|
284265
|
+
onChunk: (stream, text) => {
|
|
284266
|
+
if (text) instance.dataCallback?.(stream === "stderr" ? `\x1B[33m${text}\x1B[0m` : text);
|
|
284267
|
+
}
|
|
284268
|
+
});
|
|
284192
284269
|
}
|
|
284193
284270
|
instance.dataCallback?.(
|
|
284194
284271
|
`\r
|
|
@@ -284197,6 +284274,22 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
|
|
|
284197
284274
|
);
|
|
284198
284275
|
return result;
|
|
284199
284276
|
}
|
|
284277
|
+
/** Resolve a `cd` target (absolute or relative) against the tracked cwd. */
|
|
284278
|
+
resolveWinCwd(currentCwd, target) {
|
|
284279
|
+
if (!target) return currentCwd ?? "";
|
|
284280
|
+
if (/^[A-Za-z]:[\\/]/.test(target)) {
|
|
284281
|
+
return target.replace(/\//g, "\\").replace(/\\+$/, "");
|
|
284282
|
+
}
|
|
284283
|
+
if (/^[A-Za-z]:$/.test(target)) return `${target}\\`;
|
|
284284
|
+
const base = (currentCwd ?? "C:\\").replace(/\\+$/, "");
|
|
284285
|
+
const parts = base.split("\\").filter(Boolean);
|
|
284286
|
+
for (const seg of target.replace(/\//g, "\\").split("\\")) {
|
|
284287
|
+
if (seg === "" || seg === ".") continue;
|
|
284288
|
+
if (seg === "..") parts.pop();
|
|
284289
|
+
else parts.push(seg);
|
|
284290
|
+
}
|
|
284291
|
+
return parts.join("\\");
|
|
284292
|
+
}
|
|
284200
284293
|
async waitForReady(instance, timeoutMs) {
|
|
284201
284294
|
if (instance.ready) return true;
|
|
284202
284295
|
if (instance.failed) return false;
|
|
@@ -284217,6 +284310,10 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
|
|
|
284217
284310
|
const instance = this.instances.get(ptyId);
|
|
284218
284311
|
if (!instance) return;
|
|
284219
284312
|
this.instances.delete(ptyId);
|
|
284313
|
+
if (instance.persistentShellId) {
|
|
284314
|
+
void instance.transport.deleteShell(instance.persistentShellId);
|
|
284315
|
+
instance.persistentShellId = void 0;
|
|
284316
|
+
}
|
|
284220
284317
|
instance.exitCallback?.(0);
|
|
284221
284318
|
}
|
|
284222
284319
|
onData(ptyId, callback) {
|
|
@@ -284227,8 +284324,8 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
|
|
|
284227
284324
|
const instance = this.instances.get(ptyId);
|
|
284228
284325
|
if (instance) instance.exitCallback = callback;
|
|
284229
284326
|
}
|
|
284230
|
-
getCwd(
|
|
284231
|
-
return
|
|
284327
|
+
getCwd(ptyId) {
|
|
284328
|
+
return this.instances.get(ptyId)?.cwd;
|
|
284232
284329
|
}
|
|
284233
284330
|
getHomeDir(_ptyId) {
|
|
284234
284331
|
return Promise.resolve(void 0);
|
|
@@ -284368,6 +284465,35 @@ var SerialBackend = class {
|
|
|
284368
284465
|
if (!inst) return void 0;
|
|
284369
284466
|
return inst.ready ? "ready" : void 0;
|
|
284370
284467
|
}
|
|
284468
|
+
// --- Serial-specific controls (v3.0.5) ---
|
|
284469
|
+
/** Send a BREAK signal (Cisco password recovery / ROMMON). Default 500ms. */
|
|
284470
|
+
sendBreak(ptyId, durationMs = 500) {
|
|
284471
|
+
const inst = this.instances.get(ptyId);
|
|
284472
|
+
if (!inst || typeof inst.port.break !== "function") return false;
|
|
284473
|
+
try {
|
|
284474
|
+
inst.port.break({ duration: durationMs }, (err) => {
|
|
284475
|
+
if (err) inst.dataCallback?.(`\x1B[31m\u2718 Break failed: ${err.message}\x1B[0m\r
|
|
284476
|
+
`);
|
|
284477
|
+
});
|
|
284478
|
+
return true;
|
|
284479
|
+
} catch {
|
|
284480
|
+
return false;
|
|
284481
|
+
}
|
|
284482
|
+
}
|
|
284483
|
+
/** Set modem control lines (DTR/RTS/CTS). */
|
|
284484
|
+
setControlLines(ptyId, lines) {
|
|
284485
|
+
const inst = this.instances.get(ptyId);
|
|
284486
|
+
if (!inst || typeof inst.port.set !== "function") return false;
|
|
284487
|
+
try {
|
|
284488
|
+
inst.port.set(lines, (err) => {
|
|
284489
|
+
if (err) inst.dataCallback?.(`\x1B[31m\u2718 set() failed: ${err.message}\x1B[0m\r
|
|
284490
|
+
`);
|
|
284491
|
+
});
|
|
284492
|
+
return true;
|
|
284493
|
+
} catch {
|
|
284494
|
+
return false;
|
|
284495
|
+
}
|
|
284496
|
+
}
|
|
284371
284497
|
};
|
|
284372
284498
|
|
|
284373
284499
|
// ../../packages/backend/src/services/ShellUtility.ts
|
|
@@ -284376,6 +284502,190 @@ function escapeShellPathList(paths) {
|
|
|
284376
284502
|
return escaped.join(" ") + (escaped.length ? " " : "");
|
|
284377
284503
|
}
|
|
284378
284504
|
|
|
284505
|
+
// ../../packages/backend/src/services/terminal/autoReconnect.ts
|
|
284506
|
+
var DEFAULT_BASE = 1e3;
|
|
284507
|
+
var DEFAULT_MAX = 6e4;
|
|
284508
|
+
var DEFAULT_JITTER = 0.2;
|
|
284509
|
+
var AutoReconnect = class {
|
|
284510
|
+
baseDelayMs;
|
|
284511
|
+
maxDelayMs;
|
|
284512
|
+
maxAttempts;
|
|
284513
|
+
jitterRatio;
|
|
284514
|
+
random;
|
|
284515
|
+
setTimeoutFn;
|
|
284516
|
+
clearTimeoutFn;
|
|
284517
|
+
/** terminalId → pending timer handle. */
|
|
284518
|
+
timers = /* @__PURE__ */ new Map();
|
|
284519
|
+
/** terminalId → attempts fired so far. */
|
|
284520
|
+
attempts = /* @__PURE__ */ new Map();
|
|
284521
|
+
/** terminalId → delay used for the pending attempt. */
|
|
284522
|
+
pendingDelay = /* @__PURE__ */ new Map();
|
|
284523
|
+
constructor(opts = {}) {
|
|
284524
|
+
this.baseDelayMs = Math.max(1, opts.baseDelayMs ?? DEFAULT_BASE);
|
|
284525
|
+
this.maxDelayMs = Math.max(this.baseDelayMs, opts.maxDelayMs ?? DEFAULT_MAX);
|
|
284526
|
+
this.maxAttempts = Math.max(1, opts.maxAttempts ?? Number.POSITIVE_INFINITY);
|
|
284527
|
+
this.jitterRatio = Math.min(1, Math.max(0, opts.jitterRatio ?? DEFAULT_JITTER));
|
|
284528
|
+
this.random = opts.random ?? (() => Math.random());
|
|
284529
|
+
this.setTimeoutFn = opts.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
|
|
284530
|
+
this.clearTimeoutFn = opts.clearTimeoutFn ?? ((h) => clearTimeout(h));
|
|
284531
|
+
}
|
|
284532
|
+
/** Compute the delay for attempt N (1-based): base * 2^(n-1), capped, ±jitter. */
|
|
284533
|
+
delayForAttempt(attempt) {
|
|
284534
|
+
const expo = this.baseDelayMs * Math.pow(2, Math.max(0, attempt - 1));
|
|
284535
|
+
const capped = Math.min(this.maxDelayMs, expo);
|
|
284536
|
+
const jitter = capped * this.jitterRatio * (this.random() * 2 - 1);
|
|
284537
|
+
return Math.max(1, Math.round(capped + jitter));
|
|
284538
|
+
}
|
|
284539
|
+
/** Whether a schedule is pending for this terminal. */
|
|
284540
|
+
isScheduled(terminalId) {
|
|
284541
|
+
return this.timers.has(terminalId);
|
|
284542
|
+
}
|
|
284543
|
+
/** Attempts fired so far for this terminal. */
|
|
284544
|
+
attemptsFor(terminalId) {
|
|
284545
|
+
return this.attempts.get(terminalId) ?? 0;
|
|
284546
|
+
}
|
|
284547
|
+
/** Snapshot the schedule state for a terminal (undefined if not scheduled). */
|
|
284548
|
+
state(terminalId) {
|
|
284549
|
+
if (!this.timers.has(terminalId)) return void 0;
|
|
284550
|
+
const fired = this.attempts.get(terminalId) ?? 0;
|
|
284551
|
+
return {
|
|
284552
|
+
terminalId,
|
|
284553
|
+
nextAttempt: fired + 1,
|
|
284554
|
+
nextDelayMs: this.pendingDelay.get(terminalId) ?? 0,
|
|
284555
|
+
attemptsFired: fired,
|
|
284556
|
+
active: true
|
|
284557
|
+
};
|
|
284558
|
+
}
|
|
284559
|
+
/**
|
|
284560
|
+
* Schedule the next reconnect attempt for a terminal. `onAttempt` is called
|
|
284561
|
+
* when the timer fires (it should perform the reconnect); `onGiveUp` is
|
|
284562
|
+
* called when maxAttempts is reached. Returns the schedule state, or
|
|
284563
|
+
* undefined if attempts are already exhausted.
|
|
284564
|
+
*/
|
|
284565
|
+
schedule(terminalId, onAttempt, onGiveUp) {
|
|
284566
|
+
this.cancel(terminalId);
|
|
284567
|
+
const fired = this.attempts.get(terminalId) ?? 0;
|
|
284568
|
+
const nextAttempt = fired + 1;
|
|
284569
|
+
if (nextAttempt > this.maxAttempts) {
|
|
284570
|
+
onGiveUp?.(fired);
|
|
284571
|
+
return void 0;
|
|
284572
|
+
}
|
|
284573
|
+
const delay = this.delayForAttempt(nextAttempt);
|
|
284574
|
+
this.pendingDelay.set(terminalId, delay);
|
|
284575
|
+
const handle = this.setTimeoutFn(() => {
|
|
284576
|
+
this.timers.delete(terminalId);
|
|
284577
|
+
this.pendingDelay.delete(terminalId);
|
|
284578
|
+
const now = (this.attempts.get(terminalId) ?? 0) + 1;
|
|
284579
|
+
this.attempts.set(terminalId, now);
|
|
284580
|
+
onAttempt(now);
|
|
284581
|
+
}, delay);
|
|
284582
|
+
this.timers.set(terminalId, handle);
|
|
284583
|
+
return {
|
|
284584
|
+
terminalId,
|
|
284585
|
+
nextAttempt,
|
|
284586
|
+
nextDelayMs: delay,
|
|
284587
|
+
attemptsFired: fired,
|
|
284588
|
+
active: true
|
|
284589
|
+
};
|
|
284590
|
+
}
|
|
284591
|
+
/** Cancel a pending schedule (does NOT reset the attempt counter). */
|
|
284592
|
+
cancel(terminalId) {
|
|
284593
|
+
const handle = this.timers.get(terminalId);
|
|
284594
|
+
if (handle !== void 0) {
|
|
284595
|
+
this.clearTimeoutFn(handle);
|
|
284596
|
+
this.timers.delete(terminalId);
|
|
284597
|
+
}
|
|
284598
|
+
this.pendingDelay.delete(terminalId);
|
|
284599
|
+
return handle !== void 0;
|
|
284600
|
+
}
|
|
284601
|
+
/** Reset a terminal's attempt counter (call after a successful reconnect). */
|
|
284602
|
+
reset(terminalId) {
|
|
284603
|
+
this.attempts.delete(terminalId);
|
|
284604
|
+
this.pendingDelay.delete(terminalId);
|
|
284605
|
+
}
|
|
284606
|
+
/** Cancel + reset (call on manual kill / user reconnect / tab close). */
|
|
284607
|
+
clear(terminalId) {
|
|
284608
|
+
this.cancel(terminalId);
|
|
284609
|
+
this.reset(terminalId);
|
|
284610
|
+
}
|
|
284611
|
+
/** Cancel every pending schedule (shutdown). */
|
|
284612
|
+
clearAll() {
|
|
284613
|
+
for (const id of Array.from(this.timers.keys())) this.cancel(id);
|
|
284614
|
+
this.attempts.clear();
|
|
284615
|
+
this.pendingDelay.clear();
|
|
284616
|
+
}
|
|
284617
|
+
};
|
|
284618
|
+
|
|
284619
|
+
// ../../packages/backend/src/services/terminal/chunkedRingBuffer.ts
|
|
284620
|
+
var DEFAULT_MAX2 = 2e5;
|
|
284621
|
+
var DEFAULT_CHUNK = 16384;
|
|
284622
|
+
var ChunkedRingBuffer = class {
|
|
284623
|
+
chunks = [];
|
|
284624
|
+
maxSize;
|
|
284625
|
+
chunkSize;
|
|
284626
|
+
/** total chars currently retained. */
|
|
284627
|
+
retained = 0;
|
|
284628
|
+
/** total chars ever appended (monotonic; matches the old `offset`). */
|
|
284629
|
+
offset = 0;
|
|
284630
|
+
constructor(opts = {}) {
|
|
284631
|
+
this.maxSize = Math.max(1, opts.maxSize ?? DEFAULT_MAX2);
|
|
284632
|
+
this.chunkSize = Math.max(256, opts.chunkSize ?? DEFAULT_CHUNK);
|
|
284633
|
+
}
|
|
284634
|
+
/** Append data; drops whole old chunks if over maxSize. */
|
|
284635
|
+
append(data) {
|
|
284636
|
+
if (!data) return;
|
|
284637
|
+
this.offset += data.length;
|
|
284638
|
+
let rest = data;
|
|
284639
|
+
while (rest.length > 0) {
|
|
284640
|
+
const last = this.chunks[this.chunks.length - 1];
|
|
284641
|
+
if (last !== void 0 && last.length < this.chunkSize) {
|
|
284642
|
+
const take = Math.min(this.chunkSize - last.length, rest.length);
|
|
284643
|
+
this.chunks[this.chunks.length - 1] = last + rest.slice(0, take);
|
|
284644
|
+
this.retained += take;
|
|
284645
|
+
rest = rest.slice(take);
|
|
284646
|
+
} else {
|
|
284647
|
+
const take = Math.min(this.chunkSize, rest.length);
|
|
284648
|
+
this.chunks.push(rest.slice(0, take));
|
|
284649
|
+
this.retained += take;
|
|
284650
|
+
rest = rest.slice(take);
|
|
284651
|
+
}
|
|
284652
|
+
}
|
|
284653
|
+
this.evict();
|
|
284654
|
+
}
|
|
284655
|
+
/** Drop oldest whole chunks until retained <= maxSize. */
|
|
284656
|
+
evict() {
|
|
284657
|
+
while (this.retained > this.maxSize && this.chunks.length > 0) {
|
|
284658
|
+
const head = this.chunks[0];
|
|
284659
|
+
if (this.retained - head.length >= this.maxSize) {
|
|
284660
|
+
this.chunks.shift();
|
|
284661
|
+
this.retained -= head.length;
|
|
284662
|
+
} else {
|
|
284663
|
+
const trim = this.retained - this.maxSize;
|
|
284664
|
+
this.chunks[0] = head.slice(trim);
|
|
284665
|
+
this.retained -= trim;
|
|
284666
|
+
break;
|
|
284667
|
+
}
|
|
284668
|
+
}
|
|
284669
|
+
}
|
|
284670
|
+
/** The retained content as a single string (for reads/search). */
|
|
284671
|
+
content() {
|
|
284672
|
+
return this.chunks.join("");
|
|
284673
|
+
}
|
|
284674
|
+
/** Retained size in chars. */
|
|
284675
|
+
size() {
|
|
284676
|
+
return this.retained;
|
|
284677
|
+
}
|
|
284678
|
+
/** Number of chunks (for tests/diagnostics). */
|
|
284679
|
+
chunkCount() {
|
|
284680
|
+
return this.chunks.length;
|
|
284681
|
+
}
|
|
284682
|
+
clear() {
|
|
284683
|
+
this.chunks = [];
|
|
284684
|
+
this.retained = 0;
|
|
284685
|
+
this.offset = 0;
|
|
284686
|
+
}
|
|
284687
|
+
};
|
|
284688
|
+
|
|
284379
284689
|
// ../../node_modules/uuid/dist/esm-node/rng.js
|
|
284380
284690
|
var import_crypto = __toESM(require("crypto"));
|
|
284381
284691
|
var rnds8Pool = new Uint8Array(256);
|
|
@@ -289265,6 +289575,20 @@ function stripInternalControlMarkers(s) {
|
|
|
289265
289575
|
function stripTerminalControlSequences(s) {
|
|
289266
289576
|
return s.replace(ANSI_OSC_SEQUENCE_PATTERN, "").replace(ANSI_CSI_SEQUENCE_PATTERN, "").replace(OTHER_CONTROL_CHAR_PATTERN, "");
|
|
289267
289577
|
}
|
|
289578
|
+
function makeRingBuffer(maxSize) {
|
|
289579
|
+
const buf = new ChunkedRingBuffer({ maxSize });
|
|
289580
|
+
return {
|
|
289581
|
+
get content() {
|
|
289582
|
+
return buf.content();
|
|
289583
|
+
},
|
|
289584
|
+
get offset() {
|
|
289585
|
+
return buf.offset;
|
|
289586
|
+
},
|
|
289587
|
+
set offset(_v) {
|
|
289588
|
+
},
|
|
289589
|
+
append: (data) => buf.append(data)
|
|
289590
|
+
};
|
|
289591
|
+
}
|
|
289268
289592
|
var cloneTerminalConfig = (config2) => JSON.parse(JSON.stringify(config2));
|
|
289269
289593
|
var normalizeTerminalConfigForRuntime = (config2) => {
|
|
289270
289594
|
const normalized = cloneTerminalConfig(config2);
|
|
@@ -289327,6 +289651,7 @@ var TerminalService = class {
|
|
|
289327
289651
|
commandTrackingPromptSyncPollIntervalMs = 50;
|
|
289328
289652
|
syntheticCommandQuietWindowMs = 1e3;
|
|
289329
289653
|
terminalIdsBeingKilled = /* @__PURE__ */ new Set();
|
|
289654
|
+
autoReconnect = new AutoReconnect({ maxAttempts: 10 });
|
|
289330
289655
|
terminalClosedListeners = /* @__PURE__ */ new Set();
|
|
289331
289656
|
/** Optional session logger (records terminal output to disk per session). */
|
|
289332
289657
|
sessionLogger = null;
|
|
@@ -289683,7 +290008,7 @@ var TerminalService = class {
|
|
|
289683
290008
|
this.terminals.set(config2.id, tab);
|
|
289684
290009
|
this.terminalConfigs.set(config2.id, config2);
|
|
289685
290010
|
this.releaseReservedTitleForTerminal(config2.id, reservedTitle);
|
|
289686
|
-
this.buffers.set(config2.id,
|
|
290011
|
+
this.buffers.set(config2.id, makeRingBuffer(MAX_BUFFER_SIZE));
|
|
289687
290012
|
this.headlessPtys.set(config2.id, headless);
|
|
289688
290013
|
if (config2.type === "local" && !this.primaryLocalTerminalId) {
|
|
289689
290014
|
this.primaryLocalTerminalId = config2.id;
|
|
@@ -289766,6 +290091,8 @@ var TerminalService = class {
|
|
|
289766
290091
|
runtime.ptyId
|
|
289767
290092
|
);
|
|
289768
290093
|
this.hydrateTerminalRuntimeMetadata(terminalId);
|
|
290094
|
+
this.autoReconnect.clear(terminalId);
|
|
290095
|
+
tab.reconnectState = void 0;
|
|
289769
290096
|
this.publishTerminalTabsChanged();
|
|
289770
290097
|
this.schedulePersistTerminalState();
|
|
289771
290098
|
return tab;
|
|
@@ -289778,6 +290105,43 @@ var TerminalService = class {
|
|
|
289778
290105
|
throw error40;
|
|
289779
290106
|
}
|
|
289780
290107
|
}
|
|
290108
|
+
/** Schedule an auto-reconnect for a dropped SSH tab with backoff. Updates
|
|
290109
|
+
* tab.reconnectState so the UI can show "reconnecting (attempt N)…". */
|
|
290110
|
+
scheduleSshAutoReconnect(terminalId) {
|
|
290111
|
+
const tab = this.terminals.get(terminalId);
|
|
290112
|
+
if (!tab || tab.type !== "ssh") return;
|
|
290113
|
+
const state = this.autoReconnect.schedule(
|
|
290114
|
+
terminalId,
|
|
290115
|
+
() => {
|
|
290116
|
+
const t = this.terminals.get(terminalId);
|
|
290117
|
+
if (!t || t.type !== "ssh") return;
|
|
290118
|
+
void this.reconnectTerminal(terminalId).catch(() => {
|
|
290119
|
+
});
|
|
290120
|
+
},
|
|
290121
|
+
(attempts) => {
|
|
290122
|
+
const t = this.terminals.get(terminalId);
|
|
290123
|
+
if (t) {
|
|
290124
|
+
t.reconnectState = {
|
|
290125
|
+
scheduled: false,
|
|
290126
|
+
attempt: attempts,
|
|
290127
|
+
attempts,
|
|
290128
|
+
nextDelayMs: 0,
|
|
290129
|
+
gaveUp: true
|
|
290130
|
+
};
|
|
290131
|
+
this.publishTerminalTabsChanged();
|
|
290132
|
+
}
|
|
290133
|
+
}
|
|
290134
|
+
);
|
|
290135
|
+
if (state && tab) {
|
|
290136
|
+
tab.reconnectState = {
|
|
290137
|
+
scheduled: true,
|
|
290138
|
+
attempt: state.nextAttempt,
|
|
290139
|
+
attempts: state.attemptsFired,
|
|
290140
|
+
nextDelayMs: state.nextDelayMs
|
|
290141
|
+
};
|
|
290142
|
+
this.publishTerminalTabsChanged();
|
|
290143
|
+
}
|
|
290144
|
+
}
|
|
289781
290145
|
async restartLocalTerminalAfterExit(terminalId, code) {
|
|
289782
290146
|
const tab = this.terminals.get(terminalId);
|
|
289783
290147
|
const existingConfig = this.terminalConfigs.get(terminalId);
|
|
@@ -289889,13 +290253,8 @@ var TerminalService = class {
|
|
|
289889
290253
|
const buffer = this.buffers.get(terminalId);
|
|
289890
290254
|
let currentOffset = 0;
|
|
289891
290255
|
if (buffer) {
|
|
289892
|
-
buffer.
|
|
289893
|
-
buffer.offset += cleanedData.length;
|
|
290256
|
+
buffer.append(cleanedData);
|
|
289894
290257
|
currentOffset = buffer.offset;
|
|
289895
|
-
if (buffer.content.length > MAX_BUFFER_SIZE) {
|
|
289896
|
-
const trimAmount = buffer.content.length - MAX_BUFFER_SIZE;
|
|
289897
|
-
buffer.content = buffer.content.slice(trimAmount);
|
|
289898
|
-
}
|
|
289899
290258
|
}
|
|
289900
290259
|
this.sendToRenderer("terminal:data", { terminalId, data: cleanedData, offset: currentOffset });
|
|
289901
290260
|
}
|
|
@@ -289926,13 +290285,8 @@ var TerminalService = class {
|
|
|
289926
290285
|
const buffer = this.buffers.get(terminalId);
|
|
289927
290286
|
let currentOffset = 0;
|
|
289928
290287
|
if (buffer) {
|
|
289929
|
-
buffer.
|
|
289930
|
-
buffer.offset += data.length;
|
|
290288
|
+
buffer.append(data);
|
|
289931
290289
|
currentOffset = buffer.offset;
|
|
289932
|
-
if (buffer.content.length > MAX_BUFFER_SIZE) {
|
|
289933
|
-
const trimAmount = buffer.content.length - MAX_BUFFER_SIZE;
|
|
289934
|
-
buffer.content = buffer.content.slice(trimAmount);
|
|
289935
|
-
}
|
|
289936
290290
|
}
|
|
289937
290291
|
this.sendToRenderer("terminal:data", { terminalId, data, offset: currentOffset });
|
|
289938
290292
|
}
|
|
@@ -290144,6 +290498,9 @@ ${promptPrefix}`;
|
|
|
290144
290498
|
rows: tab.rows
|
|
290145
290499
|
});
|
|
290146
290500
|
}
|
|
290501
|
+
if (tab.type === "ssh" && !this.terminalIdsBeingKilled.has(terminalId)) {
|
|
290502
|
+
this.scheduleSshAutoReconnect(terminalId);
|
|
290503
|
+
}
|
|
290147
290504
|
}
|
|
290148
290505
|
this.sendToRenderer("terminal:exit", { terminalId, code });
|
|
290149
290506
|
this.publishTerminalTabsChanged();
|
|
@@ -290223,6 +290580,7 @@ ${promptPrefix}`;
|
|
|
290223
290580
|
}
|
|
290224
290581
|
kill(terminalId) {
|
|
290225
290582
|
this.pendingResizeByTerminal.delete(terminalId);
|
|
290583
|
+
this.autoReconnect.clear(terminalId);
|
|
290226
290584
|
const terminal = this.terminals.get(terminalId);
|
|
290227
290585
|
if (terminal) {
|
|
290228
290586
|
const backend = this.getBackend(terminal.type);
|
|
@@ -342422,6 +342780,109 @@ async function runReadFile(args, context2, readFileSupport) {
|
|
|
342422
342780
|
}
|
|
342423
342781
|
}
|
|
342424
342782
|
|
|
342783
|
+
// ../../packages/backend/src/memory/memoryManager.ts
|
|
342784
|
+
function parseMemoryEntries(content) {
|
|
342785
|
+
const out = [];
|
|
342786
|
+
const lines = String(content || "").replace(/\r\n/g, "\n").split("\n");
|
|
342787
|
+
for (const line of lines) {
|
|
342788
|
+
const t = line.trim();
|
|
342789
|
+
if (!t || t === "# Memory" || t.startsWith("- Add durable cross-session notes")) continue;
|
|
342790
|
+
if (/^#{1,6}\s/.test(t) || /^[-*]\s/.test(t) || t.length > 24) {
|
|
342791
|
+
out.push({ text: t, tokens: tokenize2(t) });
|
|
342792
|
+
}
|
|
342793
|
+
}
|
|
342794
|
+
return out;
|
|
342795
|
+
}
|
|
342796
|
+
function tokenize2(s) {
|
|
342797
|
+
return (s.toLowerCase().match(/[a-z0-9_.\-/]{3,}/g) ?? []).filter(
|
|
342798
|
+
(w) => !STOP.has(w)
|
|
342799
|
+
);
|
|
342800
|
+
}
|
|
342801
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
342802
|
+
"the",
|
|
342803
|
+
"and",
|
|
342804
|
+
"for",
|
|
342805
|
+
"with",
|
|
342806
|
+
"this",
|
|
342807
|
+
"that",
|
|
342808
|
+
"from",
|
|
342809
|
+
"are",
|
|
342810
|
+
"was",
|
|
342811
|
+
"were",
|
|
342812
|
+
"has",
|
|
342813
|
+
"have",
|
|
342814
|
+
"not",
|
|
342815
|
+
"now",
|
|
342816
|
+
"via",
|
|
342817
|
+
"into",
|
|
342818
|
+
"all",
|
|
342819
|
+
"out",
|
|
342820
|
+
"use",
|
|
342821
|
+
"using"
|
|
342822
|
+
]);
|
|
342823
|
+
function searchMemory(content, query, limit2 = 10) {
|
|
342824
|
+
const q = tokenize2(query);
|
|
342825
|
+
if (q.length === 0) return [];
|
|
342826
|
+
const qSet = new Set(q);
|
|
342827
|
+
const scored = [];
|
|
342828
|
+
for (const e of parseMemoryEntries(content)) {
|
|
342829
|
+
let score = 0;
|
|
342830
|
+
for (const tok of e.tokens) if (qSet.has(tok)) score += 1;
|
|
342831
|
+
if (score > 0) scored.push({ text: e.text, score });
|
|
342832
|
+
}
|
|
342833
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit2);
|
|
342834
|
+
}
|
|
342835
|
+
function normKey(s) {
|
|
342836
|
+
return s.toLowerCase().replace(/^\s*[-*+]\s+/, "").replace(/\s+/g, " ").trim();
|
|
342837
|
+
}
|
|
342838
|
+
function appendMemoryNote(content, note, opts = {}) {
|
|
342839
|
+
const maxChars = Math.max(1e3, opts.maxChars ?? 4e4);
|
|
342840
|
+
const body = String(content || "").replace(/\r\n/g, "\n").replace(/\s+$/, "");
|
|
342841
|
+
const noteLine = note.trim().replace(/\n+/g, " ").trim();
|
|
342842
|
+
const key = normKey(noteLine);
|
|
342843
|
+
const lines = body.split("\n");
|
|
342844
|
+
const kept = lines.filter((l) => normKey(l) !== key);
|
|
342845
|
+
const next = [...kept, "", noteLine].join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
342846
|
+
if (next.length <= maxChars) return next;
|
|
342847
|
+
const head = [];
|
|
342848
|
+
const tail4 = [];
|
|
342849
|
+
const all = next.split("\n");
|
|
342850
|
+
let i = 0;
|
|
342851
|
+
for (; i < all.length; i += 1) {
|
|
342852
|
+
if (/^#\s/.test(all[i]) || all[i].trim() === "") head.push(all[i]);
|
|
342853
|
+
else break;
|
|
342854
|
+
}
|
|
342855
|
+
for (let j = all.length - 1; j >= i; j -= 1) {
|
|
342856
|
+
tail4.unshift(all[j]);
|
|
342857
|
+
if (tail4.join("\n").length > maxChars * 0.9) break;
|
|
342858
|
+
}
|
|
342859
|
+
return [...head, ...tail4].join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
342860
|
+
}
|
|
342861
|
+
function recallForPrompt(content, opts = {}) {
|
|
342862
|
+
const maxChars = Math.max(2e3, opts.maxChars ?? 12e3);
|
|
342863
|
+
const body = String(content || "");
|
|
342864
|
+
if (body.length <= maxChars) return body;
|
|
342865
|
+
const hits = opts.query ? searchMemory(body, opts.query, 30) : [];
|
|
342866
|
+
const picked = [];
|
|
342867
|
+
let total = 0;
|
|
342868
|
+
const push2 = (t) => {
|
|
342869
|
+
if (total + t.length + 1 > maxChars) return false;
|
|
342870
|
+
picked.push(t);
|
|
342871
|
+
total += t.length + 1;
|
|
342872
|
+
return true;
|
|
342873
|
+
};
|
|
342874
|
+
if (hits.length > 0) {
|
|
342875
|
+
for (const h of hits) if (!push2(h.text)) break;
|
|
342876
|
+
} else {
|
|
342877
|
+
const entries = parseMemoryEntries(body);
|
|
342878
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
342879
|
+
if (!push2(entries[i].text)) break;
|
|
342880
|
+
}
|
|
342881
|
+
picked.reverse();
|
|
342882
|
+
}
|
|
342883
|
+
return picked.join("\n");
|
|
342884
|
+
}
|
|
342885
|
+
|
|
342425
342886
|
// ../../packages/backend/src/services/AgentHelper/prompts.ts
|
|
342426
342887
|
var SYS_INFO_MARKER = "CURRENT_SYSTEM_INFO_MSG:\n";
|
|
342427
342888
|
var GYSHELL_BASE_SYSTEM_MARKER = "# Role: GyShell Assistant";
|
|
@@ -343021,14 +343482,20 @@ function buildMemoryPromptBlock(opts) {
|
|
|
343021
343482
|
/\r\n/g,
|
|
343022
343483
|
"\n"
|
|
343023
343484
|
);
|
|
343485
|
+
const recalled = recallForPrompt(normalizedContent, {
|
|
343486
|
+
query: opts.userInput,
|
|
343487
|
+
maxChars: 12e3
|
|
343488
|
+
});
|
|
343489
|
+
const truncated = recalled.length < normalizedContent.length;
|
|
343024
343490
|
return [
|
|
343025
343491
|
GLOBAL_MEMORY_TAG.trim(),
|
|
343026
343492
|
`Memory file absolute path: ${opts.memoryFilePath}`,
|
|
343027
343493
|
"If you need to add or modify memory, use edit_file to edit this exact file path directly. Use write_file only when intentionally replacing the full memory file.",
|
|
343028
343494
|
"If you need to re-read memory later, use the read_file tool to read this exact file path directly.",
|
|
343495
|
+
truncated ? "(Memory file is large \u2014 only the most relevant entries are shown below; read the full file with read_file if needed.)" : "",
|
|
343029
343496
|
"",
|
|
343030
|
-
"# Full MEMORY.md Content",
|
|
343031
|
-
|
|
343497
|
+
truncated ? "# Relevant MEMORY.md entries" : "# Full MEMORY.md Content",
|
|
343498
|
+
recalled
|
|
343032
343499
|
].join("\n");
|
|
343033
343500
|
}
|
|
343034
343501
|
function createBaseSystemPromptText(memoryPrompt) {
|
|
@@ -345650,7 +346117,7 @@ function evalCond(expr, scope) {
|
|
|
345650
346117
|
function cleanTag(inner) {
|
|
345651
346118
|
return inner.replace(/^-|\s-$/g, "").trim();
|
|
345652
346119
|
}
|
|
345653
|
-
function
|
|
346120
|
+
function tokenize3(tpl) {
|
|
345654
346121
|
const tokenRe = /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\})/g;
|
|
345655
346122
|
let lastIndex = 0;
|
|
345656
346123
|
const tokens = [];
|
|
@@ -345720,7 +346187,7 @@ function splitIfElse(body) {
|
|
|
345720
346187
|
return { ifBody, elseBody, elifs };
|
|
345721
346188
|
}
|
|
345722
346189
|
function renderTemplate2(tpl, vars = {}) {
|
|
345723
|
-
const tokens =
|
|
346190
|
+
const tokens = tokenize3(tpl);
|
|
345724
346191
|
function renderTokens2(toks, scope) {
|
|
345725
346192
|
let s = "";
|
|
345726
346193
|
let idx = 0;
|
|
@@ -368073,6 +368540,33 @@ var WebSocketGatewayAdapter = class {
|
|
|
368073
368540
|
}
|
|
368074
368541
|
return await this.options.memoryBridge.setContent(content);
|
|
368075
368542
|
}
|
|
368543
|
+
case "memory:search": {
|
|
368544
|
+
if (!this.options.memoryBridge?.search) {
|
|
368545
|
+
throw new WebSocketRpcError(
|
|
368546
|
+
"METHOD_NOT_FOUND",
|
|
368547
|
+
"memory:search is not available on this websocket gateway."
|
|
368548
|
+
);
|
|
368549
|
+
}
|
|
368550
|
+
const query = params.query;
|
|
368551
|
+
if (typeof query !== "string") {
|
|
368552
|
+
throw new WebSocketRpcError("BAD_REQUEST", "query must be string.");
|
|
368553
|
+
}
|
|
368554
|
+
const limit2 = typeof params.limit === "number" ? params.limit : void 0;
|
|
368555
|
+
return { results: await this.options.memoryBridge.search(query, limit2) };
|
|
368556
|
+
}
|
|
368557
|
+
case "memory:append": {
|
|
368558
|
+
if (!this.options.memoryBridge?.append) {
|
|
368559
|
+
throw new WebSocketRpcError(
|
|
368560
|
+
"METHOD_NOT_FOUND",
|
|
368561
|
+
"memory:append is not available on this websocket gateway."
|
|
368562
|
+
);
|
|
368563
|
+
}
|
|
368564
|
+
const note = params.note;
|
|
368565
|
+
if (typeof note !== "string" || note.trim() === "") {
|
|
368566
|
+
throw new WebSocketRpcError("BAD_REQUEST", "note must be a non-empty string.");
|
|
368567
|
+
}
|
|
368568
|
+
return await this.options.memoryBridge.append(note);
|
|
368569
|
+
}
|
|
368076
368570
|
case "agentSettings:get": {
|
|
368077
368571
|
if (!this.options.agentSettingsBridge?.get) {
|
|
368078
368572
|
throw new WebSocketRpcError(
|
|
@@ -389081,6 +389575,20 @@ async function startGyBackend() {
|
|
|
389081
389575
|
);
|
|
389082
389576
|
gatewayService.broadcastRaw("memory:updated", snapshot);
|
|
389083
389577
|
return snapshot;
|
|
389578
|
+
},
|
|
389579
|
+
search: async (query, limit2) => {
|
|
389580
|
+
const { content } = await memoryService.getMemorySnapshot(
|
|
389581
|
+
settingsService.getSettings().agentSettings?.activeProfileId || null
|
|
389582
|
+
);
|
|
389583
|
+
return searchMemory(content, query, limit2 ?? 10);
|
|
389584
|
+
},
|
|
389585
|
+
append: async (note) => {
|
|
389586
|
+
const profileId = settingsService.getSettings().agentSettings?.activeProfileId || null;
|
|
389587
|
+
const { content } = await memoryService.getMemorySnapshot(profileId);
|
|
389588
|
+
const next = appendMemoryNote(content, note);
|
|
389589
|
+
const snapshot = await memoryService.writeMemory(next, profileId);
|
|
389590
|
+
gatewayService.broadcastRaw("memory:updated", snapshot);
|
|
389591
|
+
return snapshot;
|
|
389084
389592
|
}
|
|
389085
389593
|
},
|
|
389086
389594
|
agentSettingsBridge: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "rterm-backend — the headless AI-native backend for RTerm (dual-published as neuralOS). run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
|
+
"description": "rterm-backend — the headless AI-native backend for RTerm (dual-published as neuralOS). run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory, APM/DEM/Infra/ETW ingestion, AgentSpan durable-agent bridge). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v3.0.5: terminal core (SSH auto-reconnect, WinRM persistent+streaming, serial break), chat user-message nav, reconnecting indicator, memory search/cap.",
|
|
5
5
|
"main": "bin/gybackend.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"gybackend": "bin/gybackend.cjs",
|