@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,202 @@
1
+ /**
2
+ * rotom issue — issue list/show/events/messages/comment/create/update/cancel/delete.
3
+ */
4
+ import { api, printJson, printTable, fail, flagStr, flagInt, requireFlag, pretty, } from "./common.js";
5
+ import { route, qs, usage, unknownSubcommand } from "./routes.js";
6
+ import { ISSUE_STATUSES } from "../shared/constants.js";
7
+ export async function cmdIssue(agent, rest, flags) {
8
+ const sub = rest[0];
9
+ if (sub === "list") {
10
+ const groupId = rest[1];
11
+ if (!groupId)
12
+ usage("issue list", "<groupId> [--status S] [--type task]");
13
+ const status = flagStr(flags, "status");
14
+ const type = flagStr(flags, "type");
15
+ const routePath = `${route("/groups/:groupId/issues", groupId)}${qs({ status, type })}`;
16
+ const data = await api(agent, "GET", routePath);
17
+ printTable(data.map((i) => ({
18
+ id: i.id,
19
+ type: i.type,
20
+ status: i.status,
21
+ priority: i.priority,
22
+ title: (i.title || "").slice(0, 60),
23
+ })), ["id", "type", "status", "priority", "title"]);
24
+ return;
25
+ }
26
+ if (sub === "show") {
27
+ const id = rest[1];
28
+ if (!id)
29
+ usage("issue show", "<issueId>");
30
+ const data = await api(agent, "GET", route("/issues/:id", id));
31
+ printJson(data);
32
+ return;
33
+ }
34
+ if (sub === "events") {
35
+ const id = rest[1];
36
+ if (!id)
37
+ usage("issue events", "<issueId> [--content-len N] [--no-clean]");
38
+ const contentLen = flagInt(flags, "content-len") ?? 200;
39
+ const clean = flags["clean"] !== false;
40
+ const data = await api(agent, "GET", route("/issues/:id/events", id));
41
+ printTable(data.map((e) => {
42
+ let content = (e.content || "").replace(/\s+/g, " ");
43
+ if (clean) {
44
+ content = content
45
+ .replace(/\[(\w[\w-]*(?::\w[\w-]*)?)\].*?\[\/\1\]/g, "")
46
+ .replace(/\s+/g, " ")
47
+ .trim();
48
+ }
49
+ return {
50
+ time: e.created_at,
51
+ type: e.event_type,
52
+ agent: e.agent_name,
53
+ content: content.slice(0, contentLen),
54
+ };
55
+ }), ["time", "type", "agent", "content"]);
56
+ return;
57
+ }
58
+ if (sub === "messages") {
59
+ const id = rest[1];
60
+ if (!id)
61
+ usage("issue messages", "<issueId>");
62
+ const data = await api(agent, "GET", route("/issues/:id/messages", id));
63
+ if (pretty) {
64
+ printTable(data.map((m) => {
65
+ const quoted = m.quoted
66
+ ? `> ${(m.quoted.agent_name || "").slice(0, 10)}: ${(m.quoted.content || "").slice(0, 30)}`
67
+ : "";
68
+ return {
69
+ id: m.id,
70
+ type: m.event_type,
71
+ agent: m.agent_name,
72
+ content: (m.content || "").slice(0, 60),
73
+ quoted,
74
+ created_at: m.created_at,
75
+ };
76
+ }), ["id", "type", "agent", "content", "quoted", "created_at"]);
77
+ }
78
+ else {
79
+ printJson(data);
80
+ }
81
+ return;
82
+ }
83
+ if (sub === "comment") {
84
+ const id = rest[1];
85
+ if (!id)
86
+ usage("issue comment", "<issueId> --message M [--reply-to <eventId>]");
87
+ const message = requireFlag(flags, "message");
88
+ const replyTo = flagInt(flags, "reply-to");
89
+ const data = await api(agent, "POST", route("/issues/:id/comments", id), {
90
+ agentName: agent.name, content: message, replyTo: replyTo ?? undefined,
91
+ });
92
+ printJson(data);
93
+ return;
94
+ }
95
+ if (sub === "create") {
96
+ const groupId = rest[1];
97
+ if (!groupId)
98
+ usage("issue create", "<groupId> --description D [--title T] [--priority P] [--assignee A] [--approval-policy r_allow|rw_allow] [--run]");
99
+ const title = flagStr(flags, "title");
100
+ const description = flagStr(flags, "description") || "";
101
+ const priority = flagStr(flags, "priority") || "medium";
102
+ const assignee = flagStr(flags, "assignee");
103
+ const approvalPolicyRaw = flagStr(flags, "approval-policy");
104
+ const run = flags.run === true;
105
+ if (!description && !title) {
106
+ fail("--description (or --title) is required");
107
+ }
108
+ if (approvalPolicyRaw && approvalPolicyRaw !== "r_allow" && approvalPolicyRaw !== "rw_allow") {
109
+ fail('--approval-policy must be "r_allow" or "rw_allow"');
110
+ }
111
+ if (run && !assignee) {
112
+ fail("--run requires --assignee (cannot start an unassigned issue)");
113
+ }
114
+ const body = { description, priority, createdBy: agent.name };
115
+ if (title)
116
+ body.title = title;
117
+ if (approvalPolicyRaw)
118
+ body.approvalPolicy = approvalPolicyRaw;
119
+ const created = await api(agent, "POST", route("/groups/:groupId/issues", groupId), body);
120
+ const issueId = created?.id;
121
+ if (!issueId) {
122
+ printJson(created);
123
+ return;
124
+ }
125
+ let assigned = false;
126
+ let runPushed = null;
127
+ if (assignee) {
128
+ await api(agent, "PUT", route("/issues/:id", issueId), { assignedTo: assignee });
129
+ assigned = true;
130
+ }
131
+ if (run) {
132
+ const prompt = description.trim() || (title || "").trim();
133
+ runPushed = await api(agent, "POST", route("/issues/:id/append", issueId), {
134
+ prompt, appendedBy: agent.name,
135
+ });
136
+ }
137
+ printJson({ ...created, assignedTo: assigned ? assignee : null, run: runPushed });
138
+ return;
139
+ }
140
+ if (sub === "update") {
141
+ const id = rest[1];
142
+ if (!id)
143
+ usage("issue update", "<issueId> [--title T] [--description D] [--priority low|medium|high|critical] [--assignee A | --unassign] [--approval-policy r_allow|rw_allow] [--status open|in_progress|completed|failed|cancelled]");
144
+ const title = flagStr(flags, "title");
145
+ const description = flagStr(flags, "description");
146
+ const priority = flagStr(flags, "priority");
147
+ const assignee = flagStr(flags, "assignee");
148
+ const unassign = flags.unassign === true;
149
+ const approvalPolicyRaw = flagStr(flags, "approval-policy");
150
+ const statusRaw = flagStr(flags, "status");
151
+ if (assignee !== undefined && unassign) {
152
+ fail("--assignee and --unassign are mutually exclusive");
153
+ }
154
+ if (priority !== undefined && !["low", "medium", "high", "critical"].includes(priority)) {
155
+ fail(`--priority must be one of low|medium|high|critical (got: ${priority})`);
156
+ }
157
+ if (approvalPolicyRaw !== undefined && approvalPolicyRaw !== "r_allow" && approvalPolicyRaw !== "rw_allow") {
158
+ fail(`--approval-policy must be "r_allow" or "rw_allow" (got: ${approvalPolicyRaw})`);
159
+ }
160
+ if (statusRaw !== undefined && !ISSUE_STATUSES.includes(statusRaw)) {
161
+ fail(`--status must be one of ${ISSUE_STATUSES.join("|")} (got: ${statusRaw})`);
162
+ }
163
+ const body = {};
164
+ if (title !== undefined)
165
+ body.title = title;
166
+ if (description !== undefined)
167
+ body.description = description;
168
+ if (priority !== undefined)
169
+ body.priority = priority;
170
+ if (assignee !== undefined)
171
+ body.assignedTo = assignee;
172
+ if (unassign)
173
+ body.assignedTo = null;
174
+ if (approvalPolicyRaw !== undefined)
175
+ body.approvalPolicy = approvalPolicyRaw;
176
+ if (statusRaw !== undefined)
177
+ body.status = statusRaw;
178
+ if (Object.keys(body).length === 0) {
179
+ fail("no fields to update — pass at least one flag");
180
+ }
181
+ const data = await api(agent, "PUT", route("/issues/:id", id), body);
182
+ printJson(data);
183
+ return;
184
+ }
185
+ if (sub === "cancel") {
186
+ const id = rest[1];
187
+ if (!id)
188
+ usage("issue cancel", "<issueId>");
189
+ const data = await api(agent, "PUT", route("/issues/:id", id), { status: "cancelled" });
190
+ printJson(data);
191
+ return;
192
+ }
193
+ if (sub === "delete") {
194
+ const id = rest[1];
195
+ if (!id)
196
+ usage("issue delete", "<issueId>");
197
+ const data = await api(agent, "DELETE", route("/issues/:id", id));
198
+ printJson(data);
199
+ return;
200
+ }
201
+ unknownSubcommand("issue", sub);
202
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * rotom join — 首次申请 token 落盘到 ~/.rotom/
3
+ *
4
+ * 给"本地交互式 codex 作为 mesh host"模式用:本机不跑 executor daemon,
5
+ * codex 直接通过 Bash 调 rotom CLI 与 mesh 交互。本命令一次性完成:
6
+ * 1. POST http://<master>/api/agents { name, domain } → 拿到 plaintext token + configTemplate
7
+ * 2. 落盘 configTemplate 到 ~/.rotom/agents/<name>.json(resolveAgentFromEntry 直接能读)
8
+ * 3. 在 ~/.rotom/config.json 的 agents[name] 注册一条 { configPath, kind: "openclaw" }
9
+ * 4. 若 defaultAgent 未设 → 设为 name
10
+ *
11
+ * 之后 codex 通过 Bash 调 `rotom ...`(用 defaultAgent)或 `rotom --as <name> ...`,
12
+ * resolveAgent 自动从本地文件解出 master+token,executor daemon 完全不用起。
13
+ */
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import { ROTOM_HOME, fail, flagStr, printJson, } from "./common.js";
17
+ import { masterFetch, masterHttpBase, masterWsBase } from "./routes.js";
18
+ const DEFAULT_PORT = 28800;
19
+ function parseMasterSpec(spec) {
20
+ let s = spec.trim().replace(/^ws:\/\//, "").replace(/^wss:\/\//, "").replace(/^https?:\/\//, "");
21
+ s = s.replace(/\/+$/, "");
22
+ let host = s;
23
+ let port = DEFAULT_PORT;
24
+ const colon = s.lastIndexOf(":");
25
+ if (colon !== -1 && /^\d+$/.test(s.slice(colon + 1))) {
26
+ host = s.slice(0, colon);
27
+ port = parseInt(s.slice(colon + 1), 10);
28
+ }
29
+ if (!host)
30
+ fail(`invalid master spec: ${spec}`);
31
+ return { host, port, httpUrl: masterHttpBase(host, port), wsUrl: masterWsBase(host, port) };
32
+ }
33
+ const VALID_CLI_TOOLS = ["claude", "codex", "hermes", "openclaw"];
34
+ function detectCliTool() {
35
+ // 跟 rotom init 的 detectCliTools 同款逻辑,但只返回第一个命中的。
36
+ for (const tool of VALID_CLI_TOOLS) {
37
+ try {
38
+ const { execSync } = require("node:child_process");
39
+ const p = execSync(`command -v ${tool}`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
40
+ if (p)
41
+ return tool;
42
+ }
43
+ catch { /* not installed */ }
44
+ }
45
+ return null;
46
+ }
47
+ export async function cmdJoin(rest, flags) {
48
+ const masterSpec = rest[0];
49
+ if (!masterSpec) {
50
+ fail("usage: rotom join <masterHost:port> --name <agentName> --domain <domain>\n" +
51
+ " --cli-tool <claude|codex|hermes|openclaw> [--working-dir PATH]\n" +
52
+ " [--profile-position P] [--profile-bio B] [--force]\n" +
53
+ " 首次申请 token 落盘到 ~/.rotom/。一个机器一个 CLI 一个 agent:每次换 CLI 用不同\n" +
54
+ " --name + --cli-tool 注册,之后 `rotom --as <name> ...` 自动解出 master+token+cliTool。");
55
+ }
56
+ const name = flagStr(flags, "name");
57
+ const domain = flagStr(flags, "domain");
58
+ if (!name)
59
+ fail("--name is required (the agent name you want to register on master)");
60
+ if (!domain)
61
+ fail("--domain is required (use an existing domain on master; see dashboard)");
62
+ let cliToolRaw = flagStr(flags, "cli-tool");
63
+ if (!cliToolRaw) {
64
+ cliToolRaw = detectCliTool() ?? "";
65
+ if (!cliToolRaw) {
66
+ fail(`--cli-tool not given and auto-detect failed (none of ${VALID_CLI_TOOLS.join(",")} found in PATH)`);
67
+ }
68
+ process.stderr.write(`[rotom] --cli-tool not given, auto-detected: ${cliToolRaw}\n`);
69
+ }
70
+ if (!VALID_CLI_TOOLS.includes(cliToolRaw)) {
71
+ fail(`--cli-tool must be one of ${VALID_CLI_TOOLS.join("|")} (got: ${cliToolRaw})`);
72
+ }
73
+ const cliTool = cliToolRaw;
74
+ const workingDir = flagStr(flags, "working-dir") || process.cwd();
75
+ const profilePosition = flagStr(flags, "profile-position");
76
+ const profileBio = flagStr(flags, "profile-bio");
77
+ const profile = {};
78
+ if (profilePosition)
79
+ profile.position = profilePosition;
80
+ if (profileBio)
81
+ profile.bio = profileBio;
82
+ const force = flags.force === true;
83
+ const { httpUrl, wsUrl } = parseMasterSpec(masterSpec);
84
+ const cfgPath = path.join(ROTOM_HOME, "config.json");
85
+ const cfgExists = fs.existsSync(cfgPath);
86
+ if (cfgExists) {
87
+ // 读取已有 config 检查重名
88
+ try {
89
+ const existing = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
90
+ if (existing.agents?.[name] && !force) {
91
+ fail(`agent "${name}" already registered in ${cfgPath}.\n` +
92
+ ` 重新申请 token(旧 token 作废)请加 --force。`);
93
+ }
94
+ }
95
+ catch {
96
+ // 配置文件损坏 → 当作不存在,继续
97
+ }
98
+ }
99
+ // 1. POST /api/agents 申请 token
100
+ const registerUrl = `${httpUrl}/api/agents`;
101
+ let resp;
102
+ try {
103
+ resp = await masterFetch(registerUrl, {
104
+ method: "POST",
105
+ body: JSON.stringify({ name, domain }),
106
+ });
107
+ }
108
+ catch (e) {
109
+ fail(`failed to reach master at ${httpUrl}: ${e.message}\n run \`rotom status\` to verify reachability`);
110
+ }
111
+ const { status, data: rawData } = resp;
112
+ const text = rawData === null ? "" : typeof rawData === "string" ? rawData : JSON.stringify(rawData);
113
+ const data = rawData;
114
+ if (status < 200 || status >= 300) {
115
+ const detail = typeof data === "object" && data?.error ? data.error : text;
116
+ fail(`master rejected agent registration (HTTP ${status}): ${detail}`);
117
+ }
118
+ const token = data?.token;
119
+ const agentId = data?.id;
120
+ if (!token || !agentId) {
121
+ fail(`master did not return token/id in registration response: ${text}`);
122
+ }
123
+ // 2. 落盘到 ~/.rotom/agents/<name>.json —— 扁平结构,对齐 executor.config.json
124
+ // workers[] 单条 entry + master 字段。{ master, name, token, cliTool, workingDir, profile }
125
+ const agentsDir = path.join(ROTOM_HOME, "agents");
126
+ fs.mkdirSync(agentsDir, { recursive: true });
127
+ const agentFile = path.join(agentsDir, `${name}.json`);
128
+ const agentConfig = {
129
+ master: wsUrl,
130
+ name,
131
+ token,
132
+ cliTool,
133
+ workingDir,
134
+ };
135
+ if (Object.keys(profile).length > 0)
136
+ agentConfig.profile = profile;
137
+ fs.writeFileSync(agentFile, JSON.stringify(agentConfig, null, 2));
138
+ // 3. 写 ~/.rotom/config.json 的 agents[name] = { configPath, kind: "local" }
139
+ fs.mkdirSync(ROTOM_HOME, { recursive: true });
140
+ let cfg = {};
141
+ if (cfgExists) {
142
+ try {
143
+ cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
144
+ }
145
+ catch { /* 损坏则覆盖 */ }
146
+ }
147
+ if (!cfg.agents || typeof cfg.agents !== "object")
148
+ cfg.agents = {};
149
+ cfg.agents[name] = {
150
+ configPath: agentFile,
151
+ kind: "local",
152
+ };
153
+ // 4. 若 defaultAgent 未设 → 设为 name
154
+ let defaultSet = false;
155
+ if (!cfg.defaultAgent) {
156
+ cfg.defaultAgent = name;
157
+ defaultSet = true;
158
+ }
159
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
160
+ printJson({
161
+ id: agentId,
162
+ name,
163
+ master: wsUrl,
164
+ cliTool,
165
+ workingDir,
166
+ configFile: agentFile,
167
+ defaultAgent: defaultSet ? name : "(unchanged)",
168
+ hint: `验证: rotom whoami # 应显示 ${name}`,
169
+ });
170
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * rotom link — link daemon CLI 入口。
3
+ *
4
+ * rotom link join <coordEndpoint> [--hostname <name>] 一次性:probe coord,生成 masterId,写 link.json
5
+ * rotom link start [--port N] 启动 daemon(读 ~/.rotom/link.json)
6
+ * rotom link stop 停 daemon
7
+ * rotom link restart 重启
8
+ * rotom link status 查状态(含 /health 探活)
9
+ * rotom link logs 打印最近 200 行日志
10
+ *
11
+ * link.json 持久化 masterId(永不改),hostname 可改(--hostname 覆盖)。
12
+ */
13
+ import os from "node:os";
14
+ import { fail, flagStr, runShellScript } from "./common.js";
15
+ import { linkJoin } from "../link/server.js";
16
+ export async function cmdLink(rest, flags) {
17
+ const sub = rest[0];
18
+ if (!sub) {
19
+ fail("usage: rotom link <join|start|stop|restart|status|logs> [args]\n" +
20
+ " join <coordEndpoint> [--hostname <name>] 生成 masterId + 写 link.json (一次性)\n" +
21
+ " start [--port N] 启动 link daemon (默认端口 28900)\n" +
22
+ " stop / restart / status / logs");
23
+ }
24
+ const args = rest.slice(1);
25
+ switch (sub) {
26
+ case "join": {
27
+ const coordEndpoint = args[0];
28
+ if (!coordEndpoint) {
29
+ fail("usage: rotom link join <coordEndpoint> [--hostname <name>]\n e.g. ws://192.168.1.5:28800");
30
+ }
31
+ const hostname = flagStr(flags, "hostname") || process.env.ROTOM_HOSTNAME || os.hostname();
32
+ await linkJoin(coordEndpoint, hostname);
33
+ process.stdout.write(`✅ joined. link.json written. Run \`rotom link start\` to launch the daemon.\n`);
34
+ return;
35
+ }
36
+ case "start":
37
+ case "stop":
38
+ case "restart":
39
+ case "status":
40
+ case "logs": {
41
+ const code = await runShellScript("bin/rotom-link.sh", [sub, ...args]);
42
+ process.exit(code);
43
+ }
44
+ default:
45
+ fail(`unknown link subcommand: ${sub}\nRun 'rotom link' for usage.`);
46
+ }
47
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * rotom master — master lifecycle control (start|stop|restart|status).
3
+ */
4
+ import { fail, flagStr, runShellScript } from "./common.js";
5
+ import { usage } from "./routes.js";
6
+ /**
7
+ * Accept `master:start` colon alias, expand to [`start`, ...rest].
8
+ */
9
+ function colonExpand(cmd, rest) {
10
+ const colon = cmd.indexOf(":");
11
+ if (colon === -1)
12
+ return rest;
13
+ return [cmd.slice(colon + 1), ...rest];
14
+ }
15
+ export { colonExpand };
16
+ export async function cmdMaster(rest, flags) {
17
+ const sub = rest[0];
18
+ const args = [];
19
+ switch (sub) {
20
+ case "start":
21
+ case "stop":
22
+ case "status":
23
+ case "restart":
24
+ args.push(sub);
25
+ break;
26
+ default:
27
+ usage("master", "<start|stop|status|restart> [--daemon] [--port N] [--host A] [--data D] [--dev]\n" +
28
+ " (also accepts colon form: rotom master:start | master:stop | master:status | master:restart)");
29
+ }
30
+ if (flags.daemon === true && (sub === "start" || sub === "restart"))
31
+ args.push("--daemon");
32
+ if (flags.dev === true && sub === "start")
33
+ args.push("--dev");
34
+ const port = flagStr(flags, "port");
35
+ if (port && sub === "start")
36
+ args.push("--port", port);
37
+ const host = flagStr(flags, "host");
38
+ if (host && sub === "start")
39
+ args.push("--host", host);
40
+ const data = flagStr(flags, "data");
41
+ if (data && sub === "start")
42
+ args.push("--data", data);
43
+ let code;
44
+ try {
45
+ code = await runShellScript("bin/mesh-master.sh", args);
46
+ }
47
+ catch (e) {
48
+ fail(`failed to invoke bin/mesh-master.sh: ${e.message}`);
49
+ }
50
+ process.exit(code);
51
+ }