Add context injection system (Milestone 3)

- Add src/core/context.ts with context gathering, ranking, and formatting
- Add src/core/config.ts for persistent configuration storage
- Add CLI commands: cortex context, cortex context-hook, cortex config
- Add memory_context MCP tool for Claude Code integration
- Add GET /api/context endpoint
- Include summary.ts from main branch for heartbeat dependency
This commit is contained in:
2026-02-03 10:02:28 +01:00
parent 999c748d3d
commit 516b5ec017
8 changed files with 814 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
import { Command } from 'commander';
import chalk from 'chalk';
import { getConfig, setConfig, listConfig, resetConfig, CortexConfig } from '../../core/config';
export const configCommand = new Command('config')
.description('Manage Cortex configuration');
configCommand
.command('get <key>')
.description('Get a config value')
.action((key: string) => {
try {
const value = getConfig(key as keyof CortexConfig);
console.log(value);
} catch (err) {
console.error(chalk.red(`Unknown config key: ${key}`));
process.exit(1);
}
});
configCommand
.command('set <key> <value>')
.description('Set a config value')
.action((key: string, value: string) => {
// Validate key exists
const validKeys = [
'context.maxTokens',
'context.maxNodes',
'context.includeRecent',
'context.includeProject',
'context.includeTasks',
'context.includeDecisions',
];
if (!validKeys.includes(key)) {
console.error(chalk.red(`Unknown config key: ${key}`));
console.error(chalk.dim(`Valid keys: ${validKeys.join(', ')}`));
process.exit(1);
}
// Parse value based on key type
let parsed: string | number | boolean = value;
if (key.startsWith('context.max')) {
parsed = parseInt(value, 10);
if (isNaN(parsed)) {
console.error(chalk.red(`Invalid number: ${value}`));
process.exit(1);
}
} else if (key.startsWith('context.include')) {
if (value !== 'true' && value !== 'false') {
console.error(chalk.red(`Invalid boolean: ${value} (use true or false)`));
process.exit(1);
}
parsed = value === 'true';
}
setConfig(key as keyof CortexConfig, parsed as any);
console.log(chalk.green(`Set ${key} = ${parsed}`));
});
configCommand
.command('list')
.description('List all config values')
.action(() => {
const configs = listConfig();
console.log(chalk.bold('Configuration:'));
console.log();
for (const { key, value, isDefault } of configs) {
const displayValue = isDefault ? chalk.dim(`${value} (default)`) : chalk.cyan(value);
console.log(` ${chalk.yellow(key)}: ${displayValue}`);
}
});
configCommand
.command('reset <key>')
.description('Reset a config key to default')
.action((key: string) => {
resetConfig(key as keyof CortexConfig);
console.log(chalk.green(`Reset ${key} to default`));
});

View File

@@ -0,0 +1,85 @@
import { Command } from 'commander';
import chalk from 'chalk';
import * as path from 'path';
import { gatherContext, DEFAULT_CONTEXT_CONFIG } from '../../core/context';
export const contextCommand = new Command('context')
.description('Preview or inject context for a Claude session')
.option('-p, --project <name>', 'Project name (defaults to current directory name)')
.option('-q, --query <text>', 'Additional semantic search query')
.option('--max-tokens <n>', 'Maximum tokens', String(DEFAULT_CONTEXT_CONFIG.maxTokens))
.option('--max-nodes <n>', 'Maximum nodes', String(DEFAULT_CONTEXT_CONFIG.maxNodes))
.option('--format <fmt>', 'Output format: markdown or json', 'markdown')
.option('--no-recent', 'Exclude recent activity')
.option('--no-tasks', 'Exclude open tasks')
.option('--no-decisions', 'Exclude decisions')
.action(async (opts) => {
const project = opts.project ?? path.basename(process.cwd());
const result = await gatherContext({
project,
semanticQuery: opts.query,
config: {
maxTokens: parseInt(opts.maxTokens),
maxNodes: parseInt(opts.maxNodes),
includeRecent: opts.recent !== false,
includeTasks: opts.tasks !== false,
includeDecisions: opts.decisions !== false,
},
});
if (opts.format === 'json') {
console.log(JSON.stringify({
project,
nodeCount: result.nodes.length,
nodes: result.nodes.map(r => ({
id: r.node.id,
kind: r.node.kind,
title: r.node.title,
score: r.score,
reason: r.reason,
})),
}, null, 2));
return;
}
// Markdown output
if (result.nodes.length === 0) {
console.log(chalk.yellow('No relevant context found.'));
return;
}
console.log(chalk.cyan(`# Context for ${project}`));
console.log(chalk.dim(`(${result.nodes.length} nodes)`));
console.log();
console.log(result.formatted);
});
/**
* Hook command for Claude Code integration
* Outputs context to stdout for injection
*/
export const contextHookCommand = new Command('context-hook')
.description('Hook handler for Claude Code session start (outputs context to stdout)')
.option('-p, --project <name>', 'Project name (defaults to current directory name)')
.option('--max-tokens <n>', 'Maximum tokens', String(DEFAULT_CONTEXT_CONFIG.maxTokens))
.action(async (opts) => {
const project = opts.project ?? path.basename(process.cwd());
const result = await gatherContext({
project,
config: {
maxTokens: parseInt(opts.maxTokens),
},
});
if (result.nodes.length === 0) {
// Output nothing if no context
return;
}
// Output markdown directly to stdout for Claude to consume
console.log(`<cortex-context project="${project}">`);
console.log(result.formatted);
console.log('</cortex-context>');
});

View File

@@ -11,6 +11,8 @@ import { graphCommand } from './commands/graph';
import { serveCommand } from './commands/serve';
import { decayCommand } from './commands/decay';
import { childrenCommand } from './commands/children';
import { contextCommand, contextHookCommand } from './commands/context';
import { configCommand } from './commands/config';
import { closeDb } from '../core/db';
const program = new Command();
@@ -31,6 +33,9 @@ program.addCommand(graphCommand);
program.addCommand(serveCommand);
program.addCommand(decayCommand);
program.addCommand(childrenCommand);
program.addCommand(contextCommand);
program.addCommand(contextHookCommand);
program.addCommand(configCommand);
program.hook('postAction', () => {
closeDb();