From 3ebd33c4bea8ba21d3180cb0fcdae7eef023ebfe Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 25 Jun 2026 13:33:12 +0200 Subject: [PATCH] feat(cli): store serverUrl in config during init; auto-connect LAN clients --- README.md | 9 ++++---- src/cli/commands/init.ts | 13 ++++++++++-- src/cli/index.ts | 46 +++++++++++++++++++++++----------------- src/cli/prompts.ts | 19 +++++++++++++++++ src/core/config.ts | 1 + src/core/schema.ts | 1 + 6 files changed, 64 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 102610c..ad96af2 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,18 @@ agenthub init 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 -$env:AGENTHUB_SERVER="http://:3377" +cd my-project +agenthub init --server http://:3377 agenthub task create --title "Windows task" --role implementer agenthub status ``` -Use `--server http://: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://: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 diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 96ffb8f..f81c641 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -3,9 +3,12 @@ import { join } from 'path'; import { getAgentHubDir } from '../../core/paths.js'; import { saveConfig, defaultConfig } from '../../core/config.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 { +export async function init( + cwd: string, + options: { projectName?: string; yes?: boolean; server?: string }, +): Promise { const projectName = options.projectName ?? (options.yes ? getDefaultProjectName(cwd) : await askProjectName(getDefaultProjectName(cwd))); 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 serverUrl = options.server ?? (options.yes ? undefined : await askServerUrl()); + const config = defaultConfig(projectName); config.delegationMode = mode; + if (serverUrl) config.serverUrl = serverUrl; const dir = getAgentHubDir(cwd); 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'); 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.'); } diff --git a/src/cli/index.ts b/src/cli/index.ts index d26ea7e..74d9be7 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,10 +7,18 @@ 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'; +import { loadConfig } from '../core/config.js'; import { remoteClient, RemoteError } from './remoteClient.js'; -function getServerUrl(program: Command): string | undefined { - return (program.opts().server as string | undefined) || process.env.AGENTHUB_SERVER; +function getServerUrl(program: Command, cwd: string): string | undefined { + 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 { @@ -45,9 +53,9 @@ export function createProgram(cwd: string): Command { .description('Initialize AgentHub in the current directory') .option('-n, --project-name ', 'Project name') .option('-y, --yes', 'Use defaults without prompts') + .option('--server ', 'Connect to a remote AgentHub server (stored in config)') .action((options) => { - if (getServerUrl(program)) remoteOnly(); - init(cwd, options); + init(cwd, { ...options, server: options.server || (program.opts().server as string | undefined) }); }); program @@ -55,7 +63,7 @@ export function createProgram(cwd: string): Command { .description('Show project status') .option('-u, --update', 'Regenerate status before showing') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { 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') .option('--content ', 'Content') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const memory = await remoteClient.addMemory(serverUrl, options); @@ -88,7 +96,7 @@ export function createProgram(cwd: string): Command { .command('search ') .description('Search memory and tasks') .action(async (query) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const results = await remoteClient.searchMemory(serverUrl, query); @@ -103,7 +111,7 @@ export function createProgram(cwd: string): Command { .command('list') .description('List memory entries') .action(async () => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const memories = await remoteClient.listMemory(serverUrl); @@ -124,7 +132,7 @@ export function createProgram(cwd: string): Command { .option('--role ', 'Role') .option('--priority ', 'Priority') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const task = await remoteClient.createTask(serverUrl, options); @@ -140,7 +148,7 @@ export function createProgram(cwd: string): Command { .option('--status ', 'Filter by status') .option('--role ', 'Filter by role') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const tasks = await remoteClient.listTasks(serverUrl, options); @@ -155,7 +163,7 @@ export function createProgram(cwd: string): Command { .command('show ') .description('Show a task') .action(async (id) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const { task, body } = await remoteClient.getTask(serverUrl, id); @@ -172,7 +180,7 @@ export function createProgram(cwd: string): Command { .description('Claim a task') .requiredOption('--agent ', 'Agent name') .action(async (id, options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { await remoteClient.claimTask(serverUrl, id, options.agent); @@ -186,7 +194,7 @@ export function createProgram(cwd: string): Command { .command('done ') .description('Mark a task as done') .action(async (id) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { await remoteClient.doneTask(serverUrl, id); @@ -208,7 +216,7 @@ export function createProgram(cwd: string): Command { .option('--summary ', 'Summary') .option('--context ', 'Context') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const handoff = await remoteClient.createHandoff(serverUrl, options); @@ -222,7 +230,7 @@ export function createProgram(cwd: string): Command { .command('read ') .description('Read a handoff') .action(async (id) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const { handoff, body } = await remoteClient.getHandoff(serverUrl, id); @@ -239,7 +247,7 @@ export function createProgram(cwd: string): Command { .command('list') .description('List handoffs') .action(async () => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const handoffs = await remoteClient.listHandoffs(serverUrl); @@ -260,7 +268,7 @@ export function createProgram(cwd: string): Command { .option('--context ', 'Context') .option('--decision ', 'Decision') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const decision = await remoteClient.createDecision(serverUrl, options); @@ -274,7 +282,7 @@ export function createProgram(cwd: string): Command { .command('list') .description('List decisions') .action(async () => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const decisions = await remoteClient.listDecisions(serverUrl); @@ -292,7 +300,7 @@ export function createProgram(cwd: string): Command { .description('Suggest or auto-delegate open tasks') .option('--auto', 'Create handoff automatically') .action(async (options) => { - const serverUrl = getServerUrl(program); + const serverUrl = getServerUrl(program, cwd); if (serverUrl) { await runRemote(serverUrl, async () => { const result = await remoteClient.delegate(serverUrl, options.auto ?? false); diff --git a/src/cli/prompts.ts b/src/cli/prompts.ts index dcc055e..707082f 100644 --- a/src/cli/prompts.ts +++ b/src/cli/prompts.ts @@ -22,3 +22,22 @@ export async function askDelegationMode(): Promise<'manual' | 'suggest' | 'auto' default: 'suggest', }); } + +export async function askServerUrl(): Promise { + 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.'; + } + }, + }); +} diff --git a/src/core/config.ts b/src/core/config.ts index 4fa0e6f..6eefe85 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -14,6 +14,7 @@ export function defaultConfig(projectName: string): Config { reviewer: { preferredAgent: 'claude' }, tester: { preferredAgent: 'codex' }, }, + serverUrl: undefined, }; } diff --git a/src/core/schema.ts b/src/core/schema.ts index 0229385..fdc6262 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -82,6 +82,7 @@ export const ConfigSchema = z.object({ projectName: z.string().min(1), delegationMode: DelegationMode.default('suggest'), roles: z.record(z.string(), RoleConfigSchema), + serverUrl: z.string().url().optional(), }); export type Task = z.infer;