reeboot 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.
Files changed (110) hide show
  1. package/README.md +361 -0
  2. package/container/Dockerfile +48 -0
  3. package/container/entrypoint.sh +8 -0
  4. package/dist/agent-runner/index.d.ts +9 -0
  5. package/dist/agent-runner/index.d.ts.map +1 -0
  6. package/dist/agent-runner/index.js +21 -0
  7. package/dist/agent-runner/index.js.map +1 -0
  8. package/dist/agent-runner/interface.d.ts +56 -0
  9. package/dist/agent-runner/interface.d.ts.map +1 -0
  10. package/dist/agent-runner/interface.js +5 -0
  11. package/dist/agent-runner/interface.js.map +1 -0
  12. package/dist/agent-runner/pi-runner.d.ts +41 -0
  13. package/dist/agent-runner/pi-runner.d.ts.map +1 -0
  14. package/dist/agent-runner/pi-runner.js +162 -0
  15. package/dist/agent-runner/pi-runner.js.map +1 -0
  16. package/dist/channels/interface.d.ts +63 -0
  17. package/dist/channels/interface.d.ts.map +1 -0
  18. package/dist/channels/interface.js +33 -0
  19. package/dist/channels/interface.js.map +1 -0
  20. package/dist/channels/registry.d.ts +30 -0
  21. package/dist/channels/registry.d.ts.map +1 -0
  22. package/dist/channels/registry.js +71 -0
  23. package/dist/channels/registry.js.map +1 -0
  24. package/dist/channels/signal.d.ts +51 -0
  25. package/dist/channels/signal.d.ts.map +1 -0
  26. package/dist/channels/signal.js +263 -0
  27. package/dist/channels/signal.js.map +1 -0
  28. package/dist/channels/web.d.ts +35 -0
  29. package/dist/channels/web.d.ts.map +1 -0
  30. package/dist/channels/web.js +65 -0
  31. package/dist/channels/web.js.map +1 -0
  32. package/dist/channels/whatsapp.d.ts +25 -0
  33. package/dist/channels/whatsapp.d.ts.map +1 -0
  34. package/dist/channels/whatsapp.js +150 -0
  35. package/dist/channels/whatsapp.js.map +1 -0
  36. package/dist/config.d.ts +366 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +140 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/context.d.ts +69 -0
  41. package/dist/context.d.ts.map +1 -0
  42. package/dist/context.js +166 -0
  43. package/dist/context.js.map +1 -0
  44. package/dist/credential-proxy.d.ts +25 -0
  45. package/dist/credential-proxy.d.ts.map +1 -0
  46. package/dist/credential-proxy.js +96 -0
  47. package/dist/credential-proxy.js.map +1 -0
  48. package/dist/daemon.d.ts +25 -0
  49. package/dist/daemon.d.ts.map +1 -0
  50. package/dist/daemon.js +138 -0
  51. package/dist/daemon.js.map +1 -0
  52. package/dist/db/index.d.ts +23 -0
  53. package/dist/db/index.d.ts.map +1 -0
  54. package/dist/db/index.js +113 -0
  55. package/dist/db/index.js.map +1 -0
  56. package/dist/db/schema.d.ts +408 -0
  57. package/dist/db/schema.d.ts.map +1 -0
  58. package/dist/db/schema.js +55 -0
  59. package/dist/db/schema.js.map +1 -0
  60. package/dist/doctor.d.ts +23 -0
  61. package/dist/doctor.d.ts.map +1 -0
  62. package/dist/doctor.js +217 -0
  63. package/dist/doctor.js.map +1 -0
  64. package/dist/extensions/loader.d.ts +19 -0
  65. package/dist/extensions/loader.d.ts.map +1 -0
  66. package/dist/extensions/loader.js +124 -0
  67. package/dist/extensions/loader.js.map +1 -0
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +561 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/orchestrator.d.ts +60 -0
  73. package/dist/orchestrator.d.ts.map +1 -0
  74. package/dist/orchestrator.js +313 -0
  75. package/dist/orchestrator.js.map +1 -0
  76. package/dist/packages.d.ts +21 -0
  77. package/dist/packages.d.ts.map +1 -0
  78. package/dist/packages.js +116 -0
  79. package/dist/packages.js.map +1 -0
  80. package/dist/scheduler-registry.d.ts +8 -0
  81. package/dist/scheduler-registry.d.ts.map +1 -0
  82. package/dist/scheduler-registry.js +14 -0
  83. package/dist/scheduler-registry.js.map +1 -0
  84. package/dist/scheduler.d.ts +60 -0
  85. package/dist/scheduler.d.ts.map +1 -0
  86. package/dist/scheduler.js +143 -0
  87. package/dist/scheduler.js.map +1 -0
  88. package/dist/server.d.ts +18 -0
  89. package/dist/server.d.ts.map +1 -0
  90. package/dist/server.js +489 -0
  91. package/dist/server.js.map +1 -0
  92. package/dist/setup-wizard.d.ts +12 -0
  93. package/dist/setup-wizard.d.ts.map +1 -0
  94. package/dist/setup-wizard.js +163 -0
  95. package/dist/setup-wizard.js.map +1 -0
  96. package/extensions/confirm-destructive.ts +59 -0
  97. package/extensions/custom-compaction.ts +114 -0
  98. package/extensions/protected-paths.ts +30 -0
  99. package/extensions/sandbox/index.ts +317 -0
  100. package/extensions/sandbox/package-lock.json +92 -0
  101. package/extensions/sandbox/package.json +19 -0
  102. package/extensions/scheduler-tool.ts +65 -0
  103. package/extensions/session-name.ts +27 -0
  104. package/extensions/token-meter.ts +55 -0
  105. package/package.json +68 -0
  106. package/skills/send-message/SKILL.md +27 -0
  107. package/skills/web-search/SKILL.md +32 -0
  108. package/templates/global-agents.md +23 -0
  109. package/templates/main-agents.md +28 -0
  110. package/webchat/index.html +421 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Scheduler
3
+ *
4
+ * Loads enabled tasks from the SQLite `tasks` table on startup,
5
+ * registers node-cron jobs, and dispatches prompts to the orchestrator
6
+ * when jobs fire. Updates `last_run` after each execution.
7
+ */
8
+ import * as cron from 'node-cron';
9
+ import { nanoid } from 'nanoid';
10
+ // ─── Scheduler ────────────────────────────────────────────────────────────────
11
+ export class Scheduler {
12
+ _db;
13
+ _orchestrator;
14
+ _jobs = new Map();
15
+ constructor(db, orchestrator) {
16
+ this._db = db;
17
+ this._orchestrator = orchestrator;
18
+ }
19
+ async start() {
20
+ // Load all enabled tasks and register cron jobs
21
+ const tasks = this._db
22
+ .prepare('SELECT * FROM tasks WHERE enabled = 1')
23
+ .all();
24
+ for (const task of tasks) {
25
+ this.registerJob({
26
+ id: task.id,
27
+ contextId: task.context_id,
28
+ schedule: task.schedule,
29
+ prompt: task.prompt,
30
+ });
31
+ }
32
+ }
33
+ registerJob(task) {
34
+ // Cancel existing job for this task if any
35
+ const existing = this._jobs.get(task.id);
36
+ if (existing) {
37
+ existing.stop();
38
+ }
39
+ const job = cron.schedule(task.schedule, async () => {
40
+ try {
41
+ await this._orchestrator.handleScheduledTask({
42
+ taskId: task.id,
43
+ contextId: task.contextId,
44
+ prompt: task.prompt,
45
+ });
46
+ }
47
+ catch (err) {
48
+ console.error(`[Scheduler] Task ${task.id} failed: ${err}`);
49
+ }
50
+ finally {
51
+ // Update last_run
52
+ this._db
53
+ .prepare("UPDATE tasks SET last_run = datetime('now') WHERE id = ?")
54
+ .run(task.id);
55
+ }
56
+ });
57
+ this._jobs.set(task.id, job);
58
+ }
59
+ cancelJob(taskId) {
60
+ const job = this._jobs.get(taskId);
61
+ if (job) {
62
+ job.stop();
63
+ this._jobs.delete(taskId);
64
+ }
65
+ }
66
+ stop() {
67
+ for (const [, job] of this._jobs) {
68
+ job.stop();
69
+ }
70
+ this._jobs.clear();
71
+ }
72
+ }
73
+ export function createSchedulerTools(db, scheduler) {
74
+ return {
75
+ async schedule_task(params) {
76
+ // Validate cron expression
77
+ if (!cron.validate(params.schedule)) {
78
+ return {
79
+ content: [{ type: 'text', text: `Invalid cron expression: ${params.schedule}` }],
80
+ details: {},
81
+ isError: true,
82
+ };
83
+ }
84
+ const contextId = params.contextId ?? 'main';
85
+ const id = nanoid();
86
+ try {
87
+ db.prepare('INSERT INTO tasks (id, context_id, schedule, prompt, enabled) VALUES (?, ?, ?, ?, 1)').run(id, contextId, params.schedule, params.prompt);
88
+ scheduler.registerJob({
89
+ id,
90
+ contextId,
91
+ schedule: params.schedule,
92
+ prompt: params.prompt,
93
+ });
94
+ return {
95
+ content: [{ type: 'text', text: `Scheduled task created (id: ${id})` }],
96
+ details: { id, schedule: params.schedule, contextId },
97
+ };
98
+ }
99
+ catch (err) {
100
+ return {
101
+ content: [{ type: 'text', text: `Failed to schedule task: ${err.message}` }],
102
+ details: {},
103
+ isError: true,
104
+ };
105
+ }
106
+ },
107
+ async list_tasks(_params) {
108
+ const tasks = db
109
+ .prepare('SELECT id, context_id, schedule, prompt, enabled, last_run FROM tasks')
110
+ .all();
111
+ if (tasks.length === 0) {
112
+ return {
113
+ content: [{ type: 'text', text: 'No scheduled tasks.' }],
114
+ details: { tasks: [] },
115
+ };
116
+ }
117
+ const lines = tasks.map((t) => `[${t.id}] ${t.schedule} → ${t.prompt} (context: ${t.context_id}, enabled: ${t.enabled ? 'yes' : 'no'}, last_run: ${t.last_run ?? 'never'})`);
118
+ return {
119
+ content: [{ type: 'text', text: lines.join('\n') }],
120
+ details: { tasks },
121
+ };
122
+ },
123
+ async cancel_task(params) {
124
+ const task = db
125
+ .prepare('SELECT id FROM tasks WHERE id = ?')
126
+ .get(params.task_id);
127
+ if (!task) {
128
+ return {
129
+ content: [{ type: 'text', text: `Task not found: ${params.task_id}` }],
130
+ details: {},
131
+ isError: true,
132
+ };
133
+ }
134
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(params.task_id);
135
+ scheduler.cancelJob(params.task_id);
136
+ return {
137
+ content: [{ type: 'text', text: `Cancelled task ${params.task_id}` }],
138
+ details: { taskId: params.task_id },
139
+ };
140
+ },
141
+ };
142
+ }
143
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import type Database from 'better-sqlite3';
3
+ export interface ServerOptions {
4
+ port?: number;
5
+ host?: string;
6
+ logLevel?: string;
7
+ /** Injected DB for testing (otherwise uses getDb() singleton) */
8
+ db?: Database.Database;
9
+ /** Override ~/.reeboot directory for testing */
10
+ reebotDir?: string;
11
+ /** Auth token (if set, non-loopback WS connections must provide it) */
12
+ token?: string;
13
+ /** App config for runner creation */
14
+ config?: import('./config.js').Config;
15
+ }
16
+ export declare function startServer(opts?: ServerOptions): Promise<FastifyInstance>;
17
+ export declare function stopServer(): Promise<void>;
18
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAMnE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAmC3C,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,EAAE,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACvB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,OAAO,aAAa,EAAE,MAAM,CAAC;CACvC;AAoCD,wBAAsB,WAAW,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CA+dpF;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAmChD"}
package/dist/server.js ADDED
@@ -0,0 +1,489 @@
1
+ import Fastify from 'fastify';
2
+ import fastifyWebsocket from '@fastify/websocket';
3
+ import fastifyStatic from '@fastify/static';
4
+ import { readFileSync } from 'fs';
5
+ import { resolve, dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { createRunner } from './agent-runner/index.js';
8
+ import { listContexts, createContext, getContextById, listSessions, initContextWorkspace, createContextsTable, } from './context.js';
9
+ import { nanoid } from 'nanoid';
10
+ import { homedir } from 'os';
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ // Read version from package.json
13
+ function getVersion() {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
16
+ return pkg.version ?? '0.0.0';
17
+ }
18
+ catch {
19
+ return '0.0.0';
20
+ }
21
+ }
22
+ const startTime = Date.now();
23
+ // ─── Singleton ───────────────────────────────────────────────────────────────
24
+ let _server = null;
25
+ // Active runners: contextId → AgentRunner
26
+ const _activeRunners = new Map();
27
+ // Channel adapters (set during startServer)
28
+ let _channelAdapters = new Map();
29
+ // Orchestrator (set during startServer)
30
+ let _orchestrator = null;
31
+ // Scheduler (set during startServer)
32
+ let _scheduler = null;
33
+ // Credential proxy (set during startServer)
34
+ let _credProxy = null;
35
+ // ─── Auth helper ─────────────────────────────────────────────────────────────
36
+ function isLoopback(ip) {
37
+ return ip === '127.0.0.1' || ip === '::1' || ip === 'localhost';
38
+ }
39
+ function extractToken(req) {
40
+ const authHeader = req.headers['authorization'];
41
+ if (authHeader?.startsWith('Bearer '))
42
+ return authHeader.slice(7);
43
+ const url = new URL(req.url, 'http://localhost');
44
+ return url.searchParams.get('token') ?? undefined;
45
+ }
46
+ // ─── startServer ─────────────────────────────────────────────────────────────
47
+ export async function startServer(opts = {}) {
48
+ const port = opts.port ?? 3000;
49
+ const host = opts.host ?? '127.0.0.1';
50
+ const logLevel = opts.logLevel ?? 'info';
51
+ const reebotDir = opts.reebotDir ?? join(homedir(), '.reeboot');
52
+ const serverToken = opts.token;
53
+ const isDev = process.env.NODE_ENV !== 'production';
54
+ const logger = logLevel === 'silent'
55
+ ? false
56
+ : {
57
+ level: logLevel,
58
+ ...(isDev ? { transport: { target: 'pino-pretty', options: { colorize: true } } } : {}),
59
+ };
60
+ const server = Fastify({ logger });
61
+ // Register WebSocket plugin
62
+ await server.register(fastifyWebsocket);
63
+ // Register static file serving for webchat
64
+ const webchatDir = resolve(__dirname, '../webchat');
65
+ try {
66
+ await server.register(fastifyStatic, {
67
+ root: webchatDir,
68
+ prefix: '/',
69
+ decorateReply: false,
70
+ });
71
+ }
72
+ catch {
73
+ // webchat dir may not exist in test environments — that's OK
74
+ }
75
+ // Get or set up the DB
76
+ let db;
77
+ if (opts.db) {
78
+ db = opts.db;
79
+ createContextsTable(db);
80
+ // Ensure main context exists
81
+ if (!getContextById(db, 'main')) {
82
+ createContext(db, { id: 'main', name: 'main', modelProvider: '', modelId: '' });
83
+ }
84
+ }
85
+ else {
86
+ const { openDatabase } = await import('./db/index.js');
87
+ db = openDatabase();
88
+ // Ensure main context exists
89
+ if (!getContextById(db, 'main')) {
90
+ createContext(db, { id: 'main', name: 'main', modelProvider: '', modelId: '' });
91
+ }
92
+ }
93
+ // Ensure context workspace for "main"
94
+ await initContextWorkspace('main', reebotDir);
95
+ // ── Channel & Orchestrator init ───────────────────────────────────────────
96
+ const appConfig = opts.config;
97
+ if (appConfig) {
98
+ try {
99
+ // Import built-in adapters so they self-register
100
+ await import('./channels/web.js');
101
+ await import('./channels/whatsapp.js');
102
+ await import('./channels/signal.js');
103
+ const { globalRegistry } = await import('./channels/registry.js');
104
+ const { MessageBus } = await import('./channels/interface.js');
105
+ const { Orchestrator: OrchestratorClass } = await import('./orchestrator.js');
106
+ const bus = new MessageBus();
107
+ // Init channels from config
108
+ _channelAdapters = await globalRegistry.initChannels(appConfig, bus);
109
+ // Build runner map for orchestrator (main context)
110
+ const orchestratorRunners = new Map();
111
+ const contexts = listContexts(db);
112
+ for (const ctx of contexts) {
113
+ orchestratorRunners.set(ctx.id, createRunner({ id: ctx.id, workspacePath: join(reebotDir, 'contexts', ctx.id, 'workspace') }, appConfig));
114
+ }
115
+ _orchestrator = new OrchestratorClass(appConfig, bus, _channelAdapters, orchestratorRunners);
116
+ _orchestrator.start();
117
+ // ── Credential proxy init ──────────────────────────────────────────
118
+ if (appConfig.credentialProxy?.enabled) {
119
+ try {
120
+ const { startProxy } = await import('./credential-proxy.js');
121
+ const proxyServer = await startProxy(appConfig);
122
+ if (proxyServer) {
123
+ _credProxy = proxyServer;
124
+ console.log('[server] Credential proxy started on 127.0.0.1:3001');
125
+ }
126
+ }
127
+ catch (err) {
128
+ console.error('[server] Credential proxy init failed:', err);
129
+ }
130
+ }
131
+ // ── Scheduler init (after orchestrator) ────────────────────────────
132
+ try {
133
+ const { Scheduler } = await import('./scheduler.js');
134
+ const { setGlobalScheduler } = await import('./scheduler-registry.js');
135
+ const schedulerOrchestrator = {
136
+ handleScheduledTask: async (task) => {
137
+ // Inject scheduled task as a message via the bus
138
+ const { MessageBus: MB, createIncomingMessage } = await import('./channels/interface.js');
139
+ bus.publish(createIncomingMessage({
140
+ channelType: 'scheduler',
141
+ peerId: 'scheduler',
142
+ content: task.prompt,
143
+ raw: { taskId: task.taskId },
144
+ }));
145
+ },
146
+ };
147
+ const schedulerInstance = new Scheduler(db, schedulerOrchestrator);
148
+ await schedulerInstance.start();
149
+ setGlobalScheduler(schedulerInstance);
150
+ _scheduler = schedulerInstance;
151
+ console.log('[server] Scheduler started');
152
+ }
153
+ catch (err) {
154
+ console.error('[server] Scheduler init failed:', err);
155
+ }
156
+ }
157
+ catch (err) {
158
+ console.error('[server] Channel/orchestrator init failed:', err);
159
+ }
160
+ }
161
+ // ── Routes ────────────────────────────────────────────────────────────────
162
+ // GET / — serve WebChat
163
+ server.get('/', async (req, reply) => {
164
+ const webchatPath = resolve(__dirname, '../webchat/index.html');
165
+ try {
166
+ const html = readFileSync(webchatPath, 'utf-8');
167
+ reply.type('text/html').send(html);
168
+ }
169
+ catch {
170
+ reply.status(404).send({ error: 'WebChat not found' });
171
+ }
172
+ });
173
+ // GET /api/health
174
+ server.get('/api/health', async (_req, _reply) => {
175
+ return {
176
+ status: 'ok',
177
+ uptime: Math.floor((Date.now() - startTime) / 1000),
178
+ version: getVersion(),
179
+ };
180
+ });
181
+ // GET /api/status
182
+ server.get('/api/status', async (_req, _reply) => {
183
+ return {
184
+ agent: { name: 'Reeboot', model: { provider: '', id: '' } },
185
+ channels: [],
186
+ uptime: Math.floor((Date.now() - startTime) / 1000),
187
+ };
188
+ });
189
+ // ── Channel REST API ──────────────────────────────────────────────────────
190
+ // GET /api/channels
191
+ server.get('/api/channels', async (_req, _reply) => {
192
+ const result = [];
193
+ for (const [type, adapter] of _channelAdapters) {
194
+ result.push({ type, status: adapter.status(), connectedAt: adapter.connectedAt() });
195
+ }
196
+ return result;
197
+ });
198
+ // POST /api/channels/:type/login
199
+ server.post('/api/channels/:type/login', async (req, reply) => {
200
+ const { type } = req.params;
201
+ const adapter = _channelAdapters.get(type);
202
+ if (!adapter) {
203
+ return reply.status(404).send({ error: `Unknown channel type: ${type}` });
204
+ }
205
+ // Start login flow asynchronously (QR appears in terminal)
206
+ adapter.start().catch((err) => console.error(`[channels] login error for ${type}:`, err));
207
+ return reply.status(202).send({ message: 'Login initiated. Check terminal for QR code.' });
208
+ });
209
+ // POST /api/channels/:type/logout
210
+ server.post('/api/channels/:type/logout', async (req, reply) => {
211
+ const { type } = req.params;
212
+ const adapter = _channelAdapters.get(type);
213
+ if (!adapter) {
214
+ return reply.status(404).send({ error: `Unknown channel type: ${type}` });
215
+ }
216
+ await adapter.stop();
217
+ return reply.status(200).send({ message: `${type} logged out.` });
218
+ });
219
+ // ── Reload & Restart ──────────────────────────────────────────────────────
220
+ // POST /api/reload — hot-reload extensions/skills on all runners
221
+ server.post('/api/reload', async (_req, reply) => {
222
+ if (!_orchestrator) {
223
+ return reply.status(503).send({ error: 'Orchestrator not running' });
224
+ }
225
+ const errors = [];
226
+ for (const [id, runner] of _orchestrator.runners) {
227
+ try {
228
+ await runner.reload();
229
+ }
230
+ catch (err) {
231
+ errors.push(`${id}: ${err.message}`);
232
+ }
233
+ }
234
+ if (errors.length > 0) {
235
+ return reply.status(500).send({ error: errors.join('; ') });
236
+ }
237
+ return { message: 'Extensions and skills reloaded.' };
238
+ });
239
+ // POST /api/restart — graceful shutdown, process supervisor restarts
240
+ server.post('/api/restart', async (_req, reply) => {
241
+ reply.status(200).send({ message: 'Restarting...' });
242
+ // Drain in-flight turns (timeout 30s)
243
+ const DRAIN_TIMEOUT_MS = 30_000;
244
+ const drainStart = Date.now();
245
+ // Stop orchestrator so no new messages are dispatched
246
+ if (_orchestrator) {
247
+ _orchestrator.stop();
248
+ }
249
+ // Wait for active runners (ws handler uses _activeRunners)
250
+ while (_activeRunners.size > 0 && Date.now() - drainStart < DRAIN_TIMEOUT_MS) {
251
+ await new Promise(r => setTimeout(r, 100));
252
+ }
253
+ // Stop channels and dispose orchestrator runners
254
+ for (const adapter of _channelAdapters.values()) {
255
+ try {
256
+ await adapter.stop();
257
+ }
258
+ catch { /* ignore */ }
259
+ }
260
+ if (_orchestrator) {
261
+ for (const runner of _orchestrator.runners.values()) {
262
+ try {
263
+ await runner.dispose();
264
+ }
265
+ catch { /* ignore */ }
266
+ }
267
+ _orchestrator = null;
268
+ }
269
+ // Close server and exit — supervisor restarts
270
+ try {
271
+ await _server?.close();
272
+ }
273
+ catch { /* ignore */ }
274
+ process.exit(0);
275
+ });
276
+ // ── REST: Task API ────────────────────────────────────────────────────────
277
+ // GET /api/tasks
278
+ server.get('/api/tasks', async (_req, _reply) => {
279
+ const tasks = db
280
+ .prepare('SELECT id, context_id as contextId, schedule, prompt, enabled, last_run as lastRun, created_at as createdAt FROM tasks')
281
+ .all();
282
+ return tasks;
283
+ });
284
+ // POST /api/tasks
285
+ server.post('/api/tasks', async (req, reply) => {
286
+ const { contextId = 'main', schedule, prompt } = req.body ?? {};
287
+ if (!schedule || !prompt) {
288
+ return reply.status(400).send({ error: 'schedule and prompt are required' });
289
+ }
290
+ // Validate cron expression
291
+ const { validate: cronValidate } = await import('node-cron');
292
+ if (!cronValidate(schedule)) {
293
+ return reply.status(400).send({ error: 'Invalid cron expression' });
294
+ }
295
+ const { nanoid: nanoId } = await import('nanoid');
296
+ const id = nanoId();
297
+ db.prepare('INSERT INTO tasks (id, context_id, schedule, prompt, enabled) VALUES (?, ?, ?, ?, 1)').run(id, contextId, schedule, prompt);
298
+ // Register with scheduler if available
299
+ if (_scheduler) {
300
+ _scheduler.registerJob({ id, contextId, schedule, prompt });
301
+ }
302
+ const task = db
303
+ .prepare('SELECT id, context_id as contextId, schedule, prompt, enabled, last_run as lastRun FROM tasks WHERE id = ?')
304
+ .get(id);
305
+ return reply.status(201).send(task);
306
+ });
307
+ // DELETE /api/tasks/:id
308
+ server.delete('/api/tasks/:id', async (req, reply) => {
309
+ const { id } = req.params;
310
+ const task = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id);
311
+ if (!task) {
312
+ return reply.status(404).send({ error: `Task not found: ${id}` });
313
+ }
314
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
315
+ if (_scheduler) {
316
+ _scheduler.cancelJob(id);
317
+ }
318
+ return reply.status(204).send();
319
+ });
320
+ // ── REST: Context API ─────────────────────────────────────────────────────
321
+ // GET /api/contexts
322
+ server.get('/api/contexts', async (_req, _reply) => {
323
+ return listContexts(db);
324
+ });
325
+ // POST /api/contexts
326
+ server.post('/api/contexts', async (req, reply) => {
327
+ const { name, model_provider, model_id } = req.body ?? {};
328
+ if (!name) {
329
+ return reply.status(400).send({ error: 'name is required' });
330
+ }
331
+ const ctx = createContext(db, {
332
+ name,
333
+ modelProvider: model_provider ?? '',
334
+ modelId: model_id ?? '',
335
+ });
336
+ await initContextWorkspace(ctx.id, reebotDir);
337
+ return reply.status(201).send(ctx);
338
+ });
339
+ // GET /api/contexts/:id/sessions
340
+ server.get('/api/contexts/:id/sessions', async (req, reply) => {
341
+ const ctx = getContextById(db, req.params.id);
342
+ if (!ctx) {
343
+ return reply.status(404).send({ error: 'Context not found' });
344
+ }
345
+ const sessions = await listSessions(req.params.id, reebotDir);
346
+ return sessions;
347
+ });
348
+ // ── WebSocket: /ws/chat/:contextId ───────────────────────────────────────
349
+ server.get('/ws/chat/:contextId', { websocket: true }, async (socket, req) => {
350
+ const { contextId } = req.params;
351
+ // Auth check for non-loopback connections
352
+ if (serverToken) {
353
+ const clientIp = req.socket.remoteAddress ?? '';
354
+ if (!isLoopback(clientIp)) {
355
+ const provided = extractToken(req);
356
+ if (provided !== serverToken) {
357
+ socket.close(1008, 'Unauthorized');
358
+ return;
359
+ }
360
+ }
361
+ }
362
+ // Validate context exists
363
+ const ctx = getContextById(db, contextId);
364
+ if (!ctx) {
365
+ socket.close(4004, 'Unknown context');
366
+ return;
367
+ }
368
+ // Generate session ID
369
+ const sessionId = nanoid();
370
+ // Send connected message
371
+ socket.send(JSON.stringify({ type: 'connected', contextId, sessionId }));
372
+ let activeRunId = null;
373
+ socket.on('message', async (rawData) => {
374
+ let msg;
375
+ try {
376
+ msg = JSON.parse(rawData.toString());
377
+ }
378
+ catch {
379
+ socket.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
380
+ return;
381
+ }
382
+ if (msg.type === 'cancel') {
383
+ const runner = _activeRunners.get(contextId);
384
+ if (runner) {
385
+ runner.abort();
386
+ socket.send(JSON.stringify({ type: 'cancelled', runId: activeRunId }));
387
+ _activeRunners.delete(contextId);
388
+ activeRunId = null;
389
+ }
390
+ return;
391
+ }
392
+ if (msg.type === 'message') {
393
+ // Check if a turn is already in-flight
394
+ if (_activeRunners.has(contextId)) {
395
+ socket.send(JSON.stringify({
396
+ type: 'error',
397
+ message: 'Agent is busy. Cancel the current turn first.',
398
+ }));
399
+ return;
400
+ }
401
+ const runId = nanoid();
402
+ activeRunId = runId;
403
+ // Get or create runner
404
+ let runner;
405
+ try {
406
+ const { defaultConfig } = await import('./config.js');
407
+ const cfg = opts.config ?? defaultConfig;
408
+ runner = createRunner({ id: contextId, workspacePath: join(reebotDir, 'contexts', contextId, 'workspace') }, cfg);
409
+ }
410
+ catch (err) {
411
+ socket.send(JSON.stringify({ type: 'error', message: String(err.message) }));
412
+ return;
413
+ }
414
+ _activeRunners.set(contextId, runner);
415
+ try {
416
+ await runner.prompt(msg.content ?? '', (event) => {
417
+ socket.send(JSON.stringify(event));
418
+ });
419
+ }
420
+ catch (err) {
421
+ if (err?.name !== 'AbortError') {
422
+ socket.send(JSON.stringify({ type: 'error', message: String(err?.message ?? err) }));
423
+ }
424
+ }
425
+ finally {
426
+ _activeRunners.delete(contextId);
427
+ activeRunId = null;
428
+ }
429
+ }
430
+ });
431
+ socket.on('close', () => {
432
+ // Abort runner if still active
433
+ const runner = _activeRunners.get(contextId);
434
+ if (runner) {
435
+ runner.abort();
436
+ _activeRunners.delete(contextId);
437
+ }
438
+ });
439
+ });
440
+ // Custom 404 handler
441
+ server.setNotFoundHandler((_req, reply) => {
442
+ reply.status(404).send({ error: 'Not found' });
443
+ });
444
+ await server.listen({ port, host });
445
+ _server = server;
446
+ return server;
447
+ }
448
+ // ─── stopServer ──────────────────────────────────────────────────────────────
449
+ export async function stopServer() {
450
+ // Stop credential proxy
451
+ if (_credProxy) {
452
+ try {
453
+ await _credProxy.close();
454
+ }
455
+ catch { /* ignore */ }
456
+ _credProxy = null;
457
+ }
458
+ // Stop scheduler
459
+ if (_scheduler) {
460
+ _scheduler.stop();
461
+ _scheduler = null;
462
+ }
463
+ // Stop orchestrator
464
+ if (_orchestrator) {
465
+ _orchestrator.stop();
466
+ _orchestrator = null;
467
+ }
468
+ // Stop channel adapters
469
+ for (const adapter of _channelAdapters.values()) {
470
+ try {
471
+ await adapter.stop();
472
+ }
473
+ catch { /* ignore */ }
474
+ }
475
+ _channelAdapters.clear();
476
+ // Abort all active runners before closing
477
+ for (const runner of _activeRunners.values()) {
478
+ try {
479
+ runner.abort();
480
+ }
481
+ catch { /* ignore */ }
482
+ }
483
+ _activeRunners.clear();
484
+ if (_server) {
485
+ await _server.close();
486
+ _server = null;
487
+ }
488
+ }
489
+ //# sourceMappingURL=server.js.map