Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ Interacts with Gemini, AI Studio, ChatGPT, and Claude UIs. Injects chat, capture
- [`AIStudioProvider`](extension/providers/aistudio.js)
- [`ChatGptProvider`](extension/providers/chatgpt.js)
- [`ClaudeProvider`](extension/providers/claude.js)
- [`KimiK2Provider`](extension/providers/kimi_k2.js)

**Supported Chat Interfaces:**
- Gemini (`gemini.google.com`)
- AI Studio (`aistudio.google.com`)
- ChatGPT (`chatgpt.com`)
- Claude (`claude.ai`)
- Kimi K2 (`k2.kimi.ai`)

ChatGPT is a trademark of OpenAI. Gemini and AI Studio are trademarks of Google. Claude is a trademark of Anthropic. This project is not affiliated with, endorsed by, or sponsored by OpenAI, Google, or Anthropic.

Expand Down
34 changes: 34 additions & 0 deletions docs/provider-kimi-k2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Kimi K2 Provider Architecture

This document describes the `KimiK2Provider` used by the extension to automate the Kimi K2 chat interface.

---

## 🧩 Overview

`KimiK2Provider` is modeled after the existing AI Studio and ChatGPT providers. It sends messages and captures responses from `k2.kimi.ai` using DOM based monitoring by default with an optional debugger fallback.

---

## ⚙️ Configurable Options

```js
this.captureMethod = 'dom'; // or 'debugger'
this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*';
this.includeThinkingInMessage = false;
```

---

## 📌 DOM Selectors

- Input field: `textarea, div[contenteditable="true"]`
- Send button: `button[type="submit"], button.send-btn`
- Response blocks: `.message.ai, .chat-message.ai`
- Typing indicator: `.typing, .loading`

---

## ✅ Summary

The provider offers a lightweight integration with Kimi K2 using the same structure as the other providers. It can capture responses via DOM observation or via Chrome debugger when configured.
2 changes: 1 addition & 1 deletion extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ let lastSuccessfullyProcessedMessageText = null; // Text of the last message suc
const pendingRequestDetails = new Map(); // Stores { text: string } for active requests, keyed by requestId

// Supported domains for chat interfaces
const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai'];
const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai', 'k2.kimi.ai'];

// ===== DEBUGGER RELATED GLOBALS =====
const BG_LOG_PREFIX = '[BG DEBUGGER]';
Expand Down
10 changes: 10 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"*://*.chatgpt.com/*",
"*://*.aistudio.com/*",
"*://*.claude.ai/*",
"*://*.k2.kimi.ai/*",
"ws://localhost:*/"
],
"background": {
Expand Down Expand Up @@ -58,6 +59,15 @@
"content.js"
],
"run_at": "document_idle"
},
{
"matches": ["*://k2.kimi.ai/*"],
"js": [
"providers/provider-utils.js",
"providers/kimi_k2.js",
"content.js"
],
"run_at": "document_idle"
}
],
"action": {
Expand Down
5 changes: 4 additions & 1 deletion extension/providers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const providers = {
'gemini': window.geminiProvider,
'aistudio': window.aiStudioProvider,
'chatgpt': window.chatgptProvider,
'claude': window.claudeProvider
'claude': window.claudeProvider,
'kimi_k2': window.kimiK2Provider
};

// Get a provider by ID
Expand All @@ -40,6 +41,8 @@ function detectProvider(url) {
return providers.chatgpt;
} else if (url.includes('claude.ai')) {
return providers.claude;
} else if (url.includes('k2.kimi.ai')) {
return providers.kimi_k2;
}

// Default to aistudio if we can't detect
Expand Down
129 changes: 129 additions & 0 deletions extension/providers/kimi_k2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Chat Relay: Relay for AI Chat Interfaces
* Copyright (C) 2025 Jamison Moore
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
// AI Chat Relay - Kimi K2 Provider

class KimiK2Provider {
constructor() {
this.name = 'KimiK2Provider';
this.supportedDomains = ['k2.kimi.ai'];

// --- START OF CONFIGURABLE PROPERTIES ---
this.captureMethod = 'dom'; // DOM capture by default
this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*'; // Placeholder
this.includeThinkingInMessage = false;
// --- END OF CONFIGURABLE PROPERTIES ---

this.inputSelector = 'textarea, div[contenteditable="true"]';
this.sendButtonSelector = 'button[type="submit"], button.send-btn';
this.responseSelector = '.message.ai, .chat-message.ai';
this.thinkingIndicatorSelector = '.typing, .loading';

this.lastSentMessage = '';
this.pendingResponseCallbacks = new Map();
this.requestAccumulators = new Map();
}

async sendChatMessage(text) {
console.log(`[${this.name}] sendChatMessage called:`, text);
const inputElement = document.querySelector(this.inputSelector);
const sendButton = document.querySelector(this.sendButtonSelector);
if (!inputElement || !sendButton) {
console.error(`[${this.name}] Missing input (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
return false;
}

this.lastSentMessage = text;

if (inputElement.tagName.toLowerCase() === 'div' && inputElement.contentEditable === 'true') {
inputElement.focus();
inputElement.innerHTML = '';
inputElement.textContent = text;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
} else {
inputElement.value = text;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.focus();
}
await new Promise(r => setTimeout(r, 300));

if (!sendButton.disabled && sendButton.getAttribute('aria-disabled') !== 'true') {
sendButton.click();
return true;
}

console.warn(`[${this.name}] Send button disabled.`);
return false;
}

captureResponse(element) {
if (!element) return { found: false, text: '' };
let text = element.textContent.trim();
if (!text || text === this.lastSentMessage) return { found: false, text: '' };
return { found: true, text };
}

initiateResponseCapture(requestId, callback) {
this.pendingResponseCallbacks.set(requestId, callback);
if (this.captureMethod === 'dom') {
console.log(`[${this.name}] DOM capture active for ${requestId}`);
}
}

handleDebuggerData(requestId, rawData, isFinal) {
const cb = this.pendingResponseCallbacks.get(requestId);
if (!cb) return;

const parsed = this.parseDebuggerResponse(rawData);
if (parsed.text || isFinal) {
cb(requestId, parsed.text, isFinal);
}
if (isFinal) {
this.pendingResponseCallbacks.delete(requestId);
}
}

parseDebuggerResponse(raw) {
if (!raw) return { text: '', isFinalResponse: false };
if (raw.includes('[DONE]')) {
const clean = raw.replace('[DONE]', '').trim();
return { text: clean, isFinalResponse: true };
}
return { text: raw.trim(), isFinalResponse: false };
}

getStreamingApiPatterns() {
if (this.captureMethod === 'debugger' && this.debuggerUrlPattern) {
return [{ urlPattern: this.debuggerUrlPattern, requestStage: 'Response' }];
}
return [];
}

shouldSkipResponseMonitoring() {
return this.captureMethod === 'debugger';
}
}

(function() {
if (window.providerUtils) {
const providerInstance = new KimiK2Provider();
window.providerUtils.registerProvider(providerInstance.name, providerInstance.supportedDomains, providerInstance);
} else {
console.error('ProviderUtils not found. KimiK2Provider cannot be registered.');
}
})();