Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Context Engine — Environment Variables
# Copy to .env and customize. (.env is git-ignored.)

# ── Server ──
CE_PORT=3847 # HTTP server port (also respects PORT)
CE_ROOT= # Project root (defaults to repo root)
CE_API_KEY= # Bearer token auth (empty = no auth required)

# ── Embeddings (Ollama) ──
OLLAMA_URL=http://127.0.0.1:11434
CE_EMBED_MODEL=nomic-embed-text

# ── MCP HTTP Server ──
MCP_HTTP_HOST=127.0.0.1
MCP_HTTP_PORT=3850
MCP_HTTP_TOKEN= # Static bearer token for MCP HTTP (empty = no auth)
MCP_OAUTH_PASSWORD= # OAuth password (takes precedence over MCP_HTTP_TOKEN)
MCP_PUBLIC_URL= # Public URL for OAuth redirect (auto-detected if empty)

# ── MCP Tools (stdio bridge) ──
CE_HOST=127.0.0.1 # Host when CE runs remotely

# ── Electron Desktop ──
CE_HOT_RELOAD= # Set to "1" to watch UI files for changes
CE_NEW_USER_PROFILE= # Set to "1" for first-run / new-user mode

# ── Benchmark ──
CE_URL=http://127.0.0.1:3847 # Base URL for bench/tokenomics.py
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ jobs:
npm run smoke
npm run test:handoffs
npm run test:projects
npm run test:compiler
npm run test:skills
npm run test:rule-files
npm run test:config
npm run test:http
npm run test:api-endpoints
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run checks and tests
run: |
npm run check
npm run smoke

- name: Build
env:
# GH_TOKEN drives both release creation and asset upload via
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ data/onboarding.json
data/dedup.json
data/workspaces.json
data/modes.json
data/memory.json
data/projects.json
data/rules.json
data/skill-states.json
*.log
.DS_Store
Thumbs.db
Expand Down
11 changes: 10 additions & 1 deletion bench/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# Tokenomics benchmark
# Bench

Benchmarking and measurement tools for Context Engine.

| File | Purpose |
| --------------- | -------------------------------------- |
| `tokenomics.py` | Token-efficiency benchmark (see below) |
| `tasks.json` | Task corpus used by the benchmark |

## Tokenomics benchmark

One Python script. Run it; it prints a table. That's the whole tool.

Expand Down
Empty file added data/.gitkeep
Empty file.
4 changes: 0 additions & 4 deletions data/memory.json

This file was deleted.

4 changes: 0 additions & 4 deletions data/projects.json

This file was deleted.

5 changes: 0 additions & 5 deletions data/rules.json

This file was deleted.

7 changes: 0 additions & 7 deletions data/skill-states.json

This file was deleted.

4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import tseslint from 'typescript-eslint';
// files only get the recommended-JS rules.
const TYPECHECKED_JS = [
'server/server.js',
'server/router.js',
'server/compiler.js',
'server/lib/config.js',
'server/lib/chunker.js',
'server/lib/embeddings.js',
Expand Down Expand Up @@ -35,8 +37,6 @@ export default [
'!ui/store.js',
'!ui/compile.js',
'!ui/dashboard.js',
'server/compiler.js',
'server/router.js',
'server/lib/app-version.js',
'server/lib/backup.js',
'server/lib/crypto.js',
Expand Down
65 changes: 32 additions & 33 deletions scripts/http-smoke.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
const assert = require('assert');
const http = require('http');

const { cors, body, json } = require('../server/lib/http');
const { PORT } = require('../server/lib/config');

// ---- cors ----
function mockRes() {
const r = { _headers: {} };
r.setHeader = (name, value) => {
r._headers[name] = value;
};
return r;
}

// GIVEN a request from the allowed localhost origin
const allowedReq = new http.IncomingMessage(new (require('net').Socket)());
allowedReq.headers = { origin: 'http://localhost:3847' };
const allowedRes = new http.ServerResponse(allowedReq);
allowedRes.setHeader = (name, value) => {
allowedRes._headers = allowedRes._headers || {};
allowedRes._headers[name] = value;
};
const allowedReq = { headers: { origin: `http://localhost:${PORT}` } };
const allowedRes = mockRes();
cors(allowedReq, allowedRes);
assert.strictEqual(
allowedRes._headers['Access-Control-Allow-Origin'],
'http://localhost:3847',
`http://localhost:${PORT}`,
'cors sets origin header for localhost',
);
assert.strictEqual(
Expand All @@ -30,61 +33,57 @@ assert.strictEqual(
);

// GIVEN a request from 127.0.0.1 origin
const loopbackReq = new http.IncomingMessage(new (require('net').Socket)());
loopbackReq.headers = { origin: 'http://127.0.0.1:3847' };
const loopbackRes = new http.ServerResponse(loopbackReq);
loopbackRes.setHeader = (name, value) => {
loopbackRes._headers = loopbackRes._headers || {};
loopbackRes._headers[name] = value;
};
const loopbackReq = { headers: { origin: `http://127.0.0.1:${PORT}` } };
const loopbackRes = mockRes();
cors(loopbackReq, loopbackRes);
assert.strictEqual(
loopbackRes._headers['Access-Control-Allow-Origin'],
'http://127.0.0.1:3847',
`http://127.0.0.1:${PORT}`,
'cors sets origin for 127.0.0.1',
);

// GIVEN a request from a disallowed origin
const badReq = new http.IncomingMessage(new (require('net').Socket)());
badReq.headers = { origin: 'http://evil.example.com' };
const badRes = new http.ServerResponse(badReq);
badRes.setHeader = () => {};
const badReq = { headers: { origin: 'http://evil.example.com' } };
const badRes = mockRes();
cors(badReq, badRes);
assert.strictEqual(
badRes._headers?.Access || badRes._headers?.['access-control-allow-origin'],
badRes._headers['Access-Control-Allow-Origin'],
undefined,
'cors does NOT set origin for disallowed host',
);

// ---- json ----
// GIVEN a response object
const jsonReq = new http.IncomingMessage(new (require('net').Socket)());
const jsonRes = new http.ServerResponse(jsonReq);
let writtenHead = null;
let writtenBody = '';
jsonRes.writeHead = (status, headers) => {
writtenHead = { status, headers };
};
jsonRes.end = (data) => {
writtenBody = data;
const jsonRes = {
writeHead: (status, headers) => {
writtenHead = { status, headers };
},
end: (data) => {
writtenBody = data;
},
};
json(jsonRes, { foo: 'bar' });
assert.strictEqual(writtenHead?.status, 200, 'json defaults to status 200');
assert.strictEqual(writtenHead?.headers['Content-Type'], 'application/json', 'json sets Content-Type');
assert.strictEqual(writtenBody, '{"foo":"bar"}', 'json stringifies body');

// GIVEN a custom status code
const statusRes = new http.ServerResponse(new http.IncomingMessage(new (require('net').Socket)()));
let statusHead = null;
statusRes.writeHead = (status, headers) => {
statusHead = { status, headers };
const statusRes = {
writeHead: (status, headers) => {
statusHead = { status, headers };
},
end: () => {},
};
statusRes.end = () => {};
json(statusRes, { err: 'nope' }, 404);
assert.strictEqual(statusHead?.status, 404, 'json uses custom status code');

void (async () => {
// ---- body (JSON parse) ----
const http = require('http');

// GIVEN a request with valid JSON body
const bodyReq = new http.IncomingMessage(new (require('net').Socket)());
bodyReq.headers = { 'content-type': 'application/json' };
Expand Down
33 changes: 17 additions & 16 deletions server/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ ${flattenSection(ctx.rules.soul, ['soft'])}`);
for (const [skillId, mrc] of Object.entries(ctx.mrContext)) {
if (!selectedIds.has(skillId)) continue;
if (!mrc.chunks.length) continue;
const chunkParts = mrc.chunks.slice(0, 3).map((chunk, i) => `### ${chunk.section}\n${chunk.text}`);
const chunkParts = mrc.chunks.slice(0, 3).map((chunk) => `### ${chunk.section}\n${chunk.text}`);
sections.push(`## ${skillId} — Relevant knowledge\n${chunkParts.join('\n\n')}`);
}
}
Expand Down Expand Up @@ -429,7 +429,6 @@ function buildContext(opts) {
: allSkills.filter((s) => stateMap[s.id] !== false);

// Read skill file content and compute relative paths for output formats
const skillsDir = opts.skillsDir || path.join(dataDir, '..', 'skills');
const rootDir = opts.skillsDir ? path.dirname(opts.skillsDir) : path.join(dataDir, '..');
activeSkills.forEach((s) => {
try {
Expand All @@ -440,10 +439,13 @@ function buildContext(opts) {
s.relativePath = path.relative(rootDir, s.path).replace(/\\/g, '/');
});

const normalizedRules = rules ? normalizeRules(rules) : null;
const finalRules = opts.rulesOverride ? normalizeRules(opts.rulesOverride) : normalizedRules;

return {
memory,
rules: rules ? normalizeRules(rules) : null,
sessionStart: rules?.sessionStart || '',
rules: finalRules,
sessionStart: (opts.rulesOverride && opts.rulesOverride.sessionStart) || rules?.sessionStart || '',
activeSkills,
totalSkills: allSkills.length,
mrContext: opts.mrContext || null,
Expand All @@ -461,28 +463,28 @@ function buildContext(opts) {
* @returns {object}
*/
function normalizeRules(rules) {
const codingPriorities = ['hard', 'soft'];
const generalPriorities = ['hard', 'soft'];
const soulPriorities = ['soft'];
const codingPriorities = ['hard', 'soft', 'style'];
const generalPriorities = ['hard', 'soft', 'style'];
const soulPriorities = ['soft', 'style'];

const coding =
typeof rules.coding === 'string'
? { soft: rules.coding }
? { ...Object.fromEntries(codingPriorities.map((p) => [p, ''])), soft: rules.coding }
: typeof rules.coding === 'object' && rules.coding !== null
? pickPriorities(rules.coding, codingPriorities)
: {};
: Object.fromEntries(codingPriorities.map((p) => [p, '']));
const general =
typeof rules.general === 'string'
? { soft: rules.general }
? { ...Object.fromEntries(generalPriorities.map((p) => [p, ''])), soft: rules.general }
: typeof rules.general === 'object' && rules.general !== null
? pickPriorities(rules.general, generalPriorities)
: {};
: Object.fromEntries(generalPriorities.map((p) => [p, '']));
const soul =
typeof rules.soul === 'string'
? { soft: rules.soul }
? { ...Object.fromEntries(soulPriorities.map((p) => [p, ''])), soft: rules.soul }
: typeof rules.soul === 'object' && rules.soul !== null
? pickPriorities(rules.soul, soulPriorities)
: {};
: Object.fromEntries(soulPriorities.map((p) => [p, '']));

return { coding, general, soul };
}
Expand Down Expand Up @@ -537,7 +539,7 @@ function flattenSectionLabeled(section, sectionLabel, priorities) {
function pickPriorities(obj, allowed) {
const out = {};
for (const key of allowed) {
if (typeof obj[key] === 'string') out[key] = obj[key];
out[key] = typeof obj[key] === 'string' ? obj[key] : '';
}
return out;
}
Expand Down Expand Up @@ -594,13 +596,12 @@ function compile(opts) {
*/
function estimateTokens(text) {
if (!text) return 0;
const words = text.split(/\s+/).filter(Boolean).length;
const codeBlocks = (text.match(/```[\s\S]*?```/g) || []).join('').length;
const proseChars = text.length - codeBlocks;
// Prose: ~1.3 tokens/word, Code: ~1.5 tokens/word (higher token density)
const proseWords = Math.round(proseChars / 5); // avg word length
const codeWords = Math.round(codeBlocks / 4);
const mdMarkers = (text.match(/[#|*\->`\[\](){}]/g) || []).length;
const mdMarkers = (text.match(/[#|*\->`\[\](){}]/g) || []).length; // eslint-disable-line no-useless-escape
return Math.round(proseWords * 1.3 + codeWords * 1.5 + mdMarkers * 0.5);
}

Expand Down
19 changes: 19 additions & 0 deletions server/lib/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ function appendSession(entry) {
fs.writeFileSync(SESSION_LOG, JSON.stringify(log, null, 2), 'utf8');
}

function ensureDefaultData() {
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
const defaults = {
'memory.json': { version: '1.1', entries: [] },
'projects.json': { version: '1.0', projects: [] },
'rules.json': {
coding: 'Modular code files.\nComment the why, not the what.',
general: 'Memory is a core skill. Think independently.',
soul: 'Helpful, concise, and logical.\nObjective and critical thinker.',
},
'skill-states.json': { version: '1.0', last_updated: new Date().toISOString().split('T')[0], states: {} },
};
for (const [file, content] of Object.entries(defaults)) {
const p = path.join(DATA_DIR, file);
if (!fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify(content, null, 2), 'utf8');
}
}

module.exports = {
readData,
writeData,
Expand All @@ -83,4 +101,5 @@ module.exports = {
restoreBackup,
getSessionLog,
appendSession,
ensureDefaultData,
};
Loading
Loading