Files
ponytail/hooks/ponytail-config.js
T
Lakshya77089 53fd1e850e fix: only deactivate on a standalone "stop ponytail" / "normal mode" (#162)
The deactivation check matched the phrase anywhere in the prompt, so an
ordinary request like "add a normal mode toggle" silently turned ponytail
off for the rest of the session. Match the whole message instead (trimmed,
case-insensitive, trailing punctuation ignored) through a shared helper used
by both the Claude/Codex hook and the pi extension.

Fixes #161
2026-06-18 22:50:03 +02:00

113 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
// ponytail — shared configuration resolver
//
// Resolution order for default mode:
// 1. PONYTAIL_DEFAULT_MODE environment variable
// 2. Config file defaultMode field:
// - $XDG_CONFIG_HOME/ponytail/config.json (any platform, if set)
// - ~/.config/ponytail/config.json (macOS / Linux fallback)
// - %APPDATA%\ponytail\config.json (Windows fallback)
// 3. 'full'
const fs = require('fs');
const path = require('path');
const os = require('os');
const DEFAULT_MODE = 'full';
const VALID_MODES = ['off', 'lite', 'full', 'ultra', 'review'];
const RUNTIME_MODES = ['off', 'lite', 'full', 'ultra'];
function normalizeMode(mode) {
if (typeof mode !== 'string') return null;
const normalized = mode.trim().toLowerCase();
return RUNTIME_MODES.includes(normalized) ? normalized : null;
}
function normalizeConfigMode(mode) {
if (typeof mode !== 'string') return null;
const normalized = mode.trim().toLowerCase();
return VALID_MODES.includes(normalized) ? normalized : null;
}
function normalizePersistedMode(mode) {
return normalizeMode(mode) || normalizeConfigMode(mode);
}
// "stop ponytail" / "normal mode" turn ponytail off, but only as a standalone
// command. Matching the phrase anywhere in the message turned it off mid-task
// for ordinary requests like "add a normal mode toggle" — so require the whole
// message to be the command, ignoring case and trailing punctuation.
function isDeactivationCommand(text) {
const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
return t === 'stop ponytail' || t === 'normal mode';
}
function getConfigDir() {
if (process.env.XDG_CONFIG_HOME) {
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
}
if (process.platform === 'win32') {
return path.join(
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
'ponytail'
);
}
return path.join(os.homedir(), '.config', 'ponytail');
}
function getConfigPath() {
return path.join(getConfigDir(), 'config.json');
}
function getClaudeDir() {
// ponytail: CLAUDE_CONFIG_DIR overrides ~/.claude, matching Claude Code.
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
}
function getDefaultMode() {
// 1. Environment variable (highest priority)
const envMode = process.env.PONYTAIL_DEFAULT_MODE;
if (envMode && VALID_MODES.includes(envMode.toLowerCase())) {
return envMode.toLowerCase();
}
// 2. Config file
try {
const configPath = getConfigPath();
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) {
return config.defaultMode.toLowerCase();
}
} catch (e) {
// Config file doesn't exist or is invalid — fall through
}
// 3. Default
return DEFAULT_MODE;
}
function writeDefaultMode(mode) {
const normalized = normalizeConfigMode(mode);
if (!normalized) return null;
const configPath = getConfigPath();
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({ defaultMode: normalized }, null, 2), 'utf8');
return normalized;
}
module.exports = {
DEFAULT_MODE,
VALID_MODES,
RUNTIME_MODES,
getDefaultMode,
getConfigDir,
getConfigPath,
getClaudeDir,
normalizeMode,
normalizeConfigMode,
normalizePersistedMode,
isDeactivationCommand,
writeDefaultMode,
};