@rynx-ai/daemon 0.1.10 → 0.1.11-beta.10

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 (156) hide show
  1. package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
  2. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
  3. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
  4. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
  5. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
  6. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
  7. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
  8. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
  9. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
  10. package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
  11. package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
  12. package/bundled-plugins/plugins/lark/dist/index.js +7 -0
  13. package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
  14. package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
  15. package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
  16. package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
  17. package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
  18. package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
  19. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
  20. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
  21. package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
  22. package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
  23. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
  24. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
  25. package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
  26. package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
  27. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
  28. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
  29. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
  30. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
  31. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
  32. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
  33. package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
  34. package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
  35. package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
  36. package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
  37. package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
  38. package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
  39. package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
  40. package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
  41. package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
  42. package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
  43. package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
  44. package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
  45. package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
  46. package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
  47. package/bundled-plugins/plugins/lark/package.json +57 -0
  48. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  49. package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
  50. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  51. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  52. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  53. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  54. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  55. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  56. package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  57. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  58. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  59. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  60. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  61. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  62. package/bundled-plugins/plugins/lark/ui/index.html +13 -0
  63. package/dist/agent-file.js +0 -1
  64. package/dist/browser-artifact-management.d.ts +4 -1
  65. package/dist/browser-artifact-management.js +18 -10
  66. package/dist/browser-artifacts.d.ts +1 -0
  67. package/dist/browser-artifacts.js +1 -0
  68. package/dist/bundled-plugins.d.ts +15 -0
  69. package/dist/bundled-plugins.js +134 -0
  70. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  71. package/dist/chrome-for-testing-runtime.js +193 -0
  72. package/dist/chrome-for-testing-store.d.ts +2 -0
  73. package/dist/chrome-for-testing-store.js +9 -0
  74. package/dist/control-deps.d.ts +6 -2
  75. package/dist/control-deps.js +88 -82
  76. package/dist/daemon-server.d.ts +7 -1
  77. package/dist/daemon-server.js +310 -87
  78. package/dist/db.js +181 -124
  79. package/dist/entry-path.d.ts +3 -0
  80. package/dist/entry-path.js +12 -0
  81. package/dist/index-daemon.js +3 -66
  82. package/dist/lifecycle.d.ts +12 -0
  83. package/dist/lifecycle.js +16 -0
  84. package/dist/maintenance-lease.d.ts +39 -0
  85. package/dist/maintenance-lease.js +250 -0
  86. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  87. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  88. package/dist/official-plugins.d.ts +2 -0
  89. package/dist/official-plugins.js +2 -0
  90. package/dist/plugin-cli.d.ts +1 -3
  91. package/dist/plugin-cli.js +56 -45
  92. package/dist/plugin-host-leases.d.ts +4 -0
  93. package/dist/plugin-host-leases.js +7 -0
  94. package/dist/plugin-host-rpc.d.ts +73 -6
  95. package/dist/plugin-host-rpc.js +354 -81
  96. package/dist/plugin-installer.d.ts +24 -55
  97. package/dist/plugin-installer.js +258 -412
  98. package/dist/plugin-management-service.d.ts +30 -23
  99. package/dist/plugin-management-service.js +288 -105
  100. package/dist/plugin-marketplace.d.ts +35 -0
  101. package/dist/plugin-marketplace.js +299 -0
  102. package/dist/plugin-package.d.ts +22 -24
  103. package/dist/plugin-package.js +520 -571
  104. package/dist/plugin-skill-context.d.ts +4 -0
  105. package/dist/plugin-skill-context.js +35 -0
  106. package/dist/plugin-store.d.ts +36 -37
  107. package/dist/plugin-store.js +138 -92
  108. package/dist/plugin-supervisor.d.ts +50 -125
  109. package/dist/plugin-supervisor.js +678 -1030
  110. package/dist/pm2.d.ts +11 -2
  111. package/dist/pm2.js +130 -28
  112. package/dist/process.d.ts +12 -0
  113. package/dist/process.js +91 -0
  114. package/dist/runtime-project-store.d.ts +9 -0
  115. package/dist/runtime-project-store.js +83 -0
  116. package/dist/session-fallback-workspace.d.ts +22 -0
  117. package/dist/session-fallback-workspace.js +120 -0
  118. package/dist/session-fork-store.d.ts +41 -0
  119. package/dist/session-fork-store.js +288 -0
  120. package/dist/session-launch-operations.d.ts +23 -2
  121. package/dist/session-launch-operations.js +48 -7
  122. package/dist/session-meta-store.d.ts +12 -9
  123. package/dist/session-meta-store.js +50 -49
  124. package/dist/session-portal-grant-store.d.ts +29 -0
  125. package/dist/session-portal-grant-store.js +84 -0
  126. package/dist/setup-service.d.ts +21 -0
  127. package/dist/setup-service.js +39 -0
  128. package/dist/skills-catalog.d.ts +11 -2
  129. package/dist/skills-catalog.js +307 -7
  130. package/dist/system-dependencies.d.ts +26 -0
  131. package/dist/system-dependencies.js +149 -0
  132. package/package.json +36 -20
  133. package/dist/browser-cli-args.d.ts +0 -20
  134. package/dist/browser-cli-args.js +0 -100
  135. package/dist/channel-store.d.ts +0 -22
  136. package/dist/channel-store.js +0 -65
  137. package/dist/cli.d.ts +0 -2
  138. package/dist/cli.js +0 -732
  139. package/dist/control-client.d.ts +0 -122
  140. package/dist/control-client.js +0 -855
  141. package/dist/control-endpoint.d.ts +0 -14
  142. package/dist/control-endpoint.js +0 -124
  143. package/dist/desktop-browser-host-client.d.ts +0 -35
  144. package/dist/desktop-browser-host-client.js +0 -415
  145. package/dist/instance-store.d.ts +0 -38
  146. package/dist/instance-store.js +0 -85
  147. package/dist/plugin-install-restart.d.ts +0 -13
  148. package/dist/plugin-install-restart.js +0 -24
  149. package/dist/plugin-runtime-control.d.ts +0 -2
  150. package/dist/plugin-runtime-control.js +0 -37
  151. package/dist/registry.d.ts +0 -10
  152. package/dist/registry.js +0 -52
  153. package/dist/setup.d.ts +0 -20
  154. package/dist/setup.js +0 -374
  155. package/dist/update.d.ts +0 -25
  156. package/dist/update.js +0 -133
package/dist/pm2.d.ts CHANGED
@@ -8,12 +8,21 @@ export declare function formatControlUrl(host: string, port: number): string;
8
8
  * entry from scratch (delete + start) so config flags are always fresh; the
9
9
  * `force === false` path short-circuits when it's already online.
10
10
  */
11
- export declare function startDaemon({ force, quiet }: {
11
+ export declare function startDaemon({ force, quiet, launchEnvironment, }: {
12
12
  force: boolean;
13
13
  quiet?: boolean;
14
+ launchEnvironment?: NodeJS.ProcessEnv;
14
15
  }): number;
15
16
  export declare function isPm2DaemonOnline(): boolean;
16
- /** Stop and remove the daemon from pm2's process list. */
17
+ /**
18
+ * Stop the daemon and terminate its dedicated pm2 supervisor.
19
+ *
20
+ * PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
21
+ * applications. Leaving it alive across an npm replacement is unsafe: the
22
+ * daemon process container path is resolved inside the old pm2 package. Once
23
+ * npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
24
+ * subsequent `rynx start` enters a restart loop before our code can run.
25
+ */
17
26
  export declare function stopDaemon(): number;
18
27
  /** Print a one-line status; exit 0 only when online. */
19
28
  export declare function statusDaemon(): number;
package/dist/pm2.js CHANGED
@@ -10,15 +10,18 @@
10
10
  import { spawn, spawnSync } from "node:child_process";
11
11
  import { mkdirSync, realpathSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
- import { homedir } from "node:os";
14
- import { dirname, join, resolve } from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- import { loadConfig } from "@rynx-ai/core";
13
+ import { userInfo } from "node:os";
14
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
15
+ import { loadConfig, rynxHome } from "@rynx-ai/core";
17
16
  import { daemonEntryRestartRequired } from "./daemon-build-status.js";
17
+ import { resolveDaemonEntry } from "./entry-path.js";
18
18
  const require = createRequire(import.meta.url);
19
19
  /** pm2 app name — single resident daemon per machine. */
20
20
  const APP_NAME = "rynx";
21
21
  const PM2_COMMAND_TIMEOUT_MS = 5_000;
22
+ const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
23
+ const LOGIN_SHELL_PATH_MAX_BYTES = 256 * 1024;
24
+ const REFRESH_LOGIN_SHELL_PATH_ENV = "RYNX_REFRESH_LOGIN_SHELL_PATH";
22
25
  const DAEMON_PACKAGE_VERSION = require("../package.json").version;
23
26
  /**
24
27
  * Build a dialable control-console URL from the daemon's bind host/port. Bind-all
@@ -50,27 +53,76 @@ function pm2Bin() {
50
53
  // pm2 ships its CLI at bin/pm2; run it with our own node.
51
54
  return require.resolve("pm2/bin/pm2");
52
55
  }
53
- /** `index-daemon.js` sits next to this file in `dist/`. */
54
- function daemonEntryPath() {
55
- return join(dirname(fileURLToPath(import.meta.url)), "index-daemon.js");
56
- }
57
56
  function logsDir() {
58
- const home = process.env.RYNX_HOME?.trim() || join(homedir(), ".rynx");
59
- const dir = join(home, "logs");
57
+ const dir = join(rynxHome(), "logs");
60
58
  mkdirSync(dir, { recursive: true });
61
59
  return dir;
62
60
  }
63
61
  function nodeRuntimeEnv() {
64
- if (!process.versions.electron) {
65
- return process.env;
62
+ const home = rynxHome();
63
+ const pm2Home = join(home, "pm2");
64
+ mkdirSync(pm2Home, { recursive: true, mode: 0o700 });
65
+ return {
66
+ ...process.env,
67
+ RYNX_HOME: home,
68
+ PM2_HOME: pm2Home,
69
+ ...(process.versions.electron
70
+ ? { ELECTRON_RUN_AS_NODE: "1" }
71
+ : {}),
72
+ };
73
+ }
74
+ /**
75
+ * A detached plugin updater intentionally receives a reduced environment and
76
+ * therefore has no SHELL. Starting the replacement daemon with that reduced
77
+ * PATH would make user-installed runtimes (for example an fnm-managed Codex)
78
+ * disappear after every upgrade. Recover the user's login-shell PATH only at
79
+ * this boundary; normal interactive starts keep their exact caller environment.
80
+ */
81
+ function daemonStartEnv() {
82
+ const env = nodeRuntimeEnv();
83
+ const refreshLoginPath = env[REFRESH_LOGIN_SHELL_PATH_ENV] === "1";
84
+ delete env[REFRESH_LOGIN_SHELL_PATH_ENV];
85
+ if (!refreshLoginPath)
86
+ return env;
87
+ const shell = userInfo().shell?.trim();
88
+ if (!shell) {
89
+ throw new Error("cannot refresh daemon PATH: the user login shell is unavailable");
90
+ }
91
+ const result = spawnSync(shell, ["-lic", `printf '\\036%s\\037' "$PATH"`], {
92
+ encoding: "utf8",
93
+ // Do not feed an fnm/nvm "multishell" directory inherited from the old
94
+ // daemon back into the next login shell. Version managers prepend a new
95
+ // directory on every shell startup, so reusing the old PATH would grow
96
+ // it indefinitely across upgrades.
97
+ env: { ...env, SHELL: shell, PATH: loginShellBaselinePath() },
98
+ timeout: LOGIN_SHELL_PATH_TIMEOUT_MS,
99
+ killSignal: "SIGTERM",
100
+ maxBuffer: LOGIN_SHELL_PATH_MAX_BYTES,
101
+ });
102
+ if (result.error) {
103
+ throw new Error(`cannot refresh daemon PATH: ${result.error.message}`);
104
+ }
105
+ if (result.status !== 0) {
106
+ throw new Error(`cannot refresh daemon PATH: login shell exited ${result.status ?? "without status"}`);
66
107
  }
67
- return { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
108
+ const path = extractFramedPath(result.stdout);
109
+ if (!path) {
110
+ throw new Error("cannot refresh daemon PATH: login shell returned no framed PATH");
111
+ }
112
+ const npmPrefix = env.NPM_CONFIG_PREFIX?.trim();
113
+ const npmBin = npmPrefix && isAbsolute(npmPrefix)
114
+ ? join(npmPrefix, "bin")
115
+ : "";
116
+ const refreshedPath = npmBin === ""
117
+ ? path
118
+ : [npmBin, ...path.split(delimiter).filter((entry) => entry !== npmBin)].join(delimiter);
119
+ return { ...env, PATH: refreshedPath, SHELL: shell };
68
120
  }
69
121
  /** Run a pm2 command inheriting stdio; returns its exit code. */
70
- function runPm2(args, quiet = false) {
122
+ function runPm2(args, quiet = false, env = nodeRuntimeEnv()) {
71
123
  const res = spawnSync(process.execPath, [pm2Bin(), ...args], {
72
124
  stdio: quiet ? "ignore" : "inherit",
73
- env: nodeRuntimeEnv(),
125
+ env,
74
126
  timeout: PM2_COMMAND_TIMEOUT_MS,
75
127
  killSignal: "SIGTERM",
76
128
  });
@@ -112,7 +164,7 @@ function pm2ErrorMessage(error) {
112
164
  * entry from scratch (delete + start) so config flags are always fresh; the
113
165
  * `force === false` path short-circuits when it's already online.
114
166
  */
115
- export function startDaemon({ force, quiet = false }) {
167
+ export function startDaemon({ force, quiet = false, launchEnvironment, }) {
116
168
  let existing;
117
169
  try {
118
170
  existing = describe();
@@ -134,6 +186,20 @@ export function startDaemon({ force, quiet = false }) {
134
186
  if (staleBuild && !quiet) {
135
187
  console.log("A newer daemon build is present on disk; restarting Rynx.");
136
188
  }
189
+ let startEnv;
190
+ try {
191
+ // Resolve the replacement environment before deleting an existing app.
192
+ // A broken/hung login shell must leave the known-good daemon untouched.
193
+ startEnv = {
194
+ ...daemonStartEnv(),
195
+ ...launchEnvironment,
196
+ };
197
+ }
198
+ catch (error) {
199
+ if (!quiet)
200
+ console.error(error instanceof Error ? error.message : String(error));
201
+ return 1;
202
+ }
137
203
  const dir = logsDir();
138
204
  if (existing) {
139
205
  const removed = runPm2(["delete", APP_NAME], quiet);
@@ -142,12 +208,12 @@ export function startDaemon({ force, quiet = false }) {
142
208
  }
143
209
  const status = runPm2([
144
210
  "start",
145
- daemonEntryPath(),
211
+ resolveDaemonEntry(),
146
212
  "--name",
147
213
  APP_NAME,
148
- // Resolve `.env` relative to where the user invoked `rynx start`.
214
+ // App and CLI share one daemon-owned state/workspace root.
149
215
  "--cwd",
150
- process.cwd(),
216
+ rynxHome(),
151
217
  "--max-restarts",
152
218
  "10",
153
219
  "--restart-delay",
@@ -158,7 +224,7 @@ export function startDaemon({ force, quiet = false }) {
158
224
  join(dir, "err.log"),
159
225
  "--merge-logs",
160
226
  "--time",
161
- ], quiet);
227
+ ], quiet, startEnv);
162
228
  if (status === 0 && !quiet) {
163
229
  console.log(`rynx ${force ? "restarted" : "started"} (pm2 app "${APP_NAME}", logs: ${dir}).`);
164
230
  console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
@@ -168,16 +234,35 @@ export function startDaemon({ force, quiet = false }) {
168
234
  export function isPm2DaemonOnline() {
169
235
  return describe()?.pm2_env?.status === "online";
170
236
  }
171
- /** Stop and remove the daemon from pm2's process list. */
237
+ /**
238
+ * Stop the daemon and terminate its dedicated pm2 supervisor.
239
+ *
240
+ * PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
241
+ * applications. Leaving it alive across an npm replacement is unsafe: the
242
+ * daemon process container path is resolved inside the old pm2 package. Once
243
+ * npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
244
+ * subsequent `rynx start` enters a restart loop before our code can run.
245
+ */
172
246
  export function stopDaemon() {
173
- if (!describe()) {
247
+ const existing = describe();
248
+ if (!existing) {
249
+ // Even an empty process list can belong to a resident PM2 daemon whose
250
+ // module path is about to be replaced. `pm2 kill` is idempotent and is
251
+ // the only reliable package-upgrade boundary.
252
+ const killed = runPm2(["kill"], true);
253
+ if (killed !== 0)
254
+ return killed;
174
255
  console.log("rynx is not running.");
175
256
  return 0;
176
257
  }
177
- const status = runPm2(["delete", APP_NAME]);
178
- if (status === 0)
179
- console.log("rynx stopped.");
180
- return status;
258
+ const removed = runPm2(["delete", APP_NAME]);
259
+ if (removed !== 0)
260
+ return removed;
261
+ const killed = runPm2(["kill"]);
262
+ if (killed !== 0)
263
+ return killed;
264
+ console.log("rynx stopped.");
265
+ return 0;
181
266
  }
182
267
  /** Print a one-line status; exit 0 only when online. */
183
268
  export function statusDaemon() {
@@ -204,7 +289,7 @@ export function streamLogs() {
204
289
  }
205
290
  function residentBuildRestartRequired(app) {
206
291
  const runningEntry = app.pm2_env?.pm_exec_path;
207
- if (runningEntry && !sameEntryPath(runningEntry, daemonEntryPath()))
292
+ if (runningEntry && !sameEntryPath(runningEntry, resolveDaemonEntry()))
208
293
  return true;
209
294
  const runningVersion = app.pm2_env?.version;
210
295
  if (runningVersion &&
@@ -215,7 +300,7 @@ function residentBuildRestartRequired(app) {
215
300
  const startedAt = app.pm2_env?.pm_uptime;
216
301
  return typeof startedAt === "number" &&
217
302
  Number.isFinite(startedAt) &&
218
- daemonEntryRestartRequired(daemonEntryPath(), startedAt);
303
+ daemonEntryRestartRequired(resolveDaemonEntry(), startedAt);
219
304
  }
220
305
  function sameEntryPath(left, right) {
221
306
  try {
@@ -225,3 +310,20 @@ function sameEntryPath(left, right) {
225
310
  return resolve(left) === resolve(right);
226
311
  }
227
312
  }
313
+ function extractFramedPath(stdout) {
314
+ if (!stdout)
315
+ return undefined;
316
+ const start = stdout.lastIndexOf("\x1e");
317
+ if (start < 0)
318
+ return undefined;
319
+ const end = stdout.indexOf("\x1f", start + 1);
320
+ if (end < 0)
321
+ return undefined;
322
+ const value = stdout.slice(start + 1, end);
323
+ return value.trim() ? value : undefined;
324
+ }
325
+ function loginShellBaselinePath() {
326
+ return process.platform === "darwin"
327
+ ? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
328
+ : "/usr/local/bin:/usr/bin:/bin";
329
+ }
@@ -0,0 +1,12 @@
1
+ interface SupervisorProcess {
2
+ connected?: boolean;
3
+ disconnect?: () => void;
4
+ }
5
+ /** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
6
+ export declare function disconnectSupervisorIpc(target?: SupervisorProcess): void;
7
+ /**
8
+ * Run the daemon in the current process. Process ownership and executable
9
+ * selection belong to the caller (CLI standalone lifecycle or App lifecycle).
10
+ */
11
+ export declare function runDaemonProcess(): Promise<void>;
12
+ export {};
@@ -0,0 +1,91 @@
1
+ import { startRynxDaemonServer } from "./daemon-server.js";
2
+ import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
3
+ import { installStdioEpipeGuard } from "./stdio-epipe-guard.js";
4
+ /** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
5
+ export function disconnectSupervisorIpc(target = process) {
6
+ if (target.connected && typeof target.disconnect === "function") {
7
+ target.disconnect();
8
+ }
9
+ }
10
+ /**
11
+ * Run the daemon in the current process. Process ownership and executable
12
+ * selection belong to the caller (CLI standalone lifecycle or App lifecycle).
13
+ */
14
+ export async function runDaemonProcess() {
15
+ installStdioEpipeGuard();
16
+ const followsApp = process.env.RYNX_DAEMON_LIFECYCLE === "follow-app";
17
+ let parentDisconnected = followsApp && !process.connected;
18
+ const recordParentDisconnect = () => {
19
+ parentDisconnected = true;
20
+ };
21
+ if (followsApp)
22
+ process.once("disconnect", recordParentDisconnect);
23
+ const preparePlugins = async () => {
24
+ try {
25
+ const bundled = await ensureBundledOfficialPlugins({ reclaimHostLeases: true });
26
+ if (bundled.status === "ready") {
27
+ console.log(JSON.stringify({
28
+ level: "info",
29
+ type: "bundled_plugins_ready",
30
+ installed: bundled.installed,
31
+ updated: bundled.updated,
32
+ }));
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.error(JSON.stringify({
37
+ level: "error",
38
+ type: "bundled_plugins_failed",
39
+ error: error instanceof Error ? error.message : String(error),
40
+ }));
41
+ }
42
+ };
43
+ let server;
44
+ try {
45
+ server = await startRynxDaemonServer({
46
+ preparePlugins,
47
+ onShutdownComplete: () => disconnectSupervisorIpc(),
48
+ });
49
+ }
50
+ catch (error) {
51
+ process.off("disconnect", recordParentDisconnect);
52
+ throw error;
53
+ }
54
+ let shutdownPromise;
55
+ const shutdown = (signal) => {
56
+ if (shutdownPromise)
57
+ return shutdownPromise;
58
+ process.off("SIGINT", onSigint);
59
+ process.off("SIGTERM", onSigterm);
60
+ process.off("disconnect", onDisconnect);
61
+ shutdownPromise = (async () => {
62
+ console.log(JSON.stringify({ level: "info", type: "shutdown", signal }));
63
+ await server.shutdown();
64
+ })();
65
+ shutdownPromise.catch((error) => {
66
+ process.exitCode = 1;
67
+ console.error(`Shutdown failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
68
+ });
69
+ return shutdownPromise;
70
+ };
71
+ const onSigint = () => {
72
+ void shutdown("SIGINT");
73
+ };
74
+ const onSigterm = () => {
75
+ void shutdown("SIGTERM");
76
+ };
77
+ const onDisconnect = () => {
78
+ if (process.env.RYNX_DAEMON_LIFECYCLE === "follow-app") {
79
+ void shutdown("APP_DISCONNECT");
80
+ }
81
+ };
82
+ process.once("SIGINT", onSigint);
83
+ process.once("SIGTERM", onSigterm);
84
+ if (followsApp) {
85
+ process.once("disconnect", onDisconnect);
86
+ process.off("disconnect", recordParentDisconnect);
87
+ if (parentDisconnected || !process.connected) {
88
+ void shutdown("APP_DISCONNECT");
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,9 @@
1
+ import { type RuntimeProject, type RuntimeProjectInput, type RuntimeProjectStore } from "@rynx-ai/core";
2
+ /** SQLite-backed Project catalog owned by the current target Runtime. */
3
+ export declare class SqliteRuntimeProjectStore implements RuntimeProjectStore {
4
+ list(): Promise<RuntimeProject[]>;
5
+ get(id: string): Promise<RuntimeProject | undefined>;
6
+ create(input: RuntimeProjectInput): Promise<RuntimeProject>;
7
+ update(id: string, input: RuntimeProjectInput): Promise<RuntimeProject | undefined>;
8
+ remove(id: string): Promise<boolean>;
9
+ }
@@ -0,0 +1,83 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { canonicalizeRuntimeProjectInput, runtimeProjectSchema, } from "@rynx-ai/core";
3
+ import { db } from "./db.js";
4
+ const COLUMNS = "id, name, directories_json, primary_dir, created_at, updated_at";
5
+ /** SQLite-backed Project catalog owned by the current target Runtime. */
6
+ export class SqliteRuntimeProjectStore {
7
+ async list() {
8
+ return db()
9
+ .prepare(`SELECT ${COLUMNS} FROM runtime_projects ORDER BY updated_at DESC, id`)
10
+ .all()
11
+ .map((row) => rowToProject(row));
12
+ }
13
+ async get(id) {
14
+ const row = db()
15
+ .prepare(`SELECT ${COLUMNS} FROM runtime_projects WHERE id = ?`)
16
+ .get(id);
17
+ return row ? rowToProject(row) : undefined;
18
+ }
19
+ async create(input) {
20
+ const resolved = await canonicalizeRuntimeProjectInput(input);
21
+ const timestamp = new Date().toISOString();
22
+ const project = {
23
+ id: randomUUID(),
24
+ ...resolved,
25
+ createdAt: timestamp,
26
+ updatedAt: timestamp,
27
+ };
28
+ const parsed = runtimeProjectSchema.parse(project);
29
+ db()
30
+ .prepare(`INSERT INTO runtime_projects
31
+ (id, name, directories_json, primary_dir, created_at, updated_at)
32
+ VALUES (@id, @name, @directories, @primary, @createdAt, @updatedAt)`)
33
+ .run({
34
+ id: parsed.id,
35
+ name: parsed.name,
36
+ directories: JSON.stringify(parsed.directories),
37
+ primary: parsed.primary,
38
+ createdAt: parsed.createdAt,
39
+ updatedAt: parsed.updatedAt,
40
+ });
41
+ return parsed;
42
+ }
43
+ async update(id, input) {
44
+ const current = await this.get(id);
45
+ if (!current)
46
+ return undefined;
47
+ const resolved = await canonicalizeRuntimeProjectInput(input);
48
+ const project = runtimeProjectSchema.parse({
49
+ id: current.id,
50
+ ...resolved,
51
+ createdAt: current.createdAt,
52
+ updatedAt: new Date().toISOString(),
53
+ });
54
+ db()
55
+ .prepare(`UPDATE runtime_projects
56
+ SET name = @name,
57
+ directories_json = @directories,
58
+ primary_dir = @primary,
59
+ updated_at = @updatedAt
60
+ WHERE id = @id`)
61
+ .run({
62
+ id: project.id,
63
+ name: project.name,
64
+ directories: JSON.stringify(project.directories),
65
+ primary: project.primary,
66
+ updatedAt: project.updatedAt,
67
+ });
68
+ return project;
69
+ }
70
+ async remove(id) {
71
+ return db().prepare("DELETE FROM runtime_projects WHERE id = ?").run(id).changes > 0;
72
+ }
73
+ }
74
+ function rowToProject(row) {
75
+ return runtimeProjectSchema.parse({
76
+ id: row.id,
77
+ name: row.name,
78
+ directories: JSON.parse(row.directories_json),
79
+ primary: row.primary_dir,
80
+ createdAt: row.created_at,
81
+ updatedAt: row.updated_at,
82
+ });
83
+ }
@@ -0,0 +1,22 @@
1
+ import { type SessionWorkspaceSnapshot } from "@rynx-ai/core";
2
+ export interface SessionFallbackRootOptions {
3
+ platform?: NodeJS.Platform;
4
+ home?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ readTextFile?: (path: string) => Promise<string>;
7
+ }
8
+ export interface SessionFallbackWorkspaceOptions extends SessionFallbackRootOptions {
9
+ now?: () => Date;
10
+ randomId?: () => string;
11
+ }
12
+ /**
13
+ * Resolve the user-visible root for Sessions created without a Project.
14
+ *
15
+ * Linux desktop environments can localize or relocate Documents through the
16
+ * XDG user-dirs file. Headless installations commonly omit that file, so they
17
+ * fall back to a visible `$HOME/Rynx` instead of creating an arbitrary
18
+ * `$HOME/Documents` tree. Other platforms retain the existing Documents root.
19
+ */
20
+ export declare function resolveSessionFallbackRoot(options?: SessionFallbackRootOptions): Promise<string>;
21
+ export declare function createSessionFallbackWorkspace(options?: SessionFallbackWorkspaceOptions): Promise<SessionWorkspaceSnapshot>;
22
+ export declare function discardSessionFallbackWorkspace(workspace: SessionWorkspaceSnapshot, options?: SessionFallbackRootOptions): Promise<void>;
@@ -0,0 +1,120 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, realpath, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, normalize, relative } from "node:path";
5
+ import { sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
6
+ /**
7
+ * Resolve the user-visible root for Sessions created without a Project.
8
+ *
9
+ * Linux desktop environments can localize or relocate Documents through the
10
+ * XDG user-dirs file. Headless installations commonly omit that file, so they
11
+ * fall back to a visible `$HOME/Rynx` instead of creating an arbitrary
12
+ * `$HOME/Documents` tree. Other platforms retain the existing Documents root.
13
+ */
14
+ export async function resolveSessionFallbackRoot(options = {}) {
15
+ const platform = options.platform ?? process.platform;
16
+ const home = normalize(options.home ?? homedir());
17
+ if (!isAbsolute(home))
18
+ throw new Error("Session fallback home must be absolute");
19
+ if (platform !== "linux")
20
+ return join(home, "Documents", "Rynx");
21
+ const env = options.env ?? process.env;
22
+ const configHome = env.XDG_CONFIG_HOME && isAbsolute(env.XDG_CONFIG_HOME)
23
+ ? normalize(env.XDG_CONFIG_HOME)
24
+ : join(home, ".config");
25
+ const readTextFile = options.readTextFile ?? ((path) => readFile(path, "utf8"));
26
+ try {
27
+ const contents = await readTextFile(join(configHome, "user-dirs.dirs"));
28
+ const documents = parseXdgDocumentsDirectory(contents, home);
29
+ if (documents)
30
+ return join(documents, "Rynx");
31
+ }
32
+ catch {
33
+ // Missing or unreadable XDG configuration is normal on headless Linux.
34
+ }
35
+ return join(home, "Rynx");
36
+ }
37
+ export async function createSessionFallbackWorkspace(options = {}) {
38
+ const now = (options.now ?? (() => new Date()))();
39
+ const date = [
40
+ now.getFullYear(),
41
+ String(now.getMonth() + 1).padStart(2, "0"),
42
+ String(now.getDate()).padStart(2, "0"),
43
+ ].join("-");
44
+ const fallbackRoot = await resolveSessionFallbackRoot(options);
45
+ await mkdir(fallbackRoot, { recursive: true });
46
+ const canonicalFallbackRoot = await realpath(fallbackRoot);
47
+ const dateDirectory = join(canonicalFallbackRoot, date);
48
+ await mkdir(dateDirectory, { recursive: true });
49
+ const canonicalDateDirectory = await realpath(dateDirectory);
50
+ if (relative(canonicalFallbackRoot, canonicalDateDirectory) !== date) {
51
+ throw new Error("Session fallback date directory resolves outside the managed root");
52
+ }
53
+ const randomId = options.randomId ?? randomUUID;
54
+ for (let attempt = 0; attempt < 8; attempt += 1) {
55
+ const directory = join(canonicalDateDirectory, randomId().slice(0, 8));
56
+ try {
57
+ await mkdir(directory);
58
+ const canonical = await realpath(directory);
59
+ return sessionWorkspaceSnapshotSchema.parse({
60
+ cwd: canonical,
61
+ allowedDirs: [canonical],
62
+ });
63
+ }
64
+ catch (error) {
65
+ if (error.code !== "EEXIST")
66
+ throw error;
67
+ }
68
+ }
69
+ throw new Error("could not allocate a unique Session workspace directory");
70
+ }
71
+ export async function discardSessionFallbackWorkspace(workspace, options = {}) {
72
+ if (workspace.allowedDirs.length !== 1
73
+ || workspace.allowedDirs[0] !== workspace.cwd) {
74
+ return;
75
+ }
76
+ let canonicalRoot;
77
+ try {
78
+ canonicalRoot = await realpath(await resolveSessionFallbackRoot(options));
79
+ }
80
+ catch {
81
+ return;
82
+ }
83
+ const relativePath = relative(canonicalRoot, workspace.cwd);
84
+ const segments = relativePath.split(/[\\/]/);
85
+ if (isAbsolute(relativePath)
86
+ || relativePath.startsWith("..")
87
+ || segments.length !== 2
88
+ || !/^\d{4}-\d{2}-\d{2}$/.test(segments[0] ?? "")
89
+ || !/^[0-9a-f]{8}$/.test(segments[1] ?? "")) {
90
+ return;
91
+ }
92
+ await rm(workspace.cwd, { recursive: true, force: true });
93
+ }
94
+ function parseXdgDocumentsDirectory(contents, home) {
95
+ for (const line of contents.split(/\r?\n/)) {
96
+ const match = line.match(/^\s*XDG_DOCUMENTS_DIR\s*=\s*"((?:\\.|[^"])*)"\s*(?:#.*)?$/);
97
+ if (!match)
98
+ continue;
99
+ const raw = match[1];
100
+ let expanded;
101
+ if (raw === "$HOME" || raw === "${HOME}") {
102
+ expanded = home;
103
+ }
104
+ else if (raw.startsWith("$HOME/")) {
105
+ expanded = join(home, decodeXdgPath(raw.slice("$HOME/".length)));
106
+ }
107
+ else if (raw.startsWith("${HOME}/")) {
108
+ expanded = join(home, decodeXdgPath(raw.slice("${HOME}/".length)));
109
+ }
110
+ else {
111
+ expanded = decodeXdgPath(raw);
112
+ }
113
+ if (!expanded.includes("\0") && isAbsolute(expanded))
114
+ return normalize(expanded);
115
+ }
116
+ return undefined;
117
+ }
118
+ function decodeXdgPath(value) {
119
+ return value.replace(/\\(.)/g, "$1");
120
+ }
@@ -0,0 +1,41 @@
1
+ import { type MachineSessionRecord } from "@rynx-ai/core";
2
+ import type { SessionItem } from "@rynx-ai/protocol";
3
+ import { type MachineSessionForkOperationRecord, type MachineSessionForkStorePort } from "@rynx-ai/server";
4
+ /**
5
+ * SQLite/resource-file implementation of the fork publication boundary.
6
+ *
7
+ * Provider context is prepared before {@link commit}; this store then publishes
8
+ * Session metadata, an independent canonical transcript, referenced committed
9
+ * resources, and the completed operation in one SQLite transaction. Resource
10
+ * files use deterministic target ids, so a crash before the transaction can be
11
+ * retried safely.
12
+ */
13
+ export declare class SqliteSessionForkStore implements MachineSessionForkStorePort {
14
+ private readonly resourceRoot;
15
+ constructor(resourceRoot?: string);
16
+ claim(input: {
17
+ operationId: string;
18
+ requestHash: string;
19
+ sourceSessionId: string;
20
+ title?: string;
21
+ }): MachineSessionForkOperationRecord;
22
+ commit(input: {
23
+ operationId: string;
24
+ target: MachineSessionRecord;
25
+ items: SessionItem[];
26
+ }): void;
27
+ publishNativeFork(input: {
28
+ sourceSessionId: string;
29
+ target: MachineSessionRecord;
30
+ items: SessionItem[];
31
+ }): void;
32
+ private publishTarget;
33
+ markError(operationId: string, error: string): void;
34
+ listReserved(): MachineSessionForkOperationRecord[];
35
+ hasReservedSource(sessionId: string): boolean;
36
+ markTargetDeleted(sessionId: string): void;
37
+ private read;
38
+ private copyResourceFile;
39
+ private sessionDirectory;
40
+ private resourcePath;
41
+ }