tinyagent
Version:
Connect your local shell to any device - access your dev environment from anywhere
231 lines • 9.67 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const chalk_1 = __importDefault(require("chalk"));
const dotenv_1 = require("dotenv");
const child_process_1 = require("child_process");
const shell_client_v2_1 = require("./shell-client-v2");
const firebase_auth_simple_1 = require("./firebase-auth-simple");
(0, dotenv_1.config)();
/**
* Check if Claude Code CLI is installed
*/
function isClaudeInstalled() {
try {
(0, child_process_1.execSync)('which claude', { stdio: 'ignore' });
return true;
}
catch {
return false;
}
}
/**
* Install Claude Code CLI via npm
*/
async function installClaude() {
console.log(chalk_1.default.yellow('\nClaude Code CLI not found.'));
console.log(chalk_1.default.cyan('Installing Claude Code (@anthropic-ai/claude-code)...\n'));
try {
// Use npm to install globally
const result = (0, child_process_1.spawnSync)('npm', ['install', '-g', '@anthropic-ai/claude-code'], {
stdio: 'inherit',
shell: true
});
if (result.status === 0) {
console.log(chalk_1.default.green('\n✓ Claude Code installed successfully!\n'));
return true;
}
else {
console.log(chalk_1.default.red('\n✗ Failed to install Claude Code.'));
console.log(chalk_1.default.yellow('Try installing manually: npm install -g @anthropic-ai/claude-code\n'));
return false;
}
}
catch (error) {
console.log(chalk_1.default.red(`\n✗ Installation error: ${error}`));
console.log(chalk_1.default.yellow('Try installing manually: npm install -g @anthropic-ai/claude-code\n'));
return false;
}
}
/**
* Ensure Claude Code is available, installing if necessary
*/
async function ensureClaudeInstalled() {
if (isClaudeInstalled()) {
return true;
}
return await installClaude();
}
const program = new commander_1.Command();
// Authentication commands
program
.command('login')
.description('Authenticate with Tinyagent')
.action(async () => {
const authClient = new firebase_auth_simple_1.FirebaseAuthClient();
const success = await authClient.authenticate();
process.exit(success ? 0 : 1);
});
program
.command('logout')
.description('Log out from Tinyagent')
.action(() => {
const authClient = new firebase_auth_simple_1.FirebaseAuthClient();
authClient.logout();
process.exit(0);
});
program
.command('whoami')
.description('Display current user information')
.action(() => {
const authClient = new firebase_auth_simple_1.FirebaseAuthClient();
authClient.whoami();
process.exit(0);
});
// Sessions command - list active sessions
program
.command('sessions')
.description('List your active sessions')
.option('-r, --relay <url>', 'Relay server URL', process.env.RELAY_URL || 'https://relay.tinyagent.app')
.action(async (options) => {
const authClient = new firebase_auth_simple_1.FirebaseAuthClient();
if (!authClient.isAuthenticated()) {
console.log(chalk_1.default.yellow('You need to log in to see your sessions.'));
console.log(chalk_1.default.cyan('Run: tinyagent login'));
process.exit(1);
}
const token = authClient.getToken();
if (!token) {
console.log(chalk_1.default.red('Failed to get auth token'));
process.exit(1);
}
try {
const httpUrl = options.relay.replace('wss://', 'https://').replace('ws://', 'http://');
const response = await fetch(`${httpUrl}/api/user/sessions`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
const error = await response.text();
console.log(chalk_1.default.red(`Failed to fetch sessions: ${error}`));
process.exit(1);
}
const data = await response.json();
if (data.sessions.length === 0) {
console.log(chalk_1.default.yellow('No active sessions'));
}
else {
console.log(chalk_1.default.bold('\nYour active sessions:\n'));
for (const session of data.sessions) {
const tierNames = {
1: chalk_1.default.green('authenticated'),
2: chalk_1.default.yellow('password-protected'),
3: chalk_1.default.red('public')
};
const status = session.mobileConnected
? chalk_1.default.green('mobile connected')
: chalk_1.default.gray('no mobile client');
console.log(` ${chalk_1.default.bold(session.sessionId)}`);
console.log(` Security: ${tierNames[session.securityTier] || 'unknown'}`);
console.log(` Status: ${status}`);
if (session.currentCommand) {
console.log(` Running: ${chalk_1.default.cyan(session.currentCommand)}`);
}
console.log();
}
}
}
catch (error) {
console.log(chalk_1.default.red(`Error: ${error.message}`));
process.exit(1);
}
process.exit(0);
});
// Main shell command
program
.name('tinyagent')
.description('Connect your local shell to any device')
.version('1.3.0')
.argument('[sessionId]', 'Session ID to connect to (optional, auto-generated if not provided)')
.option('--session-id <id>', 'Session ID (alternative to positional argument)')
.option('-r, --relay <url>', 'Relay server URL', process.env.RELAY_URL || 'wss://relay.tinyagent.app')
.option('-s, --shell <shell>', 'Shell to use', process.env.SHELL || '/bin/bash')
.option('-c, --command <cmd>', 'Server command to run (e.g., "npm run dev")')
.option('-p, --port <port>', 'Local server port', '3000')
.option('--no-tunnel', 'Disable tunnel creation')
.option('--no-claude', 'Do not auto-start Claude')
.option('-v, --verbose', 'Show detailed debug output')
.option('--password <password>', 'Password required for mobile clients to connect')
.option('--public', 'Create a public session (anyone with QR can connect)')
.option('--resume', 'Resume the last Claude session')
.option('--continue', 'Continue the last Claude session (alias for --resume)')
.action(async (sessionIdArg, options) => {
// Determine session ID from argument or option, or generate one
let sessionId = sessionIdArg || options.sessionId;
if (!sessionId) {
// Generate a random session ID
sessionId = Math.random().toString(36).substring(2, 15);
console.log(chalk_1.default.cyan(`Generated session ID: ${sessionId}`));
}
// Check authentication status
const authClient = new firebase_auth_simple_1.FirebaseAuthClient();
const isAuthenticated = authClient.isAuthenticated();
// Require --public flag for unauthenticated sessions without password
if (!isAuthenticated && !options.password && !options.public) {
console.log(chalk_1.default.yellow('\n⚠️ Security Notice'));
console.log(chalk_1.default.white('You are not logged in and no password was set.'));
console.log(chalk_1.default.white('Please choose one of these options:\n'));
console.log(chalk_1.default.cyan(' 1. tinyagent login'));
console.log(chalk_1.default.gray(' Most secure - only you can connect to your sessions\n'));
console.log(chalk_1.default.cyan(` 2. tinyagent ${sessionId} --password <secret>`));
console.log(chalk_1.default.gray(' Medium security - requires password to connect\n'));
console.log(chalk_1.default.cyan(` 3. tinyagent ${sessionId} --public`));
console.log(chalk_1.default.gray(' Low security - anyone with your session ID can connect & execute code on your computer\n'));
process.exit(1);
}
try {
// Ensure Claude Code is installed (unless --no-claude is set)
if (options.claude !== false) {
const claudeReady = await ensureClaudeInstalled();
if (!claudeReady) {
console.log(chalk_1.default.yellow('Continuing without Claude Code auto-start feature.\n'));
}
}
console.log(chalk_1.default.blue(`Creating session: ${sessionId}`));
// QR code will be shown after connection (needs sessionToken)
const client = new shell_client_v2_1.ShellClient({
sessionId,
relayUrl: options.relay,
shell: options.shell,
serverCommand: options.command,
serverPort: parseInt(options.port),
createTunnel: options.tunnel,
autoStartClaude: !options.noClaude,
password: options.password,
isPublic: options.public,
resumeClaude: options.resume || options.continue,
verbose: options.verbose
});
await client.connect();
process.on('SIGINT', () => {
console.log(chalk_1.default.yellow('\nDisconnecting...'));
client.disconnect();
process.exit(0);
});
process.on('SIGTERM', () => {
client.disconnect();
process.exit(0);
});
}
catch (error) {
console.error(chalk_1.default.red(`Error: ${error}`));
process.exit(1);
}
});
program.parse();
//# sourceMappingURL=cli.js.map