@swarmroom/server 0.1.0 → 0.3.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.js +15 -4
  3. package/dist/index.js +2 -0
  4. package/dist/public/assets/_agentId-Det2HUaT.js +36 -0
  5. package/dist/public/assets/activity-C5c3pd0l.js +6 -0
  6. package/dist/public/assets/arrow-left-Bho8c3gL.js +6 -0
  7. package/dist/public/assets/avatar-D6PiXqfu.js +1 -0
  8. package/dist/public/assets/card-BWnhVACg.js +1 -0
  9. package/dist/public/assets/chevron-down-io8WQqgh.js +6 -0
  10. package/dist/public/assets/error-boundary-ONLtI3m8.js +11 -0
  11. package/dist/public/assets/external-link-D6ZNJ2De.js +6 -0
  12. package/dist/public/assets/index-B3vl6HkB.js +36 -0
  13. package/dist/public/assets/index-CRKuXBmU.js +16 -0
  14. package/dist/public/assets/index-Coo_PliT.js +213 -0
  15. package/dist/public/assets/index-CrAz8zKh.js +6 -0
  16. package/dist/public/assets/index-DSLPkkn7.js +26 -0
  17. package/dist/public/assets/index-DTF52X7L.js +6 -0
  18. package/dist/public/assets/index-DcbqFmA_.css +1 -0
  19. package/dist/public/assets/index-DrvfFRRH.js +26 -0
  20. package/dist/public/assets/input-CpGy8OTc.js +1 -0
  21. package/dist/public/assets/loader-circle-BBfeX6jI.js +6 -0
  22. package/dist/public/assets/plus-c5hdQFYj.js +6 -0
  23. package/dist/public/assets/radio-zlZI1pp6.js +6 -0
  24. package/dist/public/assets/scroll-area-BD_ZWvua.js +1 -0
  25. package/dist/public/assets/send-D7L2DhCM.js +6 -0
  26. package/dist/public/assets/useMutation-C7s7l6mS.js +1 -0
  27. package/dist/public/assets/user-minus-B2DcEtYp.js +11 -0
  28. package/dist/public/assets/utils-Dt8_kYy9.js +6 -0
  29. package/dist/public/assets/wifi-CkkG3jQZ.js +26 -0
  30. package/dist/public/index.html +14 -0
  31. package/dist/public/vite.svg +4 -0
  32. package/dist/routes/skills.d.ts +3 -0
  33. package/dist/routes/skills.js +19 -0
  34. package/dist/services/mdns-browser.js +110 -33
  35. package/dist/services/skill-service.d.ts +4 -0
  36. package/dist/services/skill-service.js +81 -0
  37. package/package.json +25 -13
@@ -0,0 +1,26 @@
1
+ import{c as a}from"./index-Coo_PliT.js";/**
2
+ * @license lucide-react v0.400.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const o=a("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);/**
7
+ * @license lucide-react v0.400.0 - ISC
8
+ *
9
+ * This source code is licensed under the ISC license.
10
+ * See the LICENSE file in the root directory of this source tree.
11
+ */const t=a("CodeXml",[["path",{d:"m18 16 4-4-4-4",key:"1inbqp"}],["path",{d:"m6 8-4 4 4 4",key:"15zrgr"}],["path",{d:"m14.5 4-5 16",key:"e7oirm"}]]);/**
12
+ * @license lucide-react v0.400.0 - ISC
13
+ *
14
+ * This source code is licensed under the ISC license.
15
+ * See the LICENSE file in the root directory of this source tree.
16
+ */const p=a("FileJson",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1",key:"1oajmo"}],["path",{d:"M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1",key:"mpwhp6"}]]);/**
17
+ * @license lucide-react v0.400.0 - ISC
18
+ *
19
+ * This source code is licensed under the ISC license.
20
+ * See the LICENSE file in the root directory of this source tree.
21
+ */const y=a("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);/**
22
+ * @license lucide-react v0.400.0 - ISC
23
+ *
24
+ * This source code is licensed under the ISC license.
25
+ * See the LICENSE file in the root directory of this source tree.
26
+ */const c=a("Wifi",[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]]);export{o as C,p as F,y as G,c as W,t as a};
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>SwarmRoom</title>
8
+ <script type="module" crossorigin src="/assets/index-Coo_PliT.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DcbqFmA_.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect width="32" height="32" rx="6" fill="#6366f1"/>
3
+ <path d="M8 16C8 11.6 11.6 8 16 8s8 3.6 8 8-3.6 8-8 8" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
4
+ <circle cx="16" cy="16" r="3" fill="#a78bfa"/>
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ declare const skillsRoute: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
3
+ export { skillsRoute };
@@ -0,0 +1,19 @@
1
+ import { Hono } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { listSkills, getSkill } from '../services/skill-service.js';
4
+ const skillsRoute = new Hono();
5
+ skillsRoute.get('/', (c) => {
6
+ const skills = listSkills();
7
+ return c.json({ success: true, data: skills });
8
+ });
9
+ skillsRoute.get('/:name', (c) => {
10
+ const name = c.req.param('name');
11
+ const skill = getSkill(name);
12
+ if (!skill) {
13
+ throw new HTTPException(404, {
14
+ message: `Skill "${name}" not found`,
15
+ });
16
+ }
17
+ return c.json({ success: true, data: skill });
18
+ });
19
+ export { skillsRoute };
@@ -1,53 +1,130 @@
1
- import { createSocket } from 'dgram';
2
- import { MDNS_SERVICE_TYPE } from '@swarmroom/shared';
3
- const MDNS_ADDRESS = '224.0.0.251';
4
- const MDNS_PORT = 5353;
5
- let socket = null;
1
+ import mdns from 'multicast-dns';
2
+ import { MDNS_SERVICE_TYPE, DEFAULT_PORT } from '@swarmroom/shared';
3
+ const SERVICE_NAME = `${MDNS_SERVICE_TYPE}.local`;
4
+ const QUERY_INTERVAL_MS = 30_000;
5
+ let browser = null;
6
+ let queryTimer = null;
6
7
  let running = false;
8
+ let lastDiscoveredUrl = null;
9
+ function normalizeName(name) {
10
+ return name.endsWith('.') ? name.slice(0, -1) : name;
11
+ }
12
+ function isServiceRecordName(name) {
13
+ return normalizeName(name).endsWith(SERVICE_NAME);
14
+ }
15
+ function parseTxtEntries(data) {
16
+ const entries = Array.isArray(data) ? data : [data];
17
+ return entries
18
+ .map((entry) => (Buffer.isBuffer(entry) ? entry.toString('utf8') : String(entry)))
19
+ .map((entry) => entry.trim())
20
+ .filter((entry) => entry.length > 0);
21
+ }
22
+ function findTxtUrl(records) {
23
+ for (const record of records) {
24
+ if (record.type !== 'TXT' || !isServiceRecordName(record.name))
25
+ continue;
26
+ const entries = parseTxtEntries(record.data);
27
+ for (const entry of entries) {
28
+ const separatorIndex = entry.indexOf('=');
29
+ if (separatorIndex === -1)
30
+ continue;
31
+ const key = entry.slice(0, separatorIndex).trim();
32
+ const value = entry.slice(separatorIndex + 1).trim();
33
+ if (key === 'url' && value) {
34
+ return value;
35
+ }
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function findSrvRecord(records) {
41
+ for (const record of records) {
42
+ if (record.type === 'SRV' && isServiceRecordName(record.name)) {
43
+ return record;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function resolveTargetIp(target, records) {
49
+ const normalizedTarget = normalizeName(target);
50
+ let fallbackIpv6 = null;
51
+ for (const record of records) {
52
+ if (record.type !== 'A' && record.type !== 'AAAA')
53
+ continue;
54
+ if (normalizeName(record.name) !== normalizedTarget)
55
+ continue;
56
+ if (record.type === 'A' && typeof record.data === 'string') {
57
+ return record.data;
58
+ }
59
+ if (record.type === 'AAAA' && typeof record.data === 'string') {
60
+ fallbackIpv6 = record.data;
61
+ }
62
+ }
63
+ return fallbackIpv6;
64
+ }
65
+ function extractHubUrl(records) {
66
+ const txtUrl = findTxtUrl(records);
67
+ if (txtUrl)
68
+ return txtUrl;
69
+ const srvRecord = findSrvRecord(records);
70
+ if (!srvRecord)
71
+ return null;
72
+ const srvData = srvRecord.data;
73
+ const targetIp = resolveTargetIp(srvData.target, records);
74
+ if (!targetIp)
75
+ return null;
76
+ const port = srvData.port ?? DEFAULT_PORT;
77
+ return `http://${targetIp}:${port}`;
78
+ }
79
+ function sendQuery() {
80
+ if (!browser)
81
+ return;
82
+ browser.query({ questions: [{ name: SERVICE_NAME, type: 'PTR' }] });
83
+ }
7
84
  export function startBrowsing() {
8
85
  if (process.env.SWARMROOM_DISABLE_MDNS === 'true') {
9
86
  return;
10
87
  }
88
+ if (running)
89
+ return;
11
90
  try {
12
- socket = createSocket({ type: 'udp4', reuseAddr: true });
13
- socket.on('message', (msg) => {
14
- const content = msg.toString('utf8');
15
- if (content.includes(MDNS_SERVICE_TYPE.replace(/^_/, '').replace(/\._tcp$/, ''))) {
16
- console.log(`[mdns-browser] Detected ${MDNS_SERVICE_TYPE} service activity on the network`);
17
- }
91
+ browser = mdns();
92
+ running = true;
93
+ console.log(`[mdns-browser] Browsing for ${MDNS_SERVICE_TYPE} services on the network`);
94
+ browser.on('response', (response) => {
95
+ const records = [...(response.answers ?? []), ...(response.additionals ?? [])];
96
+ const url = extractHubUrl(records);
97
+ if (!url)
98
+ return;
99
+ if (url === lastDiscoveredUrl)
100
+ return;
101
+ lastDiscoveredUrl = url;
102
+ console.log(`[mdns-browser] Discovered SwarmRoom hub at ${url}`);
18
103
  });
19
- socket.on('error', (err) => {
20
- console.warn('[mdns-browser] Socket error:', err.message);
21
- stopBrowsing();
22
- });
23
- socket.bind(MDNS_PORT, () => {
24
- try {
25
- socket?.addMembership(MDNS_ADDRESS);
26
- running = true;
27
- console.log(`[mdns-browser] Browsing for ${MDNS_SERVICE_TYPE} services on the network`);
28
- }
29
- catch (error) {
30
- console.warn('[mdns-browser] Failed to join multicast group:', error);
31
- stopBrowsing();
32
- }
104
+ browser.on('error', (error) => {
105
+ console.warn('[mdns-browser] mDNS error:', error.message);
33
106
  });
107
+ sendQuery();
108
+ queryTimer = setInterval(sendQuery, QUERY_INTERVAL_MS);
34
109
  }
35
110
  catch (error) {
36
111
  console.warn('[mdns-browser] Failed to start browsing — continuing without mDNS browsing:', error);
37
- socket = null;
112
+ stopBrowsing();
38
113
  }
39
114
  }
40
115
  export function stopBrowsing() {
41
- if (socket) {
42
- try {
43
- socket.close();
44
- }
45
- catch {
46
- }
47
- socket = null;
116
+ if (queryTimer) {
117
+ clearInterval(queryTimer);
118
+ queryTimer = null;
119
+ }
120
+ if (browser) {
121
+ browser.removeAllListeners();
122
+ browser.destroy();
123
+ browser = null;
48
124
  }
49
125
  if (running) {
50
126
  running = false;
127
+ lastDiscoveredUrl = null;
51
128
  console.log('[mdns-browser] Stopped browsing');
52
129
  }
53
130
  }
@@ -0,0 +1,4 @@
1
+ import type { SkillInfo, SkillSummary } from '@swarmroom/shared';
2
+ export declare function scanSkills(dirs?: string[]): void;
3
+ export declare function listSkills(): SkillSummary[];
4
+ export declare function getSkill(name: string): SkillInfo | null;
@@ -0,0 +1,81 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const skills = new Map();
5
+ function parseFrontmatter(content) {
6
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
7
+ if (!match)
8
+ return { frontmatter: {}, body: content };
9
+ const frontmatter = {};
10
+ for (const line of match[1].split('\n')) {
11
+ const [key, ...rest] = line.split(':');
12
+ if (key && rest.length) {
13
+ frontmatter[key.trim()] = rest.join(':').trim().replace(/^["']|["']$/g, '');
14
+ }
15
+ }
16
+ return { frontmatter, body: match[2].trim() };
17
+ }
18
+ function scanDirectory(dir) {
19
+ if (!existsSync(dir))
20
+ return;
21
+ try {
22
+ const entries = readdirSync(dir, { withFileTypes: true });
23
+ for (const entry of entries) {
24
+ if (entry.isDirectory()) {
25
+ const skillPath = join(dir, entry.name, 'SKILL.md');
26
+ if (existsSync(skillPath)) {
27
+ try {
28
+ const content = readFileSync(skillPath, 'utf-8');
29
+ const { frontmatter, body } = parseFrontmatter(content);
30
+ const name = frontmatter.name || entry.name;
31
+ const description = frontmatter.description || '';
32
+ const metadata = {};
33
+ for (const [key, value] of Object.entries(frontmatter)) {
34
+ if (key !== 'name' && key !== 'description') {
35
+ metadata[key] = value;
36
+ }
37
+ }
38
+ skills.set(name, {
39
+ name,
40
+ description,
41
+ location: skillPath,
42
+ content: body,
43
+ metadata,
44
+ });
45
+ }
46
+ catch (error) {
47
+ console.warn(`Failed to load skill from ${skillPath}:`, error);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ catch (error) {
54
+ console.warn(`Failed to scan directory ${dir}:`, error);
55
+ }
56
+ }
57
+ export function scanSkills(dirs) {
58
+ const defaultDirs = [
59
+ resolve(homedir(), '.swarmroom', 'skills'),
60
+ resolve(homedir(), '.opencode', 'skills'),
61
+ resolve(homedir(), '.claude', 'skills'),
62
+ resolve(process.cwd(), '.swarmroom', 'skills'),
63
+ ];
64
+ const scanDirs = dirs || defaultDirs;
65
+ skills.clear();
66
+ for (const dir of scanDirs) {
67
+ scanDirectory(dir);
68
+ }
69
+ console.log(`Loaded ${skills.size} skill(s) from ${scanDirs.length} directories`);
70
+ }
71
+ export function listSkills() {
72
+ return Array.from(skills.values()).map(skill => ({
73
+ name: skill.name,
74
+ description: skill.description,
75
+ location: skill.location,
76
+ metadata: skill.metadata,
77
+ }));
78
+ }
79
+ export function getSkill(name) {
80
+ return skills.get(name) || null;
81
+ }
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@swarmroom/server",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "SwarmRoom hub server - HTTP, WebSocket, MCP, and mDNS services",
6
- "keywords": ["swarmroom", "server", "hub", "mcp", "websocket"],
6
+ "keywords": [
7
+ "swarmroom",
8
+ "server",
9
+ "hub",
10
+ "mcp",
11
+ "websocket"
12
+ ],
7
13
  "license": "MIT",
8
14
  "publishConfig": {
9
15
  "access": "public"
@@ -19,29 +25,35 @@
19
25
  ".": "./dist/index.js",
20
26
  "./dist/*": "./dist/*"
21
27
  },
22
- "files": ["dist", "README.md", "LICENSE"],
23
- "scripts": {
24
- "build": "tsc",
25
- "dev": "tsx watch src/index.ts",
26
- "start": "node dist/index.js",
27
- "test": "vitest run",
28
- "prepublishOnly": "npm run build"
29
- },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
30
33
  "dependencies": {
31
34
  "@homebridge/ciao": "^1.3.5",
32
35
  "@hono/node-server": "^1.19.9",
33
36
  "@hono/node-ws": "^1.3.0",
34
37
  "@modelcontextprotocol/sdk": "^1.26.0",
35
- "@swarmroom/shared": "^0.1.0",
36
38
  "better-sqlite3": "^12.6.2",
37
39
  "drizzle-orm": "^0.45.1",
38
40
  "hono": "^4.11.9",
41
+ "multicast-dns": "^7.2.5",
39
42
  "tsx": "^4.21.0",
40
- "zod": "^4.3.6"
43
+ "zod": "^4.3.6",
44
+ "@swarmroom/shared": "0.3.0"
41
45
  },
42
46
  "devDependencies": {
43
47
  "@types/better-sqlite3": "^7.6.13",
48
+ "@types/multicast-dns": "^7.2.4",
44
49
  "drizzle-kit": "^0.31.9",
45
50
  "vitest": "^4.0.18"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc",
54
+ "postbuild": "node -e \"const fs=require('fs'),p=require('path'),s=p.join(__dirname,'../web/dist'),d=p.join(__dirname,'dist/public');fs.existsSync(s)?(fs.cpSync(s,d,{recursive:true}),console.log('Web dashboard bundled')):console.log('Web dist not found, skipping')\"",
55
+ "dev": "tsx watch src/index.ts",
56
+ "start": "node dist/index.js",
57
+ "test": "vitest run"
46
58
  }
47
- }
59
+ }