claudity 1.0.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/setup.sh ADDED
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
6
+ green="\033[0;32m"
7
+ yellow="\033[0;33m"
8
+ red="\033[0;31m"
9
+ dim="\033[2m"
10
+ bold="\033[1m"
11
+ reset="\033[0m"
12
+
13
+ step() { echo -e "\n${green}→${reset} $1"; }
14
+ info() { echo -e " ${dim}$1${reset}"; }
15
+ warn() { echo -e " ${yellow}!${reset} $1"; }
16
+ fail() { echo -e " ${red}✗${reset} $1"; exit 1; }
17
+ ok() { echo -e " ${dim}done${reset}"; }
18
+
19
+ ask() {
20
+ echo -en " $1 ${dim}[y/n]${reset} "
21
+ read -r answer
22
+ [[ "$answer" =~ ^[yY] ]]
23
+ }
24
+
25
+ echo -e "\n${bold}${green}claudity${reset} setup"
26
+ echo -e "${dim}personal ai agent platform${reset}"
27
+
28
+ # node check
29
+ step "checking node.js"
30
+ if ! command -v node &>/dev/null; then
31
+ fail "node.js is required. install from https://nodejs.org"
32
+ fi
33
+ NODE_VER=$(node -v | cut -d. -f1 | tr -d 'v')
34
+ if [ "$NODE_VER" -lt 18 ]; then
35
+ fail "node 18+ required (found $(node -v))"
36
+ fi
37
+ info "$(node -v)"
38
+
39
+ # dependencies
40
+ step "installing dependencies"
41
+ cd "$DIR"
42
+ npm install --silent 2>&1 | tail -1
43
+ ok
44
+
45
+ # env file
46
+ step "configuring environment"
47
+ if [ ! -f "$DIR/.env" ]; then
48
+ echo "PORT=6767" > "$DIR/.env"
49
+ echo "RELAY_SECRET=$(openssl rand -hex 24)" >> "$DIR/.env"
50
+ info "created .env"
51
+ else
52
+ if ! grep -q '^RELAY_SECRET=' "$DIR/.env" || grep -q 'change-me' "$DIR/.env"; then
53
+ SECRET=$(openssl rand -hex 24)
54
+ if grep -q '^RELAY_SECRET=' "$DIR/.env"; then
55
+ sed -i '' "s|^RELAY_SECRET=.*|RELAY_SECRET=$SECRET|" "$DIR/.env"
56
+ else
57
+ echo "RELAY_SECRET=$SECRET" >> "$DIR/.env"
58
+ fi
59
+ info "generated relay secret"
60
+ fi
61
+ info ".env already exists"
62
+ fi
63
+ ok
64
+
65
+ # auth
66
+ step "checking authentication"
67
+ KEYCHAIN_CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true)
68
+ if [ -n "$KEYCHAIN_CREDS" ]; then
69
+ info "found oauth credentials in keychain"
70
+ else
71
+ HAS_API_KEY=$(grep '^API_KEY=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)
72
+ if [ -n "$HAS_API_KEY" ] && [ "$HAS_API_KEY" != "" ]; then
73
+ info "using api key from .env"
74
+ else
75
+ warn "no authentication found"
76
+ echo ""
77
+ echo -e " option 1: run ${green}claude setup-token${reset} to connect your claude subscription"
78
+ echo -e " option 2: add ${green}API_KEY=sk-ant-api03-...${reset} to .env"
79
+ echo ""
80
+ echo -e " ${dim}you can finish this later — claudity will show a setup screen${reset}"
81
+ fi
82
+ fi
83
+
84
+ # imessage relay
85
+ step "imessage relay"
86
+ echo -e " chat with your agents over imessage by texting yourself"
87
+ echo -e " ${dim}send \"agent_name: your message\" in your self-chat${reset}"
88
+ echo ""
89
+
90
+ if ask "enable imessage relay?"; then
91
+ CHAT_DB="$HOME/Library/Messages/chat.db"
92
+
93
+ if ! sqlite3 "$CHAT_DB" "select 1 limit 1" &>/dev/null; then
94
+ echo ""
95
+ warn "terminal needs full disk access to read imessages"
96
+ warn "system settings → privacy & security → full disk access → enable your terminal app"
97
+ echo ""
98
+ info "after granting access, run ${green}npm run setup${reset} again"
99
+ else
100
+ info "full disk access: ok"
101
+ fi
102
+
103
+ echo -en " your phone number ${dim}(e.g. +15551234567)${reset}: "
104
+ read -r PHONE
105
+ if [ -z "$PHONE" ]; then
106
+ warn "no phone number provided, skipping imessage relay"
107
+ else
108
+ if grep -q '^IMESSAGE_PHONE=' "$DIR/.env"; then
109
+ sed -i '' "s|^IMESSAGE_PHONE=.*|IMESSAGE_PHONE=$PHONE|" "$DIR/.env"
110
+ else
111
+ echo "IMESSAGE_PHONE=$PHONE" >> "$DIR/.env"
112
+ fi
113
+
114
+ if grep -q '^IMESSAGE_RELAY=' "$DIR/.env"; then
115
+ sed -i '' "s|^IMESSAGE_RELAY=.*|IMESSAGE_RELAY=true|" "$DIR/.env"
116
+ else
117
+ echo "IMESSAGE_RELAY=true" >> "$DIR/.env"
118
+ fi
119
+ info "enabled in .env"
120
+ ok
121
+ fi
122
+ else
123
+ if grep -q '^IMESSAGE_RELAY=' "$DIR/.env"; then
124
+ sed -i '' "s|^IMESSAGE_RELAY=.*|IMESSAGE_RELAY=false|" "$DIR/.env"
125
+ fi
126
+ info "skipped"
127
+ fi
128
+
129
+ # done
130
+ echo -e "\n${bold}${green}ready${reset}\n"
131
+ echo -e " start claudity: ${green}npm start${reset}"
132
+ echo -e " dev mode: ${green}npm run dev${reset}"
133
+ echo -e " then visit: ${green}http://localhost:6767${reset}"
134
+ echo ""
package/src/db.js ADDED
@@ -0,0 +1,162 @@
1
+ const Database = require('better-sqlite3');
2
+ const path = require('path');
3
+
4
+ const db = new Database(path.join(__dirname, '..', 'claudity.db'));
5
+
6
+ db.pragma('journal_mode = WAL');
7
+ db.pragma('foreign_keys = ON');
8
+
9
+ db.exec(`
10
+ create table if not exists config (
11
+ key text primary key,
12
+ value text not null,
13
+ updated_at datetime default current_timestamp
14
+ );
15
+
16
+ create table if not exists agents (
17
+ id text primary key,
18
+ name text not null unique,
19
+ role text not null,
20
+ tools_config text not null default '{}',
21
+ created_at datetime default current_timestamp,
22
+ updated_at datetime default current_timestamp
23
+ );
24
+
25
+ create table if not exists memories (
26
+ id text primary key,
27
+ agent_id text not null,
28
+ summary text not null,
29
+ created_at datetime default current_timestamp,
30
+ foreign key (agent_id) references agents(id) on delete cascade
31
+ );
32
+
33
+ create table if not exists messages (
34
+ id text primary key,
35
+ agent_id text not null,
36
+ role text not null,
37
+ content text not null,
38
+ tool_calls text,
39
+ created_at datetime default current_timestamp,
40
+ foreign key (agent_id) references agents(id) on delete cascade
41
+ );
42
+
43
+ create table if not exists schedules (
44
+ id text primary key,
45
+ agent_id text not null,
46
+ description text not null,
47
+ interval_ms integer not null,
48
+ next_run_at integer not null,
49
+ last_run_at integer,
50
+ active integer not null default 1,
51
+ created_at datetime default current_timestamp,
52
+ foreign key (agent_id) references agents(id) on delete cascade
53
+ );
54
+
55
+ create table if not exists connections (
56
+ id text primary key,
57
+ platform text not null,
58
+ config text not null default '{}',
59
+ enabled integer not null default 0,
60
+ status text not null default 'disconnected',
61
+ status_detail text,
62
+ created_at datetime default current_timestamp,
63
+ updated_at datetime default current_timestamp
64
+ );
65
+
66
+ create unique index if not exists idx_connections_platform on connections(platform);
67
+ create index if not exists idx_memories_agent on memories(agent_id);
68
+ create index if not exists idx_messages_agent on messages(agent_id, created_at);
69
+ create index if not exists idx_schedules_due on schedules(active, next_run_at);
70
+ `);
71
+
72
+ try {
73
+ db.exec('alter table agents add column is_default integer not null default 0');
74
+ } catch {}
75
+
76
+ try {
77
+ db.exec('alter table agents add column heartbeat_interval integer default null');
78
+ } catch {}
79
+
80
+ try {
81
+ db.exec('alter table agents add column bootstrapped integer not null default 1');
82
+ } catch {}
83
+
84
+ try {
85
+ db.exec("alter table agents add column model text not null default 'opus'");
86
+ } catch {}
87
+
88
+ try {
89
+ db.exec("alter table agents add column thinking text not null default 'high'");
90
+ } catch {}
91
+
92
+ try {
93
+ db.exec("alter table messages add column type text not null default 'chat'");
94
+ } catch {}
95
+
96
+ try {
97
+ db.exec('alter table agents add column show_heartbeat integer not null default 0');
98
+ } catch {}
99
+
100
+ db.exec(`
101
+ create table if not exists sessions (
102
+ agent_id text primary key,
103
+ session_id text not null,
104
+ prompt_hash text not null,
105
+ updated_at datetime default current_timestamp,
106
+ foreign key (agent_id) references agents(id) on delete cascade
107
+ )
108
+ `);
109
+
110
+ const stmts = {
111
+ getConfig: db.prepare('select value from config where key = ?'),
112
+ setConfig: db.prepare('insert into config (key, value, updated_at) values (?, ?, current_timestamp) on conflict(key) do update set value = excluded.value, updated_at = current_timestamp'),
113
+ deleteConfig: db.prepare('delete from config where key = ?'),
114
+
115
+ listAgents: db.prepare("select a.*, max(m.created_at) as last_message_at from agents a left join messages m on m.agent_id = a.id and m.type = 'chat' group by a.id order by coalesce(last_message_at, a.created_at) desc"),
116
+ getAgent: db.prepare('select * from agents where id = ?'),
117
+ getAgentByName: db.prepare('select * from agents where lower(name) = lower(?)'),
118
+ createAgent: db.prepare("insert into agents (id, name, role) values (?, ?, '')"),
119
+ updateAgent: db.prepare('update agents set name = ?, updated_at = current_timestamp where id = ?'),
120
+ updateAgentToolsConfig: db.prepare('update agents set tools_config = ?, updated_at = current_timestamp where id = ?'),
121
+ deleteAgent: db.prepare('delete from agents where id = ?'),
122
+ getDefaultAgent: db.prepare('select * from agents where is_default = 1'),
123
+ clearDefaultAgent: db.prepare('update agents set is_default = 0 where is_default = 1'),
124
+ setDefaultAgent: db.prepare('update agents set is_default = 1, updated_at = current_timestamp where id = ?'),
125
+ unsetDefaultAgent: db.prepare('update agents set is_default = 0, updated_at = current_timestamp where id = ?'),
126
+
127
+ listMemories: db.prepare('select * from memories where agent_id = ? order by created_at desc'),
128
+ createMemory: db.prepare('insert into memories (id, agent_id, summary) values (?, ?, ?)'),
129
+ deleteMemories: db.prepare('delete from memories where agent_id = ?'),
130
+
131
+ listMessages: db.prepare('select * from messages where agent_id = ? order by created_at asc'),
132
+ recentMessages: db.prepare("select * from messages where agent_id = ? and type = 'chat' order by created_at desc limit ?"),
133
+ createMessage: db.prepare("insert into messages (id, agent_id, role, content, tool_calls, type) values (?, ?, ?, ?, ?, 'chat')"),
134
+ createHeartbeatMessage: db.prepare("insert into messages (id, agent_id, role, content, tool_calls, type) values (?, ?, ?, ?, ?, 'heartbeat')"),
135
+ deleteMessages: db.prepare('delete from messages where agent_id = ?'),
136
+ createSchedule: db.prepare('insert into schedules (id, agent_id, description, interval_ms, next_run_at) values (?, ?, ?, ?, ?)'),
137
+ dueSchedules: db.prepare('select * from schedules where active = 1 and next_run_at <= ?'),
138
+ updateScheduleRun: db.prepare('update schedules set last_run_at = ?, next_run_at = ? where id = ?'),
139
+ deactivateSchedule: db.prepare('update schedules set active = 0 where id = ? and agent_id = ?'),
140
+ agentSchedules: db.prepare('select * from schedules where agent_id = ? and active = 1'),
141
+ listConnections: db.prepare('select * from connections order by created_at asc'),
142
+ getConnectionByPlatform: db.prepare('select * from connections where platform = ?'),
143
+ upsertConnection: db.prepare('insert into connections (id, platform, config, enabled, status, status_detail, updated_at) values (?, ?, ?, ?, ?, ?, current_timestamp) on conflict(platform) do update set config = excluded.config, enabled = excluded.enabled, status = excluded.status, status_detail = excluded.status_detail, updated_at = current_timestamp'),
144
+ updateConnectionStatus: db.prepare('update connections set status = ?, status_detail = ?, updated_at = current_timestamp where platform = ?'),
145
+ updateConnectionEnabled: db.prepare('update connections set enabled = ?, updated_at = current_timestamp where platform = ?'),
146
+ updateConnectionConfig: db.prepare('update connections set config = ?, updated_at = current_timestamp where platform = ?'),
147
+ deleteConnection: db.prepare('delete from connections where platform = ?'),
148
+ enabledConnections: db.prepare('select * from connections where enabled = 1'),
149
+
150
+ agentsWithHeartbeat: db.prepare('select * from agents where heartbeat_interval is not null'),
151
+ setBootstrapped: db.prepare('update agents set bootstrapped = ?, updated_at = current_timestamp where id = ?'),
152
+ setHeartbeatInterval: db.prepare('update agents set heartbeat_interval = ?, updated_at = current_timestamp where id = ?'),
153
+ setModel: db.prepare('update agents set model = ?, updated_at = current_timestamp where id = ?'),
154
+ setThinking: db.prepare('update agents set thinking = ?, updated_at = current_timestamp where id = ?'),
155
+ setShowHeartbeat: db.prepare('update agents set show_heartbeat = ?, updated_at = current_timestamp where id = ?'),
156
+
157
+ getSession: db.prepare('select * from sessions where agent_id = ?'),
158
+ upsertSession: db.prepare('insert into sessions (agent_id, session_id, prompt_hash, updated_at) values (?, ?, ?, current_timestamp) on conflict(agent_id) do update set session_id = excluded.session_id, prompt_hash = excluded.prompt_hash, updated_at = current_timestamp'),
159
+ deleteSession: db.prepare('delete from sessions where agent_id = ?'),
160
+ };
161
+
162
+ module.exports = { db, stmts };
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ require('dotenv').config();
3
+ const express = require('express');
4
+ const path = require('path');
5
+ const { v4: uuid } = require('uuid');
6
+ const { execSync } = require('child_process');
7
+ const { stmts } = require('./db');
8
+ const apiRoutes = require('./routes/api');
9
+ const relayRoutes = require('./routes/relay');
10
+ const connectionRoutes = require('./routes/connections');
11
+ const scheduler = require('./services/scheduler');
12
+ const connections = require('./services/connections');
13
+ const memory = require('./services/memory');
14
+ const heartbeat = require('./services/heartbeat');
15
+
16
+ const app = express();
17
+ const PORT = process.env.PORT || 6767;
18
+
19
+ app.use(express.json());
20
+ app.use(express.static(path.join(__dirname, '..', 'public')));
21
+
22
+ app.use('/api', apiRoutes);
23
+ app.use('/relay', relayRoutes);
24
+ app.use('/api/connections', connectionRoutes);
25
+
26
+ function autoUpdate() {
27
+ const root = path.join(__dirname, '..');
28
+ try {
29
+ execSync('git rev-parse --git-dir', { cwd: root, stdio: 'ignore' });
30
+ } catch { return; }
31
+ try {
32
+ const result = execSync('git pull --ff-only 2>&1', { cwd: root, encoding: 'utf8', timeout: 15000 });
33
+ if (result && !result.includes('Already up to date')) {
34
+ console.log('[update] ' + result.trim());
35
+ }
36
+ } catch (err) {
37
+ console.log('[update] skipped: ' + (err.stderr || err.message || 'unknown error').toString().trim());
38
+ }
39
+ }
40
+
41
+ function migrateImessageFromEnv() {
42
+ if (process.env.IMESSAGE_RELAY !== 'true') return;
43
+ const existing = stmts.getConnectionByPlatform.get('imessage');
44
+ if (existing) return;
45
+ const phone = process.env.IMESSAGE_PHONE;
46
+ if (!phone) return;
47
+ const id = uuid();
48
+ stmts.upsertConnection.run(id, 'imessage', JSON.stringify({ phone }), 1, 'disconnected', null);
49
+ console.log('[connections] migrated imessage config from .env to database');
50
+ }
51
+
52
+ autoUpdate();
53
+
54
+ app.listen(PORT, () => {
55
+ console.log(`claudity running on http://localhost:${PORT}`);
56
+ scheduler.start();
57
+ memory.startConsolidation();
58
+ migrateImessageFromEnv();
59
+ connections.start();
60
+ heartbeat.start();
61
+ });
@@ -0,0 +1,194 @@
1
+ const express = require('express');
2
+ const { v4: uuid } = require('uuid');
3
+ const { stmts } = require('../db');
4
+ const auth = require('../services/auth');
5
+ const chat = require('../services/chat');
6
+ const workspace = require('../services/workspace');
7
+ const heartbeat = require('../services/heartbeat');
8
+
9
+ const router = express.Router();
10
+
11
+ router.get('/auth/status', (req, res) => {
12
+ res.json(auth.getAuthStatus());
13
+ });
14
+
15
+ router.post('/auth/api-key', (req, res) => {
16
+ const { key } = req.body;
17
+ if (!key) return res.status(400).json({ error: 'key required' });
18
+ if (!key.startsWith('sk-ant-api')) return res.status(400).json({ error: 'invalid api key — must start with sk-ant-api. setup tokens and oauth tokens are not supported here. run claude setup-token instead.' });
19
+ auth.setApiKey(key);
20
+ res.json({ saved: true });
21
+ });
22
+
23
+ router.post('/auth/setup-token', (req, res) => {
24
+ const { token } = req.body;
25
+ if (!token) return res.status(400).json({ error: 'token required' });
26
+ if (!token.startsWith('sk-ant-oat')) return res.status(400).json({ error: 'invalid setup token — must start with sk-ant-oat. if you have an api key, use the api key option instead.' });
27
+ try {
28
+ auth.writeSetupToken(token);
29
+ const status = auth.getAuthStatus();
30
+ if (status.authenticated) return res.json({ saved: true });
31
+ return res.status(400).json({ error: 'token saved to keychain but authentication failed — token may be invalid or expired' });
32
+ } catch (err) {
33
+ return res.status(500).json({ error: 'failed to write to keychain: ' + err.message });
34
+ }
35
+ });
36
+
37
+ router.delete('/auth/api-key', (req, res) => {
38
+ auth.removeApiKey();
39
+ res.json({ removed: true });
40
+ });
41
+
42
+ router.get('/agents', (req, res) => {
43
+ res.json(stmts.listAgents.all());
44
+ });
45
+
46
+ router.post('/agents', (req, res) => {
47
+ const { name, is_default, model, thinking } = req.body;
48
+ if (!name) return res.status(400).json({ error: 'name required' });
49
+ const id = uuid();
50
+ try {
51
+ stmts.createAgent.run(id, name);
52
+ stmts.setBootstrapped.run(0, id);
53
+ if (model) stmts.setModel.run(model, id);
54
+ if (thinking) stmts.setThinking.run(thinking, id);
55
+ if (is_default) {
56
+ stmts.clearDefaultAgent.run();
57
+ stmts.setDefaultAgent.run(id);
58
+ }
59
+ workspace.initWorkspace(name);
60
+ const agent = stmts.getAgent.get(id);
61
+ res.status(201).json(agent);
62
+ } catch (err) {
63
+ if (err.message.includes('UNIQUE')) return res.status(409).json({ error: 'agent name already exists' });
64
+ throw err;
65
+ }
66
+ });
67
+
68
+ router.get('/agents/:id', (req, res) => {
69
+ const agent = stmts.getAgent.get(req.params.id);
70
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
71
+ res.json(agent);
72
+ });
73
+
74
+ router.patch('/agents/:id', (req, res) => {
75
+ const agent = stmts.getAgent.get(req.params.id);
76
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
77
+ const name = req.body.name || agent.name;
78
+ if (name !== agent.name) {
79
+ workspace.renameWorkspace(agent.name, name);
80
+ }
81
+ stmts.updateAgent.run(name, req.params.id);
82
+ if (req.body.is_default) {
83
+ stmts.clearDefaultAgent.run();
84
+ stmts.setDefaultAgent.run(req.params.id);
85
+ } else if (req.body.is_default === false) {
86
+ stmts.unsetDefaultAgent.run(req.params.id);
87
+ }
88
+ if ('heartbeat_interval' in req.body) {
89
+ heartbeat.updateInterval(req.params.id, req.body.heartbeat_interval);
90
+ }
91
+ if (req.body.model) stmts.setModel.run(req.body.model, req.params.id);
92
+ if (req.body.thinking) stmts.setThinking.run(req.body.thinking, req.params.id);
93
+ if ('show_heartbeat' in req.body) stmts.setShowHeartbeat.run(req.body.show_heartbeat ? 1 : 0, req.params.id);
94
+ res.json(stmts.getAgent.get(req.params.id));
95
+ });
96
+
97
+ router.delete('/agents/:id', (req, res) => {
98
+ const agent = stmts.getAgent.get(req.params.id);
99
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
100
+ heartbeat.removeAgent(req.params.id);
101
+ workspace.deleteWorkspace(agent.name);
102
+ stmts.deleteAgent.run(req.params.id);
103
+ res.json({ deleted: true });
104
+ });
105
+
106
+ router.get('/agents/:id/memories', (req, res) => {
107
+ res.json(stmts.listMemories.all(req.params.id));
108
+ });
109
+
110
+ router.delete('/agents/:id/memories', (req, res) => {
111
+ stmts.deleteMemories.run(req.params.id);
112
+ res.json({ cleared: true });
113
+ });
114
+
115
+ router.get('/agents/:id/messages', (req, res) => {
116
+ res.json(stmts.listMessages.all(req.params.id));
117
+ });
118
+
119
+ router.delete('/agents/:id/messages', (req, res) => {
120
+ stmts.deleteMessages.run(req.params.id);
121
+ res.json({ cleared: true });
122
+ });
123
+
124
+ router.post('/agents/:id/chat', (req, res) => {
125
+ const agent = stmts.getAgent.get(req.params.id);
126
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
127
+ const { content } = req.body;
128
+ if (!content) return res.status(400).json({ error: 'content required' });
129
+
130
+ res.json({ status: 'queued' });
131
+
132
+ chat.enqueueMessage(req.params.id, content).catch(err => {
133
+ chat.emit(req.params.id, 'error', { error: err.message });
134
+ });
135
+ });
136
+
137
+ router.get('/agents/:id/stream', (req, res) => {
138
+ const agent = stmts.getAgent.get(req.params.id);
139
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
140
+
141
+ res.writeHead(200, {
142
+ 'content-type': 'text/event-stream',
143
+ 'cache-control': 'no-cache',
144
+ 'connection': 'keep-alive'
145
+ });
146
+
147
+ res.write(`event: connected\ndata: ${JSON.stringify({ agent_id: req.params.id })}\n\n`);
148
+
149
+ if (chat.isProcessing(req.params.id)) {
150
+ res.write(`event: typing\ndata: ${JSON.stringify({ active: true })}\n\n`);
151
+ }
152
+
153
+ chat.addStream(req.params.id, res);
154
+
155
+ req.on('close', () => {
156
+ chat.removeStream(req.params.id, res);
157
+ });
158
+ });
159
+
160
+ router.get('/agents/:id/workspace', (req, res) => {
161
+ const agent = stmts.getAgent.get(req.params.id);
162
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
163
+ const files = workspace.listFiles(agent.name);
164
+ res.json(agent.bootstrapped === 0 ? files.filter(f => f !== 'BOOTSTRAP.md') : files);
165
+ });
166
+
167
+ router.get('/agents/:id/logs', (req, res) => {
168
+ const agent = stmts.getAgent.get(req.params.id);
169
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
170
+ res.json(workspace.listMemoryLogs(agent.name));
171
+ });
172
+
173
+ router.get('/agents/:id/workspace/*', (req, res) => {
174
+ const agent = stmts.getAgent.get(req.params.id);
175
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
176
+ const filePath = req.params[0];
177
+ if (filePath.includes('..')) return res.status(400).json({ error: 'invalid path' });
178
+ const content = workspace.readFile(agent.name, filePath);
179
+ if (content === null) return res.status(404).json({ error: 'file not found' });
180
+ res.json({ path: filePath, content });
181
+ });
182
+
183
+ router.put('/agents/:id/workspace/*', (req, res) => {
184
+ const agent = stmts.getAgent.get(req.params.id);
185
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
186
+ const filePath = req.params[0];
187
+ if (filePath.includes('..')) return res.status(400).json({ error: 'invalid path' });
188
+ const { content } = req.body;
189
+ if (typeof content !== 'string') return res.status(400).json({ error: 'content required' });
190
+ workspace.writeFile(agent.name, filePath, content);
191
+ res.json({ written: true, path: filePath });
192
+ });
193
+
194
+ module.exports = router;
@@ -0,0 +1,41 @@
1
+ const express = require('express');
2
+ const connections = require('../services/connections');
3
+
4
+ const router = express.Router();
5
+
6
+ router.get('/', (req, res) => {
7
+ res.json(connections.list());
8
+ });
9
+
10
+ router.post('/:platform/enable', async (req, res) => {
11
+ const { platform } = req.params;
12
+ const config = req.body || {};
13
+ try {
14
+ await connections.enable(platform, config);
15
+ res.json({ enabled: true });
16
+ } catch (err) {
17
+ res.status(500).json({ error: err.message });
18
+ }
19
+ });
20
+
21
+ router.post('/:platform/disable', async (req, res) => {
22
+ const { platform } = req.params;
23
+ try {
24
+ await connections.disable(platform);
25
+ res.json({ disabled: true });
26
+ } catch (err) {
27
+ res.status(500).json({ error: err.message });
28
+ }
29
+ });
30
+
31
+ router.delete('/:platform', async (req, res) => {
32
+ const { platform } = req.params;
33
+ try {
34
+ await connections.remove(platform);
35
+ res.json({ removed: true });
36
+ } catch (err) {
37
+ res.status(500).json({ error: err.message });
38
+ }
39
+ });
40
+
41
+ module.exports = router;
@@ -0,0 +1,37 @@
1
+ const express = require('express');
2
+ const { stmts } = require('../db');
3
+ const chat = require('../services/chat');
4
+
5
+ const router = express.Router();
6
+
7
+ router.use((req, res, next) => {
8
+ const secret = process.env.RELAY_SECRET;
9
+ if (!secret) return res.status(500).json({ error: 'relay not configured' });
10
+ const auth = req.headers.authorization;
11
+ if (!auth || auth !== `Bearer ${secret}`) {
12
+ return res.status(401).json({ error: 'unauthorized' });
13
+ }
14
+ next();
15
+ });
16
+
17
+ router.post('/chat', (req, res) => {
18
+ const { agent_name, content } = req.body;
19
+ if (!agent_name || !content) return res.status(400).json({ error: 'agent_name and content required' });
20
+
21
+ const agent = stmts.getAgentByName.get(agent_name);
22
+ if (!agent) return res.status(404).json({ error: `agent not found: ${agent_name}` });
23
+
24
+ chat.enqueueMessage(agent.id, content).catch(err => {
25
+ chat.emit(agent.id, 'error', { error: err.message });
26
+ });
27
+
28
+ res.json({ status: 'queued' });
29
+ });
30
+
31
+ router.get('/messages/:agent_name', (req, res) => {
32
+ const agent = stmts.getAgentByName.get(req.params.agent_name);
33
+ if (!agent) return res.status(404).json({ error: 'agent not found' });
34
+ res.json(stmts.listMessages.all(agent.id));
35
+ });
36
+
37
+ module.exports = router;