Skip to content
This repository was archived by the owner on Feb 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/claude/utils/claudeSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Tests for Claude settings reading functionality
*
* Tests reading Claude's settings.json file and respecting the includeCoAuthoredBy setting
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync, writeFileSync, unlinkSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { readClaudeSettings, shouldIncludeCoAuthoredBy } from './claudeSettings';

describe('Claude Settings', () => {
let testClaudeDir: string;
let originalClaudeConfigDir: string | undefined;

beforeEach(() => {
// Create a temporary directory for testing
testClaudeDir = join(tmpdir(), `test-claude-${Date.now()}`);
mkdirSync(testClaudeDir, { recursive: true });

// Set environment variable to point to test directory
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = testClaudeDir;
});

afterEach(() => {
// Restore original environment variable
if (originalClaudeConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
} else {
delete process.env.CLAUDE_CONFIG_DIR;
}

// Clean up test directory
if (existsSync(testClaudeDir)) {
rmSync(testClaudeDir, { recursive: true, force: true });
}
});

describe('readClaudeSettings', () => {
it('returns null when settings file does not exist', () => {
const settings = readClaudeSettings();
expect(settings).toBe(null);
});

it('reads settings when file exists', () => {
const settingsPath = join(testClaudeDir, 'settings.json');
const testSettings = { includeCoAuthoredBy: false, otherSetting: 'value' };
writeFileSync(settingsPath, JSON.stringify(testSettings));

const settings = readClaudeSettings();
expect(settings).toEqual(testSettings);
});

it('returns null when settings file is invalid JSON', () => {
const settingsPath = join(testClaudeDir, 'settings.json');
writeFileSync(settingsPath, 'invalid json');

const settings = readClaudeSettings();
expect(settings).toBe(null);
});
});

describe('shouldIncludeCoAuthoredBy', () => {
it('returns true when no settings file exists (default behavior)', () => {
const result = shouldIncludeCoAuthoredBy();
expect(result).toBe(true);
});

it('returns true when includeCoAuthoredBy is not set (default behavior)', () => {
const settingsPath = join(testClaudeDir, 'settings.json');
writeFileSync(settingsPath, JSON.stringify({ otherSetting: 'value' }));

const result = shouldIncludeCoAuthoredBy();
expect(result).toBe(true);
});

it('returns false when includeCoAuthoredBy is explicitly set to false', () => {
const settingsPath = join(testClaudeDir, 'settings.json');
writeFileSync(settingsPath, JSON.stringify({ includeCoAuthoredBy: false }));

const result = shouldIncludeCoAuthoredBy();
expect(result).toBe(false);
});

it('returns true when includeCoAuthoredBy is explicitly set to true', () => {
const settingsPath = join(testClaudeDir, 'settings.json');
writeFileSync(settingsPath, JSON.stringify({ includeCoAuthoredBy: true }));

const result = shouldIncludeCoAuthoredBy();
expect(result).toBe(true);
});
});
});
69 changes: 69 additions & 0 deletions src/claude/utils/claudeSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Utilities for reading Claude's settings.json configuration
*
* Handles reading Claude's settings.json file to respect user preferences
* like includeCoAuthoredBy setting for commit message generation.
*/

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { logger } from '@/ui/logger';

export interface ClaudeSettings {
includeCoAuthoredBy?: boolean;
[key: string]: any;
}

/**
* Get the path to Claude's settings.json file
*/
function getClaudeSettingsPath(): string {
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
return join(claudeConfigDir, 'settings.json');
}

/**
* Read Claude's settings.json file from the default location
*
* @returns Claude settings object or null if file doesn't exist or can't be read
*/
export function readClaudeSettings(): ClaudeSettings | null {
try {
const settingsPath = getClaudeSettingsPath();

if (!existsSync(settingsPath)) {
logger.debug(`[ClaudeSettings] No Claude settings file found at ${settingsPath}`);
return null;
}

const settingsContent = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(settingsContent) as ClaudeSettings;

logger.debug(`[ClaudeSettings] Successfully read Claude settings from ${settingsPath}`);
logger.debug(`[ClaudeSettings] includeCoAuthoredBy: ${settings.includeCoAuthoredBy}`);

return settings;
} catch (error) {
logger.debug(`[ClaudeSettings] Error reading Claude settings: ${error}`);
return null;
}
}

/**
* Check if Co-Authored-By lines should be included in commit messages
* based on Claude's settings
*
* @returns true if Co-Authored-By should be included, false otherwise
*/
export function shouldIncludeCoAuthoredBy(): boolean {
const settings = readClaudeSettings();

// If no settings file or includeCoAuthoredBy is not explicitly set,
// default to true to maintain backward compatibility
if (!settings || settings.includeCoAuthoredBy === undefined) {
return true;
}

return settings.includeCoAuthoredBy;
}
27 changes: 25 additions & 2 deletions src/claude/utils/systemPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { trimIdent } from "@/utils/trimIdent";
import { shouldIncludeCoAuthoredBy } from "./claudeSettings";

export const systemPrompt = trimIdent(`
/**
* Base system prompt shared across all configurations
*/
const BASE_SYSTEM_PROMPT = (() => trimIdent(`
ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
`))();

/**
* Co-authored-by credits to append when enabled
*/
const CO_AUTHORED_CREDITS = (() => trimIdent(`
When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:

<main commit message>
Expand All @@ -12,4 +21,18 @@ export const systemPrompt = trimIdent(`

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
`);
`))();

/**
* System prompt with conditional Co-Authored-By lines based on Claude's settings.json configuration.
* Settings are read once on startup for performance.
*/
export const systemPrompt = (() => {
const includeCoAuthored = shouldIncludeCoAuthoredBy();

if (includeCoAuthored) {
return BASE_SYSTEM_PROMPT + '\n\n' + CO_AUTHORED_CREDITS;
} else {
return BASE_SYSTEM_PROMPT;
}
})();
Loading