@rynx-ai/daemon 0.1.0 → 0.1.10-beta.2
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/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- package/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +478 -30
- package/dist/control-client.d.ts +122 -0
- package/dist/control-client.js +855 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +29 -2
- package/dist/daemon-server.js +1113 -26
- package/dist/db.js +196 -10
- package/dist/desktop-browser-host-client.d.ts +35 -0
- package/dist/desktop-browser-host-client.js +415 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +132 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2173 -0
- package/dist/index-daemon.js +55 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +71 -15
- package/dist/plugin-installer.js +521 -155
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +61 -0
- package/dist/plugin-package.js +732 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +92 -18
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +62 -0
- package/dist/remote-runtime-access-store.js +393 -0
- package/dist/remote-runtime-admin.d.ts +35 -0
- package/dist/remote-runtime-admin.js +110 -0
- package/dist/remote-runtime-connection-manager.d.ts +103 -0
- package/dist/remote-runtime-connection-manager.js +1575 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +51 -0
- package/dist/remote-runtime-target-control.js +488 -0
- package/dist/remote-runtime-target-store.d.ts +56 -0
- package/dist/remote-runtime-target-store.js +326 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +59 -7
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +144 -5
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +23 -10
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { delimiter, join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { getRuntimeProfile } from "@rynx-ai/core";
|
|
8
|
+
const STATUS_COMMAND_TIMEOUT_MS = 8_000;
|
|
9
|
+
const OPERATION_TIMEOUT_MS = 10 * 60_000;
|
|
10
|
+
const COMMAND_TERMINATION_GRACE_MS = 2_000;
|
|
11
|
+
const OUTPUT_MAX_BYTES = 16 * 1024;
|
|
12
|
+
const LATEST_CACHE_MS = 15 * 60_000;
|
|
13
|
+
const TRAEX_INSTALLER_MAX_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const TRAEX_INSTALLER_URL = "https://tosv-myabc.byted.org/obj/trae-common-2-asiasebd/traex/install/install_all_platforms.sh";
|
|
15
|
+
const PROVIDER_PACKAGES = {
|
|
16
|
+
codex: "@openai/codex",
|
|
17
|
+
claude: "@anthropic-ai/claude-code",
|
|
18
|
+
};
|
|
19
|
+
const PROVIDER_CLI_IDS = ["codex", "claude", "traex"];
|
|
20
|
+
export class ProviderCliService {
|
|
21
|
+
env;
|
|
22
|
+
platform;
|
|
23
|
+
now;
|
|
24
|
+
runCommand;
|
|
25
|
+
resolveCommand;
|
|
26
|
+
download;
|
|
27
|
+
latestCache = new Map();
|
|
28
|
+
jobs = new Map();
|
|
29
|
+
activeJobs = new Map();
|
|
30
|
+
constructor(deps = {}) {
|
|
31
|
+
this.env = { ...process.env, ...deps.env };
|
|
32
|
+
this.platform = deps.platform ?? process.platform;
|
|
33
|
+
this.now = deps.now ?? (() => new Date());
|
|
34
|
+
this.runCommand = deps.run ?? runCommand;
|
|
35
|
+
this.resolveCommand = deps.resolveExecutable ?? resolveExecutable;
|
|
36
|
+
this.download = deps.download ?? downloadBounded;
|
|
37
|
+
}
|
|
38
|
+
async listStatuses() {
|
|
39
|
+
const providerClis = await Promise.all(PROVIDER_CLI_IDS.map((id) => this.inspect(id)));
|
|
40
|
+
return { providerClis };
|
|
41
|
+
}
|
|
42
|
+
async startOperation(provider, operation) {
|
|
43
|
+
const activeId = this.activeJobs.get(provider);
|
|
44
|
+
if (activeId) {
|
|
45
|
+
const active = this.jobs.get(activeId);
|
|
46
|
+
if (active?.state === "running") {
|
|
47
|
+
if (active.operation === operation)
|
|
48
|
+
return { ...active };
|
|
49
|
+
throw new ProviderCliOperationError("failed_precondition", `${provider} already has a ${active.operation} operation in progress`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const status = await this.inspect(provider, operation === "update");
|
|
53
|
+
if (operation === "install" && status.availability === "installed") {
|
|
54
|
+
throw new ProviderCliOperationError("failed_precondition", `${status.name} is already installed`);
|
|
55
|
+
}
|
|
56
|
+
if (operation === "update" && status.availability !== "installed") {
|
|
57
|
+
throw new ProviderCliOperationError("failed_precondition", `${status.name} is not installed`);
|
|
58
|
+
}
|
|
59
|
+
if (operation === "install" && !status.canInstall) {
|
|
60
|
+
throw new ProviderCliOperationError("failed_precondition", `${status.name} cannot be installed automatically on this machine`);
|
|
61
|
+
}
|
|
62
|
+
if (operation === "update" && !status.canUpdate) {
|
|
63
|
+
throw new ProviderCliOperationError("failed_precondition", `${status.name} cannot be updated automatically on this machine`);
|
|
64
|
+
}
|
|
65
|
+
const job = {
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
provider,
|
|
68
|
+
operation,
|
|
69
|
+
state: "running",
|
|
70
|
+
stage: "starting",
|
|
71
|
+
startedAt: this.now().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
this.jobs.set(job.id, job);
|
|
74
|
+
this.activeJobs.set(provider, job.id);
|
|
75
|
+
this.pruneJobs();
|
|
76
|
+
void this.execute(job.id, status).catch(() => undefined);
|
|
77
|
+
return { ...job };
|
|
78
|
+
}
|
|
79
|
+
getJob(id) {
|
|
80
|
+
const job = this.jobs.get(id);
|
|
81
|
+
return job ? { ...job } : undefined;
|
|
82
|
+
}
|
|
83
|
+
async inspect(provider, includeLatest = true) {
|
|
84
|
+
const profile = getRuntimeProfile(provider);
|
|
85
|
+
const checkedAt = this.now().toISOString();
|
|
86
|
+
const executablePath = await this.resolveProviderExecutable(provider);
|
|
87
|
+
const npmPath = provider === "traex"
|
|
88
|
+
? null
|
|
89
|
+
: await this.resolveCommand("npm", this.env, this.platform);
|
|
90
|
+
const canInstall = provider === "traex"
|
|
91
|
+
? this.platform !== "win32"
|
|
92
|
+
: npmPath !== null && this.platform !== "win32";
|
|
93
|
+
if (!executablePath) {
|
|
94
|
+
const latestVersion = includeLatest
|
|
95
|
+
? await this.latestVersion(provider, undefined, npmPath)
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
id: provider,
|
|
99
|
+
name: profile.displayName,
|
|
100
|
+
binary: profile.defaultBinary,
|
|
101
|
+
availability: "missing",
|
|
102
|
+
...(latestVersion ? { latestVersion } : {}),
|
|
103
|
+
updateState: "unknown",
|
|
104
|
+
authState: "unknown",
|
|
105
|
+
canInstall,
|
|
106
|
+
canUpdate: false,
|
|
107
|
+
checkedAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
let currentVersion;
|
|
111
|
+
let versionError;
|
|
112
|
+
try {
|
|
113
|
+
const result = await this.runCommand(executablePath, ["--version"], {
|
|
114
|
+
timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
|
|
115
|
+
env: this.env,
|
|
116
|
+
});
|
|
117
|
+
if (result.exitCode !== 0)
|
|
118
|
+
throw new Error(commandFailure(result));
|
|
119
|
+
currentVersion = extractVersion(`${result.stdout}\n${result.stderr}`);
|
|
120
|
+
if (!currentVersion)
|
|
121
|
+
throw new Error("version output was not recognized");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
versionError = errorMessage(error);
|
|
125
|
+
}
|
|
126
|
+
const [auth, latestVersion] = await Promise.all([
|
|
127
|
+
currentVersion
|
|
128
|
+
? this.authStatus(provider, executablePath)
|
|
129
|
+
: Promise.resolve({ state: "unknown" }),
|
|
130
|
+
includeLatest
|
|
131
|
+
? this.latestVersion(provider, executablePath, npmPath)
|
|
132
|
+
: Promise.resolve(undefined),
|
|
133
|
+
]);
|
|
134
|
+
const updateState = currentVersion && latestVersion
|
|
135
|
+
? (compareVersions(currentVersion, latestVersion) < 0 ? "available" : "current")
|
|
136
|
+
: "unknown";
|
|
137
|
+
return {
|
|
138
|
+
id: provider,
|
|
139
|
+
name: profile.displayName,
|
|
140
|
+
binary: profile.defaultBinary,
|
|
141
|
+
executablePath,
|
|
142
|
+
availability: currentVersion ? "installed" : "broken",
|
|
143
|
+
...(currentVersion ? { currentVersion } : {}),
|
|
144
|
+
...(latestVersion ? { latestVersion } : {}),
|
|
145
|
+
updateState,
|
|
146
|
+
authState: auth.state,
|
|
147
|
+
...(auth.mode ? { authMode: auth.mode } : {}),
|
|
148
|
+
canInstall: !currentVersion && canInstall,
|
|
149
|
+
canUpdate: Boolean(currentVersion) && this.platform !== "win32",
|
|
150
|
+
checkedAt,
|
|
151
|
+
...(versionError ? { error: versionError } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async resolveProviderExecutable(provider) {
|
|
155
|
+
const candidates = provider === "traex" ? ["traex", "traecli"] : [provider];
|
|
156
|
+
for (const candidate of candidates) {
|
|
157
|
+
const resolved = await this.resolveCommand(candidate, this.env, this.platform);
|
|
158
|
+
if (resolved)
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
async authStatus(provider, executablePath) {
|
|
164
|
+
try {
|
|
165
|
+
const args = provider === "claude" ? ["auth", "status", "--json"] : ["login", "status"];
|
|
166
|
+
const result = await this.runCommand(executablePath, args, {
|
|
167
|
+
timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
|
|
168
|
+
env: this.env,
|
|
169
|
+
});
|
|
170
|
+
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
171
|
+
if (provider === "claude") {
|
|
172
|
+
const parsed = JSON.parse(output);
|
|
173
|
+
if (parsed.loggedIn === true) {
|
|
174
|
+
const mode = [parsed.authMethod, parsed.apiProvider]
|
|
175
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
176
|
+
.join(" · ");
|
|
177
|
+
return { state: "ready", ...(mode ? { mode } : {}) };
|
|
178
|
+
}
|
|
179
|
+
if (parsed.loggedIn === false)
|
|
180
|
+
return { state: "required" };
|
|
181
|
+
return { state: "unknown" };
|
|
182
|
+
}
|
|
183
|
+
if (result.exitCode === 0 && /logged in/i.test(output)) {
|
|
184
|
+
const match = /logged in(?: using| as)?\s+(.+)/i.exec(output);
|
|
185
|
+
return { state: "ready", ...(match?.[1]?.trim() ? { mode: match[1].trim() } : {}) };
|
|
186
|
+
}
|
|
187
|
+
if (/not logged in|logged out|login required/i.test(output))
|
|
188
|
+
return { state: "required" };
|
|
189
|
+
return { state: result.exitCode === 0 ? "unknown" : "required" };
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return { state: "unknown" };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async latestVersion(provider, executablePath, npmPath) {
|
|
196
|
+
const now = this.now().getTime();
|
|
197
|
+
const cached = this.latestCache.get(provider);
|
|
198
|
+
if (cached && cached.expiresAt > now)
|
|
199
|
+
return cached.value;
|
|
200
|
+
let value;
|
|
201
|
+
try {
|
|
202
|
+
if (provider === "traex") {
|
|
203
|
+
if (!executablePath)
|
|
204
|
+
return undefined;
|
|
205
|
+
const result = await this.runCommand(executablePath, ["update", "check"], {
|
|
206
|
+
timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
|
|
207
|
+
env: this.env,
|
|
208
|
+
});
|
|
209
|
+
if (result.exitCode === 0) {
|
|
210
|
+
value = /Latest TRAE CLI version:\s*([^\s]+)/i.exec(`${result.stdout}\n${result.stderr}`)?.[1];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (npmPath) {
|
|
214
|
+
const packageName = PROVIDER_PACKAGES[provider];
|
|
215
|
+
const result = await this.runCommand(npmPath, ["view", `${packageName}@latest`, "version", "--json"], { timeoutMs: STATUS_COMMAND_TIMEOUT_MS, env: this.env });
|
|
216
|
+
if (result.exitCode === 0)
|
|
217
|
+
value = extractVersion(result.stdout);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
value = undefined;
|
|
222
|
+
}
|
|
223
|
+
this.latestCache.set(provider, { value, expiresAt: now + LATEST_CACHE_MS });
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
async execute(jobId, previousStatus) {
|
|
227
|
+
const job = this.jobs.get(jobId);
|
|
228
|
+
if (!job)
|
|
229
|
+
return;
|
|
230
|
+
try {
|
|
231
|
+
job.stage = job.operation === "install" ? "installing" : "updating";
|
|
232
|
+
if (job.operation === "install") {
|
|
233
|
+
await this.install(job.provider);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
await this.update(job.provider, previousStatus.executablePath);
|
|
237
|
+
}
|
|
238
|
+
job.stage = "verifying";
|
|
239
|
+
this.latestCache.delete(job.provider);
|
|
240
|
+
const verified = await this.inspect(job.provider, false);
|
|
241
|
+
if (verified.availability !== "installed") {
|
|
242
|
+
throw new Error(verified.error ?? `${verified.name} was not found on the daemon PATH after the operation`);
|
|
243
|
+
}
|
|
244
|
+
if (job.operation === "update") {
|
|
245
|
+
const previousVersion = previousStatus.currentVersion;
|
|
246
|
+
const verifiedVersion = verified.currentVersion;
|
|
247
|
+
if (!previousVersion || !verifiedVersion || compareVersions(verifiedVersion, previousVersion) <= 0) {
|
|
248
|
+
throw new Error(`${verified.name} reported success but its version did not change`);
|
|
249
|
+
}
|
|
250
|
+
if (previousStatus.latestVersion
|
|
251
|
+
&& compareVersions(verifiedVersion, previousStatus.latestVersion) < 0) {
|
|
252
|
+
throw new Error(`${verified.name} updated to ${verifiedVersion}, below the expected ${previousStatus.latestVersion}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
job.stage = "complete";
|
|
256
|
+
job.state = "succeeded";
|
|
257
|
+
job.finishedAt = this.now().toISOString();
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
job.state = "failed";
|
|
261
|
+
job.finishedAt = this.now().toISOString();
|
|
262
|
+
job.error = boundError(errorMessage(error));
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
if (this.activeJobs.get(job.provider) === job.id)
|
|
266
|
+
this.activeJobs.delete(job.provider);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async install(provider) {
|
|
270
|
+
if (provider === "traex") {
|
|
271
|
+
await this.installTraex();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const npmPath = await this.resolveCommand("npm", this.env, this.platform);
|
|
275
|
+
if (!npmPath)
|
|
276
|
+
throw new Error("npm is not available on PATH");
|
|
277
|
+
const result = await this.runCommand(npmPath, ["install", "--global", `${PROVIDER_PACKAGES[provider]}@latest`, "--no-audit", "--no-fund"], { timeoutMs: OPERATION_TIMEOUT_MS, env: this.env });
|
|
278
|
+
if (result.exitCode !== 0)
|
|
279
|
+
throw new Error(commandFailure(result));
|
|
280
|
+
}
|
|
281
|
+
async update(provider, executablePath) {
|
|
282
|
+
const result = await this.runCommand(executablePath, ["update"], {
|
|
283
|
+
timeoutMs: OPERATION_TIMEOUT_MS,
|
|
284
|
+
env: this.env,
|
|
285
|
+
});
|
|
286
|
+
if (result.exitCode !== 0)
|
|
287
|
+
throw new Error(commandFailure(result));
|
|
288
|
+
}
|
|
289
|
+
async installTraex() {
|
|
290
|
+
if (this.platform === "win32")
|
|
291
|
+
throw new Error("automatic Traex installation is not supported on Windows");
|
|
292
|
+
const script = await this.download(TRAEX_INSTALLER_URL, TRAEX_INSTALLER_MAX_BYTES);
|
|
293
|
+
const directory = await mkdtemp(join(tmpdir(), "rynx-traex-install-"));
|
|
294
|
+
const scriptPath = join(directory, "install.sh");
|
|
295
|
+
try {
|
|
296
|
+
await writeFile(scriptPath, script, { mode: 0o700 });
|
|
297
|
+
const shPath = await this.resolveCommand("sh", this.env, this.platform);
|
|
298
|
+
if (!shPath)
|
|
299
|
+
throw new Error("sh is not available on PATH");
|
|
300
|
+
const result = await this.runCommand(shPath, [scriptPath], {
|
|
301
|
+
timeoutMs: OPERATION_TIMEOUT_MS,
|
|
302
|
+
env: installerEnvironment(this.env, {
|
|
303
|
+
TRAEX_INSTALL_ASSUME_YES: "1",
|
|
304
|
+
TRAEX_INSTALL_MIGRATE_BREW: "0",
|
|
305
|
+
TRAEX_INSTALL_REMOVE_BREW: "0",
|
|
306
|
+
TRAEX_INSTALL_CHANNEL: "stable",
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
if (result.exitCode !== 0)
|
|
310
|
+
throw new Error(commandFailure(result));
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
await rm(directory, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
pruneJobs() {
|
|
317
|
+
if (this.jobs.size <= 50)
|
|
318
|
+
return;
|
|
319
|
+
const finished = [...this.jobs.values()]
|
|
320
|
+
.filter((job) => job.state !== "running")
|
|
321
|
+
.sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
322
|
+
for (const job of finished.slice(0, this.jobs.size - 50))
|
|
323
|
+
this.jobs.delete(job.id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export class ProviderCliOperationError extends Error {
|
|
327
|
+
code;
|
|
328
|
+
constructor(code, message) {
|
|
329
|
+
super(message);
|
|
330
|
+
this.code = code;
|
|
331
|
+
this.name = "ProviderCliOperationError";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function resolveExecutable(binary, env, platform) {
|
|
335
|
+
const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
|
|
336
|
+
const extensions = platform === "win32"
|
|
337
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
338
|
+
: [""];
|
|
339
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
340
|
+
for (const extension of extensions) {
|
|
341
|
+
const candidate = join(directory, platform === "win32" ? `${binary}${extension}` : binary);
|
|
342
|
+
try {
|
|
343
|
+
await access(candidate, fsConstants.X_OK);
|
|
344
|
+
return candidate;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Continue through the daemon's effective PATH.
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function runCommand(executable, args, options) {
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
const detached = process.platform !== "win32";
|
|
356
|
+
const child = spawn(executable, args, {
|
|
357
|
+
env: options.env,
|
|
358
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
359
|
+
windowsHide: true,
|
|
360
|
+
detached,
|
|
361
|
+
});
|
|
362
|
+
let timedOut = false;
|
|
363
|
+
let settled = false;
|
|
364
|
+
let forceTimer;
|
|
365
|
+
let stdout = Buffer.alloc(0);
|
|
366
|
+
let stderr = Buffer.alloc(0);
|
|
367
|
+
const append = (current, chunk) => Buffer.concat([current, chunk]).subarray(0, OUTPUT_MAX_BYTES);
|
|
368
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
369
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
370
|
+
const clearTimers = () => {
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
if (forceTimer)
|
|
373
|
+
clearTimeout(forceTimer);
|
|
374
|
+
};
|
|
375
|
+
const rejectOnce = (error) => {
|
|
376
|
+
if (settled)
|
|
377
|
+
return;
|
|
378
|
+
settled = true;
|
|
379
|
+
clearTimers();
|
|
380
|
+
reject(error);
|
|
381
|
+
};
|
|
382
|
+
const timer = setTimeout(() => {
|
|
383
|
+
timedOut = true;
|
|
384
|
+
terminateCommand(child.pid, child, "SIGTERM", detached);
|
|
385
|
+
forceTimer = setTimeout(() => {
|
|
386
|
+
terminateCommand(child.pid, child, "SIGKILL", detached);
|
|
387
|
+
rejectOnce(new Error(`command timed out after ${options.timeoutMs}ms`));
|
|
388
|
+
}, COMMAND_TERMINATION_GRACE_MS);
|
|
389
|
+
forceTimer.unref();
|
|
390
|
+
}, options.timeoutMs);
|
|
391
|
+
timer.unref();
|
|
392
|
+
child.once("error", (error) => {
|
|
393
|
+
if (!timedOut)
|
|
394
|
+
rejectOnce(error);
|
|
395
|
+
});
|
|
396
|
+
child.once("close", (code) => {
|
|
397
|
+
if (settled || timedOut)
|
|
398
|
+
return;
|
|
399
|
+
settled = true;
|
|
400
|
+
clearTimers();
|
|
401
|
+
resolve({
|
|
402
|
+
exitCode: code ?? -1,
|
|
403
|
+
stdout: stdout.toString("utf8"),
|
|
404
|
+
stderr: stderr.toString("utf8"),
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async function downloadBounded(url, maximumBytes) {
|
|
410
|
+
const requestedUrl = new URL(url);
|
|
411
|
+
if (requestedUrl.protocol !== "https:")
|
|
412
|
+
throw new Error("installer URL must use HTTPS");
|
|
413
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
414
|
+
if (!response.ok)
|
|
415
|
+
throw new Error(`installer download failed with HTTP ${response.status}`);
|
|
416
|
+
const finalUrl = new URL(response.url || url);
|
|
417
|
+
if (finalUrl.protocol !== "https:" || finalUrl.origin !== requestedUrl.origin) {
|
|
418
|
+
throw new Error("installer download redirected outside the trusted origin");
|
|
419
|
+
}
|
|
420
|
+
const expectedMd5 = response.headers.get("content-md5");
|
|
421
|
+
if (!expectedMd5)
|
|
422
|
+
throw new Error("installer response did not include integrity metadata");
|
|
423
|
+
const declaredLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
424
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
425
|
+
throw new Error("installer download exceeded the accepted size");
|
|
426
|
+
}
|
|
427
|
+
if (!response.body)
|
|
428
|
+
throw new Error("installer download returned an empty response");
|
|
429
|
+
const reader = response.body.getReader();
|
|
430
|
+
const chunks = [];
|
|
431
|
+
let length = 0;
|
|
432
|
+
for (;;) {
|
|
433
|
+
const { value, done } = await reader.read();
|
|
434
|
+
if (done)
|
|
435
|
+
break;
|
|
436
|
+
if (!value)
|
|
437
|
+
continue;
|
|
438
|
+
length += value.byteLength;
|
|
439
|
+
if (length > maximumBytes) {
|
|
440
|
+
await reader.cancel();
|
|
441
|
+
throw new Error("installer download exceeded the accepted size");
|
|
442
|
+
}
|
|
443
|
+
chunks.push(value);
|
|
444
|
+
}
|
|
445
|
+
if (length === 0)
|
|
446
|
+
throw new Error("installer download returned an empty response");
|
|
447
|
+
const bytes = new Uint8Array(length);
|
|
448
|
+
let offset = 0;
|
|
449
|
+
for (const chunk of chunks) {
|
|
450
|
+
bytes.set(chunk, offset);
|
|
451
|
+
offset += chunk.byteLength;
|
|
452
|
+
}
|
|
453
|
+
const actualMd5 = createHash("md5").update(bytes).digest("base64");
|
|
454
|
+
if (actualMd5 !== expectedMd5)
|
|
455
|
+
throw new Error("installer response failed its integrity check");
|
|
456
|
+
return bytes;
|
|
457
|
+
}
|
|
458
|
+
function terminateCommand(pid, child, signal, detached) {
|
|
459
|
+
try {
|
|
460
|
+
if (detached && pid)
|
|
461
|
+
process.kill(-pid, signal);
|
|
462
|
+
else
|
|
463
|
+
child.kill(signal);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// The process may have exited between the timeout and signal delivery.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function installerEnvironment(env, additions) {
|
|
470
|
+
const allowed = [
|
|
471
|
+
"PATH", "HOME", "USER", "SHELL", "TMPDIR",
|
|
472
|
+
"LANG", "LC_ALL",
|
|
473
|
+
"XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME",
|
|
474
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
475
|
+
"http_proxy", "https_proxy", "no_proxy",
|
|
476
|
+
"SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
477
|
+
];
|
|
478
|
+
const result = { ...additions };
|
|
479
|
+
for (const key of allowed) {
|
|
480
|
+
if (env[key] !== undefined)
|
|
481
|
+
result[key] = env[key];
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
function extractVersion(output) {
|
|
486
|
+
return /\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/.exec(output)?.[0];
|
|
487
|
+
}
|
|
488
|
+
function compareVersions(left, right) {
|
|
489
|
+
const a = left.split(/[.+-]/).slice(0, 3).map((value) => Number.parseInt(value, 10));
|
|
490
|
+
const b = right.split(/[.+-]/).slice(0, 3).map((value) => Number.parseInt(value, 10));
|
|
491
|
+
for (let index = 0; index < 3; index += 1) {
|
|
492
|
+
const delta = (a[index] ?? 0) - (b[index] ?? 0);
|
|
493
|
+
if (delta !== 0)
|
|
494
|
+
return delta;
|
|
495
|
+
}
|
|
496
|
+
const leftPrerelease = left.includes("-");
|
|
497
|
+
const rightPrerelease = right.includes("-");
|
|
498
|
+
if (leftPrerelease !== rightPrerelease)
|
|
499
|
+
return leftPrerelease ? -1 : 1;
|
|
500
|
+
return left.localeCompare(right);
|
|
501
|
+
}
|
|
502
|
+
function commandFailure(result) {
|
|
503
|
+
return boundError(result.stderr.trim() || result.stdout.trim() || `command exited ${result.exitCode}`);
|
|
504
|
+
}
|
|
505
|
+
function errorMessage(error) {
|
|
506
|
+
return error instanceof Error ? error.message : String(error);
|
|
507
|
+
}
|
|
508
|
+
function boundError(message) {
|
|
509
|
+
return message.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 1024) || "operation failed";
|
|
510
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
import { type ChannelInstanceDescriptor, type ChannelTypeDescriptor
|
|
2
|
-
import {
|
|
3
|
-
/** Directory
|
|
1
|
+
import { type ChannelInstanceDescriptor, type ChannelTypeDescriptor } from "@rynx-ai/core";
|
|
2
|
+
import type { PluginSupervisor } from "./plugin-supervisor.js";
|
|
3
|
+
/** Directory plugin packages and staging data live under. */
|
|
4
4
|
export declare function pluginsDir(): string;
|
|
5
|
-
/** All channel types currently available (from loaded plugins). */
|
|
6
5
|
export declare function listChannelTypes(): ChannelTypeDescriptor[];
|
|
7
6
|
export declare function getChannelType(type: string): ChannelTypeDescriptor | undefined;
|
|
8
|
-
/**
|
|
9
|
-
export declare function
|
|
10
|
-
/**
|
|
11
|
-
export declare function importPluginManifest(pkgDir: string): Promise<RynxPlugin | null>;
|
|
12
|
-
/**
|
|
13
|
-
* Rebuild the channel-type catalog from the enabled plugins in the registry.
|
|
14
|
-
* Call at boot and after an install/enable/disable change. Failures to load a
|
|
15
|
-
* single plugin are warned and skipped — one bad plugin can't break the rest.
|
|
16
|
-
*/
|
|
17
|
-
export declare function loadPlugins(warn?: (message: string) => void): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* Resolve every enabled instance (joined with its channel) to a mountable
|
|
20
|
-
* descriptor, binding each to the factory its channel `type` resolves to in the
|
|
21
|
-
* catalog. Unknown types (no plugin providing them) are skipped.
|
|
22
|
-
*/
|
|
7
|
+
/** Rebuild the catalog from static manifests projected by the supervisor. */
|
|
8
|
+
export declare function loadPlugins(supervisor: PluginSupervisor, warn?: (message: string) => void): Promise<void>;
|
|
9
|
+
/** Join enabled channel instances to their remote runner-backed factories. */
|
|
23
10
|
export declare function resolveChannelInstances(warn?: (message: string) => void): Promise<ChannelInstanceDescriptor[]>;
|
package/dist/registry.js
CHANGED
|
@@ -1,117 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Static plugin catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reads the catalog synchronously. The installer calls `loadPlugins()` after a
|
|
9
|
-
* change so a freshly-installed plugin's types appear without a restart.
|
|
4
|
+
* Installation records already contain validated pure-data manifests. This
|
|
5
|
+
* module joins their chat-channel declarations to host-side remote lifecycle
|
|
6
|
+
* adapters supplied by PluginSupervisor. It never imports an installed plugin
|
|
7
|
+
* entry point.
|
|
10
8
|
*/
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
9
|
import { join } from "node:path";
|
|
13
|
-
import { pathToFileURL } from "node:url";
|
|
14
10
|
import { rynxHome, } from "@rynx-ai/core";
|
|
15
11
|
import { listEnabledDescriptors } from "./instance-store.js";
|
|
16
|
-
|
|
17
|
-
/** Directory npm-installed plugins live under (override via `RYNX_PLUGINS_DIR`). */
|
|
12
|
+
/** Directory plugin packages and staging data live under. */
|
|
18
13
|
export function pluginsDir() {
|
|
19
14
|
return process.env.RYNX_PLUGINS_DIR?.trim() || join(rynxHome(), "plugins");
|
|
20
15
|
}
|
|
21
|
-
/** type → descriptor, rebuilt by {@link loadPlugins}. */
|
|
22
16
|
const catalog = new Map();
|
|
23
|
-
/** All channel types currently available (from loaded plugins). */
|
|
24
17
|
export function listChannelTypes() {
|
|
25
18
|
return [...catalog.values()];
|
|
26
19
|
}
|
|
27
20
|
export function getChannelType(type) {
|
|
28
21
|
return catalog.get(type);
|
|
29
22
|
}
|
|
30
|
-
/**
|
|
31
|
-
function
|
|
32
|
-
const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
|
|
33
|
-
let rel = pkg.rynx?.plugin;
|
|
34
|
-
if (!rel) {
|
|
35
|
-
const dot = pkg.exports && typeof pkg.exports === "object" && "." in pkg.exports
|
|
36
|
-
? pkg.exports["."]
|
|
37
|
-
: pkg.exports;
|
|
38
|
-
if (typeof dot === "string") {
|
|
39
|
-
rel = dot;
|
|
40
|
-
}
|
|
41
|
-
else if (dot && typeof dot === "object") {
|
|
42
|
-
const cond = dot;
|
|
43
|
-
rel = cond.import ?? cond.default ?? cond.require;
|
|
44
|
-
}
|
|
45
|
-
rel = rel ?? pkg.main ?? "index.js";
|
|
46
|
-
}
|
|
47
|
-
return pathToFileURL(join(pkgDir, rel)).href;
|
|
48
|
-
}
|
|
49
|
-
/** Absolute package directory a plugin record resolves to. */
|
|
50
|
-
export function pluginPkgDir(rec) {
|
|
51
|
-
// local: spec is an absolute directory; npm: resolve under the plugins prefix.
|
|
52
|
-
return rec.source === "local" ? rec.spec : join(pluginsDir(), "node_modules", ...rec.spec.split("/"));
|
|
53
|
-
}
|
|
54
|
-
/** Import a package directory's `plugin` manifest export (or null if absent). */
|
|
55
|
-
export async function importPluginManifest(pkgDir) {
|
|
56
|
-
const mod = (await import(entryUrl(pkgDir)));
|
|
57
|
-
const plugin = (mod.plugin ?? mod.default);
|
|
58
|
-
return plugin && typeof plugin === "object" ? plugin : null;
|
|
59
|
-
}
|
|
60
|
-
function importPlugin(rec) {
|
|
61
|
-
return importPluginManifest(pluginPkgDir(rec));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Rebuild the channel-type catalog from the enabled plugins in the registry.
|
|
65
|
-
* Call at boot and after an install/enable/disable change. Failures to load a
|
|
66
|
-
* single plugin are warned and skipped — one bad plugin can't break the rest.
|
|
67
|
-
*/
|
|
68
|
-
export async function loadPlugins(warn = () => { }) {
|
|
23
|
+
/** Rebuild the catalog from static manifests projected by the supervisor. */
|
|
24
|
+
export async function loadPlugins(supervisor, warn = () => { }) {
|
|
69
25
|
catalog.clear();
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
plugin = await importPlugin(rec);
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
warn(`plugin "${rec.name}" failed to load: ${messageOf(error)}`);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!plugin) {
|
|
80
|
-
warn(`plugin "${rec.name}" has no \`plugin\` export — skipped`);
|
|
26
|
+
for (const channel of supervisor.createChannelTypeDescriptors()) {
|
|
27
|
+
if (catalog.has(channel.type)) {
|
|
28
|
+
warn(`chat channel type "${channel.type}" is declared by more than one plugin; later declaration ignored`);
|
|
81
29
|
continue;
|
|
82
30
|
}
|
|
83
|
-
|
|
84
|
-
if (catalog.has(channel.type)) {
|
|
85
|
-
warn(`channel type "${channel.type}" already provided; "${rec.name}" ignored for it`);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
catalog.set(channel.type, channel);
|
|
89
|
-
}
|
|
31
|
+
catalog.set(channel.type, channel);
|
|
90
32
|
}
|
|
91
33
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Resolve every enabled instance (joined with its channel) to a mountable
|
|
94
|
-
* descriptor, binding each to the factory its channel `type` resolves to in the
|
|
95
|
-
* catalog. Unknown types (no plugin providing them) are skipped.
|
|
96
|
-
*/
|
|
34
|
+
/** Join enabled channel instances to their remote runner-backed factories. */
|
|
97
35
|
export async function resolveChannelInstances(warn = () => { }) {
|
|
98
36
|
const instances = [];
|
|
99
|
-
for (const
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
102
|
-
warn(`unknown channel type "${
|
|
37
|
+
for (const descriptor of listEnabledDescriptors()) {
|
|
38
|
+
const channel = catalog.get(descriptor.type);
|
|
39
|
+
if (!channel) {
|
|
40
|
+
warn(`unknown chat channel type "${descriptor.type}" (instance "${descriptor.instanceId}") — no enabled plugin declares it; skipped`);
|
|
103
41
|
continue;
|
|
104
42
|
}
|
|
105
43
|
instances.push({
|
|
106
|
-
instanceId:
|
|
107
|
-
type:
|
|
108
|
-
factory:
|
|
109
|
-
options:
|
|
110
|
-
agent:
|
|
44
|
+
instanceId: descriptor.instanceId,
|
|
45
|
+
type: descriptor.type,
|
|
46
|
+
factory: channel.factory,
|
|
47
|
+
options: descriptor.options,
|
|
48
|
+
agent: descriptor.agent,
|
|
111
49
|
});
|
|
112
50
|
}
|
|
113
51
|
return instances;
|
|
114
52
|
}
|
|
115
|
-
function messageOf(error) {
|
|
116
|
-
return error instanceof Error ? error.message : String(error);
|
|
117
|
-
}
|