@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.
- package/README.md +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rotom group — group create/list/members/history/send/upload/archive/unarchive.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { api, printJson, printTable, isPretty, fail, flagInt, flagStr, requireFlag, } from "./common.js";
|
|
7
|
+
import { route, qs, usage, unknownSubcommand } from "./routes.js";
|
|
8
|
+
// Minimal MIME sniff table — keep it inline; we accept only the 4 formats the
|
|
9
|
+
// uploads endpoint allowlists. Anything else is an error before we hit network.
|
|
10
|
+
const EXT_TO_MIME = {
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".gif": "image/gif",
|
|
15
|
+
".webp": "image/webp",
|
|
16
|
+
};
|
|
17
|
+
function guessMime(filePath) {
|
|
18
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
19
|
+
return EXT_TO_MIME[ext] ?? null;
|
|
20
|
+
}
|
|
21
|
+
export async function cmdGroup(agent, rest, flags) {
|
|
22
|
+
const sub = rest[0];
|
|
23
|
+
if (sub === "create") {
|
|
24
|
+
const title = rest[1];
|
|
25
|
+
if (!title)
|
|
26
|
+
usage("group create", "<title> --agents <a,b[,c...]> [--message M] [--note D] [--note-file F] [--cwd PATH] [--no-template] [--a2a-direct]");
|
|
27
|
+
const agentsFlag = requireFlag(flags, "agents");
|
|
28
|
+
const agents = agentsFlag.split(",").map((s) => s.trim()).filter(Boolean);
|
|
29
|
+
if (agents.length === 0)
|
|
30
|
+
fail("--agents must list at least one agent name (comma-separated)");
|
|
31
|
+
const message = flagStr(flags, "message");
|
|
32
|
+
const noteInline = flagStr(flags, "note");
|
|
33
|
+
const noteFile = flagStr(flags, "note-file");
|
|
34
|
+
if (noteInline && noteFile)
|
|
35
|
+
fail("--note and --note-file are mutually exclusive");
|
|
36
|
+
const cwd = flagStr(flags, "cwd");
|
|
37
|
+
const noTemplate = flags["no-template"] === true;
|
|
38
|
+
const a2aDirect = flags["a2a-direct"] === true;
|
|
39
|
+
// 单播群(unicast):消息不广播、worker 不被消息自动唤醒,只通过 CLI --need-reply
|
|
40
|
+
// 显式点名叫醒对方。≥2 成员,后续可追加。
|
|
41
|
+
if (a2aDirect && agents.length < 2) {
|
|
42
|
+
fail("单播群(--a2a-direct)至少需要 2 个成员: rotom group create <title> --agents a,b[,c...] --a2a-direct");
|
|
43
|
+
}
|
|
44
|
+
if (a2aDirect && new Set(agents).size !== agents.length) {
|
|
45
|
+
fail("单播群(--a2a-direct)成员不能重复");
|
|
46
|
+
}
|
|
47
|
+
// 预检:校验 --agents 名字都已注册,未注册 → fail 不建群
|
|
48
|
+
const allAgents = await api(agent, "GET", "/agents");
|
|
49
|
+
const knownNames = new Set(allAgents.map((a) => a.name));
|
|
50
|
+
const unknown = agents.filter((n) => !knownNames.has(n));
|
|
51
|
+
if (unknown.length > 0) {
|
|
52
|
+
fail(`--agents contains unregistered name(s): ${unknown.join(", ")}\n` +
|
|
53
|
+
` 注册过的 agent 见 \`rotom directory\`。建群中止,未产生任何副作用。`);
|
|
54
|
+
}
|
|
55
|
+
// 建群 + 拉人(一次 API 调用,master 内部 addGroupMembers)
|
|
56
|
+
const createBody = { name: title, memberNames: agents };
|
|
57
|
+
if (cwd)
|
|
58
|
+
createBody.workingDir = cwd;
|
|
59
|
+
if (a2aDirect)
|
|
60
|
+
createBody.type = "a2a_direct";
|
|
61
|
+
const created = await api(agent, "POST", "/groups", createBody);
|
|
62
|
+
const groupId = created.id;
|
|
63
|
+
// 默认加载"群内讨论方案设计" guidance template
|
|
64
|
+
let guidanceTemplate = null;
|
|
65
|
+
if (!noTemplate) {
|
|
66
|
+
try {
|
|
67
|
+
const templates = await api(agent, "GET", "/guidance-templates");
|
|
68
|
+
const tpl = templates.find((t) => t.name === "群内讨论方案设计");
|
|
69
|
+
if (tpl?.prompt_text) {
|
|
70
|
+
await api(agent, "PATCH", route("/groups/:groupId", groupId), { guidancePrompt: tpl.prompt_text });
|
|
71
|
+
guidanceTemplate = tpl.name;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
process.stderr.write(`[rotom] warn: guidance template "群内讨论方案设计" not found on master, skip (group still created)\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
process.stderr.write(`[rotom] warn: failed to load guidance template: ${e.message} (group still created)\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 可选:建群即发开场消息
|
|
82
|
+
let messagePosted = false;
|
|
83
|
+
if (message) {
|
|
84
|
+
await api(agent, "POST", route("/cli/groups/:groupId/send", groupId), { target: "全体", message });
|
|
85
|
+
messagePosted = true;
|
|
86
|
+
}
|
|
87
|
+
// 可选:建群即建 note
|
|
88
|
+
let noteId;
|
|
89
|
+
if (noteInline || noteFile) {
|
|
90
|
+
let noteDescription = "";
|
|
91
|
+
if (noteFile) {
|
|
92
|
+
const expanded = noteFile.startsWith("~/") ? path.join(process.env.HOME || "", noteFile.slice(2)) : noteFile;
|
|
93
|
+
if (!fs.existsSync(expanded))
|
|
94
|
+
fail(`--note-file not found: ${expanded}`);
|
|
95
|
+
noteDescription = fs.readFileSync(expanded, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
else if (noteInline) {
|
|
98
|
+
noteDescription = noteInline;
|
|
99
|
+
}
|
|
100
|
+
const noteRes = await api(agent, "POST", route("/groups/:groupId/notes", groupId), {
|
|
101
|
+
title, description: noteDescription, createdBy: agent.name,
|
|
102
|
+
});
|
|
103
|
+
noteId = noteRes?.id;
|
|
104
|
+
}
|
|
105
|
+
printJson({
|
|
106
|
+
id: groupId,
|
|
107
|
+
name: created.name,
|
|
108
|
+
working_dir: created.working_dir,
|
|
109
|
+
type: created.type ?? (a2aDirect ? "a2a_direct" : "chat"),
|
|
110
|
+
memberCount: agents.length,
|
|
111
|
+
guidanceTemplate,
|
|
112
|
+
messagePosted,
|
|
113
|
+
noteId,
|
|
114
|
+
hint: a2aDirect
|
|
115
|
+
? `单播群(unicast):消息只入库,不广播。叫醒对方: rotom --as=<你> group send ${groupId} <对方> "<问题>" --need-reply。轮询回复: bash skill/rotom-bus-host/scripts/poll-replies.sh ${groupId} --as <你>`
|
|
116
|
+
: `验证: rotom group members ${groupId} | rotom group history ${groupId} --limit 20`,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (sub === "list") {
|
|
121
|
+
const data = await api(agent, "GET", "/groups");
|
|
122
|
+
printTable(data.map((g) => ({
|
|
123
|
+
id: g.id,
|
|
124
|
+
name: g.name,
|
|
125
|
+
members: (g.members?.length ?? 0),
|
|
126
|
+
type: g.type || "chat",
|
|
127
|
+
created_at: g.created_at,
|
|
128
|
+
archived: g.archived_at ? "yes" : "",
|
|
129
|
+
})), ["id", "name", "members", "type", "created_at", "archived"]);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (sub === "members") {
|
|
133
|
+
const groupId = rest[1];
|
|
134
|
+
if (!groupId)
|
|
135
|
+
usage("group members", "<groupId>");
|
|
136
|
+
const data = await api(agent, "GET", route("/groups/:groupId", groupId));
|
|
137
|
+
const members = (data.members || []);
|
|
138
|
+
// 非.pretty 走完整 JSON,保留 profile 嵌套结构供 agent 程序化读取;
|
|
139
|
+
// .pretty 走扁平表格,bio 截断 40 字符避免撑爆终端。
|
|
140
|
+
if (!isPretty()) {
|
|
141
|
+
printJson(members);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
printTable(members.map((m) => ({
|
|
145
|
+
agent_name: m.agent_name,
|
|
146
|
+
position: m.profile?.position ?? "",
|
|
147
|
+
bio: (m.profile?.bio ?? "").slice(0, 40),
|
|
148
|
+
category: m.profile?.category ?? "",
|
|
149
|
+
status: m.status ?? "",
|
|
150
|
+
})), ["agent_name", "position", "bio", "category", "status"]);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (sub === "history") {
|
|
154
|
+
const groupId = rest[1];
|
|
155
|
+
if (!groupId)
|
|
156
|
+
usage("group history", "<groupId>");
|
|
157
|
+
const limit = flagInt(flags, "limit") ?? 50;
|
|
158
|
+
const contentLen = flagInt(flags, "content-len") ?? 200;
|
|
159
|
+
const hideExec = flags["no-exec"] === true;
|
|
160
|
+
const clean = flags["clean"] !== false;
|
|
161
|
+
const data = await api(agent, "GET", `${route("/groups/:groupId/messages", groupId)}${qs({ limit })}`);
|
|
162
|
+
// --no-exec 过滤 sender=system 且以「请求执行命令:」开头的 shell 调用通知,
|
|
163
|
+
// 这类消息占行多但很少是用户想看的回复主体。
|
|
164
|
+
const filtered = hideExec
|
|
165
|
+
? data.filter((m) => !(m.sender === "system" && /^请求执行命令[::]/.test(m.content || "")))
|
|
166
|
+
: data;
|
|
167
|
+
printTable(filtered.map((m) => {
|
|
168
|
+
// --clean 去掉行内 [xxx:yyy]...[/xxx:yyy] 形式的 agent 状态/工具标记
|
|
169
|
+
// ([status:thinking] / [tool:exec] / [tool-result:exec] 等),只留自然语言主体。
|
|
170
|
+
let content = (m.content || "").replace(/\s+/g, " ");
|
|
171
|
+
if (clean) {
|
|
172
|
+
content = content
|
|
173
|
+
.replace(/\[(\w[\w-]*(?::\w[\w-]*)?)\].*?\[\/\1\]/g, "")
|
|
174
|
+
.replace(/\s+/g, " ")
|
|
175
|
+
.trim();
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
time: m.created_at,
|
|
179
|
+
sender: m.sender,
|
|
180
|
+
content: content.slice(0, contentLen),
|
|
181
|
+
};
|
|
182
|
+
}), ["time", "sender", "content"]);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (sub === "new-messages") {
|
|
186
|
+
const groupId = rest[1];
|
|
187
|
+
if (!groupId)
|
|
188
|
+
usage("group new-messages", "<groupId> --since <ISO> [--content-len N] [--no-clean]");
|
|
189
|
+
const since = flagStr(flags, "since");
|
|
190
|
+
if (!since)
|
|
191
|
+
fail("--since is required (北京时间字符串如 \"2026-06-30 18:02:04\" 或 UTC ISO)");
|
|
192
|
+
const contentLen = flagInt(flags, "content-len") ?? 200;
|
|
193
|
+
const clean = flags["clean"] !== false;
|
|
194
|
+
const data = await api(agent, "GET", `${route("/groups/:groupId/messages", groupId)}${qs({ since })}`);
|
|
195
|
+
printTable(data.map((m) => {
|
|
196
|
+
let content = (m.content || "").replace(/\s+/g, " ");
|
|
197
|
+
if (clean) {
|
|
198
|
+
content = content
|
|
199
|
+
.replace(/\[(\w[\w-]*(?::\w[\w-]*)?)\].*?\[\/\1\]/g, "")
|
|
200
|
+
.replace(/\s+/g, " ")
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
time: m.created_at,
|
|
205
|
+
sender: m.sender,
|
|
206
|
+
content: content.slice(0, contentLen),
|
|
207
|
+
};
|
|
208
|
+
}), ["time", "sender", "content"]);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (sub === "send") {
|
|
212
|
+
const groupId = rest[1];
|
|
213
|
+
const target = rest[2];
|
|
214
|
+
const message = rest.slice(3).join(" ");
|
|
215
|
+
if (!groupId || !target || !message)
|
|
216
|
+
usage("group send", "<groupId> <target> <message...> [--no-dispatch] [--need-reply]");
|
|
217
|
+
const body = { target, message };
|
|
218
|
+
if (flags["no-dispatch"] === true)
|
|
219
|
+
body.noDispatch = true;
|
|
220
|
+
if (flags["need-reply"] === true)
|
|
221
|
+
body.needReply = true;
|
|
222
|
+
const data = await api(agent, "POST", route("/cli/groups/:groupId/send", groupId), body);
|
|
223
|
+
printJson(data);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (sub === "upload") {
|
|
227
|
+
const groupId = rest[1];
|
|
228
|
+
const filePath = rest[2];
|
|
229
|
+
if (!groupId || !filePath)
|
|
230
|
+
usage("group upload", "<groupId> <filePath> [--markdown]");
|
|
231
|
+
const expanded = filePath.startsWith("~/") ? path.join(process.env.HOME || "", filePath.slice(2)) : filePath;
|
|
232
|
+
if (!fs.existsSync(expanded))
|
|
233
|
+
fail(`file not found: ${expanded}`);
|
|
234
|
+
const mimeType = guessMime(expanded);
|
|
235
|
+
if (!mimeType)
|
|
236
|
+
fail(`unsupported extension (allowed: ${Object.keys(EXT_TO_MIME).join(", ")})`);
|
|
237
|
+
const bytes = fs.readFileSync(expanded);
|
|
238
|
+
// 15MB cap mirrors server-side MAX_UPLOAD_BYTES — fail fast instead of
|
|
239
|
+
// uploading a body the server will reject.
|
|
240
|
+
const MAX = 15 * 1024 * 1024;
|
|
241
|
+
if (bytes.length > MAX)
|
|
242
|
+
fail(`file too large: ${bytes.length} bytes > ${MAX} bytes`);
|
|
243
|
+
const dataBase64 = bytes.toString("base64");
|
|
244
|
+
const fileName = path.basename(expanded);
|
|
245
|
+
const data = await api(agent, "POST", "/uploads", { groupId, fileName, mimeType, dataBase64 });
|
|
246
|
+
if (flags.markdown) {
|
|
247
|
+
// Print only the markdown image token; agents / shell pipelines can
|
|
248
|
+
// capture and paste directly into `rotom group send` message body.
|
|
249
|
+
process.stdout.write(`\n`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
printJson(data);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (sub === "archive") {
|
|
257
|
+
const groupId = rest[1];
|
|
258
|
+
if (!groupId)
|
|
259
|
+
usage("group archive", "<groupId>");
|
|
260
|
+
const data = await api(agent, "PATCH", route("/groups/:groupId", groupId), { archived: true });
|
|
261
|
+
printJson(data);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (sub === "unarchive") {
|
|
265
|
+
const groupId = rest[1];
|
|
266
|
+
if (!groupId)
|
|
267
|
+
usage("group unarchive", "<groupId>");
|
|
268
|
+
const data = await api(agent, "PATCH", route("/groups/:groupId", groupId), { archived: false });
|
|
269
|
+
printJson(data);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
unknownSubcommand("group", sub);
|
|
273
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { nowBeijing } from "../shared/time.js";
|
|
2
|
+
/**
|
|
3
|
+
* rotom identity — whoami / status commands.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { ROTOM_HOME, masterHttpUrl, api, printJson, fail, failKind, } from "./common.js";
|
|
8
|
+
export async function cmdWhoami(agent) {
|
|
9
|
+
const data = await api(agent, "GET", "/whoami");
|
|
10
|
+
printJson(data);
|
|
11
|
+
}
|
|
12
|
+
function resolveMasterUrlForStatus() {
|
|
13
|
+
return resolveLocalMasterUrl();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 解析本机 master 的 HTTP base URL,用于 federation team CLI 等无 agent
|
|
17
|
+
* 上下文场景。优先级:ROTOM_MASTER env > ~/.rotom/executor.config.json#master
|
|
18
|
+
* > 失败(让 caller 给清晰提示)。
|
|
19
|
+
*/
|
|
20
|
+
export function resolveLocalMasterUrl() {
|
|
21
|
+
const env = process.env.ROTOM_MASTER;
|
|
22
|
+
if (env)
|
|
23
|
+
return masterHttpUrl(env);
|
|
24
|
+
const cfgPath = path.join(ROTOM_HOME, "executor.config.json");
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
27
|
+
if (raw?.master)
|
|
28
|
+
return masterHttpUrl(raw.master);
|
|
29
|
+
}
|
|
30
|
+
catch { /* ignore */ }
|
|
31
|
+
fail("cannot resolve local master URL. Set ROTOM_MASTER, have ~/.rotom/executor.config.json, " +
|
|
32
|
+
"or start the master first (e.g. `rotom master start --daemon`).");
|
|
33
|
+
}
|
|
34
|
+
export async function cmdStatus(_rest, _flags) {
|
|
35
|
+
const url = resolveMasterUrlForStatus();
|
|
36
|
+
const endpoint = `${url}/health`;
|
|
37
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
38
|
+
let resp;
|
|
39
|
+
try {
|
|
40
|
+
resp = await fetch(endpoint);
|
|
41
|
+
const data = await resp.json();
|
|
42
|
+
printJson({
|
|
43
|
+
status: resp.ok ? "ok" : "unhealthy",
|
|
44
|
+
master: url,
|
|
45
|
+
total: data.total ?? null,
|
|
46
|
+
online: data.online ?? null,
|
|
47
|
+
domains: data.domains ?? null,
|
|
48
|
+
checkedAt: nowBeijing(),
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const reason = e.message;
|
|
54
|
+
const partial = resp !== undefined;
|
|
55
|
+
if (attempt < 2 && !partial) {
|
|
56
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
failKind(partial ? "partial-response" : "network", url, reason, partial ? resp.status : 0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rotom init — first-time bootstrap (detect CLI tools, register agents, write config).
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import * as readline from "node:readline";
|
|
8
|
+
import { ROTOM_HOME, DEFAULT_EXECUTOR_CONFIG, fail, flagStr, } from "./common.js";
|
|
9
|
+
import { masterFetch, masterHttpBase, masterWsBase } from "./routes.js";
|
|
10
|
+
// ── Bootstrap helpers ─────────────────────────────────────────────────────
|
|
11
|
+
const INIT_KNOWN_TOOLS = ["claude", "codex", "hermes", "openclaw", "pi"];
|
|
12
|
+
function detectCliTools(wanted) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const tool of wanted) {
|
|
15
|
+
try {
|
|
16
|
+
const p = execSync(`command -v ${tool}`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
17
|
+
.toString()
|
|
18
|
+
.trim();
|
|
19
|
+
if (p)
|
|
20
|
+
out.push({ tool, path: p });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* not installed */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function isInteractive() {
|
|
29
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
30
|
+
}
|
|
31
|
+
function askYN(question, defaultYes) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
34
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
rl.question(`${question} ${hint}: `, (ans) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
const a = ans.trim().toLowerCase();
|
|
38
|
+
if (!a)
|
|
39
|
+
return resolve(defaultYes);
|
|
40
|
+
if (a === "y" || a === "yes")
|
|
41
|
+
return resolve(true);
|
|
42
|
+
if (a === "n" || a === "no")
|
|
43
|
+
return resolve(false);
|
|
44
|
+
resolve(defaultYes);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function askText(question, defaultValue) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
52
|
+
rl.question(`${question}${suffix}: `, (ans) => {
|
|
53
|
+
rl.close();
|
|
54
|
+
const trimmed = ans.trim();
|
|
55
|
+
resolve(trimmed || defaultValue || "");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function parseMasterSpec(spec, defaultPort) {
|
|
60
|
+
let s = spec.trim();
|
|
61
|
+
s = s.replace(/^wss?:\/\//, "").replace(/^https?:\/\//, "");
|
|
62
|
+
s = s.replace(/\/+$/, "");
|
|
63
|
+
let host = s;
|
|
64
|
+
let port = defaultPort;
|
|
65
|
+
const colon = s.lastIndexOf(":");
|
|
66
|
+
if (colon !== -1) {
|
|
67
|
+
const tail = s.slice(colon + 1);
|
|
68
|
+
const n = Number(tail);
|
|
69
|
+
if (Number.isInteger(n) && n > 0 && n < 65536) {
|
|
70
|
+
host = s.slice(0, colon);
|
|
71
|
+
port = n;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!host)
|
|
75
|
+
fail(`invalid master spec: ${spec}`);
|
|
76
|
+
return { host, port, url: masterWsBase(host, port) };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wrap masterFetch so a network-level failure (DNS, ECONNREFUSED, etc.) fails
|
|
80
|
+
* with the same message the old httpJsonNoAuth helper emitted, instead of
|
|
81
|
+
* bubbling as an unhandled rejection. The probe path in cmdInit uses
|
|
82
|
+
* masterFetch directly with `.catch(() => null)` because it wants the
|
|
83
|
+
* "master unreachable — start it first" recovery message below.
|
|
84
|
+
*/
|
|
85
|
+
async function masterFetchOrFail(url, init) {
|
|
86
|
+
try {
|
|
87
|
+
return await masterFetch(url, init);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
fail(`network error calling ${url}: ${e.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function listMasterDomains(httpBase) {
|
|
94
|
+
const { status, data } = await masterFetchOrFail(`${httpBase}/api/domains`, { method: "GET" });
|
|
95
|
+
if (status !== 200)
|
|
96
|
+
fail(`failed to list domains: HTTP ${status}`);
|
|
97
|
+
const d = data;
|
|
98
|
+
return Array.isArray(d) ? d : d?.domains ?? [];
|
|
99
|
+
}
|
|
100
|
+
async function ensureDomain(httpBase, name) {
|
|
101
|
+
const { status, data } = await masterFetchOrFail(`${httpBase}/api/domains`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: JSON.stringify({ name }),
|
|
104
|
+
});
|
|
105
|
+
if (status === 200 || status === 201)
|
|
106
|
+
return data;
|
|
107
|
+
if (status === 409)
|
|
108
|
+
return { name }; // already exists
|
|
109
|
+
fail(`failed to create domain "${name}": HTTP ${status} ${JSON.stringify(data)}`);
|
|
110
|
+
}
|
|
111
|
+
async function registerAgent(httpBase, name, domain, cliTool) {
|
|
112
|
+
const { status, data } = await masterFetchOrFail(`${httpBase}/api/agents`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: JSON.stringify({ name, domain, cliTool }),
|
|
115
|
+
});
|
|
116
|
+
if (status !== 200 && status !== 201) {
|
|
117
|
+
fail(`failed to register agent "${name}": HTTP ${status} ${JSON.stringify(data)}`);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: data.name ?? name,
|
|
121
|
+
token: data.mesh_token ?? data.token ?? "",
|
|
122
|
+
cliTool,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function pickDomain(master, hintFlag, yesMode = false) {
|
|
126
|
+
const httpBase = masterHttpBase(master.host, master.port);
|
|
127
|
+
const existing = await listMasterDomains(httpBase);
|
|
128
|
+
if (hintFlag) {
|
|
129
|
+
const match = existing.find((d) => d.name === hintFlag);
|
|
130
|
+
if (match)
|
|
131
|
+
return match.name;
|
|
132
|
+
const created = await ensureDomain(httpBase, hintFlag);
|
|
133
|
+
return created.name;
|
|
134
|
+
}
|
|
135
|
+
if (existing.length === 1 && yesMode)
|
|
136
|
+
return existing[0].name;
|
|
137
|
+
if (existing.length > 0 && !yesMode) {
|
|
138
|
+
process.stdout.write("Existing domains:\n");
|
|
139
|
+
for (const d of existing)
|
|
140
|
+
process.stdout.write(` ${d.name}\n`);
|
|
141
|
+
const pick = await askText("Domain to use", existing[0].name);
|
|
142
|
+
const match = existing.find((d) => d.name === pick);
|
|
143
|
+
if (match)
|
|
144
|
+
return match.name;
|
|
145
|
+
const created = await ensureDomain(httpBase, pick);
|
|
146
|
+
return created.name;
|
|
147
|
+
}
|
|
148
|
+
if (yesMode) {
|
|
149
|
+
const created = await ensureDomain(httpBase, "default");
|
|
150
|
+
return created.name;
|
|
151
|
+
}
|
|
152
|
+
const name = await askText("No domains found. Create domain", "default");
|
|
153
|
+
const created = await ensureDomain(httpBase, name);
|
|
154
|
+
return created.name;
|
|
155
|
+
}
|
|
156
|
+
// ── cmdInit ───────────────────────────────────────────────────────────────
|
|
157
|
+
export async function cmdInit(_rest, flags) {
|
|
158
|
+
const yesMode = flags.yes === true || flags.y === true;
|
|
159
|
+
const force = flags.force === true;
|
|
160
|
+
const masterSpec = flagStr(flags, "master");
|
|
161
|
+
const domainFlag = flagStr(flags, "domain");
|
|
162
|
+
const namePrefix = flagStr(flags, "name-prefix") || process.env.USER || "user";
|
|
163
|
+
const toolsFlag = flagStr(flags, "tools");
|
|
164
|
+
const wantedTools = (toolsFlag ? toolsFlag.split(",").map((s) => s.trim()).filter(Boolean) : [...INIT_KNOWN_TOOLS]);
|
|
165
|
+
if (!isInteractive() && !yesMode) {
|
|
166
|
+
fail("rotom init needs an interactive TTY (or pass --yes to accept all defaults)");
|
|
167
|
+
}
|
|
168
|
+
if (fs.existsSync(DEFAULT_EXECUTOR_CONFIG) && !force) {
|
|
169
|
+
if (yesMode) {
|
|
170
|
+
fail(`${DEFAULT_EXECUTOR_CONFIG} already exists. Re-run with --force to overwrite.`);
|
|
171
|
+
}
|
|
172
|
+
const overwrite = await askYN(`${DEFAULT_EXECUTOR_CONFIG} already exists. Overwrite?`, false);
|
|
173
|
+
if (!overwrite) {
|
|
174
|
+
process.stdout.write("Aborted. Existing file left untouched.\n");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
process.stdout.write(`\nScanning for CLI tools (${wantedTools.join(", ")})...\n`);
|
|
179
|
+
const detected = detectCliTools(wantedTools);
|
|
180
|
+
if (detected.length === 0) {
|
|
181
|
+
fail(`none of [${wantedTools.join(", ")}] are on PATH. Install at least one, or pass --tools with what's available.`);
|
|
182
|
+
}
|
|
183
|
+
for (const { tool, path: p } of detected) {
|
|
184
|
+
process.stdout.write(` \u2713 ${tool} (${p})\n`);
|
|
185
|
+
}
|
|
186
|
+
for (const tool of wantedTools) {
|
|
187
|
+
if (!detected.find((d) => d.tool === tool))
|
|
188
|
+
process.stdout.write(` \u2717 ${tool} (not installed)\n`);
|
|
189
|
+
}
|
|
190
|
+
const selected = [];
|
|
191
|
+
for (const { tool, path: p } of detected) {
|
|
192
|
+
if (yesMode) {
|
|
193
|
+
selected.push({ tool, path: p, name: `${namePrefix}-${tool}` });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const want = await askYN(`Register ${tool}?`, true);
|
|
197
|
+
if (!want)
|
|
198
|
+
continue;
|
|
199
|
+
const defaultName = `${namePrefix}-${tool}`;
|
|
200
|
+
const name = (await askText(` Name for ${tool}`, defaultName)).trim();
|
|
201
|
+
if (!name)
|
|
202
|
+
fail("name cannot be empty");
|
|
203
|
+
selected.push({ tool, path: p, name });
|
|
204
|
+
}
|
|
205
|
+
if (selected.length === 0) {
|
|
206
|
+
process.stdout.write("\nNo tools selected. Nothing to do.\n");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const master = masterSpec
|
|
210
|
+
? parseMasterSpec(masterSpec, 28800)
|
|
211
|
+
: yesMode
|
|
212
|
+
? parseMasterSpec("127.0.0.1:28800", 28800)
|
|
213
|
+
: await (async () => {
|
|
214
|
+
const ip = await askText("Master IP", "127.0.0.1");
|
|
215
|
+
const portStr = await askText("Master port", "28800");
|
|
216
|
+
const port = Number(portStr);
|
|
217
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
218
|
+
fail("invalid port");
|
|
219
|
+
return { host: ip, port, url: masterWsBase(ip, port) };
|
|
220
|
+
})();
|
|
221
|
+
process.stdout.write(`\nMaster: ${master.url}\n`);
|
|
222
|
+
const httpBase = masterHttpBase(master.host, master.port);
|
|
223
|
+
const probe = await masterFetch(`${httpBase}/api/domains`, { method: "GET" }).catch(() => null);
|
|
224
|
+
if (!probe || probe.status === 0) {
|
|
225
|
+
fail(`master ${master.url} unreachable. Start it first (e.g. \`mesh-master start --daemon\`) or check the IP.`);
|
|
226
|
+
}
|
|
227
|
+
if (probe.status >= 500) {
|
|
228
|
+
fail(`master ${master.url} returned HTTP ${probe.status}: ${JSON.stringify(probe.data)}`);
|
|
229
|
+
}
|
|
230
|
+
const domain = await pickDomain(master, domainFlag, yesMode);
|
|
231
|
+
process.stdout.write(`Domain: ${domain}\n`);
|
|
232
|
+
process.stdout.write(`\nRegistering ${selected.length} agent(s)...\n`);
|
|
233
|
+
const workers = [];
|
|
234
|
+
for (const s of selected) {
|
|
235
|
+
const reg = await registerAgent(httpBase, s.name, domain, s.tool);
|
|
236
|
+
process.stdout.write(` \u2713 ${reg.name} (${s.tool}) token=${reg.token.slice(0, 12)}...\n`);
|
|
237
|
+
workers.push(reg);
|
|
238
|
+
}
|
|
239
|
+
if (!fs.existsSync(ROTOM_HOME))
|
|
240
|
+
fs.mkdirSync(ROTOM_HOME, { recursive: true });
|
|
241
|
+
const defaultBase = path.join(ROTOM_HOME, "artifacts");
|
|
242
|
+
const workingDir = yesMode
|
|
243
|
+
? defaultBase
|
|
244
|
+
: (await askText("Working dir base (per-group cwd will be <base>/<groupId>)", defaultBase)).trim();
|
|
245
|
+
if (!workingDir)
|
|
246
|
+
fail("workingDir cannot be empty");
|
|
247
|
+
if (!path.isAbsolute(workingDir)) {
|
|
248
|
+
if (yesMode)
|
|
249
|
+
fail(`workingDir must be an absolute path, got: ${workingDir}`);
|
|
250
|
+
const expand = await askYN(`workingDir "${workingDir}" is not absolute. Use as-is?`, false);
|
|
251
|
+
if (!expand)
|
|
252
|
+
fail("aborted: workingDir must be absolute");
|
|
253
|
+
}
|
|
254
|
+
const cfg = {
|
|
255
|
+
master: master.url,
|
|
256
|
+
workers: workers.map((w) => ({
|
|
257
|
+
name: w.name,
|
|
258
|
+
token: w.token,
|
|
259
|
+
cliTool: w.cliTool,
|
|
260
|
+
workingDir,
|
|
261
|
+
profile: { category: "Agent" },
|
|
262
|
+
})),
|
|
263
|
+
};
|
|
264
|
+
fs.writeFileSync(DEFAULT_EXECUTOR_CONFIG, JSON.stringify(cfg, null, 2) + "\n");
|
|
265
|
+
process.stdout.write(`\nWrote ${DEFAULT_EXECUTOR_CONFIG} with ${workers.length} worker(s).\n`);
|
|
266
|
+
process.stdout.write(` base workingDir: ${workingDir} (per-group: <base>/<groupId>)\n`);
|
|
267
|
+
process.stdout.write("Next: run \`pnpm executor\` (or \`rotom executor\`) to connect them.\n");
|
|
268
|
+
}
|