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
19 changes: 19 additions & 0 deletions src/safety.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Very small safety filter for V1. Extend as needed.
const dangerousPatterns = [
/\bkill\b/i,
/\bweapon\b/i,
/\bhack(?:ing|er)?\b/i,
/\bpassword\b/i,
/\bcredit card\b/i,
/\bssn\b/i
];

function isDangerous(text) {
if (!text) return false;
for (const re of dangerousPatterns) {
if (re.test(text)) return true;
}
return false;
}

module.exports = { isDangerous };
168 changes: 50 additions & 118 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -1,127 +1,59 @@
import http from "node:http";
import { AGENTS, buildAgentPrompt } from "./agents.js";
import { addNovelty, rankSuggestions } from "./ranker.js";

const PORT = Number(process.env.PORT || 3000);
const MODEL = process.env.COMMANDER_MODEL || "gpt-5.6-luna";
const MAX_SUGGESTIONS = Math.min(Number(process.env.MAX_SUGGESTIONS || 5), AGENTS.length);
const CACHE_TTL_MS = Number(process.env.CACHE_TTL_MS || 60000);
const cache = new Map();

function json(res, status, body) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "POST, OPTIONS",
"access-control-allow-headers": "content-type"
});
res.end(JSON.stringify(body));
}

function readBody(req) {
return new Promise((resolve, reject) => {
let raw = "";
req.on("data", chunk => {
raw += chunk;
if (raw.length > 100_000) req.destroy();
});
req.on("end", () => {
try { resolve(JSON.parse(raw || "{}")); }
catch { reject(new Error("Invalid JSON")); }
});
req.on("error", reject);
});
}

function extractJson(text) {
const cleaned = String(text || "").replace(/^```json\s*/i, "").replace(/```$/i, "").trim();
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start < 0 || end < start) throw new Error("Model did not return JSON");
return JSON.parse(cleaned.slice(start, end + 1));
}

async function callModel(agent, promptPartial, context) {
if (!process.env.OPENAI_API_KEY) {
return {
text: `[mock:${agent.id}] Improve this prompt: ${promptPartial}`,
score: 0.5
};
}

const response = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"content-type": "application/json"
},
body: JSON.stringify({
model: MODEL,
input: buildAgentPrompt(agent, promptPartial, context),
text: { format: { type: "json_object" } }
})
});

if (!response.ok) {
const message = await response.text();
throw new Error(`Model request failed (${response.status}): ${message.slice(0, 300)}`);
}

const data = await response.json();
const outputText = data.output_text || data.output?.flatMap(x => x.content || []).find(x => x.text)?.text;
const parsed = extractJson(outputText);
return { text: parsed.text, score: Number(parsed.score) };
}

async function suggest({ promptPartial, context = "", max_suggestions = MAX_SUGGESTIONS }) {
const key = JSON.stringify({ promptPartial, context, max_suggestions });
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) return cached.value;

const agents = AGENTS.slice(0, Math.max(1, Math.min(Number(max_suggestions), AGENTS.length)));
const results = await Promise.allSettled(
agents.map(async agent => {
const result = await callModel(agent, promptPartial, context);
return {
id: `${agent.id}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
text: result.text,
score: result.score,
type: agent.type,
agent: agent.id
};
})
);
const express = require('express');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep server module syntax compatible with the package

Because package.json still declares "type": "module", running the configured npm start evaluates this file as an ES module and immediately throws ReferenceError: require is not defined at this line, before the server can bind a port. Convert this file and its exports to ESM or explicitly use CommonJS files/configuration so every API endpoint remains reachable.

Useful? React with 👍 / 👎.

const path = require('path');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
Comment on lines +1 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Declare the new runtime dependencies

A fresh checkout followed by npm install will not install any of these packages: the unchanged manifest has no dependencies section or lockfile, while this server now requires Express and its middleware at startup. Once the module-format issue is corrected, startup will instead fail with MODULE_NOT_FOUND unless these production dependencies are added to the manifest.

Useful? React with 👍 / 👎.

const { orchestrate } = require('./orchestrator');
const { combineSelected } = require('./combine');
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the orchestrator and combiner modules

The target commit contains no src/orchestrator.js or src/combine.js (confirmed from its tracked file tree), so these local imports cannot resolve. Even with the module format and npm dependencies fixed, Node will fail while loading the server rather than serving either /api/suggest or /api/combine; include the implementations or retain the existing request handlers.

Useful? React with 👍 / 👎.


const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '64kb' }));

const limiter = rateLimit({ windowMs: 60_000, max: 120 });
app.use(limiter);

// serve static frontend
app.use(express.static(path.join(__dirname, '..', 'public')));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the static frontend assets

The target commit tracks no public/ directory or frontend files, so this middleware has nothing to serve: a browser request for / falls through and receives Express's 404 response. Add the referenced static assets (or remove this route until they exist) so the newly introduced frontend entry point is usable.

Useful? React with 👍 / 👎.


app.get('/api/health', (req, res) => {
res.json({ status: 'ok', version: 'v1' });
});

const suggestions = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
app.post('/api/suggest', async (req, res) => {
try {
const body = req.body || {};
const { promptPartial, cursorIndex = null, context = {}, max_suggestions = 5, all = false } = body;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect MAX_SUGGESTIONS when no request override is sent

When a deployment sets the documented MAX_SUGGESTIONS environment variable and a caller omits max_suggestions, this destructuring default always passes 5 to orchestrate, bypassing the setting. The previous handler used the environment-derived default, so restore that behavior to keep configured fan-out and model cost effective.

Useful? React with 👍 / 👎.


const ranked = rankSuggestions(addNovelty(suggestions, promptPartial), Number(max_suggestions));
const value = { suggestions: ranked, model: MODEL, cached: false };
cache.set(key, { value, expires: Date.now() + CACHE_TTL_MS });
return value;
}
if (!promptPartial || typeof promptPartial !== 'string') return res.status(400).json({ error: 'promptPartial (string) required' });
if (max_suggestions <= 0) return res.status(400).json({ error: 'max_suggestions must be positive' });
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the prompt-length guard before orchestration

Requests with prompt text between the former 20,000-character limit and the 64 KB JSON-body limit now pass this validation and are forwarded to generation instead of receiving a 400 response. In deployments with model generation enabled, those oversized requests can multiply token cost and latency across the orchestrated agents; retain an explicit prompt-length limit here.

Useful? React with 👍 / 👎.


const server = http.createServer(async (req, res) => {
if (req.method === "OPTIONS") return json(res, 204, {});
if (req.method !== "POST" || req.url !== "/api/suggest") {
return json(res, 404, { error: "Not found" });
const suggestions = await orchestrate({ promptPartial, cursorIndex, context, max_suggestions, all });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke the safety filter before generating suggestions

The newly added isDangerous function is never imported or called anywhere in the target commit (verified with a repository-wide search), and this route forwards promptPartial directly to orchestration. Consequently, inputs matching the filter's dangerous patterns are generated normally, so the added safety filter has no effect until it is applied in the request and/or output path.

Useful? React with 👍 / 👎.

res.json({ suggestions });
} catch (err) {
console.error('api/suggest error', err && err.stack || err);
res.status(500).json({ error: 'generation_failed' });
}
});

app.post('/api/combine', async (req, res) => {
try {
const body = await readBody(req);
const promptPartial = String(body.promptPartial || "").trim();
if (!promptPartial) return json(res, 400, { error: "promptPartial is required" });
if (promptPartial.length > 20_000) return json(res, 400, { error: "promptPartial is too long" });

const result = await suggest(body);
return json(res, 200, result);
} catch (error) {
console.error(error);
return json(res, 500, { error: error.message || "Internal server error" });
const body = req.body || {};
const { selected = [], promptPartial = '', context = {} } = body;
if (!Array.isArray(selected) || selected.length === 0) return res.status(400).json({ error: 'selected array required' });

const combined = await combineSelected({ selected, promptPartial, context });
res.json({ combined });
} catch (err) {
console.error('api/combine error', err && err.stack || err);
res.status(500).json({ error: 'combine_failed' });
}
});

server.listen(PORT, () => {
console.log(`Commander listening on http://localhost:${PORT}`);
});
if (require.main === module) {
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Commander v1 listening on http://localhost:${port}`));
}

module.exports = app;
15 changes: 15 additions & 0 deletions tests/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const request = require('supertest');
const app = require('../src/server');

test('GET /api/health returns ok', async () => {
const res = await request(app).get('/api/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});

test('POST /api/suggest returns suggestions in mock mode', async () => {
const res = await request(app).post('/api/suggest').send({ promptPartial: 'I want to know about' });
expect(res.status).toBe(200);
expect(Array.isArray(res.body.suggestions)).toBe(true);
expect(res.body.suggestions.length).toBeGreaterThan(0);
});