mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* feat: settings toggle for AGENT_NOTIFY_SUMMARY contract injection Add a hub-persisted, default-off Settings control so operators can opt agents into emitting the trailing AGENT_NOTIFY_SUMMARY line. Propagate the resolved flag on CLI session bootstrap and inject at call time for Claude, Codex, OpenCode, and Grok (Cursor still unsupported). Closes #1375 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: lock hub settings RMW and restore abort deliveryMode Serialize settings.json updates with the shared .lock protocol and unique temp files so the new hub toggle cannot clobber CLI/relay fields under concurrency. Also supply deliveryMode on abort send-error restore so web typecheck (and CI) pass. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): wrap SettingsGeneralPage tests with QueryClientProvider The hub-settings toggle uses TanStack Query; the settings page suite was rendering without a QueryClient and blew up CI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): defer summary-contract toggle until settings load Avoid rendering an interactive false switch while the hub GET is still in flight, which could overwrite an enabled preference on early click. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): clarify Grok coverage for summary-contract toggle Local Grok has no instruction inject path; settings copy now matches remote-only Grok support (and still excludes Cursor). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import { logger } from "@/ui/logger";
|
|
import { claudeCheckSession } from "./utils/claudeCheckSession";
|
|
import { getProjectPath } from "./utils/path";
|
|
import { appendMcpConfigArg } from "./utils/mcpConfig";
|
|
import { getSystemPrompt } from "./utils/systemPrompt";
|
|
import { withBunRuntimeEnv } from "@/utils/bunRuntime";
|
|
import { spawnWithTerminalGuard } from "@/utils/spawnWithTerminalGuard";
|
|
import { getHapiBlobsDir } from "@/constants/uploadPaths";
|
|
import { stripNewlinesForWindowsShellArg } from "@/utils/shellEscape";
|
|
import { getDefaultClaudeCodePath } from "./sdk/utils";
|
|
import type { SessionModel } from "@/api/types";
|
|
|
|
function withoutTrackedModelArgs(args: string[]): string[] {
|
|
const filtered: string[] = [];
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--model') {
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
if (arg.startsWith('--model=')) {
|
|
continue;
|
|
}
|
|
filtered.push(arg);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
export async function claudeLocal(opts: {
|
|
abort: AbortSignal,
|
|
sessionId: string | null,
|
|
mcpServers?: Record<string, any>,
|
|
path: string,
|
|
claudeEnvVars?: Record<string, string>,
|
|
claudeArgs?: string[]
|
|
model?: SessionModel
|
|
allowedTools?: string[]
|
|
hookSettingsPath: string
|
|
}) {
|
|
|
|
// Ensure project directory exists
|
|
const projectDir = getProjectPath(opts.path);
|
|
mkdirSync(projectDir, { recursive: true });
|
|
|
|
// Check if user passed explicit session control flags.
|
|
const hasContinueFlag = opts.claudeArgs?.includes('--continue');
|
|
const hasResumeFlag = opts.claudeArgs?.includes('--resume');
|
|
const hasUserSessionControl = Boolean(hasContinueFlag || hasResumeFlag);
|
|
|
|
// Determine session strategy:
|
|
// - If resuming an existing session: use --resume (unless user already supplied session control)
|
|
// - If starting fresh: let Claude create a new session ID (reported via SessionStart hook)
|
|
let startFrom = opts.sessionId;
|
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
startFrom = null;
|
|
}
|
|
|
|
if (opts.abort.aborted) {
|
|
logger.debug('[ClaudeLocal] Abort already signaled before spawn; skipping launch');
|
|
return startFrom ?? null;
|
|
}
|
|
|
|
// Build args for Claude CLI
|
|
const args: string[] = [];
|
|
|
|
if (startFrom && !hasUserSessionControl) {
|
|
// Resume existing session
|
|
args.push('--resume', startFrom);
|
|
}
|
|
|
|
args.push('--append-system-prompt', stripNewlinesForWindowsShellArg(getSystemPrompt()));
|
|
|
|
const cleanupMcpConfig = appendMcpConfigArg(args, opts.mcpServers, {
|
|
baseDir: projectDir
|
|
});
|
|
|
|
if (opts.allowedTools && opts.allowedTools.length > 0) {
|
|
args.push('--allowedTools', opts.allowedTools.join(','));
|
|
}
|
|
|
|
// Once model state is available, it is authoritative over startup args.
|
|
const claudeArgs = opts.model === undefined || !opts.claudeArgs
|
|
? opts.claudeArgs
|
|
: withoutTrackedModelArgs(opts.claudeArgs);
|
|
if (claudeArgs) {
|
|
args.push(...claudeArgs);
|
|
}
|
|
if (opts.model) {
|
|
args.push('--model', opts.model);
|
|
}
|
|
|
|
// Add hook settings for session tracking
|
|
args.push('--settings', opts.hookSettingsPath);
|
|
logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
|
|
|
|
// Add blobs directory for file upload access
|
|
args.push('--add-dir', getHapiBlobsDir());
|
|
logger.debug(`[ClaudeLocal] Adding blobs directory: ${getHapiBlobsDir()}`);
|
|
|
|
// Prepare environment variables
|
|
// Note: Local mode uses global Claude installation
|
|
//
|
|
// SDK metadata extraction (extractSDKMetadata → query()) sets
|
|
// CLAUDE_CODE_ENTRYPOINT='sdk-ts' on the current process. If leaked
|
|
// into the local spawn, Claude Code thinks it was SDK-launched and
|
|
// excludes the session from `claude --resume`. Destructure it out
|
|
// so the child uses its own default entrypoint.
|
|
const { CLAUDE_CODE_ENTRYPOINT: _, ...cleanEnv } = process.env
|
|
const env = {
|
|
...cleanEnv,
|
|
DISABLE_AUTOUPDATER: '1',
|
|
...opts.claudeEnvVars
|
|
}
|
|
|
|
logger.debug(`[ClaudeLocal] Spawning claude with args: ${JSON.stringify(args)}`);
|
|
|
|
// Get Claude executable path (absolute path on Windows for shell: false)
|
|
const claudeCommand = getDefaultClaudeCodePath();
|
|
logger.debug(`[ClaudeLocal] Using claude executable: ${claudeCommand}`);
|
|
|
|
// Spawn the process
|
|
try {
|
|
await spawnWithTerminalGuard({
|
|
command: claudeCommand,
|
|
args,
|
|
cwd: opts.path,
|
|
env: withBunRuntimeEnv(env, { allowBunBeBun: false }),
|
|
signal: opts.abort,
|
|
logLabel: 'ClaudeLocal',
|
|
spawnName: 'claude',
|
|
installHint: 'Claude CLI',
|
|
includeCause: true,
|
|
logExit: true,
|
|
shell: false // Use absolute path, no shell needed
|
|
});
|
|
} finally {
|
|
cleanupMcpConfig?.();
|
|
}
|
|
|
|
return startFrom ?? null;
|
|
}
|