Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 115 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"bin": {
"agentage": "./dist/cli.js"
},
"files": ["dist"],
"files": [
"dist"
],
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0"
Expand All @@ -29,6 +31,7 @@
"dependencies": {
"@agentage/core": "^0.2.0",
"@agentage/platform": "^0.2.0",
"@supabase/supabase-js": "2.99.3",
"chalk": "latest",
"commander": "latest",
"express": "latest",
Expand All @@ -43,13 +46,13 @@
"@types/ws": "latest",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"@vitest/coverage-v8": "latest",
"eslint": "latest",
"eslint-config-prettier": "latest",
"eslint-plugin-prettier": "latest",
"prettier": "latest",
"typescript": "latest",
"vitest": "latest",
"@vitest/coverage-v8": "latest"
"vitest": "latest"
},
"repository": {
"type": "git",
Expand Down
118 changes: 115 additions & 3 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,124 @@
import { type Command } from 'commander';
import chalk from 'chalk';
import open from 'open';
import { ensureDaemon } from '../utils/ensure-daemon.js';
import { saveAuth, type AuthState } from '../hub/auth.js';
import { startCallbackServer, getCallbackPort } from '../hub/auth-callback.js';
import { loadConfig, saveConfig } from '../daemon/config.js';

const DEFAULT_HUB_URL = 'https://agentage.io';

export const registerLogin = (program: Command): void => {
program
.command('login')
.description('Authenticate with hub')
.option('--hub <url>', 'Hub URL')
.action(() => {
console.log(chalk.yellow('Hub sync not yet available.'));
.option('--hub <url>', 'Hub URL', DEFAULT_HUB_URL)
.option('--token <token>', 'Use access token directly (headless/CI)')
.action(async (opts: { hub: string; token?: string }) => {
await ensureDaemon();

const hubUrl = opts.hub;

if (opts.token) {
// Direct token mode — for headless/CI
console.log(chalk.yellow('Direct token login — skipping browser flow'));
console.log(
chalk.yellow(
'Note: refresh tokens are not available in direct mode. Session will expire.'
)
);

const config = loadConfig();
const authState: AuthState = {
session: {
access_token: opts.token,
refresh_token: '',
expires_at: 0,
},
user: { id: '', email: '' },
hub: { url: hubUrl, machineId: config.machine.id },
};

saveAuth(authState);

// Save hub URL to config
config.hub = { url: hubUrl };
saveConfig(config);

console.log(chalk.green('Logged in with token.'));
return;
}

// Fetch supabase config from hub health endpoint
let supabaseUrl: string;
let supabaseAnonKey: string;

try {
const healthRes = await fetch(`${hubUrl}/api/health`);
const health = (await healthRes.json()) as {
success: boolean;
data: { supabaseUrl: string; supabaseAnonKey: string };
};
supabaseUrl = health.data.supabaseUrl;
supabaseAnonKey = health.data.supabaseAnonKey;
} catch {
console.error(chalk.red(`Cannot reach hub at ${hubUrl}. Check the URL and try again.`));
process.exitCode = 1;
return;
}

// Start callback server, then open browser
console.log('Opening browser for authentication...');

const authPromise = startCallbackServer(supabaseUrl, supabaseAnonKey);

// Wait a tick for the server to start, then get the port
await new Promise((r) => setTimeout(r, 100));
const port = getCallbackPort();

if (!port) {
console.error(chalk.red('Failed to start callback server'));
process.exitCode = 1;
return;
}

const redirectUrl = `http://localhost:${port}/auth/callback`;
const authUrl = `${supabaseUrl}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(redirectUrl)}`;

try {
await open(authUrl);
} catch {
// Browser didn't open — print URL manually
console.log(chalk.yellow('Could not open browser. Open this URL manually:'));
console.log(authUrl);
}

console.log('Waiting for login...');

try {
const authState = await authPromise;

// Set hub info
authState.hub.url = hubUrl;
const config = loadConfig();
authState.hub.machineId = config.machine.id;

saveAuth(authState);

// Save hub URL to config
config.hub = { url: hubUrl };
saveConfig(config);

console.log(chalk.green(`✓ Logged in as ${authState.user.email}`));
console.log(
`Machine "${config.machine.name}" will register with hub on next daemon restart.`
);
console.log(chalk.dim('Run `agentage daemon restart` to connect now.'));
} catch (err) {
console.error(
chalk.red(`Login failed: ${err instanceof Error ? err.message : String(err)}`)
);
process.exitCode = 1;
}
});
};
16 changes: 13 additions & 3 deletions src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface HealthResponse {
uptime: number;
machineId: string;
hubConnected: boolean;
hubUrl: string | null;
userEmail: string | null;
}

export const registerStatus = (program: Command): void => {
Expand All @@ -28,9 +30,17 @@ export const registerStatus = (program: Command): void => {

console.log(`Daemon: ${chalk.green('running')} (PID ${pid}, port 4243)`);
console.log(`Uptime: ${uptime}`);
console.log(
`Hub: ${health.hubConnected ? chalk.green('connected') : chalk.yellow('not connected (standalone mode)')}`
);

if (health.hubConnected) {
console.log(`Hub: ${chalk.green('connected')} (${health.hubUrl})`);
console.log(`User: ${health.userEmail}`);
} else if (health.hubUrl) {
console.log(`Hub: ${chalk.yellow('disconnected')} (${health.hubUrl})`);
console.log(`User: ${health.userEmail}`);
} else {
console.log(`Hub: ${chalk.yellow('not connected (standalone mode)')}`);
}

console.log(`Machine: ${health.machineId}`);
console.log(`Agents: ${agents.length} discovered`);
console.log(`Runs: ${activeRuns} active`);
Expand Down
Loading