Compare commits
No commits in common. "22f3c7dda608cd30cc4a17bd8df2a2fce05ffb5d" and "c4fbbbd865938a1f5802394515bd7873964c0f9f" have entirely different histories.
22f3c7dda6
...
c4fbbbd865
28
README.md
28
README.md
@ -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
|
|
||||||
@ -3,9 +3,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Local coordination layer for AI coding agents",
|
"description": "Local coordination layer for AI coding agents",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"files": ["dist", "bin"],
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "./bin/agenthub.js"
|
"agenthub": "./bin/agenthub.js"
|
||||||
},
|
},
|
||||||
@ -33,8 +30,5 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"keywords": ["ai", "agents", "claude", "codex", "kimi", "collaboration"],
|
"keywords": ["ai", "agents", "claude", "codex", "kimi", "collaboration"],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": ["better-sqlite3"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2030
pnpm-lock.yaml
generated
2030
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { input, select } from '@inquirer/prompts';
|
import { input, select } from '@inquirer/prompts';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getEntityDir } from '../../core/paths.js';
|
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 { MemorySchema, type Memory } from '../../core/schema.js';
|
||||||
import { Index } from '../../core/index.js';
|
import { Index } from '../../core/index.js';
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ import { input, select } from '@inquirer/prompts';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getEntityDir } from '../../core/paths.js';
|
import { getEntityDir } from '../../core/paths.js';
|
||||||
import { getNextId } from '../../core/counter.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 { TaskSchema, type Task } from '../../core/schema.js';
|
||||||
import { Index } from '../../core/index.js';
|
import { Index } from '../../core/index.js';
|
||||||
|
|
||||||
|
|||||||
125
src/cli/index.ts
125
src/cli/index.ts
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { createProgram } from './cli/index.js';
|
|
||||||
|
|
||||||
const program = createProgram(process.cwd());
|
|
||||||
program.parse();
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user