@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21
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/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +20 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/index.html +2 -2
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +22 -11
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +194 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +37 -6
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +87 -4
- package/dist/control-link-store.js +4 -3
- package/dist/daemon-owner.js +4 -7
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +158 -45
- package/dist/db.d.ts +2 -2
- package/dist/db.js +55 -20
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -87
- package/dist/lifecycle.d.ts +13 -0
- package/dist/lifecycle.js +19 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/migrations/cleanup-legacy-sessions.js +4 -3
- package/dist/plugin-cli.d.ts +10 -0
- package/dist/plugin-cli.js +19 -2
- package/dist/plugin-host-rpc.d.ts +35 -6
- package/dist/plugin-host-rpc.js +222 -59
- package/dist/plugin-installer.js +62 -2
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-marketplace.js +2 -1
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.d.ts +5 -0
- package/dist/plugin-supervisor.js +218 -33
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +116 -19
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-target-store.d.ts +2 -2
- package/dist/remote-runtime-target-store.js +6 -6
- package/dist/session-emulator-binding-store.js +4 -6
- package/dist/session-fork-store.js +24 -8
- package/dist/session-launch-operations.d.ts +10 -1
- package/dist/session-launch-operations.js +21 -11
- package/dist/session-log-store.js +2 -1
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +25 -2
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +85 -0
- package/dist/session-resource-store.js +3 -2
- package/dist/setup-service.d.ts +21 -0
- package/dist/setup-service.js +39 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -10
- package/dist/sqlite.d.ts +22 -0
- package/dist/sqlite.js +57 -0
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +37 -21
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -550
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -386
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/skills-catalog.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
* env-build share one implementation; this module is the daemon-facing wrapper
|
|
5
5
|
* plus the read-only detail/file helpers the Skills page uses.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
8
10
|
import path from "node:path";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
11
|
+
import extractZipArchive from "extract-zip";
|
|
12
|
+
import { extractSkillFrontmatter, assertSkillPathComponent, installIntoCatalog, collectLocalSkillBundlesInWorker, collectSkillBundlesFromDirectory, scanLocalSkillCandidatesInWorker, rynxSkillsDir, rynxSkillArtifactDir, rynxSkillArtifactsDir, scanSkillsDir, readSkillMeta, writeSkillInstallInfo, } from "@rynx-ai/core";
|
|
12
13
|
/** A single file's contents are capped to keep responses small. */
|
|
13
14
|
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
15
|
+
const MAX_ARCHIVE_ENTRIES = 1_000;
|
|
16
|
+
const MAX_ARCHIVE_EXTRACTED_BYTES = 24 * 1024 * 1024;
|
|
17
|
+
/** Concurrent Control API requests for the same Skill are serialized so the
|
|
18
|
+
* catalog's backup/swap transaction cannot interleave. */
|
|
19
|
+
const localRegistrationTails = new Map();
|
|
14
20
|
export function listCatalogSkills() {
|
|
15
21
|
return listMergedSkills();
|
|
16
22
|
}
|
|
@@ -22,10 +28,91 @@ export async function installCatalogSkill(recipe) {
|
|
|
22
28
|
await installIntoCatalog(recipe, rynxSkillsDir());
|
|
23
29
|
return listMergedSkills();
|
|
24
30
|
}
|
|
31
|
+
export function scanLocalCatalogSkillCandidates(runtime) {
|
|
32
|
+
return scanLocalSkillCandidatesInWorker(runtime);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register complete local runtime Skill bundles into the owner catalog. Every
|
|
36
|
+
* content hash gets a durable immutable artifact; the mutable catalog entry's
|
|
37
|
+
* receipt points at that artifact so old Agent specs keep their exact bytes
|
|
38
|
+
* after a later catalog update.
|
|
39
|
+
*/
|
|
40
|
+
export async function importLocalCatalogSkills(runtime, keys) {
|
|
41
|
+
const collected = await collectLocalSkillBundlesInWorker(runtime, keys);
|
|
42
|
+
return registerCollectedBundles(collected);
|
|
43
|
+
}
|
|
44
|
+
export async function importCatalogSkillsFromArchive(filename, contentBase64) {
|
|
45
|
+
if (!filename.toLowerCase().endsWith(".zip"))
|
|
46
|
+
throw new Error("archive must be a .zip file");
|
|
47
|
+
const content = Buffer.from(contentBase64, "base64");
|
|
48
|
+
if (content.byteLength === 0)
|
|
49
|
+
throw new Error("archive is empty");
|
|
50
|
+
if (content.byteLength > 20 * 1024 * 1024)
|
|
51
|
+
throw new Error("archive exceeds the 20 MB limit");
|
|
52
|
+
const staging = await mkdtemp(path.join(tmpdir(), "rynx-skill-archive-"));
|
|
53
|
+
const archive = path.join(staging, "upload.zip");
|
|
54
|
+
const extracted = path.join(staging, "extracted");
|
|
55
|
+
try {
|
|
56
|
+
await writeFile(archive, content);
|
|
57
|
+
await mkdir(extracted, { recursive: true });
|
|
58
|
+
let entryCount = 0;
|
|
59
|
+
let extractedBytes = 0;
|
|
60
|
+
await extractZipArchive(archive, {
|
|
61
|
+
dir: extracted,
|
|
62
|
+
onEntry: (entry) => {
|
|
63
|
+
entryCount += 1;
|
|
64
|
+
extractedBytes += entry.uncompressedSize;
|
|
65
|
+
if (entryCount > MAX_ARCHIVE_ENTRIES) {
|
|
66
|
+
throw new Error(`archive exceeds the ${MAX_ARCHIVE_ENTRIES}-entry limit`);
|
|
67
|
+
}
|
|
68
|
+
if (extractedBytes > MAX_ARCHIVE_EXTRACTED_BYTES) {
|
|
69
|
+
throw new Error("archive exceeds the 24 MB extracted-size limit");
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const collected = collectSkillBundlesFromDirectory(extracted);
|
|
74
|
+
if (collected.bundles.length === 0 && collected.failures.length === 0) {
|
|
75
|
+
return [{
|
|
76
|
+
status: "failed",
|
|
77
|
+
name: path.basename(filename, path.extname(filename)),
|
|
78
|
+
sourcePath: filename,
|
|
79
|
+
message: "No valid Skill bundle was found in the archive.",
|
|
80
|
+
}];
|
|
81
|
+
}
|
|
82
|
+
return registerCollectedBundles({
|
|
83
|
+
bundles: collected.bundles.map((bundle) => ({ ...bundle, sourcePath: filename })),
|
|
84
|
+
failures: collected.failures.map((failure) => ({ ...failure, sourcePath: filename })),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await rm(staging, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function registerCollectedBundles(collected) {
|
|
92
|
+
const results = collected.failures.map((failure) => ({
|
|
93
|
+
status: "failed",
|
|
94
|
+
name: failure.name,
|
|
95
|
+
...(failure.sourcePath ? { sourcePath: failure.sourcePath } : {}),
|
|
96
|
+
message: failure.message,
|
|
97
|
+
}));
|
|
98
|
+
for (const bundle of collected.bundles) {
|
|
99
|
+
try {
|
|
100
|
+
results.push(await registerLocalBundle(bundle));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
results.push({
|
|
104
|
+
status: "failed",
|
|
105
|
+
name: bundle.name,
|
|
106
|
+
sourcePath: bundle.sourcePath,
|
|
107
|
+
message: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
25
113
|
/** Detail for one catalog skill: its metadata + the file tree under its dir. */
|
|
26
114
|
export async function getCatalogSkill(name) {
|
|
27
|
-
|
|
28
|
-
throw new Error(`invalid skill name: ${name}`);
|
|
115
|
+
assertSkillPathComponent(name);
|
|
29
116
|
const dir = path.join(rynxSkillsDir(), name);
|
|
30
117
|
const meta = await readSkillMeta(dir);
|
|
31
118
|
if (!meta)
|
|
@@ -62,8 +149,7 @@ async function walkSkillFiles(root, dir) {
|
|
|
62
149
|
/** Read one text file inside a catalog skill. Path-confined to the skill dir,
|
|
63
150
|
* size-capped, and binary files are rejected. */
|
|
64
151
|
export async function readCatalogSkillFile(name, relPath) {
|
|
65
|
-
|
|
66
|
-
throw new Error(`invalid skill name: ${name}`);
|
|
152
|
+
assertSkillPathComponent(name);
|
|
67
153
|
const dir = path.resolve(path.join(rynxSkillsDir(), name));
|
|
68
154
|
const target = path.resolve(dir, relPath);
|
|
69
155
|
if (target !== dir && !target.startsWith(dir + path.sep)) {
|
|
@@ -82,10 +168,221 @@ export async function readCatalogSkillFile(name, relPath) {
|
|
|
82
168
|
async function listMergedSkills() {
|
|
83
169
|
return (await scanSkillsDir(rynxSkillsDir())).sort((a, b) => a.name.localeCompare(b.name));
|
|
84
170
|
}
|
|
171
|
+
async function registerLocalBundle(bundle) {
|
|
172
|
+
const previous = localRegistrationTails.get(bundle.name) ?? Promise.resolve();
|
|
173
|
+
const pending = previous.catch(() => undefined).then(() => registerLocalBundleUnlocked(bundle));
|
|
174
|
+
localRegistrationTails.set(bundle.name, pending);
|
|
175
|
+
try {
|
|
176
|
+
return await pending;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (localRegistrationTails.get(bundle.name) === pending) {
|
|
180
|
+
localRegistrationTails.delete(bundle.name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function registerLocalBundleUnlocked(bundle) {
|
|
185
|
+
const catalogRoot = rynxSkillsDir();
|
|
186
|
+
const currentDir = path.join(catalogRoot, bundle.name);
|
|
187
|
+
const existing = await readSkillMeta(currentDir);
|
|
188
|
+
if (existing?.ref && !isManagedLocalArtifact(existing.ref.install)) {
|
|
189
|
+
return {
|
|
190
|
+
status: "conflict",
|
|
191
|
+
name: bundle.name,
|
|
192
|
+
sourcePath: bundle.sourcePath,
|
|
193
|
+
skill: toSummary(existing),
|
|
194
|
+
message: "A Skill with this name is registered from another source.",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (existing?.ref?.contentHash === bundle.contentHash) {
|
|
198
|
+
return {
|
|
199
|
+
status: "skipped",
|
|
200
|
+
name: bundle.name,
|
|
201
|
+
sourcePath: bundle.sourcePath,
|
|
202
|
+
skill: toSummary(existing),
|
|
203
|
+
message: "Skill is already up to date.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (await pathExists(currentDir) && !existing) {
|
|
207
|
+
return {
|
|
208
|
+
status: "conflict",
|
|
209
|
+
name: bundle.name,
|
|
210
|
+
sourcePath: bundle.sourcePath,
|
|
211
|
+
message: "A receipt-less Skill with this name already exists. Remove it before importing.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const artifactDir = rynxSkillArtifactDir(bundle.name, bundle.contentHash);
|
|
215
|
+
await materializeArtifact(bundle, artifactDir);
|
|
216
|
+
const ref = {
|
|
217
|
+
name: bundle.name,
|
|
218
|
+
install: { tool: "skills-cli", source: artifactDir },
|
|
219
|
+
contentHash: bundle.contentHash,
|
|
220
|
+
};
|
|
221
|
+
await mkdir(catalogRoot, { recursive: true });
|
|
222
|
+
const stagingRoot = await mkdtemp(path.join(catalogRoot, `.import-${bundle.name}-`));
|
|
223
|
+
const stagedDir = path.join(stagingRoot, bundle.name);
|
|
224
|
+
const backupDir = path.join(catalogRoot, `.backup-${bundle.name}-${process.pid}-${randomUUID()}`);
|
|
225
|
+
let backedUp = false;
|
|
226
|
+
try {
|
|
227
|
+
await cp(artifactDir, stagedDir, { recursive: true });
|
|
228
|
+
await writeSkillInstallInfo(stagedDir, ref);
|
|
229
|
+
if (await pathExists(currentDir)) {
|
|
230
|
+
await rename(currentDir, backupDir);
|
|
231
|
+
backedUp = true;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await rename(stagedDir, currentDir);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (backedUp) {
|
|
238
|
+
await rename(backupDir, currentDir);
|
|
239
|
+
backedUp = false;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
if (backedUp) {
|
|
244
|
+
await rm(backupDir, { recursive: true, force: true });
|
|
245
|
+
backedUp = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
250
|
+
if (backedUp && !(await pathExists(currentDir))) {
|
|
251
|
+
await rename(backupDir, currentDir);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const meta = await readSkillMeta(currentDir);
|
|
255
|
+
if (!meta)
|
|
256
|
+
throw new Error("Imported Skill failed catalog validation");
|
|
257
|
+
return {
|
|
258
|
+
status: existing ? "updated" : "created",
|
|
259
|
+
name: meta.name,
|
|
260
|
+
sourcePath: bundle.sourcePath,
|
|
261
|
+
skill: toSummary(meta),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function materializeArtifact(bundle, artifactDir) {
|
|
265
|
+
if (await pathExists(artifactDir)) {
|
|
266
|
+
if (await artifactMatchesBundle(bundle, artifactDir))
|
|
267
|
+
return;
|
|
268
|
+
throw new Error("Immutable Skill artifact already exists but does not match its content hash");
|
|
269
|
+
}
|
|
270
|
+
const artifactsRoot = rynxSkillArtifactsDir();
|
|
271
|
+
await mkdir(artifactsRoot, { recursive: true });
|
|
272
|
+
const stagingRoot = await mkdtemp(path.join(artifactsRoot, `.artifact-${bundle.name}-`));
|
|
273
|
+
const stagedDir = path.join(stagingRoot, bundle.name);
|
|
274
|
+
try {
|
|
275
|
+
await mkdir(stagedDir, { recursive: true });
|
|
276
|
+
const aggregate = createHash("sha256");
|
|
277
|
+
for (const file of [...bundle.files].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
278
|
+
const relative = normalizeBundlePath(file.path);
|
|
279
|
+
const content = Buffer.from(file.contentBase64, "base64");
|
|
280
|
+
if (content.byteLength !== file.sizeBytes)
|
|
281
|
+
throw new Error(`Skill file size mismatch: ${relative}`);
|
|
282
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
283
|
+
if (sha256 !== file.sha256)
|
|
284
|
+
throw new Error(`Skill file hash mismatch: ${relative}`);
|
|
285
|
+
aggregate.update(relative);
|
|
286
|
+
aggregate.update("\0");
|
|
287
|
+
aggregate.update(sha256);
|
|
288
|
+
aggregate.update("\0");
|
|
289
|
+
aggregate.update(String(content.byteLength));
|
|
290
|
+
aggregate.update("\0");
|
|
291
|
+
const target = path.join(stagedDir, ...relative.split("/"));
|
|
292
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
293
|
+
await writeFile(target, content);
|
|
294
|
+
}
|
|
295
|
+
if (aggregate.digest("hex") !== bundle.contentHash)
|
|
296
|
+
throw new Error("Skill bundle hash mismatch");
|
|
297
|
+
const parent = path.dirname(artifactDir);
|
|
298
|
+
await mkdir(parent, { recursive: true });
|
|
299
|
+
try {
|
|
300
|
+
await rename(stagedDir, artifactDir);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (await artifactMatchesBundle(bundle, artifactDir))
|
|
304
|
+
return;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function artifactMatchesBundle(bundle, artifactDir) {
|
|
313
|
+
try {
|
|
314
|
+
const actualFiles = await walkArtifactFiles(artifactDir, artifactDir);
|
|
315
|
+
const expectedFiles = [...bundle.files].sort((left, right) => left.path.localeCompare(right.path));
|
|
316
|
+
if (!actualFiles || actualFiles.length !== expectedFiles.length)
|
|
317
|
+
return false;
|
|
318
|
+
for (let index = 0; index < expectedFiles.length; index += 1) {
|
|
319
|
+
const expected = expectedFiles[index];
|
|
320
|
+
if (actualFiles[index] !== expected.path)
|
|
321
|
+
return false;
|
|
322
|
+
const content = await readFile(path.join(artifactDir, ...expected.path.split("/")));
|
|
323
|
+
if (content.byteLength !== expected.sizeBytes)
|
|
324
|
+
return false;
|
|
325
|
+
if (createHash("sha256").update(content).digest("hex") !== expected.sha256)
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function walkArtifactFiles(root, dir) {
|
|
335
|
+
const files = [];
|
|
336
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
337
|
+
if (entry.isSymbolicLink())
|
|
338
|
+
return null;
|
|
339
|
+
const absolute = path.join(dir, entry.name);
|
|
340
|
+
if (entry.isDirectory()) {
|
|
341
|
+
const nested = await walkArtifactFiles(root, absolute);
|
|
342
|
+
if (!nested)
|
|
343
|
+
return null;
|
|
344
|
+
files.push(...nested);
|
|
345
|
+
}
|
|
346
|
+
else if (entry.isFile()) {
|
|
347
|
+
files.push(normalizeBundlePath(path.relative(root, absolute)));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
351
|
+
}
|
|
352
|
+
function normalizeBundlePath(value) {
|
|
353
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
354
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
355
|
+
throw new Error(`Invalid Skill file path: ${value}`);
|
|
356
|
+
}
|
|
357
|
+
return parts.join("/");
|
|
358
|
+
}
|
|
359
|
+
function isManagedLocalArtifact(recipe) {
|
|
360
|
+
if (recipe.tool !== "skills-cli" || !path.isAbsolute(recipe.source))
|
|
361
|
+
return false;
|
|
362
|
+
const root = path.resolve(rynxSkillArtifactsDir());
|
|
363
|
+
const source = path.resolve(recipe.source);
|
|
364
|
+
const relative = path.relative(root, source);
|
|
365
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
366
|
+
}
|
|
367
|
+
function toSummary(meta) {
|
|
368
|
+
return {
|
|
369
|
+
name: meta.name,
|
|
370
|
+
description: meta.description,
|
|
371
|
+
...(meta.ref ? { ref: meta.ref } : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
async function pathExists(value) {
|
|
375
|
+
try {
|
|
376
|
+
await stat(value);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
85
383
|
/** Remove a catalog skill by name. Returns false if it wasn't present. */
|
|
86
384
|
export async function removeCatalogSkill(name) {
|
|
87
|
-
|
|
88
|
-
throw new Error(`invalid skill name: ${name}`);
|
|
385
|
+
assertSkillPathComponent(name);
|
|
89
386
|
const dir = path.join(rynxSkillsDir(), name);
|
|
90
387
|
try {
|
|
91
388
|
await stat(dir);
|
package/dist/sqlite.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DatabaseSync, type StatementSync } from "node:sqlite";
|
|
2
|
+
type SQLiteInput = null | number | bigint | string | NodeJS.ArrayBufferView;
|
|
3
|
+
type SQLiteParameter = SQLiteInput | Record<string, SQLiteInput>;
|
|
4
|
+
type SQLiteOutput = null | number | bigint | string | Uint8Array;
|
|
5
|
+
export type SQLiteTransaction<Args extends unknown[], Result> = ((...args: Args) => Result) & {
|
|
6
|
+
deferred: (...args: Args) => Result;
|
|
7
|
+
immediate: (...args: Args) => Result;
|
|
8
|
+
exclusive: (...args: Args) => Result;
|
|
9
|
+
};
|
|
10
|
+
/** Wrap a synchronous operation in a SQLite transaction. Nested calls use savepoints. */
|
|
11
|
+
export declare function transaction<Args extends unknown[], Result>(connection: DatabaseSync, operation: (...args: Args) => Result): SQLiteTransaction<Args, Result>;
|
|
12
|
+
/** Return only the first column from a native statement, matching scalar-query intent. */
|
|
13
|
+
export declare function scalar(statement: StatementSync): {
|
|
14
|
+
get(...parameters: SQLiteParameter[]): SQLiteOutput | undefined;
|
|
15
|
+
all(...parameters: SQLiteParameter[]): SQLiteOutput[];
|
|
16
|
+
};
|
|
17
|
+
/** Match a primary SQLite result code, including extended result codes. */
|
|
18
|
+
export declare function hasSqlitePrimaryCode(error: unknown, primaryCode: number): boolean;
|
|
19
|
+
export declare const SQLITE_BUSY = 5;
|
|
20
|
+
export declare const SQLITE_LOCKED = 6;
|
|
21
|
+
export declare const SQLITE_CONSTRAINT = 19;
|
|
22
|
+
export {};
|
package/dist/sqlite.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
let nextSavepointId = 0;
|
|
2
|
+
/** Wrap a synchronous operation in a SQLite transaction. Nested calls use savepoints. */
|
|
3
|
+
export function transaction(connection, operation) {
|
|
4
|
+
const run = (mode, args) => {
|
|
5
|
+
if (connection.isTransaction) {
|
|
6
|
+
const savepoint = `rynx_transaction_${++nextSavepointId}`;
|
|
7
|
+
connection.exec(`SAVEPOINT ${savepoint}`);
|
|
8
|
+
try {
|
|
9
|
+
const result = operation(...args);
|
|
10
|
+
connection.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (connection.isTransaction) {
|
|
15
|
+
connection.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
16
|
+
connection.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
connection.exec(`BEGIN ${mode}`);
|
|
22
|
+
try {
|
|
23
|
+
const result = operation(...args);
|
|
24
|
+
connection.exec("COMMIT");
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (connection.isTransaction)
|
|
29
|
+
connection.exec("ROLLBACK");
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const wrapped = ((...args) => run("DEFERRED", args));
|
|
34
|
+
wrapped.deferred = (...args) => run("DEFERRED", args);
|
|
35
|
+
wrapped.immediate = (...args) => run("IMMEDIATE", args);
|
|
36
|
+
wrapped.exclusive = (...args) => run("EXCLUSIVE", args);
|
|
37
|
+
return wrapped;
|
|
38
|
+
}
|
|
39
|
+
/** Return only the first column from a native statement, matching scalar-query intent. */
|
|
40
|
+
export function scalar(statement) {
|
|
41
|
+
statement.setReturnArrays(true);
|
|
42
|
+
const arrayStatement = statement;
|
|
43
|
+
return {
|
|
44
|
+
get: (...parameters) => arrayStatement.get(...parameters)?.[0],
|
|
45
|
+
all: (...parameters) => arrayStatement.all(...parameters).map((row) => row[0]),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Match a primary SQLite result code, including extended result codes. */
|
|
49
|
+
export function hasSqlitePrimaryCode(error, primaryCode) {
|
|
50
|
+
if (!error || typeof error !== "object" || !("errcode" in error))
|
|
51
|
+
return false;
|
|
52
|
+
const errcode = error.errcode;
|
|
53
|
+
return typeof errcode === "number" && (errcode & 0xff) === primaryCode;
|
|
54
|
+
}
|
|
55
|
+
export const SQLITE_BUSY = 5;
|
|
56
|
+
export const SQLITE_LOCKED = 6;
|
|
57
|
+
export const SQLITE_CONSTRAINT = 19;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SystemDependencyStatus {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
detail?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SystemDependencySnapshot {
|
|
6
|
+
tmux: SystemDependencyStatus;
|
|
7
|
+
}
|
|
8
|
+
export interface SystemDependencyCommandResult {
|
|
9
|
+
exitCode: number | null;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SystemDependencyCommandOptions {
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
export type SystemDependencyCommandRunner = (command: string, args: readonly string[], options: SystemDependencyCommandOptions) => Promise<SystemDependencyCommandResult>;
|
|
18
|
+
export interface SystemDependencyOptions {
|
|
19
|
+
runCommand?: SystemDependencyCommandRunner;
|
|
20
|
+
tmuxBin: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function inspectSystemDependencies(options: SystemDependencyOptions): Promise<SystemDependencySnapshot>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
3
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
4
|
+
const MINIMUM_TMUX_VERSION = { major: 3, minor: 4 };
|
|
5
|
+
export async function inspectSystemDependencies(options) {
|
|
6
|
+
const runCommand = options.runCommand ?? runSystemDependencyCommand;
|
|
7
|
+
const probe = await probeTmux(runCommand, options.tmuxBin);
|
|
8
|
+
return {
|
|
9
|
+
tmux: probe.installed
|
|
10
|
+
? { installed: true }
|
|
11
|
+
: { installed: false, detail: probe.detail },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A version probe only proves the binary can start. Live Sessions need tmux to
|
|
16
|
+
* create a server and socket, so exercise that path with a private probe server
|
|
17
|
+
* that cannot interfere with a user's existing tmux sessions.
|
|
18
|
+
*/
|
|
19
|
+
async function probeTmux(runCommand, tmuxBin) {
|
|
20
|
+
const version = await runCommand(tmuxBin, ["-V"], { timeoutMs: PROBE_TIMEOUT_MS });
|
|
21
|
+
if (version.exitCode !== 0) {
|
|
22
|
+
return {
|
|
23
|
+
installed: false,
|
|
24
|
+
detail: `tmux -V failed: ${commandFailure(version)}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (!isSupportedTmuxVersion(version.stdout)) {
|
|
28
|
+
return {
|
|
29
|
+
installed: false,
|
|
30
|
+
detail: `${version.stdout.trim() || tmuxBin} is too old; ` +
|
|
31
|
+
`Rynx requires tmux ${MINIMUM_TMUX_VERSION.major}.${MINIMUM_TMUX_VERSION.minor} or newer`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const socketName = `rynx-setup-probe-${process.pid}-${Date.now()}`;
|
|
35
|
+
const functional = await runCommand(tmuxBin, ["-L", socketName, "new-session", "-d", "-s", "probe", "true"], { timeoutMs: PROBE_TIMEOUT_MS });
|
|
36
|
+
await runCommand(tmuxBin, ["-L", socketName, "kill-server"], {
|
|
37
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
38
|
+
});
|
|
39
|
+
if (functional.exitCode !== 0) {
|
|
40
|
+
return {
|
|
41
|
+
installed: false,
|
|
42
|
+
detail: `${version.stdout.trim() || "tmux"} is installed but cannot start a server: ` +
|
|
43
|
+
commandFailure(functional),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { installed: true };
|
|
47
|
+
}
|
|
48
|
+
function isSupportedTmuxVersion(stdout) {
|
|
49
|
+
const match = /^tmux\s+(\d+)\.(\d+)/i.exec(stdout.trim());
|
|
50
|
+
if (!match)
|
|
51
|
+
return false;
|
|
52
|
+
const major = Number(match[1]);
|
|
53
|
+
const minor = Number(match[2]);
|
|
54
|
+
return major > MINIMUM_TMUX_VERSION.major ||
|
|
55
|
+
(major === MINIMUM_TMUX_VERSION.major && minor >= MINIMUM_TMUX_VERSION.minor);
|
|
56
|
+
}
|
|
57
|
+
function runSystemDependencyCommand(command, args, options) {
|
|
58
|
+
return new Promise((resolveResult) => {
|
|
59
|
+
execFile(command, [...args], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
env: { ...process.env, DEBIAN_FRONTEND: "noninteractive", LC_ALL: "C" },
|
|
62
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
63
|
+
timeout: options.timeoutMs,
|
|
64
|
+
}, (error, stdout, stderr) => {
|
|
65
|
+
resolveResult({
|
|
66
|
+
exitCode: error
|
|
67
|
+
? typeof error.code === "number" ? error.code : null
|
|
68
|
+
: 0,
|
|
69
|
+
stdout,
|
|
70
|
+
stderr,
|
|
71
|
+
...(error && typeof error.code === "string" ? { errorCode: error.code } : {}),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function commandFailure(result) {
|
|
77
|
+
const detail = result.stderr.trim() || result.stdout.trim() || result.errorCode;
|
|
78
|
+
return detail ? detail.slice(0, 2_000) : `exit code ${result.exitCode ?? "unknown"}`;
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/daemon",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.21",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
7
7
|
"directory": "packages/daemon"
|
|
8
8
|
},
|
|
9
|
-
"description": "Rynx
|
|
9
|
+
"description": "Rynx resident control-plane service and standalone lifecycle implementation.",
|
|
10
10
|
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22.16"
|
|
13
|
+
},
|
|
11
14
|
"publishConfig": {
|
|
12
15
|
"registry": "https://registry.npmjs.org/",
|
|
13
16
|
"access": "public"
|
|
@@ -16,38 +19,51 @@
|
|
|
16
19
|
"dist",
|
|
17
20
|
"bundled-plugins"
|
|
18
21
|
],
|
|
19
|
-
"bin": {
|
|
20
|
-
"rynx": "./dist/cli.js"
|
|
21
|
-
},
|
|
22
|
-
"main": "./dist/index-daemon.js",
|
|
23
22
|
"exports": {
|
|
24
|
-
"./
|
|
25
|
-
"types": "./dist/
|
|
26
|
-
"default": "./dist/
|
|
23
|
+
"./entry-path": {
|
|
24
|
+
"types": "./dist/entry-path.d.ts",
|
|
25
|
+
"default": "./dist/entry-path.js"
|
|
26
|
+
},
|
|
27
|
+
"./lifecycle": {
|
|
28
|
+
"types": "./dist/lifecycle.d.ts",
|
|
29
|
+
"default": "./dist/lifecycle.js"
|
|
30
|
+
},
|
|
31
|
+
"./browser-artifacts": {
|
|
32
|
+
"types": "./dist/browser-artifacts.d.ts",
|
|
33
|
+
"default": "./dist/browser-artifacts.js"
|
|
34
|
+
},
|
|
35
|
+
"./setup-service": {
|
|
36
|
+
"types": "./dist/setup-service.d.ts",
|
|
37
|
+
"default": "./dist/setup-service.js"
|
|
38
|
+
},
|
|
39
|
+
"./plugin-cli": {
|
|
40
|
+
"types": "./dist/plugin-cli.d.ts",
|
|
41
|
+
"default": "./dist/plugin-cli.js"
|
|
42
|
+
},
|
|
43
|
+
"./internal/process": {
|
|
44
|
+
"types": "./dist/process.d.ts",
|
|
45
|
+
"default": "./dist/process.js"
|
|
27
46
|
}
|
|
28
47
|
},
|
|
29
48
|
"dependencies": {
|
|
30
|
-
"@clack/prompts": "^1.6.0",
|
|
31
|
-
"better-sqlite3": "^12.11.1",
|
|
32
49
|
"extract-zip": "2.0.1",
|
|
33
50
|
"pm2": "^6.0.0",
|
|
34
51
|
"tar": "^7.5.19",
|
|
35
52
|
"ws": "^8.21.0",
|
|
36
|
-
"@rynx-ai/
|
|
37
|
-
"@rynx-ai/
|
|
38
|
-
"@rynx-ai/
|
|
39
|
-
"@rynx-ai/plugin-
|
|
40
|
-
"@rynx-ai/
|
|
41
|
-
"@rynx-ai/
|
|
42
|
-
"@rynx-ai/
|
|
43
|
-
"@rynx-ai/server": "0.1.11-beta.2"
|
|
53
|
+
"@rynx-ai/core": "0.1.11-beta.21",
|
|
54
|
+
"@rynx-ai/emulator": "0.1.11-beta.21",
|
|
55
|
+
"@rynx-ai/plugin-runner": "0.1.11-beta.21",
|
|
56
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.21",
|
|
57
|
+
"@rynx-ai/protocol": "0.1.11-beta.21",
|
|
58
|
+
"@rynx-ai/remote-runtime-client": "0.1.11-beta.21",
|
|
59
|
+
"@rynx-ai/server": "0.1.11-beta.21"
|
|
44
60
|
},
|
|
45
61
|
"devDependencies": {
|
|
46
62
|
"@types/ws": "^8.18.1",
|
|
47
|
-
"@rynx-ai/plugin-channel-lark": "0.1.11"
|
|
63
|
+
"@rynx-ai/plugin-channel-lark": "0.1.11-beta.21"
|
|
48
64
|
},
|
|
49
65
|
"scripts": {
|
|
50
|
-
"build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/
|
|
66
|
+
"build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",
|
|
51
67
|
"dev": "tsx watch --tsconfig ../../tsconfig.json src/index-daemon.ts"
|
|
52
68
|
}
|
|
53
69
|
}
|