@peonai/swarm 0.1.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 (61) hide show
  1. package/.dockerignore +6 -0
  2. package/Dockerfile +9 -0
  3. package/README.md +29 -0
  4. package/app/api/health/route.ts +2 -0
  5. package/app/api/v1/admin/agents/[id]/route.ts +12 -0
  6. package/app/api/v1/admin/agents/route.ts +31 -0
  7. package/app/api/v1/admin/audit/route.ts +23 -0
  8. package/app/api/v1/admin/cleanup/route.ts +21 -0
  9. package/app/api/v1/admin/export/route.ts +15 -0
  10. package/app/api/v1/admin/history/route.ts +23 -0
  11. package/app/api/v1/admin/profile/route.ts +23 -0
  12. package/app/api/v1/admin/settings/route.ts +44 -0
  13. package/app/api/v1/auth/route.ts +5 -0
  14. package/app/api/v1/memory/route.ts +105 -0
  15. package/app/api/v1/persona/[agentId]/route.ts +13 -0
  16. package/app/api/v1/persona/me/route.ts +12 -0
  17. package/app/api/v1/profile/observe/route.ts +34 -0
  18. package/app/api/v1/profile/route.ts +72 -0
  19. package/app/api/v1/reflect/route.ts +96 -0
  20. package/app/globals.css +190 -0
  21. package/app/i18n.ts +161 -0
  22. package/app/layout.tsx +12 -0
  23. package/app/page.tsx +561 -0
  24. package/docker-compose.yml +34 -0
  25. package/docs/DEBATE-ROUND1.md +244 -0
  26. package/docs/DEBATE-ROUND2.md +158 -0
  27. package/docs/REQUIREMENTS.md +162 -0
  28. package/docs/docs.html +272 -0
  29. package/docs/index.html +228 -0
  30. package/docs/script.js +103 -0
  31. package/docs/style.css +418 -0
  32. package/lib/auth.ts +63 -0
  33. package/lib/db.ts +63 -0
  34. package/lib/embedding.ts +29 -0
  35. package/lib/schema.ts +134 -0
  36. package/mcp-server.ts +56 -0
  37. package/next-env.d.ts +6 -0
  38. package/next.config.ts +5 -0
  39. package/package.json +34 -0
  40. package/packages/cli/README.md +33 -0
  41. package/packages/cli/bin/swarm.mjs +274 -0
  42. package/packages/cli/package.json +10 -0
  43. package/postcss.config.mjs +2 -0
  44. package/skill/CLAUDE.md +40 -0
  45. package/skill/CODEX.md +43 -0
  46. package/skill/GEMINI.md +38 -0
  47. package/skill/IFLOW.md +38 -0
  48. package/skill/OPENCODE.md +38 -0
  49. package/skill/swarm-ai-skill/SKILL.md +74 -0
  50. package/skill/swarm-ai-skill/env.sh +4 -0
  51. package/skill/swarm-ai-skill/scripts/bootstrap.sh +21 -0
  52. package/skill/swarm-ai-skill/scripts/env.sh +4 -0
  53. package/skill/swarm-ai-skill/scripts/env.sh.example +3 -0
  54. package/skill/swarm-ai-skill/scripts/memory-read.sh +8 -0
  55. package/skill/swarm-ai-skill/scripts/memory-write.sh +10 -0
  56. package/skill/swarm-ai-skill/scripts/observe.sh +9 -0
  57. package/skill/swarm-ai-skill/scripts/profile-read.sh +9 -0
  58. package/skill/swarm-ai-skill/scripts/profile-update.sh +9 -0
  59. package/skill/swarm-ai-skill/scripts/session-start.sh +19 -0
  60. package/tsconfig.json +21 -0
  61. package/tsconfig.tsbuildinfo +1 -0
package/lib/schema.ts ADDED
@@ -0,0 +1,134 @@
1
+ /* schema.ts — DB schema init, compatible with SQLite + PostgreSQL */
2
+ import db, { isPg } from './db';
3
+
4
+ const AUTO_ID = isPg ? 'SERIAL PRIMARY KEY' : 'INTEGER PRIMARY KEY AUTOINCREMENT';
5
+ const NOW = isPg ? 'NOW()' : "datetime('now')";
6
+ const TEXT_UNIQUE = isPg ? 'TEXT UNIQUE' : 'TEXT';
7
+
8
+ let _initialized = false;
9
+
10
+ export async function initSchema() {
11
+ if (_initialized) return;
12
+ _initialized = true;
13
+
14
+ await db.exec(`CREATE TABLE IF NOT EXISTS users (
15
+ id TEXT PRIMARY KEY, name TEXT, email ${TEXT_UNIQUE},
16
+ password_hash TEXT, role TEXT DEFAULT 'user',
17
+ created_at TEXT DEFAULT (${NOW})
18
+ )`);
19
+
20
+ if (!isPg) {
21
+ // SQLite migrations
22
+ for (const sql of [
23
+ 'ALTER TABLE users ADD COLUMN email TEXT',
24
+ 'ALTER TABLE users ADD COLUMN password_hash TEXT',
25
+ "ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'",
26
+ ]) { try { await db.exec(sql); } catch {} }
27
+ try { await db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE email IS NOT NULL'); } catch {}
28
+ }
29
+
30
+ await db.exec(`CREATE TABLE IF NOT EXISTS profiles (
31
+ id ${AUTO_ID},
32
+ user_id TEXT NOT NULL,
33
+ layer TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL,
34
+ confidence REAL DEFAULT 1.0, source TEXT, tags TEXT,
35
+ expires_at TEXT, updated_at TEXT DEFAULT (${NOW}),
36
+ UNIQUE(user_id, layer, key)
37
+ )`);
38
+
39
+ await db.exec(`CREATE TABLE IF NOT EXISTS agents (
40
+ id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
41
+ name TEXT NOT NULL, api_key TEXT UNIQUE NOT NULL,
42
+ permissions TEXT DEFAULT 'read', persona TEXT,
43
+ created_at TEXT DEFAULT (${NOW})
44
+ )`);
45
+
46
+ await db.exec(`CREATE TABLE IF NOT EXISTS memories (
47
+ id ${AUTO_ID},
48
+ user_id TEXT NOT NULL, key TEXT, content TEXT NOT NULL,
49
+ source TEXT, tags TEXT, type TEXT DEFAULT 'observation',
50
+ importance REAL DEFAULT 0.5, entities TEXT,
51
+ created_at TEXT DEFAULT (${NOW})
52
+ )`);
53
+
54
+ if (!isPg) {
55
+ // SQLite FTS5
56
+ try {
57
+ await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
58
+ content, tags, entities, key, content_rowid='id', content='memories'
59
+ )`);
60
+ await db.exec(`CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
61
+ INSERT INTO memories_fts(rowid, content, tags, entities, key) VALUES (new.id, new.content, new.tags, new.entities, new.key);
62
+ END`);
63
+ await db.exec(`CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
64
+ INSERT INTO memories_fts(memories_fts, rowid, content, tags, entities, key) VALUES ('delete', old.id, old.content, old.tags, old.entities, old.key);
65
+ END`);
66
+ await db.exec(`CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
67
+ INSERT INTO memories_fts(memories_fts, rowid, content, tags, entities, key) VALUES ('delete', old.id, old.content, old.tags, old.entities, old.key);
68
+ INSERT INTO memories_fts(rowid, content, tags, entities, key) VALUES (new.id, new.content, new.tags, new.entities, new.key);
69
+ END`);
70
+ } catch {}
71
+
72
+ for (const sql of [
73
+ "ALTER TABLE memories ADD COLUMN type TEXT DEFAULT 'observation'",
74
+ 'ALTER TABLE memories ADD COLUMN importance REAL DEFAULT 0.5',
75
+ 'ALTER TABLE memories ADD COLUMN entities TEXT',
76
+ 'ALTER TABLE memories ADD COLUMN embedding TEXT',
77
+ ]) { try { await db.exec(sql); } catch {} }
78
+ } else {
79
+ // PostgreSQL full-text search index
80
+ try {
81
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_fts ON memories USING gin(to_tsvector('english', content))`);
82
+ } catch {}
83
+ }
84
+
85
+ // Audit log table
86
+ await db.exec(`CREATE TABLE IF NOT EXISTS audit_log (
87
+ id ${AUTO_ID},
88
+ user_id TEXT NOT NULL,
89
+ agent_id TEXT,
90
+ action TEXT NOT NULL,
91
+ target_type TEXT NOT NULL,
92
+ target_id TEXT,
93
+ detail TEXT,
94
+ created_at TEXT DEFAULT (${NOW})
95
+ )`);
96
+
97
+ // Profile history table
98
+ await db.exec(`CREATE TABLE IF NOT EXISTS profile_history (
99
+ id ${AUTO_ID},
100
+ user_id TEXT NOT NULL,
101
+ layer TEXT NOT NULL,
102
+ key TEXT NOT NULL,
103
+ old_value TEXT,
104
+ new_value TEXT NOT NULL,
105
+ source TEXT,
106
+ created_at TEXT DEFAULT (${NOW})
107
+ )`);
108
+ }
109
+
110
+ export async function getAgentByKey(apiKey: string) {
111
+ await initSchema();
112
+ return db.prepare('SELECT id, user_id as "userId", permissions FROM agents WHERE api_key = ?').get(apiKey) as
113
+ Promise<{ id: string; userId: string; permissions: string } | null>;
114
+ }
115
+
116
+ export async function ensureDefaultUser() {
117
+ await initSchema();
118
+ let user = await db.prepare('SELECT id FROM users LIMIT 1').get() as any;
119
+ if (!user) {
120
+ await db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').run('default', 'Default User');
121
+ return 'default';
122
+ }
123
+ return user.id;
124
+ }
125
+
126
+ export async function logAudit(userId: string, agentId: string | null, action: string, targetType: string, targetId?: string, detail?: string) {
127
+ await db.prepare('INSERT INTO audit_log (user_id, agent_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)')
128
+ .run(userId, agentId, action, targetType, targetId || null, detail || null);
129
+ }
130
+
131
+ export async function logProfileHistory(userId: string, layer: string, key: string, oldValue: string | null, newValue: string, source: string) {
132
+ await db.prepare('INSERT INTO profile_history (user_id, layer, key, old_value, new_value, source) VALUES (?,?,?,?,?,?)')
133
+ .run(userId, layer, key, oldValue, newValue, source);
134
+ }
package/mcp-server.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+
6
+ const API = process.env.SWARM_API_URL || 'http://localhost:3777';
7
+ const KEY = process.env.SWARM_API_KEY || '';
8
+ const hdrs = { 'Content-Type': 'application/json', Authorization: `Bearer ${KEY}` };
9
+ const api = async (path: string, opts?: RequestInit) =>
10
+ (await fetch(`${API}${path}`, { ...opts, headers: { ...hdrs, ...opts?.headers } })).json();
11
+ const txt = (data: any) => ({ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] });
12
+
13
+ const server = new McpServer({ name: 'swarm-ai', version: '0.1.0' });
14
+
15
+ server.tool('read_profile', 'Read user profile. Filter by layer or tag.',
16
+ { layer: z.string().optional(), tag: z.string().optional() },
17
+ async ({ layer, tag }) => {
18
+ const p = new URLSearchParams();
19
+ if (layer) p.set('layer', layer);
20
+ if (tag) p.set('tag', tag);
21
+ return txt(await api(`/api/v1/profile?${p}`));
22
+ }
23
+ );
24
+
25
+ server.tool('update_profile', 'Update profile entries in a layer',
26
+ { layer: z.string(), entries: z.record(z.any()) },
27
+ async ({ layer, entries }) => txt(await api('/api/v1/profile', { method: 'PATCH', body: JSON.stringify({ layer, entries }) }))
28
+ );
29
+
30
+ server.tool('observe', 'Submit observations about the user',
31
+ { observations: z.array(z.object({ layer: z.string().optional(), key: z.string(), value: z.any(), confidence: z.number().optional(), tags: z.array(z.string()).optional() })) },
32
+ async ({ observations }) => txt(await api('/api/v1/profile/observe', { method: 'POST', body: JSON.stringify({ observations }) }))
33
+ );
34
+
35
+ server.tool('search_memory', 'Search shared memory across agents',
36
+ { q: z.string().optional(), tag: z.string().optional(), limit: z.number().optional() },
37
+ async ({ q, tag, limit }) => {
38
+ const p = new URLSearchParams();
39
+ if (q) p.set('q', q);
40
+ if (tag) p.set('tag', tag);
41
+ if (limit) p.set('limit', String(limit));
42
+ return txt(await api(`/api/v1/memory?${p}`));
43
+ }
44
+ );
45
+
46
+ server.tool('write_memory', 'Write a shared memory entry',
47
+ { content: z.string(), key: z.string().optional(), tags: z.array(z.string()).optional() },
48
+ async (args) => txt(await api('/api/v1/memory', { method: 'POST', body: JSON.stringify(args) }))
49
+ );
50
+
51
+ server.tool('read_persona', 'Read current agent persona config', {},
52
+ async () => txt(await api('/api/v1/persona/me'))
53
+ );
54
+
55
+ const transport = new StdioServerTransport();
56
+ server.connect(transport).catch(console.error);
package/next-env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ /// <reference path="./.next/types/routes.d.ts" />
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/next.config.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from 'next';
2
+ const config: NextConfig = {
3
+ serverExternalPackages: ['node:sqlite'],
4
+ };
5
+ export default config;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@peonai/swarm",
3
+ "version": "0.1.0",
4
+ "description": "Cross-agent user profile sync — let every AI agent know your user without re-teaching",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/peonai/swarm.git"
8
+ },
9
+ "keywords": ["ai", "agent", "profile", "mcp", "swarm"],
10
+ "license": "MIT",
11
+ "author": "PeonAI",
12
+ "scripts": {
13
+ "dev": "next dev -p 3777",
14
+ "build": "next build",
15
+ "start": "next start -p 3777",
16
+ "test": "vitest run"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.26.0",
20
+ "next": "^15.3.4",
21
+ "pg": "^8.18.0",
22
+ "react": "^19.1.0",
23
+ "react-dom": "^19.1.0",
24
+ "zod": "^3.25.76"
25
+ },
26
+ "devDependencies": {
27
+ "@tailwindcss/postcss": "^4.1.8",
28
+ "@types/node": "^24.0.3",
29
+ "@types/react": "^19.1.6",
30
+ "tailwindcss": "^4.1.8",
31
+ "typescript": "^5.8.3",
32
+ "vitest": "^4.0.18"
33
+ }
34
+ }
@@ -0,0 +1,33 @@
1
+ # @peonai/swarm
2
+
3
+ One-command install & run for [Swarm AI](https://github.com/peonai/swarm-ai).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @peonai/swarm
9
+ ```
10
+
11
+ Interactive setup will ask for:
12
+ - **Install directory** — where to clone & store data (default: `~/.swarm-ai`)
13
+ - **Port** — HTTP port (default: `3777`)
14
+ - **Admin token** — for dashboard access
15
+ - **Service mode** — install as systemd/launchd background service
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ npx @peonai/swarm # install & configure
21
+ npx @peonai/swarm start # start service
22
+ npx @peonai/swarm stop # stop service
23
+ npx @peonai/swarm status # check if running
24
+ npx @peonai/swarm uninstall # remove service (keeps data)
25
+ ```
26
+
27
+ ## Service Support
28
+
29
+ | OS | Method |
30
+ |---|---|
31
+ | Linux | systemd |
32
+ | macOS | launchd |
33
+ | Windows | manual / pm2 |
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ // @peonai/swarm — one-command install & run
3
+ import { createInterface } from 'node:readline';
4
+ import { execSync, spawn } from 'node:child_process';
5
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { homedir, platform, arch } from 'node:os';
8
+
9
+ const REPO = 'https://github.com/euynahz/swarm-ai.git';
10
+ const AMBER = '\x1b[38;2;240;168;48m';
11
+ const DIM = '\x1b[2m';
12
+ const BOLD = '\x1b[1m';
13
+ const R = '\x1b[0m';
14
+ const GREEN = '\x1b[32m';
15
+ const RED = '\x1b[31m';
16
+
17
+ const hex = `
18
+ ${DIM}⬡ ⬡ ⬡${R}
19
+ ${AMBER}⬡${R} ${DIM}⬡${R} ${AMBER}⬡${R}
20
+ ${DIM}⬡ ⬡ ⬡${R}`;
21
+
22
+ function log(msg) { console.log(` ${msg}`); }
23
+ function ok(msg) { log(`${GREEN}✓${R} ${msg}`); }
24
+ function err(msg) { log(`${RED}✗${R} ${msg}`); }
25
+ function head(msg) { console.log(`\n ${AMBER}${BOLD}${msg}${R}`); }
26
+
27
+ // ── Prompt helper ──
28
+ function ask(rl, question, fallback) {
29
+ return new Promise(res => {
30
+ const suffix = fallback ? ` ${DIM}(${fallback})${R}` : '';
31
+ rl.question(` ${question}${suffix}: `, ans => res(ans.trim() || fallback || ''));
32
+ });
33
+ }
34
+ function confirm(rl, question, def = true) {
35
+ return new Promise(res => {
36
+ const hint = def ? 'Y/n' : 'y/N';
37
+ rl.question(` ${question} ${DIM}(${hint})${R}: `, ans => {
38
+ const a = ans.trim().toLowerCase();
39
+ res(a ? a === 'y' || a === 'yes' : def);
40
+ });
41
+ });
42
+ }
43
+
44
+ // ── Check prerequisites ──
45
+ function checkPrereqs() {
46
+ head('Checking prerequisites');
47
+ const nodeV = process.versions.node.split('.').map(Number);
48
+ if (nodeV[0] < 18) { err(`Node.js >= 18 required (got ${process.version})`); process.exit(1); }
49
+ ok(`Node.js ${process.version}`);
50
+
51
+ try { execSync('git --version', { stdio: 'pipe' }); ok('git'); }
52
+ catch { err('git not found — install git first'); process.exit(1); }
53
+ }
54
+
55
+ // ── Main ──
56
+ async function main() {
57
+ console.log(hex);
58
+ head('Swarm AI Installer');
59
+ log(`${DIM}Cross-agent user profile sync${R}\n`);
60
+
61
+ const cmd = process.argv[2];
62
+ if (cmd === 'start') return startServer();
63
+ if (cmd === 'stop') return stopService();
64
+ if (cmd === 'status') return showStatus();
65
+ if (cmd === 'uninstall') return uninstall();
66
+
67
+ checkPrereqs();
68
+
69
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
70
+
71
+ try {
72
+ await setup(rl);
73
+ } finally {
74
+ rl.close();
75
+ }
76
+ }
77
+
78
+ async function setup(rl) {
79
+ const defaultDir = join(homedir(), '.swarm-ai');
80
+
81
+ head('Configuration');
82
+ const dataDir = resolve(await ask(rl, 'Install directory', defaultDir));
83
+ const port = await ask(rl, 'Port', '3777');
84
+ const adminToken = await ask(rl, 'Admin token', 'swarm-admin-dev');
85
+
86
+ head('Embedding (for semantic memory search)');
87
+ log(`${DIM}OpenAI-compatible embedding API. Leave blank to disable semantic search.${R}`);
88
+ const embedUrl = await ask(rl, 'Embedding API URL', '');
89
+ const embedKey = await ask(rl, 'Embedding API Key', '');
90
+ const embedModel = embedUrl ? await ask(rl, 'Embedding model', 'text-embedding-3-small') : '';
91
+
92
+ const asService = await confirm(rl, 'Install as background service?', true);
93
+
94
+ // ── Clone / update ──
95
+ head('Installing');
96
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
97
+
98
+ const serverDir = join(dataDir, 'server');
99
+ if (existsSync(join(serverDir, 'package.json'))) {
100
+ log('Updating existing installation...');
101
+ execSync('git pull --ff-only', { cwd: serverDir, stdio: 'inherit' });
102
+ } else {
103
+ log('Cloning repository...');
104
+ execSync(`git clone --depth 1 ${REPO} "${serverDir}"`, { stdio: 'inherit' });
105
+ }
106
+
107
+ // ── Install deps & build ──
108
+ log('Installing dependencies...');
109
+ execSync('npm install --production=false', { cwd: serverDir, stdio: 'inherit' });
110
+
111
+ log('Building...');
112
+ execSync('npm run build', { cwd: serverDir, stdio: 'inherit' });
113
+ ok('Build complete');
114
+
115
+ // ── Write config ──
116
+ const configPath = join(dataDir, 'config.json');
117
+ const config = { port: Number(port), adminToken, serverDir, service: asService, embedUrl, embedKey, embedModel };
118
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
119
+ ok(`Config saved → ${configPath}`);
120
+
121
+ // ── Write .env ──
122
+ const envPath = join(serverDir, '.env.local');
123
+ const envLines = [
124
+ `PORT=${port}`,
125
+ `SWARM_ADMIN_TOKEN=${adminToken}`,
126
+ `SWARM_DATA_DIR=${dataDir}/data`,
127
+ ];
128
+ if (embedUrl) envLines.push(`EMBED_URL=${embedUrl}`);
129
+ if (embedKey) envLines.push(`EMBED_KEY=${embedKey}`);
130
+ if (embedModel) envLines.push(`EMBED_MODEL=${embedModel}`);
131
+ writeFileSync(envPath, envLines.join('\n') + '\n');
132
+ mkdirSync(join(dataDir, 'data'), { recursive: true });
133
+ ok('.env.local written');
134
+
135
+ // ── Service install ──
136
+ if (asService) {
137
+ installService(config, dataDir);
138
+ }
139
+
140
+ // ── Done ──
141
+ head('Done! 🐝');
142
+ log(`Server directory: ${DIM}${serverDir}${R}`);
143
+ log(`Config: ${DIM}${configPath}${R}`);
144
+ log(`Port: ${AMBER}${port}${R}`);
145
+ console.log();
146
+ if (asService) {
147
+ log(`Service installed. Commands:`);
148
+ log(` ${AMBER}npx @peonai/swarm start${R} — start service`);
149
+ log(` ${AMBER}npx @peonai/swarm stop${R} — stop service`);
150
+ log(` ${AMBER}npx @peonai/swarm status${R} — check status`);
151
+ } else {
152
+ log(`Start manually:`);
153
+ log(` ${AMBER}cd ${serverDir} && npm start${R}`);
154
+ }
155
+ console.log();
156
+ }
157
+
158
+ // ── Service management ──
159
+ function loadConfig() {
160
+ const p = join(homedir(), '.swarm-ai', 'config.json');
161
+ if (!existsSync(p)) { err('Not installed. Run: npx @peonai/swarm'); process.exit(1); }
162
+ return JSON.parse(readFileSync(p, 'utf8'));
163
+ }
164
+
165
+ function installService(config, dataDir) {
166
+ const os = platform();
167
+ if (os === 'linux') return installSystemd(config, dataDir);
168
+ if (os === 'darwin') return installLaunchd(config, dataDir);
169
+ log(`${DIM}Auto-service not supported on ${os}, use pm2 or run manually${R}`);
170
+ }
171
+
172
+ function installSystemd(config, dataDir) {
173
+ const unit = `[Unit]
174
+ Description=Swarm AI Server
175
+ After=network.target
176
+
177
+ [Service]
178
+ Type=simple
179
+ WorkingDirectory=${config.serverDir}
180
+ ExecStart=${process.execPath} ${join(config.serverDir, 'node_modules/.bin/next')} start -p ${config.port}
181
+ Restart=on-failure
182
+ Environment=NODE_ENV=production
183
+
184
+ [Install]
185
+ WantedBy=multi-user.target
186
+ `;
187
+ const unitPath = join(dataDir, 'swarm-ai.service');
188
+ writeFileSync(unitPath, unit);
189
+ ok(`Systemd unit → ${unitPath}`);
190
+
191
+ try {
192
+ const sysPath = '/etc/systemd/system/swarm-ai.service';
193
+ execSync(`sudo ln -sf "${unitPath}" "${sysPath}" && sudo systemctl daemon-reload`, { stdio: 'inherit' });
194
+ ok('Systemd service registered');
195
+ } catch {
196
+ log(`${DIM}Run manually: sudo ln -sf "${unitPath}" /etc/systemd/system/swarm-ai.service${R}`);
197
+ }
198
+ }
199
+
200
+ function installLaunchd(config, dataDir) {
201
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
202
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
203
+ <plist version="1.0"><dict>
204
+ <key>Label</key><string>ai.peon.swarm</string>
205
+ <key>ProgramArguments</key><array>
206
+ <string>${process.execPath}</string>
207
+ <string>${join(config.serverDir, 'node_modules/.bin/next')}</string>
208
+ <string>start</string>
209
+ <string>-p</string>
210
+ <string>${config.port}</string>
211
+ </array>
212
+ <key>WorkingDirectory</key><string>${config.serverDir}</string>
213
+ <key>RunAtLoad</key><true/>
214
+ <key>KeepAlive</key><true/>
215
+ <key>StandardOutPath</key><string>${dataDir}/swarm.log</string>
216
+ <key>StandardErrorPath</key><string>${dataDir}/swarm.err</string>
217
+ </dict></plist>`;
218
+ const plistPath = join(homedir(), 'Library/LaunchAgents/ai.peon.swarm.plist');
219
+ writeFileSync(plistPath, plist);
220
+ ok(`LaunchAgent → ${plistPath}`);
221
+ }
222
+
223
+ function startServer() {
224
+ const config = loadConfig();
225
+ head('Starting Swarm AI');
226
+ if (platform() === 'linux') {
227
+ try { execSync('sudo systemctl start swarm-ai', { stdio: 'inherit' }); ok(`Running on port ${config.port}`); return; } catch {}
228
+ }
229
+ if (platform() === 'darwin') {
230
+ try { execSync('launchctl load ~/Library/LaunchAgents/ai.peon.swarm.plist', { stdio: 'inherit' }); ok(`Running on port ${config.port}`); return; } catch {}
231
+ }
232
+ // Fallback: direct start
233
+ log('Starting in foreground...');
234
+ const child = spawn('npm', ['start'], { cwd: config.serverDir, stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production' } });
235
+ child.on('exit', code => process.exit(code ?? 0));
236
+ }
237
+
238
+ function stopService() {
239
+ head('Stopping Swarm AI');
240
+ if (platform() === 'linux') {
241
+ try { execSync('sudo systemctl stop swarm-ai', { stdio: 'inherit' }); ok('Stopped'); return; } catch {}
242
+ }
243
+ if (platform() === 'darwin') {
244
+ try { execSync('launchctl unload ~/Library/LaunchAgents/ai.peon.swarm.plist', { stdio: 'inherit' }); ok('Stopped'); return; } catch {}
245
+ }
246
+ err('No service found');
247
+ }
248
+
249
+ function showStatus() {
250
+ const config = loadConfig();
251
+ head('Swarm AI Status');
252
+ log(`Port: ${config.port}`);
253
+ log(`Dir: ${config.serverDir}`);
254
+ try {
255
+ execSync(`curl -sf http://localhost:${config.port}/api/health`, { stdio: 'pipe' });
256
+ ok(`${GREEN}Running${R}`);
257
+ } catch {
258
+ log(`${RED}● Not running${R}`);
259
+ }
260
+ }
261
+
262
+ function uninstall() {
263
+ head('Uninstalling');
264
+ if (platform() === 'linux') {
265
+ try { execSync('sudo systemctl stop swarm-ai && sudo systemctl disable swarm-ai && sudo rm /etc/systemd/system/swarm-ai.service', { stdio: 'pipe' }); } catch {}
266
+ }
267
+ if (platform() === 'darwin') {
268
+ try { execSync('launchctl unload ~/Library/LaunchAgents/ai.peon.swarm.plist', { stdio: 'pipe' }); } catch {}
269
+ }
270
+ ok('Service removed. Data kept at ~/.swarm-ai/');
271
+ log(`${DIM}To fully remove: rm -rf ~/.swarm-ai${R}`);
272
+ }
273
+
274
+ main().catch(e => { err(e.message); process.exit(1); });
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@peonai/swarm",
3
+ "version": "0.1.0",
4
+ "description": "One-command install & run for Swarm AI",
5
+ "bin": { "swarm": "./bin/swarm.mjs" },
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "engines": { "node": ">=18" },
9
+ "files": ["bin/", "lib/", "README.md"]
10
+ }
@@ -0,0 +1,2 @@
1
+ const config = { plugins: { "@tailwindcss/postcss": {} } };
2
+ export default config;
@@ -0,0 +1,40 @@
1
+ # Swarm AI Integration
2
+
3
+ You have access to a shared user profile API. Use it to personalize responses and record what you learn.
4
+
5
+ ## API
6
+
7
+ Base URL: `http://localhost:3777`
8
+
9
+ All requests need: `Authorization: Bearer $SWARM_API_KEY`
10
+
11
+ ### Read user profile
12
+ ```bash
13
+ curl -s http://localhost:3777/api/v1/profile -H "Authorization: Bearer $SWARM_API_KEY"
14
+ ```
15
+
16
+ ### Submit observation (when you learn something new about the user)
17
+ ```bash
18
+ curl -s -X POST http://localhost:3777/api/v1/profile/observe \
19
+ -H "Content-Type: application/json" \
20
+ -H "Authorization: Bearer $SWARM_API_KEY" \
21
+ -d '{"observations":[{"layer":"context","key":"KEY","value":"VALUE","confidence":0.8}]}'
22
+ ```
23
+
24
+ ### Search shared memory
25
+ ```bash
26
+ curl -s "http://localhost:3777/api/v1/memory?q=QUERY" -H "Authorization: Bearer $SWARM_API_KEY"
27
+ ```
28
+
29
+ ### Write memory
30
+ ```bash
31
+ curl -s -X POST http://localhost:3777/api/v1/memory \
32
+ -H "Content-Type: application/json" \
33
+ -H "Authorization: Bearer $SWARM_API_KEY" \
34
+ -d '{"content":"CONTENT","tags":["tag1"]}'
35
+ ```
36
+
37
+ ## When to use
38
+ - Session start: read profile to understand user
39
+ - During interaction: observe new preferences/context
40
+ - Need history: search memory
package/skill/CODEX.md ADDED
@@ -0,0 +1,43 @@
1
+ # Swarm AI Integration
2
+
3
+ You have access to a shared user profile API at `http://localhost:3777`. Use it to personalize responses and record what you learn.
4
+
5
+ ## Session Start
6
+
7
+ Read the user profile and use it as context:
8
+
9
+ ```bash
10
+ curl -s http://localhost:3777/api/v1/profile \
11
+ -H "Authorization: Bearer $SWARM_API_KEY"
12
+ ```
13
+
14
+ ## During Interaction
15
+
16
+ When you learn something new about the user (preferences, tech stack, habits), record it:
17
+
18
+ ```bash
19
+ curl -s -X POST http://localhost:3777/api/v1/profile/observe \
20
+ -H "Content-Type: application/json" \
21
+ -H "Authorization: Bearer $SWARM_API_KEY" \
22
+ -d '{"observations":[{"layer":"LAYER","key":"KEY","value":"VALUE","confidence":0.8}]}'
23
+ ```
24
+
25
+ Layers: `identity`, `work`, `preferences`, `context` (24h TTL).
26
+
27
+ ## Memory
28
+
29
+ Search shared memory for past decisions and context:
30
+
31
+ ```bash
32
+ curl -s "http://localhost:3777/api/v1/memory?q=QUERY" \
33
+ -H "Authorization: Bearer $SWARM_API_KEY"
34
+ ```
35
+
36
+ Write significant events:
37
+
38
+ ```bash
39
+ curl -s -X POST http://localhost:3777/api/v1/memory \
40
+ -H "Content-Type: application/json" \
41
+ -H "Authorization: Bearer $SWARM_API_KEY" \
42
+ -d '{"content":"CONTENT","tags":["tag1"]}'
43
+ ```