@konglx/rotom 2.21.0

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 (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Shared helpers for resolving per-group on-disk paths.
3
+ *
4
+ * Centralises the "where does this group's working directory live" rule so
5
+ * both the artifacts REST endpoints and the web-terminal PTY hub agree on
6
+ * the same cwd. Previously this lived inline in api.ts; pulling it out
7
+ * avoids importing api.ts (and Express) from non-HTTP modules.
8
+ */
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ /** Root directory under which per-group working dirs (artifacts) live. */
13
+ export const ARTIFACTS_ROOT = path.join(os.homedir(), ".rotom", "artifacts");
14
+ /**
15
+ * Legacy root from before the `results → artifacts` rename. Kept as a
16
+ * read-only fallback so a group's pre-rename data still resolves correctly
17
+ * if the one-shot data migration missed it (or was run on an older DB
18
+ * whose `working_dir` column was back-filled to the legacy path).
19
+ */
20
+ const LEGACY_RESULTS_ROOT = path.join(os.homedir(), ".rotom", "results");
21
+ /** Absolute default working dir for a group — used as cwd when no override. */
22
+ export function defaultGroupWorkingDir(groupId) {
23
+ return path.join(ARTIFACTS_ROOT, groupId);
24
+ }
25
+ /**
26
+ * Resolve the directory the artifacts panel / terminal should use for a group.
27
+ *
28
+ * Prefers the group's configured `working_dir` (an absolute path the agent
29
+ * actually runs in), falling back to the default `~/.rotom/artifacts/<groupId>`.
30
+ *
31
+ * Backward-compat: if neither override nor the default artifacts dir exists
32
+ * on disk, fall back to the legacy `~/.rotom/results/<groupId>` (covers
33
+ * `working_dir` values persisted against the pre-rename path).
34
+ */
35
+ export function resolveGroupArtifactRoot(db, groupId) {
36
+ const group = db.getGroupById(groupId);
37
+ const dir = group?.working_dir?.trim();
38
+ if (dir && path.isAbsolute(dir)) {
39
+ if (fs.existsSync(dir))
40
+ return dir;
41
+ // Stored working_dir is stale — fall through to the default + legacy
42
+ // fallback below so a pre-rename group keeps resolving.
43
+ }
44
+ const defaultDir = defaultGroupWorkingDir(groupId);
45
+ if (fs.existsSync(defaultDir))
46
+ return defaultDir;
47
+ const legacyDir = path.join(LEGACY_RESULTS_ROOT, groupId);
48
+ if (fs.existsSync(legacyDir))
49
+ return legacyDir;
50
+ return defaultDir;
51
+ }
52
+ /**
53
+ * 内置 repo 上下文(migration 051)。master 在 dispatch issue 时调这个,
54
+ * 解析该 issue 实际使用的 repo 配置,随 WS 消息下发给 executor。
55
+ *
56
+ * 优先级:
57
+ * - issue.repo_url 非空 → 用 issue 级覆盖(连同 issue.repo_branch)
58
+ * - 否则 → 用 group.repo_url + group.repo_default_branch + group.extra_repos
59
+ * - group 也没配 → 返回 null(worker 走老路径,无 worktree)
60
+ *
61
+ * extraRepos 只在 group 级定义(issue 不支持覆盖 extra),解析失败(JSON 损坏)时
62
+ * 静默忽略该 extra,不影响 primary worktree 创建。
63
+ */
64
+ export function resolveIssueRepoCtx(db, issue) {
65
+ const group = db.getGroupById(issue.group_id);
66
+ if (!group)
67
+ return null;
68
+ const repoUrl = issue.repo_url?.trim() || group.repo_url?.trim() || "";
69
+ if (!repoUrl)
70
+ return null;
71
+ const repoBranch = issue.repo_branch?.trim() || group.repo_default_branch?.trim() || undefined;
72
+ // worktree_mode: 'issue' 显式 opt-in,其余(含 null)都归 'group'(默认轻量模式)
73
+ const worktreeMode = group.worktree_mode === "issue" ? "issue" : "group";
74
+ let extraRepos;
75
+ if (group.extra_repos) {
76
+ try {
77
+ const parsed = JSON.parse(group.extra_repos);
78
+ if (Array.isArray(parsed)) {
79
+ extraRepos = parsed
80
+ .filter((e) => !!e && typeof e === "object"
81
+ && typeof e.id === "string" && e.id
82
+ && typeof e.url === "string" && e.url
83
+ && typeof e.mountPath === "string" && e.mountPath)
84
+ .map(e => ({
85
+ id: e.id,
86
+ url: e.url,
87
+ branch: typeof e.branch === "string" && e.branch ? e.branch : undefined,
88
+ mountPath: e.mountPath,
89
+ }));
90
+ if (extraRepos.length === 0)
91
+ extraRepos = undefined;
92
+ }
93
+ }
94
+ catch { /* malformed JSON — ignore extras */ }
95
+ }
96
+ return { repoUrl, repoBranch, extraRepos, worktreeMode };
97
+ }
98
+ /**
99
+ * 判定 agent 是否与 master 同机器。worktree 模式(migration 051)只在同机生效:
100
+ * executor 在本机维护 bare clone + worktree,跨机器时 master 不下发 repoCtx,
101
+ * worker 回退到老路径(<base>/<groupId>),避免跨机 FS 协调。
102
+ *
103
+ * 判定优先级:
104
+ * 1. agent.hostname 与本机 os.hostname() 完全相等 → 同机
105
+ * 2. agent.endpoint 是 ws://127.0.0.1 / ws://localhost → 同机
106
+ * 3. 否则 → 跨机
107
+ */
108
+ export function isAgentLocalToMaster(agent) {
109
+ if (!agent)
110
+ return false;
111
+ const localHostname = os.hostname();
112
+ if (agent.hostname && agent.hostname === localHostname)
113
+ return true;
114
+ if (agent.endpoint) {
115
+ try {
116
+ const u = new URL(agent.endpoint);
117
+ if (u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "0.0.0.0")
118
+ return true;
119
+ }
120
+ catch { /* malformed endpoint — ignore */ }
121
+ }
122
+ return false;
123
+ }
124
+ /**
125
+ * 解析 group 级 repo 上下文(chat 路径用,无 issue)。与 resolveIssueRepoCtxLocalOnly
126
+ * 类似但不依赖 issue row——只看 group 配置 + agent 同机判定。
127
+ *
128
+ * chat 路径:master dispatch a2a_message 时调这个,把 repoCtx 注入消息,worker 收到后
129
+ * 在 resolveChatCwd 里走 group 模式 worktree(共享 worktree,不依赖 issueId)。
130
+ */
131
+ export function resolveGroupRepoCtxLocalOnly(db, groupId, agentName) {
132
+ const group = db.getGroupById(groupId);
133
+ if (!group)
134
+ return null;
135
+ const repoUrl = group.repo_url?.trim() || "";
136
+ if (!repoUrl)
137
+ return null;
138
+ const agent = db.getAgentByName(agentName);
139
+ if (!isAgentLocalToMaster(agent))
140
+ return null;
141
+ const repoBranch = group.repo_default_branch?.trim() || undefined;
142
+ const worktreeMode = group.worktree_mode === "issue" ? "issue" : "group";
143
+ let extraRepos;
144
+ if (group.extra_repos) {
145
+ try {
146
+ const parsed = JSON.parse(group.extra_repos);
147
+ if (Array.isArray(parsed)) {
148
+ extraRepos = parsed
149
+ .filter((e) => !!e && typeof e === "object"
150
+ && typeof e.id === "string" && e.id
151
+ && typeof e.url === "string" && e.url
152
+ && typeof e.mountPath === "string" && e.mountPath)
153
+ .map(e => ({
154
+ id: e.id,
155
+ url: e.url,
156
+ branch: typeof e.branch === "string" && e.branch ? e.branch : undefined,
157
+ mountPath: e.mountPath,
158
+ }));
159
+ if (extraRepos.length === 0)
160
+ extraRepos = undefined;
161
+ }
162
+ }
163
+ catch { /* malformed JSON — ignore extras */ }
164
+ }
165
+ return { repoUrl, repoBranch, extraRepos, worktreeMode };
166
+ }
167
+ /**
168
+ * 解析 issue 的 repo 上下文,但只在 assignee 与 master 同机器时返回非 null。
169
+ * 跨机器 agent 调用方拿不到 repoCtx,worker 走老路径(不启 worktree)。
170
+ */
171
+ export function resolveIssueRepoCtxLocalOnly(db, issue) {
172
+ if (!issue.assigned_to)
173
+ return null;
174
+ const agent = db.getAgentByName(issue.assigned_to);
175
+ if (!isAgentLocalToMaster(agent))
176
+ return null;
177
+ return resolveIssueRepoCtx(db, issue);
178
+ }
179
+ /**
180
+ * Resolve the working directory for a specific (group, agent) pair.
181
+ *
182
+ * Three-tier fallback:
183
+ * 1. per-(group, agent) override in `group_member_settings`
184
+ * 2. group's `working_dir` (when set to an absolute path)
185
+ * 3. `~/.rotom/artifacts/<groupId>` default (with legacy results fallback
186
+ * for groups whose data migration was incomplete)
187
+ *
188
+ * Used at issue-assignment time to compute the cwd that should be recorded
189
+ * on the issue. Executor workers continue to use their own per-group mapping
190
+ * (`executor.config.json.workingDirMap`); this function is the master-side
191
+ * authoritative resolution only.
192
+ */
193
+ export function resolveGroupAgentWorkingDir(db, groupId, agentName) {
194
+ const override = db.getGroupMemberSetting(groupId, agentName);
195
+ if (override && fs.existsSync(override))
196
+ return override;
197
+ const group = db.getGroupById(groupId);
198
+ const dir = group?.working_dir?.trim();
199
+ if (dir && path.isAbsolute(dir) && fs.existsSync(dir))
200
+ return dir;
201
+ const defaultDir = defaultGroupWorkingDir(groupId);
202
+ if (fs.existsSync(defaultDir))
203
+ return defaultDir;
204
+ const legacyDir = path.join(LEGACY_RESULTS_ROOT, groupId);
205
+ if (fs.existsSync(legacyDir))
206
+ return legacyDir;
207
+ return defaultDir;
208
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Digital Employee Mesh — Offline message queue
3
+ *
4
+ * Thin wrapper over db methods + JSON → OfflineMsg conversion.
5
+ */
6
+ export class OfflineQueue {
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ /** Enqueue a message for an offline agent. Returns false if limit reached. */
12
+ enqueue(targetAgentId, fromName, fromDomain, payload, routeType) {
13
+ return this.db.enqueueOffline(targetAgentId, fromName, fromDomain, JSON.stringify(payload), routeType);
14
+ }
15
+ /** Pop all pending messages for an agent (called on reconnect). */
16
+ pop(targetAgentId) {
17
+ const rows = this.db.popOffline(targetAgentId);
18
+ return rows.map((row) => {
19
+ let payload;
20
+ try {
21
+ payload = JSON.parse(row.payload);
22
+ }
23
+ catch {
24
+ payload = { message: row.payload || "" };
25
+ }
26
+ return {
27
+ from: {
28
+ name: row.from_name,
29
+ domain: row.from_domain || undefined,
30
+ status: "offline",
31
+ },
32
+ payload,
33
+ routeType: row.route_type || "unknown",
34
+ createdAt: row.created_at,
35
+ };
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * OPC bootstrap — 首次启动时自动建立"个人 OPC"最小可用环境。
3
+ *
4
+ * Phase 1 的核心:让 `mesh-master` 一命令起来就是一个完整 OPC ——
5
+ * 有本机 master 身份、有默认 agent(免 token)、有默认群。
6
+ * 用户无需任何配置就能开箱即用,断网也能完整工作。
7
+ *
8
+ * 这个模块也是 Phase 2 federation 的接入点 —— 后续加 ensureLocalExecutor、
9
+ * federation client 等都在这里挂。
10
+ */
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import fs from "node:fs";
14
+ import { randomUUID } from "node:crypto";
15
+ import { spawn } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+ import { REAL_PERSONS } from "../shared/protocol/enums.js";
18
+ import { createLogger } from "../shared/logger.js";
19
+ const log = createLogger("opc-bootstrap");
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ /**
22
+ * 把 agents 表里 hostname 为 NULL 的行回填本机 hostname。
23
+ * 这是 migration 055 的运行时补充 —— migration 跑的时候 master_node 表
24
+ * 还没有身份行,SQL UPDATE 找不到 hostname 来源,所以放到 TS 层处理。
25
+ */
26
+ export function backfillAgentsHostname(db, hostname) {
27
+ const result = db.db.prepare("UPDATE agents SET hostname = ? WHERE hostname IS NULL").run(hostname);
28
+ return result.changes;
29
+ }
30
+ /**
31
+ * 若 agents 表为空,自动建一个默认 agent(免 token,靠本机信任认证)。
32
+ * 已有 agent 时 no-op。
33
+ *
34
+ * name = os.userInfo().username(若命中 REAL_PERSONS 则标 category="真人")。
35
+ */
36
+ export function ensureDefaultAgent(db, identity) {
37
+ const existing = db.listAgents();
38
+ if (existing.length > 0)
39
+ return null;
40
+ const userInfo = os.userInfo();
41
+ const name = userInfo.username || "default";
42
+ const id = randomUUID();
43
+ const isRealPerson = REAL_PERSONS.includes(name);
44
+ const profile = JSON.stringify(isRealPerson ? { category: "真人" } : {});
45
+ db.insertAgent({
46
+ id,
47
+ name,
48
+ hostname: identity.hostname,
49
+ tokenHash: "",
50
+ token: "",
51
+ profile,
52
+ });
53
+ log.info(`Created default agent "${name}" (hostname=${identity.hostname}${isRealPerson ? ", realPerson" : ""})`);
54
+ return { id, name };
55
+ }
56
+ /**
57
+ * 若无 group,创建 `Local` 默认群并把默认 agent 加进去。
58
+ * 已有 group 时 no-op。
59
+ */
60
+ export function ensureDefaultGroup(db, defaultAgentName) {
61
+ const groups = db.listGroups();
62
+ if (groups.length > 0)
63
+ return null;
64
+ const id = randomUUID();
65
+ const name = "Local";
66
+ db.createGroup(id, name, defaultAgentName);
67
+ if (defaultAgentName) {
68
+ db.addGroupMembers(id, [defaultAgentName]);
69
+ }
70
+ log.info(`Created default group "${name}"${defaultAgentName ? ` with member "${defaultAgentName}"` : ""}`);
71
+ return { id, name };
72
+ }
73
+ /**
74
+ * OPC bootstrap 完整流程:
75
+ * 1. 写 master_node 身份行
76
+ * 2. 回填 agents.hostname
77
+ * 3. 默认 agent(若空)
78
+ * 4. 默认 group(若空)
79
+ *
80
+ * 在 master 启动 main() 里 DB 初始化之后立即调用 —— 此处 mesh.db 已跑完
81
+ * migration 054/055,master_node 表存在,可以安全写入。
82
+ */
83
+ export function runOpcBootstrap(db, identity) {
84
+ // teamName 兜底:如果用户没配 ROTOM_TEAM_NAME / master.json,查本机真人 agent
85
+ // (profile.category="真人"),用其 name + "团队" 作默认(如"西花团队")。
86
+ // 这是"每台机器 = 一个真人 + 一个团队"语义的体现:团队名跟随主理人。
87
+ let teamName = identity.teamName;
88
+ if (!teamName) {
89
+ const agents = db.listAgents();
90
+ const realPerson = agents.find((a) => {
91
+ try {
92
+ const p = a.profile ? JSON.parse(a.profile) : {};
93
+ return p.category === "真人";
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ });
99
+ if (realPerson) {
100
+ teamName = `${realPerson.name}团队`;
101
+ log.info(`Derived teamName from real-person agent: "${teamName}"`);
102
+ }
103
+ }
104
+ if (!teamName) {
105
+ // 最后兜底:用 hostname(避免 master_node.team_name 为 NULL)
106
+ teamName = identity.hostname;
107
+ }
108
+ db.upsertMasterNode({
109
+ id: identity.id,
110
+ hostname: identity.hostname,
111
+ role: identity.role,
112
+ teamName,
113
+ });
114
+ const backfilledAgents = backfillAgentsHostname(db, identity.hostname);
115
+ if (backfilledAgents > 0) {
116
+ log.info(`Backfilled hostname=${identity.hostname} for ${backfilledAgents} agent row(s)`);
117
+ }
118
+ const defaultAgent = ensureDefaultAgent(db, identity);
119
+ const defaultGroup = ensureDefaultGroup(db, defaultAgent?.name);
120
+ log.info(`OPC ready: masterId=${identity.id} hostname=${identity.hostname} role=${identity.role}`);
121
+ return {
122
+ masterId: identity.id,
123
+ hostname: identity.hostname,
124
+ role: identity.role,
125
+ defaultAgent: defaultAgent ?? undefined,
126
+ defaultGroup: defaultGroup ?? undefined,
127
+ backfilledAgents,
128
+ };
129
+ }
130
+ function isProcessAlive(pid) {
131
+ try {
132
+ process.kill(pid, 0);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ /**
140
+ * OPC 模式下由 master 自动拉起本机 executor 子进程,免去用户手动 nohup。
141
+ *
142
+ * 配置选择:
143
+ * - 用户已写 ~/.rotom/executor.config.json → 用它 spawn(尊重用户配置)
144
+ * - 否则生成 .auto-executor.json(token=null,走 isLoopback 信任)+ spawn
145
+ *
146
+ * 跳过 spawn 的条件:
147
+ * - ROTOM_FEDERATION_DISABLED=1(纯 standalone)
148
+ * - 已有 local-executor.pid 指向存活进程
149
+ *
150
+ * 子进程生命周期与 master 绑定:master 退出时自动 SIGTERM + 5s 后 SIGKILL。
151
+ */
152
+ export function ensureLocalExecutor(opts) {
153
+ const { rotomHome, masterPort, defaultAgentName } = opts;
154
+ if (process.env.ROTOM_FEDERATION_DISABLED === "1") {
155
+ log.info("ROTOM_FEDERATION_DISABLED=1 — skipping auto executor spawn");
156
+ return null;
157
+ }
158
+ const runDir = path.join(rotomHome, "run");
159
+ fs.mkdirSync(runDir, { recursive: true });
160
+ const pidFile = path.join(runDir, "local-executor.pid");
161
+ try {
162
+ if (fs.existsSync(pidFile)) {
163
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
164
+ if (pid && isProcessAlive(pid)) {
165
+ log.info(`Local executor already running (PID ${pid}) — skipping spawn`);
166
+ return null;
167
+ }
168
+ }
169
+ }
170
+ catch { /* fallthrough to spawn */ }
171
+ // 1. 选 config 路径:用户已配 executor.config.json → 用它;否则生成 .auto-executor.json
172
+ // .auto-executor.json 走 scanClis 模式 —— executor 启动时扫描本机已安装的
173
+ // claude/codex/hermes/openclaw/pi,为每个 CLI 注册一个 agent(name 默认 = CLI 名)。
174
+ const userConfigPath = path.join(rotomHome, "executor.config.json");
175
+ const autoConfigPath = path.join(rotomHome, ".auto-executor.json");
176
+ let configPath;
177
+ if (fs.existsSync(userConfigPath)) {
178
+ configPath = userConfigPath;
179
+ log.info(`Using user config ${userConfigPath} for auto-spawned executor`);
180
+ }
181
+ else {
182
+ const workspaceDir = path.join(rotomHome, "workspace");
183
+ fs.mkdirSync(workspaceDir, { recursive: true });
184
+ const autoConfig = {
185
+ master: `ws://127.0.0.1:${masterPort}`,
186
+ token: null, // 本机信任模式
187
+ scanClis: true,
188
+ workingDir: workspaceDir,
189
+ };
190
+ fs.writeFileSync(autoConfigPath, JSON.stringify(autoConfig, null, 2) + "\n", "utf-8");
191
+ configPath = autoConfigPath;
192
+ log.info(`Generated ${autoConfigPath} (scanClis mode — executor will register one agent per installed CLI)`);
193
+ }
194
+ // 2. 定位 executor 入口:dist/master/X.js → dist/executor/index.js;src/master/X.ts → src/executor/index.ts
195
+ const isTs = __filename.endsWith(".ts");
196
+ const executorExt = isTs ? "ts" : "js";
197
+ const executorPath = path.resolve(path.dirname(__filename), `../executor/index.${executorExt}`);
198
+ if (!fs.existsSync(executorPath)) {
199
+ log.warn(`Executor entry not found at ${executorPath} — skipping auto spawn`);
200
+ return null;
201
+ }
202
+ // 3. 准备日志文件(stdio 重定向)
203
+ const logDir = path.join(rotomHome, "logs");
204
+ fs.mkdirSync(logDir, { recursive: true });
205
+ const logFilePath = path.join(logDir, "local-executor.log");
206
+ const logFd = fs.openSync(logFilePath, "a");
207
+ // 4. spawn
208
+ // ts 模式跑 tsx(走 Node 的 --import tsx 加载器);js 模式直接 node
209
+ const args = isTs
210
+ ? ["--import", "tsx", executorPath, "--config", configPath]
211
+ : [executorPath, "--config", configPath];
212
+ const child = spawn(process.execPath, args, {
213
+ stdio: ["ignore", logFd, logFd],
214
+ env: { ...process.env, ROTOM_HOME: rotomHome },
215
+ detached: false,
216
+ });
217
+ fs.closeSync(logFd); // 父进程关闭 fd,子进程已继承
218
+ if (!child.pid) {
219
+ log.error("Failed to spawn local executor");
220
+ return null;
221
+ }
222
+ fs.writeFileSync(pidFile, String(child.pid), "utf-8");
223
+ log.info(`Spawned local executor (PID ${child.pid}) — log: ${logFilePath}`);
224
+ child.on("exit", (code, signal) => {
225
+ log.info(`Local executor exited (code=${code} signal=${signal})`);
226
+ try {
227
+ fs.unlinkSync(pidFile);
228
+ }
229
+ catch { /* already removed */ }
230
+ });
231
+ const stop = () => {
232
+ if (!child.killed && child.exitCode === null && child.signalCode === null) {
233
+ log.info(`Stopping local executor (PID ${child.pid})...`);
234
+ child.kill("SIGTERM");
235
+ const killTimer = setTimeout(() => {
236
+ if (!child.killed && child.exitCode === null && child.signalCode === null) {
237
+ log.warn(`Local executor (PID ${child.pid}) did not exit in 5s, sending SIGKILL`);
238
+ child.kill("SIGKILL");
239
+ }
240
+ }, 5_000);
241
+ child.once("exit", () => clearTimeout(killTimer));
242
+ }
243
+ };
244
+ return { child, stop };
245
+ }