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:
81
src/cli/commands/config.ts
Normal file
81
src/cli/commands/config.ts
Normal 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`));
|
||||
});
|
||||
85
src/cli/commands/context.ts
Normal file
85
src/cli/commands/context.ts
Normal 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>');
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user