UNPKG

claude-code-tamagotchi

Version:

A virtual pet that lives in your Claude Code statusline

1,180 lines (1,155 loc) 41.7 kB
#!/usr/bin/env bun // @bun import { createRequire } from "node:module"; var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __require = /* @__PURE__ */ createRequire(import.meta.url); // node_modules/dotenv/package.json var require_package = __commonJS((exports, module) => { module.exports = { name: "dotenv", version: "16.6.1", description: "Loads environment variables from .env file", main: "lib/main.js", types: "lib/main.d.ts", exports: { ".": { types: "./lib/main.d.ts", require: "./lib/main.js", default: "./lib/main.js" }, "./config": "./config.js", "./config.js": "./config.js", "./lib/env-options": "./lib/env-options.js", "./lib/env-options.js": "./lib/env-options.js", "./lib/cli-options": "./lib/cli-options.js", "./lib/cli-options.js": "./lib/cli-options.js", "./package.json": "./package.json" }, scripts: { "dts-check": "tsc --project tests/types/tsconfig.json", lint: "standard", pretest: "npm run lint && npm run dts-check", test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000", "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov", prerelease: "npm test", release: "standard-version" }, repository: { type: "git", url: "git://github.com/motdotla/dotenv.git" }, homepage: "https://github.com/motdotla/dotenv#readme", funding: "https://dotenvx.com", keywords: [ "dotenv", "env", ".env", "environment", "variables", "config", "settings" ], readmeFilename: "README.md", license: "BSD-2-Clause", devDependencies: { "@types/node": "^18.11.3", decache: "^4.6.2", sinon: "^14.0.1", standard: "^17.0.0", "standard-version": "^9.5.0", tap: "^19.2.0", typescript: "^4.8.4" }, engines: { node: ">=12" }, browser: { fs: false } }; }); // node_modules/dotenv/lib/main.js var require_main = __commonJS((exports, module) => { var fs = __require("fs"); var path = __require("path"); var os = __require("os"); var crypto = __require("crypto"); var packageJson = require_package(); var version = packageJson.version; var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, ` `); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, ` `); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault(options) { options = options || {}; const vaultPath = _vaultPath(options); options.path = vaultPath; const result = DotenvModule.configDotenv(options); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0;i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _warn(message) { console.log(`[dotenv@${version}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version}][DEBUG] ${message}`); } function _log(message) { console.log(`[dotenv@${version}] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path.resolve(process.cwd(), ".env.vault"); } if (fs.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath; } function _configVault(options) { const debug = Boolean(options && options.debug); const quiet = options && "quiet" in options ? options.quiet : true; if (debug || !quiet) { _log("Loading env from encrypted .env.vault"); } const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv(options) { const dotenvPath = path.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); const quiet = options && "quiet" in options ? options.quiet : true; if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path2 of optionPaths) { try { const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path2} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (debug || !quiet) { const keysCount = Object.keys(parsedAll).length; const shortPaths = []; for (const filePath of optionPaths) { try { const relative = path.relative(process.cwd(), filePath); shortPaths.push(relative); } catch (e) { if (debug) { _debug(`Failed to load ${filePath} ${e.message}`); } lastError = e; } } _log(`injecting env (${keysCount}) from ${shortPaths.join(",")}`); } if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } var DotenvModule = { configDotenv, _configVault, _parseVault, config, decrypt, parse, populate }; exports.configDotenv = DotenvModule.configDotenv; exports._configVault = DotenvModule._configVault; exports._parseVault = DotenvModule._parseVault; exports.config = DotenvModule.config; exports.decrypt = DotenvModule.decrypt; exports.parse = DotenvModule.parse; exports.populate = DotenvModule.populate; module.exports = DotenvModule; }); // src/engine/feedback/FeedbackDatabase.ts import { Database } from "bun:sqlite"; import * as fs3 from "fs"; import * as path2 from "path"; import * as os2 from "os"; class FeedbackDatabase { db; dbPath; maxSizeMB; constructor(dbPath, maxSizeMB = 50) { this.dbPath = dbPath.startsWith("~") ? path2.join(os2.homedir(), dbPath.slice(1)) : dbPath; this.maxSizeMB = maxSizeMB; const dir = path2.dirname(this.dbPath); if (!fs3.existsSync(dir)) { fs3.mkdirSync(dir, { recursive: true }); } this.db = new Database(this.dbPath); this.db.exec("PRAGMA journal_mode = WAL"); this.db.exec("PRAGMA synchronous = NORMAL"); this.initializeSchema(); } static extractWorkspaceId(transcriptPath) { const match = transcriptPath.match(/\/projects\/([^\/]+)\//); return match ? match[1] : "default"; } initializeSchema() { this.db.exec(` -- Processing lock table CREATE TABLE IF NOT EXISTS processing_lock ( message_uuid TEXT PRIMARY KEY, process_pid INTEGER NOT NULL, locked_at INTEGER NOT NULL, completed BOOLEAN DEFAULT 0, completed_at INTEGER ); -- Analysis state tracking CREATE TABLE IF NOT EXISTS analysis_state ( id INTEGER PRIMARY KEY CHECK (id = 1), last_processed_uuid TEXT, last_processed_timestamp INTEGER, total_messages_processed INTEGER DEFAULT 0, last_cleanup_at INTEGER ); -- Message metadata CREATE TABLE IF NOT EXISTS message_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id TEXT NOT NULL DEFAULT 'default', session_id TEXT NOT NULL, message_uuid TEXT UNIQUE NOT NULL, parent_uuid TEXT, timestamp TEXT NOT NULL, type TEXT NOT NULL, role TEXT, summary TEXT, intent TEXT, project_context TEXT, compliance_score INTEGER, efficiency_score INTEGER, created_at INTEGER NOT NULL ); -- Feedback CREATE TABLE IF NOT EXISTS feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id TEXT NOT NULL DEFAULT 'default', session_id TEXT NOT NULL, message_uuid TEXT NOT NULL, feedback_type TEXT NOT NULL, severity TEXT NOT NULL, remark TEXT, funny_observation TEXT, icon TEXT, shown BOOLEAN DEFAULT 0, expires_at INTEGER, created_at INTEGER NOT NULL ); -- Violations table for tracking Claude's misbehavior CREATE TABLE IF NOT EXISTS violations ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id TEXT, session_id TEXT NOT NULL, message_uuid TEXT NOT NULL, violation_type TEXT NOT NULL, severity TEXT NOT NULL, evidence TEXT NOT NULL, user_intent TEXT NOT NULL, claude_behavior TEXT NOT NULL, claude_correction_prompt TEXT NOT NULL, notified_claude BOOLEAN DEFAULT 0, notified_at INTEGER, claude_response_uuid TEXT, acknowledged BOOLEAN DEFAULT 0, created_at INTEGER NOT NULL, expires_at INTEGER ); -- Create indexes CREATE INDEX IF NOT EXISTS idx_lock_completed ON processing_lock(completed, locked_at); CREATE INDEX IF NOT EXISTS idx_metadata_session ON message_metadata(workspace_id, session_id, timestamp); CREATE INDEX IF NOT EXISTS idx_metadata_uuid ON message_metadata(message_uuid); CREATE INDEX IF NOT EXISTS idx_feedback_shown ON feedback(shown, severity, expires_at); CREATE INDEX IF NOT EXISTS idx_feedback_uuid ON feedback(message_uuid); CREATE INDEX IF NOT EXISTS idx_violations_session ON violations(session_id); CREATE INDEX IF NOT EXISTS idx_violations_notified ON violations(notified_claude, session_id); CREATE INDEX IF NOT EXISTS idx_violations_severity ON violations(severity, created_at); `); const state = this.db.query("SELECT * FROM analysis_state WHERE id = 1").get(); if (!state) { this.db.query("INSERT INTO analysis_state (id, total_messages_processed) VALUES (1, 0)").run(); } } saveMessageMetadata(metadata) { const stmt = this.db.query(` INSERT OR REPLACE INTO message_metadata ( session_id, message_uuid, parent_uuid, timestamp, type, role, summary, intent, project_context, compliance_score, efficiency_score, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(metadata.session_id, metadata.message_uuid, metadata.parent_uuid || null, metadata.timestamp, metadata.type, metadata.role || null, metadata.summary || null, metadata.intent || null, metadata.project_context || null, metadata.compliance_score || null, metadata.efficiency_score || null, metadata.created_at); } getRecentMetadata(sessionId, limit = 10) { return this.db.query(` SELECT * FROM message_metadata WHERE session_id = ? ORDER BY timestamp DESC LIMIT ? `).all(sessionId, limit); } getSessionMetadata(sessionId) { return this.db.query(` SELECT * FROM message_metadata WHERE session_id = ? ORDER BY timestamp ASC `).all(sessionId); } saveFeedback(feedback) { const stmt = this.db.query(` INSERT INTO feedback ( session_id, message_uuid, feedback_type, severity, remark, funny_observation, icon, shown, expires_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(feedback.session_id, feedback.message_uuid, feedback.feedback_type, feedback.severity, feedback.remark || null, feedback.funny_observation || null, feedback.icon || null, feedback.shown ? 1 : 0, feedback.expires_at || null, feedback.created_at); } getUnshownFeedback(limit = 5, sessionId) { const now = Date.now(); if (sessionId) { return this.db.query(` SELECT * FROM feedback WHERE session_id = ? AND shown = 0 AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC LIMIT ? `).all(sessionId, now, limit); } else { return this.db.query(` SELECT * FROM feedback WHERE shown = 0 AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC LIMIT ? `).all(now, limit); } } getLatestSessionFeedback(sessionId) { const result = this.db.query(` SELECT * FROM feedback WHERE session_id = ? AND funny_observation IS NOT NULL AND shown = 0 ORDER BY created_at DESC LIMIT 1 `).get(sessionId); return result || null; } markFeedbackShown(ids) { if (ids.length === 0) return; const placeholders = ids.map(() => "?").join(","); this.db.query(` UPDATE feedback SET shown = 1 WHERE id IN (${placeholders}) `).run(...ids); } getRecentFeedback(sessionId, limit = 10) { return this.db.query(` SELECT * FROM feedback WHERE session_id = ? ORDER BY created_at DESC LIMIT ? `).all(sessionId, limit); } getRecentFunnyObservations(sessionId, limit = 10) { const results = this.db.query(` SELECT funny_observation FROM feedback WHERE session_id = ? AND funny_observation IS NOT NULL AND funny_observation != '' ORDER BY created_at DESC LIMIT ? `).all(sessionId, limit); return results.map((r) => r.funny_observation); } cleanStaleLocks(staleLockTime) { const cutoff = Date.now() - staleLockTime; this.db.query(` DELETE FROM processing_lock WHERE completed = 0 AND locked_at < ? `).run(cutoff); return 0; } acquireLocks(messageUuids, pid) { const now = Date.now(); const acquired = []; this.db.exec("BEGIN"); try { for (const uuid of messageUuids) { const existing = this.db.query("SELECT * FROM processing_lock WHERE message_uuid = ?").get(uuid); if (!existing || existing.completed) { this.db.query(` INSERT OR REPLACE INTO processing_lock (message_uuid, process_pid, locked_at, completed) VALUES (?, ?, ?, 0) `).run(uuid, pid, now); acquired.push(uuid); } } this.db.exec("COMMIT"); } catch (error) { this.db.exec("ROLLBACK"); throw error; } return acquired; } markMessagesProcessed(uuids) { if (uuids.length === 0) return; const now = Date.now(); this.db.exec("BEGIN"); try { for (const uuid of uuids) { this.db.query(` UPDATE processing_lock SET completed = 1, completed_at = ? WHERE message_uuid = ? `).run(now, uuid); this.db.query(` UPDATE analysis_state SET last_processed_uuid = ?, last_processed_timestamp = ?, total_messages_processed = total_messages_processed + 1 WHERE id = 1 `).run(uuid, now); } this.db.exec("COMMIT"); } catch (error) { this.db.exec("ROLLBACK"); throw error; } } getAnalysisState() { return this.db.query("SELECT * FROM analysis_state WHERE id = 1").get(); } updateLastProcessed(uuid) { const now = Date.now(); this.db.query(` UPDATE analysis_state SET last_processed_uuid = ?, last_processed_timestamp = ? WHERE id = 1 `).run(uuid, now); } isMessageProcessed(uuid) { const result = this.db.query("SELECT completed FROM processing_lock WHERE message_uuid = ?").get(uuid); return result?.completed === 1; } getUnprocessedMessageUuids(afterUuid, limit = 10) { return []; } checkAndCleanup() { const stats = fs3.statSync(this.dbPath); const sizeMB = stats.size / (1024 * 1024); if (sizeMB > this.maxSizeMB) { const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; this.db.query("DELETE FROM message_metadata WHERE created_at < ?").run(cutoff); this.db.query("DELETE FROM feedback WHERE created_at < ?").run(cutoff); this.db.query("DELETE FROM processing_lock WHERE completed = 1 AND completed_at < ?").run(cutoff); this.db.exec("VACUUM"); this.db.query("UPDATE analysis_state SET last_cleanup_at = ? WHERE id = 1").run(Date.now()); } } saveViolation(violation) { const stmt = this.db.query(` INSERT INTO violations ( workspace_id, session_id, message_uuid, violation_type, severity, evidence, user_intent, claude_behavior, claude_correction_prompt, notified_claude, notified_at, claude_response_uuid, acknowledged, created_at, expires_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run(violation.workspace_id || null, violation.session_id, violation.message_uuid, violation.violation_type, violation.severity, violation.evidence, violation.user_intent, violation.claude_behavior, violation.claude_correction_prompt, violation.notified_claude ? 1 : 0, violation.notified_at || null, violation.claude_response_uuid || null, violation.acknowledged ? 1 : 0, violation.created_at, violation.expires_at || null); } getUnnotifiedViolations(sessionId) { return this.db.query(` SELECT * FROM violations WHERE session_id = ? AND notified_claude = 0 ORDER BY created_at ASC `).all(sessionId); } getSessionViolations(sessionId) { return this.db.query(` SELECT * FROM violations WHERE session_id = ? ORDER BY created_at DESC `).all(sessionId); } markViolationsNotified(violationIds, responseUuid) { const now = Date.now(); this.db.exec("BEGIN"); try { for (const id of violationIds) { this.db.query(` UPDATE violations SET notified_claude = 1, notified_at = ?, claude_response_uuid = ? WHERE id = ? `).run(now, responseUuid, id); } this.db.exec("COMMIT"); } catch (error) { this.db.exec("ROLLBACK"); throw error; } } markViolationAcknowledged(violationId) { this.db.query(` UPDATE violations SET acknowledged = 1 WHERE id = ? `).run(violationId); } cleanupExpiredViolations() { const now = Date.now(); this.db.query(` DELETE FROM violations WHERE expires_at IS NOT NULL AND expires_at < ? `).run(now); return 0; } close() { this.db.close(); } } var init_FeedbackDatabase = () => {}; // src/commands/violation-check.ts var exports_violation_check = {}; __export(exports_violation_check, { violationCheck: () => violationCheck }); async function violationCheck() { try { const enabled = process.env.PET_VIOLATION_CHECK_ENABLED !== "false"; if (!enabled) { process.exit(0); } if (process.stdin.isTTY) { if (process.env.PET_FEEDBACK_DEBUG === "true") { console.error("[violation-check] No stdin available (running from terminal) - exiting"); } process.exit(0); } const stdinText = await Bun.stdin.text(); if (!stdinText || !stdinText.trim()) { process.exit(0); } let input; let sessionId; try { input = JSON.parse(stdinText); sessionId = input.session_id; } catch (error) { if (process.env.PET_FEEDBACK_DEBUG === "true") { console.error("[violation-check] Invalid JSON from stdin:", error); } process.exit(0); } if (!sessionId) { process.exit(0); } const minSeverity = process.env.PET_VIOLATION_MIN_SEVERITY || "moderate"; const batchViolations = process.env.PET_VIOLATION_BATCH !== "false"; const maxAgeMinutes = parseInt(process.env.PET_VIOLATION_MAX_AGE || "30"); const severityLevels = { minor: 1, moderate: 2, severe: 3, critical: 4 }; const threshold = severityLevels[minSeverity] || 2; const dbPath = process.env.PET_STATE_FILE?.replace("claude-pet-state.json", "feedback.db") || process.env.PET_FEEDBACK_DB_PATH || "~/.claude/pets/feedback.db"; const db = new FeedbackDatabase(dbPath, 50); const allViolations = db.getUnnotifiedViolations(sessionId); const cutoffTime = Date.now() - maxAgeMinutes * 60 * 1000; const relevantViolations = allViolations.filter((v) => { const severityLevel = severityLevels[v.severity] || 0; const isRecent = v.created_at > cutoffTime; return severityLevel >= threshold && isRecent; }); if (relevantViolations.length === 0) { db.close(); process.exit(0); } relevantViolations.sort((a, b) => { const aLevel = severityLevels[b.severity] || 0; const bLevel = severityLevels[a.severity] || 0; return bLevel - aLevel; }); let message; let violationIdsToMark = []; if (batchViolations && relevantViolations.length > 1) { const header = `**MULTIPLE VIOLATIONS DETECTED (${relevantViolations.length})** `; const violations = relevantViolations.map((v, i) => { violationIdsToMark.push(v.id); return `--- Violation ${i + 1} (${v.severity.toUpperCase()}) --- ${v.claude_correction_prompt}`; }).join(` `); message = header + violations + ` Please acknowledge ALL violations and adjust your approach accordingly.`; } else { const violation = relevantViolations[0]; violationIdsToMark.push(violation.id); message = violation.claude_correction_prompt; } console.error(message); const messageUuid = input.tool_input?.message_uuid || `hook-${Date.now()}`; db.markViolationsNotified(violationIdsToMark, messageUuid); db.close(); process.exit(2); } catch (error) { if (process.env.PET_FEEDBACK_DEBUG === "true") { console.error(`[violation-check] Error: ${error}`); } process.exit(0); } } var init_violation_check = __esm(async () => { init_FeedbackDatabase(); if (false) {} }); // src/commands/CommandProcessor.ts import * as fs2 from "fs"; // src/utils/config.ts var dotenv = __toESM(require_main(), 1); import * as path from "path"; import * as fs from "fs"; import * as os from "os"; var __dirname = "/Users/idol/projects/claude-code-tamagotchi/src/utils"; var envPath = path.join(__dirname, "../../.env"); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); } function resolvePath(filepath) { if (filepath.startsWith("~")) { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } var config2 = { petName: process.env.PET_NAME || "Buddy", petType: process.env.PET_TYPE || "dog", petColor: process.env.PET_COLOR || "default", animationSpeed: parseInt(process.env.ANIMATION_SPEED || "300"), enableBlinking: process.env.ENABLE_BLINKING !== "false", idleAnimationChance: parseFloat(process.env.IDLE_ANIMATION_CHANCE || "0.1"), walkingAnimationChance: parseFloat(process.env.WALKING_ANIMATION_CHANCE || "0.05"), weather: process.env.WEATHER || "sunny", season: process.env.SEASON || "spring", hungerDecayRate: parseFloat(process.env.HUNGER_DECAY_RATE || "0.5"), happinessDecayRate: parseFloat(process.env.HAPPINESS_DECAY_RATE || "0.2"), energyDecayRate: parseFloat(process.env.ENERGY_DECAY_RATE || "0.3"), cleanlinessDecayRate: parseFloat(process.env.CLEANLINESS_DECAY_RATE || "0.1"), enableEvolution: process.env.ENABLE_EVOLUTION !== "false", enableAccessories: process.env.ENABLE_ACCESSORIES !== "false", enableSounds: process.env.ENABLE_SOUNDS === "true", enableWeatherEffects: process.env.ENABLE_WEATHER_EFFECTS !== "false", conversationThoughtRatio: parseFloat(process.env.PET_CONVERSATION_THOUGHT_RATIO || "1.0"), feedbackEnabled: process.env.PET_FEEDBACK_ENABLED === "true", feedbackMode: process.env.PET_FEEDBACK_MODE || "full", feedbackCheckInterval: parseInt(process.env.PET_FEEDBACK_CHECK_INTERVAL || "5"), feedbackBatchSize: parseInt(process.env.PET_FEEDBACK_BATCH_SIZE || "10"), feedbackMinMessages: parseInt(process.env.PET_FEEDBACK_MIN_MESSAGES || "3"), feedbackStaleLockTime: parseInt(process.env.PET_FEEDBACK_STALE_LOCK_TIME || "30000"), feedbackDbPath: resolvePath(process.env.PET_FEEDBACK_DB_PATH || "~/.claude/pets/feedback.db"), feedbackDbMaxSize: parseInt(process.env.PET_FEEDBACK_DB_MAX_SIZE || "50"), groqApiKey: process.env.PET_GROQ_API_KEY || process.env.GROQ_API_KEY, groqModel: process.env.PET_GROQ_MODEL || "openai/gpt-oss-20b", groqTimeout: parseInt(process.env.PET_GROQ_TIMEOUT || "2000"), groqMaxRetries: parseInt(process.env.PET_GROQ_MAX_RETRIES || "2"), moodDecayRate: parseInt(process.env.PET_MOOD_DECAY_RATE || "5"), annoyedThreshold: parseInt(process.env.PET_ANNOYED_THRESHOLD || "3"), angryThreshold: parseInt(process.env.PET_ANGRY_THRESHOLD || "5"), furiousThreshold: parseInt(process.env.PET_FURIOUS_THRESHOLD || "8"), praiseBoost: parseInt(process.env.PET_PRAISE_BOOST || "10"), feedbackIconStyle: process.env.PET_FEEDBACK_ICON_STYLE || "emoji", feedbackRemarkLength: parseInt(process.env.PET_FEEDBACK_REMARK_LENGTH || "50"), showComplianceScore: process.env.PET_SHOW_COMPLIANCE_SCORE === "true", feedbackMaxHistory: parseInt(process.env.PET_FEEDBACK_MAX_HISTORY || "200"), stateFile: resolvePath(process.env.PET_STATE_FILE || "~/.claude/pets/claude-pet-state.json"), actionFile: resolvePath(process.env.PET_ACTION_FILE || "/tmp/pet-action.json"), logFile: process.env.LOG_FILE || "/tmp/claude-pet.log", debugMode: process.env.DEBUG_MODE === "true", verboseLogging: process.env.VERBOSE_LOGGING === "true", enableLogging: process.env.ENABLE_LOGGING === "true" }; // src/commands/CommandProcessor.ts var __dirname = "/Users/idol/projects/claude-code-tamagotchi/src/commands"; var VALID_FOODS = { cookie: "\uD83C\uDF6A", pizza: "\uD83C\uDF55", sushi: "\uD83C\uDF63", apple: "\uD83C\uDF4E", carrot: "\uD83E\uDD55", steak: "\uD83E\uDD69", fish: "\uD83D\uDC1F", candy: "\uD83C\uDF6C" }; var VALID_TOYS = { ball: "\uD83C\uDFBE", frisbee: "\uD83E\uDD4F", laser: "\uD83D\uDD34", yarn: "\uD83E\uDDF6", puzzle: "\uD83E\uDDE9" }; class CommandProcessor { static async writeAction(command, parameter) { const action = { command, parameter, timestamp: Date.now() }; try { fs2.writeFileSync(config2.actionFile, JSON.stringify(action, null, 2)); } catch (error) { console.error("Failed to write action:", error); } } static async processCommand(input) { const parts = input.trim().split(/\s+/); const command = parts[0]?.toLowerCase().replace("/", ""); const parameter = parts.slice(1).join(" "); let state = null; try { if (fs2.existsSync(config2.stateFile)) { const stateData = fs2.readFileSync(config2.stateFile, "utf-8"); state = JSON.parse(stateData); } } catch {} switch (command) { case "pet": await this.writeAction("pet"); if (state?.sessionUpdateCount > 100) { return `You pet ${state.name}! They've been coding with you for a while and really appreciate the attention! \uD83D\uDC95`; } return "You pet your companion! \uD83D\uDC95"; case "feed": const food = parameter || "cookie"; if (!VALID_FOODS[food.toLowerCase()]) { return `Sorry, "${food}" is not a valid food. Try: ${Object.keys(VALID_FOODS).join(", ")}`; } const foodEmoji = VALID_FOODS[food.toLowerCase()]; await this.writeAction("feed", food.toLowerCase()); if (state?.sessionUpdateCount > 50 && state?.hunger < 50) { return `Perfect timing! ${state.name} was getting hungry from all this coding. Feeding ${food}! ${foodEmoji}`; } return `Feeding ${food} to your pet! ${foodEmoji}`; case "play": const toy = parameter || "ball"; if (!VALID_TOYS[toy.toLowerCase()]) { return `Sorry, "${toy}" is not a valid toy. Try: ${Object.keys(VALID_TOYS).join(", ")}`; } const toyEmoji = VALID_TOYS[toy.toLowerCase()]; await this.writeAction("play", toy.toLowerCase()); if (state?.sessionUpdateCount > 100) { return `Great idea! ${state.name} needs a break after coding for so long. Playing with ${toy}! ${toyEmoji}`; } return `Playing with ${toy}! ${toyEmoji}`; case "sleep": await this.writeAction("sleep"); if (state) { state.isAsleep = true; state.lastSlept = Date.now(); state.systemMessage = `${state.name} is going to sleep... \uD83D\uDE34`; state.messageTimestamp = Date.now(); fs2.writeFileSync(config2.stateFile, JSON.stringify(state, null, 2)); } if (state?.energy < 30) { return `${state.name} is exhausted and grateful for the rest... \uD83D\uDE34`; } return "Your pet is going to sleep... \uD83D\uDE34"; case "wake": await this.writeAction("wake"); return "Waking up your pet! ☀️"; case "clean": await this.writeAction("clean"); const cleanExec = __require("child_process").execSync; try { cleanExec(`${__dirname}/../../dist/index.js`, { stdio: "ignore" }); } catch {} return "Giving your pet a bath! \uD83D\uDEC1"; case "heal": await this.writeAction("heal"); return "Giving medicine to your pet! \uD83D\uDC8A"; case "pet-name": if (parameter) { await this.writeAction("name", parameter); if (state) { state.name = parameter; state.systemMessage = `I'm ${parameter} now! \uD83C\uDFF7️`; state.messageTimestamp = Date.now(); fs2.writeFileSync(config2.stateFile, JSON.stringify(state, null, 2)); return `Pet renamed to ${parameter}!`; } return `Pet renamed to ${parameter}!`; } return "Usage: /pet-name <name>"; case "pet-trick": if (parameter) { await this.writeAction("trick", parameter); return `Teaching ${parameter} trick!`; } return "Usage: /pet-trick <rollover|speak|dance>"; case "pet-stats": return await this.getStats(); case "pet-reset": return await this.resetPet(); case "pet-help": return this.getHelp(); default: return "Unknown command. Try /pet-help"; } } static async getStats() { try { if (!fs2.existsSync(config2.stateFile)) { return "No pet data found. Pet will be created on next statusline update."; } const stateData = fs2.readFileSync(config2.stateFile, "utf-8"); const state = JSON.parse(stateData); const ageHours = Math.floor(state.age / 60); const ageMinutes = state.age % 60; const sessionMinutes = Math.floor((Date.now() - state.sessionStartTime) / 60000); let activityLevel = "Low"; const recentUpdates = state.recentUpdateTimestamps?.filter((t) => Date.now() - t < 60000).length || 0; if (recentUpdates > 20) activityLevel = "Intense"; else if (recentUpdates > 10) activityLevel = "High"; else if (recentUpdates > 5) activityLevel = "Moderate"; const moodDescription = { normal: "Content", happy: "Happy", debugging: "Focused (debugging)", celebrating: "Celebrating!", tired: "Tired", focused: "Deep Focus", sleeping: "Sleeping" }[state.currentMood] || "Unknown"; const suggestions = []; if (state.hunger < 30) suggestions.push("\uD83C\uDF6A Pet is hungry!"); if (state.energy < 30) suggestions.push("\uD83D\uDE34 Pet needs rest!"); if (state.sessionUpdateCount > 150) suggestions.push("\uD83C\uDFBE Time for a play break!"); if (state.happiness < 50) suggestions.push("\uD83D\uDC95 Pet needs attention!"); if (state.cleanliness < 50) suggestions.push("\uD83D\uDEC1 Bath time!"); return ` \uD83D\uDC3E Pet Statistics \uD83D\uDC3E ━━━━━━━━━━━━━━━━━━ Name: ${state.name} Type: ${state.type} Age: ${ageHours}h ${ageMinutes}m Evolution: Stage ${state.evolutionStage} Mood: ${moodDescription} \uD83D\uDCCA Vital Stats: ❤️ Happiness: ${Math.round(state.happiness)}% \uD83C\uDF56 Hunger: ${Math.round(state.hunger)}% ⚡ Energy: ${Math.round(state.energy)}% \uD83C\uDFE5 Health: ${Math.round(state.health)}% \uD83E\uDDFC Cleanliness: ${Math.round(state.cleanliness)}% \uD83D\uDCBB Coding Session: Session Duration: ${sessionMinutes} minutes Session Updates: ${state.sessionUpdateCount} Activity Level: ${activityLevel} Total Sessions: ${state.sessionsToday || 1} \uD83D\uDCC8 Lifetime Stats: Total Feedings: ${state.totalFeedings} Play Sessions: ${state.totalPlaySessions} Favorite Food: ${state.favoriteFood} Tricks Learned: ${state.tricks.length > 0 ? state.tricks.join(", ") : "None"} ${suggestions.length > 0 ? ` \uD83D\uDCA1 Suggestions: ` + suggestions.join(` `) : ""} `; } catch (error) { return "Failed to read pet stats."; } } static async resetPet() { try { if (fs2.existsSync(config2.stateFile)) { fs2.unlinkSync(config2.stateFile); } return "Pet has been reset. A new pet will be created on next statusline update."; } catch (error) { return "Failed to reset pet."; } } static getHelp() { return `\uD83D\uDC3E Claude Code Pet Commands \uD83D\uDC3E \uD83C\uDF7C CARE: /pet-pet, /pet-feed [food], /pet-play [toy], /pet-clean, /pet-sleep \uD83D\uDCCA INFO: /pet-status, /pet-stats, /pet-help \uD83C\uDFA8 CUSTOM: /pet-name [name] ⚙️ MANAGE: /pet-reset \uD83C\uDF7D️ FOODS: cookie, pizza, sushi, apple, carrot, steak, fish, candy \uD83C\uDFAE TOYS: ball, frisbee, laser, yarn, puzzle ⚠️ NEEDS ATTENTION: \uD83C\uDF56<30% (feed) ⚡<30% (sleep) \uD83E\uDDFC<30% (clean)`; } } // src/commands/cli.ts var command = process.argv[2]; var args = process.argv.slice(3).join(" "); async function main() { if (!command) { console.log("Usage: claude-code-tamagotchi <command> [args]"); console.log('Run "claude-code-tamagotchi help" for available commands'); return; } if (command === "violation-check") { const { violationCheck: violationCheck2 } = await init_violation_check().then(() => exports_violation_check); await violationCheck2(); return; } let slashCommand; if (command === "name" || command === "reset" || command === "stats" || command === "status" || command === "help") { slashCommand = `/pet-${command} ${args}`.trim(); } else { slashCommand = `/${command} ${args}`.trim(); } const response = await CommandProcessor.processCommand(slashCommand); console.log(response); } main().catch(console.error);