{"_id":"@aituber-onair/manneri","_rev":"7-c428d2627392d70ae219d9d2100b494f","name":"@aituber-onair/manneri","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@aituber-onair/manneri","version":"0.1.0","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.1.0","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"c23459270e0da46d9f27de53477077634859d72c","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.1.0.tgz","fileCount":48,"integrity":"sha512-k7YMcUpC4MJMgoFcK/E4Q5QgakZ0/ON9xqKMU7YX/mLPMCc9bzuqL5nmSScXirxG4P2xPg8zQaNo5PVR1iT2Sw==","signatures":[{"sig":"MEYCIQCZDVmuhT+nhFY3NjhYpf1InisYWQTz077UOs0IO1/qTQIhAKEbn+YdFnfcC7qtpCN+Qx63RFOndJWqGaHc3LwIy8gn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":188528},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"06d126846f7fadfc0f542c6680b43c798484164c","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.19.3","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.1.0_1752237630447_0.3537332867124805","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aituber-onair/manneri","version":"0.2.0","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.2.0","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"21d9f5b1c35742dc345d69bef648b1d230e10c62","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.2.0.tgz","fileCount":48,"integrity":"sha512-6EKA2BbHSA3Flh4bwX/ok801Q/Kbu4qhI5xy4SKJYmsXTjF0v4PAs0hnHXxg248bvkQfBs/gmRfv5MUekbZl0w==","signatures":[{"sig":"MEUCIBU7utCdsZnjyNRLpHpmaUKh98oov//C9mQtqYr9g02/AiEAmm/nsRCFewzfQfzkVu9GUgGtjB8AJ82aYwuQ5dZorhU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":186510},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"6833d4bcc0c106578c6c12c47eac67c7a422271e","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.19.4","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.2.0_1756719642374_0.2325818417099632","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@aituber-onair/manneri","version":"0.3.0","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.3.0","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"96b006d799cc68e601cb3ae6f661f43333093aa0","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.3.0.tgz","fileCount":51,"integrity":"sha512-hE2mMQv1IQ4qppvzAPwBJV6j0gslK4HrwfceZforafRdjK3DnCsLXJFCUzMBAzCLrlmy5gxlWAk1jiJpcYsZyw==","signatures":[{"sig":"MEQCIF1ctwQofPM/ZHxOeskJLRPshQP94EwzrKs7PITyOP7GAiBtwAtCTyxAQSinHMe7UHlVR891NooigEnH9r7Pzs0SBQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":200081},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"e04dcb841ca47cd0bf3ee599a440fbff20afeb22","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.20.2","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.3.0_1775290766261_0.5234928109058188","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@aituber-onair/manneri","version":"0.3.1","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.3.1","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"0014cf219c328b32bec4bd1f84f6f18cfdad619a","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.3.1.tgz","fileCount":51,"integrity":"sha512-KAL+XNIfv0YLhgq6+dl216es1mLfjceAMEDoydlqEgpsSeu9A/tPa1QEknFNcki0iZAc+/n0qnMAGqLdRIMwNQ==","signatures":[{"sig":"MEYCIQCUTHyz3OgUA9AGdcAQ6p7a2wCjBTGWPmGXngiZoTSZsgIhAPpuMQ39U1/tBhEgWXRsaE509EbjomE6td1RwKVhM+BW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":200142},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"d2ae7dfd47e0f575942e4df66ecbd2d3d7f9d511","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.20.2","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.3.1_1775292884927_0.08948773114035058","host":"s3://npm-registry-packages-npm-production"}},"0.3.2":{"name":"@aituber-onair/manneri","version":"0.3.2","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.3.2","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"fd5109d75985e8979302ad3b7b25c83f2da64b19","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.3.2.tgz","fileCount":51,"integrity":"sha512-bKy0wpW5HszlTaPxzJDiBegplgyMVROewo3bA1bwK2y4tGPH78HtegjYGUx/KfmRp0LaDhP2hTHg/MVAS5GAYA==","signatures":[{"sig":"MEUCID7J3RiWim1APzJw9LF+Roy/u6dKbymIg0Ozs2w3rdzFAiEAzEGOtsCRTbBMtvrMZ829LzjlUYGdE1RPIbu5qosEksY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":211593},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"0b5980005599fce044b4827cb2cd5faaf0bc8b33","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","example:browser-basic":"vite --host 127.0.0.1 examples/browser-basic","example:browser-basic:build":"vite build examples/browser-basic"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.20.2","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.21","jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.3.2_1780388740776_0.3105453758426122","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"@aituber-onair/manneri","version":"0.3.3","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"url":"https://github.com/shinshin86","name":"shinshin86"},"license":"MIT","_id":"@aituber-onair/manneri@0.3.3","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"homepage":"https://github.com/shinshin86/aituber-onair#readme","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"dist":{"shasum":"5ce85197a8b1cbb3ad5a5267ed7ef552598d4184","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.3.3.tgz","fileCount":51,"integrity":"sha512-tdPVg/+K0MCnRbBnB1ItbugQ/SeKahyFcomFcQsDrVI9PD4kM58zvPqbUErMXGYsFsVdi0AiGKFOslV+SHKyGA==","signatures":[{"sig":"MEYCIQD8hCLLxqB4ZMvKirsW1FktpbCegyarX4oKFRA33bmhGgIhAJJ13EFApxMPtGzzNKjvRq7tU3JFg7O0dXiD45FVf4pr","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":219974},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","gitHead":"3cfdc8cb9553e938eda5b679fba3be2cd586c1b4","scripts":{"fmt":"biome format . --write","lint":"biome lint .","test":"npm run typecheck && vitest run","build":"tsc --project tsconfig.json","clean":"rm -rf dist","fmt:check":"biome format .","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","example:browser-basic":"vite --host 127.0.0.1 examples/browser-basic","example:browser-basic:build":"vite build examples/browser-basic"},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"repository":{"url":"git+https://github.com/shinshin86/aituber-onair.git","type":"git","directory":"packages/manneri"},"_npmVersion":"10.8.2","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","directories":{},"_nodeVersion":"20.20.2","browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.21","jsdom":"^22.1.0","vitest":"^1.3.1","typescript":"^5.0.0","@types/node":"^18.15.0","@biomejs/biome":"1.9.4","@vitest/coverage-v8":"^1.3.1"},"_npmOperationalInternal":{"tmp":"tmp/manneri_0.3.3_1780468150712_0.3320641186286122","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@aituber-onair/manneri","version":"0.4.0","description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","type":"module","main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc --project tsconfig.json","example:browser-basic":"vite --host 127.0.0.1 examples/browser-basic","example:browser-basic:build":"vite build examples/browser-basic","example:draft-review":"vite --host 127.0.0.1 examples/draft-review-basic","example:draft-review:build":"vite build examples/draft-review-basic","typecheck":"tsc --noEmit","test":"npm run typecheck && vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","fmt":"biome format . --write","fmt:check":"biome format .","lint":"biome lint .","clean":"rm -rf dist","prepublishOnly":"npm run build"},"keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"author":{"name":"shinshin86","url":"https://github.com/shinshin86"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/shinshin86/aituber-onair.git","directory":"packages/manneri"},"devDependencies":{"@biomejs/biome":"1.9.4","@types/node":"^18.15.0","@vitest/coverage-v8":"^1.3.1","jsdom":"^22.1.0","typescript":"^5.0.0","vite":"^5.4.21","vitest":"^1.3.1"},"browserslist":["defaults","not IE 11"],"publishConfig":{"access":"public"},"_id":"@aituber-onair/manneri@0.4.0","gitHead":"8881c91d7a9112029bce69241b21745b9713b89e","bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"homepage":"https://github.com/shinshin86/aituber-onair#readme","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-yrz2YJjCzalm1nujeSkNArLImZjXkaCM45qZ19TZr8IL8bFSDLQkqEKInZUBIoa5D0IXcNbePu5gS2fAUB/CLw==","shasum":"080ce779dfceadafa1fc770961f907d701b3c7d2","tarball":"https://registry.npmjs.org/@aituber-onair/manneri/-/manneri-0.4.0.tgz","fileCount":54,"unpackedSize":233543,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCdAgVWlBKhkyu6fjz4PNk6cY5eu2COljjreergRUbjcwIgMUG5PQCU24ORXjSObSY6ZUGcp+WV5aqH/h0I08//ymw="}]},"_npmUser":{"name":"shinshin86","email":"shinshin86npm@gmail.com"},"directories":{},"maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/manneri_0.4.0_1782956407386_0.9569851869669774"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-11T12:40:30.388Z","modified":"2026-07-02T01:40:07.764Z","0.1.0":"2025-07-11T12:40:30.626Z","0.2.0":"2025-09-01T09:40:42.556Z","0.3.0":"2026-04-04T08:19:26.467Z","0.3.1":"2026-04-04T08:54:45.098Z","0.3.2":"2026-06-02T08:25:40.918Z","0.3.3":"2026-06-03T06:29:10.864Z","0.4.0":"2026-07-02T01:40:07.635Z"},"bugs":{"url":"https://github.com/shinshin86/aituber-onair/issues"},"author":{"name":"shinshin86","url":"https://github.com/shinshin86"},"license":"MIT","homepage":"https://github.com/shinshin86/aituber-onair#readme","keywords":["conversation","ai","chatbot","pattern-detection","aituber","frontend","browser"],"repository":{"type":"git","url":"git+https://github.com/shinshin86/aituber-onair.git","directory":"packages/manneri"},"description":"A lightweight conversation pattern detection library to prevent repetitive AI responses","maintainers":[{"name":"shinshin86","email":"shinshin86npm@gmail.com"}],"readme":"# @aituber-onair/manneri\n\n![AITuber OnAir Manneri - logo](./images/aituber-onair-manneri.png)\n\n**Manneri** is a simple JavaScript library designed to detect repetitive conversation patterns in AI chatbots and provide topic diversification prompts for more engaging conversations.\n\n[日本語版README](README.ja.md) | [English README](README.md)\n\n## Features\n\n- 🔍 **Conversation Similarity Analysis**: Calculate text similarity to detect repetitive patterns\n- 📊 **Pattern Detection**: Identify structural patterns in conversations\n- 🎯 **Keyword Analysis**: Detect overused vocabulary and topic bias\n- 💡 **Automatic Prompt Generation**: Generate appropriate prompts for topic diversification\n- 🌐 **Frontend-Only**: Lightweight operation optimized for browser environments\n- 🌍 **Multi-Language Support**: Built-in support for Japanese and English, with easy extension to any language via custom prompts\n- 🎨 **Customizable Prompts**: Configure intervention messages and recommendations for any language\n- 🇯🇵 **Japanese Language Support**: Proper handling of Japanese text (Hiragana, Katakana, Kanji)\n- 💾 **Flexible Persistence**: Configurable data persistence with support for multiple storage backends\n- 📝 **Draft Review for Agents**: Check a drafted assistant reply before sending and rewrite only when it repeats recent conversation patterns\n\n## Installation\n\n```bash\nnpm install @aituber-onair/manneri\n```\n\n## Basic Usage\n\n```typescript\nimport { ManneriDetector, LocalStoragePersistenceProvider } from '@aituber-onair/manneri';\n\n// Create ManneriDetector with default settings\nconst detector = new ManneriDetector();\n\n// Define message array\nconst messages = [\n  { role: 'user', content: 'Hello' },\n  { role: 'assistant', content: 'Hello! How can I help you today?' },\n  { role: 'user', content: 'Hello' },\n  { role: 'assistant', content: 'Hello! What can I assist you with?' }\n];\n\n// Detect repetitive patterns\nif (detector.detectManneri(messages)) {\n  console.log('Repetitive conversation detected');\n}\n\n// Check if intervention is needed (considering cooldown)\nif (detector.shouldIntervene(messages)) {\n  // Generate topic diversification prompt\n  const prompt = detector.generateDiversificationPrompt(messages);\n  console.log('Suggested prompt:', prompt.content);\n}\n```\n\n## Configuration Options\n\n```typescript\nconst detector = new ManneriDetector({\n  similarityThreshold: 0.75,     // Similarity threshold (0-1)\n  repetitionLimit: 3,            // (deprecated) Exposed for compatibility only\n  lookbackWindow: 10,            // Number of messages to analyze\n  interventionCooldown: 300000,  // Intervention interval (milliseconds)\n  minMessageLength: 10,          // Ignore shorter messages during analysis\n  excludeKeywords: ['yes', 'no'], // Ignore exact keyword-only messages\n  enableTopicTracking: true,     // Enable topic tracking\n  enableKeywordAnalysis: true,   // Enable keyword analysis\n  debugMode: false,              // Debug mode\n  language: 'en',                // Language for prompts ('ja' | 'en' | custom)\n  customPrompts: {               // Custom intervention prompts (optional)\n    en: {\n      intervention: [\n        'Please change the topic and talk about something new.',\n        'Let\\'s explore a different subject.',\n        'How about discussing something else?'\n      ]\n    }\n  }\n}, {\n  // Optional: Configure persistence provider\n  persistenceProvider: new LocalStoragePersistenceProvider({\n    storageKey: 'my_manneri_data',\n    version: '1.0.0'\n  })\n});\n```\n\n`minMessageLength` and `excludeKeywords` are applied before similarity, pattern,\nand topic analysis. `excludeKeywords` matches exact message text after trimming,\ncase normalization, whitespace normalization, and basic punctuation removal.\n`repetitionLimit` is deprecated and is not used by the detection logic.\n\n## Detection Behavior\n\nManneri triggers an intervention when at least one actionable signal crosses its\nthreshold:\n\n- the latest message is too similar to recent messages\n- a content-based conversation pattern repeats enough times\n- topic analysis finds a high-confidence topic bias\n\nRepeated message detection groups similar messages from the same role into one\ncontent-based pattern. Leading bracket metadata such as `[happy]` is ignored for\nthis comparison, so emotion tags do not mask repeated wording.\n\nFor Japanese text, similarity also uses character n-grams to catch short phrase\noverlap such as `次へ進もう` and `うん、次へ進もう`. Short Japanese\nmessages use a lower repeated-message threshold than longer or non-Japanese\nmessages so brief stream comments can still be detected without relying on\nrole-only alternation.\n\nRole-only alternation patterns such as `user-assistant-user-assistant` are kept\nin analysis results for observability, but they do not trigger interventions by\nthemselves. This reduces false positives in normal back-and-forth conversations.\n\nWhen you call `generateDiversificationPrompt()` after an analysis, the prompt\nmetadata reflects the primary signal:\n\n- similarity and repeated content patterns use `pattern_break` with high priority\n- topic bias uses `keyword_shift` with medium priority\n- calls without a prior actionable analysis fall back to `topic_change`\n\n## Draft Review Before Sending\n\nUse `reviewDraft()` when your AI agent or chat workflow already has a candidate\nreply and needs to decide whether to send it as-is. Manneri analyzes the draft as\nthe next assistant message, then returns `shouldRewrite` and an optional\n`suggestion` only when the draft appears repetitive.\n\n> **How to think about it:** Manneri only detects and returns a result. It never\n> sends, stops, or rewrites for you. Both when to run the check and what to do\n> with the result (send as-is, rewrite, or skip) are decided by your agent or\n> app. Unlike `generateDiversificationPrompt()`, which steers the next\n> generation from the conversation so far, `reviewDraft()` evaluates one\n> specific candidate reply right before sending. It is also exposed as the\n> `review_draft_repetition` agent tool so an agent can call the check itself.\n> Detection is deterministic: text similarity and repetition patterns controlled\n> by `similarityThreshold`. It does not call an LLM.\n\nThis is useful for:\n\n- live-stream AI characters that should avoid repeating the same reaction,\n  greeting, or closing phrase\n- chatbots that want a lightweight self-review step before a response reaches\n  the user\n- agent workflows that can regenerate a reply only when the review finds a real\n  repetition signal\n- dashboards or observability tools that need to show why a draft was accepted\n  or sent back for rewriting\n\n```typescript\nimport { ManneriDetector, type Message } from '@aituber-onair/manneri';\n\nconst detector = new ManneriDetector({\n  minMessageLength: 0,\n  similarityThreshold: 0.75\n});\n\nconst messages: Message[] = [\n  { role: 'user', content: 'What should I cook?' },\n  { role: 'assistant', content: 'A quick pasta would be good.' },\n  { role: 'user', content: 'Any other dinner idea?' },\n  { role: 'assistant', content: 'A quick pasta would be good.' },\n  { role: 'user', content: 'One more suggestion?' }\n];\n\nconst review = detector.reviewDraft(\n  messages,\n  'A quick pasta would be good.'\n);\n\nif (review.shouldRewrite) {\n  console.log('Rewrite instruction for the LLM:', review.suggestion?.content);\n} else {\n  console.log('Draft is safe to send.');\n}\n```\n\nThe returned `analysis` is the same shape as `analyzeConversation()`, so you can\ninspect similarity, repeated patterns, topic bias, and the intervention reason.\n`suggestion` is a diversification instruction or prompt to feed to your LLM for\na rewrite. It is not a finished rewritten reply. Your code or agent performs the\nactual rewrite. `suggestion` is omitted when `shouldRewrite` is `false`.\n\nSee `examples/draft-review-basic` for a browser sample that shows the send vs.\nrewrite vs. skip decision, review reason, and rewrite instruction in one flow.\n\n### Agent Tool Definition\n\n`REVIEW_DRAFT_REPETITION_TOOL` is a provider-agnostic JSON Schema tool\ndefinition for `reviewDraft(messages, draft)`. It has no SDK dependency, so you\ncan adapt it to OpenAI tools, MCP-style registries, or your own agent runtime.\n\n```typescript\nimport {\n  MANNERI_AGENT_TOOLS,\n  REVIEW_DRAFT_REPETITION_TOOL\n} from '@aituber-onair/manneri';\n\nconsole.log(REVIEW_DRAFT_REPETITION_TOOL.name);\n// \"review_draft_repetition\"\n\n// Register all Manneri agent tools in your runtime.\nagent.registerTools(MANNERI_AGENT_TOOLS);\n```\n\nThe tool is intended for drafts generated by your application. Treat the\nconversation history and draft text as normal user-visible content, and avoid\nincluding secrets or private system prompts in tool inputs.\n\n## Integration with AITuberOnAirCore\n\n```typescript\nimport { ManneriDetector } from '@aituber-onair/manneri';\n\nconst manneriDetector = new ManneriDetector({\n  similarityThreshold: 0.8,\n  repetitionLimit: 3,\n  interventionCooldown: 300000\n});\n\n// Integration via AITuberOnAirCore event listener\ncore.on('beforeAIRequest', (requestData) => {\n  const chatHistory = core.getChatHistory();\n  \n  if (manneriDetector.shouldIntervene(chatHistory)) {\n    const diversificationPrompt = manneriDetector.generateDiversificationPrompt(chatHistory);\n    \n    // Add topic change instruction as system prompt\n    requestData.messages.unshift({\n      role: 'system',\n      content: diversificationPrompt.content\n    });\n    \n    console.log('Applied diversification prompt:', diversificationPrompt.type);\n  }\n});\n```\n\n## Event Handling\n\n```typescript\n// Similarity calculation event\ndetector.on('similarity_calculated', (data) => {\n  console.log(`Similarity: ${data.score}, Threshold: ${data.threshold}`);\n});\n\n// Pattern detection event\ndetector.on('pattern_detected', (result) => {\n  console.log('Detected patterns:', result.patterns);\n});\n\n// Intervention triggered event\ndetector.on('intervention_triggered', (prompt) => {\n  console.log('Intervention executed:', prompt.content);\n});\n\n// Configuration update event\ndetector.on('config_updated', (newConfig) => {\n  console.log('Configuration updated:', newConfig);\n});\n```\n\n## Detailed Analysis\n\n```typescript\n// Perform detailed conversation analysis\nconst analysis = detector.analyzeConversation(messages);\n\nconsole.log('Analysis result:', {\n  similarity: analysis.similarity,\n  topics: analysis.topics,\n  patterns: analysis.patterns,\n  shouldIntervene: analysis.shouldIntervene,\n  reason: analysis.interventionReason\n});\n\n// Get statistics\nconst stats = detector.getStatistics();\nconsole.log('Statistics:', {\n  totalInterventions: stats.totalInterventions,\n  averageInterval: stats.averageInterventionInterval,\n  thresholds: stats.configuredThresholds,\n  analysis: stats.analysisStats\n});\n```\n\n`analysisStats` includes `totalAnalyses`, `averageAnalysisTime`,\n`cacheHitRate`, and `memoryUsage`. `clearHistory()` resets intervention history\nand analyzer caches. Calling `clearCache()` on `ConversationAnalyzer` clears\ncaches while keeping analysis counters intact.\n\n## Multi-Language Support\n\n### Language Configuration\n\n```typescript\n// Use English prompts\nconst detectorEn = new ManneriDetector({\n  language: 'en'\n});\n\n// Use Japanese prompts (default)\nconst detectorJa = new ManneriDetector({\n  language: 'ja'\n});\n\n// Custom prompts for Spanish\nconst detectorSpanish = new ManneriDetector({\n  language: 'es',\n  customPrompts: {\n    es: {\n      intervention: [\n        'Cambiemos de tema y hablemos de algo nuevo.',\n        'Exploremos un tema diferente.',\n        '¿Qué tal si discutimos otra cosa?'\n      ]\n    }\n  }\n});\n\n// Chinese support\nconst detectorChinese = new ManneriDetector({\n  language: 'zh',\n  customPrompts: {\n    zh: {\n      intervention: [\n        '让我们换个话题，聊些新的内容。',\n        '我们来探讨一个不同的主题。',\n        '要不要讨论别的事情？'\n      ]\n    }\n  }\n});\n```\n\n### Built-in Language Support\n\nManneri comes with built-in prompts for:\n- **Japanese** (`'ja'`) - Default language\n- **English** (`'en'`) - Full prompt coverage\n\n```typescript\nimport { DEFAULT_PROMPTS } from '@aituber-onair/manneri';\n\n// View available built-in languages\nconsole.log(Object.keys(DEFAULT_PROMPTS)); // ['ja', 'en']\n\n// Access specific language prompts\nconst englishPrompts = DEFAULT_PROMPTS.en;\nconsole.log(englishPrompts.intervention);\n```\n\n### Adding Any Language\n\n**Any language can be supported** by providing custom prompts. The library accepts any language code and uses your custom prompts:\n\n```typescript\n// Add support for any language (French example)\nconst detectorFrench = new ManneriDetector({\n  language: 'fr',\n  customPrompts: {\n    fr: {\n      intervention: [\n        'Changeons de sujet et parlons de quelque chose de nouveau.',\n        'Explorons un sujet différent.',\n        'Que diriez-vous de discuter d\\'autre chose?'\n      ]\n    }\n  }\n});\n\n// Or extend existing languages\nimport { overridePrompts, DEFAULT_PROMPTS } from '@aituber-onair/manneri';\n\nconst multilingualPrompts = overridePrompts(DEFAULT_PROMPTS, {\n  zh: {\n    intervention: ['让我们换个话题，聊些新的内容。']\n  },\n  ko: {\n    intervention: ['새로운 주제로 변경해 주세요.']\n  },\n  fr: {\n    intervention: ['Changeons de sujet.']\n  }\n});\n```\n\n### Language-Agnostic Design\n\nThe library is designed to work with **any language** without code changes:\n\n```typescript\n// Easy language switching\nconst createDetectorForLanguage = (lang: string, prompts: any) => {\n  return new ManneriDetector({\n    language: lang,\n    customPrompts: { [lang]: prompts }\n  });\n};\n\n// Support multiple languages in the same application\nconst detectors = {\n  english: createDetectorForLanguage('en', englishPrompts),\n  chinese: createDetectorForLanguage('zh', chinesePrompts),\n  korean: createDetectorForLanguage('ko', koreanPrompts),\n  arabic: createDetectorForLanguage('ar', arabicPrompts),\n  // Add any language\n};\n\n// Dynamic language detection\nconst getUserLanguage = () => navigator.language.split('-')[0];\nconst userDetector = detectors[getUserLanguage()] || detectors.english;\n```\n\n### Prompt Utilities\n\n```typescript\nimport { getPromptTemplate, overridePrompts } from '@aituber-onair/manneri';\n\n// Get intervention prompt for any language\nconst interventionPrompt = getPromptTemplate(\n  myCustomPrompts, \n  'zh'\n);\n\n// Override default prompts with custom ones\nconst globalPrompts = overridePrompts(DEFAULT_PROMPTS, {\n  zh: { intervention: ['换个话题吧'] },\n  ko: { intervention: ['주제를 바꿔주세요'] },\n  es: { intervention: ['Cambiemos de tema'] }\n});\n```\n\n## Individual Feature Usage\n\n### Similarity Analysis\n\n```typescript\nimport { SimilarityAnalyzer } from '@aituber-onair/manneri';\n\nconst analyzer = new SimilarityAnalyzer();\nconst similarity = analyzer.calculateSimilarity('Hello', 'Hello, how are you?');\nconsole.log('Similarity:', similarity); // 0.0 - 1.0\n```\n\n### Keyword Extraction\n\n```typescript\nimport { KeywordExtractor } from '@aituber-onair/manneri';\n\nconst extractor = new KeywordExtractor();\nconst keywords = extractor.extractKeywordsFromMessages(messages);\nconsole.log('Keywords:', keywords);\n```\n\n### Pattern Detection\n\n```typescript\nimport { PatternDetector } from '@aituber-onair/manneri';\n\nconst detector = new PatternDetector();\nconst result = detector.detectPatterns(messages);\nconsole.log('Patterns:', result.patterns);\nconsole.log('Severity:', result.severity);\nconsole.log('Confidence:', result.confidence);\n```\n\n## Data Persistence\n\nManneri provides flexible persistence through configurable providers. You have full control over when and how data is saved.\n\n### Browser Environment (LocalStorage)\n\n```typescript\nimport { ManneriDetector, LocalStoragePersistenceProvider } from '@aituber-onair/manneri';\n\n// Configure with LocalStorage persistence\nconst detector = new ManneriDetector({\n  // ... configuration options\n}, {\n  persistenceProvider: new LocalStoragePersistenceProvider({\n    storageKey: 'manneri_data',  // Custom storage key\n    version: '1.0.0'             // Data version\n  })\n});\n\n// Manual persistence control\nawait detector.save();    // Save current state\nawait detector.load();    // Load saved state\nawait detector.cleanup(); // Clean up old data\n\n// Check if persistence is available\nif (detector.hasPersistenceProvider()) {\n  console.log('Persistence is configured');\n  \n  // Get storage info\n  const info = detector.getPersistenceInfo();\n  console.log('Storage info:', info);\n}\n\n// Event handling for persistence operations\ndetector.on('save_success', ({ timestamp }) => {\n  console.log('Data saved successfully at', new Date(timestamp));\n});\n\ndetector.on('save_error', ({ error }) => {\n  console.error('Failed to save data:', error);\n});\n\ndetector.on('load_success', ({ data, timestamp }) => {\n  console.log('Data loaded successfully:', data);\n});\n\ndetector.on('cleanup_completed', ({ removedItems, timestamp }) => {\n  console.log(`Cleaned up ${removedItems} old items`);\n});\n```\n\n### Custom Persistence Provider\n\nFor Node.js, Deno, or custom storage solutions, implement the `PersistenceProvider` interface:\n\n```typescript\nimport type { PersistenceProvider, StorageData } from '@aituber-onair/manneri';\n\n// Example: Database persistence provider\nclass DatabasePersistenceProvider implements PersistenceProvider {\n  constructor(private dbConnection: any) {}\n\n  async save(data: StorageData): Promise<boolean> {\n    try {\n      await this.dbConnection.query(\n        'INSERT OR REPLACE INTO manneri_data (id, data) VALUES (?, ?)',\n        [1, JSON.stringify(data)]\n      );\n      return true;\n    } catch (error) {\n      console.error('Database save failed:', error);\n      return false;\n    }\n  }\n\n  async load(): Promise<StorageData | null> {\n    try {\n      const result = await this.dbConnection.query(\n        'SELECT data FROM manneri_data WHERE id = ?',\n        [1]\n      );\n      return result.length > 0 ? JSON.parse(result[0].data) : null;\n    } catch (error) {\n      console.error('Database load failed:', error);\n      return null;\n    }\n  }\n\n  async clear(): Promise<boolean> {\n    try {\n      await this.dbConnection.query('DELETE FROM manneri_data WHERE id = ?', [1]);\n      return true;\n    } catch (error) {\n      console.error('Database clear failed:', error);\n      return false;\n    }\n  }\n\n  async cleanup(maxAge: number): Promise<number> {\n    // Implement cleanup logic for your storage\n    // Return number of items removed\n    return 0;\n  }\n}\n\n// Use custom persistence provider\nconst detector = new ManneriDetector({\n  // ... configuration\n}, {\n  persistenceProvider: new DatabasePersistenceProvider(dbConnection)\n});\n```\n\n### Manual Data Management (No Persistence)\n\n```typescript\n// Use without persistence provider\nconst detector = new ManneriDetector();\n\n// Manual export/import for custom storage\nconst data = detector.exportData();\n// Store data however you want (file, database, etc.)\nawait myCustomStorage.save(data);\n\n// Restore data\nconst restoredData = await myCustomStorage.load();\ndetector.importData(restoredData);\n\n// Clear history\ndetector.clearHistory();\n```\n\n### Environment-Specific Examples\n\n```typescript\n// Browser-only with LocalStorage\nconst browserDetector = new ManneriDetector({}, {\n  persistenceProvider: new LocalStoragePersistenceProvider()\n});\n\n// Node.js with file storage\nclass FilePersistenceProvider implements PersistenceProvider {\n  constructor(private filePath: string) {}\n  \n  save(data: StorageData): boolean {\n    try {\n      fs.writeFileSync(this.filePath, JSON.stringify(data));\n      return true;\n    } catch {\n      return false;\n    }\n  }\n  \n  load(): StorageData | null {\n    try {\n      const data = fs.readFileSync(this.filePath, 'utf8');\n      return JSON.parse(data);\n    } catch {\n      return null;\n    }\n  }\n  \n  clear(): boolean {\n    try {\n      fs.unlinkSync(this.filePath);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\nconst nodeDetector = new ManneriDetector({}, {\n  persistenceProvider: new FilePersistenceProvider('./manneri-data.json')\n});\n\n// Deno with Deno.KV\nclass DenoKvPersistenceProvider implements PersistenceProvider {\n  constructor(private kv: Deno.Kv) {}\n  \n  async save(data: StorageData): Promise<boolean> {\n    try {\n      await this.kv.set(['manneri', 'data'], data);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n  \n  async load(): Promise<StorageData | null> {\n    try {\n      const result = await this.kv.get(['manneri', 'data']);\n      return result.value as StorageData | null;\n    } catch {\n      return null;\n    }\n  }\n  \n  async clear(): Promise<boolean> {\n    try {\n      await this.kv.delete(['manneri', 'data']);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\nconst kv = await Deno.openKv();\nconst denoDetector = new ManneriDetector({}, {\n  persistenceProvider: new DenoKvPersistenceProvider(kv)\n});\n```\n\n## TypeScript Type Definitions\n\n```typescript\nimport type { \n  Message,\n  ManneriConfig,\n  AnalysisResult,\n  DiversificationPrompt,\n  LocalizedPrompts,\n  PromptTemplates,\n  SupportedLanguage,\n  PersistenceProvider,\n  PersistenceConfig,\n  StorageData\n} from '@aituber-onair/manneri';\n```\n\n## Browser Support\n\n- Chrome 90+\n- Firefox 88+\n- Safari 14+\n- Edge 90+\n\n## Performance\n\n- Lightweight: < 50KB gzipped\n- Fast: Real-time analysis < 100ms\n- Memory efficient: Automatic cache cleanup\n\n## License\n\nMIT License\n\n## Contributing\n\nPull requests and issues are welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## Support\n\n- GitHub Issues: https://github.com/shinshin86/aituber-onair/issues\n- Documentation: https://github.com/shinshin86/aituber-onair/tree/main/packages/manneri\n\n## Related Projects\n\n- [AITuber OnAir](https://github.com/shinshin86/aituber-onair) - Main project\n- [@aituber-onair/core](https://github.com/shinshin86/aituber-onair/tree/main/packages/core) - Core library\n","readmeFilename":"README.md"}