@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.41

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.
Files changed (72) hide show
  1. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
  2. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
  3. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
  4. package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6043 -6016
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6003 -5985
  8. package/bundled-plugins/plugins/lark/package.json +1 -1
  9. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  10. package/bundled-plugins/plugins/lark/ui/index.html +2 -2
  11. package/dist/agent-file.js +0 -1
  12. package/dist/browser-artifact-management.js +4 -1
  13. package/dist/chrome-for-testing-http.d.ts +9 -0
  14. package/dist/chrome-for-testing-http.js +58 -0
  15. package/dist/chrome-for-testing-release-resolver.js +13 -6
  16. package/dist/chrome-for-testing-runtime.js +4 -3
  17. package/dist/chrome-for-testing-store.d.ts +2 -0
  18. package/dist/chrome-for-testing-store.js +99 -17
  19. package/dist/control-deps.d.ts +6 -2
  20. package/dist/control-deps.js +87 -4
  21. package/dist/control-link-store.js +4 -3
  22. package/dist/daemon-owner.js +4 -7
  23. package/dist/daemon-server.js +42 -17
  24. package/dist/db.d.ts +2 -2
  25. package/dist/db.js +157 -21
  26. package/dist/headless-browser-host.js +397 -41
  27. package/dist/lifecycle.d.ts +2 -1
  28. package/dist/lifecycle.js +4 -1
  29. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  30. package/dist/plugin-cli.d.ts +10 -0
  31. package/dist/plugin-cli.js +19 -2
  32. package/dist/plugin-host-rpc.d.ts +48 -9
  33. package/dist/plugin-host-rpc.js +593 -81
  34. package/dist/plugin-installer.js +3 -2
  35. package/dist/plugin-management-service.js +0 -2
  36. package/dist/plugin-marketplace.js +2 -1
  37. package/dist/plugin-store.js +2 -4
  38. package/dist/plugin-supervisor.js +0 -4
  39. package/dist/pm2.d.ts +2 -0
  40. package/dist/pm2.js +6 -0
  41. package/dist/remote-runtime-access-store.d.ts +2 -2
  42. package/dist/remote-runtime-access-store.js +6 -9
  43. package/dist/remote-runtime-connection-manager.js +25 -42
  44. package/dist/remote-runtime-target-store.d.ts +2 -2
  45. package/dist/remote-runtime-target-store.js +6 -6
  46. package/dist/session-emulator-binding-store.js +4 -6
  47. package/dist/session-fork-store.d.ts +1 -0
  48. package/dist/session-fork-store.js +85 -30
  49. package/dist/session-launch-operations.js +6 -5
  50. package/dist/session-log-store.d.ts +1 -0
  51. package/dist/session-log-store.js +46 -22
  52. package/dist/session-meta-store.d.ts +5 -0
  53. package/dist/session-meta-store.js +25 -2
  54. package/dist/session-portal-grant-store.d.ts +55 -0
  55. package/dist/session-portal-grant-store.js +160 -0
  56. package/dist/session-resource-store.d.ts +24 -6
  57. package/dist/session-resource-store.js +387 -69
  58. package/dist/session-timeline-projection-store.d.ts +24 -0
  59. package/dist/session-timeline-projection-store.js +53 -0
  60. package/dist/setup-service.d.ts +1 -0
  61. package/dist/setup-service.js +1 -0
  62. package/dist/skills-catalog.d.ts +11 -2
  63. package/dist/skills-catalog.js +317 -10
  64. package/dist/sqlite.d.ts +22 -0
  65. package/dist/sqlite.js +57 -0
  66. package/dist/stdio-epipe-guard.d.ts +1 -1
  67. package/dist/stdio-epipe-guard.js +1 -1
  68. package/dist/system-dependencies.d.ts +22 -0
  69. package/dist/system-dependencies.js +79 -0
  70. package/package.json +17 -12
  71. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  72. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -0,0 +1,24 @@
1
+ export interface SessionTimelineProjection {
2
+ pluginId: string;
3
+ sessionId: string;
4
+ projectionId: string;
5
+ responseId: string;
6
+ itemId: string;
7
+ text: string;
8
+ state: "pending" | "completed";
9
+ disposition: "created" | "replayed";
10
+ }
11
+ /** Durable identity for a narrow text projection. The text itself deliberately
12
+ * stays in the canonical Session item; this row only fences retries. */
13
+ export declare function claimSessionTimelineProjection(input: {
14
+ pluginId: string;
15
+ sessionId: string;
16
+ projectionId: string;
17
+ responseId?: string;
18
+ text: string;
19
+ }): SessionTimelineProjection;
20
+ export declare function completeSessionTimelineProjection(input: {
21
+ pluginId: string;
22
+ sessionId: string;
23
+ projectionId: string;
24
+ }): void;
@@ -0,0 +1,53 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { db } from "./db.js";
3
+ import { transaction } from "./sqlite.js";
4
+ /** Durable identity for a narrow text projection. The text itself deliberately
5
+ * stays in the canonical Session item; this row only fences retries. */
6
+ export function claimSessionTimelineProjection(input) {
7
+ const connection = db();
8
+ const requestHash = createHash("sha256")
9
+ .update(JSON.stringify({ responseId: input.responseId ?? null, text: input.text }), "utf8")
10
+ .digest("hex");
11
+ return transaction(connection, () => {
12
+ const existing = connection.prepare(`SELECT plugin_id, session_id, projection_id, request_hash, response_id, item_id, state
13
+ FROM session_timeline_projections
14
+ WHERE plugin_id = ? AND session_id = ? AND projection_id = ?`).get(input.pluginId, input.sessionId, input.projectionId);
15
+ if (existing) {
16
+ if (existing.request_hash !== requestHash) {
17
+ throw new Error("projection id was reused with different text");
18
+ }
19
+ return present(existing, input.text, "replayed");
20
+ }
21
+ const responseId = input.responseId?.trim() || `projection_response_${input.projectionId}`;
22
+ const itemId = `msg_projection_${randomUUID()}`;
23
+ connection.prepare(`INSERT INTO session_timeline_projections
24
+ (plugin_id, session_id, projection_id, request_hash, response_id, item_id, state, created_at)
25
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`).run(input.pluginId, input.sessionId, input.projectionId, requestHash, responseId, itemId, new Date().toISOString());
26
+ return {
27
+ pluginId: input.pluginId,
28
+ sessionId: input.sessionId,
29
+ projectionId: input.projectionId,
30
+ responseId,
31
+ itemId,
32
+ text: input.text,
33
+ state: "pending",
34
+ disposition: "created",
35
+ };
36
+ })();
37
+ }
38
+ export function completeSessionTimelineProjection(input) {
39
+ db().prepare(`UPDATE session_timeline_projections SET state = 'completed'
40
+ WHERE plugin_id = ? AND session_id = ? AND projection_id = ?`).run(input.pluginId, input.sessionId, input.projectionId);
41
+ }
42
+ function present(row, text, disposition) {
43
+ return {
44
+ pluginId: row.plugin_id,
45
+ sessionId: row.session_id,
46
+ projectionId: row.projection_id,
47
+ responseId: row.response_id,
48
+ itemId: row.item_id,
49
+ text,
50
+ state: row.state,
51
+ disposition,
52
+ };
53
+ }
@@ -1,3 +1,4 @@
1
+ export { inspectSystemDependencies, type SystemDependencySnapshot, } from "./system-dependencies.js";
1
2
  export interface DaemonDiagnosticSnapshot {
2
3
  databasePath: string;
3
4
  databaseExists: boolean;
@@ -4,6 +4,7 @@ import { createChromeForTestingArtifactStore, isChromeForTestingHostSupported }
4
4
  import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
5
5
  import { dbPath } from "./db.js";
6
6
  import { listPlugins } from "./plugin-store.js";
7
+ export { inspectSystemDependencies, } from "./system-dependencies.js";
7
8
  export async function prepareBundledPlugins() {
8
9
  return ensureBundledOfficialPlugins();
9
10
  }
@@ -1,11 +1,20 @@
1
- import { type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
2
- import type { SkillDetail } from "@rynx-ai/protocol/control";
1
+ import { type LocalSkillRuntime, type SkillInstallRecipe, type SkillMeta } from "@rynx-ai/core";
2
+ import type { ControlLocalSkillCandidate, ControlLocalSkillImportResult, SkillDetail } from "@rynx-ai/protocol/control";
3
3
  export declare function listCatalogSkills(): Promise<SkillMeta[]>;
4
4
  /** Install a skill (or a repo's skills) into the catalog per a full install
5
5
  * recipe; delegates to the core catalog layer (which also writes each skill's
6
6
  * `.rynx-skill.json` install-info file — a catalog-only artifact). Returns
7
7
  * the refreshed catalog. */
8
8
  export declare function installCatalogSkill(recipe: SkillInstallRecipe): Promise<SkillMeta[]>;
9
+ export declare function scanLocalCatalogSkillCandidates(runtime: LocalSkillRuntime): Promise<ControlLocalSkillCandidate[]>;
10
+ /**
11
+ * Register complete local runtime Skill bundles into the owner catalog. Every
12
+ * content hash gets a durable immutable artifact; the mutable catalog entry's
13
+ * receipt points at that artifact so old Agent specs keep their exact bytes
14
+ * after a later catalog update.
15
+ */
16
+ export declare function importLocalCatalogSkills(runtime: LocalSkillRuntime, keys: string[]): Promise<ControlLocalSkillImportResult[]>;
17
+ export declare function importCatalogSkillsFromArchive(filename: string, contentBase64: string): Promise<ControlLocalSkillImportResult[]>;
9
18
  /** Detail for one catalog skill: its metadata + the file tree under its dir. */
10
19
  export declare function getCatalogSkill(name: string): Promise<SkillDetail | null>;
11
20
  /** Read one text file inside a catalog skill. Path-confined to the skill dir,
@@ -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 { readdir, readFile, rm, stat } from "node:fs/promises";
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import { cp, lstat, 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 { extractSkillFrontmatter, installIntoCatalog, rynxSkillsDir, scanSkillsDir, readSkillMeta, } from "@rynx-ai/core";
10
- /** Catalog skill ids are kebab-case (matches the SKILL.md `name` convention). */
11
- const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
11
+ import extractZipArchive from "@electron-internal/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,101 @@ 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
+ await extractZipArchive(archive, { dir: extracted });
59
+ await assertExtractedArchiveLimits(extracted);
60
+ const collected = collectSkillBundlesFromDirectory(extracted);
61
+ if (collected.bundles.length === 0 && collected.failures.length === 0) {
62
+ return [{
63
+ status: "failed",
64
+ name: path.basename(filename, path.extname(filename)),
65
+ sourcePath: filename,
66
+ message: "No valid Skill bundle was found in the archive.",
67
+ }];
68
+ }
69
+ return registerCollectedBundles({
70
+ bundles: collected.bundles.map((bundle) => ({ ...bundle, sourcePath: filename })),
71
+ failures: collected.failures.map((failure) => ({ ...failure, sourcePath: filename })),
72
+ });
73
+ }
74
+ finally {
75
+ await rm(staging, { recursive: true, force: true });
76
+ }
77
+ }
78
+ async function assertExtractedArchiveLimits(root) {
79
+ let entryCount = 0;
80
+ let extractedBytes = 0;
81
+ const pending = [root];
82
+ while (pending.length > 0) {
83
+ const directory = pending.pop();
84
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
85
+ entryCount += 1;
86
+ if (entryCount > MAX_ARCHIVE_ENTRIES) {
87
+ throw new Error(`archive exceeds the ${MAX_ARCHIVE_ENTRIES}-entry limit`);
88
+ }
89
+ const absolute = path.join(directory, entry.name);
90
+ if (entry.isDirectory()) {
91
+ pending.push(absolute);
92
+ continue;
93
+ }
94
+ extractedBytes += (await lstat(absolute)).size;
95
+ if (extractedBytes > MAX_ARCHIVE_EXTRACTED_BYTES) {
96
+ throw new Error("archive exceeds the 24 MB extracted-size limit");
97
+ }
98
+ }
99
+ }
100
+ }
101
+ async function registerCollectedBundles(collected) {
102
+ const results = collected.failures.map((failure) => ({
103
+ status: "failed",
104
+ name: failure.name,
105
+ ...(failure.sourcePath ? { sourcePath: failure.sourcePath } : {}),
106
+ message: failure.message,
107
+ }));
108
+ for (const bundle of collected.bundles) {
109
+ try {
110
+ results.push(await registerLocalBundle(bundle));
111
+ }
112
+ catch (error) {
113
+ results.push({
114
+ status: "failed",
115
+ name: bundle.name,
116
+ sourcePath: bundle.sourcePath,
117
+ message: error instanceof Error ? error.message : String(error),
118
+ });
119
+ }
120
+ }
121
+ return results;
122
+ }
25
123
  /** Detail for one catalog skill: its metadata + the file tree under its dir. */
26
124
  export async function getCatalogSkill(name) {
27
- if (!SKILL_NAME_PATTERN.test(name))
28
- throw new Error(`invalid skill name: ${name}`);
125
+ assertSkillPathComponent(name);
29
126
  const dir = path.join(rynxSkillsDir(), name);
30
127
  const meta = await readSkillMeta(dir);
31
128
  if (!meta)
@@ -62,8 +159,7 @@ async function walkSkillFiles(root, dir) {
62
159
  /** Read one text file inside a catalog skill. Path-confined to the skill dir,
63
160
  * size-capped, and binary files are rejected. */
64
161
  export async function readCatalogSkillFile(name, relPath) {
65
- if (!SKILL_NAME_PATTERN.test(name))
66
- throw new Error(`invalid skill name: ${name}`);
162
+ assertSkillPathComponent(name);
67
163
  const dir = path.resolve(path.join(rynxSkillsDir(), name));
68
164
  const target = path.resolve(dir, relPath);
69
165
  if (target !== dir && !target.startsWith(dir + path.sep)) {
@@ -82,10 +178,221 @@ export async function readCatalogSkillFile(name, relPath) {
82
178
  async function listMergedSkills() {
83
179
  return (await scanSkillsDir(rynxSkillsDir())).sort((a, b) => a.name.localeCompare(b.name));
84
180
  }
181
+ async function registerLocalBundle(bundle) {
182
+ const previous = localRegistrationTails.get(bundle.name) ?? Promise.resolve();
183
+ const pending = previous.catch(() => undefined).then(() => registerLocalBundleUnlocked(bundle));
184
+ localRegistrationTails.set(bundle.name, pending);
185
+ try {
186
+ return await pending;
187
+ }
188
+ finally {
189
+ if (localRegistrationTails.get(bundle.name) === pending) {
190
+ localRegistrationTails.delete(bundle.name);
191
+ }
192
+ }
193
+ }
194
+ async function registerLocalBundleUnlocked(bundle) {
195
+ const catalogRoot = rynxSkillsDir();
196
+ const currentDir = path.join(catalogRoot, bundle.name);
197
+ const existing = await readSkillMeta(currentDir);
198
+ if (existing?.ref && !isManagedLocalArtifact(existing.ref.install)) {
199
+ return {
200
+ status: "conflict",
201
+ name: bundle.name,
202
+ sourcePath: bundle.sourcePath,
203
+ skill: toSummary(existing),
204
+ message: "A Skill with this name is registered from another source.",
205
+ };
206
+ }
207
+ if (existing?.ref?.contentHash === bundle.contentHash) {
208
+ return {
209
+ status: "skipped",
210
+ name: bundle.name,
211
+ sourcePath: bundle.sourcePath,
212
+ skill: toSummary(existing),
213
+ message: "Skill is already up to date.",
214
+ };
215
+ }
216
+ if (await pathExists(currentDir) && !existing) {
217
+ return {
218
+ status: "conflict",
219
+ name: bundle.name,
220
+ sourcePath: bundle.sourcePath,
221
+ message: "A receipt-less Skill with this name already exists. Remove it before importing.",
222
+ };
223
+ }
224
+ const artifactDir = rynxSkillArtifactDir(bundle.name, bundle.contentHash);
225
+ await materializeArtifact(bundle, artifactDir);
226
+ const ref = {
227
+ name: bundle.name,
228
+ install: { tool: "skills-cli", source: artifactDir },
229
+ contentHash: bundle.contentHash,
230
+ };
231
+ await mkdir(catalogRoot, { recursive: true });
232
+ const stagingRoot = await mkdtemp(path.join(catalogRoot, `.import-${bundle.name}-`));
233
+ const stagedDir = path.join(stagingRoot, bundle.name);
234
+ const backupDir = path.join(catalogRoot, `.backup-${bundle.name}-${process.pid}-${randomUUID()}`);
235
+ let backedUp = false;
236
+ try {
237
+ await cp(artifactDir, stagedDir, { recursive: true });
238
+ await writeSkillInstallInfo(stagedDir, ref);
239
+ if (await pathExists(currentDir)) {
240
+ await rename(currentDir, backupDir);
241
+ backedUp = true;
242
+ }
243
+ try {
244
+ await rename(stagedDir, currentDir);
245
+ }
246
+ catch (error) {
247
+ if (backedUp) {
248
+ await rename(backupDir, currentDir);
249
+ backedUp = false;
250
+ }
251
+ throw error;
252
+ }
253
+ if (backedUp) {
254
+ await rm(backupDir, { recursive: true, force: true });
255
+ backedUp = false;
256
+ }
257
+ }
258
+ finally {
259
+ await rm(stagingRoot, { recursive: true, force: true });
260
+ if (backedUp && !(await pathExists(currentDir))) {
261
+ await rename(backupDir, currentDir);
262
+ }
263
+ }
264
+ const meta = await readSkillMeta(currentDir);
265
+ if (!meta)
266
+ throw new Error("Imported Skill failed catalog validation");
267
+ return {
268
+ status: existing ? "updated" : "created",
269
+ name: meta.name,
270
+ sourcePath: bundle.sourcePath,
271
+ skill: toSummary(meta),
272
+ };
273
+ }
274
+ async function materializeArtifact(bundle, artifactDir) {
275
+ if (await pathExists(artifactDir)) {
276
+ if (await artifactMatchesBundle(bundle, artifactDir))
277
+ return;
278
+ throw new Error("Immutable Skill artifact already exists but does not match its content hash");
279
+ }
280
+ const artifactsRoot = rynxSkillArtifactsDir();
281
+ await mkdir(artifactsRoot, { recursive: true });
282
+ const stagingRoot = await mkdtemp(path.join(artifactsRoot, `.artifact-${bundle.name}-`));
283
+ const stagedDir = path.join(stagingRoot, bundle.name);
284
+ try {
285
+ await mkdir(stagedDir, { recursive: true });
286
+ const aggregate = createHash("sha256");
287
+ for (const file of [...bundle.files].sort((left, right) => left.path.localeCompare(right.path))) {
288
+ const relative = normalizeBundlePath(file.path);
289
+ const content = Buffer.from(file.contentBase64, "base64");
290
+ if (content.byteLength !== file.sizeBytes)
291
+ throw new Error(`Skill file size mismatch: ${relative}`);
292
+ const sha256 = createHash("sha256").update(content).digest("hex");
293
+ if (sha256 !== file.sha256)
294
+ throw new Error(`Skill file hash mismatch: ${relative}`);
295
+ aggregate.update(relative);
296
+ aggregate.update("\0");
297
+ aggregate.update(sha256);
298
+ aggregate.update("\0");
299
+ aggregate.update(String(content.byteLength));
300
+ aggregate.update("\0");
301
+ const target = path.join(stagedDir, ...relative.split("/"));
302
+ await mkdir(path.dirname(target), { recursive: true });
303
+ await writeFile(target, content);
304
+ }
305
+ if (aggregate.digest("hex") !== bundle.contentHash)
306
+ throw new Error("Skill bundle hash mismatch");
307
+ const parent = path.dirname(artifactDir);
308
+ await mkdir(parent, { recursive: true });
309
+ try {
310
+ await rename(stagedDir, artifactDir);
311
+ }
312
+ catch (error) {
313
+ if (await artifactMatchesBundle(bundle, artifactDir))
314
+ return;
315
+ throw error;
316
+ }
317
+ }
318
+ finally {
319
+ await rm(stagingRoot, { recursive: true, force: true });
320
+ }
321
+ }
322
+ async function artifactMatchesBundle(bundle, artifactDir) {
323
+ try {
324
+ const actualFiles = await walkArtifactFiles(artifactDir, artifactDir);
325
+ const expectedFiles = [...bundle.files].sort((left, right) => left.path.localeCompare(right.path));
326
+ if (!actualFiles || actualFiles.length !== expectedFiles.length)
327
+ return false;
328
+ for (let index = 0; index < expectedFiles.length; index += 1) {
329
+ const expected = expectedFiles[index];
330
+ if (actualFiles[index] !== expected.path)
331
+ return false;
332
+ const content = await readFile(path.join(artifactDir, ...expected.path.split("/")));
333
+ if (content.byteLength !== expected.sizeBytes)
334
+ return false;
335
+ if (createHash("sha256").update(content).digest("hex") !== expected.sha256)
336
+ return false;
337
+ }
338
+ return true;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ }
344
+ async function walkArtifactFiles(root, dir) {
345
+ const files = [];
346
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
347
+ if (entry.isSymbolicLink())
348
+ return null;
349
+ const absolute = path.join(dir, entry.name);
350
+ if (entry.isDirectory()) {
351
+ const nested = await walkArtifactFiles(root, absolute);
352
+ if (!nested)
353
+ return null;
354
+ files.push(...nested);
355
+ }
356
+ else if (entry.isFile()) {
357
+ files.push(normalizeBundlePath(path.relative(root, absolute)));
358
+ }
359
+ }
360
+ return files.sort((left, right) => left.localeCompare(right));
361
+ }
362
+ function normalizeBundlePath(value) {
363
+ const parts = value.replace(/\\/g, "/").split("/");
364
+ if (parts.some((part) => !part || part === "." || part === "..")) {
365
+ throw new Error(`Invalid Skill file path: ${value}`);
366
+ }
367
+ return parts.join("/");
368
+ }
369
+ function isManagedLocalArtifact(recipe) {
370
+ if (recipe.tool !== "skills-cli" || !path.isAbsolute(recipe.source))
371
+ return false;
372
+ const root = path.resolve(rynxSkillArtifactsDir());
373
+ const source = path.resolve(recipe.source);
374
+ const relative = path.relative(root, source);
375
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
376
+ }
377
+ function toSummary(meta) {
378
+ return {
379
+ name: meta.name,
380
+ description: meta.description,
381
+ ...(meta.ref ? { ref: meta.ref } : {}),
382
+ };
383
+ }
384
+ async function pathExists(value) {
385
+ try {
386
+ await stat(value);
387
+ return true;
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
85
393
  /** Remove a catalog skill by name. Returns false if it wasn't present. */
86
394
  export async function removeCatalogSkill(name) {
87
- if (!SKILL_NAME_PATTERN.test(name))
88
- throw new Error(`invalid skill name: ${name}`);
395
+ assertSkillPathComponent(name);
89
396
  const dir = path.join(rynxSkillsDir(), name);
90
397
  try {
91
398
  await stat(dir);
@@ -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;
@@ -3,6 +3,6 @@
3
3
  * broken pipe (a `pm2 logs` stream detaching, or a pm2 restart) would otherwise
4
4
  * surface as an unhandled `'error'` on the stream and crash a daemon that has
5
5
  * no uncaughtException trap. Swallow EPIPE on the std streams; re-throw anything
6
- * else. (Borrowed from botmux's daemon bootstrap.)
6
+ * else.
7
7
  */
8
8
  export declare function installStdioEpipeGuard(): void;
@@ -3,7 +3,7 @@
3
3
  * broken pipe (a `pm2 logs` stream detaching, or a pm2 restart) would otherwise
4
4
  * surface as an unhandled `'error'` on the stream and crash a daemon that has
5
5
  * no uncaughtException trap. Swallow EPIPE on the std streams; re-throw anything
6
- * else. (Borrowed from botmux's daemon bootstrap.)
6
+ * else.
7
7
  */
8
8
  export function installStdioEpipeGuard() {
9
9
  for (const stream of [process.stdout, process.stderr]) {
@@ -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>;