feat(cli): store serverUrl in config during init; auto-connect LAN clients
This commit is contained in:
parent
c073f9a213
commit
3ebd33c4be
@ -35,17 +35,18 @@ agenthub init
|
|||||||
agenthub server start --host 0.0.0.0 --port 3377
|
agenthub server start --host 0.0.0.0 --port 3377
|
||||||
```
|
```
|
||||||
|
|
||||||
On another machine (e.g. Windows):
|
On another machine (e.g. Windows) run `init` once and point it at the host:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:AGENTHUB_SERVER="http://<mac-ip>:3377"
|
cd my-project
|
||||||
|
agenthub init --server http://<mac-ip>:3377
|
||||||
agenthub task create --title "Windows task" --role implementer
|
agenthub task create --title "Windows task" --role implementer
|
||||||
agenthub status
|
agenthub status
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--server http://<ip>:3377` on each command instead of the environment variable if you prefer.
|
After `init`, every command automatically talks to the configured server. You can still override it per command with `--server http://<ip>:3377` or via the `AGENTHUB_SERVER` environment variable.
|
||||||
|
|
||||||
`init` always runs locally on the host machine.
|
The `init --server` step only stores the server URL locally; it does not create a second project. The host machine keeps the single source of truth.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,12 @@ import { join } from 'path';
|
|||||||
import { getAgentHubDir } from '../../core/paths.js';
|
import { getAgentHubDir } from '../../core/paths.js';
|
||||||
import { saveConfig, defaultConfig } from '../../core/config.js';
|
import { saveConfig, defaultConfig } from '../../core/config.js';
|
||||||
import { agentsMd, claudeMd, codexMd, kimiMd } from '../../core/templates.js';
|
import { agentsMd, claudeMd, codexMd, kimiMd } from '../../core/templates.js';
|
||||||
import { askProjectName, askAgents, askDelegationMode } from '../prompts.js';
|
import { askProjectName, askAgents, askDelegationMode, askServerUrl } from '../prompts.js';
|
||||||
|
|
||||||
export async function init(cwd: string, options: { projectName?: string; yes?: boolean }): Promise<void> {
|
export async function init(
|
||||||
|
cwd: string,
|
||||||
|
options: { projectName?: string; yes?: boolean; server?: string },
|
||||||
|
): Promise<void> {
|
||||||
const projectName = options.projectName ?? (options.yes ? getDefaultProjectName(cwd) : await askProjectName(getDefaultProjectName(cwd)));
|
const projectName = options.projectName ?? (options.yes ? getDefaultProjectName(cwd) : await askProjectName(getDefaultProjectName(cwd)));
|
||||||
|
|
||||||
const { claude, codex, kimi } = options.yes
|
const { claude, codex, kimi } = options.yes
|
||||||
@ -14,8 +17,11 @@ export async function init(cwd: string, options: { projectName?: string; yes?: b
|
|||||||
|
|
||||||
const mode = options.yes ? 'suggest' : await askDelegationMode();
|
const mode = options.yes ? 'suggest' : await askDelegationMode();
|
||||||
|
|
||||||
|
const serverUrl = options.server ?? (options.yes ? undefined : await askServerUrl());
|
||||||
|
|
||||||
const config = defaultConfig(projectName);
|
const config = defaultConfig(projectName);
|
||||||
config.delegationMode = mode;
|
config.delegationMode = mode;
|
||||||
|
if (serverUrl) config.serverUrl = serverUrl;
|
||||||
|
|
||||||
const dir = getAgentHubDir(cwd);
|
const dir = getAgentHubDir(cwd);
|
||||||
mkdirSync(join(dir, 'tasks'), { recursive: true });
|
mkdirSync(join(dir, 'tasks'), { recursive: true });
|
||||||
@ -32,6 +38,9 @@ export async function init(cwd: string, options: { projectName?: string; yes?: b
|
|||||||
if (kimi) writeFileSync(join(cwd, 'KIMI.md'), kimiMd(), 'utf-8');
|
if (kimi) writeFileSync(join(cwd, 'KIMI.md'), kimiMd(), 'utf-8');
|
||||||
|
|
||||||
console.log(`AgentHub initialized for "${projectName}".`);
|
console.log(`AgentHub initialized for "${projectName}".`);
|
||||||
|
if (serverUrl) {
|
||||||
|
console.log(`Connected to remote server at ${serverUrl}.`);
|
||||||
|
}
|
||||||
console.log('Run `agenthub status` to see the current project state.');
|
console.log('Run `agenthub status` to see the current project state.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,18 @@ import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js';
|
|||||||
import { decisionCreate, decisionList } from './commands/decision.js';
|
import { decisionCreate, decisionList } from './commands/decision.js';
|
||||||
import { delegate } from './commands/delegate.js';
|
import { delegate } from './commands/delegate.js';
|
||||||
import { serverStart } from './commands/server.js';
|
import { serverStart } from './commands/server.js';
|
||||||
|
import { loadConfig } from '../core/config.js';
|
||||||
import { remoteClient, RemoteError } from './remoteClient.js';
|
import { remoteClient, RemoteError } from './remoteClient.js';
|
||||||
|
|
||||||
function getServerUrl(program: Command): string | undefined {
|
function getServerUrl(program: Command, cwd: string): string | undefined {
|
||||||
return (program.opts().server as string | undefined) || process.env.AGENTHUB_SERVER;
|
const flag = (program.opts().server as string | undefined) || process.env.AGENTHUB_SERVER;
|
||||||
|
if (flag) return flag;
|
||||||
|
try {
|
||||||
|
const config = loadConfig(cwd);
|
||||||
|
return config.serverUrl;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteOnly(): never {
|
function remoteOnly(): never {
|
||||||
@ -45,9 +53,9 @@ export function createProgram(cwd: string): Command {
|
|||||||
.description('Initialize AgentHub in the current directory')
|
.description('Initialize AgentHub in the current directory')
|
||||||
.option('-n, --project-name <name>', 'Project name')
|
.option('-n, --project-name <name>', 'Project name')
|
||||||
.option('-y, --yes', 'Use defaults without prompts')
|
.option('-y, --yes', 'Use defaults without prompts')
|
||||||
|
.option('--server <url>', 'Connect to a remote AgentHub server (stored in config)')
|
||||||
.action((options) => {
|
.action((options) => {
|
||||||
if (getServerUrl(program)) remoteOnly();
|
init(cwd, { ...options, server: options.server || (program.opts().server as string | undefined) });
|
||||||
init(cwd, options);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
@ -55,7 +63,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.description('Show project status')
|
.description('Show project status')
|
||||||
.option('-u, --update', 'Regenerate status before showing')
|
.option('-u, --update', 'Regenerate status before showing')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const body = options.update ? await remoteClient.updateStatus(serverUrl) : await remoteClient.getStatus(serverUrl);
|
const body = options.update ? await remoteClient.updateStatus(serverUrl) : await remoteClient.getStatus(serverUrl);
|
||||||
@ -74,7 +82,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.option('--category <category>', 'Category')
|
.option('--category <category>', 'Category')
|
||||||
.option('--content <content>', 'Content')
|
.option('--content <content>', 'Content')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const memory = await remoteClient.addMemory(serverUrl, options);
|
const memory = await remoteClient.addMemory(serverUrl, options);
|
||||||
@ -88,7 +96,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('search <query>')
|
.command('search <query>')
|
||||||
.description('Search memory and tasks')
|
.description('Search memory and tasks')
|
||||||
.action(async (query) => {
|
.action(async (query) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const results = await remoteClient.searchMemory(serverUrl, query);
|
const results = await remoteClient.searchMemory(serverUrl, query);
|
||||||
@ -103,7 +111,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('list')
|
.command('list')
|
||||||
.description('List memory entries')
|
.description('List memory entries')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const memories = await remoteClient.listMemory(serverUrl);
|
const memories = await remoteClient.listMemory(serverUrl);
|
||||||
@ -124,7 +132,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.option('--role <role>', 'Role')
|
.option('--role <role>', 'Role')
|
||||||
.option('--priority <priority>', 'Priority')
|
.option('--priority <priority>', 'Priority')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const task = await remoteClient.createTask(serverUrl, options);
|
const task = await remoteClient.createTask(serverUrl, options);
|
||||||
@ -140,7 +148,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.option('--status <status>', 'Filter by status')
|
.option('--status <status>', 'Filter by status')
|
||||||
.option('--role <role>', 'Filter by role')
|
.option('--role <role>', 'Filter by role')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const tasks = await remoteClient.listTasks(serverUrl, options);
|
const tasks = await remoteClient.listTasks(serverUrl, options);
|
||||||
@ -155,7 +163,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('show <id>')
|
.command('show <id>')
|
||||||
.description('Show a task')
|
.description('Show a task')
|
||||||
.action(async (id) => {
|
.action(async (id) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const { task, body } = await remoteClient.getTask(serverUrl, id);
|
const { task, body } = await remoteClient.getTask(serverUrl, id);
|
||||||
@ -172,7 +180,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.description('Claim a task')
|
.description('Claim a task')
|
||||||
.requiredOption('--agent <agent>', 'Agent name')
|
.requiredOption('--agent <agent>', 'Agent name')
|
||||||
.action(async (id, options) => {
|
.action(async (id, options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
await remoteClient.claimTask(serverUrl, id, options.agent);
|
await remoteClient.claimTask(serverUrl, id, options.agent);
|
||||||
@ -186,7 +194,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('done <id>')
|
.command('done <id>')
|
||||||
.description('Mark a task as done')
|
.description('Mark a task as done')
|
||||||
.action(async (id) => {
|
.action(async (id) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
await remoteClient.doneTask(serverUrl, id);
|
await remoteClient.doneTask(serverUrl, id);
|
||||||
@ -208,7 +216,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.option('--summary <summary>', 'Summary')
|
.option('--summary <summary>', 'Summary')
|
||||||
.option('--context <context>', 'Context')
|
.option('--context <context>', 'Context')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const handoff = await remoteClient.createHandoff(serverUrl, options);
|
const handoff = await remoteClient.createHandoff(serverUrl, options);
|
||||||
@ -222,7 +230,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('read <id>')
|
.command('read <id>')
|
||||||
.description('Read a handoff')
|
.description('Read a handoff')
|
||||||
.action(async (id) => {
|
.action(async (id) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const { handoff, body } = await remoteClient.getHandoff(serverUrl, id);
|
const { handoff, body } = await remoteClient.getHandoff(serverUrl, id);
|
||||||
@ -239,7 +247,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('list')
|
.command('list')
|
||||||
.description('List handoffs')
|
.description('List handoffs')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const handoffs = await remoteClient.listHandoffs(serverUrl);
|
const handoffs = await remoteClient.listHandoffs(serverUrl);
|
||||||
@ -260,7 +268,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.option('--context <context>', 'Context')
|
.option('--context <context>', 'Context')
|
||||||
.option('--decision <decision>', 'Decision')
|
.option('--decision <decision>', 'Decision')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const decision = await remoteClient.createDecision(serverUrl, options);
|
const decision = await remoteClient.createDecision(serverUrl, options);
|
||||||
@ -274,7 +282,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.command('list')
|
.command('list')
|
||||||
.description('List decisions')
|
.description('List decisions')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const decisions = await remoteClient.listDecisions(serverUrl);
|
const decisions = await remoteClient.listDecisions(serverUrl);
|
||||||
@ -292,7 +300,7 @@ export function createProgram(cwd: string): Command {
|
|||||||
.description('Suggest or auto-delegate open tasks')
|
.description('Suggest or auto-delegate open tasks')
|
||||||
.option('--auto', 'Create handoff automatically')
|
.option('--auto', 'Create handoff automatically')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const serverUrl = getServerUrl(program);
|
const serverUrl = getServerUrl(program, cwd);
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, async () => {
|
await runRemote(serverUrl, async () => {
|
||||||
const result = await remoteClient.delegate(serverUrl, options.auto ?? false);
|
const result = await remoteClient.delegate(serverUrl, options.auto ?? false);
|
||||||
|
|||||||
@ -22,3 +22,22 @@ export async function askDelegationMode(): Promise<'manual' | 'suggest' | 'auto'
|
|||||||
default: 'suggest',
|
default: 'suggest',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function askServerUrl(): Promise<string | undefined> {
|
||||||
|
const connect = await confirm({
|
||||||
|
message: 'Connect to an existing AgentHub server on the LAN?',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
if (!connect) return undefined;
|
||||||
|
return input({
|
||||||
|
message: 'Server URL (e.g. http://192.168.1.10:3377):',
|
||||||
|
validate: (value) => {
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return 'Please enter a valid URL.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export function defaultConfig(projectName: string): Config {
|
|||||||
reviewer: { preferredAgent: 'claude' },
|
reviewer: { preferredAgent: 'claude' },
|
||||||
tester: { preferredAgent: 'codex' },
|
tester: { preferredAgent: 'codex' },
|
||||||
},
|
},
|
||||||
|
serverUrl: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export const ConfigSchema = z.object({
|
|||||||
projectName: z.string().min(1),
|
projectName: z.string().min(1),
|
||||||
delegationMode: DelegationMode.default('suggest'),
|
delegationMode: DelegationMode.default('suggest'),
|
||||||
roles: z.record(z.string(), RoleConfigSchema),
|
roles: z.record(z.string(), RoleConfigSchema),
|
||||||
|
serverUrl: z.string().url().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Task = z.infer<typeof TaskSchema>;
|
export type Task = z.infer<typeof TaskSchema>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user