replicas-engine 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -3,17 +3,12 @@ import 'dotenv/config';
3
3
  import { serve } from '@hono/node-server';
4
4
  import { Hono } from 'hono';
5
5
  import { authMiddleware } from './middleware/auth.js';
6
- import ping from './routes/ping.js';
7
6
  import codex from './routes/codex.js';
8
7
  const app = new Hono();
9
- // Health check endpoint (no auth required)
10
8
  app.get('/health', (c) => {
11
9
  return c.json({ status: 'ok', timestamp: new Date().toISOString() });
12
10
  });
13
- // Apply auth middleware to all routes below
14
11
  app.use('*', authMiddleware);
15
- // Protected routes
16
- app.route('/ping', ping);
17
12
  app.route('/codex', codex);
18
13
  const port = Number(process.env.PORT) || 3737;
19
14
  serve({
@@ -2,11 +2,10 @@ import { Hono } from 'hono';
2
2
  import { stream } from 'hono/streaming';
3
3
  import { CodexManager } from '../services/codex-manager.js';
4
4
  const codex = new Hono();
5
- // Create a singleton instance of CodexManager
6
5
  const codexManager = new CodexManager();
7
6
  /**
8
7
  * POST /codex/send
9
- * Send a message to Codex and stream events back via Server-Sent Events (SSE)
8
+ * send a message to Codex and stream events back via Server-Sent Events (SSE)
10
9
  */
11
10
  codex.post('/send', async (c) => {
12
11
  try {
@@ -15,9 +14,7 @@ codex.post('/send', async (c) => {
15
14
  if (!message || typeof message !== 'string') {
16
15
  return c.json({ error: 'Message is required and must be a string' }, 400);
17
16
  }
18
- // Stream events using SSE
19
17
  return stream(c, async (stream) => {
20
- // Set SSE headers explicitly
21
18
  c.header('Content-Type', 'text/event-stream');
22
19
  c.header('Cache-Control', 'no-cache');
23
20
  c.header('Connection', 'keep-alive');
@@ -25,13 +22,10 @@ codex.post('/send', async (c) => {
25
22
  console.log('Client aborted SSE connection');
26
23
  });
27
24
  try {
28
- // Stream Codex events
29
25
  for await (const event of codexManager.sendMessage(message)) {
30
- // Format as Server-Sent Event
31
26
  const sseData = `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
32
27
  await stream.write(sseData);
33
28
  }
34
- // Send a final 'done' event to signal completion
35
29
  await stream.write('event: done\ndata: {}\n\n');
36
30
  }
37
31
  catch (error) {
@@ -53,7 +47,7 @@ codex.post('/send', async (c) => {
53
47
  });
54
48
  /**
55
49
  * GET /codex/history
56
- * Get the conversation history from the JSONL session file
50
+ * get the conversation history from the JSONL session file
57
51
  */
58
52
  codex.get('/history', async (c) => {
59
53
  try {
@@ -77,7 +71,7 @@ codex.get('/history', async (c) => {
77
71
  });
78
72
  /**
79
73
  * GET /codex/status
80
- * Get current thread status and information
74
+ * get current thread status and information
81
75
  */
82
76
  codex.get('/status', async (c) => {
83
77
  try {
@@ -94,7 +88,7 @@ codex.get('/status', async (c) => {
94
88
  });
95
89
  /**
96
90
  * POST /codex/reset
97
- * Reset the current thread and start fresh
91
+ * reset the current thread and start fresh
98
92
  */
99
93
  codex.post('/reset', async (c) => {
100
94
  try {
@@ -1,9 +1,5 @@
1
1
  import { Codex, Thread } from '@openai/codex-sdk';
2
2
  import { findSessionFile, readJSONL } from '../utils/jsonl-reader.js';
3
- /**
4
- * Manages Codex thread lifecycle and session state
5
- * Currently supports a single active thread per engine instance
6
- */
7
3
  export class CodexManager {
8
4
  codex;
9
5
  currentThreadId = null;
@@ -11,14 +7,22 @@ export class CodexManager {
11
7
  workingDirectory;
12
8
  constructor(workingDirectory) {
13
9
  this.codex = new Codex();
14
- this.workingDirectory = workingDirectory || process.env.WORKSPACE_HOME || process.env.HOME || '/home/ubuntu';
10
+ if (workingDirectory) {
11
+ this.workingDirectory = workingDirectory;
12
+ }
13
+ else {
14
+ const repoName = process.env.REPLICAS_REPO_NAME;
15
+ const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || '/home/ubuntu';
16
+ if (repoName) {
17
+ this.workingDirectory = `${workspaceHome}/workspaces/${repoName}`;
18
+ console.log(`Using repository working directory: ${this.workingDirectory}`);
19
+ }
20
+ else {
21
+ this.workingDirectory = workspaceHome;
22
+ }
23
+ }
15
24
  }
16
- /**
17
- * Send a message to Codex and stream events back
18
- * Creates a new thread on first call, resumes existing thread on subsequent calls
19
- */
20
25
  async *sendMessage(message) {
21
- // Initialize or resume thread
22
26
  if (!this.currentThread) {
23
27
  if (this.currentThreadId) {
24
28
  console.log(`Resuming thread ${this.currentThreadId}`);
@@ -37,10 +41,8 @@ export class CodexManager {
37
41
  });
38
42
  }
39
43
  }
40
- // Stream events from Codex
41
44
  const { events } = await this.currentThread.runStreamed(message);
42
45
  for await (const event of events) {
43
- // Capture thread ID when thread starts
44
46
  if (event.type === 'thread.started') {
45
47
  this.currentThreadId = event.thread_id;
46
48
  console.log(`Thread started: ${this.currentThreadId}`);
@@ -48,9 +50,6 @@ export class CodexManager {
48
50
  yield event;
49
51
  }
50
52
  }
51
- /**
52
- * Get conversation history by reading the JSONL session file
53
- */
54
53
  async getHistory() {
55
54
  if (!this.currentThreadId) {
56
55
  return {
@@ -73,9 +72,6 @@ export class CodexManager {
73
72
  events,
74
73
  };
75
74
  }
76
- /**
77
- * Get current thread status
78
- */
79
75
  async getStatus() {
80
76
  let sessionFile = null;
81
77
  if (this.currentThreadId) {
@@ -88,17 +84,11 @@ export class CodexManager {
88
84
  working_directory: this.workingDirectory,
89
85
  };
90
86
  }
91
- /**
92
- * Reset the current thread (start fresh conversation)
93
- */
94
87
  reset() {
95
88
  console.log('Resetting Codex thread');
96
89
  this.currentThread = null;
97
90
  this.currentThreadId = null;
98
91
  }
99
- /**
100
- * Get the current thread ID
101
- */
102
92
  getThreadId() {
103
93
  return this.currentThreadId;
104
94
  }
@@ -1,9 +1,6 @@
1
1
  import { readFile, readdir, stat } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- /**
5
- * Read and parse a JSONL file
6
- */
7
4
  export async function readJSONL(filePath) {
8
5
  try {
9
6
  const content = await readFile(filePath, 'utf-8');
@@ -26,24 +23,19 @@ export async function readJSONL(filePath) {
26
23
  return [];
27
24
  }
28
25
  }
29
- /**
30
- * Find the session file for a given thread ID
31
- * Sessions are stored in ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
32
- */
26
+ // Sessions are stored in ~/.codex/sessions/YYYY/MM/DD/rollout-*-{threadId}.jsonl
33
27
  export async function findSessionFile(threadId) {
34
28
  const sessionsDir = join(homedir(), '.codex', 'sessions');
35
29
  try {
36
- // Get current date for searching
30
+ // current date for searching
37
31
  const now = new Date();
38
32
  const year = now.getFullYear();
39
33
  const month = String(now.getMonth() + 1).padStart(2, '0');
40
34
  const day = String(now.getDate()).padStart(2, '0');
41
- // Search in today's directory first
42
35
  const todayDir = join(sessionsDir, String(year), month, day);
43
36
  const file = await findFileInDirectory(todayDir, threadId);
44
37
  if (file)
45
38
  return file;
46
- // If not found, search recent days (last 7 days)
47
39
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
48
40
  const date = new Date(now);
49
41
  date.setDate(date.getDate() - daysAgo);
@@ -62,9 +54,6 @@ export async function findSessionFile(threadId) {
62
54
  return null;
63
55
  }
64
56
  }
65
- /**
66
- * Search for a file containing the thread ID in a specific directory
67
- */
68
57
  async function findFileInDirectory(directory, threadId) {
69
58
  try {
70
59
  const files = await readdir(directory);
@@ -80,18 +69,13 @@ async function findFileInDirectory(directory, threadId) {
80
69
  return null;
81
70
  }
82
71
  catch (error) {
83
- // Directory might not exist, which is fine
84
72
  return null;
85
73
  }
86
74
  }
87
- /**
88
- * Get the most recent session file path (for debugging/info)
89
- */
90
75
  export async function getMostRecentSessionFile() {
91
76
  const sessionsDir = join(homedir(), '.codex', 'sessions');
92
77
  try {
93
78
  const now = new Date();
94
- // Search last 7 days for any session file
95
79
  for (let daysAgo = 0; daysAgo <= 7; daysAgo++) {
96
80
  const date = new Date(now);
97
81
  date.setDate(date.getDate() - daysAgo);
@@ -105,7 +89,6 @@ export async function getMostRecentSessionFile() {
105
89
  .filter((f) => f.endsWith('.jsonl'))
106
90
  .map((f) => join(searchDir, f));
107
91
  if (jsonlFiles.length > 0) {
108
- // Return the most recent file based on modification time
109
92
  const stats = await Promise.all(jsonlFiles.map(async (f) => ({
110
93
  path: f,
111
94
  mtime: (await stat(f)).mtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/README.md DELETED
@@ -1,286 +0,0 @@
1
- # Replicas Engine
2
-
3
- A lightweight REST API server that runs on each Replicas workspace, providing programmatic access to workspace functionality without requiring SSH access.
4
-
5
- ## Overview
6
-
7
- The Replicas Engine is automatically installed on every workspace and runs as a systemd service. It exposes a simple HTTP API protected by a secret key, allowing the Replicas backend to interact with workspaces for operations like reading logs, checking status, and more.
8
-
9
- ## Features
10
-
11
- - **Lightweight**: Built with Hono for minimal overhead
12
- - **Secure**: All endpoints (except health check) require authentication via secret header
13
- - **Automatic**: Installed and configured automatically on workspace creation
14
- - **Reliable**: Runs as a systemd service with automatic restart
15
-
16
- ## Installation
17
-
18
- The engine is automatically installed during workspace initialization. For manual installation:
19
-
20
- ```bash
21
- npm install -g replicas-engine
22
- ```
23
-
24
- ## Configuration
25
-
26
- The engine requires the following environment variables:
27
-
28
- - `PORT` - Port to listen on (default: 3737)
29
- - `REPLICAS_ENGINE_SECRET` - Secret key for authentication (required)
30
- - `NODE_ENV` - Environment mode (production/development)
31
- - `WORKSPACE_HOME` - Working directory for Codex (default: `/home/ubuntu`)
32
-
33
- ### Running as a Service
34
-
35
- The engine automatically runs as a systemd service named `replicas-engine`. You can manage it using:
36
-
37
- ```bash
38
- # Check status
39
- systemctl status replicas-engine
40
-
41
- # View logs
42
- journalctl -u replicas-engine -f
43
-
44
- # Restart service
45
- sudo systemctl restart replicas-engine
46
-
47
- # Stop service
48
- sudo systemctl stop replicas-engine
49
- ```
50
-
51
- ## API Endpoints
52
-
53
- ### Health Check
54
-
55
- **GET /health**
56
-
57
- Public endpoint for health checks (no authentication required).
58
-
59
- ```bash
60
- curl http://localhost:3737/health
61
- ```
62
-
63
- Response:
64
- ```json
65
- {
66
- "status": "ok",
67
- "timestamp": "2025-10-26T04:00:00.000Z"
68
- }
69
- ```
70
-
71
- ### Ping
72
-
73
- **GET /ping**
74
-
75
- Authenticated endpoint for testing connectivity.
76
-
77
- ```bash
78
- curl http://localhost:3737/ping \
79
- -H "X-Replicas-Engine-Secret: your-secret-here"
80
- ```
81
-
82
- Response:
83
- ```json
84
- {
85
- "message": "pong",
86
- "timestamp": "2025-10-26T04:00:00.000Z"
87
- }
88
- ```
89
-
90
- ---
91
-
92
- ## Codex Integration
93
-
94
- The engine provides remote control of Codex coding agents running on the workspace.
95
-
96
- ### Send Message (with Streaming)
97
-
98
- **POST /codex/send**
99
-
100
- Send a message to Codex and stream events back via Server-Sent Events (SSE).
101
-
102
- ```bash
103
- curl -N http://localhost:3737/codex/send \
104
- -H "X-Replicas-Engine-Secret: your-secret-here" \
105
- -H "Content-Type: application/json" \
106
- -d '{"message": "Add error handling to the login function"}'
107
- ```
108
-
109
- **Request Body:**
110
- ```json
111
- {
112
- "message": "Your instruction to Codex"
113
- }
114
- ```
115
-
116
- **Response (SSE Stream):**
117
- ```
118
- event: thread.started
119
- data: {"thread_id":"019a1caa-bceb-7731-b607-e9d22c1933a5"}
120
-
121
- event: turn.started
122
- data: {}
123
-
124
- event: item.started
125
- data: {"item":{"id":"...","type":"agent_message","text":"..."}}
126
-
127
- event: item.completed
128
- data: {"item":{"id":"...","type":"agent_message","text":"I'll help add error handling..."}}
129
-
130
- event: turn.completed
131
- data: {"usage":{"input_tokens":100,"output_tokens":50,"cached_input_tokens":0}}
132
-
133
- event: done
134
- data: {}
135
- ```
136
-
137
- **Event Types:**
138
- - `thread.started` - New thread created (includes thread_id)
139
- - `turn.started` - Turn begins
140
- - `item.started` - New item (message, command, file change, etc.)
141
- - `item.updated` - Item updated
142
- - `item.completed` - Item finished
143
- - `turn.completed` - Turn ends (includes token usage)
144
- - `error` - Error occurred
145
- - `done` - Stream completed
146
-
147
- ### Get Conversation History
148
-
149
- **GET /codex/history**
150
-
151
- Retrieve the full conversation history from the JSONL session file.
152
-
153
- ```bash
154
- curl http://localhost:3737/codex/history \
155
- -H "X-Replicas-Engine-Secret: your-secret-here"
156
- ```
157
-
158
- **Response:**
159
- ```json
160
- {
161
- "thread_id": "019a1caa-bceb-7731-b607-e9d22c1933a5",
162
- "events": [
163
- {
164
- "timestamp": "2025-10-25T18:39:02.919Z",
165
- "type": "session_meta",
166
- "payload": {...}
167
- },
168
- {
169
- "timestamp": "2025-10-25T18:52:11.923Z",
170
- "type": "response_item",
171
- "payload": {"type": "message", "role": "user", ...}
172
- }
173
- ]
174
- }
175
- ```
176
-
177
- ### Get Thread Status
178
-
179
- **GET /codex/status**
180
-
181
- Get information about the current Codex thread.
182
-
183
- ```bash
184
- curl http://localhost:3737/codex/status \
185
- -H "X-Replicas-Engine-Secret: your-secret-here"
186
- ```
187
-
188
- **Response:**
189
- ```json
190
- {
191
- "has_active_thread": true,
192
- "thread_id": "019a1caa-bceb-7731-b607-e9d22c1933a5",
193
- "session_file": "/home/ubuntu/.codex/sessions/2025/10/25/rollout-2025-10-25T18:39:02.891Z-019a1caa.jsonl",
194
- "working_directory": "/home/ubuntu"
195
- }
196
- ```
197
-
198
- ### Reset Thread
199
-
200
- **POST /codex/reset**
201
-
202
- Clear the current thread and start a fresh conversation.
203
-
204
- ```bash
205
- curl -X POST http://localhost:3737/codex/reset \
206
- -H "X-Replicas-Engine-Secret: your-secret-here"
207
- ```
208
-
209
- **Response:**
210
- ```json
211
- {
212
- "message": "Thread reset successfully",
213
- "success": true
214
- }
215
- ```
216
-
217
- ### Codex Usage Notes
218
-
219
- - **Single Session**: Currently supports one active thread per workspace
220
- - **Thread Continuity**: Subsequent calls to `/codex/send` continue the same conversation
221
- - **Persistence**: Threads are automatically saved to `~/.codex/sessions/`
222
- - **Working Directory**: Codex runs in `WORKSPACE_HOME` (default: `/home/ubuntu`)
223
- - **Git Repository**: Git check is skipped to allow running in any directory
224
-
225
- ---
226
-
227
- ## Authentication
228
-
229
- All endpoints (except `/health`) require the `X-Replicas-Engine-Secret` header:
230
-
231
- ```
232
- X-Replicas-Engine-Secret: <workspace-engine-secret>
233
- ```
234
-
235
- Requests without this header or with an invalid secret will receive a 401 Unauthorized response.
236
-
237
- ## Development
238
-
239
- ```bash
240
- # Install dependencies
241
- yarn install
242
-
243
- # Run in development mode
244
- yarn dev
245
-
246
- # Build for production
247
- yarn build
248
-
249
- # Run built version
250
- yarn start
251
- ```
252
-
253
- ## Security
254
-
255
- - The engine only listens on port 3737
256
- - All requests require a secret key
257
- - The secret is generated uniquely per workspace
258
- - The service runs as the workspace user (no root access)
259
-
260
- ## Troubleshooting
261
-
262
- ### Engine not responding
263
-
264
- 1. Check if the service is running:
265
- ```bash
266
- systemctl status replicas-engine
267
- ```
268
-
269
- 2. Check the logs:
270
- ```bash
271
- journalctl -u replicas-engine -n 50
272
- ```
273
-
274
- 3. Verify the port is listening:
275
- ```bash
276
- sudo netstat -tlnp | grep 3737
277
- ```
278
-
279
- ### Authentication errors
280
-
281
- - Ensure you're using the correct `engine_secret` from the workspace record
282
- - Verify the `X-Replicas-Engine-Secret` header is being sent
283
-
284
- ## License
285
-
286
- MIT