feat: fix leaked source to be locally runnable

- Restore full Ink TUI startup chain (cli.tsx entry point)
- Create stub .md files for verify skill (Bun text loader hang fix)
- Create stub types for filePersistence and SDK modules
- Fix Enter key not working (modifiers-napi missing, added try-catch)
- Remove overly conservative LOCAL_RECOVERY early return
- Add README with setup instructions
- Add .env.example template
- Add bin/claude-haha entry script and preload.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes)
2026-04-01 01:30:48 +08:00
parent 5a774a2b62
commit 124912c71d
33 changed files with 5572 additions and 280 deletions
@@ -0,0 +1,3 @@
// Local recovery stub for missing generated SDK types.
// The leaked source tree does not include this codegen artifact.
export {}
+2
View File
@@ -0,0 +1,2 @@
// Local recovery stub for missing SDK runtime type exports.
export {}
@@ -0,0 +1,2 @@
// Local recovery stub for missing generated SDK settings types.
export {}
+2
View File
@@ -0,0 +1,2 @@
// Local recovery stub for missing SDK tool type exports.
export {}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+291
View File
@@ -0,0 +1,291 @@
import Anthropic from '@anthropic-ai/sdk'
import { readFileSync } from 'fs'
import { createInterface } from 'readline'
type OutputFormat = 'text' | 'json'
function printHelp(): void {
process.stdout.write(
[
'Usage: claude-haha [options] [prompt]',
'',
'Local recovery mode for this leaked source tree.',
'',
'Options:',
' -h, --help Show help',
' -v, --version Show version',
' (no args) Start local interactive mode',
' -p, --print Send a single prompt and print the result',
' --model <model> Override model',
' --system-prompt <text> Override system prompt',
' --system-prompt-file <file> Read system prompt from file',
' --append-system-prompt <text> Append to the system prompt',
' --output-format <format> text (default) or json',
'',
'Environment:',
' ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN',
' ANTHROPIC_BASE_URL',
' ANTHROPIC_MODEL',
' API_TIMEOUT_MS',
'',
].join('\n'),
)
}
function printVersion(): void {
process.stdout.write('999.0.0-local (Claude Code local recovery)\n')
}
function parseArgs(argv: string[]) {
let print = false
let model = process.env.ANTHROPIC_MODEL
let systemPrompt: string | undefined
let appendSystemPrompt: string | undefined
let outputFormat: OutputFormat = 'text'
const positional: string[] = []
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (!arg) continue
if (arg === '-h' || arg === '--help') {
return { command: 'help' as const }
}
if (arg === '-v' || arg === '--version' || arg === '-V') {
return { command: 'version' as const }
}
if (arg === '-p' || arg === '--print') {
print = true
continue
}
if (arg === '--bare') {
continue
}
if (arg === '--dangerously-skip-permissions') {
continue
}
if (arg === '--model') {
model = argv[++i]
continue
}
if (arg === '--system-prompt') {
systemPrompt = argv[++i]
continue
}
if (arg === '--system-prompt-file') {
const file = argv[++i]
systemPrompt = readFileSync(file!, 'utf8')
continue
}
if (arg === '--append-system-prompt') {
appendSystemPrompt = argv[++i]
continue
}
if (arg === '--output-format') {
const value = argv[++i]
if (value === 'json' || value === 'text') {
outputFormat = value
}
continue
}
if (arg.startsWith('-')) {
continue
}
positional.push(arg)
}
return {
command: 'run' as const,
print,
model,
systemPrompt,
appendSystemPrompt,
outputFormat,
prompt: positional.join(' ').trim(),
}
}
async function readPromptFromStdin(): Promise<string> {
if (process.stdin.isTTY) return ''
const chunks: Buffer[] = []
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)))
}
return Buffer.concat(chunks).toString('utf8').trim()
}
function getSystemPrompt(
systemPrompt: string | undefined,
appendSystemPrompt: string | undefined,
): string | undefined {
if (systemPrompt && appendSystemPrompt) {
return `${systemPrompt}\n\n${appendSystemPrompt}`
}
return systemPrompt ?? appendSystemPrompt
}
async function run(): Promise<void> {
const parsed = parseArgs(process.argv.slice(2))
if (parsed.command === 'help') {
printHelp()
return
}
if (parsed.command === 'version') {
printVersion()
return
}
if (!parsed.print) {
await runInteractive(parsed)
return
}
const prompt = parsed.prompt || (await readPromptFromStdin())
if (!prompt) {
process.stderr.write('Error: prompt is required\n')
process.exitCode = 1
return
}
const apiKey = process.env.ANTHROPIC_API_KEY
const authToken = process.env.ANTHROPIC_AUTH_TOKEN
if (!apiKey && !authToken) {
process.stderr.write(
'Error: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN\n',
)
process.exitCode = 1
return
}
const model =
parsed.model ||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ||
process.env.ANTHROPIC_MODEL
if (!model) {
process.stderr.write('Error: model is required\n')
process.exitCode = 1
return
}
const client = new Anthropic({
apiKey: apiKey ?? undefined,
authToken: authToken ?? undefined,
baseURL: process.env.ANTHROPIC_BASE_URL || undefined,
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600_000), 10),
maxRetries: 0,
})
const response = await client.messages.create({
model,
max_tokens: 4096,
system: getSystemPrompt(parsed.systemPrompt, parsed.appendSystemPrompt),
messages: [{ role: 'user', content: prompt }],
})
if (parsed.outputFormat === 'json') {
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`)
return
}
const text = response.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('\n')
process.stdout.write(`${text}\n`)
}
async function runInteractive(parsed: {
model?: string
systemPrompt?: string
appendSystemPrompt?: string
}): Promise<void> {
const apiKey = process.env.ANTHROPIC_API_KEY
const authToken = process.env.ANTHROPIC_AUTH_TOKEN
if (!apiKey && !authToken) {
process.stderr.write(
'Error: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN\n',
)
process.exitCode = 1
return
}
const model =
parsed.model ||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ||
process.env.ANTHROPIC_MODEL
if (!model) {
process.stderr.write('Error: model is required\n')
process.exitCode = 1
return
}
const client = new Anthropic({
apiKey: apiKey ?? undefined,
authToken: authToken ?? undefined,
baseURL: process.env.ANTHROPIC_BASE_URL || undefined,
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600_000), 10),
maxRetries: 0,
})
const system = getSystemPrompt(parsed.systemPrompt, parsed.appendSystemPrompt)
const messages: Array<{ role: 'user' | 'assistant'; content: string }> = []
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'you> ',
})
process.stdout.write(
`Claude Haha local interactive mode\nmodel: ${model}\ncommands: /exit, /clear\n\n`,
)
rl.prompt()
for await (const line of rl) {
const input = line.trim()
if (!input) {
rl.prompt()
continue
}
if (input === '/exit' || input === '/quit') {
rl.close()
break
}
if (input === '/clear') {
messages.length = 0
process.stdout.write('history cleared\n')
rl.prompt()
continue
}
messages.push({ role: 'user', content: input })
try {
const response = await client.messages.create({
model,
max_tokens: 4096,
system,
messages,
})
const text = response.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('\n')
process.stdout.write(`claude> ${text}\n\n`)
messages.push({ role: 'assistant', content: text })
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
process.stderr.write(`error: ${message}\n`)
}
rl.prompt()
}
}
void run().catch(error => {
const message = error instanceof Error ? error.stack || error.message : String(error)
process.stderr.write(`${message}\n`)
process.exitCode = 1
})
+13 -7
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -165,6 +165,10 @@ function computeChecksum(
* getSettings() to avoid circular dependencies during settings loading.
*/
export function isPolicyLimitsEligible(): boolean {
if (process.env.CLAUDE_CODE_LOCAL_SKIP_REMOTE_PREFETCH === '1') {
return false
}
// 3p provider users should not hit the policy limits endpoint
if (getAPIProvider() !== 'firstParty') {
return false
@@ -49,6 +49,10 @@ export function resetSyncCache(): void {
export function isRemoteManagedSettingsEligible(): boolean {
if (cached !== undefined) return cached
if (process.env.CLAUDE_CODE_LOCAL_SKIP_REMOTE_PREFETCH === '1') {
return (cached = setEligibility(false))
}
// 3p provider users should not hit the settings endpoint
if (getAPIProvider() !== 'firstParty') {
return (cached = setEligibility(false))
+10
View File
@@ -159,6 +159,16 @@ export async function setup(
// IMPORTANT: setCwd() must be called before any other code that depends on the cwd
setCwd(cwd)
setOriginalCwd(cwd)
setProjectRoot(cwd)
// Local recovery mode: when CLAUDE_CODE_LOCAL_RECOVERY=1 is explicitly set,
// trim startup to minimum. Otherwise run full setup for the Ink TUI.
if (process.env.CLAUDE_CODE_LOCAL_RECOVERY === '1') {
process.stderr.write('[local-recovery] setup early return\n')
profileCheckpoint('setup_local_recovery_early_return')
return
}
// Capture hooks configuration snapshot to avoid hidden hook modifications.
// IMPORTANT: Must be called AFTER setCwd() so hooks are loaded from the correct directory
+13 -34
View File
@@ -1,16 +1,5 @@
import { feature } from 'bun:bundle'
import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
import { registerBatchSkill } from './batch.js'
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
import { registerDebugSkill } from './debug.js'
import { registerKeybindingsSkill } from './keybindings.js'
import { registerLoremIpsumSkill } from './loremIpsum.js'
import { registerRememberSkill } from './remember.js'
import { registerSimplifySkill } from './simplify.js'
import { registerSkillifySkill } from './skillify.js'
import { registerStuckSkill } from './stuck.js'
import { registerUpdateConfigSkill } from './updateConfig.js'
import { registerVerifySkill } from './verify.js'
/**
* Initialize all bundled skills.
@@ -22,58 +11,48 @@ import { registerVerifySkill } from './verify.js'
* 3. Import and call that function here
*/
export function initBundledSkills(): void {
registerUpdateConfigSkill()
registerKeybindingsSkill()
registerVerifySkill()
registerDebugSkill()
registerLoremIpsumSkill()
registerSkillifySkill()
registerRememberSkill()
registerSimplifySkill()
registerBatchSkill()
registerStuckSkill()
/* eslint-disable @typescript-eslint/no-require-imports */
require('./updateConfig.js').registerUpdateConfigSkill()
require('./keybindings.js').registerKeybindingsSkill()
require('./verify.js').registerVerifySkill()
require('./debug.js').registerDebugSkill()
require('./loremIpsum.js').registerLoremIpsumSkill()
require('./skillify.js').registerSkillifySkill()
require('./remember.js').registerRememberSkill()
require('./simplify.js').registerSimplifySkill()
require('./batch.js').registerBatchSkill()
require('./stuck.js').registerStuckSkill()
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerDreamSkill } = require('./dream.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerDreamSkill()
}
if (feature('REVIEW_ARTIFACT')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerHunterSkill } = require('./hunter.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerHunterSkill()
}
if (feature('AGENT_TRIGGERS')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerLoopSkill } = require('./loop.js')
/* eslint-enable @typescript-eslint/no-require-imports */
// /loop's isEnabled delegates to isKairosCronEnabled() — same lazy
// per-invocation pattern as the cron tools. Registered unconditionally;
// the skill's own isEnabled callback decides visibility.
registerLoopSkill()
}
if (feature('AGENT_TRIGGERS_REMOTE')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const {
registerScheduleRemoteAgentsSkill,
} = require('./scheduleRemoteAgents.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerScheduleRemoteAgentsSkill()
}
if (feature('BUILDING_CLAUDE_APPS')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerClaudeApiSkill } = require('./claudeApi.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerClaudeApiSkill()
}
if (shouldAutoEnableClaudeInChrome()) {
registerClaudeInChromeSkill()
require('./claudeInChrome.js').registerClaudeInChromeSkill()
}
if (feature('RUN_SKILL_GENERATOR')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerRunSkillGeneratorSkill } = require('./runSkillGenerator.js')
/* eslint-enable @typescript-eslint/no-require-imports */
registerRunSkillGeneratorSkill()
}
/* eslint-enable @typescript-eslint/no-require-imports */
}
+6
View File
@@ -0,0 +1,6 @@
---
name: verify
description: Verify a code change does what it should by running the app.
---
Verify the code change works as expected.
@@ -0,0 +1 @@
# CLI verification example
@@ -0,0 +1 @@
# Server verification example
@@ -0,0 +1,5 @@
import React from 'react'
export function TungstenLiveMonitor(): React.JSX.Element | null {
return null
}
+43
View File
@@ -0,0 +1,43 @@
import { z } from 'zod/v4'
const inputSchema = z.object({}).passthrough()
export const TungstenTool = {
name: 'tungsten',
aliases: [],
maxResultSizeChars: 0,
inputSchema,
async description() {
return 'Unavailable in this local recovery build.'
},
async prompt() {
return 'TungstenTool is unavailable in this local recovery build.'
},
async call() {
return {
data: {
success: false,
error: 'TungstenTool is unavailable in this local recovery build.',
},
}
},
isConcurrencySafe() {
return true
},
isEnabled() {
return false
},
isReadOnly() {
return true
},
async checkPermissions() {
return {
behavior: 'deny' as const,
message: 'TungstenTool is unavailable in this local recovery build.',
}
},
}
export function clearSessionsWithTungstenUsage(): void {}
export function resetInitializationState(): void {}
+1
View File
@@ -0,0 +1 @@
export const WORKFLOW_TOOL_NAME = 'workflow'
+20
View File
@@ -0,0 +1,20 @@
export type ConnectorTextBlock = {
type: 'connector_text';
text?: string;
};
export type ConnectorTextDelta = {
type: 'connector_text_delta';
text?: string;
};
export function isConnectorTextBlock(
value: unknown,
): value is ConnectorTextBlock {
return (
typeof value === 'object' &&
value !== null &&
'type' in value &&
(value as { type?: unknown }).type === 'connector_text'
);
}
+22
View File
@@ -0,0 +1,22 @@
// Local recovery stub for missing filePersistence types
export const DEFAULT_UPLOAD_CONCURRENCY = 5
export const FILE_COUNT_LIMIT = 100
export const OUTPUTS_SUBDIR = 'outputs'
export interface FailedPersistence {
filePath: string
error: string
}
export interface PersistedFile {
filePath: string
fileId?: string
}
export interface FilesPersistedEventData {
persisted: PersistedFile[]
failed: FailedPersistence[]
}
export type TurnStartTime = number
+9 -5
View File
@@ -28,9 +28,13 @@ export function isModifierPressed(modifier: ModifierKey): boolean {
if (process.platform !== 'darwin') {
return false
}
// Dynamic import to avoid loading native module at top level
const { isModifierPressed: nativeIsModifierPressed } =
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('modifiers-napi') as { isModifierPressed: (m: string) => boolean }
return nativeIsModifierPressed(modifier)
try {
// Dynamic import to avoid loading native module at top level
const { isModifierPressed: nativeIsModifierPressed } =
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('modifiers-napi') as { isModifierPressed: (m: string) => boolean }
return nativeIsModifierPressed(modifier)
} catch {
return false
}
}
+1
View File
@@ -0,0 +1 @@
You are a planning assistant.