Compare commits

..

No commits in common. "22f3c7dda608cd30cc4a17bd8df2a2fce05ffb5d" and "c4fbbbd865938a1f5802394515bd7873964c0f9f" have entirely different histories.

15 changed files with 3 additions and 2445 deletions

View File

@ -1,28 +0,0 @@
# AgentHub
Local coordination layer for AI coding agents.
## Install
```bash
npx agenthub init
```
## Quick Start
```bash
agenthub init
agenthub task create --title "Implement DNS cache" --role implementer
agenthub handoff create --fromRole architect --toRole implementer
agenthub status --update
```
## Supported Agents
- Claude Code
- Codex CLI
- Kimi Code CLI
## License
MIT

View File

@ -3,9 +3,6 @@
"version": "0.1.0",
"description": "Local coordination layer for AI coding agents",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "bin"],
"bin": {
"agenthub": "./bin/agenthub.js"
},
@ -33,8 +30,5 @@
"vitest": "^3.0.0"
},
"keywords": ["ai", "agents", "claude", "codex", "kimi", "collaboration"],
"license": "MIT",
"pnpm": {
"onlyBuiltDependencies": ["better-sqlite3"]
}
"license": "MIT"
}

2030
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
import { input } from '@inquirer/prompts';
import { join } from 'path';
import { getEntityDir } from '../../core/paths.js';
import { getNextId } from '../../core/counter.js';
import { readEntity, writeEntity } from '../../core/files.js';
import { DecisionSchema, type Decision } from '../../core/schema.js';
import { Index } from '../../core/index.js';
export async function decisionCreate(cwd: string, options: Partial<Decision> = {}): Promise<void> {
const title = options.title ?? await input({ message: 'Decision title:' });
const context = options.context ?? await input({ message: 'Context:' });
const decision = options.decision ?? await input({ message: 'Decision:' });
const now = new Date().toISOString();
const record: Decision = DecisionSchema.parse({
id: getNextId(cwd, 'decision'),
title,
context,
decision,
createdAt: now,
updatedAt: now,
});
const filePath = join(getEntityDir(cwd, 'decisions'), `${record.id}.md`);
writeEntity(filePath, record, `# ${record.title}\n\n## Decision\n\n${record.decision}\n\n## Context\n\n${record.context}`);
const index = new Index(cwd);
index.upsert({
id: record.id,
type: 'decision',
title: record.title,
content: `${record.context} ${record.decision}`,
filePath,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
});
index.close();
console.log(`Decision recorded: ${record.id}`);
}
export function decisionList(cwd: string): void {
const index = new Index(cwd);
const decisions = index.list('decision');
index.close();
if (decisions.length === 0) {
console.log('No decisions found.');
return;
}
for (const d of decisions) {
console.log(`${d.id}: ${d.title}`);
}
}

View File

@ -1,39 +0,0 @@
import { loadConfig } from '../../core/config.js';
import { Index } from '../../core/index.js';
import { handoffCreate } from './handoff.js';
export async function delegate(cwd: string, options: { auto?: boolean } = {}): Promise<void> {
const config = loadConfig(cwd);
const index = new Index(cwd);
const openTasks = index.list('task', { status: 'open' });
index.close();
if (openTasks.length === 0) {
console.log('No open tasks to delegate.');
return;
}
const task = openTasks[0];
const role = task.role ?? 'implementer';
const preferredAgent = config.roles[role]?.preferredAgent ?? 'codex';
console.log('Suggested delegation:');
console.log(` Task: ${task.id}${task.title}`);
console.log(` Role: ${role}`);
console.log(` Preferred agent: ${preferredAgent}`);
if (config.delegationMode === 'auto' || options.auto) {
await handoffCreate(cwd, {
fromRole: 'user',
toRole: role,
toAgent: preferredAgent,
taskId: task.id,
summary: `Delegate ${task.id} to ${role}`,
context: `Task "${task.title}" should be handled by ${preferredAgent} in ${role} role.`,
});
console.log('Handoff created automatically.');
} else {
console.log('Run with --auto to create the handoff, or run:');
console.log(` agenthub handoff create --taskId ${task.id}`);
}
}

View File

@ -1,71 +0,0 @@
import { input, select } from '@inquirer/prompts';
import { join } from 'path';
import { getEntityDir } from '../../core/paths.js';
import { getNextId } from '../../core/counter.js';
import { readEntity, writeEntity } from '../../core/files.js';
import { HandoffSchema, type Handoff } from '../../core/schema.js';
import { Index } from '../../core/index.js';
const roles = ['architect', 'implementer', 'reviewer', 'tester'];
export async function handoffCreate(cwd: string, options: Partial<Handoff> = {}): Promise<void> {
const fromRole = options.fromRole ?? await select({ message: 'From role:', choices: roles.map((r) => ({ name: r, value: r })) });
const toRole = options.toRole ?? await select({ message: 'To role:', choices: roles.map((r) => ({ name: r, value: r })) });
const taskId = options.taskId ?? await input({ message: 'Related task id (optional):' });
const summary = options.summary ?? await input({ message: 'Summary:' });
const context = options.context ?? await input({ message: 'Context:' });
const now = new Date().toISOString();
const handoff: Handoff = HandoffSchema.parse({
id: getNextId(cwd, 'handoff'),
fromRole,
toRole,
fromAgent: options.fromAgent,
toAgent: options.toAgent,
taskId: taskId || undefined,
summary,
context,
createdAt: now,
});
const filePath = join(getEntityDir(cwd, 'handoffs'), `${handoff.id}.md`);
writeEntity(filePath, handoff, `# ${handoff.summary}\n\n${handoff.context}`);
const index = new Index(cwd);
index.upsert({
id: handoff.id,
type: 'handoff',
title: handoff.summary,
content: handoff.context,
filePath,
createdAt: handoff.createdAt,
updatedAt: handoff.createdAt,
});
index.close();
console.log(`Handoff created: ${handoff.id}`);
}
export function handoffRead(cwd: string, id: string): void {
const filePath = join(getEntityDir(cwd, 'handoffs'), `${id}.md`);
const { frontmatter, body } = readEntity(filePath);
console.log(`# ${frontmatter.summary}`);
console.log(`From: ${frontmatter.fromRole}${frontmatter.toRole}`);
if (frontmatter.taskId) console.log(`Task: ${frontmatter.taskId}`);
console.log('\n' + body);
}
export function handoffList(cwd: string): void {
const index = new Index(cwd);
const handoffs = index.list('handoff');
index.close();
if (handoffs.length === 0) {
console.log('No handoffs found.');
return;
}
for (const h of handoffs) {
console.log(`${h.id}: ${h.title}`);
}
}

View File

@ -1,7 +1,7 @@
import { input, select } from '@inquirer/prompts';
import { join } from 'path';
import { getEntityDir } from '../../core/paths.js';
import { writeEntity } from '../../core/files.js';
import { readEntity, writeEntity, listEntities } from '../../core/files.js';
import { MemorySchema, type Memory } from '../../core/schema.js';
import { Index } from '../../core/index.js';

View File

@ -1,5 +0,0 @@
import { startServer } from '../../server/index.js';
export async function serverStart(cwd: string, options: { port?: number } = {}): Promise<void> {
await startServer(cwd, options.port ?? 3377);
}

View File

@ -1,14 +0,0 @@
import { existsSync } from 'fs';
import { generateStatus } from '../../core/status.js';
import { readEntity } from '../../core/files.js';
import { getStatusPath } from '../../core/paths.js';
export function status(cwd: string, options: { update?: boolean } = {}): void {
const statusPath = getStatusPath(cwd);
if (options.update || !existsSync(statusPath)) {
generateStatus(cwd);
}
const { body } = readEntity(statusPath);
console.log(body);
}

View File

@ -2,7 +2,7 @@ import { input, select } from '@inquirer/prompts';
import { join } from 'path';
import { getEntityDir } from '../../core/paths.js';
import { getNextId } from '../../core/counter.js';
import { readEntity, writeEntity } from '../../core/files.js';
import { readEntity, writeEntity, listEntities } from '../../core/files.js';
import { TaskSchema, type Task } from '../../core/schema.js';
import { Index } from '../../core/index.js';

View File

@ -1,125 +0,0 @@
import { Command } from 'commander';
import { init } from './commands/init.js';
import { status } from './commands/status.js';
import { memoryAdd, memorySearch, memoryList } from './commands/memory.js';
import { taskCreate, taskList, taskShow, taskClaim, taskDone } from './commands/task.js';
import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js';
import { decisionCreate, decisionList } from './commands/decision.js';
import { delegate } from './commands/delegate.js';
import { serverStart } from './commands/server.js';
export function createProgram(cwd: string): Command {
const program = new Command('agenthub')
.description('Local coordination layer for AI coding agents')
.version('0.1.0');
program
.command('init')
.description('Initialize AgentHub in the current directory')
.option('-n, --project-name <name>', 'Project name')
.option('-y, --yes', 'Use defaults without prompts')
.action((options) => init(cwd, options));
program
.command('status')
.description('Show project status')
.option('-u, --update', 'Regenerate status before showing')
.action((options) => status(cwd, options));
const memoryCmd = new Command('memory').description('Manage memory entries');
memoryCmd
.command('add')
.description('Add a memory entry')
.option('--title <title>', 'Title')
.option('--category <category>', 'Category')
.option('--content <content>', 'Content')
.action((options) => memoryAdd(cwd, options));
memoryCmd
.command('search <query>')
.description('Search memory and tasks')
.action((query) => memorySearch(cwd, query));
memoryCmd
.command('list')
.description('List memory entries')
.action(() => memoryList(cwd));
program.addCommand(memoryCmd);
const taskCmd = new Command('task').description('Manage tasks');
taskCmd
.command('create')
.description('Create a task')
.option('--title <title>', 'Title')
.option('--role <role>', 'Role')
.option('--priority <priority>', 'Priority')
.action((options) => taskCreate(cwd, options));
taskCmd
.command('list')
.description('List tasks')
.option('--status <status>', 'Filter by status')
.option('--role <role>', 'Filter by role')
.action((options) => taskList(cwd, options));
taskCmd
.command('show <id>')
.description('Show a task')
.action((id) => taskShow(cwd, id));
taskCmd
.command('claim <id>')
.description('Claim a task')
.requiredOption('--agent <agent>', 'Agent name')
.action((id, options) => taskClaim(cwd, id, options.agent));
taskCmd
.command('done <id>')
.description('Mark task as done')
.action((id) => taskDone(cwd, id));
program.addCommand(taskCmd);
const handoffCmd = new Command('handoff').description('Manage handoffs');
handoffCmd
.command('create')
.description('Create a handoff')
.option('--fromRole <role>', 'From role')
.option('--toRole <role>', 'To role')
.option('--taskId <id>', 'Related task id')
.option('--summary <summary>', 'Summary')
.option('--context <context>', 'Context')
.action((options) => handoffCreate(cwd, options));
handoffCmd
.command('read <id>')
.description('Read a handoff')
.action((id) => handoffRead(cwd, id));
handoffCmd
.command('list')
.description('List handoffs')
.action(() => handoffList(cwd));
program.addCommand(handoffCmd);
const decisionCmd = new Command('decision').description('Manage decisions');
decisionCmd
.command('create')
.description('Create a decision record')
.option('--title <title>', 'Title')
.option('--context <context>', 'Context')
.option('--decision <decision>', 'Decision')
.action((options) => decisionCreate(cwd, options));
decisionCmd
.command('list')
.description('List decisions')
.action(() => decisionList(cwd));
program.addCommand(decisionCmd);
program
.command('delegate')
.description('Suggest or auto-delegate open tasks')
.option('--auto', 'Create handoff automatically')
.action((options) => delegate(cwd, options));
const serverCmd = new Command('server').description('Optional local API server');
serverCmd
.command('start')
.description('Start the optional AgentHub API server')
.option('-p, --port <port>', 'Port', '3377')
.action((options) => serverStart(cwd, { port: parseInt(options.port, 10) }));
program.addCommand(serverCmd);
return program;
}

View File

@ -1,4 +0,0 @@
import { createProgram } from './cli/index.js';
const program = createProgram(process.cwd());
program.parse();

View File

@ -1,15 +0,0 @@
import Fastify from 'fastify';
import { registerRoutes } from './routes.js';
export async function startServer(cwd: string, port = 3377): Promise<void> {
const app = Fastify({ logger: false });
await registerRoutes(app, cwd);
try {
await app.listen({ port, host: '127.0.0.1' });
console.log(`AgentHub server listening on http://127.0.0.1:${port}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
}

View File

@ -1,27 +0,0 @@
import { FastifyInstance } from 'fastify';
import { Index } from '../core/index.js';
export async function registerRoutes(app: FastifyInstance, cwd: string): Promise<void> {
app.get('/status', async () => {
const index = new Index(cwd);
const open = index.list('task', { status: 'open' });
const inProgress = index.list('task', { status: 'in_progress' });
index.close();
return { open: open.length, inProgress: inProgress.length };
});
app.get('/tasks', async () => {
const index = new Index(cwd);
const tasks = index.list('task');
index.close();
return tasks;
});
app.get('/search', async (request) => {
const { q } = request.query as { q: string };
const index = new Index(cwd);
const results = index.search(q);
index.close();
return results;
});
}

View File

@ -1,23 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { execSync } from 'child_process';
describe('e2e', () => {
let cwd: string;
beforeEach(() => {
cwd = mkdtempSync(join(tmpdir(), 'agenthub-e2e-'));
});
afterEach(() => {
rmSync(cwd, { recursive: true, force: true });
});
it('initializes a project', () => {
const bin = join(process.cwd(), 'bin', 'agenthub.js');
execSync(`node ${bin} init --yes --project-name e2e-test`, { cwd });
expect(existsSync(join(cwd, '.agenthub', 'agenthub.config.json'))).toBe(true);
});
});