mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-04 00:01:41 +00:00
207 lines
8.9 KiB
TypeScript
207 lines
8.9 KiB
TypeScript
import chalk from 'chalk'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { z } from 'zod'
|
|
import { PROTOCOL_VERSION } from '@hapi/protocol'
|
|
import type { StartOptions } from '@/claude/runClaude'
|
|
import { CLAUDE_PERMISSION_MODES } from '@hapi/protocol/modes'
|
|
import { configuration } from '@/configuration'
|
|
import { isRunnerRunningCurrentlyInstalledHappyVersion } from '@/runner/controlClient'
|
|
import { authAndSetupMachineIfNeeded } from '@/ui/auth'
|
|
import { logger } from '@/ui/logger'
|
|
import { initializeToken } from '@/ui/tokenInit'
|
|
import { spawnHappyCLI } from '@/utils/spawnHappyCLI'
|
|
import { maybeAutoStartServer } from '@/utils/autoStartServer'
|
|
import { withBunRuntimeEnv } from '@/utils/bunRuntime'
|
|
import { extractErrorInfo } from '@/utils/errorUtils'
|
|
import type { CommandDefinition } from './types'
|
|
|
|
export const claudeCommand: CommandDefinition = {
|
|
name: 'default',
|
|
requiresRuntimeAssets: true,
|
|
run: async ({ commandArgs }) => {
|
|
const args = [...commandArgs]
|
|
|
|
if (args.length > 0 && args[0] === 'claude') {
|
|
args.shift()
|
|
}
|
|
|
|
const options: StartOptions = {}
|
|
let showHelp = false
|
|
const unknownArgs: string[] = []
|
|
let hasExplicitPermissionMode = false
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i]
|
|
|
|
if (arg === '-h' || arg === '--help') {
|
|
showHelp = true
|
|
unknownArgs.push(arg)
|
|
} else if (arg === '--hapi-starting-mode') {
|
|
options.startingMode = z.enum(['local', 'remote']).parse(args[++i])
|
|
} else if (arg === '--permission-mode') {
|
|
const mode = args[++i]
|
|
if (!mode || !(CLAUDE_PERMISSION_MODES as readonly string[]).includes(mode)) {
|
|
throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`)
|
|
}
|
|
options.permissionMode = mode as StartOptions['permissionMode']
|
|
hasExplicitPermissionMode = true
|
|
} else if (arg === '--yolo' && !hasExplicitPermissionMode) {
|
|
options.permissionMode = 'bypassPermissions'
|
|
unknownArgs.push('--dangerously-skip-permissions')
|
|
} else if (arg === '--dangerously-skip-permissions' && !hasExplicitPermissionMode) {
|
|
options.permissionMode = 'bypassPermissions'
|
|
unknownArgs.push(arg)
|
|
} else if (arg === '--model') {
|
|
const model = args[++i]
|
|
if (!model) {
|
|
throw new Error('Missing --model value')
|
|
}
|
|
options.model = model
|
|
unknownArgs.push('--model', model)
|
|
} else if (arg === '--effort') {
|
|
const effort = args[++i]
|
|
if (!effort) {
|
|
throw new Error('Missing --effort value')
|
|
}
|
|
options.effort = effort
|
|
unknownArgs.push('--effort', effort)
|
|
} else if (arg === '--started-by') {
|
|
options.startedBy = args[++i] as 'runner' | 'terminal'
|
|
} else {
|
|
unknownArgs.push(arg)
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
unknownArgs.push(args[++i])
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unknownArgs.length > 0) {
|
|
options.claudeArgs = [...(options.claudeArgs || []), ...unknownArgs]
|
|
}
|
|
|
|
if (showHelp) {
|
|
console.log(`
|
|
${chalk.bold('hapi')} - Claude Code On the Go
|
|
|
|
${chalk.bold('Usage:')}
|
|
hapi [options] Start Claude with Telegram control (direct-connect)
|
|
hapi auth Manage authentication
|
|
hapi codex Start Codex mode
|
|
hapi cursor Start Cursor Agent mode
|
|
hapi gemini Start Gemini ACP mode
|
|
hapi opencode Start OpenCode ACP mode
|
|
hapi resume [id] Resume an existing HAPI session locally
|
|
hapi mcp Start MCP stdio bridge
|
|
hapi connect (not available in direct-connect mode)
|
|
hapi notify (not available in direct-connect mode)
|
|
hapi hub Start the API + web hub
|
|
hapi hub --relay Start with public relay
|
|
hapi server Alias for hapi hub
|
|
hapi runner Manage background service that allows
|
|
to spawn new sessions away from your computer
|
|
hapi doctor System diagnostics & troubleshooting
|
|
|
|
${chalk.bold('Examples:')}
|
|
hapi Start session (will prompt for token if not set)
|
|
hapi auth login Configure CLI_API_TOKEN interactively
|
|
hapi --yolo Start with bypassing permissions
|
|
hapi sugar for --dangerously-skip-permissions
|
|
hapi auth status Show direct-connect status
|
|
hapi doctor Run diagnostics
|
|
|
|
${chalk.bold('hapi supports ALL Claude options!')}
|
|
Use any claude flag with hapi as you would with claude. Our favorite:
|
|
|
|
hapi --resume
|
|
|
|
${chalk.gray('─'.repeat(60))}
|
|
${chalk.bold.cyan('Claude Code Options (from `claude --help`):')}
|
|
`)
|
|
|
|
try {
|
|
const claudeHelp = execFileSync(
|
|
'claude',
|
|
['--help'],
|
|
{ encoding: 'utf8', env: withBunRuntimeEnv(), shell: process.platform === 'win32' }
|
|
)
|
|
console.log(claudeHelp)
|
|
} catch {
|
|
console.log(chalk.yellow('Could not retrieve claude help. Make sure claude is installed.'))
|
|
}
|
|
|
|
process.exit(0)
|
|
}
|
|
|
|
await initializeToken()
|
|
await maybeAutoStartServer()
|
|
await authAndSetupMachineIfNeeded()
|
|
|
|
logger.debug('Ensuring hapi background service is running & matches our version...')
|
|
|
|
if (!(await isRunnerRunningCurrentlyInstalledHappyVersion())) {
|
|
logger.debug('Starting hapi background service...')
|
|
|
|
const runnerProcess = spawnHappyCLI(['runner', 'start-sync'], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
env: process.env
|
|
})
|
|
runnerProcess.unref()
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200))
|
|
}
|
|
|
|
try {
|
|
const { runClaude } = await import('@/claude/runClaude')
|
|
await runClaude(options)
|
|
} catch (error) {
|
|
const { message, messageLower, axiosCode, httpStatus, responseErrorText, serverProtocolVersion } = extractErrorInfo(error)
|
|
|
|
if (
|
|
axiosCode === 'ECONNREFUSED' ||
|
|
axiosCode === 'ETIMEDOUT' ||
|
|
axiosCode === 'ENOTFOUND' ||
|
|
messageLower.includes('econnrefused') ||
|
|
messageLower.includes('etimedout') ||
|
|
messageLower.includes('enotfound') ||
|
|
messageLower.includes('network error')
|
|
) {
|
|
console.error(chalk.yellow('Unable to connect to HAPI hub'))
|
|
console.error(chalk.gray(` Hub URL: ${configuration.apiUrl}`))
|
|
console.error(chalk.gray(' Please check your network connection or hub status'))
|
|
} else if (httpStatus === 403 && responseErrorText === 'Machine access denied') {
|
|
console.error(chalk.red('Machine access denied.'))
|
|
console.error(chalk.gray(' This machineId is already registered under a different namespace.'))
|
|
console.error(chalk.gray(' Fix: run `hapi auth logout`, or set a separate HAPI_HOME per namespace.'))
|
|
} else if (httpStatus === 403 && responseErrorText === 'Session access denied') {
|
|
console.error(chalk.red('Session access denied.'))
|
|
console.error(chalk.gray(' This session belongs to a different namespace.'))
|
|
console.error(chalk.gray(' Use the matching CLI_API_TOKEN or switch namespaces.'))
|
|
} else if (
|
|
httpStatus === 401 ||
|
|
httpStatus === 403 ||
|
|
messageLower.includes('unauthorized') ||
|
|
messageLower.includes('forbidden')
|
|
) {
|
|
console.error(chalk.red('Authentication error:'), message)
|
|
console.error(chalk.gray(' Run: hapi auth login'))
|
|
} else {
|
|
console.error(chalk.red('Error:'), message)
|
|
}
|
|
|
|
if (serverProtocolVersion !== undefined && serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
if (serverProtocolVersion < PROTOCOL_VERSION) {
|
|
console.error(chalk.yellow(` Hint: hub protocol version (${serverProtocolVersion}) is behind CLI (${PROTOCOL_VERSION}). Please update the hub.`))
|
|
} else {
|
|
console.error(chalk.yellow(` Hint: CLI protocol version (${PROTOCOL_VERSION}) is behind hub (${serverProtocolVersion}). Please update the CLI.`))
|
|
}
|
|
}
|
|
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
}
|
|
}
|