@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.
- package/.dockerignore +6 -0
- package/Dockerfile +9 -0
- package/README.md +29 -0
- package/app/api/health/route.ts +2 -0
- package/app/api/v1/admin/agents/[id]/route.ts +12 -0
- package/app/api/v1/admin/agents/route.ts +31 -0
- package/app/api/v1/admin/audit/route.ts +23 -0
- package/app/api/v1/admin/cleanup/route.ts +21 -0
- package/app/api/v1/admin/export/route.ts +15 -0
- package/app/api/v1/admin/history/route.ts +23 -0
- package/app/api/v1/admin/profile/route.ts +23 -0
- package/app/api/v1/admin/settings/route.ts +44 -0
- package/app/api/v1/auth/route.ts +5 -0
- package/app/api/v1/memory/route.ts +105 -0
- package/app/api/v1/persona/[agentId]/route.ts +13 -0
- package/app/api/v1/persona/me/route.ts +12 -0
- package/app/api/v1/profile/observe/route.ts +34 -0
- package/app/api/v1/profile/route.ts +72 -0
- package/app/api/v1/reflect/route.ts +96 -0
- package/app/globals.css +190 -0
- package/app/i18n.ts +161 -0
- package/app/layout.tsx +12 -0
- package/app/page.tsx +561 -0
- package/docker-compose.yml +34 -0
- package/docs/DEBATE-ROUND1.md +244 -0
- package/docs/DEBATE-ROUND2.md +158 -0
- package/docs/REQUIREMENTS.md +162 -0
- package/docs/docs.html +272 -0
- package/docs/index.html +228 -0
- package/docs/script.js +103 -0
- package/docs/style.css +418 -0
- package/lib/auth.ts +63 -0
- package/lib/db.ts +63 -0
- package/lib/embedding.ts +29 -0
- package/lib/schema.ts +134 -0
- package/mcp-server.ts +56 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +5 -0
- package/package.json +34 -0
- package/packages/cli/README.md +33 -0
- package/packages/cli/bin/swarm.mjs +274 -0
- package/packages/cli/package.json +10 -0
- package/postcss.config.mjs +2 -0
- package/skill/CLAUDE.md +40 -0
- package/skill/CODEX.md +43 -0
- package/skill/GEMINI.md +38 -0
- package/skill/IFLOW.md +38 -0
- package/skill/OPENCODE.md +38 -0
- package/skill/swarm-ai-skill/SKILL.md +74 -0
- package/skill/swarm-ai-skill/env.sh +4 -0
- package/skill/swarm-ai-skill/scripts/bootstrap.sh +21 -0
- package/skill/swarm-ai-skill/scripts/env.sh +4 -0
- package/skill/swarm-ai-skill/scripts/env.sh.example +3 -0
- package/skill/swarm-ai-skill/scripts/memory-read.sh +8 -0
- package/skill/swarm-ai-skill/scripts/memory-write.sh +10 -0
- package/skill/swarm-ai-skill/scripts/observe.sh +9 -0
- package/skill/swarm-ai-skill/scripts/profile-read.sh +9 -0
- package/skill/swarm-ai-skill/scripts/profile-update.sh +9 -0
- package/skill/swarm-ai-skill/scripts/session-start.sh +19 -0
- package/tsconfig.json +21 -0
- package/tsconfig.tsbuildinfo +1 -0
package/.dockerignore
ADDED
package/Dockerfile
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Swarm AI
|
|
2
|
+
|
|
3
|
+
Cross-agent user profile sync. Let every AI agent know your user without re-teaching.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx swarm-ai
|
|
9
|
+
# or
|
|
10
|
+
npm install -g swarm-ai
|
|
11
|
+
swarm-ai
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Server runs on `http://localhost:3777`.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- User profile management (layered: identity, preferences, context)
|
|
19
|
+
- Shared memory across agents
|
|
20
|
+
- Agent persona management
|
|
21
|
+
- MCP Server for agent integration
|
|
22
|
+
- Skills for OpenClaw, Claude Code, Codex, Cursor, Gemini, OpenCode
|
|
23
|
+
- SQLite (dev) / PostgreSQL (prod)
|
|
24
|
+
- Web dashboard
|
|
25
|
+
- Audit logging
|
|
26
|
+
|
|
27
|
+
## Docs
|
|
28
|
+
|
|
29
|
+
See [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) for full architecture.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const DELETE = withAdmin(async (req, userId) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const id = req.nextUrl.pathname.split('/').pop() ?? '';
|
|
10
|
+
await db.prepare('DELETE FROM agents WHERE id = ? AND user_id = ?').run(id, userId);
|
|
11
|
+
return NextResponse.json({ ok: true });
|
|
12
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAdmin(async (_req, userId) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const rows = await db.prepare('SELECT id, name, permissions, persona, created_at FROM agents WHERE user_id = ?').all(userId) as any[];
|
|
10
|
+
return NextResponse.json(rows.map(a => ({ ...a, persona: a.persona ? JSON.parse(a.persona) : null })));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const POST = withAdmin(async (req, userId) => {
|
|
14
|
+
await initSchema();
|
|
15
|
+
const { id, name, permissions = 'read,write' } = await req.json();
|
|
16
|
+
const agentId = id || crypto.randomUUID().slice(0, 12);
|
|
17
|
+
const apiKey = `swarm_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
18
|
+
await db.prepare('INSERT INTO agents (id, user_id, name, api_key, permissions) VALUES (?,?,?,?,?)').run(agentId, userId, name || agentId, apiKey, permissions);
|
|
19
|
+
return NextResponse.json({ id: agentId, apiKey, permissions });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const PATCH = withAdmin(async (req, userId) => {
|
|
23
|
+
await initSchema();
|
|
24
|
+
const { id, persona, name } = await req.json();
|
|
25
|
+
if (!id) return NextResponse.json({ error: 'Missing agent id' }, { status: 400 });
|
|
26
|
+
if (persona !== undefined)
|
|
27
|
+
await db.prepare('UPDATE agents SET persona = ? WHERE id = ? AND user_id = ?').run(JSON.stringify(persona), id, userId);
|
|
28
|
+
if (name !== undefined)
|
|
29
|
+
await db.prepare('UPDATE agents SET name = ? WHERE id = ? AND user_id = ?').run(name, id, userId);
|
|
30
|
+
return NextResponse.json({ ok: true });
|
|
31
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAdmin(async (req, userId) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const { searchParams } = req.nextUrl;
|
|
10
|
+
const limit = Number(searchParams.get('limit') || 50);
|
|
11
|
+
const action = searchParams.get('action');
|
|
12
|
+
const agent = searchParams.get('agent');
|
|
13
|
+
|
|
14
|
+
let sql = 'SELECT * FROM audit_log WHERE user_id = ?';
|
|
15
|
+
const params: any[] = [userId];
|
|
16
|
+
if (action) { sql += ' AND action = ?'; params.push(action); }
|
|
17
|
+
if (agent) { sql += ' AND agent_id = ?'; params.push(agent); }
|
|
18
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
19
|
+
params.push(limit);
|
|
20
|
+
|
|
21
|
+
const rows = await db.prepare(sql).all(...params);
|
|
22
|
+
return NextResponse.json(rows);
|
|
23
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema, logAudit, ensureDefaultUser } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
const NOW = isPg ? 'NOW()' : "datetime('now')";
|
|
8
|
+
|
|
9
|
+
export const POST = withAdmin(async (_req, userId) => {
|
|
10
|
+
await initSchema();
|
|
11
|
+
const expired = await db.prepare(
|
|
12
|
+
`SELECT COUNT(*) as count FROM profiles WHERE expires_at IS NOT NULL AND expires_at < ${NOW}`
|
|
13
|
+
).get() as any;
|
|
14
|
+
|
|
15
|
+
await db.prepare(
|
|
16
|
+
`DELETE FROM profiles WHERE expires_at IS NOT NULL AND expires_at < ${NOW}`
|
|
17
|
+
).run();
|
|
18
|
+
|
|
19
|
+
await logAudit(userId, null, 'cleanup', 'profiles', undefined, `${expired?.count || 0} expired entries removed`);
|
|
20
|
+
return NextResponse.json({ ok: true, removed: expired?.count || 0 });
|
|
21
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAdmin(async (_req, userId) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const profiles = await db.prepare('SELECT layer, key, value, confidence, source, tags, updated_at FROM profiles WHERE user_id = ?').all(userId);
|
|
10
|
+
const agents = await db.prepare('SELECT id, name, permissions, persona, created_at FROM agents WHERE user_id = ?').all(userId);
|
|
11
|
+
const memories = await db.prepare('SELECT key, content, source, tags, type, importance, entities, created_at FROM memories WHERE user_id = ?').all(userId);
|
|
12
|
+
return NextResponse.json({ exported_at: new Date().toISOString(), profiles, agents, memories }, {
|
|
13
|
+
headers: { 'Content-Disposition': 'attachment; filename="swarm-export.json"' },
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAdmin(async (req, userId) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const { searchParams } = req.nextUrl;
|
|
10
|
+
const limit = Number(searchParams.get('limit') || 50);
|
|
11
|
+
const layer = searchParams.get('layer');
|
|
12
|
+
const key = searchParams.get('key');
|
|
13
|
+
|
|
14
|
+
let sql = 'SELECT * FROM profile_history WHERE user_id = ?';
|
|
15
|
+
const params: any[] = [userId];
|
|
16
|
+
if (layer) { sql += ' AND layer = ?'; params.push(layer); }
|
|
17
|
+
if (key) { sql += ' AND key = ?'; params.push(key); }
|
|
18
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
19
|
+
params.push(limit);
|
|
20
|
+
|
|
21
|
+
const rows = await db.prepare(sql).all(...params);
|
|
22
|
+
return NextResponse.json(rows);
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAdmin } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
const NOW = isPg ? 'NOW()' : "datetime('now')";
|
|
8
|
+
|
|
9
|
+
export const GET = withAdmin(async (_req, userId) => {
|
|
10
|
+
await initSchema();
|
|
11
|
+
const rows = await db.prepare('SELECT * FROM profiles WHERE user_id = ? ORDER BY layer, key').all(userId) as any[];
|
|
12
|
+
return NextResponse.json(rows.map((r: any) => ({ ...r, value: JSON.parse(r.value) })));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const PUT = withAdmin(async (req, userId) => {
|
|
16
|
+
await initSchema();
|
|
17
|
+
const { entries } = await req.json();
|
|
18
|
+
const sql = `INSERT INTO profiles (user_id, layer, key, value, source, updated_at)
|
|
19
|
+
VALUES (?, ?, ?, ?, 'admin', ${NOW})
|
|
20
|
+
ON CONFLICT(user_id, layer, key) DO UPDATE SET value=${isPg ? 'EXCLUDED' : 'excluded'}.value, source='admin', updated_at=${NOW}`;
|
|
21
|
+
for (const e of entries) await db.prepare(sql).run(userId, e.layer, e.key, JSON.stringify(e.value));
|
|
22
|
+
return NextResponse.json({ ok: true });
|
|
23
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { withAdmin } from '@/lib/auth';
|
|
4
|
+
import { getEmbeddingConfig } from '@/lib/embedding';
|
|
5
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const ENV_PATH = join(process.cwd(), '.env.local');
|
|
9
|
+
|
|
10
|
+
function readEnv(): Record<string, string> {
|
|
11
|
+
if (!existsSync(ENV_PATH)) return {};
|
|
12
|
+
const lines = readFileSync(ENV_PATH, 'utf8').split('\n');
|
|
13
|
+
const env: Record<string, string> = {};
|
|
14
|
+
for (const l of lines) {
|
|
15
|
+
const m = l.match(/^([^=]+)=(.*)$/);
|
|
16
|
+
if (m) env[m[1]] = m[2];
|
|
17
|
+
}
|
|
18
|
+
return env;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeEnv(env: Record<string, string>) {
|
|
22
|
+
writeFileSync(ENV_PATH, Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GET = withAdmin(async () => {
|
|
26
|
+
return NextResponse.json({
|
|
27
|
+
embedding: getEmbeddingConfig(),
|
|
28
|
+
adminToken: process.env.SWARM_ADMIN_TOKEN || 'swarm-admin-dev',
|
|
29
|
+
port: process.env.PORT || '3777',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const PATCH = withAdmin(async (req) => {
|
|
34
|
+
const { embedding } = await req.json();
|
|
35
|
+
if (!embedding) return NextResponse.json({ error: 'Missing embedding' }, { status: 400 });
|
|
36
|
+
|
|
37
|
+
const env = readEnv();
|
|
38
|
+
if (embedding.url !== undefined) env.EMBED_URL = embedding.url;
|
|
39
|
+
if (embedding.key !== undefined) env.EMBED_KEY = embedding.key;
|
|
40
|
+
if (embedding.model !== undefined) env.EMBED_MODEL = embedding.model;
|
|
41
|
+
writeEnv(env);
|
|
42
|
+
|
|
43
|
+
return NextResponse.json({ ok: true, note: 'Restart server to apply changes' });
|
|
44
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema, logAudit } from '@/lib/schema';
|
|
5
|
+
import { withAuthOrAdmin } from '@/lib/auth';
|
|
6
|
+
import { embed, cosine } from '@/lib/embedding';
|
|
7
|
+
|
|
8
|
+
export const GET = withAuthOrAdmin(async (req, agent) => {
|
|
9
|
+
await initSchema();
|
|
10
|
+
const { searchParams } = req.nextUrl;
|
|
11
|
+
const q = searchParams.get('q');
|
|
12
|
+
const tag = searchParams.get('tag');
|
|
13
|
+
const type = searchParams.get('type');
|
|
14
|
+
const entity = searchParams.get('entity');
|
|
15
|
+
const since = searchParams.get('since');
|
|
16
|
+
const limit = Number(searchParams.get('limit') || 50);
|
|
17
|
+
|
|
18
|
+
const mode = searchParams.get('mode'); // 'semantic' | null (default: fts)
|
|
19
|
+
|
|
20
|
+
// Semantic search mode
|
|
21
|
+
if (q && mode === 'semantic') {
|
|
22
|
+
const qVec = await embed(q);
|
|
23
|
+
const all = await db.prepare('SELECT * FROM memories WHERE user_id = ? AND embedding IS NOT NULL ORDER BY created_at DESC')
|
|
24
|
+
.all(agent.userId) as any[];
|
|
25
|
+
const scored = all.map(m => ({ ...m, score: cosine(qVec, JSON.parse(m.embedding)) }))
|
|
26
|
+
.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
27
|
+
return NextResponse.json(scored.map(r => ({
|
|
28
|
+
...r, embedding: undefined,
|
|
29
|
+
tags: r.tags?.split(',').filter(Boolean) || [],
|
|
30
|
+
entities: r.entities?.split(',').filter(Boolean) || [],
|
|
31
|
+
})));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let sql: string, params: any[];
|
|
35
|
+
|
|
36
|
+
if (q) {
|
|
37
|
+
const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]/.test(q);
|
|
38
|
+
if (isPg) {
|
|
39
|
+
sql = hasCJK
|
|
40
|
+
? `SELECT * FROM memories WHERE user_id = ? AND content LIKE ? ORDER BY created_at DESC LIMIT ?`
|
|
41
|
+
: `SELECT *, ts_rank(to_tsvector('english', content), plainto_tsquery('english', ?)) AS rank
|
|
42
|
+
FROM memories WHERE user_id = ? AND to_tsvector('english', content) @@ plainto_tsquery('english', ?)
|
|
43
|
+
ORDER BY rank DESC LIMIT ?`;
|
|
44
|
+
params = hasCJK ? [agent.userId, `%${q}%`, limit] : [q, agent.userId, q, limit];
|
|
45
|
+
} else {
|
|
46
|
+
if (hasCJK) {
|
|
47
|
+
sql = `SELECT * FROM memories WHERE user_id = ? AND content LIKE ? ORDER BY created_at DESC LIMIT ?`;
|
|
48
|
+
params = [agent.userId, `%${q}%`, limit];
|
|
49
|
+
} else {
|
|
50
|
+
sql = `SELECT m.*, rank FROM memories m JOIN memories_fts ON memories_fts.rowid = m.id
|
|
51
|
+
WHERE m.user_id = ? AND memories_fts MATCH ? ORDER BY rank LIMIT ?`;
|
|
52
|
+
params = [agent.userId, q, limit];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
sql = 'SELECT * FROM memories WHERE user_id = ?';
|
|
57
|
+
params = [agent.userId];
|
|
58
|
+
if (tag) { sql += ' AND tags LIKE ?'; params.push(`%${tag}%`); }
|
|
59
|
+
if (type) { sql += ' AND type = ?'; params.push(type); }
|
|
60
|
+
if (entity) { sql += ' AND entities LIKE ?'; params.push(`%${entity}%`); }
|
|
61
|
+
if (since) { sql += ' AND created_at >= ?'; params.push(since); }
|
|
62
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
63
|
+
params.push(limit);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rows = await db.prepare(sql).all(...params) as any[];
|
|
67
|
+
return NextResponse.json(rows.map(r => ({
|
|
68
|
+
...r,
|
|
69
|
+
tags: r.tags?.split(',').filter(Boolean) || [],
|
|
70
|
+
entities: r.entities?.split(',').filter(Boolean) || [],
|
|
71
|
+
})));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const POST = withAuthOrAdmin(async (req, agent) => {
|
|
75
|
+
await initSchema();
|
|
76
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
77
|
+
const { key, content, tags, type, importance, entities } = await req.json();
|
|
78
|
+
if (!content) return NextResponse.json({ error: 'Missing content' }, { status: 400 });
|
|
79
|
+
|
|
80
|
+
const tagsStr = Array.isArray(tags) ? tags.join(',') : tags || null;
|
|
81
|
+
const entStr = Array.isArray(entities) ? entities.join(',') : entities || null;
|
|
82
|
+
|
|
83
|
+
await db.prepare(`INSERT INTO memories (user_id, key, content, source, tags, type, importance, entities, embedding)
|
|
84
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
85
|
+
.run(agent.userId, key || null, content, agent.id, tagsStr, type || 'observation', importance ?? 0.5, entStr, null);
|
|
86
|
+
|
|
87
|
+
// Background embed — don't block response
|
|
88
|
+
embed(content).then(async vec => {
|
|
89
|
+
const rows = await db.prepare('SELECT id FROM memories WHERE user_id = ? AND content = ? ORDER BY id DESC LIMIT 1').all(agent.userId, content) as any[];
|
|
90
|
+
if (rows[0]) await db.prepare('UPDATE memories SET embedding = ? WHERE id = ?').run(JSON.stringify(vec), rows[0].id);
|
|
91
|
+
}).catch(() => {});
|
|
92
|
+
|
|
93
|
+
await logAudit(agent.userId, agent.id, 'memory.write', 'memory', key || undefined, content.slice(0, 100));
|
|
94
|
+
return NextResponse.json({ ok: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const DELETE = withAuthOrAdmin(async (req, agent) => {
|
|
98
|
+
await initSchema();
|
|
99
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
100
|
+
const { id } = await req.json();
|
|
101
|
+
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
|
102
|
+
await db.prepare('DELETE FROM memories WHERE id = ? AND user_id = ?').run(id, agent.userId);
|
|
103
|
+
await logAudit(agent.userId, agent.id, 'memory.delete', 'memory', String(id));
|
|
104
|
+
return NextResponse.json({ ok: true });
|
|
105
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAuth } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAuth(async (req: NextRequest & { agentId?: string }, agent) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const agentId = req.nextUrl.pathname.split('/').pop() ?? '';
|
|
10
|
+
const row = await db.prepare('SELECT id, name, persona FROM agents WHERE id = ? AND user_id = ?').get(agentId, agent.userId) as any;
|
|
11
|
+
if (!row) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
12
|
+
return NextResponse.json({ ...row, persona: row.persona ? JSON.parse(row.persona) : null });
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db from '@/lib/db';
|
|
4
|
+
import { initSchema } from '@/lib/schema';
|
|
5
|
+
import { withAuth } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
export const GET = withAuth(async (_req, agent) => {
|
|
8
|
+
await initSchema();
|
|
9
|
+
const row = await db.prepare('SELECT id, name, persona, permissions FROM agents WHERE id = ?').get(agent.id) as any;
|
|
10
|
+
if (!row) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
11
|
+
return NextResponse.json({ ...row, persona: row.persona ? JSON.parse(row.persona) : null });
|
|
12
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema, logAudit } from '@/lib/schema';
|
|
5
|
+
import { withAuth } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
const NOW_SQL = isPg ? 'NOW()' : "datetime('now')";
|
|
8
|
+
|
|
9
|
+
export const POST = withAuth(async (req, agent) => {
|
|
10
|
+
await initSchema();
|
|
11
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
12
|
+
const { observations } = await req.json();
|
|
13
|
+
if (!Array.isArray(observations)) return NextResponse.json({ error: 'Missing observations array' }, { status: 400 });
|
|
14
|
+
|
|
15
|
+
const upsertSql = `INSERT INTO profiles (user_id, layer, key, value, confidence, source, tags, expires_at, updated_at)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${NOW_SQL})
|
|
17
|
+
ON CONFLICT(user_id, layer, key) DO UPDATE SET
|
|
18
|
+
value = CASE WHEN ${isPg ? 'EXCLUDED' : 'excluded'}.confidence > profiles.confidence THEN ${isPg ? 'EXCLUDED' : 'excluded'}.value ELSE profiles.value END,
|
|
19
|
+
confidence = ${isPg ? `GREATEST(profiles.confidence, EXCLUDED.confidence)` : `MAX(profiles.confidence, excluded.confidence)`},
|
|
20
|
+
source = CASE WHEN ${isPg ? 'EXCLUDED' : 'excluded'}.confidence > profiles.confidence THEN ${isPg ? 'EXCLUDED' : 'excluded'}.source ELSE profiles.source END,
|
|
21
|
+
tags = COALESCE(${isPg ? 'EXCLUDED' : 'excluded'}.tags, profiles.tags),
|
|
22
|
+
expires_at = COALESCE(${isPg ? 'EXCLUDED' : 'excluded'}.expires_at, profiles.expires_at),
|
|
23
|
+
updated_at = ${NOW_SQL}`;
|
|
24
|
+
|
|
25
|
+
for (const obs of observations) {
|
|
26
|
+
const tags = Array.isArray(obs.tags) ? obs.tags.join(',') : obs.tags || null;
|
|
27
|
+
const defaultExpiry = (obs.layer || 'context') === 'context' && !obs.expiresAt
|
|
28
|
+
? new Date(Date.now() + 86400000).toISOString() : null;
|
|
29
|
+
await db.prepare(upsertSql).run(agent.userId, obs.layer || 'context', obs.key, JSON.stringify(obs.value),
|
|
30
|
+
obs.confidence ?? 0.5, agent.id, tags, obs.expiresAt || defaultExpiry);
|
|
31
|
+
}
|
|
32
|
+
await logAudit(agent.userId, agent.id, 'profile.observe', 'profile', undefined, `${observations.length} observations`);
|
|
33
|
+
return NextResponse.json({ ok: true, count: observations.length });
|
|
34
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema, logAudit, logProfileHistory } from '@/lib/schema';
|
|
5
|
+
import { withAuth } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
const NOW = isPg ? 'NOW()' : "datetime('now')";
|
|
8
|
+
|
|
9
|
+
export const GET = withAuth(async (req, agent) => {
|
|
10
|
+
await initSchema();
|
|
11
|
+
const layer = req.nextUrl.searchParams.get('layer');
|
|
12
|
+
const tag = req.nextUrl.searchParams.get('tag');
|
|
13
|
+
|
|
14
|
+
let sql = 'SELECT layer, key, value, confidence, source, tags, expires_at, updated_at FROM profiles WHERE user_id = ?';
|
|
15
|
+
const params: any[] = [agent.userId];
|
|
16
|
+
sql += ` AND (expires_at IS NULL OR expires_at > ${NOW})`;
|
|
17
|
+
if (layer) { sql += ' AND layer = ?'; params.push(layer); }
|
|
18
|
+
if (tag) { sql += ' AND tags LIKE ?'; params.push(`%${tag}%`); }
|
|
19
|
+
sql += ' ORDER BY layer, key';
|
|
20
|
+
|
|
21
|
+
const rows = await db.prepare(sql).all(...params) as any[];
|
|
22
|
+
const profile: Record<string, Record<string, any>> = {};
|
|
23
|
+
for (const r of rows) {
|
|
24
|
+
if (!profile[r.layer]) profile[r.layer] = {};
|
|
25
|
+
profile[r.layer][r.key] = {
|
|
26
|
+
value: JSON.parse(r.value), confidence: r.confidence,
|
|
27
|
+
source: r.source, tags: r.tags?.split(',').filter(Boolean) || [],
|
|
28
|
+
expiresAt: r.expires_at, updatedAt: r.updated_at,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.json(profile);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const PATCH = withAuth(async (req, agent) => {
|
|
35
|
+
await initSchema();
|
|
36
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
37
|
+
const { layer, entries } = await req.json();
|
|
38
|
+
if (!layer || !entries) return NextResponse.json({ error: 'Missing layer or entries' }, { status: 400 });
|
|
39
|
+
|
|
40
|
+
const upsertSql = isPg
|
|
41
|
+
? `INSERT INTO profiles (user_id, layer, key, value, confidence, source, tags, expires_at, updated_at)
|
|
42
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
43
|
+
ON CONFLICT(user_id, layer, key) DO UPDATE SET value=EXCLUDED.value, confidence=EXCLUDED.confidence,
|
|
44
|
+
source=EXCLUDED.source, tags=EXCLUDED.tags, expires_at=EXCLUDED.expires_at, updated_at=NOW()`
|
|
45
|
+
: `INSERT INTO profiles (user_id, layer, key, value, confidence, source, tags, expires_at, updated_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
47
|
+
ON CONFLICT(user_id, layer, key) DO UPDATE SET value=excluded.value, confidence=excluded.confidence,
|
|
48
|
+
source=excluded.source, tags=excluded.tags, expires_at=excluded.expires_at, updated_at=excluded.updated_at`;
|
|
49
|
+
|
|
50
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
51
|
+
const v = typeof val === 'object' && val !== null && 'value' in (val as any) ? (val as any) : { value: val };
|
|
52
|
+
const tags = Array.isArray(v.tags) ? v.tags.join(',') : v.tags || null;
|
|
53
|
+
// Get old value for history
|
|
54
|
+
const old = await db.prepare('SELECT value FROM profiles WHERE user_id = ? AND layer = ? AND key = ?').get(agent.userId, layer, key) as any;
|
|
55
|
+
await db.prepare(upsertSql).run(agent.userId, layer, key, JSON.stringify(v.value), v.confidence ?? 1.0, agent.id, tags, v.expiresAt || null);
|
|
56
|
+
await logProfileHistory(agent.userId, layer, key, old?.value || null, JSON.stringify(v.value), agent.id);
|
|
57
|
+
}
|
|
58
|
+
await logAudit(agent.userId, agent.id, 'profile.update', 'profile', layer, `${Object.keys(entries).length} entries`);
|
|
59
|
+
return NextResponse.json({ ok: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const DELETE = withAuth(async (req, agent) => {
|
|
63
|
+
await initSchema();
|
|
64
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
65
|
+
const { layer, key } = await req.json();
|
|
66
|
+
if (!layer || !key) return NextResponse.json({ error: 'Missing layer or key' }, { status: 400 });
|
|
67
|
+
const old = await db.prepare('SELECT value FROM profiles WHERE user_id = ? AND layer = ? AND key = ?').get(agent.userId, layer, key) as any;
|
|
68
|
+
await db.prepare('DELETE FROM profiles WHERE user_id = ? AND layer = ? AND key = ?').run(agent.userId, layer, key);
|
|
69
|
+
if (old) await logProfileHistory(agent.userId, layer, key, old.value, '(deleted)', agent.id);
|
|
70
|
+
await logAudit(agent.userId, agent.id, 'profile.delete', 'profile', `${layer}.${key}`);
|
|
71
|
+
return NextResponse.json({ ok: true });
|
|
72
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import db, { isPg } from '@/lib/db';
|
|
4
|
+
import { initSchema, logAudit } from '@/lib/schema';
|
|
5
|
+
import { withAuth } from '@/lib/auth';
|
|
6
|
+
|
|
7
|
+
const LLM_URL = process.env.LLM_URL || process.env.EMBED_URL?.replace('/embeddings', '/chat/completions') || '';
|
|
8
|
+
const LLM_KEY = process.env.LLM_KEY || process.env.EMBED_KEY || '';
|
|
9
|
+
const LLM_MODEL = process.env.LLM_MODEL || 'gpt-4o-mini';
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = `You are a profile extraction engine. Given a list of user memories/observations, extract structured profile updates.
|
|
12
|
+
|
|
13
|
+
Output ONLY a JSON array of objects with: {"layer", "key", "value", "confidence"}
|
|
14
|
+
- layer: "identity" | "work" | "preferences" | "communication" | custom
|
|
15
|
+
- key: snake_case identifier (e.g. "preferred_language", "tech_stack")
|
|
16
|
+
- value: extracted value (string, array, or object)
|
|
17
|
+
- confidence: 0.0-1.0 based on how certain the information is
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Merge related memories into single profile entries
|
|
21
|
+
- Use high confidence (0.8-1.0) for explicit statements, low (0.3-0.5) for inferences
|
|
22
|
+
- Skip trivial or transient information
|
|
23
|
+
- Output valid JSON array only, no markdown`;
|
|
24
|
+
|
|
25
|
+
async function llmExtract(memories: any[]): Promise<{ layer: string; key: string; value: any; confidence: number }[]> {
|
|
26
|
+
if (!LLM_URL || !LLM_KEY) return [];
|
|
27
|
+
const content = memories.map((m, i) => `[${i + 1}] (${m.type || 'observation'}) ${m.content}`).join('\n');
|
|
28
|
+
const res = await fetch(LLM_URL, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${LLM_KEY}` },
|
|
31
|
+
body: JSON.stringify({ model: LLM_MODEL, messages: [
|
|
32
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
33
|
+
{ role: 'user', content },
|
|
34
|
+
], temperature: 0.1 }),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) throw new Error(`LLM API ${res.status}`);
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
const text = data.choices?.[0]?.message?.content || '[]';
|
|
39
|
+
// Extract JSON from possible markdown fence
|
|
40
|
+
const match = text.match(/\[[\s\S]*\]/);
|
|
41
|
+
return match ? JSON.parse(match[0]) : [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const POST = withAuth(async (req, agent) => {
|
|
45
|
+
await initSchema();
|
|
46
|
+
if (!agent.permissions.includes('write')) return NextResponse.json({ error: 'No write permission' }, { status: 403 });
|
|
47
|
+
|
|
48
|
+
const { since, limit: maxMemories } = await req.json().catch(() => ({}));
|
|
49
|
+
const sinceDate = since || new Date(Date.now() - 7 * 86400000).toISOString();
|
|
50
|
+
const lim = maxMemories || 100;
|
|
51
|
+
|
|
52
|
+
const memories = await db.prepare(
|
|
53
|
+
'SELECT * FROM memories WHERE user_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?'
|
|
54
|
+
).all(agent.userId, sinceDate, lim) as any[];
|
|
55
|
+
|
|
56
|
+
if (memories.length === 0) return NextResponse.json({ reflected: 0, updates: [] });
|
|
57
|
+
|
|
58
|
+
let updates: { layer: string; key: string; value: any; confidence: number }[];
|
|
59
|
+
let method: string;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
updates = await llmExtract(memories);
|
|
63
|
+
method = 'llm';
|
|
64
|
+
} catch {
|
|
65
|
+
// Fallback: rule-based
|
|
66
|
+
updates = [];
|
|
67
|
+
for (const m of memories) {
|
|
68
|
+
const tags = m.tags?.split(',') || [];
|
|
69
|
+
if (m.type === 'preference' || tags.includes('preference'))
|
|
70
|
+
updates.push({ layer: 'preferences', key: m.key || `pref_${m.id}`, value: m.content, confidence: 0.6 });
|
|
71
|
+
if (m.type === 'fact' || tags.includes('fact'))
|
|
72
|
+
updates.push({ layer: 'identity', key: m.key || `fact_${m.id}`, value: m.content, confidence: 0.7 });
|
|
73
|
+
}
|
|
74
|
+
method = 'rules';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Deduplicate
|
|
78
|
+
const deduped = new Map<string, typeof updates[0]>();
|
|
79
|
+
for (const u of updates) deduped.set(`${u.layer}:${u.key}`, u);
|
|
80
|
+
const final = [...deduped.values()];
|
|
81
|
+
|
|
82
|
+
const NOW = isPg ? 'NOW()' : "datetime('now')";
|
|
83
|
+
const EXCL = isPg ? 'EXCLUDED' : 'excluded';
|
|
84
|
+
for (const u of final) {
|
|
85
|
+
await db.prepare(`INSERT INTO profiles (user_id, layer, key, value, confidence, source, updated_at)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, 'reflect', ${NOW})
|
|
87
|
+
ON CONFLICT(user_id, layer, key) DO UPDATE SET
|
|
88
|
+
value = CASE WHEN ${EXCL}.confidence > profiles.confidence THEN ${EXCL}.value ELSE profiles.value END,
|
|
89
|
+
confidence = CASE WHEN ${EXCL}.confidence > profiles.confidence THEN ${EXCL}.confidence ELSE profiles.confidence END,
|
|
90
|
+
source = 'reflect', updated_at = ${NOW}`)
|
|
91
|
+
.run(agent.userId, u.layer, u.key, JSON.stringify(u.value), u.confidence ?? 0.6);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await logAudit(agent.userId, agent.id, 'reflect', 'profile', undefined, `${final.length} updates from ${memories.length} memories (${method})`);
|
|
95
|
+
return NextResponse.json({ reflected: memories.length, updates: final.map(u => ({ layer: u.layer, key: u.key })), method });
|
|
96
|
+
});
|