Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
package-lock.json

# Configuration with sensitive data
config.json

# Logs
log/
*.log
5 changes: 5 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"apiKey": "your-openai-api-key-here",
"port": 4242,
"enableLogging": false
}
6 changes: 1 addition & 5 deletions decker-chatty.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ After=network.target
Type=simple
User=tramberend-admin
WorkingDirectory=/home/tramberend-admin/server/decker-chatty
ExecStart=/usr/bin/node server.mjs
ExecStart=/home/linuxbrew/.linuxbrew/bin/node server.mjs
Restart=on-failure
RestartSec=10
StandardOutput=append:/home/tramberend-admin/server/decker-chatty/chatty.log
StandardError=append:/home/tramberend-admin/server/decker-chatty/chatty.log

# Security settings
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
66 changes: 64 additions & 2 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,48 @@

import express from "express";
import cors from "cors";
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";

import config from "./config.json" with {type: "json"};

const app = express();
app.use(cors()); // relax or remove in production
// app.use(cors()); // relax or remove in production
app.use(express.json()); // parse JSON bodies

// Parse SSE stream to extract data events
function parseSSE(text) {
const events = [];
const lines = text.split('\n');
let currentEvent = {};

for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6); // Remove 'data: ' prefix
if (data === '[DONE]') {
events.push('[DONE]');
} else {
try {
currentEvent.data = JSON.parse(data);
events.push(currentEvent.data);
currentEvent = {};
} catch (e) {
// If JSON parse fails, store as raw string
currentEvent.data = data;
events.push(data);
currentEvent = {};
}
}
} else if (line.startsWith('event: ')) {
currentEvent.event = line.slice(7);
} else if (line.startsWith('id: ')) {
currentEvent.id = line.slice(4);
}
}

return events;
}

app.get("/health", (_req, res) => res.json({ ok: true }));

app.post("/chatty", async (req, res) => {
Expand Down Expand Up @@ -43,13 +78,40 @@ app.post("/chatty", async (req, res) => {
res.setHeader("Connection", "keep-alive");
res.flushHeaders?.();

for await (const chunk of upstream.body) res.write(chunk);
// Collect chunks for logging
const chunks = [];
for await (const chunk of upstream.body) {
chunks.push(chunk);
res.write(chunk);
}
res.end();

// Write log file if logging is enabled
if (config.enableLogging) {
const timestamp = new Date().toISOString();
const filename = timestamp.replace(/:/g, "-").replace(/\..+Z$/, "") + ".json";
const logPath = join("log", filename);
const rawText = Buffer.concat(chunks).toString("utf-8");
const events = parseSSE(rawText);
const completedEvent = events.find(e => e?.type === "response.completed");
const logData = {
timestamp,
request: req.body,
response: completedEvent.response.output.find((o) => o.type === "message"),
};
await writeFile(logPath, JSON.stringify(logData, null, 2));
console.log(`Logged to ${logPath}`);
}
} catch (err) {
console.error(err);
res.status(500).send("Proxy error");
}
});

// Create log directory on startup if logging is enabled
if (config.enableLogging) {
await mkdir("log", { recursive: true });
}

const port = process.env.PORT || config.port || 3000;
app.listen(port, () => console.log(`Proxy on http://localhost:${port}`));