@rynx-ai/daemon 0.1.0 → 0.1.9

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 (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
@@ -0,0 +1,124 @@
1
+ import { newSessionId } from "@rynx-ai/core";
2
+ import { db } from "./db.js";
3
+ import { createSessionMeta } from "./session-meta-store.js";
4
+ const MAX_ID_LENGTH = 512;
5
+ const SHA256_PATTERN = /^[a-fA-F0-9]{64}$/;
6
+ export class SessionLaunchOperationConflictError extends Error {
7
+ pluginId;
8
+ scopeId;
9
+ operationId;
10
+ code = "SESSION_LAUNCH_OPERATION_CONFLICT";
11
+ constructor(pluginId, scopeId, operationId) {
12
+ super(`session launch operation was reused with a different request: ${pluginId}/${scopeId}/${operationId}`);
13
+ this.pluginId = pluginId;
14
+ this.scopeId = scopeId;
15
+ this.operationId = operationId;
16
+ this.name = "SessionLaunchOperationConflictError";
17
+ }
18
+ }
19
+ export class SessionLaunchOperationTargetDeletedError extends Error {
20
+ pluginId;
21
+ scopeId;
22
+ operationId;
23
+ sessionId;
24
+ code = "SESSION_LAUNCH_OPERATION_TARGET_DELETED";
25
+ constructor(pluginId, scopeId, operationId, sessionId) {
26
+ super(`session launch operation target was deleted: ${pluginId}/${scopeId}/${operationId}`);
27
+ this.pluginId = pluginId;
28
+ this.scopeId = scopeId;
29
+ this.operationId = operationId;
30
+ this.sessionId = sessionId;
31
+ this.name = "SessionLaunchOperationTargetDeletedError";
32
+ }
33
+ }
34
+ /**
35
+ * Atomically allocate a persisted machine session for one plugin operation.
36
+ *
37
+ * The durable operation row is the idempotency boundary across runner crashes,
38
+ * daemon restarts, and remote lease re-delivery. It deliberately excludes any
39
+ * Workspace concept: `scopeId` and `operationId` are opaque to the daemon.
40
+ */
41
+ export function ensurePluginSessionLaunch(input) {
42
+ const pluginId = requiredId(input.pluginId, "pluginId");
43
+ const scopeId = requiredId(input.scopeId, "scopeId");
44
+ const operationId = requiredId(input.operationId, "operationId");
45
+ const requestHash = requiredRequestHash(input.requestHash);
46
+ const ensure = db().transaction(() => {
47
+ const existing = readOperation(pluginId, scopeId, operationId);
48
+ if (existing) {
49
+ if (existing.request_hash !== requestHash) {
50
+ throw new SessionLaunchOperationConflictError(pluginId, scopeId, operationId);
51
+ }
52
+ if (existing.state === "deleted") {
53
+ return { disposition: "deleted", sessionId: existing.session_id };
54
+ }
55
+ if (!sessionExists(existing.session_id)) {
56
+ // Preserve the operation key even when an older deletion path removed
57
+ // the session without first marking its launch operation. Throwing from
58
+ // inside this transaction would roll the repair back, so return a
59
+ // sentinel and raise the typed error after the commit.
60
+ tombstoneOperations(existing.session_id, new Date().toISOString());
61
+ return { disposition: "deleted", sessionId: existing.session_id };
62
+ }
63
+ return { disposition: "replayed", sessionId: existing.session_id };
64
+ }
65
+ const now = new Date().toISOString();
66
+ let sessionId = newSessionId();
67
+ while (sessionExists(sessionId))
68
+ sessionId = newSessionId();
69
+ createSessionMeta({
70
+ ...input.session,
71
+ id: sessionId,
72
+ source: `plugin:${pluginId}`,
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ });
76
+ if (!sessionExists(sessionId)) {
77
+ throw new Error(`plugin session launch failed to persist session metadata: ${sessionId}`);
78
+ }
79
+ db()
80
+ .prepare(`INSERT INTO plugin_session_launch_operations
81
+ (plugin_id, scope_id, operation_id, request_hash, session_id, state,
82
+ created_at, updated_at, deleted_at)
83
+ VALUES (?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
84
+ .run(pluginId, scopeId, operationId, requestHash, sessionId, now, now);
85
+ return { disposition: "created", sessionId };
86
+ });
87
+ const result = ensure.immediate();
88
+ if (result.disposition === "deleted") {
89
+ throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, result.sessionId);
90
+ }
91
+ return result;
92
+ }
93
+ function readOperation(pluginId, scopeId, operationId) {
94
+ return db()
95
+ .prepare(`SELECT request_hash, session_id, state
96
+ FROM plugin_session_launch_operations
97
+ WHERE plugin_id = ? AND scope_id = ? AND operation_id = ?`)
98
+ .get(pluginId, scopeId, operationId);
99
+ }
100
+ function sessionExists(sessionId) {
101
+ return Boolean(db().prepare("SELECT 1 FROM sessions WHERE id = ?").pluck().get(sessionId));
102
+ }
103
+ function tombstoneOperations(sessionId, now) {
104
+ return db()
105
+ .prepare(`UPDATE plugin_session_launch_operations
106
+ SET state = 'deleted', updated_at = ?, deleted_at = ?
107
+ WHERE session_id = ? AND state = 'active'`)
108
+ .run(now, now, sessionId).changes;
109
+ }
110
+ function requiredId(value, label) {
111
+ const normalized = value?.trim();
112
+ if (!normalized)
113
+ throw new Error(`${label} is required`);
114
+ if (normalized.length > MAX_ID_LENGTH)
115
+ throw new Error(`${label} is too long`);
116
+ return normalized;
117
+ }
118
+ function requiredRequestHash(value) {
119
+ const normalized = value?.trim();
120
+ if (!SHA256_PATTERN.test(normalized)) {
121
+ throw new Error("requestHash must be a SHA-256 hex digest");
122
+ }
123
+ return normalized.toLowerCase();
124
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The unified machine-session record (SQLite) — one row per session regardless
3
3
  * of origin (the web console AND every channel). Holds identity/meta (source /
4
- * agent / inline config / model / title / …) so the **session list survives a
4
+ * provider / agent / inline config / model / title / …) so the **session list survives a
5
5
  * daemon restart**; the transcript lives in `session_items` (see
6
6
  * {@link import("./session-log-store.js")}). Runtime `status` is intentionally
7
7
  * not stored — it is reconstructed as `idle` after a restart.
@@ -11,12 +11,13 @@
11
11
  * via the `ChannelContext`, so a channel registers its sessions the same way the
12
12
  * console does.
13
13
  */
14
- import type { MachineSessionRecord, SessionRegistry } from "@rynx-ai/core";
14
+ import { type MachineSessionRecord, type SessionRegistry } from "@rynx-ai/core";
15
15
  /** Back-compat alias for the unified record. */
16
16
  export type StoredSessionMeta = MachineSessionRecord;
17
17
  export declare function listSessionMetas(): StoredSessionMeta[];
18
18
  export declare function getSessionMeta(id: string): StoredSessionMeta | undefined;
19
19
  export declare function createSessionMeta(meta: StoredSessionMeta): void;
20
+ export declare function updateSessionMeta(meta: StoredSessionMeta): void;
20
21
  export declare function setSessionTitle(id: string, title: string): void;
21
22
  export declare function removeSessionMeta(id: string): void;
22
23
  /** {@link SessionRegistry} over the same `sessions` table — the form channels
@@ -1,5 +1,19 @@
1
+ /**
2
+ * The unified machine-session record (SQLite) — one row per session regardless
3
+ * of origin (the web console AND every channel). Holds identity/meta (source /
4
+ * provider / agent / inline config / model / title / …) so the **session list survives a
5
+ * daemon restart**; the transcript lives in `session_items` (see
6
+ * {@link import("./session-log-store.js")}). Runtime `status` is intentionally
7
+ * not stored — it is reconstructed as `idle` after a restart.
8
+ *
9
+ * Exposes both the standalone functions the control-plane deps wire in and a
10
+ * {@link SessionRegistry} adapter ({@link sessionRegistry}) injected into channels
11
+ * via the `ChannelContext`, so a channel registers its sessions the same way the
12
+ * console does.
13
+ */
14
+ import { normalizeSessionTitle, } from "@rynx-ai/core";
1
15
  import { db } from "./db.js";
2
- const COLS = "id, agent, config, model, reasoning_effort, title, source, created_at, updated_at";
16
+ const COLS = "id, provider, agent, config, model, reasoning_effort, title, source, created_at, updated_at";
3
17
  export function listSessionMetas() {
4
18
  return db()
5
19
  .prepare(`SELECT ${COLS} FROM sessions ORDER BY created_at`)
@@ -13,27 +27,60 @@ export function getSessionMeta(id) {
13
27
  export function createSessionMeta(meta) {
14
28
  db()
15
29
  .prepare(`INSERT OR IGNORE INTO sessions
16
- (id, agent, config, model, reasoning_effort, title, source, created_at, updated_at)
17
- VALUES (@id, @agent, @config, @model, @reasoning_effort, @title, @source, @created_at, @updated_at)`)
30
+ (id, provider, agent, config, model, reasoning_effort, title, source, created_at, updated_at)
31
+ VALUES (@id, @provider, @agent, @config, @model, @reasoning_effort, @title, @source, @created_at, @updated_at)`)
18
32
  .run({
19
33
  id: meta.id,
34
+ provider: meta.provider ?? null,
20
35
  agent: meta.agent ?? null,
21
36
  config: meta.config ? JSON.stringify(meta.config) : null,
22
37
  model: meta.model ?? null,
23
38
  reasoning_effort: meta.reasoningEffort ?? null,
24
- title: meta.title ?? null,
39
+ title: normalizeSessionTitle(meta.title) ?? null,
25
40
  source: meta.source,
26
41
  created_at: meta.createdAt,
27
42
  updated_at: meta.updatedAt ?? meta.createdAt,
28
43
  });
29
44
  }
45
+ export function updateSessionMeta(meta) {
46
+ db()
47
+ .prepare(`UPDATE sessions
48
+ SET provider = @provider,
49
+ agent = @agent,
50
+ config = @config,
51
+ model = @model,
52
+ reasoning_effort = @reasoning_effort,
53
+ title = @title,
54
+ source = @source,
55
+ updated_at = @updated_at
56
+ WHERE id = @id`)
57
+ .run({
58
+ id: meta.id,
59
+ provider: meta.provider ?? null,
60
+ agent: meta.agent ?? null,
61
+ config: meta.config ? JSON.stringify(meta.config) : null,
62
+ model: meta.model ?? null,
63
+ reasoning_effort: meta.reasoningEffort ?? null,
64
+ title: normalizeSessionTitle(meta.title) ?? null,
65
+ source: meta.source,
66
+ updated_at: meta.updatedAt ?? new Date().toISOString(),
67
+ });
68
+ }
30
69
  export function setSessionTitle(id, title) {
31
70
  db()
32
71
  .prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
33
- .run(title, new Date().toISOString(), id);
72
+ .run(normalizeSessionTitle(title), new Date().toISOString(), id);
34
73
  }
35
74
  export function removeSessionMeta(id) {
36
- db().prepare("DELETE FROM sessions WHERE id = ?").run(id);
75
+ db().transaction(() => {
76
+ const now = new Date().toISOString();
77
+ db()
78
+ .prepare(`UPDATE plugin_session_launch_operations
79
+ SET state = 'deleted', updated_at = ?, deleted_at = ?
80
+ WHERE session_id = ? AND state = 'active'`)
81
+ .run(now, now, id);
82
+ db().prepare("DELETE FROM sessions WHERE id = ?").run(id);
83
+ }).immediate();
37
84
  }
38
85
  /** {@link SessionRegistry} over the same `sessions` table — the form channels
39
86
  * receive through their `ChannelContext`. */
@@ -47,11 +94,14 @@ export const sessionRegistry = {
47
94
  function rowToMeta(row) {
48
95
  return {
49
96
  id: row.id,
97
+ provider: row.provider === "codex" || row.provider === "claude" ? row.provider : undefined,
50
98
  agent: row.agent ?? undefined,
51
99
  config: row.config ? JSON.parse(row.config) : undefined,
52
100
  model: row.model ?? undefined,
53
101
  reasoningEffort: (row.reasoning_effort ?? undefined),
54
- title: row.title ?? undefined,
102
+ // Normalize on read as well so pre-boundary database rows cannot poison an
103
+ // entire Remote Session list page after upgrading the daemon.
104
+ title: normalizeSessionTitle(row.title ?? undefined),
55
105
  source: row.source ?? "console",
56
106
  createdAt: row.created_at,
57
107
  updatedAt: row.updated_at ?? row.created_at,
package/dist/setup.js CHANGED
@@ -17,7 +17,7 @@ import { AGENT_RUNTIME_IDS, getRuntimeProfile, listAgentSpecs, loadConfig, resol
17
17
  import { listChannels } from "./channel-store.js";
18
18
  import { dbPath } from "./db.js";
19
19
  import { listInstances } from "./instance-store.js";
20
- import { listPlugins } from "./plugin-store.js";
20
+ import { isRunnablePluginRecord, listPlugins } from "./plugin-store.js";
21
21
  import { formatControlUrl } from "./pm2.js";
22
22
  const MIN_NODE_MAJOR = 20;
23
23
  // ── rynx setup — interactive wizard ─────────────────────────────────────────
@@ -26,7 +26,7 @@ export async function runSetup() {
26
26
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
27
27
  // Non-interactive (CI / piped): never block on a prompt — just ensure defaults.
28
28
  if (!existsSync(rynxConfigFile()))
29
- writeConfig({ HOST: "0.0.0.0", PORT: 3000, LOG_LEVEL: "info" });
29
+ writeConfig({ HOST: "127.0.0.1", PORT: 3000, LOG_LEVEL: "info" });
30
30
  console.log(`非交互环境:已确保 ${rynxConfigFile()} 存在(默认值)。` +
31
31
  "在终端(TTY)下重跑 `rynx setup` 可分步配置,或直接编辑该文件。");
32
32
  return 0;
@@ -81,12 +81,12 @@ export async function runSetup() {
81
81
  if (p.isCancel(port))
82
82
  return bail();
83
83
  cfg.PORT = Number(port);
84
- cfg.HOST = cfg.HOST ?? "0.0.0.0";
84
+ cfg.HOST = cfg.HOST ?? "127.0.0.1";
85
85
  cfg.LOG_LEVEL = cfg.LOG_LEVEL ?? "info";
86
86
  writeConfig(cfg);
87
87
  p.log.success(`已写入 ${rynxConfigFile()}(chmod 600)`);
88
88
  p.log.message(`HOST=${cfg.HOST} PORT=${cfg.PORT} AGENT_RUNTIME=${cfg.AGENT_RUNTIME ?? "(默认)"}`);
89
- const url = formatControlUrl(String(cfg.HOST ?? "0.0.0.0"), Number(cfg.PORT ?? 3000));
89
+ const url = formatControlUrl(String(cfg.HOST ?? "127.0.0.1"), Number(cfg.PORT ?? 3000));
90
90
  p.note([
91
91
  `启动常驻服务 rynx start`,
92
92
  `打开控制台 ${url}`,
@@ -146,6 +146,11 @@ export async function runDoctor() {
146
146
  else {
147
147
  const plugins = listPlugins();
148
148
  p.log.info(`插件:${plugins.length} 个${plugins.length ? `(${plugins.map((pl) => pl.name).join(", ")})` : ""}`);
149
+ const legacyPlugins = plugins.filter((plugin) => !isRunnablePluginRecord(plugin));
150
+ if (legacyPlugins.length > 0) {
151
+ p.log.warn(`需重新安装的旧插件:${legacyPlugins.map((plugin) => plugin.name).join(", ")}`);
152
+ warn++;
153
+ }
149
154
  const channels = listChannels();
150
155
  const configured = channels.filter((c) => c.options && Object.keys(c.options).length > 0).length;
151
156
  p.log.info(`channel:${channels.length} 个(${configured} 已配置凭据) · instance:${listInstances().length} 个`);
@@ -12,7 +12,7 @@ const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
12
12
  /** A single file's contents are capped to keep responses small. */
13
13
  const MAX_SKILL_FILE_BYTES = 256 * 1024;
14
14
  export function listCatalogSkills() {
15
- return scanSkillsDir(rynxSkillsDir());
15
+ return listMergedSkills();
16
16
  }
17
17
  /** Install a skill (or a repo's skills) into the catalog per a full install
18
18
  * recipe; delegates to the core catalog layer (which also writes each skill's
@@ -20,7 +20,7 @@ export function listCatalogSkills() {
20
20
  * the refreshed catalog. */
21
21
  export async function installCatalogSkill(recipe) {
22
22
  await installIntoCatalog(recipe, rynxSkillsDir());
23
- return scanSkillsDir(rynxSkillsDir());
23
+ return listMergedSkills();
24
24
  }
25
25
  /** Detail for one catalog skill: its metadata + the file tree under its dir. */
26
26
  export async function getCatalogSkill(name) {
@@ -30,6 +30,7 @@ export async function getCatalogSkill(name) {
30
30
  const meta = await readSkillMeta(dir);
31
31
  if (!meta)
32
32
  return null;
33
+ const ref = meta.ref;
33
34
  const files = await walkSkillFiles(dir, dir);
34
35
  let frontmatter;
35
36
  try {
@@ -41,7 +42,7 @@ export async function getCatalogSkill(name) {
41
42
  return {
42
43
  name: meta.name,
43
44
  description: meta.description,
44
- ...(meta.ref ? { ref: meta.ref } : {}),
45
+ ...(ref ? { ref } : {}),
45
46
  frontmatter,
46
47
  totalSize: files.reduce((sum, f) => sum + f.size, 0),
47
48
  files,
@@ -63,7 +64,7 @@ async function walkSkillFiles(root, dir) {
63
64
  export async function readCatalogSkillFile(name, relPath) {
64
65
  if (!SKILL_NAME_PATTERN.test(name))
65
66
  throw new Error(`invalid skill name: ${name}`);
66
- const dir = path.resolve(rynxSkillsDir(), name);
67
+ const dir = path.resolve(path.join(rynxSkillsDir(), name));
67
68
  const target = path.resolve(dir, relPath);
68
69
  if (target !== dir && !target.startsWith(dir + path.sep)) {
69
70
  throw new Error("path escapes the skill directory");
@@ -78,6 +79,9 @@ export async function readCatalogSkillFile(name, relPath) {
78
79
  throw new Error("binary file — not viewable");
79
80
  return { path: relPath, content: buf.toString("utf8") };
80
81
  }
82
+ async function listMergedSkills() {
83
+ return (await scanSkillsDir(rynxSkillsDir())).sort((a, b) => a.name.localeCompare(b.name));
84
+ }
81
85
  /** Remove a catalog skill by name. Returns false if it wasn't present. */
82
86
  export async function removeCatalogSkill(name) {
83
87
  if (!SKILL_NAME_PATTERN.test(name))
package/dist/update.d.ts CHANGED
@@ -1,11 +1,20 @@
1
- import { type UpdateCheck } from "@rynx-ai/core";
2
1
  export interface UpdateOptions {
3
2
  check?: boolean;
4
3
  /** With `check`, emit a typed {@link UpdateCheck} as JSON instead of a human line. */
5
4
  json?: boolean;
6
5
  version?: string;
7
- resultFile?: string;
8
6
  }
7
+ export type UpdateCheck = {
8
+ status: "up_to_date";
9
+ current: string;
10
+ } | {
11
+ status: "behind";
12
+ current: string;
13
+ latest: string;
14
+ } | {
15
+ status: "error";
16
+ detail: string;
17
+ };
9
18
  /** Is `latest` a newer release than `current`? Numeric major.minor.patch (prerelease ignored). */
10
19
  export declare function isNewer(latest: string, current: string): boolean;
11
20
  /** Typed `--check` result a channel consumes via `--check --json`. */
package/dist/update.js CHANGED
@@ -7,12 +7,8 @@
7
7
  * rynx update [version] upgrade (to a version, or latest) + restart
8
8
  * rynx update --check report status only (human one-liner)
9
9
  * rynx update --check --json report status only (typed UpdateCheck JSON)
10
- * rynx update --result-file <path> after finishing, write the outcome JSON to
11
- * <path> (channel-agnostic UpdateOutcome; a
12
- * plugin reads it post-restart to notify a chat)
13
10
  */
14
11
  import { spawnSync } from "node:child_process";
15
- import { mkdirSync, writeFileSync } from "node:fs";
16
12
  import { createRequire } from "node:module";
17
13
  import { dirname, sep } from "node:path";
18
14
  import { loadConfig } from "@rynx-ai/core";
@@ -98,19 +94,6 @@ async function verifyHealthy() {
98
94
  }
99
95
  return false;
100
96
  }
101
- /** Write the post-restart outcome to `--result-file` (best-effort; stamps `at`). */
102
- function writeOutcome(file, outcome) {
103
- if (!file)
104
- return;
105
- try {
106
- mkdirSync(dirname(file), { recursive: true });
107
- const full = { ...outcome, at: new Date().toISOString() };
108
- writeFileSync(file, `${JSON.stringify(full, null, 2)}\n`);
109
- }
110
- catch {
111
- /* best-effort */
112
- }
113
- }
114
97
  /** Run `rynx update`. Returns a process exit code. */
115
98
  export async function runUpdate(opts) {
116
99
  const { name, version: current } = selfPackage();
@@ -122,13 +105,11 @@ export async function runUpdate(opts) {
122
105
  // Never `npm install -g` over a working checkout / linked dev build.
123
106
  if (isLocalDevInstall()) {
124
107
  console.error(`update: ${name} is a local dev install — update your checkout (e.g. pnpm build), not npm`);
125
- writeOutcome(opts.resultFile, { status: "fail", from: current, to: current, error: "local dev install" });
126
108
  return 1;
127
109
  }
128
110
  const target = opts.version ?? latest;
129
111
  if (!target) {
130
112
  console.error(`update: could not resolve a target version for ${name} (registry unreachable?)`);
131
- writeOutcome(opts.resultFile, { status: "fail", from: current, to: current, error: "could not resolve target version" });
132
113
  return 1;
133
114
  }
134
115
  if (!opts.version && latest && !isNewer(latest, current)) {
@@ -138,12 +119,6 @@ export async function runUpdate(opts) {
138
119
  console.log(`Updating ${name}: ${current} → ${target} …`);
139
120
  if (!npmInstallGlobal(name, target)) {
140
121
  console.error("update: npm install failed");
141
- writeOutcome(opts.resultFile, { status: "fail", from: current, to: target, error: "npm install failed" });
142
- // The install failed before any restart, so the channel that spawned us never
143
- // reboots to drain the outcome. Bounce the daemon (still on the old, healthy
144
- // version) so its boot-time drain delivers the failure to chat.
145
- if (opts.resultFile)
146
- startDaemon({ force: true });
147
122
  return 1;
148
123
  }
149
124
  startDaemon({ force: true });
@@ -151,10 +126,8 @@ export async function runUpdate(opts) {
151
126
  console.error(`update: ${name}@${target} did not come back up — rolling back to ${current}`);
152
127
  npmInstallGlobal(name, current);
153
128
  startDaemon({ force: true });
154
- writeOutcome(opts.resultFile, { status: "fail", from: current, to: target, error: "did not come up; rolled back" });
155
129
  return 1;
156
130
  }
157
131
  console.log(`Updated ${name} → ${target} and restarted.`);
158
- writeOutcome(opts.resultFile, { status: "ok", from: current, to: target });
159
132
  return 0;
160
133
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.0",
4
- "description": "rynx daemon CLI: supervises the resident @rynx-ai/server via pm2 and loads channel plugins recorded in ~/.rynx/rynx.db.",
3
+ "version": "0.1.9",
4
+ "description": "Rynx daemon CLI: supervises the local agent control plane and isolated optional plugin runtimes.",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org/",
@@ -15,20 +15,28 @@
15
15
  },
16
16
  "main": "./dist/index-daemon.js",
17
17
  "exports": {
18
- "./server": {
19
- "types": "./dist/daemon-server.d.ts",
20
- "default": "./dist/daemon-server.js"
18
+ "./client": {
19
+ "types": "./dist/control-client.d.ts",
20
+ "default": "./dist/control-client.js"
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
24
  "@clack/prompts": "^1.6.0",
25
+ "@electron-internal/extract-zip": "^1.0.4",
25
26
  "better-sqlite3": "^12.11.1",
26
27
  "pm2": "^6.0.0",
27
- "zod": "^4.3.6",
28
- "@rynx-ai/core": "0.1.0",
29
- "@rynx-ai/protocol": "0.1.0",
30
- "@rynx-ai/server": "0.1.0",
31
- "@rynx-ai/emulator": "0.1.7"
28
+ "tar": "^7.5.19",
29
+ "ws": "^8.21.0",
30
+ "@rynx-ai/emulator": "0.1.9",
31
+ "@rynx-ai/core": "0.1.9",
32
+ "@rynx-ai/plugin-runner": "0.1.9",
33
+ "@rynx-ai/protocol": "0.1.9",
34
+ "@rynx-ai/plugin-sdk": "0.1.9",
35
+ "@rynx-ai/remote-runtime-client": "0.1.9",
36
+ "@rynx-ai/server": "0.1.9"
37
+ },
38
+ "devDependencies": {
39
+ "@types/ws": "^8.18.1"
32
40
  },
33
41
  "scripts": {
34
42
  "build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js dist/index-daemon.js",