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:
@@ -0,0 +1,3 @@
|
||||
// Local recovery stub for missing generated SDK types.
|
||||
// The leaked source tree does not include this codegen artifact.
|
||||
export {}
|
||||
@@ -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 {}
|
||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
@@ -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))
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
export const WORKFLOW_TOOL_NAME = 'workflow'
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
You are a planning assistant.
|
||||
Reference in New Issue
Block a user