@viraatdas/rudder 3.1.3 → 3.1.8
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/dist/cloud.js +81 -21
- package/dist/cloud.js.map +1 -1
- package/dist/native/darwin-arm64/rudder-native +0 -0
- package/dist/native/darwin-x64/rudder-native +0 -0
- package/dist/native/linux-x64/rudder-native +0 -0
- package/dist/predict.d.ts +57 -0
- package/dist/predict.js +342 -0
- package/dist/predict.js.map +1 -0
- package/package.json +2 -1
package/dist/cloud.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { EchoPredictor } from "./predict.js";
|
|
3
4
|
import fsp from "node:fs/promises";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
@@ -2769,7 +2770,7 @@ async function attachWithReconnect(target, options, reopen) {
|
|
|
2769
2770
|
let attempts = 0;
|
|
2770
2771
|
while (result === "dropped" && attempts < 120) {
|
|
2771
2772
|
attempts += 1;
|
|
2772
|
-
process.stderr.write(`\r\nLost the connection to Rudder Cloud (
|
|
2773
|
+
process.stderr.write(`\r\nLost the connection to Rudder Cloud (laptop slept or the network dropped; your agents kept running). Reconnecting${attempts > 1 ? ` (try ${attempts})` : ""}...\r\n`);
|
|
2773
2774
|
await sleep(Math.min(15_000, 2_000 * attempts));
|
|
2774
2775
|
try {
|
|
2775
2776
|
await reopen?.();
|
|
@@ -2828,12 +2829,19 @@ async function runAttach(target, options) {
|
|
|
2828
2829
|
let result = "failed";
|
|
2829
2830
|
const splashAllowed = isInteractive && !options.json && !options.quietBanner;
|
|
2830
2831
|
const splash = splashAllowed ? new AttachSplash(stdout, target.label) : null;
|
|
2832
|
+
// Predictive local echo (src/predict.ts): typed characters appear at the
|
|
2833
|
+
// caret at once and the cloud's frame confirms them. RUDDER_PREDICT=0
|
|
2834
|
+
// turns it off; it only engages once the round trip is worth hiding.
|
|
2835
|
+
const predictor = isInteractive && process.env.RUDDER_PREDICT !== "0"
|
|
2836
|
+
? new EchoPredictor(stdout.columns ?? 120, stdout.rows ?? 32, (text) => stdout.write(text))
|
|
2837
|
+
: null;
|
|
2831
2838
|
const sendResize = () => {
|
|
2832
2839
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
2833
2840
|
return;
|
|
2834
2841
|
}
|
|
2835
2842
|
const cols = stdout.columns ?? 120;
|
|
2836
2843
|
const rows = stdout.rows ?? 32;
|
|
2844
|
+
predictor?.resize(cols, rows);
|
|
2837
2845
|
socket.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
2838
2846
|
};
|
|
2839
2847
|
let lastCtrlC = 0;
|
|
@@ -2911,6 +2919,7 @@ async function runAttach(target, options) {
|
|
|
2911
2919
|
lastCtrlC = now;
|
|
2912
2920
|
}
|
|
2913
2921
|
socket.send(buffer, { binary: true });
|
|
2922
|
+
predictor?.onInput(buffer);
|
|
2914
2923
|
};
|
|
2915
2924
|
const onResize = () => {
|
|
2916
2925
|
if (hasControl) {
|
|
@@ -2925,7 +2934,14 @@ async function runAttach(target, options) {
|
|
|
2925
2934
|
// as a dropped connection, which attachWithReconnect reopens.
|
|
2926
2935
|
let lastHeard = Date.now();
|
|
2927
2936
|
let lastTick = Date.now();
|
|
2928
|
-
|
|
2937
|
+
let pingSentAt = 0;
|
|
2938
|
+
socket.on("pong", () => {
|
|
2939
|
+
lastHeard = Date.now();
|
|
2940
|
+
if (pingSentAt) {
|
|
2941
|
+
predictor?.noteRtt(Date.now() - pingSentAt);
|
|
2942
|
+
pingSentAt = 0;
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2929
2945
|
socket.on("message", () => { lastHeard = Date.now(); });
|
|
2930
2946
|
const liveness = setInterval(() => {
|
|
2931
2947
|
const now = Date.now();
|
|
@@ -2940,6 +2956,7 @@ async function runAttach(target, options) {
|
|
|
2940
2956
|
}
|
|
2941
2957
|
try {
|
|
2942
2958
|
socket.ping();
|
|
2959
|
+
pingSentAt = Date.now();
|
|
2943
2960
|
}
|
|
2944
2961
|
catch {
|
|
2945
2962
|
socket.terminate();
|
|
@@ -2952,6 +2969,7 @@ async function runAttach(target, options) {
|
|
|
2952
2969
|
}
|
|
2953
2970
|
cleaned = true;
|
|
2954
2971
|
clearInterval(liveness);
|
|
2972
|
+
predictor?.dispose();
|
|
2955
2973
|
stdin.off("data", onStdin);
|
|
2956
2974
|
stdout.off("resize", onResize);
|
|
2957
2975
|
process.off("SIGINT", onSigint);
|
|
@@ -3105,6 +3123,20 @@ async function runAttach(target, options) {
|
|
|
3105
3123
|
};
|
|
3106
3124
|
socket.on("open", () => {
|
|
3107
3125
|
opened = true;
|
|
3126
|
+
// Measure the round trip at once: predictive echo waits on it.
|
|
3127
|
+
try {
|
|
3128
|
+
socket.ping();
|
|
3129
|
+
pingSentAt = Date.now();
|
|
3130
|
+
}
|
|
3131
|
+
catch {
|
|
3132
|
+
// the liveness timer retries
|
|
3133
|
+
}
|
|
3134
|
+
if (probeMode) {
|
|
3135
|
+
// An idle dashboard sends nothing; a resize makes it repaint, which is
|
|
3136
|
+
// the first frame the probe waits for.
|
|
3137
|
+
socket.send(JSON.stringify({ type: "resize", cols: 119, rows: 32 }));
|
|
3138
|
+
setTimeout(() => socket.send(JSON.stringify({ type: "resize", cols: 120, rows: 32 })), 200);
|
|
3139
|
+
}
|
|
3108
3140
|
// Disable Nagle on the underlying TCP socket so single keystrokes don't
|
|
3109
3141
|
// sit in the send buffer waiting for piggyback ACKs (~40ms savings per
|
|
3110
3142
|
// keypress on the WAN).
|
|
@@ -3211,6 +3243,7 @@ async function runAttach(target, options) {
|
|
|
3211
3243
|
setTimeout(sendResize, 150);
|
|
3212
3244
|
}
|
|
3213
3245
|
stdout.write(data);
|
|
3246
|
+
predictor?.onServerData(data);
|
|
3214
3247
|
return;
|
|
3215
3248
|
}
|
|
3216
3249
|
const text = Buffer.isBuffer(data)
|
|
@@ -3662,16 +3695,40 @@ async function warnLocalOnlyWork(repoRoot) {
|
|
|
3662
3695
|
}
|
|
3663
3696
|
// The cloud's AWS is whatever ~/.aws the vault holds. When this laptop's copy
|
|
3664
3697
|
// has changed since (new keys, a new profile), offer to bring the vault level.
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3698
|
+
// The vault's copy of the laptop's agent and AWS config must match the
|
|
3699
|
+
// laptop: the cloud machine rewrites these files from the vault every boot,
|
|
3700
|
+
// and a drifted copy means different behavior there (verbose output, another
|
|
3701
|
+
// default model, hooks that no longer exist). Compared by checksum; AWS is
|
|
3702
|
+
// asked about once, everything else that is already shared just follows.
|
|
3703
|
+
const LAPTOP_CONFIG_FILES = [
|
|
3704
|
+
"~/.claude/settings.json",
|
|
3705
|
+
"~/.claude/CLAUDE.md",
|
|
3706
|
+
"~/.codex/config.toml",
|
|
3707
|
+
"~/.codex/AGENTS.md",
|
|
3708
|
+
"~/.codex/hooks.json",
|
|
3709
|
+
// Service logins agents use from scripts (Kalshi signs requests with this key).
|
|
3710
|
+
"~/.kalshi/config.json",
|
|
3711
|
+
"~/.kalshi/private_key.pem",
|
|
3712
|
+
];
|
|
3713
|
+
async function laptopConfigFiles() {
|
|
3714
|
+
const names = [...LAPTOP_CONFIG_FILES, "~/.aws/config", "~/.aws/credentials"];
|
|
3715
|
+
const hooksDir = path.join(os.homedir(), ".claude", "hooks");
|
|
3716
|
+
for (const entry of await fsp.readdir(hooksDir, { withFileTypes: true }).catch(() => [])) {
|
|
3717
|
+
if (entry.isFile()) {
|
|
3718
|
+
names.push(`~/.claude/hooks/${entry.name}`);
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
const out = [];
|
|
3722
|
+
for (const name of names) {
|
|
3723
|
+
const content = await fsp.readFile(path.join(os.homedir(), name.slice(2))).catch(() => null);
|
|
3671
3724
|
if (content && content.length > 0 && content.length <= MAX_SECRET_VALUE_BYTES) {
|
|
3672
|
-
|
|
3725
|
+
out.push({ name, content });
|
|
3673
3726
|
}
|
|
3674
3727
|
}
|
|
3728
|
+
return out;
|
|
3729
|
+
}
|
|
3730
|
+
async function ensureVaultAws(client) {
|
|
3731
|
+
const present = await laptopConfigFiles();
|
|
3675
3732
|
if (present.length === 0 || !isTty()) {
|
|
3676
3733
|
return;
|
|
3677
3734
|
}
|
|
@@ -3679,28 +3736,31 @@ async function ensureVaultAws(client) {
|
|
|
3679
3736
|
if (!vault) {
|
|
3680
3737
|
return;
|
|
3681
3738
|
}
|
|
3682
|
-
const stored = new Map((vault.secrets ?? []).map((secret) => [secret.name, secret.
|
|
3683
|
-
const
|
|
3739
|
+
const stored = new Map((vault.secrets ?? []).map((secret) => [secret.name, secret.sha256 ?? ""]));
|
|
3740
|
+
const sha = (content) => createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
3741
|
+
const changed = present.filter((file) => stored.get(file.name) !== sha(file.content));
|
|
3684
3742
|
if (changed.length === 0) {
|
|
3685
3743
|
return;
|
|
3686
3744
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
const
|
|
3691
|
-
if (
|
|
3692
|
-
const yes = await promptConfirm(`
|
|
3693
|
-
if (
|
|
3694
|
-
|
|
3745
|
+
const isAws = (name) => name.startsWith("~/.aws/");
|
|
3746
|
+
const awsShared = [...stored.keys()].some(isAws);
|
|
3747
|
+
let toSend = changed.filter((file) => !isAws(file.name) || awsShared);
|
|
3748
|
+
const newAws = changed.filter((file) => isAws(file.name) && !awsShared);
|
|
3749
|
+
if (newAws.length > 0) {
|
|
3750
|
+
const yes = await promptConfirm(`Copy your AWS setup (${newAws.map((file) => file.name).join(", ")}) to the cloud vault so cloud agents use the same AWS?`, true);
|
|
3751
|
+
if (yes) {
|
|
3752
|
+
toSend = [...toSend, ...newAws];
|
|
3695
3753
|
}
|
|
3696
3754
|
}
|
|
3697
|
-
for (const file of
|
|
3755
|
+
for (const file of toSend) {
|
|
3698
3756
|
await client.request("/api/rudder/secrets/item", {
|
|
3699
3757
|
method: "PUT",
|
|
3700
3758
|
body: { name: file.name, kind: "file", filePath: file.name, valueBase64: file.content.toString("base64"), source: "rudder cloud" },
|
|
3701
3759
|
});
|
|
3702
3760
|
}
|
|
3703
|
-
|
|
3761
|
+
if (toSend.length > 0) {
|
|
3762
|
+
process.stderr.write(`Updated the cloud copy of ${toSend.map((file) => file.name).join(", ")} to match this laptop.\n`);
|
|
3763
|
+
}
|
|
3704
3764
|
}
|
|
3705
3765
|
async function localGithubToken() {
|
|
3706
3766
|
if (!commandExists("gh")) {
|