@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,158 @@
1
+ /**
2
+ * Session management API
3
+ *
4
+ * Surfaces the executor-side SessionStore (keyed `${cliTool}:${groupId}`)
5
+ * to the dashboard. Every endpoint forwards a WS request to the right
6
+ * worker(s) via WSHub.routeToExecutor and returns the first response.
7
+ *
8
+ * Endpoints:
9
+ * GET /sessions?groupId=<id>
10
+ * GET /sessions/:cliTool/:groupId/:sessionId?tail=<lines>
11
+ * DELETE /sessions/:cliTool/:groupId/:sessionId
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { createLogger } from "../../shared/logger.js";
15
+ const log = createLogger("mesh-api-sessions");
16
+ /** Hex / uuid / dash-only sessionId — keeps URL paths from being abused. */
17
+ const SAFE_ID = /^[A-Za-z0-9_-]+$/;
18
+ /** Known CLI backends; matches the switch in src/executor/index.ts. */
19
+ const SAFE_CLI = /^(claude|codex|hermes|openclaw)$/;
20
+ export function registerSessionRoutes(apiRouter, db, _auth, hub) {
21
+ if (!hub) {
22
+ log.warn("[sessions] hub unavailable; routes registered as no-ops");
23
+ return;
24
+ }
25
+ // ── List sessions for a group ──────────────────────────────────────────
26
+ // Reads from master DB (`agent_sessions` table), which workers keep fresh
27
+ // via `session_snapshot` pushes (master upserts on receipt). Includes
28
+ // invalidated sessions (full history). `online` is computed by joining
29
+ // against the in-memory `connections` map.
30
+ apiRouter.get("/sessions", async (req, res) => {
31
+ const groupId = typeof req.query.groupId === "string" ? req.query.groupId : "";
32
+ if (!groupId) {
33
+ res.status(400).json({ error: "groupId is required" });
34
+ return;
35
+ }
36
+ if (!SAFE_ID.test(groupId)) {
37
+ res.status(400).json({ error: "invalid groupId" });
38
+ return;
39
+ }
40
+ const sessions = hub.listSessionsByGroup(groupId);
41
+ res.json({ sessions });
42
+ });
43
+ // ── Session usage / model / online (from DB) ──────────────────────────
44
+ // Debug 视图 SessionPanel 用它把每个 chat session 自己的 token 用量 / 模型名
45
+ // 拉出来展示。数据源是 master DB 的 agent_sessions 表(由 worker 的
46
+ // session_snapshot 推送写入)。online 由 connections 内存表 join 算出。
47
+ //
48
+ // 这条路径返回的就是该 chat session 自己的消耗,跟 issue 执行的 session 是
49
+ // 两个独立 session(issue 有自己的 session_id,不共享)。
50
+ apiRouter.get("/sessions/:cliTool/:groupId/:sessionId/usage", (req, res) => {
51
+ const { cliTool, groupId, sessionId } = req.params;
52
+ if (!SAFE_CLI.test(cliTool)) {
53
+ res.status(400).json({ error: `invalid cliTool: ${cliTool}` });
54
+ return;
55
+ }
56
+ if (!SAFE_ID.test(groupId) || !SAFE_ID.test(sessionId)) {
57
+ res.status(400).json({ error: "invalid groupId or sessionId" });
58
+ return;
59
+ }
60
+ void db; // DB 通过 hub.findSessionEntry 内部访问;这里保留签名兼容
61
+ const entry = hub.findSessionEntry(sessionId);
62
+ if (!entry) {
63
+ res.json({
64
+ cliTool, sessionId, usage: null, model: null,
65
+ cumulativeCostUsd: null,
66
+ cumulativeInputTokens: null,
67
+ cumulativeOutputTokens: null,
68
+ cumulativeCacheReadTokens: null,
69
+ cumulativeCacheCreationTokens: null,
70
+ online: false, invalidatedAt: null,
71
+ });
72
+ return;
73
+ }
74
+ res.json({
75
+ cliTool,
76
+ sessionId,
77
+ usage: entry.usage ?? null,
78
+ model: entry.model ?? null,
79
+ cumulativeCostUsd: typeof entry.cumulativeCostUsd === "number" ? entry.cumulativeCostUsd : null,
80
+ cumulativeInputTokens: entry.cumulativeInputTokens ?? null,
81
+ cumulativeOutputTokens: entry.cumulativeOutputTokens ?? null,
82
+ cumulativeCacheReadTokens: entry.cumulativeCacheReadTokens ?? null,
83
+ cumulativeCacheCreationTokens: entry.cumulativeCacheCreationTokens ?? null,
84
+ online: entry.online ?? false,
85
+ invalidatedAt: entry.invalidatedAt ?? null,
86
+ });
87
+ });
88
+ // ── View session content ───────────────────────────────────────────────
89
+ apiRouter.get("/sessions/:cliTool/:groupId/:sessionId", async (req, res) => {
90
+ const { cliTool, groupId, sessionId } = req.params;
91
+ if (!SAFE_CLI.test(cliTool)) {
92
+ res.status(400).json({ error: `invalid cliTool: ${cliTool}` });
93
+ return;
94
+ }
95
+ if (!SAFE_ID.test(groupId) || !SAFE_ID.test(sessionId)) {
96
+ res.status(400).json({ error: "invalid groupId or sessionId" });
97
+ return;
98
+ }
99
+ const tailLines = Math.min(Math.max(parseInt(req.query.tail || "200", 10) || 200, 1), 2000);
100
+ const requestId = randomUUID();
101
+ try {
102
+ const resp = await hub.routeToExecutor((c) => c.cliTool === cliTool, { type: "session_view_request", requestId, groupId, sessionId, tailLines });
103
+ if (resp.type !== "session_view_response") {
104
+ res.status(502).json({ error: "unexpected response from executor" });
105
+ return;
106
+ }
107
+ res.json({
108
+ cliTool,
109
+ groupId,
110
+ sessionId,
111
+ format: resp.format,
112
+ content: resp.content,
113
+ error: resp.error,
114
+ });
115
+ }
116
+ catch (err) {
117
+ const msg = err?.message || String(err);
118
+ log.warn(`[sessions] view failed: ${msg}`);
119
+ const code = /timeout/.test(msg) ? 504 : 502;
120
+ res.status(code).json({ error: msg });
121
+ }
122
+ });
123
+ // ── Delete a session ───────────────────────────────────────────────────
124
+ // Worker is selected by cliTool (the worker that owns this session).
125
+ // On success, the next chat / issue run for (cliTool, groupId) will
126
+ // start a fresh session instead of --resume'ing the deleted one.
127
+ apiRouter.delete("/sessions/:cliTool/:groupId/:sessionId", async (req, res) => {
128
+ const { cliTool, groupId, sessionId } = req.params;
129
+ if (!SAFE_CLI.test(cliTool)) {
130
+ res.status(400).json({ error: `invalid cliTool: ${cliTool}` });
131
+ return;
132
+ }
133
+ if (!SAFE_ID.test(groupId) || !SAFE_ID.test(sessionId)) {
134
+ res.status(400).json({ error: "invalid groupId or sessionId" });
135
+ return;
136
+ }
137
+ const requestId = randomUUID();
138
+ try {
139
+ const resp = await hub.routeToExecutor((c) => c.cliTool === cliTool, { type: "session_delete_request", requestId, groupId, sessionId });
140
+ if (resp.type !== "session_delete_response") {
141
+ res.status(502).json({ error: "unexpected response from executor" });
142
+ return;
143
+ }
144
+ if (!resp.ok) {
145
+ res.status(404).json({ error: resp.error || "session not found" });
146
+ return;
147
+ }
148
+ log.info(`[sessions] deleted ${cliTool}:${groupId}:${sessionId}`);
149
+ res.json({ ok: true });
150
+ }
151
+ catch (err) {
152
+ const msg = err?.message || String(err);
153
+ log.warn(`[sessions] delete failed: ${msg}`);
154
+ const code = /timeout/.test(msg) ? 504 : 502;
155
+ res.status(code).json({ error: msg });
156
+ }
157
+ });
158
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Digital Employee Mesh — Share / Visitor API
3
+ *
4
+ * Two surfaces:
5
+ *
6
+ * 1. Management (requires Bearer mesh_xxx):
7
+ * - POST /api/groups/:groupId/shares mint a visitor token for a group
8
+ * - DELETE /api/shares/:token revoke a visitor token
9
+ *
10
+ * 2. Visitor read-only (token in URL, no Bearer). Path shape mirrors the
11
+ * existing Dashboard routes so the frontend can keep using its existing
12
+ * `groupsApi.getById(id)` etc. — the api client just prepends
13
+ * `/share/:token` to the path:
14
+ * - GET /api/share/:token/groups/:id group info + members
15
+ * - GET /api/share/:token/groups/:id/messages group messages
16
+ * - GET /api/share/:token/groups/:groupId/issues issues in the group
17
+ * - GET /api/share/:token/issues/:id one issue + events
18
+ * - GET /api/share/:token/issues/:id/messages issue comments
19
+ * - GET /api/share/:token/issues/:id/events issue event timeline
20
+ * - GET /api/share/:token/groups/:groupId/artifacts artifact tree
21
+ * - GET /api/share/:token/groups/:groupId/artifacts/content single file
22
+ * - GET /api/share/:token/groups/:groupId/notes group notes
23
+ *
24
+ * Every visitor endpoint resolves the token via ShareTokenStore and 401s if
25
+ * the token is unknown / revoked. Scope is implicit: a token is bound to one
26
+ * groupId at mint time, so visitors cannot escape their group.
27
+ */
28
+ import fs from "node:fs";
29
+ import { resolveGroupArtifactRoot } from "../group-paths.js";
30
+ import { createLogger } from "../../shared/logger.js";
31
+ import { readFileSafely, walkDir } from "../util/fs.js";
32
+ const log = createLogger("mesh-api");
33
+ function requireAgent(req, res) {
34
+ const auth = req.agentAuth;
35
+ if (!auth) {
36
+ res.status(401).json({ error: "Authentication required" });
37
+ return null;
38
+ }
39
+ return auth;
40
+ }
41
+ /** Resolve a share token; 401 if not found. Returns the resolved record. */
42
+ function requireToken(store, token, res) {
43
+ const record = store.resolve(token);
44
+ if (!record) {
45
+ res.status(401).json({ error: "Invalid or expired share token" });
46
+ return null;
47
+ }
48
+ return record;
49
+ }
50
+ export function registerShareRoutes(apiRouter, db, store) {
51
+ // ── Management endpoints (require agent auth) ────────────────────────
52
+ apiRouter.post("/groups/:groupId/shares", (req, res) => {
53
+ const auth = requireAgent(req, res);
54
+ if (!auth)
55
+ return;
56
+ const group = db.getGroupById(req.params.groupId);
57
+ if (!group) {
58
+ res.status(404).json({ error: "Group not found" });
59
+ return;
60
+ }
61
+ const record = store.create(req.params.groupId, auth.name);
62
+ log.info(`Share token minted for group ${req.params.groupId} by ${auth.name}`);
63
+ res.status(201).json({
64
+ token: record.token,
65
+ groupId: record.groupId,
66
+ createdBy: record.createdBy,
67
+ createdAt: record.createdAt,
68
+ });
69
+ });
70
+ apiRouter.delete("/shares/:token", (req, res) => {
71
+ const auth = requireAgent(req, res);
72
+ if (!auth)
73
+ return;
74
+ const record = store.resolve(req.params.token);
75
+ if (!record) {
76
+ // Idempotent: unknown tokens are already "gone", don't 404.
77
+ res.json({ ok: true });
78
+ return;
79
+ }
80
+ const removed = store.revoke(req.params.token);
81
+ log.info(`Share token ${removed ? "revoked" : "not found"} by ${auth.name}`);
82
+ res.json({ ok: true });
83
+ });
84
+ // ── Visitor read-only endpoints (token in URL) ───────────────────────
85
+ apiRouter.get("/share/:token/groups", (req, res) => {
86
+ const record = requireToken(store, req.params.token, res);
87
+ if (!record)
88
+ return;
89
+ // Visitors can only see the single group their token grants scope to.
90
+ const group = db.getGroupById(record.groupId);
91
+ if (!group) {
92
+ res.json([]);
93
+ return;
94
+ }
95
+ const members = db.getGroupMembers(record.groupId);
96
+ res.json([{ ...group, members }]);
97
+ });
98
+ // Visitors don't have their own agent list — return only the agents that are
99
+ // members of the shared group, so the chat UI can resolve @mentions and
100
+ // group member names without leaking agents from other groups.
101
+ apiRouter.get("/share/:token/agents", (req, res) => {
102
+ const record = requireToken(store, req.params.token, res);
103
+ if (!record)
104
+ return;
105
+ const members = db.getGroupMembers(record.groupId);
106
+ const memberNames = new Set(members.map((m) => m.agent_name));
107
+ const scoped = db.listAgents().filter((a) => a.name && memberNames.has(a.name));
108
+ res.json(scoped);
109
+ });
110
+ apiRouter.get("/share/:token/groups/:id", (req, res) => {
111
+ const record = requireToken(store, req.params.token, res);
112
+ if (!record)
113
+ return;
114
+ // Hard scope guard: a token only grants access to its bound group.
115
+ if (req.params.id !== record.groupId) {
116
+ res.status(403).json({ error: "Group not in shared scope" });
117
+ return;
118
+ }
119
+ const group = db.getGroupById(record.groupId);
120
+ if (!group) {
121
+ res.status(404).json({ error: "Group not found" });
122
+ return;
123
+ }
124
+ const members = db.getGroupMembers(record.groupId);
125
+ res.json({ ...group, members });
126
+ });
127
+ apiRouter.get("/share/:token/groups/:id/messages", (req, res) => {
128
+ const record = requireToken(store, req.params.token, res);
129
+ if (!record)
130
+ return;
131
+ if (req.params.id !== record.groupId) {
132
+ res.status(403).json({ error: "Group not in shared scope" });
133
+ return;
134
+ }
135
+ if (Object.prototype.hasOwnProperty.call(req.query, "limit")) {
136
+ const total = Math.min(parseInt(req.query.limit) || 300, 500);
137
+ res.json(db.getGroupMessages(record.groupId, 5, Math.max(total - 5, 0)));
138
+ }
139
+ else {
140
+ res.json(db.getGroupMessages(record.groupId));
141
+ }
142
+ });
143
+ apiRouter.get("/share/:token/groups/:groupId/issues", (req, res) => {
144
+ const record = requireToken(store, req.params.token, res);
145
+ if (!record)
146
+ return;
147
+ if (req.params.groupId !== record.groupId) {
148
+ res.status(403).json({ error: "Group not in shared scope" });
149
+ return;
150
+ }
151
+ const status = req.query.status;
152
+ const type = req.query.type;
153
+ res.json(db.listIssuesByGroup(record.groupId, status, type));
154
+ });
155
+ apiRouter.get("/share/:token/issues/:id", (req, res) => {
156
+ const record = requireToken(store, req.params.token, res);
157
+ if (!record)
158
+ return;
159
+ const issue = db.getIssueById(req.params.id);
160
+ if (!issue) {
161
+ res.status(404).json({ error: "Issue not found" });
162
+ return;
163
+ }
164
+ // Hard scope guard: visitors can only see issues in their shared group.
165
+ if (issue.group_id !== record.groupId) {
166
+ res.status(403).json({ error: "Issue not in shared group" });
167
+ return;
168
+ }
169
+ const events = db.getIssueEvents(req.params.id);
170
+ res.json({ ...issue, events });
171
+ });
172
+ apiRouter.get("/share/:token/issues/:id/messages", (req, res) => {
173
+ const record = requireToken(store, req.params.token, res);
174
+ if (!record)
175
+ return;
176
+ const issue = db.getIssueById(req.params.id);
177
+ if (!issue) {
178
+ res.status(404).json({ error: "Issue not found" });
179
+ return;
180
+ }
181
+ if (issue.group_id !== record.groupId) {
182
+ res.status(403).json({ error: "Issue not in shared group" });
183
+ return;
184
+ }
185
+ res.json(db.getIssueMessages(req.params.id));
186
+ });
187
+ apiRouter.get("/share/:token/issues/:id/events", (req, res) => {
188
+ const record = requireToken(store, req.params.token, res);
189
+ if (!record)
190
+ return;
191
+ const issue = db.getIssueById(req.params.id);
192
+ if (!issue) {
193
+ res.status(404).json({ error: "Issue not found" });
194
+ return;
195
+ }
196
+ if (issue.group_id !== record.groupId) {
197
+ res.status(403).json({ error: "Issue not in shared group" });
198
+ return;
199
+ }
200
+ res.json(db.getIssueEvents(req.params.id));
201
+ });
202
+ apiRouter.get("/share/:token/groups/:groupId/notes", (req, res) => {
203
+ const record = requireToken(store, req.params.token, res);
204
+ if (!record)
205
+ return;
206
+ if (req.params.groupId !== record.groupId) {
207
+ res.status(403).json({ error: "Group not in shared scope" });
208
+ return;
209
+ }
210
+ res.json(db.listNotesByGroup(record.groupId));
211
+ });
212
+ // ── Visitor artifact endpoints ───────────────────────────────────────
213
+ // Mirrors src/master/api/artifacts.ts but scoped to the visitor's group.
214
+ // Path traversal guard and 500KB cap are kept identical.
215
+ apiRouter.get("/share/:token/groups/:groupId/artifacts", (req, res) => {
216
+ const record = requireToken(store, req.params.token, res);
217
+ if (!record)
218
+ return;
219
+ if (req.params.groupId !== record.groupId) {
220
+ res.status(403).json({ error: "Group not in shared scope" });
221
+ return;
222
+ }
223
+ const groupDir = resolveGroupArtifactRoot(db, record.groupId);
224
+ if (!fs.existsSync(groupDir)) {
225
+ res.json({ root: groupDir, files: [] });
226
+ return;
227
+ }
228
+ res.json({ root: groupDir, files: walkDir(groupDir, groupDir) });
229
+ });
230
+ apiRouter.get("/share/:token/groups/:groupId/artifacts/content", (req, res) => {
231
+ const record = requireToken(store, req.params.token, res);
232
+ if (!record)
233
+ return;
234
+ if (req.params.groupId !== record.groupId) {
235
+ res.status(403).json({ error: "Group not in shared scope" });
236
+ return;
237
+ }
238
+ const filePath = req.query.path;
239
+ if (!filePath) {
240
+ res.status(400).json({ error: "path query parameter is required" });
241
+ return;
242
+ }
243
+ const groupDir = resolveGroupArtifactRoot(db, record.groupId);
244
+ const result = readFileSafely(groupDir, filePath);
245
+ if (result.kind === "outside-base") {
246
+ res.status(403).json({ error: "Invalid path" });
247
+ return;
248
+ }
249
+ if (result.kind === "missing") {
250
+ res.status(404).json({ error: "File not found" });
251
+ return;
252
+ }
253
+ if (result.kind === "too-large") {
254
+ res.json({
255
+ path: filePath,
256
+ content: `[File too large: ${(result.size / 1024).toFixed(1)}KB]`,
257
+ size: result.size,
258
+ type: "text",
259
+ });
260
+ return;
261
+ }
262
+ res.json({
263
+ path: filePath,
264
+ content: result.content,
265
+ size: result.size,
266
+ type: result.type,
267
+ });
268
+ });
269
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Skill REST API —— 全局 skill 知识库 + (group, agent, skill) 绑定关系。
3
+ *
4
+ * Dashboard 端点开放(无登录),与 memory/notes 一致。
5
+ * skill 本身无可见性;可见性靠 /groups/:id/skills/:agent/bind 端点绑定。
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+ import { createLogger } from "../../shared/logger.js";
9
+ const log = createLogger("mesh-api");
10
+ // ── 校验(对齐 Claude Code SKILL.md 规范)───────────────────────────────
11
+ // name: 小写字母/数字/短横线,首字符非短横线,长度 1-64。禁中文/空格/斜杠。
12
+ const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
13
+ const MAX_NAME = 64;
14
+ const MAX_DESCRIPTION = 1024; // Claude Code 合计 1536,留余量给 when_to_use
15
+ function validateSkillFields(input) {
16
+ if (input.name !== undefined) {
17
+ const n = input.name.trim();
18
+ if (!n)
19
+ return "name 不能为空";
20
+ if (n.length > MAX_NAME)
21
+ return `name 长度超过 ${MAX_NAME}`;
22
+ if (!NAME_RE.test(n))
23
+ return "name 只能用小写字母/数字/短横线,首字符非短横线(对齐 Claude Code skill 命名,禁中文/空格/斜杠)";
24
+ }
25
+ if (input.description !== undefined) {
26
+ const d = input.description.trim();
27
+ if (!d)
28
+ return "description 不能为空";
29
+ if (d.length > MAX_DESCRIPTION)
30
+ return `description 长度超过 ${MAX_DESCRIPTION}(建议说明「做什么 + 何时用」)`;
31
+ }
32
+ if (input.content !== undefined) {
33
+ if (!input.content.trim())
34
+ return "content 不能为空";
35
+ }
36
+ return null;
37
+ }
38
+ export function registerSkillRoutes(apiRouter, db) {
39
+ // ── 全局 skill CRUD ──────────────────────────────────────────────────
40
+ apiRouter.get("/skills", (req, res) => {
41
+ const category = typeof req.query.category === "string" ? req.query.category : undefined;
42
+ res.json(db.listSkills({ category }));
43
+ });
44
+ apiRouter.get("/skills/search", (req, res) => {
45
+ const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
46
+ if (!q) {
47
+ res.status(400).json({ error: "q is required" });
48
+ return;
49
+ }
50
+ res.json(db.searchSkills(q));
51
+ });
52
+ apiRouter.get("/skills/:name", (req, res) => {
53
+ const row = db.getSkillByName(req.params.name);
54
+ if (!row) {
55
+ res.status(404).json({ error: "Skill not found" });
56
+ return;
57
+ }
58
+ // getSkillByName 不计 view_count;手动 +1
59
+ const full = db.getSkill(row.id);
60
+ res.json(full);
61
+ });
62
+ apiRouter.post("/skills", (req, res) => {
63
+ const { name, description, content, category, createdBy } = req.body;
64
+ if (!name || !description || !content || !createdBy) {
65
+ res.status(400).json({ error: "name, description, content, createdBy are required" });
66
+ return;
67
+ }
68
+ const validationErr = validateSkillFields({ name, description, content });
69
+ if (validationErr) {
70
+ res.status(400).json({ error: validationErr });
71
+ return;
72
+ }
73
+ const existing = db.getSkillByName(String(name).trim());
74
+ if (existing) {
75
+ res.status(409).json({ error: `skill "${name}" already exists` });
76
+ return;
77
+ }
78
+ const id = randomUUID();
79
+ db.createSkill({
80
+ id, name: String(name).trim(), description: String(description).trim(),
81
+ content: String(content), category: category ?? null, createdBy,
82
+ });
83
+ log.info(`Skill created: "${name}" (${id})`);
84
+ res.status(201).json({ id, name });
85
+ });
86
+ apiRouter.patch("/skills/:name", (req, res) => {
87
+ const row = db.getSkillByName(req.params.name);
88
+ if (!row) {
89
+ res.status(404).json({ error: "Skill not found" });
90
+ return;
91
+ }
92
+ const { name, description, content, category } = req.body;
93
+ const validationErr = validateSkillFields({ name, description, content });
94
+ if (validationErr) {
95
+ res.status(400).json({ error: validationErr });
96
+ return;
97
+ }
98
+ const fields = {};
99
+ if (name !== undefined)
100
+ fields.name = String(name).trim();
101
+ if (description !== undefined)
102
+ fields.description = String(description).trim();
103
+ if (content !== undefined)
104
+ fields.content = String(content);
105
+ if (category !== undefined)
106
+ fields.category = category == null ? null : String(category);
107
+ db.updateSkill(row.id, fields);
108
+ res.json({ ok: true });
109
+ });
110
+ apiRouter.delete("/skills/:name", (req, res) => {
111
+ const row = db.getSkillByName(req.params.name);
112
+ if (!row) {
113
+ res.status(404).json({ error: "Skill not found" });
114
+ return;
115
+ }
116
+ db.deactivateSkill(row.id);
117
+ res.json({ ok: true });
118
+ });
119
+ // ── 绑定关系 ──────────────────────────────────────────────────────────
120
+ // 该 agent 在该群绑定的 skill 索引(供 agent `rotom skill mine` + prompt count)
121
+ apiRouter.get("/groups/:groupId/skills/:agentName", (req, res) => {
122
+ res.json(db.listSkillsForAgent(req.params.groupId, req.params.agentName));
123
+ });
124
+ apiRouter.post("/groups/:groupId/skills/:agentName/bind", (req, res) => {
125
+ const { skillName, createdBy } = req.body;
126
+ if (!skillName || !createdBy) {
127
+ res.status(400).json({ error: "skillName, createdBy are required" });
128
+ return;
129
+ }
130
+ const skill = db.getSkillByName(String(skillName));
131
+ if (!skill) {
132
+ res.status(404).json({ error: `skill "${skillName}" not found` });
133
+ return;
134
+ }
135
+ const created = db.bindSkill({
136
+ groupId: req.params.groupId,
137
+ agentName: req.params.agentName,
138
+ skillId: skill.id,
139
+ createdBy,
140
+ });
141
+ res.json({ ok: true, created });
142
+ });
143
+ apiRouter.delete("/groups/:groupId/skills/:agentName/bind/:skillName", (req, res) => {
144
+ const skill = db.getSkillByName(req.params.skillName);
145
+ if (!skill) {
146
+ res.json({ ok: true, removed: false });
147
+ return;
148
+ }
149
+ const removed = db.unbindSkill({
150
+ groupId: req.params.groupId,
151
+ agentName: req.params.agentName,
152
+ skillId: skill.id,
153
+ });
154
+ res.json({ ok: true, removed });
155
+ });
156
+ // 群内所有绑定(群设置 modal 用)
157
+ apiRouter.get("/groups/:groupId/skill-bindings", (req, res) => {
158
+ const bindings = db.listBindings({ groupId: req.params.groupId });
159
+ res.json(bindings.map(b => ({
160
+ ...b,
161
+ skill_name: db.getSkill(b.skill_id)?.name ?? null,
162
+ })));
163
+ });
164
+ // 全局绑定总览(工具箱管理用)
165
+ apiRouter.get("/skills/bindings/all", (req, res) => {
166
+ const groupId = typeof req.query.groupId === "string" ? req.query.groupId : undefined;
167
+ const agentName = typeof req.query.agentName === "string" ? req.query.agentName : undefined;
168
+ const bindings = db.listBindings({ groupId, agentName });
169
+ res.json(bindings.map(b => ({
170
+ ...b,
171
+ skill_name: db.getSkill(b.skill_id)?.name ?? null,
172
+ })));
173
+ });
174
+ // ── playbook memory → skill ───────────────────────────────────────────
175
+ apiRouter.post("/memory/:id/promote-to-skill", (req, res) => {
176
+ const { name, description, createdBy } = req.body;
177
+ if (!createdBy) {
178
+ res.status(400).json({ error: "createdBy is required" });
179
+ return;
180
+ }
181
+ try {
182
+ const result = db.promoteMemoryToSkill(req.params.id, { name, description, createdBy });
183
+ log.info(`Memory ${req.params.id} promoted to skill "${result.name}" (${result.skillId})`);
184
+ res.status(201).json(result);
185
+ }
186
+ catch (e) {
187
+ res.status(400).json({ error: e.message });
188
+ }
189
+ });
190
+ }