claude-code-tamagotchi
Version:
A virtual pet that lives in your Claude Code statusline
1,180 lines (1,155 loc) • 41.7 kB
JavaScript
// @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);